diff --git a/.gitattributes b/.gitattributes index d1d55f96e5..b71e748ea7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,9 @@ -.gitattributes export-ignore -.gitignore export-ignore -*.texy linguist-language=Text +.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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..803c0972ef --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,347 @@ +# 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 33 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 (33 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) +- `ai-access/` - AI Access, one PHP interface for OpenAI, Claude, Gemini, DeepSeek and Grok +- `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) + +**Others** (not part of Nette, only hosted on a Nette subdomain): +- `texy/` - Texy markup language processor (https://texy.nette.org) +- `dibi/` - Dibi database abstraction layer (https://dibi.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 + +## 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-access/cs/@home.texy b/ai-access/cs/@home.texy new file mode 100644 index 0000000000..15bd83b881 --- /dev/null +++ b/ai-access/cs/@home.texy @@ -0,0 +1,162 @@ +AI Access: jedno PHP rozhraní pro OpenAI, Claude, Gemini, DeepSeek a Grok +************************************************************************* + +
+ +AI Access je PHP knihovna, která sjednocuje práci s jazykovými modely. Místo pěti různých API a pěti různých formátů odpovědí píšeš jeden kód, který mluví s ChatGPT od OpenAI, s Claude od Anthropicu, s Gemini od Googlu, s DeepSeekem i s Grokem od xAI. Přepnutí mezi nimi je změna jediného řádku. + +Zvládá celý pracovní postup: konverzaci, streamování odpovědí, volání nástrojů (function calling), strukturovaný výstup podle JSON schématu, obrázky a dokumenty na vstupu, generování obrázků, embeddingy pro vyhledávání i dávkové zpracování za poloviční cenu. + +Nemá žádné závislosti. Jen čisté PHP 8.3 a curl, žádné SDK od výrobce a žádné konflikty verzí ve tvém projektu. + +
+ + +K čemu je jazykový model v aplikaci dobrý +========================================= + +Jestli jsi žádné AI API zatím nevolal, princip je jednodušší, než se zvenčí zdá. Pošleš text a model pošle text zpátky. Všechno ostatní jsou nadstavby nad tímhle jedním pohybem. + +V praxi z toho vyroste překvapivě široká škála úloh a stojí za to je znát dřív, než se pustíš do kódu, protože podle toho poznáš, kterou část dokumentace vlastně potřebuješ: + +- **Psaní a přepisování textu.** Shrnutí článku, návrh odpovědi na e-mail, překlad, korektura. Nejběžnější a nejjednodušší případ, stačí ti [obyčejná konverzace |chat]. +- **Klasifikace a rozhodování.** Je tahle registrace spam? Do které kategorie patří tenhle dotaz? Model odpoví jedním slovem a ty se podle toho zachováš. +- **Vytahování dat z nestrukturovaného textu.** Z faktury, životopisu nebo e-mailu potřebuješ pole, která umíš uložit do databáze. Na to je [strukturovaný výstup |structured-output], kde modelu předepíšeš JSON schéma a on ho dodrží. +- **Odpovídání nad vlastními daty.** Model o tvé dokumentaci nic neví, ale když mu k dotazu přiložíš relevantní úryvky, odpoví přesně. Najít ty správné úryvky je práce pro [embeddingy |embeddings], což je právě to vyhledávání podle významu, kterému se říká RAG. +- **Práce s obrázky a dokumenty.** Popiš, co je na fotce, přečti údaje z účtenky, shrň přiložené PDF. Viz [obrázky a dokumenty na vstupu |multimodal]. +- **Akce, ne jen text.** Model může požádat o zavolání tvojí funkce, dostat výsledek a pokračovat. Tak vzniká asistent, který se opravdu podívá do tvé databáze, místo aby si odpověď vymyslel. Viz [volání nástrojů |tools]. +- **Hromadné zpracování.** Když nepotřebuješ odpověď hned, [dávkové zpracování |batch] ti dá tytéž modely za polovinu ceny. + +Co AI Access naopak není: není to agentní framework, nesnaží se za tebe vymýšlet prompty ani si nedrží konverzace v databázi. Je to vrstva, která mluví s API providerů, a končí přesně tam, kde začínají rozhodnutí tvojí aplikace. + + +Instalace +========= + +```shell +composer require ai-access/ai-access +``` + +Vyžaduje PHP 8.3 nebo novější a rozšíření curl, json a fileinfo, která bývají všude. + + +První zpráva +============ + +Potřebuješ klíč od providera, kterého chceš použít. Vydávají je ve svých konzolích [OpenAI |https://platform.openai.com/api-keys], [Anthropic |https://console.anthropic.com/settings/keys], [Google |https://aistudio.google.com/app/apikey], [DeepSeek |https://platform.deepseek.com/api_keys] a [xAI |https://console.x.ai/team/default/api-keys]. + +```php +$client = new AIAccess\Provider\OpenAI\Client($apiKey); + +$response = $client->createChat('gpt-5.6-luna') + ->sendMessage('Napiš haiku o PHP.'); + +echo $response->getText(); +``` + +To je celé. `createChat()` otevře konverzaci nad zvoleným modelem, `sendMessage()` pošle zprávu a vrátí odpověď. + +Jméno modelu je obyčejný řetězec, ne konstanta ani výčtový typ. Zní to jako maličkost, ale znamená to, že nový model funguje v den, kdy ho provider vydá, a nemusíš čekat na aktualizaci knihovny. Ověřit, že model pořád existuje, umí [seznam modelů |providers#Jaké modely provider právě nabízí]. + + +Přepnutí providera je jeden řádek +================================= + +Tohle je hlavní slib knihovny, tak ať je vidět hned. Když klient zná i model, mění se opravdu jen ten jeden řádek: + +```php +$client = new AIAccess\Provider\Claude\Client($apiKey, chatModel: 'claude-sonnet-5'); +$client = new AIAccess\Provider\Gemini\Client($apiKey, chatModel: 'gemini-3.5-flash-lite'); +$client = new AIAccess\Provider\DeepSeek\Client($apiKey, chatModel: 'deepseek-v4-flash'); +$client = new AIAccess\Provider\Grok\Client($apiKey, chatModel: 'grok-4.3'); + +// zbytek kódu je pro všechny stejný +$chat = $client->createChat(); +``` + +[Jméno modelu patří ke klíči |providers#Výchozí modely], protože obojí je vlastní jednomu provideru. V reálné aplikaci si klienta zaregistruješ do [DI kontejneru |dependency-injection:] a přepnutí providera je pak změna v konfiguraci, ne v kódu. + + +Pět API, která se neshodnou na ničem +==================================== + +Když si vlastní obal nad providery napíšeš sám, u prvních dvou to vypadá na pár hodin práce. Problém začne u třetího, protože každý z nich má jinou představu o tom, jak vypadá rozhovor s modelem. Tady je malý výběr toho, v čem se liší: + +| v čem se liší | Claude | OpenAI | Gemini | Grok a DeepSeek | +|----------------------------|----------------------|------------------------------------|---------------------------|--------------------| +| endpoint | `v1/messages` | `v1/responses` | `:generateContent` | `chat/completions` | +| autentizace | hlavička `x-api-key` | `Bearer` | hlavička `x-goog-api-key` | `Bearer` | +| tvar požadavku | `messages[]` | `input[]` a `instructions` | `contents[].parts[]` | `messages[]` | +| jak se jmenuje role modelu | `assistant` | `assistant` | **`model`** | `assistant` | +| klíče spotřeby tokenů | `input_tokens` | totéž | `promptTokenCount` | `prompt_tokens` | +| kde je důvod ukončení | `stop_reason` | `status`, pak `incomplete_details` | `finishReason` | `finish_reason` | + +Poslední řádek má háček, který stojí za vyslovení nahlas: Gemini v `finishReason` **nikdy** neohlásí, že model chce zavolat nástroj. Zůstane tam `STOP` a samotné volání najdeš až mezi částmi odpovědi. Kdo to neví, napíše kód, který tiše ignoruje polovinu toho, co model řekl, a nedozví se o tom. + +Takových drobností jsou desítky a žádná z nich není zajímavá práce. AI Access je má vyřešené a otestované proti skutečným odpovědím API, ne proti vymyšlenému JSONu. + +Zároveň ale nepředstírá, že rozdíly neexistují. Sjednocuje to, co mají provideři opravdu společné, a kde se liší, dá ti to najevo typem nebo výjimkou hned při psaní kódu, ne až chybou z produkce. + + +Co knihovna umí +=============== + +| Schopnost | OpenAI | Claude | Gemini | DeepSeek | Grok | Generický klient | +|-------------------------------------------|--------|--------|--------|----------|------|------------------| +| [Konverzace |chat] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Reasoning effort |options] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Volání nástrojů |tools] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Streamování |streaming] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Obrázky na vstupu |multimodal] | ✅ | ✅ | ✅ | ➖ | ✅ | ✅ | +| [Dokumenty na vstupu |multimodal] | ✅ | ✅ | ✅ | ➖ | ➖ | ➖ | +| [Strukturovaný výstup |structured-output] | ✅ | ✅ | ✅ | ➖ | ✅ | ✅ | +| [Generování obrázků |images] | ✅ | ➖ | ✅ | ➖ | ✅ | ➖ | +| [Dávkové zpracování |batch] | ✅ | ✅ | ✅ | ➖ | ➖ | ➖ | +| [Embeddingy |embeddings] | ✅ | ➖ | ✅ | ➖ | ➖ | ➖ | +| [Seznam modelů |providers] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | + +Kde je minus, tam buď provider takové API nemá, nebo ho knihovna zatím nezabaluje. + +Poslední sloupec je generický klient pro cokoli, co mluví dialektem `chat/completions`: běžící Ollamu na tvém notebooku, Mistral, OpenRouter, Together, vLLM nebo Azure. Značka u něj znamená něco jiného než u ostatních, totiž co knihovna umí poslat; jestli to opravdu funguje, rozhoduje endpoint a model, na který ho namíříš. Podrobnosti najdeš u [providerů |providers]. + + +Navržená, ne nabalená +===================== + +Nastavení specifická pro providera jsou pojmenované argumenty, ne klíče ve sdíleném poli. Rozdíl poznáš hned při psaní: IDE ti u každého providera nabídne přesně to, co daný provider umí, místo aby pole tiše spolklo klíč, který nikam nedojde. K tomu striktní typy všude a readonly hodnotové objekty. + +Hierarchie výjimek je postavená na jediné otázce, která v produkci opravdu dává smysl, totiž jestli má cenu volání zopakovat: + +```php +try { + $response = $chat->sendMessage('...'); + +} catch (AIAccess\ApiException $e) { + // provider řekl ne; $e->getCode() nese HTTP status + if ($e->getCode() === 429) { + // překročený limit, zkus to za chvíli + } + +} catch (AIAccess\CommunicationException $e) { + // výpadek sítě nebo nečitelná odpověď, opakování může pomoct +} +``` + +`LogicException` schválně stojí mimo tenhle strom, protože chyba ve tvém vlastním kódu není nic, co by měla produkce odchytávat a přecházet. Celou hierarchii rozebírá kapitola o [ošetření chyb |errors]. + +Opakování mimochodem nemusíš psát ručně. Knihovna má [dekorátory HTTP vrstvy |http], které umí opakovat po limitech a výpadcích, logovat každý požadavek nebo odpovědi během vývoje cachovat, aby tě opětovné spouštění skriptu nestálo peníze. + + +Kam dál +======= + +- [Začínáme |getting-started] - klíče, první volání a co dělat, když něco nesedí +- [Konverzace |chat] - historie, systémová instrukce, čtení odpovědi a spotřeby tokenů +- [Nastavení a reasoning effort |options] - kolik přemýšlení si od modelu vyžádáš +- [Streamování |streaming] - odpověď čti, zatímco ji model teprve píše +- [Volání nástrojů |tools] - model se zeptá, tvůj kód odpoví, smyčka se uzavře sama +- [Strukturovaný výstup |structured-output] - JSON schéma místo proseb v promptu +- [Provideři |providers] - co který umí, čím se liší a jak zapojit Ollamu nebo OpenRouter + +.[note] +A když si při psaní kódu necháváš pomáhat od AI agenta, mrkni na [Nette AI |ai:]. Najdeš tam plugin pro Claude Code, který agenta naučí Nette, a MCP Inspector, díky kterému se agent podívá přímo do tvojí běžící aplikace. diff --git a/ai-access/cs/@left-menu.texy b/ai-access/cs/@left-menu.texy new file mode 100644 index 0000000000..5bb6403b29 --- /dev/null +++ b/ai-access/cs/@left-menu.texy @@ -0,0 +1,14 @@ +- [Přehled |@home] +- [Začínáme |getting-started] +- [Konverzace |chat] +- [Nastavení a reasoning effort |options] +- [Streamování |streaming] +- [Volání nástrojů |tools] +- [Strukturovaný výstup |structured-output] +- [Obrázky a dokumenty |multimodal] +- [Generování obrázků |images] +- [Embeddingy |embeddings] +- [Dávkové zpracování |batch] +- [Ošetření chyb |errors] +- [HTTP vrstva |http] +- [Provideři a modely |providers] diff --git a/ai-access/cs/@meta.texy b/ai-access/cs/@meta.texy new file mode 100644 index 0000000000..b95080c3cc --- /dev/null +++ b/ai-access/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: AI Access}} diff --git a/ai-access/cs/batch.texy b/ai-access/cs/batch.texy new file mode 100644 index 0000000000..1ad371267a --- /dev/null +++ b/ai-access/cs/batch.texy @@ -0,0 +1,184 @@ +Dávkové zpracování (batch) +************************** + +.[perex] +Když nepotřebuješ odpověď hned, ušetříš zhruba polovinu peněz. Provideři tomu říkají dávkové zpracování, anglicky batch: pošleš jim spoustu požadavků najednou a výsledky si vyzvedneš později. Ukážeme si, jak takovou dávku sestavit a odeslat, jak si po čase vyzvednout výsledky a co si ohlídat, než to pustíš do provozu. + + +K čemu to je +============ + +Představ si, že máš pět tisíc produktů a ke každému chceš od modelu krátký popis. Když je pošleš jeden po druhém, platíš plnou cenu a čekáš, než se pět tisíc dotazů a odpovědí vystřídá. + +Provideři na tenhle případ mají zvláštní režim, kterému se říká **dávkové zpracování**. Předáš jim celý balík požadavků najednou, oni si ho zpracují, až budou mít volnou kapacitu, a ty si výsledky vyzvedneš později. Za to, že nespěcháš, zaplatíš **zhruba polovinu**. + +Nehodí se to všude. Odpověď nepřijde hned a v krajním případě může trvat i hodiny, takže dávkou nikdy neobsloužíš uživatele, který kouká do obrazovky. Zato je ideální na věci, které běží na pozadí: hromadné překlady, klasifikaci fronty, generování popisků, předzpracování dat na noc. + + +Dávka konverzací +================ + +Postup má tři kroky: nejdřív si vytvoříš prázdnou dávku, pak do ní jeden po druhém přidáváš úkoly a nakonec ji celou odešleš. Každý úkol je samostatná konverzace a dostane vlastní identifikátor, podle kterého ho pak spáruješ s odpovědí: + +```php +use AIAccess\Chat\Role; + +$batch = $client->createBatch(); + +foreach ($produkty as $produkt) { + $chat = $batch->addChat('produkt-' . $produkt->id, 'gpt-5.6-luna'); + $chat->setSystemInstruction('Piš krátké popisy produktů, nejvýš dvě věty.'); + $chat->addMessage($produkt->nazev . ': ' . $produkt->parametry, Role::User); +} + +$response = $batch->submit(); +echo $response->getId(); +``` + +`addChat()` vrací obyčejný objekt konverzace, takže s ním pracuješ přesně tak, jak [už umíš |chat]: nastavíš systémovou instrukci, přidáš zprávy, můžeš zapnout [strukturovaný výstup |structured-output]. Jediný rozdíl je, že místo `sendMessage()` na konci zavoláš `submit()` nad celou dávkou. + +**To identifikační číslo si ulož.** Bez něj se k výsledkům nedostaneš a provider ti dávku znovu nepošle. + + +Vyzvednutí výsledků +=================== + +Výsledky si vyzvedneš kdykoli později, klidně z úplně jiného skriptu: + +```php +use AIAccess\Batch\Status; + +$batch = $client->retrieveBatch($batchId); + +if ($batch->getStatus() === Status::InProgress) { + echo 'Ještě se pracuje, stav: ', $batch->getStatus()->name; + return; +} + +foreach ($batch->getResults() as $customId => $result) { + echo $customId, ': ', $result->message?->getText() ?? "selhalo, $result->error", "\n"; +} +``` + +Pod týmiž klíči, které jsi zadal při vkládání, dostáváš objekty `AIAccess\Batch\Result`. Každý nese **buď odpověď, nebo důvod, proč ten jeden požadavek selhal**: když se nepovede jeden, nespadne kvůli němu celá dávka, jen dorazí s vyplněným `$result->error` místo `$result->message`. + +Ta odpověď je obyčejný `AIAccess\Chat\Message`, přesně totéž, co nese [živá konverzace |chat]. Text z ní vytáhneš metodou `getText()`, obrázky metodou `getMedia()` a všechno ostatní, třeba uvažování modelu nebo volání nástrojů, najdeš mezi částmi zprávy v `getParts()`. + +Podstatné je, **jak** ty výsledky chodí: čtou se po jedné položce, jak přitékají ze sítě, ne že by se nejdřív celé stáhly a pak ti podaly. Dávka libovolné velikosti tak stojí paměť jedné odpovědi, což je rozdíl mezi "funguje to" a "stovka obrázků ti sežere gigabajt". Plyne z toho několik věcí, které stojí za zapamatování: + +- **Než začneš iterovat, neodešle se nic.** Sestavit si `getResults()` do proměnné je zadarmo. +- **Když smyčku opustíš přes `break`, přenos se ukončí** a zbytek souboru se nestahuje ani neplatí. +- **Nic se nepamatuje.** Druhé projití stahuje znovu; kdo chce data dvakrát, ať si je uloží. +- Chceš-li přece jen všechno naráz jako pole, je to `iterator_to_array($batch->getResults())`. Je to tvoje rozhodnutí a tvoje paměť. + + +Dávka obrázků +============= + +Obrázky se dávkují stejným způsobem a ze stejného důvodu: jsou [řádově dražší než text |images], takže poloviční cena je u nich znát mnohem víc. + +```php +$batch = $client->createBatch(); + +$batch->addImageRequest('majak', 'Maják na útesu za bouřky, malířský styl', 'gpt-image-2'); +$batch->addImageRequest('pristav', 'Přístav za svítání, tentýž malířský styl', 'gpt-image-2'); + +$response = $batch->submit(); +echo $response->getId(); +``` + +`addImageRequest()` vrací objekt požadavku, který si můžeš dál nastavit. Přidáš mu předlohu nebo změníš parametry, každý provider ty svoje: + +```php +use AIAccess\Media; + +$batch->addImageRequest('varianta', 'Tentýž maják za slunečného rána', 'gpt-image-2') + ->addReference(Media::fromFile('/cesta/k/majaku.png')) + ->setOptions(size: '1024x1024', quality: 'low'); +``` + +Je to tatáž dávka jako u konverzací, žádná zvláštní. Mimo dávku se jeden obrázek pořád generuje metodou [`generateImage()` |images], která má všechny volby jako pojmenované argumenty; druhá cesta k témuž tu schválně není. + +Předloha putuje ke každému požadavku ve vlastní kopii, což u velkého obrázku opakovaného přes celou dávku není zadarmo: OpenAI bere na celou úlohu 200 MB a base64 objem ještě o třetinu nafoukne. Když se blížíš ke stropu, rozděl dávku na víc menších. + +Výsledky se vyzvedávají úplně stejně jako u konverzací, jen v odpovědích nehledáš text, ale obrázky: + +```php +foreach ($batch->getResults() as $customId => $result) { + foreach ($result->message?->getMedia() ?? [] as $i => $media) { + $extension = explode('/', $media->getMimeType())[1]; + $media->save("/cesta/k/$customId-$i.$extension"); + } +} +``` + +Tady se to čtení po jedné položce vyplatí nejvíc: obrázky jsou megabajty a dávka jich klidně nese stovku, takže rozdíl mezi "drž jeden" a "drž všechny" je rozdíl mezi během a pádem na `memory_limit`. + +U OpenAI platí, že požadavek, který skončil bez obrázku, přijde jako selhání s vyplněným `$result->error`, ne jako prázdná zpráva. + +Co smí být v jedné úloze pohromadě, určuje provider, ne knihovna. **OpenAI jede jednu úlohu jedním endpointem**, takže tam obrázky nemůžou sdílet dávku s konverzacemi a generování nemůže sdílet dávku s úpravami; smíchané požadavky knihovna ohlásí dřív, než cokoli odešle. **Gemini žádné takové pravidlo nemá**, protože kreslí tímtéž endpointem, kterým mluví, takže u něj v jedné dávce klidně poletí text i obrázky. + + +Sledování a zrušení úlohy +========================= + +Stav úlohy nabývá čtyř hodnot: `InProgress`, dokud se pracuje (i když se úloha právě ruší), `Completed` po úspěšném dokončení, `Failed`, když úloha skončila neúspěchem, vypršela nebo byla zrušena, a `Other` pro stavy, které do téhle škály nezapadají. + +Za výsledky si ale nechoď jen při `Completed`. Zrušená i vypršelá úloha ti vydá požadavky, které stihla dokončit, a ty už jsou zaplacené; nic nemá jen ta, která pořád běží. + +```php +$batch = $client->retrieveBatch($batchId); + +echo 'stav: ', $batch->getStatus()->name, "\n"; +echo 'zadáno: ', $batch->getCreatedAt()?->format('j.n. H:i'), "\n"; +echo 'hotovo: ', $batch->getCompletedAt()?->format('j.n. H:i') ?? 'zatím ne', "\n"; +``` + +Rozpracovanou úlohu jde zrušit metodou `cancelBatch($id)`. Přehled svých úloh dostaneš z `listBatches()`, které se prochází `foreach`em a **další stránky si dotahuje samo, jak na ně dojdeš**, takže se stovka úloh čte stejně jako jedna a přerušený cyklus další požadavky nepošle. Seznam nese jen hlavičky úloh; **výsledky si knihovna dotáhne až ve chvíli, kdy je začneš číst** přes `getResults()`. + +```php +foreach ($client->listBatches() as $batch) { + echo $batch->getId(), ': ', $batch->getStatus()->name, "\n"; +} +``` + +Sledovat průběh ale nemusíš jen z kódu. Každý provider ukazuje odeslané úlohy i ve své webové konzoli, té samé, kde sis vydával [API klíč |getting-started], včetně stavu a času dokončení. Při ladění je to nejrychlejší způsob, jak zjistit, jestli už je hotovo, aniž bys kvůli tomu psal jediný řádek. + + +Tři mechanismy, jedno rozhraní +============================== + +Tady je vidět, co knihovna dělá, protože každý z providerů řeší dávky úplně jinak: + +| Provider | Jak dávka funguje u něj | +|----------|--------------------------------------------------------------------------------------| +| OpenAI | požadavky se serializují do souboru JSONL, ten se nahraje a teprve pak vznikne úloha | +| Claude | požadavky se posílají rovnou v těle jednoho volání | +| Gemini | dávka je takzvaná long-running operation a výsledky chodí uvnitř ní | + +Tvého kódu se to nedotkne, ať zvolíš kteréhokoli. Dávkové zpracování nabízejí OpenAI, Claude a Gemini; dávku obrázků pak OpenAI a Gemini. + + +Než to pustíš do provozu +======================== + +**U OpenAI a Gemini je jedna dávka jeden model.** Gemini má model v adrese úlohy, OpenAI ho váže na nahraný soubor, takže smíchat dva nejde. Knihovna to ohlídá a ohlásí `AIAccess\LogicException` hned při vkládání, ne až po odeslání. Claude to má jinak: model si nese každý požadavek zvlášť, takže tam můžeš klidně porovnávat dva modely vedle sebe v jedné dávce. + +**Nespoléhej na to, že to bude rychlé.** Provideři obvykle slibují dokončení do 24 hodin. V praxi bývají dávky hotové v řádu minut, ale je to slib horní meze, ne dolní; podle toho navrhni, co se stane, když výsledky ještě nejsou. + +**Identifikátor úlohy patří do databáze**, ne do proměnné. Skript, který dávku odeslal, dávno skončí, než budou výsledky k dispozici, takže si je obvykle vyzvedne až naplánovaná úloha spuštěná třeba po hodině. + +**U Gemini potřebuješ placený projekt.** Na free tieru dávkový endpoint odmítne pracovat. + +**U Gemini je paměťová úspora jen zdánlivá.** Jeho výsledky jezdí uvnitř samotné úlohy, takže než se k nim `getResults()` dostane, jsou v paměti celé; čtení po jedné položce je tam jednotnost, ne úspora. Ze stejného důvodu tam ani druhé projití nic nestojí, není co stahovat znovu. OpenAI a Claude posílají výsledky souborem, a ten se opravdu proudí. + +A drobnost, která ušetří zmatek: dokud dávka neskončí, `getResults()` prostě nic nevydá. Není to chyba, jen ještě není co číst, takže se nejdřív zeptej na stav. + + +Kam dál +======= + +- [Konverzace |chat] - jak vypadá tatáž práce v přímém režimu +- [Generování obrázků |images] - jednotlivé obrázky a jejich předlohy +- [Ošetření chyb |errors] - co znamenají jednotlivé výjimky +- [Provideři |providers] - co který umí a čím se liší diff --git a/ai-access/cs/chat.texy b/ai-access/cs/chat.texy new file mode 100644 index 0000000000..085170b388 --- /dev/null +++ b/ai-access/cs/chat.texy @@ -0,0 +1,154 @@ +Konverzace s AI modelem +*********************** + +.[perex] +Konverzace je posloupnost zpráv, kterou si knihovna drží sama. Naučíš se vést vícekolový dialog, nastavit modelu roli systémovou instrukcí, přečíst z odpovědi víc než jen text a poskládat historii ručně, když potřebuješ navázat na dřívější rozhovor. + + +Model si nic nepamatuje +======================= + +Tohle je první věc, která překvapí skoro každého: jazykový model nemá paměť. Každé volání API je samostatné a model o předchozí otázce neví nic. Iluze rozhovoru vzniká tím, že se s každým dotazem posílá celá dosavadní historie znovu. + +Právě proto tu je objekt konverzace. Stará se o historii za tebe: + +```php +$chat = $client->createChat('gpt-5.6-luna'); + +$chat->sendMessage('Jaké je hlavní město Francie?'); +$response = $chat->sendMessage('A jaká je tam známá památka?'); + +echo $response->getText(); +``` + +Druhá otázka nezmiňuje Paříž ani slovem, a přesto model odpoví správně, protože spolu s ní odešla i první výměna. Kdyby ses ptal přes dvě samostatná volání, druhá odpověď by byla nesmysl. + +Má to jeden důsledek, na který je dobré myslet dopředu: **dlouhá konverzace je drahá**. S každým kolem roste vstup, a vstup se platí. Historii proto někdy stojí za to zkrátit nebo začít znovu. A když stejný dotaz posíláš na stovky vstupů a nespěcháš, vyjde levněji [dávkové zpracování |batch]: + +```php +$chat->clearMessages(); +``` + + +Systémová instrukce +=================== + +Systémová instrukce říká modelu, jakou roli má hrát a jakých pravidel se držet. Platí pro celou konverzaci a modely jí přikládají větší váhu než běžné zprávě, takže sem patří pokyny, které mají platit i za deset kol. + +```php +$chat->setSystemInstruction('Jsi zkušený PHP vývojář. Odpovídej stručně a v příkladech používej Nette.'); +``` + +Dobrá systémová instrukce je konkrétní. Místo "buď stručný" napiš "odpovídej nejvýš třemi větami"; místo "buď přesný" napiš "když si nejsi jistý, řekni to místo hádání". Model nemá jak poznat, co si pod obecným pokynem představuješ. + +Systémová instrukce se posílá s každým požadavkem, takže se za ni platí pokaždé. Když je dlouhá a konverzace má hodně kol, sáhni po providerově cache; kolik se z ní načetlo, poznáš podle `cacheReadTokens` ve [spotřebě |#Co všechno je v odpovědi]. + + +Model se dá vyměnit uprostřed hovoru +==================================== + +Model není vlastnost konverzace, ale parametr jednotlivého požadavku, stejně jako teplota nebo limit tokenů. Na drátě se posílá s každým voláním znovu, a knihovna to tak i drží: + +```php +$chat->setModel('gpt-5.6-luna-mini'); +``` + +**Historie zůstává.** Další kolo pokračuje tam, kde předchozí skončilo, jen na něj odpoví jiný model. Hodí se to častěji, než by se zdálo: po chybě 429 přepneš na levnější model místo čekání, na těžkou otázku sáhneš po silnějším, nebo prostě necháš volbu na uživateli, jak to dělá každé chatovací rozhraní. + +Přepínat lze **v rámci jednoho providera**. Části zprávy, které nesou providerův vlastní obsah, typicky myšlenkový postup uvažovacího modelu, se přehrávají jen tomu, kdo je vytvořil, takže z Claude na GPT přejít nejde; z jednoho modelu Claude na druhý ano. + +Když má klient [nastavený výchozí model |getting-started#Kam patří klíč v ostrém provozu], obejde se `createChat()` bez argumentu úplně: + +```php +$chat = $client->createChat(); +``` + + +Historie sestavená ručně +======================== + +Někdy potřebuješ modelu předložit rozhovor, který se takhle neodehrál. Typicky když obnovuješ konverzaci uloženou v databázi, nebo když chceš ukázat pár příkladů správných odpovědí, což je technika známá jako few-shot prompting. + +```php +use AIAccess\Chat\Role; + +$chat = $client->createChat('gpt-5.6-luna'); +$chat->addMessage('Jaké je hlavní město Francie?', Role::User); +$chat->addMessage('Paříž.', Role::Model); +$chat->addMessage('A jaká je tam známá památka?', Role::User); + +$response = $chat->sendMessage(); // bez argumentu: pokračuj z historie +``` + +`addMessage()` zprávu jen přidá do historie a nic neodesílá. `sendMessage()` bez argumentu pak odešle konverzaci tak, jak je. + +Role jsou tři. `Role::User` je uživatel, `Role::Model` je model a `Role::Tool` nese výsledky [volání nástrojů |tools]. Knihovna přitom používá vlastní pojmenování: Gemini téže roli říká `model`, zatímco ostatní `assistant`, a na jméno, které zrovna ten který provider očekává, si ho knihovna přeloží sama. + +Celou historii si kdykoli vyžádáš zpátky: + +```php +foreach ($chat->getMessages() as $message) { + echo $message->getRole()->value, ': ', $message->getText(), "\n"; +} +``` + + +Co všechno je v odpovědi +======================== + +`sendMessage()` nevrací řetězec, ale objekt, protože samotný text je jen část toho, co se stalo. + +```php +$response = $chat->sendMessage('Napiš mi povídku o PHP.'); + +echo $response->getText(); +``` + +Když je text prázdný, nemusí jít o chybu. Model mohl odmítnout odpovědět, mohl narazit na limit dřív, než stihl napsat první slovo, nebo si místo odpovědi řekl o nástroj. Která z těch věcí to byla, prozradí důvod ukončení. + + +Proč model přestal psát +----------------------- + +Odpověď nekončí vždycky proto, že model dopověděl. Někdy ho zastaví limit tokenů, jindy bezpečnostní filtr a jindy čeká, až mu něco doplníš. Rozlišovat to potřebuješ, protože v každém z těch případů se zachováš jinak. Slouží k tomu `getFinishReason()`, který vrací jednu z hodnot výčtu `FinishReason`: + +| Hodnota | Co se stalo | Jak se zachovat | +|-------------------|-------------------------------------------------------------|-------------------------------------------------------------------------------| +| `Complete` | Model řekl všechno, co chtěl, a sám skončil. | Nic, tohle je ten dobrý případ. | +| `TokenLimit` | Odpověď je useknutá uprostřed, došel povolený počet tokenů. | Zvyš limit v [nastavení |options], nebo si vyžádej kratší odpověď. | +| `ContentFiltered` | Model odpověď odmítl. | Text bude prázdný. Přeformuluj dotaz; u OpenAI důvod prozradí `getRefusal()`. | +| `ToolCall` | Model si řekl o zavolání [nástroje |tools]. | Zavolej ho a výsledek mu pošli zpátky. | +| `Cancelled` | [Stream |streaming] jsi přerušil sám. | Máš jen část odpovědi, a je to v pořádku. | +| `Unknown` | Důvod mimo tuhle škálu, nebo žádný důvod vůbec. | Původní hodnotu najdeš v `getRawFinishReason()`. | + +Chybějící důvod je nejčastěji stopa po přenosu, který nedoběhl; proto se nevydává za `Complete`. U [přerušeného streamu |streaming] je to tvoje vodítko, že text není celý. + +V kódu to vypadá takhle: + +```php +use AIAccess\Chat\FinishReason; + +if ($response->getFinishReason() === FinishReason::TokenLimit) { + echo 'Odpověď je useknutá, model narazil na limit.'; +} +``` + +Uvažovací modely navíc můžou vrátit svůj myšlenkový postup. Nikdy není součástí `getText()`, protože do výstupu aplikace nepatří, ale přečíst si ho můžeš: + +```php +if ($reasoning = $response->getReasoning()) { + echo "Model uvažoval takto:\n", $reasoning; +} +``` + +A když ti abstrakce nestačí, `getRawResponse()` ti vrátí kompletní dekódovanou odpověď providera přesně tak, jak přišla. Sjednocené rozhraní je pohodlí, ne klec. + + +Kam dál +======= + +- [Nastavení a reasoning effort |options] - kolik přemýšlení si od modelu vyžádáš +- [Streamování |streaming] - odpověď čti, zatímco ji model teprve píše +- [Volání nástrojů |tools] - když má model sáhnout do tvé aplikace +- [Strukturovaný výstup |structured-output] - když potřebuješ data, ne prózu +- [Embeddingy |embeddings] - když má model odpovídat nad tvými vlastními texty diff --git a/ai-access/cs/embeddings.texy b/ai-access/cs/embeddings.texy new file mode 100644 index 0000000000..0e23a78116 --- /dev/null +++ b/ai-access/cs/embeddings.texy @@ -0,0 +1,140 @@ +Embeddingy a vyhledávání podle významu +************************************** + +.[perex] +Vyhledávání, které najde i to, co uživatel pojmenoval úplně jinými slovy. Nabídka souvisejících článků. Roztřídění dotazů do kategorií. Odpovídání nad vlastní dokumentací. Všechny tyhle úlohy stojí na jedné technice: embeddingy nechají model spočítat význam textu a převedou ho na čísla, se kterými už umí počítat i obyčejná databáze. + + +Co je embedding +=============== + +Máš na webu vyhledávání. Uživatel do něj napíše "jak zrychlit web" a nenajde nic, přestože máš článek "Optimalizace výkonu aplikace". Ani jedno slovo se neshoduje, takže `LIKE` i fulltext mlčí. Přitom je to přesně ten článek, který hledal. + +Můžeš si psát seznamy synonym, které nikdy nebudou úplné. Anebo přestaň porovnávat slova a začni porovnávat význam. Přesně to embeddingy umožňují. + +Představ si, že bys každý článek známkoval v dotazníku. Jak moc je o technice? Jak moc o vaření? Jak moc řeší nějaký problém? Jak moc je to návod? Z tvých známek vznikne řádek čísel, a dva články s podobnými známkami jsou zjevně o tomtéž, i když každý používá jiná slova. + +**A přesně tohle dělá embedding, jen v mnohem větším měřítku.** Otázky si model vymyslí sám, je jich několik set až několik tisíc a nikdo je nikdy nevyslovil; my se je nedozvíme a ani je znát nepotřebujeme. Dostaneme jen ty známky, tedy řadu čísel (desetinných). Proto nemá smysl dívat se na jednotlivá čísla a hledat v nich význam; ten dává až porovnání dvou takových řad. Řadě čísel se říká **vektor**, a proto se tak jmenuje i třída v knihovně. + +Ještě jedna vlastnost se hodí: **výpočet je deterministický**. Stejný text poslaný stejnému modelu vrátí pokaždé stejný vektor. Na rozdíl od konverzace s modelem tady nehrozí, že dostaneš zítra jiný výsledek než včera, takže si vektory můžeš uložit a už je nikdy nepočítat znovu. + + +První vektory +============= + +Embeddingy umí spočítat OpenAI a Gemini. Tenhle skript můžeš rovnou spustit: + +```php +require __DIR__ . '/vendor/autoload.php'; + +$client = new AIAccess\Provider\OpenAI\Client('sem-vloz-svuj-klic'); + +$vectors = $client->calculateEmbeddings([ + 'Jak zrychlit web', + 'Optimalizace výkonu aplikace', + 'Recept na svíčkovou', +], 'text-embedding-3-small'); + +echo 'web vs. výkon: ', $vectors[0]->cosineSimilarity($vectors[1]), "\n"; +echo 'web vs. svíčková: ', $vectors[0]->cosineSimilarity($vectors[2]), "\n"; +``` + +Dostaneš pole objektů `AIAccess\Embedding\Vector` ve stejném pořadí, v jakém jsou texty. + + +Jak číst míru podobnosti +------------------------ + +Metoda `cosineSimilarity()` porovná dvě takové řady známek a shrne jejich shodu do jediného čísla od -1 do 1. Šikovné na tom je, že na délce textu nezáleží: krátký dotaz a dlouhý článek o tomtéž si vyjdou blízko, i když je jeden desetkrát delší. + +- **1** je nejvyšší možná shoda, tedy prakticky totožný význam. +- **0** znamená, že spolu texty nemají nic společného. +- **-1** by znamenalo přesný opak. U textových embeddingů se to prakticky nestává, takže se dolní polovinou škály nemusíš zabývat. + +Nečekej ale, že se čísla roztáhnou po celé stupnici. V praxi sedí v mnohem užším pásmu: nesouvisející texty nevyjdou na nule a dokonalý zásah nevyjde na jedničce. Každý model má navíc stupnici rozprostřenou jinak, takže vlastní hranici pro "dost podobné" si musíš naměřit na svých datech. + +Nejspolehlivěji se stejně pracuje s pořadím. Seřaď kandidáty podle podobnosti a vezmi několik nejvyšších; to funguje bez ohledu na to, jak model škáluje. + + +Sémantické vyhledávání krok za krokem +===================================== + +Vyhledávání nad vlastními daty má dvě fáze a vyplatí se je oddělit, protože každá probíhá jindy. + +**Jednou při indexaci** spočítáš embedding každého dokumentu a uložíš ho. Tohle je ta placená a pomalejší část, ale děje se jen při vzniku nebo změně dokumentu. + +**Při každém dotazu** spočítáš embedding otázky, což je jedno rychlé volání, a porovnáš ho s uloženými vektory. Nejpodobnější dokumenty jsou výsledek hledání. + +Ve zkratce, nezávisle na tom, jakou databázovou vrstvu používáš: + +```php +use AIAccess\Embedding\Vector; + +// jednou při indexaci: ulož vektor k dokumentu +[$vector] = $client->calculateEmbeddings([$text], 'text-embedding-3-small'); +$binary = $vector->serialize(); + +// při hledání: spočítej vektor otázky a porovnej s uloženými +[$query] = $client->calculateEmbeddings([$question], 'text-embedding-3-small'); + +$scores = []; +foreach ($storedArticles as $id => $storedBinary) { + $scores[$id] = $query->cosineSimilarity(Vector::deserialize($storedBinary)); +} +arsort($scores); +$best = array_slice($scores, 0, 5, preserve_keys: true); +``` + +A teď to nejlepší: nalezené úryvky nemusí být cíl, ale surovina. Pošli je spolu s původní otázkou modelu do [konverzace |chat] a místo seznamu odkazů dostaneš souvislou odpověď postavenou na tvých vlastních datech, která model při trénování nikdy neviděl. Tomuhle spojení vyhledávání a odpovídání se říká RAG a je to dnes nejčastější způsob, jak modelu dodat znalosti, které nemá. + + +Kam vektory uložit +================== + +Metoda `Vector::serialize()` udělá z vektoru binární řetězec vhodný do sloupce typu `BLOB` nebo `VARBINARY`. Zpátky ho převede statická `Vector::deserialize()`. Čísla se ukládají po 32 bitech v pevném pořadí bajtů. + +Možná jsi slyšel pojem **vektorová databáze**. Je to úložiště, které umí najít nejpodobnější vektory, aniž by prošlo všechny; staví si nad nimi index podobně, jako si běžná databáze staví index nad sloupcem. Patří sem třeba PostgreSQL s rozšířením pgvector, SQLite s rozšířením sqlite-vec nebo samostatné služby jako Qdrant. + +Dokud jsi ale v řádu tisíců dokumentů, **žádnou nepotřebuješ**. Pár tisíc vektorů zabere pár desítek megabajtů v paměti a lineární projití trvá jednotky milisekund, takže obyčejné pole a `foreach` výše je plnohodnotné řešení. Specializované úložiště začni řešit, až budeš mít statisíce záznamů nebo až tě průchod začne brzdit. + + +Kolik to stojí +============== + +Embeddingy jsou proti konverzaci s modelem levné. Platí se u nich jen vstup, protože žádný výstupní text nevzniká, a účtují se stejně jako u chatu po [tokenech |getting-started#Kolik to stojí]. + +Prakticky to znamená, že zaindexovat několik tisíc článků stojí obvykle míň, než čekáš, a jeden dotaz uživatele je zanedbatelný. Jediná položka, která umí překvapit, je **přepočet celé databáze** po změně modelu, protože zaplatíš znovu úplně všechno. + + +Na co si dát pozor +================== + +**Indexace i dotaz musí používat stejný model.** Každý model má vlastní prostor, takže vektory z různých modelů se porovnávat nedají. Zrádné je, jak se to projeví: když mají různý počet čísel, `cosineSimilarity()` vyhodí `AIAccess\LogicException` a dozvíš se to hned. Když ho mají shodou okolností stejný, **nespadne nic** a jen dostaneš nesmyslné pořadí výsledků. Po změně modelu proto vždycky přepočítej celou databázi. + +Zbytek je drobnější: + +- **Prázdný vstup skončí výjimkou.** Prázdné pole i prázdný řetězec v něm vyhodí `AIAccess\LogicException` ještě před odesláním, což je lepší, než platit za dotaz, ze kterého nic nebude. +- **Jedno volání zvládne víc textů najednou** a je to výrazně rychlejší i levnější než volat je po jednom. OpenAI přijme až 2048 vstupů v jednom požadavku. +- **Dlouhý dokument rozděl na části.** Modely mají strop na délku vstupu a hlavně platí, že čím delší text, tím rozmazanější význam. Kratší úryvky se hledají přesněji. + + +Rozdíly mezi providery +====================== + +Embeddingy nabízejí jen dva z pěti providerů; Claude, DeepSeek ani Grok vlastní embedding API nemají. + +| Provider | Volitelné navíc | +|----------|----------------------------------------------------------------------------------------| +| OpenAI | `dimensions` zkrátí vektor a ušetří místo v databázi, umí to modely `text-embedding-3` | +| Gemini | `taskType` říká, k čemu vektor bude, například `RETRIEVAL_DOCUMENT` | + +Gemini rozlišuje, jestli text ukládáš do indexu, nebo se jím ptáš, a podle toho vektor mírně upraví. Když u něj použiješ `title`, musíš zároveň nastavit `taskType` na `RETRIEVAL_DOCUMENT`, jinak dostaneš `AIAccess\LogicException`; pojmenovat se dá dokument, ne dotaz. + + +Kam dál +======= + +- [Konverzace |chat] - jak nalezené úryvky předat modelu +- [Dávkové zpracování |batch] - když potřebuješ zaindexovat opravdu hodně textů +- [Ošetření chyb |errors] - co dělat, když volání selže +- [Provideři |providers] - co který umí a čím se liší diff --git a/ai-access/cs/errors.texy b/ai-access/cs/errors.texy new file mode 100644 index 0000000000..2ec0fae1f6 --- /dev/null +++ b/ai-access/cs/errors.texy @@ -0,0 +1,135 @@ +Ošetření chyb a výjimky +*********************** + +.[perex] +Volání cizího API selže dřív nebo později vždycky. Zajímavé není, že selhalo, ale jestli má cenu to zkusit znovu. Přesně kolem téhle otázky jsou postavené výjimky v AI Access. Podíváme se, které existují, jak je odchytit a proč odmítnutí modelu mezi chyby nepatří. + + +Jediná otázka, která v produkci dává smysl +========================================== + +Když ti volání spadne, můžeš se ptát na spoustu věcí. V běžící aplikaci ale rozhoduje jen jedna: **mám to zopakovat, nebo je to marné?** + +Vypadlá síť je něco jiného než špatný klíč. To první se za vteřinu spraví samo, to druhé nespraví ani sto pokusů. Kdyby knihovna házela jeden typ výjimky pro obojí, musel bys rozhodovat podle textu zprávy, a to je nejkřehčí kód, jaký můžeš napsat. + +Proto jsou výjimky rozdělené podle toho, co s nimi můžeš udělat: + +| Výjimka | Co se stalo | Zopakovat? | +|-------------------------------|----------------------------------------------------------|---------------------------------| +| `ApiException` | Provider odpověděl chybou. `getCode()` nese HTTP status. | Podle stavu, viz níže. | +| `CommunicationException` | Nespojili jsme se, nebo přišla nečitelná odpověď. | Ano, skoro vždycky má smysl. | +| `UnexpectedResponseException` | Odpověď dorazila, ale nemá očekávanou strukturu. | Ne. Zaloguj a podívej se na to. | +| `TooManyRoundsException` | [Smyčka nástrojů |tools] vyčerpala limit kol. | Ne. Zvyš limit, nebo to vzdej. | +| `LogicException` | Chyba ve tvém vlastním kódu. | Ne. Má spadnout. | +| `IOException` | Soubor nejde přečíst nebo zapsat. | Podle příčiny. | + +První čtyři mají společného předka `AIAccess\ServiceException`, takže se dají odchytit jedním `catch`, když ti stačí vědět, že služba selhala. `TooManyRoundsException` navíc nese poslední odpověď ve vlastnosti `$lastResponse` a historie zůstává celá, takže konverzace jde po zvýšení limitu rozjet dál. `AIAccess\LogicException` naopak dědí od stejnojmenné třídy z PHP, takže zapadne do ošetření, které už třeba máš. + +`AIAccess\IOException` stojí stranou úplně, protože se netýká providera: potkáš ji u `Media::fromFile()` a `Media::save()`, tedy když se nepovede přečíst předlohu nebo uložit vygenerovaný obrázek, a u [cachovacího klienta |http], když nejde založit adresář s cache. + + +Jak to odchytit +=============== + +Od nejkonkrétnějšího po nejobecnější, jak je v PHP zvykem: + +```php +try { + $response = $chat->sendMessage('Ahoj!'); + echo $response->getText(); + +} catch (AIAccess\ApiException $e) { + // provider odpověděl chybou, $e->getCode() je HTTP status + if ($e->getCode() === 429) { + // překročený limit, zkus to za chvíli + } + +} catch (AIAccess\CommunicationException $e) { + // nespojili jsme se; opakování má smysl + +} catch (AIAccess\ServiceException $e) { + // cokoli dalšího, co služba dokáže pokazit +} +``` + +Pokud ti na rozlišení nezáleží, stačí jediný `catch (AIAccess\ServiceException $e)`. Rozhodně to ale nedělej tak, že odchytíš `\Throwable`: spolkl bys tím i `LogicException`, tedy vlastní chybu, kterou chceš vidět. + + +Co znamenají jednotlivé stavy +============================= + +`ApiException` je jediná, u které se vyplatí dívat na `getCode()`, protože HTTP status pod ní říká hodně: + +- **401 a 403** - klíč je špatně, chybí, vypršel, nebo nemá na tenhle model právo. Opakování nepomůže. +- **404** - model tohoto jména neexistuje. Nejčastěji překlep nebo model, který provider vyřadil. +- **429** - vyčerpaný limit požadavků nebo prázdný kredit. Počkej a zkus to znovu; provider často pošle hlavičku `Retry-After` s údajem, jak dlouho čekat. +- **400** - požadavek se providerovi nelíbí. Typicky parametr, který ten model nezná; zpráva výjimky obvykle řekne který. +- **500 a výš** - problém na jejich straně. Opakování má smysl. + +Zvláštní případ je OpenAI, které umí selhat **uvnitř úspěšné odpovědi**: HTTP je 200, ale uvnitř je stav `failed`. Knihovna to pozná a hodí `ApiException` stejně, jako by přišel chybový stav, takže se tím nemusíš zabývat. + + +Odmítnutí není chyba +==================== + +Tohle je nejčastější nedorozumění. Když model odmítne odpovědět, protože se mu dotaz nelíbí, **není to výjimka**. Požadavek proběhl v pořádku, provider odpověděl a naúčtoval si to; jen v odpovědi není text. + +Poznáš to podle [důvodu ukončení |chat#Proč model přestal psát]: + +```php +use AIAccess\Chat\FinishReason; + +$response = $chat->sendMessage($dotaz); + +if ($response->getFinishReason() === FinishReason::ContentFiltered) { + // model odmítl; u OpenAI ti důvod řekne $response->getRefusal() +} +``` + +Stejnou logikou nejsou chybou ani useknutá odpověď při vyčerpaném limitu tokenů, ani kolo, ve kterém si model místo odpovědi řekl o nástroj. Ve všech třech případech je odpověď platná, jen jiná, než jsi čekal. + + +Chyby, které se nehází vůbec +============================ + +Na dvou místech by výjimka nedávala smysl, tak se tam nepoužívá. + +**[Dávkové zpracování |batch] může selhat jen zčásti.** Ze sta požadavků jich devadesát devět projde a jeden ne. Kvůli tomu jednomu by nemělo padat celé čtení výsledků, takže se chyby jednotlivých položek sbírají zvlášť: + +```php +foreach ($batch->getResults() as $customId => $result) { + if ($result->message === null) { + echo $customId, ' selhalo: ', $result->error, "\n"; + } else { + echo $customId, ': ', $result->message->getText(), "\n"; + } +} +``` + +**Chyba při [volání nástroje |tools] může patřit modelu, ne tobě.** Když si model vymyslí neexistující nástroj nebo pošle argumenty, které neodpovídají schématu, dostane chybovou zprávu jako výsledek a může se opravit; tvůj kód o tom vůbec nemusí vědět. Když ale selže **tvůj vlastní** nástroj, výjimka propadne k tobě, dokud si nezapneš `setToolLoop(catchErrors: true)`. Ani potom ti ale nezmizí překlep v handleru: `TypeError` a jemu podobné propadají vždycky, protože to není chyba, kterou má řešit model. + +Knihovna nikde nepoužívá `trigger_error()`, takže se žádný problém neztratí jen proto, že má aplikace vypnuté `display_errors`. Jediné varování, které zůstalo, upozorňuje na střídání rolí u Gemini. + + +Opakování nemusíš psát ručně +============================ + +Pokud jsi po přečtení tabulky nahoře přemýšlel, že si napíšeš `for` cyklus s čekáním, nemusíš. Knihovna má [dekorátor |http], který to umí, respektuje `Retry-After` a neopakuje nic, co by dopadlo stejně: + +```php +$client = new AIAccess\Provider\OpenAI\Client( + $apiKey, + new AIAccess\Http\RetryClient(new AIAccess\Http\CurlClient), +); +``` + +Od téhle chvíle se limity a výpadky řeší samy a ke tvému `catch` se dostane jen to, co opravdu neprošlo. + + +Kam dál +======= + +- [HTTP vrstva |http] - opakování, logování a cachování požadavků +- [Konverzace |chat] - důvody ukončení a co z odpovědi vyčteš +- [Dávkové zpracování |batch] - když část požadavků selže +- [Volání nástrojů |tools] - chyby, které se vracejí modelu diff --git a/ai-access/cs/getting-started.texy b/ai-access/cs/getting-started.texy new file mode 100644 index 0000000000..d007a73933 --- /dev/null +++ b/ai-access/cs/getting-started.texy @@ -0,0 +1,190 @@ +Začínáme s AI Access +******************** + +.[perex] +Od prázdného projektu k první odpovědi modelu. Vysvětlíme si tři pojmy, bez kterých se neobejdeš, vybereme providera i model, získáme klíč, spustíme první skript a podíváme se, kolik to stojí a co dělat, když to nevyjde. + + +Tři pojmy, které potřebuješ znát +================================ + +Než napíšeš první řádek, vyplatí se rozumět třem slovům, která se v dokumentaci opakují pořád dokola. + +**Provider** je firma, která jazykové modely provozuje a účtuje si za jejich používání. AI Access jich zná pět: OpenAI (známé díky ChatGPT), Anthropic (modely Claude), Google (modely Gemini), čínský DeepSeek a xAI (modely Grok). U jednoho z nich si založíš účet. + +**Model** je konkrétní mozek, se kterým mluvíš. Každý provider jich nabízí několik a liší se cenou i schopnostmi: malé a levné zvládnou klasifikaci nebo shrnutí, velké a drahé si poradí se složitým uvažováním. Model se v kódu určuje jménem, třeba `gpt-5.6-luna`. + +**API klíč** je dlouhý náhodný řetězec, který providerovi říká, kdo volá a komu to naúčtovat. Funguje jako heslo a jako platební karta zároveň. + +Volání modelu **stojí peníze**. Nejde o velké částky, u běžného dotazu mluvíme o zlomcích koruny, ale účet vzniká od prvního volání, takže si u providera nejspíš budeš muset nabít kredit nebo zadat platební kartu. Někteří provideři dávají nováčkům malý kredit zdarma. + + +Kterého providera zvolit +======================== + +Dobrá zpráva je, že tahle volba není osudová. Přepnutí providera je v AI Access změna jednoho řádku, takže když ti jeden nebude vyhovovat, zkusíš druhý bez přepisování aplikace. + +V běžné konverzaci si vedou dobře všichni. Rozdíly jsou jinde: + +- **Šíře schopností.** Chceš kromě chatu i embeddingy pro vyhledávání, dávkové zpracování nebo generování obrázků? Podívej se do [tabulky schopností |@home#Co knihovna umí]; nejširší záběr mají OpenAI a Gemini. +- **Cena.** Mezi nejlevnějším malým modelem a nejdražším uvažovacím je rozdíl zhruba dva řády. DeepSeek bývá výrazně levnější než ostatní. +- **Kam tečou data.** Pro firemní nasazení bývá rozhodující, kde se data zpracovávají a co si provider smí nechat pro trénování. Odpověď hledej v jeho obchodních podmínkách, ne v dokumentaci knihovny. +- **Dostupnost.** Ne každý provider je dostupný odevšad a některé funkce, u Gemini třeba dávkové zpracování a generování obrázků, vyžadují projekt s aktivní fakturací. + +Když si nevíš rady, začni u toho, u kterého snadno zaplatíš, a soustřeď se na samotnou aplikaci. Přepnout se dá kdykoli později. + + +Který model zvolit +================== + +Uvnitř každého providera si pak vybíráš mezi modely. Zjednodušeně platí tři pravidla. + +**Začni malým.** Modely s označením jako `flash`, `mini` nebo `lite` jsou levné a rychlé a na shrnutí, klasifikaci, přeformulování nebo vytažení údajů z textu bohatě stačí. Velký model tyhle úlohy neudělá o mnoho lépe, jen dráž a pomaleji. + +**Po velkém sáhni, až když malý selže.** Poznáš to podle výsledků: model si vymýšlí, nedodržuje instrukce nebo ztrácí nit v delším zadání. Teprve tehdy se vyplatí přejít na silnější variantu. + +**Uvažovací modely jsou zvláštní kategorie.** Než odpoví, přemýšlejí, což stojí čas i tokeny navíc, zato zvládnou úlohy o více krocích. Kolik přemýšlení chceš, řídíš přes [reasoning effort |options]; u jednoduchých dotazů ho klidně vypni. + +Rozumný start k srpnu 2026 vypadá takhle: + +| Provider | Chat model | Model pro embeddingy | +|----------|-------------------------|--------------------------| +| OpenAI | `gpt-5.6-luna` | `text-embedding-3-small` | +| Claude | `claude-sonnet-5` | – | +| Gemini | `gemini-3.5-flash-lite` | `gemini-embedding-2` | +| DeepSeek | `deepseek-v4-flash` | – | +| Grok | `grok-4.3` | – | + +Modely se obměňují rychleji, než se stíhá aktualizovat jakákoli dokumentace, takže tabulku ber jako výchozí bod, ne jako zákon. Jméno modelu je v kódu obyčejný řetězec, takže nový model funguje v den vydání; jestli ten tvůj ještě existuje, ověříš pomocí [listModels() |providers#Jaké modely provider právě nabízí]. + + +Získání klíče +============= + +Klíč se vydává v konzoli providera. Postup je všude stejný: založíš účet, přidáš platební metodu nebo kredit a v sekci s API klíči si necháš vygenerovat nový. **Klíč uvidíš jen jednou**, takže si ho rovnou ulož; když ho ztratíš, vygeneruješ jiný. + +| Provider | Konzole | +|----------|----------------------------------------------------------------------| +| OpenAI | [platform.openai.com |https://platform.openai.com/api-keys] | +| Claude | [console.anthropic.com |https://console.anthropic.com/settings/keys] | +| Gemini | [aistudio.google.com |https://aistudio.google.com/app/apikey] | +| DeepSeek | [platform.deepseek.com |https://platform.deepseek.com/api_keys] | +| Grok | [console.x.ai |https://console.x.ai/team/default/api-keys] | + +V téže konzoli najdeš i aktuální ceník a přehled, kolik jsi zatím utratil. Hned na začátku se vyplatí nastavit si tam měsíční limit útraty; je to nejjednodušší pojistka proti chybě v cyklu. + + +První skript +============ + +Tohle je kompletní soubor, který stačí uložit a spustit. Klíč je v něm napsaný přímo, protože teď jde hlavně o to, aby ti to fungovalo na první pokus. Za chvíli si ukážeme, kam patří v ostrém provozu. + +```php +require __DIR__ . '/vendor/autoload.php'; + +$client = new AIAccess\Provider\OpenAI\Client('sem-vloz-svuj-klic'); + +$chat = $client->createChat('gpt-5.6-luna'); +$response = $chat->sendMessage('Vysvětli v jedné větě, co je to dependency injection.'); + +echo $response->getText(), "\n"; +``` + +Spustíš ho z příkazové řádky příkazem `php soubor.php` a za chvíli uvidíš odpověď. `createChat()` otevře konverzaci nad zvoleným modelem, `sendMessage()` pošle zprávu a počká na odpověď. + +Návratová hodnota není řetězec, ale objekt odpovědi. Kromě textu se z něj dozvíš i to, proč model přestal psát a kolik to stálo. + + +Kam patří klíč v ostrém provozu +=============================== + +Klíč napsaný v kódu je v pořádku pro první pokus, ale ne pro nic dalšího. Kdo ho získá, utrácí na tvůj účet, a nejčastěji uniká tak, že se omylem dostane do gitu. Vzít to zpět jde jen zdánlivě: přepsat historii sice umíš, ale jakmile se commit dostal na server, musíš klíč považovat za prozrazený, protože ho mezitím mohl kdokoli zkopírovat. Spolehlivé řešení je jediné, totiž vydat nový klíč a starý zneplatnit. + +Proto se klíč zapisuje mimo kód, nejčastěji do **proměnné prostředí**. To je pojmenovaná hodnota, kterou aplikaci předá operační systém, webhosting nebo Docker, takže žije v nastavení serveru a ne v souborech projektu. V PHP ji přečteš takhle: + +```php +$apiKey = getenv('OPENAI_API_KEY'); +``` + +Na svém počítači ji nastavíš před spuštěním skriptu, ve Windows příkazem `set OPENAI_API_KEY=...`, na Linuxu a macOS příkazem `export OPENAI_API_KEY=...`. Na hostingu k tomu bývá kolonka v administraci. + +Druhá běžná cesta je konfigurační soubor uvedený v `.gitignore`, takže se nikdy neverzuje. V Nette aplikaci patří klíč do lokální konfigurace a odtud do [DI kontejneru |dependency-injection:]: + +```neon +parameters: + openaiApiKey: '...' + +services: + - AIAccess\Provider\OpenAI\Client(%openaiApiKey%, chatModel: 'gpt-5.6-luna') +``` + +Všimni si druhého parametru. **Jméno modelu patří ke klíči, ne do aplikace**, protože je to řetězec vlastní jednomu provideru. Klient, který si model nese, ho pak doplní sám a volání se obejde bez něj: + +```php +$chat = $client->createChat(); +``` + +Tím získáš to nejpříjemnější na přepínání providerů: klienta si necháš předat konstruktorem a tvoje třída opravdu nikdy neví, se kterým providerem ani modelem mluví. Předat model ve volání jde pořád a má přednost; klient bez výchozího modelu, kterému ho nedáš ani ve volání, ohlásí `LogicException`. + + +Kolik to stojí +============== + +Provideři účtují po **tokenech**, což jsou kousky slov. Anglický text má zhruba čtyři znaky na token, čeština kvůli diakritice a skloňování o dost víc, takže stejně dlouhá česká věta vyjde na víc tokenů než anglická. + +Platí se zvlášť vstup, tedy všechno, co modelu pošleš, a zvlášť výstup, tedy co napíše. **Výstup bývá několikanásobně dražší než vstup.** Přemýšlení uvažovacích modelů se počítá jako výstup, i když ho nikdy neuvidíš. + +Konkrétní ceny sem schválně nepíšeme, protože se mění a rychle by zestárly. Najdeš je na webu každého providera pod heslem Pricing a v téže konzoli, kde sis vydal klíč. Pro představu o řádech: krátká otázka s krátkou odpovědí na malém modelu stojí zlomky haléře, kdežto opakované shrnování dlouhých dokumentů největším uvažovacím modelem už je položka, kterou v účetnictví poznáš. **Rozdíl mezi nejlevnějším a nejdražším modelem téhož providera bývá zhruba dva řády**, takže volba modelu ovlivní účet mnohem víc než optimalizace promptu. + +Kolik stálo konkrétní volání, ti řekne odpověď sama: + +```php +$usage = $response->getUsage(); + +echo 'vstup: ', $usage->inputTokens, "\n"; +echo 'výstup: ', $usage->outputTokens, "\n"; +echo 'celkem: ', $usage->getTotalTokens(), "\n"; +``` + +Dvě čísla navíc stojí za pozornost. `reasoningTokens` je to, co model spotřeboval na přemýšlení, a u uvažovacích modelů bývá větší než odpověď sama. `cacheReadTokens` naopak říká, kolik vstupu se načetlo z providerovy cache za zlomek ceny; když posíláš pořád stejnou dlouhou systémovou instrukci, je tohle číslo tvůj kamarád. + + +Když první volání selže +======================= + +Chyby se hlásí výjimkami a jejich typ ti řekne, co se stalo, ještě než si přečteš zprávu. Takhle je odchytíš: + +```php +try { + $response = $chat->sendMessage('Ahoj!'); + echo $response->getText(); + +} catch (AIAccess\ApiException $e) { + // provider odpověděl chybou; kód je HTTP status + echo 'API vrátilo chybu ', $e->getCode(), ': ', $e->getMessage(); + +} catch (AIAccess\CommunicationException $e) { + // nespojili jsme se, nebo přišla nečitelná odpověď + echo 'Spojení selhalo: ', $e->getMessage(); +} +``` + +Co znamenají nejčastější stavy, které v `AIAccess\ApiException` uvidíš: + +- **401** - klíč je špatně, chybí, nebo patří jinému providerovi. +- **404** - model tohoto jména neexistuje. Nejčastěji překlep nebo model, který provider vyřadil; nech si vypsat [seznam modelů |providers#Jaké modely provider právě nabízí]. +- **429** - překročil jsi limit požadavků, nebo máš prázdný kredit. Opakování za tebe umí zařídit [RetryClient |http]. +- **500 a výš** - problém na straně providera, opakování má smysl. + +Kromě těch dvou výjimek existují ještě `AIAccess\UnexpectedResponseException`, když odpověď nemá očekávanou strukturu, a `AIAccess\LogicException` pro chybu ve tvém vlastním kódu, třeba odeslání konverzace bez jediné zprávy. Tu poslední nemá smysl odchytávat, ta má spadnout a upozornit tě. + +Celou hierarchii i to, jak si ošetření napsat jednou pro celou aplikaci, rozebírá kapitola o [ošetření chyb |errors]. + + +Kam dál +======= + +- [Konverzace |chat] - víc zpráv za sebou, systémová instrukce a čtení odpovědi +- [Nastavení a reasoning effort |options] - kolik přemýšlení si od modelu vyžádáš +- [Streamování |streaming] - ať uživatel nekouká na prázdnou stránku +- [Ošetření chyb |errors] - co dělat, když provider řekne ne diff --git a/ai-access/cs/http.texy b/ai-access/cs/http.texy new file mode 100644 index 0000000000..5708c4822c --- /dev/null +++ b/ai-access/cs/http.texy @@ -0,0 +1,150 @@ +HTTP vrstva a opakování požadavků +********************************* + +.[perex] +Každé volání providera prochází jedinou tenkou vrstvou, kterou můžeš vyměnit nebo obalit. Díky tomu se opakování po chybách, logování požadavků i cachování odpovědí řeší jednou pro celou aplikaci, aniž bys sáhl na kód, který s modelem mluví. + + +Tři problémy, jedno místo +========================= + +Až budeš mít aplikaci chvíli v provozu, narazíš na tohle: + +- Provider ti občas odpoví, že mu chodí požadavky příliš rychle za sebou, a volání spadne, přestože by za dvě vteřiny prošlo. +- Potřebuješ vidět, co se vlastně posílá ven a jak dlouho to trvá, protože něco odpovídá pomalu a ty nevíš co. +- Při vývoji spouštíš tentýž skript popadesáté a pokaždé za něj platíš, i když se vstup nezměnil. + +Všechny tři se řeší na jednom místě, a to výměnou toho, co posílá HTTP požadavky. Klient providera totiž neposílá nic sám. Postará se o to objekt, který mu předáš druhým argumentem konstruktoru: + +```php +use AIAccess\Http; + +$client = new AIAccess\Provider\OpenAI\Client( + $apiKey, + new Http\RetryClient(new Http\CurlClient), +); +``` + +Když druhý argument vynecháš, použije se `AIAccess\Http\CurlClient`. Knihovna má tři obaly, které se dají libovolně skládat, protože každý z nich je sám o sobě zase HTTP klient. + + +RetryClient: Opakování po limitech a výpadcích +============================================== + +`RetryClient` zkouší neúspěšné požadavky znovu, ale jen když to má smysl: + +```php +$http = new Http\RetryClient( + new Http\CurlClient, + maxAttempts: 3, + initialDelay: 1.0, + maxDelay: 30.0, +); +``` + +Opakují se stavy **408 a 429** a serverové chyby **od 500 výš**, plus výpadky sítě, které nastaly dřív, než dorazila odpověď. Mezi pokusy se čeká, doba se pokaždé zdvojnásobí a navíc se náhodně rozkolísá, aby ti tisíc paralelních procesů nezaútočilo na providera v jednu chvíli. Když provider pošle hlavičku `Retry-After`, řídí se podle ní. + +Stejně poučné je, co se neopakuje. Chyby ve čtyřstovkách kromě 408 a 429 dopadnou podruhé úplně stejně, takže by opakování jen zdrželo. A ze serverových chyb jsou vyňaté **501 a 505**, protože ty neříkají "teď ne", ale "tohle neumím a nikdy umět nebudu". + +Nejzajímavější pravidlo se týká [streamování |streaming]: jakmile dorazil první kousek odpovědi, opakování se zakáže. Model už začal psát a ty už za to platíš; kdyby se požadavek přehrál, dostal bys odpověď dvakrát a zaplatil ji dvakrát. + + +ObservableClient: Vidět, co se děje +=================================== + +`ObservableClient` ohlásí každý požadavek a každou odpověď i s tím, jak dlouho trvala. Hodí se do logu, do [Tracy|tracy:] nebo do vlastního přehledu útraty. + +```php +$http = new Http\ObservableClient( + new Http\CurlClient, + onRequest: function (string $url, $payload): void { + Debugger::log("-> $url"); + }, + onResponse: function (Http\Response $response, float $elapsed): void { + Debugger::log(sprintf('<- %d za %.1f s', $response->getStatusCode(), $elapsed)); + }, + onError: function (Throwable $e, float $elapsed): void { + Debugger::log(sprintf('!! %s po %.1f s', $e->getMessage(), $elapsed)); + }, +); +``` + +Požadavek, ze kterého žádná odpověď nevznikla, jde do `onError`: typicky timeout nebo zavřené spojení, ale taky cokoli vyhodí tvůj vlastní streamovací callback, protože odtud se to nedá rozeznat. Právě tyhle požadavky přitom chceš v logu vidět nejvíc, a `onResponse` je nemá jak ohlásit. Výjimka pak putuje dál k tobě nezměněná. + +Všimni si, že do `onRequest` nedostáváš hlavičky. Není to opomenutí: hlavičky nesou API klíč a ten se nesmí dostat do logu, kde ho uvidí každý, kdo má přístup k souborům. + +U streamované odpovědi měří `$elapsed` celý přenos, tedy dobu, než model dopsal, ne dobu do prvního slova. + + +CachingClient: Neplatit dvakrát za totéž +======================================== + +`CachingClient` si ukládá odpovědi na disk a stejný požadavek podruhé už neposílá. Je to nástroj **pro vývoj a testy**, ne pro produkci. Model, který na tutéž otázku odpovídá pořád stejně, není v ostrém provozu vlastnost, ale chyba. + +```php +$http = new Http\CachingClient(new Http\CurlClient, __DIR__ . '/temp/ai-cache', ttl: 3600); +``` + +Kešovací klíč se počítá z metody, URL, těla požadavku a hlaviček, přičemž **autentizační hlavičky se z klíče vynechávají**. Díky tomu tě přepnutí na jiný klíč nepřipraví o cache, ale hlavička, která mění chování API, ano. Ukládají se jen úspěšné odpovědi, takže si chybu nezapamatuje, a streamy ani nahrávání souborů cache nechává projít beze změny. + + +Na pořadí obalů záleží +====================== + +Obaly se skládají do sebe a výsledek se liší podle toho, který je zvenku: + +```php +// loguje jen konečný výsledek: opakování proběhne uvnitř a ven se dostane až ono +$http = new Http\ObservableClient(new Http\RetryClient(new Http\CurlClient), onResponse: $log); + +// loguje každý pokus včetně neúspěšných: logování je uvnitř smyčky opakování +$http = new Http\RetryClient(new Http\ObservableClient(new Http\CurlClient, onResponse: $log)); +``` + +Ani jedno není špatně, jen je dobré vědět, kterou z těch dvou variant jsi napsal. Při ladění limitů chceš to druhé, v provozním logu spíš to první. + + +Timeouty a spojení +================== + +Samotný `CurlClient` má dvě nastavení času a jedno pro proxy: + +```php +$http = (new Http\CurlClient)->setOptions(connectTimeout: 10, requestTimeout: 180); +``` + +Výchozí tři minuty stačí na běžnou konverzaci, ale ne vždycky. Generování obrázku ve vysoké kvalitě s referencemi trvá klidně několik minut, takže tam si `requestTimeout` zvedni; poznáš to podle `CommunicationException`, která přijde přesně po vypršení limitu. + +U [streamované |streaming] odpovědi platí něco jiného a stojí to za zapamatování: **celkový časový strop se nepoužívá vůbec**. Dlouhá odpověď legitimně teče minuty a useknout ji uprostřed by zahodilo text, který uživatel právě čte. Místo toho se hlídá ticho: když delší dobu nic nepřijde, spojení se vzdá. Mez je tedy "přestalo to téct", ne "trvá to dlouho". + +Spojení navíc zůstává otevřené mezi požadavky. Nejvíc se to pozná u [smyčky nástrojů |tools], která je vlastně dávkou volání na tentýž server rychle po sobě; bez toho by se pro každé kolo znovu navazovalo TLS. + + +Vlastní implementace +==================== + +Rozhraní `Http\Client` má jedinou metodu: + +```php +interface Client +{ + function fetch( + string $url, + string|array|FormData|null $payload = null, + array $headers = [], + ?string $method = null, + ?\Closure $onChunk = null, + ): Response; +} +``` + +Streamování se nepozná podle zvláštní metody, ale podle toho, jestli je zadaný `$onChunk`. Vlastní implementaci oceníš hlavně v testech, kde chceš odpovědi předepsat místo volat API, ale i tehdy, když musíš požadavky protáhnout něčím netypickým. + + +Kam dál +======= + +- [Ošetření chyb |errors] - které chyby má smysl opakovat a proč +- [Streamování |streaming] - proč se u streamu měří ticho místo času +- [Generování obrázků |images] - kde se výchozí timeout nemusí vejít +- [Provideři |providers] - co který umí a čím se liší diff --git a/ai-access/cs/images.texy b/ai-access/cs/images.texy new file mode 100644 index 0000000000..19ef290b33 --- /dev/null +++ b/ai-access/cs/images.texy @@ -0,0 +1,108 @@ +Generování obrázků +****************** + +.[perex] +Popíšeš slovy, co chceš vidět, a dostaneš obrázek. Ukážeme si, jak se generuje a ukládá, jak modelu přidat předlohu a na co narazíš, až přijde na cenu a na čekání. + + +První obrázek +============= + +Na generování stačí jediná metoda. Řekneš jí, co má nakreslit, a kterým modelem: + +```php +$client = new AIAccess\Provider\OpenAI\Client($apiKey); + +$image = $client->generateImage('Maják na útesu za bouřky, plochá vektorová ilustrace', 'gpt-image-2'); +$image->save('/cesta/k/majaku.png'); +``` + +Návratová hodnota není řetězec s adresou, ale objekt `Media` se samotnými daty obrázku. Ten už znáš z kapitoly o [obrázcích na vstupu |multimodal]; je to tentýž objekt, jen jednou putuje k modelu a podruhé od něj. + +Kromě `save()` z něj dostaneš i syrová data metodou `getData()`, pokud si obrázek chceš uložit sám třeba do databáze nebo rovnou poslat do prohlížeče. + + +Zjisti si, co ti přišlo +======================= + +Tady je první past, na kterou se dá snadno naletět: **nepředpokládej, že dostaneš PNG**. OpenAI ve výchozím nastavení posílá PNG, ale Gemini vrací JPEG. Když si natvrdo napíšeš příponu `.png`, skončíš s JPEGem uloženým pod špatným jménem. + +Typ obsahu ti obrázek řekne sám, tak ho použij: + +```php +$extension = explode('/', $image->getMimeType())[1]; +$image->save("/cesta/k/majaku.$extension"); +``` + + +Předloha místo pouhého popisu +============================= + +K popisu můžeš modelu přidat i obrázky, ze kterých má vyjít. Hodí se to na úpravy, variace jednoho motivu nebo na udržení jednotného stylu napříč sadou obrázků: + +```php +use AIAccess\Media; + +$image = $client->generateImage( + 'Stejný maják, ale za slunečného rána', + references: [Media::fromFile('/cesta/k/majaku.png')], +); +``` + +Předloh může být víc než jedna. Umí je **OpenAI a Gemini**; u Groku ten parametr vůbec neexistuje, protože xAI kreslí jen z popisu. + +Sdílené rozhraní `Image\Service` proto slibuje jen generování z popisu, tedy to, co zvládnou všichni tři. Předlohy jsou pojmenovaný argument konkrétního klienta, úplně stejně jako ostatní volby jednotlivých providerů. Kdo edituje, sáhne po `OpenAI\Client` nebo `Gemini\Client`. + + +Co který provider umí +===================== + +| Provider | Generování | Předlohy | Kde to běží | +|----------|------------|----------|-----------------------------------| +| OpenAI | ✅ | ✅ | samostatný endpoint pro obrázky | +| Gemini | ✅ | ✅ | obyčejný chat, obrázkovým modelem | +| Grok | ✅ | ➖ | samostatný endpoint pro obrázky | +| Claude | ➖ | ➖ | obrázky negeneruje | +| DeepSeek | ➖ | ➖ | obrázky negeneruje | + +Poslední sloupec stojí za vysvětlení, protože je to hezká ukázka toho, co knihovna dělá. **Gemini žádný endpoint pro obrázky nemá.** Obrázkový model se u něj oslovuje úplně stejně jako běžný chat, jen si řekne o obrázek místo textu. Ty o tom ale vědět nemusíš: `generateImage()` vypadá u všech tří providerů stejně. + +OpenAI navíc bere nepovinné parametry `size`, `quality`, `background` a `format`, kterými řekneš, jak velký a jak kvalitní obrázek chceš a jestli má mít průhledné pozadí: + +```php +$image = $client->generateImage( + 'Ikona obálky, plochý styl', + 'gpt-image-2', + size: '1024x1024', + quality: 'low', + background: 'transparent', +); +``` + + +Než to pustíš do provozu +======================== + +**Obrázky jsou řádově dražší než text.** Zatímco běžný dotaz stojí zlomky haléře, jeden obrázek ve vysoké kvalitě se počítá v korunách. Při testování proto generuj v nízké kvalitě a menším rozlišení; na ověření, že kód funguje, to bohatě stačí. + +**Když jich generuješ hodně, zvaž [dávkové zpracování |batch].** Provideři za odloženou odpověď účtují zhruba polovinu a v praxi to u obrázků nebývá pomalejší než generování po jednom, spíš naopak. Obrázky se přidávají do obyčejné dávky metodou `addImageRequest()` a umí to OpenAI a Gemini. + +**Generování trvá dlouho.** Obyčejný obrázek vznikne v řádu sekund, ale vysoká kvalita s předlohami klidně minuty. Výchozí timeout HTTP klienta je 180 sekund, což na tyhle případy nemusí stačit, takže si ho zvedni: + +```php +$http = (new AIAccess\Http\CurlClient)->setOptions(requestTimeout: 600); +$client = new AIAccess\Provider\OpenAI\Client($apiKey, $http); +``` + +**U Gemini je potřeba placený projekt.** Obrázkové modely mají na free tieru nulovou denní kvótu, takže volání skončí chybou o překročeném limitu, i když jsi ještě nic nevygeneroval. + +A jedna samozřejmost, na kterou se zapomíná: model může místo obrázku odmítnout kreslit, typicky u obsahu, který jeho pravidla nedovolují. Pak dostaneš `AIAccess\UnexpectedResponseException`, protože v odpovědi žádný obrázek není. Počítej s tím, obzvlášť když popis skládáš z uživatelského vstupu. + + +Kam dál +======= + +- [Obrázky a dokumenty na vstupu |multimodal] - opačný směr, když se má model na obrázek podívat +- [HTTP vrstva |http] - timeouty, opakování a logování požadavků +- [Ošetření chyb |errors] - co znamenají jednotlivé výjimky +- [Provideři |providers] - co který umí a čím se liší diff --git a/ai-access/cs/multimodal.texy b/ai-access/cs/multimodal.texy new file mode 100644 index 0000000000..e85f541f4d --- /dev/null +++ b/ai-access/cs/multimodal.texy @@ -0,0 +1,100 @@ +Obrázky a dokumenty na vstupu +***************************** + +.[perex] +Ke zprávě můžeš přiložit fotku nebo PDF a ptát se na jejich obsah. Ukážeme si, jak se soubor přikládá, co který provider akceptuje a proč se přiložený obrázek platí znovu v každém dalším kole konverzace. + + +Zeptej se na obrázek +==================== + +Zpráva nemusí být jen text. Když jí místo řetězce předáš pole, může v něm být kromě textu i soubor: + +```php +use AIAccess\Media; + +$chat = $client->createChat('gpt-5.6-luna'); + +$response = $chat->sendMessage([ + 'Co je na tomhle obrázku?', + Media::fromFile('/cesta/k/fotce.jpg'), +]); + +echo $response->getText(); +``` + +`Media::fromFile()` soubor načte a typ obsahu si zjistí sám, takže mu nemusíš nic říkat. Model pak vidí obrázek i otázku najednou a odpoví na obojí. + +Tímhle způsobem se dá řešit překvapivě dost práce: přečíst údaje z účtenky nebo faktury, nechat si k fotce napsat popisek pro nevidomé, přečíst chybovou hlášku ze snímku obrazovky, který ti poslal uživatel, nebo ověřit, jestli nahraná fotka opravdu ukazuje to, co má. + + +Když soubor nemáš na disku +========================== + +Obrázek často přichází z formuláře nebo z databáze a na disku vůbec není. Na to je `fromBinary()`, kterému předáš data a typ obsahu: + +```php +$media = Media::fromBinary($bytes, 'image/png'); +``` + +Jméno souboru je volitelný třetí argument. U obrázků na něm nezáleží, u dokumentů ho ale OpenAI vyžaduje a modelu ho ukazuje, takže jméno jako `faktura-2026-03.pdf` samo o sobě nese informaci. Když ho nezadáš, knihovna doplní obecné podle typu obsahu, aby požadavek nespadl. + + +Dokumenty, hlavně PDF +===================== + +Dokumenty se přikládají úplně stejně jako obrázky: + +```php +$response = $chat->sendMessage([ + 'Shrň mi hlavní body téhle smlouvy.', + Media::fromFile('/cesta/ke/smlouve.pdf'), +]); +``` + +Liší se jen tím, kteří provideři je přijmou. Model si poradí i s tabulkou nebo formulářem, tedy s tím, na čem si běžná textová extrakce v PHP vyláme zuby. PDF tedy nemusíš předem převádět na text. + + +Co který provider přijme +======================== + +| Provider | Obrázky | Dokumenty | +|----------|---------|-----------| +| OpenAI | ✅ | ✅ | +| Claude | ✅ | ✅ | +| Gemini | ✅ | ✅ | +| Grok | ✅ | ➖ | +| DeepSeek | ➖ | ➖ | + +DeepSeek zatím nemá žádný model, který by viděl; Grok obrázky přijímá, ale dokumenty ne. + +Když providerovi pošleš obsah, který neumí zpracovat, **nedozvíš se to až z chyby API**. Knihovna to pozná ještě před odesláním a vyhodí `AIAccess\LogicException` se jménem typu obsahu, takže víš rovnou, co bylo špatně: + +``` +DeepSeek cannot send image/png content: it has no vision model. +``` + +Tuhle výjimku nemá smysl odchytávat a požadavek opakovat. Říká, že posíláš obrázek modelu, který nevidí, a to žádné další odeslání nespraví; oprava patří do kódu, třeba volbou jiného providera. + + +Obrázek v historii se platí znovu +================================= + +Tohle je nejčastější nepříjemné překvapení na účtu. Přiložený obrázek se stane součástí historie konverzace, a protože se s každým dalším dotazem posílá celá historie znovu, **posílá a platí se znovu i ten obrázek**. + +Změřeno na jednoduchém příkladu: první kolo s obrázkem stálo 49 vstupních tokenů, druhé kolo, ve kterém už šlo jen o textovou doplňující otázku, jich stálo 64. Rozdíl nedělá ta krátká otázka, ale obrázek poslaný podruhé. + +U velkých obrázků a delších konverzací to roste rychle. Dají se s tím dělat tři věci: + +- **Zeptej se na obrázek najednou.** Když víš, co všechno z něj potřebuješ, zeptej se na to v jedné zprávě místo v pěti. +- **Po vytěžení obrázku začni nanovo.** Odpověď modelu si ulož a konverzaci vyčisti přes `clearMessages()`; dál pokračuj nad textem, který už obrázek popisuje. +- **Zmenši obrázek předem.** Cena roste s rozlišením, a na otázku "je na faktuře razítko" stačí menší obrázek než na čtení drobného písma. + + +Kam dál +======= + +- [Generování obrázků |images] - opačný směr, když má obrázek vzniknout +- [Konverzace |chat] - jak historie funguje a proč roste +- [Strukturovaný výstup |structured-output] - když z účtenky potřebuješ rovnou data +- [Provideři |providers] - co který umí a čím se liší diff --git a/ai-access/cs/options.texy b/ai-access/cs/options.texy new file mode 100644 index 0000000000..ac15d930c3 --- /dev/null +++ b/ai-access/cs/options.texy @@ -0,0 +1,97 @@ +Nastavení a reasoning effort +**************************** + +.[perex] +Nejdůležitější věc, kterou dnes u modelu nastavuješ, je kolik si toho má rozmyslet, než začne odpovídat. Ovlivňuje to kvalitu, rychlost i cenu. Ukážeme si, jak se to řídí jedním ovladačem napříč všemi providery, kde najdeš zbytek nastavení, a na konec se podíváme, co se stalo s parametrem `temperature`, o kterém možná někde čteš. + + +Kolik si model rozmyslí, než odpoví +=================================== + +Novější modely umí něco, co ty starší neuměly: než začnou psát odpověď, napíšou si stranou jakýsi koncept úvahy. Rozeberou si zadání, zkusí postup, najdou v něm chybu, opraví ji, a teprve pak odpovědí. Tomuhle konceptu se říká reasoning nebo thinking, česky prostě přemýšlení. Pokud tě zajímá, co se při něm uvnitř modelu děje, rozebírá to článek [Reasoning modely: co to je |https://www.umeligence.cz/blog/reasoning-modely-co-to-je]. + +Dvě věci o něm potřebuješ vědět. **Do samotné odpovědi se nedostane**, takže ho `getText()` nikdy neobsahuje; někteří provideři ho ale vracejí zvlášť, u některých jen jako shrnutí, a [přečteš si ho |chat#Co všechno je v odpovědi] přes `getReasoning()`. A hlavně **ho platíš**, protože se počítá jako výstupní tokeny, kterých bývá u složitější úlohy víc než v samotné odpovědi. + +Kolik toho model má promyslet, řekneš metodou `setEffort()`: + +```php +use AIAccess\Chat\Effort; + +$chat = $client->createChat('gpt-5.6-luna'); +$chat->setEffort(Effort::Low); + +echo $chat->sendMessage('Do které kategorie patří tato reklamace?')->getText(); +``` + +Stupňů je šest: `None`, `Low`, `Medium`, `High`, `XHigh` a `Max`. `None` znamená, že model nemá přemýšlet vůbec a má odpovědět rovnou. + +Každý provider si hodnotu přeloží do svého, protože se ani v pojmenování neshodnou: + +| Provider | Jak se to jmenuje u něj | +|----------|-----------------------------------------------------------| +| Claude | `output_config.effort`, `None` navíc vypne `thinking` | +| OpenAI | `reasoning.effort` | +| Gemini | `thinkingConfig.thinkingLevel`, `None` je nulový rozpočet | +| DeepSeek | `thinking.reasoning_effort` | +| Grok | `reasoning_effort` | + +Někteří provideři neznají všech šest stupňů, takže se mapují na nejbližší. Gemini má jen tři úrovně, takže `High`, `XHigh` i `Max` u něj skončí stejně. + +Dvě zásady stojí za vyslovení. Zaprvé, **dokud `setEffort()` nezavoláš, neposílá se nic** a platí výchozí nastavení providera. Právě proto DeepSeek přemýšlí, i když jsi o to nežádal. Zadruhé, **knihovna si nedrží tabulku schopností modelů**. Když model ovladač nemá, provider odpoví chybou, a to je správně; udržovat seznam toho, co který model zrovna umí, by znamenalo dokumentaci, která zastará dřív, než ji dopíšeš. + + +Který stupeň si vybrat +====================== + +Volba stupně není kosmetická, protože přímo určuje, jak dlouho se na odpověď čeká a kolik stojí. + +- **`None`** na úlohy, kde není co promýšlet: klasifikace, vytažení údajů z textu, přeformulování věty, překlad. Odpověď přijde nejrychleji a nejlevněji. +- **`Low` a `Medium`** na běžnou práci, kde model potřebuje chvíli přemýšlet, ale ne dlouho: shrnutí delšího textu, návrh odpovědi, jednodušší rozhodování. +- **`High` a výš** na úlohy o více krocích: rozbor kódu, matematika, plánování, uvažování nad protichůdnými informacemi. Počítej s tím, že odpověď přijde později a bude výrazně dražší. + +Nejlevnější optimalizace bývá zjistit, že úloha si vystačí s `None`. Rozdíl v ceně mezi vypnutým a maximálním přemýšlením bývá větší než rozdíl mezi dvěma modely. + + +Nastavení, která má každý provider svoje +======================================== + +Zbytek nastavení sjednocený není, protože sjednocený být nemůže: `store` má jen OpenAI, `safetySettings` jen Gemini, `seed` jen Grok. Proto jsou to **pojmenované argumenty metody `setOptions()`** na konkrétní třídě providera, ne klíče ve sdíleném poli. + +```php +// Claude +$chat->setOptions(maxOutputTokens: 1024, stopSequences: ['KONEC']); + +// OpenAI +$chat->setOptions(maxOutputTokens: 1024, store: false, parallelToolCalls: true); +``` + +Rozdíl proti poli poznáš hned při psaní. IDE ti nabídne přesně to, co daný provider zná, a překlep zachytí PHP samo. Sdílené pole by klíč, který nikam nepatří, tiše spolklo a ty by ses to dozvěděl leda tak, že by se nic nedělo. + +Nejužitečnější z nich je `maxOutputTokens`, tedy strop na délku odpovědi. Jmenuje se stejně u všech providerů, protože ho potřebuje každý; na drátě se přitom pokaždé jmenuje jinak, jednou `max_tokens`, jindy `max_completion_tokens`, ale to už je věc knihovny. + +Totéž platí pro `stopSequences`, tedy řetězce, po kterých má model přestat psát. Na drátě je to jednou `stop_sequences`, jindy `stop`, u knihovny všude, kde vůbec existuje, `stopSequences`. Přijme jeden řetězec i pole, takže na jednu sekvenci pole psát nemusíš. Jediný, kdo ji nemá, je OpenAI: Responses API takový parametr nezná. + +Kompletní seznam pro každého providera najdeš v signatuře `setOptions()` v `src/Provider/*/Chat.php`, případně ti ho ukáže IDE. A pokud používáš [generického klienta |providers] pro cizí endpoint, má navíc argument `custom`, kterým protlačíš cokoli, co ten endpoint zná a knihovna ne. + + +Co se stalo s temperature +========================= + +Když někde čteš o nastavování modelů, skoro jistě narazíš na parametr `temperature`. Stojí za to vědět, co dělal a proč ho tahle dokumentace nedoporučuje. + +Model nevybírá jedno jediné správné pokračování věty. V každém okamžiku má seznam slov, která by mohla přijít, a ke každému pravděpodobnost; z nich pak jedno vylosuje. Právě proto dostaneš na tutéž otázku pokaždé trochu jinou odpověď. `temperature` určovala, jak riskantní to losování bude: hodnota kolem nuly znamenala střízlivého a předvídatelného pisatele, který skoro vždy sáhne po nejpravděpodobnějším slově, vyšší hodnoty nápaditější text, ale i víc nepřesností. Podobnou věc dělaly jinak parametry `top_p` a `top_k`. + +Uvažovací modely tenhle způsob řízení opustily. Když jim `temperature` přesto pošleš, jedni odpoví chybou HTTP 400, což dělá Claude na nejnovějších modelech a OpenAI od GPT-5.1, a druzí ji **tiše ignorují**, což dělá Gemini a DeepSeek pokaždé, když přemýšlí. + +Nebezpečné je to druhé. Chyba tě aspoň upozorní; tiše ignorovaný parametr znamená, že aplikace vypadá funkčně, ty ladíš hodnoty a nemá to vůbec žádný účinek. + +Knihovna ti `temperature` nezakazuje a na starších modelech ji klidně použij přes `setOptions()`. Jen s ní nestav nic, co má vydržet: na modelech, které vyjdou příští rok, s velkou pravděpodobností nebude fungovat vůbec. + + +Kam dál +======= + +- [Streamování |streaming] - odpověď čti, zatímco ji model teprve píše +- [Volání nástrojů |tools] - když má model sáhnout do tvé aplikace +- [Strukturovaný výstup |structured-output] - když potřebuješ data, ne prózu +- [Provideři |providers] - co který umí a čím se liší diff --git a/ai-access/cs/providers.texy b/ai-access/cs/providers.texy new file mode 100644 index 0000000000..49f80c6719 --- /dev/null +++ b/ai-access/cs/providers.texy @@ -0,0 +1,183 @@ +Provideři a modely +****************** + +.[perex] +Referenční přehled šesti klientů: jak se který vytváří, co umí, čím se liší a na co si u něj dát pozor. Na konci se podíváme, jak si vyžádat seznam modelů, které provider právě nabízí, a proč je to užitečnější, než se zdá. + + +Šest klientů, jedno rozhraní +============================ + +Pět klientů mluví s konkrétním providerem, šestý s čímkoli, co mluví stejným dialektem jako OpenAI. Všechny se vytvářejí stejně, klíčem v konstruktoru: + +```php +$client = new AIAccess\Provider\OpenAI\Client($apiKey); +$client = new AIAccess\Provider\Claude\Client($apiKey); +$client = new AIAccess\Provider\Gemini\Client($apiKey); +$client = new AIAccess\Provider\DeepSeek\Client($apiKey); +$client = new AIAccess\Provider\Grok\Client($apiKey); +``` + +Čím se liší, je sada rozhraní, kterou každý klient implementuje. Právě podle ní poznáš, co od něj můžeš čekat, a PHP ti to pohlídá dřív, než skript spustíš: + +| Klient | `Chat\Service` | `Embedding\Service` | `Batch\Service` | `Image\Service` | +|-----------|----------------|---------------------|-----------------|-----------------| +| OpenAI | ✅ | ✅ | ✅ | ✅ | +| Gemini | ✅ | ✅ | ✅ | ✅ | +| Claude | ✅ | ➖ | ✅ | ➖ | +| Grok | ✅ | ➖ | ➖ | ✅ | +| DeepSeek | ✅ | ➖ | ➖ | ➖ | +| Generický | ✅ | ➖ | ➖ | ➖ | + +Když píšeš kód, který má fungovat s libovolným providerem, typuj si parametr na rozhraní, ne na konkrétní třídu: + +```php +public function __construct( + private AIAccess\Chat\Service $client, +) { +} +``` + +Aplikace pak o volbě providera neví vůbec nic a přepnutí je změna v [konfiguraci |dependency-injection:]. + + +Výchozí modely +============== + +Aby to platilo doopravdy, musí klient znát i model. Jméno modelu je totiž řetězec vlastní jednomu provideru, takže kdyby si o něj muselo říct volání, aplikace by o provideru věděla to hlavní. + +Modely se proto předávají konstruktorem, hned vedle klíče: + +```php +$client = new AIAccess\Provider\OpenAI\Client( + $apiKey, + chatModel: 'gpt-5.6-luna', + imageModel: 'gpt-image-2', + embeddingModel: 'text-embedding-3-small', +); + +$chat = $client->createChat(); // model doplní klient +$image = $client->generateImage('Maják na útesu'); +``` + +Každý klient bere jen ty modely, které umí použít: Claude a DeepSeek jenom `chatModel`, Grok `chatModel` a `imageModel`, OpenAI a Gemini všechny tři. Neexistující parametr ohlásí PHP samo. + +Model uvedený ve volání má vždycky přednost, takže výchozí hodnota nikomu nebrání sáhnout jinam. A klient, který výchozí model nemá a nedostane ho ani ve volání, ohlásí `AIAccess\LogicException`; je to chyba konfigurace, ne provozu. + +Dávka si výchozí modely bere od klienta, který ji vytvořil, takže `addChat('id')` a `addImageRequest('id', $popis)` se obejdou bez nich taky. + +V Nette aplikaci se tak celá volba providera vejde do konfigurace a je to jediné místo v projektu, kde se jeho jméno objeví: + +```neon +services: + - AIAccess\Provider\OpenAI\Client(%openaiApiKey%, chatModel: 'gpt-5.6-luna') +``` + +Přechod k jinému providerovi je pak otázka toho jednoho řádku a třídy typované na `AIAccess\Chat\Service` si toho ani nevšimnou: + +```neon +services: + - AIAccess\Provider\Claude\Client(%anthropicApiKey%, chatModel: 'claude-sonnet-5') +``` + +Klient, který umí víc věcí, dostane model pro každou z nich: + +```neon +services: + - AIAccess\Provider\OpenAI\Client( + %openaiApiKey%, + chatModel: 'gpt-5.6-luna', + imageModel: 'gpt-image-2', + embeddingModel: 'text-embedding-3-small', + ) +``` + +Modely piš vždycky jménem, jako je tomu tady. Druhý parametr konstruktoru je totiž [HTTP klient |http], ne model, takže model předaný jako druhý v pořadí by se bral za něj. + + +Čím se který liší +================= + +Sada rozhraní je jen půlka příběhu. Tohle jsou vlastnosti, na které narazíš v praxi. + +**OpenAI** má nejširší záběr a jako jediný umí nahrávat soubory (`uploadFile()`, `uploadContent()`), což využívá dávkové zpracování. Odmítnutí odpovědi hlásí zvlášť, takže se k němu dostaneš přes `getRefusal()`. Přes `setOptions()` mu můžeš nastavit i organizaci. + +**Claude** vyžaduje strop na délku odpovědi u každého požadavku; knihovna proto sama doplňuje `maxOutputTokens` na 4096, pokud si ho nenastavíš jinak. Embeddingy Anthropic nenabízí vůbec, takže na vyhledávání budeš potřebovat jiného providera. Umí spočítat tokeny předem přes `countTokens()`. + +**Gemini** nemá na generování obrázků samostatný endpoint: obrázkové modely se volají přes obyčejný chat, jen se řekne, že odpověď má být obrázek. Taky umí `countTokens()`. **Dávkové zpracování a generování obrázků ale vyžadují projekt s aktivní fakturací**; na volném tarifu dávka skončí chybou a obrázkové modely mají denní kvótu nula. Vygenerovaný obrázek přichází jako JPEG, ne PNG. + +**Grok** přijímá obrázky na vstupu, ale dokumenty ne. Obrázky umí i generovat, zato bez referenčních předloh. Odmítnutí hlásí vlastním polem zprávy místo důvodu ukončení, což knihovna překládá na `FinishReason::ContentFiltered`. + +**DeepSeek** je z pětice nejužší: jen chat, žádné vidění, embeddingy ani dávky. Za zmínku stojí, že **přemýšlení má zapnuté ve výchozím stavu**, takže platíš tokeny navíc, dokud ho [nevypneš |options]. + + +Generický klient pro všechno ostatní +==================================== + +Spousta služeb dnes mluví stejným dialektem jako OpenAI. Na ty je tu `OpenAICompatible\Client`, kterému kromě klíče řekneš i adresu: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://api.mistral.ai/v1/'); +$response = $client->createChat('mistral-large-latest')->sendMessage('Ahoj!'); +``` + +**Ollama** běžící lokálně žádný klíč nechce, tak mu pošli prázdný řetězec: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client('', 'http://localhost:11434/v1/'); +echo $client->createChat('llama3.2')->sendMessage('Ahoj!')->getText(); +``` + +**OpenRouter** zprostředkovává modely desítek výrobců a rád vidí hlavičky s identifikací aplikace: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://openrouter.ai/api/v1/'); +$client->setOptions(extraHeaders: ['HTTP-Referer' => 'https://example.com', 'X-Title' => 'Moje aplikace']); +``` + +**Azure OpenAI** posílá klíč ve vlastní hlavičce a bez prefixu: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://mojeinstance.openai.azure.com/openai/v1/'); +$client->setOptions(authHeader: 'api-key', authPrefix: ''); +``` + +U generického klienta platí jedno omezení, které stojí za zmínku: **knihovna posílá, co umí, ale co s tím endpoint udělá, nezaručí**. Volání nástrojů, strukturovaný výstup i obrázky na vstupu odešle, jenže jestli je model zvládne, rozhoduje služba. Když endpoint zná nějaký parametr navíc, protlačíš ho argumentem `custom` v [nastavení |options]. + +Streamování z generického klienta neposílá `stream_options`, protože je to vynález OpenAI a neznámý dialekt kvůli němu může odmítnout celý požadavek. Když ho tvůj endpoint zná a chceš z proudu i spotřebu tokenů, přidej si ho přes `custom`. + + +Jaké modely provider právě nabízí +================================= + +Protože je jméno modelu obyčejný řetězec, nikdo ti ho nezkontroluje: překlep i vyřazený model se ozvou až za běhu, a jak uvidíš za chvíli, někdy ani to ne. Aktuální nabídku si proto vyžádej přímo u providera: + +```php +foreach ($client->listModels() as $model) { + echo $model->id, "\n"; +} +``` + +Metodu má všech šest klientů a stránkování řeší uvnitř, takže dostaneš rovnou celý seznam. Kromě `id` nese každý model v `raw` i všechna metadata tak, jak je provider poslal; sjednocené nejsou, protože každý posílá něco jiného. + +Nejužitečnější je to jako kontrola v nasazení. **Vyřazený model se totiž nemusí ozvat chybou:** xAI například u starých jmen tiše přesměruje na novější model, takže aplikace běží dál, jen mluví s něčím jiným, než sis myslel. Ověření proti `listModels()` je jediný způsob, jak se to dozvíš. + +Claude a Gemini navíc umí spočítat, na kolik tokenů vyjde konverzace, **než ji odešleš**: + +```php +$chat = $client->createChat('claude-sonnet-5'); +$chat->addMessage($dlouhyText, AIAccess\Chat\Role::User); + +echo 'Tenhle dotaz bude stát ', $chat->countTokens(), " vstupních tokenů.\n"; +``` + +Hodí se to, když skládáš dlouhý kontext a potřebuješ vědět, jestli se vejde do limitu modelu, nebo kolik to bude stát, dřív než za to zaplatíš. + + +Kam dál +======= + +- [Konverzace |chat] - historie, systémová instrukce a čtení odpovědi +- [Nastavení a reasoning effort |options] - kolik přemýšlení si od modelu vyžádáš +- [Ošetření chyb |errors] - co dělat, když provider řekne ne +- [HTTP vrstva |http] - opakování, logování a cachování požadavků diff --git a/ai-access/cs/streaming.texy b/ai-access/cs/streaming.texy new file mode 100644 index 0000000000..1200dc4595 --- /dev/null +++ b/ai-access/cs/streaming.texy @@ -0,0 +1,135 @@ +Streamování odpovědí +******************** + +.[perex] +Model píše odpověď slovo po slovu a trvá to i několik desítek sekund. Streamování znamená, že ji čteš průběžně, místo abys čekal na celou. Ukážeme si, jak se to dělá jedním `foreach`, jak generování zastavit v půlce a proč to v PHP dává smysl víc, než by se zdálo. + + +Deset sekund ticha +================== + +Bez streamování se stane tohle: uživatel odešle dotaz, stránka zamrzne a deset sekund se neděje vůbec nic. Pak najednou naskočí celý text. Deset sekund ticha je v prohlížeči věčnost a uživatel mezitím stihne kliknout znovu nebo odejít. + +Streamovaná odpověď se čte cyklem, přesně jako pole: + +```php +$stream = $chat->sendMessageStream('Vysvětli třemi větami, proč je PHP pořád všude.'); + +foreach ($stream as $delta) { + echo $delta; + flush(); +} +``` + +Do `$delta` přichází pokaždé kousek textu, který právě dorazil, typicky slovo nebo jeho část. `flush()` je tam proto, aby PHP kousky opravdu poslalo ven a nedrželo si je ve výstupní vyrovnávací paměti. + +Odpověď tím nepřijde dřív. Změní se ale čekání: místo prázdné stránky uživatel sleduje, jak text přibývá, a to je rozdíl mezi aplikací, která vypadá rozbitě, a aplikací, která vypadá rychle. + +Jedna vlastnost stojí za zapamatování: **dokud nezačneš číst, neodešle se nic**. Samotné zavolání `sendMessageStream()` žádný požadavek nevyvolá, ten se rozjede až prvním průchodem cyklu. Stream si tedy můžeš připravit dopředu a přečíst ho, až se to hodí. + +Když se ti víc hodí callback než cyklus, existuje i druhá cesta: + +```php +$chat->sendMessage( + 'Vysvětli třemi větami, proč je PHP pořád všude.', + onStream: function (string $delta) { + echo $delta; + flush(); + }, +); +``` + + +Proč to v PHP dává smysl víc, než by se zdálo +============================================= + +V prohlížeči je přínos zřejmý, u serverového skriptu už méně. Důvody jsou tři a stojí za vyjmenování, protože se na ně snadno zapomene. + +**Můžeš stream posílat rovnou do prohlížeče.** Když frontend poslouchá Server-Sent Events, předáváš mu jednotlivé kousky, jak přicházejí. Kdybys je nejdřív celé posbíral a odeslal naráz, celý smysl by se ztratil. + +**Můžeš přestat platit v půlce.** Jakmile víš, že odpověď je špatná nebo že ti stačí, co už přišlo, generování zastavíš a zbytek nevznikne ani se nenaúčtuje. + +**Rozhoduje doba do prvního slova, ne celková.** U všeho, na co se dívá člověk, je vnímaná rychlost důležitější než změřená. + + +Když stream skončí, je to obyčejná odpověď +========================================== + +Po dočtení máš k dispozici všechno, co bys dostal i bez streamování: spotřebu, důvod ukončení i případná volání nástrojů. + +```php +foreach ($stream as $delta) { + echo $delta; +} + +$response = $stream->getResponse(); + +echo 'skončilo jako ', $response->getFinishReason()->value, "\n"; +echo 'výstupních tokenů: ', $response->getUsage()?->outputTokens, "\n"; +``` + +`getResponse()` **neposílá druhý požadavek**. Když jsi stream dočetl, jen ti vrátí hotový výsledek; když jsi ho nedočetl, potichu dočte zbytek a vrátí ho celý. Nikdy se tedy nestane, že bys tutéž odpověď zaplatil dvakrát. + +A pokud tě průběžné kousky vlastně nezajímají a šlo ti jen o to, aby uživatel viděl, že se něco děje, máš zkratku: + +```php +echo $stream->getText(); +``` + +Streamovaná odpověď se stejně jako každá jiná zapíše do [historie konverzace |chat], takže na ni další zpráva navazuje bez tvého přičinění. + + +Zastavení uprostřed +=================== + +Streamování ti dává možnost, kterou jinak nemáš: přestat, když už víš dost. V cyklu k tomu slouží metoda `cancel()`: + +```php +foreach ($stream as $delta) { + echo $delta; + if (str_contains($delta, 'KONEC')) { + $stream->cancel(); + break; + } +} +``` + +V callbackové variantě uděláš totéž návratovou hodnotou `false`: + +```php +$chat->sendMessage($otazka, onStream: function (string $delta) { + echo $delta; + return !str_contains($delta, 'KONEC'); // false generování ukončí +}); +``` + +Jak se to řekne modelu? Nijak, a právě v tom je ta finta. Knihovna **přeruší probíhající HTTP přenos**, čímž se spojení k providerovi zavře. Provider zjistí, že klient už neposlouchá, a generování ukončí; zbytek odpovědi tedy nevznikne a nenaúčtuje se. Odpověď pak hlásí `FinishReason::Cancelled`, takže i o kus dál v kódu poznáš, že text není úplný. Zastaví to i [smyčku volání nástrojů |tools], protože nedočtená odpověď není podklad k tomu, aby aplikace něco vykonala. Platí to i pro volání, které se přeruší uprostřed argumentů: takový úlomek knihovna zahodí, místo aby ti nástroj spustila s tím, co stihlo dorazit. + +**Samotný `break` naproti tomu generování nezastaví.** Ukončí jen tvoje čtení, ale požadavek zůstává otevřený a model píše dál. To je záměr, ne opomenutí: díky tomu se k proudu můžeš vrátit dalším `foreach`, který naváže tam, kde jsi přestal, nebo si zavolat `getResponse()`, který zbytek dočte bez druhého požadavku. `break` je tedy pauza, kdežto `cancel()` je konec. + + +Pět providerů, pět způsobů, jak stream skončí +============================================= + +Tohle při běžném používání vědět nepotřebuješ, ale hezky ukazuje, kolik práce se pod jedním `foreach` skrývá. Zjišťovalo se to měřením na skutečných odpovědích, ne čtením dokumentace. + +| Provider | Pojmenovává události | Posílá značku `[DONE]` | +|----------|----------------------|------------------------| +| Claude | ✅ | ➖ | +| OpenAI | ✅ | ➖ | +| Gemini | ➖ | ➖ | +| DeepSeek | ➖ | ✅ | +| Grok | ➖ | ✅ | + +Žádný obecný signál "konec" tedy neexistuje. Claude končí událostí `message_stop`, OpenAI závěrečnou událostí, která nese celou odpověď, Gemini prostě přestane posílat a dvojice DeepSeek s Grokem použije značku. Kousky k tomu přicházejí rozdělené tak, jak je zrovna rozsekala síť, takže jedna událost běžně dorazí na dvakrát. Knihovna z toho poskládá přesně tentýž tvar odpovědi, jaký by přišel bez streamování, takže se ti obě cesty nemůžou rozejít. + +Ještě jedna změřená vlastnost, tentokrát praktická: **stream není omezený celkovým časem, ale tichem**. Běžný požadavek má strop na celkovou dobu, jenže dlouhá odpověď legitimně teče minuty, takže by ji takový strop uťal uprostřed. Knihovna proto u streamu hlídá jen to, jestli data pořád chodí, a vzdá se, teprve když provider přestane mluvit úplně. + + +Kam dál +======= + +- [Volání nástrojů |tools] - když má model sáhnout do tvé aplikace +- [Strukturovaný výstup |structured-output] - když potřebuješ data, ne prózu +- [HTTP vrstva |http] - opakování, logování a časové limity +- [Ošetření chyb |errors] - co dělat, když provider řekne ne diff --git a/ai-access/cs/structured-output.texy b/ai-access/cs/structured-output.texy new file mode 100644 index 0000000000..1e975ac2fc --- /dev/null +++ b/ai-access/cs/structured-output.texy @@ -0,0 +1,201 @@ +Strukturovaný výstup a JSON schéma +********************************** + +.[perex] +Někdy od modelu nepotřebuješ větu, ale data, se kterými bude aplikace dál pracovat: jméno, částku, datum, kategorii. Říct si o ně v promptu nemusí stačit, protože model občas přidá úvodní větu nebo odpověď obalí do markdownu. Spolehlivé je předepsat tvar odpovědi schématem, které pohlídá provider. Zdrojem přitom nemusí být jen text: schéma se dá stejně dobře nasadit na [vyfocenou účtenku nebo přiložené PDF |multimodal]. + + +Co si řekneš v promptu, model dodržet nemusí +============================================ + +Řekněme, že z e-mailu potřebuješ vytáhnout jméno objednatele a částku. První nápad je říct si o to rovnou v promptu: + +```php +$chat->sendMessage('Vrať jméno a částku z tohoto e-mailu jako JSON: ...'); +``` + +Devětkrát z deseti to funguje. Podesáté dostaneš tohle: + +/-- +Jistě! Zde jsou požadovaná data: + +```json +{"jmeno": "Jan Novák", "castka": 1500} +``` +\-- + +Odpověď je věcně správná, ale `json_decode()` na ní selže, protože kolem je věta a markdownový blok. Jindy model klíč pojmenuje `name` místo `jmeno`, jindy vrátí částku jako řetězec `"1500 Kč"`. V testech to nepoznáš, protože ve větší části případů to projde; poznáš to až v provozu, obvykle na datech, která nikdo nečekal. + +Pokyn v promptu je totiž přání, ne záruka. A na procesu, který má běžet bez dozoru, se přání staví špatně. + + +Schéma místo pokynů +=================== + +Místo pokynu tvar rovnou předepíšeš. Metodě `setResponseSchema()` předáš [JSON schéma |https://json-schema.org] a provider se postará, aby ho model dodržel: + +```php +$chat = $client->createChat('gpt-5.6-luna'); + +$chat->setResponseSchema([ + 'type' => 'object', + 'properties' => [ + 'jmeno' => ['type' => 'string', 'description' => 'Jméno objednatele'], + 'castka' => ['type' => 'number', 'description' => 'Částka v korunách bez měny'], + ], + 'required' => ['jmeno', 'castka'], + 'additionalProperties' => false, +]); + +$response = $chat->sendMessage('Vytáhni data z tohoto e-mailu: ...'); +$data = $response->getJson(); + +echo $data['jmeno'], ' zaplatí ', $data['castka'], " Kč\n"; +``` + +`getJson()` vrátí rovnou dekódované pole, takže žádné odstraňování markdownu ani `json_decode()` navíc. Kolem odpovědi už nic není, protože formát je pevně daný a na úvodní větu v něm není místo. + +Rozdíl proti promptu není v tom, že by model pokyn najednou lépe respektoval. Je v tom, že **schéma hlídá provider**, ne dobrá vůle modelu. + + +Jak schéma napsat +================= + +JSON schéma je popis tvaru dat v JSONu a pro běžné použití si vystačíš se třemi věcmi. + +`type` říká, o co jde: `object` pro strukturu s klíči, `array` pro seznam, dále `string`, `number`, `integer` a `boolean`. `properties` vyjmenuje jednotlivé klíče objektu a ke každému znovu jeho `type`. `required` je seznam klíčů, které musí přijít vždycky; co v něm není, model vynechat může. + +Tři rady, které se vyplatí: + +- **Popisuj pole.** `description` u každého klíče model čte a řídí se jím. Rozdíl mezi "částka" a "částka v korunách bez měny a bez mezer" je rozdíl mezi `"1 500 Kč"` a `1500`. +- **Uzavřené seznamy dělej přes `enum`.** Když má být kategorie jedna ze tří, napiš `['type' => 'string', 'enum' => ['reklamace', 'dotaz', 'spam']]`. Model pak nevymyslí čtvrtou. +- **Počítej s přísným režimem.** OpenAI, Grok i generický klient dostávají schéma se zapnutým `strict`, a ten vyžaduje `additionalProperties: false` a **všechny klíče v `required`**; jinak požadavek skončí `ApiException`. Claude a Gemini jsou v tomhle volnější a nepovinné klíče snesou. Když má být údaj volitelný napříč providery, nech ho v `required` a povol mu prázdnou hodnotu nebo `null` přes `'type' => ['string', 'null']`, ať ho model nemusí vymýšlet. + +Vnořovat lze libovolně, takže seznam objektů vypadá takhle: + +```php +$chat->setResponseSchema([ + 'type' => 'object', + 'properties' => [ + 'polozky' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'nazev' => ['type' => 'string'], + 'pocet' => ['type' => 'integer'], + ], + 'required' => ['nazev', 'pocet'], + 'additionalProperties' => false, + ], + ], + ], + 'required' => ['polozky'], + 'additionalProperties' => false, +]); +``` + + +Schéma přes Nette Schema +======================== + +Když máš v projektu [Nette Schema |schema:], můžeš schéma napsat přes `Expect::` místo pole. Je to kratší a dostaneš víc než jen tvar odeslaný providerovi: totéž schéma pak odpověď na straně PHP zvaliduje a přetypuje na to, co popisuje. + +```php +use Nette\Schema\Expect; + +$chat->setResponseSchema(Expect::structure([ + 'jmeno' => Expect::string()->required()->description('Jméno objednatele'), + 'castka' => Expect::float()->min(0)->required()->description('Částka v korunách bez měny'), + 'kategorie' => Expect::anyOf('reklamace', 'dotaz', 'spam')->required(), + 'poznamka' => Expect::string()->nullable()->required(), +])); + +$response = $chat->sendMessage('Vytáhni data z tohoto e-mailu: ...'); +$data = $response->getJson(); + +echo $data->jmeno, ' zaplatí ', $data->castka, " Kč\n"; +``` + +`getJson()` pak nevrací dekódované pole, ale to, co schéma vydá: standardně objekt `stdClass`, s `castTo('array')` pole, nebo instanci tvé třídy. Nejkratší cesta k ní je `Expect::from()`, které udělá schéma z typovaných vlastností třídy: + +```php +final class Objednavka +{ + public function __construct( + public string $jmeno, + public float $castka, + public ?string $poznamka = null, + ) { + } +} + +$chat->setResponseSchema(Expect::from(new Objednavka('', 0.0))); +$objednavka = $chat->sendMessage('...')->getJson(); // instance Objednavka +``` + +Co doputuje k modelu: `description()`, `min()` a `max()`, `pattern()`, `anyOf()` s hodnotami jako `enum`, `nullable()` jako `null` v typu, `listOf()` a vnořené `structure()`. Co zůstane na straně PHP a proběhne až na odpovědi: `assert()`, `transform()` a `castTo()`. Odpověď, kterou schéma odmítne, je `AIAccess\UnexpectedResponseException` s jeho hláškami, stejně jako nevalidní JSON. + +Dvě věci je dobré vědět. Přísný režim OpenAI, Groku a generického klienta chce každý klíč povinný, takže `setResponseSchema()` Nette schéma s nepovinným klíčem odmítne rovnou výjimkou `LogicException`; nepovinnou hodnotu piš jako `->nullable()->required()`, jako `poznamka` výše. A třída jako typ, třeba `Expect::type(DateTime::class)`, nemá v JSONu protějšek a je odmítnuta také: znamená "instanci", kterou žádný model poslat nemůže. Napiš `Expect::string()->castTo(DateTime::class)` a přetypování proběhne, až odpověď dorazí. + + +Když odpověď nepřijde podle plánu +================================= + +Schéma zaručí tvar odpovědi, ale ne to, že odpověď vůbec přijde. Dvě situace stojí za ošetření. + +**Model může odmítnout odpovědět.** Bezpečnostní filtr nezmizí tím, že jsi předepsal tvar; text pak bude prázdný a `getJson()` vrátí `null`. Poznáš to podle [důvodu ukončení |chat#Proč model přestal psát]: + +```php +use AIAccess\Chat\FinishReason; + +if ($response->getFinishReason() !== FinishReason::Complete) { + // odmítnutí nebo useknutá odpověď, data nečekej +} +``` + +**Odpověď nemusí být platný JSON.** Stane se to zřídka, typicky když dojde limit tokenů uprostřed a JSON zůstane neuzavřený. `getJson()` v takovém případě vyhodí `AIAccess\UnexpectedResponseException`, takže se to nepozná až o dvě vrstvy dál: + +```php +try { + $data = $response->getJson(); +} catch (AIAccess\UnexpectedResponseException $e) { + // odpověď nešla dekódovat, syrový text je v getText() +} +``` + + +Kdo to umí a co s DeepSeekem +============================ + +Schéma zvládnou **OpenAI, Claude, Gemini a Grok** i [generický klient |providers]. DeepSeek tuhle možnost nemá, a tak u něj `setResponseSchema()` vyhodí `AIAccess\LogicException` a odkáže tě na JSON mód. Kdyby schéma odeslalo, endpoint odpoví chybou "This response_format type is unavailable now". + +JSON mód nezaručí tvar, ale aspoň zaručí, že odpověď bude platný JSON bez markdownu kolem: + +```php +$chat->setOptions(responseFormat: ['type' => 'json_object']); +``` + +Pozor na jednu past: DeepSeek požaduje, aby se slovo "json" objevilo někde v konverzaci, jinak požadavek odmítne. Stačí ho zmínit v instrukci nebo v dotazu. + +Tvar si pak musíš popsat v promptu a výsledek zkontrolovat sám. Totéž nastavení mají i Grok a generický klient, ale tam sáhni radši po schématu. + + +Schéma, nebo nástroj? +===================== + +Obojí modelu předepisuje JSON podle schématu, takže se to plete. Rozdíl je v tom, kdo komu co dává. + +- **Strukturovaný výstup je tvar odpovědi.** Model dopoví a ty dostaneš data. Použij ho, když od modelu chceš výsledek: vytáhnout údaje z textu, zařadit do kategorie, rozdělit adresu na části. +- **[Volání nástroje |tools] je otázka směrem k tobě.** Model se zastaví a čeká, až mu něco zjistíš, pak pokračuje. Použij ho, když model potřebuje informaci nebo akci, kterou má tvoje aplikace. + +Zjednodušeně: strukturovaný výstup je odpověď, nástroj je otázka. + + +Kam dál +======= + +- [Volání nástrojů |tools] - když se model potřebuje zeptat tvé aplikace +- [Konverzace |chat] - důvod ukončení a čtení odpovědi +- [Ošetření chyb |errors] - která výjimka co znamená +- [Provideři |providers] - co který umí a čím se liší diff --git a/ai-access/cs/tools.texy b/ai-access/cs/tools.texy new file mode 100644 index 0000000000..22df5bc275 --- /dev/null +++ b/ai-access/cs/tools.texy @@ -0,0 +1,154 @@ +Volání nástrojů (function calling) +********************************** + +.[perex] +Model neví nic o tvé databázi ani o dnešním počasí. Umí si ale říct o funkci, která to zjistí, a počkat, než mu výsledek pošleš. Ukážeme si, jak se modelu funkce popíše, jak knihovna celou výměnu obslouží sama a co dělat, když si model vymýšlí. + + +Model neví, tak se zeptá +======================== + +Jazykový model zná jen to, co se naučil při trénování. Nezná stav objednávky ve tvém e-shopu, nemá přístup k databázi a neví, kolik je hodin. Když se ho na to zeptáš, buď přizná, že neví, nebo si odpověď vymyslí; to druhé je horší, protože zní stejně přesvědčivě. (Když mu chceš zpřístupnit vlastní texty, třeba dokumentaci nebo znalostní bázi, hodí se spíš [vyhledávání přes embeddingy |embeddings]; nástroje jsou od toho, aby model něco **udělal**.) + +Volání nástrojů to řeší tím, že se role na chvíli prohodí. Ty modelu předem popíšeš funkce, které tvoje aplikace umí. Model pak místo odpovědi může říct: "zavolej mi `zjistiPocasi` pro Brno". Ty funkci vykonáš, výsledek mu pošleš zpátky a on z něj složí odpověď. + +Anglicky se téhle technice říká **tool calling** nebo **function calling**; oba názvy znamenají totéž a liší se jen tím, který provider je zrovna prosazuje. Česky jim tady říkáme nástroje, protože tak se jmenují i metody knihovny. + +Podstatné je, že **model tvůj kód nespouští**. Jen řekne, co by chtěl zavolat a s jakými argumenty; jestli to uděláš, rozhodne tvoje aplikace. Model je v tomhle uspořádání ten, kdo prosí, ne ten, kdo velí. + + +Nejjednodušší případ: knihovna to zařídí za tebe +================================================ + +Nástroj popíšeš objektem `Tool`: jméno, popis, k čemu je, JSON schéma argumentů a funkci, která ho vykoná: + +```php +use AIAccess\Chat\Tool; + +$chat = $client->createChat('gpt-5.6-luna'); + +$chat->addTool(new Tool( + name: 'zjistiPocasi', + description: 'Vrátí aktuální počasí pro zadané město.', + parameters: [ + 'type' => 'object', + 'properties' => [ + 'mesto' => ['type' => 'string', 'description' => 'Název města'], + ], + 'required' => ['mesto'], + ], + handler: function (array $args): string { + // tady zavoláš svoje API nebo databázi + return 'Ve městě ' . $args['mesto'] . ' je 12 °C a prší.'; + }, +)); + +echo $chat->sendMessage('Jaké je počasí v Brně a mám si vzít bundu?')->getText(); +``` + +A to je celé. Jedno `sendMessage()` obslouží celou výměnu: model si řekne o nástroj, knihovna zavolá tvůj handler, výsledek pošle zpátky a počká, co model odpoví. Jednomu takovému pohybu tam a zpátky se říká **kolo** a může jich proběhnout několik za sebou, když model potřebuje víc informací; celé té sérii pak **smyčka volání nástrojů**, anglicky tool loop. + +Na popisu záleží víc, než se zdá. Model se podle něj rozhoduje, jestli nástroj vůbec zavolat, a jiné vodítko nemá. "Vrátí aktuální počasí pro zadané město" je dobrý popis; "počasí" je špatný. + +Víc kol není žádná exotika. Když se zeptáš na počasí v Brně a v Praze, model si o nástroj řekne dvakrát. A když má nástrojů víc, běžně zavolá jeden, podívá se na výsledek a teprve podle něj sáhne po druhém: nejdřív si najde zákazníka podle e-mailu, pak k němu dohledá objednávky. + +Strop na počet kol proto nastavíš metodou `setToolLoop()`. Výchozích osm stačí na běžné úlohy a zároveň brání tomu, aby se model zacyklil na tvůj účet: + +```php +$chat->setToolLoop(maxRounds: 3); +``` + +Kolik celá výměna stála, se dozvíš z `getTotalUsage()`, protože `getUsage()` na odpovědi mluví jen o posledním kole. + + +Když chceš volání vyřídit sám +============================= + +Automatická smyčka se spustí jen tehdy, když mají všechny volané nástroje handler. Jakmile ho některý nemá, knihovna se zastaví a předá řízení tobě. To je záměr: absence handleru je způsob, jak říct "tohle chci vyřídit sám". + +```php +$chat->addTool(new Tool( + name: 'smazUcet', + description: 'Smaže uživatelský účet.', + parameters: ['type' => 'object', 'properties' => ['id' => ['type' => 'integer']]], + // handler schválně chybí +)); + +$response = $chat->sendMessage('Zruš účet číslo 42.'); + +foreach ($response->getToolCalls() as $call) { + echo "Model chce zavolat $call->name s argumenty ", json_encode($call->arguments); + + // tady se zeptáš uživatele, zkontroluješ oprávnění, cokoli potřebuješ + $chat->addToolResult($call, 'Účet byl smazán.'); +} + +echo $chat->sendMessage()->getText(); // pokračuj s doplněnými výsledky +``` + +Tenhle režim se hodí všude, kde nechceš, aby akce proběhla bez lidského souhlasu, a taky když chceš volání zaznamenat nebo omezit. + +Když chceš modelu naopak nařídit, že má sáhnout po konkrétním nástroji, použij `setToolChoice('jmenoNastroje')`. Zakázat nástroje úplně se dá tím, že žádný nezaregistruješ. + + +Parametry jako Nette Schema +=========================== + +Parametry mohou být místo pole i struktura z [Nette Schema |schema:]. Knihovna ji pro model vyexportuje do JSON schématu a navíc jí argumenty plnohodnotně zvaliduje: povinné klíče, typy, rozsahy, `assert()`, cokoli schéma říká. Tvůj handler pak místo holého pole dostane to, co schéma vydá, s doplněnými výchozími hodnotami a provedenými přetypováními: + +```php +use Nette\Schema\Expect; + +$chat->addTool(new Tool( + name: 'zjistiPocasi', + description: 'Vrátí aktuální počasí pro zadané město.', + parameters: Expect::structure([ + 'mesto' => Expect::string()->required()->description('Název města'), + 'jednotky' => Expect::anyOf('celsius', 'fahrenheit')->default('celsius'), + ]), + handler: function (stdClass $args): string { + return "Ve městě $args->mesto je 12 °C a prší."; // $args->jednotky je 'celsius', i když ho model vynechal + }, +)); +``` + +Argumenty, které schéma odmítne, se vrátí modelu i s jeho přesnými hláškami, takže se dozví nejen že něco bylo špatně, ale co. Všechno ostatní na této stránce funguje stejně, ať jsou parametry napsané tak či onak. + + +Když si model vymýšlí +===================== + +Model občas požádá o nástroj, který neexistuje, nebo pošle argumenty, které neodpovídají schématu. Není to výjimečné a hlavně to není chyba tvé aplikace, takže se to ani neřeší výjimkou. + +Knihovna takový omyl **pošle modelu zpátky jako chybový výsledek** a nechá ho, ať se opraví. Modely to zvládají překvapivě dobře: přečtou si, co bylo špatně, a zavolají nástroj znovu správně. Kdyby místo toho vyletěla výjimka, přišel bys o celou odpověď kvůli chybě, kterou si model umí opravit sám. + +Sem patří vymyšlené jméno nástroje, nečitelné argumenty i argumenty, které nesedí na schéma. U pole s JSON schématem knihovna kontroluje povinné klíče a základní typy; plnohodnotný validátor to není, protože model dostane chybovou zprávu tak jako tak. Nette schéma validuje naplno. + +Jiná věc je, když selže **tvůj vlastní handler**. Ve výchozím nastavení výjimka propadne k tobě, což je správně, protože rozbitá databáze není nic, co má model řešit. Když ale chceš, aby se model o selhání dozvěděl a mohl zkusit jinou cestu, zapneš si to: + +```php +$chat->setToolLoop(catchErrors: true); +``` + +I tehdy platí jedna výjimka z výjimek: chyby úrovně `Error`, tedy chyby ve tvém vlastním kódu, jako je volání neexistující metody, propadnou vždycky. Kdyby se posílaly modelu jako výsledek nástroje, tvoje chyba by se schovala do konverzace a ty by ses o ní nedozvěděl. + + +Co se děje na pozadí +==================== + +U nástrojů se provideři neshodnou skoro na ničem. Stojí za to vědět aspoň o dvou pastech, na které narazíš, kdyby ses někdy díval na nezpracované odpovědi z API. + +**Gemini neohlásí volání nástroje v důvodu ukončení.** Zůstane tam `STOP`, jako by odpověď normálně dopověděl, a samotné volání najdeš až mezi částmi odpovědi. Knihovna proto důvod ukončení dopočítává z obsahu, takže `FinishReason::ToolCall` dostaneš u všech pěti stejně. + +**Uvažování se musí vrátit beze změny.** Modely, které před odpovědí přemýšlejí, chtějí v dalším kole svoje uvažování zpátky přesně tak, jak ho poslaly. Claude odmítne pozměněný podpis chybou, Gemini odpoví `MISSING_THOUGHT_SIGNATURE`. Knihovna si tyhle části nese v historii a vrací je vždy jen tomu providerovi, který je vydal. + +A jedna vlastnost, která překvapí: **smyčka si každé dokončené kolo započítá**. Když sedmé kolo selže, historie i vedlejší účinky nástrojů z prvních šesti zůstanou. Zahodit je by znamenalo zaplatit šest kol za nic a případně dvakrát odeslat e-mail, který už jeden z nástrojů poslal. + + +Kam dál +======= + +- [Strukturovaný výstup |structured-output] - když chceš data, ne volání +- [Streamování |streaming] - kola nástrojů se streamují taky +- [Konverzace |chat] - historie, do které se volání i výsledky zapisují +- [Ošetření chyb |errors] - co dělat, když provider řekne ne diff --git a/ai-access/en/@home.texy b/ai-access/en/@home.texy new file mode 100644 index 0000000000..14ba630b7a --- /dev/null +++ b/ai-access/en/@home.texy @@ -0,0 +1,162 @@ +AI Access: One PHP Interface for OpenAI, Claude, Gemini, DeepSeek and Grok +************************************************************************** + +
+ +AI Access is a PHP library that unifies working with language models. Instead of five different APIs and five different response formats, you write one piece of code that talks to ChatGPT from OpenAI, Claude from Anthropic, Gemini from Google, DeepSeek and Grok from xAI. Switching between them is a one-line change. + +It covers the whole workflow: conversation, streaming responses, tool calling (function calling), structured output following a JSON schema, images and documents as input, image generation, embeddings for search, and batch processing at half the price. + +It has no dependencies. Just plain PHP 8.3 and curl, no vendor SDK and no version conflicts with the rest of your project. + +
+ + +What a Language Model Is Good For in an Application +=================================================== + +If you have never called an AI API, the principle is simpler than it looks from outside. You send text and the model sends text back. Everything else is built on top of that single move. + +In practice this grows into a surprisingly wide range of tasks, and it pays to know them before you start coding, because they tell you which part of this documentation you actually need: + +- **Writing and rewriting text.** Summarizing an article, drafting a reply to an e-mail, translating, proofreading. The most common and simplest case; [an ordinary conversation |chat] is all you need. +- **Classification and decisions.** Is this registration spam? Which category does this question belong to? The model answers in one word and you act on it. +- **Pulling data out of unstructured text.** From an invoice, a CV or an e-mail you need fields you can store in a database. That is what [structured output |structured-output] is for: you hand the model a JSON schema and it sticks to it. +- **Answering over your own data.** The model knows nothing about your documentation, but attach the relevant excerpts to the question and it answers precisely. Finding those excerpts is a job for [embeddings |embeddings], the meaning-based search that people call RAG. +- **Working with images and documents.** Describe what is in a photo, read the figures off a receipt, summarize an attached PDF. See [images and documents as input |multimodal]. +- **Actions, not just text.** The model can ask for your function to be called, receive the result and carry on. That is how you get an assistant that really looks into your database instead of inventing the answer. See [tool calling |tools]. +- **Bulk processing.** When you do not need the answer now, [batch processing |batch] gets you the same models for half the price. + +What AI Access is not: it is not an agent framework, it does not try to write your prompts for you, and it does not keep conversations in a database. It is the layer that talks to the providers' APIs, and it ends exactly where your application's own decisions begin. + + +Installation +============ + +```shell +composer require ai-access/ai-access +``` + +Requires PHP 8.3 or later and the curl, json and fileinfo extensions, which are available almost everywhere. + + +The First Message +================= + +You need a key from the provider you want to use. They are issued in their consoles: [OpenAI |https://platform.openai.com/api-keys], [Anthropic |https://console.anthropic.com/settings/keys], [Google |https://aistudio.google.com/app/apikey], [DeepSeek |https://platform.deepseek.com/api_keys] and [xAI |https://console.x.ai/team/default/api-keys]. + +```php +$client = new AIAccess\Provider\OpenAI\Client($apiKey); + +$response = $client->createChat('gpt-5.6-luna') + ->sendMessage('Write a haiku about PHP.'); + +echo $response->getText(); +``` + +That is all of it. `createChat()` opens a conversation over the chosen model, `sendMessage()` sends a message and returns the answer. + +The model name is an ordinary string, not a constant or an enum. It sounds like a detail, but it means a new model works the day the provider ships it, with no library update to wait for. To check that a model still exists, use the [list of models |providers#Which Models a Provider Currently Offers]. + + +Switching Provider Is One Line +============================== + +This is the library's central promise, so let it be visible right away. When the client knows the model too, that one line really is all that changes: + +```php +$client = new AIAccess\Provider\Claude\Client($apiKey, chatModel: 'claude-sonnet-5'); +$client = new AIAccess\Provider\Gemini\Client($apiKey, chatModel: 'gemini-3.5-flash-lite'); +$client = new AIAccess\Provider\DeepSeek\Client($apiKey, chatModel: 'deepseek-v4-flash'); +$client = new AIAccess\Provider\Grok\Client($apiKey, chatModel: 'grok-4.3'); + +// the rest of the code is the same for all of them +$chat = $client->createChat(); +``` + +[A model name belongs next to the key |providers#Default Models], because both belong wholly to one provider. In a real application you register the client in the [DI container |dependency-injection:], and switching provider becomes a change in configuration rather than in code. + + +Five APIs That Agree on Nothing +=============================== + +When you write your own wrapper over the providers, the first two look like a couple of hours of work. The trouble starts with the third, because each of them has a different idea of what a conversation with a model looks like. Here is a small sample of the differences: + +| what differs | Claude | OpenAI | Gemini | Grok and DeepSeek | +|-------------------------------|--------------------|-------------------------------------|-------------------------|--------------------| +| endpoint | `v1/messages` | `v1/responses` | `:generateContent` | `chat/completions` | +| authentication | `x-api-key` header | `Bearer` | `x-goog-api-key` header | `Bearer` | +| request shape | `messages[]` | `input[]` and `instructions` | `contents[].parts[]` | `messages[]` | +| what the model role is called | `assistant` | `assistant` | **`model`** | `assistant` | +| token usage keys | `input_tokens` | the same | `promptTokenCount` | `prompt_tokens` | +| where the finish reason is | `stop_reason` | `status`, then `incomplete_details` | `finishReason` | `finish_reason` | + +The last row hides a catch worth saying out loud: Gemini **never** reports in `finishReason` that the model wants to call a tool. It stays `STOP` and the call itself is found among the parts of the answer. Anyone who does not know this writes code that silently ignores half of what the model said, and never finds out. + +There are dozens of such details and none of them is interesting work. AI Access has them solved and tested against real API responses rather than invented JSON. + +At the same time it does not pretend the differences are gone. It unifies what the providers genuinely share, and where they differ it tells you through a type or an exception while you are writing the code, not through an error in production. + + +What the Library Can Do +======================= + +| Capability | OpenAI | Claude | Gemini | DeepSeek | Grok | Generic client | +|----------------------------------------|--------|--------|--------|----------|------|----------------| +| [Conversation |chat] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Reasoning effort |options] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Tool calling |tools] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Streaming |streaming] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Images as input |multimodal] | ✅ | ✅ | ✅ | ➖ | ✅ | ✅ | +| [Documents as input |multimodal] | ✅ | ✅ | ✅ | ➖ | ➖ | ➖ | +| [Structured output |structured-output] | ✅ | ✅ | ✅ | ➖ | ✅ | ✅ | +| [Image generation |images] | ✅ | ➖ | ✅ | ➖ | ✅ | ➖ | +| [Batch processing |batch] | ✅ | ✅ | ✅ | ➖ | ➖ | ➖ | +| [Embeddings |embeddings] | ✅ | ➖ | ✅ | ➖ | ➖ | ➖ | +| [List of models |providers] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | + +Where a minus appears, either the provider has no such API or the library does not wrap it yet. + +The last column is the generic client for anything that speaks the `chat/completions` dialect: Ollama running on your laptop, Mistral, OpenRouter, Together, vLLM or Azure. The mark means something different there, namely what the library is able to send; whether it actually works is decided by the endpoint and the model you point it at. The details are with the [providers |providers]. + + +Designed, Not Accreted +====================== + +Provider-specific settings are named arguments, not keys in a shared array. You feel the difference as you type: your IDE offers exactly what that provider supports, instead of an array quietly swallowing a key that goes nowhere. Add strict types everywhere and readonly value objects. + +The exception hierarchy is built around the only question that genuinely matters in production, namely whether the call is worth repeating: + +```php +try { + $response = $chat->sendMessage('...'); + +} catch (AIAccess\ApiException $e) { + // the provider said no; $e->getCode() carries the HTTP status + if ($e->getCode() === 429) { + // rate limited, try again shortly + } + +} catch (AIAccess\CommunicationException $e) { + // network hiccup or an unreadable response, retrying may help +} +``` + +`LogicException` deliberately sits outside that tree, because a mistake in your own code is not something production should catch and walk past. The whole hierarchy is covered by the chapter on [error handling |errors]. + +Retrying, by the way, is not something you have to write yourself. The library ships [HTTP layer decorators |http] that retry after rate limits and outages, log every request, or cache responses during development so that re-running a script costs you nothing. + + +Where to Go Next +================ + +- [Getting started |getting-started] - keys, the first call, and what to do when something does not add up +- [Conversation |chat] - history, system instruction, reading the answer and the token usage +- [Options and reasoning effort |options] - how much thinking you ask the model for +- [Streaming |streaming] - read the answer while the model is still writing it +- [Tool calling |tools] - the model asks, your code answers, the loop closes itself +- [Structured output |structured-output] - a JSON schema instead of pleading in the prompt +- [Providers |providers] - what each one can do, how they differ, and how to plug in Ollama or OpenRouter + +.[note] +And if you let an AI agent help you write the code, have a look at [Nette AI |ai:]. You will find a Claude Code plugin that teaches the agent Nette, and MCP Inspector, which lets the agent look straight into your running application. diff --git a/ai-access/en/@left-menu.texy b/ai-access/en/@left-menu.texy new file mode 100644 index 0000000000..51d2a7dd61 --- /dev/null +++ b/ai-access/en/@left-menu.texy @@ -0,0 +1,14 @@ +- [Overview |@home] +- [Getting Started |getting-started] +- [Conversation |chat] +- [Options and Reasoning Effort |options] +- [Streaming |streaming] +- [Tool Calling |tools] +- [Structured Output |structured-output] +- [Images and Documents |multimodal] +- [Image Generation |images] +- [Embeddings |embeddings] +- [Batch Processing |batch] +- [Error Handling |errors] +- [HTTP Layer |http] +- [Providers and Models |providers] diff --git a/ai-access/en/@meta.texy b/ai-access/en/@meta.texy new file mode 100644 index 0000000000..b95080c3cc --- /dev/null +++ b/ai-access/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: AI Access}} diff --git a/ai-access/en/batch.texy b/ai-access/en/batch.texy new file mode 100644 index 0000000000..adbe5e8712 --- /dev/null +++ b/ai-access/en/batch.texy @@ -0,0 +1,184 @@ +Batch Processing +**************** + +.[perex] +When you do not need the answer now, you save roughly half the money. Providers call it batch processing: you send them a pile of requests at once and collect the results later. We will look at how to assemble and submit such a batch, how to collect the results afterwards, and what to watch out for before it goes into production. + + +What It Is For +============== + +Imagine you have five thousand products and want a short description of each from the model. Send them one by one and you pay full price and wait while five thousand questions and answers take their turn. + +Providers have a special mode for exactly this case, called **batch processing**. You hand them the whole pile of requests at once, they process it when they have spare capacity, and you collect the results later. In return for not being in a hurry you pay **roughly half**. + +It does not suit everything. The answer does not come immediately and in the worst case it can take hours, so a batch will never serve a user staring at the screen. It is ideal for work that runs in the background: bulk translations, classifying a queue, generating descriptions, preparing data overnight. + + +A Batch of Conversations +======================== + +It goes in three steps: first you create an empty batch, then you add tasks to it one by one, and finally you submit the whole thing. Each task is a conversation of its own and gets an identifier of your choosing, which is how you later pair the answer with your record: + +```php +use AIAccess\Chat\Role; + +$batch = $client->createBatch(); + +foreach ($products as $product) { + $chat = $batch->addChat('product-' . $product->id, 'gpt-5.6-luna'); + $chat->setSystemInstruction('Write short product descriptions, two sentences at most.'); + $chat->addMessage($product->name . ': ' . $product->specs, Role::User); +} + +$response = $batch->submit(); +echo $response->getId(); +``` + +`addChat()` returns an ordinary conversation object, so you work with it exactly as [you already know |chat]: set the system instruction, add messages, turn on [structured output |structured-output] if you like. The only difference is that instead of `sendMessage()` at the end you call `submit()` on the whole batch. + +**Store that job id.** Without it you cannot reach the results, and the provider will not send the batch to you again. + + +Collecting the Results +====================== + +You collect the results whenever you like afterwards, from a completely different script if you want: + +```php +use AIAccess\Batch\Status; + +$batch = $client->retrieveBatch($batchId); + +if ($batch->getStatus() === Status::InProgress) { + echo 'Still working, status: ', $batch->getStatus()->name; + return; +} + +foreach ($batch->getResults() as $customId => $result) { + echo $customId, ': ', $result->message?->getText() ?? "failed, $result->error", "\n"; +} +``` + +Under the same keys you used when adding them you get `AIAccess\Batch\Result` objects. Each carries **either the answer or the reason that one request failed**: when one goes wrong, the whole batch does not fall over because of it; that item simply arrives with `$result->error` filled in instead of `$result->message`. + +The answer itself is an ordinary `AIAccess\Chat\Message`, exactly what a [live conversation |chat] carries. You pull the text out with `getText()`, the pictures with `getMedia()`, and everything else, such as the model's reasoning or its tool calls, is among the message parts in `getParts()`. + +What matters is **how** the results arrive: they are read one item at a time as they come off the network, rather than downloaded whole and handed to you afterwards. A batch of any size therefore costs the memory of a single answer, which is the difference between "it works" and "a hundred images eat a gigabyte". A few things follow that are worth remembering: + +- **Nothing is sent until you start iterating.** Putting `getResults()` in a variable is free. +- **Leaving the loop with `break` ends the transfer**, so the rest of the file is neither downloaded nor paid for. +- **Nothing is remembered.** Iterating a second time downloads again; if you need the data twice, keep it. +- If you do want everything at once as an array, that is `iterator_to_array($batch->getResults())`. Your decision, your memory. + + +A Batch of Images +================= + +Images are batched the same way and for the same reason: they are [an order of magnitude more expensive than text |images], so half price shows up far more. + +```php +$batch = $client->createBatch(); + +$batch->addImageRequest('lighthouse', 'A lighthouse on a cliff in a storm, painterly style', 'gpt-image-2'); +$batch->addImageRequest('harbour', 'A harbour at dawn, the same painterly style', 'gpt-image-2'); + +$response = $batch->submit(); +echo $response->getId(); +``` + +`addImageRequest()` returns a request object you can configure further. Add a reference image to it or change the parameters, each provider its own: + +```php +use AIAccess\Media; + +$batch->addImageRequest('variant', 'The same lighthouse on a sunny morning', 'gpt-image-2') + ->addReference(Media::fromFile('/path/to/lighthouse.png')) + ->setOptions(size: '1024x1024', quality: 'low'); +``` + +It is the same batch as for conversations, not a special one. Outside a batch a single picture is still made with [`generateImage()` |images], which takes every option as a named argument; a second way of asking for the same thing is deliberately not there. + +A reference travels to every request as its own copy, which is not free for a large image repeated across a whole batch: OpenAI takes 200 MB for a whole job and base64 inflates the bytes by another third. If you are approaching that ceiling, split the batch into several smaller ones. + +The results are collected exactly as with conversations, except that you look for pictures in the answers rather than text: + +```php +foreach ($batch->getResults() as $customId => $result) { + foreach ($result->message?->getMedia() ?? [] as $i => $media) { + $extension = explode('/', $media->getMimeType())[1]; + $media->save("/path/to/$customId-$i.$extension"); + } +} +``` + +This is where reading item by item pays off most: pictures are megabytes and a batch happily holds a hundred of them, so the difference between "hold one" and "hold all" is the difference between running and hitting `memory_limit`. + +With OpenAI, a request that ended without a picture arrives as a failure with `$result->error` filled in, rather than as an empty message. + +What may share one job is decided by the provider, not by the library. **OpenAI runs one job on one endpoint**, so there images cannot share a batch with conversations and generating cannot share one with editing; mixed requests are reported before anything is submitted. **Gemini has no such rule**, because it draws through the same endpoint it talks through, so one batch of its own can carry both text and pictures. + + +Watching and Cancelling a Job +============================= + +A job's status takes one of four values: `InProgress` while the work goes on (a job being cancelled included), `Completed` after a successful finish, `Failed` when the job failed, expired or was cancelled, and `Other` for states that do not fit this scale. + +Do not go for the results only on `Completed`, though. A cancelled or expired job hands over the requests it managed to finish, and you have paid for those; only one that is still running has nothing. + +```php +$batch = $client->retrieveBatch($batchId); + +echo 'status: ', $batch->getStatus()->name, "\n"; +echo 'created: ', $batch->getCreatedAt()?->format('j M H:i'), "\n"; +echo 'done: ', $batch->getCompletedAt()?->format('j M H:i') ?? 'not yet', "\n"; +``` + +A job in progress can be cancelled with `cancelBatch($id)`. You get an overview of your jobs from `listBatches()`, which is read with a `foreach` and **fetches further pages as you reach them**, so a hundred jobs read like one and a loop you break out of sends no more requests. The listing carries only the job headers; **the library fetches the results only at the moment you start reading them** through `getResults()`. + +```php +foreach ($client->listBatches() as $batch) { + echo $batch->getId(), ': ', $batch->getStatus()->name, "\n"; +} +``` + +You do not have to follow the progress from code alone. Every provider also shows submitted jobs in its web console, the same one where you issued the [API key |getting-started], including the status and the time it finished. While debugging that is the fastest way to find out whether it is done, without writing a single line for it. + + +Three Mechanisms, One Interface +=============================== + +Here you can see what the library does for you, because each provider solves batches completely differently: + +| Provider | How a batch works there | +|----------|-------------------------------------------------------------------------------------------------| +| OpenAI | the requests are serialized into a JSONL file, that is uploaded and only then does a job appear | +| Claude | the requests are sent straight in the body of a single call | +| Gemini | the batch is a so-called long-running operation and the results travel inside it | + +None of it touches your code, whichever you choose. Batch processing is offered by OpenAI, Claude and Gemini; a batch of images by OpenAI and Gemini. + + +Before It Goes Into Production +============================== + +**With OpenAI and Gemini, one batch is one model.** Gemini has the model in the job's address, OpenAI ties it to the uploaded file, so mixing two is not possible. The library watches for that and reports `AIAccess\LogicException` right when you add the request, not after submitting. Claude works differently: every request carries its own model, so there you can compare two models side by side within a single batch. + +**Do not rely on it being fast.** Providers usually promise completion within 24 hours. In practice batches tend to be done within minutes, but that is a promise of the upper bound, not the lower one; design accordingly for the case where the results are not there yet. + +**The job id belongs in a database**, not in a variable. The script that submitted the batch will be long gone before the results are available, so they are usually collected by a scheduled task running an hour or so later. + +**Gemini needs a paid project.** On the free tier the batch endpoint refuses to work. + +**With Gemini the memory saving is only apparent.** Its results travel inside the job itself, so by the time `getResults()` sees them they are in memory whole; reading item by item is uniformity there, not economy. For the same reason a second reading costs nothing there, as there is nothing left to download. OpenAI and Claude deliver results as a file, and that one really does stream. + +And one detail that saves confusion: until the batch finishes, `getResults()` simply yields nothing. That is not an error; there is just nothing to read yet, so ask about the status first. + + +Where to Go Next +================ + +- [Conversation |chat] - what the same work looks like in direct mode +- [Image generation |images] - single images and their references +- [Error handling |errors] - what the individual exceptions mean +- [Providers |providers] - what each one can do and how they differ diff --git a/ai-access/en/chat.texy b/ai-access/en/chat.texy new file mode 100644 index 0000000000..ec27238927 --- /dev/null +++ b/ai-access/en/chat.texy @@ -0,0 +1,154 @@ +Conversation with an AI Model +***************************** + +.[perex] +A conversation is a sequence of messages that the library keeps for you. You will learn how to hold a multi-turn dialogue, give the model a role through a system instruction, read more than the text out of a response, and assemble the history by hand when you need to resume an earlier conversation. + + +The Model Remembers Nothing +=========================== + +This is the first thing that surprises almost everyone: a language model has no memory. Every API call stands alone and the model knows nothing about the previous question. The illusion of a conversation comes from sending the entire history again with every request. + +That is exactly what the conversation object is for. It keeps the history for you: + +```php +$chat = $client->createChat('gpt-5.6-luna'); + +$chat->sendMessage('What is the capital of France?'); +$response = $chat->sendMessage('And what is a famous landmark there?'); + +echo $response->getText(); +``` + +The second question does not mention Paris at all, yet the model answers correctly, because the first exchange traveled along with it. Ask through two separate calls and the second answer is nonsense. + +There is one consequence worth planning for: **a long conversation is expensive**. Every turn grows the input, and input is billed. So it is sometimes worth trimming the history or starting over. And when you send the same question over hundreds of inputs and are in no hurry, [batch processing |batch] comes out cheaper: + +```php +$chat->clearMessages(); +``` + + +The System Instruction +====================== + +The system instruction tells the model what role to play and which rules to follow. It applies to the whole conversation and models weigh it more heavily than an ordinary message, so this is where instructions belong that must still hold ten turns later. + +```php +$chat->setSystemInstruction('You are an experienced PHP developer. Answer briefly and use Nette in examples.'); +``` + +A good system instruction is specific. Instead of "be brief" write "answer in at most three sentences"; instead of "be accurate" write "when you are not sure, say so instead of guessing". The model has no way of knowing what you picture behind a vague instruction. + +The system instruction is sent with every request, so you pay for it every time. When it is long and the conversation has many turns, reach for the provider's cache; how much came from it is reported by `cacheReadTokens` in the [usage |#What Is in the Response]. + + +The Model Can Be Swapped Mid-Conversation +========================================= + +A model is not a property of the conversation but a parameter of the individual request, just like the temperature or the token limit. It travels on the wire with every call, and the library keeps it that way: + +```php +$chat->setModel('gpt-5.6-luna-mini'); +``` + +**The history stays.** The next turn continues where the previous one left off, only a different model answers it. This comes up more often than you would think: after a 429 you switch to a cheaper model instead of waiting, for a hard question you reach for a stronger one, or you simply leave the choice to the user, the way every chat interface does. + +Switching works **within one provider**. Message parts that carry a provider's own payload, typically a reasoning model's chain of thought, are replayed only to whoever produced them, so you cannot move from Claude to GPT; from one Claude model to another you can. + +When the client has a [default model configured |getting-started#Where the Key Belongs in Production], `createChat()` needs no argument at all: + +```php +$chat = $client->createChat(); +``` + + +History Assembled by Hand +========================= + +Sometimes you need to hand the model a conversation that never happened that way. Typically when you restore a conversation stored in a database, or when you want to show a few examples of correct answers, a technique known as few-shot prompting. + +```php +use AIAccess\Chat\Role; + +$chat = $client->createChat('gpt-5.6-luna'); +$chat->addMessage('What is the capital of France?', Role::User); +$chat->addMessage('Paris.', Role::Model); +$chat->addMessage('And what is a famous landmark there?', Role::User); + +$response = $chat->sendMessage(); // no argument: continue from history +``` + +`addMessage()` only appends a message to the history and sends nothing. `sendMessage()` without an argument then sends the conversation as it stands. + +There are three roles. `Role::User` is the user, `Role::Model` is the model, and `Role::Tool` carries the results of [tool calls |tools]. The library uses its own naming here: Gemini calls the same role `model` while the others call it `assistant`, and the library translates it to whichever name the provider expects. + +You can ask for the whole history back at any time: + +```php +foreach ($chat->getMessages() as $message) { + echo $message->getRole()->value, ': ', $message->getText(), "\n"; +} +``` + + +What Is in the Response +======================= + +`sendMessage()` does not return a string but an object, because the text alone is only part of what happened. + +```php +$response = $chat->sendMessage('Write me a short story about PHP.'); + +echo $response->getText(); +``` + +An empty text is not necessarily an error. The model may have declined to answer, it may have hit a limit before writing the first word, or it may have asked for a tool instead of answering. Which of those happened is revealed by the finish reason. + + +Why the Model Stopped Writing +----------------------------- + +An answer does not always end because the model had said everything. Sometimes a token limit stops it, sometimes a safety filter, and sometimes it is waiting for you to supply something. You need to tell these apart, because each one calls for a different reaction. That is what `getFinishReason()` is for, returning one of the values of the `FinishReason` enum: + +| Value | What happened | What to do about it | +|-------------------|-------------------------------------------------------------|-------------------------------------------------------------------------------------------| +| `Complete` | The model said all it wanted and stopped on its own. | Nothing, this is the good case. | +| `TokenLimit` | The answer is cut off mid-way, the token allowance ran out. | Raise the limit in [options |options], or ask for a shorter answer. | +| `ContentFiltered` | The model refused to answer. | The text will be empty. Rephrase the question; on OpenAI `getRefusal()` gives the reason. | +| `ToolCall` | The model asked for a [tool |tools] to be called. | Call it and send the result back. | +| `Cancelled` | You interrupted the [stream |streaming] yourself. | You have only part of the answer, which is fine. | +| `Unknown` | A reason outside this scale, or no reason at all. | The original value is in `getRawFinishReason()`. | + +A missing reason is most often the mark of a transfer that never finished, which is why it is not passed off as `Complete`. With an [interrupted stream |streaming] it is your clue that the text is not whole. + +In code it looks like this: + +```php +use AIAccess\Chat\FinishReason; + +if ($response->getFinishReason() === FinishReason::TokenLimit) { + echo 'The answer is cut off, the model hit the limit.'; +} +``` + +Reasoning models may also return their chain of thought. It is never part of `getText()`, because it does not belong in your application's output, but you can read it: + +```php +if ($reasoning = $response->getReasoning()) { + echo "The model reasoned like this:\n", $reasoning; +} +``` + +And when the abstraction is not enough, `getRawResponse()` hands you the provider's complete decoded answer exactly as it arrived. The unified interface is a convenience, never a cage. + + +Where to Go Next +================ + +- [Options and reasoning effort |options] - how much thinking you ask the model for +- [Streaming |streaming] - read the answer while the model is still writing it +- [Tool calling |tools] - when the model should reach into your application +- [Structured output |structured-output] - when you need data, not prose +- [Embeddings |embeddings] - when the model should answer over your own texts diff --git a/ai-access/en/embeddings.texy b/ai-access/en/embeddings.texy new file mode 100644 index 0000000000..5760fd02eb --- /dev/null +++ b/ai-access/en/embeddings.texy @@ -0,0 +1,140 @@ +Embeddings and Semantic Search +****************************** + +.[perex] +Search that finds what the user named with completely different words. Suggestions of related articles. Sorting questions into categories. Answering over your own documentation. All of these rest on one technique: embeddings let a model work out the meaning of a text and turn it into numbers that even an ordinary database can compute with. + + +What an Embedding Is +==================== + +You have a search box on your site. A user types "how to speed up a website" and finds nothing, even though you have an article called "Optimizing application performance". Not a single word matches, so `LIKE` and full-text both stay silent. And yet that is exactly the article they wanted. + +You can write lists of synonyms that will never be complete. Or you can stop comparing words and start comparing meaning. That is exactly what embeddings make possible. + +Imagine scoring every article in a questionnaire. How much is it about technology? How much about cooking? How much does it solve some problem? How much of a tutorial is it? Your scores form a row of numbers, and two articles with similar scores are clearly about the same thing, even when each uses different words. + +**And that is exactly what an embedding does, only on a far larger scale.** The model invents the questions itself, there are several hundred to several thousand of them, and nobody ever spelled them out; we never learn them and do not need to. All we get are the scores, a row of decimal numbers. So there is no point looking at an individual number for meaning; meaning appears only when two such rows are compared. A row of numbers like this is called a **vector**, which is where the library class gets its name. + +One more property comes in handy: **the computation is deterministic**. The same text sent to the same model returns the same vector every time. Unlike a conversation with a model, there is no risk of getting a different result today than yesterday, so you can store the vectors and never compute them again. + + +The First Vectors +================= + +Embeddings can be computed by OpenAI and Gemini. You can run this script as it stands: + +```php +require __DIR__ . '/vendor/autoload.php'; + +$client = new AIAccess\Provider\OpenAI\Client('paste-your-key-here'); + +$vectors = $client->calculateEmbeddings([ + 'How to speed up a website', + 'Optimizing application performance', + 'A recipe for beef sirloin', +], 'text-embedding-3-small'); + +echo 'website vs. performance: ', $vectors[0]->cosineSimilarity($vectors[1]), "\n"; +echo 'website vs. recipe: ', $vectors[0]->cosineSimilarity($vectors[2]), "\n"; +``` + +You get an array of `AIAccess\Embedding\Vector` objects in the same order in which you sent the texts. + + +How to Read the Similarity +-------------------------- + +The `cosineSimilarity()` method compares two such rows of scores and sums their agreement into a single number from -1 to 1. The handy part is that the length of the text does not matter: a short question and a long article about the same thing come out close together, even when one is ten times longer. + +- **1** is the highest possible agreement, so practically identical meaning. +- **0** means the texts have nothing in common. +- **-1** would mean the exact opposite. With text embeddings this practically never happens, so you can ignore the lower half of the scale. + +Do not expect the numbers to spread across the whole scale, though. In practice they sit in a much narrower band: unrelated texts do not come out at zero and a perfect hit does not come out at one. Every model spreads the scale differently as well, so you have to measure your own threshold for "similar enough" on your own data. + +Working with the ordering is the most reliable approach anyway. Sort the candidates by similarity and take the top few; that works no matter how the model scales. + + +Semantic Search Step by Step +============================ + +Search over your own data has two phases and it pays to separate them, because each happens at a different time. + +**Once, during indexing**, you compute an embedding for every document and store it. This is the paid and slower part, but it only happens when a document is created or changed. + +**On every query** you compute an embedding of the question, which is one quick call, and compare it against the stored vectors. The most similar documents are the search result. + +In short, whatever database layer you use: + +```php +use AIAccess\Embedding\Vector; + +// once, during indexing: store the vector with the document +[$vector] = $client->calculateEmbeddings([$text], 'text-embedding-3-small'); +$binary = $vector->serialize(); + +// when searching: compute the question's vector and compare with the stored ones +[$query] = $client->calculateEmbeddings([$question], 'text-embedding-3-small'); + +$scores = []; +foreach ($storedArticles as $id => $storedBinary) { + $scores[$id] = $query->cosineSimilarity(Vector::deserialize($storedBinary)); +} +arsort($scores); +$best = array_slice($scores, 0, 5, preserve_keys: true); +``` + +And now the best part: the excerpts you found do not have to be the goal; they can be the raw material. Send them together with the original question to the model in a [conversation |chat] and instead of a list of links you get a coherent answer built on your own data, which the model never saw during training. This combination of search and answering is called RAG, and it is today the most common way to give a model knowledge it does not have. + + +Where to Store the Vectors +========================== + +The `Vector::serialize()` method turns a vector into a binary string suited to a `BLOB` or `VARBINARY` column. The static `Vector::deserialize()` converts it back. The numbers are stored as 32-bit values in a fixed byte order. + +You may have heard the term **vector database**. It is a store that can find the most similar vectors without walking through all of them; it builds an index over them much as an ordinary database builds an index over a column. Examples include PostgreSQL with the pgvector extension, SQLite with sqlite-vec, or standalone services such as Qdrant. + +As long as you are in the thousands of documents, though, **you need none of them**. A few thousand vectors take a few tens of megabytes in memory and walking them linearly takes single-digit milliseconds, so a plain array and the `foreach` above are a full solution. Start considering dedicated storage once you have hundreds of thousands of records, or once the walk starts slowing you down. + + +What It Costs +============= + +Embeddings are cheap compared with a conversation with a model. You pay only for the input, because no output text is produced, and it is billed by [tokens |getting-started#What It Costs] just like chat. + +In practice that means indexing a few thousand articles usually costs less than you expect, and a single user query is negligible. The one item that can surprise you is **recomputing the whole database** after a model change, because you pay for absolutely everything again. + + +What to Watch Out For +===================== + +**Indexing and querying must use the same model.** Every model has its own space, so vectors from different models cannot be compared. The tricky part is how that shows up: when they have a different number of values, `cosineSimilarity()` throws `AIAccess\LogicException` and you find out immediately. When they happen to have the same number, **nothing fails** and you merely get nonsensical ordering. So after changing a model, always recompute the whole database. + +The rest is smaller: + +- **Empty input ends in an exception.** An empty array, or an empty string inside it, throws `AIAccess\LogicException` before anything is sent, which beats paying for a request that yields nothing. +- **One call handles many texts at once** and is markedly faster and cheaper than calling them one by one. OpenAI accepts up to 2048 inputs per request. +- **Split long documents into parts.** Models cap the input length, and more importantly, the longer the text, the blurrier its meaning. Shorter excerpts are found more precisely. + + +Differences Between Providers +============================= + +Only two of the five providers offer embeddings; Claude, DeepSeek and Grok have no embedding API of their own. + +| Provider | Optional extras | +|----------|-----------------------------------------------------------------------------------------| +| OpenAI | `dimensions` shortens the vector and saves database space, on `text-embedding-3` models | +| Gemini | `taskType` says what the vector is for, for example `RETRIEVAL_DOCUMENT` | + +Gemini distinguishes whether you are storing a text in the index or asking with it, and adjusts the vector slightly. If you use `title` there, you must also set `taskType` to `RETRIEVAL_DOCUMENT`, otherwise you get `AIAccess\LogicException`; it is a document that can be titled, not a query. + + +Where to Go Next +================ + +- [Conversation |chat] - how to hand the excerpts you found to the model +- [Batch processing |batch] - when you need to index a really large amount of text +- [Error handling |errors] - what to do when a call fails +- [Providers |providers] - what each one can do and how they differ diff --git a/ai-access/en/errors.texy b/ai-access/en/errors.texy new file mode 100644 index 0000000000..bbea0eea81 --- /dev/null +++ b/ai-access/en/errors.texy @@ -0,0 +1,135 @@ +Error Handling and Exceptions +***************************** + +.[perex] +A call to someone else's API fails sooner or later, always. What matters is not that it failed but whether it is worth trying again. The exceptions in AI Access are built around exactly that question. We will look at which ones exist, how to catch them, and why a model's refusal is not among them. + + +The Only Question That Matters in Production +============================================ + +When a call fails you could ask plenty of things. In a running application only one decides what happens next: **should I repeat it, or is it hopeless?** + +A dropped network is a different thing from a wrong key. The first fixes itself in a second, the second will not be fixed by a hundred attempts. If the library threw one type of exception for both, you would have to decide by the text of the message, and that is the most brittle code you can write. + +So the exceptions are split by what you can do about them: + +| Exception | What happened | Repeat? | +|-------------------------------|-----------------------------------------------------------------------|-----------------------------------| +| `ApiException` | The provider answered with an error. `getCode()` has the HTTP status. | Depends on the status, see below. | +| `CommunicationException` | We did not get through, or the response was unreadable. | Yes, it almost always helps. | +| `UnexpectedResponseException` | A response arrived but does not have the expected shape. | No. Log it and look into it. | +| `TooManyRoundsException` | The [tool loop |tools] ran out of rounds. | No. Raise the limit, or give up. | +| `LogicException` | A mistake in your own code. | No. It is meant to crash. | +| `IOException` | A file cannot be read or written. | Depends on the cause. | + +The first four share the ancestor `AIAccess\ServiceException`, so a single `catch` covers them when all you need to know is that the service failed. `TooManyRoundsException` also carries the last response in its `$lastResponse` property, and the history stays whole, so the conversation can go on once you raise the limit. `AIAccess\LogicException`, on the other hand, extends PHP's class of the same name, so it fits into handling you may already have. + +`AIAccess\IOException` stands apart entirely, because it has nothing to do with the provider: you meet it at `Media::fromFile()` and `Media::save()`, that is when a reference cannot be read or a generated image cannot be saved, and at the [caching client |http] when the cache directory cannot be created. + + +How to Catch It +=============== + +From the most specific to the most general, as is the custom in PHP: + +```php +try { + $response = $chat->sendMessage('Hello!'); + echo $response->getText(); + +} catch (AIAccess\ApiException $e) { + // the provider answered with an error, $e->getCode() is the HTTP status + if ($e->getCode() === 429) { + // rate limited, try again shortly + } + +} catch (AIAccess\CommunicationException $e) { + // we did not get through; repeating makes sense + +} catch (AIAccess\ServiceException $e) { + // anything else the service can get wrong +} +``` + +If the distinction does not matter to you, a single `catch (AIAccess\ServiceException $e)` will do. What you should definitely not do is catch `\Throwable`: that would swallow `LogicException` too, the very bug you want to see. + + +What the Individual Statuses Mean +================================= + +`ApiException` is the only one where looking at `getCode()` pays off, because the HTTP status underneath says a lot: + +- **401 and 403** - the key is wrong, missing, expired, or has no right to this model. Repeating will not help. +- **404** - no model of that name exists. Usually a typo or a model the provider retired. +- **429** - the rate limit is exhausted or the credit is empty. Wait and try again; the provider often sends a `Retry-After` header saying how long. +- **400** - the provider does not like the request. Typically a parameter that the model does not know; the exception message usually says which. +- **500 and above** - a problem on their side. Repeating makes sense. + +A special case is OpenAI, which can fail **inside a successful response**: the HTTP status is 200 but the state inside is `failed`. The library spots this and throws `ApiException` just as if an error status had arrived, so you do not have to deal with it. + + +A Refusal Is Not an Error +========================= + +This is the most common misunderstanding. When a model declines to answer because it does not like the question, **that is not an exception**. The request went through fine, the provider answered and billed it; there is just no text in the answer. + +You recognize it by the [finish reason |chat#Why the Model Stopped Writing]: + +```php +use AIAccess\Chat\FinishReason; + +$response = $chat->sendMessage($question); + +if ($response->getFinishReason() === FinishReason::ContentFiltered) { + // the model refused; on OpenAI $response->getRefusal() tells you why +} +``` + +By the same logic, neither an answer cut off by an exhausted token limit nor a round in which the model asked for a tool instead of answering is an error. In all three cases the response is valid, just different from what you expected. + + +Errors That Are Never Thrown +============================ + +In two places an exception would make no sense, so it is not used there. + +**[Batch processing |batch] can fail only partially.** Out of a hundred requests, ninety-nine go through and one does not. Reading the results should not blow up because of that one, so per-item errors are collected separately: + +```php +foreach ($batch->getResults() as $customId => $result) { + if ($result->message === null) { + echo $customId, ' failed: ', $result->error, "\n"; + } else { + echo $customId, ': ', $result->message->getText(), "\n"; + } +} +``` + +**An error in a [tool call |tools] may belong to the model, not to you.** When the model invents a tool that does not exist, or sends arguments that do not match the schema, it receives an error message as the result and can correct itself; your code need not know at all. But when **your own** tool fails, the exception reaches you unless you turn on `setToolLoop(catchErrors: true)`. Even then a typo in the handler will not disappear: a `TypeError` and its kin always propagate, because that is not a mistake for the model to solve. + +The library never uses `trigger_error()`, so no problem gets lost merely because the application has `display_errors` off. The one warning that remains points out alternating roles on Gemini. + + +You Do Not Have to Write the Retrying +===================================== + +If reading the table above made you think about writing a `for` loop with a delay, you do not have to. The library ships a [decorator |http] that does it, honors `Retry-After` and repeats nothing that would fail identically: + +```php +$client = new AIAccess\Provider\OpenAI\Client( + $apiKey, + new AIAccess\Http\RetryClient(new AIAccess\Http\CurlClient), +); +``` + +From then on rate limits and outages take care of themselves, and only what genuinely did not go through reaches your `catch`. + + +Where to Go Next +================ + +- [HTTP layer |http] - retrying, logging and caching of requests +- [Conversation |chat] - finish reasons and what you read out of a response +- [Batch processing |batch] - when some of the requests fail +- [Tool calling |tools] - errors that go back to the model diff --git a/ai-access/en/getting-started.texy b/ai-access/en/getting-started.texy new file mode 100644 index 0000000000..2299c49a05 --- /dev/null +++ b/ai-access/en/getting-started.texy @@ -0,0 +1,190 @@ +Getting Started with AI Access +****************************** + +.[perex] +From an empty project to the model's first answer. We will explain three terms you cannot do without, choose a provider and a model, get a key, run the first script, and look at what it costs and what to do when it does not work. + + +Three Terms You Need to Know +============================ + +Before you write the first line, it pays to understand three words that this documentation keeps repeating. + +**Provider** is the company that runs the language models and bills you for using them. AI Access knows five: OpenAI (famous for ChatGPT), Anthropic (the Claude models), Google (the Gemini models), the Chinese DeepSeek and xAI (the Grok models). You open an account with one of them. + +**Model** is the particular brain you are talking to. Each provider offers several and they differ in price and ability: small and cheap ones handle classification or summarizing, large and expensive ones cope with complex reasoning. In code a model is identified by its name, such as `gpt-5.6-luna`. + +**API key** is a long random string that tells the provider who is calling and whom to bill. It works as a password and a payment card at the same time. + +Calling a model **costs money**. Not large amounts, an ordinary question costs a fraction of a cent, but the bill starts with the first call, so you will most likely have to top up credit or enter a payment card with the provider. Some providers give newcomers a small free credit. + + +Which Provider to Choose +======================== + +The good news is that this choice is not fatal. Switching provider in AI Access is a one-line change, so if one does not suit you, you try another without rewriting the application. + +For ordinary conversation they all do well. The differences are elsewhere: + +- **Breadth of capabilities.** Do you want embeddings for search, batch processing or image generation alongside chat? Look at the [capability table |@home#What the Library Can Do]; OpenAI and Gemini have the widest reach. +- **Price.** Between the cheapest small model and the most expensive reasoning one there is a difference of roughly two orders of magnitude. DeepSeek tends to be markedly cheaper than the rest. +- **Where the data goes.** For company use it often comes down to where the data is processed and what the provider may keep for training. Look for the answer in their terms of service, not in a library's documentation. +- **Availability.** Not every provider is reachable from everywhere, and some features, such as Gemini's batch processing and image generation, require a project with active billing. + +If you are unsure, start with the one you can pay easily and concentrate on the application itself. You can switch at any time later. + + +Which Model to Choose +===================== + +Inside each provider you then choose among models. Simplified, three rules hold. + +**Start small.** Models labeled `flash`, `mini` or `lite` are cheap and fast, and they are plenty for summarizing, classification, rephrasing or pulling data out of text. A large model will not do these tasks much better, only slower and at a higher price. + +**Reach for a large one only when the small one fails.** You will know from the results: the model invents things, ignores instructions or loses the thread in a longer task. Only then does moving up pay off. + +**Reasoning models are a category of their own.** Before answering they think, which costs time and extra tokens, but they handle tasks with several steps. How much thinking you want is set through [reasoning effort |options]; for simple questions feel free to turn it off. + +A sensible starting point as of August 2026 looks like this: + +| Provider | Chat model | Embedding model | +|----------|-------------------------|--------------------------| +| OpenAI | `gpt-5.6-luna` | `text-embedding-3-small` | +| Claude | `claude-sonnet-5` | – | +| Gemini | `gemini-3.5-flash-lite` | `gemini-embedding-2` | +| DeepSeek | `deepseek-v4-flash` | – | +| Grok | `grok-4.3` | – | + +Models change faster than any documentation can keep up with, so treat the table as a starting point rather than as law. A model name is an ordinary string in code, so a new model works the day it ships; whether yours still exists you check with [listModels() |providers#Which Models a Provider Currently Offers]. + + +Getting the Key +=============== + +Keys are issued in the provider's console. The procedure is the same everywhere: create an account, add a payment method or credit, and generate a new key in the API keys section. **You will see the key only once**, so store it right away; if you lose it, you generate another. + +| Provider | Console | +|----------|----------------------------------------------------------------------| +| OpenAI | [platform.openai.com |https://platform.openai.com/api-keys] | +| Claude | [console.anthropic.com |https://console.anthropic.com/settings/keys] | +| Gemini | [aistudio.google.com |https://aistudio.google.com/app/apikey] | +| DeepSeek | [platform.deepseek.com |https://platform.deepseek.com/api_keys] | +| Grok | [console.x.ai |https://console.x.ai/team/default/api-keys] | + +The same console holds the current price list and a summary of what you have spent so far. Right at the start it pays to set a monthly spending limit there; it is the simplest insurance against a mistake in a loop. + + +The First Script +================ + +This is a complete file you can save and run. The key is written in it directly, because right now the point is that it works on the first try. In a moment we will look at where it really belongs. + +```php +require __DIR__ . '/vendor/autoload.php'; + +$client = new AIAccess\Provider\OpenAI\Client('paste-your-key-here'); + +$chat = $client->createChat('gpt-5.6-luna'); +$response = $chat->sendMessage('Explain in one sentence what dependency injection is.'); + +echo $response->getText(), "\n"; +``` + +Run it from the command line with `php file.php` and in a moment you will see the answer. `createChat()` opens a conversation over the chosen model, `sendMessage()` sends a message and waits for the answer. + +The return value is not a string but a response object. Besides the text it tells you why the model stopped writing and what it cost. + + +Where the Key Belongs in Production +=================================== + +A key written in the code is fine for a first try and for nothing beyond that. Whoever obtains it spends on your account, and the most common leak is a key accidentally committed to git. Taking it back only looks possible: you can rewrite history, but once the commit has reached a server you must treat the key as leaked, because anyone could have copied it in the meantime. There is only one reliable fix, namely to issue a new key and revoke the old one. + +So the key is written outside the code, most often into an **environment variable**. That is a named value handed to the application by the operating system, the hosting or Docker, so it lives in the server's settings rather than in the project's files. In PHP you read it like this: + +```php +$apiKey = getenv('OPENAI_API_KEY'); +``` + +On your own machine you set it before running the script, on Windows with `set OPENAI_API_KEY=...`, on Linux and macOS with `export OPENAI_API_KEY=...`. Hosting providers usually have a field for it in their control panel. + +The other common route is a configuration file listed in `.gitignore`, so it is never versioned. In a Nette application the key belongs in the local configuration and from there into the [DI container |dependency-injection:]: + +```neon +parameters: + openaiApiKey: '...' + +services: + - AIAccess\Provider\OpenAI\Client(%openaiApiKey%, chatModel: 'gpt-5.6-luna') +``` + +Note the second parameter. **A model name belongs next to the key, not in the application**, because it is a string that belongs wholly to one provider. A client carrying its own model then fills it in, and the call goes without one: + +```php +$chat = $client->createChat(); +``` + +That buys you the nicest part of switching providers: the client arrives through the constructor and your class truly never learns which provider or model it is talking to. Passing a model to the call still works and takes precedence; a client with no default that is given none in the call reports a `LogicException`. + + +What It Costs +============= + +Providers bill by **tokens**, which are pieces of words. English text runs about four characters to a token, and languages with diacritics and inflection considerably fewer, so the same sentence costs more tokens in Czech than in English. + +You pay separately for the input, meaning everything you send the model, and for the output, meaning what it writes. **Output is usually several times more expensive than input.** The thinking of reasoning models counts as output, even though you never see it. + +We deliberately do not print actual prices here, because they change and would age quickly. You will find them on each provider's website under Pricing and in the same console where you issued the key. To give a sense of scale: a short question with a short answer on a small model costs a fraction of a cent, whereas summarizing long documents over and over with the largest reasoning model becomes a line you notice in the accounts. **Between the cheapest and the most expensive model of the same provider there is usually a difference of two orders of magnitude**, so the choice of model shapes your bill far more than optimizing the prompt. + +What a particular call cost is reported by the response itself: + +```php +$usage = $response->getUsage(); + +echo 'input: ', $usage->inputTokens, "\n"; +echo 'output: ', $usage->outputTokens, "\n"; +echo 'total: ', $usage->getTotalTokens(), "\n"; +``` + +Two further numbers deserve attention. `reasoningTokens` is what the model spent on thinking, and on reasoning models it is often larger than the answer itself. `cacheReadTokens`, on the other hand, says how much of the input came from the provider's cache at a fraction of the price; when you keep sending the same long system instruction, that number is your friend. + + +When the First Call Fails +========================= + +Errors are reported as exceptions and their type tells you what happened before you even read the message. This is how you catch them: + +```php +try { + $response = $chat->sendMessage('Hello!'); + echo $response->getText(); + +} catch (AIAccess\ApiException $e) { + // the provider answered with an error; the code is the HTTP status + echo 'The API returned error ', $e->getCode(), ': ', $e->getMessage(); + +} catch (AIAccess\CommunicationException $e) { + // we did not get through, or the response was unreadable + echo 'The connection failed: ', $e->getMessage(); +} +``` + +What the most common statuses inside `AIAccess\ApiException` mean: + +- **401** - the key is wrong, missing, or belongs to a different provider. +- **404** - no model of that name exists. Usually a typo or a model the provider retired; print the [list of models |providers#Which Models a Provider Currently Offers]. +- **429** - you hit a rate limit, or your credit is empty. [RetryClient |http] can do the repeating for you. +- **500 and above** - a problem on the provider's side, retrying makes sense. + +Besides those two exceptions there are also `AIAccess\UnexpectedResponseException`, when a response does not have the expected structure, and `AIAccess\LogicException` for a mistake in your own code, such as sending a conversation without a single message. There is no point catching the last one; it is meant to crash and tell you. + +The whole hierarchy, including how to handle errors once for the entire application, is covered by the chapter on [error handling |errors]. + + +Where to Go Next +================ + +- [Conversation |chat] - several messages in a row, the system instruction and reading the answer +- [Options and reasoning effort |options] - how much thinking you ask the model for +- [Streaming |streaming] - so the user is not staring at a blank page +- [Error handling |errors] - what to do when the provider says no diff --git a/ai-access/en/http.texy b/ai-access/en/http.texy new file mode 100644 index 0000000000..32262e3713 --- /dev/null +++ b/ai-access/en/http.texy @@ -0,0 +1,150 @@ +HTTP Layer and Retrying Requests +******************************** + +.[perex] +Every call to a provider passes through a single thin layer that you can replace or wrap. That is why retrying after errors, logging requests and caching responses are solved once for the whole application, without touching the code that talks to the model. + + +Three Problems, One Place +========================= + +Once your application has been running for a while, you will meet these: + +- The provider occasionally answers "too fast, slow down" and the call fails, even though it would go through two seconds later. +- You need to see what is actually being sent out and how long it takes, because something is slow and you do not know what. +- During development you run the same script for the fiftieth time and pay for it every time, even though the input has not changed. + +All three are solved in one place, by replacing whatever sends the HTTP requests. The provider's client sends nothing on its own; it gets that from an object you hand it as the second constructor argument: + +```php +use AIAccess\Http; + +$client = new AIAccess\Provider\OpenAI\Client( + $apiKey, + new Http\RetryClient(new Http\CurlClient), +); +``` + +Leave the second argument out and `AIAccess\Http\CurlClient` is used. The library ships three wrappers that compose freely, because each of them is itself just an HTTP client. + + +RetryClient: Retrying After Rate Limits and Outages +=================================================== + +`RetryClient` retries failed requests, but only when there is a point: + +```php +$http = new Http\RetryClient( + new Http\CurlClient, + maxAttempts: 3, + initialDelay: 1.0, + maxDelay: 30.0, +); +``` + +Statuses **408 and 429** are repeated, as are server errors **from 500 up**, plus network failures that happened before a response arrived. Between attempts it waits, doubling the delay each time and scattering it slightly at random, so that a thousand parallel processes do not hit the provider at the same instant. When the provider sends a `Retry-After` header, that wins. + +What is not repeated is just as instructive. Errors in the four hundreds other than 408 and 429 would fail identically the second time, so repeating would only add delay. And among the server errors, **501 and 505** are excluded, because they do not say "not now" but "I cannot do this and never will". + +The most interesting rule concerns [streaming |streaming]: once the first piece of the answer has arrived, retrying is disabled. The model has started writing and you are already paying for it; replaying the request would give you the answer twice and bill it twice. + + +ObservableClient: Seeing What Happens +===================================== + +`ObservableClient` reports every request and every response along with how long it took. It fits a log, [Tracy|tracy:], or your own spending overview. + +```php +$http = new Http\ObservableClient( + new Http\CurlClient, + onRequest: function (string $url, $payload): void { + Debugger::log("-> $url"); + }, + onResponse: function (Http\Response $response, float $elapsed): void { + Debugger::log(sprintf('<- %d in %.1f s', $response->getStatusCode(), $elapsed)); + }, + onError: function (Throwable $e, float $elapsed): void { + Debugger::log(sprintf('!! %s after %.1f s', $e->getMessage(), $elapsed)); + }, +); +``` + +A request that produced no response at all goes to `onError`: a timeout or a closed connection, but also anything your own streaming callback throws, because from here the two are indistinguishable. Those are exactly the requests you want in the log most, and `onResponse` has no way to report them. The exception then travels on to you unchanged. + +Notice that `onRequest` does not receive the headers. That is not an omission: headers carry the API key, and it must not reach a log where anyone with access to the files can read it. + +For a streamed response, `$elapsed` measures the whole transfer, meaning the time until the model finished writing, not the time to the first word. + + +CachingClient: Not Paying Twice for the Same Thing +================================================== + +`CachingClient` stores responses on disk and does not send the same request twice. It is a tool **for development and tests**, not for production. A model that answers the same question identically forever is not a feature in a live application; it is a bug. + +```php +$http = new Http\CachingClient(new Http\CurlClient, __DIR__ . '/temp/ai-cache', ttl: 3600); +``` + +The cache key is computed from the method, the URL, the request body and the headers, except that **authentication headers are left out of it**. Thanks to that, switching to another key does not cost you the cache, while a header that changes how the API behaves does. Only successful responses are stored, so an error is never remembered, and streams and file uploads pass through untouched. + + +The Order of Wrappers Matters +============================= + +Wrappers nest, and the result differs depending on which one is on the outside: + +```php +// logs only the final outcome: the retrying happens inside, and only the result comes out +$http = new Http\ObservableClient(new Http\RetryClient(new Http\CurlClient), onResponse: $log); + +// logs every attempt including the failed ones: the logging sits inside the retry loop +$http = new Http\RetryClient(new Http\ObservableClient(new Http\CurlClient, onResponse: $log)); +``` + +Neither is wrong, just know which one you wrote. While debugging rate limits you want the second, in a production log rather the first. + + +Timeouts and the Connection +=========================== + +`CurlClient` itself has two time settings and one for a proxy: + +```php +$http = (new Http\CurlClient)->setOptions(connectTimeout: 10, requestTimeout: 180); +``` + +The default three minutes are enough for an ordinary conversation, but not always. Generating a high-quality image with reference pictures easily takes several minutes, so raise `requestTimeout` there; you recognize the problem by a `CommunicationException` arriving exactly when the limit expires. + +For a [streamed |streaming] response something else applies and it is worth remembering: **no total time cap is used at all**. A long answer legitimately flows for minutes, and cutting it off midway would throw away text the user is reading right now. Silence is watched instead: when nothing arrives for a while, the connection gives up. The limit is therefore "it stopped flowing", not "it is taking long". + +The connection also stays open between requests. You notice it most in a [tool loop |tools], which is really a burst of calls to the same server in quick succession; without it, every round would negotiate TLS again. + + +Your Own Implementation +======================= + +The `Http\Client` interface has a single method: + +```php +interface Client +{ + function fetch( + string $url, + string|array|FormData|null $payload = null, + array $headers = [], + ?string $method = null, + ?\Closure $onChunk = null, + ): Response; +} +``` + +Streaming is not marked by a separate method but by whether `$onChunk` is given. You will appreciate your own implementation mostly in tests, where you want to prescribe responses instead of calling the API, but also when requests have to travel through something unusual. + + +Where to Go Next +================ + +- [Error handling |errors] - which errors are worth repeating and why +- [Streaming |streaming] - why silence rather than time is measured on a stream +- [Image generation |images] - where the default timeout may not be enough +- [Providers |providers] - what each one can do and how they differ diff --git a/ai-access/en/images.texy b/ai-access/en/images.texy new file mode 100644 index 0000000000..bffe89b8f7 --- /dev/null +++ b/ai-access/en/images.texy @@ -0,0 +1,108 @@ +Image Generation +**************** + +.[perex] +You describe what you want to see and get an image back. We will look at how to generate and save one, how to give the model a reference to work from, how the providers differ, and what you will run into once price and waiting time come up. + + +The First Image +=============== + +Generating takes a single method. You tell it which model should draw, and what: + +```php +$client = new AIAccess\Provider\OpenAI\Client($apiKey); + +$image = $client->generateImage('A lighthouse on a cliff in a storm, flat vector illustration', 'gpt-image-2'); +$image->save('/path/to/lighthouse.png'); +``` + +The return value is not a string with a URL but a `Media` object holding the image data itself. You already know it from the chapter on [images as input |multimodal]; it is the same object, traveling to the model one way and back from it the other. + +Besides `save()` it gives you the raw data through `getData()`, in case you want to store the image yourself, in a database say, or send it straight to the browser. + + +Find Out What You Received +========================== + +Here is the first trap that is easy to fall into: **do not assume you will get a PNG**. OpenAI sends PNG by default, but Gemini returns JPEG. Hard-code the `.png` extension and you end up with a JPEG saved under the wrong name. + +The image tells you its content type, so use it: + +```php +$extension = explode('/', $image->getMimeType())[1]; +$image->save("/path/to/lighthouse.$extension"); +``` + + +A Reference Instead of Words Alone +================================== + +Alongside the description you can give the model images to work from. That is useful for edits, for variations on one motif, or for holding a single style across a set of images: + +```php +use AIAccess\Media; + +$image = $client->generateImage( + 'The same lighthouse, but on a sunny morning', + references: [Media::fromFile('/path/to/lighthouse.png')], +); +``` + +There can be more than one reference. **OpenAI and Gemini** take them; on Grok the parameter does not exist at all, because xAI draws from a description alone. + +The shared `Image\Service` interface therefore promises only generating from a description, which is what all three can do. References are a named argument of the concrete client, exactly like every other provider-specific option. Whoever edits reaches for `OpenAI\Client` or `Gemini\Client`. + + +What Each Provider Can Do +========================= + +| Provider | Generating | References | Where it runs | +|----------|------------|------------|------------------------------------| +| OpenAI | ✅ | ✅ | a separate endpoint for images | +| Gemini | ✅ | ✅ | ordinary chat, with an image model | +| Grok | ✅ | ➖ | a separate endpoint for images | +| Claude | ➖ | ➖ | does not generate images | +| DeepSeek | ➖ | ➖ | does not generate images | + +The last column is worth explaining, because it is a neat illustration of what the library does for you. **Gemini has no image endpoint at all.** Its image models are addressed exactly like ordinary chat, only asking for a picture instead of text. You never need to know that: `generateImage()` looks the same with all three providers. + +OpenAI additionally takes the optional `size`, `quality`, `background` and `format` parameters, telling it how large and how good an image you want and whether the background should be transparent: + +```php +$image = $client->generateImage( + 'An envelope icon, flat style', + 'gpt-image-2', + size: '1024x1024', + quality: 'low', + background: 'transparent', +); +``` + + +Before You Put It into Production +================================= + +**Images are orders of magnitude more expensive than text.** While an ordinary question costs a fraction of a cent, a single high-quality image costs real money. While testing, generate at low quality and a smaller resolution; that is more than enough to prove the code works. + +**When you generate many of them, consider [batch processing |batch].** Providers charge roughly half for a deferred answer, and in practice image generation in a batch tends not to be slower than one at a time, rather the opposite. Images are added to an ordinary batch with `addImageRequest()`, which OpenAI and Gemini support. + +**Generating takes a long time.** An ordinary image appears within seconds, but high quality with references can take minutes. The HTTP client's default timeout is 180 seconds, which may not be enough for those cases, so raise it: + +```php +$http = (new AIAccess\Http\CurlClient)->setOptions(requestTimeout: 600); +$client = new AIAccess\Provider\OpenAI\Client($apiKey, $http); +``` + +**Gemini needs a paid project.** Its image models have a daily quota of zero on the free tier, so the call fails with a quota error even though you have generated nothing yet. + +And one obvious thing that is easy to forget: instead of an image the model may refuse to draw, typically for content its rules do not allow. You then get an `AIAccess\UnexpectedResponseException`, because there is no image in the response. Expect that, especially when the description is assembled from user input. + + +Where to Go Next +================ + +- [Images and documents as input |multimodal] - the opposite direction, when the model should look at an image +- [HTTP layer |http] - timeouts, retries and request logging +- [Error handling |errors] - what the individual exceptions mean +- [Providers |providers] - what each one can do and how they differ diff --git a/ai-access/en/multimodal.texy b/ai-access/en/multimodal.texy new file mode 100644 index 0000000000..63eea5cbe6 --- /dev/null +++ b/ai-access/en/multimodal.texy @@ -0,0 +1,100 @@ +Images and Documents as Input +***************************** + +.[perex] +You can attach a photo or a PDF to a message and ask about its contents. We will look at how a file is attached, what each provider can take, and why an attached image is paid for again in every further turn of the conversation. + + +Ask About an Image +================== + +A message does not have to be only text. When you hand it an array instead of a string, the array can hold a file alongside the text: + +```php +use AIAccess\Media; + +$chat = $client->createChat('gpt-5.6-luna'); + +$response = $chat->sendMessage([ + 'What is in this picture?', + Media::fromFile('/path/to/photo.jpg'), +]); + +echo $response->getText(); +``` + +`Media::fromFile()` loads the file and works out its content type on its own, so you do not have to tell it anything. The model then sees the image and the question at once and answers both. + +This covers a surprising amount of work: reading figures off a receipt or an invoice, having a caption written for a photo so screen readers can read it out, reading an error message from a screenshot a user sent you, or checking that an uploaded photo really shows what it should. + + +When the File Is Not on Disk +============================ + +An image often arrives from a form or from a database and is not on disk at all. That is what `fromBinary()` is for, taking the data and the content type: + +```php +$media = Media::fromBinary($bytes, 'image/png'); +``` + +The file name is an optional third argument. For images it does not matter, but for documents OpenAI requires it and shows it to the model, so a name like `invoice-2026-03.pdf` carries information by itself. When you leave it out, the library fills in a generic one based on the content type so the request does not fail. + + +Documents, PDFs Above All +========================= + +Documents are attached exactly like images: + +```php +$response = $chat->sendMessage([ + 'Summarize the main points of this contract.', + Media::fromFile('/path/to/contract.pdf'), +]); +``` + +They differ only in which providers accept them. The model copes with a table or a form as well, which is where ordinary text extraction in PHP breaks down. So you do not have to convert the PDF to text beforehand. + + +What Each Provider Accepts +========================== + +| Provider | Images | Documents | +|----------|--------|-----------| +| OpenAI | ✅ | ✅ | +| Claude | ✅ | ✅ | +| Gemini | ✅ | ✅ | +| Grok | ✅ | ➖ | +| DeepSeek | ➖ | ➖ | + +DeepSeek has no model that can see yet; Grok accepts images but not documents. + +When you send a provider content it cannot process, **you do not find out from an API error**. The library notices before the request leaves and throws `AIAccess\LogicException` naming the content type, so you know straight away what was wrong: + +``` +DeepSeek cannot send image/png content: it has no vision model. +``` + +There is no point catching this exception and repeating the request. It says you are sending an image to a model that cannot see, and no further attempt will fix that; the fix belongs in the code, for instance by choosing a different provider. + + +An Image in the History Is Paid For Again +========================================= + +This is the most common unpleasant surprise on the bill. An attached image becomes part of the conversation history, and because the whole history is sent again with every further question, **the image is sent and paid for again too**. + +Measured on a simple example: the first turn with the image cost 49 input tokens, and the second turn, which was only a short follow-up question in text, cost 64. The difference is not that short question but the image sent a second time. + +With large images and longer conversations this grows quickly. There are three things you can do about it: + +- **Ask everything about the image at once.** When you know what you need from it, ask in one message rather than in five. +- **Start over once the image is exhausted.** Store the model's answer and clear the conversation with `clearMessages()`; carry on over the text that already describes the image. +- **Shrink the image beforehand.** The price grows with resolution, and "is there a stamp on the invoice" needs a smaller image than reading fine print. + + +Where to Go Next +================ + +- [Image generation |images] - the opposite direction, when an image is to be created +- [Conversation |chat] - how the history works and why it grows +- [Structured output |structured-output] - when you need data straight out of a receipt +- [Providers |providers] - what each one can do and how they differ diff --git a/ai-access/en/options.texy b/ai-access/en/options.texy new file mode 100644 index 0000000000..bc2fc03a91 --- /dev/null +++ b/ai-access/en/options.texy @@ -0,0 +1,97 @@ +Options and Reasoning Effort +**************************** + +.[perex] +The most important thing you set on a model today is how much it should think through before it starts answering. It shapes quality, speed and price alike. We will look at how one dial controls that across all providers, where to find the rest of the settings, and at the end at what happened to the `temperature` parameter you may have read about elsewhere. + + +How Much the Model Thinks Before Answering +========================================== + +Newer models can do something the older ones could not: before they start writing the answer, they write themselves a kind of draft reasoning. They take the task apart, try an approach, find a mistake in it, fix it, and only then answer. This draft is called reasoning or thinking. + +There are two things you need to know about it. **It does not reach the answer itself**, so `getText()` never contains it; some providers do return it separately, some only as a summary, and you can [read it |chat#What Is in the Response] with `getReasoning()`. And above all you **pay for it**, because it counts as output tokens, and on a harder task there are more of them than in the answer itself. + +How much the model should think through is what `setEffort()` says: + +```php +use AIAccess\Chat\Effort; + +$chat = $client->createChat('gpt-5.6-luna'); +$chat->setEffort(Effort::Low); + +echo $chat->sendMessage('Which category does this complaint belong to?')->getText(); +``` + +There are six levels: `None`, `Low`, `Medium`, `High`, `XHigh` and `Max`. `None` means the model should not think at all and should answer straight away. + +Each provider translates the value into its own, because they do not even agree on the naming: + +| Provider | What it is called there | +|----------|-------------------------------------------------------------| +| Claude | `output_config.effort`, and `None` also disables `thinking` | +| OpenAI | `reasoning.effort` | +| Gemini | `thinkingConfig.thinkingLevel`, `None` is a zero budget | +| DeepSeek | `thinking.reasoning_effort` | +| Grok | `reasoning_effort` | + +Some providers do not know all six levels, so those map to the nearest one. Gemini has only three, which is why `High`, `XHigh` and `Max` all end up the same there. + +Two principles are worth saying out loud. First, **nothing is sent until you call `setEffort()`** and the provider's own default applies. That is exactly why DeepSeek thinks even though you never asked. Second, **the library keeps no table of model capabilities**. When a model lacks the dial, the provider answers with an error, and that is right; maintaining a list of what each model currently supports would mean documentation that ages before it is finished. + + +Which Level to Pick +=================== + +The choice of level is not cosmetic, because it directly decides how long you wait for the answer and what it costs. + +- **`None`** for tasks with nothing to think through: classification, extracting data from text, rephrasing a sentence, translation. The answer arrives fastest and cheapest. +- **`Low` and `Medium`** for ordinary work where the model needs a moment but not long: summarizing a longer text, drafting a reply, simpler decisions. +- **`High` and above** for tasks with several steps: analyzing code, mathematics, planning, reasoning over contradictory information. Expect the answer later and markedly more expensive. + +The cheapest optimization is usually discovering that the task does fine with `None`. The price difference between thinking turned off and turned up to maximum tends to be larger than the difference between two models. + + +Settings Each Provider Keeps to Itself +====================================== + +The rest of the settings is not unified, because it cannot be: only OpenAI has `store`, only Gemini has `safetySettings`, only Grok has `seed`. So they are **named arguments of the `setOptions()` method** on the specific provider class, not keys in a shared array. + +```php +// Claude +$chat->setOptions(maxOutputTokens: 1024, stopSequences: ['END']); + +// OpenAI +$chat->setOptions(maxOutputTokens: 1024, store: false, parallelToolCalls: true); +``` + +You feel the difference from an array as you type. The IDE offers exactly what that provider knows, and PHP itself catches a typo. A shared array would quietly swallow a key that belongs nowhere, and you would find out only by nothing happening. + +The most useful of them is `maxOutputTokens`, the cap on the answer's length. It has the same name with every provider because everyone needs it; on the wire it is called something different each time, `max_tokens` here and `max_completion_tokens` there, but that is the library's business. + +The same goes for `stopSequences`, the strings that make the model stop writing. On the wire it is `stop_sequences` in one place and `stop` in another; in the library it is `stopSequences` wherever it exists at all. It takes a single string as well as an array, so one sequence needs no array around it. The one provider without it is OpenAI: the Responses API has no such parameter. + +The complete list for each provider is in the `setOptions()` signature in `src/Provider/*/Chat.php`, or your IDE will show it. And if you use the [generic client |providers] for a third-party endpoint, it has an extra `custom` argument for pushing through anything that endpoint knows and the library does not. + + +What Happened to Temperature +============================ + +When you read about configuring models elsewhere, you will almost certainly run into the `temperature` parameter. It is worth knowing what it did and why this documentation does not recommend it. + +A model does not pick one single correct continuation of a sentence. At every moment it holds a list of words that could come next, each with a probability, and draws one of them. That is exactly why the same question gives you a slightly different answer every time. `temperature` decided how risky that draw would be: a value near zero meant a sober, predictable writer that almost always reaches for the most likely word, higher values meant more inventive text but also more inaccuracies. `top_p` and `top_k` did a similar thing in a different way. + +Reasoning models abandoned this way of steering. If you send them `temperature` anyway, some answer with HTTP 400, which is what Claude does on its newest models and OpenAI from GPT-5.1 on, and others **silently ignore** it, which is what Gemini does and DeepSeek does whenever it is thinking. + +The dangerous one is the second. An error at least tells you; a silently ignored parameter means the application looks like it works, you keep tuning values, and nothing happens at all. + +The library does not forbid `temperature` and on older models you are welcome to use it through `setOptions()`. Just do not build anything lasting on it: on the models shipping next year it will very likely not work at all. + + +Where to Go Next +================ + +- [Streaming |streaming] - read the answer while the model is still writing it +- [Tool calling |tools] - when the model should reach into your application +- [Structured output |structured-output] - when you need data, not prose +- [Providers |providers] - what each one can do and how they differ diff --git a/ai-access/en/providers.texy b/ai-access/en/providers.texy new file mode 100644 index 0000000000..3f8c5828e0 --- /dev/null +++ b/ai-access/en/providers.texy @@ -0,0 +1,183 @@ +Providers and Models +******************** + +.[perex] +A reference tour of the six clients: how each is created, what it can do, how they differ and what to watch out for. At the end we look at how to ask for the list of models a provider currently offers, and why that is more useful than it sounds. + + +Six Clients, One Interface +========================== + +Five clients talk to a specific provider, the sixth to anything that speaks the same dialect as OpenAI. They are all created the same way, with a key in the constructor: + +```php +$client = new AIAccess\Provider\OpenAI\Client($apiKey); +$client = new AIAccess\Provider\Claude\Client($apiKey); +$client = new AIAccess\Provider\Gemini\Client($apiKey); +$client = new AIAccess\Provider\DeepSeek\Client($apiKey); +$client = new AIAccess\Provider\Grok\Client($apiKey); +``` + +What differs is the set of interfaces each client implements. That is how you tell what to expect from it, and PHP checks it for you before the script even runs: + +| Client | `Chat\Service` | `Embedding\Service` | `Batch\Service` | `Image\Service` | +|----------|----------------|---------------------|-----------------|-----------------| +| OpenAI | ✅ | ✅ | ✅ | ✅ | +| Gemini | ✅ | ✅ | ✅ | ✅ | +| Claude | ✅ | ➖ | ✅ | ➖ | +| Grok | ✅ | ➖ | ➖ | ✅ | +| DeepSeek | ✅ | ➖ | ➖ | ➖ | +| Generic | ✅ | ➖ | ➖ | ➖ | + +When you write code that should work with any provider, type the parameter against the interface rather than the concrete class: + +```php +public function __construct( + private AIAccess\Chat\Service $client, +) { +} +``` + +Your application then knows nothing at all about the choice of provider, and switching is a change in [configuration |dependency-injection:]. + + +Default Models +============== + +For that to be true in earnest, the client has to know the model too. A model name is a string belonging wholly to one provider, so if the call had to supply it, the application would know the main thing about the provider after all. + +Models are therefore passed to the constructor, right next to the key: + +```php +$client = new AIAccess\Provider\OpenAI\Client( + $apiKey, + chatModel: 'gpt-5.6-luna', + imageModel: 'gpt-image-2', + embeddingModel: 'text-embedding-3-small', +); + +$chat = $client->createChat(); // the client fills the model in +$image = $client->generateImage('A lighthouse on a cliff'); +``` + +Each client takes only the models it can put to use: Claude and DeepSeek just `chatModel`, Grok `chatModel` and `imageModel`, OpenAI and Gemini all three. A parameter that does not exist is reported by PHP itself. + +A model given in the call always takes precedence, so a default never stops anyone from reaching elsewhere. And a client with no default that is given none in the call reports an `AIAccess\LogicException`; it is a configuration error, not a runtime one. + +A batch takes the defaults from the client that created it, so `addChat('id')` and `addImageRequest('id', $prompt)` go without them as well. + +In a Nette application the whole choice of provider therefore fits into the configuration, and that is the only place in the project where its name shows up: + +```neon +services: + - AIAccess\Provider\OpenAI\Client(%openaiApiKey%, chatModel: 'gpt-5.6-luna') +``` + +Switching to another provider is then a matter of that one line, and classes typed against `AIAccess\Chat\Service` will not even notice: + +```neon +services: + - AIAccess\Provider\Claude\Client(%anthropicApiKey%, chatModel: 'claude-sonnet-5') +``` + +A client that can do more gets a model for each of those things: + +```neon +services: + - AIAccess\Provider\OpenAI\Client( + %openaiApiKey%, + chatModel: 'gpt-5.6-luna', + imageModel: 'gpt-image-2', + embeddingModel: 'text-embedding-3-small', + ) +``` + +Always write the models by name, as we do here. The constructor's second parameter is the [HTTP client |http], not a model, so a model passed second in order would be taken for it. + + +How They Differ +=============== + +The set of interfaces is only half the story. These are the traits you meet in practice. + +**OpenAI** has the widest reach and is the only one that can upload files (`uploadFile()`, `uploadContent()`), which batch processing uses. It reports a refusal separately, so you reach it through `getRefusal()`. Through `setOptions()` you can also set the organization. + +**Claude** requires a cap on the answer's length with every request, so the library fills in `maxOutputTokens` as 4096 unless you set it otherwise. Anthropic offers no embeddings at all, so search will need a different provider. It can count tokens in advance with `countTokens()`. + +**Gemini** has no separate endpoint for image generation: image models are called through ordinary chat; you merely say the answer should be a picture. It also has `countTokens()`. **Batch processing and image generation, however, require a project with active billing**; on the free tier a batch ends in an error and image models have a daily quota of zero. A generated image arrives as JPEG, not PNG. + +**Grok** accepts images as input but not documents. It can generate images too, though without reference pictures. It reports a refusal in a message field of its own rather than as a finish reason, which the library translates into `FinishReason::ContentFiltered`. + +**DeepSeek** is the narrowest of the five: chat only, no vision, embeddings or batches. It is worth knowing that **thinking is on by default**, so you pay extra tokens until you [turn it off |options]. + + +The Generic Client for Everything Else +====================================== + +Plenty of services today speak the same dialect as OpenAI. `OpenAICompatible\Client` is there for those, and besides the key you also give it an address: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://api.mistral.ai/v1/'); +$response = $client->createChat('mistral-large-latest')->sendMessage('Hello!'); +``` + +**Ollama** running locally wants no key, so send it an empty string: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client('', 'http://localhost:11434/v1/'); +echo $client->createChat('llama3.2')->sendMessage('Hello!')->getText(); +``` + +**OpenRouter** brokers models from dozens of vendors and likes headers identifying the application: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://openrouter.ai/api/v1/'); +$client->setOptions(extraHeaders: ['HTTP-Referer' => 'https://example.com', 'X-Title' => 'My application']); +``` + +**Azure OpenAI** sends the key in a header of its own and without a prefix: + +```php +$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://myinstance.openai.azure.com/openai/v1/'); +$client->setOptions(authHeader: 'api-key', authPrefix: ''); +``` + +With the generic client one limitation deserves saying out loud: **the library sends what it knows how to send, but what the endpoint does with it is not guaranteed**. Tool calls, structured output and images as input all go out, yet whether the model behind that address handles them is decided by that service, not by us. When the endpoint knows an extra parameter, you push it through with the `custom` argument in [options |options]. + +Streaming from the generic client sends no `stream_options`, because that is an OpenAI invention and an unknown dialect may refuse the whole request over it. If your endpoint knows it and you want token usage from the stream, add it through `custom`. + + +Which Models a Provider Currently Offers +======================================== + +Because a model name is an ordinary string, nothing checks it for you: a typo or a retired model shows up only at runtime, and as you will see in a moment, sometimes not even then. So ask the provider for the current offering: + +```php +foreach ($client->listModels() as $model) { + echo $model->id, "\n"; +} +``` + +All six clients have the method and handle pagination internally, so you get the whole list at once. Besides `id`, every model carries all the metadata in `raw` exactly as the provider sent it; those are not unified, because each one sends something different. + +It is most useful as a deployment check. **A retired model need not announce itself with an error:** xAI, for instance, quietly redirects old names to a newer model, so the application keeps running and merely talks to something other than you thought. Checking against `listModels()` is the only way you find out. + +Claude and Gemini can additionally count how many tokens a conversation will cost **before you send it**: + +```php +$chat = $client->createChat('claude-sonnet-5'); +$chat->addMessage($longText, AIAccess\Chat\Role::User); + +echo 'This question will cost ', $chat->countTokens(), " input tokens.\n"; +``` + +It comes in handy when you assemble a long context and need to know whether it fits the model's limit, or what it will cost, before you pay for it. + + +Where to Go Next +================ + +- [Conversation |chat] - history, the system instruction and reading the answer +- [Options and reasoning effort |options] - how much thinking you ask the model for +- [Error handling |errors] - what to do when the provider says no +- [HTTP layer |http] - retrying, logging and caching requests diff --git a/ai-access/en/streaming.texy b/ai-access/en/streaming.texy new file mode 100644 index 0000000000..b12f7f5ad4 --- /dev/null +++ b/ai-access/en/streaming.texy @@ -0,0 +1,135 @@ +Streaming Responses +******************* + +.[perex] +The model writes its answer word by word and that takes tens of seconds. Streaming means reading it as it arrives instead of waiting for the whole thing. We will look at how one `foreach` does it, how to stop generating half way, and why it makes more sense in PHP than you would think. + + +Ten Seconds of Silence +====================== + +Without streaming this is what happens: the user submits a question, the page freezes and for ten seconds absolutely nothing happens. Then the whole text appears at once. Ten seconds of silence is an eternity in a browser, and the user has time to click again or leave. + +A streamed answer is read with a loop, just like an array: + +```php +$stream = $chat->sendMessageStream('Explain in three sentences why PHP is still everywhere.'); + +foreach ($stream as $delta) { + echo $delta; + flush(); +} +``` + +Each `$delta` is the piece of text that has just arrived, typically a word or part of one. `flush()` is there so that PHP really sends the pieces out instead of holding them in its output buffer. + +The answer does not arrive any sooner. What changes is the waiting: instead of a blank page the user watches the text grow, and that is the difference between an application that looks broken and one that looks fast. + +One property is worth remembering: **nothing is sent until you start reading**. Calling `sendMessageStream()` triggers no request at all; that happens on the first pass of the loop. So you can prepare a stream in advance and read it when it suits you. + +When a callback fits you better than a loop, there is a second route: + +```php +$chat->sendMessage( + 'Explain in three sentences why PHP is still everywhere.', + onStream: function (string $delta) { + echo $delta; + flush(); + }, +); +``` + + +Why It Makes More Sense in PHP Than You Would Think +=================================================== + +In a browser the benefit is obvious, in a server script less so. There are three reasons and they are worth spelling out, because they are easy to forget. + +**You can forward the stream straight to the browser.** When the frontend listens to Server-Sent Events, you pass on the individual pieces as they arrive. Collecting them all and sending them at once would defeat the whole point. + +**You can stop paying half way.** The moment you know the answer is wrong, or that what has arrived is enough, you stop the generation and the rest is never produced or billed. + +**Time to the first word matters more than the total.** For anything a human is watching, perceived speed beats measured speed. + + +When the Stream Ends It Is an Ordinary Response +=============================================== + +Once you have read it, you have everything you would have got without streaming: the usage, the finish reason and any tool calls. + +```php +foreach ($stream as $delta) { + echo $delta; +} + +$response = $stream->getResponse(); + +echo 'finished as ', $response->getFinishReason()->value, "\n"; +echo 'output tokens: ', $response->getUsage()?->outputTokens, "\n"; +``` + +`getResponse()` **does not send a second request**. If you have read the stream, it just hands you the finished result; if you have not, it quietly reads the rest and returns the whole thing. So you never pay for the same answer twice. + +And if the individual pieces do not really interest you and the point was only that the user sees something happening, there is a shortcut: + +```php +echo $stream->getText(); +``` + +A streamed answer goes into the [conversation history |chat] like any other, so the next message follows on without any work from you. + + +Stopping Half Way +================= + +Streaming gives you an option you do not otherwise have: stopping once you know enough. In a loop that is what the `cancel()` method is for: + +```php +foreach ($stream as $delta) { + echo $delta; + if (str_contains($delta, 'END')) { + $stream->cancel(); + break; + } +} +``` + +In the callback form you do the same with a `false` return value: + +```php +$chat->sendMessage($question, onStream: function (string $delta) { + echo $delta; + return !str_contains($delta, 'END'); // false ends the generation +}); +``` + +How is that communicated to the model? It is not, and that is the trick. The library **aborts the HTTP transfer in progress**, which closes the connection to the provider. The provider sees that the client has stopped listening and ends the generation; the rest of the answer is therefore never produced and never billed. The response then reports `FinishReason::Cancelled`, so even further down in the code you can tell the text is incomplete. It also stops the [tool loop |tools], because a half-read answer is no basis for the application to go and do something. That holds for a call cut off mid-arguments as well: the library drops such a fragment rather than running your tool with whatever managed to arrive. + +**A bare `break`, on the other hand, does not stop the generation.** It ends only your reading, while the request stays open and the model keeps writing. That is deliberate rather than an oversight: it lets you come back to the stream with another `foreach`, which picks up where you left off, or call `getResponse()`, which reads the rest without a second request. So `break` is a pause, while `cancel()` is an end. + + +Five Providers, Five Ways a Stream Ends +======================================= + +You do not need this for everyday use, but it shows nicely how much work hides under that one `foreach`. It was established by measuring real responses rather than by reading documentation. + +| Provider | Names its events | Sends a `[DONE]` marker | +|----------|------------------|-------------------------| +| Claude | ✅ | ➖ | +| OpenAI | ✅ | ➖ | +| Gemini | ➖ | ➖ | +| DeepSeek | ➖ | ✅ | +| Grok | ➖ | ✅ | + +So no general "the end" signal exists. Claude finishes with a `message_stop` event, OpenAI with a terminal event carrying the whole response, Gemini simply stops sending, and the DeepSeek and Grok pair use the marker. On top of that the pieces arrive split wherever the network happened to cut them, so a single event routinely turns up in two halves. The library rebuilds exactly the shape of response you would have got without streaming, so the two routes cannot drift apart on you. + +One more measured property, this one practical: **a stream is bounded by silence, not by total time**. An ordinary request is capped on how long it may take as a whole, but a long answer legitimately flows for minutes, so such a cap would cut it off mid-way. For a stream the library therefore watches only whether data keeps coming, and gives up only when the provider stops talking altogether. + + +Where to Go Next +================ + +- [Tool calling |tools] - when the model should reach into your application +- [Structured output |structured-output] - when you need data, not prose +- [HTTP layer |http] - retrying, logging and time limits +- [Error handling |errors] - what to do when the provider says no diff --git a/ai-access/en/structured-output.texy b/ai-access/en/structured-output.texy new file mode 100644 index 0000000000..813b137725 --- /dev/null +++ b/ai-access/en/structured-output.texy @@ -0,0 +1,201 @@ +Structured Output and JSON Schema +********************************* + +.[perex] +Sometimes you do not want a sentence from the model but data: a name, an amount, a date, a list of categories. Asking nicely in the prompt is not enough, because the model occasionally adds an introduction or wraps the answer in markdown. The fix is to prescribe the shape of the answer with a schema that the provider enforces. The source does not have to be text either: a schema works just as well on [a photographed receipt or an attached PDF |multimodal]. + + +Asking Nicely Is Not Enough +=========================== + +Say you need the customer's name and the amount out of an e-mail. The first idea is to ask: + +```php +$chat->sendMessage('Return the name and amount from this e-mail as JSON: ...'); +``` + +Nine times out of ten it works. The tenth time you get this: + +/-- +Certainly! Here is the requested data: + +```json +{"name": "John Smith", "amount": 1500} +``` +\-- + +The answer is factually right, but `json_decode()` fails on it, because there is a sentence and a markdown block around it. Another time the model names the key `customer` instead of `name`, or returns the amount as the string `"1500 USD"`. You will not catch this in testing, because most of the time it passes; you catch it in production, usually on data nobody expected. + +A prompt is a request, not a guarantee. And a request is not what you build unattended processing on. + + +A Schema Instead of a Request +============================= + +Rather than asking, you prescribe the shape. You hand `setResponseSchema()` a [JSON schema |https://json-schema.org] and the provider makes sure the model sticks to it: + +```php +$chat = $client->createChat('gpt-5.6-luna'); + +$chat->setResponseSchema([ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'description' => 'Name of the customer'], + 'amount' => ['type' => 'number', 'description' => 'Amount without the currency'], + ], + 'required' => ['name', 'amount'], + 'additionalProperties' => false, +]); + +$response = $chat->sendMessage('Extract the data from this e-mail: ...'); +$data = $response->getJson(); + +echo $data['name'], ' pays ', $data['amount'], "\n"; +``` + +`getJson()` returns the decoded array directly, so there is no stripping of markdown and no extra `json_decode()`. Nothing surrounds the answer, because the model has nowhere to put it. + +The difference from a prompt is not that the model suddenly respects the instruction better. It is that **the schema is enforced by the provider**, not by the model's good will. + + +How to Write a Schema +===================== + +A JSON schema describes the shape of data in JSON, and for ordinary use three things are enough. + +`type` says what it is: `object` for a structure with keys, `array` for a list, plus `string`, `number`, `integer` and `boolean`. `properties` lists the individual keys of an object, each with its own `type`. `required` is the list of keys that must always arrive; anything not in it the model may omit. + +Three pieces of advice that pay off: + +- **Describe the fields.** The model reads the `description` of each key and follows it. The difference between "amount" and "amount without the currency and without spaces" is the difference between `"1 500 USD"` and `1500`. +- **Use `enum` for closed lists.** When the category must be one of three, write `['type' => 'string', 'enum' => ['complaint', 'question', 'spam']]`. The model then cannot invent a fourth. +- **Expect strict mode.** OpenAI, Grok and the generic client receive the schema with `strict` turned on, which demands `additionalProperties: false` and **every key in `required`**; anything else ends as an `ApiException`. Claude and Gemini are more relaxed and tolerate optional keys. When a value should be optional across providers, keep it in `required` and let it be empty or null via `'type' => ['string', 'null']`, so the model does not have to invent it. + +Nesting is unlimited, so a list of objects looks like this: + +```php +$chat->setResponseSchema([ + 'type' => 'object', + 'properties' => [ + 'items' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'title' => ['type' => 'string'], + 'count' => ['type' => 'integer'], + ], + 'required' => ['title', 'count'], + 'additionalProperties' => false, + ], + ], + ], + 'required' => ['items'], + 'additionalProperties' => false, +]); +``` + + +Writing the Schema with Nette Schema +==================================== + +If you have [Nette Schema |schema:] in the project, you can write the schema as `Expect::` instead of an array. It is shorter, and it gives you more than the shape sent to the provider: the same schema then validates the answer on the PHP side and casts it to what it describes. + +```php +use Nette\Schema\Expect; + +$chat->setResponseSchema(Expect::structure([ + 'name' => Expect::string()->required()->description('Name of the customer'), + 'amount' => Expect::float()->min(0)->required()->description('Amount without the currency'), + 'category' => Expect::anyOf('complaint', 'question', 'spam')->required(), + 'note' => Expect::string()->nullable()->required(), +])); + +$response = $chat->sendMessage('Extract the data from this e-mail: ...'); +$data = $response->getJson(); + +echo $data->name, ' pays ', $data->amount, "\n"; +``` + +`getJson()` then no longer returns a decoded array but what the schema yields: a `stdClass` object by default, an array with `castTo('array')`, or an instance of your class. The shortest route to that is `Expect::from()`, which turns the typed properties of a class into the schema: + +```php +final class Order +{ + public function __construct( + public string $name, + public float $amount, + public ?string $note = null, + ) { + } +} + +$chat->setResponseSchema(Expect::from(new Order('', 0.0))); +$order = $chat->sendMessage('...')->getJson(); // an instance of Order +``` + +What reaches the model: `description()`, `min()` and `max()`, `pattern()`, `anyOf()` with values as an `enum`, `nullable()` as `null` in the type, `listOf()` and nested `structure()`. What stays on the PHP side and runs on the answer: `assert()`, `transform()` and `castTo()`. An answer the schema refuses is an `AIAccess\UnexpectedResponseException` carrying its messages, just like invalid JSON. + +Two things to know. The strict mode of OpenAI, Grok and the generic client wants every key required, so `setResponseSchema()` refuses a Nette schema with an optional key right away with a `LogicException`; write an optional value as `->nullable()->required()`, like `note` above. And a class as a type, such as `Expect::type(DateTime::class)`, has no JSON counterpart and is refused too: it means "an instance", which no model can send. Write `Expect::string()->castTo(DateTime::class)` and the cast happens once the answer arrives. + + +When the Answer Does Not Go to Plan +=================================== + +A schema guarantees the shape of the answer, not that an answer arrives at all. Two situations deserve handling. + +**The model may refuse to answer.** A safety filter does not disappear because you prescribed a shape; the text will then be empty and `getJson()` returns `null`. You recognize it by the [finish reason |chat#Why the Model Stopped Writing]: + +```php +use AIAccess\Chat\FinishReason; + +if ($response->getFinishReason() !== FinishReason::Complete) { + // a refusal or a truncated answer, do not expect data +} +``` + +**The answer may not be valid JSON.** It happens rarely, typically when the token limit runs out mid-way and the JSON is left unclosed. `getJson()` then throws `AIAccess\UnexpectedResponseException`, so you do not discover it two layers further on: + +```php +try { + $data = $response->getJson(); +} catch (AIAccess\UnexpectedResponseException $e) { + // the answer could not be decoded, the raw text is in getText() +} +``` + + +Who Supports It, and What About DeepSeek +======================================== + +Schemas work on **OpenAI, Claude, Gemini and Grok** and on the [generic client |providers]. DeepSeek has no such option, so `setResponseSchema()` throws `AIAccess\LogicException` there and points you at the JSON mode. Were the schema sent, the endpoint would answer "This response_format type is unavailable now". + +JSON mode does not guarantee the shape, but it does guarantee that the answer is valid JSON with no markdown around it: + +```php +$chat->setOptions(responseFormat: ['type' => 'json_object']); +``` + +Mind one trap: DeepSeek insists the word "json" appears somewhere in the conversation, and refuses the request otherwise. Mentioning it in the instruction or the question is enough. + +You then have to describe the shape in the prompt and check the result yourself. Grok and the generic client have the same setting, but there you are better off with a schema. + + +A Schema, or a Tool? +==================== + +Both make the model produce JSON following a schema, which is why they get confused. The difference is in who hands what to whom. + +- **Structured output is the shape of the answer.** The model finishes and you receive data. Use it when you want a result from the model: extracting values from text, sorting into a category, splitting an address into parts. +- **A [tool call |tools] is a question aimed at you.** The model stops and waits for you to find something out, then carries on. Use it when the model needs information or an action that your application owns. + +Put simply: structured output is an answer, a tool is a question. + + +Where to Go Next +================ + +- [Tool calling |tools] - when the model needs to ask your application +- [Conversation |chat] - the finish reason and reading the answer +- [Error handling |errors] - which exception means what +- [Providers |providers] - what each one can do and how they differ diff --git a/ai-access/en/tools.texy b/ai-access/en/tools.texy new file mode 100644 index 0000000000..f9d29c4a0e --- /dev/null +++ b/ai-access/en/tools.texy @@ -0,0 +1,154 @@ +Tool Calling (Function Calling) +******************************* + +.[perex] +The model knows nothing about your database or about today's weather. It can, however, ask for a function that will find out, and wait until you send it the result. We will look at how you describe a function to the model, how the library handles the whole exchange on its own, and what to do when the model makes things up. + + +The Model Does Not Know, So It Asks +=================================== + +A language model knows only what it learned during training. It does not know the state of an order in your e-shop, it has no access to your database, and it does not know what time it is. Ask it anyway and it will either admit it does not know or invent an answer; the second is worse, because it sounds every bit as convincing. (When you want to give it your own texts, documentation or a knowledge base, [search through embeddings |embeddings] fits better; tools are for making the model **do** something.) + +Tool calling solves this by swapping the roles for a moment. You describe up front the functions your application can perform. The model can then say, instead of answering: "call `getWeather` for Brno". You run the function, send the result back, and it composes the answer out of that. + +The technique goes by two names, **tool calling** and **function calling**; they mean the same thing and differ only in which provider happens to promote which. This documentation says tools, because that is what the library's own methods are called. + +The important part is that **the model does not run your code**. It only says what it would like called and with which arguments; whether that happens is your application's decision. In this arrangement the model is the one asking, not the one commanding. + + +The Simplest Case: the Library Does It for You +============================================== + +You describe a tool with a `Tool` object: a name, a description of what it is for, a JSON schema of the arguments, and the function that performs it. + +```php +use AIAccess\Chat\Tool; + +$chat = $client->createChat('gpt-5.6-luna'); + +$chat->addTool(new Tool( + name: 'getWeather', + description: 'Returns the current weather for the given city.', + parameters: [ + 'type' => 'object', + 'properties' => [ + 'city' => ['type' => 'string', 'description' => 'City name'], + ], + 'required' => ['city'], + ], + handler: function (array $args): string { + // here you would call your API or database + return 'In ' . $args['city'] . ' it is 12 °C and raining.'; + }, +)); + +echo $chat->sendMessage('What is the weather in Brno, and should I take a coat?')->getText(); +``` + +That is all of it. A single `sendMessage()` covers the whole exchange: the model asks for a tool, the library calls your handler, sends the result back and waits for the model's answer. One such there-and-back is called a **round**, and several of them can happen in a row when the model needs more information; the whole series is the **tool loop**. + +The description matters more than it looks. The model decides whether to call the tool at all based on it, and has no other clue. "Returns the current weather for the given city" is a good description; "weather" is a bad one. + +Several rounds are nothing exotic. Ask about the weather in Brno and in Prague and the model asks for the tool twice. And when it has more tools, it commonly calls one, looks at the result and only then reaches for the second: first it finds the customer by e-mail, then it looks up their orders. + +You set the cap on the number of rounds with `setToolLoop()`. The default of eight is enough for ordinary tasks and at the same time stops the model from looping at your expense: + +```php +$chat->setToolLoop(maxRounds: 3); +``` + +What the whole exchange cost is reported by `getTotalUsage()`, because `getUsage()` on the response speaks only about the last round. + + +When You Want to Handle the Calls Yourself +========================================== + +The automatic loop starts only when every tool being called has a handler. As soon as one does not, the library stops and hands control back to you. That is deliberate: a missing handler is how you say "I want to deal with this myself". + +```php +$chat->addTool(new Tool( + name: 'deleteAccount', + description: 'Deletes a user account.', + parameters: ['type' => 'object', 'properties' => ['id' => ['type' => 'integer']]], + // the handler is deliberately missing +)); + +$response = $chat->sendMessage('Cancel account number 42.'); + +foreach ($response->getToolCalls() as $call) { + echo "The model wants to call $call->name with arguments ", json_encode($call->arguments); + + // here you ask the user, check permissions, whatever you need + $chat->addToolResult($call, 'The account has been deleted.'); +} + +echo $chat->sendMessage()->getText(); // continue with the results filled in +``` + +This mode fits anywhere an action must not happen without human consent, and also when you want to log or restrict the calls. + +When you want the opposite, telling the model to reach for one particular tool, use `setToolChoice('toolName')`. To forbid tools entirely, register none. + + +Parameters as a Nette Schema +============================ + +The parameters can also be a [Nette Schema |schema:] structure instead of an array. The library exports it as JSON schema for the model, and on top of that validates the arguments with it in full: required keys, types, ranges, `assert()`, whatever the schema says. Your handler then receives what the schema yields, with defaults filled in and casts applied, instead of a bare array: + +```php +use Nette\Schema\Expect; + +$chat->addTool(new Tool( + name: 'getWeather', + description: 'Returns the current weather for the given city.', + parameters: Expect::structure([ + 'city' => Expect::string()->required()->description('City name'), + 'units' => Expect::anyOf('celsius', 'fahrenheit')->default('celsius'), + ]), + handler: function (stdClass $args): string { + return "In $args->city it is 12 °C and raining."; // $args->units is 'celsius' even when the model left it out + }, +)); +``` + +Arguments the schema refuses go back to the model with its exact messages, so it learns not just that something was wrong but what. Everything else on this page works the same, whichever way the parameters are written. + + +When the Model Makes Things Up +============================== + +Now and then the model asks for a tool that does not exist, or sends arguments that do not match the schema. This is not exceptional, and above all it is not your application's fault, so it is not treated as an exception either. + +The library **sends such a mistake back to the model** as an error result and lets it correct itself. Models are surprisingly good at this: they read what was wrong and call the tool again properly. Had an exception been thrown instead, you would lose the whole answer over a mistake the model can fix on its own. + +This covers an invented tool name, unreadable arguments, and arguments that do not fit the schema. With a JSON schema array the library checks the required keys and basic types; it is not a full validator, because the model gets the error message either way. A Nette schema validates in full. + +A failure of **your own handler** is a different matter. By default the exception propagates to you, which is right, because a broken database is not something the model should be dealing with. When you do want the model to learn about the failure and try another route, you turn it on: + +```php +$chat->setToolLoop(catchErrors: true); +``` + +Even then one exception to the exceptions holds: errors of the `Error` kind, meaning bugs in your own code such as calling a method that does not exist, always propagate. Were they sent to the model as a tool result, your bug would hide inside the conversation and you would never hear about it. + + +What Happens Underneath +======================= + +Tools are one of those areas where the providers agree on nothing at all, and two traps are worth knowing about in case you ever look into raw responses. + +**Gemini does not announce a tool call in the finish reason.** It stays `STOP`, as if the model had finished normally, and the call itself is found among the parts of the answer. The library therefore derives the finish reason from the content, so you get `FinishReason::ToolCall` from all five alike. + +**Reasoning has to come back unchanged.** Models that think before answering want their reasoning back in the next round exactly as they sent it. Claude rejects a modified signature with an error, Gemini answers `MISSING_THOUGHT_SIGNATURE`. The library carries these parts in the history and returns them only to the provider that issued them. + +And one property that surprises people: **the loop commits every completed round**. When the seventh round fails, the history and the side effects of the tools from the first six remain. Throwing them away would mean paying for six rounds for nothing, and possibly sending twice an e-mail that one of the tools had already sent. + + +Where to Go Next +================ + +- [Structured output |structured-output] - when you want data, not a call +- [Streaming |streaming] - tool rounds stream as well +- [Conversation |chat] - the history that calls and results are written into +- [Error handling |errors] - what to do when the provider says no diff --git a/ai-access/files/cover.png b/ai-access/files/cover.png new file mode 100644 index 0000000000..56b6033ba6 Binary files /dev/null and b/ai-access/files/cover.png differ diff --git a/ai-access/meta.json b/ai-access/meta.json new file mode 100644 index 0000000000..55e4a45afe --- /dev/null +++ b/ai-access/meta.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "repo": "dg/ai-access", + "composer": "ai-access/ai-access" +} diff --git a/ai/cs/@home.texy b/ai/cs/@home.texy new file mode 100644 index 0000000000..58a9eed4a8 --- /dev/null +++ b/ai/cs/@home.texy @@ -0,0 +1,97 @@ +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 + + +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 +====================== + +**[Plugin pro Claude Code |claude-code]** dodá 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 plugin pro Nette |claude-code], 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 |ai-access:], 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 new file mode 100644 index 0000000000..5491396881 --- /dev/null +++ b/ai/cs/@left-menu.texy @@ -0,0 +1,5 @@ +- [Úvod |@home] +- [Začínáme |getting-started] +- [Plugin pro Claude Code |claude-code] +- [MCP Inspector |mcp-inspector] +- [Vibe coding v praxi |vibe-coding] diff --git a/ai/cs/@meta.texy b/ai/cs/@meta.texy new file mode 100644 index 0000000000..e06cc9886c --- /dev/null +++ b/ai/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette AI}} diff --git a/ai/cs/guide.texy b/ai/cs/guide.texy new file mode 100644 index 0000000000..3ec981a90e --- /dev/null +++ b/ai/cs/guide.texy @@ -0,0 +1 @@ +{{redirect: @home}} diff --git a/ai/cs/mcp-inspector.texy b/ai/cs/mcp-inspector.texy new file mode 100644 index 0000000000..355d9dca67 --- /dev/null +++ b/ai/cs/mcp-inspector.texy @@ -0,0 +1,326 @@ +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}} diff --git a/ai/cs/tips.texy b/ai/cs/tips.texy new file mode 100644 index 0000000000..d348348e9a --- /dev/null +++ b/ai/cs/tips.texy @@ -0,0 +1 @@ +{{redirect: vibe-coding}} diff --git a/ai/cs/vibe-coding.texy b/ai/cs/vibe-coding.texy new file mode 100644 index 0000000000..1b1bc69e89 --- /dev/null +++ b/ai/cs/vibe-coding.texy @@ -0,0 +1,298 @@ +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 |claude-code], 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! + +{{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 new file mode 100644 index 0000000000..2c0e0779d6 --- /dev/null +++ b/ai/en/@home.texy @@ -0,0 +1,97 @@ +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 + + +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 **[Claude Code plugin |claude-code]** supplies 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 plugin |claude-code] 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 |ai-access:] 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 new file mode 100644 index 0000000000..d57821b9f1 --- /dev/null +++ b/ai/en/@left-menu.texy @@ -0,0 +1,5 @@ +- [Overview |@home] +- [Getting Started |getting-started] +- [Claude Code Plugin |claude-code] +- [MCP Inspector |mcp-inspector] +- [Vibe Coding in Practice |vibe-coding] diff --git a/ai/en/@meta.texy b/ai/en/@meta.texy new file mode 100644 index 0000000000..e06cc9886c --- /dev/null +++ b/ai/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Nette AI}} diff --git a/ai/en/claude-code.texy b/ai/en/claude-code.texy new file mode 100644 index 0000000000..a6b8dcca16 --- /dev/null +++ b/ai/en/claude-code.texy @@ -0,0 +1,416 @@ +Claude Code Plugin +****************** + +
+ +The "Nette plugin for Claude Code":https://github.com/nette/claude-code gives 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. + +Together with its companion plugins, you get: +- 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 +============ + +If you haven't installed Claude Code yet, see the [complete setup guide |getting-started]. Once Claude Code is running, install the Nette plugin: + +```shell +/plugin marketplace add nette/claude-code +/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 +``` + +The full family of plugins: + +| 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 (optional) | +| `nette-dev` | Coding standards for framework contributors | + + +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. + + +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-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, Claude knows about it immediately and can suggest a fix. No more discovering errors in the browser. + +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 Claude Code is running. So even if you keep several applications in one repository and run Claude Code 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 +``` + +| 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: + +```shell +/plugin install php-fixer@nette +/install-php-fixer +``` + +The second command installs `nette/coding-standard` globally. After that, every PHP file you edit will be automatically formatted according to Nette coding standards. + +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. Exclude such paths with a `fix-php-style` entry in `.nette-claude.json`: + +```json +{ + "fix-php-style": { + "exclude": ["fixtures*"] + } +} +``` + +The patterns follow the same gitignore-like rules as the [validation hooks |#automatic-validation] above. + +By default the fixer runs a single preset, the one matching your project's PHP version. A `presets` list defines exactly which presets run and in what order, where the name `php` stands for that version-derived preset. This is how you add `optimize-fn`, which maintains grouped `use function` and `use const` imports for compiler-optimized functions: + +```json +{ + "fix-php-style": { + "presets": ["php", "optimize-fn"] + } +} +``` + +Each entry is a separate pass over the edited file, so drop `php` from the list only if you really want the standard preset gone. + +The plugin also comes with the `php-auto-fixer` skill, which teaches Claude about one subtle pitfall: the fixer 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. + + +MCP Inspector Integration +========================= + +The plugin works even better with [MCP Inspector |mcp-inspector] - a tool that lets Claude 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. See the [MCP Inspector documentation |mcp-inspector] for all the available tools. + +{{repo: nette/claude-code}} diff --git a/ai/en/getting-started.texy b/ai/en/getting-started.texy new file mode 100644 index 0000000000..3ad49746fc --- /dev/null +++ b/ai/en/getting-started.texy @@ -0,0 +1,260 @@ +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/claude-code +``` + +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 Claude Code plugin provides, 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 +- [Claude Code Plugin |claude-code] - Explore all the skills (Claude Code users) +- [Vibe Coding in Practice |vibe-coding] - Get the most out of your AI assistant + +{{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 new file mode 100644 index 0000000000..3ec981a90e --- /dev/null +++ b/ai/en/guide.texy @@ -0,0 +1 @@ +{{redirect: @home}} diff --git a/ai/en/mcp-inspector.texy b/ai/en/mcp-inspector.texy new file mode 100644 index 0000000000..c75ee4aae7 --- /dev/null +++ b/ai/en/mcp-inspector.texy @@ -0,0 +1,326 @@ +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}} diff --git a/ai/en/tips.texy b/ai/en/tips.texy new file mode 100644 index 0000000000..d348348e9a --- /dev/null +++ b/ai/en/tips.texy @@ -0,0 +1 @@ +{{redirect: vibe-coding}} diff --git a/ai/en/vibe-coding.texy b/ai/en/vibe-coding.texy new file mode 100644 index 0000000000..60ba097af5 --- /dev/null +++ b/ai/en/vibe-coding.texy @@ -0,0 +1,298 @@ +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 [Claude Code plugins |claude-code], 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! + +{{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/ai/meta.json b/ai/meta.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/ai/meta.json @@ -0,0 +1 @@ +{} diff --git a/application/bg/@home.texy b/application/bg/@home.texy deleted file mode 100644 index 4c10c2af32..0000000000 --- a/application/bg/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application е ядрото на Nette framework, което предоставя мощни инструменти за създаване на модерни уеб приложения. Предлага редица изключителни характеристики, които значително улесняват разработката и подобряват сигурността и поддръжката на кода. - - -Инсталация ----------- - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Защо да изберете Nette Application? ------------------------------------ - -Nette винаги е бил пионер в областта на уеб технологиите. - -**Двупосочен рутер:** Nette разполага с усъвършенствана система за маршрутизация, която е уникална със своята двупосочност - не само преобразува URL адреси в действия на приложението, но също така може да генерира обратно URL адреси. Това означава, че: -- Можете по всяко време да промените структурата на URL адресите на цялото приложение, без да е необходимо да редактирате шаблоните -- URL адресите се канонизират автоматично, което подобрява SEO -- Маршрутизацията се дефинира на едно място, а не е разпръсната в анотации - -**Компоненти и сигнали:** Вградената компонентна система, вдъхновена от Delphi и React.js, е напълно изключителна сред PHP framework-ците: -- Позволява създаването на повторно използваеми UI елементи -- Поддържа йерархично композиране на компоненти -- Предлага елегантна обработка на AJAX заявки с помощта на сигнали -- Богата библиотека от готови компоненти на [Componette](https://componette.org) - -**AJAX и снипети:** Nette представи революционен начин за работа с AJAX още през 2009 г., много преди подобни решения като Hotwire за Ruby on Rails или Symfony UX Turbo: -- Снипетите позволяват актуализиране само на части от страницата, без да е необходимо да се пише JavaScript -- Автоматична интеграция с компонентната система -- Интелигентна инвалидация на части от страници -- Минимално количество предавани данни - -**Интуитивни шаблони [Latte|latte:]:** Най-сигурната система за шаблони за PHP с разширени функции: -- Автоматична защита срещу XSS с контекстно чувствително екраниране -- Разширяемост с помощта на персонализирани филтри, функции и тагове -- Наследяване на шаблони и снипети за AJAX -- Отлична поддръжка на PHP 8.x с типова система - -**Dependency Injection:** Nette напълно използва Dependency Injection: -- Автоматично предаване на зависимости (autowiring) -- Конфигурация чрез ясен NEON формат -- Поддръжка на фабрики за компоненти - - -Основни предимства ------------------- - -- **Сигурност**: Автоматична защита срещу [уязвимости|nette:vulnerability-protection] като XSS, CSRF и др. -- **Продуктивност**: По-малко писане, повече функции благодарение на интелигентния дизайн -- **Дебъгване**: [Tracy debugger|tracy:] с панел за маршрутизация -- **Производителност**: Интелигентен кеш, lazy loading на компоненти -- **Гъвкавост**: Лесно модифициране на URL адреси дори след завършване на приложението -- **Компоненти**: Уникална система от повторно използваеми UI елементи -- **Модерност**: Пълна поддръжка на PHP 8.4+ и типова система - - -Да започваме ------------- - -1. [Как работят приложенията? |how-it-works] - Разбиране на основната архитектура -2. [Presenters |presenters] - Работа с презентери и действия -3. [Шаблони |templates] - Създаване на шаблони в Latte -4. [Маршрутизация |routing] - Конфигуриране на URL адреси -5. [Интерактивни компоненти |components] - Използване на компонентната система - - -Съвместимост с PHP ------------------- - -| версия | съвместим с PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 - -Важи за последната пач версия. diff --git a/application/bg/@left-menu.texy b/application/bg/@left-menu.texy deleted file mode 100644 index 89db4642b8..0000000000 --- a/application/bg/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Как работят приложенията? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Шаблони |templates] -- [Директорийна структура |directory-structure] -- [Маршрутизация |routing] -- [Създаване на URL връзки |creating-links] -- [Интерактивни компоненти |components] -- [AJAX & снипети |ajax] -- [Multiplier |multiplier] -- [Конфигурация |configuration] - - -Допълнително четене -******************* -- [Защо да използвате Nette? |www:10-reasons-why-nette] -- [Инсталация |nette:installation] -- [Пишем първото си приложение! |quickstart:] -- [Ръководства и процедури |best-practices:] -- [Решаване на проблеми |nette:troubleshooting] diff --git a/application/bg/@meta.texy b/application/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/application/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/application/bg/ajax.texy b/application/bg/ajax.texy deleted file mode 100644 index 1cfe2fdaf2..0000000000 --- a/application/bg/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & снипети -************** - -
- -В ерата на съвременните уеб приложения, където функционалността често се разпределя между сървъра и браузъра, 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 рендира тези снипети и ги изпраща под формата на масив във формат JSON. Обслужващият код в браузъра вмъква получените снипети обратно в страницата. Така се пренася само кодът на променените снипети, което спестява трафик и ускорява зареждането в сравнение с пренасянето на съдържанието на цялата страница. - - -Naja ----- - -За обслужване на снипети от страна на браузъра се използва [библиотеката Naja |https://naja.js.org]. [Инсталирайте я |https://naja.js.org/#/guide/01-install-setup-naja] като node.js пакет (за използване с приложения Webpack, Rollup, Vite, Parcel и други): - -```shell -npm install naja -``` - -…или директно я вмъкнете в шаблона на страницата: - -```latte - -``` - -Първо е необходимо библиотеката да бъде [инициализирана |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -За да превърнете обикновена връзка (сигнал) или изпращане на формуляр в AJAX заявка, е достатъчно да маркирате съответната връзка, формуляр или бутон с клас `ajax`: - -```latte -Go - -
- -
- -или - -
- -
-``` - - -Прерисуване на снипети ----------------------- - -Всеки обект от клас [Control |components] (включително самият Presenter) следи дали са настъпили промени, изискващи неговото прерисуване. За това служи методът `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // след влизане е необходимо да се прерисува съответната част - $this->redrawControl(); - // ... -} -``` - -Nette позволява още по-фин контрол върху това, което трябва да се прерисува. Споменатият метод може да приема името на снипета като аргумент. Така може да се инвалидира (разбирай: да се наложи прерисуване) на ниво части от шаблона. Ако се инвалидира целият компонент, тогава се прерисува и всеки негов снипет: - -```php -// инвалидира снипета 'header' -$this->redrawControl('header'); -``` - - -Снипети в Latte ---------------- - -Използването на снипети в Latte е изключително лесно. Ако искате да дефинирате част от шаблона като снипет, просто я обвийте с таговете `{snippet}` и `{/snippet}`: - -```latte -{snippet header} -

Hello ...

-{/snippet} -``` - -Снипетът създава в HTML страницата елемент `
` със специално генериран `id`. При прерисуване на снипета се актуализира съдържанието на този елемент. Затова е необходимо при първоначалното рендиране на страницата да се рендират и всички снипети, дори и ако в началото са празни. - -Можете да създадете и снипет с друг елемент освен `
` с помощта на n:атрибут: - -```latte -
-

Hello ...

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

    Гласувайте

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

    {$item->title}

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

    Hello

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

    Homepage

    -... -{/block} -``` - -Той дефинира блок `content`, който се вмъква на мястото на `{include content}` в лейаута, и също така ре-дефинира блок `title`, с който презаписва `{block title}` в лейаута. Опитайте да си представите резултата. - - -Търсене на шаблони ------------------- - -Не е необходимо в презентерите да посочвате кой шаблон трябва да се рендира, фреймуъркът сам извежда пътя и ви спестява писане. - -Ако използвате директорийна структура, където всеки презентер има собствена директория, просто поставете шаблона в тази директория под името на действието (респ. view), т.е. за действието `default` използвайте шаблона `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Ако използвате структура, където презентерите са заедно в една директория, а шаблоните в папка `templates`, съхранете го или във файл `..latte`, или `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1. вариант - └── Home/ - └── default.latte ← 2. вариант -\-- - -Директорията `templates` може да бъде разположена и едно ниво по-високо, т.е. на същото ниво, на което е директорията с класовете на презентерите. - -Ако шаблонът не бъде намерен, презентерът отговаря с [грешка 404 - page not found |presenters#Грешка 404 и др]. - -View се променя с помощта на `$this->setView('jineView')`. Също така може директно да се посочи файл с шаблон с помощта на `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Файловете, където се търсят шаблони, могат да се променят чрез презаписване на метода [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], който връща масив от възможни имена на файлове. - - -Търсене на шаблон на лейаута ----------------------------- - -Nette също така автоматично търси файл с лейаут. - -Ако използвате директорийна структура, където всеки презентер има собствена директория, поставете лейаута или в папката с презентера, ако е специфичен само за него, или едно ниво по-високо, ако е общ за няколко презентера: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← общ лейаут - └── Home/ - ├── @layout.latte ← само за презентера Home - ├── HomePresenter.php - └── default.latte -\-- - -Ако използвате структура, където презентерите са заедно в една директория, а шаблоните в папка `templates`, лейаутът ще се очаква на тези места: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← общ лейаут - ├── Home.@layout.latte ← само за Home, 1. вариант - └── Home/ - └── @layout.latte ← само за Home, 2. вариант -\-- - -Ако презентерът се намира в модул, ще се търси и на други директорийни нива по-високо, според влагането на модула. - -Името на лейаута може да се промени с помощта на `$this->setLayout('layoutAdmin')` и тогава ще се очаква във файл `@layoutAdmin.latte`. Също така може директно да се посочи файл с шаблон на лейаута с помощта на `$this->setLayout('/path/to/template.latte')`. - -С помощта на `$this->setLayout(false)` или тага `{layout none}` вътре в шаблона търсенето на лейаут се изключва. - -.[note] -Файловете, където се търсят шаблони на лейаута, могат да се променят чрез презаписване на метода [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], който връща масив от възможни имена на файлове. - - -Променливи в шаблона --------------------- - -Променливите в шаблона предаваме така, че ги записваме в `$this->template` и след това ги имаме на разположение в шаблона като локални променливи: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Така лесно можем да предадем в шаблоните всякакви променливи. При разработката на стабилни приложения обаче е по-полезно да се ограничим. Например така, че изрично да дефинираме списък с променливите, които шаблонът очаква, и техните типове. Благодарение на това PHP ще може да проверява типовете, IDE правилно да подсказва и статичният анализ да открива грешки. - -А как да дефинираме такъв списък? Просто под формата на клас и неговите свойства. Ще го наречем подобно на презентера, само с `Template` накрая: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // и други променливи -} -``` - -Обектът `$this->template` в презентера сега ще бъде инстанция на класа `ArticleTemplate`. Така че PHP при запис ще проверява декларираните типове. И започвайки от версия PHP 8.2 ще предупреждава и за запис в несъществуваща променлива, в предишните версии същото може да се постигне с използването на trait [Nette\SmartObject |utils:smartobject]. - -Анотацията `@property-read` е предназначена за IDE и статичен анализ, благодарение на нея ще работи подсказването, вижте "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Лукса на подсказването можете да си позволите и в шаблоните, достатъчно е да инсталирате в PhpStorm плъгин за Latte и да посочите в началото на шаблона името на класа, повече в статията "Latte: как да използваме системата за типове":https://blog.nette.org/bg/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Така работят и шаблоните в компонентите, достатъчно е само да се спазва именната конвенция и за компонент напр. `FifteenControl` да се създаде клас на шаблона `FifteenTemplate`. - -Ако трябва да създадете `$template` като инстанция на друг клас, използвайте метода `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Променливи по подразбиране --------------------------- - -Презентерите и компонентите предават на шаблоните няколко полезни променливи автоматично: - -- `$basePath` е абсолютният URL път до коренната директория (напр. `/eshop`) -- `$baseUrl` е абсолютният URL до коренната директория (напр. `http://localhost/eshop`) -- `$user` е обект [представляващ потребителя |security:authentication] -- `$presenter` е текущият презентер -- `$control` е текущият компонент или презентер -- `$flashes` масив от [съобщения |presenters#Flash съобщения], изпратени с функцията `flashMessage()` - -Ако използвате собствен клас на шаблона, тези променливи се предават, ако създадете свойство за тях. - - -Създаване на връзки -------------------- - -В шаблона се създават връзки към други презентери и действия по следния начин: - -```latte -детайл на продукта -``` - -Атрибутът `n:href` е много удобен за HTML тагове ``. Ако искаме да изпишем връзка другаде, например в текст, използваме `{link}`: - -```latte -Адресът е: {link Home:default} -``` - -Повече информация ще намерите в главата [Създаване на URL връзки|creating-links]. - - -Собствени филтри, тагове и др. ------------------------------- - -Шаблониращата система Latte може да бъде разширена със собствени филтри, функции, тагове и др. Това може да се направи директно в метода `render` или `beforeRender()`: - -```php -public function beforeRender(): void -{ - // добавяне на филтър - $this->template->addFilter('foo', /* ... */); - - // или конфигурираме директно обекта Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte във версия 3 предлага по-напреднал начин, а именно създаването на [extension |latte:extending-latte#Latte Extension] за всеки уеб проект. Частичен пример за такъв клас: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Регистрираме го с помощта на [конфигурацията |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Превод ------- - -Ако програмирате многоезично приложение, най-вероятно ще трябва да изпишете някои текстове в шаблона на различни езици. Nette Framework за тази цел дефинира интерфейс за превод [api:Nette\Localization\Translator], който има единствен метод `translate()`. Той приема съобщение `$message`, което обикновено е низ, и всякакви други параметри. Задачата е да върне преведен низ. В Nette няма реализация по подразбиране, можете да изберете според своите нужди от няколко готови решения, които ще намерите на [Componette |https://componette.org/search/localization]. В тяхната документация ще научите как да конфигурирате преводача. - -На шаблоните може да се зададе преводач, който си [изискваме |dependency-injection:passing-dependencies], с метода `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Преводачът може алтернативно да се настрои с помощта на [конфигурацията |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -След това преводачът може да се използва например като филтър `|translate`, и то включително с допълнителни параметри, които се предават на метода `translate()` (виж `foo, bar`): - -```latte -{='Количка'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Или като таг с долна черта: - -```latte -{_'Количка'} -{_$item} -{_$item, foo, bar} -``` - -За превод на част от шаблона съществува двоен таг `{translate}` (от Latte 2.11, преди се използваше тагът `{_}`): - -```latte -{translate}Поръчка{/translate} -{translate foo, bar}Поръчка{/translate} -``` - -Преводачът стандартно се извиква по време на изпълнение при рендиране на шаблона. Latte версия 3 обаче може да превежда всички статични текстове още по време на компилацията на шаблона. С това се спестява производителност, защото всеки низ се превежда само веднъж и крайният превод се записва в компилираната форма. В директорията с кеша така възникват повече компилирани версии на шаблона, по една за всеки език. За това е достатъчно само да се посочи езикът като втори параметър: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Статичен текст е например `{_'hello'}` или `{translate}hello{/translate}`. Нестатичните текстове, като например `{_$foo}`, ще продължат да се превеждат по време на изпълнение. diff --git a/application/cs/@home.texy b/application/cs/@home.texy index 9bbdba7d6b..88228c1487 100644 --- a/application/cs/@home.texy +++ b/application/cs/@home.texy @@ -52,13 +52,13 @@ Nette bylo vždy průkopníkem v oblasti webových technologií. Hlavní výhody ------------- -- **Bezpečnost**: Automatická obrana proti [zranitelnostem|nette:vulnerability-protection] jako XSS, CSRF, atd. +- **Bezpečnost**: Automatická obrana proti [zranitelnostem|nette:vulnerability-protection] jako XSS, CSRF atd. - **Produktivita**: Méně psaní, více funkcí díky chytrému návrhu - **Debugging**: [Tracy debugger|tracy:] s routovacím panelem - **Výkon**: Chytrá cache, lazy loading komponent - **Flexibilita**: Snadná úprava URL i po dokončení aplikace - **Komponenty**: Unikátní systém znovupoužitelných UI prvků -- **Moderní**: Plná podpora PHP 8.4+ a typového systému +- **Moderní**: Plná podpora PHP 8.3+ a typového systému Začínáme @@ -71,15 +71,15 @@ Začínáme 5. [Interaktivní komponenty |components] - Využití komponentového systému -Kompatbility s PHP ------------------- +Kompatibilita s PHP +------------------- | verze | kompatibilní s PHP |-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| 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. diff --git a/application/cs/@left-menu.texy b/application/cs/@left-menu.texy index 68d0549dc9..8848df94ca 100644 --- a/application/cs/@left-menu.texy +++ b/application/cs/@left-menu.texy @@ -1,5 +1,6 @@ Nette Application ***************** +- [Úvod |@home] - [Jak fungují aplikace? |how-it-works] - [Bootstrapping] - [Presentery |presenters] @@ -11,6 +12,7 @@ Nette Application - [AJAX & snippety |ajax] - [Multiplier |multiplier] - [Konfigurace |configuration] +- [Upgrade |upgrading] Další četba diff --git a/application/cs/ajax.texy b/application/cs/ajax.texy index fe7772083f..1e5f8bd9e5 100644 --- a/application/cs/ajax.texy +++ b/application/cs/ajax.texy @@ -14,7 +14,7 @@ V éře moderních webových aplikací, kde se často rozkládá funkcionalita m 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. +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()`: @@ -35,7 +35,7 @@ Chcete-li odeslat data ve formátu JSON, použijte metodu [`sendJson()` |present ```php public function actionExport(): void { - $this->sendJson($this->model->getData); + $this->sendJson($this->model->getData()); } ``` @@ -55,17 +55,17 @@ public function handleClick($param): void Snippety ======== -Nejsilnější prostředek, který nabízí Nette pro propojení serveru s klientem, představují snippety. Díky nim můžete z obyčejné aplikace udělat AJAXovou jen s minimálním úsilím a několika řádky kódu. Jak to celé funguje demonstruje příklad Fifteen, jehož kód najdete na [GitHubu |https://github.com/nette-examples/fifteen]. +Nejsilnější prostředek, který nabízí Nette pro propojení serveru s klientem, představují snippety. Díky nim můžete z obyčejné aplikace udělat AJAXovou jen s minimálním úsilím a několika řádky kódu. Jak to celé funguje, demonstruje příklad Fifteen, jehož kód najdete na [GitHubu |https://github.com/nette-examples/fifteen]. -Snippety, nebo-li výstřižky, umožnují aktualizovat jen části stránky, místo toho, aby se celá stránka znovunačítala. Jednak je to rychlejší a efektivnější, ale poskytuje to také komfortnější uživatelský zážitek. Snippety vám mohou připomínat Hotwire pro Ruby on Rails nebo Symfony UX Turbo. Zajímavé je, že Nette představilo snippety již o 14 let dříve. +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 ve formě pole ve formátu JSON. Obslužný kód v prohlížeči získané snippety vloží zpět do stránky. Přenáší se tedy jen kód změněných snippetů, což šetří šířku pásma a zrychluje načítání oproti přenášení obsahu celé stránky. +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 aplikacemi Webpack, Rollup, Vite, Parcel a dalšími): +K obsluze snippetů na straně prohlížeče slouží [knihovna Naja |https://naja.js.org]. Tu [nainstalujte |https://naja.js.org/#/guide/01-install-setup-naja] jako Node.js balíček (pro použití s bundlery jako Webpack, Rollup, Vite, Parcel a dalšími): ```shell npm install naja @@ -74,7 +74,7 @@ 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]: @@ -121,6 +121,8 @@ Nette umožňuje ještě jemnější kontrolu toho, co se má překreslit. Uvede $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 ---------------- @@ -133,7 +135,7 @@ Používání snippetů v Latte je nesmírně snadné. Chcete-li definovat čás {/snippet} ``` -Snippet vytvoří v HTML stránce element `
    ` se speciálním vygenerovaným `id`. Při překreslení snippetu se pak aktulizuje obsah tohoto elementu. Proto je nutné, aby při prvotním vykreslení stránky se vykreslily také všechny snippety, byť mohou být třeba na začátku prázdné. +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: @@ -155,7 +157,9 @@ Názvy snippetů mohou být také výrazy: {/foreach} ``` -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}`: +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
      @@ -198,7 +202,7 @@ $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í: pro překreslení snippetů volá metodu `render()` bez parametrů. Tedy nebude fungovat předávání parametrů v šabloně: +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 @@ -226,6 +230,12 @@ public function actionDelete(int $id): void ``` +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ů =================== diff --git a/application/cs/bootstrapping.texy b/application/cs/bootstrapping.texy index 2b4a2dec58..c40a9ea5ac 100644 --- a/application/cs/bootstrapping.texy +++ b/application/cs/bootstrapping.texy @@ -16,6 +16,9 @@ Bootstrapping je proces inicializace prostředí aplikace, vytvoření 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 @@ -78,6 +81,9 @@ $application = $container->getByType(Nette\Application\Application::class); $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. @@ -93,7 +99,7 @@ Nette se chová různě podle toho, zda běží na vývojářském nebo produkč 🚀 Produkční režim (Production): - - Nezobrazuje žádné ladící informace, všechny chyby zapisuje do logu + - 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 @@ -101,7 +107,7 @@ Nette se chová různě podle toho, zda běží na vývojářském nebo produkč 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) +- 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()`: @@ -124,6 +130,12 @@ $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 ======================== @@ -181,6 +193,8 @@ Konfigurační soubory se obvykle zapisují ve formátu [NEON |neon:format]. V s .[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 @@ -208,7 +222,7 @@ Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou Statické parametry ------------------ -Parametry používané v konfiguračních souborech můžeme definovat [v sekci `parameters` |dependency-injection:configuration#Parametry] a také je předávat (či přepisovat) metodou `addStaticParameters()` (má alias `addParameters()`). Důležité je, že různé hodnoty parametrů způsobí vygenerování dalších DI kontejnerů, tedy dalších tříd. +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([ @@ -222,7 +236,7 @@ Na parametr `projectId` se lze v konfiguraci odkázat obvyklým zápisem `%proje Dynamické parametry ------------------- -Do kontejneru můžeme přidat i dynamické parametry, jejichž různé hodnoty na rozdíl od statických parameterů nezpůsobí generování nových DI kontejnerů. +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([ @@ -242,14 +256,14 @@ $this->configurator->addDynamicParameters([ Výchozí parametry ----------------- -V konfiguračních souborech můžete využít tyto statické 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 +- `%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 @@ -257,7 +271,7 @@ V konfiguračních souborech můžete využít tyto statické parametry: Importované služby ------------------ -Nyní už jdeme hlouběji. Ačkoliv je smyslem DI kontejneru objekty vyrábet, výjimečně může vzniknout potřeba do kontejneru existující objekt vložit. Uděláme to tak, že službu definujeme s příznakem `imported: true`. +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: diff --git a/application/cs/components.texy b/application/cs/components.texy index 7d433ab890..08f8f8df35 100644 --- a/application/cs/components.texy +++ b/application/cs/components.texy @@ -23,7 +23,7 @@ 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, a která komponentu vytvoří a vrátí. +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 @@ -31,7 +31,7 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } @@ -46,7 +46,7 @@ Továrny nikdy nevoláme přímo, zavolají se samy ve chvíli, kdy komponentu p ```php .{file:DefaultPresenter.php} // přistoupíme ke komponentě a pokud to bylo poprvé, -// zavolá se createComponentPoll() která ji vytvoří +// zavolá se createComponentPoll(), která ji vytvoří $poll = $this->getComponent('poll'); // alternativní syntax: $poll = $this['poll']; ``` @@ -59,13 +59,18 @@ V šabloně je možné vykreslit komponentu pomocí značky [{control} |#Vykresl {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. +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. +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. @@ -73,7 +78,7 @@ Tohle zcela mění pohled na psaní aplikací. Čím víc úkolů můžete necha 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`. +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; @@ -87,7 +92,7 @@ class PollControl extends Control Vykreslení ========== -Už víme, že k vykreslení komponenty se používá značka `{control componentName}`. Ta vlastně zavolá metodu `render()` komponenty, ve které se postáráme o vykreslení. K dispozici máme, úplně stejně jako v presenteru, [Latte š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: +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 @@ -141,10 +146,10 @@ $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 jiný způsob renderování uveden ve značce za dvojtečkou. +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 kdekoliv v parametrech objeví **`=>`**, všechny parametry budou zabaleny do pole a předány jako první argument: +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'} @@ -156,7 +161,7 @@ se přeloží jako: $control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); ``` -Vykreslení sub-komponety: +Vykreslení sub-komponenty: ```latte {control cartControl-someForm} @@ -175,7 +180,7 @@ Komponenty, stejně jako presentery, předávají do šablon několik užitečn - `$user` je objekt [reprezentující uživatele |security:authentication] - `$presenter` je aktuální presenter - `$control` je aktuální komponenta -- `$flashes` pole [zpráv |#Flash zprávy] zaslaných funkcí `flashMessage()` +- `$flashes` je pole [zpráv |#Flash zprávy] zaslaných funkcí `flashMessage()` Signál @@ -188,7 +193,7 @@ Tomuto druhu požadavků se říká signály. A podobně jako akce vyvolávají ```php public function handleClick(int $x, int $y): void { - // ... processing of signal ... + // ... zpracování signálu ... } ``` @@ -198,7 +203,7 @@ Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně click here ``` -Signál se vždy volá na aktuálním presenteru a action, není možné jej vyvolat na jiném presenteru nebo jiné action. +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. @@ -212,9 +217,9 @@ Signály vám možná trošku připomínají AJAX: handlery, které se vyvoláva 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í. +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 nebo objekt `stdClass` reprezentující zprávu. Nepovinným druhým parametrem její typ (error, warning, info apod.). Metoda `flashMessage()` vrací instanci flash zprávy jako objekt `stdClass`, které je možné přidávat další informace. +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.'); @@ -230,19 +235,19 @@ $this->redirect(/* ... */); // a přesměrujeme ``` -Přesměrování po signálu -======================= +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 action +$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 +$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: @@ -259,7 +264,7 @@ Persistentní parametry slouží k udržování stavu v komponentách mezi různ 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 */`) +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ý @@ -289,25 +294,25 @@ Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchoz Persistentní komponenty ======================= -Nejen parametry, ale také komponenty mohou být persistentní. U takové komponenty se její persistentní parametry přenáší i mezi různými akcemi presenteru nebo mezi více presentery. Persistentní komponenty značíme anotací u třídy presenteru. Třeba takto označíme komponenty `calendar` a `poll`: +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 -/** - * @persistent(calendar, poll) - */ +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í taky. +Podkomponenty uvnitř těchto komponent není třeba značit, stanou se persistentní také. -V PHP 8 můžete pro označení persistentních komponent použít také atributy: +Starší anotace `@persistent` stále funguje, ale je zastaralá a vyhazuje varování: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -317,7 +322,7 @@ 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. +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`: @@ -325,7 +330,7 @@ Vezměme si jako příklad komponentu, která má závislost na službě `PollFa class PollControl extends Control { public function __construct( - private int $id, // Id ankety pro kterou vytváříme komponentu + private int $id, // ID ankety, pro kterou vytváříme komponentu private PollFacade $facade, ) { } @@ -407,7 +412,7 @@ Komponenty v Nette Application představují znovupoužitelné součásti webov 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í hierachické struktury (5) třídy [Container a Component |component-model:]. +Každou z těchto funkcí obstarává některá z tříd dědičné linie. Vykreslování (1 + 2) má na starosti [api:Nette\Application\UI\Control], začlenění do [životního cyklu |presenters#Životní cyklus presenteru] (3, 4) třída [api:Nette\Application\UI\Component] a vytváření hierarchické struktury (5) třídy [Container a Component |component-model:]. ``` Nette\ComponentModel\Component { IComponent } @@ -431,7 +436,7 @@ Nette\ComponentModel\Component { IComponent } Validace persistentních parametrů --------------------------------- -Hodnoty [persistentních parametrů |#Persistentní parametry] přijatých z URL zapisuje do properties metoda `loadState()`. Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí. +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()`: @@ -455,24 +460,38 @@ class PaginatingControl extends Control 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{signal}`. 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{signal}` s příslušnými parametry. +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]. -Jinými slovy: vezme se definice funkce `handle{signal}` 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 prametr `$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ě. +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 action s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává hned na začátku **parametr `?do`, který určuje signál**. +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. +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ý parameter lze uvést `true` a tím ověřit, jestli je příjemcem nejen uvedená komponenta, ale také kterýkoliv její potomek. +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{signal}` můžeme vykonat signál manuálně zavoláním metody [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], která si bere 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. +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: diff --git a/application/cs/configuration.texy b/application/cs/configuration.texy index b105f5154e..84d79b50a3 100644 --- a/application/cs/configuration.texy +++ b/application/cs/configuration.texy @@ -11,11 +11,11 @@ Application ```neon application: # zobrazit "Nette Application" panel v Tracy BlueScreen? - debugger: ... # (bool) výchozí je true + debugger: ... # (bool) zapne se, je-li k dispozici Tracy - # bude se při chybě volat error-presenter? - # má efekt pouze ve vývojářském režimu - catchExceptions: ... # (bool) výchozí je true + # 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' @@ -40,7 +40,9 @@ application: 5xx: Error5xx # pro ostatní výjimky ``` -Volba `silentLinks` určuje, jak se Nette zachová ve vývojářském režimu, když selže generování odkazu (třeba proto, že neexistuje presenter, atd). Výchozí hodnota `false` znamená, že Nette vyhodí `E_USER_WARNING` chybu. Nastavením na `true` dojde k potlačení této chybové hlášky. V produkčním prostředí se `E_USER_WARNING` vyvolá vždy. Toto chování můžeme také ovlivnit nastavením proměnné presenteru [$invalidLinkMode |creating-links#Neplatné odkazy]. +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. @@ -50,7 +52,7 @@ Volba `silentLinks` určuje, jak se Nette zachová ve vývojářském režimu, k 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: +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: @@ -73,7 +75,7 @@ application: - %vendorDir%/mymodule ``` -Skenování adresářů lze vypnout uvedením hodnoty false. Nedoporučujeme úplně potlačit automatické přidávání presenterů, protože jinak dojde ke snížení výkonu aplikace. +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 @@ -84,7 +86,7 @@ Tímto nastavením lze globálně ovlivnit chování Latte v komponentách a pre ```neon latte: # zobrazit Latte panel v Tracy Baru pro hlavní šablonu (true) nebo všechny komponenty (all)? - debugger: ... # (true|false|'all') výchozí je true + 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 @@ -92,6 +94,12 @@ latte: # 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 @@ -102,7 +110,7 @@ latte: templateClass: App\MyTemplateClass # výchozí je Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Pokud používáte Latte verze 3, můžete přidávat nové [rozšíření |latte:extending-latte#Latte Extension] pomocí: +Nová [rozšíření |latte:extending-latte#Latte Extension] můžete přidávat pomocí: ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Pokud používáte Latte verze 2, můžete registrovat nové tagy (makra) buď uvedením jména třídy, nebo referencí na službu. Jako výchozí je zavolána metoda `install()`, ale to lze změnit tím, že uvedeme jméno jiné metody: - -```neon -latte: - # registrace uživatelských Latte značek - macros: - - App\MyLatteMacros::register # statická metoda, classname nebo callable - - @App\MyLatteMacrosFactory # služba s metodou install() - - @App\MyLatteMacrosFactory::register # služba s metodou register() - -services: - - App\MyLatteMacrosFactory -``` - Routování ========= @@ -133,7 +127,7 @@ Základní nastavení: ```neon routing: # zobrazit routovací panel v Tracy Bar? - debugger: ... # (bool) výchozí je true + debugger: ... # (bool) zapne se s Tracy (jen v debug módu) # serializuje router do DI kontejneru cache: ... # (bool) výchozí je false @@ -185,7 +179,8 @@ Tyto služby se přidávají do DI kontejneru: |---------------------------------------------------------- | `application.application` | [api:Nette\Application\Application] | [spouštěč celé aplikace |how-it-works#Nette Application] | `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | továrna na presentery +| `application.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] diff --git a/application/cs/creating-links.texy b/application/cs/creating-links.texy index 011beea750..703a1206ea 100644 --- a/application/cs/creating-links.texy +++ b/application/cs/creating-links.texy @@ -12,7 +12,7 @@ Tvořit odkazy v Nette je jednoduché, jako ukazovat prstem. Stačí jen namíř
    -Díky [obousměrnému routování |routing] nebudete nikdy muset do šablon či kódu zapisovat navrdo 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. +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 @@ -40,11 +40,11 @@ Je možné předávat i pojmenované parametry. Následující odkaz předává Pokud metoda `ProductPresenter::renderShow()` nemá `$lang` ve své signatuře, může si hodnotu parametru zjistit pomocí `$lang = $this->getParameter('lang')` nebo z [property |presenters#Parametry požadavku]. -Pokud jsou parametry uložené v poli, lze je rozvinout operátorem `...` (v Latte 2.x operátorem `(expand)`): +Pokud jsou parametry uložené v poli, lze je rozvinout operátorem `...`: ```latte {var $args = [$product->id, lang => cs]} -detail produktu +detail produktu ``` V odkazech se také automaticky předávají tzv. [persistentní parametry |presenters#Persistentní parametry]. @@ -73,6 +73,14 @@ $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 =================== @@ -122,6 +130,13 @@ Odkazovat můžeme na určitou část stránky přes tzv. fragment za znakem mř 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 =============== @@ -142,9 +157,9 @@ Cíl `this` vytvoří odkaz na aktuální stránku: 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 parameter. +Zároveň se přenáší i všechny parametry uvedené v signatuře metody `action()` nebo `render()`, pokud není `action()` definovaná. Takže pokud jsme na stránce `Product:show` a `id: 123`, odkaz na `this` předá i tento parametr. -Samozřejmě je možné parametry specifikovat přímo: +Samozřejmě je možné parametry uvést přímo: ```latte refresh @@ -181,8 +196,8 @@ Pro zjištění, zda jsme v určitém modulu nebo jeho submodulu, použijeme met ``` -Změna základu pro odkazy .{data-version:v3.2.7} -=============================================== +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}`: @@ -194,12 +209,13 @@ Ve výchozím stavu se relativní odkazy odvíjejí od aktuálního presenteru. 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] (volají metodu `handle()`). Pak je syntaxe následující: +Cílem odkazu nemusí být jen presenter a akce, ale také [signál |components#Signál] (volá metodu `handle()`). Pak je syntaxe následující: ``` [//] [sub-component:]signal! [#fragment] @@ -240,8 +256,8 @@ $this->getPresenter()->link('Home:default') ``` -Aliasy .{data-version:v3.2.2} -============================= +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`. @@ -267,7 +283,7 @@ Podporované jsou i ve všech metodách pracujících s odkazy, jako je `redirec Neplatné odkazy =============== -Může se stát, že vytvoříme neplatný odkaz - buď proto, že vede na neexistující presenter, nebo proto, že předává víc parametrů, než které cílová metoda přijímá ve své signatuře, nebo když pro cílovou akci nelze vygenerovat URL. Jak naložit s neplatnými odkazy určuje statická proměnná `Presenter::$invalidLinkMode`. Ta může nabývat kombinaci těchto hodnot (konstant): +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 @@ -283,7 +299,7 @@ a[href^="#error:"] { } ``` -Pokud nechceme, aby se ve vývojovém prostředí produkovala varování, můžeme nastavit tichý režim přímo v [konfiguraci|configuration]. +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: @@ -294,9 +310,9 @@ application: 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]. +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]. -LinkGenerátor je služba, kterou si můžete nechat předat přes konstruktor a poté vytvářet odkazy jeho metodou `link()`. +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. diff --git a/application/cs/directory-structure.texy b/application/cs/directory-structure.texy index 3e3f48918a..c1620c48bf 100644 --- a/application/cs/directory-structure.texy +++ b/application/cs/directory-structure.texy @@ -39,7 +39,7 @@ Tuto strukturu můžete libovolně upravovat podle svých potřeb - složky pře 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: +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/ @@ -89,13 +89,13 @@ Pro konzistenci doporučujeme používat: Veřejný adresář `www/` ====================== -Tento adresář je jediný přístupný z webu (tzv. document-root). Často se můžete setkat i s názvem `public/` místo `www/` - je to jen otázka konvence a na funkčnost rostlináře to nemá vliv. Adresář obsahuje: +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 ostranit z URL adresář www]. +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é. @@ -193,7 +193,7 @@ Jednou z velkých výhod této struktury je, jak elegantně se přizpůsobuje ro └── feed.latte ← šablona pro RSS feed \-- -Časem přibydou další typy feedů a potřebujeme pro ně více logiky... Žádný problém! Složka `Export/` se jednoduše stane modulem: +Č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/ @@ -208,7 +208,7 @@ Jednou z velkých výhod této struktury je, jak elegantně se přizpůsobuje ro Tato transformace je naprosto plynulá - stačí vytvořit nové podsložky, rozdělit do nich kód a aktualizovat odkazy (např. z `Export:feed` na `Export:Feed:zbozi`). Díky tomu můžeme strukturu postupně rozšiřovat podle potřeby, úroveň zanoření není nijak omezena. -Pokud například v administraci máte mnoho presenterů týkajících se správy objednávek, jako jsou `OrderDetail`, `OrderEdit`, `OrderDispatch` atd., můžete pro lepší organizovanost v tomto místě vytvořit modul (složku) `Order`, ve kterém budou (složky pro) presentery `Detail`, `Edit`, `Dispatch` a další. +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 @@ -225,13 +225,13 @@ V předchozích ukázkách jsme viděli, že šablony jsou umístěny přímo ve Toto umístění se v praxi ukazuje jako nejpohodlnější - všechny související soubory máte hned po ruce. -Alternativně můžete šablony umístit do podsložky `templates/`. Nette podporuje obě varianty. Dokonce můžete šablony umístit i úplně mimo `Presentation/` složku. Vše o možnostech umístění šablon najdete v kapitole [Hledání šablon |templates#Hledání šablon]. +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 prezenterům a šablonám často patří i další pomocné soubory. Umístíme je logicky podle jejich působnosti: +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: @@ -313,7 +313,7 @@ class PricingService } ``` -**Repozitáře**: zajišťují veškerou komunikaci s datovým úložištěm, typicky databází. Jeho úkolem je načítání a ukládání entit a implementace metod pro jejich vyhledávání. Repozitář odstiňuje zbytek aplikace od implementačních detailů databáze a poskytuje objektově orientované rozhraní pro práci s daty. +**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 @@ -401,7 +401,7 @@ Příklad pro lepší pochopení: 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/`). +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: @@ -420,7 +420,7 @@ Příklad: Co patří do modelu a co do příkazových skriptů? Například logika pro odeslání jednoho e-mailu je součástí modelu, hromadná rozesílka tisíců e-mailů už patří do `Tasks/`. -Úlohy obvykle [spouštíme z příkazového řádku |https://blog.nette.org/cs/cli-skripty-v-nette-aplikaci] nebo přes cron. Lze je spouštět i přes HTTP požadavek, ale je nutné myslet na bezpečnost. Presenter, který úlohu spustí, je potřeba zabezpečit, například jen pro přihlášené uživatele nebo silným tokenem a přístupem z povolených IP adres. U dlouhých úloh je nutné zvýšit časový limit skriptu a použít `session_write_close()`, aby se nezamykala session. +Ú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 @@ -470,7 +470,7 @@ Mapování presenterů Mapování definuje pravidla pro odvozování názvu třídy z názvu presenteru. Specifikujeme je v [konfiguraci|configuration] pod klíčem `application › mapping`. -Na této stránce jsme si ukázali, že presentery umísťujeme do složky `app/Presentation` (případně `app/UI`). Tuto konvenci musíme Nette sdělit v konfiguračním souboru. Stačí jeden řádek: +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: @@ -486,7 +486,7 @@ application: Mapování funguje tak, že název presenteru `Home` nahradí hvězdičku v masce `App\Presentation\*Presenter`, čímž získáme výsledný název třídy `App\Presentation\HomePresenter`. Jednoduché! -Jak ale vidíte v ukázkách v této a dalších kapitolách, třídy presenterů umisťujeme do eponymních podadresářů, například presenter `Home` se mapuje na třídu `App\Presentation\Home\HomePresenter`. Toho dosáhneme zdvojením dvojtečky (vyžaduje Nette Application 3.2): +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: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -Funguje to i pro hlouběji zanořené adresářové struktury, jako je například presenter `Admin:User:Edit`, se segment s hvězdičkou opakuje pro každou úroveň a výsledkem je třída `App\Presentation\Admin\User\Edit\EditPresenter`. +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ů. Tento zápis je ekvivaletní s předchozím: +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: diff --git a/application/cs/how-it-works.texy b/application/cs/how-it-works.texy index f3c4b84a03..63516d0459 100644 --- a/application/cs/how-it-works.texy +++ b/application/cs/how-it-works.texy @@ -51,7 +51,7 @@ Adresářovou strukturu můžete jakkoliv měnit, složky přejmenovat či přes U trošku větších aplikací můžeme složky s presentery a šablonami [rozčlenit do podadresářů |directory-structure#Presentery a šablony] a třídy do jmenných prostorů, kterým říkáme moduly. -Adresář `www/` představuje tzv. veřejný adresář neboli document-root projektu. Můžete jej přejmenovat bez nutnosti cokoliv dalšího nastavovat na straně aplikace. Jen je potřeba [nakonfigurovat hosting |nette:troubleshooting#Jak změnit či ostranit z URL adresář www] tak, aby document-root mířil do tohoto adresáře. +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]: @@ -77,7 +77,7 @@ Jeho úkolem je: Jakou že továrnu? Nevyrábíme přece traktory, ale webové stránky! Vydržte, hned se to vysvětlí. -Slovy „inicializace prostředí“ myslíme například to, že se aktivuje [Tracy|tracy:], což je úžasný nástroj pro logování nebo vizualizaci chyb. Na produkčním serveru chyby loguje, na vývojovém rovnou zobrazuje. Tudíž k inicializaci patří i rozhodnutí, zda web běží v produkčním nebo vývojářském režimu. K tomu Nette používá [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]. +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. @@ -93,9 +93,9 @@ Aplikace psané v Nette se člení do spousty tzv. presenterů (v jiných framew Application začne tím, že požádá tzv. router, aby rozhodl, kterému z presenterů předat aktuální požadavek k vyřízení. Router rozhodne, čí je to zodpovědnost. Podívá se na vstupní URL `https://example.com/product/123` a na základě toho, jak je nastavený, rozhodne, že tohle je práce např. pro **presenter** `Product`, po kterém bude chtít jako **akci** zobrazení (`show`) produktu s `id: 123`. Dvojici presenter + akce je dobrým zvykem zapisovat oddělené dvojtečkou jako `Product:show`. -Tedy router transformoval URL na dvojici `Presenter:action` + parametry, v našem případě `Product:show` + `id: 123`. Jak takový router vypadá se můžete podívat v souboru `app/Core/RouterFactory.php` a podrobně ho popisujeme v kapitole [Routing]. +Tedy router transformoval URL na dvojici `Presenter:action` + parametry, v našem případě `Product:show` + `id: 123`. Jak takový router vypadá, se můžete podívat v souboru `app/Core/RouterFactory.php` a podrobně ho popisujeme v kapitole [Routing]. -Pojďme dál. Application už zná jméno presenteru a může pokračovat dál. Tím že vyrobí objekt třídy `ProductPresenter`, což je kód presenteru `Product`. Přesněji řečeno, požádá DI kontejner, aby presenter vyrobil, protože od vyrábění je tu on. +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: @@ -119,11 +119,11 @@ Vyřizování požadavku přebírá presenter. A úkol zní jasně: proveď akci 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, zavolala se 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`. +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]). +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]. @@ -132,7 +132,7 @@ A následně šablony vykreslí. Tím je úkol presenteru i celé aplikace dokon Pro jistotu, zkusme si zrekapitulovat celý proces s trošku jinou URL: 1) URL bude `https://example.com` -2) bootujeme aplikaci, vytvoří se kontejner a spustí `Application::run()` +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) @@ -147,7 +147,7 @@ Možná jste se teď setkali s velkou spoustou nových pojmů, ale věříme, ž Když už přišla řeč na šablony, v Nette se používá šablonovací systém [Latte |latte:]. Proto taky ty koncovky `.latte` u šablon. Latte se používá jednak proto, že jde o nejlépe zabezpečený šablonovací systém pro PHP, a zároveň také systém nejintuitivnější. Nemusíte se učit mnoho nového, vystačíte si se znalostí PHP a několika značek. Všechno se dozvíte [v dokumentaci |templates]. -V šabloně se [vytvářejí odkazy |creating-links] na další presentery & akce takto: +V šabloně se [vytvářejí odkazy |creating-links] na další presentery a akce takto: ```latte detail produktu @@ -159,7 +159,7 @@ Prostě místo reálného URL napíšete známý pár `Presenter:action` a uvede detail produktu ``` -Generování URL má na starosti už dříve zmíněný router. Totiž routery v Nette jsou výjimečné tím, že dokáží provádět nejen transformace z URL na dvojici presenter:action, ale také obráceně, tedy z názvu presenteru + akce + parametrů vygenerovat URL. Díky tomu v Nette můžete úplně změnit tvary URL v celé hotové aplikaci, aniž byste změnili jediný znak v šabloně nebo presenteru. Jen tím, že upravíte router. Také díky tomu funguje tzv. kanonizace, což je další unikátní vlastnost Nette, která přispívá k lepšímu SEO (optimalizaci nalezitelnosti na internetu) tím, že automaticky zabraňuje existenci duplicitního obsahu na různých URL. Hodně programátorů to považuje za ohromující. +Generování URL má na starosti už dříve zmíněný router. 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 @@ -169,7 +169,7 @@ O presenterech vám musíme prozradit ještě jednu věc: mají v sobě zabudova Komponenty jsou samostatné znovupoužitelné celky, které vkládáme do stránek (tedy presenterů). Mohou to být [formuláře |forms:in-presenter], [datagridy |https://componette.org/contributte/datagrid/], menu, hlasovací ankety, vlastně cokoliv, co má smysl používat opakovaně. Můžeme vytvářet vlastní komponenty nebo používat některé z [ohromné nabídky |https://componette.org] open source komponent. -Komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikacím. Otevřou vám nové možnosti skládání stránek z předpřipravených jednotek. A navíc mají něco společného s [Hollywoodem |components#Hollywood style]. +Komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikací. 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 @@ -177,13 +177,13 @@ 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()`, 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. +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. -Co je na této třídě opravdu speciálního, tak že ji neprogramujete vy, ale framework. On skutečně vygeneruje PHP kód a uloží ho na disk. Vy jen dáváte instrukce, jaké objekty má umět kontejner vyrábět a jak přesně. A tyhle instrukce jsou zapsané v [konfiguračních souborech |bootstrapping#Konfigurace DI kontejneru], pro které se používá formát [NEON|neon:format] a tedy mají i příponu `.neon`. +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 uvedu v sekci [session |http:configuration#Session] volbu `expiration: 14 days`, tak DI kontejner při vytváření objektu `Nette\Http\Session` reprezentujícího session zavolá jeho metodu `setExpiration('14 days')` a tím se konfigurace stane realitou. +Konfigurační soubory slouží čistě k instruování DI kontejneru. Takže když například 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]. diff --git a/application/cs/multiplier.texy b/application/cs/multiplier.texy index 4bd4c80f76..dddd5f573a 100644 --- a/application/cs/multiplier.texy +++ b/application/cs/multiplier.texy @@ -2,7 +2,7 @@ Multiplier: dynamické komponenty ******************************** .[perex] -Nástroj na dynamickou tvorbu interaktivních komponent +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]. @@ -37,7 +37,7 @@ Nyní můžeme v šabloně jednoduše u každého zboží nechat vykreslit formu {/foreach} ``` -Argument předaný v značce `{control}` je ve formátu, který říká: +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` @@ -46,12 +46,12 @@ Při prvním volání bodu **1.** `shopForm` ještě neexistuje, takže se zavol 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 jako svůj první argument dostává název tvořené komponenty. V našem případě to bude `$item->id`, což je přesně ten údaj, který potřebujeme. Stačí tedy lehce upravit tvorbu formuláře: +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 ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; $form->addInteger('count', 'Počet zboží:') ->setRequired(); diff --git a/application/cs/presenters.texy b/application/cs/presenters.texy index ca1a69f0dd..a867660fb6 100644 --- a/application/cs/presenters.texy +++ b/application/cs/presenters.texy @@ -50,16 +50,19 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `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. Je vyžadováno, aby metoda vždy volala předka `parent::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 tak podobně, a poté [přesměruje jinam |#Přesměrování]. +Obdoba metody `render()`. Zatímco `render()` je určená k tomu, aby připravila data pro konkrétní šablonu, která se následně vykreslí, tak v `action()` se zpracovává požadavek bez návaznosti na vykreslování šablony. Například se zpracují data, přihlásí či odhlásí uživatel a 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. @@ -105,7 +108,27 @@ Metoda `afterRender`, jak název opět napovídá, se volá za každou metodou ` Volá se na konci životního cyklu presenteru. -**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. +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 @@ -115,7 +138,7 @@ Odpovědí presenteru je zpravidla [vykreslení šablony s HTML stránkou|templa Kdykoliv během životního cyklu můžeme některou z následujících metod odeslat odpověď a zároveň tak ukončit presenter: -- `redirect()`, `redirectPermanent()`, `redirectUrl()` a `forward()` [přesměruje |#Přesměrování] +- `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] @@ -124,13 +147,13 @@ Kdykoliv během životního cyklu můžeme některou z následujících metod od 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 vykreslí šablony. Proč? Protože v 99 % případů chceme vykreslit šablonu, tudíž presenter tohle chování bere jako výchozí a chce nám ulehčit práci. +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 & akce, následují předávané argumenty, které mohou být uvedeny jako pole: +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); @@ -138,7 +161,7 @@ $url = $this->link('Product:show', $id); $url = $this->link('Product:show', [$id, 'lang' => 'cs']); ``` -V šabloně se vytvářejí odkazy na další presentery & akce tímto způsobem: +V šabloně se vytvářejí odkazy na další presentery a akce tímto způsobem: ```latte detail produktu @@ -152,7 +175,7 @@ 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 syntax jako metoda [link() |#Vytváření odkazů]. +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í: @@ -186,9 +209,9 @@ Před přesměrováním lze odeslat [flash message |#Flash zprávy], tedy zpráv 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í. +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, které je možné přidávat další informace. +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.'); @@ -207,7 +230,7 @@ $this->redirect(/* ... */); // a přesměrujeme Chyba 404 a spol. ================= -Pokud nelze splnit požadavek, třeba z důvodu, že článek který chceme zobrazit neexistuje v databázi, vyhodíme chybu 404 metodou `error(?string $message = null, int $httpCode = 404)`. +Pokud nelze splnit požadavek, třeba z důvodu, že článek, který chceme zobrazit, v databázi neexistuje, vyhodíme chybu 404 metodou `error(string $message = '', int $httpCode = 404)`. ```php public function renderShow(int $id): void @@ -220,13 +243,13 @@ public function renderShow(int $id): void } ``` -HTTP kód chyby lze předat jako druhý parametr, výchozí je 404. Metoda funguje tak, že vyhodí výjimku `Nette\Application\BadRequestException`, načež `Application` předá řízení error-presenteru. Což je presenter, jehož úkolem je zobrazit stránku informující o nastalé chybě. Nastavení error-preseteru se provádí v [konfiguraci application|configuration]. +HTTP kód chyby lze předat jako druhý parametr, výchozí je 404. Metoda funguje tak, že vyhodí výjimku `Nette\Application\BadRequestException`, načež `Application` předá řízení error-presenteru. Což je presenter, jehož úkolem je zobrazit stránku informující o nastalé chybě. Nastavení error-presenteru se provádí v [konfiguraci application|configuration]. Odeslání JSON ============= -Příklad action-metody, která odešle data ve formátu JSON a ukončí presenter: +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 @@ -240,7 +263,7 @@ public function actionData(): void Parametry požadavku .{data-version:3.1.14} ========================================== -Presenter a také každá komponenta získává z HTTP požadavku své parametry. Jejich hodnotu zjistíte metodou `getParameter($name)` nebo `getParameters()`. Hodnoty jsou řetězce či pole řetězců, jde v podstatě o surové data získané přímo z URL. +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]`: @@ -268,9 +291,9 @@ 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 parameter, který musí být neustále součástí URL. Ale bylo by neskutečně únavné ho v každém odkazu uvádět. Tak z něj uděláte persistentní parametr `lang` a bude se přenášet sám. Paráda! +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 */`) +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ý @@ -282,7 +305,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Pokud bude `$this->lang` mít hodnotu například `'en'`, tak i odkazy vytvořené pomocí `link()` nebo `n:href` budou obsahovat parameter `lang=en`. A po kliknutí na odkaz bude opět `$this->lang = 'en'`. +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ů]. @@ -317,6 +340,26 @@ Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchoz ``` +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 ======================= @@ -324,7 +367,7 @@ Presentery v sobě mají zabudovaný komponentový systém. Komponenty jsou samo 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ňku pro Nette, které sem umístili dobrovolníci z komunity okolo frameworku. +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 @@ -337,7 +380,7 @@ S tím, co jsme si dosud v této kapitole ukázali, si nejspíš úplně vystač 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 odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí. +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()`: @@ -364,9 +407,9 @@ Uložení a obnovení požadavku Požadavek, který vyřizuje presenter, je objekt [api:Nette\Application\Request] a vrací ho metoda presenteru `getRequest()`. -Aktuální požadavek lze uložit do session nebo naopak z ní obnovit a nechat jej presenter znovu vykonat. To se hodí například v situaci, když uživatel vyplňuje formulář a vyprší mu přihlášení. Aby o data nepřišel, před přesměrováním na přihlašovací stránku aktuální požadavek uložíme do session pomocí `$reqId = $this->storeRequest()`, které vrátí jeho identifikátor v podobě krátkého řetězce a ten předáme jako parameter přihlašovacímu presenteru. +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 a forwarduje na něj. Metoda přitom ověří, že požadavek vytvořil stejný uživatel, jako se nyní přihlásil. Pokud by se přihlásil jiný uživatel nebo klíč byl neplatný, neudělá nic a program pokračuje dál. +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]. @@ -376,7 +419,7 @@ 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. Kanonickou URL je ta, kterou vygeneruje [router|routing], zpravidla tedy první odpovídající routa v kolekci. +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`. @@ -388,30 +431,12 @@ Kanonizaci můžete vyvolat i manuálně pomocí metody `canonicalize()`, které public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); - // přesměruje, pokud $slug se liší od $realSlug + // přesměruje, pokud se $slug liší od $realSlug $this->canonicalize('Product:show', [$id, $realSlug]); } ``` - -Události --------- - -Kromě metod `startup()`, `beforeRender()` a `shutdown()`, které se volají jako součást životního cyklu presenteru, lze definovat ještě další funkce, které se mají automaticky zavolat. Presenter definuje tzv. [událost |nette:glossary#události], jejichž handlery přidáte do polí `$onStartup`, `$onRender` a `$onShutdown`. - -```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()`. +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 @@ -447,19 +472,79 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht $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); -Omezení přístupu pomocí `#[Requires]` .{data-version:3.2.2} + $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 pro specifikaci HTTP metod, vyžadování AJAXového požadavku, omezení na stejný původ (same origin), a přístup pouze přes forwardování. Atribut lze aplikovat jak na třídy presenterů, tak na jednotlivé metody `action()`, `render()`, `handle()` a `createComponent()`. +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 tyto omezení: +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]. @@ -468,7 +553,7 @@ 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): +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'])] @@ -477,20 +562,28 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Ve verzi 3.1 se ověření provádí v `checkHttpMethod()`, která zjišťuje, zda je metoda specifikovaná v požadavku obsažena v poli `$presenter->allowedMethods`. Přidání metody udělejte takto: +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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -Je důležité zdůraznit, že pokud povolíte metodu `OPTIONS`, musíte ji následně také patřičně obsloužit v rámci svého presenteru. Metoda je často používána jako tzv. preflight request, který prohlížeč automaticky odesílá před skutečným požadavkem, když je potřeba zjistit, zda je požadavek povolený z hlediska CORS (Cross-Origin Resource Sharing) politiky. Pokud metodu povolíte, ale neimplementujete správnou odpověď, může to vést k nekonzistencím a potenciálním bezpečnostním problémům. +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 diff --git a/application/cs/routing.texy b/application/cs/routing.texy index 826bed4363..e7f69b6cb7 100644 --- a/application/cs/routing.texy +++ b/application/cs/routing.texy @@ -3,7 +3,7 @@ Routování
    -Router má na starosti vše okolo URL adres, aby vy už jste nad nimi nemuseli přemýšlet. Ukážeme si: +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í @@ -14,15 +14,15 @@ Router má na starosti vše okolo URL adres, aby vy už jste nad nimi nemuseli p Lidštější URL (nebo taky cool či pretty URL) jsou použitelnější, zapamatovatelnější a pozitivně přispívají k SEO. Nette na to myslí a vychází vývojářům plně vstříc. Můžete si pro svou aplikaci navrhnout přesně takovou strukturu URL adres, jakou budete chtít. Můžete ji navrhnout dokonce až ve chvíli, když už je aplikace hotová, protože se to obejde bez zásahů do kódu či šablon. Definuje se totiž elegantním způsobem na jednom [jediném místě |#Začlenění do aplikace], v routeru, a není tak roztroušen ve formě anotací ve všech presenterech. -Router v Nette je mimořádný tím, že je **obousměrný.** Umí jak dekódovat URL v HTTP požadavku, tak i odkazy vytvářet. Hraje tedy zásadní roli v [Nette Application |how-it-works#Nette Application], protože jednak rozhoduje o tom, který presenter a action bude vykonávat aktuální požadavek, ale také se využívá pro [generování URL |creating-links] v šabloně atd. +Router v Nette je mimořádný tím, že je **obousměrný.** Umí jak dekódovat URL v HTTP požadavku, tak i odkazy vytvářet. Hraje tedy zásadní roli v [Nette Application |how-it-works#Nette Application], protože jednak rozhoduje o tom, který presenter a 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í limitován jen pro tohle využití, můžete jej použít v aplikacích, kde se vůbec presentery nepoužívají, pro REST API, atd. Více v části [#samostatné použití]. +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]. Definice je tvořena seznamem tzv. rout, tedy masek URL adres a k nim přidružených presenterů a akcí pomocí jednoduchého API. Routy nemusíme nijak pojmenovávat. +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; @@ -97,7 +97,7 @@ $router->addRoute('/', 'Home:default'); Uvedená routa akceptuje např. URL ve tvaru `/article/edit` nebo také `/catalog/list` a chápe je jako presentery a akce `Article:edit` a `Catalog:list`. -Zaroveň dává parametrům `presenter` a `action` výchozí hodnoty `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 `/`. +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: @@ -146,7 +146,7 @@ $router->addRoute('[/]', /* ... */); // /download => lang => null, name => download ``` -Když je parametr součásti volitelné sekvence, stává se pochopitelně také volitelným. Pokud nemá uvedenou výchozí hodnotu, tak bude null. +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ě: @@ -198,7 +198,7 @@ $router->addRoute('[[/[/]]]', /* ... */); Zástupné znaky -------------- -V masce absolutní cesty 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í: +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` @@ -208,14 +208,14 @@ V masce absolutní cesty můžeme použít následující zástupné znaky a vyh ```php $router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` Rozšířený zápis --------------- -Cíl routy obvykle zapisovaný ve tvaru `Presenter:action` může být také zapsát pomocí pole, které definuje jednotlivé parametry a jejich výchozí hodnoty: +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('/[/]', [ @@ -244,6 +244,16 @@ $router->addRoute('/[/]', [ 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 ----------------- @@ -280,7 +290,7 @@ $router->addRoute('/', [ Více klíčů překladového slovníku může vést na tentýž presenter. Tím se k němu vytvoří různé aliasy. Za kanonickou variantu (tedy tu, která bude ve vygenerovaném URL) se považuje poslední klíč. -Překladovou tabulku lze tímto způsobem použít na jakýkoliv parametr. Přičemž pokud překlad neexistuje, bere se původní hodnota. Tohle chování můžeme změnit doplněním `Route::FilterStrict => true` a routa pak odmítne URL, pokud hodnota není ve slovníku. +Překladovou tabulku lze tímto způsobem použít na jakýkoliv parametr, přičemž pokud překlad neexistuje, bere se původní hodnota. Tohle chování můžeme změnit doplněním `Route::FilterStrict => true` a routa pak odmítne URL, pokud hodnota není ve slovníku. Kromě překladového slovníku v podobě pole lze nasadit i vlastní překladové funkce. @@ -300,13 +310,13 @@ $router->addRoute('//', [ Funkce `Route::FilterIn` převádí mezi parametrem v URL a řetězcem, který se poté předá do presenteru, funkce `FilterOut` zajišťuje převod opačným směrem. -Parametry `presenter`, `action` a `module` už mají předdefinované filtry, které převádějí mezi stylem PascalCase resp. camelCase a kebab-case používaným v URL. Výchozí hodnota parametrů se zapisuje už v transformované podobě, takže třeba v případě presenteru píšeme ``, nikoliv ``. +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ů, které mohou jakkoliv modifikovat a poté je vrátí. Obecné filtry definujeme pod klíčem `null`. +Vedle filtrů určených pro konkrétní parametry můžeme definovat též obecné filtry, které obdrží asociativní pole všech parametrů, mohou je jakkoliv upravit a poté vrátí. Obecné filtry definujeme pod prázdným klíčem. ```php use Nette\Routing\Route; @@ -314,7 +324,7 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], @@ -323,7 +333,9 @@ $router->addRoute('/', [ 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. +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 @@ -333,7 +345,7 @@ Jednosměrné routy se používají pro zachování funkčnosti starých URL, kt ```php // staré URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); +$router->addRoute('product-info', 'Product:detail', oneWay: true); // nové URL /product/123 $router->addRoute('product/', 'Product:detail'); ``` @@ -363,11 +375,19 @@ $router->addRoute('', function (string $lang) { }); ``` +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()`: +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; @@ -379,7 +399,7 @@ $router->withModule('Forum') // následující routy jsou součástí modulu For ->addRoute('sign:in', 'Sign:in'); ``` -Alternativou je použití parametru `module`: +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 @@ -388,6 +408,12 @@ $router->addRoute('manage//', [ ]); ``` +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 --------- @@ -501,7 +527,7 @@ 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]: +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 @@ -522,7 +548,7 @@ Generuje adresy zhruba v tomto tvaru: http://example.com/?presenter=Product&action=detail&id=123 ``` -Parametrem konstruktoru SimpleRouteru je výchozí presenter & akce, na který se má směřovat, pokud otevřeme stránku bez parametrů, např. `http://example.com/`. +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' @@ -542,7 +568,7 @@ SEO a kanonizace Framework přispívá k SEO (optimalizaci nalezitelnosti na internetu) tím, že zabraňuje duplicitě obsahu na různých URL. Pokud k určitému cíli vede více adres, např. `/index` a `/index.html`, framework první z nich určí za primární (kanonickou) a ostatní na ni přesměruje pomocí HTTP kódu 301. Díky tomu vám vyhledávače stránky nezaindexují dvakrát a nerozmělní jejich page rank. -Tomuto procesu se říká kanonizace. Kanonickou URL je ta, kterou vygeneruje router, tj. první vyhovující routa v kolekci bez příznaku OneWay. Proto v kolekci uvádíme **primární routy jako první**. +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]. @@ -564,7 +590,7 @@ Přesměrování celého webu na HTTPS je nutné nastavit na úrovni serveru, na ``` -Router generuje URL se stejným protokolem, s jakým byla stránka načtena, takže nic víc není pořeba nastavovat. +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: @@ -582,14 +608,14 @@ 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 & akci. +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ádaly přesměrování. +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 @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -Metoda `match` zpracuje aktuální požadavek [$httpRequest |http:request], ze kterého lze získat nejen URL, ale i hlavičky atd., do pole obsahující název presenteru a jeho parametry. Pokud požadavek zpracovat neumí, vrátí null. Při zpracování požadavku musíme vrátit minimálně presenter a akci. Název presenteru je úplný a obsahuje i případné moduly: +Metoda `match` zpracuje aktuální požadavek [$httpRequest |http:request], ze kterého lze získat nejen URL, ale i hlavičky atd., do pole obsahující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 [ @@ -682,7 +708,7 @@ class RouterFactory } ``` -Pokud používáte DI kontejner, což doporučujeme, opět metodu přidáme do konfigurace a poté router společne s HTTP požadavkem získáme z kontejneru: +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); @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/cs/templates.texy b/application/cs/templates.texy index 4ded503e34..b5ae830d67 100644 --- a/application/cs/templates.texy +++ b/application/cs/templates.texy @@ -31,7 +31,7 @@ A tohle bude šablona akce: {/block} ``` -Ta definuje blok `content`, který se vloží na místo `{include content}` v layoutu, a také re-definuje blok `title`, kterým přepíše `{block title}` v layoutu. Zkuste si představit výsledek. +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 @@ -39,7 +39,7 @@ 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`: +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/ @@ -49,16 +49,16 @@ app/ └── 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`: +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 + ├── 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ů. @@ -96,9 +96,9 @@ app/ ├── HomePresenter.php └── templates/ ├── @layout.latte ← společný layout - ├── Home.@layout.latte ← jen pro Home, 1. varianta - └── Home/ - └── @layout.latte ← jen pro Home, 2. varianta + ├── 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. @@ -114,15 +114,48 @@ Soubory, kde se dohledávají šablony layoutu, lze změnit překrytím metody [ Proměnné v šabloně ------------------ -Proměnné do šablony předáváme tak, že je zapíšeme do `$this->template` a potom je máme k dispozici v šabloně jako lokální proměnné: +Proměnné do šablony předáváme zápisem do `$this->template`. V šabloně jsou pak dostupné jako lokální proměnné: ```php $this->template->article = $this->articles->getById($id); ``` -Takto jednoduše můžeme do šablon předat jakékoliv proměnné. Při vývoji robustních aplikací ale bývá užitečnější se omezit. Například tak, že explicitně nadefinujeme výčet proměnných, které šablona očekává, a jejich typů. Díky tomu nám bude moci PHP kontrolovat typy, IDE správně našeptávat a statická analýza odhalovat chyby. +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} -A jak takový výčet nadefinujeme? Jednoduše v podobě třídy a její properties. Pojmenujeme ji podobně jako presenter, jen s `Template` na konci: +```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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -Objekt `$this->template` v presenteru bude nyní instancí třídy `ArticleTemplate`. Takže PHP bude při zápisu kontrolovat deklarované typy. A počínaje verzí PHP 8.2 upozorní i na zápis do neexistující proměnné, v předchozích verzích lze téhož dosáhnout použitím traity [Nette\SmartObject |utils:smartobject]. +Objekt `$this->template` v presenteru bude nyní instancí třídy `ArticleTemplate`. PHP tak bude při zápisu kontrolovat deklarované typy. + +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` je určená pro IDE a statickou analýzu, díky ní bude fungovat našeptávání, viz "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. +Anotace `@property-read` slouží 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 *] -Luxusu našeptávání si můžete dopřát i v šablonách, stačí do PhpStorm nainstalovat plugin pro Latte a uvést na začátek šablony název třídy, více v článku "Latte: jak na typový systém":https://blog.nette.org/cs/latte-jak-na-typovy-system: +Našeptávání můžete využít i přímo v šablonách. Stačí do PhpStorm nainstalovat plugin pro Latte a uvést na začátek šablony název třídy parametrů šablony, více v kapitole [Latte: typový systém |latte:type-system]: ```latte {templateType App\Presentation\Article\ArticleTemplate} ... ``` -Takto fungují i šablony v komponentách, stačí jen dodržet jmennou konvenci a pro komponentu např. `FifteenControl` vytvořit třídu šablony `FifteenTemplate`. +Totéž platí i pro komponenty. Stačí dodržet jmennou konvenci a pro komponentu např. `FifteenControl` vytvořit třídu parametrů `FifteenTemplate`. -Pokud potřebujete vytvořit `$template` jako instanci jiné třídy, využijte metodu `createTemplate()`: +Pokud potřebujete použít jinou třídu parametrů, využijte metodu `createTemplate()`: ```php public function renderDefault(): void @@ -168,26 +203,22 @@ public function renderDefault(): void } ``` +.{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: -Výchozí proměnné ----------------- - -Presentery a komponenty předávají do šablon několik užitečných proměnných automaticky: - -- `$basePath` je absolutní URL cesta ke kořenovému adresáři (např. `/eshop`) -- `$baseUrl` je absolutní URL ke kořenovému adresáři (např. `http://localhost/eshop`) -- `$user` je objekt [reprezentující uživatele |security:authentication] -- `$presenter` je aktuální presenter -- `$control` je aktuální komponenta nebo presenter -- `$flashes` pole [zpráv |presenters#Flash zprávy] zaslaných funkcí `flashMessage()` - -Pokud používáte vlastní třídu šablony, tyto proměnné se předají, pokud pro ně vytvoříte property. +```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 & akce tímto způsobem: +V šabloně se vytvářejí odkazy na další presentery a akce tímto způsobem: ```latte detail produktu @@ -205,21 +236,67 @@ Více informací najdete v kapitole [Vytváření odkazů URL|creating-links]. Vlastní filtry, značky apod. ---------------------------- -Šablonovací systém Latte lze rozšířit o vlastní filtry, funkce, značky apod. Lze tak učinit přímo v metodě `render` nebo `beforeRender()`: +Šablonovací systém Latte lze rozšířit o vlastní filtry, funkce, značky a další prvky. K dispozici jsou tři způsoby, jak to udělat, od nejrychlejších ad-hoc řešení až po architektonický přístup pro celou aplikaci. + +**Ad-hoc v metodách presenteru** + +Nejrychlejší způsob je přidat filtr nebo funkci přímo v kódu presenteru či komponenty. V presenteru je k tomu vhodná metoda `beforeRender()` nebo `render()`: ```php -public function beforeRender(): void +protected function beforeRender(): void { // přidání filtru - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => number_format($val, 2) . ' Kč'); - // nebo konfigurujeme přímo objekt Latte\Engine + // 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->addFilterLoader(/* ... */); + $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 ve verzi 3 nabízí pokročilejší způsob a to vytvoření si [extension |latte:extending-latte#Latte Extension] pro každý webový projekt. Kusý příklad takové třídy: +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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -Zaregistrujeme ji pomocí [konfigurace |configuration#Šablony Latte]: +Extension zaregistrujeme pomocí [konfigurace |configuration#Šablony Latte]: ```neon latte: @@ -263,11 +345,25 @@ latte: - App\Presentation\Accessory\LatteExtension ``` +Výhodou extension je, že lze využít dependency injection, mít přístup k modelové vrstvě aplikace a všechna rozšíření mít přehledně na jednom místě. Extension umožň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ích, které najdete na [Componette |https://componette.org/search/localization]. V jejich dokumentaci se dozvíte, jak translator konfigurovat. +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()`: diff --git a/application/cs/upgrading.texy b/application/cs/upgrading.texy new file mode 100644 index 0000000000..d4b144c738 --- /dev/null +++ b/application/cs/upgrading.texy @@ -0,0 +1,47 @@ +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í. diff --git a/application/de/@home.texy b/application/de/@home.texy index a151863801..65ca4bdcc0 100644 --- a/application/de/@home.texy +++ b/application/de/@home.texy @@ -2,13 +2,13 @@ Nette Application ***************** .[perex] -Nette Application ist der Kern des Nette Frameworks und bietet leistungsstarke Werkzeuge zur 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. +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 herunter und installieren Sie sie mit [Composer|best-practices:composer]: +Laden Sie die Bibliothek mit [Composer|best-practices:composer] herunter und installieren Sie sie: ```shell composer require nette/application @@ -18,68 +18,68 @@ composer require nette/application Warum Nette Application wählen? ------------------------------- -Nette war schon immer ein Pionier im Bereich der Webtechnologien. +Nette war schon immer ein Vorreiter bei Webtechnologien. -**Bidirektionaler Router:** Nette verfügt über ein fortschrittliches Routing-System, das durch seine Bidirektionalität einzigartig ist – es übersetzt nicht nur URLs in Anwendungsaktionen, sondern kann auch rückwirkend URL-Adressen generieren. Das bedeutet: -- Sie können jederzeit die URL-Struktur der gesamten Anwendung ändern, ohne die Templates anpassen zu müssen -- URLs werden automatisch kanonisiert, was die SEO verbessert -- Das Routing wird an einer Stelle definiert, nicht verstreut in Annotationen +**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 integrierte Komponentensystem, inspiriert von Delphi und React.js, ist unter PHP-Frameworks völlig einzigartig: -- Ermöglicht die Erstellung wiederverwendbarer UI-Elemente -- Unterstützt die hierarchische Zusammensetzung von Komponenten -- Bietet eine elegante Verarbeitung von AJAX-Anfragen mittels Signalen +**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 bereits 2009 eine revolutionäre Methode zur Arbeit mit AJAX ein, lange vor ähnlichen Lösungen wie Hotwire für Ruby on Rails oder Symfony UX Turbo: -- Snippets ermöglichen die Aktualisierung nur von Teilen der Seite, ohne JavaScript schreiben zu müssen +**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 Seitenteilen -- Minimale Menge an übertragenen Daten +- Intelligente Invalidierung von Seitenabschnitten +- Minimale Datenübertragung -**Intuitive Templates [Latte|latte:]:** Das sicherste Template-System für PHP mit erweiterten Funktionen: -- Automatischer Schutz vor XSS durch kontextsensitives Escaping -- Erweiterbarkeit durch eigene Filter, Funktionen und Tags +**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 -- Hervorragende Unterstützung für PHP 8.x mit Typsystem +- Ausgezeichnete Unterstützung für PHP 8.x samt Typsystem -**Dependency Injection:** Nette nutzt Dependency Injection vollständig aus: +**Dependency Injection:** Nette nutzt Dependency Injection voll aus: - Automatische Übergabe von Abhängigkeiten (Autowiring) -- Konfiguration mittels übersichtlichem NEON-Format +- Konfiguration im übersichtlichen NEON-Format - Unterstützung für Komponenten-Factories Hauptvorteile ------------- -- **Sicherheit**: Automatischer Schutz vor [Schwachstellen|nette:vulnerability-protection] wie XSS, CSRF, etc. -- **Produktivität**: Weniger schreiben, mehr Funktionen dank cleverem Design +- **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 Anpassung von URLs auch nach Fertigstellung der Anwendung +- **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.4+ und Typsystem +- **Modern**: Volle Unterstützung für PHP 8.3+ und das Typsystem Erste Schritte -------------- -1. [Wie Anwendungen funktionieren |how-it-works] - Verständnis der grundlegenden Architektur +1. [Wie funktionieren Anwendungen? |how-it-works] - Verständnis der grundlegenden Architektur 2. [Presenter |presenters] - Arbeit mit Presentern und Aktionen -3. [Templates |templates] - Erstellung von Templates in Latte -4. [Routing |routing] - Konfiguration von URL-Adressen -5. [Interaktive Komponenten |components] - Nutzung des Komponentensystems +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 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| 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 letzte Patch-Version. +Gilt für die jeweils letzten Patch-Versionen. diff --git a/application/de/@left-menu.texy b/application/de/@left-menu.texy index 2a16b701bb..c9bd5699cf 100644 --- a/application/de/@left-menu.texy +++ b/application/de/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** -- [Wie Anwendungen funktionieren |how-it-works] -- [Bootstrapping] -- [Presenter |presenters] -- [Templates |templates] +- [Übersicht |@home] +- [Wie funktionieren Anwendungen? |how-it-works] +- [Bootstrapping|bootstrapping] +- [Presenter|presenters] +- [Templates|templates] - [Verzeichnisstruktur |directory-structure] -- [Routing |routing] -- [Erstellen von URL-Links |creating-links] +- [Routing|routing] +- [URL-Links erstellen |creating-links] - [Interaktive Komponenten |components] - [AJAX & Snippets |ajax] -- [Multiplier |Multiplier] -- [Konfiguration |configuration] +- [Multiplier |multiplier] +- [Konfiguration|configuration] +- [Upgrade|upgrading] -Weitere Lektüre -*************** -- [Warum Nette verwenden? |www:10-reasons-why-nette] +Weiterführende Lektüre +********************** +- [Warum Nette verwenden?|www:10-reasons-why-nette] - [Installation |nette:installation] -- [Schreiben wir die erste Anwendung! |quickstart:] -- [Anleitungen und Verfahren |best-practices:] +- [Erstellen Sie Ihre erste Anwendung! |quickstart:] +- [Best Practices |best-practices:] - [Fehlerbehebung |nette:troubleshooting] diff --git a/application/de/ajax.texy b/application/de/ajax.texy index 87844ef98b..5fa29383e4 100644 --- a/application/de/ajax.texy +++ b/application/de/ajax.texy @@ -3,20 +3,20 @@ AJAX & Snippets
    -In der Ära moderner Webanwendungen, in der die Funktionalität oft zwischen Server und Browser aufgeteilt ist, ist AJAX ein unverzichtbares Bindeglied. Welche Möglichkeiten bietet uns das Nette Framework in diesem Bereich? +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 -- Tools zum Debuggen von AJAX-Anfragen +- Werkzeuge zum Debuggen von AJAX-Requests
    -AJAX-Anfrage +AJAX-Request ============ -Eine AJAX-Anfrage unterscheidet sich im Grunde nicht von einer klassischen HTTP-Anfrage. Ein Presenter wird mit bestimmten Parametern aufgerufen. Und es liegt am Presenter, wie er auf die Anfrage reagiert – er kann Daten im JSON-Format zurückgeben, einen Teil des HTML-Codes senden, ein XML-Dokument usw. +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 initialisieren wir die AJAX-Anfrage mit der Funktion `fetch()`: +Auf der Browserseite starten wir einen AJAX-Request mit der Funktion `fetch()`: ```js fetch(url, { @@ -24,22 +24,22 @@ fetch(url, { }) .then(response => response.json()) .then(payload => { - // Verarbeitung der Antwort + // die Antwort verarbeiten }); ``` -Auf der Serverseite erkennen wir eine AJAX-Anfrage mit der Methode `$httpRequest->isAjax()` des [die HTTP-Anfrage kapselnden |http:request] Dienstes. Zur Erkennung verwendet sie den HTTP-Header `X-Requested-With`, daher ist es wichtig, diesen mitzusenden. Innerhalb des Presenters kann die Methode `$this->isAjax()` verwendet werden. +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 möchten, verwenden Sie die Methode [`sendJson()` |presenters#Senden der Antwort]. Die Methode beendet auch die Aktivität des Presenters. +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); + $this->sendJson($this->model->getData()); } ``` -Wenn Sie planen, mit einem speziellen Template für AJAX zu antworten, können Sie dies wie folgt tun: +Wenn Sie mit einem speziellen, für AJAX gedachten Template antworten wollen, geht das so: ```php public function handleClick($param): void @@ -55,38 +55,38 @@ public function handleClick($param): void Snippets ======== -Das stärkste Mittel, das Nette zur Verbindung von Server und Client bietet, sind Snippets. Dank ihnen können Sie eine gewöhnliche Anwendung mit minimalem Aufwand und wenigen Codezeilen in eine AJAX-Anwendung verwandeln. Wie das Ganze funktioniert, demonstriert das Beispiel Fifteen, dessen Code Sie auf [GitHub |https://github.com/nette-examples/fifteen] finden. +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, oder Ausschnitte, ermöglichen es, nur Teile der Seite zu aktualisieren, anstatt die gesamte Seite neu zu laden. Dies ist nicht nur schneller und effizienter, sondern bietet auch eine komfortablere Benutzererfahrung. Snippets erinnern vielleicht an Hotwire für Ruby on Rails oder Symfony UX Turbo. Interessanterweise hat Nette Snippets bereits 14 Jahre früher eingeführt. +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 (nicht-AJAX-Anfrage) wird die gesamte Seite einschließlich aller Snippets geladen. Wenn der Benutzer mit der Seite interagiert (z. B. auf einen Button klickt, ein Formular absendet usw.), wird anstelle des Ladens der gesamten Seite eine AJAX-Anfrage 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 in Form eines Arrays im JSON-Format. Der Verarbeitungscode im Browser fügt die empfangenen Snippets wieder in die Seite ein. Es wird also nur der Code der geänderten Snippets übertragen, was Bandbreite spart und das Laden im Vergleich zur Übertragung des gesamten Seiteninhalts beschleunigt. +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 ---- -Zur Verarbeitung von Snippets auf der Browserseite dient die [Bibliothek Naja |https://naja.js.org]. Diese [installieren Sie |https://naja.js.org/#/guide/01-install-setup-naja] als node.js-Paket (zur Verwendung mit Anwendungen wie Webpack, Rollup, Vite, Parcel und anderen): +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 fügen Sie sie direkt in das Seiten-Template ein: +…oder binden Sie sie direkt in das Template der Seite ein: ```latte - + ``` -Zuerst muss die Bibliothek [initialisiert |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] werden: +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 eine AJAX-Anfrage zu machen, genügt es, den entsprechenden Link, das Formular oder den Button mit der Klasse `ajax` zu kennzeichnen: +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 -Go +Los
    @@ -100,32 +100,34 @@ oder ``` -Neuzeichnen von Snippets ------------------------- +Snippets neu zeichnen +--------------------- -Jedes Objekt der Klasse [Control |components] (einschließlich des Presenters selbst) verfolgt, ob Änderungen aufgetreten sind, die sein Neuzeichnen erfordern. Dazu dient die Methode `redrawControl()`: +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 relevante Teil neu gezeichnet werden + // nach dem Login muss der betreffende Teil neu gezeichnet werden $this->redrawControl(); // ... } ``` -Nette ermöglicht eine noch feinere Kontrolle darüber, was neu gezeichnet werden soll. Die genannte Methode kann nämlich als Argument den Namen des Snippets entgegennehmen. Man kann also auf der Ebene von Template-Teilen invalidieren (sprich: Neuzeichnen erzwingen). Wenn die gesamte Komponente invalidiert wird, wird auch jedes ihrer Snippets neu gezeichnet: +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 extrem einfach. Um einen Teil des Templates als Snippet zu definieren, umschließen Sie ihn einfach mit den Tags `{snippet}` und `{/snippet}`: +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} @@ -133,9 +135,9 @@ Die Verwendung von Snippets in Latte ist extrem einfach. Um einen Teil des Templ {/snippet} ``` -Das Snippet erstellt in der HTML-Seite ein `
    `-Element mit einer speziellen generierten `id`. Beim Neuzeichnen des Snippets wird dann der Inhalt dieses Elements aktualisiert. Daher ist es notwendig, dass beim erstmaligen Rendern der Seite auch alle Snippets gerendert werden, auch wenn sie anfangs leer sein mögen. +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. -Sie können auch ein Snippet mit einem anderen Element als `
    ` erstellen, indem Sie ein n:Attribut verwenden: +Ein Snippet lässt sich mit einem n:Attribut auch mit einem anderen Element als `
    ` erzeugen: ```latte
    @@ -147,7 +149,7 @@ Sie können auch ein Snippet mit einem anderen Element als `
    ` erstellen, in Snippet-Bereiche ---------------- -Snippet-Namen können auch Ausdrücke sein: +Namen von Snippets können auch Ausdrücke sein: ```latte {foreach $items as $id => $item} @@ -155,7 +157,9 @@ Snippet-Namen können auch Ausdrücke sein: {/foreach} ``` -So entstehen mehrere Snippets `item-0`, `item-1` usw. Wenn wir ein dynamisches Snippet direkt invalidieren würden (zum Beispiel `item-1`), würde nichts neu gezeichnet werden. Der Grund dafür ist, dass Snippets wirklich wie Ausschnitte funktionieren und nur sie selbst direkt gerendert werden. Im Template gibt es jedoch faktisch kein Snippet namens `item-1`. Dieses entsteht erst durch die Ausführung des Codes um das Snippet herum, also der foreach-Schleife. Wir kennzeichnen daher den Teil des Templates, der ausgeführt werden soll, mit dem Tag `{snippetArea}`: +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
      @@ -165,16 +169,16 @@ So entstehen mehrere Snippets `item-0`, `item-1` usw. Wenn wir ein dynamisches S
    ``` -Und lassen sowohl das Snippet selbst als auch den gesamten übergeordneten Bereich neu zeichnen: +Und wir fordern das Neuzeichnen sowohl des einzelnen Snippets als auch des gesamten übergeordneten Bereichs an: ```php $this->redrawControl('itemsContainer'); $this->redrawControl('item-1'); ``` -Gleichzeitig ist es ratsam sicherzustellen, dass das Array `$items` nur die Elemente enthält, die neu gezeichnet werden sollen. +Zugleich ist es ratsam, dafür zu sorgen, dass das Array `$items` nur die Einträge enthält, die neu gezeichnet werden sollen. -Wenn wir mittels des `{include}`-Tags ein anderes Template einfügen, das Snippets enthält, muss das Einfügen des Templates ebenfalls in eine `snippetArea` eingeschlossen und diese zusammen mit dem Snippet invalidiert werden: +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} @@ -198,22 +202,22 @@ $this->redrawControl('item'); Snippets in Komponenten ----------------------- -Sie können Snippets auch in [Komponenten|components] erstellen und Nette wird sie automatisch neu zeichnen. Es gibt jedoch eine gewisse Einschränkung: Für das Neuzeichnen von Snippets ruft Nette die Methode `render()` ohne Parameter auf. Das bedeutet, dass die Übergabe von Parametern im Template nicht funktioniert: +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} -wird nicht funktionieren: +funktioniert nicht: {control productGrid $arg, $arg} {control productGrid:paginator} ``` -Senden von Benutzerdaten ------------------------- +Eigene Daten senden +------------------- -Zusammen mit den Snippets können Sie beliebige zusätzliche Daten an den Client senden. Schreiben Sie diese einfach in das `payload`-Objekt: +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 @@ -226,10 +230,16 @@ public function actionDelete(int $id): void ``` -Parameterübergabe -================= +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. + -Wenn wir einer Komponente über eine AJAX-Anfrage Parameter senden, seien es Signalparameter oder persistente Parameter, müssen wir bei der Anfrage deren globalen Namen angeben, der auch den Namen der Komponente enthält. Den vollständigen Namen des Parameters gibt die Methode `getParameterId()` zurück. +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!}); @@ -247,3 +257,9 @@ 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 index 6a61ac820c..dabfea3a5b 100644 --- a/application/de/bootstrapping.texy +++ b/application/de/bootstrapping.texy @@ -3,19 +3,22 @@ Bootstrapping
    -Bootstrapping ist der Prozess der Initialisierung der Anwendungsumgebung, der Erstellung eines Dependency Injection (DI) Containers und des Startens der Anwendung. Wir werden besprechen: +Bootstrapping ist der Prozess der Initialisierung der Anwendungsumgebung, der Erzeugung des Dependency-Injection-Containers (DI) und des Startens der Anwendung. Wir besprechen: -- wie die Bootstrap-Klasse die Umgebung initialisiert +- wie die Klasse Bootstrap die Umgebung initialisiert - wie Anwendungen mit NEON-Dateien konfiguriert werden -- wie zwischen Produktions- und Entwicklungsmodus unterschieden wird -- wie der DI-Container erstellt und konfiguriert wird +- wie man Produktions- und Entwicklungsmodus unterscheidet +- wie man den DI-Container erzeugt und konfiguriert
    -Anwendungen, egal ob Webanwendungen oder von der Kommandozeile gestartete Skripte, beginnen ihre Ausführung mit einer Form der Initialisierung der Umgebung. Früher war dafür eine Datei wie `include.inc.php` verantwortlich, die von der initialen Datei eingebunden wurde. In modernen Nette-Anwendungen wurde dies durch die Klasse `Bootstrap` ersetzt, die Sie als Teil der Anwendung in der Datei `app/Bootstrap.php` finden. Sie könnte zum Beispiel so aussehen: +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 @@ -26,9 +29,9 @@ class Bootstrap public function __construct() { $this->rootDir = dirname(__DIR__); - // Der Configurator ist für die Einstellung der Anwendungsumgebung und der Dienste verantwortlich. + // Der Configurator ist für das Einrichten der Anwendungsumgebung und der Services zuständig. $this->configurator = new Configurator; - // Legt das Verzeichnis für temporäre Dateien fest, die von Nette generiert werden (z. B. kompilierte Templates) + // Verzeichnis für von Nette erzeugte temporäre Dateien festlegen (z. B. kompilierte Templates) $this->configurator->setTempDirectory($this->rootDir . '/temp'); } @@ -41,14 +44,14 @@ class Bootstrap private function initializeEnvironment(): void { - // Nette ist schlau und der Entwicklungsmodus wird automatisch aktiviert, - // oder Sie können ihn für eine bestimmte IP-Adresse aktivieren, indem Sie die folgende Zeile auskommentieren: + // 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" für das Debugging. + // Aktiviert Tracy: das ultimative "Schweizer Taschenmesser" zum Debuggen. $this->configurator->enableTracy($this->rootDir . '/log'); - // RobotLoader: lädt automatisch alle Klassen im ausgewählten Verzeichnis + // RobotLoader: lädt automatisch alle Klassen im gewählten Verzeichnis $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); @@ -56,7 +59,7 @@ class Bootstrap private function setupContainer(): void { - // Lädt Konfigurationsdateien + // Konfigurationsdateien laden $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } @@ -66,69 +69,78 @@ class Bootstrap index.php ========= -Die initiale Datei für Webanwendungen ist `index.php`, die sich im [öffentlichen Verzeichnis |directory-structure#Öffentliches Verzeichnis www] `www/` befindet. Diese lässt die Umgebung von der Bootstrap-Klasse initialisieren und den DI-Container erstellen. Danach holt sie sich den Dienst `Application` daraus, der die Webanwendung startet: +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; -// Initialisierung der Umgebung + Erstellung des DI-Containers +// Umgebung initialisieren + DI-Container erzeugen $container = $bootstrap->bootWebApplication(); -// Der DI-Container erstellt das Objekt Nette\Application\Application +// Der DI-Container erzeugt ein Objekt Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// Start der Nette-Anwendung und Verarbeitung der eingehenden Anfrage +// Die Nette-Anwendung starten und den eingehenden Request verarbeiten $application->run(); ``` -Wie man sieht, hilft die Klasse [api:Nette\Bootstrap\Configurator] bei der Einstellung der Umgebung und der Erstellung des Dependency Injection (DI) Containers, die wir uns nun genauer ansehen werden. +.[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 Produktionsserver läuft: +Nette verhält sich unterschiedlich, je nachdem, ob es auf einem Entwicklungs- oder einem Produktionsserver läuft: -🛠️ Entwicklungsmodus (Development): - - Zeigt die Tracy Debugbar mit nützlichen Informationen an (SQL-Abfragen, Ausführungszeit, verwendeter Speicher) - - Zeigt im Fehlerfall eine detaillierte Fehlerseite mit Funktionsaufrufen und Variableninhalten an - - Erneuert automatisch den Cache bei Änderungen an Latte-Templates, Konfigurationsdateien usw. +🛠️ 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 (Production): - - Zeigt keine Debugging-Informationen an, alle Fehler werden im Log protokolliert - - Zeigt im Fehlerfall den ErrorPresenter oder eine allgemeine "Server Error"-Seite an - - Der Cache wird niemals automatisch erneuert! - - Optimiert für Geschwindigkeit und Sicherheit +🚀 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 Moduswahl erfolgt durch Auto-Detektion, sodass normalerweise nichts konfiguriert oder manuell umgeschaltet werden muss: +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`), wenn kein Proxy vorhanden ist (d. h. dessen HTTP-Header) +- 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 -Wenn wir den Entwicklungsmodus auch in anderen Fällen aktivieren möchten, z. B. für Programmierer, die von einer bestimmten IP-Adresse zugreifen, verwenden wir `setDebugMode()`: +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 kann auch ein Array von IP-Adressen angegeben werden +$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. Wir speichern einen geheimen Token, z. B. `secret1234`, im Cookie `nette-debug` und aktivieren auf diese Weise den Entwicklungsmodus für Programmierer, die von einer bestimmten IP-Adresse zugreifen und gleichzeitig den erwähnten Token im Cookie haben: +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'); ``` -Wir können den Entwicklungsmodus auch vollständig deaktivieren, sogar für Localhost: +Den Entwicklungsmodus können wir auch vollständig abschalten, sogar für localhost: ```php $this->configurator->setDebugMode(false); ``` -Achtung, der Wert `true` schaltet den Entwicklungsmodus fest ein, was auf einem Produktionsserver niemals passieren darf. +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 einfaches Debugging aktivieren wir noch das großartige Werkzeug [Tracy |tracy:]. Im Entwicklungsmodus visualisiert es Fehler und im Produktionsmodus protokolliert es Fehler in das angegebene Verzeichnis: +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'); @@ -138,19 +150,19 @@ $this->configurator->enableTracy($this->rootDir . '/log'); Temporäre Dateien ================= -Nette verwendet einen Cache für den DI-Container, RobotLoader, Templates usw. Daher ist es notwendig, den Pfad zum Verzeichnis festzulegen, in dem der Cache gespeichert wird: +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'); ``` -Unter Linux oder macOS setzen Sie für die Verzeichnisse `log/` und `temp/` [Schreibrechte |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. +Setzen Sie unter Linux oder macOS für die Verzeichnisse `log/` und `temp/` [Schreibrechte |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. RobotLoader =========== -In der Regel möchten wir Klassen automatisch mit dem [RobotLoader |robot-loader:] laden, also müssen wir ihn starten und ihn Klassen aus dem Verzeichnis laden lassen, in dem sich `Bootstrap.php` befindet (d. h. `__DIR__`), sowie aus allen Unterverzeichnissen: +Ü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() @@ -158,13 +170,13 @@ $this->configurator->createRobotLoader() ->register(); ``` -Ein alternativer Ansatz besteht darin, Klassen nur über [Composer |best-practices:composer] unter Einhaltung von PSR-4 laden zu lassen. +Ein alternativer Weg ist, Klassen ausschließlich über [Composer |best-practices:composer] nach PSR-4 zu laden. Zeitzone ======== -Über den Konfigurator können Sie die Standard-Zeitzone einstellen. +Über den Configurator lässt sich die Standard-Zeitzone einstellen. ```php $this->configurator->setTimeZone('Europe/Prague'); @@ -174,20 +186,22 @@ $this->configurator->setTimeZone('Europe/Prague'); Konfiguration des DI-Containers =============================== -Ein Teil des Boot-Prozesses ist die Erstellung des DI-Containers, auch Objektfabrik genannt, der das Herz der gesamten Anwendung ist. Es handelt sich tatsächlich um eine PHP-Klasse, die von Nette generiert und im Cache-Verzeichnis gespeichert wird. Die Fabrik erstellt die Schlüsselobjekte der Anwendung, und mithilfe von Konfigurationsdateien weisen wir sie an, wie sie diese erstellen und einstellen soll, wodurch wir das Verhalten der gesamten Anwendung beeinflussen. +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 normalerweise im [NEON |neon:format]-Format geschrieben. In einem separaten Kapitel erfahren Sie, [was alles konfiguriert werden kann |nette:configuring]. +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 generiert, und Änderungen werden zur Maximierung der Leistung nicht überprüft. +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 laden wir mit `addConfig()`: +Konfigurationsdateien werden mit `addConfig()` geladen: ```php $this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` -Wenn wir mehrere Konfigurationsdateien hinzufügen möchten, können wir die Funktion `addConfig()` mehrmals aufrufen. +Wollen wir weitere Konfigurationsdateien hinzufügen, können wir die Funktion `addConfig()` mehrfach aufrufen. ```php $configDir = $this->rootDir . '/config'; @@ -198,17 +212,17 @@ if (PHP_SAPI === 'cli') { } ``` -Der Name `cli.php` ist kein Tippfehler; die Konfiguration kann auch in einer PHP-Datei geschrieben sein, die sie als Array zurückgibt. +Der Name `cli.php` ist kein Tippfehler; die Konfiguration lässt sich auch in einer PHP-Datei schreiben, die sie als Array zurückgibt. -Wir können auch weitere Konfigurationsdateien im [Abschnitt `includes` |dependency-injection:configuration#Dateien einbinden] hinzufügen. +Weitere Konfigurationsdateien können wir auch [im Abschnitt `includes` |dependency-injection:configuration#Dateien einbinden] hinzufügen. -Wenn in den Konfigurationsdateien Elemente mit denselben Schlüsseln erscheinen, werden sie überschrieben oder im Falle von [Arrays zusammengeführt |dependency-injection:configuration#Zusammenführen]. Die später eingebundene Datei hat eine höhere Priorität als die vorherige. Die Datei, in der der Abschnitt `includes` aufgeführt ist, hat eine höhere Priorität als die darin eingebundenen Dateien. +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, können [im Abschnitt `parameters` |dependency-injection:configuration#Parameter] definiert und auch über die Methode `addStaticParameters()` (hat den Alias `addParameters()`) übergeben (oder überschrieben) werden. Wichtig ist, dass unterschiedliche Werte der Parameter die Generierung zusätzlicher DI-Container, also zusätzlicher Klassen, verursachen. +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([ @@ -216,13 +230,13 @@ $this->configurator->addStaticParameters([ ]); ``` -Auf den Parameter `projectId` kann in der Konfiguration mit der üblichen Schreibweise `%projectId%` verwiesen werden. +Auf den Parameter `projectId` lässt sich in der Konfiguration mit der üblichen Schreibweise `%projectId%` verweisen. Dynamische Parameter -------------------- -Wir können dem Container auch dynamische Parameter hinzufügen, deren unterschiedliche Werte im Gegensatz zu statischen Parametern nicht die Generierung neuer DI-Container verursachen. +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([ @@ -230,7 +244,7 @@ $this->configurator->addDynamicParameters([ ]); ``` -So können wir einfach z. B. Umgebungsvariablen hinzufügen, auf die dann in der Konfiguration mit der Schreibweise `%env.variable%` verwiesen werden kann. +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([ @@ -242,21 +256,22 @@ $this->configurator->addDynamicParameters([ Standardparameter ----------------- -In Konfigurationsdateien können Sie diese statischen Parameter verwenden: +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 Eingabedatei `index.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 dem Composer Bibliotheken installiert -- `%rootDir%` ist der absolute Pfad zum Stammverzeichnis des Projekts -- `%debugMode%` gibt an, ob sich die Anwendung im Debugging-Modus befindet -- `%consoleMode%` gibt an, ob die Anfrage über die Kommandozeile kam +- `%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 Dienste -------------------- +Importierte Services +-------------------- -Jetzt gehen wir tiefer. Obwohl der Zweck des DI-Containers darin besteht, Objekte zu erstellen, kann es ausnahmsweise notwendig sein, ein vorhandenes Objekt in den Container einzufügen. Dies tun wir, indem wir den Dienst mit dem Flag `imported: true` definieren. +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: @@ -274,10 +289,10 @@ $this->configurator->addServices([ ``` -Unterschiedliche Umgebungen -=========================== +Verschiedene Umgebungen +======================= -Scheuen Sie sich nicht, die Bootstrap-Klasse an Ihre Bedürfnisse anzupassen. Sie können der Methode `bootWebApplication()` Parameter hinzufügen, um Webprojekte zu unterscheiden. Oder wir können weitere Methoden hinzufügen, zum Beispiel `bootTestEnvironment()`, die die Umgebung für Unit-Tests initialisiert, `bootConsoleApplication()` für Skripte, die von der Kommandozeile aufgerufen werden, usw. +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 diff --git a/application/de/components.texy b/application/de/components.texy index b39a8e5abd..b9b3713eee 100644 --- a/application/de/components.texy +++ b/application/de/components.texy @@ -3,27 +3,27 @@ Interaktive Komponenten
    -Komponenten sind eigenständige, wiederverwendbare Objekte, die wir in Seiten einfügen. Das können Formulare, Datengrids, Umfragen sein, eigentlich alles, was sinnvoll wiederverwendet werden kann. Wir zeigen Ihnen: +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 sind Signale? +- wie man Komponenten verwendet? +- wie man sie schreibt? +- was Signale sind?
    -Nette verfügt über ein eingebautes Komponentensystem. Etwas Ähnliches kennen Kenner vielleicht noch aus Delphi oder ASP.NET Web Forms, und React oder Vue.js bauen auf etwas entfernt Ähnlichem auf. In der Welt der PHP-Frameworks ist dies jedoch eine einzigartige Angelegenheit. +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. -Dabei beeinflussen Komponenten den Ansatz zur Anwendungsentwicklung grundlegend. Sie können Seiten aus vorgefertigten Einheiten zusammenstellen. Benötigen Sie ein Datagrid in der Administration? Sie finden es auf [Componette |https://componette.org/search/component], einem Repository für Open-Source-Add-ons (also nicht nur Komponenten) für Nette, und fügen es einfach in den Presenter ein. +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 einen Presenter integrieren. Und in einige Komponenten können Sie weitere Komponenten einfügen. So entsteht ein Komponentenbaum, dessen Wurzel der Presenter ist. +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? Normalerweise über Factory-Methoden. +Wie werden Komponenten in den Presenter eingefügt und anschließend verwendet? Üblicherweise über Factory-Methoden. -Eine Komponenten-Factory stellt eine elegante Möglichkeit dar, Komponenten erst dann zu erstellen, wenn sie tatsächlich benötigt werden (lazy / on demand). Der ganze Zauber besteht darin, eine Methode mit dem Namen `createComponent()` zu implementieren, wobei `` der Name der zu erstellenden Komponente ist, und die die Komponente erstellt und zurückgibt. +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 @@ -31,49 +31,54 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } ``` -Dadurch, dass alle Komponenten in separaten Methoden erstellt werden, gewinnt der Code an Übersichtlichkeit. +Weil alle Komponenten in eigenen Methoden erzeugt werden, wird der Code übersichtlicher. .[note] -Komponentennamen beginnen immer mit einem Kleinbuchstaben, obwohl sie im Methodennamen großgeschrieben werden. +Namen von Komponenten beginnen immer mit einem Kleinbuchstaben, obwohl sie im Methodennamen großgeschrieben werden. -Factories werden niemals direkt aufgerufen, sie rufen sich selbst auf, wenn die Komponente zum ersten Mal verwendet wird. Dadurch wird die Komponente zum richtigen Zeitpunkt erstellt und nur dann, wenn sie tatsächlich benötigt wird. Wenn wir die Komponente nicht verwenden (z. B. bei einer AJAX-Anfrage, bei der nur ein Teil der Seite übertragen wird, oder beim Caching des Templates), wird sie überhaupt nicht erstellt und wir sparen Serverleistung. +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 erstellt +// wir greifen auf die Komponente zu, und wenn es das erste Mal war, +// wird createComponentPoll() aufgerufen, die sie erzeugt $poll = $this->getComponent('poll'); -// alternative Syntax: $poll = $this['poll']; +// alternative Schreibweise: $poll = $this['poll']; ``` -Im Template kann eine Komponente mit dem Tag [{control} |#Rendern] gerendert werden. Es ist daher nicht notwendig, Komponenten manuell an das Template zu übergeben. +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 -

    Stimmen Sie ab

    +

    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 verwenden üblicherweise eine frische Technik, die wir gerne Hollywood Style nennen. Sie kennen sicher den geflügelten Satz, den Teilnehmer von Filmcastings so oft hören: „Rufen Sie uns nicht an, wir rufen Sie an“. Und genau darum geht es. +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 sagen Sie dem Framework nämlich, anstatt ständig nachfragen zu müssen („wurde das Formular abgeschickt?“, „war es gültig?“ oder „hat der Benutzer diesen Button gedrückt?“), „wenn das passiert, ruf diese Methode auf“ und überlassen die weitere Arbeit ihm. Wenn Sie in JavaScript programmieren, kennen Sie diesen Programmierstil genau. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Ereignis eintritt. Und die Sprache übergibt ihnen die entsprechenden Parameter. +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. -Dies verändert die Sichtweise auf das Schreiben von Anwendungen grundlegend. Je mehr Aufgaben Sie dem Framework überlassen können, desto weniger Arbeit haben Sie. Und desto weniger können Sie vielleicht übersehen. +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. -Wir schreiben eine Komponente -============================= +Eine Komponente schreiben +========================= -Unter dem Begriff Komponente verstehen wir normalerweise einen Nachfahren der Klasse [api:Nette\Application\UI\Control]. (Genauer wäre es also, den Begriff „Controls“ zu verwenden, aber „Kontrollen“ hat im Deutschen eine ganz andere Bedeutung, und eher haben sich „Komponenten“ durchgesetzt.) Der Presenter [api:Nette\Application\UI\Presenter] selbst ist übrigens auch ein Nachfahre der Klasse `Control`. +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; @@ -84,22 +89,22 @@ class PollControl extends Control ``` -Rendern -======= +Rendering +========= -Wir wissen bereits, dass zum Rendern einer Komponente der Tag `{control componentName}` verwendet wird. Dieser ruft eigentlich die Methode `render()` der Komponente auf, in der wir uns um das Rendern kümmern. Uns steht, genau wie im Presenter, eine [Latte-Vorlage|templates] in der Variablen `$this->template` zur Verfügung, an die wir Parameter übergeben. Im Gegensatz zum Presenter müssen wir die Template-Datei angeben und sie rendern lassen: +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 { - // Wir fügen einige Parameter in das Template ein + // einige Parameter in das Template einfügen $this->template->param = $value; - // und rendern es + // und es rendern $this->template->render(__DIR__ . '/poll.latte'); } ``` -Der `{control}`-Tag ermöglicht es, Parameter an die `render()`-Methode zu übergeben: +Der Tag `{control}` erlaubt es, der Methode `render()` Parameter zu übergeben: ```latte {control poll $id, $message} @@ -112,7 +117,7 @@ public function render(int $id, string $message): void } ``` -Manchmal kann eine Komponente aus mehreren Teilen bestehen, die wir getrennt rendern möchten. Für jeden davon erstellen wir eine eigene Rendering-Methode, hier im Beispiel etwa `renderPaginator()`: +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 @@ -121,69 +126,69 @@ public function renderPaginator(): void } ``` -Und im Template rufen wir sie dann auf mit: +Und im Template rufen wir sie dann so auf: ```latte {control poll:paginator} ``` -Zum besseren Verständnis ist es gut zu wissen, wie dieser Tag in PHP übersetzt wird. +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'} ``` -wird übersetzt als: +übersetzt sich zu: ```php $control->getComponent('poll')->render(); $control->getComponent('poll')->renderPaginator(123, 'hello'); ``` -Die Methode `getComponent()` gibt die Komponente `poll` zurück und ruft für diese Komponente die Methode `render()` bzw. `renderPaginator()` auf, wenn im Tag nach dem Doppelpunkt eine andere Rendering-Art angegeben ist. +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, wenn irgendwo in den Parametern **`=>`** vorkommt, werden alle Parameter in ein Array verpackt und als erstes Argument übergeben: +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'} ``` -wird übersetzt als: +übersetzt sich zu: ```php $control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); ``` -Rendern einer Sub-Komponente: +Rendering einer Unterkomponente: ```latte {control cartControl-someForm} ``` -wird übersetzt als: +übersetzt sich zu: ```php $control->getComponent("cartControl-someForm")->render(); ``` -Komponenten, ebenso wie Presenter, übergeben automatisch einige nützliche Variablen an die Templates: +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 das Objekt, das [den Benutzer repräsentiert |security:authentication] +- `$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 [Nachrichten |#Flash-Nachrichten], die mit der Funktion `flashMessage()` gesendet wurden +- `$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 von `Presenter:action` besteht. Aber was ist, wenn wir nur eine Aktion auf der **aktuellen Seite** durchführen wollen? Zum Beispiel die Sortierreihenfolge von Spalten in einer Tabelle ändern; einen Eintrag löschen; den Hell-/Dunkelmodus umschalten; ein Formular absenden; in einer Umfrage abstimmen; usw. +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 Anfragen wird als Signale bezeichnet. Und ähnlich wie Aktionen die Methoden `action()` oder `render()` aufrufen, rufen Signale die Methoden `handle()` auf. Während der Begriff Aktion (oder View) rein mit Presentern zusammenhängt, betreffen Signale alle Komponenten. Und somit auch Presenter, da `UI\Presenter` ein Nachfahre von `UI\Control` ist. +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 @@ -192,36 +197,36 @@ public function handleClick(int $x, int $y): void } ``` -Einen Link, der ein Signal aufruft, erstellen wir auf die übliche Weise, d. h. im Template mit dem Attribut `n:href` oder dem Tag `{link}`, im Code mit der Methode `link()`. Mehr dazu im Kapitel [Erstellen von URL-Links |creating-links#Links zu Signalen]. +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 +hier klicken ``` -Ein Signal wird immer im aktuellen Presenter und der aktuellen Action aufgerufen, es kann nicht in einem anderen Presenter oder einer anderen Action ausgelöst werden. +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 das Neuladen der Seite genau wie bei der ursprünglichen Anfrage, ruft aber zusätzlich die Signal-Handler-Methode mit den entsprechenden Parametern auf. Wenn die Methode nicht existiert, wird eine Ausnahme [api:Nette\Application\UI\BadSignalException] ausgelöst, die dem Benutzer als Fehlerseite 403 Forbidden angezeigt wird. +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 mittels AJAX aufgerufen, und anschließend übertragen wir nur die geänderten Teile der Seite an den Browser. Also sogenannte Snippets. Weitere Informationen finden Sie auf der [AJAX gewidmeten Seite |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-Nachrichten -================= +Flash-Meldungen +=============== -Eine Komponente hat ihren eigenen Speicher für Flash-Nachrichten, unabhängig vom Presenter. Dies sind Nachrichten, die z. B. über das Ergebnis einer Operation informieren. Ein wichtiges Merkmal von Flash-Nachrichten ist, dass sie auch nach einer Weiterleitung im Template verfügbar sind. Auch nach der Anzeige bleiben sie weitere 30 Sekunden aktiv – zum Beispiel für den Fall, dass der Benutzer aufgrund einer fehlerhaften Übertragung die Seite neu lädt – die Nachricht verschwindet also nicht sofort. +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 Nachrichtentext oder ein `stdClass`-Objekt, das die Nachricht repräsentiert. Der optionale zweite Parameter ist ihr Typ (error, warning, info usw.). Die Methode `flashMessage()` gibt eine Instanz der Flash-Nachricht als `stdClass`-Objekt zurück, dem weitere Informationen hinzugefügt werden können. +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('Der Eintrag wurde gelöscht.'); -$this->redirect(/* ... */); // und wir leiten weiter +$this->flashMessage('Das Element wurde gelöscht.'); +$this->redirect(/* ... */); // und weiterleiten ``` -Dem Template stehen diese Nachrichten in der Variablen `$flashes` als `stdClass`-Objekte zur Verfügung, die die Eigenschaften `message` (Nachrichtentext), `type` (Nachrichtentyp) enthalten und die bereits erwähnten Benutzerinformationen enthalten können. Wir rendern sie zum Beispiel so: +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} @@ -230,36 +235,36 @@ Dem Template stehen diese Nachrichten in der Variablen `$flashes` als `stdClass` ``` -Weiterleitung nach Signal -========================= +Weiterleitung nach Verarbeitung eines Signals +============================================= -Nach der Verarbeitung eines Komponentensignals folgt oft eine Weiterleitung. Dies ist eine ähnliche Situation wie bei Formularen – nach dem Absenden leiten wir ebenfalls weiter, damit beim Neuladen der Seite im Browser die Daten nicht erneut gesendet werden. +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 zum aktuellen Presenter und zur aktuellen Action weiter +$this->redirect('this'); // leitet auf den aktuellen Presenter und die aktuelle Aktion weiter ``` -Da eine Komponente ein wiederverwendbares Element ist und normalerweise keine direkte Bindung an bestimmte Presenter haben sollte, interpretieren die Methoden `redirect()` und `link()` den Parameter automatisch als Signal der Komponente: +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 zum Signal 'click' derselben Komponente weiter +$this->redirect('click'); // leitet auf das Signal 'click' derselben Komponente weiter ``` -Wenn Sie zu einem anderen Presenter oder einer anderen Aktion weiterleiten müssen, können Sie dies über den Presenter tun: +Müssen Sie auf einen anderen Presenter oder eine andere Aktion weiterleiten, geht das über den Presenter: ```php -$this->getPresenter()->redirect('Product:show'); // leitet zu einem anderen Presenter/Action weiter +$this->getPresenter()->redirect('Product:show'); // leitet auf einen anderen Presenter/eine andere Aktion weiter ``` Persistente Parameter ===================== -Persistente Parameter dienen dazu, den Zustand in Komponenten über verschiedene Anfragen hinweg zu erhalten. Ihr Wert bleibt auch nach dem Klick auf einen Link gleich. Im Gegensatz zu Daten in der Session werden sie in der URL übertragen. Und das vollautomatisch, einschließlich Links, die in anderen Komponenten auf derselben Seite erstellt wurden. +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 z. B. eine Komponente für die Paginierung von Inhalten. Solche Komponenten können auf einer Seite mehrmals vorkommen. Und wir möchten, dass nach dem Klick auf einen Link alle Komponenten auf ihrer aktuellen Seite bleiben. Deshalb machen wir die Seitenzahl (`page`) zu einem persistenten Parameter. +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. -Die Erstellung eines persistenten Parameters ist in Nette äußerst einfach. Es genügt, eine öffentliche Eigenschaft zu erstellen und sie mit einem Attribut zu kennzeichnen: (früher wurde `/** @persistent */` verwendet) +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 @@ -271,15 +276,15 @@ class PaginatingControl extends Control } ``` -Für die Eigenschaft empfehlen wir, auch den Datentyp anzugeben (z. B. `int`), und Sie können auch einen Standardwert angeben. Die Werte der Parameter können [validiert |#Validierung persistenter Parameter] werden. +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 kann der Wert des persistenten Parameters geändert werden: +Beim Erstellen eines Links lässt sich der Wert eines persistenten Parameters ändern: ```latte weiter ``` -Oder er kann *zurückgesetzt* werden, d. h. aus der URL entfernt werden. Dann nimmt er seinen Standardwert an: +Oder er lässt sich *zurücksetzen*, also aus der URL entfernen. Er nimmt dann seinen Standardwert an: ```latte zurücksetzen @@ -289,25 +294,25 @@ Oder er kann *zurückgesetzt* werden, d. h. aus der URL entfernt werden. Dann ni Persistente Komponenten ======================= -Nicht nur Parameter, sondern auch Komponenten können persistent sein. Bei einer solchen Komponente werden ihre persistenten Parameter auch zwischen verschiedenen Aktionen des Presenters oder zwischen mehreren Presentern übertragen. Persistente Komponenten kennzeichnen wir mit einer Annotation in der Presenter-Klasse. So kennzeichnen wir beispielsweise die Komponenten `calendar` und `poll`: +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 -/** - * @persistent(calendar, poll) - */ +use Nette\Application\Attributes\Persistent; + +#[Persistent('calendar', 'poll')] class DefaultPresenter extends Nette\Application\UI\Presenter { } ``` -Sub-Komponenten innerhalb dieser Komponenten müssen nicht gekennzeichnet werden, sie werden ebenfalls persistent. +Unterkomponenten innerhalb dieser Komponenten müssen nicht gekennzeichnet werden; auch sie werden persistent. -In PHP 8 können Sie zur Kennzeichnung persistenter Komponenten auch Attribute verwenden: +Die ältere Annotation `@persistent` funktioniert weiterhin, ist aber veraltet und löst eine Warnung aus: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -317,9 +322,9 @@ 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 „verschmutzen“? Dank der intelligenten Eigenschaften des DI-Containers in Nette kann man, wie bei der Verwendung klassischer Dienste, den größten Teil der Arbeit dem Framework überlassen. +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 Dienst `PollFacade` hat: +Nehmen wir als Beispiel eine Komponente, die eine Abhängigkeit vom Service `PollFacade` hat: ```php class PollControl extends Control @@ -332,17 +337,17 @@ class PollControl extends Control public function handleVote(int $voteId): void { - $this->facade->vote($id, $voteId); + $this->facade->vote($this->id, $voteId); // ... } } ``` -Wenn wir einen klassischen Dienst schreiben würden, gäbe es nichts zu lösen. Der DI-Container würde sich unsichtbar um die Übergabe aller Abhängigkeiten kümmern. Aber mit Komponenten gehen wir normalerweise so um, dass wir ihre neue Instanz direkt im Presenter in den [#Factory-Methoden] `createComponent…()` erstellen. Aber alle Abhängigkeiten aller Komponenten an den Presenter zu übergeben, nur um sie dann an die Komponenten weiterzugeben, ist umständlich. Und der viele geschriebene Code… +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 ist, warum registrieren wir die Komponente nicht einfach als klassischen Dienst, übergeben sie an den Presenter und geben sie dann in der Methode `createComponent…()` zurück? Dieser Ansatz ist jedoch ungeeignet, da wir die Komponente möglicherweise mehrmals erstellen möchten. +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, eine Factory für die Komponente zu schreiben, also eine Klasse, die uns die Komponente erstellt: +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 @@ -359,7 +364,7 @@ class PollControlFactory } ``` -Diese Factory registrieren wir in unserem Container in der Konfiguration: +Diese Factory registrieren wir in der Konfiguration in unserem Container: ```neon services: @@ -384,7 +389,7 @@ class PollPresenter extends Nette\Application\UI\Presenter } ``` -Das Tolle ist, dass Nette DI solche einfachen Factories [generieren |dependency-injection:factory] kann, sodass statt des gesamten Codes nur ihr Interface geschrieben werden muss: +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 @@ -393,21 +398,21 @@ interface PollControlFactory } ``` -Und das ist alles. Nette implementiert dieses Interface intern und übergibt es an den Presenter, wo wir es bereits verwenden können. Es fügt magischerweise auch den Parameter `$id` und eine Instanz der Klasse `PollFacade` zu unserer Komponente hinzu. +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 in Nette Application stellen wiederverwendbare Teile einer Webanwendung dar, die wir in Seiten einfügen und denen dieses ganze Kapitel gewidmet ist. Welche Fähigkeiten hat eine solche Komponente genau? +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 im Template renderbar. -2) Sie weiß, [welchen Teil |ajax#Snippets] sie bei einer AJAX-Anfrage rendern soll (Snippets). -3) Sie hat die Fähigkeit, ihren Zustand in der URL zu speichern (persistente Parameter). -4) Sie hat die Fähigkeit, auf Benutzeraktionen zu reagieren (Signale). -5) Sie bildet eine hierarchische Struktur (deren Wurzel der Presenter ist). +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 wird von einer der Klassen der Vererbungslinie übernommen. Das Rendern (1 + 2) übernimmt [api:Nette\Application\UI\Control], die Einbindung in den [Lebenszyklus |presenters#Lebenszyklus des Presenters] (3, 4) die Klasse [api:Nette\Application\UI\Component] und die Erstellung der hierarchischen Struktur (5) die Klassen [Container und Component |component-model:]. +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 } @@ -431,9 +436,9 @@ Lebenszyklus einer Komponente Validierung persistenter Parameter ---------------------------------- -Die Werte der [persistenten Parameter |#Persistente Parameter], die aus der URL empfangen wurden, schreibt die Methode `loadState()` in die Eigenschaften. Sie prüft auch, ob der bei der Eigenschaft angegebene Datentyp übereinstimmt, andernfalls antwortet sie mit einem 404-Fehler und die Seite wird nicht angezeigt. +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 niemals blind persistenten Parametern, da sie vom Benutzer leicht in der URL überschrieben werden können. So überprüfen wir beispielsweise, ob die Seitenzahl `$this->page` größer als 0 ist. Ein geeigneter Weg ist, die erwähnte Methode `loadState()` zu überschreiben: +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 @@ -452,27 +457,41 @@ class PaginatingControl extends Control } ``` -Der umgekehrte Prozess, also das Sammeln von Werten aus persistenten Eigenschaften, wird von der Methode `saveState()` übernommen. +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 das Neuladen der Seite genau wie bei der ursprünglichen Anfrage (außer wenn es per AJAX aufgerufen wird) und ruft die Methode `signalReceived($signal)` auf, deren Standardimplementierung in der Klasse `Nette\Application\UI\Component` versucht, eine Methode aufzurufen, die aus den Wörtern `handle{signal}` zusammengesetzt ist. Die weitere Verarbeitung liegt beim jeweiligen Objekt. Objekte, die von `Component` erben (d. h. `Control` und `Presenter`), reagieren, indem sie versuchen, die Methode `handle{signal}` mit den entsprechenden Parametern aufzurufen. +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]. -Mit anderen Worten: Es wird die Definition der Funktion `handle{signal}` und alle mit der Anfrage übermittelten Parameter genommen, und den Argumenten werden anhand des Namens Parameter aus der URL zugewiesen, und es wird versucht, die betreffende Methode aufzurufen. Z. B. wird als Parameter `$id` der Wert des Parameters `id` aus der URL übergeben, als `$something` wird `something` aus der URL übergeben, usw. Und wenn die Methode nicht existiert, löst die Methode `signalReceived` eine [Ausnahme |api:Nette\Application\UI\BadSignalException] aus. +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 von jeder Komponente, jedem Presenter oder jedem Objekt empfangen werden, das das Interface `SignalReceiver` implementiert und in den Komponentenbaum eingebunden ist. +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 sind `Presenter` und visuelle Komponenten, die von `Control` erben. Ein Signal soll als Zeichen für ein Objekt dienen, dass es etwas tun soll – die Umfrage soll eine Stimme vom Benutzer zählen, der Nachrichtenblock soll sich aufklappen und doppelt so viele Nachrichten anzeigen, das Formular wurde abgeschickt und soll die Daten verarbeiten und so weiter. +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 erstellen wir mit der Methode [Component::link() |api:Nette\Application\UI\Component::link()]. Als Parameter `$destination` übergeben wir den String `{signal}!` und als `$args` ein Array von Argumenten, die wir dem Signal übergeben möchten. Das Signal wird immer im aktuellen Presenter und der aktuellen Action mit den aktuellen Parametern aufgerufen, die Signalparameter werden nur hinzugefügt. Zusätzlich wird ganz am Anfang der **Parameter `?do`, der das Signal bestimmt**, hinzugefügt. +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 ist entweder `{signal}` oder `{signalReceiver}-{signal}`. `{signalReceiver}` ist der Name der Komponente im Presenter. Deshalb darf im Komponentennamen kein Bindestrich vorkommen – er wird zur Trennung von Komponentennamen und Signal verwendet, es ist jedoch möglich, mehrere Komponenten auf diese Weise zu verschachteln. +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 eine Komponente (erstes Argument) der Empfänger eines Signals ist (zweites Argument). Das zweite Argument kann weggelassen werden – dann wird geprüft, ob die Komponente Empfänger irgendeines Signals ist. Als zweites Parameter kann `true` angegeben werden, um zu prüfen, ob nicht nur die angegebene Komponente, sondern auch einer ihrer Nachfahren Empfänger ist. +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{signal}` können wir das Signal manuell ausführen, indem wir die Methode [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] aufrufen, die die Bearbeitung des Signals übernimmt – sie nimmt die Komponente, die als Signalempfänger bestimmt wurde (wenn kein Signalempfänger bestimmt ist, ist es der Presenter selbst) und sendet ihr das Signal. +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: @@ -482,4 +501,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -Damit wird das Signal vorzeitig ausgeführt und nicht mehr erneut aufgerufen. +Dadurch wird das Signal vorzeitig ausgeführt und nicht erneut aufgerufen. diff --git a/application/de/configuration.texy b/application/de/configuration.texy index 638fba748b..bc2f529a4c 100644 --- a/application/de/configuration.texy +++ b/application/de/configuration.texy @@ -1,8 +1,8 @@ -Konfiguration von Anwendungen -***************************** +Konfiguration der Anwendung +*************************** .[perex] -Übersicht über die Konfigurationsoptionen für Nette-Anwendungen. +Übersicht der Konfigurationsoptionen für Nette Application. Application @@ -10,12 +10,12 @@ Application ```neon application: - # Das "Nette Application"-Panel im Tracy BlueScreen anzeigen? - debugger: ... # (bool) Standard ist true + # das Panel "Nette Application" im Tracy BlueScreen anzeigen? + debugger: ... # (bool) aktiv, wenn Tracy verfügbar ist - # Soll bei einem Fehler der Error-Presenter aufgerufen werden? - # wirkt sich nur im Entwicklungsmodus aus - catchExceptions: ... # (bool) Standard ist true + # 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' @@ -23,49 +23,51 @@ application: # definiert Aliase für Presenter und Aktionen aliases: ... - # definiert Regeln für die Übersetzung des Presenter-Namens in eine Klasse + # definiert die Regeln für die Übersetzung des Presenter-Namens in eine Klasse mapping: ... - # Erzeugen fehlerhafte Links keine Warnungen? - # wirkt sich nur im Entwicklungsmodus aus + # Warnungen für ungültige Links unterdrücken? + # wirkt nur im Entwicklungsmodus silentLinks: ... # (bool) Standard ist false ``` -Seit `nette/application` Version 3.2 kann ein Paar von Error-Presentern definiert werden: +Seit `nette/application` Version 3.2 lässt sich ein Paar von Error-Presentern definieren: ```neon application: errorPresenter: - 4xx: Error4xx # für die Ausnahme Nette\Application\BadRequestException - 5xx: Error5xx # für andere Ausnahmen + 4xx: Error4xx # für Nette\Application\BadRequestException + 5xx: Error5xx # für übrige Exceptions ``` -Die Option `silentLinks` bestimmt, wie sich Nette im Entwicklungsmodus verhält, wenn die Linkgenerierung fehlschlägt (z. B. weil der Presenter nicht existiert usw.). Der Standardwert `false` bedeutet, dass Nette einen `E_USER_WARNING`-Fehler auslöst. Durch Setzen auf `true` wird diese Fehlermeldung unterdrückt. In der Produktionsumgebung wird `E_USER_WARNING` immer ausgelöst. Dieses Verhalten kann auch durch Setzen der Presenter-Variable [$invalidLinkMode |creating-links#Ungültige Links] beeinflusst werden. +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. -[Aliase vereinfachen das Verlinken |creating-links#Aliase] zu häufig verwendeten Presentern. +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. -[Mapping definiert Regeln |directory-structure#Presenter-Mapping], nach denen der Klassenname vom Presenter-Namen abgeleitet wird. +[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 Dienste zum DI-Container hinzu, was deren Erstellung erheblich beschleunigt. Wie Nette Presenter findet, kann konfiguriert werden: +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? + # 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 sollen Presenter gesucht werden? + # in welchen Verzeichnissen nach Presentern suchen? scanDirs: # (string[]|false) Standard ist '%appDir%' - %vendorDir%/mymodule ``` -Die in `scanDirs` angegebenen Verzeichnisse überschreiben nicht den Standardwert `%appDir%`, sondern ergänzen ihn, sodass `scanDirs` beide Pfade `%appDir%` und `%vendorDir%/mymodule` enthält. Wenn wir das Standardverzeichnis auslassen möchten, verwenden wir ein [Ausrufezeichen |dependency-injection:configuration#Zusammenführen], das den Wert überschreibt: +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: @@ -73,36 +75,42 @@ application: - %vendorDir%/mymodule ``` -Das Scannen von Verzeichnissen kann durch Angabe des Wertes false deaktiviert werden. Wir empfehlen nicht, das automatische Hinzufügen von Presentern vollständig zu unterdrücken, da dies die Leistung der Anwendung beeinträchtigen würde. +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 =============== -Mit dieser Einstellung kann das Verhalten von Latte in Komponenten und Presentern global beeinflusst werden. +Diese Einstellung beeinflusst global das Verhalten von Latte in Komponenten und Presentern. ```neon latte: - # Latte-Panel in der Tracy Bar für das Haupttemplate (true) oder alle Komponenten (all) anzeigen? - debugger: ... # (true|false|'all') Standard ist true + # 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) - # generiert Templates mit dem Header declare(strict_types=1) + # Templates mit dem Header declare(strict_types=1) erzeugen strictTypes: ... # (bool) Standard ist false - # schaltet den [strengen Parser-Modus |latte:develop#strict-mode] ein + # [strikten Parser-Modus |latte:develop#strict mode] aktivieren strictParsing: ... # (bool) Standard ist false - # aktiviert die [Überprüfung des generierten Codes |latte:develop#checking-generated-code] + # 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 die Locale - locale: cs_CZ # (string) Standard ist null + # setzt das Locale + locale: de_DE # (string) Standard ist null - # Klasse des $this->template-Objekts + # Klasse des Objekts $this->template templateClass: App\MyTemplateClass # Standard ist Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Wenn Sie Latte Version 3 verwenden, können Sie neue [Erweiterungen |latte:extending-latte#Latte Extension] hinzufügen mit: +Neue [Extensions |latte:extending-latte#Latte Extension] fügen Sie so hinzu: ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Wenn Sie Latte Version 2 verwenden, können Sie neue Tags (Makros) registrieren, indem Sie entweder den Klassennamen oder eine Referenz auf einen Dienst angeben. Standardmäßig wird die Methode `install()` aufgerufen, dies kann jedoch geändert werden, indem ein anderer Methodenname angegeben wird: - -```neon -latte: - # Registrierung benutzerdefinierter Latte-Tags - macros: - - App\MyLatteMacros::register # statische Methode, Klassenname oder Callable - - @App\MyLatteMacrosFactory # Dienst mit install()-Methode - - @App\MyLatteMacrosFactory::register # Dienst mit register()-Methode - -services: - - App\MyLatteMacrosFactory -``` - Routing ======= @@ -132,14 +126,14 @@ Grundeinstellungen: ```neon routing: - # Routing-Panel in der Tracy Bar anzeigen? - debugger: ... # (bool) Standard ist true + # das Routing-Panel in der Tracy Bar anzeigen? + debugger: ... # (bool) aktiv, wenn Tracy verfügbar ist (nur im Debug-Modus) - # serialisiert den Router in den DI-Container + # den Router in den DI-Container serialisieren cache: ... # (bool) Standard ist false ``` -Das Routing wird normalerweise in der Klasse [RouterFactory |routing#Routen-Sammlung] definiert. Alternativ können Routen auch in der Konfiguration über Paare `Maske: Aktion` definiert werden, aber diese Methode bietet nicht so viel Flexibilität bei den Einstellungen: +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: @@ -152,23 +146,23 @@ routing: Konstanten ========== -Erstellen von PHP-Konstanten. +Erzeugung von PHP-Konstanten. ```neon constants: Foobar: 'baz' ``` -Nach dem Start der Anwendung wird die Konstante `Foobar` erstellt. +Die Konstante `Foobar` wird nach dem Start der Anwendung erzeugt. .[note] -Konstanten sollten nicht als global verfügbare Variablen dienen. Verwenden Sie [Dependency Injection |dependency-injection:passing-dependencies], um Werte an Objekte zu übergeben. +Konstanten sollten nicht als global verfügbare Variablen dienen. Verwenden Sie zur Übergabe von Werten an Objekte [Dependency Injection |dependency-injection:passing-dependencies]. PHP === -Einstellen von PHP-Direktiven. Eine Übersicht über alle Direktiven finden Sie auf [php.net |https://www.php.net/manual/en/ini.list.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: @@ -176,16 +170,17 @@ php: ``` -DI-Dienste -========== - -Diese Dienste werden dem DI-Container hinzugefügt: +DI-Services +=========== -| Name | Typ | Beschreibung -|---------------------------------------------------------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [Starter der gesamten Anwendung |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | Factory für Presenter -| `application.###` | [api:Nette\Application\UI\Presenter] | einzelne Presenter -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | Factory für das `Latte\Engine`-Objekt -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | Factory für [`$this->template` |templates] +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 index ff63ee2419..66821107bd 100644 --- a/application/de/creating-links.texy +++ b/application/de/creating-links.texy @@ -3,56 +3,56 @@ Erstellen von URL-Links
    -Das Erstellen von Links in Nette 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 Ihnen: +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 zur aktuellen Seite unterscheidet -- was mit ungültigen Links zu tun ist +- wie man einen Link auf die aktuelle Seite erkennt +- was man mit ungültigen Links macht
    -Dank [bidirektionalem Routing |routing] müssen Sie niemals fest codierte URL-Adressen Ihrer Anwendung in Templates oder Code schreiben, die sich später ändern könnten, oder sie kompliziert zusammensetzen. Im Link genügt es, den Presenter und die Aktion anzugeben, eventuelle Parameter zu übergeben, und das Framework generiert die URL selbst. Eigentlich ist es sehr ähnlich wie der Aufruf einer Funktion. Das wird Ihnen gefallen. +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 Presenter-Template -===================== +Im Template des Presenters +========================== -Am häufigsten erstellen wir Links in Templates, und ein großartiger Helfer ist das Attribut `n:href`: +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 anstelle des HTML-Attributs `href` das [n:Attribut |latte:syntax#n:Attribute] `n:href` verwendet haben. Sein Wert ist dann nicht eine URL, wie es beim `href`-Attribut der Fall wäre, sondern der Name des Presenters und der Aktion. +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. -Ein Klick auf den Link ist, vereinfacht gesagt, so etwas wie der Aufruf der Methode `ProductPresenter::renderShow()`. Und wenn diese Parameter in ihrer Signatur hat, können wir sie mit Argumenten aufrufen: +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 ist auch möglich, benannte Parameter zu übergeben. Der folgende Link übergibt den Parameter `lang` mit dem Wert `cs`: +Es lassen sich auch benannte Parameter übergeben. Der folgende Link übergibt den Parameter `lang` mit dem Wert `en`: ```latte -Produktdetail +Produktdetail ``` -Wenn die Methode `ProductPresenter::renderShow()` `$lang` nicht in ihrer Signatur hat, kann sie den Wert des Parameters mit `$lang = $this->getParameter('lang')` oder aus der [Property |presenters#Anfrageparameter] ermitteln. +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. -Wenn die Parameter in einem Array gespeichert sind, können sie mit dem Operator `...` (in Latte 2.x mit dem Operator `(expand)`) erweitert werden: +Sind die Parameter in einem Array gespeichert, lassen sie sich mit dem Operator `...` entpacken: ```latte -{var $args = [$product->id, lang => cs]} -Produktdetail +{var $args = [$product->id, lang => en]} +Produktdetail ``` -In Links werden auch automatisch sogenannte [persistente Parameter |presenters#Persistente Parameter] übergeben. +In Links werden automatisch auch die sogenannten [persistenten Parameter |presenters#Persistente Parameter] übergeben. -Das Attribut `n:href` ist sehr praktisch für HTML-Tags ``. Wenn wir einen Link an anderer Stelle ausgeben möchten, zum Beispiel im Text, verwenden wir `{link}`: +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 Adresse lautet: {link Home:default} +Die URL lautet: {link Home:default} ``` @@ -65,92 +65,109 @@ Zum Erstellen eines Links im Presenter dient die Methode `link()`: $url = $this->link('Product:show', $product->id); ``` -Parameter können auch über ein Array übergeben werden, wo auch benannte Parameter angegeben werden können: +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' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -Links können auch ohne Presenter erstellt werden, dafür gibt es den [#LinkGenerator] und seine Methode `link()`. +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: -Links zu Presentern +```php +$link = $this->lazyLink('Product:show', $id); +// ... +echo $link; // erst hier wird die URL erzeugt +``` + + +Links auf Presenter =================== -Wenn das Ziel des Links ein Presenter und eine Aktion ist, hat er diese Syntax: +Ist das Ziel des Links ein Presenter und eine Aktion, hat er diese Syntax: ``` [//] [[[[:]module:]presenter:]action | this] [#fragment] ``` -Das Format wird von allen Latte-Tags und allen Presenter-Methoden unterstützt, die mit Links arbeiten, also `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` und auch dem [#LinkGenerator]. Auch wenn in den Beispielen `n:href` verwendet wird, könnte dort jede dieser Funktionen stehen. +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 also `Presenter:action`: +Die Grundform ist demnach `Presenter:action`: ```latte Startseite ``` -Wenn wir auf eine Aktion des aktuellen Presenters verlinken, können wir seinen Namen weglassen: +Verlinken wir auf eine Aktion des aktuellen Presenters, können wir dessen Namen weglassen: ```latte Startseite ``` -Wenn das Ziel die Aktion `default` ist, können wir sie weglassen, aber der Doppelpunkt muss bleiben: +Ist die Zielaktion `default`, können wir sie weglassen, der Doppelpunkt muss aber bleiben: ```latte Startseite ``` -Links können auch zu anderen [Modulen |directory-structure#Presenter und Templates] führen. Hier werden Links in relative zu verschachtelten Submodulen oder absolute unterschieden. Das Prinzip ist analog zu Pfaden auf der Festplatte, nur dass anstelle von Schrägstrichen Doppelpunkte verwendet werden. Angenommen, der aktuelle Presenter ist Teil des Moduls `Front`, dann schreiben wir: +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 zu Front:Shop:Product:show -Link zu Admin:Product:show +Link auf Front:Shop:Product:show +Link auf Admin:Product:show ``` -Ein Sonderfall ist der Link [auf sich selbst |#Link zur aktuellen Seite], bei dem wir als Ziel `this` angeben. +Ein Sonderfall ist ein Link [auf sich selbst |#Link auf die aktuelle Seite], bei dem wir als Ziel `this` angeben. ```latte aktualisieren ``` -Wir können auf einen bestimmten Teil der Seite über das sogenannte Fragment nach dem Rautezeichen `#` verlinken: +Auf einen bestimmten Teil der Seite können wir über ein sogenanntes Fragment nach dem Rautezeichen `#` verlinken: ```latte -Link zu Home:default und Fragment #main +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 ============== -Links, die mit `link()` oder `n:href` generiert werden, sind immer absolute Pfade (d. h. sie beginnen mit einem `/`), aber keine absoluten URLs mit Protokoll und Domain wie `https://domain`. +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. -Um eine absolute URL zu generieren, fügen Sie am Anfang zwei Schrägstriche hinzu (z. B. `n:href="//Home:"`). Oder Sie können den Presenter so umschalten, dass er nur absolute Links generiert, indem Sie `$this->absoluteUrls = true` setzen. +Im Template lässt sich außerdem der Filter `|absoluteUrl` verwenden, der einen relativen Pfad in einen absoluten umwandelt. -Link zur aktuellen Seite -======================== +Link auf die aktuelle Seite +=========================== -Das Ziel `this` erstellt einen Link zur aktuellen Seite: +Das Ziel `this` erzeugt einen Link auf die aktuelle Seite: ```latte aktualisieren ``` -Gleichzeitig werden auch alle Parameter übertragen, die in der Signatur der Methode `action()` oder `render()` angegeben sind, falls `action()` nicht definiert ist. Wenn wir also auf der Seite `Product:show` mit `id: 123` sind, übergibt der Link auf `this` auch diesen Parameter. +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 ist es möglich, Parameter direkt anzugeben: +Natürlich lassen sich Parameter auch direkt angeben: ```latte aktualisieren ``` -Die Funktion `isLinkCurrent()` prüft, ob das Ziel des Links mit der aktuellen Seite übereinstimmt. Dies kann beispielsweise in einem Template verwendet werden, um Links zu unterscheiden usw. +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 die gleichen wie bei der Methode `link()`, zusätzlich ist es jedoch möglich, anstelle einer konkreten Aktion den Platzhalter `*` anzugeben, der jede Aktion des gegebenen Presenters bedeutet. +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')} @@ -162,15 +179,15 @@ Die Parameter sind die gleichen wie bei der Methode `link()`, zusätzlich ist es ``` -In Kombination mit `n:href` in einem Element kann eine verkürzte Form verwendet werden: +In Kombination mit `n:href` im selben Element lässt sich eine Kurzform verwenden: ```latte ... ``` -Der Platzhalter `*` kann nur anstelle der Aktion verwendet werden, nicht für den Presenter. +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 Submodul befinden, verwenden wir die Methode `isModuleCurrent(moduleName)`. +Um festzustellen, ob wir uns in einem bestimmten Modul oder dessen Untermodul befinden, dient die Methode `isModuleCurrent(moduleName)`. ```latte
  • @@ -179,10 +196,26 @@ Um festzustellen, ob wir uns in einem bestimmten Modul oder dessen Submodul befi ``` -Links zu Signalen +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 auch ein [Signal |components#Signal] (sie rufen die Methode `handle()` auf). Dann lautet die Syntax wie folgt: +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] @@ -194,7 +227,7 @@ Das Signal wird also durch ein Ausrufezeichen unterschieden: Signal ``` -Es kann auch ein Link zum Signal einer Subkomponente (oder Sub-Subkomponente) erstellt werden: +Sie können auch einen Link auf ein Signal einer Unterkomponente (oder Unter-Unterkomponente) erstellen: ```latte Signal @@ -204,13 +237,13 @@ Es kann auch ein Link zum Signal einer Subkomponente (oder Sub-Subkomponente) er Links in einer Komponente ========================= -Da [Komponenten |components] separate wiederverwendbare Einheiten sind, die keine Bindungen an umgebende Presenter haben sollten, funktionieren Links hier etwas anders. Das Latte-Attribut `n:href` und der Tag `{link}` sowie Komponentenmethoden wie `link()` und andere betrachten das Linkziel **immer als den Namen des Signals**. Daher ist es nicht notwendig, ein Ausrufezeichen anzugeben: +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, nicht Aktion +Signal, keine Aktion ``` -Wenn wir im Template einer Komponente auf Presenter verlinken möchten, verwenden wir dazu den Tag `{plink}`: +Wollen wir im Template der Komponente auf Presenter verlinken, verwenden wir den Tag `{plink}`: ```latte Startseite @@ -223,12 +256,12 @@ $this->getPresenter()->link('Home:default') ``` -Aliase .{data-version:v3.2.2} -============================= +Aliase .{data-version:3.2.3} +============================ -Manchmal kann es nützlich sein, dem Paar Presenter:Aktion einen leicht zu merkenden Alias zuzuordnen. Zum Beispiel die Startseite `Front:Home:default` einfach als `home` benennen oder `Admin:Dashboard:default` als `admin`. +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: +Aliase werden in der [Konfiguration|configuration] unter dem Schlüssel `application › aliases` definiert: ```neon application: @@ -238,26 +271,26 @@ application: sign: Front:Sign:in ``` -In Links werden sie dann mit einem At-Zeichen geschrieben, zum Beispiel: +In Links schreibt man sie dann mit einem At-Zeichen, zum Beispiel: ```latte Administration ``` -Sie werden auch in allen Methoden unterstützt, die mit Links arbeiten, wie `redirect()` und ähnliche. +Unterstützt werden sie auch in allen Methoden, die mit Links arbeiten, etwa `redirect()` und ähnlichen. Ungültige Links =============== -Es kann vorkommen, dass wir einen ungültigen Link erstellen – entweder weil er auf einen nicht existierenden Presenter verweist, oder weil er mehr Parameter übergibt, als die Zielmethode in ihrer Signatur akzeptiert, oder wenn für die Zielaktion keine URL generiert werden kann. Wie mit ungültigen Links umgegangen wird, bestimmt die statische Variable `Presenter::$invalidLinkMode`. Diese kann eine Kombination der folgenden Werte (Konstanten) annehmen: +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, als URL wird das Zeichen # zurückgegeben -- `Presenter::InvalidLinkWarning` - es wird eine E_USER_WARNING-Warnung ausgegeben, die im Produktionsmodus protokolliert wird, aber die Skriptausführung nicht unterbricht -- `Presenter::InvalidLinkTextual` - visuelle Warnung, gibt den Fehler direkt im Link aus -- `Presenter::InvalidLinkException` - es wird eine InvalidLinkException-Ausnahme ausgelöst +- `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` im Produktionsmodus führt nicht zur Unterbrechung des Skripts, aber die Warnung wird protokolliert. Im Entwicklungsmodus wird sie von [Tracy |tracy:] abgefangen und zeigt einen Bluescreen an. `InvalidLinkTextual` funktioniert so, dass als URL eine Fehlermeldung zurückgegeben wird, die mit den Zeichen `#error:` beginnt. Damit solche Links auf den ersten Blick erkennbar sind, ergänzen wir unser CSS: +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:"] { @@ -266,7 +299,7 @@ a[href^="#error:"] { } ``` -Wenn wir nicht möchten, dass im Entwicklungsmodus Warnungen erzeugt werden, können wir den stillen Modus direkt in der [Konfiguration |configuration] einstellen. +Wollen wir, dass in der Entwicklungsumgebung keine Warnungen entstehen, können wir sie direkt in der [Konfiguration|configuration] unterdrücken. ```neon application: @@ -277,10 +310,10 @@ application: LinkGenerator ============= -Wie erstellt man Links mit ähnlichem Komfort wie die Methode `link()`, aber ohne die Anwesenheit eines Presenters? Dafür gibt es den [api:Nette\Application\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 Dienst, den Sie sich über den Konstruktor übergeben lassen und dann Links mit seiner Methode `link()` erstellen können. +Der LinkGenerator ist ein Service, den Sie sich über den Konstruktor übergeben lassen können und mit dessen Methode `link()` Sie dann Links erstellen. -Im Vergleich zu Presentern gibt es hier einen Unterschied. Der LinkGenerator erstellt alle Links direkt als absolute URLs. Außerdem gibt es keinen "aktuellen Presenter", sodass man als Ziel nicht nur den Aktionsnamen `link('default')` angeben oder relative Pfade zu Modulen verwenden kann. +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 lösen immer eine `Nette\Application\UI\InvalidLinkException` aus. +Ungültige Links werfen immer eine `Nette\Application\UI\InvalidLinkException`. diff --git a/application/de/directory-structure.texy b/application/de/directory-structure.texy index c7c34615b7..450885cdd4 100644 --- a/application/de/directory-structure.texy +++ b/application/de/directory-structure.texy @@ -5,41 +5,41 @@ 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 Sie die Anwendung **logisch in Verzeichnisse gliedern** -- wie Sie die Struktur so gestalten, dass sie mit dem Wachstum des Projekts **gut skaliert** -- welche **möglichen Alternativen** es gibt und welche Vor- oder Nachteile sie haben +- 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
  • -Es ist wichtig zu erwähnen, dass das Nette Framework selbst keine bestimmte Struktur vorschreibt. Es ist so konzipiert, dass es sich leicht an alle Bedürfnisse und Präferenzen anpassen lässt. +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 ============================ -Obwohl das Nette Framework keine feste Verzeichnisstruktur vorschreibt, gibt es eine bewährte Standardanordnung in Form des [Web Project|https://github.com/nette/web-project]: +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/ ← Anwendungsverzeichnis -├── assets/ ← SCSS-, JS-Dateien, Bilder..., alternativ resources/ -├── bin/ ← Skripte für die Befehlszeile +├── 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/ ← Bibliotheken, die mit Composer installiert wurden +├── vendor/ ← von Composer installierte Bibliotheken └── www/ ← öffentliches Verzeichnis (Document-Root) \-- -Sie können diese Struktur beliebig an Ihre Bedürfnisse anpassen – Ordner umbenennen oder verschieben. Anschließend müssen Sie nur die relativen Pfade zu den Verzeichnissen in der Datei `Bootstrap.php` und gegebenenfalls `composer.json` anpassen. Mehr ist nicht nötig, keine komplexe Neukonfiguration, keine Änderung von Konstanten. Nette verfügt über eine intelligente Autoerkennung und erkennt automatisch den Speicherort der Anwendung einschließlich ihrer URL-Basis. +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 untersuchen, sollten Sie sich schnell darin zurechtfinden. Stellen Sie sich vor, Sie klicken auf das Verzeichnis `app/Model/` und sehen diese Struktur: +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/ @@ -48,9 +48,9 @@ Wenn Sie ein neues Projekt zum ersten Mal untersuchen, sollten Sie sich schnell └── Entities/ \-- -Daraus können Sie nur entnehmen, dass das Projekt einige Dienste, Repositories und Entitäten verwendet. Über den tatsächlichen Zweck der Anwendung erfahren Sie überhaupt nichts. +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**: +Schauen wir uns einen anderen Ansatz an - die **Organisation nach Domänen**: /--pre app/Model/ @@ -60,62 +60,62 @@ Schauen wir uns einen anderen Ansatz an – die **Organisation nach Domänen**: └── 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. +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 Klassentypen) bringt in der Praxis eine Reihe von Problemen mit sich: Code, der logisch zusammenhängt, ist auf verschiedene Ordner verteilt, und Sie müssen zwischen ihnen hin- und herspringen. Deshalb werden wir nach Domänen organisieren. +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 mit den Namespaces in der Anwendung korrespondiert. Das bedeutet, dass der physische Speicherort der Dateien ihrem Namespace entspricht. Zum Beispiel sollte eine Klasse, die sich in `app/Model/Product/ProductRepository.php` befindet, den Namespace `App\Model\Product` haben. Dieses Prinzip hilft bei der Orientierung im Code und vereinfacht das Autoloading. +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`. Ebenso innerhalb der Anwendung: `Model`, `Core`, `Presentation`. Das liegt daran, dass jedes dieser Verzeichnisse ein zusammenhängendes Konzept darstellt. +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. -Ähnlich repräsentiert z.B. `app/Model/Product` alles rund um Produkte. Wir nennen es nicht `Products`, weil es sich nicht um einen Ordner voller Produkte handelt (dann wären dort Dateien wie `nokia.php`, `samsung.php`). Es ist ein Namespace, der Klassen für die Arbeit mit Produkten enthält – `ProductRepository.php`, `ProductService.php`. +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 einen Satz eigenständiger ausführbarer Skripte enthält – `CleanupTask.php`, `ImportTask.php`. Jedes davon ist eine eigenständige Einheit. +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. -Zur Konsistenz empfehlen wir die Verwendung von: -- Singular für einen Namespace, der eine funktionale Einheit repräsentiert (auch wenn er mit mehreren Entitäten arbeitet) -- Plural für Sammlungen eigenständiger Einheiten -- Im Zweifelsfall oder wenn Sie nicht darüber nachdenken möchten, wählen Sie den Singular +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 vom Web aus zugänglich ist (sog. Document-Root). Oft trifft man auch auf den Namen `public/` anstelle von `www/` – das ist nur eine Frage der Konvention und hat keinen Einfluss auf die Funktionalität des Frameworks. Das Verzeichnis enthält: -- Den [Einstiegspunkt |bootstrapping#index.php] der Anwendung `index.php` -- Die Datei `.htaccess` mit Regeln für mod_rewrite (bei Apache) -- Statische Dateien (CSS, JavaScript, Bilder) -- Hochgeladene Dateien +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 korrekte Sicherheit der Anwendung ist es entscheidend, den [konfigurierten Document-Root |nette:troubleshooting#Wie ändert oder entfernt man das Verzeichnis www aus der URL] richtig eingestellt zu haben. +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] -Platzieren Sie niemals den Ordner `node_modules/` in diesem Verzeichnis – er enthält Tausende von Dateien, die ausführbar sein könnten und nicht öffentlich zugänglich sein sollten. +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. -Anwendungsverzeichnis `app/` -============================ +Verzeichnis der Anwendung `app/` +================================ -Dies ist das Hauptverzeichnis mit dem Anwendungscode. Die Grundstruktur: +Das ist das Hauptverzeichnis mit dem Code der Anwendung. Grundstruktur: /--pre app/ -├── Core/ ← Infrastrukturangelegenheiten +├── Core/ ← infrastrukturelle Belange ├── Model/ ← Geschäftslogik ├── Presentation/ ← Presenter und Templates -├── Tasks/ ← Befehlszeilenskripte -└── Bootstrap.php ← Bootstrap-Klasse der Anwendung +├── 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 erstellt. +`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. @@ -123,13 +123,13 @@ Schauen wir uns nun die einzelnen Unterverzeichnisse genauer an. Presenter und Templates ======================= -Der Präsentationsteil der Anwendung befindet sich im Verzeichnis `app/Presentation`. Eine Alternative ist das kurze `app/UI`. Dies ist der Ort für alle Presenter, ihre Templates und eventuelle Hilfsklassen. +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 einen E-Shop, einen Blog und eine API kombiniert, würde die Struktur so aussehen: +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/ ← E-Shop Frontend +├── Shop/ ← Frontend des E-Shops │ ├── Product/ │ ├── Cart/ │ └── Order/ @@ -143,7 +143,7 @@ Diese Schicht organisieren wir nach Domänen. In einem komplexen Projekt, das ei └── V1/ \-- -Bei einem einfachen Blog hingegen würden wir folgende Gliederung verwenden: +Für einen einfachen Blog würden wir dagegen folgende Struktur verwenden: /--pre app/Presentation/ @@ -154,12 +154,12 @@ Bei einem einfachen Blog hingegen würden wir folgende Gliederung verwenden: │ ├── Dashboard/ │ └── Posts/ ├── Error/ -└── Export/ ← RSS, Sitemaps etc. +└── Export/ ← RSS, Sitemaps usw. \-- -Ordner wie `Home/` oder `Dashboard/` enthalten Presenter und Templates. Ordner wie `Front/`, `Admin/` oder `Api/` nennen wir **Module**. Technisch gesehen sind dies normale Verzeichnisse, die zur logischen Gliederung der Anwendung dienen. +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 den gleichnamigen Presenter und seine Templates. Zum Beispiel enthält der Ordner `Dashboard/`: +Jeder Ordner mit einem Presenter enthält die Presenter-Datei selbst und ihre Templates. Der Ordner `Dashboard/` enthält zum Beispiel: /--pre Dashboard/ @@ -167,7 +167,7 @@ Jeder Ordner mit einem Presenter enthält den gleichnamigen Presenter und seine └── default.latte ← Template \-- -Diese Verzeichnisstruktur spiegelt sich in den Namespaces der Klassen wider. Zum Beispiel befindet sich `DashboardPresenter` im Namespace `App\Presentation\Admin\Dashboard` (siehe [#Presenter-Mapping]): +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; @@ -178,13 +178,13 @@ class DashboardPresenter extends Nette\Application\UI\Presenter } ``` -Auf den Presenter `Dashboard` innerhalb des Moduls `Admin` verweisen wir in der Anwendung mittels Doppelpunktnotation als `Admin:Dashboard`. Auf seine Aktion `default` dann als `Admin:Dashboard:default`. Bei verschachtelten Modulen verwenden wir mehrere Doppelpunkte, zum Beispiel `Shop:Order:Detail:default`. +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 Strukturentwicklung ----------------------------- +Flexible Entwicklung der Struktur +--------------------------------- -Einer der großen Vorteile dieser Struktur ist, wie elegant sie sich an die wachsenden Anforderungen des Projekts anpasst. Nehmen wir als Beispiel den Teil, der XML-Feeds generiert. Am Anfang haben wir eine einfache Form: +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/ @@ -193,7 +193,7 @@ Einer der großen Vorteile dieser Struktur ist, wie elegant sie sich an die wach └── feed.latte ← Template für den RSS-Feed \-- -Mit der Zeit kommen weitere Feed-Typen hinzu und wir benötigen mehr Logik für sie... Kein Problem! Der Ordner `Export/` wird einfach zu einem Modul: +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/ @@ -202,47 +202,47 @@ Mit der Zeit kommen weitere Feed-Typen hinzu und wir benötigen mehr Logik für │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← Feed für Zboží.cz - └── heureka.latte ← Feed für Heureka.cz + ├── amazon.latte ← Feed für Amazon + └── ebay.latte ← Feed für eBay \-- -Diese Transformation ist absolut nahtlos – es genügt, neue Unterordner zu erstellen, den Code darin aufzuteilen und die Links zu aktualisieren (z.B. von `Export:feed` zu `Export:Feed:zbozi`). Dadurch können wir die Struktur nach Bedarf schrittweise erweitern, die Verschachtelungsebene ist in keiner Weise begrenzt. +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 beispielsweise in der Administration viele Presenter haben, die sich auf die Verwaltung von Bestellungen beziehen, wie `OrderDetail`, `OrderEdit`, `OrderDispatch` usw., können Sie zur besseren Organisation an dieser Stelle ein Modul (Ordner) `Order` erstellen, in dem sich die (Ordner für die) Presenter `Detail`, `Edit`, `Dispatch` und weitere befinden. +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. -Platzierung von Templates -------------------------- +Ort der Templates +----------------- -In den vorherigen Beispielen haben wir gesehen, dass die Templates direkt im Ordner mit dem Presenter platziert sind: +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 Klasse für das Template +├── DashboardTemplate.php ← optionale Template-Klasse └── default.latte ← Template \-- -Diese Platzierung erweist sich in der Praxis als am bequemsten – Sie haben alle zusammengehörigen Dateien sofort zur Hand. +Dieser Ort erweist sich in der Praxis als der bequemste - Sie haben alle zusammengehörigen Dateien griffbereit. -Alternativ können Sie die Templates in einem Unterordner `templates/` platzieren. Nette unterstützt beide Varianten. Sie können Templates sogar ganz außerhalb des `Presentation/`-Ordners platzieren. Alles über die Möglichkeiten zur Platzierung von Templates finden Sie im Kapitel [Suche nach Templates |templates#Finden von Vorlagen]. +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 gehören oft auch weitere Hilfsdateien. Wir platzieren sie logisch nach ihrem Wirkungsbereich: +Zu Presentern und Templates kommen oft weitere Hilfsdateien. Wir platzieren sie logisch nach ihrem Geltungsbereich: -1. **Direkt beim Presenter** im Falle spezifischer Komponenten für den jeweiligen Presenter: +1. **Direkt beim Presenter** im Fall von Komponenten, die für diesen Presenter spezifisch sind: /--pre Product/ ├── ProductPresenter.php -├── ProductGrid.php ← Komponente zur Produktauflistung -└── FilterForm.php ← Formular zur Filterung +├── ProductGrid.php ← Komponente für die Produktauflistung +└── FilterForm.php ← Formular zum Filtern \-- -2. **Für das Modul** – wir empfehlen die Verwendung des Ordners `Accessory`, der übersichtlich gleich am Anfang des Alphabets platziert wird: +2. **Für das Modul** - wir empfehlen den Ordner `Accessory`, der alphabetisch praktischerweise ganz am Anfang steht: /--pre Front/ @@ -253,7 +253,7 @@ Zu Presentern und Templates gehören oft auch weitere Hilfsdateien. Wir platzier └── Cart/ \-- -3. **Für die gesamte Anwendung** – in `Presentation/Accessory/`: +3. **Für die gesamte Anwendung** - in `Presentation/Accessory/`: /--pre app/Presentation/ ├── Accessory/ @@ -263,20 +263,20 @@ Zu Presentern und Templates gehören oft auch weitere Hilfsdateien. Wir platzier └── Admin/ \-- -Oder Sie können Hilfsklassen wie `LatteExtension.php` oder `TemplateFilters.php` im Infrastrukturordner `app/Core/Latte/` platzieren. Und Komponenten in `app/Components`. Die Wahl hängt von den Gewohnheiten des Teams ab. +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 - Das Herz der Anwendung -============================== +Model - Herz der Anwendung +========================== -Das Model enthält die gesamte Geschäftslogik der Anwendung. Für seine Organisation gilt wieder die Regel – wir strukturieren nach Domänen: +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 ← Hauptzugangspunkt +│ ├── PaymentFacade.php ← Haupteinstiegspunkt │ ├── PaymentRepository.php -│ ├── Payment.php ← Entität +│ ├── Payment.php ← Entity ├── Order/ ← alles rund um Bestellungen │ ├── OrderFacade.php │ ├── OrderRepository.php @@ -284,9 +284,9 @@ Das Model enthält die gesamte Geschäftslogik der Anwendung. Für seine Organis └── Shipping/ ← alles rund um den Versand \-- -Im Model treffen Sie typischerweise auf diese Klassentypen: +Im Model begegnen Ihnen typischerweise diese Arten von Klassen: -**Fassaden**: stellen den Hauptzugangspunkt zu einer bestimmten Domäne in der Anwendung dar. Sie fungieren als Orchestrator, der die Zusammenarbeit zwischen verschiedenen Diensten koordiniert, um vollständige Use Cases (wie "Bestellung erstellen" oder "Zahlung verarbeiten") zu implementieren. Unter ihrer Orchestrierungsschicht verbirgt die Fassade Implementierungsdetails vor dem Rest der Anwendung und bietet so eine saubere Schnittstelle für die Arbeit mit der jeweiligen Domäne. +**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 @@ -294,14 +294,14 @@ class OrderFacade public function createOrder(Cart $cart): Order { // Validierung - // Erstellung der Bestellung - // Senden der E-Mail - // Eintrag in die Statistiken + // Anlegen der Bestellung + // Versand der E-Mail + // Schreiben in die Statistik } } ``` -**Dienste**: konzentrieren sich auf eine spezifische Geschäftsoperation innerhalb der Domäne. Im Gegensatz zur Fassade, die ganze Use Cases orchestriert, implementiert ein Dienst spezifische Geschäftslogik (wie Preisberechnungen oder Zahlungsverarbeitung). Dienste sind typischerweise zustandslos und können entweder von Fassaden als Bausteine für komplexere Operationen oder direkt von anderen Teilen der Anwendung für einfachere Aufgaben verwendet werden. +**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 @@ -313,7 +313,7 @@ class PricingService } ``` -**Repositories**: stellen die gesamte Kommunikation mit dem Datenspeicher sicher, typischerweise einer Datenbank. Ihre Aufgabe ist das Laden und Speichern von Entitäten und die Implementierung von Methoden zu deren Suche. Das Repository schirmt den Rest der Anwendung von den Implementierungsdetails der Datenbank ab und bietet eine objektorientierte Schnittstelle für die Arbeit mit Daten. +**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 @@ -328,10 +328,10 @@ class OrderRepository } ``` -**Entitäten**: Objekte, die die Hauptgeschäftskonzepte in der Anwendung repräsentieren, ihre eigene Identität haben und sich im Laufe der Zeit ändern. Typischerweise handelt es sich um Klassen, die mittels ORM (wie Nette Database Explorer oder Doctrine) auf Datenbanktabellen abgebildet sind. Entitäten können Geschäftsregeln enthalten, die sich auf ihre Daten beziehen, sowie Validierungslogik. +**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 -// Entität, die auf die Datenbanktabelle orders abgebildet ist +// Entity, die auf die Datenbanktabelle 'orders' abgebildet ist class Order extends Nette\Database\Table\ActiveRow { public function addItem(Product $product, int $quantity): void @@ -345,17 +345,17 @@ class Order extends Nette\Database\Table\ActiveRow } ``` -**Value Objects**: unveränderliche Objekte, die Werte ohne eigene Identität repräsentieren – beispielsweise ein Geldbetrag oder eine E-Mail-Adresse. Zwei Instanzen eines Value Objects mit gleichen Werten werden als identisch betrachtet. +**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/` (oder auch `Infrastructure/`) ist die Heimat für die technische Grundlage der Anwendung. Infrastrukturcode umfasst typischerweise: +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-Management +├── Router/ ← Routing und URL-Verwaltung │ └── RouterFactory.php ├── Security/ ← Authentifizierung und Autorisierung │ ├── Authenticator.php @@ -365,12 +365,12 @@ Der Ordner `Core/` (oder auch `Infrastructure/`) ist die Heimat für die technis │ └── FileLogger.php ├── Cache/ ← Caching-Schicht │ └── FullPageCache.php -└── Integration/ ← Integration mit externen Diensten +└── Integration/ ← Integration externer Dienste ├── Slack/ └── Stripe/ \-- -Bei kleineren Projekten genügt natürlich eine flache Gliederung: +Für kleinere Projekte genügt natürlich eine flache Struktur: /--pre Core/ @@ -379,29 +379,29 @@ Bei kleineren Projekten genügt natürlich eine flache Gliederung: └── QueueMailer.php \-- -Es handelt sich um Code, der: +Das ist Code, der: -- Sich um die technische Infrastruktur kümmert (Routing, Logging, Caching) -- Externe Dienste integriert (Sentry, Elasticsearch, Redis) -- Basisdienste für die gesamte Anwendung bereitstellt (Mail, Datenbank) -- Meistens unabhängig von der spezifischen Domäne ist – Cache oder Logger funktionieren gleich für E-Shop oder Blog. +- 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. -Sind Sie unsicher, ob eine bestimmte Klasse hierher oder ins Modell gehört? Der Hauptunterschied besteht darin, dass der Code in `Core/`: +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) -- Meistens in ein anderes Projekt übertragen werden kann -- Löst "wie es funktioniert" (wie man eine E-Mail sendet), nicht "was es tut" (welche E-Mail gesendet werden soll) +- 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) -Beispiel zum besseren Verständnis: +Ein Beispiel zum besseren Verständnis: -- `App\Core\MailerFactory` - erstellt Instanzen der Klasse zum Senden von E-Mails, kümmert sich um SMTP-Einstellungen -- `App\Model\OrderMailer` - verwendet `MailerFactory` zum Senden von E-Mails über Bestellungen, kennt deren Templates und weiß, wann sie gesendet werden sollen +- `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 -Befehlszeilenskripte -==================== +Kommandoskripte +=============== -Anwendungen müssen oft Tätigkeiten außerhalb normaler HTTP-Anfragen ausführen – sei es Datenverarbeitung im Hintergrund, Wartung oder periodische Aufgaben. Zur Ausführung dienen einfache Skripte im Verzeichnis `bin/`, die eigentliche Implementierungslogik platzieren wir dann in `app/Tasks/` (oder `app/Commands/`). +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: @@ -409,52 +409,52 @@ Beispiel: app/Tasks/ ├── Maintenance/ ← Wartungsskripte │ ├── CleanupCommand.php ← Löschen alter Daten -│ └── DbOptimizeCommand.php ← Datenbankoptimierung -├── Integration/ ← Integration mit externen Systemen -│ ├── ImportProducts.php ← Import aus dem Liefersystem -│ └── SyncOrders.php ← Synchronisation von Bestellungen +│ └── 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 Modell und was in die Befehlszeilenskripte? Zum Beispiel ist die Logik zum Senden einer einzelnen E-Mail Teil des Modells, der Massenversand von Tausenden von E-Mails gehört bereits zu `Tasks/`. +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. -Aufgaben werden normalerweise [über die Befehlszeile ausgeführt |https://blog.nette.org/en/cli-scripts-in-nette-application] oder über Cron. Sie können auch über eine HTTP-Anfrage gestartet werden, aber man muss an die Sicherheit denken. Der Presenter, der die Aufgabe startet, muss abgesichert werden, zum Beispiel nur für angemeldete Benutzer oder mit einem starken Token und Zugriff von erlaubten IP-Adressen. Bei langen Aufgaben muss das Zeitlimit des Skripts erhöht und `session_write_close()` verwendet werden, damit die Session nicht gesperrt wird. +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 grundlegenden Verzeichnissen können Sie je nach Projektbedarf weitere spezialisierte Ordner hinzufügen. Schauen wir uns die häufigsten davon und ihre Verwendung an: +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/ ← gemeinsam genutzte visuelle Komponenten über die gesamte Anwendung hinweg -├── Event/ ← nützlich, wenn Sie eine ereignisgesteuerte Architektur verwenden +├── 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 gemeinsam genutzte visuelle Komponenten, die in Presentern über die gesamte Anwendung hinweg verwendet werden, kann der Ordner `app/Components` oder `app/Controls` verwendet werden: +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/ ← gemeinsam genutzte Formularkomponenten +├── Form/ ← gemeinsame Formularkomponenten │ ├── SignInForm.php │ └── UserForm.php -├── Grid/ ← Komponenten für Datenlisten +├── Grid/ ← Komponenten für Datenauflistungen │ └── DataGrid.php └── Navigation/ ← Navigationselemente ├── Breadcrumbs.php └── Menu.php \-- -Hierher gehören Komponenten mit komplexerer Logik. Wenn Sie Komponenten zwischen mehreren Projekten teilen möchten, ist es ratsam, sie in ein separates Composer-Paket auszulagern. +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 platzieren: +Im Verzeichnis `app/Mail` können Sie die Verwaltung der E-Mail-Kommunikation unterbringen: /--pre app/Mail/ @@ -468,32 +468,32 @@ Im Verzeichnis `app/Mail` können Sie die Verwaltung der E-Mail-Kommunikation pl Presenter-Mapping ================= -Das Mapping definiert Regeln zur Ableitung des Klassennamens aus dem Presenter-Namen. Wir spezifizieren sie in der [Konfiguration|configuration] unter dem Schlüssel `application › 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`) platzieren. Diese Konvention müssen wir Nette in der Konfigurationsdatei mitteilen. Eine Zeile genügt: +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 möchten, dass die Presenter-Klassen in den Namespace `App\Presentation` fallen, damit der Presenter `Home` auf die Klasse `App\Presentation\HomePresenter` abgebildet wird. Was wir mit dieser Konfiguration erreichen: +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 Presenter-Name `Home` das Sternchen in der Maske `App\Presentation\*Presenter` ersetzt, wodurch wir den resultierenden Klassennamen `App\Presentation\HomePresenter` erhalten. Einfach! +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, platzieren wir die Presenter-Klassen in gleichnamigen Unterverzeichnissen, zum Beispiel wird der Presenter `Home` auf die Klasse `App\Presentation\Home\HomePresenter` abgebildet. Dies erreichen wir mit der `***Presenter`-Maske (erfordert Nette Application 3.2): +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 kommen wir zum Mapping von Presentern in Module. Für jedes Modul können wir ein spezifisches Mapping definieren: +Nun gehen wir zum Mapping von Presentern in Module über. Für jedes Modul können wir ein eigenes Mapping definieren: ```neon application: @@ -503,9 +503,9 @@ application: Api: App\Api\*Presenter ``` -Gemäß 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. +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` eine ähnliche Mapping-Methode haben und es solche Module wahrscheinlich mehr geben wird, ist es möglich, eine allgemeine Regel zu erstellen, die sie ersetzt. Zur Klassenmaske kommt somit ein neues Sternchen für das Modul hinzu: +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: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -Es funktioniert auch für tiefer verschachtelte Verzeichnisstrukturen, wie zum Beispiel den Presenter `Admin:User:Edit`, wobei sich das Segment mit dem Sternchen für jede Ebene wiederholt und das Ergebnis die Klasse `App\Presentation\Admin\User\Edit\EditPresenter` ist. +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 die Verwendung eines Arrays anstelle einer Zeichenkette, bestehend aus drei Segmenten. Diese Schreibweise ist äquivalent zur vorherigen: +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: diff --git a/application/de/how-it-works.texy b/application/de/how-it-works.texy index f58652fbde..0766651e3b 100644 --- a/application/de/how-it-works.texy +++ b/application/de/how-it-works.texy @@ -3,10 +3,10 @@ Wie funktionieren Anwendungen?
    -Sie lesen gerade das grundlegende Dokument der Nette-Dokumentation. Sie werden das gesamte Funktionsprinzip von Webanwendungen kennenlernen. Schön von A bis Z, von dem Moment der Entstehung bis zum letzten Atemzug des PHP-Skripts. Nach dem Lesen werden Sie wissen: +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 DI-Container sind +- was Bootstrap, Presenter und der DI-Container sind - wie die Verzeichnisstruktur aussieht
    @@ -15,89 +15,89 @@ Sie lesen gerade das grundlegende Dokument der Nette-Dokumentation. Sie werden d Verzeichnisstruktur =================== -Öffnen Sie das Beispiel-Skelett einer Webanwendung namens [WebProject|https://github.com/nette/web-project] und beim Lesen können Sie sich die Dateien ansehen, von denen die Rede ist. +Ö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 mit der Anwendung -│ ├── Core/ ← grundlegende Klassen, die für den Betrieb notwendig sind +├── app/ ← Verzeichnis der Anwendung +│ ├── Core/ ← für den Betrieb notwendige Basisklassen │ │ └── RouterFactory.php ← Konfiguration der URL-Adressen │ ├── Presentation/ ← Presenter, Templates & Co. -│ │ ├── @layout.latte ← Layout-Template +│ │ ├── @layout.latte ← Template des Layouts │ │ └── Home/ ← Verzeichnis des Home-Presenters │ │ ├── HomePresenter.php ← Klasse des Home-Presenters -│ │ └── default.latte ← Template der default-Aktion +│ │ └── default.latte ← Template für die Aktion default │ └── Bootstrap.php ← Startklasse Bootstrap ├── assets/ ← Ressourcen (SCSS, TypeScript, Quellbilder) -├── bin/ ← Skripte, die von der Kommandozeile ausgeführt werden +├── bin/ ← von der Kommandozeile ausgeführte Skripte ├── config/ ← Konfigurationsdateien │ ├── common.neon │ └── services.neon ├── log/ ← protokollierte Fehler ├── temp/ ← temporäre Dateien, Cache, … -├── vendor/ ← Bibliotheken, die mit Composer installiert wurden +├── vendor/ ← von Composer installierte Bibliotheken │ ├── ... │ └── autoload.php ← Autoloading aller installierten Pakete -├── www/ ← öffentliches Verzeichnis oder Document-Root des Projekts +├── www/ ← öffentliches Verzeichnis, Document-Root des Projekts │ ├── assets/ ← kompilierte statische Dateien (CSS, JS, Bilder, ...) -│ ├── .htaccess ← mod_rewrite-Regeln -│ └── index.php ← initiale Datei, mit der die Anwendung gestartet wird +│ ├── .htaccess ← Regeln für mod_rewrite +│ └── index.php ← Startdatei, die die Anwendung startet └── .htaccess ← verbietet den Zugriff auf alle Verzeichnisse außer www \-- -Die Verzeichnisstruktur können Sie beliebig ändern, Ordner umbenennen oder verschieben, sie ist völlig flexibel. Nette verfügt zudem über eine intelligente Autodetektion und erkennt automatisch den Speicherort der Anwendung einschließlich ihrer URL-Basis. +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 aufteilen |directory-structure#Presenter und Templates] und Klassen in Namespaces, die wir Module nennen. +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 sogenannte öffentliche Verzeichnis oder Document-Root des Projekts dar. Sie können es umbenennen, ohne etwas Weiteres auf Anwendungsseite einstellen zu müssen. Es ist nur notwendig, [das Hosting zu konfigurieren |nette:troubleshooting#Wie ändert oder entfernt man das Verzeichnis www aus der URL], damit der Document-Root auf dieses Verzeichnis zeigt. +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 sich auch direkt inklusive Nette herunterladen, und zwar mittels [Composer |best-practices:composer]: +WebProject können Sie samt Nette auch direkt mit [Composer |best-practices:composer] herunterladen: ```shell composer create-project nette/web-project ``` -Unter Linux oder macOS setzen Sie für die Verzeichnisse `log/` und `temp/` [Schreibrechte |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. +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 muss überhaupt nichts konfiguriert werden und Sie können sie direkt im Browser anzeigen, indem Sie auf den Ordner `www/` zugreifen. +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-Anfrage +HTTP-Request ============ -Alles beginnt in dem Moment, in dem der Benutzer im Browser eine Seite öffnet. Also wenn der Browser beim Server mit einer HTTP-Anfrage anklopft. Die Anfrage zielt auf eine einzige PHP-Datei, die sich im öffentlichen Verzeichnis `www/` befindet, und das ist `index.php`. Nehmen wir an, es handelt sich um eine Anfrage an die Adresse `https://example.com/product/123`. Dank geeigneter [Serverkonfiguration |nette:troubleshooting#Wie konfiguriert man den Server für schöne URLs Pretty URLs] wird auch diese URL auf die Datei `index.php` abgebildet und diese wird ausgeführt. +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 erhalten -3) die Nette-Anwendung zu starten, die die Anfrage bearbeitet +2) die Factory zu beschaffen +3) die Nette-Anwendung zu starten, die den Request bearbeitet -Welche Factory denn? Wir stellen doch keine Traktoren her, sondern Webseiten! Bleiben Sie dran, das wird gleich erklärt. +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, dass [Tracy|tracy:] aktiviert wird, ein großartiges Werkzeug zur Protokollierung oder Visualisierung von Fehlern. Auf dem Produktionsserver protokolliert es Fehler, auf dem Entwicklungsserver zeigt es sie direkt an. Zur Initialisierung gehört also auch die Entscheidung, ob die Website im Produktions- oder Entwicklungsmodus läuft. Dazu verwendet Nette eine [intelligente Autodetektion |bootstrapping#Entwicklungs- vs. Produktionsmodus]: Wenn Sie die Website auf localhost starten, läuft sie im Entwicklungsmodus. Sie müssen also nichts konfigurieren und die Anwendung ist direkt bereit sowohl für die Entwicklung als auch für den Live-Einsatz. Diese Schritte werden durchgeführt und sind im Kapitel über die [Bootstrap-Klasse|bootstrapping] ausführlich beschrieben. +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-Anfragen übernimmt in Nette die Klasse `Nette\Application\Application` (weiter `Application`), wenn wir also sagen, die Anwendung starten, meinen wir konkret den Aufruf der Methode mit dem treffenden Namen `run()` auf einem Objekt dieser Klasse. +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 Methoden zu schreiben. Und eine der absolut bewährtesten heißt **Dependency Injection**, abgekürzt DI. An dieser Stelle möchten wir Sie nicht mit der Erklärung von DI belasten, dafür gibt es ein [eigenes Kapitel|dependency-injection:introduction], wesentlich ist die Konsequenz, dass uns Schlüsselobjekte üblicherweise von einer Objekt-Factory erstellt werden, die **DI-Container** (abgekürzt DIC) genannt wird. Ja, das ist die Factory, von der vorhin die Rede war. Und sie stellt uns auch das `Application`-Objekt her, deshalb benötigen wir zuerst den Container. Wir erhalten ihn mittels der Klasse `Configurator` und lassen ihn das `Application`-Objekt erstellen, rufen darauf die Methode `run()` auf und damit startet die Nette-Anwendung. Genau das geschieht in der Datei [index.php |bootstrapping#index.php]. +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 eine HTTP-Anfrage zu antworten. +Die Klasse `Application` hat eine einzige Aufgabe: auf den HTTP-Request zu antworten. -Anwendungen, die in Nette geschrieben sind, gliedern sich in viele sogenannte Presenter (in anderen Frameworks können Sie auf den Begriff Controller stoßen, es handelt sich um dasselbe), das sind Klassen, von denen jede eine bestimmte Webseite repräsentiert: z.B. die Homepage; ein Produkt im E-Shop; ein Anmeldeformular; ein Sitemap-Feed usw. Eine Anwendung kann von einem bis zu Tausenden von Presentern haben. +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, den sogenannten Router zu bitten, zu entscheiden, welchem der Presenter die aktuelle Anfrage zur Bearbeitung übergeben werden soll. Der Router entscheidet, wessen Verantwortung das ist. Er schaut sich die Eingabe-URL `https://example.com/product/123` an und entscheidet auf Basis seiner Konfiguration, dass dies die Arbeit z.B. für den **Presenter** `Product` ist, von dem er als **Aktion** die Anzeige (`show`) des Produkts mit `id: 123` verlangen wird. Das Paar Presenter + Aktion wird üblicherweise durch einen Doppelpunkt getrennt als `Product:show` geschrieben. +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`. -Also hat der Router die URL in das Paar `Presenter:action` + Parameter transformiert, in unserem Fall `Product:show` + `id: 123`. Wie ein solcher Router aussieht, können Sie sich in der Datei `app/Core/RouterFactory.php` ansehen und wir beschreiben ihn detailliert im Kapitel [Routing]. +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 weitermachen. Indem sie ein Objekt der Klasse `ProductPresenter` erstellt, was der Code des Presenters `Product` ist. Genauer gesagt, bittet sie den DI-Container, den Presenter zu erstellen, denn für das Erstellen ist er da. +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 kann etwa so aussehen: +Der Presenter könnte so aussehen: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -109,92 +109,92 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function renderShow(int $id): void { - // Wir holen Daten aus dem Modell und übergeben sie an das Template + // Daten aus dem Model holen und an das Template übergeben $this->template->product = $this->repository->getProduct($id); } } ``` -Die Bearbeitung der Anfrage übernimmt der Presenter. Und die Aufgabe lautet klar: führe die Aktion `show` mit `id: 123` aus. Was in der Sprache der Presenter bedeutet, dass die Methode `renderShow()` aufgerufen wird und im Parameter `$id` den Wert `123` erhält. +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 bedienen, also mehrere Methoden `render()` haben. Wir empfehlen jedoch, Presenter mit einer oder möglichst wenigen Aktionen zu entwerfen. +Ein Presenter kann mehrere Aktionen bearbeiten, also mehrere Methoden `render()` haben. Wir empfehlen jedoch, Presenter mit einer oder möglichst wenigen Aktionen zu entwerfen. -Also, die Methode `renderShow(123)` wurde aufgerufen, deren Code zwar ein fiktives Beispiel ist, aber Sie können daran sehen, wie Daten an das Template übergeben werden, nämlich durch Schreiben in `$this->template`. +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, ein Bild, ein XML-Dokument, das Senden einer Datei von der Festplatte, JSON oder auch eine Weiterleitung auf eine andere Seite sein. Wichtig ist, dass wenn wir nicht explizit sagen, wie geantwortet werden soll (was der Fall bei `ProductPresenter` ist), die Antwort das Rendern eines Templates mit einer HTML-Seite sein wird. Warum? Weil wir in 99 % der Fälle ein Template rendern wollen, daher nimmt der Presenter dieses Verhalten als Standard an und möchte uns die Arbeit erleichtern. Das ist der Sinn von Nette. +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 dazu leitet er selbst ab. Im Falle der Aktion `show` versucht er einfach, das Template `show.latte` im Verzeichnis der Klasse `ProductPresenter` zu laden. Ebenso versucht er, das Layout in der Datei `@layout.latte` zu finden (mehr dazu unter [Template-Suche |templates#Finden von Vorlagen]). +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]). -Und anschließend rendert er die Templates. Damit ist die Aufgabe des Presenters und der gesamten Anwendung erfüllt und das Werk vollendet. Wenn das Template nicht existiert, wird eine Seite mit dem Fehler 404 zurückgegeben. Mehr über Presenter erfahren Sie auf der Seite [Presenter |presenters]. +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 versuchen wir, den gesamten Prozess mit einer etwas anderen URL zu rekapitulieren: +Zur Sicherheit fassen wir den gesamten Ablauf noch einmal mit einer etwas anderen URL zusammen: -1) Die URL lautet `https://example.com` -2) Wir booten die Anwendung, der DI-Container wird erstellt und `Application::run()` wird gestartet -3) Der Router dekodiert die URL als Paar `Home:default` -4) Ein Objekt der Klasse `HomePresenter` wird erstellt -5) Die Methode `renderDefault()` wird aufgerufen (falls sie existiert) -6) Das Template z.B. `default.latte` mit dem Layout z.B. `@layout.latte` wird gerendert +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 Sie jetzt auf viele neue Begriffe gestoßen, aber wir glauben, dass sie Sinn ergeben. Die Entwicklung von Anwendungen in Nette ist unglaublich entspannt. +Vielleicht sind Ihnen gerade viele neue Begriffe begegnet, aber wir glauben, dass sie Sinn ergeben. Anwendungen in Nette zu entwickeln ist bemerkenswert unkompliziert. Templates ========= -Wo wir schon bei Templates sind, in Nette wird das Template-System [Latte |latte:] verwendet. Daher auch die Endungen `.latte` bei den Templates. Latte wird zum einen verwendet, weil es das sicherste Template-System für PHP ist, und zum anderen auch das intuitivste System. Sie müssen nicht viel Neues lernen, Sie kommen mit PHP-Kenntnissen und ein paar Tags aus. Alles erfahren Sie [in der Dokumentation |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 werden [Links erstellt |creating-links] zu anderen Presentern & Aktionen wie folgt: +Im Template [erstellen Sie Links |creating-links] auf andere Presenter & Aktionen so: ```latte Produktdetail ``` -Einfach statt der realen URL schreiben Sie das bekannte Paar `Presenter:action` und geben eventuelle Parameter an. Der Trick liegt im `n:href`, das besagt, dass dieses Attribut von Nette verarbeitet wird. Und es generiert: +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 ``` -Die Generierung der URL übernimmt der bereits erwähnte Router. Denn Router in Nette sind außergewöhnlich, da sie nicht nur Transformationen von URL zum Paar Presenter:Aktion durchführen können, sondern auch umgekehrt, also aus dem Namen des Presenters + Aktion + Parametern eine URL generieren. Dadurch können Sie in Nette die Formen der URLs in der gesamten fertigen Anwendung vollständig ändern, ohne ein einziges Zeichen im Template oder Presenter zu ändern. Nur durch die Anpassung des Routers. Dadurch funktioniert auch die sogenannte Kanonisierung, was eine weitere einzigartige Eigenschaft von Nette ist, die zu besserem SEO (Optimierung der Auffindbarkeit im Internet) beiträgt, indem sie automatisch die Existenz von doppeltem Inhalt unter verschiedenen URLs verhindert. Viele Programmierer finden das erstaunlich. +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 eine Sache verraten: Sie haben ein eingebautes Komponentensystem. Etwas Ähnliches kennen Ältere vielleicht aus Delphi oder ASP.NET Web Forms, auf etwas entfernt Ähnlichem bauen React oder Vue.js auf. In der Welt der PHP-Frameworks ist dies eine absolut einzigartige Angelegenheit. +Ü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 Presenter) einfügen. Das können [Formulare |forms:in-presenter], [Datagrids |https://componette.org/contributte/datagrid/], Menüs, Abstimmungsumfragen sein, eigentlich alles, was sinnvoll wiederverwendet werden kann. Wir können eigene Komponenten erstellen oder einige aus dem [riesigen Angebot |https://componette.org] an Open-Source-Komponenten verwenden. +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 grundlegend den Ansatz zur Anwendungsentwicklung. Sie eröffnen Ihnen neue Möglichkeiten, Seiten aus vorgefertigten Einheiten zusammenzusetzen. Und außerdem haben sie etwas mit [Hollywood gemeinsam |components#Hollywood Style]. +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 oder die Objekt-Factory ist das Herz der gesamten Anwendung. +Der DI-Container, also die Objekt-Factory, ist das Herz der gesamten Anwendung. -Keine Sorge, es ist keine magische Blackbox, wie es vielleicht aus den vorherigen Zeilen scheinen mag. Eigentlich ist es eine ziemlich langweilige PHP-Klasse, die Nette generiert und im Cache-Verzeichnis speichert. Sie hat viele Methoden namens `createServiceAbcd()` und jede von ihnen kann ein bestimmtes Objekt erstellen und zurückgeben. Ja, es gibt auch eine Methode `createServiceApplication()`, die `Nette\Application\Application` erstellt, das wir in der Datei `index.php` zum Starten der Anwendung benötigten. Und es gibt Methoden, die einzelne Presenter erstellen. Und so weiter. +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. -Objekte, die der DI-Container erstellt, werden aus irgendeinem Grund Dienste genannt. +Die vom DI-Container erzeugten Objekte nennt man aus irgendeinem Grund Services. -Was an dieser Klasse wirklich besonders ist, ist, dass nicht Sie sie programmieren, sondern das Framework. Es generiert tatsächlich den PHP-Code und speichert ihn auf der Festplatte. Sie geben nur Anweisungen, welche Objekte der Container erstellen können soll und wie genau. Und diese Anweisungen sind in [Konfigurationsdateien |bootstrapping#Konfiguration des DI-Containers] geschrieben, für die das Format [NEON|neon:format] verwendet wird und die daher auch die Erweiterung `.neon` haben. +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 rein dazu, den DI-Container zu instruieren. Wenn ich also zum Beispiel im Abschnitt [session |http:configuration#Session] die Option `expiration: 14 days` angebe, ruft der DI-Container beim Erstellen des Objekts `Nette\Http\Session`, das die Session repräsentiert, dessen Methode `setExpiration('14 days')` auf und damit wird die Konfiguration Realität. +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. -Es gibt für Sie ein ganzes Kapitel, das beschreibt, was alles [konfiguriert |nette:configuring] werden kann und wie man [eigene Dienste definiert |dependency-injection:services]. +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 etwas tiefer in die Erstellung von Diensten eintauchen, werden Sie auf das Wort [Autowiring |dependency-injection:autowiring] stoßen. Das ist ein Feature, das Ihnen das Leben unglaublich vereinfacht. Es kann Objekte automatisch dorthin übergeben, wo Sie sie benötigen (z.B. in den Konstruktoren Ihrer Klassen), ohne dass Sie etwas tun müssen. Sie werden feststellen, dass der DI-Container in Nette ein kleines Wunder ist. +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. -Wohin als nächstes? +Wie geht es weiter? =================== -Wir sind die Grundprinzipien von Anwendungen in Nette durchgegangen. Bisher sehr oberflächlich, aber bald werden Sie in die Tiefe vordringen und mit der Zeit wunderbare Webanwendungen erstellen. Wohin soll es als nächstes gehen? Haben Sie schon das Tutorial [Meine erste Anwendung schreiben|quickstart:] ausprobiert? +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? -Neben dem oben Beschriebenen verfügt Nette über ein ganzes Arsenal an [nützlichen Klassen|utils:], eine [Datenbankschicht|database:], usw. Versuchen Sie doch mal, einfach so durch die Dokumentation zu klicken. Oder den [Blog|https://blog.nette.org]. Sie werden viele interessante Dinge entdecken. +Ü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 index c4b504a032..61c6c08e84 100644 --- a/application/de/multiplier.texy +++ b/application/de/multiplier.texy @@ -2,31 +2,31 @@ Multiplier: dynamische Komponenten ********************************** .[perex] -Werkzeug zur dynamischen Erstellung interaktiver Komponenten +Ein Werkzeug für die dynamische Erstellung interaktiver Komponenten. -Beginnen wir mit einem typischen Beispiel: Wir haben eine Liste von Waren in einem E-Shop, und für jede Ware möchten wir ein Formular zum Hinzufügen in den Warenkorb anzeigen. Eine mögliche Variante ist, die gesamte Auflistung in ein einziges Formular zu packen. Einen viel bequemeren Weg bietet uns jedoch [api:Nette\Application\UI\Multiplier]. +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]. -Multiplier ermöglicht es, bequem eine Factory für mehrere Komponenten zu definieren. Es funktioniert nach dem Prinzip verschachtelter Komponenten - jede Komponente, die von [api:Nette\ComponentModel\Container] erbt, kann weitere Komponenten enthalten. +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 oder den [Vortrag von Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. +Siehe das Kapitel über das [Komponentenmodell |components#Komponenten im Detail] in der Dokumentation. -Das Wesen des Multipliers ist, dass er als Elternteil fungiert, der seine Nachkommen dynamisch mithilfe eines im Konstruktor übergebenen Callbacks erstellen kann. Siehe Beispiel: +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('count', 'Anzahl der Artikel:') + $form->addInteger('amount', 'Anzahl:') ->setRequired(); - $form->addSubmit('send', 'In den Warenkorb legen'); + $form->addSubmit('send', 'In den Warenkorb'); return $form; }); } ``` -Jetzt können wir im Template einfach für jeden Artikel das Formular rendern lassen - und jedes wird tatsächlich eine eindeutige Komponente sein. +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} @@ -37,26 +37,26 @@ Jetzt können wir im Template einfach für jeden Artikel das Formular rendern la {/foreach} ``` -Das im Tag `{control}` übergebene Argument hat ein Format, das besagt: +Das im Tag `{control}` übergebene Argument hat ein Format, das bedeutet: -1. hole die Komponente `shopForm` -2. und hole daraus den Nachkommen `$item->id` +1. Hole die Komponente `shopForm`. +2. Hole aus ihr das Kind mit dem Namen `$item->id`. -Beim ersten Aufruf von Punkt **1.** existiert `shopForm` noch nicht, also wird seine Factory `createComponentShopForm` aufgerufen. Auf der erhaltenen Komponente (Instanz des Multipliers) wird dann die Factory des konkreten Formulars aufgerufen - was die anonyme Funktion ist, die wir dem Multiplier im Konstruktor übergeben haben. +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 des foreache wird die Methode `createComponentShopForm` nicht mehr aufgerufen (die Komponente existiert), aber da wir einen anderen Nachkommen suchen (`$item->id` wird in jeder Iteration anders sein), wird die anonyme Funktion erneut aufgerufen und gibt uns ein neues Formular zurück. +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. -Das Einzige, was übrig bleibt, ist sicherzustellen, dass das Formular tatsächlich die richtige Ware in den Warenkorb legt - derzeit ist das Formular für jeden Artikel völlig identisch. Hier hilft uns eine Eigenschaft des Multipliers (und generell jeder Komponenten-Factory im Nette Framework), nämlich dass jede Factory als erstes Argument den Namen der zu erstellenden Komponente erhält. In unserem Fall ist das `$item->id`, was genau die Information ist, die wir benötigen. Es genügt also, die Erstellung des Formulars leicht anzupassen: +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 ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Anzahl der Artikel:') + $form->addInteger('amount', 'Anzahl:') ->setRequired(); $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'In den Warenkorb legen'); + $form->addSubmit('send', 'In den Warenkorb'); return $form; }); } diff --git a/application/de/presenters.texy b/application/de/presenters.texy index e8764b84bc..27a758fbbe 100644 --- a/application/de/presenters.texy +++ b/application/de/presenters.texy @@ -3,37 +3,37 @@ Presenter
    -Wir werden lernen, wie man Presenter und Templates in Nette schreibt. Nach dem Lesen werden Sie wissen: +Wir sehen uns an, wie in Nette Presenter und Templates geschrieben werden. Nach der Lektüre werden Sie wissen: -- wie ein Presenter funktioniert +- 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 bestimmte Seite einer Webanwendung repräsentiert, z. B. die Homepage, ein Produkt in einem E-Shop, ein Anmeldeformular, einen Sitemap-Feed usw. Eine Anwendung kann einen bis tausende Presenter haben. In anderen Frameworks werden sie auch Controller genannt. +[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. -Normalerweise ist mit dem Begriff Presenter ein Nachkomme der Klasse [api:Nette\Application\UI\Presenter] gemeint, der für die Generierung von Weboberflächen geeignet ist und dem wir uns im Rest dieses Kapitels widmen werden. Im allgemeinen Sinn ist ein Presenter jedes Objekt, das das Interface [api:Nette\Application\IPresenter] implementiert. +Ü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, eine Anfrage zu bearbeiten und eine Antwort zurückzugeben (dies kann eine HTML-Seite, ein Bild, eine Weiterleitung usw. sein). +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 eine Anfrage übergeben. Dies ist keine direkte HTTP-Anfrage, sondern ein Objekt [api:Nette\Application\Request], in das die HTTP-Anfrage mithilfe des Routers umgewandelt wurde. Mit diesem Objekt kommen wir normalerweise nicht in Berührung, da der Presenter die Verarbeitung der Anfrage geschickt an weitere Methoden delegiert, die wir uns jetzt ansehen werden. +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* .<> +[* lifecycle.svg *] *** Lebenszyklus des Presenters .<> -Das Bild zeigt eine Liste von Methoden, die der Reihe nach von oben nach unten aufgerufen werden, sofern sie existieren. Keine davon muss existieren, wir können einen völlig leeren Presenter ohne eine einzige Methode haben und darauf eine einfache statische Website aufbauen. +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 nicht ganz zum Lebenszyklus des Presenters, da er im Moment der Objekterstellung aufgerufen wird. Aber wir erwähnen ihn wegen seiner Wichtigkeit. Der Konstruktor (zusammen mit der [inject-Methode|best-practices:inject-method-attribute]) dient zur Übergabe von Abhängigkeiten. +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. -Ein Presenter sollte nicht die Geschäftslogik der Anwendung handhaben, aus der Datenbank schreiben und lesen, Berechnungen durchführen usw. Dafür gibt es Klassen aus der Schicht, die wir als Model bezeichnen. Zum Beispiel kann die Klasse `ArticleRepository` für das Laden und Speichern von Artikeln zuständig sein. Damit der Presenter damit arbeiten kann, lässt er sie sich [mittels Dependency Injection übergeben |dependency-injection:passing-dependencies]: +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 @@ -50,44 +50,47 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -Unmittelbar nach Erhalt der Anfrage wird die Methode `startup()` aufgerufen. Sie können sie zur Initialisierung von Properties, zur Überprüfung von Benutzerberechtigungen usw. verwenden. Es ist erforderlich, dass die Methode immer den Vorfahren `parent::startup()` aufruft. +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 der Methode `render()`. Während `render()` dazu dient, Daten für ein bestimmtes Template vorzubereiten, das anschließend gerendert wird, wird in `action()` die Anfrage ohne Bezug zum Rendern des Templates verarbeitet. Zum Beispiel werden Daten verarbeitet, der Benutzer an- oder abgemeldet usw., und danach [woandershin weitergeleitet |#Weiterleitung]. +Ä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, sodass wir darin gegebenenfalls den weiteren Verlauf ändern können, d.h. das zu rendernde Template und auch die aufzurufende `render()`-Methode ändern können. Und zwar mittels `setView('anderesView')`. +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. -Der Methode werden Parameter aus der Anfrage übergeben. Es ist möglich und empfohlen, den Parametern Typen anzugeben, z.B. `actionShow(int $id, ?string $slug = null)` - wenn der Parameter `id` fehlt oder kein Integer ist, gibt der Presenter einen [Fehler 404 |#Fehler 404 und Co] zurück und beendet die Tätigkeit. +.{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 sogenannte Signale, die wir im Kapitel über [Komponenten |components#Signal] kennenlernen werden. Sie ist nämlich hauptsächlich für Komponenten und die Verarbeitung von AJAX-Anfragen gedacht. +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 Parameter aus der Anfrage übergeben, wie im Fall von `action()`, einschließlich Typüberprüfung. +Der Methode werden, genau wie bei `action()`, die Parameter aus dem Request übergeben, samt Typprüfung. `beforeRender()` ---------------- -Die Methode `beforeRender`, wie der Name schon sagt, wird vor jeder `render()`-Methode aufgerufen. Sie wird für die gemeinsame Konfiguration des Templates, die Übergabe von Variablen für das Layout und ähnliches verwendet. +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()} ---------------------------------------------- -Der Ort, an dem wir das Template für das anschließende Rendern vorbereiten, ihm Daten übergeben usw. +Hier bereiten wir das Template für das anschließende Rendern vor, übergeben ihm Daten usw. -Der Methode werden Parameter aus der Anfrage übergeben, wie im Fall von `action()`, einschließlich Typüberprüfung. +Der Methode werden, genau wie bei `action()`, die Parameter aus dem Request übergeben, samt Typprüfung. ```php public function renderShow(int $id): void { - // Wir holen Daten aus dem Model und übergeben sie an das Template + // Daten aus dem Model holen und an das Template übergeben $this->template->article = $this->articles->getById($id); } ``` @@ -96,7 +99,7 @@ public function renderShow(int $id): void `afterRender()` --------------- -Die Methode `afterRender`, wie der Name wiederum andeutet, wird nach jeder `render()`-Methode aufgerufen. Sie wird eher selten verwendet. +Die Methode `afterRender` wird, wie der Name wiederum nahelegt, nach jeder Methode `render()` aufgerufen. Sie wird eher selten verwendet. `shutdown()` @@ -105,95 +108,117 @@ Die Methode `afterRender`, wie der Name wiederum andeutet, wird nach jeder `rend Wird am Ende des Lebenszyklus des Presenters aufgerufen. -**Ein guter Rat, bevor wir weitermachen**. Ein Presenter kann, wie man sieht, mehrere Aktionen/Views bedienen, also mehrere `render()`-Methoden haben. Wir empfehlen jedoch, Presenter mit einer oder möglichst wenigen Aktionen zu entwerfen. +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 () { + // ... + }; + } +} +``` -Senden der Antwort -================== +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()`. -Die Antwort des Presenters ist normalerweise das [Rendern eines Templates mit einer HTML-Seite|templates], es kann aber auch das Senden einer Datei, JSON oder eine Weiterleitung zu einer anderen Seite sein. -Wir können jederzeit während des Lebenszyklus mit einer der folgenden Methoden eine Antwort senden und gleichzeitig den Presenter beenden: +**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. -- `redirect()`, `redirectPermanent()`, `redirectUrl()` und `forward()` [leiten weiter |#Weiterleitung] -- `error()` beendet den Presenter [aufgrund eines Fehlers |#Fehler 404 und Co] -- `sendJson($data)` beendet den Presenter und [sendet Daten |#Senden von JSON] im JSON-Format + +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 -Wenn Sie keine dieser Methoden aufrufen, greift der Presenter automatisch auf das Rendern des Templates zurück. Warum? Weil wir in 99 % der Fälle ein Template rendern möchten, daher betrachtet der Presenter dieses Verhalten als Standard und möchte uns die Arbeit erleichtern. +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. -Erstellen von Links -=================== -Der Presenter verfügt über die Methode `link()`, mit der URL-Links zu anderen Presentern erstellt werden können. Der erste Parameter ist der Ziel-Presenter & Aktion, gefolgt von den übergebenen Argumenten, die als Array angegeben werden können: +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' => 'cs']); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -Im Template werden Links zu anderen Presentern & Aktionen auf diese Weise erstellt: +Im Template werden Links auf andere Presenter & Aktionen so erstellt: ```latte Produktdetail ``` -Statt der echten URL schreiben Sie einfach das bekannte Paar `Presenter:action` und geben eventuelle Parameter an. Der Trick liegt in `n:href`, das besagt, dass dieses Attribut von Latte verarbeitet wird und die echte URL generiert. In Nette müssen Sie also überhaupt nicht über URLs nachdenken, nur über Presenter und Aktionen. +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. -Weitere Informationen finden Sie im Kapitel [Erstellen von URL-Links|creating-links]. +Mehr dazu finden Sie im Kapitel [Erstellen von URL-Links|creating-links]. Weiterleitung ============= -Zum Wechsel zu einem anderen Presenter dienen die Methoden `redirect()` und `forward()`, die eine sehr ähnliche Syntax wie die Methode [link() |#Erstellen von Links] haben. +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 sofort zum neuen Presenter ohne HTTP-Weiterleitung: +Die Methode `forward()` wechselt ohne HTTP-Weiterleitung sofort auf den neuen Presenter: ```php $this->forward('Product:show'); ``` -Ein Beispiel für eine sogenannte temporäre Weiterleitung mit dem HTTP-Code 302 (oder 303, wenn die Methode der aktuellen Anfrage POST ist): +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); ``` -Eine permanente Weiterleitung mit dem HTTP-Code 301 erreichen Sie so: +Für eine dauerhafte Weiterleitung mit dem HTTP-Code 301 verwenden Sie dies: ```php $this->redirectPermanent('Product:show', $id); ``` -Zu einer anderen URL außerhalb der Anwendung kann mit der Methode `redirectUrl()` weitergeleitet werden. Als zweiter Parameter kann der HTTP-Code angegeben werden, Standard ist 302 (oder 303, wenn die Methode der aktuellen Anfrage POST ist): +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'); ``` -Die Weiterleitung beendet sofort die Tätigkeit des Presenters durch Auslösen der sogenannten stillen Beendigungs-Ausnahme `Nette\Application\AbortException`. +Eine Weiterleitung beendet die Tätigkeit des Presenters sofort, indem sie die sogenannte stille Beendigungs-Exception `Nette\Application\AbortException` wirft. -Vor der Weiterleitung können [#Flash-Nachrichten] gesendet werden, also Nachrichten, die nach der Weiterleitung im Template angezeigt werden. +Vor der Weiterleitung lassen sich [#Flash-Meldungen] senden, also Meldungen, die nach der Weiterleitung im Template angezeigt werden. -Flash-Nachrichten -================= +Flash-Meldungen +=============== -Dies sind Nachrichten, die normalerweise über das Ergebnis einer Operation informieren. Ein wichtiges Merkmal von Flash-Nachrichten ist, dass sie auch nach einer Weiterleitung im Template verfügbar sind. Auch nach der Anzeige bleiben sie noch weitere 30 Sekunden aktiv – zum Beispiel für den Fall, dass der Benutzer aufgrund einer fehlerhaften Übertragung die Seite neu lädt - die Nachricht verschwindet also nicht sofort. +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, und der Presenter kümmert sich um die Übergabe an das Template. Der erste Parameter ist der Text der Nachricht und der optionale zweite Parameter ihr Typ (error, warning, info usw.). Die Methode `flashMessage()` gibt eine Instanz der Flash-Nachricht zurück, der weitere Informationen hinzugefügt werden können. +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('Der Eintrag wurde gelöscht.'); -$this->redirect(/* ... */); // und wir leiten weiter +$this->flashMessage('Das Element wurde gelöscht.'); +$this->redirect(/* ... */); // und weiterleiten ``` -Im Template stehen diese Nachrichten in der Variable `$flashes` als `stdClass`-Objekte zur Verfügung, die die Eigenschaften `message` (Nachrichtentext), `type` (Nachrichtentyp) enthalten und die bereits erwähnten Benutzerinformationen enthalten können. Wir rendern sie zum Beispiel so: +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} @@ -202,10 +227,10 @@ Im Template stehen diese Nachrichten in der Variable `$flashes` als `stdClass`-O ``` -Fehler 404 und Co. -================== +Fehler 404 usw. +=============== -Wenn die Anfrage nicht erfüllt werden kann, zum Beispiel weil der Artikel, den wir anzeigen möchten, nicht in der Datenbank existiert, werfen wir einen 404-Fehler mit der Methode `error(?string $message = null, int $httpCode = 404)` aus. +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 @@ -218,13 +243,13 @@ public function renderShow(int $id): void } ``` -Der HTTP-Fehlercode kann als zweiter Parameter übergeben werden, Standard ist 404. Die Methode funktioniert so, dass sie die Ausnahme `Nette\Application\BadRequestException` auslöst, woraufhin die `Application` die Steuerung an den Error-Presenter übergibt. Dies ist ein Presenter, dessen Aufgabe es ist, eine Seite anzuzeigen, die über den aufgetretenen Fehler informiert. Die Einstellung des Error-Presenters erfolgt in der [Anwendungskonfiguration|configuration]. +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. -Senden von JSON -=============== +JSON senden +=========== -Ein Beispiel für eine Action-Methode, die Daten im JSON-Format sendet und den Presenter beendet: +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 @@ -235,12 +260,12 @@ public function actionData(): void ``` -Anfrageparameter .{data-version:3.1.14} -======================================= +Parameter des Requests .{data-version:3.1.14} +============================================= -Der Presenter und auch jede Komponente erhalten ihre Parameter aus der HTTP-Anfrage. Ihren Wert erhalten Sie mit der Methode `getParameter($name)` oder `getParameters()`. Die Werte sind Zeichenketten oder Arrays von Zeichenketten, es handelt sich im Grunde um Rohdaten, die direkt aus der URL stammen. +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, die Parameter über Properties zugänglich zu machen. Markieren Sie sie einfach mit dem Attribut `#[Parameter]`: +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 @@ -252,9 +277,9 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Bei der Property empfehlen wir, auch den Datentyp anzugeben (z.B. `string`), und Nette wandelt den Wert entsprechend automatisch um. Die Parameterwerte können auch [validiert werden |#Validierung von Parametern]. +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 kann der Wert der Parameter direkt festgelegt werden: +Beim Erstellen eines Links lässt sich der Wert des Parameters direkt setzen: ```latte klicken @@ -264,11 +289,11 @@ Beim Erstellen eines Links kann der Wert der Parameter direkt festgelegt werden: Persistente Parameter ===================== -Persistente Parameter dienen dazu, den Zustand zwischen verschiedenen Anfragen aufrechtzuerhalten. Ihr Wert bleibt auch nach dem Klicken auf einen Link gleich. Im Gegensatz zu Daten in der Session werden sie in der URL übertragen. Und das völlig automatisch, es ist also nicht notwendig, sie explizit in `link()` oder `n:href` anzugeben. +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? Sie haben eine mehrsprachige Anwendung. Die aktuelle Sprache ist ein Parameter, der ständig Teil der URL sein muss. Aber es wäre unglaublich mühsam, ihn in jedem Link anzugeben. Also machen Sie daraus einen persistenten Parameter `lang`, und er wird von selbst übertragen. Großartig! +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! -Das Erstellen eines persistenten Parameters ist in Nette extrem einfach. Erstellen Sie einfach eine öffentliche Property und markieren Sie sie mit einem Attribut: (früher wurde `/** @persistent */` verwendet) +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 @@ -280,14 +305,14 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Wenn `$this->lang` beispielsweise den Wert `'en'` hat, dann werden auch Links, die mit `link()` oder `n:href` erstellt wurden, den Parameter `lang=en` enthalten. Und nach dem Klicken auf den Link wird wieder `$this->lang = 'en'` 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'`. -Bei der Property empfehlen wir, auch den Datentyp anzugeben (z.B. `string`), und Sie können auch einen Standardwert angeben. Die Parameterwerte können [validiert werden |#Validierung von Parametern]. +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 standardmäßig zwischen allen Aktionen des jeweiligen Presenters übertragen. Damit sie auch zwischen mehreren Presentern übertragen werden, müssen sie entweder definiert werden: +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 -- in einem Trait, den die Presenter verwenden: +- oder in einem Trait, den die Presenter verwenden: ```php trait LanguageAware @@ -302,42 +327,62 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Beim Erstellen eines Links kann der Wert eines persistenten Parameters geändert werden: +Beim Erstellen eines Links lässt sich der Wert eines persistenten Parameters ändern: ```latte Detail auf Tschechisch ``` -Oder er kann *zurückgesetzt* werden, d.h. aus der URL entfernt werden. Dann nimmt er seinen Standardwert an: +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 separate, wiederverwendbare Einheiten, die wir in Presenter einfügen. Dies können [Formulare |forms:in-presenter], Datagrids, Menüs sein, eigentlich alles, was sinnvoll wiederverwendet werden kann. +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 den Presenter eingefügt und anschließend verwendet? Das erfahren Sie im Kapitel [Komponenten |components]. Sie werden sogar herausfinden, was sie mit Hollywood gemeinsam haben. +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 wo kann ich Komponenten bekommen? Auf der Seite [Componette |https://componette.org/search/component] finden Sie Open-Source-Komponenten sowie eine Reihe weiterer Add-ons für Nette, die von Freiwilligen aus der Community rund um das Framework hier platziert wurden. +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. -Wir gehen in die Tiefe -====================== +Tiefer eintauchen +================= .[tip] -Mit dem, was wir bisher in diesem Kapitel gezeigt haben, werden Sie wahrscheinlich vollkommen auskommen. Die folgenden Zeilen sind für diejenigen gedacht, die sich eingehender für Presenter interessieren und alles wissen möchten. +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 von Parametern --------------------------- +Validierung der Parameter +------------------------- -Die Werte der [#Anfrageparameter] und [persistenten Parameter |#Persistente Parameter], die aus der URL empfangen werden, schreibt die Methode `loadState()` in die Properties. Sie überprüft auch, ob der bei der Property angegebene Datentyp übereinstimmt, andernfalls antwortet sie mit einem 404-Fehler und die Seite wird nicht angezeigt. +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 niemals blind Parametern, da sie vom Benutzer leicht in der URL überschrieben werden können. So überprüfen wir beispielsweise, ob die Sprache `$this->lang` zu den unterstützten gehört. Ein geeigneter Weg ist, die erwähnte Methode `loadState()` zu überschreiben: +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 @@ -348,7 +393,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function loadState(array $params): void { parent::loadState($params); // hier wird $this->lang gesetzt - // es folgt die eigene Überprüfung des Wertes: + // es folgt die eigene Wertprüfung: if (!in_array($this->lang, ['en', 'cs'])) { $this->error(); } @@ -357,65 +402,47 @@ class ProductPresenter extends Nette\Application\UI\Presenter ``` -Speichern und Wiederherstellen der Anfrage ------------------------------------------- +Request speichern und wiederherstellen +-------------------------------------- -Die Anfrage, die der Presenter bearbeitet, ist ein Objekt [api:Nette\Application\Request] und wird von der Presenter-Methode `getRequest()` zurückgegeben. +Der vom Presenter bearbeitete Request ist ein Objekt [api:Nette\Application\Request], das die Methode `getRequest()` des Presenters zurückgibt. -Die aktuelle Anfrage kann in der Session gespeichert oder daraus wiederhergestellt und vom Presenter erneut ausgeführt werden. Dies ist nützlich, zum Beispiel in einer Situation, in der ein Benutzer ein Formular ausfüllt und seine Anmeldung abläuft. Um die Daten nicht zu verlieren, speichern wir vor der Weiterleitung zur Anmeldeseite die aktuelle Anfrage mit `$reqId = $this->storeRequest()` in der Session. Diese Methode gibt ihren Identifikator in Form einer kurzen Zeichenkette zurück, den wir als Parameter an den Anmelde-Presenter übergeben. +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 die Anfrage aus der Session holt und dorthin weiterleitet (forward). Die Methode überprüft dabei, ob die Anfrage vom selben Benutzer erstellt wurde, der sich jetzt angemeldet hat. Wenn sich ein anderer Benutzer angemeldet hat oder der Schlüssel ungültig ist, tut sie nichts und das Programm läuft weiter. +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. -Schauen Sie sich die Anleitung [Wie man zu einer früheren Seite zurückkehrt |best-practices:restore-request] an. +Siehe die Anleitung [Wie man zu einer früheren Seite zurückkehrt |best-practices:restore-request]. Kanonisierung ------------- -Presenter haben eine wirklich großartige Eigenschaft, die zu besserem SEO (Optimierung der Auffindbarkeit im Internet) beiträgt. Sie verhindern automatisch das Vorhandensein von doppeltem Inhalt unter verschiedenen URLs. Wenn zu einem bestimmten Ziel mehrere URL-Adressen führen, z.B. `/index` und `/index?page=1`, bestimmt das Framework eine davon als primäre (kanonische) und leitet die anderen mit dem HTTP-Code 301 dorthin weiter. Dank dessen indizieren Suchmaschinen die Seiten nicht zweimal und verwässern nicht deren Page Rank. +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 Prozess wird Kanonisierung genannt. Die kanonische URL ist diejenige, die vom [Router|routing] generiert wird, in der Regel also die erste passende Route in der Sammlung. +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 aktiviert und kann über `$this->autoCanonicalize = false` deaktiviert werden. +Die Kanonisierung ist standardmäßig eingeschaltet und lässt sich mit `$this->autoCanonicalize = false` abschalten. -Bei einer AJAX- oder POST-Anfrage erfolgt keine Weiterleitung, da dies zu Datenverlust führen würde oder aus SEO-Sicht keinen Mehrwert hätte. +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. -Sie können die Kanonisierung auch manuell mit der Methode `canonicalize()` auslösen, der ähnlich wie der Methode `link()` der Presenter, die Aktion und die Parameter übergeben werden. Sie erstellt einen Link und vergleicht ihn mit der aktuellen URL-Adresse. Wenn sie sich unterscheiden, wird auf den generierten Link weitergeleitet. +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 sich von $realSlug unterscheidet + // leitet weiter, wenn $slug von $realSlug abweicht $this->canonicalize('Product:show', [$id, $realSlug]); } ``` - -Ereignisse ----------- - -Neben den Methoden `startup()`, `beforeRender()` und `shutdown()`, die als Teil des Lebenszyklus des Presenters aufgerufen werden, können noch weitere Funktionen definiert werden, die automatisch aufgerufen werden sollen. Der Presenter definiert sogenannte [Ereignisse |nette:glossary#Events Ereignisse], deren Handler Sie zu den Arrays `$onStartup`, `$onRender` und `$onShutdown` hinzufügen. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Die Handler im Array `$onStartup` werden kurz vor der Methode `startup()` aufgerufen, `$onRender` zwischen `beforeRender()` und `render()` und schließlich `$onShutdown` kurz vor `shutdown()`. +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 Antwort, die ein Presenter zurückgibt, ist ein Objekt, das das Interface [api:Nette\Application\Response] implementiert. Es stehen eine Reihe von vorbereiteten Antworten zur Verfügung: +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 @@ -430,43 +457,103 @@ Antworten werden mit der Methode `sendResponse()` gesendet: ```php use Nette\Application\Responses; -// Einfacher Text +// Reiner Text $this->sendResponse(new Responses\TextResponse('Hello Nette!')); // Sendet eine Datei $this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); -// Die Antwort wird ein Callback sein +// Sendet einen Callback $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; + 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: -Zugriffsbeschränkung mit `#[Requires]` .{data-version:3.2.2} +```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 erweiterte Möglichkeiten zur Zugriffsbeschränkung für Presenter und deren Methoden. Es kann verwendet werden, um HTTP-Methoden zu spezifizieren, eine AJAX-Anfrage zu erfordern, den Zugriff auf den gleichen Ursprung (Same Origin) zu beschränken und den Zugriff nur über Forwarding zu erlauben. Das Attribut kann sowohl auf Presenter-Klassen als auch auf einzelne Methoden `action()`, `render()`, `handle()` und `createComponent()` angewendet werden. +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 folgende Beschränkungen festlegen: +Sie können diese Einschränkungen angeben: - auf HTTP-Methoden: `#[Requires(methods: ['GET', 'POST'])]` -- Erfordern einer AJAX-Anfrage: `#[Requires(ajax: true)]` -- Zugriff nur vom gleichen Ursprung: `#[Requires(sameOrigin: true)]` +- Erfordernis eines AJAX-Requests: `#[Requires(ajax: true)]` +- Zugriff nur vom selben Origin: `#[Requires(sameOrigin: true)]` - Zugriff nur über Forwarding: `#[Requires(forward: true)]` -- Beschränkung auf bestimmte Aktionen: `#[Requires(actions: 'default')]` +- Einschränkung auf bestimmte Aktionen: `#[Requires(actions: 'default')]` -Details finden Sie in der Anleitung [Verwendung des Requires-Attributs |best-practices:attribute-requires]. +.[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]. -Überprüfung der HTTP-Methode ----------------------------- -Presenter in Nette überprüfen automatisch die HTTP-Methode jeder eingehenden Anfrage. Der Grund für diese Überprüfung ist hauptsächlich die Sicherheit. Standardmäßig sind die Methoden `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` erlaubt. +Prüfung der HTTP-Methode +------------------------ -Wenn Sie zusätzlich beispielsweise die Methode `OPTIONS` erlauben möchten, verwenden Sie dazu das Attribut `#[Requires]` (ab Nette Application v3.2): +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'])] @@ -475,26 +562,34 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -In Version 3.1 erfolgt die Überprüfung in `checkHttpMethod()`, die prüft, ob die in der Anfrage angegebene Methode im Array `$presenter->allowedMethods` enthalten ist. Fügen Sie die Methode wie folgt hinzu: +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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -Es ist wichtig zu betonen, dass wenn Sie die Methode `OPTIONS` zulassen, Sie diese anschließend auch entsprechend in Ihrem Presenter behandeln müssen. Die Methode wird oft als sogenannter Preflight Request verwendet, den der Browser automatisch vor der eigentlichen Anfrage sendet, wenn überprüft werden muss, ob die Anfrage gemäß der CORS (Cross-Origin Resource Sharing) Richtlinie zulässig ist. Wenn Sie die Methode zulassen, aber keine korrekte Antwort implementieren, kann dies zu Inkonsistenzen und potenziellen Sicherheitsproblemen führen. +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. -Weitere Lektüre -=============== +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] -- [Zusammensetzen von Presentern aus Traits |best-practices:presenter-traits] -- [Übergabe von Einstellungen an Presenter |best-practices:passing-settings-to-presenters] +- [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 index 251247119d..f175e154aa 100644 --- a/application/de/routing.texy +++ b/application/de/routing.texy @@ -3,26 +3,26 @@ Routing
    -Der Router kümmert sich um alles rund um URL-Adressen, damit Sie nicht mehr darüber nachdenken müssen. Wir zeigen Ihnen: +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 den Vorstellungen entsprechen +- 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
    -Menschenfreundlichere URLs (oder auch coole oder pretty URLs) sind benutzbarer, leichter zu merken und tragen positiv zur SEO bei. Nette berücksichtigt dies 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, da dies ohne Eingriffe in den Code oder die Templates auskommt. Sie wird nämlich auf elegante Weise an einer [einzigen Stelle |#Integration in die Anwendung] definiert, im Router, und ist somit nicht in Form von Annotationen in allen Presentern verstreut. +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, da er **bidirektional** ist. Er kann sowohl URLs in einer HTTP-Anfrage dekodieren als auch Links erstellen. Er spielt daher eine entscheidende Rolle in [Nette Application |how-it-works#Nette Application], da er einerseits entscheidet, welcher Presenter und welche Aktion die aktuelle Anfrage ausführen wird, andererseits aber auch für die [Generierung von URLs |creating-links] im Template usw. verwendet wird. +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 nur auf diese Verwendung beschränkt, Sie können ihn auch in Anwendungen einsetzen, in denen überhaupt keine Presenter verwendet werden, für REST-APIs usw. Mehr dazu im Abschnitt [#Eigenständige Verwendung]. +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]. -Routen-Sammlung -=============== +Routensammlung +============== -Die angenehmste Art, 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 Routen, d. h. Masken von URL-Adressen und den ihnen zugeordneten Presentern und Aktionen über eine einfache API. Wir müssen die Routen nicht benennen. +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; @@ -31,16 +31,16 @@ $router->addRoute('article/', 'Article:view'); // ... ``` -Das Beispiel besagt, dass wenn wir im Browser `https://domain.com/rss.xml` öffnen, der Presenter `Feed` mit der Aktion `rss` angezeigt wird, wenn `https://domain.com/article/12`, wird der Presenter `Article` mit der Aktion `view` angezeigt usw. Wenn keine passende Route gefunden wird, reagiert Nette Application mit dem Auslösen einer [BadRequestException |api:Nette\Application\BadRequestException], die dem Benutzer als Fehlerseite 404 Not Found angezeigt wird. +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 Routen +Reihenfolge der Routes ---------------------- -Ganz **entscheidend ist die Reihenfolge**, in der die einzelnen Routen aufgeführt sind, da sie schrittweise von oben nach unten ausgewertet werden. Es gilt die Regel, dass wir Routen **von spezifisch nach allgemein** deklarieren: +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 und diese Zeichenkette als verstanden +// FALSCH: 'rss.xml' wird von der ersten Route abgefangen, die diesen String als versteht $router->addRoute('', 'Article:view'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -49,10 +49,10 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('', 'Article:view'); ``` -Routen werden auch bei der Generierung von Links von oben nach unten ausgewertet: +Auch beim Erzeugen von Links werden die Routes von oben nach unten ausgewertet: ```php -// FALSCH: Ein Link zu 'Feed:rss' wird als 'admin/feed/rss' generiert +// FALSCH: Ein Link auf 'Feed:rss' wird als 'admin/feed/rss' erzeugt $router->addRoute('admin//', 'Admin:default'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -61,27 +61,27 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('admin//', 'Admin:default'); ``` -Wir werden Ihnen nicht verheimlichen, dass die korrekte Zusammenstellung der Routen eine gewisse Fertigkeit erfordert. Bevor Sie diese beherrschen, wird Ihnen das [Routing-Panel |#Debugging des Routers] ein nützlicher Helfer sein. +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 Stammverzeichnis der Website. Die einfachste Maske ist eine statische URL: +Die Maske beschreibt den relativen Pfad vom Wurzelverzeichnis der Website. Die einfachste Maske ist eine statische URL: ```php $router->addRoute('products', 'Products:default'); ``` -Oft enthalten Masken sogenannte **Parameter**. Diese werden in spitzen Klammern angegeben (z. B. ``) und an den Ziel-Presenter übergeben, beispielsweise an die Methode `renderShow(int $year)` oder an den persistenten Parameter `$year`: +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, dass wenn wir im Browser `https://example.com/chronicle/2020` öffnen, der Presenter `History` mit der Aktion `show` und dem Parameter `year: 2020` angezeigt wird. +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. -Wir können Parametern direkt in der Maske einen Standardwert zuweisen, wodurch sie optional werden: +Parametern können wir direkt in der Maske einen Standardwert zuweisen, wodurch sie optional werden: ```php $router->addRoute('chronicle/', 'History:show'); @@ -95,14 +95,14 @@ Parameter können natürlich auch der Name des Presenters und der Aktion sein. Z $router->addRoute('/', 'Home:default'); ``` -Die angegebene Route akzeptiert z. B. URLs in der Form `/article/edit` oder auch `/catalog/list` und versteht sie als Presenter und Aktionen `Article:edit` und `Catalog:list`. +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`. -Gleichzeitig gibt sie den Parametern `presenter` und `action` die Standardwerte `Home` und `default`, wodurch sie ebenfalls optional sind. Die Route akzeptiert also auch eine URL in der Form `/article` und versteht sie als `Article:default`. Oder umgekehrt, ein Link zu `Product:default` generiert den Pfad `/product`, ein Link zum Standard `Home:default` den Pfad `/`. +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 Stammverzeichnis der Website beschreiben, sondern auch einen absoluten Pfad, wenn sie mit einem Schrägstrich beginnt, oder sogar eine gesamte absolute URL, wenn sie mit zwei Schrägstrichen beginnt: +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 +// relativ zum Document-Root $router->addRoute('/', /* ... */); // absoluter Pfad (relativ zur Domain) @@ -119,16 +119,16 @@ $router->addRoute('https://.example.com//', /* ... */); Validierungsausdrücke --------------------- -Für jeden Parameter kann eine Validierungsbedingung mithilfe eines [Regulären Ausdrucks|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php] festgelegt werden. Zum Beispiel bestimmen wir für den Parameter `id`, dass er nur Ziffern annehmen darf, mit dem Regulären Ausdruck `\d+`: +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 standardmäßige reguläre Ausdruck für alle Parameter ist `[^/]+`, d. h. alles außer einem Schrägstrich. Wenn ein Parameter auch Schrägstriche akzeptieren soll, geben wir den Ausdruck `.+` an: +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 wird 'a/b/c' +// akzeptiert https://example.com/a/b/c, path ist dann 'a/b/c' $router->addRoute('', /* ... */); ``` @@ -136,17 +136,17 @@ $router->addRoute('', /* ... */); Optionale Sequenzen ------------------- -In der Maske können optionale Teile mithilfe von eckigen Klammern markiert werden. Jeder Teil der Maske kann optional sein, und er kann auch Parameter enthalten: +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 Pfade: -// /cs/download => lang => cs, name => download +// Akzeptiert die Pfade: +// /en/download => lang => en, name => download // /download => lang => null, name => download ``` -Wenn ein Parameter Teil einer optionalen Sequenz ist, wird er natürlich auch optional. Wenn kein Standardwert angegeben ist, wird er null sein. +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: @@ -154,7 +154,7 @@ Optionale Teile können auch in der Domain vorkommen: $router->addRoute('//[.]example.com//', /* ... */); ``` -Sequenzen können beliebig verschachtelt und kombiniert werden: +Sequenzen lassen sich beliebig verschachteln und kombinieren: ```php $router->addRoute( @@ -162,24 +162,24 @@ $router->addRoute( 'Home:default', ); -// Akzeptiert Pfade: -// /cs/hello +// Akzeptiert die Pfade: +// /en/hello // /en-us/hello // /hello // /hello/page-12 ``` -Bei der URL-Generierung wird die kürzeste Variante angestrebt, sodass alles, was weggelassen werden kann, weggelassen wird. Daher generiert beispielsweise die Route `index[.html]` den Pfad `/index`. Das Verhalten kann durch Angabe eines Ausrufezeichens nach der linken eckigen Klammer umgekehrt werden: +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, generiert /hello +// akzeptiert /hello und /hello.html, erzeugt /hello $router->addRoute('[.html]', /* ... */); -// akzeptiert /hello und /hello.html, generiert /hello.html +// akzeptiert /hello und /hello.html, erzeugt /hello.html $router->addRoute('[!.html]', /* ... */); ``` -Optionale Parameter (d. h. Parameter mit einem Standardwert) ohne eckige Klammern verhalten sich im Grunde so, als wären sie wie folgt geklammert: +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('//', /* ... */); @@ -188,34 +188,34 @@ $router->addRoute('//', /* ... */); $router->addRoute('[/[/[]]]', /* ... */); ``` -Wenn wir das Verhalten des abschließenden Schrägstrichs beeinflussen möchten, damit z. B. anstelle von `/home/` nur `/home` generiert wird, kann dies wie folgt erreicht werden: +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('[[/[/]]]', /* ... */); ``` -Platzhalter ------------ +Wildcards +--------- -In der Maske des absoluten Pfads können wir folgende Platzhalter verwenden, um beispielsweise die Notwendigkeit zu vermeiden, die Domain in die Maske zu schreiben, die sich in der Entwicklungs- und Produktionsumgebung unterscheiden kann: +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 Stammverzeichnis +- `%host%` = gesamter Host, z. B. `www.example.com` +- `%basePath%` = Pfad zum Wurzelverzeichnis ```php $router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` -Erweiterte Notation -------------------- +Erweiterte Schreibweise +----------------------- -Das Ziel der Route, normalerweise in der Form `Presenter:action` geschrieben, kann auch mithilfe eines Arrays notiert werden, das die einzelnen Parameter und ihre Standardwerte definiert: +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('/[/]', [ @@ -224,7 +224,7 @@ $router->addRoute('/[/]', [ ]); ``` -Für eine detailliertere Spezifikation kann eine noch erweiterte Form verwendet werden, in der wir neben den Standardwerten auch andere Eigenschaften der Parameter einstellen können, wie z. B. den Validierungs-regulären Ausdruck (siehe Parameter `id`): +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; @@ -242,19 +242,29 @@ $router->addRoute('/[/]', [ ]); ``` -Es ist wichtig zu beachten, dass wenn die im Array definierten Parameter nicht in der Pfadmaske aufgeführt sind, ihre Werte nicht geändert werden können, auch nicht durch Query-Parameter, die nach dem Fragezeichen in der URL aufgeführt sind. +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 ------------------------ -Wir schreiben den Quellcode der Anwendung auf Englisch, aber wenn die Website deutsche URLs haben soll, dann wird einfaches Routing vom Typ: +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 generieren, wie z. B. `/product/123` oder `/cart`. Wenn wir Presenter und Aktionen in der URL durch deutsche Wörter repräsentieren lassen wollen (z. B. `/produkt/123` oder `/warenkorb`), können wir ein Übersetzungswörterbuch verwenden. Für dessen Notation benötigen wir bereits die "gesprächigere" Variante des zweiten Parameters: +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; @@ -263,26 +273,26 @@ $router->addRoute('/', [ 'presenter' => [ Route::Value => 'Home', Route::FilterTable => [ - // Zeichenkette in der URL => Presenter + // String in der URL => Presenter 'produkt' => 'Product', - 'warenkorb' => 'Cart', + 'kosik' => 'Cart', 'katalog' => 'Catalog', ], ], 'action' => [ Route::Value => 'default', Route::FilterTable => [ - 'liste' => 'list', + 'seznam' => 'list', ], ], ]); ``` -Mehrere Schlüssel des Übersetzungswörterbuchs können auf denselben Presenter verweisen. Dadurch werden verschiedene Aliase dafür erstellt. Als kanonische Variante (also diejenige, die in der generierten URL enthalten sein wird) gilt der letzte Schlüssel. +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 kann auf diese Weise für jeden Parameter verwendet werden. Wobei, wenn die Übersetzung nicht existiert, der ursprüngliche Wert genommen wird. Dieses Verhalten können wir durch Hinzufügen von `Route::FilterStrict => true` ändern, und die Route lehnt dann die URL ab, wenn der Wert nicht im Wörterbuch enthalten ist. +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 können auch eigene Übersetzungsfunktionen eingesetzt werden. +Neben dem Übersetzungswörterbuch in Form eines Arrays lassen sich eigene Übersetzungsfunktionen einsetzen. ```php use Nette\Routing\Route; @@ -298,15 +308,15 @@ $router->addRoute('//', [ ]); ``` -Die Funktion `Route::FilterIn` konvertiert zwischen dem Parameter in der URL und der Zeichenkette, die dann an den Presenter übergeben wird, die Funktion `FilterOut` stellt die Konvertierung in die entgegengesetzte Richtung sicher. +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 konvertieren. Der Standardwert der Parameter wird bereits in transformierter Form geschrieben, sodass wir beispielsweise im Fall des Presenters `` schreiben, nicht ``. +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, die für spezifische Parameter bestimmt sind, können wir auch allgemeine Filter definieren, die ein assoziatives Array aller Parameter erhalten, die sie beliebig modifizieren und dann zurückgeben können. Allgemeine Filter definieren wir unter dem Schlüssel `null`. +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; @@ -314,85 +324,101 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -Allgemeine Filter bieten die Möglichkeit, das Verhalten der Route auf beliebige Weise anzupassen. Wir können sie beispielsweise zur Modifikation von Parametern basierend auf anderen Parametern verwenden. Zum Beispiel die Übersetzung von `` und `` basierend auf dem aktuellen Wert des Parameters ``. +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. -Wenn ein Parameter einen eigenen Filter definiert hat und gleichzeitig ein allgemeiner Filter existiert, wird der eigene `FilterIn` vor dem allgemeinen ausgeführt und umgekehrt der allgemeine `FilterOut` vor dem eigenen. Das heißt, innerhalb des allgemeinen Filters sind die Werte der Parameter `presenter` bzw. `action` im PascalCase- bzw. camelCase-Stil geschrieben. +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]. -Einwegrouten (OneWay) ---------------------- -Einwegrouten werden verwendet, um die Funktionalität alter URLs beizubehalten, die die Anwendung nicht mehr generiert, aber weiterhin akzeptiert. Wir markieren sie mit dem Flag `OneWay`: +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', $router::ONE_WAY); +$router->addRoute('product-info', 'Product:detail', oneWay: true); // neue URL /product/123 $router->addRoute('product/', 'Product:detail'); ``` -Beim Zugriff auf die alte URL leitet der Presenter automatisch auf die neue URL weiter, sodass Suchmaschinen diese Seiten nicht doppelt indizieren (siehe [#SEO und Kanonisierung]). +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 ermöglicht es Ihnen, Routen direkt Funktionen (Callbacks) zuzuordnen, die ausgeführt werden, wenn der entsprechende Pfad besucht wird. Diese flexible Funktionalität ermöglicht es Ihnen, schnell und effizient verschiedene Endpunkte (Endpoints) für Ihre Anwendung zu erstellen: +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 unter der Adresse /test'; + echo 'Sie befinden sich auf der Adresse /test'; }); ``` -Sie können auch Parameter in der Maske definieren, die automatisch an Ihren Callback übergeben werden: +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 auf der tschechischen Version unserer Website!', - 'en' => 'Welcome to the English version of our website!', + '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 ------ -Wenn wir mehrere Routen haben, die zu einem gemeinsamen [Modul |directory-structure#Presenter und Templates] gehören, verwenden wir `withModule()`: +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 Routen sind Teil des Moduls Forum - ->addRoute('rss', 'Feed:rss') // Der Presenter wird Forum:Feed sein +$router->withModule('Forum') // die folgenden Routes sind Teil des Moduls Forum + ->addRoute('rss', 'Feed:rss') // Presenter ist Forum:Feed ->addRoute('/') - ->withModule('Admin') // Die folgenden Routen sind Teil des Moduls Forum:Admin + ->withModule('Admin') // die folgenden Routes sind Teil des Moduls Forum:Admin ->addRoute('sign:in', 'Sign:in'); ``` -Eine Alternative ist die Verwendung des Parameters `module`: +Eine Alternative ist der Parameter `module`, der ebenfalls ein festes Modul setzt und es aus der URL heraushält: ```php -// Die URL manage/dashboard/default wird auf den Presenter Admin:Dashboard gemappt +// 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 ---------- -Routen-Sammlungen können nach Subdomains gegliedert werden: +Routensammlungen lassen sich nach Subdomains aufteilen: ```php $router = new RouteList; @@ -401,7 +427,7 @@ $router->withDomain('example.com') ->addRoute('/'); ``` -Im Domainnamen können auch [#Platzhalter] verwendet werden: +Im Domainnamen lassen sich auch [#Wildcards] verwenden: ```php $router = new RouteList; @@ -413,20 +439,20 @@ $router->withDomain('example.%tld%') Pfadpräfix ---------- -Routen-Sammlungen können nach dem Pfad in der URL gegliedert werden: +Routensammlungen lassen sich nach dem Pfad in der URL aufteilen: ```php $router = new RouteList; $router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // Fängt URL /eshop/rss ab - ->addRoute('/'); // Fängt URL /eshop// ab + ->addRoute('rss', 'Feed:rss') // passt auf die URL /eshop/rss + ->addRoute('/'); // passt auf die URL /eshop// ``` Kombinationen ------------- -Die oben genannten Gliederungen können miteinander kombiniert werden: +Die obigen Gruppierungen lassen sich miteinander kombinieren: ```php $router = (new RouteList) @@ -449,10 +475,10 @@ $router = (new RouteList) Query-Parameter --------------- -Masken können auch Query-Parameter enthalten (Parameter nach dem Fragezeichen in der URL). Für diese kann kein Validierungsausdruck definiert werden, aber der Name, unter dem sie an den Presenter übergeben werden, kann geändert werden: +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 -// Den Query-Parameter 'cat' möchten wir in der Anwendung unter dem Namen 'categoryId' verwenden +// wir wollen den Query-Parameter 'cat' in der Anwendung unter dem Namen 'categoryId' verwenden $router->addRoute('product ? id= & cat=', /* ... */); ``` @@ -460,23 +486,23 @@ $router->addRoute('product ? id= & cat=', /* ... */); Foo-Parameter ------------- -Jetzt gehen wir tiefer. Foo-Parameter sind im Grunde unbenannte Parameter, die es ermöglichen, einen regulären Ausdruck abzugleichen. Ein Beispiel ist eine Route, die `/index`, `/index.html`, `/index.htm` und `/index.php` akzeptiert: +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 kann auch explizit eine Zeichenkette definiert werden, die bei der URL-Generierung verwendet wird. Die Zeichenkette muss direkt nach dem Fragezeichen platziert werden. Die folgende Route ähnelt der vorherigen, aber generiert `/index.html` anstelle von `/index`, da die Zeichenkette `.html` als Generierungswert festgelegt ist: +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 in die Anwendung -============================ +Integration +=========== -Um den erstellten Router in die Anwendung einzubinden, müssen wir den DI-Container darüber informieren. Der einfachste Weg ist, eine Factory vorzubereiten, die das Router-Objekt erstellt, und dem Container in der Konfiguration mitzuteilen, dass er sie verwenden soll. Nehmen wir an, wir schreiben zu diesem Zweck eine Methode `App\Core\RouterFactory::createRouter()`: +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; @@ -494,14 +520,14 @@ class RouterFactory } ``` -In die [Konfiguration |dependency-injection:services] schreiben wir dann: +Dann schreiben wir in die [Konfiguration |dependency-injection:services]: ```neon services: - App\Core\RouterFactory::createRouter ``` -Jegliche Abhängigkeiten, zum Beispiel auf eine Datenbank usw., werden der Factory-Methode als ihre Parameter mittels [Autowiring|dependency-injection:autowiring] übergeben: +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 @@ -514,18 +540,18 @@ public static function createRouter(Nette\Database\Connection $db): RouteList SimpleRouter ============ -Ein viel einfacherer Router als die Routen-Sammlung ist [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Wir verwenden ihn, wenn wir keine besonderen Anforderungen an die URL-Form haben, wenn `mod_rewrite` (oder seine Alternativen) nicht verfügbar ist oder wenn wir uns noch nicht mit schönen URLs befassen wollen. +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 generiert Adressen ungefähr in dieser Form: +Er erzeugt Adressen ungefähr in dieser Form: ``` http://example.com/?presenter=Product&action=detail&id=123 ``` -Der Parameter des SimpleRouter-Konstruktors ist der Standard-Presenter & die Standard-Aktion, auf den verwiesen werden soll, wenn wir eine Seite ohne Parameter öffnen, z. B. `http://example.com/`. +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' sein und die Aktion 'default' +// der Standard-Presenter wird 'Home' und die Aktion 'default' sein $router = new Nette\Application\Routers\SimpleRouter('Home:default'); ``` @@ -540,19 +566,19 @@ services: SEO und Kanonisierung ===================== -Das Framework trägt zur SEO (Optimierung der Auffindbarkeit im Internet) bei, indem es doppelte Inhalte unter verschiedenen URLs verhindert. Wenn mehrere Adressen zu einem bestimmten Ziel führen, z. B. `/index` und `/index.html`, bestimmt das Framework die erste davon als primäre (kanonische) und leitet die anderen mit dem HTTP-Code 301 darauf um. Dadurch indizieren Suchmaschinen die Seiten nicht doppelt und verwässern ihren Page Rank nicht. +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 Prozess wird Kanonisierung genannt. Die kanonische URL ist diejenige, die vom Router generiert wird, d. h. die erste passende Route in der Sammlung ohne das OneWay-Flag. Deshalb führen wir in der Sammlung die **primären Routen zuerst** auf. +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 wird vom Presenter durchgeführt, mehr im Kapitel [Kanonisierung |presenters#Kanonisierung]. +Die Kanonisierung führt der Presenter durch, mehr im Kapitel [Kanonisierung |presenters#Kanonisierung]. HTTPS ===== -Um das HTTPS-Protokoll verwenden zu können, ist es notwendig, es beim Hosting zu aktivieren und den Server korrekt zu konfigurieren. +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 eingestellt werden, zum Beispiel mithilfe der .htaccess-Datei im Stammverzeichnis unserer Anwendung, und zwar mit dem HTTP-Code 301. Die Einstellungen können je nach Hosting variieren und sehen etwa so aus: +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: ``` @@ -564,15 +590,15 @@ Die Weiterleitung der gesamten Website auf HTTPS muss auf Serverebene eingestell ``` -Der Router generiert URLs mit demselben Protokoll, mit dem die Seite geladen wurde, daher muss nichts weiter eingestellt werden. +Der Router erzeugt URLs mit demselben Protokoll, mit dem die Seite geladen wurde, es muss also nichts weiter eingestellt werden. -Wenn wir aber ausnahmsweise benötigen, dass verschiedene Routen unter verschiedenen Protokollen laufen, geben wir es in der Routenmaske an: +Brauchen wir jedoch ausnahmsweise, dass verschiedene Routes unter verschiedenen Protokollen laufen, geben wir das in der Maske der Route an: ```php -// Wird eine Adresse mit HTTP generieren +// Erzeugt eine HTTP-Adresse $router->addRoute('http://%host%//', /* ... */); -// Wird eine Adresse mit HTTPS generieren +// Erzeugt eine HTTPS-Adresse $router->addRoute('https://%host%//', /* ... */); ``` @@ -580,24 +606,24 @@ $router->addRoute('https://%host%//', /* ... */); Debugging des Routers ===================== -Das Routing-Panel, das in der [Tracy Bar |tracy:] angezeigt wird, ist ein nützlicher Helfer, der eine Liste der Routen sowie die Parameter anzeigt, die der Router aus der URL erhalten hat. +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, mit blauer Farbe und dem Symbol ≈ sind Routen gekennzeichnet, die die URL ebenfalls verarbeitet hätten, wenn die grüne sie nicht überholt hätte. Weiterhin sehen wir den aktuellen Presenter & die aktuelle Aktion. +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 *] -Gleichzeitig, wenn es zu einer unerwarteten Weiterleitung aufgrund der [Kanonisierung |#SEO und Kanonisierung] kommt, ist es nützlich, in das Panel in der Leiste *redirect* zu schauen, wo Sie herausfinden, wie der Router die URL ursprünglich verstanden hat und warum er weitergeleitet hat. +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, die Developer Tools im Browser zu öffnen (Strg+Shift+I oder Cmd+Option+I) und im Network-Panel den Cache zu deaktivieren, damit Weiterleitungen nicht darin gespeichert werden. +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 Routen beeinflusst die Geschwindigkeit des Routers. Ihre Anzahl sollte definitiv nicht mehrere Dutzend überschreiten. Wenn Ihre Website eine zu komplizierte URL-Struktur hat, können Sie einen maßgeschneiderten [#Eigener Router] schreiben. +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. -Wenn der Router keine Abhängigkeiten hat, zum Beispiel von einer Datenbank, und seine Factory keine Argumente entgegennimmt, können wir seine kompilierte Form direkt in den DI-Container serialisieren und dadurch die Anwendung geringfügig beschleunigen. +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: @@ -608,7 +634,7 @@ routing: Eigener Router ============== -Die folgenden Zeilen sind für sehr fortgeschrittene Benutzer bestimmt. Sie können Ihren eigenen Router erstellen und ihn ganz natürlich in die Routen-Sammlung integrieren. Der Router ist eine Implementierung des Interfaces [api:Nette\Routing\Router] mit zwei Methoden: +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; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -Die Methode `match` verarbeitet die aktuelle Anfrage [$httpRequest |http:request], aus der nicht nur die URL, sondern auch Header usw. abgerufen werden können, in ein Array, das den Namen des Presenters und seine Parameter enthält. Wenn sie die Anfrage nicht verarbeiten kann, gibt sie null zurück. Bei der Verarbeitung der Anfrage müssen wir mindestens den Presenter und die Aktion zurückgeben. Der Name des Presenters ist vollständig und enthält auch eventuelle Module: +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 [ @@ -637,9 +663,9 @@ Die Methode `match` verarbeitet die aktuelle Anfrage [$httpRequest |http:request ] ``` -Die Methode `constructUrl` erstellt umgekehrt aus dem Parameter-Array die resultierende absolute URL. Dazu kann sie Informationen aus dem Parameter [`$refUrl`|api:Nette\Http\UrlScript] verwenden, was die aktuelle URL ist. +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 Routen-Sammlung fügen Sie ihn mit `add()` hinzu: +Zur Routensammlung fügen Sie ihn mit `add()` hinzu: ```php $router = new Nette\Application\Routers\RouteList; @@ -652,13 +678,13 @@ $router->addRoute(/* ... */); Eigenständige Verwendung ======================== -Unter eigenständiger Verwendung verstehen wir die Nutzung der Fähigkeiten des Routers in einer Anwendung, die Nette Application und Presenter nicht verwendet. Dafür gilt fast alles, was wir in diesem Kapitel gezeigt haben, mit diesen Unterschieden: +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 Routen-Sammlungen verwenden wir die Klasse [api:Nette\Routing\RouteList] +- für Routensammlungen verwenden wir die Klasse [api:Nette\Routing\RouteList] - als einfachen Router die Klasse [api:Nette\Routing\SimpleRouter] -- da das Paar `Presenter:action` nicht existiert, verwenden wir die [#Erweiterte Notation] +- weil das Paar `Presenter:action` nicht existiert, verwenden wir die [#Erweiterte Schreibweise] -Also erstellen wir wieder eine Methode, die uns den Router zusammenstellt, z.B.: +Wir erstellen also wieder eine Methode, die uns den Router zusammenbaut, z. B.: ```php namespace App\Core; @@ -682,35 +708,35 @@ class RouterFactory } ``` -Wenn Sie einen DI-Container verwenden, was wir empfehlen, fügen wir die Methode wieder zur Konfiguration hinzu und holen danach den Router zusammen mit der HTTP-Anfrage aus dem Container: +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 wir erstellen die Objekte direkt: +Oder erzeugen Sie die Objekte direkt: ```php $router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` -Jetzt muss der Router nur noch seine Arbeit aufnehmen: +Nun bleibt nur noch, den Router seine Arbeit tun zu lassen: ```php $params = $router->match($httpRequest); if ($params === null) { - // Es wurde keine passende Route gefunden, senden wir einen 404-Fehler + // keine passende Route gefunden, Fehler 404 senden exit; } -// Wir verarbeiten die erhaltenen Parameter +// die gewonnenen Parameter verarbeiten $controller = $params['controller']; // ... ``` -Und umgekehrt verwenden wir den Router, um einen Link zu erstellen: +Und umgekehrt den Router zum Konstruieren eines Links verwenden: ```php $params = ['controller' => 'ArticleController', 'id' => 123]; @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/de/templates.texy b/application/de/templates.texy index 4b36ccee84..5b4d74fdc4 100644 --- a/application/de/templates.texy +++ b/application/de/templates.texy @@ -2,9 +2,9 @@ Templates ********* .[perex] -Nette verwendet das Template-System [Latte |latte:]. Zum einen, weil es das sicherste Template-System für PHP ist, und zum anderen, weil es das intuitivste System ist. Sie müssen nicht viel Neues lernen, PHP-Kenntnisse und ein paar Tags reichen aus. +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. -Es ist üblich, dass eine Seite aus einer Layout-Vorlage und einer Vorlage für die jeweilige Aktion zusammengesetzt wird. So kann zum Beispiel eine Layout-Vorlage aussehen, beachten Sie die `{block}` Blöcke und den `{include}` Tag: +Ü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 @@ -20,26 +20,26 @@ Es ist üblich, dass eine Seite aus einer Layout-Vorlage und einer Vorlage für ``` -Und das wird die Aktionsvorlage sein: +Und so würde das Template der Aktion aussehen: ```latte -{block title}Homepage{/block} +{block title}Startseite{/block} {block content} -

    Homepage

    +

    Startseite

    ... {/block} ``` -Diese definiert den Block `content`, der anstelle von `{include content}` im Layout eingefügt wird, und definiert auch den Block `title` neu, der `{block title}` im Layout überschreibt. Versuchen Sie, sich das Ergebnis vorzustellen. +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. -Finden von Vorlagen -------------------- +Suche nach Templates +-------------------- -Sie müssen in den Presentern nicht angeben, welche Vorlage gerendert werden soll; das Framework leitet den Pfad selbst ab und erspart Ihnen das Schreiben. +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, in der jeder Presenter sein eigenes Verzeichnis hat, platzieren Sie die Vorlage einfach in diesem Verzeichnis unter dem Namen der Aktion (bzw. der View), d.h. für die Aktion `default` verwenden Sie die Vorlage `default.latte`: +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/ @@ -49,46 +49,46 @@ app/ └── default.latte \-- -Wenn Sie eine Struktur verwenden, bei der sich die Presenter gemeinsam in einem Verzeichnis und die Vorlagen im Ordner `templates` befinden, speichern Sie sie entweder in der Datei `..latte` oder `/.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 + ├── Home/ + │ └── default.latte ← 1. Variante + └── Home.default.latte ← 2. Variante \-- -Der Ordner `templates` kann auch eine Ebene höher platziert werden, d.h. auf derselben Ebene wie das Verzeichnis mit den Presenter-Klassen. +Das Verzeichnis `templates` kann auch eine Ebene höher liegen, also auf derselben Ebene wie das Verzeichnis mit den Presenter-Klassen. -Wenn die Vorlage nicht gefunden wird, antwortet der Presenter [mit einem Fehler 404 - Seite nicht gefunden |presenters#Fehler 404 und Co]. +Wird das Template nicht gefunden, antwortet der Presenter mit dem [Fehler 404 - Seite nicht gefunden |presenters#Fehler 404 usw.]. -Die View ändern Sie mit `$this->setView('andereView')`. Es ist auch möglich, die Datei mit der Vorlage direkt mit `$this->template->setFile('/path/to/template.latte')` anzugeben. +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 Vorlagen gesucht wird, können durch Überschreiben der Methode [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] geändert werden, die ein Array möglicher Dateinamen zurückgibt. +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. -Finden der Layout-Vorlage -------------------------- +Suche nach dem Layout-Template +------------------------------ -Nette sucht auch automatisch nach der Layout-Datei. +Nette sucht auch die Layout-Datei automatisch. -Wenn Sie eine Verzeichnisstruktur verwenden, in der jeder Presenter sein eigenes Verzeichnis hat, platzieren Sie das Layout entweder im Ordner des Presenters, wenn es nur für diesen spezifisch ist, oder eine Ebene höher, wenn es für mehrere Presenter gemeinsam genutzt wird: +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 Presenter Home + ├── @layout.latte ← nur für den Presenter Home ├── HomePresenter.php └── default.latte \-- -Wenn Sie eine Struktur verwenden, bei der sich die Presenter gemeinsam in einem Verzeichnis und die Vorlagen im Ordner `templates` befinden, wird das Layout an diesen Stellen erwartet: +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/ @@ -96,33 +96,66 @@ app/ ├── 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 + ├── Home/ + │ └── @layout.latte ← nur für Home, 1. Variante + └── Home.@layout.latte ← nur für Home, 2. Variante \-- -Wenn sich der Presenter in einem Modul befindet, wird auch auf weiteren Verzeichnisebenen höher gesucht, je nach Verschachtelung des Moduls. +Liegt der Presenter in einem Modul, wird entsprechend der Verschachtelung der Module auch in den höheren Verzeichnisebenen gesucht. -Der Name des Layouts kann mit `$this->setLayout('layoutAdmin')` geändert werden, dann wird es in der Datei `@layoutAdmin.latte` erwartet. Es ist auch möglich, die Datei mit der Layout-Vorlage direkt mit `$this->setLayout('/path/to/template.latte')` anzugeben. +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}` innerhalb der Vorlage wird die Layout-Suche deaktiviert. +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-Vorlagen gesucht wird, können durch Überschreiben der Methode [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] geändert werden, die ein Array möglicher Dateinamen zurückgibt. +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 in der Vorlage ------------------------- +Variablen im Template +--------------------- -Variablen werden an die Vorlage übergeben, indem wir sie in `$this->template` schreiben. Danach stehen sie in der Vorlage als lokale Variablen zur Verfügung: +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); ``` -So einfach können wir beliebige Variablen an Vorlagen übergeben. Bei der Entwicklung robuster Anwendungen ist es jedoch nützlicher, sich einzuschränken. Zum Beispiel, indem wir explizit eine Liste der Variablen definieren, die die Vorlage erwartet, und deren Typen. Dadurch kann PHP die Typen prüfen, die IDE korrekt Vorschläge machen und die statische Analyse Fehler aufdecken. +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. + -Und wie definieren wir eine solche Liste? Einfach in Form einer Klasse und ihrer Eigenschaften. Wir benennen sie ähnlich wie den Presenter, nur mit `Template` am Ende: +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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -Das Objekt `$this->template` im Presenter ist nun eine Instanz der Klasse `ArticleTemplate`. PHP prüft also beim Schreiben die deklarierten Typen. Ab PHP Version 8.2 wird auch auf das Schreiben in eine nicht existierende Variable hingewiesen, in früheren Versionen kann dasselbe durch die Verwendung des Traits [Nette\SmartObject |utils:smartobject] erreicht werden. +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 IDEs und statische Analyse gedacht, dank ihr funktioniert die Autovervollständigung, siehe [PhpStorm und Code-Vervollständigung für $this->template |https://blog.nette.org/de/phpstorm-und-code-completion-for-this-template]. +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 *] -Den Luxus der Autovervollständigung können Sie sich auch in Vorlagen gönnen. Installieren Sie einfach das Latte-Plugin für PhpStorm und geben Sie den Klassennamen am Anfang der Vorlage an, mehr dazu im Artikel [Latte: wie man das Typsystem benutzt |https://blog.nette.org/de/latte-wie-man-das-typsystem-benutzt]: +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} ... ``` -So funktionieren auch Vorlagen in Komponenten. Halten Sie einfach die Namenskonvention ein und erstellen Sie für eine Komponente wie `FifteenControl` eine Vorlagenklasse `FifteenTemplate`. +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 `$template` als Instanz einer anderen Klasse erstellen müssen, verwenden Sie die Methode `createTemplate()`: +Wenn Sie eine andere Parameterklasse verwenden müssen, nutzen Sie die Methode `createTemplate()`: ```php public function renderDefault(): void @@ -168,58 +203,100 @@ public function renderDefault(): void } ``` +.{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: -Standardvariablen ------------------ - -Presenter und Komponenten übergeben automatisch einige nützliche Variablen an die Vorlagen: - -- `$basePath` ist der absolute URL-Pfad zum Stammverzeichnis (z.B. `/eshop`) -- `$baseUrl` ist die absolute URL zum Stammverzeichnis (z.B. `http://localhost/eshop`) -- `$user` ist das Objekt, das den [Benutzer repräsentiert |security:authentication] -- `$presenter` ist der aktuelle Presenter -- `$control` ist die aktuelle Komponente oder der aktuelle Presenter -- `$flashes` Array von [Nachrichten |presenters#Flash-Nachrichten], die mit der Funktion `flashMessage()` gesendet wurden - -Wenn Sie Ihre eigene Vorlagenklasse verwenden, werden diese Variablen übergeben, wenn Sie eine Eigenschaft für sie erstellen. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'Mein Blog'; +} +``` -Erstellen von Links -------------------- +Links erstellen +--------------- -In der Vorlage werden Links zu anderen Presentern & Aktionen auf diese Weise erstellt: +Im Template werden Links auf andere Presenter & Aktionen so erstellt: ```latte Produktdetail ``` -Das Attribut `n:href` ist sehr praktisch für HTML-Tags ``. Wenn wir den Link an anderer Stelle ausgeben möchten, zum Beispiel im Text, verwenden wir `{link}`: +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 Adresse ist: {link Home:default} +Die URL lautet: {link Home:default} ``` -Weitere Informationen finden Sie im Kapitel [Erstellen von URL-Links|creating-links]. +Mehr dazu finden Sie im Kapitel [Erstellen von URL-Links|creating-links]. Eigene Filter, Tags usw. ------------------------ -Das Latte-Template-System kann um eigene Filter, Funktionen, Tags usw. erweitert werden. Dies kann direkt in der Methode `render` oder `beforeRender()` geschehen: +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 -public function beforeRender(): void +protected function beforeRender(): void { - // Hinzufügen eines Filters - $this->template->addFilter('foo', /* ... */); + // 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: - // oder wir konfigurieren direkt das Latte\Engine Objekt +```php +protected function beforeRender(): void +{ $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); + $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` -Latte in Version 3 bietet einen fortgeschritteneren Weg, nämlich die Erstellung einer [Extension |latte:extending-latte#Latte Extension] für jedes Webprojekt. Ein kurzes Beispiel einer solchen Klasse: +**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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -Wir registrieren sie über die [Konfiguration |configuration#Latte-Templates]: +Registrieren Sie die Extension über die [Konfiguration |configuration#Latte-Templates]: ```neon latte: @@ -263,13 +345,27 @@ latte: - 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) . ' €'); +}; +``` + -Übersetzung ------------ +Übersetzen +---------- -Wenn Sie eine mehrsprachige Anwendung programmieren, müssen Sie wahrscheinlich einige Texte in der Vorlage in verschiedenen Sprachen ausgeben. Zu diesem Zweck definiert das Nette Framework ein Übersetzungs-Interface [api:Nette\Localization\Translator], das nur eine Methode `translate()` hat. Diese nimmt die Nachricht `$message` entgegen, die normalerweise eine Zeichenkette ist, sowie beliebige weitere Parameter. Die Aufgabe besteht darin, die übersetzte Zeichenkette zurückzugeben. In Nette gibt es keine Standardimplementierung; Sie können je nach Bedarf aus mehreren fertigen Lösungen wählen, die Sie auf [Componette |https://componette.org/search/localization] finden. In deren Dokumentation erfahren Sie, wie Sie den Translator konfigurieren. +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. -Für Vorlagen kann ein Translator, den [wir uns übergeben lassen |dependency-injection:passing-dependencies], mit der Methode `setTranslator()` festgelegt werden: +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 @@ -279,7 +375,7 @@ protected function beforeRender(): void } ``` -Alternativ kann der Translator über die [Konfiguration |configuration#Latte-Templates] eingestellt werden: +Alternativ lässt sich der Translator über die [Konfiguration |configuration#Latte-Templates] setzen: ```neon latte: @@ -287,7 +383,7 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Danach kann der Translator beispielsweise als Filter `|translate` verwendet werden, einschließlich zusätzlicher Parameter, die an die Methode `translate()` übergeben werden (siehe `foo, bar`): +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} @@ -303,14 +399,14 @@ Oder als Unterstrich-Tag: {_$item, foo, bar} ``` -Für die Übersetzung eines Vorlagenabschnitts gibt es den paarweisen Tag `{translate}` (seit Latte 2.11, früher wurde der Tag `{_}` verwendet): +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}Bestellung{/translate} -{translate foo, bar}Bestellung{/translate} +{translate}Bestellen{/translate} +{translate foo, bar}Bestellen{/translate} ``` -Der Translator wird standardmäßig zur Laufzeit beim Rendern der Vorlage aufgerufen. Latte Version 3 kann jedoch alle statischen Texte bereits während der Kompilierung der Vorlage übersetzen. Dadurch wird Leistung gespart, da jede Zeichenkette nur einmal übersetzt wird und die resultierende Übersetzung in die kompilierte Form geschrieben wird. Im Cache-Verzeichnis entstehen so mehrere kompilierte Versionen der Vorlage, eine für jede Sprache. Dazu genügt es, die Sprache als zweiten Parameter anzugeben: +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 @@ -320,4 +416,4 @@ protected function beforeRender(): void } ``` -Mit statischem Text ist z.B. `{_'hello'}` oder `{translate}hello{/translate}` gemeint. Nicht-statische Texte, wie z.B. `{_$foo}`, werden weiterhin zur Laufzeit übersetzt. +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 new file mode 100644 index 0000000000..a3a0914585 --- /dev/null +++ b/application/de/upgrading.texy @@ -0,0 +1,47 @@ +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/el/@home.texy b/application/el/@home.texy deleted file mode 100644 index 1abad7558e..0000000000 --- a/application/el/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Η Nette Application είναι ο πυρήνας του Nette Framework, παρέχοντας ισχυρά εργαλεία για τη δημιουργία σύγχρονων web εφαρμογών. Προσφέρει μια σειρά από εξαιρετικά χαρακτηριστικά που διευκολύνουν σημαντικά την ανάπτυξη και βελτιώνουν την ασφάλεια και τη συντηρησιμότητα του κώδικα. - - -Εγκατάσταση ------------ - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Γιατί να επιλέξετε την Nette Application; ------------------------------------------ - -Το Nette ήταν πάντα πρωτοπόρο στον τομέα των web τεχνολογιών. - -**Αμφίδρομος router:** Το Nette διαθέτει ένα προηγμένο σύστημα δρομολόγησης, το οποίο είναι μοναδικό για την αμφίδρομη φύση του - όχι μόνο μεταφράζει τα URL σε ενέργειες (actions) της εφαρμογής, αλλά μπορεί επίσης να δημιουργήσει αντίστροφα διευθύνσεις URL. Αυτό σημαίνει ότι: -- Μπορείτε να αλλάξετε τη δομή των URL ολόκληρης της εφαρμογής ανά πάσα στιγμή χωρίς να χρειάζεται να επεξεργαστείτε τα templates -- Τα URL κανονικοποιούνται αυτόματα, γεγονός που βελτιώνει το SEO -- Η δρομολόγηση ορίζεται σε ένα σημείο, αντί να είναι διάσπαρτη σε annotations - -**Components και signals:** Το ενσωματωμένο σύστημα component, εμπνευσμένο από το Delphi και το React.js, είναι εντελώς μοναδικό μεταξύ των PHP frameworks: -- Επιτρέπει τη δημιουργία επαναχρησιμοποιήσιμων στοιχείων UI -- Υποστηρίζει την ιεραρχική σύνθεση components -- Προσφέρει κομψή επεξεργασία αιτημάτων AJAX χρησιμοποιώντας signals -- Πλούσια βιβλιοθήκη έτοιμων components στο [Componette](https://componette.org) - -**AJAX και snippets:** Το Nette παρουσίασε έναν επαναστατικό τρόπο εργασίας με AJAX ήδη από το 2009, πολύ πριν από παρόμοιες λύσεις όπως το Hotwire για Ruby on Rails ή το Symfony UX Turbo: -- Τα snippets επιτρέπουν την ενημέρωση μόνο τμημάτων της σελίδας χωρίς την ανάγκη γραφής JavaScript -- Αυτόματη ενσωμάτωση με το σύστημα component -- Έξυπνη ακύρωση (invalidation) τμημάτων σελίδων -- Ελάχιστη ποσότητα μεταφερόμενων δεδομένων - -**Διαισθητικά templates [Latte|latte:]:** Το ασφαλέστερο σύστημα templating για PHP με προηγμένες λειτουργίες: -- Αυτόματη προστασία από XSS με context-aware escaping -- Επεκτασιμότητα μέσω προσαρμοσμένων φίλτρων, συναρτήσεων και tags -- Κληρονομικότητα templates και snippets για AJAX -- Εξαιρετική υποστήριξη PHP 8.x με σύστημα τύπων - -**Dependency Injection:** Το Nette αξιοποιεί πλήρως το Dependency Injection: -- Αυτόματη μεταβίβαση εξαρτήσεων (autowiring) -- Διαμόρφωση μέσω σαφούς μορφής NEON -- Υποστήριξη για factories component - - -Κύρια πλεονεκτήματα -------------------- - -- **Ασφάλεια**: Αυτόματη άμυνα έναντι [ευπαθειών |nette:vulnerability-protection] όπως XSS, CSRF, κ.λπ. -- **Παραγωγικότητα**: Λιγότερη πληκτρολόγηση, περισσότερες λειτουργίες χάρη στον έξυπνο σχεδιασμό -- **Debugging**: [Tracy debugger |tracy:] με πίνακα δρομολόγησης -- **Απόδοση**: Έξυπνη cache, lazy loading components -- **Ευελιξία**: Εύκολη τροποποίηση των URL ακόμη και μετά την ολοκλήρωση της εφαρμογής -- **Components**: Μοναδικό σύστημα επαναχρησιμοποιήσιμων στοιχείων UI -- **Σύγχρονο**: Πλήρης υποστήριξη PHP 8.4+ και συστήματος τύπων - - -Ξεκινώντας ----------- - -1. [Πώς λειτουργούν οι εφαρμογές; |how-it-works] - Κατανόηση της βασικής αρχιτεκτονικής -2. [Presenters |presenters] - Εργασία με presenters και actions -3. [Templates |templates] - Δημιουργία templates στο Latte -4. [Δρομολόγηση |routing] - Διαμόρφωση διευθύνσεων URL -5. [Διαδραστικά components |components] - Χρήση του συστήματος component - - -Συμβατότητες με PHP -------------------- - -| έκδοση | συμβατό με PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 - -Ισχύει για την τελευταία έκδοση patch. diff --git a/application/el/@left-menu.texy b/application/el/@left-menu.texy deleted file mode 100644 index 2f84a69e5d..0000000000 --- a/application/el/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Πώς λειτουργούν οι εφαρμογές; |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Templates |templates] -- [Δομή Καταλόγων |directory-structure] -- [Δρομολόγηση |routing] -- [Δημιουργία συνδέσμων URL |creating-links] -- [Διαδραστικά Components |components] -- [AJAX & snippets |ajax] -- [Multiplier] -- [Διαμόρφωση |configuration] - - -Περαιτέρω ανάγνωση -****************** -- [Γιατί να χρησιμοποιήσετε το Nette; |www:10-reasons-why-nette] -- [Εγκατάσταση |nette:installation] -- [Γράφουμε την πρώτη εφαρμογή! |quickstart:] -- [Οδηγοί και διαδικασίες |best-practices:] -- [Αντιμετώπιση προβλημάτων |nette:troubleshooting] diff --git a/application/el/@meta.texy b/application/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/application/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/application/el/ajax.texy b/application/el/ajax.texy deleted file mode 100644 index 5158f9a656..0000000000 --- a/application/el/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & Snippets -*************** - -
    - -Στην εποχή των σύγχρονων διαδικτυακών εφαρμογών, όπου η λειτουργικότητα συχνά κατανέμεται μεταξύ του διακομιστή και του προγράμματος περιήγησης, το AJAX είναι ένα απαραίτητο συνδετικό στοιχείο. Ποιες επιλογές μας προσφέρει το Nette Framework σε αυτόν τον τομέα; -- αποστολή τμημάτων του template, τα λεγόμενα snippets -- μεταβίβαση μεταβλητών μεταξύ PHP και JavaScript -- εργαλεία για την αποσφαλμάτωση αιτήσεων AJAX - -
    - - -Αίτηση AJAX -=========== - -Μια αίτηση AJAX δεν διαφέρει ουσιαστικά από μια κλασική αίτηση HTTP. Καλείται ένας presenter με συγκεκριμένες παραμέτρους. Και εξαρτάται από τον presenter πώς θα ανταποκριθεί στην αίτηση - μπορεί να επιστρέψει δεδομένα σε μορφή JSON, να στείλει ένα τμήμα κώδικα HTML, ένα έγγραφο XML κ.λπ. - -Στην πλευρά του προγράμματος περιήγησης, αρχικοποιούμε την αίτηση AJAX χρησιμοποιώντας τη συνάρτηση `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // επεξεργασία της απάντησης -}); -``` - -Στην πλευρά του διακομιστή, αναγνωρίζουμε μια αίτηση AJAX χρησιμοποιώντας τη μέθοδο `$httpRequest->isAjax()` της υπηρεσίας [που ενσωματώνει την αίτηση HTTP |http:request]. Χρησιμοποιεί την κεφαλίδα HTTP `X-Requested-With` για την ανίχνευση, γι' αυτό είναι σημαντικό να την στέλνετε. Μέσα στον presenter, μπορείτε να χρησιμοποιήσετε τη μέθοδο `$this->isAjax()`. - -Αν θέλετε να στείλετε δεδομένα σε μορφή JSON, χρησιμοποιήστε τη μέθοδο [`sendJson()` |presenters#Αποστολή απάντησης]. Η μέθοδος τερματίζει επίσης τη δραστηριότητα του presenter. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Αν σκοπεύετε να απαντήσετε με ένα ειδικό template σχεδιασμένο για AJAX, μπορείτε να το κάνετε ως εξής: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -Το πιο ισχυρό εργαλείο που προσφέρει το Nette για τη σύνδεση του διακομιστή με τον client είναι τα snippets. Χάρη σε αυτά, μπορείτε να μετατρέψετε μια συνηθισμένη εφαρμογή σε μια εφαρμογή AJAX με ελάχιστη προσπάθεια και λίγες γραμμές κώδικα. Το παράδειγμα Fifteen, του οποίου ο κώδικας βρίσκεται στο [GitHub |https://github.com/nette-examples/fifteen], δείχνει πώς λειτουργεί όλο αυτό. - -Τα snippets, ή αποσπάσματα, επιτρέπουν την ενημέρωση μόνο τμημάτων της σελίδας, αντί για την επαναφόρτωση ολόκληρης της σελίδας. Αυτό δεν είναι μόνο ταχύτερο και πιο αποτελεσματικό, αλλά παρέχει επίσης μια πιο άνετη εμπειρία χρήστη. Τα snippets μπορεί να σας θυμίζουν το Hotwire για Ruby on Rails ή το Symfony UX Turbo. Είναι ενδιαφέρον ότι το Nette εισήγαγε τα snippets 14 χρόνια νωρίτερα. - -Πώς λειτουργούν τα snippets; Κατά την πρώτη φόρτωση της σελίδας (αίτηση μη-AJAX), φορτώνεται ολόκληρη η σελίδα, συμπεριλαμβανομένων όλων των snippets. Όταν ο χρήστης αλληλεπιδρά με τη σελίδα (π.χ. κάνει κλικ σε ένα κουμπί, υποβάλλει μια φόρμα κ.λπ.), αντί να φορτωθεί ολόκληρη η σελίδα, γίνεται μια αίτηση AJAX. Ο κώδικας στον presenter εκτελεί την ενέργεια και αποφασίζει ποια snippets πρέπει να ενημερωθούν. Το Nette αποδίδει αυτά τα snippets και τα στέλνει με τη μορφή ενός πίνακα σε μορφή JSON. Ο κώδικας χειρισμού στο πρόγραμμα περιήγησης εισάγει τα ληφθέντα snippets πίσω στη σελίδα. Έτσι, μεταδίδεται μόνο ο κώδικας των αλλαγμένων snippets, εξοικονομώντας εύρος ζώνης και επιταχύνοντας τη φόρτωση σε σύγκριση με τη μετάδοση του περιεχομένου ολόκληρης της σελίδας. - - -Naja ----- - -Για τον χειρισμό των snippets στην πλευρά του προγράμματος περιήγησης, χρησιμοποιείται η [βιβλιοθήκη Naja |https://naja.js.org]. [Εγκαταστήστε την |https://naja.js.org/#/guide/01-install-setup-naja] ως πακέτο node.js (για χρήση με εφαρμογές Webpack, Rollup, Vite, Parcel και άλλες): - -```shell -npm install naja -``` - -…ή εισάγετέ την απευθείας στο template της σελίδας: - -```latte - -``` - -Πρώτα, πρέπει να [αρχικοποιήσετε |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] τη βιβλιοθήκη: - -```js -naja.initialize(); -``` - -Για να μετατρέψετε έναν συνηθισμένο σύνδεσμο (signal) ή την υποβολή μιας φόρμας σε αίτηση AJAX, απλά επισημάνετε τον σχετικό σύνδεσμο, φόρμα ή κουμπί με την κλάση `ajax`: - -```latte -Go - - - - - -ή - -
    - -
    -``` - - -Επανασχεδίαση Snippets ----------------------- - -Κάθε αντικείμενο της κλάσης [Control |components] (συμπεριλαμβανομένου του ίδιου του Presenter) παρακολουθεί εάν έχουν γίνει αλλαγές που απαιτούν την επανασχεδίασή του. Η μέθοδος `redrawControl()` χρησιμοποιείται για αυτό: - -```php -public function handleLogin(string $user): void -{ - // μετά τη σύνδεση, το σχετικό τμήμα πρέπει να επανασχεδιαστεί - $this->redrawControl(); - // ... -} -``` - -Το Nette επιτρέπει ακόμη πιο λεπτομερή έλεγχο του τι πρέπει να επανασχεδιαστεί. Η αναφερόμενη μέθοδος μπορεί να δεχτεί το όνομα του snippet ως όρισμα. Έτσι, μπορείτε να ακυρώσετε (δηλαδή: να επιβάλετε την επανασχεδίαση) σε επίπεδο τμημάτων του template. Εάν ακυρωθεί ολόκληρο το component, κάθε snippet του θα επανασχεδιαστεί επίσης: - -```php -// ακυρώνει το snippet 'header' -$this->redrawControl('header'); -``` - - -Snippets στο Latte ------------------- - -Η χρήση snippets στο Latte είναι εξαιρετικά εύκολη. Για να ορίσετε ένα τμήμα του template ως snippet, απλά περικλείστε το με τις ετικέτες `{snippet}` και `{/snippet}`: - -```latte -{snippet header} -

    Hello ...

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

    Hello ...

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

    Ψηφίστε

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

    {$item->title}

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

    Hello

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

    Homepage

    -... -{/block} -``` - -Αυτό ορίζει το μπλοκ `content`, το οποίο εισάγεται στη θέση του `{include content}` στη διάταξη, και επίσης επαναπροσδιορίζει το μπλοκ `title`, το οποίο αντικαθιστά το `{block title}` στη διάταξη. Προσπαθήστε να φανταστείτε το αποτέλεσμα. - - -Αναζήτηση προτύπου ------------------- - -Δεν χρειάζεται να καθορίσετε στους presenters ποιο πρότυπο πρέπει να αποδοθεί, το framework θα βρει τη διαδρομή μόνο του και θα σας γλιτώσει από το γράψιμο. - -Αν χρησιμοποιείτε μια δομή καταλόγων όπου κάθε presenter έχει τον δικό του κατάλογο, απλά τοποθετήστε το πρότυπο σε αυτόν τον κατάλογο με το όνομα της action (ή του view), δηλαδή για την action `default` χρησιμοποιήστε το πρότυπο `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Αν χρησιμοποιείτε μια δομή όπου οι presenters βρίσκονται μαζί σε έναν κατάλογο και τα πρότυπα στον φάκελο `templates`, αποθηκεύστε το είτε στο αρχείο `..latte` είτε στο `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1η παραλλαγή - └── Home/ - └── default.latte ← 2η παραλλαγή -\-- - -Ο κατάλογος `templates` μπορεί επίσης να βρίσκεται ένα επίπεδο πιο πάνω, δηλαδή στο ίδιο επίπεδο με τον κατάλογο με τις κλάσεις των presenters. - -Αν το πρότυπο δεν βρεθεί, ο presenter απαντά με [σφάλμα 404 - η σελίδα δεν βρέθηκε |presenters#Σφάλμα 404 κ.λπ]. - -Μπορείτε να αλλάξετε το view χρησιμοποιώντας το `$this->setView('jineView')`. Μπορείτε επίσης να καθορίσετε απευθείας το αρχείο με το πρότυπο χρησιμοποιώντας το `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Τα αρχεία όπου αναζητούνται τα πρότυπα μπορούν να αλλάξουν αντικαθιστώντας τη μέθοδο [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών ονομάτων αρχείων. - - -Αναζήτηση προτύπου διάταξης ---------------------------- - -Το Nette αναζητά επίσης αυτόματα το αρχείο με τη διάταξη. - -Αν χρησιμοποιείτε μια δομή καταλόγων όπου κάθε presenter έχει τον δικό του κατάλογο, τοποθετήστε τη διάταξη είτε στον φάκελο με τον presenter, αν είναι συγκεκριμένη μόνο γι' αυτόν, είτε ένα επίπεδο πιο πάνω, αν είναι κοινή για πολλούς presenters: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← κοινή διάταξη - └── Home/ - ├── @layout.latte ← μόνο για τον presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Αν χρησιμοποιείτε μια δομή όπου οι presenters βρίσκονται μαζί σε έναν κατάλογο και τα πρότυπα στον φάκελο `templates`, η διάταξη θα αναμένεται σε αυτές τις θέσεις: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← κοινή διάταξη - ├── Home.@layout.latte ← μόνο για Home, 1η παραλλαγή - └── Home/ - └── @layout.latte ← μόνο για Home, 2η παραλλαγή -\-- - -Αν ο presenter βρίσκεται σε ένα module, η αναζήτηση θα γίνει και σε περαιτέρω επίπεδα καταλόγων, ανάλογα με την ένθεση του module. - -Το όνομα της διάταξης μπορεί να αλλάξει χρησιμοποιώντας το `$this->setLayout('layoutAdmin')` και τότε θα αναμένεται στο αρχείο `@layoutAdmin.latte`. Μπορείτε επίσης να καθορίσετε απευθείας το αρχείο με το πρότυπο διάταξης χρησιμοποιώντας το `$this->setLayout('/path/to/template.latte')`. - -Χρησιμοποιώντας το `$this->setLayout(false)` ή την ετικέτα `{layout none}` μέσα στο πρότυπο, η αναζήτηση διάταξης απενεργοποιείται. - -.[note] -Τα αρχεία όπου αναζητούνται τα πρότυπα διάταξης μπορούν να αλλάξουν αντικαθιστώντας τη μέθοδο [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών ονομάτων αρχείων. - - -Μεταβλητές στο πρότυπο ----------------------- - -Μεταβιβάζουμε μεταβλητές στο πρότυπο γράφοντάς τες στο `$this->template` και στη συνέχεια τις έχουμε διαθέσιμες στο πρότυπο ως τοπικές μεταβλητές: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Με αυτόν τον απλό τρόπο, μπορούμε να μεταβιβάσουμε οποιεσδήποτε μεταβλητές στα πρότυπα. Ωστόσο, κατά την ανάπτυξη στιβαρών εφαρμογών, είναι συνήθως πιο χρήσιμο να περιοριστούμε. Για παράδειγμα, ορίζοντας ρητά μια λίστα μεταβλητών που αναμένει το πρότυπο και τους τύπους τους. Χάρη σε αυτό, η PHP θα μπορεί να ελέγχει τους τύπους, το IDE να προτείνει σωστά και η στατική ανάλυση να εντοπίζει σφάλματα. - -Και πώς ορίζουμε μια τέτοια λίστα; Απλά με τη μορφή μιας κλάσης και των ιδιοτήτων της. Την ονομάζουμε παρόμοια με τον presenter, απλώς με το `Template` στο τέλος: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // και άλλες μεταβλητές -} -``` - -Το αντικείμενο `$this->template` στον presenter θα είναι τώρα μια παρουσία της κλάσης `ArticleTemplate`. Έτσι, η PHP θα ελέγχει τους δηλωμένους τύπους κατά την εγγραφή. Και από την έκδοση PHP 8.2, θα προειδοποιεί και για εγγραφή σε ανύπαρκτη μεταβλητή, σε προηγούμενες εκδόσεις το ίδιο μπορεί να επιτευχθεί χρησιμοποιώντας το trait [Nette\SmartObject |utils:smartobject]. - -Η σχολιαστική παρατήρηση `@property-read` προορίζεται για το IDE και τη στατική ανάλυση, χάρη σε αυτήν θα λειτουργεί η αυτόματη συμπλήρωση, βλ. "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Μπορείτε να απολαύσετε την πολυτέλεια της αυτόματης συμπλήρωσης και στα πρότυπα, αρκεί να εγκαταστήσετε το plugin για το Latte στο PhpStorm και να αναφέρετε στην αρχή του προτύπου το όνομα της κλάσης, περισσότερα στο άρθρο "Latte: πώς να χρησιμοποιήσετε το σύστημα τύπων":https://blog.nette.org/el/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Έτσι λειτουργούν και τα πρότυπα στα components, αρκεί απλώς να τηρήσετε τη σύμβαση ονοματοδοσίας και για ένα component π.χ. `FifteenControl` να δημιουργήσετε μια κλάση προτύπου `FifteenTemplate`. - -Αν χρειαστεί να δημιουργήσετε το `$template` ως παρουσία μιας άλλης κλάσης, χρησιμοποιήστε τη μέθοδο `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Προεπιλεγμένες μεταβλητές -------------------------- - -Οι presenters και τα components μεταβιβάζουν αυτόματα αρκετές χρήσιμες μεταβλητές στα πρότυπα: - -- `$basePath` είναι η απόλυτη διαδρομή URL προς τον ριζικό κατάλογο (π.χ. `/eshop`) -- `$baseUrl` είναι η απόλυτη URL προς τον ριζικό κατάλογο (π.χ. `http://localhost/eshop`) -- `$user` είναι το αντικείμενο [που αντιπροσωπεύει τον χρήστη |security:authentication] -- `$presenter` είναι ο τρέχων presenter -- `$control` είναι το τρέχον component ή presenter -- `$flashes` πίνακας [μηνυμάτων |presenters#Flash μηνύματα] που στάλθηκαν από τη συνάρτηση `flashMessage()` - -Αν χρησιμοποιείτε τη δική σας κλάση προτύπου, αυτές οι μεταβλητές μεταβιβάζονται αν δημιουργήσετε μια ιδιότητα γι' αυτές. - - -Δημιουργία συνδέσμων --------------------- - -Στο πρότυπο, οι σύνδεσμοι προς άλλους presenters & actions δημιουργούνται με αυτόν τον τρόπο: - -```latte -λεπτομέρεια προϊόντος -``` - -Το attribute `n:href` είναι πολύ χρήσιμο για τις ετικέτες HTML ``. Αν θέλουμε να εμφανίσουμε έναν σύνδεσμο αλλού, για παράδειγμα σε κείμενο, χρησιμοποιούμε το `{link}`: - -```latte -Η διεύθυνση είναι: {link Home:default} -``` - -Περισσότερες πληροφορίες θα βρείτε στο κεφάλαιο [Δημιουργία συνδέσμων URL|creating-links]. - - -Προσαρμοσμένα φίλτρα, ετικέτες κ.λπ. ------------------------------------- - -Το σύστημα προτύπων Latte μπορεί να επεκταθεί με προσαρμοσμένα φίλτρα, συναρτήσεις, ετικέτες κ.λπ. Αυτό μπορεί να γίνει απευθείας στη μέθοδο `render` ή `beforeRender()`: - -```php -public function beforeRender(): void -{ - // προσθήκη φίλτρου - $this->template->addFilter('foo', /* ... */); - - // ή διαμορφώνουμε απευθείας το αντικείμενο Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Το Latte στην έκδοση 3 προσφέρει έναν πιο προηγμένο τρόπο, δημιουργώντας μια [extension |latte:extending-latte#Latte Extension] για κάθε διαδικτυακό έργο. Ένα αποσπασματικό παράδειγμα μιας τέτοιας κλάσης: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Την καταχωρούμε χρησιμοποιώντας τη [διαμόρφωση |configuration#Templates Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Μετάφραση ---------- - -Αν προγραμματίζετε μια πολύγλωσση εφαρμογή, πιθανότατα θα χρειαστεί να εμφανίσετε ορισμένα κείμενα στο πρότυπο σε διαφορετικές γλώσσες. Το Nette Framework ορίζει γι' αυτόν τον σκοπό ένα interface για τη μετάφραση [api:Nette\Localization\Translator], το οποίο έχει μία μόνο μέθοδο `translate()`. Αυτή δέχεται το μήνυμα `$message`, το οποίο συνήθως είναι μια συμβολοσειρά, και οποιεσδήποτε άλλες παραμέτρους. Ο στόχος είναι να επιστρέψει τη μεταφρασμένη συμβολοσειρά. Στο Nette δεν υπάρχει προεπιλεγμένη υλοποίηση, μπορείτε να επιλέξετε ανάλογα με τις ανάγκες σας από πολλές έτοιμες λύσεις που θα βρείτε στο [Componette |https://componette.org/search/localization]. Στην τεκμηρίωσή τους θα μάθετε πώς να διαμορφώσετε τον translator. - -Στα πρότυπα μπορεί να οριστεί ένας μεταφραστής, τον οποίο [ζητάμε να μας περάσει |dependency-injection:passing-dependencies], με τη μέθοδο `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Ο Translator μπορεί εναλλακτικά να οριστεί χρησιμοποιώντας τη [διαμόρφωση |configuration#Templates Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Στη συνέχεια, ο μεταφραστής μπορεί να χρησιμοποιηθεί, για παράδειγμα, ως φίλτρο `|translate`, συμπεριλαμβανομένων των συμπληρωματικών παραμέτρων που μεταβιβάζονται στη μέθοδο `translate()` (βλ. `foo, bar`): - -```latte -{='Καλάθι'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Ή ως ετικέτα με κάτω παύλα: - -```latte -{_'Καλάθι'} -{_$item} -{_$item, foo, bar} -``` - -Για τη μετάφραση ενός τμήματος του προτύπου, υπάρχει η ζευγαρωτή ετικέτα `{translate}` (από το Latte 2.11, παλαιότερα χρησιμοποιούνταν η ετικέτα `{_}`): - -```latte -{translate}Παραγγελία{/translate} -{translate foo, bar}Παραγγελία{/translate} -``` - -Ο Translator καλείται κανονικά κατά το χρόνο εκτέλεσης κατά την απόδοση του προτύπου. Ωστόσο, το Latte έκδοση 3 μπορεί να μεταφράσει όλα τα στατικά κείμενα ήδη κατά τη μεταγλώττιση του προτύπου. Αυτό εξοικονομεί απόδοση, επειδή κάθε συμβολοσειρά μεταφράζεται μόνο μία φορά και η τελική μετάφραση γράφεται στη μεταγλωττισμένη μορφή. Έτσι, στον κατάλογο cache δημιουργούνται πολλαπλές μεταγλωττισμένες εκδόσεις του προτύπου, μία για κάθε γλώσσα. Γι' αυτό, αρκεί απλώς να αναφέρετε τη γλώσσα ως δεύτερη παράμετρο: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Στατικό κείμενο σημαίνει, για παράδειγμα, `{_'hello'}` ή `{translate}hello{/translate}`. Μη στατικά κείμενα, όπως για παράδειγμα `{_$foo}`, θα συνεχίσουν να μεταφράζονται κατά το χρόνο εκτέλεσης. diff --git a/application/en/@home.texy b/application/en/@home.texy index 3999c48492..6552aab1eb 100644 --- a/application/en/@home.texy +++ b/application/en/@home.texy @@ -58,13 +58,13 @@ Main Benefits - **Performance**: Smart cache, lazy loading of components - **Flexibility**: Easy URL modification even after application completion - **Components**: Unique system of reusable UI elements -- **Modern**: Full support for PHP 8.4+ and type system +- **Modern**: Full support for PHP 8.3+ and type system Getting Started --------------- -1. [How Applications Work? |how-it-works] - Understanding the basic architecture +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 @@ -76,10 +76,10 @@ PHP Compatibility | version | compatible with PHP |-----------------------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| 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. diff --git a/application/en/@left-menu.texy b/application/en/@left-menu.texy index 0b0defb537..64ae948bd7 100644 --- a/application/en/@left-menu.texy +++ b/application/en/@left-menu.texy @@ -1,5 +1,6 @@ Nette Application ***************** +- [Overview |@home] - [How Do Applications Work? |how-it-works] - [Bootstrapping] - [Presenters] @@ -11,6 +12,7 @@ Nette Application - [AJAX & Snippets |ajax] - [Multiplier |multiplier] - [Configuration] +- [Upgrading] Further Reading diff --git a/application/en/ajax.texy b/application/en/ajax.texy index b65ca6adce..e6a0919e4a 100644 --- a/application/en/ajax.texy +++ b/application/en/ajax.texy @@ -35,7 +35,7 @@ If you want to send data in JSON format, use the [`sendJson()` |presenters#Sendi ```php public function actionExport(): void { - $this->sendJson($this->model->getData); + $this->sendJson($this->model->getData()); } ``` @@ -55,17 +55,17 @@ public function handleClick($param): void Snippets ======== -The most powerful tool offered by Nette for connecting the server with the client are snippets. With them, you can turn an ordinary application into an AJAX one with minimal effort and just a few lines of code. The Fifteen example demonstrates how it all works, and its code can be found on [GitHub |https://github.com/nette-examples/fifteen]. +Snippets 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. +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 applications like Webpack, Rollup, Vite, Parcel, and others): +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 @@ -74,7 +74,7 @@ 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: @@ -121,6 +121,8 @@ Nette allows for even finer control over what needs to be redrawn. The method ca $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 ----------------- @@ -155,6 +157,8 @@ Snippet names can also be expressions: {/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 @@ -226,6 +230,12 @@ public function actionDelete(int $id): void ``` +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 ================== diff --git a/application/en/bootstrapping.texy b/application/en/bootstrapping.texy index 8ee9c78236..1fcef2414c 100644 --- a/application/en/bootstrapping.texy +++ b/application/en/bootstrapping.texy @@ -16,6 +16,9 @@ Bootstrapping is the process of initializing the application environment, creati 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 @@ -78,6 +81,9 @@ $application = $container->getByType(Nette\Application\Application::class); $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. @@ -124,6 +130,12 @@ $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 ==================== @@ -181,6 +193,8 @@ Configuration files are usually written in the [NEON format |neon:format]. In a .[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 @@ -208,7 +222,7 @@ If items with the same keys appear in configuration files, they will be overwrit Static Parameters ----------------- -Parameters used in configuration files can be defined [in the `parameters` section |dependency-injection:configuration#Parameters] and also passed (or overridden) using the `addStaticParameters()` method (it has an alias `addParameters()`). It is important that different parameter values will cause the generation of additional DI containers, i.e., additional classes. +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([ @@ -242,14 +256,14 @@ $this->configurator->addDynamicParameters([ Default Parameters ------------------ -You can use these static parameters in the configuration files: +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 +- `%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 @@ -278,7 +292,7 @@ $this->configurator->addServices([ Different Environments ====================== -Feel free to modify the `Bootstrap` class according to your needs. You can add parameters to the `bootWebApplication()` method to distinguish between web projects. Or we can add other methods, such as `bootTestEnvironment()` which initializes the environment for unit tests, `bootConsoleApplication()` for scripts called from the command line, etc. +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 diff --git a/application/en/components.texy b/application/en/components.texy index 615b03033b..28bbf8dac7 100644 --- a/application/en/components.texy +++ b/application/en/components.texy @@ -31,7 +31,7 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } @@ -59,6 +59,11 @@ In the template, it is possible to render a component using the [{control} |#Ren {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 =============== @@ -144,7 +149,7 @@ $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 anywhere in the parameters, all parameters will be wrapped in an array and passed as the first argument: +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'} @@ -212,9 +217,9 @@ Signals might remind you a bit of AJAX: handlers that are invoked on the current 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. +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 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. +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.'); @@ -230,8 +235,8 @@ These messages are available to the template in the `$flashes` variable as `stdC ``` -Redirection After a Signal -========================== +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. @@ -259,7 +264,7 @@ Persistent parameters are used to maintain state in components across different 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) +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 @@ -289,12 +294,12 @@ Or it can be *reset*, i.e., removed from the URL. It will then assume its defaul Persistent Components ===================== -Not only parameters but also components can be persistent. For such a component, its persistent parameters are transferred even between different actions of the presenter or between multiple presenters. Persistent components are marked with an annotation in the presenter class. For example, we mark the `calendar` and `poll` components like this: +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 -/** - * @persistent(calendar, poll) - */ +use Nette\Application\Attributes\Persistent; + +#[Persistent('calendar', 'poll')] class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -302,12 +307,12 @@ class DefaultPresenter extends Nette\Application\UI\Presenter Subcomponents within these components do not need to be marked; they become persistent too. -In PHP 8, you can also use attributes to mark persistent components: +The older annotation `@persistent` still works, but it is deprecated and triggers a warning: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -455,24 +460,38 @@ class PaginatingControl extends Control 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{Signal}`. Further processing is up to the given object. Objects inheriting from `Component` (i.e., `Control` and `Presenter`) react by trying to call the `handle{Signal}` method with the appropriate parameters. +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]. -In other words: the definition of the `handle{Signal}` function is taken, along with all parameters that came with the request, and parameters from the URL are assigned to the arguments by name, and an attempt is made to call the method. For example, the value from the `id` parameter in the URL is passed as the `$id` argument, `something` from the URL is passed as `$something`, etc. And if the method does not exist, the `signalReceived` method throws an [exception |api:Nette\Application\UI\BadSignalException]. +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 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 right at the beginning. +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. +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. +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{Signal}`, we can execute the signal manually by calling the [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] method, which takes care of handling the signal – it takes the component identified as the signal recipient (if no recipient is specified, it is the presenter itself) and sends the signal to it. +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: diff --git a/application/en/configuration.texy b/application/en/configuration.texy index a7c91cb890..09fed449ef 100644 --- a/application/en/configuration.texy +++ b/application/en/configuration.texy @@ -11,11 +11,11 @@ Application ```neon application: # show the "Nette Application" panel in Tracy BlueScreen? - debugger: ... # (bool) defaults to true + debugger: ... # (bool) enabled if Tracy is available - # will the error-presenter be called on error? - # effective only in development mode - catchExceptions: ... # (bool) defaults to true + # 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' @@ -40,6 +40,8 @@ application: 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. @@ -73,7 +75,7 @@ application: - %vendorDir%/mymodule ``` -Directory scanning can be turned off by setting the value to false. We do not recommend completely suppressing the automatic addition of presenters, as this will reduce application performance. +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 @@ -84,7 +86,7 @@ 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') defaults to true + 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 @@ -92,6 +94,12 @@ latte: # 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 @@ -102,7 +110,7 @@ latte: templateClass: App\MyTemplateClass # defaults to Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -If you are using Latte version 3, you can add new [extensions |latte:extending-latte#Latte Extension] using: +You can add new [extensions |latte:extending-latte#Latte Extension] using: ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -If you are using Latte version 2, you can register new tags either by specifying the class name or by referencing a service. By default, the `install()` method is called, but this can be changed by specifying the name of another method: - -```neon -latte: - # registration of custom Latte tags - macros: - - App\MyLatteMacros::register # static method, classname or callable - - @App\MyLatteMacrosFactory # service with install() method - - @App\MyLatteMacrosFactory::register # service with register() method - -services: - - App\MyLatteMacrosFactory -``` - Routing ======= @@ -133,7 +127,7 @@ Basic settings: ```neon routing: # show the routing panel in Tracy Bar? - debugger: ... # (bool) defaults to true + debugger: ... # (bool) enabled if Tracy is available (debug mode only) # serialize the router into the DI container cache: ... # (bool) defaults to false @@ -185,7 +179,8 @@ These services are added to the DI container: |----------------------------|---------------------------------------------------|----------------------------------------- | `application.application` | [api:Nette\Application\Application] | the [application runner |how-it-works#Nette Application] | `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | presenter factory +| `application.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] diff --git a/application/en/creating-links.texy b/application/en/creating-links.texy index f831b5e018..c841e96c2b 100644 --- a/application/en/creating-links.texy +++ b/application/en/creating-links.texy @@ -40,11 +40,11 @@ It is also possible to pass named parameters. The following link passes the para 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 (or the `(expand)` operator in Latte 2.x): +If the parameters are stored in an array, they can be expanded using the `...` operator: ```latte {var $args = [$product->id, lang => en]} -product detail +product detail ``` So-called [persistent parameters |presenters#Persistent Parameters] are also automatically passed in links. @@ -73,6 +73,14 @@ $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 ================== @@ -122,6 +130,13 @@ We can link to a specific part of the page via a so-called fragment after the ha 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 ============== @@ -181,8 +196,8 @@ To determine if we are in a specific module or its submodule, use the `isModuleC ``` -Changing Link Base .{data-version:v3.2.7} -========================================= +Changing Link Base .{data-version:3.2.7} +======================================== By default, relative links are derived from the current presenter. This can be changed using `{linkBase}`: @@ -194,12 +209,13 @@ By default, relative links are derived from the current presenter. This can be c 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] (they call the `handle()` method). Then the syntax is as follows: +The target of a link doesn't have to be just a presenter and action, but also a [signal |components#Signal] (it calls the `handle()` method). Then the syntax is as follows: ``` [//] [sub-component:]signal! [#fragment] @@ -240,8 +256,8 @@ $this->getPresenter()->link('Home:default') ``` -Aliases .{data-version:v3.2.2} -============================== +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`. @@ -267,7 +283,7 @@ They are also supported in all methods that work with links, such as `redirect() 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 determined by the static variable `Presenter::$invalidLinkMode`. It can take a combination of these values (constants): +It may happen that we create an invalid link - either because it leads to a non-existent presenter, or because it passes more parameters than the target method accepts in its signature, or when a URL cannot be generated for the target action. How to handle invalid links is 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 @@ -283,7 +299,7 @@ a[href^="#error:"] { } ``` -If we do not want warnings to be produced in the development environment, we can set the silent mode directly in the [configuration|configuration]. +If we do not want warnings to be produced in the development environment, we can suppress them directly in the [configuration|configuration]. ```neon application: @@ -294,7 +310,7 @@ application: LinkGenerator ============= -How to create links with similar comfort as the `link()` method, but without the presence of a presenter? That's what [api:Nette\Application\LinkGenerator] is for. +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. diff --git a/application/en/directory-structure.texy b/application/en/directory-structure.texy index a94b6148c7..9f837dc25f 100644 --- a/application/en/directory-structure.texy +++ b/application/en/directory-structure.texy @@ -206,7 +206,7 @@ Over time, more feed types are added, and we need more logic for them... No prob └── 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. +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. @@ -420,7 +420,7 @@ Example: What belongs in the model and what in command scripts? For example, the logic for sending a single email is part of the model, while the bulk sending of thousands of emails belongs in `Tasks/`. -Tasks are usually [run from the command line |https://blog.nette.org/en/cli-scripts-in-nette-application] or via cron. They can also be run via an HTTP request, but security must be considered. The presenter that runs the task needs to be secured, for example, only for logged-in users or with a strong token and access from allowed IP addresses. For long-running tasks, it is necessary to increase the script time limit and use `session_write_close()` to avoid locking the session. +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 @@ -470,7 +470,7 @@ Presenter Mapping Mapping defines the rules for deriving the class name from the presenter name. We specify them in the [configuration|configuration] under the key `application › mapping`. -On this page, we have shown that we place presenters in the `app/Presentation` folder (or `app/UI`). We must inform Nette of this convention in the configuration file. One line is sufficient: +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: @@ -486,7 +486,7 @@ application: 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): +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: @@ -516,7 +516,7 @@ application: It also works for deeper nested directory structures, such as the presenter `Admin:User:Edit`, where the segment with the asterisk repeats for each module level, resulting in the class `App\Presentation\Admin\User\Edit\EditPresenter`. -An alternative notation is to use an array consisting of three segments instead of a string. This notation is equivalent to the previous one: +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: diff --git a/application/en/how-it-works.texy b/application/en/how-it-works.texy index 69d2cc91b4..0f2a627fdc 100644 --- a/application/en/how-it-works.texy +++ b/application/en/how-it-works.texy @@ -159,7 +159,7 @@ Simply write the familiar `Presenter:action` pair instead of the actual URL and 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. +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 @@ -167,7 +167,7 @@ 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 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]. @@ -177,11 +177,11 @@ 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()` 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. +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. +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. diff --git a/application/en/multiplier.texy b/application/en/multiplier.texy index 2fc8d760c4..3f8e6dcbac 100644 --- a/application/en/multiplier.texy +++ b/application/en/multiplier.texy @@ -6,7 +6,7 @@ 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. +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. @@ -26,7 +26,7 @@ protected function createComponentShopForm(): Multiplier } ``` -Now, in the template, we can simply render the form for each product – and each one will truly be a unique component. +Now, in the template, we can simply render the form for each product - and each one will truly be a unique component. ```latte {foreach $items as $item} @@ -42,16 +42,16 @@ 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. +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. In our case, this will be `$item->id`, which is precisely the information we need. So, we just need to slightly modify the form creation: +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 ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; $form->addInteger('amount', 'Amount:') ->setRequired(); diff --git a/application/en/presenters.texy b/application/en/presenters.texy index daa9cc87c9..15e69e8988 100644 --- a/application/en/presenters.texy +++ b/application/en/presenters.texy @@ -60,6 +60,9 @@ Similar to the `render()` method. While `render()` is intended to pr 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. @@ -105,6 +108,26 @@ The `afterRender` method, as the name suggests again, is called after every `ren 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. @@ -186,7 +209,7 @@ Before redirection, it's possible to send [#flash messages], i.e., messages that 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. +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. @@ -207,7 +230,7 @@ In the template, these messages are available in the `$flashes` variable as `std 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 = null, int $httpCode = 404)` method. +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 @@ -226,7 +249,7 @@ The HTTP error code can be passed as the second parameter; the default is 404. T Sending JSON ============ -Example of an action method that sends data in JSON format and terminates the presenter: +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 @@ -270,7 +293,7 @@ Persistent parameters are used to maintain state across different requests. Thei 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) +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 @@ -317,6 +340,26 @@ Alternatively, it can be *reset*, i.e., removed from the URL. It will then assum ``` +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 ====================== @@ -324,7 +367,7 @@ Presenters have a built-in component system. Components are separate reusable un 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 I get components? On [Componette |https://componette.org/search/component], you'll find open-source components and many other add-ons for Nette, contributed by volunteers from the framework community. +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 @@ -366,7 +409,7 @@ The request handled by the presenter is a [api:Nette\Application\Request] object 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 and forwards to it. The method verifies that the request was created by the same user who is now logged in. If a different user logs in or the key is invalid, it does nothing, and the program continues as usual. +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]. @@ -393,25 +436,7 @@ public function actionShow(int $id, ?string $slug = null): void } ``` - -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()`. +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 @@ -447,8 +472,65 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht $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: -Access Restriction Using `#[Requires]` .{data-version:3.2.2} +```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()`. @@ -460,6 +542,9 @@ You can specify these restrictions: - 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]. @@ -468,7 +553,7 @@ 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): +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'])] @@ -477,22 +562,30 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -In version 3.1, verification is performed in `checkHttpMethod()`, which checks if the method specified in the request is included in the `$presenter->allowedMethods` array. Add a method like this: +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(); - } + 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 =============== diff --git a/application/en/routing.texy b/application/en/routing.texy index daa1c181d8..4c6c32aa86 100644 --- a/application/en/routing.texy +++ b/application/en/routing.texy @@ -198,7 +198,7 @@ $router->addRoute('[[/[/]]]', /* ... */); Wildcards --------- -In the absolute path mask, we can use the following wildcards to avoid, for example, having to write the domain into the mask, which might differ between development and production environments: +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` @@ -208,7 +208,7 @@ In the absolute path mask, we can use the following wildcards to avoid, for exam ```php $router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` @@ -244,6 +244,16 @@ $router->addRoute('/[/]', [ 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 ------------------------ @@ -265,14 +275,14 @@ $router->addRoute('/', [ Route::FilterTable => [ // string in URL => presenter 'produkt' => 'Product', - 'einkaufswagen' => 'Cart', + 'kosik' => 'Cart', 'katalog' => 'Catalog', ], ], 'action' => [ Route::Value => 'default', Route::FilterTable => [ - 'liste' => 'list', + 'seznam' => 'list', ], ], ]); @@ -300,13 +310,13 @@ $router->addRoute('//', [ 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 transformed form, so for example, in the case of a presenter, we write ``, not ``. +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 key `null`. +Besides filters intended for specific parameters, we can also define general filters that receive an associative array of all parameters, which they can modify in any way and then return. General filters are defined under the empty key. ```php use Nette\Routing\Route; @@ -314,7 +324,7 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], @@ -325,6 +335,8 @@ General filters provide the ability to modify the route's behavior in absolutely 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 ----------- @@ -333,7 +345,7 @@ One-way routes are used to maintain the functionality of old URLs that the appli ```php // old URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); +$router->addRoute('product-info', 'Product:detail', oneWay: true); // new URL /product/123 $router->addRoute('product/', 'Product:detail'); ``` @@ -363,11 +375,19 @@ $router->addRoute('', function (string $lang) { }); ``` +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()`: +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; @@ -379,7 +399,7 @@ $router->withModule('Forum') // the following routes are part of the Forum modul ->addRoute('sign:in', 'Sign:in'); ``` -An alternative is to use the `module` parameter: +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 @@ -388,6 +408,12 @@ $router->addRoute('manage//', [ ]); ``` +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 ---------- @@ -595,7 +621,7 @@ When debugging the router, we recommend opening Developer Tools in the browser ( Performance =========== -The number of routes affects the speed of the router. Their number should definitely not exceed several dozen. If your website has a too complicated URL structure, you can write a custom [#Custom Router]. +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. @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -The `match` method processes the current request [$httpRequest |http:request], from which not only the URL but also headers, etc., can be obtained, into an array containing the presenter name and its parameters. If it cannot process the request, it returns null. When processing the request, we must return at least the presenter and action. The presenter name is complete and includes any modules: +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 [ @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/en/templates.texy b/application/en/templates.texy index 2950a8f35c..8466900e39 100644 --- a/application/en/templates.texy +++ b/application/en/templates.texy @@ -56,9 +56,9 @@ app/ └── Presenters/ ├── HomePresenter.php └── templates/ - ├── Home.default.latte ← 1st variant - └── Home/ - └── default.latte ← 2nd variant + ├── 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. @@ -96,9 +96,9 @@ app/ ├── HomePresenter.php └── templates/ ├── @layout.latte ← common layout - ├── Home.@layout.latte ← only for Home, 1st variant - └── Home/ - └── @layout.latte ← only for Home, 2nd variant + ├── 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. @@ -111,18 +111,51 @@ Using `$this->setLayout(false)` or the `{layout none}` tag inside the template d The files where layout templates are looked up can be changed by overriding the [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] method, which returns an array of possible file names. -Variables in the Template -------------------------- +Template Variables +------------------ -Variables are passed to the template by writing them to `$this->template`, and then they are available in the template as local variables: +Variables are passed to templates by writing them to `$this->template`. They then become available in the template as local variables: ```php $this->template->article = $this->articles->getById($id); ``` -This way, we can easily pass any variables to templates. However, when developing robust applications, it is often more useful to impose limitations. For example, by explicitly defining a list of variables that the template expects and their types. This allows PHP to perform type checking, the IDE to provide correct autocompletion, and static analysis to detect errors. +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} -And how do we define such a list? Simply in the form of a class and its properties. We name it similarly to the presenter, but with `Template` at the end: +```php +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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -The `$this->template` object in the presenter will now be an instance of the `ArticleTemplate` class. So PHP will check the declared types upon writing. And starting from PHP 8.2, it will also warn about writing to a non-existent variable; in previous versions, the same can be achieved using the [Nette\SmartObject |utils:smartobject] trait. +The `$this->template` object in the presenter will now be an instance of the `ArticleTemplate` class. PHP will thus check the declared types when writing. + +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 intended for IDEs and static analysis; thanks to it, autocompletion will work, see "PhpStorm and code completion for $this->template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. +The `@property-read` annotation is for the IDE and static analysis, enabling code completion, see "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. [* phpstorm-completion.webp *] -You can enjoy the luxury of autocompletion in templates too; just install the Latte plugin for PhpStorm and specify the class name at the beginning of the template, more in the article "Latte: How to Use Type System":https://blog.nette.org/en/latte-how-to-use-type-system: +You can also use code completion directly in templates. Just install the Latte plugin for PhpStorm and specify the template parameter class name at the beginning of the template, more in the chapter [Latte: type system |latte:type-system]: ```latte {templateType App\Presentation\Article\ArticleTemplate} ... ``` -This is also how templates work in components; just follow the naming convention and create a template class `FifteenTemplate` for a component like `FifteenControl`. +The same applies to components. Just follow the naming convention and create a parameter class `FifteenTemplate` for a component like `FifteenControl`. -If you need to create `$template` as an instance of another class, use the `createTemplate()` method: +If you need to use a different parameter class, use the `createTemplate()` method: ```php public function renderDefault(): void @@ -168,20 +203,16 @@ public function renderDefault(): void } ``` +.{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: -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. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My blog'; +} +``` Creating Links @@ -205,21 +236,67 @@ More information can be found in the chapter [Creating URL Links|creating-links] Custom Filters, Tags, etc. -------------------------- -The Latte templating system can be extended with custom filters, functions, tags, etc. This can be done directly in the `render` or `beforeRender()` method: +The Latte templating system can be extended with custom filters, functions, tags, and other elements. There are three approaches available, ranging from quick ad-hoc solutions to architectural patterns for entire applications. + +**Ad-hoc in Presenter Methods** + +The quickest approach is adding filters or functions directly in presenter or component code. In presenters, the `beforeRender()` or `render()` methods work well for this: ```php -public function beforeRender(): void +protected function beforeRender(): void { // adding a filter - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - // or configure the Latte\Engine object directly + // 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->addFilterLoader(/* ... */); + $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 version 3 offers a more advanced way by creating an [extension |latte:extending-latte#Latte Extension] for each web project. Here is a brief example of such a class: +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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -We register it using [configuration |configuration#Latte Templates]: +Register the extension through [configuration |configuration#Latte Templates]: ```neon latte: @@ -263,6 +345,20 @@ latte: - App\Presentation\Accessory\LatteExtension ``` +Extensions offer several advantages: dependency injection support, access to your application's model layer, and centralized management of all extensions. They also support custom tags, providers, compiler passes, and more. + + +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 ----------- diff --git a/application/en/upgrading.texy b/application/en/upgrading.texy new file mode 100644 index 0000000000..f95527093f --- /dev/null +++ b/application/en/upgrading.texy @@ -0,0 +1,47 @@ +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. diff --git a/application/es/@home.texy b/application/es/@home.texy index 3e865c2c10..9d18759735 100644 --- a/application/es/@home.texy +++ b/application/es/@home.texy @@ -2,13 +2,13 @@ Nette Application ***************** .[perex] -Nette Application es el núcleo del framework Nette, que proporciona potentes herramientas para crear aplicaciones web modernas. Ofrece una serie de características excepcionales que facilitan significativamente el desarrollo y mejoran la seguridad y la mantenibilidad del código. +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 ----------- -Puede descargar e instalar la librería usando [Composer|best-practices:composer]: +Descargue e instale la biblioteca con [Composer|best-practices:composer]: ```shell composer require nette/application @@ -18,68 +18,68 @@ composer require nette/application ¿Por qué elegir Nette Application? ---------------------------------- -Nette siempre ha sido un pionero en el campo de las tecnologías web. +Nette ha sido siempre un pionero en tecnologías web. -**Router bidireccional:** Nette cuenta con un sistema de enrutamiento avanzado que es único por su bidireccionalidad: no solo traduce las URL en acciones de la aplicación, sino que también puede generar direcciones URL inversamente. Esto significa que: -- Puede cambiar la estructura de URL de toda la aplicación en cualquier momento sin necesidad de modificar las plantillas +**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 lugar, no disperso en anotaciones +- El enrutamiento se define en un solo sitio, no disperso en anotaciones -**Componentes y señales:** El sistema de componentes incorporado, inspirado en Delphi y React.js, es completamente excepcional entre los frameworks PHP: -- Permite crear elementos de UI reutilizables -- Soporta la composición jerárquica de componentes -- Ofrece un manejo elegante de las peticiones AJAX mediante señales -- Una rica librería de componentes listos para usar en [Componette](https://componette.org) +**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 una forma revolucionaria de trabajar con AJAX ya en 2009, mucho antes de soluciones similares como Hotwire para Ruby on Rails o Symfony UX Turbo: +**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 partes de las páginas -- Cantidad mínima de datos transferidos +- Invalidación inteligente de secciones de la página +- Transferencia mínima de datos -**Plantillas intuitivas [Latte|latte:]:** El sistema de plantillas más seguro para PHP con funciones avanzadas: -- Protección automática contra XSS con escape sensible al contexto -- Extensibilidad mediante filtros, funciones y etiquetas personalizadas +**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 para PHP 8.x con sistema de tipos +- Excelente soporte de PHP 8.x con sistema de tipos -**Inyección de dependencias:** Nette aprovecha al máximo la Inyección de Dependencias: +**Inyección de dependencias:** Nette aprovecha al máximo la inyección de dependencias: - Paso automático de dependencias (autowiring) -- Configuración mediante el claro formato NEON -- Soporte para fábricas de componentes +- Configuración con el claro formato NEON +- Soporte de factories de componentes Principales ventajas -------------------- -- **Seguridad**: Defensa automática contra [vulnerabilidades|nette:vulnerability-protection] como XSS, CSRF, etc. -- **Productividad**: Menos escritura, 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**: Fácil modificación de URL incluso después de completar la aplicación -- **Componentes**: Sistema único de elementos de UI reutilizables -- **Moderno**: Soporte completo para PHP 8.4+ y sistema de tipos +- **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 -Empezando ---------- +Primeros pasos +-------------- -1. [¿Cómo funcionan las aplicaciones? |how-it-works] - Comprensión de la arquitectura básica -2. [Presenters |presenters] - Trabajo con presenters y acciones -3. [Plantillas |templates] - Creación de plantillas en Latte -4. [Enrutamiento |routing] - Configuración de direcciones URL -5. [Componentes interactivos |components] - Uso del sistema de componentes +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 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| 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 -Válido para la última versión del parche. +Vale para las últimas versiones correctivas. diff --git a/application/es/@left-menu.texy b/application/es/@left-menu.texy index caab7237fb..3813496c5b 100644 --- a/application/es/@left-menu.texy +++ b/application/es/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** +- [Introducción |@home] - [¿Cómo funcionan las aplicaciones? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Plantillas |templates] +- [Bootstrapping|bootstrapping] +- [Presenters|presenters] +- [Plantillas|templates] - [Estructura de directorios |directory-structure] -- [Enrutamiento |routing] +- [Enrutamiento|routing] - [Creación de enlaces URL |creating-links] - [Componentes interactivos |components] -- [AJAX & snippets |ajax] +- [AJAX y snippets |ajax] - [Multiplier |multiplier] -- [Configuración |configuration] +- [Configuración|configuration] +- [Actualización|upgrading] -Lectura adicional -***************** -- [¿Por qué usar Nette? |www:10-reasons-why-nette] +Lecturas adicionales +******************** +- [¿Por qué usar Nette?|www:10-reasons-why-nette] - [Instalación |nette:installation] -- [¡Escribamos nuestra primera aplicación! |quickstart:] -- [Tutoriales y procedimientos |best-practices:] -- [Resolución de problemas |nette:troubleshooting] +- [¡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 index 1670b124ad..3798d9cda4 100644 --- a/application/es/@meta.texy +++ b/application/es/@meta.texy @@ -1 +1 @@ -{{sitename: Nette Documentación}} +{{sitename: Documentación de Nette}} diff --git a/application/es/ajax.texy b/application/es/ajax.texy index 1da5afd1e4..e7eb69f6cd 100644 --- a/application/es/ajax.texy +++ b/application/es/ajax.texy @@ -1,12 +1,12 @@ -AJAX y fragmentos -***************** +AJAX y snippets +***************
    -En la era de las aplicaciones web modernas, donde la funcionalidad a menudo se divide entre el servidor y el navegador, AJAX es un elemento de conexión esencial. ¿Qué posibilidades nos ofrece Nette Framework en esta área? -- envío de partes de la plantilla, los llamados fragmentos (snippets) -- paso de variables entre PHP y JavaScript -- herramientas para depurar peticiones AJAX +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
    @@ -14,9 +14,9 @@ En la era de las aplicaciones web modernas, donde la funcionalidad a menudo se d Petición AJAX ============= -Una petición AJAX no difiere en principio de una petición HTTP clásica. Se llama a un presenter con ciertos parámetros. Y depende del presenter cómo reaccionará a la petición: puede devolver datos en formato JSON, enviar parte del código HTML, un documento XML, etc. +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. -En el lado del navegador, inicializamos la petición AJAX usando la función `fetch()`: +Del lado del navegador iniciamos una petición AJAX con la función `fetch()`: ```js fetch(url, { @@ -24,22 +24,22 @@ fetch(url, { }) .then(response => response.json()) .then(payload => { - // procesar la respuesta + // procesa la respuesta }); ``` -En el lado del servidor, reconocemos una petición AJAX con el método `$httpRequest->isAjax()` del servicio [que encapsula la petición HTTP |http:request]. Para la detección, utiliza la cabecera HTTP `X-Requested-With`, por lo que es importante enviarla. Dentro del presenter, se puede usar el método `$this->isAjax()`. +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 desea enviar datos en formato JSON, use el método [`sendJson()` |presenters#Envío de la respuesta]. El método también finaliza la actividad del presenter. +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); + $this->sendJson($this->model->getData()); } ``` -Si planea responder con una plantilla especial diseñada para AJAX, puede hacerlo de la siguiente manera: +Si piensa responder con una plantilla especial pensada para AJAX, puede hacerlo así: ```php public function handleClick($param): void @@ -52,20 +52,20 @@ public function handleClick($param): void ``` -Fragmentos (Snippets) -===================== +Snippets +======== -El recurso más potente que ofrece Nette para conectar el servidor con el cliente son los fragmentos (snippets). Gracias a ellos, puedes convertir una aplicación ordinaria en una AJAX con un esfuerzo mínimo y unas pocas líneas de código. El ejemplo Fifteen demuestra cómo funciona todo, cuyo código puedes encontrar en [GitHub |https://github.com/nette-examples/fifteen]. +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 fragmentos, o recortes, permiten actualizar solo partes de la página, en lugar de recargar toda la página. Esto no solo es más rápido y eficiente, sino que también proporciona una experiencia de usuario más cómoda. Los fragmentos pueden recordarle a Hotwire para Ruby on Rails o Symfony UX Turbo. Curiosamente, Nette introdujo los fragmentos 14 años antes. +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 fragmentos? En la primera carga de la página (petición no AJAX), se carga toda la página, incluidos todos los fragmentos. Cuando el usuario interactúa con la página (por ejemplo, hace clic en un botón, envía un formulario, etc.), en lugar de cargar toda la página, se realiza una petición AJAX. El código en el presenter ejecuta la acción y decide qué fragmentos deben actualizarse. Nette renderiza estos fragmentos y los envía en forma de array en formato JSON. El código de manejo en el navegador inserta los fragmentos recibidos de nuevo en la página. Por lo tanto, solo se transfiere el código de los fragmentos modificados, lo que ahorra ancho de banda y acelera la carga en comparación con la transferencia del contenido de toda la página. +¿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 manejar los fragmentos en el lado del navegador, se utiliza la [librería Naja |https://naja.js.org]. [Instálela |https://naja.js.org/#/guide/01-install-setup-naja] como un paquete node.js (para usar con aplicaciones Webpack, Rollup, Vite, Parcel y otras): +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 @@ -74,25 +74,25 @@ npm install naja …o insértela directamente en la plantilla de la página: ```latte - + ``` -Primero, es necesario [inicializar |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la librería: +Primero hay que [inicializar |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la biblioteca: ```js naja.initialize(); ``` -Para convertir un enlace ordinario (señal) o el envío de un formulario en una petición AJAX, basta con marcar el enlace, formulario o botón correspondiente con la clase `ajax`: +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 -Ir +Go
    -o +or
    @@ -100,54 +100,56 @@ o ``` -Redibujar fragmentos --------------------- +Redibujar los snippets +---------------------- -Cada objeto de la clase [Control |components] (incluido el propio Presenter) registra si se han producido cambios que requieran su redibujado. Para ello se utiliza el método `redrawControl()`: +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 { - // después de iniciar sesión, es necesario redibujar la parte relevante + // tras el acceso hay que redibujar la parte correspondiente $this->redrawControl(); // ... } ``` -Nette permite un control aún más fino de lo que se debe redibujar. De hecho, el método mencionado puede aceptar el nombre del fragmento como argumento. Por lo tanto, es posible invalidar (léase: forzar el redibujado) a nivel de partes de la plantilla. Si se invalida todo el componente, también se redibujará cada uno de sus fragmentos: +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 fragmento 'header' +// 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)`. + -Fragmentos en Latte -------------------- +Snippets en Latte +----------------- -Usar fragmentos en Latte es extremadamente fácil. Si desea definir una parte de la plantilla como un fragmento, simplemente envuélvala con las etiquetas `{snippet}` y `{/snippet}`: +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} -

    Hola ...

    +

    Hello ...

    {/snippet} ``` -El fragmento crea un elemento `
    ` en la página HTML con un `id` especial generado. Al redibujar el fragmento, se actualiza el contenido de este elemento. Por lo tanto, es necesario que en la renderización inicial de la página se rendericen también todos los fragmentos, aunque puedan estar vacíos al principio. +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 fragmento con un elemento distinto de `
    ` usando un n:attribute: +También puede crear un snippet con un elemento distinto de `
    ` mediante un n:atributo: ```latte
    -

    Hola ...

    +

    Hello ...

    ``` -Áreas de fragmentos -------------------- +Áreas de snippets +----------------- -Los nombres de los fragmentos también pueden ser expresiones: +Los nombres de los snippets pueden ser también expresiones: ```latte {foreach $items as $id => $item} @@ -155,7 +157,9 @@ Los nombres de los fragmentos también pueden ser expresiones: {/foreach} ``` -De esta manera, creamos varios fragmentos `item-0`, `item-1`, etc. Si invalidáramos directamente un fragmento dinámico (por ejemplo, `item-1`), no se redibujaría nada. La razón es que los fragmentos realmente funcionan como recortes y solo se renderizan ellos mismos directamente. Sin embargo, en la plantilla no hay realmente ningún fragmento llamado `item-1`. Este se crea solo al ejecutar el código alrededor del fragmento, es decir, el bucle foreach. Por lo tanto, marcamos la parte de la plantilla que se debe ejecutar usando la etiqueta `{snippetArea}`: +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
      @@ -165,16 +169,16 @@ De esta manera, creamos varios fragmentos `item-0`, `item-1`, etc. Si invalidár
    ``` -Y hacemos que se redibuje tanto el fragmento en sí como toda el área padre: +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, es conveniente asegurarse de que el array `$items` contenga solo los elementos que se deben redibujar. +Al mismo tiempo, conviene asegurarse de que el array `$items` contenga solo los elementos que deben redibujarse. -Si incluimos otra plantilla que contiene fragmentos en la plantilla usando la etiqueta `{include}`, es necesario incluir de nuevo la inclusión de la plantilla en `snippetArea` e invalidarla junto con el fragmento: +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} @@ -195,41 +199,47 @@ $this->redrawControl('item'); ``` -Fragmentos en componentes -------------------------- +Snippets en los componentes +--------------------------- -También puede crear fragmentos en [componentes|components] y Nette los redibujará automáticamente. Pero hay una cierta limitación: para redibujar los fragmentos, llama al método `render()` sin parámetros. Por lo tanto, no funcionará pasar parámetros en la plantilla: +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} -no funcionará: +will not work: {control productGrid $arg, $arg} {control productGrid:paginator} ``` -Envío de datos de usuario -------------------------- +Enviar datos propios +-------------------- -Junto con los fragmentos, puede enviar cualquier otro dato al cliente. Simplemente escríbalos en el objeto `payload`: +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 = 'Éxito'; + $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 ================== -Si enviamos 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 también incluye el nombre del componente. El nombre completo del parámetro lo devuelve el método `getParameterId()`. +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!}); @@ -247,3 +257,9 @@ 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 index 88870cd20d..283c9e693c 100644 --- a/application/es/bootstrapping.texy +++ b/application/es/bootstrapping.texy @@ -3,19 +3,22 @@ Bootstrapping
    -El bootstrapping es el proceso de inicialización del entorno de la aplicación, creación del contenedor de inyección de dependencias (DI) e inicio de la aplicación. Discutiremos: +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 la clase Bootstrap inicializa el entorno -- cómo las aplicaciones se configuran usando archivos NEON -- cómo distinguir entre modo de producción y desarrollo +- 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, comienzan su ejecución con alguna forma de inicialización del entorno. En tiempos pasados, esto solía estar a cargo de un archivo llamado, por ejemplo, `include.inc.php`, que el archivo inicial incluía. En las aplicaciones Nette modernas, ha sido reemplazado por la clase `Bootstrap`, que como parte de la aplicación se encuentra en el archivo `app/Bootstrap.php`. Puede verse, por ejemplo, así: +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 @@ -26,9 +29,9 @@ class Bootstrap public function __construct() { $this->rootDir = dirname(__DIR__); - // El configurador es responsable de configurar el entorno de la aplicación y los servicios. + // El configurador se encarga de preparar el entorno y los servicios de la aplicación. $this->configurator = new Configurator; - // Establece el directorio para los archivos temporales generados por Nette (p. ej., plantillas compiladas) + // Establece el directorio de los archivos temporales que genera Nette (p. ej. las plantillas compiladas) $this->configurator->setTempDirectory($this->rootDir . '/temp'); } @@ -41,14 +44,14 @@ class Bootstrap private function initializeEnvironment(): void { - // Nette es inteligente y el modo de desarrollo se activa automáticamente, - // o puedes habilitarlo para una dirección IP específica descomentando la siguiente línea: + // 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 la depuración. + // Activa Tracy: la navaja suiza definitiva para depurar. $this->configurator->enableTracy($this->rootDir . '/log'); - // RobotLoader: carga automáticamente todas las clases en el directorio seleccionado + // RobotLoader: carga automáticamente todas las clases del directorio elegido $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); @@ -66,69 +69,78 @@ class Bootstrap index.php ========= -El archivo inicial en el caso de las aplicaciones web es `index.php`, que se encuentra en el [directorio público |directory-structure#Directorio público www] `www/`. Este solicita a la clase Bootstrap que inicialice el entorno y cree el contenedor DI. Luego, obtiene de él el servicio `Application`, que ejecuta la aplicación web: +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; -// Inicialización del entorno + creación del contenedor DI +// 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); -// Ejecución de la aplicación Nette y procesamiento de la petición entrante +// Arranca la aplicación Nette y procesa la petición entrante $application->run(); ``` -Como se puede ver, la clase [api:Nette\Bootstrap\Configurator] ayuda con la configuración del entorno y la creación del contenedor de inyección de dependencias (DI), que ahora presentaremos con más detalle. +.[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 vs producción -================================ +Modo de desarrollo y modo de producción +======================================= -Nette se comporta de manera diferente dependiendo de si se ejecuta en un servidor de desarrollo o 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 (Development): - - Muestra la barra de depuración de Tracy con información útil (consultas SQL, tiempo de ejecución, memoria utilizada) - - En caso de error, muestra una página de error detallada con llamadas a funciones y contenido de variables - - Actualiza automáticamente la caché al cambiar las plantillas Latte, modificar archivos de configuración, etc. +🛠️ 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 (Production): - - No muestra ninguna información de depuración, todos los errores se registran en el log - - En caso de error, muestra ErrorPresenter o una página genérica "Server Error" - - ¡La caché nunca se actualiza automáticamente! - - Optimizado para velocidad y seguridad +🚀 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 -La elección del modo se realiza por autodetección, por lo que generalmente no es necesario configurar nada ni cambiar manualmente: +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 proxy presente (es decir, su cabecera HTTP) -- modo de producción: en cualquier otro lugar +- 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 habilitar el modo de desarrollo también en otros casos, por ejemplo, para programadores que acceden desde una dirección IP específica, usamos `setDebugMode()`: +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 ``` -Definitivamente recomendamos combinar la dirección IP con una cookie. Guardamos un token secreto en la cookie `nette-debug`, por ejemplo, `secret1234`, y de esta manera activamos el modo de desarrollo para los programadores que acceden desde una dirección IP específica y que además tienen el token mencionado en la cookie: +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 completamente el modo de desarrollo, incluso para localhost: +También podemos desactivar por completo el modo de desarrollo, incluso en localhost: ```php $this->configurator->setDebugMode(false); ``` -Atención, el valor `true` activa el modo de desarrollo de forma permanente, lo cual nunca debe ocurrir en un servidor de producción. +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 facilitar la depuración, también activaremos la excelente herramienta [Tracy |tracy:]. En el modo de desarrollo, visualiza los errores y en el modo de producción, registra los errores en el directorio especificado: +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'); @@ -138,19 +150,19 @@ $this->configurator->enableTracy($this->rootDir . '/log'); Archivos temporales =================== -Nette utiliza caché para el contenedor DI, RobotLoader, plantillas, etc. Por lo tanto, es necesario establecer la ruta al directorio donde se almacenará la caché: +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, establezca [permisos de escritura |nette:troubleshooting#Configuración de permisos de directorio] para los directorios `log/` y `temp/`. +En Linux o macOS, dé [permisos de escritura |nette:troubleshooting#Establecer los permisos de los directorios] a los directorios `log/` y `temp/`. RobotLoader =========== -Generalmente, querremos cargar clases automáticamente usando [RobotLoader |robot-loader:], por lo que debemos iniciarlo y hacer que cargue clases desde el directorio donde se encuentra `Bootstrap.php` (es decir, `__DIR__`), y todos los subdirectorios: +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() @@ -158,13 +170,13 @@ $this->configurator->createRobotLoader() ->register(); ``` -Un enfoque alternativo es dejar que las clases se carguen solo a través de [Composer |best-practices:composer] cumpliendo con PSR-4. +Un enfoque alternativo es cargar las clases únicamente mediante [Composer |best-practices:composer], siguiendo PSR-4. Zona horaria ============ -A través del configurador, puede establecer la zona horaria predeterminada. +Puede fijar la zona horaria predeterminada mediante el configurador. ```php $this->configurator->setTimeZone('Europe/Prague'); @@ -174,20 +186,22 @@ $this->configurator->setTimeZone('Europe/Prague'); Configuración del contenedor DI =============================== -Parte del proceso de arranque es la creación del contenedor DI o fábrica de objetos, que es el corazón de toda la aplicación. En realidad, es una clase PHP que Nette genera y guarda en el directorio de caché. La fábrica produce los objetos clave de la aplicación y, mediante archivos de configuración, le instruimos cómo debe crearlos y configurarlos, influyendo así en el comportamiento de toda la aplicación. +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 generalmente se escriben en formato [NEON |neon:format]. En un capítulo aparte, aprenderá [qué se puede configurar |nette:configuring]. +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 el modo de desarrollo, el contenedor se actualiza automáticamente cada vez que se cambia el código o los archivos de configuración. En el modo de producción, se genera solo una vez y los cambios no se verifican para maximizar el rendimiento. +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. -Cargamos los archivos de configuración usando `addConfig()`: +Los archivos de configuración se cargan con `addConfig()`: ```php $this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` -Si queremos agregar más archivos de configuración, podemos llamar a la función `addConfig()` varias veces. +Si queremos añadir más archivos de configuración, podemos llamar varias veces a la función `addConfig()`. ```php $configDir = $this->rootDir . '/config'; @@ -198,17 +212,17 @@ if (PHP_SAPI === 'cli') { } ``` -El nombre `cli.php` no es un error tipográfico, la configuración también puede estar escrita en un archivo PHP que la devuelve como un array. +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 agregar otros archivos de configuración en la [sección `includes` |dependency-injection:configuration#Inclusión de archivos]. +También podemos añadir otros archivos de configuración en [la sección `includes` |dependency-injection:configuration#Inclusión de archivos]. -Si aparecen elementos con las mismas claves en los archivos de configuración, se sobrescribirán o, en el caso de [arrays, se fusionarán |dependency-injection:configuration#Fusión]. El archivo cargado posteriormente tiene mayor prioridad que el anterior. El archivo en el que se indica la sección `includes` tiene mayor prioridad que los archivos incluidos en él. +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 utilizados en los archivos de configuración se pueden definir en la [sección `parameters` |dependency-injection:configuration#Parámetros] y también se pueden pasar (o sobrescribir) con el método `addStaticParameters()` (tiene el alias `addParameters()`). Es importante que diferentes valores de parámetros provoquen la generación de contenedores DI adicionales, es decir, clases adicionales. +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([ @@ -216,13 +230,13 @@ $this->configurator->addStaticParameters([ ]); ``` -Al parámetro `projectId` se puede hacer referencia en la configuración con la notación habitual `%projectId%`. +El parámetro `projectId` se puede referenciar en la configuración con la notación habitual `%projectId%`. Parámetros dinámicos -------------------- -También podemos agregar parámetros dinámicos al contenedor, cuyos diferentes valores, a diferencia de los parámetros estáticos, no provocan la generación de nuevos contenedores DI. +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([ @@ -230,7 +244,7 @@ $this->configurator->addDynamicParameters([ ]); ``` -De esta manera, podemos agregar fácilmente, por ejemplo, variables de entorno, a las que luego se puede hacer referencia en la configuración con la notación `%env.variable%`. +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([ @@ -242,21 +256,22 @@ $this->configurator->addDynamicParameters([ Parámetros predeterminados -------------------------- -En los archivos de configuración, puede utilizar estos parámetros estáticos: +Puede usar estos parámetros en los archivos de configuración: -- `%appDir%` es la ruta absoluta al directorio con el archivo `Bootstrap.php` -- `%wwwDir%` es la ruta absoluta al directorio con el archivo de entrada `index.php` -- `%tempDir%` es la ruta absoluta al directorio para archivos temporales -- `%vendorDir%` es la ruta absoluta al directorio donde Composer instala las librerías +- `%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ó a través de la línea de comandos +- `%consoleMode%` indica si la petición llegó por la línea de comandos Servicios importados -------------------- -Ahora vamos más profundo. Aunque el propósito del contenedor DI es fabricar objetos, excepcionalmente puede surgir la necesidad de insertar un objeto existente en el contenedor. Hacemos esto definiendo el servicio con el indicador `imported: true`. +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: @@ -265,7 +280,7 @@ services: imported: true ``` -Y en bootstrap insertamos el objeto en el contenedor: +Y en el bootstrap insertamos el objeto en el contenedor: ```php $this->configurator->addServices([ @@ -274,10 +289,10 @@ $this->configurator->addServices([ ``` -Entorno diferente -================= +Entornos distintos +================== -No dude en modificar la clase Bootstrap según sus necesidades. Puede agregar parámetros al método `bootWebApplication()` para distinguir proyectos web. O podemos agregar otros métodos, por ejemplo, `bootTestEnvironment()`, que inicializa el entorno para pruebas unitarias, `bootConsoleApplication()` para scripts llamados desde la línea de comandos, etc. +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 diff --git a/application/es/components.texy b/application/es/components.texy index e4cce895a9..a768929b2d 100644 --- a/application/es/components.texy +++ b/application/es/components.texy @@ -3,27 +3,27 @@ Componentes interactivos
    -Los componentes son objetos reutilizables independientes que insertamos en las páginas. Pueden ser formularios, datagrids, encuestas, en realidad cualquier cosa que tenga sentido usar repetidamente. Mostraremos: +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 usar componentes? -- ¿cómo escribirlos? +- ¿cómo se usan los componentes? +- ¿cómo se escriben? - ¿qué son las señales?
    -Nette tiene incorporado un sistema de componentes. Algo similar pueden recordar los veteranos de Delphi o ASP.NET Web Forms, algo remotamente similar es la base de React o Vue.js. Sin embargo, en el mundo de los frameworks PHP, es una característica única. +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. -Mientras tanto, los componentes influyen fundamentalmente en el enfoque para la creación de aplicaciones. Puede componer páginas a partir de unidades prefabricadas. ¿Necesita un datagrid en la administración? Lo encontrará en [Componette |https://componette.org/search/component], un repositorio de complementos de código abierto (es decir, no solo componentes) para Nette y simplemente insértelo en el presenter. +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 cualquier número de componentes en el presenter. Y en algunos componentes puede insertar otros componentes. Esto crea un árbol de componentes, cuya raíz es 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 de fábrica -================== +Métodos factory +=============== -¿Cómo se insertan los componentes en el presenter y se usan posteriormente? Generalmente mediante métodos de fábrica. +¿Cómo se insertan los componentes en el presenter y cómo se usan después? Normalmente mediante métodos factory. -Una fábrica de componentes es una forma elegante de crear componentes solo cuando realmente se necesitan (lazy / on demand). Toda la magia reside en la implementación de un método llamado `createComponent()`, donde `` es el nombre del componente a crear, y que crea y devuelve el componente. +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 @@ -31,49 +31,54 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } ``` -Gracias a que todos los componentes se crean en métodos separados, el código gana en claridad. +Como todos los componentes se crean en métodos separados, el código gana en claridad. .[note] -Los nombres de los componentes siempre comienzan con una letra minúscula, aunque en el nombre del método se escriban con mayúscula. +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 fábricas directamente, se llaman solas en el momento en que usamos el componente por primera vez. Gracias a esto, el componente se crea en el momento adecuado y solo si realmente es necesario. Si no usamos el componente (por ejemplo, en una petición AJAX donde solo se transfiere una parte de la página, o al almacenar en caché la plantilla), no se crea en absoluto y ahorramos rendimiento del servidor. +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 fue la primera vez, -// se llama a createComponentPoll() que lo crea +// 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, es posible renderizar el componente usando la etiqueta [{control} |#Renderizado]. Por lo tanto, no es necesario pasar manualmente los componentes a la plantilla. +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 -

    Votar

    +

    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 suelen utilizar una técnica fresca, que nos gusta llamar estilo Hollywood. Seguramente conoce la frase célebre que tan a menudo escuchan los participantes en las audiciones de cine: "No nos llame, nosotros le llamaremos". Y de eso se trata precisamente. +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 envió el formulario?", "¿fue válido?" o "¿presionó el usuario este botón?"), le dice al framework "cuando suceda, llama a este método" y deja el resto del trabajo en él. Si programa en JavaScript, conoce íntimamente este estilo de programación. Escribe funciones que se llaman cuando ocurre un evento determinado. Y el lenguaje les pasa los parámetros apropiados. +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 sobre la escritura de aplicaciones. Cuantas más tareas pueda dejar en manos del framework, menos trabajo tendrá usted. Y menos cosas podrá olvidar. +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. -Escribiendo un componente -========================= +Escribir un componente +====================== -Bajo el término componente, generalmente nos referimos a un descendiente de la clase [api:Nette\Application\UI\Control]. (Por lo tanto, sería más preciso usar el término "controls", pero "controles" tiene un significado completamente diferente en español y más bien se ha impuesto "componentes".) El propio presenter [api:Nette\Application\UI\Presenter] es, por cierto, también un descendiente de la clase `Control`. +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; @@ -87,12 +92,12 @@ class PollControl extends Control Renderizado =========== -Ya sabemos que para renderizar un componente se usa la etiqueta `{control componentName}`. Esta en realidad llama al método `render()` del componente, en el que nos encargamos del renderizado. Tenemos disponible, al igual que en el presenter, una [plantilla Latte|templates] en la variable `$this->template`, a la que pasamos parámetros. A diferencia del presenter, debemos indicar el archivo con la plantilla y hacer que se renderice: +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 { - // insertamos algunos parámetros en la plantilla + // pasamos algunos parámetros a la plantilla $this->template->param = $value; // y la renderizamos $this->template->render(__DIR__ . '/poll.latte'); @@ -112,7 +117,7 @@ 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 nuestro propio método de renderizado, aquí en el ejemplo, por ejemplo, `renderPaginator()`: +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 @@ -121,36 +126,36 @@ public function renderPaginator(): void } ``` -Y en la plantilla, luego la llamamos usando: +Y en la plantilla lo invocamos después con: ```latte {control poll:paginator} ``` -Para una mejor comprensión, es bueno saber cómo se traduce esta etiqueta a PHP. +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 como: +se traduce a: ```php $control->getComponent('poll')->render(); $control->getComponent('poll')->renderPaginator(123, 'hello'); ``` -El método `getComponent()` devuelve el componente `poll` y sobre este componente llama al método `render()`, respectivamente `renderPaginator()` si se indica otro método de renderizado en la etiqueta después de los dos puntos. +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 cualquier lugar de los parámetros aparece **`=>`**, todos los parámetros se empaquetarán en un array y se pasarán como primer argumento: +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 como: +se traduce a: ```php $control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); @@ -162,28 +167,28 @@ Renderizado de un subcomponente: {control cartControl-someForm} ``` -se traduce como: +se traduce a: ```php $control->getComponent("cartControl-someForm")->render(); ``` -Los componentes, al igual que los presenters, pasan automáticamente varias variables útiles a las plantillas: +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 el objeto [que representa al usuario |security:authentication] +- `$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` array de [mensajes |#Mensajes flash] enviados por la función `flashMessage()` +- `$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, ¿qué pasa si solo queremos realizar una acción en la **página actual**? Por ejemplo, cambiar el orden de las columnas en una tabla; eliminar un elemento; cambiar el modo claro/oscuro; enviar un formulario; votar en una encuesta; etc. +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 peticiones se llaman señales. Y de manera similar a como 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) está relacionado puramente con los presenters, las señales se aplican a todos los componentes. Y, por lo tanto, también a los presenters, porque `UI\Presenter` es un descendiente de `UI\Control`. +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 @@ -192,36 +197,36 @@ public function handleClick(int $x, int $y): void } ``` -El enlace que llama a la señal se crea de la manera habitual, es decir, en la plantilla con el atributo `n:href` o la etiqueta `{link}`, en el código con el método `link()`. Más en el capítulo [Creación de enlaces URL |creating-links#Enlaces a 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 -haz clic aquí +click here ``` -La señal siempre se llama en el presenter y la acción actuales, no es posible invocarla en otro presenter u otra acción. +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. -Por lo tanto, la señal provoca la recarga de la página exactamente igual que en la petición original, solo que además llama al método de manejo de la señal con los parámetros correspondientes. Si el método no existe, se lanza una excepción [api:Nette\Application\UI\BadSignalException], que se muestra al usuario como una página de error 403 Forbidden. +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. -Fragmentos (Snippets) y AJAX -============================ +Snippets y AJAX +=============== -Las señales pueden recordarle un poco a AJAX: manejadores que se invocan en la página actual. Y tiene razón, las señales realmente se llaman a menudo usando AJAX y posteriormente transferimos al navegador solo las partes modificadas de la página. Es decir, los llamados fragmentos (snippets). Encontrará más información en la [página dedicada a AJAX |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 ============== -El componente tiene su propio almacenamiento de mensajes flash independiente del presenter. Son mensajes que, por ejemplo, informan sobre el resultado de una operación. Una característica importante de los mensajes flash es que están disponibles en la plantilla incluso después de una redirección. Incluso después de mostrarse, permanecen activos durante otros 30 segundos, por ejemplo, en caso de que el usuario actualice la página debido a una transmisión errónea, el mensaje no desaparecerá de inmediato. +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. -El envío lo realiza el método [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. El primer parámetro es el texto del mensaje 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 un objeto `stdClass`, al que se le puede agregar información adicional. +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('El elemento ha sido eliminado.'); -$this->redirect(/* ... */); // y redirigimos +$this->flashMessage('Item was 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` (texto del mensaje), `type` (tipo de mensaje) y pueden contener la información de usuario ya mencionada. Los renderizamos, por ejemplo, así: +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} @@ -230,19 +235,19 @@ En la plantilla, estos mensajes están disponibles en la variable `$flashes` com ``` -Redirección después de una señal -================================ +Redirección tras procesar una señal +=================================== -Después de procesar una señal de componente, a menudo sigue una redirección. Es una situación similar a la de los formularios: después de enviarlos, también redirigimos para que al actualizar la página en el navegador no se vuelvan a enviar los datos. +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 acción actuales +$this->redirect('this'); // redirige al presenter y la acción actuales ``` -Dado que el componente es un elemento reutilizable y generalmente no debería tener una vinculación directa con presenters específicos, los métodos `redirect()` y `link()` interpretan automáticamente el parámetro como una señal del componente: +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 +$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: @@ -255,11 +260,11 @@ $this->getPresenter()->redirect('Product:show'); // redirige a otro presenter/ac Parámetros persistentes ======================= -Los parámetros persistentes sirven para mantener el estado en los componentes entre diferentes peticiones. Su valor permanece igual incluso después de hacer clic en un enlace. A diferencia de los datos en la sesión, se transfieren en la URL. Y esto de forma totalmente automática, incluidos los enlaces creados en otros componentes en la misma página. +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. -Tiene, por ejemplo, un componente para paginar contenido. Puede haber varios de estos componentes en una página. Y deseamos que después de hacer clic en un enlace, todos los componentes permanezcan en su página actual. Por lo tanto, hacemos que el número de página (`page`) sea un parámetro persistente. +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 es extremadamente simple en Nette. Basta con crear una propiedad pública y marcarla con un atributo: (anteriormente se usaba `/** @persistent */`) +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 @@ -271,43 +276,43 @@ class PaginatingControl extends Control } ``` -Recomendamos indicar también el tipo de dato para la propiedad (p. ej., `int`) y puede indicar también un valor predeterminado. Los valores de los parámetros se pueden [validar |#Validación de parámetros persistentes]. +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 del parámetro persistente: +Al crear un enlace se puede cambiar el valor de un parámetro persistente: ```latte -siguiente +next ``` -O se puede *resetear*, es decir, eliminar de la URL. Entonces tomará su valor predeterminado: +O se puede *resetear*, es decir, eliminar de la URL. Entonces adoptará su valor por defecto: ```latte -resetear +reset ``` Componentes persistentes ======================== -No solo los parámetros, sino también los componentes pueden ser persistentes. En tal componente, sus parámetros persistentes se transfieren también entre diferentes acciones del presenter o entre varios presenters. Marcamos los componentes persistentes con una anotación en la clase del presenter. Por ejemplo, así marcamos los componentes `calendar` y `poll`: +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 -/** - * @persistent(calendar, poll) - */ +use Nette\Application\Attributes\Persistent; + +#[Persistent('calendar', 'poll')] class DefaultPresenter extends Nette\Application\UI\Presenter { } ``` -Los subcomponentes dentro de estos componentes no necesitan ser marcados, también se volverán persistentes. +No hace falta marcar los subcomponentes dentro de estos componentes; también pasan a ser persistentes. -En PHP 8, también puede usar atributos para marcar componentes persistentes: +La anotación antigua `@persistent` sigue funcionando, pero está obsoleta y provoca una advertencia: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -317,15 +322,15 @@ class DefaultPresenter extends Nette\Application\UI\Presenter Componentes con dependencias ============================ -¿Cómo crear componentes con dependencias sin "ensuciar" los presenters que los usarán? Gracias a las propiedades inteligentes del contenedor DI en Nette, al igual que al usar servicios clásicos, se puede dejar la mayor parte del trabajo al framework. +¿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 tiene una dependencia del servicio `PollFacade`: +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 int $id, // ID de la encuesta para la que creamos el componente private PollFacade $facade, ) { } @@ -338,11 +343,11 @@ class PollControl extends Control } ``` -Si estuviéramos escribiendo un servicio clásico, no habría nada que resolver. El contenedor DI se encargaría invisiblemente de pasar todas las dependencias. Pero con los componentes, generalmente los tratamos de tal manera que creamos su nueva instancia directamente en el presenter en los [#métodos de fábrica] `createComponent…()`. Pero pasar todas las dependencias de todos los componentes al presenter para luego pasarlas a los componentes es engorroso. Y la cantidad de código escrito… +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é simplemente no registramos el componente como un servicio clásico, lo pasamos al presenter y luego lo devolvemos en el método `createComponent…()`? Sin embargo, tal enfoque es inapropiado, porque queremos poder crear el componente incluso varias veces. +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 fábrica para el componente, es decir, una clase que nos cree el componente: +La solución correcta es escribir una factory para el componente, es decir, una clase que nos cree el componente: ```php class PollControlFactory @@ -359,7 +364,7 @@ class PollControlFactory } ``` -Así registramos la fábrica en nuestro contenedor en la configuración: +Registramos esta factory en nuestro contenedor en la configuración: ```neon services: @@ -384,7 +389,7 @@ class PollPresenter extends Nette\Application\UI\Presenter } ``` -Lo genial es que Nette DI puede [generar |dependency-injection:factory] tales fábricas simples, por lo que en lugar de todo su código, basta con escribir solo su interfaz: +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 @@ -393,21 +398,21 @@ interface PollControlFactory } ``` -Y eso es todo. Nette implementa internamente esta interfaz y la pasa al presenter, donde ya podemos usarla. Mágicamente, también agrega a nuestro componente el parámetro `$id` y la instancia de la clase `PollFacade`. +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`. -Componentes en profundidad -========================== +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, por cierto, se dedica todo este capítulo. ¿Qué capacidades exactas tiene tal componente? +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) es renderizable en la plantilla -2) sabe [qué parte suya |ajax#Fragmentos Snippets] debe renderizar en una petición AJAX (fragmentos) +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 (donde la raíz es el presenter) +5) crea una estructura jerárquica (cuya raíz es el presenter) -Cada una de estas funciones la realiza alguna de las clases de la línea de herencia. El renderizado (1 + 2) está a cargo de [api:Nette\Application\UI\Control], la inclusión en el [ciclo de vida |presenters#Ciclo de vida del presenter] (3, 4) de la clase [api:Nette\Application\UI\Component] y la creación de la estructura jerárquica (5) de las clases [Container y Component |component-model:]. +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 } @@ -428,12 +433,12 @@ Ciclo de vida del componente [* lifecycle-component.svg *] *** *Ciclo de vida del componente* .<> -Validación de parámetros persistentes -------------------------------------- +Validación de los parámetros persistentes +----------------------------------------- -Los valores de los [#parámetros persistentes] recibidos de la URL se escriben en las propiedades mediante el método `loadState()`. Este también comprueba si el tipo de dato indicado en la propiedad coincide, de lo contrario responde con un error 404 y la página no se muestra. +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, ya que pueden ser fácilmente sobrescritos por el usuario en la URL. Así, por ejemplo, verificamos si el número de página `$this->page` es mayor que 0. Una forma adecuada es sobrescribir el método mencionado `loadState()`: +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 @@ -444,7 +449,7 @@ class PaginatingControl extends Control public function loadState(array $params): void { parent::loadState($params); // aquí se establece $this->page - // sigue la verificación propia del valor: + // sigue la comprobación propia del valor: if ($this->page < 1) { $this->error(); } @@ -452,27 +457,41 @@ class PaginatingControl extends Control } ``` -El proceso opuesto, es decir, la recopilación de valores de las propiedades persistentes, está a cargo del método `saveState()`. +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 +-------------------------- -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. -Una señal provoca la recarga de la página exactamente igual que en la petición original (excepto en el caso de que se llame por AJAX) e invoca el método `signalReceived($signal)`, cuya implementación predeterminada en la clase `Nette\Application\UI\Component` intenta llamar a un método compuesto por las palabras `handle{signal}`. 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{signal}` con los parámetros correspondientes. +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]. -En otras palabras: se toma la definición de la función `handle{signal}` y todos los parámetros que llegaron con la petición, y a los argumentos se les asignan los parámetros de la URL según el nombre e intenta llamar al método dado. Por ejemplo, como parámetro `$id` se pasa el valor del parámetro `id` en la URL, como `$something` se pasa `something` de la URL, 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]. -La señal puede ser recibida por cualquier componente, presenter u objeto que implemente la interfaz `SignalReceiver` y esté conectado al árbol de componentes. +Una señal puede recibirla cualquier componente, presenter u objeto que implemente la interfaz `SignalReceiver` y esté conectado al árbol de componentes. -Los principales receptores de señales serán los `Presenters` y los componentes visuales que heredan de `Control`. La señal debe servir como una indicación para el 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 envió y debe procesar los datos, y así sucesivamente. +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 para la señal la creamos usando 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 siempre se llama en el presenter y acción actuales con los parámetros actuales, los parámetros de la señal simplemente se agregan. Además, se agrega al principio el **parámetro `?do`, que determina la señal**. +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 haber un guion en el nombre del componente; se usa para separar el nombre del componente y la señal, sin embargo, es posible anidar varios componentes de esta manera. +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()] verifica si el componente (primer argumento) es el receptor de la señal (segundo argumento). Podemos omitir el segundo argumento; entonces verifica si el componente es receptor de cualquier señal. Como segundo parámetro se puede indicar `true` y así verificar si el receptor no es solo el componente indicado, sino también cualquiera de sus descendientes. +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 anterior a `handle{signal}` podemos ejecutar la señal manualmente llamando al método [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], que se encarga de gestionar la señal: toma el componente que se determinó como receptor de la señal (si no se especifica un receptor de señal, es el propio presenter) y le envía la señal. +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: @@ -482,4 +501,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -De esta manera, la señal se ejecuta prematuramente y ya no se volverá a llamar. +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 index 9496f35bd5..9ff649801c 100644 --- a/application/es/configuration.texy +++ b/application/es/configuration.texy @@ -1,8 +1,8 @@ -Configuración de aplicaciones -***************************** +Configuración de la aplicación +****************************** .[perex] -Resumen de las opciones de configuración para las aplicaciones Nette. +Resumen de las opciones de configuración de Nette Application. Application @@ -10,62 +10,64 @@ Application ```neon application: - # mostrar el panel "Nette Application" en Tracy BlueScreen? - debugger: ... # (bool) predeterminado es true + # ¿mostrar el panel "Nette Application" en la BlueScreen de Tracy? + debugger: ... # (bool) activado si Tracy está disponible - # se llamará al error-presenter en caso de error? - # solo tiene efecto en modo de desarrollo - catchExceptions: ... # (bool) predeterminado es true + # 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) predeterminado es 'Nette:Error' + errorPresenter: Error # (string|array) el valor predeterminado es 'Nette:Error' - # define alias para presenters y acciones + # define alias para los presenters y las acciones aliases: ... - # define reglas para traducir el nombre del presenter a una clase + # define las reglas para traducir el nombre del presenter a una clase mapping: ... - # los enlaces erróneos no generan advertencias? + # ¿suprimir las advertencias de los enlaces no válidos? # solo tiene efecto en modo de desarrollo - silentLinks: ... # (bool) predeterminado es false + silentLinks: ... # (bool) el valor predeterminado es false ``` -Desde la versión 3.2 de `nette/application`, se puede definir un par de error-presenters: +Desde la versión 3.2 de `nette/application` es posible definir un par de error-presenters: ```neon application: errorPresenter: - 4xx: Error4xx # para la excepción Nette\Application\BadRequestException - 5xx: Error5xx # para otras excepciones + 4xx: Error4xx # para Nette\Application\BadRequestException + 5xx: Error5xx # para las demás excepciones ``` -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 no existe el presenter, etc.). El valor predeterminado `false` significa que Nette lanzará un error `E_USER_WARNING`. Establecerlo en `true` suprimirá este mensaje de error. En el entorno de producción, siempre se lanza `E_USER_WARNING`. Este comportamiento también se puede influir estableciendo la variable del presenter [$invalidLinkMode |creating-links#Enlaces no válidos]. +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 el enlace |creating-links#Alias] a presenters de uso frecuente. +Los [alias simplifican la referencia |creating-links#Alias] a los presenters de uso frecuente. -El [Mapeo define reglas |directory-structure#Mapeo de presenters] según las cuales se deriva el nombre de la clase a partir del nombre del presenter. +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 agrega automáticamente los presenters como servicios al contenedor DI, lo que acelera significativamente su creación. Cómo Nette busca los presenters se puede configurar: +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 presenters en el mapa de clases de Composer? - scanComposer: ... # (bool) predeterminado es true + # ¿buscar los presenters en el class map de Composer? + scanComposer: ... # (bool) el valor predeterminado es true - # máscara que debe cumplir el nombre de la clase y el archivo - scanFilter: ... # (string) predeterminado es '*Presenter' + # 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 presenters? - scanDirs: # (string[]|false) predeterminado es '%appDir%' + # ¿en qué directorios buscar los presenters? + scanDirs: # (string[]|false) el valor predeterminado es '%appDir%' - %vendorDir%/mymodule ``` -Los directorios indicados en `scanDirs` no sobrescriben el valor predeterminado `%appDir%`, sino que lo complementan, por lo que `scanDirs` contendrá ambas rutas `%appDir%` y `%vendorDir%/mymodule`. Si quisiéramos omitir el directorio predeterminado, usaríamos un [signo de exclamación |dependency-injection:configuration#Fusión], que sobrescribe el valor: +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: @@ -73,36 +75,42 @@ application: - %vendorDir%/mymodule ``` -El escaneo de directorios se puede desactivar indicando el valor false. No recomendamos suprimir por completo la adición automática de presenters, ya que de lo contrario se reducirá el rendimiento de la aplicación. +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 ================ -Con esta configuración, se puede influir globalmente en el comportamiento de Latte en componentes y presenters. +Este ajuste afecta globalmente al comportamiento de Latte en los componentes y los presenters. ```neon latte: - # mostrar el panel Latte en Tracy Bar para la plantilla principal (true) o todos los componentes (all)? - debugger: ... # (true|false|'all') predeterminado es true + # ¿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 - # genera plantillas con la cabecera declare(strict_types=1) - strictTypes: ... # (bool) predeterminado es false + # activa el [modo estricto del parser |latte:develop#strict mode] + strictParsing: ... # (bool) el valor predeterminado es false - # activa el modo de [parser estricto |latte:develop#striktní režim] - strictParsing: ... # (bool) predeterminado es false + # limita el alcance de las variables al cuerpo del bucle + scopedLoopVariables: ... # (bool) el valor predeterminado es false - # activa la [verificación del código generado |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) predeterminado es null + # elimina la indentación causada por el anidamiento en etiquetas pareadas + dedent: ... # (bool) el valor predeterminado es false - # establece la configuración regional - locale: cs_CZ # (string) predeterminado es null + # 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 # predeterminado es Nette\Bridges\ApplicationLatte\DefaultTemplate + templateClass: App\MyTemplateClass # el valor predeterminado es Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Si usa Latte versión 3, puede agregar nuevas [extensiones |latte:extending-latte#Latte Extension] usando: +Puede añadir nuevas [extensiones |latte:extending-latte#Latte Extension] así: ```neon latte: @@ -110,36 +118,22 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Si usa Latte versión 2, puede registrar nuevas etiquetas (macros) ya sea indicando el nombre de la clase o una referencia al servicio. Por defecto, se llama al método `install()`, pero esto se puede cambiar indicando el nombre de otro método: - -```neon -latte: - # registro de etiquetas Latte personalizadas - macros: - - App\MyLatteMacros::register # método estático, nombre de clase o callable - - @App\MyLatteMacrosFactory # servicio con método install() - - @App\MyLatteMacrosFactory::register # servicio con método register() - -services: - - App\MyLatteMacrosFactory -``` - Enrutamiento ============ -Configuración básica: +Ajustes básicos: ```neon routing: - # mostrar el panel de enrutamiento en Tracy Bar? - debugger: ... # (bool) predeterminado es true + # ¿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) predeterminado es false + cache: ... # (bool) el valor predeterminado es false ``` -El enrutamiento generalmente lo definimos en la clase [RouterFactory |routing#Colección de rutas]. Alternativamente, las rutas también se pueden definir en la configuración usando pares `máscara: acción`, pero este método no ofrece una variabilidad tan amplia en la configuración: +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: @@ -152,23 +146,23 @@ routing: Constantes ========== -Creación de constantes PHP. +Creación de constantes de PHP. ```neon constants: Foobar: 'baz' ``` -Después de iniciar la aplicación, se creará la constante `Foobar`. +La constante `Foobar` se creará al arrancar la aplicación. .[note] -Las constantes no deben servir como variables disponibles globalmente. Para pasar valores a objetos, utilice la [inyección de dependencias |dependency-injection:passing-dependencies]. +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 === -Configuración de directivas PHP. Un resumen de todas las directivas se encuentra en [php.net |https://www.php.net/manual/en/ini.list.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: @@ -179,13 +173,14 @@ php: Servicios DI ============ -Estos servicios se agregan al contenedor DI: - -| Nombre | Tipo | Descripción -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [ejecutor de toda la aplicación |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | fábrica de presenters -| `application.###` | [api:Nette\Application\UI\Presenter] | presenters individuales -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | fábrica del objeto `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | fábrica para [`$this->template` |templates] +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 index 76be2a6ad9..11815e20ca 100644 --- a/application/es/creating-links.texy +++ b/application/es/creating-links.texy @@ -3,132 +3,149 @@ Creación de enlaces URL
    -Crear enlaces en Nette es tan simple como señalar con el dedo. Solo necesita apuntar y el framework hará todo el trabajo por usted. Mostraremos: +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 plantillas y en otros lugares +- 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 direcciones URL de su aplicación directamente en plantillas o código, que pueden cambiar más tarde, o componerlas de forma complicada. En el enlace, basta con indicar el presenter y la acción, pasar los parámetros necesarios y el framework generará la URL por sí mismo. De hecho, es muy similar a llamar a una función. Esto le gustará. +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 ============================= -La mayoría de las veces creamos enlaces en plantillas y un excelente ayudante es el atributo `n:href`: +Lo más habitual es crear los enlaces en las plantillas, y el atributo `n:href` es una gran ayuda: ```latte -detalle +detail ``` -Observe que en lugar del atributo HTML `href`, usamos el [n:atributo |latte:syntax#n:atributos] `n:href`. Su valor no es una URL, como sería el caso del atributo `href`, sino el nombre del presenter y la acción. +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. -Hacer clic en un enlace es, simplificando, algo así como llamar al método `ProductPresenter::renderShow()`. Y si tiene parámetros en su firma, podemos llamarlo con argumentos: +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 -detalle del producto +product detail ``` -También es posible pasar parámetros con nombre. El siguiente enlace pasa el parámetro `lang` con el valor `cs`: +También es posible pasar parámetros nombrados. El siguiente enlace pasa el parámetro `lang` con el valor `en`: ```latte -detalle del producto +product detail ``` -Si el método `ProductPresenter::renderShow()` no tiene `$lang` en su firma, puede obtener el valor del parámetro usando `$lang = $this->getParameter('lang')` o desde la [propiedad |presenters#Parámetros de la petición]. +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 almacenados en un array, se pueden expandir con el operador `...` (en Latte 2.x con el operador `(expand)`): +Si los parámetros están guardados en un array, se pueden expandir con el operador `...`: ```latte -{var $args = [$product->id, lang => cs]} -detalle del producto +{var $args = [$product->id, lang => en]} +product detail ``` -En los enlaces también se pasan automáticamente los llamados [parámetros persistentes |presenters#Parámetros persistentes]. +Los llamados [parámetros persistentes |presenters#Parámetros persistentes] también se pasan automáticamente en los enlaces. -El atributo `n:href` es muy útil para las etiquetas HTML ``. Si queremos mostrar el enlace en otro lugar, por ejemplo en el texto, usamos `{link}`: +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 -La dirección es: {link Home:default} +URL is: {link Home:default} ``` En el código ============ -Para crear un enlace en el presenter, se utiliza el método `link()`: +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 mediante un array, donde también se pueden indicar parámetros con nombre: +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' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -Los enlaces también se pueden crear sin un presenter, para eso está [#LinkGenerator] y su método `link()`. +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: -Enlaces a presenter -=================== +```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, tiene esta sintaxis: +Si el destino del enlace es un presenter y una acción, la sintaxis es esta: ``` [//] [[[[:]module:]presenter:]action | this] [#fragment] ``` -El formato es compatible con todas las etiquetas 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 [#LinkGenerator]. Así que, aunque en los ejemplos se use `n:href`, podría estar cualquiera de las funciones. +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 lo tanto, `Presenter:action`: +La forma básica es, por tanto, `Presenter:acción`: ```latte -página de inicio +home page ``` Si enlazamos a una acción del presenter actual, podemos omitir su nombre: ```latte -página de inicio +home page ``` -Si el destino es la acción `default`, podemos omitirla, pero los dos puntos deben permanecer: +Si la acción de destino es `default`, la podemos omitir, pero los dos puntos deben quedarse: ```latte -página de inicio +home page ``` -Los enlaces también pueden apuntar a otros [módulos |directory-structure#Presenters y plantillas]. Aquí, los enlaces se distinguen entre relativos a un submódulo anidado o absolutos. El principio es análogo a las rutas en el disco, solo que en lugar de barras inclinadas hay dos puntos. Supongamos que el presenter actual es parte del módulo `Front`, entonces escribimos: +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 -enlace a Front:Shop:Product:show -enlace a Admin:Product:show +link to Front:Shop:Product:show +link to Admin:Product:show ``` -Un caso especial es un enlace [a sí mismo |#Enlace a la página actual], donde indicamos `this` como destino. +Un caso especial es el enlace [a sí mismo |#Enlace a la página actual], donde indicamos `this` como destino. ```latte -refrescar +refresh ``` -Podemos enlazar a una parte específica de la página a través del llamado fragmento después del signo de almohadilla `#`: +Podemos enlazar a una parte concreta de la página mediante un fragmento tras el signo de almohadilla `#`: ```latte -enlace a Home:default y fragmento #main +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 mediante `link()` o `n:href` son siempre rutas absolutas (es decir, comienzan con el carácter `/`), pero no URL absolutas con protocolo y dominio como `https://domain`. +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`. -Para generar una URL absoluta, agregue dos barras inclinadas al principio (p. ej., `n:href="//Home:"`). O se puede cambiar el presenter para que genere solo enlaces absolutos estableciendo `$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 @@ -137,24 +154,24 @@ Enlace a la página actual El destino `this` crea un enlace a la página actual: ```latte -refrescar +refresh ``` -Al mismo tiempo, se transfieren todos los parámetros indicados en la firma del método `action()` o `render()`, si `action()` no está definida. Así que si estamos en la página `Product:show` e `id: 123`, el enlace a `this` también pasará este parámetro. +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 especificar los parámetros directamente: +Por supuesto, es posible indicar los parámetros directamente: ```latte -refrescar +refresh ``` -La función `isLinkCurrent()` comprueba si el destino del enlace coincide con la página actual. Esto se puede usar, por ejemplo, en la plantilla para distinguir enlaces, etc. +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 en el método `link()`, pero además es posible indicar un comodín `*` en lugar de una acción específica, que significa cualquier acción del presenter dado. +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')} - Inicie sesión + Login {/if}
  • @@ -162,15 +179,15 @@ Los parámetros son los mismos que en el método `link()`, pero además es posib
  • ``` -En combinación con `n:href` en un solo elemento, se puede usar una forma abreviada: +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, nikoliv presenteru. +El comodín `*` solo se puede usar en lugar de la acción, no del presenter. -Para determinar si estamos en un módulo específico o en su submódulo, usamos el método `isModuleCurrent(moduleName)`. +Para saber si estamos en un módulo concreto o en uno de sus submódulos, use el método `isModuleCurrent(moduleName)`. ```latte
  • @@ -179,41 +196,57 @@ Para determinar si estamos en un módulo específico o en su submódulo, usamos ``` -Enlaces a señal -=============== +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] (llaman al método `handle()`). Entonces la sintaxis es la siguiente: +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 por el signo de exclamación: +La señal se distingue, pues, por el signo de exclamación: ```latte -señal +signal ``` -También se puede crear un enlace a la señal de un subcomponente (o sub-subcomponente): +También puede crear un enlace a la señal de un subcomponente (o de un subsubcomponente): ```latte -señal +signal ``` -Enlaces en componente -===================== +Enlaces en un componente +======================== -Dado que los [componentes|components] son unidades reutilizables independientes que no deberían tener ninguna vinculación con los presenters circundantes, los enlaces funcionan aquí de manera un poco diferente. El atributo Latte `n:href` y la etiqueta `{link}`, así como los métodos del componente como `link()` y otros, consideran el destino del enlace **siempre como el nombre de la señal**. Por lo tanto, ni siquiera es necesario indicar el signo de exclamación: +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 -señal, no acción +signal, not an action ``` -Si quisiéramos enlazar a presenters en la plantilla del componente, usaríamos la etiqueta `{plink}`: +Si en la plantilla del componente quisiéramos enlazar a presenters, usaríamos la etiqueta `{plink}`: ```latte -inicio +home ``` o en el código @@ -223,12 +256,12 @@ $this->getPresenter()->link('Home:default') ``` -Alias .{data-version:v3.2.2} -============================ +Alias .{data-version:3.2.3} +=========================== -A veces puede ser útil asignar un alias fácil de recordar al par Presenter:acción. Por ejemplo, nombrar la página de inicio `Front:Home:default` simplemente como `home` o `Admin:Dashboard:default` como `admin`. +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`: +Los alias se definen en la [configuración|configuration], bajo la clave `application › aliases`: ```neon application: @@ -238,26 +271,26 @@ application: sign: Front:Sign:in ``` -En los enlaces, luego se escriben usando arroba, por ejemplo: +En los enlaces se escriben después con una arroba, por ejemplo: ```latte -administración +administration ``` -También son compatibles con todos los métodos que trabajan con enlaces, como `redirect()` y similares. +También están admitidos en todos los métodos que trabajan con enlaces, como `redirect()` y similares. Enlaces no válidos ================== -Puede suceder que creemos un enlace no válido, ya sea porque apunta a un presenter inexistente, o porque pasa más parámetros de los que el método de destino acepta en su firma, o cuando no se puede generar una URL para la acción de destino. Cómo tratar los enlaces no válidos lo determina la variable estática `Presenter::$invalidLinkMode`. Esta puede tomar una combinación de estos valores (constantes): +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, se 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 causará la interrupción de la ejecución del script -- `Presenter::InvalidLinkTextual` - advertencia visual, muestra el error directamente en el enlace -- `Presenter::InvalidLinkException` - se lanza la excepción InvalidLinkException +- `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 -La configuración predeterminada es `InvalidLinkWarning` en modo de producción y `InvalidLinkWarning | InvalidLinkTextual` en desarrollo. `InvalidLinkWarning` en el entorno de producción no causa la interrupción del script, pero la advertencia se registrará. En el entorno de desarrollo, [Tracy |tracy:] lo captura y muestra una pantalla azul. `InvalidLinkTextual` funciona devolviendo un mensaje de error como URL, que comienza con los caracteres `#error:`. Para que dichos enlaces sean evidentes a primera vista, agregaremos a nuestro CSS: +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:"] { @@ -266,7 +299,7 @@ a[href^="#error:"] { } ``` -Si no queremos que se produzcan advertencias en el entorno de desarrollo, podemos establecer el modo silencioso directamente en la [configuración|configuration]. +Si no queremos que se produzcan advertencias en el entorno de desarrollo, podemos silenciarlas directamente en la [configuración|configuration]. ```neon application: @@ -277,10 +310,10 @@ application: LinkGenerator ============= -¿Cómo crear enlaces con una comodidad similar a la del método `link()`, pero sin la presencia de un presenter? Para eso está [api:Nette\Application\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 solicitar que se le pase a través del constructor y luego crear enlaces con su método `link()`. +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 con respecto a los presenters. LinkGenerator crea todos los enlaces directamente como URL absolutas. Y además, no existe un "presenter actual", por lo que no se puede indicar solo el nombre de la acción como destino `link('default')` ni indicar rutas relativas a módulos. +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 siempre lanzan `Nette\Application\UI\InvalidLinkException`. +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 index 2392b7c719..057717ee41 100644 --- a/application/es/directory-structure.texy +++ b/application/es/directory-structure.texy @@ -3,43 +3,43 @@ Estructura de directorios de la aplicación
    -¿Cómo diseñar una estructura de directorios clara y escalable para proyectos en Nette Framework? Mostraremos las mejores prácticas que le ayudarán a organizar su código. Aprenderá: +¿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 **dividir lógicamente** la aplicación en directorios -- cómo diseñar la estructura para que **escale bien** con el crecimiento del proyecto -- cuáles son las **alternativas posibles** y sus ventajas o desventajas +- 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í mismo no impone ninguna estructura específica. Está diseñado para adaptarse fácilmente a cualquier necesidad y preferencia. +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 de [Web Project|https://github.com/nette/web-project]: +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 con la aplicación +├── 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/ ← pruebas -├── vendor/ ← librerías instaladas por Composer +├── 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 necesita actualizar las rutas relativas a los directorios en el archivo `Bootstrap.php` y, opcionalmente, en `composer.json`. No se necesita 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 base de URL. +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 nuevo proyecto por primera vez, debería poder orientarse rápidamente en él. Imagine que expande el directorio `app/Model/` y ve esta estructura: +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/ @@ -48,9 +48,9 @@ Cuando explora un nuevo proyecto por primera vez, debería poder orientarse ráp └── Entities/ \-- -De ella, solo deduce que el proyecto utiliza algunos servicios, repositorios y entidades. No aprenderá nada sobre el propósito real de la aplicación. +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: **organización por dominios**: +Veamos otro enfoque distinto: la **organización por dominios**: /--pre app/Model/ @@ -60,51 +60,51 @@ Veamos otro enfoque: **organización por dominios**: └── Product/ \-- -Aquí es diferente: a primera vista, está claro que se trata de una tienda electrónica. Los propios nombres de los directorios revelan lo que hace la aplicación: trabaja con pagos, pedidos y productos. +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) presenta una serie de problemas en la práctica: el código que está lógicamente relacionado está disperso en diferentes carpetas y tiene que saltar entre ellas. Por lo tanto, organizaremos por dominios. +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 costumbre que la estructura de directorios corresponda a los espacios de nombres en la aplicación. Esto significa que la ubicación física de los archivos corresponde a su espacio de nombres. Por ejemplo, una clase ubicada en `app/Model/Product/ProductRepository.php` debería tener el espacio de nombres `App\Model\Product`. Este principio ayuda a orientarse en el código y simplifica la autocarga. +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 vs plural en los nombres ---------------------------------- +Singular y plural en los nombres +-------------------------------- -Observe que para los directorios principales de la aplicación usamos el singular: `app`, `config`, `log`, `temp`, `www`. Lo mismo dentro de la aplicación: `Model`, `Core`, `Presentation`. Esto se debe a que cada uno de ellos representa un concepto coherente. +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 manera similar, por ejemplo, `app/Model/Product` representa todo lo relacionado con los productos. No lo llamaremos `Products`, porque no es una carpeta llena de productos (eso significaría que habría archivos `nokia.php`, `samsung.php`). Es un espacio de nombres que contiene clases para trabajar con productos: `ProductRepository.php`, `ProductService.php`. +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 independiente. +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. -Para mantener la coherencia, recomendamos usar: -- Singular para espacios de nombres que representan una unidad funcional (aunque trabajen con múltiples entidades) -- Plural para colecciones de unidades independientes -- En caso de duda o si no quiere pensar en ello, elija el singular +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 llamado document-root). A menudo también puede encontrar el nombre `public/` en lugar de `www/`: es solo una cuestión de convención y no afecta la funcionalidad del framework. El directorio contiene: -- El [punto de entrada |bootstrapping#index.php] de la aplicación `index.php` -- El archivo `.htaccess` con reglas para mod_rewrite (para Apache) -- Archivos estáticos (CSS, JavaScript, imágenes) -- Archivos subidos +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 seguridad adecuada de la aplicación, es crucial tener el [document-root correctamente configurado |nette:troubleshooting#Cómo cambiar o eliminar el directorio www de la URL]. +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 la carpeta `node_modules/` en este directorio: contiene miles de archivos que pueden ser ejecutables y no deben ser accesibles públicamente. +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 aplicación `app/` -=============================== +Directorio de la aplicación `app/` +================================== -Este es el directorio principal con el código de la aplicación. Estructura básica: +Este es el directorio principal, el que contiene el código de la aplicación. Estructura básica: /--pre app/ @@ -115,21 +115,21 @@ Este es el directorio principal con el código de la aplicación. Estructura bá └── Bootstrap.php ← clase de arranque de la aplicación \-- -`Bootstrap.php` es la [clase de inicio de la aplicación|bootstrapping] que inicializa el entorno, carga la configuración y crea el contenedor DI. +`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. -Ahora veamos los subdirectorios individuales con más detalle. +Veamos ahora con más detalle cada uno de los subdirectorios. Presenters y plantillas ======================= -La parte de presentación de la aplicación la tenemos en el directorio `app/Presentation`. Una alternativa es el corto `app/UI`. Es el lugar para todos los presenters, sus plantillas y posibles clases auxiliares. +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. -Organizamos esta capa por dominios. En un proyecto complejo que combina una tienda electrónica, un blog y una API, la estructura se vería así: +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 electrónica +├── Shop/ ← frontend de la tienda │ ├── Product/ │ ├── Cart/ │ └── Order/ @@ -143,7 +143,7 @@ Organizamos esta capa por dominios. En un proyecto complejo que combina una tien └── V1/ \-- -Por el contrario, para un blog simple, usaríamos la siguiente división: +Por el contrario, para un blog sencillo usaríamos la siguiente estructura: /--pre app/Presentation/ @@ -157,9 +157,9 @@ Por el contrario, para un blog simple, usaríamos la siguiente división: └── Export/ ← RSS, sitemaps, etc. \-- -Carpetas como `Home/` o `Dashboard/` contienen presenters y plantillas. Carpetas como `Front/`, `Admin/` o `Api/` las llamamos **módulos**. Técnicamente, son directorios normales que sirven para la división lógica de la aplicación. +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 con un presenter contiene un presenter con el mismo nombre y sus plantillas. Por ejemplo, la carpeta `Dashboard/` contiene: +Cada carpeta que contiene un presenter incluye el propio archivo del presenter y sus plantillas. Por ejemplo, la carpeta `Dashboard/` contiene: /--pre Dashboard/ @@ -167,7 +167,7 @@ Cada carpeta con un presenter contiene un presenter con el mismo nombre y sus pl └── default.latte ← plantilla \-- -Esta estructura de directorios se refleja en los espacios de nombres de las clases. Por ejemplo, `DashboardPresenter` se encuentra en el espacio de nombres `App\Presentation\Admin\Dashboard` (ver [#Mapeo de presenters]): +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; @@ -178,13 +178,13 @@ class DashboardPresenter extends Nette\Application\UI\Presenter } ``` -Al presenter `Dashboard` dentro del módulo `Admin` nos referimos en la aplicación usando la notación de dos puntos como `Admin:Dashboard`. A su acción `default` entonces como `Admin:Dashboard:default`. En caso de módulos anidados, usamos más dos puntos, por ejemplo `Shop:Order:Detail:default`. +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 cómo se adapta elegantemente a las crecientes necesidades del proyecto. Como ejemplo, tomemos la parte que genera feeds XML. Al principio, tenemos una forma simple: +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/ @@ -193,7 +193,7 @@ Una de las grandes ventajas de esta estructura es cómo se adapta elegantemente └── feed.latte ← plantilla para el feed RSS \-- -Con el tiempo, se agregan otros tipos de feeds y necesitamos más lógica para ellos... ¡No hay problema! La carpeta `Export/` simplemente se convierte en un módulo: +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/ @@ -202,47 +202,47 @@ Con el tiempo, se agregan otros tipos de feeds y necesitamos más lógica para e │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← feed para Zboží.cz - └── heureka.latte ← feed para Heureka.cz + ├── amazon.latte ← feed para Amazon + └── ebay.latte ← feed para eBay \-- -Esta transformación es completamente fluida: basta con crear nuevas subcarpetas, dividir el código en ellas y actualizar los enlaces (p. ej., de `Export:feed` a `Export:Feed:zbozi`). Gracias a esto, podemos expandir gradualmente la estructura según sea necesario, el nivel de anidamiento no está limitado de ninguna manera. +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. -Si, por ejemplo, en la administración tiene muchos presenters relacionados con la gestión de pedidos, como `OrderDetail`, `OrderEdit`, `OrderDispatch`, etc., puede crear un módulo (carpeta) `Order` en este lugar para una mejor organización, que contendrá (carpetas para) los presenters `Detail`, `Edit`, `Dispatch` y otros. +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, vimos que las plantillas se ubican directamente en la carpeta con el presenter: +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 opcional para la plantilla +├── DashboardTemplate.php ← clase de plantilla opcional └── default.latte ← plantilla \-- -Esta ubicación resulta ser la más conveniente en la práctica: tiene todos los archivos relacionados a mano. +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 plantillas se encuentra en el capítulo [Búsqueda de plantillas |templates#Búsqueda de plantillas]. +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 plantillas a menudo les pertenecen otros archivos auxiliares. Los ubicamos lógicamente según su ámbito: +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 caso de componentes específicos para ese presenter: +1. **Directamente junto al presenter** en el caso de componentes específicos de ese presenter: /--pre Product/ ├── ProductPresenter.php -├── ProductGrid.php ← componente para listar productos +├── ProductGrid.php ← componente para el listado de productos └── FilterForm.php ← formulario para filtrar \-- -2. **Para el módulo** - recomendamos usar la carpeta `Accessory`, que se coloca convenientemente al principio del alfabeto: +2. **Para el módulo**: recomendamos usar la carpeta `Accessory`, que queda convenientemente al principio por orden alfabético: /--pre Front/ @@ -253,7 +253,7 @@ A los presenters y plantillas a menudo les pertenecen otros archivos auxiliares. └── Cart/ \-- -3. **Para toda la aplicación** - en `Presentation/Accessory/`: +3. **Para toda la aplicación**: en `Presentation/Accessory/`: /--pre app/Presentation/ ├── Accessory/ @@ -263,30 +263,30 @@ A los presenters y plantillas a menudo les pertenecen otros archivos auxiliares. └── Admin/ \-- -O puede colocar 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 costumbres del equipo. +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. -Modelo - el corazón de la aplicación -==================================== +Model: el corazón de la aplicación +================================== -El modelo contiene toda la lógica de negocio de la aplicación. Para su organización, se aplica nuevamente la regla: estructuramos por dominios: +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 pagos +├── Payment/ ← todo sobre los pagos │ ├── PaymentFacade.php ← punto de entrada principal │ ├── PaymentRepository.php │ ├── Payment.php ← entidad -├── Order/ ← todo sobre pedidos +├── Order/ ← todo sobre los pedidos │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php -└── Shipping/ ← todo sobre envíos +└── Shipping/ ← todo sobre los envíos \-- -En el modelo, típicamente encontrará estos tipos de clases: +En el modelo se encuentra normalmente con estos tipos de clases: -**Fachadas (Facades)**: representan el punto de entrada principal a un dominio específico en la aplicación. Actúan como un orquestador que coordina la colaboración entre diferentes servicios con el fin de implementar casos de uso completos (como "crear pedido" o "procesar pago"). Bajo su capa de orquestación, la fachada oculta los detalles de implementación del resto de la aplicación, proporcionando así una interfaz limpia para trabajar con el dominio dado. +**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 @@ -295,13 +295,13 @@ class OrderFacade { // validación // creación del pedido - // envío de correo electrónico - // registro en estadísticas + // envío del correo + // escritura en las estadísticas } } ``` -**Servicios**: se centran en una operación de negocio específica dentro del dominio. A diferencia de la fachada, que orquesta casos de uso completos, el servicio implementa lógica de negocio específica (como cálculos de precios o procesamiento de pagos). Los servicios suelen ser sin estado y pueden ser utilizados ya sea por fachadas como bloques de construcción para operaciones más complejas, o directamente por otras partes de la aplicación para tareas más simples. +**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 @@ -313,7 +313,7 @@ class PricingService } ``` -**Repositorios**: aseguran toda la comunicación con el almacenamiento de datos, típicamente una base de datos. Su tarea es cargar y guardar entidades e implementar métodos para su búsqueda. El repositorio aísla al resto de la aplicación de los detalles de implementación de la base de datos y proporciona una interfaz orientada a objetos para trabajar con los datos. +**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 @@ -328,10 +328,10 @@ class OrderRepository } ``` -**Entidades**: objetos que representan los principales conceptos de negocio en la aplicación, que tienen su identidad y cambian con el tiempo. Típicamente, son clases mapeadas a tablas de bases de datos usando un ORM (como Nette Database Explorer o Doctrine). Las entidades pueden contener reglas de negocio relacionadas con sus datos y lógica de validación. +**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 de base de datos orders +// 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 @@ -345,13 +345,13 @@ class Order extends Nette\Database\Table\ActiveRow } ``` -**Objetos de valor (Value objects)**: objetos inmutables que representan valores sin identidad propia - por ejemplo, una cantidad monetaria o una dirección de correo electrónico. Dos instancias de un objeto de valor con los mismos valores son consideradas idénticas. +**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 también `Infrastructure/`) es el hogar de la base técnica de la aplicación. El código de infraestructura típicamente incluye: +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/ @@ -360,17 +360,17 @@ La carpeta `Core/` (o también `Infrastructure/`) es el hogar de la base técnic ├── Security/ ← autenticación y autorización │ ├── Authenticator.php │ └── Authorizator.php -├── Logging/ ← registro y monitoreo +├── Logging/ ← registro y monitorización │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← capa de caché │ └── FullPageCache.php -└── Integration/ ← integración con servicios ext. +└── Integration/ ← integración con servicios externos ├── Slack/ └── Stripe/ \-- -En proyectos más pequeños, por supuesto, basta con una estructura plana: +Para proyectos más pequeños basta naturalmente con una estructura plana: /--pre Core/ @@ -379,66 +379,66 @@ En proyectos más pequeños, por supuesto, basta con una estructura plana: └── QueueMailer.php \-- -Se trata de código que: +Es el código que: -- Resuelve 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 mayormente independiente del dominio específico - la caché o el logger funciona igual para una tienda electrónica o un blog. +- 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. -¿Duda si una clase determinada pertenece aquí o al modelo? La diferencia clave es que el código en `Core/`: +¿Se pregunta si una determinada clase pertenece aquí o al modelo? La diferencia clave es que el código de `Core/`: -- No sabe nada sobre el dominio (productos, pedidos, artículos) -- Es mayormente posible transferirlo a otro proyecto -- Resuelve "cómo funciona" (cómo enviar un correo), no "qué hace" (qué correo enviar) +- 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) -Ejemplo para una mejor comprensión: +Un ejemplo para entenderlo mejor: -- `App\Core\MailerFactory` - crea instancias de la clase para enviar correos electrónicos, resuelve la configuración SMTP -- `App\Model\OrderMailer` - usa `MailerFactory` para enviar correos electrónicos sobre pedidos, conoce sus plantillas y sabe cuándo deben enviarse +- `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 a menudo necesitan realizar actividades fuera de las peticiones HTTP normales - ya sea procesamiento de datos en segundo plano, mantenimiento o tareas periódicas. Para la ejecución sirven scripts simples en el directorio `bin/`, la lógica de implementación la colocamos en `app/Tasks/` (o `app/Commands/`). +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 ← eliminación de datos antiguos +│ ├── 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 periódicas +└── Scheduled/ ← tareas regulares ├── NewsletterCommand.php ← envío de newsletters - └── ReminderCommand.php ← notificaciones a clientes + └── ReminderCommand.php ← avisos a los clientes \-- -¿Qué pertenece al modelo y qué a los scripts de comandos? Por ejemplo, la lógica para enviar un correo electrónico es parte del modelo, el envío masivo de miles de correos electrónicos ya pertenece a `Tasks/`. +¿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 generalmente [se ejecutan desde la línea de comandos |https://blog.nette.org/en/cli-scripts-in-nette-application] o a través de cron. También se pueden ejecutar a través de una petición HTTP, pero es necesario pensar en la seguridad. El presenter que ejecuta la tarea debe estar protegido, por ejemplo, solo para usuarios autenticados o con un token fuerte y acceso desde direcciones IP permitidas. Para tareas largas, es necesario aumentar el límite de tiempo del script y usar `session_write_close()` para que la sesión no se bloquee. +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 agregar otras carpetas especializadas según las necesidades del proyecto. Veamos las más comunes y su uso: +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 para la API independiente de la capa de presentación +├── 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 en toda la aplicación -├── Event/ ← útil si usa arquitectura dirigida por eventos -├── Mail/ ← plantillas de correo electrónico y lógica relacionada +├── 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 componentes visuales compartidos utilizados en presenters en toda la aplicación, se puede usar la carpeta `app/Components` o `app/Controls`: +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/ @@ -452,13 +452,13 @@ Para componentes visuales compartidos utilizados en presenters en toda la aplica └── Menu.php \-- -Aquí pertenecen los componentes que tienen una lógica más compleja. Si desea compartir componentes entre varios proyectos, es recomendable extraerlos a un paquete composer separado. +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 electrónico: +En el directorio `app/Mail` puede colocar la gestión de la comunicación por correo: /--pre app/Mail/ -├── templates/ ← plantillas de correo electrónico +├── templates/ ← plantillas de correo │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php @@ -468,32 +468,32 @@ En el directorio `app/Mail` puede colocar la gestión de la comunicación por co Mapeo de presenters =================== -El mapeo define reglas para derivar el nombre de la clase a partir del nombre del presenter. Las especificamos en la [configuración|configuration] bajo la clave `application › mapping`. +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`). Debemos comunicar esta convención a Nette en el archivo de configuración. Basta con una línea: +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 una mejor comprensión, imaginemos primero una aplicación sin módulos. Queremos que las clases de los presenters caigan en el espacio de nombres `App\Presentation`, para que el presenter `Home` se mapee a la clase `App\Presentation\HomePresenter`. Lo cual logramos con esta configuración: +¿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 de tal manera que el nombre del presenter `Home` reemplaza el asterisco en la máscara `App\Presentation\*Presenter`, obteniendo así el nombre de clase resultante `App\Presentation\HomePresenter`. ¡Simple! +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! -Pero como puede ver en los ejemplos de este y otros capítulos, colocamos las clases de los presenters en subdirectorios epónimos, por ejemplo, el presenter `Home` se mapea a la clase `App\Presentation\Home\HomePresenter`. Esto se logra duplicando los dos puntos (requiere Nette Application 3.2): +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 procederemos a mapear los presenters a módulos. Para cada módulo podemos definir un mapeo específico: +Ahora pasamos al mapeo de presenters en módulos. Podemos definir un mapeo específico para cada módulo: ```neon application: @@ -503,9 +503,9 @@ application: 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` a la clase `App\Api\OAuthPresenter`. +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`. -Dado que los módulos `Front` y `Admin` tienen una forma similar de mapeo y probablemente habrá más módulos de este tipo, es posible crear una regla general que los reemplace. Así, se agregará un nuevo asterisco a la máscara de clase para el módulo: +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: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -Funciona también para estructuras de directorios más profundamente anidadas, como por ejemplo el presenter `Admin:User:Edit`, el segmento con asterisco se repite para cada nivel y el resultado es la clase `App\Presentation\Admin\User\Edit\EditPresenter`. +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 es usar un array compuesto por tres segmentos en lugar de una cadena. Esta notación es equivalente a la anterior: +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: diff --git a/application/es/how-it-works.texy b/application/es/how-it-works.texy index 40246de021..f09f7e4a0f 100644 --- a/application/es/how-it-works.texy +++ b/application/es/how-it-works.texy @@ -3,11 +3,11 @@
    -Está leyendo el documento fundamental de la documentación de Nette. Aprenderá todo el principio de funcionamiento de las aplicaciones web. De la A a la Z, desde el momento del nacimiento hasta el último suspiro del script PHP. Después de leerlo, sabrá: +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 -- cómo es la estructura de directorios +- qué aspecto tiene la estructura de directorios
    @@ -15,89 +15,89 @@ Está leyendo el documento fundamental de la documentación de Nette. Aprenderá Estructura de directorios ========================= -Abra el ejemplo del esqueleto de una aplicación web llamado [WebProject|https://github.com/nette/web-project] y mientras lee, puede mirar los archivos de los que se habla. +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 este aspecto: +La estructura de directorios tiene más o menos este aspecto: /--pre web-project/ -├── app/ ← directorio con la aplicación -│ ├── Core/ ← clases básicas necesarias para el funcionamiento -│ │ └── RouterFactory.php ← configuración de direcciones URL -│ ├── Presentation/ ← presenters, plantillas, etc. -│ │ ├── @layout.latte ← plantilla de layout +├── 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 de origen). +├── 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é, etc. -├── vendor/ ← librerías instaladas por Composer +├── temp/ ← archivos temporales, caché, … +├── vendor/ ← bibliotecas instaladas por Composer │ ├── ... -│ └── autoload.php ← autoloading de todos los paquetes instalados -├── www/ ← directorio público o document-root del proyecto +│ └── 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 mod_rewrite -│ └── index.php ← archivo inicial con el que se inicia la aplicación -└── .htaccess ← prohíbe el acceso a todos los directorios excepto www +│ ├── .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 desee, renombrar o mover carpetas, es completamente flexible. Nette, además, dispone de una autodetección inteligente y reconoce automáticamente la ubicación de la aplicación, incluida su base de URL. +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 un poco más grandes, podemos [dividir las carpetas con presenters y plantillas en subdirectorios |directory-structure#Presenters y plantillas] y las clases en espacios de nombres, que llamamos módulos. +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 llamado directorio público o document-root del proyecto. Puede renombrarlo sin necesidad de configurar nada más en el lado de la aplicación. Solo es necesario [configurar el hosting |nette:troubleshooting#Cómo cambiar o eliminar el directorio www de la URL] para que el document-root apunte a este directorio. +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 incluyendo Nette usando [Composer |best-practices:composer]: +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, establezca [permisos de escritura |nette:troubleshooting#Configuración de permisos de directorio] para los directorios `log/` y `temp/`. +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 ejecutarse, no es necesario configurar absolutamente nada y puede mostrarla directamente en el navegador accediendo a la carpeta `www/`. +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 comienza en el momento en que el usuario abre una página en el navegador. Es decir, cuando el navegador llama al servidor con una petición HTTP. La petición apunta a un único archivo PHP, que se encuentra en el directorio público `www/`, y este es `index.php`. Supongamos que se trata de una petición a la dirección `https://example.com/product/123`. Gracias a la [configuración adecuada del servidor |nette:troubleshooting#Cómo configurar el servidor para URLs amigables], incluso esta URL se mapea al archivo `index.php` y este se ejecuta. +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 fábrica -3) iniciar la aplicación Nette, que gestionará la petición +2) obtener la factory +3) ejecutar la aplicación de Nette, que atiende la petición -¿Qué fábrica? ¡No fabricamos tractores, sino páginas web! Espere, se explicará de inmediato. +¿Qué factory? ¡No fabricamos tractores, construimos webs! Espere, se explicará enseguida. -Con las palabras "inicialización del entorno" nos referimos, por ejemplo, a que se activa [Tracy|tracy:], que es una herramienta increíble para el registro o la visualización de errores. En el servidor de producción, registra los errores, en el de desarrollo los muestra directamente. Por lo tanto, la inicialización también incluye la decisión de si el sitio web se ejecuta en modo de producción o de desarrollo. Para esto, Nette utiliza una [autodetección inteligente |bootstrapping#Modo de desarrollo vs producción]: si ejecuta el sitio web en localhost, se ejecuta en modo de desarrollo. Por lo tanto, no necesita configurar nada y la aplicación está lista tanto para el desarrollo como para la implementación en producción. Estos pasos se realizan y se describen detalladamente en el capítulo sobre la [clase Bootstrap|bootstrapping]. +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í, saltamos el segundo, pero volveremos a él) es el inicio de la aplicación. La gestión de las peticiones HTTP en Nette está a cargo de la clase `Nette\Application\Application` (en adelante `Application`), por lo que cuando decimos iniciar la aplicación, nos referimos específicamente a llamar al método con el nombre apropiado `run()` en el objeto de esta clase. +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 es un mentor que le guía para escribir aplicaciones limpias según metodologías probadas. Y una de las más probadas se llama **inyección de dependencias**, abreviada como DI. En este momento no queremos cargarle con la explicación de DI, para eso está [un capítulo aparte|dependency-injection:introduction], lo importante es la consecuencia de que los objetos clave generalmente nos los creará una fábrica de objetos, que se llama **contenedor DI** (abreviado como DIC). Sí, esa es la fábrica de la que hablamos hace un momento. Y también nos fabricará el objeto `Application`, por eso necesitamos primero el contenedor. Lo obtenemos usando la clase `Configurator` y le pedimos que fabrique el objeto `Application`, llamamos al método `run()` en él y así se inicia la aplicación Nette. Exactamente esto sucede en el archivo [index.php |bootstrapping#index.php]. +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. +La clase `Application` tiene una única tarea: responder a la petición HTTP. -Las aplicaciones escritas en Nette se dividen en muchos llamados presenters (en otros frameworks puede encontrar el término controller, es lo mismo), que son clases, cada una de las cuales representa alguna página específica del sitio web: p. ej., la página de inicio; un producto en una tienda electrónica; un formulario de inicio de sesión; un feed sitemap, etc. Una aplicación puede tener desde uno hasta miles de presenters. +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 comienza pidiendo al llamado router que decida a cuál de los presenters pasar la petición actual para su gestión. El router decide de quién es la responsabilidad. Mira la URL de entrada `https://example.com/product/123` y, basándose en cómo está configurado, decide que este es trabajo, por ejemplo, para el **presenter** `Product`, al que le pedirá como **acción** la visualización (`show`) del producto con `id: 123`. Es una buena costumbre escribir el par presenter + acción separado por dos puntos como `Product:show`. +`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`. -Por lo tanto, el router transformó la URL en el par `Presenter:action` + parámetros, en nuestro caso `Product:show` + `id: 123`. Puede ver cómo es un router de este tipo en el archivo `app/Core/RouterFactory.php` y lo describimos detalladamente en el capítulo [Enrutamiento |Routing]. +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]. -Sigamos. Application ya conoce el nombre del presenter y puede continuar. Creando el objeto de la clase `ProductPresenter`, que es el código del presenter `Product`. Más precisamente, pide al contenedor DI que fabrique el presenter, porque para eso está él. +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 puede verse así: +El presenter podría tener este aspecto: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -109,92 +109,92 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function renderShow(int $id): void { - // obtenemos datos del modelo y los pasamos a la plantilla + // obtiene los datos del modelo y se los pasa a la plantilla $this->template->product = $this->repository->getProduct($id); } } ``` -La gestión de la petición la asume el presenter. Y la tarea es clara: realiza la acción `show` con `id: 123`. Lo que en el lenguaje de los presenters significa que se llama al método `renderShow()` y en el parámetro `$id` recibe `123`. +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 manejar múltiples acciones, es decir, tener múltiples métodos `render()`. Pero recomendamos diseñar presenters con una o la menor cantidad posible de acciones. +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. -Entonces, se llamó al método `renderShow(123)`, cuyo código es un ejemplo ficticio, pero puede ver en él cómo se pasan los datos a la plantilla, es decir, escribiendo en `$this->template`. +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`. -Posteriormente, el presenter devuelve una respuesta. Esta puede ser una página HTML, una imagen, un documento XML, el envío de un archivo desde el disco, JSON o incluso una redirección a otra página. Es importante que si no decimos explícitamente cómo debe responder (que es el caso de `ProductPresenter`), la respuesta será la renderización de una plantilla con una página HTML. ¿Por qué? Porque en el 99% de los casos queremos renderizar una plantilla, por lo que el presenter toma este comportamiento como predeterminado y quiere facilitarnos el trabajo. Ese es el propósito de Nette. +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 tenemos que indicar qué plantilla renderizar, él mismo deduce la ruta hacia ella. En el caso de la acción `show`, simplemente intenta cargar la plantilla `show.latte` en el directorio con la clase `ProductPresenter`. También intentará localizar el layout en el archivo `@layout.latte` (más detalles sobre la [búsqueda de plantillas |templates#Búsqueda de plantillas]). +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]). -Y posteriormente renderiza las plantillas. Con esto, la tarea del presenter y de toda la aplicación está completa y la obra está terminada. Si la plantilla no existiera, se devolvería una página con error 404. Puede leer más sobre los presenters en la página [Presenters|presenters]. +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, intentemos recapitular todo el proceso con una URL ligeramente diferente: +Por si acaso, recapitulemos todo el proceso con una URL ligeramente distinta: -1) La URL será `https://example.com` -2) Arrancamos la aplicación, se crea el contenedor y se ejecuta `Application::run()` -3) El router decodifica la URL como el par `Home:default` -4) Se crea el objeto de la clase `HomePresenter` -5) Se llama al método `renderDefault()` (si existe) -6) Se renderiza la plantilla, p. ej., `default.latte` con el layout, p. ej., `@layout.latte` +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 ahora se haya encontrado con muchos conceptos nuevos, pero creemos que tienen sentido. Crear aplicaciones en Nette es increíblemente fácil. +Puede que se haya encontrado ahora con muchos conceptos nuevos, pero creemos que tienen sentido. Desarrollar aplicaciones en Nette es notablemente sencillo. Plantillas ========== -Ya que hablamos de plantillas, en Nette se utiliza el sistema de plantillas [Latte |latte:]. Por eso esas extensiones `.latte` en las plantillas. Latte se utiliza, por un lado, porque es el sistema de plantillas más seguro para PHP y, al mismo tiempo, el sistema más intuitivo. No necesita aprender mucho nuevo, le basta con conocer PHP y algunas etiquetas. Todo lo aprenderá [en la documentación |templates]. +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, se [crean enlaces |creating-links] a otros presenters y acciones de esta manera: +En la plantilla, los [enlaces |creating-links] a otros presenters y acciones se crean así: ```latte -detalle del producto +product detail ``` -Simplemente, en lugar de la URL real, escribe el par conocido `Presenter:action` e indica los parámetros necesarios. El truco está en `n:href`, que indica que este atributo será procesado por Nette. Y generará: +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 -detalle del producto +product detail ``` -La generación de URL está a cargo del router mencionado anteriormente. Es decir, los routers en Nette son excepcionales porque pueden realizar no solo transformaciones de URL al par presenter:action, sino también al revés, es decir, generar una URL a partir del nombre del presenter + acción + parámetros. Gracias a esto, en Nette puede cambiar completamente las formas de las URL en toda la aplicación terminada, sin cambiar un solo carácter en la plantilla o el presenter. Simplemente modificando el router. También gracias a esto funciona la llamada canonización, que es otra característica única de Nette que contribuye a un mejor SEO (optimización para motores de búsqueda) al evitar automáticamente la existencia de contenido duplicado en diferentes URL. Muchos programadores lo consideran asombroso. +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 ======================== -Sobre los presenters debemos revelarle una cosa más: tienen incorporado un sistema de componentes. Algo similar pueden recordar los veteranos de Delphi o ASP.NET Web Forms, algo remotamente similar es la base de React o Vue.js. En el mundo de los frameworks PHP, es una característica absolutamente única. +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 reutilizables independientes que insertamos en las páginas (es decir, presenters). Pueden ser [formularios |forms:in-presenter], [datagrids |https://componette.org/contributte/datagrid/], menús, encuestas de votación, en realidad cualquier cosa que tenga sentido usar repetidamente. Podemos crear nuestros propios componentes o usar algunos de la [enorme oferta |https://componette.org] de componentes de código abierto. +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 fundamentalmente en el enfoque para la creación de aplicaciones. Le abrirán nuevas posibilidades de componer páginas a partir de unidades prefabricadas. Y además tienen algo en común con [Hollywood |components#Estilo Hollywood]. +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 fábrica de objetos es el corazón de toda la aplicació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, como podría parecer por las líneas anteriores. En realidad, es una clase PHP bastante aburrida, que Nette genera y guarda en el directorio de caché. Tiene muchos métodos llamados como `createServiceAbcd()` y cada uno de ellos sabe cómo fabricar y devolver algún objeto. Sí, también está el método `createServiceApplication()`, que fabrica `Nette\Application\Application`, que necesitábamos en el archivo `index.php` para iniciar la aplicación. Y hay métodos que fabrican los presenters individuales. Y así sucesivamente. +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 que crea el contenedor DI, por alguna razón, se les llama servicios. +A los objetos creados por el contenedor DI se los llama, por algún motivo, servicios. -Lo que es realmente especial de esta clase es que no la programa usted, sino el framework. Él realmente genera el código PHP y lo guarda en el disco. Usted solo da instrucciones sobre qué objetos debe saber fabricar el contenedor y cómo exactamente. Y estas instrucciones están escritas en [archivos de configuración |bootstrapping#Configuración del contenedor DI], para los cuales se utiliza el formato [NEON|neon:format] y, por lo tanto, también tienen la extensión `.neon`. +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í que, por ejemplo, si indico en la sección [session |http:configuration#Sesión] la opción `expiration: 14 days`, el contenedor DI al crear el objeto `Nette\Http\Session` que representa la sesión, llamará a su método `setExpiration('14 days')` y así la configuración se hará realidad. +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. -Hay un capítulo completo preparado para usted que describe qué se puede [configurar |nette:configuring] y cómo [definir servicios propios |dependency-injection:services]. +Tiene preparado todo un capítulo que describe qué se puede [configurar |nette:configuring] y cómo [definir sus propios servicios |dependency-injection:services]. -Una vez que se adentre un poco en la creación de servicios, se encontrará con la palabra [autowiring |dependency-injection:autowiring]. Esta es una característica que le simplificará la vida de manera increíble. Sabe cómo pasar automáticamente objetos donde los necesita (por ejemplo, en los constructores de sus clases), sin que tenga que hacer nada. Descubrirá que el contenedor DI en Nette es un pequeño milagro. +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. -¿A dónde ir ahora? -================== +¿Y ahora qué? +============= -Hemos repasado los principios básicos de las aplicaciones en Nette. Hasta ahora muy superficialmente, pero pronto profundizará y con el tiempo creará maravillosas aplicaciones web. ¿A dónde continuar ahora? ¿Ya ha probado el tutorial [Escribiendo la primera aplicación|quickstart:]? +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 anteriormente, Nette dispone de todo un arsenal de [clases útiles|utils:], [capa de base de datos|database:], etc. Intente simplemente hacer clic en la documentación. O en el [blog|https://blog.nette.org]. Descubrirá muchas cosas interesantes. +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 index 7dc7ac9411..c121342b6a 100644 --- a/application/es/multiplier.texy +++ b/application/es/multiplier.texy @@ -2,31 +2,31 @@ Multiplier: componentes dinámicos ********************************* .[perex] -Herramienta para la creación dinámica de componentes interactivos +Una herramienta para crear dinámicamente componentes interactivos. -Partamos de un ejemplo típico: tenemos una lista de productos en una tienda online, y para cada uno queremos mostrar un formulario para añadir el producto al carrito. Una de las posibles variantes es envolver todo el listado en un único formulario. Sin embargo, un método mucho más cómodo nos lo ofrece [api:Nette\Application\UI\Multiplier]. +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 permite definir cómodamente una pequeña fábrica para múltiples componentes. Funciona según el principio de componentes anidados: cada componente que hereda de [api:Nette\ComponentModel\Container] puede contener otros componentes. +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#Componentes en profundidad] en la documentación o la [charla de Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. +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 que puede crear dinámicamente sus hijos mediante un callback pasado en el constructor. Vea el ejemplo: +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('count', 'Cantidad de productos:') + $form->addInteger('amount', 'Amount:') ->setRequired(); - $form->addSubmit('send', 'Añadir al carrito'); + $form->addSubmit('send', 'Add to cart'); return $form; }); } ``` -Ahora podemos simplemente, en la plantilla, renderizar un formulario para cada producto, y cada uno será realmente un componente único. +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} @@ -37,26 +37,26 @@ Ahora podemos simplemente, en la plantilla, renderizar un formulario para cada p {/foreach} ``` -El argumento pasado en la etiqueta `{control}` tiene un formato que indica: +El argumento pasado en la etiqueta `{control}` sigue un formato que significa: -1. obtén el componente `shopForm` -2. y de él obtén el hijo `$item->id` +1. Obtener el componente `shopForm`. +2. De él, obtener el hijo llamado `$item->id`. -En la primera llamada al punto **1.**, `shopForm` aún no existe, por lo que se llama a su fábrica `createComponentShopForm`. Sobre el componente obtenido (instancia de Multiplier) se llama entonces a la fábrica del formulario específico, que es la función anónima que pasamos a Multiplier en el constructor. +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 `foreach`, el método `createComponentShopForm` ya no será llamado (el componente existe), pero como buscamos a su otro hijo (`$item->id` será diferente en cada iteración), se volverá a llamar a la función anónima y nos devolverá un nuevo formulario. +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. -Lo único que queda es asegurar que el formulario añada al carrito realmente el producto que debe; actualmente, el formulario es completamente idéntico para cada producto. Nos ayudará una propiedad de Multiplier (y en general de cada fábrica de componentes en Nette Framework), y es que cada fábrica recibe como primer argumento el nombre del componente que se está creando. En nuestro caso, será `$item->id`, que es exactamente el dato que necesitamos. Basta con modificar ligeramente la creación del formulario: +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 ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Cantidad de productos:') + $form->addInteger('amount', 'Amount:') ->setRequired(); $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Añadir al carrito'); + $form->addSubmit('send', 'Add to cart'); return $form; }); } diff --git a/application/es/presenters.texy b/application/es/presenters.texy index 20185ba405..e714797364 100644 --- a/application/es/presenters.texy +++ b/application/es/presenters.texy @@ -3,37 +3,37 @@ Presenters
    -Nos familiarizaremos con cómo se escriben los presenters y las plantillas en Nette. Después de leerlo, sabrá: +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 dibujan las plantillas +- cómo se renderizan las plantillas
    -[Ya sabemos |how-it-works#Nette Application] que un presenter es una clase que representa alguna página específica de una aplicación web, p. ej., la página de inicio; un producto en una tienda electrónica; un formulario de inicio de sesión; un feed sitemap, etc. Una aplicación puede tener desde uno hasta miles de presenters. En otros frameworks también se les llama controllers. +[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. -Generalmente, bajo el término presenter se entiende un descendiente de la clase [api:Nette\Application\UI\Presenter], que es adecuado para generar interfaces web y al que nos dedicaremos en el resto de este capítulo. En sentido general, un presenter es cualquier objeto que implementa la interfaz [api:Nette\Application\IPresenter]. +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 gestionar una petición y devolver una respuesta (que puede ser una página HTML, una imagen, una redirección, etc.). +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.). -Por lo tanto, al principio se le pasa una petición. No es directamente una petición HTTP, sino un objeto [api:Nette\Application\Request], al que se transformó la petición HTTP con la ayuda del router. Generalmente no interactuamos con este objeto, ya que el presenter delega inteligentemente el procesamiento de la petición a otros métodos, que ahora mostraremos. +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* .<> +[* lifecycle.svg *] *** Ciclo de vida del presenter .<> -La imagen representa una lista de métodos que se llaman sucesivamente de arriba abajo, si existen. Ninguno de ellos tiene por qué existir, podemos tener un presenter completamente vacío sin un solo método y construir sobre él un sitio web estático simple. +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 exactamente al ciclo de vida del presenter, porque se llama en el momento de la creación del objeto. Pero lo mencionamos por su importancia. El constructor (junto con el [método inject|best-practices:inject-method-attribute]) sirve para pasar dependencias. +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 encargarse de la lógica de negocio de la aplicación, escribir y leer de la base de datos, realizar cálculos, etc. Para eso están las clases de la capa que llamamos modelo. Por ejemplo, la clase `ArticleRepository` puede encargarse de cargar y guardar artículos. Para que el presenter pueda trabajar con ella, se la deja [pasar mediante inyección de dependencias |dependency-injection:passing-dependencies]: +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 @@ -50,44 +50,47 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -Inmediatamente después de recibir la petición, se llama al método `startup()`. Puede usarlo para inicializar propiedades, verificar permisos de usuario, etc. Se requiere que el método siempre llame al ancestro `parent::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()} -------------------------------------------------- -Análogo al método `render()`. Mientras que `render()` está destinado a preparar datos para una plantilla específica que luego se renderizará, en `action()` se procesa la petición sin conexión con la renderización de la plantilla. Por ejemplo, se procesan datos, se inicia o cierra sesión de usuario, y así sucesivamente, y luego [se redirige a otro lugar |#Redirección]. +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()`, por lo que en él podemos cambiar el curso posterior de los acontecimientos, es decir, cambiar la plantilla que se dibujará, y también el método `render()` que se llamará. Y esto usando `setView('otraVista')`. +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')`. -Al método se le pasan parámetros de la petición. Es posible y recomendable indicar tipos para los parámetros, p. ej., `actionShow(int $id, ?string $slug = null)` - si falta el parámetro `id` o si no es un entero, el presenter devolverá un [error 404 |#Error 404 y cía] y finalizará la actividad. +.{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()} -------------------------------------------------- -El método procesa las llamadas señales, con las que nos familiarizaremos en el capítulo dedicado a los [componentes |components#Señal]. De hecho, está destinado principalmente a componentes y al procesamiento de peticiones AJAX. +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. -Al método se le pasan parámetros de la petición, como en el caso de `action()`, incluida la verificación de tipos. +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()`. Se utiliza para la configuración común de la plantilla, pasar variables para el layout y similares. +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()} ---------------------------------------------- -El lugar donde preparamos la plantilla para su posterior renderización, le pasamos datos, etc. +Aquí es donde preparamos la plantilla para su posterior renderizado, le pasamos los datos, etc. -Al método se le pasan parámetros de la petición, como en el caso de `action()`, incluida la verificación de tipos. +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 { - // obtenemos datos del modelo y los pasamos a la plantilla + // obtiene los datos del modelo y se los pasa a la plantilla $this->template->article = $this->articles->getById($id); } ``` @@ -96,7 +99,7 @@ public function renderShow(int $id): void `afterRender()` --------------- -El método `afterRender`, como su nombre indica de nuevo, se llama después de cada método `render()`. Se usa más bien excepcionalmente. +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()` @@ -105,44 +108,66 @@ El método `afterRender`, como su nombre indica de nuevo, se llama después de c Se llama al final del ciclo de vida del presenter. -**Un buen consejo antes de continuar**. Como puede ver, un presenter puede manejar múltiples acciones/vistas, es decir, tener múltiples métodos `render()`. Pero recomendamos diseñar presenters con una o la menor cantidad posible de acciones. +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 la respuesta -===================== -La respuesta del presenter suele ser la [renderización de una plantilla con una página HTML|templates], pero también puede ser el envío de un archivo, JSON o incluso una redirección a otra página. +Envío de una respuesta +====================== -En cualquier momento durante el ciclo de vida, podemos enviar una respuesta con uno de los siguientes métodos y, al mismo tiempo, finalizar el presenter: +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. -- `redirect()`, `redirectPermanent()`, `redirectUrl()` y `forward()` [redirigen |#Redirección] -- `error()` finaliza el presenter [debido a un error |#Error 404 y cía] -- `sendJson($data)` finaliza el presenter y [envía datos |#Envío de JSON] en formato JSON -- `sendTemplate()` finaliza el presenter e inmediatamente [renderiza la plantilla |templates] -- `sendResponse($response)` finaliza el presenter y envía una [respuesta propia |#Respuestas] -- `terminate()` finaliza el presenter sin respuesta +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: -Si no llama a ninguno de estos métodos, el presenter procederá automáticamente a renderizar la plantilla. ¿Por qué? Porque en el 99% de los casos queremos renderizar una plantilla, por lo que el presenter toma este comportamiento como predeterminado y quiere facilitarnos el trabajo. +- `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 dispone del método `link()`, mediante el cual se pueden crear enlaces URL a otros presenters. El primer parámetro es el presenter y la acción de destino, seguido de los argumentos pasados, que pueden indicarse como un array: +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' => 'cs']); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -En la plantilla, se crean enlaces a otros presenters y acciones de esta manera: +En la plantilla, los enlaces a otros presenters y acciones se crean así: ```latte -detalle del producto +product detail ``` -Simplemente, en lugar de la URL real, escribe el par conocido `Presenter:action` e indica los parámetros necesarios. El truco está en `n:href`, que indica que este atributo será procesado por Latte y generará la URL real. En Nette, por lo tanto, no necesita pensar en absoluto en las URL, solo en los presenters y las acciones. +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]. @@ -150,50 +175,50 @@ Encontrará más información en el capítulo [Creación de enlaces URL|creating Redirección =========== -Para pasar a otro presenter se utilizan los métodos `redirect()` y `forward()`, que tienen una sintaxis muy similar al método [link() |#Creación de enlaces]. +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: +El método `forward()` pasa al nuevo presenter inmediatamente, sin redirección HTTP: ```php $this->forward('Product:show'); ``` -Ejemplo de la llamada redirección temporal con código HTTP 302 (o 303, si el método de la petición actual es POST): +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); ``` -La redirección permanente con código HTTP 301 se logra así: +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 se puede redirigir con el método `redirectUrl()`. Como segundo parámetro se puede indicar el código HTTP, el predeterminado es 302 (o 303, si el método de la petición actual es POST): +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 finaliza inmediatamente la actividad del presenter lanzando la llamada excepción de finalización silenciosa `Nette\Application\AbortException`. +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 puede enviar un [mensaje flash |#Mensajes flash], es decir, mensajes que se mostrarán en la plantilla después de la redirección. +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 generalmente informan sobre el resultado de alguna operación. Una característica importante de los mensajes flash es que están disponibles en la plantilla incluso después de una redirección. Incluso después de mostrarse, permanecen activos durante otros 30 segundos, por ejemplo, en caso de que el usuario actualice la página debido a una transmisión errónea, el mensaje no desaparecerá de inmediato. +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 encargará de pasarlo a la plantilla. El primer parámetro es el texto del mensaje y el segundo parámetro opcional es su tipo (error, warning, info, etc.). El método `flashMessage()` devuelve una instancia del mensaje flash, a la que se le puede agregar información adicional. +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('El elemento ha sido eliminado.'); -$this->redirect(/* ... */); // y redirigimos +$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` (texto del mensaje), `type` (tipo de mensaje) y pueden contener la información de usuario ya mencionada. Los renderizamos, por ejemplo, así: +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} @@ -202,10 +227,10 @@ En la plantilla, estos mensajes están disponibles en la variable `$flashes` com ``` -Error 404 y cía. -================ +Error 404 y otros +================= -Si no se puede cumplir 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 = null, int $httpCode = 404)`. +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 @@ -218,13 +243,13 @@ public function renderShow(int $id): void } ``` -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 lo cual `Application` pasa el control al error-presenter. Que es un presenter cuya tarea es mostrar una página informando sobre el error ocurrido. La configuración del error-presenter se realiza en la [configuración de application|configuration]. +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 ============= -Ejemplo de un método de acción que envía datos en formato JSON y finaliza el presenter: +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 @@ -238,9 +263,9 @@ public function actionData(): void Parámetros de la petición .{data-version:3.1.14} ================================================ -El presenter y también cada componente obtienen sus parámetros de la petición HTTP. Puede obtener su valor con el método `getParameter($name)` o `getParameters()`. Los valores son cadenas o arrays de cadenas, son básicamente datos brutos obtenidos directamente de la URL. +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 a través de propiedades. Basta con marcarlas con el atributo `#[Parameter]`: +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 @@ -252,9 +277,9 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Recomendamos indicar también el tipo de dato para la propiedad (p. ej., `string`) y Nette lo convertirá automáticamente según él. Los valores de los parámetros también se pueden [validar |#Validación de parámetros]. +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, se puede establecer directamente el valor de los parámetros: +Al crear un enlace puede establecer directamente el valor del parámetro: ```latte click @@ -264,11 +289,11 @@ Al crear un enlace, se puede establecer directamente el valor de los parámetros Parámetros persistentes ======================= -Los parámetros persistentes sirven para mantener el estado entre diferentes peticiones. Su valor permanece igual incluso después de hacer clic en un enlace. A diferencia de los datos en la sesión, se transfieren en la URL. Y esto de forma totalmente automática, por lo que no es necesario indicarlos explícitamente en `link()` o `n:href`. +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? Tiene una aplicación multilingüe. El idioma actual es un parámetro que debe estar constantemente presente en la URL. Pero sería increíblemente tedioso indicarlo en cada enlace. Así que lo convierte en un parámetro persistente `lang` y se transferirá solo. ¡Genial! +¿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 es extremadamente simple en Nette. Basta con crear una propiedad pública y marcarla con un atributo: (anteriormente se usaba `/** @persistent */`) +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 @@ -280,14 +305,14 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Si `$this->lang` tiene el valor, por ejemplo, `'en'`, entonces también los enlaces creados mediante `link()` o `n:href` contendrán el parámetro `lang=en`. Y después de hacer clic en el enlace, nuevamente `$this->lang = 'en'`. +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'`. -Recomendamos indicar también el tipo de dato para la propiedad (p. ej., `string`) y puede indicar también un valor predeterminado. Los valores de los parámetros se pueden [validar |#Validación de parámetros]. +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 estándarmente entre todas las acciones del presenter dado. Para que se transfieran también entre varios presenters, es necesario definirlos ya sea: +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 ancestro común del que heredan los presenters -- en un trait que usen los presenters: +- en un antecesor común del que hereden los presenters +- o en un trait que usen los presenters: ```php trait LanguageAware @@ -302,42 +327,62 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Al crear un enlace, se puede cambiar el valor del parámetro persistente: +Al crear un enlace se puede cambiar el valor de un parámetro persistente: ```latte -detalle en checo +detail in Czech ``` -O se puede *resetear*, es decir, eliminar de la URL. Entonces tomará su valor predeterminado: +O se puede *resetear*, es decir, eliminar de la URL. Entonces adoptará su valor por defecto: ```latte -haz clic +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 tienen incorporado un sistema de componentes. Los componentes son unidades reutilizables independientes que insertamos en los presenters. Pueden ser [formularios |forms:in-presenter], datagrids, menús, en realidad cualquier cosa que tenga sentido usar repetidamente. +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 el presenter y se usan posteriormente? Eso lo aprenderá en el capítulo [Componentes |components]. Incluso descubrirá qué tienen en común con Hollywood. +¿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 obtener componentes? En la página [Componette |https://componette.org/search/component] encontrará componentes de código abierto y también muchos otros complementos para Nette, que voluntarios de la comunidad alrededor del framework han colocado aquí. +¿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. -Vamos a profundizar -=================== +Profundizando +============= .[tip] -Con lo que hemos mostrado hasta ahora en este capítulo, probablemente le sea suficiente. Las siguientes líneas están destinadas a aquellos que se interesan por los presenters en profundidad y quieren saber absolutamente todo. +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 parámetros ------------------------- +Validación de los parámetros +---------------------------- -Los valores de los [#parámetros de la petición] y los [#parámetros persistentes] recibidos de la URL se escriben en las propiedades mediante el método `loadState()`. Este también comprueba si el tipo de dato indicado en la propiedad coincide, de lo contrario responde con un error 404 y la página no se muestra. +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, ya que pueden ser fácilmente sobrescritos por el usuario en la URL. Así, por ejemplo, verificamos si el idioma `$this->lang` está entre los soportados. Una forma adecuada es sobrescribir el método mencionado `loadState()`: +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 @@ -348,7 +393,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function loadState(array $params): void { parent::loadState($params); // aquí se establece $this->lang - // sigue la verificación propia del valor: + // sigue la comprobación propia del valor: if (!in_array($this->lang, ['en', 'cs'])) { $this->error(); } @@ -357,65 +402,47 @@ class ProductPresenter extends Nette\Application\UI\Presenter ``` -Guardado y restauración de la petición --------------------------------------- +Guardar y restaurar la petición +------------------------------- -La petición que gestiona el presenter es un objeto [api:Nette\Application\Request] y lo devuelve el método del presenter `getRequest()`. +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, por el contrario, restaurarla desde ella y hacer que el presenter la ejecute de nuevo. Esto es útil, por ejemplo, en una situación en la que el usuario está rellenando un formulario y su sesión expira. Para no perder los datos, antes de redirigir a la página de inicio de sesión, guardamos la petición actual en la sesión usando `$reqId = $this->storeRequest()`, que devuelve su identificador en forma de cadena corta y lo pasamos como parámetro al presenter de inicio de sesión. +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. -Después de iniciar sesión, llamamos al método `$this->restoreRequest($reqId)`, que recupera la petición de la sesión y hace forward a ella. El método, al mismo tiempo, verifica que la petición la creó el mismo usuario que ahora ha iniciado sesión. Si iniciara sesión otro usuario o la clave fuera inválida, no hace nada y el programa continúa. +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. -Consulte el tutorial [Cómo volver a una página anterior |best-practices:restore-request]. +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 genial que contribuye a un mejor SEO (optimización para motores de búsqueda). Evitan automáticamente la existencia de contenido duplicado en diferentes URL. Si a un destino determinado conducen varias direcciones URL, p. ej., `/index` y `/index?page=1`, el framework determina una de ellas como primaria (canónica) y redirige las demás a ella usando el código HTTP 301. Gracias a esto, los motores de búsqueda no indexan sus páginas dos veces y no diluyen su page rank. +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], generalmente, por lo tanto, la primera ruta correspondiente en la colección. +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 por defecto y se puede desactivar mediante `$this->autoCanonicalize = false`. +La canonización está activada de forma predeterminada y se puede desactivar con `$this->autoCanonicalize = false`. -La redirección no ocurre en una petición AJAX o POST, porque se perderían datos o no tendría valor añadido desde el punto de vista del SEO. +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 invocar la canonización manualmente usando el método `canonicalize()`, al que, de manera similar al método `link()`, se le pasa el presenter, la acción y los parámetros. Crea un enlace y lo compara con la dirección URL actual. Si difieren, redirige al enlace generado. +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 difiere de $realSlug + // redirige si $slug es distinto de $realSlug $this->canonicalize('Product:show', [$id, $realSlug]); } ``` - -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 deben llamarse automáticamente. El presenter define los llamados [eventos |nette:glossary#Eventos], cuyos manejadores agrega a los arrays `$onStartup`, `$onRender` y `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Los manejadores en el array `$onStartup` se llaman justo antes del método `startup()`, luego `$onRender` entre `beforeRender()` y `render()` y finalmente `$onShutdown` justo antes de `shutdown()`. +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 preparadas: +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 @@ -436,7 +463,7 @@ $this->sendResponse(new Responses\TextResponse('Hello Nette!')); // Envía un archivo $this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); -// La respuesta será un callback +// Envía un callback $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { if ($httpResponse->getHeader('Content-Type') === 'text/html') { echo '

    Hello

    '; @@ -445,28 +472,88 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht $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 mediante `#[Requires]` .{data-version:3.2.2} ------------------------------------------------------------------- +Restricción de acceso con `#[Requires]` .{data-version:3.2.3} +------------------------------------------------------------- -El atributo `#[Requires]` proporciona opciones avanzadas para restringir el acceso a presenters y sus métodos. Se puede usar para especificar métodos HTTP, requerir una petición AJAX, restringir al mismo origen (same origin) y acceso solo a través de forward. El atributo se puede aplicar tanto a clases de presenters como a métodos individuales `action()`, `render()`, `handle()` y `createComponent()`. +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 especificar estas restricciones: -- a métodos HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- requerir una petición AJAX: `#[Requires(ajax: true)]` +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 a través de forward: `#[Requires(forward: true)]` -- restricción a acciones específicas: `#[Requires(actions: 'default')]` +- 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. -Encontrará detalles en el tutorial [Cómo usar el atributo Requires |best-practices:attribute-requires]. +Los detalles los encontrará en la guía [Cómo usar el atributo Requires |best-practices:attribute-requires]. -Verificación del método HTTP +Comprobación del método HTTP ---------------------------- -Los presenters en Nette verifican automáticamente el método HTTP de cada petición entrante. La razón de esta verificación es principalmente la seguridad. Por defecto, se permiten los métodos `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. +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 desea permitir adicionalmente, por ejemplo, el método `OPTIONS`, use para ello el atributo `#[Requires]` (desde Nette Application v3.2): +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'])] @@ -475,26 +562,34 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -En la versión 3.1, la verificación se realiza en `checkHttpMethod()`, que comprueba si el método especificado en la petición está contenido en el array `$presenter->allowedMethods`. La adición del método se hace así: +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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -Es importante destacar que si permite el método `OPTIONS`, debe luego también gestionarlo adecuadamente dentro de su presenter. El método se usa a menudo como la llamada petición preflight, que el navegador envía automáticamente antes de la petición real, cuando es necesario averiguar si la petición está permitida desde el punto de vista de la política CORS (Cross-Origin Resource Sharing). Si permite el método, pero no implementa la respuesta correcta, puede llevar a inconsistencias y posibles problemas de seguridad. +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. -Lectura adicional -================= +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] -- [Composición de presenters a partir de traits |best-practices:presenter-traits] -- [Paso de configuraciones a presenters |best-practices:passing-settings-to-presenters] +- [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 index 829edb3c7d..7aa9ce9fb1 100644 --- a/application/es/routing.texy +++ b/application/es/routing.texy @@ -3,26 +3,26 @@ Enrutamiento
    -El Router se encarga de todo lo relacionado con las direcciones URL, para que usted ya no tenga que pensar en ellas. Mostraremos: +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 sean según sus deseos -- hablaremos de SEO y redirección -- y mostraremos cómo escribir su propio router +- 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 humanas (o también cool o pretty URL) son más usables, memorables y contribuyen positivamente al SEO. Nette piensa en esto y apoya plenamente a los desarrolladores. Puede diseñar para su aplicación exactamente la estructura de direcciones URL que desee. Incluso puede diseñarla cuando la aplicación ya está terminada, porque se puede hacer sin intervenciones en el código o las plantillas. Se define de manera elegante en un [único lugar |#Integración en la aplicación], en el router, y no está dispersa en forma de anotaciones en todos los presenters. +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 extraordinario porque es **bidireccional.** Sabe tanto decodificar URL en la petición HTTP como crear enlaces. Juega, por lo tanto, un papel fundamental en [Nette Application |how-it-works#Nette Application], porque por un lado decide qué presenter y acción ejecutará la petición actual, pero también se utiliza para [generar URL |creating-links] en la plantilla, etc. +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. -Sin embargo, el router no está limitado solo a este uso, puede usarlo en aplicaciones donde no se usan presenters en absoluto, para API REST, etc. Más en la sección [#Uso independiente]. +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 apariencia de las direcciones URL en la 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 sus presenters y acciones asociados mediante una API simple. No necesitamos nombrar las rutas de ninguna manera. +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; @@ -31,16 +31,16 @@ $router->addRoute('article/', 'Article:view'); // ... ``` -El ejemplo dice que si abrimos `https://domain.com/rss.xml` en el navegador, se mostrará el presenter `Feed` con la acción `rss`, si `https://domain.com/article/12`, se mostrará el presenter `Article` con la acción `view`, etc. En caso de no encontrar una ruta adecuada, Nette Application reacciona lanzando una excepción [BadRequestException |api:Nette\Application\BadRequestException], que se muestra al usuario como una página de error 404 Not Found. +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 ------------------ -Es absolutamente **clave el orden** en que se indican las rutas individuales, porque se evalúan secuencialmente de arriba abajo. Se aplica la regla de que declaramos las rutas **de específicas a generales**: +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 esta cadena como +// MAL: 'rss.xml' lo captura la primera ruta y entiende esa cadena como $router->addRoute('', 'Article:view'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -49,7 +49,7 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('', 'Article:view'); ``` -Las rutas también se evalúan de arriba abajo al generar enlaces: +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' @@ -61,57 +61,57 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('admin//', 'Admin:default'); ``` -No le ocultaremos que la correcta composición de las rutas requiere cierta habilidad. Antes de que la domine, le será útil el [panel de enrutamiento |#Depuración del router]. +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 simple es una URL estática: +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**. Estos se indican 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`: +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 dice que si abrimos `https://example.com/chronicle/2020` en el navegador, se mostrará el presenter `History` con la acción `show` y el parámetro `year: 2020`. +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`. -Podemos asignar un valor predeterminado a los parámetros directamente en la máscara y así se vuelven opcionales: +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 ahora aceptará también la URL `https://example.com/chronicle/`, que nuevamente mostrará `History:show` con el parámetro `year: 2020`. +La ruta aceptará ahora también la URL `https://example.com/chronicle/`, que mostrará de nuevo `History:show` con el parámetro `year: 2020`. -El parámetro puede ser, por supuesto, también el nombre del presenter y la acción. Por ejemplo, así: +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, p. ej., URL en la forma `/article/edit` o también `/catalog/list` y las entiende como presenters y acciones `Article:edit` y `Catalog:list`. +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 predeterminados `Home` y `default` y, por lo tanto, también son opcionales. Así que la ruta acepta también URL en la forma `/article` y la entiende como `Article:default`. O al revés, un enlace a `Product:default` generará la ruta `/product`, un enlace al predeterminado `Home:default` la ruta `/`. +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 la ruta absoluta, si comienza con una barra inclinada, o incluso una URL absoluta completa, si comienza con dos barras inclinadas: +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 -// relativo al document root +// relativa al document root $router->addRoute('/', /* ... */); // ruta absoluta (relativa al dominio) $router->addRoute('//', /* ... */); -// URL absoluta incluyendo dominio (relativa al esquema) +// URL absoluta incluido el dominio (relativa al esquema) $router->addRoute('//.example.com//', /* ... */); -// URL absoluta incluyendo esquema +// URL absoluta incluido el esquema $router->addRoute('https://.example.com//', /* ... */); ``` @@ -119,13 +119,13 @@ $router->addRoute('https://.example.com//', /* ... */); Expresiones de validación ------------------------- -Para cada parámetro se puede establecer una condición de validación mediante una [expresión regular|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Por ejemplo, para el parámetro `id` determinamos que solo puede tomar dígitos usando la expresión regular `\d+`: +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 excepto la barra inclinada. Si un parámetro debe aceptar también barras inclinadas, indicamos la expresión `.+`: +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' @@ -136,19 +136,19 @@ $router->addRoute('', /* ... */); Secuencias opcionales --------------------- -En la máscara se pueden marcar partes opcionales usando corchetes. Opcional puede ser cualquier parte de la máscara, también pueden contener parámetros: +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 rutas: -// /cs/download => lang => cs, name => download +// Acepta las rutas: +// /en/download => lang => en, name => download // /download => lang => null, name => download ``` -Cuando un parámetro es parte de una secuencia opcional, se vuelve, por supuesto, también opcional. Si no tiene un valor predeterminado indicado, será null. +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 también pueden estar en el dominio: +Las partes opcionales pueden estar también en el dominio: ```php $router->addRoute('//[.]example.com//', /* ... */); @@ -162,14 +162,14 @@ $router->addRoute( 'Home:default', ); -// Acepta rutas: -// /cs/hello +// Acepta las rutas: +// /en/hello // /en-us/hello // /hello // /hello/page-12 ``` -Al generar URL, se busca la variante más corta, por lo que todo lo que se puede omitir, se omite. Por eso, por ejemplo, la ruta `index[.html]` genera la ruta `/index`. Se puede invertir el comportamiento indicando un signo de exclamación después del corchete izquierdo: +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 @@ -179,7 +179,7 @@ $router->addRoute('[.html]', /* ... */); $router->addRoute('[!.html]', /* ... */); ``` -Los parámetros opcionales (es decir, parámetros que tienen un valor predeterminado) sin corchetes se comportan básicamente como si estuvieran entre paréntesis de la siguiente manera: +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('//', /* ... */); @@ -188,7 +188,7 @@ $router->addRoute('//', /* ... */); $router->addRoute('[/[/[]]]', /* ... */); ``` -Si quisiéramos influir en el comportamiento de la barra inclinada final, para que, por ejemplo, en lugar de `/home/` se genere solo `/home`, se puede lograr así: +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('[[/[/]]]', /* ... */); @@ -198,24 +198,24 @@ $router->addRoute('[[/[/]]]', /* ... */); Comodines --------- -En la máscara de ruta absoluta, podemos usar los siguientes comodines y evitar así, por ejemplo, la necesidad de escribir en la máscara el dominio, que puede diferir en el entorno de desarrollo y producción: +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 nivel superior, p. ej., `com` u `org` -- `%sld%` = dominio de segundo nivel, p. ej., `example` -- `%domain%` = dominio sin subdominios, p. ej., `example.com` -- `%host%` = host completo, p. ej., `www.example.com` +- `%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%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` -Notación extendida ------------------- +Notación avanzada +----------------- -El destino de la ruta, generalmente escrito en la forma `Presenter:action`, también puede escribirse usando un array que define los parámetros individuales y sus valores predeterminados: +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('/[/]', [ @@ -224,7 +224,7 @@ $router->addRoute('/[/]', [ ]); ``` -Para una especificación más detallada, se puede usar una forma aún más extendida, donde además de los valores predeterminados podemos establecer otras propiedades de los parámetros, como por ejemplo la expresión regular de validación (ver parámetro `id`): +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; @@ -242,19 +242,29 @@ $router->addRoute('/[/]', [ ]); ``` -Es importante señalar que si los parámetros definidos en el array no se indican en la máscara de la ruta, sus valores no se pueden cambiar, ni siquiera mediante parámetros de consulta indicados después del signo de interrogación en la URL. +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 ---------------------- -Escribimos los códigos fuente de la aplicación en inglés, pero si el sitio web debe tener URL en español, entonces un enrutamiento simple como: +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 en español (p. ej., `/producto/123` o `/carrito`), podemos utilizar un diccionario de traducción. Para su escritura ya necesitamos la variante "más detallada" del segundo parámetro: +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; @@ -264,25 +274,25 @@ $router->addRoute('/', [ Route::Value => 'Home', Route::FilterTable => [ // cadena en la URL => presenter - 'producto' => 'Product', - 'carrito' => 'Cart', - 'catalogo' => 'Catalog', + 'produkt' => 'Product', + 'kosik' => 'Cart', + 'katalog' => 'Catalog', ], ], 'action' => [ Route::Value => 'default', Route::FilterTable => [ - 'lista' => 'list', + 'seznam' => 'list', ], ], ]); ``` -Varias claves del diccionario de traducción pueden llevar al mismo presenter. De esta manera se crean diferentes alias para él. La variante canónica (es decir, la que estará en la URL generada) se considera la última clave. +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. Este comportamiento se puede cambiar agregando `Route::FilterStrict => true` y la ruta rechazará la URL si el valor no está en el diccionario. +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 implementar funciones de traducción propias. +Además del diccionario de traducción en forma de array, se pueden emplear funciones de traducción propias. ```php use Nette\Routing\Route; @@ -298,15 +308,15 @@ $router->addRoute('//', [ ]); ``` -La función `Route::FilterIn` convierte entre el parámetro en la URL y la cadena que luego se pasa al presenter, la función `FilterOut` asegura la conversión en la dirección opuesta. +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 resp. camelCase y kebab-case utilizado en la URL. El valor predeterminado de los parámetros se escribe ya en la forma transformada, por lo que, por ejemplo, en el caso del presenter escribimos ``, no ``. +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 específicos, también podemos definir filtros generales que reciben un array asociativo de todos los parámetros, que pueden modificar de cualquier manera y luego devolverlos. Los filtros generales los definimos bajo la clave `null`. +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; @@ -314,85 +324,101 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -Los filtros generales dan la posibilidad de modificar el comportamiento de la ruta de absolutamente cualquier manera. Podemos usarlos, por ejemplo, para modificar parámetros basándose en otros parámetros. Por ejemplo, la traducción de `` y `` basada en el valor actual del parámetro ``. +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 un filtro propio y al mismo tiempo existe un filtro general, se ejecuta el `FilterIn` propio antes del general y, a la inversa, el `FilterOut` general antes del propio. Es decir, dentro del filtro general, los valores de los parámetros `presenter` resp. `action` están escritos en estilo PascalCase resp. camelCase. +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. -Rutas de un solo sentido OneWay -------------------------------- -Las rutas de un solo sentido se utilizan para mantener la funcionalidad de las URL antiguas que la aplicación ya no genera, pero sigue aceptando. Las marcamos con el indicador `OneWay`: +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', $router::ONE_WAY); -// nueva URL /product/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 URL, por lo que los motores de búsqueda no indexarán estas páginas dos veces (ver [#SEO y canonización]). +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 funciones (callbacks) a las rutas, que se ejecutarán cuando se visite la ruta dada. Esta funcionalidad flexible le permite crear rápida y eficientemente diferentes puntos finales (endpoints) para su aplicación: +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 'estás en la dirección /test'; + echo 'You are at the /test address'; }); ``` -También puede definir parámetros en la máscara, que se pasarán automáticamente a su callback: +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' => '¡Bienvenido a la versión checa de nuestro sitio web!', + '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, utilizamos `withModule()`: +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 siguientes rutas son parte del módulo Forum +$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 siguientes rutas son parte del módulo Forum:Admin + ->withModule('Admin') // las rutas siguientes forman parte del módulo Forum:Admin ->addRoute('sign:in', 'Sign:in'); ``` -Una alternativa es usar el parámetro `module`: +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 +// 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 ----------- -Podemos dividir las colecciones de rutas según subdominios: +Las colecciones de rutas se pueden dividir según los subdominios: ```php $router = new RouteList; @@ -401,7 +427,7 @@ $router->withDomain('example.com') ->addRoute('/'); ``` -En el nombre del dominio también se pueden usar [#Comodines]: +En el nombre del dominio se pueden usar también [#Comodines]: ```php $router = new RouteList; @@ -413,20 +439,20 @@ $router->withDomain('example.%tld%') Prefijo de ruta --------------- -Podemos dividir las colecciones de rutas según la ruta en la URL: +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') // captura la URL /eshop/rss - ->addRoute('/'); // captura la URL /eshop// + ->addRoute('rss', 'Feed:rss') // coincide con la URL /eshop/rss + ->addRoute('/'); // coincide con la URL /eshop// ``` Combinaciones ------------- -Podemos combinar las divisiones anteriores entre sí: +Las agrupaciones anteriores se pueden combinar entre sí: ```php $router = (new RouteList) @@ -449,34 +475,34 @@ $router = (new RouteList) Parámetros de consulta ---------------------- -Las máscaras también pueden contener parámetros de consulta (parámetros después del signo de interrogación en la URL). A estos no se les puede definir una expresión de validación, pero se puede cambiar el nombre bajo el cual se pasan al presenter: +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' en la aplicación bajo el nombre 'categoryId' +// queremos usar el parámetro de consulta 'cat' con el nombre 'categoryId' en la aplicación $router->addRoute('product ? id= & cat=', /* ... */); ``` -Parámetros Foo +Parámetros foo -------------- -Ahora vamos más profundo. Los parámetros Foo son básicamente parámetros sin nombre que permiten hacer coincidir una expresión regular. Un ejemplo es una ruta que acepta `/index`, `/index.html`, `/index.htm` y `/index.php`: +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 se puede definir explícitamente la cadena que se usará al generar la URL. La cadena debe colocarse directamente después del signo de interrogación. La siguiente ruta es similar a la anterior, pero genera `/index.html` en lugar de `/index`, porque la cadena `.html` está configurada como valor de generación: +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 en la aplicación -============================ +Integración +=========== -Para incorporar el router creado en la aplicación, debemos decírselo al contenedor DI. La forma más fácil es preparar una fábrica que produzca el objeto router e indicar en la configuración del contenedor que debe usarla. Supongamos que para este propósito escribimos el método `App\Core\RouterFactory::createRouter()`: +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; @@ -494,14 +520,14 @@ class RouterFactory } ``` -En la [configuración |dependency-injection:services] luego escribimos: +Después escribimos en la [configuración |dependency-injection:services]: ```neon services: - App\Core\RouterFactory::createRouter ``` -Cualquier dependencia, por ejemplo, a la base de datos, etc., se pasa al método de fábrica como sus parámetros mediante [autowiring|dependency-injection:autowiring]: +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 @@ -514,15 +540,15 @@ public static function createRouter(Nette\Database\Connection $db): RouteList SimpleRouter ============ -Un router mucho más simple que la colección de rutas es [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Lo usamos cuando no tenemos requisitos especiales sobre la forma de la URL, si no está disponible `mod_rewrite` (o sus alternativas) o si aún no queremos ocuparnos de URL bonitas. +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 aproximadamente en esta forma: +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 a los que se debe dirigir si abrimos la página sin parámetros, p. ej., `http://example.com/`. +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' @@ -540,19 +566,19 @@ services: SEO y canonización ================== -El framework contribuye al SEO (optimización para motores de búsqueda) evitando la duplicidad de contenido en diferentes URL. Si a un destino determinado conducen varias direcciones, p. ej., `/index` y `/index.html`, el framework determina la primera de ellas como primaria (canónica) y redirige las demás a ella usando el código HTTP 301. Gracias a esto, los motores de búsqueda no indexan sus páginas dos veces y no diluyen su page rank. +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 que cumple en la colección sin el indicador OneWay. Por eso, en la colección indicamos **las rutas primarias primero**. +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**. -La canonización la realiza el presenter, más en el capítulo [canonización |presenters#Canonización]. +De la canonización se encarga el presenter, más en el capítulo [canonización |presenters#Canonización]. HTTPS ===== -Para poder usar el protocolo HTTPS, es necesario habilitarlo en el hosting y configurar correctamente el servidor. +Para usar el protocolo HTTPS es necesario activarlo en el hosting y configurar el servidor correctamente. -La redirección de todo el sitio a HTTPS debe configurarse a nivel de servidor, por ejemplo, mediante el archivo .htaccess en el directorio raíz de nuestra aplicación, y con el código HTTP 301. La configuración puede variar según el hosting y se ve aproximadamente así: +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: ``` @@ -564,15 +590,15 @@ La redirección de todo el sitio a HTTPS debe configurarse a nivel de servidor, ``` -El router genera URL con el mismo protocolo con el que se cargó la página, por lo que no es necesario configurar nada más. +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. -Pero si excepcionalmente necesitamos que diferentes rutas se ejecuten bajo diferentes protocolos, lo indicamos en la máscara de la ruta: +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 con HTTP +// Generará una dirección HTTP $router->addRoute('http://%host%//', /* ... */); -// Generará una dirección con HTTPs +// Generará una dirección HTTPS $router->addRoute('https://%host%//', /* ... */); ``` @@ -580,24 +606,24 @@ $router->addRoute('https://%host%//', /* ... */); Depuración del router ===================== -El panel de enrutamiento que se muestra en [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. +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, en color azul y con el símbolo ≈ están marcadas las rutas que también procesarían la URL si la verde no se les hubiera adelantado. Además, vemos el presenter y la acción actuales. +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 ocurre una redirección inesperada debido a la [canonización |#SEO y canonización], es útil mirar el panel en la barra *redirect*, donde descubrirá cómo el router entendió originalmente la URL y por qué redirigió. +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 de desarrollador en el navegador (Ctrl+Shift+I o Cmd+Option+I) y en el panel Network desactivar la caché, para que no se guarden las redirecciones en ella. +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 definitivamente no debería exceder varias decenas. Si su sitio web tiene una estructura de URL demasiado complicada, puede escribir un [#Router propio] a medida. +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, a la base de datos, y su fábrica no acepta ningún argumento, podemos serializar su forma compilada directamente en el contenedor DI y así acelerar ligeramente la aplicación. +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: @@ -608,7 +634,7 @@ routing: Router propio ============= -Las siguientes líneas están destinadas a usuarios muy avanzados. Puede crear su propio router e integrarlo de forma completamente natural en la colección de rutas. El router es una implementación de la interfaz [api:Nette\Routing\Router] con dos métodos: +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; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -El método `match` procesa la petición actual [$httpRequest |http:request], de la cual se puede obtener no solo la URL, sino también las cabeceras, etc., 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 como mínimo el presenter y la acción. El nombre del presenter es completo y contiene también posibles módulos: +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 [ @@ -637,9 +663,9 @@ El método `match` procesa la petición actual [$httpRequest |http:request], de ] ``` -El método `constructUrl`, por el contrario, construye la URL absoluta resultante a partir del array de parámetros. Para ello puede utilizar información del parámetro [`$refUrl`|api:Nette\Http\UrlScript], que es la URL actual. +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 agrega a la colección de rutas usando `add()`: +Lo añadimos a la colección de rutas con `add()`: ```php $router = new Nette\Application\Routers\RouteList; @@ -652,13 +678,13 @@ $router->addRoute(/* ... */); Uso independiente ================= -Por uso independiente entendemos el uso de las capacidades del router en una aplicación que no utiliza Nette Application ni presenters. Se aplica casi todo lo que hemos mostrado en este capítulo, con estas diferencias: +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 colecciones de rutas usamos la clase [api:Nette\Routing\RouteList] -- como simple router la clase [api:Nette\Routing\SimpleRouter] -- como no existe el par `Presenter:action`, usamos la [#Notación extendida] +- 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 nuevamente creamos un método que nos construya el router, p. ej.: +Así que creamos de nuevo un método que nos monte el router, p. ej.: ```php namespace App\Core; @@ -682,35 +708,35 @@ class RouterFactory } ``` -Si usa un contenedor DI, lo cual recomendamos, nuevamente agregamos el método a la configuración y luego obtenemos el router junto con la petición HTTP del contenedor: +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 fabricamos los objetos directamente: +O cree los objetos directamente: ```php $router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` -Ahora solo queda poner el router a trabajar: +Ahora ya solo queda dejar que el router haga su trabajo: ```php $params = $router->match($httpRequest); if ($params === null) { - // no se encontró una ruta que cumpliera, enviamos error 404 + // no se encontró ninguna ruta coincidente, se envía un error 404 exit; } -// procesamos los parámetros obtenidos +// procesa los parámetros obtenidos $controller = $params['controller']; // ... ``` -Y a la inversa, usamos el router para construir un enlace: +Y al revés, use el router para construir un enlace: ```php $params = ['controller' => 'ArticleController', 'id' => 123]; @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/es/templates.texy b/application/es/templates.texy index b9c7148897..1aa2c3ba8d 100644 --- a/application/es/templates.texy +++ b/application/es/templates.texy @@ -2,15 +2,15 @@ Plantillas ********** .[perex] -Nette utiliza el sistema de plantillas [Latte |latte:]. Por un lado, porque es el sistema de plantillas más seguro para PHP y, al mismo tiempo, el sistema más intuitivo. No necesita aprender mucho nuevo, le basta con conocer PHP y algunas etiquetas. +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 + la plantilla de la acción dada. Así puede verse, por ejemplo, una plantilla de layout, observe los bloques `{block}` y la etiqueta `{include}`: +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}Mi Aplicación{/block} + {block title}My App{/block}
    ...
    @@ -20,26 +20,26 @@ Es habitual que una página se componga de una plantilla de layout + la plantill ``` -Y esta será la plantilla de la acción: +Y esta sería la plantilla de la acción: ```latte -{block title}Página de inicio{/block} +{block title}Homepage{/block} {block content} -

    Página de inicio

    +

    Homepage

    ... {/block} ``` -Esta define el bloque `content`, que se inserta en lugar de `{include content}` en el layout, y también re-define el bloque `title`, que sobrescribe `{block title}` en el layout. Intente imaginar el resultado. +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 ---------------------- -No necesita indicar en los presenters qué plantilla se debe renderizar, el framework deduce la ruta por sí mismo y le ahorra escribir. +En los presenters no necesita indicar qué plantilla debe renderizarse; el framework deduce la ruta automáticamente y le ahorra escribirla. -Si utiliza una estructura de directorios donde cada presenter tiene su propio directorio, simplemente coloque la plantilla en este directorio bajo el nombre de la acción (resp. vista), es decir, para la acción `default` use la plantilla `default.latte`: +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/ @@ -49,34 +49,34 @@ app/ └── default.latte \-- -Si utiliza una estructura donde los presenters están juntos en un directorio y las plantillas en la carpeta `templates`, guárdela ya sea en el archivo `..latte` o `/.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 + ├── Home/ + │ └── default.latte ← 1.ª variante + └── Home.default.latte ← 2.ª variante \-- -El directorio `templates` también puede estar ubicado un nivel más arriba, es decir, al mismo nivel que el directorio con las clases de los presenters. +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 cía]. +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 usando `$this->setView('otraVista')`. También puede especificar directamente el archivo de plantilla usando `$this->template->setFile('/ruta/a/plantilla.latte')`. +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 donde se buscan las plantillas se pueden cambiar sobrescribiendo el método [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], que devuelve un array de posibles nombres de archivo. +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 también localiza automáticamente el archivo de layout. +Nette busca también automáticamente el archivo del layout. -Si utiliza una estructura de directorios donde cada presenter tiene su propio directorio, coloque el layout ya sea en la carpeta con el presenter, si es específico solo para él, o un nivel más arriba, si es común para varios presenters: +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/ @@ -88,7 +88,7 @@ app/ └── default.latte \-- -Si utiliza una estructura donde los presenters están juntos en un directorio y las plantillas en la carpeta `templates`, se esperará el layout en estos lugares: +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/ @@ -96,33 +96,66 @@ app/ ├── HomePresenter.php └── templates/ ├── @layout.latte ← layout común - ├── Home.@layout.latte ← solo para Home, 1ª variante - └── Home/ - └── @layout.latte ← solo para Home, 2ª variante + ├── Home/ + │ └── @layout.latte ← solo para Home, 1.ª variante + └── Home.@layout.latte ← solo para Home, 2.ª variante \-- -Si el presenter se encuentra en un módulo, también se buscará en niveles de directorio superiores, según el anidamiento del módulo. +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 usando `$this->setLayout('layoutAdmin')` y entonces se esperará en el archivo `@layoutAdmin.latte`. También se puede especificar directamente el archivo de plantilla de layout usando `$this->setLayout('/ruta/a/plantilla.latte')`. +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')`. -Usando `$this->setLayout(false)` o la etiqueta `{layout none}` dentro de la plantilla, se desactiva la búsqueda de layout. +Usar `$this->setLayout(false)` o la etiqueta `{layout none}` dentro de la plantilla desactiva la búsqueda del layout. .[note] -Los archivos donde se buscan las plantillas de layout se pueden cambiar sobrescribiendo el método [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], que devuelve un array de posibles nombres de archivo. +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 en la plantilla +Variables de la plantilla ------------------------- -Pasamos variables a la plantilla escribiéndolas en `$this->template` y luego las tenemos disponibles en la plantilla como variables locales: +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); ``` -De esta manera simple podemos pasar cualquier variable a las plantillas. Sin embargo, en el desarrollo de aplicaciones robustas, suele ser más útil limitarse. Por ejemplo, definiendo explícitamente la lista de variables que espera la plantilla y sus tipos. Gracias a esto, PHP podrá verificar los tipos, el IDE sugerir correctamente y el análisis estático detectar errores. +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} -¿Y cómo definimos tal lista? Simplemente en forma de una clase y sus propiedades. La nombramos de manera similar al presenter, solo que con `Template` al final: +```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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -El objeto `$this->template` en el presenter será ahora una instancia de la clase `ArticleTemplate`. Así que PHP verificará los tipos declarados al escribir. Y a partir de la versión PHP 8.2 advertirá también sobre la escritura en una variable inexistente, en versiones anteriores se puede lograr lo mismo usando el trait [Nette\SmartObject |utils:smartobject]. +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` está destinada al IDE y al análisis estático, gracias a ella funcionará la sugerencia, ver [PhpStorm y autocompletado de código para $this⁠-⁠>⁠template |https://blog.nette.org/en/phpstorm-and-code-completion-for-this-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 *] -Puede disfrutar del lujo de la sugerencia también en las plantillas, basta con instalar el plugin para Latte en PhpStorm e indicar al principio de la plantilla el nombre de la clase, más en el artículo [Latte: cómo usar el sistema de tipos |https://blog.nette.org/es/latte-how-to-use-type-system]: +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} ... ``` -Así funcionan también las plantillas en los componentes, basta con seguir la convención de nombres y para un componente, p. ej., `FifteenControl` crear una clase de plantilla `FifteenTemplate`. +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 crear `$template` como una instancia de otra clase, utilice el método `createTemplate()`: +Si necesita usar otra clase de parámetros, use el método `createTemplate()`: ```php public function renderDefault(): void @@ -168,58 +203,100 @@ public function renderDefault(): void } ``` +.{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: -Variables predeterminadas -------------------------- - -Los presenters y componentes pasan automáticamente varias variables útiles a las plantillas: - -- `$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 el objeto [que representa al usuario |security:authentication] -- `$presenter` es el presenter actual -- `$control` es el componente o presenter actual -- `$flashes` array de [mensajes |presenters#Mensajes flash] enviados por la función `flashMessage()` - -Si utiliza su propia clase de plantilla, estas variables se pasarán si crea una propiedad para ellas. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My blog'; +} +``` Creación de enlaces ------------------- -En la plantilla, se crean enlaces a otros presenters y acciones de esta manera: +En la plantilla, los enlaces a otros presenters y acciones se crean así: ```latte -detalle del producto +product detail ``` -El atributo `n:href` es muy útil para las etiquetas HTML ``. Si queremos mostrar el enlace en otro lugar, por ejemplo en el texto, usamos `{link}`: +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 -La dirección es: {link Home:default} +URL is: {link Home:default} ``` Encontrará más información en el capítulo [Creación de enlaces URL|creating-links]. -Filtros personalizados, etiquetas, etc. ---------------------------------------- +Filtros, etiquetas y demás propios +---------------------------------- -El sistema de plantillas Latte se puede extender con filtros, funciones, etiquetas, etc. personalizados. Se puede hacer directamente en el método `render` o `beforeRender()`: +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 -public function beforeRender(): void +protected function beforeRender(): void { - // agregar filtro - $this->template->addFilter('foo', /* ... */); + // 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}

    - // o configuramos directamente el objeto Latte\Engine +{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->addFilterLoader(/* ... */); + $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` -Latte en la versión 3 ofrece una forma más avanzada y es crear una [extension |latte:extending-latte#Latte Extension] para cada proyecto web. Un ejemplo fragmentario de tal clase: +**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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -La registramos usando la [configuración |configuration#Plantillas Latte]: +Registre la extensión mediante la [configuración |configuration#Plantillas Latte]: ```neon latte: @@ -263,13 +345,27 @@ latte: - 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á mostrar algunos textos en la plantilla en diferentes idiomas. Nette Framework define para este propósito una interfaz para la traducción [api:Nette\Localization\Translator], que tiene un único método `translate()`. Este recibe el mensaje `$message`, que generalmente suele ser una cadena, y cualquier otro parámetro. La tarea es devolver la cadena traducida. En Nette no hay ninguna implementación predeterminada, puede elegir según sus necesidades entre varias soluciones listas que encontrará en [Componette |https://componette.org/search/localization]. En su documentación aprenderá cómo configurar el traductor. +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. -A las plantillas se les puede establecer un traductor, que [pasamos |dependency-injection:passing-dependencies], con el método `setTranslator()`: +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 @@ -279,7 +375,7 @@ protected function beforeRender(): void } ``` -El traductor alternativamente se puede establecer mediante la [configuración |configuration#Plantillas Latte]: +Como alternativa, el traductor se puede fijar mediante la [configuración |configuration#Plantillas Latte]: ```neon latte: @@ -287,10 +383,10 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Luego, el traductor se puede usar, por ejemplo, como filtro `|translate`, incluyendo parámetros complementarios que se pasan al método `translate()` (ver `foo, bar`): +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 -
    {='Carrito'|translate} +{='Basket'|translate} {$item|translate} {$item|translate, foo, bar} ``` @@ -298,19 +394,19 @@ Luego, el traductor se puede usar, por ejemplo, como filtro `|translate`, incluy O como etiqueta de guion bajo: ```latte -{_'Carrito'} +{_'Basket'} {_$item} {_$item, foo, bar} ``` -Para traducir una sección de la plantilla, existe una etiqueta par `{translate}` (desde Latte 2.11, antes se usaba la etiqueta `{_}`): +Para traducir una sección de la plantilla existe la etiqueta par `{translate}` (desde Latte 2.11; antes se usaba la etiqueta `{_}`): ```latte -{translate}Pedido{/translate} -{translate foo, bar}Pedido{/translate} +{translate}Order{/translate} +{translate foo, bar}Order{/translate} ``` -El traductor se llama estándarmente en tiempo de ejecución al renderizar la plantilla. Sin embargo, Latte versión 3 puede traducir todos los textos estáticos ya durante la compilación de la plantilla. Esto ahorra rendimiento, porque cada cadena se traduce solo una vez y la traducción resultante se escribe en la forma compilada. En el directorio de caché se crean así múltiples versiones compiladas de la plantilla, una para cada idioma. Para ello basta con indicar el idioma como segundo parámetro: +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 @@ -320,4 +416,4 @@ protected function beforeRender(): void } ``` -Por texto estático se entiende, por ejemplo, `{_'hello'}` o `{translate}hello{/translate}`. Los textos no estáticos, como por ejemplo `{_$foo}`, seguirán traduciéndose en tiempo de ejecución. +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 new file mode 100644 index 0000000000..405d660c16 --- /dev/null +++ b/application/es/upgrading.texy @@ -0,0 +1,47 @@ +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/fr/@home.texy b/application/fr/@home.texy index 2548bf3db7..b35a1bddef 100644 --- a/application/fr/@home.texy +++ b/application/fr/@home.texy @@ -74,12 +74,12 @@ Pour commencer Compatibilité avec PHP ---------------------- -| version | compatible avec PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| 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 index 3d59487c72..650fd56ae6 100644 --- a/application/fr/@left-menu.texy +++ b/application/fr/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** +- [Introduction |@home] - [Comment fonctionnent les applications ? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Templates |templates] +- [Bootstrapping|bootstrapping] +- [Presenters|presenters] +- [Templates|templates] - [Structure des répertoires |directory-structure] -- [Routage |routing] +- [Routage|routing] - [Création de liens URL |creating-links] - [Composants interactifs |components] - [AJAX & snippets |ajax] - [Multiplier |multiplier] -- [Configuration |configuration] +- [Configuration|configuration] +- [Mise à niveau|upgrading] -Lectures complémentaires -************************ -- [Pourquoi utiliser Nette ? |www:10-reasons-why-nette] +Pour aller plus loin +******************** +- [Pourquoi utiliser Nette ?|www:10-reasons-why-nette] - [Installation |nette:installation] -- [Écrivons notre première application ! |quickstart:] -- [Tutoriels et bonnes pratiques |best-practices:] +- [Créez votre première application ! |quickstart:] +- [Bonnes pratiques |best-practices:] - [Résolution de problèmes |nette:troubleshooting] diff --git a/application/fr/ajax.texy b/application/fr/ajax.texy index 40956d685b..7abb4466fa 100644 --- a/application/fr/ajax.texy +++ b/application/fr/ajax.texy @@ -59,7 +59,7 @@ Le moyen le plus puissant offert par Nette pour connecter le serveur et le clien 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 sous forme de tableau au format JSON. 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. +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 @@ -121,6 +121,8 @@ Nette permet un contrôle encore plus fin de ce qui doit être redessiné. En ef $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 ------------------- @@ -155,6 +157,8 @@ Les noms des snippets peuvent aussi être des expressions : {/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 @@ -226,6 +230,12 @@ public function actionDelete(int $id): void ``` +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 ========================== @@ -247,3 +257,9 @@ 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 index eb39389dad..d1c2d317b9 100644 --- a/application/fr/bootstrapping.texy +++ b/application/fr/bootstrapping.texy @@ -16,6 +16,9 @@ Le bootstrapping est le processus d'initialisation de l'environnement de l'appli 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 @@ -66,7 +69,7 @@ class Bootstrap 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 : +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; @@ -78,6 +81,9 @@ $application = $container->getByType(Nette\Application\Application::class); $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. @@ -124,6 +130,12 @@ $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 ======================= @@ -144,7 +156,7 @@ Nette utilise un cache pour le conteneur DI, RobotLoader, les templates, etc. Il $this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` -Sous Linux ou macOS, définissez les [permissions d'écriture |nette:troubleshooting#Configuration des permissions de répertoire] pour les répertoires `log/` et `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 @@ -181,7 +193,9 @@ Les fichiers de configuration sont généralement écrits au format [NEON |neon: .[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. -Nous chargeons les fichiers de configuration à l'aide de `addConfig()` : +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'); @@ -200,7 +214,7 @@ if (PHP_SAPI === 'cli') { 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#Inclusion de fichiers]. +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. @@ -249,6 +263,7 @@ Dans les fichiers de configuration, vous pouvez utiliser ces paramètres statiqu - `%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 @@ -277,7 +292,7 @@ $this->configurator->addServices([ 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. +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 diff --git a/application/fr/components.texy b/application/fr/components.texy index 4392cdea5b..561d497835 100644 --- a/application/fr/components.texy +++ b/application/fr/components.texy @@ -59,6 +59,11 @@ Dans le template, il est possible de rendre un composant à l'aide de la balise {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 =============== @@ -212,9 +217,9 @@ Les signaux vous rappellent peut-être un peu AJAX : des gestionnaires qui sont 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. +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 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. +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é.'); @@ -289,25 +294,25 @@ Ou il peut être *réinitialisé*, c'est-à-dire supprimé de l'URL. Il prendra 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 avec une annotation dans la classe du presenter. Par exemple, nous marquons ainsi les composants `calendar` et `poll` : +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 -/** - * @persistent(calendar, poll) - */ +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 deviendront également persistants. +Il n'est pas nécessaire de marquer les sous-composants à l'intérieur de ces composants ; ils deviennent persistants eux aussi. -En PHP 8, vous pouvez également utiliser des attributs pour marquer les composants persistants : +L'ancienne annotation `@persistent` fonctionne encore, mais elle est dépréciée et déclenche un avertissement : ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -338,7 +343,7 @@ class PollControl extends Control } ``` -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… +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. @@ -431,7 +436,7 @@ 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. +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 : @@ -455,6 +460,18 @@ class PaginatingControl extends Control 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 --------------------- @@ -462,17 +479,19 @@ Un signal provoque un rechargement de la page exactement comme lors de la requê 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. +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. +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. +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. +À 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 : diff --git a/application/fr/configuration.texy b/application/fr/configuration.texy index aedc9c6739..ec195628c6 100644 --- a/application/fr/configuration.texy +++ b/application/fr/configuration.texy @@ -40,6 +40,8 @@ application: 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. @@ -73,7 +75,7 @@ application: - %vendorDir%/mymodule ``` -L'analyse des répertoires peut être désactivée en spécifiant la valeur `false`. Nous ne recommandons pas de supprimer complètement l'ajout automatique des presenters, car cela réduirait les performances de l'application. +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 @@ -83,16 +85,22 @@ Ce paramètre permet d'influencer globalement le comportement de Latte dans les ```neon latte: - # afficher le panneau Latte dans la barre Tracy pour le template principal (true) ou tous les composants (all) ? - debugger: ... # (true|false|'all') par défaut true + # 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] + # active le mode [parseur strict |latte:develop#strict mode] strictParsing: ... # (bool) par défaut false - # active le [contrôle du code généré |latte:develop#checking-generated-code] + # 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 @@ -102,7 +110,7 @@ latte: templateClass: App\MyTemplateClass # par défaut Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Si vous utilisez Latte version 3, vous pouvez ajouter de nouvelles [extensions |latte:extending-latte#Latte Extension] en utilisant : +Vous pouvez ajouter de nouvelles [extensions |latte:extending-latte#Latte Extension] avec : ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Si vous utilisez Latte version 2, vous pouvez enregistrer de nouvelles balises soit en spécifiant le nom de la classe, soit par référence à un service. Par défaut, la méthode `install()` est appelée, mais cela peut être modifié en spécifiant le nom d'une autre méthode : - -```neon -latte: - # enregistrement des balises Latte personnalisées - macros: - - App\MyLatteMacros::register # méthode statique, nom de classe ou callable - - @App\MyLatteMacrosFactory # service avec la méthode install() - - @App\MyLatteMacrosFactory::register # service avec la méthode register() - -services: - - App\MyLatteMacrosFactory -``` - Routage ======= @@ -181,11 +175,12 @@ Services DI Ces services sont ajoutés au conteneur DI : -| Nom | Type | Description -|---------------------------------------------------------------------------------| +| 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\PresenterFactory] | factory de presenters +| `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 index 1ceacf2bd0..1df0b3cecb 100644 --- a/application/fr/creating-links.texy +++ b/application/fr/creating-links.texy @@ -40,7 +40,7 @@ Il est également possible de passer des paramètres nommés. Le lien suivant pa 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 `...` (dans Latte 2.x, l'opérateur `(expand)`) : +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]} @@ -73,6 +73,14 @@ $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 ======================= @@ -122,6 +130,13 @@ Nous pouvons lier vers une partie spécifique de la page via un fragment après 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 =============== @@ -130,6 +145,8 @@ Les liens générés à l'aide de `link()` ou `n:href` sont toujours des chemins 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 ========================== @@ -179,6 +196,22 @@ Pour vérifier si nous sommes dans un certain module ou son sous-module, utilise ``` +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 ==================== diff --git a/application/fr/directory-structure.texy b/application/fr/directory-structure.texy index 48cf3ac4bd..1c9f1a4f77 100644 --- a/application/fr/directory-structure.texy +++ b/application/fr/directory-structure.texy @@ -95,7 +95,7 @@ Ce répertoire est le seul accessible depuis le web (appelé document-root). Vou - 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]. +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. @@ -202,8 +202,8 @@ Avec le temps, d'autres types de flux sont ajoutés et nous avons besoin de plus │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← flux pour Zboží.cz - └── heureka.latte ← flux pour Heureka.cz + ├── 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é. @@ -420,7 +420,7 @@ Exemple : 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 |https://blog.nette.org/en/cli-scripts-in-nette-application] ou via cron. 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. +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 @@ -470,7 +470,7 @@ 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`). Nous devons communiquer cette convention à Nette dans le fichier de configuration. Une seule ligne suffit : +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: @@ -486,7 +486,7 @@ application: 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 en doublant les deux-points (nécessite Nette Application 3.2) : +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: diff --git a/application/fr/how-it-works.texy b/application/fr/how-it-works.texy index 160ad93fba..78d5411595 100644 --- a/application/fr/how-it-works.texy +++ b/application/fr/how-it-works.texy @@ -3,7 +3,7 @@ Comment fonctionnent les applications ?
    -Vous lisez actuellement le guide fondamental de la documentation Nette. Vous apprendrez tout le principe de fonctionnement des applications web. De A à Z, du début à la fin de l'exécution du script PHP. Après lecture, vous saurez : +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 @@ -51,7 +51,7 @@ Vous pouvez modifier la structure des répertoires comme vous le souhaitez, reno 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. +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] : @@ -59,7 +59,7 @@ Vous pouvez également télécharger directement WebProject incluant Nette en ut composer create-project nette/web-project ``` -Sous Linux ou macOS, définissez les [permissions d'écriture |nette:troubleshooting#Configuration des permissions de répertoire] pour les répertoires `log/` et `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/`. 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/`. @@ -67,7 +67,7 @@ L'application WebProject est prête à être lancée, il n'y a absolument rien 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 le serveur pour les jolies URL], cette URL est également associée au fichier `index.php`, qui est ensuite exécuté. +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 : @@ -95,7 +95,7 @@ Les applications écrites en Nette sont divisées en de nombreux presenters (dan 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éjà le nom du presenter et peut continuer. En créant l'objet de la classe `ProductPresenter`, qui est le code du presenter `Product`. Plus précisément, il demande au conteneur DI de créer le presenter, car c'est son rôle. +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 : @@ -145,7 +145,7 @@ Vous avez peut-être rencontré de nombreux nouveaux termes maintenant, mais nou Templates ========= -Puisque nous avons parlé des templates, 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. Vous apprendrez tout dans [la documentation |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 : @@ -159,7 +159,7 @@ Au lieu d'une URL réelle, écrivez simplement la paire connue `Presenter:action 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 peuvent effectuer non seulement des transformations d'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. +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 diff --git a/application/fr/multiplier.texy b/application/fr/multiplier.texy index c8c57bdd44..0948ec3122 100644 --- a/application/fr/multiplier.texy +++ b/application/fr/multiplier.texy @@ -9,7 +9,7 @@ Partons d'un exemple typique : nous avons une liste de produits dans une boutiqu 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 ou la [présentation de Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. +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 : diff --git a/application/fr/presenters.texy b/application/fr/presenters.texy index 670c01a8b7..a4c22cce50 100644 --- a/application/fr/presenters.texy +++ b/application/fr/presenters.texy @@ -58,9 +58,12 @@ Immédiatement après réception de la requête, la méthode `startup()` est app 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()`, nous pouvons donc éventuellement y modifier le déroulement ultérieur, c'est-à-dire changer le template qui sera rendu, ainsi que la méthode `render()` qui sera appelée. Ceci est fait en utilisant `setView('autreVue')`. +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')`. -Les paramètres de la requête sont passés à la méthode. Il est possible et recommandé de spécifier les types des paramètres, par ex. `actionShow(int $id, ?string $slug = null)` - si le paramètre `id` est manquant ou s'il n'est pas un entier, le presenter retournera une [erreur 404 |#Erreur 404 et autres] et terminera son activité. +.{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()} @@ -105,7 +108,27 @@ La méthode `afterRender`, comme son nom l'indique encore une fois, est appelée Appelée à la fin du cycle de vie du presenter. -**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()`. Mais nous recommandons de concevoir des presenters avec une seule ou le moins d'actions possible. +É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 @@ -116,13 +139,15 @@ La réponse d'un presenter est généralement le [rendu d'un template avec une p À 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] +- `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 -Si vous n'appelez aucune de ces méthodes, le presenter procédera automatiquement au rendu du template. 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. +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 @@ -178,13 +203,13 @@ $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. +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. +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. @@ -224,7 +249,7 @@ Le code d'erreur HTTP peut être passé comme deuxième paramètre, la valeur pa Envoi de JSON ============= -Exemple de méthode d'action qui envoie des données au format JSON et termine le presenter : +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 @@ -315,6 +340,26 @@ Ou il peut être *réinitialisé*, c'est-à-dire supprimé de l'URL. Il prendra ``` +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 ====================== @@ -335,7 +380,7 @@ Ce que nous avons montré jusqu'à présent dans ce chapitre vous suffira probab 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. +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 : @@ -391,25 +436,7 @@ public function actionShow(int $id, ?string $slug = null): void } ``` - -É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 qui doivent être appelées automatiquement. Le presenter définit ce qu'on appelle des [événements |nette:glossary#Événements events], 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 dans le tableau `$onStartup` sont appelés juste avant la méthode `startup()`, ensuite `$onRender` entre `beforeRender()` et `render()` et enfin `$onShutdown` juste avant `shutdown()`. +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 @@ -445,6 +472,63 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht $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} ------------------------------------------------------------ @@ -458,6 +542,9 @@ Vous pouvez spécifier ces restrictions : - 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]. @@ -475,7 +562,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Dans la version 3.1, la vérification est effectuée dans `checkHttpMethod()`, qui vérifie si la méthode spécifiée dans la requête est contenue dans le tableau `$presenter->allowedMethods`. L'ajout de la méthode se fait comme ceci : +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 @@ -491,6 +578,14 @@ class MyPresenter extends Nette\Application\UI\Presenter 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 ======================== diff --git a/application/fr/routing.texy b/application/fr/routing.texy index 41a575cac4..64ec3dafce 100644 --- a/application/fr/routing.texy +++ b/application/fr/routing.texy @@ -12,7 +12,7 @@ Le routeur s'occupe de tout ce qui concerne les adresses URL, afin que vous n'ay
    -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. +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. @@ -244,6 +244,16 @@ $router->addRoute('/[/]', [ 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 ---------------------- @@ -306,7 +316,7 @@ Les paramètres `presenter`, `action` et `module` ont déjà des filtres prédé 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é `null`. +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; @@ -314,7 +324,7 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], @@ -325,6 +335,8 @@ Les filtres généraux donnent la possibilité de modifier le comportement de la 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 -------------------------------- @@ -344,7 +356,7 @@ Lors de l'accès à l'ancienne URL, le presenter redirige automatiquement vers l 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 : +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 () { @@ -363,11 +375,19 @@ $router->addRoute('', function (string $lang) { }); ``` +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()` : +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; @@ -379,7 +399,7 @@ $router->withModule('Forum') // les routes suivantes font partie du module Forum ->addRoute('sign:in', 'Sign:in'); ``` -Une alternative est d'utiliser le paramètre `module` : +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 @@ -388,6 +408,12 @@ $router->addRoute('manage//', [ ]); ``` +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 ------------- @@ -522,7 +548,7 @@ 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 vers lequel il faut diriger si nous ouvrons la page sans paramètres, par ex. `http://example.com/`. +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' @@ -552,7 +578,7 @@ 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 : +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 : ``` @@ -608,7 +634,7 @@ routing: 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] se dvěma metodami: +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; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -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 retourner au minimum le presenter et l'action. Le nom du presenter est complet et contient également d'éventuels modules : +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 [ @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/fr/templates.texy b/application/fr/templates.texy index 7216f421d8..2b1931c088 100644 --- a/application/fr/templates.texy +++ b/application/fr/templates.texy @@ -63,7 +63,7 @@ app/ 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]. +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')`. @@ -114,15 +114,48 @@ Les fichiers où les templates de layout sont recherchés peuvent être modifié Variables dans le template -------------------------- -Nous passons des variables au template en les écrivant dans `$this->template` et elles sont ensuite disponibles dans le template comme variables locales : +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); ``` -Nous pouvons ainsi passer facilement n'importe quelle variable aux templates. Cependant, lors du développement d'applications robustes, il est plus utile de se limiter. Par exemple, en définissant explicitement la liste des variables que le template attend et leurs types. Grâce à cela, PHP pourra vérifier les types, l'IDE pourra suggérer correctement et l'analyse statique pourra détecter les erreurs. +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} -Et comment définir une telle liste ? Simplement sous la forme d'une classe et de ses propriétés. Nous la nommerons de manière similaire au presenter, mais avec `Template` à la fin : +```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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -L'objet `$this->template` dans le presenter sera désormais une instance de la classe `ArticleTemplate`. Ainsi, PHP vérifiera les types déclarés lors de l'écriture. Et à partir de PHP 8.2, il avertira également en cas d'écriture dans une variable inexistante ; dans les versions précédentes, le même résultat peut être obtenu en utilisant le trait [Nette\SmartObject |utils:smartobject]. +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, grâce à elle, l'autocomplétion fonctionnera, voir [PhpStorm et l'autocomplétion pour $this⁠-⁠>⁠template|https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template]. +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 également profiter du luxe de l'autocomplétion dans les templates, il suffit d'installer le plugin pour Latte dans PhpStorm et d'indiquer le nom de la classe au début du template, plus d'informations dans l'article [Latte : comment gérer le système de types|https://blog.nette.org/fr/latte-how-to-use-type-system] : +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} ... ``` -C'est ainsi que fonctionnent également les templates dans les composants, il suffit de respecter la convention de nommage et pour un composant par ex. `FifteenControl` créer une classe de template `FifteenTemplate`. +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 de créer `$template` comme instance d'une autre classe, utilisez la méthode `createTemplate()` : +Si vous avez besoin d'une autre classe de paramètres, utilisez la méthode `createTemplate()` : ```php public function renderDefault(): void @@ -168,20 +203,16 @@ public function renderDefault(): void } ``` +.{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 : -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. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My blog'; +} +``` Création de liens @@ -205,21 +236,67 @@ Plus d'informations peuvent être trouvées dans le chapitre [Création de liens Filtres personnalisés, balises, etc. ------------------------------------ -Le système de templates Latte peut être étendu avec des filtres, fonctions, balises personnalisés, etc. Cela peut être fait directement dans la méthode `render` ou `beforeRender()` : +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 -public function beforeRender(): void +protected function beforeRender(): void { // ajout d'un filtre - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); - // ou nous configurons directement l'objet Latte\Engine + // 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->addFilterLoader(/* ... */); + $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 version 3 offre une méthode plus avancée, à savoir la création d'une [extension |latte:extending-latte#Latte Extension] pour chaque projet web. Exemple succinct d'une telle classe : +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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -Nous l'enregistrons à l'aide de la [configuration |configuration#Templates Latte] : +Enregistrez l'extension via la [configuration |configuration#Templates Latte] : ```neon latte: @@ -263,6 +345,20 @@ latte: - 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 ---------- diff --git a/application/fr/upgrading.texy b/application/fr/upgrading.texy new file mode 100644 index 0000000000..0df1a3b5ea --- /dev/null +++ b/application/fr/upgrading.texy @@ -0,0 +1,47 @@ +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/hu/@home.texy b/application/hu/@home.texy deleted file mode 100644 index b9bc4c1a11..0000000000 --- a/application/hu/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -A Nette Application a Nette keretrendszer magja, amely hatékony eszközöket kínál modern webalkalmazások létrehozásához. Számos kivételes tulajdonságot kínál, amelyek jelentősen megkönnyítik a fejlesztést, és javítják a kód biztonságát és karbantarthatóságát. - - -Telepítés ---------- - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/application -``` - - -Miért válassza a Nette Applicationt? ------------------------------------- - -A Nette mindig is úttörő volt a webes technológiák területén. - -**Kétirányú router:** A Nette fejlett router rendszerrel rendelkezik, amely kétirányúsága miatt egyedülálló - nemcsak az URL-eket fordítja le az alkalmazás akcióira, hanem visszafelé is képes URL-címeket generálni. Ez azt jelenti, hogy: -- Bármikor megváltoztathatja az egész alkalmazás URL-struktúráját anélkül, hogy a sablonokat módosítania kellene -- Az URL-ek automatikusan kanonizálódnak, ami javítja a SEO-t -- Az útválasztás egy helyen van definiálva, nem pedig szétszórva az annotációkban - -**Komponensek és szignálok:** A Delphi és a React.js által inspirált beépített komponensrendszer teljesen egyedülálló a PHP keretrendszerek között: -- Lehetővé teszi újrafelhasználható UI elemek létrehozását -- Támogatja a komponensek hierarchikus összeállítását -- Elegáns AJAX kérések kezelését kínálja szignálok segítségével -- Kész komponensek gazdag könyvtára a [Componette](https://componette.org) oldalon - -**AJAX és snippettek:** A Nette már 2009-ben forradalmi módszert vezetett be az AJAX-szal való munkára, jóval megelőzve az olyan hasonló megoldásokat, mint a Hotwire a Ruby on Railshez vagy a Symfony UX Turbo: -- A snippettek lehetővé teszik az oldal csak egyes részeinek frissítését JavaScript írása nélkül -- Automatikus integráció a komponensrendszerrel -- Oldalrészek intelligens érvénytelenítése -- Minimális mennyiségű továbbított adat - -**Intuitív [Latte|latte:] sablonok:** A legbiztonságosabb sablonrendszer PHP-hoz fejlett funkciókkal: -- Automatikus védelem XSS ellen kontextusérzékeny escapeléssel -- Bővíthetőség saját szűrőkkel, függvényekkel és tagekkel -- Sablon öröklődés és snippettek AJAX-hoz -- Kiváló PHP 8.x támogatás típusrendszerrel - -**Dependency Injection:** A Nette teljes mértékben kihasználja a Dependency Injectiont: -- Függőségek automatikus átadása (autowiring) -- Konfiguráció áttekinthető NEON formátumban -- Komponens factory-k támogatása - - -Fő előnyök ----------- - -- **Biztonság**: Automatikus védelem a [sebezhetőségekkel|nette:vulnerability-protection] szemben, mint az XSS, CSRF stb. -- **Termelékenység**: Kevesebb írás, több funkció az intelligens tervezésnek köszönhetően -- **Debuggolás**: [Tracy debugger|tracy:] útválasztó panellel -- **Teljesítmény**: Intelligens cache, komponensek lusta betöltése (lazy loading) -- **Rugalmasság**: Az URL-ek egyszerű módosítása az alkalmazás befejezése után is -- **Komponensek**: Egyedülálló újrafelhasználható UI elemek rendszere -- **Modern**: Teljes PHP 8.4+ és típusrendszer támogatás - - -Első lépések ------------- - -1. [Hogyan működnek az alkalmazások? |how-it-works] - Az alapvető architektúra megértése -2. [Presenterek |presenters] - Munka presenterekkel és akciókkal -3. [Sablonok |templates] - Sablonok készítése Latte-ban -4. [Route-ok |routing] - URL címek konfigurálása -5. [Interaktív komponensek |components] - A komponensrendszer kihasználása - - -PHP kompatibilitás ------------------- - -| verzió | kompatibilis PHP-vel -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 - -Az utolsó patch verzióra érvényes. diff --git a/application/hu/@left-menu.texy b/application/hu/@left-menu.texy deleted file mode 100644 index 1c42a160dd..0000000000 --- a/application/hu/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Hogyan működnek az alkalmazások? |how-it-works] -- [Bootstrapping] -- [Presenterek |presenters] -- [Sablonok |templates] -- [Könyvtárstruktúra |directory-structure] -- [Route-ok |routing] -- [URL linkek létrehozása |creating-links] -- [Interaktív komponensek |components] -- [AJAX & snippettek |ajax] -- [Multiplier |multiplier] -- [Konfiguráció |configuration] - - -További olvasmányok -******************* -- [Miért használjuk a Nette-t? |www:10-reasons-why-nette] -- [Telepítés |nette:installation] -- [Írjuk meg az első alkalmazásunkat! |quickstart:] -- [Útmutatók és eljárások |best-practices:] -- [Problémamegoldás |nette:troubleshooting] diff --git a/application/hu/@meta.texy b/application/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/application/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/application/hu/ajax.texy b/application/hu/ajax.texy deleted file mode 100644 index 27f9403885..0000000000 --- a/application/hu/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & Snippetek -**************** - -
    - -A modern webalkalmazások korában, ahol a funkcionalitás gyakran megoszlik a szerver és a böngésző között, az AJAX elengedhetetlen összekötő elem. Milyen lehetőségeket kínál nekünk a Nette Framework ezen a területen? -- sablonrészek, úgynevezett snippetek küldése -- változók átadása PHP és JavaScript között -- eszközök AJAX kérések debuggolásához - -
    - - -AJAX kérés -========== - -Az AJAX kérés alapvetően nem különbözik a klasszikus HTTP kéréstől. Meghív egy presentert bizonyos paraméterekkel. És a presenteren múlik, hogyan reagál a kérésre - visszaadhat adatokat JSON formátumban, küldhet HTML kód egy részét, XML dokumentumot stb. - -A böngésző oldalán az AJAX kérést a `fetch()` függvénnyel inicializáljuk: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // válasz feldolgozása -}); -``` - -Szerveroldalon az AJAX kérést a [HTTP kérést becsomagoló |http:request] szolgáltatás `$httpRequest->isAjax()` metódusával ismerjük fel. Az észleléshez a `X-Requested-With` HTTP fejlécet használja, ezért fontos elküldeni. A presenterben a `$this->isAjax()` metódus használható. - -Ha adatokat szeretne küldeni JSON formátumban, használja a [`sendJson()` |presenters#Válasz küldése] metódust. A metódus szintén befejezi a presenter működését. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Ha egy speciális, AJAX-hoz szánt sablonnal tervez válaszolni, a következőképpen teheti meg: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippetek -========= - -A Nette által kínált legerősebb eszköz a szerver és a kliens összekapcsolására a snippetek. Ezeknek köszönhetően egy átlagos alkalmazást minimális erőfeszítéssel és néhány sor kóddal AJAX-alapúvá alakíthat. Hogy mindez hogyan működik, azt a Fifteen példa demonstrálja, amelynek kódját a [GitHubon |https://github.com/nette-examples/fifteen] találja meg. - -A snippetek, vagyis kódrészletek, lehetővé teszik az oldal csak bizonyos részeinek frissítését, ahelyett, hogy az egész oldalt újra kellene tölteni. Ez nemcsak gyorsabb és hatékonyabb, hanem kényelmesebb felhasználói élményt is nyújt. A snippetek emlékeztethetnek a Hotwire for Ruby on Rails vagy a Symfony UX Turbo megoldásokra. Érdekesség, hogy a Nette már 14 évvel korábban bemutatta a snippeteket. - -Hogyan működnek a snippetek? Az oldal első betöltésekor (nem AJAX kérés esetén) az egész oldal betöltődik, beleértve az összes snippetet is. Amikor a felhasználó interakcióba lép az oldallal (pl. gombra kattint, űrlapot küld stb.), az egész oldal betöltése helyett egy AJAX kérés indul. A presenterben lévő kód végrehajtja a műveletet, és eldönti, mely snippeteket kell frissíteni. A Nette ezeket a snippeteket rendereli és JSON formátumú tömbként küldi el. A böngészőben lévő kezelő kód a kapott snippeteket visszailleszti az oldalba. Így csak a megváltozott snippetek kódja kerül átvitelre, ami sávszélességet takarít meg és gyorsítja a betöltést az egész oldal tartalmának átvitelével szemben. - - -Naja ----- - -A snippetek böngészőoldali kezelésére a [Naja könyvtár |https://naja.js.org] szolgál. Ezt [telepítse |https://naja.js.org/#/guide/01-install-setup-naja] node.js csomagként (Webpack, Rollup, Vite, Parcel és más alkalmazásokkal való használathoz): - -```shell -npm install naja -``` - -…vagy közvetlenül illessze be az oldal sablonjába: - -```latte - -``` - -Először is [inicializálni |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] kell a könyvtárat: - -```js -naja.initialize(); -``` - -Ahhoz, hogy egy egyszerű linkből (signal) vagy űrlapküldésből AJAX kérés legyen, elegendő a megfelelő linket, űrlapot vagy gombot `ajax` osztállyal megjelölni: - -```latte -Go - - - - - -vagy - -
    - -
    -``` - - -Snippetek újrarajzolása ------------------------ - -Minden [Control |components] osztályú objektum (beleértve magát a Presentert is) nyilvántartja, hogy történt-e olyan változás, amely az újrarajzolását igényli. Erre szolgál a `redrawControl()` metódus: - -```php -public function handleLogin(string $user): void -{ - // bejelentkezés után újra kell rajzolni a releváns részt - $this->redrawControl(); - // ... -} -``` - -A Nette még finomabb vezérlést tesz lehetővé afölött, hogy mit kell újrarajzolni. Az említett metódus ugyanis argumentumként fogadhatja a snippet nevét. Így lehet invalidálni (értsd: újrarajzolást kényszeríteni) a sablon részei szintjén. Ha az egész komponenst invalidáljuk, akkor annak minden snippetje is újrarajzolódik: - -```php -// invalidálja a 'header' snippetet -$this->redrawControl('header'); -``` - - -Snippetek a Latte-ban ---------------------- - -A snippetek használata a Latte-ban rendkívül egyszerű. Ha egy sablonrészt snippetként szeretne definiálni, egyszerűen csomagolja be `{snippet}` és `{/snippet}` tag-ekkel: - -```latte -{snippet header} -

    Hello ...

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

    Hello ...

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

    Szavazzon

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

    {$item->title}

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

    Hello

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

    Kezdőlap

    -... -{/block} -``` - -Ez definiálja a `content` blokkot, amely a `{include content}` helyére kerül a layoutban, és újra definiálja a `title` blokkot, amely felülírja a `{block title}`-t a layoutban. Próbálja meg elképzelni az eredményt. - - -Sablonok keresése ------------------ - -Nem kell a presenterekben megadnia, hogy melyik sablont kell renderelni, a keretrendszer maga vezeti le az utat, és megspórolja Önnek az írást. - -Ha olyan könyvtárstruktúrát használ, ahol minden presenternek saját könyvtára van, egyszerűen helyezze el a sablont ebben a könyvtárban az akció (ill. view) nevével, azaz a `default` akcióhoz használja a `default.latte` sablont: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Ha olyan struktúrát használ, ahol a presenterek egy könyvtárban vannak, a sablonok pedig a `templates` mappában, mentse el vagy a `..latte` vagy a `/.latte` fájlba: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1. változat - └── Home/ - └── default.latte ← 2. változat -\-- - -A `templates` könyvtár egy szinttel feljebb is elhelyezkedhet, azaz ugyanazon a szinten, mint a presenter osztályokat tartalmazó könyvtár. - -Ha a sablon nem található, a presenter [404 - page not found hibával |presenters#Hiba 404 és társai] válaszol. - -A view-t a `$this->setView('jineView')` segítségével változtathatja meg. Közvetlenül is megadhatja a sablonfájlt a `$this->template->setFile('/path/to/template.latte')` segítségével. - -.[note] -A fájlokat, ahol a sablonokat keresi, meg lehet változtatni a [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] metódus felülírásával, amely visszaadja a lehetséges fájlnevek tömbjét. - - -Layout sablon keresése ----------------------- - -A Nette automatikusan megkeresi a layout fájlt is. - -Ha olyan könyvtárstruktúrát használ, ahol minden presenternek saját könyvtára van, helyezze el a layoutot vagy a presenter mappájában, ha csak rá specifikus, vagy egy szinttel feljebb, ha több presenter számára közös: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← közös layout - └── Home/ - ├── @layout.latte ← csak a Home presenterhez - ├── HomePresenter.php - └── default.latte -\-- - -Ha olyan struktúrát használ, ahol a presenterek egy könyvtárban vannak, a sablonok pedig a `templates` mappában, a layoutot ezeken a helyeken várja: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← közös layout - ├── Home.@layout.latte ← csak a Home-hoz, 1. változat - └── Home/ - └── @layout.latte ← csak a Home-hoz, 2. változat -\-- - -Ha a presenter egy modulban található, akkor további könyvtárszinteken is keresni fog, a modul beágyazási mélységétől függően. - -A layout nevét a `$this->setLayout('layoutAdmin')` segítségével lehet megváltoztatni, és akkor a `@layoutAdmin.latte` fájlban várja. Közvetlenül is megadhatja a layout sablonfájlt a `$this->setLayout('/path/to/template.latte')` segítségével. - -A `$this->setLayout(false)` vagy a `{layout none}` tag használatával a sablonon belül kikapcsolható a layout keresése. - -.[note] -A fájlokat, ahol a layout sablonokat keresi, meg lehet változtatni a [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] metódus felülírásával, amely visszaadja a lehetséges fájlnevek tömbjét. - - -Változók a sablonban --------------------- - -Változókat úgy adunk át a sablonnak, hogy beírjuk őket a `$this->template`-be, és utána lokális változókként érhetők el a sablonban: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Így egyszerűen bármilyen változót átadhatunk a sablonoknak. Robusztus alkalmazások fejlesztésekor azonban hasznosabb korlátozni magunkat. Például úgy, hogy explicit módon definiáljuk a sablon által várt változók listáját és azok típusait. Ennek köszönhetően a PHP ellenőrizni tudja a típusokat, az IDE helyesen tud súgni, és a statikus analízis felfedezheti a hibákat. - -És hogyan definiálunk egy ilyen listát? Egyszerűen egy osztály és annak property-jei formájában. Nevezzük el hasonlóan a presenterhez, csak `Template` végződéssel: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // és további változók -} -``` - -A `$this->template` objektum a presenterben mostantól az `ArticleTemplate` osztály példánya lesz. Így a PHP ellenőrizni fogja a deklarált típusokat íráskor. És a PHP 8.2-es verziójától kezdve figyelmeztet a nem létező változóba való írásra is, korábbi verziókban ugyanezt a [Nette\SmartObject |utils:smartobject] trait használatával lehet elérni. - -Az `@property-read` annotáció az IDE-nek és a statikus analízisnek szól, ennek köszönhetően működni fog a súgó, lásd "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -A súgó luxusát a sablonokban is élvezheti, csak telepíteni kell a PhpStorm-ba a Latte plugint, és a sablon elejére beírni az osztály nevét, további információk a "Latte: hogyan a típusrendszerre":https://blog.nette.org/hu/latte-how-to-use-type-system cikkben: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Így működnek a sablonok a komponensekben is, csak be kell tartani a névkonvenciót, és például a `FifteenControl` komponenshez létrehozni egy `FifteenTemplate` sablonosztályt. - -Ha a `$template`-et egy másik osztály példányaként kell létrehoznia, használja a `createTemplate()` metódust: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Alapértelmezett változók ------------------------- - -A presenterek és komponensek automatikusan átadnak néhány hasznos változót a sablonoknak: - -- `$basePath` az abszolút URL elérési út a gyökérkönyvtárhoz (pl. `/eshop`) -- `$baseUrl` az abszolút URL a gyökérkönyvtárhoz (pl. `http://localhost/eshop`) -- `$user` a [felhasználót reprezentáló |security:authentication] objektum -- `$presenter` az aktuális presenter -- `$control` az aktuális komponens vagy presenter -- `$flashes` a `flashMessage()` függvénnyel küldött [üzenetek |presenters#Flash üzenetek] tömbje - -Ha saját sablonosztályt használ, ezek a változók átadódnak, ha létrehoz hozzájuk property-t. - - -Linkek létrehozása ------------------- - -A sablonban a linkek más presenterekhez & akciókhoz a következőképpen hozhatók létre: - -```latte -termék részletei -``` - -Az `n:href` attribútum nagyon praktikus a HTML `` tag-ekhez. Ha máshol szeretnénk kiírni a linket, például szövegben, használjuk a `{link}`-et: - -```latte -A cím: {link Home:default} -``` - -További információkat az [URL linkek létrehozása|creating-links] fejezetben talál. - - -Saját szűrők, tag-ek stb. -------------------------- - -A Latte sablonrendszert ki lehet bővíteni saját szűrőkkel, függvényekkel, tag-ekkel stb. Ezt meg lehet tenni közvetlenül a `render` vagy `beforeRender()` metódusban: - -```php -public function beforeRender(): void -{ - // szűrő hozzáadása - $this->template->addFilter('foo', /* ... */); - - // vagy közvetlenül konfiguráljuk a Latte\Engine objektumot - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -A Latte 3-as verziója fejlettebb módszert kínál, mégpedig egy [extension |latte:extending-latte#Latte Extension] létrehozását minden webprojekthez. Egy ilyen osztály töredékes példája: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Regisztráljuk a [konfiguráció |configuration#Latte sablonok] segítségével: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Fordítás --------- - -Ha többnyelvű alkalmazást programoz, valószínűleg szüksége lesz néhány szöveg lefordítására a sablonban különböző nyelvekre. A Nette Framework erre a célra definiál egy interfészt a fordításhoz [api:Nette\Localization\Translator], amelynek egyetlen metódusa van, a `translate()`. Ez fogadja az üzenetet `$message`, ami általában egy string, és tetszőleges további paramétereket. Feladata a lefordított string visszaadása. A Nette-ben nincs alapértelmezett implementáció, választhat igényei szerint több kész megoldás közül, amelyeket a [Componette |https://componette.org/search/localization] oldalon talál. Dokumentációjukban megtudhatja, hogyan konfigurálja a translatort. - -A sablonoknak beállítható egy fordító, amelyet [átkérünk |dependency-injection:passing-dependencies], a `setTranslator()` metódussal: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -A translatort alternatívaként be lehet állítani a [konfiguráció |configuration#Latte sablonok] segítségével is: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Ezután a fordítót például használhatjuk `|translate` szűrőként, beleértve a kiegészítő paramétereket is, amelyek átadódnak a `translate()` metódusnak (lásd `foo, bar`): - -```latte -{='Kosár'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Vagy aláhúzásos tagként: - -```latte -{_'Kosár'} -{_$item} -{_$item, foo, bar} -``` - -A sablon egy szakaszának fordításához létezik egy páros `{translate}` tag (Latte 2.11-től, korábban a `{_}` tag volt használatos): - -```latte -{translate}Rendelés{/translate} -{translate foo, bar}Rendelés{/translate} -``` - -A translator alapértelmezés szerint futásidőben hívódik meg a sablon renderelésekor. A Latte 3-as verziója azonban képes az összes statikus szöveget már a sablon fordítása során lefordítani. Ezzel teljesítményt takarítunk meg, mert minden string csak egyszer fordítódik le, és az eredményül kapott fordítás beíródik a lefordított formába. A cache könyvtárban így több lefordított sablonverzió jön létre, minden nyelvre egy. Ehhez elegendő csak a nyelvet második paraméterként megadni: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Statikus szöveg alatt például a `{_'hello'}` vagy `{translate}hello{/translate}` értendő. A nem statikus szövegek, mint például a `{_$foo}`, továbbra is futásidőben fordítódnak. diff --git a/application/it/@home.texy b/application/it/@home.texy index ff9cedc27c..3a4b81e569 100644 --- a/application/it/@home.texy +++ b/application/it/@home.texy @@ -2,13 +2,13 @@ Nette Application ***************** .[perex] -Nette Application è il cuore del framework Nette e fornisce potenti strumenti per la creazione di moderne applicazioni web. Offre una serie di funzionalità eccezionali che semplificano notevolmente lo sviluppo e migliorano la sicurezza e la manutenibilità del codice. +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 ------------- -È possibile scaricare e installare la libreria utilizzando lo strumento [Composer|best-practices:composer]: +Scaricate e installate la libreria con [Composer|best-practices:composer]: ```shell composer require nette/application @@ -18,68 +18,68 @@ composer require nette/application Perché scegliere Nette Application? ----------------------------------- -Nette è sempre stato un pioniere nel campo delle tecnologie web. +Nette è sempre stata pioniera nelle tecnologie web. -**Router bidirezionale:** Nette dispone di un sistema di routing avanzato, unico per la sua bidirezionalità: non solo traduce gli URL in azioni dell'applicazione, ma può anche generare indirizzi URL a ritroso. Ciò significa che: -- È possibile modificare in qualsiasi momento la struttura degli URL dell'intera applicazione senza dover modificare i template. -- Gli URL vengono automaticamente canonizzati, migliorando la SEO. -- Il routing è definito in un unico punto, non sparso nelle annotazioni. +**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 React.js, è del tutto eccezionale tra i framework PHP: -- Permette di creare elementi UI riutilizzabili. -- Supporta la composizione gerarchica dei componenti. -- Offre un'elegante gestione delle request AJAX tramite segnali. -- Una ricca libreria di componenti pronti è disponibile su [Componette](https://componette.org). +**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:** Nette ha introdotto un modo rivoluzionario di lavorare con AJAX già nel 2009, molto prima di soluzioni simili come Hotwire per Ruby on Rails o Symfony UX Turbo: -- Gli snippet consentono di aggiornare solo parti della pagina senza dover scrivere JavaScript. -- Integrazione automatica con il sistema dei componenti. -- Invalidazione intelligente di parti delle pagine. -- Quantità minima di dati trasferiti. +**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 intuitivi [Latte|latte:]:** Il sistema di template più sicuro per PHP con funzionalità avanzate: -- Protezione automatica contro XSS con escaping sensibile al contesto. -- Estensibilità tramite filtri, funzioni e tag personalizzati. -- Ereditarietà dei template e snippet per AJAX. -- Eccellente supporto per PHP 8.x con sistema di tipi. +**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 appieno la Dependency Injection: -- Passaggio automatico delle dipendenze (autowiring). -- Configurazione tramite il chiaro formato NEON. -- Supporto per le factory di componenti. +**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**: Difesa automatica contro [vulnerabilità|nette:vulnerability-protection] come XSS, CSRF, ecc. -- **Produttività**: Meno codice da scrivere, più funzionalità grazie a un design intelligente. -- **Debugging**: [Tracy debugger|tracy:] con pannello di routing. -- **Prestazioni**: Cache intelligente, lazy loading dei componenti. -- **Flessibilità**: Facile modifica degli URL anche dopo il completamento dell'applicazione. -- **Componenti**: Sistema unico di elementi UI riutilizzabili. -- **Moderno**: Pieno supporto per PHP 8.4+ e sistema di tipi. +- **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 -Per iniziare ------------- +Primi passi +----------- -1. [Come funzionano le applicazioni? |how-it-works] - Comprensione dell'architettura di base. -2. [Presenter |presenters] - Lavorare con i presenter e le azioni. -3. [Template |templates] - Creazione di template in Latte. -4. [Routing |routing] - Configurazione degli indirizzi URL. -5. [Componenti interattivi |components] - Utilizzo del sistema di componenti. +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 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| 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 -Si applica all'ultima versione patch. +Vale per le ultime versioni patch. diff --git a/application/it/@left-menu.texy b/application/it/@left-menu.texy index 011cf72065..9db6fbd67b 100644 --- a/application/it/@left-menu.texy +++ b/application/it/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** +- [Panoramica |@home] - [Come funzionano le applicazioni? |how-it-works] -- [Bootstrapping] -- [Presenter |presenters] -- [Template |templates] +- [Bootstrapping|bootstrapping] +- [Presenter|presenters] +- [Template|templates] - [Struttura delle directory |directory-structure] -- [Routing |routing] +- [Routing|routing] - [Creazione di link URL |creating-links] - [Componenti interattivi |components] -- [AJAX & snippet |ajax] +- [AJAX e snippet |ajax] - [Multiplier |multiplier] -- [Configurazione |configuration] +- [Configurazione|configuration] +- [Aggiornamento|upgrading] -Ulteriori letture -***************** -- [Perché usare Nette? |www:10-reasons-why-nette] +Letture consigliate +******************* +- [Perché usare Nette?|www:10-reasons-why-nette] - [Installazione |nette:installation] -- [Scriviamo la prima applicazione! |quickstart:] -- [Guide e procedure |best-practices:] +- [Crea la tua prima applicazione! |quickstart:] +- [Best practice |best-practices:] - [Risoluzione dei problemi |nette:troubleshooting] diff --git a/application/it/ajax.texy b/application/it/ajax.texy index 54c00466b6..0968c573d6 100644 --- a/application/it/ajax.texy +++ b/application/it/ajax.texy @@ -1,12 +1,12 @@ -AJAX & snippet +AJAX e snippet **************
    -Nell'era delle moderne applicazioni web, dove la funzionalità è spesso suddivisa tra server e browser, AJAX è un elemento di collegamento essenziale. Quali opzioni ci offre Nette Framework in questo campo? -- invio di parti del template, i cosiddetti snippet -- passaggio di variabili tra PHP e JavaScript -- strumenti per il debug delle richieste AJAX +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
    @@ -14,9 +14,9 @@ Nell'era delle moderne applicazioni web, dove la funzionalità è spesso suddivi Richiesta AJAX ============== -Una richiesta AJAX non è fondamentalmente diversa da una classica richiesta HTTP. Viene chiamato un presenter con determinati parametri. E spetta al presenter decidere come reagire alla richiesta: può restituire dati in formato JSON, inviare una parte di codice HTML, un documento XML, ecc. +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, inizializziamo la richiesta AJAX utilizzando la funzione `fetch()`: +Sul lato browser avviamo una richiesta AJAX con la funzione `fetch()`: ```js fetch(url, { @@ -24,22 +24,22 @@ fetch(url, { }) .then(response => response.json()) .then(payload => { - // elaborazione della risposta + // elabora la risposta }); ``` -Sul lato server, riconosciamo una richiesta AJAX con il metodo `$httpRequest->isAjax()` del servizio [incapsulando la richiesta HTTP |http:request]. Per il rilevamento utilizza l'header HTTP `X-Requested-With`, quindi è importante inviarlo. All'interno del presenter è possibile utilizzare il metodo `$this->isAjax()`. +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 si desidera inviare dati in formato JSON, utilizzare il metodo [`sendJson()` |presenters#Invio della risposta]. Il metodo termina anche l'attività del presenter. +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); + $this->sendJson($this->model->getData()); } ``` -Se si prevede di rispondere con un template speciale progettato per AJAX, è possibile farlo come segue: +Se avete in programma di rispondere con un template speciale pensato per AJAX, potete farlo così: ```php public function handleClick($param): void @@ -55,35 +55,35 @@ public function handleClick($param): void Snippet ======= -Lo strumento più potente offerto da Nette per collegare il server al client sono gli snippet. Grazie ad essi, è possibile trasformare un'applicazione ordinaria in una AJAX con uno sforzo minimo e poche righe di codice. L'esempio Fifteen, il cui codice si trova su [GitHub |https://github.com/nette-examples/fifteen], dimostra come funziona il tutto. +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, o frammenti, consentono di aggiornare solo parti della pagina invece di ricaricare l'intera pagina. Questo non solo è più veloce ed efficiente, ma offre anche un'esperienza utente più confortevole. Gli snippet potrebbero ricordarvi Hotwire per Ruby on Rails o Symfony UX Turbo. È interessante notare che Nette ha introdotto gli snippet già 14 anni prima. +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 (richiesta non AJAX), viene caricata l'intera pagina, inclusi tutti gli snippet. Quando l'utente interagisce con la pagina (ad esempio, fa clic su un pulsante, invia un form, ecc.), viene attivata una richiesta AJAX invece di caricare l'intera pagina. Il codice nel presenter esegue l'azione e decide quali snippet devono essere aggiornati. Nette esegue il rendering di questi snippet e li invia come array in formato JSON. Il codice di gestione nel browser riceve gli snippet e li inserisce nuovamente nella pagina. Viene trasferito solo il codice degli snippet modificati, risparmiando larghezza di banda e accelerando il caricamento rispetto al trasferimento dell'intero contenuto della pagina. +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, viene utilizzata la [libreria Naja |https://naja.js.org]. [Installala |https://naja.js.org/#/guide/01-install-setup-naja] come pacchetto node.js (per l'uso con applicazioni Webpack, Rollup, Vite, Parcel e altre): +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 ``` -…o inseriscila direttamente nel template della pagina: +…oppure inseritela direttamente nel template della pagina: ```latte - + ``` -Innanzitutto, è necessario [inizializzare |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la libreria: +Per prima cosa dovete [inizializzare |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] la libreria: ```js naja.initialize(); ``` -Per trasformare un link ordinario (segnale) o l'invio di un form in una richiesta AJAX, è sufficiente contrassegnare il link, il form o il pulsante pertinente con la classe `ajax`: +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 @@ -100,32 +100,34 @@ oppure ``` -Ridisegno degli snippet +Ridisegnare gli snippet ----------------------- -Ogni oggetto della classe [Control |components] (incluso il Presenter stesso) tiene traccia se ci sono state modifiche che richiedono il suo ridisegno. A tale scopo viene utilizzato il metodo `redrawControl()`: +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 pertinente + // dopo il login è necessario ridisegnare la parte interessata $this->redrawControl(); // ... } ``` -Nette consente un controllo ancora più preciso su cosa deve essere ridisegnato. Il metodo menzionato può infatti accettare il nome dello snippet come argomento. È quindi possibile invalidare (cioè: forzare il ridisegno) a livello di parti del template. Se l'intero componente viene invalidato, verrà ridisegnato anche ogni suo snippet: +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 ---------------- -L'uso degli snippet in Latte è estremamente facile. Per definire una parte del template come snippet, è sufficiente racchiuderla tra i tag `{snippet}` e `{/snippet}`: +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} @@ -133,9 +135,9 @@ L'uso degli snippet in Latte è estremamente facile. Per definire una parte del {/snippet} ``` -Lo snippet crea un elemento `
    ` nella pagina HTML con un `id` speciale generato. Quando lo snippet viene ridisegnato, il contenuto di questo elemento viene aggiornato. Pertanto, è necessario che al rendering iniziale della pagina vengano renderizzati anche tutti gli snippet, anche se potrebbero essere inizialmente vuoti. +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. -È possibile creare uno snippet con un elemento diverso da `
    ` utilizzando l'attributo n: +Potete creare uno snippet anche con un elemento diverso da `
    `, usando un n:attributo: ```latte
    @@ -144,10 +146,10 @@ Lo snippet crea un elemento `
    ` nella pagina HTML con un `id` speciale gener ``` -Aree di snippet ---------------- +Aree snippet +------------ -I nomi degli snippet possono anche essere espressioni: +I nomi degli snippet possono essere anche espressioni: ```latte {foreach $items as $id => $item} @@ -155,7 +157,9 @@ I nomi degli snippet possono anche essere espressioni: {/foreach} ``` -In questo modo vengono creati diversi snippet `item-0`, `item-1`, ecc. Se invalidassimo direttamente uno snippet dinamico (ad esempio `item-1`), non verrebbe ridisegnato nulla. Il motivo è che gli snippet funzionano davvero come ritagli e vengono renderizzati solo direttamente. Tuttavia, nel template non esiste effettivamente uno snippet chiamato `item-1`. Questo viene creato solo eseguendo il codice circostante lo snippet, cioè il ciclo foreach. Pertanto, contrassegniamo la parte del template che deve essere eseguita utilizzando il tag `{snippetArea}`: +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
      @@ -165,16 +169,16 @@ In questo modo vengono creati diversi snippet `item-0`, `item-1`, ecc. Se invali
    ``` -E facciamo ridisegnare sia lo snippet stesso che l'intera area genitore: +E chiediamo il ridisegno sia del singolo snippet sia dell'intera area genitore: ```php $this->redrawControl('itemsContainer'); $this->redrawControl('item-1'); ``` -Allo stesso tempo, è consigliabile assicurarsi che l'array `$items` contenga solo gli elementi che devono essere ridisegnati. +Allo stesso tempo è opportuno assicurarsi che l'array `$items` contenga solo gli elementi da ridisegnare. -Se inseriamo un altro template nel template utilizzando il tag `{include}`, che contiene snippet, è necessario includere nuovamente l'inserimento del template in `snippetArea` e invalidarlo insieme allo snippet: +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} @@ -198,7 +202,7 @@ $this->redrawControl('item'); Snippet nei componenti ---------------------- -È possibile creare snippet anche nei [componenti |components] e Nette li ridisegnerà automaticamente. Ma c'è una limitazione: per ridisegnare gli snippet, chiama il metodo `render()` senza parametri. Quindi, il passaggio di parametri nel template non funzionerà: +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 @@ -210,10 +214,10 @@ non funzionerà: ``` -Invio di dati utente --------------------- +Inviare dati personalizzati +--------------------------- -Insieme agli snippet, è possibile inviare al client qualsiasi altro dato. È sufficiente scriverli nell'oggetto `payload`: +Insieme agli snippet potete inviare al client qualsiasi dato aggiuntivo. Basta scriverlo nell'oggetto `payload`: ```php public function actionDelete(int $id): void @@ -226,10 +230,16 @@ public function actionDelete(int $id): void ``` -Passaggio di parametri -====================== +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). + -Se inviamo parametri a un componente tramite una richiesta AJAX, siano essi parametri di segnale o parametri persistenti, dobbiamo specificare il loro nome globale nella richiesta, che include anche il nome del componente. Il nome completo del parametro viene restituito dal metodo `getParameterId()`. +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!}); @@ -247,3 +257,9 @@ 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 index 63e749dfe6..ab11b76910 100644 --- a/application/it/bootstrapping.texy +++ b/application/it/bootstrapping.texy @@ -3,19 +3,22 @@ Bootstrapping
    -Il bootstrapping è il processo di inizializzazione dell'ambiente dell'applicazione, creazione di un contenitore di dependency injection (DI) e avvio dell'applicazione. Discuteremo: +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 le applicazioni sono configurate utilizzando file NEON -- come distinguere tra modalità di produzione e sviluppo -- come creare e configurare il contenitore DI +- 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 loro esecuzione con una qualche forma di inizializzazione dell'ambiente. In passato, questo compito era spesso affidato a un file chiamato, ad esempio, `include.inc.php`, che il file iniziale includeva. Nelle moderne applicazioni Nette, questo è stato sostituito dalla classe `Bootstrap`, che, come parte dell'applicazione, si trova nel file `app/Bootstrap.php`. Potrebbe assomigliare, ad esempio, a questo: +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 @@ -26,9 +29,9 @@ class Bootstrap public function __construct() { $this->rootDir = dirname(__DIR__); - // Il Configurator è responsabile dell'impostazione dell'ambiente e dei servizi dell'applicazione. + // il configurator si occupa di impostare l'ambiente dell'applicazione e i servizi. $this->configurator = new Configurator; - // Imposta la directory per i file temporanei generati da Nette (es. template compilati) + // imposta la directory dei file temporanei generati da Nette (per esempio i template compilati) $this->configurator->setTempDirectory($this->rootDir . '/temp'); } @@ -41,14 +44,14 @@ class Bootstrap private function initializeEnvironment(): void { - // Nette è intelligente e la modalità di sviluppo si attiva automaticamente, - // oppure puoi abilitarla per un indirizzo IP specifico decommentando la riga seguente: + // 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: l'ultimo "coltellino svizzero" per il debug. + // attiva Tracy: il "coltellino svizzero" definitivo per il debugging. $this->configurator->enableTracy($this->rootDir . '/log'); - // RobotLoader: carica automaticamente tutte le classi nella directory selezionata + // RobotLoader: carica automaticamente tutte le classi della directory scelta $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); @@ -56,7 +59,7 @@ class Bootstrap private function setupContainer(): void { - // Carica i file di configurazione + // carica i file di configurazione $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } @@ -66,91 +69,100 @@ class Bootstrap index.php ========= -Il file iniziale nel caso delle applicazioni web è `index.php`, che si trova nella [directory pubblica |directory-structure#Directory pubblica www] `www/`. Questo file fa inizializzare l'ambiente e creare il container DI dalla classe Bootstrap. Successivamente, ottiene il servizio `Application` da esso, che avvia l'applicazione web: +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; -// Inizializzazione dell'ambiente + creazione del container DI +// inizializza l'ambiente e crea il container DI $container = $bootstrap->bootWebApplication(); -// Il container DI crea l'oggetto Nette\Application\Application +// il container DI crea un oggetto Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// Avvio dell'applicazione Nette ed elaborazione della richiesta in arrivo +// avvia l'applicazione Nette ed elabora la richiesta in arrivo $application->run(); ``` -Come si può vedere, la classe [api:Nette\Bootstrap\Configurator] aiuta con l'impostazione dell'ambiente e la creazione del container di dependency injection (DI), che ora presenteremo più in dettaglio. +.[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à Sviluppo vs Produzione -=============================== -Nette si comporta diversamente a seconda che sia in esecuzione su un server di sviluppo o di produzione: +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à Sviluppo (Development): - - Mostra la debugbar di Tracy con informazioni utili (query SQL, tempo di esecuzione, memoria utilizzata) - - In caso di errore, mostra una pagina di errore dettagliata con le chiamate alle funzioni e il contenuto delle variabili - - Aggiorna automaticamente la cache quando vengono modificati i template Latte, i file di configurazione, ecc. +🛠️ 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à Produzione (Production): - - Non mostra alcuna informazione di debug, tutti gli errori vengono scritti nel log - - In caso di errore, mostra ErrorPresenter o una pagina generica "Server Error" - - La cache non viene mai aggiornata automaticamente! - - Ottimizzato per velocità e sicurezza +🚀 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 selezione della modalità avviene tramite autodetect, quindi di solito non è necessario configurare nulla o passare manualmente: +La scelta della modalità avviene per rilevamento automatico, quindi di norma non c'è bisogno di configurare nulla né di cambiare modalità manualmente: -- modalità sviluppo: su localhost (indirizzo IP `127.0.0.1` o `::1`) se non è presente un proxy (cioè il suo header HTTP) -- modalità produzione: ovunque altrove +- 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 abilitare la modalità di sviluppo anche in altri casi, ad esempio per i programmatori che accedono da un indirizzo IP specifico, utilizziamo `setDebugMode()`: +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'); // è possibile specificare anche un array di indirizzi IP +$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. Memorizziamo un token segreto nel cookie `nette-debug`, ad esempio `secret1234`, e in questo modo attiviamo la modalità di sviluppo per i programmatori che accedono da un indirizzo IP specifico e che hanno anche il token menzionato nel cookie: +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 completamente la modalità di sviluppo, anche per localhost: +Possiamo anche disattivare del tutto la modalità di sviluppo, perfino su localhost: ```php $this->configurator->setDebugMode(false); ``` -Attenzione, il valore `true` attiva forzatamente la modalità di sviluppo, cosa che non deve mai accadere su un server di produzione. +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: -Strumento di Debug Tracy -======================== +```php +$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); +``` -Per un facile debug, attiviamo anche l'ottimo strumento [Tracy |tracy:]. In modalità sviluppo, visualizza gli errori e in modalità produzione, registra gli errori nella directory specificata: + +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 +File temporanei =============== -Nette utilizza la cache per il container DI, RobotLoader, template, ecc. Pertanto, è necessario impostare il percorso della directory in cui verrà memorizzata la cache: +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, imposta i [permessi di scrittura |nette:troubleshooting#Impostazione dei permessi delle directory] per le directory `log/` e `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 utilizzando [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 sottodirectory: +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() @@ -158,36 +170,38 @@ $this->configurator->createRobotLoader() ->register(); ``` -Un approccio alternativo è far caricare le classi solo tramite [Composer |best-practices:composer] rispettando PSR-4. +Un approccio alternativo è caricare le classi esclusivamente tramite [Composer |best-practices:composer], secondo PSR-4. -Timezone -======== +Fuso orario +=========== -Tramite il configuratore è possibile impostare il fuso orario predefinito. +Tramite il configurator potete impostare il fuso orario predefinito. ```php $this->configurator->setTimeZone('Europe/Prague'); ``` -Configurazione del Container DI +Configurazione del container DI =============================== -Parte del processo di avvio è la creazione del container DI, ovvero la factory di oggetti, che è il cuore dell'intera applicazione. Si tratta in realtà di una classe PHP generata da Nette e salvata nella directory della cache. La factory produce gli oggetti chiave dell'applicazione e, tramite i file di configurazione, le istruiamo su come crearli e impostarli, influenzando così il comportamento dell'intera applicazione. +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 sono solitamente scritti nel formato [NEON |neon:format]. In un capitolo separato, imparerai [cosa può essere configurato |nette:configuring]. +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à sviluppo, il container viene aggiornato automaticamente ad ogni modifica del codice o dei file di configurazione. In modalità produzione, viene generato solo una volta e le modifiche non vengono controllate per massimizzare le prestazioni. +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. -Carichiamo i file di configurazione utilizzando `addConfig()`: +I file di configurazione si caricano con `addConfig()`: ```php $this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` -Se vogliamo aggiungere più file di configurazione, possiamo chiamare la funzione `addConfig()` più volte. +Se vogliamo aggiungere altri file di configurazione, possiamo chiamare la funzione `addConfig()` più volte. ```php $configDir = $this->rootDir . '/config'; @@ -198,17 +212,17 @@ if (PHP_SAPI === 'cli') { } ``` -Il nome `cli.php` non è un errore di battitura, la configurazione può anche essere scritta in un file PHP che la restituisce come array. +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 anche aggiungere altri file di configurazione nella [sezione `includes` |dependency-injection:configuration#Inclusione di file]. +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, verranno sovrascritti o, nel caso di [array, uniti |dependency-injection:configuration#Unione]. Il file incluso successivamente ha una priorità maggiore rispetto al precedente. Il file in cui è specificata la sezione `includes` ha una priorità maggiore rispetto ai file inclusi in esso. +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 +Parametri statici ----------------- -I parametri utilizzati nei file di configurazione possono essere definiti [nella sezione `parameters` |dependency-injection:configuration#Parametri] e anche passati (o sovrascritti) con il metodo `addStaticParameters()` (ha l'alias `addParameters()`). È importante notare che valori diversi dei parametri causeranno la generazione di ulteriori container DI, ovvero ulteriori classi. +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([ @@ -216,13 +230,13 @@ $this->configurator->addStaticParameters([ ]); ``` -Al parametro `projectId` si può fare riferimento nella configurazione con la consueta notazione `%projectId%`. +Nella configurazione si può fare riferimento al parametro `projectId` con la notazione consueta `%projectId%`. -Parametri Dinamici +Parametri dinamici ------------------ -Possiamo aggiungere al container anche parametri dinamici, i cui valori diversi, a differenza dei parametri statici, non causano la generazione di nuovi container DI. +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([ @@ -230,7 +244,7 @@ $this->configurator->addDynamicParameters([ ]); ``` -In questo modo possiamo aggiungere semplicemente, ad esempio, variabili d'ambiente, a cui si può fare riferimento nella configurazione con la notazione `%env.variable%`. +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([ @@ -239,24 +253,25 @@ $this->configurator->addDynamicParameters([ ``` -Parametri Predefiniti +Parametri predefiniti --------------------- -Nei file di configurazione è possibile utilizzare questi parametri statici: +Nei file di configurazione potete usare questi parametri: -- `%appDir%` è il percorso assoluto alla directory contenente il file `Bootstrap.php` -- `%wwwDir%` è il percorso assoluto alla directory contenente il file di input `index.php` -- `%tempDir%` è il percorso assoluto alla directory per i file temporanei -- `%vendorDir%` è il percorso assoluto alla directory in cui Composer installa le librerie -- `%rootDir%` è il percorso assoluto alla directory principale del progetto +- `%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 tramite la riga di comando +- `%consoleMode%` indica se la richiesta è arrivata dalla riga di comando -Servizi Importati +Servizi importati ----------------- -Ora andiamo più a fondo. Sebbene lo scopo del container DI sia quello di creare oggetti, eccezionalmente potrebbe sorgere la necessità di inserire un oggetto esistente nel container. Lo facciamo definendo il servizio con il flag `imported: true`. +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: @@ -274,10 +289,10 @@ $this->configurator->addServices([ ``` -Ambienti Diversi +Ambienti diversi ================ -Non aver paura di modificare la classe Bootstrap secondo le tue esigenze. Puoi aggiungere parametri al metodo `bootWebApplication()` per distinguere i progetti web. Oppure possiamo aggiungere altri metodi, ad esempio `bootTestEnvironment()`, che inizializza l'ambiente per i test unitari, `bootConsoleApplication()` per gli script chiamati dalla riga di comando, ecc. +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 diff --git a/application/it/components.texy b/application/it/components.texy index 2d21975f48..fe52021058 100644 --- a/application/it/components.texy +++ b/application/it/components.texy @@ -1,9 +1,9 @@ -Componenti Interattivi +Componenti interattivi **********************
    -I componenti sono oggetti riutilizzabili indipendenti che inseriamo nelle pagine. Possono essere form, datagrid, sondaggi, praticamente qualsiasi cosa che abbia senso usare ripetutamente. Vedremo: +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? @@ -11,19 +11,19 @@ I componenti sono oggetti riutilizzabili indipendenti che inseriamo nelle pagine
    -Nette ha un sistema di componenti integrato. Qualcosa di simile potrebbe essere familiare ai veterani di Delphi o ASP.NET Web Forms, qualcosa di lontanamente simile è alla base di React o Vue.js. Tuttavia, nel mondo dei framework PHP, è una caratteristica unica. +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. -Eppure, i componenti influenzano fondamentalmente l'approccio alla creazione di applicazioni. Puoi comporre le pagine da unità pre-preparate. Hai bisogno di un datagrid nell'amministrazione? Lo trovi su [Componette |https://componette.org/search/component], un repository di add-on open-source (quindi non solo componenti) per Nette, e lo inserisci semplicemente nel presenter. +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. -Puoi incorporare un numero qualsiasi di componenti in un presenter. E in alcuni componenti puoi inserire altri componenti. Questo crea un albero di componenti, la cui radice è il 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 +Metodi factory ============== -Come vengono inseriti i componenti nel presenter e successivamente utilizzati? Di solito tramite 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 per creare componenti solo quando sono effettivamente necessari (lazy / on demand). L'intera magia sta nell'implementare un metodo chiamato `createComponent()`, dove `` è il nome del componente da creare, che crea e restituisce il componente. +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 @@ -31,27 +31,27 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } ``` -Grazie al fatto che tutti i componenti vengono creati in metodi separati, il codice guadagna in chiarezza. +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 sono scritti con una lettera maiuscola nel nome del metodo. +I nomi dei componenti iniziano sempre con una lettera minuscola, anche se nel nome del metodo sono scritti con la maiuscola. -Le factory non vengono mai chiamate direttamente; vengono chiamate automaticamente la prima volta che utilizziamo il componente. Grazie a ciò, il componente viene creato al momento giusto e solo quando è effettivamente necessario. Se non utilizziamo il componente (ad esempio, durante una richiesta AJAX in cui viene trasferita solo una parte della pagina, o durante la cache del template), non viene creato affatto e risparmiamo le prestazioni del server. +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 +// 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 renderizzare un componente utilizzando il tag [{control} |#Rendering]. Pertanto, non è necessario passare manualmente i componenti al template. +Nel template è possibile disegnare un componente con il tag [{control} |#Rendering]. Non c'è quindi bisogno di passare manualmente i componenti al template. ```latte

    Vota

    @@ -59,21 +59,26 @@ Nel template, è possibile renderizzare un componente utilizzando il tag [{contr {control poll} ``` +.[tip] +Per creare dinamicamente un numero variabile di componenti usate [Multiplier |multiplier]. -Stile Hollywood -=============== +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. -I componenti utilizzano comunemente una tecnica fresca che ci piace chiamare Stile Hollywood. Sicuramente conosci la frase famosa che i partecipanti ai casting cinematografici sentono così spesso: "Non chiamateci, vi chiameremo noi". Ed è proprio di questo che si tratta. -In Nette, invece di dover chiedere costantemente qualcosa ("il form è stato inviato?", "era valido?" o "l'utente ha premuto questo pulsante?"), dici al framework "quando succede, chiama questo metodo" e lasci il resto del lavoro a lui. Se programmi in JavaScript, conosci intimamente questo stile di programmazione. Scrivi funzioni che vengono chiamate quando si verifica un certo evento. E il linguaggio passa loro i parametri appropriati. +Stile hollywoodiano +=================== -Questo cambia completamente la prospettiva sulla scrittura delle applicazioni. Più compiti puoi lasciare al framework, meno lavoro hai tu. E meno cose puoi dimenticare. +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. -Scrivere un Componente +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, di solito intendiamo un discendente della classe [api:Nette\Application\UI\Control]. (Sarebbe quindi più preciso usare il termine "controlli", ma "controlli" ha un significato completamente diverso in italiano e "componenti" si è affermato di più.) Il presenter stesso [api:Nette\Application\UI\Presenter] è, tra l'altro, anche un discendente della classe `Control`. +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; @@ -87,19 +92,19 @@ class PollControl extends Control Rendering ========= -Sappiamo già che per renderizzare un componente si usa il tag `{control componentName}`. Questo in realtà chiama il metodo `render()` del componente, in cui ci occupiamo del rendering. Abbiamo a disposizione, proprio come nel presenter, un [template Latte|templates] nella variabile `$this->template`, a cui passiamo i parametri. A differenza del presenter, dobbiamo specificare il file del template e farlo renderizzare: +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 { - // inseriamo alcuni parametri nel template + // inserisce alcuni parametri nel template $this->template->param = $value; - // e lo renderizziamo + // e lo disegna $this->template->render(__DIR__ . '/poll.latte'); } ``` -Il tag `{control}` consente di passare parametri al metodo `render()`: +Il tag `{control}` permette di passare parametri al metodo `render()`: ```latte {control poll $id, $message} @@ -112,7 +117,7 @@ public function render(int $id, string $message): void } ``` -A volte un componente può essere composto da più parti che vogliamo renderizzare separatamente. Per ognuna di esse, creiamo il nostro metodo di rendering, qui nell'esempio `renderPaginator()`: +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 @@ -121,69 +126,69 @@ public function renderPaginator(): void } ``` -E nel template, lo chiamiamo poi usando: +E nel template lo richiamiamo poi così: ```latte {control poll:paginator} ``` -Per una migliore comprensione, è utile sapere come questo tag viene tradotto in PHP. +Per capire meglio è utile sapere come questo tag si traduce in codice PHP. ```latte {control poll} {control poll:paginator 123, 'hello'} ``` -viene tradotto come: +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 chiama il metodo `render()`, rispettivamente `renderPaginator()` se nel tag dopo i due punti è specificato un modo di rendering diverso. +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 da qualche parte nei parametri compare **`=>`**, tutti i parametri verranno impacchettati in un array e passati come primo argomento: +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'} ``` -viene tradotto come: +si traduce in: ```php $control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); ``` -Rendering di un sub-componente: +Rendering di un sottocomponente: ```latte {control cartControl-someForm} ``` -viene tradotto come: +si traduce in: ```php $control->getComponent("cartControl-someForm")->render(); ``` -I componenti, come i presenter, passano automaticamente diverse variabili utili ai template: +I componenti, come i presenter, passano automaticamente ai template alcune variabili utili: -- `$basePath` è il percorso URL assoluto alla directory principale (es. `/eshop`) -- `$baseUrl` è l'URL assoluto alla directory principale (es. `http://localhost/eshop`) -- `$user` è l'oggetto [che rappresenta l'utente |security:authentication] +- `$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` è l'array di [messaggi |#Messaggi flash] inviati dalla funzione `flashMessage()` +- `$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 reindirizzare a coppie `Presenter:action`. Ma cosa succede se vogliamo solo eseguire un'azione sulla **pagina corrente**? Ad esempio, cambiare l'ordinamento delle colonne in una tabella; eliminare un elemento; passare alla modalità chiaro/scuro; inviare un form; votare in un sondaggio; ecc. +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 è chiamato segnale. E proprio come le azioni invocano i metodi `action()` o `render()`, i segnali chiamano i metodi `handle()`. Mentre il concetto di azione (o view) è legato puramente ai presenter, i segnali riguardano tutti i componenti. E quindi anche i presenter, perché `UI\Presenter` è un discendente di `UI\Control`. +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 @@ -192,36 +197,36 @@ public function handleClick(int $x, int $y): void } ``` -Un link che chiama un segnale viene creato nel modo consueto, cioè nel template con l'attributo `n:href` o il tag `{link}`, nel codice con il metodo `link()`. Maggiori informazioni nel capitolo [Creazione di link URL |creating-links#Link a segnali]. +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 invocarlo su un altro presenter o un'altra azione. +Un segnale viene sempre chiamato sul presenter e sull'azione correnti; non è possibile richiamarlo su un altro presenter o su un'altra azione. -Quindi, un segnale provoca il ricaricamento della pagina proprio come nella richiesta originale, ma in più chiama il metodo di gestione del segnale con i parametri appropriati. Se il metodo non esiste, viene lanciata un'eccezione [api:Nette\Application\UI\BadSignalException], che viene mostrata all'utente come una pagina di errore 403 Forbidden. +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 potrebbero ricordarvi un po' AJAX: gestori che vengono invocati sulla pagina corrente. E avete ragione, i segnali vengono infatti spesso chiamati tramite AJAX e successivamente vengono trasferite al browser solo le parti modificate della pagina. Ovvero i cosiddetti snippet. Maggiori informazioni si trovano sulla [pagina dedicata ad AJAX |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 il proprio storage di messaggi flash indipendente dal presenter. Si tratta di messaggi che, ad esempio, informano sul risultato di un'operazione. Una caratteristica importante dei messaggi flash è che sono disponibili nel template anche dopo un redirect. Anche dopo essere stati visualizzati, rimangono attivi per altri 30 secondi – ad esempio, nel caso in cui l'utente aggiorni la pagina a causa di un errore di trasmissione - il messaggio non scomparirà immediatamente. +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. -L'invio è gestito dal metodo [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Il primo parametro è il testo del messaggio o un oggetto `stdClass` che rappresenta il messaggio. Il secondo parametro opzionale è il suo tipo (error, warning, info, ecc.). Il metodo `flashMessage()` restituisce un'istanza del messaggio flash come oggetto `stdClass`, a cui è possibile aggiungere ulteriori informazioni. +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('L\'elemento è stato eliminato.'); -$this->redirect(/* ... */); // e reindirizziamo +$this->flashMessage('Item was deleted.'); +$this->redirect(/* ... */); // e reindirizza ``` -Nel template, questi messaggi sono disponibili nella variabile `$flashes` come oggetti `stdClass`, che contengono le proprietà `message` (testo del messaggio), `type` (tipo del messaggio) e possono contenere le informazioni utente già menzionate. Li renderizziamo ad esempio così: +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} @@ -230,22 +235,22 @@ Nel template, questi messaggi sono disponibili nella variabile `$flashes` come o ``` -Redirect dopo un segnale -======================== +Redirect dopo l'elaborazione di un segnale +========================================== -Dopo l'elaborazione di un segnale di componente, spesso segue un redirect. È una situazione simile a quella dei form - dopo il loro invio reindirizziamo anche, in modo che l'aggiornamento della pagina nel browser non provochi un nuovo invio dei dati. +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 +$this->redirect('this'); // reindirizza al presenter e all'azione correnti ``` -Poiché un componente è un elemento riutilizzabile e di solito non dovrebbe avere un legame diretto con presenter specifici, i metodi `redirect()` e `link()` interpretano automaticamente il parametro come un segnale del componente: +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 +$this->redirect('click'); // reindirizza al segnale 'click' dello stesso componente ``` -Se è necessario reindirizzare a un altro presenter o azione, è possibile farlo tramite il presenter: +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 @@ -255,11 +260,11 @@ $this->getPresenter()->redirect('Product:show'); // reindirizza a un altro prese Parametri persistenti ===================== -I parametri persistenti servono a mantenere lo stato nei componenti tra richieste diverse. Il loro valore rimane lo stesso anche dopo aver cliccato su un link. A differenza dei dati nella sessione, vengono trasferiti nell'URL. E questo avviene in modo completamente automatico, inclusi i link creati in altri componenti sulla stessa pagina. +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. -Ad esempio, hai un componente per la paginazione del contenuto. Possono esserci più componenti di questo tipo su una pagina. E desideriamo che, dopo aver cliccato su un link, tutti i componenti rimangano sulla loro pagina corrente. Pertanto, rendiamo il numero di pagina (`page`) un parametro persistente. +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. -La creazione di un parametro persistente in Nette è estremamente semplice. Basta creare una proprietà pubblica e contrassegnarla con un attributo: (in precedenza si usava `/** @persistent */`) +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 @@ -271,43 +276,43 @@ class PaginatingControl extends Control } ``` -Per la proprietà, si consiglia di specificare anche il tipo di dati (es. `int`) e si può anche specificare un valore predefinito. I valori dei parametri possono essere [validati |#Validazione dei parametri persistenti]. +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]. -Durante la creazione di un link, è possibile modificare il valore del parametro persistente: +Quando si crea un link, il valore di un parametro persistente si può cambiare: ```latte -successivo +successiva ``` -Oppure può essere *resettato*, cioè rimosso dall'URL. Assumerà quindi il suo valore predefinito: +Oppure lo si può *azzerare*, cioè rimuovere dall'URL. Assumerà allora il proprio valore predefinito: ```latte -reset +azzera ``` Componenti persistenti ====================== -Non solo i parametri, ma anche i componenti possono essere persistenti. Per un tale componente, i suoi parametri persistenti vengono trasferiti anche tra diverse azioni del presenter o tra più presenter. I componenti persistenti sono contrassegnati da un'annotazione nella classe del presenter. Ad esempio, in questo modo contrassegniamo i componenti `calendar` e `poll`: +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 -/** - * @persistent(calendar, poll) - */ +use Nette\Application\Attributes\Persistent; + +#[Persistent('calendar', 'poll')] class DefaultPresenter extends Nette\Application\UI\Presenter { } ``` -I sottocomponenti all'interno di questi componenti non devono essere contrassegnati, diventeranno persistenti anch'essi. +I sottocomponenti di questi componenti non devono essere contrassegnati: diventano persistenti anch'essi. -In PHP 8, è possibile utilizzare anche attributi per contrassegnare i componenti persistenti: +La vecchia annotazione `@persistent` funziona ancora, ma è deprecata ed emette un avviso: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -317,15 +322,15 @@ class DefaultPresenter extends Nette\Application\UI\Presenter Componenti con dipendenze ========================= -Come creare componenti con dipendenze senza "inquinare" i presenter che li utilizzeranno? Grazie alle proprietà intelligenti del container DI in Nette, proprio come nell'uso dei servizi classici, è possibile lasciare la maggior parte del lavoro al framework. +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 come esempio un componente che ha una dipendenza dal servizio `PollFacade`: +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 int $id, // ID del sondaggio per cui creiamo il componente private PollFacade $facade, ) { } @@ -338,11 +343,11 @@ class PollControl extends Control } ``` -Se stessimo scrivendo un servizio classico, non ci sarebbe nulla da risolvere. Il container DI si occuperebbe invisibilmente di passare tutte le dipendenze. Ma con i componenti, di solito li gestiamo creando una nuova istanza direttamente nel presenter nei [#metodi factory] `createComponent…()`. Ma passare tutte le dipendenze di tutti i componenti al presenter per poi passarle ai componenti è macchinoso. E quanto codice scritto… +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 un servizio classico, lo passiamo al presenter e poi lo restituiamo nel metodo `createComponent…()`? Tale approccio è però inappropriato, perché vogliamo poter creare il componente anche più volte. +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 ci creerà il componente: +La soluzione corretta è scrivere una factory per il componente, cioè una classe che crei il componente al posto nostro: ```php class PollControlFactory @@ -366,7 +371,7 @@ services: - PollControlFactory ``` -e infine la utilizziamo nel nostro presenter: +e infine la usiamo nel nostro presenter: ```php class PollPresenter extends Nette\Application\UI\Presenter @@ -384,7 +389,7 @@ class PollPresenter extends Nette\Application\UI\Presenter } ``` -La cosa fantastica è che Nette DI può [generare |dependency-injection:factory] tali semplici factory, quindi invece del suo intero codice, basta scrivere solo la sua interfaccia: +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 @@ -393,21 +398,21 @@ interface PollControlFactory } ``` -E questo è tutto. Nette implementa internamente questa interfaccia e la passa al presenter, dove possiamo già utilizzarla. Aggiunge magicamente anche il parametro `$id` e l'istanza della classe `PollFacade` al nostro componente. +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`. -Componenti in profondità -======================== +I componenti in profondità +========================== -I componenti in Nette Application rappresentano parti riutilizzabili dell'applicazione web che inseriamo nelle pagine e a cui, del resto, è dedicato l'intero capitolo. Quali capacità ha esattamente un tale componente? +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) è renderizzabile nel template -2) sa [quale sua parte |ajax#Snippet] deve renderizzare durante una richiesta AJAX (snippet) -3) ha la capacità di memorizzare il proprio stato nell'URL (parametri persistenti) -4) ha la capacità di reagire alle azioni dell'utente (segnali) -5) crea una struttura gerarchica (dove la radice è il presenter) +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) -Ognuna di queste funzioni è gestita da una delle classi della linea ereditaria. Il rendering (1 + 2) è gestito da [api:Nette\Application\UI\Control], l'integrazione nel [ciclo di vita |presenters#Ciclo di vita del presenter] (3, 4) dalla classe [api:Nette\Application\UI\Component] e la creazione della struttura gerarchica (5) dalle classi [Container e Component |component-model:]. +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 } @@ -431,9 +436,9 @@ Ciclo di vita del componente Validazione dei parametri persistenti ------------------------------------- -I valori dei [#parametri persistenti] ricevuti dall'URL vengono scritti nelle proprietà dal metodo `loadState()`. Questo controlla anche se il tipo di dati specificato nella proprietà corrisponde, altrimenti risponde con un errore 404 e la pagina non viene visualizzata. +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 fidarti mai ciecamente dei parametri persistenti, perché possono essere facilmente sovrascritti dall'utente nell'URL. In questo modo, ad esempio, verifichiamo se il numero di pagina `$this->page` è maggiore di 0. Un modo appropriato è sovrascrivere il metodo `loadState()` menzionato: +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 @@ -444,7 +449,7 @@ class PaginatingControl extends Control public function loadState(array $params): void { parent::loadState($params); // qui viene impostato $this->page - // segue il controllo del valore personalizzato: + // segue il controllo personalizzato del valore: if ($this->page < 1) { $this->error(); } @@ -452,27 +457,41 @@ class PaginatingControl extends Control } ``` -Il processo opposto, cioè la raccolta dei valori dalle proprietà persistenti, è gestito dal metodo `saveState()`. +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à +----------------------- -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. -Un segnale provoca il ricaricamento della pagina proprio come nella richiesta originale (tranne quando viene chiamato tramite AJAX) e invoca il metodo `signalReceived($signal)`, la cui implementazione predefinita nella classe `Nette\Application\UI\Component` tenta di chiamare un metodo composto dalle parole `handle{signal}`. L'ulteriore elaborazione dipende dall'oggetto specifico. Gli oggetti che ereditano da `Component` (cioè `Control` e `Presenter`) reagiscono cercando di chiamare il metodo `handle{signal}` 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]. -In altre parole: prende la definizione della funzione `handle{signal}` e tutti i parametri che sono arrivati con la richiesta, e agli argomenti vengono assegnati i parametri dall'URL in base al nome e tenta di chiamare il metodo dato. Ad esempio, come parametro `$id` viene passato il valore dal parametro `id` nell'URL, come `$something` viene passato `something` dall'URL, ecc. E se il metodo non esiste, il metodo `signalReceived` lancia 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 implementa l'interfaccia `SignalReceiver` ed è connesso all'albero dei componenti. +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 serve come indicazione per un oggetto che deve fare qualcosa – un sondaggio deve contare il voto di un 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. +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 per un segnale viene creato utilizzando il metodo [Component::link() |api:Nette\Application\UI\Component::link()]. Come parametro `$destination` passiamo la stringa `{signal}!` 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, vengono aggiunti solo i parametri del segnale. Inoltre, viene aggiunto all'inizio il **parametro `?do`, che specifica il segnale**. +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 è `{signal}` o `{signalReceiver}-{signal}`. `{signalReceiver}` è il nome del componente nel presenter. Pertanto, non può esserci un trattino nel nome del componente – viene utilizzato per separare il nome del componente e il segnale, tuttavia è possibile nidificare più componenti in questo modo. +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()] verifica se il componente (primo argomento) è il destinatario del segnale (secondo argomento). Possiamo omettere il secondo argomento – quindi verifica se il componente è il destinatario di qualsiasi segnale. Come secondo parametro è possibile specificare `true` e verificare così se il destinatario non è solo il componente specificato, ma anche uno qualsiasi dei suoi discendenti. +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{signal}` possiamo eseguire il segnale manualmente chiamando il metodo [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], che si occupa di gestire il segnale – prende il componente che è stato determinato come destinatario del segnale (se non è specificato alcun destinatario del segnale, è il presenter stesso) e gli invia il segnale. +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: @@ -482,4 +501,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -In questo modo il segnale viene eseguito prematuramente e non verrà più chiamato. +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 index 04a2397c8a..10520edde4 100644 --- a/application/it/configuration.texy +++ b/application/it/configuration.texy @@ -1,8 +1,8 @@ -Configurazione delle Applicazioni -********************************* +Configurazione dell'applicazione +******************************** .[perex] -Panoramica delle opzioni di configurazione per le Applicazioni Nette. +Panoramica delle opzioni di configurazione di Nette Application. Application @@ -10,62 +10,64 @@ Application ```neon application: - # visualizzare il pannello "Nette Application" in Tracy BlueScreen? - debugger: ... # (bool) il default è true + # mostrare il pannello "Nette Application" nella BlueScreen di Tracy? + debugger: ... # (bool) attivo se Tracy è disponibile - # verrà chiamato l'error-presenter in caso di errore? - # ha effetto solo in modalità sviluppo - catchExceptions: ... # (bool) il default è true + # 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) il default è 'Nette:Error' + errorPresenter: Error # (string|array) di norma 'Nette:Error' - # definisce alias per presenter e azioni + # definisce gli alias per i presenter e le azioni aliases: ... - # definisce le regole per la traduzione del nome del presenter in classe + # definisce le regole per tradurre il nome del presenter in una classe mapping: ... - # i link errati non generano avvisi? - # ha effetto solo in modalità sviluppo - silentLinks: ... # (bool) il default è false + # silenziare gli avvisi sui link non validi? + # ha effetto solo in modalità di sviluppo + silentLinks: ... # (bool) di norma false ``` -Dalla versione `nette/application` 3.2 è possibile definire una coppia di error-presenter: +Da `nette/application` versione 3.2 è possibile definire una coppia di error presenter: ```neon application: errorPresenter: - 4xx: Error4xx # per l'eccezione Nette\Application\BadRequestException + 4xx: Error4xx # per Nette\Application\BadRequestException 5xx: Error5xx # per le altre eccezioni ``` -L'opzione `silentLinks` determina come Nette si comporta in modalità sviluppo quando la generazione di un link fallisce (ad esempio perché il presenter non esiste, ecc.). Il valore predefinito `false` significa che Nette genera un errore `E_USER_WARNING`. Impostandolo su `true` si sopprime questo messaggio di errore. Nell'ambiente di produzione, `E_USER_WARNING` viene sempre generato. Questo comportamento può essere influenzato anche impostando la variabile del presenter [$invalidLinkMode |creating-links#Link non validi]. +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. -Gli [Alias semplificano il collegamento |creating-links#Alias] ai presenter usati frequentemente. +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]. -La [Mappatura definisce le regole |directory-structure#Mappatura dei presenter], secondo le quali dal nome del presenter si deriva il nome della classe. +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 accelera notevolmente la loro creazione. Come Nette trova i presenter può essere configurato: +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 mappa delle classi di Composer? - scanComposer: ... # (bool) il default è true + # cercare i presenter nella class map di Composer? + scanComposer: ... # (bool) di norma true - # maschera a cui devono corrispondere il nome della classe e del file - scanFilter: ... # (string) il default è '*Presenter' + # 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) il default è '%appDir%' + scanDirs: # (string[]|false) di norma '%appDir%' - %vendorDir%/mymodule ``` -Le directory specificate in `scanDirs` non sovrascrivono il valore predefinito `%appDir%`, ma lo completano, quindi `scanDirs` conterrà entrambi i percorsi `%appDir%` e `%vendorDir%/mymodule`. Se volessimo omettere la directory predefinita, useremmo un [punto esclamativo |dependency-injection:configuration#Unione], che sovrascrive il valore: +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: @@ -73,36 +75,42 @@ application: - %vendorDir%/mymodule ``` -La scansione delle directory può essere disattivata specificando il valore `false`. Non consigliamo di sopprimere completamente l'aggiunta automatica dei presenter, perché altrimenti le prestazioni dell'applicazione diminuiranno. +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 ============== -Con questa impostazione è possibile influenzare globalmente il comportamento di Latte nei componenti e nei presenter. +Questa impostazione influisce globalmente sul comportamento di Latte nei componenti e nei presenter. ```neon latte: - # visualizzare il pannello Latte nella Tracy Bar per il template principale (true) o tutti i componenti (all)? - debugger: ... # (true|false|'all') il default è true + # 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 - # genera template con l'intestazione declare(strict_types=1) - strictTypes: ... # (bool) il default è false + # attiva la [modalità di analisi rigorosa |latte:develop#strict mode] + strictParsing: ... # (bool) di norma false - # attiva la modalità [parser rigoroso |latte:develop#striktní režim] - strictParsing: ... # (bool) il default è false + # limita l'ambito delle variabili al corpo del ciclo + scopedLoopVariables: ... # (bool) di norma false - # attiva il [controllo del codice generato |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) il default è null + # rimuove l'indentazione dovuta all'annidamento nei tag di tipo pari + dedent: ... # (bool) di norma false - # imposta la locale - locale: it_IT # (string) il default è null + # 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 # il default è Nette\Bridges\ApplicationLatte\DefaultTemplate + templateClass: App\MyTemplateClass # di norma Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Se usi Latte versione 3, puoi aggiungere nuove [estensioni |latte:extending-latte#Latte Extension] usando: +Potete aggiungere nuove [estensioni |latte:extending-latte#Estensione di Latte] così: ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Se usi Latte versione 2, puoi registrare nuovi tag specificando il nome della classe o un riferimento a un servizio. Per impostazione predefinita, viene chiamato il metodo `install()`, ma questo può essere modificato specificando il nome di un altro metodo: - -```neon -latte: - # registrazione di tag Latte personalizzati - macros: - - App\MyLatteMacros::register # metodo statico, nomeclasse o callable - - @App\MyLatteMacrosFactory # servizio con metodo install() - - @App\MyLatteMacrosFactory::register # servizio con metodo register() - -services: - - App\MyLatteMacrosFactory -``` - Routing ======= @@ -132,14 +126,14 @@ Impostazioni di base: ```neon routing: - # visualizzare il pannello di routing nella Tracy Bar? - debugger: ... # (bool) il default è true + # 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) il default è false + cache: ... # (bool) di norma false ``` -Il routing viene solitamente definito nella classe [RouterFactory |routing#Collezione di route]. In alternativa, le route possono essere definite anche nella configurazione utilizzando coppie `maschera: azione`, ma questo metodo non offre una così ampia variabilità nelle impostazioni: +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: @@ -159,20 +153,20 @@ constants: Foobar: 'baz' ``` -Dopo l'avvio dell'applicazione, verrà creata la costante `Foobar`. +La costante `Foobar` verrà creata dopo l'avvio dell'applicazione. .[note] -Le costanti non dovrebbero servire come una sorta di variabili globalmente disponibili. Per passare valori agli oggetti, utilizza la [dependency injection |dependency-injection:passing-dependencies]. +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 PHP. Una panoramica di tutte le direttive si trova su [php.net |https://www.php.net/manual/en/ini.list.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/Rome + date.timezone: Europe/Prague ``` @@ -181,11 +175,12 @@ Servizi DI Questi servizi vengono aggiunti al container DI: -| Nome | Tipo | Descrizione -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [avviatore dell'intera applicazione |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | factory per i presenter -| `application.###` | [api:Nette\Application\UI\Presenter] | singoli presenter -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | factory dell'oggetto `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory per [`$this->template` |templates] +| 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 index 6dc629a81b..9068de2e9f 100644 --- a/application/it/creating-links.texy +++ b/application/it/creating-links.texy @@ -3,132 +3,149 @@ Creazione di link URL
    -Creare link in Nette è semplice come puntare il dito. Basta mirare e il framework farà tutto il lavoro per te. Vedremo: +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 distinguere un link alla pagina corrente +- come riconoscere un link alla pagina corrente - cosa fare con i link non validi
    -Grazie al [routing bidirezionale |routing], non dovrai mai scrivere hardcoded gli indirizzi URL della tua applicazione nei template o nel codice, che potrebbero cambiare in seguito, o comporli in modo complicato. Nel link, basta specificare il presenter e l'azione, passare eventuali parametri e il framework genererà l'URL da solo. In realtà, è molto simile a chiamare una funzione. Ti piacerà. +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 link nei template e un ottimo aiuto è l'attributo `n:href`: +Il più delle volte creiamo i link nei template, e l'attributo `n:href` è un ottimo aiuto: ```latte dettaglio ``` -Nota che invece dell'attributo HTML `href`, abbiamo usato l'[attributo n |latte:syntax#n:attributi] `n:href`. Il suo valore non è quindi un URL, come sarebbe nel caso dell'attributo `href`, ma il nome del presenter e dell'azione. +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 sul link è, in parole povere, qualcosa come chiamare il metodo `ProductPresenter::renderShow()`. E se ha parametri nella sua firma, possiamo chiamarlo con argomenti: +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 prodotto +dettaglio del prodotto ``` -È possibile passare anche parametri nominati. Il seguente link passa il parametro `lang` con il valore `cs`: +È possibile passare anche parametri nominali. Il link seguente passa il parametro `lang` con il valore `en`: ```latte -dettaglio prodotto +dettaglio del prodotto ``` -Se il metodo `ProductPresenter::renderShow()` non ha `$lang` nella sua firma, può ottenere il valore del parametro usando `$lang = $this->getParameter('lang')` o dalla [proprietà |presenters#Parametri della richiesta]. +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 memorizzati in un array, possono essere espansi con l'operatore `...` (in Latte 2.x con l'operatore `(expand)`): +Se i parametri sono salvati in un array, si possono espandere con l'operatore `...`: ```latte -{var $args = [$product->id, lang => cs]} -dettaglio prodotto +{var $args = [$product->id, lang => en]} +dettaglio del prodotto ``` -Nei link vengono automaticamente passati anche i cosiddetti [parametri persistenti |presenters#Parametri persistenti]. +Nei link vengono passati automaticamente anche i cosiddetti [parametri persistenti |presenters#Parametri persistenti]. -L'attributo `n:href` è molto utile per i tag HTML ``. Se vogliamo stampare il link altrove, ad esempio nel testo, usiamo `{link}`: +L'attributo `n:href` è molto comodo per i tag HTML ``. Se vogliamo stampare il link altrove, per esempio nel testo, usiamo `{link}`: ```latte -L'indirizzo è: {link Home:default} +L'URL è: {link Home:default} ``` Nel codice ========== -Per creare un link nel presenter, si usa il metodo `link()`: +Per creare un link nel presenter si usa il metodo `link()`: ```php $url = $this->link('Product:show', $product->id); ``` -I parametri possono essere passati anche tramite un array, dove è possibile specificare anche parametri nominati: +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' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -I link possono essere creati anche senza un presenter, per questo c'è [#LinkGenerator] e il suo metodo `link()`. +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: -Link al presenter -================= +```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 e un'azione, ha questa sintassi: +Se la destinazione del link è un presenter con un'azione, la sintassi è questa: ``` [//] [[[[:]module:]presenter:]action | this] [#fragment] ``` -Il formato è supportato da tutti i tag 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 [#LinkGenerator]. Quindi, anche se negli esempi viene usato `n:href`, potrebbe esserci una qualsiasi delle funzioni. +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 base è quindi `Presenter:action`: +La forma di base è dunque `Presenter:azione`: ```latte -pagina iniziale +home page ``` -Se ci colleghiamo a un'azione del presenter corrente, possiamo omettere il suo nome: +Se colleghiamo a un'azione del presenter corrente, possiamo ometterne il nome: ```latte -pagina iniziale +home page ``` -Se la destinazione è l'azione `default`, possiamo ometterla, ma i due punti devono rimanere: +Se l'azione di destinazione è `default`, possiamo ometterla, ma i due punti devono restare: ```latte -pagina iniziale +home page ``` -I link possono anche puntare ad altri [moduli |directory-structure#Presenter e template]. Qui i link si distinguono in relativi a un sottomodulo nidificato o assoluti. Il principio è analogo ai percorsi su disco, solo che al posto degli slash ci sono i due punti. Supponiamo che il presenter corrente faccia parte del modulo `Front`, allora scriveremo: +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 speciale è un link [a se stesso |#Link alla pagina corrente], dove specifichiamo `this` come destinazione. +Un caso particolare è il link [a sé stessi |#Link alla pagina corrente], dove indichiamo come destinazione `this`. ```latte aggiorna ``` -Possiamo collegarci a una parte specifica della pagina tramite il cosiddetto frammento dopo il simbolo cancelletto `#`: +Possiamo collegare a una parte precisa della pagina tramite il cosiddetto frammento dopo il cancelletto `#`: ```latte -link a Home:default e frammento #main +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 usando `link()` o `n:href` sono sempre percorsi assoluti (cioè iniziano con `/`), ma non URL assoluti con protocollo e dominio come `https://domain`. +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`. -Per generare un URL assoluto, aggiungi due slash all'inizio (es. `n:href="//Home:"`). Oppure si può impostare il presenter per generare 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 @@ -140,17 +157,17 @@ La destinazione `this` crea un link alla pagina corrente: aggiorna ``` -Allo stesso tempo, vengono trasferiti anche tutti i parametri specificati nella firma del metodo `action()` o `render()`, se `action()` non è definita. Quindi, se siamo sulla pagina `Product:show` e `id: 123`, il link a `this` passerà anche questo parametro. +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. -Ovviamente, è possibile specificare i parametri direttamente: +Naturalmente è possibile indicare i parametri direttamente: ```latte aggiorna ``` -La funzione `isLinkCurrent()` verifica se la destinazione del link è identica alla pagina corrente. Questo può essere utilizzato, ad esempio, nel template per distinguere i link, ecc. +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 è anche possibile specificare un carattere jolly `*` invece di un'azione specifica, che significa qualsiasi azione del presenter dato. +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')} @@ -162,15 +179,15 @@ I parametri sono gli stessi del metodo `link()`, ma è anche possibile specifica ``` -In combinazione con `n:href` in un unico elemento, si può usare la forma abbreviata: +In combinazione con `n:href` su un unico elemento si può usare una forma abbreviata: ```latte ... ``` -Il carattere jolly `*` può essere usato solo al posto dell'azione, non del presenter. +Il carattere jolly `*` si può usare solo al posto dell'azione, non del presenter. -Per verificare se siamo in un certo modulo o in un suo sottomodulo, usiamo il metodo `isModuleCurrent(moduleName)`. +Per stabilire se ci troviamo in un determinato modulo o in un suo sottomodulo, usate il metodo `isModuleCurrent(moduleName)`. ```latte
  • @@ -179,56 +196,72 @@ Per verificare se siamo in un certo modulo o in un suo sottomodulo, usiamo il me ``` -Link a segnali -============== +Cambiare la base dei link .{data-version:3.2.7} +=============================================== -La destinazione di un link non deve essere solo un presenter e un'azione, ma anche un [segnale |components#Segnale] (chiamano il metodo `handle()`). Allora la sintassi è la seguente: +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: ``` -[//] [sub-component:]signal! [#fragment] +[//] [sotto-componente:]segnale! [#fragment] ``` -Il segnale è quindi distinto dal punto esclamativo: +Il segnale si distingue quindi per il punto esclamativo: ```latte segnale ``` -È possibile creare anche un link al segnale di un sottocomponente (o sotto-sottocomponente): +Potete creare anche un link a un segnale di un sottocomponente (o di un sotto-sottocomponente): ```latte segnale ``` -Link nel componente -=================== +Link in un componente +===================== -Poiché i [componenti |components] sono unità riutilizzabili separate che non dovrebbero avere legami con i presenter circostanti, i link funzionano in modo leggermente diverso qui. L'attributo Latte `n:href` e il tag `{link}` così come i metodi del componente come `link()` e altri considerano la destinazione del link **sempre come il nome del segnale**. Pertanto, non è nemmeno necessario specificare il punto esclamativo: +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 azione +segnale, non un'azione ``` -Se volessimo collegarci ai presenter nel template del componente, useremmo il tag `{plink}`: +Se volessimo collegare ai presenter nel template di un componente, useremmo il tag `{plink}`: ```latte -inizio +home ``` -o nel codice +oppure nel codice ```php $this->getPresenter()->link('Home:default') ``` -Alias .{data-version:v3.2.2} -============================ +Alias .{data-version:3.2.3} +=========================== -A volte può essere utile assegnare un alias facilmente memorizzabile alla coppia Presenter:azione. Ad esempio, chiamare la pagina iniziale `Front:Home:default` semplicemente `home` o `Admin:Dashboard:default` come `admin`. +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 sono definiti nella [configurazione|configuration] sotto la chiave `application › aliases`: +Gli alias si definiscono nella [configurazione|configuration], sotto la chiave `application › aliases`: ```neon application: @@ -238,7 +271,7 @@ application: sign: Front:Sign:in ``` -Nei link, vengono poi scritti usando la chiocciola, ad esempio: +Nei link si scrivono poi con la chiocciola, per esempio: ```latte amministrazione @@ -250,14 +283,14 @@ Sono supportati anche in tutti i metodi che lavorano con i link, come `redirect( Link non validi =============== -Può capitare di creare un link non valido - sia perché punta a un presenter inesistente, sia perché passa più parametri di quelli che il metodo di destinazione accetta nella sua firma, o quando non è possibile generare un URL per l'azione di destinazione. Come gestire i link non validi è determinato dalla variabile statica `Presenter::$invalidLinkMode`. Può assumere una combinazione di questi valori (costanti): +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, come URL viene restituito il carattere # -- `Presenter::InvalidLinkWarning` - viene generato un avviso E_USER_WARNING, che verrà registrato in modalità produzione, ma non causerà l'interruzione dell'esecuzione dello script +- `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` - viene lanciata l'eccezione InvalidLinkException +- `Presenter::InvalidLinkException` - solleva InvalidLinkException -L'impostazione predefinita è `InvalidLinkWarning` in modalità produzione e `InvalidLinkWarning | InvalidLinkTextual` in modalità sviluppo. `InvalidLinkWarning` nell'ambiente di produzione non causerà l'interruzione dello script, ma l'avviso verrà registrato. Nell'ambiente di sviluppo, verrà catturato da [Tracy |tracy:] e verrà visualizzato un bluescreen. `InvalidLinkTextual` funziona restituendo un messaggio di errore come URL, che inizia con i caratteri `#error:`. Per rendere tali link evidenti a prima vista, aggiungiamo al CSS: +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:"] { @@ -266,7 +299,7 @@ a[href^="#error:"] { } ``` -Se non vogliamo che vengano prodotti avvisi nell'ambiente di sviluppo, possiamo impostare la modalità silenziosa direttamente nella [configurazione|configuration]. +Se non vogliamo che in ambiente di sviluppo vengano emessi avvisi, possiamo silenziarli direttamente nella [configurazione|configuration]. ```neon application: @@ -277,10 +310,10 @@ application: LinkGenerator ============= -Come creare link con una comodità simile a quella del metodo `link()`, ma senza la presenza di un presenter? Per questo c'è [api:Nette\Application\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 puoi farti passare tramite il costruttore e poi creare link con il suo metodo `link()`. +LinkGenerator è un servizio che potete farvi passare tramite il costruttore e con cui potete poi creare link usandone il metodo `link()`. -Rispetto ai presenter, c'è una differenza. LinkGenerator crea tutti i link direttamente come URL assoluti. Inoltre, non esiste un "presenter corrente", quindi non è possibile specificare solo il nome dell'azione `link('default')` come destinazione o specificare percorsi relativi ai moduli. +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 lanciano sempre `Nette\Application\UI\InvalidLinkException`. +I link non validi sollevano sempre `Nette\Application\UI\InvalidLinkException`. diff --git a/application/it/directory-structure.texy b/application/it/directory-structure.texy index 7ed91fdfa6..f4a1ae9c48 100644 --- a/application/it/directory-structure.texy +++ b/application/it/directory-structure.texy @@ -1,29 +1,29 @@ -Struttura della Directory dell'Applicazione +Struttura delle directory dell'applicazione *******************************************
    -Come progettare una struttura di directory chiara e scalabile per i progetti in Nette Framework? Mostreremo le best practice che ti aiuteranno a organizzare il codice. Imparerai: +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 **dividere logicamente** l'applicazione in directory -- come progettare la struttura in modo che **scali bene** con la crescita del progetto -- quali sono le **alternative possibili** e i loro vantaggi o svantaggi +- 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 menzionare che Nette Framework stesso non impone alcuna struttura specifica. È progettato per essere facilmente adattabile a qualsiasi esigenza e preferenza. +È 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 ============================== -Sebbene Nette Framework non detti alcuna struttura di directory fissa, esiste una disposizione predefinita comprovata sotto forma di [Web Project|https://github.com/nette/web-project]: +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 con l'applicazione -├── assets/ ← file SCSS, JS, immagini..., alternativamente resources/ +├── app/ ← directory dell'applicazione +├── assets/ ← file SCSS, JS, immagini..., in alternativa resources/ ├── bin/ ← script per la riga di comando ├── config/ ← configurazione ├── log/ ← errori registrati @@ -33,13 +33,13 @@ Sebbene Nette Framework non detti alcuna struttura di directory fissa, esiste un └── www/ ← directory pubblica (document-root) \-- -Puoi modificare liberamente questa struttura in base alle tue esigenze - rinominare o spostare le cartelle. Successivamente, basta solo aggiornare i percorsi relativi alle directory nel file `Bootstrap.php` e eventualmente `composer.json`. Non è necessario nient'altro, nessuna riconfigurazione complessa, nessuna modifica delle costanti. Nette dispone di un intelligente autodetect e riconosce automaticamente la posizione dell'applicazione, inclusa la sua base URL. +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 esplori per la prima volta un nuovo progetto, dovresti orientarti rapidamente. Immagina di espandere la directory `app/Model/` e vedere questa struttura: +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/ @@ -48,9 +48,9 @@ Quando esplori per la prima volta un nuovo progetto, dovresti orientarti rapidam └── Entities/ \-- -Da essa deduci solo che il progetto utilizza alcuni servizi, repository ed entità. Non impari assolutamente nulla sullo scopo effettivo dell'applicazione. +Da qui imparate solo che il progetto usa dei servizi, dei repository e delle entità. Non imparate nulla sullo scopo reale dell'applicazione. -Vediamo un approccio diverso - **organizzazione per domini**: +Guardiamo un approccio diverso, l'**organizzazione per domini**: /--pre app/Model/ @@ -60,76 +60,76 @@ Vediamo un approccio diverso - **organizzazione per domini**: └── Product/ \-- -Qui è diverso - a prima vista è 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. +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 (organizzazione per tipo di classi) porta in pratica una serie di problemi: il codice che è logicamente correlato è frammentato in diverse cartelle e devi saltare tra di esse. Pertanto, organizzeremo per domini. +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 nell'applicazione. Ciò significa che la posizione fisica dei file corrisponde al loro namespace. Ad esempio, una classe situata in `app/Model/Product/ProductRepository.php` dovrebbe avere il namespace `App\Model\Product`. Questo principio aiuta nell'orientamento nel codice e semplifica l'autoloading. +È 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 vs Plurale nei nomi ------------------------------ +Singolare o plurale nei nomi +---------------------------- -Nota che per le directory principali dell'applicazione usiamo il singolare: `app`, `config`, `log`, `temp`, `www`. Allo stesso modo anche all'interno dell'applicazione: `Model`, `Core`, `Presentation`. Questo perché ognuna di esse rappresenta un concetto unitario. +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, ad esempio, `app/Model/Product` rappresenta tutto ciò che riguarda i prodotti. Non lo chiameremo `Products`, perché non è una cartella piena di prodotti (ci sarebbero file `nokia.php`, `samsung.php`). È un namespace contenente classi per lavorare con i prodotti - `ProductRepository.php`, `ProductService.php`. +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à separata. +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 utilizzare: -- Singolare per namespace che rappresentano un'unità funzionale (anche se lavorano con più entità) -- Plurale per collezioni di unità separate -- In caso di incertezza o se non vuoi pensarci, scegli il singolare +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 (la cosiddetta document-root). Spesso si può incontrare anche il nome `public/` invece di `www/` - è solo una questione di convenzione e non influisce sulla funzionalità del framework. La directory contiene: -- [Punto di ingresso |bootstrapping#index.php] dell'applicazione `index.php` -- File `.htaccess` con regole per mod_rewrite (per Apache) -- File statici (CSS, JavaScript, immagini) -- File caricati +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, è fondamentale avere la [document-root configurata correttamente |nette:troubleshooting#Come modificare o rimuovere la directory www dall URL]. +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 posizionare mai la cartella `node_modules/` in questa directory - contiene migliaia di file che possono essere eseguibili e non dovrebbero essere accessibili pubblicamente. +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/` ================================== -Questa è la directory principale con il codice dell'applicazione. Struttura di base: +È 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 di comando +├── 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ù nel dettaglio le singole sottodirectory. +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 breve `app/UI`. È il posto per tutti i presenter, i loro template e eventuali classi di supporto. +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 layer per domini. In un progetto complesso che combina e-shop, blog e API, la struttura sarebbe simile a questa: +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 e-shop +├── Shop/ ← frontend dell'e-shop │ ├── Product/ │ ├── Cart/ │ └── Order/ @@ -139,11 +139,11 @@ Organizziamo questo layer per domini. In un progetto complesso che combina e-sho ├── Admin/ ← amministrazione │ ├── Dashboard/ │ └── Products/ -└── Api/ ← endpoint API +└── Api/ ← endpoint dell'API └── V1/ \-- -Al contrario, per un semplice blog, useremmo la seguente suddivisione: +Al contrario, per un semplice blog useremmo questa struttura: /--pre app/Presentation/ @@ -154,12 +154,12 @@ Al contrario, per un semplice blog, useremmo la seguente suddivisione: │ ├── Dashboard/ │ └── Posts/ ├── Error/ -└── Export/ ← RSS, sitemap, ecc. +└── Export/ ← RSS, sitemap ecc. \-- -Cartelle come `Home/` o `Dashboard/` contengono presenter e template. Cartelle come `Front/`, `Admin/` o `Api/` le chiamiamo **moduli**. Tecnicamente, sono directory normali che servono a dividere logicamente l'applicazione. +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 con un presenter contiene un presenter con lo stesso nome e i suoi template. Ad esempio, la cartella `Dashboard/` contiene: +Ogni cartella che contiene un presenter comprende il file del presenter stesso e i suoi template. Per esempio la cartella `Dashboard/` contiene: /--pre Dashboard/ @@ -167,7 +167,7 @@ Ogni cartella con un presenter contiene un presenter con lo stesso nome e i suoi └── default.latte ← template \-- -Questa struttura di directory si riflette nei namespace delle classi. Ad esempio, `DashboardPresenter` si trova nel namespace `App\Presentation\Admin\Dashboard` (vedi [#Mappatura dei presenter]): +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; @@ -178,22 +178,22 @@ class DashboardPresenter extends Nette\Application\UI\Presenter } ``` -Al presenter `Dashboard` all'interno del modulo `Admin` facciamo riferimento nell'applicazione usando la notazione con i due punti come `Admin:Dashboard`. Alla sua azione `default` poi come `Admin:Dashboard:default`. In caso di moduli nidificati, usiamo più due punti, ad esempio `Shop:Order:Detail:default`. +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 è come si adatta elegantemente alle crescenti esigenze del progetto. Prendiamo come esempio la parte che genera feed XML. All'inizio abbiamo una forma semplice: +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 per la sitemap -└── feed.latte ← template per il feed RSS +├── sitemap.latte ← template della sitemap +└── feed.latte ← template del feed RSS \-- -Con il tempo, si aggiungono altri tipi di feed e abbiamo bisogno di più logica per essi... Nessun problema! La cartella `Export/` diventa semplicemente un modulo: +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/ @@ -202,38 +202,38 @@ Con il tempo, si aggiungono altri tipi di feed e abbiamo bisogno di più logica │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← feed per Zboží.cz - └── heureka.latte ← feed per Heureka.cz + ├── amazon.latte ← feed per Amazon + └── ebay.latte ← feed per eBay \-- -Questa trasformazione è assolutamente fluida - basta creare nuove sottocartelle, dividerci il codice e aggiornare i link (ad esempio da `Export:feed` a `Export:Feed:zbozi`). Grazie a ciò, possiamo espandere gradualmente la struttura secondo necessità, il livello di nidificazione non è limitato in alcun modo. +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, ad esempio, nell'amministrazione hai molti presenter relativi alla gestione degli ordini, come sono `OrderDetail`, `OrderEdit`, `OrderDispatch` ecc., puoi creare un modulo (cartella) `Order` in questo punto per una migliore organizzazione, che conterrà (le cartelle per) i presenter `Detail`, `Edit`, `Dispatch` e altri. +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. -Posizionamento dei template ---------------------------- +Posizione dei template +---------------------- -Negli esempi precedenti abbiamo visto che i template si trovano direttamente nella cartella con il presenter: +Negli esempi precedenti abbiamo visto che i template si trovano direttamente nella cartella del presenter: /--pre Dashboard/ ├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← classe opzionale per il template +├── DashboardTemplate.php ← classe del template, facoltativa └── default.latte ← template \-- -Questa posizione si rivela in pratica la più comoda - hai tutti i file correlati subito a portata di mano. +Nella pratica questa collocazione si rivela la più comoda: avete tutti i file collegati subito a portata di mano. -In alternativa, puoi posizionare i template in una sottocartella `templates/`. Nette supporta entrambe le varianti. Puoi persino posizionare i template completamente al di fuori della cartella `Presentation/`. Tutto sulle possibilità di posizionamento dei template si trova nel capitolo [Ricerca dei template |templates#Ricerca dei template]. +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 spesso appartengono anche altri file di supporto. Li posizioniamo logicamente in base al loro ambito di applicazione: +Ai presenter e ai template si accompagnano spesso altri file di supporto. Li collochiamo logicamente in base al loro ambito: -1. **Direttamente presso il presenter** nel caso di componenti specifici per quel presenter: +1. **Direttamente con il presenter**, nel caso di componenti specifici di quel presenter: /--pre Product/ @@ -242,7 +242,7 @@ Ai presenter e ai template spesso appartengono anche altri file di supporto. Li └── FilterForm.php ← form per il filtraggio \-- -2. **Per il modulo** - consigliamo di utilizzare la cartella `Accessory`, che si posiziona ordinatamente all'inizio dell'alfabeto: +2. **Per il modulo**: consigliamo di usare la cartella `Accessory`, che in ordine alfabetico si colloca comodamente all'inizio: /--pre Front/ @@ -253,7 +253,7 @@ Ai presenter e ai template spesso appartengono anche altri file di supporto. Li └── Cart/ \-- -3. **Per l'intera applicazione** - in `Presentation/Accessory/`: +3. **Per l'intera applicazione**: in `Presentation/Accessory/`: /--pre app/Presentation/ ├── Accessory/ @@ -263,30 +263,30 @@ Ai presenter e ai template spesso appartengono anche altri file di supporto. Li └── Admin/ \-- -Oppure puoi posizionare 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 abitudini del team. +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 -================================== +Model, il cuore dell'applicazione +================================= -Il model contiene tutta la logica di business dell'applicazione. Per la sua organizzazione vale di nuovo la regola - strutturiamo per domini: +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 di ingresso principale +│ ├── 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 la spedizione +└── Shipping/ ← tutto ciò che riguarda le spedizioni \-- -Nel model si incontrano tipicamente questi tipi di classi: +Nel model incontrate di norma questi tipi di classi: -**Facade**: rappresentano il punto di ingresso principale a un dominio specifico nell'applicazione. Agiscono come orchestratori che coordinano la collaborazione tra diversi servizi allo scopo di implementare use-case completi (come "crea ordine" o "elabora pagamento"). Sotto il suo layer di orchestrazione, la facade nasconde i dettagli implementativi al resto dell'applicazione, fornendo così un'interfaccia pulita per lavorare con il dominio dato. +**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 @@ -301,7 +301,7 @@ class OrderFacade } ``` -**Servizi**: si concentrano su un'operazione di business specifica all'interno del dominio. A differenza della facade, che orchestra interi use-case, un servizio implementa una logica di business specifica (come calcoli di prezzi o elaborazione di pagamenti). I servizi sono tipicamente senza stato e possono essere utilizzati sia dalle facade come blocchi di costruzione per operazioni più complesse, sia direttamente da altre parti dell'applicazione per compiti più semplici. +**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 @@ -313,7 +313,7 @@ class PricingService } ``` -**Repository**: assicurano tutta la comunicazione con l'archivio dati, tipicamente un database. Il suo compito è caricare e salvare entità e implementare metodi per la loro ricerca. Il repository isola il resto dell'applicazione dai dettagli implementativi del database e fornisce un'interfaccia orientata agli oggetti per lavorare con i dati. +**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 @@ -328,10 +328,10 @@ class OrderRepository } ``` -**Entità**: oggetti che rappresentano i principali concetti di business nell'applicazione, che hanno una loro identità e cambiano nel tempo. Tipicamente si tratta di classi mappate su tabelle di database tramite ORM (come Nette Database Explorer o Doctrine). Le entità possono contenere regole di business relative ai loro dati e logica di validazione. +**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 +// entità mappata sulla tabella di database 'orders' class Order extends Nette\Database\Table\ActiveRow { public function addItem(Product $product, int $quantity): void @@ -345,17 +345,17 @@ class Order extends Nette\Database\Table\ActiveRow } ``` -**Value object**: oggetti immutabili che rappresentano valori senza una propria identità - ad esempio un importo monetario o un indirizzo e-mail. Due istanze di un value object con gli stessi valori sono considerate identiche. +**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 anche `Infrastructure/`) è la casa della base tecnica dell'applicazione. Il codice infrastrutturale include tipicamente: +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 URL +├── Router/ ← routing e gestione degli URL │ └── RouterFactory.php ├── Security/ ← autenticazione e autorizzazione │ ├── Authenticator.php @@ -363,14 +363,14 @@ La cartella `Core/` (o anche `Infrastructure/`) è la casa della base tecnica de ├── Logging/ ← logging e monitoraggio │ ├── SentryLogger.php │ └── FileLogger.php -├── Cache/ ← layer di caching +├── Cache/ ← strato di caching │ └── FullPageCache.php -└── Integration/ ← integrazione con servizi est. +└── Integration/ ← integrazione con servizi esterni ├── Slack/ └── Stripe/ \-- -Per progetti più piccoli, ovviamente, basta una suddivisione piatta: +Per i progetti più piccoli basta naturalmente una struttura piatta: /--pre Core/ @@ -379,121 +379,121 @@ Per progetti più piccoli, ovviamente, basta una suddivisione piatta: └── QueueMailer.php \-- -Si tratta di codice che: +È il codice che: -- Risolve l'infrastruttura tecnica (routing, logging, caching) -- Integra servizi esterni (Sentry, Elasticsearch, Redis) -- Fornisce servizi di base per l'intera applicazione (mail, database) -- È per lo più indipendente dal dominio specifico - la cache o il logger funzionano allo stesso modo per un eshop o un blog. +- 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. -Hai dubbi se una certa classe appartiene qui o al model? La differenza chiave è che il codice in `Core/`: +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) -- È per lo più possibile trasferirlo a un altro progetto -- Risolve "come funziona" (come inviare una mail), non "cosa fa" (quale mail inviare) +- 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) -Esempio per una migliore comprensione: +Un esempio per capire meglio: -- `App\Core\MailerFactory` - crea istanze della classe per l'invio di e-mail, gestisce le impostazioni SMTP -- `App\Model\OrderMailer` - utilizza `MailerFactory` per inviare e-mail sugli ordini, conosce i loro template e sa quando devono essere inviati +- `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 di comando -================= +Script da riga di comando +========================= -Le applicazioni spesso necessitano di eseguire attività al di fuori delle normali richieste HTTP - che si tratti di elaborazione dati in background, manutenzione o attività periodiche. Per l'esecuzione servono semplici script nella directory `bin/`, la logica implementativa la posizioniamo poi in `app/Tasks/` (eventualmente `app/Commands/`). +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 ← cancellazione di dati vecchi +│ ├── 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à regolari - ├── NewsletterCommand.php ← invio di newsletter +└── Scheduled/ ← attività periodiche + ├── NewsletterCommand.php ← invio delle newsletter └── ReminderCommand.php ← notifiche ai clienti \-- -Cosa appartiene al model e cosa agli script di comando? Ad esempio, la logica per l'invio di una singola e-mail fa parte del model, l'invio massivo di migliaia di e-mail appartiene già a `Tasks/`. +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/`. -Le attività vengono solitamente [eseguite dalla riga di comando |https://blog.nette.org/en/cli-scripts-in-nette-application] o tramite cron. Possono essere eseguite anche tramite richiesta HTTP, ma è necessario pensare alla sicurezza. Il presenter che avvia l'attività deve essere protetto, ad esempio solo per utenti loggati o con un token forte e accesso da indirizzi IP consentiti. Per attività lunghe è necessario aumentare il limite di tempo dello script e utilizzare `session_write_close()`, per non bloccare la sessione. +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 possibili directory +Altre directory possibili ========================= -Oltre alle directory di base menzionate, puoi aggiungere altre cartelle specializzate in base alle esigenze del progetto. Vediamo le più comuni e il loro utilizzo: +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 per API indipendente dal layer di presentazione -├── Database/ ← script di migrazione e seeder per dati di test +├── 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 usi architettura event-driven -├── Mail/ ← template e-mail e logica correlata -└── Utils/ ← classi di utilità +├── 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 utilizzati nei presenter in tutta l'applicazione, è possibile utilizzare la cartella `app/Components` o `app/Controls`: +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 form condivisi +├── Form/ ← componenti di form condivisi │ ├── SignInForm.php │ └── UserForm.php -├── Grid/ ← componenti per elenchi di dati +├── Grid/ ← componenti per gli elenchi di dati │ └── DataGrid.php └── Navigation/ ← elementi di navigazione ├── Breadcrumbs.php └── Menu.php \-- -Qui appartengono i componenti che hanno una logica più complessa. Se vuoi condividere componenti tra più progetti, è consigliabile estrarli in un pacchetto composer separato. +È 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` puoi posizionare la gestione della comunicazione e-mail: +Nella directory `app/Mail` potete collocare la gestione della comunicazione via e-mail: /--pre app/Mail/ -├── templates/ ← template e-mail +├── templates/ ← template delle e-mail │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php \-- -Mappatura dei presenter -======================= +Mapping dei presenter +===================== -La mappatura definisce le regole per derivare il nome della classe dal nome del presenter. Le specifichiamo nella [configurazione|configuration] sotto la chiave `application › mapping`. +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 posizioniamo i presenter nella cartella `app/Presentation` (eventualmente `app/UI`). Dobbiamo comunicare questa convenzione a Nette nel file di configurazione. Basta una riga: +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 la mappatura? Per una migliore comprensione, immaginiamo prima un'applicazione senza moduli. Vogliamo che le classi dei presenter rientrino nel namespace `App\Presentation`, in modo che il presenter `Home` si mappi sulla classe `App\Presentation\HomePresenter`. Cosa che otteniamo con questa configurazione: +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 ``` -La mappatura funziona in modo che il nome del presenter `Home` sostituisca l'asterisco nella maschera `App\Presentation\*Presenter`, ottenendo così il nome della classe risultante `App\Presentation\HomePresenter`. Semplice! +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 però vedi negli esempi in questo e altri capitoli, posizioniamo le classi dei presenter in sottodirectory omonime, ad esempio il presenter `Home` si mappa sulla classe `App\Presentation\Home\HomePresenter`. Otteniamo ciò raddoppiando i due punti (richiede Nette Application 3.2): +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 ``` -Ora passiamo alla mappatura dei presenter nei moduli. Per ogni modulo possiamo definire una mappatura specifica: +Passiamo ora al mapping dei presenter nei moduli. Possiamo definire un mapping specifico per ogni modulo: ```neon application: @@ -503,9 +503,9 @@ application: Api: App\Api\*Presenter ``` -Secondo questa configurazione, il presenter `Front:Home` si mappa sulla classe `App\Presentation\Front\Home\HomePresenter`, mentre il presenter `Api:OAuth` sulla classe `App\Api\OAuthPresenter`. +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 un modo simile di mappatura e probabilmente ci saranno più moduli di questo tipo, è possibile creare una regola generale che li sostituisca. Alla maschera della classe si aggiunge così un nuovo asterisco per il modulo: +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: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -Funziona anche per strutture di directory più profondamente nidificate, come ad esempio il presenter `Admin:User:Edit`, il segmento con l'asterisco si ripete per ogni livello e il risultato è la classe `App\Presentation\Admin\User\Edit\EditPresenter`. +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 un array composto da tre segmenti invece di una stringa. Questa notazione è equivalente alla precedente: +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: diff --git a/application/it/how-it-works.texy b/application/it/how-it-works.texy index 5231df6437..faf548f9db 100644 --- a/application/it/how-it-works.texy +++ b/application/it/how-it-works.texy @@ -3,11 +3,11 @@ Come funzionano le applicazioni?
    -Stai leggendo il documento fondamentale della documentazione di Nette. Imparerai l'intero principio di funzionamento delle applicazioni web. Dalla A alla Z, dal momento della nascita fino all'ultimo respiro dello script PHP. Dopo aver letto, saprai: +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 -- cos'è Bootstrap, Presenter e il container DI -- come appare la struttura delle directory +- cosa sono Bootstrap, il presenter e il container DI +- che aspetto ha la struttura delle directory
    @@ -15,14 +15,14 @@ Stai leggendo il documento fondamentale della documentazione di Nette. Imparerai Struttura delle directory ========================= -Apri l'esempio dello scheletro dell'applicazione web chiamato [WebProject|https://github.com/nette/web-project] e mentre leggi, puoi guardare i file di cui si parla. +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 assomiglia a qualcosa del genere: +La struttura delle directory ha più o meno questo aspetto: /--pre web-project/ -├── app/ ← directory con l'applicazione -│ ├── Core/ ← classi di base necessarie per il funzionamento +├── 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 @@ -40,64 +40,64 @@ La struttura delle directory assomiglia a qualcosa del genere: ├── vendor/ ← librerie installate da Composer │ ├── ... │ └── autoload.php ← autoloading di tutti i pacchetti installati -├── www/ ← directory pubblica o document-root del progetto +├── www/ ← directory pubblica, document root del progetto │ ├── assets/ ← file statici compilati (CSS, JS, immagini, ...) -│ ├── .htaccess ← regole mod_rewrite -│ └── index.php ← file iniziale con cui si avvia l'applicazione +│ ├── .htaccess ← regole di mod_rewrite +│ └── index.php ← file iniziale che avvia l'applicazione └── .htaccess ← vieta l'accesso a tutte le directory tranne www \-- -Puoi modificare la struttura delle directory come preferisci, rinominare o spostare le cartelle, è completamente flessibile. Nette dispone inoltre di un intelligente autodetect e riconosce automaticamente la posizione dell'applicazione, inclusa la sua base URL. +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. -Per applicazioni un po' più grandi, possiamo [dividere le cartelle con presenter e template in sottodirectory |directory-structure#Presenter e template] e le classi in namespace, che chiamiamo moduli. +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 cosiddetta directory pubblica o document-root del progetto. Puoi rinominarla senza dover configurare nient'altro lato applicazione. È solo necessario [configurare l'hosting |nette:troubleshooting#Come modificare o rimuovere la directory www dall URL] in modo che la document-root punti a questa directory. +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. -Puoi anche scaricare direttamente WebProject incluso Nette usando [Composer |best-practices:composer]: +Potete anche scaricare WebProject direttamente, Nette compreso, con [Composer |best-practices:composer]: ```shell composer create-project nette/web-project ``` -Su Linux o macOS, imposta i [permessi di scrittura |nette:troubleshooting#Impostazione dei permessi delle directory] per le directory `log/` e `temp/`. +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 per essere eseguita, non è necessario configurare assolutamente nulla e puoi visualizzarla direttamente nel browser accedendo alla cartella `www/`. +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 nel momento in cui l'utente apre una pagina nel browser. Cioè quando il browser bussa al server con una richiesta HTTP. La richiesta punta a un unico file PHP, che si trova nella directory pubblica `www/`, e questo è `index.php`. Supponiamo che si tratti di una richiesta all'indirizzo `https://example.com/product/123`. Grazie a un'adeguata [configurazione del server |nette:troubleshooting#Come configurare il server per URL leggibili] anche questo URL viene mappato sul file `index.php` e questo viene eseguito. +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 gestirà la richiesta +3) avviare l'applicazione Nette, che gestisce la richiesta -Quale factory? Non produciamo trattori, ma pagine web! Aspetta, si chiarirà subito. +Quale factory? Non stiamo mica producendo trattori, stiamo costruendo siti web! Un attimo di pazienza, verrà spiegato tra poco. -Con "inizializzazione dell'ambiente" intendiamo, ad esempio, che viene attivato [Tracy|tracy:], che è uno strumento fantastico per il logging o la visualizzazione degli errori. Sul server di produzione registra gli errori, su quello di sviluppo li visualizza direttamente. Quindi l'inizializzazione include anche la decisione se il sito web è in esecuzione in modalità produzione o sviluppo. Per questo Nette utilizza un [intelligente autodetect |bootstrapping#Modalità Sviluppo vs Produzione]: se avvii il sito web su localhost, viene eseguito in modalità sviluppo. Non devi quindi configurare nulla e l'applicazione è subito pronta sia per lo sviluppo che per la distribuzione in produzione. Questi passaggi vengono eseguiti e sono descritti in dettaglio nel capitolo sulla [classe Bootstrap|bootstrapping]. +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. La gestione delle richieste HTTP in Nette è affidata alla classe `Nette\Application\Application` (di seguito `Application`), quindi quando diciamo avviare l'applicazione, intendiamo specificamente chiamare il metodo con il nome appropriato `run()` sull'oggetto di questa classe. +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 è un mentore che ti guida a scrivere applicazioni pulite secondo metodologie comprovate. E una di quelle assolutamente più comprovate si chiama **dependency injection**, abbreviata in DI. In questo momento non vogliamo appesantirti con la spiegazione della DI, per questo c'è un [capitolo separato|dependency-injection:introduction], l'importante è la conseguenza che gli oggetti chiave ci verranno solitamente creati da una factory di oggetti, chiamata **container DI** (abbreviato in DIC). Sì, è quella factory di cui si parlava poco fa. E ci produrrà anche l'oggetto `Application`, perciò abbiamo prima bisogno del container. Lo otteniamo tramite la classe `Configurator` e gli facciamo produrre l'oggetto `Application`, chiamiamo su di esso il metodo `run()` e così si avvia l'applicazione Nette. Esattamente questo accade nel file [index.php |bootstrapping#index.php]. +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 unico compito: rispondere alla richiesta HTTP. +La classe `Application` ha un solo compito: rispondere alla richiesta HTTP. -Le applicazioni scritte in Nette si dividono in molti cosiddetti presenter (in altri framework potresti incontrare il termine controller, è la stessa cosa), che sono classi, ognuna delle quali rappresenta una specifica pagina del sito web: ad esempio homepage; prodotto in un e-shop; modulo di login; feed sitemap ecc. Un'applicazione può avere da uno a migliaia di presenter. +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 inizia chiedendo al cosiddetto router di decidere a quale dei presenter passare la richiesta corrente per la gestione. Il router decide di chi è la responsabilità. Guarda l'URL di input `https://example.com/product/123` e in base a come è impostato, decide che questo è il lavoro, ad esempio, per il **presenter** `Product`, dal quale vorrà come **azione** la visualizzazione (`show`) del prodotto con `id: 123`. È buona abitudine scrivere la coppia presenter + azione separata da due punti come `Product:show`. +`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`. -Quindi il router ha trasformato l'URL nella coppia `Presenter:action` + parametri, nel nostro caso `Product:show` + `id: 123`. Come appare un tale router puoi vederlo nel file `app/Core/RouterFactory.php` e lo descriviamo in dettaglio nel capitolo [Routing]. +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 già il nome del presenter e può continuare. Creando l'oggetto della classe `ProductPresenter`, che è il codice del presenter `Product`. Più precisamente, chiede al container DI di creare il presenter, perché la creazione è compito suo. +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 assomigliare a questo: +Il presenter potrebbe avere questo aspetto: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -109,92 +109,92 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function renderShow(int $id): void { - // otteniamo i dati dal model e li passiamo al template + // ottiene i dati dal modello e li passa al template $this->template->product = $this->repository->getProduct($id); } } ``` -La gestione della richiesta viene assunta dal presenter. E il compito è chiaro: esegui l'azione `show` con `id: 123`. Che nel linguaggio dei presenter significa che viene chiamato il metodo `renderShow()` e nel parametro `$id` riceve `123`. +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, cioè avere più metodi `render()`. Ma consigliamo di progettare presenter con una o il minor numero possibile di azioni. +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. -Quindi, è stato chiamato il metodo `renderShow(123)`, il cui codice è un esempio fittizio, ma puoi vedere come vengono passati i dati al template, cioè scrivendo in `$this->template`. +È 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`. -Successivamente, il presenter restituisce una risposta. Questa può essere una pagina HTML, un'immagine, un documento XML, l'invio di un file dal disco, JSON o magari un redirect a un'altra pagina. È importante notare che se non diciamo esplicitamente come deve rispondere (che è il caso di `ProductPresenter`), la risposta sarà il rendering del template con la pagina HTML. Perché? Perché nel 99% dei casi vogliamo renderizzare un template, quindi il presenter considera questo comportamento come predefinito e vuole semplificarci il lavoro. Questo è lo scopo di Nette. +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 specificare quale template renderizzare, ne deduce il percorso da solo. Nel caso dell'azione `show`, prova semplicemente a caricare il template `show.latte` nella directory con la classe `ProductPresenter`. Tenterà anche di trovare il layout nel file `@layout.latte` (maggiori dettagli sulla [ricerca dei template |templates#Ricerca dei template]). +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]). -E successivamente renderizza i template. Con questo, il compito del presenter e dell'intera applicazione è completato e l'opera è finita. Se il template non esistesse, verrebbe restituita una pagina con errore 404. Maggiori informazioni sui presenter si trovano nella pagina [Presenter|presenters]. +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, proviamo a riepilogare l'intero processo con un URL leggermente diverso: +Per sicurezza ricapitoliamo l'intero processo con un URL leggermente diverso: -1) L'URL sarà `https://example.com` -2) avviamo l'applicazione, viene creato il container e viene eseguito `Application::run()` -3) il router decodifica l'URL come coppia `Home:default` -4) viene creato l'oggetto della classe `HomePresenter` -5) viene chiamato il metodo `renderDefault()` (se esiste) -6) viene renderizzato il template ad es. `default.latte` con il layout ad es. `@layout.latte` +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`. -Potresti aver incontrato molti nuovi concetti ora, ma crediamo che abbiano senso. Creare applicazioni in Nette è un'enorme comodità. +Avrete forse incontrato ora molti concetti nuovi, ma crediamo che abbiano senso. Sviluppare applicazioni in Nette è straordinariamente semplice. Template ======== -Dato che abbiamo menzionato i template, in Nette si utilizza il sistema di templating [Latte |latte:]. Questo perché è il sistema di templating più sicuro per PHP, e allo stesso tempo il sistema più intuitivo. Non devi imparare molto di nuovo, ti basta la conoscenza di PHP e alcuni tag. Tutto si trova [nella documentazione |templates]. +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 si [creano link |creating-links] ad altri presenter & azioni in questo modo: +Nel template [create i link |creating-links] verso altri presenter e azioni così: ```latte -dettaglio prodotto +dettaglio del prodotto ``` -Semplicemente, invece dell'URL reale, scrivi la coppia nota `Presenter:action` e specifichi eventuali parametri. Il trucco sta in `n:href`, che dice che questo attributo sarà elaborato da Nette. E genererà: +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 prodotto +dettaglio del prodotto ``` -La generazione dell'URL è gestita dal router menzionato in precedenza. Infatti, i router in Nette sono eccezionali perché possono eseguire non solo trasformazioni da URL a coppia presenter:action, ma anche viceversa, cioè generare un URL dal nome del presenter + azione + parametri. Grazie a ciò, in Nette puoi cambiare completamente le forme degli URL in tutta l'applicazione finita, senza cambiare un singolo carattere nel template o nel presenter. Semplicemente modificando il router. Grazie a ciò funziona anche la cosiddetta canonizzazione, che è un'altra caratteristica unica di Nette, che contribuisce a un migliore SEO (ottimizzazione della reperibilità su Internet) impedendo automaticamente l'esistenza di contenuti duplicati su URL diversi. Molti programmatori lo trovano sorprendente. +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 +Componenti interattivi ====================== -Sui presenter dobbiamo rivelarti ancora una cosa: hanno un sistema di componenti integrato. Qualcosa di simile potrebbe essere familiare ai veterani di Delphi o ASP.NET Web Forms, qualcosa di lontanamente simile è alla base di React o Vue.js. Nel mondo dei framework PHP, è una caratteristica assolutamente unica. +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à riutilizzabili separate che inseriamo nelle pagine (cioè nei presenter). Possono essere [form |forms:in-presenter], [datagrid |https://componette.org/contributte/datagrid/], menu, sondaggi, praticamente qualsiasi cosa che abbia senso usare ripetutamente. Possiamo creare i nostri componenti o usare alcuni dell'[enorme offerta |https://componette.org] di componenti open source. +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 fondamentalmente l'approccio alla creazione di applicazioni. Ti apriranno nuove possibilità di comporre pagine da unità pre-preparate. E inoltre hanno qualcosa in comune con [Hollywood |components#Stile Hollywood]. +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 +Container DI e configurazione ============================= -Il container DI o factory di oggetti è il cuore dell'intera applicazione. +Il container DI, cioè la factory di oggetti, è il cuore dell'intera applicazione. -Non preoccuparti, non è una scatola nera magica, come potrebbe sembrare dalle righe precedenti. In realtà, è una classe PHP piuttosto noiosa, che viene generata da Nette e salvata nella directory della cache. Ha molti metodi chiamati come `createServiceAbcd()` e ognuno di essi sa come creare e restituire un oggetto. Sì, c'è anche il metodo `createServiceApplication()`, che crea `Nette\Application\Application`, di cui avevamo bisogno nel file `index.php` per avviare l'applicazione. E ci sono metodi che creano i singoli presenter. E così via. +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. -Agli oggetti che il container DI crea, per qualche motivo, si dice servizi. +Gli oggetti creati dal container DI si chiamano, per qualche motivo, servizi. -Ciò che è veramente speciale di questa classe è che non la programmi tu, ma il framework. Genera effettivamente il codice PHP e lo salva su disco. Tu dai solo istruzioni su quali oggetti il container deve saper creare e come esattamente. E queste istruzioni sono scritte nei [file di configurazione |bootstrapping#Configurazione del Container DI], per i quali si usa il formato [NEON|neon:format] e quindi hanno anche l'estensione `.neon`. +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 puramente a istruire il container DI. Quindi, se ad esempio specifico nella sezione [session |http:configuration#Sessione] l'opzione `expiration: 14 days`, il container DI, durante la creazione dell'oggetto `Nette\Http\Session` che rappresenta la sessione, chiamerà il suo metodo `setExpiration('14 days')` e così la configurazione diventerà realtà. +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 preparato per te che descrive cosa può essere [configurato |nette:configuring] e come [definire i propri servizi |dependency-injection:services]. +C'è un intero capitolo pronto per voi che descrive cosa si può [configurare |nette:configuring] e come [definire i propri servizi |dependency-injection:services]. -Non appena ti addentrerai un po' nella creazione di servizi, incontrerai la parola [autowiring |dependency-injection:autowiring]. Questa è una chicca che ti semplificherà la vita in modo incredibile. Sa passare automaticamente gli oggetti dove ne hai bisogno (ad esempio nei costruttori delle tue classi), senza che tu debba fare nulla. Scoprirai che il container DI in Nette è un piccolo miracolo. +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. -Dove andare dopo? -================= +E adesso? +========= -Abbiamo esaminato i principi di base delle applicazioni in Nette. Finora molto superficialmente, ma presto approfondirai e col tempo creerai fantastiche applicazioni web. Dove continuare? Hai già provato il tutorial [Scriviamo la prima applicazione|quickstart:]? +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 dispone di un intero arsenale di [classi utili|utils:], un [layer di database|database:], ecc. Prova a sfogliare la documentazione solo per curiosità. O il [blog|https://blog.nette.org]. Scoprirai molte cose interessanti. +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 ti porti molta gioia 💙 +Che il framework vi porti molta gioia 💙 diff --git a/application/it/multiplier.texy b/application/it/multiplier.texy index 4bdb5b42a0..f4c003c96d 100644 --- a/application/it/multiplier.texy +++ b/application/it/multiplier.texy @@ -2,23 +2,23 @@ Multiplier: componenti dinamici ******************************* .[perex] -Strumento per la creazione dinamica di componenti interattivi +Uno strumento per creare dinamicamente componenti interattivi. -Partiamo da un esempio tipico: abbiamo un elenco di prodotti in un e-shop, e per ognuno vogliamo visualizzare un form per aggiungere il prodotto al carrello. Una delle possibili varianti è racchiudere l'intero elenco in un unico form. Tuttavia, un modo molto più comodo ci viene offerto da [api:Nette\Application\UI\Multiplier]. +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 consente di definire comodamente una piccola factory per più componenti. Funziona sul principio dei componenti nidificati - ogni componente che eredita da [api:Nette\ComponentModel\Container] può contenere altri componenti. +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 il capitolo sul [modello a componenti |components#Componenti in profondità] nella documentazione o la [presentazione di Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. +Vedi nella documentazione il capitolo sul [modello a componenti |components#I componenti in profondità]. -L'essenza di Multiplier è che agisce come genitore, che può creare dinamicamente i propri figli tramite un callback passato nel costruttore. Vedi l'esempio: +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('count', 'Quantità:') + $form->addInteger('amount', 'Quantità:') ->setRequired(); $form->addSubmit('send', 'Aggiungi al carrello'); return $form; @@ -26,7 +26,7 @@ protected function createComponentShopForm(): Multiplier } ``` -Ora possiamo semplicemente far renderizzare un form per ogni prodotto nel template - e ognuno sarà effettivamente un componente unico. +Ora, nel template, possiamo semplicemente disegnare il form per ogni prodotto, e ognuno sarà davvero un componente a sé. ```latte {foreach $items as $item} @@ -37,23 +37,23 @@ Ora possiamo semplicemente far renderizzare un form per ogni prodotto nel templa {/foreach} ``` -L'argomento passato nel tag `{control}` è nel formato che dice: +L'argomento passato nel tag `{control}` segue un formato che significa: -1. ottieni il componente `shopForm` -2. e da esso ottieni il figlio `$item->id` +1. Ottieni il componente `shopForm`. +2. Da esso ottieni il figlio chiamato `$item->id`. -Alla prima chiamata del punto **1.** `shopForm` non esiste ancora, quindi viene chiamata la sua factory `createComponentShopForm`. Sul componente ottenuto (istanza di Multiplier) viene poi chiamata la factory del form specifico - che è la funzione anonima che abbiamo passato a Multiplier nel costruttore. +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. -Nella successiva iterazione del foreach, il metodo `createComponentShopForm` non verrà più chiamato (il componente esiste), ma poiché stiamo cercando un suo figlio diverso (`$item->id` sarà diverso in ogni iterazione), la funzione anonima verrà chiamata di nuovo e ci restituirà un nuovo form. +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. -L'unica cosa che resta da fare è assicurarsi che il form aggiunga al carrello effettivamente il prodotto che deve - attualmente il form è completamente identico per ogni prodotto. Ci aiuta una proprietà di Multiplier (e in generale di ogni factory di componente in Nette Framework), ovvero che ogni factory riceve come primo argomento il nome del componente creato. Nel nostro caso sarà `$item->id`, che è esattamente l'informazione di cui abbiamo bisogno. Basta quindi modificare leggermente la creazione del 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 ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Quantità:') + $form->addInteger('amount', 'Quantità:') ->setRequired(); $form->addHidden('itemId', $itemId); $form->addSubmit('send', 'Aggiungi al carrello'); diff --git a/application/it/presenters.texy b/application/it/presenters.texy index 21c74bdd7f..072276889b 100644 --- a/application/it/presenters.texy +++ b/application/it/presenters.texy @@ -3,37 +3,37 @@ Presenter
    -Impareremo come scrivere presenter e template in Nette. Dopo aver letto, saprai: +Esamineremo come si scrivono i presenter e i template in Nette. Dopo la lettura capirete: -- come funziona un presenter +- come funzionano i presenter - cosa sono i parametri persistenti -- come vengono renderizzati i template +- come vengono disegnati i template
    -[Sappiamo già |how-it-works#Nette Application] che un presenter è una classe che rappresenta una specifica pagina di un'applicazione web, ad es. la homepage; un prodotto in un e-shop; un modulo di login; un feed sitemap, ecc. Un'applicazione può avere da uno a migliaia di presenter. In altri framework vengono anche chiamati controller. +[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 solito, con il termine presenter si intende un discendente della classe [api:Nette\Application\UI\Presenter], che è adatto per generare interfacce web e al quale ci dedicheremo nel resto di questo capitolo. In senso generale, un presenter è qualsiasi oggetto che implementa l'interfaccia [api:Nette\Application\IPresenter]. +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 la richiesta e restituire una risposta (che può essere una pagina HTML, un'immagine, un redirect, ecc.). +Il compito del presenter è gestire una richiesta e restituire una risposta (che può essere una pagina HTML, un'immagine, un redirect ecc.). -Quindi, all'inizio, gli viene passata la richiesta. Non è direttamente la richiesta HTTP, ma l'oggetto [api:Nette\Application\Request], nel quale la richiesta HTTP è stata trasformata con l'aiuto del router. Di solito non entriamo in contatto con questo oggetto, poiché il presenter delega intelligentemente l'elaborazione della richiesta ad altri metodi, che ora vedremo. +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* .<> +[* lifecycle.svg *] *** Ciclo di vita del presenter .<> -L'immagine rappresenta l'elenco dei metodi che vengono chiamati in sequenza dall'alto verso il basso, se esistono. Nessuno di essi deve esistere, possiamo avere un presenter completamente vuoto senza un singolo metodo e costruirci sopra un semplice sito web statico. +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 propriamente al ciclo di vita del presenter, perché viene chiamato al momento della creazione dell'oggetto. Ma lo menzioniamo per la sua importanza. Il costruttore (insieme al [metodo inject|best-practices:inject-method-attribute]) serve a passare le dipendenze. +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 dovrebbe occuparsi della logica di business dell'applicazione, scrivere e leggere dal database, eseguire calcoli, ecc. Per questo ci sono classi del layer che chiamiamo model. Ad esempio, la classe `ArticleRepository` può essere responsabile del caricamento e del salvataggio degli articoli. Affinché il presenter possa lavorarci, se la fa [passare tramite dependency injection |dependency-injection:passing-dependencies]: +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 @@ -50,44 +50,47 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -Subito dopo aver ricevuto la richiesta, viene chiamato il metodo `startup()`. Puoi usarlo per inizializzare le proprietà, verificare i permessi utente, ecc. È richiesto che il metodo chiami sempre il genitore `parent::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()} +`action(args...)` .{toc: action()} -------------------------------------------------- -Analogo al metodo `render()`. Mentre `render()` è destinato a preparare i dati per un template specifico che verrà successivamente renderizzato, in `action()` la richiesta viene elaborata senza relazione con il rendering del template. Ad esempio, vengono elaborati i dati, l'utente viene loggato o sloggato, e così via, e poi [reindirizza altrove |#Redirect]. +È 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 notare che `action()` viene chiamato prima di `render()`, quindi al suo interno possiamo eventualmente cambiare il corso successivo degli eventi, cioè cambiare il template che verrà renderizzato, e anche il metodo `render()` che verrà chiamato. E questo tramite `setView('altraView')`. +È 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')`. -Al metodo vengono passati i parametri dalla richiesta. È possibile e consigliato specificare i tipi per i parametri, ad es. `actionShow(int $id, ?string $slug = null)` - se il parametro `id` manca o se non è un intero, il presenter restituirà un [errore 404 |#Errore 404 e simili] e terminerà l'attività. +.{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()} --------------------------------------------------- -Il metodo elabora i cosiddetti segnali, che conosceremo nel capitolo dedicato ai [componenti |components#Segnale]. È infatti destinato principalmente ai componenti e all'elaborazione delle richieste AJAX. +`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 dalla richiesta, come nel caso di `action()`, incluso il controllo del tipo. +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()`. Viene utilizzato per la configurazione comune del template, il passaggio di variabili per il layout e simili. +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()} ----------------------------------------------- +`render(args...)` .{toc: render()} +----------------------------------------------- -Il luogo dove prepariamo il template per il successivo rendering, gli passiamo i dati, ecc. +È qui che prepariamo il template per il successivo rendering, gli passiamo i dati e così via. -Al metodo vengono passati i parametri dalla richiesta, come nel caso di `action()`, incluso il controllo del tipo. +Al metodo vengono passati i parametri della richiesta, come per `action()`, compreso il controllo dei tipi. ```php public function renderShow(int $id): void { - // otteniamo i dati dal model e li passiamo al template + // ottiene i dati dal modello e li passa al template $this->template->article = $this->articles->getById($id); } ``` @@ -96,7 +99,7 @@ public function renderShow(int $id): void `afterRender()` --------------- -Il metodo `afterRender`, come suggerisce nuovamente il nome, viene chiamato dopo ogni metodo `render()`. Viene utilizzato piuttosto eccezionalmente. +Il metodo `afterRender`, come suggerisce di nuovo il nome, viene chiamato dopo ogni metodo `render()`. Si usa piuttosto di rado. `shutdown()` @@ -105,44 +108,66 @@ Il metodo `afterRender`, come suggerisce nuovamente il nome, viene chiamato dopo Viene chiamato alla fine del ciclo di vita del presenter. -**Un buon consiglio prima di andare avanti**. Come si vede, un presenter può gestire più azioni/view, cioè avere più metodi `render()`. Ma consigliamo di progettare presenter con una o il minor numero possibile di azioni. +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. -Invio della risposta +Inviare una risposta ==================== -La risposta del presenter è di solito il [rendering di un template con una pagina HTML|templates], ma può anche essere l'invio di un file, JSON o magari un redirect a un'altra pagina. +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 durante il ciclo di vita, possiamo inviare una risposta con uno dei seguenti metodi e allo stesso tempo terminare il presenter: +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()` [reindirizzano |#Redirect] -- `error()` termina il presenter [a causa di un errore |#Errore 404 e simili] -- `sendJson($data)` termina il presenter e [invia dati |#Invio di JSON] in formato JSON -- `sendTemplate()` termina il presenter e immediatamente [renderizza il template |templates] +- `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 risposta +- `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 chiami nessuno di questi metodi, il presenter procederà automaticamente al rendering del template. Perché? Perché nel 99% dei casi vogliamo renderizzare un template, quindi il presenter considera questo comportamento come predefinito e vuole semplificarci il lavoro. +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. -Creazione di link -================= +Creare i link +============= -Il presenter dispone del metodo `link()`, tramite il quale è possibile creare link URL ad altri presenter. Il primo parametro è il presenter & azione di destinazione, seguono gli argomenti passati, che possono essere specificati come array: +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' => 'cs']); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -Nel template, i link ad altri presenter & azioni vengono creati in questo modo: +Nel template i link verso altri presenter e azioni si creano così: ```latte -dettaglio prodotto +dettaglio del prodotto ``` -Semplicemente, invece dell'URL reale, scrivi la coppia nota `Presenter:action` e specifichi eventuali parametri. Il trucco sta in `n:href`, che dice che questo attributo sarà elaborato da Latte e genererà l'URL reale. In Nette, quindi, non devi affatto pensare agli URL, ma solo ai presenter e alle azioni. +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]. @@ -150,50 +175,50 @@ Maggiori informazioni si trovano nel capitolo [Creazione di link URL|creating-li Redirect ======== -Per passare a un altro presenter si usano i metodi `redirect()` e `forward()`, che hanno una sintassi molto simile al metodo [link() |#Creazione di link]. +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 immediatamente al nuovo presenter senza redirect HTTP: +Il metodo `forward()` passa subito al nuovo presenter, senza un redirect HTTP: ```php $this->forward('Product:show'); ``` -Esempio del cosiddetto redirect temporaneo con codice HTTP 302 (o 303, se il metodo della richiesta corrente è POST): +Esempio di redirect temporaneo con codice HTTP 302 (o 303 se il metodo della richiesta corrente è POST): ```php $this->redirect('Product:show', $id); ``` -Il redirect permanente con codice HTTP 301 si ottiene così: +Per ottenere un redirect permanente con codice HTTP 301, usate questo: ```php $this->redirectPermanent('Product:show', $id); ``` -È possibile reindirizzare a un altro URL al di fuori dell'applicazione con il metodo `redirectUrl()`. Come secondo parametro è possibile specificare il codice HTTP, il predefinito è 302 (o 303, se il metodo della richiesta corrente è POST): +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 lanciando la cosiddetta eccezione di terminazione silenziosa `Nette\Application\AbortException`. +Il redirect termina immediatamente l'attività del presenter sollevando la cosiddetta eccezione di terminazione silenziosa, `Nette\Application\AbortException`. -Prima del redirect è possibile inviare un [messaggio flash |#Messaggi flash], cioè messaggi che verranno visualizzati nel template dopo il redirect. +Prima del redirect è possibile inviare [messaggi flash |#Messaggi flash], cioè messaggi che verranno mostrati nel template dopo il redirect. Messaggi flash ============== -Si tratta di messaggi che solitamente informano sul risultato di qualche operazione. Una caratteristica importante dei messaggi flash è che sono disponibili nel template anche dopo un redirect. Anche dopo essere stati visualizzati, rimangono attivi per altri 30 secondi – ad esempio, nel caso in cui l'utente aggiorni la pagina a causa di un errore di trasmissione - il messaggio non scomparirà immediatamente. +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 occuperà di passarlo al template. Il primo parametro è il testo del messaggio e il secondo parametro opzionale è il suo tipo (error, warning, info, ecc.). Il metodo `flashMessage()` restituisce un'istanza del messaggio flash, alla quale è possibile aggiungere ulteriori informazioni. +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 reindirizziamo +$this->redirect(/* ... */); // e reindirizza ``` -Nel template, questi messaggi sono disponibili nella variabile `$flashes` come oggetti `stdClass`, che contengono le proprietà `message` (testo del messaggio), `type` (tipo del messaggio) e possono contenere le informazioni utente già menzionate. Li renderizziamo ad esempio così: +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} @@ -205,7 +230,7 @@ Nel template, questi messaggi sono disponibili nella variabile `$flashes` come o Errore 404 e simili =================== -Se non è possibile soddisfare la richiesta, ad esempio perché l'articolo che vogliamo visualizzare non esiste nel database, lanciamo un errore 404 con il metodo `error(?string $message = null, int $httpCode = 404)`. +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 @@ -218,13 +243,13 @@ public function renderShow(int $id): void } ``` -Il codice HTTP dell'errore può essere passato come secondo parametro, il predefinito è 404. Il metodo funziona lanciando l'eccezione `Nette\Application\BadRequestException`, dopodiché `Application` passa il controllo all'error-presenter. Che è un presenter il cui compito è visualizzare una pagina che informa sull'errore verificatosi. L'impostazione dell'error-presenter viene eseguita nella [configurazione application|configuration]. +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]. -Invio di JSON -============= +Inviare JSON +============ -Esempio di un metodo action che invia dati in formato JSON e termina il presenter: +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 @@ -238,9 +263,9 @@ public function actionData(): void Parametri della richiesta .{data-version:3.1.14} ================================================ -Il presenter e anche ogni componente ottengono i propri parametri dalla richiesta HTTP. Il loro valore si ottiene con il metodo `getParameter($name)` o `getParameters()`. I valori sono stringhe o array di stringhe, si tratta fondamentalmente di dati grezzi ottenuti direttamente dall'URL. +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 rendere accessibili i parametri tramite property. Basta contrassegnarli con l'attributo `#[Parameter]`: +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 @@ -252,9 +277,9 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Per la property, consigliamo di specificare anche il tipo di dati (es. `string`) e Nette convertirà automaticamente il valore in base ad esso. I valori dei parametri possono anche essere [validati |#Validazione dei parametri]. +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]. -Durante la creazione di un link, è possibile impostare direttamente il valore dei parametri: +Quando create un link potete impostare direttamente il valore del parametro: ```latte clicca @@ -264,11 +289,11 @@ Durante la creazione di un link, è possibile impostare direttamente il valore d Parametri persistenti ===================== -I parametri persistenti servono a mantenere lo stato tra richieste diverse. Il loro valore rimane lo stesso anche dopo aver cliccato su un link. A differenza dei dati nella sessione, vengono trasferiti nell'URL. E questo avviene in modo completamente automatico, non è quindi necessario specificarli esplicitamente in `link()` o `n:href`. +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`. -Esempio di utilizzo? Hai un'applicazione multilingue. La lingua corrente è un parametro che deve essere costantemente parte dell'URL. Ma sarebbe incredibilmente noioso specificarlo in ogni link. Quindi lo rendi un parametro persistente `lang` e verrà trasferito da solo. Fantastico! +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! -La creazione di un parametro persistente in Nette è estremamente semplice. Basta creare una property pubblica e contrassegnarla con un attributo: (in precedenza si usava `/** @persistent */`) +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 @@ -280,14 +305,14 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Se `$this->lang` avrà il valore, ad esempio, `'en'`, anche i link creati tramite `link()` o `n:href` conterranno il parametro `lang=en`. E dopo aver cliccato sul link, sarà di nuovo `$this->lang = 'en'`. +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 property, consigliamo di specificare anche il tipo di dati (es. `string`) e puoi anche specificare un valore predefinito. I valori dei parametri possono essere [validati |#Validazione dei parametri]. +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 standardmente trasferiti tra tutte le azioni del presenter dato. Affinché vengano trasferiti anche tra più presenter, è necessario definirli o: +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 da cui ereditano i presenter -- in un trait che i presenter utilizzeranno: +- in un antenato comune dal quale i presenter ereditano +- oppure in un trait che i presenter usano: ```php trait LanguageAware @@ -302,42 +327,62 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Durante la creazione di un link, è possibile modificare il valore del parametro persistente: +Quando si crea un link, il valore di un parametro persistente si può cambiare: ```latte dettaglio in ceco ``` -Oppure può essere *resettato*, cioè rimosso dall'URL. Assumerà quindi il suo valore predefinito: +In alternativa lo si può *azzerare*, cioè rimuovere dall'URL. Assumerà allora il proprio valore predefinito: ```latte clicca ``` -Componenti Interattivi +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 separate che inseriamo nei presenter. Possono essere [form |forms:in-presenter], datagrid, menu, praticamente qualsiasi cosa che abbia senso usare ripetutamente. +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 vengono inseriti i componenti nel presenter e successivamente utilizzati? Lo imparerai nel capitolo [Componenti |components]. Scoprirai persino cosa hanno in comune con Hollywood. +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 posso ottenere i componenti? Sulla pagina [Componette |https://componette.org/search/component] troverai componenti open-source e anche numerosi altri add-on per Nette, che sono stati inseriti qui da volontari della comunità attorno al framework. +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 in profondità -===================== +Andiamo più a fondo +=================== .[tip] -Con quello che abbiamo mostrato finora in questo capitolo, probabilmente ti basterà. Le righe seguenti sono destinate a coloro che sono interessati ai presenter in profondità e vogliono sapere assolutamente tutto. +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] e dei [#parametri persistenti] ricevuti dall'URL vengono scritti nelle properties dal metodo `loadState()`. Questo controlla anche se il tipo di dati specificato nella property corrisponde, altrimenti risponde con un errore 404 e la pagina non viene visualizzata. +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 fidarti mai ciecamente dei parametri, perché possono essere facilmente sovrascritti dall'utente nell'URL. In questo modo, ad esempio, verifichiamo se la lingua `$this->lang` è tra quelle supportate. Un modo appropriato è sovrascrivere il metodo `loadState()` menzionato: +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 @@ -348,7 +393,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function loadState(array $params): void { parent::loadState($params); // qui viene impostato $this->lang - // segue il controllo del valore personalizzato: + // segue il controllo personalizzato del valore: if (!in_array($this->lang, ['en', 'cs'])) { $this->error(); } @@ -357,106 +402,148 @@ class ProductPresenter extends Nette\Application\UI\Presenter ``` -Salvataggio e ripristino della richiesta ----------------------------------------- +Salvare e ripristinare la richiesta +----------------------------------- -La richiesta che il presenter gestisce è un oggetto [api:Nette\Application\Request] e viene restituita dal metodo del presenter `getRequest()`. +La richiesta gestita dal presenter è un oggetto [api:Nette\Application\Request], restituito dal metodo `getRequest()` del presenter. -La richiesta corrente può essere salvata nella sessione o, al contrario, ripristinata da essa e fatta eseguire nuovamente dal presenter. Questo è utile, ad esempio, nella situazione in cui l'utente sta compilando un modulo e la sua sessione scade. Per non perdere i dati, prima del redirect alla pagina di login, salviamo la richiesta corrente nella sessione tramite `$reqId = $this->storeRequest()`, che restituisce il suo identificatore sotto forma di una breve stringa e lo passiamo come parametro al presenter di login. +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 e vi inoltra. Il metodo verifica nel frattempo che la richiesta sia stata creata dallo stesso utente che si è appena loggato. Se si loggasse un altro utente o la chiave non fosse valida, non fa nulla e il programma continua. +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. -Dai un'occhiata alla guida [Come tornare alla pagina precedente |best-practices:restore-request]. +Vedi la guida [Come tornare a una pagina precedente |best-practices:restore-request]. Canonizzazione -------------- -I presenter hanno una caratteristica davvero fantastica che contribuisce a un migliore SEO (ottimizzazione della reperibilità su Internet). Impediscono automaticamente l'esistenza di contenuti duplicati su URL diversi. Se a una determinata destinazione portano più indirizzi URL, ad es. `/index` e `/index?page=1`, il framework ne determina uno come primario (canonico) e reindirizza gli altri ad esso tramite il codice HTTP 301. Grazie a ciò, i motori di ricerca non indicizzano le pagine due volte e non diluiscono il loro page rank. +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 solito quindi la prima route corrispondente nella collezione. +Questo processo si chiama canonizzazione. L'URL canonico è quello generato dal [router|routing], di norma la prima route corrispondente della collezione. -La canonizzazione è attivata per impostazione predefinita e può essere disattivata tramite `$this->autoCanonicalize = false`. +La canonizzazione è attiva per impostazione predefinita e si può disattivare con `$this->autoCanonicalize = false`. -Il redirect non avviene durante una richiesta AJAX o POST, perché si verificherebbe una perdita di dati o non avrebbe valore aggiunto dal punto di vista SEO. +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. -La canonizzazione può essere invocata anche manualmente tramite il metodo `canonicalize()`, al quale, in modo simile al metodo `link()`, vengono passati il presenter, l'azione e i parametri. Crea un link e lo confronta con l'URL corrente. Se differiscono, reindirizza al link generato. +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 differisce da $realSlug + // reindirizza se $slug è diverso da $realSlug $this->canonicalize('Product:show', [$id, $realSlug]); } ``` - -Eventi ------- - -Oltre ai metodi `startup()`, `beforeRender()` e `shutdown()`, che vengono chiamati come parte del ciclo di vita del presenter, è possibile definire altre funzioni che devono essere chiamate automaticamente. Il presenter definisce i cosiddetti [eventi |nette:glossary#Eventi], i cui handler aggiungi agli array `$onStartup`, `$onRender` e `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Gli handler nell'array `$onStartup` vengono chiamati poco prima del metodo `startup()`, poi `$onRender` tra `beforeRender()` e `render()` e infine `$onShutdown` poco prima di `shutdown()`. +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 numerose risposte pronte: +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 un callback -- [api:Nette\Application\Responses\FileResponse] - invia un file +- [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 vengono inviate con il metodo `sendResponse()`: +Le risposte si inviano con il metodo `sendResponse()`: ```php use Nette\Application\Responses; -// Testo semplice -$this->sendResponse(new Responses\TextResponse('Ciao Nette!')); +// testo semplice +$this->sendResponse(new Responses\TextResponse('Hello Nette!')); -// Invia un file +// invia un file $this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); -// La risposta sarà un callback +// invia una callback $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Ciao

    '; + 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'; +} +``` + -Restrizione dell'accesso tramite `#[Requires]` .{data-version:3.2.2} --------------------------------------------------------------------- +Limitare l'accesso con `#[Requires]` .{data-version:3.2.3} +---------------------------------------------------------- -L'attributo `#[Requires]` offre opzioni avanzate per limitare l'accesso ai presenter e ai loro metodi. Può essere utilizzato per specificare metodi HTTP, richiedere una richiesta AJAX, limitare alla stessa origine (same origin) e l'accesso solo tramite forward. L'attributo può essere applicato sia alle classi dei presenter che ai singoli metodi `action()`, `render()`, `handle()` e `createComponent()`. +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()`. -È possibile specificare queste restrizioni: +Potete indicare queste restrizioni: - sui metodi HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- richiesta di una richiesta AJAX: `#[Requires(ajax: true)]` +- richiedere una richiesta AJAX: `#[Requires(ajax: true)]` - accesso solo dalla stessa origine: `#[Requires(sameOrigin: true)]` -- accesso solo tramite forward: `#[Requires(forward: true)]` -- restrizione ad azioni specifiche: `#[Requires(actions: 'default')]` +- 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]. @@ -464,9 +551,9 @@ I dettagli si trovano nella guida [Come usare l'attributo Requires |best-practic Controllo del metodo HTTP ------------------------- -I presenter in Nette verificano automaticamente il metodo HTTP di ogni richiesta in arrivo. Il motivo di questo controllo è principalmente la sicurezza. Standardmente sono consentiti i metodi `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. +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 si desidera consentire inoltre, ad esempio, il metodo `OPTIONS`, utilizzare l'attributo `#[Requires]` (da Nette Application v3.2): +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'])] @@ -475,26 +562,34 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Nella versione 3.1, la verifica viene eseguita in `checkHttpMethod()`, che verifica se il metodo specificato nella richiesta è contenuto nell'array `$presenter->allowedMethods`. L'aggiunta del metodo si fa così: +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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -È importante sottolineare che se si consente il metodo `OPTIONS`, è necessario successivamente gestirlo adeguatamente all'interno del proprio presenter. Il metodo è spesso utilizzato come cosiddetta richiesta preflight, che il browser invia automaticamente prima della richiesta effettiva, quando è necessario verificare se la richiesta è consentita dal punto di vista della politica CORS (Cross-Origin Resource Sharing). Se si consente il metodo, ma non si implementa la risposta corretta, ciò può portare a incoerenze e potenziali problemi di sicurezza. +È 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. -Ulteriori letture -================= +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] -- [Composizione di presenter da trait |best-practices:presenter-traits] -- [Passaggio di impostazioni ai presenter |best-practices:passing-settings-to-presenters] -- [Come tornare alla pagina precedente |best-practices:restore-request] +- [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 index c308d57f62..88b63e9400 100644 --- a/application/it/routing.texy +++ b/application/it/routing.texy @@ -3,26 +3,26 @@ Routing
    -Il Router si occupa di tutto ciò che riguarda gli indirizzi URL, così non dovrai più pensarci tu. Vedremo: +Il router si occupa di tutto ciò che riguarda gli indirizzi URL, così non dovete pensarci voi. Vi mostreremo: -- come impostare il router affinché gli URL siano come desiderato -- parleremo di SEO e redirect +- come configurare il router perché gli URL appaiano come volete +- parleremo di SEO e di redirect - e mostreremo come scrivere un router personalizzato
    -URL più umani (o anche cool o pretty URL) sono più usabili, memorizzabili e contribuiscono positivamente al SEO. Nette ci pensa e va incontro pienamente agli sviluppatori. Puoi progettare per la tua applicazione esattamente la struttura degli indirizzi URL che desideri. Puoi progettarla persino quando l'applicazione è già finita, perché si può fare senza interventi nel codice o nei template. Si definisce infatti in modo elegante in un [unico posto |#Integrazione nell applicazione], nel router, e non è quindi disseminata sotto forma di annotazioni in tutti i presenter. +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 nella richiesta HTTP, sia creare i link. Svolge quindi un ruolo fondamentale in [Nette Application |how-it-works#Nette Application], perché decide quale presenter e azione eseguirà la richiesta corrente, ma viene anche utilizzato per la [generazione di URL |creating-links] nel template, ecc. +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. -Tuttavia, il router non è limitato solo a questo utilizzo, puoi usarlo in applicazioni dove i presenter non vengono affatto utilizzati, per API REST, ecc. Maggiori informazioni nella sezione [#Utilizzo indipendente]. +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 per definire la forma degli indirizzi URL nell'applicazione è offerto dalla classe [api:Nette\Application\Routers\RouteList]. La definizione è costituita da un elenco delle cosiddette route, cioè maschere di indirizzi URL e presenter e azioni associati ad esse tramite una semplice API. Non è necessario dare un nome alle 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; @@ -31,16 +31,16 @@ $router->addRoute('article/', 'Article:view'); // ... ``` -L'esempio dice che se apriamo `https://domain.com/rss.xml` nel browser, verrà visualizzato il presenter `Feed` con l'azione `rss`, se `https://domain.com/article/12`, verrà visualizzato il presenter `Article` con l'azione `view`, ecc. In caso di mancata corrispondenza di una route adatta, Nette Application reagisce lanciando un'eccezione [BadRequestException |api:Nette\Application\BadRequestException], che viene mostrata all'utente come una pagina di errore 404 Not Found. +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 cruciale**, perché vengono valutate sequenzialmente dall'alto verso il basso. Vale la regola che dichiariamo le route **dalle specifiche alle generali**: +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 e interpreta questa stringa come +// SBAGLIATO: 'rss.xml' viene catturato dalla prima route, che intende questa stringa come $router->addRoute('', 'Article:view'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -49,10 +49,10 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('', 'Article:view'); ``` -Le route vengono valutate dall'alto verso il basso anche durante la generazione dei link: +Anche nella generazione dei link le route vengono valutate dall'alto verso il basso: ```php -// SBAGLIATO: il link a 'Feed:rss' genera come 'admin/feed/rss' +// SBAGLIATO: il link a 'Feed:rss' viene generato come 'admin/feed/rss' $router->addRoute('admin//', 'Admin:default'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -61,57 +61,57 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('admin//', 'Admin:default'); ``` -Non ti nasconderemo che la corretta composizione delle route richiede una certa abilità. Prima di padroneggiarla, ti sarà utile il [pannello di routing |#Debug del router]. +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 dalla directory principale del sito web. La maschera più semplice è un URL statico: +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**. Questi sono indicati tra parentesi angolari (es. ``) e vengono passati al presenter di destinazione, ad esempio al metodo `renderShow(int $year)` o al parametro persistente `$year`: +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 dice che se apriamo `https://example.com/chronicle/2020` nel browser, verrà visualizzato il presenter `History` con l'azione `show` e il parametro `year: 2020`. +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 specificare un valore predefinito per i parametri direttamente nella maschera, rendendoli così opzionali: +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 visualizzerà nuovamente `History:show` con il parametro `year: 2020`. +La route accetterà ora anche l'URL `https://example.com/chronicle/`, che mostrerà di nuovo `History:show` con il parametro `year: 2020`. -Un parametro può ovviamente essere anche il nome del presenter e dell'azione. Ad esempio così: +Naturalmente anche i nomi del presenter e dell'azione possono essere parametri. Per esempio: ```php $router->addRoute('/', 'Home:default'); ``` -La route specificata accetta ad es. URL nella forma `/article/edit` o anche `/catalog/list` e li interpreta come presenter e azioni `Article:edit` e `Catalog:list`. +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` e sono quindi anche opzionali. Quindi la route accetta anche URL nella forma `/article` e la interpreta come `Article:default`. O viceversa, un link a `Product:default` genererà il percorso `/product`, un link al predefinito `Home:default` il percorso `/`. +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 dalla directory principale del sito web, ma anche il percorso assoluto, se inizia con uno slash, o persino l'intero URL assoluto, se inizia con due slash: +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 -// relativamente alla document root +// relativo al document root $router->addRoute('/', /* ... */); // percorso assoluto (relativo al dominio) $router->addRoute('//', /* ... */); -// URL assoluto incluso il dominio (relativo allo schema) +// URL assoluto, dominio compreso (relativo allo schema) $router->addRoute('//.example.com//', /* ... */); -// URL assoluto incluso lo schema +// URL assoluto, schema compreso $router->addRoute('https://.example.com//', /* ... */); ``` @@ -119,42 +119,42 @@ $router->addRoute('https://.example.com//', /* ... */); Espressioni di validazione -------------------------- -Per ogni parametro è possibile stabilire una condizione di validazione tramite un'[espressione regolare|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Ad esempio, per il parametro `id` specifichiamo che può assumere solo cifre tramite l'espressione regolare `\d+`: +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 per tutti i parametri è `[^/]+`, cioè tutto tranne lo slash. Se un parametro deve accettare anche gli slash, specifichiamo l'espressione `.+`: +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, path sarà 'a/b/c' +// accetta https://example.com/a/b/c, il percorso sarà 'a/b/c' $router->addRoute('', /* ... */); ``` -Sequenze opzionali ------------------- +Sequenze facoltative +-------------------- -Nella maschera è possibile contrassegnare parti opzionali tramite parentesi quadre. Qualsiasi parte della maschera può essere opzionale, possono esserci anche parametri al suo interno: +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 percorsi: -// /cs/download => lang => cs, name => download +// accetta i percorsi: +// /en/download => lang => en, name => download // /download => lang => null, name => download ``` -Quando un parametro fa parte di una sequenza opzionale, diventa ovviamente anche opzionale. Se non ha un valore predefinito specificato, sarà null. +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 opzionali possono essere anche nel dominio: +Le parti facoltative possono trovarsi anche nel dominio: ```php $router->addRoute('//[.]example.com//', /* ... */); ``` -Le sequenze possono essere nidificate e combinate liberamente: +Le sequenze si possono annidare e combinare a piacere: ```php $router->addRoute( @@ -162,14 +162,14 @@ $router->addRoute( 'Home:default', ); -// Accetta percorsi: -// /cs/hello +// accetta i percorsi: +// /en/hello // /en-us/hello // /hello // /hello/page-12 ``` -Durante la generazione dell'URL, si cerca la variante più corta, quindi tutto ciò che può essere omesso viene omesso. Per questo, ad esempio, la route `index[.html]` genera il percorso `/index`. È possibile invertire il comportamento specificando un punto esclamativo dopo la parentesi quadra sinistra: +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 @@ -179,7 +179,7 @@ $router->addRoute('[.html]', /* ... */); $router->addRoute('[!.html]', /* ... */); ``` -I parametri opzionali (cioè i parametri con un valore predefinito) senza parentesi quadre si comportano essenzialmente come se fossero racchiusi tra parentesi nel modo seguente: +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('//', /* ... */); @@ -188,7 +188,7 @@ $router->addRoute('//', /* ... */); $router->addRoute('[/[/[]]]', /* ... */); ``` -Se volessimo influenzare il comportamento dello slash finale, in modo che ad esempio invece di `/home/` venga generato solo `/home`, si può ottenere così: +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('[[/[/]]]', /* ... */); @@ -198,24 +198,24 @@ $router->addRoute('[[/[/]]]', /* ... */); Caratteri jolly --------------- -Nella maschera del percorso assoluto possiamo utilizzare i seguenti caratteri jolly ed evitare così, ad esempio, la necessità di scrivere nella maschera il dominio, che può differire nell'ambiente di sviluppo e produzione: +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%` = top level domain, es. `com` o `org` -- `%sld%` = second level domain, es. `example` -- `%domain%` = dominio senza sottodomini, es. `example.com` -- `%host%` = intero host, es. `www.example.com` -- `%basePath%` = percorso alla directory principale +- `%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%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` -Notazione estesa ----------------- +Notazione avanzata +------------------ -La destinazione della route, solitamente scritta nella forma `Presenter:action`, può anche essere scritta utilizzando un array che definisce i singoli parametri e i loro valori predefiniti: +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('/[/]', [ @@ -224,7 +224,7 @@ $router->addRoute('/[/]', [ ]); ``` -Per una specifica più dettagliata, è possibile utilizzare una forma ancora più estesa, dove oltre ai valori predefiniti possiamo impostare altre proprietà dei parametri, come ad esempio l'espressione regolare di validazione (vedi parametro `id`): +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; @@ -242,19 +242,29 @@ $router->addRoute('/[/]', [ ]); ``` -È importante notare che se i parametri definiti nell'array non sono specificati nella maschera del percorso, i loro valori non possono essere modificati, nemmeno tramite i parametri query specificati dopo il punto interrogativo nell'URL. +È 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 ------------------- -Scriviamo il codice sorgente dell'applicazione in inglese, ma se il sito web deve avere URL in italiano, allora un semplice routing del tipo: +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 in inglese, come `/product/123` o `/cart`. Se vogliamo che i presenter e le azioni nell'URL siano rappresentati da parole italiane (es. `/prodotto/123` o `/carrello`), possiamo utilizzare un dizionario di traduzione. Per la sua scrittura abbiamo già bisogno della variante "più verbosa" del secondo parametro: +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; @@ -264,25 +274,25 @@ $router->addRoute('/', [ Route::Value => 'Home', Route::FilterTable => [ // stringa nell'URL => presenter - 'prodotto' => 'Product', - 'carrello' => 'Cart', - 'catalogo' => 'Catalog', + 'produkt' => 'Product', + 'kosik' => 'Cart', + 'katalog' => 'Catalog', ], ], 'action' => [ Route::Value => 'default', Route::FilterTable => [ - 'elenco' => 'list', + 'seznam' => 'list', ], ], ]); ``` -Più chiavi del dizionario di traduzione possono portare allo stesso presenter. In questo modo si creano diversi alias per esso. La variante canonica (cioè quella che sarà nell'URL generato) è considerata l'ultima chiave. +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 può essere utilizzata in questo modo per qualsiasi parametro. Se la traduzione non esiste, viene preso il valore originale. Possiamo cambiare questo comportamento aggiungendo `Route::FilterStrict => true` e la route rifiuterà quindi l'URL se il valore non è nel dizionario. +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, è possibile implementare anche funzioni di traduzione personalizzate. +Oltre al dizionario di traduzione sotto forma di array, si possono usare funzioni di traduzione personalizzate. ```php use Nette\Routing\Route; @@ -298,15 +308,15 @@ $router->addRoute('//', [ ]); ``` -La funzione `Route::FilterIn` converte tra il parametro nell'URL e la stringa, che viene poi passata al presenter, la funzione `FilterOut` assicura la conversione nella direzione opposta. +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 kebab-case utilizzato nell'URL. Il valore predefinito dei parametri viene scritto già nella forma trasformata, quindi ad esempio nel caso del presenter scriviamo ``, non ``. +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 in qualsiasi modo e poi restituirli. I filtri generali li definiamo sotto la chiave `null`. +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; @@ -314,85 +324,101 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -I filtri generali danno la possibilità di modificare il comportamento della route in modo assolutamente qualsiasi. Possiamo usarli ad esempio per modificare i parametri in base ad altri parametri. Ad esempio, la traduzione di `` e `` in base al valore corrente del parametro ``. +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 definito un filtro personalizzato e contemporaneamente esiste un filtro generale, viene eseguito il `FilterIn` personalizzato prima di quello generale e viceversa il `FilterOut` generale prima di quello personalizzato. Quindi all'interno del filtro generale i valori dei parametri `presenter` o `action` sono scritti nello stile PascalCase o camelCase. +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. -Sensi unici OneWay ------------------- -Le route a senso unico vengono utilizzate per mantenere la funzionalità dei vecchi URL, che l'applicazione non genera più, ma accetta ancora. Le contrassegniamo con il flag `OneWay`: +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', $router::ONE_WAY); +$router->addRoute('product-info', 'Product:detail', oneWay: true); // nuovo URL /product/123 $router->addRoute('product/', 'Product:detail'); ``` -Accedendo al vecchio URL, il presenter reindirizza automaticamente al nuovo URL, così i motori di ricerca non indicizzeranno due volte queste pagine (vedi [#SEO e canonizzazione]). +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 callback ------------------------------ +Routing dinamico con le callback +-------------------------------- -Il routing dinamico con callback ti consente di assegnare direttamente alle route funzioni (callback) che vengono eseguite quando viene visitato il percorso dato. Questa funzionalità flessibile ti consente di creare rapidamente ed efficacemente vari endpoint per la tua applicazione: +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 'sei all\'indirizzo /test'; + echo 'Vi trovate all\'indirizzo /test'; }); ``` -Puoi anche definire parametri nella maschera, che verranno automaticamente passati al tuo callback: +Nella maschera potete definire anche parametri, che vengono passati automaticamente alla vostra callback: ```php $router->addRoute('', function (string $lang) { echo match ($lang) { - 'cs' => 'Vítejte na české verzi našeho webu!', // Welcome to the Czech version of our website! - 'en' => 'Welcome to the English version of our website!', + '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 rientrano in un [modulo |directory-structure#Presenter e template] comune, utilizziamo `withModule()`: +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 seguenti route fanno parte del modulo Forum +$router->withModule('Forum') // le route seguenti fanno parte del modulo Forum ->addRoute('rss', 'Feed:rss') // il presenter sarà Forum:Feed ->addRoute('/') - ->withModule('Admin') // le seguenti route fanno parte del modulo Forum:Admin + ->withModule('Admin') // le route seguenti fanno parte del modulo Forum:Admin ->addRoute('sign:in', 'Sign:in'); ``` -Un'alternativa è l'uso del parametro `module`: +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 si mappa sul presenter Admin:Dashboard +// 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 ----------- -Possiamo suddividere le collezioni di route per sottodomini: +Le collezioni di route si possono suddividere per sottodomini: ```php $router = new RouteList; @@ -401,7 +427,7 @@ $router->withDomain('example.com') ->addRoute('/'); ``` -Nel nome del dominio è possibile utilizzare anche [#Caratteri jolly]: +Nel nome del dominio si possono usare anche i [caratteri jolly |#Caratteri jolly]: ```php $router = new RouteList; @@ -413,20 +439,20 @@ $router->withDomain('example.%tld%') Prefisso del percorso --------------------- -Possiamo suddividere le collezioni di route per percorso nell'URL: +Le collezioni di route si possono suddividere in base al percorso nell'URL: ```php $router = new RouteList; $router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // cattura l'URL /eshop/rss - ->addRoute('/'); // cattura l'URL /eshop// + ->addRoute('rss', 'Feed:rss') // corrisponde all'URL /eshop/rss + ->addRoute('/'); // corrisponde all'URL /eshop// ``` Combinazioni ------------ -Le suddivisioni sopra menzionate possono essere combinate tra loro: +I raggruppamenti visti sopra si possono combinare tra loro: ```php $router = (new RouteList) @@ -446,37 +472,37 @@ $router = (new RouteList) ``` -Parametri query ---------------- +Parametri di query +------------------ -Le maschere possono anche contenere parametri query (parametri dopo il punto interrogativo nell'URL). A questi non è possibile definire un'espressione di validazione, ma è possibile cambiare il nome con cui vengono passati al presenter: +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 -// il parametro query 'cat' vogliamo usarlo nell'applicazione con il nome 'categoryId' +// vogliamo usare il parametro di query 'cat' con il nome 'categoryId' nell'applicazione $router->addRoute('product ? id= & cat=', /* ... */); ``` -Parametri Foo +Parametri foo ------------- -Ora andiamo più a fondo. I parametri Foo sono essenzialmente parametri senza nome che consentono di abbinare un'espressione regolare. Un esempio è una route che accetta `/index`, `/index.html`, `/index.htm` e `/index.php`: +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à utilizzata durante la generazione dell'URL. La stringa deve essere posizionata direttamente dopo il punto interrogativo. La seguente route è simile alla precedente, ma genera `/index.html` invece di `/index`, perché la stringa `.html` è impostata come valore di generazione: +È 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 nell'applicazione -============================== +Integrazione +============ -Per integrare il router creato nell'applicazione, dobbiamo informarne il container DI. Il modo più semplice è preparare una factory che produca l'oggetto router e comunicare nella configurazione del container che deve usarla. Supponiamo di scrivere a tale scopo il metodo `App\Core\RouterFactory::createRouter()`: +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; @@ -494,14 +520,14 @@ class RouterFactory } ``` -Nella [configurazione |dependency-injection:services] scriveremo quindi: +Scriviamo poi nella [configurazione |dependency-injection:services]: ```neon services: - App\Core\RouterFactory::createRouter ``` -Qualsiasi dipendenza, ad esempio dal database ecc., viene passata al metodo factory come suoi parametri tramite [autowiring|dependency-injection:autowiring]: +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 @@ -514,15 +540,15 @@ 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 useremo quando non abbiamo particolari esigenze sulla forma dell'URL, se non è disponibile `mod_rewrite` (o le sue alternative) o se per ora non vogliamo occuparci di URL leggibili. +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 approssimativamente in questa forma: +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 & azione predefinito, a cui si deve puntare se apriamo la pagina senza parametri, ad es. `http://example.com/`. +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' @@ -540,9 +566,9 @@ services: SEO e canonizzazione ==================== -Il framework contribuisce al SEO (ottimizzazione della reperibilità su Internet) impedendo la duplicazione di contenuti su URL diversi. Se a una determinata destinazione portano più indirizzi, ad es. `/index` e `/index.html`, il framework determina il primo come primario (canonico) e reindirizza gli altri ad esso tramite il codice HTTP 301. Grazie a ciò, i motori di ricerca non indicizzano le pagine due volte e non diluiscono il loro page rank. +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è la prima route corrispondente nella collezione senza il flag OneWay. Pertanto, nella collezione elenchiamo **le route primarie per prime**. +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]. @@ -550,9 +576,9 @@ La canonizzazione viene eseguita dal presenter, maggiori informazioni nel capito HTTPS ===== -Per poter utilizzare il protocollo HTTPS, è necessario abilitarlo sull'hosting e configurare correttamente il server. +Per usare il protocollo HTTPS è necessario attivarlo sull'hosting e configurare correttamente il server. -Il redirect dell'intero sito a HTTPS deve essere impostato a livello di server, ad esempio tramite il file .htaccess nella directory principale della nostra applicazione, e con il codice HTTP 301. L'impostazione può variare a seconda dell'hosting e assomiglia circa a questo: +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: ``` @@ -564,40 +590,40 @@ Il redirect dell'intero sito a HTTPS deve essere impostato a livello di server, ``` -Il router genera URL con lo stesso protocollo con cui è stata caricata la pagina, quindi non è necessario impostare nient'altro. +Il router genera gli URL con lo stesso protocollo con cui è stata caricata la pagina, quindi non serve impostare altro. -Se però eccezionalmente abbiamo bisogno che diverse route vengano eseguite sotto protocolli diversi, lo specifichiamo nella maschera della route: +Se però, in via eccezionale, abbiamo bisogno che route diverse girino con protocolli diversi, lo indichiamo nella maschera della route: ```php -// Genererà un indirizzo con HTTP +// genererà un indirizzo HTTP $router->addRoute('http://%host%//', /* ... */); -// Genererà un indirizzo con HTTPs +// genererà un indirizzo HTTPS $router->addRoute('https://%host%//', /* ... */); ``` -Debug del router -================ +Debugging del router +==================== -Il pannello di routing visualizzato nella [Tracy Bar |tracy:] è un aiuto utile che mostra l'elenco delle route e anche i parametri che il router ha ottenuto dall'URL. +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, con il colore blu e il simbolo ≈ sono contrassegnate le route che avrebbero elaborato anche l'URL se la verde non le avesse precedute. Vediamo inoltre il presenter & azione corrente. +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 si verifica un redirect inaspettato a causa della [canonizzazione |#SEO e canonizzazione], è utile guardare il pannello nella barra *redirect*, dove scoprirete come il router ha originariamente compreso l'URL e perché ha reindirizzato. +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 debug del router, consigliamo di aprire gli Strumenti per sviluppatori nel browser (Ctrl+Shift+I o Cmd+Option+I) e nel pannello Network disattivare la cache, in modo che non vi vengano salvati i redirect. +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 alcune decine. Se il tuo sito web ha una struttura URL troppo complicata, puoi scrivere un [#Router personalizzato] su misura. +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, ad esempio dal database, e la sua factory non accetta argomenti, possiamo serializzare la sua forma compilata direttamente nel container DI e accelerare così leggermente l'applicazione. +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: @@ -608,7 +634,7 @@ routing: Router personalizzato ===================== -Le righe seguenti sono destinate a utenti molto avanzati. Puoi creare un tuo router personalizzato e integrarlo in modo del tutto naturale nella collezione di route. Il router è un'implementazione dell'interfaccia [api:Nette\Routing\Router] con due metodi: +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; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -Il metodo `match` elabora la richiesta corrente [$httpRequest |http:request], dalla quale è possibile ottenere non solo l'URL, ma anche gli header, ecc., in un array contenente il nome del presenter e i suoi parametri. Se non sa elaborare la richiesta, restituisce null. Durante l'elaborazione della richiesta, dobbiamo restituire almeno il presenter e l'azione. Il nome del presenter è completo e contiene anche eventuali moduli: +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 [ @@ -637,9 +663,9 @@ Il metodo `match` elabora la richiesta corrente [$httpRequest |http:request], da ] ``` -Il metodo `constructUrl` al contrario costruisce dall'array di parametri l'URL assoluto risultante. A tal fine può utilizzare le informazioni dal parametro [`$refUrl`|api:Nette\Http\UrlScript], che è l'URL corrente. +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 aggiungi alla collezione di route usando `add()`: +Lo aggiungete alla collezione di route con `add()`: ```php $router = new Nette\Application\Routers\RouteList; @@ -649,16 +675,16 @@ $router->addRoute(/* ... */); ``` -Utilizzo indipendente -===================== +Uso autonomo +============ -Per utilizzo indipendente intendiamo l'utilizzo delle capacità del router in un'applicazione che non utilizza Nette Application e i presenter. Vale per esso quasi tutto ciò che abbiamo mostrato in questo capitolo, con queste differenze: +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 utilizziamo la classe [api:Nette\Routing\RouteList] -- come simple router la classe [api:Nette\Routing\SimpleRouter] -- poiché non esiste la coppia `Presenter:action`, utilizziamo la [#Notazione estesa] +- 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] -Quindi di nuovo creiamo un metodo che ci costruisca il router, ad es.: +Creiamo quindi di nuovo un metodo che ci componga il router, per esempio: ```php namespace App\Core; @@ -682,35 +708,35 @@ class RouterFactory } ``` -Se usi un container DI, cosa che consigliamo, aggiungiamo di nuovo il metodo alla configurazione e poi otteniamo il router insieme alla richiesta HTTP dal container: +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 creiamo direttamente gli oggetti: +Oppure create direttamente gli oggetti: ```php $router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` -Ora resta solo da mettere al lavoro il router: +Ora non resta che lasciare al router il suo lavoro: ```php $params = $router->match($httpRequest); if ($params === null) { - // non è stata trovata una route corrispondente, inviamo errore 404 + // nessuna route corrispondente trovata, invia un errore 404 exit; } -// elaboriamo i parametri ottenuti +// elabora i parametri ottenuti $controller = $params['controller']; // ... ``` -E viceversa usiamo il router per costruire un link: +E, al contrario, usate il router per costruire un link: ```php $params = ['controller' => 'ArticleController', 'id' => 123]; @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/it/templates.texy b/application/it/templates.texy index 2080f7cd54..55b7110349 100644 --- a/application/it/templates.texy +++ b/application/it/templates.texy @@ -2,15 +2,15 @@ Template ******** .[perex] -Nette utilizza il sistema di templating [Latte |latte:]. Questo perché è il sistema di templating più sicuro per PHP, e allo stesso tempo il sistema più intuitivo. Non devi imparare molto di nuovo, ti basta la conoscenza di PHP e alcuni tag. +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. -È comune che una pagina sia composta da un template di layout + il template dell'azione specifica. Ecco come potrebbe apparire un template di layout, nota i blocchi `{block}` e il tag `{include}`: +È 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}La mia App{/block} + {block title}My App{/block}
    ...
    @@ -20,7 +20,7 @@ Nette utilizza il sistema di templating [Latte |latte:]. Questo perché è il si ``` -E questo sarà il template dell'azione: +E questo sarebbe il template dell'azione: ```latte {block title}Homepage{/block} @@ -31,15 +31,15 @@ E questo sarà il template dell'azione: {/block} ``` -Definisce il blocco `content`, che verrà inserito al posto di `{include content}` nel layout, e ridefinisce anche il blocco `title`, che sovrascriverà `{block title}` nel layout. Prova a immaginare il risultato. +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 -------------------- -Non devi specificare nei presenter quale template deve essere renderizzato, il framework deduce il percorso da solo e ti risparmia la scrittura. +Nei presenter non dovete indicare quale template va disegnato: il framework ne deduce automaticamente il percorso, risparmiandovi di scriverlo. -Se utilizzi una struttura di directory in cui ogni presenter ha la propria directory, posiziona semplicemente il template in questa directory con il nome dell'azione (o view), cioè per l'azione `default` usa il template `default.latte`: +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/ @@ -49,34 +49,34 @@ app/ └── default.latte \-- -Se utilizzi una struttura in cui i presenter sono insieme in una directory e i template nella cartella `templates`, salvalo o nel file `..latte` o `/.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 ← 1a variante - └── Home/ - └── default.latte ← 2a variante + ├── Home/ + │ └── default.latte ← 1ª variante + └── Home.default.latte ← 2ª variante \-- -La directory `templates` può trovarsi anche un livello sopra, cioè allo stesso livello della directory con le classi dei presenter. +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 - page not found |presenters#Errore 404 e simili]. +Se il template non viene trovato, il presenter risponde con un [errore 404 - pagina non trovata |presenters#Errore 404 e simili]. -La view viene cambiata usando `$this->setView('altraView')`. È anche possibile specificare direttamente il file del template usando `$this->template->setFile('/path/to/template.latte')`. +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 possono essere modificati sovrascrivendo il metodo [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], che restituisce un array di possibili nomi di file. +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 di layout. +Nette cerca automaticamente anche il file del layout. -Se utilizzi una struttura di directory in cui ogni presenter ha la propria directory, posiziona il layout o nella cartella con il presenter, se è specifico solo per esso, o un livello sopra, se è comune a più presenter: +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/ @@ -88,7 +88,7 @@ app/ └── default.latte \-- -Se utilizzi una struttura in cui i presenter sono insieme in una directory e i template nella cartella `templates`, il layout sarà atteso in queste posizioni: +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/ @@ -96,33 +96,66 @@ app/ ├── HomePresenter.php └── templates/ ├── @layout.latte ← layout comune - ├── Home.@layout.latte ← solo per Home, 1a variante - └── Home/ - └── @layout.latte ← solo per Home, 2a variante + ├── 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 avverrà anche ai livelli di directory superiori, in base alla nidificazione del modulo. +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 può essere cambiato usando `$this->setLayout('layoutAdmin')` e quindi sarà atteso nel file `@layoutAdmin.latte`. È anche possibile specificare direttamente il file del template di layout usando `$this->setLayout('/path/to/template.latte')`. +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')`. -Usando `$this->setLayout(false)` o il tag `{layout none}` all'interno del template, la ricerca del layout viene disattivata. +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 possono essere modificati sovrascrivendo il metodo [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], che restituisce un array di possibili nomi di file. +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 nel template +Variabili del template ---------------------- -Passiamo le variabili al template scrivendole in `$this->template` e poi le abbiamo disponibili nel template come variabili locali: +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); ``` -In questo modo semplice possiamo passare qualsiasi variabile ai template. Tuttavia, nello sviluppo di applicazioni robuste, è più utile limitarsi. Ad esempio, definendo esplicitamente l'elenco delle variabili che il template si aspetta e i loro tipi. Grazie a ciò, PHP potrà controllare i tipi, l'IDE suggerirà correttamente e l'analisi statica rivelerà errori. +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} -E come definiamo tale elenco? Semplicemente sotto forma di una classe e delle sue properties. La nominiamo in modo simile al presenter, ma con `Template` alla fine: +```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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -L'oggetto `$this->template` nel presenter sarà ora un'istanza della classe `ArticleTemplate`. Quindi PHP controllerà i tipi dichiarati durante la scrittura. E a partire dalla versione PHP 8.2 avviserà anche sulla scrittura in una variabile inesistente, nelle versioni precedenti si può ottenere lo stesso risultato usando il trait [Nette\SmartObject |utils:smartobject]. +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, grazie ad essa funzionerà il suggerimento, vedi [PhpStorm and code completion for $this⁠-⁠>⁠template |https://blog.nette.org/en/phpstorm-and-code-completion-for-this-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 *] -Puoi goderti il lusso del suggerimento anche nei template, basta installare il plugin per Latte in PhpStorm e specificare all'inizio del template il nome della classe, maggiori informazioni nell'articolo [Latte: jak na typový systém|https://blog.nette.org/it/latte-how-to-use-type-system]: +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} ... ``` -Così funzionano anche i template nei componenti, basta solo rispettare la convenzione di denominazione e per un componente ad es. `FifteenControl` creare una classe di template `FifteenTemplate`. +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 hai bisogno di creare `$template` come istanza di un'altra classe, utilizza il metodo `createTemplate()`: +Se vi serve usare una classe dei parametri diversa, usate il metodo `createTemplate()`: ```php public function renderDefault(): void @@ -168,58 +203,100 @@ public function renderDefault(): void } ``` +.{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: -Variabili predefinite ---------------------- - -I presenter e i componenti passano automaticamente diverse variabili utili ai template: - -- `$basePath` è il percorso URL assoluto alla directory principale (es. `/eshop`) -- `$baseUrl` è l'URL assoluto alla directory principale (es. `http://localhost/eshop`) -- `$user` è l'oggetto [che rappresenta l'utente |security:authentication] -- `$presenter` è il presenter corrente -- `$control` è il componente o presenter corrente -- `$flashes` è l'array di [messaggi |presenters#Messaggi flash] inviati dalla funzione `flashMessage()` - -Se utilizzi una classe di template personalizzata, queste variabili vengono passate se crei una property per esse. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My blog'; +} +``` -Creazione di link ------------------ +Creare i link +------------- -Nel template, i link ad altri presenter & azioni vengono creati in questo modo: +Nel template i link verso altri presenter e azioni si creano così: ```latte -dettaglio prodotto +dettaglio del prodotto ``` -L'attributo `n:href` è molto utile per i tag HTML ``. Se vogliamo stampare il link altrove, ad esempio nel testo, usiamo `{link}`: +L'attributo `n:href` è molto comodo per i tag HTML ``. Se vogliamo stampare il link altrove, per esempio nel testo, usiamo `{link}`: ```latte -L'indirizzo è: {link Home:default} +L'URL è: {link Home:default} ``` Maggiori informazioni si trovano nel capitolo [Creazione di link URL|creating-links]. -Filtri personalizzati, tag, ecc. --------------------------------- +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** -Il sistema di templating Latte può essere esteso con filtri, funzioni, tag personalizzati, ecc. Ciò può essere fatto direttamente nel metodo `render` o `beforeRender()`: +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 -public function beforeRender(): void +protected function beforeRender(): void { // aggiunta di un filtro - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); + + // aggiunta di una funzione + $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); +} +``` - // o configuriamo direttamente l'oggetto Latte\Engine +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->addFilterLoader(/* ... */); + $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` -Latte nella versione 3 offre un modo più avanzato, ovvero creare un'[extension |latte:extending-latte#Latte Extension] per ogni progetto web. Esempio parziale di tale classe: +**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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -La registriamo tramite la [configurazione |configuration#Template Latte]: +Registrate l'estensione tramite la [configurazione |configuration#Template Latte]: ```neon latte: @@ -263,13 +345,27 @@ latte: - 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 programmi un'applicazione multilingue, probabilmente avrai bisogno di stampare alcuni testi nel template in diverse lingue. Nette Framework definisce a tale scopo un'interfaccia per la traduzione [api:Nette\Localization\Translator], che ha un unico metodo `translate()`. Questo accetta il messaggio `$message`, che di solito è una stringa, e qualsiasi altro parametro. Il compito è restituire la stringa tradotta. In Nette non c'è un'implementazione predefinita, puoi scegliere in base alle tue esigenze tra diverse soluzioni pronte, che trovi su [Componette |https://componette.org/search/localization]. Nella loro documentazione imparerai come configurare il translator. +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 è possibile impostare un traduttore, che ci [facciamo passare |dependency-injection:passing-dependencies], con il metodo `setTranslator()`: +Ai template si può impostare un traduttore, che ci facciamo [passare |dependency-injection:passing-dependencies], con il metodo `setTranslator()`: ```php protected function beforeRender(): void @@ -279,7 +375,7 @@ protected function beforeRender(): void } ``` -Il translator può alternativamente essere impostato tramite la [configurazione |configuration#Template Latte]: +In alternativa il traduttore si può impostare tramite la [configurazione |configuration#Template Latte]: ```neon latte: @@ -287,7 +383,7 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Successivamente, il traduttore può essere utilizzato ad esempio come filtro `|translate`, inclusi parametri aggiuntivi che vengono passati al metodo `translate()` (vedi `foo, bar`): +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} @@ -295,7 +391,7 @@ Successivamente, il traduttore può essere utilizzato ad esempio come filtro `|t {$item|translate, foo, bar} ``` -O come tag con trattino basso: +Oppure come tag con il trattino basso: ```latte {_'Carrello'} @@ -303,14 +399,14 @@ O come tag con trattino basso: {_$item, foo, bar} ``` -Per la traduzione di una sezione del template esiste un tag di coppia `{translate}` (da Latte 2.11, prima si usava il tag `{_}`): +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 translator viene chiamato standardmente durante l'esecuzione al rendering del template. Latte versione 3, tuttavia, può tradurre tutti i testi statici già durante la compilazione del template. Ciò consente di risparmiare prestazioni, poiché ogni stringa viene tradotta solo una volta e la traduzione risultante viene scritta nella forma compilata. Nella directory della cache vengono così create più versioni compilate del template, una per ogni lingua. Per fare ciò, basta solo specificare la lingua come secondo parametro: +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 @@ -320,4 +416,4 @@ protected function beforeRender(): void } ``` -Per testo statico si intende ad esempio `{_'ciao'}` o `{translate}ciao{/translate}`. I testi non statici, come ad esempio `{_$foo}`, continueranno ad essere tradotti durante l'esecuzione. +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 new file mode 100644 index 0000000000..7cfec2644a --- /dev/null +++ b/application/it/upgrading.texy @@ -0,0 +1,47 @@ +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 index 51000ea766..bf788071fd 100644 --- a/application/ja/@home.texy +++ b/application/ja/@home.texy @@ -2,84 +2,84 @@ Nette Application ***************** .[perex] -Nette ApplicationはNetteフレームワークの中核であり、最新のWebアプリケーションを作成するための強力なツールを提供します。開発を大幅に容易にし、コードのセキュリティと保守性を向上させる多くの優れた機能を提供します。 +Nette Application は Nette フレームワークの中核で、現代的なウェブアプリケーションを作るための強力な道具を提供します。開発を大きく楽にし、コードの安全性と保守性を高める、他にはない機能が数多く揃っています。 インストール ------ -[Composer|best-practices:composer]を使用してライブラリをダウンロードし、インストールします: +ライブラリは [Composer|best-practices:composer]でダウンロードしてインストールします。 ```shell composer require nette/application ``` -なぜNette Applicationを選ぶのか? -------------------------- +なぜ Nette Application を選ぶのか +-------------------------- -Netteは常にWeb技術分野のパイオニアでした。 +Nette は常にウェブ技術の先駆けであり続けてきました。 -**双方向ルーター:** Netteは高度なルーティングシステムを備えており、その双方向性でユニークです - URLをアプリケーションのアクションに変換するだけでなく、逆にURLアドレスを生成することもできます。これは次のことを意味します: -- テンプレートを編集することなく、いつでもアプリケーション全体のURL構造を変更できます -- URLは自動的に正規化され、SEOが向上します -- ルーティングはアノテーションに散在するのではなく、一箇所で定義されます +**双方向のルーター:** Nette には双方向性という点で他にない高度なルーティングのしくみがあります。URL をアプリケーションのアクションに変換するだけでなく、逆に URL を生成することもできます。つまり、 +- アプリケーション全体の URL の構造を、テンプレートを直さずにいつでも変えられます +- URL が自動的に正規化され、SEO が良くなります +- ルーティングが 1 か所で定義され、アノテーションに散らばりません -**コンポーネントとシグナル:** DelphiとReact.jsに触発された組み込みコンポーネントシステムは、PHPフレームワークの中で完全にユニークです: -- 再利用可能なUI要素の作成を可能にします -- コンポーネントの階層的な構成をサポートします -- シグナルを使用してAJAXリクエストをエレガントに処理します -- [Componette](https://componette.org)には豊富な既製コンポーネントライブラリがあります +**コンポーネントとシグナル:** Delphi と React.js に着想を得た組み込みのコンポーネントのしくみは、PHP のフレームワークの中でも他に例がありません。 +- 再利用できる UI 要素を作れます +- 階層的なコンポーネントの組み立てに対応しています +- シグナルを使った優雅な AJAX リクエストの処理を提供します +- [Componette](https://componette.org)に既製のコンポーネントの豊富なライブラリがあります -**AJAXとスニペット:** Netteは、Ruby on RailsのHotwireやSymfony UX Turboのような同様のソリューションが登場するずっと前の2009年に、AJAXを扱う革新的な方法を導入しました: -- スニペットを使用すると、JavaScriptを記述することなくページの一部のみを更新できます -- コンポーネントシステムとの自動統合 -- ページの一部を賢く無効化 -- 転送されるデータの最小量 +**AJAX とスニペット:** Nette は 2009 年に、AJAX を扱う画期的な方法を導入しました。Ruby on Rails の Hotwire や Symfony UX Turbo のような似た解よりずっと前のことです。 +- スニペットを使うと、JavaScript を書かずにページの一部だけを更新できます +- コンポーネントのしくみと自動的に統合されます +- ページの部分を賢く無効化します +- データの転送は最小限です -**直感的なテンプレート [Latte|latte:]:** PHP用の最も安全なテンプレートシステムで、高度な機能を備えています: -- コンテキストに応じたエスケープによるXSSからの自動保護 -- カスタムフィルタ、関数、タグによる拡張性 -- AJAX用のテンプレート継承とスニペット -- 型システムを備えたPHP 8.xの優れたサポート +**直感的な [Latte|latte:]テンプレート:** 高度な機能を備えた、PHP で最も安全なテンプレートシステムです。 +- コンテキストに応じたエスケープによる自動的な XSS 対策 +- 独自のフィルタ、関数、タグで拡張できます +- テンプレートの継承と AJAX 用のスニペット +- 型システムを備えた優れた PHP 8.x サポート -**Dependency Injection:** NetteはDependency Injectionを完全に活用しています: -- 依存関係の自動受け渡し(autowiring) -- わかりやすいNEON形式による設定 -- コンポーネントファクトリのサポート +**Dependency Injection:** Nette は依存性注入を全面的に活用します。 +- 依存関係の自動的な受け渡し(オートワイヤリング) +- 分かりやすい NEON 形式による設定 +- コンポーネントのファクトリのサポート 主な利点 ---- -- **セキュリティ**: XSS、CSRFなどの[脆弱性|nette:vulnerability-protection]に対する自動防御 -- **生産性**: スマートな設計により、少ない記述でより多くの機能を実現 -- **デバッグ**: ルーティングパネルを備えた[Tracyデバッガー|tracy:] -- **パフォーマンス**: スマートキャッシュ、コンポーネントの遅延読み込み -- **柔軟性**: アプリケーション完成後でもURLを簡単に変更可能 -- **コンポーネント**: 再利用可能なUI要素のユニークなシステム -- **モダン**: PHP 8.4+と型システムを完全にサポート +- **安全性**: XSS、CSRF などの[脆弱性|nette:vulnerability-protection]に対する自動的な保護 +- **生産性**: 賢い設計のおかげで、書く量は少なく機能は多く +- **デバッグ**: ルーティングのパネルを備えた [Tracy デバッガ|tracy:] +- **性能**: 賢いキャッシュ、コンポーネントの遅延読み込み +- **柔軟さ**: アプリケーションが完成したあとでも URL を簡単に変えられます +- **コンポーネント**: 再利用できる UI 要素の他にないしくみ +- **現代的**: PHP 8.3+ と型システムの完全なサポート はじめに ---- -1. [アプリケーションはどのように動作しますか? |how-it-works] - 基本的なアーキテクチャの理解 -2. [Presenter |presenters] - Presenterとアクションの操作 -3. [テンプレート |templates] - Latteでのテンプレート作成 -4. [ルーティング |routing] - URLアドレスの設定 -5. [インタラクティブコンポーネント |components] - コンポーネントシステムの活用 +1. [アプリケーションはどう動くのか |how-it-works] - 基本のアーキテクチャの理解 +2. [プレゼンター |presenters] - プレゼンターとアクションの扱い +3. [テンプレート |templates] - Latte でのテンプレートの作成 +4. [ルーティング |routing] - URL アドレスの設定 +5. [インタラクティブなコンポーネント |components] - コンポーネントのしくみの利用 -PHPとの互換性 +PHP の互換性 -------- -| バージョン | PHPとの互換性 -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| バージョン | 対応する 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 index d08783d23f..54cd9501d3 100644 --- a/application/ja/@left-menu.texy +++ b/application/ja/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** -- [アプリケーションはどのように動作しますか? |how-it-works] -- [Bootstrapping] -- [Presenter |presenters] -- [テンプレート |templates] -- [ディレクトリ構造 |directory-structure] -- [ルーティング |routing] -- [URLリンクの作成 |creating-links] -- [インタラクティブコンポーネント |components] -- [AJAX & スニペット |ajax] +- [概要 |@home] +- [アプリケーションはどう動くのか |how-it-works] +- [ブートストラップ|bootstrapping] +- [プレゼンター|presenters] +- [テンプレート|templates] +- [ディレクトリ構成 |directory-structure] +- [ルーティング|routing] +- [URL リンクの作成 |creating-links] +- [インタラクティブなコンポーネント |components] +- [AJAX とスニペット |ajax] - [Multiplier |multiplier] -- [設定 |configuration] +- [設定|configuration] +- [アップグレード|upgrading] -参考文献 +関連情報 **** -- [なぜNetteを使うのか? |www:10-reasons-why-nette] +- [なぜ Nette を使うのか|www:10-reasons-why-nette] - [インストール |nette:installation] -- [最初のアプリケーションを作成しましょう! |quickstart:] -- [ガイドとベストプラクティス |best-practices:] -- [問題解決 |nette:troubleshooting] +- [最初のアプリケーションを作ろう |quickstart:] +- [ベストプラクティス |best-practices:] +- [トラブルシューティング |nette:troubleshooting] diff --git a/application/ja/@meta.texy b/application/ja/@meta.texy index d3c41dc3d7..43b85f3cac 100644 --- a/application/ja/@meta.texy +++ b/application/ja/@meta.texy @@ -1 +1 @@ -{{sitename: Nette ドキュメンテーション}} +{{sitename: Nette ドキュメント}} diff --git a/application/ja/ajax.texy b/application/ja/ajax.texy index 9b07928011..473d163c99 100644 --- a/application/ja/ajax.texy +++ b/application/ja/ajax.texy @@ -3,20 +3,20 @@ AJAX とスニペット
    -最新のWebアプリケーションの時代では、機能がサーバーとブラウザの間で分割されることが多いため、AJAXは不可欠な接続要素です。Nette Frameworkはこの分野でどのような可能性を提供しているでしょうか? +機能がサーバーとブラウザに分かれることの多い現代のウェブアプリケーションにおいて、AJAX は欠かせないつなぎ役です。この領域で Nette Framework は何を提供するのでしょうか。 - テンプレートの一部、いわゆるスニペットの送信 -- PHPとJavaScript間の変数渡し -- AJAXリクエストのデバッグツール +- PHP と JavaScript のあいだの変数の受け渡し +- AJAX リクエストをデバッグするための道具
    -AJAXリクエスト -========= +AJAX リクエスト +========== -AJAXリクエストは、基本的に通常のHTTPリクエストと変わりません。特定のパラメータでPresenterが呼び出されます。そして、Presenterがリクエストにどのように応答するかは、Presenter次第です。JSON形式のデータを返す、HTMLコードの一部を送信する、XMLドキュメントを送信するなど、さまざまな方法があります。 +AJAX のリクエストは、本質的には通常の HTTP リクエストと変わりません。特定のパラメータでプレゼンターが呼ばれます。リクエストにどう応えるかはプレゼンター次第で、JSON 形式のデータを返すことも、HTML コードの一部や XML のドキュメントを送ることもできます。 -ブラウザ側では、`fetch()` 関数を使用してAJAXリクエストを初期化します。 +ブラウザ側では、`fetch()` 関数で AJAX のリクエストを始めます。 ```js fetch(url, { @@ -24,22 +24,22 @@ fetch(url, { }) .then(response => response.json()) .then(payload => { - // レスポンスの処理 + // レスポンスを処理します }); ``` -サーバー側では、[HTTPリクエストをカプセル化するサービス |http:request] の `$httpRequest->isAjax()` メソッドでAJAXリクエストを認識します。検出には `X-Requested-With` HTTPヘッダーを使用するため、これを送信することが重要です。Presenter内では `$this->isAjax()` メソッドを使用できます。 +サーバー側では、[HTTP リクエストを包む |http:request]サービスの `$httpRequest->isAjax()` メソッドで AJAX のリクエストを見分けます。検出には `X-Requested-With` の HTTP ヘッダーを使うので、それを送ることが決定的に重要です。プレゼンターの中では `$this->isAjax()` メソッドが使えます。 -JSON形式でデータを送信したい場合は、[`sendJson()` |presenters#応答の送信] メソッドを使用します。このメソッドはPresenterの動作も終了させます。 +JSON 形式でデータを送りたい場合は [`sendJson()` |presenters#レスポンスの送信]メソッドを使います。このメソッドはプレゼンターの動作も終わらせます。 ```php public function actionExport(): void { - $this->sendJson($this->model->getData); + $this->sendJson($this->model->getData()); } ``` -AJAX用に特別なテンプレートで応答する予定がある場合は、次のように行うことができます。 +AJAX 用に用意した特別なテンプレートで応えるつもりなら、次のようにできます。 ```php public function handleClick($param): void @@ -55,35 +55,35 @@ public function handleClick($param): void スニペット ===== -Netteがサーバーとクライアントを接続するために提供する最も強力な手段は、スニペットです。これらのおかげで、最小限の労力と数行のコードで、通常のアプリケーションをAJAXアプリケーションに変えることができます。これがどのように機能するかは、Fifteenの例で示されています。そのコードは[GitHub |https://github.com/nette-examples/fifteen]にあります。 +サーバーとクライアントをつなぐために Nette が提供する最も強力な道具がスニペットです。これを使えば、ごくわずかな手間と数行のコードで、ふつうのアプリケーションを AJAX のアプリケーションに変えられます。Fifteen の例が全体の動きを示していて、そのコードは [GitHub |https://github.com/nette-examples/fifteen]にあります。 -スニペット、つまり切り抜きは、ページ全体を再読み込みする代わりに、ページの一部だけを更新することを可能にします。これはより速く、より効率的であるだけでなく、より快適なユーザーエクスペリエンスも提供します。スニペットは、Ruby on RailsのHotwireやSymfony UX Turboを思い出させるかもしれません。興味深いことに、Netteはスニペットを14年も前に導入しました。 +スニペットを使うと、ページ全体を読み込み直す代わりに、一部だけを更新できます。速く効率がよいだけでなく、より快適な利用体験も提供します。スニペットは Ruby on Rails の Hotwire や Symfony UX Turbo を思い起こさせるかもしれません。面白いことに、Nette はスニペットをその 14 年前に導入していました。 -スニペットはどのように機能しますか?ページの最初の読み込み(非AJAXリクエスト)では、すべてのスニペットを含むページ全体が読み込まれます。ユーザーがページと対話する(例えば、ボタンをクリックする、フォームを送信するなど)と、ページ全体を読み込む代わりにAJAXリクエストが発行されます。Presenterのコードはアクションを実行し、どのスニペットを更新する必要があるかを決定します。Netteはこれらのスニペットをレンダリングし、JSON形式の配列として送信します。ブラウザの処理コードは、受信したスニペットをページに挿入します。したがって、変更されたスニペットのコードのみが転送され、帯域幅を節約し、ページ全体のコンテンツを転送するよりも読み込みを高速化します。 +スニペットはどう動くのでしょうか。ページが最初に読み込まれるとき(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などのアプリケーションで使用するため)。 +ブラウザ側でスニペットを扱うには [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]する必要があります。 +まずライブラリを[初期化する |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]必要があります。 ```js naja.initialize(); ``` -通常のリンク(シグナル)やフォーム送信からAJAXリクエストを作成するには、関連するリンク、フォーム、またはボタンに `ajax` クラスを付けるだけです。 +ふつうのリンク(シグナル)やフォームの送信を AJAX のリクエストに変えるには、対象のリンク、フォーム、ボタンに `ajax` クラスを付けるだけです。 ```latte Go @@ -103,29 +103,31 @@ naja.initialize(); スニペットの再描画 --------- -[Control |components]クラスの各オブジェクト(Presenter自体を含む)は、再描画が必要な変更が発生したかどうかを記録します。これには `redrawControl()` メソッドが使用されます。 +[Control |components]クラスのオブジェクト(プレゼンター自身も含みます)はすべて、再描画が必要な変更が起きたかどうかを覚えています。そのために `redrawControl()` メソッドを使います。 ```php public function handleLogin(string $user): void { - // ログイン後、関連部分を再描画する必要がある + // ログイン後に該当する部分を再描画する必要があります $this->redrawControl(); // ... } ``` -Netteは、再描画する内容をさらに細かく制御できます。このメソッドは、引数としてスニペット名を受け取ることができます。したがって、テンプレートの一部のレベルで無効化(つまり、再描画を強制)できます。コンポーネント全体が無効化されると、そのすべてのスニペットも再描画されます。 +Nette は、何を再描画すべきかをさらに細かく制御できます。このメソッドはスニペットの名前を引数に取れます。つまりテンプレートの部分の水準で無効化(すなわち再描画の強制)ができます。コンポーネント全体が無効化されると、その中のすべてのスニペットも再描画されます。 ```php -// 'header' スニペットを無効化 +// 'header' スニペットを無効化します $this->redrawControl('header'); ``` +第 2 パラメータ `$redraw` を使えば、保留中の無効化を取り消すこともできます。`$this->redrawControl('header', redraw: false)` を呼ぶと、そのスニペットは再描画不要と印を付けられます。完全なシグネチャは `redrawControl(?string $snippet = null, bool $redraw = true)` です。 + -Latteのスニペット ------------ +Latte でのスニペット +------------- -Latteでスニペットを使用するのは非常に簡単です。テンプレートの一部をスニペットとして定義するには、単に `{snippet}` と `{/snippet}` タグで囲みます。 +Latte でスニペットを使うのはきわめて簡単です。テンプレートの一部をスニペットとして定義するには、`{snippet}` と `{/snippet}` のタグで囲むだけです。 ```latte {snippet header} @@ -133,9 +135,9 @@ Latteでスニペットを使用するのは非常に簡単です。テンプレ {/snippet} ``` -スニペットは、特別な生成された `id` を持つ `
    ` 要素をHTMLページに作成します。スニペットが再描画されると、この要素のコンテンツが更新されます。したがって、ページの初期レンダリング時に、たとえ最初は空であっても、すべてのスニペットもレンダリングする必要があります。 +スニペットは、生成された特別な `id` を持つ `
    ` 要素を HTML のページに作ります。スニペットが再描画されると、この要素の内容が更新されます。ですから、ページが最初に描かれるときには、たとえ最初は空でも、すべてのスニペットが描かれている必要があります。 -`
    ` 以外の要素でスニペットを作成することもできます。`n:attribute` を使用します。 +n:属性を使えば、`
    ` 以外の要素でスニペットを作ることもできます。 ```latte
    @@ -147,7 +149,7 @@ Latteでスニペットを使用するのは非常に簡単です。テンプレ スニペット領域 ------- -スニペット名は式にすることもできます。 +スニペットの名前は式にもできます。 ```latte {foreach $items as $id => $item} @@ -155,7 +157,9 @@ Latteでスニペットを使用するのは非常に簡単です。テンプレ {/foreach} ``` -これにより、`item-0`、`item-1`などの複数のスニペットが作成されます。動的スニペット(例えば `item-1`)を直接無効化した場合、何も再描画されません。理由は、スニペットは本当に切り抜きとして機能し、それ自体だけが直接レンダリングされるためです。しかし、テンプレートには実際には `item-1` という名前のスニペットはありません。それは、スニペットの周りのコード、つまりforeachループを実行することによってのみ作成されます。したがって、実行されるべきテンプレートの部分を `{snippetArea}` タグでマークします。 +これだけでは動かない途中の段階です。静的な `{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
      @@ -165,16 +169,16 @@ Latteでスニペットを使用するのは非常に簡単です。テンプレ
    ``` -そして、スニペット自体と親領域全体の両方を再描画させます。 +そして個々のスニペットと親の領域の両方の再描画を要求します。 ```php $this->redrawControl('itemsContainer'); $this->redrawControl('item-1'); ``` -同時に、`$items` 配列には再描画されるべき項目のみが含まれるようにすることが望ましいです。 +あわせて、`$items` 配列には再描画すべき項目だけが入るようにするとよいでしょう。 -`{include}` タグを使用して、スニペットを含む別のテンプレートをテンプレートに挿入する場合、テンプレートの挿入を再度 `snippetArea` に含め、それをスニペットと一緒に無効化する必要があります。 +`{include}` タグを使って、スニペットを含む別のテンプレートを主テンプレートに取り込む場合は、そのテンプレートの取り込みをもう一度 `snippetArea` で包み、スニペットとともに無効化する必要があります。 ```latte {snippetArea include} @@ -195,25 +199,25 @@ $this->redrawControl('item'); ``` -コンポーネントのスニペット -------------- +コンポーネントの中のスニペット +--------------- -[コンポーネント|components] 内にスニペットを作成することもでき、Netteはそれらを自動的に再描画します。ただし、制限があります。スニペットを再描画するために、パラメータなしで `render()` メソッドを呼び出します。したがって、テンプレートでパラメータを渡すことは機能しません。 +[コンポーネント|components]の中にもスニペットを作れ、Nette が自動的に再描画します。ただし制限があります。スニペットを再描画するために、Nette は `render()` メソッドをパラメータなしで呼びます。ですからテンプレートでパラメータを渡しても働きません。 ```latte OK {control productGrid} -動作しません: +動きません: {control productGrid $arg, $arg} {control productGrid:paginator} ``` -ユーザーデータの送信 ----------- +独自データの送信 +-------- -スニペットと一緒に、任意の追加データをクライアントに送信できます。それらを `payload` オブジェクトに書き込むだけです。 +スニペットとあわせて、任意の追加データをクライアントに送れます。`payload` オブジェクトに書き込むだけです。 ```php public function actionDelete(int $id): void @@ -226,10 +230,16 @@ public function actionDelete(int $id): void ``` +リダイレクト +------ + +AJAX のリクエスト中、`redirect()` と `redirectUrl()` メソッドは HTTP のリダイレクトを送りません。代わりに、AJAX のレスポンスで送られるデータオブジェクトであるペイロードの `payload.redirect` プロパティに宛先の URL を書き込んで送ります。実際のリダイレクトはクライアント側のライブラリ(Naja)が行います。 + + パラメータの受け渡し ========== -AJAXリクエストを使用してコンポーネントにパラメータを送信する場合、それがシグナルパラメータであろうと永続パラメータであろうと、リクエストでコンポーネント名を含むグローバル名を指定する必要があります。パラメータの完全な名前は `getParameterId()` メソッドによって返されます。 +AJAX のリクエストでコンポーネントにパラメータを送るとき、それがシグナルのパラメータであれ永続パラメータであれ、コンポーネント名を含むグローバルな名前をリクエストで指定しなければなりません。`getParameterId()` メソッドが完全なパラメータ名を返します。 ```js let url = new URL({link //foo!}); @@ -240,10 +250,16 @@ fetch(url, { }) ``` -そして、コンポーネント内の対応するパラメータを持つハンドルメソッド: +そしてコンポーネント側の、対応するパラメータを持つ handle メソッドです。 ```php public function handleFoo(int $bar): void { } ``` + + +関連情報 +==== + +- [動的スニペット |best-practices:dynamic-snippets] diff --git a/application/ja/bootstrapping.texy b/application/ja/bootstrapping.texy index 602daba9c8..53830bd4ea 100644 --- a/application/ja/bootstrapping.texy +++ b/application/ja/bootstrapping.texy @@ -3,19 +3,22 @@
    -ブートストラップは、アプリケーション環境の初期化、依存性注入(DI)コンテナの作成、およびアプリケーションの開始プロセスです。以下について説明します: +ブートストラップとは、アプリケーションの環境を初期化し、依存性注入(DI)コンテナを作り、アプリケーションを起動する過程のことです。ここでは次のことを扱います。 -- Bootstrapクラスが環境を初期化する方法 -- NEONファイルを使用してアプリケーションを設定する方法 -- 本番モードと開発モードを区別する方法 -- DIコンテナを作成および設定する方法 +- Bootstrap クラスが環境をどう初期化するか +- NEON ファイルでアプリケーションをどう設定するか +- 本番モードと開発モードをどう見分けるか +- DI コンテナをどう作り、どう設定するか
    -Webアプリケーションであれ、コマンドラインから実行されるスクリプトであれ、アプリケーションはその実行を開始する際に何らかの形で環境を初期化します。昔は、例えば `include.inc.php` のようなファイルがこれを担当し、最初のファイルがインクルードしていました。 最新のNetteアプリケーションでは、これは `Bootstrap` クラスに置き換えられ、アプリケーションの一部として `app/Bootstrap.php` ファイルにあります。例えば、次のようになります。 +ウェブのアプリケーションであれ、コマンドラインから実行するスクリプトであれ、その実行は何らかの環境の初期化から始まります。昔は `include.inc.php` のような名前のファイルがそれを担い、最初のファイルから読み込まれていました。現代の Nette のアプリケーションでは、それが `Bootstrap` クラスに置き換わり、アプリケーションの一部として `app/Bootstrap.php` ファイルにあります。たとえば次のような形になります。 ```php +namespace App; + +use Nette; use Nette\Bootstrap\Configurator; class Bootstrap @@ -26,9 +29,9 @@ class Bootstrap public function __construct() { $this->rootDir = dirname(__DIR__); - // Configuratorはアプリケーション環境とサービスの設定を担当します。 + // Configurator はアプリケーションの環境とサービスの設定を担当します。 $this->configurator = new Configurator; - // Netteによって生成される一時ファイル(コンパイルされたテンプレートなど)のディレクトリを設定します。 + // Nette が生成する一時ファイル(コンパイル済みテンプレートなど)のディレクトリを設定します $this->configurator->setTempDirectory($this->rootDir . '/temp'); } @@ -41,14 +44,14 @@ class Bootstrap private function initializeEnvironment(): void { - // Netteは賢く、開発モードは自動的に有効になります。 - // または、次の行のコメントを解除して特定のIPアドレスに対して有効にすることもできます。 + // Nette は賢いので開発モードは自動的に有効になります。 + // あるいは次の行のコメントを外して、特定の IP アドレスに対して有効にできます: // $this->configurator->setDebugMode('secret@23.75.345.200'); - // Tracyを有効にします:デバッグのための究極の「スイスアーミーナイフ」。 + // Tracy を有効にします。究極の「万能ナイフ」的デバッグツールです。 $this->configurator->enableTracy($this->rootDir . '/log'); - // RobotLoader: 選択したディレクトリ内のすべてのクラスを自動的にロードします。 + // RobotLoader: 選んだディレクトリのすべてのクラスを自動的に読み込みます $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); @@ -56,7 +59,7 @@ class Bootstrap private function setupContainer(): void { - // 設定ファイルをロードします。 + // 設定ファイルを読み込みます $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } @@ -66,69 +69,78 @@ class Bootstrap index.php ========= -Webアプリケーションの場合、最初のファイルは `www/` [公開ディレクトリ |directory-structure#公開ディレクトリ www] にある `index.php` です。これは `Bootstrap` クラスに環境を初期化させ、DIコンテナを作成させます。その後、そこから `Application` サービスを取得し、Webアプリケーションを起動します。 +ウェブアプリケーションの場合、最初のファイルは[公開ディレクトリ |directory-structure#公開ディレクトリ www/] `www/` にある `index.php` です。これは Bootstrap クラスに、環境を初期化して DI コンテナを作るよう指示します。そしてコンテナから `Application` サービスを取り出し、それがウェブアプリケーションを実行します。 ```php $bootstrap = new App\Bootstrap; -// 環境の初期化 + DIコンテナの作成 +// 環境を初期化し、DI コンテナを作ります $container = $bootstrap->bootWebApplication(); -// DIコンテナは Nette\Application\Application オブジェクトを作成します +// DI コンテナが Nette\Application\Application オブジェクトを作ります $application = $container->getByType(Nette\Application\Application::class); -// Netteアプリケーションを起動し、受信リクエストを処理します +// Nette のアプリケーションを起動し、届いたリクエストを処理します $application->run(); ``` -ご覧のとおり、環境の設定と依存性注入(DI)コンテナの作成は、[api:Nette\Bootstrap\Configurator] クラスによって支援されます。これについて詳しく見ていきましょう。 +.[note] +`$application` オブジェクトは、リクエストを処理しながら[イベント |nette:glossary#イベント]を発します。`onStartup`、`onRequest`、`onPresenter`、`onResponse`、`onShutdown`、そして(処理されなかった例外に対する)`onError` です。ハンドラを結びつけられるので、ログ記録やアプリケーション全体の監視に便利です。 + +ご覧のとおり、[api:Nette\Bootstrap\Configurator]クラスが環境の設定と依存性注入(DI)コンテナの生成を助けます。ここからそれを詳しく紹介します。 -開発環境 vs 本番環境 -============ +開発モードと本番モード +=========== -Netteは、開発サーバーで実行されているか、本番サーバーで実行されているかによって動作が異なります。 +Nette は、開発サーバーで動いているか本番サーバーで動いているかによって振る舞いを変えます。 -🛠️ 開発環境 (Development): - - 役立つ情報(SQLクエリ、実行時間、使用メモリ)を含むTracyデバッグバーを表示します。 - - エラーが発生した場合、関数呼び出しと変数内容を含む詳細なエラーページを表示します。 - - Latteテンプレートの変更、設定ファイルの編集などがあった場合にキャッシュを自動的に更新します。 +🛠️ 開発モード: + - 役立つ情報(SQL クエリ、実行時間、使用メモリ)を載せた Tracy のデバッグバーを表示します + - エラーのときは、関数の呼び出しと変数の内容を含む詳しいエラーページを表示します + - Latte のテンプレートや設定ファイルなどが変わると、キャッシュを自動的に更新します -🚀 本番環境 (Production): - - デバッグ情報は表示せず、すべてのエラーをログに記録します。 - - エラーが発生した場合、ErrorPresenterまたは一般的な「Server Error」ページを表示します。 - - キャッシュは自動的に更新されません! - - 速度とセキュリティのために最適化されています。 +🚀 本番モード: + - デバッグ情報は一切表示せず、すべてのエラーをログに書きます + - エラーのときは ErrorPresenter か、一般的な「Server Error」のページを表示します + - キャッシュは決して自動更新されません + - 速度と安全性のために最適化されています -モードの選択は自動検出によって行われるため、通常は何も設定したり手動で切り替えたりする必要はありません。 +モードの選択は自動検出で行われるので、ふつうは何も設定したりモードを手で切り替えたりする必要はありません。 -- 開発環境: localhost(IPアドレス `127.0.0.1` または `::1`)で、プロキシが存在しない場合(つまり、そのHTTPヘッダーがない場合)。 -- 本番環境: それ以外のすべての場合。 +- 開発モード: localhost(IP アドレス `127.0.0.1` または `::1`)で、プロキシがない場合(つまりその HTTP ヘッダーが検出されない場合) +- 本番モード: それ以外のすべての場所 -他の場合、例えば特定のIPアドレスからアクセスするプログラマーに対して開発環境を有効にしたい場合は、`setDebugMode()` を使用します。 +たとえば特定の IP アドレスからアクセスするプログラマーのために、ほかの場合にも開発モードを有効にしたいなら、`setDebugMode()` を使います。 ```php -$this->configurator->setDebugMode('23.75.345.200'); // IPアドレスの配列も指定できます +$this->configurator->setDebugMode('23.75.345.200'); // IP アドレスの配列も渡せます ``` -IPアドレスとCookieを組み合わせることを強くお勧めします。`nette-debug` Cookieに秘密のトークン、例えば `secret1234` を保存し、この方法で特定のIPアドレスからアクセスし、かつCookieに言及されたトークンを持つプログラマーに対して開発環境を有効にします。 +IP アドレスと cookie の組み合わせを強くおすすめします。`nette-debug` の cookie に秘密のトークン、たとえば `secret1234` を保存し、特定の IP アドレスからアクセスし、かつその cookie にそのトークンを持つプログラマーに対して開発モードを有効にします。 ```php $this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` -localhostに対しても、開発環境を完全に無効にすることもできます。 +localhost も含めて、開発モードを完全に無効にすることもできます。 ```php $this->configurator->setDebugMode(false); ``` -注意:値 `true` は開発環境を強制的に有効にします。これは本番サーバーでは絶対に行ってはいけません。 +値 `true` は開発モードを強制することに注意してください。本番サーバーでは**決して**あってはなりません。 + +自動検出は内部で静的メソッド `Configurator::detectDebugMode()` が行います。これは自分で呼ぶこともでき、たとえば configurator の外で開発モードを検出するのに使えます。IP アドレスやコンピュータ名の許可リストを任意で受け取り、現在のリクエストを開発モードで動かすべきかを返します。 + +```php +$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); +``` デバッグツール Tracy ============= -簡単なデバッグのために、優れたツール[Tracy |tracy:]を有効にします。開発環境ではエラーを視覚化し、本番環境では指定されたディレクトリにエラーをログ記録します。 +デバッグを楽にするために、素晴らしい道具 [Tracy |tracy:]を有効にします。開発モードではエラーを可視化し、本番モードでは指定したディレクトリにエラーを記録します。 ```php $this->configurator->enableTracy($this->rootDir . '/log'); @@ -138,19 +150,19 @@ $this->configurator->enableTracy($this->rootDir . '/log'); 一時ファイル ====== -NetteはDIコンテナ、RobotLoader、テンプレートなどにキャッシュを使用します。したがって、キャッシュが保存されるディレクトリへのパスを設定する必要があります。 +Nette は DI コンテナ、RobotLoader、テンプレートなどにキャッシュを使います。ですからキャッシュを保存するディレクトリのパスを設定する必要があります。 ```php $this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` -LinuxまたはmacOSでは、`log/` および `temp/` ディレクトリに[書き込み権限を設定 |nette:troubleshooting#ディレクトリ権限の設定]してください。 +Linux や macOS では、`log/` と `temp/` ディレクトリに[書き込みの権限 |nette:troubleshooting#ディレクトリの権限の設定]を設定してください。 RobotLoader =========== -通常、[RobotLoader |robot-loader:]を使用してクラスを自動的にロードしたいので、それを起動し、`Bootstrap.php` が配置されているディレクトリ(つまり `__DIR__`)とそのすべてのサブディレクトリからクラスをロードさせます。 +ふつうはクラスを [RobotLoader |robot-loader:]で自動的に読み込みたいので、それを起動し、`Bootstrap.php` があるディレクトリ(つまり `__DIR__`)とそのすべてのサブディレクトリからクラスを読み込ませます。 ```php $this->configurator->createRobotLoader() @@ -158,36 +170,38 @@ $this->configurator->createRobotLoader() ->register(); ``` -代替アプローチは、PSR-4に準拠しながら[Composer |best-practices:composer]経由でのみクラスをロードさせることです。 +別の方法として、PSR-4 に従って [Composer |best-practices:composer]だけでクラスを読み込むこともできます。 タイムゾーン ====== -Configuratorを使用して、デフォルトのタイムゾーンを設定できます。 +既定のタイムゾーンは configurator で設定できます。 ```php $this->configurator->setTimeZone('Europe/Prague'); ``` -DIコンテナの設定 -========= +DI コンテナの設定 +========== -ブートプロセスの一部は、アプリケーション全体の心臓部であるDIコンテナ、つまりオブジェクトのファクトリを作成することです。これは実際にはNetteによって生成され、キャッシュディレクトリに保存されるPHPクラスです。ファクトリはアプリケーションの主要なオブジェクトを生成し、設定ファイルを使用してそれらをどのように作成および設定するかを指示することで、アプリケーション全体の動作に影響を与えます。 +起動の過程の一部が DI コンテナ、つまりオブジェクトのファクトリの生成で、これはアプリケーション全体の心臓です。実際には Nette が生成してキャッシュディレクトリに保存する PHP のクラスです。このファクトリがアプリケーションの主要なオブジェクトを作り、設定ファイルでその作り方と設定の仕方を指示することで、アプリケーション全体の振る舞いに影響を与えます。 -設定ファイルは通常、[NEON |neon:format]形式で記述されます。別の章で、[設定できるすべてのこと |nette:configuring]について学びます。 +設定ファイルはふつう [NEON 形式 |neon:format]で書きます。[何を設定できるか |nette:configuring]は別の章で読めます。 .[tip] -開発環境では、コードまたは設定ファイルが変更されるたびにコンテナが自動的に更新されます。本番環境では、一度だけ生成され、パフォーマンスを最大化するために変更はチェックされません。 +開発モードでは、コードや設定ファイルが変わるたびにコンテナが自動的に更新されます。本番モードでは一度だけ生成され、性能を最大にするために変更は確認されません。 + +`createContainer()` がコンテナを構築してそのインスタンスを返すのに対し、`loadContainer()` メソッドは生成されたコンテナのクラス名だけを返すので、自分でインスタンス化できます。高度な場面で役立ちます。 -`addConfig()` を使用して設定ファイルをロードします。 +設定ファイルは `addConfig()` で読み込みます。 ```php $this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` -複数の設定ファイルを追加したい場合は、`addConfig()` 関数を複数回呼び出すことができます。 +設定ファイルをさらに足したい場合は、`addConfig()` 関数を何度も呼べます。 ```php $configDir = $this->rootDir . '/config'; @@ -198,17 +212,17 @@ if (PHP_SAPI === 'cli') { } ``` -`cli.php` という名前はタイプミスではありません。設定はPHPファイルに記述することもでき、そのファイルが配列として返します。 +`cli.php` という名前は誤りではありません。設定は、それを配列として返す PHP ファイルで書くこともできます。 -[`includes` セクション |dependency-injection:configuration#ファイルのインクルード]で他の設定ファイルを追加することもできます。 +ほかの設定ファイルは [`includes` セクション |dependency-injection:configuration#ファイルの読み込み]でも足せます。 -設定ファイルに同じキーを持つ要素が表示された場合、それらは上書きされるか、[配列の場合はマージ |dependency-injection:configuration#マージ]されます。後でインクルードされたファイルは、前のファイルよりも優先度が高くなります。`includes` セクションが記載されているファイルは、それにインクルードされているファイルよりも優先度が高くなります。 +設定ファイルに同じキーの項目が現れた場合、それらは上書きされるか、[配列なら統合されます |dependency-injection:configuration#統合]。あとで読み込まれたファイルのほうが、前のものより優先度が高くなります。`includes` セクションを持つファイルは、そこで読み込まれるファイルより優先度が高くなります。 静的パラメータ ------- -設定ファイルで使用されるパラメータは、[`parameters` セクション |dependency-injection:configuration#パラメータ]で定義でき、`addStaticParameters()` メソッド(エイリアス `addParameters()` を持つ)で渡す(または上書きする)こともできます。重要なのは、パラメータの値が異なると、追加のDIコンテナ、つまり追加のクラスが生成されることです。 +設定ファイルで使うパラメータは [`parameters` セクション |dependency-injection:configuration#パラメータ]で定義でき、`addStaticParameters()` メソッド(古い、いまは非推奨の別名は `addParameters()`)で渡す(または上書きする)こともできます。大事なのは、パラメータの値が違えば別の DI コンテナ、つまり別のクラスが生成されるという点です。 ```php $this->configurator->addStaticParameters([ @@ -216,13 +230,13 @@ $this->configurator->addStaticParameters([ ]); ``` -`projectId` パラメータは、設定で通常の `%projectId%` 表記で参照できます。 +`projectId` パラメータは、設定の中で標準の書き方 `%projectId%` で参照できます。 動的パラメータ ------- -コンテナに動的パラメータを追加することもできます。静的パラメータとは異なり、それらの異なる値は新しいDIコンテナの生成を引き起こしません。 +コンテナには動的パラメータも足せます。静的パラメータと違い、その値が違っても新しい DI コンテナは生成されません。 ```php $this->configurator->addDynamicParameters([ @@ -230,7 +244,7 @@ $this->configurator->addDynamicParameters([ ]); ``` -このようにして、例えば環境変数を簡単に追加でき、それらは設定で `%env.variable%` 表記で参照できます。 +こうすれば、たとえば環境変数を簡単に足せます。設定の中では `%env.variable%` の書き方で参照できます。 ```php $this->configurator->addDynamicParameters([ @@ -239,24 +253,25 @@ $this->configurator->addDynamicParameters([ ``` -デフォルトパラメータ ----------- +既定のパラメータ +-------- -設定ファイルでは、これらの静的パラメータを使用できます。 +設定ファイルでは次のパラメータが使えます。 -- `%appDir%` は `Bootstrap.php` ファイルを含むディレクトリへの絶対パスです。 -- `%wwwDir%` はエントリファイル `index.php` を含むディレクトリへの絶対パスです。 -- `%tempDir%` は一時ファイル用のディレクトリへの絶対パスです。 -- `%vendorDir%` はComposerがライブラリをインストールするディレクトリへの絶対パスです。 -- `%rootDir%` はプロジェクトのルートディレクトリへの絶対パスです。 -- `%debugMode%` はアプリケーションがデバッグモードであるかどうかを示します。 -- `%consoleMode%` はリクエストがコマンドライン経由で来たかどうかを示します。 +- `%appDir%` は `Bootstrap.php` ファイルがあるディレクトリへの絶対パスです +- `%wwwDir%` は入口のファイル `index.php` があるディレクトリへの絶対パスです +- `%tempDir%` は一時ファイル用のディレクトリへの絶対パスです +- `%vendorDir%` は Composer がライブラリをインストールするディレクトリへの絶対パスです +- `%rootDir%` はプロジェクトのルートディレクトリへの絶対パスです +- `%baseUrl%` はルートディレクトリへの絶対 URL です(実行時に解決される動的パラメータです) +- `%debugMode%` はアプリケーションがデバッグモードかどうかを示します +- `%consoleMode%` はリクエストがコマンドラインから来たかどうかを示します -インポートされたサービス ------------- +取り込まれるサービス +---------- -ここではさらに深く掘り下げます。DIコンテナの目的はオブジェクトを作成することですが、例外的に既存のオブジェクトをコンテナに挿入する必要がある場合があります。これを行うには、`imported: true` フラグを使用してサービスを定義します。 +さらに深く踏み込みましょう。DI コンテナの目的はオブジェクトを作ることですが、ときには既存のオブジェクトをコンテナに入れる必要が出てきます。そのためには `imported: true` フラグを付けてサービスを定義します。 ```neon services: @@ -265,7 +280,7 @@ services: imported: true ``` -そして、ブートストラップでオブジェクトをコンテナに挿入します。 +そして bootstrap でオブジェクトをコンテナに入れます。 ```php $this->configurator->addServices([ @@ -274,15 +289,15 @@ $this->configurator->addServices([ ``` -異なる環境 -===== +さまざまな環境 +======= -必要に応じて `Bootstrap` クラスを自由に変更してください。`bootWebApplication()` メソッドにパラメータを追加して、Webプロジェクトを区別することができます。または、他のメソッドを追加することもできます。例えば、単体テスト用の環境を初期化する `bootTestEnvironment()`、コマンドラインから呼び出されるスクリプト用の `bootConsoleApplication()` などです。 +`Bootstrap` クラスは必要に応じて自由に書き換えてください。ウェブのプロジェクトを見分けるために `bootWebApplication()` メソッドにパラメータを足せます。あるいはメソッドを増やしてもよいでしょう。ユニットテスト用に環境を初期化する `bootTestEnvironment()`、コマンドラインから呼ばれるスクリプト用の `bootConsoleApplication()` などです。 ```php public function bootTestEnvironment(): Nette\DI\Container { - Tester\Environment::setup(); // Nette Testerの初期化 + Tester\Environment::setup(); // Nette Tester の初期化 $this->setupContainer(); return $this->configurator->createContainer(); } diff --git a/application/ja/components.texy b/application/ja/components.texy index e8f6b5e4f1..faf42836b1 100644 --- a/application/ja/components.texy +++ b/application/ja/components.texy @@ -1,29 +1,29 @@ -インタラクティブコンポーネント -*************** +インタラクティブなコンポーネント +****************
    -コンポーネントは、ページに挿入する独立した再利用可能なオブジェクトです。フォーム、データグリッド、投票など、繰り返し使用する意味のあるものであれば何でもかまいません。ここでは以下について説明します。 +コンポーネントは、ページに埋め込む独立した再利用できるオブジェクトです。フォーム、データグリッド、アンケートなど、繰り返し使う意味のあるものなら何でもかまいません。ここでは次のことを扱います。 -- コンポーネントの使用方法 -- コンポーネントの作成方法 +- コンポーネントの使い方 +- その書き方 - シグナルとは何か
    -Netteには組み込みのコンポーネントシステムがあります。DelphiやASP.NET Web Formsを知っている古い世代の方々には馴染みがあるかもしれません。ReactやVue.jsも、遠いながらも似たようなものに基づいています。しかし、PHPフレームワークの世界では、これはユニークな機能です。 +Nette には組み込みのコンポーネントのしくみがあります。Delphi や ASP.NET Web Forms の熟練者には似たものが馴染み深いかもしれません。React や Vue.js も、遠く似た考えの上に築かれています。とはいえ PHP のフレームワークの世界では、これは他にない機能です。 -一方、コンポーネントはアプリケーション開発へのアプローチに根本的な影響を与えます。事前に準備されたユニットからページを組み立てることができます。管理画面にデータグリッドが必要ですか?Nette用のオープンソースアドオン(コンポーネントだけではありません)のリポジトリである[Componette |https://componette.org/search/component]で見つけて、Presenterに簡単に追加できます。 +同時にコンポーネントは、アプリケーション開発への向き合い方を根本から変えます。あらかじめ用意された部品からページを組み立てられます。管理画面にデータグリッドが必要ですか。Nette 向けのオープンソースのアドオン(コンポーネントに限りません)を集めた [Componette |https://componette.org/search/component]で見つけて、プレゼンターに差し込むだけです。 -Presenterには任意の数のコンポーネントを含めることができます。そして、一部のコンポーネントには他のコンポーネントを挿入できます。これにより、Presenterをルートとするコンポーネントツリーが作成されます。 +プレゼンターにはいくつでもコンポーネントを組み込めます。そしてコンポーネントの中にほかのコンポーネントを埋め込めます。こうしてプレゼンターを根とするコンポーネントの木ができます。 ファクトリメソッド ========= -コンポーネントはどのようにPresenterに挿入され、その後使用されるのでしょうか?通常はファクトリメソッドを使用します。 +コンポーネントはどうプレゼンターに差し込まれ、どう使われるのでしょうか。ふつうはファクトリメソッドを通じてです。 -コンポーネントファクトリは、コンポーネントが実際に必要になったときにのみ作成する(遅延/オンデマンド)エレガントな方法です。全体の魔法は、`createComponent()` という名前のメソッドを実装することにあります。ここで `` は作成されるコンポーネントの名前であり、このメソッドがコンポーネントを作成して返します。 +コンポーネントのファクトリは、本当に必要になったときにだけコンポーネントを作る(遅延、オンデマンド)優雅な方法です。魔法のすべては `createComponent()` という名前のメソッドを実装することにあります。`` は作られるコンポーネントの名前で、このメソッドがそれを作って返します。 ```php .{file:DefaultPresenter.php} class DefaultPresenter extends Nette\Application\UI\Presenter @@ -31,27 +31,27 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } ``` -すべてのコンポーネントが個別のメソッドで作成されるため、コードがより明確になります。 +すべてのコンポーネントが別々のメソッドで作られるので、コードが分かりやすくなります。 .[note] -コンポーネント名は常に小文字で始まりますが、メソッド名では大文字で記述されます。 +コンポーネントの名前は、メソッド名では大文字で始まっていても、常に小文字で始まります。 -ファクトリは直接呼び出すことはありません。コンポーネントを初めて使用するときに自動的に呼び出されます。これにより、コンポーネントは適切なタイミングで、実際に必要な場合にのみ作成されます。コンポーネントを使用しない場合(例えば、ページの一部のみが転送されるAJAXリクエストの場合や、テンプレートのキャッシュの場合)、コンポーネントはまったく作成されず、サーバーのパフォーマンスを節約できます。 +ファクトリを直接呼ぶことは決してありません。コンポーネントを最初に使ったときに自動的に呼ばれます。おかげでコンポーネントは適切な瞬間に、しかも本当に必要な場合にだけ作られます。コンポーネントを使わなければ(ページの一部だけを転送する AJAX のリクエストや、テンプレートをキャッシュする場合など)まったく作られないので、サーバーの性能を節約できます。 ```php .{file:DefaultPresenter.php} -// コンポーネントにアクセスし、初めての場合は -// それを作成する createComponentPoll() が呼び出されます +// コンポーネントにアクセスします。それが最初なら +// createComponentPoll() が呼ばれて作られます $poll = $this->getComponent('poll'); -// 代替構文: $poll = $this['poll']; +// 別の書き方: $poll = $this['poll']; ``` -テンプレートでは、[{control} |#レンダリング] タグを使用してコンポーネントを描画できます。したがって、コンポーネントを手動でテンプレートに渡す必要はありません。 +テンプレートでは [{control} |#描画]タグでコンポーネントを描けます。ですからコンポーネントを手でテンプレートに渡す必要はありません。 ```latte

    投票してください

    @@ -59,21 +59,26 @@ $poll = $this->getComponent('poll'); {control poll} ``` +.[tip] +数が変わるコンポーネントを動的に作るには [Multiplier |multiplier]を使います。 -ハリウッドスタイル -========= +`createComponent()` のファクトリメソッドはプレゼンターだけのものではありません。同じやり方でコンポーネントの中にコンポーネントを入れ子にし、木に組み立てられます。たとえばコンポーネントの中で別々に描かれるフォームに便利です。 + + +ハリウッド流 +====== -コンポーネントは通常、私たちがハリウッドスタイルと呼ぶのが好きな新鮮なテクニックを使用します。映画のオーディション参加者がよく聞く決まり文句をきっとご存知でしょう:「こちらから連絡しますので、電話しないでください」。まさにそれです。 +コンポーネントはふつう、私たちがハリウッド流と呼びたくなる新鮮な手法を使います。映画のオーディションの参加者がよく耳にする決まり文句をご存じでしょう。「こちらから連絡します、あなたからはしないでください。」まさにそういうことです。 -Netteでは、常に何かを尋ねる(「フォームは送信されましたか?」、「有効でしたか?」または「ユーザーはこのボタンを押しましたか?」)代わりに、フレームワークに「それが起こったら、このメソッドを呼び出して」と伝え、残りの作業を任せます。JavaScriptでプログラミングしている場合、このプログラミングスタイルには精通しているでしょう。特定のイベントが発生したときに呼び出される関数を記述します。そして、言語は適切なパラメータを渡します。 +Nette では、絶えず問い続ける(「フォームは送信されたか」「それは正しかったか」「ユーザーはこのボタンを押したか」)代わりに、フレームワークに「これが起きたら、このメソッドを呼んで」と伝えて、あとは任せます。JavaScript でプログラムしているなら、この書き方はよくご存じでしょう。ある出来事が起きたときに呼ばれる関数を書き、言語が適切なパラメータをそこに渡してくれます。 -これはアプリケーションの作成方法を完全に変えます。フレームワークに任せられるタスクが多ければ多いほど、あなたの作業は少なくなります。そして、見落とす可能性のあることも少なくなります。 +これはアプリケーションを書くときの見方をすっかり変えます。フレームワークに任せられる仕事が多いほど、あなたの手間は減ります。そして見落としも減ります。 -コンポーネントの作成 +コンポーネントを書く ========== -コンポーネントという用語は、通常、[api:Nette\Application\UI\Control] クラスの子孫を意味します。(したがって、「コントロール」という用語を使用する方が正確ですが、日本語では「コントロール」は他の意味合いを持つことがあり、「コンポーネント」の方が一般的になりました。)Presenter自体 [api:Nette\Application\UI\Presenter] も、ちなみに `Control` クラスの子孫です。 +コンポーネントという語は、ふつう [api:Nette\Application\UI\Control]クラスの子孫を指します。(「control」という語のほうが正確ですが、言語によっては別の意味を持つので、「component」のほうが定着しました。)プレゼンター [api:Nette\Application\UI\Presenter]自身も `Control` クラスの子孫です。 ```php .{file:PollControl.php} use Nette\Application\UI\Control; @@ -84,22 +89,22 @@ class PollControl extends Control ``` -レンダリング -====== +描画 +=== -コンポーネントをレンダリングするために `{control componentName}` タグが使用されることはすでに知っています。これは実際にはコンポーネントの `render()` メソッドを呼び出し、そこでレンダリングを処理します。Presenterとまったく同じように、`$this->template` 変数に [Latteテンプレート|templates] があり、それにパラメータを渡します。Presenterとは異なり、テンプレートファイルを指定してレンダリングさせる必要があります。 +コンポーネントを描くのに `{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()` メソッドにパラメータを渡すことができます。 +`{control}` タグは `render()` メソッドにパラメータを渡せます。 ```latte {control poll $id, $message} @@ -112,7 +117,7 @@ public function render(int $id, string $message): void } ``` -コンポーネントがいくつかの部分で構成され、それらを別々にレンダリングしたい場合があります。それぞれについて、独自のレンダリングメソッドを作成します。ここでは例として `renderPaginator()` を作成します。 +コンポーネントが、別々に描きたいいくつかの部分から成ることもあります。それぞれについて独自の描画メソッドを作ります。この例では `renderPaginator()` です。 ```php .{file:PollControl.php} public function renderPaginator(): void @@ -121,69 +126,69 @@ public function renderPaginator(): void } ``` -そして、テンプレートで次のように呼び出します。 +テンプレートでは次のように呼び出します。 ```latte {control poll:paginator} ``` -よりよく理解するために、このタグがどのようにPHPに変換されるかを知っておくと良いでしょう。 +理解を深めるために、このタグが PHP のコードにどう変換されるかを知っておくとよいでしょう。 ```latte {control poll} {control poll:paginator 123, 'hello'} ``` -は次のように変換されます。 +は次に変換されます。 ```php $control->getComponent('poll')->render(); $control->getComponent('poll')->renderPaginator(123, 'hello'); ``` -`getComponent()` メソッドは `poll` コンポーネントを返し、このコンポーネントに対して `render()` メソッド、またはタグのコロンの後に異なるレンダリング方法が指定されている場合は `renderPaginator()` メソッドを呼び出します。 +`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(); ``` -コンポーネントは、Presenterと同様に、いくつかの便利な変数を自動的にテンプレートに渡します。 +コンポーネントもプレゼンターと同じく、いくつかの役立つ変数をテンプレートに自動的に渡します。 -- `$basePath` はルートディレクトリへの絶対URLパスです(例:`/eshop`) -- `$baseUrl` はルートディレクトリへの絶対URLです(例:`http://localhost/eshop`) +- `$basePath` はルートディレクトリへの絶対 URL パスです(たとえば `/eshop`) +- `$baseUrl` はルートディレクトリへの絶対 URL です(たとえば `http://localhost/eshop`) - `$user` は[ユーザーを表す |security:authentication]オブジェクトです -- `$presenter` は現在のPresenterです +- `$presenter` は現在のプレゼンターです - `$control` は現在のコンポーネントです -- `$flashes` は `flashMessage()` 関数によって送信された[メッセージ |#フラッシュメッセージ]の配列です +- `$flashes` は `flashMessage()` 関数で送られた[メッセージ |#フラッシュメッセージ]の配列です シグナル ==== -Netteアプリケーションのナビゲーションは、`Presenter:action` のペアへのリンクまたはリダイレクトに基づいていることはすでに知っています。しかし、**現在のページ**でアクションを実行したいだけの場合はどうでしょうか?例えば、テーブルの列の並び替えを変更する、項目を削除する、ライト/ダークモードを切り替える、フォームを送信する、投票するなどです。 +Nette のアプリケーションでの移動が、`Presenter:action` の組へのリンクやリダイレクトから成ることはすでに見ました。しかし**現在のページ**で何か処理をしたいだけの場合はどうでしょうか。たとえば表の列の並べ替えを変える、項目を削除する、ライト/ダークモードを切り替える、フォームを送信する、アンケートに投票する、などです。 -この種のリクエストはシグナルと呼ばれます。そして、アクションが `action()` または `render()` メソッドを呼び出すのと同様に、シグナルは `handle()` メソッドを呼び出します。アクション(またはビュー)という概念は純粋にPresenterに関連していますが、シグナルはすべてのコンポーネントに関係します。したがって、`UI\Presenter` は `UI\Control` の子孫であるため、Presenterにも関係します。 +この種のリクエストをシグナルと呼びます。アクションが `action()` や `render()` メソッドを呼ぶのと同じように、シグナルは `handle()` メソッドを呼びます。アクション(やビュー)の考え方が純粋にプレゼンターに関わるのに対し、シグナルはすべてのコンポーネントに関わります。`UI\Presenter` は `UI\Control` の子孫なので、プレゼンターにも関わります。 ```php public function handleClick(int $x, int $y): void @@ -192,36 +197,36 @@ public function handleClick(int $x, int $y): void } ``` -シグナルを呼び出すリンクは、通常の方法で作成します。つまり、テンプレートでは `n:href` 属性または `{link}` タグを使用し、コードでは `link()` メソッドを使用します。詳細については、[URLリンクの作成 |creating-links#シグナルへのリンク]の章を参照してください。 +シグナルを呼ぶリンクはいつもどおりに作ります。つまりテンプレートでは `n:href` 属性か `{link}` タグ、コードでは `link()` メソッドです。詳しくは [URL リンクの作成 |creating-links#シグナルへのリンク]の章をご覧ください。 ```latte ここをクリック ``` -シグナルは常に現在のPresenterとアクションで呼び出され、別のPresenterや別のアクションで呼び出すことはできません。 +シグナルは常に現在のプレゼンターとアクションで呼ばれます。別のプレゼンターやアクションで呼ぶことはできません。 -したがって、シグナルは元のリクエストとまったく同じようにページの再読み込みを引き起こしますが、さらに適切なパラメータを持つシグナル処理メソッドを呼び出します。メソッドが存在しない場合、[api:Nette\Application\UI\BadSignalException] 例外がスローされ、ユーザーには403 Forbiddenエラーページとして表示されます。 +ですからシグナルは、もとのリクエストと同じようにページを読み込み直しつつ、加えてシグナルを処理するメソッドを適切なパラメータで呼びます。そのメソッドがなければ [api:Nette\Application\UI\BadSignalException]例外が投げられ、ユーザーには 403 Forbidden のエラーページとして表示されます。 -スニペットとAJAX -========== +スニペットと AJAX +=========== -シグナルはAJAXを少し思い出させるかもしれません:現在のページで呼び出されるハンドラです。そして、その通りです。シグナルは実際にはAJAXを使用して呼び出されることが多く、その後、変更されたページの部分のみがブラウザに転送されます。つまり、いわゆるスニペットです。詳細については、[AJAX専用ページ |ajax]を参照してください。 +シグナルは AJAX を少し思い起こさせるかもしれません。現在のページで呼ばれるハンドラだからです。そのとおりで、シグナルは実際に AJAX で呼ばれることが多く、そのあとページの変わった部分だけがブラウザに転送されます。これをスニペットと呼びます。詳しくは [AJAX のページ |ajax]をご覧ください。 フラッシュメッセージ ========== -コンポーネントには、Presenterとは独立した独自のフラッシュメッセージストレージがあります。これらは、例えば操作の結果を通知するメッセージです。フラッシュメッセージの重要な特徴は、リダイレクト後もテンプレートで利用できることです。表示後もさらに30秒間有効です。例えば、転送エラーのためにユーザーがページを更新した場合でも、メッセージはすぐには消えません。 +コンポーネントは、プレゼンターとは独立した自分のフラッシュメッセージの保管場所を持ちます。これはたとえば操作の結果を知らせるメッセージです。フラッシュメッセージの大事な性質は、リダイレクト後もテンプレートで使えることです。一度表示されたあとも、さらに 30 秒は有効なままです。たとえば通信のエラーでユーザーがページを再読み込みしても、メッセージがすぐ消えることはありません。 -送信は [flashMessage |api:Nette\Application\UI\Control::flashMessage()] メソッドによって処理されます。最初のパラメータはメッセージのテキストまたはメッセージを表す `stdClass` オブジェクトです。オプションの2番目のパラメータはそのタイプ(error、warning、infoなど)です。`flashMessage()` メソッドは、フラッシュメッセージのインスタンスを `stdClass` オブジェクトとして返し、これに追加情報を追加できます。 +送信は [flashMessage |api:Nette\Application\UI\Control::flashMessage()]メソッドが担当します。第 1 パラメータはメッセージの本文(`string`、`Stringable`)か、メッセージを表す `stdClass` オブジェクトです。省略可能な第 2 パラメータはその種類(error、warning、info など)です。`flashMessage()` メソッドはフラッシュメッセージのインスタンスを `stdClass` オブジェクトとして返すので、さらに情報を足せます。 ```php -$this->flashMessage('項目が削除されました。'); -$this->redirect(/* ... */); // そしてリダイレクトします +$this->flashMessage('項目を削除しました。'); +$this->redirect(/* ... */); // そしてリダイレクト ``` -これらのメッセージは、テンプレートでは `$flashes` 変数で `stdClass` オブジェクトとして利用できます。これらには `message`(メッセージテキスト)、`type`(メッセージタイプ)プロパティが含まれ、前述のユーザー情報を含むこともできます。例えば、次のようにレンダリングします。 +これらのメッセージは、テンプレートの `$flashes` 変数に `stdClass` オブジェクトとして入っていて、`message`(メッセージの本文)、`type`(メッセージの種類)のプロパティを持ち、先ほど触れたユーザーの情報を含むこともあります。たとえば次のように描きます。 ```latte {foreach $flashes as $flash} @@ -230,84 +235,84 @@ $this->redirect(/* ... */); // そしてリダイレクトします ``` -シグナル後のリダイレクト -============ +シグナルの処理後のリダイレクト +=============== -コンポーネントのシグナル処理後には、しばしばリダイレクトが続きます。これはフォームの場合と似ています。フォーム送信後もリダイレクトして、ブラウザでページを更新したときにデータが再送信されないようにします。 +コンポーネントのシグナルの処理のあとには、リダイレクトが続くことがよくあります。フォームと同じで、送信後にはリダイレクトして、ブラウザでページを再読み込みしてもデータが再送信されないようにします。 ```php -$this->redirect('this') // 現在のPresenterとアクションにリダイレクトします +$this->redirect('this'); // 現在のプレゼンターとアクションにリダイレクトします ``` -コンポーネントは再利用可能な要素であり、通常は特定のPresenterへの直接的な依存関係を持つべきではないため、`redirect()` および `link()` メソッドはパラメータを自動的にコンポーネントのシグナルとして解釈します。 +コンポーネントは再利用できる部品で、ふつう特定のプレゼンターへの直接のつながりを持つべきではないので、`redirect()` と `link()` メソッドはパラメータを自動的にコンポーネントのシグナルと解釈します。 ```php -$this->redirect('click') // 同じコンポーネントの 'click' シグナルにリダイレクトします +$this->redirect('click'); // 同じコンポーネントの 'click' シグナルにリダイレクトします ``` -別のPresenterやアクションにリダイレクトする必要がある場合は、Presenterを介して行うことができます。 +別のプレゼンターやアクションにリダイレクトする必要があるなら、プレゼンターを通して行えます。 ```php -$this->getPresenter()->redirect('Product:show'); // 別のPresenter/アクションにリダイレクトします +$this->getPresenter()->redirect('Product:show'); // 別のプレゼンター/アクションにリダイレクトします ``` -パーシステントパラメータ -============ +永続パラメータ +======= -パーシステントパラメータは、異なるリクエスト間でコンポーネントの状態を維持するために使用されます。その値は、リンクをクリックした後も同じままです。セッションデータとは異なり、URLで転送されます。そして、これは完全に自動的に行われ、同じページの他のコンポーネントで作成されたリンクも含みます。 +永続パラメータは、コンポーネントの状態をリクエストをまたいで保つために使います。その値はリンクをクリックしたあとも変わりません。セッションのデータと違い、URL で運ばれます。しかもそれは完全に自動的に起こり、同じページのほかのコンポーネントで作られたリンクにも及びます。 -例えば、コンテンツをページ分割するためのコンポーネントがあるとします。このようなコンポーネントはページ上に複数存在する可能性があります。そして、リンクをクリックした後、すべてのコンポーネントが現在のページにとどまるようにしたいとします。したがって、ページ番号(`page`)をパーシステントパラメータにします。 +たとえば内容をページ分けするコンポーネントがあるとします。そうしたコンポーネントがページに複数あるかもしれません。そしてリンクをクリックしたあとも、すべてのコンポーネントが今のページにとどまってほしいとします。ですからページ番号(`page`)を永続パラメータにします。 -Netteでパーシステントパラメータを作成するのは非常に簡単です。パブリックプロパティを作成し、属性でマークするだけです。(以前は `/** @persistent */` が使用されていました) +Nette で永続パラメータを作るのはきわめて簡単です。public のプロパティを作り、アトリビュートで印を付けるだけです(以前は `/** @persistent */` が使われていました)。 ```php -use Nette\Application\Attributes\Persistent; // この行は重要です +use Nette\Application\Attributes\Persistent; // この行が大事です class PaginatingControl extends Control { #[Persistent] - public int $page = 1; // publicである必要があります + public int $page = 1; // public でなければなりません } ``` -プロパティにはデータ型(例:`int`)を指定することをお勧めします。また、デフォルト値を指定することもできます。パラメータ値は[検証 |#パーシステントパラメータの検証]できます。 +プロパティにはデータ型(`int` など)を指定することをおすすめしますし、既定値も与えられます。パラメータの値は[検証できます |#永続パラメータの検証]。 -リンクを作成するときに、パーシステントパラメータの値を変更できます。 +リンクを作るとき、永続パラメータの値は変えられます。 ```latte 次へ ``` -または、*リセット*することもできます。つまり、URLから削除します。その後、デフォルト値を取ります。 +あるいは*リセット*して URL から取り除けます。その場合は既定値になります。 ```latte リセット ``` -パーシステントコンポーネント -============== +永続コンポーネント +========= -パラメータだけでなく、コンポーネントもパーシステントにすることができます。このようなコンポーネントでは、そのパーシステントパラメータはPresenterの異なるアクション間、または複数のPresenter間でも転送されます。パーシステントコンポーネントは、Presenterクラスのアノテーションでマークします。例えば、このようにして `calendar` および `poll` コンポーネントをマークします。 +パラメータだけでなく、コンポーネントも永続にできます。その永続パラメータは、プレゼンターの異なるアクションのあいだや、複数のプレゼンターのあいだでも引き継がれます。永続コンポーネントには、プレゼンターのクラスにアトリビュートで印を付けます。たとえば `calendar` と `poll` のコンポーネントには次のように印を付けます。 ```php -/** - * @persistent(calendar, poll) - */ +use Nette\Application\Attributes\Persistent; + +#[Persistent('calendar', 'poll')] class DefaultPresenter extends Nette\Application\UI\Presenter { } ``` -これらのコンポーネント内のサブコンポーネントをマークする必要はありません。それらもパーシステントになります。 +これらのコンポーネントの中のサブコンポーネントには印を付ける必要がありません。それらも永続になります。 -PHP 8では、属性を使用してパーシステントコンポーネントをマークすることもできます。 +古いアノテーション `@persistent` もまだ動きますが、非推奨で警告を出します。 ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -317,32 +322,32 @@ class DefaultPresenter extends Nette\Application\UI\Presenter 依存関係を持つコンポーネント ============== -それらを使用するPresenterを「汚す」ことなく、依存関係を持つコンポーネントを作成するにはどうすればよいでしょうか?NetteのDIコンテナの賢い機能のおかげで、従来のサービスを使用する場合と同様に、ほとんどの作業をフレームワークに任せることができます。 +依存関係を持つコンポーネントを、それを使うプレゼンターを「散らかさず」に作るにはどうすればよいでしょうか。Nette の DI コンテナの賢い機能のおかげで、通常のサービスと同じく、ほとんどの仕事をフレームワークに任せられます。 -例として、`PollFacade` サービスに依存するコンポーネントを取り上げましょう。 +`PollFacade` サービスに依存するコンポーネントを例に取りましょう。 ```php class PollControl extends Control { public function __construct( - private int $id, // コンポーネントを作成する投票のID + private int $id, // コンポーネントを作る対象のアンケートの ID private PollFacade $facade, ) { } public function handleVote(int $voteId): void { - $this->facade->vote($id, $voteId); + $this->facade->vote($this->id, $voteId); // ... } } ``` -従来のサービスを作成する場合、問題はありませんでした。すべての依存関係の受け渡しは、DIコンテナによって目に見えない形で処理されます。しかし、コンポーネントの場合、通常はPresenterの[#ファクトリメソッド] `createComponent…()` で直接新しいインスタンスを作成します。しかし、すべてのコンポーネントのすべての依存関係をPresenterに渡してからコンポーネントに渡すのは面倒です。そして、書かれたコードの量も… +通常のサービスを書いているなら、議論の余地はありません。DI コンテナがすべての依存関係の受け渡しを見えないところでこなしてくれます。しかしコンポーネントの場合、ふつうは[ファクトリメソッド |#ファクトリメソッド] `createComponent…()` の中で、プレゼンターの中に新しいインスタンスを作って扱います。とはいえ、すべてのコンポーネントのすべての依存関係を、コンポーネントに渡すためだけにプレゼンターへ渡すのは面倒です。しかも書くコードの量ときたら……。 -論理的な疑問は、なぜコンポーネントを従来のサービスとして登録し、Presenterに渡してから `createComponent…()` メソッドで返さないのかということです。しかし、このアプローチは不適切です。なぜなら、コンポーネントを複数回作成できるようにしたいからです。 +当然の疑問として、コンポーネントを通常のサービスとして登録し、プレゼンターに渡して `createComponent…()` メソッドで返せばよいのでは、と思うかもしれません。しかしこのやり方はふさわしくありません。必要ならコンポーネントを何度も作れるようにしたいからです。 -正しい解決策は、コンポーネントのファクトリ、つまりコンポーネントを作成するクラスを作成することです。 +正しい解は、コンポーネントのファクトリ、つまりコンポーネントを作ってくれるクラスを書くことです。 ```php class PollControlFactory @@ -359,14 +364,14 @@ class PollControlFactory } ``` -このようにして、ファクトリを構成内のコンテナに登録します。 +このファクトリを設定でコンテナに登録します。 ```neon services: - PollControlFactory ``` -そして最後に、Presenterで使用します。 +そして最後にプレゼンターで使います。 ```php class PollPresenter extends Nette\Application\UI\Presenter @@ -378,13 +383,13 @@ class PollPresenter extends Nette\Application\UI\Presenter protected function createComponentPollControl(): PollControl { - $pollId = 1; // パラメータを渡すことができます + $pollId = 1; // 自分のパラメータを渡せます return $this->pollControlFactory->create($pollId); } } ``` -素晴らしいことに、Nette DIはそのような単純なファクトリを[生成 |dependency-injection:factory]できるので、そのコード全体を書く代わりに、そのインターフェースを書くだけで済みます。 +素晴らしいのは、Nette DI がこうした単純なファクトリを[生成できる |dependency-injection:factory]ことです。ですからそのコード全体を書く代わりに、インターフェースを書くだけで済みます。 ```php interface PollControlFactory @@ -393,21 +398,21 @@ interface PollControlFactory } ``` -これで完了です。Netteは内部的にこのインターフェースを実装し、Presenterに渡します。そこで使用できます。魔法のように、パラメータ `$id` と `PollFacade` クラスのインスタンスをコンポーネントに追加します。 +これだけです。Nette が内部でこのインターフェースを実装し、プレゼンターに注入してくれるので、そこで使えます。`$id` パラメータと `PollFacade` クラスのインスタンスを、魔法のようにコンポーネントに足してくれます。 コンポーネントの詳細 ========== -Nette Applicationのコンポーネントは、Webアプリケーションの再利用可能な部分であり、ページに挿入され、この章全体で扱われています。そのようなコンポーネントには、具体的にどのような機能があるのでしょうか? +Nette Application のコンポーネントは、ページに埋め込むウェブアプリケーションの再利用できる部分で、この章はまるごとそれに捧げられてきました。そうしたコンポーネントには、正確には何ができるのでしょうか。 -1) テンプレートでレンダリング可能 -2) AJAXリクエスト時に[どの部分 |ajax#スニペット]をレンダリングするかを知っている(スニペット) -3) 状態をURLに保存する機能がある(パーシステントパラメータ) -4) ユーザーアクションに応答する機能がある(シグナル) -5) 階層構造を作成する(ルートはPresenter) +1) テンプレートに描ける +2) AJAX のリクエストのときに[自分のどの部分を |ajax#スニペット]描くべきかを知っている(スニペット) +3) 自分の状態を URL に保存できる(永続パラメータ) +4) ユーザーの操作に反応できる(シグナル) +5) 階層的な構造を作る(根はプレゼンター) -これらの各機能は、継承ラインのいずれかのクラスによって処理されます。レンダリング(1 + 2)は[api:Nette\Application\UI\Control]が担当し、[ライフサイクル |presenters#Presenterのライフサイクル]への統合(3, 4)は[api:Nette\Application\UI\Component]クラスが担当し、階層構造の作成(5)は[ContainerおよびComponent |component-model:]クラスが担当します。 +これらの機能は、それぞれ継承の系列の中のいずれかのクラスが担当します。描画(1 + 2)は [api:Nette\Application\UI\Control]が、[ライフサイクル |presenters#プレゼンターのライフサイクル]への統合(3、4)は [api:Nette\Application\UI\Component]クラスが、階層的な構造の生成(5)は [Container と Component |component-model:]のクラスが担当します。 ``` Nette\ComponentModel\Component { IComponent } @@ -428,12 +433,12 @@ Nette\ComponentModel\Component { IComponent } [* lifecycle-component.svg *] *** *コンポーネントのライフサイクル* .<> -パーシステントパラメータの検証 ---------------- +永続パラメータの検証 +---------- -URLから受け取った[#パーシステントパラメータ]の値は、`loadState()` メソッドによってプロパティに書き込まれます。また、プロパティで指定されたデータ型と一致するかどうかもチェックし、一致しない場合は404エラーで応答し、ページは表示されません。 +URL から受け取った[永続パラメータ |#永続パラメータ]の値は、`loadState()` メソッドがプロパティに書き込みます。あわせてプロパティに指定されたデータ型と合うかも確認し、合わなければ 404 のエラーで応え、ページは表示されません。 -パーシステントパラメータは、ユーザーがURLで簡単に上書きできるため、決して盲目的に信用しないでください。例えば、このようにしてページ番号 `$this->page` が0より大きいかどうかを検証します。適切な方法は、前述の `loadState()` メソッドをオーバーライドすることです。 +永続パラメータを決して盲信しないでください。ユーザーに URL で簡単に書き換えられます。たとえばページ番号 `$this->page` が 0 より大きいかを次のように確かめます。適切な方法が、先ほどの `loadState()` メソッドの上書きです。 ```php class PaginatingControl extends Control @@ -444,7 +449,7 @@ class PaginatingControl extends Control public function loadState(array $params): void { parent::loadState($params); // ここで $this->page が設定されます - // 値の独自のチェックが続きます: + // 続いて独自の値のチェック: if ($this->page < 1) { $this->error(); } @@ -452,29 +457,43 @@ class PaginatingControl extends Control } ``` -逆のプロセス、つまりパーシステントプロパティから値を収集するプロセスは、`saveState()` メソッドが担当します。 +逆の処理、つまり永続プロパティから値を集めるのは `saveState()` メソッドが担当します。 + + +プレゼンターへの接続 +---------- + +コンポーネントがプレゼンターの階層の一部になった瞬間、`$onAnchor` 配列に入っているコールバックが呼ばれます。その時点からコンポーネントはプレゼンターを使えるようになり、安全にリンクを作ったり、永続パラメータを読んだりできます。 + +```php +$control->onAnchor[] = function ($control): void { + // コンポーネントがプレゼンターを使えるようになりました +}; +``` シグナルの詳細 ------- -シグナルは、元のリクエストとまったく同じようにページの再読み込みを引き起こし(AJAXで呼び出された場合を除く)、`signalReceived($signal)` メソッドを呼び出します。`Nette\Application\UI\Component` クラスのデフォルト実装は、`handle{signal}` という単語で構成されるメソッドを呼び出そうとします。その後の処理は、特定のオブジェクト次第です。`Component` から継承するオブジェクト(つまり `Control` と `Presenter`)は、適切なパラメータを持つ `handle{signal}` メソッドを呼び出そうとすることで応答します。 +シグナルは(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]を投げます。 -言い換えれば、`handle{signal}` 関数の定義とリクエストで渡されたすべてのパラメータが取得され、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` インターフェースを実装し、コンポーネントツリーに接続されている任意のコンポーネント、Presenter、またはオブジェクトが受信できます。 +シグナルは、`SignalReceiver` インターフェースを実装してコンポーネントの木につながっている任意のコンポーネント、プレゼンター、オブジェクトが受け取れます。 -シグナルの主な受信者は、`Presenter` および `Control` から継承するビジュアルコンポーネントになります。シグナルは、オブジェクトに何かをするように指示する合図として機能することを目的としています。投票はユーザーからの投票をカウントする必要があり、ニュースブロックは展開して2倍のニュースを表示する必要があり、フォームは送信されてデータを処理する必要がある、などです。 +シグナルの主な受け手は `Presenter` と、`Control` を継承した画面のコンポーネントになるでしょう。シグナルは、オブジェクトに何かをすべきだと知らせる合図として働きます。アンケートはユーザーの票を数えるべき、ニュースの欄は広がって 2 倍のニュースを表示すべき、フォームが送信されたのでデータを処理すべき、といった具合です。 -シグナルのURLは、[Component::link() |api:Nette\Application\UI\Component::link()] メソッドを使用して作成します。`$destination` パラメータとして文字列 `{signal}!` を渡し、`$args` としてシグナルに渡したい引数の配列を渡します。シグナルは常に現在のPresenterとアクションで現在のパラメータとともに呼び出され、シグナルパラメータのみが追加されます。さらに、最初に**シグナルを指定するパラメータ `?do`** が追加されます。 +シグナルの URL は [Component::link() |api:Nette\Application\UI\Component::link()]メソッドで作ります。`$destination` パラメータには文字列 `{signal}!` を、`$args` にはシグナルに渡したい引数の配列を渡します。シグナルは常に現在のプレゼンターとアクションで、現在のパラメータとともに呼ばれ、シグナルのパラメータが足されるだけです。さらに**シグナルを指定するパラメータ `?do`** が足されます。 -その形式は `{signal}` または `{signalReceiver}-{signal}` のいずれかです。`{signalReceiver}` はPresenter内のコンポーネントの名前です。したがって、コンポーネント名にハイフンを使用することはできません。ハイフンはコンポーネント名とシグナルを区切るために使用されますが、このようにして複数のコンポーネントをネストすることが可能です。 +その形式は `{signal}` か `{signalReceiver}-{signal}` です。`{signalReceiver}` はプレゼンターの中のコンポーネントの名前です。ですからコンポーネント名にハイフンは使えません。コンポーネント名とシグナルを分けるのに使われるからです。とはいえ、この方法で複数のコンポーネントを入れ子にできます。 -[isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] メソッドは、コンポーネント(最初の引数)がシグナル(2番目の引数)の受信者であるかどうかを検証します。2番目の引数は省略できます。その場合、コンポーネントが任意のシグナルの受信者であるかどうかを判断します。2番目のパラメータとして `true` を指定すると、指定されたコンポーネントだけでなく、その子孫のいずれかが受信者であるかどうかも検証できます。 +[isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()]メソッドは、コンポーネント(第 1 引数)がシグナル(第 2 引数)の受け手かどうかを調べます。第 2 引数は省略でき、その場合はそのコンポーネントが何らかのシグナルの受け手かを調べます。第 2 パラメータを `true` にすると、指定したコンポーネントかその子孫のいずれかが受け手かどうかを確かめます。 -`handle{signal}` に先行する任意の段階で、[processSignal()|api:Nette\Application\UI\Presenter::processSignal()] メソッドを呼び出すことでシグナルを手動で実行できます。このメソッドはシグナルの処理を担当します。シグナルの受信者として指定されたコンポーネント(受信者が指定されていない場合はPresenter自体)を取得し、それにシグナルを送信します。 +`handle` より前のどの段階でも、[processSignal()|api:Nette\Application\UI\Presenter::processSignal()]メソッドを呼んでシグナルを手動で実行できます。このメソッドがシグナルの処理を引き受け、シグナルの受け手とされたコンポーネント(受け手が指定されていなければプレゼンター自身)を取って、そこへシグナルを送ります。 -例: +例を挙げます。 ```php if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { @@ -482,4 +501,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -これにより、シグナルは早期に実行され、再度呼び出されることはありません。 +これでシグナルが前倒しで実行され、もう一度呼ばれることはありません。 diff --git a/application/ja/configuration.texy b/application/ja/configuration.texy index cb0e58a8fe..28bb3a9af9 100644 --- a/application/ja/configuration.texy +++ b/application/ja/configuration.texy @@ -1,8 +1,8 @@ -アプリケーション設定 -********** +アプリケーションの設定 +*********** .[perex] -Netteアプリケーションの設定オプションの概要。 +Nette Application の設定オプションの概要です。 Application @@ -10,62 +10,64 @@ Application ```neon application: - # Tracy BlueScreenに「Nette Application」パネルを表示しますか? - debugger: ... # (bool) デフォルトは true + # Tracy の BlueScreen に「Nette Application」パネルを表示しますか? + debugger: ... # (bool) Tracy が使えれば有効 - # エラー時に error-presenter を呼び出しますか? - # 開発モードでのみ効果があります - catchExceptions: ... # (bool) デフォルトは true + # 本番では例外は常に error-presenter が扱います。 + # このオプションは開発モードでもその振る舞いを有効にするだけです + catchExceptions: ... # (bool) 既定は false。つまり開発では無効、本番では常に有効 # error-presenter の名前 - errorPresenter: Error # (string|array) デフォルトは 'Nette:Error' + errorPresenter: Error # (string|array) 既定は 'Nette:Error' - # Presenterとアクションのエイリアスを定義します + # プレゼンターとアクションの別名を定義します aliases: ... - # Presenter名をクラスに変換するルールを定義します + # プレゼンター名をクラスに変換する規則を定義します mapping: ... - # 不正なリンクは警告を生成しませんか? - # 開発モードでのみ効果があります - silentLinks: ... # (bool) デフォルトは false + # 不正なリンクの警告を抑制しますか? + # 開発モードでのみ有効です + silentLinks: ... # (bool) 既定は false ``` -`nette/application` バージョン 3.2 以降、エラープレゼンターのペアを定義できます。 +`nette/application` のバージョン 3.2 以降、エラー用のプレゼンターを 2 つ定義できます。 ```neon application: errorPresenter: - 4xx: Error4xx # Nette\Application\BadRequestException 例外用 - 5xx: Error5xx # その他の例外用 + 4xx: Error4xx # Nette\Application\BadRequestException 用 + 5xx: Error5xx # そのほかの例外用 ``` -`silentLinks` オプションは、リンク生成が失敗した場合(例えば、Presenterが存在しないためなど)に、開発モードでNetteがどのように動作するかを決定します。デフォルト値 `false` は、Netteが `E_USER_WARNING` エラーをスローすることを意味します。`true` に設定すると、このエラーメッセージが抑制されます。本番環境では、`E_USER_WARNING` は常に発生します。この動作は、Presenter変数 [$invalidLinkMode |creating-links#不正なリンク] を設定することでも制御できます。 +分けると便利なのは、この 2 つの状況が根本的に異なるからです。`BadRequestException`(4xx のコード)は、アプリケーションは正常で、訪問者が存在しないものを求めただけであることを意味します。ですからサイトのレイアウトの中で親切なメッセージを出す、機能の揃ったプレゼンターを使えます。逆に 5xx のエラーは、アプリケーションの何かが壊れていて、それが何かは分からないことを意味します。5xx のプレゼンターは、描画中にほかの何かが失敗しないよう、できるだけ最小限に保ってください。理想としては、データベースにもレイアウトにもログイン中のユーザーにも触れないことです。 -[エイリアスは、頻繁に使用されるPresenterへのリンクを簡略化 |creating-links#エイリアス]します。 +`silentLinks` オプションは、開発モードでリンクの生成が失敗したとき(たとえばプレゼンターが存在しないときなど)に Nette がどう振る舞うかを決めます。既定値の `false` は、Nette が `E_USER_WARNING` のエラーを出すことを意味します。`true` にするとこのエラーメッセージが抑制されます。本番環境では常に `E_USER_WARNING` が出ます。この振る舞いは、プレゼンターの変数 [$invalidLinkMode |creating-links#不正なリンク]の設定でも変えられます。 -[マッピングは、Presenter名からクラス名を導出するルールを定義 |directory-structure#Presenterのマッピング]します。 +[別名を使うと |creating-links#別名]、よく使うプレゼンターを簡単に参照できます。 +[mapping は規則を定義し |directory-structure#プレゼンターのマッピング]、プレゼンター名からクラス名が導かれます。 -Presenterの自動登録 --------------- -NetteはPresenterをサービスとしてDIコンテナに自動的に追加し、これによりPresenterの作成が大幅に高速化されます。NetteがPresenterをどのように検索するかは設定可能です。 +プレゼンターの自動登録 +----------- + +Nette はプレゼンターをサービスとして DI コンテナに自動的に追加し、その生成を大きく速くします。Nette がプレゼンターをどう探すかは設定できます。 ```neon application: - # ComposerクラスマップでPresenterを検索しますか? - scanComposer: ... # (bool) デフォルトは true + # Composer のクラスマップでプレゼンターを探しますか? + scanComposer: ... # (bool) 既定は true - # クラス名とファイル名が一致する必要があるマスク - scanFilter: ... # (string) デフォルトは '*Presenter' + # クラス名とファイル名が一致すべきマスク + scanFilter: ... # (string) 既定は '*Presenter' - # どのディレクトリでPresenterを検索しますか? - scanDirs: # (string[]|false) デフォルトは '%appDir%' + # どのディレクトリでプレゼンターを探しますか? + scanDirs: # (string[]|false) 既定は '%appDir%' - %vendorDir%/mymodule ``` -`scanDirs` にリストされたディレクトリは、デフォルト値 `%appDir%` を上書きするのではなく、補完します。したがって、`scanDirs` には `%appDir%` と `%vendorDir%/mymodule` の両方のパスが含まれます。デフォルトのディレクトリを除外したい場合は、値を上書きする[感嘆符 |dependency-injection:configuration#マージ]を使用します。 +`scanDirs` に並べたディレクトリは既定値 `%appDir%` を上書きせず、それを補うので、`scanDirs` には `%appDir%` と `%vendorDir%/mymodule` の両方のパスが入ります。既定のディレクトリを外したい場合は[感嘆符 |dependency-injection:configuration#統合]を使います。 ```neon application: @@ -73,36 +75,42 @@ application: - %vendorDir%/mymodule ``` -ディレクトリのスキャンは、false値を指定することで無効にできます。Presenterの自動追加を完全に抑制することはお勧めしません。そうしないと、アプリケーションのパフォーマンスが低下します。 +値を `false` にすると、ディレクトリの走査を無効にできます。するとプレゼンターはサービスとして登録されなくなるので、[decorator |dependency-injection:configuration#Decorator]セクションで調整できなくなり、生成も遅くなります。ですから自動登録を完全に抑えることはおすすめしません。アプリケーションの性能が落ちるからです。 Latte テンプレート ============ -この設定により、コンポーネントとPresenterにおけるLatteの動作をグローバルに影響させることができます。 +この設定は、コンポーネントとプレゼンターにおける Latte の振る舞いに全体として影響します。 ```neon latte: - # メインテンプレート(true)またはすべてのコンポーネント(all)に対してTracy BarにLatteパネルを表示しますか? - debugger: ... # (true|false|'all') デフォルトは true + # Tracy バーに Latte パネルを、主テンプレートについて表示(true)、それとも全コンポーネントについて(all)? + debugger: ... # (true|false|'all') Tracy が使えれば有効(デバッグモードのみ) + + # declare(strict_types=1) のヘッダー付きでテンプレートを生成します + strictTypes: ... # (bool) 既定は false + + # [厳格なパーサーモード |latte:develop#strict mode]を有効にします + strictParsing: ... # (bool) 既定は false - # declare(strict_types=1) ヘッダーを持つテンプレートを生成します - strictTypes: ... # (bool) デフォルトは false + # 変数のスコープをループ本体に限定します + scopedLoopVariables: ... # (bool) 既定は false - # [厳密なパーサーモード |latte:develop#striktní režim]を有効にします - strictParsing: ... # (bool) デフォルトは false + # ペアタグの入れ子によるインデントを取り除きます + dedent: ... # (bool) 既定は false - # [生成されたコードのチェック |latte:develop#Kontrola vygenerovaného kódu]を有効にします - phpLinter: ... # (string) デフォルトは null + # [生成されたコードのチェック |latte:develop#Checking Generated Code]を有効にします + phpLinter: ... # (string) 既定は null # ロケールを設定します - locale: cs_CZ # (string) デフォルトは null + locale: cs_CZ # (string) 既定は null # $this->template オブジェクトのクラス - templateClass: App\MyTemplateClass # デフォルトは Nette\Bridges\ApplicationLatte\DefaultTemplate + templateClass: App\MyTemplateClass # 既定は Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Latteバージョン3を使用している場合は、次のようにして新しい[拡張機能 |latte:extending-latte#Latte Extension]を追加できます。 +新しい[拡張 |latte:extending-latte#Latte Extension]は次のように追加できます。 ```neon latte: @@ -110,36 +118,22 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Latteバージョン2を使用している場合は、クラス名を指定するか、サービスへの参照を指定することで、新しいタグ(マクロ)を登録できます。デフォルトでは `install()` メソッドが呼び出されますが、別のメソッド名を指定することで変更できます。 - -```neon -latte: - # カスタムLatteタグの登録 - macros: - - App\MyLatteMacros::register # 静的メソッド、クラス名またはcallable - - @App\MyLatteMacrosFactory # install() メソッドを持つサービス - - @App\MyLatteMacrosFactory::register # register() メソッドを持つサービス - -services: - - App\MyLatteMacrosFactory -``` - ルーティング ====== -基本設定: +基本の設定です。 ```neon routing: - # Tracy Barにルーティングパネルを表示しますか? - debugger: ... # (bool) デフォルトは true + # Tracy バーにルーティングのパネルを表示しますか? + debugger: ... # (bool) Tracy が使えれば有効(デバッグモードのみ) - # ルーターをDIコンテナにシリアライズします - cache: ... # (bool) デフォルトは false + # ルーターを DI コンテナにシリアライズします + cache: ... # (bool) 既定は false ``` -ルーティングは通常、[RouterFactory |routing#ルートコレクション]クラスで定義します。あるいは、`maska: akce` のペアを使用して設定でルートを定義することもできますが、この方法では設定の柔軟性がそれほど高くありません。 +ルーティングはふつう [RouterFactory |routing#ルートのコレクション]クラスで定義します。あるいは `mask: action` の組を使って設定でルートを定義することもできますが、この方法はあまり柔軟ではありません。 ```neon routing: @@ -150,25 +144,25 @@ routing: 定数 -========= +=== -PHP定数の作成。 +PHP の定数を作ります。 ```neon constants: Foobar: 'baz' ``` -アプリケーション起動後、`Foobar` 定数が作成されます。 +`Foobar` 定数はアプリケーションの起動後に作られます。 .[note] -定数は、グローバルにアクセス可能な変数として使用すべきではありません。オブジェクトに値を渡すには、[依存関係注入 |dependency-injection:passing-dependencies]を使用してください。 +定数はグローバルに使える変数の代わりにすべきではありません。オブジェクトに値を渡すには[依存性注入 |dependency-injection:passing-dependencies]を使ってください。 PHP === -PHPディレクティブの設定。すべてのディレクティブの概要は[php.net |https://www.php.net/manual/en/ini.list.php]にあります。 +PHP のディレクティブの設定です。すべてのディレクティブの一覧は [php.net |https://www.php.net/manual/en/ini.list.php]にあります。 ```neon php: @@ -179,13 +173,14 @@ php: DI サービス ======= -これらのサービスはDIコンテナに追加されます。 - -| 名前 | 型 | 説明 -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [アプリケーション全体の起動 |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | Presenter のファクトリ -| `application.###` | [api:Nette\Application\UI\Presenter] | 個々の Presenter -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | `Latte\Engine` オブジェクトのファクトリ -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | [`$this->template` |templates] のファクトリ +次のサービスが 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 index 9b86073140..d08068f318 100644 --- a/application/ja/creating-links.texy +++ b/application/ja/creating-links.texy @@ -1,156 +1,173 @@ -URLリンクの作成 -********* +URL リンクの作成 +**********
    -Netteでリンクを作成するのは、指をさすのと同じくらい簡単です。指し示すだけで、フレームワークがすべての作業を代行します。ここでは以下について説明します。 +Nette でのリンクの作成は、指を差すのと同じくらい簡単です。狙いを定めるだけで、あとはフレームワークが全部やってくれます。ここでは次のことを扱います。 -- テンプレートやその他の場所でリンクを作成する方法 -- 現在のページへのリンクを区別する方法 -- 不正なリンクの対処法 +- テンプレートやそのほかの場所でリンクを作る方法 +- 現在のページへのリンクを見分ける方法 +- 不正なリンクへの対処
    -[双方向ルーティング |routing]のおかげで、後で変更される可能性のあるアプリケーションのURLをテンプレートやコードにハードコーディングしたり、複雑に組み立てたりする必要は決してありません。リンクでPresenterとアクションを指定し、必要に応じてパラメータを渡すだけで、フレームワークがURLを自動的に生成します。実際には、関数を呼び出すのと非常によく似ています。これは気に入るはずです。 +[双方向のルーティング |routing]のおかげで、あとで変わるかもしれない、あるいは組み立てが面倒なアプリケーションの URL を、テンプレートやコードに直接書き込む必要はもうありません。リンクにはプレゼンターとアクションを指定し、必要なパラメータを渡すだけで、URL はフレームワークが生成します。実のところ、関数を呼ぶのとよく似ています。きっと気に入るでしょう。 -Presenterテンプレート内 -================ +プレゼンターのテンプレートで +============== -最も頻繁にリンクを作成するのはテンプレートであり、`n:href` 属性は素晴らしいヘルパーです。 +リンクを作るのはたいていテンプレートの中で、そこでは `n:href` 属性が頼もしい助けになります。 ```latte 詳細 ``` -HTML属性 `href` の代わりに、[n:属性 |latte:syntax#n:属性] `n:href` を使用していることに注意してください。その値は、`href` 属性の場合のようにURLではなく、Presenterとアクションの名前です。 +HTML の属性 `href` の代わりに [n:属性 |latte:syntax#n:属性]の `n:href` を使っていることに注目してください。その値は、`href` 属性の場合のような URL ではなく、プレゼンターとアクションの名前です。 -リンクをクリックすることは、簡単に言えば、`ProductPresenter::renderShow()` メソッドを呼び出すようなものです。そして、そのシグネチャにパラメータがある場合は、引数を付けて呼び出すことができます。 +リンクをクリックすることは、簡単にいえば `ProductPresenter::renderShow()` メソッドを呼ぶようなものです。そしてそのシグネチャにパラメータがあれば、引数を付けて呼べます。 ```latte -製品詳細 +商品の詳細 ``` -名前付きパラメータを渡すことも可能です。次のリンクは、値 `cs` を持つ `lang` パラメータを渡します。 +名前付きパラメータも渡せます。次のリンクは、値 `en` を持つパラメータ `lang` を渡します。 ```latte -製品詳細 +商品の詳細 ``` -`ProductPresenter::renderShow()` メソッドがそのシグネチャに `$lang` を持っていない場合、`$lang = $this->getParameter('lang')` を使用してパラメータの値を取得するか、[プロパティ |presenters#リクエストパラメータ]から取得できます。 +`ProductPresenter::renderShow()` メソッドのシグネチャに `$lang` がなければ、`$lang = $this->getParameter('lang')` か[プロパティ |presenters#リクエストのパラメータ]でその値を取得できます。 -パラメータが配列に格納されている場合、`...` 演算子(Latte 2.xでは `(expand)` 演算子)を使用して展開できます。 +パラメータが配列に入っているなら、`...` 演算子で展開できます。 ```latte -{var $args = [$product->id, lang => cs]} -製品詳細 +{var $args = [$product->id, lang => en]} +商品の詳細 ``` -リンクでは、いわゆる[パーシステントパラメータ |presenters#パーシステントパラメータ]も自動的に渡されます。 +いわゆる[永続パラメータ |presenters#永続パラメータ]もリンクに自動的に渡されます。 -`n:href` 属性はHTMLタグ `` に非常に便利です。リンクを他の場所、例えばテキスト内に出力したい場合は、`{link}` を使用します。 +`n:href` 属性は HTML の `` タグにとても便利です。リンクをそれ以外の場所、たとえば文章の中に出したい場合は `{link}` を使います。 ```latte -アドレスは: {link Home:default} +URL は次のとおりです: {link Home:default} ``` -コード内 -==== +コードの中で +====== -Presenterでリンクを作成するには、`link()` メソッドを使用します。 +プレゼンターでリンクを作るには `link()` メソッドを使います。 ```php $url = $this->link('Product:show', $product->id); ``` -パラメータは、名前付きパラメータを含めることができる配列を使用して渡すこともできます。 +パラメータは配列としても渡せ、その中で名前付きパラメータも指定できます。 ```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -リンクはPresenterなしでも作成できます。そのために[#LinkGenerator]とその `link()` メソッドがあります。 +リンクは [#LinkGenerator]とその `link()` メソッドを使えば、プレゼンターなしでも作れます。 +ときには、いまリンクを作りつつ、実際の URL の生成はあとにしたいことがあります。そのために `lazyLink()` メソッドがあり、`Nette\Application\UI\Link` オブジェクトを返します。利点は、このオブジェクトをたとえばテンプレートへ渡し、描かれる前に `setParameter()` メソッドでそのパラメータをまだ調整できることです。URL そのものは、オブジェクトが文字列に変換されるときにはじめて組み立てられます。 -Presenterへのリンク -============== +```php +$link = $this->lazyLink('Product:show', $id); +// ... +echo $link; // URL はここではじめて生成されます +``` -リンクのターゲットがPresenterとアクションの場合、構文は次のようになります。 + +プレゼンターへのリンク +=========== + +リンクの行き先がプレゼンターとアクションなら、次の構文になります。 ``` [//] [[[[:]module:]presenter:]action | this] [#fragment] ``` -この形式は、すべてのLatteタグと、リンクを扱うすべてのPresenterメソッド(`n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()`、および[#LinkGenerator])でサポートされています。したがって、例で `n:href` が使用されていても、これらの関数のいずれかを使用できます。 +この形式は、すべての Latte のタグと、リンクを扱うすべてのプレゼンターのメソッド、つまり `n:href`、`{link}`、`{plink}`、`link()`、`lazyLink()`、`isLinkCurrent()`、`redirect()`、`redirectPermanent()`、`forward()`、`canonicalize()`、そして [#LinkGenerator]で使えます。ですから例で `n:href` を使っていても、そこにはこれらのどの関数が来てもかまいません。 -したがって、基本形式は `Presenter:action` です。 +基本の形は `Presenter:action` です。 ```latte -ホームページ +トップページ ``` -現在のPresenterのアクションにリンクする場合、その名前を省略できます。 +現在のプレゼンターのアクションへリンクするなら、その名前は省けます。 ```latte -ホームページ +トップページ ``` -ターゲットが `default` アクションの場合、省略できますが、コロンは残す必要があります。 +行き先のアクションが `default` なら省けますが、コロンは残さなければなりません。 ```latte -ホームページ +トップページ ``` -リンクは他の[モジュール |directory-structure#Presenterとテンプレート]にも向かうことができます。ここでは、リンクはネストされたサブモジュールへの相対リンク、または絶対リンクに区別されます。原理はディスク上のパスに似ていますが、スラッシュの代わりにコロンが使用されます。現在のPresenterが `Front` モジュールの一部であると仮定すると、次のように記述します。 +リンクはほかの[モジュール |directory-structure#プレゼンターとテンプレート]も指せます。ここでは、入れ子のサブモジュールからの相対と、絶対とが区別されます。原理はディスクのパスと同じで、スラッシュの代わりにコロンを使うだけです。現在のプレゼンターが `Front` モジュールの一部だとすると、次のように書きます。 ```latte Front:Shop:Product:show へのリンク Admin:Product:show へのリンク ``` -特別なケースは、[自分自身へのリンク |#現在のページへのリンク]で、ターゲットとして `this` を指定します。 +特別な場合が[自分自身への |#現在のページへのリンク]リンクで、行き先に `this` を指定します。 ```latte 更新 ``` -ハッシュマーク `#` の後のいわゆるフラグメントを介して、ページの特定の部分にリンクできます。 +ハッシュ記号 `#` のあとのいわゆるフラグメントで、ページの特定の部分を指せます。 ```latte Home:default とフラグメント #main へのリンク ``` +.{data-version:3.3.0} +フラグメントは `#` のキーを持つ引数として動的に設定することもできます。その値は自動的にエンコードされ、行き先に指定されたフラグメントより優先されます。 + +```php +$this->link('Home:default', ['#' => $fragment]); +``` + 絶対パス ==== -`link()` または `n:href` を使用して生成されたリンクは常に絶対パス(つまり、`/` 文字で始まる)ですが、`https://domain` のようなプロトコルとドメインを持つ絶対URLではありません。 +`link()` や `n:href` で生成されるリンクは常に絶対パス(つまり `/` で始まるもの)ですが、`https://domain` のようにプロトコルとドメインを含む絶対 URL ではありません。 + +絶対 URL を生成するには、先頭にスラッシュを 2 つ足します(たとえば `n:href="//Home:"`)。あるいは `$this->absoluteUrls = true` を設定して、プレゼンターが絶対リンクだけを生成するように切り替えられます。 -絶対URLを生成するには、先頭に2つのスラッシュを追加します(例:`n:href="//Home:"`)。または、`$this->absoluteUrls = true` を設定して、Presenterが絶対リンクのみを生成するように切り替えることもできます。 +テンプレートでは `|absoluteUrl` フィルタを使って、相対パスを絶対パスに変換することもできます。 現在のページへのリンク =========== -ターゲット `this` は現在のページへのリンクを作成します。 +行き先 `this` は現在のページへのリンクを作ります。 ```latte 更新 ``` -同時に、`action()` または `render()` メソッドのシグネチャで指定されたすべてのパラメータも転送されます(`action()` が定義されていない場合)。したがって、`Product:show` ページで `id: 123` の場合、`this` へのリンクもこのパラメータを渡します。 +同時に、`action()` または(`action()` が定義されていなければ)`render()` メソッドのシグネチャに書かれたすべてのパラメータが引き継がれます。ですから `id: 123` で `Product:show` のページにいるなら、`this` へのリンクもこのパラメータを渡します。 -もちろん、パラメータを直接指定することも可能です。 +もちろん、パラメータを直接指定することもできます。 ```latte 更新 ``` -`isLinkCurrent()` 関数は、リンクのターゲットが現在のページと同じかどうかを判断します。これは、例えばテンプレートでリンクを区別するためなどに使用できます。 +`isLinkCurrent()` 関数は、リンクの行き先が現在のページと同じかどうかを調べます。たとえばテンプレートでリンクを見分けるのに使えます。 -パラメータは `link()` メソッドと同じですが、さらに特定のアクションの代わりにワイルドカード `*` を指定できます。これは、そのPresenterの任意のアクションを意味します。 +パラメータは `link()` メソッドと同じですが、具体的なアクションの代わりにワイルドカード `*` も使えます。これは指定したプレゼンターの任意のアクションを意味します。 ```latte {if !isLinkCurrent('Admin:login')} @@ -162,15 +179,15 @@ Presenterへのリンク
  • ``` -1つの要素で `n:href` と組み合わせる場合、短縮形を使用できます。 +ひとつの要素の中で `n:href` と組み合わせる場合は、短い形が使えます。 ```latte ... ``` -ワイルドカード `*` はアクションの代わりにのみ使用でき、Presenterの代わりには使用できません。 +ワイルドカード `*` はアクションの代わりにだけ使え、プレゼンターの代わりには使えません。 -特定のモジュールまたはそのサブモジュールにいるかどうかを判断するには、`isModuleCurrent(moduleName)` メソッドを使用します。 +特定のモジュールやそのサブモジュールにいるかどうかを判定するには、`isModuleCurrent(moduleName)` メソッドを使います。 ```latte
  • @@ -179,56 +196,72 @@ Presenterへのリンク ``` +リンクの基準の変更 .{data-version:3.2.7} +=============================== + +既定では、相対リンクは現在のプレゼンターから導かれます。これは `{linkBase}` で変えられます。 + +```latte +{linkBase Admin:Dashboard} +商品の詳細 +``` + +このリンクは `Admin:Dashboard:Product:show` に向かいます。影響を受けるのは相対リンクだけで、コロンで始まる絶対リンクと現在のプレゼンターへのリンク(`this`、`show`)は変わりません。 + +`{linkBase}` はテンプレート全体に適用され、とくにレイアウトのテンプレートで役立ちます。呼び出し元のプレゼンターに関係なく、一貫したリンクを保証してくれるからです。 +このタグはテンプレートの先頭に置かなければならず、さもないと `CompileException` を投げます。 + + シグナルへのリンク ========= -リンクのターゲットはPresenterとアクションだけでなく、[シグナル |components#シグナル](`handle()` メソッドを呼び出す)にすることもできます。その場合、構文は次のようになります。 +リンクの行き先はプレゼンターとアクションだけでなく、[シグナル |components#シグナル]でもかまいません(`handle()` メソッドを呼びます)。その場合の構文は次のとおりです。 ``` [//] [sub-component:]signal! [#fragment] ``` -したがって、シグナルは感嘆符で区別されます。 +シグナルは感嘆符で見分けられます。 ```latte シグナル ``` -サブコンポーネント(またはサブサブコンポーネント)のシグナルへのリンクを作成することもできます。 +サブコンポーネント(やそのサブコンポーネント)のシグナルへのリンクも作れます。 ```latte シグナル ``` -コンポーネント内のリンク -============ +コンポーネントの中のリンク +============= -[コンポーネント|components]は独立した再利用可能なユニットであり、周囲のPresenterへの依存関係を持つべきではないため、リンクはここで少し異なります。Latte属性 `n:href` とタグ `{link}`、および `link()` などのコンポーネントメソッドは、リンクのターゲットを**常にシグナル名と見なします**。したがって、感嘆符を指定する必要さえありません。 +[コンポーネント|components]は、周りのプレゼンターと何のつながりも持つべきでない独立した再利用可能な部品なので、ここではリンクの働きが少し違います。Latte の属性 `n:href` とタグ `{link}`、そしてコンポーネントの `link()` などのメソッドは、**リンクの行き先を常にシグナル名とみなします**。ですから感嘆符を付ける必要さえありません。 ```latte アクションではなくシグナル ``` -コンポーネントテンプレートでPresenterにリンクしたい場合は、`{plink}` タグを使用します。 +コンポーネントのテンプレートからプレゼンターへリンクしたい場合は、`{plink}` タグを使います。 ```latte -ホーム +トップ ``` -またはコードで +あるいはコードの中で ```php $this->getPresenter()->link('Home:default') ``` -エイリアス .{data-version:v3.2.2} -============================ +別名 .{data-version:3.2.3} +======================== -Presenter:アクションのペアに覚えやすいエイリアスを割り当てると便利な場合があります。例えば、ホームページ `Front:Home:default` を単に `home` と名付けたり、`Admin:Dashboard:default` を `admin` と名付けたりします。 +Presenter:action の組に、覚えやすい別名を割り当てると便利なことがあります。たとえばトップページの `Front:Home:default` を単に `home`、`Admin:Dashboard:default` を `admin` と呼ぶ、といった具合です。 -エイリアスは、[設定|configuration]の `application › aliases` キーの下で定義されます。 +別名は[設定|configuration]の `application › aliases` キーで定義します。 ```neon application: @@ -238,26 +271,26 @@ application: sign: Front:Sign:in ``` -リンクでは、アットマークを使用して記述されます。例えば: +リンクの中ではアットマークを付けて書きます。たとえば次のようにです。 ```latte -管理 +管理画面 ``` -これらは、`redirect()` などのリンクを扱うすべてのメソッドでもサポートされています。 +`redirect()` など、リンクを扱うすべてのメソッドでも使えます。 不正なリンク ====== -存在しないPresenterにつながる、ターゲットメソッドがシグネチャで受け入れるよりも多くのパラメータを渡す、またはターゲットアクションのURLを生成できないなどの理由で、不正なリンクを作成することがあります。不正なリンクをどのように処理するかは、静的変数 `Presenter::$invalidLinkMode` によって決定されます。これは、これらの値(定数)の組み合わせを取ることができます。 +不正なリンクを作ってしまうことがあります。存在しないプレゼンターを指している、行き先のメソッドのシグネチャが受け取るより多くのパラメータを渡している、あるいは行き先のアクションに対して URL を生成できない場合です。不正なリンクをどう扱うかは、プレゼンターの `$this->invalidLinkMode` で設定します。次の値(定数)の組み合わせを取れます。 -- `Presenter::InvalidLinkSilent` - サイレントモード、URLとして `#` 文字が返されます -- `Presenter::InvalidLinkWarning` - E_USER_WARNING 警告がスローされ、本番モードではログに記録されますが、スクリプトの実行は中断されません -- `Presenter::InvalidLinkTextual` - 視覚的な警告、エラーをリンクに直接出力します -- `Presenter::InvalidLinkException` - InvalidLinkException 例外がスローされます +- `Presenter::InvalidLinkSilent` - 静かなモード。URL として文字 # を返します +- `Presenter::InvalidLinkWarning` - E_USER_WARNING の警告が出ます。本番モードでは記録されますが、スクリプトの実行は止まりません +- `Presenter::InvalidLinkTextual` - 目に見える警告。エラーをリンクに直接書き出します +- `Presenter::InvalidLinkException` - InvalidLinkException を投げます -デフォルト設定は、本番モードでは `InvalidLinkWarning`、開発モードでは `InvalidLinkWarning | InvalidLinkTextual` です。本番環境での `InvalidLinkWarning` はスクリプトの実行を中断しませんが、警告はログに記録されます。開発環境では、[Tracy |tracy:]によってキャッチされ、ブルースクリーンが表示されます。`InvalidLinkTextual` は、`#error:` 文字で始まるエラーメッセージをURLとして返すように機能します。そのようなリンクを一目でわかるようにするには、CSSに以下を追加します。 +既定の設定は、本番モードでは `InvalidLinkWarning`、開発モードでは `InvalidLinkWarning | InvalidLinkTextual` です。本番環境の `InvalidLinkWarning` はスクリプトを止めませんが、警告は記録されます。開発環境では [Tracy |tracy:]がそれを捕まえてブルースクリーンを表示します。`InvalidLinkTextual` は、`#error:` の文字で始まるエラーメッセージを URL として返すことで働きます。そうしたリンクを一目で見つけられるよう、CSS に次を足してください。 ```css a[href^="#error:"] { @@ -266,7 +299,7 @@ a[href^="#error:"] { } ``` -開発環境で警告が生成されないようにしたい場合は、[設定|configuration]で直接サイレントモードを設定できます。 +開発環境で警告を出したくない場合は、[設定|configuration]で直接抑制できます。 ```neon application: @@ -277,10 +310,10 @@ application: LinkGenerator ============= -Presenterが存在しない場合に、`link()` メソッドと同様の快適さでリンクを作成するにはどうすればよいでしょうか?そのために[api:Nette\Application\LinkGenerator]があります。 +`link()` メソッドと同じ快適さで、しかしプレゼンターなしでリンクを作るにはどうすればよいでしょうか。そのためにあるのが [api:Nette\Application\LinkGenerator]です。 -LinkGeneratorは、コンストラクタ経由で渡してもらい、その後その `link()` メソッドを使用してリンクを作成できるサービスです。 +LinkGenerator はサービスで、コンストラクタで受け取り、その `link()` メソッドでリンクを作れます。 -Presenterとの違いがあります。LinkGeneratorはすべてのリンクを直接絶対URLとして作成します。さらに、「現在のPresenter」は存在しないため、ターゲットとしてアクション名 `link('default')` だけを指定したり、モジュールへの相対パスを指定したりすることはできません。 +プレゼンターとの違いがひとつあります。LinkGenerator はすべてのリンクを絶対 URL として直接作ります。さらに「現在のプレゼンター」がないので、行き先にアクション名だけを指定する `link('default')` はできませんし、モジュールへの相対パスも使えません。 -不正なリンクは常に `Nette\Application\UI\InvalidLinkException` をスローします。 +不正なリンクは常に `Nette\Application\UI\InvalidLinkException` を投げます。 diff --git a/application/ja/directory-structure.texy b/application/ja/directory-structure.texy index 5ad882a036..ad5c9b5e19 100644 --- a/application/ja/directory-structure.texy +++ b/application/ja/directory-structure.texy @@ -1,45 +1,45 @@ -アプリケーションのディレクトリ構造 +アプリケーションのディレクトリ構成 *****************
    -Nette Frameworkプロジェクトのために、明確でスケーラブルなディレクトリ構造をどのように設計すればよいでしょうか?コードの整理に役立つベストプラクティスを紹介します。以下について学びます。 +Nette Framework のプロジェクトで、見通しがよく拡張しやすいディレクトリ構成をどう設計すればよいでしょうか。コードの整理に役立つ実績ある作法を紹介します。次のことを学びます。 -- アプリケーションをディレクトリに**論理的に分割**する方法 -- プロジェクトの成長に合わせて**うまくスケール**するように構造を設計する方法 -- **可能な代替案**とその利点または欠点 +- アプリケーションをディレクトリに**論理的に構造化する**方法 +- プロジェクトの成長に**うまく追随する**構成の設計 +- **考えられる代替案**とその長所・短所
    -Nette Framework自体は特定の構造に固執しないことを言及することが重要です。あらゆるニーズや好みに簡単に適応できるように設計されています。 +まず大事なこととして、Nette Framework 自体は特定の構成を強制しません。どんな必要や好みにも簡単に合わせられるよう設計されています。 -プロジェクトの基本構造 +プロジェクトの基本構成 =========== -Nette Frameworkは固定のディレクトリ構造を指示しませんが、[Web Project|https://github.com/nette/web-project]の形で実証済みのデフォルトの配置があります。 +Nette Framework は決まったディレクトリ構成を押し付けませんが、[Web Project|https://github.com/nette/web-project]という形で実績のある既定の配置があります。 /--pre web-project/ -├── app/ ← アプリケーションディレクトリ -├── assets/ ← SCSS、JS、画像ファイルなど、代替として resources/ -├── bin/ ← コマンドラインスクリプト +├── app/ ← アプリケーションのディレクトリ +├── assets/ ← SCSS、JS ファイル、画像など。あるいは resources/ +├── bin/ ← コマンドライン用のスクリプト ├── config/ ← 設定 -├── log/ ← ログ記録されたエラー +├── log/ ← 記録されたエラー ├── temp/ ← 一時ファイル、キャッシュ ├── tests/ ← テスト -├── vendor/ ← Composerによってインストールされたライブラリ -└── www/ ← 公開ディレクトリ (document-root) +├── vendor/ ← Composer がインストールしたライブラリ +└── www/ ← 公開ディレクトリ(document-root) \-- -この構造は、ニーズに応じて自由に調整できます。フォルダの名前を変更したり、移動したりできます。その後、`Bootstrap.php` ファイルと、場合によっては `composer.json` のディレクトリへの相対パスを更新するだけです。それ以上のことは必要ありません。複雑な再設定や定数の変更は不要です。Netteは賢い自動検出機能を備えており、URLベースを含むアプリケーションの場所を自動的に認識します。 +この構成は必要に応じて自由に変えられます。フォルダの名前を変えたり移動したりできます。あとは `Bootstrap.php` と、必要なら `composer.json` のディレクトリへの相対パスを調整するだけです。それ以上は何も要りません。複雑な設定のやり直しも、定数の変更も不要です。Nette には賢い自動検出があり、アプリケーションの場所とその基準 URL を自動的に認識します。 -コード整理の原則 -======== +コードを整理する原則 +========== -新しいプロジェクトを初めて調べるときは、すぐに慣れることができるはずです。`app/Model/` ディレクトリを開いて、この構造を見ると想像してみてください。 +新しいプロジェクトをはじめて見るとき、素早く見当が付くべきです。`app/Model/` ディレクトリをクリックして、次の構成が現れたと想像してください。 /--pre app/Model/ @@ -48,9 +48,9 @@ Nette Frameworkは固定のディレクトリ構造を指示しませんが、[W └── Entities/ \-- -これから読み取れるのは、プロジェクトがいくつかのサービス、リポジトリ、エンティティを使用していることだけです。アプリケーションの実際の目的については何もわかりません。 +ここから分かるのは、このプロジェクトが何らかのサービス、リポジトリ、エンティティを使っているということだけです。アプリケーションが実際に何をするのかは何も分かりません。 -別のアプローチを見てみましょう - **ドメインによる整理**: +別のやり方、**ドメインによる整理**を見てみましょう。 /--pre app/Model/ @@ -60,76 +60,76 @@ Nette Frameworkは固定のディレクトリ構造を指示しませんが、[W └── Product/ \-- -ここでは違います - 一目でeコマースサイトであることがわかります。ディレクトリ名自体が、アプリケーションができること、つまり支払い、注文、製品を扱うことを示しています。 +こちらは違います。一目でネットショップだと分かります。ディレクトリの名前そのものが、アプリケーションに何ができるかを教えてくれます。支払い、注文、商品を扱うのだ、と。 -最初のアプローチ(クラスタイプによる整理)は、実際には多くの問題を引き起こします。論理的に関連するコードが異なるフォルダに分散され、それらの間を行き来する必要があります。したがって、ドメインごとに整理します。 +ひとつめのやり方(クラスの種類による整理)は、実務でいくつかの問題を生みます。論理的に関係するコードが別々のフォルダに散らばり、そのあいだを行き来しなければなりません。ですからドメインで整理します。 名前空間 ---- -ディレクトリ構造がアプリケーションの名前空間に対応するのが慣例です。つまり、ファイルの物理的な場所がその名前空間に対応します。例えば、`app/Model/Product/ProductRepository.php` に配置されたクラスは、`App\Model\Product` 名前空間を持つべきです。この原則は、コードの理解を助け、オートローディングを簡素化します。 +ディレクトリ構成をアプリケーションの名前空間に対応させるのが慣習です。つまりファイルの物理的な場所が、その名前空間と一致するということです。たとえば `app/Model/Product/ProductRepository.php` にあるクラスは、名前空間 `App\Model\Product` を持つべきです。この原則はコードを辿るのを助け、オートローディングを単純にします。 -名前の単数形 vs 複数形 -------------- +名前の単数形と複数形 +---------- -アプリケーションのメインディレクトリでは単数形を使用していることに注意してください:`app`, `config`, `log`, `temp`, `www`。同様に、アプリケーション内部でも:`Model`, `Core`, `Presentation`。これは、それぞれが1つのまとまった概念を表しているためです。 +アプリケーションの主要なディレクトリには単数形を使っていることに注目してください。`app`、`config`、`log`、`temp`、`www` です。アプリケーションの中でも同じで、`Model`、`Core`、`Presentation` です。それぞれがひとつのまとまった概念を表しているからです。 -同様に、例えば `app/Model/Product` は製品に関するすべてを表します。`Products` とは呼びません。なぜなら、それは製品でいっぱいのフォルダではないからです(そこには `nokia.php`, `samsung.php` のようなファイルがあるでしょう)。それは、製品を扱うクラス、つまり `ProductRepository.php`, `ProductService.php` を含む名前空間です。 +同じく `app/Model/Product` は、商品に関わるすべてを表します。`Products` と呼ばないのは、商品が詰まったフォルダ(それなら `nokia.php`、`samsung.php` のようなファイルが入るでしょう)ではないからです。商品を扱うクラス、`ProductRepository.php`、`ProductService.php` を含む名前空間なのです。 -`app/Tasks` フォルダは複数形です。なぜなら、それは独立した実行可能なスクリプトのセット、つまり `CleanupTask.php`, `ImportTask.php` を含んでいるからです。それぞれが独立したユニットです。 +`app/Tasks` フォルダが複数形なのは、独立した実行可能なスクリプトの集まり、`CleanupTask.php`、`ImportTask.php` を含むからです。そのひとつひとつが独立した単位です。 -一貫性のために、以下を使用することをお勧めします。 -- 機能的な全体を表す名前空間には単数形(複数のエンティティを扱う場合でも) -- 独立したユニットのコレクションには複数形 -- 不確かな場合、またはそれについて考えたくない場合は、単数形を選択してください +一貫性のために、次をおすすめします。 +- 機能のまとまりを表す名前空間には単数形(複数のエンティティを扱う場合でも) +- 独立した単位の集まりには複数形 +- 迷ったとき、あるいは考えたくないときは単数形 公開ディレクトリ `www/` =============== -このディレクトリは、Webからアクセスできる唯一のディレクトリ(いわゆるdocument-root)です。`www/` の代わりに `public/` という名前をよく見かけることもありますが、これは単なる慣例の問題であり、機能には影響しません。ディレクトリには以下が含まれます。 -- アプリケーションの[エントリポイント |bootstrapping#index.php] `index.php` -- mod_rewrite(Apacheの場合)のルールを含む `.htaccess` ファイル +このディレクトリだけがウェブからアクセスできます(document-root です)。`www/` の代わりに `public/` という名前もよく見かけますが、これは慣習の問題にすぎず、アプリケーションの動作には影響しません。このディレクトリには次のものが入ります。 +- アプリケーションの[入口 |bootstrapping#index.php] `index.php` +- mod_rewrite の規則を含む `.htaccess` ファイル(Apache 用) - 静的ファイル(CSS、JavaScript、画像) - アップロードされたファイル -アプリケーションの適切なセキュリティのためには、[document-rootを正しく設定 |nette:troubleshooting#URLから www ディレクトリを変更または削除する方法は]することが不可欠です。 +アプリケーションを適切に守るには、[document-root が正しく設定されている |nette:troubleshooting#URL から www のディレクトリを変えたり取り除いたりするには]ことが決定的に重要です。 .[note] -このディレクトリに `node_modules/` フォルダを決して配置しないでください。実行可能であり、公開すべきではない数千のファイルが含まれています。 +`node_modules/` フォルダをこのディレクトリに置いては決していけません。実行可能かもしれない何千ものファイルが入っていて、公開すべきではありません。 -アプリケーションディレクトリ `app/` -===================== +アプリケーションのディレクトリ `app/` +====================== -これはアプリケーションコードを含むメインディレクトリです。基本構造: +アプリケーションのコードを含む主要なディレクトリです。基本の構成は次のとおりです。 /--pre app/ -├── Core/ ← インフラストラクチャ関連 +├── Core/ ← インフラに関わる事柄 ├── Model/ ← ビジネスロジック -├── Presentation/ ← Presenterとテンプレート -├── Tasks/ ← コマンドスクリプト -└── Bootstrap.php ← アプリケーションのブートストラップクラス +├── Presentation/ ← プレゼンターとテンプレート +├── Tasks/ ← コマンドのスクリプト +└── Bootstrap.php ← アプリケーションの起動クラス \-- -`Bootstrap.php` は、環境を初期化し、設定をロードし、DIコンテナを作成する[アプリケーションの起動クラス|bootstrapping]です。 +`Bootstrap.php` は[アプリケーションの起動クラス|bootstrapping]で、環境を初期化し、設定を読み込み、DI コンテナを作ります。 -次に、個々のサブディレクトリについて詳しく見ていきましょう。 +では個々のサブディレクトリを詳しく見ていきましょう。 -Presenterとテンプレート -================ +プレゼンターとテンプレート +============= -アプリケーションのプレゼンテーション部分は `app/Presentation` ディレクトリにあります。代替案は短い `app/UI` です。これは、すべてのPresenter、そのテンプレート、および可能なヘルパークラスのための場所です。 +アプリケーションのプレゼンテーション層は `app/Presentation` ディレクトリにあります。短い `app/UI` でもかまいません。ここがすべてのプレゼンター、そのテンプレート、そして関連する補助クラスの置き場です。 -このレイヤーをドメインごとに整理します。eコマース、ブログ、APIを組み合わせた複雑なプロジェクトでは、構造は次のようになります。 +この層はドメインで整理します。ネットショップ、ブログ、API を組み合わせた複雑なプロジェクトなら、構成は次のようになります。 /--pre app/Presentation/ -├── Shop/ ← eコマースフロントエンド +├── Shop/ ← ネットショップのフロント │ ├── Product/ │ ├── Cart/ │ └── Order/ @@ -139,15 +139,15 @@ Presenterとテンプレート ├── Admin/ ← 管理画面 │ ├── Dashboard/ │ └── Products/ -└── Api/ ← APIエンドポイント +└── Api/ ← API のエンドポイント └── V1/ \-- -一方、単純なブログでは、次のような分割を使用します。 +逆に単純なブログなら、次の構成を使います。 /--pre app/Presentation/ -├── Front/ ← Webフロントエンド +├── Front/ ← サイトのフロント │ ├── Home/ │ └── Post/ ├── Admin/ ← 管理画面 @@ -157,17 +157,17 @@ Presenterとテンプレート └── Export/ ← RSS、サイトマップなど \-- -`Home/` や `Dashboard/` のようなフォルダには、Presenterとテンプレートが含まれます。`Front/`, `Admin/`, `Api/` のようなフォルダは**モジュール**と呼ばれます。技術的には、これらはアプリケーションを論理的に分割するために使用される通常のディレクトリです。 +`Home/` や `Dashboard/` のようなフォルダには、プレゼンターとテンプレートが入ります。`Front/`、`Admin/`、`Api/` のようなフォルダは**モジュール**と呼ばれます。技術的には、アプリケーションを論理的に分けるために使うふつうのディレクトリです。 -Presenterを含む各フォルダには、同じ名前のPresenterとそのテンプレートが含まれます。例えば、`Dashboard/` フォルダには以下が含まれます。 +プレゼンターを含む各フォルダには、プレゼンターのファイル自体とそのテンプレートが入ります。たとえば `Dashboard/` フォルダには次のものが入ります。 /--pre Dashboard/ -├── DashboardPresenter.php ← Presenter +├── DashboardPresenter.php ← プレゼンター └── default.latte ← テンプレート \-- -このディレクトリ構造は、クラスの名前空間に反映されます。例えば、`DashboardPresenter` は `App\Presentation\Admin\Dashboard` 名前空間に配置されます([#Presenterのマッピング]を参照)。 +このディレクトリ構成はクラスの名前空間に反映されます。たとえば `DashboardPresenter` は `App\Presentation\Admin\Dashboard` 名前空間にあります([#プレゼンターのマッピング]をご覧ください)。 ```php namespace App\Presentation\Admin\Dashboard; @@ -178,22 +178,22 @@ class DashboardPresenter extends Nette\Application\UI\Presenter } ``` -`Admin` モジュール内の `Dashboard` Presenterには、アプリケーション内でコロン表記を使用して `Admin:Dashboard` として参照します。その `default` アクションには `Admin:Dashboard:default` として参照します。ネストされたモジュールの場合、複数のコロンを使用します。例えば `Shop:Order:Detail:default` です。 +`Admin` モジュールの中の `Dashboard` プレゼンターは、アプリケーションの中でコロン記法を使って `Admin:Dashboard` と参照します。その `default` アクションは `Admin:Dashboard:default` です。入れ子のモジュールでは、コロンを複数使います。たとえば `Shop:Order:Detail:default` です。 -構造の柔軟な開発 +構成の柔軟な発展 -------- -この構造の大きな利点の1つは、プロジェクトの成長するニーズにエレガントに適応する方法です。例として、XMLフィードを生成する部分を取り上げましょう。最初は単純な形式です。 +この構成の大きな利点のひとつが、プロジェクトの膨らむ要求に優雅に追随できることです。例として XML のフィードを生成する部分を取り上げましょう。最初は単純な形です。 /--pre Export/ -├── ExportPresenter.php ← すべてのエクスポート用の単一Presenter -├── sitemap.latte ← サイトマップ用テンプレート -└── feed.latte ← RSSフィード用テンプレート +├── ExportPresenter.php ← すべてのエクスポート用のプレゼンター 1 つ +├── sitemap.latte ← サイトマップのテンプレート +└── feed.latte ← RSS フィードのテンプレート \-- -時間が経つにつれて、さらに多くのフィードタイプが追加され、それらに対してより多くのロジックが必要になります... 問題ありません!`Export/` フォルダは簡単にモジュールになります。 +やがてフィードの種類が増え、それらのためにもっとロジックが必要になります……。問題ありません。`Export/` フォルダをそのままモジュールにするだけです。 /--pre Export/ @@ -202,58 +202,58 @@ class DashboardPresenter extends Nette\Application\UI\Presenter │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← Zboží.cz用フィード - └── heureka.latte ← Heureka.cz用フィード + ├── amazon.latte ← Amazon 用のフィード + └── ebay.latte ← eBay 用のフィード \-- -この変換は完全にスムーズです - 新しいサブフォルダを作成し、コードをそれらに分割し、リンクを更新するだけです(例:`Export:feed` から `Export:Feed:zbozi` へ)。これにより、必要に応じて構造を徐々に拡張でき、ネストのレベルに制限はありません。 +この変身はまったく滑らかです。新しいサブフォルダを作り、コードをそこに分け、リンクを更新する(たとえば `Export:feed` から `Export:Feed:amazon` へ)だけです。おかげで必要に応じて構成を少しずつ広げられ、入れ子の深さにも制限はありません。 -例えば、管理画面で注文管理に関連する多くのPresenter(`OrderDetail`, `OrderEdit`, `OrderDispatch` など)がある場合、より良い整理のために、この場所に `Order` モジュール(フォルダ)を作成できます。そこにはPresenter `Detail`, `Edit`, `Dispatch` などの(フォルダ)が含まれます。 +たとえば管理画面に、`OrderDetail`、`OrderEdit`、`OrderDispatch` など注文管理に関わるプレゼンターがたくさんあるなら、整理のために `Order` という名前のモジュール(フォルダ)を作り、そこに `Detail`、`Edit`、`Dispatch` などのプレゼンター(のフォルダ)を入れられます。 -テンプレートの配置 ---------- +テンプレートの置き場 +---------- -前の例では、テンプレートがPresenterと同じフォルダに直接配置されていることを見ました。 +これまでの例では、テンプレートがプレゼンターと同じフォルダに置かれていました。 /--pre Dashboard/ -├── DashboardPresenter.php ← Presenter -├── DashboardTemplate.php ← テンプレート用のオプションクラス +├── DashboardPresenter.php ← プレゼンター +├── DashboardTemplate.php ← 任意のテンプレートクラス └── default.latte ← テンプレート \-- -この配置は、実際には最も便利であることが証明されています - すべての関連ファイルがすぐに手元にあります。 +実務ではこの置き場が最も便利だと分かっています。関係するファイルがすべてすぐ手元にあるからです。 -あるいは、テンプレートを `templates/` サブフォルダに配置することもできます。Netteは両方のバリアントをサポートしています。テンプレートを `Presentation/` フォルダの外に完全に配置することもできます。テンプレートの配置オプションに関するすべての情報は、[テンプレートの検索 |templates#テンプレートの検索]の章にあります。 +あるいはテンプレートを `templates/` のサブフォルダに置くこともできます。Nette はどちらの形にも対応しています。テンプレートを `Presentation/` フォルダの完全に外に置くことさえできます。テンプレートの置き場の選択肢については、[テンプレートの探索 |templates#テンプレートの探索]の章にすべてあります。 -ヘルパークラスとコンポーネント ---------------- +補助クラスとコンポーネント +------------- -Presenterとテンプレートには、しばしば他のヘルパーファイルも伴います。それらをその適用範囲に応じて論理的に配置します。 +プレゼンターとテンプレートには、ほかの補助ファイルが伴うことがよくあります。それらは適用範囲に応じて論理的に置きます。 -1. **Presenterのすぐ隣**、特定のPresenter用の特定のコンポーネントの場合: +1. そのプレゼンター固有のコンポーネントなら、**プレゼンターと同じ場所に**: /--pre Product/ ├── ProductPresenter.php -├── ProductGrid.php ← 製品リスト用コンポーネント -└── FilterForm.php ← フィルタリング用フォーム +├── ProductGrid.php ← 商品一覧のコンポーネント +└── FilterForm.php ← 絞り込みのフォーム \-- -2. **モジュール用** - アルファベット順の先頭に明確に配置される `Accessory` フォルダを使用することをお勧めします。 +2. **モジュール用** - `Accessory` フォルダの利用をおすすめします。アルファベット順で都合よく先頭に来ます。 /--pre Front/ ├── Accessory/ -│ ├── NavbarControl.php ← フロントエンド用コンポーネント +│ ├── NavbarControl.php ← フロント用のコンポーネント │ └── TemplateFilters.php ├── Product/ └── Cart/ \-- -3. **アプリケーション全体用** - `Presentation/Accessory/` 内: +3. **アプリケーション全体用** - `Presentation/Accessory/` に: /--pre app/Presentation/ ├── Accessory/ @@ -263,30 +263,30 @@ Presenterとテンプレートには、しばしば他のヘルパーファイ └── Admin/ \-- -または、`LatteExtension.php` や `TemplateFilters.php` のようなヘルパークラスをインフラストラクチャフォルダ `app/Core/Latte/` に配置することもできます。そして、コンポーネントを `app/Components` に配置します。選択はチームの慣習によります。 +あるいは `LatteExtension.php` や `TemplateFilters.php` のような補助クラスを、インフラのフォルダ `app/Core/Latte/` に置くこともできます。コンポーネントは `app/Components` に。選び方はチームの慣習によります。 -モデル - アプリケーションの心臓部 -================== +Model - アプリケーションの心臓 +=================== -モデルには、アプリケーションのすべてのビジネスロジックが含まれています。その整理には、再びルールが適用されます - ドメインごとに構造化します。 +モデルには、アプリケーションのビジネスロジックがすべて入ります。その整理の規則もやはり、ドメインによる構造化です。 /--pre app/Model/ -├── Payment/ ← 支払いに関するすべて -│ ├── PaymentFacade.php ← メインエントリポイント +├── Payment/ ← 支払いに関わるすべて +│ ├── PaymentFacade.php ← 主な入口 │ ├── PaymentRepository.php │ ├── Payment.php ← エンティティ -├── Order/ ← 注文に関するすべて +├── Order/ ← 注文に関わるすべて │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php -└── Shipping/ ← 配送に関するすべて +└── Shipping/ ← 配送に関わるすべて \-- -モデルでは、通常、これらのタイプのクラスに遭遇します。 +モデルでは、ふつう次の種類のクラスに出会います。 -**ファサード**: アプリケーション内の特定のドメインへのメインエントリポイントを表します。完全なユースケース(「注文を作成する」や「支払いを処理する」など)を実装するために、異なるサービス間の協力を調整するオーケストレーターとして機能します。オーケストレーションレイヤーの下で、ファサードは実装の詳細をアプリケーションの他の部分から隠し、特定のドメインを扱うためのクリーンなインターフェースを提供します。 +**ファサード**: アプリケーションの中の特定のドメインへの主な入口を表します。まとめ役として働き、さまざまなサービスの協調を取り持って、「注文を作る」「支払いを処理する」といったユースケースをまるごと実現します。ファサードはその取りまとめの層の下に実装の詳細を隠し、アプリケーションのほかの部分に対して、そのドメインを扱うためのきれいなインターフェースを提供します。 ```php class OrderFacade @@ -295,25 +295,25 @@ class OrderFacade { // 検証 // 注文の作成 - // 電子メールの送信 + // メールの送信 // 統計への書き込み } } ``` -**サービス**: ドメイン内の特定のビジネス操作に焦点を当てます。ユースケース全体をオーケストレーションするファサードとは異なり、サービスは特定のビジネスロジック(価格計算や支払い処理など)を実装します。サービスは通常ステートレスであり、より複雑な操作のための構成要素としてファサードによって使用されるか、より単純なタスクのためにアプリケーションの他の部分によって直接使用されることができます。 +**サービス**: ドメインの中の特定のビジネス上の操作に集中します。ユースケース全体を取りまとめるファサードと違い、サービスは具体的なビジネスロジック(価格の計算や支払いの処理など)を実装します。サービスはふつう状態を持たず、より複雑な操作の部品としてファサードから使われることも、もっと単純な用途でアプリケーションのほかの部分から直接使われることもあります。 ```php class PricingService { public function calculateTotal(Order $order): Money { - // 価格計算 + // 価格の計算 } } ``` -**リポジトリ**: データストレージ、通常はデータベースとのすべての通信を保証します。そのタスクは、エンティティのロードと保存、およびそれらを検索するためのメソッドの実装です。リポジトリは、アプリケーションの他の部分をデータベースの実装の詳細から分離し、データを扱うためのオブジェクト指向インターフェースを提供します。 +**リポジトリ**: データの保管場所、ふつうはデータベースとのやり取りをすべて担います。その役目はエンティティを読み書きし、それを探すメソッドを提供することです。リポジトリはアプリケーションのほかの部分をデータベースの実装の詳細から守り、データを扱うためのオブジェクト指向のインターフェースを提供します。 ```php class OrderRepository @@ -328,10 +328,10 @@ class OrderRepository } ``` -**エンティティ**: アプリケーションの主要なビジネスコンセプトを表すオブジェクトで、独自のアイデンティティを持ち、時間とともに変化します。通常、これらはORM(Nette Database ExplorerやDoctrineなど)を使用してデータベーステーブルにマッピングされるクラスです。エンティティは、そのデータに関するビジネスルールと検証ロジックを含むことができます。 +**エンティティ**: アプリケーションの主要なビジネス上の概念を表すオブジェクトで、自分の同一性を持ち、時とともに変化します。ふつうは ORM(Nette Database Explorer や Doctrine など)でデータベースのテーブルに対応づけられたクラスです。エンティティは、そのデータに関わるビジネスの規則や検証のロジックを持てます。 ```php -// orders データベーステーブルにマッピングされたエンティティ +// 'orders' データベーステーブルに対応づけられたエンティティ class Order extends Nette\Database\Table\ActiveRow { public function addItem(Product $product, int $quantity): void @@ -345,32 +345,32 @@ class Order extends Nette\Database\Table\ActiveRow } ``` -**値オブジェクト**: 独自のアイデンティティを持たない値を表す不変オブジェクト - 例えば、金額や電子メールアドレス。同じ値を持つ値オブジェクトの2つのインスタンスは同一と見なされます。 +**値オブジェクト**: 自分の同一性を持たない値、たとえば金額やメールアドレスを表す不変のオブジェクトです。同じ値を持つ 2 つの値オブジェクトのインスタンスは、同一とみなされます。 -インフラストラクチャコード -============= +インフラのコード +======== -`Core/` フォルダ(または `Infrastructure/`)は、アプリケーションの技術的な基盤のホームです。インフラストラクチャコードには通常、以下が含まれます。 +`Core/` フォルダ(あるいは `Infrastructure/`)は、アプリケーションの技術的な土台の置き場です。インフラのコードにはふつう次のものが含まれます。 /--pre app/Core/ -├── Router/ ← ルーティングとURL管理 +├── Router/ ← ルーティングと URL の管理 │ └── RouterFactory.php ├── Security/ ← 認証と認可 │ ├── Authenticator.php │ └── Authorizator.php -├── Logging/ ← ロギングと監視 +├── Logging/ ← ログ記録と監視 │ ├── SentryLogger.php │ └── FileLogger.php -├── Cache/ ← キャッシュレイヤー +├── Cache/ ← キャッシュの層 │ └── FullPageCache.php └── Integration/ ← 外部サービスとの統合 ├── Slack/ └── Stripe/ \-- -小規模なプロジェクトでは、もちろんフラットな分割で十分です。 +小さなプロジェクトなら、当然ながら平らな構成で十分です。 /--pre Core/ @@ -379,121 +379,121 @@ class Order extends Nette\Database\Table\ActiveRow └── QueueMailer.php \-- -これは次のようなコードです。 +ここに入るのは次のようなコードです。 -- 技術的なインフラストラクチャ(ルーティング、ロギング、キャッシュ)を扱います -- 外部サービス(Sentry、Elasticsearch、Redis)を統合します -- アプリケーション全体に基本的なサービス(メール、データベース)を提供します -- ほとんどの場合、特定のドメイン(製品、注文、記事)に依存しません - キャッシュやロガーはeコマースやブログで同じように機能します。 +- 技術的なインフラ(ルーティング、ログ記録、キャッシュ)を扱う +- 外部サービス(Sentry、Elasticsearch、Redis)と統合する +- アプリケーション全体に基本的なサービス(メール、データベース)を提供する +- 特定のドメインからはおおむね独立している。キャッシュやロガーは、ネットショップでもブログでも同じように働きます。 -特定のクラスがここに属するか、モデルに属するか迷っていますか?重要な違いは、`Core/` のコードは: +あるクラスがここに属するのか、モデルに属するのか迷いますか。決定的な違いは、`Core/` のコードが次の性質を持つことです。 -- ドメイン(製品、注文、記事)について何も知りません -- ほとんどの場合、別のプロジェクトに転送できます -- 「どのように機能するか」(メールを送信する方法)を扱い、「何をするか」(どのメールを送信するか)ではありません +- ドメイン(商品、注文、記事)について何も知らない +- たいてい別のプロジェクトに持っていける +- 「どう動くか」(メールをどう送るか)を解き、「何をするか」(どのメールを送るか)は解かない -よりよく理解するための例: +分かりやすくするための例です。 -- `App\Core\MailerFactory` - 電子メール送信用のクラスのインスタンスを作成し、SMTP設定を扱います -- `App\Model\OrderMailer` - `MailerFactory` を使用して注文に関する電子メールを送信し、そのテンプレートを知っており、いつ送信すべきかを知っています +- `App\Core\MailerFactory` - メールを送るクラスのインスタンスを作り、SMTP の設定を扱います +- `App\Model\OrderMailer` - `MailerFactory` を使って注文についてのメールを送り、そのテンプレートといつ送るべきかを知っています -コマンドスクリプト -========= +コマンドのスクリプト +========== -アプリケーションは、通常のHTTPリクエスト以外のアクティビティを実行する必要があることがよくあります - バックグラウンドでのデータ処理、メンテナンス、または定期的なタスクなどです。実行には `bin/` ディレクトリの単純なスクリプトが使用され、実装ロジック自体は `app/Tasks/`(または `app/Commands/`)に配置されます。 +アプリケーションは、通常の HTTP リクエストの外で処理を行う必要にしばしば迫られます。バックグラウンドのデータ処理、メンテナンス、定期的な仕事などです。実行には `bin/` ディレクトリの単純なスクリプトを使い、実装のロジック自体は `app/Tasks/`(あるいは `app/Commands/`)に置きます。 -例: +例を挙げます。 /--pre app/Tasks/ -├── Maintenance/ ← メンテナンススクリプト +├── Maintenance/ ← メンテナンスのスクリプト │ ├── CleanupCommand.php ← 古いデータの削除 │ └── DbOptimizeCommand.php ← データベースの最適化 ├── Integration/ ← 外部システムとの統合 -│ ├── ImportProducts.php ← サプライヤーシステムからのインポート +│ ├── ImportProducts.php ← 仕入先システムからの取り込み │ └── SyncOrders.php ← 注文の同期 -└── Scheduled/ ← 定期的なタスク +└── Scheduled/ ← 定期的な仕事 ├── NewsletterCommand.php ← ニュースレターの送信 └── ReminderCommand.php ← 顧客への通知 \-- -モデルに属するものとコマンドスクリプトに属するものは何ですか?例えば、1つの電子メールを送信するロジックはモデルの一部ですが、数千の電子メールの一括送信は `Tasks/` に属します。 +何がモデルに属し、何がコマンドのスクリプトに属するのでしょうか。たとえばメールを 1 通送るロジックはモデルの一部で、何千通ものメールの一括送信は `Tasks/` に属します。 -タスクは通常、[コマンドラインから実行 |https://blog.nette.org/en/cli-scripts-in-nette-application]されるか、cron経由で実行されます。HTTPリクエスト経由で実行することもできますが、セキュリティを考慮する必要があります。タスクを実行するPresenterは、例えばログインしたユーザーのみ、または強力なトークンと許可されたIPアドレスからのアクセスのみに保護する必要があります。長いタスクの場合、スクリプトのタイムアウト制限を増やし、セッションがロックされないように `session_write_close()` を使用する必要があります。 +タスクはふつうコマンドラインか cron から実行します。`bin/` のスクリプトが [bootConsoleApplication() |bootstrapping#さまざまな環境]メソッドで DI コンテナを作り、そこから必要なサービスを取り出します。HTTP のリクエストから実行することもできますが、安全性を考える必要があります。タスクを実行するプレゼンターは、たとえばログイン済みのユーザーだけに限る、あるいは強力なトークンと許可された IP アドレスからのアクセスに限る、といった形で守らなければなりません。長時間かかるタスクでは、スクリプトの時間制限を延ばし、セッションのロックを避けるために `session_write_close()` を使う必要があります。 -その他の可能なディレクトリ -============= +そのほかの考えられるディレクトリ +================ -前述の基本ディレクトリに加えて、プロジェクトのニーズに応じて他の特殊なフォルダを追加できます。最も一般的なものとその使用法を見てみましょう。 +ここまで挙げた基本のディレクトリのほかにも、プロジェクトの必要に応じて専用のフォルダを足せます。よくあるものとその用途を見てみましょう。 /--pre app/ -├── Api/ ← プレゼンテーションレイヤーに依存しないAPIロジック -├── Database/ ← テストデータ用のマイグレーションスクリプトとシーダー -├── Components/ ← アプリケーション全体で共有されるビジュアルコンポーネント -├── Event/ ← イベント駆動アーキテクチャを使用する場合に便利 -├── Mail/ ← 電子メールテンプレートと関連ロジック -└── Utils/ ← ヘルパークラス +├── Api/ ← プレゼンテーション層から独立した API のロジック +├── Database/ ← マイグレーションのスクリプトとテストデータの seeder +├── Components/ ← アプリケーション全体で共有する画面の部品 +├── Event/ ← イベント駆動のアーキテクチャを使うなら便利です +├── Mail/ ← メールのテンプレートと関連するロジック +└── Utils/ ← 補助クラス \-- -アプリケーション全体のPresenterで使用される共有ビジュアルコンポーネントには、`app/Components` または `app/Controls` フォルダを使用できます。 +アプリケーション全体のプレゼンターで使う共有の画面部品には、`app/Components` や `app/Controls` フォルダを使えます。 /--pre app/Components/ -├── Form/ ← 共有フォームコンポーネント +├── Form/ ← 共有のフォームの部品 │ ├── SignInForm.php │ └── UserForm.php -├── Grid/ ← データリスト用コンポーネント +├── Grid/ ← データ一覧の部品 │ └── DataGrid.php -└── Navigation/ ← ナビゲーション要素 +└── Navigation/ ← ナビゲーションの要素 ├── Breadcrumbs.php └── Menu.php \-- -ここには、より複雑なロジックを持つコンポーネントが属します。複数のプロジェクト間でコンポーネントを共有したい場合は、それらを別のComposerパッケージに分離することをお勧めします。 +ここには、より複雑なロジックを持つコンポーネントが属します。複数のプロジェクトでコンポーネントを共有したいなら、独立した Composer のパッケージに切り出すとよいでしょう。 -`app/Mail` ディレクトリに電子メール通信の管理を配置できます。 +`app/Mail` ディレクトリには、メールでのやり取りの管理を置けます。 /--pre app/Mail/ -├── templates/ ← 電子メールテンプレート +├── templates/ ← メールのテンプレート │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php \-- -Presenterのマッピング -=============== +プレゼンターのマッピング +============ -マッピングは、Presenter名からクラス名を導出するためのルールを定義します。これらは[設定|configuration]の `application › mapping` キーの下で指定します。 +マッピングは、プレゼンター名からクラス名を導く規則を定めます。[設定|configuration]の `application › mapping` キーで指定します。 -このページでは、Presenterを `app/Presentation` フォルダ(または `app/UI`)に配置することを示しました。この慣例をNetteに設定ファイルで伝える必要があります。1行で十分です。 +このページでは、プレゼンターを `app/Presentation` フォルダ(または `app/UI`)に置いてきました。Nette Application 3.3 以降、これは設定しなくてよい既定の慣習です。別の構成を使う場合や、マッピングを明示的に指定したい場合、既定の設定は次の行に相当します。 ```neon application: mapping: App\Presentation\*\**Presenter ``` -マッピングはどのように機能しますか?よりよく理解するために、まずモジュールなしのアプリケーションを想像してみましょう。Presenterクラスが `App\Presentation` 名前空間に属するようにし、Presenter `Home` がクラス `App\Presentation\HomePresenter` にマッピングされるようにしたいとします。これは、この設定で実現できます。 +マッピングはどう働くのでしょうか。分かりやすくするために、まずモジュールのないアプリケーションを考えましょう。プレゼンターのクラスを `App\Presentation` 名前空間の下に置き、`Home` プレゼンターが `App\Presentation\HomePresenter` クラスに対応するようにしたいとします。それは次の設定で実現できます。 ```neon application: mapping: App\Presentation\*Presenter ``` -マッピングは、Presenter名 `Home` がマスク `App\Presentation\*Presenter` のアスタリスクを置き換え、結果としてクラス名 `App\Presentation\HomePresenter` を得るように機能します。簡単です! +マッピングは、マスク `App\Presentation\*Presenter` のアスタリスクをプレゼンター名 `Home` に置き換えて働き、最終的なクラス名 `App\Presentation\HomePresenter` になります。簡単ですね。 -しかし、この章や他の章の例でわかるように、Presenterクラスを同名のサブディレクトリに配置します。例えば、Presenter `Home` はクラス `App\Presentation\Home\HomePresenter` にマッピングされます。これは、コロンを2重にすることで実現できます(Nette Application 3.2が必要)。 +とはいえ、この章やほかの章の例で見たとおり、私たちはプレゼンターのクラスを同じ名前のサブディレクトリに置きます。たとえば `Home` プレゼンターは `App\Presentation\Home\HomePresenter` クラスに対応します。これは二重のアスタリスク `**` で実現します(Nette Application 3.2.3 以上が必要です)。 ```neon application: mapping: App\Presentation\**Presenter ``` -次に、Presenterをモジュールにマッピングします。各モジュールに対して特定のマッピングを定義できます。 +次はプレゼンターをモジュールに対応づける話に進みます。モジュールごとに個別のマッピングを定義できます。 ```neon application: @@ -503,9 +503,9 @@ application: Api: App\Api\*Presenter ``` -この設定によると、Presenter `Front:Home` はクラス `App\Presentation\Front\Home\HomePresenter` にマッピングされ、Presenter `Api:OAuth` はクラス `App\Api\OAuthPresenter` にマッピングされます。 +この設定に従うと、プレゼンター `Front:Home` は `App\Presentation\Front\Home\HomePresenter` クラスに、プレゼンター `Api:OAuth` は `App\Api\OAuthPresenter` クラスに対応します。 -モジュール `Front` と `Admin` は同様のマッピング方法を持ち、そのようなモジュールはおそらくもっと多いため、それらを置き換える一般的なルールを作成することが可能です。したがって、クラスマスクにモジュール用の新しいアスタリスクが追加されます。 +`Front` と `Admin` のモジュールは似たマッピングの形をしていて、そうしたモジュールはこれからも増えそうなので、それらをまとめる一般的な規則を作れます。クラスのマスクにモジュール用の新しいアスタリスクを足します。 ```neon application: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -これは、例えばPresenter `Admin:User:Edit` のような、より深くネストされたディレクトリ構造でも機能します。アスタリスクを持つセグメントは各レベルで繰り返され、結果はクラス `App\Presentation\Admin\User\Edit\EditPresenter` になります。 +これはより深く入れ子になったディレクトリ構成でも働きます。たとえばプレゼンター `Admin:User:Edit` では、アスタリスクの部分がモジュールの階層ごとに繰り返され、クラス `App\Presentation\Admin\User\Edit\EditPresenter` になります。 -代替の表記法は、文字列の代わりに3つのセグメントからなる配列を使用することです。この表記法は前のものと同等です。 +別の書き方として、文字列の代わりに 3 つの要素から成る配列を使えます。上に示した例について、この書き方は前のものと等価です。 ```neon application: diff --git a/application/ja/how-it-works.texy b/application/ja/how-it-works.texy index 5ba6dd8368..d7d78af3f6 100644 --- a/application/ja/how-it-works.texy +++ b/application/ja/how-it-works.texy @@ -1,103 +1,103 @@ -アプリケーションはどのように動作しますか? -********************* +アプリケーションはどう動くのか +***************
    -あなたは今、Netteドキュメントの基本憲章を読んでいます。Webアプリケーションがどのように機能するかの全体像を学びます。AからZまで、誕生の瞬間からPHPスクリプトの最後の処理まで。読み終えた後、あなたは知っているでしょう: +あなたは今、Nette ドキュメントの土台となる章を読んでいます。リクエストが生まれた瞬間から PHP スクリプトの実行が終わるまで、ウェブアプリケーションがどう動くのかを A から Z まで学びます。読み終わると次のことが分かります。 -- 全体がどのように機能するか -- Bootstrap、Presenter、DIコンテナとは何か -- ディレクトリ構造はどのようになっているか +- 全体がどう動くのか +- Bootstrap、プレゼンター、DI コンテナとは何か +- ディレクトリ構成はどんな形か
    -ディレクトリ構造 +ディレクトリ構成 ======== -[WebProject|https://github.com/nette/web-project]と呼ばれるWebアプリケーションのスケルトンの例を開き、読みながら言及されているファイルを見ることができます。 +[WebProject|https://github.com/nette/web-project]というウェブアプリケーションの雛形の例を開いてください。読みながら、話に出てくるファイルを参照できます。 -ディレクトリ構造は次のようになります。 +ディレクトリ構成はおおよそ次のようになっています。 /--pre web-project/ -├── app/ ← アプリケーションディレクトリ -│ ├── Core/ ← 実行に必要な基本クラス -│ │ └── RouterFactory.php ← URLアドレスの設定 -│ ├── Presentation/ ← Presenter、テンプレートなど -│ │ ├── @layout.latte ← レイアウトテンプレート -│ │ └── Home/ ← Home Presenterのディレクトリ -│ │ ├── HomePresenter.php ← Home Presenterクラス -│ │ └── default.latte ← defaultアクションのテンプレート -│ └── Bootstrap.php ← ブートストラップクラス Bootstrap -├── assets/ ←リソース(SCSS、TypeScript、ソース画像) -├── bin/ ← コマンドラインから実行されるスクリプト +├── 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/ ← ログ記録されたエラー +├── log/ ← 記録されたエラー ├── temp/ ← 一時ファイル、キャッシュなど -├── vendor/ ← Composerによってインストールされたライブラリ +├── vendor/ ← Composer がインストールしたライブラリ │ ├── ... -│ └── autoload.php ← インストールされたすべてのパッケージのオートローディング -├── www/ ← 公開ディレクトリまたはプロジェクトのドキュメントルート -│ ├── assets/ ←コンパイルされた静的ファイル(CSS、JS、画像、...) -│ ├── .htaccess ← mod_rewriteルール -│ └── index.php ← アプリケーションが起動する最初のファイル -└── .htaccess ← www以外のすべてのディレクトリへのアクセスを禁止 +│ └── autoload.php ← インストールされた全パッケージのオートローディング +├── www/ ← 公開ディレクトリ。プロジェクトの document-root +│ ├── assets/ ← コンパイル済みの静的ファイル(CSS、JS、画像など) +│ ├── .htaccess ← mod_rewrite の規則 +│ └── index.php ← アプリケーションを起動する最初のファイル +└── .htaccess ← www 以外のすべてのディレクトリへのアクセスを禁じます \-- -ディレクトリ構造は自由に変更でき、フォルダの名前を変更したり移動したりできます。完全に柔軟です。さらに、Netteは賢い自動検出機能を備えており、URLベースを含むアプリケーションの場所を自動的に認識します。 +ディレクトリ構成は好きなように変えられ、フォルダの名前を変えたり移動したりできます。完全に柔軟です。Nette には賢い自動検出もあり、アプリケーションの場所とその URL の基準を自動的に認識します。 -少し大きなアプリケーションでは、Presenterとテンプレートのフォルダを[サブディレクトリに分割 |directory-structure#Presenterとテンプレート]し、クラスをモジュールと呼ばれる名前空間に分割できます。 +少し大きなアプリケーションでは、プレゼンターとテンプレートのフォルダを[サブディレクトリ |directory-structure#プレゼンターとテンプレート]にまとめ、クラスを名前空間でまとめられます。これをモジュールと呼びます。 -`www/` ディレクトリは、プロジェクトのいわゆる公開ディレクトリまたはドキュメントルートを表します。アプリケーション側で何も設定を変更することなく名前を変更できます。ただし、ドキュメントルートがこのディレクトリを指すように[ホスティングを設定 |nette:troubleshooting#URLから www ディレクトリを変更または削除する方法は]する必要があります。 +`www/` ディレクトリは、プロジェクトの公開ディレクトリ、つまり document-root にあたります。アプリケーション側でほかに何も設定せずに名前を変えられます。document-root がこのディレクトリを指すよう[ホスティングを設定する |nette:troubleshooting#URL から www のディレクトリを変えたり取り除いたりするには]だけです。 -WebProjectは、[Composer |best-practices:composer]を使用してNetteを含めて直接ダウンロードすることもできます。 +WebProject は Nette ごと [Composer |best-practices:composer]で直接ダウンロードすることもできます。 ```shell composer create-project nette/web-project ``` -LinuxまたはmacOSでは、`log/` および `temp/` ディレクトリに[書き込み権限を設定 |nette:troubleshooting#ディレクトリ権限の設定]してください。 +Linux や macOS では、`log/` と `temp/` ディレクトリに[書き込みの権限 |nette:troubleshooting#ディレクトリの権限の設定]を設定してください。 -WebProjectアプリケーションは実行準備ができており、何も設定する必要はなく、`www/` フォルダにアクセスしてブラウザですぐに表示できます。 +WebProject アプリケーションはすぐに動く状態です。何も設定する必要はなく、`www/` フォルダにアクセスすればブラウザでそのまま見られます。 -HTTPリクエスト -========= +HTTP リクエスト +========== -すべては、ユーザーがブラウザでページを開いたときに始まります。つまり、ブラウザがHTTPリクエストでサーバーにノックするときです。リクエストは、公開ディレクトリ `www/` にある単一のPHPファイル、つまり `index.php` に向けられます。アドレス `https://example.com/product/123` へのリクエストであるとしましょう。適切な[サーバー設定 |nette:troubleshooting#きれいなURLのためにサーバーを設定する方法は]のおかげで、このURLも `index.php` ファイルにマッピングされ、実行されます。 +すべては、ユーザーがブラウザでページを開いたときに始まります。ブラウザはサーバーに HTTP リクエストを送ります。このリクエストは、公開ディレクトリ `www/` にあるただひとつの PHP ファイル、`index.php` に向かいます。リクエストがアドレス `https://example.com/product/123` に対するものだとしましょう。適切な[サーバーの設定 |nette:troubleshooting#きれいな URL のためにサーバーを設定するには]のおかげで、この URL も `index.php` ファイルに割り当てられ、それが実行されます。 -そのタスクは次のとおりです。 +その役目は次のとおりです。 1) 環境を初期化する -2) ファクトリを取得する -3) リクエストを処理するNetteアプリケーションを起動する +2) ファクトリを手に入れる +3) リクエストを処理する Nette アプリケーションを実行する -どのファクトリですか?私たちはトラクターではなく、Webページを作成しています!お待ちください、すぐに説明します。 +ファクトリ? 私たちはトラクターを作っているのではなく、ウェブサイトを作っているのですが。ご心配なく、すぐに説明します。 -「環境の初期化」という言葉は、例えば[Tracy|tracy:]を有効にすることを意味します。これは、エラーのログ記録や視覚化のための素晴らしいツールです。本番サーバーではエラーをログに記録し、開発サーバーでは直接表示します。したがって、初期化には、Webが本番モードで実行されているか開発モードで実行されているかを判断することも含まれます。これを行うために、Netteは[賢い自動検出 |bootstrapping#開発環境 vs 本番環境]を使用します。Webをlocalhostで実行すると、開発モードで実行されます。したがって、何も設定する必要はなく、アプリケーションは開発と本番展開の両方にすぐに準備ができています。[Bootstrapクラス|bootstrapping]に関する章で、これらの手順が実行され、詳細に説明されています。 +「環境の初期化」とは、たとえば [Tracy|tracy:]を有効にすることです。Tracy はエラーの記録と可視化のための素晴らしい道具です。本番サーバーではエラーを記録し、開発環境では直接表示します。ですから初期化には、サイトが本番モードで動いているのか開発モードで動いているのかの判定も含まれます。Nette はそのために[賢い自動検出 |bootstrapping#開発モードと本番モード]を使います。localhost でサイトを動かせば開発モードになります。何も設定する必要はなく、アプリケーションは開発にも本番の公開にもすぐ対応できます。これらの手順は [Bootstrap クラス|bootstrapping]の章で実行され、詳しく説明されています。 -3番目のポイント(はい、2番目はスキップしましたが、戻ってきます)は、アプリケーションの起動です。Netteでは、HTTPリクエストの処理は `Nette\Application\Application` クラス(以下 `Application`)が担当します。したがって、アプリケーションを起動すると言うとき、具体的にはこのクラスのオブジェクトで `run()` という適切な名前のメソッドを呼び出すことを意味します。 +3 つめ(2 つめは飛ばしましたが、あとで戻ります)はアプリケーションの実行です。Nette では HTTP リクエストの処理を `Nette\Application\Application` クラス(以下 `Application`)が担当します。ですからアプリケーションを実行するとは、具体的にはこのクラスのオブジェクトで、その名も `run()` というメソッドを呼ぶことを指します。 -Netteは、実証済みの方法論に従ってクリーンなアプリケーションを作成するように導くメンターです。そして、それらの完全に実証済みの方法論の1つは、**dependency injection**、略してDIと呼ばれます。現時点ではDIの説明で負担をかけたくありません。それについては[別の章|dependency-injection:introduction]があります。重要な結果は、主要なオブジェクトは通常、**DIコンテナ**(略してDIC)と呼ばれるオブジェクトのファクトリによって作成されることです。はい、それが少し前に話したファクトリです。そして、それは `Application` オブジェクトも作成します。そのため、最初にコンテナが必要です。`Configurator` クラスを使用してそれを取得し、`Application` オブジェクトを作成させ、その上で `run()` メソッドを呼び出すことで、Netteアプリケーションが起動します。これはまさに[index.php |bootstrapping#index.php]ファイルで行われていることです。 +Nette は指導役として、実績のある方法論に沿ってきれいなアプリケーションを書くよう導いてくれます。その中でも最も確立されたもののひとつが**依存性注入**、略して DI です。ここで DI の説明で負担をかけるつもりはありません。それには[専用の章|dependency-injection:introduction]があります。肝心の帰結は、重要なオブジェクトがふつう **DI コンテナ**(DIC)と呼ばれるオブジェクトのファクトリによって作られるということです。そう、これが先ほど触れたファクトリです。`Application` オブジェクトもここが作ってくれるので、まずコンテナが必要になります。コンテナは `Configurator` クラスで手に入れ、`Application` オブジェクトを作らせ、その `run()` メソッドを呼ぶ。こうして Nette のアプリケーションが始まります。これがまさに [index.php |bootstrapping#index.php]ファイルで起きていることです。 Nette Application ================= -Applicationクラスには1つのタスクしかありません:HTTPリクエストに応答することです。 +`Application` クラスの役目はひとつだけです。HTTP リクエストに応えることです。 -Netteで書かれたアプリケーションは、多数のいわゆるPresenter(他のフレームワークではコントローラーという用語に出会うかもしれませんが、同じものです)に分割されます。これらは、Webサイトの特定のページ(ホームページ、eコマースの製品、ログインフォーム、サイトマップフィードなど)を表すクラスです。アプリケーションは1つから数千のPresenterを持つことができます。 +Nette で書かれたアプリケーションは、いわゆるプレゼンターの集まりに分かれています(ほかのフレームワークでは「コントローラ」という言葉に出会うかもしれませんが、本質的には同じものです)。プレゼンターはクラスで、それぞれがウェブサイトの特定のページを表します。トップページ、ネットショップの商品、ログインフォーム、サイトマップのフィードなどです。アプリケーションはプレゼンターをひとつから何千まで持てます。 -Applicationは、まずいわゆるルーターに、現在のリクエストを処理するためにどのPresenterに渡すかを決定するように依頼します。ルーターは、誰の責任かを決定します。入力URL `https://example.com/product/123` を見て、設定方法に基づいて、これが例えば `id: 123` の製品を表示する(`show`)**アクション**を要求する**Presenter** `Product` の仕事であると判断します。Presenter + アクションのペアは、`Product:show` のようにコロンで区切って記述するのが良い習慣です。 +`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]の章で詳細に説明されています。 +こうしてルーターは URL を `Presenter:action` の組とパラメータに変換しました。この場合は `Product:show` と `id: 123` です。そうしたルーターがどんな形かは `app/Core/RouterFactory.php` ファイルで見られますし、[ルーティング |Routing]の章で詳しく説明します。 -続けましょう。ApplicationはPresenterの名前を知っているので、次に進むことができます。`ProductPresenter` クラスのオブジェクトを作成します。これはPresenter `Product` のコードです。より正確には、Presenterを作成するようにDIコンテナに依頼します。なぜなら、作成するのはDIコンテナの仕事だからです。 +先へ進みましょう。`Application` はプレゼンターの名前を知ったので、次に進めます。`Product` プレゼンターのコードを含む `ProductPresenter` クラスのインスタンスを作るのです。より正確には、オブジェクトの生成はその役目なので、DI コンテナにプレゼンターを作らせます。 -Presenterは次のようになります。 +プレゼンターはたとえば次のような形になります。 ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -109,92 +109,92 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function renderShow(int $id): void { - // モデルからデータを取得し、テンプレートに渡します + // モデルからデータを取得してテンプレートに渡します $this->template->product = $this->repository->getProduct($id); } } ``` -リクエストの処理はPresenterが引き継ぎます。そして、タスクは明確です:`id: 123` でアクション `show` を実行します。Presenterの言葉で言えば、これは `renderShow()` メソッドが呼び出され、パラメータ `$id` で `123` を受け取ることを意味します。 +プレゼンターがリクエストの処理を引き継ぎます。仕事ははっきりしています。`id: 123` で `show` アクションを実行することです。プレゼンターの用語でいえば、`renderShow()` メソッドが呼ばれ、`$id` パラメータに `123` を受け取る、ということです。 -Presenterは複数のアクションを処理できます。つまり、複数の `render()` メソッドを持つことができます。ただし、1つまたはできるだけ少ないアクションを持つPresenterを設計することをお勧めします。 +プレゼンターは複数のアクションを扱えます。つまり `render()` メソッドを複数持てます。とはいえ、プレゼンターはアクションひとつ、あるいはできるだけ少ない数で設計することをおすすめします。 -したがって、`renderShow(123)` メソッドが呼び出されました。そのコードは架空の例ですが、`$this->template` への書き込みによってデータがテンプレートにどのように渡されるかを見ることができます。 +というわけで `renderShow(123)` メソッドが呼ばれました。そのコードは架空の例ですが、`$this->template` に書き込むことでデータがテンプレートに渡される様子を示しています。 -その後、Presenterは応答を返します。これは、HTMLページ、画像、XMLドキュメント、ディスクからのファイルの送信、JSON、または別のページへのリダイレクトなどです。重要なのは、明示的に応答方法を指示しない場合(これは `ProductPresenter` の場合です)、応答はHTMLページを含むテンプレートのレンダリングになることです。なぜですか?なぜなら、99%の場合、テンプレートをレンダリングしたいので、Presenterはこの動作をデフォルトと見なし、作業を楽にしたいからです。それがNetteの目的です。 +続いてプレゼンターはレスポンスを返します。HTML のページ、画像、XML のドキュメント、ディスクからのファイルの送信、JSON、あるいは別のページへのリダイレクトかもしれません。大事なのは、どう応えるかを明示的に指定しなければ(`ProductPresenter` の場合がそうです)、テンプレートを HTML のページに描くのがレスポンスになる、という点です。なぜでしょうか。99 % の場合、私たちはテンプレートを描きたいからです。ですからプレゼンターは、私たちの手間を省くためにこの振る舞いを既定としています。それが Nette の本質です。 -どのテンプレートをレンダリングするかを指定する必要さえありません。パスは自動的に推測されます。`show` アクションの場合、単に `ProductPresenter` クラスと同じディレクトリにある `show.latte` テンプレートをロードしようとします。また、`@layout.latte` ファイルでレイアウトを見つけようとします([テンプレートの検索 |templates#テンプレートの検索]について詳しくはこちら)。 +描くテンプレートを指定する必要さえありません。フレームワークがパスを自動的に導きます。`show` アクションの場合は単に、`ProductPresenter` クラスと同じディレクトリにある `show.latte` テンプレートを読み込もうとします。レイアウトも `@layout.latte` ファイルから探そうとします(詳しくは[テンプレートの探索 |templates#テンプレートの探索]をご覧ください)。 -そして、テンプレートがレンダリングされます。これでPresenterとアプリケーション全体のタスクが完了し、作業は完了です。テンプレートが存在しない場合、404エラーページが返されます。Presenterについて詳しくは、[Presenter|presenters]ページをご覧ください。 +そしてテンプレートが描かれます。これでプレゼンターの、そしてアプリケーション全体の仕事が終わります。テンプレートが存在しなければ 404 のエラーページが返ります。プレゼンターについて詳しくは[プレゼンター|presenters]のページで学べます。 [* request-flow.svg *] -念のため、少し異なるURLでプロセス全体を要約してみましょう。 +念のため、少し違う URL で全体の流れをおさらいしましょう。 -1) URLは `https://example.com` になります -2) アプリケーションを起動し、コンテナを作成し、`Application::run()` を実行します -3) ルーターはURLを `Home:default` ペアとしてデコードします -4) `HomePresenter` クラスのオブジェクトが作成されます -5) `renderDefault()` メソッドが呼び出されます(存在する場合) -6) 例えば `@layout.latte` のレイアウトを持つ `default.latte` などのテンプレートがレンダリングされます +1) URL は `https://example.com` +2) アプリケーションが起動し、DI コンテナが作られ、`Application::run()` が実行されます。 +3) ルーターが URL を `Home:default` の組に読み解きます。 +4) `HomePresenter` クラスのインスタンスが作られます。 +5) (存在すれば)`renderDefault()` メソッドが呼ばれます。 +6) テンプレート、たとえば `default.latte` が、レイアウト、たとえば `@layout.latte` とともに描かれます。 -おそらく、今、多くの新しい概念に出会ったかもしれませんが、それらが意味をなすことを願っています。Netteでのアプリケーション開発は非常に快適です。 +ここまでで新しい概念にたくさん出会ったかもしれませんが、どれも筋が通っていると感じてもらえたはずです。Nette でのアプリケーション開発は、驚くほど素直です。 テンプレート ====== -テンプレートについて言えば、Netteでは[Latte |latte:]テンプレートシステムが使用されます。そのため、テンプレートの拡張子は `.latte` です。Latteが使用される理由は、PHPで最も安全なテンプレートシステムであると同時に、最も直感的なシステムでもあるためです。多くの新しいことを学ぶ必要はありません。PHPの知識といくつかのタグで十分です。すべては[ドキュメント |templates]で学ぶことができます。 +テンプレートといえば、Nette は [Latte |latte:]というテンプレートシステムを使います。テンプレートのファイルの拡張子が `.latte` なのはそのためです。Latte を使う第一の理由は、PHP で最も安全なテンプレートシステムであり、しかも最も直感的だからです。新しく覚えることは多くありません。PHP の知識といくつかのタグで十分です。必要なことはすべて[ドキュメント |templates]にあります。 -テンプレートでは、他のPresenterとアクションへの[リンクが作成 |creating-links]されます。 +テンプレートでは、ほかのプレゼンターやアクションへの[リンクを作ります |creating-links]。次のようにです。 ```latte -製品詳細 +商品の詳細 ``` -単に実際のURLの代わりに、既知の `Presenter:action` ペアを記述し、必要に応じてパラメータを指定します。トリックは `n:href` にあり、これはこの属性がNetteによって処理されることを示します。そして、次のように生成します。 +実際の URL の代わりに、見慣れた `Presenter:action` の組を書き、必要なパラメータを添えるだけです。仕掛けは `n:href` にあり、これがこの属性を処理するよう Nette に伝えます。すると次が生成されます。 ```latte -製品詳細 +商品の詳細 ``` -URLの生成は、前述のルーターが担当します。Netteのルーターが例外的なのは、URLからPresenter:アクションのペアへの変換だけでなく、逆方向、つまりPresenter名+アクション+パラメータからURLを生成することもできることです。 これにより、NetteではテンプレートやPresenterの文字を1つも変更することなく、完成したアプリケーション全体のURL形式を完全に変更できます。ルーターを編集するだけで。 また、これにより、いわゆるカノニカル化が可能になります。これはNetteのもう1つのユニークな機能であり、異なるURLで重複コンテンツが存在するのを自動的に防ぐことで、より良いSEO(インターネットでの検索エンジンの最適化)に貢献します。 多くのプログラマーはこれを驚くべきことだと考えています。 +URL の生成は、先ほどのルーターが担当します。Nette のルーターが並外れているのは、URL から `Presenter:action` の組への変換だけでなく、その逆、つまりプレゼンター名、アクション、パラメータから URL を生成することもできる点です。おかげで、完成した Nette のアプリケーション全体の URL の形式を、テンプレートやプレゼンターの文字を 1 つも変えずに、ルーターを直すだけで丸ごと変えられます。これはいわゆる正規化も可能にします。同じ内容が異なる URL に存在するのを自動的に防いで SEO を高める、Nette ならではのもうひとつの機能です。多くのプログラマーがこの能力に驚きます。 -インタラクティブコンポーネント -=============== +インタラクティブなコンポーネント +================ -Presenterについてもう1つお伝えしなければならないことがあります:それらには組み込みのコンポーネントシステムがあります。DelphiやASP.NET Web Formsを知っている古い世代の方々には馴染みがあるかもしれません。ReactやVue.jsも、遠いながらも似たようなものに基づいています。PHPフレームワークの世界では、これは完全にユニークな機能です。 +プレゼンターについてもうひとつお伝えすることがあります。プレゼンターには組み込みのコンポーネントのしくみがあります。経験のある方は Delphi や ASP.NET Web Forms の似たものを思い出すかもしれません。React や Vue.js も、いくらか近い考え方の上に築かれています。PHP のフレームワークの世界では、これはまったく他にない機能です。 -コンポーネントは、ページ(つまりPresenter)に挿入する独立した再利用可能なユニットです。[フォーム |forms:in-presenter]、[データグリッド |https://componette.org/contributte/datagrid/]、メニュー、投票など、繰り返し使用する意味のあるものであれば何でもかまいません。独自のコンポーネントを作成したり、[膨大な品揃え |https://componette.org]のオープンソースコンポーネントを使用したりできます。 +コンポーネントは独立した再利用できる部品で、ページ(つまりプレゼンター)に埋め込みます。[フォーム |forms:in-presenter]、[データグリッド |https://componette.org/contributte/datagrid/]、メニュー、アンケートなど、再利用する意味のあるものなら何でもです。自分でコンポーネントを作ることも、[豊富な品揃え |https://componette.org]のオープンソースのコンポーネントを使うこともできます。 -コンポーネントは、アプリケーション開発へのアプローチに根本的な影響を与えます。事前に準備されたユニットからページを組み立てる新しい可能性を開きます。そして、さらに[ハリウッド |components#ハリウッドスタイル]と共通点があります。 +コンポーネントはアプリケーション開発への向き合い方を根本から変えます。あらかじめ用意された部品からページを組み立てるという新しい可能性を開きます。そして[ハリウッド |components#ハリウッド流]とも通じるものがあります。 -DIコンテナと設定 -========= +DI コンテナと設定 +========== -DIコンテナ、またはオブジェクトファクトリは、アプリケーション全体の心臓部です。 +DI コンテナ、つまりオブジェクトのファクトリは、アプリケーション全体の心臓です。 -心配しないでください。前の行からそう思われるかもしれませんが、これは魔法のブラックボックスではありません。実際には、Netteによって生成され、キャッシュディレクトリに保存される、かなり退屈なPHPクラスです。`createServiceAbcd()` のような名前の多くのメソッドがあり、それぞれがオブジェクトを作成して返すことができます。はい、`createServiceApplication()` メソッドもあり、これは `index.php` ファイルでアプリケーションを起動するために必要だった `Nette\Application\Application` を作成します。そして、個々のPresenterを作成するメソッドもあります。などなど。 +ご心配なく。これまでの行から想像されるような魔法のブラックボックスではありません。実際には、Nette が生成してキャッシュディレクトリに保存する、かなり地味な PHP のクラスです。`createServiceAbcd()` のような名前のメソッドが数多く入っていて、それぞれが特定のオブジェクトを作って返せます。そう、`index.php` でアプリケーションを実行するのに必要だった `Nette\Application\Application` のインスタンスを作る `createServiceApplication__application()` メソッドもあります。個々のプレゼンターを作るメソッドなどもあります。 -DIコンテナが作成するオブジェクトは、何らかの理由でサービスと呼ばれます。 +DI コンテナが作るオブジェクトは、なぜかサービスと呼ばれます。 -このクラスの本当に特別な点は、あなたがそれをプログラミングするのではなく、フレームワークがプログラミングすることです。それは実際にPHPコードを生成し、ディスクに保存します。あなたは、コンテナがどのオブジェクトを作成できるようにすべきか、そして具体的にどのように作成すべきかの指示を与えるだけです。そして、これらの指示は[設定ファイル |bootstrapping#DIコンテナの設定]に書かれており、[NEON|neon:format]形式が使用されるため、拡張子も `.neon` です。 +このクラスが本当に特別なのは、あなたがそれをプログラムしないという点です。フレームワークがするのです。実際に PHP のコードを生成してディスクに保存します。あなたは、コンテナがどのオブジェクトを作れるべきか、そしてそれを正確にどう作るかの指示を与えるだけです。この指示は[設定ファイル |bootstrapping#DI コンテナの設定]に書きます。設定ファイルは [NEON|neon:format]形式を使うので、拡張子は `.neon` です。 -設定ファイルは、純粋にDIコンテナに指示するために使用されます。したがって、例えば[session |http:configuration#セッション]セクションで `expiration: 14 days` オプションを指定すると、DIコンテナはセッションを表す `Nette\Http\Session` オブジェクトを作成するときに、その `setExpiration('14 days')` メソッドを呼び出し、それによって設定が現実になります。 +設定ファイルは、純粋に DI コンテナへ指示を与えるためのものです。ですからたとえば [session |http:configuration#セッション]セクションで `expiration: 14 days` を指定すると、DI コンテナはセッションを表す `Nette\Http\Session` オブジェクトを作るときに `setExpiration('14 days')` メソッドを呼び、その設定を実現します。 -[設定できるすべてのこと |nette:configuring]と、[独自のサービスを定義する方法 |dependency-injection:services]を説明する章全体が用意されています。 +何を[設定できるか |nette:configuring]、そして[独自のサービスをどう定義するか |dependency-injection:services]を説明する章がまるごと用意されています。 -サービスの作成に少し慣れると、[autowiring |dependency-injection:autowiring]という言葉に出くわします。これは、信じられないほど人生を簡素化する機能です。何もする必要なく、必要な場所(例えばクラスのコンストラクタ)にオブジェクトを自動的に渡すことができます。NetteのDIコンテナが小さな奇跡であることに気づくでしょう。 +サービスの生成に少し踏み込むと、[オートワイヤリング |dependency-injection:autowiring]という言葉に出会います。これはあなたの生活を驚くほど楽にしてくれる機能です。あなたが何もしなくても、必要な場所(たとえばクラスのコンストラクタ)にオブジェクトを自動的に渡してくれます。Nette の DI コンテナが小さな奇跡であることに気づくでしょう。 -次へ進むには? -======= +次は何を? +===== -Netteのアプリケーションの基本原則を見てきました。まだ非常に表面的ですが、すぐに深く掘り下げ、やがて素晴らしいWebアプリケーションを作成できるようになるでしょう。次にどこへ進むべきですか?[最初のアプリケーションを作成する|quickstart:]チュートリアルを試しましたか? +Nette のアプリケーションの基本原則を見てきました。ここまでは表面をなぞる概観でしたが、まもなくもっと深く踏み込み、やがては素晴らしいウェブアプリケーションを作れるようになります。次はどこへ行きましょうか。[最初のアプリケーションを作ろう|quickstart:]のチュートリアルはもう試しましたか。 -上記に加えて、Netteには[便利なクラス|utils:]の武器庫全体、[データベースレイヤー|database:]などがあります。ドキュメントをざっと見てみてください。または[ブログ|https://blog.nette.org]をご覧ください。多くの興味深いことを見つけるでしょう。 +ここまで説明したもののほかにも、Nette には[便利なクラス|utils:]の一式や[データベース層|database:]などがあります。ドキュメントをあちこちクリックしてみてください。あるいは[ブログ|https://blog.nette.org]を訪ねてみてください。面白いものがたくさん見つかります。 -フレームワークがあなたに多くの喜びをもたらしますように 💙 +このフレームワークがあなたに大きな喜びをもたらしますように 💙 diff --git a/application/ja/multiplier.texy b/application/ja/multiplier.texy index 64f8a4ef3c..40d0258078 100644 --- a/application/ja/multiplier.texy +++ b/application/ja/multiplier.texy @@ -1,24 +1,24 @@ -Multiplier: 動的コンポーネント -********************* +Multiplier: 動的なコンポーネント +********************** .[perex] -インタラクティブコンポーネントを動的に作成するためのツール +インタラクティブなコンポーネントを動的に作るための道具です。 -典型的な例から始めましょう:eコマースサイトに商品のリストがあり、それぞれについてカートに商品を追加するためのフォームを表示したいとします。可能なバリアントの1つは、リスト全体を1つのフォームでラップすることです。しかし、[api:Nette\Application\UI\Multiplier]ははるかに便利な方法を提供します。 +典型的な例から始めましょう。ネットショップの商品一覧で、各商品に「カートに追加」のフォームを付けたいとします。ひとつのやり方は、一覧全体をひとつのフォームで包むことです。しかし [api:Nette\Application\UI\Multiplier]は、はるかに便利な方法を提供します。 -Multiplierを使用すると、複数のコンポーネントのファクトリを便利に定義できます。これはネストされたコンポーネントの原則に基づいて機能します - [api:Nette\ComponentModel\Container]から継承する各コンポーネントは、他のコンポーネントを含むことができます。 +Multiplier を使うと、複数のコンポーネントのファクトリを手軽に定義できます。原理は入れ子のコンポーネントで、[api:Nette\ComponentModel\Container]を継承したコンポーネントは、ほかのコンポーネントを含められます。 .[tip] -ドキュメントの[コンポーネントモデル |components#コンポーネントの詳細]に関する章、または[Honza Tvrdíkによる講演|https://www.youtube.com/watch?v=8y3LLexWu-I]を参照してください。 +ドキュメントの[コンポーネントモデル |components#コンポーネントの詳細]の章をご覧ください。 -Multiplierの本質は、コンストラクタで渡されたコールバックを使用して子を動的に作成できる親の役割を果たすことです。例を参照してください。 +Multiplier の本質は、コンストラクタに渡されたコールバックを使って子を動的に作れる親として働くことです。例を見てみましょう。 ```php protected function createComponentShopForm(): Multiplier { return new Multiplier(function () { $form = new Nette\Application\UI\Form; - $form->addInteger('count', '商品数:') + $form->addInteger('amount', '数量:') ->setRequired(); $form->addSubmit('send', 'カートに追加'); return $form; @@ -26,7 +26,7 @@ protected function createComponentShopForm(): Multiplier } ``` -これで、テンプレートで各商品についてフォームを簡単にレンダリングできます - そして、それぞれが本当にユニークなコンポーネントになります。 +これでテンプレートでは、商品ごとにフォームをそのまま描けます。しかもそのひとつひとつが本当に別々のコンポーネントになります。 ```latte {foreach $items as $item} @@ -37,23 +37,23 @@ protected function createComponentShopForm(): Multiplier {/foreach} ``` -`{control}` タグで渡される引数は、次のことを示す形式です。 +`{control}` タグに渡した引数は、次の意味を持つ形式に従っています。 -1. `shopForm` コンポーネントを取得する -2. そして、そこから子 `$item->id` を取得する +1. コンポーネント `shopForm` を取得する。 +2. そこから `$item->id` という名前の子を取得する。 -ポイント **1.** の最初の呼び出しでは、`shopForm` はまだ存在しないため、そのファクトリ `createComponentShopForm` が呼び出されます。取得されたコンポーネント(Multiplierのインスタンス)で、特定のフォームのファクトリが呼び出されます - これは、コンストラクタでMultiplierに渡した匿名関数です。 +**1** の最初の呼び出しでは `shopForm` コンポーネントがまだ存在しないので、そのファクトリ `createComponentShopForm` が呼ばれます。次に、得られたコンポーネント(Multiplier のインスタンス)に対して、個別のフォームのファクトリが呼ばれます。それが Multiplier のコンストラクタに渡した無名関数です。 -foreachの次の反復では、`createComponentShopForm` メソッドは呼び出されません(コンポーネントは存在します)が、異なる子を探しているため(`$item->id` は各反復で異なります)、匿名関数が再度呼び出され、新しいフォームが返されます。 +foreach ループの次の反復では、`createComponentShopForm` メソッドはもう呼ばれません(コンポーネントがすでに存在するからです)。しかし探している子が違う(反復ごとに `$item->id` が違う)ので、無名関数がもう一度呼ばれ、新しいフォームを返します。 -残っているのは、フォームが実際に意図した商品をカートに追加することを確認することだけです - 現在、各商品のフォームはまったく同じです。Multiplierのプロパティ(および一般的にNette Frameworkのコンポーネントファクトリ)が役立ちます。つまり、各ファクトリは最初の引数として作成されるコンポーネントの名前を受け取ります。この場合、それは `$item->id` であり、これはまさに必要なデータです。したがって、フォームの作成を少し変更するだけで十分です。 +あとはフォームが正しい商品をカートに追加するようにするだけです。今のところフォームはどの商品でも同じだからです。ここで Multiplier の(そして一般に Nette Framework のどのコンポーネントファクトリの)性質が役立ちます。どのファクトリも、作られるコンポーネントの名前を第 1 引数として受け取ります。さらに Multiplier のファクトリは、第 2 引数として Multiplier のインスタンス自身を受け取ります。この場合、第 1 引数は `$item->id` であり、まさに必要な情報です。ですからフォームの生成を少し書き換えるだけで済みます。 ```php protected function createComponentShopForm(): Multiplier { - return new Multiplier(function ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; - $form->addInteger('count', '商品数:') + $form->addInteger('amount', '数量:') ->setRequired(); $form->addHidden('itemId', $itemId); $form->addSubmit('send', 'カートに追加'); diff --git a/application/ja/presenters.texy b/application/ja/presenters.texy index 6e7e8e2932..61138b6df4 100644 --- a/application/ja/presenters.texy +++ b/application/ja/presenters.texy @@ -1,39 +1,39 @@ -Presenter -********* +プレゼンター +******
    -NetteでPresenterとテンプレートを作成する方法について学びます。読み終えた後、あなたは知っているでしょう: +Nette でプレゼンターとテンプレートをどう書くのかを見ていきます。読み終えると次のことが分かります。 -- Presenterがどのように機能するか -- パーシステントパラメータとは何か -- テンプレートがどのようにレンダリングされるか +- プレゼンターがどう動くのか +- 永続パラメータとは何か +- テンプレートがどう描かれるのか
    -[すでに知っているように |how-it-works#Nette Application]、PresenterはWebアプリケーションの特定のページ(ホームページ、eコマースの製品、ログインフォーム、サイトマップフィードなど)を表すクラスです。アプリケーションは1つから数千のPresenterを持つことができます。他のフレームワークでは、コントローラーとも呼ばれます。 +プレゼンターがウェブアプリケーションの特定のページ、たとえばトップページ、ネットショップの商品、ログインフォーム、サイトマップのフィードなどを表すクラスであることは、[すでに見てきました |how-it-works#Nette Application]。アプリケーションはプレゼンターをひとつから何千まで持てます。ほかのフレームワークではコントローラとも呼ばれます。 -通常、Presenterという用語は、Webインターフェースの生成に適した[api:Nette\Application\UI\Presenter]クラスの子孫を意味し、この章の残りの部分で扱います。一般的な意味では、Presenterは[api:Nette\Application\IPresenter]インターフェースを実装する任意のオブジェクトです。 +ふつうプレゼンターという語は、ウェブのインターフェースを生成するのに向いた [api:Nette\Application\UI\Presenter]クラスの子孫を指し、この章の以降もそれを中心に扱います。より一般的な意味では、プレゼンターとは [api:Nette\Application\IPresenter]インターフェースを実装した任意のオブジェクトです。 -Presenterのライフサイクル -================= +プレゼンターのライフサイクル +============== -Presenterのタスクは、リクエストを処理し、応答(HTMLページ、画像、リダイレクトなど)を返すことです。 +プレゼンターの役目は、リクエストを処理してレスポンス(HTML のページ、画像、リダイレクトなど)を返すことです。 -したがって、最初にリクエストが渡されます。これは直接のHTTPリクエストではなく、ルーターの助けを借りてHTTPリクエストが変換された[api:Nette\Application\Request]オブジェクトです。Presenterはリクエストの処理をこれから示す他のメソッドに賢く委任するため、通常はこのオブジェクトに直接触れることはありません。 +ですからまずリクエストが渡されます。これは直接の HTTP リクエストではなく、ルーターの助けを借りて HTTP リクエストが変換された [api:Nette\Application\Request]オブジェクトです。このオブジェクトを直接扱うことはふつうありません。プレゼンターがリクエストの処理をほかのメソッドに巧みに委ねるからです。それをこれから見ていきます。 -[* lifecycle.svg *] *** *Presenterのライフサイクル* .<> +[* lifecycle.svg *] *** プレゼンターのライフサイクル .<> -この図は、存在する場合に上から下に順に呼び出されるメソッドのリストを表しています。これらのメソッドのいずれも存在する必要はなく、単一のメソッドを持たない完全に空のPresenterを持ち、それに基づいて単純な静的Webサイトを構築できます。 +図は、存在すれば上から下へ順に呼ばれるメソッドの一覧を示しています。どれも必須ではありません。メソッドがひとつもない、まったく空のプレゼンターを作り、その上に単純な静的サイトを築くこともできます。 `__construct()` --------------- -コンストラクタは、オブジェクト作成時に呼び出されるため、Presenterのライフサイクルには完全には属しません。しかし、その重要性のために記載しています。コンストラクタ([injectメソッド|best-practices:inject-method-attribute]とともに)は、依存関係を渡すために使用されます。 +コンストラクタは、オブジェクトが作られる瞬間に呼ばれるので、厳密にはプレゼンターのライフサイクルには属しません。それでもその重要さゆえに触れておきます。コンストラクタは([inject メソッド|best-practices:inject-method-attribute]とともに)依存関係を渡すために使います。 -Presenterは、アプリケーションのビジネスロジックを処理したり、データベースへの書き込みや読み取りを行ったり、計算を実行したりすべきではありません。これらは、モデルと呼ばれるレイヤーのクラスの仕事です。例えば、`ArticleRepository` クラスは記事の読み込みと保存を担当するかもしれません。Presenterがそれを使用できるようにするには、[依存性注入 |dependency-injection:passing-dependencies]を使用して渡してもらいます。 +プレゼンターは、アプリケーションのビジネスロジックを扱ったり、データベースに読み書きしたり、計算したりすべきではありません。それはモデルと呼ばれる層のクラスの責務です。たとえば `ArticleRepository` クラスが記事の読み込みと保存を担うでしょう。プレゼンターがそれを使うには、[依存性注入で渡してもらう |dependency-injection:passing-dependencies]必要があります。 ```php @@ -50,44 +50,47 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -リクエストを受信するとすぐに `startup()` メソッドが呼び出されます。プロパティの初期化、ユーザー権限の検証などに使用できます。メソッドは常に親 `parent::startup()` を呼び出す必要があります。 +リクエストを受け取った直後に `startup()` メソッドが呼ばれます。プロパティの初期化やユーザーの権限の確認などに使えます。このメソッドは必ず親を呼ぶ必要があります: `parent::startup()`。 `action(args...)` .{toc: action()} -------------------------------------------------- -`render()` メソッドに似ています。`render()` は後でレンダリングされる特定のテンプレートのデータを準備することを目的としていますが、`action()` ではテンプレートのレンダリングとは無関係にリクエストが処理されます。例えば、データが処理され、ユーザーがログインまたはログアウトされ、などが行われ、その後[別の場所にリダイレクト |#リダイレクト]されます。 +`render()` メソッドと似ています。`render()` がこのあと描かれる特定のテンプレートのためにデータを用意するものであるのに対し、`action()` はリクエストを処理するもので、そのあとにテンプレートを描くとは限りません。たとえばデータを処理したり、ユーザーをログイン・ログアウトさせたりしてから、[別の場所へリダイレクト |#リダイレクト]することもあります。 -重要なのは、`action()` が `render()` より前に呼び出されるため、そこで将来のイベントの流れを変更できることです。つまり、レンダリングされるテンプレートと呼び出される `render()` メソッドを `setView('jineView')` を使用して変更できます。 +大事なのは、`action()` が `render()` より*先に*呼ばれることです。おかげで、アクションのメソッドの中でリクエストの流れを変えられます。たとえば `setView('otherView')` を使って、描かれるテンプレートや、呼ばれる `render()` メソッドさえ変えられます。 -メソッドにはリクエストからのパラメータが渡されます。パラメータに型を指定することが可能であり、推奨されます。例えば `actionShow(int $id, ?string $slug = null)` - パラメータ `id` が欠落している場合、または整数でない場合、Presenterは[404エラー |#404エラーなど]を返し、動作を終了します。 +.{data-version:3.2.3} +`switch('otherAction')` メソッドを使えば、まったく別のアクションに切り替えることもできます。現在のメソッドを中断し、代わりに新しいアクションの `action()` と `render()` メソッドを実行します(そして自動的な[正規化|#正規化]を無効にします)。リクエスト自体は続き、いま走っているメソッドだけが中断されます。 + +リクエストのパラメータがメソッドに渡されます。これらのパラメータには型を指定でき、そうすることをおすすめします。たとえば `actionShow(int $id, ?string $slug = null)` です。`id` パラメータがない、あるいは整数でない場合、プレゼンターは [404 エラー |#404 などのエラー]を返して終わります。 `handle(args...)` .{toc: handle()} -------------------------------------------------- -このメソッドは、[コンポーネント |components#シグナル]に関する章で学ぶ、いわゆるシグナルを処理します。これは主にコンポーネントとAJAXリクエストの処理を目的としています。 +このメソッドは、いわゆるシグナルを処理します。シグナルについては[コンポーネント |components#シグナル]の章で学びます。主にコンポーネントと AJAX リクエストの処理のためのものです。 -メソッドには、`action()` の場合と同様に、型チェックを含むリクエストからのパラメータが渡されます。 +`action()` と同じく、型チェックも含めてリクエストのパラメータがメソッドに渡されます。 `beforeRender()` ---------------- -`beforeRender` メソッドは、その名前が示すように、各 `render()` メソッドの前に呼び出されます。共通のテンプレート設定、レイアウトへの変数の受け渡しなどに使用されます。 +`beforeRender` メソッドは、その名のとおり、すべての `render()` メソッドの前に呼ばれます。テンプレートの共通の設定、レイアウトへの変数の受け渡しなどに使います。 `render(args...)` .{toc: render()} ---------------------------------------------- -後続のレンダリングのためにテンプレートを準備し、データを渡すなどの場所です。 +ここでは、このあと描かれるテンプレートを準備し、データを渡したりします。 -メソッドには、`action()` の場合と同様に、型チェックを含むリクエストからのパラメータが渡されます。 +`action()` と同じく、型チェックも含めてリクエストのパラメータがメソッドに渡されます。 ```php public function renderShow(int $id): void { - // モデルからデータを取得し、テンプレートに渡します + // モデルからデータを取得してテンプレートに渡します $this->template->article = $this->articles->getById($id); } ``` @@ -96,104 +99,126 @@ public function renderShow(int $id): void `afterRender()` --------------- -`afterRender` メソッドは、名前が再び示すように、各 `render()` メソッドの後に呼び出されます。これはむしろ例外的に使用されます。 +`afterRender` メソッドは、これもまた名のとおり、すべての `render()` メソッドのあとに呼ばれます。使われることはむしろ稀です。 `shutdown()` ------------ -Presenterのライフサイクルの最後に呼び出されます。 +プレゼンターのライフサイクルの終わりに呼ばれます。 -**先に進む前に、良いアドバイス**。ご覧のとおり、Presenterは複数のアクション/ビューを処理できます。つまり、複数の `render()` メソッドを持つことができます。ただし、1つまたはできるだけ少ないアクションを持つPresenterを設計することをお勧めします。 +イベント +---- +プレゼンターのライフサイクルの一部として呼ばれる `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()` の直前に呼ばれます。 -応答の送信 -===== -Presenterの応答は通常、[HTMLページを含むテンプレートのレンダリング|templates]ですが、ファイルの送信、JSON、または別のページへのリダイレクトなども可能です。 +**先へ進む前にひとつ助言を。** ご覧のとおり、プレゼンターは複数のアクション/ビューを扱えます。つまり `render()` メソッドを複数持てます。とはいえ、プレゼンターはアクションひとつ、あるいはできるだけ少ない数で設計することをおすすめします。 + + +レスポンスの送信 +======== -ライフサイクルのいつでも、次のいずれかのメソッドを使用して応答を送信し、同時にPresenterを終了できます。 +プレゼンターのレスポンスはふつう[テンプレートを HTML のページに描くこと|templates]ですが、ファイルや JSON を送ることも、別のページへリダイレクトすることもできます。 -- `redirect()`, `redirectPermanent()`, `redirectUrl()`, `forward()` は[#リダイレクト]します -- `error()` は[エラーのため |#404エラーなど]にPresenterを終了します -- `sendJson($data)` はPresenterを終了し、JSON形式で[データを送信 |#JSONの送信]します -- `sendTemplate()` はPresenterを終了し、すぐに[テンプレートをレンダリング |templates]します -- `sendResponse($response)` はPresenterを終了し、[カスタム応答 |#応答]を送信します -- `terminate()` は応答なしでPresenterを終了します +ライフサイクルのどの時点でも、次のいずれかのメソッドでレスポンスを送り、同時にプレゼンターを終わらせられます。 -これらのメソッドのいずれも呼び出さない場合、Presenterは自動的にテンプレートのレンダリングに進みます。なぜですか?なぜなら、99%の場合、テンプレートをレンダリングしたいので、Presenterはこの動作をデフォルトと見なし、作業を楽にしたいからです。 +- `redirect()`、`redirectPermanent()`、`redirectUrl()`、`forward()` は[リダイレクト |#リダイレクト]を行います +- `error()` は[エラーのために |#404 などのエラー]プレゼンターを終わらせます +- `sendJson($data)` はプレゼンターを終わらせ、JSON 形式で[データを送ります |#JSON の送信] +- `sendTemplate()` はプレゼンターを終わらせ、すぐに[テンプレートを描きます |templates] +- `sendResponse($response)` はプレゼンターを終わらせ、[独自のレスポンス |#レスポンス]を送ります +- `terminate()` はレスポンスなしでプレゼンターを終わらせます + +これらのメソッドはいずれも、静かな終了の例外 `Nette\Application\AbortException` を投げて、ただちにプレゼンターを終わらせます。 + +これらのメソッドをどれも呼ばなければ、プレゼンターは自動的にテンプレートの描画に進みます。なぜでしょうか。99 % の場合、私たちはテンプレートを描きたいからです。ですからプレゼンターは、私たちの手間を省くためにこの振る舞いを既定としています。 リンクの作成 ====== -Presenterには `link()` メソッドがあり、これを使用して他のPresenterへのURLリンクを作成できます。最初のパラメータはターゲットのPresenterとアクションであり、その後に渡される引数が続きます。引数は配列として指定できます。 +プレゼンターには、ほかのプレゼンターへの URL リンクを作る `link()` メソッドがあります。第 1 パラメータは行き先のプレゼンターとアクションで、そのあとに引数が続きます。引数は配列としても渡せます。 ```php $url = $this->link('Product:show', $id); -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -テンプレートでは、他のPresenterとアクションへのリンクは次のように作成されます。 +テンプレートでは、ほかのプレゼンターやアクションへのリンクを次のように作ります。 ```latte -製品詳細 +商品の詳細 ``` -単に実際のURLの代わりに、既知の `Presenter:action` ペアを記述し、必要に応じてパラメータを指定します。トリックは `n:href` にあり、これはこの属性がLatteによって処理され、実際のURLが生成されることを示します。Netteでは、URLについて考える必要はまったくなく、Presenterとアクションについて考えるだけです。 +実際の URL の代わりに、見慣れた `Presenter:action` の組を書き、必要なパラメータを添えるだけです。仕掛けは `n:href` にあり、これがこの属性を処理して本当の URL を生成するよう Latte に伝えます。Nette では URL のことを考える必要はまったくなく、プレゼンターとアクションのことだけを考えればよいのです。 -詳細については、[URLリンクの作成|creating-links]の章を参照してください。 +詳しくは [URL リンクの作成|creating-links]の章をご覧ください。 リダイレクト ====== -別のPresenterに移動するには、`redirect()` および `forward()` メソッドを使用します。これらは[link() |#リンクの作成]メソッドと非常によく似た構文を持っています。 +別のプレゼンターに切り替えるには `redirect()` と `forward()` メソッドを使います。[link() |#リンクの作成]メソッドとよく似た構文です。 -`forward()` メソッドは、HTTPリダイレクトなしで即座に新しいPresenterに移動します。 +`forward()` メソッドは、HTTP のリダイレクトなしにただちに新しいプレゼンターへ切り替えます。 ```php $this->forward('Product:show'); ``` -HTTPコード302(または現在のリクエストメソッドがPOSTの場合は303)を持ついわゆる一時的なリダイレクトの例: +HTTP コード 302(現在のリクエストのメソッドが POST なら 303)の一時的なリダイレクトの例です。 ```php $this->redirect('Product:show', $id); ``` -HTTPコード301を持つ永続的なリダイレクトは、次のように実現できます。 +HTTP コード 301 の恒久的なリダイレクトには、次を使います。 ```php $this->redirectPermanent('Product:show', $id); ``` -アプリケーション外の別のURLにリダイレクトするには、`redirectUrl()` メソッドを使用します。2番目のパラメータとしてHTTPコードを指定できます。デフォルトは302(または現在のリクエストメソッドがPOSTの場合は303)です。 +アプリケーションの外の別の URL へは `redirectUrl()` メソッドでリダイレクトできます。HTTP コードは第 2 パラメータで指定でき、既定は 302(現在のリクエストのメソッドが POST なら 303)です。 ```php $this->redirectUrl('https://nette.org'); ``` -リダイレクトは、いわゆるサイレント終了例外 `Nette\Application\AbortException` をスローすることで、Presenterの動作を即座に終了します。 +リダイレクトは、いわゆる静かな終了の例外 `Nette\Application\AbortException` を投げて、ただちにプレゼンターの動作を終わらせます。 -リダイレクトの前に、[#フラッシュメッセージ]、つまりリダイレクト後にテンプレートに表示されるメッセージを送信できます。 +リダイレクトの前に、[フラッシュメッセージ |#フラッシュメッセージ]、つまりリダイレクト後のテンプレートに表示されるメッセージを送れます。 フラッシュメッセージ ========== -これらは通常、何らかの操作の結果を通知するメッセージです。フラッシュメッセージの重要な特徴は、リダイレクト後もテンプレートで利用できることです。表示後もさらに30秒間有効です。例えば、転送エラーのためにユーザーがページを更新した場合でも、メッセージはすぐには消えません。 +これはふつう、何らかの操作の結果を知らせるメッセージです。フラッシュメッセージの大事な性質は、リダイレクト後もテンプレートで使えることです。一度表示されたあとも、さらに 30 秒は有効なままです。たとえば通信のエラーでユーザーがページを再読み込みしても、メッセージがすぐ消えることはありません。 -[flashMessage() |api:Nette\Application\UI\Control::flashMessage()] メソッドを呼び出すだけで、Presenterがテンプレートへの受け渡しを処理します。最初のパラメータはメッセージのテキストであり、オプションの2番目のパラメータはそのタイプ(error、warning、infoなど)です。`flashMessage()` メソッドは、フラッシュメッセージのインスタンスを返し、これに追加情報を追加できます。 +[flashMessage() |api:Nette\Application\UI\Control::flashMessage()]メソッドを呼ぶだけで、テンプレートへの受け渡しはプレゼンターが担当します。第 1 パラメータはメッセージの本文、省略可能な第 2 パラメータはその種類(error、warning、info など)です。`flashMessage()` メソッドはフラッシュメッセージのインスタンスを返すので、追加の情報を足せます。 ```php -$this->flashMessage('項目が削除されました。'); -$this->redirect(/* ... */); // そしてリダイレクトします +$this->flashMessage('項目を削除しました。'); +$this->redirect(/* ... */); // そしてリダイレクト ``` -これらのメッセージは、テンプレートでは `$flashes` 変数で `stdClass` オブジェクトとして利用できます。これらには `message`(メッセージテキスト)、`type`(メッセージタイプ)プロパティが含まれ、前述のユーザー情報を含むこともできます。例えば、次のようにレンダリングします。 +テンプレートでは、これらのメッセージが `$flashes` 変数に `stdClass` オブジェクトとして入っていて、`message`(メッセージの本文)、`type`(メッセージの種類)、そして先ほど触れたユーザーが足した情報のプロパティを持ちます。次のように描きます。 ```latte {foreach $flashes as $flash} @@ -202,10 +227,10 @@ $this->redirect(/* ... */); // そしてリダイレクトします ``` -404エラーなど -======== +404 などのエラー +========== -リクエストを満たせない場合、例えば表示したい記事がデータベースに存在しないなどの理由で、`error(?string $message = null, int $httpCode = 404)` メソッドを使用して404エラーをスローします。 +たとえば表示したい記事がデータベースにないなど、リクエストに応えられない場合は、`error(string $message = '', int $httpCode = 404)` メソッドで 404 のエラーを投げます。 ```php public function renderShow(int $id): void @@ -218,13 +243,13 @@ public function renderShow(int $id): void } ``` -エラーのHTTPコードは2番目のパラメータとして渡すことができます。デフォルトは404です。メソッドは `Nette\Application\BadRequestException` 例外をスローするように機能し、その後 `Application` は制御をエラーPresenterに渡します。これは、発生したエラーを通知するページを表示するタスクを持つPresenterです。 エラーPresenterの設定は、[application設定|configuration]で行われます。 +HTTP のエラーコードは第 2 パラメータで渡せ、既定は 404 です。このメソッドは `Nette\Application\BadRequestException` を投げることで働き、そのあと `Application` が制御をエラー用のプレゼンターに渡します。これは、起きたエラーを知らせるページを表示する役目のプレゼンターです。エラー用のプレゼンターは[アプリケーションの設定|configuration]で指定します。 -JSONの送信 -======= +JSON の送信 +======== -JSON形式でデータを送信し、Presenterを終了するアクションメソッドの例: +`sendJson($data)` メソッドは、渡されたデータを JSON にエンコードして HTTP のレスポンスとして送り、プレゼンターを終わらせます。例を挙げます。 ```php public function actionData(): void @@ -235,59 +260,59 @@ public function actionData(): void ``` -リクエストパラメータ .{data-version:3.1.14} -================================= +リクエストのパラメータ .{data-version:3.1.14} +================================== -Presenterおよび各コンポーネントは、HTTPリクエストからパラメータを取得します。その値は `getParameter($name)` または `getParameters()` メソッドで取得できます。値は文字列または文字列の配列であり、基本的にはURLから直接取得された生のデータです。 +プレゼンターも、各コンポーネントも、そのパラメータを HTTP のリクエストから得ます。値は `getParameter($name)` や `getParameters()` メソッドで取り出せます。値は文字列か文字列の配列で、要するに URL から直接得た生のデータです。 -より便利にするために、プロパティを介してパラメータにアクセスすることをお勧めします。`#[Parameter]` 属性でマークするだけです。 +もっと便利にするために、プロパティ経由でパラメータにアクセスすることをおすすめします。`#[Parameter]` アトリビュートで印を付けるだけです。 ```php -use Nette\Application\Attributes\Parameter; // この行は重要です +use Nette\Application\Attributes\Parameter; // この行が大事です class HomePresenter extends Nette\Application\UI\Presenter { #[Parameter] - public string $theme; // publicである必要があります + public string $theme; // public でなければなりません } ``` -プロパティにはデータ型(例:`string`)を指定することをお勧めします。Netteはそれに基づいて値を自動的にキャストします。パラメータ値は[検証 |#パラメータの検証]することもできます。 +プロパティにはデータ型(`string` など)を指定することをおすすめします。そうすれば Nette が値を自動的にキャストします。パラメータの値は[検証する |#パラメータの検証]こともできます。 -リンクを作成するときに、パラメータの値を直接設定できます。 +リンクを作るとき、パラメータの値を直接設定できます。 ```latte クリック ``` -パーシステントパラメータ -============ +永続パラメータ +======= -パーシステントパラメータは、異なるリクエスト間で状態を維持するために使用されます。その値は、リンクをクリックした後も同じままです。セッションデータとは異なり、URLで転送されます。そして、これは完全に自動的に行われるため、`link()` や `n:href` で明示的に指定する必要はありません。 +永続パラメータは、リクエストをまたいで状態を保つために使います。その値はリンクをクリックしたあとも変わりません。セッションのデータと違い、URL で運ばれます。しかもそれは完全に自動的に起こるので、`link()` や `n:href` で明示的に書く必要はありません。 -使用例は?多言語アプリケーションがあるとします。現在の言語は、常にURLの一部である必要があるパラメータです。しかし、すべてのリンクでそれを指定するのは非常に面倒です。そこで、それをパーシステントパラメータ `lang` にし、自動的に転送されるようにします。素晴らしい! +使いどころの例を挙げましょう。多言語のアプリケーションがあるとします。現在の言語は、常に URL の一部でなければならないパラメータです。しかしそれをすべてのリンクに書くのは、途方もなく面倒です。ですからそれを永続パラメータ `lang` にすれば、自動的に運ばれていきます。素敵ですね。 -Netteでパーシステントパラメータを作成するのは非常に簡単です。パブリックプロパティを作成し、属性でマークするだけです。(以前は `/** @persistent */` が使用されていました) +Nette で永続パラメータを作るのはきわめて簡単です。public のプロパティを作り、アトリビュートで印を付けるだけです(以前は `/** @persistent */` が使われていました)。 ```php -use Nette\Application\Attributes\Persistent; // この行は重要です +use Nette\Application\Attributes\Persistent; // この行が大事です class ProductPresenter extends Nette\Application\UI\Presenter { #[Persistent] - public string $lang; // publicである必要があります + public string $lang; // public でなければなりません } ``` -`$this->lang` が例えば `'en'` の値を持つ場合、`link()` または `n:href` を使用して作成されたリンクも `lang=en` パラメータを含みます。そして、リンクをクリックした後も、再び `$this->lang = 'en'` になります。 +`$this->lang` が `'en'` のような値を持っていれば、`link()` や `n:href` で作られたリンクにもパラメータ `lang=en` が入ります。そしてリンクをクリックしたあと、`$this->lang` はまた `'en'` になります。 -プロパティにはデータ型(例:`string`)を指定することをお勧めします。また、デフォルト値を指定することもできます。パラメータ値は[検証 |#パラメータの検証]できます。 +プロパティにはデータ型(`string` など)を指定することをおすすめしますし、既定値も与えられます。パラメータの値は[検証できます |#パラメータの検証]。 -パーシステントパラメータは、通常、特定のPresenterのすべてのアクション間で転送されます。複数のPresenter間で転送するには、次のいずれかで定義する必要があります。 +永続パラメータはふつう、あるプレゼンターのすべてのアクションのあいだで引き継がれます。複数のプレゼンターをまたいで引き継ぐには、次のいずれかで定義する必要があります。 -- Presenterが継承する共通の祖先で -- Presenterが使用するトレイトで: +- プレゼンターが継承する共通の祖先で +- あるいはプレゼンターが使うトレイトで: ```php trait LanguageAware @@ -302,42 +327,62 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -リンクを作成するときに、パーシステントパラメータの値を変更できます。 +リンクを作るとき、永続パラメータの値は変えられます。 ```latte -チェコ語の詳細 +チェコ語での詳細 ``` -または、*リセット*することもできます。つまり、URLから削除します。その後、デフォルト値を取ります。 +あるいは*リセット*して URL から取り除けます。その場合は既定値になります。 ```latte クリック ``` -インタラクティブコンポーネント -=============== +共有されるパラメータの空間 +============= -Presenterには組み込みのコンポーネントシステムがあります。コンポーネントは、Presenterに挿入する独立した再利用可能なユニットです。[フォーム |forms:in-presenter]、データグリッド、メニューなど、繰り返し使用する意味のあるものであれば何でもかまいません。 +リクエストのパラメータ、[永続パラメータ |#永続パラメータ]、そして `action`、`render`、`handle`(シグナル)メソッドのパラメータは、ひとつの空間を共有していて、それぞれが名前で識別されます。同じ名前が複数に現れれば、それらはまったく同じ値を指します。 -コンポーネントがどのようにPresenterに挿入され、その後使用されるのでしょうか?それは[コンポーネント |components]の章で学びます。ハリウッドと共通点があることさえ発見するでしょう。 +これはしばしば都合よく使われます。たとえば永続パラメータ `lang` とアクションやシグナルのメソッドの引数 `$lang` は同じものなので、メソッドのシグネチャに並べるだけで永続パラメータの現在の値を読めます。 -そして、どこでコンポーネントを入手できますか?[Componette |https://componette.org/search/component]ページでは、オープンソースコンポーネントや、フレームワーク周辺のコミュニティのボランティアによってここに配置されたNette用の他の多くのアドオンを見つけることができます。 +```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] -この章でこれまで見てきたことで、おそらく完全に十分でしょう。以下の行は、Presenterについて深く掘り下げ、すべてを知りたい人のためのものです。 +この章でここまで扱った内容で、たいていの用途には十分でしょう。以降の節は、プレゼンターをもっと深く知りたい、何もかも知りたいという方のためのものです。 パラメータの検証 -------- -URLから受け取った[#リクエストパラメータ]と[#パーシステントパラメータ]の値は、`loadState()` メソッドによってプロパティに書き込まれます。また、プロパティで指定されたデータ型と一致するかどうかもチェックし、一致しない場合は404エラーで応答し、ページは表示されません。 +URL から受け取った[リクエストのパラメータ |#リクエストのパラメータ]と[永続パラメータ |#永続パラメータ]の値は、`loadState()` メソッドがプロパティに書き込みます。あわせてプロパティに指定されたデータ型と合うかも確認し、合わなければ 404 のエラーで応え、ページは表示されません。 -パラメータは、ユーザーがURLで簡単に上書きできるため、決して盲目的に信用しないでください。例えば、このようにして言語 `$this->lang` がサポートされている言語の中にあるかどうかを検証します。適切な方法は、前述の `loadState()` メソッドをオーバーライドすることです。 +URL から受け取ったパラメータを決して盲信しないでください。ユーザーに簡単に書き換えられます。たとえば言語 `$this->lang` が対応しているものの中にあるかを、次のように確かめます。そのための適切な方法が、先ほどの `loadState()` メソッドの上書きです。 ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -348,7 +393,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function loadState(array $params): void { parent::loadState($params); // ここで $this->lang が設定されます - // 値の独自のチェックが続きます: + // 続いて独自の値のチェック: if (!in_array($this->lang, ['en', 'cs'])) { $this->error(); } @@ -360,83 +405,65 @@ class ProductPresenter extends Nette\Application\UI\Presenter リクエストの保存と復元 ----------- -Presenterが処理するリクエストは[api:Nette\Application\Request]オブジェクトであり、Presenterの `getRequest()` メソッドによって返されます。 +プレゼンターが処理するリクエストは [api:Nette\Application\Request]オブジェクトで、プレゼンターの `getRequest()` メソッドが返します。 -現在のリクエストはセッションに保存したり、逆にそこから復元してPresenterに再度実行させたりすることができます。これは、例えばユーザーがフォームに入力していてログインが期限切れになった場合に便利です。データを失わないように、ログインページにリダイレクトする前に現在のリクエストを `$reqId = $this->storeRequest()` を使用してセッションに保存します。これは短い文字列の形式でその識別子を返し、それをログインPresenterにパラメータとして渡します。 +現在のリクエストはセッションに保存でき、逆にそこから復元してプレゼンターにもう一度実行させられます。これは、たとえばユーザーがフォームに入力している最中にログインのセッションが切れたときに役立ちます。データを失わないよう、ログインページにリダイレクトする前に `$reqId = $this->storeRequest()` で現在のリクエストをセッションに保存します。これは短い文字列の識別子を返すので、それをパラメータとしてログインのプレゼンターに渡します。 -ログイン後、`$this->restoreRequest($reqId)` メソッドを呼び出します。これはセッションからリクエストを取得し、それにフォワードします。メソッドは、リクエストが現在ログインしているユーザーと同じユーザーによって作成されたことを検証します。別のユーザーがログインした場合、またはキーが無効な場合、何もしませんでプログラムは続行します。 +ログイン後に `$this->restoreRequest($reqId)` メソッドを呼ぶと、セッションからリクエストを取り出します。POST のリクエストはそこへ forward され、それ以外(GET)はリクエストの URL にリダイレクトされます。このメソッドは、そのリクエストがいまログインしているのと同じユーザーによって作られたかを確認します。別のユーザーがログインした場合やキーが正しくない場合は何もせず、プログラムはいつもどおり続きます。 -[以前のページに戻る方法 |best-practices:restore-request]のガイドをご覧ください。 +ガイド[前のページに戻るには |best-practices:restore-request]をご覧ください。 -カノニカル化 ------- +正規化 +--- -Presenterには、より良いSEO(インターネットでの検索エンジンの最適化)に貢献する本当に素晴らしい機能が1つあります。異なるURLで重複コンテンツが存在するのを自動的に防ぎます。特定のターゲットに複数のURLアドレス(例:`/index` と `/index?page=1`)がある場合、フレームワークはそのうちの1つをプライマリ(カノニカル)として決定し、HTTPコード301を使用して他のアドレスをそれにリダイレクトします。これにより、検索エンジンはページを2回インデックス付けせず、ページランクを希釈しません。 +プレゼンターには、SEO(検索エンジン最適化)の向上に寄与する本当に優れた機能があります。異なる URL に同じ内容が存在するのを自動的に防ぐのです。たとえば `/index` と `/index?page=1` のように複数の URL が特定の行き先に通じている場合、フレームワークはそのひとつを主要(正規)なものと定め、ほかを HTTP コード 301 でそこへリダイレクトします。おかげで検索エンジンがページを二重に登録して、そのページランクを薄めることがなくなります。 -このプロセスはカノニカル化と呼ばれます。カノニカルURLは、[ルーター|routing]によって生成されるURLであり、通常はコレクション内の最初の一致するルートです。 +この過程を正規化と呼びます。正規の URL は[ルーター|routing]が生成するもので、ふつうはコレクションの中で最初に一致するルートのものです。 -カノニカル化はデフォルトで有効になっており、`$this->autoCanonicalize = false` を介して無効にできます。 +正規化は既定で有効で、`$this->autoCanonicalize = false` で無効にできます。 -AJAXまたはPOSTリクエストの場合、データが失われたり、SEOの観点から付加価値がなかったりするため、リダイレクトは発生しません。 +AJAX や POST のリクエストではリダイレクトは起こりません。データを失いかねませんし、SEO 上の利点もないからです。 -カノニカル化は、`canonicalize()` メソッドを使用して手動で呼び出すこともできます。このメソッドには、`link()` メソッドと同様に、Presenter、アクション、およびパラメータが渡されます。リンクを作成し、現在のURLアドレスと比較します。異なる場合は、生成されたリンクにリダイレクトします。 +`canonicalize()` メソッドを使えば、正規化を手動で起こすこともできます。`link()` メソッドと同じように、プレゼンター、アクション、パラメータを渡します。リンクを生成して現在の URL アドレスと比べ、違っていれば生成したリンクへリダイレクトします。 ```php public function actionShow(int $id, ?string $slug = null): void { $realSlug = $this->facade->getSlugForId($id); - // $slug が $realSlug と異なる場合にリダイレクトします + // $slug が $realSlug と違えばリダイレクトします $this->canonicalize('Product:show', [$id, $realSlug]); } ``` +ルートのフィルタと `canonicalize()` を組み合わせて SEO に強い URL を作る完全なパターンは、[スラッグを使った読みやすい URL |best-practices:pretty-urls]をご覧ください。 -イベント ----- -Presenterのライフサイクルの一部として呼び出される `startup()`、`beforeRender()`、`shutdown()` メソッドに加えて、自動的に呼び出されるように他の関数を定義することもできます。Presenterはいわゆる[イベント |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()` の直前に呼び出されます。 - - -応答 --------- - -Presenterが返す応答は、[api:Nette\Application\Response]インターフェースを実装するオブジェクトです。多くの準備された応答が利用可能です。 +プレゼンターが返すレスポンスは、[api:Nette\Application\Response]インターフェースを実装したオブジェクトです。あらかじめ用意されたレスポンスがいくつかあります。 -- [api:Nette\Application\Responses\CallbackResponse] - コールバックを送信します -- [api:Nette\Application\Responses\FileResponse] - ファイルを送信します +- [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\JsonResponse] - JSON を送ります - [api:Nette\Application\Responses\RedirectResponse] - リダイレクト -- [api:Nette\Application\Responses\TextResponse] - テキストを送信します -- [api:Nette\Application\Responses\VoidResponse] - 空の応答 +- [api:Nette\Application\Responses\TextResponse] - テキストを送ります +- [api:Nette\Application\Responses\VoidResponse] - 空のレスポンス -応答は `sendResponse()` メソッドを使用して送信されます。 +レスポンスは `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

    '; @@ -445,28 +472,88 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht $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); + // ... +} +``` + -`#[Requires]` を使用したアクセス制限 .{data-version:3.2.2} ------------------------------------------------ +テンプレートの仕上げ .{data-version:3.3.0} +-------------------------------- -`#[Requires]` 属性は、Presenterとそのメソッドへのアクセスを制限するための高度なオプションを提供します。HTTPメソッドの指定、AJAXリクエストの要求、同一オリジン(same origin)への制限、およびフォワーディング経由のアクセスのみに使用できます。属性は、Presenterクラスと個々の `action()`、`render()`、`handle()`、`createComponent()` メソッドの両方に適用できます。 +プレゼンターがテンプレートを描くとき、`sendTemplate()` メソッドは描画の直前に `completeTemplate()` を呼びます。このメソッドは `#[TemplateVariable]` アトリビュートで印の付いた変数を埋め、テンプレートのファイルを探します(既定の変数は、テンプレートが作られるときに `TemplateFactory` がすでに設定しています)。この protected のメソッドを上書きすれば、すべてのビューで共通の変数を足したり、別のファイルを指定したりできます。 -これらの制限を指定できます。 -- HTTPメソッドについて:`#[Requires(methods: ['GET', 'POST'])]` -- AJAXリクエストの要求:`#[Requires(ajax: true)]` -- 同一オリジンからのアクセスのみ:`#[Requires(sameOrigin: true)]` -- フォワード経由のアクセスのみ:`#[Requires(forward: true)]` -- 特定のアクションへの制限:`#[Requires(actions: 'default')]` +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My App'; +} +``` -詳細については、[Requires属性の使用方法 |best-practices:attribute-requires]のガイドをご覧ください。 +`#[Requires]` によるアクセスの制限 .{data-version:3.2.3} +---------------------------------------------- -HTTPメソッドのチェック -------------- +`#[Requires]` アトリビュートは、プレゼンターとそのメソッドへのアクセスを制限する高度な選択肢を提供します。HTTP のメソッドを指定する、AJAX のリクエストを要求する、同一オリジンに限る、forward 経由のアクセスだけを許すといったことができます。このアトリビュートは、プレゼンターのクラスにも、`action()`、`render()`、`handle()`、`createComponent()` といった個々のメソッドにも付けられます。 -NetteのPresenterは、各受信リクエストのHTTPメソッドを自動的に検証します。このチェックの理由は主にセキュリティです。標準では、`GET`、`POST`、`HEAD`、`PUT`、`DELETE`、`PATCH` メソッドが許可されています。 +次の制限を指定できます。 +- HTTP のメソッドについて: `#[Requires(methods: ['GET', 'POST'])]` +- AJAX のリクエストを要求する: `#[Requires(ajax: true)]` +- 同一オリジンからのアクセスだけ: `#[Requires(sameOrigin: true)]` +- forward 経由のアクセスだけ: `#[Requires(forward: true)]` +- 特定のアクションへの制限: `#[Requires(actions: 'default')]` -例えば `OPTIONS` メソッドを追加で許可したい場合は、`#[Requires]` 属性を使用します(Nette Application v3.2以降)。 +.[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'])] @@ -475,26 +562,34 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -バージョン3.1では、検証は `checkHttpMethod()` で行われます。これは、リクエストで指定されたメソッドが `$presenter->allowedMethods` 配列に含まれているかどうかを判断します。メソッドを追加するには、次のようにします。 +バージョン 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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -`OPTIONS` メソッドを許可する場合、その後Presenter内で適切に処理する必要があることを強調することが重要です。このメソッドは、いわゆるプリフライトリクエストとしてよく使用されます。これは、CORS(Cross-Origin Resource Sharing)ポリシーの観点からリクエストが許可されているかどうかを判断する必要がある場合に、ブラウザが実際のリクエストの前に自動的に送信します。メソッドを許可しても正しい応答を実装しない場合、不整合や潜在的なセキュリティ問題につながる可能性があります。 +強調しておくべきなのは、`OPTIONS` メソッドを有効にしたら、プレゼンターの中でそれを適切に扱わなければならない、ということです。このメソッドはいわゆるプリフライトのリクエストとしてよく使われ、CORS(Cross-Origin Resource Sharing)のポリシーに照らしてリクエストが許されるかを判断する必要があるとき、ブラウザが実際のリクエストの前に自動的に送ります。メソッドを有効にしながら正しい応答を実装しないと、食い違いや潜在的なセキュリティの問題につながりかねません。 -その他の読み物 -======= +非推奨のアクションへの印 .{data-version:3.2.3} +---------------------------------- + +`#[Deprecated]` アトリビュートは、アクション、シグナル、あるいはプレゼンター全体を非推奨で将来削除予定と印を付けます。アプリケーションの非推奨の部分へのリンクを生成すると、Nette は警告を出して開発者に知らせます。 + +このアトリビュートは、プレゼンターのクラス全体にも、個々の `action()`、`render()`、`handle()` メソッドにも付けられます。 + + +関連情報 +==== -- [injectメソッドと属性 |best-practices:inject-method-attribute] -- [トレイトからのPresenterの構成 |best-practices:presenter-traits] -- [Presenterへの設定の受け渡し |best-practices:passing-settings-to-presenters] -- [以前のページに戻る方法 |best-practices:restore-request] +- [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 index d3c524f7ab..b0cabf02a8 100644 --- a/application/ja/routing.texy +++ b/application/ja/routing.texy @@ -3,26 +3,26 @@
    -ルータはURLアドレスに関するすべてを担当するため、もはやそれについて考える必要はありません。以下に示します: +ルーターは URL アドレスにまつわるすべてを引き受けるので、あなたが URL のことを考える必要はありません。ここでは次のことを扱います。 -- URLが期待通りになるようにルータを設定する方法 -- SEOとリダイレクトについて説明します -- そして、独自のルータを作成する方法を示します +- URL を思いどおりの見た目にするためのルーターの設定 +- SEO とリダイレクト +- そして独自のルーターの書き方
    -より人間的なURL(クールまたはプリティURLとも呼ばれます)は、より使いやすく、覚えやすく、SEOに積極的に貢献します。Netteはこれを考慮し、開発者の要望に完全に応えます。アプリケーション用に、まさにあなたが望むURLアドレス構造を設計できます。 コードやテンプレートへの介入なしに行えるため、アプリケーションがすでに完成している時点でも設計できます。それは、ルータ内の[1つの場所 |#アプリケーションへの統合]でエレガントな方法で定義され、すべてのPresenterのアノテーションに散らばることはありません。 +人にやさしい URL(かっこいい URL、きれいな URL とも呼ばれます)は使いやすく、覚えやすく、SEO にもよい影響を与えます。Nette はそれを心得ていて、開発者の求めるものにしっかり応えます。アプリケーションの URL の構造を、思いどおりに設計できます。しかもアプリケーションが完成したあとでも設計できます。コードやテンプレートの変更が要らないからです。それはすべてのプレゼンターにアノテーションとして散らばるのではなく、[ただ一か所 |#組み込み]、つまりルーターで優雅に定義されます。 -Netteのルータは、**双方向**であるという点で特別です。HTTPリクエスト内のURLをデコードするだけでなく、リンクを作成することもできます。したがって、[Nette Application |how-it-works#Nette Application]において重要な役割を果たします。なぜなら、現在のリクエストを実行するPresenterとアクションを決定するだけでなく、テンプレートなどで[URLを生成 |creating-links]するためにも使用されるからです。 +Nette のルーターが並外れているのは、**双方向**だからです。HTTP のリクエストから URL を読み解くことも、リンクを作ることもできます。ですから [Nette Application |how-it-works#Nette Application]で欠かせない役割を果たします。現在のリクエストをどのプレゼンターとアクションが処理するかを決めるだけでなく、テンプレートなどで [URL を生成する |creating-links]のにも使われるからです。 -ただし、ルータはこの用途に限定されません。Presenterをまったく使用しないアプリケーション、REST APIなどで使用できます。詳細は[#スタンドアロンでの使用]セクションを参照してください。 +とはいえルーターの用途はそれだけではありません。プレゼンターをまったく使わないアプリケーションや REST API などでも使えます。詳しくは [#単独での利用]の節をご覧ください。 -ルートコレクション -========= +ルートのコレクション +========== -アプリケーションのURLアドレスの形式を定義する最も快適な方法は、[api:Nette\Application\Routers\RouteList]クラスを使用することです。定義は、いわゆるルートのリスト、つまりURLアドレスのマスクと、それに関連付けられたPresenterおよびアクションで構成され、シンプルなAPIを使用して行われます。ルートに名前を付ける必要はありません。 +アプリケーションの URL アドレスの構造を定義するもっとも心地よい方法は、[api:Nette\Application\Routers\RouteList]クラスが提供します。その定義は、いわゆるルートの一覧、つまり URL アドレスのマスクと、それに結びつくプレゼンターやアクションから成り、単純な API で書けます。ルートに名前を付ける必要はまったくありません。 ```php $router = new Nette\Application\Routers\RouteList; @@ -31,16 +31,16 @@ $router->addRoute('article/', 'Article:view'); // ... ``` -この例は、ブラウザで`https://domain.com/rss.xml`を開くと、Presenter `Feed`とアクション `rss`が表示され、`https://domain.com/article/12`を開くと、Presenter `Article`とアクション `view`が表示されることを示しています。適切なルートが見つからない場合、Nette Applicationは[BadRequestException |api:Nette\Application\BadRequestException]例外をスローし、これはユーザーにエラーページ404 Not Foundとして表示されます。 +この例からわかるように、ブラウザで `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' は最初のルートにキャッチされ、この文字列は として解釈されます +// 誤り: 'rss.xml' は最初のルートに捕まえられ、その文字列が と解釈されます $router->addRoute('', 'Article:view'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -49,10 +49,10 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('', 'Article:view'); ``` -ルートは、リンクを生成する際にも上から下に評価されます: +リンクを生成するときも、ルートは上から下へ評価されます。 ```php -// 間違い: 'Feed:rss' へのリンクは 'admin/feed/rss' として生成されます +// 誤り: 'Feed:rss' へのリンクが 'admin/feed/rss' として生成されます $router->addRoute('admin//', 'Admin:default'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -61,100 +61,100 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('admin//', 'Admin:default'); ``` -ルートを正しく組み立てるには、ある程度のスキルが必要であることを隠すつもりはありません。それを習得するまでは、[ルーティングパネル |#ルータのデバッグ]が便利なツールになります。 +ルートを正しく組み立てるにはいくらか慣れが要ることは、隠さずに申し上げます。身につくまでは [ルーティングのパネル |#ルーターのデバッグ]が役に立つ道具になるでしょう。 マスクとパラメータ --------- -マスクは、Webサイトのルートディレクトリからの相対パスを記述します。最も単純なマスクは静的なURLです: +マスクはウェブサイトのルートディレクトリからの相対パスを表します。もっとも単純なマスクは静的な URL です。 ```php $router->addRoute('products', 'Products:default'); ``` -マスクには、いわゆる**パラメータ**が含まれることがよくあります。これらは山括弧(例:``)で示され、ターゲットPresenterに渡されます。たとえば、メソッド `renderShow(int $year)` や永続パラメータ `$year` に渡されます: +マスクには**パラメータ**が入ることがよくあります。これは山かっこで囲まれ(たとえば ``)、対象のプレゼンターに、たとえば `renderShow(int $year)` メソッドや永続パラメータ `$year` に渡されます。 ```php $router->addRoute('chronicle/', 'History:show'); ``` -この例は、ブラウザで `https://example.com/chronicle/2020` を開くと、Presenter `History` とアクション `show` がパラメータ `year: 2020` とともに表示されることを示しています。 +この例からわかるように、ブラウザで `https://example.com/chronicle/2020` を開くと、`History` プレゼンターの `show` アクションがパラメータ `year: 2020` とともに表示されます。 -パラメータには、マスク内で直接デフォルト値を指定でき、これによりオプションになります: +パラメータの既定値はマスクの中で直接指定でき、そうするとそのパラメータは省略できるようになります。 ```php $router->addRoute('chronicle/', 'History:show'); ``` -これで、ルートはURL `https://example.com/chronicle/` も受け入れ、これも `History:show` をパラメータ `year: 2020` とともに表示します。 +これでこのルートは `https://example.com/chronicle/` という URL も受け付け、やはり `History:show` をパラメータ `year: 2020` とともに表示します。 -パラメータは、もちろんPresenter名やアクション名にもなり得ます。たとえば、このように: +もちろんプレゼンターやアクションの名前もパラメータにできます。たとえば次のようにです。 ```php $router->addRoute('/', 'Home:default'); ``` -指定されたルートは、たとえば `/article/edit` や `/catalog/list` の形式のURLを受け入れ、それらをPresenterとアクション `Article:edit` および `Catalog:list` として解釈します。 +このルートはたとえば `/article/edit` や `/catalog/list` の形の URL を受け付け、それぞれプレゼンターとアクション `Article:edit`、`Catalog:list` と解釈します。 -同時に、パラメータ `presenter` と `action` にデフォルト値 `Home` と `default` を与え、それらもオプションになります。したがって、ルートは `/article` の形式のURLも受け入れ、それを `Article:default` として解釈します。または逆に、`Product:default` へのリンクはパス `/product` を生成し、デフォルトの `Home:default` へのリンクはパス `/` を生成します。 +同時に `presenter` と `action` のパラメータに既定値 `Home` と `default` を与えるので、これらも省略できるようになります。ですからこのルートは `/article` という URL も受け付け、`Article:default` と解釈します。逆に `Product:default` へのリンクはパス `/product` を生成し、既定の `Home:default` へのリンクはパス `/` を生成します。 -マスクは、Webサイトのルートディレクトリからの相対パスだけでなく、スラッシュで始まる場合は絶対パス、または2つのスラッシュで始まる場合は完全な絶対URLも記述できます: +マスクはウェブサイトのルートディレクトリからの相対パスだけでなく、スラッシュで始めれば絶対パスを、スラッシュ 2 つで始めれば絶対 URL 全体を表せます。 ```php -// ドキュメントルートからの相対パス +// ドキュメントルートからの相対 $router->addRoute('/', /* ... */); -// 絶対パス(ドメインからの相対パス) +// 絶対パス(ドメインからの相対) $router->addRoute('//', /* ... */); -// ドメインを含む絶対URL(スキーマからの相対パス) +// ドメインを含む絶対 URL(スキームからの相対) $router->addRoute('//.example.com//', /* ... */); -// スキーマを含む絶対URL +// スキームを含む絶対 URL $router->addRoute('https://.example.com//', /* ... */); ``` -検証式 ---- +検証の正規表現 +------- -各パラメータには、[正規表現|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]を使用して検証条件を設定できます。たとえば、パラメータ `id` には、正規表現 `\d+` を使用して数字のみを受け入れるように指定します: +パラメータごとに [正規表現|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' になります +// https://example.com/a/b/c を受け付け、path は 'a/b/c' になります $router->addRoute('', /* ... */); ``` -オプションシーケンス ----------- +省略できる部分 +------- -マスクでは、角括弧を使用してオプションの部分をマークできます。マスクの任意の部分をオプションにでき、パラメータを含めることもできます: +マスクの中では、角かっこで省略できる部分に印を付けられます。マスクのどの部分も省略できるようにでき、その中にパラメータを入れられます。 ```php $router->addRoute('[/]', /* ... */); -// 受け入れるパス: -// /cs/download => lang => cs, name => download +// 次のパスを受け付けます: +// /en/download => lang => en, name => download // /download => lang => null, name => download ``` -パラメータがオプションシーケンスの一部である場合、当然ながらオプションにもなります。デフォルト値が指定されていない場合は、nullになります。 +パラメータが省略できる部分の中にあると、当然そのパラメータも省略できるようになります。既定値が指定されていなければ null になります。 -オプションの部分はドメインにも含めることができます: +省略できる部分はドメインにも置けます。 ```php $router->addRoute('//[.]example.com//', /* ... */); ``` -シーケンスは任意にネストおよび組み合わせることができます: +省略できる部分は自由に入れ子にしたり組み合わせたりできます。 ```php $router->addRoute( @@ -162,33 +162,33 @@ $router->addRoute( 'Home:default', ); -// 受け入れるパス: -// /cs/hello +// 次のパスを受け付けます: +// /en/hello // /en-us/hello // /hello // /hello/page-12 ``` -URLを生成する際には、最短のバリアントが試みられるため、省略できるものはすべて省略されます。したがって、たとえばルート `index[.html]` はパス `/index` を生成します。左角括弧の後に感嘆符を付けることで、動作を逆にすることができます: +URL を生成するときは、もっとも短い形が優先されるので、省ける部分はすべて省かれます。ですからたとえばルート `index[.html]` はパス `/index` を生成します。この振る舞いは、開き角かっこのうしろに感嘆符を置くと逆にできます。 ```php -// /hello と /hello.html を受け入れ、/hello を生成します +// /hello と /hello.html を受け付け、/hello を生成します $router->addRoute('[.html]', /* ... */); -// /hello と /hello.html を受け入れ、/hello.html を生成します +// /hello と /hello.html を受け付け、/hello.html を生成します $router->addRoute('[!.html]', /* ... */); ``` -角括弧なしのオプションパラメータ(つまり、デフォルト値を持つパラメータ)は、基本的に次のように括弧で囲まれているかのように動作します: +角かっこのない、省略できるパラメータ(つまり既定値を持つパラメータ)は、実質的に次のように囲まれているかのように振る舞います。 ```php $router->addRoute('//', /* ... */); -// これに対応します: +// これは次と同じです: $router->addRoute('[/[/[]]]', /* ... */); ``` -末尾のスラッシュの動作に影響を与えたい場合、たとえば `/home/` の代わりに `/home` だけを生成するようにするには、次のようにします: +末尾のスラッシュの振る舞いに手を入れて、たとえば `/home/` の代わりに `/home` を生成させたいなら、次のようにできます。 ```php $router->addRoute('[[/[/]]]', /* ... */); @@ -198,24 +198,24 @@ $router->addRoute('[[/[/]]]', /* ... */); ワイルドカード ------- -絶対パスのマスクでは、次のワイルドカードを使用して、たとえば開発環境と本番環境で異なる可能性のあるドメインをマスクに書き込む必要性を回避できます: +絶対 URL のマスクでは次のワイルドカードを使えます。たとえば開発環境と本番環境で違うかもしれないドメインを、マスクに書かずに済ませられます。 -- `%tld%` = トップレベルドメイン、例:`com` または `org` -- `%sld%` = セカンドレベルドメイン、例:`example` -- `%domain%` = サブドメインなしのドメイン、例:`example.com` -- `%host%` = 完全なホスト、例:`www.example.com` +- `%tld%` = トップレベルドメイン、たとえば `com` や `org` +- `%sld%` = セカンドレベルドメイン、たとえば `example` +- `%domain%` = サブドメインを除いたドメイン、たとえば `example.com` +- `%host%` = ホスト全体、たとえば `www.example.com` - `%basePath%` = ルートディレクトリへのパス ```php $router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` -拡張表記 ----- +詳しい書き方 +------ -通常 `Presenter:action` の形式で記述されるルートのターゲットは、個々のパラメータとそのデフォルト値を定義する配列を使用して記述することもできます: +ふつう `Presenter:action` の形で書くルートの行き先は、個々のパラメータとその既定値を定義する配列でも書けます。 ```php $router->addRoute('/[/]', [ @@ -224,7 +224,7 @@ $router->addRoute('/[/]', [ ]); ``` -より詳細な指定には、さらに拡張された形式を使用できます。ここでは、デフォルト値に加えて、パラメータの他のプロパティ(たとえば、検証正規表現(`id` パラメータを参照))も設定できます: +より細かく指定するには、さらに広げた形も使えます。そこでは既定値のほかに、検証の正規表現などパラメータのほかの性質も設定できます(`id` パラメータをご覧ください)。 ```php use Nette\Routing\Route; @@ -242,19 +242,29 @@ $router->addRoute('/[/]', [ ]); ``` -配列で定義されたパラメータがパスのマスクに含まれていない場合、その値はURLの疑問符の後に指定されたクエリパラメータを使用しても変更できないことに注意することが重要です。 +大事なのは、配列で定義したパラメータがパスのマスクに現れていない場合、その値は変えられないという点です。URL の疑問符のうしろに書くクエリパラメータでも変えられません。 + +これは**固定のパラメータ**、つまりあるページに短く覚えやすい URL を与えるのに役立ちます。たとえば `/tos` がいつも `Article:view` を `id: 123` で開くようにするには次のようにします。 + +```php +$router->addRoute('tos', [ + 'presenter' => 'Article', + 'action' => 'view', + 'id' => 123, +]); +``` -フィルタと翻訳 +フィルタと変換 ------- -アプリケーションのソースコードは英語で記述しますが、Webサイトにチェコ語のURLが必要な場合は、次のような単純なルーティングでは: +アプリケーションのソースコードは英語で書きますが、ウェブサイトの URL をチェコ語にしたい場合、次のような単純なルーティングでは、 ```php $router->addRoute('/', 'Home:default'); ``` -`/product/123` や `/cart` のような英語のURLが生成されます。URL内のPresenterとアクションをチェコ語の単語(例:`/produkt/123` や `/kosik`)で表現したい場合は、翻訳辞書を使用できます。その記述には、2番目のパラメータのより「冗長な」バリアントが必要です: +`/product/123` や `/cart` のような英語の URL が生成されます。URL の中のプレゼンターとアクションをチェコ語(たとえば `/produkt/123` や `/kosik`)で表したいなら、変換の辞書を使えます。それを書くには、第 2 パラメータの「詳しい」書き方がもう必要です。 ```php use Nette\Routing\Route; @@ -263,7 +273,7 @@ $router->addRoute('/', [ 'presenter' => [ Route::Value => 'Home', Route::FilterTable => [ - // URL内の文字列 => presenter + // URL の中の文字列 => プレゼンター 'produkt' => 'Product', 'kosik' => 'Cart', 'katalog' => 'Catalog', @@ -278,11 +288,11 @@ $router->addRoute('/', [ ]); ``` -翻訳辞書の複数のキーが同じPresenterにつながる可能性があります。これにより、異なるエイリアスが作成されます。正規のバリアント(つまり、生成されたURLに含まれるバリアント)は、最後のキーと見なされます。 +変換の辞書では、複数のキーが同じプレゼンターを指しても構いません。そうするとそのプレゼンターにいろいろな別名ができます。最後のキーが正式な形(つまり生成される URL に入る形)と見なされます。 -翻訳テーブルはこの方法で任意のパラメータに使用できます。翻訳が存在しない場合は、元の値が使用されます。この動作は、`Route::FilterStrict => true` を追加することで変更でき、値が辞書にない場合、ルートはURLを拒否します。 +変換の表はこのやり方でどのパラメータにも使えます。変換が存在しなければ、もとの値がそのまま使われます。この振る舞いは `Route::FilterStrict => true` を足すと変えられ、値が辞書にない場合、そのルートは URL を受け付けなくなります。 -配列形式の翻訳辞書に加えて、独自の翻訳関数をデプロイすることもできます。 +配列の形の変換の辞書のほかに、独自の変換の関数も使えます。 ```php use Nette\Routing\Route; @@ -298,15 +308,15 @@ $router->addRoute('//', [ ]); ``` -関数 `Route::FilterIn` は、URL内のパラメータとPresenterに渡される文字列の間で変換を行い、関数 `FilterOut` は逆方向の変換を保証します。 +`Route::FilterIn` の関数は、URL の中のパラメータと、そのあとプレゼンターに渡される文字列とのあいだの変換を行い、`FilterOut` の関数は逆向きの変換を受け持ちます。 -パラメータ `presenter`、`action`、`module` には、PascalCaseまたはcamelCaseスタイルとURLで使用されるkebab-caseの間で変換を行う事前定義されたフィルタがすでにあります。パラメータのデフォルト値はすでに変換された形式で記述されるため、たとえばPresenterの場合は `` と記述し、`` とは記述しません。 +`presenter`、`action`、`module` のパラメータには、PascalCase や camelCase の書き方と、URL で使われる kebab-case とのあいだを変換するフィルタがあらかじめ用意されています。パラメータの既定値は、アプリケーションに渡される形(プレゼンターとモジュールは PascalCase、アクションは camelCase)で書くので、たとえばプレゼンターなら `` と書き、`` とは書きません。 -一般フィルタ ------- +一般のフィルタ +------- -特定のパラメータ向けのフィルタに加えて、すべてのパラメータの連想配列を受け取り、それらを任意に変更して返すことができる一般フィルタも定義できます。一般フィルタはキー `null` の下に定義します。 +特定のパラメータ向けのフィルタのほかに、一般のフィルタも定義できます。これはすべてのパラメータの連想配列を受け取り、好きなように変えて返せます。一般のフィルタは空のキーの下に定義します。 ```php use Nette\Routing\Route; @@ -314,85 +324,101 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -一般フィルタを使用すると、ルートの動作を完全に任意の方法で変更できます。たとえば、他のパラメータに基づいてパラメータを変更するために使用できます。たとえば、パラメータ `` の現在の値に基づいて `` と `` を翻訳するなどです。 +一般のフィルタを使えば、ルートの振る舞いを本当に好きなように変えられます。たとえばほかのパラメータをもとにパラメータを変えるのに使えます。たとえば `` パラメータの今の値をもとに `` と `` を変換する、といった具合です。 -パラメータに独自のフィルタが定義されており、同時に一般フィルタが存在する場合、独自の `FilterIn` が一般フィルタの前に実行され、逆に一般フィルタの `FilterOut` が独自のフィルタの前に実行されます。したがって、一般フィルタ内では、パラメータ `presenter` および `action` の値はPascalCaseおよびcamelCaseスタイルで記述されます。 +パラメータに独自のフィルタが定義されていて、一般のフィルタもある場合、独自の `FilterIn` が一般のものより先に実行され、逆に一般の `FilterOut` が独自のものより先に実行されます。ですから一般のフィルタの中では、`presenter` と `action` のパラメータの値はそれぞれ PascalCase と camelCase の書き方になっています。 +これらのフィルタの実用的な使い方は [スラッグ付きのきれいな URL |best-practices:pretty-urls]をご覧ください。テンプレートを一切変えずに `/article/123-how-to-bake-bread` のような SEO にやさしい URL を生成します。 -一方向OneWay ---------- -一方向ルートは、アプリケーションがもはや生成しないが、まだ受け入れている古いURLの機能を維持するために使用されます。それらを `OneWay` フラグでマークします: +OneWay フラグ +---------- + +一方通行のルートは、アプリケーションがもう生成しないけれど受け付けはする古い URL を生かしておくのに使います。`OneWay` フラグで印を付けます。 ```php -// 古いURL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// 新しいURL /product/123 +// 古い URL /product-info?id=123 +$router->addRoute('product-info', 'Product:detail', oneWay: true); +// 新しい URL /product/123 $router->addRoute('product/', 'Product:detail'); ``` -古いURLにアクセスすると、Presenterは自動的に新しいURLにリダイレクトするため、検索エンジンはこれらのページを2回インデックス付けしません([#SEOとカノニカル化]を参照)。 +古い URL にアクセスすると、プレゼンターが自動的に新しい URL へリダイレクトするので、検索エンジンがこれらのページを二重に登録することはありません([#SEO と正規化]をご覧ください)。 -コールバックによる動的ルーティング ------------------ +コールバックによる動的なルーティング +------------------ -コールバックによる動的ルーティングを使用すると、ルートに直接関数(コールバック)を割り当てることができ、特定のパスが訪問されたときに実行されます。この柔軟な機能により、アプリケーションのさまざまなエンドポイントを迅速かつ効率的に作成できます: +コールバックによる動的なルーティングを使うと、ルートに関数(コールバック)を直接割り当てられ、そのパスが訪れられたときに実行されます。この柔軟な機能のおかげで、アプリケーションのさまざまなエンドポイントを手早く効率よく作れます。 ```php $router->addRoute('test', function () { - echo 'あなたは /test アドレスにいます'; + echo 'You are at the /test address'; }); ``` -マスクにパラメータを定義することもでき、それらは自動的にコールバックに渡されます: +マスクの中にパラメータを定義することもでき、それは自動的にコールバックに渡されます。 ```php $router->addRoute('', function (string $lang) { echo match ($lang) { - 'cs' => '私たちのウェブサイトのチェコ語版へようこそ!', + '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#Presenterとテンプレート]に属する複数のルートがある場合は、`withModule()` を使用します: +共通の[モジュール |directory-structure#プレゼンターとテンプレート]に属するルートが複数あるなら、`withModule()` を使います。指定したモジュールは、そのグループのすべてのルートのプレゼンターに自動的に前置され、URL からはすっかり消えます。 ```php $router = new RouteList; $router->withModule('Forum') // 以下のルートは Forum モジュールの一部です - ->addRoute('rss', 'Feed:rss') // presenter は Forum:Feed になります + ->addRoute('rss', 'Feed:rss') // プレゼンターは Forum:Feed になります ->addRoute('/') ->withModule('Admin') // 以下のルートは Forum:Admin モジュールの一部です ->addRoute('sign:in', 'Sign:in'); ``` -代替案は、パラメータ `module` を使用することです: +代わりに `module` パラメータも使えます。これも同じように固定のモジュールを設定し、URL には現れないようにします。 ```php -// URL manage/dashboard/default は Admin:Dashboard presenter にマッピングされます +// 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; @@ -401,7 +427,7 @@ $router->withDomain('example.com') ->addRoute('/'); ``` -ドメイン名には[#ワイルドカード]を使用することもできます: +ドメイン名には [#ワイルドカード]も使えます。 ```php $router = new RouteList; @@ -410,23 +436,23 @@ $router->withDomain('example.%tld%') ``` -パスプレフィックス ---------- +パスの接頭辞 +------ -ルートコレクションをURLのパスごとに分割できます: +ルートのコレクションは URL のパスごとに分けられます。 ```php $router = new RouteList; $router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // URL /eshop/rss をキャッチします - ->addRoute('/'); // URL /eshop// をキャッチします + ->addRoute('rss', 'Feed:rss') // URL /eshop/rss に一致します + ->addRoute('/'); // URL /eshop// に一致します ``` 組み合わせ ----- -上記の分割は相互に組み合わせることができます: +上のグループ分けは互いに組み合わせられます。 ```php $router = (new RouteList) @@ -449,34 +475,34 @@ $router = (new RouteList) クエリパラメータ -------- -マスクにはクエリパラメータ(URLの疑問符の後のパラメータ)も含めることができます。これらには検証式を定義できませんが、Presenterに渡される名前を変更できます: +マスクにはクエリパラメータ(URL の疑問符のうしろのパラメータ)も入れられます。これには検証の正規表現を定義できませんが、プレゼンターに渡される名前は変えられます。 ```php -// クエリパラメータ 'cat' をアプリケーションで 'categoryId' という名前で使用したい +// クエリパラメータ 'cat' をアプリケーションでは 'categoryId' という名前で使いたい $router->addRoute('product ? id= & cat=', /* ... */); ``` -Fooパラメータ --------- +Foo パラメータ +--------- -さて、さらに深く掘り下げます。Fooパラメータは、基本的に名前のないパラメータであり、正規表現をマッチさせることができます。例としては、`/index`、`/index.html`、`/index.htm`、`/index.php` を受け入れるルートがあります: +さらに深いところへ進みます。Foo パラメータは要するに名前のないパラメータで、正規表現に一致させられます。例として `/index`、`/index.html`、`/index.htm`、`/index.php` を受け付けるルートを挙げます。 ```php $router->addRoute('index', /* ... */); ``` -URLを生成する際に使用される文字列を明示的に定義することもできます。文字列は疑問符の直後に配置する必要があります。次のルートは前のルートに似ていますが、文字列 `.html` が生成値として設定されているため、`/index` の代わりに `/index.html` を生成します: +URL を生成するときに使われる文字列を、はっきり定義することもできます。その文字列は疑問符のすぐうしろに置かなければなりません。次のルートは前のものと似ていますが、`/index` の代わりに `/index.html` を生成します。生成に使う値として文字列 `.html` が設定されているからです。 ```php $router->addRoute('index', /* ... */); ``` -アプリケーションへの統合 -============ +組み込み +==== -作成したルータをアプリケーションに組み込むには、DIコンテナにそれについて伝える必要があります。最も簡単な方法は、ルータオブジェクトを作成するファクトリを準備し、コンテナの設定でそれを使用するように指示することです。その目的のために、メソッド `App\Core\RouterFactory::createRouter()` を記述するとしましょう: +作ったルーターをアプリケーションに組み込むには、DI コンテナにそれを教える必要があります。もっとも簡単なのは、ルーターのオブジェクトを作るファクトリを用意して、設定でそれを使うようにコンテナに伝えることです。そのために `App\Core\RouterFactory::createRouter()` メソッドを書くとしましょう。 ```php namespace App\Core; @@ -494,14 +520,14 @@ class RouterFactory } ``` -次に、[設定 |dependency-injection:services]に次のように記述します: +そして[設定 |dependency-injection:services]に次のように書きます。 ```neon services: - App\Core\RouterFactory::createRouter ``` -データベースなどへの依存関係は、[autowiring|dependency-injection:autowiring]を使用してファクトリメソッドにそのパラメータとして渡されます: +データベースなどへの依存関係があれば、[オートワイヤリング|dependency-injection:autowiring]によってファクトリメソッドのパラメータとして渡されます。 ```php public static function createRouter(Nette\Database\Connection $db): RouteList @@ -514,22 +540,22 @@ public static function createRouter(Nette\Database\Connection $db): RouteList SimpleRouter ============ -ルートコレクションよりもはるかに単純なルータは、[SimpleRouter |api:Nette\Application\Routers\SimpleRouter]です。URLの形式に特別な要件がない場合、`mod_rewrite`(またはその代替)が利用できない場合、またはまだきれいなURLを扱いたくない場合に使用します。 +ルートのコレクションよりずっと単純なルーターが [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]です。URL の形に特別な要求がないとき、`mod_rewrite`(やその代わりになるもの)が使えないとき、あるいはまだきれいな URL に手を出したくないときに使います。 -おおよそ次のような形式のアドレスを生成します: +だいたい次のような形のアドレスを生成します。 ``` http://example.com/?presenter=Product&action=detail&id=123 ``` -SimpleRouterのコンストラクタのパラメータは、パラメータなしでページを開いた場合(例:`http://example.com/`)にリダイレクトされるデフォルトのPresenterとアクションです。 +`SimpleRouter` のコンストラクタのパラメータは既定のプレゼンターとアクション、つまり `http://example.com/` をほかのパラメータなしで開いたときに実行されるアクションです。 ```php -// デフォルトのpresenterは 'Home'、アクションは 'default' になります +// 既定のプレゼンターは 'Home'、アクションは 'default' になります $router = new Nette\Application\Routers\SimpleRouter('Home:default'); ``` -SimpleRouterを[設定 |dependency-injection:services]で直接定義することをお勧めします: +SimpleRouter は[設定 |dependency-injection:services]で直接定義することをおすすめします。 ```neon services: @@ -537,22 +563,22 @@ services: ``` -SEOとカノニカル化 -========== +SEO と正規化 +======== -フレームワークは、異なるURLでコンテンツが重複するのを防ぐことで、SEO(検索エンジン最適化)に貢献します。特定のターゲットに複数のアドレス(例:`/index` と `/index.html`)がある場合、フレームワークは最初のものをプライマリ(カノニカル)として指定し、その他をHTTPコード301でリダイレクトします。これにより、検索エンジンはページを2回インデックス付けせず、ページランクを希釈しません。 +フレームワークは、異なる URL に同じ内容が現れるのを防ぐことで SEO(検索エンジン最適化)に貢献します。ある行き先に複数のアドレス、たとえば `/index` と `/index.html` が通じている場合、フレームワークは最初のものを主要なもの(正式なもの)と定め、ほかはそこへ HTTP コード 301 でリダイレクトします。おかげで検索エンジンがページを二重に登録して、そのページランクを薄めることがありません。 -このプロセスはカノニカル化と呼ばれます。カノニカルURLは、ルータによって生成されるURL、つまりOneWayフラグのないコレクション内の最初の適合するルートです。したがって、コレクションでは**プライマリルートを最初に**リストします。 +この処理を正規化と呼びます。正式な URL は、ルーターが生成するもの、つまりコレクションの中で最初に一致した OneWay フラグのないルートが生成するものです。ですからコレクションでは**主要なルートを先に**並べます。 -カノニカル化はPresenterによって実行されます。詳細は[カノニカル化 |presenters#カノニカル化]の章を参照してください。 +正規化はプレゼンターが行います。詳しくは [正規化 |presenters#正規化]の章をご覧ください。 HTTPS ===== -HTTPSプロトコルを使用するには、ホスティングで有効にし、サーバーを正しく設定する必要があります。 +HTTPS プロトコルを使うには、ホスティングでそれを有効にし、サーバーを正しく設定する必要があります。 -Webサイト全体をHTTPSにリダイレクトするには、サーバーレベルで設定する必要があります。たとえば、アプリケーションのルートディレクトリにある.htaccessファイルを使用して、HTTPコード301で設定します。設定はホスティングによって異なる場合があり、おおよそ次のようになります: +ウェブサイト全体の HTTPS へのリダイレクトは、サーバーの水準で、たとえばアプリケーションのルートディレクトリの `.htaccess` ファイルで HTTP コード 301 を使って設定しなければなりません。設定はホスティングによって違うことがあり、だいたい次のようになります。 ``` @@ -564,40 +590,40 @@ Webサイト全体をHTTPSにリダイレクトするには、サーバーレベ ``` -ルータはページが読み込まれたのと同じプロトコルでURLを生成するため、他に設定する必要はありません。 +ルーターはページが読み込まれたのと同じプロトコルで URL を生成するので、これ以上の設定は要りません。 -ただし、例外的に異なるルートを異なるプロトコルで実行する必要がある場合は、ルートのマスクで指定します: +とはいえ、例外的にルートごとに違うプロトコルで動かす必要があるなら、ルートのマスクで指定します。 ```php -// HTTPでアドレスを生成します +// HTTP のアドレスを生成します $router->addRoute('http://%host%//', /* ... */); -// HTTPSでアドレスを生成します +// HTTPS のアドレスを生成します $router->addRoute('https://%host%//', /* ... */); ``` -ルータのデバッグ -======== +ルーターのデバッグ +========= -[Tracy Bar |tracy:]に表示されるルーティングパネルは、ルートのリストと、ルータがURLから取得したパラメータを表示する便利なツールです。 +[Tracy Bar |tracy:]に表示されるルーティングのパネルは役に立つ助っ人で、ルートの一覧と、ルーターが URL から取り出したパラメータを見せてくれます。 -緑色のバーと✓記号は、現在のURLを処理したルートを表し、青色と≈記号は、緑色のルートが先行しなかった場合にURLを処理したであろうルートを示します。次に、現在のPresenterとアクションが表示されます。 +✓ の印が付いた緑の帯は、現在の URL を処理したルートを表します。青色と ≈ の印は、緑のルートに先を越されなければやはりその URL を処理したであろうルートを示します。さらに現在のプレゼンターとアクションも見られます。 [* routing-debugger.webp *] -同時に、[カノニカル化 |#SEOとカノニカル化]による予期しないリダイレクトが発生した場合、*redirect*バーのパネルを見て、ルータが最初にURLをどのように理解し、なぜリダイレクトしたかを確認すると便利です。 +同時に、[正規化 |#SEO と正規化]によって思いがけないリダイレクトが起きたときは、パネルの *redirect* の帯を見るとよいでしょう。そこでルーターがもともとその URL をどう解釈し、なぜリダイレクトしたのかがわかります。 .[note] -ルータをデバッグする際には、ブラウザで開発者ツール(Ctrl+Shift+IまたはCmd+Option+I)を開き、ネットワークパネルでキャッシュを無効にして、リダイレクトがキャッシュされないようにすることをお勧めします。 +ルーターをデバッグするときは、ブラウザで開発者ツールを開き(Ctrl+Shift+I または Cmd+Option+I)、Network のパネルでキャッシュを無効にして、リダイレクトが保存されないようにすることをおすすめします。 -パフォーマンス -======= +性能 +=== -ルートの数はルータの速度に影響します。その数は数十を超えるべきではありません。WebサイトのURL構造が複雑すぎる場合は、カスタム[#カスタムルータ]を作成できます。 +ルートの数はルーターの速さに影響します。その数は数十を超えないようにすべきです。ウェブサイトの URL の構造が複雑すぎるなら、独自の[ルーター |#独自のルーター]を書けます。 -ルータにデータベースなどの依存関係がなく、そのファクトリが引数を受け取らない場合は、そのコンパイル済み形式をDIコンテナに直接シリアライズして、アプリケーションをわずかに高速化できます。 +ルーターがデータベースなどへの依存関係を持たず、そのファクトリが引数を取らないなら、その組み立て済みの形を DI コンテナに直接シリアライズして、アプリケーションをわずかに速くできます。 ```neon routing: @@ -605,10 +631,10 @@ routing: ``` -カスタムルータ +独自のルーター ======= -以下の行は、非常に上級のユーザー向けです。独自のルータを作成し、それを自然にルートコレクションに統合できます。ルータは、2つのメソッドを持つ[api:Nette\Routing\Router]インターフェースの実装です: +以下の記述はとても進んだ利用者向けです。独自のルーターを作って、ルートのコレクションに自然に組み込めます。ルーターは 2 つのメソッドを持つ [api:Nette\Routing\Router]インターフェースの実装です。 ```php use Nette\Http\IRequest as HttpRequest; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -メソッド `match` は、現在のリクエスト [$httpRequest |http:request] を処理し、そこからURLだけでなくヘッダーなども取得して、Presenter名とそのパラメータを含む配列に変換します。リクエストを処理できない場合は、nullを返します。 リクエストを処理する際には、少なくともPresenterとアクションを返す必要があります。Presenter名は完全であり、存在する可能性のあるモジュールも含まれます: +`match` メソッドは現在のリクエスト [$httpRequest |http:request](そこからは URL だけでなくヘッダーなども取り出せます)を、プレゼンターの名前とそのパラメータを含む配列に変えます。リクエストを処理できなければ null を返します。リクエストを処理するときは、少なくともプレゼンターを返さなければなりません。アクションは省略でき、指定がなければ `default` になります。プレゼンターの名前は完全で、モジュールも含みます。 ```php [ @@ -637,9 +663,9 @@ class MyRouter implements Nette\Routing\Router ] ``` -メソッド `constructUrl` は、逆にパラメータの配列から結果の絶対URLを構築します。そのために、パラメータ [`$refUrl`|api:Nette\Http\UrlScript](現在のURL)からの情報を使用できます。 +一方 `constructUrl` メソッドは、パラメータの配列から結果となる絶対 URL を組み立てます。そのとき現在の URL である [`$refUrl`|api:Nette\Http\UrlScript]パラメータの情報を使えます。 -`add()` を使用してルートコレクションに追加します: +`add()` でルートのコレクションに足します。 ```php $router = new Nette\Application\Routers\RouteList; @@ -649,16 +675,16 @@ $router->addRoute(/* ... */); ``` -スタンドアロンでの使用 -=========== +単独での利用 +====== -スタンドアロンでの使用とは、Nette ApplicationやPresenterを使用しないアプリケーションでルータの機能を利用することを意味します。この章で示したことのほとんどすべてが適用されますが、以下の違いがあります: +単独での利用とは、Nette Application とプレゼンターを使わないアプリケーションでルーターの機能を活かすことです。この章で見てきたことのほとんどはそこでも当てはまり、違いは次の点だけです。 -- ルートコレクションには[api:Nette\Routing\RouteList]クラスを使用します -- シンプルルータとして[api:Nette\Routing\SimpleRouter]クラスを使用します -- `Presenter:action` のペアが存在しないため、[#拡張表記]を使用します +- ルートのコレクションには [api:Nette\Routing\RouteList]クラスを使います +- 単純なルーターには [api:Nette\Routing\SimpleRouter]クラスを使います +- `Presenter:action` の組が存在しないので、[#詳しい書き方]を使います -したがって、再びルータを構築するメソッドを作成します。例: +ですからここでもルーターを組み立てるメソッドを作ります。たとえば次のようにです。 ```php namespace App\Core; @@ -682,35 +708,35 @@ class RouterFactory } ``` -DIコンテナを使用している場合(推奨)、再びメソッドを設定に追加し、その後ルータとHTTPリクエストをコンテナから取得します: +おすすめの 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を送信します + // 一致するルートが見つからなかったので 404 のエラーを送ります exit; } -// 取得したパラメータを処理します +// 得られたパラメータを処理します $controller = $params['controller']; // ... ``` -そして逆に、ルータを使用してリンクを構築します: +逆にルーターを使ってリンクを組み立てます。 ```php $params = ['controller' => 'ArticleController', 'id' => 123]; @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/ja/templates.texy b/application/ja/templates.texy index 2492f2325a..5e38d60b86 100644 --- a/application/ja/templates.texy +++ b/application/ja/templates.texy @@ -2,9 +2,9 @@ ****** .[perex] -Netteは[Latte |latte:]テンプレートエンジンを使用しています。これはPHPで最も安全なテンプレートエンジンであり、同時に最も直感的なシステムでもあるためです。多くの新しいことを学ぶ必要はなく、PHPの知識といくつかのタグで十分です。 +Nette は [Latte |latte:]というテンプレートシステムを使います。Latte を使うのは、PHP で最も安全なテンプレートシステムであり、同時に最も直感的なシステムだからです。新しく覚えることは多くありません。PHP の知識といくつかのタグで十分です。 -ページは通常、レイアウトテンプレートと特定のアクションのテンプレートから構成されます。これはレイアウトテンプレートの例です。`{block}`ブロックと`{include}`タグに注目してください: +ページはたいてい、レイアウトのテンプレートと個別のアクションのテンプレートから組み立てられます。レイアウトのテンプレートはたとえば次のような形です。`{block}` のブロックと `{include}` タグに注目してください。 ```latte @@ -20,7 +20,7 @@ Netteは[Latte |latte:]テンプレートエンジンを使用しています。 ``` -そして、これはアクションテンプレートになります: +そしてこれがアクションのテンプレートです。 ```latte {block title}Homepage{/block} @@ -31,15 +31,15 @@ Netteは[Latte |latte:]テンプレートエンジンを使用しています。 {/block} ``` -これは、レイアウトの`{include content}`の場所に挿入される`content`ブロックを定義し、レイアウトの`{block title}`を上書きする`title`ブロックも再定義します。結果を想像してみてください。 +ここでは `content` ブロックを定義していて、レイアウトの `{include content}` の場所に挿入されます。あわせて `title` ブロックを定義し直していて、レイアウトの `{block title}` を上書きします。結果を思い浮かべてみてください。 -テンプレートの検索 +テンプレートの探索 --------- -Presenterでどのテンプレートをレンダリングするかを指定する必要はありません。フレームワークはパスを自動的に推測し、記述の手間を省きます。 +プレゼンターでは、どのテンプレートを描くべきかを指定する必要はありません。フレームワークが自動的にパスを導くので、書く手間が省けます。 -各Presenterが独自のディレクトリを持つディレクトリ構造を使用している場合は、アクション(またはビュー)の名前でこのディレクトリにテンプレートを配置するだけです。つまり、アクション`default`にはテンプレート`default.latte`を使用します: +プレゼンターごとにディレクトリを持つ構成を使っているなら、そのディレクトリにアクション(つまりビュー)の名前でテンプレートを置くだけです。たとえば `default` アクションには `default.latte` テンプレートを使います。 /--pre app/ @@ -49,80 +49,113 @@ app/ └── default.latte \-- -Presenterが1つのディレクトリにまとめられ、テンプレートが`templates`フォルダにある構造を使用している場合は、ファイルを`..latte`または`/.latte`に保存します: +プレゼンターをひとつのディレクトリにまとめ、テンプレートを `templates` フォルダに置く構成を使っているなら、`..latte` か `/.latte` のファイルに保存します。 /--pre app/ └── Presenters/ ├── HomePresenter.php └── templates/ - ├── Home.default.latte ← 1番目のバリアント - └── Home/ - └── default.latte ← 2番目のバリアント + ├── Home/ + │ └── default.latte ← 1 つめの形 + └── Home.default.latte ← 2 つめの形 \-- -`templates`ディレクトリは、Presenterクラスを含むディレクトリと同じレベル、つまり1つ上のレベルに配置することもできます。 +`templates` ディレクトリは 1 階層上、つまりプレゼンターのクラスがあるディレクトリと同じ階層に置くこともできます。 -テンプレートが見つからない場合、Presenterは[エラー404 - ページが見つかりません |presenters#404エラーなど]で応答します。 +テンプレートが見つからない場合、プレゼンターは [404 - ページが見つかりません |presenters#404 などのエラー]で応えます。 -`$this->setView('jineView')`を使用してビューを変更します。`$this->template->setFile('/path/to/template.latte')`を使用してテンプレートファイルを直接指定することもできます。 +ビューは `$this->setView('otherView')` で変えられます。`$this->template->setFile('/path/to/template.latte')` でテンプレートのファイルを直接指定することもできます。 .[note] -テンプレートが検索されるファイルは、可能なファイル名の配列を返すメソッド[formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()]をオーバーライドすることで変更できます。 +テンプレートを探すファイルは、可能なファイル名の配列を返す [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()]メソッドを上書きすれば変えられます。 -レイアウトテンプレートの検索 --------------- +レイアウトのテンプレートの探索 +--------------- -Netteはレイアウトファイルも自動的に検索します。 +Nette はレイアウトのファイルも自動的に探します。 -各Presenterが独自のディレクトリを持つディレクトリ構造を使用している場合は、レイアウトをPresenterのフォルダに配置します(そのPresenterに固有の場合)。または、複数のPresenterで共有されている場合は1つ上のレベルに配置します: +プレゼンターごとにディレクトリを持つ構成を使っているなら、そのプレゼンター専用のレイアウトはプレゼンターのフォルダに、複数のプレゼンターで共通のレイアウトは 1 階層上に置きます。 /--pre app/ └── Presentation/ - ├── @layout.latte ← 共通レイアウト + ├── @layout.latte ← 共通のレイアウト └── Home/ - ├── @layout.latte ← Home presenter 専用 + ├── @layout.latte ← Home プレゼンター専用 ├── HomePresenter.php └── default.latte \-- -Presenterが1つのディレクトリにまとめられ、テンプレートが`templates`フォルダにある構造を使用している場合、レイアウトは次の場所にあると想定されます: +プレゼンターをひとつのディレクトリにまとめ、テンプレートを `templates` フォルダに置く構成なら、レイアウトは次の場所で探されます。 /--pre app/ └── Presenters/ ├── HomePresenter.php └── templates/ - ├── @layout.latte ← 共通レイアウト - ├── Home.@layout.latte ← Home 専用、1番目のバリアント - └── Home/ - └── @layout.latte ← Home 専用、2番目のバリアント + ├── @layout.latte ← 共通のレイアウト + ├── Home/ + │ └── @layout.latte ← Home 専用、1 つめの形 + └── Home.@layout.latte ← Home 専用、2 つめの形 \-- -Presenterがモジュール内にある場合、モジュールのネストに応じて、さらに上のディレクトリレベルでも検索されます。 +プレゼンターがモジュールの中にある場合は、モジュールの入れ子に応じてディレクトリの階層をさらに上へ探していきます。 -レイアウト名は`$this->setLayout('layoutAdmin')`を使用して変更でき、その場合、ファイル`@layoutAdmin.latte`にあると想定されます。`$this->setLayout('/path/to/template.latte')`を使用してレイアウトテンプレートファイルを直接指定することもできます。 +レイアウトの名前は `$this->setLayout('layoutAdmin')` で変えられ、その場合は `@layoutAdmin.latte` ファイルが期待されます。`$this->setLayout('/path/to/template.latte')` でレイアウトのテンプレートファイルを直接指定することもできます。 -`$this->setLayout(false)`またはテンプレート内の`{layout none}`タグを使用すると、レイアウト検索が無効になります。 +`$this->setLayout(false)` を使うか、テンプレートの中で `{layout none}` タグを使うと、レイアウトの探索を無効にできます。 .[note] -レイアウトテンプレートが検索されるファイルは、可能なファイル名の配列を返すメソッド[formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()]をオーバーライドすることで変更できます。 +レイアウトのテンプレートを探すファイルは、可能なファイル名の配列を返す [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()]メソッドを上書きすれば変えられます。 -テンプレート内の変数 ----------- +テンプレートの変数 +--------- -テンプレートに変数を渡すには、それらを`$this->template`に書き込みます。その後、テンプレート内でローカル変数として利用できます: +変数は `$this->template` に書き込むことでテンプレートに渡します。テンプレートの中ではローカル変数として使えるようになります。 ```php $this->template->article = $this->articles->getById($id); ``` -このようにして、任意の変数をテンプレートに簡単に渡すことができます。ただし、堅牢なアプリケーションを開発する場合、制限を設ける方が役立つ場合があります。たとえば、テンプレートが期待する変数のリストとその型を明示的に定義するなどです。これにより、PHPは型をチェックでき、IDEは正しく提案でき、静的解析はエラーを検出できます。 +プロパティの値を変数として自動的にテンプレートへ渡すには、`#[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 の賢い補完が得られ、静的解析でエラーを捕まえられるようになります。 -そして、そのようなリストをどのように定義するのでしょうか? 単純にクラスとそのプロパティの形式で定義します。Presenterと同様に名前を付けますが、最後に`Template`を付けます: +そうした一覧はどう定義するのでしょうか。テンプレートの変数を表すプロパティを持つクラスとして書くだけです。名前はプレゼンターに似せ、末尾に `Template` を付けます。 ```php /** @@ -137,26 +170,28 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template public Model\Article $article; public Nette\Security\User $user; - // その他の変数 + // そしてほかの変数 } ``` -Presenterの`$this->template`オブジェクトは、`ArticleTemplate`クラスのインスタンスになります。したがって、PHPは書き込み時に宣言された型をチェックします。そして、PHP 8.2以降では、存在しない変数への書き込みについても警告します。以前のバージョンでは、トレイト[Nette\SmartObject |utils:smartobject]を使用することで同じことが達成できます。 +これでプレゼンターの `$this->template` オブジェクトは `ArticleTemplate` クラスのインスタンスになります。書き込むとき、PHP が宣言された型を確認してくれます。 + +Nette はテンプレートのクラスを自動的に選びます。まず `Template` という名前のクラス、たとえば `edit` アクションなら `ArticleEditTemplate` を探し、それがない場合にだけ `Template` に戻ります。 -`@property-read`アノテーションはIDEと静的解析向けであり、これにより提案が機能します。「PhpStorm and code completion for $this⁠-⁠>⁠template」:https://blog.nette.org/en/phpstorm-and-code-completion-for-this-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: 型システムの使い方」:https://blog.nette.org/en/latte-how-to-use-type-system の記事を参照してください: +テンプレートの中でも直接コード補完を使えます。PhpStorm 用の Latte プラグインをインストールし、テンプレートの先頭でテンプレートパラメータのクラス名を指定するだけです。詳しくは [Latte: 型システム |latte:type-system]の章をご覧ください。 ```latte {templateType App\Presentation\Article\ArticleTemplate} ... ``` -これはコンポーネントのテンプレートでも機能します。命名規則に従い、たとえばコンポーネント`FifteenControl`に対してテンプレートクラス`FifteenTemplate`を作成するだけです。 +同じことがコンポーネントにも当てはまります。命名の決まりに従って、`FifteenControl` のようなコンポーネントには `FifteenTemplate` というパラメータのクラスを作るだけです。 -`$template`を別のクラスのインスタンスとして作成する必要がある場合は、`createTemplate()`メソッドを使用します: +別のパラメータのクラスを使う必要があるなら、`createTemplate()` メソッドを使います。 ```php public function renderDefault(): void @@ -168,58 +203,100 @@ public function renderDefault(): void } ``` +.{data-version:3.3.0} +描画の前にテンプレートがどう仕上げられるかに手を入れたい場合、たとえばすべてのアクションで共通の変数を足したい場合は、プレゼンターの `completeTemplate()` メソッドを上書きできます。これはテンプレートが描かれる直前に呼ばれます。 -デフォルト変数 -------- - -Presenterとコンポーネントは、いくつかの便利な変数を自動的にテンプレートに渡します: - -- `$basePath` はルートディレクトリへの絶対URLパスです(例:`/eshop`) -- `$baseUrl` はルートディレクトリへの絶対URLです(例:`http://localhost/eshop`) -- `$user` は[ユーザーを表す |security:authentication]オブジェクトです -- `$presenter` は現在のPresenterです -- `$control` は現在のコンポーネントまたはPresenterです -- `$flashes` は関数 `flashMessage()` によって送信された[メッセージ |presenters#フラッシュメッセージ]の配列です - -独自のテンプレートクラスを使用している場合、これらの変数はプロパティを作成すれば渡されます。 +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My blog'; +} +``` リンクの作成 ------ -テンプレートでは、他のPresenterとアクションへのリンクは次のように作成されます: +テンプレートでは、ほかのプレゼンターやアクションへのリンクを次のように作ります。 ```latte -製品詳細 +商品の詳細 ``` -属性 `n:href` はHTMLタグ `` に非常に便利です。リンクを他の場所、たとえばテキスト内に出力したい場合は、`{link}` を使用します: +`n:href` 属性は HTML の `` タグにとても便利です。リンクをそれ以外の場所、たとえば文章の中に出したい場合は `{link}` を使います。 ```latte -アドレスは: {link Home:default} +URL は次のとおりです: {link Home:default} ``` -詳細については、[URLリンクの作成|creating-links]の章を参照してください。 +詳しくは [URL リンクの作成|creating-links]の章をご覧ください。 カスタムフィルタ、タグなど ------------- -Latteテンプレートシステムは、カスタムフィルタ、関数、タグなどで拡張できます。これは、`render`または`beforeRender()`メソッドで直接行うことができます: +Latte のテンプレートシステムは、独自のフィルタ、関数、タグなどで拡張できます。手早いその場しのぎの解から、アプリケーション全体のためのアーキテクチャ上のパターンまで、3 つの方法があります。 + +**プレゼンターのメソッドでその場しのぎに** + +最も手早いのは、プレゼンターやコンポーネントのコードで直接フィルタや関数を足す方法です。プレゼンターでは `beforeRender()` や `render()` メソッドがこれに向いています。 ```php -public function beforeRender(): void +protected function beforeRender(): void { // フィルタの追加 - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); + + // 関数の追加 + $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); +} +``` + +テンプレートでは次のようにします。 - // または Latte\Engine オブジェクトを直接設定 +```latte +

    価格: {$price|money}

    + +{if isWeekend($now)} ... {/if} +``` + +もっと複雑なロジックが必要なら、`Latte\Engine` オブジェクトを直接設定できます。 + +```php +protected function beforeRender(): void +{ $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); + $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バージョン3では、より高度な方法として、各Webプロジェクト用に[extension |latte:extending-latte#Latte Extension]を作成する方法が提供されています。そのようなクラスの簡単な例: +Latte はこれらのアトリビュートが付いたメソッドを自動的に見つけて登録します。テンプレートでのフィルタや関数の名前はメソッド名と同じです。これらのメソッドは public でなければなりません。 + +**拡張を使って全体に** + +ここまでの方法は、特定のプレゼンターやコンポーネントでだけ必要なフィルタや関数に向いていて、アプリケーション全体には向きません。アプリケーション全体には[拡張 |latte:extending-latte#Latte Extension]を作るのが最適です。このクラスが、プロジェクトのすべての Latte の拡張をひとつにまとめます。短い例を挙げます。 ```php namespace App\Presentation\Accessory; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -[設定 |configuration#Latte テンプレート]を使用して登録します: +拡張は[設定 |configuration#Latte テンプレート]で登録します。 ```neon latte: @@ -263,13 +345,27 @@ latte: - 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()`という1つのメソッドがあります。これはメッセージ`$message`(通常は文字列)と任意の追加パラメータを受け取ります。タスクは翻訳された文字列を返すことです。 Netteにはデフォルトの実装はありません。[Componette |https://componette.org/search/localization]で見つけることができるいくつかの既製のソリューションから、ニーズに合わせて選択できます。それらのドキュメントで、トランスレータの設定方法を学びます。 +多言語のアプリケーションを作っているなら、テンプレートのテキストを言語ごとに出し分ける必要が出てくるでしょう。そのために Nette Framework は翻訳のインターフェース [api:Nette\Localization\Translator]を定義していて、`translate()` というメソッドをひとつだけ持ちます。メッセージ `$message`(ふつうは文字列です)とそのほかのパラメータを受け取ります。役目は翻訳された文字列を返すことです。Nette には既定の実装がありません。[Componette |https://componette.org/search/localization]にある既製の解決策から、必要に合うものを選べます。トランスレーターの設定方法は、それぞれのドキュメントで説明されています。 -テンプレートには、[渡してもらう |dependency-injection:passing-dependencies]トランスレータを`setTranslator()`メソッドで設定できます: +テンプレートには、[注入してもらった |dependency-injection:passing-dependencies]トランスレーターを `setTranslator()` メソッドで設定できます。 ```php protected function beforeRender(): void @@ -279,7 +375,7 @@ protected function beforeRender(): void } ``` -あるいは、トランスレータは[設定 |configuration#Latte テンプレート]を使用して設定することもできます: +あるいはトランスレーターを[設定 |configuration#Latte テンプレート]で指定することもできます。 ```neon latte: @@ -287,7 +383,7 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -その後、トランスレータは、たとえばフィルタ`|translate`として使用でき、`translate()`メソッドに渡される追加パラメータ(`foo, bar`を参照)も含みます: +そうすればトランスレーターを、たとえばフィルタ `|translate` として使えます。`translate()` メソッドに渡される追加のパラメータも書けます(`foo, bar` を参照)。 ```latte
    {='カート'|translate} @@ -295,7 +391,7 @@ latte: {$item|translate, foo, bar} ``` -またはアンダースコアタグとして: +あるいはアンダースコアのタグとして。 ```latte {_'カート'} @@ -303,14 +399,14 @@ latte: {_$item, foo, bar} ``` -テンプレートの一部を翻訳するには、ペアタグ`{translate}`があります(Latte 2.11以降、以前は`{_}`タグが使用されていました): +テンプレートの一部を翻訳するには、ペアタグ `{translate}` があります(Latte 2.11 以降。以前は `{_}` タグが使われていました)。 ```latte {translate}注文{/translate} {translate foo, bar}注文{/translate} ``` -トランスレータは通常、テンプレートのレンダリング中に実行時に呼び出されます。ただし、Latteバージョン3では、テンプレートのコンパイル中にすべての静的テキストを翻訳できます。これにより、各文字列が一度だけ翻訳され、結果の翻訳がコンパイル済み形式に書き込まれるため、パフォーマンスが節約されます。キャッシュディレクトリには、言語ごとに複数のコンパイル済みバージョンのテンプレートが作成されます。これを行うには、言語を2番目のパラメータとして指定するだけです: +トランスレーターはふつう、テンプレートが描かれる実行時に呼ばれます。ただし Latte のバージョン 3 は、すべての静的なテキストをテンプレートのコンパイル時に翻訳できます。各文字列が一度だけ翻訳され、その結果がコンパイル済みの形に書き込まれるので、性能が上がります。この場合、キャッシュディレクトリには言語ごとに複数のコンパイル済みテンプレートができます。そのためには、第 2 パラメータに言語を指定するだけです。 ```php protected function beforeRender(): void @@ -320,4 +416,4 @@ protected function beforeRender(): void } ``` -静的テキストとは、たとえば `{_'hello'}` や `{translate}hello{/translate}` のようなものを意味します。`{_$foo}` のような非静的テキストは、引き続き実行時に翻訳されます。 +静的なテキストとは、たとえば `{_'hello'}` や `{translate}hello{/translate}` のことです。`{_$foo}` のような静的でないテキストは、引き続き実行時に翻訳されます。 diff --git a/application/ja/upgrading.texy b/application/ja/upgrading.texy new file mode 100644 index 0000000000..e69196b350 --- /dev/null +++ b/application/ja/upgrading.texy @@ -0,0 +1,47 @@ +アップグレード +******* + + +バージョン 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 index da1901a760..725762aab4 100644 --- a/application/meta.json +++ b/application/meta.json @@ -1,5 +1,6 @@ { - "version": "4.0", + "version": "4.x", "repo": "nette/application", - "composer": "nette/application" + "composer": "nette/application", + "api": "https://api.nette.org/application/" } diff --git a/application/pl/@home.texy b/application/pl/@home.texy index e7a87853d3..5f8b714738 100644 --- a/application/pl/@home.texy +++ b/application/pl/@home.texy @@ -2,13 +2,13 @@ Nette Application ***************** .[perex] -Nette Application jest rdzeniem frameworka Nette, który dostarcza potężne narzędzia do tworzenia nowoczesnych aplikacji internetowych. Oferuje szereg wyjątkowych funkcji, które znacząco ułatwiają rozwój oraz poprawiają bezpieczeństwo i utrzymywalność kodu. +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ą narzędzia [Composer|best-practices:composer]: +Bibliotekę pobierzesz i zainstalujesz za pomocą [Composera|best-practices:composer]: ```shell composer require nette/application @@ -18,68 +18,68 @@ composer require nette/application Dlaczego wybrać Nette Application? ---------------------------------- -Nette zawsze było pionierem w dziedzinie technologii internetowych. +Nette od zawsze było pionierem technologii webowych. -**Dwukierunkowy router:** Nette dysponuje zaawansowanym systemem routingu, który jest unikalny dzięki swojej dwukierunkowości - nie tylko tłumaczy URL na akcje aplikacji, ale także potrafi generować adresy URL wstecz. Oznacza to, że: -- Możesz w dowolnym momencie zmienić strukturę URL całej aplikacji bez konieczności modyfikowania szablonów +**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 jest definiowany w jednym miejscu, a nie rozproszony w adnotacjach - -**Komponenty i sygnały:** Wbudowany system komponentów inspirowany Delphi i React.js jest całkowicie wyjątkowy wśród frameworków PHP: -- Umożliwia tworzenie reużywalnych elementów UI -- Obsługuje hierarchiczne składanie komponentów -- Oferuje eleganckie przetwarzanie żą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 AJAXem już w 2009 roku, długo przed podobnymi rozwiązaniami jak Hotwire dla Ruby on Rails czy Symfony UX Turbo: -- Snippety umożliwiają aktualizację tylko części strony bez konieczności pisania JavaScriptu -- Automatyczna integracja z systemem komponentów -- Inteligentna inwalidacja części stron -- Minimalna ilość przesyłanych danych - -**Intuicyjne szablony [Latte|latte:]:** Najbezpieczniejszy system szablonów dla PHP z zaawansowanymi funkcjami: -- Automatyczna ochrona przed XSS z kontekstowym escapowaniem -- Rozszerzalność za pomocą własnych filtrów, funkcji i znaczników -- Dziedziczenie szablonów i snippety dla AJAX -- Doskonałe wsparcie PHP 8.x z systemem typów - -**Dependency Injection:** Nette w pełni wykorzystuje Dependency Injection: -- Automatyczne przekazywanie zależności (autowiring) -- Konfiguracja za pomocą przejrzystego formatu NEON -- Wsparcie dla fabryk komponentów - - -Główne zalety -------------- - -- **Bezpieczeństwo**: Automatyczna obrona przed [podatnościami|nette:vulnerability-protection] takimi jak XSS, CSRF, itd. -- **Produktywność**: Mniej pisania, więcej funkcji dzięki inteligentnemu projektowi -- **Debugowanie**: [Debugger Tracy|tracy:] z panelem routingu -- **Wydajność**: Inteligentny cache, leniwe ładowanie komponentów -- **Elastyczność**: Łatwa modyfikacja URL nawet po zakończeniu aplikacji -- **Komponenty**: Unikalny system reużywalnych elementów UI -- **Nowoczesność**: Pełne wsparcie PHP 8.4+ i systemu typów - - -Zaczynamy ---------- - -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] - Konfiguracja adresów URL -5. [Komponenty interaktywne |components] - Wykorzystanie systemu komponentów - - -Kompatybilność z PHP --------------------- - -| wersja | kompatybilna z PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 - -Dotyczy ostatniej wersji patch. +- 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 index 8600cb220d..ed22b0f063 100644 --- a/application/pl/@left-menu.texy +++ b/application/pl/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** +- [Przegląd |@home] - [Jak działają aplikacje? |how-it-works] -- [Bootstrapping] -- [Presentery |presenters] -- [Szablony |templates] +- [Bootstrapping|bootstrapping] +- [Presentery|presenters] +- [Szablony|templates] - [Struktura katalogów |directory-structure] -- [Routing |routing] -- [Tworzenie linków URL |creating-links] +- [Routing|routing] +- [Tworzenie odnośników URL |creating-links] - [Komponenty interaktywne |components] -- [AJAX & snippety |ajax] -- [Multiplier |Multiplier] -- [Konfiguracja |configuration] +- [AJAX i snippety |ajax] +- [Multiplier |multiplier] +- [Konfiguracja|configuration] +- [Aktualizacja|upgrading] Dalsza lektura ************** -- [Dlaczego używać Nette? |www:10-reasons-why-nette] +- [Dlaczego warto używać Nette?|www:10-reasons-why-nette] - [Instalacja |nette:installation] -- [Pisanie pierwszej aplikacji! |quickstart:] -- [Przewodniki i dobre praktyki |best-practices:] +- [Stwórz swoją pierwszą aplikację! |quickstart:] +- [Dobre praktyki |best-practices:] - [Rozwiązywanie problemów |nette:troubleshooting] diff --git a/application/pl/ajax.texy b/application/pl/ajax.texy index d50269c03f..b7e7b78f9e 100644 --- a/application/pl/ajax.texy +++ b/application/pl/ajax.texy @@ -1,10 +1,10 @@ -AJAX & snippety +AJAX i snippety ***************
    -W erze nowoczesnych aplikacji internetowych, gdzie funkcjonalność jest często rozdzielona między serwerem a przeglądarką, AJAX jest niezbędnym elementem łączącym. Jakie możliwości oferuje nam Nette Framework w tej dziedzinie? -- wysyłanie fragmentów szablonu, tzw. snippetów +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 @@ -14,9 +14,9 @@ W erze nowoczesnych aplikacji internetowych, gdzie funkcjonalność jest często Żądanie AJAX ============ -Żądanie AJAX zasadniczo nie różni się od klasycznego żądania HTTP. Wywoływany jest presenter z określonymi parametrami. Od presentera zależy, w jaki sposób zareaguje na żądanie - może zwrócić dane w formacie JSON, wysłać fragment kodu HTML, dokument XML itp. +Żą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 za pomocą funkcji `fetch()`: +Po stronie przeglądarki inicjujemy żądanie AJAX funkcją `fetch()`: ```js fetch(url, { @@ -24,22 +24,22 @@ fetch(url, { }) .then(response => response.json()) .then(payload => { - // przetwarzanie odpowiedzi + // przetwarzamy odpowiedź }); ``` -Po stronie serwera rozpoznajemy żądanie AJAX za pomocą metody `$httpRequest->isAjax()` usługi [enkapsulującej żądanie HTTP |http:request]. Do detekcji używa nagłówka HTTP `X-Requested-With`, dlatego ważne jest, aby go wysyłać. W ramach presentera można użyć metody `$this->isAjax()`. +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#Wysłanie odpowiedzi]. Metoda ta również kończy działanie presentera. +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); + $this->sendJson($this->model->getData()); } ``` -Jeśli planujesz odpowiedzieć za pomocą specjalnego szablonu przeznaczonego dla AJAX, możesz to zrobić w następujący sposób: +Jeśli planujesz odpowiedzieć specjalnym szablonem przeznaczonym dla AJAX-a, możesz zrobić to tak: ```php public function handleClick($param): void @@ -55,44 +55,44 @@ public function handleClick($param): void Snippety ======== -Najpotężniejszym narzędziem oferowanym przez Nette do łączenia serwera z klientem są snippety. Dzięki nim można przekształcić zwykłą aplikację w aplikację AJAXową przy minimalnym wysiłku i kilku linijkach kodu. Jak to wszystko działa, demonstruje przykład Fifteen, którego kod znajdziesz na [GitHubie |https://github.com/nette-examples/fifteen]. +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, czyli fragmenty, umożliwiają aktualizację tylko części strony, zamiast ponownego ładowania całej strony. Jest to nie tylko szybsze i bardziej efektywne, ale także zapewnia bardziej komfortowe doświadczenie użytkownika. Snippety mogą przypominać Hotwire dla Ruby on Rails lub Symfony UX Turbo. Co ciekawe, Nette wprowadziło snippety już 14 lat wcześniej. +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 załadowaniu strony (żądanie nie-AJAXowe) ładowana jest cała strona wraz ze wszystkimi snippetami. Kiedy użytkownik wchodzi w interakcję ze stroną (np. klika przycisk, wysyła formularz itp.), zamiast ładowania całej strony wywoływane jest żądanie AJAX. Kod w presenterze wykonuje akcję i decyduje, które snippety należy zaktualizować. Nette renderuje te snippety i wysyła je w formie tablicy w formacie JSON. Kod obsługujący w przeglądarce wstawia otrzymane snippety z powrotem na stronę. Przesyłany jest więc tylko kod zmienionych snippetów, co oszczędza przepustowość i przyspiesza ładowanie w porównaniu do przesyłania całej zawartości strony. +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 |https://naja.js.org/#/guide/01-install-setup-naja] ją jako pakiet node.js (do użytku z aplikacjami Webpack, Rollup, Vite, Parcel i innymi): +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 ``` -…lub bezpośrednio wstaw do szablonu strony: +…albo wstaw ją bezpośrednio do szablonu strony: ```latte - + ``` -Najpierw należy [zainicjować |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] bibliotekę: +Najpierw trzeba bibliotekę [zainicjować |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: ```js naja.initialize(); ``` -Aby zwykły link (sygnał) lub wysłanie formularza przekształcić w żądanie AJAX, wystarczy oznaczyć odpowiedni link, formularz lub przycisk klasą `ajax`: +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 -Go +Idź
    -lub +albo
    @@ -100,46 +100,48 @@ lub ``` -Przerysowanie snippetów ------------------------ +Przerysowywanie snippetów +------------------------- -Każdy obiekt klasy [Control |components] (w tym sam Presenter) śledzi, czy nastąpiły zmiany wymagające jego przerysowania. Służy do tego metoda `redrawControl()`: +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 należy przerysować odpowiednią część + // po zalogowaniu trzeba przerysować odpowiednią część $this->redrawControl(); // ... } ``` -Nette pozwala na jeszcze dokładniejszą kontrolę tego, co ma zostać przerysowane. Wspomniana metoda może bowiem przyjmować jako argument nazwę snippetu. Można więc unieważnić (czytaj: wymusić przerysowanie) na poziomie części szablonu. Jeśli unieważniony zostanie cały komponent, przerysowany zostanie również każdy jego snippet: +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 niezwykle proste. Aby zdefiniować część szablonu jako snippet, wystarczy otoczyć ją znacznikami `{snippet}` i `{/snippet}`: +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} -

    Witaj ...

    +

    Cześć ...

    {/snippet} ``` -Snippet tworzy w stronie HTML element `
    ` ze specjalnym wygenerowanym `id`. Podczas przerysowywania snippeta aktualizowana jest zawartość tego elementu. Dlatego konieczne jest, aby podczas pierwszego renderowania strony renderowane były również wszystkie snippety, nawet jeśli na początku mogą być puste. +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. -Możesz również utworzyć snippet z innym elementem niż `
    ` za pomocą n:atrybutu: +Snippet możesz też utworzyć na elemencie innym niż `
    `, używając n:atrybutu: ```latte
    -

    Witaj ...

    +

    Cześć ...

    ``` @@ -155,7 +157,9 @@ Nazwy snippetów mogą być również wyrażeniami: {/foreach} ``` -W ten sposób powstanie kilka snippetów `item-0`, `item-1` itd. Gdybyśmy bezpośrednio unieważnili dynamiczny snippet (na przykład `item-1`), nic by się nie przerysowało. Powodem jest to, że snippety naprawdę działają jak wycinki i renderowane są tylko one same. Jednak w szablonie faktycznie nie ma żadnego snippeta o nazwie `item-1`. Powstaje on dopiero podczas wykonywania kodu wokół snippeta, czyli pętli foreach. Dlatego oznaczamy część szablonu, która ma zostać wykonana, za pomocą znacznika `{snippetArea}`: +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
      @@ -165,7 +169,7 @@ W ten sposób powstanie kilka snippetów `item-0`, `item-1` itd. Gdybyśmy bezpo
    ``` -I zlecamy przerysowanie zarówno samego snippeta, jak i całego nadrzędnego obszaru: +I żądamy przerysowania zarówno pojedynczego snippetu, jak i całego obszaru nadrzędnego: ```php $this->redrawControl('itemsContainer'); @@ -174,7 +178,7 @@ $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 szablonu za pomocą znacznika `{include}` wstawiamy inny szablon zawierający snippety, konieczne jest ponowne umieszczenie wstawionego szablonu w `snippetArea` i unieważnienie go razem ze snippetem: +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} @@ -198,22 +202,22 @@ $this->redrawControl('item'); Snippety w komponentach ----------------------- -Snippety można tworzyć również w [komponentach|components], a Nette będzie je automatycznie przerysowywać. Istnieje jednak pewne ograniczenie: do przerysowania snippetów wywoływana jest metoda `render()` bez parametrów. Oznacza to, że przekazywanie parametrów w szablonie nie będzie działać: +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 będzie działać: +nie zadziała: {control productGrid $arg, $arg} {control productGrid:paginator} ``` -Wysyłanie danych użytkownika ----------------------------- +Wysyłanie własnych danych +------------------------- -Wraz ze snippetami możesz wysłać klientowi dowolne inne dane. Wystarczy je zapisać w obiekcie `payload`: +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 @@ -226,10 +230,16 @@ public function actionDelete(int $id): void ``` +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 ======================== -Jeśli za pomocą żądania AJAX wysyłamy parametry do komponentu, czy to parametry sygnału, czy parametry persistentne, musimy w żądaniu podać ich globalną nazwę, która zawiera również nazwę komponentu. Pełną nazwę parametru zwraca metoda `getParameterId()`. +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!}); @@ -240,10 +250,16 @@ fetch(url, { }) ``` -A metoda handle z odpowiednimi parametrami w komponencie: +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 index 4cc015bed0..13b0288e7b 100644 --- a/application/pl/bootstrapping.texy +++ b/application/pl/bootstrapping.texy @@ -3,19 +3,22 @@ Bootstrapping
    -Bootstrapping to proces inicjalizacji środowiska aplikacji, tworzenia kontenera dependency injection (DI) i uruchamiania aplikacji. Omówimy: +Bootstrapping to proces inicjalizacji środowiska aplikacji, tworzenia kontenera wstrzykiwania zależności (DI) i uruchamiania aplikacji. Omówimy: - jak klasa Bootstrap inicjalizuje środowisko -- jak aplikacje są konfigurowane przy użyciu plików NEON -- jak rozróżnić tryb produkcyjny i deweloperski +- jak konfiguruje się aplikacje za pomocą plików NEON +- jak odróżnić tryb produkcyjny od deweloperskiego - jak utworzyć i skonfigurować kontener DI
    -Aplikacje, czy to webowe, czy skrypty uruchamiane z wiersza poleceń, rozpoczynają swoje działanie od pewnej formy inicjalizacji środowiska. W dawnych czasach odpowiadał za to plik o nazwie np. `include.inc.php`, który był 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: +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 @@ -26,9 +29,9 @@ class Bootstrap public function __construct() { $this->rootDir = dirname(__DIR__); - // Konfigurator jest odpowiedzialny za ustawienie środowiska aplikacji i usług. + // konfigurator odpowiada za ustawienie środowiska aplikacji i usług $this->configurator = new Configurator; - // Ustawia katalog dla plików tymczasowych generowanych przez Nette (np. skompilowane szablony) + // ustawiamy katalog na pliki tymczasowe generowane przez Nette (np. skompilowane szablony) $this->configurator->setTempDirectory($this->rootDir . '/temp'); } @@ -42,13 +45,13 @@ class Bootstrap private function initializeEnvironment(): void { // Nette jest sprytne i tryb deweloperski włącza się automatycznie, - // lub możesz go włączyć dla konkretnego adresu IP, odkomentowując poniższą linię: + // albo możesz włączyć go dla konkretnego adresu IP, odkomentowując poniższy wiersz: // $this->configurator->setDebugMode('secret@23.75.345.200'); - // Aktywuje Tracy: ostateczny "szwajcarski scyzoryk" do debugowania. + // włącza Tracy: najlepszy "scyzoryk szwajcarski" do debugowania $this->configurator->enableTracy($this->rootDir . '/log'); - // RobotLoader: automatycznie ładuje wszystkie klasy w wybranym katalogu + // RobotLoader: automatycznie wczytuje wszystkie klasy z wybranego katalogu $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); @@ -56,7 +59,7 @@ class Bootstrap private function setupContainer(): void { - // Ładuje pliki konfiguracyjne + // wczytujemy pliki konfiguracyjne $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } @@ -66,69 +69,78 @@ class Bootstrap index.php ========= -Plikiem początkowym w przypadku aplikacji webowych jest `index.php`, który znajduje się w [katalogu publicznym |directory-structure#Katalog publiczny www] `www/`. Ten plik zleca klasie Bootstrap inicjalizację środowiska i utworzenie kontenera DI. Następnie pobiera z niego usługę `Application`, która uruchamia aplikację webową: +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 +// inicjalizacja środowiska + utworzenie kontenera DI $container = $bootstrap->bootWebApplication(); -// Kontener DI tworzy obiekt Nette\Application\Application +// kontener DI tworzy obiekt Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// Uruchomienie aplikacji Nette i przetworzenie przychodzącego żądania +// uruchamiamy aplikację Nette i obsługujemy przychodzące żądanie $application->run(); ``` -Jak widać, w ustawieniu środowiska i tworzeniu kontenera dependency injection (DI) pomaga klasa [api:Nette\Bootstrap\Configurator], którą teraz bliżej przedstawimy. +.[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 vs produkcyjny -================================ +Tryb deweloperski kontra produkcyjny +==================================== -Nette zachowuje się różnie w zależności od tego, czy działa na serwerze deweloperskim czy produkcyjnym: +Nette zachowuje się różnie w zależności od tego, czy działa na serwerze deweloperskim, czy produkcyjnym: -🛠️ Tryb deweloperski (Development): - - Wyświetla pasek debugowania Tracy z użytecznymi informacjami (zapytania SQL, czas wykonania, użyta pamięć) - - W przypadku błędu 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, modyfikacji plików konfiguracyjnych itp. +🛠️ 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 (Production): - - Nie wyświetla żadnych informacji debugowania, wszystkie błędy zapisuje do logu - - W przypadku błędu wyświetla ErrorPresenter lub ogólną stronę "Server Error" - - Cache nigdy nie jest automatycznie odświeżany! - - Zoptymalizowany pod kątem szybkości i bezpieczeństwa +🚀 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 -Wybór trybu odbywa się przez autodetekcję, więc zazwyczaj nie trzeba niczego konfigurować ani ręcznie przełączać: +Tryb wybierany jest przez autodetekcję, więc zwykle nie trzeba niczego konfigurować ani przełączać ręcznie: -- tryb deweloperski: na localhost (adres IP `127.0.0.1` lub `::1`) jeśli nie ma proxy (tj. jego nagłówka HTTP) +- 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ć również tablicę adresów IP +$this->configurator->setDebugMode('23.75.345.200'); // można podać także tablicę adresów IP ``` -Zdecydowanie zalecamy łączenie adresu IP z ciasteczkiem (cookie). W ciasteczku `nette-debug` zapisujemy tajny token, np. `secret1234`, i w ten sposób aktywujemy tryb deweloperski dla programistów łączących się z konkretnego adresu IP i jednocześnie posiadających w ciasteczku wspomniany token: +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 również całkowicie wyłączyć, nawet dla localhost: +Tryb deweloperski możemy też całkowicie wyłączyć, nawet dla localhosta: ```php $this->configurator->setDebugMode(false); ``` -Uwaga, wartość `true` włącza tryb deweloperski na stałe, co nigdy nie powinno mieć miejsca na serwerze produkcyjnym. +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 debugujące Tracy -========================== +Narzędzie do debugowania Tracy +============================== -Dla łatwego debugowania włączymy jeszcze świetne narzędzie [Tracy |tracy:]. W trybie deweloperskim wizualizuje błędy, a w trybie produkcyjnym loguje błędy do podanego katalogu: +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'); @@ -138,19 +150,19 @@ $this->configurator->enableTracy($this->rootDir . '/log'); Pliki tymczasowe ================ -Nette wykorzystuje cache dla kontenera DI, RobotLoadera, szablonów itp. Dlatego konieczne jest ustawienie ścieżki do katalogu, w którym będzie przechowywany cache: +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 lub macOS ustaw katalogom `log/` i `temp/` [uprawnienia do zapisu |nette:troubleshooting#Ustawianie uprawnień do katalogów]. +Na Linuksie albo macOS ustaw katalogom `log/` i `temp/` [prawa do zapisu |nette:troubleshooting#Ustawienie uprawnień do katalogów]. RobotLoader =========== -Zazwyczaj będziemy chcieli automatycznie ładować klasy za pomocą [RobotLoadera |robot-loader:], musimy go więc uruchomić i pozwolić mu ładować klasy z katalogu, w którym znajduje się `Bootstrap.php` (tj. `__DIR__`), oraz wszystkich podkatalogów: +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() @@ -158,13 +170,13 @@ $this->configurator->createRobotLoader() ->register(); ``` -Alternatywnym podejściem jest ładowanie klas wyłącznie przez [Composer |best-practices:composer] przy zachowaniu PSR-4. +Alternatywnym podejściem jest wczytywanie klas wyłącznie przez [Composera |best-practices:composer] zgodnie z PSR-4. Strefa czasowa ============== -Za pomocą konfiguratora można ustawić domyślną strefę czasową. +Za pomocą konfiguratora możesz ustawić domyślną strefę czasową. ```php $this->configurator->setTimeZone('Europe/Prague'); @@ -174,14 +186,16 @@ $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. Jest to właściwie klasa PHP, którą generuje Nette i zapisuje w katalogu z cache. Fabryka tworzy kluczowe obiekty aplikacji, a za pomocą plików konfiguracyjnych instruujemy ją, jak ma je tworzyć i ustawiać, co wpływa na zachowanie całej aplikacji. +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 zazwyczaj zapisuje się w formacie [NEON |neon:format]. W osobnym rozdziale dowiesz się, [co można skonfigurować |nette:configuring]. +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 automatycznie aktualizuje się przy każdej zmianie kodu lub plików konfiguracyjnych. W trybie produkcyjnym generowany jest tylko raz, a zmiany nie są sprawdzane w celu maksymalizacji wydajności. +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ść. -Pliki konfiguracyjne ładujemy za pomocą `addConfig()`: +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'); @@ -198,17 +212,17 @@ if (PHP_SAPI === 'cli') { } ``` -Nazwa `cli.php` nie jest pomyłką, konfiguracja może być zapisana również w pliku PHP, który zwraca ją jako tablicę. +Nazwa `cli.php` nie jest literówką; konfigurację można zapisać także w pliku PHP, który zwraca ją jako tablicę. -Możemy również dodać inne pliki konfiguracyjne w [sekcji `includes` |dependency-injection:configuration#Dołączanie plików]. +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ą one nadpisane lub w przypadku [tablic połączone |dependency-injection:configuration#Łączenie]. Później dołączony plik ma wyższy priorytet niż poprzedni. Plik, w którym znajduje się sekcja `includes`, ma wyższy priorytet niż pliki w nim zawarte. +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żemy zdefiniować [w sekcji `parameters` |dependency-injection:configuration#Parametry], a także przekazywać (lub nadpisywać) metodą `addStaticParameters()` (ma alias `addParameters()`). Ważne jest, że różne wartości parametrów spowodują wygenerowanie kolejnych kontenerów DI, czyli kolejnych klas. +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([ @@ -216,13 +230,13 @@ $this->configurator->addStaticParameters([ ]); ``` -Do parametru `projectId` można odwołać się w konfiguracji za pomocą zwykłego zapisu `%projectId%`. +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 przeciwieństwie do parametrów statycznych, nie spowodują generowania nowych kontenerów DI. +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([ @@ -230,7 +244,7 @@ $this->configurator->addDynamicParameters([ ]); ``` -W prosty sposób możemy dodać np. zmienne środowiskowe, do których można się następnie odwołać w konfiguracji za pomocą zapisu `%env.variable%`. +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([ @@ -242,21 +256,22 @@ $this->configurator->addDynamicParameters([ Parametry domyślne ------------------ -W plikach konfiguracyjnych można używać następujących parametrów statycznych: +W plikach konfiguracyjnych możesz używać tych parametrów: -- `%appDir%` to ścieżka absolutna do katalogu z plikiem `Bootstrap.php` -- `%wwwDir%` to ścieżka absolutna do katalogu z plikiem wejściowym `index.php` -- `%tempDir%` to ścieżka absolutna do katalogu dla plików tymczasowych -- `%vendorDir%` to ścieżka absolutna do katalogu, w którym Composer instaluje biblioteki -- `%rootDir%` to ścieżka absolutna do katalogu głównego projektu -- `%debugMode%` wskazuje, czy aplikacja jest w trybie debugowania -- `%consoleMode%` wskazuje, czy żądanie przyszło przez wiersz poleceń +- `%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 zagłębiamy się bardziej. Chociaż celem kontenera DI jest tworzenie obiektów, wyjątkowo może zaistnieć potrzeba wstawienia istniejącego obiektu do kontenera. Robimy to, definiując usługę z flagą `imported: true`. +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: @@ -274,10 +289,10 @@ $this->configurator->addServices([ ``` -Odmienne środowisko -=================== +Różne środowiska +================ -Nie bój się modyfikować klasy Bootstrap według własnych potrzeb. Do metody `bootWebApplication()` możesz dodać parametry do rozróżniania projektów webowych. Możemy też dodać inne metody, na przykład `bootTestEnvironment()`, która inicjalizuje środowisko dla testów jednostkowych, `bootConsoleApplication()` dla skryptów wywoływanych z wiersza poleceń itp. +Ś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 diff --git a/application/pl/components.texy b/application/pl/components.texy index 0ddc2e7e66..189db24a74 100644 --- a/application/pl/components.texy +++ b/application/pl/components.texy @@ -3,7 +3,7 @@ Komponenty interaktywne
    -Komponenty to samodzielne obiekty wielokrotnego użytku, które wstawiamy na strony. Mogą to być formularze, datagridy, ankiety, właściwie wszystko, co ma sens używać wielokrotnie. Pokażemy: +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ć? @@ -11,19 +11,19 @@ Komponenty to samodzielne obiekty wielokrotnego użytku, które wstawiamy na str
    -Nette ma wbudowany system komponentów. Coś podobnego mogą pamiętać weterani z Delphi lub ASP.NET Web Forms, na czymś zdalnie podobnym opiera się React czy Vue.js. Jednak w świecie frameworków PHP jest to unikalna sprawa. +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. -Przy tym komponenty zasadniczo wpływają na podejście do tworzenia aplikacji. Możesz bowiem składać strony z gotowych jednostek. Potrzebujesz w administracji datagrid? Znajdziesz go na [Componette |https://componette.org/search/component], repozytorium open-source dodatków (czyli nie tylko komponentów) dla Nette i po prostu wstawisz do presentera. +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 do niektórych komponentów możesz wstawiać kolejne komponenty. Powstaje w ten sposób drzewo komponentów, którego korzeniem jest presenter. +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 fabrykujące -================== +Metody fabryczne +================ -Jak wstawiać komponenty do presentera i następnie ich używać? Zazwyczaj za pomocą metod fabrykujących. +Jak komponenty trafiają do presentera i jak się ich potem używa? Zwykle za pomocą metod fabrycznych. -Fabryka komponentów stanowi elegancki sposób na tworzenie komponentów dopiero w chwili, gdy są rzeczywiście potrzebne (lazy / on demand). Cały urok polega na implementacji metody o nazwie `createComponent()`, gdzie `` to nazwa tworzonego komponentu, która tworzy i zwraca komponent. +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 @@ -31,49 +31,54 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } ``` -Dzięki temu, że wszystkie komponenty są tworzone w osobnych metodach, kod zyskuje na przejrzystości. +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ą z dużej. +Nazwy komponentów zawsze zaczynają się małą literą, mimo że w nazwie metody pisane są wielką. -Fabryk nigdy nie wywołujemy bezpośrednio, wywołują się same w chwili, gdy komponent użyjemy po raz pierwszy. Dzięki temu komponent jest tworzony we właściwym momencie i tylko wtedy, gdy jest rzeczywiście potrzebny. Jeśli komponentu nie użyjemy (np. przy żądaniu AJAX, gdy przesyłana jest tylko część strony, lub przy cachowaniu szablonu), nie zostanie on w ogóle utworzony i oszczędzimy wydajność serwera. +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} -// uzyskujemy dostęp do komponentu i jeśli to było po raz pierwszy, -// wywołuje się createComponentPoll(), która go tworzy +// sięgamy po komponent i jeśli był to pierwszy raz, +// wywoła się createComponentPoll(), które go utworzy $poll = $this->getComponent('poll'); -// alternatywna składnia: $poll = $this['poll']; +// alternatywny zapis: $poll = $this['poll']; ``` -W szablonie można wyrenderować komponent za pomocą znacznika [{control} |#Renderowanie]. Nie ma więc potrzeby ręcznego przekazywania komponentów do szablonu. +W szablonie komponent można wyrenderować tagiem [{control} |#Renderowanie]. Nie ma więc potrzeby ręcznego przekazywania komponentów do szablonu. ```latte -

    Głosuj

    +

    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 Hollywood -============== -Komponenty często używają jednej świeżej techniki, którą lubimy nazywać stylem Hollywood. Na pewno znasz skrzydlate zdanie, które tak często słyszą uczestnicy castingów filmowych: „Nie dzwońcie do nas, my zadzwonimy do was”. I właśnie o to chodzi. +Styl hollywoodzki +================= -W Nette bowiem, zamiast ciągle pytać („czy formularz został wysłany?”, „czy był poprawny?” lub „czy użytkownik nacisnął ten przycisk?”), mówisz frameworkowi „kiedy to się stanie, wywołaj tę metodę” i zostawiasz dalszą pracę jemu. Jeśli programujesz w JavaScript, ten styl programowania jest Ci dobrze znany. Piszesz funkcje, które są wywoływane, gdy nastąpi określone zdarzenie. A język przekazuje im odpowiednie parametry. +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. -To całkowicie zmienia spojrzenie na pisanie aplikacji. Im więcej zadań możesz zostawić frameworkowi, tym mniej masz pracy. I tym mniej możesz czegoś np. pominąć. +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 ================== -Pod pojęciem komponent zazwyczaj rozumiemy potomka klasy [api:Nette\Application\UI\Control]. (Dokładniej byłoby więc używać terminu „controls”, ale „kontrolki” mają w języku polskim zupełnie inne znaczenie i raczej przyjęły się „komponenty”.) Sam presenter [api:Nette\Application\UI\Presenter] jest zresztą również potomkiem klasy `Control`. +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; @@ -87,7 +92,7 @@ class PollControl extends Control Renderowanie ============ -Już wiemy, że do renderowania komponentu używa się znacznika `{control componentName}`. Ten znacznik właściwie wywołuje metodę `render()` komponentu, w której dbamy o renderowanie. Do dyspozycji mamy, tak samo jak w presenterze, [szablon Latte|templates] w zmiennej `$this->template`, do której przekazujemy parametry. W przeciwieństwie do presentera musimy podać plik z szablonem i zlecić jego wyrenderowanie: +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 @@ -99,7 +104,7 @@ public function render(): void } ``` -Znacznik `{control}` umożliwia przekazanie parametrów do metody `render()`: +Tag `{control}` pozwala przekazać metodzie `render()` parametry: ```latte {control poll $id, $message} @@ -112,7 +117,7 @@ public function render(int $id, string $message): void } ``` -Czasami komponent może składać się z kilku części, które chcemy renderować oddzielnie. Dla każdej z nich tworzymy własną metodę renderującą, tutaj w przykładzie np. `renderPaginator()`: +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 @@ -121,69 +126,69 @@ public function renderPaginator(): void } ``` -A w szablonie wywołujemy ją za pomocą: +A w szablonie wywołujemy ją potem tak: ```latte {control poll:paginator} ``` -Dla lepszego zrozumienia warto wiedzieć, jak ten znacznik jest tłumaczony na PHP. +Dla lepszego zrozumienia warto wiedzieć, jak ten tag przekłada się na kod PHP. ```latte {control poll} {control poll:paginator 123, 'hello'} ``` -zostanie przetłumaczone jako: +przekłada się na: ```php $control->getComponent('poll')->render(); $control->getComponent('poll')->renderPaginator(123, 'hello'); ``` -Metoda `getComponent()` zwraca komponent `poll` i na tym komponencie wywołuje metodę `render()`, lub `renderPaginator()`, jeśli inny sposób renderowania jest podany w znaczniku za dwukropkiem. +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 gdziekolwiek w parametrach pojawi się **`=>`**, wszystkie parametry zostaną zapakowane do tablicy i przekazane jako pierwszy argument: +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'} ``` -zostanie przetłumaczone jako: +przekłada się na: ```php $control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); ``` -Renderowanie podkomponentu: +Renderowanie subkomponentu: ```latte {control cartControl-someForm} ``` -zostanie przetłumaczone jako: +przekłada się na: ```php $control->getComponent("cartControl-someForm")->render(); ``` -Komponenty, podobnie jak presentery, automatycznie przekazują do szablonów kilka użytecznych zmiennych: +Komponenty, podobnie jak presentery, automatycznie przekazują do szablonów kilka przydatnych zmiennych: -- `$basePath` to absolutna ścieżka URL do katalogu głównego (np. `/eshop`) -- `$baseUrl` to absolutny URL do katalogu głównego (np. `http://localhost/eshop`) +- `$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 aktualny presenter -- `$control` to aktualny komponent -- `$flashes` to tablica [wiadomości |#Wiadomości flash] wysłanych przez funkcję `flashMessage()` +- `$presenter` to bieżący presenter +- `$control` to bieżący komponent +- `$flashes` to tablica [wiadomości |#Wiadomości flash] wysłanych funkcją `flashMessage()` Sygnał ====== -Już wiemy, że nawigacja w aplikacji Nette polega na linkowaniu lub przekierowywaniu do par `Presenter:action`. Ale co, jeśli chcemy tylko wykonać akcję na **aktualnej stronie**? Na przykład zmienić sortowanie kolumn w tabeli; usunąć pozycję; przełączyć tryb jasny/ciemny; wysłać formularz; zagłosować w ankiecie; itp. +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 rodzaj żądań nazywa się sygnałami. I podobnie jak akcje wywołują metody `action()` lub `render()`, sygnały wywołują metody `handle()`. Podczas gdy pojęcie akcji (lub widoku) jest związane wyłącznie z presenterami, sygnały dotyczą wszystkich komponentów. A więc także presenterów, ponieważ `UI\Presenter` jest potomkiem `UI\Control`. +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 @@ -192,36 +197,36 @@ public function handleClick(int $x, int $y): void } ``` -Link, który wywoła sygnał, tworzymy w zwykły sposób, czyli w szablonie za pomocą atrybutu `n:href` lub znacznika `{link}`, w kodzie za pomocą metody `link()`. Więcej w rozdziale [Tworzenie linków URL |creating-links#Linki do 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ł zawsze jest wywoływany na aktualnym presenterze i akcji, nie można go wywołać na innym presenterze lub innej akcji. +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 ponowne załadowanie strony dokładnie tak samo, jak przy pierwotnym żądaniu, tylko dodatkowo wywołuje metodę obsługującą sygnał z odpowiednimi parametrami. Jeśli metoda nie istnieje, rzucany jest wyjątek [api:Nette\Application\UI\BadSignalException], który użytkownikowi wyświetla się jako strona błędu 403 Forbidden. +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ć AJAX: handlery, które są wywoływane na aktualnej stronie. I masz rację, sygnały naprawdę często są wywoływane za pomocą AJAXu, a następnie przesyłamy do przeglądarki tylko zmienione części strony. Czyli tzw. snippety. Więcej informacji znajdziesz na [stronie poświęconej AJAX |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. Są to wiadomości, które np. informują o wyniku operacji. Ważną cechą wiadomości flash jest to, że są dostępne w szablonie nawet po przekierowaniu. Nawet po wyświetleniu pozostają aktywne przez kolejne 30 sekund – na przykład na wypadek, gdyby z powodu błędnego transferu użytkownik odświeżył stronę - wiadomość mu więc od razu nie zniknie. +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łanie zapewnia metoda [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Pierwszym parametrem jest tekst wiadomości lub obiekt `stdClass` reprezentujący wiadomość. Opcjonalnym drugim parametrem jest jej typ (error, warning, info itp.). Metoda `flashMessage()` zwraca instancję wiadomości flash jako obiekt `stdClass`, do którego można dodawać kolejne informacje. +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('Pozycja została usunięta.'); +$this->flashMessage('Element został usunięty.'); $this->redirect(/* ... */); // i przekierowujemy ``` -W szablonie te wiadomości są dostępne w zmiennej `$flashes` jako obiekty `stdClass`, które zawierają właściwości `message` (tekst wiadomości), `type` (typ wiadomości) i mogą zawierać już wspomniane informacje użytkownika. Wyrenderujemy je na przykład tak: +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} @@ -230,39 +235,39 @@ W szablonie te wiadomości są dostępne w zmiennej `$flashes` jako obiekty `std ``` -Przekierowanie po sygnale -========================= +Przekierowanie po przetworzeniu sygnału +======================================= -Po przetworzeniu sygnału komponentu często następuje przekierowanie. Jest to podobna sytuacja jak w przypadku formularzy - po ich wysłaniu również przekierowujemy, aby przy odświeżeniu strony w przeglądarce nie doszło do ponownego wysłania danych. +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 aktualny presenter i akcję +$this->redirect('this'); // przekierowuje na bieżący presenter i akcję ``` -Ponieważ komponent jest elementem wielokrotnego użytku i zazwyczaj nie powinien mieć bezpośredniego powiązania z konkretnymi presenterami, metody `redirect()` i `link()` automatycznie interpretują parametr jako sygnał komponentu: +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 +$this->redirect('click'); // przekierowuje na sygnał 'click' tego samego komponentu ``` -Jeśli potrzebujesz przekierować na inny presenter lub akcję, możesz to zrobić za pośrednictwem presentera: +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 persistentne -====================== +Parametry trwałe +================ -Parametry persistentne służą do utrzymywania stanu w komponentach między różnymi żądaniami. Ich wartość pozostaje taka sama nawet po kliknięciu na link. W przeciwieństwie do danych w sesji, są one przesyłane w URL. I to całkowicie automatycznie, w tym w linkach tworzonych w innych komponentach na tej samej stronie. +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 np. komponent do paginacji treści. Takich komponentów może być na stronie kilka. I chcemy, aby po kliknięciu na link wszystkie komponenty pozostały na swojej aktualnej stronie. Dlatego z numeru strony (`page`) zrobimy parametr persistentny. +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. -Tworzenie parametru persistentnego w Nette jest niezwykle proste. Wystarczy utworzyć publiczną właściwość i oznaczyć ją atrybutem: (wcześniej używano `/** @persistent */`) +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; // ta linia jest ważna +use Nette\Application\Attributes\Persistent; // ten wiersz jest ważny class PaginatingControl extends Control { @@ -271,43 +276,43 @@ class PaginatingControl extends Control } ``` -Przy właściwości zalecamy podanie również typu danych (np. `int`) i można podać również wartość domyślną. Wartości parametrów można [walidować |#Walidacja parametrów persistentnych]. +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]. -Podczas tworzenia linku można zmienić wartość parametru persistentnego: +Przy tworzeniu odnośnika wartość parametru trwałego można zmienić: ```latte -następna +dalej ``` -Lub można go *zresetować*, tj. usunąć z URL. Wtedy przyjmie swoją wartość domyślną: +Albo *zresetować*, czyli usunąć z URL. Przyjmie wtedy swoją wartość domyślną: ```latte resetuj ``` -Komponenty persistentne -======================= +Komponenty trwałe +================= -Nie tylko parametry, ale także komponenty mogą być persistentne. W przypadku takiego komponentu jego parametry persistentne są przenoszone również między różnymi akcjami presentera lub między wieloma presenterami. Komponenty persistentne oznaczamy adnotacją przy klasie presentera. Na przykład tak oznaczymy komponenty `calendar` i `poll`: +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 -/** - * @persistent(calendar, poll) - */ +use Nette\Application\Attributes\Persistent; + +#[Persistent('calendar', 'poll')] class DefaultPresenter extends Nette\Application\UI\Presenter { } ``` -Podkomponentów wewnątrz tych komponentów nie trzeba oznaczać, staną się również persistentne. +Subkomponentów wewnątrz tych komponentów nie trzeba oznaczać; również stają się trwałe. -W PHP 8 można również użyć atrybutów do oznaczenia komponentów persistentnych: +Starsza adnotacja `@persistent` nadal działa, ale jest przestarzała i wywołuje ostrzeżenie: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -317,15 +322,15 @@ class DefaultPresenter extends Nette\Application\UI\Presenter Komponenty z zależnościami ========================== -Jak tworzyć komponenty z zależnościami, nie „zaśmiecając” sobie presenterów, które będą ich używać? Dzięki sprytnym właściwościom kontenera DI w Nette, podobnie jak przy używaniu klasycznych usług, można pozostawić większość pracy frameworkowi. +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 jako przykład komponent, który ma zależność od usługi `PollFacade`: +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 int $id, // ID ankiety, dla której tworzymy komponent private PollFacade $facade, ) { } @@ -338,11 +343,11 @@ class PollControl extends Control } ``` -Gdybyśmy pisali klasyczną usługę, nie byłoby problemu. O przekazanie wszystkich zależności zadbałby niewidocznie kontener DI. Jednak z komponentami zazwyczaj postępujemy tak, że ich nową instancję tworzymy bezpośrednio w presenterze w [metodach fabrykujących |#Metody fabrykujące] `createComponent…()`. Ale przekazywanie wszystkich zależności wszystkich komponentów do presentera, aby je następnie przekazać komponentom, jest uciążliwe. I tyle napisanego kodu… +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... -Logicznym pytaniem jest, dlaczego po prostu nie zarejestrujemy komponentu jako klasycznej usługi, nie przekażemy go do presentera, a następnie w metodzie `createComponent…()` nie zwrócimy? Takie podejście jest jednak nieodpowiednie, ponieważ chcemy mieć możliwość tworzenia komponentu nawet wielokrotnie. +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. -Prawidłowym rozwiązaniem jest napisanie dla komponentu fabryki, czyli klasy, która nam komponent utworzy: +Poprawnym rozwiązaniem jest napisanie fabryki komponentu, czyli klasy, która tworzy komponent za nas: ```php class PollControlFactory @@ -359,14 +364,14 @@ class PollControlFactory } ``` -Taką fabrykę zarejestrujemy w naszym kontenerze w konfiguracji: +Fabrykę tę rejestrujemy w kontenerze w konfiguracji: ```neon services: - PollControlFactory ``` -a na koniec użyjemy jej w naszym presenterze: +i wreszcie używamy jej w naszym presenterze: ```php class PollPresenter extends Nette\Application\UI\Presenter @@ -378,13 +383,13 @@ class PollPresenter extends Nette\Application\UI\Presenter protected function createComponentPollControl(): PollControl { - $pollId = 1; // możemy przekazać nasz parametr + $pollId = 1; // możemy przekazać własny parametr return $this->pollControlFactory->create($pollId); } } ``` -Świetne jest to, że Nette DI potrafi takie proste fabryki [generować |dependency-injection:factory], więc zamiast całego jej kodu wystarczy napisać tylko jej interfejs: +Ś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 @@ -393,21 +398,21 @@ interface PollControlFactory } ``` -I to wszystko. Nette wewnętrznie zaimplementuje ten interfejs i przekaże go do presentera, gdzie już możemy go używać. Magicznie doda nam do naszego komponentu również parametr `$id` i instancję klasy `PollFacade`. +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 dogłębnie -==================== +Komponenty w głąb +================= -Komponenty w Nette Application stanowią części aplikacji internetowej wielokrotnego użytku, które wstawiamy na strony i którym poświęcony jest cały ten rozdział. Jakie dokładnie możliwości ma taki komponent? +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) jest renderowalny w szablonie -2) wie, [którą swoją część |ajax#Snippety] ma wyrenderować przy żądaniu AJAX (snippety) -3) ma możliwość zapisywania swojego stanu w URL (parametry persistentne) -4) ma możliwość reagowania na akcje użytkownika (sygnały) -5) tworzy strukturę hierarchiczną (gdzie korzeniem jest presenter) +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) -Każdą z tych funkcji obsługuje któraś z klas linii dziedziczenia. Renderowanie (1 + 2) obsługuje [api:Nette\Application\UI\Control], włączenie do [cyklu życia |presenters#Cykl życia presentera] (3, 4) klasa [api:Nette\Application\UI\Component], a tworzenie struktury hierarchicznej (5) klasy [Container i Component |component-model:]. +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 } @@ -428,12 +433,12 @@ Cykl życia komponentu [* lifecycle-component.svg *] *** *Cykl życia komponentu* .<> -Walidacja parametrów persistentnych ------------------------------------ +Walidacja parametrów trwałych +----------------------------- -Wartości [parametrów persistentnych |#Parametry persistentne] otrzymanych z URL zapisuje do właściwości metoda `loadState()`. Sprawdza ona również, czy odpowiada typ danych podany przy właściwości, w przeciwnym razie odpowiada błędem 404 i strona się nie wyświetla. +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 ślepo nie wierz parametrom persistentnym, ponieważ mogą być łatwo nadpisane przez użytkownika w URL. W ten sposób na przykład sprawdzimy, czy numer strony `$this->page` jest większy niż 0. Odpowiednią drogą jest nadpisanie wspomnianej metody `loadState()`: +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 @@ -443,8 +448,8 @@ class PaginatingControl extends Control public function loadState(array $params): void { - parent::loadState($params); // tutaj ustawia się $this->page - // następuje własna kontrola wartości: + parent::loadState($params); // tutaj ustawiane jest $this->page + // następuje własne sprawdzenie wartości: if ($this->page < 1) { $this->error(); } @@ -452,27 +457,41 @@ class PaginatingControl extends Control } ``` -Proces odwrotny, czyli zebranie wartości z właściwości persistentnych, obsługuje metoda `saveState()`. +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ły dogłębnie ------------------ +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. -Sygnał powoduje ponowne załadowanie strony dokładnie tak samo, jak przy pierwotnym żądaniu (z wyjątkiem przypadku, gdy jest wywoływany 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{signal}`. Dalsze przetwarzanie zależy od danego obiektu. Obiekty dziedziczące po `Component` (tj. `Control` i `Presenter`) reagują tak, że próbują wywołać metodę `handle{signal}` 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]. -Innymi słowy: bierze się definicję funkcji `handle{signal}` i wszystkie parametry, które przyszły z żądaniem, a do argumentów według nazwy dopasowuje się parametry z URL i próbuje wywołać daną metodę. Np. jako parametr `$id` przekazuje się wartość z parametru `id` w URL, jako `$something` przekazuje się `something` z URL, itd. A jeśli metoda nie istnieje, metoda `signalReceived` rzuca [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 odbierać dowolny komponent, presenter lub obiekt, który implementuje interfejs `SignalReceiver` i jest podłączony do drzewa komponentó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 ma coś zrobić – ankieta ma zliczyć głos od użytkownika, blok z nowościami ma się rozwinąć i wyświetlić dwa razy więcej nowości, formularz został wysłany i ma przetworzyć dane itp. +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 dla sygnału tworzymy za pomocą metody [Component::link() |api:Nette\Application\UI\Component::link()]. Jako parametr `$destination` przekazujemy ciąg `{signal}!` a jako `$args` tablicę argumentów, które chcemy przekazać sygnałowi. Sygnał zawsze jest wywoływany na aktualnym presenterze i akcji z aktualnymi parametrami, parametry sygnału są tylko dodawane. Dodatkowo na początku dodawany jest **parametr `?do`, który określa sygnał**. +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 `{signal}`, albo `{signalReceiver}-{signal}`. `{signalReceiver}` to nazwa komponentu w presenterze. Dlatego w nazwie komponentu nie może być myślnika – używa się go do oddzielenia nazwy komponentu i sygnału, jednak można w ten sposób zagnieździć kilka komponentów. +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żemy pominąć – wtedy sprawdza, czy komponent jest odbiorcą jakiegokolwiek sygnału. Jako drugi parametr można podać `true` i tym samym sprawdzić, czy odbiorcą jest nie tylko podany komponent, ale także którykolwiek jego potomek. +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. -W dowolnej fazie poprzedzającej `handle{signal}` 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, który został określony jako odbiorca sygnału (jeśli nie jest określony odbiorca sygnału, jest to sam presenter) i wysyła mu sygnał. +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: @@ -482,4 +501,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -Tym samym sygnał jest wykonany przedwcześnie i nie będzie już ponownie wywoływany. +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 index 01f37a6477..0281e17a4b 100644 --- a/application/pl/configuration.texy +++ b/application/pl/configuration.texy @@ -2,7 +2,7 @@ Konfiguracja aplikacji ********************** .[perex] -Przegląd opcji konfiguracyjnych dla aplikacji Nette. +Przegląd opcji konfiguracyjnych Nette Application. Application @@ -10,39 +10,41 @@ Application ```neon application: - # wyświetlić panel "Nette Application" w Tracy BlueScreen? - debugger: ... # (bool) domyślnie true + # pokazywać panel "Nette Application" w Tracy BlueScreen? + debugger: ... # (bool) włączone, jeśli Tracy jest dostępne - # czy przy błędzie będzie wywoływany error-presenter? - # ma efekt tylko w trybie deweloperskim - catchExceptions: ... # (bool) domyślnie true + # 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 dla presenterów i akcji + # definiuje aliasy presenterów i akcji aliases: ... # definiuje reguły tłumaczenia nazwy presentera na klasę mapping: ... - # nieprawidłowe linki nie generują ostrzeżeń? - # ma efekt tylko w trybie deweloperskim + # wyciszać ostrzeżenia o nieprawidłowych odnośnikach? + # działa tylko w trybie deweloperskim silentLinks: ... # (bool) domyślnie false ``` -Od wersji `nette/application` 3.2 można zdefiniować parę error-presenterów: +Od wersji 3.2 pakietu `nette/application` można zdefiniować parę error-presenterów: ```neon application: errorPresenter: - 4xx: Error4xx # dla wyjątku Nette\Application\BadRequestException + 4xx: Error4xx # dla Nette\Application\BadRequestException 5xx: Error5xx # dla pozostałych wyjątków ``` -Opcja `silentLinks` określa, jak Nette zachowa się w trybie deweloperskim, gdy generowanie linku nie powiodło się (np. dlatego, że presenter nie istnieje itp.). Domyślna wartość `false` oznacza, że Nette zgłosi błąd `E_USER_WARNING`. Ustawienie na `true` spowoduje stłumienie tego komunikatu błędu. W środowisku produkcyjnym `E_USER_WARNING` jest zawsze zgłaszany. To zachowanie możemy również kontrolować, ustawiając zmienną presentera [$invalidLinkMode |creating-links#Nieprawidłowe linki]. +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. -[Aliasy upraszczają linkowanie |creating-links#Aliasy] do często używanych presenterów. +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. @@ -50,14 +52,14 @@ Opcja `silentLinks` określa, jak Nette zachowa się w trybie deweloperskim, gdy Automatyczna rejestracja presenterów ------------------------------------ -Nette automatycznie dodaje presentery jako usługi do kontenera DI, co znacząco przyspiesza ich tworzenie. Sposób, w jaki Nette wyszukuje presentery, można skonfigurować: +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 Composer class map? + # szukać presenterów w class mapie Composera? scanComposer: ... # (bool) domyślnie true - # maska, której musi odpowiadać nazwa klasy i pliku + # maska, do której musi pasować nazwa klasy i pliku scanFilter: ... # (string) domyślnie '*Presenter' # w których katalogach szukać presenterów? @@ -65,7 +67,7 @@ application: - %vendorDir%/mymodule ``` -Katalogi podane w `scanDirs` nie nadpisują wartości domyślnej `%appDir%`, ale uzupełniają ją, więc `scanDirs` będzie zawierać obie ścieżki `%appDir%` i `%vendorDir%/mymodule`. Jeśli chcielibyśmy pominąć katalog domyślny, użyjemy [wykrzyknika |dependency-injection:configuration#Łączenie], który nadpisze wartość: +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: @@ -73,26 +75,32 @@ application: - %vendorDir%/mymodule ``` -Skanowanie katalogów można wyłączyć, podając wartość `false`. Nie zalecamy całkowitego wyłączania automatycznego dodawania presenterów, ponieważ w przeciwnym razie dojdzie do obniżenia wydajności aplikacji. +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 ============== -Tym ustawieniem można globalnie wpłynąć na zachowanie Latte w komponentach i presenterach. +To ustawienie globalnie wpływa na zachowanie Latte w komponentach i presenterach. ```neon latte: - # wyświetlić panel Latte w Tracy Bar dla głównego szablonu (true) lub wszystkich komponentów (all)? - debugger: ... # (true|false|'all') domyślnie true + # 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) - # generuje szablony z nagłówkiem declare(strict_types=1) + # generować szablony z nagłówkiem declare(strict_types=1) strictTypes: ... # (bool) domyślnie false - # włącza tryb [ścisłego parsera |latte:develop#striktní režim] + # włącza [tryb ścisłego parsera |latte:develop#strict mode] strictParsing: ... # (bool) domyślnie false - # aktywuje [kontrolę wygenerowanego kodu |latte:develop#Kontrola vygenerovaného kódu] + # 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 @@ -102,7 +110,7 @@ latte: templateClass: App\MyTemplateClass # domyślnie Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Jeśli używasz Latte w wersji 3, możesz dodawać nowe [rozszerzenia |latte:extending-latte#Latte Extension] za pomocą: +Nowe [rozszerzenia |latte:extending-latte#Latte Extension] dodasz tak: ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Jeśli używasz Latte w wersji 2, możesz rejestrować nowe tagi, podając nazwę klasy lub referencję do usługi. Domyślnie wywoływana jest metoda `install()`, ale można to zmienić, podając nazwę innej metody: - -```neon -latte: - # rejestracja niestandardowych znaczników Latte - macros: - - App\MyLatteMacros::register # metoda statyczna, nazwa klasy lub callable - - @App\MyLatteMacrosFactory # usługa z metodą install() - - @App\MyLatteMacrosFactory::register # usługa z metodą register() - -services: - - App\MyLatteMacrosFactory -``` - Routing ======= @@ -132,14 +126,14 @@ Podstawowe ustawienia: ```neon routing: - # wyświetlić panel routingu w Tracy Bar? - debugger: ... # (bool) domyślnie true + # pokazywać panel routingu w pasku Tracy? + debugger: ... # (bool) włączone, jeśli Tracy jest dostępne (tylko w trybie debug) - # serializuje router do kontenera DI + # serializować router do kontenera DI cache: ... # (bool) domyślnie false ``` -Routing zazwyczaj definiujemy 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 oferuje tak szerokiej zmienności w ustawieniach: +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: @@ -159,16 +153,16 @@ constants: Foobar: 'baz' ``` -Po uruchomieniu aplikacji zostanie utworzona stała `Foobar`. +Stała `Foobar` zostanie utworzona po starcie aplikacji. .[note] -Stałe nie powinny służyć jako swego rodzaju globalnie dostępne zmienne. Do przekazywania wartości do obiektów wykorzystaj [dependency injection |dependency-injection:passing-dependencies]. +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 === -Ustawienia dyrektyw PHP. Przegląd wszystkich dyrektyw znajdziesz na [php.net |https://www.php.net/manual/en/ini.list.php]. +Ustawianie dyrektyw PHP. Przegląd wszystkich dyrektyw znajdziesz na [php.net |https://www.php.net/manual/en/ini.list.php]. ```neon php: @@ -179,13 +173,14 @@ php: Usługi DI ========= -Te usługi są dodawane do kontenera DI: - -| Nazwa | Typ | Opis -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [uruchamiacz całej aplikacji |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | fabryka presenterów -| `application.###` | [api:Nette\Application\UI\Presenter] | poszczególne presentery -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | fabryka obiektu `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | fabryka dla [`$this->template` |templates] +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 index 40b1445c2c..b8abbf5d05 100644 --- a/application/pl/creating-links.texy +++ b/application/pl/creating-links.texy @@ -1,156 +1,173 @@ -Tworzenie linków URL -******************** +Tworzenie odnośników URL +************************
    -Tworzenie linków w Nette jest proste jak wskazywanie palcem. Wystarczy tylko wycelować, a framework już za Ciebie wykona całą pracę. Pokażemy: +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ć linki w szablonach i gdzie indziej -- jak odróżnić link do aktualnej strony -- co z nieprawidłowymi linkami +- 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ł w szablonach czy kodzie zapisywać na sztywno adresów URL Twojej aplikacji, które mogą się później zmienić, lub skomplikowanie je składać. W linku wystarczy podać presenter i akcję, przekazać ewentualne parametry, a framework już sam wygeneruje URL. Właściwie jest to bardzo podobne do wywoływania funkcji. Spodoba Ci się to. +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 linki w szablonach, a świetnym pomocnikiem jest atrybut `n:href`: +Najczęściej tworzymy odnośniki w szablonach i świetnym pomocnikiem jest atrybut `n:href`: ```latte szczegóły ``` -Zauważ, że zamiast atrybutu HTML `href` użyliśmy [n:atrybutu |latte:syntax#n:atrybuty] `n:href`. Jego wartością nie jest URL, jak by to było w przypadku atrybutu `href`, ale nazwa presentera i akcji. +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 na link jest, upraszczając, czymś w rodzaju wywołania metody `ProductPresenter::renderShow()`. A jeśli ma w swojej sygnaturze parametry, możemy ją wywołać z argumentami: +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żliwe jest również przekazywanie parametrów nazwanych. Poniższy link przekazuje parametr `lang` o wartości `cs`: +Można też przekazywać parametry nazwane. Poniższy odnośnik przekazuje parametr `lang` o wartości `en`: ```latte -szczegóły produktu +szczegóły produktu ``` -Jeśli metoda `ProductPresenter::renderShow()` nie ma `$lang` w swojej sygnaturze, może uzyskać wartość parametru za pomocą `$lang = $this->getParameter('lang')` lub z [właściwości |presenters#Parametry żądania]. +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ą przechowywane w tablicy, można je rozwinąć operatorem `...` (w Latte 2.x operatorem `(expand)`): +Jeśli parametry są w tablicy, można je rozwinąć operatorem `...`: ```latte -{var $args = [$product->id, lang => cs]} -szczegóły produktu +{var $args = [$product->id, lang => en]} +szczegóły produktu ``` -W linkach automatycznie przekazywane są również tzw. [parametry persistentne |presenters#Parametry trwałe]. +W odnośnikach automatycznie przekazywane są również tak zwane [parametry trwałe |presenters#Parametry trwałe]. -Atrybut `n:href` jest bardzo przydatny dla znaczników HTML ``. Jeśli chcemy wypisać link gdzie indziej, na przykład w tekście, użyjemy `{link}`: +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 -Adres to: {link Home:default} +URL to: {link Home:default} ``` W kodzie ======== -Do tworzenia linku w presenterze służy metoda `link()`: +Do utworzenia odnośnika w presenterze służy metoda `link()`: ```php $url = $this->link('Product:show', $product->id); ``` -Parametry można przekazać również za pomocą tablicy, gdzie można podać również parametry nazwane: +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' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -Linki można tworzyć również bez presentera, do tego służy [#LinkGenerator] i jego metoda `link()`. +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: -Linki do presentera -=================== +```php +$link = $this->lazyLink('Product:show', $id); +// ... +echo $link; // URL generowany jest dopiero tutaj +``` -Jeśli celem linku jest presenter i akcja, ma on następującą składnię: + +Odnośniki do presentera +======================= + +Jeśli celem odnośnika jest presenter i akcja, ma on taką składnię: ``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] +[//] [[[[:]moduł:]presenter:]akcja | this] [#fragment] ``` -Format ten obsługują wszystkie znaczniki Latte i wszystkie metody presentera, które pracują z linkami, czyli `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` oraz [#LinkGenerator]. Więc nawet jeśli w przykładach użyto `n:href`, mogłaby tam być dowolna z tych funkcji. +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ą formą jest więc `Presenter:action`: +Podstawową postacią jest więc `Presenter:akcja`: ```latte strona główna ``` -Jeśli linkujemy do akcji aktualnego presentera, możemy pominąć jego nazwę: +Jeśli linkujemy do akcji bieżącego presentera, możemy pominąć jego nazwę: ```latte strona główna ``` -Jeśli celem jest akcja `default`, możemy ją pominąć, ale dwukropek musi pozostać: +Jeśli akcją docelową jest `default`, możemy ją pominąć, ale dwukropek musi zostać: ```latte strona główna ``` -Linki mogą również prowadzić do innych [modułów |directory-structure#Presentery i szablony]. Tutaj linki dzielą się na względne do zagnieżdżonego podmodułu lub absolutne. Zasada jest analogiczna do ścieżek na dysku, tylko zamiast ukośników są dwukropki. Załóżmy, że aktualny presenter jest częścią modułu `Front`, wtedy zapiszemy: +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 -link do Front:Shop:Product:show -link do Admin:Product:show +odnośnik do Front:Shop:Product:show +odnośnik do Admin:Product:show ``` -Specjalnym przypadkiem jest link [do siebie samego |#Link do aktualnej strony], gdy jako cel podamy `this`. +Szczególnym przypadkiem jest odnośnik [do samego siebie |#Odnośnik do bieżącej strony], gdzie jako cel podajemy `this`. ```latte odśwież ``` -Możemy linkować do określonej części strony za pomocą tzw. fragmentu za znakiem kratki `#`: +Do konkretnej części strony możemy linkować przez tak zwany fragment po znaku kratki `#`: ```latte -link do Home:default i fragmentu #main +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 +=================== -Ścieżki absolutne -================= +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`. -Linki generowane za pomocą `link()` lub `n:href` są zawsze ścieżkami absolutnymi (tj. zaczynają się znakiem `/`), ale nie są absolutnymi 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`. -Aby wygenerować absolutny URL, dodaj na początku dwie ukośniki (np. `n:href="//Home:"`). Lub można przełączyć presenter, aby generował tylko linki absolutne, ustawiając `$this->absoluteUrls = true`. +Do zamiany ścieżki względnej na bezwzględną można też użyć w szablonie filtra `|absoluteUrl`. -Link do aktualnej strony -======================== +Odnośnik do bieżącej strony +=========================== -Cel `this` tworzy link do aktualnej strony: +Cel `this` tworzy odnośnik do bieżącej strony: ```latte odśwież ``` -Jednocześnie przekazywane są wszystkie parametry podane w sygnaturze metody `action()` lub `render()`, jeśli `action()` nie jest zdefiniowana. Więc jeśli jesteśmy na stronie `Product:show` i `id: 123`, link do `this` przekaże również ten parametr. +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 można parametry specyfikować bezpośrednio: +Oczywiście parametry można podać bezpośrednio: ```latte odśwież ``` -Funkcja `isLinkCurrent()` sprawdza, czy cel linku jest zgodny z aktualną stroną. Można tego użyć na przykład w szablonie do odróżnienia linków itp. +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 w metodzie `link()`, dodatkowo jednak można zamiast konkretnej akcji podać symbol wieloznaczny `*`, który oznacza dowolną akcję danego presentera. +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')} @@ -162,15 +179,15 @@ Parametry są takie same jak w metodzie `link()`, dodatkowo jednak można zamias
  • ``` -W połączeniu z `n:href` w jednym elemencie można użyć skróconej formy: +W połączeniu z `n:href` w jednym elemencie można użyć postaci skróconej: ```latte ... ``` -Symbol wieloznaczny `*` można użyć tylko zamiast akcji, a nie presentera. +Symbolu wieloznacznego `*` można użyć tylko zamiast akcji, nie presentera. -Aby sprawdzić, czy jesteśmy w określonym module lub jego podmodule, użyjemy metody `isModuleCurrent(moduleName)`. +Aby ustalić, czy jesteśmy w konkretnym module albo jego submodule, użyj metody `isModuleCurrent(moduleName)`. ```latte
  • @@ -179,13 +196,29 @@ Aby sprawdzić, czy jesteśmy w określonym module lub jego podmodule, użyjemy ``` -Linki do sygnału -================ +Zmiana bazy odnośników .{data-version:3.2.7} +============================================ -Celem linku nie musi być tylko presenter i akcja, ale także [sygnał |components#Sygnał] (wywołują metodę `handle()`). Wtedy składnia jest następująca: +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: ``` -[//] [sub-component:]signal! [#fragment] +[//] [subkomponent:]sygnał! [#fragment] ``` Sygnał odróżnia więc wykrzyknik: @@ -194,39 +227,39 @@ Sygnał odróżnia więc wykrzyknik: sygnał ``` -Można również utworzyć link do sygnału podkomponentu (lub pod-podkomponentu): +Możesz też utworzyć odnośnik do sygnału subkomponentu (albo sub-subkomponentu): ```latte sygnał ``` -Linki w komponencie -=================== +Odnośniki w komponencie +======================= -Ponieważ [komponenty|components] są samodzielnymi jednostkami wielokrotnego użytku, które nie powinny mieć żadnych powiązań z otaczającymi presenterami, linki działają tu trochę inaczej. Atrybut Latte `n:href` i znacznik `{link}` oraz metody komponentu takie jak `link()` i inne uważają cel linku **zawsze za nazwę sygnału**. Dlatego nie jest nawet konieczne podawanie wykrzyknika: +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ł, a nie akcja +sygnał, nie akcja ``` -Jeśli chcielibyśmy w szablonie komponentu linkować do presenterów, użyjemy do tego znacznika `{plink}`: +Gdybyśmy chcieli w szablonie komponentu linkować do presenterów, użyjemy tagu `{plink}`: ```latte -główna +strona główna ``` -lub w kodzie +albo w kodzie ```php $this->getPresenter()->link('Home:default') ``` -Aliasy .{data-version:v3.2.2} -============================= +Aliasy .{data-version:3.2.3} +============================ -Czasami może się przydać przypisanie parze Presenter:akcja łatwo zapamiętywalnego aliasu. Na przykład stronę główną `Front:Home:default` nazwać po prostu `home` lub `Admin:Dashboard:default` jako `admin`. +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`: @@ -238,26 +271,26 @@ application: sign: Front:Sign:in ``` -W linkach zapisuje się je za pomocą znaku @, na przykład: +W odnośnikach zapisuje się je potem za pomocą małpy, na przykład: ```latte administracja ``` -Są one obsługiwane również we wszystkich metodach pracujących z linkami, takich jak `redirect()` i podobnych. +Obsługiwane są też we wszystkich metodach pracujących z odnośnikami, takich jak `redirect()` i podobne. -Nieprawidłowe linki -=================== +Nieprawidłowe odnośniki +======================= -Może się zdarzyć, że utworzymy nieprawidłowy link - albo dlatego, że prowadzi do nieistniejącego presentera, albo dlatego, że przekazuje więcej parametrów, niż akceptuje metoda docelowa w swojej sygnaturze, albo gdy dla akcji docelowej nie można wygenerować URL. Sposób postępowania z nieprawidłowymi linkami określa zmienna statyczna `Presenter::$invalidLinkMode`. Może ona przyjmować kombinację tych wartości (stałych): +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 zwracany jest znak # -- `Presenter::InvalidLinkWarning` - zgłaszane jest ostrzeżenie E_USER_WARNING, które w trybie produkcyjnym zostanie zalogowane, ale nie spowoduje przerwania działania skryptu -- `Presenter::InvalidLinkTextual` - ostrzeżenie wizualne, wypisuje błąd bezpośrednio w linku -- `Presenter::InvalidLinkException` - rzucany jest wyjątek InvalidLinkException +- `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 -Domyślne ustawienie to `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 zostanie przechwycone przez [Tracy |tracy:] i wyświetli bluescreen. `InvalidLinkTextual` działa tak, że jako URL zwraca komunikat błędu zaczynający się od znaków `#error:`. Aby takie linki były od razu widoczne, dodajmy do CSS: +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:"] { @@ -266,7 +299,7 @@ a[href^="#error:"] { } ``` -Jeśli nie chcemy, aby w środowisku deweloperskim generowane były ostrzeżenia, możemy ustawić tryb cichy bezpośrednio w [konfiguracji|configuration]. +Jeśli nie chcemy, aby w środowisku deweloperskim powstawały ostrzeżenia, możemy wyciszyć je bezpośrednio w [konfiguracji|configuration]. ```neon application: @@ -277,10 +310,10 @@ application: LinkGenerator ============= -Jak tworzyć linki z podobnym komfortem jak metoda `link()`, ale bez obecności presentera? Do tego służy [api:Nette\Application\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żna sobie przekazać przez konstruktor, a następnie tworzyć linki za pomocą jej metody `link()`. +LinkGenerator to usługa, którą możesz otrzymać przez konstruktor, a następnie tworzyć odnośniki jej metodą `link()`. -W porównaniu do presenterów jest tu różnica. LinkGenerator tworzy wszystkie linki od razu jako absolutne URL. Ponadto nie istnieje żaden "aktualny presenter", więc nie można jako celu podać tylko nazwy akcji `link('default')` ani podawać ścieżek względnych do modułów. +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 linki zawsze rzucają `Nette\Application\UI\InvalidLinkException`. +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 index f3a84e2f67..4df3dbebdd 100644 --- a/application/pl/directory-structure.texy +++ b/application/pl/directory-structure.texy @@ -3,43 +3,43 @@ Struktura katalogów aplikacji
    -Jak zaprojektować przejrzystą i skalowalną strukturę katalogów dla projektów w Nette Framework? Pokażemy sprawdzone praktyki, które pomogą w organizacji kodu. Dowiesz się: +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ę tak, aby **dobrze skalowała się** wraz ze wzrostem projektu -- jakie są **możliwe alternatywy** i ich zalety czy wady +- jak zaprojektować strukturę, aby **dobrze się skalowała** wraz z rozwojem projektu +- jakie są **możliwe alternatywy** oraz ich zalety i wady
    -Ważne jest, aby wspomnieć, że sam Nette Framework nie narzuca żadnej konkretnej struktury. Jest zaprojektowany tak, aby można go było łatwo dostosować do wszelkich potrzeb i preferencji. +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 ============================= -Chociaż Nette Framework nie dyktuje żadnej sztywnej struktury katalogów, istnieje sprawdzony domyślny układ w postaci [Web Project|https://github.com/nette/web-project]: +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 z aplikacją +├── app/ ← katalog aplikacji ├── assets/ ← pliki SCSS, JS, obrazy..., alternatywnie resources/ -├── bin/ ← skrypty dla wiersza poleceń +├── bin/ ← skrypty do wiersza poleceń ├── config/ ← konfiguracja ├── log/ ← zalogowane błędy ├── temp/ ← pliki tymczasowe, cache ├── tests/ ← testy -├── vendor/ ← biblioteki zainstalowane przez Composer +├── vendor/ ← biblioteki zainstalowane przez Composera └── www/ ← katalog publiczny (document-root) \-- -Tę strukturę można dowolnie modyfikować zgodnie z własnymi potrzebami - zmieniać nazwy lub przenosić foldery. Następnie wystarczy tylko zmodyfikować ścieżki względne do katalogów w pliku `Bootstrap.php` i ewentualnie `composer.json`. Nic więcej nie jest potrzebne, żadna skomplikowana rekonfiguracja, żadne zmiany stałych. Nette dysponuje inteligentną autodetekcją i automatycznie rozpozna lokalizację aplikacji, w tym jej bazę URL. +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 ======================= -Kiedy po raz pierwszy eksplorujesz nowy projekt, powinieneś szybko się w nim zorientować. Wyobraź sobie, że rozwijasz katalog `app/Model/` i widzisz taką strukturę: +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/ @@ -48,7 +48,7 @@ Kiedy po raz pierwszy eksplorujesz nowy projekt, powinieneś szybko się w nim z └── Entities/ \-- -Z niej wyczytasz tylko to, że projekt używa jakichś usług, repozytoriów i encji. O rzeczywistym celu aplikacji nie dowiesz się absolutnie nic. +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**: @@ -60,42 +60,42 @@ Spójrzmy na inne podejście - **organizację według domen**: └── Product/ \-- -Tutaj jest inaczej - na pierwszy rzut oka widać, że chodzi o e-sklep. Już same nazwy katalogów zdradzają, co aplikacja potrafi - pracuje z płatnościami, zamówieniami i produktami. +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 szereg problemów: kod, który jest ze sobą logicznie powiązany, jest rozproszony w różnych folderach i trzeba między nimi przeskakiwać. Dlatego będziemy organizować według domen. +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 ----------------- -Jest zwyczajem, że struktura katalogów odpowiada przestrzeniom nazw w aplikacji. Oznacza to, że fizyczna lokalizacja plików odpowiada ich namespace. Na przykład klasa umieszczona w `app/Model/Product/ProductRepository.php` powinna mieć namespace `App\Model\Product`. Ta zasada pomaga w orientacji w kodzie i upraszcza autoloading. +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 vs mnoga w nazwach ------------------------------------- +Liczba pojedyncza kontra mnoga w nazwach +---------------------------------------- -Zauważ, że w głównych katalogach aplikacji używamy liczby pojedynczej: `app`, `config`, `log`, `temp`, `www`. Podobnie wewnątrz aplikacji: `Model`, `Core`, `Presentation`. Dzieje się tak dlatego, że każdy z nich reprezentuje jeden spójny koncept. +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 np. `app/Model/Product` reprezentuje wszystko związane z produktami. Nie nazwiemy tego `Products`, ponieważ nie jest to folder pełen produktów (byłyby tam pliki `nokia.php`, `samsung.php`). Jest to namespace zawierający klasy do pracy z produktami - `ProductRepository.php`, `ProductService.php`. +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, ponieważ zawiera zestaw samodzielnych skryptów wykonywalnych - `CleanupTask.php`, `ImportTask.php`. Każdy z nich jest samodzielną jednostką. +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żywanie: -- Liczby pojedynczej dla namespace reprezentującego funkcjonalną całość (nawet jeśli pracuje z wieloma encjami) -- Liczby mnogiej dla kolekcji samodzielnych jednostek -- W przypadku niepewności lub jeśli nie chcesz się nad tym zastanawiać, wybierz liczbę pojedynczą +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 jest jedynym dostępnym z sieci (tzw. document-root). Często można spotkać się również z nazwą `public/` zamiast `www/` - jest to tylko kwestia konwencji i nie ma wpływu na funkcjonalność. Katalog zawiera: -- [Punkt wejściowy |bootstrapping#index.php] aplikacji `index.php` -- Plik `.htaccess` z regułami dla mod_rewrite (w Apache) -- Pliki statyczne (CSS, JavaScript, obrazy) -- Przesłane pliki +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 prawidłowego zabezpieczenia aplikacji kluczowe jest posiadanie poprawnie [skonfigurowanego document-root |nette:troubleshooting#Jak zmienić lub usunąć katalog www z adresu URL]. +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. @@ -104,32 +104,32 @@ Nigdy nie umieszczaj w tym katalogu folderu `node_modules/` - zawiera tysiące p Katalog aplikacji `app/` ======================== -To jest główny katalog z kodem aplikacji. Podstawowa struktura: +To główny katalog zawierający kod aplikacji. Podstawowa struktura: /--pre app/ -├── Core/ ← kwestie infrastrukturalne +├── 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, ładuje konfigurację i tworzy kontener DI. +`Bootstrap.php` to [klasa startowa aplikacji|bootstrapping], która inicjalizuje środowisko, wczytuje konfigurację i tworzy kontener DI. -Przyjrzyjmy się teraz poszczególnym podkatalogom bardziej szczegółowo. +Przyjrzyjmy się teraz poszczególnym podkatalogom szczegółowo. Presentery i szablony ===================== -Część prezentacyjną aplikacji mamy w katalogu `app/Presentation`. Alternatywą jest krótkie `app/UI`. Jest to miejsce dla wszystkich presenterów, ich szablonów i ewentualnych klas pomocniczych. +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. -Tę warstwę organizujemy według domen. W złożonym projekcie, który łączy e-sklep, blog i API, struktura wyglądałaby tak: +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 e-sklepu +├── Shop/ ← frontend sklepu │ ├── Product/ │ ├── Cart/ │ └── Order/ @@ -143,23 +143,23 @@ Tę warstwę organizujemy według domen. W złożonym projekcie, który łączy └── V1/ \-- -Natomiast w prostym blogu użylibyśmy podziału: +Odwrotnie, dla prostego bloga użylibyśmy takiej struktury: /--pre app/Presentation/ -├── Front/ ← frontend strony +├── Front/ ← frontend witryny │ ├── Home/ │ └── Post/ ├── Admin/ ← administracja │ ├── Dashboard/ │ └── Posts/ ├── Error/ -└── Export/ ← RSS, mapy strony itp. +└── Export/ ← RSS, mapy strony itd. \-- -Foldery takie jak `Home/` czy `Dashboard/` zawierają presentery i szablony. Foldery takie jak `Front/`, `Admin/` czy `Api/` nazywamy **modułami**. Technicznie są to zwykłe katalogi, które służą do logicznego podziału aplikacji. +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 z presenterem zawiera tak samo nazwany presenter i jego szablony. Na przykład folder `Dashboard/` zawiera: +Każdy folder zawierający presenter obejmuje sam plik presentera i jego szablony. Na przykład folder `Dashboard/` zawiera: /--pre Dashboard/ @@ -167,7 +167,7 @@ Każdy folder z presenterem zawiera tak samo nazwany presenter i jego szablony. └── default.latte ← szablon \-- -Ta struktura katalogów odzwierciedla się w przestrzeniach nazw klas. Na przykład `DashboardPresenter` znajduje się w przestrzeni nazw `App\Presentation\Admin\Dashboard` (zobacz [#Mapowanie presenterów]): +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; @@ -178,22 +178,22 @@ class DashboardPresenter extends Nette\Application\UI\Presenter } ``` -Do presentera `Dashboard` wewnątrz modułu `Admin` odwołujemy się w aplikacji za pomocą notacji dwukropkowej jako `Admin:Dashboard`. Do jego akcji `default` następnie jako `Admin:Dashboard:default`. W przypadku zagnieżdżonych modułów używamy więcej dwukropków, na przykład `Shop:Order:Detail:default`. +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ą kanały XML. Na początku mamy prostą postać: +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 dla mapy strony -└── feed.latte ← szablon dla kanału RSS +├── sitemap.latte ← szablon mapy strony +└── feed.latte ← szablon feedu RSS \-- -Z czasem pojawią się kolejne typy kanałów i będziemy potrzebować dla nich więcej logiki... Żaden problem! Folder `Export/` po prostu stanie się modułem: +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/ @@ -202,38 +202,38 @@ Z czasem pojawią się kolejne typy kanałów i będziemy potrzebować dla nich │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← kanał dla Zboží.cz - └── heureka.latte ← kanał dla Heureka.cz + ├── amazon.latte ← feed dla Amazona + └── ebay.latte ← feed dla eBaya \-- -Ta transformacja jest całkowicie płynna - wystarczy utworzyć nowe podfoldery, podzielić do nich kod i zaktualizować linki (np. z `Export:feed` na `Export:Feed:zbozi`). Dzięki temu możemy strukturę stopniowo rozszerzać w miarę potrzeb, poziom zagnieżdżenia nie jest w żaden sposób ograniczony. +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 dotyczących zarządzania zamówieniami, takich jak `OrderDetail`, `OrderEdit`, `OrderDispatch` itp., możesz dla lepszej organizacji w tym miejscu utworzyć moduł (folder) `Order`, w którym będą (foldery dla) presenterów `Detail`, `Edit`, `Dispatch` i inne. +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. -Lokalizacja szablonów ---------------------- +Położenie szablonów +------------------- -W poprzednich przykładach widzieliśmy, że szablony są umieszczone bezpośrednio w folderze z presenterem: +W poprzednich przykładach widzieliśmy, że szablony leżą bezpośrednio w folderze z presenterem: /--pre Dashboard/ ├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← opcjonalna klasa dla szablonu +├── DashboardTemplate.php ← opcjonalna klasa szablonu └── default.latte ← szablon \-- -Ta lokalizacja w praktyce okazuje się najwygodniejsza - wszystkie powiązane pliki masz od razu pod ręką. +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 wspiera obie warianty. Możesz nawet umieścić szablony całkowicie poza folderem `Presentation/`. Wszystko o możliwościach umieszczania szablonów znajdziesz w rozdziale [Wyszukiwanie szablonów |templates#Wyszukiwanie szablonów]. +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 ----------------------------- -Do presenterów i szablonów często należą również inne pliki pomocnicze. Umieszczamy je logicznie według ich zakresu: +Presenterom i szablonom często towarzyszą inne pliki pomocnicze. Umieszczamy je logicznie według ich zasięgu: -1. **Bezpośrednio przy presenterze** w przypadku specyficznych komponentów dla danego presentera: +1. **Bezpośrednio przy presenterze** w przypadku komponentów specyficznych dla tego presentera: /--pre Product/ @@ -242,7 +242,7 @@ Do presenterów i szablonów często należą również inne pliki pomocnicze. U └── FilterForm.php ← formularz do filtrowania \-- -2. **Dla modułu** - zalecamy wykorzystanie folderu `Accessory`, który umieści się przejrzyście na początku alfabetu: +2. **Dla modułu** - zalecamy folder `Accessory`, który wygodnie ląduje na początku alfabetu: /--pre Front/ @@ -263,30 +263,30 @@ Do presenterów i szablonów często należą również inne pliki pomocnicze. U └── Admin/ \-- -Lub możesz umieścić klasy pomocnicze takie jak `LatteExtension.php` czy `TemplateFilters.php` w folderze infrastruktury `app/Core/Latte/`. A komponenty w `app/Components`. Wybór zależy od zwyczajów zespołu. +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. Dla jego organizacji obowiązuje ponownie zasada - strukturyzujemy według domen: +Model zawiera całą logikę biznesową aplikacji. Zasada jego organizacji jest znów taka sama: strukturyzuj według domen: /--pre app/Model/ -├── Payment/ ← wszystko związane z płatnościami -│ ├── PaymentFacade.php ← główny punkt wejściowy +├── Payment/ ← wszystko o płatnościach +│ ├── PaymentFacade.php ← główny punkt wejścia │ ├── PaymentRepository.php │ ├── Payment.php ← encja -├── Order/ ← wszystko związane z zamówieniami +├── Order/ ← wszystko o zamówieniach │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php -└── Shipping/ ← wszystko związane z wysyłką +└── Shipping/ ← wszystko o wysyłce \-- -W modelu typowo spotkasz się z tymi typami klas: +W modelu typowo spotkasz te rodzaje klas: -**Fasady**: reprezentują główny punkt wejściowy do konkretnej domeny w aplikacji. Działają jako orkiestrator, który koordynuje współpracę między różnymi usługami w celu implementacji kompletnych przypadków użycia (jak "utwórz zamówienie" lub "przetwórz płatność"). Pod swoją warstwą orkiestracji fasada ukrywa szczegóły implementacyjne przed resztą aplikacji, dostarczając czysty interfejs do pracy z daną domeną. +**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 @@ -296,12 +296,12 @@ class OrderFacade // walidacja // utworzenie zamówienia // wysłanie e-maila - // zapisanie do statystyk + // zapis do statystyk } } ``` -**Usługi**: koncentrują się na specyficznej operacji biznesowej w ramach domeny. W przeciwieństwie do fasady, która orkiestruje całe przypadki użycia, usługa implementuje konkretną logikę biznesową (jak kalkulacje cen lub przetwarzanie płatności). Usługi są typowo bezstanowe i mogą być używane albo przez fasady jako bloki budulcowe dla bardziej złożonych operacji, albo bezpośrednio przez inne części aplikacji dla prostszych zadań. +**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 @@ -313,7 +313,7 @@ class PricingService } ``` -**Repozytoria**: zapewniają całą komunikację z magazynem danych, typowo bazą danych. Jego zadaniem jest wczytywanie i zapisywanie encji oraz implementacja metod do ich wyszukiwania. Repozytorium odizolowuje resztę aplikacji od szczegółów implementacyjnych bazy danych i dostarcza interfejs zorientowany obiektowo do pracy z danymi. +**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 @@ -328,10 +328,10 @@ class OrderRepository } ``` -**Encje**: obiekty reprezentujące główne koncepty biznesowe w aplikacji, które mają swoją tożsamość i zmieniają się w czasie. Typowo są to klasy mapowane na tabele bazy danych za pomocą ORM (jak Nette Database Explorer lub Doctrine). Encje mogą zawierać reguły biznesowe dotyczące ich danych oraz logikę walidacji. +**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 +// encja mapowana na tabelę bazy danych 'orders' class Order extends Nette\Database\Table\ActiveRow { public function addItem(Product $product, int $quantity): void @@ -345,13 +345,13 @@ class Order extends Nette\Database\Table\ActiveRow } ``` -**Obiekty wartości**: niemutowalne obiekty reprezentujące wartości bez własnej tożsamości - na przykład kwota pieniężna lub adres e-mail. Dwie instancje obiektu wartości z tymi samymi wartościami są uważane za identyczne. +**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 infrastruktury -================== +Kod infrastrukturalny +===================== -Folder `Core/` (lub także `Infrastructure/`) jest domem dla technicznego fundamentu aplikacji. Kod infrastruktury typowo obejmuje: +Folder `Core/` (albo alternatywnie `Infrastructure/`) jest domem technicznego fundamentu aplikacji. Kod infrastrukturalny obejmuje zwykle: /--pre app/Core/ @@ -370,7 +370,7 @@ Folder `Core/` (lub także `Infrastructure/`) jest domem dla technicznego fundam └── Stripe/ \-- -W mniejszych projektach oczywiście wystarczy płaska struktura: +Przy mniejszych projektach naturalnie wystarczy struktura płaska: /--pre Core/ @@ -379,29 +379,29 @@ W mniejszych projektach oczywiście wystarczy płaska struktura: └── QueueMailer.php \-- -Chodzi o kod, który: +To kod, który: -- Rozwiązuje problemy techniczne infrastruktury (routing, logowanie, cachowanie) -- Integruje usługi zewnętrzne (Sentry, Elasticsearch, Redis) -- Dostarcza podstawowe usługi dla całej aplikacji (mail, baza danych) -- Jest zazwyczaj niezależny od konkretnej domeny - cache lub logger działa tak samo dla e-sklepu czy bloga. +- 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 -Wahasz się, czy dana klasa należy tutaj, czy do modelu? Kluczowa różnica polega na tym, że kod w `Core/`: +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) -- Jest zazwyczaj możliwe przeniesienie go do innego projektu -- Rozwiązuje "jak to działa" (jak wysłać maila), a nie "co to robi" (jakiego maila wysłać) +- 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, obsługuje ustawienia SMTP -- `App\Model\OrderMailer` - używa `MailerFactory` do wysyłania e-maili o zamówieniach, zna ich szablony i wie, kiedy mają być wysłane +- `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 chodzi o przetwarzanie danych w tle, konserwację, czy zadania okresowe. Do uruchamiania służą proste skrypty w katalogu `bin/`, samą logikę implementacyjną umieszczamy w `app/Tasks/` (ewentualnie `app/Commands/`). +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: @@ -418,27 +418,27 @@ Przykład: └── ReminderCommand.php ← powiadomienia dla klientów \-- -Co należy do modelu, a co do skryptów poleceń? Na przykład logika do wysłania jednego e-maila jest częścią modelu, masowa wysyłka tysięcy e-maili już należy do `Tasks/`. +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 zazwyczaj [uruchamiamy z wiersza poleceń |https://blog.nette.org/en/cli-scripts-in-nette-application] lub przez cron. Można je uruchamiać również przez żądanie HTTP, ale trzeba pamiętać o bezpieczeństwie. Presenter, który uruchomi zadanie, trzeba zabezpieczyć, na przykład tylko dla zalogowanych użytkowników lub silnym tokenem i dostępem z dozwolonych adresów IP. W przypadku długich zadań trzeba zwiększyć limit czasu skryptu i użyć `session_write_close()`, aby nie blokować sesji. +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 ===================== -Oprócz wspomnianych podstawowych katalogów można w zależności od potrzeb projektu dodać inne specjalistyczne foldery. Spójrzmy na najczęstsze z nich i ich zastosowanie: +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 dla API niezależna od warstwy prezentacji -├── Database/ ← skrypty migracyjne i seedery dla danych testowych +├── 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 jeśli używasz architektury sterowanej zdarzeniami -├── Mail/ ← szablony e-mail i powiązana logika +├── Event/ ← przydatne przy architekturze zdarzeniowej +├── Mail/ ← szablony e-maili i powiązana logika └── Utils/ ← klasy pomocnicze \-- -Dla współdzielonych komponentów wizualnych używanych w presenterach w całej aplikacji można użyć folderu `app/Components` lub `app/Controls`: +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/ @@ -447,18 +447,18 @@ Dla współdzielonych komponentów wizualnych używanych w presenterach w całej │ └── UserForm.php ├── Grid/ ← komponenty do listowania danych │ └── DataGrid.php -└── Navigation/ ← elementy nawigacyjne +└── Navigation/ ← elementy nawigacji ├── Breadcrumbs.php └── Menu.php \-- -Tutaj należą komponenty, które mają bardziej złożoną logikę. Jeśli chcesz współdzielić komponenty między wieloma projektami, wskazane jest wydzielenie ich do osobnego pakietu composera. +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. -Do katalogu `app/Mail` można umieścić zarządzanie komunikacją e-mail: +W katalogu `app/Mail` możesz umieścić zarządzanie komunikacją e-mailową: /--pre app/Mail/ -├── templates/ ← szablony e-mail +├── templates/ ← szablony e-maili │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php @@ -468,32 +468,32 @@ Do katalogu `app/Mail` można umieścić zarządzanie komunikacją e-mail: Mapowanie presenterów ===================== -Mapowanie definiuje reguły wnioskowania nazwy klasy z nazwy presentera. Specyfikujemy je w [konfiguracji|configuration] pod kluczem `application › mapping`. +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` (ewentualnie `app/UI`). Tę konwencję musimy przekazać Nette w pliku konfiguracyjnym. Wystarczy jedna linia: +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 najpierw wyobraźmy sobie aplikację bez modułów. Chcemy, aby klasy presenterów należały do przestrzeni nazw `App\Presentation`, aby presenter `Home` mapował się na klasę `App\Presentation\HomePresenter`. Co osiągniemy tą konfiguracją: +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 nazwa presentera `Home` zastępuje gwiazdkę w masce `App\Presentation\*Presenter`, przez co uzyskujemy wynikową nazwę klasy `App\Presentation\HomePresenter`. Proste! +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 widać w przykładach w tym i innych rozdziałach, klasy presenterów umieszczamy w eponimicznych podkatalogach, na przykład presenter `Home` mapuje się na klasę `App\Presentation\Home\HomePresenter`. Osiągniemy to przez podwojenie dwukropka (wymaga Nette Application 3.2): +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 ``` -Teraz przystąpimy do mapowania presenterów do modułów. Dla każdego modułu możemy zdefiniować specyficzne mapowanie: +Przejdźmy teraz do mapowania presenterów w modułach. Dla każdego modułu możemy zdefiniować własne mapowanie: ```neon application: @@ -503,9 +503,9 @@ application: Api: App\Api\*Presenter ``` -Zgodnie z tą konfiguracją presenter `Front:Home` mapuje się na klasę `App\Presentation\Front\Home\HomePresenter`, podczas gdy presenter `Api:OAuth` na klasę `App\Api\OAuthPresenter`. +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 sposób mapowania i takich modułów będzie prawdopodobnie więcej, możliwe jest utworzenie ogólnej reguły, która je zastąpi. Do maski klasy dojdzie więc nowa gwiazdka dla modułu: +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: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -Działa to również dla głębiej zagnieżdżonych struktur katalogów, jak na przykład presenter `Admin:User:Edit`, segment z gwiazdką powtarza się dla każdego poziomu, a wynikiem jest klasa `App\Presentation\Admin\User\Edit\EditPresenter`. +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 składającej się z trzech segmentów. Ten zapis jest ekwiwalentny z poprzednim: +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: diff --git a/application/pl/how-it-works.texy b/application/pl/how-it-works.texy index b0a9e60584..08541766b4 100644 --- a/application/pl/how-it-works.texy +++ b/application/pl/how-it-works.texy @@ -3,10 +3,10 @@ Jak działają aplikacje?
    -Właśnie czytasz podstawowy dokument dokumentacji Nette. Poznasz całą zasadę działania aplikacji internetowych. Krok po kroku, od A do Z, od momentu powstania aż do ostatniego tchnienia skryptu PHP. Po przeczytaniu będziesz wiedzieć: +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 -- co to jest Bootstrap, Presenter i kontener DI +- czym są Bootstrap, Presenter i kontener DI - jak wygląda struktura katalogów
    @@ -15,16 +15,16 @@ Właśnie czytasz podstawowy dokument dokumentacji Nette. Poznasz całą zasadę Struktura katalogów =================== -Otwórz przykład szkieletu aplikacji internetowej o nazwie [WebProject|https://github.com/nette/web-project] i podczas czytania możesz patrzeć na pliki, o których jest mowa. +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 z aplikacją -│ ├── Core/ ← podstawowe klasy niezbędne do działania +├── app/ ← katalog aplikacji +│ ├── Core/ ← klasy rdzenia niezbędne do działania │ │ └── RouterFactory.php ← konfiguracja adresów URL -│ ├── Presentation/ ← presentery, szablony & spółka. +│ ├── Presentation/ ← presentery, szablony i spółka │ │ ├── @layout.latte ← szablon layoutu │ │ └── Home/ ← katalog presentera Home │ │ ├── HomePresenter.php ← klasa presentera Home @@ -35,53 +35,53 @@ Struktura katalogów wygląda mniej więcej tak: ├── config/ ← pliki konfiguracyjne │ ├── common.neon │ └── services.neon -├── log/ ← logowane błędy +├── log/ ← zalogowane błędy ├── temp/ ← pliki tymczasowe, cache, … -├── vendor/ ← biblioteki zainstalowane przez Composer +├── vendor/ ← biblioteki zainstalowane przez Composera │ ├── ... │ └── autoload.php ← autoloading wszystkich zainstalowanych pakietów -├── www/ ← katalog publiczny czyli document-root projektu +├── www/ ← katalog publiczny, document root projektu │ ├── assets/ ← skompilowane pliki statyczne (CSS, JS, obrazy, ...) │ ├── .htaccess ← reguły mod_rewrite -│ └── index.php ← pierwszy plik, którym uruchamia się aplikacja -└── .htaccess ← zabrania dostępu do wszystkich katalogów oprócz www +│ └── index.php ← plik początkowy uruchamiający aplikację +└── .htaccess ← zabrania dostępu do wszystkich katalogów poza www \-- -Strukturę katalogów można dowolnie zmieniać, foldery przemianować lub przenieść, jest całkowicie elastyczna. Nette dodatkowo dysponuje inteligentną autodetekcją i automatycznie rozpozna lokalizację aplikacji, w tym jej bazę URL. +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 foldery z presenterami i szablonami [podzielić na podkatalogi |directory-structure#Presentery i szablony] i klasy na przestrzenie nazw, które nazywamy modułami. +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 tzw. katalog publiczny czyli document-root projektu. Można go przemianować bez konieczności ustawiania czegokolwiek dodatkowego po stronie aplikacji. Trzeba tylko [skonfigurować hosting |nette:troubleshooting#Jak zmienić lub usunąć katalog www z adresu URL] tak, aby document-root wskazywał na ten katalog. +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żna również od razu pobrać wraz z Nette za pomocą [Composera |best-practices:composer]: +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 lub macOS ustaw katalogom `log/` i `temp/` [prawa do zapisu |nette:troubleshooting#Ustawianie uprawnień do katalogów]. +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 niczego konfigurować i można ją od razu wyświetlić w przeglądarce, uzyskując dostęp do folderu `www/`. +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ę w chwili, gdy użytkownik w przeglądarce otwiera stronę. Czyli gdy przeglądarka puka do serwera z żądaniem HTTP. Żądanie kieruje się na jeden plik PHP, który znajduje się w publicznym katalogu `www/`, a jest nim `index.php`. Powiedzmy, że chodzi o żądanie na adres `https://example.com/product/123`. Dzięki odpowiedniemu [ustawieniu serwera |nette:troubleshooting#Jak skonfigurować serwer dla przyjaznych adresów URL] również ten URL mapuje się na plik `index.php` i ten się wykonuje. +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) zainicjalizowanie środowiska -2) uzyskanie fabryki -3) uruchomienie aplikacji Nette, która obsłuży żądanie +1) zainicjować środowisko +2) pozyskać fabrykę +3) uruchomić aplikację Nette, która obsłuży żądanie -Jaką fabrykę? Przecież nie produkujemy traktorów, ale strony internetowe! Poczekajcie, zaraz się to wyjaśni. +Jaką fabrykę? Przecież nie produkujemy traktorów, tylko robimy strony! Zaraz, zaraz, zaraz to wyjaśnimy. -Słowami „inicjalizacja środowiska” rozumiemy na przykład to, że aktywuje się [Tracy|tracy:], co jest niesamowitym narzędziem do logowania lub wizualizacji błędów. Na serwerze produkcyjnym błędy loguje, na deweloperskim od razu wyświetla. Dlatego do inicjalizacji należy również decyzja, czy strona działa w trybie produkcyjnym czy deweloperskim. Do tego Nette używa [inteligentnej autodetekcji |bootstrapping#Tryb deweloperski vs produkcyjny]: jeśli stronę uruchamiasz na localhost, działa w trybie deweloperskim. Nie musisz więc nic konfigurować, a aplikacja jest od razu gotowa zarówno do rozwoju, jak i ostrego wdrożenia. Te kroki są wykonywane i szczegółowo opisane w rozdziale o [klasie Bootstrap|bootstrapping]. +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]. -Trzecim punktem (tak, drugi pominęliśmy, ale wrócimy do niego) jest uruchomienie aplikacji. Obsługą żądań HTTP w Nette zajmuje się klasa `Nette\Application\Application` (dalej `Application`), więc gdy mówimy uruchomić aplikację, mamy na myśli konkretnie wywołanie metody o wymownej nazwie `run()` na obiekcie tej klasy. +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 metodyk. A jedna z tych absolutnie najsprawdzonych nazywa się **dependency injection**, w skrócie DI. W tej chwili nie chcemy Cię obciążać wyjaśnianiem DI, od tego jest [osobny rozdział|dependency-injection:introduction], istotny jest skutek, że kluczowe obiekty będzie nam zazwyczaj tworzyć fabryka obiektów, której mówi się **kontener DI** (w skrócie DIC). Tak, to ta fabryka, o której była przed chwilą mowa. I wyprodukuje nam również obiekt `Application`, dlatego potrzebujemy najpierw kontenera. Uzyskamy go za pomocą klasy `Configurator` i pozwolimy mu wyprodukować obiekt `Application`, wywołamy na nim metodę `run()` i tym samym uruchomi się aplikacja Nette. Dokładnie to dzieje się w pliku [index.php |bootstrapping#index.php]. +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 @@ -89,15 +89,15 @@ Nette Application Klasa `Application` ma jedno zadanie: odpowiedzieć na żądanie HTTP. -Aplikacje pisane w Nette dzielą się na mnóstwo tzw. presenterów (w innych frameworkach można spotkać się z terminem controller, chodzi o to samo), które są klasami, z których każda reprezentuje jakąś konkretną stronę internetową: np. stronę główną; produkt w e-sklepie; formularz logowania; kanał sitemap itp. Aplikacja może mieć od jednego do tysięcy presenterów. +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 tego, że prosi tzw. router, aby zdecydował, któremu z presenterów przekazać aktualne żądanie do obsłużenia. Router decyduje, czyja to odpowiedzialność. Spogląda na wejściowy URL `https://example.com/product/123` i na podstawie tego, jak jest ustawiony, decyduje, że to praca np. dla **presentera** `Product`, od którego będzie chciał jako **akcję** wyświetlenie (`show`) produktu o `id: 123`. Parę presenter + akcja jest dobrym zwyczajem zapisywać oddzieloną dwukropkiem jako `Product:show`. +`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`. -Zatem router przekształcił URL na parę `Presenter:action` + parametry, w naszym przypadku `Product:show` + `id: 123`. Jak taki router wygląda, można zobaczyć w pliku `app/Core/RouterFactory.php` i szczegółowo opisujemy go w rozdziale [Routingu |Routing]. +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` już zna nazwę presentera i może kontynuować. Tworząc obiekt klasy `ProductPresenter`, który jest kodem presentera `Product`. Dokładniej mówiąc, prosi kontener DI, aby wyprodukował presenter, ponieważ od produkowania jest on. +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ć na przykład tak: +Presenter może wyglądać tak: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -115,86 +115,86 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Obsługę żądania przejmuje presenter. A zadanie brzmi jasno: wykonaj akcję `show` z `id: 123`. Co w języku presenterów oznacza, że wywołana zostanie metoda `renderShow()` i w parametrze `$id` otrzyma `123`. +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ć więcej akcji, czyli mieć więcej metod `render()`. Ale zalecamy projektowanie presenterów z jedną lub jak najmniejszą liczbą akcji. +Presenter może obsługiwać wiele akcji, czyli mieć wiele metod `render()`. Zalecamy jednak projektować presentery z jedną albo możliwie niewieloma akcjami. -Zatem, wywołano metodę `renderShow(123)`, której kod jest wprawdzie wymyślonym przykładem, ale można na nim zobaczyć, jak przekazuje się dane do szablonu, czyli zapisem do `$this->template`. +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 to być strona HTML, obrazek, dokument XML, wysłanie pliku z dysku, JSON lub na przykład przekierowanie na inną stronę. Ważne jest, że jeśli jawnie nie powiemy, jak ma odpowiedzieć (co jest przypadkiem `ProductPresenter`), odpowiedzią będzie wyrenderowanie szablonu ze stroną HTML. Dlaczego? Ponieważ w 99% przypadków chcemy wyrenderować szablon, dlatego presenter to zachowanie traktuje jako domyślne i chce nam ułatwić pracę. To jest sens Nette. +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 podawać, jaki szablon wyrenderować, ścieżkę do niego wywnioskuje sam. W przypadku akcji `show` po prostu spróbuje załadować szablon `show.latte` w katalogu z klasą `ProductPresenter`. Podobnie spróbuje odnaleźć layout w pliku `@layout.latte` (więcej o [wyszukiwaniu szablonów |templates#Wyszukiwanie szablonów]). +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]). -A następnie szablony wyrenderuje. Tym samym zadanie presentera i całej aplikacji jest zakończone, a dzieło jest ukończone. Jeśli szablon by nie istniał, zwrócona zostanie strona z błędem 404. Więcej o presenterach przeczytasz na stronie [Presentery |presenters]. +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, spróbujmy podsumować cały proces z nieco innym URL: +Dla pewności podsumujmy cały proces na nieco innym URL: -1) URL będzie `https://example.com` -2) uruchamiamy aplikację, tworzy się kontener i uruchamia `Application::run()` -3) router dekoduje URL jako parę `Home:default` -4) tworzy się obiekt klasy `HomePresenter` +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` z layoutem np. `@layout.latte` +6) renderowany jest szablon, np. `default.latte`, wraz z layoutem, np. `@layout.latte` -Być może spotkałeś się teraz z dużą ilością nowych pojęć, ale wierzymy, że mają sens. Tworzenie aplikacji w Nette to ogromna przyjemność. +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 już mowa o szablonach, w Nette używa się systemu szablonów [Latte |latte:]. Dlatego też te końcówki `.latte` przy szablonach. Latte używa się po pierwsze dlatego, że jest to najlepiej zabezpieczony system szablonów dla PHP, a jednocześnie system najbardziej intuicyjny. Nie musisz uczyć się wielu nowych rzeczy, wystarczy znajomość PHP i kilku znaczników. Wszystko dowiesz się [w dokumentacji |templates]. +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 [tworzy się linki |creating-links] do innych presenterów i akcji w ten sposób: +W szablonie [tworzysz odnośniki |creating-links] do innych presenterów i akcji tak: ```latte szczegóły produktu ``` -Po prostu zamiast rzeczywistego URL wpisujesz znaną parę `Presenter:action` i podajesz ewentualne parametry. Sztuczka tkwi w `n:href`, które mówi, że ten atrybut przetworzy Nette. I wygeneruje: +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ę już wcześniej wspomniany router. Otóż routery w Nette są wyjątkowe tym, że potrafią wykonywać nie tylko transformacje z URL na parę presenter:action, ale także odwrotnie, czyli z nazwy presentera + akcji + parametrów wygenerować URL. Dzięki temu w Nette możesz całkowicie zmienić kształty URL w całej gotowej aplikacji, nie zmieniając ani jednego znaku w szablonie czy presenterze. Tylko przez modyfikację routera. Również dzięki temu działa tzw. kanonizacja, co jest kolejną unikalną cechą Nette, która przyczynia się do lepszego SEO (optymalizacji dla wyszukiwarek internetowych) przez automatyczne zapobieganie istnieniu duplikatów treści pod różnymi URL. Wielu programistów uważa to za niesamowite. +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 ======================= -O presenterach musimy Ci jeszcze zdradzić jedną rzecz: mają w sobie wbudowany system komponentów. Coś podobnego mogą pamiętać weterani z Delphi lub ASP.NET Web Forms, na czymś zdalnie podobnym opiera się React czy Vue.js. W świecie frameworków PHP jest to absolutnie unikalna sprawa. +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 samodzielne, wielokrotnego użytku całości, które wstawiamy do stron (czyli presenterów). Mogą to być [formularze |forms:in-presenter], [siatki danych |https://componette.org/contributte/datagrid/], menu, ankiety, właściwie cokolwiek, co ma sens używać wielokrotnie. Możemy tworzyć własne komponenty lub używać niektórych z [ogromnej oferty |https://componette.org] komponentów open source. +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. Otworzą Ci nowe możliwości składania stron z gotowych jednostek. A ponadto mają coś wspólnego z [Hollywoodem |components#Styl Hollywood]. +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. +Kontener DI, czyli fabryka obiektów, jest sercem całej aplikacji. -Nie obawiaj się, nie jest to żaden magiczny black box, jak mogłoby się wydawać z poprzednich linijek. Właściwie jest to jedna dość nudna klasa PHP, którą generuje Nette i zapisuje w katalogu z cache. Ma mnóstwo metod nazwanych jak `createServiceAbcd()` i każda z nich potrafi wyprodukować i zwrócić jakiś obiekt. Tak, jest tam również metoda `createServiceApplication()`, która wyprodukuje `Nette\Application\Application`, którego potrzebowaliśmy w pliku `index.php` do uruchomienia aplikacji. I są tam metody produkujące poszczególne presentery. I tak dalej. +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. -Obiektom, które tworzy kontener DI, z jakiegoś powodu mówi się usługi. +Obiekty tworzone przez kontener DI z jakiegoś powodu nazywane są usługami. -Co jest w tej klasie naprawdę specjalnego, to że nie programujesz jej Ty, ale framework. On rzeczywiście generuje kod PHP i zapisuje go na dysku. Ty tylko dajesz instrukcje, jakie obiekty ma umieć kontener produkować i jak dokładnie. A te instrukcje są zapisane w [plikach konfiguracyjnych |bootstrapping#Konfiguracja kontenera DI], dla których używa się formatu [NEON|neon:format] i dlatego mają też rozszerzenie `.neon`. +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żą czysto do instruowania kontenera DI. Więc gdy na przykład podam w sekcji [sesji |http:configuration#Sesja] opcję `expiration: 14 days`, to kontener DI przy tworzeniu obiektu `Nette\Http\Session` reprezentującego sesję wywoła jego metodę `setExpiration('14 days')` i tym samym konfiguracja stanie się rzeczywistością. +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ę. -Jest tu dla Ciebie przygotowany cały rozdział opisujący, co wszystko można [konfigurować |nette:configuring] i jak [definiować własne usługi |dependency-injection:services]. +Czeka na Ciebie cały rozdział opisujący, co można [skonfigurować |nette:configuring] i jak [zdefiniować własne usługi |dependency-injection:services]. -Jak tylko trochę zagłębisz się w tworzenie usług, natkniesz się na słowo [autowiring |dependency-injection:autowiring]. To jest bajer, który niesamowicie uprości Ci życie. Potrafi automatycznie przekazywać obiekty tam, gdzie ich potrzebujesz (na przykład w konstruktorach Twoich klas), bez konieczności robienia czegokolwiek. Odkryjesz, że kontener DI w Nette to mały cud. +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. -Dokąd dalej? -============ +Co dalej? +========= -Przeszliśmy przez podstawowe zasady aplikacji w Nette. Na razie bardzo powierzchownie, ale wkrótce zagłębisz się w temat i z czasem stworzysz wspaniałe aplikacje internetowe. Dokąd kontynuować dalej? Czy wypróbowałeś już tutorial [Pisanie pierwszej aplikacji|quickstart:]? +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:]? -Oprócz wyżej opisanego, Nette dysponuje całym arsenałem [przydatnych klas|utils:], [warstwą bazy danych|database:], itd. Spróbuj sobie po prostu przeklikać dokumentację. Lub [blog|https://blog.nette.org]. Odkryjesz mnóstwo ciekawych rzeczy. +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 przynosi Ci mnóstwo radości 💙 +Niech framework przyniesie Ci wiele radości 💙 diff --git a/application/pl/multiplier.texy b/application/pl/multiplier.texy index 68e68e1852..dcb7e97ab9 100644 --- a/application/pl/multiplier.texy +++ b/application/pl/multiplier.texy @@ -2,31 +2,31 @@ Multiplier: dynamiczne komponenty ********************************* .[perex] -Narzędzie do dynamicznego tworzenia interaktywnych komponentów +Narzędzie do dynamicznego tworzenia komponentów interaktywnych. -Wyjdźmy od typowego przykładu: mamy listę towarów w sklepie internetowym, przy czym przy każdym będziemy chcieli wyświetlić formularz do dodania towaru do koszyka. Jedną z możliwych opcji jest opakowanie całego listingu w jeden formularz. Znacznie wygodniejszy sposób oferuje nam jednak [api:Nette\Application\UI\Multiplier]. +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 umożliwia wygodne definiowanie fabryczki dla wielu komponentów. Działa na zasadzie zagnieżdżonych komponentów - każdy komponent dziedziczący po [api:Nette\ComponentModel\Container] może zawierać kolejne komponenty. +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 rozdział o [modelu komponentowym |components#Komponenty dogłębnie] w dokumentacji lub [wykład Honzy Tvrdíka|https://www.youtube.com/watch?v=8y3LLexWu-I]. +Zobacz w dokumentacji rozdział o [modelu komponentów |components#Komponenty w głąb]. -Istotą Multipliera jest to, że występuje w pozycji rodzica, który potrafi dynamicznie tworzyć swoje potomstwo za pomocą callbacku przekazanego w konstruktorze. Zobacz przykład: +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('count', 'Ilość towaru:') + $form->addInteger('amount', 'Ilość:') ->setRequired(); - $form->addSubmit('send', 'Dodaj do koszyka'); + $form->addSubmit('send', 'Do koszyka'); return $form; }); } ``` -Teraz możemy w szablonie po prostu przy każdym towarze wyświetlić formularz - i każdy będzie rzeczywiście unikalnym komponentem. +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} @@ -37,26 +37,26 @@ Teraz możemy w szablonie po prostu przy każdym towarze wyświetlić formularz {/foreach} ``` -Argument przekazany w znaczniku `{control}` jest w formacie, który mówi: +Argument przekazany w tagu `{control}` ma format, który oznacza: -1. pobierz komponent `shopForm` -2. a z niego pobierz potomka `$item->id` +1. Pobierz komponent `shopForm`. +2. Z niego pobierz potomka o nazwie `$item->id`. -Przy pierwszym wywołaniu punktu **1.** `shopForm` jeszcze nie istnieje, więc wywołana zostanie jego fabryka `createComponentShopForm`. Na uzyskanym komponencie (instancji Multipliera) jest następnie wywoływana fabryka konkretnego formularza - co jest anonimową funkcją, którą przekazaliśmy Multiplierowi w konstruktorze. +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 foreache już metoda `createComponentShopForm` nie będzie wywoływana (komponent istnieje), ale ponieważ szukamy jego innego potomka (`$item->id` będzie w każdej iteracji inne), ponownie zostanie wywołana anonimowa funkcja i zwróci nam nowy formularz. +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. -Jedyne, co pozostaje, to zapewnić, aby formularz dodał do koszyka rzeczywiście ten towar, który ma - obecnie formularz przy każdym towarze jest całkowicie identyczny. Pomoże nam właściwość Multipliera (i ogólnie każdej fabryki komponentu w Nette Framework), a mianowicie ta, że każda fabryka jako swój pierwszy argument otrzymuje nazwę tworzonego komponentu. W naszym przypadku będzie to `$item->id`, co jest dokładnie tą informacją, której potrzebujemy. Wystarczy więc lekko zmodyfikować tworzenie formularza: +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 ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Ilość towaru:') + $form->addInteger('amount', 'Ilość:') ->setRequired(); $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Dodaj do koszyka'); + $form->addSubmit('send', 'Do koszyka'); return $form; }); } diff --git a/application/pl/presenters.texy b/application/pl/presenters.texy index c450af53eb..ac3ce3dd2d 100644 --- a/application/pl/presenters.texy +++ b/application/pl/presenters.texy @@ -3,37 +3,37 @@ Presentery
    -Zapoznamy się z tym, jak w Nette pisze się presentery i szablony. Po przeczytaniu będziesz wiedzieć: +Przyjrzymy się temu, jak w Nette pisze się presentery i szablony. Po lekturze będziesz rozumieć: -- jak działa presenter -- co to są parametry trwałe -- jak rysuje się szablony +- jak działają presentery +- czym są parametry trwałe +- jak renderowane są szablony
    -[Już wiemy |how-it-works#Nette Application], że presenter to klasa, która reprezentuje jakąś konkretną stronę aplikacji internetowej, np. stronę główną; produkt w e-sklepie; formularz logowania; kanał sitemap itp. Aplikacja może mieć od jednego do tysięcy presenterów. W innych frameworkach nazywa się je również kontrolerami. +[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. -Zazwyczaj pod pojęciem presenter rozumie się potomka klasy [api:Nette\Application\UI\Presenter], który jest odpowiedni do generowania interfejsów internetowych i któremu poświęcimy resztę tego rozdziału. W ogólnym sensie presenter to dowolny obiekt implementujący interfejs [api:Nette\Application\IPresenter]. +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 (co może być stroną HTML, obrazkiem, przekierowaniem itp.). +Zadaniem presentera jest obsłużenie żądania i zwrócenie odpowiedzi (którą może być strona HTML, obraz, przekierowanie itd.). -Zatem na początku przekazywane jest mu żądanie. Nie jest to bezpośrednio żądanie HTTP, ale obiekt [api:Nette\Application\Request], na który zostało przekształcone żądanie HTTP za pomocą routera. Z tym obiektem zazwyczaj nie mamy do czynienia, ponieważ presenter inteligentnie deleguje przetwarzanie żądania do innych metod, które teraz pokażemy. +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* .<> +[* lifecycle.svg *] *** Cykl życia presentera .<> -Obrazek przedstawia listę metod, które są kolejno wywoływane od góry do dołu, jeśli istnieją. Żadna z nich nie musi istnieć, możemy mieć całkowicie pusty presenter bez ani jednej metody i zbudować na nim prostą statyczną stronę internetową. +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 tak do końca do cyklu życia presentera, ponieważ jest wywoływany w momencie tworzenia obiektu. Ale podajemy go ze względu na ważność. Konstruktor (wraz z [metodą inject|best-practices:inject-method-attribute]) służy do przekazywania zależności. +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ć i czytać z bazy danych, wykonywać obliczeń itp. Od tego są klasy z warstwy, którą określamy jako model. Na przykład klasa `ArticleRepository` może odpowiadać za ładowanie i zapisywanie artykułów. Aby presenter mógł z nią pracować, pozwoli sobie ją [przekazać za pomocą dependency injection |dependency-injection:passing-dependencies]: +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 @@ -50,39 +50,42 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -Natychmiast po otrzymaniu żądania wywoływana jest metoda `startup()`. Można jej użyć do inicjalizacji właściwości, weryfikacji uprawnień użytkownika itp. Wymagane jest, aby metoda zawsze wywoływała przodka `parent::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()} --------------------------------------------------- +`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]. -Odpowiednik metody `render()`. Podczas gdy `render()` jest przeznaczona do przygotowania danych dla konkretnego szablonu, który następnie zostanie wyrenderowany, to w `action()` przetwarza się żądanie bez związku z renderowaniem szablonu. Na przykład przetwarza się dane, loguje lub wylogowuje użytkownika i tak dalej, a następnie [przekierowuje 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')`. -Ważne jest, że `action()` jest wywoływana wcześniej niż `render()`, więc możemy w niej ewentualnie zmienić dalszy bieg wydarzeń, tj. zmienić szablon, który będzie rysowany, a także metodę `render()`, która będzie wywoływana. A to za pomocą `setView('innyView')`. +.{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. -Metodzie przekazywane są parametry z żądania. Możliwe i zalecane jest podanie typów parametrów, np. `actionShow(int $id, ?string $slug = null)` - jeśli parametr `id` będzie brakował lub jeśli nie będzie liczbą całkowitą, presenter zwróci [błąd 404 |#Błąd 404 i spółka] i zakończy działanie. +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()} +`handle(args...)` .{toc: handle()} -------------------------------------------------- -Metoda przetwarza tzw. sygnały, z którymi zapoznamy się w rozdziale poświęconym [komponentom |components#Sygnał]. Jest bowiem przeznaczona głównie dla komponentów i przetwarzania żądań AJAX. +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. -Metodzie przekazywane są parametry z żądania, jak w przypadku `action()`, w tym kontrola typów. +Do metody przekazywane są parametry z żądania, tak jak przy `action()`, wraz z kontrolą typów. `beforeRender()` ---------------- -Metoda `beforeRender`, jak sama nazwa wskazuje, jest wywoływana przed każdą metodą `render()`. Używa się jej do wspólnej konfiguracji szablonu, przekazania zmiennych dla layoutu i podobnych. +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()} ----------------------------------------------- +`render(args...)` .{toc: render()} +----------------------------------------------- -Miejsce, gdzie przygotowujemy szablon do późniejszego wyrenderowania, przekazujemy mu dane itp. +Tutaj przygotowujemy szablon do późniejszego renderowania, przekazujemy mu dane itd. -Metodzie przekazywane są parametry z żądania, jak w przypadku `action()`, w tym kontrola typów. +Do metody przekazywane są parametry z żądania, tak jak przy `action()`, wraz z kontrolą typów. ```php public function renderShow(int $id): void @@ -96,7 +99,7 @@ public function renderShow(int $id): void `afterRender()` --------------- -Metoda `afterRender`, jak nazwa ponownie wskazuje, jest wywoływana po każdej metodzie `render()`. Używa się jej raczej wyjątkowo. +Metoda `afterRender`, jak znów wskazuje nazwa, wywoływana jest po każdej metodzie `render()`. Używana jest raczej rzadko. `shutdown()` @@ -105,95 +108,117 @@ Metoda `afterRender`, jak nazwa ponownie wskazuje, jest wywoływana po każdej m Wywoływana na końcu cyklu życia presentera. -**Dobra rada, zanim pójdziemy dalej**. Presenter, jak widać, może obsługiwać więcej akcji/view, czyli mieć więcej metod `render()`. Ale zalecamy projektowanie presenterów z jedną lub jak najmniejszą liczbą akcji. +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. -Wysłanie odpowiedzi -=================== +Wysyłanie odpowiedzi +==================== -Odpowiedzią presentera jest zazwyczaj [wyrenderowanie szablonu ze stroną HTML|templates], ale może nią być również wysłanie pliku, JSON lub na przykład przekierowanie na inną stronę. +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 za pomocą jednej z poniższych metod wysłać odpowiedź i jednocześnie zakończyć działanie presentera: +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()` [przekierowuje |#Przekierowanie] -- `error()` kończy presenter [z powodu błędu |#Błąd 404 i spółka] -- `sendJson($data)` kończy presenter i [wysyła dane |#Wysłanie JSON] w formacie JSON +- `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 -Jeśli nie wywołasz żadnej z tych metod, presenter automatycznie przystąpi do renderowania szablonu. Dlaczego? Ponieważ w 99% przypadków chcemy wyrenderować szablon, dlatego presenter to zachowanie traktuje jako domyślne i chce nam ułatwić pracę. +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 linków -================ -Presenter dysponuje metodą `link()`, za pomocą której można tworzyć linki URL do innych presenterów. Pierwszym parametrem jest docelowy presenter & akcja, następnie przekazywane argumenty, które mogą być podane jako tablica: +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' => 'cs']); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -W szablonie tworzy się linki do innych presenterów & akcji w ten sposób: +W szablonie odnośniki do innych presenterów i akcji tworzy się tak: ```latte szczegóły produktu ``` -Po prostu zamiast rzeczywistego URL wpisujesz znaną parę `Presenter:action` i podajesz ewentualne parametry. Sztuczka tkwi w `n:href`, które mówi, że ten atrybut przetworzy Latte i wygeneruje rzeczywisty URL. W Nette więc w ogóle nie musisz zastanawiać się nad URL, tylko nad presenterami i akcjami. +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 linków URL|creating-links]. +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()`, które mają bardzo podobną składnię jak metoda [link() |#Tworzenie linków]. +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: +Metoda `forward()` przechodzi na nowy presenter natychmiast, bez przekierowania HTTP: ```php $this->forward('Product:show'); ``` -Przykład tzw. tymczasowego przekierowania z kodem HTTP 302 (lub 303, jeśli metodą aktualnego żądania jest POST): +Przykład tymczasowego przekierowania z kodem HTTP 302 (albo 303, jeśli metodą bieżącego żądania jest POST): ```php $this->redirect('Product:show', $id); ``` -Stałe przekierowanie z kodem HTTP 301 osiągniesz w ten sposób: +Aby uzyskać trwałe przekierowanie z kodem HTTP 301, użyj tego: ```php $this->redirectPermanent('Product:show', $id); ``` -Na inny URL poza aplikacją można przekierować metodą `redirectUrl()`. Jako drugi parametr można podać kod HTTP, domyślny to 302 (lub 303, jeśli metodą aktualnego żądania jest POST): +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 przez wyrzucenie tzw. cichego wyjątku kończącego `Nette\Application\AbortException`. +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ć [flash message |#Wiadomości flash], czyli wiadomości, które zostaną po przekierowaniu wyświetlone w szablonie. +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 ================ -Są to wiadomości zazwyczaj informujące o wyniku jakiejś 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 jeszcze przez 30 sekund – na przykład na wypadek, gdyby z powodu błędnego transferu użytkownik odświeżył stronę - wiadomość mu więc od razu nie zniknie. +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 o przekazanie do szablonu zadba presenter. Pierwszym parametrem jest tekst wiadomości, a opcjonalnym drugim parametrem jej typ (error, warning, info itp.). Metoda `flashMessage()` zwraca instancję wiadomości flash, której można dodawać dalsze informacje. +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('Pozycja została usunięta.'); +$this->flashMessage('Element został usunięty.'); $this->redirect(/* ... */); // i przekierowujemy ``` -W szablonie te wiadomości są dostępne w zmiennej `$flashes` jako obiekty `stdClass`, które zawierają właściwości `message` (tekst wiadomości), `type` (typ wiadomości) i mogą zawierać już wspomniane informacje użytkownika. Wyrenderujemy je na przykład tak: +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} @@ -202,10 +227,10 @@ W szablonie te wiadomości są dostępne w zmiennej `$flashes` jako obiekty `std ``` -Błąd 404 i spółka. -================== +Błąd 404 itd. +============= -Jeśli nie można spełnić żądania, na przykład z powodu, że artykuł, który chcemy wyświetlić, nie istnieje w bazie danych, wyrzucamy błąd 404 metodą `error(?string $message = null, int $httpCode = 404)`. +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 @@ -218,13 +243,13 @@ public function renderShow(int $id): void } ``` -Kod HTTP błędu można przekazać jako drugi parametr, domyślny to 404. Metoda działa tak, że wyrzuca wyjątek `Nette\Application\BadRequestException`, po czym `Application` przekazuje sterowanie do error-presentera. Co jest presenterem, którego zadaniem jest wyświetlenie strony informującej o zaistniałym błędzie. Ustawienie error-preseteru dokonuje się w [konfiguracji application|configuration]. +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]. -Wysłanie JSON -============= +Wysyłanie JSON +============== -Przykład metody action, która wysyła dane w formacie JSON i kończy presenter: +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 @@ -238,12 +263,12 @@ public function actionData(): void Parametry żądania .{data-version:3.1.14} ======================================== -Presenter, a także każdy komponent, uzyskuje z żądania HTTP swoje parametry. Ich wartość można uzyskać metodą `getParameter($name)` lub `getParameters()`. Wartości są ciągami znaków lub tablicami ciągów znaków, są to w zasadzie surowe dane uzyskane bezpośrednio z URL. +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 udostępnianie parametrów przez właściwości. Wystarczy oznaczyć je atrybutem `#[Parameter]`: +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; // ta linia jest ważna +use Nette\Application\Attributes\Parameter; // ten wiersz jest ważny class HomePresenter extends Nette\Application\UI\Presenter { @@ -252,9 +277,9 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Przy właściwości zalecamy podanie również typu danych (np. `string`), a Nette na jego podstawie automatycznie przeliczy wartość. Wartości parametrów można również [walidować |#Walidacja parametrów]. +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 linku można parametrom wartość ustawić bezpośrednio: +Przy tworzeniu odnośnika możesz ustawić wartość parametru bezpośrednio: ```latte kliknij @@ -264,14 +289,14 @@ Przy tworzeniu linku można parametrom wartość ustawić bezpośrednio: Parametry trwałe ================ -Parametry trwałe służą do utrzymywania stanu między różnymi żądaniami. Ich wartość pozostaje taka sama nawet po kliknięciu na link. W przeciwieństwie do danych w sesji, są one przekazywane w URL. I to całkowicie automatycznie, nie trzeba ich więc jawnie podawać w `link()` lub `n:href`. +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ład użycia? Masz aplikację wielojęzyczną. Aktualny język jest parametrem, który musi być stale częścią URL. Ale byłoby niesamowicie męczące podawanie go w każdym linku. Więc zrobisz z niego parametr trwały `lang` i będzie się przenosił sam. Parada! +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! -Tworzenie parametru trwałego jest w Nette niezwykle proste. Wystarczy utworzyć publiczną właściwość i oznaczyć ją atrybutem: (wcześniej używano `/** @persistent */`) +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; // ta linia jest ważna +use Nette\Application\Attributes\Persistent; // ten wiersz jest ważny class ProductPresenter extends Nette\Application\UI\Presenter { @@ -280,14 +305,14 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Jeśli `$this->lang` będzie miał wartość na przykład `'en'`, to również linki utworzone za pomocą `link()` lub `n:href` będą zawierać parametr `lang=en`. A po kliknięciu na link ponownie `$this->lang = 'en'`. +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'`. -Przy właściwości zalecamy podanie również typu danych (np. `string`) i można podać również wartość domyślną. Wartości parametrów można [walidować |#Walidacja parametrów]. +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 standardowo przenoszą się między wszystkimi akcjami danego presentera. Aby przenosiły się również między wieloma presenterami, trzeba je zdefiniować albo: +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, od którego dziedziczą presentery -- w trait, którego użyją presentery: +- we wspólnym przodku, po którym presentery dziedziczą +- albo w traicie, którego presentery używają: ```php trait LanguageAware @@ -302,42 +327,62 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Przy tworzeniu linku można parametrowi trwałemu zmienić wartość: +Przy tworzeniu odnośnika wartość parametru trwałego można zmienić: ```latte szczegóły po czesku ``` -Lub można go *zresetować*, tj. usunąć z URL. Wtedy przyjmie swoją wartość domyślną: +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 samodzielne, wielokrotnego użytku całości, które wstawiamy do presenterów. Mogą to być [formularze |forms:in-presenter], siatki danych, menu, właściwie cokolwiek, co ma sens używać wielokrotnie. +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 wstawia się komponenty do presentera i następnie używa? Dowiesz się tego w rozdziale [Komponenty |components]. Nawet dowiesz się, co mają wspólnego z Hollywoodem. +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 gdzie mogę zdobyć komponenty? Na stronie [Componette |https://componette.org/search/component] znajdziesz komponenty open-source oraz wiele innych dodatków do Nette, które umieścili tu wolontariusze ze społeczności wokół frameworka. +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 do hloubky -================== +Idziemy głębiej +=============== .[tip] -Z tym, co do tej pory pokazaliśmy w tym rozdziale, prawdopodobnie w zupełności sobie poradzisz. Poniższe linijki są przeznaczone dla tych, którzy interesują się presenterami dogłębnie i chcą wiedzieć absolutnie wszystko. +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] otrzymanych z URL zapisuje do właściwości metoda `loadState()`. Ta również kontroluje, czy odpowiada typ danych podany przy właściwości, w przeciwnym razie odpowie błędem 404 i strona się nie wyświetli. +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 ślepo nie wierz parametrom, ponieważ mogą być łatwo przez użytkownika nadpisane w URL. W ten sposób na przykład zweryfikujemy, czy język `$this->lang` jest wśród wspieranych. Odpowiednią drogą jest nadpisanie wspomnianej metody `loadState()`: +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 @@ -347,8 +392,8 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function loadState(array $params): void { - parent::loadState($params); // tutaj ustawia się $this->lang - // następuje własna kontrola wartości: + parent::loadState($params); // tutaj ustawiane jest $this->lang + // następuje własne sprawdzenie wartości: if (!in_array($this->lang, ['en', 'cs'])) { $this->error(); } @@ -357,30 +402,30 @@ class ProductPresenter extends Nette\Application\UI\Presenter ``` -Zapisanie i odtworzenie żądania -------------------------------- +Zapis i przywrócenie żądania +---------------------------- -Żądanie, które obsługuje presenter, jest obiektem [api:Nette\Application\Request] i zwraca go metoda presentera `getRequest()`. +Żądanie obsługiwane przez presenter to obiekt [api:Nette\Application\Request], zwracany metodą presentera `getRequest()`. -Aktualne żądanie można zapisać do sesji lub odwrotnie, odtworzyć z niej i pozwolić presenterowi ponownie je wykonać. Przydaje się to na przykład w sytuacji, gdy użytkownik wypełnia formularz i wygaśnie mu sesja logowania. Aby nie stracił danych, przed przekierowaniem na stronę logowania aktualne żądanie zapisujemy do sesji za pomocą `$reqId = $this->storeRequest()`, które zwraca jego identyfikator w postaci krótkiego ciągu znaków, a ten przekazujemy jako parametr do presentera logowania. +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 i forwarduje na nie. Metoda przy tym weryfikuje, czy żądanie utworzył ten sam użytkownik, który się teraz zalogował. Jeśli zalogowałby się inny użytkownik lub klucz byłby nieprawidłowy, nie zrobi nic i program kontynuuje dalej. +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 poradnik [Jak wrócić do poprzedniej strony |best-practices:restore-request]. +Zobacz przewodnik [Jak wrócić do poprzedniej strony |best-practices:restore-request]. Kanonizacja ----------- -Presentery mają jedną naprawdę świetną cechę, która przyczynia się do lepszego SEO (optymalizacji dla wyszukiwarek internetowych). Automatycznie zapobiegają istnieniu duplikatów treści pod różnymi URL. Jeśli do określonego celu prowadzi więcej adresów URL, np. `/index` i `/index?page=1`, framework określa jeden z nich jako podstawowy (kanoniczny) i pozostałe na niego przekierowuje za pomocą kodu HTTP 301. Dzięki temu wyszukiwarki nie indeksują stron dwukrotnie i nie rozdrabniają ich page rank. +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. -Ten proces nazywa się kanonizacją. Kanonicznym URL jest ten, który generuje [router|routing], zazwyczaj więc pierwsza pasująca trasa w kolekcji. +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`. -Do przekierowania nie dochodzi przy żądaniu AJAX lub POST, ponieważ doszłoby do utraty danych lub nie miałoby to wartości dodanej z punktu widzenia SEO. +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żna wywołać również manualnie za pomocą metody `canonicalize()`, której podobnie jak metodzie `link()` przekazuje się presenter, akcję i parametry. Tworzy link i porównuje go z aktualnym adresem URL. Jeśli się różnią, przekierowuje na wygenerowany link. +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 @@ -391,31 +436,13 @@ public function actionShow(int $id, ?string $slug = null): void } ``` - -Zdarzenia ---------- - -Oprócz metod `startup()`, `beforeRender()` i `shutdown()`, które są wywoływane jako część cyklu życia presentera, można zdefiniować jeszcze inne funkcje, które mają być automatycznie wywoływane. Presenter definiuje tzw. [zdarzenia |nette:glossary#Eventy zdarzenia], których handlery dodasz do tablic `$onStartup`, `$onRender` i `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Handlery w tablicy `$onStartup` są wywoływane tuż przed metodą `startup()`, dalej `$onRender` między `beforeRender()` a `render()` i na końcu `$onShutdown` tuż przed `shutdown()`. +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ź, którą zwraca presenter, jest obiektem implementującym interfejs [api:Nette\Application\Response]. Dostępnych jest szereg gotowych 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 @@ -430,43 +457,103 @@ Odpowiedzi wysyła się metodą `sendResponse()`: ```php use Nette\Application\Responses; -// Zwykły tekst +// zwykły tekst $this->sendResponse(new Responses\TextResponse('Hello Nette!')); -// Wysyła plik +// wysyła plik $this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); -// Odpowiedzią będzie callback +// wysyła callback $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; + 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));` + -Ograniczenie dostępu za pomocą `#[Requires]` .{data-version:3.2.2} ------------------------------------------------------------------- +Cache HTTP +---------- -Atrybut `#[Requires]` zapewnia zaawansowane możliwości ograniczania dostępu do presenterów i ich metod. Można go użyć do specyfikacji metod HTTP, wymagania żądania AJAX, ograniczenia do tego samego pochodzenia (same origin) oraz dostępu tylko przez forwardowanie. Atrybut można stosować zarówno do klas presenterów, jak i do poszczególnych metod `action()`, `render()`, `handle()` i `createComponent()`. +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: -Można określić te ograniczenia: +```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)]` -- ograniczenie do konkretnych akcji: `#[Requires(actions: 'default')]` +- 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 poradniku [Jak używać atrybutu Requires |best-practices:attribute-requires]. +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. Powodem tej kontroli jest przede wszystkim bezpieczeństwo. Standardowo dozwolone są metody `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. +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 zezwolić na przykład na metodę `OPTIONS`, użyj do tego atrybutu `#[Requires]` (od Nette Application v3.2): +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'])] @@ -475,26 +562,34 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -W wersji 3.1 weryfikacja odbywa się w `checkHttpMethod()`, która sprawdza, czy metoda określona w żądaniu jest zawarta w tablicy `$presenter->allowedMethods`. Dodanie metody wykonaj w ten sposób: +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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -Ważne jest podkreślenie, że jeśli zezwolisz na metodę `OPTIONS`, musisz ją następnie również odpowiednio obsłużyć w ramach swojego presentera. Metoda jest często używana jako tzw. preflight request, który przeglądarka automatycznie wysyła przed rzeczywistym żądaniem, gdy trzeba sprawdzić, czy żądanie jest dozwolone z punktu widzenia polityki CORS (Cross-Origin Resource Sharing). Jeśli zezwolisz na metodę, ale nie zaimplementujesz prawidłowej odpowiedzi, może to prowadzić do niespójności i potencjalnych problemów bezpieczeństwa. +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 |best-practices:presenter-traits] +- [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 index 49f68aa093..b93081a766 100644 --- a/application/pl/routing.texy +++ b/application/pl/routing.texy @@ -1,28 +1,28 @@ -Routowanie -********** +Routing +*******
    -Router odpowiada za wszystko związane z adresami URL, abyś Ty już nie musiał się nad nimi zastanawiać. Pokażemy: +Router zajmuje się wszystkim, co dotyczy adresów URL, dzięki czemu nie musisz o nich myśleć. Pokażemy Ci: -- jak ustawić router, aby URL były zgodne z oczekiwaniami -- powiemy o SEO i przekierowaniach +- 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 ludzkie URL (lub też cool czy pretty URL) są bardziej użyteczne, łatwiejsze do zapamiętania i pozytywnie wpływają na SEO. Nette o tym myśli i w pełni wychodzi naprzeciw deweloperom. Możesz dla swojej aplikacji zaprojektować dokładnie taką strukturę adresów URL, jaką będziesz chciał. Możesz ją zaprojektować nawet w chwili, gdy aplikacja jest już gotowa, ponieważ obejdzie się to bez ingerencji w kod czy szablony. Definiuje się ją bowiem w elegancki sposób w jednym [jedynym miejscu |#Włączenie do aplikacji], w routerze, i nie jest rozproszona w formie adnotacji we wszystkich presenterach. +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 tym, że jest **dwukierunkowy.** Potrafi zarówno dekodować URL w żądaniu HTTP, jak i tworzyć linki. Odgrywa więc kluczową rolę w [Nette Application |how-it-works#Nette Application], ponieważ nie tylko decyduje o tym, który presenter i akcja będzie wykonywać aktualne żądanie, ale także wykorzystuje się go do [generowania URL |creating-links] w szablonie itp. +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. -Jednak router nie jest ograniczony tylko do tego zastosowania, można go używać w aplikacjach, gdzie w ogóle nie używa się presenterów, dla REST API, itd. Więcej w części [#Samostatné použití]. +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 postaci adresów URL w aplikacji oferuje klasa [api:Nette\Application\Routers\RouteList]. Definicja składa się z listy tzw. tras (routes), czyli masek adresów URL i przypisanych do nich presenterów i akcji za pomocą prostego API. Tras nie musimy w żaden sposób nazywać. +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; @@ -31,16 +31,16 @@ $router->addRoute('article/', 'Article:view'); // ... ``` -Przykład mówi, że jeśli w przeglądarce otworzymy `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. W przypadku nieznalezienia odpowiedniej trasy Nette Application reaguje wyrzuceniem wyjątku [BadRequestException |api:Nette\Application\BadRequestException], który użytkownikowi wyświetli się jako strona błędu 404 Not Found. +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 -------------- -Absolutnie **kluczowa jest kolejność**, w jakiej są wymienione poszczególne trasy, ponieważ są one ewaluowane kolejno od góry do dołu. Obowiązuje zasada, że trasy deklarujemy **od szczegółowych do ogólnych**: +**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' przechwyci pierwsza trasa i rozumie ten ciąg jako +// ŹLE: 'rss.xml' przechwycone zostanie przez pierwszą trasę i zrozumiane jako $router->addRoute('', 'Article:view'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -49,10 +49,10 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('', 'Article:view'); ``` -Trasy są ewaluowane od góry do dołu również przy generowaniu linków: +Trasy obliczane są od góry do dołu również przy generowaniu odnośników: ```php -// ŹLE: link do 'Feed:rss' wygeneruje jako 'admin/feed/rss' +// ŹLE: odnośnik do 'Feed:rss' wygeneruje się jako 'admin/feed/rss' $router->addRoute('admin//', 'Admin:default'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -61,57 +61,57 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('admin//', 'Admin:default'); ``` -Nie będziemy przed Tobą ukrywać, że prawidłowe zestawienie tras wymaga pewnej wprawy. Zanim ją opanujesz, użytecznym pomocnikiem będzie [panel routingu |#Debugowanie routera]. +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 strony internetowej. Najprostszą maską jest statyczny URL: +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ą tzw. **parametry**. Są one podane w nawiasach ostrych (np. ``) i są przekazywane do docelowego presentera, na przykład do metody `renderShow(int $year)` lub do trwałego parametru `$year`: +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 mówi, że jeśli w przeglądarce otworzymy `https://example.com/chronicle/2020`, wyświetli się presenter `History` z akcją `show` i parametrem `year: 2020`. +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`. -Parametrom możemy określić wartość domyślną bezpośrednio w masce i tym samym stają się one opcjonalne: +Wartość domyślną parametrów możemy podać bezpośrednio w masce, czyniąc je opcjonalnymi: ```php $router->addRoute('chronicle/', 'History:show'); ``` -Trasa będzie teraz akceptować również URL `https://example.com/chronicle/`, które ponownie wyświetli `History:show` z parametrem `year: 2020`. +Trasa przyjmie teraz również URL `https://example.com/chronicle/`, który znów wyświetli `History:show` z parametrem `year: 2020`. -Parametrem może być oczywiście również nazwa presentera i akcji. Na przykład tak: +Parametrami mogą być oczywiście również nazwy presentera i akcji. Na przykład: ```php $router->addRoute('/', 'Home:default'); ``` -Podana trasa akceptuje np. URL w postaci `/article/edit` lub także `/catalog/list` i rozumie je jako presentery i akcje `Article:edit` i `Catalog:list`. +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`, a zatem są one również opcjonalne. Tak więc trasa akceptuje również URL w postaci `/article` i rozumie go jako `Article:default`. Lub odwrotnie, link do `Product:default` wygeneruje ścieżkę `/product`, link do domyślnego `Home:default` ścieżkę `/`. +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 strony internetowej, ale także ścieżkę absolutną, jeśli zaczyna się od ukośnika, lub nawet cały absolutny URL, jeśli zaczyna się od dwóch ukośników: +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ędnie do document root +// względem document root $router->addRoute('/', /* ... */); -// ścieżka absolutna (względna do domeny) +// ścieżka bezwzględna (względem domeny) $router->addRoute('//', /* ... */); -// absolutny URL włącznie z domeną (względny do schematu) +// bezwzględny URL wraz z domeną (względem schematu) $router->addRoute('//.example.com//', /* ... */); -// absolutny URL włącznie ze schematem +// bezwzględny URL wraz ze schematem $router->addRoute('https://.example.com//', /* ... */); ``` @@ -119,16 +119,16 @@ $router->addRoute('https://.example.com//', /* ... */); Wyrażenia walidacyjne --------------------- -Dla każdego parametru można ustalić 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ślimy, że może przyjmować tylko cyfry za pomocą wyrażenia regularnego `\d+`: +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 `[^/]+`, tj. wszystko oprócz ukośnika. Jeśli parametr ma akceptować również ukośniki, podamy wyrażenie `.+`: +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 -// akceptuje https://example.com/a/b/c, path będzie 'a/b/c' +// przyjmuje https://example.com/a/b/c, path będzie 'a/b/c' $router->addRoute('', /* ... */); ``` @@ -136,25 +136,25 @@ $router->addRoute('', /* ... */); Sekwencje opcjonalne -------------------- -W masce można oznaczać opcjonalne części za pomocą nawiasów kwadratowych. Opcjonalna może być dowolna część maski, mogą się w niej znajdować również parametry: +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('[/]', /* ... */); -// Akceptuje ścieżki: -// /cs/download => lang => cs, name => download +// przyjmuje ścieżki: +// /en/download => lang => en, name => download // /download => lang => null, name => download ``` -Gdy parametr jest częścią sekwencji opcjonalnej, staje się oczywiście również opcjonalny. Jeśli nie ma podanej wartości domyślnej, będzie miał wartość null. +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. -Opcjonalne części mogą być również w domenie: +Części opcjonalne mogą znajdować się także w domenie: ```php $router->addRoute('//[.]example.com//', /* ... */); ``` -Sekwencje można dowolnie zagnieżdżać i kombinować: +Sekwencje można dowolnie zagnieżdżać i łączyć: ```php $router->addRoute( @@ -162,24 +162,24 @@ $router->addRoute( 'Home:default', ); -// Akceptuje ścieżki: -// /cs/hello +// przyjmuje ścieżki: +// /en/hello // /en-us/hello // /hello // /hello/page-12 ``` -Przy generowaniu URL dąży się do najkrótszej warianty, więc wszystko, co można pominąć, jest pomijane. Dlatego na przykład trasa `index[.html]` generuje ścieżkę `/index`. Odwrócić zachowanie można przez podanie wykrzyknika za lewym nawiasem kwadratowym: +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 -// akceptuje /hello i /hello.html, generuje /hello +// przyjmuje /hello i /hello.html, generuje /hello $router->addRoute('[.html]', /* ... */); -// akceptuje /hello i /hello.html, generuje /hello.html +// przyjmuje /hello i /hello.html, generuje /hello.html $router->addRoute('[!.html]', /* ... */); ``` -Parametry opcjonalne (tj. parametry mające wartość domyślną) bez nawiasów kwadratowych zachowują się w zasadzie tak, jakby były ujęte w nawiasy w następujący sposób: +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('//', /* ... */); @@ -188,7 +188,7 @@ $router->addRoute('//', /* ... */); $router->addRoute('[/[/[]]]', /* ... */); ``` -Jeśli chcielibyśmy wpłynąć na zachowanie końcowego ukośnika, aby np. zamiast `/home/` generowało się tylko `/home`, można to osiągnąć w ten sposób: +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('[[/[/]]]', /* ... */); @@ -198,24 +198,24 @@ $router->addRoute('[[/[/]]]', /* ... */); Symbole wieloznaczne -------------------- -W masce ścieżki absolutnej możemy użyć następujących symboli wieloznacznych i uniknąć w ten sposób np. konieczności zapisywania w masce domeny, która może się różnić w środowisku deweloperskim i produkcyjnym: +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%` = top level domain, np. `com` lub `org` -- `%sld%` = second level domain, np. `example` +- `%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%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` -Zapis rozszerzony ------------------ +Zapis zaawansowany +------------------ -Cel trasy, zazwyczaj zapisywany w postaci `Presenter:action`, może być również zapisany za pomocą tablicy, która definiuje poszczególne parametry i ich wartości domyślne: +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('/[/]', [ @@ -224,7 +224,7 @@ $router->addRoute('/[/]', [ ]); ``` -Dla bardziej szczegółowej specyfikacji można użyć jeszcze bardziej rozszerzonej formy, gdzie oprócz wartości domyślnych możemy ustawić również inne właściwości parametrów, takie jak na przykład walidacyjne wyrażenie regularne (zobacz parametr `id`): +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; @@ -242,19 +242,29 @@ $router->addRoute('/[/]', [ ]); ``` -Ważne jest zauważenie, że jeśli parametry zdefiniowane w tablicy nie są wymienione w masce ścieżki, ich wartości nie można zmienić, nawet za pomocą parametrów query podanych za znakiem zapytania w URL. +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 -------------------- -Kody źródłowe aplikacji piszemy w języku angielskim, ale jeśli strona ma mieć polskie URL, to proste routowanie typu: +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'); ``` -będzie generować angielskie URL, takie jak `/product/123` lub `/cart`. Jeśli chcemy mieć presentery i akcje w URL reprezentowane polskimi słowami (np. `/produkt/123` lub `/koszyk`), możemy wykorzystać słownik tłumaczeń. Do jego zapisu potrzebujemy już "bardziej gadatliwej" warianty drugiego parametru: +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; @@ -263,26 +273,26 @@ $router->addRoute('/', [ 'presenter' => [ Route::Value => 'Home', Route::FilterTable => [ - // ciąg w URL => presenter + // string w URL => presenter 'produkt' => 'Product', - 'koszyk' => 'Cart', + 'kosik' => 'Cart', 'katalog' => 'Catalog', ], ], 'action' => [ Route::Value => 'default', Route::FilterTable => [ - 'lista' => 'list', + 'seznam' => 'list', ], ], ]); ``` -Wiele kluczy słownika tłumaczeń może prowadzić do tego samego presentera. W ten sposób tworzy się dla niego różne aliasy. Za wariant kanoniczny (czyli ten, który będzie w wygenerowanym URL) uważa się ostatni klucz. +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). -Tabelę tłumaczeń można w ten sposób użyć dla dowolnego parametru. Przy czym jeśli tłumaczenie nie istnieje, bierze się pierwotną wartość. To zachowanie możemy zmienić dodając `Route::FilterStrict => true` i trasa wtedy odrzuci URL, jeśli wartość nie jest w słowniku. +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. -Oprócz słownika tłumaczeń w postaci tablicy można zastosować również własne funkcje tłumaczące. +Poza słownikiem tłumaczeń w postaci tablicy można zastosować własne funkcje tłumaczące. ```php use Nette\Routing\Route; @@ -298,15 +308,15 @@ $router->addRoute('//', [ ]); ``` -Funkcja `Route::FilterIn` konwertuje między parametrem w URL a ciągiem, który następnie jest przekazywany do presentera, funkcja `FilterOut` zapewnia konwersję w przeciwnym kierunku. +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` już mają predefiniowane filtry, które konwertują między stylem PascalCase resp. camelCase a kebab-case używanym w URL. Wartość domyślna parametrów zapisuje się już w przekształconej postaci, więc na przykład w przypadku presentera piszemy ``, a nie ``. +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 ------------- -Oprócz filtrów przeznaczonych dla konkretnych parametrów możemy zdefiniować również filtry ogólne, które otrzymają tablicę asocjacyjną wszystkich parametrów, które mogą dowolnie modyfikować, a następnie je zwrócą. Filtry ogólne definiujemy pod kluczem `null`. +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; @@ -314,72 +324,82 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -Filtry ogólne dają możliwość modyfikacji zachowania trasy w absolutnie dowolny sposób. Możemy je użyć na przykład do modyfikacji parametrów na podstawie innych parametrów. Na przykład tłumaczenie `` i `` na podstawie aktualnej wartości parametru ``. +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. -Jeśli parametr ma zdefiniowany własny filtr i jednocześnie istnieje filtr ogólny, wykonuje się własny `FilterIn` przed ogólnym i odwrotnie ogólny `FilterOut` przed własnym. Zatem wewnątrz filtra ogólnego wartości parametrów `presenter` resp. `action` są zapisane w stylu PascalCase resp. 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]. -Jednokierunkowe OneWay ----------------------- +Flaga OneWay +------------ -Trasy jednokierunkowe używa się do zachowania funkcjonalności starych URL, których aplikacja już nie generuje, ale nadal akceptuje. Oznaczamy je flagą `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 -// stare URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// nowe URL /product/123 +// stary URL /product-info?id=123 +$router->addRoute('product-info', 'Product:detail', oneWay: true); +// nowy URL /product/123 $router->addRoute('product/', 'Product:detail'); ``` -Przy dostępie do starego URL presenter automatycznie przekierowuje na nowy URL, dzięki czemu wyszukiwarki nie zaindeksują tych stron dwukrotnie (zobacz [#SEO i kanonizacja]). +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]). -Dynamiczne routowanie z callbackami ------------------------------------ +Routing dynamiczny z callbackami +-------------------------------- -Dynamiczne routowanie z callbackami pozwala przypisać trasom bezpośrednio funkcje (callbacki), które zostaną wykonane, gdy dana ścieżka zostanie odwiedzona. Ta elastyczna funkcjonalność pozwala szybko i efektywnie tworzyć różne punkty końcowe (endpoints) dla Twojej aplikacji: +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'; + echo 'Jesteś pod adresem /test'; }); ``` -Możesz również zdefiniować w masce parametry, które zostaną automatycznie przekazane do Twojego callbacku: +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' => 'Witaj na czeskiej wersji naszej strony!', - 'en' => 'Welcome to the English version of our website!', + '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 więcej tras, które należą do wspólnego [modułu |directory-structure#Presentery i szablony], wykorzystamy `withModule()`: +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') // następujące trasy są częścią modułu Forum - ->addRoute('rss', 'Feed:rss') // presenter będzie Forum:Feed +$router->withModule('Forum') // kolejne trasy są częścią modułu Forum + ->addRoute('rss', 'Feed:rss') // presenterem będzie Forum:Feed ->addRoute('/') - ->withModule('Admin') // następujące trasy są częścią modułu Forum:Admin + ->withModule('Admin') // kolejne trasy są częścią modułu Forum:Admin ->addRoute('sign:in', 'Sign:in'); ``` -Alternatywą jest użycie parametru `module`: +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 @@ -388,11 +408,17 @@ $router->addRoute('manage//', [ ]); ``` +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żemy dzielić według subdomen: +Kolekcje tras można podzielić według subdomen: ```php $router = new RouteList; @@ -413,20 +439,20 @@ $router->withDomain('example.%tld%') Prefiks ścieżki --------------- -Kolekcje tras możemy dzielić według ścieżki w URL: +Kolekcje tras można podzielić według ścieżki w URL: ```php $router = new RouteList; $router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // łapie URL /eshop/rss - ->addRoute('/'); // łapie URL /eshop// + ->addRoute('rss', 'Feed:rss') // pasuje do URL /eshop/rss + ->addRoute('/'); // pasuje do URL /eshop// ``` Kombinacje ---------- -Powyższe podziały możemy wzajemnie kombinować: +Powyższe grupowania można ze sobą łączyć: ```php $router = (new RouteList) @@ -446,37 +472,37 @@ $router = (new RouteList) ``` -Parametry Query ---------------- +Parametry zapytania +------------------- -Maski mogą również zawierać parametry query (parametry za znakiem zapytania w URL). Nie można im zdefiniować wyrażenia walidacyjnego, ale można zmienić nazwę, pod którą zostaną przekazane do presentera: +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 -// parametr query 'cat' chcemy w aplikacji użyć pod nazwą 'categoryId' +// chcemy używać parametru zapytania 'cat' w aplikacji pod nazwą 'categoryId' $router->addRoute('product ? id= & cat=', /* ... */); ``` -Parametry Foo +Parametry foo ------------- -Teraz już idziemy głębiej. Parametry Foo to w zasadzie nienazwane parametry, które umożliwiają dopasowanie wyrażenia regularnego. Przykładem jest trasa akceptująca `/index`, `/index.html`, `/index.htm` i `/index.php`: +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 również jawnie zdefiniować ciąg, który będzie użyty przy generowaniu URL. Ciąg musi być umieszczony bezpośrednio za znakiem zapytania. Następująca trasa jest podobna do poprzedniej, ale generuje `/index.html` zamiast `/index`, ponieważ ciąg `.html` jest ustawiony jako wartość generująca: +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', /* ... */); ``` -Włączenie do aplikacji -====================== +Integracja +========== -Aby włączyć utworzony router do aplikacji, musimy o nim powiedzieć kontenerowi DI. Najłatwiejszą drogą jest przygotowanie fabryki, która wyprodukuje obiekt routera, i poinformowanie w konfiguracji kontenera, że ma jej użyć. Powiedzmy, że w tym celu napiszemy metodę `App\Core\RouterFactory::createRouter()`: +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; @@ -494,14 +520,14 @@ class RouterFactory } ``` -Do [konfiguracji |dependency-injection:services] następnie zapiszemy: +Następnie zapisujemy w [konfiguracji |dependency-injection:services]: ```neon services: - App\Core\RouterFactory::createRouter ``` -Wszelkie zależności, na przykład od bazy danych itp., zostaną przekazane do metody fabrycznej jako jej parametry za pomocą [autowiringu|dependency-injection:autowiring]: +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 @@ -514,22 +540,22 @@ public static function createRouter(Nette\Database\Connection $db): RouteList SimpleRouter ============ -Znacznie prostszym routerem niż kolekcja tras jest [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Użyjemy go wtedy, gdy nie mamy szczególnych wymagań co do kształtu URL, gdy nie jest dostępny `mod_rewrite` (lub jego alternatywy) lub gdy na razie nie chcemy zajmować się ładnymi URL. +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 tym kształcie: +Generuje adresy mniej więcej w takiej postaci: ``` http://example.com/?presenter=Product&action=detail&id=123 ``` -Parametrem konstruktora SimpleRoutera jest domyślny presenter & akcja, na który ma kierować, jeśli otworzymy stronę bez parametrów, np. `http://example.com/`. +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 akcja 'default' +// domyślnym presenterem będzie 'Home', a akcją 'default' $router = new Nette\Application\Routers\SimpleRouter('Home:default'); ``` -Zalecamy SimpleRouter bezpośrednio zdefiniować w [konfiguracji |dependency-injection:services]: +Zalecamy definiowanie SimpleRoutera bezpośrednio w [konfiguracji |dependency-injection:services]: ```neon services: @@ -540,19 +566,19 @@ services: SEO i kanonizacja ================= -Framework przyczynia się do SEO (optymalizacji dla wyszukiwarek internetowych) przez zapobieganie duplikacji treści pod różnymi URL. Jeśli do określonego celu prowadzi więcej adresów, np. `/index` i `/index.html`, framework pierwszy z nich określa jako podstawowy (kanoniczny) i pozostałe na niego przekierowuje za pomocą kodu HTTP 301. Dzięki temu wyszukiwarki nie indeksują stron dwukrotnie i nie rozdrabniają ich page rank. +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. -Ten proces nazywa się kanonizacją. Kanonicznym URL jest ten, który generuje router, tj. pierwsza pasująca trasa w kolekcji bez flagi OneWay. Dlatego w kolekcji podajemy **podstawowe trasy jako pierwsze**. +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ę przeprowadza presenter, więcej w rozdziale [kanonizacja |presenters#Kanonizacja]. +Kanonizację wykonuje presenter, więcej w rozdziale [kanonizacja |presenters#Kanonizacja]. HTTPS ===== -Aby móc używać protokołu HTTPS, konieczne jest jego włączenie na hostingu i prawidłowe skonfigurowanie serwera. +Aby używać protokołu HTTPS, trzeba włączyć go na hostingu i poprawnie skonfigurować serwer. -Przekierowanie całej strony na HTTPS należy ustawić na poziomie serwera, na przykład za pomocą pliku .htaccess w katalogu głównym naszej aplikacji, i to z kodem HTTP 301. Ustawienie może się różnić w zależności od hostingu i wygląda mniej więcej tak: +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: ``` @@ -564,15 +590,15 @@ Przekierowanie całej strony na HTTPS należy ustawić na poziomie serwera, na p ``` -Router generuje URL z tym samym protokołem, z jakim została załadowana strona, więc nic więcej nie trzeba ustawiać. +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, podamy go w masce trasy: +Jeśli jednak wyjątkowo potrzebujemy, aby różne trasy działały pod różnymi protokołami, podajemy to w masce trasy: ```php -// Będzie generować adres z HTTP +// wygeneruje adres HTTP $router->addRoute('http://%host%//', /* ... */); -// Będzie generować adres z HTTPS +// wygeneruje adres HTTPS $router->addRoute('https://%host%//', /* ... */); ``` @@ -580,24 +606,24 @@ $router->addRoute('https://%host%//', /* ... */); Debugowanie routera =================== -Panel routingu wyświetlający się w [Tracy Bar |tracy:] jest użytecznym pomocnikiem, który wyświetla listę tras oraz parametrów, które router uzyskał z URL. +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 przetworzyła aktualny URL, niebieskim kolorem i symbolem ≈ są oznaczone trasy, które również przetworzyłyby URL, gdyby zielona ich nie wyprzedziła. Dalej widzimy aktualny presenter & akcję. +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 spojrzeć do panelu w pasku *redirect*, gdzie dowiesz się, jak router pierwotnie zrozumiał URL i dlaczego przekierował. +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] -Podczas debugowania routera zalecamy otwarcie w przeglądarce Developer Tools (Ctrl+Shift+I lub Cmd+Option+I) i w panelu Network wyłączenie cache, aby nie zapisywały się w niej przekierowania. +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 ma wpływ na szybkość routera. Ich liczba zdecydowanie nie powinna przekraczać kilkudziesięciu. Jeśli Twoja strona ma zbyt skomplikowaną strukturę URL, możesz napisać na miarę [#Własny router]. +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 żadnych zależności, na przykład od bazy danych, a jego fabryka nie przyjmuje żadnych argumentów, możemy jego skompilowaną postać zserializować bezpośrednio do kontenera DI i tym samym nieznacznie przyspieszyć aplikację. +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: @@ -608,7 +634,7 @@ routing: Własny router ============= -Poniższe linijki są przeznaczone dla bardzo zaawansowanych użytkowników. Możesz stworzyć własny router i całkowicie naturalnie włączyć go do kolekcji tras. Router jest implementacją interfejsu [api:Nette\Routing\Router] z dwiema metodami: +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; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -Metoda `match` przetwarza aktualne żądanie [$httpRequest |http:request], z którego można uzyskać nie tylko URL, ale i nagłówki itp., do tablicy zawierającej nazwę presentera i jego parametry. Jeśli nie potrafi przetworzyć żądania, zwraca null. Przy przetwarzaniu żądania musimy zwrócić co najmniej presenter i akcję. Nazwa presentera jest pełna i zawiera również ewentualne moduły: +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 [ @@ -637,9 +663,9 @@ Metoda `match` przetwarza aktualne żądanie [$httpRequest |http:request], z kt ] ``` -Metoda `constructUrl` odwrotnie, składa z tablicy parametrów wynikowy absolutny URL. Do tego może wykorzystać informacje z parametru [`$refUrl`|api:Nette\Http\UrlScript], który jest aktualnym URL. +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. -Do kolekcji tras dodasz go za pomocą `add()`: +Dodasz go do kolekcji tras metodą `add()`: ```php $router = new Nette\Application\Routers\RouteList; @@ -649,16 +675,16 @@ $router->addRoute(/* ... */); ``` -Samostatné použití +Użycie samodzielne ================== -Samodzielnym użyciem rozumiemy wykorzystanie możliwości routera w aplikacji, która nie wykorzystuje Nette Application i presenterów. Dotyczy go prawie wszystko, co pokazaliśmy w tym rozdziale, z tymi różnicami: +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 simple router klasy [api:Nette\Routing\SimpleRouter] -- ponieważ nie istnieje para `Presenter:action`, używamy [#Zapis rozszerzony] +- jako prostego routera klasy [api:Nette\Routing\SimpleRouter] +- ponieważ para `Presenter:akcja` nie istnieje, używamy [#Zapis zaawansowany] -Więc ponownie tworzymy metodę, która nam zbuduje router, np.: +Znów tworzymy więc metodę, która złoży nam router, np.: ```php namespace App\Core; @@ -682,21 +708,21 @@ class RouterFactory } ``` -Jeśli używasz kontenera DI, co zalecamy, ponownie dodamy metodę do konfiguracji, a następnie router wraz z żądaniem HTTP uzyskamy z kontenera: +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 obiekty bezpośrednio wyprodukujemy: +Albo utwórz obiekty bezpośrednio: ```php $router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` -Teraz już pozostaje puścić router do pracy: +Teraz pozostaje już tylko pozwolić routerowi wykonać swoją pracę: ```php $params = $router->match($httpRequest); @@ -705,12 +731,12 @@ if ($params === null) { exit; } -// przetwarzamy uzyskane parametry +// przetwarzamy pozyskane parametry $controller = $params['controller']; // ... ``` -I odwrotnie użyjemy routera do zbudowania linku: +I odwrotnie, użyj routera do złożenia odnośnika: ```php $params = ['controller' => 'ArticleController', 'id' => 123]; @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/pl/templates.texy b/application/pl/templates.texy index 496242c1d0..790c08acc3 100644 --- a/application/pl/templates.texy +++ b/application/pl/templates.texy @@ -2,15 +2,15 @@ Szablony ******** .[perex] -Nette używa systemu szablonów [Latte |latte:]. Po pierwsze dlatego, że jest to najlepiej zabezpieczony system szablonów dla PHP, a jednocześnie system najbardziej intuicyjny. Nie musisz uczyć się wielu nowych rzeczy, wystarczy znajomość PHP i kilku znaczników. +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. -Jest typowe, że strona składa się z szablonu layoutu + szablonu danej akcji. Tak na przykład może wyglądać szablon layoutu, zwróć uwagę na bloki `{block}` i znacznik `{include}`: +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} + {block title}Moja aplikacja{/block}
    ...
    @@ -20,7 +20,7 @@ Jest typowe, że strona składa się z szablonu layoutu + szablonu danej akcji. ``` -A to będzie szablon akcji: +A tak wyglądałby szablon akcji: ```latte {block title}Strona główna{/block} @@ -31,15 +31,15 @@ A to będzie szablon akcji: {/block} ``` -Definiuje on blok `content`, który zostanie wstawiony w miejsce `{include content}` w layoucie, a także redefiniuje blok `title`, którym nadpisze `{block title}` w layoucie. Spróbuj sobie wyobrazić wynik. +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 ---------------------- -Nie musisz w presenterach podawać, jaki szablon ma być wyrenderowany, framework sam wywnioskuje ścieżkę i oszczędzi Ci pisania. +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, gdzie każdy presenter ma własny katalog, po prostu umieść szablon w tym katalogu pod nazwą akcji (resp. view), tj. dla akcji `default` użyj szablonu `default.latte`: +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/ @@ -49,34 +49,34 @@ app/ └── default.latte \-- -Jeśli używasz struktury, gdzie presentery są razem w jednym katalogu, a szablony w folderze `templates`, zapisz go albo w pliku `..latte` albo `/.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 + ├── Home/ + │ └── default.latte ← 1. wariant + └── Home.default.latte ← 2. wariant \-- -Katalog `templates` może być umieszczony również o poziom wyżej, tj. na tym samym poziomie, co katalog z klasami presenterów. +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 nie zostanie znaleziony, presenter odpowie [błędem 404 - page not found |presenters#Błąd 404 i spółka]. +Jeśli szablon się nie znajdzie, presenter odpowiada [błędem 404 - strona nie znaleziona |presenters#Błąd 404 itd.]. -View zmienisz za pomocą `$this->setView('innyView')`. Można również bezpośrednio określić plik z szablonem za pomocą `$this->template->setFile('/path/to/template.latte')`. +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ć przez nadpisanie metody [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], która zwraca tablicę możliwych nazw plików. +Pliki, w których wyszukiwane są szablony, można zmienić, nadpisując metodę [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], która zwraca tablicę możliwych nazw plików. Wyszukiwanie szablonu layoutu ----------------------------- -Nette również automatycznie wyszukuje plik z layoutem. +Nette automatycznie wyszukuje również plik layoutu. -Jeśli używasz struktury katalogów, gdzie każdy presenter ma własny katalog, umieść layout albo w folderze z presenterem, jeśli jest specyficzny tylko dla niego, albo o poziom wyżej, jeśli jest wspólny dla wielu presenterów: +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/ @@ -88,7 +88,7 @@ app/ └── default.latte \-- -Jeśli używasz struktury, gdzie presentery są razem w jednym katalogu, a szablony w folderze `templates`, layout będzie oczekiwany w tych miejscach: +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/ @@ -96,33 +96,66 @@ app/ ├── HomePresenter.php └── templates/ ├── @layout.latte ← wspólny layout - ├── Home.@layout.latte ← tylko dla Home, 1. wariant - └── Home/ - └── @layout.latte ← tylko dla Home, 2. wariant + ├── Home/ + │ └── @layout.latte ← tylko dla Home, 1. wariant + └── Home.@layout.latte ← tylko dla Home, 2. wariant \-- -Jeśli presenter znajduje się w module, będzie wyszukiwany również o kolejne poziomy katalogów wyżej, zgodnie z zagnieżdżeniem modułu. +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ć za pomocą `$this->setLayout('layoutAdmin')`, a wtedy będzie oczekiwany w pliku `@layoutAdmin.latte`. Można również bezpośrednio określić plik z szablonem layoutu za pomocą `$this->setLayout('/path/to/template.latte')`. +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')`. -Za pomocą `$this->setLayout(false)` lub znacznika `{layout none}` wewnątrz szablonu wyszukiwanie layoutu zostanie wyłączone. +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ć przez nadpisanie metody [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], która zwraca tablicę możliwych nazw plików. +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 w szablonie -------------------- +Zmienne szablonu +---------------- -Zmienne do szablonu przekazujemy tak, że zapisujemy je do `$this->template`, a potem mamy je dostępne w szablonie jako zmienne lokalne: +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); ``` -W ten prosty sposób możemy przekazać do szablonów dowolne zmienne. Jednak przy tworzeniu solidnych aplikacji bywa bardziej użyteczne ograniczenie się. Na przykład tak, że jawnie zdefiniujemy wykaz zmiennych, których oczekuje szablon, oraz ich typów. Dzięki temu PHP będzie mógł kontrolować typy, IDE poprawnie podpowiadać, a analiza statyczna wykrywać błędy. +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 +-------------------------- -A jak taki wykaz zdefiniujemy? Po prostu w postaci klasy i jej właściwości. Nazwiemy ją podobnie jak presenter, tylko z `Template` na końcu: +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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -Obiekt `$this->template` w presenterze będzie teraz instancją klasy `ArticleTemplate`. Więc PHP podczas zapisu będzie kontrolował zadeklarowane typy. A począwszy od wersji PHP 8.2 powiadomi również o zapisie do nieistniejącej zmiennej, w poprzednich wersjach tego samego można osiągnąć używając traity [Nette\SmartObject |utils:smartobject]. +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` jest przeznaczona dla IDE i analizy statycznej, dzięki niej będzie działać podpowiadanie, zobacz "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-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 *] -Luksusu podpowiadania możesz sobie pozwolić również w szablonach, wystarczy zainstalować w PhpStorm wtyczkę dla Latte i podać na początku szablonu nazwę klasy, więcej w artykule "Latte: jak na typowy system":https://blog.nette.org/pl/latte-how-to-use-type-system: +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} ... ``` -Tak działają również szablony w komponentach, wystarczy tylko przestrzegać konwencji nazewnictwa i dla komponentu np. `FifteenControl` utworzyć klasę szablonu `FifteenTemplate`. +To samo dotyczy komponentów. Wystarczy trzymać się konwencji nazewniczej i dla komponentu w rodzaju `FifteenControl` utworzyć klasę parametrów `FifteenTemplate`. -Jeśli potrzebujesz utworzyć `$template` jako instancję innej klasy, wykorzystaj metodę `createTemplate()`: +Jeśli potrzebujesz użyć innej klasy parametrów, skorzystaj z metody `createTemplate()`: ```php public function renderDefault(): void @@ -168,58 +203,100 @@ public function renderDefault(): void } ``` +.{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: -Zmienne domyślne ----------------- - -Presentery i komponenty przekazują do szablonów kilka użytecznych zmiennych automatycznie: - -- `$basePath` to absolutna ścieżka URL do katalogu głównego (np. `/eshop`) -- `$baseUrl` to absolutny URL do katalogu głównego (np. `http://localhost/eshop`) -- `$user` to obiekt [reprezentujący użytkownika |security:authentication] -- `$presenter` to aktualny presenter -- `$control` to aktualny komponent lub presenter -- `$flashes` tablica [wiadomości |presenters#Wiadomości flash] wysłanych funkcją `flashMessage()` - -Jeśli używasz własnej klasy szablonu, te zmienne zostaną przekazane, jeśli utworzysz dla nich właściwość. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'Mój blog'; +} +``` -Tworzenie linków ----------------- +Tworzenie odnośników +-------------------- -W szablonie tworzy się linki do innych presenterów & akcji w ten sposób: +W szablonie odnośniki do innych presenterów i akcji tworzy się tak: ```latte szczegóły produktu ``` -Atrybut `n:href` jest bardzo przydatny dla znaczników HTML ``. Jeśli chcemy link wypisać gdzie indziej, na przykład w tekście, użyjemy `{link}`: +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 -Adres to: {link Home:default} +URL to: {link Home:default} ``` -Więcej informacji znajdziesz w rozdziale [Tworzenie linków URL|creating-links]. +Więcej informacji znajdziesz w rozdziale [Tworzenie odnośników URL|creating-links]. -Własne filtry, znaczniki itp. ------------------------------ +Własne filtry, tagi itd. +------------------------ -System szablonów Latte można rozszerzyć o własne filtry, funkcje, znaczniki itp. Można to zrobić bezpośrednio w metodzie `render` lub `beforeRender()`: +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 -public function beforeRender(): void +protected function beforeRender(): void { // dodanie filtra - $this->template->addFilter('foo', /* ... */); + $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}

    - // lub konfigurujemy bezpośrednio obiekt Latte\Engine +{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->addFilterLoader(/* ... */); + $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` -Latte w wersji 3 oferuje bardziej zaawansowany sposób, a mianowicie utworzenie sobie [extension |latte:extending-latte#Latte Extension] dla każdego projektu internetowego. Przykładowy fragment takiej klasy: +**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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -Zarejestrujemy ją za pomocą [konfiguracji |configuration#Szablony Latte]: +Rozszerzenie zarejestrujesz przez [konfigurację |configuration#Szablony Latte]: ```neon latte: @@ -263,13 +345,27 @@ latte: - 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ć niektóre teksty w szablonie wypisać w różnych językach. Nette Framework w tym celu definiuje interfejs do tłumaczenia [api:Nette\Localization\Translator], który ma jedyną metodę `translate()`. Przyjmuje ona wiadomość `$message`, co zazwyczaj jest ciągiem znaków, oraz dowolne inne parametry. Zadaniem jest zwrócenie przetłumaczonego ciągu. W Nette nie ma żadnej domyślnej implementacji, możesz wybrać według swoich potrzeb spośród kilku gotowych rozwiązań, które znajdziesz na [Componette |https://componette.org/search/localization]. W ich dokumentacji dowiesz się, jak konfigurować translator. +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 sobie [przekażemy |dependency-injection:passing-dependencies], metodą `setTranslator()`: +Szablonom można ustawić translator, który [otrzymamy wstrzyknięty |dependency-injection:passing-dependencies], metodą `setTranslator()`: ```php protected function beforeRender(): void @@ -279,7 +375,7 @@ protected function beforeRender(): void } ``` -Translator alternatywnie można ustawić za pomocą [konfiguracji |configuration#Szablony Latte]: +Alternatywnie translator można ustawić przez [konfigurację |configuration#Szablony Latte]: ```neon latte: @@ -287,7 +383,7 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Następnie można używać translatora na przykład jako filtra `|translate`, w tym z dodatkowymi parametrami, które zostaną przekazane metodzie `translate()` (zobacz `foo, bar`): +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} @@ -295,7 +391,7 @@ Następnie można używać translatora na przykład jako filtra `|translate`, w {$item|translate, foo, bar} ``` -Lub jako znacznika z podkreśleniem: +Albo jako tagu z podkreśleniem: ```latte {_'Koszyk'} @@ -303,14 +399,14 @@ Lub jako znacznika z podkreśleniem: {_$item, foo, bar} ``` -Do tłumaczenia fragmentu szablonu istnieje parzysty znacznik `{translate}` (od Latte 2.11, wcześniej używano znacznika `{_}`): +Do przetłumaczenia fragmentu szablonu służy tag parzysty `{translate}` (od Latte 2.11, wcześniej używano tagu `{_}`): ```latte -{translate}Zamówienie{/translate} -{translate foo, bar}Zamówienie{/translate} +{translate}Zamów{/translate} +{translate foo, bar}Zamów{/translate} ``` -Translator standardowo jest wywoływany w czasie rzeczywistym podczas renderowania szablonu. Latte w wersji 3 jednak potrafi wszystkie statyczne teksty tłumaczyć już podczas kompilacji szablonu. Tym samym oszczędza się wydajność, ponieważ każdy ciąg jest tłumaczony tylko raz, a wynikowe tłumaczenie jest zapisywane w skompilowanej postaci. W katalogu z cache powstaje więc więcej skompilowanych wersji szablonu, jedna dla każdego języka. Do tego wystarczy tylko podać język jako drugi parametr: +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 @@ -320,4 +416,4 @@ protected function beforeRender(): void } ``` -Tekstem statycznym jest na przykład `{_'hello'}` lub `{translate}hello{/translate}`. Teksty niestatyczne, jak na przykład `{_$foo}`, nadal będą tłumaczone w czasie rzeczywistym. +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 new file mode 100644 index 0000000000..e7ea4851fa --- /dev/null +++ b/application/pl/upgrading.texy @@ -0,0 +1,47 @@ +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/pt/@home.texy b/application/pt/@home.texy deleted file mode 100644 index 5f79b47fc4..0000000000 --- a/application/pt/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application é o núcleo do Nette Framework, que fornece ferramentas poderosas para criar aplicações web modernas. Oferece uma série de recursos excepcionais que facilitam significativamente o desenvolvimento e melhoram a segurança e a manutenção do código. - - -Instalação ----------- - -Faça o download e instale a biblioteca usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Porquê escolher Nette Application? ----------------------------------- - -Nette sempre foi pioneiro no campo das tecnologias web. - -**Roteador bidirecional:** Nette possui um sistema de roteamento avançado que é único pela sua bidirecionalidade - não só traduz URLs para ações da aplicação, mas também consegue gerar URLs de volta. Isso significa que: -- Pode alterar a estrutura de URLs de toda a aplicação a qualquer momento sem precisar de editar os templates -- As URLs são automaticamente canonizadas, o que melhora o SEO -- O roteamento é definido num único local, em vez de espalhado em anotações - -**Componentes e sinais:** O sistema de componentes integrado, inspirado no Delphi e React.js, é completamente excecional entre os frameworks PHP: -- Permite criar elementos de UI reutilizáveis -- Suporta composição hierárquica de componentes -- Oferece um tratamento elegante de requisições AJAX usando sinais -- Uma vasta biblioteca de componentes prontos em [Componette](https://componette.org) - -**AJAX e snippets:** Nette introduziu uma forma revolucionária de trabalhar com AJAX já em 2009, muito antes de soluções semelhantes como Hotwire para Ruby on Rails ou Symfony UX Turbo: -- Snippets permitem atualizar apenas partes da página sem a necessidade de escrever JavaScript -- Integração automática com o sistema de componentes -- Invalidação inteligente de partes das páginas -- Quantidade mínima de dados transferidos - -**Templates intuitivos [Latte|latte:]:** O sistema de templates mais seguro para PHP com recursos avançados: -- Proteção automática contra XSS com escaping sensível ao contexto -- Extensibilidade através de filtros, funções e tags personalizadas -- Herança de templates e snippets para AJAX -- Excelente suporte a PHP 8.x com sistema de tipos - -**Dependency Injection:** Nette utiliza totalmente a Injeção de Dependência: -- Passagem automática de dependências (autowiring) -- Configuração através do formato claro NEON -- Suporte para fábricas de componentes - - -Principais vantagens --------------------- - -- **Segurança**: Defesa automática contra [vulnerabilidades|nette:vulnerability-protection] como XSS, CSRF, etc. -- **Produtividade**: Menos escrita, mais funções graças a um design inteligente -- **Depuração**: [Depurador Tracy|tracy:] com painel de roteamento -- **Desempenho**: Cache inteligente, lazy loading de componentes -- **Flexibilidade**: Fácil modificação de URLs mesmo após a conclusão da aplicação -- **Componentes**: Sistema único de elementos de UI reutilizáveis -- **Moderno**: Suporte total a PHP 8.4+ e sistema de tipos - - -Começando ---------- - -1. [Como funcionam as aplicações? |how-it-works] - Compreender a arquitetura básica -2. [Presenters |presenters] - Trabalhar com presenters e ações -3. [Templates |templates] - Criar templates em Latte -4. [Roteamento |routing] - Configurar endereços URL -5. [Componentes interativos |components] - Utilizar o sistema de componentes - - -Compatibilidade com PHP ------------------------ - -| versão | compatível com PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 - -Aplica-se à última versão de patch. diff --git a/application/pt/@left-menu.texy b/application/pt/@left-menu.texy deleted file mode 100644 index 9f525b8f0e..0000000000 --- a/application/pt/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Como funcionam as aplicações? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Templates |templates] -- [Estrutura de diretórios |directory-structure] -- [Roteamento |routing] -- [Criando links URL |creating-links] -- [Componentes interativos |components] -- [AJAX & snippets |ajax] -- [Multiplier |multiplier] -- [Configuração |configuration] - - -Leitura adicional -***************** -- [Por que usar o Nette? |www:10-reasons-why-nette] -- [Instalação |nette:installation] -- [Escrevendo a primeira aplicação! |quickstart:] -- [Guias e melhores práticas |best-practices:] -- [Solução de problemas |nette:troubleshooting] diff --git a/application/pt/@meta.texy b/application/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/application/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/application/pt/ajax.texy b/application/pt/ajax.texy deleted file mode 100644 index b9ffa8bd66..0000000000 --- a/application/pt/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & Snippets -*************** - -
    - -Na era das aplicações web modernas, onde a funcionalidade é frequentemente dividida entre o servidor e o navegador, o AJAX é um elemento de ligação essencial. Que opções o Nette Framework nos oferece nesta área? -- envio de partes do template, os chamados snippets -- passagem de variáveis entre PHP e JavaScript -- ferramentas para depuração de requisições AJAX - -
    - - -Requisição AJAX -=============== - -Uma requisição AJAX, em princípio, não difere de uma requisição HTTP clássica. Um presenter é chamado com determinados parâmetros. E cabe ao presenter decidir como responder à requisição - ele pode retornar dados em formato JSON, enviar uma parte do código HTML, um documento XML, etc. - -No lado do navegador, inicializamos a requisição AJAX usando a função `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // processamento da resposta -}); -``` - -No lado do servidor, reconhecemos uma requisição AJAX usando o método `$httpRequest->isAjax()` do serviço [encapsulando a requisição HTTP |http:request]. Para a deteção, ele usa o cabeçalho HTTP `X-Requested-With`, por isso é importante enviá-lo. Dentro do presenter, pode-se usar o método `$this->isAjax()`. - -Se desejar enviar dados em formato JSON, use o método [`sendJson()` |presenters#Envio da resposta]. O método também encerra a atividade do presenter. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Se você planeja responder com um template especial destinado ao AJAX, pode fazê-lo da seguinte forma: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -O recurso mais poderoso que o Nette oferece para conectar o servidor ao cliente são os snippets. Graças a eles, você pode transformar uma aplicação comum em uma aplicação AJAX com esforço mínimo e algumas linhas de código. O exemplo Fifteen demonstra como tudo funciona, e seu código pode ser encontrado no [GitHub |https://github.com/nette-examples/fifteen]. - -Snippets, ou trechos, permitem atualizar apenas partes da página, em vez de recarregar a página inteira. Isso não só é mais rápido e eficiente, mas também proporciona uma experiência de usuário mais confortável. Os snippets podem lembrá-lo do Hotwire para Ruby on Rails ou do Symfony UX Turbo. Curiosamente, o Nette introduziu os snippets 14 anos antes. - -Como os snippets funcionam? No primeiro carregamento da página (requisição não-AJAX), a página inteira é carregada, incluindo todos os snippets. Quando o usuário interage com a página (por exemplo, clica em um botão, envia um formulário, etc.), em vez de carregar a página inteira, uma requisição AJAX é disparada. O código no presenter executa a ação e decide quais snippets precisam ser atualizados. O Nette renderiza esses snippets e os envia como um array em formato JSON. O código de manipulação no navegador insere os snippets recebidos de volta na página. Assim, apenas o código dos snippets alterados é transmitido, economizando largura de banda e acelerando o carregamento em comparação com a transferência do conteúdo da página inteira. - - -Naja ----- - -Para manipular snippets no lado do navegador, utiliza-se a [biblioteca Naja |https://naja.js.org]. [Instale-a |https://naja.js.org/#/guide/01-install-setup-naja] como um pacote node.js (para uso com aplicações Webpack, Rollup, Vite, Parcel e outras): - -```shell -npm install naja -``` - -…ou insira-a diretamente no template da página: - -```latte - -``` - -Primeiro, é necessário [inicializar |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] a biblioteca: - -```js -naja.initialize(); -``` - -Para transformar um link comum (sinal) ou o envio de um formulário em uma requisição AJAX, basta marcar o link, formulário ou botão correspondente com a classe `ajax`: - -```latte -Go - - - - - -ou - -
    - -
    -``` - - -Redesenho de snippets ---------------------- - -Cada objeto da classe [Control |components] (incluindo o próprio Presenter) regista se ocorreram alterações que exigem o seu redesenho. Para isso, serve o método `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // após o login, é necessário redesenhar a parte relevante - $this->redrawControl(); - // ... -} -``` - -O Nette permite um controlo ainda mais fino do que deve ser redesenhado. O método mencionado pode receber o nome do snippet como argumento. Assim, é possível invalidar (entenda-se: forçar o redesenho) ao nível das partes do template. Se todo o componente for invalidado, cada um dos seus snippets também será redesenhado: - -```php -// invalida o snippet 'header' -$this->redrawControl('header'); -``` - - -Snippets em Latte ------------------ - -Usar snippets em Latte é extremamente fácil. Para definir uma parte do template como um snippet, basta envolvê-la com as tags `{snippet}` e `{/snippet}`: - -```latte -{snippet header} -

    Olá ...

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

    Olá ...

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

    Vote

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

    {$item->title}

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

    Olá

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

    Página Inicial

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

    Hello ...

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

    Hello ...

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

    Votați

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

    {$item->title}

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

    Hello

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

    Homepage

    -... -{/block} -``` - -Acesta definește blocul `content`, care se va insera în locul `{include content}` din layout, și, de asemenea, re-definește blocul `title`, care va suprascrie `{block title}` din layout. Încercați să vă imaginați rezultatul. - - -Căutarea șabloanelor --------------------- - -Nu trebuie să specificați în presentere ce șablon trebuie redat, framework-ul deduce singur calea și vă scutește de scris. - -Dacă utilizați o structură de directoare unde fiecare presenter are propriul director, plasați pur și simplu șablonul în acest director sub numele acțiunii (resp. view), adică pentru acțiunea `default` utilizați șablonul `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Dacă utilizați o structură unde presenterele sunt împreună într-un singur director și șabloanele în folderul `templates`, salvați-l fie în fișierul `..latte`, fie `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← prima variantă - └── Home/ - └── default.latte ← a doua variantă -\-- - -Directorul `templates` poate fi plasat și cu un nivel mai sus, adică la același nivel cu directorul cu clasele presenterelor. - -Dacă șablonul nu este găsit, presenterul răspunde cu [eroarea 404 - page not found |presenters#Eroare 404 etc]. - -View-ul îl schimbați folosind `$this->setView('jineView')`. De asemenea, se poate specifica direct fișierul cu șablonul folosind `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Fișierele unde se caută șabloanele pot fi modificate prin suprascrierea metodei [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], care returnează un array de nume posibile de fișiere. - - -Căutarea șablonului de layout ------------------------------ - -Nette caută automat și fișierul cu layout-ul. - -Dacă utilizați o structură de directoare unde fiecare presenter are propriul director, plasați layout-ul fie în folderul cu presenterul, dacă este specific doar pentru el, fie cu un nivel mai sus, dacă este comun pentru mai mulți presenteri: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← layout comun - └── Home/ - ├── @layout.latte ← doar pentru presenterul Home - ├── HomePresenter.php - └── default.latte -\-- - -Dacă utilizați o structură unde presenterele sunt împreună într-un singur director și șabloanele în folderul `templates`, layout-ul va fi așteptat în aceste locații: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← layout comun - ├── Home.@layout.latte ← doar pentru Home, prima variantă - └── Home/ - └── @layout.latte ← doar pentru Home, a doua variantă -\-- - -Dacă presenterul se află într-un modul, se va căuta și la niveluri de directoare superioare, în funcție de imbricarea modulului. - -Numele layout-ului poate fi schimbat folosind `$this->setLayout('layoutAdmin')` și atunci se va aștepta în fișierul `@layoutAdmin.latte`. De asemenea, se poate specifica direct fișierul cu șablonul layout-ului folosind `$this->setLayout('/path/to/template.latte')`. - -Folosind `$this->setLayout(false)` sau tag-ul `{layout none}` în interiorul șablonului, căutarea layout-ului se dezactivează. - -.[note] -Fișierele unde se caută șabloanele de layout pot fi modificate prin suprascrierea metodei [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], care returnează un array de nume posibile de fișiere. - - -Variabile în șablon -------------------- - -Variabilele le transmitem șablonului scriindu-le în `$this->template` și apoi le avem disponibile în șablon ca variabile locale: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Astfel de simplu putem transmite șabloanelor orice variabile. Însă, la dezvoltarea aplicațiilor robuste, este mai util să ne limităm. De exemplu, prin definirea explicită a listei de variabile pe care șablonul le așteaptă și a tipurilor lor. Datorită acestui fapt, PHP ne va putea verifica tipurile, IDE-ul ne va sugera corect și analiza statică va dezvălui erorile. - -Și cum definim o astfel de listă? Simplu, sub forma unei clase și a proprietăților sale. O numim similar cu presenterul, doar cu `Template` la sfârșit: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // și alte variabile -} -``` - -Obiectul `$this->template` din presenter va fi acum o instanță a clasei `ArticleTemplate`. Deci, PHP va verifica tipurile declarate la scriere. Și începând cu versiunea PHP 8.2 va avertiza și la scrierea într-o variabilă inexistentă, în versiunile anterioare se poate obține același lucru folosind trait-ul [Nette\SmartObject |utils:smartobject]. - -Adnotarea `@property-read` este destinată IDE-ului și analizei statice, datorită ei va funcționa sugerarea, vezi "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -De luxul sugerării vă puteți bucura și în șabloane, este suficient să instalați în PhpStorm plugin-ul pentru Latte și să specificați la începutul șablonului numele clasei, mai multe în articolul "Latte: cum să folosiți sistemul de tipuri":https://blog.nette.org/ro/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Astfel funcționează și șabloanele în componente, este suficient doar să respectați convenția de nume și pentru componenta, de ex. `FifteenControl`, să creați clasa șablonului `FifteenTemplate`. - -Dacă aveți nevoie să creați `$template` ca instanță a altei clase, utilizați metoda `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Variabile implicite -------------------- - -Presenterele și componentele transmit automat către șabloane câteva variabile utile: - -- `$basePath` este calea URL absolută către directorul rădăcină (de ex. `/eshop`) -- `$baseUrl` este URL-ul absolut către directorul rădăcină (de ex. `http://localhost/eshop`) -- `$user` este obiectul [reprezentând utilizatorul |security:authentication] -- `$presenter` este presenterul curent -- `$control` este componenta sau presenterul curent -- `$flashes` array de [mesaje |presenters#Mesaje flash] trimise de funcția `flashMessage()` - -Dacă utilizați propria clasă de șablon, aceste variabile se transmit dacă creați proprietăți pentru ele. - - -Crearea linkurilor ------------------- - -În șablon se creează linkuri către alți presenteri & acțiuni în acest mod: - -```latte -detaliu produs -``` - -Atributul `n:href` este foarte util pentru tag-urile HTML ``. Dacă dorim să afișăm linkul în altă parte, de exemplu în text, folosim `{link}`: - -```latte -Adresa este: {link Home:default} -``` - -Mai multe informații găsiți în capitolul [Crearea linkurilor URL|creating-links]. - - -Filtre personalizate, tag-uri etc. ----------------------------------- - -Sistemul de șabloane Latte poate fi extins cu filtre, funcții, tag-uri etc. personalizate. Acest lucru se poate face direct în metoda `render` sau `beforeRender()`: - -```php -public function beforeRender(): void -{ - // adăugarea unui filtru - $this->template->addFilter('foo', /* ... */); - - // sau configurăm direct obiectul Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte în versiunea 3 oferă o modalitate mai avansată și anume crearea unei [extensii |latte:extending-latte#Latte Extension] pentru fiecare proiect web. Un exemplu fragmentar al unei astfel de clase: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -O înregistrăm folosind [configurația |configuration#Șabloane Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Traducere ---------- - -Dacă programați o aplicație multilingvă, probabil veți avea nevoie să afișați unele texte din șablon în diferite limbi. Nette Framework definește în acest scop o interfață pentru traducere [api:Nette\Localization\Translator], care are o singură metodă `translate()`. Aceasta primește mesajul `$message`, care de obicei este un șir de caractere, și orice alți parametri. Sarcina este de a returna șirul tradus. În Nette nu există nicio implementare implicită, puteți alege în funcție de nevoile dvs. dintre mai multe soluții gata făcute, pe care le găsiți pe [Componette |https://componette.org/search/localization]. În documentația lor veți afla cum să configurați translatorul. - -Șabloanelor li se poate seta un traducător, pe care îl [primim transmis |dependency-injection:passing-dependencies], prin metoda `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Translatorul poate fi setat alternativ folosind [configurația |configuration#Șabloane Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Apoi, traducătorul poate fi utilizat, de exemplu, ca filtru `|translate`, inclusiv cu parametri suplimentari, care sunt transmiși metodei `translate()` (vezi `foo, bar`): - -```latte -{='Coș'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Sau ca tag cu underscore: - -```latte -{_'Coș'} -{_$item} -{_$item, foo, bar} -``` - -Pentru traducerea unei secțiuni a șablonului există un tag pereche `{translate}` (de la Latte 2.11, anterior se folosea tag-ul `{_}`): - -```latte -{translate}Comandă{/translate} -{translate foo, bar}Comandă{/translate} -``` - -Translatorul este apelat standard în timpul rulării la redarea șablonului. Latte versiunea 3, însă, poate traduce toate textele statice deja în timpul compilării șablonului. Astfel se economisește performanță, deoarece fiecare șir se traduce o singură dată și traducerea rezultată se scrie în forma compilată. În directorul cu cache se creează astfel mai multe versiuni compilate ale șablonului, una pentru fiecare limbă. Pentru aceasta este suficient doar să specificați limba ca al doilea parametru: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Prin text static se înțelege, de exemplu, `{_'hello'}` sau `{translate}hello{/translate}`. Textele nestatice, cum ar fi `{_$foo}`, se vor traduce în continuare în timpul rulării. diff --git a/application/ru/@home.texy b/application/ru/@home.texy index eee8a946c1..701ddd7ee1 100644 --- a/application/ru/@home.texy +++ b/application/ru/@home.texy @@ -2,13 +2,13 @@ Nette Application ***************** .[perex] -Nette Application является ядром фреймворка Nette, предоставляя мощные инструменты для создания современных веб-приложений. Оно предлагает ряд исключительных возможностей, которые значительно упрощают разработку и повышают безопасность и поддерживаемость кода. +Nette Application - ядро фреймворка Nette, дающее мощные инструменты для создания современных веб-приложений. Оно предлагает ряд исключительных возможностей, которые заметно упрощают разработку и повышают безопасность и поддерживаемость кода. Установка --------- -Скачать и установить библиотеку можно с помощью [Composer|best-practices:composer]: +Скачайте и установите библиотеку с помощью [Composer|best-practices:composer]: ```shell composer require nette/application @@ -18,68 +18,68 @@ composer require nette/application Почему стоит выбрать Nette Application? --------------------------------------- -Nette всегда был пионером в области веб-технологий. +Nette всегда был первопроходцем в веб-технологиях. -**Двунаправленный маршрутизатор:** Nette обладает продвинутой системой маршрутизации, уникальной своей двунаправленностью — она не только преобразует URL в действия (actions) приложения, но и способна генерировать URL-адреса в обратную сторону. Это означает, что: -- Вы можете в любое время изменить структуру URL всего приложения без необходимости редактировать шаблоны +**Двунаправленный маршрутизатор:** в Nette есть продвинутая система маршрутизации, уникальная своей двунаправленностью: она не только преобразует URL в действия приложения, но и умеет порождать URL в обратную сторону. Это значит: +- Вы можете в любой момент изменить структуру URL всего приложения, не трогая шаблоны - URL автоматически канонизируются, что улучшает SEO -- Маршрутизация определяется в одном месте, а не разбросана по аннотациям +- Маршрутизация задаётся в одном месте, а не разбросана по аннотациям -**Компоненты и сигналы:** Встроенная компонентная система, вдохновленная Delphi и React.js, является совершенно уникальной среди PHP-фреймворков: -- Позволяет создавать повторно используемые элементы UI +**Компоненты и сигналы:** встроенная система компонентов, вдохновлённая Delphi и React.js, уникальна среди PHP-фреймворков: +- Позволяет создавать переиспользуемые элементы интерфейса - Поддерживает иерархическую композицию компонентов -- Предлагает элегантную обработку AJAX-запросов с помощью сигналов +- Предлагает изящную обработку AJAX-запросов через сигналы - Богатая библиотека готовых компонентов на [Componette](https://componette.org) -**AJAX и сниппеты:** Nette представил революционный способ работы с AJAX еще в 2009 году, задолго до появления аналогичных решений, таких как Hotwire для Ruby on Rails или Symfony UX Turbo: -- Сниппеты позволяют обновлять только части страницы без необходимости писать JavaScript -- Автоматическая интеграция с компонентной системой -- Умная инвалидация частей страниц -- Минимальное количество передаваемых данных +**AJAX и сниппеты:** Nette представил революционный способ работы с AJAX ещё в 2009 году, задолго до похожих решений вроде Hotwire для Ruby on Rails или Symfony UX Turbo: +- Сниппеты позволяют обновлять только части страницы, не требуя писать JavaScript +- Автоматическая интеграция с системой компонентов +- Умная инвалидация участков страницы +- Минимальный объём передаваемых данных -**Интуитивные шаблоны [Latte|latte:]:** Самая безопасная система шаблонов для PHP с расширенными функциями: +**Интуитивные шаблоны [Latte|latte:]:** самая безопасная система шаблонов для PHP с продвинутыми возможностями: - Автоматическая защита от XSS с контекстно-зависимым экранированием -- Расширяемость с помощью пользовательских фильтров, функций и тегов +- Расширяемость собственными фильтрами, функциями и тегами - Наследование шаблонов и сниппеты для AJAX - Отличная поддержка PHP 8.x с системой типов **Dependency Injection:** Nette полностью использует Dependency Injection: - Автоматическая передача зависимостей (autowiring) -- Конфигурация с помощью понятного формата NEON +- Настройка в наглядном формате NEON - Поддержка фабрик компонентов -Основные преимущества ---------------------- +Главные преимущества +-------------------- -- **Безопасность**: Автоматическая защита от [уязвимостей|nette:vulnerability-protection], таких как XSS, CSRF и т. д. -- **Продуктивность**: Меньше кода, больше функций благодаря умному дизайну +- **Безопасность**: автоматическая защита от [уязвимостей|nette:vulnerability-protection] вроде XSS, CSRF и других +- **Продуктивность**: меньше писать, больше возможностей благодаря продуманной архитектуре - **Отладка**: [отладчик Tracy|tracy:] с панелью маршрутизации -- **Производительность**: Умный кеш, ленивая загрузка компонентов -- **Гибкость**: Легкое изменение URL даже после завершения приложения -- **Компоненты**: Уникальная система повторно используемых элементов UI -- **Современность**: Полная поддержка PHP 8.4+ и системы типов +- **Производительность**: умный кеш, ленивая загрузка компонентов +- **Гибкость**: лёгкое изменение URL даже после завершения приложения +- **Компоненты**: уникальная система переиспользуемых элементов интерфейса +- **Современность**: полная поддержка PHP 8.3+ и системы типов -Начало работы -------------- +Первые шаги +----------- -1. [Как работают приложения? |how-it-works] - Понимание базовой архитектуры -2. [Презентеры |presenters] - Работа с презентерами и действиями -3. [Шаблоны |templates] - Создание шаблонов в Latte -4. [Маршрутизация |routing] - Конфигурация URL-адресов -5. [Интерактивные компоненты |components] - Использование компонентной системы +1. [Как работают приложения? |how-it-works] - понимание базовой архитектуры +2. [Презентеры |presenters] - работа с презентерами и действиями +3. [Шаблоны |templates] - создание шаблонов на Latte +4. [Маршрутизация |routing] - настройка URL-адресов +5. [Интерактивные компоненты |components] - использование системы компонентов Совместимость с PHP ------------------- -| версия | совместим с PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| версия | совместимость с 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 index bcf2cfd383..dadb55bbc3 100644 --- a/application/ru/@left-menu.texy +++ b/application/ru/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** +- [Обзор |@home] - [Как работают приложения? |how-it-works] -- [Bootstrapping] -- [Презентеры |presenters] -- [Шаблоны |templates] +- [Bootstrapping|bootstrapping] +- [Презентеры|presenters] +- [Шаблоны|templates] - [Структура каталогов |directory-structure] -- [Маршрутизация |routing] +- [Маршрутизация|routing] - [Создание URL-ссылок |creating-links] - [Интерактивные компоненты |components] - [AJAX и сниппеты |ajax] - [Multiplier |multiplier] -- [Конфигурация |configuration] +- [Конфигурация|configuration] +- [Обновление|upgrading] -Дополнительное чтение -********************* -- [Зачем использовать Nette? |www:10-reasons-why-nette] +Дополнительные материалы +************************ +- [Зачем использовать Nette?|www:10-reasons-why-nette] - [Установка |nette:installation] -- [Пишем первое приложение! |quickstart:] -- [Руководства и лучшие практики |best-practices:] +- [Создайте своё первое приложение! |quickstart:] +- [Лучшие практики |best-practices:] - [Устранение неполадок |nette:troubleshooting] diff --git a/application/ru/ajax.texy b/application/ru/ajax.texy index 533d8ed638..a61b4aa5a7 100644 --- a/application/ru/ajax.texy +++ b/application/ru/ajax.texy @@ -3,9 +3,9 @@ AJAX и сниппеты
    -В эпоху современных веб-приложений, где функциональность часто распределяется между сервером и браузером, AJAX является необходимым связующим элементом. Какие возможности предлагает нам Nette Framework в этой области? -- отправка частей шаблона, так называемых сниппетов -- передача переменных между PHP и JavaScript +В эпоху современных веб-приложений, где функциональность часто распределена между сервером и браузером, AJAX служит незаменимым связующим звеном. Какие возможности предлагает в этой области Nette Framework? +- отправку частей шаблона, называемых сниппетами +- передачу переменных между PHP и JavaScript - инструменты для отладки AJAX-запросов
    @@ -14,9 +14,9 @@ AJAX и сниппеты AJAX-запрос =========== -AJAX-запрос в принципе не отличается от классического HTTP-запроса. Вызывается презентер (presenter) с определенными параметрами. И презентер решает, как реагировать на запрос — он может вернуть данные в формате JSON, отправить часть HTML-кода, XML-документ и т. д. +AJAX-запрос принципиально не отличается от обычного HTTP-запроса. Вызывается презентер с определёнными параметрами. Как ответить на запрос, решает сам презентер: он может вернуть данные в формате JSON, отправить часть HTML-кода, XML-документ и так далее. -На стороне браузера мы инициализируем AJAX-запрос с помощью функции `fetch()`: +На стороне браузера мы инициируем AJAX-запрос функцией `fetch()`: ```js fetch(url, { @@ -24,22 +24,22 @@ fetch(url, { }) .then(response => response.json()) .then(payload => { - // обработка ответа + // обрабатываем ответ }); ``` -На стороне сервера мы распознаем AJAX-запрос с помощью метода `$httpRequest->isAjax()` сервиса [инкапсулирующего HTTP-запрос |http:request]. Для обнаружения используется HTTP-заголовок `X-Requested-With`, поэтому важно его отправлять. В рамках презентера можно использовать метод `$this->isAjax()`. +На стороне сервера AJAX-запрос распознаётся методом `$httpRequest->isAjax()` сервиса, [упаковывающего HTTP-запрос |http:request]. Для определения он использует HTTP-заголовок `X-Requested-With`, поэтому принципиально важно его отправлять. Внутри презентера можно использовать метод `$this->isAjax()`. -Если вы хотите отправить данные в формате JSON, используйте метод [`sendJson()` |presenters#Отправка ответа]. Метод также завершает работу презентера. +Если вы хотите отправить данные в формате JSON, используйте метод [`sendJson()` |presenters#Отправка ответа]. Этот метод также завершает работу презентера. ```php public function actionExport(): void { - $this->sendJson($this->model->getData); + $this->sendJson($this->model->getData()); } ``` -Если вы планируете ответить с помощью специального шаблона, предназначенного для AJAX, вы можете сделать это следующим образом: +Если вы собираетесь ответить особым шаблоном, предназначенным для AJAX, это делается так: ```php public function handleClick($param): void @@ -55,29 +55,29 @@ public function handleClick($param): void Сниппеты ======== -Самый мощный инструмент, который предлагает Nette для связи сервера с клиентом, — это сниппеты. Благодаря им вы можете превратить обычное приложение в AJAX-приложение с минимальными усилиями и несколькими строками кода. Как все это работает, демонстрирует пример Fifteen, код которого вы найдете на [GitHub |https://github.com/nette-examples/fifteen]. +Самый мощный инструмент, который Nette предлагает для связи сервера с клиентом, - это сниппеты. С их помощью вы можете превратить обычное приложение в AJAX-приложение минимальными усилиями и несколькими строками кода. Пример Fifteen показывает, как всё это работает, а его код можно найти на [GitHub |https://github.com/nette-examples/fifteen]. -Сниппеты, или фрагменты, позволяют обновлять только части страницы, вместо того чтобы перезагружать всю страницу целиком. Это не только быстрее и эффективнее, но и обеспечивает более комфортный пользовательский опыт. Сниппеты могут напомнить вам Hotwire для Ruby on Rails или Symfony UX Turbo. Интересно, что Nette представила сниппеты на 14 лет раньше. +Сниппеты позволяют обновлять только части страницы вместо перезагрузки всей страницы. Это не только быстрее и эффективнее, но и удобнее для пользователя. Сниппеты могут напомнить вам Hotwire для Ruby on Rails или Symfony UX Turbo. Любопытно, что Nette представил сниппеты на 14 лет раньше. -Как работают сниппеты? При первой загрузке страницы (не-AJAX запросе) загружается вся страница, включая все сниппеты. Когда пользователь взаимодействует со страницей (например, нажимает кнопку, отправляет форму и т. д.), вместо загрузки всей страницы вызывается AJAX-запрос. Код в презентере выполняет действие (action) и решает, какие сниппеты необходимо обновить. Nette отрисовывает эти сниппеты и отправляет их в виде массива в формате JSON. Обслуживающий код в браузере вставляет полученные сниппеты обратно в страницу. Таким образом, передается только код измененных сниппетов, что экономит пропускную способность и ускоряет загрузку по сравнению с передачей всего содержимого страницы. +Как работают сниппеты? При первой загрузке страницы (не-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 и другими): +Для работы со сниппетами на стороне браузера используется [библиотека 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] библиотеку: +Сначала библиотеку нужно [инициализировать |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: ```js naja.initialize(); @@ -103,43 +103,45 @@ naja.initialize(); Перерисовка сниппетов --------------------- -Каждый объект класса [Control |components] (включая сам Presenter) отслеживает, произошли ли изменения, требующие его перерисовки. Для этого используется метод `redrawControl()`: +Каждый объект класса [Control |components] (включая сам презентер) следит за тем, произошли ли изменения, требующие перерисовки. Для этого служит метод `redrawControl()`: ```php public function handleLogin(string $user): void { - // после входа необходимо перерисовать соответствующую часть + // после входа нужно перерисовать соответствующую часть $this->redrawControl(); // ... } ``` -Nette позволяет еще более тонко контролировать то, что нужно перерисовать. Указанный метод может принимать в качестве аргумента имя сниппета. Таким образом, можно инвалидировать (то есть: принудительно перерисовать) на уровне частей шаблона. Если инвалидируется весь компонент, то перерисовывается и каждый его сниппет: +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}` и `{/snippet}`: ```latte {snippet header} -

    Привет ...

    +

    Hello ...

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

    Привет ...

    +

    Hello ...

    ``` @@ -147,7 +149,7 @@ $this->redrawControl('header'); Области сниппетов ----------------- -Имена сниппетов также могут быть выражениями: +Имена сниппетов могут быть и выражениями: ```latte {foreach $items as $id => $item} @@ -155,7 +157,9 @@ $this->redrawControl('header'); {/foreach} ``` -Таким образом, у нас получится несколько сниппетов `item-0`, `item-1` и т. д. Если бы мы напрямую инвалидировали динамический сниппет (например, `item-1`), ничего бы не перерисовалось. Причина в том, что сниппеты действительно работают как фрагменты и отрисовываются только они сами. Однако в шаблоне фактически нет сниппета с именем `item-1`. Он создается только при выполнении кода вокруг сниппета, то есть цикла foreach. Поэтому мы помечаем часть шаблона, которая должна быть выполнена, с помощью тега `{snippetArea}`: +Сам по себе это неработающий промежуточный шаг: отрисованный вне статического `{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
      @@ -165,16 +169,16 @@ $this->redrawControl('header');
    ``` -И заставляем перерисовать как сам сниппет, так и всю родительскую область: +И запрашиваем перерисовку как отдельного сниппета, так и всей родительской области: ```php $this->redrawControl('itemsContainer'); $this->redrawControl('item-1'); ``` -В то же время желательно убедиться, что массив `$items` содержит только те элементы, которые должны быть перерисованы. +Одновременно стоит позаботиться о том, чтобы в массиве `$items` были только те элементы, которые нужно перерисовать. -Если мы вставляем в шаблон с помощью тега `{include}` другой шаблон, содержащий сниппеты, необходимо включение шаблона снова обернуть в `snippetArea` и инвалидировать его вместе со сниппетом: +Если мы подключаем в главный шаблон другой шаблон со сниппетами через тег `{include}`, подключение шаблона нужно снова обернуть в `snippetArea` и инвалидировать её вместе со сниппетом: ```latte {snippetArea include} @@ -198,38 +202,44 @@ $this->redrawControl('item'); Сниппеты в компонентах ---------------------- -Вы можете создавать сниппеты и в [компонентах |components], и Nette будет автоматически их перерисовывать. Но есть определенное ограничение: для перерисовки сниппетов вызывается метод `render()` без параметров. То есть передача параметров в шаблоне не будет работать: +Сниппеты можно создавать и внутри [компонентов|components], и Nette будет автоматически их перерисовывать. Однако есть ограничение: для перерисовки сниппетов Nette вызывает метод `render()` без параметров. Поэтому передача параметров в шаблоне работать не будет: ```latte OK {control productGrid} -не будет работать: +работать не будет: {control productGrid $arg, $arg} {control productGrid:paginator} ``` -Отправка пользовательских данных --------------------------------- +Отправка собственных данных +--------------------------- -Вместе со сниппетами вы можете отправить клиенту любые другие данные. Достаточно записать их в объект `payload`: +Вместе со сниппетами вы можете отправить клиенту любые дополнительные данные. Достаточно записать их в объект `payload`: ```php public function actionDelete(int $id): void { // ... if ($this->isAjax()) { - $this->payload->message = 'Успешно'; + $this->payload->message = 'Success'; } } ``` +Перенаправление +--------------- + +Во время AJAX-запроса методы `redirect()` и `redirectUrl()` не отправляют HTTP-перенаправление. Вместо этого они записывают целевой URL в payload (объект данных, отправляемый в AJAX-ответе), а именно в его свойство `payload.redirect`, и отправляют его; само перенаправление затем выполняет клиентская библиотека (Naja). + + Передача параметров =================== -Если мы отправляем компоненту параметры с помощью AJAX-запроса, будь то параметры сигнала или персистентные параметры, мы должны указать в запросе их глобальное имя, которое также содержит имя компонента. Полное имя параметра возвращает метод `getParameterId()`. +Отправляя параметры компоненту через AJAX-запрос, будь то параметры сигнала или постоянные параметры, мы должны указать в запросе их глобальное имя, включающее имя компонента. Полное имя параметра возвращает метод `getParameterId()`. ```js let url = new URL({link //foo!}); @@ -247,3 +257,9 @@ public function handleFoo(int $bar): void { } ``` + + +Дополнительные материалы +======================== + +- [Динамические сниппеты |best-practices:dynamic-snippets] diff --git a/application/ru/bootstrapping.texy b/application/ru/bootstrapping.texy index 58aeb4f9b0..3842b68a2b 100644 --- a/application/ru/bootstrapping.texy +++ b/application/ru/bootstrapping.texy @@ -1,21 +1,24 @@ -Загрузка -******** +Bootstrapping +*************
    -Загрузка — это процесс инициализации среды приложения, создания контейнера внедрения зависимостей (DI) и запуска приложения. Мы обсудим: +Bootstrapping - это процесс инициализации окружения приложения, создания контейнера внедрения зависимостей (DI) и запуска приложения. Мы обсудим: -- как класс Bootstrap инициализирует среду -- как приложения настраиваются с помощью NEON файлов -- как различать режим производства и разработки -- как создать и настроить DI контейнер +- как класс Bootstrap инициализирует окружение +- как приложения настраиваются файлами NEON +- как различать производственный режим и режим разработки +- как создать и настроить DI-контейнер
    -Приложения, будь то веб-приложения или скрипты, запускаемые из командной строки, начинают свою работу с некоторой формы инициализации среды. В давние времена за это отвечал файл с именем, например, `include.inc.php`, который включался в первоначальный файл. В современных приложениях Nette его заменил класс `Bootstrap`, который как часть приложения находится в файле `app/Bootstrap.php`. Он может выглядеть, например, так: +Приложения, будь то веб-приложения или скрипты, запускаемые из командной строки, начинают своё выполнение с той или иной инициализации окружения. В былые времена за это отвечал файл с именем вроде `include.inc.php`, подключаемый начальным файлом. В современных приложениях Nette его заменил класс `Bootstrap`, который как часть приложения находится в файле `app/Bootstrap.php`. Он может выглядеть, например, так: ```php +namespace App; + +use Nette; use Nette\Bootstrap\Configurator; class Bootstrap @@ -26,9 +29,9 @@ class Bootstrap public function __construct() { $this->rootDir = dirname(__DIR__); - // Конфигуратор отвечает за настройку среды приложения и сервисов. + // Configurator отвечает за настройку окружения приложения и сервисов. $this->configurator = new Configurator; - // Устанавливает каталог для временных файлов, генерируемых Nette (например, скомпилированных шаблонов) + // Задаём каталог для временных файлов, порождаемых Nette (например, скомпилированных шаблонов) $this->configurator->setTempDirectory($this->rootDir . '/temp'); } @@ -41,11 +44,11 @@ class Bootstrap private function initializeEnvironment(): void { - // Nette умный, и режим разработки включается автоматически, - // или вы можете включить его для конкретного IP-адреса, раскомментировав следующую строку: + // Nette умён, и режим разработки включается автоматически, + // либо вы можете включить его для конкретного IP-адреса, раскомментировав следующую строку: // $this->configurator->setDebugMode('secret@23.75.345.200'); - // Активирует Tracy: ультимативный "швейцарский нож" для отладки. + // Включает Tracy - лучший "швейцарский нож" для отладки. $this->configurator->enableTracy($this->rootDir . '/log'); // RobotLoader: автоматически загружает все классы в выбранном каталоге @@ -56,7 +59,7 @@ class Bootstrap private function setupContainer(): void { - // Загружает конфигурационные файлы + // Загружаем конфигурационные файлы $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } @@ -66,69 +69,78 @@ class Bootstrap index.php ========= -Первоначальным файлом для веб-приложений является `index.php`, который находится в [публичном каталоге |directory-structure#Публичный каталог www] `www/`. Он запрашивает у класса Bootstrap инициализацию среды и создание DI-контейнера. Затем он получает из него сервис `Application`, который запускает веб-приложение: +У веб-приложений начальным файлом служит `index.php`, лежащий в [публичном каталоге |directory-structure#Публичный каталог www/] `www/`. Он поручает классу Bootstrap инициализировать окружение и создать DI-контейнер. Затем он получает из контейнера сервис `Application`, который и запускает веб-приложение: ```php $bootstrap = new App\Bootstrap; -// Инициализация среды + создание DI-контейнера +// Инициализируем окружение и создаём DI-контейнер $container = $bootstrap->bootWebApplication(); -// DI-контейнер создает объект Nette\Application\Application +// DI-контейнер создаёт объект Nette\Application\Application $application = $container->getByType(Nette\Application\Application::class); -// Запуск приложения Nette и обработка входящего запроса +// Запускаем приложение Nette и обрабатываем входящий запрос $application->run(); ``` -Как видно, с настройкой среды и созданием контейнера внедрения зависимостей (DI) помогает класс [api:Nette\Bootstrap\Configurator], который мы сейчас рассмотрим подробнее. +.[note] +Объект `$application` в ходе обработки запроса испускает [события |nette:glossary#События]: `onStartup`, `onRequest`, `onPresenter`, `onResponse`, `onShutdown` и `onError` (при необработанном исключении). Вы можете привязать к ним обработчики, что удобно для ведения лога или мониторинга всего приложения. + +Как видите, настроить окружение и создать контейнер внедрения зависимостей (DI) помогает класс [api:Nette\Bootstrap\Configurator]. Сейчас мы познакомим вас с ним подробнее. -Режим разработки vs режим production -==================================== +Режим разработки и производственный режим +========================================= -Nette ведет себя по-разному в зависимости от того, работает ли он на сервере разработки или production: +Nette ведёт себя по-разному в зависимости от того, работает он на сервере разработки или на производственном: -🛠️ Режим разработки (Development): - - Отображает панель отладки Tracy с полезной информацией (SQL-запросы, время выполнения, использованная память) - - При ошибке отображает подробную страницу ошибки с вызовами функций и содержимым переменных - - Автоматически обновляет кеш при изменении шаблонов Latte, редактировании конфигурационных файлов и т. д. +🛠️ Режим разработки: + - Показывает панель отладки Tracy с полезными сведениями (SQL-запросы, время выполнения, использованная память) + - При ошибке показывает подробную страницу ошибки с вызовами функций и содержимым переменных + - Автоматически обновляет кеш при изменении шаблонов Latte, конфигурационных файлов и прочего -🚀 Режим production (Production): - - Не отображает никакой отладочной информации, все ошибки записывает в лог - - При ошибке отображает ErrorPresenter или общую страницу "Server Error" - - Кеш никогда автоматически не обновляется! - - Оптимизирован для скорости и безопасности +🚀 Производственный режим: + - Не показывает никаких отладочных сведений, все ошибки записываются в лог + - При ошибке показывает ErrorPresenter или общую страницу "Server Error" + - Кеш никогда не обновляется автоматически! + - Оптимизирован ради скорости и безопасности -Выбор режима осуществляется автоопределением, поэтому обычно не требуется ничего настраивать или вручную переключать: +Режим выбирается автоопределением, поэтому обычно ничего настраивать и переключать вручную не нужно: -- режим разработки: на localhost (IP-адрес `127.0.0.1` или `::1`), если нет прокси (т. е. его HTTP-заголовка) -- режим production: везде в остальных случаях +- режим разработки: на localhost (IP-адрес `127.0.0.1` или `::1`), если нет прокси (то есть его HTTP-заголовок не обнаружен) +- производственный режим: везде остальном -Если мы хотим включить режим разработки и в других случаях, например, для программистов, обращающихся с конкретного IP-адреса, используем `setDebugMode()`: +Если мы хотим включить режим разработки и в других случаях, например для программистов, заходящих с определённого IP-адреса, мы используем `setDebugMode()`: ```php -$this->configurator->setDebugMode('23.75.345.200'); // можно указать и массив IP-адресов +$this->configurator->setDebugMode('23.75.345.200'); // можно передать и массив IP-адресов ``` -Настоятельно рекомендуем комбинировать IP-адрес с cookie. В cookie `nette-debug` сохраним секретный токен, например `secret1234`, и таким образом активируем режим разработки для программистов, обращающихся с конкретного IP-адреса и одновременно имеющих в cookie упомянутый токен: +Мы настоятельно рекомендуем сочетать IP-адрес с cookie. Сохраните в cookie `nette-debug` секретный токен, например `secret1234`, и тем самым включите режим разработки для программистов, заходящих с определённого IP-адреса и имеющих в cookie этот токен: ```php $this->configurator->setDebugMode('secret1234@23.75.345.200'); ``` -Режим разработки можно также полностью отключить, даже для localhost: +Мы можем и полностью отключить режим разработки, даже на localhost: ```php $this->configurator->setDebugMode(false); ``` -Внимание, значение `true` включает режим разработки принудительно, что никогда не должно происходить на production-сервере. +Учтите, что значение `true` принудительно включает режим разработки, чего на производственном сервере быть **никогда** не должно. + +Автоопределением внутренне занимается статический метод `Configurator::detectDebugMode()`, который вы можете вызвать и сами, например чтобы определить режим разработки вне конфигуратора. Он принимает необязательный белый список IP-адресов или имён компьютеров и возвращает, должен ли текущий запрос выполняться в режиме разработки: + +```php +$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200'); +``` Инструмент отладки Tracy ======================== -Для легкой отладки мы также включим отличный инструмент [Tracy |tracy:]. В режиме разработки он визуализирует ошибки, а в режиме production записывает ошибки в указанный каталог: +Ради удобной отладки мы включим прекрасный инструмент [Tracy |tracy:]. В режиме разработки он наглядно показывает ошибки, а в производственном записывает их в указанный каталог: ```php $this->configurator->enableTracy($this->rootDir . '/log'); @@ -138,19 +150,19 @@ $this->configurator->enableTracy($this->rootDir . '/log'); Временные файлы =============== -Nette использует кеш для DI-контейнера, RobotLoader, шаблонов и т. д. Поэтому необходимо установить путь к каталогу, куда будет сохраняться кеш: +Nette использует кеш для DI-контейнера, RobotLoader, шаблонов и прочего. Поэтому нужно задать путь к каталогу, где будет храниться кеш: ```php $this->configurator->setTempDirectory($this->rootDir . '/temp'); ``` -На Linux или macOS установите для каталогов `log/` и `temp/` [права на запись |nette:troubleshooting#Настройка прав доступа к каталогам]. +В Linux или macOS задайте каталогам `log/` и `temp/` [права на запись |nette:troubleshooting#Задание прав на каталоги]. RobotLoader =========== -Как правило, мы захотим автоматически загружать классы с помощью [RobotLoader |robot-loader:], поэтому мы должны его запустить и позволить ему загружать классы из каталога, где находится `Bootstrap.php` (т. е. `__DIR__`), и всех подкаталогов: +Обычно мы хотим автоматически загружать классы с помощью [RobotLoader |robot-loader:], поэтому нам нужно его запустить и дать ему загружать классы из каталога, где лежит `Bootstrap.php` (то есть `__DIR__`), и из всех его подкаталогов: ```php $this->configurator->createRobotLoader() @@ -158,13 +170,13 @@ $this->configurator->createRobotLoader() ->register(); ``` -Альтернативный подход — позволить загружать классы только через [Composer |best-practices:composer] при соблюдении PSR-4. +Альтернативный подход - загружать классы исключительно через [Composer |best-practices:composer] по PSR-4. Часовой пояс ============ -Через конфигуратор можно установить часовой пояс по умолчанию. +Часовой пояс по умолчанию можно задать через конфигуратор. ```php $this->configurator->setTimeZone('Europe/Prague'); @@ -174,20 +186,22 @@ $this->configurator->setTimeZone('Europe/Prague'); Конфигурация DI-контейнера ========================== -Частью процесса загрузки является создание DI-контейнера, или фабрики объектов, которая является сердцем всего приложения. Это фактически PHP-класс, который генерирует Nette и сохраняет в каталоге кеша. Фабрика производит ключевые объекты приложения, и с помощью конфигурационных файлов мы инструктируем ее, как их создавать и настраивать, тем самым влияя на поведение всего приложения. +Частью процесса запуска является создание DI-контейнера, то есть фабрики объектов, которая служит сердцем всего приложения. На деле это PHP-класс, порождённый Nette и сохранённый в каталоге кеша. Фабрика создаёт ключевые объекты приложения, а конфигурационными файлами мы указываем ей, как их создавать и настраивать, и тем самым влияем на поведение всего приложения. -Конфигурационные файлы обычно записываются в формате [NEON |neon:format]. В отдельной главе вы узнаете, [что все можно настроить |nette:configuring]. +Конфигурационные файлы обычно пишутся в [формате NEON |neon:format]. В отдельной главе вы можете прочитать, [что можно настраивать |nette:configuring]. .[tip] -В режиме разработки контейнер автоматически обновляется при каждом изменении кода или конфигурационных файлов. В режиме production он генерируется только один раз, и изменения не проверяются для максимальной производительности. +В режиме разработки контейнер автоматически обновляется при изменении кода или конфигурационных файлов. В производственном режиме он порождается только один раз, а изменения не проверяются ради максимальной производительности. -Конфигурационные файлы загружаем с помощью `addConfig()`: +Метод `createContainer()` собирает контейнер и возвращает его экземпляр, а метод `loadContainer()` возвращает только имя порождённого класса контейнера, который вы затем можете создать сами. Это полезно в продвинутых сценариях. + +Конфигурационные файлы загружаются методом `addConfig()`: ```php $this->configurator->addConfig($this->rootDir . '/config/common.neon'); ``` -Если мы хотим добавить несколько конфигурационных файлов, мы можем вызвать функцию `addConfig()` несколько раз. +Если мы хотим добавить больше конфигурационных файлов, мы можем вызвать функцию `addConfig()` несколько раз. ```php $configDir = $this->rootDir . '/config'; @@ -198,17 +212,17 @@ if (PHP_SAPI === 'cli') { } ``` -Имя `cli.php` — не опечатка, конфигурация может быть записана и в PHP-файле, который вернет ее в виде массива. +Имя `cli.php` - не опечатка: конфигурацию можно записать и в PHP-файле, который возвращает её массивом. -Также мы можем добавить другие конфигурационные файлы в [секции `includes` |dependency-injection:configuration#Включение файлов]. +Другие конфигурационные файлы мы можем добавить и в [секции `includes` |dependency-injection:configuration#Подключение файлов]. -Если в конфигурационных файлах появляются элементы с одинаковыми ключами, они будут перезаписаны или, в случае [массивов, объединены |dependency-injection:configuration#Слияние]. Позже включенный файл имеет более высокий приоритет, чем предыдущий. Файл, в котором указана секция `includes`, имеет более высокий приоритет, чем включенные в нем файлы. +Если в конфигурационных файлах встречаются элементы с одинаковыми ключами, они будут перезаписаны или, в случае [массивов, объединены |dependency-injection:configuration#Слияние]. Файл, подключённый позже, имеет более высокий приоритет, чем предыдущий. Файл, в котором указана секция `includes`, имеет более высокий приоритет, чем подключённые в нём файлы. Статические параметры --------------------- -Параметры, используемые в конфигурационных файлах, можно определить [в секции `parameters` |dependency-injection:configuration#Параметры], а также передавать (или перезаписывать) методом `addStaticParameters()` (имеет псевдоним `addParameters()`). Важно, что разные значения параметров приводят к генерации дополнительных DI-контейнеров, то есть дополнительных классов. +Параметры, используемые в конфигурационных файлах, можно определить [в секции `parameters` |dependency-injection:configuration#Параметры], а также передать (или переопределить) методом `addStaticParameters()` (его более старый, ныне устаревший псевдоним - `addParameters()`). Важно, что разные значения параметров вызовут порождение дополнительных DI-контейнеров, то есть дополнительных классов. ```php $this->configurator->addStaticParameters([ @@ -216,13 +230,13 @@ $this->configurator->addStaticParameters([ ]); ``` -На параметр `projectId` можно ссылаться в конфигурации обычным способом `%projectId%`. +На параметр `projectId` можно сослаться в конфигурации обычной записью `%projectId%`. Динамические параметры ---------------------- -В контейнер можно добавить и динамические параметры, различные значения которых, в отличие от статических параметров, не приводят к генерации новых DI-контейнеров. +Мы можем добавить в контейнер и динамические параметры, разные значения которых, в отличие от статических, не вызовут порождения новых DI-контейнеров. ```php $this->configurator->addDynamicParameters([ @@ -230,7 +244,7 @@ $this->configurator->addDynamicParameters([ ]); ``` -Таким образом, мы можем легко добавить, например, переменные среды, на которые затем можно ссылаться в конфигурации с помощью записи `%env.variable%`. +Так мы легко добавим, например, переменные окружения, на которые затем можно сослаться в конфигурации записью `%env.variable%`. ```php $this->configurator->addDynamicParameters([ @@ -242,21 +256,22 @@ $this->configurator->addDynamicParameters([ Параметры по умолчанию ---------------------- -В конфигурационных файлах вы можете использовать эти статические параметры: +В конфигурационных файлах вы можете использовать эти параметры: -- `%appDir%` — абсолютный путь к каталогу с файлом `Bootstrap.php` -- `%wwwDir%` — абсолютный путь к каталогу с входным файлом `index.php` -- `%tempDir%` — абсолютный путь к каталогу для временных файлов -- `%vendorDir%` — абсолютный путь к каталогу, куда Composer устанавливает библиотеки -- `%rootDir%` — абсолютный путь к корневому каталогу проекта -- `%debugMode%` — указывает, находится ли приложение в режиме отладки -- `%consoleMode%` — указывает, пришел ли запрос через командную строку +- `%appDir%` - абсолютный путь к каталогу с файлом `Bootstrap.php` +- `%wwwDir%` - абсолютный путь к каталогу с начальным файлом `index.php` +- `%tempDir%` - абсолютный путь к каталогу временных файлов +- `%vendorDir%` - абсолютный путь к каталогу, куда Composer устанавливает библиотеки +- `%rootDir%` - абсолютный путь к корневому каталогу проекта +- `%baseUrl%` - абсолютный URL корневого каталога (динамический параметр, вычисляемый во время выполнения) +- `%debugMode%` - находится ли приложение в режиме отладки +- `%consoleMode%` - пришёл ли запрос из командной строки Импортированные сервисы ----------------------- -Теперь мы углубляемся. Хотя смысл DI-контейнера заключается в создании объектов, в исключительных случаях может возникнуть необходимость вставить существующий объект в контейнер. Мы делаем это, определяя сервис с флагом `imported: true`. +Теперь копнём глубже. Хотя назначение DI-контейнера - создавать объекты, изредка может понадобиться вставить в контейнер уже существующий объект. Мы делаем это, определив сервис с флагом `imported: true`. ```neon services: @@ -265,7 +280,7 @@ services: imported: true ``` -И в bootstrap вставляем объект в контейнер: +А в bootstrap вставляем объект в контейнер: ```php $this->configurator->addServices([ @@ -274,10 +289,10 @@ $this->configurator->addServices([ ``` -Различные среды -=============== +Разные окружения +================ -Не бойтесь изменять класс Bootstrap в соответствии со своими потребностями. Методу `bootWebApplication()` можно добавить параметры для различения веб-проектов. Или мы можем добавить другие методы, например `bootTestEnvironment()`, который инициализирует среду для модульных тестов, `bootConsoleApplication()` для скриптов, вызываемых из командной строки, и т. д. +Смело меняйте класс `Bootstrap` под свои нужды. Вы можете добавить в метод `bootWebApplication()` параметры, чтобы различать веб-проекты. Или можно добавить другие методы, например `bootTestEnvironment()`, инициализирующий окружение для модульных тестов, `bootConsoleApplication()` для скриптов, вызываемых из командной строки, и так далее. ```php public function bootTestEnvironment(): Nette\DI\Container diff --git a/application/ru/components.texy b/application/ru/components.texy index be5ba317a5..a1942e5f9e 100644 --- a/application/ru/components.texy +++ b/application/ru/components.texy @@ -3,7 +3,7 @@
    -Компоненты — это отдельные повторно используемые объекты, которые мы вставляем на страницы. Это могут быть формы, датагриды, опросы, в общем, все, что имеет смысл использовать повторно. Мы покажем: +Компоненты - самостоятельные переиспользуемые объекты, которые мы встраиваем в страницы. Это могут быть формы, таблицы данных, опросы - в общем, всё, что имеет смысл использовать повторно. Мы покажем: - как использовать компоненты? - как их писать? @@ -11,19 +11,19 @@
    -Nette имеет встроенную систему компонентов. Что-то подобное могут помнить старожилы из Delphi или ASP.NET Web Forms, на чем-то отдаленно похожем построены React или Vue.js. Однако в мире PHP-фреймворков это уникальное явление. +В Nette встроена система компонентов. Нечто похожее может быть знакомо ветеранам Delphi или ASP.NET Web Forms; React или Vue.js построены на чём-то отдалённо похожем. Однако в мире PHP-фреймворков это уникальная возможность. -При этом компоненты кардинально влияют на подход к созданию приложений. Вы можете собирать страницы из готовых блоков. Нужен датагрид в админке? Найдите его на [Componette |https://componette.org/search/component], репозитории open-source дополнений (то есть не только компонентов) для Nette, и просто вставьте в презентер. +При этом компоненты принципиально меняют подход к разработке приложений. Вы можете собирать страницы из заранее подготовленных единиц. Нужна таблица данных в вашей администраторской части? Найдите её на [Componette |https://componette.org/search/component], в хранилище дополнений с открытым кодом (не только компонентов) для Nette, и просто вставьте в презентер. -В презентер можно включить любое количество компонентов. А в некоторые компоненты можно вставлять другие компоненты. Таким образом, создается дерево компонентов, корнем которого является презентер. +Вы можете встроить в презентер сколько угодно компонентов. А в некоторые компоненты можно встраивать другие компоненты. Так возникает дерево компонентов, корнем которого служит презентер. Фабричные методы ================ -Как компоненты вставляются в презентер и затем используются? Обычно с помощью фабричных методов. +Как компоненты вставляются в презентер и затем используются? Обычно через фабричные методы. -Фабрика компонентов представляет собой элегантный способ создания компонентов только тогда, когда они действительно необходимы (lazy / on demand). Все волшебство заключается в реализации метода с именем `createComponent()`, где `` — это имя создаваемого компонента, и который создает и возвращает компонент. +Фабрика компонента - изящный способ создавать компоненты только тогда, когда они действительно нужны (лениво, по требованию). Вся магия заключается в реализации метода с именем `createComponent()`, где `` - имя создаваемого компонента; этот метод создаёт и возвращает компонент. ```php .{file:DefaultPresenter.php} class DefaultPresenter extends Nette\Application\UI\Presenter @@ -31,49 +31,54 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } ``` -Благодаря тому, что все компоненты создаются в отдельных методах, код становится более понятным. +Поскольку все компоненты создаются в отдельных методах, код становится нагляднее. .[note] -Имена компонентов всегда начинаются с маленькой буквы, хотя в названии метода они пишутся с большой. +Имена компонентов всегда начинаются со строчной буквы, хотя в имени метода они пишутся с заглавной. -Фабрики никогда не вызываются напрямую, они вызываются сами в тот момент, когда мы впервые используем компонент. Благодаря этому компонент создается в нужный момент и только в том случае, когда он действительно необходим. Если мы не используем компонент (например, при AJAX-запросе, когда передается только часть страницы, или при кешировании шаблона), он вообще не создается, и мы экономим ресурсы сервера. +Мы никогда не вызываем фабрики напрямую: они вызываются автоматически при первом использовании компонента. Благодаря этому компонент создаётся в нужный момент и только если он действительно нужен. Если мы компонент не используем (например, при AJAX-запросе, когда передаётся лишь часть страницы, или при кешировании шаблона), он вообще не создастся, что сэкономит производительность сервера. ```php .{file:DefaultPresenter.php} -// обращаемся к компоненту, и если это было впервые, -// вызывается createComponentPoll(), который его создает +// обращаемся к компоненту, и если это в первый раз, +// вызывается createComponentPoll(), который его создаёт $poll = $this->getComponent('poll'); -// альтернативный синтаксис: $poll = $this['poll']; +// альтернативная запись: $poll = $this['poll']; ``` -В шаблоне можно отрисовать компонент с помощью тега [{control} |#Отрисовка]. Поэтому нет необходимости вручную передавать компоненты в шаблон. +В шаблоне компонент можно отрисовать тегом [{control} |#Отрисовка]. Поэтому передавать компоненты в шаблон вручную не нужно. ```latte -

    Голосуйте

    +

    Please Vote

    {control poll} ``` +.[tip] +Для динамического создания переменного числа компонентов используйте [Multiplier |multiplier]. -Стиль Голливуда -=============== +Фабричные методы `createComponent()` работают не только в презентерах. Тем же способом вы можете вложить компонент в другой компонент и собрать их в дерево - это удобно, например, для отдельно отрисовываемой формы внутри компонента. -Компоненты обычно используют одну свежую технику, которую мы любим называть стилем Голливуда. Вы наверняка знаете крылатую фразу, которую так часто слышат участники кинопроб: «Не звоните нам, мы вам позвоним». Именно об этом и идет речь. -В Nette вместо того, чтобы постоянно спрашивать («была ли отправлена форма?», «было ли это валидно?» или «нажал ли пользователь эту кнопку?»), вы говорите фреймворку «когда это произойдет, вызови этот метод» и оставляете дальнейшую работу ему. Если вы программируете на JavaScript, этот стиль программирования вам хорошо знаком. Вы пишете функции, которые вызываются, когда происходит определенное событие. И язык передает им соответствующие параметры. +Голливудский стиль +================== -Это полностью меняет взгляд на написание приложений. Чем больше задач вы можете оставить фреймворку, тем меньше у вас работы. И тем меньше вы можете что-то упустить. +Компоненты обычно используют бодрый приём, который мы любим называть голливудским стилем. Вы наверняка знаете штамп, который часто слышат участники кинопроб: "Не звоните нам, мы позвоним вам". Именно об этом и речь. +В Nette вместо того, чтобы постоянно задавать вопросы ("была ли форма отправлена?", "была ли она корректна?", "нажал ли пользователь эту кнопку?"), вы говорите фреймворку: "когда произойдёт вот это, вызови этот метод", и оставляете дальнейшую работу ему. Если вы программируете на JavaScript, этот стиль вам близко знаком. Вы пишете функции, которые вызываются, когда происходит определённое событие. И язык передаёт им нужные параметры. -Пишем компонент -=============== +Это полностью меняет взгляд на написание приложений. Чем больше задач вы можете оставить фреймворку, тем меньше у вас работы. И тем меньше вы можете упустить. -Под понятием компонент обычно подразумевается потомок класса [api:Nette\Application\UI\Control]. (Точнее было бы использовать термин «controls», но «контролы» в русском языке имеют совершенно другое значение, и скорее прижились «компоненты».) Сам презентер [api:Nette\Application\UI\Presenter] также является потомком класса `Control`. + +Написание компонента +==================== + +Под словом "компонент" мы обычно понимаем потомка класса [api:Nette\Application\UI\Control]. (Точнее было бы говорить "controls", но в некоторых языках это слово имеет другое значение, и "компоненты" прижилось лучше.) Сам презентер [api:Nette\Application\UI\Presenter] тоже является потомком класса `Control`. ```php .{file:PollControl.php} use Nette\Application\UI\Control; @@ -87,7 +92,7 @@ class PollControl extends Control Отрисовка ========= -Мы уже знаем, что для отрисовки компонента используется тег `{control componentName}`. Он фактически вызывает метод `render()` компонента, в котором мы заботимся об отрисовке. В нашем распоряжении, точно так же, как и в презентере, есть [шаблон Latte |templates] в переменной `$this->template`, в который мы передаем параметры. В отличие от презентера, мы должны указать файл с шаблоном и заставить его отрисоваться: +Мы уже знаем, что для отрисовки компонента служит тег `{control componentName}`. На деле он вызывает метод `render()` компонента, в котором мы заботимся об отрисовке. У нас есть, как и в презентере, [шаблон Latte|templates] в переменной `$this->template`, куда мы передаём параметры. В отличие от презентера, мы обязаны указать файл шаблона и заставить его отрисоваться: ```php .{file:PollControl.php} public function render(): void @@ -112,7 +117,7 @@ public function render(int $id, string $message): void } ``` -Иногда компонент может состоять из нескольких частей, которые мы хотим отрисовывать отдельно. Для каждой из них мы создадим собственный метод отрисовки, здесь в примере, например, `renderPaginator()`: +Иногда компонент может состоять из нескольких частей, которые мы хотим отрисовывать по отдельности. Для каждой из них мы создаём собственный метод отрисовки, здесь в примере `renderPaginator()`: ```php .{file:PollControl.php} public function renderPaginator(): void @@ -121,69 +126,69 @@ public function renderPaginator(): void } ``` -А в шаблоне мы затем вызовем его с помощью: +А в шаблоне вызываем его так: ```latte {control poll:paginator} ``` -Для лучшего понимания полезно знать, как этот тег переводится в PHP. +Для лучшего понимания полезно знать, как этот тег превращается в PHP-код. ```latte {control poll} {control poll:paginator 123, 'hello'} ``` -переводится как: +превращается в: ```php $control->getComponent('poll')->render(); $control->getComponent('poll')->renderPaginator(123, 'hello'); ``` -Метод `getComponent()` возвращает компонент `poll`, и над этим компонентом вызывается метод `render()`, соответственно `renderPaginator()`, если в теге после двоеточия указан другой способ рендеринга. +Метод `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()` +- `$basePath` - абсолютный путь URL к корневому каталогу (например, `/eshop`) +- `$baseUrl` - абсолютный URL корневого каталога (например, `http://localhost/eshop`) +- `$user` - объект, [представляющий пользователя |security:authentication] +- `$presenter` - текущий презентер +- `$control` - текущий компонент +- `$flashes` - массив [сообщений |#Flash-сообщения], отправленных функцией `flashMessage()` Сигнал ====== -Мы уже знаем, что навигация в приложении Nette заключается в ссылках или перенаправлениях на пары `Presenter:action`. Но что, если мы просто хотим выполнить действие на **текущей странице**? Например, изменить сортировку столбцов в таблице; удалить элемент; переключить светлый/темный режим; отправить форму; проголосовать в опросе; и т. д. +Мы уже знаем, что навигация в приложении Nette состоит из ссылок или перенаправлений на пары `Презентер:действие`. Но что, если мы хотим просто выполнить действие на **текущей странице**? Например, изменить сортировку столбцов таблицы; удалить элемент; переключить светлый или тёмный режим; отправить форму; проголосовать в опросе и так далее. -Этот тип запросов называется сигналами. И подобно тому, как действия вызывают методы `action()` или `render()`, сигналы вызывают методы `handle()`. В то время как понятие действия (или view) связано исключительно с презентерами, сигналы относятся ко всем компонентам. И, следовательно, и к презентерам, потому что `UI\Presenter` является потомком `UI\Control`. +Такой вид запроса называется сигналом. И так же как действия вызывают методы `action()` или `render()`, сигналы вызывают методы `handle()`. Понятие действия (или представления) относится исключительно к презентерам, а сигналы касаются всех компонентов. А значит, и презентеров, потому что `UI\Presenter` - потомок `UI\Control`. ```php public function handleClick(int $x, int $y): void @@ -192,36 +197,36 @@ public function handleClick(int $x, int $y): void } ``` -Ссылку, которая вызывает сигнал, мы создаем обычным способом, то есть в шаблоне атрибутом `n:href` или тегом `{link}`, в коде методом `link()`. Подробнее в главе [Создание URL-ссылок |creating-links#Ссылки на сигнал]. +Ссылка, вызывающая сигнал, создаётся обычным способом, то есть в шаблоне атрибутом `n:href` или тегом `{link}`, а в коде методом `link()`. Подробнее в главе [Создание URL-ссылок |creating-links#Ссылки на сигнал]. ```latte -нажмите здесь +click here ``` -Сигнал всегда вызывается на текущем презентере и action, невозможно вызвать его на другом презентере или другом action. +Сигнал всегда вызывается на текущем презентере и действии; вызвать его на другом презентере или действии нельзя. -Таким образом, сигнал вызывает перезагрузку страницы точно так же, как при первоначальном запросе, только дополнительно вызывает метод обработки сигнала с соответствующими параметрами. Если метод не существует, выбрасывается исключение [api:Nette\Application\UI\BadSignalException], которое отображается пользователю как страница ошибки 403 Forbidden. +Таким образом, сигнал вызывает перезагрузку страницы точно так же, как исходный запрос, но дополнительно вызывает метод обработки сигнала с нужными параметрами. Если метода не существует, выбрасывается исключение [api:Nette\Application\UI\BadSignalException], которое показывается пользователю как страница ошибки 403 Forbidden. Сниппеты и AJAX =============== -Сигналы могут немного напомнить вам AJAX: обработчики, которые вызываются на текущей странице. И вы правы, сигналы действительно часто вызываются с помощью AJAX, и затем мы передаем в браузер только измененные части страницы. То есть так называемые сниппеты. Дополнительную информацию можно найти на [странице, посвященной AJAX |ajax]. +Сигналы могут немного напомнить вам AJAX: обработчики, вызываемые на текущей странице. И вы правы, сигналы действительно часто вызываются через AJAX, и затем в браузер передаются только изменившиеся части страницы. Они называются сниппетами. Подробнее на [странице, посвящённой AJAX |ajax]. Flash-сообщения =============== -Компонент имеет собственное хранилище flash-сообщений, независимое от презентера. Это сообщения, которые, например, информируют о результате операции. Важной особенностью flash-сообщений является то, что они доступны в шаблоне даже после перенаправления. Даже после отображения они остаются активными еще 30 секунд — например, на случай, если из-за ошибки передачи пользователь обновит страницу — сообщение не исчезнет сразу. +У компонента есть собственное хранилище flash-сообщений, независимое от презентера. Это сообщения, которые, например, извещают о результате операции. Важная особенность flash-сообщений в том, что они доступны в шаблоне и после перенаправления. Даже после показа они остаются активными ещё 30 секунд: например, на случай, если пользователь обновит страницу из-за ошибки передачи, сообщение не исчезнет сразу. -Отправку обеспечивает метод [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Первым параметром является текст сообщения или объект `stdClass`, представляющий сообщение. Необязательным вторым параметром является его тип (error, warning, info и т. д.). Метод `flashMessage()` возвращает экземпляр flash-сообщения как объект `stdClass`, к которому можно добавлять дополнительную информацию. +Отправкой занимается метод [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Первый параметр - текст сообщения (`string`, `Stringable`) или объект `stdClass`, представляющий сообщение. Необязательный второй параметр - его тип (error, warning, info и так далее). Метод `flashMessage()` возвращает экземпляр flash-сообщения как объект `stdClass`, в который можно добавить дополнительные сведения. ```php -$this->flashMessage('Элемент был удален.'); +$this->flashMessage('Item was deleted.'); $this->redirect(/* ... */); // и перенаправляем ``` -В шаблоне эти сообщения доступны в переменной `$flashes` как объекты `stdClass`, которые содержат свойства `message` (текст сообщения), `type` (тип сообщения) и могут содержать уже упомянутую пользовательскую информацию. Отрисуем их, например, так: +Эти сообщения доступны шаблону в переменной `$flashes` как объекты `stdClass`, содержащие свойства `message` (текст сообщения), `type` (тип сообщения) и, возможно, упомянутые пользовательские сведения. Отрисовываем мы их, например, так: ```latte {foreach $flashes as $flash} @@ -230,36 +235,36 @@ $this->redirect(/* ... */); // и перенаправляем ``` -Перенаправление после сигнала -============================= +Перенаправление после обработки сигнала +======================================= -После обработки сигнала компонента часто следует перенаправление. Это похожая ситуация, как с формами — после их отправки мы также перенаправляем, чтобы при обновлении страницы в браузере не произошло повторной отправки данных. +За обработкой сигнала компонента часто следует перенаправление. Это похоже на формы: после их отправки мы тоже перенаправляем, чтобы данные не отправились повторно при обновлении страницы в браузере. ```php -$this->redirect('this') // перенаправляет на текущий презентер и action +$this->redirect('this'); // перенаправляет на текущие презентер и действие ``` -Поскольку компонент является повторно используемым элементом и обычно не должен иметь прямой связи с конкретными презентерами, методы `redirect()` и `link()` автоматически интерпретируют параметр как сигнал компонента: +Поскольку компонент - переиспользуемый элемент, который обычно не должен быть напрямую связан с конкретными презентерами, методы `redirect()` и `link()` автоматически истолковывают параметр как сигнал компонента: ```php -$this->redirect('click') // перенаправляет на сигнал 'click' того же компонента +$this->redirect('click'); // перенаправляет на сигнал 'click' того же компонента ``` -Если вам нужно перенаправить на другой презентер или действие, вы можете сделать это через презентер: +Если вам нужно перенаправить на другой презентер или действие, это можно сделать через презентер: ```php -$this->getPresenter()->redirect('Product:show'); // перенаправляет на другой презентер/action +$this->getPresenter()->redirect('Product:show'); // перенаправляет на другой презентер или действие ``` -Персистентные параметры -======================= +Постоянные параметры +==================== -Персистентные параметры служат для поддержания состояния в компонентах между различными запросами. Их значение остается неизменным даже после нажатия на ссылку. В отличие от данных в сессии, они передаются в URL. И это происходит полностью автоматически, включая ссылки, созданные в других компонентах на той же странице. +Постоянные параметры служат для сохранения состояния компонентов между разными запросами. Их значение остаётся тем же и после щелчка по ссылке. В отличие от данных сессии, они передаются в URL. И происходит это полностью автоматически, включая ссылки, созданные в других компонентах на той же странице. -Например, у вас есть компонент для постраничной навигации контента. Таких компонентов на странице может быть несколько. И мы хотим, чтобы после нажатия на ссылку все компоненты остались на своей текущей странице. Поэтому мы сделаем номер страницы (`page`) персистентным параметром. +Например, у вас есть компонент постраничного вывода содержимого. Таких компонентов на странице может быть несколько. И мы хотим, чтобы все компоненты после щелчка по ссылке остались на своей текущей странице. Поэтому мы делаем номер страницы (`page`) постоянным параметром. -Создание персистентного параметра в Nette невероятно просто. Достаточно создать публичное свойство и пометить его атрибутом: (ранее использовалось `/** @persistent */`) +Создать постоянный параметр в Nette исключительно просто. Достаточно создать публичное свойство и пометить его атрибутом (раньше использовалось `/** @persistent */`): ```php use Nette\Application\Attributes\Persistent; // эта строка важна @@ -271,43 +276,43 @@ class PaginatingControl extends Control } ``` -У свойства рекомендуется указывать тип данных (например, `int`), и вы можете указать значение по умолчанию. Значения параметров можно [валидировать |#Валидация персистентных параметров]. +Мы рекомендуем указывать тип данных свойства (например, `int`), а также можно задать значение по умолчанию. Значения параметров можно [проверять |#Проверка постоянных параметров]. -При создании ссылки можно изменить значение персистентного параметра: +При создании ссылки значение постоянного параметра можно изменить: ```latte -следующая +next ``` -Или его можно *сбросить*, то есть удалить из URL. Тогда он примет свое значение по умолчанию: +Или его можно *сбросить*, то есть убрать из URL. Тогда он примет значение по умолчанию: ```latte -сбросить +reset ``` -Персистентные компоненты -======================== +Постоянные компоненты +===================== -Не только параметры, но и компоненты могут быть персистентными. У такого компонента его персистентные параметры передаются и между различными действиями презентера, или между несколькими презентерами. Персистентные компоненты помечаются аннотацией у класса презентера. Например, так мы пометим компоненты `calendar` и `poll`: +Постоянными могут быть не только параметры, но и компоненты. Их постоянные параметры тогда передаются даже между разными действиями презентера или между несколькими презентерами. Постоянные компоненты мы помечаем атрибутом на классе презентера. Например, компоненты `calendar` и `poll` мы помечаем так: ```php -/** - * @persistent(calendar, poll) - */ +use Nette\Application\Attributes\Persistent; + +#[Persistent('calendar', 'poll')] class DefaultPresenter extends Nette\Application\UI\Presenter { } ``` -Подкомпоненты внутри этих компонентов помечать не нужно, они также станут персистентными. +Подкомпоненты внутри этих компонентов помечать не нужно, они тоже становятся постоянными. -В PHP 8 вы можете использовать атрибуты для обозначения персистентных компонентов: +Более старая аннотация `@persistent` ещё работает, но объявлена устаревшей и выдаёт предупреждение: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } @@ -317,15 +322,15 @@ class DefaultPresenter extends Nette\Application\UI\Presenter Компоненты с зависимостями ========================== -Как создавать компоненты с зависимостями, не «засоряя» презентеры, которые их будут использовать? Благодаря умным свойствам DI-контейнера в Nette, так же как при использовании классических сервисов, можно оставить большую часть работы фреймворку. +Как создавать компоненты с зависимостями, не "захламляя" презентеры, которые будут их использовать? Благодаря продуманным возможностям DI-контейнера Nette, как и в случае с обычными сервисами, большую часть работы можно оставить фреймворку. -Возьмем в качестве примера компонент, который имеет зависимость от сервиса `PollFacade`: +Возьмём для примера компонент, зависящий от сервиса `PollFacade`: ```php class PollControl extends Control { public function __construct( - private int $id, // Id опроса, для которого мы создаем компонент + private int $id, // ID опроса, для которого мы создаём компонент private PollFacade $facade, ) { } @@ -338,11 +343,11 @@ class PollControl extends Control } ``` -Если бы мы писали классический сервис, проблем бы не было. О передаче всех зависимостей невидимо позаботился бы DI-контейнер. Однако с компонентами мы обычно обращаемся так, что создаем их новый экземпляр прямо в презентере в [фабричных методах |#Фабричные методы] `createComponent…()`. Но передавать все зависимости всех компонентов в презентер, чтобы затем передать их компонентам, громоздко. И сколько написанного кода… +Если бы мы писали обычный сервис, обсуждать было бы нечего. DI-контейнер незаметно позаботился бы о передаче всех зависимостей. Однако с компонентами мы обычно обходимся созданием нового экземпляра прямо в презентере, в [фабричных методах |#Фабричные методы] `createComponent…()`. Но передавать в презентер все зависимости всех компонентов только для того, чтобы передать их дальше компонентам, утомительно. И сколько кода приходится писать... -Логичный вопрос: почему бы просто не зарегистрировать компонент как классический сервис, не передать его в презентер и затем в методе `createComponent…()` не возвращать? Такой подход, однако, неуместен, потому что мы хотим иметь возможность создавать компонент даже несколько раз. +Логично спросить: почему бы просто не зарегистрировать компонент как обычный сервис, передать его в презентер и затем возвращать в методе `createComponent…()`? Однако такой подход неуместен, потому что мы хотим иметь возможность при необходимости создавать компонент несколько раз. -Правильным решением является написание для компонента фабрики, то есть класса, который нам создаст компонент: +Правильное решение - написать для компонента фабрику, то есть класс, который создаёт компонент за нас: ```php class PollControlFactory @@ -359,14 +364,14 @@ class PollControlFactory } ``` -Так мы зарегистрируем фабрику в нашем контейнере в конфигурации: +Эту фабрику мы регистрируем в нашем контейнере в конфигурации: ```neon services: - PollControlFactory ``` -и, наконец, используем ее в нашем презентере: +и наконец используем её в своём презентере: ```php class PollPresenter extends Nette\Application\UI\Presenter @@ -378,13 +383,13 @@ class PollPresenter extends Nette\Application\UI\Presenter protected function createComponentPollControl(): PollControl { - $pollId = 1; // можем передать наш параметр + $pollId = 1; // мы можем передать свой параметр return $this->pollControlFactory->create($pollId); } } ``` -Замечательно то, что Nette DI умеет [генерировать |dependency-injection:factory] такие простые фабрики, поэтому вместо всего ее кода достаточно написать только ее интерфейс: +Прекрасно то, что Nette DI умеет [порождать |dependency-injection:factory] такие простые фабрики, поэтому вместо всего её кода вам достаточно написать её интерфейс: ```php interface PollControlFactory @@ -393,21 +398,21 @@ interface PollControlFactory } ``` -И это все. Nette внутренне реализует этот интерфейс и передаст его в презентер, где мы уже можем его использовать. Он волшебным образом добавит в наш компонент и параметр `$id`, и экземпляр класса `PollFacade`. +И это всё. Nette внутренне реализует этот интерфейс и внедрит его в презентер, где мы сможем им пользоваться. Он волшебным образом добавит в наш компонент параметр `$id` и экземпляр класса `PollFacade`. -Компоненты в глубину -==================== +Компоненты в подробностях +========================= -Компоненты в Nette Application представляют собой повторно используемые части веб-приложения, которые мы вставляем на страницы и которым, собственно, посвящена вся эта глава. Какие именно возможности имеет такой компонент? +Компоненты в Nette Application представляют переиспользуемые части веб-приложения, которые мы встраиваем в страницы и которым посвящена вся эта глава. Каковы же в точности возможности такого компонента? -1) он может быть отрисован в шаблоне -2) он знает, [какую свою часть |ajax#Сниппеты] нужно отрисовать при AJAX-запросе (сниппеты) -3) он имеет возможность сохранять свое состояние в URL (персистентные параметры) -4) он имеет возможность реагировать на действия пользователя (сигналы) -5) он создает иерархическую структуру (где корнем является презентер) +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:]. +За каждую из этих функций отвечает один из классов в цепочке наследования. За отрисовку (1 + 2) отвечает [api:Nette\Application\UI\Control], за встраивание в [жизненный цикл |presenters#Жизненный цикл презентера] (3, 4) - класс [api:Nette\Application\UI\Component], а за создание иерархической структуры (5) - классы [Container и Component |component-model:]. ``` Nette\ComponentModel\Component { IComponent } @@ -428,12 +433,12 @@ Nette\ComponentModel\Component { IComponent } [* lifecycle-component.svg *] *** *Жизненный цикл компонента* .<> -Валидация персистентных параметров ----------------------------------- +Проверка постоянных параметров +------------------------------ -Значения [персистентных параметров |#Персистентные параметры], полученные из URL, записываются в свойства методом `loadState()`. Он также проверяет, соответствует ли тип данных, указанный у свойства, иначе отвечает ошибкой 404, и страница не отображается. +Значения [постоянных параметров |#Постоянные параметры], полученные из URL, записываются в свойства методом `loadState()`. Он также проверяет, соответствуют ли они типу данных, указанному у свойства; иначе он отвечает ошибкой 404, и страница не отображается. -Никогда слепо не доверяйте персистентным параметрам, потому что они могут быть легко изменены пользователем в URL. Так, например, мы проверим, что номер страницы `$this->page` больше 0. Подходящий способ — переопределить упомянутый метод `loadState()`: +Никогда не доверяйте постоянным параметрам вслепую, потому что пользователь легко может переписать их в URL. Вот так мы проверяем, например, что номер страницы `$this->page` больше 0. Подходящий способ - переопределить упомянутый метод `loadState()`: ```php class PaginatingControl extends Control @@ -443,8 +448,8 @@ class PaginatingControl extends Control public function loadState(array $params): void { - parent::loadState($params); // здесь устанавливается $this->page - // следует собственная проверка значения: + parent::loadState($params); // здесь задаётся $this->page + // далее идёт собственная проверка значения: if ($this->page < 1) { $this->error(); } @@ -452,27 +457,41 @@ class PaginatingControl extends Control } ``` -Обратный процесс, то есть сбор значений из персистентных свойств, отвечает метод `saveState()`. +Обратным процессом, то есть сбором значений из постоянных свойств, занимается метод `saveState()`. + + +Подключение к презентеру +------------------------ + +В момент, когда компонент становится частью иерархии презентера, вызываются его callback-функции, хранящиеся в массиве `$onAnchor`. С этого момента у компонента есть презентер, он может безопасно создавать ссылки, читать постоянные параметры и так далее. + +```php +$control->onAnchor[] = function ($control): void { + // у компонента теперь есть презентер +}; +``` + +Сигналы в подробностях +---------------------- -Сигналы в глубину ------------------ +Сигнал вызывает перезагрузку страницы ровно так же, как исходный запрос (кроме случая вызова через AJAX), и вызывает метод `signalReceived($signal)`, реализация которого по умолчанию в классе `Nette\Application\UI\Component` пытается вызвать метод, составленный из слов `handle`. Дальнейшая обработка зависит от объекта. Объекты, наследующие от `Component` (то есть `Control` и `Presenter`), реагируют попыткой вызвать метод `handle` с нужными параметрами. -Сигнал вызывает перезагрузку страницы точно так же, как при первоначальном запросе (кроме случая, когда он вызывается AJAX-ом), и вызывает метод `signalReceived($signal)`, чья реализация по умолчанию в классе `Nette\Application\UI\Component` пытается вызвать метод, составленный из слов `handle{signal}`. Дальнейшая обработка зависит от данного объекта. Объекты, наследующие от `Component` (т. е. `Control` и `Presenter`), реагируют так, что пытаются вызвать метод `handle{signal}` с соответствующими параметрами. +Иначе говоря: берётся определение функции `handle` вместе со всеми параметрами, пришедшими с запросом, параметры из URL сопоставляются аргументам по имени, и делается попытка вызвать метод. Например, значение параметра `id` из URL передаётся как аргумент `$id`, `something` из URL - как `$something` и так далее. А если метода не существует, метод `signalReceived` выбрасывает [исключение |api:Nette\Application\UI\BadSignalException]. -Другими словами: берется определение функции `handle{signal}` и все параметры, пришедшие с запросом, и к аргументам по имени подставляются параметры из URL, и делается попытка вызвать данный метод. Например, в качестве параметра `$id` передается значение из параметра `id` в URL, в качестве `$something` передается `something` из URL и т. д. И если метод не существует, метод `signalReceived` выбрасывает [исключение |api:Nette\Application\UI\BadSignalException]. +Помимо параметров из URL сигнал читает и параметры, отправленные в **теле POST-запроса**. Это удобно, потому что сигналы часто вызываются из JavaScript, где данные естественно отправлять методом POST. Однако если параметр с одним и тем же именем приходит и из URL, и из тела POST, приоритет имеет значение **из URL**. Поэтому не давайте полю POST такое же имя, как у параметра URL или маршрута, иначе значение из URL молча его перекроет. Параметры сигнала делят общее пространство с параметрами действия и постоянными параметрами, см. [Общее пространство параметров |presenters#Общее пространство параметров]. -Сигнал может принимать любой компонент, презентер или объект, который реализует интерфейс `SignalReceiver` и подключен к дереву компонентов. +Сигнал может принять любой компонент, презентер или объект, который реализует интерфейс `SignalReceiver` и подключён к дереву компонентов. -Основными получателями сигналов будут `Presenters` и визуальные компоненты, наследующие от `Control`. Сигнал должен служить знаком для объекта, что он должен что-то сделать — опрос должен засчитать голос пользователя, блок с новостями должен развернуться и показать в два раза больше новостей, форма была отправлена и должна обработать данные и т. п. +Главными получателями сигналов будут `Presenters` и визуальные компоненты, наследующие от `Control`. Сигнал предназначен служить объекту знаком, что нужно что-то сделать: опрос должен засчитать голос пользователя, блок новостей должен раскрыться и показать вдвое больше новостей, форма была отправлена и должна обработать данные и так далее. -URL для сигнала создаем с помощью метода [Component::link() |api:Nette\Application\UI\Component::link()]. В качестве параметра `$destination` передаем строку `{signal}!` и в качестве `$args` — массив аргументов, которые мы хотим передать сигналу. Сигнал всегда вызывается на текущем презентере и action с текущими параметрами, параметры сигнала просто добавляются. Кроме того, в самом начале добавляется **параметр `?do`, который определяет сигнал**. +URL для сигнала создаётся методом [Component::link() |api:Nette\Application\UI\Component::link()]. Как параметр `$destination` мы передаём строку `{signal}!`, а как `$args` - массив аргументов, которые хотим передать сигналу. Сигнал всегда вызывается на текущем презентере и действии с текущими параметрами; параметры сигнала лишь добавляются. Кроме того, добавляется **параметр `?do`, задающий сигнал**. -Его формат — либо `{signal}`, либо `{signalReceiver}-{signal}`. `{signalReceiver}` — это имя компонента в презентере. Поэтому в имени компонента не может быть дефиса — он используется для разделения имени компонента и сигнала, однако таким образом можно вложить несколько компонентов. +Его формат - либо `{signal}`, либо `{signalReceiver}-{signal}`. `{signalReceiver}` - имя компонента в презентере. Поэтому в имени компонента нельзя использовать дефис: он служит для разделения имени компонента и сигнала, хотя таким способом можно вкладывать несколько компонентов друг в друга. -Метод [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] проверяет, является ли компонент (первый аргумент) получателем сигнала (второй аргумент). Второй аргумент можно опустить — тогда проверяется, является ли компонент получателем любого сигнала. В качестве второго параметра можно указать `true`, чтобы проверить, является ли получателем не только указанный компонент, но и любой его потомок. +Метод [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] проверяет, является ли компонент (первый аргумент) получателем сигнала (второй аргумент). Второй аргумент можно опустить, тогда проверяется, является ли компонент получателем какого-либо сигнала. Если второй параметр установлен в `true`, проверяется, является ли получателем указанный компонент или любой из его потомков. -На любом этапе, предшествующем `handle{signal}`, мы можем выполнить сигнал вручную, вызвав метод [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], который берет на себя обработку сигнала — берет компонент, который определен как получатель сигнала (если получатель сигнала не указан, это сам презентер), и отправляет ему сигнал. +На любом этапе, предшествующем `handle`, мы можем выполнить сигнал вручную, вызвав метод [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], который заботится об обработке сигнала: он берёт компонент, определённый как получатель сигнала (если получатель не указан, это сам презентер), и отправляет ему сигнал. Пример: @@ -482,4 +501,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -Таким образом, сигнал выполняется преждевременно и больше не будет вызываться. +Это выполняет сигнал досрочно, и повторно он вызван не будет. diff --git a/application/ru/configuration.texy b/application/ru/configuration.texy index 4f2aa91381..7b37255d05 100644 --- a/application/ru/configuration.texy +++ b/application/ru/configuration.texy @@ -1,8 +1,8 @@ -Конфигурация приложений +Конфигурация приложения *********************** .[perex] -Обзор опций конфигурации для приложений Nette. +Обзор параметров конфигурации Nette Application. Application @@ -10,54 +10,56 @@ Application ```neon application: - # отображать панель "Nette Application" в Tracy BlueScreen? - debugger: ... # (bool) по умолчанию true + # показывать панель "Nette Application" в Tracy BlueScreen? + debugger: ... # (bool) включено при наличии Tracy - # будет ли при ошибке вызываться error-presenter? - # имеет эффект только в режиме разработки - catchExceptions: ... # (bool) по умолчанию true + # в продакшене исключения всегда обрабатывает error-презентер; + # этот параметр лишь включает такое поведение и в режиме разработки + catchExceptions: ... # (bool) по умолчанию false, то есть выключено в dev, всегда включено в продакшене - # имя error-presenter + # имя error-презентера errorPresenter: Error # (string|array) по умолчанию 'Nette:Error' - # определяет псевдонимы для презентеров и действий + # задаёт псевдонимы презентеров и действий aliases: ... - # определяет правила для перевода имени презентера в класс + # задаёт правила преобразования имени презентера в класс mapping: ... - # неверные ссылки не генерируют предупреждения? - # имеет эффект только в режиме разработки + # подавлять предупреждения о некорректных ссылках? + # действует только в режиме разработки silentLinks: ... # (bool) по умолчанию false ``` -Начиная с версии `nette/application` 3.2, можно определить пару error-presenter'ов: +Начиная с версии `nette/application` 3.2 можно задать пару error-презентеров: ```neon application: errorPresenter: - 4xx: Error4xx # для исключения Nette\Application\BadRequestException + 4xx: Error4xx # для Nette\Application\BadRequestException 5xx: Error5xx # для остальных исключений ``` -Опция `silentLinks` определяет, как Nette поведет себя в режиме разработки, если генерация ссылки не удалась (например, потому что презентер не существует и т. д.). Значение по умолчанию `false` означает, что Nette выбросит ошибку `E_USER_WARNING`. Установка на `true` подавит это сообщение об ошибке. В production-среде `E_USER_WARNING` всегда будет вызываться. Это поведение также можно контролировать, установив переменную презентера [$invalidLinkMode |creating-links#Недействительные ссылки]. +Разделять их полезно, потому что эти две ситуации принципиально различны. `BadRequestException` (коды 4xx) означает, что с приложением всё в порядке и просто посетитель запросил то, чего не существует. Поэтому вы можете использовать полноценный презентер, показывающий дружелюбное сообщение в макете вашего сайта. Напротив, ошибка 5xx означает, что в приложении что-то сломалось и вы не знаете что. Держите презентер для 5xx максимально простым, чтобы при его отрисовке уже ничто не могло отказать: в идеале он не должен трогать ни базу данных, ни макет, ни вошедшего пользователя. -[Псевдонимы упрощают создание ссылок |creating-links#Псевдонимы] на часто используемые презентеры. +Параметр `silentLinks` определяет, как ведёт себя Nette в режиме разработки, когда порождение ссылки не удаётся (например, потому что презентера не существует). Значение по умолчанию `false` означает, что Nette выдаёт ошибку `E_USER_WARNING`. Значение `true` подавляет это сообщение. В производственной среде `E_USER_WARNING` выдаётся всегда. На это поведение можно повлиять и переменной презентера [$invalidLinkMode |creating-links#Некорректные ссылки]. -[Маппинг определяет правила |directory-structure#Маппинг презентеров], по которым из имени презентера выводится имя класса. +[Псевдонимы упрощают обращение |creating-links#Псевдонимы] к часто используемым презентерам. + +[Mapping задаёт правила |directory-structure#Отображение презентеров], по которым имя класса выводится из имени презентера. Автоматическая регистрация презентеров -------------------------------------- -Nette автоматически добавляет презентеры как сервисы в DI-контейнер, что значительно ускоряет их создание. Как Nette находит презентеры, можно настроить: +Nette автоматически добавляет презентеры как сервисы в DI-контейнер, что заметно ускоряет их создание. То, как Nette находит презентеры, можно настроить: ```neon application: - # искать презентеры в Composer class map? + # искать презентеры в class map Composer? scanComposer: ... # (bool) по умолчанию true - # маска, которой должно соответствовать имя класса и файла + # маска, которой должны соответствовать имя класса и имя файла scanFilter: ... # (string) по умолчанию '*Presenter' # в каких каталогах искать презентеры? @@ -65,7 +67,7 @@ application: - %vendorDir%/mymodule ``` -Каталоги, указанные в `scanDirs`, не перезаписывают значение по умолчанию `%appDir%`, а дополняют его, `scanDirs` таким образом будет содержать оба пути `%appDir%` и `%vendorDir%/mymodule`. Если мы хотим исключить каталог по умолчанию, используем [восклицательный знак |dependency-injection:configuration#Слияние], который перезапишет значение: +Каталоги, перечисленные в `scanDirs`, не заменяют значение по умолчанию `%appDir%`, а дополняют его, поэтому в `scanDirs` окажутся оба пути: `%appDir%` и `%vendorDir%/mymodule`. Если мы хотим обойтись без каталога по умолчанию, мы используем [восклицательный знак |dependency-injection:configuration#Слияние]: ```neon application: @@ -73,36 +75,42 @@ application: - %vendorDir%/mymodule ``` -Сканирование каталогов можно отключить, указав значение false. Не рекомендуем полностью подавлять автоматическое добавление презентеров, так как это приведет к снижению производительности приложения. +Сканирование каталогов можно отключить значением `false`. Тогда презентеры больше не регистрируются как сервисы, поэтому их нельзя настроить через секцию [decorator |dependency-injection:configuration#Decorator], а их создание идёт медленнее. Поэтому мы не рекомендуем полностью отключать автоматическую регистрацию: это снизит производительность приложения. Шаблоны Latte ============= -С помощью этой настройки можно глобально повлиять на поведение Latte в компонентах и презентерах. +Эта настройка глобально влияет на поведение Latte в компонентах и презентерах. ```neon latte: - # отображать панель Latte в Tracy Bar для главного шаблона (true) или всех компонентов (all)? - debugger: ... # (true|false|'all') по умолчанию true + # показывать панель Latte в Tracy Bar для главного шаблона (true) или для всех компонентов (all)? + debugger: ... # (true|false|'all') включено при наличии Tracy (только в режиме отладки) - # генерирует шаблоны с заголовком declare(strict_types=1) + # порождать шаблоны с заголовком declare(strict_types=1) strictTypes: ... # (bool) по умолчанию false - # включает режим [строгого парсера |latte:develop#strict-mode] + # включить [строгий режим разбора |latte:develop#strict mode] strictParsing: ... # (bool) по умолчанию false - # активирует [проверку сгенерированного кода |latte:develop#checking-generated-code] + # ограничивает область видимости переменных телом цикла + scopedLoopVariables: ... # (bool) по умолчанию false + + # убирает отступы, возникающие из-за вложенности в парные теги + dedent: ... # (bool) по умолчанию false + + # включить [проверку порождённого кода |latte:develop#Checking Generated Code] phpLinter: ... # (string) по умолчанию null - # устанавливает локаль - locale: ru_RU # (string) по умолчанию null + # задать локаль + locale: cs_CZ # (string) по умолчанию null # класс объекта $this->template templateClass: App\MyTemplateClass # по умолчанию Nette\Bridges\ApplicationLatte\DefaultTemplate ``` -Если вы используете Latte версии 3, вы можете добавлять новые [расширения |latte:extending-latte#Latte Extension] с помощью: +Новые [расширения |latte:extending-latte#Расширение Latte] добавляются так: ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Если вы используете Latte версии 2, вы можете регистрировать новые теги, указав имя класса или ссылку на сервис. По умолчанию вызывается метод `install()`, но это можно изменить, указав имя другого метода: - -```neon -latte: - # регистрация пользовательских тегов Latte - macros: - - App\MyLatteMacros::register # статический метод, classname или callable - - @App\MyLatteMacrosFactory # сервис с методом install() - - @App\MyLatteMacrosFactory::register # сервис с методом register() - -services: - - App\MyLatteMacrosFactory -``` - Маршрутизация ============= @@ -132,14 +126,14 @@ services: ```neon routing: - # отображать панель маршрутизации в Tracy Bar? - debugger: ... # (bool) по умолчанию true + # показывать панель маршрутизации в Tracy Bar? + debugger: ... # (bool) включено при наличии Tracy (только в режиме отладки) - # сериализует маршрутизатор в DI-контейнер + # сериализовать маршрутизатор в DI-контейнер cache: ... # (bool) по умолчанию false ``` -Маршрутизацию обычно определяем в классе [RouterFactory |routing#Коллекция маршрутов]. Альтернативно, маршруты можно определить также в конфигурации с помощью пар `маска: действие`, но этот способ не предлагает такой широкой вариативности в настройке: +Маршрутизация обычно задаётся в классе [RouterFactory |routing#Набор маршрутов]. Как вариант, маршруты можно задать и в конфигурации парами `маска: действие`, но такой способ не даёт большой гибкости: ```neon routing: @@ -152,23 +146,23 @@ routing: Константы ========= -Создание PHP-констант. +Создание констант PHP. ```neon constants: Foobar: 'baz' ``` -После запуска приложения будет создана константа `Foobar`. +Константа `Foobar` будет создана после старта приложения. .[note] -Константы не должны служить некими глобально доступными переменными. Для передачи значений в объекты используйте [внедрение зависимостей |dependency-injection:passing-dependencies]. +Константы не должны служить глобально доступными переменными. Для передачи значений объектам используйте [внедрение зависимостей |dependency-injection:passing-dependencies]. PHP === -Настройка директив PHP. Обзор всех директив можно найти на [php.net |https://www.php.net/manual/en/ini.list.php]. +Настройка директив PHP. Обзор всех директив есть на [php.net |https://www.php.net/manual/en/ini.list.php]. ```neon php: @@ -181,11 +175,12 @@ php: Эти сервисы добавляются в DI-контейнер: -| Имя | Тип | Описание -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [запускающий все приложение |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | фабрика презентеров -| `application.###` | [api:Nette\Application\UI\Presenter] | отдельные презентеры -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | фабрика объекта `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | фабрика для [`$this->template` |templates] +| Имя | Тип | Описание +|----------------------------|---------------------------------------------------|----------------------------------------- +| `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 index 99e54aa1ff..2643963f02 100644 --- a/application/ru/creating-links.texy +++ b/application/ru/creating-links.texy @@ -3,56 +3,56 @@
    -Создавать ссылки в Nette просто, как указывать пальцем. Достаточно просто направить, и фреймворк уже сделает всю работу за вас. Мы покажем: +Создавать ссылки в Nette так же просто, как показать пальцем. Достаточно прицелиться, а всю работу сделает фреймворк. Мы покажем: -- как создавать ссылки в шаблонах и в других местах +- как создавать ссылки в шаблонах и не только - как отличить ссылку на текущую страницу -- что делать с недействительными ссылками +- что делать с некорректными ссылками
    -Благодаря [двусторонней маршрутизации |routing] вам никогда не придется жестко прописывать URL-адреса вашего приложения в шаблонах или коде, которые могут позже измениться, или сложно их составлять. В ссылке достаточно указать презентер и действие, передать возможные параметры, и фреймворк сам сгенерирует URL. На самом деле, это очень похоже на вызов функции. Вам это понравится. +Благодаря [двунаправленной маршрутизации |routing] вам никогда не придётся жёстко прописывать URL вашего приложения в шаблонах или коде, ведь они могут позже измениться или оказаться сложными в сборке. В ссылке достаточно указать презентер и действие, передать параметры, а URL фреймворк породит сам. По сути это очень похоже на вызов функции. Вам понравится. В шаблоне презентера ==================== -Чаще всего мы создаем ссылки в шаблонах, и отличным помощником является атрибут `n:href`: +Чаще всего мы создаём ссылки в шаблонах, и отличный помощник здесь - атрибут `n:href`: ```latte -деталь +detail ``` -Обратите внимание, что вместо HTML-атрибута `href` мы использовали [n:атрибут |latte:syntax#n:атрибуты] `n:href`. Его значением является не URL, как это было бы в случае атрибута `href`, а имя презентера и действия. +Обратите внимание, что вместо HTML-атрибута `href` мы использовали [n:атрибут |latte:syntax#n:атрибуты] `n:href`. Его значением служит не URL, как было бы у атрибута `href`, а имя презентера и действия. -Нажатие на ссылку, упрощенно говоря, похоже на вызов метода `ProductPresenter::renderShow()`. И если у него в сигнатуре есть параметры, мы можем вызвать его с аргументами: +Щелчок по ссылке, попросту говоря, похож на вызов метода `ProductPresenter::renderShow()`. А если у него есть параметры в сигнатуре, мы можем вызвать его с аргументами: ```latte -деталь продукта +product detail ``` -Можно передавать и именованные параметры. Следующая ссылка передает параметр `lang` со значением `cs`: +Можно передавать и именованные параметры. Следующая ссылка передаёт параметр `lang` со значением `en`: ```latte -деталь продукта +product detail ``` -Если метод `ProductPresenter::renderShow()` не имеет `$lang` в своей сигнатуре, он может получить значение параметра с помощью `$lang = $this->getParameter('lang')` или из [свойства |presenters#Параметры запроса]. +Если у метода `ProductPresenter::renderShow()` нет `$lang` в сигнатуре, он может получить значение параметра через `$lang = $this->getParameter('lang')` или из [свойства |presenters#Параметры запроса]. -Если параметры хранятся в массиве, их можно развернуть с помощью оператора `...` (в Latte 2.x оператором `(expand)`): +Если параметры хранятся в массиве, их можно развернуть оператором `...`: ```latte -{var $args = [$product->id, lang => cs]} -деталь продукта +{var $args = [$product->id, lang => en]} +product detail ``` -В ссылках также автоматически передаются так называемые [персистентные параметры |presenters#Персистентные параметры]. +Так называемые [постоянные параметры |presenters#Постоянные параметры] тоже передаются в ссылках автоматически. -Атрибут `n:href` очень удобен для HTML-тегов ``. Если мы хотим вывести ссылку в другом месте, например, в тексте, используем `{link}`: +Атрибут `n:href` очень удобен для HTML-тегов ``. Если мы хотим вывести ссылку в другом месте, например в тексте, мы используем `{link}`: ```latte -Адрес: {link Home:default} +URL is: {link Home:default} ``` @@ -65,96 +65,113 @@ $url = $this->link('Product:show', $product->id); ``` -Параметры можно передать также с помощью массива, где можно указать и именованные параметры: +Параметры можно передать и массивом, где можно указать и именованные параметры: ```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -Ссылки можно создавать и без презентера, для этого есть [#LinkGenerator] и его метод `link()`. +Ссылки можно создавать и без презентера, с помощью [#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 и все методы презентера, работающие со ссылками, то есть `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()`, а также [#LinkGenerator]. Так что, хотя в примерах используется `n:href`, на его месте могла бы быть любая из этих функций. -Основной формой является `Presenter:action`: +Основная форма, таким образом, - `Презентер:действие`: ```latte -главная страница +home page ``` -Если мы ссылаемся на действие текущего презентера, мы можем опустить его имя: +Если мы ссылаемся на действие текущего презентера, его имя можно опустить: ```latte -главная страница +home page ``` -Если целью является действие `default`, мы можем его опустить, но двоеточие должно остаться: +Если целевое действие - `default`, его можно опустить, но двоеточие должно остаться: ```latte -главная страница +home page ``` -Ссылки также могут указывать на другие [модули |directory-structure#Презентеры и шаблоны]. Здесь ссылки делятся на относительные к вложенному подмодулю или абсолютные. Принцип аналогичен путям на диске, только вместо слешей используются двоеточия. Предположим, что текущий презентер является частью модуля `Front`, тогда запишем: +Ссылки могут указывать и на другие [модули |directory-structure#Презентеры и шаблоны]. Здесь ссылки различаются как относительные к вложенному подмодулю или абсолютные. Принцип аналогичен путям на диске, только вместо слешей используются двоеточия. Если считать, что текущий презентер входит в модуль `Front`, мы написали бы: ```latte -ссылка на Front:Shop:Product:show -ссылка на Admin:Product:show +link to Front:Shop:Product:show +link to Admin:Product:show ``` -Особым случаем является ссылка [на себя |#Ссылка на текущую страницу], когда в качестве цели указываем `this`. +Особый случай - ссылка [на саму себя |#Ссылка на текущую страницу], где мы указываем целью `this`. ```latte -обновить +refresh ``` -Мы можем ссылаться на определенную часть страницы через так называемый фрагмент после символа решетки `#`: +Мы можем сослаться на определённую часть страницы через так называемый фрагмент после знака решётки `#`: ```latte -ссылка на Home:default и фрагмент #main +link to Home:default and fragment #main +``` + +.{data-version:3.3.0} +Фрагмент можно задать и динамически, аргументом с ключом `#`. Его значение автоматически кодируется и имеет приоритет над фрагментом, указанным в цели: + +```php +$this->link('Home:default', ['#' => $fragment]); ``` Абсолютные пути =============== -Ссылки, генерируемые с помощью `link()` или `n:href`, всегда являются абсолютными путями (т. е. начинаются с символа `/`), но не абсолютными URL с протоколом и доменом, как `https://domain`. +Ссылки, порождаемые через `link()` или `n:href`, всегда являются абсолютными путями (то есть начинаются с `/`), но не абсолютными URL с протоколом и доменом вроде `https://domain`. + +Чтобы породить абсолютный URL, добавьте в начале два слеша (например, `n:href="//Home:"`). Как вариант, вы можете переключить презентер на порождение только абсолютных ссылок, задав `$this->absoluteUrls = true`. -Для генерации абсолютного URL добавьте в начало два слеша (например, `n:href="//Home:"`). Или можно переключить презентер, чтобы он генерировал только абсолютные ссылки, установив `$this->absoluteUrls = true`. +В шаблоне для преобразования относительного пути в абсолютный можно использовать и фильтр `|absoluteUrl`. Ссылка на текущую страницу ========================== -Цель `this` создаст ссылку на текущую страницу: +Цель `this` создаёт ссылку на текущую страницу: ```latte -обновить +refresh ``` -При этом передаются все параметры, указанные в сигнатуре метода `action()` или `render()`, если `action()` не определена. Так что если мы находимся на странице `Product:show` и `id: 123`, ссылка на `this` передаст и этот параметр. +При этом передаются все параметры, указанные в сигнатуре метода `action()` или `render()` (если `action()` не определён). Так что если мы находимся на странице `Product:show` с `id: 123`, ссылка на `this` передаст и этот параметр. -Конечно, можно указать параметры напрямую: +Разумеется, параметры можно указать и напрямую: ```latte -обновить +refresh ``` -Функция `isLinkCurrent()` проверяет, совпадает ли цель ссылки с текущей страницей. Это можно использовать, например, в шаблоне для выделения ссылок и т. п. +Функция `isLinkCurrent()` проверяет, совпадает ли цель ссылки с текущей страницей. Это можно использовать, например, в шаблоне, чтобы выделять ссылки и подобное. -Параметры такие же, как у метода `link()`, но дополнительно можно вместо конкретного действия указать подстановочный знак `*`, который означает любое действие данного презентера. +Параметры те же, что и у метода `link()`, но вместо конкретного действия можно использовать подстановочный знак `*`, означающий любое действие данного презентера. ```latte {if !isLinkCurrent('Admin:login')} - Войдите + Login {/if}
  • @@ -162,15 +179,15 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']);
  • ``` -В сочетании с `n:href` в одном элементе можно использовать сокращенную форму: +В сочетании с `n:href` в одном элементе можно использовать сокращённую форму: ```latte ... ``` -Подстановочный знак `*` можно использовать только вместо действия, а не презентера. +Подстановочный знак `*` можно использовать только вместо действия, но не вместо презентера. -Для проверки, находимся ли мы в определенном модуле или его подмодуле, используем метод `isModuleCurrent(moduleName)`. +Чтобы определить, находимся ли мы в определённом модуле или его подмодуле, используйте метод `isModuleCurrent(moduleName)`. ```latte
  • @@ -179,41 +196,57 @@ $url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); ``` +Изменение базы ссылок .{data-version:3.2.7} +=========================================== + +По умолчанию относительные ссылки отсчитываются от текущего презентера. Это можно изменить с помощью `{linkBase}`: + +```latte +{linkBase Admin:Dashboard} +product detail +``` + +Ссылка приведёт на `Admin:Dashboard:Product:show`. Затрагиваются только относительные ссылки: абсолютные, начинающиеся с двоеточия, и ссылки на текущий презентер (`this`, `show`) остаются без изменений. + +`{linkBase}` действует на весь шаблон и особенно полезен в шаблонах макетов, где обеспечивает единообразные ссылки независимо от вызывающего презентера. +Тег должен стоять в начале шаблона, иначе он выбросит `CompileException`. + + Ссылки на сигнал ================ -Целью ссылки может быть не только презентер и действие, но и [сигнал |components#Сигнал] (вызывают метод `handle()`). Тогда синтаксис следующий: +Целью ссылки может быть не только презентер и действие, но и [сигнал |components#Сигнал] (он вызывает метод `handle()`). Тогда синтаксис такой: ``` [//] [sub-component:]signal! [#fragment] ``` -Сигнал отличает восклицательный знак: +Сигнал, таким образом, отличается восклицательным знаком: ```latte -сигнал +signal ``` Можно создать и ссылку на сигнал подкомпонента (или под-подкомпонента): ```latte -сигнал +signal ``` Ссылки в компоненте =================== -Поскольку [компоненты |components] являются отдельными повторно используемыми единицами, которые не должны иметь никаких связей с окружающими презентерами, ссылки здесь работают немного иначе. Атрибут Latte `n:href` и тег `{link}`, а также методы компонента, такие как `link()` и другие, считают цель ссылки **всегда именем сигнала**. Поэтому не нужно даже указывать восклицательный знак: +Поскольку [компоненты|components] - самостоятельные переиспользуемые единицы, которые не должны быть связаны с окружающими презентерами, ссылки здесь работают немного иначе. Атрибут Latte `n:href` и тег `{link}`, а также методы компонента вроде `link()` и другие **всегда считают целью ссылки имя сигнала**. Поэтому восклицательный знак даже не нужен: ```latte -сигнал, а не действие +signal, not an action ``` -Если бы мы хотели в шаблоне компонента ссылаться на презентеры, мы бы использовали для этого тег `{plink}`: +Если бы мы хотели сослаться в шаблоне компонента на презентеры, мы использовали бы тег `{plink}`: ```latte -введение +home ``` или в коде @@ -223,12 +256,12 @@ $this->getPresenter()->link('Home:default') ``` -Псевдонимы .{data-version:v3.2.2} -================================= +Псевдонимы .{data-version:3.2.3} +================================ -Иногда может быть полезно присвоить паре Presenter:action легко запоминающийся псевдоним. Например, главную страницу `Front:Home:default` назвать просто `home` или `Admin:Dashboard:default` как `admin`. +Иногда бывает полезно назначить паре "презентер:действие" легко запоминающийся псевдоним. Например, назвать главную страницу `Front:Home:default` просто `home`, а `Admin:Dashboard:default` - `admin`. -Псевдонимы определяются в [конфигурации |configuration] под ключом `application › aliases`: +Псевдонимы задаются в [конфигурации|configuration] под ключом `application › aliases`: ```neon application: @@ -238,26 +271,26 @@ application: sign: Front:Sign:in ``` -В ссылках они затем записываются с помощью символа @, например: +В ссылках они затем записываются со знаком собаки, например: ```latte -администрирование +administration ``` -Они также поддерживаются во всех методах, работающих со ссылками, таких как `redirect()` и подобных. +Они поддерживаются и во всех методах, работающих со ссылками, таких как `redirect()` и подобные. -Недействительные ссылки -======================= +Некорректные ссылки +=================== -Может случиться, что мы создадим недействительную ссылку — либо потому, что она ведет на несуществующий презентер, либо потому, что передает больше параметров, чем принимает целевой метод в своей сигнатуре, либо когда для целевого действия невозможно сгенерировать URL. Как обращаться с недействительными ссылками, определяет статическая переменная `Presenter::$invalidLinkMode`. Она может принимать комбинацию следующих значений (констант): +Может случиться, что мы создадим некорректную ссылку: либо потому, что она ведёт на несуществующий презентер, либо потому, что передаёт больше параметров, чем принимает в сигнатуре целевой метод, либо когда URL для целевого действия породить нельзя. Как обходиться с некорректными ссылками, задаётся в презентере через `$this->invalidLinkMode`. Он может принимать сочетание этих значений (констант): -- `Presenter::InvalidLinkSilent` — тихий режим, в качестве URL возвращается символ # -- `Presenter::InvalidLinkWarning` — выбрасывается предупреждение E_USER_WARNING, которое в режиме production будет залогировано, но не вызовет прерывания выполнения скрипта -- `Presenter::InvalidLinkTextual` — визуальное предупреждение, выводит ошибку прямо в ссылку -- `Presenter::InvalidLinkException` — выбрасывается исключение InvalidLinkException +- `Presenter::InvalidLinkSilent` - молчаливый режим, возвращает в качестве URL символ # +- `Presenter::InvalidLinkWarning` - выдаётся предупреждение E_USER_WARNING, которое в производственном режиме попадёт в лог, но не прервёт выполнение скрипта +- `Presenter::InvalidLinkTextual` - наглядное предупреждение, выводит ошибку прямо в ссылку +- `Presenter::InvalidLinkException` - выбрасывает InvalidLinkException -Настройка по умолчанию — `InvalidLinkWarning` в режиме production и `InvalidLinkWarning | InvalidLinkTextual` в режиме разработки. `InvalidLinkWarning` в production-среде не вызывает прерывания скрипта, но предупреждение будет залогировано. В среде разработки его перехватывает [Tracy |tracy:] и отображает синий экран. `InvalidLinkTextual` работает так, что в качестве URL возвращает сообщение об ошибке, которое начинается символами `#error:`. Чтобы такие ссылки были заметны с первого взгляда, добавим в CSS: +По умолчанию задано `InvalidLinkWarning` в производственном режиме и `InvalidLinkWarning | InvalidLinkTextual` в режиме разработки. `InvalidLinkWarning` в производственной среде не прерывает скрипт, но предупреждение попадает в лог. В среде разработки его перехватывает [Tracy |tracy:] и показывает синий экран. `InvalidLinkTextual` работает так, что возвращает как URL сообщение об ошибке, начинающееся с символов `#error:`. Чтобы такие ссылки бросались в глаза с первого взгляда, добавьте в свой CSS: ```css a[href^="#error:"] { @@ -266,7 +299,7 @@ a[href^="#error:"] { } ``` -Если мы не хотим, чтобы в среде разработки генерировались предупреждения, мы можем установить тихий режим прямо в [конфигурации |configuration]. +Если мы не хотим, чтобы в среде разработки возникали предупреждения, мы можем подавить их прямо в [конфигурации|configuration]. ```neon application: @@ -277,10 +310,10 @@ application: LinkGenerator ============= -Как создавать ссылки с таким же удобством, как у метода `link()`, но без присутствия презентера? Для этого существует [api:Nette\Application\LinkGenerator]. +Как создавать ссылки с таким же удобством, как методом `link()`, но без презентера? Для этого и существует [api:Nette\Application\LinkGenerator]. -LinkGenerator — это сервис, который вы можете получить через конструктор и затем создавать ссылки его методом `link()`. +LinkGenerator - сервис, который вы можете получить через конструктор и затем создавать ссылки его методом `link()`. -По сравнению с презентерами есть разница. LinkGenerator создает все ссылки сразу как абсолютные URL. И далее не существует "текущего презентера", поэтому нельзя в качестве цели указать только имя действия `link('default')` или указывать относительные пути к модулям. +По сравнению с презентерами есть отличие. LinkGenerator создаёт все ссылки сразу как абсолютные URL. Кроме того, "текущего презентера" нет, поэтому нельзя указать целью только имя действия `link('default')` или использовать относительные пути к модулям. -Недействительные ссылки всегда выбрасывают `Nette\Application\UI\InvalidLinkException`. +Некорректные ссылки всегда выбрасывают `Nette\Application\UI\InvalidLinkException`. diff --git a/application/ru/directory-structure.texy b/application/ru/directory-structure.texy index 9c1abc41af..58cb3e5bba 100644 --- a/application/ru/directory-structure.texy +++ b/application/ru/directory-structure.texy @@ -3,43 +3,43 @@
    -Как спроектировать понятную и масштабируемую структуру каталогов для проектов на Nette Framework? Мы покажем проверенные практики, которые помогут вам организовать код. Вы узнаете: +Как спроектировать наглядную и масштабируемую структуру каталогов для проектов на Nette Framework? Мы покажем проверенные практики, которые помогут вам упорядочить код. Вы узнаете: -- как **логически разделить** приложение на каталоги -- как спроектировать структуру так, чтобы она **хорошо масштабировалась** с ростом проекта -- какие существуют **возможные альтернативы** и их преимущества или недостатки +- как **логично разложить** приложение по каталогам +- как спроектировать структуру так, чтобы она **хорошо масштабировалась** по мере роста проекта +- какие есть **возможные альтернативы** и в чём их плюсы и минусы
    -Важно отметить, что сам Nette Framework не привязан к какой-либо конкретной структуре. Он разработан так, чтобы его можно было легко адаптировать к любым потребностям и предпочтениям. +Важно упомянуть, что сам Nette Framework не навязывает никакой конкретной структуры. Он спроектирован так, чтобы легко подстраиваться под любые потребности и предпочтения. Базовая структура проекта ========================= -Хотя Nette Framework не диктует никакой жесткой структуры каталогов, существует проверенное стандартное расположение в виде [Web Project |https://github.com/nette/web-project]: +Хотя Nette Framework не диктует никакой фиксированной структуры каталогов, есть проверенное расположение по умолчанию в виде [Web Project|https://github.com/nette/web-project]: /--pre web-project/ -├── app/ ← каталог с приложением -├── assets/ ← файлы SCSS, JS, изображения..., альтернативно resources/ +├── app/ ← каталог приложения +├── assets/ ← файлы SCSS, JS, изображения..., как вариант resources/ ├── bin/ ← скрипты для командной строки ├── config/ ← конфигурация -├── log/ ← логируемые ошибки +├── log/ ← записанные ошибки ├── temp/ ← временные файлы, кеш ├── tests/ ← тесты ├── vendor/ ← библиотеки, установленные Composer └── www/ ← публичный каталог (document-root) \-- -Эту структуру можно произвольно изменять в соответствии с вашими потребностями - папки можно переименовывать или перемещать. Затем достаточно лишь изменить относительные пути к каталогам в файле `Bootstrap.php` и, возможно, `composer.json`. Больше ничего не требуется, никакой сложной реконфигурации, никаких изменений констант. Nette обладает умным автоопределением и автоматически распознает расположение приложения, включая его базовый URL. +Вы можете свободно менять эту структуру под свои нужды: переименовывать или переносить папки. Затем достаточно поправить относительные пути к каталогам в `Bootstrap.php` и, возможно, в `composer.json`. Больше ничего не нужно, никакой сложной перенастройки, никаких изменений констант. У Nette есть умное автоопределение, и он сам распознаёт расположение приложения, включая его базовый URL. Принципы организации кода ========================= -Когда вы впервые изучаете новый проект, вы должны быстро в нем сориентироваться. Представьте, что вы открываете каталог `app/Model/` и видите следующую структуру: +Когда вы впервые изучаете новый проект, вы должны быстро сориентироваться. Представьте, что вы заходите в каталог `app/Model/` и видите такую структуру: /--pre app/Model/ @@ -48,7 +48,7 @@ └── Entities/ \-- -Из нее вы узнаете только то, что проект использует какие-то сервисы, репозитории и сущности. О реальном назначении приложения вы не узнаете абсолютно ничего. +Отсюда вы узнаёте только то, что в проекте используются какие-то сервисы, репозитории и сущности. О настоящем назначении приложения вы не узнаёте ничего. Посмотрим на другой подход - **организацию по доменам**: @@ -60,51 +60,51 @@ └── Product/ \-- -Здесь все иначе - с первого взгляда ясно, что это интернет-магазин. Уже сами названия каталогов говорят о том, что умеет приложение - работает с платежами, заказами и продуктами. +Здесь всё иначе: с первого взгляда ясно, что это интернет-магазин. Сами имена каталогов раскрывают, что умеет приложение: оно работает с платежами, заказами и товарами. -Первый подход (организация по типу классов) на практике приносит ряд проблем: код, который логически связан, разбросан по разным папкам, и вам приходится переключаться между ними. Поэтому мы будем организовывать по доменам. +Первый подход (организация по типам классов) приносит на практике несколько проблем: логически связанный код раздроблен по разным папкам, и вам приходится прыгать между ними. Поэтому мы будем организовывать по доменам. -Пространства имен +Пространства имён ----------------- -Принято, чтобы структура каталогов соответствовала пространствам имен в приложении. Это означает, что физическое расположение файлов соответствует их пространству имен. Например, класс, расположенный в `app/Model/Product/ProductRepository.php`, должен иметь пространство имен `App\Model\Product`. Этот принцип помогает ориентироваться в коде и упрощает автозагрузку. +Принято, чтобы структура каталогов соответствовала пространствам имён приложения. Это значит, что физическое расположение файлов совпадает с их пространством имён. Например, у класса, лежащего в `app/Model/Product/ProductRepository.php`, должно быть пространство имён `App\Model\Product`. Этот принцип помогает ориентироваться в коде и упрощает автозагрузку. -Единственное vs множественное число в названиях ------------------------------------------------ +Единственное и множественное число в именах +------------------------------------------- -Обратите внимание, что для основных каталогов приложения мы используем единственное число: `app`, `config`, `log`, `temp`, `www`. Точно так же и внутри приложения: `Model`, `Core`, `Presentation`. Это потому, что каждый из них представляет собой единую целостную концепцию. +Обратите внимание, что для главных каталогов приложения мы используем единственное число: `app`, `config`, `log`, `temp`, `www`. То же и внутри приложения: `Model`, `Core`, `Presentation`. Это потому, что каждый из них представляет одно цельное понятие. -Аналогично, например, `app/Model/Product` представляет все, что связано с продуктами. Мы не назовем это `Products`, потому что это не папка, полная продуктов (там были бы файлы `nokia.php`, `samsung.php`). Это пространство имен, содержащее классы для работы с продуктами - `ProductRepository.php`, `ProductService.php`. +Точно так же `app/Model/Product` представляет всё, что связано с товарами. Мы не называем его `Products`, потому что это не папка, полная товаров (в ней лежали бы файлы вроде `nokia.php`, `samsung.php`). Это пространство имён с классами для работы с товарами - `ProductRepository.php`, `ProductService.php`. -Папка `app/Tasks` находится во множественном числе, потому что она содержит набор отдельных исполняемых скриптов - `CleanupTask.php`, `ImportTask.php`. Каждый из них является отдельной единицей. +Папка `app/Tasks` во множественном числе, потому что содержит набор отдельных исполняемых скриптов - `CleanupTask.php`, `ImportTask.php`. Каждый из них - самостоятельная единица. -Для согласованности рекомендуем использовать: -- Единственное число для пространства имен, представляющего функциональное целое (даже если оно работает с несколькими сущностями) -- Множественное число для коллекций отдельных единиц -- В случае неопределенности или если вы не хотите об этом думать, выберите единственное число +Ради единообразия мы рекомендуем использовать: +- единственное число для пространств имён, представляющих функциональную единицу (даже если она работает с несколькими сущностями) +- множественное число для наборов самостоятельных единиц +- в случае сомнений или если не хочется над этим думать, выбирайте единственное число Публичный каталог `www/` ======================== -Этот каталог является единственным доступным из веба (так называемый document-root). Часто можно встретить название `public/` вместо `www/` - это всего лишь вопрос соглашения и на функциональность не влияет. Каталог содержит: -- [Точку входа |bootstrapping#index.php] приложения `index.php` -- Файл `.htaccess` с правилами для mod_rewrite (для Apache) -- Статические файлы (CSS, JavaScript, изображения) -- Загруженные файлы +Этот каталог - единственный, доступный из веба (document-root). Часто вместо `www/` вам может встретиться имя `public/` - это лишь вопрос соглашения, на работу приложения оно не влияет. Каталог содержит: +- [точку входа |bootstrapping#index.php] приложения `index.php` +- файл `.htaccess` с правилами для mod_rewrite (для Apache) +- статические файлы (CSS, JavaScript, изображения) +- загруженные файлы -Для правильной защиты приложения крайне важно иметь правильно [настроенный document-root |nette:troubleshooting#Как изменить или удалить каталог www из URL]. +Для правильной безопасности приложения принципиально важно [правильно настроить document-root |nette:troubleshooting#Как изменить или убрать из URL каталог www?]. .[note] -Никогда не размещайте в этом каталоге папку `node_modules/` - она содержит тысячи файлов, которые могут быть исполняемыми и не должны быть общедоступными. +Никогда не помещайте в этот каталог папку `node_modules/`: в ней тысячи файлов, которые могут быть исполняемыми и не должны быть публично доступны. Каталог приложения `app/` ========================= -Это основной каталог с кодом приложения. Базовая структура: +Это главный каталог с кодом приложения. Базовая структура: /--pre app/ @@ -112,24 +112,24 @@ ├── Model/ ← бизнес-логика ├── Presentation/ ← презентеры и шаблоны ├── Tasks/ ← командные скрипты -└── Bootstrap.php ← загрузочный класс приложения +└── Bootstrap.php ← стартовый класс приложения \-- -`Bootstrap.php` — это [стартовый класс приложения |bootstrapping], который инициализирует среду, загружает конфигурацию и создает DI-контейнер. +`Bootstrap.php` - [стартовый класс приложения|bootstrapping], который инициализирует окружение, загружает конфигурацию и создаёт DI-контейнер. -Давайте теперь рассмотрим отдельные подкаталоги подробнее. +Теперь рассмотрим отдельные подкаталоги подробнее. Презентеры и шаблоны ==================== -Презентационная часть приложения находится в каталоге `app/Presentation`. Альтернативой является короткое `app/UI`. Это место для всех презентеров, их шаблонов и возможных вспомогательных классов. +Презентационная часть приложения находится в каталоге `app/Presentation`. Альтернатива - более короткий `app/UI`. Это место для всех презентеров, их шаблонов и любых связанных вспомогательных классов. -Этот слой мы организуем по доменам. В сложном проекте, который сочетает в себе интернет-магазин, блог и API, структура выглядела бы так: +Этот слой мы организуем по доменам. В сложном проекте, объединяющем интернет-магазин, блог и API, структура выглядела бы так: /--pre app/Presentation/ -├── Shop/ ← фронтенд интернет-магазина +├── Shop/ ← витрина интернет-магазина │ ├── Product/ │ ├── Cart/ │ └── Order/ @@ -139,27 +139,27 @@ ├── Admin/ ← администрирование │ ├── Dashboard/ │ └── Products/ -└── Api/ ← конечные точки API +└── Api/ ← точки входа API └── V1/ \-- -Напротив, для простого блога мы бы использовали разделение: +И наоборот, для простого блога мы использовали бы такую структуру: /--pre app/Presentation/ -├── Front/ ← фронтенд сайта +├── Front/ ← публичная часть сайта │ ├── Home/ │ └── Post/ ├── Admin/ ← администрирование │ ├── Dashboard/ │ └── Posts/ ├── Error/ -└── Export/ ← RSS, карты сайта и т. д. +└── Export/ ← RSS, карты сайта и прочее \-- -Папки, такие как `Home/` или `Dashboard/`, содержат презентеры и шаблоны. Папки, такие как `Front/`, `Admin/` или `Api/`, называются **модулями**. Технически это обычные каталоги, которые служат для логического разделения приложения. +Папки вроде `Home/` или `Dashboard/` содержат презентеры и шаблоны. Папки вроде `Front/`, `Admin/` или `Api/` называются **модулями**. Технически это обычные каталоги, служащие для логического разбиения приложения. -Каждая папка с презентером содержит одноименный презентер и его шаблоны. Например, папка `Dashboard/` содержит: +Каждая папка с презентером содержит сам файл презентера и его шаблоны. Например, папка `Dashboard/` содержит: /--pre Dashboard/ @@ -167,7 +167,7 @@ └── default.latte ← шаблон \-- -Эта структура каталогов отражается в пространствах имен классов. Например, `DashboardPresenter` находится в пространстве имен `App\Presentation\Admin\Dashboard` (см. [#маппинг презентеров]): +Эта структура каталогов отражается в пространствах имён классов. Например, `DashboardPresenter` находится в пространстве имён `App\Presentation\Admin\Dashboard` (см. [#Отображение презентеров]): ```php namespace App\Presentation\Admin\Dashboard; @@ -178,22 +178,22 @@ class DashboardPresenter extends Nette\Application\UI\Presenter } ``` -На презентер `Dashboard` внутри модуля `Admin` мы ссылаемся в приложении с помощью двоеточия как на `Admin:Dashboard`. На его действие `default` затем как на `Admin:Dashboard:default`. В случае вложенных модулей мы используем больше двоеточий, например `Shop:Order:Detail:default`. +К презентеру `Dashboard` внутри модуля `Admin` мы обращаемся в приложении записью через двоеточие как `Admin:Dashboard`. К его действию `default` - как `Admin:Dashboard:default`. Для вложенных модулей мы используем несколько двоеточий, например `Shop:Order:Detail:default`. Гибкое развитие структуры ------------------------- -Одним из больших преимуществ этой структуры является то, как элегантно она адаптируется к растущим потребностям проекта. В качестве примера возьмем часть, генерирующую XML-фиды. В начале у нас простая форма: +Одно из больших преимуществ этой структуры в том, как изящно она подстраивается под растущие потребности проекта. Возьмём для примера часть, порождающую XML-ленты. Поначалу у нас простой вид: /--pre Export/ -├── ExportPresenter.php ← один презентер для всех экспортов -├── sitemap.latte ← шаблон для карты сайта -└── feed.latte ← шаблон для RSS-фида +├── ExportPresenter.php ← один презентер для всех лент +├── sitemap.latte ← шаблон карты сайта +└── feed.latte ← шаблон RSS-ленты \-- -Со временем появляются новые типы фидов, и для них требуется больше логики... Нет проблем! Папка `Export/` просто становится модулем: +Со временем добавляются новые виды лент, и логики для них нужно больше... Не беда! Папка `Export/` просто становится модулем: /--pre Export/ @@ -202,52 +202,52 @@ class DashboardPresenter extends Nette\Application\UI\Presenter │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← фид для Zboží.cz - └── heureka.latte ← фид для Heureka.cz + ├── amazon.latte ← лента для Amazon + └── ebay.latte ← лента для eBay \-- -Эта трансформация абсолютно плавная - достаточно создать новые подпапки, распределить в них код и обновить ссылки (например, с `Export:feed` на `Export:Feed:zbozi`). Благодаря этому мы можем постепенно расширять структуру по мере необходимости, уровень вложенности никак не ограничен. +Это преобразование проходит совершенно гладко: достаточно создать новые подпапки, разложить по ним код и обновить ссылки (например, с `Export:feed` на `Export:Feed:amazon`). Благодаря этому мы можем постепенно расширять структуру по мере надобности, уровень вложенности ничем не ограничен. -Если, например, в администрировании у вас много презентеров, связанных с управлением заказами, таких как `OrderDetail`, `OrderEdit`, `OrderDispatch` и т. д., вы можете для лучшей организации в этом месте создать модуль (папку) `Order`, в котором будут (папки для) презентеров `Detail`, `Edit`, `Dispatch` и другие. +Например, если в администрировании у вас много презентеров, связанных с управлением заказами, таких как `OrderDetail`, `OrderEdit`, `OrderDispatch` и другие, для лучшего порядка вы можете создать модуль (папку) `Order`, которая будет содержать (папки для) презентеров `Detail`, `Edit`, `Dispatch` и прочих. Расположение шаблонов --------------------- -В предыдущих примерах мы видели, что шаблоны размещаются прямо в папке с презентером: +В предыдущих примерах мы видели, что шаблоны лежат прямо в папке с презентером: /--pre Dashboard/ ├── DashboardPresenter.php ← презентер -├── DashboardTemplate.php ← необязательный класс для шаблона +├── DashboardTemplate.php ← необязательный класс шаблона └── default.latte ← шаблон \-- -Это расположение на практике оказывается наиболее удобным - все связанные файлы у вас под рукой. +На практике такое расположение оказывается самым удобным: все связанные файлы у вас под рукой. -Альтернативно, вы можете разместить шаблоны в подпапке `templates/`. Nette поддерживает оба варианта. Вы даже можете разместить шаблоны совершенно вне папки `Presentation/`. Все о возможностях размещения шаблонов вы найдете в главе [Поиск шаблонов |templates#Поиск шаблонов]. +Как вариант, вы можете поместить шаблоны в подпапку `templates/`. Nette поддерживает оба варианта. Вы можете даже разместить шаблоны совсем вне папки `Presentation/`. Всё о возможностях размещения шаблонов можно найти в главе [Поиск шаблонов |templates#Поиск шаблона]. Вспомогательные классы и компоненты ----------------------------------- -К презентерам и шаблонам часто относятся и другие вспомогательные файлы. Мы разместим их логически в соответствии с их областью действия: +Презентерам и шаблонам часто сопутствуют другие вспомогательные файлы. Мы размещаем их логично, по области действия: -1. **Прямо у презентера** в случае специфических компонентов для данного презентера: +1. **Прямо рядом с презентером** в случае компонентов, относящихся только к нему: /--pre Product/ ├── ProductPresenter.php -├── ProductGrid.php ← компонент для вывода продуктов -└── FilterForm.php ← форма для фильтрации +├── ProductGrid.php ← компонент для вывода товаров +└── FilterForm.php ← форма фильтрации \-- -2. **Для модуля** - рекомендуем использовать папку `Accessory`, которая размещается удобно в начале алфавита: +2. **Для модуля** - мы рекомендуем использовать папку `Accessory`, которая удобно оказывается в начале по алфавиту: /--pre Front/ ├── Accessory/ -│ ├── NavbarControl.php ← компоненты для фронтенда +│ ├── NavbarControl.php ← компоненты публичной части │ └── TemplateFilters.php ├── Product/ └── Cart/ @@ -263,57 +263,57 @@ class DashboardPresenter extends Nette\Application\UI\Presenter └── Admin/ \-- -Или вы можете разместить вспомогательные классы, такие как `LatteExtension.php` или `TemplateFilters.php`, в инфраструктурной папке `app/Core/Latte/`. А компоненты в `app/Components`. Выбор зависит от привычек команды. +Как вариант, вспомогательные классы вроде `LatteExtension.php` или `TemplateFilters.php` можно поместить в инфраструктурную папку `app/Core/Latte/`. А компоненты в `app/Components`. Выбор зависит от соглашений команды. Модель - сердце приложения ========================== -Модель содержит всю бизнес-логику приложения. Для ее организации снова действует правило - структурируем по доменам: +Модель содержит всю бизнес-логику приложения. Правило её организации снова то же - структура по доменам: /--pre app/Model/ -├── Payment/ ← все, что связано с платежами +├── Payment/ ← всё о платежах │ ├── PaymentFacade.php ← главная точка входа │ ├── PaymentRepository.php │ ├── Payment.php ← сущность -├── Order/ ← все, что связано с заказами +├── Order/ ← всё о заказах │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php -└── Shipping/ ← все, что связано с доставкой +└── Shipping/ ← всё о доставке \-- -В модели обычно встречаются следующие типы классов: +В модели вам обычно встречаются такие типы классов: -**Фасады**: представляют собой главную точку входа в конкретный домен приложения. Они действуют как оркестратор, координирующий взаимодействие между различными сервисами для реализации полных сценариев использования (например, "создать заказ" или "обработать платеж"). Под своим оркестрационным слоем фасад скрывает детали реализации от остальной части приложения, предоставляя чистый интерфейс для работы с данным доменом. +**Фасады**: представляют главную точку входа в определённый домен приложения. Они выступают дирижёрами, согласующими взаимодействие разных сервисов ради выполнения полных сценариев (вроде "создать заказ" или "обработать платёж"). Под своим дирижёрским слоем фасад скрывает от остального приложения подробности реализации и тем самым даёт чистый интерфейс для работы с этим доменом. ```php class OrderFacade { public function createOrder(Cart $cart): Order { - // валидация + // проверка // создание заказа - // отправка электронной почты + // отправка письма // запись в статистику } } ``` -**Сервисы**: фокусируются на специфической бизнес-операции в рамках домена. В отличие от фасада, который оркеструет целые сценарии использования, сервис реализует конкретную бизнес-логику (например, расчет цен или обработку платежей). Сервисы обычно не имеют состояния и могут использоваться либо фасадами как строительные блоки для более сложных операций, либо напрямую другими частями приложения для более простых задач. +**Сервисы**: сосредоточены на конкретных бизнес-операциях внутри домена. В отличие от фасадов, дирижирующих целыми сценариями, сервис реализует конкретную бизнес-логику (вроде расчёта цен или обработки платежей). Сервисы обычно не хранят состояния и могут использоваться либо фасадами как строительные блоки более сложных операций, либо напрямую другими частями приложения для более простых задач. ```php class PricingService { public function calculateTotal(Order $order): Money { - // расчет цены + // расчёт цены } } ``` -**Репозитории**: обеспечивают все взаимодействие с хранилищем данных, обычно базой данных. Их задача - загрузка и сохранение сущностей и реализация методов для их поиска. Репозиторий изолирует остальную часть приложения от деталей реализации базы данных и предоставляет объектно-ориентированный интерфейс для работы с данными. +**Репозитории**: занимаются всем общением с хранилищем данных, обычно с базой данных. Их задача - загружать и сохранять сущности и реализовывать методы их поиска. Репозиторий заслоняет остальное приложение от подробностей реализации базы данных и даёт объектный интерфейс для работы с данными. ```php class OrderRepository @@ -328,10 +328,10 @@ class OrderRepository } ``` -**Сущности**: объекты, представляющие основные бизнес-концепции в приложении, которые имеют свою идентичность и изменяются со временем. Обычно это классы, отображаемые на таблицы базы данных с помощью ORM (например, Nette Database Explorer или Doctrine). Сущности могут содержать бизнес-правила, касающиеся их данных, и логику валидации. +**Сущности**: объекты, представляющие главные бизнес-понятия приложения, у которых есть собственная идентичность и которые меняются со временем. Обычно это классы, отображённые на таблицы базы данных через ORM (например, Nette Database Explorer или Doctrine). Сущности могут содержать бизнес-правила, связанные с их данными, и логику проверки. ```php -// Сущность, отображаемая на таблицу базы данных orders +// Сущность, отображённая на таблицу 'orders' в базе данных class Order extends Nette\Database\Table\ActiveRow { public function addItem(Product $product, int $quantity): void @@ -345,13 +345,13 @@ class Order extends Nette\Database\Table\ActiveRow } ``` -**Объекты-значения**: неизменяемые объекты, представляющие значения без собственной идентичности - например, денежная сумма или адрес электронной почты. Два экземпляра объекта-значения с одинаковыми значениями считаются идентичными. +**Объекты-значения**: неизменяемые объекты, представляющие значения без собственной идентичности, например денежную сумму или адрес электронной почты. Два экземпляра объекта-значения с одинаковыми значениями считаются одинаковыми. Инфраструктурный код ==================== -Папка `Core/` (или также `Infrastructure/`) является домом для технической основы приложения. Инфраструктурный код обычно включает: +Папка `Core/` (или, как вариант, `Infrastructure/`) - дом для технической основы приложения. Инфраструктурный код обычно включает: /--pre app/Core/ @@ -370,7 +370,7 @@ class Order extends Nette\Database\Table\ActiveRow └── Stripe/ \-- -Для небольших проектов, разумеется, достаточно плоского разделения: +Для небольших проектов, естественно, достаточно плоской структуры: /--pre Core/ @@ -381,27 +381,27 @@ class Order extends Nette\Database\Table\ActiveRow Это код, который: -- Решает техническую инфраструктуру (маршрутизация, логирование, кеширование) +- Занимается технической инфраструктурой (маршрутизация, логирование, кеширование) - Интегрирует внешние сервисы (Sentry, Elasticsearch, Redis) -- Предоставляет базовые сервисы для всего приложения (почта, база данных) -- В основном не зависит от конкретного домена - кеш или логгер работает одинаково для интернет-магазина или блога. +- Предоставляет базовые сервисы всему приложению (почта, база данных) +- Как правило, не зависит от конкретного домена: кеш или логгер работают одинаково и для интернет-магазина, и для блога. -Сомневаетесь, принадлежит ли определенный класс сюда или к модели? Ключевое различие в том, что код в `Core/`: +Не уверены, куда относится определённый класс - сюда или в модель? Ключевое отличие в том, что код в `Core/`: -- Ничего не знает о домене (продукты, заказы, статьи) -- В основном его можно перенести в другой проект -- Решает "как это работает" (как отправить письмо), а не "что это делает" (какое письмо отправить) +- Ничего не знает о домене (товарах, заказах, статьях) +- Обычно можно перенести в другой проект +- Решает "как это работает" (как отправить письмо), а не "что оно делает" (какое письмо отправить) Пример для лучшего понимания: -- `App\Core\MailerFactory` - создает экземпляры класса для отправки электронной почты, решает настройки SMTP -- `App\Model\OrderMailer` - использует `MailerFactory` для отправки электронных писем о заказах, знает их шаблоны и когда их нужно отправлять +- `App\Core\MailerFactory` - создаёт экземпляры класса для отправки писем, занимается настройками SMTP +- `App\Model\OrderMailer` - использует `MailerFactory` для отправки писем о заказах, знает их шаблоны и то, когда их следует отправлять Командные скрипты ================= -Приложения часто нуждаются в выполнении действий вне обычных HTTP-запросов - будь то обработка данных в фоновом режиме, обслуживание или периодические задачи. Для запуска служат простые скрипты в каталоге `bin/`, саму логику реализации мы размещаем в `app/Tasks/` (или `app/Commands/`). +Приложениям часто нужно выполнять действия вне обычных HTTP-запросов, будь то фоновая обработка данных, обслуживание или периодические задачи. Для запуска служат простые скрипты в каталоге `bin/`, а сама логика реализации размещается в `app/Tasks/` (или `app/Commands/`). Пример: @@ -414,27 +414,27 @@ class Order extends Nette\Database\Table\ActiveRow │ ├── ImportProducts.php ← импорт из системы поставщика │ └── SyncOrders.php ← синхронизация заказов └── Scheduled/ ← регулярные задачи - ├── NewsletterCommand.php ← рассылка новостей - └── ReminderCommand.php ← уведомления клиентам + ├── NewsletterCommand.php ← рассылка новостных писем + └── ReminderCommand.php ← уведомления клиентов \-- -Что относится к модели, а что к командным скриптам? Например, логика отправки одного электронного письма является частью модели, массовая рассылка тысяч писем уже относится к `Tasks/`. +Что относится к модели, а что к командным скриптам? Например, логика отправки одного письма - часть модели, а массовая отправка тысяч писем относится к `Tasks/`. -Задачи обычно [запускаем из командной строки |https://blog.nette.org/en/cli-scripts-in-nette-application] или через cron. Их можно запускать и через HTTP-запрос, но необходимо помнить о безопасности. Презентер, который запускает задачу, нужно защитить, например, только для вошедших пользователей или сильным токеном и доступом с разрешенных IP-адресов. Для длительных задач необходимо увеличить временной лимит скрипта и использовать `session_write_close()`, чтобы сессия не блокировалась. +Задачи обычно запускаются из командной строки или через cron: скрипт в `bin/` создаёт DI-контейнер методом [bootConsoleApplication() |bootstrapping#Разные окружения] и достаёт из него нужный сервис. Их можно запускать и HTTP-запросом, но при этом нужно подумать о безопасности. Презентер, запускающий задачу, нужно защитить, например только для вошедших пользователей или сильным токеном и доступом с разрешённых IP-адресов. Для долгих задач нужно увеличить ограничение времени работы скрипта и использовать `session_write_close()`, чтобы не блокировать сессию. Другие возможные каталоги ========================= -Кроме упомянутых базовых каталогов, вы можете в соответствии с потребностями проекта добавить другие специализированные папки. Посмотрим на наиболее частые из них и их использование: +Помимо упомянутых базовых каталогов вы можете добавить и другие специализированные папки под нужды проекта. Посмотрим на самые частые и на их применение: /--pre app/ -├── Api/ ← логика для API, независимая от презентационного слоя -├── Database/ ← миграционные скрипты и сидеры для тестовых данных +├── Api/ ← логика API, независимая от презентационного слоя +├── Database/ ← миграционные скрипты и сидеры тестовых данных ├── Components/ ← общие визуальные компоненты для всего приложения -├── Event/ ← полезно, если вы используете событийно-ориентированную архитектуру -├── Mail/ ← шаблоны электронной почты и связанная логика +├── Event/ ← полезно при событийно-ориентированной архитектуре +├── Mail/ ← шаблоны писем и связанная логика └── Utils/ ← вспомогательные классы \-- @@ -447,53 +447,53 @@ class Order extends Nette\Database\Table\ActiveRow │ └── UserForm.php ├── Grid/ ← компоненты для вывода данных │ └── DataGrid.php -└── Navigation/ ← навигационные элементы +└── Navigation/ ← элементы навигации ├── Breadcrumbs.php └── Menu.php \-- -Сюда относятся компоненты, имеющие более сложную логику. Если вы хотите делиться компонентами между несколькими проектами, рекомендуется выделить их в отдельный composer-пакет. +Сюда относятся компоненты с более сложной логикой. Если вы хотите использовать компоненты в нескольких проектах, стоит вынести их в отдельный пакет Composer. -В каталог `app/Mail` можно поместить управление электронной почтой: +В каталоге `app/Mail` вы можете разместить управление почтовым общением: /--pre app/Mail/ -├── templates/ ← шаблоны электронной почты +├── templates/ ← шаблоны писем │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php \-- -Маппинг презентеров -=================== +Отображение презентеров +======================= -Маппинг определяет правила для вывода имени класса из имени презентера. Мы указываем их в [конфигурации |configuration] под ключом `application › mapping`. +Отображение задаёт правила выведения имени класса из имени презентера. Мы указываем их в [конфигурации|configuration] под ключом `application › mapping`. -На этой странице мы показали, что презентеры размещаем в папке `app/Presentation` (или `app/UI`). Эту конвенцию мы должны сообщить Nette в конфигурационном файле. Достаточно одной строки: +На этой странице мы показали, что размещаем презентеры в папке `app/Presentation` (или `app/UI`). Начиная с Nette Application 3.3 это соглашение по умолчанию, которое настраивать не нужно. Если вы используете другую структуру или хотите указать отображение явно, настройке по умолчанию соответствует такая строка: ```neon application: mapping: App\Presentation\*\**Presenter ``` -Как работает маппинг? Для лучшего понимания сначала представим приложение без модулей. Мы хотим, чтобы классы презентеров попадали в пространство имен `App\Presentation`, чтобы презентер `Home` отображался на класс `App\Presentation\HomePresenter`. Этого мы достигнем с помощью следующей конфигурации: +Как работает отображение? Для лучшего понимания сначала представим приложение без модулей. Мы хотим, чтобы классы презентеров попадали в пространство имён `App\Presentation`, так чтобы презентер `Home` отображался в класс `App\Presentation\HomePresenter`. Этого мы добиваемся такой конфигурацией: ```neon application: mapping: App\Presentation\*Presenter ``` -Маппинг работает так, что имя презентера `Home` заменяет звездочку в маске `App\Presentation\*Presenter`, тем самым мы получаем итоговое имя класса `App\Presentation\HomePresenter`. Просто! +Отображение работает так, что звёздочка в маске `App\Presentation\*Presenter` заменяется именем презентера `Home`, и получается итоговое имя класса `App\Presentation\HomePresenter`. Просто! -Однако, как вы видите в примерах в этой и других главах, классы презентеров мы размещаем в одноименных подкаталогах, например, презентер `Home` отображается на класс `App\Presentation\Home\HomePresenter`. Этого мы достигнем удвоением звездочки: +Однако, как вы видите в примерах в этой и других главах, мы размещаем классы презентеров в одноимённых подкаталогах, например презентер `Home` отображается в класс `App\Presentation\Home\HomePresenter`. Этого мы добиваемся двойной звёздочкой `**` (требуется Nette Application 3.2.3): ```neon application: mapping: App\Presentation\**Presenter ``` -Теперь перейдем к маппингу презентеров в модули. Для каждого модуля можно определить специфический маппинг: +Теперь перейдём к отображению презентеров в модулях. Мы можем задать для каждого модуля своё отображение: ```neon application: @@ -503,9 +503,9 @@ application: Api: App\Api\*Presenter ``` -Согласно этой конфигурации, презентер `Front:Home` отображается на класс `App\Presentation\Front\Home\HomePresenter`, в то время как презентер `Api:OAuth` на класс `App\Api\OAuthPresenter`. +По этой конфигурации презентер `Front:Home` отображается в класс `App\Presentation\Front\Home\HomePresenter`, а презентер `Api:OAuth` - в класс `App\Api\OAuthPresenter`. -Поскольку модули `Front` и `Admin` имеют схожий способ маппинга, и таких модулей, скорее всего, будет больше, можно создать общее правило, которое их заменит. В маску класса так добавится новая звездочка для модуля: +Поскольку у модулей `Front` и `Admin` похожий шаблон отображения, а таких модулей, скорее всего, будет больше, можно создать общее правило, которое их заменит. В маску класса добавляется новая звёздочка для модуля: ```neon application: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -Это работает и для более глубоко вложенных структур каталогов, таких как, например, презентер `Admin:User:Edit`, сегмент со звездочкой повторяется для каждого уровня, и результатом является класс `App\Presentation\Admin\User\Edit\EditPresenter`. +Это работает и для более глубоко вложенных структур каталогов, например для презентера `Admin:User:Edit`, где сегмент со звёздочкой повторяется для каждого уровня модуля, и получается класс `App\Presentation\Admin\User\Edit\EditPresenter`. -Альтернативной записью является использование массива, состоящего из трех сегментов, вместо строки. Эта запись эквивалентна предыдущей: +Альтернативная запись - использовать вместо строки массив из трёх сегментов. Для показанных выше примеров такая запись равнозначна предыдущей: ```neon application: diff --git a/application/ru/how-it-works.texy b/application/ru/how-it-works.texy index 684db1a876..41a097e559 100644 --- a/application/ru/how-it-works.texy +++ b/application/ru/how-it-works.texy @@ -3,10 +3,10 @@
    -Вы читаете основной документ документации Nette. Вы узнаете весь принцип работы веб-приложений. От А до Я, с момента рождения до последнего вздоха PHP-скрипта. После прочтения вы будете знать: +Сейчас вы читаете основополагающую главу документации Nette. Вы узнаете полные принципы работы веб-приложений от А до Я, с момента рождения запроса до завершения выполнения PHP-скрипта. После прочтения вы будете понимать: -- как все это работает -- что такое Bootstrap, Presenter и DI-контейнер +- как всё это работает +- что такое Bootstrap, презентер и DI-контейнер - как выглядит структура каталогов
    @@ -15,89 +15,89 @@ Структура каталогов =================== -Откройте пример скелета веб-приложения под названием [WebProject |https://github.com/nette/web-project] и во время чтения вы можете смотреть на файлы, о которых идет речь. +Откройте пример скелета веб-приложения под названием [WebProject|https://github.com/nette/web-project]. По ходу чтения вы можете заглядывать в обсуждаемые файлы. Структура каталогов выглядит примерно так: /--pre web-project/ -├── app/ ← каталог с приложением +├── app/ ← каталог приложения │ ├── Core/ ← базовые классы, необходимые для работы -│ │ └── RouterFactory.php ← конфигурация URL-адресов -│ ├── Presentation/ ← презентеры, шаблоны и т.п. +│ │ └── RouterFactory.php ← настройка URL-адресов +│ ├── Presentation/ ← презентеры, шаблоны и прочее │ │ ├── @layout.latte ← шаблон макета │ │ └── Home/ ← каталог презентера Home │ │ ├── HomePresenter.php ← класс презентера Home -│ │ └── default.latte ← шаблон действия default -│ └── Bootstrap.php ← загрузочный класс Bootstrap +│ │ └── default.latte ← шаблон для действия default +│ └── Bootstrap.php ← стартовый класс Bootstrap ├── assets/ ← ресурсы (SCSS, TypeScript, исходные изображения) ├── bin/ ← скрипты, запускаемые из командной строки ├── config/ ← конфигурационные файлы │ ├── common.neon │ └── services.neon -├── log/ ← логируемые ошибки +├── log/ ← записанные ошибки ├── temp/ ← временные файлы, кеш, … ├── vendor/ ← библиотеки, установленные Composer │ ├── ... │ └── autoload.php ← автозагрузка всех установленных пакетов -├── www/ ← публичный каталог или document-root проекта +├── www/ ← публичный каталог, document-root проекта │ ├── assets/ ← скомпилированные статические файлы (CSS, JS, изображения, ...) -│ ├── .htaccess ← правила mod_rewrite -│ └── index.php ← первоначальный файл, которым запускается приложение +│ ├── .htaccess ← правила для mod_rewrite +│ └── index.php ← начальный файл, запускающий приложение └── .htaccess ← запрещает доступ ко всем каталогам, кроме www \-- -Структуру каталогов можно изменять как угодно, папки переименовывать или перемещать, она абсолютно гибкая. Nette, кроме того, обладает умным автоопределением и автоматически распознает расположение приложения, включая его базовый URL. +Структуру каталогов можно изменять как угодно, переименовывать или переносить папки, она полностью гибкая. У Nette есть и умное автоопределение, которое само распознаёт расположение приложения, включая базовый URL. -Для немного больших приложений мы можем [разделить папки с презентерами и шаблонами на подкаталоги |directory-structure#Презентеры и шаблоны] и классы на пространства имен, которые мы называем модулями. +Для чуть более крупных приложений мы можем разложить каталоги презентеров и шаблонов по [подкаталогам |directory-structure#Презентеры и шаблоны] и сгруппировать классы в пространства имён, которые мы называем модулями. -Каталог `www/` представляет собой так называемый публичный каталог или document-root проекта. Вы можете его переименовать без необходимости что-либо еще настраивать на стороне приложения. Нужно только [настроить хостинг |nette:troubleshooting#Как изменить или удалить каталог www из URL] так, чтобы document-root указывал на этот каталог. +Каталог `www/` представляет собой публичный каталог, или document-root проекта. Вы можете его переименовать, ничего больше на стороне приложения настраивать не придётся. Нужно лишь [настроить хостинг |nette:troubleshooting#Как изменить или убрать из URL каталог www?] так, чтобы document-root указывал на этот каталог. -WebProject можно также сразу скачать вместе с Nette с помощью [Composer |best-practices:composer]: +WebProject можно скачать сразу вместе с Nette через [Composer |best-practices:composer]: ```shell composer create-project nette/web-project ``` -На Linux или macOS установите для каталогов `log/` и `temp/` [права на запись |nette:troubleshooting#Настройка прав доступа к каталогам]. +В Linux или macOS задайте каталогам `log/` и `temp/` [права на запись |nette:troubleshooting#Задание прав на каталоги]. -Приложение WebProject готово к запуску, не нужно ничего настраивать, и его можно сразу отобразить в браузере, обратившись к папке `www/`. +Приложение WebProject готово к запуску, настраивать вообще ничего не нужно, и вы можете открыть его прямо в браузере, обратившись к папке `www/`. HTTP-запрос =========== -Все начинается в тот момент, когда пользователь в браузере открывает страницу. То есть когда браузер стучится на сервер с HTTP-запросом. Запрос направляется на единственный PHP-файл, который находится в публичном каталоге `www/`, и это `index.php`. Допустим, это запрос на адрес `https://example.com/product/123`. Благодаря подходящей [настройке сервера |nette:troubleshooting#Как настроить сервер для красивых URL] и этот URL отображается на файл `index.php`, и он выполняется. +Всё начинается с того, что пользователь открывает страницу в браузере. Браузер отправляет на сервер HTTP-запрос. Этот запрос нацелен на единственный PHP-файл в публичном каталоге `www/`, а именно на `index.php`. Допустим, запрос идёт по адресу `https://example.com/product/123`. Благодаря подходящей [настройке сервера |nette:troubleshooting#Как настроить сервер для красивых URL?] даже такой URL сопоставляется файлу `index.php`, который и выполняется. Его задача: -1) инициализировать среду +1) инициализировать окружение 2) получить фабрику 3) запустить приложение Nette, которое обработает запрос -Какую фабрику? Мы же не тракторы производим, а веб-страницы! Потерпите, сейчас все объяснится. +Какую фабрику? Мы же не тракторы производим, а сайты делаем! Погодите, сейчас всё объяснится. -Под словами «инициализация среды» мы подразумеваем, например, активацию [Tracy |tracy:], что является замечательным инструментом для логирования или визуализации ошибок. На production-сервере он логирует ошибки, на сервере разработки сразу отображает. Следовательно, к инициализации относится и решение, работает ли веб в режиме production или разработки. Для этого Nette использует [умное автоопределение |bootstrapping#Режим разработки vs режим production]: если вы запускаете веб на localhost, он работает в режиме разработки. Вам не нужно ничего настраивать, и приложение сразу готово как для разработки, так и для реального развертывания. Эти шаги выполняются и подробно описаны в главе о [классе Bootstrap |bootstrapping]. +Под "инициализацией окружения" мы понимаем, например, включение [Tracy|tracy:] - потрясающего инструмента для записи в лог и наглядного показа ошибок. На производственном сервере она записывает ошибки в лог, а в среде разработки показывает их прямо на экране. Поэтому инициализация включает и определение того, работает сайт в производственном режиме или в режиме разработки. Nette использует для этого [умное автоопределение |bootstrapping#Режим разработки и производственный режим]: если вы запускаете сайт на localhost, он работает в режиме разработки. Настраивать ничего не нужно, и приложение сразу готово и к разработке, и к боевому развёртыванию. Эти шаги выполняются и подробно описываются в главе о [классе Bootstrap|bootstrapping]. -Третьим пунктом (да, второй мы пропустили, но вернемся к нему) является запуск приложения. Обработкой HTTP-запросов в Nette занимается класс `Nette\Application\Application` (далее `Application`), поэтому когда мы говорим запустить приложение, мы имеем в виду конкретно вызов метода с характерным названием `run()` на объекте этого класса. +Третий пункт (да, второй мы пропустили, но вернёмся к нему) - запуск приложения. Обработка HTTP-запросов в Nette лежит на классе `Nette\Application\Application` (далее `Application`). Так что, когда мы говорим "запустить приложение", мы имеем в виду вызов метода с говорящим именем `run()` у объекта этого класса. -Nette — это наставник, который ведет вас к написанию чистых приложений по проверенным методикам. И одна из самых проверенных называется **dependency injection**, сокращенно DI. В данный момент мы не хотим загружать вас объяснением DI, для этого есть [отдельная глава |dependency-injection:introduction], важен результат, что ключевые объекты нам обычно будет создавать фабрика объектов, которая называется **DI-контейнер** (сокращенно DIC). Да, это та самая фабрика, о которой шла речь минуту назад. И она создаст нам и объект `Application`, поэтому нам сначала нужен контейнер. Мы получаем его с помощью класса `Configurator` и позволяем ему создать объект `Application`, вызываем на нем метод `run()`, и тем самым запускается приложение Nette. Именно это происходит в файле [index.php |bootstrapping#index.php]. +Nette выступает наставником, который направляет вас писать чистые приложения по проверенным методикам. Одна из самых устоявшихся - **внедрение зависимостей**, сокращённо DI. Мы не хотим сейчас нагружать вас объяснением DI, для этого есть [отдельная глава|dependency-injection:introduction]. Важное следствие в том, что ключевые объекты обычно создаёт фабрика объектов, известная как **DI-контейнер** (или DIC). Да, это та самая фабрика, о которой шла речь. Она порождает нам и объект `Application`, поэтому сначала нам нужен контейнер. Мы получаем его через класс `Configurator`, даём ему создать объект `Application`, вызываем у него метод `run()`, и приложение Nette запускается. Именно это и происходит в файле [index.php |bootstrapping#index.php]. Nette Application ================= -У класса Application одна задача: ответить на HTTP-запрос. +У класса `Application` единственная задача: ответить на HTTP-запрос. -Приложения, написанные на Nette, делятся на множество так называемых презентеров (в других фреймворках вы можете встретить термин controller, это одно и то же), которые представляют собой классы, каждый из которых представляет какую-то конкретную страницу сайта: например, главную страницу; продукт в интернет-магазине; форму входа; фид карты сайта и т. д. Приложение может иметь от одного до тысяч презентеров. +Приложения, написанные на Nette, делятся на множество так называемых презентеров (в других фреймворках вы можете встретить термин "контроллер", это по сути одно и то же). Это классы, каждый из которых представляет определённую страницу сайта: например, главную страницу, товар в интернет-магазине, форму входа, ленту карты сайта и так далее. У приложения может быть от одного до тысяч презентеров. -Application начинает с того, что запрашивает у так называемого маршрутизатора (router), чтобы он решил, какому из презентеров передать текущий запрос для обработки. Маршрутизатор решает, чья это ответственность. Он смотрит на входной URL `https://example.com/product/123` и на основе того, как он настроен, решает, что это работа, например, для **презентера** `Product`, от которого потребуется в качестве **действия** отображение (`show`) продукта с `id: 123`. Пару презентер + действие принято записывать, разделяя двоеточием, как `Product:show`. +`Application` начинает с того, что спрашивает так называемый маршрутизатор, какой презентер должен обработать текущий запрос. Маршрутизатор определяет ответственного. Он изучает входной URL `https://example.com/product/123` и по своей настройке решает, что эта задача принадлежит, например, **презентеру** `Product`, который должен выполнить **действие** `show` для товара с `id: 123`. Пару "презентер + действие" принято записывать через двоеточие: `Product:show`. -Таким образом, маршрутизатор преобразовал URL в пару `Presenter:action` + параметры, в нашем случае `Product:show` + `id: 123`. Как выглядит такой маршрутизатор, вы можете посмотреть в файле `app/Core/RouterFactory.php`, и мы подробно описываем его в главе [Маршрутизация |Routing]. +Итак, маршрутизатор превратил URL в пару `Презентер:действие` с параметрами, в нашем случае `Product:show` и `id: 123`. Как выглядит такой маршрутизатор, вы можете увидеть в файле `app/Core/RouterFactory.php`, а подробно мы описываем его в главе [Маршрутизация |Routing]. -Идем дальше. Application уже знает имя презентера и может продолжать. Создавая объект класса `ProductPresenter`, который является кодом презентера `Product`. Точнее говоря, он просит DI-контейнер создать презентер, потому что создание — это его работа. +Пойдём дальше. `Application` теперь знает имя презентера и может действовать. Он создаёт экземпляр класса `ProductPresenter`, содержащего код презентера `Product`. Точнее, он просит DI-контейнер создать презентер, потому что создание объектов - его обязанность. -Презентер может выглядеть примерно так: +Презентер может выглядеть так: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -109,92 +109,92 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function renderShow(int $id): void { - // получаем данные из модели и передаем в шаблон + // получаем данные из модели и передаём их в шаблон $this->template->product = $this->repository->getProduct($id); } } ``` -Обработку запроса берет на себя презентер. И задача ясна: выполнить действие `show` с `id: 123`. Что на языке презентеров означает, что вызывается метод `renderShow()`, и в параметре `$id` он получает `123`. +Презентер берёт обработку запроса на себя. Задача ясна: выполнить действие `show` с `id: 123`. В терминологии презентеров это значит, что вызывается метод `renderShow()`, получающий `123` в параметре `$id`. -Презентер может обслуживать несколько действий, то есть иметь несколько методов `render()`. Но мы рекомендуем проектировать презентеры с одним или как можно меньшим количеством действий. +Презентер может обрабатывать несколько действий, то есть у него может быть несколько методов `render()`. Однако мы рекомендуем проектировать презентеры с одним действием или с как можно меньшим их числом. -Итак, вызвался метод `renderShow(123)`, код которого, хотя и является вымышленным примером, но на нем вы можете увидеть, как передаются данные в шаблон, то есть записью в `$this->template`. +Итак, был вызван метод `renderShow(123)`. Его код - выдуманный пример, но он показывает, как данные передаются в шаблон, а именно записью в `$this->template`. -Затем презентер возвращает ответ. Это может быть HTML-страница, изображение, XML-документ, отправка файла с диска, JSON или, например, перенаправление на другую страницу. Важно, что если мы явно не скажем, как он должен ответить (что и происходит в случае `ProductPresenter`), ответом будет отрисовка шаблона с HTML-страницей. Почему? Потому что в 99% случаев мы хотим отрисовать шаблон, поэтому презентер воспринимает это поведение как стандартное и хочет облегчить нам работу. В этом смысл Nette. +Затем презентер возвращает ответ. Это может быть HTML-страница, изображение, XML-документ, отправка файла с диска, JSON или, скажем, перенаправление на другую страницу. Важно, что, если мы явно не указываем, как отвечать (а именно так и обстоит дело с `ProductPresenter`), ответом будет отрисовка шаблона в HTML-страницу. Почему? Потому что в 99 % случаев мы хотим отрисовать шаблон. Поэтому презентер принимает такое поведение как поведение по умолчанию, чтобы упростить нам работу. В этом суть Nette. -Нам даже не нужно указывать, какой шаблон отрисовать, путь к нему он выведет сам. В случае действия `show` он просто попытается загрузить шаблон `show.latte` в каталоге с классом `ProductPresenter`. Также он попытается найти макет в файле `@layout.latte` (подробнее о [поиске шаблонов |templates#Поиск шаблонов]). +Нам даже не нужно указывать, какой шаблон отрисовать: фреймворк выведет путь автоматически. В случае действия `show` он просто попробует загрузить шаблон `show.latte`, лежащий в том же каталоге, что и класс `ProductPresenter`. Он также попробует найти макет в файле `@layout.latte` (подробнее о [поиске шаблонов |templates#Поиск шаблона]). -И затем шаблоны отрисовываются. На этом задача презентера и всего приложения завершена, и работа выполнена. Если бы шаблон не существовал, вернулась бы страница с ошибкой 404. Больше о презентерах вы можете прочитать на странице [Презентеры |presenters]. +Затем шаблоны отрисовываются. На этом задача презентера и всего приложения завершена. Если шаблона не существует, возвращается страница с ошибкой 404. Подробнее о презентерах можно узнать на странице [Презентеры|presenters]. [* request-flow.svg *] -На всякий случай, попробуем повторить весь процесс с немного другим URL: +Для верности повторим весь ход событий с чуть другим URL: -1) URL будет `https://example.com` -2) загружаем приложение, создается контейнер и запускается `Application::run()` -3) маршрутизатор декодирует URL как пару `Home:default` -4) создается объект класса `HomePresenter` -5) вызывается метод `renderDefault()` (если существует) -6) отрисовывается шаблон, например, `default.latte` с макетом, например, `@layout.latte` +1) URL - `https://example.com` +2) Приложение стартует, создаётся DI-контейнер и выполняется `Application::run()`. +3) Маршрутизатор расшифровывает URL в пару `Home:default`. +4) Создаётся экземпляр класса `HomePresenter`. +5) Вызывается метод `renderDefault()` (если он существует). +6) Отрисовывается шаблон, например `default.latte`, вместе с макетом, например `@layout.latte`. -Возможно, вы сейчас столкнулись с большим количеством новых понятий, но мы верим, что они имеют смысл. Создание приложений в Nette — это огромное удовольствие. +Возможно, вы только что столкнулись со множеством новых понятий, но мы верим, что они осмысленны. Разрабатывать приложения на Nette на удивление просто. Шаблоны ======= -Раз уж речь зашла о шаблонах, в Nette используется система шаблонов [Latte |latte:]. Поэтому и расширения `.latte` у шаблонов. Latte используется, во-первых, потому что это самая безопасная система шаблонов для PHP, а во-вторых, самая интуитивно понятная. Вам не нужно учить много нового, достаточно знания PHP и нескольких тегов. Все вы узнаете [в документации |templates]. +Раз уж речь зашла о шаблонах: Nette использует систему шаблонов [Latte |latte:]. Поэтому файлы шаблонов имеют расширение `.latte`. Latte используется прежде всего потому, что это самая безопасная система шаблонов для PHP, а ещё самая интуитивная. Учить много нового не нужно: достаточно знания PHP и нескольких тегов. Всё нужное вы найдёте [в документации |templates]. -В шаблоне [создаются ссылки |creating-links] на другие презентеры и действия так: +В шаблоне вы [создаёте ссылки |creating-links] на другие презентеры и действия вот так: ```latte -деталь продукта +product detail ``` -Просто вместо реального URL вы пишете известную пару `Presenter:action` и указываете возможные параметры. Хитрость в `n:href`, которое говорит, что этот атрибут обработает Nette. И сгенерирует: +Просто напишите привычную пару `Презентер:действие` вместо настоящего URL и добавьте нужные параметры. Хитрость в `n:href`, которая говорит Nette обработать этот атрибут. Он затем породит: ```latte -деталь продукта +product detail ``` -Генерацией URL занимается уже упомянутый маршрутизатор. Дело в том, что маршрутизаторы в Nette уникальны тем, что они могут выполнять не только преобразование из URL в пару presenter:action, но и наоборот, то есть из имени презентера + действия + параметров генерировать URL. Благодаря этому в Nette вы можете полностью изменить формы URL во всем готовом приложении, не изменяя ни одного символа в шаблоне или презентере. Просто изменив маршрутизатор. Также благодаря этому работает так называемая канонизация, что является еще одной уникальной особенностью Nette, которая способствует лучшему SEO (оптимизации для поисковых систем), автоматически предотвращая существование дублирующегося контента на разных URL. Многие программисты считают это потрясающим. +Порождением URL занимается упомянутый маршрутизатор. Маршрутизаторы в Nette исключительны тем, что умеют не только преобразовывать URL в пару `Презентер:действие`, но и наоборот: порождать URL из имени презентера, действия и параметров. Благодаря этому вы можете полностью изменить формат URL во всём готовом приложении на Nette, не меняя ни одного символа в шаблонах или презентерах, - достаточно поправить маршрутизатор. Это же обеспечивает так называемую канонизацию, ещё одну уникальную возможность Nette, которая улучшает SEO, автоматически не давая одному и тому же содержимому существовать под разными URL. Многих программистов эта возможность поражает. Интерактивные компоненты ======================== -О презентерах мы должны вам рассказать еще одну вещь: у них есть встроенная система компонентов. Что-то подобное могут помнить старожилы из Delphi или ASP.NET Web Forms, на чем-то отдаленно похожем построены React или Vue.js. В мире PHP-фреймворков это совершенно уникальное явление. +Нам нужно рассказать о презентерах ещё одну вещь: в них встроена система компонентов. Те, у кого больше опыта, могут вспомнить нечто похожее из Delphi или ASP.NET Web Forms; React или Vue.js построены на в чём-то родственных идеях. В мире PHP-фреймворков это совершенно уникальная возможность. -Компоненты — это отдельные повторно используемые единицы, которые мы вставляем на страницы (то есть в презентеры). Это могут быть [формы |forms:in-presenter], [датагриды |https://componette.org/contributte/datagrid/], меню, опросы, в общем, все, что имеет смысл использовать повторно. Мы можем создавать собственные компоненты или использовать некоторые из [огромного выбора |https://componette.org] open source компонентов. +Компоненты - самостоятельные переиспользуемые единицы, которые мы встраиваем в страницы (то есть в презентеры). Это могут быть [формы |forms:in-presenter], [таблицы данных |https://componette.org/contributte/datagrid/], меню, опросы - в общем, всё, что имеет смысл использовать повторно. Мы можем создавать собственные компоненты или воспользоваться какими-то из [огромного выбора |https://componette.org] компонентов с открытым кодом. -Компоненты кардинально влияют на подход к созданию приложений. Они откроют вам новые возможности сборки страниц из готовых блоков. И к тому же у них есть что-то общее с [Голливудом |components#Стиль Голливуда]. +Компоненты принципиально меняют подход к разработке приложений. Они открывают новые возможности собирать страницы из заранее подготовленных единиц. И у них есть кое-что общее с [Голливудом |components#Голливудский стиль]. DI-контейнер и конфигурация =========================== -DI-контейнер или фабрика объектов — это сердце всего приложения. +DI-контейнер, то есть фабрика объектов, - сердце всего приложения. -Не беспокойтесь, это не какой-то магический черный ящик, как могло показаться из предыдущих строк. На самом деле, это один довольно скучный PHP-класс, который генерирует Nette и сохраняет в каталоге кеша. У него много методов, названных как `createServiceAbcd()`, и каждый из них умеет создавать и возвращать какой-то объект. Да, там есть и метод `createServiceApplication()`, который создает `Nette\Application\Application`, который нам был нужен в файле `index.php` для запуска приложения. И там есть методы, создающие отдельные презентеры. И так далее. +Не переживайте, это не какой-то волшебный чёрный ящик, как могли бы навести на мысль предыдущие строки. На деле это довольно обыденный PHP-класс, порождённый Nette и сохранённый в каталоге кеша. В нём много методов с именами вроде `createServiceAbcd()`, каждый из которых умеет создать и вернуть определённый объект. Да, там есть и метод `createServiceApplication__application()`, порождающий экземпляр `Nette\Application\Application`, который понадобился нам в `index.php` для запуска приложения. Есть и методы для создания отдельных презентеров и так далее. -Объектам, которые создает DI-контейнер, по какой-то причине говорят сервисы. +Объекты, создаваемые DI-контейнером, по некоторым причинам называют сервисами. -Что действительно особенного в этом классе, так это то, что его программируете не вы, а фреймворк. Он действительно генерирует PHP-код и сохраняет его на диск. Вы только даете инструкции, какие объекты должен уметь создавать контейнер и как именно. И эти инструкции записаны в [конфигурационных файлах |bootstrapping#Конфигурация DI-контейнера], для которых используется формат [NEON |neon:format], и поэтому они имеют расширение `.neon`. +По-настоящему особенное в этом классе то, что вы его не программируете - это делает фреймворк. Он действительно порождает PHP-код и сохраняет его на диск. Вы лишь даёте указания, какие объекты контейнер должен уметь создавать и как именно. Эти указания записываются в [конфигурационных файлах |bootstrapping#Конфигурация DI-контейнера], которые используют формат [NEON|neon:format] и потому имеют расширение `.neon`. -Конфигурационные файлы служат исключительно для инструктирования DI-контейнера. Так что, например, если я укажу в секции [session |http:configuration#Сессия] опцию `expiration: 14 days`, то DI-контейнер при создании объекта `Nette\Http\Session`, представляющего сессию, вызовет его метод `setExpiration('14 days')`, и тем самым конфигурация станет реальностью. +Конфигурационные файлы служат исключительно для указаний DI-контейнеру. Так что, если вы, например, зададите параметр `expiration: 14 days` в секции [session |http:configuration#Сессия], DI-контейнер при создании объекта `Nette\Http\Session`, представляющего сессию, вызовет его метод `setExpiration('14 days')` и тем самым воплотит конфигурацию в жизнь. -Для вас подготовлена целая глава, описывающая, что все можно [настроить |nette:configuring] и как [определить собственные сервисы |dependency-injection:services]. +Для вас подготовлена целая глава о том, что можно [настраивать |nette:configuring] и как [определять собственные сервисы |dependency-injection:services]. -Как только вы немного разберетесь в создании сервисов, вы столкнетесь со словом [autowiring |dependency-injection:autowiring]. Это фишка, которая невероятным образом упростит вам жизнь. Она умеет автоматически передавать объекты туда, где они вам нужны (например, в конструкторах ваших классов), без необходимости что-либо делать. Вы обнаружите, что DI-контейнер в Nette — это маленькое чудо. +Как только вы немного погрузитесь в создание сервисов, вы столкнётесь с термином [autowiring |dependency-injection:autowiring]. Это возможность, которая невероятно упростит вам жизнь. Она умеет автоматически передавать объекты туда, где они вам нужны (например, в конструкторы ваших классов), без каких-либо действий с вашей стороны. Вы обнаружите, что DI-контейнер в Nette - маленькое чудо. -Куда дальше? -============ +Что дальше? +=========== -Мы рассмотрели основные принципы приложений в Nette. Пока очень поверхностно, но скоро вы проникнете в глубину и со временем создадите замечательные веб-приложения. Куда двигаться дальше? Вы уже пробовали учебник [Пишем первое приложение |quickstart:]? +Мы разобрали основополагающие принципы приложений Nette. Пока это был поверхностный обзор, но вскоре вы погрузитесь глубже и со временем начнёте создавать замечательные веб-приложения. Куда двигаться дальше? Вы уже пробовали руководство [Создайте своё первое приложение|quickstart:]? -Кроме вышеописанного, Nette располагает целым арсеналом [полезных классов |utils:], [слоем базы данных |database:], и т. д. Попробуйте просто пролистать документацию. Или [блог |https://blog.nette.org]. Вы откроете много интересного. +Помимо описанного выше Nette предлагает целый арсенал [полезных классов|utils:], [слой работы с базой данных|database:] и многое другое. Попробуйте походить по документации. Или загляните в [блог|https://blog.nette.org]. Вы обнаружите много интересного. -Пусть фреймворк приносит вам много радости 💙 +Пусть фреймворк принесёт вам много радости 💙 diff --git a/application/ru/multiplier.texy b/application/ru/multiplier.texy index 15e753817b..415a0d5f84 100644 --- a/application/ru/multiplier.texy +++ b/application/ru/multiplier.texy @@ -2,23 +2,23 @@ Multiplier: динамические компоненты *********************************** .[perex] -Инструмент для динамического создания интерактивных компонентов +Инструмент для динамического создания интерактивных компонентов. -Начнем с типичного примера: у нас есть список товаров в интернет-магазине, и для каждого мы хотим вывести форму для добавления товара в корзину. Один из возможных вариантов — обернуть весь список в одну форму. Однако гораздо более удобный способ предлагает нам [api:Nette\Application\UI\Multiplier]. +Начнём с типичного примера: представьте список товаров в интернет-магазине, где для каждой позиции нужна форма "Добавить в корзину". Один из возможных подходов - обернуть весь список в одну форму. Однако куда более удобный способ предлагает [api:Nette\Application\UI\Multiplier]. -Multiplier позволяет удобно определить фабрику для нескольких компонентов. Он работает по принципу вложенных компонентов — каждый компонент, наследующий от [api:Nette\ComponentModel\Container], может содержать другие компоненты. +Multiplier позволяет удобно определить фабрику сразу для нескольких компонентов. Он работает по принципу вложенных компонентов: любой компонент, наследующий от [api:Nette\ComponentModel\Container], может содержать другие компоненты. .[tip] -См. главу о [модели компонентов |components#Компоненты в глубину] в документации или [лекцию Яна Тврдика |https://www.youtube.com/watch?v=8y3LLexWu-I]. +См. главу о [компонентной модели |components#Компоненты в подробностях] в документации. -Суть Multiplier заключается в том, что он выступает в роли родителя, который может динамически создавать своих потомков с помощью callback-функции, переданной в конструкторе. См. пример: +Суть Multiplier в том, что он выступает родителем, который может динамически создавать своих потомков с помощью callback, переданного в конструктор. Смотрите пример: ```php protected function createComponentShopForm(): Multiplier { return new Multiplier(function () { $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Количество товара:') + $form->addInteger('amount', 'Количество:') ->setRequired(); $form->addSubmit('send', 'Добавить в корзину'); return $form; @@ -26,7 +26,7 @@ protected function createComponentShopForm(): Multiplier } ``` -Теперь мы можем в шаблоне просто для каждого товара отрисовать форму — и каждая будет действительно уникальным компонентом. +Теперь в шаблоне мы можем просто отрисовать форму для каждого товара, и каждая из них действительно будет отдельным компонентом. ```latte {foreach $items as $item} @@ -37,23 +37,23 @@ protected function createComponentShopForm(): Multiplier {/foreach} ``` -Аргумент, переданный в теге `{control}`, имеет формат, который говорит: +Аргумент, переданный в тег `{control}`, следует формату, который означает: -1. получи компонент `shopForm` -2. и из него получи потомка `$item->id` +1. Получить компонент `shopForm`. +2. Получить у него потомка с именем `$item->id`. -При первом вызове пункта **1.** `shopForm` еще не существует, поэтому вызывается его фабрика `createComponentShopForm`. На полученном компоненте (экземпляре Multiplier) затем вызывается фабрика конкретной формы — это анонимная функция, которую мы передали Multiplier в конструкторе. +При первом выполнении пункта **1** компонента `shopForm` ещё нет, поэтому вызывается его фабрика `createComponentShopForm`. Затем у полученного компонента (экземпляра Multiplier) вызывается фабрика конкретной формы - та самая анонимная функция, которую мы передали в конструктор Multiplier. -В следующей итерации foreach метод `createComponentShopForm` уже не будет вызван (компонент существует), но поскольку мы ищем другого его потомка (`$item->id` будет разным в каждой итерации), снова будет вызвана анонимная функция и вернет нам новую форму. +На следующей итерации цикла foreach метод `createComponentShopForm` уже не вызовется (компонент существует). Однако, поскольку мы ищем другого потомка (ведь `$item->id` на каждой итерации будет разным), анонимная функция вызовется снова и вернёт новую форму. -Единственное, что остается, — это убедиться, что форма добавит в корзину действительно тот товар, который нужно — в настоящее время форма для каждого товара абсолютно одинакова. Нам поможет свойство Multiplier (и вообще любой фабрики компонентов в Nette Framework), а именно то, что каждая фабрика в качестве своего первого аргумента получает имя создаваемого компонента. В нашем случае это будет `$item->id`, что является именно той информацией, которая нам нужна. Достаточно немного изменить создание формы: +Остаётся только позаботиться о том, чтобы форма добавляла в корзину правильный товар - сейчас форма одинакова для всех товаров. Здесь нам помогает особенность Multiplier (да и вообще любой фабрики компонентов в Nette Framework): каждая фабрика получает первым аргументом имя создаваемого компонента. Кроме того, фабрика Multiplier получает вторым аргументом сам экземпляр Multiplier. В нашем случае первым аргументом будет `$item->id`, а это ровно те сведения, которые нам нужны. Значит, достаточно немного изменить создание формы: ```php protected function createComponentShopForm(): Multiplier { - return new Multiplier(function ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Количество товара:') + $form->addInteger('amount', 'Количество:') ->setRequired(); $form->addHidden('itemId', $itemId); $form->addSubmit('send', 'Добавить в корзину'); diff --git a/application/ru/presenters.texy b/application/ru/presenters.texy index 0c75055dae..3adcc01fcc 100644 --- a/application/ru/presenters.texy +++ b/application/ru/presenters.texy @@ -3,37 +3,37 @@
    -Мы познакомимся с тем, как в Nette пишутся презентеры и шаблоны. После прочтения вы будете знать: +Мы разберём, как в Nette пишутся презентеры и шаблоны. После прочтения вы будете понимать: -- как работает презентер -- что такое персистентные параметры +- как работают презентеры +- что такое постоянные параметры - как отрисовываются шаблоны
    -[Мы уже знаем |how-it-works#Nette Application], что презентер — это класс, представляющий какую-либо конкретную страницу веб-приложения, например, главную страницу; товар в интернет-магазине; форму входа; фид карты сайта и т. д. Приложение может иметь от одного до тысяч презентеров. В других фреймворках их также называют контроллерами. +[Мы уже знаем |how-it-works#Nette Application], что презентер - класс, представляющий определённую страницу веб-приложения, например главную страницу, товар в интернет-магазине, форму входа, ленту карты сайта и так далее. У приложения может быть от одного до тысяч презентеров. В других фреймворках их называют контроллерами. -Обычно под понятием презентер подразумевается потомок класса [api:Nette\Application\UI\Presenter], который подходит для генерации веб-интерфейсов и которому мы будем уделять внимание в оставшейся части этой главы. В общем смысле презентер — это любой объект, реализующий интерфейс [api:Nette\Application\IPresenter]. +Обычно под словом "презентер" понимают потомка класса [api:Nette\Application\UI\Presenter], который подходит для создания веб-интерфейсов и которому посвящена остальная часть этой главы. В общем смысле презентер - любой объект, реализующий интерфейс [api:Nette\Application\IPresenter]. Жизненный цикл презентера ========================= -Задача презентера — обработать запрос и вернуть ответ (это может быть HTML-страница, изображение, перенаправление и т. д.). +Задача презентера - обработать запрос и вернуть ответ (которым может быть HTML-страница, изображение, перенаправление и так далее). -То есть вначале ему передается запрос. Это не непосредственно HTTP-запрос, а объект [api:Nette\Application\Request], в который был преобразован HTTP-запрос с помощью маршрутизатора. С этим объектом мы обычно не сталкиваемся, так как презентер умно делегирует обработку запроса другим методам, которые мы сейчас покажем. +Итак, сначала ему передаётся запрос. Это не прямой HTTP-запрос, а объект [api:Nette\Application\Request], в который HTTP-запрос был преобразован с помощью маршрутизатора. Обычно мы с этим объектом напрямую не работаем, потому что презентер ловко передаёт обработку запроса другим методам, которые мы сейчас и рассмотрим. [* lifecycle.svg *] *** Жизненный цикл презентера .<> -Изображение представляет список методов, которые последовательно вызываются сверху вниз, если они существуют. Ни один из них не обязан существовать, у нас может быть совершенно пустой презентер без единого метода, и на нем можно построить простой статический сайт. +Диаграмма показывает список методов, которые вызываются по очереди сверху вниз, если они существуют. Ни один из них не обязателен: у вас может быть совершенно пустой презентер без единого метода, на котором вы построите простой статический сайт. `__construct()` --------------- -Конструктор не совсем относится к жизненному циклу презентера, так как вызывается в момент создания объекта. Но мы упоминаем его из-за важности. Конструктор (вместе с [методом inject |best-practices:inject-method-attribute]) служит для передачи зависимостей. +Строго говоря, конструктор не относится к жизненному циклу презентера, потому что вызывается в момент создания объекта. Но мы упоминаем его из-за его важности. Конструктор (вместе с [методом inject|best-practices:inject-method-attribute]) служит для передачи зависимостей. -Презентер не должен заниматься бизнес-логикой приложения, записывать и читать из базы данных, выполнять вычисления и т. д. Для этого существуют классы из слоя, который мы называем моделью. Например, класс `ArticleRepository` может отвечать за загрузку и сохранение статей. Чтобы презентер мог с ним работать, он получает его [через внедрение зависимостей |dependency-injection:passing-dependencies]: +Презентер не должен заниматься бизнес-логикой приложения, писать в базу данных или читать из неё, выполнять вычисления и подобное. За это отвечают классы слоя, который мы называем моделью. Например, класс `ArticleRepository` может отвечать за загрузку и сохранение статей. Чтобы презентер мог с ним работать, класс нужно [передать через внедрение зависимостей |dependency-injection:passing-dependencies]: ```php @@ -50,44 +50,47 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -Сразу после получения запроса вызывается метод `startup()`. Вы можете использовать его для инициализации свойств, проверки прав пользователя и т. д. Требуется, чтобы метод всегда вызывал предка `parent::startup()`. +Сразу после получения запроса вызывается метод `startup()`. Вы можете использовать его для инициализации свойств, проверки прав пользователя и подобного. Требуется, чтобы этот метод всегда вызывал родительский: `parent::startup()`. `action(args...)` .{toc: action()} -------------------------------------------------- -Аналог метода `render()`. В то время как `render()` предназначен для подготовки данных для конкретного шаблона, который затем будет отрисован, в `action()` обрабатывается запрос без связи с отрисовкой шаблона. Например, обрабатываются данные, пользователь входит в систему или выходит из нее, и так далее, а затем [перенаправляется в другое место |#Перенаправление]. +Похож на метод `render()`. Если `render()` предназначен для подготовки данных конкретного шаблона, который затем будет отрисован, то `action()` обрабатывает запрос, не обязательно отрисовывая после этого шаблон. Например, он может обработать данные, выполнить вход или выход пользователя и затем [перенаправить куда-то |#Перенаправление]. -Важно, что `action()` вызывается раньше, чем `render()`, поэтому в нем мы можем при необходимости изменить дальнейший ход событий, т. е. изменить шаблон, который будет отрисовываться, а также метод `render()`, который будет вызываться. Это делается с помощью `setView('jineView')`. +Важно, что `action()` вызывается *перед* `render()`. Это позволяет нам при необходимости изменить ход запроса внутри метода действия, например поменять шаблон, который будет отрисован, или даже метод `render()`, который будет вызван, с помощью `setView('otherView')`. -Методу передаются параметры из запроса. Возможно и рекомендуется указывать типы параметров, например, `actionShow(int $id, ?string $slug = null)` — если параметр `id` будет отсутствовать или не будет целым числом, презентер вернет [ошибку 404 |#Ошибка 404 и т.п] и завершит работу. +.{data-version:3.2.3} +Вы можете даже переключиться на совершенно другое действие методом `switch('otherAction')`. Он прерывает текущий метод и вместо этого запускает методы `action()` и `render()` нового действия (и отключает автоматическую [канонизацию|#Канонизация]). Сам запрос продолжается; прерывается лишь выполняющийся в данный момент метод. + +В метод передаются параметры из запроса. У этих параметров можно и рекомендуется указывать типы, например `actionShow(int $id, ?string $slug = null)`. Если параметра `id` нет или он не является целым числом, презентер возвращает [ошибку 404 |#Ошибка 404 и подобные] и завершается. `handle(args...)` .{toc: handle()} -------------------------------------------------- -Метод обрабатывает так называемые сигналы, с которыми мы познакомимся в главе, посвященной [компонентам |components#Сигнал]. Он предназначен в основном для компонентов и обработки AJAX-запросов. +Этот метод обрабатывает так называемые сигналы, о которых мы узнаем в главе, посвящённой [компонентам |components#Сигнал]. Он предназначен прежде всего для компонентов и обработки AJAX-запросов. -Методу передаются параметры из запроса, как и в случае `action()`, включая проверку типов. +В метод передаются параметры из запроса, как и в `action()`, включая проверку типов. `beforeRender()` ---------------- -Метод `beforeRender`, как следует из названия, вызывается перед каждым методом `render()`. Используется для общей конфигурации шаблона, передачи переменных для макета и т. п. +Метод `beforeRender`, как следует из его имени, вызывается перед каждым методом `render()`. Он служит для общей настройки шаблона, передачи переменных в макет и подобных задач. `render(args...)` .{toc: render()} ---------------------------------------------- -Место, где мы подготавливаем шаблон к последующей отрисовке, передаем ему данные и т. д. +Здесь мы готовим шаблон к последующей отрисовке, передаём в него данные и так далее. -Методу передаются параметры из запроса, как и в случае `action()`, включая проверку типов. +В метод передаются параметры из запроса, как и в `action()`, включая проверку типов. ```php public function renderShow(int $id): void { - // получаем данные из модели и передаем в шаблон + // получаем данные из модели и передаём их в шаблон $this->template->article = $this->articles->getById($id); } ``` @@ -96,7 +99,7 @@ public function renderShow(int $id): void `afterRender()` --------------- -Метод `afterRender`, как снова следует из названия, вызывается после каждого метода `render()`. Используется довольно редко. +Метод `afterRender`, как опять же следует из имени, вызывается после каждого метода `render()`. Используется он довольно редко. `shutdown()` @@ -105,95 +108,117 @@ public function renderShow(int $id): void Вызывается в конце жизненного цикла презентера. -**Хороший совет, прежде чем идти дальше**. Презентер, как видно, может обслуживать несколько действий/представлений, то есть иметь несколько методов `render()`. Но мы рекомендуем проектировать презентеры с одним или как можно меньшим количеством действий. +События +------- + +Помимо методов `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 или, например, перенаправление на другую страницу. +Ответом презентера обычно служит [отрисовка шаблона в HTML-страницу|templates], но это может быть и отправка файла, JSON или даже перенаправление на другую страницу. -В любой момент жизненного цикла мы можем одним из следующих методов отправить ответ и одновременно завершить работу презентера: +В любой момент жизненного цикла мы можем воспользоваться одним из следующих методов, чтобы отправить ответ и одновременно завершить презентер: -- `redirect()`, `redirectPermanent()`, `redirectUrl()` и `forward()` [перенаправляют |#Перенаправление] -- `error()` завершает презентер [из-за ошибки |#Ошибка 404 и т.п] +- `redirect()`, `redirectPermanent()`, `redirectUrl()` и `forward()` выполняют [перенаправление |#Перенаправление] +- `error()` завершает презентер [из-за ошибки |#Ошибка 404 и подобные] - `sendJson($data)` завершает презентер и [отправляет данные |#Отправка JSON] в формате JSON -- `sendTemplate()` завершает презентер и немедленно [отрисовывает шаблон |templates] +- `sendTemplate()` завершает презентер и сразу [отрисовывает шаблон |templates] - `sendResponse($response)` завершает презентер и отправляет [собственный ответ |#Ответы] - `terminate()` завершает презентер без ответа -Если вы не вызовете ни один из этих методов, презентер автоматически приступит к отрисовке шаблона. Почему? Потому что в 99% случаев мы хотим отрисовать шаблон, поэтому презентер воспринимает это поведение как стандартное и хочет облегчить нам работу. +Каждый из этих методов немедленно завершает презентер, выбрасывая исключение молчаливого завершения `Nette\Application\AbortException`. + +Если вы не вызовете ни один из этих методов, презентер автоматически перейдёт к отрисовке шаблона. Почему? Потому что в 99 % случаев мы хотим отрисовать шаблон, поэтому презентер принимает такое поведение как поведение по умолчанию, чтобы упростить нам работу. Создание ссылок =============== -Презентер располагает методом `link()`, с помощью которого можно создавать URL-ссылки на другие презентеры. Первым параметром является целевой презентер и действие, за ним следуют передаваемые аргументы, которые могут быть указаны как массив: +У презентера есть метод `link()`, служащий для создания URL-ссылок на другие презентеры. Первый параметр - целевой презентер и действие, за ними идут аргументы, которые можно передать массивом: ```php $url = $this->link('Product:show', $id); -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -В шаблоне ссылки на другие презентеры и действия создаются следующим образом: +В шаблоне ссылки на другие презентеры и действия создаются так: ```latte -деталь продукта +product detail ``` -Просто вместо реального URL вы пишете известную пару `Presenter:action` и указываете возможные параметры. Хитрость в `n:href`, которое говорит, что этот атрибут обработает Latte и сгенерирует реальный URL. В Nette вам вообще не нужно думать об URL, только о презентерах и действиях. +Просто напишите привычную пару `Презентер:действие` вместо настоящего URL и добавьте нужные параметры. Хитрость в `n:href`, которая говорит Latte обработать этот атрибут и породить настоящий URL. В Nette вам вообще не нужно думать об URL, только о презентерах и действиях. -Дополнительную информацию можно найти в главе [Создание URL-ссылок |creating-links]. +Подробнее в главе [Создание URL-ссылок|creating-links]. Перенаправление =============== -Для перехода на другой презентер служат методы `redirect()` и `forward()`, которые имеют очень похожий синтаксис, как и метод [link() |#Создание ссылок]. +Для перехода к другому презентеру служат методы `redirect()` и `forward()`. Их синтаксис очень похож на метод [link() |#Создание ссылок]. -Метод `forward()` переходит на новый презентер немедленно без HTTP-перенаправления: +Метод `forward()` переходит к новому презентеру сразу, без HTTP-перенаправления: ```php $this->forward('Product:show'); ``` -Пример так называемого временного перенаправления с HTTP-кодом 302 (или 303, если метод текущего запроса POST): +Пример временного перенаправления с HTTP-кодом 302 (или 303, если текущий метод запроса - POST): ```php $this->redirect('Product:show', $id); ``` -Постоянное перенаправление с HTTP-кодом 301 достигается так: +Чтобы добиться постоянного перенаправления с HTTP-кодом 301, используйте: ```php $this->redirectPermanent('Product:show', $id); ``` -На другой URL вне приложения можно перенаправить методом `redirectUrl()`. В качестве второго параметра можно указать HTTP-код, по умолчанию 302 (или 303, если метод текущего запроса POST): +Перенаправить на другой URL вне приложения можно методом `redirectUrl()`. HTTP-код можно указать вторым параметром; по умолчанию это 302 (или 303, если текущий метод запроса - POST): ```php $this->redirectUrl('https://nette.org'); ``` -Перенаправление немедленно завершает работу презентера, выбрасывая так называемое тихое завершающее исключение `Nette\Application\AbortException`. +Перенаправление немедленно завершает работу презентера, выбрасывая так называемое исключение молчаливого завершения `Nette\Application\AbortException`. -Перед перенаправлением можно отправить [flash-сообщение |#Flash-сообщения], то есть сообщения, которые будут отображены в шаблоне после перенаправления. +Перед перенаправлением можно отправить [flash-сообщения |#Flash-сообщения], то есть сообщения, которые отобразятся в шаблоне после перенаправления. Flash-сообщения =============== -Это сообщения, обычно информирующие о результате какой-либо операции. Важной особенностью flash-сообщений является то, что они доступны в шаблоне даже после перенаправления. Даже после отображения они остаются активными еще 30 секунд — например, на случай, если из-за ошибки передачи пользователь обновит страницу — сообщение не исчезнет сразу. +Это сообщения, обычно извещающие о результате какой-то операции. Важная особенность flash-сообщений в том, что они остаются доступны в шаблоне и после перенаправления. После показа они остаются активными ещё 30 секунд: например, если пользователь обновит страницу из-за ошибки передачи, сообщение не исчезнет сразу. -Достаточно вызвать метод [flashMessage() |api:Nette\Application\UI\Control::flashMessage()], и о передаче в шаблон позаботится презентер. Первым параметром является текст сообщения, а необязательным вторым параметром — его тип (error, warning, info и т. п.). Метод `flashMessage()` возвращает экземпляр flash-сообщения, которому можно добавлять дополнительную информацию. +Достаточно вызвать метод [flashMessage() |api:Nette\Application\UI\Control::flashMessage()], а презентер позаботится о передаче сообщения в шаблон. Первый параметр - текст сообщения, необязательный второй - его тип (например, error, warning, info). Метод `flashMessage()` возвращает экземпляр flash-сообщения, что позволяет добавить дополнительные сведения. ```php -$this->flashMessage('Элемент был удален.'); +$this->flashMessage('The item has been deleted.'); $this->redirect(/* ... */); // и перенаправляем ``` -В шаблоне эти сообщения доступны в переменной `$flashes` как объекты `stdClass`, которые содержат свойства `message` (текст сообщения), `type` (тип сообщения) и могут содержать уже упомянутую пользовательскую информацию. Отрисуем их, например, так: +В шаблоне эти сообщения доступны в переменной `$flashes` как объекты `stdClass`, содержащие свойства `message` (текст сообщения), `type` (тип сообщения) и, возможно, упомянутые добавленные пользователем сведения. Отрисовываем мы их так: ```latte {foreach $flashes as $flash} @@ -202,10 +227,10 @@ $this->redirect(/* ... */); // и перенаправляем ``` -Ошибка 404 и т.п. -================= +Ошибка 404 и подобные +===================== -Если невозможно выполнить запрос, например, из-за того, что статья, которую мы хотим отобразить, не существует в базе данных, мы выбрасываем ошибку 404 методом `error(?string $message = null, int $httpCode = 404)`. +Если запрос выполнить нельзя, например потому что статьи, которую мы хотим показать, нет в базе данных, мы выбрасываем ошибку 404 методом `error(string $message = '', int $httpCode = 404)`. ```php public function renderShow(int $id): void @@ -218,13 +243,13 @@ public function renderShow(int $id): void } ``` -HTTP-код ошибки можно передать вторым параметром, по умолчанию 404. Метод работает так, что выбрасывает исключение `Nette\Application\BadRequestException`, после чего `Application` передает управление error-презентеру. Это презентер, задачей которого является отображение страницы, информирующей о возникшей ошибке. Настройка error-презентера выполняется в [конфигурации application |configuration]. +HTTP-код ошибки можно передать вторым параметром; по умолчанию это 404. Метод работает так, что выбрасывает `Nette\Application\BadRequestException`, после чего `Application` передаёт управление error-презентеру. Это презентер, задача которого - показать страницу с сообщением о произошедшей ошибке. Error-презентер задаётся в [конфигурации приложения|configuration]. Отправка JSON ============= -Пример action-метода, который отправляет данные в формате JSON и завершает работу презентера: +Метод `sendJson($data)` кодирует заданные данные в JSON, отправляет их как HTTP-ответ и завершает презентер. Пример: ```php public function actionData(): void @@ -238,9 +263,9 @@ public function actionData(): void Параметры запроса .{data-version:3.1.14} ======================================== -Презентер, а также каждый компонент, получает свои параметры из HTTP-запроса. Их значение можно узнать методами `getParameter($name)` или `getParameters()`. Значениями являются строки или массивы строк, это, по сути, необработанные данные, полученные непосредственно из URL. +Презентер, а также каждый компонент получают свои параметры из HTTP-запроса. Получить их значения можно методами `getParameter($name)` или `getParameters()`. Значениями служат строки или массивы строк, по сути сырые данные, полученные прямо из URL. -Для большего удобства рекомендуем сделать параметры доступными через свойства. Достаточно пометить их атрибутом `#[Parameter]`: +Ради большего удобства мы рекомендуем обращаться к параметрам через свойства. Достаточно пометить их атрибутом `#[Parameter]`: ```php use Nette\Application\Attributes\Parameter; // эта строка важна @@ -252,23 +277,23 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -У свойства рекомендуется указывать тип данных (например, `string`), и Nette автоматически преобразует значение в соответствии с ним. Значения параметров также можно [валидировать |#Валидация параметров]. +У свойства мы рекомендуем указывать тип данных (например, `string`), и Nette автоматически приведёт значение соответствующим образом. Значения параметров можно и [проверять |#Проверка параметров]. -При создании ссылки можно напрямую установить значение параметра: +При создании ссылки значение параметра можно задать напрямую: ```latte -нажмите +click ``` -Персистентные параметры -======================= +Постоянные параметры +==================== -Персистентные параметры служат для поддержания состояния между различными запросами. Их значение остается неизменным даже после нажатия на ссылку. В отличие от данных в сессии, они передаются в URL. И это происходит полностью автоматически, то есть нет необходимости явно указывать их в `link()` или `n:href`. +Постоянные параметры служат для сохранения состояния между разными запросами. Их значение остаётся тем же и после щелчка по ссылке. В отличие от данных сессии, они передаются в URL. И происходит это полностью автоматически, поэтому явно указывать их в `link()` или `n:href` не нужно. -Пример использования? У вас многоязычное приложение. Текущий язык — это параметр, который должен постоянно присутствовать в URL. Но было бы невероятно утомительно указывать его в каждой ссылке. Так что вы делаете его персистентным параметром `lang`, и он будет передаваться сам. Отлично! +Пример применения? Представьте многоязычное приложение. Текущий язык - параметр, который всегда должен быть частью URL. Но включать его в каждую ссылку было бы невероятно утомительно. Поэтому вы делаете его постоянным параметром `lang`, и он будет переноситься автоматически. Красота! -Создание персистентного параметра в Nette невероятно просто. Достаточно создать публичное свойство и пометить его атрибутом: (ранее использовалось `/** @persistent */`) +Создать постоянный параметр в Nette исключительно просто. Достаточно создать публичное свойство и пометить его атрибутом (раньше использовалось `/** @persistent */`): ```php use Nette\Application\Attributes\Persistent; // эта строка важна @@ -280,14 +305,14 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Если `$this->lang` будет иметь значение, например, `'en'`, то и ссылки, созданные с помощью `link()` или `n:href`, будут содержать параметр `lang=en`. И после нажатия на ссылку снова будет `$this->lang = 'en'`. +Если у `$this->lang` значение вроде `'en'`, то ссылки, созданные через `link()` или `n:href`, будут содержать и параметр `lang=en`. А после щелчка по ссылке `$this->lang` снова будет `'en'`. -У свойства рекомендуется указывать тип данных (например, `string`), и вы можете указать значение по умолчанию. Значения параметров можно [валидировать |#Валидация параметров]. +У свойства мы рекомендуем указывать тип данных (например, `string`), а также можно задать значение по умолчанию. Значения параметров можно [проверять |#Проверка параметров]. -Персистентные параметры по умолчанию передаются между всеми действиями данного презентера. Чтобы они передавались и между несколькими презентерами, их нужно определить либо: +Постоянные параметры обычно переносятся между всеми действиями данного презентера. Чтобы переносить их и между несколькими презентерами, их нужно определить либо: -- в общем предке, от которого наследуют презентеры -- в трейте, который используют презентеры: +- в общем предке, от которого наследуются презентеры +- либо в трейте, который презентеры используют: ```php trait LanguageAware @@ -302,42 +327,62 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -При создании ссылки можно изменить значение персистентного параметра: +При создании ссылки значение постоянного параметра можно изменить: ```latte -деталь на чешском +detail in Czech ``` -Или его можно *сбросить*, то есть удалить из URL. Тогда он примет свое значение по умолчанию: +Как вариант, его можно *сбросить*, то есть убрать из 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], датагриды, меню, в общем, все, что имеет смысл использовать повторно. +В презентеры встроена система компонентов. Компоненты - самостоятельные переиспользуемые единицы, которые мы встраиваем в презентеры. Это могут быть [формы |forms:in-presenter], таблицы данных, меню - в общем, всё, что имеет смысл использовать повторно. -Как компоненты вставляются в презентер и затем используются? Это вы узнаете в главе [Компоненты |components]. Вы даже узнаете, что у них общего с Голливудом. +Как компоненты встраиваются в презентеры и затем используются? Вы узнаете это в главе [Компоненты |components]. Вы даже выясните, что у них общего с Голливудом. -А где я могу получить компоненты? На странице [Componette |https://componette.org/search/component] вы найдете open-source компоненты, а также множество других дополнений для Nette, которые разместили добровольцы из сообщества вокруг фреймворка. +А где взять компоненты? На [Componette |https://componette.org/search/component] вы найдёте компоненты с открытым кодом и множество других дополнений для Nette, созданных добровольцами из сообщества фреймворка. -Идем в глубину -============== +Погружаемся глубже +================== .[tip] -Того, что мы до сих пор показали в этой главе, вам, скорее всего, будет вполне достаточно. Следующие строки предназначены для тех, кто интересуется презентерами в глубину и хочет знать абсолютно все. +То, что мы разобрали в этой главе до сих пор, скорее всего, покроет большинство случаев. Следующие разделы предназначены тем, кто хочет погрузиться в презентеры глубже и знать совершенно всё. -Валидация параметров --------------------- +Проверка параметров +------------------- -Значения [параметров запроса |#Параметры запроса] и [персистентных параметров |#Персистентные параметры], полученные из URL, записываются в свойства методом `loadState()`. Он также проверяет, соответствует ли тип данных, указанный у свойства, иначе отвечает ошибкой 404, и страница не отображается. +Значения [параметров запроса |#Параметры запроса] и [постоянных параметров |#Постоянные параметры], полученные из URL, записываются в свойства методом `loadState()`. Он также проверяет, соответствуют ли они типу данных, указанному у свойства; иначе он ответит ошибкой 404, и страница не отобразится. -Никогда слепо не доверяйте параметрам, потому что они могут быть легко изменены пользователем в URL. Так, например, мы проверим, что язык `$this->lang` находится среди поддерживаемых. Подходящий способ — переопределить упомянутый метод `loadState()`: +Никогда не доверяйте параметрам из URL вслепую, потому что пользователь легко может их переписать. Вот, например, как мы проверили бы, входит ли язык `$this->lang` в число поддерживаемых. Подходящий способ - переопределить упомянутый метод `loadState()`: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -347,8 +392,8 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function loadState(array $params): void { - parent::loadState($params); // здесь устанавливается $this->lang - // следует собственная проверка значения: + parent::loadState($params); // здесь задаётся $this->lang + // далее идёт собственная проверка значения: if (!in_array($this->lang, ['en', 'cs'])) { $this->error(); } @@ -360,27 +405,27 @@ class ProductPresenter extends Nette\Application\UI\Presenter Сохранение и восстановление запроса ----------------------------------- -Запрос, который обрабатывает презентер, является объектом [api:Nette\Application\Request] и возвращается методом презентера `getRequest()`. +Запрос, обрабатываемый презентером, - объект [api:Nette\Application\Request], возвращаемый методом презентера `getRequest()`. -Текущий запрос можно сохранить в сессии или, наоборот, восстановить из нее и позволить презентеру снова его выполнить. Это полезно, например, в ситуации, когда пользователь заполняет форму, и у него истекает сессия. Чтобы не потерять данные, перед перенаправлением на страницу входа мы сохраняем текущий запрос в сессию с помощью `$reqId = $this->storeRequest()`, который возвращает его идентификатор в виде короткой строки, и передаем его как параметр презентеру входа. +Текущий запрос можно сохранить в сессию или, наоборот, восстановить из неё и дать презентеру выполнить его заново. Это полезно, например, когда пользователь заполняет форму, а его сессия входа истекает. Чтобы не потерять данные, перед перенаправлением на страницу входа мы сохраняем текущий запрос в сессию через `$reqId = $this->storeRequest()`. Это возвращает его идентификатор в виде короткой строки, которую мы затем передаём параметром презентеру входа. -После входа мы вызываем метод `$this->restoreRequest($reqId)`, который извлекает запрос из сессии и перенаправляет на него. Метод при этом проверяет, что запрос создал тот же пользователь, который сейчас вошел в систему. Если вошел другой пользователь или ключ недействителен, он ничего не делает, и программа продолжает работу. +После входа мы вызываем метод `$this->restoreRequest($reqId)`, который достаёт запрос из сессии. POST-запросы перебрасываются в него, а остальные (GET) перенаправляются на URL запроса. Метод проверяет, что запрос был создан тем же пользователем, который сейчас вошёл. Если войдёт другой пользователь или ключ недействителен, метод ничего не делает, и программа продолжает работу как обычно. -Посмотрите руководство [Как вернуться к предыдущей странице |best-practices:restore-request]. +См. руководство [Как вернуться на предыдущую страницу |best-practices:restore-request]. Канонизация ----------- -Презентеры обладают одной действительно замечательной особенностью, которая способствует лучшему SEO (оптимизации для поисковых систем). Они автоматически предотвращают существование дублирующегося контента на разных URL. Если к определенной цели ведет несколько URL-адресов, например, `/index` и `/index?page=1`, фреймворк определяет один из них как основной (канонический) и остальные перенаправляет на него с помощью HTTP-кода 301. Благодаря этому поисковые системы не индексируют страницы дважды и не размывают их page rank. +У презентеров есть по-настоящему прекрасная возможность, способствующая лучшему SEO (поисковой оптимизации). Они автоматически предотвращают существование одинакового содержимого под разными URL. Если к определённой цели ведут несколько URL, например `/index` и `/index?page=1`, фреймворк объявляет один из них основным (каноническим) и перенаправляет остальные на него HTTP-кодом 301. Благодаря этому поисковые системы не индексируют ваши страницы дважды и не размывают их вес. -Этот процесс называется канонизацией. Каноническим URL является тот, который генерирует [маршрутизатор |routing], как правило, это первый соответствующий маршрут в коллекции. +Этот процесс называется канонизацией. Канонический URL - тот, который порождает [маршрутизатор|routing], обычно первый подходящий маршрут в наборе. Канонизация включена по умолчанию и может быть отключена через `$this->autoCanonicalize = false`. -Перенаправление не происходит при AJAX- или POST-запросе, так как это привело бы к потере данных или не имело бы дополнительной ценности с точки зрения SEO. +Перенаправление не происходит при AJAX- или POST-запросах, потому что это могло бы привести к потере данных или не дало бы никакой пользы для SEO. -Канонизацию можно вызвать и вручную с помощью метода `canonicalize()`, которому, подобно методу `link()`, передается презентер, действие и параметры. Он создает ссылку и сравнивает ее с текущим URL-адресом. Если они отличаются, он перенаправляет на сгенерированную ссылку. +Вы можете вызвать канонизацию и вручную, методом `canonicalize()`. Как и методу `link()`, вы передаёте ему презентер, действие и параметры. Он порождает ссылку и сравнивает её с текущим URL-адресом. Если они различаются, он перенаправляет на порождённую ссылку. ```php public function actionShow(int $id, ?string $slug = null): void @@ -391,31 +436,13 @@ public function actionShow(int $id, ?string $slug = null): void } ``` - -События -------- - -Кроме методов `startup()`, `beforeRender()` и `shutdown()`, которые вызываются как часть жизненного цикла презентера, можно определить еще другие функции, которые должны вызываться автоматически. Презентер определяет так называемые [события |nette:glossary#События Events], обработчики которых вы добавляете в массивы `$onStartup`, `$onRender` и `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Обработчики в массиве `$onStartup` вызываются непосредственно перед методом `startup()`, далее `$onRender` между `beforeRender()` и `render()`, и наконец `$onShutdown` непосредственно перед `shutdown()`. +Полный пример, объединяющий фильтры маршрутов с `canonicalize()` ради дружественных к SEO URL, см. в [Красивые URL со слагами |best-practices:pretty-urls]. Ответы ------ -Ответ, который возвращает презентер, является объектом, реализующим интерфейс [api:Nette\Application\Response]. Доступно несколько готовых ответов: +Ответ, возвращаемый презентером, - объект, реализующий интерфейс [api:Nette\Application\Response]. Доступно несколько готовых ответов: - [api:Nette\Application\Responses\CallbackResponse] - отправляет callback - [api:Nette\Application\Responses\FileResponse] - отправляет файл @@ -430,13 +457,13 @@ class ArticlePresenter extends Nette\Application\UI\Presenter ```php use Nette\Application\Responses; -// Простой текст +// Обычный текст $this->sendResponse(new Responses\TextResponse('Hello Nette!')); // Отправляет файл $this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); -// Ответом будет callback +// Отправляет callback $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { if ($httpResponse->getHeader('Content-Type') === 'text/html') { echo '

    Hello

    '; @@ -445,28 +472,88 @@ $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $ht $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} +---------------------------------------- -Ограничение доступа с помощью `#[Requires]` .{data-version:3.2.2} ------------------------------------------------------------------ +Когда презентер отрисовывает шаблон, метод `sendTemplate()` прямо перед отрисовкой вызывает `completeTemplate()`. Этот метод заполняет переменные, помеченные атрибутом `#[TemplateVariable]`, и находит файл шаблона (переменные по умолчанию уже задаёт `TemplateFactory` при создании шаблона). Вы можете переопределить этот защищённый метод, чтобы добавить переменные, общие для всех представлений, или задать другой файл: -Атрибут `#[Requires]` предоставляет расширенные возможности для ограничения доступа к презентерам и их методам. Его можно использовать для указания HTTP-методов, требования AJAX-запроса, ограничения на тот же источник (same origin) и доступа только через переадресацию (forwarding). Атрибут можно применять как к классам презентеров, так и к отдельным методам `action()`, `render()`, `handle()` и `createComponent()`. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My App'; +} +``` -Вы можете указать следующие ограничения: -- на HTTP-методы: `#[Requires(methods: ['GET', 'POST'])]` + +Ограничение доступа через `#[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)]` -- доступ только через forward: `#[Requires(forward: true)]` -- ограничение на конкретные действия: `#[Requires(actions: 'default')]` +- доступ только через переброску: `#[Requires(forward: true)]` +- ограничения для конкретных действий: `#[Requires(actions: 'default')]` -Подробности можно найти в руководстве [Как использовать атрибут Requires |best-practices:attribute-requires]. +.[note] +Начиная с версии 3.3 совпадение источника проверяется по заголовку браузера `Sec-Fetch-Site` (раньше через cookie SameSite), что надёжнее и проверяет точное совпадение схемы, домена и порта. + +Подробности в руководстве [Как использовать атрибут Requires |best-practices:attribute-requires]. Проверка HTTP-метода -------------------- -Презентеры в Nette автоматически проверяют HTTP-метод каждого входящего запроса. Причиной этой проверки является прежде всего безопасность. По умолчанию разрешены методы `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. +Презентеры в Nette автоматически проверяют HTTP-метод каждого входящего запроса, прежде всего из соображений безопасности. По умолчанию разрешены методы `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. -Если вы хотите дополнительно разрешить, например, метод `OPTIONS`, используйте для этого атрибут `#[Requires]` (начиная с Nette Application v3.2): +Если вы хотите дополнительно разрешить, например, метод `OPTIONS`, используйте атрибут `#[Requires]` (начиная с Nette Application v3.2.3): ```php #[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] @@ -475,26 +562,34 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -В версии 3.1 проверка выполняется в `checkHttpMethod()`, которая проверяет, содержится ли метод, указанный в запросе, в массиве `$presenter->allowedMethods`. Добавление метода сделайте так: +Начиная с версии 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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -Важно подчеркнуть, что если вы разрешите метод `OPTIONS`, вы должны затем также соответствующим образом обработать его в рамках своего презентера. Метод часто используется как так называемый preflight request, который браузер автоматически отправляет перед фактическим запросом, когда необходимо выяснить, разрешен ли запрос с точки зрения политики CORS (Cross-Origin Resource Sharing). Если вы разрешите метод, но не реализуете правильный ответ, это может привести к несоответствиям и потенциальным проблемам безопасности. +Важно подчеркнуть, что если вы разрешаете метод `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:presenter-traits] - [Передача настроек в презентеры |best-practices:passing-settings-to-presenters] -- [Как вернуться к предыдущей странице |best-practices:restore-request] +- [Как вернуться на предыдущую страницу |best-practices:restore-request] diff --git a/application/ru/routing.texy b/application/ru/routing.texy index dbe0c6a228..7981746ea4 100644 --- a/application/ru/routing.texy +++ b/application/ru/routing.texy @@ -3,26 +3,26 @@
    -Маршрутизатор отвечает за все, что связано с URL-адресами, чтобы вам больше не приходилось о них думать. Мы покажем: +Маршрутизатор берёт на себя всё, что связано с URL-адресами, так что вам о них думать не приходится. Мы покажем: -- как настроить маршрутизатор, чтобы URL были такими, как вы хотите -- расскажем о SEO и перенаправлении +- как настроить маршрутизатор, чтобы URL выглядели так, как вы хотите +- обсудим SEO и перенаправления - и покажем, как написать собственный маршрутизатор
    -Более человечные URL (или также крутые или красивые URL) более удобны в использовании, запоминаемы и положительно влияют на SEO. Nette учитывает это и полностью идет навстречу разработчикам. Вы можете спроектировать для своего приложения именно такую структуру URL-адресов, какую захотите. Вы можете спроектировать ее даже тогда, когда приложение уже готово, потому что это не потребует изменений в коде или шаблонах. Она определяется элегантным способом в одном [единственном месте |#Включение в приложение], в маршрутизаторе, и таким образом не разбросана в виде аннотаций во всех презентерах. +Более дружелюбные к человеку URL (их называют также красивыми) удобнее, лучше запоминаются и положительно влияют на SEO. Nette помнит об этом и полностью идёт навстречу потребностям разработчиков. Вы можете спроектировать для своего приложения ровно ту структуру URL, которую хотите. Вы можете спроектировать её даже тогда, когда приложение уже готово, потому что это не требует изменений ни в коде, ни в шаблонах. Она изящно задаётся в [одном месте |#Встраивание], в маршрутизаторе, а не разбросана аннотациями по всем презентерам. -Маршрутизатор в Nette уникален тем, что он **двусторонний.** Он умеет как декодировать URL в HTTP-запросе, так и создавать ссылки. Таким образом, он играет ключевую роль в [Nette Application |how-it-works#Nette Application], поскольку не только решает, какой презентер и действие будут выполнять текущий запрос, но также используется для [генерации URL |creating-links] в шаблоне и т. д. +Маршрутизатор в Nette исключителен тем, что он **двунаправленный**. Он умеет и расшифровывать URL из HTTP-запросов, и создавать ссылки. Тем самым он играет ключевую роль в [Nette Application |how-it-works#Nette Application], потому что не только решает, какой презентер и действие выполнят текущий запрос, но и используется для [порождения URL |creating-links] в шаблонах и не только. -Однако маршрутизатор не ограничен только этим использованием, вы можете использовать его в приложениях, где презентеры вообще не используются, для REST API и т. д. Подробнее в разделе [#Самостоятельное использование]. +Однако маршрутизатор не ограничивается таким применением: вы можете использовать его в приложениях, где презентеры вообще не применяются, для REST API и прочего. Подробности в разделе [#Самостоятельное использование]. -Коллекция маршрутов -=================== +Набор маршрутов +=============== -Самый приятный способ определения вида URL-адресов в приложении предлагает класс [api:Nette\Application\Routers\RouteList]. Определение состоит из списка так называемых маршрутов, то есть масок URL-адресов и связанных с ними презентеров и действий, с помощью простого API. Маршруты не нужно никак именовать. +Самый приятный способ задать структуру URL-адресов приложения предлагает класс [api:Nette\Application\Routers\RouteList]. Определение состоит из списка так называемых маршрутов, то есть масок URL-адресов и связанных с ними презентеров и действий, через простой API. Никак называть маршруты не нужно. ```php $router = new Nette\Application\Routers\RouteList; @@ -31,104 +31,104 @@ $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. +Пример показывает, что если мы откроем в браузере `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' перехватит первый маршрут и поймет эту строку как +// НЕВЕРНО: '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' +// НЕВЕРНО: ссылка на '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: +Маска описывает относительный путь от корневого каталога сайта. Простейшая маска - статический URL: ```php $router->addRoute('products', 'Products:default'); ``` -Часто маски содержат так называемые **параметры**. Они указываются в угловых скобках (например, ``) и передаются в целевой презентер, например, методу `renderShow(int $year)` или в персистентный параметр `$year`: +Часто маски содержат так называемые **параметры**. Они заключаются в угловые скобки (например, ``) и передаются целевому презентеру, например в метод `renderShow(int $year)` или в постоянный параметр `$year`: ```php $router->addRoute('chronicle/', 'History:show'); ``` -Пример говорит, что если в браузере открыть `https://example.com/chronicle/2020`, отобразится презентер `History` с действием `show` и параметром `year: 2020`. +Пример показывает, что если мы откроем в браузере `https://example.com/chronicle/2020`, отобразится презентер `History` с действием `show` и параметром `year: 2020`. -Параметрам можно определить значение по умолчанию прямо в маске, и тем самым они станут необязательными: +Значение по умолчанию для параметров можно задать прямо в маске, и тогда они станут необязательными: ```php $router->addRoute('chronicle/', 'History:show'); ``` -Маршрут теперь будет принимать и URL `https://example.com/chronicle/`, который снова отобразит `History:show` с параметром `year: 2020`. +Теперь маршрут примет и URL `https://example.com/chronicle/`, который снова отобразит `History:show` с параметром `year: 2020`. -Параметром может быть, конечно, и имя презентера и действия. Например, так: +Разумеется, параметрами могут быть и имена презентера и действия. Например: ```php $router->addRoute('/', 'Home:default'); ``` -Указанный маршрут принимает, например, URL вида `/article/edit` или `/catalog/list` и понимает их как презентеры и действия `Article:edit` и `Catalog:list`. +Указанный маршрут принимает, например, URL вида `/article/edit` или `/catalog/list` и понимает их как презентеры и действия `Article:edit` и `Catalog:list` соответственно. -Одновременно он дает параметрам `presenter` и `action` значения по умолчанию `Home` и `default`, и они, следовательно, также необязательны. Так что маршрут принимает и URL вида `/article` и понимает его как `Article:default`. Или наоборот, ссылка на `Product:default` сгенерирует путь `/product`, ссылка на стандартный `Home:default` путь `/`. +При этом он задаёт параметрам `presenter` и `action` значения по умолчанию `Home` и `default`, благодаря чему они тоже становятся необязательными. Таким образом, маршрут принимает и URL вида `/article` и понимает его как `Article:default`. И наоборот, ссылка на `Product:default` порождает путь `/product`, а ссылка на `Home:default` по умолчанию порождает путь `/`. -Маска может описывать не только относительный путь от корневого каталога сайта, но и абсолютный путь, если начинается со слеша, или даже полный абсолютный URL, если начинается с двух слешей: +Маска может описывать не только относительный путь от корневого каталога сайта, но и абсолютный путь, если начинается со слеша, или даже весь абсолютный URL, если начинается с двух слешей: ```php -// относительно document root +// относительно корня документов $router->addRoute('/', /* ... */); // абсолютный путь (относительно домена) $router->addRoute('//', /* ... */); -// абсолютный URL включая домен (относительно схемы) +// абсолютный URL с доменом (относительно схемы) $router->addRoute('//.example.com//', /* ... */); -// абсолютный URL включая схему +// абсолютный URL со схемой $router->addRoute('https://.example.com//', /* ... */); ``` -Выражения валидации -------------------- +Проверочные выражения +--------------------- -Для каждого параметра можно установить условие валидации с помощью [регулярного выражения |https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Например, для параметра `id` мы укажем, что он может принимать только цифры с помощью регулярного выражения `\d+`: +Для каждого параметра можно задать условие проверки с помощью [регулярного выражения|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' +// принимает https://example.com/a/b/c, путь будет 'a/b/c' $router->addRoute('', /* ... */); ``` @@ -136,17 +136,17 @@ $router->addRoute('', /* ... */); Необязательные последовательности --------------------------------- -В маске можно обозначать необязательные части с помощью квадратных скобок. Необязательной может быть любая часть маски, в ней могут находиться и параметры: +В маске необязательные части можно пометить квадратными скобками. Необязательной может быть любая часть маски, и она может содержать параметры: ```php $router->addRoute('[/]', /* ... */); // Принимает пути: -// /cs/download => lang => cs, name => download +// /en/download => lang => en, name => download // /download => lang => null, name => download ``` -Когда параметр является частью необязательной последовательности, он, разумеется, также становится необязательным. Если у него нет указанного значения по умолчанию, то он будет null. +Когда параметр входит в необязательную последовательность, он, естественно, тоже становится необязательным. Если у него не задано значение по умолчанию, он будет null. Необязательные части могут быть и в домене: @@ -154,7 +154,7 @@ $router->addRoute('[/]', /* ... */); $router->addRoute('//[.]example.com//', /* ... */); ``` -Последовательности можно произвольно вкладывать и комбинировать: +Последовательности можно вкладывать и сочетать как угодно: ```php $router->addRoute( @@ -163,23 +163,23 @@ $router->addRoute( ); // Принимает пути: -// /cs/hello +// /en/hello // /en-us/hello // /hello // /hello/page-12 ``` -При генерации URL стремимся к кратчайшему варианту, поэтому все, что можно опустить, опускается. Поэтому, например, маршрут `index[.html]` генерирует путь `/index`. Изменить поведение можно, указав восклицательный знак после левой квадратной скобки: +При порождении URL предпочитается самый короткий вариант, поэтому всё, что можно опустить, опускается. Так, например, маршрут `index[.html]` порождает путь `/index`. Это поведение можно перевернуть, поставив после левой квадратной скобки восклицательный знак: ```php -// принимает /hello и /hello.html, генерирует /hello +// принимает /hello и /hello.html, порождает /hello $router->addRoute('[.html]', /* ... */); -// принимает /hello и /hello.html, генерирует /hello.html +// принимает /hello и /hello.html, порождает /hello.html $router->addRoute('[!.html]', /* ... */); ``` -Необязательные параметры (т. е. параметры, имеющие значение по умолчанию) без квадратных скобок ведут себя по сути так, как если бы они были заключены в скобки следующим образом: +Необязательные параметры (то есть параметры со значением по умолчанию) без квадратных скобок по сути ведут себя так, будто заключены следующим образом: ```php $router->addRoute('//', /* ... */); @@ -188,7 +188,7 @@ $router->addRoute('//', /* ... */); $router->addRoute('[/[/[]]]', /* ... */); ``` -Если мы хотим повлиять на поведение конечного слеша, чтобы, например, вместо `/home/` генерировалось только `/home`, этого можно достичь так: +Если мы хотим повлиять на поведение завершающего слеша, чтобы, например, порождался `/home` вместо `/home/`, этого можно добиться так: ```php $router->addRoute('[[/[/]]]', /* ... */); @@ -198,7 +198,7 @@ $router->addRoute('[[/[/]]]', /* ... */); Подстановочные знаки -------------------- -В маске абсолютного пути мы можем использовать следующие подстановочные знаки и избежать, например, необходимости записывать в маску домен, который может отличаться в среде разработки и production: +В маске абсолютного URL мы можем использовать следующие подстановочные знаки, чтобы, например, не приходилось писать в маске домен, который может различаться в среде разработки и в производственной среде: - `%tld%` = домен верхнего уровня, например `com` или `org` - `%sld%` = домен второго уровня, например `example` @@ -208,14 +208,14 @@ $router->addRoute('[[/[/]]]', /* ... */); ```php $router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` Расширенная запись ------------------ -Цель маршрута, обычно записываемая в виде `Presenter:action`, может быть также записана с помощью массива, который определяет отдельные параметры и их значения по умолчанию: +Цель маршрута, обычно записываемую в формате `Презентер:действие`, можно записать и массивом, задающим отдельные параметры и их значения по умолчанию: ```php $router->addRoute('/[/]', [ @@ -224,7 +224,7 @@ $router->addRoute('/[/]', [ ]); ``` -Для более детальной спецификации можно использовать еще более расширенную форму, где кроме значений по умолчанию можно настроить и другие свойства параметров, например, валидационное регулярное выражение (см. параметр `id`): +Для более подробного описания можно использовать ещё более развёрнутую форму, где, помимо значений по умолчанию, можно задать другие свойства параметров, например проверочное регулярное выражение (см. параметр `id`): ```php use Nette\Routing\Route; @@ -242,19 +242,29 @@ $router->addRoute('/[/]', [ ]); ``` -Важно отметить, что если параметры, определенные в массиве, не указаны в маске пути, их значения нельзя изменить, даже с помощью query-параметров, указанных после вопросительного знака в URL. +Важно отметить, что если параметры, заданные в массиве, не перечислены в маске пути, их значения изменить нельзя, даже параметрами запроса, указанными после вопросительного знака в URL. + +Это полезно для **фиксированных параметров**: чтобы дать конкретной странице короткий запоминающийся URL. Например, чтобы `/tos` всегда открывал `Article:view` с `id: 123`: + +```php +$router->addRoute('tos', [ + 'presenter' => 'Article', + 'action' => 'view', + 'id' => 123, +]); +``` Фильтры и переводы ------------------ -Исходные коды приложения мы пишем на английском языке, но если сайт должен иметь русские URL, то простая маршрутизация типа: +Исходный код приложения мы пишем по-английски, но если у сайта должны быть чешские URL, то простая маршрутизация вида: ```php $router->addRoute('/', 'Home:default'); ``` -будет генерировать английские URL, например `/product/123` или `/cart`. Если мы хотим, чтобы презентеры и действия в URL были представлены русскими словами (например, `/продукт/123` или `/корзина`), мы можем использовать словарь перевода. Для его записи уже нужна "более многословная" версия второго параметра: +будет порождать английские URL вроде `/product/123` или `/cart`. Если мы хотим, чтобы презентеры и действия в URL были представлены чешскими словами (например, `/produkt/123` или `/kosik`), мы можем использовать словарь переводов. Для его записи нам уже нужен "более многословный" вариант второго параметра: ```php use Nette\Routing\Route; @@ -265,24 +275,24 @@ $router->addRoute('/', [ Route::FilterTable => [ // строка в URL => презентер 'produkt' => 'Product', - 'korzina' => 'Cart', + 'kosik' => 'Cart', 'katalog' => 'Catalog', ], ], 'action' => [ Route::Value => 'default', Route::FilterTable => [ - 'spisok' => 'list', + 'seznam' => 'list', ], ], ]); ``` -Несколько ключей словаря перевода могут вести на один и тот же презентер. Таким образом, к нему создаются различные псевдонимы. Каноническим вариантом (то есть тем, который будет в сгенерированном URL) считается последний ключ. +Несколько ключей словаря переводов могут вести к одному презентеру. Так у него возникают разные псевдонимы. Последний ключ считается каноническим вариантом (то есть тем, который окажется в порождённом URL). -Таблицу перевода можно таким образом использовать для любого параметра. При этом, если перевод не существует, берется исходное значение. Это поведение можно изменить, добавив `Route::FilterStrict => true`, и маршрут тогда отклонит URL, если значение отсутствует в словаре. +Таблицу переводов можно так использовать для любого параметра. Если перевода нет, берётся исходное значение. Это поведение можно изменить, добавив `Route::FilterStrict => true`, и тогда маршрут отклонит URL, если значения нет в словаре. -Кроме словаря перевода в виде массива, можно применить и собственные функции перевода. +Помимо словаря переводов в виде массива можно применять и собственные функции перевода. ```php use Nette\Routing\Route; @@ -298,15 +308,15 @@ $router->addRoute('//', [ ]); ``` -Функция `Route::FilterIn` преобразует параметр в URL в строку, которая затем передается в презентер, функция `FilterOut` обеспечивает преобразование в обратном направлении. +Функция `Route::FilterIn` преобразует параметр из URL в строку, которая затем передаётся презентеру; функция `FilterOut` обеспечивает преобразование в обратную сторону. -Параметры `presenter`, `action` и `module` уже имеют предопределенные фильтры, которые преобразуют между стилем PascalCase или camelCase и kebab-case, используемым в URL. Значение по умолчанию параметров записывается уже в преобразованном виде, поэтому, например, в случае презентера пишем ``, а не ``. +У параметров `presenter`, `action` и `module` уже есть предопределённые фильтры, преобразующие стиль PascalCase или camelCase в kebab-case, используемый в URL. Значение параметров по умолчанию записывается в том виде, в каком оно передаётся приложению (PascalCase для презентера и модуля, camelCase для действия), поэтому, например, в случае презентера мы пишем ``, а не ``. Общие фильтры ------------- -Помимо фильтров, предназначенных для конкретных параметров, мы можем определить также общие фильтры, которые получают ассоциативный массив всех параметров, которые могут как угодно модифицировать и затем вернуть. Общие фильтры определяем под ключом `null`. +Помимо фильтров, предназначенных для конкретных параметров, мы можем задать и общие фильтры, которые получают ассоциативный массив всех параметров, могут как угодно его изменить и вернуть. Общие фильтры задаются под пустым ключом. ```php use Nette\Routing\Route; @@ -314,72 +324,82 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -Общие фильтры дают возможность настроить поведение маршрута абсолютно любым способом. Мы можем использовать их, например, для модификации параметров на основе других параметров. Например, перевод `` и `` на основе текущего значения параметра ``. +Общие фильтры дают возможность совершенно как угодно изменить поведение маршрута. Мы можем использовать их, например, чтобы менять параметры на основе других параметров. Скажем, переводить `` и `` по текущему значению параметра ``. -Если у параметра определен собственный фильтр и одновременно существует общий фильтр, выполняется собственный `FilterIn` перед общим и, наоборот, общий `FilterOut` перед собственным. То есть внутри общего фильтра значения параметров `presenter` или `action` записаны в стиле PascalCase или camelCase. +Если у параметра задан собственный фильтр и при этом существует общий, свой `FilterIn` выполняется раньше общего, и наоборот, общий `FilterOut` выполняется раньше своего. Таким образом, внутри общего фильтра значения параметров `presenter` и `action` записаны в стиле PascalCase или camelCase соответственно. +О практическом применении этих фильтров, порождении дружественных к SEO URL вроде `/article/123-how-to-bake-bread` без изменения шаблонов, см. [Красивые URL со слагами |best-practices:pretty-urls]. -Односторонние маршруты OneWay ------------------------------ -Односторонние маршруты используются для сохранения функциональности старых URL, которые приложение уже не генерирует, но все еще принимает. Мы помечаем их флагом `OneWay`: +Флаг OneWay +----------- + +Односторонние маршруты служат для сохранения работоспособности старых URL, которые приложение больше не порождает, но всё ещё принимает. Мы помечаем их флагом `OneWay`: ```php // старый URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); +$router->addRoute('product-info', 'Product:detail', oneWay: true); // новый URL /product/123 $router->addRoute('product/', 'Product:detail'); ``` -При доступе к старому URL презентер автоматически перенаправляет на новый URL, так что поисковые системы не проиндексируют эти страницы дважды (см. [#SEO и канонизация]). +При обращении к старому URL презентер автоматически перенаправляет на новый, поэтому поисковые системы не проиндексируют эти страницы дважды (см. [#SEO и канонизация]). Динамическая маршрутизация с callback-функциями ----------------------------------------------- -Динамическая маршрутизация с callback-функциями позволяет вам напрямую назначать маршрутам функции (callback), которые будут выполнены при посещении данного пути. Эта гибкая функциональность позволяет быстро и эффективно создавать различные конечные точки (endpoints) для вашего приложения: +Динамическая маршрутизация с callback-функциями позволяет напрямую сопоставить маршрутам функции (callback), которые выполняются при посещении данного пути. Эта гибкая возможность позволяет быстро и эффективно создавать разные точки входа в ваше приложение: ```php $router->addRoute('test', function () { - echo 'вы находитесь по адресу /test'; + echo 'You are at the /test address'; }); ``` -Вы также можете определить в маске параметры, которые автоматически передадутся в ваш callback: +Вы можете задать в маске и параметры, которые автоматически передаются в ваш callback: ```php $router->addRoute('', function (string $lang) { echo match ($lang) { - 'cs' => 'Добро пожаловать на чешскую версию нашего сайта!', + '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()`: +Если у нас несколько маршрутов, относящихся к общему [модулю |directory-structure#Презентеры и шаблоны], мы используем `withModule()`. Указанный модуль автоматически подставляется перед презентером каждого маршрута группы и полностью исчезает из URL: ```php $router = new RouteList; -$router->withModule('Forum') // следующие маршруты являются частью модуля Forum - ->addRoute('rss', 'Feed:rss') // презентер будет Forum:Feed +$router->withModule('Forum') // следующие маршруты входят в модуль Forum + ->addRoute('rss', 'Feed:rss') // презентером будет Forum:Feed ->addRoute('/') - ->withModule('Admin') // следующие маршруты являются частью модуля Forum:Admin + ->withModule('Admin') // следующие маршруты входят в модуль Forum:Admin ->addRoute('sign:in', 'Sign:in'); ``` -Альтернативой является использование параметра `module`: +Альтернатива - параметр `module`, который так же задаёт фиксированный модуль и держит его вне URL: ```php // URL manage/dashboard/default отображается на презентер Admin:Dashboard @@ -388,11 +408,17 @@ $router->addRoute('manage//', [ ]); ``` +Имя каждого презентера полно только вместе с его модулем, например `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; @@ -401,7 +427,7 @@ $router->withDomain('example.com') ->addRoute('/'); ``` -В имени домена можно использовать и [#Подстановочные знаки]: +В имени домена тоже можно использовать [#Подстановочные знаки]: ```php $router = new RouteList; @@ -413,20 +439,20 @@ $router->withDomain('example.%tld%') Префикс пути ------------ -Коллекции маршрутов можно разделять по пути в URL: +Наборы маршрутов можно разделять по пути в URL: ```php $router = new RouteList; $router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // ловит URL /eshop/rss - ->addRoute('/'); // ловит URL /eshop// + ->addRoute('rss', 'Feed:rss') // соответствует URL /eshop/rss + ->addRoute('/'); // соответствует URL /eshop// ``` -Комбинации ----------- +Сочетания +--------- -Вышеупомянутые разделения можно взаимно комбинировать: +Перечисленные группировки можно сочетать друг с другом: ```php $router = (new RouteList) @@ -446,37 +472,37 @@ $router = (new RouteList) ``` -Query-параметры ---------------- +Параметры запроса +----------------- -Маски могут также содержать query-параметры (параметры после вопросительного знака в URL). Для них нельзя определить валидационное выражение, но можно изменить имя, под которым они передадутся в презентер: +Маски могут содержать и параметры запроса (параметры после вопросительного знака в URL). Задать для них проверочное выражение нельзя, но можно изменить имя, под которым они передаются презентеру: ```php -// query-параметр 'cat' мы хотим использовать в приложении под именем 'categoryId' +// мы хотим использовать параметр запроса 'cat' в приложении под именем 'categoryId' $router->addRoute('product ? id= & cat=', /* ... */); ``` -Foo-параметры +Параметры Foo ------------- -Теперь мы углубляемся. Foo-параметры — это, по сути, безымянные параметры, которые позволяют сопоставлять регулярное выражение. Примером является маршрут, принимающий `/index`, `/index.html`, `/index.htm` и `/index.php`: +Теперь копнём глубже. Параметры Foo - по сути безымянные параметры, позволяющие сопоставить регулярное выражение. Пример - маршрут, принимающий `/index`, `/index.html`, `/index.htm` и `/index.php`: ```php $router->addRoute('index', /* ... */); ``` -Можно также явно определить строку, которая будет использоваться при генерации URL. Строка должна быть размещена непосредственно за вопросительным знаком. Следующий маршрут похож на предыдущий, но генерирует `/index.html` вместо `/index`, потому что строка `.html` установлена как генерируемое значение: +Можно и явно задать строку, которая будет использоваться при порождении URL. Строку нужно поместить сразу после вопросительного знака. Следующий маршрут похож на предыдущий, но порождает `/index.html` вместо `/index`, потому что как значение для порождения задана строка `.html`: ```php $router->addRoute('index', /* ... */); ``` -Включение в приложение -====================== +Встраивание +=========== -Чтобы подключить созданный маршрутизатор к приложению, мы должны сообщить о нем DI-контейнеру. Самый простой способ — подготовить фабрику, которая создаст объект маршрутизатора, и сообщить в конфигурации контейнера, что ее нужно использовать. Допустим, для этой цели мы напишем метод `App\Core\RouterFactory::createRouter()`: +Чтобы встроить созданный маршрутизатор в приложение, нам нужно сообщить о нём DI-контейнеру. Проще всего подготовить фабрику, которая создаст объект маршрутизатора, и сказать контейнеру в конфигурации использовать её. Допустим, для этого мы пишем метод `App\Core\RouterFactory::createRouter()`: ```php namespace App\Core; @@ -494,14 +520,14 @@ class RouterFactory } ``` -В [конфигурацию |dependency-injection:services] затем запишем: +Затем мы пишем в [конфигурации |dependency-injection:services]: ```neon services: - App\Core\RouterFactory::createRouter ``` -Любые зависимости, например, от базы данных и т. д., передаются фабричному методу как его параметры с помощью [autowiring |dependency-injection:autowiring]: +Любые зависимости, например от базы данных, передаются в фабричный метод его параметрами через [autowiring|dependency-injection:autowiring]: ```php public static function createRouter(Nette\Database\Connection $db): RouteList @@ -514,22 +540,22 @@ public static function createRouter(Nette\Database\Connection $db): RouteList SimpleRouter ============ -Гораздо более простым маршрутизатором, чем коллекция маршрутов, является [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Мы используем его тогда, когда у нас нет особых требований к форме URL, если недоступен `mod_rewrite` (или его альтернативы) или если мы пока не хотим заниматься красивыми URL. +Гораздо более простой маршрутизатор, чем набор маршрутов, - [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Мы используем его, когда у нас нет особых требований к формату URL, если `mod_rewrite` (или его аналоги) недоступен либо если мы пока не хотим возиться с красивыми URL. -Генерирует адреса примерно в таком виде: +Он порождает адреса примерно такого вида: ``` http://example.com/?presenter=Product&action=detail&id=123 ``` -Параметром конструктора SimpleRouter является стандартный презентер и действие, на который нужно направлять, если мы открываем страницу без параметров, например, `http://example.com/`. +Параметром конструктора `SimpleRouter` служит презентер и действие по умолчанию, то есть действие, которое выполнится, если мы откроем, например, `http://example.com/` без дополнительных параметров. ```php -// стандартным презентером будет 'Home' и действие 'default' +// презентером по умолчанию будет 'Home', а действием 'default' $router = new Nette\Application\Routers\SimpleRouter('Home:default'); ``` -Рекомендуем SimpleRouter напрямую определить в [конфигурации |dependency-injection:services]: +Мы рекомендуем задавать SimpleRouter прямо в [конфигурации |dependency-injection:services]: ```neon services: @@ -540,9 +566,9 @@ services: SEO и канонизация ================= -Фреймворк способствует SEO (оптимизации для поисковых систем), предотвращая дублирование контента на разных URL. Если к определенной цели ведет несколько адресов, например, `/index` и `/index.html`, фреймворк определяет первый из них как основной (канонический) и остальные перенаправляет на него с помощью HTTP-кода 301. Благодаря этому поисковые системы не индексируют страницы дважды и не размывают их page rank. +Фреймворк способствует SEO (поисковой оптимизации), предотвращая существование одинакового содержимого под разными URL. Если к определённой цели ведут несколько адресов, например `/index` и `/index.html`, фреймворк объявляет первый основным (каноническим) и перенаправляет остальные на него HTTP-кодом 301. Благодаря этому поисковые системы не индексируют страницы дважды и не размывают их вес. -Этот процесс называется канонизацией. Каноническим URL является тот, который генерирует маршрутизатор, то есть первый подходящий маршрут в коллекции без флага OneWay. Поэтому в коллекции мы указываем **основные маршруты первыми**. +Этот процесс называется канонизацией. Канонический URL - тот, который порождает маршрутизатор, то есть первый подходящий маршрут в наборе без флага OneWay. Поэтому в наборе мы перечисляем **основные маршруты первыми**. Канонизацию выполняет презентер, подробнее в главе [канонизация |presenters#Канонизация]. @@ -550,9 +576,9 @@ SEO и канонизация HTTPS ===== -Чтобы использовать протокол HTTPS, необходимо включить его на хостинге и правильно настроить сервер. +Чтобы использовать протокол HTTPS, нужно включить его на хостинге и правильно настроить сервер. -Перенаправление всего сайта на HTTPS необходимо настроить на уровне сервера, например, с помощью файла .htaccess в корневом каталоге нашего приложения, и это с HTTP-кодом 301. Настройки могут отличаться в зависимости от хостинга и выглядят примерно так: +Перенаправление всего сайта на HTTPS должно быть задано на уровне сервера, например через файл `.htaccess` в корневом каталоге нашего приложения, с HTTP-кодом 301. Настройки могут отличаться в зависимости от хостинга и выглядеть примерно так: ``` @@ -564,15 +590,15 @@ HTTPS ``` -Маршрутизатор генерирует URL с тем же протоколом, с которым была загружена страница, так что больше ничего настраивать не нужно. +Маршрутизатор порождает URL с тем же протоколом, по которому была загружена страница, поэтому больше ничего задавать не нужно. -Однако, если нам в исключительных случаях нужно, чтобы разные маршруты работали под разными протоколами, мы укажем его в маске маршрута: +Однако если нам в виде исключения нужно, чтобы разные маршруты работали по разным протоколам, мы указываем это в маске маршрута: ```php -// Будет генерировать адрес с HTTP +// Будет порождать HTTP-адрес $router->addRoute('http://%host%//', /* ... */); -// Будет генерировать адрес с HTTPS +// Будет порождать HTTPS-адрес $router->addRoute('https://%host%//', /* ... */); ``` @@ -580,24 +606,24 @@ $router->addRoute('https://%host%//', /* ... */); Отладка маршрутизатора ====================== -Панель маршрутизации, отображаемая в [Tracy Bar |tracy:], является полезным помощником, который показывает список маршрутов, а также параметры, которые маршрутизатор получил из URL. +Панель маршрутизации, отображаемая в [Tracy Bar |tracy:], - полезный помощник, показывающий список маршрутов, а также параметры, которые маршрутизатор получил из URL. -Зеленая полоса с символом ✓ представляет маршрут, который обработал текущий URL, синим цветом и символом ≈ обозначены маршруты, которые также обработали бы URL, если бы зеленый их не опередил. Далее мы видим текущий презентер и действие. +Зелёная полоса с символом ✓ обозначает маршрут, который обработал текущий URL; синий цвет и символ ≈ указывают маршруты, которые тоже обработали бы URL, если бы зелёный их не опередил. Далее мы видим текущие презентер и действие. [* routing-debugger.webp *] -Одновременно, если происходит неожиданное перенаправление из-за [канонизации |#SEO и канонизация], полезно посмотреть в панель в строке *redirect*, где вы узнаете, как маршрутизатор изначально понял URL и почему перенаправил. +Кроме того, если из-за [канонизации |#SEO и канонизация] происходит неожиданное перенаправление, полезно посмотреть на панель в полосе *redirect*, где можно выяснить, как маршрутизатор изначально понял URL и почему он перенаправил. .[note] -При отладке маршрутизатора рекомендуем открыть в браузере Developer Tools (Ctrl+Shift+I или Cmd+Option+I) и в панели Network отключить кеш, чтобы в него не сохранялись перенаправления. +При отладке маршрутизатора мы рекомендуем открыть инструменты разработчика в браузере (Ctrl+Shift+I или Cmd+Option+I) и отключить кеш в панели Network, чтобы перенаправления в нём не сохранялись. Производительность ================== -Количество маршрутов влияет на скорость маршрутизатора. Их число определенно не должно превышать нескольких десятков. Если у вашего сайта слишком сложная структура URL, вы можете написать собственный [#Собственный маршрутизатор] под свои нужды. +Число маршрутов влияет на скорость работы маршрутизатора. Их число точно не должно превышать нескольких десятков. Если у вашего сайта слишком сложная структура URL, вы можете написать собственный [#Собственный маршрутизатор]. -Если маршрутизатор не имеет зависимостей, например, от базы данных, и его фабрика не принимает никаких аргументов, мы можем его скомпилированную форму сериализовать прямо в DI-контейнер и тем самым немного ускорить приложение. +Если у маршрутизатора нет зависимостей, например от базы данных, а его фабрика не принимает аргументов, мы можем сериализовать его скомпилированный вид прямо в DI-контейнер и тем самым немного ускорить приложение. ```neon routing: @@ -608,7 +634,7 @@ routing: Собственный маршрутизатор ========================= -Следующие строки предназначены для очень продвинутых пользователей. Вы можете создать собственный маршрутизатор и совершенно естественно включить его в коллекцию маршрутов. Маршрутизатор — это реализация интерфейса [api:Nette\Routing\Router] с двумя методами: +Следующие строки предназначены очень продвинутым пользователям. Вы можете создать собственный маршрутизатор и естественным образом встроить его в набор маршрутов. Маршрутизатор - реализация интерфейса [api:Nette\Routing\Router] с двумя методами: ```php use Nette\Http\IRequest as HttpRequest; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -Метод `match` обрабатывает текущий запрос [$httpRequest |http:request], из которого можно получить не только URL, но и заголовки и т. д., в массив, содержащий имя презентера и его параметры. Если он не может обработать запрос, возвращает null. При обработке запроса мы должны вернуть как минимум презентер и действие. Имя презентера является полным и содержит также возможные модули: +Метод `match` обрабатывает текущий запрос [$httpRequest |http:request], из которого можно получить не только URL, но и заголовки и прочее, в массив с именем презентера и его параметрами. Если он не может обработать запрос, он возвращает null. При обработке запроса мы должны вернуть как минимум презентер; действие необязательно и по умолчанию равно `default`, если не указано. Имя презентера полное и включает все модули: ```php [ @@ -637,9 +663,9 @@ class MyRouter implements Nette\Routing\Router ] ``` -Метод `constructUrl`, наоборот, составляет из массива параметров итоговый абсолютный URL. Для этого он может использовать информацию из параметра [`$refUrl` |api:Nette\Http\UrlScript], который является текущим URL. +Метод `constructUrl`, наоборот, собирает итоговый абсолютный URL из массива параметров. Он может использовать сведения из параметра [`$refUrl`|api:Nette\Http\UrlScript], которым служит текущий URL. -В коллекцию маршрутов его добавите с помощью `add()`: +Добавьте его в набор маршрутов методом `add()`: ```php $router = new Nette\Application\Routers\RouteList; @@ -652,13 +678,13 @@ $router->addRoute(/* ... */); Самостоятельное использование ============================= -Под самостоятельным использованием мы подразумеваем использование возможностей маршрутизатора в приложении, которое не использует Nette Application и презентеры. Для него действует почти все, что мы показали в этой главе, со следующими отличиями: +Под самостоятельным использованием мы понимаем применение возможностей маршрутизатора в приложении, которое не использует Nette Application и презентеры. К нему относится почти всё, что мы показали в этой главе, со следующими отличиями: -- для коллекций маршрутов мы используем класс [api:Nette\Routing\RouteList] -- в качестве простого маршрутизатора класс [api:Nette\Routing\SimpleRouter] -- поскольку не существует пары `Presenter:action`, мы используем [#Расширенная запись] +- для наборов маршрутов мы используем класс [api:Nette\Routing\RouteList] +- как простой маршрутизатор - класс [api:Nette\Routing\SimpleRouter] +- поскольку пары `Презентер:действие` не существует, мы используем [#Расширенная запись] -Итак, мы снова создаем метод, который нам составит маршрутизатор, например: +Итак, снова создаём метод, который соберёт нам маршрутизатор, например: ```php namespace App\Core; @@ -682,26 +708,26 @@ class RouterFactory } ``` -Если вы используете DI-контейнер, что мы рекомендуем, снова добавляем метод в конфигурацию, а затем получаем маршрутизатор вместе с HTTP-запросом из контейнера: +Если вы используете 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 + // подходящий маршрут не найден, отправляем ошибку 404 exit; } @@ -710,7 +736,7 @@ $controller = $params['controller']; // ... ``` -И наоборот, используем маршрутизатор для составления ссылки: +И наоборот, используйте маршрутизатор для сборки ссылки: ```php $params = ['controller' => 'ArticleController', 'id' => 123]; @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/ru/templates.texy b/application/ru/templates.texy index c399dcdc7a..89fcead967 100644 --- a/application/ru/templates.texy +++ b/application/ru/templates.texy @@ -2,9 +2,9 @@ ******* .[perex] -Nette использует систему шаблонов [Latte |latte:]. Во-первых, потому что это самая безопасная система шаблонов для PHP, а во-вторых, самая интуитивно понятная. Вам не нужно учить много нового, достаточно знания PHP и нескольких тегов. +Nette использует систему шаблонов [Latte |latte:]. Latte выбран потому, что это самая безопасная система шаблонов для PHP и одновременно самая интуитивная. Учить много нового не нужно: достаточно знания PHP и нескольких тегов. -Обычно страница состоит из шаблона макета + шаблона данного действия. Так, например, может выглядеть шаблон макета, обратите внимание на блоки `{block}` и тег `{include}`: +Обычно страница состоит из шаблона макета и шаблона конкретного действия. Вот как может выглядеть шаблон макета; обратите внимание на блоки `{block}` и тег `{include}`: ```latte @@ -20,7 +20,7 @@ Nette использует систему шаблонов [Latte |latte:]. Во ``` -А это будет шаблон действия: +А вот шаблон действия: ```latte {block title}Homepage{/block} @@ -31,15 +31,15 @@ Nette использует систему шаблонов [Latte |latte:]. Во {/block} ``` -Он определяет блок `content`, который вставляется на место `{include content}` в макете, а также переопределяет блок `title`, которым перезаписывает `{block title}` в макете. Попробуйте представить результат. +Он задаёт блок `content`, который вставляется на место `{include content}` в макете, а также переопределяет блок `title`, перекрывающий `{block title}` в макете. Попробуйте представить результат. -Поиск шаблонов --------------- +Поиск шаблона +------------- -Вам не нужно указывать в презентерах, какой шаблон должен быть отрисован, фреймворк сам выведет путь и сэкономит вам написание. +В презентерах вам не нужно указывать, какой шаблон отрисовать: фреймворк выводит путь автоматически и избавляет вас от его написания. -Если вы используете структуру каталогов, где каждый презентер имеет собственный каталог, просто поместите шаблон в этот каталог под именем действия (соответственно, view), т. е. для действия `default` используйте шаблон `default.latte`: +Если вы используете структуру каталогов, где у каждого презентера свой каталог, просто положите шаблон в этот каталог под именем действия (то есть представления). Например, для действия `default` используйте шаблон `default.latte`: /--pre app/ @@ -49,34 +49,34 @@ app/ └── default.latte \-- -Если вы используете структуру, где презентеры находятся вместе в одном каталоге, а шаблоны — в папке `templates`, сохраните его либо в файле `..latte`, либо `/.latte`: +Если вы используете структуру, где презентеры лежат вместе в одном каталоге, а шаблоны в папке `templates`, сохраните его либо в файле `..latte`, либо `/.latte`: /--pre app/ └── Presenters/ ├── HomePresenter.php └── templates/ - ├── Home.default.latte ← 1-й вариант - └── Home/ - └── default.latte ← 2-й вариант + ├── Home/ + │ └── default.latte ← 1-й вариант + └── Home.default.latte ← 2-й вариант \-- -Каталог `templates` может быть расположен также на уровень выше, т. е. на том же уровне, что и каталог с классами презентеров. +Каталог `templates` можно поместить и на уровень выше, то есть рядом с каталогом классов презентеров. -Если шаблон не найден, презентер отвечает [ошибкой 404 - страница не найдена |presenters#Ошибка 404 и т.п]. +Если шаблон не найден, презентер отвечает [ошибкой 404 - страница не найдена |presenters#Ошибка 404 и подобные]. -View можно изменить с помощью `$this->setView('jineView')`. Также можно напрямую указать файл с шаблоном с помощью `$this->template->setFile('/path/to/template.latte')`. +Изменить представление можно через `$this->setView('otherView')`. Можно и прямо указать файл шаблона через `$this->template->setFile('/path/to/template.latte')`. .[note] -Файлы, где ищутся шаблоны, можно изменить, переопределив метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], который возвращает массив возможных имен файлов. +Файлы, в которых ищутся шаблоны, можно изменить, переопределив метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], возвращающий массив возможных имён файлов. Поиск шаблона макета -------------------- -Nette также автоматически ищет файл с макетом. +Nette также автоматически ищет файл макета. -Если вы используете структуру каталогов, где каждый презентер имеет собственный каталог, поместите макет либо в папку с презентером, если он специфичен только для него, либо на уровень выше, если он общий для нескольких презентеров: +Если вы используете структуру каталогов, где у каждого презентера свой каталог, поместите макет либо в папку с презентером, если он относится только к нему, либо на уровень выше, если он общий для нескольких презентеров: /--pre app/ @@ -88,7 +88,7 @@ app/ └── default.latte \-- -Если вы используете структуру, где презентеры находятся вместе в одном каталоге, а шаблоны — в папке `templates`, макет будет ожидаться в следующих местах: +Если вы используете структуру, где презентеры собраны в одном каталоге, а шаблоны в папке `templates`, макет будет ожидаться в этих местах: /--pre app/ @@ -96,33 +96,66 @@ app/ ├── HomePresenter.php └── templates/ ├── @layout.latte ← общий макет - ├── Home.@layout.latte ← только для Home, 1-й вариант - └── Home/ - └── @layout.latte ← только для Home, 2-й вариант + ├── Home/ + │ └── @layout.latte ← только для Home, 1-й вариант + └── Home.@layout.latte ← только для Home, 2-й вариант \-- -Если презентер находится в модуле, поиск будет производиться и на более высоких уровнях каталогов, в соответствии с вложенностью модуля. +Если презентер находится в модуле, поиск идёт и выше по уровням каталогов, соответственно вложенности модулей. -Название макета можно изменить с помощью `$this->setLayout('layoutAdmin')`, и тогда он будет ожидаться в файле `@layoutAdmin.latte`. Также можно напрямую указать файл с шаблоном макета с помощью `$this->setLayout('/path/to/template.latte')`. +Имя макета можно изменить через `$this->setLayout('layoutAdmin')`, и тогда он будет ожидаться в файле `@layoutAdmin.latte`. Можно и прямо указать файл шаблона макета через `$this->setLayout('/path/to/template.latte')`. -С помощью `$this->setLayout(false)` или тега `{layout none}` внутри шаблона поиск макета отключается. +Вызов `$this->setLayout(false)` или тег `{layout none}` внутри шаблона отключают поиск макета. .[note] -Файлы, где ищутся шаблоны макета, можно изменить, переопределив метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], который возвращает массив возможных имен файлов. +Файлы, в которых ищутся шаблоны макетов, можно изменить, переопределив метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], возвращающий массив возможных имён файлов. -Переменные в шаблоне --------------------- +Переменные шаблона +------------------ -Переменные в шаблон передаем, записывая их в `$this->template`, и затем они доступны в шаблоне как локальные переменные: +Переменные передаются в шаблоны записью в `$this->template`. Затем они становятся доступны в шаблоне как локальные переменные: ```php $this->template->article = $this->articles->getById($id); ``` -Таким простым способом мы можем передать в шаблоны любые переменные. Однако при разработке надежных приложений полезнее ограничиться. Например, явно определив перечень переменных, которые ожидает шаблон, и их типы. Благодаря этому PHP сможет проверять типы, IDE правильно подсказывать, а статический анализ выявлять ошибки. +Чтобы значение свойства автоматически передавалось в шаблон как переменная, пометьте его атрибутом `#[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()` + +Если вы используете собственный класс шаблона, эти переменные передаются, если вы создадите для них свойства. + + +Типобезопасные шаблоны +---------------------- -А как такой перечень определить? Просто в виде класса и его свойств. Назовем его похоже на презентер, только с `Template` на конце: +При разработке надёжных приложений полезно явно определить, какие переменные ожидает шаблон и какого они типа. Это даёт проверку типов в PHP, умные подсказки в IDE и позволяет статическому анализу отлавливать ошибки. + +Как определить такой список? Просто как класс со свойствами, представляющими переменные шаблона. Назовите его похоже на презентер, только с `Template` на конце: ```php /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -Объект `$this->template` в презентере теперь будет экземпляром класса `ArticleTemplate`. Так что PHP при записи будет проверять объявленные типы. А начиная с версии PHP 8.2 предупредит и о записи в несуществующую переменную, в предыдущих версиях того же можно достичь с помощью трейта [Nette\SmartObject |utils:smartobject]. +Объект `$this->template` в презентере теперь будет экземпляром класса `ArticleTemplate`. PHP таким образом проверит объявленные типы при записи. + +Класс шаблона Nette выбирает автоматически. Сначала он ищет класс с именем `Template`, например `ArticleEditTemplate` для действия `edit`, и только если такого нет, переходит к `Template`. -Аннотация `@property-read` предназначена для IDE и статического анализа, благодаря ей будет работать автодополнение, см. "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-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: как работать с системой типов":https://blog.nette.org/ru/latte-how-to-use-type-system: +Дополнение кода можно использовать и прямо в шаблонах. Достаточно установить плагин Latte для PhpStorm и указать в начале шаблона имя класса параметров шаблона, подробнее в главе [Latte: система типов |latte:type-system]: ```latte {templateType App\Presentation\Article\ArticleTemplate} ... ``` -Так же работают и шаблоны в компонентах, достаточно лишь соблюдать соглашение об именах и для компонента, например, `FifteenControl` создать класс шаблона `FifteenTemplate`. +То же относится и к компонентам. Достаточно придерживаться соглашения об именовании и создать класс параметров `FifteenTemplate` для компонента вида `FifteenControl`. -Если вам нужно создать `$template` как экземпляр другого класса, используйте метод `createTemplate()`: +Если вам нужно использовать другой класс параметров, воспользуйтесь методом `createTemplate()`: ```php public function renderDefault(): void @@ -168,58 +203,100 @@ public function renderDefault(): void } ``` +.{data-version:3.3.0} +Если вам нужно повлиять на то, как шаблон завершается перед отрисовкой, например добавить переменные, общие для всех действий, вы можете переопределить в презентере метод `completeTemplate()`. Он вызывается прямо перед отрисовкой шаблона: -Переменные по умолчанию ------------------------ - -Презентеры и компоненты автоматически передают в шаблоны несколько полезных переменных: - -- `$basePath` — абсолютный URL-путь к корневому каталогу (например, `/eshop`) -- `$baseUrl` — абсолютный URL к корневому каталогу (например, `http://localhost/eshop`) -- `$user` — объект, [представляющий пользователя |security:authentication] -- `$presenter` — текущий презентер -- `$control` — текущий компонент или презентер -- `$flashes` — массив [сообщений |presenters#Flash-сообщения], отправленных функцией `flashMessage()` - -Если вы используете собственный класс шаблона, эти переменные передадутся, если вы создадите для них свойства. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'My blog'; +} +``` Создание ссылок --------------- -В шаблоне ссылки на другие презентеры и действия создаются следующим образом: +В шаблоне ссылки на другие презентеры и действия создаются так: ```latte -деталь продукта +product detail ``` -Атрибут `n:href` очень удобен для HTML-тегов ``. Если мы хотим вывести ссылку в другом месте, например, в тексте, используем `{link}`: +Атрибут `n:href` очень удобен для HTML-тегов ``. Если мы хотим вывести ссылку в другом месте, например в тексте, мы используем `{link}`: ```latte -Адрес: {link Home:default} +URL is: {link Home:default} ``` -Дополнительную информацию можно найти в главе [Создание URL-ссылок |creating-links]. +Подробнее в главе [Создание URL-ссылок|creating-links]. + + +Собственные фильтры, теги и прочее +---------------------------------- +Систему шаблонов Latte можно расширять собственными фильтрами, функциями, тегами и другими элементами. Есть три подхода, от быстрых разовых решений до архитектурных приёмов для целых приложений. -Собственные фильтры, теги и т.п. --------------------------------- +**Разово в методах презентера** -Систему шаблонов Latte можно расширить собственными фильтрами, функциями, тегами и т. п. Это можно сделать прямо в методе `render` или `beforeRender()`: +Самый быстрый подход - добавить фильтры или функции прямо в код презентера или компонента. В презентерах для этого хорошо подходят методы `beforeRender()` или `render()`: ```php -public function beforeRender(): void +protected function beforeRender(): void { - // добавление фильтра - $this->template->addFilter('foo', /* ... */); + // добавляем фильтр + $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); + + // добавляем функцию + $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); +} +``` - // или конфигурируем непосредственно объект Latte\Engine +В шаблоне: + +```latte +

    Price: {$price|money}

    + +{if isWeekend($now)} ... {/if} +``` + +Для более сложной логики можно настроить объект `Latte\Engine` напрямую: + +```php +protected function beforeRender(): void +{ $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); + $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 версии 3 предлагает более продвинутый способ — создание [расширения |latte:extending-latte#Latte Extension] для каждого веб-проекта. Пример такого класса: +Latte автоматически обнаруживает и регистрирует методы, помеченные этими атрибутами. Имя фильтра или функции в шаблонах совпадает с именем метода. Эти методы должны быть публичными. + +**Глобально через расширения** + +Предыдущие подходы годятся для фильтров и функций, нужных только в отдельных презентерах или компонентах, а не во всём приложении. Для всего приложения лучше всего создать [расширение |latte:extending-latte#Расширение Latte]. Этот класс собирает все расширения Latte вашего проекта в одном месте. Краткий пример: ```php namespace App\Presentation\Accessory; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -Зарегистрируем его с помощью [конфигурации |configuration#Шаблоны Latte]: +Зарегистрируйте расширение через [конфигурацию |configuration#Шаблоны Latte]: ```neon latte: @@ -263,13 +345,27 @@ latte: - 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], в соответствии со своими потребностями. В их документации вы узнаете, как настроить транслятор. +Если вы разрабатываете многоязычное приложение, вам, скорее всего, понадобится выводить в шаблоне часть текстов на разных языках. Для этого Nette Framework определяет интерфейс перевода [api:Nette\Localization\Translator] с единственным методом `translate()`. Он принимает сообщение `$message`, которым обычно служит строка, и любые другие параметры. Задача - вернуть переведённую строку. Реализации по умолчанию в Nette нет; вы можете выбрать из нескольких готовых решений, доступных на [Componette |https://componette.org/search/localization], по своим потребностям. Их документация объясняет, как настроить переводчик. -Шаблонам можно установить переводчик, который мы [получим |dependency-injection:passing-dependencies], методом `setTranslator()`: +Шаблонам можно задать переводчик, который мы [получаем через внедрение |dependency-injection:passing-dependencies], методом `setTranslator()`: ```php protected function beforeRender(): void @@ -279,7 +375,7 @@ protected function beforeRender(): void } ``` -Транслятор альтернативно можно настроить с помощью [конфигурации |configuration#Шаблоны Latte]: +Как вариант, переводчик можно задать через [конфигурацию |configuration#Шаблоны Latte]: ```neon latte: @@ -287,7 +383,7 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Затем переводчик можно использовать, например, как фильтр `|translate`, включая дополнительные параметры, которые передаются методу `translate()` (см. `foo, bar`): +Затем переводчик можно использовать, например, как фильтр `|translate`, в том числе с дополнительными параметрами, которые передаются в метод `translate()` (см. `foo, bar`): ```latte
    {='Корзина'|translate} @@ -295,7 +391,7 @@ latte: {$item|translate, foo, bar} ``` -Или как тег с подчеркиванием: +Или как тег с подчёркиванием: ```latte {_'Корзина'} @@ -303,14 +399,14 @@ latte: {_$item, foo, bar} ``` -Для перевода участка шаблона существует парный тег `{translate}` (начиная с Latte 2.11, ранее использовался тег `{_}`): +Для перевода участка шаблона есть парный тег `{translate}` (начиная с Latte 2.11, раньше использовался тег `{_}`): ```latte {translate}Заказ{/translate} {translate foo, bar}Заказ{/translate} ``` -Транслятор по умолчанию вызывается во время выполнения при отрисовке шаблона. Однако Latte версии 3 умеет переводить все статические тексты уже во время компиляции шаблона. Это экономит производительность, так как каждая строка переводится только один раз, и итоговый перевод записывается в скомпилированную форму. В каталоге кеша таким образом создается несколько скомпилированных версий шаблона, по одной для каждого языка. Для этого достаточно лишь указать язык вторым параметром: +Переводчик обычно вызывается во время выполнения, при отрисовке шаблона. Однако Latte версии 3 умеет переводить все статические тексты уже во время компиляции шаблона. Это экономит производительность, потому что каждая строка переводится только один раз, а готовый перевод записывается в скомпилированный вид. При этом в каталоге кеша появляется несколько скомпилированных версий шаблона, по одной на каждый язык. Для этого достаточно указать язык вторым параметром: ```php protected function beforeRender(): void @@ -320,4 +416,4 @@ protected function beforeRender(): void } ``` -Статическим текстом считается, например, `{_'hello'}` или `{translate}hello{/translate}`. Нестатические тексты, такие как `{_$foo}`, по-прежнему будут переводиться во время выполнения. +Под статическим текстом понимается, например, `{_'hello'}` или `{translate}hello{/translate}`. Нестатические тексты вроде `{_$foo}` по-прежнему будут переводиться во время выполнения. diff --git a/application/ru/upgrading.texy b/application/ru/upgrading.texy new file mode 100644 index 0000000000..a98a84b81c --- /dev/null +++ b/application/ru/upgrading.texy @@ -0,0 +1,47 @@ +Обновление +********** + + +Обновление до версии 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/sl/@home.texy b/application/sl/@home.texy deleted file mode 100644 index ead8f24c66..0000000000 --- a/application/sl/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application je jedro ogrodja Nette, ki prinaša zmogljiva orodja za ustvarjanje sodobnih spletnih aplikacij. Ponuja vrsto izjemnih lastnosti, ki znatno olajšajo razvoj ter izboljšajo varnost in vzdržljivost kode. - - -Namestitev ----------- - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Zakaj izbrati Nette Application? --------------------------------- - -Nette je bil vedno pionir na področju spletnih tehnologij. - -**Dvosmerni usmerjevalnik (Router):** Nette ima napreden sistem usmerjanja, ki je edinstven po svoji dvosmernosti - ne samo da prevaja URL-je v akcije aplikacije, ampak lahko tudi povratno generira URL naslove. To pomeni, da: -- Lahko kadarkoli spremenite strukturo URL-jev celotne aplikacije brez potrebe po urejanju predlog -- URL-ji so samodejno kanonizirani, kar izboljšuje SEO -- Usmerjanje je definirano na enem mestu, ne pa razpršeno v anotacijah - -**Komponente in signali:** Vgrajen komponentni sistem, navdihnjen z Delphi in React.js, je med PHP ogrodji popolnoma izjemen: -- Omogoča ustvarjanje ponovno uporabnih UI elementov -- Podpira hierarhično sestavljanje komponent -- Ponuja elegantno obdelavo AJAX zahtev s pomočjo signalov -- Bogata knjižnica pripravljenih komponent na [Componette](https://componette.org) - -**AJAX in odrezki (snippets):** Nette je predstavil revolucionaren način dela z AJAX-om že leta 2009, dolgo pred podobnimi rešitvami, kot sta Hotwire za Ruby on Rails ali Symfony UX Turbo: -- Odrezki omogočajo posodabljanje samo delov strani brez potrebe po pisanju JavaScripta -- Samodejna integracija s komponentnim sistemom -- Pametna invalidacija delov strani -- Minimalna količina prenesenih podatkov - -**Intuitivne predloge [Latte|latte:]:** Najvarnejši sistem predlog za PHP z naprednimi funkcijami: -- Samodejna zaščita pred XSS s kontekstno občutljivim ubežanjem znakov -- Razširljivost s pomočjo lastnih filtrov, funkcij in značk -- Dedovanje predlog in odrezki za AJAX -- Odlična podpora za PHP 8.x s sistemom tipov - -**Dependency Injection:** Nette v celoti izkorišča Dependency Injection: -- Samodejno posredovanje odvisnosti (autowiring) -- Konfiguracija s pomočjo preglednega formata NEON -- Podpora za tovarne komponent - - -Glavne prednosti ----------------- - -- **Varnost**: Samodejna obramba pred [ranljivostmi|nette:vulnerability-protection] kot so XSS, CSRF itd. -- **Produktivnost**: Manj pisanja, več funkcij zahvaljujoč pametnemu načrtovanju -- **Razhroščevanje**: [Tracy razhroščevalnik|tracy:] z usmerjevalno ploščo -- **Zmogljivost**: Pameten predpomnilnik, leno nalaganje komponent -- **Fleksibilnost**: Enostavna prilagoditev URL-jev tudi po zaključku aplikacije -- **Komponente**: Edinstven sistem ponovno uporabnih UI elementov -- **Sodobno**: Polna podpora za PHP 8.4+ in sistem tipov - - -Prvi koraki ------------ - -1. [Kako delujejo aplikacije? |how-it-works] - Razumevanje osnovne arhitekture -2. [Presenterji |presenters] - Delo s presenterji in akcijami -3. [Predloge |templates] - Ustvarjanje predlog v Latte -4. [Usmerjanje |routing] - Konfiguracija URL naslovov -5. [Interaktivne komponente |components] - Uporaba komponentnega sistema - - -Združljivost s PHP ------------------- - -| različica | združljivo s PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 - -Velja za zadnjo patch različico. diff --git a/application/sl/@left-menu.texy b/application/sl/@left-menu.texy deleted file mode 100644 index 5e3ed359c7..0000000000 --- a/application/sl/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Kako delujejo aplikacije? |how-it-works] -- [Bootstrapping] -- [Presenterji |presenters] -- [Predloge |templates] -- [Struktura imenikov |directory-structure] -- [Usmerjanje |routing] -- [Ustvarjanje URL povezav |creating-links] -- [Interaktivne komponente |components] -- [AJAX & odrezki |ajax] -- [Multiplier |multiplier] -- [Konfiguracija |configuration] - - -Nadaljnje branje -**************** -- [Zakaj uporabljati Nette? |www:10-reasons-why-nette] -- [Namestitev |nette:installation] -- [Napišimo prvo aplikacijo! |quickstart:] -- [Navodila in postopki |best-practices:] -- [Reševanje težav |nette:troubleshooting] diff --git a/application/sl/@meta.texy b/application/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/application/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/application/sl/ajax.texy b/application/sl/ajax.texy deleted file mode 100644 index b64566c406..0000000000 --- a/application/sl/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & odrezki -************** - -
    - -V dobi sodobnih spletnih aplikacij, kjer je funkcionalnost pogosto razdeljena med strežnikom in brskalnikom, je AJAX nujen povezovalni element. Kakšne možnosti nam na tem področju ponuja Nette Framework? -- pošiljanje delov predloge, t.i. odrezkov -- posredovanje spremenljivk med PHP in JavaScriptom -- orodja za razhroščevanje AJAX zahtevkov - -
    - - -AJAX zahtevek -============= - -AJAX zahtevek se v bistvu ne razlikuje od klasičnega HTTP zahtevka. Pokliče se presenter z določenimi parametri. Od presenterja pa je odvisno, kako se bo na zahtevek odzval - lahko vrne podatke v formatu JSON, pošlje del HTML kode, XML dokument itd. - -Na strani brskalnika inicializiramo AJAX zahtevek s funkcijo `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // obdelava odgovora -}); -``` - -Na strani strežnika prepoznamo AJAX zahtevek z metodo `$httpRequest->isAjax()` storitve [enkapsulirajoč HTTP zahtevek |http:request]. Za zaznavanje uporablja HTTP glavo `X-Requested-With`, zato je pomembno, da jo pošiljamo. V okviru presenterja lahko uporabimo metodo `$this->isAjax()`. - -Če želite poslati podatke v formatu JSON, uporabite metodo [`sendJson()` |presenters#Pošiljanje odgovora]. Metoda prav tako zaključi delovanje presenterja. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Če nameravate odgovoriti s posebno predlogo, namenjeno za AJAX, lahko to storite na naslednji način: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Odrezki -======= - -Najmočnejše sredstvo, ki ga Nette ponuja za povezovanje strežnika s klientom, so odrezki. Zahvaljujoč njim lahko iz navadne aplikacije naredite AJAX aplikacijo z minimalnim naporom in nekaj vrsticami kode. Kako vse skupaj deluje, prikazuje primer Fifteen, katerega kodo najdete na [GitHubu |https://github.com/nette-examples/fifteen]. - -Odrezki omogočajo posodabljanje samo delov strani, namesto da bi se celotna stran ponovno nalagala. To ni samo hitrejše in učinkovitejše, ampak zagotavlja tudi udobnejšo uporabniško izkušnjo. Odrezki vas lahko spominjajo na Hotwire za Ruby on Rails ali Symfony UX Turbo. Zanimivo je, da je Nette predstavil odrezke že 14 let prej. - -Kako odrezki delujejo? Ob prvem nalaganju strani (ne-AJAX zahtevek) se naloži celotna stran, vključno z vsemi odrezki. Ko uporabnik interagira s stranjo (npr. klikne na gumb, pošlje obrazec itd.), se namesto nalaganja celotne strani sproži AJAX zahtevek. Koda v presenterju izvede akcijo in odloči, katere odrezke je treba posodobiti. Nette te odrezke izriše in jih pošlje v obliki polja v formatu JSON. Obdelovalna koda v brskalniku prejete odrezke vstavi nazaj v stran. Prenaša se torej samo koda spremenjenih odrezkov, kar prihrani pasovno širino in pospeši nalaganje v primerjavi s prenosom vsebine celotne strani. - - -Naja ----- - -Za obdelavo odrezkov na strani brskalnika služi [knjižnica Naja |https://naja.js.org]. To [namestite |https://naja.js.org/#/guide/01-install-setup-naja] kot node.js paket (za uporabo z aplikacijami Webpack, Rollup, Vite, Parcel in drugimi): - -```shell -npm install naja -``` - -…ali pa jo neposredno vstavite v predlogo strani: - -```latte - -``` - -Najprej je treba knjižnico [inicializirati |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -Da bi se iz navadne povezave (signala) ali pošiljanja obrazca ustvaril AJAX zahtevek, je dovolj označiti ustrezno povezavo, obrazec ali gumb z razredom `ajax`: - -```latte -Go - -
    - -
    - -ali - -
    - -
    -``` - - -Prekresljevanje odrezkov ------------------------- - -Vsak objekt razreda [Control |components] (vključno s samim Presenterjem) beleži, ali so se zgodile spremembe, ki zahtevajo njegovo prekreslitev. Za to služi metoda `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // po prijavi je treba prekresliti relevantni del - $this->redrawControl(); - // ... -} -``` - -Nette omogoča še natančnejši nadzor nad tem, kaj se mora prekresliti. Navedena metoda namreč lahko kot argument sprejme ime odrezka. Tako lahko razveljavimo (razumite: prisilimo prekreslitev) na ravni delov predloge. Če se razveljavi celotna komponenta, se prekresli tudi vsak njen odrezek: - -```php -// razveljavi odrezek 'header' -$this->redrawControl('header'); -``` - - -Odrezki v Latte ---------------- - -Uporaba odrezkov v Latte je izjemno enostavna. Če želite definirati del predloge kot odrezek, ga preprosto ovijte z značkama `{snippet}` in `{/snippet}`: - -```latte -{snippet header} -

    Hello ...

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

    Hello ...

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

    Glasujte

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

    {$item->title}

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

    Hello

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

    Domača stran

    -... -{/block} -``` - -Ta definira blok `content`, ki se vstavi na mesto `{include content}` v postavitvi, in tudi ponovno definira blok `title`, s katerim prepiše `{block title}` v postavitvi. Poskusite si predstavljati rezultat. - - -Iskanje predlog ---------------- - -Ni vam treba v presenterjih navajati, katera predloga naj se izriše, ogrodje pot izpelje samo in vam prihrani pisanje. - -Če uporabljate strukturo map, kjer ima vsak presenter svojo mapo, preprosto namestite predlogo v to mapo pod imenom akcije (oz. view), tj. za akcijo `default` uporabite predlogo `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Če uporabljate strukturo, kjer so skupaj presenterji v eni mapi in predloge v mapi `templates`, jo shranite bodisi v datoteko `..latte` ali `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1. varianta - └── Home/ - └── default.latte ← 2. varianta -\-- - -Mapa `templates` je lahko nameščena tudi eno raven višje, tj. na isti ravni, kot je mapa z razredi presenterjev. - -Če predloga ni najdena, presenter odgovori z [napako 404 - stran ni najdena |presenters#Napaka 404 in podobno]. - -View spremenite s pomočjo `$this->setView('jineView')`. Prav tako lahko neposredno določite datoteko s predlogo s pomočjo `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Datoteke, kjer se iščejo predloge, lahko spremenite s prepisom metode [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], ki vrne polje možnih imen datotek. - - -Iskanje predloge postavitve ---------------------------- - -Nette tudi samodejno išče datoteko s postavitvijo. - -Če uporabljate strukturo map, kjer ima vsak presenter svojo mapo, namestite postavitev bodisi v mapo s presenterjem, če je specifična samo zanj, ali eno raven višje, če je skupna za več presenterjev: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← skupna postavitev - └── Home/ - ├── @layout.latte ← samo za presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Če uporabljate strukturo, kjer so skupaj presenterji v eni mapi in predloge v mapi `templates`, se bo postavitev pričakovala na teh mestih: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← skupna postavitev - ├── Home.@layout.latte ← samo za Home, 1. varianta - └── Home/ - └── @layout.latte ← samo za Home, 2. varianta -\-- - -Če se presenter nahaja v modulu, se bo iskalo tudi na višjih ravneh map, glede na gnezdenje modula. - -Ime postavitve lahko spremenite s pomočjo `$this->setLayout('layoutAdmin')` in potem se bo pričakovalo v datoteki `@layoutAdmin.latte`. Prav tako lahko neposredno določite datoteko s predlogo postavitve s pomočjo `$this->setLayout('/path/to/template.latte')`. - -S pomočjo `$this->setLayout(false)` ali značke `{layout none}` znotraj predloge se iskanje postavitve izklopi. - -.[note] -Datoteke, kjer se iščejo predloge postavitve, lahko spremenite s prepisom metode [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], ki vrne polje možnih imen datotek. - - -Spremenljivke v predlogi ------------------------- - -Spremenljivke v predlogo posredujemo tako, da jih zapišemo v `$this->template` in potem jih imamo na voljo v predlogi kot lokalne spremenljivke: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Tako enostavno lahko v predloge posredujemo kakršnekoli spremenljivke. Pri razvoju robustnih aplikacij pa je običajno bolj koristno se omejiti. Na primer tako, da eksplicitno definiramo seznam spremenljivk, ki jih predloga pričakuje, in njihovih tipov. Zahvaljujoč temu nam bo lahko PHP preverjal tipe, IDE pravilno predlagal in statična analiza odkrivala napake. - -In kako takšen seznam definiramo? Preprosto v obliki razreda in njegovih lastnosti. Poimenujemo ga podobno kot presenter, le s `Template` na koncu: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // in druge spremenljivke -} -``` - -Objekt `$this->template` v presenterju bo zdaj instanca razreda `ArticleTemplate`. Tako bo PHP pri zapisu preverjal deklarirane tipe. In od različice PHP 8.2 naprej bo opozoril tudi na zapis v neobstoječo spremenljivko, v prejšnjih različicah lahko isto dosežemo z uporabo traite [Nette\SmartObject |utils:smartobject]. - -Anotacija `@property-read` je namenjena za IDE in statično analizo, zahvaljujoč njej bo delovalo predlaganje, glej "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Luksuza predlaganja si lahko privoščite tudi v predlogah, dovolj je v PhpStorm namestiti vtičnik za Latte in navesti na začetek predloge ime razreda, več v članku "Latte: kako do tipskega sistema":https://blog.nette.org/sl/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Tako delujejo tudi predloge v komponentah, dovolj je le upoštevati imensko konvencijo in za komponento npr. `FifteenControl` ustvariti razred predloge `FifteenTemplate`. - -Če potrebujete ustvariti `$template` kot instanco drugega razreda, uporabite metodo `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Privzete spremenljivke ----------------------- - -Presenterji in komponente samodejno posredujejo v predloge nekaj uporabnih spremenljivk: - -- `$basePath` je absolutna URL pot do korenskega direktorija (npr. `/eshop`) -- `$baseUrl` je absolutni URL do korenskega direktorija (npr. `http://localhost/eshop`) -- `$user` je objekt [ki predstavlja uporabnika |security:authentication] -- `$presenter` je trenutni presenter -- `$control` je trenutna komponenta ali presenter -- `$flashes` polje [sporočil |presenters#Flash sporočila] poslanih s funkcijo `flashMessage()` - -Če uporabljate lasten razred predloge, se te spremenljivke posredujejo, če zanje ustvarite lastnost. - - -Ustvarjanje povezav -------------------- - -V predlogi se ustvarjajo povezave na druge presenterje & akcije na ta način: - -```latte -podrobnosti izdelka -``` - -Atribut `n:href` je zelo priročen za HTML značke ``. Če želimo povezavo izpisati drugje, na primer v besedilu, uporabimo `{link}`: - -```latte -Naslov je: {link Home:default} -``` - -Več informacij najdete v poglavju [Ustvarjanje URL povezav |creating-links]. - - -Lastni filtri, značke ipd. --------------------------- - -Sistem predlog Latte lahko razširimo z lastnimi filtri, funkcijami, značkami ipd. To lahko storimo neposredno v metodi `render` ali `beforeRender()`: - -```php -public function beforeRender(): void -{ - // dodajanje filtra - $this->template->addFilter('foo', /* ... */); - - // ali konfiguriramo neposredno objekt Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte v različici 3 ponuja naprednejši način in to je ustvarjanje si [extension |latte:extending-latte#Latte Extension] za vsak spletni projekt. Primer takšnega razreda: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Registriramo jo s pomočjo [konfiguracije |configuration#Predloge Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Prevajanje ----------- - -Če programirate večjezično aplikacijo, boste najverjetneje potrebovali nekatera besedila v predlogi izpisati v različnih jezikih. Nette Framework za ta namen definira vmesnik za prevajanje [api:Nette\Localization\Translator], ki ima eno samo metodo `translate()`. Ta sprejema sporočilo `$message`, kar je praviloma niz, in poljubne druge parametre. Naloga je vrniti preveden niz. V Nette ni nobene privzete implementacije, lahko si izberete glede na svoje potrebe iz več pripravljenih rešitev, ki jih najdete na [Componette |https://componette.org/search/localization]. V njihovi dokumentaciji boste izvedeli, kako prevajalnik konfigurirati. - -Predlogam lahko nastavimo prevajalnik, ki si ga [pustimo posredovati |dependency-injection:passing-dependencies], z metodo `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Prevajalnik je alternativno mogoče nastaviti s pomočjo [konfiguracije |configuration#Predloge Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Nato lahko prevajalnik uporabljamo na primer kot filter `|translate`, in to vključno z dopolnilnimi parametri, ki se posredujejo metodi `translate()` (glej `foo, bar`): - -```latte -{='Košarica'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Ali kot podčrtajno značko: - -```latte -{_'Košarica'} -{_$item} -{_$item, foo, bar} -``` - -Za prevod odseka predloge obstaja parna značka `{translate}` (od Latte 2.11, prej se je uporabljala značka `{_}`): - -```latte -{translate}Naročilo{/translate} -{translate foo, bar}Naročilo{/translate} -``` - -Prevajalnik se standardno kliče med izvajanjem pri izrisovanju predloge. Latte različice 3 pa zna vsa statična besedila prevajati že med kompilacijo predloge. S tem se prihrani zmogljivost, ker se vsak niz prevede samo enkrat in končni prevod se zapiše v prevedeno obliko. V mapi s predpomnilnikom tako nastane več prevedenih različic predloge, ena za vsak jezik. Za to je dovolj le navesti jezik kot drugi parameter: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Statično besedilo je mišljeno na primer `{_'hello'}` ali `{translate}hello{/translate}`. Nestatična besedila, kot na primer `{_$foo}`, se bodo še naprej prevajala med izvajanjem. diff --git a/application/tr/@home.texy b/application/tr/@home.texy index be4ca80d0f..5b5f2f9b5a 100644 --- a/application/tr/@home.texy +++ b/application/tr/@home.texy @@ -2,84 +2,84 @@ Nette Application ***************** .[perex] -Nette Application, modern web uygulamaları oluşturmak için güçlü araçlar sunan Nette framework'ünün çekirdeğidir. Geliştirmeyi önemli ölçüde kolaylaştıran ve kodun güvenliğini ve sürdürülebilirliğini artıran bir dizi olağanüstü özellik sunar. +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] aracını kullanarak indirip kurabilirsiniz: +Kütüphaneyi [Composer|best-practices:composer] ile indirin ve kurun: ```shell composer require nette/application ``` -Neden Nette Application'ı Seçmelisiniz? ---------------------------------------- +Neden Nette Application? +------------------------ -Nette, web teknolojileri alanında her zaman öncü olmuştur. +Nette web teknolojilerinde her zaman öncü olmuştur. -**Çift Yönlü Yönlendirici:** Nette, benzersiz çift yönlülüğü ile gelişmiş bir yönlendirme sistemine sahiptir - yalnızca URL'leri uygulama eylemlerine çevirmekle kalmaz, aynı zamanda geriye dönük olarak URL adresleri de oluşturabilir. Bu şu anlama gelir: -- Şablonları düzenlemeye gerek kalmadan tüm uygulamanın URL yapısını istediğiniz zaman değiştirebilirsiniz -- URL'ler otomatik olarak standartlaştırılır, bu da SEO'yu iyileştirir -- Yönlendirme, ek açıklamalara dağılmış olarak değil, tek bir yerde tanımlanır +**Ç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'den ilham alan yerleşik bileşen sistemi, PHP framework'leri arasında tamamen benzersizdir: -- Yeniden kullanılabilir UI öğeleri oluşturmanıza olanak tanır +**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 -- Sinyalleri kullanarak AJAX isteklerinin zarif bir şekilde işlenmesini sunar -- [Componette](https://componette.org) üzerinde zengin hazır bileşen kütüphanesi +- 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, Ruby on Rails için Hotwire veya Symfony UX Turbo gibi benzer çözümlerden çok önce, 2009'da AJAX ile çalışmanın devrim niteliğinde bir yolunu tanıttı: -- Snippet'ler, JavaScript yazmaya gerek kalmadan sayfanın yalnızca bölümlerini güncellemenizi sağlar +**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ıca geçersizleştirilmesi -- Minimum miktarda aktarılan veri +- Sayfa bölümlerinin akıllı biçimde geçersiz kılınması +- En az veri aktarımı -**Sezgisel Şablonlar [Latte|latte:]:** Gelişmiş özelliklere sahip PHP için en güvenli şablonlama sistemi: -- Bağlama duyarlı kaçış (escaping) ile XSS'ye karşı otomatik koruma -- Özel filtreler, fonksiyonlar ve etiketler aracılığıyla genişletilebilirlik -- AJAX için şablon kalıtımı ve snippet'ler -- Tip sistemi ile PHP 8.x için mükemmel destek +**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 -**Dependency Injection:** Nette, Dependency Injection'ı tam olarak kullanır: -- Bağımlılıkların otomatik olarak geçirilmesi (autowiring) -- Anlaşılır NEON formatı kullanılarak yapılandırma -- Bileşen fabrikaları için destek +**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 +Başlıca avantajlar ------------------ -- **Güvenlik**: XSS, CSRF vb. gibi [güvenlik açıklarına|nette:vulnerability-protection] karşı otomatik koruma. -- **Verimlilik**: Akıllı tasarım sayesinde daha az yazma, daha fazla işlev. -- **Hata Ayıklama**: Yönlendirme panelli [Tracy hata ayıklayıcı|tracy:]. -- **Performans**: Akıllı önbellek, bileşenlerin geç yüklenmesi (lazy loading). -- **Esneklik**: Uygulama tamamlandıktan sonra bile URL'lerin kolayca değiştirilmesi. -- **Bileşenler**: Yeniden kullanılabilir UI öğelerinin benzersiz sistemi. -- **Modern**: PHP 8.4+ ve tip sistemi için tam destek. +- **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 anlama -2. [Presenter'lar |presenters] - Presenter'lar ve eylemlerle çalışma -3. [Şablonlar |templates] - Latte'de şablon oluşturma -4. [Yönlendirme |routing] - URL adreslerini yapılandırma -5. [Etkileşimli bileşenler |components] - Bileşen sistemini kullanma +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 ile Uyumluluk ------------------ +PHP uyumluluğu +-------------- -| sürüm | PHP ile uyumlu -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 +| sü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 -Son yama sürümü için geçerlidir. +En son yama sürümleri için geçerlidir. diff --git a/application/tr/@left-menu.texy b/application/tr/@left-menu.texy index 05dd6798ff..3a084f4197 100644 --- a/application/tr/@left-menu.texy +++ b/application/tr/@left-menu.texy @@ -1,22 +1,24 @@ Nette Application ***************** +- [Genel bakış |@home] - [Uygulamalar nasıl çalışır? |how-it-works] -- [Bootstrapping] -- [Presenter'lar |presenters] -- [Şablonlar |templates] +- [Bootstrapping|bootstrapping] +- [Presenter'lar|presenters] +- [Şablonlar|templates] - [Dizin yapısı |directory-structure] -- [Yönlendirme |routing] -- [URL Bağlantıları Oluşturma |creating-links] +- [Yönlendirme|routing] +- [URL bağlantıları oluşturma |creating-links] - [Etkileşimli bileşenler |components] -- [AJAX & snippet'ler |ajax] -- [Multiplier |Multiplier] -- [Yapılandırma |configuration] +- [AJAX ve snippet'ler |ajax] +- [Multiplier |multiplier] +- [Yapılandırma|configuration] +- [Yükseltme|upgrading] -Daha Fazla Okuma -**************** -- [Neden Nette kullanmalı? |www:10-reasons-why-nette] +İleri okuma +*********** +- [Neden Nette kullanmalı?|www:10-reasons-why-nette] - [Kurulum |nette:installation] -- [İlk uygulamamızı yazıyoruz! |quickstart:] -- [Kılavuzlar ve yöntemler |best-practices:] -- [Sorun Giderme |nette:troubleshooting] +- [İlk uygulamanızı oluşturun! |quickstart:] +- [En iyi uygulamalar |best-practices:] +- [Sorun giderme |nette:troubleshooting] diff --git a/application/tr/ajax.texy b/application/tr/ajax.texy index cad8af482b..77de245dba 100644 --- a/application/tr/ajax.texy +++ b/application/tr/ajax.texy @@ -1,22 +1,22 @@ -AJAX & Snippet'ler -****************** +AJAX ve snippet'ler +*******************
    -Sunucu ve tarayıcı arasında işlevselliğin sıklıkla bölündüğü modern web uygulamaları çağında, AJAX vazgeçilmez bir bağlantı elemanıdır. Nette Framework bize bu alanda hangi seçenekleri sunuyor? -- şablonun parçalarını, yani snippet'leri gönderme -- PHP ve JavaScript arasında değişkenleri iletme -- AJAX isteklerinin hatalarını ayıklama araçları +İş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 İsteği +AJAX isteği =========== -Bir AJAX isteği, temelde klasik bir HTTP isteğinden farklı değildir. Belirli parametrelerle bir presenter çağrılır. Ve isteğe nasıl yanıt vereceği presenter'a bağlıdır - JSON formatında veri döndürebilir, HTML kodunun bir kısmını, bir XML belgesini vb. gönderebilir. +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, `fetch()` fonksiyonunu kullanarak bir AJAX isteği başlatırız: +Tarayıcı tarafında AJAX isteğini `fetch()` fonksiyonuyla başlatırız: ```js fetch(url, { @@ -24,22 +24,22 @@ fetch(url, { }) .then(response => response.json()) .then(payload => { - // yanıtın işlenmesi + // yanıtı işle }); ``` -Sunucu tarafında, [HTTP isteğini kapsayan |http:request] servisin `$httpRequest->isAjax()` metoduyla bir AJAX isteğini tanırız. Algılama için `X-Requested-With` HTTP başlığını kullanır, bu yüzden onu göndermek önemlidir. Presenter içinde `$this->isAjax()` metodunu kullanabilirsiniz. +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. -Verileri JSON formatında göndermek istiyorsanız, [`sendJson()` |presenters#Yanıt Gönderme] metodunu kullanın. Metot ayrıca presenter'ın etkinliğini de sonlandırır. +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); + $this->sendJson($this->model->getData()); } ``` -AJAX için tasarlanmış özel bir şablonla yanıt vermeyi planlıyorsanız, bunu aşağıdaki gibi yapabilirsiniz: +AJAX için tasarlanmış özel bir şablonla yanıt vermeyi planlıyorsanız şöyle yapabilirsiniz: ```php public function handleClick($param): void @@ -55,26 +55,26 @@ public function handleClick($param): void Snippet'ler =========== -Nette'nin sunucuyu istemciyle bağlamak için sunduğu en güçlü araç snippet'lerdir. Onlar sayesinde, sıradan bir uygulamayı minimum çaba ve birkaç satır kodla AJAX uygulamasına dönüştürebilirsiniz. Tüm bunların nasıl çalıştığını, kodunu [GitHub'da |https://github.com/nette-examples/fifteen] bulabileceğiniz Fifteen örneği göstermektedir. +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 veya kesitler, tüm sayfanın yeniden yüklenmesi yerine sayfanın yalnızca bölümlerini güncellemenize olanak tanır. Bu sadece daha hızlı ve daha verimli olmakla kalmaz, aynı zamanda daha rahat bir kullanıcı deneyimi de sağlar. Snippet'ler size Ruby on Rails için Hotwire'ı veya Symfony UX Turbo'yu hatırlatabilir. İlginç bir şekilde, Nette snippet'leri 14 yıl önce tanıttı. +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 snippet'ler dahil olmak üzere tüm sayfa yüklenir. Kullanıcı sayfayla etkileşime girdiğinde (örneğin, bir düğmeye tıkladığında, bir form gönderdiğinde vb.), tüm sayfayı yüklemek yerine bir AJAX isteği tetiklenir. Presenter'daki kod eylemi gerçekleştirir ve hangi snippet'lerin güncellenmesi gerektiğine karar verir. Nette bu snippet'leri oluşturur ve JSON formatında bir dizi olarak gönderir. Tarayıcıdaki işleyici kod, alınan snippet'leri sayfaya geri ekler. Bu nedenle, yalnızca değiştirilen snippet'lerin kodu aktarılır, bu da bant genişliğinden tasarruf sağlar ve tüm sayfanın içeriğini aktarmaya kıyasla yüklemeyi hızlandırır. +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. Bunu bir node.js paketi olarak [kurun |https://naja.js.org/#/guide/01-install-setup-naja] (Webpack, Rollup, Vite, Parcel ve diğer uygulamalarla kullanım için): +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 ``` -…veya doğrudan sayfa şablonuna ekleyin: +…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: @@ -83,7 +83,7 @@ npm install naja naja.initialize(); ``` -Sıradan bir bağlantıdan (sinyal) veya form gönderiminden bir AJAX isteği oluşturmak için, ilgili bağlantıyı, formu veya düğmeyi `ajax` sınıfıyla işaretlemek yeterlidir: +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 @@ -100,32 +100,34 @@ veya ``` -Snippet'lerin Yeniden Çizilmesi +Snippet'lerin yeniden çizilmesi ------------------------------- -[Control |components] sınıfının her nesnesi (Presenter'ın kendisi dahil), yeniden çizilmesini gerektiren değişiklikler olup olmadığını takip eder. Bunun için `redrawControl()` metodu kullanılır: +[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ş yaptıktan sonra ilgili bölümü yeniden çizmek gerekir + // girişten sonra ilgili bölümün yeniden çizilmesi gerekir $this->redrawControl(); // ... } ``` -Nette, neyin yeniden çizileceği konusunda daha da hassas kontrol sağlar. Bahsedilen metot, argüman olarak snippet adını alabilir. Böylece, şablonun bölümleri düzeyinde geçersiz kılma (yani yeniden çizmeyi zorlama) mümkündür. Tüm bileşen geçersiz kılınırsa, her bir snippet'i de yeniden çizilir: +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'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'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} @@ -133,9 +135,9 @@ Latte'de snippet kullanmak son derece kolaydır. Şablonun bir bölümünü snip {/snippet} ``` -Snippet, HTML sayfasında özel olarak oluşturulmuş bir `id` ile bir `
    ` öğesi oluşturur. Snippet yeniden çizildiğinde, bu öğenin içeriği güncellenir. Bu nedenle, sayfanın ilk oluşturulmasında, başlangıçta boş olsalar bile tüm snippet'lerin de oluşturulması gerekir. +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. -n:attribute kullanarak `
    ` dışında bir öğeyle de bir snippet oluşturabilirsiniz: +Snippet'i `
    ` dışında bir elemanla, n:niteliği kullanarak da oluşturabilirsiniz: ```latte
    @@ -144,10 +146,10 @@ n:attribute kullanarak `
    ` dışında bir öğeyle de bir snippet oluşturab ``` -Snippet Alanları +Snippet alanları ---------------- -Snippet adları ifadeler de olabilir: +Snippet adları ifade de olabilir: ```latte {foreach $items as $id => $item} @@ -155,7 +157,9 @@ Snippet adları ifadeler de olabilir: {/foreach} ``` -Bu şekilde birkaç snippet oluşturulur: `item-0`, `item-1` vb. 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 kesitler gibi çalışması ve yalnızca kendilerinin doğrudan oluşturulmasıdır. Ancak şablonda aslında `item-1` adında bir snippet yoktur. Bu, yalnızca snippet'in etrafındaki kodun, yani foreach döngüsünün yürütülmesiyle ortaya çıkar. Bu nedenle, yürütülmesi gereken şablon bölümünü `{snippetArea}` etiketiyle işaretleriz: +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
      @@ -165,16 +169,16 @@ Bu şekilde birkaç snippet oluşturulur: `item-0`, `item-1` vb. Dinamik bir sni
    ``` -Ve hem snippet'in kendisini hem de tüm üst alanı yeniden çizeriz: +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 uygundur. +Aynı zamanda `$items` dizisinin yalnızca yeniden çizilmesi gereken öğeleri içermesini sağlamak yerinde olur. -Şablona `{include}` etiketi kullanarak snippet'ler içeren başka bir şablon eklersek, şablon eklemesini tekrar `snippetArea` içine almalı ve onu snippet ile birlikte geçersiz kılmalıyız: +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} @@ -195,25 +199,25 @@ $this->redrawControl('item'); ``` -Bileşenlerde Snippet'ler +Bileşenlerde snippet'ler ------------------------ -[Bileşenlerde|components] de snippet'ler oluşturabilirsiniz ve Nette bunları otomatik olarak yeniden çizer. Ancak burada belirli bir sınırlama vardır: snippet'leri yeniden çizmek için `render()` metodunu parametresiz çağırır. Yani, şablonda parametreleri iletmek işe yaramaz: +[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 -OK +Tamam {control productGrid} -işe yaramayacak: +çalışmaz: {control productGrid $arg, $arg} {control productGrid:paginator} ``` -Kullanıcı Verilerini Gönderme ------------------------------ +Kendi verilerinizi gönderme +--------------------------- -Snippet'lerle birlikte istemciye herhangi bir ek veri gönderebilirsiniz. Bunları `payload` nesnesine yazmanız yeterlidir: +Snippet'lerle birlikte istemciye istediğiniz ek veriyi gönderebilirsiniz. Onları `payload` nesnesine yazmanız yeterlidir: ```php public function actionDelete(int $id): void @@ -226,10 +230,16 @@ public function actionDelete(int $id): void ``` -Parametreleri İletme -==================== +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ği ile parametreler gönderirsek, bunlar ister sinyal parametreleri ister kalıcı parametreler olsun, istekte bileşenin adını da içeren global adlarını belirtmemiz gerekir. Parametrenin tam adını `getParameterId()` metodu döndürür. +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!}); @@ -240,10 +250,16 @@ fetch(url, { }) ``` -Ve bileşendeki karşılık gelen parametrelerle handle metodu: +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 index 6900984407..90487846bb 100644 --- a/application/tr/bootstrapping.texy +++ b/application/tr/bootstrapping.texy @@ -3,19 +3,22 @@ Bootstrapping
    -Bootstrapping, uygulama ortamının başlatılması, bir dependency injection (DI) konteynerinin oluşturulması ve uygulamanın başlatılması sürecidir. Şunları tartışacağız: +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 başlattığı -- uygulamaların NEON dosyaları kullanılarak nasıl yapılandırıldığı -- üretim ve geliştirme modları arasında nasıl ayrım yapılacağı -- DI konteynerinin nasıl oluşturulacağı ve yapılandırılacağı +- 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 uygulamaları ister komut satırından çalıştırılan betikler olsun, çalışmalarına bir tür ortam başlatma ile başlarlar. Eski zamanlarda, bu genellikle ilk dosyanın dahil ettiği `include.inc.php` gibi bir dosyanın sorumluluğundaydı. Modern Nette uygulamalarında, bunun yerini uygulamanın bir parçası olarak `app/Bootstrap.php` dosyasında bulunan `Bootstrap` sınıfı almıştır. Örneğin şöyle görünebilir: +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 @@ -26,9 +29,9 @@ class Bootstrap public function __construct() { $this->rootDir = dirname(__DIR__); - // Yapılandırıcı, uygulama ortamını ve servisleri ayarlamaktan sorumludur. + // Configurator, uygulama ortamının ve servislerin kurulumundan sorumludur. $this->configurator = new Configurator; - // Nette tarafından oluşturulan geçici dosyalar için dizini ayarlar (örn. derlenmiş şablonlar) + // Nette'in ürettiği geçici dosyalar (örneğin derlenmiş şablonlar) için dizini ayarla $this->configurator->setTempDirectory($this->rootDir . '/temp'); } @@ -41,14 +44,14 @@ class Bootstrap private function initializeEnvironment(): void { - // Nette akıllıdır ve geliştirme modu otomatik olarak açılır, - // veya aşağıdaki satırın yorumunu kaldırarak belirli bir IP adresi için etkinleştirebilirsiniz: + // 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: hata ayıklama için nihai "İsviçre çakısı". + // 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 olarak yükler + // RobotLoader: seçilen dizindeki tüm sınıfları otomatik yükler $this->configurator->createRobotLoader() ->addDirectory(__DIR__) ->register(); @@ -56,7 +59,7 @@ class Bootstrap private function setupContainer(): void { - // Yapılandırma dosyalarını yükler + // Yapılandırma dosyalarını yükle $this->configurator->addConfig($this->rootDir . '/config/common.neon'); } } @@ -66,91 +69,100 @@ class Bootstrap index.php ========= -Web uygulamaları durumunda ilk dosya, [genel dizinde |directory-structure#Genel Dizin www] `www/` bulunan `index.php` dosyasıdır. Bu dosya, Bootstrap sınıfından ortamı başlatmasını ve DI konteynerini oluşturmasını ister. Ardından, web uygulamasını başlatan `Application` servisini ondan alır: +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ı başlat + DI konteynerini oluştur +// Ortamı hazırla + DI konteynerini oluştur $container = $bootstrap->bootWebApplication(); -// DI konteyneri Nette\Application\Application nesnesini oluşturur +// 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(); ``` -Gördüğünüz gibi, ortamı ayarlamaya ve bağımlılık enjeksiyonu (DI) konteynerini oluşturmaya [api:Nette\Bootstrap\Configurator] sınıfı yardımcı olur, şimdi onu daha yakından tanıyacağız. +.[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 vs Üretim Modu -========================= -Nette, geliştirme veya üretim sunucusunda çalışıp çalışmadığına bağlı olarak farklı davranır: +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 (Development): - - Yararlı bilgilerle (SQL sorguları, yürütme süresi, kullanılan bellek) Tracy hata ayıklama çubuğunu gösterir - - Bir hata durumunda, fonksiyon çağrıları ve değişken içerikleriyle ayrıntılı bir hata sayfası gösterir - - Latte şablonları değiştiğinde, yapılandırma dosyaları düzenlendiğinde vb. önbelleği otomatik olarak yeniler +🛠️ 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 (Production): - - Hata ayıklama bilgisi göstermez, tüm hataları günlüğe yazar - - Bir hata durumunda, ErrorPresenter'ı veya genel "Server Error" sayfasını gösterir - - Önbellek asla otomatik olarak yenilenmez! - - Hız ve güvenlik için optimize edilmiştir +🚀 Ü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ılama ile yapılır, bu nedenle genellikle herhangi bir şeyi yapılandırmaya veya manuel olarak değiştirmeye gerek yoktur: +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 mevcut değilse (yani HTTP başlığı yoksa) -- üretim modu: her yerde +- 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 diğer durumlarda da etkinleştirmek istersek, örneğin belirli bir IP adresinden erişen programcılar için, `setDebugMode()` kullanırız: +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'); // IP adresleri dizisi de belirtilebilir +$this->configurator->setDebugMode('23.75.345.200'); // bir IP adresi dizisi de verilebilir ``` -Kesinlikle IP adresini bir çerezle birleştirmenizi öneririz. `nette-debug` çerezine gizli bir belirteç, örneğin `secret1234` kaydederiz ve bu şekilde belirli bir IP adresinden erişen ve aynı zamanda çerezde belirtilen belirtece sahip olan programcılar için geliştirme modunu etkinleştiririz: +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 tamamen kapatabiliriz, localhost için bile: +Geliştirme modunu, localhost için bile, tamamen kapatabiliriz de: ```php $this->configurator->setDebugMode(false); ``` -Dikkat, `true` değeri geliştirme modunu zorla açar, bu üretim sunucusunda asla olmamalıdır. +`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 +Hata ayıklama aracı Tracy ========================= -Kolay hata ayıklama için harika [Tracy |tracy:] aracını da etkinleştireceğiz. Geliştirme modunda hataları görselleştirir ve üretim modunda hataları belirtilen dizine günlüğe kaydeder: +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 +Geçici dosyalar =============== -Nette, DI konteyneri, RobotLoader, şablonlar vb. için önbellek kullanır. Bu nedenle, önbelleğin depolanacağı dizinin yolunu ayarlamak gerekir: +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/` dizinlerine [yazma izinlerini |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. +Linux veya macOS'ta `log/` ve `temp/` dizinleri için [yazma izinlerini |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. RobotLoader =========== -Genellikle [RobotLoader |robot-loader:] kullanarak sınıfları otomatik olarak yüklemek isteyeceğiz, bu yüzden onu başlatmalı ve `Bootstrap.php` dosyasının bulunduğu dizinden (yani `__DIR__`) ve tüm alt dizinlerden sınıfları yüklemesine izin vermeliyiz: +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() @@ -158,36 +170,38 @@ $this->configurator->createRobotLoader() ->register(); ``` -Alternatif bir yaklaşım, PSR-4'e uyarak sınıfları yalnızca [Composer |best-practices:composer] aracılığıyla yüklemektir. +Alternatif bir yaklaşım, sınıfları yalnızca PSR-4'e uygun olarak [Composer |best-practices:composer] üzerinden yüklemektir. -Zaman Dilimi -============ +Saat dilimi +=========== -Yapılandırıcı aracılığıyla varsayılan zaman dilimini ayarlayabilirsiniz. +Varsayılan saat dilimini configurator ile ayarlayabilirsiniz. ```php $this->configurator->setTimeZone('Europe/Prague'); ``` -DI Konteyner Yapılandırması -=========================== +DI konteynerinin yapılandırması +=============================== -Başlatma sürecinin bir parçası, tüm uygulamanın kalbi olan nesneler için bir fabrika olan DI konteynerinin oluşturulmasıdır. Aslında bu, Nette tarafından oluşturulan ve önbellek dizinine kaydedilen bir PHP sınıfıdır. Fabrika, uygulamanın temel nesnelerini üretir ve yapılandırma dosyaları aracılığıyla ona nasıl oluşturulacağını ve ayarlanacağını bildiririz, böylece tüm uygulamanın davranışını etkileriz. +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 |neon:format] formatında yazılır. Ayrı bir bölümde, [nelerin yapılandırılabileceğini |nette:configuring] öğreneceksiniz. +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, kod veya yapılandırma dosyaları her değiştiğinde konteyner otomatik olarak güncellenir. Üretim modunda, yalnızca bir kez oluşturulur ve performansı en üst düzeye çıkarmak için değişiklikler kontrol edilmez. +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. -Yapılandırma dosyalarını `addConfig()` kullanarak yükleriz: +`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 istiyorsak, `addConfig()` fonksiyonunu birden çok kez çağırabiliriz. +Daha fazla yapılandırma dosyası eklemek istersek, `addConfig()` fonksiyonunu birden çok kez çağırabiliriz. ```php $configDir = $this->rootDir . '/config'; @@ -198,17 +212,17 @@ if (PHP_SAPI === 'cli') { } ``` -`cli.php` adı bir yazım hatası değildir, yapılandırma bir dizi olarak döndüren bir PHP dosyasında da yazılabilir. +`cli.php` adı yazım hatası değildir; yapılandırma, onu dizi olarak döndüren bir PHP dosyasında da yazılabilir. -Ayrıca [`includes` bölümünde |dependency-injection:configuration#Dosya Dahil Etme] başka yapılandırma dosyaları da ekleyebiliriz. +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 görünürse, bunlar üzerine yazılır veya [diziler durumunda birleştirilir |dependency-injection:configuration#Birleştirme]. Daha sonra eklenen dosya, öncekinden daha yüksek önceliğe sahiptir. `includes` bölümünün belirtildiği dosya, içine dahil edilen dosyalardan daha yüksek önceliğe sahiptir. +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 +Statik parametreler ------------------- -Yapılandırma dosyalarında kullanılan parametreleri [`parameters` bölümünde |dependency-injection:configuration#Parametreler] tanımlayabilir ve ayrıca `addStaticParameters()` metoduyla (diğer adı `addParameters()`) iletebilir (veya üzerine yazabiliriz). Önemli olan, farklı parametre değerlerinin ek DI konteynerlerinin, yani ek sınıfların oluşturulmasına neden olmasıdır. +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([ @@ -216,13 +230,13 @@ $this->configurator->addStaticParameters([ ]); ``` -`projectId` parametresine yapılandırmada normal `%projectId%` gösterimiyle başvurulabilir. +`projectId` parametresine yapılandırmada standart `%projectId%` gösterimiyle başvurulabilir. -Dinamik Parametreler +Dinamik parametreler -------------------- -Konteynere dinamik parametreler de ekleyebiliriz; bunların farklı değerleri, statik parametrelerin aksine, yeni DI konteynerlerinin oluşturulmasına neden olmaz. +Konteynere dinamik parametreler de ekleyebiliriz; bunların farklı değerleri, statik parametrelerin aksine, yeni DI konteynerlerinin üretilmesine yol açmaz. ```php $this->configurator->addDynamicParameters([ @@ -230,7 +244,7 @@ $this->configurator->addDynamicParameters([ ]); ``` -Bu şekilde, örneğin ortam değişkenlerini kolayca ekleyebiliriz, bunlara daha sonra yapılandırmada `%env.variable%` gösterimiyle başvurulabilir. +Böylece örneğin ortam değişkenlerini kolayca ekleyebiliriz; onlara yapılandırmada `%env.degisken%` gösterimiyle başvurulabilir. ```php $this->configurator->addDynamicParameters([ @@ -239,24 +253,25 @@ $this->configurator->addDynamicParameters([ ``` -Varsayılan Parametreler +Varsayılan parametreler ----------------------- -Yapılandırma dosyalarında şu statik parametreleri kullanabilirsiniz: +Yapılandırma dosyalarında şu parametreleri kullanabilirsiniz: -- `%appDir%`, `Bootstrap.php` dosyasını içeren dizine mutlak yoldur -- `%wwwDir%`, giriş dosyası `index.php` dosyasını içeren dizine mutlak yoldur -- `%tempDir%`, geçici dosyalar için dizine mutlak yoldur -- `%vendorDir%`, Composer'ın kütüphaneleri kurduğu dizine mutlak yoldur -- `%rootDir%`, projenin kök dizinine mutlak yoldur +- `%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 +İçe aktarılan servisler ----------------------- -Şimdi daha derine iniyoruz. DI konteynerinin amacı nesneleri üretmek olsa da, istisnai olarak mevcut bir nesneyi konteynere ekleme ihtiyacı doğabilir. Bunu, servisi `imported: true` bayrağıyla tanımlayarak yaparız. +Ş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: @@ -265,7 +280,7 @@ services: imported: true ``` -Ve bootstrap'ta nesneyi konteynere ekleriz: +Ve bootstrap'ta nesneyi konteynere yerleştiririz: ```php $this->configurator->addServices([ @@ -274,15 +289,15 @@ $this->configurator->addServices([ ``` -Farklı Ortam -============ +Farklı ortamlar +=============== -Bootstrap sınıfını ihtiyaçlarınıza göre değiştirmekten çekinmeyin. Web projelerini ayırt etmek için `bootWebApplication()` metoduna parametreler ekleyebilirsiniz. Veya birim testleri için ortamı başlatan `bootTestEnvironment()`, komut satırından çağrılan betikler için `bootConsoleApplication()` gibi başka metotlar ekleyebiliriz. +`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'ı başlat + Tester\Environment::setup(); // Nette Tester'ın başlatılması $this->setupContainer(); return $this->configurator->createContainer(); } diff --git a/application/tr/components.texy b/application/tr/components.texy index 68661abfc4..2d08ea1a4d 100644 --- a/application/tr/components.texy +++ b/application/tr/components.texy @@ -1,9 +1,9 @@ -Etkileşimli Bileşenler +Etkileşimli bileşenler **********************
    -Bileşenler, sayfalara eklediğimiz bağımsız, yeniden kullanılabilir nesnelerdir. Bunlar formlar, veri ızgaraları, anketler, aslında tekrar tekrar kullanılması mantıklı olan her şey olabilir. Şunları göstereceğiz: +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? @@ -11,19 +11,19 @@ Bileşenler, sayfalara eklediğimiz bağımsız, yeniden kullanılabilir nesnele
    -Nette'nin yerleşik bir bileşen sistemi vardır. Delphi veya ASP.NET Web Forms'tan aşina olanlar benzer bir şey hatırlayabilir, React veya Vue.js de uzaktan benzer bir şeye dayanmaktadır. Ancak, PHP framework dünyasında bu benzersiz bir özelliktir. +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. -Bununla birlikte, bileşenler uygulama geliştirme yaklaşımını temelden etkiler. Sayfaları önceden hazırlanmış birimlerden oluşturabilirsiniz. Yönetimde bir veri ızgarasına mı ihtiyacınız var? Onu Nette için açık kaynaklı eklentilerin (yani sadece bileşenlerin değil) deposu olan [Componette |https://componette.org/search/component] adresinde bulabilir ve presenter'a kolayca ekleyebilirsiniz. +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 ekleyebilirsiniz. Ve bazı bileşenlere başka bileşenler ekleyebilirsiniz. Bu, kökü presenter olan bir bileşen ağacı oluşturur. +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. -Fabrika Metotları +Factory metotları ================= -Bileşenler presenter'a nasıl eklenir ve ardından kullanılır? Genellikle fabrika metotları aracılığıyla. +Bileşenler presenter'a nasıl yerleştirilir ve sonra nasıl kullanılır? Genellikle factory metotlarıyla. -Bileşen fabrikası, bileşenleri yalnızca gerçekten ihtiyaç duyulduğunda (lazy / on demand) oluşturmanın zarif bir yoludur. Tüm sihir, `` öğesinin oluşturulan bileşenin adı olduğu ve bileşeni oluşturup döndüren `createComponent()` adlı bir metodun uygulanmasında yatar. +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 @@ -31,49 +31,54 @@ class DefaultPresenter extends Nette\Application\UI\Presenter protected function createComponentPoll(): PollControl { $poll = new PollControl; - $poll->items = $this->item; + $poll->items = $this->items; return $poll; } } ``` -Tüm bileşenlerin ayrı metotlarda oluşturulması sayesinde kod daha okunaklı hale gelir. +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ılsa bile her zaman küçük harfle başlar. +Bileşen adları, metot adında büyük harfle yazılsalar da, her zaman küçük harfle başlar. -Fabrikaları asla doğrudan çağırmayız, bileşeni ilk kullandığımızda kendiliğinden çağrılırlar. Bu sayede bileşen doğru zamanda ve yalnızca gerçekten ihtiyaç duyulduğunda oluşturulur. Bileşeni kullanmazsak (örneğin, sayfanın yalnızca bir kısmının aktarıldığı bir AJAX isteğinde veya şablon önbelleğe alınırken), hiç oluşturulmaz ve sunucu performansından tasarruf ederiz. +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 eğer ilk kez ise, +// 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şeni [{control} |#Oluşturma] etiketi kullanarak oluşturmak mümkündür. Bu nedenle bileşenleri şablona manuel olarak iletmeye gerek yoktur. +Şablonda bir bileşen [{control} |#Render] etiketiyle render edilebilir. Bu yüzden bileşenleri şablona elle aktarmaya gerek yoktur. ```latte -

    Oy Verin

    +

    Lütfen oy verin

    {control poll} ``` +.[tip] +Sayısı değişken bileşenleri dinamik olarak oluşturmak için [Multiplier |multiplier] kullanın. -Hollywood Tarzı +`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ı dediğimiz yeni bir tekniği kullanır. Film seçmelerine katılanların sıkça duyduğu şu meşhur cümleyi mutlaka bilirsiniz: "Bizi aramayın, biz sizi ararız." İşte tam olarak bundan bahsediyoruz. +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'de, sürekli bir şeyler sormak yerine ("form gönderildi mi?", "geçerli miydi?" veya "kullanıcı bu düğmeye bastı mı?"), framework'e "bu olduğunda, şu metodu çağır" dersiniz ve geri kalan işi ona bırakırsınız. JavaScript ile programlama yapıyorsanız, bu programlama tarzına aşinasınızdır. Belirli bir olay gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara ilgili parametreleri iletir. +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 yazma şeklini tamamen değiştirir. Framework'e ne kadar çok görev bırakabilirseniz, o kadar az işiniz olur. Ve dolayısıyla gözden kaçırabileceğiniz şeyler de o kadar azalı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 yazma ============= -Bileşen terimiyle genellikle [api:Nette\Application\UI\Control] sınıfının bir alt sınıfını kastederiz. (Dolayısıyla "kontroller" terimini kullanmak daha doğru olurdu, ancak "kontroller" Türkçede tamamen farklı bir anlama sahiptir ve "bileşenler" daha çok yerleşmiştir.) Bu arada, presenter [api:Nette\Application\UI\Presenter] kendisi de `Control` sınıfının bir alt sınıfıdır. +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; @@ -84,22 +89,22 @@ class PollControl extends Control ``` -Oluşturma -========= +Render +====== -Biliyoruz ki bir bileşeni oluşturmak için `{control componentName}` etiketi kullanılır. Bu aslında bileşenin `render()` metodunu çağırır ve burada oluşturmayı biz hallederiz. Tıpkı presenter'da olduğu gibi, `$this->template` değişkeninde [Latte şablonuna|templates] sahibiz ve ona parametreleri iletiriz. Presenter'dan farklı olarak, şablon dosyasını belirtmeli ve oluşturulmasını sağlamalıyız: +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 ekleriz + // şablona bazı parametreler ekle $this->template->param = $value; - // ve onu oluştururuz + // ve render et $this->template->render(__DIR__ . '/poll.latte'); } ``` -`{control}` etiketi, `render()` metoduna parametreler iletmeyi sağlar: +`{control}` etiketi, `render()` metoduna parametre aktarmayı sağlar: ```latte {control poll $id, $message} @@ -112,7 +117,7 @@ public function render(int $id, string $message): void } ``` -Bazen bir bileşen, ayrı ayrı oluşturmak istediğimiz birkaç bölümden oluşabilir. Her biri için kendi oluşturma metodumuzu oluştururuz, buradaki örnekte örneğin `renderPaginator()`: +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 @@ -121,69 +126,69 @@ public function renderPaginator(): void } ``` -Ve şablonda onu şu şekilde çağırırız: +Şablonda ise onu şöyle çağırırız: ```latte {control poll:paginator} ``` -Daha iyi anlamak için, bu etiketin PHP'ye nasıl çevrildiğini bilmek iyidir. +Daha iyi anlamak için bu etiketin PHP koduna nasıl çevrildiğini bilmek iyidir. ```latte {control poll} {control poll:paginator 123, 'hello'} ``` -şu şekilde çevrilir: +ş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()` metodunu veya etikette iki noktadan sonra farklı bir oluşturma yöntemi belirtilmişse `renderPaginator()` metodunu çağırır. +`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, parametrelerin herhangi bir yerinde **`=>`** görünürse, tüm parametreler bir diziye paketlenir ve ilk argüman olarak iletilir: +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'} ``` -şu şekilde çevrilir: +şuna çevrilir: ```php $control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); ``` -Alt bileşenin oluşturulması: +Bir alt bileşenin render edilmesi: ```latte {control cartControl-someForm} ``` -şu şekilde çevrilir: +şuna çevrilir: ```php $control->getComponent("cartControl-someForm")->render(); ``` -Bileşenler, presenter'lar gibi, şablonlara otomatik olarak birkaç yararlı değişken iletir: +Bileşenler, presenter'lar gibi, şablonlara birkaç yararlı değişkeni otomatik aktarır: -- `$basePath`, kök dizine mutlak URL yoludur (örn. `/eshop`) -- `$baseUrl`, kök dizine mutlak URL'dir (örn. `http://localhost/eshop`) -- `$user`, [kullanıcıyı temsil eden |security:authentication] nesnedir -- `$presenter`, mevcut presenter'dır -- `$control`, mevcut bileşendir -- `$flashes`, `flashMessage()` fonksiyonu tarafından gönderilen [mesajlar |#Flash Mesajları] dizisidir +- `$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 ====== -Nette uygulamasında gezinmenin `Presenter:action` çiftlerine bağlantı verme veya yönlendirme yapmaktan ibaret olduğunu zaten biliyoruz. Peki ya sadece **mevcut 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 form göndermek; bir ankette oy kullanmak; vb. +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 isteklere sinyal denir. Ve eylemlerin `action()` veya `render()` metotlarını tetiklemesi gibi, sinyaller de `handle()` metotlarını çağırır. Eylem (veya view) kavramı yalnızca presenter'larla ilgiliyken, sinyaller tüm bileşenlerle ilgilidir. Ve dolayısıyla presenter'larla da, çünkü `UI\Presenter`, `UI\Control`'un bir alt sınıfıdır. +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 @@ -192,36 +197,36 @@ public function handleClick(int $x, int $y): void } ``` -Sinyali çağıran bir bağlantıyı normal şekilde oluştururuz, yani şablonda `n:href` niteliğiyle veya `{link}` etiketiyle, kodda `link()` metoduyla. Daha fazla bilgi için [URL Bağlantıları Oluşturma |creating-links#Sinyale Bağlantılar] bölümüne bakın. +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 mevcut presenter ve eylem üzerinde çağrılır, başka bir presenter veya başka bir eylem üzerinde çağrılamaz. +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. -Dolayısıyla sinyal, sayfanın orijinal istekteki gibi tamamen yeniden yüklenmesine neden olur, ancak ek olarak ilgili parametrelerle sinyal işleyici metodunu çağırır. Metot mevcut değilse, kullanıcıya 403 Forbidden hata sayfası olarak gösterilen [api:Nette\Application\UI\BadSignalException] istisnası atılır. +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: mevcut sayfada çağrılan işleyiciler. Ve haklısınız, sinyaller gerçekten de sık sık AJAX kullanılarak çağrılır ve ardından sayfanın yalnızca değiştirilmiş bölümleri tarayıcıya aktarılır. Yani sözde snippet'ler. Daha fazla bilgi için [AJAX'a ayrılmış sayfada |ajax] bulabilirsiniz. +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ı +Flash mesajları =============== -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 bile şablonda kullanılabilir olmalarıdır. Görüntülendikten sonra bile 30 saniye daha canlı kalırlar - örneğin, hatalı bir aktarım nedeniyle kullanıcının sayfayı yenilemesi durumunda mesaj hemen kaybolmaz. +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önderme işlemi [flashMessage |api:Nette\Application\UI\Control::flashMessage()] metodu tarafından gerçekleştirilir. İlk parametre mesaj metni veya mesajı temsil eden bir `stdClass` nesnesidir. İsteğe bağlı ikinci parametre türüdür (error, warning, info vb.). `flashMessage()` metodu, flash mesajının bir örneğini `stdClass` nesnesi olarak döndürür ve buna ek bilgiler eklenebilir. +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önlendiririz +$this->redirect(/* ... */); // ve yönlendir ``` -Şablonda bu mesajlar `$flashes` değişkeninde `stdClass` nesneleri olarak bulunur ve `message` (mesaj metni), `type` (mesaj türü) özelliklerini içerir ve daha önce bahsedilen kullanıcı bilgilerini içerebilir. Onları örneğin şu şekilde oluştururuz: +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} @@ -230,39 +235,39 @@ $this->redirect(/* ... */); // ve yönlendiririz ``` -Sinyal Sonrası Yönlendirme -========================== +Sinyalin işlenmesinden sonra yönlendirme +======================================== -Bileşen sinyallerinin işlenmesinden sonra genellikle bir yönlendirme takip eder. Bu, formlardaki duruma benzer - gönderildikten sonra da yönlendiririz, böylece tarayıcıda sayfa yenilendiğinde veriler tekrar gönderilmez. +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') // mevcut presenter ve eyleme yönlendirir +$this->redirect('this'); // geçerli presenter ve eyleme yönlendirir ``` -Bileşen yeniden kullanılabilir bir öğe olduğundan ve genellikle belirli presenter'larla doğrudan bir bağlantısı olmaması gerektiğinden, `redirect()` ve `link()` metotları parametreyi otomatik olarak bileşen sinyali olarak yorumlar: +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 +$this->redirect('click'); // aynı bileşenin 'click' sinyaline yönlendirir ``` -Başka bir presenter'a veya eyleme yönlendirmeniz gerekiyorsa, bunu presenter aracılığıyla yapabilirsiniz: +Başka bir presenter'a veya eyleme yönlendirmeniz gerekirse bunu presenter üzerinden yapabilirsiniz: ```php -$this->getPresenter()->redirect('Product:show'); // başka bir presenter/eyleme yönlendirir +$this->getPresenter()->redirect('Product:show'); // başka bir presenter'a/eyleme yönlendirir ``` -Kalıcı Parametreler +Kalıcı parametreler =================== -Kalıcı parametreler, farklı istekler arasında bileşenlerde durumu korumak için kullanılır. Değerleri, bir bağlantıya tıklandıktan sonra bile aynı kalır. Oturumdaki verilerin aksine, URL'de aktarılırlar. Ve bu tamamen otomatiktir, aynı sayfadaki diğer bileşenlerde oluşturulan bağlantılar dahil. +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 sayfalandırmak için bir bileşeniniz var. Sayfada bu tür birkaç bileşen olabilir. Ve bir bağlantıya tıklandığında tüm bileşenlerin mevcut sayfalarında kalmasını istiyoruz. Bu nedenle, sayfa numarasını (`page`) kalıcı bir parametre yaparız. +Ö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'de kalıcı bir parametre oluşturmak son derece basittir. Sadece genel bir özellik oluşturmanız ve onu bir nitelikle işaretlemeniz yeterlidir: (eskiden `/** @persistent */` kullanılırdı) +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 önemlidir +use Nette\Application\Attributes\Persistent; // bu satır önemli class PaginatingControl extends Control { @@ -271,61 +276,61 @@ class PaginatingControl extends Control } ``` -Özellik için veri türünü (örn. `int`) belirtmenizi öneririz ve varsayılan bir değer de belirtebilirsiniz. Parametre değerleri [doğrulanabilir |#Kalıcı Parametrelerin Doğrulanması]. +Ö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ı]. -Bir bağlantı oluştururken, kalıcı parametrenin değeri değiştirilebilir: +Bağlantı oluştururken kalıcı parametrenin değeri değiştirilebilir: ```latte sonraki ``` -Veya *sıfırlanabilir*, yani URL'den kaldırılabilir. O zaman varsayılan değerini alacaktır: +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 +Kalıcı bileşenler ================= -Sadece parametreler değil, bileşenler de kalıcı olabilir. Böyle bir bileşenin kalıcı parametreleri, presenter'ın farklı eylemleri arasında veya birden fazla presenter arasında da aktarılır. Kalıcı bileşenleri presenter sınıfındaki bir ek açıklama ile işaretleriz. Örneğin, `calendar` ve `poll` bileşenlerini şu şekilde işaretleriz: +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 -/** - * @persistent(calendar, poll) - */ +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ı hale gelirler. +Bu bileşenlerin içindeki alt bileşenleri işaretlemeye gerek yoktur; onlar da kalıcı olur. -PHP 8'de, kalıcı bileşenleri işaretlemek için nitelikleri de kullanabilirsiniz: +Eski `@persistent` anotasyonu hâlâ çalışır, ama kullanımdan kaldırılmıştır ve bir uyarı tetikler: ```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] +/** + * @persistent(calendar, poll) + */ class DefaultPresenter extends Nette\Application\UI\Presenter { } ``` -Bağımlılıklara Sahip Bileşenler -=============================== +Bağımlılıkları olan bileşenler +============================== -Bağımlılıklara sahip bileşenleri, onları kullanacak presenter'ları "kirletmeden" nasıl oluşturabiliriz? Nette'deki DI konteynerinin akıllı özellikleri sayesinde, klasik servisleri kullanırken olduğu gibi, işin çoğunu framework'e bırakabiliriz. +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. -Örnek olarak, `PollFacade` servisine bağımlılığı olan bir bileşeni ele alalım: +`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 kimliği + private int $id, // bileşeni oluşturduğumuz anketin ID'si private PollFacade $facade, ) { } @@ -338,11 +343,11 @@ class PollControl extends Control } ``` -Klasik bir servis yazıyor olsaydık, çözülecek bir şey olmazdı. Tüm bağımlılıkların iletilmesi DI konteyneri tarafından görünmez bir şekilde halledilirdi. Ancak bileşenlerle genellikle, yeni örneklerini doğrudan presenter'da [fabrika metotlarında |#Fabrika Metotları] `createComponent…()` oluşturacak şekilde çalışırız. Ancak tüm bileşenlerin tüm bağımlılıklarını presenter'a iletmek, sonra onları bileşenlere iletmek hantaldır. Ve yazılan kod miktarı… +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ıksal soru şudur: neden bileşeni basitçe klasik bir servis olarak kaydetmiyor, presenter'a iletmiyor ve sonra `createComponent…()` metodunda döndürmüyoruz? Ancak bu yaklaşım uygunsuzdur, çünkü bileşeni birden çok kez oluşturabilmek istiyoruz. +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 fabrika yazmaktır, yani bize bileşeni oluşturacak bir sınıf: +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 @@ -359,14 +364,14 @@ class PollControlFactory } ``` -Bu fabrikayı yapılandırmamızda konteynerimize kaydederiz: +Bu factory'yi yapılandırmada konteynerimize kaydederiz: ```neon services: - PollControlFactory ``` -ve son olarak onu presenter'ımızda kullanırız: +ve son olarak presenter'ımızda kullanırız: ```php class PollPresenter extends Nette\Application\UI\Presenter @@ -378,13 +383,13 @@ class PollPresenter extends Nette\Application\UI\Presenter protected function createComponentPollControl(): PollControl { - $pollId = 1; // parametremizi iletebiliriz + $pollId = 1; // kendi parametremizi aktarabiliriz return $this->pollControlFactory->create($pollId); } } ``` -Harika olan şey, Nette DI'nin bu tür basit fabrikaları [oluşturabilmesidir |dependency-injection:factory], bu yüzden tüm kodunu yazmak yerine sadece arayüzünü yazmak yeterlidir: +İş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 @@ -393,21 +398,21 @@ interface PollControlFactory } ``` -Ve hepsi bu. Nette bu arayüzü dahili olarak uygular ve presenter'a iletir, burada onu zaten kullanabiliriz. Sihirli bir şekilde `$id` parametresini ve `PollFacade` sınıfının bir örneğini bileşenimize ekler. +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 +Bileşenler derinlemesine ======================== -Nette Application'daki bileşenler, web uygulamasının yeniden kullanılabilir parçalarıdır, bunları sayfalara ekleriz ve bu bölümün tamamı onlara ayrılmıştır. Böyle bir bileşenin tam olarak hangi yetenekleri vardır? +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) şablonda oluşturulabilir -2) AJAX isteğinde [hangi bölümünün |ajax#Snippet ler] oluşturulacağını bilir (snippet'ler) -3) durumunu URL'de saklama yeteneğine sahiptir (kalıcı parametreler) -4) kullanıcı eylemlerine yanıt verme yeteneğine sahiptir (sinyaller) -5) hiyerarşik bir yapı oluşturur (kökü presenter'dır) +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 fonksiyonların her biri kalıtım çizgisindeki sınıflardan biri tarafından sağlanır. Oluşturma (1 + 2) [api:Nette\Application\UI\Control] tarafından, [yaşam döngüsüne |presenters#Presenter Yaşam Döngüsü] dahil etme (3, 4) [api:Nette\Application\UI\Component] sınıfı tarafından ve hiyerarşik yapı oluşturma (5) [Container ve Component |component-model:] sınıfları tarafından halledilir. +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 } @@ -422,18 +427,18 @@ Nette\ComponentModel\Component { IComponent } ``` -Bileşenin Yaşam Döngüsü +Bileşenin yaşam döngüsü ----------------------- [* lifecycle-component.svg *] *** *Bileşenin yaşam döngüsü* .<> -Kalıcı Parametrelerin Doğrulanması +Kalıcı parametrelerin doğrulanması ---------------------------------- -URL'den alınan [kalıcı parametrelerin |#Kalıcı Parametreler] değerleri `loadState()` metodu tarafından özelliklere yazılır. Bu metot ayrıca özellikte belirtilen veri türünün eşleşip eşleşmediğini de kontrol eder, aksi takdirde 404 hatasıyla yanıt verir ve sayfa görüntülenmez. +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ı tarafından URL'de kolayca üzerine yazılabilirler. Örneğin, `$this->page` sayfa numarasının 0'dan büyük olup olmadığını bu şekilde doğrularız. Uygun yol, bahsedilen `loadState()` metodunu geçersiz kılmaktır: +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 @@ -443,8 +448,8 @@ class PaginatingControl extends Control public function loadState(array $params): void { - parent::loadState($params); // burada $this->page ayarlanır - // ardından kendi değer kontrolümüz gelir: + parent::loadState($params); // $this->page burada ayarlanır + // ardından kendi değer denetimimiz gelir: if ($this->page < 1) { $this->error(); } @@ -452,27 +457,41 @@ class PaginatingControl extends Control } ``` -Ters işlem, yani kalıcı özelliklerden değerlerin toplanması, `saveState()` metodunun sorumluluğundadır. +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. -Sinyaller Derinlemesine +```php +$control->onAnchor[] = function ($control): void { + // bileşenin artık presenter'ı elinin altında +}; +``` + + +Sinyaller derinlemesine ----------------------- -Bir sinyal, sayfanın orijinal istekteki gibi tamamen yeniden yüklenmesine neden olur (AJAX ile çağrıldığı durumlar hariç) ve `signalReceived($signal)` metodunu çağırır; bu metodun `Nette\Application\UI\Component` sınıfındaki varsayılan uygulaması, `handle{signal}` kelimelerinden oluşan bir metodu çağırmaya çalışır. Daha sonraki işlemler ilgili nesneye bağlıdır. `Component`'ten (yani `Control` ve `Presenter`) miras alan nesneler, ilgili parametrelerle `handle{signal}` metodunu çağırmaya çalışarak yanıt verirler. +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. -Başka bir deyişle: `handle{signal}` fonksiyonunun tanımı alınır ve istekle birlikte gelen tüm parametreler alınır ve argümanlara URL'den parametreler ada göre atanır ve ilgili metot çağrılmaya çalışılır. Örneğin, `$id` parametresi olarak URL'deki `id` parametresinin değeri, `$something` olarak URL'deki `something` vb. iletilir. Ve metot mevcut değilse, `signalReceived` metodu [bir istisna |api:Nette\Application\UI\BadSignalException] atar. +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ı]. -Sinyal, `SignalReceiver` arayüzünü uygulayan ve bileşen ağacına bağlı olan herhangi bir bileşen, presenter veya nesne tarafından alınabilir. +Bir sinyali, `SignalReceiver` arayüzünü uygulayan ve bileşen ağacına bağlı olan her bileşen, presenter veya nesne alabilir. -Sinyallerin ana alıcıları `Presenter`'lar ve `Control`'dan miras alan görsel bileşenler olacaktır. Sinyal, nesneye bir şey yapması gerektiğine dair bir işaret görevi görmelidir - anket kullanıcının oyunu saymalı, haber bloğu genişlemeli ve iki kat daha fazla haber göstermeli, form gönderildi ve verileri işlemeli vb. +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. -Sinyal için URL, [Component::link() |api:Nette\Application\UI\Component::link()] metodu kullanılarak oluşturulur. `$destination` parametresi olarak `{signal}!` dizesini ve `$args` olarak sinyale iletmek istediğimiz argümanlar dizisini iletiriz. Sinyal her zaman mevcut presenter ve eylem üzerinde mevcut parametrelerle çağrılır, sinyal parametreleri yalnızca eklenir. Ek olarak, en başta **sinyali belirten `?do` parametresi** eklenir. +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. -Formatı ya `{signal}` ya da `{signalReceiver}-{signal}` şeklindedir. `{signalReceiver}`, presenter'daki bileşenin adıdır. Bu nedenle, bileşen adında tire olamaz - bileşen adını ve sinyali ayırmak için kullanılır, ancak bu şekilde birkaç bileşeni iç içe geçirmek mümkündür. +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ı doğrular. İkinci argümanı atlayabiliriz - o zaman bileşenin herhangi bir sinyalin alıcısı olup olmadığını kontrol eder. İkinci parametre olarak `true` belirtilebilir ve böylece yalnızca belirtilen bileşenin değil, aynı zamanda herhangi bir alt öğesinin de alıcı olup olmadığını doğrular. +[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{signal}`'den önceki herhangi bir aşamada, sinyali manuel olarak [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] metodunu çağırarak yürütebiliriz; bu metot sinyalin işlenmesini üstlenir - sinyalin alıcısı olarak belirlenen bileşeni alır (sinyal alıcısı belirtilmemişse, presenter'ın kendisidir) ve ona sinyali gönderir. +`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: @@ -482,4 +501,4 @@ if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, ' } ``` -Böylece sinyal erken yürütülür ve tekrar çağrılmaz. +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 index c933edfdd1..31ccd0b872 100644 --- a/application/tr/configuration.texy +++ b/application/tr/configuration.texy @@ -1,8 +1,8 @@ -Uygulama Yapılandırması +Uygulama yapılandırması *********************** .[perex] -Nette Uygulamaları için yapılandırma seçeneklerine genel bakış. +Nette Application'ın yapılandırma seçeneklerine genel bakış. Application @@ -10,62 +10,64 @@ Application ```neon application: - # Tracy BlueScreen'de "Nette Application" panelini göster? - debugger: ... # (bool) varsayılan true + # Tracy BlueScreen'de "Nette Application" paneli gösterilsin mi? + debugger: ... # (bool) Tracy varsa etkin - # hata durumunda error-presenter çağrılacak mı? - # yalnızca geliştirme modunda etkilidir - catchExceptions: ... # (bool) varsayılan true + # ü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 adı + # error-presenter'ın adı errorPresenter: Error # (string|array) varsayılan 'Nette:Error' - # presenter'lar ve eylemler için takma adları tanımlar + # presenter'lar ve eylemler için takma adlar tanımlar aliases: ... - # presenter adını sınıfa çevirme kurallarını tanımlar + # presenter adının sınıfa çevrilme kurallarını tanımlar mapping: ... - # hatalı bağlantılar uyarı oluşturmaz mı? + # geçersiz bağlantı uyarıları bastırılsın mı? # yalnızca geliştirme modunda etkilidir silentLinks: ... # (bool) varsayılan false ``` -`nette/application` sürüm 3.2'den itibaren bir çift error-presenter tanımlanabilir: +`nette/application` 3.2 sürümünden itibaren bir çift error presenter tanımlanabilir: ```neon application: errorPresenter: - 4xx: Error4xx # Nette\Application\BadRequestException istisnası için + 4xx: Error4xx # Nette\Application\BadRequestException için 5xx: Error5xx # diğer istisnalar için ``` -`silentLinks` seçeneği, Nette'nin geliştirme modunda bir bağlantı oluşturma başarısız olduğunda (örneğin, presenter mevcut olmadığı için vb.) nasıl davranacağını belirler. Varsayılan `false` değeri, Nette'nin bir `E_USER_WARNING` hatası atacağı anlamına gelir. `true` olarak ayarlamak bu hata mesajını bastırır. Üretim ortamında `E_USER_WARNING` her zaman tetiklenir. Bu davranışı, presenter değişkeni [$invalidLinkMode |creating-links#Geçersiz Bağlantılar] ayarlayarak da etkileyebiliriz. +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 presenter'lara bağlantı vermeyi basitleştirir |creating-links#Takma Adlar Alias]. +[Takma adlar, sık kullanılan |creating-links#Takma adlar] presenter'lara başvurmayı kolaylaştırır. -[Eşleme, presenter adından sınıf adının nasıl türetileceğine ilişkin kuralları tanımlar |directory-structure#Presenter Eşlemesi]. +[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ı +Presenter'ların otomatik kaydı ------------------------------ -Nette, presenter'ları otomatik olarak DI konteynerine servis olarak ekler, bu da oluşturulmalarını önemli ölçüde hızlandırır. Nette'nin presenter'ları nasıl bulduğu yapılandırılabilir: +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: - # Composer sınıf haritasında presenter'ları ara? + # 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 aranacak? + # presenter'lar hangi dizinlerde aransın? scanDirs: # (string[]|false) varsayılan '%appDir%' - %vendorDir%/mymodule ``` -`scanDirs` içinde belirtilen dizinler, varsayılan `%appDir%` değerinin üzerine yazmaz, ancak onu tamamlar, bu nedenle `scanDirs` hem `%appDir%` hem de `%vendorDir%/mymodule` yollarını içerecektir. Varsayılan dizini atlamak istiyorsak, değeri üzerine yazan [ünlem işaretini |dependency-injection:configuration#Birleştirme] kullanırız: +`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: @@ -73,36 +75,42 @@ application: - %vendorDir%/mymodule ``` -Dizin taraması, false değeri belirtilerek kapatılabilir. Presenter'ların otomatik olarak eklenmesini tamamen bastırmanızı önermiyoruz, çünkü aksi takdirde uygulama performansı düşer. +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ı +Latte şablonları ================ -Bu ayar, Latte'nin bileşenlerdeki ve presenter'lardaki davranışını genel olarak etkilemenizi sağlar. +Bu ayar, Latte'nin bileşenlerdeki ve presenter'lardaki davranışını genel olarak etkiler. ```neon latte: - # Ana şablon için (true) veya tüm bileşenler için (all) Tracy Bar'da Latte panelini göster? - debugger: ... # (true|false|'all') varsayılan true + # 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) - # declare(strict_types=1) başlığıyla şablonlar oluşturur + # şablonları declare(strict_types=1) başlığıyla üret strictTypes: ... # (bool) varsayılan false - # [katı ayrıştırıcı |latte:develop#striktní režim] modunu açar + # [katı ayrıştırıcı modunu |latte:develop#strict mode] etkinleştir strictParsing: ... # (bool) varsayılan false - # [oluşturulan kodun kontrolünü |latte:develop#Kontrola vygenerovaného kódu] etkinleştirir + # 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ı ayarlar + # 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 ``` -Latte sürüm 3 kullanıyorsanız, yeni [uzantıları |latte:extending-latte#Latte Extension] şu şekilde ekleyebilirsiniz: +Yeni [uzantıları |latte:extending-latte#Latte Extension] şöyle ekleyebilirsiniz: ```neon latte: @@ -110,20 +118,6 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Latte sürüm 2 kullanıyorsanız, yeni etiketleri sınıf adını belirterek veya bir servise referans vererek kaydedebilirsiniz. Varsayılan olarak `install()` metodu çağrılır, ancak bu, başka bir metodun adını belirterek değiştirilebilir: - -```neon -latte: - # özel Latte etiketlerini kaydet - macros: - - App\MyLatteMacros::register # statik metot, sınıf adı veya çağrılabilir - - @App\MyLatteMacrosFactory # install() metoduna sahip servis - - @App\MyLatteMacrosFactory::register # register() metoduna sahip servis - -services: - - App\MyLatteMacrosFactory -``` - Yönlendirme =========== @@ -132,14 +126,14 @@ Temel ayarlar: ```neon routing: - # Tracy Bar'da yönlendirme panelini göster? - debugger: ... # (bool) varsayılan true + # Tracy Bar'da yönlendirme paneli gösterilsin mi? + debugger: ... # (bool) Tracy varsa etkin (yalnızca hata ayıklama modunda) - # yönlendiriciyi DI konteynerine serileştirir + # 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` çiftleri kullanılarak da tanımlanabilir, ancak bu yöntem ayar konusunda o kadar geniş bir çeşitlilik sunmaz: +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: @@ -152,23 +146,23 @@ routing: Sabitler ======== -PHP sabitleri oluşturma. +PHP sabitlerinin oluşturulması. ```neon constants: Foobar: 'baz' ``` -Uygulama başlatıldıktan sonra `Foobar` sabiti oluşturulacaktır. +`Foobar` sabiti uygulama başladıktan sonra oluşturulur. .[note] -Sabitler, genel olarak erişilebilir değişkenler gibi kullanılmamalıdır. Nesnelere değer iletmek için [bağımlılık enjeksiyonunu |dependency-injection:passing-dependencies] kullanın. +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 yönergelerinin ayarlanması. Tüm yönergelerin bir listesini [php.net |https://www.php.net/manual/en/ini.list.php] adresinde bulabilirsiniz. +PHP direktiflerinin ayarlanması. Tüm direktiflere genel bakışı [php.net |https://www.php.net/manual/en/ini.list.php] adresinde bulabilirsiniz. ```neon php: @@ -176,16 +170,17 @@ php: ``` -DI Servisleri +DI servisleri ============= Bu servisler DI konteynerine eklenir: -| Ad | Tip | Açıklama -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [tüm uygulamanın başlatıcısı |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | presenter'lar için fabrika -| `application.###` | [api:Nette\Application\UI\Presenter] | bireysel presenter'lar -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | `Latte\Engine` nesnesi için fabrika -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | [`$this->template` |templates] için fabrika +| 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 index ff2a3d3020..ca11c0a42b 100644 --- a/application/tr/creating-links.texy +++ b/application/tr/creating-links.texy @@ -1,146 +1,163 @@ -URL Bağlantıları Oluşturma +URL bağlantıları oluşturma **************************
    -Nette'de bağlantı oluşturmak parmakla göstermek kadar kolaydır. Sadece işaret etmeniz yeterlidir ve framework tüm işi sizin için yapar. Şunları göstereceğiz: +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 bağlantılar nasıl oluşturulur -- mevcut sayfaya bir bağlantı nasıl ayırt edilir -- geçersiz bağlantılarla ne yapılmalı +- ş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, şablonlarınıza veya kodunuza daha sonra değişebilecek veya karmaşık bir şekilde birleştirilmesi gereken uygulamanızın URL adreslerini asla sabit kodlamanız gerekmeyecektir. Bağlantıda presenter'ı ve eylemi belirtmeniz, olası parametreleri iletmeniz yeterlidir ve framework URL'yi kendisi oluşturacaktır. Aslında, bir fonksiyonu çağırmaya çok benzer. Bunu seveceksiniz. +[Ç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 ==================== -En sık olarak şablonlarda bağlantılar oluştururuz ve `n:href` niteliği harika bir yardımcıdır: +Bağlantıları çoğu zaman şablonlarda oluştururuz ve `n:href` niteliği harika bir yardımcıdır: ```latte detay ``` -HTML niteliği `href` yerine [n:niteliği |latte:syntax#n:nitelikler] `n:href` kullandığımıza dikkat edin. Değeri, `href` niteliğinde olduğu gibi bir URL değil, presenter'ın ve eylemin adıdır. +`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, basitleştirilmiş bir ifadeyle, `ProductPresenter::renderShow()` metodunu çağırmak gibidir. Ve eğer imzasında parametreler varsa, onu argümanlarla çağırabiliriz: +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ış parametreleri iletmek de mümkündür. Aşağıdaki bağlantı, `lang` parametresini `cs` değeriyle iletir: +Adlandırılmış parametreler de aktarılabilir. Aşağıdaki bağlantı, `lang` parametresini `en` değeriyle aktarır: ```latte -ürün detayı +ürün detayı ``` -Eğer `ProductPresenter::renderShow()` metodu imzasında `$lang` içermiyorsa, parametrenin değerini `$lang = $this->getParameter('lang')` kullanarak veya [özellikten |presenters#İstek Parametreleri] öğrenebilir. +`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ü (Latte 2.x'te `(expand)` operatörü) ile genişletilebilirler: +Parametreler bir dizide saklanıyorsa, `...` operatörüyle açılabilirler: ```latte -{var $args = [$product->id, lang => cs]} -ürün detayı +{var $args = [$product->id, lang => en]} +ürün detayı ``` -Bağlantılarda sözde [kalıcı parametreler |presenters#Kalıcı Parametreler] de otomatik olarak iletilir. +[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 metinde yazdırmak istiyorsak, `{link}` kullanırız: +`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 -Adres: {link Home:default} +URL şudur: {link Home:default} ``` Kodda ===== -Presenter'da bir bağlantı oluşturmak için `link()` metodu kullanılır: +Presenter'da bağlantı oluşturmak için `link()` metodu kullanılır: ```php $url = $this->link('Product:show', $product->id); ``` -Parametreler, adlandırılmış parametrelerin de belirtilebildiği bir dizi kullanılarak da iletilebilir: +Parametreler dizi olarak da aktarılabilir; burada adlandırılmış parametreler de belirtilebilir: ```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); +$url = $this->link('Product:show', [$product->id, 'lang' => 'en']); ``` -Bağlantılar presenter olmadan da oluşturulabilir, bunun için [#LinkGenerator] ve onun `link()` metodu vardır. +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'a Bağlantılar -======================= -Bağlantının hedefi bir presenter ve eylem ise, şu sözdizimine sahiptir: +Presenter'lara bağlantılar +========================== + +Bağlantının hedefi bir presenter ve eylem ise, sözdizimi şöyledir: ``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] +[//] [[[[:]modül:]presenter:]eylem | this] [#fragman] ``` -Format, tüm Latte etiketleri ve bağlantılarla çalışan tüm presenter metotları tarafından desteklenir, yani `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` ve ayrıca [#LinkGenerator]. Dolayısıyla, örneklerde `n:href` kullanılmış olsa bile, fonksiyonlardan herhangi biri orada olabilirdi. +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 form bu nedenle `Presenter:action` şeklindedir: +Temel biçim dolayısıyla `Presenter:eylem`'dir: ```latte ana sayfa ``` -Mevcut presenter'ın bir eylemine bağlantı veriyorsak, adını atlayabiliriz: +Geçerli presenter'ın bir eylemine bağlantı veriyorsak adını atlayabiliriz: ```latte ana sayfa ``` -Hedef eylem `default` ise, onu atlayabiliriz, ancak iki nokta üst üste kalmalıdır: +Hedef eylem `default` ise onu atlayabiliriz, ama iki nokta üst üste kalmalıdır: ```latte ana sayfa ``` -Bağlantılar ayrıca diğer [modüllere |directory-structure#Presenter lar ve Şablonlar] de yönlendirebilir. Burada bağlantılar, iç içe geçmiş alt modüle göreceli veya mutlak olarak ayırt edilir. Prensip, diskteki yollara benzer, yalnızca eğik çizgiler yerine iki nokta üst üste kullanılır. Mevcut presenter'ın `Front` modülünün bir parçası olduğunu varsayalım, o zaman şunu yazarız: +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 bağlantıdır |#Mevcut Sayfaya Bağlantı]. +Özel bir durum, hedef olarak `this` belirttiğimiz [kendine giden |#Geçerli sayfaya bağlantı] bağlantıdır. ```latte yenile ``` -Izgara işareti `#` sonrasındaki bir parça (fragment) aracılığıyla sayfanın belirli bir bölümüne bağlantı verebiliriz: +Diyez işareti `#` ardından gelen fragman ile sayfanın belirli bir bölümüne bağlantı verebiliriz: ```latte -Home:default ve #main parçasına bağlantı +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: -Mutlak Yollar +```php +$this->link('Home:default', ['#' => $fragment]); +``` + + +Mutlak yollar ============= -`link()` veya `n:href` kullanılarak oluşturulan bağlantılar her zaman mutlak yollardır (yani `/` karakteriyle başlarlar), ancak `https://domain` gibi protokol ve alan adı içeren mutlak URL'ler değildir. +`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. -Mutlak bir URL oluşturmak için başına iki eğik çizgi ekleyin (örn. `n:href="//Home:"`). Veya presenter'ı yalnızca mutlak bağlantılar oluşturacak şekilde `$this->absoluteUrls = true` ayarlayarak değiştirebilirsiniz. +Şablonda göreli bir yolu mutlak yola dönüştürmek için `|absoluteUrl` filtresi de kullanılabilir. -Mevcut Sayfaya Bağlantı -======================= +Geçerli sayfaya bağlantı +======================== -`this` hedefi mevcut sayfaya bir bağlantı oluşturur: +`this` hedefi, geçerli sayfaya bir bağlantı oluşturur: ```latte yenile ``` -Aynı zamanda, `action()` veya `render()` metotlarının imzasında belirtilen tüm parametreler de, `action()` tanımlanmamışsa iletilir. Dolayısıyla, `Product:show` sayfasındaysak ve `id: 123` ise, `this` bağlantısı bu parametreyi de iletecektir. +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: @@ -148,13 +165,13 @@ Elbette parametreleri doğrudan belirtmek de mümkündür: yenile ``` -`isLinkCurrent()` fonksiyonu, bağlantı hedefinin mevcut sayfayla aynı olup olmadığını kontrol eder. Bu, örneğin şablonda bağlantıları ayırt etmek vb. için kullanılabilir. +`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. -Parametreler `link()` metoduyla aynıdır, ancak ek olarak belirli bir eylem yerine, söz konusu presenter'ın herhangi bir eylemi anlamına gelen `*` joker karakterini belirtmek mümkündür. +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ş Yap + Giriş {/if}
  • @@ -162,15 +179,15 @@ Parametreler `link()` metoduyla aynıdır, ancak ek olarak belirli bir eylem yer
  • ``` -Tek bir öğede `n:href` ile birlikte kullanıldığında, kısaltılmış bir form kullanılabilir: +Tek bir elemanda `n:href` ile birlikte kısaltılmış bir biçim kullanılabilir: ```latte ... ``` -`*` joker karakteri yalnızca eylem yerine kullanılabilir, presenter yerine kullanılamaz. +`*` joker karakteri yalnızca eylemin yerine kullanılabilir, presenter'ın yerine değil. -Belirli bir modülde veya alt modülünde olup olmadığımızı kontrol etmek için `isModuleCurrent(moduleName)` metodunu kullanırız. +Belirli bir modülde veya onun alt modülünde olup olmadığımızı belirlemek için `isModuleCurrent(modulAdi)` metodunu kullanın. ```latte
  • @@ -179,56 +196,72 @@ Belirli bir modülde veya alt modülünde olup olmadığımızı kontrol etmek i ``` -Sinyale Bağlantılar -=================== +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, aynı zamanda bir [sinyal |components#Sinyal] de olabilir (`handle()` metodunu çağırırlar). O zaman sözdizimi aşağıdaki gibidir: +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: ``` -[//] [sub-component:]signal! [#fragment] +[//] [alt-bileşen:]sinyal! [#fragman] ``` -Sinyal bu nedenle bir ünlem işaretiyle ayırt edilir: +Sinyal, ünlem işaretiyle ayırt edilir: ```latte sinyal ``` -Bir alt bileşenin (veya alt-alt bileşenin) sinyaline bir bağlantı oluşturmak da mümkündür: +Bir alt bileşenin (veya alt alt bileşenin) sinyaline de bağlantı oluşturabilirsiniz: ```latte sinyal ``` -Bileşendeki Bağlantılar -======================= +Bileşende bağlantılar +===================== -[Bileşenler|components] bağımsız, yeniden kullanılabilir birimler olduğundan ve çevreleyen presenter'larla herhangi bir bağlantısı olmaması gerektiğinden, bağlantılar burada biraz farklı çalışır. Latte niteliği `n:href` ve `{link}` etiketi ile `link()` gibi bileşen metotları ve diğerleri, bağlantı hedefini **her zaman sinyal adı olarak** kabul eder. Bu nedenle ünlem işareti belirtmek bile gerekli değildir: +[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 isteseydik, bunun için `{plink}` etiketini kullanırdık: +Bileşen şablonunda presenter'lara bağlantı vermek istersek `{plink}` etiketini kullanırız: ```latte -giriş +ana sayfa ``` -veya kodda +ya da kodda ```php $this->getPresenter()->link('Home:default') ``` -Takma Adlar (Alias) .{data-version:v3.2.2} -========================================== +Takma adlar .{data-version:3.2.3} +================================= -Bazen Presenter:eylem çiftine kolayca hatırlanabilir bir takma ad atamak yararlı olabilir. Örneğin, `Front:Home:default` giriş sayfasını basitçe `home` olarak veya `Admin:Dashboard:default` sayfasını `admin` olarak adlandırmak. +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: +Takma adlar [yapılandırmada|configuration] `application › aliases` anahtarı altında tanımlanır: ```neon application: @@ -238,7 +271,7 @@ application: sign: Front:Sign:in ``` -Bağlantılarda, örneğin bir at işareti kullanılarak yazılırlar: +Bağlantılarda ise et işaretiyle yazılırlar, örneğin: ```latte yönetim @@ -247,17 +280,17 @@ Bağlantılarda, örneğin bir at işareti kullanılarak yazılırlar: `redirect()` ve benzeri gibi bağlantılarla çalışan tüm metotlarda da desteklenirler. -Geçersiz Bağlantılar +Geçersiz bağlantılar ==================== -Geçersiz bir bağlantı oluşturmamız olabilir - ya var olmayan bir presenter'a yönlendirdiği için, ya hedef metodun imzasında kabul ettiğinden daha fazla parametre ilettiği için ya da hedef eylem için bir URL oluşturulamadığı için. Geçersiz bağlantılarla nasıl başa çıkılacağını statik değişken `Presenter::$invalidLinkMode` belirler. Bu, şu değerlerin bir kombinasyonunu alabilir (sabitler): +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 # karakteri döndürülür -- `Presenter::InvalidLinkWarning` - E_USER_WARNING uyarısı atılır, bu üretim modunda günlüğe kaydedilir, ancak betiğin çalışmasını kesintiye uğratmaz -- `Presenter::InvalidLinkTextual` - görsel uyarı, hatayı doğrudan bağlantıya yazar -- `Presenter::InvalidLinkException` - InvalidLinkException istisnası atılır +- `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` ve geliştirme modunda `InvalidLinkWarning | InvalidLinkTextual` şeklindedir. Üretim ortamındaki `InvalidLinkWarning`, betiğin kesintiye uğramasına neden olmaz, ancak uyarı günlüğe kaydedilir. Geliştirme ortamında, [Tracy |tracy:] tarafından yakalanır ve bir mavi ekran görüntüler. `InvalidLinkTextual`, `#error:` karakterleriyle başlayan bir hata mesajını URL olarak döndürerek çalışır. Bu tür bağlantıların ilk bakışta belirgin olmasını sağlamak için CSS'imize ekleriz: +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:"] { @@ -266,7 +299,7 @@ a[href^="#error:"] { } ``` -Geliştirme ortamında uyarıların üretilmesini istemiyorsak, sessiz modu doğrudan [yapılandırmada|configuration] ayarlayabiliriz. +Geliştirme ortamında uyarı üretilmesini istemiyorsak, onları doğrudan [yapılandırmada|configuration] bastırabiliriz. ```neon application: @@ -277,10 +310,10 @@ application: LinkGenerator ============= -`link()` metodunun sunduğu benzer konforla, ancak presenter olmadan bağlantılar nasıl oluşturulur? Bunun için [api:Nette\Application\LinkGenerator] vardır. +`link()` metodundaki rahatlıkla, ama presenter olmadan nasıl bağlantı oluşturulur? İşte [api:Nette\Application\LinkGenerator] bunun içindir. -LinkGenerator, kurucu aracılığıyla size iletilmesini isteyebileceğiniz ve ardından `link()` metoduyla bağlantılar oluşturabileceğiniz bir servistir. +LinkGenerator, constructor üzerinden size aktarılabilen ve sonra `link()` metoduyla bağlantı oluşturabileceğiniz bir servistir. -Presenter'lara kıyasla burada bir fark vardır. LinkGenerator tüm bağlantıları doğrudan mutlak URL'ler olarak oluşturur. Ayrıca, "mevcut presenter" diye bir şey yoktur, bu nedenle hedef olarak yalnızca `link('default')` eylem adını belirtmek veya modüllere göreceli yollar belirtmek mümkün değildir. +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` atar. +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 index 16b6298815..5b65e0b64f 100644 --- a/application/tr/directory-structure.texy +++ b/application/tr/directory-structure.texy @@ -1,45 +1,45 @@ -Uygulama Dizin Yapısı -********************* +Uygulamanın dizin yapısı +************************
    -Nette Framework projeleri için anlaşılır ve ölçeklenebilir bir dizin yapısı nasıl tasarlanır? Kodunuzu düzenlemenize yardımcı olacak kanıtlanmış en iyi uygulamaları göstereceğiz. Şunları öğreneceksiniz: +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ıksal olarak nasıl bölersiniz** -- yapıyı projenin büyümesiyle **iyi ölçeklenecek** şekilde nasıl tasarlarsınız -- **olası alternatifler** ve avantajları veya dezavantajları nelerdir +- 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 herhangi bir belirli yapıya bağlı olmadığını belirtmek önemlidir. Herhangi bir ihtiyaca ve tercihe kolayca uyarlanabilecek şekilde tasarlanmıştır. +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ı +Temel proje yapısı ================== -Nette Framework herhangi bir sabit dizin yapısı dikte etmese de, [Web Project|https://github.com/nette/web-project] şeklinde kanıtlanmış bir varsayılan düzenleme vardır: +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ı, resimler..., alternatif olarak resources/ +├── assets/ ← SCSS, JS dosyaları, görseller..., alternatif olarak resources/ ├── bin/ ← komut satırı betikleri ├── config/ ← yapılandırma -├── log/ ← günlüğe kaydedilen hatalar +├── log/ ← günlüklenen hatalar ├── temp/ ← geçici dosyalar, önbellek ├── tests/ ← testler -├── vendor/ ← Composer tarafından kurulan kütüphaneler +├── 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ırabilir veya taşıyabilirsiniz. Ardından, yalnızca `Bootstrap.php` dosyasındaki ve muhtemelen `composer.json` dosyasındaki dizinlere giden göreceli yolları ayarlamanız yeterlidir. Başka hiçbir şeye gerek yoktur, karmaşık yeniden yapılandırma yok, sabitlerde değişiklik yok. Nette akıllı otomatik algılamaya sahiptir ve URL tabanı da dahil olmak üzere uygulamanın konumunu otomatik olarak tanır. +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. -Kod Organizasyon Prensipleri -============================ +Kodu düzenleme ilkeleri +======================= -Yeni bir projeyi ilk kez incelerken, içinde hızla yönünüzü bulabilmelisiniz. `app/Model/` dizinini açtığınızı ve şu yapıyı gördüğünüzü hayal edin: +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/ @@ -48,9 +48,9 @@ Yeni bir projeyi ilk kez incelerken, içinde hızla yönünüzü bulabilmelisini └── Entities/ \-- -Bundan yalnızca projenin bazı servisler, depolar ve varlıklar kullandığını anlarsınız. Uygulamanın gerçek amacı hakkında hiçbir şey öğrenemezsiniz. +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. -Başka bir yaklaşıma bakalım - **alanlara göre organizasyon**: +Farklı bir yaklaşıma bakalım: **alan adına göre düzenleme**: /--pre app/Model/ @@ -60,76 +60,76 @@ Başka bir yaklaşıma bakalım - **alanlara göre organizasyon**: └── Product/ \-- -Burada durum farklı - ilk bakışta bunun bir e-ticaret sitesi olduğu açık. Dizin adlarının kendisi uygulamanın neler yapabildiğini ortaya koyuyor - ödemeler, siparişler ve ürünlerle çalışıyor. +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 türüne göre organizasyon) pratikte bir dizi sorun getirir: mantıksal olarak birbiriyle ilişkili kod farklı klasörlere dağılmıştır ve aralarında atlamanız gerekir. Bu nedenle alanlara göre organize edeceğiz. +İ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ı (Namespaces) --------------------------- +İsim alanları +------------- -Dizin yapısının uygulamadaki isim alanlarıyla örtüşmesi adettendir. Bu, dosyaların fiziksel konumunun isim alanlarına karşılık geldiği anlamına gelir. Örneğin, `app/Model/Product/ProductRepository.php` içinde bulunan bir sınıfın `App\Model\Product` isim alanına sahip olması gerekir. Bu prensip, kodda gezinmeye yardımcı olur ve otomatik yüklemeyi basitleştirir. +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 vs Çoğul Sayı ---------------------------- +Adlarda tekil mi çoğul mu +------------------------- -Uygulamanın ana dizinlerinde tekil sayı kullandığımıza dikkat edin: `app`, `config`, `log`, `temp`, `www`. Aynı şekilde uygulama içinde de: `Model`, `Core`, `Presentation`. Bunun nedeni, her birinin bütün bir kavramı temsil etmesidir. +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, örneğin `app/Model/Product`, ürünlerle ilgili her şeyi temsil eder. Buna `Products` demeyiz, çünkü ürünlerle dolu bir klasör değildir (orada `nokia.php`, `samsung.php` dosyaları olurdu). Ürünlerle çalışmak için sınıflar içeren bir isim alanıdır - `ProductRepository.php`, `ProductService.php`. +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ğul sayıdadır çünkü bir dizi bağımsız yürütülebilir betik içerir - `CleanupTask.php`, `ImportTask.php`. Her biri bağımsız bir birimdir. +`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 şunları kullanmanızı öneririz: -- İşlevsel bir bütünü temsil eden isim alanı için tekil sayı (birden fazla varlıkla çalışsa bile) -- Bağımsız birimlerin koleksiyonları için çoğul sayı -- Emin olmadığınızda veya bunun hakkında düşünmek istemiyorsanız, tekil sayıyı seçin +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/` +Genel dizin `www/` ================== -Bu dizin, web'den erişilebilen tek dizindir (document-root olarak da bilinir). `www/` yerine `public/` adıyla da sıkça karşılaşabilirsiniz - bu sadece bir gelenek meselesidir ve uygulamanın işlevselliği üzerinde hiçbir etkisi yoktur. Dizin şunları içerir: -- Uygulamanın [Giriş noktası |bootstrapping#index.php] `index.php` -- mod_rewrite (Apache için) kuralları içeren `.htaccess` dosyası -- Statik dosyalar (CSS, JavaScript, resimler) +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 doğru güvenliği için [document-root'un doğru şekilde yapılandırılması |nette:troubleshooting#URL den www Dizini Nasıl Değiştirilir veya Kaldırılır] esastır. +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 yerleştirmeyin - yürütülebilir olabilecek ve genel olarak erişilebilir olmaması gereken binlerce dosya içerir. +`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/` +Uygulama dizini `app/` ====================== Bu, uygulama kodunu içeren ana dizindir. Temel yapı: /--pre app/ -├── Core/ ← altyapısal konular +├── Core/ ← altyapı konuları ├── Model/ ← iş mantığı ├── Presentation/ ← presenter'lar ve şablonlar ├── Tasks/ ← komut betikleri -└── Bootstrap.php ← uygulamanın başlatma sınıfı +└── Bootstrap.php ← uygulamanın başlatıcı sınıfı \-- -`Bootstrap.php`, ortamı başlatan, yapılandırmayı yükleyen ve DI konteynerini oluşturan [uygulamanın başlangıç sınıfıdır|bootstrapping]. +`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 bireysel alt dizinlere daha ayrıntılı bakalım. +Şimdi tek tek alt dizinlere daha ayrıntılı bakalım. -Presenter'lar ve Şablonlar +Presenter'lar ve şablonlar ========================== -Uygulamanın sunum kısmı `app/Presentation` dizinindedir. Alternatif olarak kısa `app/UI` da kullanılabilir. Bu, tüm presenter'lar, şablonları ve olası yardımcı sınıflar için yerdir. +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ı alanlara göre organize ederiz. E-ticaret, blog ve API'yi birleştiren karmaşık bir projede yapı şöyle görünürdü: +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 ön yüzü +├── Shop/ ← e-ticaret frontend'i │ ├── Product/ │ ├── Cart/ │ └── Order/ @@ -143,11 +143,11 @@ Bu katmanı alanlara göre organize ederiz. E-ticaret, blog ve API'yi birleştir └── V1/ \-- -Buna karşılık, basit bir blog için şu bölümlemeyi kullanırdık: +Buna karşılık basit bir blog için şu yapıyı kullanırdık: /--pre app/Presentation/ -├── Front/ ← web sitesi ön yüzü +├── Front/ ← sitenin frontend'i │ ├── Home/ │ └── Post/ ├── Admin/ ← yönetim @@ -157,9 +157,9 @@ Buna karşılık, basit bir blog için şu bölümlemeyi kullanırdık: └── 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üller** diyoruz. Teknik olarak bunlar, uygulamayı mantıksal olarak bölmek için kullanılan normal dizinlerdir. +`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, aynı adı taşıyan bir presenter ve şablonlarını içerir. Örneğin, `Dashboard/` klasörü şunları içerir: +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/ @@ -167,7 +167,7 @@ Presenter içeren her klasör, aynı adı taşıyan bir presenter ve şablonlar └── 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ında bulunur ([#Presenter Eşlemesi] bölümüne bakın): +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; @@ -178,22 +178,22 @@ class DashboardPresenter extends Nette\Application\UI\Presenter } ``` -Uygulamada `Admin` modülü içindeki `Dashboard` presenter'ına iki nokta üst üste gösterimiyle `Admin:Dashboard` olarak başvururuz. `default` eylemine ise `Admin:Dashboard:default` olarak başvururuz. İç içe geçmiş modüller durumunda, daha fazla iki nokta üst üste kullanırız, örneğin `Shop:Order:Detail:default`. +`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`. -Esnek Yapı Geliştirme ---------------------- +Yapının esnek biçimde gelişmesi +------------------------------- -Bu yapının büyük avantajlarından biri, projenin artan ihtiyaçlarına ne kadar zarif bir şekilde uyum sağladığıdır. Örnek olarak, XML beslemeleri oluşturan bölümü ele alalım. Başlangıçta basit bir formumuz var: +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 bir presenter -├── sitemap.latte ← site haritası için şablon -└── feed.latte ← RSS beslemesi için şablon +├── ExportPresenter.php ← tüm dışa aktarımlar için tek presenter +├── sitemap.latte ← site haritası şablonu +└── feed.latte ← RSS beslemesi şablonu \-- -Zamanla başka besleme türleri eklenir ve onlar için daha fazla mantığa ihtiyacımız olur... Sorun değil! `Export/` klasörü basitçe bir modül haline gelir: +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/ @@ -202,52 +202,52 @@ Zamanla başka besleme türleri eklenir ve onlar için daha fazla mantığa ihti │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php - ├── zbozi.latte ← Zboží.cz için besleme - └── heureka.latte ← Heureka.cz için besleme + ├── amazon.latte ← Amazon için besleme + └── ebay.latte ← eBay için besleme \-- -Bu dönüşüm tamamen sorunsuzdur - sadece yeni alt klasörler oluşturmanız, kodu bunlara bölmeniz ve bağlantıları güncellemeniz yeterlidir (örneğin, `Export:feed` yerine `Export:Feed:zbozi`). Bu sayede yapıyı ihtiyaçlara göre kademeli olarak genişletebiliriz, iç içe geçme seviyesi sınırlı değildir. +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önetimde sipariş yönetimiyle ilgili `OrderDetail`, `OrderEdit`, `OrderDispatch` vb. gibi birçok presenter'ınız varsa, daha iyi organizasyon için bu noktada (klasörler için) `Detail`, `Edit`, `Dispatch` ve diğer presenter'ları içerecek bir `Order` modülü (klasörü) oluşturabilirsiniz. +Ö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 +Şablonların konumu ------------------ -Önceki örneklerde, şablonların doğrudan presenter içeren klasörde bulunduğunu gördük: +Önceki örneklerde şablonların doğrudan presenter'ın bulunduğu klasörde olduğunu gördük: /--pre Dashboard/ ├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← şablon için isteğe bağlı sınıf +├── DashboardTemplate.php ← isteğe bağlı şablon sınıfı └── default.latte ← şablon \-- -Bu konum pratikte en uygun olanıdır - tüm ilgili dosyalarınız hemen elinizin altındadır. +Bu konum pratikte en kullanışlısı olduğunu kanıtlar: ilişkili tüm dosyalar elinizin altındadır. -Alternatif olarak, şablonları `templates/` alt klasörüne yerleştirebilirsiniz. Nette her iki seçeneği de destekler. Hatta şablonları tamamen `Presentation/` klasörünün dışına bile yerleştirebilirsiniz. Şablonların yerleştirilme seçenekleri hakkında her şeyi [Şablonları Bulma |templates#Şablonları Bulma] bölümünde bulabilirsiniz. +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 +Yardımcı sınıflar ve bileşenler ------------------------------- -Presenter'lara ve şablonlara genellikle başka yardımcı dosyalar da eşlik eder. Bunları etki alanlarına göre mantıksal olarak yerleştiririz: +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 yanında**, belirli bir presenter için özel bileşenler durumunda: +1. **Doğrudan presenter'ın yanına**, o presenter'a özgü bileşenler söz konusuysa: /--pre Product/ ├── ProductPresenter.php -├── ProductGrid.php ← ürün listeleme için bileşen -└── FilterForm.php ← filtreleme için form +├── ProductGrid.php ← ürün listelemesi bileşeni +└── FilterForm.php ← filtreleme formu \-- -2. **Modül için** - alfabenin hemen başında düzgün bir şekilde yerleştirilecek olan `Accessory` klasörünü kullanmanızı öneririz: +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 ← ön yüz için bileşenler +│ ├── NavbarControl.php ← frontend bileşenleri │ └── TemplateFilters.php ├── Product/ └── Cart/ @@ -263,13 +263,13 @@ Presenter'lara ve şablonlara genellikle başka yardımcı dosyalar da eşlik ed └── Admin/ \-- -Veya `LatteExtension.php` veya `TemplateFilters.php` gibi yardımcı sınıfları altyapısal `app/Core/Latte/` klasörüne yerleştirebilirsiniz. Ve bileşenleri `app/Components` içine. Seçim, ekibin alışkanlıklarına bağlıdır. +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 kalbi ========================= -Model, uygulamanın tüm iş mantığını içerir. Organizasyonu için yine kural geçerlidir - alanlara göre yapılandırırız: +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/ @@ -284,9 +284,9 @@ Model, uygulamanın tüm iş mantığını içerir. Organizasyonu için yine kur └── Shipping/ ← kargoyla ilgili her şey \-- -Modelde tipik olarak şu tür sınıflarla karşılaşırsınız: +Model'de genellikle şu sınıf tipleriyle karşılaşırsınız: -**Fasadlar (Facades)**: Uygulamadaki belirli bir alana ana giriş noktasını temsil ederler. Tam kullanım senaryolarını (use-cases) uygulamak için farklı servisler arasındaki işbirliğini koordine eden bir orkestratör görevi görürler (örneğin "sipariş oluştur" veya "ödemeyi işle"). Orkestrasyon katmanının altında, fasad uygulama ayrıntılarını uygulamanın geri kalanından gizler, böylece söz konusu alanla çalışmak için temiz bir arayüz sağlar. +**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 @@ -294,26 +294,26 @@ class OrderFacade public function createOrder(Cart $cart): Order { // doğrulama - // sipariş oluşturma - // e-posta gönderme + // siparişin oluşturulması + // e-posta gönderimi // istatistiklere yazma } } ``` -**Servisler**: Alan içindeki belirli bir iş operasyonuna odaklanırlar. Tüm kullanım senaryolarını düzenleyen bir fasadın aksine, bir servis belirli bir iş mantığını uygular (fiyat hesaplamaları veya ödeme işlemleri gibi). Servisler tipik olarak durumsuzdur ve daha karmaşık operasyonlar için yapı taşları olarak fasadlar tarafından veya daha basit görevler için doğrudan uygulamanın diğer bölümleri tarafından kullanılabilirler. +**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 hesaplama + // fiyat hesaplaması } } ``` -**Depolar (Repositories)**: Veri deposuyla, tipik olarak veritabanıyla tüm iletişimi sağlarlar. Görevi, varlıkları yüklemek ve kaydetmek ve bunları aramak için metotlar uygulamaktır. Depo, uygulamanın geri kalanını veritabanının uygulama ayrıntılarından soyutlar ve verilerle çalışmak için nesne yönelimli bir arayüz sağlar. +**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 @@ -328,10 +328,10 @@ class OrderRepository } ``` -**Varlıklar (Entities)**: Uygulamadaki ana iş kavramlarını temsil eden, kendi kimlikleri olan ve zamanla değişen nesnelerdir. Tipik olarak bunlar, ORM (Nette Database Explorer veya Doctrine gibi) kullanılarak veritabanı tablolarına eşlenen sınıflardır. Varlıklar, verileriyle ilgili iş kurallarını ve doğrulama mantığını içerebilir. +**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 +// 'orders' veritabanı tablosuna eşlenen varlık class Order extends Nette\Database\Table\ActiveRow { public function addItem(Product $product, int $quantity): void @@ -345,13 +345,13 @@ class Order extends Nette\Database\Table\ActiveRow } ``` -**Değer Nesneleri (Value Objects)**: Kendi kimlikleri olmayan değerleri temsil eden değişmez nesnelerdir - örneğin bir para tutarı veya bir e-posta adresi. Aynı değerlere sahip iki değer nesnesi örneği özdeş kabul edilir. +**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ısal Kod -============== +Altyapı kodu +============ -`Core/` (veya `Infrastructure/`) klasörü, uygulamanın teknik temelinin evidir. Altyapısal kod tipik olarak şunları içerir: +`Core/` klasörü (ya da alternatif olarak `Infrastructure/`) uygulamanın teknik temelinin yuvasıdır. Altyapı kodu genellikle şunları içerir: /--pre app/Core/ @@ -363,14 +363,14 @@ Altyapısal Kod ├── Logging/ ← günlükleme ve izleme │ ├── SentryLogger.php │ └── FileLogger.php -├── Cache/ ← önbellekleme katmanı +├── Cache/ ← önbellek katmanı │ └── FullPageCache.php -└── Integration/ ← harici servislerle entegrasyon +└── Integration/ ← dış servislerle entegrasyon ├── Slack/ └── Stripe/ \-- -Daha küçük projelerde, elbette düz bir bölümleme yeterlidir: +Daha küçük projelerde doğal olarak düz bir yapı yeterlidir: /--pre Core/ @@ -379,82 +379,82 @@ Daha küçük projelerde, elbette düz bir bölümleme yeterlidir: └── QueueMailer.php \-- -Bu, şu kodu ifade eder: +Bu, şöyle bir koddur: -- Teknik altyapıyı çözer (yönlendirme, günlükleme, önbellekleme) -- Harici servisleri entegre eder (Sentry, Elasticsearch, Redis) +- 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 e-ticaret veya blog için aynı şekilde çalışır. +- Ç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 modele mi ait olduğundan emin değil misiniz? Anahtar fark, `Core/` içindeki kodun: +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 bilmemesi (ürünler, siparişler, makaleler) -- Çoğunlukla başka bir projeye taşınabilmesi -- "Nasıl çalıştığını" (bir e-posta nasıl gönderilir) çözmesi, "ne yaptığını" (hangi e-postanın gönderileceği) değil +- 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öndermek için sınıf örnekleri oluşturur, SMTP ayarlarını çözer -- `App\Model\OrderMailer` - siparişlerle ilgili e-postaları göndermek için `MailerFactory` kullanır, şablonlarını bilir ve ne zaman gönderilmeleri gerektiğini bilir +- `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 +Komut betikleri =============== -Uygulamaların genellikle normal HTTP istekleri dışında etkinlikler gerçekleştirmesi gerekir - ister arka planda veri işleme, ister bakım, ister periyodik görevler olsun. Çalıştırma için `bin/` dizinindeki basit betikler kullanılır, uygulama mantığının kendisi ise `app/Tasks/` (veya `app/Commands/`) içine yerleştirilir. +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 verileri silme -│ └── DbOptimizeCommand.php ← veritabanı optimizasyonu -├── Integration/ ← harici sistemlerle entegrasyon -│ ├── ImportProducts.php ← tedarikçi sisteminden içe aktarma -│ └── SyncOrders.php ← sipariş senkronizasyonu +│ ├── 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ülten gönderme + ├── NewsletterCommand.php ← bültenlerin gönderimi └── ReminderCommand.php ← müşteri bildirimleri \-- -Modele ne aittir ve komut betiklerine ne aittir? Örneğin, tek bir e-posta gönderme mantığı modelin bir parçasıdır, binlerce e-postanın toplu gönderimi zaten `Tasks/` içine aittir. +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 |https://blog.nette.org/en/cli-scripts-in-nette-application] veya cron aracılığıyla çalıştırılır. HTTP isteği aracılığıyla da çalıştırılabilirler, ancak güvenliği göz önünde bulundurmak gerekir. Görevi başlatan presenter'ın güvenliğini sağlamak gerekir, örneğin yalnızca oturum açmış kullanıcılar için veya güçlü bir belirteç ve izin verilen IP adreslerinden erişimle. Uzun görevler için betik zaman aşımını artırmak ve oturumun kilitlenmemesi için `session_write_close()` kullanmak gerekir. +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. -Diğer Olası Dizinler +Olası diğer dizinler ==================== -Bahsedilen temel dizinlere ek olarak, proje ihtiyaçlarına göre başka özel klasörler de ekleyebilirsiniz. En yaygın olanlarına ve kullanımlarına bakalım: +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/ ← test verileri için geçiş betikleri ve tohumlayıcılar -├── Components/ ← tüm uygulama genelinde paylaşılan görsel bileşenler -├── Event/ ← olay odaklı mimari kullanıyorsanız yararlıdır +├── 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 genelinde presenter'larda kullanılan paylaşılan görsel bileşenler için `app/Components` veya `app/Controls` klasörünü kullanabilirsiniz: +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 listeleri için bileşenler +├── Grid/ ← veri listelemeleri için bileşenler │ └── DataGrid.php └── Navigation/ ← gezinme öğeleri ├── Breadcrumbs.php └── Menu.php \-- -Buraya daha karmaşık mantığa sahip bileşenler aittir. Bileşenleri birden fazla proje arasında paylaşmak istiyorsanız, bunları ayrı bir composer paketine ayırmak uygundur. +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. -E-posta iletişiminin yönetimini `app/Mail` dizinine yerleştirebilirsiniz: +`app/Mail` dizinine e-posta iletişiminin yönetimini koyabilirsiniz: /--pre app/Mail/ @@ -465,35 +465,35 @@ E-posta iletişiminin yönetimini `app/Mail` dizinine yerleştirebilirsiniz: \-- -Presenter Eşlemesi -================== +Presenter mapping +================= -Eşleme, presenter adından sınıf adını türetme kurallarını tanımlar. Bunları [yapılandırmada|configuration] `application › mapping` anahtarı altında belirtiriz. +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 yerleştirdiğimizi gösterdik. Bu geleneği Nette'ye yapılandırma dosyasında bildirmeliyiz. Tek bir satır yeterlidir: +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 ``` -Eşleme nasıl çalışır? Daha iyi anlamak için önce modülsüz bir uygulama hayal edelim. 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şlenir. Bunu şu yapılandırmayla başarırız: +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 ``` -Eşleme, `Home` presenter adının `App\Presentation\*Presenter` maskesindeki yıldız işaretini değiştirmesiyle çalışır, böylece sonuçta `App\Presentation\HomePresenter` sınıf adını elde ederiz. Basit! +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 yerleştiririz, örneğin `Home` presenter'ı `App\Presentation\Home\HomePresenter` sınıfına eşlenir. Bunu iki nokta üst üste işaretini iki katına çıkararak başarırız (Nette Application 3.2 gerektirir): +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çelim. Her modül için belirli bir eşleme tanımlayabiliriz: +Şimdi presenter'ları modüllere eşlemeye geçiyoruz. Her modül için özel bir mapping tanımlayabiliriz: ```neon application: @@ -503,9 +503,9 @@ application: Api: App\Api\*Presenter ``` -Bu yapılandırmaya göre, `Front:Home` presenter'ı `App\Presentation\Front\Home\HomePresenter` sınıfına eşlenirken, `Api:OAuth` presenter'ı `App\Api\OAuthPresenter` sınıfına eşlenir. +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ülleri benzer bir eşleme yöntemine sahip olduğundan ve muhtemelen bu türden daha fazla modül olacağından, bunları değiştirecek genel bir kural oluşturmak mümkündür. Sınıf maskesine modül için yeni bir yıldız işareti eklenir: +`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: @@ -514,9 +514,9 @@ application: Api: App\Api\*Presenter ``` -Bu, örneğin `Admin:User:Edit` presenter'ı gibi daha derinlemesine iç içe geçmiş dizin yapıları için de çalışır, yıldız işaretli segment her seviye için tekrarlanır ve sonuç `App\Presentation\Admin\User\Edit\EditPresenter` sınıfıdır. +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 gösterim, bir dize yerine üç segmentten oluşan bir dizi kullanmaktır. Bu gösterim öncekiyle eşdeğerdir: +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: diff --git a/application/tr/how-it-works.texy b/application/tr/how-it-works.texy index 6b64f1a29a..14887db71f 100644 --- a/application/tr/how-it-works.texy +++ b/application/tr/how-it-works.texy @@ -1,101 +1,101 @@ -Uygulamalar Nasıl Çalışır? +Uygulamalar nasıl çalışır? **************************
    -Şu anda Nette dokümantasyonunun temel sayfasını okuyorsunuz. Web uygulamalarının çalışma prensibini öğreneceksiniz. A'dan Z'ye, başlangıç anından PHP betiğinin son nefesine kadar. Okuduktan sonra şunları bileceksiniz: +Ş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 şey nasıl çalışır -- Bootstrap, Presenter ve DI konteynerinin ne olduğu -- dizin yapısının nasıl göründüğü +- 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ı +Dizin yapısı ============ -[WebProject|https://github.com/nette/web-project] adlı web uygulaması iskelet örneğini açın ve okurken bahsedilen dosyalara bakabilirsiniz. +[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ışması için gerekli temel sınıflar -│ │ └── RouterFactory.php ← URL adreslerinin yapılandırılması -│ ├── Presentation/ ← presenter'lar, şablonlar ve ilgili dosyalar -│ │ ├── @layout.latte ← düzen şablonu -│ │ └── Home/ ← Home presenter 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 eyleminin şablonu -│ └── Bootstrap.php ← Bootstrap başlatma sınıfı -├── assets/ ← kaynaklar (SCSS, TypeScript, kaynak görüntüler) +│ │ └── 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üğe kaydedilen hatalar +├── log/ ← günlüklenen hatalar ├── temp/ ← geçici dosyalar, önbellek, … -├── vendor/ ← Composer tarafından kurulan kütüphaneler +├── vendor/ ← Composer ile kurulan kütüphaneler │ ├── ... -│ └── autoload.php ← kurulan tüm paketlerin otomatik yüklenmesi -├── www/ ← genel dizin veya projenin document-root'u -│ ├── assets/ ← derlenmiş statik dosyalar (CSS, JS, resimler, ...) +│ └── 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 ← uygulamanın başlatıldığı ilk dosya +│ └── 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 URL tabanı da dahil olmak üzere uygulamanın konumunu otomatik olarak tanır. +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 ayırabilir |directory-structure#Presenter lar ve Şablonlar] ve sınıfları modül dediğimiz isim alanlarına bölebiliriz. +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 sözde genel dizinini veya document-root'unu temsil eder. Uygulama tarafında başka bir şey ayarlamaya gerek kalmadan yeniden adlandırabilirsiniz. Yalnızca [hosting'i yapılandırmak |nette:troubleshooting#URL den www Dizini Nasıl Değiştirilir veya Kaldırılır] gerekir, böylece document-root bu dizine işaret eder. +`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 olmak üzere doğrudan [Composer|best-practices:composer] kullanarak da indirebilirsiniz: +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/` dizinlerine [yazma izinlerini |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. +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 erişerek doğrudan tarayıcıda görüntüleyebilirsiniz. +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 İsteği +HTTP isteği =========== -Her şey, kullanıcının tarayıcıda bir sayfa açmasıyla başlar. Yani tarayıcı bir HTTP isteği ile sunucuya dokunduğunda. İstek, genel `www/` dizininde bulunan tek bir PHP dosyasına, yani `index.php`'ye yönlendirilir. Diyelim ki istek `https://example.com/product/123` adresine yapıldı. Uygun [sunucu yapılandırması |nette:troubleshooting#Sunucu Kullanıcı Dostu URL ler İçin Nasıl Ayarlanır] sayesinde, bu URL bile `index.php` dosyasına eşlenir ve yürütülür. +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ı başlatmak -2) fabrikayı almak -3) isteği işleyecek Nette uygulamasını başlatmak +1) ortamı hazırlamak +2) factory'yi elde etmek +3) isteği işleyen Nette uygulamasını çalıştırmak -Hangi fabrika? Traktör üretmiyoruz, web sayfaları üretiyoruz! Sabırlı olun, hemen açıklanacak. +Ne factory'si? Traktör üretmiyoruz, web siteleri yapıyoruz! Sabırlı olun, birazdan açıklanacak. -"Ortamı başlatmak" ifadesiyle, örneğin hataları günlüğe kaydetmek veya görselleştirmek için harika bir araç olan [Tracy|tracy:]'nin etkinleştirilmesini kastediyoruz. Üretim sunucusunda hataları günlüğe kaydeder, geliştirme sunucusunda doğrudan görüntüler. Dolayısıyla başlatma, web sitesinin üretim veya geliştirme modunda çalışıp çalışmadığına karar vermeyi de içerir. Bunun için Nette [akıllı otomatik algılama |bootstrapping#Geliştirme vs Üretim Modu] kullanır: web sitesini localhost'ta çalıştırırsanız, geliştirme modunda çalışır. Bu nedenle hiçbir şey yapılandırmanıza gerek yoktur ve uygulama hem geliştirme hem de canlı dağıtım için hemen hazırdır. Bu adımlar [Bootstrap sınıfı|bootstrapping] hakkındaki bölümde gerçekleştirilir ve ayrıntılı olarak açıklanır. +"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, ikincisini atladık, ama ona geri döneceğiz) uygulamayı başlatmaktır. Nette'de HTTP isteklerini işlemekten `Nette\Application\Application` sınıfı (bundan sonra `Application` olarak anılacaktır) sorumludur, bu yüzden uygulamayı başlatmak dediğimizde, özellikle bu sınıfın nesnesinde anlamlı bir ada sahip `run()` metodunu çağırmayı kastediyoruz. +Üçü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, sizi kanıtlanmış metodolojilere göre temiz uygulamalar yazmaya yönlendiren bir akıl hocasıdır. Ve bu kesinlikle en kanıtlanmış olanlardan biri **dependency injection** (bağımlılık enjeksiyonu), kısaca DI olarak adlandırılır. Şu anda sizi DI'yi açıklamakla yormak istemiyoruz, bunun için [ayrı bir bölüm|dependency-injection:introduction] var, önemli olan sonuç, temel nesnelerin genellikle **DI konteyner** (kısaca DIC) olarak adlandırılan bir nesne fabrikası tarafından oluşturulacağıdır. Evet, bu az önce bahsedilen fabrika. Ve bize `Application` nesnesini de üretecek, bu yüzden önce konteynere ihtiyacımız var. Onu `Configurator` sınıfını kullanarak alırız ve `Application` nesnesini üretmesine izin veririz, üzerinde `run()` metodunu çağırırız ve böylece Nette uygulaması başlar. Tam olarak bu, [index.php |bootstrapping#index.php] dosyasında olur. +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. +`Application` sınıfının tek bir görevi vardır: HTTP isteğine yanıt vermek. -Nette'de yazılan uygulamalar, her biri web sitesinin belirli bir sayfasını temsil eden birçok sözde presenter'a (diğer framework'lerde controller terimiyle karşılaşabilirsiniz, aynı şeydir) bölünür: örn. ana sayfa; e-ticaretteki bir ürün; giriş formu; site haritası beslemesi vb. Bir uygulamanın bir ila binlerce presenter'ı olabilir. +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, mevcut isteği işlemek için hangi presenter'a ileteceğine karar vermesi için sözde yönlendiriciye (router) sorarak başlar. Yönlendirici, bunun kimin sorumluluğu olduğuna karar verir. Giriş URL'si `https://example.com/product/123`'e bakar ve nasıl ayarlandığına bağlı olarak, bunun örneğin `id: 123` olan ürünü görüntüleme (**eylem**) isteyeceği **presenter** `Product`'ın işi olduğuna karar verir. Presenter + eylem çiftini iki nokta üst üste ile `Product:show` olarak yazmak iyi bir alışkanlıktır. +`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`. -Yani yönlendirici, URL'yi `Presenter:action` + parametreler çiftine dönüştürdü, bizim durumumuzda `Product:show` + `id: 123`. Böyle bir yönlendiricinin nasıl göründüğünü `app/Core/RouterFactory.php` dosyasında görebilirsiniz ve onu [Yönlendirme|Routing] bölümünde ayrıntılı olarak açıklıyoruz. +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 devam edebilir. `ProductPresenter` sınıfının nesnesini üreterek, ki bu presenter `Product`'ın kodudur. Daha doğrusu, presenter'ı üretmesi için DI konteynerine sorar, çünkü üretmek onun işidir. +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: @@ -109,92 +109,92 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function renderShow(int $id): void { - // modelden verileri alır ve şablona iletiriz + // model'den veriyi al ve şablona aktar $this->template->product = $this->repository->getProduct($id); } } ``` -İsteğin işlenmesini presenter devralır. Ve görev açıktır: `id: 123` ile `show` eylemini gerçekleştirin. Bu, presenter'ların dilinde, `renderShow()` metodunun çağrılacağı ve `$id` parametresinde `123` alacağı anlamına gelir. +İ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. -Presenter birden fazla eylemi işleyebilir, yani birden fazla `render()` metoduna sahip olabilir. Ancak, bir veya mümkün olduğunca az eyleme sahip presenter'lar tasarlamanızı öneririz. +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. -Yani, `renderShow(123)` metodu çağrıldı, kodu hayali bir örnek olsa da, verilerin şablona nasıl iletildiğini, yani `$this->template`'e yazılarak görebilirsiniz. +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 yanıtı döndürür. Bu bir HTML sayfası, bir resim, bir XML belgesi, diskten bir dosya gönderimi, JSON veya başka bir sayfaya yönlendirme olabilir. Önemli olan, açıkça nasıl yanıt vereceğini söylemezsek (ki bu `ProductPresenter` durumudur), yanıtın bir HTML sayfasıyla şablonun oluşturulması olacağıdır. Neden? Çünkü vakaların %99'unda bir şablon oluşturmak istiyoruz, bu yüzden presenter bu davranışı varsayılan olarak kabul eder ve işimizi kolaylaştırmak ister. Nette'nin amacı budur. +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 oluşturulacağını bile belirtmemize gerek yok, yolunu kendisi türetir. `show` eylemi durumunda, `ProductPresenter` sınıfıyla aynı dizindeki `show.latte` şablonunu basitçe yüklemeye çalışır. Ayrıca `@layout.latte` dosyasındaki düzeni bulmaya çalışır ([şablonları bulma |templates#Şablonları Bulma] hakkında daha fazla bilgi). +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ı). -Ve ardından şablonları oluşturur. Böylece presenter'ın ve tüm uygulamanın görevi tamamlanır ve iş biter. Şablon mevcut değilse, 404 hata sayfası döndürülür. Presenter'lar hakkında daha fazla bilgiyi [Presenter'lar|presenters] sayfasında okuyabilirsiniz. +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` olacak -2) uygulamayı başlatırız, konteyner oluşturulur ve `Application::run()` çalıştırılır -3) yönlendirici URL'yi `Home:default` çifti olarak kodlar -4) `HomePresenter` sınıfının nesnesi oluşturulur -5) `renderDefault()` metodu çağrılır (varsa) -6) örneğin `@layout.latte` düzeniyle `default.latte` şablonu oluşturulur +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. -Şimdi birçok yeni terimle karşılaşmış olabilirsiniz, ancak anlamlı olduklarına inanıyoruz. Nette'de uygulama oluşturmak son derece keyiflidir. +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 bahsetmişken, Nette'de [Latte |latte:] şablonlama sistemi kullanılır. Bu nedenle şablonlarda `.latte` uzantıları bulunur. Latte kullanılır çünkü hem PHP için en güvenli şablonlama sistemidir hem de en sezgisel sistemdir. Çok fazla yeni şey öğrenmenize gerek yok, PHP bilginiz ve birkaç etiket yeterlidir. Her şeyi [belgelerde |templates] öğreneceksiniz. +Ş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, diğer presenter'lara ve eylemlere [bağlantılar oluşturulur |creating-links] şu şekilde: +Şablonda başka presenter'lara ve eylemlere şöyle [bağlantı oluşturursunuz |creating-links]: ```latte ürün detayı ``` -Sadece gerçek URL yerine bilinen `Presenter:action` çiftini yazın ve olası parametreleri belirtin. İşin püf noktası, bu niteliğin Nette tarafından işleneceğini söyleyen `n:href`'tir. Ve şunu oluşturur: +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 oluşturmaktan daha önce bahsedilen yönlendirici sorumludur. Aslında, Nette'deki yönlendiriciler olağanüstüdür çünkü yalnızca URL'den presenter:action çiftine dönüşümler yapmakla kalmaz, aynı zamanda tersini de yapabilirler, yani presenter adı + eylem + parametrelerden bir URL oluşturabilirler. Bu sayede Nette'de, şablonda veya presenter'da tek bir karakteri bile değiştirmeden tüm hazır uygulamanın URL şekillerini tamamen değiştirebilirsiniz. Sadece yönlendiriciyi düzenleyerek. Ayrıca bu sayede, Nette'nin başka bir benzersiz özelliği olan ve farklı URL'lerde yinelenen içeriğin varlığını otomatik olarak önleyerek daha iyi SEO'ya (arama motoru optimizasyonu) katkıda bulunan sözde kanonikleştirme çalışır. Birçok programcı bunu şaşırtıcı buluyor. +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. -Interaktif Bileşenler -===================== +Etkileşimli bileşenler +====================== -Presenter'lar hakkında size bir şey daha söylemeliyiz: yerleşik bir bileşen sistemleri vardır. Delphi veya ASP.NET Web Forms'tan aşina olanlar benzer bir şey hatırlayabilir, React veya Vue.js de uzaktan benzer bir şeye dayanmaktadır. PHP framework dünyasında bu tamamen benzersiz bir özelliktir. +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) eklediğimiz bağımsız, yeniden kullanılabilir birimlerdir. Bunlar [formlar |forms:in-presenter], [veri ızgaraları |https://componette.org/contributte/datagrid/], menüler, oylama anketleri, aslında tekrar tekrar kullanılması mantıklı olan her şey olabilir. Kendi bileşenlerimizi oluşturabilir veya [geniş yelpazedeki |https://componette.org] açık kaynaklı bileşenlerden bazılarını kullanabiliriz. +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 oluşturma yaklaşımını temelden etkiler. Size sayfaları önceden hazırlanmış birimlerden oluşturma konusunda yeni olanaklar sunarlar. Ve ayrıca [Hollywood |components#Hollywood Tarzı] ile ortak bir yanları vardır. +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 ve yapılandırma ============================= -DI konteyneri veya nesne fabrikası, tüm uygulamanın kalbidir. +DI konteyneri, yani nesne factory'si, tüm uygulamanın kalbidir. -Endişelenmeyin, önceki satırlardan görünebileceği gibi sihirli bir kara kutu değildir. Aslında, Nette tarafından oluşturulan ve önbellek dizinine kaydedilen oldukça sıkıcı bir PHP sınıfıdır. `createServiceAbcd()` gibi adlandırılmış birçok metodu vardır ve her biri belirli bir nesneyi üretebilir ve döndürebilir. Evet, uygulamayı başlatmak için `index.php` dosyasında ihtiyaç duyduğumuz `Nette\Application\Application`'ı üreten `createServiceApplication()` metodu da vardır. Ve bireysel presenter'ları üreten metotlar da vardır. Ve bu böyle devam eder. +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 nedenden dolayı servis denir. +DI konteynerinin oluşturduğu nesnelere, bir nedenle, servis denir. -Bu sınıfın gerçekten özel olan yanı, onu sizin değil, framework'ün programlamasıdır. Gerçekten PHP kodunu oluşturur ve diske kaydeder. Siz sadece konteynerin hangi nesneleri üretebileceği ve tam olarak nasıl üreteceği konusunda talimatlar verirsiniz. Ve bu talimatlar, [NEON|neon:format] formatının kullanıldığı ve dolayısıyla `.neon` uzantısına sahip olan [yapılandırma dosyalarında|bootstrapping] yazılır. +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 konteynerini bilgilendirmek için kullanılır. Yani örneğin, [session |http:configuration#Oturum Session] bölümünde `expiration: 14 days` seçeneğini belirtirsem, DI konteyneri oturumu temsil eden `Nette\Http\Session` nesnesini oluştururken onun `setExpiration('14 days')` metodunu çağırır ve böylece yapılandırma gerçeğe dönüşü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. -Nelerin [yapılandırılabileceğini |nette:configuring] ve kendi [servislerinizi nasıl tanımlayacağınızı |dependency-injection:services] açıklayan tam bir bölüm sizin için hazırlanmıştı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] kelimesiyle karşılaşırsınız. Bu, hayatınızı inanılmaz derecede basitleştiren bir özelliktir. Nesneleri ihtiyaç duyduğunuz yerlere (örneğin sınıflarınızın kurucularına) otomatik olarak iletebilir, hiçbir şey yapmanıza gerek kalmadan. Nette'deki DI konteynerinin küçük bir mucize olduğunu keşfedeceksiniz. +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. -Nereye Devam Edelim? -==================== +Sırada ne var? +============== -Nette'deki uygulamaların temel prensiplerini gözden geçirdik. Henüz çok yüzeysel, ancak yakında derinlemesine dalacak ve zamanla harika web uygulamaları oluşturacaksınız. Nereye devam etmeli? [İlk Uygulamamızı Yazıyoruz|quickstart:] eğitimini denediniz mi? +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 açıklananlara ek olarak, Nette [kullanışlı sınıflar|utils:], [veritabanı katmanı|database:] vb. içeren tam bir cephaneliğe sahiptir. Sadece belgeleri veya [blogu|https://blog.nette.org] gözden geçirmeyi deneyin. Birçok ilginç şey keşfedeceksiniz. +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'ün size bolca neşe getirmesini dileriz 💙 +Framework size bol keyif getirsin 💙 diff --git a/application/tr/multiplier.texy b/application/tr/multiplier.texy index 746642ce3e..98a05df472 100644 --- a/application/tr/multiplier.texy +++ b/application/tr/multiplier.texy @@ -1,24 +1,24 @@ -Multiplier: Dinamik Bileşenler +Multiplier: dinamik bileşenler ****************************** .[perex] -Etkileşimli bileşenlerin dinamik olarak oluşturulması için bir araç +Etkileşimli bileşenleri dinamik olarak oluşturmak için bir araç. -Tipik bir örnekten başlayalım: bir e-ticaret sitesinde ürün listemiz var ve her biri için sepete ürün eklemek için bir form yazdırmak istiyoruz. Olası seçeneklerden biri, tüm listeyi tek bir formla sarmaktır. Ancak, [api:Nette\Application\UI\Multiplier] bize çok daha uygun bir yol sunar. +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 bir fabrika tanımlamayı kolaylaştırır. İç içe geçmiş bileşenler prensibine göre çalışır - [api:Nette\ComponentModel\Container]'dan miras alan her bileşen başka bileşenler içerebilir. +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] -Belgelerdeki [bileşen modeli |components#Bileşenler Derinlemesine] bölümüne veya [Honza Tvrdík'in sunumuna|https://www.youtube.com/watch?v=8y3LLexWu-I] bakın. +Dokümantasyondaki [bileşen modeli |components#Bileşenler derinlemesine] bölümüne bakın. -Multiplier'ın özü, kurucuda iletilen bir geri çağırma (callback) kullanarak yavrularını dinamik olarak oluşturabilen bir ebeveyn konumunda hareket etmesidir. Örneğe 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('count', 'Ürün sayısı:') + $form->addInteger('amount', 'Miktar:') ->setRequired(); $form->addSubmit('send', 'Sepete ekle'); return $form; @@ -26,7 +26,7 @@ protected function createComponentShopForm(): Multiplier } ``` -Şimdi şablonda her ürün için formu kolayca oluşturabiliriz - ve her biri gerçekten benzersiz bir bileşen olacaktır. +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} @@ -37,23 +37,23 @@ protected function createComponentShopForm(): Multiplier {/foreach} ``` -`{control}` etiketinde iletilen argüman şu formatta: +`{control}` etiketinde verilen argüman şu anlama gelen bir formatı izler: -1. `shopForm` bileşenini al -2. ve ondan `$item->id` yavrusunu al +1. `shopForm` bileşenini al. +2. Ondan `$item->id` adlı çocuğu al. -**1.** noktasının ilk çağrısında `shopForm` henüz mevcut değildir, bu yüzden fabrikası `createComponentShopForm` çağrılır. Alınan bileşen (Multiplier örneği) üzerinde daha sonra belirli formun fabrikası çağrılır - ki bu, Multiplier'a kurucuda ilettiğimiz anonim fonksiyondur. +**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'in bir sonraki yinelemesinde, `createComponentShopForm` metodu artık çağrılmaz (bileşen mevcuttur), ancak farklı bir yavrusunu aradığımız için (`$item->id` her yinelemede farklı olacaktır), anonim fonksiyon tekrar çağrılır ve bize yeni bir form döndürür. +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 kalan tek şey, formun sepete gerçekten eklemesi gereken ürünü eklemesini sağlamaktır - şu anda her ürün için form tamamen aynıdır. Multiplier'ın (ve genel olarak Nette Framework'teki her bileşen fabrikasının) özelliği bize yardımcı olur, yani her fabrikanın ilk argümanı olarak oluşturulan bileşenin adını almasıdır. Bizim durumumuzda bu `$item->id` olacaktır, ki bu tam olarak ihtiyacımız olan bilgidir. Bu nedenle, form oluşturmayı hafifçe ayarlamak yeterlidir: +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 ($itemId) { + return new Multiplier(function (string $itemId) { $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Ürün sayısı:') + $form->addInteger('amount', 'Miktar:') ->setRequired(); $form->addHidden('itemId', $itemId); $form->addSubmit('send', 'Sepete ekle'); diff --git a/application/tr/presenters.texy b/application/tr/presenters.texy index 2e9d6aa932..842739248d 100644 --- a/application/tr/presenters.texy +++ b/application/tr/presenters.texy @@ -3,37 +3,37 @@ Presenter'lar
    -Nette'de presenter'ların ve şablonların nasıl yazıldığını öğreneceğiz. Okuduktan sonra şunları bileceksiniz: +Nette'te presenter'ların ve şablonların nasıl yazıldığını inceleyeceğiz. Okuduktan sonra şunları anlayacaksınız: -- presenter nasıl çalışır -- kalıcı parametreler nedir -- şablonlar nasıl oluşturulur +- presenter'ların nasıl çalıştığını +- kalıcı parametrelerin ne olduğunu +- şablonların nasıl render edildiğini
    -[Zaten biliyoruz |how-it-works#Nette Application], presenter'ın bir web uygulamasının belirli bir sayfasını temsil eden bir sınıf olduğunu, örn. ana sayfa; e-ticaretteki bir ürün; giriş formu; site haritası beslemesi vb. Bir uygulamanın bir ila binlerce presenter'ı olabilir. Diğer framework'lerde bunlara controller da denir. +[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 oluşturmak için uygun olan ve bu bölümün geri kalanında ele alacağımız [api:Nette\Application\UI\Presenter] sınıfının bir alt sınıfı kastedilir. Genel anlamda, presenter [api:Nette\Application\IPresenter] arayüzünü uygulayan herhangi bir nesnedir. +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 Yaşam Döngüsü -======================= +Presenter'ın yaşam döngüsü +========================== -Presenter'ın görevi, isteği işlemek ve bir yanıt döndürmektir (bu bir HTML sayfası, bir resim, bir yönlendirme vb. olabilir). +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 başlangıçta ona bir istek iletilir. Bu doğrudan bir HTTP isteği değil, HTTP isteğinin yönlendirici yardımıyla dönüştürüldüğü [api:Nette\Application\Request] nesnesidir. Bu nesneyle genellikle temas etmeyiz, çünkü presenter isteğin işlenmesini akıllıca şimdi göstereceğimiz diğer metotlara devreder. +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 yaşam döngüsü* .<> +[* lifecycle.svg *] *** Presenter'ın yaşam döngüsü .<> -Resim, mevcutsa yukarıdan aşağıya sırayla çağrılan metotların bir listesini temsil eder. Hiçbirinin mevcut olması gerekmez, tek bir metodu olmayan tamamen boş bir presenter'a sahip olabilir ve üzerine basit bir statik web sitesi kurabiliriz. +Ş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()` --------------- -Kurucu, nesnenin oluşturulma anında çağrıldığı için tam olarak presenter yaşam döngüsüne ait değildir. Ancak önemi nedeniyle bahsediyoruz. Kurucu ([inject metodu|best-practices:inject-method-attribute] ile birlikte) bağımlılıkları iletmek için kullanılır. +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ı işlememeli, veritabanından yazıp okumamalı, hesaplamalar yapmamalı vb. Bunun için model olarak adlandırdığımız katmandan sınıflar vardır. Örneğin, `ArticleRepository` sınıfı makaleleri yüklemek ve kaydetmekten sorumlu olabilir. Presenter'ın onunla çalışabilmesi için, onu [bağımlılık enjeksiyonu |dependency-injection:passing-dependencies] aracılığıyla iletmesini ister: +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 @@ -50,44 +50,47 @@ class ArticlePresenter extends Nette\Application\UI\Presenter `startup()` ----------- -İstek alındıktan hemen sonra `startup()` metodu çağrılır. Özellikleri başlatmak, kullanıcı izinlerini doğrulamak vb. için kullanabilirsiniz. Metodun her zaman atası `parent::startup()`'ı çağırması gerekir. +İ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()} --------------------------------------------------- +`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]. -`render()` metodunun bir benzeri. `render()` belirli bir şablon için veri hazırlamak ve ardından onu oluşturmak için tasarlanmışken, `action()`'da istek şablon oluşturmaya bağlı kalmadan işlenir. Örneğin, veriler işlenir, kullanıcı giriş yapar veya çıkış yapar vb. ve ardından [başka bir yere yönlendirilir |#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. -Önemli olan, `action()`'ın `render()`'dan önce çağrılmasıdır, bu yüzden içinde olayların sonraki seyrini değiştirebiliriz, yani oluşturulacak şablonu ve ayrıca çağrılacak `render()` metodunu değiştirebiliriz. Ve bunu `setView('jineView')` kullanarak yaparız. +.{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 parametreler iletilir. Parametrelere tür belirtmek mümkündür ve önerilir, örn. `actionShow(int $id, ?string $slug = null)` - eğer `id` parametresi eksikse veya tamsayı değilse, presenter [404 hatası |#Hata 404 ve Benzerleri] döndürür ve çalışmayı sonlandırır. +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()} +`handle(args...)` .{toc: handle()} -------------------------------------------------- -Metot, [bileşenler |components#Sinyal] bölümünde tanışacağımız sözde sinyalleri işler. Aslında özellikle bileşenler ve AJAX isteklerinin işlenmesi için tasarlanmıştır. +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()` durumunda olduğu gibi, tür kontrolü dahil olmak üzere istekten parametreler iletilir. +Metoda, `action()`'de olduğu gibi, tip denetimi dahil, istekten gelen parametreler aktarılır. `beforeRender()` ---------------- -`beforeRender` metodu, adından da anlaşılacağı gibi, her `render()` metodundan önce çağrılır. Şablonun ortak yapılandırması, layout için değişkenlerin iletilmesi vb. için kullanılır. +`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()} ----------------------------------------------- +`render(args...)` .{toc: render()} +------------------------------------------------- -Şablonu sonraki oluşturma için hazırladığımız, ona veri ilettiğimiz vb. yer. +Burada şablonu ardından render edilmek üzere hazırlar, ona veri aktarırız vb. -Metoda, `action()` durumunda olduğu gibi, tür kontrolü dahil olmak üzere istekten parametreler iletilir. +Metoda, `action()`'de olduğu gibi, tip denetimi dahil, istekten gelen parametreler aktarılır. ```php public function renderShow(int $id): void { - // modelden verileri alır ve şablona iletiriz + // model'den veriyi al ve şablona aktar $this->template->article = $this->articles->getById($id); } ``` @@ -96,61 +99,83 @@ public function renderShow(int $id): void `afterRender()` --------------- -`afterRender` metodu, adından da anlaşılacağı gibi, her `render()` metodundan sonra çağrılır. Daha çok istisnai durumlarda kullanılır. +`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 yaşam döngüsünün sonunda çağrılır. +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 iyi bir tavsiye**. Gördüğünüz gibi presenter birden fazla eylemi/view'i işleyebilir, yani birden fazla `render()` metoduna sahip olabilir. Ancak, bir veya mümkün olduğunca az eyleme sahip presenter'lar tasarlamanızı öneririz. +**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 +Yanıt gönderme ============== -Presenter'ın yanıtı genellikle [bir HTML sayfasıyla şablonun oluşturulmasıdır|templates], ancak bir dosya gönderimi, JSON veya başka bir sayfaya yönlendirme de olabilir. +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 anında, aşağıdaki metotlardan biriyle bir yanıt gönderebilir ve aynı zamanda presenter'ı sonlandırabiliriz: +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()` [yönlendirir |#Yönlendirme] -- `error()` presenter'ı [bir hata nedeniyle |#Hata 404 ve Benzerleri] sonlandırır -- `sendJson($data)` presenter'ı sonlandırır ve verileri JSON formatında [gönderir |#JSON Gönderme] -- `sendTemplate()` presenter'ı sonlandırır ve hemen [şablonu oluşturur |templates] -- `sendResponse($response)` presenter'ı sonlandırır ve [özel bir yanıt |#Yanıtlar] gönderir +- `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 metotlardan hiçbirini çağırmazsanız, presenter otomatik olarak şablonu oluşturmaya devam eder. Neden? Çünkü vakaların %99'unda bir şablon oluşturmak istiyoruz, bu yüzden presenter bu davranışı varsayılan olarak kabul eder ve işimizi kolaylaştırmak ister. +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 +Bağlantı oluşturma ================== -Presenter, diğer presenter'lara URL bağlantıları oluşturmak için kullanılabilecek `link()` metoduna sahiptir. İlk parametre hedef presenter ve eylemdir, ardından bir dizi olarak belirtilebilecek iletilen argümanlar gelir: +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' => 'tr']); +$url = $this->link('Product:show', [$id, 'lang' => 'en']); ``` -Şablonda, diğer presenter'lara ve eylemlere bağlantılar şu şekilde oluşturulur: +Şablonda başka presenter'lara ve eylemlere bağlantılar şöyle oluşturulur: ```latte ürün detayı ``` -Sadece gerçek URL yerine bilinen `Presenter:action` çiftini yazın ve olası parametreleri belirtin. İşin püf noktası, bu niteliğin Latte tarafından işleneceğini ve gerçek bir URL oluşturacağını söyleyen `n:href`'tir. Nette'de URL'leri hiç düşünmenize gerek yoktur, sadece presenter'ları ve eylemleri düşünmeniz yeterlidir. +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 bilgi için [URL Bağlantıları Oluşturma|creating-links] bölümünde bulabilirsiniz. +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, [link() |#Bağlantı Oluşturma] metoduyla çok benzer bir sözdizimine sahip olan `redirect()` ve `forward()` metotları kullanılır. +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: @@ -158,42 +183,42 @@ Başka bir presenter'a geçmek için, [link() |#Bağlantı Oluşturma] metoduyla $this->forward('Product:show'); ``` -HTTP kodu 302 (veya mevcut isteğin metodu POST ise 303) ile sözde geçici yönlendirme örneği: +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önlendirmeyi şu şekilde başarırsınız: +HTTP kodu 301 ile kalıcı yönlendirme için şunu kullanın: ```php $this->redirectPermanent('Product:show', $id); ``` -Uygulama dışındaki başka bir URL'ye `redirectUrl()` metoduyla yönlendirilebilir. İkinci parametre olarak HTTP kodu belirtilebilir, varsayılan 302'dir (veya mevcut isteğin metodu POST ise 303): +`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, sözde sessiz sonlandırma istisnası `Nette\Application\AbortException` atarak presenter'ın etkinliğini hemen sonlandırır. +Yönlendirme, sessiz sonlandırma istisnası denilen `Nette\Application\AbortException`'ı fırlatarak presenter'ın çalışmasını hemen sonlandırır. -Yönlendirmeden önce, yönlendirmeden sonra şablonda görüntülenecek olan [#flash mesajları] (geçici mesajlar) gönderilebilir. +Yönlendirmeden önce [#Flash mesajları], yani yönlendirmeden sonra şablonda gösterilecek mesajlar gönderilebilir. -Flash Mesajları +Flash mesajları =============== -Bunlar genellikle bir işlemin sonucunu bildiren mesajlardır. Flash mesajlarının önemli bir özelliği, yönlendirmeden sonra bile şablonda kullanılabilir olmalarıdır. Görüntülendikten sonra bile 30 saniye daha canlı kalırlar - örneğin, hatalı bir aktarım nedeniyle kullanıcının sayfayı yenilemesi durumunda mesaj hemen kaybolmaz. +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. -Sadece [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] metodunu çağırmanız yeterlidir ve presenter şablona iletilmesini halleder. İlk parametre mesaj metni ve isteğe bağlı ikinci parametre türüdür (error, warning, info vb.). `flashMessage()` metodu, flash mesajının bir örneğini döndürür ve buna ek bilgiler eklenebilir. +[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önlendiririz +$this->redirect(/* ... */); // ve yönlendir ``` -Şablonda bu mesajlar `$flashes` değişkeninde `stdClass` nesneleri olarak bulunur ve `message` (mesaj metni), `type` (mesaj türü) özelliklerini içerir ve daha önce bahsedilen kullanıcı bilgilerini içerebilir. Onları örneğin şu şekilde oluştururuz: +Ş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} @@ -202,10 +227,10 @@ $this->redirect(/* ... */); // ve yönlendiririz ``` -Hata 404 ve Benzerleri -====================== +Hata 404 vb. +============ -İstek yerine getirilemiyorsa, örneğin görüntülemek istediğimiz makale veritabanında mevcut olmadığı için, `error(?string $message = null, int $httpCode = 404)` metoduyla 404 hatası atarız. +İ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 @@ -218,13 +243,13 @@ public function renderShow(int $id): void } ``` -Hatanın HTTP kodu ikinci parametre olarak iletilebilir, varsayılan 404'tür. Metot, `Nette\Application\BadRequestException` istisnası atarak çalışır, bunun üzerine `Application` kontrolü error-presenter'a devreder. Bu, meydana gelen hatayı bildiren bir sayfa görüntülemekle görevli bir presenter'dır. Error-presenter ayarı [application yapılandırmasında|configuration] yapılır. +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 +JSON gönderme ============= -Verileri JSON formatında gönderen ve presenter'ı sonlandıran bir action-metodu örneği: +`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 @@ -235,15 +260,15 @@ public function actionData(): void ``` -İstek Parametreleri .{data-version:3.1.14} -========================================== +İsteğin parametreleri .{data-version:3.1.14} +============================================ -Presenter ve ayrıca her bileşen, HTTP isteğinden parametrelerini alır. Değerlerini `getParameter($name)` veya `getParameters()` metoduyla öğrenebilirsiniz. Değerler dizeler veya dize dizileridir, aslında doğrudan URL'den alınan ham verilerdir. +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 kolaylık için parametreleri özellikler aracılığıyla erişilebilir hale getirmenizi öneririz. Bunları `#[Parameter]` niteliğiyle işaretlemeniz yeterlidir: +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 önemlidir +use Nette\Application\Attributes\Parameter; // bu satır önemli class HomePresenter extends Nette\Application\UI\Presenter { @@ -252,26 +277,26 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Özellik için veri türünü (örn. `string`) belirtmenizi öneririz ve Nette değeri buna göre otomatik olarak dönüştürür. Parametre değerleri ayrıca [doğrulanabilir |#Parametre Doğrulaması]. +Ö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ı]. -Bir bağlantı oluştururken, parametrelere doğrudan değer atanabilir: +Bağlantı oluştururken parametrenin değerini doğrudan ayarlayabilirsiniz: ```latte tıkla ``` -Kalıcı Parametreler +Kalıcı parametreler =================== -Kalıcı parametreler, farklı istekler arasında durumu korumak için kullanılır. Değerleri, bir bağlantıya tıklandıktan sonra bile aynı kalır. Oturumdaki verilerin aksine, URL'de aktarılırlar. Ve bu tamamen otomatiktir, bu yüzden onları `link()` veya `n:href` içinde açıkça belirtmeye gerek yoktur. +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. -Kullanım örneği? Çok dilli bir uygulamanız var. Mevcut dil, sürekli olarak URL'nin bir parçası olması gereken bir parametredir. Ancak her bağlantıda onu belirtmek inanılmaz derecede yorucu olurdu. Bu yüzden onu kalıcı bir `lang` parametresi yaparsınız ve kendi kendine aktarılır. Harika! +Ö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'de kalıcı bir parametre oluşturmak son derece basittir. Sadece genel bir özellik oluşturmanız ve onu bir nitelikle işaretlemeniz yeterlidir: (eskiden `/** @persistent */` kullanılırdı) +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 önemlidir +use Nette\Application\Attributes\Persistent; // bu satır önemli class ProductPresenter extends Nette\Application\UI\Presenter { @@ -280,14 +305,14 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Eğer `$this->lang` değeri örneğin `'tr'` ise, `link()` veya `n:href` kullanılarak oluşturulan bağlantılar da `lang=tr` parametresini içerecektir. Ve bağlantıya tıklandıktan sonra yine `$this->lang = 'tr'` olacaktır. +`$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 türünü (örn. `string`) belirtmenizi öneririz ve varsayılan bir değer de belirtebilirsiniz. Parametre değerleri [doğrulanabilir |#Parametre Doğrulaması]. +Ö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 standart olarak söz konusu presenter'ın tüm eylemleri arasında aktarılır. Birden fazla presenter arasında da aktarılmaları için, ya: +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 miras aldığı ortak bir atada tanımlanmaları gerekir -- presenter'ların kullandığı bir trait'te tanımlanmaları gerekir: +- 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 @@ -302,42 +327,62 @@ class ProductPresenter extends Nette\Application\UI\Presenter } ``` -Bir bağlantı oluştururken, kalıcı parametrenin değeri değiştirilebilir: +Bağlantı oluştururken kalıcı parametrenin değeri değiştirilebilir: ```latte -İngilizce detay +Çekçe detay ``` -Veya *sıfırlanabilir*, yani URL'den kaldırılabilir. O zaman varsayılan değerini alacaktır: +Ya da *sıfırlanabilir*, yani URL'den kaldırılabilir. O zaman varsayılan değerini alır: ```latte tıkla ``` -İnteraktif Bileşenler +Ortak parametre alanı ===================== -Presenter'ların yerleşik bir bileşen sistemi vardır. Bileşenler, presenter'lara eklediğimiz bağımsız, yeniden kullanılabilir birimlerdir. Bunlar [formlar |forms:in-presenter], veri ızgaraları, menüler, aslında tekrar tekrar kullanılması mantıklı olan her şey olabilir. +İ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. -Bileşenler presenter'a nasıl eklenir ve ardından kullanılır? Bunu [Bileşenler |components] bölümünde öğreneceksiniz. Hatta Hollywood ile ortak yanlarının ne olduğunu bile keşfedeceksiniz. +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: -Ve bileşenleri nereden alabilirim? [Componette |https://componette.org/search/component] sayfasında, framework etrafındaki topluluktan gönüllülerin buraya yerleştirdiği açık kaynaklı bileşenleri ve Nette için bir dizi başka eklentiyi bulabilirsiniz. +```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 +====================== -Derinlemesine İnceliyoruz -========================= +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 şimdiye kadar gösterdiklerimizle muhtemelen tamamen idare edeceksiniz. Aşağıdaki satırlar, presenter'ları derinlemesine merak eden ve kesinlikle her şeyi bilmek isteyenler içindir. +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. -Parametre Doğrulaması ---------------------- +Parametrelerin doğrulanması +--------------------------- -URL'den alınan [istek parametrelerinin |#İstek Parametreleri] ve [kalıcı parametrelerin |#Kalıcı Parametreler] değerleri `loadState()` metodu tarafından özelliklere yazılır. Bu metot ayrıca özellikte belirtilen veri türünün eşleşip eşleşmediğini de kontrol eder, aksi takdirde 404 hatasıyla yanıt verir ve sayfa görüntülenmez. +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. -Parametrelere asla körü körüne güvenmeyin, çünkü kullanıcı tarafından URL'de kolayca üzerine yazılabilirler. Örneğin, `$this->lang` dilinin desteklenenler arasında olup olmadığını bu şekilde doğrularız. Uygun yol, bahsedilen `loadState()` metodunu geçersiz kılmaktır: +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 @@ -347,9 +392,9 @@ class ProductPresenter extends Nette\Application\UI\Presenter public function loadState(array $params): void { - parent::loadState($params); // burada $this->lang ayarlanır - // ardından kendi değer kontrolümüz gelir: - if (!in_array($this->lang, ['en', 'tr'])) { + parent::loadState($params); // $this->lang burada ayarlanır + // ardından kendi değer denetimimiz gelir: + if (!in_array($this->lang, ['en', 'cs'])) { $this->error(); } } @@ -357,30 +402,30 @@ class ProductPresenter extends Nette\Application\UI\Presenter ``` -İsteği Kaydetme ve Geri Yükleme -------------------------------- +İsteğin kaydedilmesi ve geri yüklenmesi +--------------------------------------- -Presenter'ın işlediği istek [api:Nette\Application\Request] nesnesidir ve presenter'ın `getRequest()` metodu tarafından döndürülür. +Presenter'ın işlediği istek, presenter'ın `getRequest()` metodunun döndürdüğü bir [api:Nette\Application\Request] nesnesidir. -Mevcut istek oturuma kaydedilebilir veya tam tersi, ondan geri yüklenebilir ve presenter'ın onu tekrar yürütmesi sağlanabilir. Bu, örneğin kullanıcı bir form doldururken ve oturumu sona erdiğinde kullanışlıdır. Verileri kaybetmemek için, giriş sayfasına yönlendirmeden önce mevcut isteği `$reqId = $this->storeRequest()` kullanarak oturuma kaydederiz, bu da kısa bir dize şeklinde kimliğini döndürür ve bunu giriş presenter'ına parametre olarak iletiriz. +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ş yaptıktan sonra, isteği oturumdan alan ve ona yönlendiren `$this->restoreRequest($reqId)` metodunu çağırırız. Metot bu arada isteği oluşturan kullanıcının şimdi giriş yapanla aynı olduğunu doğrular. Farklı bir kullanıcı giriş yaparsa veya anahtar geçersizse, hiçbir şey yapmaz ve program devam eder. +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 geri dönülür |best-practices:restore-request] kılavuzuna bakın. +[Önceki sayfaya nasıl dönülür |best-practices:restore-request] kılavuzuna bakın. Kanonikleştirme --------------- -Presenter'ların SEO'ya (arama motoru optimizasyonu) katkıda bulunan gerçekten harika bir özelliği vardır. Farklı URL'lerde yinelenen içeriğin varlığını otomatik olarak önlerler. Belirli bir hedefe birden fazla URL adresi yönlendiriyorsa, örn. `/index` ve `/index?page=1`, framework bunlardan birini birincil (kanonik) olarak belirler ve diğerlerini HTTP kodu 301 ile ona yönlendirir. Bu sayede arama motorları sayfalarınızı iki kez indekslemez ve sayfa sıralamalarını seyreltmez. +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, [yönlendirici|routing] tarafından oluşturulan URL'dir, yani genellikle koleksiyondaki ilk eşleşen rotadır. +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 açıktır ve `$this->autoCanonicalize = false` aracılığıyla kapatılabilir. +Kanonikleştirme varsayılan olarak etkindir ve `$this->autoCanonicalize = false` ile kapatılabilir. -AJAX veya POST isteği sırasında yönlendirme gerçekleşmez, çünkü veri kaybına neden olur veya SEO açısından ek bir değeri olmaz. +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 manuel olarak `canonicalize()` metoduyla da tetikleyebilirsiniz; bu metoda `link()` metoduna benzer şekilde presenter, eylem ve parametreler iletilir. Bir bağlantı oluşturur ve onu mevcut URL adresiyle karşılaştırır. Farklıysa, oluşturulan bağlantıya yönlendirir. +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 @@ -391,34 +436,16 @@ public function actionShow(int $id, ?string $slug = null): void } ``` - -Olaylar -------- - -Presenter yaşam döngüsünün bir parçası olarak çağrılan `startup()`, `beforeRender()` ve `shutdown()` metotlarına ek olarak, otomatik olarak çağrılması gereken başka fonksiyonlar da tanımlanabilir. Presenter, işleyicilerini `$onStartup`, `$onRender` ve `$onShutdown` dizilerine ekleyeceğiniz sözde [olayları |nette:glossary#Olaylar Events] tanımlar. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -`$onStartup` dizisindeki işleyiciler `startup()` metodundan hemen önce, `$onRender` `beforeRender()` ile `render()` arasında ve son olarak `$onShutdown` `shutdown()` metodundan hemen önce çağrılır. +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. Bir dizi hazır yanıt mevcuttur: +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 geri çağırma gönderir -- [api:Nette\Application\Responses\FileResponse] - bir dosya gönderir +- [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 @@ -431,42 +458,102 @@ Yanıtlar `sendResponse()` metoduyla gönderilir: use Nette\Application\Responses; // Düz metin -$this->sendResponse(new Responses\TextResponse('Merhaba Nette!')); +$this->sendResponse(new Responses\TextResponse('Hello Nette!')); -// Bir dosya gönderir +// Dosya gönderir $this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); -// Yanıt bir geri çağırma olacak +// Callback gönderir $callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Merhaba

    '; + 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));` + -`#[Requires]` ile Erişim Kısıtlaması .{data-version:3.2.2} ----------------------------------------------------------- +HTTP önbelleklemesi +------------------- -`#[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 gerektirmek, aynı kaynağa (same origin) kısıtlamak ve yalnızca yönlendirme (forwarding) yoluyla erişime izin vermek için kullanılabilir. Nitelik, hem presenter sınıflarına hem de `action()`, `render()`, `handle()` ve `createComponent()` bireysel metotlarına uygulanabilir. +`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: -Şu kısıtlamaları belirleyebilirsiniz: +```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 gerektirme: `#[Requires(ajax: true)]` -- yalnızca aynı kaynaktan erişim: `#[Requires(sameOrigin: true)]` -- yalnızca yönlendirme (forward) yoluyla erişim: `#[Requires(forward: true)]` +- 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')]` -Ayrıntılar için [#Requires Niteliği Nasıl Kullanılır |best-practices:attribute-requires] kılavuzuna bakın. +.[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 Metodu Kontrolü --------------------- -Nette'deki presenter'lar, her gelen isteğin HTTP metodunu otomatik olarak doğrular. Bu kontrolün nedeni öncelikle güvenliktir. Standart olarak `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` metotlarına izin verilir. +HTTP metodunun denetimi +----------------------- -Örneğin `OPTIONS` metoduna ek olarak izin vermek istiyorsanız, `#[Requires]` niteliğini kullanın (Nette Application v3.2'den itibaren): +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'])] @@ -475,26 +562,34 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Sürüm 3.1'de doğrulama, istekte belirtilen metodun `$presenter->allowedMethods` dizisinde bulunup bulunmadığını kontrol eden `checkHttpMethod()` içinde yapılır. Metodu şu şekilde ekleyin: +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(); - } + protected function checkHttpMethod(): void + { + $this->allowedMethods[] = 'OPTIONS'; + parent::checkHttpMethod(); + } } ``` -`OPTIONS` metoduna izin verirseniz, onu daha sonra presenter'ınız içinde uygun şekilde işlemeniz gerektiğini vurgulamak önemlidir. Metot genellikle, isteğin CORS (Cross-Origin Resource Sharing) politikası açısından izinli olup olmadığını belirlemek gerektiğinde tarayıcının gerçek istekten önce otomatik olarak gönderdiği sözde bir preflight isteği olarak kullanılır. Metoda izin verir ancak doğru yanıtı uygulamazsanız, bu tutarsızlıklara ve potansiyel güvenlik sorunlarına yol açabilir. +Ş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. -Daha Fazla Okuma -================ +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] -- [Trait'lerden Presenter Oluşturma |best-practices:presenter-traits] -- [Ayarları Presenter'lara İletme |best-practices:passing-settings-to-presenters] -- [Önceki Sayfaya Nasıl Geri Dönülür |best-practices:restore-request] +- [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 index 5b0b81e30a..bf1556e382 100644 --- a/application/tr/routing.texy +++ b/application/tr/routing.texy @@ -1,28 +1,28 @@ -Yönlendirme (Routing) -********************* +Yönlendirme +***********
    -Yönlendirici (Router), URL adresleriyle ilgili her şeyden sorumludur, böylece artık onlar hakkında düşünmek zorunda kalmazsınız. Şunları göstereceğiz: +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 olması için yönlendirici nasıl ayarlanır -- SEO ve yönlendirme hakkında konuşacağız -- ve kendi yönlendiricinizi nasıl yazacağınızı 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
    -Daha insancıl URL'ler (veya havalı ya da güzel URL'ler) daha kullanışlı, daha akılda kalıcıdır ve SEO'ya olumlu katkıda bulunurlar. Nette bunu düşünür ve geliştiricilere tam destek verir. Uygulamanız için tam olarak istediğiniz URL adresi yapısını tasarlayabilirsiniz. Hatta uygulama bittiğinde bile tasarlayabilirsiniz, çünkü kodda veya şablonlarda herhangi bir müdahale gerektirmez. Zarif bir şekilde tek bir [yerde |#Uygulamaya Dahil Etme], yönlendiricide tanımlanır ve böylece tüm presenter'lara anotasyonlar şeklinde dağılmaz. +İ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'deki yönlendirici, **çift yönlü** olmasıyla olağanüstüdür. Hem HTTP isteğindeki URL'yi çözebilir hem de bağlantılar oluşturabilir. Bu nedenle [Nette Application |how-it-works#Nette Application]'da hayati bir rol oynar, çünkü hem mevcut isteği hangi presenter ve eylemin yürüteceğine karar verir hem de şablonda vb. [URL oluşturmak |creating-links] için kullanılı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 yönlendirici yalnızca bu kullanımla sınırlı değildir, presenter'ların hiç kullanılmadığı uygulamalarda, REST API'ler için vb. kullanabilirsiniz. Daha fazla bilgi [#Bağımsız Kullanım] bölümünde. +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 +Rota koleksiyonu ================ -Uygulamadaki URL adreslerinin şeklini tanımlamanın en hoş yolu [api:Nette\Application\Routers\RouteList] sınıfını sunar. Tanım, sözde rotaların, yani URL adresi maskelerinin ve bunlarla ilişkili presenter'ların ve eylemlerin basit bir API kullanılarak bir listesinden oluşur. Rotaları herhangi bir şekilde adlandırmamız gerekmez. +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; @@ -31,16 +31,16 @@ $router->addRoute('article/', 'Article:view'); // ... ``` -Örnek, tarayıcıda `https://domain.com/rss.xml` açarsak, `Feed` presenter'ının `rss` eylemiyle görüntüleneceğini, `https://domain.com/article/12` açarsak, `Article` presenter'ının `view` eylemiyle görüntüleneceğini vb. söyler. Uygun bir rota bulunamazsa, Nette Application [BadRequestException |api:Nette\Application\BadRequestException] istisnası atarak yanıt verir, bu da kullanıcıya 404 Not Found hata sayfası olarak gösterilir. +Ö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 sırası ---------------- -Bireysel rotaların listelendiği sıra **kesinlikle anahtar** öneme sahiptir, çünkü yukarıdan aşağıya doğru sırayla değerlendirilirler. Rotaları **özgülden genele** doğru bildirme kuralı geçerlidir: +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 dizeyi olarak anlar +// 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'); @@ -49,10 +49,10 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('', 'Article:view'); ``` -Rotalar, bağlantılar oluşturulurken de yukarıdan aşağıya doğru değerlendirilir: +Bağlantı üretilirken de rotalar yukarıdan aşağıya değerlendirilir: ```php -// YANLIŞ: 'Feed:rss' bağlantısı 'admin/feed/rss' olarak oluşturulur +// YANLIŞ: 'Feed:rss' bağlantısı 'admin/feed/rss' olarak üretilir $router->addRoute('admin//', 'Admin:default'); $router->addRoute('rss.xml', 'Feed:rss'); @@ -61,54 +61,54 @@ $router->addRoute('rss.xml', 'Feed:rss'); $router->addRoute('admin//', 'Admin:default'); ``` -Doğru rota derlemesinin belirli bir beceri gerektirdiğini sizden saklamayacağız. Buna hakim olana kadar, [yönlendirme paneli |#Yönlendirici Hata Ayıklaması] sizin için yararlı bir yardımcı olacaktır. +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 ve parametreler --------------------- -Maske, web sitesinin kök dizininden göreceli yolu tanımlar. En basit maske statik bir URL'dir: +Maske, sitenin kök dizininden itibaren göreli yolu tanımlar. En basit maske statik bir URL'dir: ```php $router->addRoute('products', 'Products:default'); ``` -Genellikle maskeler sözde **parametreler** içerir. Bunlar sivri parantez içinde belirtilir (örn. ``) ve hedef presenter'a, örneğin `renderShow(int $year)` metoduna veya kalıcı parametre `$year`'a iletilir: +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, tarayıcıda `https://example.com/chronicle/2020` açarsak, `History` presenter'ının `show` eylemiyle ve `year: 2020` parametresiyle görüntüleneceğini söyler. +Örnek şunu gösterir: tarayıcıda `https://example.com/chronicle/2020` açarsak, `show` eylemiyle ve `year: 2020` parametresiyle `History` presenter'ı gösterilir. -Parametrelere doğrudan maskede varsayılan bir değer atayabiliriz ve böylece isteğe bağlı hale gelirler: +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 şimdi `https://example.com/chronicle/` URL'sini de kabul edecektir, bu da yine `History:show`'u `year: 2020` parametresiyle görüntüler. +Rota artık `https://example.com/chronicle/` URL'sini de kabul edecek ve yine `year: 2020` parametresiyle `History:show`'u gösterecektir. -Parametre elbette presenter ve eylem adı da olabilir. Örneğin şöyle: +Elbette presenter ve eylem adları da parametre olabilir. Örneğin: ```php $router->addRoute('/', 'Home:default'); ``` -Belirtilen rota, örn. `/article/edit` veya `/catalog/list` şeklindeki URL'leri kabul eder ve bunları `Article:edit` ve `Catalog:list` presenter'ları ve eylemleri olarak anlar. +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 ve dolayısıyla bunlar da isteğe bağlıdır. Yani rota, `/article` şeklindeki URL'yi de kabul eder ve onu `Article:default` olarak anlar. Veya tersi, `Product:default` bağlantısı `/product` yolunu, varsayılan `Home:default` bağlantısı `/` yolunu oluşturur. +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 web sitesinin kök dizininden göreceli yolu değil, aynı zamanda eğik çizgiyle başlıyorsa mutlak yolu veya hatta iki eğik çizgiyle başlıyorsa tüm mutlak URL'yi tanımlayabilir: +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öreceli +// document root'a göreli $router->addRoute('/', /* ... */); -// mutlak yol (alan adına göreceli) +// mutlak yol (alan adına göreli) $router->addRoute('//', /* ... */); -// alan adı dahil mutlak URL (şemaya göreceli) +// alan adı dahil mutlak URL (şemaya göreli) $router->addRoute('//.example.com//', /* ... */); // şema dahil mutlak URL @@ -116,16 +116,16 @@ $router->addRoute('https://.example.com//', /* ... */); ``` -Doğrulama İfadeleri +Doğrulama ifadeleri ------------------- -Her parametre için [düzenli ifade|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php] kullanarak bir doğrulama koşulu belirlenebilir. Örneğin, `id` parametresinin yalnızca rakamlardan oluşabileceğini `\d+` düzenli ifadesiyle belirleriz: +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 `[^/]+`'dır, yani eğik çizgi dışındaki her şey. Parametrenin eğik çizgileri de kabul etmesi gerekiyorsa, `.+` ifadesini belirtiriz: +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 @@ -133,20 +133,20 @@ $router->addRoute('', /* ... */); ``` -İsteğe Bağlı Diziler +İsteğe bağlı diziler -------------------- -Maskede isteğe bağlı bölümleri köşeli parantez kullanarak işaretleyebilirsiniz. Maskenin herhangi bir bölümü isteğe bağlı olabilir, içinde parametreler de bulunabilir: +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('[/]', /* ... */); -// Yolları kabul eder: -// /tr/download => lang => tr, name => download +// Şu yolları kabul eder: +// /en/download => lang => en, name => download // /download => lang => null, name => download ``` -Parametre isteğe bağlı bir dizinin parçası olduğunda, doğal olarak isteğe bağlı hale gelir. Varsayılan bir değeri belirtilmemişse, null olur. +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: @@ -154,7 +154,7 @@ Parametre isteğe bağlı bir dizinin parçası olduğunda, doğal olarak isteğ $router->addRoute('//[.]example.com//', /* ... */); ``` -Diziler istenildiği gibi iç içe geçirilebilir ve birleştirilebilir: +Diziler iç içe geçirilebilir ve istenildiği gibi birleştirilebilir: ```php $router->addRoute( @@ -162,60 +162,60 @@ $router->addRoute( 'Home:default', ); -// Yolları kabul eder: -// /tr/hello +// Şu yolları kabul eder: +// /en/hello // /en-us/hello // /hello // /hello/page-12 ``` -URL oluşturulurken en kısa varyant hedeflenir, bu nedenle atlanabilecek her şey atlanır. Bu yüzden örneğin `index[.html]` rotası `/index` yolunu oluşturur. Sol köşeli parantezden sonra bir ünlem işareti belirterek davranışı tersine çevirmek mümkündür: +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 oluşturur +// /hello ve /hello.html kabul eder, /hello üretir $router->addRoute('[.html]', /* ... */); -// /hello ve /hello.html kabul eder, /hello.html oluşturur +// /hello ve /hello.html kabul eder, /hello.html üretir $router->addRoute('[!.html]', /* ... */); ``` -Köşeli parantez olmadan isteğe bağlı parametreler (yani varsayılan değere sahip parametreler) aslında aşağıdaki gibi parantez içine alınmış gibi davranırlar: +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('//', /* ... */); -// şuna karşılık gelir: +// buna karşılık gelir: $router->addRoute('[/[/[]]]', /* ... */); ``` -Eğer bitiş eğik çizgisinin davranışını etkilemek isteseydik, örneğin `/home/` yerine sadece `/home` oluşturulsun, bunu şu şekilde başarabiliriz: +Sondaki eğik çizginin davranışını etkilemek, örneğin `/home/` yerine `/home` üretilmesini istemek istersek, bu şöyle sağlanabilir: ```php $router->addRoute('[[/[/]]]', /* ... */); ``` -Joker Karakterler +Joker karakterler ----------------- -Mutlak yol maskesinde aşağıdaki joker karakterleri kullanabilir ve böylece örneğin geliştirme ve üretim ortamlarında farklı olabilen alan adını maskeye yazma zorunluluğundan kaçınabiliriz: +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ı, örn. `com` veya `org` -- `%sld%` = ikinci düzey alan adı, örn. `example` -- `%domain%` = alt alan adları olmadan alan adı, örn. `example.com` -- `%host%` = tüm ana bilgisayar adı, örn. `www.example.com` +- `%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%//addRoute('//www.%sld%.%tld%/%basePath%//', /* ... */); ``` -Genişletilmiş Gösterim ----------------------- +Gelişmiş yazım +-------------- -Genellikle `Presenter:eylem` şeklinde yazılan rota hedefi, bireysel parametreleri ve varsayılan değerlerini tanımlayan bir dizi kullanılarak da yazılabilir: +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('/[/]', [ @@ -224,7 +224,7 @@ $router->addRoute('/[/]', [ ]); ``` -Daha ayrıntılı belirtim için, varsayılan değerlere ek olarak parametrelerin diğer özelliklerini, örneğin doğrulama düzenli ifadesini (bkz. `id` parametresi) ayarlayabileceğimiz daha da genişletilmiş bir form kullanılabilir: +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; @@ -242,19 +242,29 @@ $router->addRoute('/[/]', [ ]); ``` -Dizide tanımlanan parametreler yol maskesinde belirtilmemişse, değerlerinin URL'deki soru işaretinden sonra belirtilen sorgu parametreleri kullanılarak bile değiştirilemeyeceğini belirtmek önemlidir. +Ş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 +Filtreler ve çeviriler ---------------------- -Uygulamanın kaynak kodlarını İngilizce yazıyoruz, ancak web sitesinin Türkçe URL'lere sahip olması gerekiyorsa, o zaman basit yönlendirme türü: +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'); ``` -`/product/123` veya `/cart` gibi İngilizce URL'ler üretecektir. URL'deki presenter'ların ve eylemlerin Türkçe kelimelerle temsil edilmesini istiyorsak (örn. `/urun/123` veya `/sepet`), bir çeviri sözlüğü kullanabiliriz. Yazımı için zaten ikinci parametrenin "daha konuşkan" varyantına ihtiyacımız var: +İ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; @@ -264,25 +274,25 @@ $router->addRoute('/', [ Route::Value => 'Home', Route::FilterTable => [ // URL'deki dize => presenter - 'urun' => 'Product', - 'sepet' => 'Cart', + 'produkt' => 'Product', + 'kosik' => 'Cart', 'katalog' => 'Catalog', ], ], 'action' => [ Route::Value => 'default', Route::FilterTable => [ - 'liste' => 'list', + 'seznam' => 'list', ], ], ]); ``` -Çeviri sözlüğünün birden fazla anahtarı aynı presenter'a yol açabilir. Böylece ona farklı takma adlar oluşturulur. Kanonik varyant (yani oluşturulan URL'de olacak olan) olarak son anahtar kabul edilir. +Ç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 parametreye uygulanabilir. Çeviri mevcut değilse, orijinal değer alınır. Bu davranışı `Route::FilterStrict => true` ekleyerek değiştirebiliriz ve rota daha sonra değer sözlükte yoksa URL'yi reddeder. +Ç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 şeklindeki çeviri sözlüğüne ek olarak, kendi çeviri fonksiyonlarımızı da dağıtabiliriz. +Dizi biçimindeki çeviri sözlüğünün yanı sıra kendi çeviri fonksiyonlarınız da devreye sokulabilir. ```php use Nette\Routing\Route; @@ -298,15 +308,15 @@ $router->addRoute('//', [ ]); ``` -`Route::FilterIn` fonksiyonu, URL'deki parametre ile daha sonra presenter'a iletilen dize arasında dönüştürme yapar, `FilterOut` fonksiyonu ters yönde dönüştürmeyi sağlar. +`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 zaten URL'de kullanılan PascalCase veya camelCase ve kebab-case stilleri arasında dönüştürme yapan önceden tanımlanmış filtreleri vardır. Parametrelerin varsayılan değeri zaten dönüştürülmüş biçimde yazılır, bu yüzden örneğin presenter durumunda `` yazarız, `` değil. +`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 +Genel filtreler --------------- -Belirli parametrelere yönelik filtrelere ek olarak, tüm parametrelerin ilişkisel bir dizisini alan, bunları herhangi bir şekilde değiştirebilen ve sonra döndüren genel filtreler de tanımlayabiliriz. Genel filtreleri `null` anahtarı altında tanımlarız. +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; @@ -314,37 +324,39 @@ use Nette\Routing\Route; $router->addRoute('/', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], ]); ``` -Genel filtreler, rotanın davranışını kesinlikle herhangi bir şekilde değiştirme yeteneği verir. Bunları örneğin parametreleri diğer parametrelere göre değiştirmek için kullanabiliriz. Örneğin, `` ve ``'ın mevcut `` parametresinin değerine göre çevrilmesi. +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ımlanmışsa ve aynı zamanda genel bir filtre varsa, kendi `FilterIn` filtresi genel filtreden önce ve tersine genel `FilterOut` filtresi kendi filtresinden önce yürütülür. Yani genel filtre içinde, `presenter` veya `action` parametrelerinin değerleri PascalCase veya camelCase stilinde yazılır. +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. -Tek Yönlüler (OneWay) ---------------------- -Tek yönlü rotalar, uygulamanın artık oluşturmadığı ancak hala kabul ettiği eski URL'lerin işlevselliğini korumak için kullanılır. Bunları `OneWay` bayrağıyla işaretleriz: +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', $router::ONE_WAY); +$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 ([#SEO ve kanonikleştirme] bölümüne bakın). +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]). -Geri Çağrılarla Dinamik Yönlendirme ------------------------------------ +Callback'lerle dinamik yönlendirme +---------------------------------- -Geri çağırmalarla dinamik yönlendirme, rotalara doğrudan, ilgili yol ziyaret edildiğinde yürütülecek fonksiyonlar (geri çağırmalar) atamanıza olanak tanır. Bu esnek işlevsellik, uygulamanız için çeşitli uç noktaları (endpoints) hızlı ve verimli bir şekilde oluşturmanıza olanak tanır: +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 () { @@ -352,47 +364,61 @@ $router->addRoute('test', function () { }); ``` -Ayrıca maskede, geri çağırmanıza otomatik olarak iletilecek parametreler tanımlayabilirsiniz: +Maskede, callback'inize otomatik aktarılan parametreler de tanımlayabilirsiniz: ```php -$router->addRoute('', function (string $lang) { +$router->addRoute('', function (string $lang) { echo match ($lang) { - 'tr' => 'Web sitemizin Türkçe versiyonuna hoş geldiniz!', - 'en' => 'Welcome to the English version of our website!', + '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: +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') // aşağıdaki rotalar Forum modülünün bir parçasıdır +$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') // aşağıdaki rotalar Forum:Admin modülünün bir parçasıdır + ->withModule('Admin') // sonraki rotalar Forum:Admin modülünün parçasıdır ->addRoute('sign:in', 'Sign:in'); ``` -Alternatif olarak `module` parametresini kullanmaktır: +Bir alternatif de `module` parametresidir; o da sabit bir modül belirler ve onu URL'nin dışında tutar: ```php -// URL manage/dashboard/default, Admin:Dashboard presenter'ına eşlenir +// 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ı + +Alt alan adları --------------- -Rota koleksiyonlarını alt alan adlarına göre bölebiliriz: +Rota koleksiyonları alt alan adlarına göre bölünebilir: ```php $router = new RouteList; @@ -401,7 +427,7 @@ $router->withDomain('example.com') ->addRoute('/'); ``` -Alan adında [#joker karakterler] de kullanılabilir: +Alan adında [#Joker karakterler] de kullanılabilir: ```php $router = new RouteList; @@ -410,23 +436,23 @@ $router->withDomain('example.%tld%') ``` -Yol Öneki ---------- +Yol ön eki +---------- -Rota koleksiyonlarını URL'deki yola göre bölebiliriz: +Rota koleksiyonları URL'deki yola göre bölünebilir: ```php $router = new RouteList; $router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // /eshop/rss URL'sini yakalar - ->addRoute('/'); // /eshop// URL'sini yakalar + ->addRoute('rss', 'Feed:rss') // /eshop/rss URL'siyle eşleşir + ->addRoute('/'); // /eshop// URL'siyle eşleşir ``` -Kombinasyonlar --------------- +Bileşimler +---------- -Yukarıdaki bölümlemeyi birbirleriyle birleştirebiliriz: +Yukarıdaki gruplamalar birbirleriyle birleştirilebilir: ```php $router = (new RouteList) @@ -446,10 +472,10 @@ $router = (new RouteList) ``` -Sorgu Parametreleri +Sorgu parametreleri ------------------- -Maskeler ayrıca sorgu parametrelerini (URL'deki soru işaretinden sonraki parametreler) de içerebilir. Bunlar için bir doğrulama ifadesi tanımlanamaz, ancak presenter'a iletilecekleri adı değiştirebilirsiniz: +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 @@ -457,26 +483,26 @@ $router->addRoute('product ? id= & cat=', /* ... */); ``` -Foo Parametreleri +Foo parametreleri ----------------- -Şimdi daha derine iniyoruz. Foo parametreleri aslında düzenli bir ifadeyle eşleşmeyi sağlayan isimsiz parametrelerdir. Örnek olarak `/index`, `/index.html`, `/index.htm` ve `/index.php` kabul eden bir rota verilebilir: +Ş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 oluşturulurken kullanılacak dizeyi açıkça tanımlamak da mümkündür. Dize doğrudan soru işaretinden sonra yerleştirilmelidir. Aşağıdaki rota öncekine benzer, ancak `/index` yerine `/index.html` oluşturur, çünkü `.html` dizesi oluşturma değeri olarak ayarlanmıştır: +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', /* ... */); ``` -Uygulamaya Dahil Etme -===================== +Entegrasyon +=========== -Oluşturulan yönlendiriciyi uygulamaya dahil etmek için DI konteynerine ondan bahsetmeliyiz. En kolay yol, yönlendirici nesnesini üretecek bir fabrika hazırlamak ve konteyner yapılandırmasında onu kullanmasını söylemektir. Bu amaçla `App\Core\RouterFactory::createRouter()` metodunu yazdığımızı varsayalım: +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; @@ -494,14 +520,14 @@ class RouterFactory } ``` -[Yapılandırmaya |dependency-injection:services] şunu yazarız: +Sonra [yapılandırmaya |dependency-injection:services] şunu yazarız: ```neon services: - App\Core\RouterFactory::createRouter ``` -Veritabanı vb. gibi herhangi bir bağımlılık, fabrika metoduna parametreleri olarak [autowiring|dependency-injection:autowiring] kullanılarak iletilir: +Ö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 @@ -514,18 +540,18 @@ public static function createRouter(Nette\Database\Connection $db): RouteList SimpleRouter ============ -Rota koleksiyonundan çok daha basit bir yönlendirici [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]'dır. URL şekli konusunda özel gereksinimlerimiz olmadığında, `mod_rewrite` (veya alternatifleri) mevcut olmadığında veya henüz güzel URL'lerle uğraşmak istemediğimizde kullanırız. +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. -Kabaca şu şekilde adresler üretir: +Aşağı yukarı şu biçimde adresler üretir: ``` http://example.com/?presenter=Product&action=detail&id=123 ``` -SimpleRouter'ın kurucu parametresi, parametresiz bir sayfa açtığımızda, örn. `http://example.com/`, yönlendirilmesi gereken varsayılan presenter ve eylemdir. +`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' ve eylem 'default' olacak +// varsayılan presenter 'Home', eylem 'default' olacak $router = new Nette\Application\Routers\SimpleRouter('Home:default'); ``` @@ -537,22 +563,22 @@ services: ``` -SEO ve Kanonikleştirme +SEO ve kanonikleştirme ====================== -Framework, farklı URL'lerde yinelenen içeriği önleyerek SEO'ya (arama motoru optimizasyonu) katkıda bulunur. Belirli bir hedefe birden fazla adres yönlendiriyorsa, örn. `/index` ve `/index.html`, framework bunlardan ilkini birincil (kanonik) olarak belirler ve diğerlerini HTTP kodu 301 ile ona yönlendirir. Bu sayede arama motorları sayfalarınızı iki kez indekslemez ve sayfa sıralamalarını seyreltmez. +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, yönlendirici tarafından oluşturulan URL'dir, yani koleksiyondaki OneWay bayrağı olmayan ilk uygun rotadır. Bu nedenle koleksiyonda **birincil rotaları ilk olarak** belirtiriz. +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ştirme presenter tarafından yapılır, daha fazla bilgi [kanonikleştirme |presenters#Kanonikleştirme] bölümünde. +Kanonikleştirmeyi presenter gerçekleştirir; daha fazlası [kanonikleştirme |presenters#Kanonikleştirme] bölümünde. HTTPS ===== -HTTPS protokolünü kullanabilmek için barındırmada etkinleştirmek ve sunucuyu doğru şekilde yapılandırmak gerekir. +HTTPS protokolünü kullanmak için onu hosting'de etkinleştirmek ve sunucuyu doğru yapılandırmak gerekir. -Tüm web sitesinin HTTPS'ye yönlendirilmesi sunucu düzeyinde ayarlanmalıdır, örneğin uygulamamızın kök dizinindeki .htaccess dosyası kullanılarak ve HTTP kodu 301 ile. Ayar barındırmaya göre değişebilir ve kabaca şöyle görünür: +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: ``` @@ -564,40 +590,40 @@ Tüm web sitesinin HTTPS'ye yönlendirilmesi sunucu düzeyinde ayarlanmalıdır, ``` -Yönlendirici, sayfanın yüklendiği protokolle aynı protokole sahip URL'ler üretir, bu yüzden başka bir şey ayarlamaya gerek yoktur. +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ı gerekiyorsa, bunu rota maskesinde belirtiriz: +Ancak istisnai olarak farklı rotaların farklı protokoller altında çalışmasına ihtiyaç duyarsak, bunu rota maskesinde belirtiriz: ```php -// HTTP ile bir adres üretecek +// HTTP adresi üretecek $router->addRoute('http://%host%//', /* ... */); -// HTTPS ile bir adres üretecek +// HTTPS adresi üretecek $router->addRoute('https://%host%//', /* ... */); ``` -Yönlendirici Hata Ayıklaması -============================ +Router'ın hata ayıklaması +========================= -[Tracy Bar |tracy:]'da görüntülenen yönlendirme paneli, rotaların listesini ve yönlendiricinin URL'den aldığı parametreleri gösteren yararlı bir yardımcıdır. +[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. -Yeşil çubuk ve ✓ sembolü, mevcut URL'yi işleyen rotayı temsil eder; mavi renk ve ≈ sembolü, yeşil olan onları geçmeseydi URL'yi de işleyecek olan rotaları gösterir. Ayrıca mevcut presenter ve eylemi de görürüz. +✓ 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, yönlendiricinin URL'yi başlangıçta nasıl anladığını ve neden yönlendirdiğini öğrenmek için *redirect* çubuğundaki panele bakmak yararlıdır. +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] -Yönlendiriciyi hata ayıklarken, tarayıcıda Geliştirici Araçları'nı (Ctrl+Shift+I veya Cmd+Option+I) açmanızı ve Ağ panelinde önbelleği devre dışı bırakmanızı öneririz, böylece yönlendirmeler orada saklanmaz. +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ı yönlendiricinin hızını etkiler. Sayıları kesinlikle birkaç düzineyi geçmemelidir. Web sitenizin çok karmaşık bir URL yapısı varsa, özel bir [#Özel Yönlendirici] yazabilirsiniz. +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. -Yönlendiricinin örneğin veritabanı gibi herhangi bir bağımlılığı yoksa ve fabrikası herhangi bir argüman kabul etmiyorsa, derlenmiş formunu doğrudan DI konteynerine serileştirebilir ve böylece uygulamayı biraz hızlandırabiliriz. +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: @@ -605,10 +631,10 @@ routing: ``` -Özel Yönlendirici +Kendi router'ınız ================= -Aşağıdaki satırlar çok ileri düzey kullanıcılar içindir. Kendi yönlendiricinizi oluşturabilir ve onu tamamen doğal bir şekilde rota koleksiyonuna dahil edebilirsiniz. Yönlendirici, iki metoda sahip [api:Nette\Routing\Router] arayüzünün bir uygulamasıdır: +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; @@ -628,7 +654,7 @@ class MyRouter implements Nette\Routing\Router } ``` -`match` metodu, yalnızca URL'yi değil, aynı zamanda başlıkları vb. de alabileceğiniz mevcut isteği [$httpRequest |http:request] işler ve presenter adını ve parametrelerini içeren bir diziye dönüştürür. İsteği işleyemezse, null döndürür. İsteği işlerken en azından presenter ve eylemi döndürmeliyiz. Presenter adı tamdır ve olası modülleri de içerir: +`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 [ @@ -637,9 +663,9 @@ class MyRouter implements Nette\Routing\Router ] ``` -`constructUrl` metodu ise tam tersine, parametreler dizisinden sonuçta ortaya çıkan mutlak URL'yi oluşturur. Bunun için mevcut URL olan [`$refUrl`|api:Nette\Http\UrlScript] parametresindeki bilgileri kullanabilir. +`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. -`add()` kullanarak rota koleksiyonuna eklersiniz: +Onu rota koleksiyonuna `add()` ile ekleyin: ```php $router = new Nette\Application\Routers\RouteList; @@ -649,16 +675,16 @@ $router->addRoute(/* ... */); ``` -Bağımsız Kullanım +Bağımsız kullanım ================= -Bağımsız kullanım derken, Nette Application ve presenter'ları kullanmayan bir uygulamada yönlendiricinin yeteneklerini kullanmayı kastediyoruz. Bu bölümde gösterdiğimiz hemen hemen her şey onun için geçerlidir, şu farklılıklarla: +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 yönlendirici olarak [api:Nette\Routing\SimpleRouter] sınıfını kullanırız -- `Presenter:eylem` çifti olmadığı için, [#Genişletilmiş Gösterim] 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 -Yani yine bize yönlendiriciyi oluşturacak bir metot oluştururuz, örn.: +Yine router'ı bizim için kuracak bir metot oluştururuz, örneğin: ```php namespace App\Core; @@ -682,35 +708,35 @@ class RouterFactory } ``` -DI konteyneri kullanıyorsanız, ki bunu öneririz, metodu tekrar yapılandırmaya ekleriz ve ardından yönlendiriciyi HTTP isteğiyle birlikte konteynerden alırız: +Ö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); ``` -Veya nesneleri doğrudan üretiriz: +Ya da nesneleri doğrudan oluşturun: ```php $router = App\Core\RouterFactory::createRouter(); $httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); ``` -Şimdi geriye sadece yönlendiriciyi işe koymak kalıyor: +Şimdi geriye yalnızca router'ın işini yapmasına izin vermek kalıyor: ```php $params = $router->match($httpRequest); if ($params === null) { - // uygun bir rota bulunamadı, 404 hatası göndeririz + // uyan rota bulunamadı, 404 hatası gönder exit; } -// alınan parametreleri işleriz +// elde edilen parametreleri işle $controller = $params['controller']; // ... ``` -Ve tersine, bir bağlantı oluşturmak için yönlendiriciyi kullanırız: +Ve tersine, bir bağlantı kurmak için router'ı kullanın: ```php $params = ['controller' => 'ArticleController', 'id' => 123]; @@ -718,4 +744,6 @@ $url = $router->constructUrl($params, $httpRequest->getUrl()); ``` -{{composer: nette/router}} +{{composer: nette/routing}} +{{repo: nette/routing}} +{{api: https://api.nette.org/routing/}} diff --git a/application/tr/templates.texy b/application/tr/templates.texy index b335c961a8..635ca43481 100644 --- a/application/tr/templates.texy +++ b/application/tr/templates.texy @@ -2,9 +2,9 @@ ********* .[perex] -Nette, [Latte |latte:] şablonlama sistemini kullanır. Bunun nedeni, hem PHP için en güvenli şablonlama sistemi olması hem de en sezgisel sistem olmasıdır. Çok fazla yeni şey öğrenmenize gerek yoktur, PHP bilginiz ve birkaç etiket yeterlidir. +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 bir layout şablonu + ilgili eylemin şablonundan oluşması yaygındır. Örneğin bir layout şablonu şöyle görünebilir, `{block}` bloklarına ve `{include}` etiketine dikkat edin: +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 @@ -20,26 +20,26 @@ Bir sayfanın bir layout şablonu + ilgili eylemin şablonundan oluşması yayg ``` -Ve bu da eylemin şablonu olacaktır: +Eylem şablonu ise şöyle olurdu: ```latte -{block title}Ana Sayfa{/block} +{block title}Ana sayfa{/block} {block content} -

    Ana Sayfa

    +

    Ana sayfa

    ... {/block} ``` -Bu, layout'taki `{include content}` yerine eklenecek olan `content` bloğunu tanımlar ve ayrıca layout'taki `{block title}` öğesinin üzerine yazacak olan `title` bloğunu yeniden tanımlar. Sonucu hayal etmeye çalışın. +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. -Şablonları Bulma ----------------- +Şablon arama +------------ -Presenter'larda hangi şablonun oluşturulacağını belirtmeniz gerekmez, framework yolu kendisi türetir ve size yazmaktan tasarruf sağlar. +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 basitçe bu dizine eylemin (veya view'in) adıyla yerleştirin, yani `default` eylemi için `default.latte` şablonunu kullanın: +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/ @@ -49,46 +49,46 @@ app/ └── default.latte \-- -Presenter'ların birlikte tek bir dizinde ve şablonların `templates` klasöründe olduğu bir yapı kullanıyorsanız, onu ya `..latte` dosyasına ya da `/.latte` dosyasına kaydedin: +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 + ├── Home/ + │ └── default.latte ← 1. seçenek + └── Home.default.latte ← 2. seçenek \-- -`templates` dizini bir seviye yukarıda da yer alabilir, yani presenter sınıflarını içeren dizinle aynı seviyede. +`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ı |presenters#Hata 404 ve Benzerleri] ile yanıt verir. +Şablon bulunamazsa presenter [404 - sayfa bulunamadı hatasıyla |presenters#Hata 404 vb.] yanıt verir. -View'i `$this->setView('jineView')` kullanarak değiştirirsiniz. Ayrıca şablon dosyasını doğrudan `$this->template->setFile('/path/to/template.latte')` kullanarak belirtebilirsiniz. +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ı dizisini döndüren [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] metodunu geçersiz kılarak değiştirilebilir. +Ş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 Şablonunu Bulma ----------------------- +Layout şablonu arama +-------------------- -Nette ayrıca layout dosyasını otomatik olarak bulur. +Nette layout dosyasını da otomatik arar. -Her presenter'ın kendi dizinine sahip olduğu bir dizin yapısı kullanıyorsanız, layout'u ya yalnızca ona özelse presenter içeren klasöre ya da birden fazla presenter için ortaksa bir seviye yukarıya yerleştirin: +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 + ├── @layout.latte ← yalnızca Home presenter'ı için ├── HomePresenter.php └── default.latte \-- -Presenter'ların birlikte tek bir dizinde ve şablonların `templates` klasöründe olduğu bir yapı kullanıyorsanız, layout şu yerlerde beklenecektir: +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/ @@ -96,33 +96,66 @@ app/ ├── 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 + ├── 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 bulunuyorsa, modülün iç içe geçme durumuna göre daha üst dizin seviyelerinde de aranacaktır. +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 adı `$this->setLayout('layoutAdmin')` kullanılarak değiştirilebilir ve ardından `@layoutAdmin.latte` dosyasında beklenecektir. Ayrıca layout şablonu dosyasını doğrudan `$this->setLayout('/path/to/template.latte')` kullanarak belirtebilirsiniz. +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 şablon içindeki `{layout none}` etiketi kullanılarak layout araması kapatılır. +`$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ı dizisini döndüren [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] metodunu geçersiz kılarak değiştirilebilir. +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. -Şablondaki Değişkenler ----------------------- +Şablon değişkenleri +------------------- -Değişkenleri şablona `$this->template`'e yazarak iletiriz ve ardından şablonda yerel değişkenler olarak kullanılabilirler: +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); ``` -Bu şekilde herhangi bir değişkeni şablonlara kolayca iletebiliriz. Ancak sağlam uygulamalar geliştirirken kendimizi sınırlamak daha yararlı olur. Örneğin, şablonun beklediği değişkenlerin listesini ve türlerini açıkça tanımlayarak. Bu sayede PHP türleri kontrol edebilir, IDE doğru şekilde öneride bulunabilir ve statik analiz hataları ortaya çıkarabilir. +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 +--------------------- -Ve böyle bir listeyi nasıl tanımlarız? Basitçe bir sınıf ve onun özellikleri şeklinde. Onu presenter'a benzer şekilde adlandırırız, ancak sonunda `Template` ile: +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 /** @@ -141,22 +174,24 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -Presenter'daki `$this->template` nesnesi artık `ArticleTemplate` sınıfının bir örneği olacaktır. Böylece PHP yazarken bildirilen türleri kontrol edecektir. Ve PHP 8.2 sürümünden itibaren, mevcut olmayan bir değişkene yazma konusunda da uyaracaktır, önceki sürümlerde aynı şeye [Nette\SmartObject |utils:smartobject] trait'ini kullanarak ulaşılabilir. +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` ek açıklaması IDE ve statik analiz içindir, sayesinde öneri işlevi çalışacaktır, bkz. "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. +`@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 *] -Öneri lüksünün tadını şablonlarda da çıkarabilirsiniz, sadece PhpStorm'a Latte eklentisini kurmanız ve şablonun başına sınıf adını belirtmeniz yeterlidir, daha fazla bilgi için "Latte: tür sistemi nasıl kullanılır":https://blog.nette.org/tr/latte-how-to-use-type-system makalesine bakın: +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} ... ``` -Bu şekilde bileşenlerdeki şablonlar da çalışır, sadece adlandırma kuralına uymanız ve örneğin `FifteenControl` bileşeni için `FifteenTemplate` şablon sınıfını oluşturmanız yeterlidir. +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. -`$template`'i farklı bir sınıfın örneği olarak oluşturmanız gerekiyorsa, `createTemplate()` metodunu kullanın: +Farklı bir parametre sınıfı kullanmanız gerekirse `createTemplate()` metodunu kullanın: ```php public function renderDefault(): void @@ -168,58 +203,100 @@ public function renderDefault(): void } ``` +.{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: -Varsayılan Değişkenler ----------------------- - -Presenter'lar ve bileşenler, şablonlara otomatik olarak birkaç yararlı değişken iletir: - -- `$basePath`, kök dizine mutlak URL yoludur (örn. `/eshop`) -- `$baseUrl`, kök dizine mutlak URL'dir (örn. `http://localhost/eshop`) -- `$user`, [kullanıcıyı temsil eden |security:authentication] nesnedir -- `$presenter`, mevcut presenter'dır -- `$control`, mevcut bileşen veya presenter'dır -- `$flashes`, `flashMessage()` fonksiyonu tarafından gönderilen [mesajlar |presenters#Flash Mesajları] dizisidir - -Kendi şablon sınıfınızı kullanıyorsanız, bu değişkenler için bir özellik oluşturursanız iletilirler. +```php +protected function completeTemplate(Nette\Application\UI\Template $template): void +{ + parent::completeTemplate($template); + $template->siteName = 'Blogum'; +} +``` -Bağlantı Oluşturma +Bağlantı oluşturma ------------------ -Şablonda, diğer presenter'lara ve eylemlere bağlantılar şu şekilde oluşturulur: +Ş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 metinde yazdırmak istiyorsak, `{link}` kullanırız: +`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 -Adres: {link Home:default} +URL şudur: {link Home:default} ``` -Daha fazla bilgi için [URL Bağlantıları Oluşturma|creating-links] bölümünde bulabilirsiniz. +Daha fazla bilgiyi [URL bağlantıları oluşturma|creating-links] bölümünde bulabilirsiniz. -Özel Filtreler, Etiketler vb. +Özel filtreler, etiketler vb. ----------------------------- -Latte şablonlama sistemi özel filtreler, fonksiyonlar, etiketler vb. ile genişletilebilir. Bu, doğrudan `render` veya `beforeRender()` metodunda yapılabilir: +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 -public function beforeRender(): void +protected function beforeRender(): void { // filtre ekleme - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); + + // fonksiyon ekleme + $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); +} +``` + +Şablonda: - // veya doğrudan Latte\Engine nesnesini yapılandırırız +```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->addFilterLoader(/* ... */); + $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 sürüm 3, her web projesi için bir [extension |latte:extending-latte#Latte Extension] oluşturarak daha gelişmiş bir yol sunar. Böyle bir sınıfın kaba bir örneği: +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; @@ -251,11 +328,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -Onu [yapılandırma |configuration#Latte Şablonları] kullanarak kaydederiz: +Uzantıyı [yapılandırma |configuration#Latte şablonları] üzerinden kaydedin: ```neon latte: @@ -263,13 +345,27 @@ latte: - 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, muhtemelen şablondaki bazı metinleri farklı dillerde yazdırmanız gerekecektir. Nette Framework bu amaçla tek bir metodu `translate()` olan [api:Nette\Localization\Translator] çeviri arayüzünü tanımlar. Bu metot, genellikle bir dize olan `$message` mesajını ve isteğe bağlı diğer parametreleri alır. Görevi, çevrilmiş dizeyi döndürmektir. Nette'de varsayılan bir uygulama yoktur, ihtiyaçlarınıza göre [Componette |https://componette.org/search/localization] adresinde bulabileceğiniz birkaç hazır çözüm arasından seçim yapabilirsiniz. Belgelerinde çevirmeni nasıl yapılandıracağınızı öğreneceksiniz. +Ç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, `setTranslator()` metoduyla [bize iletilmesini istediğimiz |dependency-injection:passing-dependencies] bir çevirmen ayarlanabilir: +Şablonlara, [aktarılmasını sağladığımız |dependency-injection:passing-dependencies] çevirmen `setTranslator()` metoduyla ayarlanabilir: ```php protected function beforeRender(): void @@ -279,7 +375,7 @@ protected function beforeRender(): void } ``` -Çevirmen alternatif olarak [yapılandırma |configuration#Latte Şablonları] kullanılarak ayarlanabilir: +Alternatif olarak çevirmen [yapılandırma |configuration#Latte şablonları] ile ayarlanabilir: ```neon latte: @@ -287,7 +383,7 @@ latte: - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) ``` -Ardından çevirmen örneğin `|translate` filtresi olarak kullanılabilir ve `translate()` metoduna iletilen ek parametreler dahil (bkz. `foo, bar`): +Sonra çevirmen örneğin bir `|translate` filtresi olarak, `translate()` metoduna aktarılan ek parametrelerle birlikte kullanılabilir (bkz. `foo, bar`): ```latte
    {='Sepet'|translate} @@ -295,7 +391,7 @@ Ardından çevirmen örneğin `|translate` filtresi olarak kullanılabilir ve `t {$item|translate, foo, bar} ``` -Veya alt çizgi etiketi olarak: +Ya da alt çizgi etiketi olarak: ```latte {_'Sepet'} @@ -303,14 +399,14 @@ Veya alt çizgi etiketi olarak: {_$item, foo, bar} ``` -Şablonun bir bölümünü çevirmek için eşli `{translate}` etiketi vardır (Latte 2.11'den itibaren, daha önce `{_}` etiketi kullanılırdı): +Ş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 standart olarak şablon oluşturulurken çalışma zamanında çağrılır. Ancak Latte sürüm 3, tüm statik metinleri şablon derlemesi sırasında çevirebilir. Bu, her dize yalnızca bir kez çevrildiği ve sonuçtaki çeviri derlenmiş forma yazıldığı için performanstan tasarruf sağlar. Önbellek dizininde, her dil için bir tane olmak üzere şablonun birden fazla derlenmiş sürümü oluşturulur. Bunun için dili ikinci parametre olarak belirtmek yeterlidir: +Ç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 @@ -320,4 +416,4 @@ protected function beforeRender(): void } ``` -Statik metin derken örneğin `{_'merhaba'}` veya `{translate}merhaba{/translate}` kastedilir. `{_$foo}` gibi statik olmayan metinler çalışma zamanında çevrilmeye devam edecektir. +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 new file mode 100644 index 0000000000..64046aa60b --- /dev/null +++ b/application/tr/upgrading.texy @@ -0,0 +1,47 @@ +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/application/uk/@home.texy b/application/uk/@home.texy deleted file mode 100644 index 5627e56f5d..0000000000 --- a/application/uk/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application є ядром фреймворку Nette, яке надає потужні інструменти для створення сучасних веб-застосунків. Воно пропонує низку виняткових функцій, які значно полегшують розробку та покращують безпеку й підтримуваність коду. - - -Встановлення ------------- - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Чому варто обрати Nette Application? ------------------------------------- - -Nette завжди був піонером у галузі веб-технологій. - -**Двосторонній роутер:** Nette має вдосконалену систему маршрутизації, яка є унікальною завдяки своїй двосторонності — вона не тільки перетворює URL-адреси на дії застосунку, але й може генерувати URL-адреси у зворотному напрямку. Це означає, що: -- Ви можете будь-коли змінити структуру URL-адрес усього застосунку без необхідності редагувати шаблони -- URL-адреси автоматично канонізуються, що покращує SEO -- Маршрутизація визначається в одному місці, а не розкидана по анотаціях - -**Компоненти та сигнали:** Вбудована система компонентів, натхненна Delphi та React.js, є абсолютно унікальною серед PHP-фреймворків: -- Дозволяє створювати багаторазові UI-елементи -- Підтримує ієрархічне складання компонентів -- Пропонує елегантну обробку AJAX-запитів за допомогою сигналів -- Багата бібліотека готових компонентів на [Componette](https://componette.org) - -**AJAX та сніпети:** Nette представив революційний спосіб роботи з AJAX ще у 2009 році, задовго до появи подібних рішень, таких як Hotwire для Ruby on Rails або Symfony UX Turbo: -- Сніпети дозволяють оновлювати лише частини сторінки без необхідності писати JavaScript -- Автоматична інтеграція з компонентною системою -- Розумна інвалідація частин сторінок -- Мінімальна кількість переданих даних - -**Інтуїтивні шаблони [Latte|latte:]:** Найбезпечніша система шаблонів для PHP з розширеними функціями: -- Автоматичний захист від XSS за допомогою контекстно-залежного екранування -- Розширюваність за допомогою власних фільтрів, функцій та тегів -- Спадкування шаблонів та сніпети для AJAX -- Відмінна підтримка PHP 8.x з системою типів - -**Dependency Injection:** Nette повністю використовує Dependency Injection: -- Автоматична передача залежностей (autowiring) -- Конфігурація за допомогою зрозумілого формату NEON -- Підтримка фабрик для компонентів - - -Основні переваги ----------------- - -- **Безпека**: Автоматичний захист від [вразливостей|nette:vulnerability-protection], таких як XSS, CSRF тощо. -- **Продуктивність**: Менше коду, більше функцій завдяки розумному дизайну -- **Налагодження**: [Tracy debugger|tracy:] з панеллю маршрутизації -- **Швидкодія**: Розумний кеш, ліниве завантаження компонентів -- **Гнучкість**: Легка зміна URL-адрес навіть після завершення розробки застосунку -- **Компоненти**: Унікальна система багаторазових UI-елементів -- **Сучасність**: Повна підтримка PHP 8.4+ та системи типів - - -Починаємо ---------- - -1. [Як працюють застосунки? |how-it-works] - Розуміння базової архітектури -2. [Presenters |presenters] - Робота з презентерами та діями -3. [Шаблони |templates] - Створення шаблонів у Latte -4. [Маршрутизація |routing] - Конфігурація URL-адрес -5. [Інтерактивні компоненти |components] - Використання компонентної системи - - -Сумісність з PHP ----------------- - -| версія | сумісна з PHP -|-----------|------------------- -| Nette Application 4.0 | PHP 8.1 – 8.4 -| Nette Application 3.2 | PHP 8.1 – 8.4 -| Nette Application 3.1 | PHP 7.2 – 8.3 -| Nette Application 3.0 | PHP 7.1 – 8.0 -| Nette Application 2.4 | PHP 5.6 – 8.0 - -Застосовується до останньої версії патчу. diff --git a/application/uk/@left-menu.texy b/application/uk/@left-menu.texy deleted file mode 100644 index 5ad8904c1a..0000000000 --- a/application/uk/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Як працюють застосунки? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Шаблони |templates] -- [Структура каталогів |directory-structure] -- [Маршрутизація |routing] -- [Створення посилань URL |creating-links] -- [Інтерактивні компоненти |components] -- [AJAX & сніпети |ajax] -- [Multiplier |Multiplier] -- [Конфігурація |configuration] - - -Додаткове читання -***************** -- [Чому варто використовувати Nette? |www:10-reasons-why-nette] -- [Встановлення |nette:installation] -- [Пишемо перший застосунок! |quickstart:] -- [Посібники та практики |best-practices:] -- [Вирішення проблем |nette:troubleshooting] diff --git a/application/uk/@meta.texy b/application/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/application/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/application/uk/ajax.texy b/application/uk/ajax.texy deleted file mode 100644 index 92d0d2837a..0000000000 --- a/application/uk/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & сніпети -************** - -
    - -В епоху сучасних веб-застосунків, де функціональність часто розподілена між сервером і браузером, AJAX є необхідним сполучним елементом. Які можливості пропонує нам Nette Framework у цій галузі? -- надсилання частин шаблону, так званих сніпетів -- передача змінних між PHP і JavaScript -- інструменти для налагодження AJAX-запитів - -
    - - -AJAX-запит -========== - -AJAX-запит, по суті, не відрізняється від класичного HTTP-запиту. Викликається presenter із певними параметрами. І від presenter'а залежить, як він реагуватиме на запит - він може повернути дані у форматі JSON, надіслати частину HTML-коду, XML-документ тощо. - -На стороні браузера ми ініціюємо AJAX-запит за допомогою функції `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // обробка відповіді -}); -``` - -На стороні сервера ми розпізнаємо AJAX-запит за допомогою методу `$httpRequest->isAjax()` сервісу [що інкапсулює HTTP-запит |http:request]. Для виявлення він використовує HTTP-заголовок `X-Requested-With`, тому важливо його надсилати. У presenter'і можна використовувати метод `$this->isAjax()`. - -Якщо ви хочете надіслати дані у форматі JSON, використовуйте метод [`sendJson()` |presenters#Надсилання відповіді]. Метод також завершує роботу presenter'а. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Якщо ви плануєте відповісти за допомогою спеціального шаблону, призначеного для AJAX, ви можете зробити це так: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Сніпети -======= - -Найпотужнішим засобом, який пропонує Nette для зв'язку сервера з клієнтом, є сніпети. Завдяки їм ви можете перетворити звичайний застосунок на AJAX-застосунок з мінімальними зусиллями та кількома рядками коду. Як це все працює, демонструє приклад Fifteen, код якого ви знайдете на [GitHub |https://github.com/nette-examples/fifteen]. - -Сніпети, або фрагменти, дозволяють оновлювати лише частини сторінки, замість того, щоб перезавантажувати всю сторінку. Це не тільки швидше та ефективніше, але й забезпечує більш комфортний користувацький досвід. Сніпети можуть нагадувати вам Hotwire для Ruby on Rails або Symfony UX Turbo. Цікаво, що Nette представило сніпети на 14 років раніше. - -Як працюють сніпети? При першому завантаженні сторінки (не AJAX-запит) завантажується вся сторінка, включно з усіма сніпетами. Коли користувач взаємодіє зі сторінкою (наприклад, натискає кнопку, надсилає форму тощо), замість завантаження всієї сторінки викликається AJAX-запит. Код у presenter'і виконує дію і вирішує, які сніпети потрібно оновити. Nette рендерить ці сніпети та надсилає їх у вигляді масиву у форматі JSON. Обробний код у браузері отримує сніпети та вставляє їх назад у сторінку. Таким чином, передається лише код змінених сніпетів, що економить пропускну здатність і прискорює завантаження порівняно з передачею вмісту всієї сторінки. - - -Naja ----- - -Для обробки сніпетів на стороні браузера використовується [бібліотека Naja |https://naja.js.org]. Її [встановіть |https://naja.js.org/#/guide/01-install-setup-naja] як пакет node.js (для використання з застосунками Webpack, Rollup, Vite, Parcel та іншими): - -```shell -npm install naja -``` - -…або безпосередньо вставте в шаблон сторінки: - -```latte - -``` - -Спочатку потрібно бібліотеку [ініціалізувати |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -Щоб перетворити звичайне посилання (сигнал) або надсилання форми на AJAX-запит, достатньо позначити відповідне посилання, форму або кнопку класом `ajax`: - -```latte -Перейти - -
    - -
    - -або - -
    - -
    -``` - - -Перемальовування сніпетів -------------------------- - -Кожен об'єкт класу [Control |components] (включно з самим Presenter'ом) відстежує, чи відбулися зміни, що вимагають його перемальовування. Для цього використовується метод `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // після входу потрібно перемалювати відповідну частину - $this->redrawControl(); - // ... -} -``` - -Nette дозволяє ще більш точно контролювати, що саме потрібно перемалювати. Згаданий метод може приймати як аргумент назву сніпета. Таким чином, можна інвалідувати (тобто: змусити перемалювати) на рівні частин шаблону. Якщо інвалідується весь компонент, то перемальовується і кожен його сніпет: - -```php -// інвалідує сніпет 'header' -$this->redrawControl('header'); -``` - - -Сніпети в Latte ---------------- - -Використання сніпетів у Latte надзвичайно просте. Щоб визначити частину шаблону як сніпет, просто оберніть її тегами `{snippet}` та `{/snippet}`: - -```latte -{snippet header} -

    Привіт ...

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

    Привіт ...

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

    Голосуйте

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

    {$item->title}

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

    Hello

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

    Homepage

    -... -{/block} -``` - -Він визначає блок `content`, який буде вставлено замість `{include content}` у layout, а також перевизначає блок `title`, який перезапише `{block title}` у layout. Спробуйте уявити результат. - - -Пошук шаблонів --------------- - -Вам не потрібно вказувати в presenter'ах, який шаблон потрібно відобразити, фреймворк сам визначить шлях і заощадить вам час на написання коду. - -Якщо ви використовуєте структуру каталогів, де кожен presenter має власний каталог, просто розмістіть шаблон у цьому каталозі під назвою дії (або view), тобто для дії `default` використовуйте шаблон `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Якщо ви використовуєте структуру, де presenter'и знаходяться разом в одному каталозі, а шаблони — у папці `templates`, збережіть його або у файлі `..latte`, або `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1-й варіант - └── Home/ - └── default.latte ← 2-й варіант -\-- - -Каталог `templates` також може знаходитись на рівень вище, тобто на тому ж рівні, що й каталог із класами presenter'ів. - -Якщо шаблон не знайдено, presenter відповість [помилкою 404 - сторінку не знайдено |presenters#Помилка 404 тощо]. - -View можна змінити за допомогою `$this->setView('іншийView')`. Також можна безпосередньо вказати файл шаблону за допомогою `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Файли, де шукаються шаблони, можна змінити, перевизначивши метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], який повертає масив можливих імен файлів. - - -Пошук шаблону layout --------------------- - -Nette також автоматично шукає файл layout. - -Якщо ви використовуєте структуру каталогів, де кожен presenter має власний каталог, розмістіть layout або в папці з presenter'ом, якщо він специфічний лише для нього, або на рівень вище, якщо він спільний для кількох presenter'ів: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← спільний layout - └── Home/ - ├── @layout.latte ← тільки для presenter'а Home - ├── HomePresenter.php - └── default.latte -\-- - -Якщо ви використовуєте структуру, де presenter'и знаходяться разом в одному каталозі, а шаблони — у папці `templates`, layout очікуватиметься в таких місцях: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← спільний layout - ├── Home.@layout.latte ← тільки для Home, 1-й варіант - └── Home/ - └── @layout.latte ← тільки для Home, 2-й варіант -\-- - -Якщо presenter знаходиться в модулі, пошук буде здійснюватися також на вищих рівнях каталогів, відповідно до вкладеності модуля. - -Назву layout можна змінити за допомогою `$this->setLayout('layoutAdmin')`, і тоді він очікуватиметься у файлі `@layoutAdmin.latte`. Також можна безпосередньо вказати файл шаблону layout за допомогою `$this->setLayout('/path/to/template.latte')`. - -За допомогою `$this->setLayout(false)` або тегу `{layout none}` всередині шаблону пошук layout вимикається. - -.[note] -Файли, де шукаються шаблони layout, можна змінити, перевизначивши метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], який повертає масив можливих імен файлів. - - -Змінні в шаблоні ----------------- - -Змінні передаються в шаблон шляхом запису їх у `$this->template`, після чого вони стають доступними в шаблоні як локальні змінні: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Таким чином, ми можемо легко передавати будь-які змінні в шаблони. Однак при розробці надійних додатків корисніше обмежити себе. Наприклад, явно визначивши перелік змінних, які очікує шаблон, та їхні типи. Завдяки цьому PHP зможе перевіряти типи, IDE правильно підказуватиме, а статичний аналіз виявлятиме помилки. - -А як визначити такий перелік? Просто у вигляді класу та його властивостей. Назвемо його подібно до presenter'а, але з `Template` на кінці: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // та інші змінні -} -``` - -Об'єкт `$this->template` у presenter'і тепер буде екземпляром класу `ArticleTemplate`. Таким чином, PHP перевірятиме оголошені типи під час запису. А починаючи з версії PHP 8.2, він також попереджатиме про запис у неіснуючу змінну; у попередніх версіях цього можна досягти за допомогою трейту [Nette\SmartObject |utils:smartobject]. - -Анотація `@property-read` призначена для IDE та статичного аналізу, завдяки їй працюватиме автодоповнення, див. "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Розкішшю автодоповнення можна насолоджуватися і в шаблонах, достатньо встановити плагін для Latte в PhpStorm та вказати на початку шаблону назву класу, більше в статті "Latte: як працювати з системою типів":https://blog.nette.org/uk/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Так само працюють і шаблони в компонентах, достатньо дотримуватися конвенції іменування і для компонента, наприклад, `FifteenControl` створити клас шаблону `FifteenTemplate`. - -Якщо вам потрібно створити `$template` як екземпляр іншого класу, використовуйте метод `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Змінні за замовчуванням ------------------------ - -Presenter'и та компоненти автоматично передають у шаблони кілька корисних змінних: - -- `$basePath` — це абсолютний URL-шлях до кореневого каталогу (наприклад, `/eshop`) -- `$baseUrl` — це абсолютний URL до кореневого каталогу (наприклад, `http://localhost/eshop`) -- `$user` — це об'єкт, [що представляє користувача |security:authentication] -- `$presenter` — це поточний presenter -- `$control` — це поточний компонент або presenter -- `$flashes` — масив [повідомлень |presenters#Flash-повідомлення], надісланих функцією `flashMessage()` - -Якщо ви використовуєте власний клас шаблону, ці змінні будуть передані, якщо ви створите для них властивості. - - -Створення посилань ------------------- - -У шаблоні посилання на інші presenter'и та дії створюються таким чином: - -```latte -деталі продукту -``` - -Атрибут `n:href` дуже зручний для HTML-тегів ``. Якщо ми хочемо вивести посилання в іншому місці, наприклад, у тексті, використовуємо `{link}`: - -```latte -Адреса: {link Home:default} -``` - -Більше інформації ви знайдете в розділі [Створення URL-посилань|creating-links]. - - -Власні фільтри, теги тощо. --------------------------- - -Систему шаблонів Latte можна розширити власними фільтрами, функціями, тегами тощо. Це можна зробити безпосередньо в методі `render` або `beforeRender()`: - -```php -public function beforeRender(): void -{ - // додавання фільтра - $this->template->addFilter('foo', /* ... */); - - // або конфігуруємо безпосередньо об'єкт Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte версії 3 пропонує більш просунутий спосіб, а саме створення [extension |latte:extending-latte#Latte Extension] для кожного веб-проекту. Приклад такого класу: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Зареєструємо його за допомогою [конфігурації |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Переклад --------- - -Якщо ви програмуєте багатомовний додаток, вам, швидше за все, знадобиться виводити деякі тексти в шаблоні різними мовами. Для цього Nette Framework визначає інтерфейс для перекладу [api:Nette\Localization\Translator], який має єдиний метод `translate()`. Він приймає повідомлення `$message`, яке зазвичай є рядком, та будь-які інші параметри. Завдання полягає в тому, щоб повернути перекладений рядок. У Nette немає реалізації за замовчуванням, ви можете вибрати відповідно до своїх потреб з кількох готових рішень, які можна знайти на [Componette |https://componette.org/search/localization]. У їхній документації ви дізнаєтеся, як налаштувати перекладач. - -Шаблонам можна встановити перекладач, який ми [передамо |dependency-injection:passing-dependencies], за допомогою методу `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Перекладач також можна налаштувати за допомогою [конфігурації |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Після цього перекладач можна використовувати, наприклад, як фільтр `|translate`, включаючи додаткові параметри, які передаються методу `translate()` (див. `foo, bar`): - -```latte -{='Кошик'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Або як тег з підкресленням: - -```latte -{_'Кошик'} -{_$item} -{_$item, foo, bar} -``` - -Для перекладу частини шаблону існує парний тег `{translate}` (з Latte 2.11, раніше використовувався тег `{_}`): - -```latte -{translate}Замовлення{/translate} -{translate foo, bar}Замовлення{/translate} -``` - -Перекладач зазвичай викликається під час виконання при рендерингу шаблону. Однак Latte версії 3 може перекладати всі статичні тексти вже під час компіляції шаблону. Це економить продуктивність, оскільки кожен рядок перекладається лише один раз, а результат перекладу записується в скомпільовану форму. У каталозі кешу таким чином створюється кілька скомпільованих версій шаблону, по одній для кожної мови. Для цього достатньо лише вказати мову як другий параметр: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Статичним текстом мається на увазі, наприклад, `{_'hello'}` або `{translate}hello{/translate}`. Нестатичні тексти, такі як `{_$foo}`, продовжуватимуть перекладатися під час виконання. diff --git a/assets/bg/@home.texy b/assets/bg/@home.texy deleted file mode 100644 index 287b4b6636..0000000000 --- a/assets/bg/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -Омръзна ли ви ръчното управление на статични файлове във вашите уеб приложения? Забравете за хардкодиране на пътища, справяне с инвалидиране на кеша или притеснения относно версиирането на файлове. Nette Assets трансформира начина, по който работите с изображения, стилови таблици, скриптове и други статични ресурси. - -- **Интелигентно версииране** гарантира, че браузърите винаги зареждат най-новите файлове -- **Автоматично откриване** на типове файлове и размери -- **Безпроблемна Latte интеграция** с интуитивни тагове -- **Гъвкава архитектура**, поддържаща файлови системи, CDN и Vite -- **Лениво зареждане** за оптимална производителност - -
    - - -Защо Nette Assets? -================== - -Работата със статични файлове често означава повтарящ се, податлив на грешки код. Ръчно конструирате URL адреси, добавяте параметри за версии за кеш изчистване и обработвате различни типове файлове по различен начин. Това води до код като: - -```latte -Logo - -``` - -С Nette Assets цялата тази сложност изчезва: - -```latte -{* Всичко автоматизирано - URL, версииране, размери *} - - - -{* Или просто *} -{asset 'css/style.css'} -``` - -Това е! Библиотеката автоматично: -- Добавя параметри за версии въз основа на времето на последна модификация на файла -- Открива размерите на изображението и ги включва в HTML -- Генерира правилния HTML елемент за всеки тип файл -- Обработва както развойна, така и продукционна среда - - -Инсталация -========== - -Инсталирайте Nette Assets с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Изисква PHP 8.1 или по-нова и работи перфектно с Nette Framework, но може да се използва и самостоятелно. - - -Първи стъпки -============ - -Nette Assets работи веднага без никаква конфигурация. Поставете статичните си файлове в директорията `www/assets/` и започнете да ги използвате: - -```latte -{* Показва изображение с автоматични размери *} -{asset 'logo.png'} - -{* Включва стилова таблица с версииране *} -{asset 'style.css'} - -{* Зарежда JavaScript модул *} -{asset 'app.js'} -``` - -За повече контрол върху генерирания HTML, използвайте атрибута `n:asset` или функцията `asset()`. - - -Как работи -========== - -Nette Assets е изграден около три основни концепции, които го правят мощен, но лесен за използване: - - -Активи - Вашите файлове стават интелигентни -------------------------------------------- - -**Актив** представлява всеки статичен файл във вашето приложение. Всеки файл става обект с полезни свойства само за четене: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Различните типове файлове предоставят различни свойства: -- **Изображения**: ширина, височина, алтернативен текст, лениво зареждане -- **Скриптове**: тип модул, хешове за цялост, crossorigin -- **Стилови таблици**: медийни заявки, цялост -- **Аудио/Видео**: продължителност, размери -- **Шрифтове**: правилно предварително зареждане с CORS - -Библиотеката автоматично открива типовете файлове и създава подходящия клас актив. - - -Мапъри - Откъде идват файловете -------------------------------- - -**Мапърът** знае как да намира файлове и да създава URL адреси за тях. Можете да имате множество мапъри за различни цели - локални файлове, CDN, облачно хранилище или инструменти за изграждане (всеки от тях има име). Вграденият `FilesystemMapper` обработва локални файлове, докато `ViteMapper` се интегрира с модерни инструменти за изграждане. - -Мапърите се дефинират в [Конфигурация |Configuration]. - - -Регистър - Вашият основен интерфейс ------------------------------------ - -**Регистърът** управлява всички мапъри и предоставя основния API: - -```php -// Инжектирайте регистъра във вашата услуга -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Вземете активи от различни мапъри -$logo = $this->assets->getAsset('images:logo.png'); // мапър 'image' -$app = $this->assets->getAsset('app:main.js'); // мапър 'app' -$style = $this->assets->getAsset('style.css'); // използва мапъра по подразбиране -``` - -Регистърът автоматично избира правилния мапър и кешира резултатите за производителност. - - -Работа с активи в PHP -===================== - -Регистърът предоставя два метода за извличане на активи: - -```php -// Хвърля Nette\Assets\AssetNotFoundException, ако файлът не съществува -$logo = $assets->getAsset('logo.png'); - -// Връща null, ако файлът не съществува -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Указване на мапъри ------------------- - -Можете изрично да изберете кой мапър да използвате: - -```php -// Използвайте мапъра по подразбиране -$file = $assets->getAsset('document.pdf'); - -// Използвайте конкретен мапър с префикс -$image = $assets->getAsset('images:photo.jpg'); - -// Използвайте конкретен мапър със синтаксис на масив -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Свойства и типове активи ------------------------- - -Всеки тип актив предоставя съответните свойства само за четене: - -```php -// Свойства на изображение -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Свойства на скрипт -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' или null - -// Свойства на аудио -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // продължителност в секунди - -// Всички активи могат да бъдат преобразувани в низ (връща URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Свойства като размери или продължителност се зареждат лениво само при достъп, поддържайки библиотеката бърза. - - -Използване на активи в Latte шаблони -==================================== - -Nette Assets предоставя интуитивна [Latte|latte:] интеграция с тагове и функции. - - -`{asset}` ---------- - -Тагът `{asset}` рендира пълни HTML елементи: - -```latte -{* Рендира: *} -{asset 'hero.jpg'} - -{* Рендира: *} -{asset 'app.js'} - -{* Рендира: *} -{asset 'style.css'} -``` - -Тагът автоматично: -- Открива типа актив и генерира подходящ HTML -- Включва версииране за кеш изчистване -- Добавя размери за изображения -- Задава правилни атрибути (тип, медия и т.н.) - -Когато се използва вътре в HTML атрибути, той извежда само URL адреса: - -```latte -
    - -``` - - -`n:asset` ---------- - -За пълен контрол върху HTML атрибутите: - -```latte -{* Атрибутът n:asset попълва src, размери и т.н. *} -Product - -{* Работи с всеки подходящ елемент *} - - - -``` - -Използвайте променливи и мапъри: - -```latte -{* Променливите работят естествено *} - - -{* Укажете мапър с къдрави скоби *} - - -{* Укажете мапър с нотация на масив *} - -``` - - -`asset()` ---------- - -За максимална гъвкавост, използвайте функцията `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Или директно *} -Logo -``` - - -Опционални активи ------------------ - -Обработвайте липсващи активи елегантно с `{asset?}`, `n:asset?` и `tryAsset()`: - -```latte -{* Опционален таг - не рендира нищо, ако активът липсва *} -{asset? 'optional-banner.jpg'} - -{* Опционален атрибут - пропуска, ако активът липсва *} -Avatar - -{* С резервен вариант *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Подобрете производителността на зареждане на страницата: - -```latte -{* Във вашата секция *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Генерира подходящи preload връзки: - -```latte - - - -``` - - -Разширени функции -================= - - -Автоматично откриване на разширения ------------------------------------ - -Автоматично обработвайте множество формати: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Опитайте по ред -``` - -Сега можете да изисквате без разширение: - -```latte -{* Намира logo.webp, logo.jpg или logo.png автоматично *} -{asset 'images:logo'} -``` - -Перфектно за прогресивно подобрение с модерни формати. - - -Интелигентно версииране ------------------------ - -Файловете автоматично се версиират въз основа на времето на модификация: - -```latte -{asset 'style.css'} -{* Изход: *} -``` - -Когато актуализирате файла, времевият печат се променя, принуждавайки опресняване на кеша на браузъра. - -Контролирайте версиирането за всеки актив: - -```php -// Деактивирайте версиирането за конкретен актив -$asset = $assets->getAsset('style.css', ['version' => false]); - -// В Latte -{asset 'style.css', version: false} -``` - - -Шрифтови активи ---------------- - -Шрифтовете получават специално отношение с правилен CORS: - -```latte -{* Правилно предварително зареждане с crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Използвайте в CSS *} - -``` - - -Персонализирани мапъри -====================== - -Създайте персонализирани мапъри за специални нужди като облачно хранилище или динамично генериране: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Регистрирайте в конфигурацията: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Използвайте като всеки друг мапър: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Методът `Helpers::createAssetFromUrl()` автоматично създава правилния тип актив въз основа на разширението на файла. - - -Допълнително четене -=================== - -- [Нетни активи: Най-накрая унифициран API за всичко - от изображения до Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/bg/@left-menu.texy b/assets/bg/@left-menu.texy deleted file mode 100644 index 5b04a76bdb..0000000000 --- a/assets/bg/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Преглед |@home] -- [Vite |vite] -- [Конфигурация |Configuration] diff --git a/assets/bg/@meta.texy b/assets/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/assets/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/assets/bg/configuration.texy b/assets/bg/configuration.texy deleted file mode 100644 index 666a7ec7a1..0000000000 --- a/assets/bg/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Конфигурация на активи -********************** - -.[perex] -Преглед на опциите за конфигурация за Nette Assets. - - -```neon -assets: - # базов път за разрешаване на относителни пътища на мапъри - basePath: ... # (string) по подразбиране е %wwwDir% - - # базов URL за разрешаване на относителни URL адреси на мапъри - baseUrl: ... # (string) по подразбиране е %baseUrl% - - # активиране на версииране на активи глобално? - versioning: ... # (bool) по подразбиране е true - - # дефинира мапъри на активи - mapping: ... # (array) по подразбиране е път 'assets' -``` - -`basePath` задава директорията на файловата система по подразбиране за разрешаване на относителни пътища в мапъри. По подразбиране използва уеб директорията (`%wwwDir%`). - -`baseUrl` задава URL префикса по подразбиране за разрешаване на относителни URL адреси в мапъри. По подразбиране използва основния URL адрес (`%baseUrl%`). - -Опцията `versioning` глобално контролира дали параметрите за версии се добавят към URL адресите на активи за изчистване на кеша. Отделните мапъри могат да презапишат тази настройка. - - -Мапъри ------- - -Мапърите могат да бъдат конфигурирани по три начина: проста нотация на низ, подробна нотация на масив или като препратка към услуга. - -Най-простият начин за дефиниране на мапър: - -```neon -assets: - mapping: - default: assets # Създава мапър на файлова система за %wwwDir%/assets/ - images: img # Създава мапър на файлова система за %wwwDir%/img/ - scripts: js # Създава мапър на файлова система за %wwwDir%/js/ -``` - -Всеки мапър създава `FilesystemMapper`, който: -- Търси файлове в `%wwwDir%/` -- Генерира URL адреси като `%baseUrl%/` -- Наследява глобалната настройка за версииране - - -За повече контрол, използвайте подробната нотация: - -```neon -assets: - mapping: - images: - # директория, където се съхраняват файловете - path: ... # (string) опционално, по подразбиране е '' - - # URL префикс за генерирани връзки - url: ... # (string) опционално, по подразбиране е path - - # активиране на версииране за този мапър? - versioning: ... # (bool) опционално, наследява глобалната настройка - - # автоматично добавяне на разширение(я) при търсене на файлове - extension: ... # (string|array) опционално, по подразбиране е null -``` - -Разбиране как се разрешават стойностите на конфигурацията: - -Разрешаване на пътя: - - Относителните пътища се разрешават от `basePath` (или `%wwwDir%`, ако `basePath` не е зададен) - - Абсолютните пътища се използват такива, каквито са - -Разрешаване на URL: - - Относителните URL адреси се разрешават от `baseUrl` (или `%baseUrl%`, ако `baseUrl` не е зададен) - - Абсолютните URL адреси (със схема или `//`) се използват такива, каквито са - - Ако `url` не е указан, той използва стойността на `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Относителен път и URL - images: - path: img # Разрешено до: /var/www/project/www/img - url: images # Разрешено до: https://example.com/assets/images - - # Абсолютен път и URL - uploads: - path: /var/shared/uploads # Използва се както е: /var/shared/uploads - url: https://cdn.example.com # Използва се както е: https://cdn.example.com - - # Указан е само пътят - styles: - path: css # Път: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Персонализирани мапъри ----------------------- - -За персонализирани мапъри, препратете или дефинирайте услуга: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Vite мапърът изисква само да добавите `type: vite`. Това е пълен списък с опции за конфигурация: - -```neon -assets: - mapping: - default: - # тип мапър (задължителен за Vite) - type: vite # (string) задължителен, трябва да е 'vite' - - # директория за изход на Vite build - path: ... # (string) опционално, по подразбиране е '' - - # URL префикс за изградени активи - url: ... # (string) опционално, по подразбиране е path - - # местоположение на Vite manifest файл - manifest: ... # (string) опционално, по подразбиране е /.vite/manifest.json - - # конфигурация на Vite dev сървър - devServer: ... # (bool|string) опционално, по подразбиране е true - - # версииране за файлове в публична директория - versioning: ... # (bool) опционално, наследява глобалната настройка - - # автоматично разширение за файлове в публична директория - extension: ... # (string|array) опционално, по подразбиране е null -``` - -Опцията `devServer` контролира как се зареждат активи по време на разработка: - -- `true` (по подразбиране) - Автоматично открива Vite dev сървъра на текущия хост и порт. Ако dev сървърът работи **и вашето приложение е в режим на отстраняване на грешки**, активите се зареждат от него с поддръжка на гореща подмяна на модули. Ако dev сървърът не работи, активите се зареждат от изградените файлове в публичната директория. -- `false` - Напълно деактивира интеграцията на dev сървъра. Активите винаги се зареждат от изградените файлове. -- Персонализиран URL (напр. `https://localhost:5173`) - Ръчно указва URL адреса на dev сървъра, включително протокол и порт. Полезно, когато dev сървърът работи на различен хост или порт. - -Опциите `versioning` и `extension` се прилагат само за файлове в публичната директория на Vite, които не се обработват от Vite. - - -Ръчна конфигурация ------------------- - -Когато не използвате Nette DI, конфигурирайте мапърите ръчно: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Добавяне на мапър на файлова система -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Добавяне на Vite мапър -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/bg/vite.texy b/assets/bg/vite.texy deleted file mode 100644 index 45c188b3e3..0000000000 --- a/assets/bg/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Vite интеграция -*************** - -
    - -Модерните JavaScript приложения изискват сложни инструменти за изграждане. Nette Assets предоставя първокласна интеграция с [Vite |https://vitejs.dev/], инструментът за изграждане на фронтенд от следващо поколение. Получете светкавично бързо развитие с Hot Module Replacement (HMR) и оптимизирани продукционни компилации без никакви проблеми с конфигурацията. - -- **Нулева конфигурация** - автоматичен мост между Vite и PHP шаблони -- **Пълно управление на зависимостите** - един таг обработва всички активи -- **Hot Module Replacement** - незабавни JavaScript и CSS актуализации -- **Оптимизирани продукционни компилации** - разделяне на кода и tree shaking - -
    - - -Nette Assets се интегрира безпроблемно с Vite, така че получавате всички тези предимства, докато пишете шаблоните си както обикновено. - - -Настройка на Vite -================= - -Нека настроим Vite стъпка по стъпка. Не се притеснявайте, ако сте нов в инструментите за изграждане - ще обясним всичко! - - -Стъпка 1: Инсталирайте Vite ---------------------------- - -Първо, инсталирайте Vite и Nette плъгина във вашия проект: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Това инсталира Vite и специален плъгин, който помага на Vite да работи перфектно с Nette. - - -Стъпка 2: Структура на проекта ------------------------------- - -Стандартният подход е да поставите изходните файлове на активи в папка `assets/` в корена на проекта, а компилираните версии в `www/assets/`: - -/--pre -web-project/ -├── assets/ ← изходни файлове (SCSS, TypeScript, изходни изображения) -│ ├── public/ ← статични файлове (копират се както са) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← основна входна точка -│ └── style.css ← вашите стилове -└── www/ ← публична директория (документен корен) - ├── assets/ ← компилираните файлове ще отидат тук - └── index.php -\-- - -Папката `assets/` съдържа вашите изходни файлове - кода, който пишете. Vite ще обработи тези файлове и ще постави компилираните версии в `www/assets/`. - - -Стъпка 3: Конфигурирайте Vite ------------------------------ - -Създайте файл `vite.config.ts` в корена на проекта. Този файл казва на Vite къде да намери вашите изходни файлове и къде да постави компилираните. - -Плъгинът Nette Vite идва с интелигентни настройки по подразбиране, които опростяват конфигурацията. Той предполага, че вашите изходни фронтенд файлове са в директорията `assets/` (опция `root`) и компилираните файлове отиват в `www/assets/` (опция `outDir`). Трябва само да укажете [Входни точки |#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Ако искате да укажете друго име на директория за изграждане на вашите активи, ще трябва да промените няколко опции: - -```js -export default defineConfig({ - root: 'assets', // основна директория на изходни активи - - build: { - outDir: '../www/assets', // къде отиват компилираните файлове - }, - - // ... друга конфигурация ... -}); -``` - -.[note] -Пътят `outDir` се счита за относителен спрямо `root`, поради което има `../` в началото. - - -Стъпка 4: Конфигурирайте Nette ------------------------------- - -Кажете на Nette Assets за Vite във вашия `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # казва на Nette да използва ViteMapper - path: assets -``` - - -Стъпка 5: Добавете скриптове ----------------------------- - -Добавете тези скриптове към вашия `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Сега можете: -- `npm run dev` - стартирайте сървър за разработка с горещо презареждане -- `npm run build` - създайте оптимизирани продукционни файлове - - -Входни точки -============ - -**Входна точка** е основният файл, от който започва вашето приложение. От този файл импортирате други файлове (CSS, JavaScript модули, изображения), създавайки дърво на зависимостите. Vite следва тези импорти и пакетира всичко заедно. - -Примерна входна точка `assets/app.js`: - -```js -// Импортиране на стилове -import './style.css' - -// Импортиране на JavaScript модули -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Инициализиране на вашето приложение -netteForms.initOnLoad(); -naja.initialize(); -``` - -В шаблона можете да вмъкнете входна точка, както следва: - -```latte -{asset 'app.js'} -``` - -Nette Assets автоматично генерира всички необходими HTML тагове - JavaScript, CSS и всякакви други зависимости. - - -Множество входни точки ----------------------- - -По-големите приложения често се нуждаят от отделни входни точки: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // публични страници - 'admin.js', // административен панел - ], - }), - ], -}); -``` - -Използвайте ги в различни шаблони: - -```latte -{* В публични страници *} -{asset 'app.js'} - -{* В административен панел *} -{asset 'admin.js'} -``` - - -Важно: Изходни срещу компилирани файлове ----------------------------------------- - -Ключово е да се разбере, че в продукция можете да зареждате само: - -1. **Входни точки**, дефинирани в `entry` -2. **Файлове от директорията `assets/public/`** - -Не можете да зареждате с `{asset}` произволни файлове от `assets/` - само активи, реферирани от JavaScript или CSS файлове. Ако вашият файл не е рефериран никъде, той няма да бъде компилиран. Ако искате да направите Vite наясно с други активи, можете да ги преместите в [Публична папка |#public folder]. - -Моля, имайте предвид, че по подразбиране Vite ще вгради всички активи, по-малки от 4KB, така че няма да можете да реферирате тези файлове директно. (Вижте [документацията на Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Това работи - това е входна точка *} -{asset 'app.js'} - -{* ✓ Това работи - това е в assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Това няма да работи - произволен файл в assets/ *} -{asset 'components/button.js'} -``` - - -Режим на разработка -=================== - -Режимът на разработка е напълно опционален, но предоставя значителни предимства, когато е активиран. Основното предимство е **Hot Module Replacement (HMR)** - вижте промените незабавно, без да губите състоянието на приложението, което прави процеса на разработка много по-плавен и бърз. - -Vite е модерен инструмент за изграждане, който прави разработката невероятно бърза. За разлика от традиционните пакетиращи инструменти, Vite обслужва вашия код директно на браузъра по време на разработка, което означава незабавен старт на сървъра, независимо колко голям е вашият проект, и светкавично бързи актуализации. - - -Стартиране на сървър за разработка ----------------------------------- - -Стартирайте сървъра за разработка: - -```shell -npm run dev -``` - -Ще видите: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Дръжте този терминал отворен, докато разработвате. - -Плъгинът Nette Vite автоматично открива кога: -1. Vite dev сървърът работи -2. Вашето Nette приложение е в режим на отстраняване на грешки - -Когато и двете условия са изпълнени, Nette Assets зарежда файлове от Vite dev сървъра вместо от компилираната директория: - -```latte -{asset 'app.js'} -{* В разработка: *} -{* В продукция: *} -``` - -Не е необходима конфигурация - просто работи! - - -Работа на различни домейни --------------------------- - -Ако вашият сървър за разработка работи на нещо различно от `localhost` (като `myapp.local`), може да срещнете проблеми с CORS (Cross-Origin Resource Sharing). CORS е функция за сигурност в уеб браузърите, която по подразбиране блокира заявки между различни домейни. Когато вашето PHP приложение работи на `myapp.local`, но Vite работи на `localhost:5173`, браузърът ги вижда като различни домейни и блокира заявките. - -Имате две опции за решаване на това: - -**Опция 1: Конфигурирайте CORS** - -Най-простото решение е да разрешите заявки от различни източници от вашето PHP приложение: - -```js -export default defineConfig({ - // ... друга конфигурация ... - - server: { - cors: { - origin: 'http://myapp.local', // URL на вашето PHP приложение - }, - }, -}); -``` -**Опция 2: Пуснете Vite на вашия домейн** - -Другото решение е да накарате Vite да работи на същия домейн като вашето PHP приложение. - -```js -export default defineConfig({ - // ... друга конфигурация ... - - server: { - host: 'myapp.local', // същото като вашето PHP приложение - }, -}); -``` - -Всъщност, дори в този случай, трябва да конфигурирате CORS, защото dev сървърът работи на същия хост, но на различен порт. Въпреки това, в този случай CORS се конфигурира автоматично от плъгина Nette Vite. - - -HTTPS разработка ----------------- - -Ако разработвате на HTTPS, имате нужда от сертификати за вашия Vite сървър за разработка. Най-лесният начин е да използвате плъгин, който генерира сертификати автоматично: - -```shell -npm install -D vite-plugin-mkcert -``` - -Ето как да го конфигурирате във `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... друга конфигурация ... - - plugins: [ - mkcert(), // генерира сертификати автоматично и активира https - nette(), - ], -}); -``` - -Имайте предвид, че ако използвате CORS конфигурацията (Опция 1 отгоре), трябва да актуализирате URL адреса на източника, за да използва `https://` вместо `http://`. - - -Продукционни компилации -======================= - -Създайте оптимизирани продукционни файлове: - -```shell -npm run build -``` - -Vite ще: -- Минифицира целия JavaScript и CSS -- Раздели кода на оптимални части -- Генерира хеширани имена на файлове за кеш-изчистване -- Създаде манифест файл за Nette Assets - -Примерен изход: - -``` -www/assets/ -├── app-4f3a2b1c.js # Вашият основен JavaScript (минифициран) -├── app-7d8e9f2a.css # Извлечен CSS (минифициран) -├── vendor-8c4b5e6d.js # Споделени зависимости -└── .vite/ - └── manifest.json # Мапиране за Nette Assets -``` - -Хешираните имена на файлове гарантират, че браузърите винаги зареждат най-новата версия. - - -Публична папка -============== - -Файловете в директорията `assets/public/` се копират в изхода без обработка: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Реферирайте ги нормално: - -```latte -{* Тези файлове се копират както са *} - - -``` - -За публични файлове можете да използвате функциите на FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Първо опитайте WebP - versioning: true # Добавете cache-busting -``` - -В конфигурацията `vite.config.ts` можете да промените публичната папка, като използвате опцията `publicDir`. - - -Динамични импорти -================= - -Vite автоматично разделя кода за оптимално зареждане. Динамичните импорти ви позволяват да зареждате код само когато е наистина необходим, намалявайки първоначалния размер на пакета: - -```js -// Зареждане на тежки компоненти при поискване -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Динамичните импорти създават отделни части, които се зареждат само когато е необходимо. Това се нарича "разделяне на кода" и е една от най-мощните функции на Vite. Когато използвате динамични импорти, Vite автоматично създава отделни JavaScript файлове за всеки динамично импортиран модул. - -Тагът `{asset 'app.js'}` **не** зарежда автоматично тези динамични части. Това е умишлено поведение - не искаме да изтегляме код, който може никога да не бъде използван. Частите се изтеглят само когато динамичният импорт бъде изпълнен. - -Въпреки това, ако знаете, че определени динамични импорти са критични и ще са необходими скоро, можете да ги предварително заредите: - -```latte -{* Основна входна точка *} -{asset 'app.js'} - -{* Предварително зареждане на критични динамични импорти *} -{preload 'components/chart.js'} -``` - -Това казва на браузъра да изтегли компонента на диаграмата във фонов режим, така че да е готов веднага, когато е необходим. - - -Поддръжка на TypeScript -======================= - -TypeScript работи веднага: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Реферирайте TypeScript файлове нормално: - -```latte -{asset 'main.ts'} -``` - -За пълна поддръжка на TypeScript, инсталирайте го: - -```shell -npm install -D typescript -``` - - -Допълнителна конфигурация на Vite -================================= - -Ето някои полезни опции за конфигурация на Vite с подробни обяснения: - -```js -export default defineConfig({ - // Основна директория, съдържаща изходни активи - root: 'assets', - - // Папка, чието съдържание се копира в изходната директория както е - // По подразбиране: 'public' (относително спрямо 'root') - publicDir: 'public', - - build: { - // Къде да се поставят компилираните файлове (относително спрямо 'root') - outDir: '../www/assets', - - // Изчистване на изходната директория преди изграждане? - // Полезно за премахване на стари файлове от предишни компилации - emptyOutDir: true, - - // Поддиректория в outDir за генерирани части и активи - // Това помага да се организира изходната структура - assetsDir: 'static', - - rollupOptions: { - // Входна(и) точка(и) - може да бъде един файл или масив от файлове - // Всяка входна точка става отделен пакет - input: [ - 'app.js', // основно приложение - 'admin.js', // административен панел - ], - }, - }, - - server: { - // Хост, към който да се свърже сървърът за разработка - // Използвайте '0.0.0.0', за да изложите на мрежата - host: 'localhost', - - // Порт за сървъра за разработка - port: 5173, - - // CORS конфигурация за заявки от различни източници - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Активиране на CSS source maps в разработка - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Това е! Вече имате модерна система за изграждане, интегрирана с Nette Assets. diff --git a/assets/cs/@home.texy b/assets/cs/@home.texy index ba230c929a..fcd04cf3cd 100644 --- a/assets/cs/@home.texy +++ b/assets/cs/@home.texy @@ -3,7 +3,7 @@ Nette Assets
    -Už vás nebaví ručně spravovat statické soubory ve vašich webových aplikacích? Zapomeňte na pevné kódování cest, ř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. +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ů @@ -17,7 +17,7 @@ Už vás nebaví ručně spravovat statické soubory ve vašich webových aplika Proč Nette Assets? ================== -Práce se statickými soubory často znamená opakující se, chybový kód. Ručně konstruujete URL, přidáváte parametry verzí pro zrušení cache a řešíte různé typy souborů odlišně. To vede ke kódu jako: +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 @@ -57,7 +57,7 @@ Vyžaduje PHP 8.1 nebo vyšší a perfektně funguje s Nette Frameworkem, ale lz První kroky =========== -Nette Assets funguje ihned po instalaci bez jakékékoli konfigurace. Umístěte své statické soubory do adresáře `www/assets/` a začněte je používat: +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 *} @@ -66,7 +66,7 @@ Nette Assets funguje ihned po instalaci bez jakékékoli konfigurace. Umístěte {* Zahrne šablonu stylů s verzováním *} {asset 'style.css'} -{* Načte JavaScript modul *} +{* Načte skript *} {asset 'app.js'} ``` @@ -79,14 +79,15 @@ 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í: -Assets – vaše soubory chytře ----------------------------- +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' @@ -96,41 +97,43 @@ 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 +- **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í +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. +**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]. +Mappery jsou definovány v [konfiguraci |configuration]. -Registry – vaše hlavní rozhraní +Registry - vaše hlavní rozhraní ------------------------------- **Registry** spravuje všechny mappery a poskytuje hlavní API: ```php -// Vstříkněte registry do vaší služby +// 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'); // 'image' mapper -$app = $this->assets->getAsset('app:main.js'); // 'app' 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 kešuje výsledky pro výkon. +Registry automaticky vybírá správný mapper a kvůli výkonu kešuje výsledky. -Práce s Assets v PHP +Práce s assety v PHP ==================== Registry poskytuje dvě metody pro získávání assetů: @@ -178,7 +181,7 @@ echo $image->mimeType; // 'image/jpeg' // Vlastnosti skriptu $script = $assets->getAsset('app.js'); -echo $script->type; // 'module' or null +echo $script->type; // null ('module' u Vite entry pointů) // Vlastnosti audia $audio = $assets->getAsset('song.mp3'); @@ -191,8 +194,11 @@ $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í Assets v Latte šablonách +Použití assetů v šablonách Latte ================================ Nette Assets poskytuje intuitivní integraci s [Latte|latte:] pomocí tagů a funkcí. @@ -207,7 +213,7 @@ Tag `{asset}` vykresluje kompletní HTML elementy: {* Vykreslí: *} {asset 'hero.jpg'} -{* Vykreslí: *} +{* Vykreslí: *} {asset 'app.js'} {* Vykreslí: *} @@ -216,11 +222,11 @@ Tag `{asset}` vykresluje kompletní HTML elementy: Tag automaticky: - Detekuje typ assetu a generuje odpovídající HTML -- Zahrnuje verzování pro zrušení cache +- 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ů vypíše pouze URL: +Při použití uvnitř HTML atributů nebo uvnitř elementů ` -``` - - -Προσαρμοσμένοι Mappers -====================== - -Δημιουργήστε προσαρμοσμένους mappers για ειδικές ανάλογες ανάγκες όπως αποθήκευση στο cloud ή δυναμική δημιουργία: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Καταχωρήστε στη διαμόρφωση: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Χρησιμοποιήστε όπως οποιονδήποτε άλλο mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Η μέθοδος `Helpers::createAssetFromUrl()` δημιουργεί αυτόματα τον σωστό τύπο asset με βάση την επέκταση αρχείου. - - -Περαιτέρω ανάγνωση -================== - -- [Nette Assets: για τα πάντα, από εικόνες έως Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/el/@left-menu.texy b/assets/el/@left-menu.texy deleted file mode 100644 index 4e74c3d9a0..0000000000 --- a/assets/el/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Ξεκινώντας |@home] -- [Vite |vite] -- [Διαμόρφωση |Configuration] diff --git a/assets/el/@meta.texy b/assets/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/assets/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/assets/el/configuration.texy b/assets/el/configuration.texy deleted file mode 100644 index 8ec9f2944c..0000000000 --- a/assets/el/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Διαμόρφωση Assets -***************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette Assets. - - -```neon -assets: - # base path for resolving relative mapper paths - basePath: ... # (string) defaults to %wwwDir% - - # base URL for resolving relative mapper URLs - baseUrl: ... # (string) defaults to %baseUrl% - - # enable asset versioning globally? - versioning: ... # (bool) defaults to true - - # defines asset mappers - mapping: ... # (array) defaults to path 'assets' -``` - -Το `basePath` ορίζει τον προεπιλεγμένο κατάλογο συστήματος αρχείων για την επίλυση σχετικών διαδρομών σε mappers. Από προεπιλογή, χρησιμοποιεί τον κατάλογο web (`%wwwDir%`). - -Το `baseUrl` ορίζει το προεπιλεγμένο πρόθεμα URL για την επίλυση σχετικών URL σε mappers. Από προεπιλογή, χρησιμοποιεί το root URL (`%baseUrl%`). - -Η επιλογή `versioning` ελέγχει καθολικά εάν προστίθενται παράμετροι έκδοσης στις διευθύνσεις URL των assets για την εκκαθάριση της κρυφής μνήμης. Οι μεμονωμένοι mappers μπορούν να παρακάμψουν αυτήν τη ρύθμιση. - - -Mappers -------- - -Οι Mappers μπορούν να διαμορφωθούν με τρεις τρόπους: απλή σύνταξη συμβολοσειράς, λεπτομερής σύνταξη πίνακα ή ως αναφορά σε μια υπηρεσία. - -Ο απλούστερος τρόπος για να ορίσετε έναν mapper: - -```neon -assets: - mapping: - default: assets # Creates filesystem mapper for %wwwDir%/assets/ - images: img # Creates filesystem mapper for %wwwDir%/img/ - scripts: js # Creates filesystem mapper for %wwwDir%/js/ -``` - -Κάθε mapper δημιουργεί έναν `FilesystemMapper` που: -- Αναζητά αρχεία στο `%wwwDir%/` -- Δημιουργεί διευθύνσεις URL όπως `%baseUrl%/` -- Κληρονομεί την καθολική ρύθμιση έκδοσης - - -Για περισσότερο έλεγχο, χρησιμοποιήστε τη λεπτομερή σύνταξη: - -```neon -assets: - mapping: - images: - # directory where files are stored - path: ... # (string) optional, defaults to '' - - # URL prefix for generated links - url: ... # (string) optional, defaults to path - - # enable versioning for this mapper? - versioning: ... # (bool) optional, inherits global setting - - # auto-add extension(s) when searching for files - extension: ... # (string|array) optional, defaults to null -``` - -Κατανόηση του τρόπου επίλυσης των τιμών διαμόρφωσης: - -Επίλυση Διαδρομής: - - Οι σχετικές διαδρομές επιλύονται από το `basePath` (ή `%wwwDir%` εάν το `basePath` δεν έχει οριστεί) - - Οι απόλυτες διαδρομές χρησιμοποιούνται ως έχουν - -Επίλυση URL: - - Οι σχετικές διευθύνσεις URL επιλύονται από το `baseUrl` (ή `%baseUrl%` εάν το `baseUrl` δεν έχει οριστεί) - - Οι απόλυτες διευθύνσεις URL (με σχήμα ή `//`) χρησιμοποιούνται ως έχουν - - Εάν το `url` δεν έχει καθοριστεί, χρησιμοποιεί την τιμή του `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relative path and URL - images: - path: img # Resolved to: /var/www/project/www/img - url: images # Resolved to: https://example.com/assets/images - - # Absolute path and URL - uploads: - path: /var/shared/uploads # Used as-is: /var/shared/uploads - url: https://cdn.example.com # Used as-is: https://cdn.example.com - - # Only path specified - styles: - path: css # Path: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Προσαρμοσμένοι Mappers ----------------------- - -Για προσαρμοσμένους mappers, αναφέρετε ή ορίστε μια υπηρεσία: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Ο Vite mapper απαιτεί μόνο να προσθέσετε `type: vite`. Αυτή είναι μια πλήρης λίστα επιλογών διαμόρφωσης: - -```neon -assets: - mapping: - default: - # mapper type (required for Vite) - type: vite # (string) required, must be 'vite' - - # Vite build output directory - path: ... # (string) optional, defaults to '' - - # URL prefix for built assets - url: ... # (string) optional, defaults to path - - # location of Vite manifest file - manifest: ... # (string) optional, defaults to /.vite/manifest.json - - # Vite dev server configuration - devServer: ... # (bool|string) optional, defaults to true - - # versioning for public directory files - versioning: ... # (bool) optional, inherits global setting - - # auto-extension for public directory files - extension: ... # (string|array) optional, defaults to null -``` - -Η επιλογή `devServer` ελέγχει τον τρόπο φόρτωσης των assets κατά την ανάπτυξη: - -- `true` (προεπιλογή) - Ανιχνεύει αυτόματα τον Vite dev server στον τρέχοντα host και port. Εάν ο dev server εκτελείται **και η εφαρμογή σας είναι σε λειτουργία debug**, τα assets φορτώνονται από αυτόν με υποστήριξη hot module replacement. Εάν ο dev server δεν εκτελείται, τα assets φορτώνονται από τα δημιουργημένα αρχεία στον δημόσιο κατάλογο. -- `false` - Απενεργοποιεί πλήρως την ενσωμάτωση του dev server. Τα assets φορτώνονται πάντα από τα δημιουργημένα αρχεία. -- Προσαρμοσμένη διεύθυνση URL (π.χ., `https://localhost:5173`) - Καθορίστε χειροκίνητα τη διεύθυνση URL του dev server συμπεριλαμβανομένου του πρωτοκόλλου και του port. Χρήσιμο όταν ο dev server εκτελείται σε διαφορετικό host ή port. - -Οι επιλογές `versioning` και `extension` ισχύουν μόνο για αρχεία στον δημόσιο κατάλογο του Vite που δεν επεξεργάζονται από το Vite. - - -Μη Αυτόματη Διαμόρφωση ----------------------- - -Όταν δεν χρησιμοποιείτε το Nette DI, διαμορφώστε τους mappers χειροκίνητα: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Add filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Add Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/el/vite.texy b/assets/el/vite.texy deleted file mode 100644 index d526f68d5e..0000000000 --- a/assets/el/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Ενσωμάτωση Vite -*************** - -
    - -Οι σύγχρονες εφαρμογές JavaScript απαιτούν εξελιγμένα εργαλεία δημιουργίας. Το Nette Assets παρέχει ενσωμάτωση πρώτης κατηγορίας με το [Vite |https://vitejs.dev/], το εργαλείο δημιουργίας frontend επόμενης γενιάς. Αποκτήστε αστραπιαία ανάπτυξη με Hot Module Replacement (HMR) και βελτιστοποιημένες εκδόσεις παραγωγής χωρίς προβλήματα διαμόρφωσης. - -- **Μηδενική διαμόρφωση** - αυτόματη γέφυρα μεταξύ Vite και προτύπων PHP -- **Πλήρης διαχείριση εξαρτήσεων** - μία ετικέτα χειρίζεται όλα τα assets -- **Hot Module Replacement** - άμεσες ενημερώσεις JavaScript και CSS -- **Βελτιστοποιημένες εκδόσεις παραγωγής** - code splitting και tree shaking - -
    - - -Το Nette Assets ενσωματώνεται απρόσκοπτα με το Vite, οπότε έχετε όλα αυτά τα οφέλη ενώ γράφετε τα πρότυπά σας ως συνήθως. - - -Ρύθμιση του Vite -================ - -Ας ρυθμίσουμε το Vite βήμα προς βήμα. Μην ανησυχείτε αν είστε νέοι στα εργαλεία δημιουργίας - θα εξηγήσουμε τα πάντα! - - -Βήμα 1: Εγκατάσταση του Vite ----------------------------- - -Πρώτα, εγκαταστήστε το Vite και το Nette plugin στο έργο σας: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Αυτό εγκαθιστά το Vite και ένα ειδικό plugin που βοηθά το Vite να λειτουργεί τέλεια με το Nette. - - -Βήμα 2: Δομή Έργου ------------------- - -Η τυπική προσέγγιση είναι να τοποθετήσετε τα αρχεία asset πηγής σε έναν φάκελο `assets/` στον ριζικό κατάλογο του έργου σας και τις μεταγλωττισμένες εκδόσεις στο `www/assets/`: - -/--pre -web-project/ -├── assets/ ← αρχεία πηγής (SCSS, TypeScript, εικόνες πηγής) -│ ├── public/ ← στατικά αρχεία (αντιγράφονται ως έχουν) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← κύριο σημείο εισόδου -│ └── style.css ← τα στυλ σας -└── www/ ← δημόσιος κατάλογος (document root) - ├── assets/ ← τα μεταγλωττισμένα αρχεία θα πάνε εδώ - └── index.php -\-- - -Ο φάκελος `assets/` περιέχει τα αρχεία πηγής σας - τον κώδικα που γράφετε. Το Vite θα επεξεργαστεί αυτά τα αρχεία και θα τοποθετήσει τις μεταγλωττισμένες εκδόσεις στο `www/assets/`. - - -Βήμα 3: Διαμόρφωση του Vite ---------------------------- - -Δημιουργήστε ένα αρχείο `vite.config.ts` στον ριζικό κατάλογο του έργου σας. Αυτό το αρχείο λέει στο Vite πού να βρει τα αρχεία πηγής σας και πού να τοποθετήσει τα μεταγλωττισμένα. - -Το Nette Vite plugin έρχεται με έξυπνες προεπιλογές που κάνουν τη διαμόρφωση απλή. Υποθέτει ότι τα αρχεία πηγής frontend βρίσκονται στον κατάλογο `assets/` (επιλογή `root`) και τα μεταγλωττισμένα αρχεία πηγαίνουν στο `www/assets/` (επιλογή `outDir`). Χρειάζεται μόνο να καθορίσετε το [σημείο εισόδου|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Εάν θέλετε να καθορίσετε άλλο όνομα καταλόγου για να δημιουργήσετε τα assets σας, θα χρειαστεί να αλλάξετε μερικές επιλογές: - -```js -export default defineConfig({ - root: 'assets', // root directory of source assets - - build: { - outDir: '../www/assets', // where compiled files go - }, - - // ... other config ... -}); -``` - -.[note] -Η διαδρομή `outDir` θεωρείται σχετική με το `root`, γι' αυτό υπάρχει το `../` στην αρχή. - - -Βήμα 4: Διαμόρφωση του Nette ----------------------------- - -Ενημερώστε το Nette Assets για το Vite στο `common.neon` σας: - -```neon -assets: - mapping: - default: - type: vite # tells Nette to use the ViteMapper - path: assets -``` - - -Βήμα 5: Προσθήκη σεναρίων -------------------------- - -Προσθέστε αυτά τα σενάρια στο `package.json` σας: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Τώρα μπορείτε: -- `npm run dev` - εκκίνηση του development server με hot reloading -- `npm run build` - δημιουργία βελτιστοποιημένων αρχείων παραγωγής - - -Σημεία Εισόδου -============== - -Ένα **σημείο εισόδου** είναι το κύριο αρχείο από όπου ξεκινά η εφαρμογή σας. Από αυτό το αρχείο, εισάγετε άλλα αρχεία (CSS, μονάδες JavaScript, εικόνες), δημιουργώντας ένα δέντρο εξαρτήσεων. Το Vite ακολουθεί αυτές τις εισαγωγές και ομαδοποιεί τα πάντα μαζί. - -Παράδειγμα σημείου εισόδου `assets/app.js`: - -```js -// Import styles -import './style.css' - -// Import JavaScript modules -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Initialize your application -netteForms.initOnLoad(); -naja.initialize(); -``` - -Στο πρότυπο μπορείτε να εισάγετε ένα σημείο εισόδου ως εξής: - -```latte -{asset 'app.js'} -``` - -Το Nette Assets δημιουργεί αυτόματα όλες τις απαραίτητες ετικέτες HTML - JavaScript, CSS και οποιεσδήποτε άλλες εξαρτήσεις. - - -Πολλαπλά Σημεία Εισόδου ------------------------ - -Μεγαλύτερες εφαρμογές συχνά χρειάζονται ξεχωριστά σημεία εισόδου: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // public pages - 'admin.js', // admin panel - ], - }), - ], -}); -``` - -Χρησιμοποιήστε τα σε διαφορετικά πρότυπα: - -```latte -{* In public pages *} -{asset 'app.js'} - -{* In admin panel *} -{asset 'admin.js'} -``` - - -Σημαντικό: Αρχεία Πηγής έναντι Μεταγλωττισμένων Αρχείων -------------------------------------------------------- - -Είναι κρίσιμο να κατανοήσετε ότι στην παραγωγή μπορείτε να φορτώσετε μόνο: - -1. **Σημεία εισόδου** που ορίζονται στο `entry` -2. **Αρχεία από τον κατάλογο `assets/public/`** - -Δεν μπορείτε να φορτώσετε χρησιμοποιώντας `{asset}` αυθαίρετα αρχεία από το `assets/` - μόνο assets που αναφέρονται από αρχεία JavaScript ή CSS. Εάν το αρχείο σας δεν αναφέρεται πουθενά, δεν θα μεταγλωττιστεί. Εάν θέλετε να κάνετε το Vite να γνωρίζει άλλα assets, μπορείτε να τα μετακινήσετε στον [δημόσιο φάκελο|#public folder]. - -Λάβετε υπόψη ότι από προεπιλογή, το Vite θα ενσωματώσει όλα τα assets μικρότερα από 4KB, οπότε δεν θα μπορείτε να αναφέρετε αυτά τα αρχεία απευθείας. (Δείτε την [τεκμηρίωση του Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ This works - it's an entry point *} -{asset 'app.js'} - -{* ✓ This works - it's in assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ This won't work - random file in assets/ *} -{asset 'components/button.js'} -``` - - -Λειτουργία Ανάπτυξης -==================== - -Η λειτουργία ανάπτυξης είναι εντελώς προαιρετική, αλλά παρέχει σημαντικά οφέλη όταν είναι ενεργοποιημένη. Το κύριο πλεονέκτημα είναι το **Hot Module Replacement (HMR)** - δείτε τις αλλαγές άμεσα χωρίς να χάσετε την κατάσταση της εφαρμογής, κάνοντας την εμπειρία ανάπτυξης πολύ πιο ομαλή και ταχύτερη. - -Το Vite είναι ένα σύγχρονο εργαλείο δημιουργίας που κάνει την ανάπτυξη απίστευτα γρήγορη. Σε αντίθεση με τους παραδοσιακούς bundlers, το Vite εξυπηρετεί τον κώδικά σας απευθείας στο πρόγραμμα περιήγησης κατά την ανάπτυξη, πράγμα που σημαίνει άμεση εκκίνηση του server ανεξάρτητα από το μέγεθος του έργου σας και αστραπιαίες ενημερώσεις. - - -Εκκίνηση του Development Server -------------------------------- - -Εκτελέστε τον development server: - -```shell -npm run dev -``` - -Θα δείτε: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Κρατήστε αυτό το τερματικό ανοιχτό κατά την ανάπτυξη. - -Το Nette Vite plugin ανιχνεύει αυτόματα όταν: -1. Ο Vite dev server εκτελείται -2. Η εφαρμογή Nette σας είναι σε λειτουργία debug - -Όταν πληρούνται και οι δύο προϋποθέσεις, το Nette Assets φορτώνει αρχεία από τον Vite dev server αντί από τον μεταγλωττισμένο κατάλογο: - -```latte -{asset 'app.js'} -{* In development: *} -{* In production: *} -``` - -Δεν απαιτείται διαμόρφωση - απλά λειτουργεί! - - -Εργασία σε Διαφορετικούς Τομείς (Domains) ------------------------------------------ - -Εάν ο development server σας εκτελείται σε κάτι άλλο εκτός από το `localhost` (όπως `myapp.local`), ενδέχεται να αντιμετωπίσετε προβλήματα CORS (Cross-Origin Resource Sharing). Το CORS είναι ένα χαρακτηριστικό ασφαλείας στα προγράμματα περιήγησης ιστού που μπλοκάρει τις αιτήσεις μεταξύ διαφορετικών τομέων από προεπιλογή. Όταν η εφαρμογή PHP σας εκτελείται στο `myapp.local` αλλά το Vite εκτελείται στο `localhost:5173`, το πρόγραμμα περιήγησης τα βλέπει ως διαφορετικούς τομείς και μπλοκάρει τις αιτήσεις. - -Έχετε δύο επιλογές για να το λύσετε: - -**Επιλογή 1: Διαμόρφωση CORS** - -Η απλούστερη λύση είναι να επιτρέψετε αιτήσεις cross-origin από την εφαρμογή PHP σας: - -```js -export default defineConfig({ - // ... other config ... - - server: { - cors: { - origin: 'http://myapp.local', // your PHP app URL - }, - }, -}); -``` -**Επιλογή 2: Εκτελέστε το Vite στον τομέα σας** - -Η άλλη λύση είναι να κάνετε το Vite να εκτελείται στον ίδιο τομέα με την εφαρμογή PHP σας. - -```js -export default defineConfig({ - // ... other config ... - - server: { - host: 'myapp.local', // same as your PHP app - }, -}); -``` - -Πράγματι, ακόμη και σε αυτή την περίπτωση, πρέπει να διαμορφώσετε το CORS επειδή ο dev server εκτελείται στον ίδιο hostname αλλά σε διαφορετικό port. Ωστόσο, σε αυτή την περίπτωση, το CORS διαμορφώνεται αυτόματα από το Nette Vite plugin. - - -Ανάπτυξη HTTPS --------------- - -Εάν αναπτύσσετε σε HTTPS, χρειάζεστε πιστοποιητικά για τον Vite development server σας. Ο ευκολότερος τρόπος είναι να χρησιμοποιήσετε ένα plugin που δημιουργεί αυτόματα πιστοποιητικά: - -```shell -npm install -D vite-plugin-mkcert -``` - -Δείτε πώς να το διαμορφώσετε στο `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... other config ... - - plugins: [ - mkcert(), // generates certificates automatically and enables https - nette(), - ], -}); -``` - -Σημειώστε ότι εάν χρησιμοποιείτε τη διαμόρφωση CORS (Επιλογή 1 από παραπάνω), πρέπει να ενημερώσετε τη διεύθυνση URL προέλευσης για να χρησιμοποιήσετε `https://` αντί για `http://`. - - -Εκδόσεις Παραγωγής -================== - -Δημιουργήστε βελτιστοποιημένα αρχεία παραγωγής: - -```shell -npm run build -``` - -Το Vite θα: -- Συμπιέσει (minify) όλα τα JavaScript και CSS -- Χωρίσει τον κώδικα σε βέλτιστα τμήματα (chunks) -- Δημιουργήσει ονόματα αρχείων με hash για cache-busting -- Δημιουργήσει ένα αρχείο manifest για το Nette Assets - -Παράδειγμα εξόδου: - -``` -www/assets/ -├── app-4f3a2b1c.js # Your main JavaScript (minified) -├── app-7d8e9f2a.css # Extracted CSS (minified) -├── vendor-8c4b5e6d.js # Shared dependencies -└── .vite/ - └── manifest.json # Mapping for Nette Assets -``` - -Τα ονόματα αρχείων με hash διασφαλίζουν ότι τα προγράμματα περιήγησης φορτώνουν πάντα την τελευταία έκδοση. - - -Δημόσιος Φάκελος -================ - -Τα αρχεία στον κατάλογο `assets/public/` αντιγράφονται στην έξοδο χωρίς επεξεργασία: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Αναφερθείτε σε αυτά κανονικά: - -```latte -{* These files are copied as-is *} - - -``` - -Για δημόσια αρχεία, μπορείτε να χρησιμοποιήσετε τις λειτουργίες του FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Try WebP first - versioning: true # Add cache-busting -``` - -Στη διαμόρφωση `vite.config.ts` μπορείτε να αλλάξετε τον δημόσιο φάκελο χρησιμοποιώντας την επιλογή `publicDir`. - - -Δυναμικές Εισαγωγές -=================== - -Το Vite χωρίζει αυτόματα τον κώδικα για βέλτιστη φόρτωση. Οι δυναμικές εισαγωγές σάς επιτρέπουν να φορτώνετε κώδικα μόνο όταν είναι πραγματικά απαραίτητος, μειώνοντας το αρχικό μέγεθος του bundle: - -```js -// Load heavy components on demand -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Οι δυναμικές εισαγωγές δημιουργούν ξεχωριστά τμήματα (chunks) που φορτώνονται μόνο όταν χρειάζονται. Αυτό ονομάζεται "code splitting" και είναι μία από τις πιο ισχυρές λειτουργίες του Vite. Όταν χρησιμοποιείτε δυναμικές εισαγωγές, το Vite δημιουργεί αυτόματα ξεχωριστά αρχεία JavaScript για κάθε δυναμικά εισαγόμενη ενότητα (module). - -Η ετικέτα `{asset 'app.js'}` **δεν** προφορτώνει αυτόματα αυτά τα δυναμικά τμήματα. Αυτή είναι σκόπιμη συμπεριφορά - δεν θέλουμε να κατεβάσουμε κώδικα που μπορεί να μην χρησιμοποιηθεί ποτέ. Τα τμήματα κατεβάζονται μόνο όταν εκτελείται η δυναμική εισαγωγή. - -Ωστόσο, εάν γνωρίζετε ότι ορισμένες δυναμικές εισαγωγές είναι κρίτιμες και θα χρειαστούν σύντομα, μπορείτε να τις προφορτώσετε: - -```latte -{* Main entry point *} -{asset 'app.js'} - -{* Preload critical dynamic imports *} -{preload 'components/chart.js'} -``` - -Αυτό λέει στο πρόγραμμα περιήγησης να κατεβάσει το στοιχείο του γραφήματος στο παρασκήνιο, ώστε να είναι άμεσα διαθέσιμο όταν χρειαστεί. - - -Υποστήριξη TypeScript -===================== - -Το TypeScript λειτουργεί άμεσα: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Αναφερθείτε στα αρχεία TypeScript κανονικά: - -```latte -{asset 'main.ts'} -``` - -Για πλήρη υποστήριξη TypeScript, εγκαταστήστε το: - -```shell -npm install -D typescript -``` - - -Πρόσθετη Διαμόρφωση Vite -======================== - -Ακολουθούν ορισμένες χρήσιμες επιλογές διαμόρφωσης Vite με λεπτομερείς επεξηγήσεις: - -```js -export default defineConfig({ - // Root directory containing source assets - root: 'assets', - - // Folder whose contents are copied to output directory as-is - // Default: 'public' (relative to 'root') - publicDir: 'public', - - build: { - // Where to put compiled files (relative to 'root') - outDir: '../www/assets', - - // Empty output directory before building? - // Useful to remove old files from previous builds - emptyOutDir: true, - - // Subdirectory within outDir for generated chunks and assets - // This helps organize the output structure - assetsDir: 'static', - - rollupOptions: { - // Entry point(s) - can be a single file or array of files - // Each entry point becomes a separate bundle - input: [ - 'app.js', // main application - 'admin.js', // admin panel - ], - }, - }, - - server: { - // Host to bind the dev server to - // Use '0.0.0.0' to expose to network - host: 'localhost', - - // Port for the dev server - port: 5173, - - // CORS configuration for cross-origin requests - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Enable CSS source maps in development - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Αυτό είναι όλο! Έχετε τώρα ένα σύγχρονο σύστημα δημιουργίας ενσωματωμένο με το Nette Assets. diff --git a/assets/en/@home.texy b/assets/en/@home.texy index 4440b9a8ba..53160c7b4e 100644 --- a/assets/en/@home.texy +++ b/assets/en/@home.texy @@ -66,7 +66,7 @@ Nette Assets works out of the box with zero configuration. Place your static fil {* Include a stylesheet with versioning *} {asset 'style.css'} -{* Load a JavaScript module *} +{* Load a script *} {asset 'app.js'} ``` @@ -87,6 +87,7 @@ An **asset** represents any static file in your application. Each file becomes a ```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' @@ -96,7 +97,7 @@ 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 +- **Audio/Video**: duration, dimensions (video only) - **Fonts**: proper preloading with CORS The library automatically detects file types and creates the appropriate asset class. @@ -120,9 +121,11 @@ The **registry** manages all mappers and provides the main API: public function __construct( private Nette\Assets\Registry $assets ) {} +``` +```php // Get assets from different mappers -$logo = $this->assets->getAsset('images:logo.png'); // 'image' mapper +$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 ``` @@ -178,7 +181,7 @@ echo $image->mimeType; // 'image/jpeg' // Script properties $script = $assets->getAsset('app.js'); -echo $script->type; // 'module' or null +echo $script->type; // null ('module' for Vite entry points) // Audio properties $audio = $assets->getAsset('song.mp3'); @@ -191,6 +194,9 @@ $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 =============================== @@ -207,7 +213,7 @@ The `{asset}` tag renders complete HTML elements: {* Renders: *} {asset 'hero.jpg'} -{* Renders: *} +{* Renders: *} {asset 'app.js'} {* Renders: *} @@ -220,7 +226,7 @@ The tag automatically: - Adds dimensions for images - Sets correct attributes (type, media, etc.) -When used inside HTML attributes, it outputs just the URL: +When used inside HTML attributes or inside ` -``` - - -Egyedi mapperek -=============== - -Hozzon létre egyedi mappereket különleges igényekhez, mint például felhőtárhely vagy dinamikus generálás: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Az asset '$reference' nem található"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Regisztrálja a konfigurációban: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Használja, mint bármely más mappert: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -A `Helpers::createAssetFromUrl()` metódus automatikusan létrehozza a megfelelő asset típust a fájlkiterjesztés alapján. - - -További olvasnivalók -==================== - -- [Nette Assets: Végre egységes API a képektől a Vite-ig mindenhez |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/hu/@left-menu.texy b/assets/hu/@left-menu.texy deleted file mode 100644 index 143719af1e..0000000000 --- a/assets/hu/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Első lépések |@home] -- [Vite |vite] -- [Konfiguráció |Configuration] diff --git a/assets/hu/@meta.texy b/assets/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/assets/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/assets/hu/configuration.texy b/assets/hu/configuration.texy deleted file mode 100644 index a4c3bac847..0000000000 --- a/assets/hu/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Assets Konfiguráció -******************* - -.[perex] -A Nette Assets konfigurációs lehetőségeinek áttekintése. - - -```neon -assets: - # alapútvonal a relatív mapper útvonalak feloldásához - basePath: ... # (string) alapértelmezés szerint %wwwDir% - - # alap URL a relatív mapper URL-ek feloldásához - baseUrl: ... # (string) alapértelmezés szerint %baseUrl% - - # asset verziózás engedélyezése globálisan? - versioning: ... # (bool) alapértelmezés szerint true - - # asset mapperek definiálása - mapping: ... # (array) alapértelmezés szerint 'assets' útvonal -``` - -A `basePath` beállítja az alapértelmezett fájlrendszer könyvtárat a mapperek relatív útvonalainak feloldásához. Alapértelmezés szerint a webkönyvtárat (`%wwwDir%`) használja. - -A `baseUrl` beállítja az alapértelmezett URL prefixet a mapperek relatív URL-einek feloldásához. Alapértelmezés szerint a gyökér URL-t (`%baseUrl%`) használja. - -A `versioning` opció globálisan szabályozza, hogy a verzióparaméterek hozzáadódnak-e az asset URL-ekhez a gyorsítótár törléséhez. Az egyes mapperek felülírhatják ezt a beállítást. - - -Mapperek --------- - -A mapperek háromféleképpen konfigurálhatók: egyszerű string jelöléssel, részletes tömb jelöléssel, vagy egy szolgáltatásra való hivatkozással. - -A mapper definiálásának legegyszerűbb módja: - -```neon -assets: - mapping: - default: assets # Fájlrendszer mappert hoz létre a %wwwDir%/assets/ számára - images: img # Fájlrendszer mappert hoz létre a %wwwDir%/img/ számára - scripts: js # Fájlrendszer mappert hoz létre a %wwwDir%/js/ számára -``` - -Minden mapper létrehoz egy `FilesystemMapper`-t, amely: -- Fájlokat keres a `%wwwDir%/`-ban -- URL-eket generál, mint `%baseUrl%/` -- Örökli a globális verziózási beállítást - - -A nagyobb kontroll érdekében használd a részletes jelölést: - -```neon -assets: - mapping: - images: - # könyvtár, ahol a fájlok tárolódnak - path: ... # (string) opcionális, alapértelmezés szerint '' - - # URL prefix a generált linkekhez - url: ... # (string) opcionális, alapértelmezés szerint path - - # verziózás engedélyezése ehhez a mapperhez? - versioning: ... # (bool) opcionális, örökli a globális beállítást - - # automatikus kiterjesztés(ek) hozzáadása fájlok keresésekor - extension: ... # (string|array) opcionális, alapértelmezés szerint null -``` - -A konfigurációs értékek feloldásának megértése: - -Útvonal feloldás: - - A relatív útvonalak a `basePath`-ból (vagy `%wwwDir%`, ha a `basePath` nincs beállítva) oldódnak fel - - Az abszolút útvonalak változatlanul használatosak - -URL feloldás: - - A relatív URL-ek a `baseUrl`-ből (vagy `%baseUrl%`, ha a `baseUrl` nincs beállítva) oldódnak fel - - Az abszolút URL-ek (sémával vagy `//`) változatlanul használatosak - - Ha az `url` nincs megadva, akkor a `path` értékét használja - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relatív útvonal és URL - images: - path: img # Feloldva: /var/www/project/www/img - url: images # Feloldva: https://example.com/assets/images - - # Abszolút útvonal és URL - uploads: - path: /var/shared/uploads # Változatlanul használva: /var/shared/uploads - url: https://cdn.example.com # Változatlanul használva: https://cdn.example.com - - # Csak az útvonal megadva - styles: - path: css # Útvonal: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Egyedi mapperek ---------------- - -Egyedi mapperek esetén hivatkozzon vagy definiáljon egy szolgáltatást: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -A Vite mapperhez csak a `type: vite` hozzáadása szükséges. Ez a konfigurációs lehetőségek teljes listája: - -```neon -assets: - mapping: - default: - # mapper típus (kötelező a Vite-hez) - type: vite # (string) kötelező, 'vite' kell legyen - - # Vite build kimeneti könyvtár - path: ... # (string) opcionális, alapértelmezés szerint '' - - # URL prefix a beépített assetekhez - url: ... # (string) opcionális, alapértelmezés szerint path - - # Vite manifest fájl helye - manifest: ... # (string) opcionális, alapértelmezés szerint /.vite/manifest.json - - # Vite dev szerver konfiguráció - devServer: ... # (bool|string) opcionális, alapértelmezés szerint true - - # verziózás a public könyvtár fájljaihoz - versioning: ... # (bool) opcionális, örökli a globális beállítást - - # automatikus kiterjesztés a public könyvtár fájljaihoz - extension: ... # (string|array) opcionális, alapértelmezés szerint null -``` - -A `devServer` opció szabályozza, hogyan töltődnek be az assetek fejlesztés közben: - -- `true` (alapértelmezett) - Automatikusan felismeri a Vite dev szervert az aktuális hoston és porton. Ha a dev szerver fut **és az alkalmazásod debug módban van**, az assetek onnan töltődnek be hot module replacement támogatással. Ha a dev szerver nem fut, az assetek a buildelt fájlokból töltődnek be a public könyvtárból. -- `false` - Teljesen letiltja a dev szerver integrációt. Az assetek mindig a buildelt fájlokból töltődnek be. -- Egyedi URL (pl. `https://localhost:5173`) - Manuálisan adja meg a dev szerver URL-jét, beleértve a protokollt és a portot. Hasznos, ha a dev szerver más hoston vagy porton fut. - -Az `versioning` és `extension` opciók csak a Vite public könyvtárában lévő olyan fájlokra vonatkoznak, amelyeket a Vite nem dolgoz fel. - - -Manuális konfiguráció ---------------------- - -Ha nem használja a Nette DI-t, konfigurálja a mappereket manuálisan: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Fájlrendszer mapper hozzáadása -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Vite mapper hozzáadása -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/hu/vite.texy b/assets/hu/vite.texy deleted file mode 100644 index eefb77fdaa..0000000000 --- a/assets/hu/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Vite Integráció -*************** - -
    - -A modern JavaScript alkalmazások kifinomult build eszközöket igényelnek. A Nette Assets első osztályú integrációt biztosít a [Vite |https://vitejs.dev/] nevű, következő generációs frontend build eszközzel. Villámgyors fejlesztést érhet el Hot Module Replacement (HMR) funkcióval és optimalizált éles build-ekkel, nulla konfigurációs gonddal. - -- **Nulla konfiguráció** - automatikus híd a Vite és a PHP sablonok között -- **Teljes függőségkezelés** - egyetlen tag kezeli az összes assetet -- **Hot Module Replacement** - azonnali JavaScript és CSS frissítések -- **Optimalizált éles build-ek** - kód felosztás és tree shaking - -
    - - -A Nette Assets zökkenőmentesen integrálódik a Vite-tel, így az összes előnyét élvezheti, miközben a sablonokat a szokásos módon írja. - - -Vite beállítása -=============== - -Állítsuk be a Vite-et lépésről lépésre. Ne aggódj, ha még új vagy a build eszközök terén - mindent elmagyarázunk! - - -1. lépés: Vite telepítése -------------------------- - -Először telepítsd a Vite-et és a Nette plugint a projektedbe: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Ez telepíti a Vite-et és egy speciális plugint, amely segít a Vite-nek tökéletesen működni a Nette-tel. - - -2. lépés: Projektstruktúra --------------------------- - -A standard megközelítés az, hogy a forrás asset fájlokat a projekt gyökerében lévő `assets/` mappába helyezzük, a fordított verziókat pedig a `www/assets/` mappába: - -/--pre -web-project/ -├── assets/ ← forrásfájlok (SCSS, TypeScript, forrásképek) -│ ├── public/ ← statikus fájlok (változatlanul másolva) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← fő belépési pont -│ └── style.css ← a stíluslapjaid -└── www/ ← nyilvános könyvtár (dokumentum gyökér) - ├── assets/ ← ide kerülnek a fordított fájlok - └── index.php -\-- - -Az `assets/` mappa tartalmazza a forrásfájljaidat - a kódot, amit írsz. A Vite feldolgozza ezeket a fájlokat, és a fordított verziókat a `www/assets/` mappába helyezi. - - -3. lépés: Vite konfigurálása ----------------------------- - -Hozzon létre egy `vite.config.ts` fájlt a projekt gyökerében. Ez a fájl megmondja a Vite-nek, hol találja a forrásfájlokat és hova tegye a fordított fájlokat. - -A Nette Vite plugin intelligens alapértelmezett beállításokkal érkezik, amelyek leegyszerűsítik a konfigurációt. Feltételezi, hogy a frontend forrásfájlok az `assets/` könyvtárban vannak (`root` opció), és a fordított fájlok a `www/assets/` mappába kerülnek (`outDir` opció). Csak a [belépési pontot |#Entry Points] kell megadnia: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Ha másik könyvtárnevet szeretne megadni az assetek buildeléséhez, néhány opciót módosítania kell: - -```js -export default defineConfig({ - root: 'assets', // forrás assetek gyökérkönyvtára - - build: { - outDir: '../www/assets', // ahova a fordított fájlok kerülnek - }, - - // ... egyéb konfiguráció ... -}); -``` - -.[note] -Az `outDir` útvonal a `root`-hoz képest relatív, ezért van `../` az elején. - - -4. lépés: Nette konfigurálása ------------------------------ - -Mondja meg a Nette Assets-nek a Vite-ről a `common.neon` fájlban: - -```neon -assets: - mapping: - default: - type: vite # megmondja a Nette-nek, hogy a ViteMapper-t használja - path: assets -``` - - -5. lépés: Szkriptek hozzáadása ------------------------------- - -Add hozzá ezeket a szkripteket a `package.json` fájlhoz: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Most már tudsz: -- `npm run dev` - fejlesztői szerver indítása hot reloading-gal -- `npm run build` - optimalizált éles fájlok létrehozása - - -Belépési pontok -=============== - -A **belépési pont** az a fő fájl, ahol az alkalmazásod elindul. Ebből a fájlból importálsz más fájlokat (CSS, JavaScript modulok, képek), létrehozva egy függőségi fát. A Vite követi ezeket az importokat és mindent egybe csomagol. - -Példa belépési pont `assets/app.js`: - -```js -// Stílusok importálása -import './style.css' - -// JavaScript modulok importálása -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Alkalmazás inicializálása -netteForms.initOnLoad(); -naja.initialize(); -``` - -A sablonban a belépési pontot a következőképpen illesztheti be: - -```latte -{asset 'app.js'} -``` - -A Nette Assets automatikusan generálja az összes szükséges HTML taget - JavaScript, CSS és bármely más függőség. - - -Több belépési pont ------------------- - -Nagyobb alkalmazásoknak gyakran külön belépési pontokra van szükségük: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // nyilvános oldalak - 'admin.js', // admin panel - ], - }), - ], -}); -``` - -Használd őket különböző sablonokban: - -```latte -{* Nyilvános oldalakon *} -{asset 'app.js'} - -{* Admin panelen *} -{asset 'admin.js'} -``` - - -Fontos: Forrás vs. fordított fájlok ------------------------------------ - -Fontos megérteni, hogy éles környezetben csak a következőket töltheti be: - -1. A `entry` fájlban definiált **belépési pontok** -2. Fájlok az `assets/public/` könyvtárból - -Nem tölthet be `{asset}` segítségével tetszőleges fájlokat az `assets/` könyvtárból - csak azokat az asseteket, amelyekre JavaScript vagy CSS fájlok hivatkoznak. Ha a fájlra sehol sem hivatkoznak, az nem lesz fordítva. Ha más asseteket is tudatosítani szeretne a Vite-tel, áthelyezheti őket a [public mappa |#public folder]-be. - -Kérjük, vegye figyelembe, hogy alapértelmezés szerint a Vite az összes 4KB-nál kisebb assetet beágyazza, így ezekre a fájlokra nem hivatkozhat közvetlenül. (Lásd [Vite dokumentáció |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Ez működik - ez egy belépési pont *} -{asset 'app.js'} - -{* ✓ Ez működik - az assets/public/ mappában van *} -{asset 'favicon.ico'} - -{* ✗ Ez nem fog működni - véletlenszerű fájl az assets/ mappában *} -{asset 'components/button.js'} -``` - - -Fejlesztői mód -============== - -A fejlesztői mód teljesen opcionális, de jelentős előnyökkel jár, ha engedélyezve van. A fő előny a **Hot Module Replacement (HMR)** - azonnal láthatja a változásokat az alkalmazás állapotának elvesztése nélkül, ami sokkal simábbá és gyorsabbá teszi a fejlesztési élményt. - -A Vite egy modern build eszköz, amely hihetetlenül gyorssá teszi a fejlesztést. A hagyományos bundlerekkel ellentétben a Vite közvetlenül a böngészőnek szolgálja ki a kódot fejlesztés közben, ami azt jelenti, hogy azonnali szerverindítás történik, függetlenül a projekt méretétől, és villámgyors frissítések. - - -Fejlesztői szerver indítása ---------------------------- - -Futtassa a fejlesztői szervert: - -```shell -npm run dev -``` - -Látni fogja: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Tartsa nyitva ezt a terminált a fejlesztés során. - -A Nette Vite plugin automatikusan felismeri, ha: -1. A Vite dev szerver fut -2. A Nette alkalmazás debug módban van - -Ha mindkét feltétel teljesül, a Nette Assets a Vite dev szerverről tölti be a fájlokat a fordított könyvtár helyett: - -```latte -{asset 'app.js'} -{* Fejlesztésben: *} -{* Éles környezetben: *} -``` - -Nincs szükség konfigurációra - egyszerűen működik! - - -Különböző domaineken való munka -------------------------------- - -Ha a fejlesztői szervered nem `localhost`-on (például `myapp.local`-on) fut, akkor CORS (Cross-Origin Resource Sharing) problémákkal találkozhatsz. A CORS egy biztonsági funkció a webböngészőkben, amely alapértelmezés szerint blokkolja a különböző domainek közötti kéréseket. Amikor a PHP alkalmazásod `myapp.local`-on fut, de a Vite `localhost:5173`-on, a böngésző ezeket különböző domaineknek tekinti, és blokkolja a kéréseket. - -Két lehetőséged van ennek megoldására: - -**1. opció: CORS konfigurálása** - -A legegyszerűbb megoldás, ha engedélyezi a cross-origin kéréseket a PHP alkalmazásából: - -```js -export default defineConfig({ - // ... egyéb konfiguráció ... - - server: { - cors: { - origin: 'http://myapp.local', // a PHP alkalmazásod URL-je - }, - }, -}); -``` -**2. opció: Futtassa a Vite-et a domainjén** - -A másik megoldás, ha a Vite-et ugyanazon a domainen futtatja, mint a PHP alkalmazását. - -```js -export default defineConfig({ - // ... egyéb konfiguráció ... - - server: { - host: 'myapp.local', // ugyanaz, mint a PHP alkalmazásod - }, -}); -``` - -Valójában ebben az esetben is konfigurálnia kell a CORS-t, mert a dev szerver ugyanazon a hostnéven, de más porton fut. Azonban ebben az esetben a CORS-t a Nette Vite plugin automatikusan konfigurálja. - - -HTTPS fejlesztés ----------------- - -Ha HTTPS-en fejlesztesz, tanúsítványokra lesz szükséged a Vite fejlesztői szerveredhez. A legegyszerűbb módja egy olyan plugin használata, amely automatikusan generál tanúsítványokat: - -```shell -npm install -D vite-plugin-mkcert -``` - -Így konfigurálhatja a `vite.config.ts` fájlban: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... egyéb konfiguráció ... - - plugins: [ - mkcert(), // automatikusan generál tanúsítványokat és engedélyezi a https-t - nette(), - ], -}); -``` - -Ne feledje, hogy ha a CORS konfigurációt használja (az 1. opciót fentebb), akkor frissítenie kell az origin URL-t `https://` használatára `http://` helyett. - - -Éles build-ek -============= - -Hozzon létre optimalizált éles fájlokat: - -```shell -npm run build -``` - -A Vite: -- Minifikálja az összes JavaScriptet és CSS-t -- Optimális részekre osztja a kódot -- Hash-elt fájlneveket generál a gyorsítótár törléséhez -- Létrehoz egy manifest fájlt a Nette Assets számára - -Példa kimenet: - -``` -www/assets/ -├── app-4f3a2b1c.js # A fő JavaScripted (minifikált) -├── app-7d8e9f2a.css # Kinyert CSS (minifikált) -├── vendor-8c4b5e6d.js # Megosztott függőségek -└── .vite/ - └── manifest.json # Leképezés a Nette Assets számára -``` - -A hash-elt fájlnevek biztosítják, hogy a böngészők mindig a legújabb verziót töltsék be. - - -Nyilvános mappa -=============== - -Az `assets/public/` könyvtárban lévő fájlok feldolgozás nélkül másolódnak a kimenetbe: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Hivatkozzon rájuk normálisan: - -```latte -{* Ezek a fájlok változatlanul másolódnak *} - - -``` - -Nyilvános fájlokhoz használhatja a FilesystemMapper funkcióit: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Először a WebP-t próbálja - versioning: true # Gyorsítótár törlés hozzáadása -``` - -A `vite.config.ts` konfigurációban a `publicDir` opcióval módosíthatja a nyilvános mappát. - - -Dinamikus importok -================== - -A Vite automatikusan felosztja a kódot az optimális betöltés érdekében. A dinamikus importok lehetővé teszik, hogy a kódot csak akkor töltse be, amikor arra ténylegesen szükség van, csökkentve az kezdeti csomagméretet: - -```js -// Nehéz komponensek betöltése igény szerint -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -A dinamikus importok külön chunkokat hoznak létre, amelyek csak akkor töltődnek be, amikor ténylegesen szükség van rájuk. Ezt "kód felosztásnak" nevezik, és ez a Vite egyik legerősebb funkciója. Amikor dinamikus importokat használ, a Vite automatikusan külön JavaScript fájlokat hoz létre minden dinamikusan importált modulhoz. - -Az `{asset 'app.js'}` tag **nem** tölti be automatikusan ezeket a dinamikus chunkokat. Ez szándékos viselkedés - nem akarunk olyan kódot letölteni, amelyet esetleg soha nem használnak. A chunkok csak akkor töltődnek le, amikor a dinamikus import végrehajtásra kerül. - -Azonban, ha tudja, hogy bizonyos dinamikus importok kritikusak, és hamarosan szükség lesz rájuk, előtöltheti őket: - -```latte -{* Fő belépési pont *} -{asset 'app.js'} - -{* Kritikus dinamikus importok előtöltése *} -{preload 'components/chart.js'} -``` - -Ez azt mondja a böngészőnek, hogy töltse le a diagramkomponenst a háttérben, így azonnal készen áll, amikor szükség van rá. - - -TypeScript támogatás -==================== - -A TypeScript azonnal működik: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Hivatkozzon a TypeScript fájlokra normálisan: - -```latte -{asset 'main.ts'} -``` - -A teljes TypeScript támogatáshoz telepítse: - -```shell -npm install -D typescript -``` - - -További Vite konfiguráció -========================= - -Íme néhány hasznos Vite konfigurációs opció részletes magyarázattal: - -```js -export default defineConfig({ - // A forrás asseteket tartalmazó gyökérkönyvtár - root: 'assets', - - // Az a mappa, amelynek tartalma változatlanul másolódik a kimeneti könyvtárba - // Alapértelmezett: 'public' (a 'root'-hoz képest relatív) - publicDir: 'public', - - build: { - // Hova kerüljenek a fordított fájlok (a 'root'-hoz képest relatív) - outDir: '../www/assets', - - // Ürítse ki a kimeneti könyvtárat a buildelés előtt? - // Hasznos a régi fájlok eltávolításához az előző buildekből - emptyOutDir: true, - - // Alkönvtár az outDir-en belül a generált chunkok és assetek számára - // Ez segít a kimeneti struktúra rendezésében - assetsDir: 'static', - - rollupOptions: { - // Belépési pont(ok) - lehet egyetlen fájl vagy fájltömb - // Minden belépési pont külön csomaggá válik - input: [ - 'app.js', // fő alkalmazás - 'admin.js', // admin panel - ], - }, - }, - - server: { - // Host, amelyhez a dev szerver kötődik - // Használja a '0.0.0.0'-t a hálózaton való közzétételhez - host: 'localhost', - - // Port a dev szerverhez - port: 5173, - - // CORS konfiguráció a cross-origin kérésekhez - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // CSS forrástérképek engedélyezése fejlesztésben - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Ennyi! Most már van egy modern build rendszered, amely integrálva van a Nette Assets-szel. diff --git a/assets/it/@home.texy b/assets/it/@home.texy index 3e665fef57..539dd5e9f8 100644 --- a/assets/it/@home.texy +++ b/assets/it/@home.texy @@ -3,13 +3,13 @@ Nette Assets
    -Stanco di gestire manualmente i file statici nelle tue applicazioni web? Dimentica la codifica manuale dei percorsi, la gestione dell'invalidazione della cache o la preoccupazione per il versioning dei file. Nette Assets trasforma il modo in cui lavori con immagini, fogli di stile, script e altre risorse statiche. +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. -- **Versioning intelligente** assicura che i browser carichino sempre i file più recenti +- Il **versionamento intelligente** garantisce che i browser carichino sempre i file più recenti - **Rilevamento automatico** dei tipi di file e delle dimensioni -- **Integrazione Latte senza soluzione di continuità** con tag intuitivi +- **Integrazione fluida con Latte** grazie a tag intuitivi - **Architettura flessibile** che supporta filesystem, CDN e Vite -- **Caricamento pigro (Lazy loading)** per prestazioni ottimali +- **Caricamento pigro** per prestazioni ottimali
    @@ -17,129 +17,132 @@ Stanco di gestire manualmente i file statici nelle tue applicazioni web? Dimenti Perché Nette Assets? ==================== -Lavorare con i file statici spesso significa codice ripetitivo e soggetto a errori. Costruisci manualmente URL, aggiungi parametri di versione per il cache busting e gestisci diversi tipi di file in modo diverso. Questo porta a codice come: +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à scompare: +Con Nette Assets tutta questa complessità sparisce: ```latte -{* Tutto automatizzato - URL, versioning, dimensioni *} +{* tutto automatizzato: URL, versionamento, dimensioni *} -{* O semplicemente *} +{* oppure solo *} {asset 'css/style.css'} ``` -Questo è tutto! La libreria automaticamente: -- Aggiunge parametri di versione basati sull'ora di modifica del file -- Rileva le dimensioni dell'immagine e le include nell'HTML -- Genera l'elemento HTML corretto per ogni tipo di file -- Gestisce sia gli ambienti di sviluppo che di produzione +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 ============= -Installa Nette Assets usando [Composer|best-practices:composer]: +Installate Nette Assets con [Composer|best-practices:composer]: ```shell composer require nette/assets ``` -Richiede PHP 8.1 o superiore e funziona perfettamente con Nette Framework, ma può essere usato anche in modo standalone. +Richiede PHP 8.1 o superiore e funziona perfettamente con il Nette Framework, ma si può usare anche da solo. -Primi Passi +Primi passi =========== -Nette Assets funziona subito senza alcuna configurazione. Posiziona i tuoi file statici nella directory `www/assets/` e inizia ad usarli: +Nette Assets funziona subito senza alcuna configurazione. Mettete i vostri file statici nella directory `www/assets/` e cominciate a usarli: ```latte -{* Visualizza un'immagine con dimensioni automatiche *} +{* mostra un'immagine con le dimensioni automatiche *} {asset 'logo.png'} -{* Includi un foglio di stile con versioning *} +{* include un foglio di stile con il versionamento *} {asset 'style.css'} -{* Carica un modulo JavaScript *} +{* carica uno script *} {asset 'app.js'} ``` -Per un maggiore controllo sull'HTML generato, usa l'attributo `n:asset` o la funzione `asset()`. +Per un maggiore controllo sull'HTML generato usate l'attributo `n:asset` oppure la funzione `asset()`. -Come Funziona +Come funziona ============= -Nette Assets è costruito attorno a tre concetti fondamentali che lo rendono potente ma semplice da usare: +Nette Assets è costruito attorno a tre concetti fondamentali che lo rendono potente e allo stesso tempo semplice da usare: -Assets - I Tuoi File Resi Intelligenti +Asset: i vostri file resi intelligenti -------------------------------------- -Un **asset** rappresenta qualsiasi file statico nella tua applicazione. Ogni file diventa un oggetto con utili proprietà di sola lettura: +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' ``` -Diversi tipi di file forniscono proprietà diverse: +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 queries, integrità -- **Audio/Video**: durata, dimensioni -- **Font**: precaricamento corretto con CORS +- **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 asset appropriata. +La libreria rileva automaticamente i tipi di file e crea la classe di asset appropriata. -Mappers - Da Dove Vengono i File --------------------------------- +Mapper: da dove vengono i file +------------------------------ -Un **mapper** sa come trovare i file e creare URL per essi. Puoi avere più mapper per scopi diversi - file locali, CDN, cloud storage o strumenti di build (ognuno di essi ha un nome). Il `FilesystemMapper` integrato gestisce i file locali, mentre `ViteMapper` si integra con i moderni strumenti di build. +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 sono definiti nella [Configurazione | Configuration]. +I mapper si definiscono nella [configurazione |configuration]. -Registry - La Tua Interfaccia Principale ----------------------------------------- +Registry: la vostra interfaccia principale +------------------------------------------ -Il **registry** gestisce tutti i mapper e fornisce l'API principale: +Il **registry** gestisce tutti i mapper e offre l'API principale: ```php -// Inietta il registry nel tuo servizio +// fatevi iniettare il registry nel vostro servizio public function __construct( private Nette\Assets\Registry $assets ) {} +``` -// Ottieni assets da diversi mapper -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'image' +```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 seleziona automaticamente il mapper corretto e memorizza i risultati nella cache per le prestazioni. +Il registry sceglie automaticamente il mapper giusto e mette in cache i risultati per le prestazioni. -Lavorare con gli Assets in PHP -============================== +Lavorare con gli asset in PHP +============================= -Il Registry fornisce due metodi per recuperare gli asset: +Il Registry offre due metodi per ottenere gli asset: ```php -// Lancia Nette\Assets\AssetNotFoundException se il file non esiste +// lancia Nette\Assets\AssetNotFoundException se il file non esiste $logo = $assets->getAsset('logo.png'); -// Restituisce null se il file non esiste +// restituisce null se il file non esiste $banner = $assets->tryGetAsset('banner.jpg'); if ($banner) { echo $banner->url; @@ -147,55 +150,58 @@ if ($banner) { ``` -Specificare i Mapper --------------------- +Indicare i mapper +----------------- -Puoi scegliere esplicitamente quale mapper usare: +Potete scegliere esplicitamente quale mapper usare: ```php -// Usa il mapper predefinito +// usa il mapper predefinito $file = $assets->getAsset('document.pdf'); -// Usa un mapper specifico con prefisso +// usa un mapper specifico con il prefisso $image = $assets->getAsset('images:photo.jpg'); -// Usa un mapper specifico con sintassi array +// usa un mapper specifico con la sintassi ad array $script = $assets->getAsset(['scripts', 'app.js']); ``` -Proprietà e Tipi di Asset -------------------------- +Proprietà e tipi degli asset +---------------------------- -Ogni tipo di asset fornisce proprietà di sola lettura rilevanti: +Ogni tipo di asset offre le proprietà readonly pertinenti: ```php -// Proprietà dell'immagine +// proprietà di un'immagine $image = $assets->getAsset('photo.jpg'); echo $image->width; // 1920 echo $image->height; // 1080 echo $image->mimeType; // 'image/jpeg' -// Proprietà dello script +// proprietà di uno script $script = $assets->getAsset('app.js'); -echo $script->type; // 'module' o null +echo $script->type; // null ('module' per i punti di ingresso di Vite) -// Proprietà audio +// proprietà di un audio $audio = $assets->getAsset('song.mp3'); echo $audio->duration; // durata in secondi -// Tutti gli asset possono essere convertiti in stringa (restituisce URL) +// tutti gli asset si possono convertire in stringa (restituisce l'URL) $url = (string) $assets->getAsset('document.pdf'); ``` .[note] -Le proprietà come le dimensioni o la durata vengono caricate pigramente solo quando vi si accede, mantenendo la libreria veloce. +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. -Uso degli Assets nei Template Latte -=================================== +Usare gli asset nei template Latte +================================== -Nette Assets fornisce un'integrazione [Latte|latte:] intuitiva con tag e funzioni. +Nette Assets offre un'integrazione intuitiva con [Latte|latte:] tramite tag e funzioni. `{asset}` @@ -204,23 +210,23 @@ Nette Assets fornisce un'integrazione [Latte|latte:] intuitiva con tag e funzion Il tag `{asset}` renderizza elementi HTML completi: ```latte -{* Renderizza: *} +{* renderizza: *} {asset 'hero.jpg'} -{* Renderizza: *} +{* renderizza: *} {asset 'app.js'} -{* Renderizza: *} +{* renderizza: *} {asset 'style.css'} ``` Il tag automaticamente: -- Rileva il tipo di asset e genera l'HTML appropriato -- Include il versioning per il cache busting -- Aggiunge le dimensioni per le immagini -- Imposta gli attributi corretti (type, media, ecc.) +- 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 usato all'interno di attributi HTML, produce solo l'URL: +Quando si usa dentro un attributo HTML oppure dentro gli elementi ` -``` - - -Mappers Personalizados -====================== - -Crie mappers personalizados para necessidades especiais como armazenamento em nuvem ou geração dinâmica: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Registre na configuração: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Use como qualquer outro mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -O método `Helpers::createAssetFromUrl()` cria automaticamente o tipo de asset correto com base na extensão do arquivo. - - -Leitura adicional -================= - -- [Nette Assets: Finalmente uma API unificada para tudo, desde imagens até o Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/pt/@left-menu.texy b/assets/pt/@left-menu.texy deleted file mode 100644 index 67ea06e0d2..0000000000 --- a/assets/pt/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Primeiros Passos |@home] -- [Vite |vite] -- [Configuração |Configuration] diff --git a/assets/pt/@meta.texy b/assets/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/assets/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/assets/pt/configuration.texy b/assets/pt/configuration.texy deleted file mode 100644 index 7bca5174a1..0000000000 --- a/assets/pt/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Configuração de Assets -********************** - -.[perex] -Visão geral das opções de configuração para Nette Assets. - - -```neon -assets: - # caminho base para resolver caminhos de mapper relativos - basePath: ... # (string) padrão para %wwwDir% - - # URL base para resolver URLs de mapper relativas - baseUrl: ... # (string) padrão para %baseUrl% - - # habilitar versionamento de asset globalmente? - versioning: ... # (bool) padrão para true - - # define os mappers de asset - mapping: ... # (array) padrão para o caminho 'assets' -``` - -O `basePath` define o diretório padrão do sistema de arquivos para resolver caminhos relativos em mappers. Por padrão, ele usa o diretório web (`%wwwDir%`). - -A `baseUrl` define o prefixo de URL padrão para resolver URLs relativas em mappers. Por padrão, ele usa a URL raiz (`%baseUrl%`). - -A opção `versioning` controla globalmente se os parâmetros de versão são adicionados às URLs dos assets para cache busting. Mappers individuais podem substituir essa configuração. - - -Mappers -------- - -Mappers podem ser configurados de três maneiras: notação de string simples, notação de array detalhada ou como uma referência a um serviço. - -A maneira mais simples de definir um mapper: - -```neon -assets: - mapping: - default: assets # Cria um mapper de sistema de arquivos para %wwwDir%/assets/ - images: img # Cria um mapper de sistema de arquivos para %wwwDir%/img/ - scripts: js # Cria um mapper de sistema de arquivos para %wwwDir%/js/ -``` - -Cada mapper cria um `FilesystemMapper` que: -- Procura arquivos em `%wwwDir%/` -- Gera URLs como `%baseUrl%/` -- Herda a configuração de versionamento global - - -Para mais controle, use a notação detalhada: - -```neon -assets: - mapping: - images: - # diretório onde os arquivos são armazenados - path: ... # (string) opcional, padrão para '' - - # prefixo de URL para links gerados - url: ... # (string) opcional, padrão para path - - # habilitar versionamento para este mapper? - versioning: ... # (bool) opcional, herda a configuração global - - # adicionar automaticamente extensão(ões) ao procurar arquivos - extension: ... # (string|array) opcional, padrão para null -``` - -Entendendo como os valores de configuração são resolvidos: - -Resolução de Caminho: - - Caminhos relativos são resolvidos a partir de `basePath` (ou `%wwwDir%` se `basePath` não estiver definido) - - Caminhos absolutos são usados como estão - -Resolução de URL: - - URLs relativas são resolvidas a partir de `baseUrl` (ou `%baseUrl%` se `baseUrl` não estiver definido) - - URLs absolutas (com esquema ou `//`) são usadas como estão - - Se `url` não for especificado, ele usa o valor de `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Caminho e URL relativos - images: - path: img # Resolvido para: /var/www/project/www/img - url: images # Resolvido para: https://example.com/assets/images - - # Caminho e URL absolutos - uploads: - path: /var/shared/uploads # Usado como está: /var/shared/uploads - url: https://cdn.example.com # Usado como está: https://cdn.example.com - - # Apenas o caminho especificado - styles: - path: css # Caminho: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Mappers Personalizados ----------------------- - -Para mappers personalizados, faça referência ou defina um serviço: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -O mapper Vite exige apenas que você adicione `type: vite`. Esta é uma lista completa de opções de configuração: - -```neon -assets: - mapping: - default: - # tipo de mapper (obrigatório para Vite) - type: vite # (string) obrigatório, deve ser 'vite' - - # diretório de saída de construção do Vite - path: ... # (string) opcional, padrão para '' - - # prefixo de URL para assets construídos - url: ... # (string) opcional, padrão para path - - # localização do arquivo de manifesto do Vite - manifest: ... # (string) opcional, padrão para /.vite/manifest.json - - # configuração do servidor de desenvolvimento do Vite - devServer: ... # (bool|string) opcional, padrão para true - - # versionamento para arquivos do diretório público - versioning: ... # (bool) opcional, herda a configuração global - - # auto-extensão para arquivos do diretório público - extension: ... # (string|array) opcional, padrão para null -``` - -A opção `devServer` controla como os assets são carregados durante o desenvolvimento: - -- `true` (padrão) - Detecta automaticamente o servidor de desenvolvimento Vite no host e porta atuais. Se o servidor de desenvolvimento estiver em execução **e sua aplicação estiver em modo de depuração**, os assets são carregados dele com suporte a hot module replacement. Se o servidor de desenvolvimento não estiver em execução, os assets são carregados dos arquivos construídos no diretório público. -- `false` - Desativa completamente a integração do servidor de desenvolvimento. Os assets são sempre carregados dos arquivos construídos. -- URL personalizada (por exemplo, `https://localhost:5173`) - Especifique manualmente a URL do servidor de desenvolvimento, incluindo protocolo e porta. Útil quando o servidor de desenvolvimento é executado em um host ou porta diferente. - -As opções `versioning` e `extension` aplicam-se apenas a arquivos no diretório público do Vite que não são processados pelo Vite. - - -Configuração Manual -------------------- - -Quando não estiver usando Nette DI, configure os mappers manualmente: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Adiciona o mapper de sistema de arquivos -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Adiciona o mapper Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/pt/vite.texy b/assets/pt/vite.texy deleted file mode 100644 index 542aae02f8..0000000000 --- a/assets/pt/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Integração com Vite -******************* - -
    - -Aplicações JavaScript modernas exigem ferramentas de construção sofisticadas. Nette Assets oferece integração de primeira classe com [Vite |https://vitejs.dev/], a ferramenta de construção frontend de próxima geração. Obtenha desenvolvimento ultrarrápido com Hot Module Replacement (HMR) e construções de produção otimizadas com zero complicações de configuração. - -- **Zero configuração** - ponte automática entre Vite e templates PHP -- **Gerenciamento completo de dependências** - uma tag lida com todos os assets -- **Hot Module Replacement** - atualizações instantâneas de JavaScript e CSS -- **Construções de produção otimizadas** - code splitting e tree shaking - -
    - - -Nette Assets se integra perfeitamente com Vite, para que você obtenha todos esses benefícios enquanto escreve seus templates como de costume. - - -Configurando o Vite -=================== - -Vamos configurar o Vite passo a passo. Não se preocupe se você é novo em ferramentas de construção - vamos explicar tudo! - - -Passo 1: Instalar o Vite ------------------------- - -Primeiro, instale o Vite e o plugin Nette em seu projeto: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Isso instala o Vite e um plugin especial que ajuda o Vite a funcionar perfeitamente com o Nette. - - -Passo 2: Estrutura do Projeto ------------------------------ - -A abordagem padrão é colocar os arquivos de asset de origem em uma pasta `assets/` na raiz do seu projeto, e as versões compiladas em `www/assets/`: - -/--pre -web-project/ -├── assets/ ← arquivos de origem (SCSS, TypeScript, imagens de origem) -│ ├── public/ ← arquivos estáticos (copiados como estão) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← ponto de entrada principal -│ └── style.css ← seus estilos -└── www/ ← diretório público (document root) - ├── assets/ ← arquivos compilados irão para cá - └── index.php -\-- - -A pasta `assets/` contém seus arquivos de origem - o código que você escreve. O Vite processará esses arquivos e colocará as versões compiladas em `www/assets/`. - - -Passo 3: Configurar o Vite --------------------------- - -Crie um arquivo `vite.config.ts` na raiz do seu projeto. Este arquivo informa ao Vite onde encontrar seus arquivos de origem e onde colocar os compilados. - -O plugin Nette Vite vem com padrões inteligentes que simplificam a configuração. Ele assume que seus arquivos de origem frontend estão no diretório `assets/` (opção `root`) e os arquivos compilados vão para `www/assets/` (opção `outDir`). Você só precisa especificar o [ponto de entrada|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Se você quiser especificar outro nome de diretório para construir seus assets, precisará alterar algumas opções: - -```js -export default defineConfig({ - root: 'assets', // diretório raiz dos assets de origem - - build: { - outDir: '../www/assets', // onde os arquivos compilados vão - }, - - // ... outras configurações ... -}); -``` - -.[note] -O caminho `outDir` é considerado relativo a `root`, por isso há `../` no início. - - -Passo 4: Configurar o Nette ---------------------------- - -Informe ao Nette Assets sobre o Vite em seu `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # informa ao Nette para usar o ViteMapper - path: assets -``` - - -Passo 5: Adicionar scripts --------------------------- - -Adicione estes scripts ao seu `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Agora você pode: -- `npm run dev` - iniciar o servidor de desenvolvimento com hot reloading -- `npm run build` - criar arquivos de produção otimizados - - -Pontos de Entrada -================= - -Um **ponto de entrada** é o arquivo principal onde sua aplicação começa. A partir deste arquivo, você importa outros arquivos (CSS, módulos JavaScript, imagens), criando uma árvore de dependências. O Vite segue essas importações e agrupa tudo. - -Exemplo de ponto de entrada `assets/app.js`: - -```js -// Importa estilos -import './style.css' - -// Importa módulos JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inicializa sua aplicação -netteForms.initOnLoad(); -naja.initialize(); -``` - -No template você pode inserir um ponto de entrada da seguinte forma: - -```latte -{asset 'app.js'} -``` - -Nette Assets gera automaticamente todas as tags HTML necessárias - JavaScript, CSS e quaisquer outras dependências. - - -Múltiplos Pontos de Entrada ---------------------------- - -Aplicações maiores geralmente precisam de pontos de entrada separados: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // páginas públicas - 'admin.js', // painel de administração - ], - }), - ], -}); -``` - -Use-os em diferentes templates: - -```latte -{* Em páginas públicas *} -{asset 'app.js'} - -{* No painel de administração *} -{asset 'admin.js'} -``` - - -Importante: Arquivos de Origem vs. Compilados ---------------------------------------------- - -É crucial entender que em produção você só pode carregar: - -1. **Pontos de entrada** definidos em `entry` -2. **Arquivos do diretório `assets/public/`** - -Você **não pode** carregar usando `{asset}` arquivos arbitrários de `assets/` - apenas assets referenciados por arquivos JavaScript ou CSS. Se seu arquivo não for referenciado em nenhum lugar, ele não será compilado. Se você quiser que o Vite esteja ciente de outros assets, você pode movê-los para a [pasta pública|#Public Folder]. - -Observe que, por padrão, o Vite incorporará todos os assets menores que 4KB, então você não poderá referenciar esses arquivos diretamente. (Consulte a [documentação do Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Isso funciona - é um ponto de entrada *} -{asset 'app.js'} - -{* ✓ Isso funciona - está em assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Isso não funcionará - arquivo aleatório em assets/ *} -{asset 'components/button.js'} -``` - - -Modo de Desenvolvimento -======================= - -O modo de desenvolvimento é completamente opcional, mas oferece benefícios significativos quando ativado. A principal vantagem é o **Hot Module Replacement (HMR)** - veja as mudanças instantaneamente sem perder o estado da aplicação, tornando a experiência de desenvolvimento muito mais suave e rápida. - -Vite é uma ferramenta de construção moderna que torna o desenvolvimento incrivelmente rápido. Ao contrário dos bundlers tradicionais, o Vite serve seu código diretamente para o navegador durante o desenvolvimento, o que significa um início instantâneo do servidor, não importa o tamanho do seu projeto, e atualizações ultrarrápidas. - - -Iniciando o Servidor de Desenvolvimento ---------------------------------------- - -Execute o servidor de desenvolvimento: - -```shell -npm run dev -``` - -Você verá: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Mantenha este terminal aberto durante o desenvolvimento. - -O plugin Nette Vite detecta automaticamente quando: -1. O servidor de desenvolvimento Vite está em execução -2. Sua aplicação Nette está em modo de depuração - -Quando ambas as condições são atendidas, o Nette Assets carrega os arquivos do servidor de desenvolvimento Vite em vez do diretório compilado: - -```latte -{asset 'app.js'} -{* Em desenvolvimento: *} -{* Em produção: *} -``` - -Nenhuma configuração necessária - simplesmente funciona! - - -Trabalhando em Diferentes Domínios ----------------------------------- - -Se o seu servidor de desenvolvimento estiver sendo executado em algo diferente de `localhost` (como `myapp.local`), você pode encontrar problemas de CORS (Cross-Origin Resource Sharing). CORS é um recurso de segurança em navegadores da web que bloqueia solicitações entre diferentes domínios por padrão. Quando sua aplicação PHP é executada em `myapp.local`, mas o Vite é executado em `localhost:5173`, o navegador os vê como domínios diferentes e bloqueia as solicitações. - -Você tem duas opções para resolver isso: - -**Opção 1: Configurar CORS** - -A solução mais simples é permitir solicitações cross-origin de sua aplicação PHP: - -```js -export default defineConfig({ - // ... outras configurações ... - - server: { - cors: { - origin: 'http://myapp.local', // URL da sua aplicação PHP - }, - }, -}); -``` -**Opção 2: Executar o Vite em seu domínio** - -A outra solução é fazer com que o Vite seja executado no mesmo domínio da sua aplicação PHP. - -```js -export default defineConfig({ - // ... outras configurações ... - - server: { - host: 'myapp.local', // o mesmo da sua aplicação PHP - }, -}); -``` - -Na verdade, mesmo neste caso, você precisa configurar o CORS porque o servidor de desenvolvimento é executado no mesmo hostname, mas em uma porta diferente. No entanto, neste caso, o CORS é configurado automaticamente pelo plugin Nette Vite. - - -Desenvolvimento HTTPS ---------------------- - -Se você desenvolve em HTTPS, precisa de certificados para o seu servidor de desenvolvimento Vite. A maneira mais fácil é usar um plugin que gera certificados automaticamente: - -```shell -npm install -D vite-plugin-mkcert -``` - -Veja como configurá-lo em `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... outras configurações ... - - plugins: [ - mkcert(), // gera certificados automaticamente e habilita https - nette(), - ], -}); -``` - -Observe que, se você estiver usando a configuração CORS (Opção 1 acima), precisará atualizar a URL de origem para usar `https://` em vez de `http://`. - - -Construções de Produção -======================= - -Crie arquivos de produção otimizados: - -```shell -npm run build -``` - -O Vite irá: -- Minificar todo o JavaScript e CSS -- Dividir o código em chunks ideais -- Gerar nomes de arquivo com hash para cache-busting -- Criar um arquivo de manifesto para Nette Assets - -Exemplo de saída: - -``` -www/assets/ -├── app-4f3a2b1c.js # Seu JavaScript principal (minificado) -├── app-7d8e9f2a.css # CSS extraído (minificado) -├── vendor-8c4b5e6d.js # Dependências compartilhadas -└── .vite/ - └── manifest.json # Mapeamento para Nette Assets -``` - -Os nomes de arquivo com hash garantem que os navegadores sempre carreguem a versão mais recente. - - -Pasta Pública -============= - -Os arquivos no diretório `assets/public/` são copiados para a saída sem processamento: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Referencie-os normalmente: - -```latte -{* Estes arquivos são copiados como estão *} - - -``` - -Para arquivos públicos, você pode usar os recursos do FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Tenta WebP primeiro - versioning: true # Adiciona cache-busting -``` - -Na configuração `vite.config.ts` você pode alterar a pasta pública usando a opção `publicDir`. - - -Importações Dinâmicas -===================== - -O Vite divide automaticamente o código para carregamento ideal. As importações dinâmicas permitem que você carregue o código apenas quando ele é realmente necessário, reduzindo o tamanho inicial do bundle: - -```js -// Carrega componentes pesados sob demanda -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -As importações dinâmicas criam chunks separados que são carregados apenas quando necessário. Isso é chamado de "code splitting" e é um dos recursos mais poderosos do Vite. Quando você usa importações dinâmicas, o Vite cria automaticamente arquivos JavaScript separados para cada módulo importado dinamicamente. - -A tag `{asset 'app.js'}` **não** pré-carrega automaticamente esses chunks dinâmicos. Este é um comportamento intencional - não queremos baixar código que talvez nunca seja usado. Os chunks são baixados apenas quando a importação dinâmica é executada. - -No entanto, se você souber que certas importações dinâmicas são críticas e serão necessárias em breve, você pode pré-carregá-las: - -```latte -{* Ponto de entrada principal *} -{asset 'app.js'} - -{* Pré-carrega importações dinâmicas críticas *} -{preload 'components/chart.js'} -``` - -Isso informa ao navegador para baixar o componente do gráfico em segundo plano, para que esteja pronto imediatamente quando necessário. - - -Suporte a TypeScript -==================== - -TypeScript funciona de imediato: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Referencie arquivos TypeScript normalmente: - -```latte -{asset 'main.ts'} -``` - -Para suporte completo a TypeScript, instale-o: - -```shell -npm install -D typescript -``` - - -Configuração Adicional do Vite -============================== - -Aqui estão algumas opções úteis de configuração do Vite com explicações detalhadas: - -```js -export default defineConfig({ - // Diretório raiz contendo os assets de origem - root: 'assets', - - // Pasta cujo conteúdo é copido para o diretório de saída como está - // Padrão: 'public' (relativo a 'root') - publicDir: 'public', - - build: { - // Onde colocar os arquivos compilados (relativo a 'root') - outDir: '../www/assets', - - // Esvaziar o diretório de saída antes de construir? - // Útil para remover arquivos antigos de construções anteriores - emptyOutDir: true, - - // Subdiretório dentro de outDir para chunks e assets gerados - // Isso ajuda a organizar a estrutura de saída - assetsDir: 'static', - - rollupOptions: { - // Ponto(s) de entrada - pode ser um único arquivo ou array de arquivos - // Cada ponto de entrada se torna um bundle separado - input: [ - 'app.js', // aplicação principal - 'admin.js', // painel de administração - ], - }, - }, - - server: { - // Host para o qual o servidor de desenvolvimento deve se vincular - // Use '0.0.0.0' para expor à rede - host: 'localhost', - - // Porta para o servidor de desenvolvimento - port: 5173, - - // Configuração CORS para solicitações cross-origin - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Habilitar sourcemaps CSS em desenvolvimento - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -É isso! Agora você tem um sistema de construção moderno integrado com Nette Assets. diff --git a/assets/ro/@home.texy b/assets/ro/@home.texy deleted file mode 100644 index e1f406932c..0000000000 --- a/assets/ro/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -V-ați săturat să gestionați manual fișierele statice în aplicațiile dumneavoastră web? Uitați de codificarea manuală a căilor, de gestionarea invalidării cache-ului sau de îngrijorarea legată de versionarea fișierelor. Nette Assets transformă modul în care lucrați cu imagini, foi de stil, scripturi și alte resurse statice. - -- **Versionare inteligentă** asigură că browserele încarcă întotdeauna cele mai recente fișiere -- **Detecție automată** a tipurilor și dimensiunilor fișierelor -- **Integrare perfectă cu Latte** cu tag-uri intuitive -- **Arhitectură flexibilă** care suportă sisteme de fișiere, CDN-uri și Vite -- **Încărcare leneșă** pentru performanță optimă - -
    - - -De ce Nette Assets? -=================== - -Lucrul cu fișiere statice înseamnă adesea cod repetitiv, predispus la erori. Construiți manual URL-uri, adăugați parametri de versiune pentru invalidarea cache-ului și gestionați diferit tipurile de fișiere. Acest lucru duce la cod de genul: - -```latte -Logo - -``` - -Cu Nette Assets, toată această complexitate dispare: - -```latte -{* Totul automatizat - URL, versionare, dimensiuni *} - - - -{* Sau pur și simplu *} -{asset 'css/style.css'} -``` - -Asta e tot! Biblioteca automat: -- Adaugă parametri de versiune bazat pe timpul de modificare al fișierului -- Detectează dimensiunile imaginii și le include în HTML -- Generează elementul HTML corect pentru fiecare tip de fișier -- Gestionează atât mediile de dezvoltare, cât și cele de producție - - -Instalare -========= - -Instalați Nette Assets folosind [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Necesită PHP 8.1 sau o versiune superioară și funcționează perfect cu Nette Framework, dar poate fi folosit și independent. - - -Primii Pași -=========== - -Nette Assets funcționează imediat, fără configurare. Plasați fișierele statice în directorul `www/assets/` și începeți să le utilizați: - -```latte -{* Afișează o imagine cu dimensiuni automate *} -{asset 'logo.png'} - -{* Include o foaie de stil cu versionare *} -{asset 'style.css'} - -{* Încarcă un modul JavaScript *} -{asset 'app.js'} -``` - -Pentru mai mult control asupra HTML-ului generat, utilizați atributul `n:asset` sau funcția `asset()`. - - -Cum Funcționează -================ - -Nette Assets este construit în jurul a trei concepte cheie care îl fac puternic, dar simplu de utilizat: - - -Asset-uri - Fișierele Dumneavoastră Făcute Inteligente ------------------------------------------------------- - -Un **asset** reprezintă orice fișier static din aplicația dumneavoastră. Fiecare fișier devine un obiect cu proprietăți utile, doar pentru citire: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Diferite tipuri de fișiere oferă proprietăți diferite: -- **Imagini**: lățime, înălțime, text alternativ, încărcare leneșă -- **Scripturi**: tip modul, hash-uri de integritate, crossorigin -- **Foi de stil**: interogări media, integritate -- **Audio/Video**: durată, dimensiuni -- **Fonturi**: preîncărcare corectă cu CORS - -Biblioteca detectează automat tipurile de fișiere și creează clasa de asset corespunzătoare. - - -Mapperi - De unde provin fișierele ----------------------------------- - -Un **mapper** știe cum să găsească fișiere și să creeze URL-uri pentru ele. Puteți avea mai mulți mapperi pentru diferite scopuri - fișiere locale, CDN, stocare în cloud sau instrumente de construire (fiecare dintre ele are un nume). FilesystemMapper-ul încorporat gestionează fișierele locale, în timp ce ViteMapper se integrează cu instrumente moderne de construire. - -Mapperii sunt definiți în [Configurare |Configuration]. - - -Registrul - Interfața dumneavoastră principală ----------------------------------------------- - -**Registrul** gestionează toți mapperii și oferă API-ul principal: - -```php -// Injectați registrul în serviciul dumneavoastră -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Obțineți asset-uri de la diferiți mapperi -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'image' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // utilizează mapper-ul implicit -``` - -Registrul selectează automat mapper-ul potrivit și memorează în cache rezultatele pentru performanță. - - -Lucrul cu Asset-uri în PHP -========================== - -Registrul oferă două metode pentru recuperarea asset-urilor: - -```php -// Aruncă Nette\Assets\AssetNotFoundException dacă fișierul nu există -$logo = $assets->getAsset('logo.png'); - -// Returnează null dacă fișierul nu există -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Specificarea Mapper-ilor ------------------------- - -Puteți alege explicit ce mapper să utilizați: - -```php -// Utilizează mapper-ul implicit -$file = $assets->getAsset('document.pdf'); - -// Utilizează mapper-ul specific cu prefix -$image = $assets->getAsset('images:photo.jpg'); - -// Utilizează mapper-ul specific cu sintaxă de array -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Proprietăți și Tipuri de Asset-uri ----------------------------------- - -Fiecare tip de asset oferă proprietăți relevante, doar pentru citire: - -```php -// Proprietăți imagine -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Proprietăți script -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' or null - -// Proprietăți audio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duration in seconds - -// Toate asset-urile pot fi convertite la șir (returnează URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Proprietățile precum dimensiunile sau durata sunt încărcate leneș, doar la accesare, menținând biblioteca rapidă. - - -Utilizarea Asset-urilor în Șabloanele Latte -=========================================== - -Nette Assets oferă o integrare intuitivă cu [Latte|latte:] prin tag-uri și funcții. - - -`{asset}` ---------- - -Tag-ul `{asset}` randează elemente HTML complete: - -```latte -{* Randează: *} -{asset 'hero.jpg'} - -{* Randează: *} -{asset 'app.js'} - -{* Randează: *} -{asset 'style.css'} -``` - -Tag-ul automat: -- Detectează tipul asset-ului și generează HTML-ul corespunzător -- Include versionare pentru invalidarea cache-ului -- Adaugă dimensiuni pentru imagini -- Setează atributele corecte (tip, media, etc.) - -Când este utilizat în interiorul atributelor HTML, acesta afișează doar URL-ul: - -```latte -
    - -``` - - -`n:asset` ---------- - -Pentru control complet asupra atributelor HTML: - -```latte -{* Atributul n:asset completează src, dimensiuni etc. *} -Product - -{* Funcționează cu orice element relevant *} - - - -``` - -Utilizați variabile și mapperi: - -```latte -{* Variabilele funcționează natural *} - - -{* Specificați mapper-ul cu acolade *} - - -{* Specificați mapper-ul cu notație de array *} - -``` - - -`asset()` ---------- - -Pentru flexibilitate maximă, utilizați funcția `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Sau direct *} -Logo -``` - - -Asset-uri Opționale -------------------- - -Gestionați asset-urile lipsă în mod elegant cu `{asset?}`, `n:asset?` și `tryAsset()`: - -```latte -{* Tag opțional - nu randează nimic dacă asset-ul lipsește *} -{asset? 'optional-banner.jpg'} - -{* Atribut opțional - sare peste dacă asset-ul lipsește *} -Avatar - -{* Cu fallback *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Îmbunătățiți performanța de încărcare a paginii: - -```latte -{* În secțiunea *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Generează link-uri de preîncărcare adecvate: - -```latte - - - -``` - - -Funcționalități Avansate -======================== - - -Auto-Detecția Extensiilor -------------------------- - -Gestionați automat mai multe formate: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Încearcă în ordine -``` - -Acum puteți solicita fără extensie: - -```latte -{* Găsește automat logo.webp, logo.jpg sau logo.png *} -{asset 'images:logo'} -``` - -Perfect pentru îmbunătățirea progresivă cu formate moderne. - - -Versionare Inteligentă ----------------------- - -Fișierele sunt versionate automat pe baza timpului de modificare: - -```latte -{asset 'style.css'} -{* Ieșire: *} -``` - -Când actualizați fișierul, timestamp-ul se modifică, forțând reîmprospătarea cache-ului browserului. - -Controlați versionarea per asset: - -```php -// Dezactivează versionarea pentru un asset specific -$asset = $assets->getAsset('style.css', ['version' => false]); - -// În Latte -{asset 'style.css', version: false} -``` - - -Asset-uri Font --------------- - -Fonturile beneficiază de un tratament special cu CORS adecvat: - -```latte -{* Preîncărcare corectă cu crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Utilizați în CSS *} - -``` - - -Mapperi Personalizați -===================== - -Creați mapperi personalizați pentru nevoi speciale, cum ar fi stocarea în cloud sau generarea dinamică: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Înregistrați în configurare: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Utilizați ca orice alt mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Metoda `Helpers::createAssetFromUrl()` creează automat tipul corect de asset pe baza extensiei fișierului. - - -Lectură suplimentară -==================== - -- [Nette Assets: În sfârșit, API unificat pentru orice, de la imagini la Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/ro/@left-menu.texy b/assets/ro/@left-menu.texy deleted file mode 100644 index 654f12eacf..0000000000 --- a/assets/ro/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Noțiuni de bază |@home] -- [Vite |vite] -- [Configurare |Configuration] diff --git a/assets/ro/@meta.texy b/assets/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/assets/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/assets/ro/configuration.texy b/assets/ro/configuration.texy deleted file mode 100644 index 75d20c374a..0000000000 --- a/assets/ro/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Configurarea Asset-urilor -************************* - -.[perex] -Prezentare generală a opțiunilor de configurare pentru Nette Assets. - - -```neon -assets: - # cale de bază pentru rezolvarea căilor relative ale mapper-ilor - basePath: ... # (șir de caractere) implicit %wwwDir% - - # URL de bază pentru rezolvarea URL-urilor relative ale mapper-ilor - baseUrl: ... # (șir de caractere) implicit %baseUrl% - - # activează versionarea asset-urilor global? - versioning: ... # (boolean) implicit true - - # definește mapper-ii de asset-uri - mapping: ... # (array) implicit cale 'assets' -``` - -`basePath` setează directorul implicit al sistemului de fișiere pentru rezolvarea căilor relative în mapperi. Implicit, utilizează directorul web (`%wwwDir%`). - -`baseUrl` setează prefixul URL implicit pentru rezolvarea URL-urilor relative în mapperi. Implicit, utilizează URL-ul rădăcină (`%baseUrl%`). - -Opțiunea `versioning` controlează global dacă parametrii de versiune sunt adăugați la URL-urile asset-urilor pentru invalidarea cache-ului. Mapperii individuali pot suprascrie această setare. - - -Mapperi -------- - -Mapperii pot fi configurați în trei moduri: notație simplă de șir, notație detaliată de array sau ca referință la un serviciu. - -Cel mai simplu mod de a defini un mapper: - -```neon -assets: - mapping: - default: assets # Creează un mapper de sistem de fișiere pentru %wwwDir%/assets/ - images: img # Creează un mapper de sistem de fișiere pentru %wwwDir%/img/ - scripts: js # Creează un mapper de sistem de fișiere pentru %wwwDir%/js/ -``` - -Fiecare mapper creează un `FilesystemMapper` care: -- Caută fișiere în `%wwwDir%/` -- Generează URL-uri precum `%baseUrl%/` -- Moștenește setarea globală de versionare - - -Pentru mai mult control, utilizați notația detaliată: - -```neon -assets: - mapping: - images: - # directorul unde sunt stocate fișierele - path: ... # (șir de caractere) opțional, implicit '' - - # prefix URL pentru link-urile generate - url: ... # (șir de caractere) opțional, implicit cale - - # activează versionarea pentru acest mapper? - versioning: ... # (boolean) opțional, moștenește setarea globală - - # adaugă automat extensie(i) la căutarea fișierelor - extension: ... # (șir de caractere|array) opțional, implicit null -``` - -Înțelegerea modului în care valorile de configurare sunt rezolvate: - -Rezolvarea Căii: - - Căile relative sunt rezolvate din `basePath` (sau `%wwwDir%` dacă `basePath` nu este setat) - - Căile absolute sunt utilizate ca atare - -Rezolvarea URL-ului: - - URL-urile relative sunt rezolvate din `baseUrl` (sau `%baseUrl%` dacă `baseUrl` nu este setat) - - URL-urile absolute (cu schemă sau `//`) sunt utilizate ca atare - - Dacă `url` nu este specificat, utilizează valoarea `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Cale și URL relativ - images: - path: img # Rezolvat la: /var/www/project/www/img - url: images # Rezolvat la: https://example.com/assets/images - - # Cale și URL absolut - uploads: - path: /var/shared/uploads # Utilizat ca atare: /var/shared/uploads - url: https://cdn.example.com # Utilizat ca atare: https://cdn.example.com - - # Doar calea specificată - styles: - path: css # Cale: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Mapperi Personalizați ---------------------- - -Pentru mapperi personalizați, referențiați sau definiți un serviciu: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Mapper-ul Vite necesită doar adăugarea `type: vite`. Aceasta este o listă completă de opțiuni de configurare: - -```neon -assets: - mapping: - default: - # tip mapper (obligatoriu pentru Vite) - type: vite # (șir de caractere) obligatoriu, trebuie să fie 'vite' - - # directorul de ieșire al construirii Vite - path: ... # (șir de caractere) opțional, implicit '' - - # prefix URL pentru asset-urile construite - url: ... # (șir de caractere) opțional, implicit cale - - # locația fișierului manifest Vite - manifest: ... # (șir de caractere) opțional, implicit /.vite/manifest.json - - # configurare server de dezvoltare Vite - devServer: ... # (boolean|șir de caractere) opțional, implicit true - - # versionare pentru fișierele din directorul public - versioning: ... # (boolean) opțional, moștenește setarea globală - - # auto-extensie pentru fișierele din directorul public - extension: ... # (șir de caractere|array) opțional, implicit null -``` - -Opțiunea `devServer` controlează modul în care asset-urile sunt încărcate în timpul dezvoltării: - -- `true` (implicit) - Detectează automat serverul de dezvoltare Vite pe gazda și portul curente. Dacă serverul de dezvoltare rulează **și aplicația dumneavoastră este în modul de depanare**, asset-urile sunt încărcate de la acesta cu suport pentru înlocuirea la cald a modulelor (HMR). Dacă serverul de dezvoltare nu rulează, asset-urile sunt încărcate din fișierele construite din directorul public. -- `false` - Dezactivează complet integrarea serverului de dezvoltare. Asset-urile sunt întotdeauna încărcate din fișierele construite. -- URL personalizat (de ex., `https://localhost:5173`) - Specificați manual URL-ul serverului de dezvoltare, inclusiv protocolul și portul. Util atunci când serverul de dezvoltare rulează pe o altă gazdă sau port. - -Opțiunile `versioning` și `extension` se aplică doar fișierelor din directorul public al Vite care nu sunt procesate de Vite. - - -Configurare Manuală -------------------- - -Când nu utilizați Nette DI, configurați mapperii manual: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Adaugă mapper de sistem de fișiere -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Adaugă mapper Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/ro/vite.texy b/assets/ro/vite.texy deleted file mode 100644 index 3415ffd29f..0000000000 --- a/assets/ro/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Integrare Vite -************** - -
    - -Aplicațiile JavaScript moderne necesită instrumente de construire sofisticate. Nette Assets oferă o integrare de primă clasă cu [Vite |https://vitejs.dev/], instrumentul de construire frontend de ultimă generație. Obțineți o dezvoltare ultra-rapidă cu Hot Module Replacement (HMR) și build-uri de producție optimizate, fără bătăi de cap legate de configurare. - -- **Zero configurare** - punte automată între Vite și șabloanele PHP -- **Gestionare completă a dependențelor** - un singur tag gestionează toate asset-urile -- **Hot Module Replacement** - actualizări instantanee JavaScript și CSS -- **Build-uri de producție optimizate** - împărțirea codului și tree shaking - -
    - - -Nette Assets se integrează perfect cu Vite, astfel încât obțineți toate aceste beneficii în timp ce scrieți șabloanele ca de obicei. - - -Configurarea Vite -================= - -Să configurăm Vite pas cu pas. Nu vă faceți griji dacă sunteți nou în lumea instrumentelor de construire - vom explica totul! - - -Pasul 1: Instalarea Vite ------------------------- - -Mai întâi, instalați Vite și plugin-ul Nette în proiectul dumneavoastră: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Aceasta instalează Vite și un plugin special care ajută Vite să funcționeze perfect cu Nette. - - -Pasul 2: Structura Proiectului ------------------------------- - -Abordarea standard este de a plasa fișierele asset sursă într-un folder `assets/` în rădăcina proiectului, iar versiunile compilate în `www/assets/`: - -/--pre -web-project/ -├── assets/ ← fișiere sursă (SCSS, TypeScript, imagini sursă) -│ ├── public/ ← fișiere statice (copiate ca atare) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← punct de intrare principal -│ └── style.css ← stilurile dumneavoastră -└── www/ ← director public (rădăcina documentului) - ├── assets/ ← fișierele compilate vor ajunge aici - └── index.php -\-- - -Folderul `assets/` conține fișierele dumneavoastră sursă - codul pe care îl scrieți. Vite va procesa aceste fișiere și va plasa versiunile compilate în `www/assets/`. - - -Pasul 3: Configurarea Vite --------------------------- - -Creați un fișier `vite.config.ts` în rădăcina proiectului dumneavoastră. Acest fișier îi spune lui Vite unde să găsească fișierele sursă și unde să plaseze cele compilate. - -Plugin-ul Nette Vite vine cu setări implicite inteligente care simplifică configurarea. Presupune că fișierele dumneavoastră sursă de frontend se află în directorul `assets/` (opțiunea `root`) și că fișierele compilate ajung în `www/assets/` (opțiunea `outDir`). Trebuie doar să specificați [punctul de intrare|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Dacă doriți să specificați un alt nume de director pentru a construi asset-urile, va trebui să modificați câteva opțiuni: - -```js -export default defineConfig({ - root: 'assets', // directorul rădăcină al asset-urilor sursă - - build: { - outDir: '../www/assets', // unde ajung fișierele compilate - }, - - // ... alte configurații ... -}); -``` - -.[note] -Calea `outDir` este considerată relativă la `root`, de aceea există `../` la început. - - -Pasul 4: Configurarea Nette ---------------------------- - -Spuneți Nette Assets despre Vite în fișierul dumneavoastră `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # îi spune lui Nette să utilizeze ViteMapper - path: assets -``` - - -Pasul 5: Adăugați scripturi ---------------------------- - -Adăugați aceste scripturi în `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Acum puteți: -- `npm run dev` - pornește serverul de dezvoltare cu reîncărcare la cald -- `npm run build` - creează fișiere de producție optimizate - - -Puncte de Intrare -================= - -Un **punct de intrare** este fișierul principal de unde pornește aplicația dumneavoastră. Din acest fișier, importați alte fișiere (CSS, module JavaScript, imagini), creând un arbore de dependențe. Vite urmărește aceste importuri și le grupează pe toate împreună. - -Exemplu de punct de intrare `assets/app.js`: - -```js -// Importă stiluri -import './style.css' - -// Importă module JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inițializează aplicația dumneavoastră -netteForms.initOnLoad(); -naja.initialize(); -``` - -În șablon puteți insera un punct de intrare după cum urmează: - -```latte -{asset 'app.js'} -``` - -Nette Assets generează automat toate tag-urile HTML necesare - JavaScript, CSS și orice alte dependențe. - - -Puncte de Intrare Multiple --------------------------- - -Aplicațiile mai mari necesită adesea puncte de intrare separate: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // pagini publice - 'admin.js', // panou de administrare - ], - }), - ], -}); -``` - -Utilizați-le în diferite șabloane: - -```latte -{* În pagini publice *} -{asset 'app.js'} - -{* În panoul de administrare *} -{asset 'admin.js'} -``` - - -Important: Fișiere Sursă vs. Fișiere Compilate ----------------------------------------------- - -Este crucial să înțelegeți că în producție puteți încărca doar: - -1. **Puncte de intrare** definite în `entry` -2. **Fișiere din directorul `assets/public/`** - -Nu **puteți** încărca utilizând `{asset}` fișiere arbitrare din `assets/` - doar asset-uri referențiate de fișiere JavaScript sau CSS. Dacă fișierul dumneavoastră nu este referențiat nicăieri, nu va fi compilat. Dacă doriți ca Vite să fie conștient de alte asset-uri, le puteți muta în [folderul public |#public-folder]. - -Vă rugăm să rețineți că, implicit, Vite va încorpora toate asset-urile mai mici de 4KB, deci nu veți putea referenția aceste fișiere direct. (Vezi [documentația Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Acesta funcționează - este un punct de intrare *} -{asset 'app.js'} - -{* ✓ Acesta funcționează - este în assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Acesta nu va funcționa - fișier aleatoriu în assets/ *} -{asset 'components/button.js'} -``` - - -Modul de Dezvoltare -=================== - -Modul de dezvoltare este complet opțional, dar oferă beneficii semnificative atunci când este activat. Principalul avantaj este **Hot Module Replacement (HMR)** - vedeți modificările instantaneu fără a pierde starea aplicației, făcând experiența de dezvoltare mult mai fluidă și mai rapidă. - -Vite este un instrument modern de construire care face dezvoltarea incredibil de rapidă. Spre deosebire de bundler-ele tradiționale, Vite servește codul dumneavoastră direct browserului în timpul dezvoltării, ceea ce înseamnă pornire instantanee a serverului indiferent de mărimea proiectului și actualizări ultra-rapide. - - -Pornirea Serverului de Dezvoltare ---------------------------------- - -Rulați serverul de dezvoltare: - -```shell -npm run dev -``` - -Veți vedea: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Țineți acest terminal deschis în timpul dezvoltării. - -Plugin-ul Nette Vite detectează automat când: -1. Serverul de dezvoltare Vite rulează -2. Aplicația dumneavoastră Nette este în modul de depanare - -Când ambele condiții sunt îndeplinite, Nette Assets încarcă fișierele de la serverul de dezvoltare Vite în loc de directorul compilat: - -```latte -{asset 'app.js'} -{* În dezvoltare: *} -{* În producție: *} -``` - -Nu este necesară configurare - pur și simplu funcționează! - - -Lucrul pe Domenii Diferite --------------------------- - -Dacă serverul dumneavoastră de dezvoltare rulează pe altceva decât `localhost` (cum ar fi `myapp.local`), s-ar putea să întâmpinați probleme CORS (Cross-Origin Resource Sharing). CORS este o funcționalitate de securitate în browserele web care blochează implicit cererile între domenii diferite. Când aplicația dumneavoastră PHP rulează pe `myapp.local`, dar Vite rulează pe `localhost:5173`, browserul le consideră domenii diferite și blochează cererile. - -Aveți două opțiuni pentru a rezolva acest lucru: - -**Opțiunea 1: Configurați CORS** - -Cea mai simplă soluție este să permiteți cererile cross-origin din aplicația dumneavoastră PHP: - -```js -export default defineConfig({ - // ... alte configurații ... - - server: { - cors: { - origin: 'http://myapp.local', // URL-ul aplicației dumneavoastră PHP - }, - }, -}); -``` -**Opțiunea 2: Rulați Vite pe domeniul dumneavoastră** - -Cealaltă soluție este să faceți Vite să ruleze pe același domeniu ca aplicația dumneavoastră PHP. - -```js -export default defineConfig({ - // ... alte configurații ... - - server: { - host: 'myapp.local', // la fel ca aplicația dumneavoastră PHP - }, -}); -``` - -De fapt, chiar și în acest caz, trebuie să configurați CORS deoarece serverul de dezvoltare rulează pe același hostname, dar pe un port diferit. Totuși, în acest caz, CORS este configurat automat de plugin-ul Nette Vite. - - -Dezvoltare HTTPS ----------------- - -Dacă dezvoltați pe HTTPS, aveți nevoie de certificate pentru serverul dumneavoastră de dezvoltare Vite. Cel mai simplu mod este utilizarea unui plugin care generează certificate automat: - -```shell -npm install -D vite-plugin-mkcert -``` - -Iată cum să-l configurați în `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... alte configurații ... - - plugins: [ - mkcert(), // generează certificate automat și activează https - nette(), - ], -}); -``` - -Rețineți că dacă utilizați configurația CORS (Opțiunea 1 de mai sus), trebuie să actualizați URL-ul de origine pentru a utiliza `https://` în loc de `http://`. - - -Build-uri de Producție -====================== - -Creați fișiere de producție optimizate: - -```shell -npm run build -``` - -Vite va: -- Minifica tot JavaScript-ul și CSS-ul -- Împărți codul în bucăți optime -- Genera nume de fișiere hash-uite pentru invalidarea cache-ului -- Crea un fișier manifest pentru Nette Assets - -Exemplu de ieșire: - -``` -www/assets/ -├── app-4f3a2b1c.js # JavaScript-ul dumneavoastră principal (minificat) -├── app-7d8e9f2a.css # CSS extras (minificat) -├── vendor-8c4b5e6d.js # Dependențe partajate -└── .vite/ - └── manifest.json # Mapare pentru Nette Assets -``` - -Numele de fișiere hash-uite asigură că browserele încarcă întotdeauna cea mai recentă versiune. - - -Folder Public -============= - -Fișierele din directorul `assets/public/` sunt copiate în ieșire fără procesare: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Referențiați-le în mod normal: - -```latte -{* Aceste fișiere sunt copiate ca atare *} - - -``` - -Pentru fișierele publice, puteți utiliza funcționalitățile FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Încearcă WebP mai întâi - versioning: true # Adaugă invalidare cache -``` - -În configurația `vite.config.ts` puteți schimba folderul public utilizând opțiunea `publicDir`. - - -Importuri Dinamice -================== - -Vite împarte automat codul pentru o încărcare optimă. Importurile dinamice vă permit să încărcați codul doar atunci când este efectiv necesar, reducând dimensiunea inițială a bundle-ului: - -```js -// Încarcă componente grele la cerere -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Importurile dinamice creează bucăți separate care sunt încărcate doar atunci când este necesar. Acesta se numește "code splitting" și este una dintre cele mai puternice funcționalități ale Vite. Când utilizați importuri dinamice, Vite creează automat fișiere JavaScript separate pentru fiecare modul importat dinamic. - -Tag-ul `{asset 'app.js'}` **nu** preîncarcă automat aceste bucăți dinamice. Acesta este un comportament intenționat - nu dorim să descărcăm cod care s-ar putea să nu fie folosit niciodată. Bucățile sunt descărcate doar atunci când importul dinamic este executat. - -Totuși, dacă știți că anumite importuri dinamice sunt critice și vor fi necesare în curând, le puteți preîncărca: - -```latte -{* Punct de intrare principal *} -{asset 'app.js'} - -{* Preîncarcă importuri dinamice critice *} -{preload 'components/chart.js'} -``` - -Acest lucru îi spune browserului să descarce componenta grafic în fundal, astfel încât să fie gata imediat când este necesar. - - -Suport TypeScript -================= - -TypeScript funcționează imediat: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Referențiați fișierele TypeScript în mod normal: - -```latte -{asset 'main.ts'} -``` - -Pentru suport complet TypeScript, instalați-l: - -```shell -npm install -D typescript -``` - - -Configurație Suplimentară Vite -============================== - -Iată câteva opțiuni utile de configurare Vite cu explicații detaliate: - -```js -export default defineConfig({ - // Directorul rădăcină care conține asset-urile sursă - root: 'assets', - - // Folderul al cărui conținut este copiat în directorul de ieșire ca atare - // Implicit: 'public' (relativ la 'root') - publicDir: 'public', - - build: { - // Unde să plasezi fișierele compilate (relativ la 'root') - outDir: '../www/assets', - - // Golește directorul de ieșire înainte de construire? - // Util pentru a elimina fișierele vechi din build-urile anterioare - emptyOutDir: true, - - // Subdirector în outDir pentru bucățile și asset-urile generate - // Acest lucru ajută la organizarea structurii de ieșire - assetsDir: 'static', - - rollupOptions: { - // Punct(e) de intrare - poate fi un singur fișier sau un array de fișiere - // Fiecare punct de intrare devine un bundle separat - input: [ - 'app.js', // aplicația principală - 'admin.js', // panoul de administrare - ], - }, - }, - - server: { - // Gazda la care să se lege serverul de dezvoltare - // Utilizați '0.0.0.0' pentru a expune la rețea - host: 'localhost', - - // Port pentru serverul de dezvoltare - port: 5173, - - // Configurare CORS pentru cererile cross-origin - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Activează hărțile sursă CSS în dezvoltare - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Asta e tot! Acum aveți un sistem de construire modern integrat cu Nette Assets. diff --git a/assets/ru/@home.texy b/assets/ru/@home.texy index d4e2b0e30c..2f70db4b47 100644 --- a/assets/ru/@home.texy +++ b/assets/ru/@home.texy @@ -3,21 +3,21 @@ Nette Assets
    -Устали вручную управлять статическими файлами в ваших веб-приложениях? Забудьте о жестком кодировании путей, проблемах с инвалидацией кэша или беспокойстве о версионировании файлов. Nette Assets преобразует ваш способ работы с изображениями, таблицами стилей, скриптами и другими статическими ресурсами. +Устали вручную управлять статическими файлами в своих веб-приложениях? Забудьте о жёстко прописанных путях, возне со сбросом кеша и заботах о версиях файлов. Nette Assets меняет то, как вы работаете с изображениями, таблицами стилей, скриптами и другими статическими ресурсами. -- **Умное версионирование** гарантирует, что браузеры всегда загружают последние версии файлов -- **Автоматическое определение** типов и размеров файлов -- **Бесшовная интеграция с Latte** с интуитивно понятными тегами -- **Гибкая архитектура**, поддерживающая файловые системы, CDN и Vite -- **Ленивая загрузка** для оптимальной производительности +- **Умное версионирование** обеспечивает, что браузеры всегда загружают свежие файлы +- **Автоматическое определение** типов файлов и размеров +- **Бесшовная интеграция с Latte** через интуитивные теги +- **Гибкая архитектура** с поддержкой файловых систем, CDN и Vite +- **Ленивая загрузка** ради максимальной производительности
    -Зачем Nette Assets? -=================== +Зачем нужен Nette Assets? +========================= -Работа со статическими файлами часто означает повторяющийся, подверженный ошибкам код. Вы вручную конструируете URL, добавляете параметры версии для обхода кэша и по-разному обрабатываете различные типы файлов. Это приводит к такому коду: +Работа со статическими файлами часто означает повторяющийся код, в котором легко ошибиться. Вы вручную составляете URL, добавляете параметры версии для сброса кеша и по-разному обрабатываете разные типы файлов. Это приводит к коду вроде такого: ```latte Logo @@ -27,19 +27,19 @@ Nette Assets С Nette Assets вся эта сложность исчезает: ```latte -{* Everything automated - URL, versioning, dimensions *} +{* Всё автоматизировано - URL, версионирование, размеры *} -{* Or just *} +{* Или просто *} {asset 'css/style.css'} ``` -Вот и все! Библиотека автоматически: -- Добавляет параметры версии на основе времени модификации файла -- Определяет размеры изображения и включает их в HTML -- Генерирует правильный HTML-элемент для каждого типа файла -- Обрабатывает как среды разработки, так и производственные среды +Вот и всё! Библиотека автоматически: +- добавляет параметры версии по времени изменения файла +- определяет размеры изображений и включает их в HTML +- порождает правильный HTML-элемент для каждого типа файла +- справляется и со средой разработки, и с продакшном Установка @@ -51,95 +51,98 @@ Nette Assets composer require nette/assets ``` -Требуется PHP 8.1 или выше, и он отлично работает с Nette Framework, но также может использоваться автономно. +Он требует PHP 8.1 или новее и прекрасно работает с Nette Framework, но может использоваться и самостоятельно. Первые шаги =========== -Nette Assets работает из коробки без какой-либо конфигурации. Разместите свои статические файлы в каталоге `www/assets/` и начните их использовать: +Nette Assets работает сразу, без всякой настройки. Поместите свои статические файлы в каталог `www/assets/` и начинайте их использовать: ```latte -{* Display an image with automatic dimensions *} +{* Показываем изображение с автоматическими размерами *} {asset 'logo.png'} -{* Include a stylesheet with versioning *} +{* Подключаем таблицу стилей с версионированием *} {asset 'style.css'} -{* Load a JavaScript module *} +{* Загружаем скрипт *} {asset 'app.js'} ``` -Для большего контроля над генерируемым HTML используйте атрибут `n:asset` или функцию `asset()`. +Для большего контроля над порождаемым HTML используйте атрибут `n:asset` или функцию `asset()`. Как это работает ================ -Nette Assets построен на трех основных концепциях, которые делают его мощным, но простым в использовании: +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 +- **Таблицы стилей**: медиазапросы, целостность +- **Аудио и видео**: длительность, размеры (только видео) +- **Шрифты**: правильная предзагрузка с CORS -Библиотека автоматически определяет типы файлов и создает соответствующий класс актива. +Библиотека автоматически определяет типы файлов и создаёт подходящий класс ресурса. -Сопоставители - Откуда берутся файлы ------------------------------------- +Мапперы: откуда берутся файлы +----------------------------- -**Сопоставитель** знает, как находить файлы и создавать для них URL. У вас может быть несколько сопоставителей для разных целей - локальные файлы, CDN, облачное хранилище или инструменты сборки (каждый из них имеет имя). Встроенный `FilesystemMapper` обрабатывает локальные файлы, а `ViteMapper` интегрируется с современными инструментами сборки. +**Маппер** знает, как найти файлы и создать для них URL. У вас может быть несколько мапперов для разных задач: локальные файлы, CDN, облачное хранилище или инструменты сборки (у каждого из них есть имя). Встроенный `FilesystemMapper` занимается локальными файлами, а `ViteMapper` интегрируется с современными инструментами сборки. -Сопоставители определяются в [конфигурации |Configuration]. +Мапперы определяются в [конфигурации |configuration]. -Реестр - Ваш основной интерфейс -------------------------------- +Реестр: ваш основной интерфейс +------------------------------ -**Реестр** управляет всеми сопоставителями и предоставляет основной API: +**Реестр** управляет всеми мапперами и предоставляет основной API: ```php -// Inject the registry in your service +// Внедряем реестр в свой сервис public function __construct( private Nette\Assets\Registry $assets ) {} +``` -// Get assets from different mappers -$logo = $this->assets->getAsset('images:logo.png'); // 'image' mapper -$app = $this->assets->getAsset('app:main.js'); // 'app' mapper -$style = $this->assets->getAsset('style.css'); // uses default mapper +```php +// Получаем ресурсы из разных мапперов +$logo = $this->assets->getAsset('images:logo.png'); // маппер 'images' +$app = $this->assets->getAsset('app:main.js'); // маппер 'app' +$style = $this->assets->getAsset('style.css'); // использует маппер по умолчанию ``` -Реестр автоматически выбирает правильный сопоставитель и кэширует результаты для повышения производительности. +Реестр автоматически выбирает нужный маппер и кеширует результаты ради производительности. -Работа с активами в PHP -======================= +Работа с ресурсами в PHP +======================== -Реестр предоставляет два метода для получения активов: +Реестр предоставляет два метода получения ресурсов: ```php -// Throws Nette\Assets\AssetNotFoundException if file doesn't exist +// Выбрасывает Nette\Assets\AssetNotFoundException, если файла нет $logo = $assets->getAsset('logo.png'); -// Returns null if file doesn't exist +// Возвращает null, если файла нет $banner = $assets->tryGetAsset('banner.jpg'); if ($banner) { echo $banner->url; @@ -147,80 +150,83 @@ if ($banner) { ``` -Указание сопоставителей ------------------------ +Указание мапперов +----------------- -Вы можете явно выбрать, какой сопоставитель использовать: +Вы можете явно выбрать, какой маппер использовать: ```php -// Use default mapper +// Использовать маппер по умолчанию $file = $assets->getAsset('document.pdf'); -// Use specific mapper with prefix +// Использовать конкретный маппер через приставку $image = $assets->getAsset('images:photo.jpg'); -// Use specific mapper with array syntax +// Использовать конкретный маппер через запись массивом $script = $assets->getAsset(['scripts', 'app.js']); ``` -Свойства и типы активов ------------------------ +Свойства и типы ресурсов +------------------------ -Каждый тип актива предоставляет соответствующие свойства только для чтения: +Каждый тип ресурса даёт подходящие свойства только для чтения: ```php -// Image properties +// Свойства изображения $image = $assets->getAsset('photo.jpg'); echo $image->width; // 1920 echo $image->height; // 1080 echo $image->mimeType; // 'image/jpeg' -// Script properties +// Свойства скрипта $script = $assets->getAsset('app.js'); -echo $script->type; // 'module' or null +echo $script->type; // null ('module' для точек входа Vite) -// Audio properties +// Свойства аудио $audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duration in seconds +echo $audio->duration; // длительность в секундах -// All assets can be cast to string (returns URL) +// Все ресурсы можно привести к строке (вернётся URL) $url = (string) $assets->getAsset('document.pdf'); ``` .[note] -Свойства, такие как размеры или продолжительность, загружаются лениво только при обращении к ним, что обеспечивает быструю работу библиотеки. +Свойства вроде размеров или длительности загружаются лениво, только при обращении, благодаря чему библиотека остаётся быстрой. + +.[tip] +Для точного статического анализа установите расширение [nette/phpstan-rules |tools:phpstan-rules#Assets]. PHPStan тогда знает конкретный тип каждого ресурса, так что `getAsset('photo.jpg')` понимается как `ImageAsset`, а обращение к `->width` не вызывает ошибки. -Использование активов в Latte-шаблонах -====================================== +Использование ресурсов в шаблонах Latte +======================================= -Nette Assets обеспечивает интуитивно понятную [интеграцию с Latte|latte:] с помощью тегов и функций. +Nette Assets предлагает интуитивную интеграцию с [Latte|latte:] через теги и функции. `{asset}` --------- -Тег `{asset}` отображает полные HTML-элементы: +Тег `{asset}` отрисовывает целые HTML-элементы: ```latte -{* Renders: *} +{* Отрисует: *} {asset 'hero.jpg'} -{* Renders: *} +{* Отрисует: *} {asset 'app.js'} -{* Renders: *} +{* Отрисует: *} {asset 'style.css'} ``` Тег автоматически: -- Определяет тип актива и генерирует соответствующий HTML -- Включает версионирование для обхода кэша -- Добавляет размеры для изображений -- Устанавливает правильные атрибуты (type, media и т.д.) +- определяет тип ресурса и порождает подходящий HTML +- добавляет версионирование для сброса кеша +- добавляет размеры для изображений +- задаёт правильные атрибуты (type, media и т. д.) -При использовании внутри HTML-атрибутов он выводит только URL: +Внутри HTML-атрибутов и внутри элементов ` -``` - - -Vlastné Mappery -=============== - -Vytvorte vlastné mappery pre špeciálne potreby, ako je cloudové úložisko alebo dynamické generovanie: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Zaregistrujte v konfigurácii: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Použite ako akýkoľvek iný mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Metóda `Helpers::createAssetFromUrl()` automaticky vytvorí správny typ assetu na základe prípony súboru. - - -Nadaljnje branje -================ - -- [Nette Assets: Končno poenoten API za vse, od slik do Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/sl/@left-menu.texy b/assets/sl/@left-menu.texy deleted file mode 100644 index d7dd6293b8..0000000000 --- a/assets/sl/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Začíname |@home] -- [Vite |vite] -- [Konfigurácia |Configuration] diff --git a/assets/sl/@meta.texy b/assets/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/assets/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/assets/sl/configuration.texy b/assets/sl/configuration.texy deleted file mode 100644 index dd0d7fb301..0000000000 --- a/assets/sl/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Konfigurácia Assets -******************* - -.[perex] -Prehľad možností konfigurácie pre Nette Assets. - - -```neon -assets: - # základná cesta pre rozlíšenie relatívnych ciest mapperov - basePath: ... # (string) predvolené na %wwwDir% - - # základná URL pre rozlíšenie relatívnych URL mapperov - baseUrl: ... # (string) predvolené na %baseUrl% - - # povoliť globálne verzovanie assetov? - versioning: ... # (bool) predvolené na true - - # definuje asset mappery - mapping: ... # (array) predvolené na cestu 'assets' -``` - -`basePath` nastavuje predvolený adresár súborového systému pre rozlíšenie relatívnych ciest v mapperoch. Východiskovo používa webový adresár (`%wwwDir%`). - -`baseUrl` nastavuje predvolený URL prefix pre rozlíšenie relatívnych URL v mapperoch. Východiskovo používa koreňovú URL (`%baseUrl%`). - -Možnosť `versioning` globálne riadi, či sa do URL adries assetov pridávajú parametre verzie pre cache busting. Jednotlivé mappery môžu toto nastavenie prepísať. - - -Mappery -------- - -Mappery môžu byť konfigurované tromi spôsobmi: jednoduchou reťazcovou notáciou, detailnou notáciou poľa alebo ako odkaz na službu. - -Najjednoduchší spôsob definovania mappera: - -```neon -assets: - mapping: - default: assets # Vytvorí filesystem mapper pre %wwwDir%/assets/ - images: img # Vytvorí filesystem mapper pre %wwwDir%/img/ - scripts: js # Vytvorí filesystem mapper pre %wwwDir%/js/ -``` - -Každý mapper vytvorí `FilesystemMapper`, ktorý: -- Hľadá súbory v `%wwwDir%/` -- Generuje URL adresy ako `%baseUrl%/` -- Dedí globálne nastavenie verzovania - - -Pre väčšiu kontrolu použite detailnú notáciu: - -```neon -assets: - mapping: - images: - # adresár, kde sú súbory uložené - path: ... # (string) voliteľné, predvolené na '' - - # URL prefix pre generované odkazy - url: ... # (string) voliteľné, predvolené na path - - # povoliť verzovanie pre tento mapper? - versioning: ... # (bool) voliteľné, dedí globálne nastavenie - - # automaticky pridať príponu(y) pri hľadaní súborov - extension: ... # (string|array) voliteľné, predvolené na null -``` - -Pochopenie, ako sa riešia konfiguračné hodnoty: - -Riešenie ciest: - - Relatívne cesty sa riešia z `basePath` (alebo `%wwwDir%`, ak `basePath` nie je nastavená) - - Absolútne cesty sa používajú tak, ako sú - -Riešenie URL: - - Relatívne URL sa riešia z `baseUrl` (alebo `%baseUrl%`, ak `baseUrl` nie je nastavená) - - Absolútne URL (so schémou alebo `//`) sa používajú tak, ako sú - - Ak `url` nie je špecifikovaná, použije sa hodnota `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relatívna cesta a URL - images: - path: img # Rozlíšené na: /var/www/project/www/img - url: images # Rozlíšené na: https://example.com/assets/images - - # Absolútna cesta a URL - uploads: - path: /var/shared/uploads # Použité tak, ako je: /var/shared/uploads - url: https://cdn.example.com # Použité tak, ako je: https://cdn.example.com - - # Špecifikovaná len cesta - styles: - path: css # Cesta: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Vlastné Mappery ---------------- - -Pre vlastné mappery, odkážte alebo definujte službu: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Vite mapper vyžaduje iba pridanie `type: vite`. Toto je kompletný zoznam konfiguračných možností: - -```neon -assets: - mapping: - default: - # typ mappera (povinný pre Vite) - type: vite # (string) povinné, musí byť 'vite' - - # výstupný adresár Vite buildu - path: ... # (string) voliteľné, predvolené na '' - - # URL prefix pre vybudované assets - url: ... # (string) voliteľné, predvolené na path - - # umiestnenie súboru Vite manifestu - manifest: ... # (string) voliteľné, predvolené na /.vite/manifest.json - - # konfigurácia dev servera Vite - devServer: ... # (bool|string) voliteľné, predvolené na true - - # verzovanie pre súbory vo verejnom adresári - versioning: ... # (bool) voliteľné, dedí globálne nastavenie - - # auto-prípona pre súbory vo verejnom adresári - extension: ... # (string|array) voliteľné, predvolené na null -``` - -Možnosť `devServer` riadi, ako sa assets načítavajú počas vývoja: - -- `true` (predvolené) – Automaticky detekuje Vite dev server na aktuálnom hostiteľovi a porte. Ak dev server beží **a vaša aplikácia je v režime ladenia**, assets sa z neho načítavajú s podporou hot module replacement. Ak dev server nebeží, assets sa načítavajú z vybudovaných súborov vo verejnom adresári. -- `false` – Úplne zakáže integráciu dev servera. Assets sa vždy načítavajú z vybudovaných súborov. -- Vlastná URL (napr. `https://localhost:5173`) – Manuálne špecifikujte URL dev servera vrátane protokolu a portu. Užitočné, keď dev server beží na inom hostiteľovi alebo porte. - -Možnosti `versioning` a `extension` sa vzťahujú iba na súbory vo verejnom adresári Vite, ktoré nie sú spracované Vite. - - -Manuálna konfigurácia ---------------------- - -Ak nepoužívate Nette DI, nakonfigurujte mappery manuálne: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Pridajte filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Pridajte Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/sl/vite.texy b/assets/sl/vite.texy deleted file mode 100644 index 39fa7d687d..0000000000 --- a/assets/sl/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Integrácia Vite -*************** - -
    - -Moderné JavaScript aplikácie vyžadujú sofistikované build nástroje. Nette Assets poskytuje prvotriednu integráciu s [Vite |https://vitejs.dev/], nástrojom na tvorbu frontendu novej generácie. Získajte bleskurýchly vývoj s Hot Module Replacement (HMR) a optimalizované produkčné buildy bez problémov s konfiguráciou. - -- **Nulová konfigurácia** – automatický most medzi Vite a PHP šablónami -- **Kompletná správa závislostí** – jeden tag spracuje všetky assets -- **Hot Module Replacement** – okamžité aktualizácie JavaScriptu a CSS -- **Optimalizované produkčné buildy** – code splitting a tree shaking - -
    - - -Nette Assets sa bezproblémovo integruje s Vite, takže získate všetky tieto výhody, zatiaľ čo svoje šablóny píšete ako obvykle. - - -Nastavenie Vite -=============== - -Poďme nastaviť Vite krok za krokom. Nebojte sa, ak ste nováčik v build nástrojoch – všetko vysvetlíme! - - -Krok 1: Inštalácia Vite ------------------------ - -Najprv nainštalujte Vite a Nette plugin do vášho projektu: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Tým sa nainštaluje Vite a špeciálny plugin, ktorý pomáha Vite perfektne fungovať s Nette. - - -Krok 2: Štruktúra projektu --------------------------- - -Štandardný prístup je umiestniť zdrojové súbory assetov do priečinka `assets/` v koreni vášho projektu a kompilované verzie do `www/assets/`: - -/--pre -web-project/ -├── assets/ ← zdrojové súbory (SCSS, TypeScript, zdrojové obrázky) -│ ├── public/ ← statické súbory (kopírované tak, ako sú) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← hlavný vstupný bod -│ └── style.css ← vaše štýly -└── www/ ← verejný adresár (document root) - ├── assets/ ← sem pôjdu kompilované súbory - └── index.php -\-- - -Priečinok `assets/` obsahuje vaše zdrojové súbory – kód, ktorý píšete. Vite spracuje tieto súbory a umiestni kompilované verzie do `www/assets/`. - - -Krok 3: Konfigurácia Vite -------------------------- - -Vytvorte súbor `vite.config.ts` v koreni vášho projektu. Tento súbor hovorí Vite, kde nájsť vaše zdrojové súbory a kam umiestniť kompilované súbory. - -Nette Vite plugin prichádza s inteligentnými predvolenými nastaveniami, ktoré zjednodušujú konfiguráciu. Predpokladá, že vaše front-end zdrojové súbory sú v adresári `assets/` (možnosť `root`) a kompilované súbory idú do `www/assets/` (možnosť `outDir`). Potrebujete špecifikovať iba [vstupný bod|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Ak chcete špecifikovať iný názov adresára pre build vašich assetov, budete musieť zmeniť niekoľko možností: - -```js -export default defineConfig({ - root: 'assets', // koreňový adresár zdrojových assetov - - build: { - outDir: '../www/assets', // kam idú kompilované súbory - }, - - // ... iná konfigurácia ... -}); -``` - -.[note] -Cesta `outDir` sa považuje za relatívnu k `root`, preto je na začiatku `../`. - - -Krok 4: Konfigurácia Nette --------------------------- - -Povedzte Nette Assets o Vite vo vašom `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # hovorí Nette, aby použilo ViteMapper - path: assets -``` - - -Krok 5: Pridajte skripty ------------------------- - -Pridajte tieto skripty do vášho `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Teraz môžete: -- `npm run dev` – spustiť vývojový server s hot reloadingom -- `npm run build` – vytvoriť optimalizované produkčné súbory - - -Vstupné body -============ - -**Vstupný bod** je hlavný súbor, kde sa spúšťa vaša aplikácia. Z tohto súboru importujete ďalšie súbory (CSS, JavaScript moduly, obrázky), čím vytvárate strom závislostí. Vite sleduje tieto importy a všetko zbalí dohromady. - -Príklad vstupného bodu `assets/app.js`: - -```js -// Import štýlov -import './style.css' - -// Import JavaScript modulov -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inicializujte vašu aplikáciu -netteForms.initOnLoad(); -naja.initialize(); -``` - -V šablóne môžete vložiť vstupný bod nasledovne: - -```latte -{asset 'app.js'} -``` - -Nette Assets automaticky generuje všetky potrebné HTML tagy – JavaScript, CSS a akékoľvek iné závislosti. - - -Viacero vstupných bodov ------------------------ - -Väčšie aplikácie často potrebujú samostatné vstupné body: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // verejné stránky - 'admin.js', // administrátorský panel - ], - }), - ], -}); -``` - -Použite ich v rôznych šablónach: - -```latte -{* Na verejných stránkach *} -{asset 'app.js'} - -{* V administrátorskom paneli *} -{asset 'admin.js'} -``` - - -Dôležité: Zdrojové vs. kompilované súbory ------------------------------------------ - -Je kľúčové pochopiť, že v produkcii môžete načítať iba: - -1. **Vstupné body** definované v `entry` -2. **Súbory z adresára `assets/public/`** - -**Nemôžete** načítať pomocou `{asset}` ľubovoľné súbory z `assets/` – iba assets odkazované JavaScriptovými alebo CSS súbormi. Ak váš súbor nie je nikde odkazovaný, nebude skompilovaný. Ak chcete, aby Vite vedelo o iných assets, môžete ich presunúť do [verejného priečinka |#public folder]. - -Upozorňujeme, že predvolene Vite vloží všetky assets menšie ako 4KB, takže tieto súbory nebudete môcť odkazovať priamo. (Pozri [dokumentáciu Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Toto funguje - je to vstupný bod *} -{asset 'app.js'} - -{* ✓ Toto funguje - je to v assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Toto nebude fungovať - náhodný súbor v assets/ *} -{asset 'components/button.js'} -``` - - -Vývojový režim -============== - -Vývojový režim je úplne voliteľný, ale pri jeho povolením poskytuje značné výhody. Hlavnou výhodou je **Hot Module Replacement (HMR)** – okamžité zobrazenie zmien bez straty stavu aplikácie, čo robí vývoj oveľa plynulejším a rýchlejším. - -Vite je moderný build nástroj, ktorý robí vývoj neuveriteľne rýchlym. Na rozdiel od tradičných bundlerov, Vite počas vývoja servíruje váš kód priamo do prehliadača, čo znamená okamžitý štart servera bez ohľadu na veľkosť vášho projektu a bleskurýchle aktualizácie. - - -Spustenie vývojového servera ----------------------------- - -Spustite vývojový server: - -```shell -npm run dev -``` - -Uvidíte: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Tento terminál nechajte otvorený počas vývoja. - -Nette Vite plugin automaticky detekuje, keď: -1. Vite dev server beží -2. Vaša Nette aplikácia je v režime ladenia - -Keď sú splnené obe podmienky, Nette Assets načíta súbory z Vite dev servera namiesto kompilovaného adresára: - -```latte -{asset 'app.js'} -{* Vo vývoji: *} -{* V produkcii: *} -``` - -Nie je potrebná žiadna konfigurácia – jednoducho to funguje! - - -Práca na rôznych doménach -------------------------- - -Ak váš vývojový server beží na niečom inom ako `localhost` (napríklad `myapp.local`), môžete naraziť na problémy s CORS (Cross-Origin Resource Sharing). CORS je bezpečnostná funkcia vo webových prehliadačoch, ktorá predvolene blokuje požiadavky medzi rôznymi doménami. Keď vaša PHP aplikácia beží na `myapp.local`, ale Vite beží na `localhost:5173`, prehliadač ich považuje za rôzne domény a blokuje požiadavky. - -Máte dve možnosti, ako to vyriešiť: - -**Možnosť 1: Konfigurácia CORS** - -Najjednoduchším riešením je povoliť cross-origin požiadavky z vašej PHP aplikácie: - -```js -export default defineConfig({ - // ... iná konfigurácia ... - - server: { - cors: { - origin: 'http://myapp.local', // URL vašej PHP aplikácie - }, - }, -}); -``` -**Možnosť 2: Spustite Vite na vašej doméne** - -Ďalším riešením je spustiť Vite na rovnakej doméne ako vaša PHP aplikácia. - -```js -export default defineConfig({ - // ... iná konfigurácia ... - - server: { - host: 'myapp.local', // rovnaké ako vaša PHP aplikácia - }, -}); -``` - -V skutočnosti aj v tomto prípade musíte nakonfigurovať CORS, pretože dev server beží na rovnakom hostname, ale na inom porte. V tomto prípade však CORS automaticky konfiguruje Nette Vite plugin. - - -Vývoj s HTTPS -------------- - -Ak vyvíjate na HTTPS, potrebujete certifikáty pre váš Vite vývojový server. Najjednoduchší spôsob je použiť plugin, ktorý automaticky generuje certifikáty: - -```shell -npm install -D vite-plugin-mkcert -``` - -Tu je návod, ako ho nakonfigurovať v `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... iná konfigurácia ... - - plugins: [ - mkcert(), // automaticky generuje certifikáty a povolí https - nette(), - ], -}); -``` - -Upozorňujeme, že ak používate konfiguráciu CORS (možnosť 1 z vyššie uvedených), musíte aktualizovať URL pôvodu, aby používala `https://` namiesto `http://`. - - -Produkčné buildy -================ - -Vytvorte optimalizované produkčné súbory: - -```shell -npm run build -``` - -Vite bude: -- Minifikovať všetok JavaScript a CSS -- Rozdeliť kód na optimálne časti -- Generovať hashované názvy súborov pre cache-busting -- Vytvoriť manifest súbor pre Nette Assets - -Príklad výstupu: - -``` -www/assets/ -├── app-4f3a2b1c.js # Váš hlavný JavaScript (minifikovaný) -├── app-7d8e9f2a.css # Extrahovaný CSS (minifikovaný) -├── vendor-8c4b5e6d.js # Zdieľané závislosti -└── .vite/ - └── manifest.json # Mapovanie pre Nette Assets -``` - -Hashované názvy súborov zaisťujú, že prehliadače vždy načítajú najnovšiu verziu. - - -Verejný priečinok -================= - -Súbory v adresári `assets/public/` sú kopírované do výstupu bez spracovania: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Odkazujte na ne normálne: - -```latte -{* Tieto súbory sú kopírované tak, ako sú *} - - -``` - -Pre verejné súbory môžete použiť funkcie FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Skúste najprv WebP - versioning: true # Pridajte cache-busting -``` - -V konfigurácii `vite.config.ts` môžete zmeniť verejný priečinok pomocou možnosti `publicDir`. - - -Dynamické importy -================= - -Vite automaticky rozdeľuje kód pre optimálne načítanie. Dynamické importy vám umožňujú načítať kód iba vtedy, keď je skutočne potrebný, čím sa znižuje počiatočná veľkosť balíka: - -```js -// Načítajte ťažké komponenty na požiadanie -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Dynamické importy vytvárajú samostatné časti, ktoré sa načítavajú iba vtedy, keď sú potrebné. Toto sa nazýva „code splitting“ a je to jedna z najvýkonnejších funkcií Vite. Keď použijete dynamické importy, Vite automaticky vytvorí samostatné JavaScript súbory pre každý dynamicky importovaný modul. - -Tag `{asset 'app.js'}` automaticky **neprednačítava** tieto dynamické časti. Toto je zámerné správanie – nechceme sťahovať kód, ktorý sa možno nikdy nepoužije. Časti sa sťahujú iba vtedy, keď sa vykoná dynamický import. - -Ak však viete, že určité dynamické importy sú kritické a budú čoskoro potrebné, môžete ich prednačítať: - -```latte -{* Hlavný vstupný bod *} -{asset 'app.js'} - -{* Prednačítajte kritické dynamické importy *} -{preload 'components/chart.js'} -``` - -Týmto sa prehliadaču povie, aby stiahol komponent grafu na pozadí, takže je okamžite pripravený, keď je potrebný. - - -Podpora TypeScriptu -=================== - -TypeScript funguje hneď po vybalení: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Odkazujte na TypeScript súbory normálne: - -```latte -{asset 'main.ts'} -``` - -Pre plnú podporu TypeScriptu ho nainštalujte: - -```shell -npm install -D typescript -``` - - -Dodatočná konfigurácia Vite -=========================== - -Tu sú niektoré užitočné možnosti konfigurácie Vite s podrobnými vysvetleniami: - -```js -export default defineConfig({ - // Koreňový adresár obsahujúci zdrojové assets - root: 'assets', - - // Priečinok, ktorého obsah sa kopíruje do výstupného adresára tak, ako je - // Predvolené: 'public' (relatívne k 'root') - publicDir: 'public', - - build: { - // Kam umiestniť skompilované súbory (relatívne k 'root') - outDir: '../www/assets', - - // Vyprázdniť výstupný adresár pred buildom? - // Užitočné na odstránenie starých súborov z predchádzajúcich buildov - emptyOutDir: true, - - // Podadresár v rámci outDir pre generované časti a assets - // To pomáha organizovať výstupnú štruktúru - assetsDir: 'static', - - rollupOptions: { - // Vstupný(é) bod(y) - môže byť jeden súbor alebo pole súborov - // Každý vstupný bod sa stáva samostatným balíkom - input: [ - 'app.js', // hlavná aplikácia - 'admin.js', // administrátorský panel - ], - }, - }, - - server: { - // Hostiteľ, na ktorý sa má naviazať dev server - // Použite '0.0.0.0' na vystavenie do siete - host: 'localhost', - - // Port pre dev server - port: 5173, - - // Konfigurácia CORS pre cross-origin požiadavky - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Povoliť CSS source mapy vo vývoji - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -To je všetko! Teraz máte moderný build systém integrovaný s Nette Assets. diff --git a/assets/tr/@home.texy b/assets/tr/@home.texy index f5961b769c..6bd13bc926 100644 --- a/assets/tr/@home.texy +++ b/assets/tr/@home.texy @@ -3,13 +3,13 @@ Nette Assets
    -Web uygulamalarınızdaki statik dosyaları manuel olarak yönetmekten yoruldunuz mu? Yolları elle kodlamayı, önbellek geçersiz kılma sorunlarıyla uğraşmayı veya dosya sürümlemeyi dert etmeyi unutun. Nette Assets, görseller, stil sayfaları, betikler ve diğer statik kaynaklarla çalışma şeklinizi dönüştürür. +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 güncel dosyaları yüklemesini sağlar -- Dosya türlerinin ve boyutlarının **otomatik algılanması** -- Sezgisel etiketlerle **sorunsuz Latte entegrasyonu** +- **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** -- Optimal performans için **tembel yükleme** +- En iyi başarım için **tembel yükleme**
    @@ -17,7 +17,7 @@ Web uygulamalarınızdaki statik dosyaları manuel olarak yönetmekten yoruldunu Neden Nette Assets? =================== -Statik dosyalarla çalışmak genellikle tekrarlayan, hataya açık kod anlamına gelir. URL'leri manuel olarak oluşturur, önbelleği temizlemek için sürüm parametreleri eklersiniz ve farklı dosya türlerini farklı şekilde ele alırsınız. Bu, şöyle bir koda yol açar: +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 @@ -31,15 +31,15 @@ Nette Assets ile tüm bu karmaşıklık ortadan kalkar: -{* Veya sadece *} +{* Ya da yalnızca *} {asset 'css/style.css'} ``` -Hepsi bu kadar! Kütüphane otomatik olarak: -- Dosya değiştirme zamanına göre sürüm parametreleri ekler -- Görsel boyutlarını algılar ve bunları HTML'ye dahil eder -- Her dosya türü için doğru HTML öğesini oluşturur -- Hem geliştirme hem de üretim ortamlarını yönetir +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 @@ -51,89 +51,92 @@ Nette Assets'i [Composer|best-practices:composer] kullanarak kurun: composer require nette/assets ``` -PHP 8.1 veya daha yüksek bir sürüm gerektirir ve Nette Framework ile mükemmel çalışır, ancak bağımsız olarak da kullanılabilir. +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 yerleştirin ve kullanmaya başlayın: +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 -{* Otomatik boyutlarla bir görseli göster *} +{* Bir görseli otomatik boyutlarla göster *} {asset 'logo.png'} -{* Sürümlemeli bir stil sayfası dahil et *} +{* Sürümlemeyle bir stil sayfası ekle *} {asset 'style.css'} -{* Bir JavaScript modülünü yükle *} +{* Bir betik yükle *} {asset 'app.js'} ``` -Oluşturulan HTML üzerinde daha fazla kontrol için `n:asset` niteliğini veya `asset()` fonksiyonunu kullanın. +Üretilen HTML üzerinde daha fazla denetim için `n:asset` niteliğini ya da `asset()` fonksiyonunu kullanın. Nasıl Çalışır ============= -Nette Assets, güçlü ama kullanımı basit olmasını sağlayan üç temel konsept üzerine kuruludur: +Nette Assets, onu güçlü ama kullanımı basit kılan üç temel kavram üzerine kurulmuştur: -Varlıklar (Assets) - Dosyalarınız Akıllandı -------------------------------------------- +Varlıklar - Akıllanmış Dosyalarınız +----------------------------------- -Bir **varlık (asset)**, uygulamanızdaki herhangi bir statik dosyayı temsil eder. Her dosya, kullanışlı salt okunur özelliklere sahip bir nesne haline gelir: +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 türleri farklı özellikler sağlar: +Farklı dosya tipleri farklı özellikler sunar: - **Görseller**: genişlik, yükseklik, alternatif metin, tembel yükleme -- **Betikler**: modül türü, bütünlük hash'leri, crossorigin +- **Betikler**: modül tipi, bütünlük hash'leri, crossorigin - **Stil sayfaları**: medya sorguları, bütünlük -- **Ses/Video**: süre, boyutlar -- **Fontlar**: uygun CORS ile ön yükleme +- **Ses/Video**: süre, boyutlar (yalnızca video) +- **Yazı tipleri**: CORS ile doğru önyükleme -Kütüphane, dosya türlerini otomatik olarak algılar ve uygun varlık sınıfını oluşturur. +Kütüphane dosya tiplerini otomatik algılar ve uygun varlık sınıfını oluşturur. -Eşleştiriciler (Mappers) - Dosyalar Nereden Geliyor ---------------------------------------------------- +Mapper'lar - Dosyaların Geldiği Yer +----------------------------------- -Bir **eşleştirici (mapper)**, dosyaları nasıl bulacağını ve onlar için URL'leri nasıl oluşturacağını bilir. Farklı amaçlar için birden fazla eşleştiriciniz olabilir - yerel dosyalar, CDN, bulut depolama veya derleme araçları (her birinin bir adı vardır). Yerleşik `FilesystemMapper` yerel dosyaları yönetirken, `ViteMapper` modern derleme araçlarıyla entegre olur. +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. -Eşleştiriciler [yapılandırma |Configuration] içinde tanımlanır. +Mapper'lar [yapılandırmada |configuration] tanımlanır. -Kayıt Defteri (Registry) - Ana Arayüzünüz ------------------------------------------ +Registry - Ana Arayüzünüz +------------------------- -**Kayıt defteri (registry)**, tüm eşleştiricileri yönetir ve ana API'yi sağlar: +**Registry**, tüm mapper'ları yönetir ve ana API'yi sunar: ```php -// Kayıt defterini servisinizde enjekte edin +// Registry'yi servisinize enjekte edin public function __construct( private Nette\Assets\Registry $assets ) {} +``` -// Farklı eşleştiricilerden varlıkları al -$logo = $this->assets->getAsset('images:logo.png'); // 'image' eşleştiricisi -$app = $this->assets->getAsset('app:main.js'); // 'app' eşleştiricisi -$style = $this->assets->getAsset('style.css'); // varsayılan eşleştiriciyi kullanır +```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 ``` -Kayıt defteri, doğru eşleştiriciyi otomatik olarak seçer ve performans için sonuçları önbelleğe alı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 ========================== -Kayıt Defteri, varlıkları almak için iki metot sağlar: +Registry, varlıkları almak için iki metot sunar: ```php // Dosya yoksa Nette\Assets\AssetNotFoundException fırlatır @@ -147,27 +150,27 @@ if ($banner) { ``` -Eşleştiricileri Belirtme ------------------------- +Mapper Belirtme +--------------- -Hangi eşleştiricinin kullanılacağını açıkça seçebilirsiniz: +Hangi mapper'ın kullanılacağını açıkça seçebilirsiniz: ```php -// Varsayılan eşleştiriciyi kullan +// Varsayılan mapper'ı kullan $file = $assets->getAsset('document.pdf'); -// Önekle belirli bir eşleştiriciyi kullan +// Belirli bir mapper'ı önekle kullan $image = $assets->getAsset('images:photo.jpg'); -// Dizi sözdizimiyle belirli bir eşleştiriciyi kullan +// Belirli bir mapper'ı dizi sözdizimiyle kullan $script = $assets->getAsset(['scripts', 'app.js']); ``` -Varlık Özellikleri ve Türleri +Varlık Özellikleri ve Tipleri ----------------------------- -Her varlık türü ilgili salt okunur özellikler sağlar: +Her varlık tipi, ilgili salt okunur özellikleri sunar: ```php // Görsel özellikleri @@ -178,7 +181,7 @@ echo $image->mimeType; // 'image/jpeg' // Betik özellikleri $script = $assets->getAsset('app.js'); -echo $script->type; // 'module' veya null +echo $script->type; // null (Vite giriş noktaları için 'module') // Ses özellikleri $audio = $assets->getAsset('song.mp3'); @@ -189,38 +192,41 @@ $url = (string) $assets->getAsset('document.pdf'); ``` .[note] -Boyutlar veya süre gibi özellikler, kütüphaneyi hızlı tutmak için yalnızca erişildiğinde tembelce yüklenir. +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 sağlar. +Nette Assets, etiketler ve fonksiyonlarla sezgisel [Latte|latte:] entegrasyonu sunar. `{asset}` --------- -`{asset}` etiketi, eksiksiz HTML öğeleri oluşturur: +`{asset}` etiketi tam HTML elemanları render eder: ```latte -{* Oluşturur: *} +{* Render eder: *} {asset 'hero.jpg'} -{* Oluşturur: *} +{* Render eder: *} {asset 'app.js'} -{* Oluşturur: *} +{* Render eder: *} {asset 'style.css'} ``` Etiket otomatik olarak: -- Varlık türünü algılar ve uygun HTML'yi oluşturur -- Önbellek temizleme için sürümlemeyi dahil eder +- 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 (tür, medya vb.) ayarlar +- Doğru nitelikleri ayarlar (type, media vb.) -HTML nitelikleri içinde kullanıldığında, yalnızca URL'yi çıktı verir: +HTML niteliklerinin içinde ya da ` -``` - - -Користувацькі мапери -==================== - -Створюйте користувацькі мапери для особливих потреб, таких як хмарне сховище або динамічна генерація: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Зареєструвати в конфігурації: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Використовувати як будь-який інший мапер: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Метод `Helpers::createAssetFromUrl()` автоматично створює правильний тип активу на основі розширення файлу. - - -Читати далі -=========== - -- [Nette Assets: Нарешті уніфікований API для всього, від зображень до Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/uk/@left-menu.texy b/assets/uk/@left-menu.texy deleted file mode 100644 index 2461434065..0000000000 --- a/assets/uk/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Початок роботи |@home] -- [Vite |vite] -- [Конфігурація |Configuration] diff --git a/assets/uk/@meta.texy b/assets/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/assets/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/assets/uk/configuration.texy b/assets/uk/configuration.texy deleted file mode 100644 index 712ddbf2b7..0000000000 --- a/assets/uk/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Конфігурація активів -******************** - -.[perex] -Огляд опцій конфігурації для Nette Assets. - - -```neon -assets: - # base path for resolving relative mapper paths - basePath: ... # (string) defaults to %wwwDir% - - # base URL for resolving relative mapper URLs - baseUrl: ... # (string) defaults to %baseUrl% - - # enable asset versioning globally? - versioning: ... # (bool) defaults to true - - # defines asset mappers - mapping: ... # (array) defaults to path 'assets' -``` - -`basePath` встановлює типовий каталог файлової системи для розв'язання відносних шляхів у маперах. За замовчуванням він використовує веб-каталог (`%wwwDir%`). - -`baseUrl` встановлює типовий префікс URL для розв'язання відносних URL у маперах. За замовчуванням він використовує кореневий URL (`%baseUrl%`). - -Опція `versioning` глобально контролює, чи додаються параметри версії до URL-адрес активів для обходу кешу. Окремі мапери можуть перевизначати це налаштування. - - -Мапери ------- - -Мапери можуть бути налаштовані трьома способами: проста рядкова нотація, детальна масивна нотація або як посилання на сервіс. - -Найпростіший спосіб визначити мапер: - -```neon -assets: - mapping: - default: assets # Creates filesystem mapper for %wwwDir%/assets/ - images: img # Creates filesystem mapper for %wwwDir%/img/ - scripts: js # Creates filesystem mapper for %wwwDir%/js/ -``` - -Кожен мапер створює `FilesystemMapper`, який: -- Шукає файли в `%wwwDir%/` -- Генерує URL-адреси, такі як `%baseUrl%/` -- Успадковує глобальні налаштування версіонування - - -Для більшого контролю використовуйте детальну нотацію: - -```neon -assets: - mapping: - images: - # directory where files are stored - path: ... # (string) optional, defaults to '' - - # URL prefix for generated links - url: ... # (string) optional, defaults to path - - # enable versioning for this mapper? - versioning: ... # (bool) optional, inherits global setting - - # auto-add extension(s) when searching for files - extension: ... # (string|array) optional, defaults to null -``` - -Розуміння того, як розв'язуються значення конфігурації: - -Розв'язання шляхів: - - Відносні шляхи розв'язуються з `basePath` (або `%wwwDir%`, якщо `basePath` не встановлено) - - Абсолютні шляхи використовуються як є - -Розв'язання URL: - - Відносні URL-адреси розв'язуються з `baseUrl` (або `%baseUrl%`, якщо `baseUrl` не встановлено) - - Абсолютні URL-адреси (зі схемою або `//`) використовуються як є - - Якщо `url` не вказано, використовується значення `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relative path and URL - images: - path: img # Resolved to: /var/www/project/www/img - url: images # Resolved to: https://example.com/assets/images - - # Absolute path and URL - uploads: - path: /var/shared/uploads # Used as-is: /var/shared/uploads - url: https://cdn.example.com # Used as-is: https://cdn.example.com - - # Only path specified - styles: - path: css # Path: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Користувацькі мапери --------------------- - -Для користувацьких маперів посилайтеся або визначте сервіс: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Мапер Vite вимагає лише додати `type: vite`. Це повний список опцій конфігурації: - -```neon -assets: - mapping: - default: - # mapper type (required for Vite) - type: vite # (string) required, must be 'vite' - - # Vite build output directory - path: ... # (string) optional, defaults to '' - - # URL prefix for built assets - url: ... # (string) optional, defaults to path - - # location of Vite manifest file - manifest: ... # (string) optional, defaults to /.vite/manifest.json - - # Vite dev server configuration - devServer: ... # (bool|string) optional, defaults to true - - # versioning for public directory files - versioning: ... # (bool) optional, inherits global setting - - # auto-extension for public directory files - extension: ... # (string|array) optional, defaults to null -``` - -Опція `devServer` контролює, як активи завантажуються під час розробки: - -- `true` (за замовчуванням) - Автоматично виявляє dev-сервер Vite на поточному хості та порту. Якщо dev-сервер запущений **і ваш додаток знаходиться в режимі налагодження**, активи завантажуються з нього з підтримкою гарячої заміни модулів. Якщо dev-сервер не запущений, активи завантажуються з збудованих файлів у публічному каталозі. -- `false` - Повністю вимикає інтеграцію dev-сервера. Активи завжди завантажуються з збудованих файлів. -- Користувацький URL (наприклад, `https://localhost:5173`) - Вручну вказує URL dev-сервера, включаючи протокол та порт. Корисно, коли dev-сервер працює на іншому хості або порту. - -Опції `versioning` та `extension` застосовуються лише до файлів у публічному каталозі Vite, які не обробляються Vite. - - -Ручна конфігурація ------------------- - -Якщо не використовуєте Nette DI, налаштуйте мапери вручну: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Add filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Add Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/uk/vite.texy b/assets/uk/vite.texy deleted file mode 100644 index fe44373505..0000000000 --- a/assets/uk/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Інтеграція Vite -*************** - -
    - -Сучасні JavaScript-додатки вимагають складних інструментів збірки. Nette Assets надає першокласну інтеграцію з [Vite |https://vitejs.dev/], інструментом збірки фронтенду нового покоління. Отримайте блискавично швидку розробку з Hot Module Replacement (HMR) та оптимізовані виробничі збірки без проблем з конфігурацією. - -- **Нульова конфігурація** – автоматичний міст між Vite та PHP-шаблонами -- **Повне управління залежностями** – один тег обробляє всі активи -- **Гаряча заміна модулів** – миттєві оновлення JavaScript та CSS -- **Оптимізовані виробничі збірки** – розділення коду та tree shaking - -
    - - -Nette Assets бездоганно інтегрується з Vite, тому ви отримуєте всі ці переваги, пишучи свої шаблони як зазвичай. - - -Налаштування Vite -================= - -Давайте налаштуємо Vite крок за кроком. Не хвилюйтеся, якщо ви новачок в інструментах збірки – ми все пояснимо! - - -Крок 1: Встановіть Vite ------------------------ - -Спершу встановіть Vite та плагін Nette у вашому проекті: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Це встановлює Vite та спеціальний плагін, який допомагає Vite ідеально працювати з Nette. - - -Крок 2: Структура проекту -------------------------- - -Стандартний підхід полягає в розміщенні вихідних файлів активів у папці `assets/` у корені вашого проекту, а скомпільованих версій – у `www/assets/`: - -/--pre -web-project/ -├── assets/ ← source files (SCSS, TypeScript, source images) -│ ├── public/ ← static files (copied as-is) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← main entry point -│ └── style.css ← your styles -└── www/ ← public directory (document root) - ├── assets/ ← compiled files will go here - └── index.php -\-- - -Папка `assets/` містить ваші вихідні файли – код, який ви пишете. Vite обробить ці файли та помістить скомпільовані версії в `www/assets/`. - - -Крок 3: Налаштуйте Vite ------------------------ - -Створіть файл `vite.config.ts` у корені вашого проекту. Цей файл вказує Vite, де шукати ваші вихідні файли та куди поміщати скомпільовані. - -Плагін Nette Vite поставляється з розумними значеннями за замовчуванням, які спрощують конфігурацію. Він припускає, що ваші вихідні файли фронтенду знаходяться в каталозі `assets/` (опція `root`), а скомпільовані файли потрапляють до `www/assets/` (опція `outDir`). Вам потрібно лише вказати [точку входу |#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Якщо ви хочете вказати іншу назву каталогу для збірки ваших активів, вам потрібно буде змінити кілька опцій: - -```js -export default defineConfig({ - root: 'assets', // root directory of source assets - - build: { - outDir: '../www/assets', // where compiled files go - }, - - // ... other config ... -}); -``` - -.[note] -Шлях `outDir` вважається відносним до `root`, тому на початку є `../`. - - -Крок 4: Налаштуйте Nette ------------------------- - -Повідомте Nette Assets про Vite у вашому `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # tells Nette to use the ViteMapper - path: assets -``` - - -Крок 5: Додайте скрипти ------------------------ - -Додайте ці скрипти до вашого `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Тепер ви можете: -- `npm run dev` - запустити dev-сервер з гарячою перезавантаженням -- `npm run build` - створити оптимізовані файли для продакшену - - -Точки входу -=========== - -**Точка входу** – це головний файл, з якого починається ваш додаток. З цього файлу ви імпортуєте інші файли (CSS, модулі JavaScript, зображення), створюючи дерево залежностей. Vite слідує цим імпортам і об'єднує все разом. - -Приклад точки входу `assets/app.js`: - -```js -// Import styles -import './style.css' - -// Import JavaScript modules -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Initialize your application -netteForms.initOnLoad(); -naja.initialize(); -``` - -У шаблоні ви можете вставити точку входу наступним чином: - -```latte -{asset 'app.js'} -``` - -Nette Assets автоматично генерує всі необхідні HTML-теги – JavaScript, CSS та будь-які інші залежності. - - -Кілька точок входу ------------------- - -Більші додатки часто потребують окремих точок входу: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // public pages - 'admin.js', // admin panel - ], - }), - ], -}); -``` - -Використовуйте їх у різних шаблонах: - -```latte -{* In public pages *} -{asset 'app.js'} - -{* In admin panel *} -{asset 'admin.js'} -``` - - -Важливо: Вихідні проти скомпільованих файлів --------------------------------------------- - -Важливо розуміти, що на продакшені ви можете завантажувати лише: - -1. **Точки входу**, визначені в `entry` -2. **Файли з каталогу `assets/public/`** - -Ви **не можете** завантажувати за допомогою `{asset}` довільні файли з `assets/` – лише активи, на які посилаються файли JavaScript або CSS. Якщо на ваш файл ніде немає посилання, він не буде скомпільований. Якщо ви хочете, щоб Vite знав про інші активи, ви можете перемістити їх до [публічної папки |#public folder]. - -Зверніть увагу, що за замовчуванням Vite вбудовуватиме всі активи розміром менше 4 КБ, тому ви не зможете посилатися на ці файли безпосередньо. (Див. [документацію Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ This works - it's an entry point *} -{asset 'app.js'} - -{* ✓ This works - it's in assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ This won't work - random file in assets/ *} -{asset 'components/button.js'} -``` - - -Режим розробки -============== - -Режим розробки є повністю необов'язковим, але надає значні переваги при увімкненні. Головна перевага – це **Гаряча заміна модулів (HMR)** – миттєве відображення змін без втрати стану програми, що робить процес розробки набагато плавніше та швидше. - -Vite – це сучасний інструмент збірки, який робить розробку неймовірно швидкою. На відміну від традиційних бандлерів, Vite під час розробки подає ваш код безпосередньо в браузер, що означає миттєвий запуск сервера незалежно від розміру вашого проекту та блискавичні оновлення. - - -Запуск dev-сервера ------------------- - -Запустіть dev-сервер: - -```shell -npm run dev -``` - -Ви побачите: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Залишайте цей термінал відкритим під час розробки. - -Плагін Nette Vite автоматично виявляє, коли: -1. Vite dev-сервер запущений -2. Ваш Nette-додаток знаходиться в режимі налагодження - -Коли обидві умови виконані, Nette Assets завантажує файли з dev-сервера Vite замість скомпільованого каталогу: - -```latte -{asset 'app.js'} -{* In development: *} -{* In production: *} -``` - -Конфігурація не потрібна – просто працює! - - -Робота на різних доменах ------------------------- - -Якщо ваш dev-сервер працює не на `localhost` (наприклад, `myapp.local`), ви можете зіткнутися з проблемами CORS (Cross-Origin Resource Sharing). CORS – це функція безпеки у веб-браузерах, яка за замовчуванням блокує запити між різними доменами. Коли ваш PHP-додаток працює на `myapp.local`, а Vite – на `localhost:5173`, браузер розглядає їх як різні домени та блокує запити. - -У вас є два варіанти вирішення цієї проблеми: - -**Варіант 1: Налаштуйте CORS** - -Найпростіше рішення – дозволити крос-доменні запити з вашого PHP-додатку: - -```js -export default defineConfig({ - // ... other config ... - - server: { - cors: { - origin: 'http://myapp.local', // URL вашого PHP-додатку - }, - }, -}); -``` -**Варіант 2: Запустіть Vite на вашому домені** - -Інше рішення – змусити Vite працювати на тому ж домені, що й ваш PHP-додаток. - -```js -export default defineConfig({ - // ... other config ... - - server: { - host: 'myapp.local', // те саме, що й ваш PHP-додаток - }, -}); -``` - -Насправді, навіть у цьому випадку вам потрібно налаштувати CORS, оскільки dev-сервер працює на тому ж хості, але на іншому порту. Однак у цьому випадку CORS автоматично налаштовується плагіном Nette Vite. - - -Розробка HTTPS --------------- - -Якщо ви розробляєте на HTTPS, вам потрібні сертифікати для вашого dev-сервера Vite. Найпростіший спосіб – використовувати плагін, який автоматично генерує сертифікати: - -```shell -npm install -D vite-plugin-mkcert -``` - -Ось як налаштувати його в `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... other config ... - - plugins: [ - mkcert(), // generates certificates automatically and enables https - nette(), - ], -}); -``` - -Зверніть увагу, що якщо ви використовуєте конфігурацію CORS (Варіант 1 вище), вам потрібно оновити URL походження, щоб використовувати `https://` замість `http://`. - - -Продакшен збірки -================ - -Створіть оптимізовані файли для продакшену: - -```shell -npm run build -``` - -Vite зробить: -- Мініфікувати весь JavaScript та CSS -- Розділити код на оптимальні чанки -- Згенерувати хешовані імена файлів для обходу кешу -- Створити файл маніфесту для Nette Assets - -Приклад виводу: - -``` -www/assets/ -├── app-4f3a2b1c.js # Your main JavaScript (minified) -├── app-7d8e9f2a.css # Extracted CSS (minified) -├── vendor-8c4b5e6d.js # Shared dependencies -└── .vite/ - └── manifest.json # Mapping for Nette Assets -``` - -Хешовані імена файлів гарантують, що браузери завжди завантажують найновішу версію. - - -Публічна папка -============== - -Файли в каталозі `assets/public/` копіюються у вихідний каталог без обробки: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Посилайтеся на них звичайно: - -```latte -{* These files are copied as-is *} - - -``` - -Для публічних файлів можна використовувати функції FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Try WebP first - versioning: true # Add cache-busting -``` - -У конфігурації `vite.config.ts` ви можете змінити публічну папку за допомогою опції `publicDir`. - - -Динамічні імпорти -================= - -Vite автоматично розділяє код для оптимального завантаження. Динамічні імпорти дозволяють завантажувати код лише тоді, коли він дійсно потрібен, зменшуючи початковий розмір бандлу: - -```js -// Load heavy components on demand -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Динамічні імпорти створюють окремі чанки, які завантажуються лише за потреби. Це називається "розділення коду" (code splitting) і є однією з найпотужніших функцій Vite. Коли ви використовуєте динамічні імпорти, Vite автоматично створює окремі файли JavaScript для кожного динамічно імпортованого модуля. - -Тег `{asset 'app.js'}` **не** попередньо завантажує ці динамічні чанки автоматично. Це навмисна поведінка – ми не хочемо завантажувати код, який може ніколи не використовуватися. Чанки завантажуються лише тоді, коли виконується динамічний імпорт. - -Однак, якщо ви знаєте, що певні динамічні імпорти є критичними і знадобляться незабаром, ви можете попередньо завантажити їх: - -```latte -{* Main entry point *} -{asset 'app.js'} - -{* Preload critical dynamic imports *} -{preload 'components/chart.js'} -``` - -Це вказує браузеру завантажити компонент діаграми у фоновому режимі, щоб він був готовий негайно, коли це знадобиться. - - -Підтримка TypeScript -==================== - -TypeScript працює "з коробки": - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Посилайтеся на файли TypeScript звичайно: - -```latte -{asset 'main.ts'} -``` - -Для повної підтримки TypeScript встановіть його: - -```shell -npm install -D typescript -``` - - -Додаткова конфігурація Vite -=========================== - -Ось деякі корисні опції конфігурації Vite з детальними поясненнями: - -```js -export default defineConfig({ - // Root directory containing source assets - root: 'assets', - - // Folder whose contents are copied to output directory as-is - // Default: 'public' (relative to 'root') - publicDir: 'public', - - build: { - // Where to put compiled files (relative to 'root') - outDir: '../www/assets', - - // Empty output directory before building? - // Useful to remove old files from previous builds - emptyOutDir: true, - - // Subdirectory within outDir for generated chunks and assets - // This helps organize the output structure - assetsDir: 'static', - - rollupOptions: { - // Entry point(s) - can be a single file or array of files - // Each entry point becomes a separate bundle - input: [ - 'app.js', // main application - 'admin.js', // admin panel - ], - }, - }, - - server: { - // Host to bind the dev server to - // Use '0.0.0.0' to expose to network - host: 'localhost', - - // Port for the dev server - port: 5173, - - // CORS configuration for cross-origin requests - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Enable CSS source maps in development - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Ось і все! Тепер у вас є сучасна система збірки, інтегрована з Nette Assets. diff --git a/best-practices/bg/@home.texy b/best-practices/bg/@home.texy deleted file mode 100644 index 808c4d31b7..0000000000 --- a/best-practices/bg/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Ръководства и процедури -*********************** - -.[perex] -Ръководства, решения на често срещани задачи и *добри практики* за Nette. - - -
    -
    - - -Nette Приложения ----------------- -- [Методи и атрибути inject |inject-method-attribute] -- [Съставяне на презентери от trait |presenter-traits] -- [Предаване на настройки към презентери |passing-settings-to-presenters] -- [Как да се върнем към предишна страница |restore-request] -- [Странициране на резултати от база данни |pagination] -- [Динамични снипети |dynamic-snippets] -- [Как да използваме атрибута #Requires |attribute-requires] -- [Как правилно да използваме POST връзки |post-links] - -
    -
    - - -Форми ------ -- [Повторно използване на форми |form-reuse] -- [Форма за създаване и редактиране на запис |creating-editing-form] -- [Създаваме контактна форма |lets-create-contact-form] -- [Зависими селектбокси |https://blog.nette.org/bg/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -
    -
    - - -Общи ----- -- [Как да заредим конфигурационен файл |bootstrap:] -- [Как да пишем микро-уебсайтове |microsites] -- [Защо Nette използва PascalCase нотация за константи? |https://blog.nette.org/bg/for-less-screaming-in-the-code] -- [Защо Nette не използва суфикс Interface? |https://blog.nette.org/bg/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: съвети за използване |composer] -- [Съвети за редактори & инструменти |editors-and-tools] -- [Въведение в обектно-ориентираното програмиране |nette:introduction-to-object-oriented-programming] - -
    -
    - - -Примерни решения ----------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Бърз старт |quickstart:] - -
    -
    - - -Видеа ------ -Стотици записи от Posledních sobot и видеа за Nette можете да намерите под един покрив в "Youtube канала на Nette Framework":https://www.youtube.com/user/NetteFramework. - -
    -
    diff --git a/best-practices/bg/@meta.texy b/best-practices/bg/@meta.texy deleted file mode 100644 index dc4e6c5b2b..0000000000 --- a/best-practices/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Ръководства и процедури}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/bg/attribute-requires.texy b/best-practices/bg/attribute-requires.texy deleted file mode 100644 index c779502d44..0000000000 --- a/best-practices/bg/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Как да използваме атрибута `#[Requires]` -**************************************** - -.[perex] -Когато пишете уеб приложение, често се сблъсквате с необходимостта да ограничите достъпа до определени части от вашето приложение. Може би искате някои заявки да могат да изпращат данни само чрез формуляр (т.е. с метод POST), или да бъдат достъпни само за AJAX извиквания. В Nette Framework 3.2 се появи нов инструмент, който ви позволява да настроите такива ограничения много елегантно и прегледно: атрибутът `#[Requires]`. - -Атрибутът е специална маркировка в PHP, която добавяте преди дефиницията на клас или метод. Тъй като всъщност е клас, за да работят следващите примери, е необходимо да се посочи клаузата use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Атрибутът `#[Requires]` можете да използвате при самия клас на презентера, както и на тези методи: - -- `action()` -- `render()` -- `handle()` -- `createComponent()` - -Последните два метода се отнасят и до компоненти, т.е. атрибутът можете да използвате и при тях. - -Ако не са изпълнени условията, които атрибутът посочва, ще се предизвика HTTP грешка 4xx. - - -Методи HTTP ------------ - -Можете да специфицирате кои HTTP методи (като GET, POST и т.н.) са разрешени за достъп. Например, ако искате да разрешите достъп само чрез изпращане на формуляр, настройте: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Защо трябва да използвате POST вместо GET за действия, променящи състоянието, и как да го направите? [Прочетете ръководството |post-links]. - -Можете да посочите метод или масив от методи. Специален случай е стойността `'*'`, която разрешава всички методи, което стандартно презентерите [от съображения за сигурност не позволяват |application:presenters#Проверка на HTTP метода]. - - -AJAX извикване --------------- - -Ако искате презентерът или методът да бъдат достъпни само за AJAX заявки, използвайте: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Същият произход ---------------- - -За повишаване на сигурността можете да изисквате заявката да бъде направена от същия домейн. С това предотвратявате [уязвимостта CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -При методите `handle()` достъпът от същия домейн се изисква автоматично. Така че, ако обратно искате да разрешите достъп от всеки домейн, посочете: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Достъп чрез forward -------------------- - -Понякога е полезно да се ограничи достъпът до презентера така, че да бъде достъпен само непряко, например с използването на метода `forward()` или `switch()` от друг презентер. Така например се защитават error-presenter-ите, за да не могат да бъдат извикани от URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -На практика често е необходимо да се маркират определени views, до които може да се стигне едва въз основа на логиката в презентера. Тоест отново, за да не могат да бъдат отворени директно: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Конкретни действия ------------------- - -Можете също така да ограничите, че определен код, например създаване на компонент, ще бъде достъпен само за специфични действия в презентера: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -В случай на едно действие не е необходимо да се записва масив: `#[Requires(actions: 'default')]` - - -Собствени атрибути ------------------- - -Ако искате да използвате атрибута `#[Requires]` многократно със същите настройки, можете да си създадете собствен атрибут, който ще наследява `#[Requires]` и ще го настрои според нуждите. - -Например `#[SingleAction]` ще позволи достъп само чрез действието `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Или `#[RestMethods]` ще позволи достъп чрез всички HTTP методи, използвани за REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Заключение ----------- - -Атрибутът `#[Requires]` ви дава голяма гъвкавост и контрол върху това как са достъпни вашите уеб страници. С помощта на прости, но мощни правила можете да повишите сигурността и правилното функциониране на вашето приложение. Както виждате, използването на атрибути в Nette може не само да улесни вашата работа, но и да я обезопаси. diff --git a/best-practices/bg/composer.texy b/best-practices/bg/composer.texy deleted file mode 100644 index 2346c9485f..0000000000 --- a/best-practices/bg/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: съвети за употреба -**************************** - -
    - -Composer е инструмент за управление на зависимости в PHP. Позволява ни да изброим библиотеките, от които зависи нашият проект, и ще ги инсталира и актуализира вместо нас. Ще покажем: - -- как да инсталираме Composer -- неговото използване в нов или съществуващ проект - -
    - - -Инсталация -========== - -Composer е изпълним `.phar` файл, който изтегляте и инсталирате по следния начин: - - -Windows -------- - -Използвайте официалния инсталатор [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Достатъчни са 4 команди, които копирайте от [тази страница |https://getcomposer.org/download/]. - -Освен това, като го поставите в папка, която е в системния `PATH`, Composer става достъпен глобално: - -```shell -$ mv ./composer.phar ~/bin/composer # или /usr/local/bin/composer -``` - - -Използване в проект -=================== - -За да можем да започнем да използваме Composer в нашия проект, се нуждаем само от файл `composer.json`. Той описва зависимостите на нашия проект и може също да съдържа други метаданни. Основният `composer.json` следователно може да изглежда така: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Тук казваме, че нашето приложение (или библиотека) изисква пакета `nette/database` (името на пакета се състои от името на организацията и името на проекта) и иска версия, която отговаря на условието `^3.0` (т.е. най-новата версия 3). - -Имаме следователно в корена на проекта файл `composer.json` и стартираме инсталацията: - -```shell -composer update -``` - -Composer ще изтегли Nette Database в папката `vendor/`. Освен това ще създаде файл `composer.lock`, който съдържа информация за това кои версии на библиотеките точно е инсталирал. - -Composer генерира файл `vendor/autoload.php`, който можем лесно да включим и да започнем да използваме библиотеките без никаква друга работа: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Актуализация на пакетите до най-новите версии -============================================= - -Актуализацията на използваните библиотеки до най-новите версии според условията, дефинирани в `composer.json`, се извършва от командата `composer update`. Напр. при зависимост `"nette/database": "^3.0"` ще инсталира най-новата версия 3.x.x, но не и версия 4. - -За актуализация на условията във файла `composer.json`, например на `"nette/database": "^4.1"`, за да може да се инсталира най-новата версия, използвайте командата `composer require nette/database`. - -За актуализация на всички използвани пакети на Nette би било необходимо всички те да се изброят в командния ред, напр.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Което е непрактично. Затова използвайте простия скрипт "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, който ще го направи вместо вас: - -```shell -php composer-frontline.php -``` - - -Създаване на нов проект -======================= - -Нов проект на Nette създавате с една-единствена команда: - -```shell -composer create-project nette/web-project име-на-проекта -``` - -Като `име-на-проекта` въведете името на директорията за вашия проект и потвърдете. Composer ще изтегли хранилището `nette/web-project` от GitHub, което вече съдържа файл `composer.json`, и веднага след това Nette Framework. Трябва вече да е достатъчно само да [настроите правата |nette:troubleshooting#Настройка на правата на директориите] за запис в папките `temp/` и `log/` и проектът трябва да оживее. - -Ако знаете на коя версия на PHP ще бъде хостван проектът, не забравяйте [да я настроите |#Версия на PHP]. - - -Версия на PHP -============= - -Composer винаги инсталира тези версии на пакетите, които са съвместими с версията на PHP, която в момента използвате (по-точно с версията на PHP, използвана в командния ред при стартиране на Composer). Което обаче най-вероятно не е същата версия, която използва вашият хостинг. Затова е много важно да добавите в файла `composer.json` информация за версията на PHP на хостинга. След това ще се инсталират само версии на пакетите, съвместими с хостинга. - -Това, че проектът ще работи например на PHP 8.2.3, настройваме с командата: - -```shell -composer config platform.php 8.2.3 -``` - -Така версията се записва във файла `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Въпреки това, номерът на версията на PHP се посочва и на друго място във файла, а именно в секцията `require`. Докато първото число определя за коя версия ще се инсталират пакетите, второто число казва за коя версия е написано самото приложение. И според него например PhpStorm настройва *PHP language level*. (Разбира се, няма смисъл тези версии да се различават, така че двойният запис е недомислица.) Тази версия настройвате с командата: - -```shell -composer require php 8.2.3 --no-update -``` - -Или директно във файла `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Игнориране на версията на PHP -============================= - -Пакетите обикновено имат посочена както най-ниската версия на PHP, с която са съвместими, така и най-високата, с която са тествани. Ако се готвите да използвате версия на PHP още по-нова, например с цел тестване, Composer ще откаже да инсталира такъв пакет. Решението е опцията `--ignore-platform-req=php+`, която кара Composer да игнорира горните граници на изискваната версия на PHP. - - -Фалшиви съобщения -================= - -При надграждане на пакети или промени в номерата на версиите се случва да възникне конфликт. Един пакет има изисквания, които са в противоречие с друг и подобни. Composer обаче понякога изписва фалшиви съобщения. Съобщава за конфликт, който реално не съществува. В такъв случай помага да се изтрие файлът `composer.lock` и да се опита отново. - -Ако съобщението за грешка продължава, тогава е сериозно и трябва да се разчете от него какво и как да се промени. - - -Packagist.org - централно хранилище -=================================== - -[Packagist |https://packagist.org] е основното хранилище, в което Composer се опитва да търси пакети, ако не му кажем друго. Тук можем да публикуваме и собствени пакети. - - -Какво, ако не искаме да използваме централното хранилище? ---------------------------------------------------------- - -Ако имаме вътрешнофирмени приложения, които просто не можем да хостваме публично, тогава ще си създадем фирмено хранилище за тях. - -Повече по темата за хранилищата [в официалната документация |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Ключова характеристика на Composer е, че предоставя autoloading за всички инсталирани от него класове, който стартирате, като включите файла `vendor/autoload.php`. - -Въпреки това е възможно да използвате Composer и за зареждане на други класове и извън папката `vendor`. Първата възможност е да оставите Composer да претърси дефинираните папки и подпапки, да намери всички класове и да ги включи в autoloader-а. Това постигате, като настроите `autoload > classmap` в `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # включва папката src/ и нейните подпапки - ] - } -} -``` - -След това е необходимо при всяка промяна да се стартира командата `composer dumpautoload` и да се оставят autoloading таблиците да се прегенерират. Това е изключително неудобно и далеч по-добре е тази задача да се повери на [RobotLoader|robot-loader:], който извършва същата дейност автоматично във фонов режим и много по-бързо. - -Втората възможност е да се спазва [PSR-4|https://www.php-fig.org/psr/psr-4/]. Опростено казано, става въпрос за система, при която именните пространства и имената на класовете съответстват на директорийната структура и имената на файловете, т.е. напр. `App\Core\RouterFactory` ще бъде във файла `/path/to/App/Core/RouterFactory.php`. Пример за конфигурация: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # именното пространство App\ е в директорията app/ - } - } -} -``` - -Как точно да конфигурирате поведението ще научите в [документацията на Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Тестване на нови версии -======================= - -Искате да тествате нова разработваща се версия на пакет. Как да го направите? Първо в файла `composer.json` добавете тази двойка опции, която позволява инсталиране на разработващи се версии на пакети, но прибягва до това само в случай, че не съществува никаква комбинация от стабилни версии, която да удовлетворява изискванията: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Освен това препоръчваме да изтриете файла `composer.lock`, понякога Composer необяснимо отказва инсталацията и това решава проблема. - -Да кажем, че става въпрос за пакет `nette/utils` и новата версия има номер 4.0. Инсталирате я с командата: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Или можете да инсталирате конкретна версия, например 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Но ако от библиотеката зависи друг пакет, който е заключен на по-стара версия (напр. `^3.1`), тогава е идеално пакетът да се актуализира, за да работи с новата версия. Ако обаче искате само да заобиколите ограничението и да принудите Composer да инсталира разработващата се версия и да се преструва, че става въпрос за по-стара версия (напр. 3.1.6), можете да използвате ключовата дума `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Извикване на команди -==================== - -Чрез Composer могат да се извикват собствени предварително подготвени команди и скриптове, сякаш става въпрос за нативни команди на Composer. При скриптове, които се намират в папката `vendor/bin`, не е необходимо тази папка да се посочва. - -Като пример ще дефинираме във файла `composer.json` скрипт, който с помощта на [Nette Tester|tester:] стартира тестове: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Тестовете след това стартираме с помощта на `composer tester`. Командата можем да извикаме и в случай, че не сме в коренната папка на проекта, а в някоя поддиректория. - - -Изпратете благодарност -====================== - -Ще ви покажем трик, с който ще зарадвате авторите на open source. По прост начин ще дадете звездичка в GitHub на библиотеките, които вашият проект използва. Достатъчно е да инсталирате библиотеката `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -И след това да стартирате: - -```shell -composer thanks -``` - -Опитайте! - - -Конфигурация -============ - -Composer е тясно свързан с инструмента за версиониране [Git |https://git-scm.com]. Ако не го имате инсталиран, трябва да кажете на Composer да не го използва: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/bg/creating-editing-form.texy b/best-practices/bg/creating-editing-form.texy deleted file mode 100644 index 5f4f84a06c..0000000000 --- a/best-practices/bg/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Форма за създаване и редактиране на запис -***************************************** - -.[perex] -Как правилно да се реализира добавяне и редактиране на запис в Nette, като се използва една и съща форма и за двете? - -В много случаи формите за добавяне и редактиране на записи са еднакви, като се различават само по етикета на бутона. Ще покажем примери за прости презентери, където ще използваме формата първо за добавяне на запис, след това за редактиране и накрая ще комбинираме двете решения. - - -Добавяне на запис ------------------ - -Пример за презентер, използван за добавяне на запис. Ще оставим действителната работа с базата данни на класа `Facade`, чийто код не е от съществено значение за примера. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... добавяме полета към формата ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // добавяне на запис в базата данни - $this->flashMessage('Успешно добавено'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Редактиране на запис --------------------- - -Сега ще покажем как би изглеждал презентер, използван за редактиране на запис: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // проверка за съществуване на запис - || !$this->facade->isEditAllowed(/*...*/) // проверка на правата - ) { - $this->error(); // грешка 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // проверяваме дали действието е 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... добавяме полета към формата ... - - $form->setDefaults($this->record); // задаване на стойности по подразбиране - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // актуализиране на запис - $this->flashMessage('Успешно актуализирано'); - $this->redirect('...'); - } -} -``` - -В метода *action*, който се стартира в самото начало на [жизнения цикъл на презентера |application:presenters#Жизнен цикъл на презентера], проверяваме съществуването на записа и правата на потребителя да го редактира. - -Запазваме записа в свойството `$record`, за да го имаме на разположение в метода `createComponentRecordForm()` за задаване на стойности по подразбиране и в `recordFormSucceeded()` заради ID. Алтернативно решение би било да зададем стойностите по подразбиране директно в `actionEdit()` и да получим стойността на ID, която е част от URL адреса, като използваме `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // проверка за съществуване и проверка на правата - ) { - $this->error(); - } - - // задаване на стойности по подразбиране на формата - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Въпреки това, и това трябва да бъде **най-важният извод от целия код**, трябва да се уверим при създаването на формата, че действието наистина е `edit`. В противен случай проверката в метода `actionEdit()` изобщо няма да се извърши! - - -Същата форма за добавяне и редактиране --------------------------------------- - -И сега комбинираме двата презентера в един. Можем или да разграничим в метода `createComponentRecordForm()` кое е действието и да конфигурираме формата съответно, или можем да го оставим директно на action-методите и да се отървем от условието: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // проверка за съществуване на запис - || !$this->facade->isEditAllowed(/*...*/) // проверка на правата - ) { - $this->error(); // грешка 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // задаване на стойности по подразбиране - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // проверяваме дали действието е 'add' или 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... добавяме полета към формата ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // добавяне на запис в базата данни - $this->flashMessage('Успешно добавено'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // актуализиране на запис - $this->flashMessage('Успешно актуализирано'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/bg/dynamic-snippets.texy b/best-practices/bg/dynamic-snippets.texy deleted file mode 100644 index c8baa990be..0000000000 --- a/best-practices/bg/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Динамични снипети -***************** - -Доста често при разработването на приложения възниква необходимостта от извършване на AJAX операции, например върху отделни редове на таблица или елементи от списък. Като пример можем да вземем списък със статии, като за всяка от тях ще позволим на влезлия потребител да избере оценка "харесвам/не харесвам". Кодът на презентера и съответният шаблон без AJAX ще изглеждат приблизително по следния начин (представям най-важните части, кодът предполага съществуването на сървис за маркиране на оценките и получаване на колекция от статии - конкретната реализация не е важна за целите на това ръководство): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Шаблон: - -```latte - -``` - - -Ajaxизация -========== - -Нека сега оборудваме това просто приложение с AJAX. Промяната на оценката на статията не е толкова важна, че да изисква пренасочване, затова в идеалния случай тя трябва да се извършва чрез AJAX във фонов режим. Ще използваме [обслужващия скрипт от добавките |application:ajax#Naja] с обичайната конвенция, че AJAX връзките имат CSS клас `ajax`. - -Но как да го направим конкретно? Nette предлага 2 начина: пътя на т.нар. динамични снипети и пътя на компонентите. И двата имат своите плюсове и минуси, затова ще ги покажем един по един. - - -Пътят на динамичните снипети -============================ - -Динамичен снипет в терминологията на Latte означава специфичен случай на използване на макроса `{snippet}`, при който в името на снипета се използва променлива. Такъв снипет не може да се намира навсякъде в шаблона - той трябва да бъде обвит в статичен снипет, т.е. обикновен, или вътре в `{snippetArea}`. Можем да модифицираме нашия шаблон по следния начин. - - -```latte -{snippet articlesContainer} - -{/snippet} -``` - -Всяка статия сега дефинира един снипет, който има ID на статията в името си. Всички тези снипети след това са обвити заедно в един снипет с име `articlesContainer`. Ако пропуснем този обвиващ снипет, Latte ще ни предупреди с изключение. - -Остава ни да добавим прерисуването в презентера - достатъчно е да прерисуваме статичната обвивка. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- не е необходимо - } else { - $this->redirect('this'); - } -} -``` - -По същия начин модифицираме и сестринския метод `handleUnlike()`, и AJAX работи! - -Решението обаче има и една тъмна страна. Ако разгледаме по-подробно как протича AJAX заявката, ще открием, че въпреки че приложението изглежда икономично отвън (връща само един снипет за дадената статия), всъщност на сървъра то е рендирало всички снипети. Желаният снипет е поставен в payload-а, а останалите са изхвърлени (следователно са били извлечени от базата данни напълно ненужно). - -За да оптимизираме този процес, ще трябва да се намесим там, където предаваме колекцията `$articles` към шаблона (да речем в метода `renderDefault()`). Ще използваме факта, че обработката на сигналите се извършва преди методите `render`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Сега, при обработката на сигнала, вместо колекция с всички статии, към шаблона се предава само масив с една статия - тази, която искаме да рендираме и изпратим в payload-а към браузъра. Следователно `{foreach}` ще се изпълни само веднъж и няма да се рендират допълнителни снипети. - - -Пътят на компонентите -===================== - -Напълно различен начин на решаване избягва динамичните снипети. Трикът се състои в прехвърлянето на цялата логика в отделен компонент - отсега нататък въвеждането на оценки няма да се обработва от презентера, а от специализирания `LikeControl`. Класът ще изглежда по следния начин (освен това ще съдържа и методите `render`, `handleUnlike` и т.н.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Шаблон на компонента: - -```latte -{snippet} - {if !$article->liked} - харесвам - {else} - вече не ми харесва - {/if} -{/snippet} -``` - -Разбира се, шаблонът на изгледа ще се промени и ще трябва да добавим фабрика към презентера. Тъй като ще създадем компонента толкова пъти, колкото статии получим от базата данни, ще използваме класа [application:Multiplier] за неговото "размножаване". - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Шаблонът на изгледа се свежда до необходимия минимум (и е напълно лишен от снипети!): - -```latte -
    -

    {$article->title}

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

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

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

    Име: {$name}

    -

    E-mail: {$email}

    -

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

    - - -``` - -Остава да променим `ContactFacade`, за да използва този шаблон. В конструктора ще изискаме класа `LatteFactory`, който може да произведе обект `Latte\Engine`, т.е. [рендериращ механизъм за Latte шаблони |latte:develop#Как да рендираме шаблон]. С помощта на метода `renderToString()` ще рендираме шаблона във файл, първият параметър е пътят до шаблона, а вторият са променливите. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // вашият имейл - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Генерирания HTML имейл след това ще предадем на метода `setHtmlBody()` вместо оригиналния `setBody()`. Също така не е необходимо да посочваме темата на имейла в `setSubject()`, тъй като библиотеката ще я вземе от елемента `` на шаблона. - - -Конфигурация ------------- - -В кода на класа `ContactFacade` все още е твърдо кодиран нашият администраторски имейл `admin@example.com`. Би било по-добре да го преместим в конфигурационния файл. Как да го направим? - -Първо ще променим класа `ContactFacade` и ще заменим низа с имейла с променлива, предадена чрез конструктора: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -А втората стъпка е да посочим стойността на тази променлива в конфигурацията. Във файла `app/config/services.neon` ще запишем: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -И това е. Ако елементите в секцията `services` са много и имате чувството, че имейлът се губи сред тях, можем да го превърнем в променлива. Ще променим записа на: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -И във файла `app/config/common.neon` ще дефинираме тази променлива: - -```neon -parameters: - adminEmail: admin@example.com -``` - -И е готово! diff --git a/best-practices/bg/microsites.texy b/best-practices/bg/microsites.texy deleted file mode 100644 index e29edcb2ed..0000000000 --- a/best-practices/bg/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Как да пишем микро-уебсайтове -***************************** - -Представете си, че трябва бързо да създадете малък уебсайт за предстоящо събитие на вашата фирма. Трябва да е просто, бързо и без излишни усложнения. Може би си мислите, че за такъв малък проект не ви е необходим стабилен framework. Но какво ще стане, ако използването на Nette framework може значително да опрости и ускори този процес? - -Все пак, дори при създаването на прости уебсайтове, не искате да се отказвате от удобството. Не искате да измисляте това, което вече е решено. Бъдете спокойно мързеливи и се оставете да ви глезят. Nette Framework може отлично да се използва и като micro framework. - -Как може да изглежда такъв микросайт? Например така, че целият код на уебсайта да се постави в един файл `index.php` в публичната папка: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// създаване на DI контейнер въз основа на конфигурацията в config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// настройване на маршрутизацията -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// маршрут за URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // откриване на езика на браузъра и пренасочване към URL /en или /de и т.н. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// маршрут за URL https://example.com/cs или https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // показване на съответния шаблон, например ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// стартиране на приложението! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Всичко останало ще бъдат шаблони, съхранени в родителската папка `/templates`. - -PHP кодът в `index.php` първо [подготвя средата |bootstrap:], след това дефинира [маршрутите |application:routing#Динамично маршрутизиране с callback-ове] и накрая стартира приложението. Предимството е, че вторият параметър на функцията `addRoute()` може да бъде callable, който се изпълнява след отваряне на съответната страница. - - -Защо да използвате Nette за микросайт? --------------------------------------- - -- Програмистите, които някога са опитвали [Tracy|tracy:], днес не могат да си представят да програмират нещо без нея. -- Преди всичко обаче ще използвате системата за шаблони [Latte|latte:], защото още от 2 страници ще искате да имате отделен [лейаут и съдържание|latte:template-inheritance]. -- И определено искате да разчитате на [автоматично екраниране |latte:safety-first], за да не възникне уязвимост XSS -- Nette също така гарантира, че при грешка никога няма да се покажат програмни съобщения за грешки на PHP, а разбираема за потребителя страница. -- Ако искате да получавате обратна връзка от потребителите, например под формата на контактна форма, тогава ще добавите и [форми|forms:] и [база данни|database:]. -- Попълнените формуляри можете лесно да [изпращате по имейл|mail:]. -- Понякога може да ви е полезно [кеширането|caching:], например ако изтегляте и показвате фийдове. - -В днешно време, когато скоростта и ефективността са ключови, е важно да имате инструменти, които ви позволяват да постигнете резултати без излишно забавяне. Nette framework ви предлага точно това - бърза разработка, сигурност и широк набор от инструменти, като Tracy и Latte, които опростяват процеса. Достатъчно е да инсталирате няколко Nette пакета и изграждането на такъв микросайт изведнъж става напълно лесно. И знаете, че никъде не се крие никаква дупка в сигурността. diff --git a/best-practices/bg/pagination.texy b/best-practices/bg/pagination.texy deleted file mode 100644 index 15a33b739e..0000000000 --- a/best-practices/bg/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Пагиниране на резултати от база данни -************************************* - -.[perex] -При създаването на уеб приложения много често ще се сблъскате с изискването за ограничаване на броя на изведените елементи на страница. - -Ще изходим от състояние, в което извеждаме всички данни без пагиниране. За избор на данни от базата данни имаме клас ArticleRepository, който освен конструктор съдържа метод `findPublishedArticles`, който връща всички публикувани статии, сортирани низходящо по дата на публикуване. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -В презентера след това инжектираме моделния клас и в render метода изискваме публикуваните статии, които предаваме на шаблона: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -В шаблона `default.latte` след това се грижим за извеждането на статиите: - -```latte -{block content} -<h1>Статии</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -По този начин можем да изведем всички статии, което обаче започва да създава проблеми в момента, когато броят на статиите нарасне. В този момент е подходящо да се внедри механизъм за пагиниране. - -Той гарантира, че всички статии ще бъдат разделени на няколко страници и ние ще покажем само статиите от една текуща страница. [utils:Paginator] сам ще изчисли общия брой страници и разпределението на статиите според това колко статии общо имаме и колко статии на страница искаме да покажем. - -В първата стъпка ще променим метода за получаване на статии в класа на repository така, че да може да връща само статии за една страница. Също така ще добавим метод за установяване на общия брой статии в базата данни, който ще ни е необходим за настройка на Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Връща общия брой публикувани статии - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -След това ще се заемем с промените в презентера. В render метода ще предаваме номера на текущо показваната страница. За случая, когато този номер не е част от URL, ще зададем стойност по подразбиране за първата страница. - -Освен това ще разширим render метода с получаване на инстанция на Paginator, неговата настройка и избор на правилните статии за показване в шаблона. HomePresenter след промените ще изглежда така: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Ще установим общия брой публикувани статии - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Ще създадем инстанция на Paginator и ще го настроим - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // общ брой статии - $paginator->setItemsPerPage(10); // брой елементи на страница - $paginator->setPage($page); // номер на текущата страница - - // От базата данни ще изтеглим ограничено множество статии според изчислението на Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // което ще предадем на шаблона - $this->template->articles = $articles; - // и също така самия Paginator за показване на възможностите за пагиниране - $this->template->paginator = $paginator; - } -} -``` - -Шаблонът ни вече итерира само върху статиите от една страница, достатъчно е да добавим връзките за пагиниране: - -```latte -{block content} -<h1>Статии</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Първа</a> -  |  - <a n:href="default, $paginator->page-1">Предишна</a> -  |  - {/if} - - Страница {$paginator->getPage()} от {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Следваща</a> -  |  - <a n:href="default, $paginator->getPageCount()">Последна</a> - {/if} -</div> -``` - - -Така допълнихме страницата с възможност за пагиниране с помощта на Paginator. В случай, че вместо [Nette Database Core |database:sql-way] като слой за база данни използваме [Nette Database Explorer |database:explorer], можем да внедрим пагиниране и без използване на Paginator. Класът `Nette\Database\Table\Selection` съдържа метод [page |api:Nette\Database\Table\Selection::_page] с логика за пагиниране, взета от Paginator. - -Repository при този начин на внедряване ще изглежда така: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -В презентера не е необходимо да създаваме Paginator, вместо него ще използваме метода на класа `Selection`, който ни връща repository: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Ще изтеглим публикуваните статии - $articles = $this->articleRepository->findPublishedArticles(); - - // и в шаблона ще изпратим само тяхната част, ограничена според изчислението на метода page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // и също така необходимите данни за показване на възможностите за пагиниране - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Тъй като в шаблона сега не изпращаме Paginator, ще променим частта, показваща връзките за пагиниране: - -```latte -{block content} -<h1>Статии</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Първа</a> -  |  - <a n:href="default, $page - 1">Предишна</a> -  |  - {/if} - - Страница {$page} от {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Следваща</a> -  |  - <a n:href="default, $lastPage">Последна</a> - {/if} -</div> -``` - -По този начин внедрихме механизъм за пагиниране без използване на Paginator. - -{{priority: -1}} diff --git a/best-practices/bg/passing-settings-to-presenters.texy b/best-practices/bg/passing-settings-to-presenters.texy deleted file mode 100644 index 076be18938..0000000000 --- a/best-practices/bg/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Предаване на настройки към презентерите -*************************************** - -.[perex] -Трябва ли да предавате аргументи към презентерите, които не са обекти (напр. информация дали работят в debug режим, пътища до директории и т.н.), и следователно не могат да бъдат предадени автоматично чрез autowiring? Решението е да ги капсулирате в обект `Settings`. - -Сървисът `Settings` представлява много лесен и същевременно полезен начин за предоставяне на информация за работещото приложение на презентерите. Конкретният му вид зависи изцяло от вашите конкретни нужди. Пример: - -```php -namespace App; - -class Settings -{ - public function __construct( - // от PHP 8.1 е възможно да се посочи readonly - public bool $debugMode, - public string $appDir, - // и така нататък - ) {} -} -``` - -Пример за регистрация в конфигурацията: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Когато презентерът се нуждае от информация, предоставена от този сървис, той просто я изисква в конструктора: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/bg/post-links.texy b/best-practices/bg/post-links.texy deleted file mode 100644 index 452bb3b18a..0000000000 --- a/best-practices/bg/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Как правилно да използваме POST връзки -************************************** - -.[perex] -В уеб приложенията, особено в административните интерфейси, основно правило трябва да бъде, че действията, променящи състоянието на сървъра, не трябва да се извършват чрез HTTP метода GET. Както подсказва името на метода, GET трябва да служи само за получаване на данни, а не за тяхната промяна. За действия като изтриване на записи е по-подходящо да се използва методът POST. Въпреки че идеалният би бил методът DELETE, но той не може да бъде извикан без JavaScript, затова исторически се използва POST. - -Как да го направим на практика? Използвайте този прост трик. В началото на шаблона си създайте помощна форма с идентификатор `postForm`, която след това ще използвате за бутоните за изтриване: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Благодарение на тази форма можете вместо класическа връзка `<a>` да използвате бутон `<button>`, който може да бъде визуално оформен така, че да изглежда като обикновена връзка. Например CSS framework Bootstrap предлага класове `btn btn-link`, с които ще постигнете това, че бутонът няма да се различава визуално от останалите връзки. С помощта на атрибута `form="postForm"` го свързваме с предварително подготвената форма: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button> - <!-- вместо <a n:href="delete $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -При кликване върху връзката сега се извиква действието `delete`. За да се гарантира, че заявките ще бъдат приемани само чрез метода POST и от същия домейн (което е ефективна защита срещу CSRF атаки), използвайте атрибута `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // хипотетичен код, изтриващ запис - $this->redirect('default'); - } -} -``` - -Атрибутът съществува от Nette Application 3.2 и повече за неговите възможности ще научите на страницата [Как да използваме атрибута #Requires |attribute-requires]. - -Ако вместо действието `actionDelete()` използвате сигнал `handleDelete()`, не е необходимо да посочвате `sameOrigin: true`, тъй като сигналите имат тази защита зададена имплицитно: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Този подход не само подобрява сигурността на вашето приложение, но също така допринася за спазването на правилните уеб стандарти и практики. Чрез използването на методи POST за действия, променящи състоянието, ще постигнете по-стабилно и по-сигурно приложение. diff --git a/best-practices/bg/presenter-traits.texy b/best-practices/bg/presenter-traits.texy deleted file mode 100644 index c2cf88f074..0000000000 --- a/best-practices/bg/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Композиране на презентери от trait -********************************** - -.[perex] -Ако трябва да внедрим един и същ код в няколко презентера (напр. проверка дали потребителят е влязъл), предлага се да поставим кода в общ родител. Втората възможност е създаването на едноцелеви [trait |nette:introduction-to-object-oriented-programming#Traits]. - -Предимството на това решение е, че всеки от презентерите може да използва точно тези trait, които наистина са му необходими, докато множественото наследяване не е възможно в PHP. - -Тези trait могат да използват факта, че при създаването на презентера последователно се извикват всички [inject методи |inject-method-attribute#Методи inject]. Необходимо е само да се гарантира, че името на всеки inject метод е уникално. - -Trait могат да прикачат инициализационен код към събитията [onStartup или onRender |application:presenters#Събития]. - -Примери: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Презентерът след това просто използва тези trait: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/bg/restore-request.texy b/best-practices/bg/restore-request.texy deleted file mode 100644 index 191350748d..0000000000 --- a/best-practices/bg/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Как да се върнем към предишна страница? -*************************************** - -.[perex] -Какво ще стане, ако потребителят попълва формуляр и сесията му изтече? За да не загуби данните, преди пренасочването към страницата за вход ще запазим данните в сесията. В Nette това е напълно лесно. - -Текущата заявка може да бъде запазена в сесията с помощта на метода `storeRequest()`, който връща нейния идентификатор под формата на кратък низ. Методът запазва името на текущия презентер, изгледа и неговите параметри. В случай, че е изпратен и формуляр, се запазва и съдържанието на полетата (с изключение на качените файлове). - -Възстановяването на заявката се извършва от метода `restoreRequest($key)`, на който предаваме получения идентификатор. Той пренасочва към оригиналния презентер и изглед. Ако обаче запазената заявка съдържа изпращане на формуляр, към оригиналния презентер се преминава с метода `forward()`, на формуляра се предават предишно попълнените стойности и той се рендира отново. По този начин потребителят има възможност да изпрати формуляра отново и никакви данни не се губят. - -Важно е, че `restoreRequest()` проверява дали нововъведеният потребител е същият, който първоначално е попълнил формуляра. Ако не е, заявката се отхвърля и нищо не се прави. - -Ще покажем всичко на пример. Нека имаме презентер `AdminPresenter`, в който се редактират данни и в чийто метод `startup()` проверяваме дали потребителят е влязъл. Ако не е, го пренасочваме към `SignPresenter`. Същевременно запазваме текущата заявка и нейния ключ изпращаме до `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Презентерът `SignPresenter` освен формуляра за вход ще съдържа и персистентен параметър `$backlink`, в който се записва ключът. Тъй като параметърът е персистентен, той ще се пренася и след изпращане на формуляра за вход. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... добавяме полета на формуляра ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... тук вписваме потребителя ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -На метода `restoreRequest()` предаваме ключа на запазената заявка и той пренасочва (или преминава) към оригиналния презентер. - -Ако обаче ключът е невалиден (например вече не съществува в сесията), методът не прави нищо. Следователно следва извикването на `$this->redirect('Admin:')`, което пренасочва към `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/cs/@home.texy b/best-practices/cs/@home.texy index 612d75048f..e95aa101a3 100644 --- a/best-practices/cs/@home.texy +++ b/best-practices/cs/@home.texy @@ -19,6 +19,7 @@ Nette Aplikace - [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] </div> <div> @@ -42,7 +43,6 @@ Obecné - [Proč Nette používá PascalCase notaci konstant? |https://blog.nette.org/cs/za-mene-kriku-v-kodu] - [Proč Nette nepoužívá příponu Interface? |https://blog.nette.org/cs/predpony-a-pripony-do-nazvu-rozhrani-nepatri] - [Composer: tipy pro použití |composer] -- [Tipy na editory & nástroje |editors-and-tools] - [Úvod do objektově orientovaného programování |nette:introduction-to-object-oriented-programming] </div> diff --git a/best-practices/cs/@left-menu.texy b/best-practices/cs/@left-menu.texy new file mode 100644 index 0000000000..b991715da9 --- /dev/null +++ b/best-practices/cs/@left-menu.texy @@ -0,0 +1,34 @@ +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 index 0c9a1e9689..68d2f76d0d 100644 --- a/best-practices/cs/@meta.texy +++ b/best-practices/cs/@meta.texy @@ -1,2 +1 @@ {{sitename: Návody a postupy}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/cs/attribute-requires.texy b/best-practices/cs/attribute-requires.texy index 682d1640a5..8effbed720 100644 --- a/best-practices/cs/attribute-requires.texy +++ b/best-practices/cs/attribute-requires.texy @@ -25,7 +25,7 @@ Pokud nejsou splněny podmínky, které atribut uvádí, dojde k vyvolání HTTP Metody HTTP ----------- -Můžete specifikovat, které HTTP metody (jako GET, POST atd.) jsou pro přístup povolené. Například, pokud chcete povolit přístup pouze odesíláním formuláře, nastavíte: +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 @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -V praxi bývá často potřeba označit určité views, ke kterým se lze dostat až na základě logiky v presenteru. Tedy opět, aby je nebylo možné otevřít přímo: +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 @@ -114,7 +114,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Konkrétní akce -------------- -Můžete také omezit, že určitý kód, třeba vytvoření komponenty, bude dostupné pouze pro specifické akce v presenteru: +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 diff --git a/best-practices/cs/composer.texy b/best-practices/cs/composer.texy index aebe726bbc..15a0534723 100644 --- a/best-practices/cs/composer.texy +++ b/best-practices/cs/composer.texy @@ -28,7 +28,7 @@ Linux, macOS Stačí 4 příkazy, které si zkopírujte z [této stránky |https://getcomposer.org/download/]. -Dále vložením do složky, která je v systémovém `PATH`, se stane Composer přístupný globálně: +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 @@ -38,7 +38,7 @@ $ mv ./composer.phar ~/bin/composer # nebo /usr/local/bin/composer Použití v projektu ================== -Abychom mohli ve svém projektu začít používat Composer, potřebujete pouze soubor `composer.json`. Ten popisuje závislosti našeho projektu a může také obsahovat další metadata. Základní `composer.json` tedy může vypadat takto: +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 { @@ -70,7 +70,7 @@ $db = new Nette\Database\Connection('sqlite::memory:'); Aktualizace balíčků na nejnovější verze ======================================= -Aktualizaci použiváných knihoven na nejnovější verze podle podmínek definovaných v `composer.json` má na starosti příkaz `composer update`. Např. u závislosti `"nette/database": "^3.0"` nainstaluje nejnovější verzi 3.x.x, ale nikoliv už verzi 4. +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`. diff --git a/best-practices/cs/creating-editing-form.texy b/best-practices/cs/creating-editing-form.texy index 8babb5c58c..c47c9f7535 100644 --- a/best-practices/cs/creating-editing-form.texy +++ b/best-practices/cs/creating-editing-form.texy @@ -29,14 +29,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... přidáme políčka formuláře ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // přidání záznamu do databáze - $this->flashMessage('Successfully added'); + $this->flashMessage('Záznam byl přidán'); $this->redirect('...'); } @@ -70,7 +70,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // oveření existence záznamu + !$record // ověření existence záznamu || !$this->facade->isEditAllowed(/*...*/) // kontrola oprávnění ) { $this->error(); // chyba 404 @@ -91,14 +91,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... přidáme políčka formuláře ... $form->setDefaults($this->record); // nastavení výchozích hodnot - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->update($this->record->id, $data); // aktualizace záznamu - $this->flashMessage('Successfully updated'); + $this->flashMessage('Záznam byl upraven'); $this->redirect('...'); } } @@ -114,7 +114,7 @@ Záznam si uložíme do property `$record`, abychom jej měli k dispozici v meto { $record = $this->facade->get($id); if ( - // oveření existence a kontrola oprávnění + // ověření existence a kontrola oprávnění ) { $this->error(); } @@ -130,16 +130,15 @@ Záznam si uložíme do property `$record`, abychom jej měli k dispozici v meto $this->facade->update($id, $data); // ... } -} ``` -Nicméně, a to by mělo být **nejdůležitejším poznatkem celého kódu**, musíme se při tvorbě formuláře ujistit, že akce je skutečně `edit`. Protože jinak by ověření v metodě `actionEdit()` vůbec neproběhlo! +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: +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 @@ -153,14 +152,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // oveření existence záznamu + !$record // ověření existence záznamu || !$this->facade->isEditAllowed(/*...*/) // kontrola oprávnění ) { $this->error(); // chyba 404 @@ -168,7 +167,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter $form = $this->getComponent('recordForm'); $form->setDefaults($record); // nastavení výchozích hodnot - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form @@ -185,18 +184,18 @@ class RecordPresenter extends Nette\Application\UI\Presenter return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // přidání záznamu do databáze - $this->flashMessage('Successfully added'); + $this->flashMessage('Záznam byl přidán'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); $this->facade->update($id, $data); // aktualizace záznamu - $this->flashMessage('Successfully updated'); + $this->flashMessage('Záznam byl upraven'); $this->redirect('...'); } } diff --git a/best-practices/cs/dynamic-snippets.texy b/best-practices/cs/dynamic-snippets.texy index 104844e59c..fb05bbd60f 100644 --- a/best-practices/cs/dynamic-snippets.texy +++ b/best-practices/cs/dynamic-snippets.texy @@ -1,7 +1,10 @@ Dynamické snippety ****************** -Poměrně často při vývoji aplikací vyvstává potřeba provádět AJAXové operace například nad jednotlivými řádky tabulky či položkami seznamu. Pro příklad si můžeme zvolit výpis článků, přičemž u každého z nich umožníme přihlášenému uživateli zvolit hodnocení "líbí/nelíbí". Kód presenteru a odpovídající šablony bez AJAXu bude vypadat přibližně následovně (uvádím nejdůležitější výseky, kód počítá s existencí služby pro značení si hodnocení a získáním kolekce článků - konkrétní implementace není pro účely tohoto návodu důležitá): +.[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 @@ -37,13 +40,13 @@ 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ě dvě mají svá pro a proti, a proto si je ukážeme jednu po druhé. +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í makra `{snippet}`, kdy je v názvu snippetu použita proměnná. Takový snippet se nemůže v šabloně nalézat jen tak kdekoliv - musí být obalen statickým snippetem, tj. obyčejným, nebo uvnitř `{snippetArea}`. Naši šablonu bychom mohli upravit následovně. +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 @@ -79,9 +82,9 @@ public function handleLike(int $articleId): void } ``` -Nápodobně upravíme i sesterskou metodu `handleUnlike()`, a AJAX je funkční! +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á, zjistíme, že ačkoliv navenek se aplikace tváří úsporně (vrátí pouze jeden jediný snippet pro daný článek), ve skutečnosti na serveru vykreslila snippety všechny. Kýžený snippet nám umístila do payloadu, a ostatní zahodila (zcela zbytečně je tedy také získala z databáze). +Ř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<Something>`: diff --git a/best-practices/cs/editors-and-tools.texy b/best-practices/cs/editors-and-tools.texy deleted file mode 100644 index 13e3d10138..0000000000 --- a/best-practices/cs/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editory & nástroje -****************** - -.[perex] -Můžete být zdatný programátor, ale teprve s dobrými nástroji se z vás stane mistr. V této kapitole najdete tipy na důležité nástroje, editory a pluginy. - - -IDE editor -========== - -Rozhodně doporučujeme pro vývoj používat plnohodnotné IDE, jako je třeba PhpStorm, NetBeans, VS Code, a nikoliv jen textový editor s podporou PHP. Rozdíl je opravdu zásadní. Není důvod se spokojit s pouhým editorem, který sice umí obarvovat syntaxi, ale nedosahuje možností špičkového IDE, které přesně napovídá, hlídá chyby, umí refaktorovat kód a spoustu dalšího. Některé IDE jsou placené, jiné dokonce zdarma. - -**NetBeans IDE** má podporu pro Nette, Latte a NEON už vestavěnou. - -**PhpStorm**: nainstalujte si tyto pluginy v `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: najděte v marketplace "Nette Latte + Neon" plugin. - -Také si propojte Tracy s editorem. Při zobrazení chybové stránky pak půjde kliknout na jména souborů a ty se otevřou v editoru s kurzorem na příslušné řádce. Přečtěte si, [jak systém nakonfigurovat|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan je nástroj, který odhalí logické chyby v kódu dřív, než jej spustíte. - -Nainstalujeme jej pomocí Composeru: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Vytvoříme v projektu konfigurační soubor `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -A následně jej necháme zanalyzovat třídy ve složce `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Vyčerpávající dokumentaci najdete přímo na [stránkách PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] zkontroluje a případně opraví některé z formálních chyb ve vašich zdrojových kódech: - -- odstraňuje [BOM |nette:glossary#BOM] -- kontroluje validitu [Latte |latte:] šablon -- kontroluje validitu souborů `.neon`, `.php` a `.json` -- kontroluje výskyt [kontrolních znaků |nette:glossary#Kontrolní znaky] -- kontroluje, zda je soubor kódován v UTF-8 -- kontroluje chybně zapsané `/* @anotace */` (chybí hvězdička) -- odstraňuje ukončovací `?>` u PHP souborů -- odstraňuje pravostranné mezery a zbytečné řádky na konci souboru -- normalizuje oddělovače řádků na systémové (pokud uvedete volbu `-l`) - - -Composer -======== - -[Composer] je nástroj na správu závislostí v PHP. Dovoluje nám deklarovat libovolně složité závislosti jednotlivých knihoven a pak je za nás nainstaluje do našeho projektu. - - -Requirements Checker -==================== - -Šlo o nástroj, který testoval běhové prostředí serveru a informoval, zda (a do jaké míry) je možné framework používat. V současnosti je Nette možné používat na každém serveru, který má minimální požadovanou verzi PHP. diff --git a/best-practices/cs/form-reuse.texy b/best-practices/cs/form-reuse.texy index bd6c3b8afd..5a7a520878 100644 --- a/best-practices/cs/form-reuse.texy +++ b/best-practices/cs/form-reuse.texy @@ -168,7 +168,7 @@ class EditFormFactory extends FormFactory } ``` -Použití dedičnosti by bylo v tomto případě zcela kontraproduktivní. Na problémy byste narazili velmi rychle. Třeba ve chvíli, kdybyste chtěli přidat metodě `create()` parametry; PHP by zahlásilo chybu, že se její signatura liší od rodičovské. Nebo při předávání závislosti do třídy `EditFormFactory` přes konstruktor. Nastala by situace, které říkáme [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. +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í]. @@ -193,11 +193,11 @@ class EditFormFactory $form->addText('title', 'Titulek:'); // zde se přidávají další formulářová pole $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // zpracování odeslaných dat @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Samotné přesměrování ale necháme na presenteru. Ten přidá události `onSuccess` další handler, který přesmerová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. +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 @@ -232,7 +232,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Toto řešení využívá vlastnost formulářů, že když se nad formulářem nebo jeho prvkem zavolá `addError()`, už další handler `onSuccess` se nevolá. +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 @@ -263,7 +263,7 @@ Je potřeba si uvědomit, že třída `Form` je v první řadě nástrojem pro s 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 renderovat formulář specifickým způsobem, neboť součástí komponenty je i šablona. Nebo lze využít signály pro AJAXovou komunikaci a donačítání informací do formuláře, například pro napovídání, atd. +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 @@ -284,12 +284,12 @@ class EditControl extends Nette\Application\UI\Control $form->addText('title', 'Titulek:'); // zde se přidávají další formulářová pole $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // zpracování odeslaných dat diff --git a/best-practices/cs/inject-method-attribute.texy b/best-practices/cs/inject-method-attribute.texy index f831f0535f..d22dafecbd 100644 --- a/best-practices/cs/inject-method-attribute.texy +++ b/best-practices/cs/inject-method-attribute.texy @@ -44,7 +44,7 @@ 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. -Properites označíme atributem: (dříve se používala anotace `/** @inject */`) +Properties označíme atributem (dříve se používala anotace `/** @inject */`): ```php use Nette\DI\Attributes\Inject; // tento řádek je důležitý @@ -58,4 +58,4 @@ class MyPresenter extends Nette\Application\UI\Presenter Výhodou tohoto způsobu předávání závislostí byla velice úsporná podoba zápisu. Nicméně s příchodem [constructor property promotion |https://blog.nette.org/cs/php-8-0-kompletni-prehled-novinek#toc-constructor-property-promotion] se jeví snazší použít konstruktor. -Naopak tento způsob trpí stejnými nedostatky, jako předávání závislosti do properties obecně: nemáme kontrolu nad změnami v proměnné a zároveň se proměnná stává součástí veřejného rozhraní třídy, což je nežádnoucí. +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í. diff --git a/best-practices/cs/lets-create-contact-form.texy b/best-practices/cs/lets-create-contact-form.texy index ec298951be..573befbc70 100644 --- a/best-practices/cs/lets-create-contact-form.texy +++ b/best-practices/cs/lets-create-contact-form.texy @@ -4,7 +4,7 @@ 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. +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í: @@ -21,14 +21,14 @@ class HomePresenter extends Presenter ->setRequired('Zadejte jméno'); $form->addEmail('email', 'E-mail:') ->setRequired('Zadejte e-mail'); - $form->addTextarea('message', 'Zpráva:') + $form->addTextArea('message', 'Zpráva:') ->setRequired('Zadejte zprávu'); $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // odeslání emailu } @@ -41,16 +41,13 @@ Komponentu `contactForm` necháme vykreslit v šabloně `Home/default.latte`: ```latte {block content} -<h1>Kontantní formulář</h1> +<h1>Kontaktní formulář</h1> {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 -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -129,7 +126,7 @@ Zatím se odesílá prostý textový email obsahující pouze zprávu odeslanou </html> ``` -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 souboru, prvním parametrem je cesta k šabloně a druhým jsou proměnné. +Zbývá upravit `ContactFacade`, aby tuto šablonu používal. V konstruktoru si vyžádáme třídu `LatteFactory`, která umí vyrobit objekt `Latte\Engine`, tedy [vykreslovač Latte šablon |latte:develop#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; @@ -204,7 +201,7 @@ services: - App\Model\ContactFacade(adminEmail: admin@example.com) ``` -A je to. Pokud by položek v sekci `services` bylo hodně a měli byste pocit, že email se mezi nimi ztrácí, můžeme z něj udělat proměnnou. Upravíme zápis na: +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: diff --git a/best-practices/cs/microsites.texy b/best-practices/cs/microsites.texy index b2b0c92b0a..fb27c0ce3b 100644 --- a/best-practices/cs/microsites.texy +++ b/best-practices/cs/microsites.texy @@ -3,9 +3,9 @@ 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. +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: +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 @@ -29,11 +29,11 @@ $router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { // detekujeme jazyk prohlížeče a přesměrujeme na URL /en nebo /de atd. $supportedLangs = ['en', 'de', 'cs']; $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); // routa pro URL https://example.com/cs nebo https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { +$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'); @@ -54,8 +54,8 @@ Proč používat Nette pro microsite? - Programátoři, kteří někdy vyzkoušeli [Tracy|tracy:], si dnes neumí představit, že by něco programovali bez ní. - Především ale využijete šablonovací systém [Latte|latte:], protože už od 2 stránek budete chtít mít oddělený [layout a obsah|latte:template-inheritance]. -- A rozhodně se chcete spolehout na [automatické escapování |latte:safety-first], aby nevznikla zranitelnost XSS -- Nette taky zajistí, že se při chybě nikdy neobrazí programátorské chybové hlášky PHP, ale uživateli srozumitelná stránka. +- 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. diff --git a/best-practices/cs/pagination.texy b/best-practices/cs/pagination.texy index cbf0a7798d..b472525c7b 100644 --- a/best-practices/cs/pagination.texy +++ b/best-practices/cs/pagination.texy @@ -30,7 +30,7 @@ class ArticleRepository } ``` -V presenteru si pak injectujeme modelovou třídu a v render metodě si vyžádáme publikované články, které předáme do šablony: +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; @@ -67,11 +67,11 @@ V šabloně `default.latte` se pak postaráme o výpis článků: ``` -Tímto způsobem umíme vypsat všechny články, což však začne působit problémy v momentě, kdy počet článků vzroste. V tom okamžiku se příjde vhod implementace stránkovacího mechanismu. +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ánku v databázi, kterou budeme potřebovat pro nastavení Paginatoru: +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; @@ -164,7 +164,7 @@ class HomePresenter extends Nette\Application\UI\Presenter {if !$paginator->isFirst()} <a n:href="default, 1">První</a>  |  - <a n:href="default, $paginator->page-1">Předchozí</a> + <a n:href="default, $paginator->getPage() - 1">Předchozí</a>  |  {/if} @@ -180,7 +180,7 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Takto jsme doplnili stránku o možnost stránkování pomocí Paginatoru. V případě, kdy namísto [Nette Database Core |database:sql-way] jako databázovou vrstvu použijeme [Nette Database Explorer |database:explorer], jsme schopni implementovat stránkování i bez použití Paginatoru. Třída `Nette\Database\Table\Selection` totiž obsahuje metodu [page |api:Nette\Database\Table\Selection::_page] s logikou stránkování převzatou z Paginatoru. +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: @@ -205,7 +205,7 @@ class ArticleRepository } ``` -V presenteru nemusíme vytvářet Paginator, použijeme místo něj metodu třídy `Selection`, kterou nám vrací repositář: +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; diff --git a/best-practices/cs/passing-settings-to-presenters.texy b/best-practices/cs/passing-settings-to-presenters.texy index b510a65c1a..941783e97d 100644 --- a/best-practices/cs/passing-settings-to-presenters.texy +++ b/best-practices/cs/passing-settings-to-presenters.texy @@ -4,7 +4,7 @@ Předání nastavení do presenterů .[perex] Potřebujete do presenterů předávat argumenty, které nejsou objekty (např. informaci, zda běží v debug režimu, cesty k adresářům apod.), a tedy nemohou být předány automaticky pomocí autowiringu? Řešením je zapouzdřit je do objektu `Settings`. -Služba `Settings` přestavuje velmi snadný a přitom užitečný způsob, jak poskytovat informace o běžící aplikaci presenterům. Její konkrétní podoba záleží čistě na vašich konkrétních potřebách. Příklad: +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; diff --git a/best-practices/cs/post-links.texy b/best-practices/cs/post-links.texy index 2d24e00a67..1de8883a78 100644 --- a/best-practices/cs/post-links.texy +++ b/best-practices/cs/post-links.texy @@ -2,7 +2,7 @@ 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. I když ideální by byla metoda DELETE, ale tu nelze bez JavaScriptu vyvolat, proto se historicky používá POST. +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: @@ -10,7 +10,7 @@ Jak na to v praxi? Využijte tento jednoduchý trik. Na začátku šablony si vy <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: +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> diff --git a/best-practices/cs/presenter-traits.texy b/best-practices/cs/presenter-traits.texy index 2e5091ce00..b9a6bdc77b 100644 --- a/best-practices/cs/presenter-traits.texy +++ b/best-practices/cs/presenter-traits.texy @@ -6,7 +6,7 @@ Pokud potřebujeme ve více presenterech implementovat stejný kód (např. ově 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í. +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]. diff --git a/best-practices/cs/pretty-urls.texy b/best-practices/cs/pretty-urls.texy new file mode 100644 index 0000000000..397ec995f1 --- /dev/null +++ b/best-practices/cs/pretty-urls.texy @@ -0,0 +1,204 @@ +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]. diff --git a/best-practices/cs/restore-request.texy b/best-practices/cs/restore-request.texy index 46a25c7f87..c5f6c94511 100644 --- a/best-practices/cs/restore-request.texy +++ b/best-practices/cs/restore-request.texy @@ -8,7 +8,7 @@ Aktuální požadavek lze uložit do session pomocí metody `storeRequest()`, kt Obnovení požadavku provádí metoda `restoreRequest($key)`, které předáme získaný identifikátor. Ta přesměruje na původní presenter a view. Pokud však uložený požadavek obsahuje odeslání formuláře, na původní presenter přejde metodou `forward()`, formuláři předá dříve vyplněné hodnoty a nechá jej znovu vykreslit. Uživatel tak má možnost formulář opětovně odeslat a žádná data se neztratí. -Důležité je, že `restoreRequest()` kontroluje, zda nově přihlášený uživatel je tentýž, co formulář původně vyplňoval. Pokud ne, požadavek zahodí a nic neudělá. +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`. @@ -41,11 +41,11 @@ class SignPresenter extends Nette\Application\UI\Presenter { $form = new Nette\Application\UI\Form; // ... přidáme políčka formuláře ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + $form->onSuccess[] = $this->signInFormSucceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSucceeded($form) { // ... tady uživatele přihlásíme ... diff --git a/best-practices/de/@home.texy b/best-practices/de/@home.texy index d855293435..e61ec24211 100644 --- a/best-practices/de/@home.texy +++ b/best-practices/de/@home.texy @@ -1,5 +1,5 @@ -Anleitungen und Verfahren -************************* +Anleitungen und Best Practices +****************************** .[perex] Anleitungen, Lösungen für häufige Aufgaben und *Best Practices* für Nette. @@ -9,16 +9,17 @@ Anleitungen, Lösungen für häufige Aufgaben und *Best Practices* für Nette. <div> -Nette-Anwendung ---------------- -- [Methoden und Attribute inject |inject-method-attribute] -- [Zusammensetzen von Presentern aus Traits |presenter-traits] -- [Übergeben von Einstellungen an Presenter |passing-settings-to-presenters] -- [Wie man zu einer früheren Seite zurückkehrt |restore-request] -- [Paginierung von Datenbankergebnissen |pagination] +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 korrekt verwendet |post-links] +- [Wie man POST-Links richtig verwendet |post-links] +- [Schöne URLs mit Slugs |pretty-urls] </div> <div> @@ -26,23 +27,22 @@ Nette-Anwendung Formulare --------- -- [Wiederverwendung von Formularen |form-reuse] +- [Formulare wiederverwenden |form-reuse] - [Formular zum Erstellen und Bearbeiten von Datensätzen |creating-editing-form] -- [Erstellen eines Kontaktformulars |lets-create-contact-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> -Allgemeines ------------ +Allgemein +--------- - [Wie man eine Konfigurationsdatei lädt |bootstrap:] -- [Wie man Micro-Websites schreibt |microsites] -- [Warum Nette die PascalCase-Notation für Konstanten verwendet |https://blog.nette.org/de/for-less-screaming-in-the-code] -- [Warum Nette das Interface-Suffix nicht verwendet |https://blog.nette.org/de/prefixes-and-suffixes-do-not-belong-in-interface-names] +- [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] -- [Tipps für Editoren & Werkzeuge |editors-and-tools] - [Einführung in die objektorientierte Programmierung |nette:introduction-to-object-oriented-programming] </div> @@ -51,11 +51,11 @@ Allgemeines Beispiellösungen ---------------- -- [Nette Beispiele |https://github.com/nette-examples] +- [Nette examples |https://github.com/nette-examples] - [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte Beispiele |https://contributte.org/examples.html] +- [Contributte examples |https://contributte.org/examples.html] - [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quickstart |quickstart:] +- [Quick start |quickstart:] </div> <div> @@ -63,7 +63,7 @@ Beispiellösungen Videos ------ -Hunderte von Aufzeichnungen von "Poslední sobota" und Videos über Nette finden Sie unter einem Dach auf dem [Youtube-Kanal des Nette Frameworks |https://www.youtube.com/user/NetteFramework]. +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 new file mode 100644 index 0000000000..cedf8742b7 --- /dev/null +++ b/best-practices/de/@left-menu.texy @@ -0,0 +1,34 @@ +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 index c3bc189de7..8aa9f9c5b2 100644 --- a/best-practices/de/@meta.texy +++ b/best-practices/de/@meta.texy @@ -1,2 +1 @@ -{{sitename: Anleitungen und Verfahren}} -{{leftbar: www:@menu-common}} +{{sitename: Anleitungen und Best Practices}} diff --git a/best-practices/de/attribute-requires.texy b/best-practices/de/attribute-requires.texy index b6a5b9f6c2..d4f34945a5 100644 --- a/best-practices/de/attribute-requires.texy +++ b/best-practices/de/attribute-requires.texy @@ -2,30 +2,30 @@ Wie man das Attribut `#[Requires]` verwendet ******************************************** .[perex] -Beim Schreiben einer Webanwendung stoßen Sie oft auf die Notwendigkeit, den Zugriff auf bestimmte Teile Ihrer Anwendung zu beschränken. Vielleicht möchten Sie, dass einige Anfragen nur über ein Formular (also mit der POST-Methode) Daten senden können oder nur für AJAX-Aufrufe zugänglich sind. Im Nette Framework 3.2 gibt es ein neues Werkzeug, mit dem Sie solche Einschränkungen sehr elegant und übersichtlich festlegen können: das Attribut `#[Requires]`. +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 hinzufügen. Da es sich eigentlich um eine Klasse handelt, müssen Sie für die Funktion der folgenden Beispiele die use-Klausel angeben: +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 auch bei diesen Methoden verwenden: +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, das Attribut können Sie also auch bei ihnen verwenden. +Die letzten beiden Methoden betreffen auch Komponenten, Sie können das Attribut also auch bei ihnen einsetzen. -Wenn die im Attribut angegebenen Bedingungen nicht erfüllt sind, wird ein HTTP-Fehler 4xx ausgelöst. +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 beispielsweise den Zugriff nur durch das Absenden eines Formulars erlauben möchten, stellen Sie ein: +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 @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Warum sollten Sie POST anstelle von GET für Aktionen verwenden, die den Zustand ändern, und wie geht das? [Lesen Sie die Anleitung |post-links]. +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 standardmäßig aus [Sicherheitsgründen nicht zulassen |application:presenters#Überprüfung der HTTP-Methode]. +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 möchten, dass ein Presenter oder eine Methode nur für AJAX-Anfragen verfügbar ist, verwenden Sie: +Wenn Sie wollen, dass ein Presenter oder eine Methode nur für AJAX-Requests zugänglich ist, verwenden Sie: ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ class AjaxPresenter extends Nette\Application\UI\Presenter Gleicher Ursprung ----------------- -Zur Erhöhung der Sicherheit können Sie verlangen, dass die Anfrage von derselben Domain stammt. Dadurch verhindern Sie [CSRF-Schwachstellen |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: +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)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -Bei `handle<Signal>()`-Methoden wird der Zugriff von derselben Domain automatisch verlangt. Wenn Sie also den Zugriff von jeder beliebigen Domain erlauben möchten, geben Sie an: +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)] @@ -80,7 +80,7 @@ public function handleList(): void Zugriff über forward -------------------- -Manchmal ist es nützlich, den Zugriff auf einen Presenter so zu beschränken, dass er nur indirekt verfügbar ist, beispielsweise durch Verwendung der Methode `forward()` oder `switch()` aus einem anderen Presenter. So werden beispielsweise Error-Presenter geschützt, damit sie nicht über die URL aufgerufen werden können: +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)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -In der Praxis ist es oft notwendig, bestimmte Views zu markieren, auf die erst aufgrund der Logik im Presenter zugegriffen werden kann. Also wieder, damit sie nicht direkt geöffnet werden können: +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 @@ -114,7 +114,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Konkrete Aktionen ----------------- -Sie können auch einschränken, dass bestimmter Code, wie das Erstellen einer Komponente, nur für spezifische Aktionen im Presenter verfügbar ist: +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 @@ -126,15 +126,15 @@ class EditDeletePresenter extends Nette\Application\UI\Presenter } ``` -Im Falle einer einzelnen Aktion ist es nicht notwendig, ein Array zu schreiben: `#[Requires(actions: 'default')]` +Bei einer einzelnen Aktion muss kein Array geschrieben werden: `#[Requires(actions: 'default')]` Eigene Attribute ---------------- -Wenn Sie das Attribut `#[Requires]` wiederholt mit denselben Einstellungen verwenden möchten, können Sie ein eigenes Attribut erstellen, das `#[Requires]` erbt und es nach Bedarf konfiguriert. +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. -Beispielsweise ermöglicht `#[SingleAction]` den Zugriff nur über die Aktion `default`: +Zum Beispiel erlaubt `#[SingleAction]` den Zugriff nur über die Aktion `default`: ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -Oder `#[RestMethods]` erlaubt den Zugriff über alle HTTP-Methoden, die für REST-APIs verwendet werden: +Oder `#[RestMethods]` erlaubt den Zugriff über alle HTTP-Methoden, die für eine REST-API verwendet werden: ```php #[\Attribute] @@ -174,4 +174,4 @@ class ApiPresenter extends Nette\Application\UI\Presenter Fazit ----- -Das Attribut `#[Requires]` gibt Ihnen große Flexibilität und Kontrolle darüber, wie auf Ihre Webseiten zugegriffen wird. Mit einfachen, aber leistungsstarken Regeln können Sie die Sicherheit und die korrekte Funktion Ihrer Anwendung erhöhen. Wie Sie sehen, kann die Verwendung von Attributen in Nette Ihre Arbeit nicht nur erleichtern, sondern auch sicherer machen. +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 index 554def763b..3c5fdb9006 100644 --- a/best-practices/de/composer.texy +++ b/best-practices/de/composer.texy @@ -3,10 +3,10 @@ Composer: Tipps zur Verwendung <div class=perex> -Composer ist ein Werkzeug zur Verwaltung von Abhängigkeiten in PHP. Es ermöglicht uns, die Bibliotheken aufzulisten, von denen unser Projekt abhängt, und wird sie für uns installieren und aktualisieren. Wir zeigen Ihnen: +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 man Composer installiert -- seine Verwendung in einem neuen oder bestehenden Projekt +- wie Sie Composer installieren +- wie Sie ihn in einem neuen oder bestehenden Projekt verwenden </div> @@ -14,7 +14,7 @@ Composer ist ein Werkzeug zur Verwaltung von Abhängigkeiten in PHP. Es ermögli Installation ============ -Composer ist eine ausführbare `.phar`-Datei, die Sie herunterladen und wie folgt installieren: +Composer ist eine ausführbare `.phar`-Datei, die Sie folgendermaßen herunterladen und installieren: Windows @@ -26,9 +26,9 @@ Verwenden Sie den offiziellen Installer [Composer-Setup.exe |https://getcomposer Linux, macOS ------------ -Es genügen 4 Befehle, die Sie von [dieser Seite |https://getcomposer.org/download/] kopieren können. +Es genügen 4 Befehle, die Sie sich von [dieser Seite |https://getcomposer.org/download/] kopieren können. -Durch das Ablegen im Ordner, der im System-`PATH` enthalten ist, wird Composer global zugänglich: +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 @@ -38,7 +38,7 @@ $ mv ./composer.phar ~/bin/composer # oder /usr/local/bin/composer Verwendung im Projekt ===================== -Um Composer in Ihrem Projekt verwenden zu können, benötigen Sie lediglich die Datei `composer.json`. Diese beschreibt die Abhängigkeiten Ihres Projekts und kann auch weitere Metadaten enthalten. Eine grundlegende `composer.json` kann also so aussehen: +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 { @@ -48,17 +48,17 @@ Um Composer in Ihrem Projekt verwenden zu können, benötigen Sie lediglich die } ``` -Hier geben wir an, dass unsere Anwendung (oder Bibliothek) das Paket `nette/database` benötigt (der Paketname setzt sich aus dem Organisationsnamen und dem Projektnamen zusammen) und eine Version wünscht, die der Bedingung `^3.0` entspricht (d.h. die neueste Version 3). +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 Projektstamm die Datei `composer.json` und starten die Installation mit: +Wir haben also im Wurzelverzeichnis des Projekts die Datei `composer.json` und starten: ```shell composer update ``` -Composer lädt Nette Database in den Ordner `vendor/`. Außerdem erstellt er die Datei `composer.lock`, die Informationen darüber enthält, welche Versionen der Bibliotheken genau installiert wurden. +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 generiert die Datei `vendor/autoload.php`, die wir einfach einbinden können und sofort mit der Verwendung der Bibliotheken beginnen können, ohne weitere Arbeit: +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'; @@ -70,17 +70,17 @@ $db = new Nette\Database\Connection('sqlite::memory:'); Aktualisierung der Pakete auf die neuesten Versionen ==================================================== -Die Aktualisierung der verwendeten Bibliotheken auf die neuesten Versionen gemäß den in `composer.json` definierten Bedingungen übernimmt der Befehl `composer update`. Z.B. bei der Abhängigkeit `"nette/database": "^3.0"` installiert er die neueste Version 3.x.x, aber nicht mehr Version 4. +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` beispielsweise auf `"nette/database": "^4.1"` zu aktualisieren, damit die neueste Version installiert werden kann, verwenden Sie den Befehl `composer require nette/database`. +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 in der Befehlszeile auflisten, z.B.: +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 ... ``` -Was unpraktisch ist. Verwenden Sie stattdessen das einfache Skript "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, das dies für Sie erledigt: +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 @@ -96,17 +96,17 @@ Ein neues Nette-Projekt erstellen Sie mit einem einzigen Befehl: composer create-project nette/web-project projektname ``` -Geben Sie als `projektname` den Namen des Verzeichnisses für Ihr Projekt ein und bestätigen Sie. Composer lädt das Repository `nette/web-project` von GitHub herunter, das bereits die Datei `composer.json` enthält, und gleich danach das Nette Framework. Es sollte nur noch notwendig sein, die [Berechtigungen |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen] für das Schreiben in die Ordner `temp/` und `log/` festzulegen, und das Projekt sollte zum Leben erweckt werden. +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 das Projekt gehostet wird, vergessen Sie nicht, [sie einzustellen |#PHP-Version]. +Wenn Sie wissen, auf welcher PHP-Version Ihr Projekt gehostet wird, [stellen Sie sie unbedingt ein |#PHP-Version]. PHP-Version =========== -Composer installiert immer die Versionen von Paketen, die mit der PHP-Version kompatibel sind, die Sie gerade verwenden (genauer gesagt, mit der PHP-Version, die in der Befehlszeile beim Ausführen von Composer verwendet wird). Dies ist jedoch wahrscheinlich nicht die gleiche Version, die Ihr Hosting verwendet. Daher ist es sehr wichtig, der Datei `composer.json` Informationen über die PHP-Version auf dem Hosting hinzuzufügen. Danach werden nur noch Versionen von Paketen installiert, die mit dem Hosting kompatibel sind. +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 beispielsweise auf PHP 8.2.3 laufen wird, legen wir mit dem Befehl fest: +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 @@ -124,7 +124,7 @@ So wird die Version in die Datei `composer.json` geschrieben: } ``` -Die PHP-Versionsnummer wird jedoch noch an einer anderen Stelle der Datei angegeben, und zwar im Abschnitt `require`. Während die erste Zahl angibt, für welche Version Pakete installiert werden, gibt die zweite Zahl an, für welche Version die Anwendung selbst geschrieben ist. Und danach stellt beispielsweise PhpStorm das *PHP language level* ein. (Natürlich macht es keinen Sinn, dass sich diese Versionen unterscheiden, daher ist die doppelte Angabe eine Ungeschicklichkeit.) Diese Version stellen Sie mit dem Befehl ein: +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 @@ -144,69 +144,69 @@ Oder direkt in der Datei `composer.json`: 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 möchten, beispielsweise zum Testen, wird Composer die Installation eines solchen Pakets verweigern. Die Lösung ist die Option `--ignore-platform-req=php+`, die bewirkt, dass Composer die Obergrenzen der erforderlichen PHP-Version ignoriert. +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 Änderungen der Versionsnummern kommt es vor, dass ein Konflikt auftritt. Ein Paket hat Anforderungen, die im Widerspruch zu einem anderen stehen und ähnliches. Composer gibt jedoch manchmal falsche Meldungen aus. Er meldet einen Konflikt, der real nicht existiert. In diesem Fall hilft es, die Datei `composer.lock` zu löschen und es erneut zu versuchen. +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 daraus entnehmen, was und wie Sie es anpassen müssen. +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 versucht, Pakete zu finden, wenn wir ihm nichts anderes sagen. Wir können hier auch eigene Pakete veröffentlichen. +[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 haben, die wir einfach nicht öffentlich hosten können, erstellen wir dafür ein Firmen-Repository. +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]. +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, das Sie durch Einbinden der Datei `vendor/autoload.php` starten. +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. -Es ist jedoch auch möglich, Composer zum Laden weiterer Klassen auch außerhalb des Ordners `vendor` zu verwenden. Die erste Möglichkeit ist, Composer definierte Ordner und Unterordner durchsuchen zu lassen, alle Klassen zu finden und sie in den Autoloader aufzunehmen. Dies erreichen Sie durch die Einstellung `autoload > classmap` in `composer.json`: +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/", # beinhaltet den Ordner src/ und seine Unterordner + "src/", # schließt das Verzeichnis src/ und seine Unterverzeichnisse ein ] } } ``` -Anschließend muss bei jeder Änderung der Befehl `composer dumpautoload` ausgeführt und die Autoloading-Tabellen neu generiert werden. Das ist äußerst unpraktisch, und es ist viel besser, diese Aufgabe dem [RobotLoader|robot-loader:] zu überlassen, der dieselbe Tätigkeit automatisch im Hintergrund und viel schneller erledigt. +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, [PSR-4|https://www.php-fig.org/psr/psr-4/] einzuhalten. Vereinfacht gesagt handelt es sich um ein System, bei dem Namensräume und Klassennamen der Verzeichnisstruktur und den Dateinamen entsprechen, d.h. z.B. `App\Core\RouterFactory` befindet sich in der Datei `/path/to/App/Core/RouterFactory.php`. Beispielkonfiguration: +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 Namensraum App\ befindet sich im Verzeichnis app/ + "App\\": "app/" # der Namespace App\ liegt im Verzeichnis app/ } } } ``` -Wie genau das Verhalten konfiguriert wird, erfahren Sie in der [Composer-Dokumentation|https://getcomposer.org/doc/04-schema.md#psr-4]. +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 möchten eine neue Entwicklungsversion eines Pakets testen. Wie gehen Sie vor? Fügen Sie zunächst dieses Optionspaar zur Datei `composer.json` hinzu, das die Installation von Entwicklungsversionen von Paketen erlaubt, aber nur darauf zurückgreift, wenn keine Kombination von stabilen Versionen existiert, die den Anforderungen entspricht: +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 { @@ -215,21 +215,21 @@ Sie möchten eine neue Entwicklungsversion eines Pakets testen. Wie gehen Sie vo } ``` -Weiterhin empfehlen wir, die Datei `composer.lock` zu löschen, da Composer manchmal unverständlicherweise die Installation verweigert und dies das Problem löst. +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 hat die Nummer 4.0. Sie installieren sie mit dem Befehl: +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 bestimmte Version installieren, zum Beispiel 4.0.0-RC2: +Oder Sie können eine konkrete Version installieren, zum Beispiel 4.0.0-RC2: ```shell composer require nette/utils:4.0.0-RC2 ``` -Wenn jedoch ein anderes Paket von der Bibliothek abhängt, das auf eine ältere Version gesperrt ist (z.B. `^3.1`), ist es ideal, das Paket zu aktualisieren, damit es mit der neuen Version funktioniert. Wenn Sie jedoch die Einschränkung nur umgehen und Composer zwingen möchten, die Entwicklungsversion zu installieren und so zu tun, als wäre es eine ältere Version (z.B. 3.1.6), können Sie das Schlüsselwort `as` verwenden: +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" @@ -239,9 +239,9 @@ composer require nette/utils "4.0.x-dev as 3.1.6" Aufruf von Befehlen =================== -Über Composer können eigene vorbereitete Befehle und Skripte aufgerufen werden, als wären es native Composer-Befehle. Bei Skripten, die sich im Ordner `vendor/bin` befinden, muss dieser Ordner nicht angegeben werden. +Ü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 dem [Nette Tester|tester:] Tests startet: +Als Beispiel definieren wir in der Datei `composer.json` ein Skript, das mit [Nette Tester |tester:] die Tests startet: ```js { @@ -251,19 +251,19 @@ Als Beispiel definieren wir in der Datei `composer.json` ein Skript, das mit dem } ``` -Die Tests starten wir dann mit `composer tester`. Den Befehl können wir auch aufrufen, wenn wir uns nicht im Stammverzeichnis des Projekts, sondern in einem Unterverzeichnis befinden. +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 die Autoren von Open Source erfreuen können. Geben Sie auf GitHub den Bibliotheken, die Ihr Projekt verwendet, auf einfache Weise einen Stern. Installieren Sie einfach die Bibliothek `symfony/thanks`: +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 führen Sie dann aus: +Und dann auszuführen: ```shell composer thanks @@ -275,7 +275,7 @@ 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, es nicht zu verwenden: +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 index 4238bf13fa..b91ee3eec1 100644 --- a/best-practices/de/creating-editing-form.texy +++ b/best-practices/de/creating-editing-form.texy @@ -2,15 +2,15 @@ Formular zum Erstellen und Bearbeiten von Datensätzen ***************************************************** .[perex] -Wie implementiert man in Nette das Hinzufügen und Bearbeiten von Datensätzen korrekt, sodass für beides dasselbe Formular verwendet wird? +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 Bearbeiten von Datensätzen identisch, sie unterscheiden sich vielleicht nur durch die Beschriftung auf dem Button. Wir zeigen Beispiele für einfache Presenter, in denen wir das Formular zunächst zum Hinzufügen eines Datensatzes verwenden, dann zum Bearbeiten und schließlich beide Lösungen zusammenführen. +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 das Beispiel nicht wesentlich ist. +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 @@ -27,13 +27,13 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $form = new Form; - // ... Formularfelder hinzufügen ... + // ... wir fügen die Formularfelder hinzu ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // Datensatz zur Datenbank hinzufügen $this->flashMessage('Erfolgreich hinzugefügt'); @@ -51,7 +51,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter Bearbeiten eines Datensatzes ---------------------------- -Nun zeigen wir, wie ein Presenter zum Bearbeiten eines Datensatzes aussehen würde: +Sehen wir uns nun an, wie ein Presenter zum Bearbeiten eines Datensatzes aussehen würde: ```php @@ -70,8 +70,8 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // Überprüfung der Existenz des Datensatzes - || !$this->facade->isEditAllowed(/*...*/) // Berechtigungsprüfung + !$record // Existenz des Datensatzes prüfen + || !$this->facade->isEditAllowed(/*...*/) // Berechtigungen prüfen ) { $this->error(); // Fehler 404 } @@ -81,21 +81,21 @@ class RecordPresenter extends Nette\Application\UI\Presenter protected function createComponentRecordForm(): Form { - // Überprüfen, ob die Aktion 'edit' ist + // wir prüfen, dass die Aktion 'edit' ist if ($this->getAction() !== 'edit') { $this->error(); } $form = new Form; - // ... Formularfelder hinzufügen ... + // ... wir fügen die Formularfelder hinzu ... $form->setDefaults($this->record); // Standardwerte setzen - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->update($this->record->id, $data); // Datensatz aktualisieren $this->flashMessage('Erfolgreich aktualisiert'); @@ -104,9 +104,9 @@ class RecordPresenter extends Nette\Application\UI\Presenter } ``` -In der `actionEdit()`-Methode, die gleich zu Beginn des [Presenter-Lebenszyklus |application:presenters#Lebenszyklus des Presenters] ausgeführt wird, überprüfen wir die Existenz des Datensatzes und die Berechtigung des Benutzers, ihn zu bearbeiten. +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. -Wir speichern den Datensatz in der Eigenschaft `$record`, um ihn in der Methode `createComponentRecordForm()` zum Setzen der Standardwerte und in `recordFormSucceeded()` für die ID zur Verfügung zu haben. 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')` abzurufen: +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 @@ -114,7 +114,7 @@ Wir speichern den Datensatz in der Eigenschaft `$record`, um ihn in der Methode { $record = $this->facade->get($id); if ( - // Überprüfung der Existenz und Berechtigungsprüfung + // Existenz prüfen und Berechtigungen kontrollieren ) { $this->error(); } @@ -130,16 +130,15 @@ Wir speichern den Datensatz in der Eigenschaft `$record`, um ihn in der Methode $this->facade->update($id, $data); // ... } -} ``` -Jedoch, und das sollte **die wichtigste Erkenntnis des gesamten Codes** sein, müssen wir beim Erstellen des Formulars sicherstellen, dass die Aktion tatsächlich `edit` ist. Denn andernfalls würde die Überprüfung in der Methode `actionEdit()` überhaupt nicht stattfinden! +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 können dies direkt den Action-Methoden überlassen und die Bedingung entfernen: +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 @@ -153,46 +152,46 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // Überprüfung der Existenz des Datensatzes - || !$this->facade->isEditAllowed(/*...*/) // Berechtigungsprüfung + !$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']; + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form { - // Überprüfen, ob die Aktion 'add' oder 'edit' ist + // wir prüfen, dass die Aktion 'add' oder 'edit' ist if (!in_array($this->getAction(), ['add', 'edit'])) { $this->error(); } $form = new Form; - // ... Formularfelder hinzufügen ... + // ... wir fügen die Formularfelder hinzu ... return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // Datensatz zur Datenbank hinzufügen $this->flashMessage('Erfolgreich hinzugefügt'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); $this->facade->update($id, $data); // Datensatz aktualisieren diff --git a/best-practices/de/dynamic-snippets.texy b/best-practices/de/dynamic-snippets.texy index b4ea7a67d5..8d1b40f455 100644 --- a/best-practices/de/dynamic-snippets.texy +++ b/best-practices/de/dynamic-snippets.texy @@ -1,7 +1,10 @@ Dynamische Snippets ******************* -Bei der Entwicklung von Anwendungen entsteht relativ häufig die Notwendigkeit, AJAX-Operationen beispielsweise auf einzelnen Zeilen einer Tabelle oder Elementen einer Liste durchzuführen. Als Beispiel können wir die Auflistung von Artikeln wählen, wobei wir jedem angemeldeten Benutzer ermöglichen, eine Bewertung "gefällt mir/gefällt mir nicht" abzugeben. Der Code des Presenters und des entsprechenden Templates ohne AJAX wird ungefähr wie folgt aussehen (ich gebe die wichtigsten Ausschnitte an, der Code rechnet mit der Existenz eines Dienstes zum Markieren von Bewertungen und dem Abrufen einer Artikelsammlung - die konkrete Implementierung ist für die Zwecke dieser Anleitung nicht wichtig): +.[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 @@ -24,9 +27,9 @@ Template: <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> + <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> + <a n:href="unlike! $article->id" class=ajax>gefällt mir nicht mehr</a> {/if} </article> ``` @@ -35,15 +38,15 @@ Template: Ajaxifizierung ============== -Lassen Sie uns nun diese einfache Anwendung mit AJAX ausstatten. Die Änderung der Artikelbewertung ist nicht so wichtig, dass eine Weiterleitung erfolgen muss, daher sollte sie idealerweise im Hintergrund per AJAX erfolgen. Wir verwenden [das Hilfsskript aus den Add-ons |application:ajax#Naja] mit der üblichen Konvention, dass AJAX-Links die CSS-Klasse `ajax` haben. +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 genau geht das? Nette bietet 2 Wege: den Weg der sogenannten dynamischen Snippets und den Weg der Komponenten. Beide haben ihre Vor- und Nachteile, daher werden wir sie nacheinander vorstellen. +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 bedeutet in der Latte-Terminologie einen spezifischen Anwendungsfall des `{snippet}`-Tags, bei dem im Snippetnamen eine Variable verwendet wird. Ein solches Snippet kann nicht einfach irgendwo im Template stehen - es muss von einem statischen Snippet, d.h. einem gewöhnlichen, oder innerhalb von `{snippetArea}` umschlossen sein. Unser Template könnten wir wie folgt anpassen. +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 @@ -53,18 +56,18 @@ Ein dynamisches Snippet bedeutet in der Latte-Terminologie einen spezifischen An <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> + <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> + <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 Snippets sind dann zusammen in einem Snippet mit dem Namen `articlesContainer` verpackt. Wenn wir dieses umschließende Snippet weglassen würden, würde uns Latte mit einer Ausnahme darauf hinweisen. +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 uns übrig, das Neuzeichnen im Presenter zu ergänzen - es genügt, die statische Hülle neu zu zeichnen. +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 @@ -72,18 +75,18 @@ public function handleLike(int $articleId): void $this->ratingService->saveLike($articleId, $this->user->id); if ($this->isAjax()) { $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- ist nicht notwendig + // $this->redrawControl('article-' . $articleId); -- nicht nötig } else { $this->redirect('this'); } } ``` -Analog passen wir auch die Schwestermethode `handleUnlike()` an, und AJAX ist funktionsfähig! +Passen Sie die zugehörige Methode `handleUnlike()` auf dieselbe Weise an, und AJAX funktioniert! -Die Lösung hat jedoch einen Nachteil. Wenn wir genauer untersuchen, wie die AJAX-Anfrage abläuft, stellen wir fest, dass, obwohl sich die Anwendung nach außen hin sparsam verhält (sie gibt nur ein einziges Snippet für den gegebenen Artikel zurück), sie tatsächlich auf dem Server alle Snippets gerendert hat. Das gewünschte Snippet wurde uns in den Payload platziert, und die anderen wurden verworfen (sie wurden also auch völlig unnötig aus der Datenbank abgerufen). +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 Prozess zu optimieren, müssen wir dort eingreifen, wo wir die Sammlung `$articles` an das Template übergeben (angenommen in der Methode `renderDefault()`). Wir nutzen die Tatsache, dass die Signalverarbeitung vor den `render<Something>`-Methoden erfolgt: +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 @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -Nun wird bei der Signalverarbeitung anstelle der Sammlung mit allen Artikeln nur ein Array mit einem einzigen Artikel an das Template übergeben - demjenigen, den wir rendern und im Payload an den Browser senden möchten. `{foreach}` wird also nur einmal durchlaufen und keine zusätzlichen Snippets werden gerendert. +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 ======================= -Eine völlig andere Lösung vermeidet dynamische Snippets. Der Trick besteht darin, die gesamte Logik in eine separate Komponente zu übertragen - um die Eingabe von Bewertungen kümmert sich von nun an nicht mehr der Presenter, sondern eine dedizierte `LikeControl`. Die Klasse wird wie folgt aussehen (außerdem wird sie auch Methoden `render`, `handleUnlike` usw. enthalten): +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 @@ -139,14 +142,14 @@ Template der Komponente: ```latte {snippet} {if !$article->liked} - <a n:href="like!" class=ajax>Gefällt mir</a> + <a n:href="like!" class=ajax>gefällt mir</a> {else} - <a n:href="unlike!" class=ajax>Gefällt mir nicht mehr</a> + <a n:href="unlike!" class=ajax>gefällt mir nicht mehr</a> {/if} {/snippet} ``` -Natürlich ändert sich unser View-Template und wir müssen eine Factory-Methode zum Presenter hinzufügen. Da wir die Komponente so oft erstellen, wie wir Artikel aus der Datenbank abrufen, verwenden wir zu ihrer "Vervielfältigung" die Klasse [application:Multiplier]. +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() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -Das View-Template wird auf das notwendige Minimum reduziert (und völlig frei von Snippets!): +Das Template des Views schrumpft auf das nötige Minimum (und ist völlig frei von Snippets!): ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ Das View-Template wird auf das notwendige Minimum reduziert (und völlig frei vo </article> ``` -Wir sind fast fertig: Die Anwendung wird nun AJAX-fähig funktionieren. Auch hier müssen wir die Anwendung optimieren, da aufgrund der Verwendung von Nette Database bei der Signalverarbeitung unnötigerweise alle Artikel aus der Datenbank anstelle von nur einem geladen werden. Der Vorteil ist jedoch, dass es nicht zu deren Rendern kommt, da tatsächlich nur unsere Komponente gerendert wird. +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/editors-and-tools.texy b/best-practices/de/editors-and-tools.texy deleted file mode 100644 index bda0c9c7fb..0000000000 --- a/best-practices/de/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editoren & Werkzeuge -******************** - -.[perex] -Sie können ein geschickter Programmierer sein, aber erst mit guten Werkzeugen werden Sie zum Meister. In diesem Kapitel finden Sie Tipps zu wichtigen Werkzeugen, Editoren und Plugins. - - -IDE-Editor -========== - -Wir empfehlen dringend, für die Entwicklung eine vollwertige IDE wie PhpStorm, NetBeans, VS Code zu verwenden und nicht nur einen Texteditor mit PHP-Unterstützung. Der Unterschied ist wirklich grundlegend. Es gibt keinen Grund, sich mit einem reinen Editor zufrieden zu geben, der zwar Syntax hervorheben kann, aber nicht die Möglichkeiten einer Spitzen-IDE erreicht, die präzise Vorschläge macht, Fehler überwacht, Code refaktorieren kann und vieles mehr. Einige IDEs sind kostenpflichtig, andere sogar kostenlos. - -**NetBeans IDE** hat bereits integrierte Unterstützung für Nette, Latte und NEON. - -**PhpStorm**: Installieren Sie diese Plugins unter `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: Suchen Sie im Marketplace nach dem Plugin "Nette Latte + Neon". - -Verbinden Sie auch Tracy mit dem Editor. Bei der Anzeige einer Fehlerseite können Sie dann auf Dateinamen klicken und diese werden im Editor mit dem Cursor an der entsprechenden Zeile geöffnet. Lesen Sie, [wie das System konfiguriert wird|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan ist ein Werkzeug, das logische Fehler im Code aufdeckt, bevor Sie ihn ausführen. - -Wir installieren es mit Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Wir erstellen im Projekt eine Konfigurationsdatei `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Und lassen es anschließend die Klassen im Ordner `app/` analysieren: - -```shell -vendor/bin/phpstan analyse app -``` - -Eine ausführliche Dokumentation finden Sie direkt auf den [PHPStan-Seiten |https://phpstan.org]. - - -Code Checker -============ - -Der [Code Checker|code-checker:] überprüft und korrigiert gegebenenfalls einige formale Fehler in Ihren Quellcodes: - -- entfernt [BOM |nette:glossary#BOM] -- überprüft die Gültigkeit von [Latte |latte:]-Templates -- überprüft die Gültigkeit von `.neon`-, `.php`- und `.json`-Dateien -- überprüft das Vorkommen von [Steuerzeichen |nette:glossary#Steuerzeichen] -- überprüft, ob die Datei in UTF-8 kodiert ist -- überprüft falsch geschriebene `/* @annotation */` (Stern fehlt) -- entfernt abschließende `?>` bei PHP-Dateien -- entfernt Leerzeichen am Zeilenende und unnötige Zeilen am Ende der Datei -- normalisiert Zeilentrenner auf Systemstandard (wenn Sie die Option `-l` angeben) - - -Composer -======== - -[Composer] ist ein Werkzeug zur Verwaltung von Abhängigkeiten in PHP. Es ermöglicht uns, beliebig komplexe Abhängigkeiten einzelner Bibliotheken zu deklarieren und diese dann für uns in unser Projekt zu installieren. - - -Requirements Checker -==================== - -Dies war ein Werkzeug, das die Laufzeitumgebung des Servers testete und informierte, ob (und inwieweit) das Framework verwendet werden kann. Derzeit kann Nette auf jedem Server verwendet werden, der die minimal erforderliche PHP-Version hat. diff --git a/best-practices/de/form-reuse.texy b/best-practices/de/form-reuse.texy index 0da82ed0e4..bbe84b7eff 100644 --- a/best-practices/de/form-reuse.texy +++ b/best-practices/de/form-reuse.texy @@ -2,15 +2,15 @@ Wiederverwendung von Formularen an mehreren Stellen *************************************************** .[perex] -In Nette haben Sie mehrere Möglichkeiten, dasselbe Formular an mehreren Stellen zu verwenden und Code nicht zu duplizieren. In diesem Artikel zeigen wir Ihnen verschiedene Lösungen, einschließlich solcher, die Sie vermeiden sollten. +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 ================ -Eine der grundlegenden Herangehensweisen, dieselbe Komponente an mehreren Stellen zu verwenden, ist die Erstellung einer Methode oder Klasse, die diese Komponente generiert, und der anschließende Aufruf dieser Methode an verschiedenen Stellen der Anwendung. Eine solche Methode oder Klasse wird *Factory* genannt. Bitte nicht verwechseln mit dem Entwurfsmuster *Factory Method*, das eine spezifische Verwendung von Factories beschreibt und nichts mit diesem Thema zu tun hat. +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 erstellt: +Als Beispiel erstellen wir eine Factory, die ein Bearbeitungsformular zusammensetzt: ```php use Nette\Application\UI\Form; @@ -22,13 +22,13 @@ class FormFactory $form = new Form; $form->addText('title', 'Titel:'); // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Senden'); + $form->addSubmit('send', 'Speichern'); return $form; } } ``` -Nun können Sie diese Factory an verschiedenen Stellen in Ihrer Anwendung verwenden, beispielsweise in Presentern oder Komponenten. Und zwar, indem Sie sie [als Abhängigkeit anfordern|dependency-injection:passing-dependencies]. Zuerst schreiben wir also die Klasse in die Konfigurationsdatei: +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: @@ -50,14 +50,14 @@ class MyPresenter extends Nette\Application\UI\Presenter { $form = $this->formFactory->createEditForm(); $form->onSuccess[] = function () { - // Verarbeitung der gesendeten Daten + // Verarbeitung der übermittelten Daten }; return $form; } } ``` -Sie können die Formular-Factory um weitere Methoden zur Erstellung anderer Formulartypen erweitern, je nach Bedarf Ihrer Anwendung. Und natürlich können wir auch eine Methode hinzufügen, die ein Basisformular ohne Elemente erstellt, und diese werden die anderen Methoden nutzen: +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 @@ -73,19 +73,19 @@ class FormFactory $form = $this->createForm(); $form->addText('title', 'Titel:'); // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Senden'); + $form->addSubmit('send', 'Speichern'); return $form; } } ``` -Die Methode `createForm()` tut bisher nichts Nützliches, aber das wird sich schnell ändern. +Die Methode `createForm()` tut noch nichts Nützliches, aber das ändert sich gleich. Abhängigkeiten der Factory ========================== -Mit der Zeit stellt sich heraus, dass wir Formulare mehrsprachig benötigen. Das bedeutet, dass wir allen Formularen einen sogenannten [Translator |forms:rendering#Übersetzung] zuweisen müssen. Zu diesem Zweck passen wir die Klasse `FormFactory` so an, dass sie das `Translator`-Objekt als Abhängigkeit im Konstruktor akzeptiert und es an das Formular übergibt: +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; @@ -108,13 +108,13 @@ class FormFactory } ``` -Da die Methode `createForm()` auch von anderen Methoden aufgerufen wird, die spezifische Formulare erstellen, genügt es, den Translator nur in ihr zu setzen. Und wir sind fertig. Es ist nicht nötig, den Code irgendeines Presenters oder einer Komponente zu ändern, was großartig ist. +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 mehrere Klassen für jedes Formular erstellen, das Sie in Ihrer Anwendung verwenden möchten. Dieser Ansatz kann die Lesbarkeit des Codes erhöhen und die Verwaltung von Formularen erleichtern. Die ursprüngliche `FormFactory` lassen wir nur ein reines Formular mit Grundkonfiguration erstellen (z.B. mit Übersetzungsunterstützung) und für das Bearbeitungsformular erstellen wir eine neue Factory `EditFormFactory`. +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 @@ -145,16 +145,16 @@ class EditFormFactory { $form = $this->formFactory->create(); // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Senden'); + $form->addSubmit('send', 'Speichern'); return $form; } } ``` -Sehr wichtig ist, dass die Bindung zwischen den Klassen `FormFactory` und `EditFormFactory` durch [Komposition |nette:introduction-to-object-oriented-programming#Komposition] realisiert wird, nicht durch [objektorientierte Vererbung |nette:introduction-to-object-oriented-programming#Vererbung]: +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 -// ⛔ SO NICHT! HIER GEHÖRT KEINE VERERBUNG HIN +// ⛔ NEIN! HIER GEHÖRT KEINE VERERBUNG HIN class EditFormFactory extends FormFactory { public function create(): Form @@ -162,21 +162,21 @@ class EditFormFactory extends FormFactory $form = parent::create(); $form->addText('title', 'Titel:'); // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Senden'); + $form->addSubmit('send', 'Speichern'); return $form; } } ``` -Die Verwendung von Vererbung wäre in diesem Fall völlig kontraproduktiv. Sie würden sehr schnell auf Probleme stoßen. Zum Beispiel, wenn Sie der Methode `create()` Parameter hinzufügen möchten; PHP würde einen Fehler melden, dass ihre Signatur von der der Elternklasse abweicht. Oder bei der Übergabe von Abhängigkeiten an die Klasse `EditFormFactory` über den Konstruktor. Es würde eine Situation entstehen, die wir [Constructor Hell |dependency-injection:passing-dependencies#Constructor Hell] nennen. +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. -Im Allgemeinen ist es besser, [Komposition der Vererbung vorzuziehen |dependency-injection:faq#Warum wird Komposition der Vererbung vorgezogen]. +Generell ist es besser, [Komposition der Vererbung vorzuziehen |dependency-injection:faq#Warum ist Komposition der Vererbung vorzuziehen?]. Formularverarbeitung ==================== -Die Verarbeitung des Formulars, die nach erfolgreichem Absenden aufgerufen wird, kann auch Teil der Factory-Klasse sein. Sie wird so funktionieren, dass sie die gesendeten Daten zur Verarbeitung an das Modell übergibt. Eventuelle Fehler [gibt sie zurück |forms:validation#Fehler bei der Verarbeitung] an das Formular. Das Modell wird im folgenden Beispiel durch die Klasse `Facade` repräsentiert: +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 @@ -192,15 +192,15 @@ class EditFormFactory $form = $this->formFactory->create(); $form->addText('title', 'Titel:'); // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Senden'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addSubmit('send', 'Speichern'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { - // Verarbeitung der gesendeten Daten + // Verarbeitung der übermittelten Daten $this->facade->process($data); } catch (AnyModelException $e) { @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Die eigentliche Weiterleitung überlassen wir jedoch dem Presenter. Dieser fügt dem `onSuccess`-Ereignis einen weiteren Handler hinzu, der die Weiterleitung durchführt. Dadurch wird es möglich, das Formular in verschiedenen Presentern zu verwenden und in jedem woandershin weiterzuleiten. +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 @@ -232,16 +232,16 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Diese Lösung nutzt die Eigenschaft von Formularen, dass, wenn `addError()` über dem Formular oder einem seiner Elemente aufgerufen wird, der nächste `onSuccess`-Handler nicht mehr aufgerufen wird. +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 ============================= -Das erstellte Formular soll kein Nachkomme der Klasse `Form` sein. Mit anderen Worten, verwenden Sie nicht diese Lösung: +Ein zusammengesetztes Formular soll kein Nachkomme der Klasse `Form` sein. Mit anderen Worten: Verwenden Sie diese Lösung nicht: ```php -// ⛔ SO NICHT! HIER GEHÖRT KEINE VERERBUNG HIN +// ⛔ NEIN! HIER GEHÖRT KEINE VERERBUNG HIN class EditForm extends Form { public function __construct(Translator $translator) @@ -249,21 +249,21 @@ class EditForm extends Form parent::__construct(); $this->addText('title', 'Titel:'); // hier werden weitere Formularfelder hinzugefügt - $this->addSubmit('send', 'Senden'); + $this->addSubmit('send', 'Speichern'); $this->setTranslator($translator); } } ``` -Anstatt das Formular im Konstruktor zu erstellen, verwenden Sie eine Factory. +Statt das Formular im Konstruktor zusammenzusetzen, verwenden Sie eine Factory. -Es ist wichtig zu erkennen, dass die Klasse `Form` in erster Linie ein Werkzeug zur Erstellung eines Formulars ist, also ein *Form Builder*. Und das erstellte Formular kann als ihr Produkt betrachtet werden. Aber ein Produkt ist kein spezifischer Fall eines Builders, es gibt keine *is a*-Beziehung zwischen ihnen, die die Grundlage der Vererbung bildet. +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 ======================= -Ein völlig anderer Ansatz ist die Erstellung einer [Komponente|application:components], die ein Formular enthält. Dies eröffnet neue Möglichkeiten, zum Beispiel das Formular auf spezifische Weise zu rendern, da die Komponente auch ein Template enthält. Oder man kann Signale für die AJAX-Kommunikation und das Nachladen von Informationen in das Formular nutzen, zum Beispiel für Vorschläge usw. +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 @@ -283,16 +283,16 @@ class EditControl extends Nette\Application\UI\Control $form = new Form; $form->addText('title', 'Titel:'); // hier werden weitere Formularfelder hinzugefügt - $form->addSubmit('send', 'Senden'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addSubmit('send', 'Speichern'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { - // Verarbeitung der gesendeten Daten + // Verarbeitung der übermittelten Daten $this->facade->process($data); } catch (AnyModelException $e) { @@ -300,13 +300,13 @@ class EditControl extends Nette\Application\UI\Control return; } - // Ereignis auslösen + // Auslösen des Events $this->onSave($this, $data); } } ``` -Wir erstellen noch eine Factory, die diese Komponente herstellt. Es genügt, [ihr Interface zu definieren |application:components#Komponenten mit Abhängigkeiten]: +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 @@ -315,7 +315,7 @@ interface EditControlFactory } ``` -Und in die Konfigurationsdatei hinzufügen: +Und es der Konfigurationsdatei hinzuzufügen: ```neon services: @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // oder wir leiten zum Ergebnis der Bearbeitung weiter, z.B.: + // oder wir leiten zum Ergebnis der Bearbeitung weiter, z. B.: // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/de/inject-method-attribute.texy b/best-practices/de/inject-method-attribute.texy index 45788eca01..7e60e09447 100644 --- a/best-practices/de/inject-method-attribute.texy +++ b/best-practices/de/inject-method-attribute.texy @@ -1,18 +1,18 @@ -Methoden und Attribute inject -***************************** +Inject-Methoden und -Attribute +****************************** .[perex] -In diesem Artikel konzentrieren wir uns auf verschiedene Methoden zur Übergabe von Abhängigkeiten an Presenter im Nette Framework. Wir vergleichen die bevorzugte Methode, nämlich den Konstruktor, mit anderen Möglichkeiten wie `inject`-Methoden und -Attributen. +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 per Konstruktor] der bevorzugte Weg ist. Wenn Sie jedoch einen gemeinsamen Vorfahren erstellen, von dem andere Presenter erben (z. B. `BasePresenter`), und dieser Vorfahre ebenfalls Abhängigkeiten hat, tritt ein Problem auf, das wir [Constructor Hell |dependency-injection:passing-dependencies#Constructor Hell] nennen. Dies kann durch alternative Wege umgangen werden, die `inject`-Methoden und -Attribute (früher Annotationen) darstellen. +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 ==================== -Dies ist eine Form der Abhängigkeitsübergabe per [Setter |dependency-injection:passing-dependencies#Übergabe per Setter]. Der Name dieser Setter beginnt mit dem Präfix `inject`. Nette DI ruft solche benannten Methoden automatisch sofort nach der Erstellung der Presenter-Instanz auf und übergibt ihnen alle erforderlichen Abhängigkeiten. Sie müssen daher als `public` deklariert sein. +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. -`inject*()`-Methoden können als eine Art Erweiterung des Konstruktors auf mehrere Methoden betrachtet werden. Dadurch kann `BasePresenter` Abhängigkeiten über eine andere Methode übernehmen und den Konstruktor für seine Nachkommen frei lassen: +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 @@ -36,15 +36,15 @@ class MyPresenter extends BasePresenter } ``` -Ein Presenter kann beliebig viele `inject*()`-Methoden enthalten, und jede kann beliebig viele Parameter haben. Sie eignen sich auch hervorragend in Fällen, in denen der Presenter [aus Traits zusammengesetzt ist |presenter-traits] und jede von ihnen ihre eigene Abhängigkeit benötigt. +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 ================== -Dies ist eine Form der [Injection in eine Eigenschaft |dependency-injection:passing-dependencies#Zuweisung zu einer Variablen]. Es genügt, zu markieren, in welche Eigenschaften injiziert werden soll, und Nette DI übergibt die Abhängigkeiten automatisch sofort nach der Erstellung der Presenter-Instanz. Damit sie eingefügt werden können, müssen sie als `public` deklariert sein. +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. -Eigenschaften markieren wir mit einem Attribut: (früher wurde die Annotation `/** @inject */` verwendet) +Die Properties kennzeichnen wir mit einem Attribut: (früher wurde die Annotation `/** @inject */` verwendet) ```php use Nette\DI\Attributes\Inject; // diese Zeile ist wichtig @@ -56,6 +56,6 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -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-kompletter-ueberblick-ueber-neuerungen#toc-constructor-property-promotion] erscheint es jedoch einfacher, den Konstruktor zu verwenden. +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. -Im Gegenteil leidet diese Methode unter denselben Nachteilen wie die Übergabe von Abhängigkeiten an Eigenschaften im Allgemeinen: Wir haben keine Kontrolle über Änderungen in der Variablen, und gleichzeitig wird die Variable Teil der öffentlichen Schnittstelle der Klasse, was unerwünscht ist. +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 index 998313e20f..02c59866d7 100644 --- a/best-practices/de/lets-create-contact-form.texy +++ b/best-practices/de/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -Wir erstellen ein Kontaktformular +Erstellen wir ein Kontaktformular ********************************* .[perex] -Wir schauen uns an, wie man in Nette ein Kontaktformular erstellt, einschließlich des E-Mail-Versands. Los geht's! +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 erstellen. Wie das geht, erklärt die Seite [Erste Schritte |nette:installation]. Und dann können wir mit der Erstellung des Formulars beginnen. +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 die Erstellung eines [Formulars direkt im Presenter |forms:in-presenter]. Wir können den vorbereiteten `HomePresenter` verwenden. In ihn fügen wir die Komponente `contactForm` hinzu, die das Formular darstellt. Dazu schreiben wir eine Factory-Methode `createComponentContactForm()` in den Code, die die Komponente erstellt: +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; @@ -18,24 +18,24 @@ class HomePresenter extends Presenter { $form = new Form; $form->addText('name', 'Name:') - ->setRequired('Geben Sie einen Namen ein'); + ->setRequired('Bitte geben Sie Ihren Namen ein'); $form->addEmail('email', 'E-Mail:') - ->setRequired('Geben Sie eine E-Mail-Adresse ein'); - $form->addTextarea('message', 'Nachricht:') - ->setRequired('Geben Sie eine Nachricht ein'); + ->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']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { - // E-Mail senden + // Versand der E-Mail } } ``` -Wie Sie sehen, haben wir zwei Methoden erstellt. Die erste Methode `createComponentContactForm()` erstellt ein neues Formular. Dieses hat Felder für Name, E-Mail und Nachricht, die wir mit den Methoden `addText()`, `addEmail()` und `addTextArea()` hinzufügen. Wir haben auch einen Button zum Absenden des Formulars hinzugefügt. Aber was, wenn der Benutzer ein Feld nicht ausfüllt? 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 auch das [Ereignis |nette:glossary#Events Ereignisse] `onSuccess` hinzugefügt, das ausgelöst wird, wenn das Formular erfolgreich gesendet wird. In unserem Fall ruft es die Methode `contactFormSucceeded` auf, die sich um die Verarbeitung des gesendeten Formulars kümmert. Das werden wir gleich in den Code einfügen. +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: @@ -45,12 +45,9 @@ Die Komponente `contactForm` lassen wir im Template `Home/default.latte` rendern {control contactForm} ``` -Für den eigentlichen E-Mail-Versand erstellen wir eine neue Klasse, die wir `ContactFacade` nennen und in der Datei `app/Model/ContactFacade.php` ablegen: +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 -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -66,9 +63,9 @@ class ContactFacade public function sendMessage(string $email, string $name, string $message): void { $mail = new Message; - $mail->addTo('admin@example.com') // Ihre E-Mail + $mail->addTo('admin@example.com') // Ihre E-Mail-Adresse ->setFrom($email, $name) - ->setSubject('Nachricht vom Kontaktformular') + ->setSubject('Nachricht aus dem Kontaktformular') ->setBody($message); $this->mailer->send($mail); @@ -76,9 +73,9 @@ class ContactFacade } ``` -Die Methode `sendMessage()` erstellt und sendet eine E-Mail. Sie verwendet dazu einen sogenannten Mailer, den sie als Abhängigkeit über den Konstruktor erhält. Lesen Sie mehr über das [Senden von E-Mails |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()`. Diese ruft die Methode `sendMessage()` der Klasse `ContactFacade` auf und übergibt ihr die Daten aus dem Formular. Und wie erhalten wir das Objekt `ContactFacade`? Wir lassen es uns über den Konstruktor übergeben: +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; @@ -106,20 +103,20 @@ class HomePresenter extends Presenter } ``` -Nachdem die E-Mail gesendet wurde, zeigen wir dem Benutzer noch eine sogenannte [Flash-Nachricht |application:components#Flash-Nachrichten] an, die bestätigt, dass die Nachricht gesendet wurde, und leiten dann auf dieselbe Seite weiter (`this`), damit das Formular nicht durch *Aktualisieren* im Browser erneut gesendet werden kann. +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 funktioniert, sollten Sie in der Lage sein, eine E-Mail von Ihrem Kontaktformular zu senden. Herzlichen Glückwunsch! +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 gesendet, die nur die über das Formular gesendete Nachricht enthält. In der E-Mail können wir jedoch HTML verwenden und ihr Erscheinungsbild attraktiver gestalten. Wir erstellen dafür ein Template in Latte, das wir in `app/Model/contactEmail.latte` speichern: +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 vom Kontaktformular + Nachricht aus dem Kontaktformular

    Name: {$name}

    @@ -129,7 +126,7 @@ Bisher wird eine einfache Text-E-Mail gesendet, die nur die über das Formular g ``` -Es bleibt übrig, `ContactFacade` anzupassen, damit dieses Template verwendet wird. Im Konstruktor fordern wir die Klasse `LatteFactory` an, die ein Objekt `Latte\Engine`, also den [Latte-Template-Renderer |latte:develop#Wie rendert man ein Template], erstellen kann. Mit der Methode `renderToString()` rendern wir das Template in einen String, der erste Parameter ist der Pfad zum Template und der zweite sind die Variablen. +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; @@ -156,7 +153,7 @@ class ContactFacade ]); $mail = new Message; - $mail->addTo('admin@example.com') // Ihre E-Mail + $mail->addTo('admin@example.com') // Ihre E-Mail-Adresse ->setFrom($email, $name) ->setHtmlBody($body); @@ -165,15 +162,15 @@ class ContactFacade } ``` -Die generierte HTML-E-Mail übergeben wir dann der Methode `setHtmlBody()` anstelle der ursprünglichen `setBody()`. Ebenso müssen wir den Betreff der E-Mail nicht in `setSubject()` angeben, da ihn die Bibliothek aus dem ``-Element des Templates übernimmt. +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 `<title>` des Templates. Konfiguration ------------- -Im Code der Klasse `ContactFacade` ist immer noch unsere Administrator-E-Mail `admin@example.com` fest codiert. Es wäre besser, sie in die Konfigurationsdatei zu verschieben. Wie geht das? +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 String mit der E-Mail durch eine Variable, die über den Konstruktor übergeben wird: +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 @@ -197,21 +194,21 @@ class ContactFacade } ``` -Und der zweite Schritt ist die Angabe des Wertes dieser Variablen in der Konfiguration. In die Datei `app/config/services.neon` schreiben wir: +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 es viele Einträge im Abschnitt `services` gäbe und Sie das Gefühl hätten, dass die E-Mail dazwischen untergeht, können wir sie zu einer Variablen machen. Wir ändern die Notation auf: +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 diese Variable: +Und in der Datei `app/config/common.neon` definieren wir diesen Parameter: ```neon parameters: diff --git a/best-practices/de/microsites.texy b/best-practices/de/microsites.texy index 71eac0ad76..33045463e9 100644 --- a/best-practices/de/microsites.texy +++ b/best-practices/de/microsites.texy @@ -1,11 +1,11 @@ -Wie man Mikro-Websites schreibt -******************************* +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, schnell und ohne unnötige Komplikationen sein. Vielleicht denken Sie, dass Sie für ein so kleines Projekt kein robustes Framework benötigen. Aber was, wenn die Verwendung des Nette Frameworks diesen Prozess grundlegend vereinfachen und beschleunigen kann? +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? -Denn auch bei der Erstellung einfacher Websites möchten Sie nicht auf Komfort verzichten. Sie möchten nicht das neu erfinden, was bereits einmal gelöst wurde. Seien Sie ruhig faul und lassen Sie sich verwöhnen. Das Nette Framework kann auch hervorragend als Micro Framework genutzt werden. +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 der gesamte Code der Website in einer einzigen Datei `index.php` im öffentlichen Ordner platziert wird: +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 @@ -16,48 +16,48 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// DI-Container basierend auf der Konfiguration in config.neon erstellen +// erzeuge den DI-Container auf Basis der Konfiguration in config.neon $configurator->addConfig(__DIR__ . '/../app/config.neon'); $container = $configurator->createContainer(); -// Routing einstellen +// wir richten das Routing ein $router = new Nette\Application\Routers\RouteList; $container->addService('router', $router); -// Route für URL https://example.com/ +// Route für die URL https://example.com/ $router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // Browsersprache erkennen und auf URL /en oder /de usw. umleiten + // 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); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); -// Route für URL https://example.com/cs oder https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // entsprechende Vorlage anzeigen, z.B. ../templates/en.latte +// 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; }); -// Anwendung starten! +// starte die Anwendung! $container->getByType(Nette\Application\Application::class)->run(); ``` -Alles andere sind Templates, die im übergeordneten Ordner `/templates` gespeichert sind. +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 ausgeführt wird, wenn die entsprechende Seite geöffnet wird. +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 jemals [Tracy|tracy:] ausprobiert haben, können sich heute nicht mehr vorstellen, ohne sie etwas zu programmieren. -- Vor allem aber werden Sie das Templating-System [Latte|latte:] nutzen, denn schon ab 2 Seiten werden Sie [Layout und Inhalt|latte:template-inheritance] trennen wollen. -- Und Sie möchten sich auf jeden Fall auf das [automatische Escaping |latte:safety-first] verlassen, damit keine XSS-Schwachstelle entsteht. -- Nette stellt auch sicher, dass bei einem Fehler niemals PHP-Fehlermeldungen für Programmierer angezeigt werden, sondern eine für den Benutzer verständliche Seite. -- Wenn Sie Feedback von Benutzern erhalten möchten, zum Beispiel in Form eines Kontaktformulars, fügen Sie noch [Formulare|forms:] und eine [Datenbank|database:] hinzu. -- Ausgefüllte Formulare können Sie sich auch einfach [per E-Mail zusenden lassen|mail:]. -- Manchmal kann Ihnen [Caching|caching:] nützlich sein, zum Beispiel wenn Sie Feeds herunterladen und anzeigen. +- 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, die es Ihnen ermöglichen, Ergebnisse ohne unnötige Verzögerung zu erzielen. Das Nette Framework bietet Ihnen genau das - schnelle Entwicklung, Sicherheit und eine breite Palette von Werkzeugen wie Tracy und Latte, die den Prozess vereinfachen. Installieren Sie einfach ein paar Nette-Pakete und der Aufbau einer solchen Microsite ist plötzlich ein Kinderspiel. Und Sie wissen, dass sich nirgendwo eine Sicherheitslücke verbirgt. +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 index 17aabcacc3..f7eb8d6d15 100644 --- a/best-practices/de/pagination.texy +++ b/best-practices/de/pagination.texy @@ -2,9 +2,9 @@ Paginierung von Datenbankergebnissen ************************************ .[perex] -Bei der Erstellung von Webanwendungen stoßen Sie sehr oft auf die Anforderung, die Anzahl der angezeigten Elemente pro Seite zu begrenzen. +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. -Wir gehen von einem Zustand aus, in dem wir alle Daten ohne Paginierung auflisten. Für die Auswahl der Daten aus der Datenbank haben wir die Klasse `ArticleRepository`, die neben dem Konstruktor die Methode `findPublishedArticles` enthält, die alle veröffentlichten Artikel absteigend nach Veröffentlichungsdatum sortiert zurückgibt. +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; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -Im Presenter injizieren wir dann die Modellklasse und in der Render-Methode fordern wir die veröffentlichten Artikel an, die wir an das Template übergeben: +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; @@ -52,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -In der Vorlage `default.latte` kümmern wir uns dann um die Auflistung der Artikel: +Im Template `default.latte` kümmern wir uns dann um die Ausgabe der Artikel: ```latte {block content} @@ -67,11 +67,11 @@ In der Vorlage `default.latte` kümmern wir uns dann um die Auflistung der Artik ``` -Auf diese Weise können wir alle Artikel auflisten, was jedoch Probleme verursacht, sobald die Anzahl der Artikel steigt. In diesem Moment ist die Implementierung eines Paginierungsmechanismus sinnvoll. +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 sorgt dafür, dass alle Artikel auf mehrere Seiten aufgeteilt werden und wir nur die Artikel der aktuellen Seite anzeigen. Die Gesamtzahl der Seiten und die Aufteilung der Artikel berechnet der [utils:Paginator] selbst, basierend darauf, wie viele Artikel wir insgesamt haben und wie viele Artikel wir pro Seite anzeigen möchten. +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 uns nur Artikel für eine Seite zurückgeben kann. Wir fügen auch eine Methode hinzu, um die Gesamtzahl der Artikel in der Datenbank zu ermitteln, die wir zum Einrichten des Paginators benötigen: +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; @@ -108,9 +108,9 @@ class ArticleRepository } ``` -Anschließend widmen wir uns den Anpassungen des Presenters. An die Render-Methode übergeben wir die Nummer der aktuell angezeigten Seite. Für den Fall, dass diese Nummer nicht Teil der URL ist, legen wir den Standardwert auf die erste Seite fest. +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). -Weiterhin erweitern wir die Render-Methode um das Abrufen der Paginator-Instanz, deren Einrichtung und die Auswahl der richtigen Artikel zur Anzeige im Template. Der `HomePresenter` sieht nach den Anpassungen wie folgt aus: +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; @@ -130,24 +130,24 @@ class HomePresenter extends Nette\Application\UI\Presenter // Wir ermitteln die Gesamtzahl der veröffentlichten Artikel $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Wir erstellen eine Instanz des Paginators und richten sie ein + // Wir erzeugen eine Paginator-Instanz und konfigurieren sie $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // Gesamtzahl der Artikel - $paginator->setItemsPerPage(10); // Anzahl der Elemente pro Seite + $paginator->setItemCount($articlesCount); // Gesamtzahl der Einträge + $paginator->setItemsPerPage(10); // Einträge pro Seite $paginator->setPage($page); // Nummer der aktuellen Seite - // Aus der Datenbank ziehen wir eine begrenzte Menge von Artikeln gemäß der Berechnung des Paginators + // 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 die Vorlage übergeben + // die wir an das Template übergeben $this->template->articles = $articles; - // und auch den Paginator selbst zur Anzeige der Paginierungsoptionen + // und auch den Paginator selbst für die Anzeige der Paginierung $this->template->paginator = $paginator; } } ``` -Das Template iteriert nun bereits nur über die Artikel einer Seite, wir müssen lediglich die Paginierungslinks hinzufügen: +Das Template iteriert nun nur noch über die Artikel der aktuellen Seite. Wir müssen lediglich die Paginierungslinks hinzufügen: ```latte {block content} @@ -164,7 +164,7 @@ Das Template iteriert nun bereits nur über die Artikel einer Seite, wir müssen {if !$paginator->isFirst()} <a n:href="default, 1">Erste</a>  |  - <a n:href="default, $paginator->page-1">Vorherige</a> + <a n:href="default, $paginator->getPage() - 1">Vorherige</a>  |  {/if} @@ -180,9 +180,9 @@ Das Template iteriert nun bereits nur über die Artikel einer Seite, wir müssen ``` -So haben wir die Seite um die Möglichkeit der Paginierung mit dem Paginator ergänzt. Falls wir anstelle von [Nette Database Core |database:sql-way] als Datenbankschicht [Nette Database Explorer |database:explorer] verwenden, können wir die Paginierung auch ohne Paginator implementieren. Die Klasse `Nette\Database\Table\Selection` enthält nämlich die Methode [page() |api:Nette\Database\Table\Selection::page()] mit der vom Paginator übernommenen Paginierungslogik. +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 Implementierungsmethode wie folgt aus: +Das Repository sieht bei dieser Art der Implementierung so aus: ```php namespace App\Model; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -Im Presenter müssen wir keinen Paginator erstellen, wir verwenden stattdessen die Methode der `Selection`-Klasse, die uns das Repository zurückgibt: +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; @@ -222,21 +222,21 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Wir ziehen die veröffentlichten Artikel heraus + // Wir holen die veröffentlichten Artikel $articles = $this->articleRepository->findPublishedArticles(); - // und senden nur einen Teil davon an die Vorlage, begrenzt durch die Berechnung der page-Methode + // 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 auch die notwendigen Daten zur Anzeige der Paginierungsoptionen + // und außerdem die nötigen Daten für die Anzeige der Paginierung $this->template->page = $page; $this->template->lastPage = $lastPage; } } ``` -Da wir nun keinen Paginator an das Template senden, passen wir den Teil an, der die Paginierungslinks anzeigt: +Da wir dem Template jetzt kein Paginator-Objekt mehr übergeben, passen wir den Teil an, der die Paginierungslinks anzeigt: ```latte {block content} @@ -268,6 +268,6 @@ Da wir nun keinen Paginator an das Template senden, passen wir den Teil an, der </div> ``` -Auf diese Weise haben wir den Paginierungsmechanismus ohne Verwendung des Paginators implementiert. +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 index d84c9fb780..561fe9e506 100644 --- a/best-practices/de/passing-settings-to-presenters.texy +++ b/best-practices/de/passing-settings-to-presenters.texy @@ -2,9 +2,9 @@ Einstellungen an Presenter übergeben ************************************ .[perex] -Müssen Sie Argumente an Presenter übergeben, die keine Objekte sind (z. B. Information, ob im Debug-Modus ausgeführt wird, Pfade zu Verzeichnissen usw.) und daher nicht automatisch über Autowiring übergeben werden können? Die Lösung besteht darin, sie in ein `Settings`-Objekt zu kapseln. +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 `Settings`-Dienst stellt eine sehr einfache und dennoch nützliche Methode dar, um Informationen über die laufende Anwendung an Presenter bereitzustellen. Seine konkrete Form hängt ganz von Ihren spezifischen Bedürfnissen ab. Beispiel: +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; @@ -12,7 +12,7 @@ namespace App; class Settings { public function __construct( - // ab PHP 8.1 kann readonly angegeben werden + // seit PHP 8.1 kann readonly angegeben werden public bool $debugMode, public string $appDir, // und so weiter @@ -30,7 +30,7 @@ services: ) ``` -Wenn der Presenter die von diesem Dienst bereitgestellten Informationen benötigt, fordert er sie einfach im Konstruktor an: +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 diff --git a/best-practices/de/post-links.texy b/best-practices/de/post-links.texy index f4f90e1f24..8f654ea3e5 100644 --- a/best-practices/de/post-links.texy +++ b/best-practices/de/post-links.texy @@ -2,29 +2,29 @@ Wie man POST-Links richtig verwendet ************************************ .[perex] -In Webanwendungen, insbesondere in administrativen Oberflächen, sollte es eine Grundregel sein, dass Aktionen, die den Serverzustand ändern, nicht über die HTTP-Methode GET durchgeführt werden sollten. Wie der Name der Methode andeutet, sollte GET nur zum Abrufen von Daten verwendet werden, nicht zu deren Änderung. Für Aktionen wie das Löschen von Datensätzen ist es besser, die POST-Methode zu verwenden. Obwohl die DELETE-Methode ideal wäre, kann sie ohne JavaScript nicht aufgerufen werden, daher wird historisch POST verwendet. +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. Am Anfang des Templates erstellen Sie ein Hilfsformular mit dem Bezeichner `postForm`, das Sie anschließend für die Löschbuttons verwenden: +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 anstelle eines klassischen Links `<a>` einen Button `<button>` verwenden, der visuell so angepasst werden kann, dass er wie ein normaler Link aussieht. Beispielsweise bietet das CSS-Framework Bootstrap die Klassen `btn btn-link`, mit denen Sie erreichen, dass der Button visuell nicht von anderen Links zu unterscheiden ist. Mit dem Attribut `form="postForm"` verknüpfen wir ihn mit dem vorbereiteten Formular: +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}">löschen</button> - <!-- anstelle von <a n:href="delete $post->id">löschen</a> --> + <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 Klicken auf den Link wird nun die Aktion `delete` aufgerufen. Um sicherzustellen, dass Anfragen nur über die POST-Methode und von derselben Domain akzeptiert werden (was eine wirksame Verteidigung gegen CSRF-Angriffe ist), verwenden Sie das Attribut `#[Requires]`: +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; @@ -34,15 +34,15 @@ 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 des Datensatzes + $this->facade->deletePost($id); // hypothetischer Code zum Löschen eines Datensatzes $this->redirect('default'); } } ``` -Das Attribut existiert seit Nette Application 3.2 und mehr über seine Möglichkeiten erfahren Sie auf der Seite [Wie man das Attribut #Requires verwendet |attribute-requires]. +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 anstelle der Aktion `actionDelete()` das Signal `handleDelete()` verwenden würden, ist es nicht notwendig, `sameOrigin: true` anzugeben, da Signale diesen Schutz implizit eingestellt haben: +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')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -Dieser Ansatz verbessert nicht nur die Sicherheit Ihrer Anwendung, sondern trägt auch zur Einhaltung korrekter Webstandards und Praktiken bei. Durch die Verwendung von POST-Methoden für zustandsändernde Aktionen erreichen Sie eine robustere und sicherere Anwendung. +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 index e6cf4290dc..442522895b 100644 --- a/best-practices/de/presenter-traits.texy +++ b/best-practices/de/presenter-traits.texy @@ -2,13 +2,13 @@ Presenter aus Traits zusammensetzen *********************************** .[perex] -Wenn wir in mehreren Presentern denselben Code implementieren müssen (z. B. die Überprüfung, ob der Benutzer angemeldet ist), bietet es sich an, den Code in einem gemeinsamen Vorfahren zu platzieren. Die zweite Möglichkeit ist die Erstellung von zweckgebundenen [Traits |nette:introduction-to-object-oriented-programming#Traits]. +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 der Presenter genau die Traits verwenden kann, die er tatsächlich benötigt, während Mehrfachvererbung in PHP nicht möglich ist. +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 die Tatsache nutzen, dass bei der Erstellung des Presenters nacheinander alle [inject-Methoden |inject-method-attribute#inject -Methoden] aufgerufen werden. Es muss nur sichergestellt werden, dass der Name jeder inject-Methode eindeutig 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 Ereignisse [onStartup oder onRender |application:presenters#Ereignisse] anhängen. +Traits können Initialisierungscode an die Events [onStartup oder onRender |application:presenters#Events] hängen. Beispiele: diff --git a/best-practices/de/pretty-urls.texy b/best-practices/de/pretty-urls.texy new file mode 100644 index 0000000000..9cde1971e6 --- /dev/null +++ b/best-practices/de/pretty-urls.texy @@ -0,0 +1,204 @@ +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 index 957f7c6605..9816cb9cfc 100644 --- a/best-practices/de/restore-request.texy +++ b/best-practices/de/restore-request.texy @@ -2,15 +2,15 @@ Wie man zu einer früheren Seite zurückkehrt? ******************************************** .[perex] -Was, wenn ein Benutzer ein Formular ausfüllt und seine Anmeldung abläuft? Damit er die Daten nicht verliert, speichern wir sie vor der Weiterleitung zur Anmeldeseite in der Session. In Nette ist das ein Kinderspiel. +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. -Die aktuelle Anfrage kann mit der Methode `storeRequest()` in der Session gespeichert werden, die ihren Bezeichner in Form einer kurzen Zeichenkette zurückgibt. Die Methode speichert den Namen des aktuellen Presenters, die View und ihre Parameter. Falls auch ein Formular gesendet wurde, wird auch der Inhalt der Felder gespeichert (mit Ausnahme von hochgeladenen Dateien). +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 der Anfrage erfolgt durch die Methode `restoreRequest($key)`, der wir den erhaltenen Bezeichner übergeben. Diese leitet zum ursprünglichen Presenter und zur View weiter. Wenn die gespeicherte Anfrage jedoch das Senden eines Formulars enthält, wechselt sie zum ursprünglichen Presenter mit der Methode `forward()`, übergibt dem Formular die zuvor ausgefüllten Werte und lässt es erneut rendern. Der Benutzer hat so die Möglichkeit, das Formular erneut zu senden, und es gehen keine Daten verloren. +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()` überprüft, ob der neu angemeldete Benutzer derselbe ist, der das Formular ursprünglich ausgefüllt hat. Wenn nicht, verwirft sie die Anfrage und tut nichts. +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 alles an einem Beispiel. Wir haben einen Presenter `AdminPresenter`, in dem Daten bearbeitet werden und in dessen Methode `startup()` wir überprüfen, ob der Benutzer angemeldet ist. Wenn nicht, leiten wir ihn zum `SignPresenter` weiter. Gleichzeitig speichern wir die aktuelle Anfrage und senden ihren Schlüssel an den `SignPresenter`. +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 @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Der Presenter `SignPresenter` enthält neben dem Anmeldeformular auch einen persistenten Parameter `$backlink`, in den der Schlüssel geschrieben wird. Da der Parameter persistent ist, wird er auch nach dem Absenden des Anmeldeformulars übertragen. +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 @@ -40,14 +40,14 @@ class SignPresenter extends Nette\Application\UI\Presenter protected function createComponentSignInForm() { $form = new Nette\Application\UI\Form; - // ... Formularfelder hinzufügen ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + // ... wir fügen die Formularfelder hinzu ... + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { - // ... hier den Benutzer anmelden ... + // ... hier melden wir den Benutzer an ... $this->restoreRequest($this->backlink); $this->redirect('Admin:'); @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -Wir übergeben der Methode `restoreRequest()` den Schlüssel der gespeicherten Anfrage, und sie leitet (oder wechselt per `forward()`) zum ursprünglichen Presenter weiter. +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). -Wenn der Schlüssel jedoch ungültig ist (z. B. nicht mehr in der Session existiert oder der Benutzer nicht übereinstimmt), tut die Methode nichts. Es folgt also der Aufruf `$this->redirect('Admin:')`, der zum `AdminPresenter` (oder einer anderen Standardseite nach dem Login) weiterleitet. +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/el/@home.texy b/best-practices/el/@home.texy deleted file mode 100644 index 0e8088a224..0000000000 --- a/best-practices/el/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Οδηγοί και διαδικασίες -********************** - -.[perex] -Οδηγοί, λύσεις για συχνές εργασίες και *βέλτιστες πρακτικές* για το Nette. - - -<div class=documentation> -<div> - - -Εφαρμογές Nette ---------------- -- [Μέθοδοι και χαρακτηριστικά inject |inject-method-attribute] -- [Σύνθεση presenters από traits |presenter-traits] -- [Πέρασμα ρυθμίσεων σε presenters |passing-settings-to-presenters] -- [Πώς να επιστρέψετε σε προηγούμενη σελίδα |restore-request] -- [Σελίδωση αποτελεσμάτων βάσης δεδομένων |pagination] -- [Δυναμικά snippets |dynamic-snippets] -- [Πώς να χρησιμοποιήσετε το attribute #Requires |attribute-requires] -- [Πώς να χρησιμοποιήσετε σωστά τους συνδέσμους POST |post-links] - -</div> -<div> - - -Φόρμες ------- -- [Επαναχρησιμοποίηση φορμών |form-reuse] -- [Φόρμα για δημιουργία και επεξεργασία εγγραφής |creating-editing-form] -- [Δημιουργούμε φόρμα επικοινωνίας |lets-create-contact-form] -- [Εξαρτώμενα selectboxes |https://blog.nette.org/el/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Γενικά ------- -- [Πώς να φορτώσετε ένα αρχείο διαμόρφωσης |bootstrap:] -- [Πώς να γράψετε micro-sites |microsites] -- [Γιατί το Nette χρησιμοποιεί τη σημειογραφία PascalCase για σταθερές; |https://blog.nette.org/el/for-less-screaming-in-the-code] -- [Γιατί το Nette δεν χρησιμοποιεί το επίθημα Interface; |https://blog.nette.org/el/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: συμβουλές χρήσης |composer] -- [Συμβουλές για editors & εργαλεία |editors-and-tools] -- [Εισαγωγή στον αντικειμενοστρεφή προγραμματισμό |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Δείγματα λύσεων ---------------- -- [Παραδείγματα Nette |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Παραδείγματα Contributte |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Βίντεο ------- -Εκατοντάδες ηχογραφήσεις από τα Poslední soboty και βίντεο για το Nette μπορείτε να βρείτε κάτω από μία στέγη στο "κανάλι YouTube του Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/el/@meta.texy b/best-practices/el/@meta.texy deleted file mode 100644 index 9ae15ea14a..0000000000 --- a/best-practices/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Οδηγοί και διαδικασίες}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/el/attribute-requires.texy b/best-practices/el/attribute-requires.texy deleted file mode 100644 index 6a3211185c..0000000000 --- a/best-practices/el/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Πώς να χρησιμοποιήσετε το attribute `#[Requires]` -************************************************* - -.[perex] -Όταν γράφετε μια διαδικτυακή εφαρμογή, συχνά αντιμετωπίζετε την ανάγκη να περιορίσετε την πρόσβαση σε ορισμένα τμήματα της εφαρμογής σας. Ίσως θέλετε ορισμένα αιτήματα να μπορούν να στέλνουν δεδομένα μόνο μέσω φόρμας (δηλαδή με τη μέθοδο POST), ή να είναι προσβάσιμα μόνο για κλήσεις AJAX. Στο Nette Framework 3.2, εμφανίστηκε ένα νέο εργαλείο που σας επιτρέπει να ορίσετε τέτοιους περιορισμούς με πολύ κομψό και σαφή τρόπο: το attribute `#[Requires]`. - -Το attribute είναι μια ειδική ετικέτα στην PHP, την οποία προσθέτετε πριν από τον ορισμό μιας κλάσης ή μεθόδου. Επειδή είναι στην πραγματικότητα μια κλάση, για να λειτουργήσουν τα παρακάτω παραδείγματα, είναι απαραίτητο να συμπεριλάβετε τη δήλωση use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Μπορείτε να χρησιμοποιήσετε το attribute `#[Requires]` στην ίδια την κλάση του presenter και επίσης σε αυτές τις μεθόδους: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Οι δύο τελευταίες μέθοδοι αφορούν επίσης τα components, οπότε μπορείτε να χρησιμοποιήσετε το attribute και σε αυτά. - -Αν δεν πληρούνται οι προϋποθέσεις που αναφέρει το attribute, προκαλείται σφάλμα HTTP 4xx. - - -Μέθοδοι HTTP ------------- - -Μπορείτε να καθορίσετε ποιες μέθοδοι HTTP (όπως GET, POST κ.λπ.) επιτρέπονται για πρόσβαση. Για παράδειγμα, αν θέλετε να επιτρέψετε την πρόσβαση μόνο με την υποβολή φόρμας, ορίζετε: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Γιατί πρέπει να χρησιμοποιείτε POST αντί για GET για ενέργειες που αλλάζουν την κατάσταση και πώς να το κάνετε; [Διαβάστε τον οδηγό |post-links]. - -Μπορείτε να καθορίσετε μια μέθοδο ή έναν πίνακα μεθόδων. Μια ειδική περίπτωση είναι η τιμή `'*'`, η οποία επιτρέπει όλες τις μεθόδους, κάτι που οι presenters κανονικά [δεν επιτρέπουν για λόγους ασφαλείας |application:presenters#Έλεγχος μεθόδου HTTP]. - - -Κλήσεις AJAX ------------- - -Αν θέλετε ο presenter ή η μέθοδος να είναι διαθέσιμη μόνο για αιτήσεις AJAX, χρησιμοποιήστε: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Ίδια προέλευση --------------- - -Για να αυξήσετε την ασφάλεια, μπορείτε να απαιτήσετε η αίτηση να γίνεται από τον ίδιο τομέα. Αυτό αποτρέπει την [ευπάθεια CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Για τις μεθόδους `handle<Signal>()`, η πρόσβαση από τον ίδιο τομέα απαιτείται αυτόματα. Έτσι, αν αντίθετα θέλετε να επιτρέψετε την πρόσβαση από οποιονδήποτε τομέα, καθορίστε: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Πρόσβαση μέσω forward ---------------------- - -Μερικές φορές είναι χρήσιμο να περιορίσετε την πρόσβαση στον presenter έτσι ώστε να είναι διαθέσιμος μόνο έμμεσα, για παράδειγμα, χρησιμοποιώντας τη μέθοδο `forward()` ή `switch()` από άλλο presenter. Έτσι προστατεύονται, για παράδειγμα, οι error-presenters, ώστε να μην είναι δυνατό να κληθούν από το URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Στην πράξη, συχνά είναι απαραίτητο να επισημάνετε ορισμένα views, στα οποία μπορείτε να φτάσετε μόνο βάσει της λογικής στον presenter. Δηλαδή, ξανά, ώστε να μην είναι δυνατό να ανοίξουν απευθείας: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Συγκεκριμένες ενέργειες ------------------------ - -Μπορείτε επίσης να περιορίσετε ότι ένας συγκεκριμένος κώδικας, όπως η δημιουργία ενός component, θα είναι διαθέσιμος μόνο για συγκεκριμένες actions στον presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Σε περίπτωση μίας action, δεν χρειάζεται να γράψετε πίνακα: `#[Requires(actions: 'default')]` - - -Προσαρμοσμένα attributes ------------------------- - -Αν θέλετε να χρησιμοποιήσετε το attribute `#[Requires]` επανειλημμένα με τις ίδιες ρυθμίσεις, μπορείτε να δημιουργήσετε το δικό σας attribute που θα κληρονομεί το `#[Requires]` και θα το ρυθμίζει ανάλογα με τις ανάγκες. - -Για παράδειγμα, το `#[SingleAction]` θα επιτρέπει την πρόσβαση μόνο μέσω της action `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Ή το `#[RestMethods]` θα επιτρέπει την πρόσβαση μέσω όλων των μεθόδων HTTP που χρησιμοποιούνται για το REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Συμπέρασμα ----------- - -Το attribute `#[Requires]` σας δίνει μεγάλη ευελιξία και έλεγχο στο πώς είναι προσβάσιμες οι ιστοσελίδες σας. Χρησιμοποιώντας απλούς αλλά ισχυρούς κανόνες, μπορείτε να αυξήσετε την ασφάλεια και τη σωστή λειτουργία της εφαρμογής σας. Όπως βλέπετε, η χρήση attributes στο Nette μπορεί όχι μόνο να διευκολύνει τη δουλειά σας, αλλά και να την ασφαλίσει. diff --git a/best-practices/el/composer.texy b/best-practices/el/composer.texy deleted file mode 100644 index 9e0a6a7715..0000000000 --- a/best-practices/el/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: συμβουλές για χρήση -***************************** - -<div class=perex> - -Ο Composer είναι ένα εργαλείο για τη διαχείριση εξαρτήσεων στην PHP. Μας επιτρέπει να απαριθμήσουμε τις βιβλιοθήκες από τις οποίες εξαρτάται το έργο μας και θα τις εγκαθιστά και θα τις ενημερώνει για εμάς. Θα δείξουμε: - -- πώς να εγκαταστήσετε τον Composer -- τη χρήση του σε ένα νέο ή υπάρχον έργο - -</div> - - -Εγκατάσταση -=========== - -Ο Composer είναι ένα εκτελέσιμο αρχείο `.phar`, το οποίο κατεβάζετε και εγκαθιστάτε ως εξής: - - -Windows -------- - -Χρησιμοποιήστε τον επίσημο εγκαταστάτη [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Αρκούν 4 εντολές, τις οποίες αντιγράφετε από [αυτή τη σελίδα |https://getcomposer.org/download/]. - -Στη συνέχεια, τοποθετώντας τον στον φάκελο που βρίσκεται στο σύστημα `PATH`, ο Composer γίνεται προσβάσιμος καθολικά: - -```shell -$ mv ./composer.phar ~/bin/composer # or /usr/local/bin/composer -``` - - -Χρήση στο έργο -============== - -Για να αρχίσουμε να χρησιμοποιούμε τον Composer στο έργο μας, χρειαζόμαστε μόνο το αρχείο `composer.json`. Αυτό περιγράφει τις εξαρτήσεις του έργου μας και μπορεί επίσης να περιέχει άλλα μεταδεδομένα. Ένα βασικό `composer.json` μπορεί λοιπόν να μοιάζει ως εξής: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Λέμε εδώ ότι η εφαρμογή μας (ή η βιβλιοθήκη) απαιτεί το πακέτο `nette/database` (το όνομα του πακέτου αποτελείται από το όνομα του οργανισμού και το όνομα του έργου) και θέλει μια έκδοση που αντιστοιχεί στη συνθήκη `^3.0` (δηλαδή την τελευταία έκδοση 3). - -Έχουμε λοιπόν στη ρίζα του έργου το αρχείο `composer.json` και εκκινούμε την εγκατάσταση: - -```shell -composer update -``` - -Ο Composer θα κατεβάσει το Nette Database στον φάκελο `vendor/`. Στη συνέχεια, θα δημιουργήσει το αρχείο `composer.lock`, το οποίο περιέχει πληροφορίες για τις ακριβείς εκδόσεις των βιβλιοθηκών που εγκατέστησε. - -Ο Composer θα δημιουργήσει το αρχείο `vendor/autoload.php`, το οποίο μπορούμε απλά να συμπεριλάβουμε και να αρχίσουμε να χρησιμοποιούμε τις βιβλιοθήκες χωρίς καμία περαιτέρω εργασία: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Ενημέρωση πακέτων στις τελευταίες εκδόσεις -========================================== - -Η ενημέρωση των χρησιμοποιούμενων βιβλιοθηκών στις τελευταίες εκδόσεις σύμφωνα με τις συνθήκες που ορίζονται στο `composer.json` γίνεται με την εντολή `composer update`. Για παράδειγμα, για την εξάρτηση `"nette/database": "^3.0"`, θα εγκαταστήσει την τελευταία έκδοση 3.x.x, αλλά όχι την έκδοση 4. - -Για να ενημερώσετε τις συνθήκες στο αρχείο `composer.json`, για παράδειγμα σε `"nette/database": "^4.1"`, ώστε να είναι δυνατή η εγκατάσταση της τελευταίας έκδοσης, χρησιμοποιήστε την εντολή `composer require nette/database`. - -Για να ενημερώσετε όλα τα χρησιμοποιούμενα πακέτα Nette, θα ήταν απαραίτητο να τα απαριθμήσετε όλα στη γραμμή εντολών, π.χ.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Κάτι που είναι μη πρακτικό. Χρησιμοποιήστε επομένως το απλό σενάριο "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, το οποίο θα το κάνει για εσάς: - -```shell -php composer-frontline.php -``` - - -Δημιουργία νέου έργου -===================== - -Δημιουργείτε ένα νέο έργο Nette με μία μόνο εντολή: - -```shell -composer create-project nette/web-project project-name -``` - -Ως `project-name` εισάγετε το όνομα του καταλόγου για το έργο σας και επιβεβαιώστε. Ο Composer θα κατεβάσει το αποθετήριο `nette/web-project` από το GitHub, το οποίο περιέχει ήδη το αρχείο `composer.json`, και αμέσως μετά το Nette Framework. Θα πρέπει ήδη να αρκεί μόνο να [ορίσετε τα δικαιώματα |nette:troubleshooting#Ρύθμιση δικαιωμάτων καταλόγου] εγγραφής στους φακέλους `temp/` και `log/` και το έργο θα πρέπει να ζωντανέψει. - -Αν γνωρίζετε σε ποια έκδοση PHP θα φιλοξενηθεί το έργο, μην ξεχάσετε να [την ορίσετε |#Έκδοση PHP]. - - -Έκδοση PHP -========== - -Ο Composer εγκαθιστά πάντα τις εκδόσεις των πακέτων που είναι συμβατές με την έκδοση PHP που χρησιμοποιείτε αυτή τη στιγμή (καλύτερα, με την έκδοση PHP που χρησιμοποιείται στη γραμμή εντολών κατά την εκτέλεση του Composer). Αυτό όμως πιθανότατα δεν είναι η ίδια έκδοση που χρησιμοποιεί το hosting σας. Γι' αυτό είναι πολύ σημαντικό να προσθέσετε στο αρχείο `composer.json` την πληροφορία για την έκδοση PHP στο hosting. Στη συνέχεια, θα εγκαθίστανται μόνο οι εκδόσεις των πακέτων που είναι συμβατές με το hosting. - -Το ότι το έργο θα εκτελείται, για παράδειγμα, σε PHP 8.2.3, το ορίζουμε με την εντολή: - -```shell -composer config platform.php 8.2.3 -``` - -Έτσι, η έκδοση θα καταγραφεί στο αρχείο `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Ωστόσο, ο αριθμός έκδοσης της PHP αναφέρεται και σε άλλο σημείο του αρχείου, στην ενότητα `require`. Ενώ ο πρώτος αριθμός καθορίζει για ποια έκδοση θα εγκατασταθούν τα πακέτα, ο δεύτερος αριθμός λέει για ποια έκδοση είναι γραμμένη η ίδια η εφαρμογή. Και σύμφωνα με αυτόν, για παράδειγμα, το PhpStorm ορίζει το *PHP language level*. (Φυσικά, δεν έχει νόημα αυτές οι εκδόσεις να διαφέρουν, οπότε η διπλή καταγραφή είναι αβλεψία.) Αυτή την έκδοση την ορίζετε με την εντολή: - -```shell -composer require php 8.2.3 --no-update -``` - -Ή απευθείας στο αρχείο `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Αγνόηση έκδοσης PHP -=================== - -Τα πακέτα συνήθως αναφέρουν τόσο την κατώτατη έκδοση PHP με την οποία είναι συμβατά, όσο και την ανώτατη με την οποία έχουν δοκιμαστεί. Αν σκοπεύετε να χρησιμοποιήσετε μια έκδοση PHP ακόμα νεότερη, για παράδειγμα για λόγους δοκιμών, ο Composer θα αρνηθεί να εγκαταστήσει ένα τέτοιο πακέτο. Η λύση είναι η επιλογή `--ignore-platform-req=php+`, η οποία προκαλεί τον Composer να αγνοήσει τα ανώτατα όρια της απαιτούμενης έκδοσης PHP. - - -Ψευδή μηνύματα -============== - -Κατά την αναβάθμιση πακέτων ή την αλλαγή αριθμών έκδοσης, συμβαίνει να προκύψει σύγκρουση. Ένα πακέτο έχει απαιτήσεις που έρχονται σε αντίθεση με ένα άλλο και παρόμοια. Ο Composer όμως μερικές φορές εμφανίζει ψευδή μηνύματα. Αναφέρει σύγκρουση που στην πραγματικότητα δεν υπάρχει. Σε τέτοια περίπτωση, βοηθά η διαγραφή του αρχείου `composer.lock` και η επανάληψη της προσπάθειας. - -Αν το μήνυμα σφάλματος επιμένει, τότε εννοείται σοβαρά και πρέπει να διαβάσετε από αυτό τι και πώς να τροποποιήσετε. - - -Packagist.org - κεντρικό αποθετήριο -=================================== - -Το [Packagist |https://packagist.org] είναι το κύριο αποθετήριο στο οποίο ο Composer προσπαθεί να αναζητήσει πακέτα, αν δεν του πούμε διαφορετικά. Μπορούμε επίσης να δημοσιεύσουμε εδώ τα δικά μας πακέτα. - - -Τι γίνεται αν δεν θέλουμε να χρησιμοποιήσουμε το κεντρικό αποθετήριο; ---------------------------------------------------------------------- - -Αν έχουμε εσωτερικές εταιρικές εφαρμογές, τις οποίες απλά δεν μπορούμε να φιλοξενήσουμε δημόσια, τότε δημιουργούμε γι' αυτές ένα εταιρικό αποθετήριο. - -Περισσότερα για το θέμα των αποθετηρίων [στην επίσημη τεκμηρίωση |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Ένα θεμελιώδες χαρακτηριστικό του Composer είναι ότι παρέχει αυτόματη φόρτωση για όλες τις κλάσεις που έχει εγκαταστήσει, την οποία ξεκινάτε συμπεριλαμβάνοντας το αρχείο `vendor/autoload.php`. - -Ωστόσο, είναι δυνατό να χρησιμοποιήσετε τον Composer και για τη φόρτωση άλλων κλάσεων εκτός του φακέλου `vendor`. Η πρώτη επιλογή είναι να αφήσετε τον Composer να σαρώσει καθορισμένους φακέλους και υποφακέλους, να βρει όλες τις κλάσεις και να τις συμπεριλάβει στον autoloader. Αυτό επιτυγχάνεται ορίζοντας το `autoload > classmap` στο `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # περιλαμβάνει τον φάκελο src/ και τους υποφακέλους του - ] - } -} -``` - -Στη συνέχεια, είναι απαραίτητο σε κάθε αλλαγή να εκτελείτε την εντολή `composer dumpautoload` και να αφήνετε τους πίνακες αυτόματης φόρτωσης να αναδημιουργηθούν. Αυτό είναι εξαιρετικά άβολο και είναι πολύ καλύτερο να αναθέσετε αυτή την εργασία στο [RobotLoader|robot-loader:], το οποίο εκτελεί την ίδια δραστηριότητα αυτόματα στο παρασκήνιο και πολύ πιο γρήγορα. - -Η δεύτερη επιλογή είναι η τήρηση του [PSR-4|https://www.php-fig.org/psr/psr-4/]. Με απλά λόγια, πρόκειται για ένα σύστημα όπου οι χώροι ονομάτων και τα ονόματα κλάσεων αντιστοιχούν στη δομή καταλόγων και τα ονόματα αρχείων, δηλαδή π.χ. το `App\Core\RouterFactory` θα βρίσκεται στο αρχείο `/path/to/App/Core/RouterFactory.php`. Παράδειγμα διαμόρφωσης: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # ο χώρος ονομάτων App\ βρίσκεται στον κατάλογο app/ - } - } -} -``` - -Πώς ακριβώς να διαμορφώσετε τη συμπεριφορά θα μάθετε στην [τεκμηρίωση του Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Δοκιμή νέων εκδόσεων -==================== - -Θέλετε να δοκιμάσετε μια νέα αναπτυξιακή έκδοση ενός πακέτου. Πώς να το κάνετε; Πρώτα, προσθέστε στο αρχείο `composer.json` αυτό το ζεύγος επιλογών, το οποίο επιτρέπει την εγκατάσταση αναπτυξιακών εκδόσεων πακέτων, αλλά καταφεύγει σε αυτό μόνο στην περίπτωση που δεν υπάρχει κανένας συνδυασμός σταθερών εκδόσεων που να ικανοποιεί τις απαιτήσεις: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Στη συνέχεια, συνιστούμε να διαγράψετε το αρχείο `composer.lock`, μερικές φορές ο Composer ανεξήγητα αρνείται την εγκατάσταση και αυτό λύνει το πρόβλημα. - -Ας υποθέσουμε ότι πρόκειται για το πακέτο `nette/utils` και η νέα έκδοση έχει τον αριθμό 4.0. Την εγκαθιστάτε με την εντολή: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ή μπορείτε να εγκαταστήσετε μια συγκεκριμένη έκδοση, για παράδειγμα 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Αν όμως από τη βιβλιοθήκη εξαρτάται ένα άλλο πακέτο που είναι κλειδωμένο σε παλαιότερη έκδοση (π.χ. `^3.1`), τότε είναι ιδανικό να ενημερώσετε το πακέτο, ώστε να λειτουργεί με τη νέα έκδοση. Αν όμως θέλετε απλώς να παρακάμψετε τον περιορισμό και να αναγκάσετε τον Composer να εγκαταστήσει την αναπτυξιακή έκδοση και να προσποιηθεί ότι πρόκειται για παλαιότερη έκδοση (π.χ. 3.1.6), μπορείτε να χρησιμοποιήσετε τη λέξη-κλειδί `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Κλήση εντολών -============= - -Μέσω του Composer μπορείτε να καλέσετε τις δικές σας προκαθορισμένες εντολές και σενάρια, σαν να ήταν εγγενείς εντολές του Composer. Για σενάρια που βρίσκονται στον φάκελο `vendor/bin`, δεν χρειάζεται να αναφέρετε αυτόν τον φάκελο. - -Ως παράδειγμα, ορίζουμε στο αρχείο `composer.json` ένα σενάριο που χρησιμοποιεί το [Nette Tester|tester:] για την εκτέλεση δοκιμών: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Στη συνέχεια, εκτελούμε τις δοκιμές χρησιμοποιώντας το `composer tester`. Μπορούμε να καλέσουμε την εντολή ακόμα κι αν δεν βρισκόμαστε στον ριζικό φάκελο του έργου, αλλά σε κάποιον υποφάκελο. - - -Στείλτε ευχαριστίες -=================== - -Θα σας δείξουμε ένα κόλπο με το οποίο θα ευχαριστήσετε τους δημιουργούς open source. Με έναν απλό τρόπο, δίνετε αστέρι στο GitHub στις βιβλιοθήκες που χρησιμοποιεί το έργο σας. Αρκεί να εγκαταστήσετε τη βιβλιοθήκη `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -Και στη συνέχεια να εκτελέσετε: - -```shell -composer thanks -``` - -Δοκιμάστε το! - - -Διαμόρφωση -========== - -Ο Composer είναι στενά συνδεδεμένος με το εργαλείο διαχείρισης εκδόσεων [Git |https://git-scm.com]. Αν δεν το έχετε εγκατεστημένο, πρέπει να πείτε στον Composer να μην το χρησιμοποιεί: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/el/creating-editing-form.texy b/best-practices/el/creating-editing-form.texy deleted file mode 100644 index 4566a81628..0000000000 --- a/best-practices/el/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Φόρμα για τη δημιουργία και την επεξεργασία εγγραφών -**************************************************** - -.[perex] -Πώς να υλοποιήσετε σωστά την προσθήκη και την επεξεργασία εγγραφών στο Nette, χρησιμοποιώντας την ίδια φόρμα και για τις δύο λειτουργίες; - -Σε πολλές περιπτώσεις, οι φόρμες για την προσθήκη και την επεξεργασία εγγραφών είναι ίδιες, διαφέροντας ίσως μόνο στην ετικέτα του κουμπιού. Θα δείξουμε παραδείγματα απλών presenters, όπου θα χρησιμοποιήσουμε τη φόρμα πρώτα για την προσθήκη εγγραφής, μετά για την επεξεργασία και τέλος θα συνδυάσουμε τις δύο λύσεις. - - -Προσθήκη εγγραφής ------------------ - -Παράδειγμα presenter που χρησιμεύει για την προσθήκη εγγραφής. Την πραγματική εργασία με τη βάση δεδομένων θα την αφήσουμε στην κλάση `Facade`, ο κώδικας της οποίας δεν είναι ουσιαστικός για το παράδειγμα. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... προσθέτουμε πεδία φόρμας ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // προσθήκη εγγραφής στη βάση δεδομένων - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Επεξεργασία εγγραφής --------------------- - -Τώρα θα δείξουμε πώς θα έμοιαζε ο presenter που χρησιμεύει για την επεξεργασία εγγραφής: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // έλεγχος ύπαρξης εγγραφής - || !$this->facade->isEditAllowed(/*...*/) // έλεγχος δικαιωμάτων - ) { - $this->error(); // σφάλμα 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // ελέγχουμε ότι η action είναι 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... προσθέτουμε πεδία φόρμας ... - - $form->setDefaults($this->record); // ορισμός προεπιλεγμένων τιμών - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // ενημέρωση εγγραφής - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -Στη μέθοδο *action*, η οποία εκτελείται αμέσως στην αρχή του [κύκλου ζωής του presenter |application:presenters#Κύκλος ζωής του presenter], ελέγχουμε την ύπαρξη της εγγραφής και τα δικαιώματα του χρήστη να την επεξεργαστεί. - -Αποθηκεύουμε την εγγραφή στην ιδιότητα `$record`, ώστε να την έχουμε διαθέσιμη στη μέθοδο `createComponentRecordForm()` για τον ορισμό των προεπιλεγμένων τιμών, και στη `recordFormSucceeded()` για το ID. Μια εναλλακτική λύση θα ήταν να ορίσουμε τις προεπιλεγμένες τιμές απευθείας στην `actionEdit()` και να λάβουμε την τιμή του ID, η οποία είναι μέρος του URL, χρησιμοποιώντας το `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // έλεγχος ύπαρξης και έλεγχος δικαιωμάτων - ) { - $this->error(); - } - - // ορισμός προεπιλεγμένων τιμών της φόρμας - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Ωστόσο, και αυτό θα έπρεπε να είναι **το πιο σημαντικό συμπέρασμα όλου του κώδικα**, πρέπει κατά τη δημιουργία της φόρμας να βεβαιωθούμε ότι η action είναι όντως `edit`. Διότι διαφορετικά, ο έλεγχος στη μέθοδο `actionEdit()` δεν θα είχε πραγματοποιηθεί καθόλου! - - -Ίδια φόρμα για προσθήκη και επεξεργασία ---------------------------------------- - -Και τώρα συνδυάζουμε τους δύο presenters σε έναν. Είτε θα μπορούσαμε στη μέθοδο `createComponentRecordForm()` να διακρίνουμε ποια action είναι και ανάλογα να διαμορφώσουμε τη φόρμα, είτε μπορούμε να το αφήσουμε απευθείας στις action-μεθόδους και να απαλλαγούμε από τη συνθήκη: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // έλεγχος ύπαρξης εγγραφής - || !$this->facade->isEditAllowed(/*...*/) // έλεγχος δικαιωμάτων - ) { - $this->error(); // σφάλμα 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // ορισμός προεπιλεγμένων τιμών - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // ελέγχουμε ότι η action είναι 'add' ή 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... προσθέτουμε πεδία φόρμας ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // προσθήκη εγγραφής στη βάση δεδομένων - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // ενημέρωση εγγραφής - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/el/dynamic-snippets.texy b/best-practices/el/dynamic-snippets.texy deleted file mode 100644 index 134ee63491..0000000000 --- a/best-practices/el/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Δυναμικά snippets -***************** - -Αρκετά συχνά κατά την ανάπτυξη εφαρμογών προκύπτει η ανάγκη εκτέλεσης λειτουργιών AJAX, για παράδειγμα, σε μεμονωμένες γραμμές πίνακα ή στοιχεία λίστας. Ως παράδειγμα, μπορούμε να επιλέξουμε την εμφάνιση άρθρων, όπου για κάθε ένα από αυτά επιτρέπουμε στον συνδεδεμένο χρήστη να επιλέξει βαθμολογία "μου αρέσει/δεν μου αρέσει". Ο κώδικας του presenter και του αντίστοιχου template χωρίς AJAX θα μοιάζει περίπου ως εξής (παραθέτω τα πιο σημαντικά αποσπάσματα, ο κώδικας υπολογίζει την ύπαρξη μιας υπηρεσίας για την επισήμανση της βαθμολογίας και τη λήψη της συλλογής άρθρων - η συγκεκριμένη υλοποίηση δεν είναι σημαντική για τους σκοπούς αυτού του οδηγού): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Template: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>μου αρέσει</a> - {else} - <a n:href="unlike! $article->id" class=ajax>δεν μου αρέσει πια</a> - {/if} -</article> -``` - - -Ajaxification -============= - -Ας εξοπλίσουμε τώρα αυτήν την απλή εφαρμογή με AJAX. Η αλλαγή της βαθμολογίας ενός άρθρου δεν είναι τόσο σημαντική ώστε να απαιτείται ανακατεύθυνση, και επομένως θα έπρεπε ιδανικά να γίνεται με AJAX στο παρασκήνιο. Θα χρησιμοποιήσουμε το [βοηθητικό script από τα add-ons |application:ajax#Naja] με τη συνήθη σύμβαση ότι οι σύνδεσμοι AJAX έχουν την CSS κλάση `ajax`. - -Ωστόσο, πώς να το κάνουμε συγκεκριμένα; Το Nette προσφέρει 2 δρόμους: τον δρόμο των λεγόμενων δυναμικών snippets και τον δρόμο των components. Και οι δύο έχουν τα υπέρ και τα κατά τους, και γι' αυτό θα τους παρουσιάσουμε έναν προς έναν. - - -Ο δρόμος των δυναμικών snippets -=============================== - -Ένα δυναμικό snippet σημαίνει στην ορολογία του Latte μια συγκεκριμένη περίπτωση χρήσης του tag `{snippet}`, όπου στο όνομα του snippet χρησιμοποιείται μια μεταβλητή. Ένα τέτοιο snippet δεν μπορεί να βρίσκεται οπουδήποτε στο template - πρέπει να περιβάλλεται από ένα στατικό snippet, δηλαδή ένα συνηθισμένο, ή μέσα σε `{snippetArea}`. Θα μπορούσαμε να τροποποιήσουμε το template μας ως εξής. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>μου αρέσει</a> - {else} - <a n:href="unlike! $article->id" class=ajax>δεν μου αρέσει πια</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Κάθε άρθρο ορίζει τώρα ένα snippet, το οποίο έχει στο όνομά του το ID του άρθρου. Όλα αυτά τα snippets είναι στη συνέχεια ομαδοποιημένα μαζί με ένα snippet με το όνομα `articlesContainer`. Αν παραλείπαμε αυτό το περιβάλλον snippet, το Latte θα μας ειδοποιούσε με μια εξαίρεση. - -Μας μένει να συμπληρώσουμε την επανασχεδίαση στον presenter - αρκεί να επανασχεδιάσουμε το στατικό περιτύλιγμα. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- δεν χρειάζεται - } else { - $this->redirect('this'); - } -} -``` - -Ομοίως, τροποποιούμε και την αδελφή μέθοδο `handleUnlike()`, και το AJAX είναι λειτουργικό! - -Η λύση έχει όμως ένα σκοτεινό σημείο. Αν εξετάζαμε περισσότερο πώς διεξάγεται το αίτημα AJAX, θα διαπιστώναμε ότι παρόλο που εξωτερικά η εφαρμογή φαίνεται οικονομική (επιστρέφει μόνο ένα μοναδικό snippet για το συγκεκριμένο άρθρο), στην πραγματικότητα στον server σχεδίασε όλα τα snippets. Το επιθυμητό snippet τοποθετήθηκε στο payload, και τα υπόλοιπα απορρίφθηκαν (εντελώς άσκοπα τα απέκτησε επίσης από τη βάση δεδομένων). - -Για να βελτιστοποιήσουμε αυτή τη διαδικασία, θα πρέπει να παρέμβουμε εκεί όπου περνάμε τη συλλογή `$articles` στο template (ας πούμε στη μέθοδο `renderDefault()`). Θα εκμεταλλευτούμε το γεγονός ότι η επεξεργασία των σημάτων γίνεται πριν από τις μεθόδους `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Τώρα, κατά την επεξεργασία του σήματος, στο template περνιέται αντί για τη συλλογή με όλα τα άρθρα, μόνο ένας πίνακας με ένα μοναδικό άρθρο - αυτό που θέλουμε να σχεδιάσουμε και να στείλουμε στο payload στον browser. Το `{foreach}` λοιπόν θα εκτελεστεί μόνο μία φορά και κανένα επιπλέον snippet δεν θα σχεδιαστεί. - - -Ο δρόμος των components -======================= - -Ένας εντελώς διαφορετικός τρόπος λύσης αποφεύγει τα δυναμικά snippets. Το κόλπο έγκειται στη μεταφορά ολόκληρης της λογικής σε ένα ξεχωριστό component - από τώρα και στο εξής, η εισαγωγή βαθμολογίας δεν θα γίνεται από τον presenter, αλλά από ένα εξειδικευμένο `LikeControl`. Η κλάση θα μοιάζει ως εξής (εκτός από αυτό, θα περιέχει επίσης τις μεθόδους `render`, `handleUnlike` κ.λπ.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Το template του component: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>μου αρέσει</a> - {else} - <a n:href="unlike!" class=ajax>δεν μου αρέσει πια</a> - {/if} -{/snippet} -``` - -Φυσικά, το template της προβολής θα αλλάξει και θα πρέπει να προσθέσουμε ένα factory στον presenter. Επειδή θα δημιουργήσουμε το component τόσες φορές όσα άρθρα λάβουμε από τη βάση δεδομένων, θα χρησιμοποιήσουμε την κλάση [Multiplier |application:Multiplier] για τον "πολλαπλασιασμό" του. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Το template της προβολής θα μειωθεί στο ελάχιστο απαραίτητο (και εντελώς απαλλαγμένο από snippets!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Έχουμε σχεδόν τελειώσει: η εφαρμογή τώρα θα λειτουργεί με AJAX. Και εδώ μας περιμένει η βελτιστοποίηση της εφαρμογής, επειδή λόγω της χρήσης του Nette Database, κατά την επεξεργασία του σήματος φορτώνονται άσκοπα όλα τα άρθρα από τη βάση δεδομένων αντί για ένα. Το πλεονέκτημα όμως είναι ότι δεν θα γίνει η σχεδίασή τους, επειδή θα αποδοθεί πραγματικά μόνο το component μας. - -{{priority: -1}} diff --git a/best-practices/el/editors-and-tools.texy b/best-practices/el/editors-and-tools.texy deleted file mode 100644 index a5d46f739c..0000000000 --- a/best-practices/el/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Επεξεργαστές & εργαλεία -*********************** - -.[perex] -Μπορεί να είστε ένας ικανός προγραμματιστής, αλλά μόνο με καλά εργαλεία γίνεστε μάστορας. Σε αυτό το κεφάλαιο θα βρείτε συμβουλές για σημαντικά εργαλεία, επεξεργαστές και plugins. - - -IDE editor -========== - -Συνιστούμε ανεπιφύλακτα τη χρήση ενός πλήρους IDE για την ανάπτυξη, όπως το PhpStorm, το NetBeans, το VS Code, και όχι απλώς ενός επεξεργαστή κειμένου με υποστήριξη PHP. Η διαφορά είναι πραγματικά θεμελιώδης. Δεν υπάρχει λόγος να αρκεστείτε σε έναν απλό επεξεργαστή που, αν και μπορεί να χρωματίζει τη σύνταξη, δεν φτάνει τις δυνατότητες ενός κορυφαίου IDE, το οποίο προτείνει με ακρίβεια, ελέγχει για σφάλματα, μπορεί να αναδιαμορφώσει τον κώδικα και πολλά άλλα. Ορισμένα IDE είναι επί πληρωμή, άλλα είναι ακόμη και δωρεάν. - -Το **NetBeans IDE** έχει ενσωματωμένη υποστήριξη για Nette, Latte και NEON. - -**PhpStorm**: εγκαταστήστε αυτά τα plugins στο `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: βρείτε το plugin "Nette Latte + Neon" στο marketplace. - -Συνδέστε επίσης το Tracy με τον επεξεργαστή σας. Όταν εμφανίζεται μια σελίδα σφάλματος, θα μπορείτε να κάνετε κλικ στα ονόματα των αρχείων και αυτά θα ανοίγουν στον επεξεργαστή με τον κέρσορα στην αντίστοιχη γραμμή. Διαβάστε [πώς να διαμορφώσετε το σύστημα |tracy:open-files-in-ide]. - - -PHPStan -======= - -Το PHPStan είναι ένα εργαλείο που εντοπίζει λογικά σφάλματα στον κώδικα πριν τον εκτελέσετε. - -Το εγκαθιστούμε χρησιμοποιώντας το Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Δημιουργούμε στο έργο ένα αρχείο διαμόρφωσης `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Και στη συνέχεια το αφήνουμε να αναλύσει τις κλάσεις στον φάκελο `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Μπορείτε να βρείτε εξαντλητική τεκμηρίωση απευθείας στην [ιστοσελίδα του PHPStan |https://phpstan.org]. - - -Code Checker -============ - -Ο [Code Checker|code-checker:] ελέγχει και ενδεχομένως διορθώνει ορισμένα από τα τυπικά σφάλματα στους πηγαίους κώδικές σας: - -- αφαιρεί το [BOM |nette:glossary#BOM] -- ελέγχει την εγκυρότητα των templates [Latte |latte:] -- ελέγχει την εγκυρότητα των αρχείων `.neon`, `.php` και `.json` -- ελέγχει την ύπαρξη [χαρακτήρων ελέγχου |nette:glossary#Control characters] -- ελέγχει αν το αρχείο είναι κωδικοποιημένο σε UTF-8 -- ελέγχει λανθασμένα γραμμένα `/* @anotace */` (λείπει ο αστερίσκος) -- αφαιρεί το τελικό `?>` από τα αρχεία PHP -- αφαιρεί τα δεξιά κενά και τις περιττές γραμμές στο τέλος του αρχείου -- κανονικοποιεί τους διαχωριστές γραμμών σε συστήματος (αν δώσετε την επιλογή `-l`) - - -Composer -======== - -Ο [Composer |Composer] είναι ένα εργαλείο διαχείρισης εξαρτήσεων στο PHP. Μας επιτρέπει να δηλώνουμε αυθαίρετα πολύπλοκες εξαρτήσεις μεμονωμένων βιβλιοθηκών και στη συνέχεια τις εγκαθιστά για εμάς στο έργο μας. - - -Requirements Checker -==================== - -Ήταν ένα εργαλείο που δοκίμαζε το περιβάλλον εκτέλεσης του server και ενημέρωνε αν (και σε ποιο βαθμό) ήταν δυνατό να χρησιμοποιηθεί το framework. Επί του παρόντος, το Nette μπορεί να χρησιμοποιηθεί σε κάθε server που έχει την ελάχιστη απαιτούμενη έκδοση PHP. diff --git a/best-practices/el/form-reuse.texy b/best-practices/el/form-reuse.texy deleted file mode 100644 index 28f0bf1b38..0000000000 --- a/best-practices/el/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Επαναχρησιμοποίηση φορμών σε πολλαπλά μέρη -****************************************** - -.[perex] -Στο Nette έχετε στη διάθεσή σας αρκετές επιλογές για να χρησιμοποιήσετε την ίδια φόρμα σε πολλαπλά μέρη και να μην επαναλαμβάνετε τον κώδικα. Σε αυτό το άρθρο θα δείξουμε διάφορες λύσεις, συμπεριλαμβανομένων εκείνων που θα έπρεπε να αποφύγετε. - - -Factory φορμών -============== - -Μία από τις βασικές προσεγγίσεις για τη χρήση του ίδιου component σε πολλαπλά μέρη είναι η δημιουργία μιας μεθόδου ή κλάσης που παράγει αυτό το component, και στη συνέχεια η κλήση αυτής της μεθόδου σε διάφορα μέρη της εφαρμογής. Μια τέτοια μέθοδος ή κλάση ονομάζεται *factory*. Μην τη συγχέετε με το design pattern *factory method*, το οποίο περιγράφει έναν συγκεκριμένο τρόπο χρήσης των factories και δεν σχετίζεται με αυτό το θέμα. - -Ως παράδειγμα, θα δημιουργήσουμε ένα factory που θα κατασκευάζει μια φόρμα επεξεργασίας: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Τώρα μπορείτε να χρησιμοποιήσετε αυτό το factory σε διάφορα μέρη της εφαρμογής σας, για παράδειγμα σε presenters ή components. Και αυτό γίνεται [ζητώντας το ως εξάρτηση |dependency-injection:passing-dependencies]. Πρώτα, λοιπόν, καταχωρούμε την κλάση στο αρχείο διαμόρφωσης: - -```neon -services: - - FormFactory -``` - -Και στη συνέχεια τη χρησιμοποιούμε στον presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // επεξεργασία των απεσταλμένων δεδομένων - }; - return $form; - } -} -``` - -Μπορείτε να επεκτείνετε το factory φορμών με επιπλέον μεθόδους για τη δημιουργία άλλων τύπων φορμών ανάλογα με τις ανάγκες της εφαρμογής σας. Και φυσικά, μπορούμε να προσθέσουμε και μια μέθοδο που δημιουργεί μια βασική φόρμα χωρίς στοιχεία, και αυτή θα χρησιμοποιείται από τις άλλες μεθόδους: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Η μέθοδος `createForm()` προς το παρόν δεν κάνει τίποτα χρήσιμο, αλλά αυτό θα αλλάξει γρήγορα. - - -Εξαρτήσεις του factory -====================== - -Με τον καιρό θα φανεί ότι χρειαζόμαστε οι φόρμες να είναι πολυγλωσσικές. Αυτό σημαίνει ότι σε όλες τις φόρμες πρέπει να ορίσουμε τον λεγόμενο [translator |forms:rendering#Μετάφραση]. Για τον σκοπό αυτό, τροποποιούμε την κλάση `FormFactory` ώστε να δέχεται το αντικείμενο `Translator` ως εξάρτηση στον constructor, και το περνάμε στη φόρμα: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Επειδή η μέθοδος `createForm()` καλείται και από τις άλλες μεθόδους που δημιουργούν συγκεκριμένες φόρμες, αρκεί να ορίσουμε τον translator μόνο σε αυτήν. Και τελειώσαμε. Δεν χρειάζεται να αλλάξουμε τον κώδικα κανενός presenter ή component, πράγμα που είναι εξαιρετικό. - - -Περισσότερες κλάσεις factory -============================ - -Εναλλακτικά, μπορείτε να δημιουργήσετε περισσότερες κλάσεις για κάθε φόρμα που θέλετε να χρησιμοποιήσετε στην εφαρμογή σας. Αυτή η προσέγγιση μπορεί να αυξήσει την αναγνωσιμότητα του κώδικα και να διευκολύνει τη διαχείριση των φορμών. Το αρχικό `FormFactory` θα το αφήσουμε να δημιουργεί μόνο μια καθαρή φόρμα με βασική διαμόρφωση (για παράδειγμα με υποστήριξη μεταφράσεων) και για τη φόρμα επεξεργασίας θα δημιουργήσουμε ένα νέο factory `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ χρήση σύνθεσης -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Είναι πολύ σημαντικό η σχέση μεταξύ των κλάσεων `FormFactory` και `EditFormFactory` να υλοποιείται με [σύνθεση |nette:introduction-to-object-oriented-programming#Σύνθεση], και όχι με [κληρονομικότητα αντικειμένων |nette:introduction-to-object-oriented-programming#Κληρονομικότητα]: - -```php -// ⛔ ΟΧΙ ΕΤΣΙ! Η ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΔΕΝ ΑΝΗΚΕΙ ΕΔΩ -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Η χρήση κληρονομικότητας θα ήταν σε αυτή την περίπτωση εντελώς αντιπαραγωγική. Θα αντιμετωπίζατε προβλήματα πολύ γρήγορα. Για παράδειγμα, τη στιγμή που θα θέλατε να προσθέσετε παραμέτρους στη μέθοδο `create()`; η PHP θα ανέφερε σφάλμα ότι η υπογραφή της διαφέρει από την γονική. Ή κατά το πέρασμα εξαρτήσεων στην κλάση `EditFormFactory` μέσω του constructor. Θα προέκυπτε η κατάσταση που ονομάζουμε [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -Γενικά, είναι καλύτερο να προτιμάτε τη [σύνθεση έναντι κληρονομικότητας |dependency-injection:faq#Γιατί προτιμάται η σύνθεση composition έναντι της κληρονομικότητας]. - - -Χειρισμός φόρμας -================ - -Ο χειρισμός της φόρμας, που καλείται μετά την επιτυχή υποβολή, μπορεί επίσης να είναι μέρος της κλάσης factory. Θα λειτουργεί έτσι ώστε να παραδίδει τα υποβληθέντα δεδομένα στο μοντέλο για επεξεργασία. Τυχόν σφάλματα θα τα [επιστρέψει |forms:validation#Σφάλματα κατά την Επεξεργασία] στη φόρμα. Το μοντέλο στο ακόλουθο παράδειγμα αντιπροσωπεύεται από την κλάση `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // επεξεργασία των απεσταλμένων δεδομένων - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Την ίδια την ανακατεύθυνση όμως θα την αφήσουμε στον presenter. Αυτός θα προσθέσει στο event `onSuccess` έναν επιπλέον handler που θα πραγματοποιήσει την ανακατεύθυνση. Χάρη σε αυτό, θα είναι δυνατό να χρησιμοποιηθεί η φόρμα σε διάφορους presenters και σε καθέναν να γίνει ανακατεύθυνση αλλού. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Η εγγραφή αποθηκεύτηκε'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Αυτή η λύση εκμεταλλεύεται την ιδιότητα των φορμών ότι όταν καλείται το `addError()` πάνω στη φόρμα ή στα στοιχεία της, ο επόμενος handler `onSuccess` δεν καλείται πλέον. - - -Κληρονομικότητα από την κλάση Form -================================== - -Η συναρμολογημένη φόρμα δεν πρέπει να είναι απόγονος της φόρμας. Με άλλα λόγια, μην χρησιμοποιείτε αυτή τη λύση: - -```php -// ⛔ ΟΧΙ ΕΤΣΙ! Η ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΔΕΝ ΑΝΗΚΕΙ ΕΔΩ -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $this->addSubmit('send', 'Αποστολή'); - $this->setTranslator($translator); - } -} -``` - -Αντί να συναρμολογείτε τη φόρμα στον constructor, χρησιμοποιήστε ένα factory. - -Πρέπει να συνειδητοποιήσετε ότι η κλάση `Form` είναι πρωτίστως ένα εργαλείο για τη συναρμολόγηση μιας φόρμας, δηλαδή ένας *form builder*. Και η συναρμολογημένη φόρμα μπορεί να θεωρηθεί ως προϊόν της. Όμως το προϊόν δεν είναι μια ειδική περίπτωση του builder, δεν υπάρχει μεταξύ τους σχέση *is a* που αποτελεί τη βάση της κληρονομικότητας. - - -Component με φόρμα -================== - -Μια εντελώς διαφορετική προσέγγιση είναι η δημιουργία ενός [component |application:components], μέρος του οποίου είναι μια φόρμα. Αυτό δίνει νέες δυνατότητες, για παράδειγμα την απόδοση της φόρμας με συγκεκριμένο τρόπο, καθώς μέρος του component είναι και ένα template. Ή μπορεί να χρησιμοποιηθούν σήματα για επικοινωνία AJAX και φόρτωση πληροφοριών στη φόρμα, για παράδειγμα για προτάσεις, κ.λπ. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // επεξεργασία των απεσταλμένων δεδομένων - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // εκκίνηση του event - $this->onSave($this, $data); - } -} -``` - -Θα δημιουργήσουμε επίσης ένα factory που θα παράγει αυτό το component. Αρκεί να [καταχωρήσετε το interface του |application:components#Components με Εξαρτήσεις]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -Και να το προσθέσουμε στο αρχείο διαμόρφωσης: - -```neon -services: - - EditControlFactory -``` - -Και τώρα μπορούμε ήδη να ζητήσουμε το factory και να το χρησιμοποιήσουμε στον presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ή ανακατευθύνουμε στο αποτέλεσμα της επεξεργασίας, π.χ.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/el/inject-method-attribute.texy b/best-practices/el/inject-method-attribute.texy deleted file mode 100644 index a13bfcff48..0000000000 --- a/best-practices/el/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Μέθοδοι και attributes inject -***************************** - -.[perex] -Σε αυτό το άρθρο, θα επικεντρωθούμε στους διάφορους τρόπους περάσματος εξαρτήσεων στους presenters στο Nette framework. Θα συγκρίνουμε τον προτιμώμενο τρόπο, που είναι ο constructor, με άλλες επιλογές, όπως οι μέθοδοι και τα attributes `inject`. - -Και για τους presenters ισχύει ότι το πέρασμα εξαρτήσεων μέσω του [constructor |dependency-injection:passing-dependencies#Παράδοση μέσω κατασκευαστή] είναι ο προτιμώμενος δρόμος. Αν όμως δημιουργείτε έναν κοινό πρόγονο από τον οποίο κληρονομούν άλλοι presenters (π.χ. `BasePresenter`), και αυτός ο πρόγονος έχει επίσης εξαρτήσεις, προκύπτει ένα πρόβλημα που ονομάζουμε [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Αυτό μπορεί να παρακαμφθεί χρησιμοποιώντας εναλλακτικούς δρόμους, που αντιπροσωπεύουν οι μέθοδοι και τα attributes (annotations) `inject`. - - -Μέθοδοι `inject*()` -=================== - -Πρόκειται για μια μορφή περάσματος εξάρτησης με [setter |dependency-injection:passing-dependencies#Παράδοση μέσω setter]. Το όνομα αυτών των setters ξεκινά με το πρόθεμα `inject`. Το Nette DI καλεί αυτόματα τις μεθόδους με αυτό το όνομα αμέσως μετά τη δημιουργία της παρουσίας του presenter και τους περνά όλες τις απαιτούμενες εξαρτήσεις. Πρέπει επομένως να δηλώνονται ως public. - -Οι μέθοδοι `inject*()` μπορούν να θεωρηθούν ως ένα είδος επέκτασης του constructor σε περισσότερες μεθόδους. Χάρη σε αυτό, ο `BasePresenter` μπορεί να λάβει εξαρτήσεις μέσω μιας άλλης μεθόδου και να αφήσει τον constructor ελεύθερο για τους απογόνους του: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Ο presenter μπορεί να περιέχει οποιονδήποτε αριθμό μεθόδων `inject*()` και καθεμία μπορεί να έχει οποιονδήποτε αριθμό παραμέτρων. Ταιριάζουν εξαιρετικά επίσης σε περιπτώσεις όπου ο presenter [αποτελείται από traits |presenter-traits] και καθεμία από αυτές απαιτεί τη δική της εξάρτηση. - - -Attributes `Inject` -=================== - -Πρόκειται για μια μορφή [injection στην ιδιότητα |dependency-injection:passing-dependencies#Ρύθμιση μεταβλητής]. Αρκεί να επισημάνετε σε ποιες μεταβλητές πρέπει να γίνει inject, και το Nette DI περνά αυτόματα τις εξαρτήσεις αμέσως μετά τη δημιουργία της παρουσίας του presenter. Για να μπορέσει να τις εισαγάγει, είναι απαραίτητο να δηλώνονται ως public. - -Επισημαίνουμε τις properties με το attribute: (παλαιότερα χρησιμοποιούνταν η annotation `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // αυτή η γραμμή είναι σημαντική - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Το πλεονέκτημα αυτού του τρόπου περάσματος εξαρτήσεων ήταν η πολύ οικονομική μορφή γραφής. Ωστόσο, με την έλευση του [constructor property promotion |https://blog.nette.org/el/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], φαίνεται ευκολότερο να χρησιμοποιηθεί ο constructor. - -Αντίθετα, αυτός ο τρόπος πάσχει από τις ίδιες αδυναμίες με το πέρασμα εξαρτήσεων σε properties γενικά: δεν έχουμε έλεγχο στις αλλαγές στη μεταβλητή και ταυτόχρονα η μεταβλητή γίνεται μέρος του δημόσιου interface της κλάσης, πράγμα που είναι ανεπιθύμητο. diff --git a/best-practices/el/lets-create-contact-form.texy b/best-practices/el/lets-create-contact-form.texy deleted file mode 100644 index fa4e52cc69..0000000000 --- a/best-practices/el/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Δημιουργούμε μια φόρμα επικοινωνίας -*********************************** - -.[perex] -Θα δούμε πώς να δημιουργήσουμε μια φόρμα επικοινωνίας στο Nette, συμπεριλαμβανομένης της αποστολής μέσω email. Ας ξεκινήσουμε λοιπόν! - -Πρώτα πρέπει να δημιουργήσουμε ένα νέο έργο. Πώς να το κάνετε αυτό εξηγείται στη σελίδα [Ξεκινώντας |nette:installation]. Και μετά μπορούμε ήδη να αρχίσουμε να δημιουργούμε τη φόρμα. - -Ο ευκολότερος τρόπος είναι να δημιουργήσετε τη [φόρμα απευθείας στον presenter |forms:in-presenter]. Μπορούμε να χρησιμοποιήσουμε τον προετοιμασμένο `HomePresenter`. Σε αυτόν θα προσθέσουμε το component `contactForm` που αντιπροσωπεύει τη φόρμα. Θα το κάνουμε γράφοντας στον κώδικα τη μέθοδο factory `createComponentContactForm()`, η οποία θα κατασκευάσει το component: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:') - ->setRequired('Εισάγετε όνομα'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Εισάγετε e-mail'); - $form->addTextarea('message', 'Μήνυμα:') - ->setRequired('Εισάγετε μήνυμα'); - $form->addSubmit('send', 'Αποστολή'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // αποστολή email - } -} -``` - -Όπως βλέπετε, δημιουργήσαμε δύο μεθόδους. Η πρώτη μέθοδος `createComponentContactForm()` δημιουργεί μια νέα φόρμα. Αυτή έχει πεδία για όνομα, email και μήνυμα, τα οποία προσθέτουμε με τις μεθόδους `addText()`, `addEmail()` και `addTextArea()`. Προσθέσαμε επίσης ένα κουμπί για την αποστολή της φόρμας. Αλλά τι γίνεται αν ο χρήστης δεν συμπληρώσει κάποιο πεδίο; Σε αυτή την περίπτωση, θα πρέπει να τον ενημερώσουμε ότι είναι υποχρεωτικό πεδίο. Αυτό το πετύχαμε με τη μέθοδο `setRequired()`. Τέλος, προσθέσαμε επίσης το [event |nette:glossary#Events] `onSuccess`, το οποίο ενεργοποιείται εάν η φόρμα υποβληθεί επιτυχώς. Στην περίπτωσή μας, καλεί τη μέθοδο `contactFormSucceeded`, η οποία θα αναλάβει την επεξεργασία της υποβληθείσας φόρμας. Αυτό θα το συμπληρώσουμε στον κώδικα σε μια στιγμή. - -Το component `contactForm` θα το αφήσουμε να αποδοθεί στο template `Home/default.latte`: - -```latte -{block content} -<h1>Φόρμα επικοινωνίας</h1> -{control contactForm} -``` - -Για την ίδια την αποστολή του email θα δημιουργήσουμε μια νέα κλάση, την οποία θα ονομάσουμε `ContactFacade` και θα την τοποθετήσουμε στο αρχείο `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // το email σας - ->setFrom($email, $name) - ->setSubject('Μήνυμα από τη φόρμα επικοινωνίας') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Η μέθοδος `sendMessage()` δημιουργεί και στέλνει το email. Χρησιμοποιεί για αυτό τον λεγόμενο mailer, τον οποίο λαμβάνει ως εξάρτηση μέσω του constructor. Διαβάστε περισσότερα για την [αποστολή emails |mail:]. - -Τώρα θα επιστρέψουμε στον presenter και θα ολοκληρώσουμε τη μέθοδο `contactFormSucceeded()`. Αυτή θα καλέσει τη μέθοδο `sendMessage()` της κλάσης `ContactFacade` και θα της παραδώσει τα δεδομένα από τη φόρμα. Και πώς θα αποκτήσουμε το αντικείμενο `ContactFacade`; Θα το ζητήσουμε να μας παραδοθεί μέσω του constructor: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Το μήνυμα στάλθηκε'); - $this->redirect('this'); - } -} -``` - -Αφού σταλεί το email, θα εμφανίσουμε επίσης στον χρήστη ένα λεγόμενο [flash message |application:components#Flash Μηνύματα], επιβεβαιώνοντας ότι το μήνυμα στάλθηκε, και στη συνέχεια θα ανακατευθύνουμε σε άλλη σελίδα, ώστε να μην είναι δυνατή η επανειλημμένη αποστολή της φόρμας μέσω *refresh* στον browser. - - -Λοιπόν, και αν όλα λειτουργούν, θα πρέπει να μπορείτε να στείλετε email από τη φόρμα επικοινωνίας σας. Συγχαρητήρια! - - -HTML template email -------------------- - -Μέχρι στιγμής, αποστέλλεται ένα απλό email κειμένου που περιέχει μόνο το μήνυμα που στάλθηκε από τη φόρμα. Στο email όμως μπορούμε να χρησιμοποιήσουμε HTML και να κάνουμε την εμφάνισή του πιο ελκυστική. Θα δημιουργήσουμε γι' αυτό ένα template στο Latte, το οποίο θα γράψουμε στο `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Μήνυμα από τη φόρμα επικοινωνίας - - -

    Όνομα: {$name}

    -

    E-mail: {$email}

    -

    Μήνυμα: {$message}

    - - -``` - -Μένει να τροποποιήσουμε το `ContactFacade`, ώστε να χρησιμοποιεί αυτό το template. Στον constructor θα ζητήσουμε την κλάση `LatteFactory`, η οποία μπορεί να δημιουργήσει ένα αντικείμενο `Latte\Engine`, δηλαδή τον [Latte template renderer |latte:develop#Πώς να Αποδώσετε ένα Πρότυπο]. Με τη μέθοδο `renderToString()` θα αποδώσουμε το template σε αρχείο, η πρώτη παράμετρος είναι η διαδρομή προς το template και η δεύτερη είναι οι μεταβλητές. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // το email σας - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Το παραγόμενο HTML email θα το παραδώσουμε στη συνέχεια στη μέθοδο `setHtmlBody()` αντί της αρχικής `setBody()`. Επίσης, δεν χρειάζεται να αναφέρουμε το θέμα του email στο `setSubject()`, επειδή η βιβλιοθήκη θα το πάρει από το στοιχείο `` του template. - - -Διαμόρφωση ----------- - -Στον κώδικα της κλάσης `ContactFacade` είναι ακόμα σκληρά κωδικοποιημένο το διαχειριστικό μας email `admin@example.com`. Θα ήταν καλύτερο να το μεταφέρουμε στο αρχείο διαμόρφωσης. Πώς να το κάνουμε αυτό; - -Πρώτα θα τροποποιήσουμε την κλάση `ContactFacade` και θα αντικαταστήσουμε το string με το email με μια μεταβλητή που παραδίδεται μέσω του constructor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Και το δεύτερο βήμα είναι η αναφορά της τιμής αυτής της μεταβλητής στη διαμόρφωση. Στο αρχείο `app/config/services.neon` γράφουμε: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Και αυτό είναι όλο. Αν τα στοιχεία στην ενότητα `services` ήταν πολλά και είχατε την αίσθηση ότι το email χάνεται ανάμεσά τους, μπορούμε να το κάνουμε μεταβλητή. Τροποποιούμε την καταχώρηση σε: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Και στο αρχείο `app/config/common.neon` ορίζουμε αυτή τη μεταβλητή: - -```neon -parameters: - adminEmail: admin@example.com -``` - -Και τελειώσαμε! diff --git a/best-practices/el/microsites.texy b/best-practices/el/microsites.texy deleted file mode 100644 index 2224119362..0000000000 --- a/best-practices/el/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Πώς να γράφετε μικρο-ιστοσελίδες -******************************** - -Φανταστείτε ότι χρειάζεστε να δημιουργήσετε γρήγορα μια μικρή ιστοσελίδα για την επερχόμενη εκδήλωση της εταιρείας σας. Πρέπει να είναι απλό, γρήγορο και χωρίς περιττές πολυπλοκότητες. Ίσως σκέφτεστε ότι για ένα τόσο μικρό έργο δεν χρειάζεστε ένα στιβαρό framework. Αλλά τι γίνεται αν η χρήση του Nette framework μπορεί να απλοποιήσει και να επιταχύνει θεμελιωδώς αυτή τη διαδικασία; - -Ακόμα και κατά τη δημιουργία απλών ιστοσελίδων, δεν θέλετε να εγκαταλείψετε την άνεση. Δεν θέλετε να εφευρίσκετε αυτό που έχει ήδη λυθεί μία φορά. Μείνετε ήσυχα τεμπέλης και αφήστε τον εαυτό σας να κακομάθει. Το Nette Framework μπορεί να χρησιμοποιηθεί εξαιρετικά και ως micro framework. - -Πώς μπορεί να μοιάζει ένα τέτοιο microsite; Για παράδειγμα, έτσι ώστε ολόκληρος ο κώδικας της ιστοσελίδας να τοποθετηθεί σε ένα μόνο αρχείο `index.php` στον δημόσιο φάκελο: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// δημιουργία DI container βάσει της διαμόρφωσης στο config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// ορίζουμε το routing -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// route για το URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // ανιχνεύουμε τη γλώσσα του browser και ανακατευθύνουμε στο URL /en ή /de κ.λπ. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// route για το URL https://example.com/cs ή https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // εμφανίζουμε το αντίστοιχο template, για παράδειγμα ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// εκκίνηση της εφαρμογής! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Όλα τα υπόλοιπα θα είναι templates αποθηκευμένα στον γονικό φάκελο `/templates`. - -Ο κώδικας PHP στο `index.php` πρώτα [προετοιμάζει το περιβάλλον |bootstrap:], στη συνέχεια ορίζει τις [routes |application:routing#Δυναμική δρομολόγηση με callbacks] και τέλος εκκινεί την εφαρμογή. Το πλεονέκτημα είναι ότι η δεύτερη παράμετρος της συνάρτησης `addRoute()` μπορεί να είναι ένα callable, το οποίο εκτελείται μετά το άνοιγμα της αντίστοιχης σελίδας. - - -Γιατί να χρησιμοποιήσετε το Nette για microsite; ------------------------------------------------- - -- Οι προγραμματιστές που έχουν δοκιμάσει ποτέ το [Tracy |tracy:] δεν μπορούν σήμερα να φανταστούν ότι θα προγραμματίσουν κάτι χωρίς αυτό. -- Πάνω απ' όλα, όμως, θα χρησιμοποιήσετε το σύστημα προτύπων [Latte |latte:], επειδή ήδη από 2 σελίδες θα θέλετε να έχετε ξεχωριστή [διάταξη και περιεχόμενο |latte:template-inheritance]. -- Και σίγουρα θέλετε να βασιστείτε στο [αυτόματο escaping |latte:safety-first], ώστε να μην προκύψει ευπάθεια XSS -- Το Nette επίσης εξασφαλίζει ότι σε περίπτωση σφάλματος δεν θα εμφανιστούν ποτέ τα μηνύματα σφαλμάτων PHP για προγραμματιστές, αλλά μια κατανοητή σελίδα για τον χρήστη. -- Αν θέλετε να λαμβάνετε ανατροφοδότηση από τους χρήστες, για παράδειγμα με τη μορφή μιας φόρμας επικοινωνίας, τότε θα προσθέσετε επίσης [φόρμες |forms:] και [βάση δεδομένων |database:]. -- Μπορείτε επίσης εύκολα να [στείλετε μέσω email |mail:] τις συμπληρωμένες φόρμες. -- Μερικές φορές μπορεί να σας φανεί χρήσιμο το [caching |caching:], για παράδειγμα αν κατεβάζετε και εμφανίζετε feeds. - -Στη σημερινή εποχή, όπου η ταχύτητα και η αποτελεσματικότητα είναι καθοριστικής σημασίας, είναι σημαντικό να έχετε εργαλεία που σας επιτρέπουν να επιτύχετε αποτελέσματα χωρίς περιττές καθυστερήσεις. Το Nette framework σας προσφέρει ακριβώς αυτό - γρήγορη ανάπτυξη, ασφάλεια και ένα ευρύ φάσμα εργαλείων, όπως το Tracy και το Latte, που απλοποιούν τη διαδικασία. Αρκεί να εγκαταστήσετε μερικά πακέτα Nette και η κατασκευή ενός τέτοιου microsite γίνεται ξαφνικά παιχνιδάκι. Και ξέρετε ότι πουθενά δεν κρύβεται καμία τρύπα ασφαλείας. diff --git a/best-practices/el/pagination.texy b/best-practices/el/pagination.texy deleted file mode 100644 index cdefb763e4..0000000000 --- a/best-practices/el/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Σελίδωση αποτελεσμάτων βάσης δεδομένων -************************************** - -.[perex] -Κατά τη δημιουργία web εφαρμογών, πολύ συχνά θα συναντήσετε την απαίτηση για περιορισμό του αριθμού των εμφανιζόμενων στοιχείων ανά σελίδα. - -Θα ξεκινήσουμε από την κατάσταση όπου εμφανίζουμε όλα τα δεδομένα χωρίς σελίδωση. Για την επιλογή δεδομένων από τη βάση δεδομένων έχουμε την κλάση ArticleRepository, η οποία εκτός από τον constructor περιέχει τη μέθοδο `findPublishedArticles`, η οποία επιστρέφει όλα τα δημοσιευμένα άρθρα ταξινομημένα φθίνοντα κατά ημερομηνία δημοσίευσης. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -Στον presenter, στη συνέχεια, κάνουμε inject την κλάση του μοντέλου και στη μέθοδο render ζητάμε τα δημοσιευμένα άρθρα, τα οποία περνάμε στο template: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -Στο template `default.latte` φροντίζουμε στη συνέχεια για την εμφάνιση των άρθρων: - -```latte -{block content} -<h1>Άρθρα</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Με αυτόν τον τρόπο μπορούμε να εμφανίσουμε όλα τα άρθρα, πράγμα που όμως αρχίζει να δημιουργεί προβλήματα τη στιγμή που ο αριθμός των άρθρων αυξάνεται. Σε εκείνη τη στιγμή, έρχεται βολική η υλοποίηση ενός μηχανισμού σελίδωσης. - -Αυτός εξασφαλίζει ότι όλα τα άρθρα θα χωριστούν σε αρκετές σελίδες και εμείς θα εμφανίσουμε μόνο τα άρθρα μιας τρέχουσας σελίδας. Τον συνολικό αριθμό σελίδων και τη διαίρεση των άρθρων θα τον υπολογίσει ο [Paginator |utils:Paginator] μόνος του ανάλογα με το πόσα άρθρα έχουμε συνολικά και πόσα άρθρα ανά σελίδα θέλουμε να εμφανίσουμε. - -Στο πρώτο βήμα, θα τροποποιήσουμε τη μέθοδο για την απόκτηση άρθρων στην κλάση του repository έτσι ώστε να μπορεί να μας επιστρέφει μόνο άρθρα για μία σελίδα. Θα προσθέσουμε επίσης μια μέθοδο για τη διαπίστωση του συνολικού αριθμού άρθρων στη βάση δεδομένων, την οποία θα χρειαστούμε για τη ρύθμιση του Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Επιστρέφει τον συνολικό αριθμό δημοσιευμένων άρθρων - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Στη συνέχεια, θα προχωρήσουμε στις τροποποιήσεις του presenter. Στη μέθοδο render θα περνάμε τον αριθμό της τρέχουσας εμφανιζόμενης σελίδας. Για την περίπτωση που αυτός ο αριθμός δεν θα είναι μέρος του URL, θα ορίσουμε την προεπιλεγμένη τιμή της πρώτης σελίδας. - -Επίσης, θα επεκτείνουμε τη μέθοδο render με την απόκτηση της παρουσίας του Paginator, τη ρύθμισή του και την επιλογή των σωστών άρθρων για εμφάνιση στο template. Ο HomePresenter μετά τις τροποποιήσεις θα μοιάζει ως εξής: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Θα διαπιστώσουμε τον συνολικό αριθμό δημοσιευμένων άρθρων - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Θα δημιουργήσουμε μια παρουσία του Paginator και θα τον ρυθμίσουμε - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // συνολικός αριθμός άρθρων - $paginator->setItemsPerPage(10); // αριθμός στοιχείων ανά σελίδα - $paginator->setPage($page); // αριθμός τρέχουσας σελίδας - - // Από τη βάση δεδομένων θα τραβήξουμε ένα περιορισμένο σύνολο άρθρων σύμφωνα με τον υπολογισμό του Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // το οποίο θα περάσουμε στο template - $this->template->articles = $articles; - // και επίσης τον ίδιο τον Paginator για την εμφάνιση των επιλογών σελίδωσης - $this->template->paginator = $paginator; - } -} -``` - -Το template μας ήδη τώρα επαναλαμβάνεται μόνο πάνω στα άρθρα μιας σελίδας, αρκεί να προσθέσουμε τους συνδέσμους σελίδωσης: - -```latte -{block content} -<h1>Άρθρα</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Πρώτη</a> -  |  - <a n:href="default, $paginator->page-1">Προηγούμενη</a> -  |  - {/if} - - Σελίδα {$paginator->getPage()} από {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Επόμενη</a> -  |  - <a n:href="default, $paginator->getPageCount()">Τελευταία</a> - {/if} -</div> -``` - - -Έτσι συμπληρώσαμε τη σελίδα με τη δυνατότητα σελίδωσης χρησιμοποιώντας τον Paginator. Στην περίπτωση που αντί του [Nette Database Core |database:sql-way] ως επίπεδο βάσης δεδομένων χρησιμοποιήσουμε το [Nette Database Explorer |database:explorer], είμαστε σε θέση να υλοποιήσουμε τη σελίδωση και χωρίς τη χρήση του Paginator. Η κλάση `Nette\Database\Table\Selection` περιέχει τη μέθοδο [page |api:Nette\Database\Table\Selection::_page] με τη λογική σελίδωσης που έχει ληφθεί από τον Paginator. - -Το repository με αυτόν τον τρόπο υλοποίησης θα μοιάζει ως εξής: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -Στον presenter δεν χρειάζεται να δημιουργήσουμε Paginator, θα χρησιμοποιήσουμε αντί γι' αυτόν τη μέθοδο της κλάσης `Selection`, την οποία μας επιστρέφει το repository: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Θα τραβήξουμε τα δημοσιευμένα άρθρα - $articles = $this->articleRepository->findPublishedArticles(); - - // και στο template θα στείλουμε μόνο το μέρος τους που περιορίζεται σύμφωνα με τον υπολογισμό της μεθόδου page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // και επίσης τα απαραίτητα δεδομένα για την εμφάνιση των επιλογών σελίδωσης - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Επειδή στο template τώρα δεν στέλνουμε τον Paginator, θα τροποποιήσουμε το μέρος που εμφανίζει τους συνδέσμους σελίδωσης: - -```latte -{block content} -<h1>Άρθρα</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Πρώτη</a> -  |  - <a n:href="default, $page - 1">Προηγούμενη</a> -  |  - {/if} - - Σελίδα {$page} από {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Επόμενη</a> -  |  - <a n:href="default, $lastPage">Τελευταία</a> - {/if} -</div> -``` - -Με αυτόν τον τρόπο υλοποιήσαμε τον μηχανισμό σελίδωσης χωρίς τη χρήση του Paginator. - -{{priority: -1}} diff --git a/best-practices/el/passing-settings-to-presenters.texy b/best-practices/el/passing-settings-to-presenters.texy deleted file mode 100644 index f600bbb498..0000000000 --- a/best-practices/el/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Πέρασμα ρυθμίσεων στους presenters -********************************** - -.[perex] -Χρειάζεστε να περάσετε ορίσματα στους presenters που δεν είναι αντικείμενα (π.χ. πληροφορία αν τρέχουν σε debug mode, διαδρομές προς καταλόγους κ.λπ.), και επομένως δεν μπορούν να περαστούν αυτόματα μέσω autowiring; Η λύση είναι να τα ενσωματώσετε σε ένα αντικείμενο `Settings`. - -Η υπηρεσία `Settings` αποτελεί έναν πολύ εύκολο και ταυτόχρονα χρήσιμο τρόπο παροχής πληροφοριών σχετικά με την τρέχουσα εφαρμογή στους presenters. Η συγκεκριμένη της μορφή εξαρτάται αποκλειστικά από τις δικές σας συγκεκριμένες ανάγκες. Παράδειγμα: - -```php -namespace App; - -class Settings -{ - public function __construct( - // από την PHP 8.1 είναι δυνατό να δηλωθεί readonly - public bool $debugMode, - public string $appDir, - // και ούτω καθεξής - ) {} -} -``` - -Παράδειγμα καταχώρησης στη διαμόρφωση: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Όταν ο presenter χρειαστεί τις πληροφορίες που παρέχονται από αυτή την υπηρεσία, απλά θα τη ζητήσει στον constructor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/el/post-links.texy b/best-practices/el/post-links.texy deleted file mode 100644 index a6147e263a..0000000000 --- a/best-practices/el/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Πώς να χρησιμοποιείτε σωστά τους συνδέσμους POST -************************************************ - -.[perex] -Σε web εφαρμογές, ειδικά σε διαχειριστικά interfaces, θα έπρεπε να είναι βασικός κανόνας ότι οι ενέργειες που αλλάζουν την κατάσταση του server δεν θα έπρεπε να εκτελούνται μέσω της μεθόδου HTTP GET. Όπως υποδηλώνει το όνομα της μεθόδου, η GET θα έπρεπε να χρησιμεύει μόνο για τη λήψη δεδομένων, όχι για την αλλαγή τους. Για ενέργειες όπως η διαγραφή εγγραφών, είναι προτιμότερη η χρήση της μεθόδου POST. Αν και η ιδανική θα ήταν η μέθοδος DELETE, αλλά αυτή δεν μπορεί να κληθεί χωρίς JavaScript, γι' αυτό ιστορικά χρησιμοποιείται η POST. - -Πώς να το κάνετε στην πράξη; Χρησιμοποιήστε αυτό το απλό κόλπο. Στην αρχή του template, δημιουργήστε μια βοηθητική φόρμα με το αναγνωριστικό `postForm`, την οποία στη συνέχεια θα χρησιμοποιήσετε για τα κουμπιά διαγραφής: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Χάρη σε αυτή τη φόρμα, μπορείτε αντί για τον κλασικό σύνδεσμο `<a>` να χρησιμοποιήσετε ένα κουμπί `<button>`, το οποίο μπορεί να διαμορφωθεί οπτικά ώστε να μοιάζει με συνηθισμένο σύνδεσμο. Για παράδειγμα, το CSS framework Bootstrap προσφέρει τις κλάσεις `btn btn-link` με τις οποίες επιτυγχάνετε το κουμπί να μην διαφέρει οπτικά από τους άλλους συνδέσμους. Με το attribute `form="postForm"` το συνδέουμε με την προετοιμασμένη φόρμα: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button> - <!-- instead of <a n:href="delete $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Κατά το κλικ στον σύνδεσμο, καλείται τώρα η ενέργεια `delete`. Για να διασφαλίσετε ότι τα αιτήματα θα γίνονται δεκτά μόνο μέσω της μεθόδου POST και από τον ίδιο τομέα (που είναι μια αποτελεσματική άμυνα κατά των επιθέσεων CSRF), χρησιμοποιήστε το attribute `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // υποθετικός κώδικας που διαγράφει την εγγραφή - $this->redirect('default'); - } -} -``` - -Το attribute υπάρχει από το Nette Application 3.2 και περισσότερα για τις δυνατότητές του θα μάθετε στη σελίδα [Πώς να χρησιμοποιήσετε το attribute #Requires |attribute-requires]. - -Αν αντί για την ενέργεια `actionDelete()` χρησιμοποιούσατε το σήμα `handleDelete()`, δεν είναι απαραίτητο να αναφέρετε `sameOrigin: true`, επειδή τα σήματα έχουν αυτή την προστασία ρυθμισμένη από προεπιλογή: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Αυτή η προσέγγιση όχι μόνο βελτιώνει την ασφάλεια της εφαρμογής σας, αλλά συμβάλλει επίσης στην τήρηση των σωστών web προτύπων και πρακτικών. Χρησιμοποιώντας τις μεθόδους POST για ενέργειες που αλλάζουν την κατάσταση, επιτυγχάνετε μια πιο στιβαρή και ασφαλή εφαρμογή. diff --git a/best-practices/el/presenter-traits.texy b/best-practices/el/presenter-traits.texy deleted file mode 100644 index b5c9e24f28..0000000000 --- a/best-practices/el/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Σύνθεση presenters από traits -***************************** - -.[perex] -Αν χρειαζόμαστε να υλοποιήσουμε τον ίδιο κώδικα σε περισσότερους presenters (π.χ. έλεγχος ότι ο χρήστης είναι συνδεδεμένος), προσφέρεται η τοποθέτηση του κώδικα σε έναν κοινό πρόγονο. Η δεύτερη δυνατότητα είναι η δημιουργία μονοσκοπικών [traits |nette:introduction-to-object-oriented-programming#Traits]. - -Το πλεονέκτημα αυτής της λύσης είναι ότι καθένας από τους presenters μπορεί να χρησιμοποιήσει ακριβώς τα traits που πραγματικά χρειάζεται, ενώ η πολλαπλή κληρονομικότητα δεν είναι δυνατή στην PHP. - -Αυτά τα traits μπορούν να εκμεταλλευτούν το γεγονός ότι κατά τη δημιουργία του presenter καλούνται διαδοχικά όλες οι [μέθοδοι inject |inject-method-attribute#Μέθοδοι inject]. Απλά πρέπει να διασφαλιστεί ότι το όνομα κάθε μεθόδου inject είναι μοναδικό. - -Τα traits μπορούν να επισυνάψουν κώδικα αρχικοποίησης στα events [onStartup ή onRender |application:presenters#Γεγονότα]. - -Παραδείγματα: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Ο presenter στη συνέχεια χρησιμοποιεί απλά αυτά τα traits: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/el/restore-request.texy b/best-practices/el/restore-request.texy deleted file mode 100644 index 631a0c432b..0000000000 --- a/best-practices/el/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Πώς να επιστρέψετε σε προηγούμενη σελίδα; -***************************************** - -.[perex] -Τι γίνεται αν ο χρήστης συμπληρώνει μια φόρμα και η σύνδεσή του λήξει; Για να μην χάσει τα δεδομένα, πριν την ανακατεύθυνση στη σελίδα σύνδεσης, αποθηκεύουμε τα δεδομένα στο session. Στο Nette αυτό είναι παιχνιδάκι. - -Το τρέχον αίτημα μπορεί να αποθηκευτεί στο session χρησιμοποιώντας τη μέθοδο `storeRequest()`, η οποία επιστρέφει το αναγνωριστικό του με τη μορφή ενός σύντομου string. Η μέθοδος αποθηκεύει το όνομα του τρέχοντος presenter, την προβολή και τις παραμέτρους του. Σε περίπτωση που έχει υποβληθεί και φόρμα, αποθηκεύεται επίσης το περιεχόμενο των πεδίων (με εξαίρεση τα ανεβασμένα αρχεία). - -Η επαναφορά του αιτήματος γίνεται με τη μέθοδο `restoreRequest($key)`, στην οποία περνάμε το ληφθέν αναγνωριστικό. Αυτή ανακατευθύνει στον αρχικό presenter και προβολή. Αν όμως το αποθηκευμένο αίτημα περιέχει υποβολή φόρμας, μεταβαίνει στον αρχικό presenter με τη μέθοδο `forward()`, παραδίδει στη φόρμα τις προηγουμένως συμπληρωμένες τιμές και την αφήνει να αποδοθεί ξανά. Ο χρήστης έτσι έχει τη δυνατότητα να υποβάλει ξανά τη φόρμα και κανένα δεδομένο δεν χάνεται. - -Σημαντικό είναι ότι το `restoreRequest()` ελέγχει αν ο νέος συνδεδεμένος χρήστης είναι ο ίδιος που συμπλήρωσε αρχικά τη φόρμα. Αν όχι, απορρίπτει το αίτημα και δεν κάνει τίποτα. - -Θα δείξουμε τα πάντα με ένα παράδειγμα. Έστω ότι έχουμε έναν presenter `AdminPresenter`, στον οποίο επεξεργαζόμαστε δεδομένα και στη μέθοδο `startup()` του οποίου ελέγχουμε αν ο χρήστης είναι συνδεδεμένος. Αν δεν είναι, τον ανακατευθύνουμε στον `SignPresenter`. Ταυτόχρονα αποθηκεύουμε το τρέχον αίτημα και στέλνουμε το κλειδί του στον `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Ο presenter `SignPresenter` θα περιέχει εκτός από τη φόρμα σύνδεσης και μια persistent παράμετρο `$backlink`, στην οποία θα γραφτεί το κλειδί. Επειδή η παράμετρος είναι persistent, θα μεταφέρεται και μετά την υποβολή της φόρμας σύνδεσης. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... προσθέτουμε πεδία φόρμας ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... εδώ συνδέουμε τον χρήστη ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Στη μέθοδο `restoreRequest()` περνάμε το κλειδί του αποθηκευμένου αιτήματος και αυτή ανακατευθύνει (ή μεταβαίνει) στον αρχικό presenter. - -Αν όμως το κλειδί είναι άκυρο (για παράδειγμα δεν υπάρχει πλέον στο session), η μέθοδος δεν κάνει τίποτα. Ακολουθεί επομένως η κλήση `$this->redirect('Admin:')`, η οποία ανακατευθύνει στον `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/en/@home.texy b/best-practices/en/@home.texy index 04508b55aa..630ab6a882 100644 --- a/best-practices/en/@home.texy +++ b/best-practices/en/@home.texy @@ -19,6 +19,7 @@ Nette Application - [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> @@ -42,7 +43,6 @@ General - [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] -- [Tips for Editors & Tools |editors-and-tools] - [Introduction to Object-Oriented Programming |nette:introduction-to-object-oriented-programming] </div> @@ -63,7 +63,7 @@ Example Solutions Videos ------ -Hundreds of recordings from Last Saturday meetups and videos about Nette can be found all in one place on the "Nette Framework YouTube Channel":https://www.youtube.com/user/NetteFramework. +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> diff --git a/best-practices/en/@left-menu.texy b/best-practices/en/@left-menu.texy new file mode 100644 index 0000000000..cb2b5244eb --- /dev/null +++ b/best-practices/en/@left-menu.texy @@ -0,0 +1,34 @@ +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 index 10c126830b..cc2101524f 100644 --- a/best-practices/en/@meta.texy +++ b/best-practices/en/@meta.texy @@ -1,2 +1 @@ {{sitename: Tutorials and Best Practices}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/en/attribute-requires.texy b/best-practices/en/attribute-requires.texy index 54da7db490..129c6f6563 100644 --- a/best-practices/en/attribute-requires.texy +++ b/best-practices/en/attribute-requires.texy @@ -137,7 +137,7 @@ If you want to use the `#[Requires]` attribute repeatedly with the same settings For example, `#[SingleAction]` allows access only through the `default` action: ```php -#[Attribute] +#[\Attribute] class SingleAction extends Nette\Application\Attributes\Requires { public function __construct() @@ -155,7 +155,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter Or `#[RestMethods]` will allow access via all HTTP methods used for the REST API: ```php -#[Attribute] +#[\Attribute] class RestMethods extends Nette\Application\Attributes\Requires { public function __construct() diff --git a/best-practices/en/composer.texy b/best-practices/en/composer.texy index 1e08a82668..ab81e44d8a 100644 --- a/best-practices/en/composer.texy +++ b/best-practices/en/composer.texy @@ -87,8 +87,8 @@ php composer-frontline.php ``` -Creating New Project -==================== +Creating a New Project +====================== You can create a new Nette project using a single command: @@ -161,7 +161,7 @@ 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 +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. diff --git a/best-practices/en/creating-editing-form.texy b/best-practices/en/creating-editing-form.texy index 9349473ca3..86003dffc6 100644 --- a/best-practices/en/creating-editing-form.texy +++ b/best-practices/en/creating-editing-form.texy @@ -29,11 +29,11 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... add form fields ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // add record to the database $this->flashMessage('Successfully added'); @@ -91,11 +91,11 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... add form fields ... $form->setDefaults($this->record); // set default values - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->update($this->record->id, $data); // update record $this->flashMessage('Successfully updated'); @@ -130,7 +130,6 @@ We store the record in the `$record` property, making it available in the `creat $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! @@ -153,7 +152,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void @@ -168,7 +167,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter $form = $this->getComponent('recordForm'); $form->setDefaults($record); // set default values - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form @@ -185,14 +184,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // add record to the database $this->flashMessage('Successfully added'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); $this->facade->update($id, $data); // update record diff --git a/best-practices/en/dynamic-snippets.texy b/best-practices/en/dynamic-snippets.texy index 144d00268a..499b0c70c4 100644 --- a/best-practices/en/dynamic-snippets.texy +++ b/best-practices/en/dynamic-snippets.texy @@ -1,6 +1,9 @@ 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 @@ -64,7 +67,7 @@ In Latte terminology, a dynamic snippet refers to a specific use of the `{snippe 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. +All that remains is to add the redrawing logic to the presenter - simply redraw the static wrapper. ```php public function handleLike(int $articleId): void @@ -106,7 +109,7 @@ public function renderDefault(): void } ``` -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. +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 @@ -134,7 +137,7 @@ class LikeControl extends Nette\Application\UI\Control } ``` -Template of component: +The component's template: ```latte {snippet} diff --git a/best-practices/en/editors-and-tools.texy b/best-practices/en/editors-and-tools.texy deleted file mode 100644 index c6109dcbab..0000000000 --- a/best-practices/en/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editors & Tools -*************** - -.[perex] -You might be a skilled programmer, but good tools are what make you a master. This chapter provides tips on essential tools, editors, and plugins. - - -IDE Editor -========== - -We strongly recommend using a full-featured IDE for development, like PhpStorm, NetBeans, or VS Code, rather than just a text editor with PHP support. The difference is truly significant. There's no reason to settle for a basic editor that only offers syntax highlighting when you can have a top-tier IDE providing accurate code suggestions, error checking, refactoring capabilities, and much more. Some IDEs are paid, while others are free. - -**NetBeans IDE** has built-in support for Nette, Latte, and NEON. - -**PhpStorm**: Install these plugins via `Settings > Plugins > Marketplace`: -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: Find the "Nette Latte + Neon" plugin in the marketplace. - -Also, integrate Tracy with your editor. When an error page is displayed, clicking on file names will open them directly in your editor at the corresponding line. Learn [how to configure this feature |tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan is a static analysis tool that detects logical errors in your code before you even run it. - -Install it using Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Create a configuration file `phpstan.neon` in your project: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Then, let it analyze the classes within the `app/` directory: - -```shell -vendor/bin/phpstan analyse app -``` - -You can find comprehensive documentation directly on the [PHPStan website |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] checks and potentially fixes some formal errors in your source code: - -- removes [BOM |nette:glossary#BOM] -- checks the validity of [Latte |latte:] templates -- checks the validity of `.neon`, `.php`, and `.json` files -- checks for [control characters |nette:glossary#Control Characters] -- checks if the file is encoded in UTF-8 -- checks for incorrectly written `/* @annotations */` (missing second asterisk) -- removes trailing `?>` PHP tags from files containing only PHP code -- removes trailing whitespace and unnecessary blank lines at the end of files -- normalizes line endings to the system default (using the `-l` option) - - -Composer -======== - -[Composer] is a tool for dependency management in PHP. It allows you to declare the libraries your project depends on and manages their installation and updates. - - -Requirements Checker -==================== - -This was a tool that tested the server's runtime environment and indicated whether (and to what extent) the framework could be used. Currently, Nette can be used on any server that meets the minimum required PHP version. diff --git a/best-practices/en/form-reuse.texy b/best-practices/en/form-reuse.texy index c2027ab6f0..2d91c006fc 100644 --- a/best-practices/en/form-reuse.texy +++ b/best-practices/en/form-reuse.texy @@ -193,11 +193,11 @@ class EditFormFactory $form->addText('title', 'Title:'); // additional form fields are added here $form->addSubmit('send', 'Save'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // processing of submitted data @@ -284,12 +284,12 @@ class EditControl extends Nette\Application\UI\Control $form->addText('title', 'Title:'); // additional form fields are added here $form->addSubmit('send', 'Save'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // processing of submitted data diff --git a/best-practices/en/inject-method-attribute.texy b/best-practices/en/inject-method-attribute.texy index b1c36d8df1..f37c76402b 100644 --- a/best-practices/en/inject-method-attribute.texy +++ b/best-practices/en/inject-method-attribute.texy @@ -44,7 +44,7 @@ A presenter can have any number of `inject*()` methods, and each can accept any 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) +Properties are marked with an attribute (previously, the `/** @inject */` annotation was used): ```php use Nette\DI\Attributes\Inject; // this line is important diff --git a/best-practices/en/lets-create-contact-form.texy b/best-practices/en/lets-create-contact-form.texy index dab7982265..52a0df88c1 100644 --- a/best-practices/en/lets-create-contact-form.texy +++ b/best-practices/en/lets-create-contact-form.texy @@ -21,14 +21,14 @@ class HomePresenter extends Presenter ->setRequired('Please enter your name'); $form->addEmail('email', 'E-mail:') ->setRequired('Please enter your email'); - $form->addTextarea('message', 'Message:') + $form->addTextArea('message', 'Message:') ->setRequired('Please enter a message'); $form->addSubmit('send', 'Send'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // sending an email } @@ -48,9 +48,6 @@ Let's render the `contactForm` component in the `Home/default.latte` template: For sending the email itself, we'll create a new class named `ContactFacade` and place it in the file `app/Model/ContactFacade.php`: ```php -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -204,7 +201,7 @@ services: - App\Model\ContactFacade(adminEmail: admin@example.com) ``` -And that's it. If the `services` section contains many items and you feel the email address gets lost among them, we can turn it into a parameter. Modify the entry like this: +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: diff --git a/best-practices/en/microsites.texy b/best-practices/en/microsites.texy index cc4e038ff6..cd9c96b841 100644 --- a/best-practices/en/microsites.texy +++ b/best-practices/en/microsites.texy @@ -29,11 +29,11 @@ $router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { // detect browser language and redirect to URL /en or /de etc. $supportedLangs = ['en', 'de', 'cs']; $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); // route for URL https://example.com/cs or https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { +$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'); @@ -60,4 +60,4 @@ Why use Nette for Microsites? - 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. +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. diff --git a/best-practices/en/pagination.texy b/best-practices/en/pagination.texy index 7f5e0db062..04622fe991 100644 --- a/best-practices/en/pagination.texy +++ b/best-practices/en/pagination.texy @@ -164,7 +164,7 @@ The template now iterates only over the articles for the current page. We just n {if !$paginator->isFirst()} <a n:href="default, 1">First</a>  |  - <a n:href="default, $paginator->page-1">Previous</a> + <a n:href="default, $paginator->getPage() - 1">Previous</a>  |  {/if} @@ -180,7 +180,7 @@ The template now iterates only over the articles for the current page. We just n ``` -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 incorporates the pagination logic. +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: diff --git a/best-practices/en/pretty-urls.texy b/best-practices/en/pretty-urls.texy new file mode 100644 index 0000000000..8dd3d9addc --- /dev/null +++ b/best-practices/en/pretty-urls.texy @@ -0,0 +1,204 @@ +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. diff --git a/best-practices/en/restore-request.texy b/best-practices/en/restore-request.texy index 6a95370d1c..f374e7be20 100644 --- a/best-practices/en/restore-request.texy +++ b/best-practices/en/restore-request.texy @@ -8,7 +8,7 @@ The current request can be stored in the session using the `storeRequest()` meth 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 discarded, and the method does nothing, enhancing security. +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`. @@ -41,11 +41,11 @@ class SignPresenter extends Nette\Application\UI\Presenter { $form = new Nette\Application\UI\Form; // ... add form fields ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { // ... log the user in here ... diff --git a/best-practices/es/@home.texy b/best-practices/es/@home.texy index 6a6ee9df47..5300606b2c 100644 --- a/best-practices/es/@home.texy +++ b/best-practices/es/@home.texy @@ -1,24 +1,25 @@ -Tutoriales y procedimientos -*************************** +Tutoriales y buenas prácticas +***************************** .[perex] -Tutoriales, soluciones a tareas comunes y *best practices* para Nette. +Tutoriales, soluciones a tareas habituales y buenas prácticas para Nette. <div class=documentation> <div> -Aplicación Nette ----------------- +Nette Application +----------------- - [Métodos y atributos inject |inject-method-attribute] -- [Composición de presenters a partir de traits |presenter-traits] -- [Pasar configuraciones a los presenters |passing-settings-to-presenters] +- [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 base de datos |pagination] +- [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> @@ -28,7 +29,7 @@ Formularios ----------- - [Reutilización de formularios |form-reuse] - [Formulario para crear y editar registros |creating-editing-form] -- [Creando un formulario de contacto |lets-create-contact-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> @@ -38,19 +39,18 @@ Formularios General ------- - [Cómo cargar un archivo de configuración |bootstrap:] -- [Cómo escribir micro-sitios web |microsites] +- [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 para su uso |composer] -- [Consejos sobre editores y herramientas |editors-and-tools] +- [Composer: consejos de uso |composer] - [Introducción a la programación orientada a objetos |nette:introduction-to-object-oriented-programming] </div> <div> -Solución de ejemplo -------------------- +Ejemplos de soluciones +---------------------- - [Nette examples |https://github.com/nette-examples] - [Doctrine & Nette |https://contributte.org/nettrine/] - [Contributte examples |https://contributte.org/examples.html] @@ -63,7 +63,7 @@ Solución de ejemplo Vídeos ------ -Cientos de grabaciones de los Últimos Sábados y vídeos sobre Nette se pueden encontrar bajo un mismo techo en el "Canal de Youtube de Nette Framework":https://www.youtube.com/user/NetteFramework. +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 new file mode 100644 index 0000000000..fdc33e5ade --- /dev/null +++ b/best-practices/es/@left-menu.texy @@ -0,0 +1,34 @@ +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 index 524cb19ad0..16230381cf 100644 --- a/best-practices/es/@meta.texy +++ b/best-practices/es/@meta.texy @@ -1,2 +1 @@ -{{sitename: Tutoriales y procedimientos}} -{{leftbar: www:@menu-common}} +{{sitename: Tutoriales y buenas prácticas}} diff --git a/best-practices/es/attribute-requires.texy b/best-practices/es/attribute-requires.texy index 0eebe50721..fed7bec56e 100644 --- a/best-practices/es/attribute-requires.texy +++ b/best-practices/es/attribute-requires.texy @@ -2,30 +2,30 @@ Cómo usar el atributo `#[Requires]` *********************************** .[perex] -Cuando escribe una aplicación web, a menudo se encuentra con la necesidad de restringir el acceso a ciertas partes de su aplicación. Quizás quiera que algunas peticiones solo puedan enviar datos mediante un formulario (es decir, con el método POST), o que sean accesibles solo para llamadas AJAX. En Nette Framework 3.2 apareció una nueva herramienta que le permitirá establecer tales restricciones de manera muy elegante y clara: el atributo `#[Requires]`. +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 una marca especial en PHP que agrega antes de la definición de una clase o método. Como en realidad es una clase, para que los siguientes ejemplos funcionen, es necesario indicar la cláusula use: +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; ``` -Puede usar el atributo `#[Requires]` en la propia clase del presenter y también en estos métodos: +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 también se aplican a los componentes, por lo que también puede usar el atributo en ellos. +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 que indica el atributo, se producirá un error HTTP 4xx. +Si no se cumplen las condiciones indicadas por el atributo, se lanza un error HTTP 4xx. Métodos HTTP ------------ -Puede especificar qué métodos HTTP (como GET, POST, etc.) están permitidos para el acceso. Por ejemplo, si desea permitir el acceso solo enviando un formulario, establezca: +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 @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -¿Por qué debería usar POST en lugar de GET para acciones que cambian el estado y cómo hacerlo? [Lea el tutorial |post-links]. +¿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, lo cual los presenters estándarmente [no permiten por razones de seguridad |application:presenters#Verificación del método HTTP]. +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]. -Llamada AJAX ------------- +Llamadas AJAX +------------- -Si desea que el presenter o el método esté disponible solo para peticiones AJAX, use: +Si quiere que un presenter o un método sea accesible solo para peticiones AJAX, use: ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ class AjaxPresenter extends Nette\Application\UI\Presenter Mismo origen ------------ -Para aumentar la seguridad, puede requerir que la petición se realice desde el mismo dominio. Con esto evitará la [vulnerabilidad CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: +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)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -Para los métodos `handle<Signal>()`, el acceso desde el mismo dominio se requiere automáticamente. Así que si, por el contrario, desea permitir el acceso desde cualquier dominio, indique: +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)] @@ -77,10 +77,10 @@ public function handleList(): void ``` -Acceso a través de forward --------------------------- +Acceso mediante forward +----------------------- -A veces es útil restringir el acceso a un presenter para que esté disponible solo indirectamente, por ejemplo, usando el método `forward()` o `switch()` desde otro presenter. Así se protegen, por ejemplo, los error-presenters, para que no sea posible invocarlos desde la URL: +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)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -En la práctica, a menudo es necesario marcar ciertas vistas a las que solo se puede acceder en función de la lógica en el presenter. Es decir, nuevamente, para que no sea posible abrirlas directamente: +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 @@ -111,10 +111,10 @@ class ProductPresenter extends Nette\Application\UI\Presenter ``` -Acciones específicas --------------------- +Acciones concretas +------------------ -También puede restringir que cierto código, como la creación de un componente, esté disponible solo para acciones específicas en el presenter: +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 @@ -126,15 +126,15 @@ class EditDeletePresenter extends Nette\Application\UI\Presenter } ``` -En caso de una sola acción, no es necesario escribir un array: `#[Requires(actions: 'default')]` +En el caso de una sola acción no hace falta escribir un array: `#[Requires(actions: 'default')]` -Atributos personalizados ------------------------- +Atributos propios +----------------- -Si desea usar el atributo `#[Requires]` repetidamente con la misma configuración, puede crear su propio atributo que herede `#[Requires]` y lo configure según sus necesidades. +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]` permitirá el acceso solo a través de la acción `default`: +Por ejemplo, `#[SingleAction]` permite el acceso solo a través de la acción `default`: ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -O `#[RestMethods]` permitirá el acceso a través de todos los métodos HTTP utilizados para la API REST: +O `#[RestMethods]` permitirá el acceso mediante todos los métodos HTTP usados por la API REST: ```php #[\Attribute] @@ -174,4 +174,4 @@ class ApiPresenter extends Nette\Application\UI\Presenter Conclusión ---------- -El atributo `#[Requires]` le da una gran flexibilidad y control sobre cómo son accesibles sus páginas web. Usando reglas simples pero potentes, puede aumentar la seguridad y el correcto funcionamiento de su aplicación. Como puede ver, el uso de atributos en Nette no solo puede facilitar su trabajo, sino también asegurarlo. +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 index 20959bbc87..715d2c4ca3 100644 --- a/best-practices/es/composer.texy +++ b/best-practices/es/composer.texy @@ -1,12 +1,12 @@ -Composer: consejos para su uso -****************************** +Composer: consejos de uso +************************* <div class=perex> -Composer es una herramienta para gestionar dependencias en PHP. Nos permite enumerar las librerías de las que depende nuestro proyecto, y las instalará y actualizará por nosotros. Mostraremos: +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 -- su uso en un proyecto nuevo o existente +- cómo usarlo en un proyecto nuevo o ya existente </div> @@ -14,31 +14,31 @@ Composer es una herramienta para gestionar dependencias en PHP. Nos permite enum Instalación =========== -Composer es un archivo `.phar` ejecutable, que descarga e instala de la siguiente manera: +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]. +Use el instalador oficial [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. Linux, macOS ------------ -Bastarán 4 comandos, que puede copiar de [esta página |https://getcomposer.org/download/]. +Bastan 4 comandos, que puede copiar de [esta página |https://getcomposer.org/download/]. -Además, insertándolo en una carpeta que esté en el `PATH` del sistema, Composer se volverá accesible globalmente: +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 el proyecto +Uso en un proyecto ================== -Para poder empezar a usar Composer en su proyecto, necesita solo el archivo `composer.json`. Este describe las dependencias de nuestro proyecto y también puede contener otros metadatos. Un `composer.json` básico, por lo tanto, puede verse así: +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 { @@ -48,17 +48,17 @@ Para poder empezar a usar Composer en su proyecto, necesita solo el archivo `com } ``` -Aquí decimos que nuestra aplicación (o librería) requiere el paquete `nette/database` (el nombre del paquete se compone del nombre de la organización y el nombre del proyecto) y quiere una versión que cumpla la condición `^3.0` (es decir, la última versión 3). +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). -Tenemos, por lo tanto, en la raíz del proyecto el archivo `composer.json` y ejecutamos la instalación: +Así pues, con el archivo `composer.json` en la raíz del proyecto, ejecute: ```shell composer update ``` -Composer descargará Nette Database en la carpeta `vendor/`. Además, creará el archivo `composer.lock`, que contiene información sobre qué versiones exactas de las librerías instaló. +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 generará el archivo `vendor/autoload.php`, que podemos simplemente incluir y empezar a usar las librerías sin ningún trabajo adicional: +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'; @@ -67,52 +67,52 @@ $db = new Nette\Database\Connection('sqlite::memory:'); ``` -Actualización de paquetes a las últimas versiones -================================================= +Actualizar los paquetes a las últimas versiones +=============================================== -La actualización de las librerías usadas a las últimas versiones según las condiciones definidas en `composer.json` está a cargo del comando `composer update`. Por ejemplo, para la dependencia `"nette/database": "^3.0"` instalará la última versión 3.x.x, pero ya no la versión 4. +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 condiciones en el archivo `composer.json`, por ejemplo a `"nette/database": "^4.1"`, para poder instalar la última versión, use el comando `composer require nette/database`. +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 Nette usados sería necesario enumerarlos todos en la línea de comandos, p. ej.: +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 ... ``` -Lo cual es poco práctico. Use por lo tanto el script simple "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, que lo hará por usted: +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 ``` -Creación de un nuevo proyecto -============================= +Crear un proyecto nuevo +======================= -Creará un nuevo proyecto en Nette con un solo comando: +Puede crear un proyecto nuevo de Nette con un solo comando: ```shell -composer create-project nette/web-project nombre-proyecto +composer create-project nette/web-project name-of-the-project ``` -Como `nombre-proyecto` inserte el nombre del directorio para su proyecto y confirme. Composer descargará el repositorio `nette/web-project` de GitHub, que ya contiene el archivo `composer.json`, e inmediatamente después Nette Framework. Ya debería bastar con [establecer los permisos |nette:troubleshooting#Configuración de permisos de directorio] de escritura en las carpetas `temp/` y `log/` y el proyecto debería cobrar vida. +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á el proyecto, no olvide [configurarla |#Versión de PHP]. +Si sabe en qué versión de PHP se alojará su proyecto, no olvide [indicarla |#Versión de PHP]. Versión de PHP ============== -Composer siempre instala aquellas versiones de paquetes que son compatibles con la versión de PHP que está usando actualmente (mejor dicho, con la versión de PHP usada en la línea de comandos al ejecutar Composer). Lo cual, sin embargo, probablemente no sea la misma versión que usa su hosting. Por lo tanto, es muy importante agregar al archivo `composer.json` información sobre la versión de PHP en el hosting. Después, solo se instalarán versiones de paquetes compatibles con el hosting. +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. -Que el proyecto se ejecutará, por ejemplo, en PHP 8.2.3, lo configuramos con el comando: +Por ejemplo, para indicar que el proyecto funcionará en PHP 8.2.3, use el comando: ```shell composer config platform.php 8.2.3 ``` -Así se escribe la versión en el archivo `composer.json`: +La versión se escribirá en el archivo `composer.json` así: ```js { @@ -124,7 +124,7 @@ Así se escribe la versión en el archivo `composer.json`: } ``` -Sin embargo, el número de versión de PHP se indica también en otro lugar del archivo, en la sección `require`. Mientras que el primer número determina para qué versión se instalarán los paquetes, el segundo número dice para qué versión está escrita la propia aplicación. Y según él, por ejemplo, PhpStorm establece el *PHP language level*. (Por supuesto, no tiene sentido que estas versiones difieran, por lo que la doble escritura es una falta de previsión.) Esta versión la establece con el comando: +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 @@ -144,69 +144,69 @@ O directamente en el archivo `composer.json`: Ignorar la versión de PHP ========================= -Los paquetes generalmente suelen tener indicada tanto la versión más baja de PHP con la que son compatibles, como la más alta con la que están probados. Si se dispone a usar una versión de PHP aún más nueva, por ejemplo, con fines de prueba, Composer se negará a instalar tal paquete. 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. +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. -Informes falsos -=============== +Avisos falsos +============= -Al actualizar paquetes o cambiar números de versión, sucede que se produce un conflicto. Un paquete tiene requisitos que están en conflicto con otro y similares. Composer, sin embargo, a veces emite informes falsos. Informa de un conflicto que realmente no existe. En tal caso, ayuda eliminar el archivo `composer.lock` e intentarlo de nuevo. +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, entonces se toma en serio y es necesario leer de él qué y cómo modificar. +Si el mensaje de error persiste, es real y hay que leerlo para entender qué hay que modificar y cómo. -Packagist.org - repositorio central -=================================== +Packagist.org: el repositorio global +==================================== -[Packagist |https://packagist.org] es el repositorio principal en el que Composer intenta buscar paquetes, si no le decimos lo contrario. Aquí también podemos publicar nuestros propios paquetes. +[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 usar el repositorio central? ----------------------------------------------- +¿Y si no queremos el repositorio central? +----------------------------------------- -Si tenemos aplicaciones internas de la empresa, que simplemente no podemos alojar públicamente, entonces crearemos un repositorio de empresa para ellas. +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 el tema de repositorios [en la documentación oficial |https://getcomposer.org/doc/05-repositories.md#repositories]. +Más sobre los repositorios en [la documentación oficial |https://getcomposer.org/doc/05-repositories.md#repositories]. Autoloading =========== -Una característica fundamental de Composer es que proporciona autoloading para todas las clases instaladas por él, que inicia incluyendo el archivo `vendor/autoload.php`. +Una característica clave de Composer es que proporciona autoloading para todas las clases que instala. Se activa incluyendo el archivo `vendor/autoload.php`. -Sin embargo, es posible usar Composer también para cargar otras clases incluso fuera de la carpeta `vendor`. La primera opción es dejar que Composer explore las carpetas y subcarpetas definidas, encuentre todas las clases y las incluya en el autoloader. Esto se logra configurando `autoload > classmap` en `composer.json`: +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 la carpeta src/ y sus subcarpetas + "src/", # incluye el directorio src/ y sus subdirectorios ] } } ``` -Posteriormente, es necesario ejecutar el comando `composer dumpautoload` cada vez que se realice un cambio y dejar que las tablas de autoloading se regeneren. Esto es extremadamente incómodo y es mucho mejor confiar esta tarea a [RobotLoader|robot-loader:], que realiza la misma actividad automáticamente en segundo plano y mucho más rápido. +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 cumplir con [PSR-4|https://www.php-fig.org/psr/psr-4/]. Simplificando, se trata de un sistema donde los espacios de nombres y los nombres de las clases corresponden a la estructura de directorios y los nombres de los archivos, es decir, p. ej., `App\Core\RouterFactory` estará en el archivo `/path/to/App/Core/RouterFactory.php`. Ejemplo de configuración: +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/ + "App\\": "app/" # el espacio de nombres App\ está en el directorio app/ } } } ``` -Cómo configurar exactamente el comportamiento se aprende en la [documentación de Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. +Consulte la [documentación de Composer |https://getcomposer.org/doc/04-schema.md#psr-4] para saber cómo configurar este comportamiento. -Prueba de nuevas versiones -========================== +Probar versiones nuevas +======================= -Quiere probar una nueva versión de desarrollo de un paquete. ¿Cómo hacerlo? Primero, agregue al archivo `composer.json` este par de opciones, que permiten instalar versiones de desarrollo de paquetes, pero recurrirá a ello solo si no existe ninguna combinación de versiones estables que cumpla los requisitos: +¿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 { @@ -215,33 +215,33 @@ Quiere probar una nueva versión de desarrollo de un paquete. ¿Cómo hacerlo? P } ``` -Además, recomendamos eliminar el archivo `composer.lock`, a veces Composer inexplicablemente se niega a la instalación y esto resuelve el problema. +También recomendamos borrar el archivo `composer.lock`, porque Composer a veces se niega a instalar sin explicación y esto puede resolverlo. -Supongamos que se trata del paquete `nette/utils` y la nueva versión tiene el número 4.0. La instala con el comando: +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 específica, por ejemplo 4.0.0-RC2: +O puede instalar una versión concreta, por ejemplo la 4.0.0-RC2: ```shell composer require nette/utils:4.0.0-RC2 ``` -Pero si otro paquete depende de la librería, que está bloqueado en una versión anterior (p. ej., `^3.1`), entonces lo ideal es actualizar el paquete para que funcione con la nueva versión. Sin embargo, si solo quiere eludir la restricción y forzar a Composer a instalar la versión de desarrollo y fingir que es una versión anterior (p. ej., 3.1.6), puede usar la palabra clave `as`: +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" ``` -Llamada de comandos -=================== +Llamar a comandos +================= -A través de Composer se pueden llamar comandos y scripts propios pre-preparados, como si fueran comandos nativos de Composer. Para los scripts que se encuentran en la carpeta `vendor/bin`, no es necesario indicar esta carpeta. +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, definimos en el archivo `composer.json` un script que usando [Nette Tester|tester:] ejecuta las pruebas: +Como ejemplo, definamos en `composer.json` un script que use [Nette Tester |tester:] para ejecutar los tests: ```js { @@ -251,19 +251,19 @@ Como ejemplo, definimos en el archivo `composer.json` un script que usando [Nett } ``` -Las pruebas luego las ejecutamos con `composer tester`. El comando podemos llamarlo incluso si no estamos en la carpeta raíz del proyecto, sino en algún subdirectorio. +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. -Envíe un agradecimiento -======================= +Dar las gracias +=============== -Le mostraremos un truco con el que complacerá a los autores de código abierto. De manera simple, dará una estrella en GitHub a las librerías que usa su proyecto. Basta con instalar la librería `symfony/thanks`: +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 luego ejecutar: +Y después ejecutar: ```shell composer thanks @@ -275,7 +275,7 @@ composer thanks Configuración ============= -Composer está estrechamente vinculado con la herramienta de versionado [Git |https://git-scm.com]. Si no la tiene instalada, es necesario decirle a Composer que no la use: +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 index 9d44a9d52f..0d7af59ac5 100644 --- a/best-practices/es/creating-editing-form.texy +++ b/best-practices/es/creating-editing-form.texy @@ -2,15 +2,15 @@ Formulario para crear y editar un registro ****************************************** .[perex] -¿Cómo implementar correctamente la adición y edición de un registro en Nette, utilizando el mismo formulario para ambos? +¿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 un registro son los mismos, diferenciándose quizás sólo en la etiqueta del botón. Mostraremos ejemplos de Presenters simples donde usaremos el formulario primero para añadir un registro, luego para editarlo, y finalmente combinaremos ambas soluciones. +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 que sirve para añadir un registro. Dejaremos el trabajo real con la base de datos a la clase `Facade`, cuyo código no es esencial para la demostración. +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 @@ -27,16 +27,16 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $form = new Form; - // ... añadimos los campos del formulario ... + // ... añadir los campos del formulario ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // añadir registro a la base de datos - $this->flashMessage('Añadido correctamente'); + $this->facade->add($data); // añade el registro a la base de datos + $this->flashMessage('Successfully added'); $this->redirect('...'); } @@ -51,7 +51,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter Editar un registro ------------------ -Ahora mostraremos cómo sería un Presenter para editar un registro: +Veamos ahora qué aspecto tendría un presenter para editar un registro: ```php @@ -70,10 +70,10 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // verificar la existencia del registro - || !$this->facade->isEditAllowed(/*...*/) // comprobar permisos + !$record // comprueba que el registro existe + || !$this->facade->isEditAllowed(/*...*/) // comprueba los permisos ) { - $this->error(); // error 404 + $this->error(); // error 404 } $this->record = $record; @@ -81,32 +81,32 @@ class RecordPresenter extends Nette\Application\UI\Presenter protected function createComponentRecordForm(): Form { - // verificar que la acción es 'edit' + // comprueba que la acción es 'edit' if ($this->getAction() !== 'edit') { $this->error(); } $form = new Form; - // ... añadimos los campos del formulario ... + // ... añadir los campos del formulario ... - $form->setDefaults($this->record); // establecer valores por defecto - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->setDefaults($this->record); // establece los valores predeterminados + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->update($this->record->id, $data); // actualizar el registro - $this->flashMessage('Actualizado correctamente'); + $this->facade->update($this->record->id, $data); // actualiza el registro + $this->flashMessage('Successfully updated'); $this->redirect('...'); } } ``` -En el método *action*, que se ejecuta al principio del [ciclo de vida del Presenter |application:presenters#Ciclo de vida del presenter], verificamos la existencia del registro y los permisos del usuario para editarlo. +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`, para tenerlo disponible en el método `createComponentRecordForm()` para establecer los valores por defecto, y en `recordFormSucceeded()` para el ID. Una solución alternativa sería establecer los valores por defecto directamente en `actionEdit()` y obtener el valor del ID, que forma parte de la URL, usando `getParameter('id')`: +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 @@ -114,12 +114,12 @@ Guardamos el registro en la propiedad `$record`, para tenerlo disponible en el m { $record = $this->facade->get($id); if ( - // verificar existencia y comprobar permisos + // comprueba la existencia y los permisos ) { $this->error(); } - // establecer valores por defecto del formulario + // establece los valores predeterminados del formulario $this->getComponent('recordForm') ->setDefaults($record); } @@ -130,16 +130,15 @@ Guardamos el registro en la propiedad `$record`, para tenerlo disponible en el m $this->facade->update($id, $data); // ... } -} ``` -Sin embargo, y esto debería ser **el punto más importante de todo el código**, debemos asegurarnos al crear el formulario de que la acción es realmente `edit`. ¡Porque de lo contrario, la verificación en el método `actionEdit()` no se realizaría en absoluto! +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 ---------------------------------------- -Y ahora combinaremos ambos Presenters en uno. Podríamos distinguir en el método `createComponentRecordForm()` de qué acción se trata y configurar el formulario en consecuencia, o podemos dejarlo directamente en los métodos action y deshacernos de la condición: +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 @@ -153,50 +152,50 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // verificar la existencia del registro - || !$this->facade->isEditAllowed(/*...*/) // comprobar permisos + !$record // comprueba que el registro existe + || !$this->facade->isEditAllowed(/*...*/) // comprueba los permisos ) { - $this->error(); // error 404 + $this->error(); // error 404 } $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // establecer valores por defecto - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->setDefaults($record); // establece los valores predeterminados + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form { - // verificamos que la acción es 'add' o 'edit' + // comprueba que la acción es 'add' o 'edit' if (!in_array($this->getAction(), ['add', 'edit'])) { $this->error(); } $form = new Form; - // ... añadimos los campos del formulario ... + // ... añadir los campos del formulario ... return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // añadir registro a la base de datos - $this->flashMessage('Añadido correctamente'); + $this->facade->add($data); // añade el registro a la base de datos + $this->flashMessage('Successfully added'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // actualizar el registro - $this->flashMessage('Actualizado correctamente'); + $this->facade->update($id, $data); // actualiza el registro + $this->flashMessage('Successfully updated'); $this->redirect('...'); } } diff --git a/best-practices/es/dynamic-snippets.texy b/best-practices/es/dynamic-snippets.texy index 37ca43eec5..2861259396 100644 --- a/best-practices/es/dynamic-snippets.texy +++ b/best-practices/es/dynamic-snippets.texy @@ -1,7 +1,10 @@ Snippets dinámicos ****************** -Con bastante frecuencia, durante el desarrollo de aplicaciones, surge la necesidad de realizar operaciones AJAX, por ejemplo, sobre filas individuales de una tabla o elementos de una lista. Como ejemplo, podemos elegir mostrar artículos, permitiendo a cada usuario conectado elegir una calificación de "me gusta/no me gusta". El código del Presenter y la plantilla correspondiente sin AJAX se verían aproximadamente así (presento los fragmentos más importantes, el código asume la existencia de un servicio para marcar calificaciones y obtener una colección de artículos; la implementación específica no es importante para los fines de este tutorial): +.[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 @@ -24,26 +27,26 @@ Plantilla: <h2>{$article->title}</h2> <div class="content">{$article->content}</div> {if !$article->liked} - <a n:href="like! $article->id" class=ajax>me gusta</a> + <a n:href="like! $article->id" class=ajax>I like it</a> {else} - <a n:href="unlike! $article->id" class=ajax>ya no me gusta</a> + <a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a> {/if} </article> ``` -Ajaxificación -============= +Ajaxización +=========== -Ahora equipemos esta sencilla aplicación con AJAX. El cambio de calificación de un artículo no es tan importante como para requerir una redirección, por lo que idealmente debería ocurrir mediante AJAX en segundo plano. Utilizaremos [el script de manejo de complementos |application:ajax#Naja] con la convención habitual de que los enlaces AJAX tienen la clase CSS `ajax`. +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 hacerlo específicamente? Nette ofrece 2 enfoques: el enfoque de los llamados snippets dinámicos y el enfoque de los componentes. Ambos tienen sus pros y sus contras, por lo que los mostraremos uno por uno. +¿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. -El enfoque de los snippets dinámicos -==================================== +La vía de los snippets dinámicos +================================ -Un snippet dinámico, en la terminología de Latte, significa un caso específico de uso de la etiqueta `{snippet}`, donde se utiliza una variable en el nombre del snippet. Dicho snippet no puede encontrarse en cualquier lugar de la plantilla; debe estar envuelto por un snippet estático, es decir, uno normal, o dentro de `{snippetArea}`. Podríamos modificar nuestra plantilla de la siguiente manera. +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 @@ -53,18 +56,18 @@ Un snippet dinámico, en la terminología de Latte, significa un caso específic <div class="content">{$article->content}</div> {snippet article-{$article->id}} {if !$article->liked} - <a n:href="like! $article->id" class=ajax>me gusta</a> + <a n:href="like! $article->id" class=ajax>I like it</a> {else} - <a n:href="unlike! $article->id" class=ajax>ya no me gusta</a> + <a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a> {/if} {/snippet} </article> {/snippet} ``` -Cada artículo ahora define un snippet que tiene el ID del artículo en su nombre. Todos estos snippets se envuelven juntos en un snippet llamado `articlesContainer`. Si omitiéramos este snippet envolvente, Latte nos advertiría con una excepción. +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. -Nos queda añadir el redibujado al Presenter; basta con redibujar el envoltorio estático. +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 @@ -72,18 +75,18 @@ public function handleLike(int $articleId): void $this->ratingService->saveLike($articleId, $this->user->id); if ($this->isAjax()) { $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- no es necesario + // $this->redrawControl('article-' . $articleId); -- no hace falta } else { $this->redirect('this'); } } ``` -Modificamos de manera similar el método hermano `handleUnlike()`, ¡y AJAX funciona! +Modifique de forma parecida el método `handleUnlike()` correspondiente y ¡AJAX ya funciona! -Sin embargo, la solución tiene un inconveniente. Si investigáramos más a fondo cómo se procesa la solicitud AJAX, descubriríamos que aunque la aplicación parece eficiente externamente (devuelve solo un snippet para el artículo dado), en realidad renderizó todos los snippets en el servidor. Colocó el snippet deseado en el payload y descartó los demás (obteniéndolos también innecesariamente de la base de datos). +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 optimizar este proceso, tendremos que intervenir donde pasamos la colección `$articles` a la plantilla (digamos, en el método `renderDefault()`). Aprovecharemos el hecho de que el procesamiento de señales ocurre antes de los métodos `render<Something>`: +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 @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -Ahora, al procesar la señal, en lugar de la colección con todos los artículos, se pasa a la plantilla solo un array con un único artículo: el que queremos renderizar y enviar en el payload al navegador. Por lo tanto, `{foreach}` se ejecutará solo una vez y no se renderizarán snippets adicionales. +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. -El enfoque de los componentes -============================= +La vía de los componentes +========================= -Una forma completamente diferente de resolverlo evita los snippets dinámicos. El truco consiste en trasladar toda la lógica a un componente separado: a partir de ahora, no será el Presenter el que se encargue de introducir las calificaciones, sino un `LikeControl` dedicado. La clase se verá así (además, también contendrá los métodos `render`, `handleUnlike`, etc.): +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 @@ -139,14 +142,14 @@ Plantilla del componente: ```latte {snippet} {if !$article->liked} - <a n:href="like!" class=ajax>me gusta</a> + <a n:href="like!" class=ajax>I like it</a> {else} - <a n:href="unlike!" class=ajax>ya no me gusta</a> + <a n:href="unlike!" class=ajax>I don't like it anymore</a> {/if} {/snippet} ``` -Por supuesto, la plantilla de la vista cambiará y tendremos que añadir una fábrica al Presenter. Dado que crearemos el componente tantas veces como artículos obtengamos de la base de datos, utilizaremos la clase [application:Multiplier] para su "multiplicación". +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() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -La plantilla de la vista se reduce al mínimo indispensable (¡y completamente libre de snippets!): +La plantilla de la vista se reduce al mínimo (¡y queda completamente libre de snippets!): ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ La plantilla de la vista se reduce al mínimo indispensable (¡y completamente l </article> ``` -Casi hemos terminado: la aplicación ahora funcionará con AJAX. Aquí también tendremos que optimizar la aplicación, porque debido al uso de Nette Database, al procesar la señal se cargan innecesariamente todos los artículos de la base de datos en lugar de uno solo. Sin embargo, la ventaja es que no se renderizarán, ya que realmente solo se renderizará nuestro componente. +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/editors-and-tools.texy b/best-practices/es/editors-and-tools.texy deleted file mode 100644 index 36d9c62866..0000000000 --- a/best-practices/es/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editores y herramientas -*********************** - -.[perex] -Puedes ser un programador competente, pero solo con buenas herramientas te convertirás en un maestro. En este capítulo encontrarás consejos sobre herramientas, editores y plugins importantes. - - -Editor IDE -========== - -Recomendamos encarecidamente utilizar un IDE completo para el desarrollo, como PhpStorm, NetBeans, VS Code, y no solo un editor de texto con soporte para PHP. La diferencia es realmente fundamental. No hay razón para conformarse con un simple editor que colorea la sintaxis pero no alcanza las capacidades de un IDE de primer nivel, que sugiere con precisión, detecta errores, puede refactorizar código y mucho más. Algunos IDE son de pago, otros incluso gratuitos. - -**NetBeans IDE** ya tiene soporte integrado para Nette, Latte y NEON. - -**PhpStorm**: instala estos plugins en `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: busca el plugin "Nette Latte + Neon" en el marketplace. - -También vincula Tracy con tu editor. Cuando se muestre una página de error, podrás hacer clic en los nombres de los archivos y se abrirán en el editor con el cursor en la línea correspondiente. Lee [cómo configurar el sistema|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan es una herramienta que detecta errores lógicos en el código antes de ejecutarlo. - -Lo instalamos usando Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Creamos un archivo de configuración `phpstan.neon` en el proyecto: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Y luego le pedimos que analice las clases en la carpeta `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Encontrarás documentación exhaustiva directamente en el [sitio web de PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] comprueba y, opcionalmente, corrige algunos de los errores formales en tus códigos fuente: - -- elimina [BOM |nette:glossary#BOM] -- comprueba la validez de las plantillas [Latte |latte:] -- comprueba la validez de los archivos `.neon`, `.php` y `.json` -- comprueba la presencia de [caracteres de control |nette:glossary#Caracteres de control] -- comprueba si el archivo está codificado en UTF-8 -- comprueba `/* @anotaciones */` mal escritas (falta el asterisco) -- elimina el `?>` final de los archivos PHP -- elimina los espacios finales y las líneas innecesarias al final del archivo -- normaliza los separadores de línea a los del sistema (si se especifica la opción `-l`) - - -Composer -======== - -[Composer |Composer] es una herramienta para gestionar dependencias en PHP. Nos permite declarar dependencias arbitrariamente complejas de bibliotecas individuales y luego las instala por nosotros en nuestro proyecto. - - -Requirements Checker -==================== - -Era una herramienta que probaba el entorno de ejecución del servidor e informaba si (y en qué medida) se podía utilizar el framework. Actualmente, Nette se puede utilizar en cualquier servidor que tenga la versión mínima requerida de PHP. diff --git a/best-practices/es/form-reuse.texy b/best-practices/es/form-reuse.texy index 3998002707..b26c80c6d8 100644 --- a/best-practices/es/form-reuse.texy +++ b/best-practices/es/form-reuse.texy @@ -2,15 +2,15 @@ Reutilización de formularios en múltiples lugares ************************************************* .[perex] -En Nette, tienes varias opciones para usar el mismo formulario en múltiples lugares sin duplicar código. En este artículo, mostraremos diferentes soluciones, incluidas aquellas que deberías evitar. +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. -Fábrica de formularios +Factory de formularios ====================== -Uno de los enfoques básicos para usar el mismo componente en múltiples lugares es crear un método o clase que genere este componente y luego llamar a este método en diferentes lugares de la aplicación. Tal método o clase se llama *fábrica*. Por favor, no lo confundas con el patrón de diseño *factory method*, que describe una forma específica de usar fábricas y no está relacionado con este tema. +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, crearemos una fábrica que construirá un formulario de edición: +Como ejemplo, creemos una factory que construya un formulario de edición: ```php use Nette\Application\UI\Form; @@ -20,22 +20,22 @@ class FormFactory public function createEditForm(): Form { $form = new Form; - $form->addText('title', 'Título:'); - // aquí se añaden otros campos del formulario - $form->addSubmit('send', 'Enviar'); + $form->addText('title', 'Title:'); + // aquí se añaden más campos del formulario + $form->addSubmit('send', 'Save'); return $form; } } ``` -Ahora puedes usar esta fábrica en diferentes lugares de tu aplicación, por ejemplo, en Presenters o componentes. Y lo haces [solicitándola como dependencia|dependency-injection:passing-dependencies]. Primero, registramos la clase en el archivo de configuración: +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 luego la usamos en un Presenter: +Y después úsela en el presenter: ```php @@ -57,7 +57,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Puedes extender la fábrica de formularios con métodos adicionales para crear otros tipos de formularios según las necesidades de tu aplicación. Y, por supuesto, también podemos añadir un método que cree un formulario base sin elementos, que los otros métodos utilizarán: +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 @@ -71,21 +71,21 @@ class FormFactory public function createEditForm(): Form { $form = $this->createForm(); - $form->addText('title', 'Título:'); - // aquí se añaden otros campos del formulario - $form->addSubmit('send', 'Enviar'); + $form->addText('title', 'Title:'); + // aquí se añaden más campos del formulario + $form->addSubmit('send', 'Save'); return $form; } } ``` -El método `createForm()` aún no hace nada útil, pero eso cambiará rápidamente. +El método `createForm()` todavía no hace nada demasiado útil, pero eso cambiará enseguida. -Dependencias de la fábrica +Dependencias de la factory ========================== -Con el tiempo, resultará que necesitamos que los formularios sean multilingües. Esto significa que debemos establecer el llamado [traductor |forms:rendering#Traducción] para todos los formularios. Para ello, modificaremos la clase `FormFactory` para que acepte un objeto `Translator` como dependencia en el constructor y lo pasaremos al formulario: +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; @@ -108,13 +108,13 @@ class FormFactory } ``` -Dado que el método `createForm()` también es llamado por otros métodos que crean formularios específicos, basta con establecer el traductor solo en él. Y hemos terminado. No es necesario cambiar el código de ningún Presenter o componente, lo cual es genial. +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últiples clases de fábrica -=========================== +Más clases factory +================== -Alternativamente, puedes crear múltiples clases para cada formulario que quieras usar en tu aplicación. Este enfoque puede aumentar la legibilidad del código y facilitar la gestión de los formularios. Dejaremos que la `FormFactory` original cree solo un formulario limpio con la configuración básica (por ejemplo, con soporte para traducciones) y crearemos una nueva fábrica `EditFormFactory` para el formulario de edición. +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 @@ -133,7 +133,7 @@ class FormFactory } -// ✅ uso de composición +// ✅ usando composición class EditFormFactory { public function __construct( @@ -144,39 +144,39 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - // aquí se añaden otros campos del formulario - $form->addSubmit('send', 'Enviar'); + // aquí se añaden más campos del formulario + $form->addSubmit('send', 'Save'); return $form; } } ``` -Es muy importante que la relación entre las clases `FormFactory` y `EditFormFactory` se realice mediante [composición |nette:introduction-to-object-oriented-programming#Composición], y no mediante [herencia de objetos |nette:introduction-to-object-oriented-programming#Herencia]: +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 -// ⛔ ¡ASÍ NO! LA HERENCIA NO PERTENECE AQUÍ +// ⛔ ¡NO! LA HERENCIA NO PINTA NADA AQUÍ class EditFormFactory extends FormFactory { public function create(): Form { $form = parent::create(); - $form->addText('title', 'Título:'); - // aquí se añaden otros campos del formulario - $form->addSubmit('send', 'Enviar'); + $form->addText('title', 'Title:'); + // aquí se añaden más campos del formulario + $form->addSubmit('send', 'Save'); return $form; } } ``` -Usar la herencia en este caso sería completamente contraproducente. Te encontrarías con problemas muy rápidamente. Por ejemplo, en el momento en que quisieras añadir parámetros al método `create()`; PHP informaría de un error indicando que su firma difiere de la del padre. O al pasar dependencias a la clase `EditFormFactory` a través del constructor. Se produciría una situación que llamamos [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. +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 sobre la herencia |dependency-injection:faq#Por qué se prefiere la composición sobre la herencia]. +En general, es mejor preferir la [composición a la herencia |dependency-injection:faq#¿Por qué se prefiere la composición a la herencia?]. -Manejo del formulario -===================== +Procesamiento del formulario +============================ -El manejador del formulario, que se llama después de un envío exitoso, también puede ser parte de la clase de fábrica. Funcionará pasando los datos enviados al modelo para su procesamiento. Los posibles errores se [pasarán de vuelta |forms:validation#Errores durante el procesamiento] al formulario. El modelo en el siguiente ejemplo está representado por la clase `Facade`: +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 @@ -190,14 +190,14 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - $form->addText('title', 'Título:'); - // aquí se añaden otros campos del formulario - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addText('title', 'Title:'); + // aquí se añaden más campos del formulario + $form->addSubmit('send', 'Save'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // procesamiento de los datos enviados @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Sin embargo, dejaremos la redirección real al Presenter. Este añadirá otro manejador al evento `onSuccess` que realizará la redirección. Gracias a esto, será posible usar el formulario en diferentes Presenters y redirigir a un lugar diferente en cada uno. +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 @@ -224,7 +224,7 @@ class MyPresenter extends Nette\Application\UI\Presenter { $form = $this->formFactory->create(); $form->onSuccess[] = function () { - $this->flashMessage('El registro ha sido guardado'); + $this->flashMessage('Record was saved'); $this->redirect('Homepage:'); }; return $form; @@ -232,38 +232,38 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Esta solución aprovecha la propiedad de los formularios de que cuando se llama a `addError()` en el formulario o en uno de sus elementos, el siguiente manejador `onSuccess` ya no se llama. +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. -Herencia de la clase Form -========================= +Heredar de la clase Form +======================== -Un formulario construido no debe ser un descendiente del formulario. En otras palabras, no uses esta solución: +Un formulario ya montado no debería ser descendiente de la clase `Form`. Dicho de otro modo, evite este enfoque: ```php -// ⛔ ¡ASÍ NO! LA HERENCIA NO PERTENECE AQUÍ +// ⛔ ¡NO! LA HERENCIA NO PINTA NADA AQUÍ class EditForm extends Form { public function __construct(Translator $translator) { parent::__construct(); - $this->addText('title', 'Título:'); - // aquí se añaden otros campos del formulario - $this->addSubmit('send', 'Enviar'); + $this->addText('title', 'Title:'); + // aquí se añaden más campos del formulario + $this->addSubmit('send', 'Save'); $this->setTranslator($translator); } } ``` -En lugar de construir el formulario en el constructor, usa una fábrica. +En lugar de montar el formulario en el constructor, use una factory. -Es necesario darse cuenta de que la clase `Form` es, ante todo, una herramienta para construir un formulario, es decir, un *form builder*. Y el formulario construido puede entenderse como su producto. Pero el producto no es un caso específico del constructor, no hay una relación *es un* entre ellos que forme la base de la herencia. +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 con formulario -========================= +Componente de formulario +======================== -Un enfoque completamente diferente es la creación de un [componente|application:components] que incluya un formulario. Esto ofrece nuevas posibilidades, por ejemplo, renderizar el formulario de una manera específica, ya que el componente también incluye una plantilla. O se pueden usar señales para la comunicación AJAX y la carga de información en el formulario, por ejemplo, para sugerencias, etc. +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 @@ -281,15 +281,15 @@ class EditControl extends Nette\Application\UI\Control protected function createComponentForm(): Form { $form = new Form; - $form->addText('title', 'Título:'); - // aquí se añaden otros campos del formulario - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addText('title', 'Title:'); + // aquí se añaden más campos del formulario + $form->addSubmit('send', 'Save'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // procesamiento de los datos enviados @@ -300,13 +300,13 @@ class EditControl extends Nette\Application\UI\Control return; } - // disparar el evento + // invocación del evento $this->onSave($this, $data); } } ``` -También crearemos una fábrica que producirá este componente. Basta con [escribir su interfaz |application:components#Componentes con dependencias]: +A continuación, creemos una factory que produzca este componente. Basta con [definir su interfaz |application:components#Componentes con dependencias]: ```php interface EditControlFactory @@ -322,7 +322,7 @@ services: - EditControlFactory ``` -Y ahora podemos solicitar la fábrica y usarla en el Presenter: +Ahora ya podemos pedir la factory y usarla en el presenter: ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // o redirigimos al resultado de la edición, p.ej.: + // o redirigir al resultado de la edición, p. ej.: // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/es/inject-method-attribute.texy b/best-practices/es/inject-method-attribute.texy index c24c918236..3116c7f84e 100644 --- a/best-practices/es/inject-method-attribute.texy +++ b/best-practices/es/inject-method-attribute.texy @@ -2,17 +2,17 @@ Métodos y atributos inject ************************** .[perex] -En este artículo, nos centraremos en las diferentes formas de pasar dependencias a los Presenters en el framework Nette. Compararemos la forma preferida, que es el constructor, con otras opciones como los métodos y atributos `inject`. +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`. -También para los Presenters, pasar dependencias mediante el [constructor |dependency-injection:passing-dependencies#Paso por constructor] es la ruta preferida. Sin embargo, si creas un ancestro común del que heredan otros Presenters (p. ej., `BasePresenter`), y este ancestro también tiene dependencias, surge un problema que llamamos [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Esto se puede evitar utilizando rutas alternativas, que son los métodos y atributos (anotaciones) `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 [setter |dependency-injection:passing-dependencies#Paso por setter]. El nombre de estos setters comienza con el prefijo `inject`. Nette DI llama automáticamente a los métodos con este nombre justo después de crear la instancia del Presenter y les pasa todas las dependencias requeridas. Por lo tanto, deben declararse como public. +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*()` pueden considerarse como una especie de extensión del constructor en múltiples métodos. Gracias a esto, `BasePresenter` puede recibir dependencias a través de otro método y dejar el constructor libre para sus descendientes: +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 @@ -36,15 +36,15 @@ class MyPresenter extends BasePresenter } ``` -Un Presenter puede contener cualquier número de métodos `inject*()` y cada uno puede tener cualquier número de parámetros. También son excelentes en casos donde el Presenter está [compuesto de traits |presenter-traits] y cada uno requiere su propia dependencia. +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 [inyección en la propiedad |dependency-injection:passing-dependencies#Asignación a variable]. Simplemente marca en qué variables se debe inyectar, y Nette DI pasa automáticamente las dependencias justo después de crear la instancia del Presenter. Para poder insertarlas, es necesario declararlas como public. +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. -Marcamos las propiedades con un atributo: (anteriormente se usaba la anotación `/** @inject */`) +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 @@ -56,6 +56,6 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -La ventaja de esta forma de pasar dependencias era una sintaxis muy concisa. Sin embargo, con la llegada de [constructor property promotion |https://blog.nette.org/es/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], parece más fácil usar el constructor. +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, esta forma sufre las mismas deficiencias que pasar dependencias a propiedades en general: no tenemos control sobre los cambios en la variable y, al mismo tiempo, la variable se convierte en parte de la interfaz pública de la clase, lo cual no es deseable. +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 index 208b6174f5..e9ccbcf99c 100644 --- a/best-practices/es/lets-create-contact-form.texy +++ b/best-practices/es/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -Creando un formulario de contacto +Creemos un formulario de contacto ********************************* .[perex] -Veremos cómo crear un formulario de contacto en Nette, incluyendo el envío por correo electrónico. ¡Así que manos a la obra! +Veamos cómo crear en Nette un formulario de contacto, incluido el envío por correo de los datos enviados. ¡Vamos allá! -Primero, debemos crear un nuevo proyecto. Cómo hacerlo se explica en la página [Empezando |nette:installation]. Y luego podemos empezar a crear el formulario. +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. -La forma más sencilla es crear el [formulario directamente en el Presenter |forms:in-presenter]. Podemos usar el `HomePresenter` predefinido. Añadiremos el componente `contactForm` que representa el formulario. Haremos esto escribiendo el método de fábrica `createComponentContactForm()` en el código, que producirá el componente: +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; @@ -17,40 +17,37 @@ class HomePresenter extends Presenter protected function createComponentContactForm(): Form { $form = new Form; - $form->addText('name', 'Nombre:') - ->setRequired('Introduce tu nombre'); + $form->addText('name', 'Name:') + ->setRequired('Please enter your name'); $form->addEmail('email', 'E-mail:') - ->setRequired('Introduce tu e-mail'); - $form->addTextarea('message', 'Mensaje:') - ->setRequired('Introduce tu mensaje'); - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + ->setRequired('Please enter your email'); + $form->addTextArea('message', 'Message:') + ->setRequired('Please enter a message'); + $form->addSubmit('send', 'Send'); + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { - // envío de correo electrónico + // envío del correo } } ``` -Como puedes ver, hemos creado dos métodos. El primer método `createComponentContactForm()` crea un nuevo formulario. Tiene campos para el nombre, correo electrónico y mensaje, que añadimos con los métodos `addText()`, `addEmail()` y `addTextArea()`. También hemos añadido un botón para enviar el formulario. Pero, ¿y si el usuario no rellena algún campo? En ese caso, deberíamos informarle de que es un campo obligatorio. Logramos esto con el método `setRequired()`. Finalmente, también añadimos el [evento |nette:glossary#Eventos] `onSuccess`, que se dispara si el formulario se envía con éxito. En nuestro caso, llama al método `contactFormSucceeded`, que se encargará de procesar el formulario enviado. Añadiremos esto al código en un momento. +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. -Haremos que el componente `contactForm` se renderice en la plantilla `Home/default.latte`: +Rendericemos el componente `contactForm` en la plantilla `Home/default.latte`: ```latte {block content} -<h1>Formulario de contacto</h1> +<h1>Contact Form</h1> {control contactForm} ``` -Para el envío real del correo electrónico, crearemos una nueva clase, que llamaremos `ContactFacade` y la ubicaremos en el archivo `app/Model/ContactFacade.php`: +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 -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -66,9 +63,9 @@ class ContactFacade public function sendMessage(string $email, string $name, string $message): void { $mail = new Message; - $mail->addTo('admin@example.com') // tu correo electrónico + $mail->addTo('admin@example.com') // su correo ->setFrom($email, $name) - ->setSubject('Mensaje del formulario de contacto') + ->setSubject('Message from the contact form') ->setBody($message); $this->mailer->send($mail); @@ -76,9 +73,9 @@ class ContactFacade } ``` -El método `sendMessage()` crea y envía el correo electrónico. Utiliza para ello el llamado mailer, que recibe como dependencia a través del constructor. Lee más sobre [envío de correos electrónicos |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 volveremos al Presenter y completaremos el método `contactFormSucceeded()`. Este llamará al método `sendMessage()` de la clase `ContactFacade` y le pasará los datos del formulario. ¿Y cómo obtenemos el objeto `ContactFacade`? Lo recibiremos a través del constructor: +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; @@ -100,36 +97,36 @@ class HomePresenter extends Presenter public function contactFormSucceeded(stdClass $data): void { $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('El mensaje ha sido enviado'); + $this->flashMessage('Message has been sent'); $this->redirect('this'); } } ``` -Después de enviar el correo electrónico, mostraremos al usuario un llamado [flash message |application:components#Mensajes flash], confirmando que el mensaje se ha enviado, y luego redirigiremos a la siguiente página para que no sea posible reenviar el formulario usando *refresh* en el navegador. +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. -Bien, y si todo funciona, deberías poder enviar un correo electrónico desde tu formulario de contacto. ¡Felicidades! +Así que, si todo está bien configurado, ya debería poder enviar un correo desde su formulario de contacto. ¡Enhorabuena! -Plantilla HTML del correo electrónico -------------------------------------- +Plantilla HTML del correo +------------------------- -Hasta ahora, se envía un correo electrónico de texto sin formato que contiene solo el mensaje enviado por el formulario. Pero podemos usar HTML en el correo electrónico y hacer que su apariencia sea más atractiva. Crearemos una plantilla para ello en Latte, que escribiremos en `app/Model/contactEmail.latte`: +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>Mensaje del formulario de contacto + Message from the contact form -

    Nombre: {$name}

    +

    Name: {$name}

    E-mail: {$email}

    -

    Mensaje: {$message}

    +

    Message: {$message}

    ``` -Queda por modificar `ContactFacade` para que use esta plantilla. En el constructor, solicitaremos la clase `LatteFactory`, que puede crear un objeto `Latte\Engine`, es decir, el [renderizador de plantillas Latte |latte:develop#Cómo renderizar una plantilla]. Usando el método `renderToString()`, renderizaremos la plantilla en un archivo, el primer parámetro es la ruta a la plantilla y el segundo son las variables. +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; @@ -156,7 +153,7 @@ class ContactFacade ]); $mail = new Message; - $mail->addTo('admin@example.com') // tu correo electrónico + $mail->addTo('admin@example.com') // su correo ->setFrom($email, $name) ->setHtmlBody($body); @@ -165,15 +162,15 @@ class ContactFacade } ``` -Luego pasaremos el correo electrónico HTML generado al método `setHtmlBody()` en lugar del `setBody()` original. Tampoco necesitamos especificar el asunto del correo electrónico en `setSubject()`, ya que la biblioteca lo tomará del elemento `` de la plantilla. +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 `<title>` de la plantilla. Configuración ------------- -En el código de la clase `ContactFacade`, nuestro correo electrónico de administrador `admin@example.com` todavía está codificado. Sería mejor moverlo al archivo de configuración. ¿Cómo hacerlo? +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, modificaremos la clase `ContactFacade` y reemplazaremos la cadena con el correo electrónico por una variable pasada a través del constructor: +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 @@ -197,21 +194,21 @@ class ContactFacade } ``` -Y el segundo paso es especificar el valor de esta variable en la configuración. En el archivo `app/config/services.neon`, escribimos: +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 eso es todo. Si hubiera muchos elementos en la sección `services` y sintieras que el correo electrónico se pierde entre ellos, podemos convertirlo en una variable. Modificamos la entrada a: +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 en el archivo `app/config/common.neon`, definimos esta variable: +Y defina este parámetro en el archivo `app/config/common.neon`: ```neon parameters: diff --git a/best-practices/es/microsites.texy b/best-practices/es/microsites.texy index 392f299d5f..df58bf3bf2 100644 --- a/best-practices/es/microsites.texy +++ b/best-practices/es/microsites.texy @@ -1,11 +1,11 @@ -Cómo escribir micro-sitios web -****************************** +Cómo escribir microsites +************************ -Imagina que necesitas crear rápidamente un pequeño sitio web para el próximo evento de tu empresa. Tiene que ser simple, rápido y sin complicaciones innecesarias. Quizás pienses que para un proyecto tan pequeño no necesitas un framework robusto. Pero, ¿y si usar el framework Nette puede simplificar y acelerar fundamentalmente este proceso? +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 sitios web simples, no quieres renunciar a la comodidad. No quieres inventar lo que ya ha sido resuelto una vez. Siéntete libre de ser perezoso y déjate mimar. Nette Framework también se puede utilizar perfectamente como un micro framework. +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. -¿Cómo puede verse un micrositio así? Por ejemplo, colocando todo el código del sitio web en un único archivo `index.php` en la carpeta pública: +¿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 @@ -16,48 +16,48 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// crear contenedor DI basado en la configuración en config.neon +// crea el contenedor DI a partir de la configuración de config.neon $configurator->addConfig(__DIR__ . '/../app/config.neon'); $container = $configurator->createContainer(); -// configurar el enrutamiento +// 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) { - // detectar el idioma del navegador y redirigir a la URL /en o /de, etc. + // detecta el idioma del navegador y redirige a la URL /en o /de, etc. $supportedLangs = ['en', 'de', 'cs']; $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); // ruta para la URL https://example.com/cs o https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // mostrar la plantilla correspondiente, por ejemplo ../templates/en.latte +$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; }); -// ¡ejecutar la aplicación! +// ¡ejecuta la aplicación! $container->getByType(Nette\Application\Application::class)->run(); ``` -Todo lo demás serán plantillas almacenadas en la carpeta padre `/templates`. +Todo lo demás serán plantillas guardadas en el directorio superior `/templates`. -El código PHP en `index.php` primero [prepara el entorno |bootstrap:], luego define las [rutas |application:routing#Enrutamiento dinámico con callbacks] y finalmente ejecuta la aplicación. La ventaja es que el segundo parámetro de la función `addRoute()` puede ser un callable que se ejecuta después de abrir la página correspondiente. +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 un micrositio? ---------------------------------------- +¿Por qué usar Nette para microsites? +------------------------------------ -- Los programadores que alguna vez han probado [Tracy|tracy:] hoy no pueden imaginar programar nada sin ella. -- Pero sobre todo, utilizarás el sistema de plantillas [Latte|latte:], porque a partir de 2 páginas querrás tener separados el [layout y el contenido|latte:template-inheritance]. -- Y definitivamente quieres confiar en el [escape automático |latte:safety-first] para evitar la vulnerabilidad XSS. -- Nette también asegura que en caso de error, nunca se muestren mensajes de error de PHP para programadores, sino una página comprensible para el usuario. -- Si quieres obtener retroalimentación de los usuarios, por ejemplo, en forma de formulario de contacto, entonces también añadirás [formularios|forms:] y [base de datos|database:]. -- También puedes hacer que los formularios completados se [envíen fácilmente por correo electrónico|mail:]. -- A veces te puede resultar útil el [caching|caching:], por ejemplo, si descargas y muestras feeds. +- 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 la actualidad, donde la velocidad y la eficiencia son clave, es importante tener herramientas que te permitan lograr resultados sin demoras innecesarias. Nette framework te ofrece precisamente eso: desarrollo rápido, seguridad y una amplia gama de herramientas, como Tracy y Latte, que simplifican el proceso. Simplemente instala algunos paquetes de Nette y construir un micrositio así se convierte de repente en un juego de niños. Y sabes que no hay ninguna brecha de seguridad oculta en ninguna parte. +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 index 13d443cbaf..b3184ebf22 100644 --- a/best-practices/es/pagination.texy +++ b/best-practices/es/pagination.texy @@ -2,9 +2,9 @@ Paginación de resultados de la base de datos ******************************************** .[perex] -Al crear aplicaciones web, muy a menudo te encontrarás con el requisito de limitar el número de elementos mostrados por página. +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. -Partiremos del estado en el que mostramos todos los datos sin paginación. Para seleccionar datos de la base de datos, tenemos la clase ArticleRepository, que además del constructor contiene el método `findPublishedArticles`, que devuelve todos los artículos publicados ordenados descendentemente por fecha de publicació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; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -En el Presenter, inyectamos la clase del modelo y en el método render solicitamos los artículos publicados, que pasamos a la plantilla: +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; @@ -52,11 +52,11 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -En la plantilla `default.latte` nos encargamos de mostrar los artículos: +De listar los artículos se encargará después la plantilla `default.latte`: ```latte {block content} -<h1>Artículos</h1> +<h1>Articles</h1> <div class="articles"> {foreach $articles as $article} @@ -67,11 +67,11 @@ En la plantilla `default.latte` nos encargamos de mostrar los artículos: ``` -De esta manera, podemos mostrar todos los artículos, lo que, sin embargo, comenzará a causar problemas cuando el número de artículos aumente. En ese momento, será útil implementar un mecanismo de paginación. +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 asegurará que todos los artículos se dividan en varias páginas y solo mostraremos los artículos de la página actual. El número total de páginas y la división de los artículos los calculará [utils:Paginator] por sí mismo según cuántos artículos tengamos en total y cuántos artículos por página queramos mostrar. +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 para obtener artículos en la clase del repositorio para que pueda devolver solo los artículos de una página. También añadiremos un método para averiguar el número total de artículos en la base de datos, que necesitaremos para configurar el Paginator: +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; @@ -99,7 +99,7 @@ class ArticleRepository } /** - * Devuelve el número total de artículos publicados + * Returns the total number of published articles */ public function getPublishedArticlesCount(): int { @@ -108,9 +108,9 @@ class ArticleRepository } ``` -A continuación, procederemos a modificar el Presenter. Pasaremos el número de la página actualmente mostrada al método render. En caso de que este número no forme parte de la URL, estableceremos el valor predeterminado de la primera página. +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). -También ampliaremos el método render para obtener una instancia de Paginator, configurarlo y seleccionar los artículos correctos para mostrar en la plantilla. HomePresenter se verá así después de las modificaciones: +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; @@ -127,31 +127,31 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Averiguamos el número total de artículos publicados + // Obtiene el número total de artículos publicados $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Creamos una instancia de Paginator y la configuramos - $paginator = new Paginator; - $paginator->setItemCount($articlesCount); // número total de artículos - $paginator->setItemsPerPage(10); // número de elementos por página + // 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 - // Extraemos de la base de datos un conjunto limitado de artículos según el cálculo de Paginator + // 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()); - // que pasamos a la plantilla + // se los pasa a la plantilla $this->template->articles = $articles; - // y también el propio Paginator para mostrar las opciones de paginación + // y también el propio Paginator para mostrar los controles de paginación $this->template->paginator = $paginator; } } ``` -La plantilla ahora solo itera sobre los artículos de una página, solo necesitamos añadir los enlaces de paginación: +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>Artículos</h1> +<h1>Articles</h1> <div class="articles"> {foreach $articles as $article} @@ -162,27 +162,27 @@ La plantilla ahora solo itera sobre los artículos de una página, solo necesita <div class="pagination"> {if !$paginator->isFirst()} - <a n:href="default, 1">Primera</a> + <a n:href="default, 1">First</a>  |  - <a n:href="default, $paginator->page-1">Anterior</a> + <a n:href="default, $paginator->getPage() - 1">Previous</a>  |  {/if} - Página {$paginator->getPage()} de {$paginator->getPageCount()} + Page {$paginator->getPage()} of {$paginator->getPageCount()} {if !$paginator->isLast()}  |  - <a n:href="default, $paginator->getPage() + 1">Siguiente</a> + <a n:href="default, $paginator->getPage() + 1">Next</a>  |  - <a n:href="default, $paginator->getPageCount()">Última</a> + <a n:href="default, $paginator->getPageCount()">Last</a> {/if} </div> ``` -Así hemos añadido la opción de paginación a la página usando Paginator. En caso de que en lugar de [Nette Database Core |database:sql-way] usemos [Nette Database Explorer |database:explorer] como capa de base de datos, podemos implementar la paginación incluso sin usar Paginator. La clase `Nette\Database\Table\Selection` contiene el método [page |api:Nette\Database\Table\Selection::_page] con la lógica de paginación tomada de Paginator. +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. -El repositorio se verá así con esta forma de implementación: +Con este enfoque, el repositorio tendrá este aspecto: ```php namespace App\Model; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -En el Presenter no necesitamos crear un Paginator, usaremos en su lugar el método de la clase `Selection`, que nos devuelve el repositorio: +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; @@ -222,10 +222,10 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Extraemos los artículos publicados + // Obtiene los artículos publicados $articles = $this->articleRepository->findPublishedArticles(); - // y a la plantilla enviamos solo una parte de ellos limitada según el cálculo del método page + // 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); @@ -236,11 +236,11 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Dado que ahora no enviamos Paginator a la plantilla, modificaremos la parte que muestra los enlaces de paginación: +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>Artículos</h1> +<h1>Articles</h1> <div class="articles"> {foreach $articles as $article} @@ -251,23 +251,23 @@ Dado que ahora no enviamos Paginator a la plantilla, modificaremos la parte que <div class="pagination"> {if $page > 1} - <a n:href="default, 1">Primera</a> + <a n:href="default, 1">First</a>  |  - <a n:href="default, $page - 1">Anterior</a> + <a n:href="default, $page - 1">Previous</a>  |  {/if} - Página {$page} de {$lastPage} + Page {$page} of {$lastPage} {if $page < $lastPage}  |  - <a n:href="default, $page + 1">Siguiente</a> + <a n:href="default, $page + 1">Next</a>  |  - <a n:href="default, $lastPage">Última</a> + <a n:href="default, $lastPage">Last</a> {/if} </div> ``` -De esta manera, hemos implementado el mecanismo de paginación sin usar Paginator. +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 index a54e1ab5da..c14195c5bd 100644 --- a/best-practices/es/passing-settings-to-presenters.texy +++ b/best-practices/es/passing-settings-to-presenters.texy @@ -1,10 +1,10 @@ -Pasar la configuración a los Presenters -*************************************** +Pasar ajustes a los presenters +****************************** .[perex] -¿Necesitas pasar argumentos a los Presenters que no son objetos (p. ej., información sobre si se ejecuta en modo de depuración, rutas a directorios, etc.) y, por lo tanto, no se pueden pasar automáticamente mediante autowiring? La solución es encapsularlos en un objeto `Settings`. +¿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` representa una forma muy fácil y útil de proporcionar información sobre la aplicación en ejecución a los Presenters. Su forma específica depende puramente de tus necesidades concretas. Ejemplo: +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; @@ -12,15 +12,15 @@ namespace App; class Settings { public function __construct( - // desde PHP 8.1 es posible indicar readonly - public readonly bool $debugMode, - public readonly string $appDir, + // desde PHP 8.1 se puede usar readonly + public bool $debugMode, + public string $appDir, // y así sucesivamente ) {} } ``` -Ejemplo de registro en la configuración: +Ejemplo de su registro en la configuración: ```neon services: @@ -30,7 +30,7 @@ services: ) ``` -Cuando un Presenter necesite la información proporcionada por este servicio, simplemente la solicitará en el constructor: +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 diff --git a/best-practices/es/post-links.texy b/best-practices/es/post-links.texy index 8b0d2e0bab..25956231fa 100644 --- a/best-practices/es/post-links.texy +++ b/best-practices/es/post-links.texy @@ -2,29 +2,29 @@ Cómo usar correctamente los enlaces POST **************************************** .[perex] -En las aplicaciones web, especialmente en las interfaces administrativas, debería ser una regla básica que las acciones que cambian el estado del servidor no se realicen mediante el método HTTP GET. Como sugiere el nombre del método, GET solo debe usarse para obtener datos, no para modificarlos. Para acciones como eliminar registros, es preferible usar el método POST. Aunque lo ideal sería el método DELETE, pero no se puede invocar sin JavaScript, por lo que históricamente se usa POST. +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 hacerlo en la práctica? Utiliza este simple truco. Al principio de la plantilla, crea un formulario auxiliar con el identificador `postForm`, que luego usarás para los botones de eliminación: +¿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 del enlace clásico `<a>`, puedes usar un botón `<button>`, que se puede modificar visualmente para que parezca un enlace normal. Por ejemplo, el framework CSS Bootstrap ofrece las clases `btn btn-link` con las que conseguirás que el botón no sea visualmente diferente de otros enlaces. Mediante el atributo `form="postForm"`, lo vinculamos con el formulario predefinido: +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}">eliminar</button> - <!-- en lugar de <a n:href="delete $post->id">eliminar</a> --> + <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 hacer clic en el enlace, ahora se invoca la acción `delete`. Para asegurar que las solicitudes se acepten solo mediante el método POST y desde el mismo dominio (lo cual es una defensa eficaz contra los ataques CSRF), utiliza el atributo `#[Requires]`: +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; @@ -34,15 +34,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter #[Requires(methods: 'POST', sameOrigin: true)] public function actionDelete(int $id): void { - $this->facade->deletePost($id); // código hipotético que elimina el registro + $this->facade->deletePost($id); // código hipotético para borrar un registro $this->redirect('default'); } } ``` -El atributo existe desde Nette Application 3.2 y puedes aprender más sobre sus posibilidades en la página [Cómo usar el atributo #Requires |attribute-requires]. +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()` usaras la señal `handleDelete()`, no es necesario especificar `sameOrigin: true`, porque las señales tienen esta protección configurada implícitamente: +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')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -Este enfoque no solo mejora la seguridad de tu aplicación, sino que también contribuye al cumplimiento de los estándares y prácticas web correctos. Al utilizar métodos POST para acciones que cambian el estado, lograrás una aplicación más robusta y segura. +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 index b1dc7af72f..72e84158db 100644 --- a/best-practices/es/presenter-traits.texy +++ b/best-practices/es/presenter-traits.texy @@ -1,14 +1,14 @@ -Composición de Presenters a partir de traits -******************************************** +Componer presenters a partir de traits +************************************** .[perex] -Si necesitamos implementar el mismo código en varios Presenters (p. ej., verificar que el usuario haya iniciado sesión), una opción es colocar el código en un ancestro común. La segunda opción es crear [traits |nette:introduction-to-object-oriented-programming#Traits] de propósito único. +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 esta solución es que cada Presenter puede usar exactamente los traits que realmente necesita, mientras que la herencia múltiple no es posible en PHP. +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 el hecho de que al crear un Presenter, todos los [métodos inject |inject-method-attribute#Métodos inject] se llaman secuencialmente. Solo es necesario asegurarse de que el nombre de cada método inject sea único. +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 adjuntar código de inicialización a los eventos [onStartup u onRender |application:presenters#Eventos]. +Los traits pueden enganchar código de inicialización a los eventos [onStartup u onRender |application:presenters#Eventos]. Ejemplos: @@ -36,7 +36,7 @@ trait StandardTemplateFilters } ``` -El Presenter luego simplemente usa estos traits: +El presenter después simplemente usa estos traits: ```php class ArticlePresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/es/pretty-urls.texy b/best-practices/es/pretty-urls.texy new file mode 100644 index 0000000000..8616ddec6b --- /dev/null +++ b/best-practices/es/pretty-urls.texy @@ -0,0 +1,204 @@ +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 index 23de10d19f..3325951191 100644 --- a/best-practices/es/restore-request.texy +++ b/best-practices/es/restore-request.texy @@ -1,16 +1,16 @@ -¿Cómo volver a la página anterior? -********************************** +¿Cómo volver a una página anterior? +*********************************** .[perex] -¿Qué pasa si un usuario está llenando un formulario y su sesión expira? Para que no pierda los datos, antes de redirigir a la página de inicio de sesión, guardamos los datos en la sesión. En Nette, esto es pan comido. +¿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 solicitud actual se puede guardar en la sesión usando el método `storeRequest()`, que devuelve su identificador en forma de una cadena corta. El método guarda el nombre del Presenter actual, la vista y sus parámetros. En caso de que también se haya enviado un formulario, también se guarda el contenido de los campos (con la excepción de los archivos subidos). +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 restauración de la solicitud la realiza el método `restoreRequest($key)`, al que le pasamos el identificador obtenido. Este redirige al Presenter y vista originales. Sin embargo, si la solicitud guardada contiene el envío de un formulario, pasa al Presenter original mediante el método `forward()`, le pasa los valores previamente completados al formulario y lo vuelve a renderizar. De esta manera, el usuario tiene la posibilidad de reenviar el formulario y no se pierde ningún dato. +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 importante que `restoreRequest()` compruebe si el usuario recién conectado es el mismo que completó originalmente el formulario. Si no es así, descarta la solicitud y no hace nada. +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. -Mostraremos todo con un ejemplo. Tenemos un Presenter `AdminPresenter`, en el que se editan datos y en cuyo método `startup()` verificamos si el usuario ha iniciado sesión. Si no es así, lo redirigimos a `SignPresenter`. Al mismo tiempo, guardamos la solicitud actual y enviamos su clave a `SignPresenter`. +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 @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -El Presenter `SignPresenter`, además del formulario de inicio de sesión, contendrá también un parámetro persistente `$backlink`, en el que se escribirá la clave. Dado que el parámetro es persistente, se transferirá también después de enviar el formulario de inicio de sesión. +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 @@ -37,17 +37,17 @@ class SignPresenter extends Nette\Application\UI\Presenter #[Persistent] public string $backlink = ''; - protected function createComponentSignInForm(): Form + protected function createComponentSignInForm() { - $form = new Form; - // ... añadimos los campos del formulario ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + $form = new Nette\Application\UI\Form; + // ... añadir los campos del formulario ... + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { - // ... aquí iniciamos sesión del usuario ... + // ... aquí se autentica al usuario ... $this->restoreRequest($this->backlink); $this->redirect('Admin:'); @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -Pasamos la clave de la solicitud guardada al método `restoreRequest()` y este redirige (o avanza) al Presenter original. +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. -Sin embargo, si la clave no es válida (por ejemplo, ya no existe en la sesión), el método no hace nada. Por lo tanto, sigue la llamada `$this->redirect('Admin:')`, que redirige a `AdminPresenter`. +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 index 829741034c..b18699884c 100644 --- a/best-practices/fr/@home.texy +++ b/best-practices/fr/@home.texy @@ -2,23 +2,24 @@ Tutoriels et bonnes pratiques ***************************** .[perex] -Tutoriels, solutions pour les tâches courantes et *bonnes pratiques* pour Nette. +Tutoriels, solutions aux tâches courantes et bonnes pratiques pour Nette. <div class=documentation> <div> -Application Nette +Nette Application ----------------- - [Méthodes et attributs inject |inject-method-attribute] -- [Composition des presenters à partir de traits |presenter-traits] -- [Passage des paramètres aux presenters |passing-settings-to-presenters] -- [Comment revenir à une page précédente |restore-request] -- [Pagination des résultats de la base de données |pagination] +- [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 utiliser correctement les liens POST |post-links] +- [Comment bien utiliser les liens POST |post-links] +- [URLs élégantes avec slugs |pretty-urls] </div> <div> @@ -26,10 +27,10 @@ Application Nette Formulaires ----------- -- [Réutilisation des formulaires |form-reuse] -- [Formulaire pour la création et l'édition d'enregistrements |creating-editing-form] +- [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] -- [Selectbox dépendants |https://blog.nette.org/fr/dependent-selectboxes-elegantly-in-nette-and-pure-js] +- [Listes déroulantes dépendantes |https://blog.nette.org/fr/dependent-selectboxes-elegantly-in-nette-and-pure-js] </div> <div> @@ -38,11 +39,10 @@ Formulaires Général ------- - [Comment charger un fichier de configuration |bootstrap:] -- [Comment écrire des micro-sites |microsites] -- [Pourquoi Nette utilise la notation PascalCase pour les constantes ? |https://blog.nette.org/fr/for-less-screaming-in-the-code] -- [Pourquoi Nette n'utilise pas le suffixe Interface ? |https://blog.nette.org/fr/prefixes-and-suffixes-do-not-belong-in-interface-names] +- [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] -- [Conseils sur les éditeurs & outils |editors-and-tools] - [Introduction à la programmation orientée objet |nette:introduction-to-object-oriented-programming] </div> @@ -54,7 +54,7 @@ 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] +- [Site web Doctrine ORM |https://github.com/MinecordNetwork/Website] - [Démarrage rapide |quickstart:] </div> @@ -63,7 +63,7 @@ Exemples de solutions Vidéos ------ -Des centaines d'enregistrements des Derniers Samedis et de vidéos sur Nette peuvent être trouvés sous un même toit sur [la chaîne Youtube de Nette Framework |https://www.youtube.com/user/NetteFramework]. +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 new file mode 100644 index 0000000000..96d5e6f5ff --- /dev/null +++ b/best-practices/fr/@left-menu.texy @@ -0,0 +1,34 @@ +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 index ae1deef2d5..12ceedd5d5 100644 --- a/best-practices/fr/@meta.texy +++ b/best-practices/fr/@meta.texy @@ -1,2 +1 @@ {{sitename: Tutoriels et bonnes pratiques}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/fr/attribute-requires.texy b/best-practices/fr/attribute-requires.texy index afeb4699c6..803289ae62 100644 --- a/best-practices/fr/attribute-requires.texy +++ b/best-practices/fr/attribute-requires.texy @@ -2,30 +2,30 @@ Comment utiliser l'attribut `#[Requires]` ***************************************** .[perex] -Lorsque vous écrivez une application web, vous rencontrez souvent le besoin de restreindre l'accès à certaines parties de votre application. Peut-être souhaitez-vous que certaines requêtes ne puissent envoyer des données qu'à l'aide d'un formulaire (c'est-à-dire avec la méthode POST), ou qu'elles ne soient accessibles que pour les appels AJAX. Dans Nette Framework 3.2, un nouvel outil est apparu qui vous permet de définir de telles restrictions de manière très élégante et claire : l'attribut `#[Requires]`. +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 une marque spéciale en PHP que vous ajoutez avant la définition d'une classe ou d'une méthode. Comme il s'agit en fait d'une classe, pour que les exemples suivants fonctionnent, il est nécessaire d'inclure la clause use : +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 également sur ces méthodes : +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 concernent également les composants, vous pouvez donc également utiliser l'attribut avec eux. +Les deux dernières méthodes valent aussi pour les composants, vous pouvez donc utiliser l'attribut avec eux également. -Si les conditions spécifiées par l'attribut ne sont pas remplies, une erreur HTTP 4xx est levée. +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 spécifier quelles méthodes HTTP (comme GET, POST, etc.) sont autorisées pour l'accès. Par exemple, si vous souhaitez autoriser l'accès uniquement en soumettant un formulaire, définissez : +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 @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Pourquoi devriez-vous utiliser POST au lieu de GET pour les actions modifiant l'état et comment faire ? [Lisez le guide |post-links]. +Pourquoi utiliser POST plutôt que GET pour les actions qui changent l'état, et comment faire ? [Lisez le guide |post-links]. -Vous pouvez spécifier une méthode ou un tableau de méthodes. Un cas spécial est la valeur `'*'`, qui autorise toutes les méthodes, ce que les presenters par défaut [n'autorisent pas pour des raisons de sécurité |application:presenters#Vérification de la méthode HTTP]. +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]. -Appel AJAX ----------- +Appels AJAX +----------- -Si vous souhaitez que le presenter ou la méthode ne soit disponible que pour les requêtes AJAX, utilisez : +Si vous voulez qu'un presenter ou une méthode ne soit accessible qu'aux requêtes AJAX, utilisez : ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ class AjaxPresenter extends Nette\Application\UI\Presenter Même origine ------------ -Pour augmenter la sécurité, vous pouvez exiger que la requête soit effectuée depuis le même domaine. Cela empêche la [vulnérabilité CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF] : +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)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -Pour les méthodes `handle<Signal>()`, l'accès depuis le même domaine est requis automatiquement. Donc, si au contraire vous souhaitez autoriser l'accès depuis n'importe quel domaine, spécifiez : +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)] @@ -77,10 +77,10 @@ public function handleList(): void ``` -Accès via forward +Accès par forward ----------------- -Parfois, il est utile de restreindre l'accès à un presenter de manière à ce qu'il ne soit disponible qu'indirectement, par exemple en utilisant la méthode `forward()` ou `switch()` depuis un autre presenter. C'est ainsi que l'on protège par exemple les error-presenters, afin qu'ils ne puissent pas être appelés depuis une URL : +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)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -En pratique, il est souvent nécessaire de marquer certaines vues auxquelles on ne peut accéder qu'en fonction de la logique du presenter. Donc encore une fois, pour qu'elles ne puissent pas être ouvertes directement : +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 @@ -111,10 +111,10 @@ class ProductPresenter extends Nette\Application\UI\Presenter ``` -Actions spécifiques -------------------- +Actions précises +---------------- -Vous pouvez également restreindre l'accès à un certain code, comme la création d'un composant, pour qu'il ne soit disponible que pour des actions spécifiques dans le presenter : +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 @@ -132,9 +132,9 @@ Dans le cas d'une seule action, il n'est pas nécessaire d'écrire un tableau : Attributs personnalisés ----------------------- -Si vous souhaitez utiliser l'attribut `#[Requires]` de manière répétée avec les mêmes paramètres, vous pouvez créer votre propre attribut qui héritera de `#[Requires]` et le configurera selon vos besoins. +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]` permettra l'accès uniquement via l'action `default` : +Par exemple, `#[SingleAction]` n'autorise l'accès que par l'action `default` : ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -Ou `#[RestMethods]` permettra l'accès via toutes les méthodes HTTP utilisées pour les API REST : +Ou `#[RestMethods]` autorisera l'accès par toutes les méthodes HTTP utilisées pour une API REST : ```php #[\Attribute] @@ -174,4 +174,4 @@ class ApiPresenter extends Nette\Application\UI\Presenter Conclusion ---------- -L'attribut `#[Requires]` vous offre une grande flexibilité et un contrôle sur la manière dont vos pages web sont accessibles. À l'aide de règles simples mais puissantes, vous pouvez augmenter la sécurité et le bon fonctionnement de votre application. Comme vous pouvez le voir, l'utilisation des attributs dans Nette peut non seulement faciliter votre travail, mais aussi le sécuriser. +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 index b8f9a3e43b..dc80f39788 100644 --- a/best-practices/fr/composer.texy +++ b/best-practices/fr/composer.texy @@ -1,12 +1,12 @@ -Composer : Conseils d'utilisation +Composer : conseils d'utilisation ********************************* <div class=perex> -Composer est un outil de gestion des dépendances en PHP. Il nous permet de lister les bibliothèques dont notre projet dépend, et il les installera et les mettra à jour pour nous. Nous allons montrer : +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 -- son utilisation dans un projet nouveau ou existant +- comment l'utiliser dans un projet nouveau ou existant </div> @@ -14,21 +14,21 @@ Composer est un outil de gestion des dépendances en PHP. Il nous permet de list Installation ============ -Composer est un fichier `.phar` exécutable que vous téléchargez et installez de la manière suivante : +Composer est un fichier exécutable `.phar` que vous téléchargez et installez comme suit. Windows ------- -Utilisez l'installeur officiel [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. +Utilisez l'installateur officiel [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. Linux, macOS ------------ -Il suffit de 4 commandes que vous copiez depuis [cette page |https://getcomposer.org/download/]. +Il vous suffit de 4 commandes, que vous pouvez copier depuis [cette page |https://getcomposer.org/download/]. -Ensuite, en le plaçant dans un dossier qui se trouve dans le `PATH` système, Composer devient accessible globalement : +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 @@ -38,7 +38,7 @@ $ mv ./composer.phar ~/bin/composer # ou /usr/local/bin/composer Utilisation dans un projet ========================== -Pour pouvoir commencer à utiliser Composer dans votre projet, vous n'avez besoin que du fichier `composer.json`. Celui-ci décrit les dépendances de notre projet et peut également contenir d'autres métadonnées. Un `composer.json` de base peut donc ressembler à ceci : +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 { @@ -48,17 +48,17 @@ Pour pouvoir commencer à utiliser Composer dans votre projet, vous n'avez besoi } ``` -Nous indiquons ici que notre application (ou bibliothèque) nécessite le paquet `nette/database` (le nom du paquet se compose du nom de l'organisation et du nom du projet) et veut une version qui correspond à la contrainte `^3.0` (c'est-à-dire la dernière version 3). +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). -Nous avons donc à la racine du projet le fichier `composer.json` et nous lançons l'installation : +Avec le fichier `composer.json` à la racine du projet, exécutez donc : ```shell composer update ``` -Composer téléchargera Nette Database dans le dossier `vendor/`. Il créera également le fichier `composer.lock`, qui contient des informations sur les versions exactes des bibliothèques qu'il a installées. +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 le fichier `vendor/autoload.php`, que nous pouvons simplement inclure et commencer à utiliser les bibliothèques sans aucun travail supplémentaire : +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'; @@ -67,52 +67,52 @@ $db = new Nette\Database\Connection('sqlite::memory:'); ``` -Mise à jour des paquets vers les dernières versions -=================================================== +Mettre les paquets à jour vers les dernières versions +===================================================== -La mise à jour des bibliothèques utilisées vers les dernières versions selon les contraintes définies dans `composer.json` est gérée par la commande `composer update`. Par exemple, pour 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 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 dans le fichier `composer.json`, par exemple vers `"nette/database": "^4.1"`, afin de pouvoir installer la dernière version, utilisez la commande `composer require nette/database`. +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 lister dans la ligne de commande, par ex. : +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 qui n'est pas pratique. Utilisez donc le script simple "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, qui le fera pour vous : +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éation d'un nouveau projet -============================ +Créer un nouveau projet +======================= -Vous créez un nouveau projet Nette à l'aide d'une seule commande : +Vous pouvez créer un nouveau projet Nette en une seule commande : ```shell composer create-project nette/web-project nom-du-projet ``` -Comme `nom-du-projet`, insérez le nom du répertoire pour votre projet et confirmez. Composer téléchargera le dépôt `nette/web-project` depuis GitHub, qui contient déjà le fichier `composer.json`, puis Nette Framework. Il devrait suffire de [définir les permissions |nette:troubleshooting#Configuration des permissions de répertoire] d'écriture sur les dossiers `temp/` et `log/` et le projet devrait prendre vie. +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 le projet sera hébergé, n'oubliez pas de [la définir |#Version de PHP]. +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, avec la version de PHP utilisée dans la ligne de commande lors de l'exécution de Composer). Ce qui n'est probablement pas la même version que celle utilisée par votre hébergement. C'est pourquoi il est très important d'ajouter au fichier `composer.json` l'information sur la version de PHP sur l'hébergement. Ensuite, seules les versions de paquets compatibles avec l'hébergement seront installées. +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. -Le fait que le projet fonctionnera par exemple sur PHP 8.2.3 est défini par la commande : +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 est ainsi écrite dans le fichier `composer.json` : +La version sera écrite dans le fichier `composer.json` de cette façon : ```js { @@ -124,7 +124,7 @@ La version est ainsi écrite dans le fichier `composer.json` : } ``` -Cependant, le numéro de version de PHP est indiqué à un autre endroit du fichier, dans la section `require`. Alors que le premier numéro détermine pour quelle version les paquets seront installés, le second numéro indique pour quelle version l'application elle-même est écrite. Et selon lui, par exemple, PhpStorm définit le *niveau de langage PHP*. (Bien sûr, il n'est pas logique que ces versions diffèrent, donc la double écriture est une imperfection.) Vous définissez cette version avec la commande : +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 @@ -144,51 +144,51 @@ Ou directement dans le fichier `composer.json` : Ignorer la version de PHP ========================= -Les paquets indiquent généralement à la fois la version minimale de PHP avec laquelle ils sont compatibles et la version maximale avec laquelle ils sont testés. Si vous prévoyez d'utiliser une version de PHP encore plus récente, par exemple à des fins de test, Composer refusera d'installer un tel paquet. La solution est l'option `--ignore-platform-req=php+`, qui fait que Composer ignorera les limites supérieures de la version de PHP requise. +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 rapports -============= +Faux signalements +================= -Lors de la mise à niveau des paquets ou des changements de numéros de version, il arrive qu'un conflit se produise. Un paquet a des exigences qui sont en conflit avec un autre, etc. Mais Composer affiche parfois un faux rapport. Il signale un conflit qui n'existe pas réellement. Dans ce cas, il est utile de supprimer le fichier `composer.lock` et de réessayer. +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, alors il est sérieux et il faut en déduire quoi et comment modifier. +Si le message d'erreur persiste, il est authentique et vous devez le lire pour comprendre quoi modifier et comment. -Packagist.org - dépôt central -============================= +Packagist.org - le dépôt global +=============================== -[Packagist |https://packagist.org] est le dépôt principal dans lequel Composer essaie de rechercher des paquets, sauf indication contraire. Nous pouvons y publier nos propres paquets. +[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 utiliser le dépôt central ? ------------------------------------------------------ +Et si nous ne voulons pas du dépôt central +------------------------------------------ -Si nous avons des applications internes à l'entreprise que nous ne pouvons tout simplement pas héberger publiquement, nous créons pour elles un dépôt d'entreprise. +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. -Plus d'informations sur le sujet des dépôts [dans la documentation officielle |https://getcomposer.org/doc/05-repositories.md#repositories]. +Pour en savoir plus sur les dépôts, voir [la documentation officielle |https://getcomposer.org/doc/05-repositories.md#repositories]. Autoloading =========== -Une caractéristique essentielle de Composer est qu'il fournit l'autoloading pour toutes les classes qu'il a installées, que vous démarrez en incluant le fichier `vendor/autoload.php`. +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`. -Cependant, il est possible d'utiliser Composer également pour charger d'autres classes en dehors du dossier `vendor`. La première option est de laisser Composer parcourir les dossiers et sous-dossiers définis, trouver toutes les classes et les inclure dans l'autoloader. Vous obtenez cela en définissant `autoload > classmap` dans `composer.json` : +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 dossier src/ et ses sous-dossiers + "src/", # inclut le répertoire src/ et ses sous-répertoires ] } } ``` -Ensuite, il est nécessaire, à chaque modification, d'exécuter la commande `composer dumpautoload` et de laisser les tables d'autoloading se régénérer. C'est extrêmement inconfortable et il est bien préférable de confier cette tâche à [RobotLoader|robot-loader:], qui effectue la même activité automatiquement en arrière-plan et beaucoup plus rapidement. +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 option est de respecter [PSR-4|https://www.php-fig.org/psr/psr-4/]. En termes simples, 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, donc par ex. `App\Core\RouterFactory` sera dans le fichier `/chemin/vers/App/Core/RouterFactory.php`. Exemple de configuration : +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 { @@ -200,13 +200,13 @@ La deuxième option est de respecter [PSR-4|https://www.php-fig.org/psr/psr-4/]. } ``` -Comment configurer précisément le comportement est expliqué dans la [documentation de Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. +Voir la [documentation de Composer |https://getcomposer.org/doc/04-schema.md#psr-4] pour les détails de configuration de ce comportement. -Test de nouvelles versions -========================== +Tester de nouvelles versions +============================ -Vous voulez tester une nouvelle version de développement d'un paquet. Comment faire ? Tout d'abord, ajoutez cette paire d'options au fichier `composer.json`, qui permettra d'installer les versions de développement des paquets, mais n'y recourra que s'il n'existe aucune combinaison de versions stables qui satisferait aux exigences : +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 { @@ -215,33 +215,33 @@ Vous voulez tester une nouvelle version de développement d'un paquet. Comment f } ``` -Ensuite, nous recommandons de supprimer le fichier `composer.lock`, parfois Composer refuse inexplicablement l'installation et cela résout le problème. +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. -Supposons qu'il s'agisse du paquet `nette/utils` et que la nouvelle version porte le numéro 4.0. Vous l'installez avec la commande : +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 spécifique, par exemple 4.0.0-RC2 : +Ou vous pouvez installer une version précise, par exemple la 4.0.0-RC2 : ```shell composer require nette/utils:4.0.0-RC2 ``` -Mais si un autre paquet dépend de la bibliothèque et est verrouillé sur une version plus ancienne (par ex. `^3.1`), alors il est idéal de mettre à jour le paquet pour qu'il fonctionne avec la nouvelle version. Cependant, si vous voulez simplement contourner la restriction et forcer Composer à installer la version de développement et prétendre qu'il s'agit d'une version plus ancienne (par ex. 3.1.6), vous pouvez utiliser le mot-clé `as` : +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" ``` -Appel de commandes -================== +Appeler des commandes +===================== -Via Composer, il est possible d'appeler des commandes et des scripts personnalisés prédéfinis, comme s'il s'agissait de commandes natives de Composer. Pour les scripts qui se trouvent dans le dossier `vendor/bin`, il n'est pas nécessaire de spécifier ce dossier. +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. -Comme exemple, définissons dans le fichier `composer.json` un script qui utilise [Nette Tester|tester:] pour lancer les tests : +Définissons par exemple dans `composer.json` un script qui utilise [Nette Tester |tester:] pour lancer les tests : ```js { @@ -251,19 +251,19 @@ Comme exemple, définissons dans le fichier `composer.json` un script qui utilis } ``` -Nous lançons ensuite les tests à l'aide de `composer tester`. Nous pouvons appeler la commande même si nous ne sommes pas dans le dossier racine du projet, mais dans l'un des sous-répertoires. +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. -Envoyez un merci -================ +Dire merci +========== -Nous allons vous montrer une astuce qui fera plaisir aux auteurs open source. Vous pouvez facilement donner une étoile sur GitHub aux bibliothèques que votre projet utilise. Il suffit d'installer la bibliothèque `symfony/thanks` : +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 ``` -Et ensuite exécuter : +Puis d'exécuter : ```shell composer thanks @@ -275,7 +275,7 @@ Essayez ! Configuration ============= -Composer est étroitement lié à l'outil de versionnement [Git |https://git-scm.com]. Si vous ne l'avez pas installé, il faut dire à Composer de ne pas l'utiliser : +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 index b05616ac5e..b9904f4a77 100644 --- a/best-practices/fr/creating-editing-form.texy +++ b/best-practices/fr/creating-editing-form.texy @@ -1,16 +1,16 @@ -Formulaire pour la création et l'édition d'enregistrements -********************************************************** +Formulaire de création et d'édition d'enregistrements +***************************************************** .[perex] -Comment implémenter correctement l'ajout et l'édition d'enregistrements dans Nette, en utilisant le même formulaire pour les deux ? +Comment implémenter correctement dans Nette l'ajout et l'édition d'un enregistrement, en utilisant le même formulaire pour les deux ? -Dans de nombreux cas, les formulaires pour ajouter et éditer des enregistrements sont identiques, ne différant peut-être que par l'étiquette du bouton. Nous montrerons des exemples de presenters simples où nous utiliserons d'abord le formulaire pour ajouter un enregistrement, puis pour l'éditer, et enfin combinerons les deux solutions. +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. -Ajout d'un enregistrement +Ajouter un enregistrement ------------------------- -Exemple de presenter servant à ajouter un enregistrement. Nous laisserons le travail réel avec la base de données à la classe `Facade`, dont le code n'est pas essentiel pour la démonstration. +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 @@ -27,15 +27,15 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $form = new Form; - // ... ajouter les champs du formulaire ... + // ... ajout des champs du formulaire ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // ajout de l'enregistrement à la base de données + $this->facade->add($data); // ajoute l'enregistrement dans la base de données $this->flashMessage('Ajouté avec succès'); $this->redirect('...'); } @@ -48,10 +48,10 @@ class RecordPresenter extends Nette\Application\UI\Presenter ``` -Édition d'un enregistrement ---------------------------- +Éditer un enregistrement +------------------------ -Maintenant, montrons à quoi ressemblerait un presenter servant à éditer un enregistrement : +Voyons maintenant à quoi ressemblerait un presenter d'édition d'enregistrement : ```php @@ -70,8 +70,8 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // vérification de l'existence de l'enregistrement - || !$this->facade->isEditAllowed(/*...*/) // contrôle des permissions + !$record // vérifie l'existence de l'enregistrement + || !$this->facade->isEditAllowed(/*...*/) // vérifie les permissions ) { $this->error(); // erreur 404 } @@ -81,32 +81,32 @@ class RecordPresenter extends Nette\Application\UI\Presenter protected function createComponentRecordForm(): Form { - // vérifions que l'action est 'edit' + // vérifie que l'action est bien 'edit' if ($this->getAction() !== 'edit') { $this->error(); } $form = new Form; - // ... ajouter les champs du formulaire ... + // ... ajout des champs du formulaire ... - $form->setDefaults($this->record); // définition des valeurs par défaut - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->setDefaults($this->record); // définit les valeurs par défaut + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->update($this->record->id, $data); // mise à jour de l'enregistrement + $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*, qui s'exécute au tout début du [cycle de vie du presenter |application:presenters#Cycle de vie du presenter], nous vérifions l'existence de l'enregistrement et les permissions de l'utilisateur pour l'éditer. +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 sauvegardons l'enregistrement dans la propriété `$record` pour l'avoir disponible dans la méthode `createComponentRecordForm()` pour définir les valeurs par défaut, et dans `recordFormSucceeded()` pour l'ID. Une solution alternative serait de définir les valeurs par défaut directement dans `actionEdit()` et d'obtenir la valeur de l'ID, qui fait partie de l'URL, en utilisant `getParameter('id')` : +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 @@ -114,12 +114,12 @@ Nous sauvegardons l'enregistrement dans la propriété `$record` pour l'avoir di { $record = $this->facade->get($id); if ( - // vérification de l'existence et contrôle des permissions + // vérifie l'existence et les permissions ) { $this->error(); } - // définition des valeurs par défaut du formulaire + // définit les valeurs par défaut du formulaire $this->getComponent('recordForm') ->setDefaults($record); } @@ -130,16 +130,15 @@ Nous sauvegardons l'enregistrement dans la propriété `$record` pour l'avoir di $this->facade->update($id, $data); // ... } -} ``` -Cependant, et cela devrait être **la leçon la plus importante de tout le code**, nous devons nous assurer lors de la création du formulaire que l'action est bien `edit`. Sinon, la vérification dans la méthode `actionEdit()` n'aurait pas lieu du tout ! +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 ----------------------------------------- -Et maintenant, combinons les deux presenters en un seul. Soit nous pourrions distinguer dans la méthode `createComponentRecordForm()` de quelle action il s'agit et configurer le formulaire en conséquence, soit nous pouvons laisser cela directement aux méthodes d'action et nous débarrasser de la condition : +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 @@ -153,49 +152,49 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // vérification de l'existence de l'enregistrement - || !$this->facade->isEditAllowed(/*...*/) // contrôle des permissions + !$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éfinition des valeurs par défaut - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->setDefaults($record); // définit les valeurs par défaut + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form { - // vérifions que l'action est 'add' ou 'edit' + // vérifie que l'action est 'add' ou 'edit' if (!in_array($this->getAction(), ['add', 'edit'])) { $this->error(); } $form = new Form; - // ... ajouter les champs du formulaire ... + // ... ajout des champs du formulaire ... return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // ajout de l'enregistrement à la base de données + $this->facade->add($data); // ajoute l'enregistrement dans la base de données $this->flashMessage('Ajouté avec succès'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // mise à jour de l'enregistrement + $this->facade->update($id, $data); // met à jour l'enregistrement $this->flashMessage('Mis à jour avec succès'); $this->redirect('...'); } diff --git a/best-practices/fr/dynamic-snippets.texy b/best-practices/fr/dynamic-snippets.texy index d285c2c138..98fbbb5ca0 100644 --- a/best-practices/fr/dynamic-snippets.texy +++ b/best-practices/fr/dynamic-snippets.texy @@ -1,7 +1,10 @@ -Snippets Dynamiques +Snippets dynamiques ******************* -Assez souvent, lors du développement d'applications, le besoin se fait sentir d'effectuer des opérations AJAX, par exemple sur des lignes individuelles d'une table ou des éléments d'une liste. À titre d'exemple, nous pouvons choisir l'affichage d'articles, où pour chacun d'eux, nous permettons à l'utilisateur connecté de choisir une évaluation "j'aime/je n'aime pas". Le code du presenter et du template correspondant sans AJAX ressemblera approximativement à ceci (je présente les extraits les plus importants, le code suppose l'existence d'un service pour marquer les évaluations et obtenir la collection d'articles - l'implémentation spécifique n'est pas importante aux fins de ce tutoriel) : +.[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 @@ -32,18 +35,18 @@ Template : ``` -Ajaxification -============= +Ajaxisation +=========== -Ajoutons maintenant AJAX à cette application simple. Le changement d'évaluation d'un article n'est pas assez important pour nécessiter une redirection, il devrait donc idéalement se faire en arrière-plan via AJAX. Nous utiliserons le [script de gestion des extensions |application:ajax#Naja] avec la convention habituelle selon laquelle les liens AJAX ont la classe CSS `ajax`. +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 faire concrètement ? Nette propose 2 voies : la voie des snippets dynamiques et la voie des composants. Les deux ont leurs avantages et leurs inconvénients, nous allons donc les présenter l'une après l'autre. +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 =============================== -Un snippet dynamique, dans la terminologie Latte, signifie un cas d'utilisation spécifique de la balise `{snippet}`, où une variable est utilisée dans le nom du snippet. Un tel snippet ne peut pas se trouver n'importe où dans le template - il doit être enveloppé par un snippet statique, c'est-à-dire ordinaire, ou à l'intérieur de `{snippetArea}`. Nous pourrions modifier notre template comme suit. +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 @@ -62,9 +65,9 @@ Un snippet dynamique, dans la terminologie Latte, signifie un cas d'utilisation {/snippet} ``` -Chaque article définit maintenant un snippet qui a l'ID de l'article dans son nom. Tous ces snippets sont ensuite regroupés dans un seul snippet nommé `articlesContainer`. Si nous omettions ce snippet enveloppant, Latte nous le signalerait par une exception. +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 nous reste à ajouter le redessin dans le presenter - il suffit de redessiner l'enveloppe statique. +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 @@ -72,18 +75,18 @@ public function handleLike(int $articleId): void $this->ratingService->saveLike($articleId, $this->user->id); if ($this->isAjax()) { $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- pas nécessaire + // $this->redrawControl('article-' . $articleId); -- inutile } else { $this->redirect('this'); } } ``` -Nous modifions de la même manière la méthode sœur `handleUnlike()`, et AJAX est fonctionnel ! +Modifiez de la même façon la méthode `handleUnlike()` correspondante, et l'AJAX fonctionne ! -Cependant, la solution a un inconvénient. Si nous examinions plus en détail le déroulement de la requête AJAX, nous constaterions que bien que l'application semble économe en apparence (elle ne renvoie qu'un seul snippet pour l'article donné), elle a en fait rendu tous les snippets sur le serveur. Elle a placé le snippet souhaité dans le payload et a jeté les autres (les récupérant donc également inutilement de la base de données). +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 ce processus, nous devrons intervenir là où nous passons la collection `$articles` au template (disons dans la méthode `renderDefault()`). Nous utiliserons le fait que le traitement des signaux a lieu avant les méthodes `render<Something>` : +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 @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -Maintenant, lors du traitement du signal, au lieu de la collection avec tous les articles, seul un tableau avec un seul article est passé au template - celui que nous voulons rendre et envoyer dans le payload au navigateur. `{foreach}` ne s'exécutera donc qu'une seule fois et aucun snippet supplémentaire ne sera rendu. +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 les snippets dynamiques. L'astuce consiste à transférer toute la logique dans un composant séparé - la saisie des évaluations ne sera plus gérée par le presenter, mais par un `LikeControl` dédié. La classe ressemblera à ceci (en plus, elle contiendra également les méthodes `render`, `handleUnlike`, etc.) : +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 @@ -146,7 +149,7 @@ Template du composant : {/snippet} ``` -Bien sûr, le template de la vue changera et nous devrons ajouter une factory au presenter. Comme nous créerons le composant autant de fois que nous obtiendrons d'articles de la base de données, nous utiliserons la classe [Multiplier |application:Multiplier] pour sa "multiplication". +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() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -Le template de la vue est réduit au minimum nécessaire (et totalement dépourvu de snippets !) : +Le template de la vue se réduit au strict minimum (et ne contient plus aucun snippet !) : ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ Le template de la vue est réduit au minimum nécessaire (et totalement dépourv </article> ``` -Nous avons presque terminé : l'application fonctionnera désormais en AJAX. Ici aussi, nous devons optimiser l'application, car en raison de l'utilisation de Nette Database, lors du traitement du signal, tous les articles sont inutilement chargés depuis la base de données au lieu d'un seul. L'avantage, cependant, est qu'ils ne seront pas rendus, car seul notre composant sera réellement rendu. +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/editors-and-tools.texy b/best-practices/fr/editors-and-tools.texy deleted file mode 100644 index f80342f3fe..0000000000 --- a/best-practices/fr/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Éditeurs & Outils -***************** - -.[perex] -Vous pouvez être un programmeur compétent, mais ce n'est qu'avec de bons outils que vous deviendrez un maître. Dans ce chapitre, vous trouverez des conseils sur les outils, éditeurs et plugins importants. - - -Éditeur IDE -=========== - -Nous recommandons vivement d'utiliser un IDE complet pour le développement, tel que PhpStorm, NetBeans, VS Code, et pas seulement un éditeur de texte avec prise en charge PHP. La différence est vraiment fondamentale. Il n'y a aucune raison de se contenter d'un simple éditeur qui colore la syntaxe mais n'atteint pas les capacités d'un IDE de pointe, qui suggère précisément, surveille les erreurs, peut refactoriser le code et bien plus encore. Certains IDE sont payants, d'autres sont même gratuits. - -**NetBeans IDE** intègre déjà la prise en charge de Nette, Latte et NEON. - -**PhpStorm** : installez ces plugins dans `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code** : trouvez le plugin "Nette Latte + Neon" dans le marketplace. - -Connectez également Tracy à votre éditeur. Lorsque la page d'erreur s'affiche, vous pourrez cliquer sur les noms de fichiers et ils s'ouvriront dans l'éditeur avec le curseur sur la ligne correspondante. Lisez [comment configurer le système|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan est un outil qui détecte les erreurs logiques dans le code avant même de l'exécuter. - -Nous l'installons à l'aide de Composer : - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Nous créons un fichier de configuration `phpstan.neon` dans le projet : - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Et ensuite, nous le laissons analyser les classes dans le dossier `app/` : - -```shell -vendor/bin/phpstan analyse app -``` - -Vous trouverez une documentation exhaustive directement sur le [site web de PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] vérifie et corrige éventuellement certaines erreurs formelles dans vos codes sources : - -- supprime le [BOM |nette:glossary#BOM] -- vérifie la validité des templates [Latte |latte:] -- vérifie la validité des fichiers `.neon`, `.php` et `.json` -- vérifie la présence de [caractères de contrôle |nette:glossary#Caractères de contrôle] -- vérifie si le fichier est encodé en UTF-8 -- vérifie les `/* @anotace */` mal écrites (manque une étoile) -- supprime les `?>` de fin dans les fichiers PHP -- supprime les espaces de fin de ligne et les lignes vides inutiles à la fin du fichier -- normalise les séparateurs de ligne en séparateurs système (si vous spécifiez l'option `-l`) - - -Composer -======== - -[Composer |best-practices:composer] est un outil de gestion des dépendances en PHP. Il nous permet de déclarer des dépendances arbitrairement complexes de différentes bibliothèques et les installe ensuite pour nous dans notre projet. - - -Requirements Checker -==================== - -C'était un outil qui testait l'environnement d'exécution du serveur et informait si (et dans quelle mesure) le framework pouvait être utilisé. Actuellement, Nette peut être utilisé sur n'importe quel serveur disposant de la version minimale requise de PHP. diff --git a/best-practices/fr/form-reuse.texy b/best-practices/fr/form-reuse.texy index 234ff21ee0..439a6cc1ef 100644 --- a/best-practices/fr/form-reuse.texy +++ b/best-practices/fr/form-reuse.texy @@ -1,16 +1,16 @@ -Réutilisation des formulaires à plusieurs endroits -************************************************** +Réutiliser les formulaires à plusieurs endroits +*********************************************** .[perex] -Dans Nette, vous disposez de plusieurs options pour utiliser le même formulaire à plusieurs endroits sans dupliquer de code. Dans cet article, nous allons examiner différentes solutions, y compris celles que vous devriez éviter. +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 formulaires -====================== +Factory de formulaire +===================== -L'une des approches fondamentales pour utiliser le même composant à plusieurs endroits est de créer une méthode ou une classe qui génère ce composant, puis d'appeler cette méthode à différents endroits de l'application. Une telle méthode ou classe est appelée une *factory*. Ne confondez pas s'il vous plaît avec le patron de conception *factory method*, qui décrit une manière spécifique d'utiliser les factories et n'est pas lié à ce sujet. +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. -À titre d'exemple, créons une factory qui assemblera un formulaire d'édition : +Créons par exemple une factory qui construit un formulaire d'édition : ```php use Nette\Application\UI\Form; @@ -21,21 +21,21 @@ class FormFactory { $form = new Form; $form->addText('title', 'Titre :'); - // ici, on ajoute d'autres champs de formulaire - $form->addSubmit('send', 'Envoyer'); + // d'autres champs du formulaire sont ajoutés ici + $form->addSubmit('send', 'Enregistrer'); return $form; } } ``` -Vous pouvez maintenant utiliser cette factory à différents endroits de votre application, par exemple dans les presenters ou les composants. Et ce, en la [demandant comme dépendance|dependency-injection:passing-dependencies]. Tout d'abord, inscrivons la classe dans le fichier de configuration : +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 ``` -Et ensuite, utilisons-la dans un presenter : +Puis utilisez-la dans un presenter : ```php @@ -57,7 +57,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Vous pouvez étendre la factory de formulaires avec d'autres méthodes pour créer d'autres types de formulaires selon les besoins de votre application. Et bien sûr, nous pouvons également ajouter une méthode qui créera un formulaire de base sans éléments, et que les autres méthodes utiliseront : +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 @@ -72,20 +72,20 @@ class FormFactory { $form = $this->createForm(); $form->addText('title', 'Titre :'); - // ici, on ajoute d'autres champs de formulaire - $form->addSubmit('send', 'Envoyer'); + // d'autres champs du formulaire sont ajoutés ici + $form->addSubmit('send', 'Enregistrer'); return $form; } } ``` -La méthode `createForm()` ne fait rien d'utile pour le moment, mais cela changera rapidement. +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 s'avérera que nous avons besoin que les formulaires soient multilingues. Cela signifie que nous devons définir un [traducteur |forms:rendering#Traduction] pour tous les formulaires. À cette fin, nous modifierons la classe `FormFactory` pour qu'elle accepte un objet `Translator` comme dépendance dans le constructeur, et nous le transmettrons au formulaire : +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; @@ -108,13 +108,13 @@ class FormFactory } ``` -Comme la méthode `createForm()` est également appelée par les autres méthodes créant des formulaires spécifiques, il suffit de définir le traducteur uniquement dans celle-ci. Et c'est fait. Il n'est pas nécessaire de modifier le code d'aucun presenter ou composant, ce qui est génial. +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 de factory -============================ +Plusieurs classes factory +========================= -Alternativement, vous pouvez créer plusieurs classes pour chaque formulaire que vous souhaitez utiliser dans votre application. Cette approche peut améliorer la lisibilité du code et faciliter la gestion des formulaires. Nous laisserons la `FormFactory` originale créer uniquement un formulaire propre avec une configuration de base (par exemple, avec prise en charge des traductions) et pour le formulaire d'édition, nous créerons une nouvelle factory `EditFormFactory`. +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 @@ -144,39 +144,39 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - // ici, on ajoute d'autres champs de formulaire - $form->addSubmit('send', 'Envoyer'); + // d'autres champs du formulaire sont ajoutés ici + $form->addSubmit('send', 'Enregistrer'); return $form; } } ``` -Il est très important que la liaison entre les classes `FormFactory` et `EditFormFactory` soit réalisée par [composition |nette:introduction-to-object-oriented-programming#Composition], et non par [héritage objet |nette:introduction-to-object-oriented-programming#Héritage] : +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 -// ⛔ PAS COMME ÇA ! L'HÉRITAGE N'A PAS SA PLACE ICI +// ⛔ 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 :'); - // ici, on ajoute d'autres champs de formulaire - $form->addSubmit('send', 'Envoyer'); + // d'autres champs du formulaire sont ajoutés ici + $form->addSubmit('send', 'Enregistrer'); return $form; } } ``` -L'utilisation de l'héritage serait dans ce cas totalement contre-productive. Vous rencontreriez des problèmes très rapidement. Par exemple, au moment où vous voudriez ajouter des paramètres à la méthode `create()` ; PHP signalerait une erreur indiquant que sa signature diffère de celle du parent. Ou lors de la transmission de dépendances à la classe `EditFormFactory` via le constructeur. Une situation appelée [enfer du constructeur |dependency-injection:passing-dependencies#Constructor hell] se produirait. +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]. -En général, il est préférable de privilégier la [composition plutôt que l'héritage |dependency-injection:faq#Pourquoi la composition est-elle préférée à l héritage]. +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 ?]. -Gestion du formulaire -===================== +Traitement du formulaire +======================== -La gestion du formulaire, qui est appelée après une soumission réussie, peut également faire partie de la classe factory. Elle fonctionnera en transmettant les données soumises au modèle pour traitement. Les erreurs éventuelles seront [retournées |forms:validation#Erreurs lors du traitement] au formulaire. Le modèle dans l'exemple suivant est représenté par la classe `Facade` : +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 @@ -191,13 +191,13 @@ class EditFormFactory { $form = $this->formFactory->create(); $form->addText('title', 'Titre :'); - // ici, on ajoute d'autres champs de formulaire - $form->addSubmit('send', 'Envoyer'); - $form->onSuccess[] = [$this, 'processForm']; + // d'autres champs du formulaire sont ajoutés ici + $form->addSubmit('send', 'Enregistrer'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // traitement des données soumises @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Cependant, nous laisserons la redirection elle-même au presenter. Celui-ci ajoutera un autre gestionnaire à l'événement `onSuccess`, qui effectuera la redirection. Grâce à cela, il sera possible d'utiliser le formulaire dans différents presenters et de rediriger différemment dans chacun d'eux. +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 @@ -232,24 +232,24 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Cette solution utilise la propriété des formulaires selon laquelle si `addError()` est appelé sur le formulaire ou l'un de ses éléments, le gestionnaire `onSuccess` suivant n'est plus appelé. +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éritage de la classe Form -========================== +Hériter de la classe Form +========================= -Un formulaire assemblé ne doit pas être un descendant du formulaire. En d'autres termes, n'utilisez pas cette solution : +Un formulaire assemblé ne devrait pas être un descendant de la classe `Form`. Autrement dit, évitez cette approche : ```php -// ⛔ PAS COMME ÇA ! L'HÉRITAGE N'A PAS SA PLACE ICI +// ⛔ 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 :'); - // ici, on ajoute d'autres champs de formulaire - $this->addSubmit('send', 'Envoyer'); + // d'autres champs du formulaire sont ajoutés ici + $this->addSubmit('send', 'Enregistrer'); $this->setTranslator($translator); } } @@ -257,13 +257,13 @@ class EditForm extends Form Au lieu d'assembler le formulaire dans le constructeur, utilisez une factory. -Il faut comprendre que la classe `Form` est avant tout un outil pour assembler un formulaire, c'est-à-dire un *form builder*. Et le formulaire assemblé peut être considéré comme son produit. Or, le produit n'est pas un cas spécifique du builder, il n'y a pas entre eux de relation *is a* qui constitue la base de l'héritage. +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 avec formulaire -========================= +Composant formulaire +==================== -Une approche totalement différente consiste à créer un [composant|application:components] qui inclut un formulaire. Cela offre de nouvelles possibilités, par exemple rendre le formulaire d'une manière spécifique, car le composant inclut également un template. Ou bien, on peut utiliser des signaux pour la communication AJAX et le chargement différé d'informations dans le formulaire, par exemple pour l'autocomplétion, etc. +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 @@ -282,14 +282,14 @@ class EditControl extends Nette\Application\UI\Control { $form = new Form; $form->addText('title', 'Titre :'); - // ici, on ajoute d'autres champs de formulaire - $form->addSubmit('send', 'Envoyer'); - $form->onSuccess[] = [$this, 'processForm']; + // d'autres champs du formulaire sont ajoutés ici + $form->addSubmit('send', 'Enregistrer'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // traitement des données soumises @@ -306,7 +306,7 @@ class EditControl extends Nette\Application\UI\Control } ``` -Nous allons également créer une factory qui produira ce composant. Il suffit d'[écrire son interface |application:components#Composants avec dépendances] : +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 @@ -315,14 +315,14 @@ interface EditControlFactory } ``` -Et l'ajouter au fichier de configuration : +Et de l'ajouter au fichier de configuration : ```neon services: - EditControlFactory ``` -Et maintenant, nous pouvons demander la factory et l'utiliser dans le presenter : +Nous pouvons maintenant demander la factory et l'utiliser dans le presenter : ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // ou nous redirigeons vers le résultat de l'édition, par ex. : + // ou redirection vers le résultat de l'édition, par exemple : // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/fr/inject-method-attribute.texy b/best-practices/fr/inject-method-attribute.texy index a9896afe31..d8629951a4 100644 --- a/best-practices/fr/inject-method-attribute.texy +++ b/best-practices/fr/inject-method-attribute.texy @@ -2,17 +2,17 @@ Méthodes et attributs inject **************************** .[perex] -Dans cet article, nous nous concentrerons sur les différentes manières de transmettre des dépendances aux presenters dans le framework Nette. Nous comparerons la méthode préférée, qui est le constructeur, avec d'autres options telles que les méthodes et les attributs `inject`. +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 également, la transmission de dépendances via le [constructeur |dependency-injection:passing-dependencies#Passage par constructeur] est la voie préférée. Cependant, si vous créez un ancêtre commun dont héritent d'autres presenters (par exemple `BasePresenter`), et que cet ancêtre a également des dépendances, un problème survient que nous appelons [l'enfer du constructeur |dependency-injection:passing-dependencies#Constructor hell]. Celui-ci peut être contourné en utilisant des voies alternatives, qui sont les méthodes et les attributs (annotations) `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*()` ==================== -Il s'agit d'une forme de transmission de dépendances par [setter |dependency-injection:passing-dependencies#Passage par setter]. Le nom de ces setters commence 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 et leur transmet toutes les dépendances requises. Elles doivent donc être déclarées comme public. +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 considérées comme une sorte d'extension du constructeur en plusieurs méthodes. Grâce à cela, `BasePresenter` peut recevoir des dépendances via une autre méthode et laisser le constructeur libre pour ses descendants : +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 @@ -36,15 +36,15 @@ class MyPresenter extends BasePresenter } ``` -Un presenter peut contenir un nombre quelconque de méthodes `inject*()` et chacune peut avoir un nombre quelconque de paramètres. Elles sont également très utiles dans les cas où le presenter est [composé de traits |presenter-traits] et que chacun d'eux nécessite sa propre dépendance. +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` ================== -Il s'agit d'une forme d'[injection dans la propriété |dependency-injection:passing-dependencies#Assignation à une variable]. Il suffit de marquer les propriétés dans lesquelles injecter, et Nette DI transmet automatiquement les dépendances juste après la création de l'instance du presenter. Pour pouvoir les insérer, il est nécessaire de les déclarer comme public. +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. -Nous marquons les propriétés avec un attribut : (auparavant, l'annotation `/** @inject */` était utilisée) +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 @@ -56,6 +56,6 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -L'avantage de cette méthode de transmission des 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], il semble plus facile d'utiliser le constructeur. +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. -Inversement, cette méthode souffre des mêmes défauts que la transmission de dépendances aux propriétés en général : nous n'avons aucun contrôle sur les changements dans la variable, et en même temps, la variable devient partie intégrante de l'interface publique de la classe, ce qui n'est pas souhaitable. +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 index 081268c0b0..e888d3c771 100644 --- a/best-practices/fr/lets-create-contact-form.texy +++ b/best-practices/fr/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -Création d'un formulaire de contact -*********************************** +Créons un formulaire de contact +******************************* .[perex] -Nous allons voir comment créer un formulaire de contact dans Nette, y compris l'envoi par e-mail. Alors, allons-y ! +Voyons comment créer dans Nette un formulaire de contact, envoi des données saisies par e-mail compris. Allons-y ! -Tout d'abord, nous devons créer un nouveau projet. La page [Démarrage |nette:installation] explique comment faire. Ensuite, nous pouvons commencer à créer le formulaire. +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. -Le plus simple est de créer le [formulaire directement dans le presenter |forms:in-presenter]. Nous pouvons utiliser le `HomePresenter` pré-préparé. Nous y ajouterons le composant `contactForm` représentant le formulaire. Pour ce faire, nous écrirons dans le code une méthode factory `createComponentContactForm()` qui fabriquera le composant : +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; @@ -18,26 +18,26 @@ class HomePresenter extends Presenter { $form = new Form; $form->addText('name', 'Nom :') - ->setRequired('Veuillez entrer votre nom'); + ->setRequired('Veuillez saisir votre nom'); $form->addEmail('email', 'E-mail :') - ->setRequired('Veuillez entrer votre e-mail'); - $form->addTextarea('message', 'Message :') - ->setRequired('Veuillez entrer votre message'); + ->setRequired('Veuillez saisir votre e-mail'); + $form->addTextArea('message', 'Message :') + ->setRequired('Veuillez saisir un message'); $form->addSubmit('send', 'Envoyer'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { - // envoi de l'e-mail + // envoi d'un e-mail } } ``` -Comme vous pouvez le voir, nous avons créé deux méthodes. La première méthode `createComponentContactForm()` crée un nouveau formulaire. Il comporte des champs pour le nom, l'e-mail et le message, que nous ajoutons avec les méthodes `addText()`, `addEmail()` et `addTextArea()`. Nous avons également ajouté un bouton pour soumettre le formulaire. Mais que se passe-t-il si l'utilisateur ne remplit pas un champ ? Dans ce cas, nous devrions lui faire savoir que c'est un champ obligatoire. Nous y sommes parvenus avec la méthode `setRequired()`. Enfin, nous avons également ajouté un [événement |nette:glossary#Événements events] `onSuccess`, qui se déclenche si le formulaire est soumis avec succès. Dans notre cas, il appelle la méthode `contactFormSucceeded`, qui se chargera du traitement du formulaire soumis. Nous ajouterons cela au code dans un instant. +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. -Nous laisserons le composant `contactForm` se rendre dans le template `Home/default.latte` : +Rendons le composant `contactForm` dans le template `Home/default.latte` : ```latte {block content} @@ -45,12 +45,9 @@ Nous laisserons le composant `contactForm` se rendre dans le template `Home/defa {control contactForm} ``` -Pour l'envoi de l'e-mail lui-même, nous créerons une nouvelle classe que nous appellerons `ContactFacade` et la placerons dans le fichier `app/Model/ContactFacade.php` : +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 -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -76,9 +73,9 @@ class ContactFacade } ``` -La méthode `sendMessage()` crée et envoie l'e-mail. Pour ce faire, elle utilise ce qu'on appelle un mailer, qu'elle reçoit comme dépendance via le constructeur. Apprenez-en davantage sur l'[envoi d'e-mails |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:]. -Maintenant, revenons au presenter et complétons la méthode `contactFormSucceeded()`. Elle appellera la méthode `sendMessage()` de la classe `ContactFacade` et lui transmettra les données du formulaire. Et comment obtenir l'objet `ContactFacade` ? Nous le laisserons nous être transmis par le constructeur : +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; @@ -106,16 +103,16 @@ class HomePresenter extends Presenter } ``` -Après l'envoi de l'e-mail, nous afficherons également à l'utilisateur un [message flash |application:components#Messages Flash] confirmant que le message a été envoyé, puis nous redirigerons vers la page actuelle afin qu'il ne soit pas possible de soumettre à nouveau le formulaire en utilisant *refresh* dans le navigateur. +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. -Voilà, et si tout fonctionne, vous devriez pouvoir envoyer un e-mail depuis votre formulaire de contact. Félicitations ! +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, un e-mail en texte brut est envoyé, contenant uniquement le message soumis par le formulaire. Mais nous pouvons utiliser le HTML dans l'e-mail et rendre son apparence plus attrayante. Nous allons créer un template pour cela en Latte, que nous écrirons dans `app/Model/contactEmail.latte` : +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> @@ -129,7 +126,7 @@ Pour l'instant, un e-mail en texte brut est envoyé, contenant uniquement le mes </html> ``` -Il reste à modifier `ContactFacade`, pour qu'il utilise ce template. Dans le constructeur, nous demanderons la classe `LatteFactory`, qui sait fabriquer un objet `Latte\Engine`, c'est-à-dire le [moteur de rendu de templates Latte |latte:develop#Comment rendre un template]. Avec la méthode `renderToString()`, nous rendrons le template dans un fichier, le premier paramètre est le chemin vers le template et le second sont les variables. +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; @@ -165,15 +162,15 @@ class ContactFacade } ``` -Nous transmettrons ensuite l'e-mail HTML généré à la méthode `setHtmlBody()` au lieu de l'original `setBody()`. De même, nous n'avons pas besoin de spécifier l'objet de l'e-mail dans `setSubject()`, car la bibliothèque le prendra à partir de l'élément `<title>` du template. +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 `<title>` du template. Configuration ------------- -Dans le code de la classe `ContactFacade`, notre e-mail administrateur `admin@example.com` est toujours codé en dur. Il serait préférable de le déplacer dans le fichier de configuration. Comment faire ? +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 ? -Tout d'abord, modifions la classe `ContactFacade` et remplaçons la chaîne avec l'e-mail par une variable transmise par le constructeur : +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 @@ -197,21 +194,21 @@ class ContactFacade } ``` -Et la deuxième étape consiste à indiquer la valeur de cette variable dans la configuration. Dans le fichier `config/services.neon` (ou `app/config/services.neon` dans les versions plus anciennes), nous écrirons : +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 c'est tout. S'il y a beaucoup d'éléments dans la section `services` et que vous avez l'impression que l'e-mail se perd parmi eux, nous pouvons en faire une variable. Modifions l'écriture en : +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 dans le fichier `app/config/common.neon`, nous définirons cette variable : +Et définissez ce paramètre dans le fichier `app/config/common.neon` : ```neon parameters: diff --git a/best-practices/fr/microsites.texy b/best-practices/fr/microsites.texy index cf0410f4ed..1f70d1363f 100644 --- a/best-practices/fr/microsites.texy +++ b/best-practices/fr/microsites.texy @@ -1,11 +1,11 @@ -Comment écrire des micro-sites -****************************** +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 pourriez penser que pour un si petit projet, vous n'avez pas besoin d'un framework robuste. Mais que se passerait-il si l'utilisation du framework Nette pouvait simplifier et accélérer considérablement ce processus ? +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 lors de la création de sites web simples, vous ne voulez pas renoncer au confort. Vous ne voulez pas réinventer ce qui a déjà été résolu une fois. Soyez paresseux et laissez-vous choyer. Nette Framework peut également être parfaitement utilisé comme micro framework. +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, en plaçant tout le code du site web dans un seul fichier `index.php` dans le dossier public : +À 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 @@ -16,48 +16,48 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// créer un conteneur DI basé sur la configuration dans config.neon +// crée le conteneur DI d'après la configuration dans config.neon $configurator->addConfig(__DIR__ . '/../app/config.neon'); $container = $configurator->createContainer(); -// configurer le routage +// 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étecter la langue du navigateur et rediriger vers l'URL /en ou /de etc. - $supportedLangs = ['en', 'de', 'fr']; + // 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); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); -// route pour l'URL https://example.com/fr ou https://example.com/en -$router->addRoute('<lang fr|en>', function ($presenter, string $lang) { - // afficher le template correspondant, par exemple ../templates/fr.latte +// 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; }); -// lancer l'application ! +// lance l'application ! $container->getByType(Nette\Application\Application::class)->run(); ``` -Tout le reste sera constitué de templates stockés dans le dossier parent `/templates`. +Tout le reste sera constitué de templates enregistrés dans le répertoire parent `/templates`. -Le code PHP dans `index.php` [prépare d'abord l'environnement |bootstrap:], puis définit les [routes |application:routing#Routage dynamique avec callbacks] et enfin lance l'application. L'avantage est que le deuxième paramètre de la fonction `addRoute()` peut être un callable, qui sera exécuté après l'ouverture de la page correspondante. +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 un microsite ? -------------------------------------------- +Pourquoi utiliser Nette pour des microsites ? +--------------------------------------------- -- Les programmeurs qui ont déjà essayé [Tracy|tracy:] ne peuvent plus imaginer programmer quoi que ce soit sans elle aujourd'hui. -- Mais surtout, vous utiliserez le système de templates [Latte|latte:], car dès 2 pages, vous voudrez avoir une [mise en page et un contenu|latte:template-inheritance] séparés. -- Et vous voulez absolument compter sur l'[échappement automatique |latte:safety-first] pour éviter la vulnérabilité XSS. -- Nette garantit également qu'en cas d'erreur, les messages d'erreur PHP destinés aux programmeurs ne s'afficheront jamais, mais une page compréhensible par l'utilisateur. -- Si vous souhaitez obtenir des retours d'utilisateurs, par exemple sous la forme d'un formulaire de contact, vous ajouterez également des [formulaires|forms:] et une [base de données|database:]. -- Vous pouvez également faire [envoyer par e-mail|mail:] facilement les formulaires remplis. -- Parfois, la [mise en cache|caching:] peut vous être utile, par exemple si vous téléchargez et affichez des flux. +- 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. -À notre époque, où la vitesse et l'efficacité sont essentielles, il est important de disposer d'outils qui vous permettent d'obtenir des résultats sans délai inutile. Le framework Nette vous offre exactement cela - un développement rapide, la sécurité et une large gamme d'outils, tels que Tracy et Latte, qui simplifient le processus. Il suffit d'installer quelques paquets Nette et construire un tel microsite devient soudain un jeu d'enfant. Et vous savez qu'aucune faille de sécurité ne se cache nulle part. +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 index 4c008577ae..7d42712ba4 100644 --- a/best-practices/fr/pagination.texy +++ b/best-practices/fr/pagination.texy @@ -1,10 +1,10 @@ -Pagination des résultats de la base de données -********************************************** +Paginer les résultats de base de données +**************************************** .[perex] -Lors de la création d'applications web, vous rencontrerez très souvent la nécessité de limiter le nombre d'éléments affichés par page. +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. -Nous partons d'un état où nous affichons toutes les données sans pagination. Pour sélectionner les données de la base de données, nous avons une classe `ArticleRepository` qui, en plus du constructeur, contient une méthode `findPublishedArticles` qui renvoie tous les articles publiés triés par ordre décroissant de date de publication. +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; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -Dans le presenter, nous injectons ensuite la classe de modèle et dans la méthode render, nous demandons les articles publiés, que nous transmettons au template : +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; @@ -52,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Dans le template `default.latte`, nous nous occupons ensuite de l'affichage des articles : +Le template `default.latte` se chargera ensuite de lister les articles : ```latte {block content} @@ -67,11 +67,11 @@ Dans le template `default.latte`, nous nous occupons ensuite de l'affichage des ``` -De cette manière, nous savons afficher tous les articles, ce qui commencera cependant à poser problème lorsque le nombre d'articles augmentera. À ce moment-là, l'implémentation d'un mécanisme de pagination s'avérera utile. +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. -Celui-ci garantira que tous les articles sont répartis sur plusieurs pages et que nous n'affichons que les articles d'une page actuelle. Le nombre total de pages et la répartition des articles seront calculés par [Paginator |utils:paginator] lui-même en fonction du nombre total d'articles que nous avons et du nombre d'articles que nous voulons afficher par page. +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 modifions la méthode d'obtention des articles dans la classe du repository afin qu'elle puisse nous renvoyer uniquement les articles d'une page. Nous ajoutons également une méthode pour connaître le nombre total d'articles dans la base de données, dont nous aurons besoin pour configurer le Paginator : +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; @@ -108,9 +108,9 @@ class ArticleRepository } ``` -Ensuite, nous nous attaquons aux modifications du presenter. Dans la méthode render, nous transmettrons le numéro de la page actuellement affichée. Au cas où ce numéro ne ferait pas partie de l'URL, nous définirons la valeur par défaut de la première page. +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 également la méthode render pour obtenir l'instance du Paginator, la configurer et sélectionner les bons articles à afficher dans le template. Le `HomePresenter` ressemblera à ceci après les modifications : +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; @@ -127,27 +127,27 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Nous obtenons le nombre total d'articles publiés + // Récupère le nombre total d'articles publiés $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Nous fabriquons une instance de Paginator et la configurons + // Crée et configure l'instance de Paginator $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // nombre total d'articles - $paginator->setItemsPerPage(10); // nombre d'éléments par page - $paginator->setPage($page); // numéro de la page actuelle + $paginator->setItemCount($articlesCount); // nombre total d'éléments + $paginator->setItemsPerPage(10); // éléments par page + $paginator->setPage($page); // numéro de la page courante - // Nous extrayons de la base de données un ensemble limité d'articles selon le calcul du Paginator + // Récupère dans la base un ensemble limité d'articles selon le calcul du Paginator $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - // que nous transmettons au template + // les passe au template $this->template->articles = $articles; - // et aussi le Paginator lui-même pour afficher les options de pagination + // ainsi que le Paginator lui-même, pour afficher les commandes de pagination $this->template->paginator = $paginator; } } ``` -Le template itère désormais uniquement sur les articles d'une seule page, il nous suffit d'ajouter les liens de pagination : +Le template ne parcourt maintenant que les articles de la page courante. Il ne reste qu'à ajouter les liens de pagination : ```latte {block content} @@ -164,7 +164,7 @@ Le template itère désormais uniquement sur les articles d'une seule page, il n {if !$paginator->isFirst()} <a n:href="default, 1">Première</a>  |  - <a n:href="default, $paginator->page - 1">Précédente</a> + <a n:href="default, $paginator->getPage() - 1">Précédente</a>  |  {/if} @@ -180,9 +180,9 @@ Le template itère désormais uniquement sur les articles d'une seule page, il n ``` -Nous avons ainsi complété la page avec la possibilité de pagination à l'aide du Paginator. Dans le cas où, au lieu de [Nette Database Core |database:sql-way] comme couche de base de données, nous utilisons [Nette Database Explorer |database:explorer], nous sommes capables d'implémenter la pagination i sans utiliser le Paginator. La classe `Nette\Database\Table\Selection` contient en effet une méthode [page() |api:Nette\Database\Table\Selection::page] avec la logique de pagination intégrée. +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. -Le repository ressemblera à ceci avec cette méthode d'implémentation : +Avec cette approche, le repository ressemblera à ceci : ```php namespace App\Model; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -Dans le presenter, nous n'avons pas besoin de créer de Paginator, nous utilisons directement la méthode `page()` de la classe `Selection` que nous renvoie le repository : +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; @@ -222,21 +222,21 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Nous extrayons les articles publiés + // Récupère les articles publiés $articles = $this->articleRepository->findPublishedArticles(); - // et nous envoyons au template seulement une partie d'entre eux limitée selon le calcul de la méthode page + // 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); - // et aussi les données nécessaires pour afficher les options de pagination + // ainsi que les données nécessaires à l'affichage des options de pagination $this->template->page = $page; $this->template->lastPage = $lastPage; } } ``` -Comme nous n'envoyons plus de Paginator au template, nous modifions la partie affichant les liens de pagination : +Comme nous ne passons plus l'objet Paginator au template, nous devons adapter la partie qui affiche les liens de pagination : ```latte {block content} @@ -268,6 +268,6 @@ Comme nous n'envoyons plus de Paginator au template, nous modifions la partie af </div> ``` -De cette manière, nous avons implémenté le mécanisme de pagination en utilisant la méthode `page()` de Nette Database Explorer. +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 index 3aeba172c5..0d1fae937d 100644 --- a/best-practices/fr/passing-settings-to-presenters.texy +++ b/best-practices/fr/passing-settings-to-presenters.texy @@ -1,10 +1,10 @@ -Transmission des paramètres aux presenters -****************************************** +Passer des réglages aux presenters +********************************** .[perex] -Avez-vous besoin de transmettre aux presenters des arguments qui ne sont pas des objets (par exemple, l'information s'ils s'exécutent en mode débogage, les chemins vers les répertoires, etc.) et qui ne peuvent donc pas être transmis automatiquement via l'autowiring ? La solution est de les encapsuler dans un objet `Settings`. +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` représente une manière très simple et pourtant utile de fournir des informations sur l'application en cours d'exécution aux presenters. Sa forme concrète dépend entièrement de vos besoins spécifiques. Exemple : +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; @@ -12,7 +12,7 @@ namespace App; class Settings { public function __construct( - // à partir de PHP 8.1, il est possible d'indiquer readonly + // depuis PHP 8.1, readonly peut être utilisé public bool $debugMode, public string $appDir, // et ainsi de suite @@ -30,7 +30,7 @@ services: ) ``` -Lorsque le presenter aura besoin des informations fournies par ce service, il les demandera simplement dans le constructeur : +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 diff --git a/best-practices/fr/post-links.texy b/best-practices/fr/post-links.texy index d4c098524f..fba0b19720 100644 --- a/best-practices/fr/post-links.texy +++ b/best-practices/fr/post-links.texy @@ -1,16 +1,16 @@ -Comment utiliser correctement les liens POST -******************************************** +Comment bien utiliser les liens POST +************************************ .[perex] -Dans les applications web, en particulier dans les interfaces d'administration, une règle de base devrait être que les actions modifiant l'état du serveur ne devraient pas être effectuées via la méthode HTTP GET. Comme le nom de la méthode l'indique, GET devrait servir uniquement à obtenir des données, non à les modifier. Pour des actions telles que la suppression d'enregistrements, il est préférable d'utiliser la méthode POST. Bien que l'idéal serait la méthode DELETE, mais elle ne peut pas être invoquée sans JavaScript, c'est pourquoi POST est historiquement utilisé. +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 faire en pratique ? Utilisez cette astuce simple. Au début du template de votre layout, créez un formulaire auxiliaire avec l'identifiant `postForm`, que vous utiliserez ensuite pour les boutons de suppression : +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, vous pouvez utiliser un bouton `<button>` au lieu d'un lien classique `<a>`, qui peut être visuellement stylisé pour ressembler à un lien normal. Par exemple, le framework CSS Bootstrap propose les classes `btn btn-link` avec lesquelles vous obtiendrez que le bouton ne soit pas visuellement différent des autres liens. À l'aide de l'attribut `form="postForm"`, nous le lions au formulaire pré-préparé : +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> @@ -24,7 +24,7 @@ Grâce à ce formulaire, vous pouvez utiliser un bouton `<button>` au lieu d'un </table> ``` -En cliquant sur le bouton, l'action `delete` est maintenant invoquée. Pour garantir que les requêtes ne soient acceptées que via la méthode POST et depuis le même domaine (ce qui est une défense efficace contre les attaques CSRF), utilisez l'attribut `#[Requires]` : +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; @@ -34,15 +34,15 @@ 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 supprimant l'enregistrement + $this->facade->deletePost($id); // code hypothétique de suppression d'un enregistrement $this->redirect('default'); } } ``` -L'attribut existe depuis Nette Application 3.2 et vous en apprendrez plus sur ses possibilités sur la page [Comment utiliser l'attribut #Requires |attribute-requires]. +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()`, il n'est pas nécessaire d'indiquer `sameOrigin: true`, car les signaux ont cette protection définie implicitement : +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')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -Cette approche améliore non seulement la sécurité de votre application, mais contribue également au respect des normes et pratiques web correctes. En utilisant les méthodes POST pour les actions modifiant l'état, vous obtiendrez une application plus robuste et plus sûre. +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 index 551c1d5368..f56299f78b 100644 --- a/best-practices/fr/presenter-traits.texy +++ b/best-practices/fr/presenter-traits.texy @@ -1,12 +1,12 @@ -Composition des presenters à partir de traits -********************************************* +Composer des presenters à partir de traits +****************************************** .[perex] -Si nous avons besoin d'implémenter le même code dans plusieurs presenters (par exemple, vérifier si l'utilisateur est connecté), il est possible de placer le code dans un ancêtre commun. La deuxième option est de créer des [traits |nette:introduction-to-object-oriented-programming#Traits] à usage unique. +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 de cette solution est que chaque presenter peut utiliser exactement les traits dont il a réellement besoin, tandis que l'héritage multiple n'est pas possible en PHP. +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 lors de la création du presenter, toutes les [méthodes inject |inject-method-attribute#Méthodes inject] sont appelées successivement. Il faut juste s'assurer que le nom de chaque méthode inject est unique. +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]. @@ -36,7 +36,7 @@ trait StandardTemplateFilters } ``` -Le presenter utilise ensuite simplement ces traits : +Le presenter se contente ensuite d'utiliser ces traits : ```php class ArticlePresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/fr/pretty-urls.texy b/best-practices/fr/pretty-urls.texy new file mode 100644 index 0000000000..244b566ba4 --- /dev/null +++ b/best-practices/fr/pretty-urls.texy @@ -0,0 +1,204 @@ +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 index 277e87d9ff..9f6e7f584f 100644 --- a/best-practices/fr/restore-request.texy +++ b/best-practices/fr/restore-request.texy @@ -2,15 +2,15 @@ Comment revenir à une page précédente ? *************************************** .[perex] -Que se passe-t-il si un utilisateur remplit un formulaire et que sa session expire ? Pour qu'il ne perde pas ses données, nous sauvegardons les données dans la session avant de le rediriger vers la page de connexion. Dans Nette, c'est un jeu d'enfant. +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 actuelle peut être sauvegardée dans la session à l'aide de la méthode `storeRequest()`, qui renvoie son identifiant sous forme de chaîne courte. La méthode sauvegarde le nom du presenter actuel, la vue et ses paramètres. Si un formulaire a également été soumis, le contenu des champs (à l'exception des fichiers téléchargés) est également sauvegardé. +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 restauration de la requête est effectuée par la méthode `restoreRequest($key)`, à laquelle nous passons l'identifiant obtenu. Elle redirige vers le presenter et la vue d'origine. Cependant, si la requête sauvegardée contient une soumission de formulaire, elle passe au presenter d'origine via la méthode `forward()`, transmet les valeurs précédemment remplies au formulaire et le laisse se rendre à nouveau. L'utilisateur a ainsi la possibilité de soumettre à nouveau le formulaire et aucune donnée n'est perdue. +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. -Il est important de noter que `restoreRequest()` vérifie si l'utilisateur nouvellement connecté est le même que celui qui a initialement rempli le formulaire. Si ce n'est pas le cas, elle rejette la requête et ne fait rien. +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 tout cela par un exemple. Supposons que nous ayons un presenter `AdminPresenter`, dans lequel des données sont éditées et dans la méthode `startup()` duquel nous vérifions si l'utilisateur est connecté. S'il ne l'est pas, nous le redirigeons vers `SignPresenter`. En même temps, nous sauvegardons la requête actuelle et envoyons sa clé à `SignPresenter`. +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 @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Le presenter `SignPresenter` contiendra, en plus du formulaire de connexion, un paramètre persistant `$backlink`, dans lequel la clé sera écrite. Comme le paramètre est persistant, il sera également transmis après la soumission du formulaire de connexion. +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 @@ -40,14 +40,14 @@ class SignPresenter extends Nette\Application\UI\Presenter protected function createComponentSignInForm() { $form = new Nette\Application\UI\Form; - // ... ajouter les champs du formulaire ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + // ... ajout des champs du formulaire ... + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { - // ... ici, nous connectons l'utilisateur ... + // ... connexion de l'utilisateur ici ... $this->restoreRequest($this->backlink); $this->redirect('Admin:'); @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -Nous passons la clé de la requête sauvegardée à la méthode `restoreRequest()` et elle redirige (ou avance) vers le presenter d'origine. +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. -Cependant, si la clé n'est pas valide (par exemple, elle n'existe plus dans la session), la méthode ne fait rien. L'appel `$this->redirect('Admin:')` suit donc, qui redirige vers `AdminPresenter`. +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/hu/@home.texy b/best-practices/hu/@home.texy deleted file mode 100644 index e1ab1cabd8..0000000000 --- a/best-practices/hu/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Útmutatók és eljárások -********************** - -.[perex] -Útmutatók, gyakori feladatok megoldásai és *best practices* a Nette-hez. - - -<div class=documentation> -<div> - - -Nette Alkalmazások ------------------- -- [Inject metódusok és attribútumok |inject-method-attribute] -- [Presenterek összeállítása trait-ekből |presenter-traits] -- [Beállítások átadása presentereknek |passing-settings-to-presenters] -- [Hogyan térjünk vissza egy korábbi oldalra |restore-request] -- [Adatbázis eredmények lapozása |pagination] -- [Dinamikus snippettek |dynamic-snippets] -- [Hogyan használjuk a #Requires attribútumot |attribute-requires] -- [Hogyan használjuk helyesen a POST linkeket |post-links] - -</div> -<div> - - -Űrlapok -------- -- [Űrlapok újrafelhasználása |form-reuse] -- [Űrlap rekord létrehozásához és szerkesztéséhez |creating-editing-form] -- [Készítsünk kapcsolatfelvételi űrlapot |lets-create-contact-form] -- [Függő selectboxok |https://blog.nette.org/hu/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Általános ---------- -- [Hogyan töltsünk be egy konfigurációs fájlt |bootstrap:] -- [Hogyan írjunk mikro-weboldalakat |microsites] -- [Miért használja a Nette a PascalCase konstans jelölést? |https://blog.nette.org/hu/for-less-screaming-in-the-code] -- [Miért nem használja a Nette az Interface utótagot? |https://blog.nette.org/hu/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: használati tippek |composer] -- [Tippek szerkesztőkhöz & eszközökhöz |editors-and-tools] -- [Bevezetés az objektumorientált programozásba |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Példa megoldások ----------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Videók ------- -Több száz felvétel a Poslední sobota eseményekről és Nette videók egy helyen a "Nette Framework Youtube csatornáján":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/hu/@meta.texy b/best-practices/hu/@meta.texy deleted file mode 100644 index 9a70856e97..0000000000 --- a/best-practices/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Útmutatók és eljárások}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/hu/attribute-requires.texy b/best-practices/hu/attribute-requires.texy deleted file mode 100644 index 819f823943..0000000000 --- a/best-practices/hu/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Hogyan használjuk a `#[Requires]` attribútumot -********************************************** - -.[perex] -Amikor webalkalmazást ír, gyakran találkozik azzal az igénnyel, hogy korlátozza a hozzáférést az alkalmazás bizonyos részeihez. Talán azt szeretné, hogy bizonyos kérések csak űrlapon keresztül küldhessenek adatokat (azaz POST metódussal), vagy hogy csak AJAX hívások számára legyenek elérhetők. A Nette Framework 3.2-ben megjelent egy új eszköz, amely lehetővé teszi az ilyen korlátozások nagyon elegáns és áttekinthető beállítását: a `#[Requires]` attribútum. - -Az attribútum egy speciális jelölés a PHP-ban, amelyet az osztály vagy metódus definíciója elé adunk hozzá. Mivel valójában egy osztályról van szó, ahhoz, hogy a következő példák működjenek, meg kell adni a use klauzult: - -```php -use Nette\Application\Attributes\Requires; -``` - -A `#[Requires]` attribútumot használhatja magánál a presenter osztálynál és ezeknél a metódusoknál is: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Az utolsó két metódus a komponensekre is vonatkozik, tehát az attribútumot náluk is használhatja. - -Ha az attribútum által megadott feltételek nem teljesülnek, HTTP 4xx hiba váltódik ki. - - -HTTP metódusok --------------- - -Megadhatja, hogy mely HTTP metódusok (mint GET, POST stb.) engedélyezettek a hozzáféréshez. Például, ha csak űrlapküldéssel szeretné engedélyezni a hozzáférést, állítsa be: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Miért kellene POST-ot használnia GET helyett az állapotot megváltoztató akciókhoz, és hogyan tegye ezt? [Olvassa el az útmutatót |post-links]. - -Megadhat egy metódust vagy metódusok tömbjét. Speciális eset a `'*'` érték, amely minden metódust engedélyez, amit a presenterek [biztonsági okokból |application:presenters#HTTP metódus ellenőrzése] alapértelmezés szerint nem engednek meg. - - -AJAX hívás ----------- - -Ha azt szeretné, hogy a presenter vagy metódus csak AJAX kérések számára legyen elérhető, használja: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Azonos eredet -------------- - -A biztonság növelése érdekében megkövetelheti, hogy a kérés ugyanarról a domainről érkezzen. Ezzel megakadályozhatja a [CSRF sebezhetőséget |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -A `handle<Signal>()` metódusoknál az azonos domainről való hozzáférés automatikusan megkövetelt. Tehát ha fordítva, bármely domainről szeretné engedélyezni a hozzáférést, adja meg: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Hozzáférés forwardon keresztül ------------------------------- - -Néha hasznos korlátozni a presenterhez való hozzáférést úgy, hogy csak közvetve legyen elérhető, például a `forward()` vagy `switch()` metódus használatával egy másik presenterből. Így védik például az error-presentereket, hogy ne lehessen őket URL-ből meghívni: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -A gyakorlatban gyakran szükség van bizonyos view-k megjelölésére, amelyekhez csak a presenter logikája alapján lehet eljutni. Tehát ismét, hogy ne lehessen őket közvetlenül megnyitni: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Konkrét akciók --------------- - -Korlátozhatja azt is, hogy egy bizonyos kód, például egy komponens létrehozása, csak specifikus akciókhoz legyen elérhető a presenterben: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Egyetlen akció esetén nem szükséges tömböt írni: `#[Requires(actions: 'default')]` - - -Saját attribútumok ------------------- - -Ha a `#[Requires]` attribútumot ismételten ugyanazzal a beállítással szeretné használni, létrehozhat saját attribútumot, amely örökli a `#[Requires]`-t, és az igényeknek megfelelően állítja be. - -Például a `#[SingleAction]` csak a `default` akción keresztül engedélyezi a hozzáférést: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Vagy a `#[RestMethods]` engedélyezi a hozzáférést az összes REST API-hoz használt HTTP metóduson keresztül: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Következtetés -------------- - -A `#[Requires]` attribútum nagy rugalmasságot és kontrollt ad Önnek afölött, hogyan érhetők el a weboldalai. Egyszerű, de erőteljes szabályok segítségével növelheti alkalmazása biztonságát és helyes működését. Mint láthatja, az attribútumok használata a Nette-ben nemcsak megkönnyítheti a munkáját, hanem biztonságosabbá is teheti. diff --git a/best-practices/hu/composer.texy b/best-practices/hu/composer.texy deleted file mode 100644 index 97d7faa75c..0000000000 --- a/best-practices/hu/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: tippek a használathoz -******************************* - -<div class=perex> - -A Composer egy eszköz a PHP függőségek kezelésére. Lehetővé teszi számunkra, hogy felsoroljuk azokat a könyvtárakat, amelyektől a projektünk függ, és telepíti és frissíti őket helyettünk. Megmutatjuk: - -- hogyan telepítsük a Composert -- használatát új vagy meglévő projektben - -</div> - - -Telepítés -========= - -A Composer egy futtatható `.phar` fájl, amelyet a következő módon tölthet le és telepíthet: - - -Windows -------- - -Használja a hivatalos telepítőt [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Csak 4 parancsra van szükség, amelyeket másoljon le [erről az oldalról |https://getcomposer.org/download/]. - -Továbbá, ha egy olyan mappába helyezi, amely a rendszer `PATH`-jában van, a Composer globálisan elérhetővé válik: - -```shell -$ mv ./composer.phar ~/bin/composer # vagy /usr/local/bin/composer -``` - - -Használat a projektben -====================== - -Ahhoz, hogy a projektünkben elkezdhessük használni a Composert, csak egy `composer.json` fájlra van szükségünk. Ez leírja a projektünk függőségeit, és tartalmazhat további metaadatokat is. Egy alap `composer.json` tehát így nézhet ki: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Itt azt mondjuk, hogy az alkalmazásunk (vagy könyvtárunk) megköveteli a `nette/database` csomagot (a csomag neve a szervezet nevéből és a projekt nevéből áll), és olyan verziót szeretne, amely megfelel a `^3.0` feltételnek (azaz a legújabb 3-as verziót). - -Tehát a projekt gyökerében van egy `composer.json` fájlunk, és elindítjuk a telepítést: - -```shell -composer update -``` - -A Composer letölti a Nette Database-t a `vendor/` mappába. Továbbá létrehoz egy `composer.lock` fájlt, amely információkat tartalmaz arról, hogy pontosan melyik verziójú könyvtárakat telepítette. - -A Composer generál egy `vendor/autoload.php` fájlt, amelyet egyszerűen includálhatunk, és elkezdhetjük használni a könyvtárakat bármilyen további munka nélkül: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Csomagok frissítése a legújabb verziókra -======================================== - -A használt könyvtárak frissítését a `composer.json`-ban definiált feltételek szerinti legújabb verziókra a `composer update` parancs végzi. Pl. a `"nette/database": "^3.0"` függőségnél a legújabb 3.x.x verziót telepíti, de a 4-es verziót már nem. - -A `composer.json` fájlban lévő feltételek frissítéséhez, például `"nette/database": "^4.1"`-re, hogy telepíthető legyen a legújabb verzió, használja a `composer require nette/database` parancsot. - -Az összes használt Nette csomag frissítéséhez mindet fel kellene sorolni a parancssorban, pl.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Ami nem praktikus. Használja ezért az egyszerű "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff szkriptet, amely ezt megteszi Ön helyett: - -```shell -php composer-frontline.php -``` - - -Új projekt létrehozása -====================== - -Új Nette projektet egyetlen paranccsal hozhat létre: - -```shell -composer create-project nette/web-project projekt-neve -``` - -A `projekt-neve` helyére illessze be a projekt könyvtárának nevét, és erősítse meg. A Composer letölti a `nette/web-project` repository-t a GitHubról, amely már tartalmazza a `composer.json` fájlt, és rögtön utána a Nette Frameworköt. Már csak a [jogosultságokat kell beállítani |nette:troubleshooting#Könyvtárjogosultságok beállítása] a `temp/` és `log/` mappákra való íráshoz, és a projektnek életre kell kelnie. - -Ha tudja, milyen PHP verzióval fog futni a projekt a hostingen, ne felejtse el [beállítani |#PHP verzió]. - - -PHP verzió -========== - -A Composer mindig azokat a csomagverziókat telepíti, amelyek kompatibilisek az Ön által éppen használt PHP verzióval (pontosabban a parancssorban a Composer futtatásakor használt PHP verzióval). Ami azonban valószínűleg nem ugyanaz a verzió, mint amit a hostingja használ. Ezért nagyon fontos, hogy a `composer.json` fájlba hozzáadja az információt a hostingen lévő PHP verzióról. Ezután csak a hostinggal kompatibilis csomagverziók kerülnek telepítésre. - -Azt, hogy a projekt például PHP 8.2.3-on fog futni, a következő paranccsal állítjuk be: - -```shell -composer config platform.php 8.2.3 -``` - -Így a verzió beíródik a `composer.json` fájlba: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Azonban a PHP verziószám a fájl egy másik helyén is szerepel, mégpedig a `require` szekcióban. Míg az első szám azt határozza meg, hogy melyik verzióhoz települjenek a csomagok, a második szám azt mondja meg, hogy melyik verzióhoz íródott maga az alkalmazás. És például a PhpStorm ez alapján állítja be a *PHP language level*-t. (Természetesen nincs értelme, hogy ezek a verziók eltérjenek, tehát a kettős beírás egy átgondolatlanság.) Ezt a verziót a következő paranccsal állíthatja be: - -```shell -composer require php 8.2.3 --no-update -``` - -Vagy közvetlenül a `composer.json` fájlban: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -PHP verzió figyelmen kívül hagyása -================================== - -A csomagok általában megadják mind a legalacsonyabb PHP verziót, amellyel kompatibilisek, mind a legmagasabbat, amellyel tesztelve vannak. Ha még újabb PHP verziót tervez használni, például tesztelés céljából, a Composer megtagadja az ilyen csomag telepítését. A megoldás az `--ignore-platform-req=php+` opció, amely miatt a Composer figyelmen kívül hagyja a megkövetelt PHP verzió felső határait. - - -Hamis jelentések -================ - -Csomagok frissítésekor vagy verziószámok változásakor előfordul, hogy konfliktus lép fel. Egy csomag olyan követelményekkel rendelkezik, amelyek ellentmondanak egy másiknak, és így tovább. A Composer azonban néha hamis jelentést ad. Olyan konfliktust jelez, amely valójában nem létezik. Ilyen esetben segít a `composer.lock` fájl törlése és az újrapróbálkozás. - -Ha a hibaüzenet továbbra is fennáll, akkor komolyan kell venni, és ki kell olvasni belőle, mit és hogyan kell módosítani. - - -Packagist.org - központi repository -=================================== - -A [Packagist |https://packagist.org] a fő repository, amelyben a Composer megpróbálja megkeresni a csomagokat, hacsak nem mondjuk neki másképp. Itt publikálhatunk saját csomagokat is. - - -Mi van, ha nem akarjuk használni a központi repository-t? ---------------------------------------------------------- - -Ha belső vállalati alkalmazásaink vannak, amelyeket egyszerűen nem hostolhatunk nyilvánosan, akkor létrehozunk hozzájuk egy vállalati repository-t. - -Több információ a repository-król [a hivatalos dokumentációban |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -A Composer alapvető tulajdonsága, hogy autoloadingot biztosít az összes általa telepített osztályhoz, amelyet a `vendor/autoload.php` fájl includálásával indíthat el. - -Azonban a Composert lehet használni további osztályok betöltésére is a `vendor` mappán kívül. Az első lehetőség az, hogy hagyjuk a Composert átkutatni a definiált mappákat és almappákat, megtalálni az összes osztályt, és bevenni őket az autoloaderbe. Ezt a `composer.json` `autoload > classmap` beállításával érhetjük el: - -```js -{ - "autoload": { - "classmap": [ - "src/", # beleveszi a src/ mappát és annak almappáit - ] - } -} -``` - -Ezután minden változáskor futtatni kell a `composer dumpautoload` parancsot, és hagyni kell az autoloading táblák újragenerálását. Ez rendkívül kényelmetlen, és sokkal jobb ezt a feladatot a [RobotLoaderra|robot-loader:] bízni, amely ugyanazt a tevékenységet automatikusan a háttérben és sokkal gyorsabban végzi. - -A második lehetőség a [PSR-4|https://www.php-fig.org/psr/psr-4/] betartása. Egyszerűsítve ez egy olyan rendszer, ahol a névterek és osztálynevek megfelelnek a könyvtárstruktúrának és a fájlneveknek, tehát pl. az `App\Core\RouterFactory` az `/path/to/App/Core/RouterFactory.php` fájlban lesz. Példa konfiguráció: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # az App\ névtér az app/ könyvtárban van - } - } -} -``` - -Hogyan konfigurálja pontosan a viselkedést, megtudhatja a [Composer dokumentációjában|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Új verziók tesztelése -===================== - -Szeretné tesztelni egy csomag új fejlesztői verzióját. Hogyan tegye? Először adja hozzá ezt a két opciót a `composer.json` fájlhoz, amely lehetővé teszi a fejlesztői verziójú csomagok telepítését, de csak akkor folyamodik ehhez, ha nincs olyan stabil verziókombináció, amely megfelelne a követelményeknek: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Továbbá javasoljuk a `composer.lock` fájl törlését, néha ugyanis a Composer érthetetlen módon megtagadja a telepítést, és ez megoldja a problémát. - -Tegyük fel, hogy a `nette/utils` csomagról van szó, és az új verzió száma 4.0. Telepítse a következő paranccsal: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Vagy telepíthet konkrét verziót is, például 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Ha azonban a könyvtártól egy másik csomag függ, amely egy régebbi verzióra van zárolva (pl. `^3.1`), akkor ideális a csomagot frissíteni, hogy az új verzióval működjön. Ha azonban csak meg akarja kerülni a korlátozást, és rávenni a Composert, hogy telepítse a fejlesztői verziót, és úgy tegyen, mintha egy régebbi verzió lenne (pl. 3.1.6), használhatja az `as` kulcsszót: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Parancsok hívása -================ - -A Composer segítségével saját előre elkészített parancsokat és szkripteket hívhat meg, mintha natív Composer parancsok lennének. A `vendor/bin` mappában található szkriptek esetében nem kell ezt a mappát megadni. - -Példaként definiálunk a `composer.json` fájlban egy szkriptet, amely a [Nette Testerrel|tester:] futtatja a teszteket: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -A teszteket ezután a `composer tester` segítségével futtatjuk. A parancsot akkor is meghívhatjuk, ha nem a projekt gyökérkönyvtárában vagyunk, hanem valamelyik alkönyvtárban. - - -Küldjön köszönetet -================== - -Mutatunk egy trükköt, amellyel örömet szerezhet az open source szerzőknek. Egyszerű módon adhat csillagot a GitHubon azoknak a könyvtáraknak, amelyeket a projektje használ. Csak telepíteni kell a `symfony/thanks` könyvtárat: - -```shell -composer global require symfony/thanks -``` - -Majd futtatni: - -```shell -composer thanks -``` - -Próbálja ki! - - -Konfiguráció -============ - -A Composer szorosan kapcsolódik a [Git |https://git-scm.com] verziókezelő eszközhöz. Ha nincs telepítve, szólni kell a Composernek, hogy ne használja: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/hu/creating-editing-form.texy b/best-practices/hu/creating-editing-form.texy deleted file mode 100644 index 959ed13a7b..0000000000 --- a/best-practices/hu/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Űrlap rekord létrehozásához és szerkesztéséhez -********************************************** - -.[perex] -Hogyan implementáljuk helyesen a Nette-ben egy rekord hozzáadását és szerkesztését úgy, hogy mindkettőhöz ugyanazt az űrlapot használjuk? - -Sok esetben a rekord hozzáadására és szerkesztésére szolgáló űrlapok ugyanazok, legfeljebb a gomb felirata különbözik. Egyszerű presenterek példáit mutatjuk be, ahol az űrlapot először rekord hozzáadására, majd szerkesztésére használjuk, végül pedig egyesítjük a két megoldást. - - -Rekord hozzáadása ------------------ - -Példa egy presenter-re, amely rekord hozzáadására szolgál. Magát az adatbázis-kezelést a `Facade` osztályra bízzuk, amelynek kódja a példa szempontjából nem lényeges. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... hozzáadjuk az űrlap mezőit ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // rekord hozzáadása az adatbázishoz - $this->flashMessage('Sikeresen hozzáadva'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Rekord szerkesztése -------------------- - -Most megmutatjuk, hogyan nézne ki egy presenter, amely rekord szerkesztésére szolgál: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // rekord létezésének ellenőrzése - || !$this->facade->isEditAllowed(/*...*/) // jogosultság ellenőrzése - ) { - $this->error(); // 404 hiba - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // ellenőrizzük, hogy az akció 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... hozzáadjuk az űrlap mezőit ... - - $form->setDefaults($this->record); // alapértelmezett értékek beállítása - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // rekord frissítése - $this->flashMessage('Sikeresen frissítve'); - $this->redirect('...'); - } -} -``` - -Az *action* metódusban, amely rögtön a [presenter életciklusának |application:presenters#Presenter életciklusa] elején fut le, ellenőrizzük a rekord létezését és a felhasználó jogosultságát annak szerkesztésére. - -A rekordot a `$record` property-be mentjük, hogy elérhető legyen a `createComponentRecordForm()` metódusban az alapértelmezett értékek beállításához, és a `recordFormSucceeded()` metódusban az ID miatt. Alternatív megoldásként beállíthatnánk az alapértelmezett értékeket közvetlenül az `actionEdit()` metódusban, és az URL részét képező ID értékét a `getParameter('id')` segítségével szerezhetnénk meg: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // létezés ellenőrzése és jogosultság ellenőrzése - ) { - $this->error(); - } - - // űrlap alapértelmezett értékeinek beállítása - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Azonban, és ez kellene, hogy **az egész kód legfontosabb tanulsága** legyen, az űrlap létrehozásakor meg kell győződnünk arról, hogy az akció valóban `edit`. Mert különben az `actionEdit()` metódusban lévő ellenőrzés egyáltalán nem futna le! - - -Ugyanaz az űrlap hozzáadáshoz és szerkesztéshez ------------------------------------------------ - -És most egyesítjük a két presentert egybe. Vagy megkülönböztethetnénk a `createComponentRecordForm()` metódusban, hogy melyik akcióról van szó, és ennek megfelelően konfigurálhatnánk az űrlapot, vagy ezt közvetlenül az action-metódusokra bízhatnánk, és megszabadulhatnánk a feltételtől: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // rekord létezésének ellenőrzése - || !$this->facade->isEditAllowed(/*...*/) // jogosultság ellenőrzése - ) { - $this->error(); // 404 hiba - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // alapértelmezett értékek beállítása - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // ellenőrizzük, hogy az akció 'add' vagy 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... hozzáadjuk az űrlap mezőit ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // rekord hozzáadása az adatbázishoz - $this->flashMessage('Sikeresen hozzáadva'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // rekord frissítése - $this->flashMessage('Sikeresen frissítve'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/hu/dynamic-snippets.texy b/best-practices/hu/dynamic-snippets.texy deleted file mode 100644 index b6df024d32..0000000000 --- a/best-practices/hu/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Dinamikus Snippetek -******************* - -Az alkalmazásfejlesztés során meglehetősen gyakran felmerül az igény AJAX műveletek végrehajtására, például táblázatok egyes sorain vagy listaelemeken. Példaként választhatjuk a cikkek listázását, ahol minden cikknél lehetővé tesszük a bejelentkezett felhasználó számára, hogy "tetszik/nem tetszik" értékelést adjon. A presenter és a hozzá tartozó sablon kódja AJAX nélkül körülbelül így fog kinézni (a legfontosabb részeket mutatom be, a kód számol az értékelések jelölésére szolgáló szolgáltatás létezésével és a cikkek gyűjteményének megszerzésével - a konkrét implementáció nem fontos ennek az útmutatónak a céljaihoz): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Sablon: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>tetszik</a> - {else} - <a n:href="unlike! $article->id" class=ajax>már nem tetszik</a> - {/if} -</article> -``` - - -Ajaxizálás -========== - -Most lássuk el ezt az egyszerű alkalmazást AJAX-szal. A cikk értékelésének megváltoztatása nem annyira fontos, hogy átirányításra legyen szükség, ezért ideális esetben AJAX-szal kellene történnie a háttérben. Használjuk [a kiegészítők kiszolgáló szkriptjét |application:ajax#Naja] a szokásos konvencióval, miszerint az AJAX linkeknek `ajax` CSS osztályuk van. - -De hogyan is csináljuk ezt konkrétan? A Nette 2 utat kínál: az ún. dinamikus snippetek útját és a komponensek útját. Mindkettőnek megvannak az előnyei és hátrányai, ezért egyenként bemutatjuk őket. - - -A dinamikus snippetek útja -========================== - -A dinamikus snippet a Latte terminológiájában a `{snippet}` tag egy speciális használati esetét jelenti, amikor a snippet nevében egy változó szerepel. Egy ilyen snippet nem lehet bárhol a sablonban - egy statikus snippetbe, azaz egy közönséges snippetbe vagy egy `{snippetArea}`-ba kell csomagolni. A sablonunkat a következőképpen módosíthatnánk. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>tetszik</a> - {else} - <a n:href="unlike! $article->id" class=ajax>már nem tetszik</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Most minden cikk definiál egy snippetet, amelynek nevében a cikk ID-ja szerepel. Mindezeket a snippeket aztán egyetlen, `articlesContainer` nevű snippetbe csomagoljuk. Ha ezt a csomagoló snippetet kihagynánk, a Latte kivétellel figyelmeztetne minket. - -Már csak a presenterben kell kiegészítenünk az újrarajzolást - elég a statikus burkolót újrarajzolni. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- nem szükséges - } else { - $this->redirect('this'); - } -} -``` - -Hasonlóképpen módosítjuk a testvér `handleUnlike()` metódust is, és az AJAX működik! - -A megoldásnak azonban van egy árnyoldala. Ha jobban megvizsgálnánk, hogyan zajlik az AJAX kérés, rájönnénk, hogy bár kifelé az alkalmazás takarékosnak tűnik (csak egyetlen snippetet ad vissza az adott cikkhez), valójában a szerveren az összes snippetet kirajzolta. A kívánt snippetet a payloadba helyezte, a többit pedig eldobta (tehát teljesen feleslegesen szerezte be őket az adatbázisból is). - -Ahhoz, hogy ezt a folyamatot optimalizáljuk, ott kell beavatkoznunk, ahol a `$articles` gyűjteményt átadjuk a sablonnak (mondjuk a `renderDefault()` metódusban). Kihasználjuk azt a tényt, hogy a signálok feldolgozása a `render<Something>` metódusok előtt történik: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Most a signál feldolgozásakor a sablonba az összes cikket tartalmazó gyűjtemény helyett csak egy tömb kerül átadásra egyetlen cikkel - azzal, amelyet ki akarunk rajzolni és a payloadban elküldeni a böngészőnek. A `{foreach}` tehát csak egyszer fut le, és nem rajzolódnak ki felesleges snippettek. - - -A komponensek útja -================== - -Egy teljesen más megoldási mód elkerüli a dinamikus snippetteket. A trükk abban rejlik, hogy az egész logikát egy külön komponensbe helyezzük át - az értékelések megadásától kezdve nem a presenter fog gondoskodni, hanem egy dedikált `LikeControl`. Az osztály a következőképpen fog kinézni (ezen kívül tartalmazni fogja a `render`, `handleUnlike` stb. metódusokat is): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -A komponens sablonja: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>tetszik</a> - {else} - <a n:href="unlike!" class=ajax>már nem tetszik</a> - {/if} -{/snippet} -``` - -Természetesen megváltozik a view sablonja, és a presenterbe be kell illesztenünk egy factory-t. Mivel a komponenst annyiszor hozzuk létre, ahány cikket lekérünk az adatbázisból, a "sokszorosításához" a [Multiplier |application:Multiplier] osztályt használjuk. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -A view sablonja a szükséges minimumra csökken (és teljesen mentes a snippettektől!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Majdnem készen vagyunk: az alkalmazás mostantól AJAX-osan fog működni. Itt is optimalizálnunk kell az alkalmazást, mert a Nette Database használata miatt a signál feldolgozásakor feleslegesen betöltődik az összes cikk az adatbázisból egy helyett. Előnye azonban, hogy nem kerülnek kirajzolásra, mert valóban csak a mi komponensünk renderelődik. - -{{priority: -1}} diff --git a/best-practices/hu/editors-and-tools.texy b/best-practices/hu/editors-and-tools.texy deleted file mode 100644 index 7104666da7..0000000000 --- a/best-practices/hu/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Szerkesztők és eszközök -*********************** - -.[perex] -Lehetsz ügyes programozó, de csak jó eszközökkel válsz mesterré. Ebben a fejezetben tippeket találsz fontos eszközökhöz, szerkesztőkhöz és bővítményekhez. - - -IDE szerkesztő -============== - -Határozottan javasoljuk, hogy a fejlesztéshez teljes értékű IDE-t használj, mint például a PhpStorm, NetBeans, VS Code, és ne csak egy PHP támogatással rendelkező szövegszerkesztőt. A különbség valóban alapvető. Nincs ok megelégedni egy egyszerű szerkesztővel, amely ugyan tudja színezni a szintaxist, de nem éri el egy csúcskategóriás IDE képességeit, amely pontosan súg, figyeli a hibákat, képes refaktorálni a kódot és sok minden mást. Néhány IDE fizetős, mások pedig ingyenesek. - -A **NetBeans IDE** beépített támogatással rendelkezik a Nette, Latte és NEON számára. - -**PhpStorm**: telepítsd ezeket a bővítményeket a `Settings > Plugins > Marketplace` menüpontban: -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: keresd meg a marketplace-en a "Nette Latte + Neon" bővítményt. - -Kapcsold össze a Tracy-t is a szerkesztővel. Amikor egy hibaoldal jelenik meg, rákattinthatsz a fájlnevekre, és azok megnyílnak a szerkesztőben a megfelelő sorra állított kurzorral. Olvasd el, [hogyan konfiguráld a rendszert |tracy:open-files-in-ide]. - - -PHPStan -======= - -A PHPStan egy eszköz, amely logikai hibákat tár fel a kódban, mielőtt futtatnád azt. - -Telepítsük a Composer segítségével: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Hozzunk létre egy konfigurációs fájlt a projektben `phpstan.neon` néven: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Majd futtassuk az elemzést az `app/` mappában lévő osztályokon: - -```shell -vendor/bin/phpstan analyse app -``` - -Kimerítő dokumentációt találsz közvetlenül a [PHPStan oldalán |https://phpstan.org]. - - -Code Checker -============ - -A [Code Checker|code-checker:] ellenőrzi és szükség esetén kijavítja a forráskódok néhány formai hibáját: - -- eltávolítja a [BOM |nette:glossary#BOM]-ot -- ellenőrzi a [Latte |latte:] sablonok érvényességét -- ellenőrzi a `.neon`, `.php` és `.json` fájlok érvényességét -- ellenőrzi a [vezérlőkarakterek |nette:glossary#Vezérlő karakterek] előfordulását -- ellenőrzi, hogy a fájl UTF-8 kódolású-e -- ellenőrzi a hibásan írt `/* @anotace */` (hiányzik a csillag) -- eltávolítja a záró `?>` PHP fájlokból -- eltávolítja a jobb oldali szóközöket és a felesleges sorokat a fájl végéről -- normalizálja a sorelválasztókat a rendszer alapértelmezettjére (ha megadja a `-l` opciót) - - -Composer -======== - -A [Composer |best-practices:composer] egy függőségkezelő eszköz PHP-hez. Lehetővé teszi számunkra, hogy tetszőlegesen összetett függőségeket deklaráljunk az egyes könyvtárakhoz, majd telepíti őket a projektünkbe. - - -Requirements Checker -==================== - -Ez egy eszköz volt, amely tesztelte a szerver futási környezetét, és tájékoztatott arról, hogy (és milyen mértékben) lehet használni a keretrendszert. Jelenleg a Nette minden olyan szerveren használható, amely rendelkezik a minimálisan szükséges PHP verzióval. diff --git a/best-practices/hu/form-reuse.texy b/best-practices/hu/form-reuse.texy deleted file mode 100644 index cbda52b931..0000000000 --- a/best-practices/hu/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Űrlapok újrafelhasználása több helyen -************************************* - -.[perex] -A Nette-ben több lehetőség is rendelkezésre áll ugyanazon űrlap több helyen történő használatára a kód duplikálása nélkül. Ebben a cikkben különböző megoldásokat mutatunk be, beleértve azokat is, amelyeket érdemes elkerülni. - - -Űrlap Factory -============= - -Az egyik alapvető megközelítés ugyanazon komponens több helyen történő használatára egy olyan metódus vagy osztály létrehozása, amely ezt a komponenst generálja, majd ennek a metódusnak a meghívása az alkalmazás különböző pontjain. Egy ilyen metódust vagy osztályt *factory*-nak nevezünk. Kérjük, ne keverje össze a *factory method* tervezési mintával, amely a factory-k specifikus felhasználási módját írja le, és nem kapcsolódik ehhez a témához. - -Példaként létrehozunk egy factory-t, amely egy szerkesztő űrlapot fog összeállítani: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -Most már használhatja ezt a factory-t az alkalmazás különböző pontjain, például presenterekben vagy komponensekben. Ezt úgy teheti meg, hogy [függőségként kérjük |dependency-injection:passing-dependencies]. Először tehát regisztráljuk az osztályt a konfigurációs fájlban: - -```neon -services: - - FormFactory -``` - -Majd használjuk a presenterben: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // beküldött adatok feldolgozása - }; - return $form; - } -} -``` - -Az űrlap factory-t kibővítheti további metódusokkal más típusú űrlapok létrehozásához az alkalmazás igényei szerint. És természetesen hozzáadhatunk egy metódust is, amely létrehoz egy alap űrlapot elemek nélkül, és ezt a többi metódus fogja használni: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -A `createForm()` metódus egyelőre nem csinál semmi hasznosat, de ez hamarosan megváltozik. - - -A Factory függőségei -==================== - -Idővel kiderül, hogy szükségünk van arra, hogy az űrlapok többnyelvűek legyenek. Ez azt jelenti, hogy minden űrlaphoz be kell állítanunk az úgynevezett [translator |forms:rendering#Fordítás]-t. Ebből a célból módosítjuk a `FormFactory` osztályt úgy, hogy a konstruktorban függőségként fogadja el a `Translator` objektumot, és átadjuk azt az űrlapnak: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Mivel a `createForm()` metódust a többi, specifikus űrlapokat létrehozó metódus is meghívja, elegendő a translatort csak ebben beállítani. És készen is vagyunk. Nincs szükség egyetlen presenter vagy komponens kódjának módosítására sem, ami nagyszerű. - - -Több Factory osztály -==================== - -Alternatív megoldásként létrehozhat több osztályt minden egyes űrlaphoz, amelyet használni szeretne az alkalmazásában. Ez a megközelítés növelheti a kód olvashatóságát és megkönnyítheti az űrlapok kezelését. Az eredeti `FormFactory`-t csak egy tiszta űrlap létrehozására hagyjuk meg alapkonfigurációval (például fordítási támogatással), és a szerkesztő űrlaphoz létrehozunk egy új `EditFormFactory` factory-t. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ kompozíció használata -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -Nagyon fontos, hogy a `FormFactory` és az `EditFormFactory` osztályok közötti kapcsolat [kompozícióval |nette:introduction-to-object-oriented-programming#Kompozíció] valósuljon meg, nem pedig [objektum öröklődéssel |nette:introduction-to-object-oriented-programming#Öröklődés]: - -```php -// ⛔ ÍGY NE! IDE NEM VALÓ AZ ÖRÖKLŐDÉS -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -Az öröklődés használata ebben az esetben teljesen kontraproduktív lenne. Nagyon gyorsan problémákba ütköznél. Például abban a pillanatban, amikor paramétereket szeretnél hozzáadni a `create()` metódushoz; a PHP hibát jelezne, hogy a szignatúrája eltér a szülőétől. Vagy amikor függőséget adnál át az `EditFormFactory` osztálynak a konstruktoron keresztül. Olyan helyzet állna elő, amelyet [constructor hell |dependency-injection:passing-dependencies#Constructor hell]-nek nevezünk. - -Általában jobb előnyben részesíteni a [kompozíciót az öröklődéssel szemben |dependency-injection:faq#Miért részesítjük előnyben a kompozíciót az öröklődéssel szemben]. - - -Űrlapkezelés -============ - -Az űrlapkezelő, amely a sikeres beküldés után hívódik meg, szintén lehet a factory osztály része. Úgy fog működni, hogy a beküldött adatokat átadja a modellnek feldolgozásra. Az esetleges hibákat [visszaadja |forms:validation#Hibák a feldolgozás során] az űrlapnak. A modellt a következő példában a `Facade` osztály képviseli: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // beküldött adatok feldolgozása - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Magát az átirányítást azonban a presenterre bízzuk. Az `onSuccess` eseményhez hozzáad egy további handlert, amely végrehajtja az átirányítást. Ennek köszönhetően az űrlapot különböző presenterekben lehet majd használni, és mindegyikben máshová lehet átirányítani. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('A rekord mentésre került'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Ez a megoldás kihasználja az űrlapok azon tulajdonságát, hogy ha az űrlapon vagy annak egy elemén meghívják az `addError()` metódust, akkor a további `onSuccess` handler már nem hívódik meg. - - -Öröklődés a Form osztályból -=========================== - -Az összeállított űrlapnak nem szabad az űrlap leszármazottjának lennie. Más szavakkal, ne használja ezt a megoldást: - -```php -// ⛔ ÍGY NE! IDE NEM VALÓ AZ ÖRÖKLŐDÉS -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $this->addSubmit('send', 'Küldés'); - $this->setTranslator($translator); - } -} -``` - -Az űrlap konstruktorban történő összeállítása helyett használjon factory-t. - -Fontos megérteni, hogy a `Form` osztály elsősorban egy eszköz az űrlap összeállítására, tehát egy *form builder*. Az összeállított űrlap pedig tekinthető annak termékének. Azonban a termék nem a builder specifikus esete, nincs közöttük *is a* kapcsolat, amely az öröklődés alapját képezi. - - -Komponens űrlappal -================== - -Egy teljesen más megközelítés egy olyan [komponens |application:components] létrehozását jelenti, amelynek része egy űrlap. Ez új lehetőségeket kínál, például az űrlap specifikus módon történő renderelését, mivel a komponensnek része egy sablon is. Vagy használhatunk signálokat AJAX kommunikációhoz és információk betöltéséhez az űrlapba, például súgáshoz stb. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // beküldött adatok feldolgozása - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // esemény kiváltása - $this->onSave($this, $data); - } -} -``` - -Még létrehozunk egy factory-t, amely ezt a komponenst fogja gyártani. Elég [felírni az interfészét |application:components#Komponensek függőségekkel]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -És hozzáadjuk a konfigurációs fájlhoz: - -```neon -services: - - EditControlFactory -``` - -És most már kérhetjük a factory-t és használhatjuk a presenterben: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // vagy átirányítunk a szerkesztés eredményére, pl.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/hu/inject-method-attribute.texy b/best-practices/hu/inject-method-attribute.texy deleted file mode 100644 index f36ead2d52..0000000000 --- a/best-practices/hu/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Inject metódusok és attribútumok -******************************** - -.[perex] -Ebben a cikkben a függőségek Nette keretrendszerbeli presenterekbe történő átadásának különböző módjaira összpontosítunk. Összehasonlítjuk az előnyben részesített módszert, amely a konstruktor, más lehetőségekkel, mint például az `inject` metódusok és attribútumok. - -A presenterekre is igaz, hogy a függőségek [konstruktoron |dependency-injection:passing-dependencies#Konstruktoron keresztüli átadás] keresztüli átadása az előnyben részesített út. Ha azonban létrehozol egy közös őst, amelyből a többi presenter öröklődik (pl. `BasePresenter`), és ennek az ősnek is vannak függőségei, akkor egy problémába ütközünk, amelyet [constructor hell |dependency-injection:passing-dependencies#Constructor hell]-nek nevezünk. Ezt meg lehet kerülni alternatív utakkal, amelyeket az `inject` metódusok és attribútumok (korábban annotációk) jelentenek. - - -`inject*()` metódusok -===================== - -Ez a függőségátadás [setterrel |dependency-injection:passing-dependencies#Setteren keresztüli átadás] történő formája. Ezeknek a settereknek a neve `inject` előtaggal kezdődik. A Nette DI az így elnevezett metódusokat automatikusan meghívja rögtön a presenter példányának létrehozása után, és átadja nekik az összes szükséges függőséget. Ezért public-ként kell deklarálni őket. - -Az `inject*()` metódusok tekinthetők a konstruktor egyfajta kiterjesztésének több metódusba. Ennek köszönhetően a `BasePresenter` más metóduson keresztül veheti át a függőségeket, és a konstruktort szabadon hagyhatja a leszármazottai számára: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -A presenter tetszőleges számú `inject*()` metódust tartalmazhat, és mindegyiknek tetszőleges számú paramétere lehet. Kiválóan alkalmasak olyan esetekben is, amikor a presenter [traitekből |presenter-traits] áll össze, és mindegyik saját függőséget igényel. - - -`Inject` attribútumok -===================== - -Ez a [property-be történő injektálás |dependency-injection:passing-dependencies#Property beállításával] formája. Elég megjelölni, hogy mely változókba kell injektálni, és a Nette DI automatikusan átadja a függőségeket rögtön a presenter példányának létrehozása után. Ahhoz, hogy be tudja illeszteni őket, public-ként kell deklarálni őket. - -A property-ket attribútummal jelöljük meg: (korábban a `/** @inject */` annotációt használták) - -```php -use Nette\DI\Attributes\Inject; // ez a sor fontos - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Ennek a függőségátadási módnak az előnye a nagyon tömör írásmód volt. Azonban a [constructor property promotion |https://blog.nette.org/hu/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] megjelenésével egyszerűbbnek tűnik a konstruktor használata. - -Másrészt ez a módszer ugyanazoktól a hiányosságoktól szenved, mint a függőségek általános property-kbe történő átadása: nincs ellenőrzésünk a változóban bekövetkező változások felett, és ugyanakkor a változó az osztály nyilvános interfészének részévé válik, ami nem kívánatos. diff --git a/best-practices/hu/lets-create-contact-form.texy b/best-practices/hu/lets-create-contact-form.texy deleted file mode 100644 index 3ceafb1f03..0000000000 --- a/best-practices/hu/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Kapcsolatfelvételi űrlap létrehozása -************************************ - -.[perex] -Megnézzük, hogyan hozzunk létre egy kapcsolatfelvételi űrlapot a Nette-ben, beleértve az e-mail küldést is. Vágjunk bele! - -Először létre kell hoznunk egy új projektet. Hogy hogyan, azt az [Első lépések |nette:installation] oldal magyarázza el. Ezután elkezdhetjük az űrlap létrehozását. - -A legegyszerűbb módja az [űrlap létrehozása közvetlenül a presenterben |forms:in-presenter]. Használhatjuk az előkészített `HomePresenter`-t. Hozzáadjuk a `contactForm` komponenst, amely az űrlapot képviseli. Ezt úgy tesszük, hogy a kódba beírjuk a `createComponentContactForm()` factory metódust, amely létrehozza a komponenst: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Név:') - ->setRequired('Adja meg a nevét'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Adja meg az e-mail címét'); - $form->addTextarea('message', 'Üzenet:') - ->setRequired('Adja meg az üzenetet'); - $form->addSubmit('send', 'Küldés'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // e-mail küldése - } -} -``` - -Amint látja, két metódust hoztunk létre. Az első, `createComponentContactForm()` metódus létrehoz egy új űrlapot. Ennek vannak mezői a név, e-mail és üzenet számára, amelyeket az `addText()`, `addEmail()` és `addTextArea()` metódusokkal adunk hozzá. Hozzáadtunk egy gombot is az űrlap elküldéséhez. De mi van, ha a felhasználó nem tölt ki valamelyik mezőt? Ebben az esetben tudatnunk kell vele, hogy ez egy kötelező mező. Ezt a `setRequired()` metódussal értük el. Végül hozzáadtuk az [onSuccess |nette:glossary#Eventek események] eseményt is, amely akkor fut le, ha az űrlapot sikeresen elküldték. Esetünkben a `contactFormSucceeded` metódust hívja meg, amely gondoskodik az elküldött űrlap feldolgozásáról. Ezt hamarosan kiegészítjük a kódban. - -A `contactForm` komponenst a `Home/default.latte` sablonban rajzoltatjuk ki: - -```latte -{block content} -<h1>Kapcsolatfelvételi űrlap</h1> -{control contactForm} -``` - -Magához az e-mail küldéshez létrehozunk egy új osztályt, amelyet `ContactFacade`-nek nevezünk el, és az `app/Model/ContactFacade.php` fájlba helyezzük: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // az Ön e-mail címe - ->setFrom($email, $name) - ->setSubject('Üzenet a kapcsolatfelvételi űrlapról') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -A `sendMessage()` metódus létrehozza és elküldi az e-mailt. Ehhez az úgynevezett mailert használja, amelyet függőségként kap meg a konstruktoron keresztül. Olvasson többet az [e-mailek küldéséről |mail:]. - -Most visszatérünk a presenterhez, és befejezzük a `contactFormSucceeded()` metódust. Ez meghívja a `ContactFacade` osztály `sendMessage()` metódusát, és átadja neki az űrlap adatait. És hogyan szerezzük meg a `ContactFacade` objektumot? Megkapjuk a konstruktoron keresztül: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Az üzenet elküldve'); - $this->redirect('this'); - } -} -``` - -Miután az e-mail elküldésre került, még megjelenítünk a felhasználónak egy úgynevezett [flash üzenetet |application:components#Flash üzenetek], amely megerősíti, hogy az üzenet elküldésre került, majd átirányítjuk egy másik oldalra, hogy ne lehessen az űrlapot ismételten elküldeni a böngésző *frissítésével*. - - -Nos, ha minden működik, képesnek kell lennie e-mailt küldeni a kapcsolatfelvételi űrlapjáról. Gratulálok! - - -HTML e-mail sablon ------------------- - -Eddig egy egyszerű szöveges e-mail került elküldésre, amely csak az űrlapon elküldött üzenetet tartalmazta. Az e-mailben azonban használhatunk HTML-t, és vonzóbbá tehetjük a megjelenését. Létrehozunk hozzá egy Latte sablont, amelyet az `app/Model/contactEmail.latte` fájlba írunk: - -```latte -<html> - <title>Üzenet a kapcsolatfelvételi űrlapról - - -

    Név: {$name}

    -

    E-mail: {$email}

    -

    Üzenet: {$message}

    - - -``` - -Már csak a `ContactFacade`-et kell módosítani, hogy ezt a sablont használja. A konstruktorban kérjük a `LatteFactory` osztályt, amely képes létrehozni egy `Latte\Engine` objektumot, azaz egy [Latte sablon renderelőt |latte:develop#Hogyan rendereljünk sablont]. A `renderToString()` metódussal rendereljük a sablont egy fájlba, az első paraméter a sablon elérési útja, a második pedig a változók. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // az Ön e-mail címe - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -A generált HTML e-mailt ezután a `setHtmlBody()` metódusnak adjuk át az eredeti `setBody()` helyett. Szintén nem kell megadnunk az e-mail tárgyát a `setSubject()`-ben, mert a könyvtár azt a sablon `` eleméből veszi át. - - -Konfiguráció ------------- - -A `ContactFacade` osztály kódjában még mindig fixen be van írva az adminisztrátori e-mail címünk, az `admin@example.com`. Jobb lenne ezt a konfigurációs fájlba helyezni. Hogyan tegyük ezt? - -Először módosítjuk a `ContactFacade` osztályt, és az e-mail címet tartalmazó stringet egy konstruktoron keresztül átadott változóval helyettesítjük: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -A második lépés ennek a változónak az értékének megadása a konfigurációban. Az `app/config/services.neon` fájlba írjuk: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -És kész is. Ha a `services` szekcióban sok elem lenne, és úgy éreznénk, hogy az e-mail elveszik közöttük, akkor változóvá tehetjük. Módosítjuk a bejegyzést erre: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -És az `app/config/common.neon` fájlban definiáljuk ezt a változót: - -```neon -parameters: - adminEmail: admin@example.com -``` - -És kész is vagyunk! diff --git a/best-practices/hu/microsites.texy b/best-practices/hu/microsites.texy deleted file mode 100644 index feb5f639bb..0000000000 --- a/best-practices/hu/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Hogyan írjunk mikro-weboldalakat -******************************** - -Képzelje el, hogy gyorsan létre kell hoznia egy kis weboldalt a cége közelgő eseményére. Egyszerűnek, gyorsnak és felesleges bonyodalmaktól mentesnek kell lennie. Talán úgy gondolja, hogy egy ilyen kis projekthez nincs szüksége egy robusztus keretrendszerre. De mi van, ha a Nette keretrendszer használata alapvetően leegyszerűsítheti és felgyorsíthatja ezt a folyamatot? - -Hiszen még egyszerű weboldalak készítésekor sem akar lemondani a kényelemről. Nem akarja újra feltalálni azt, amit már egyszer megoldottak. Legyen nyugodtan lusta, és hagyja magát kényeztetni. A Nette Framework kiválóan használható mikro keretrendszerként is. - -Hogyan nézhet ki egy ilyen microsite? Például úgy, hogy a weboldal teljes kódját egyetlen `index.php` fájlba helyezzük a nyilvános mappában: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// hozzon létre DI konténert a config.neon konfiguráció alapján -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// beállítjuk a routingot -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// route a https://example.com/ URL-hez -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // érzékeljük a böngésző nyelvét és átirányítunk az /en vagy /de stb. URL-re - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// route a https://example.com/cs vagy https://example.com/en URL-hez -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // megjelenítjük a megfelelő sablont, például ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// indítsa el az alkalmazást! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Minden más sablon lesz, amelyek a szülő `/templates` mappában vannak tárolva. - -Az `index.php` PHP kódja először [előkészíti a környezetet |bootstrap:], majd definiálja a [route-okat |application:routing#Dinamikus routing callbackekkel], és végül elindítja az alkalmazást. Az előnye, hogy a `addRoute()` függvény második paramétere lehet egy callable, amely a megfelelő oldal megnyitása után végrehajtódik. - - -Miért használjunk Nette-t microsite-hoz? ----------------------------------------- - -- Azok a programozók, akik valaha kipróbálták a [Tracy |tracy:]-t, ma már el sem tudják képzelni, hogy nélküle programozzanak valamit. -- Mindenekelőtt azonban a [Latte |latte:] sablonrendszert fogja használni, mert már 2 oldaltól kezdve külön szeretné választani az [elrendezést és a tartalmat |latte:template-inheritance]. -- És határozottan szeretne támaszkodni az [automatikus escapelésre |latte:safety-first], hogy ne keletkezzen XSS sebezhetőség. -- A Nette azt is biztosítja, hogy hiba esetén soha ne jelenjenek meg a programozói PHP hibaüzenetek, hanem egy felhasználóbarát oldal. -- Ha visszajelzést szeretne kapni a felhasználóktól, például egy kapcsolatfelvételi űrlap formájában, akkor még hozzáadja az [űrlapokat |forms:] és az [adatbázist |database:]. -- A kitöltött űrlapokat szintén könnyedén [elküldheti e-mailben |mail:]. -- Néha hasznos lehet a [gyorsítótárazás |caching:], például ha feedeket tölt le és jelenít meg. - -Napjainkban, amikor a sebesség és a hatékonyság kulcsfontosságú, fontos, hogy olyan eszközök álljanak rendelkezésre, amelyek lehetővé teszik az eredmények elérését felesleges késedelem nélkül. A Nette keretrendszer pontosan ezt kínálja - gyors fejlesztést, biztonságot és széles körű eszközöket, mint például a Tracy és a Latte, amelyek egyszerűsítik a folyamatot. Elég telepíteni néhány Nette csomagot, és egy ilyen microsite létrehozása hirtelen gyerekjáték. És tudja, hogy sehol sem rejtőzik biztonsági rés. diff --git a/best-practices/hu/pagination.texy b/best-practices/hu/pagination.texy deleted file mode 100644 index 286a369a9b..0000000000 --- a/best-practices/hu/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Adatbázis eredmények lapozása -***************************** - -.[perex] -Webalkalmazások fejlesztése során nagyon gyakran találkozhat azzal a követelménnyel, hogy korlátozni kell az oldalon megjelenített elemek számát. - -Kezdjük azzal az állapottal, amikor minden adatot lapozás nélkül listázunk ki. Az adatok adatbázisból történő kiválasztásához van egy ArticleRepository osztályunk, amely a konstruktoron kívül tartalmaz egy `findPublishedArticles` metódust, amely visszaadja az összes publikált cikket a publikálás dátuma szerint csökkenő sorrendben. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -A presenterben ezután injectáljuk a modell osztályt, és a render metódusban lekérjük a publikált cikkeket, amelyeket átadunk a sablonnak: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -A `default.latte` sablonban pedig gondoskodunk a cikkek kiírásáról: - -```latte -{block content} -<h1>Cikkek</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Ezzel a módszerrel ki tudjuk listázni az összes cikket, ami azonban problémákat kezd okozni, amint a cikkek száma megnő. Ebben a pillanatban válik hasznossá egy lapozó mechanizmus implementálása. - -Ez biztosítja, hogy az összes cikk több oldalra legyen osztva, és mi csak az aktuális oldal cikkeit jelenítjük meg. Az oldalak teljes számát és a cikkek elosztását a [Paginator |utils:Paginator] maga számítja ki attól függően, hogy összesen hány cikkünk van, és hány cikket szeretnénk megjeleníteni egy oldalon. - -Az első lépésben módosítjuk a cikkek lekérésére szolgáló metódust a repository osztályban úgy, hogy csak egy oldal cikkeit tudja visszaadni. Hozzáadunk egy metódust is az adatbázisban lévő cikkek teljes számának lekérdezésére, amelyre szükségünk lesz a Paginator beállításához: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Visszaadja a publikált cikkek teljes számát - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Ezután nekilátunk a presenter módosításának. A render metódusba átadjuk az aktuálisan megjelenített oldal számát. Arra az esetre, ha ez a szám nem lenne része az URL-nek, beállítjuk az első oldal alapértelmezett értékét. - -Továbbá kibővítjük a render metódust a Paginator példányának megszerzésével, beállításával és a sablonban megjelenítendő megfelelő cikkek kiválasztásával. A HomePresenter a módosítások után így fog kinézni: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Lekérdezzük a publikált cikkek teljes számát - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Létrehozunk egy Paginator példányt és beállítjuk - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // cikkek teljes száma - $paginator->setItemsPerPage(10); // elemek száma oldalanként - $paginator->setPage($page); // aktuális oldal száma - - // Az adatbázisból lekérünk egy korlátozott cikkhalmazt a Paginator számítása szerint - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // amelyet átadunk a sablonnak - $this->template->articles = $articles; - // és magát a Paginatort is a lapozási lehetőségek megjelenítéséhez - $this->template->paginator = $paginator; - } -} -``` - -A sablonunk most már csak egy oldal cikkein iterál, elég hozzáadnunk a lapozó linkeket: - -```latte -{block content} -<h1>Cikkek</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Első</a> -  |  - <a n:href="default, $paginator->page-1">Előző</a> -  |  - {/if} - - Oldal {$paginator->getPage()} / {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Következő</a> -  |  - <a n:href="default, $paginator->getPageCount()">Utolsó</a> - {/if} -</div> -``` - - -Így egészítettük ki az oldalt a Paginator segítségével történő lapozás lehetőségével. Abban az esetben, ha a [Nette Database Core |database:sql-way] helyett adatbázisrétegként a [Nette Database Explorer |database:explorer]-t használjuk, képesek vagyunk implementálni a lapozást Paginator használata nélkül is. A `Nette\Database\Table\Selection` osztály ugyanis tartalmaz egy [page |api:Nette\Database\Table\Selection::_page] metódust a Paginatorból átvett lapozási logikával. - -A repository ebben az implementációs módban így fog kinézni: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -A presenterben nem kell Paginatort létrehoznunk, helyette a `Selection` osztály metódusát használjuk, amelyet a repository ad vissza: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Lekérjük a publikált cikkeket - $articles = $this->articleRepository->findPublishedArticles(); - - // és a sablonba csak azok egy részét küldjük el, amelyet a page metódus számítása korlátoz - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // és a szükséges adatokat is a lapozási lehetőségek megjelenítéséhez - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Mivel most nem küldünk Paginatort a sablonba, módosítjuk a lapozó linkeket megjelenítő részt: - -```latte -{block content} -<h1>Cikkek</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Első</a> -  |  - <a n:href="default, $page - 1">Előző</a> -  |  - {/if} - - Oldal {$page} / {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Következő</a> -  |  - <a n:href="default, $lastPage">Utolsó</a> - {/if} -</div> -``` - -Ezzel a módszerrel implementáltuk a lapozó mechanizmust Paginator használata nélkül. - -{{priority: -1}} diff --git a/best-practices/hu/passing-settings-to-presenters.texy b/best-practices/hu/passing-settings-to-presenters.texy deleted file mode 100644 index 077d860afd..0000000000 --- a/best-practices/hu/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Beállítások átadása presentereknek -********************************** - -.[perex] -Szüksége van arra, hogy olyan argumentumokat adjon át a presentereknek, amelyek nem objektumok (pl. információ arról, hogy debug módban fut-e, könyvtárak elérési útjai stb.), és ezért nem adhatók át automatikusan autowiring segítségével? A megoldás az, hogy becsomagolja őket egy `Settings` objektumba. - -A `Settings` szolgáltatás egy nagyon egyszerű, mégis hasznos módja annak, hogy információkat szolgáltassunk a futó alkalmazásról a presentereknek. Konkrét formája kizárólag az Ön igényeitől függ. Példa: - -```php -namespace App; - -class Settings -{ - public function __construct( - // PHP 8.1-től kezdve megadható a readonly - public bool $debugMode, - public string $appDir, - // és így tovább - ) {} -} -``` - -Példa a konfigurációba történő regisztrálásra: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Amikor egy presenternek szüksége van az e szolgáltatás által nyújtott információkra, egyszerűen elkéri a konstruktorban: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/hu/post-links.texy b/best-practices/hu/post-links.texy deleted file mode 100644 index 8faa625780..0000000000 --- a/best-practices/hu/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Hogyan használjuk helyesen a POST linkeket -****************************************** - -.[perex] -Webalkalmazásokban, különösen adminisztrációs felületeken, alapvető szabálynak kellene lennie, hogy a szerver állapotát megváltoztató műveleteket ne a GET HTTP metódussal végezzük. Ahogy a metódus neve is sugallja, a GET csak adatok lekérésére szolgál, nem pedig azok megváltoztatására. Olyan műveletekhez, mint például a rekordok törlése, célszerűbb a POST metódust használni. Bár ideális a DELETE metódus lenne, de azt JavaScript nélkül nem lehet meghívni, ezért történelmileg a POST-ot használják. - -Hogyan tegyük ezt a gyakorlatban? Használja ezt az egyszerű trükköt. A sablon elején hozzon létre egy segédűrlapot `postForm` azonosítóval, amelyet aztán a törlő gombokhoz használ: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Ennek az űrlapnak köszönhetően a klasszikus `<a>` link helyett használhat egy `<button>` gombot, amelyet vizuálisan úgy lehet módosítani, hogy úgy nézzen ki, mint egy normál link. Például a Bootstrap CSS keretrendszer `btn btn-link` osztályokat kínál, amelyekkel elérheti, hogy a gomb vizuálisan ne különbözzön a többi linktől. A `form="postForm"` attribútummal összekapcsoljuk az előkészített űrlappal: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">törlés</button> - <!-- <a n:href="delete $post->id">törlés</a> helyett --> - </td> - </tr> -</table> -``` - -A linkre kattintva most a `delete` akció hívódik meg. Annak biztosítására, hogy a kérések csak a POST metóduson keresztül és ugyanarról a domainről érkezzenek (ami hatékony védelem a CSRF támadások ellen), használja a `#[Requires]` attribútumot: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hipotetikus kód a rekord törlésére - $this->redirect('default'); - } -} -``` - -Az attribútum a Nette Application 3.2 óta létezik, és további lehetőségeiről a [Hogyan használjuk a #Requires attribútumot |attribute-requires] oldalon olvashat többet. - -Ha az `actionDelete()` akció helyett a `handleDelete()` signált használná, nem szükséges megadni a `sameOrigin: true`-t, mert a signáloknak ez a védelme alapértelmezetten be van állítva: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Ez a megközelítés nemcsak javítja az alkalmazás biztonságát, hanem hozzájárul a helyes webes szabványok és gyakorlatok betartásához is. A POST metódusok használatával az állapotot megváltoztató műveletekhez robusztusabb és biztonságosabb alkalmazást érhet el. diff --git a/best-practices/hu/presenter-traits.texy b/best-practices/hu/presenter-traits.texy deleted file mode 100644 index a8ec3c6482..0000000000 --- a/best-practices/hu/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Presenterek összeállítása traitekkel -************************************ - -.[perex] -Ha több presenterben ugyanazt a kódot kell implementálnunk (pl. annak ellenőrzése, hogy a felhasználó be van-e jelentkezve), kézenfekvő a kódot egy közös ősbe helyezni. A második lehetőség egycélú [traitek |nette:introduction-to-object-oriented-programming#Traitek] létrehozása. - -Ennek a megoldásnak az az előnye, hogy minden presenter pontosan azokat a traiteket használhatja, amelyekre valóban szüksége van, míg a többszörös öröklődés PHP-ban nem lehetséges. - -Ezek a traitek kihasználhatják azt a tényt, hogy a presenter létrehozásakor sorban meghívódnak az összes [inject metódus |inject-method-attribute#inject metódusok]. Csak arra kell ügyelni, hogy minden inject metódus neve egyedi legyen. - -A traitek inicializáló kódot csatolhatnak az [onStartup vagy onRender |application:presenters#Események] eseményekhez. - -Példák: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -A presenter ezután egyszerűen használja ezeket a traiteket: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/hu/restore-request.texy b/best-practices/hu/restore-request.texy deleted file mode 100644 index 9de22622f4..0000000000 --- a/best-practices/hu/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Hogyan térjünk vissza egy korábbi oldalra? -****************************************** - -.[perex] -Mi van, ha a felhasználó egy űrlapot tölt ki, és lejár a bejelentkezése? Hogy ne vesszenek el az adatok, a bejelentkezési oldalra történő átirányítás előtt az adatokat a sessionbe mentjük. A Nette-ben ez gyerekjáték. - -Az aktuális kérést a `storeRequest()` metódussal lehet a sessionbe menteni, amely visszaadja annak azonosítóját egy rövid string formájában. A metódus elmenti az aktuális presenter nevét, a view-t és annak paramétereit. Abban az esetben, ha egy űrlap is elküldésre került, a mezők tartalma is elmentésre kerül (a feltöltött fájlok kivételével). - -A kérés visszaállítását a `restoreRequest($key)` metódus végzi, amelynek átadjuk a kapott azonosítót. Ez átirányít az eredeti presenterhez és view-hoz. Ha azonban a mentett kérés egy űrlap elküldését tartalmazza, akkor az eredeti presenterhez a `forward()` metódussal lép át, átadja az űrlapnak a korábban kitöltött értékeket, és újra kirajzoltatja azt. Így a felhasználónak lehetősége van újra elküldeni az űrlapot, és nem vesznek el adatok. - -Fontos, hogy a `restoreRequest()` ellenőrzi, hogy az újonnan bejelentkezett felhasználó ugyanaz-e, aki eredetileg kitöltötte az űrlapot. Ha nem, eldobja a kérést, és nem tesz semmit. - -Mutassuk be mindezt egy példán. Legyen egy `AdminPresenter` presenterünk, amelyben adatokat szerkesztünk, és amelynek `startup()` metódusában ellenőrizzük, hogy a felhasználó be van-e jelentkezve. Ha nincs, átirányítjuk a `SignPresenter`-re. Ezzel egyidejűleg elmentjük az aktuális kérést, és annak kulcsát elküldjük a `SignPresenter`-nek. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -A `SignPresenter` a bejelentkezési űrlapon kívül tartalmazni fog egy `$backlink` perzisztens paramétert is, amelybe a kulcs beíródik. Mivel a paraméter perzisztens, a bejelentkezési űrlap elküldése után is átadásra kerül. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... hozzáadjuk az űrlap mezőit ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... itt bejelentkeztetjük a felhasználót ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -A `restoreRequest()` metódusnak átadjuk a mentett kérés kulcsát, és az átirányít (vagy átlép) az eredeti presenterhez. - -Ha azonban a kulcs érvénytelen (például már nem létezik a sessionben), a metódus nem tesz semmit. Ezt követi a `$this->redirect('Admin:')` hívása, amely átirányít az `AdminPresenter`-re. - -{{priority: -1}} diff --git a/best-practices/it/@home.texy b/best-practices/it/@home.texy index 7a11feda65..ebfbdea34c 100644 --- a/best-practices/it/@home.texy +++ b/best-practices/it/@home.texy @@ -1,24 +1,25 @@ -Guide e procedure -***************** +Tutorial e best practice +************************ .[perex] -Guide, soluzioni per compiti comuni e *best practices* per Nette. +Tutorial, soluzioni ai compiti più comuni e best practice per Nette. <div class=documentation> <div> -Applicazione Nette ------------------- +Nette Application +----------------- - [Metodi e attributi inject |inject-method-attribute] -- [Composizione dei presenter da trait |presenter-traits] -- [Passare le impostazioni ai presenter |passing-settings-to-presenters] -- [Come tornare a una pagina precedente |restore-request] -- [Paginazione dei risultati del database |pagination] +- [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> @@ -26,10 +27,10 @@ Applicazione Nette Form ---- -- [Riutilizzo dei form |form-reuse] -- [Form per creare e modificare un record |creating-editing-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/it/dependent-selectboxes-elegantly-in-nette-and-pure-js] +- [Selectbox dipendenti |https://blog.nette.org/en/dependent-selectboxes-elegantly-in-nette-and-pure-js] </div> <div> @@ -38,11 +39,10 @@ Form Generale -------- - [Come caricare un file di configurazione |bootstrap:] -- [Come scrivere micro-siti |microsites] -- [Perché Nette usa la notazione PascalCase per le costanti? |https://blog.nette.org/it/for-less-screaming-in-the-code] -- [Perché Nette non usa il suffisso Interface? |https://blog.nette.org/it/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: suggerimenti per l'uso |composer] -- [Suggerimenti per editor e strumenti |editors-and-tools] +- [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> @@ -51,10 +51,10 @@ Generale Soluzioni di esempio -------------------- -- [Nette examples |https://github.com/nette-examples] +- [Esempi di Nette |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] +- [Esempi di Contributte |https://contributte.org/examples.html] +- [Sito di Doctrine ORM |https://github.com/MinecordNetwork/Website] - [Quick start |quickstart:] </div> @@ -63,7 +63,7 @@ Soluzioni di esempio Video ----- -Centinaia di registrazioni dagli Ultimi Sabati e video su Nette si trovano sotto un unico tetto sul "canale Youtube di Nette Framework":https://www.youtube.com/user/NetteFramework. +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 new file mode 100644 index 0000000000..b1050b0687 --- /dev/null +++ b/best-practices/it/@left-menu.texy @@ -0,0 +1,34 @@ +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 index 6cd5c59394..353ed5b4b6 100644 --- a/best-practices/it/@meta.texy +++ b/best-practices/it/@meta.texy @@ -1,2 +1 @@ -{{sitename: Guide e procedure}} -{{leftbar: www:@menu-common}} +{{sitename: Tutorial e best practice}} diff --git a/best-practices/it/attribute-requires.texy b/best-practices/it/attribute-requires.texy index a2d48132d6..3953b3c047 100644 --- a/best-practices/it/attribute-requires.texy +++ b/best-practices/it/attribute-requires.texy @@ -2,30 +2,30 @@ Come usare l'attributo `#[Requires]` ************************************ .[perex] -Quando scrivi un'applicazione web, ti imbatterai spesso nella necessità di limitare l'accesso a determinate parti della tua applicazione. Forse vuoi che alcune richieste possano inviare dati solo tramite un form (cioè con il metodo POST), o che siano accessibili solo per chiamate AJAX. In Nette Framework 3.2 è apparso un nuovo strumento che ti permette di impostare tali limitazioni in modo molto elegante e chiaro: l'attributo `#[Requires]`. +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]`. -L'attributo è un marcatore speciale in PHP che aggiungi prima della definizione di una classe o di un metodo. Poiché si tratta effettivamente di una classe, affinché gli esempi seguenti funzionino, è necessario specificare la clausola use: +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]` può essere utilizzato sulla classe stessa del presenter e anche su questi metodi: +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 puoi usare l'attributo anche su di essi. +Gli ultimi due metodi riguardano anche i componenti, quindi potete usare l'attributo pure con essi. -Se le condizioni specificate dall'attributo non sono soddisfatte, verrà generato un errore HTTP 4xx. +Se le condizioni indicate dall'attributo non sono soddisfatte, viene generato un errore HTTP 4xx. Metodi HTTP ----------- -Puoi specificare quali metodi HTTP (come GET, POST, ecc.) sono consentiti per l'accesso. Ad esempio, se vuoi consentire l'accesso solo inviando un form, imposta: +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 @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Perché dovresti usare POST invece di GET per azioni che modificano lo stato e come farlo? [Leggi la guida |post-links]. +Perché per le azioni che cambiano lo stato dovreste usare POST invece di GET e come farlo? [Leggete la guida |post-links]. -Puoi specificare un metodo o un array di metodi. Un caso speciale è il valore `'*'`, che consente tutti i metodi, cosa che i presenter standard [non permettono per motivi di sicurezza |application:presenters#Controllo del metodo HTTP]. +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]. -Chiamata AJAX +Chiamate AJAX ------------- -Se vuoi che il presenter o il metodo sia disponibile solo per le richieste AJAX, usa: +Se volete che un presenter o un metodo sia accessibile solo per le richieste AJAX, usate: ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ class AjaxPresenter extends Nette\Application\UI\Presenter Stessa origine -------------- -Per aumentare la sicurezza, puoi richiedere che la richiesta sia effettuata dallo stesso dominio. Ciò previene la [vulnerabilità CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: +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)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -Per i metodi `handle<Signal>()`, l'accesso dallo stesso dominio è richiesto automaticamente. Quindi, se al contrario vuoi consentire l'accesso da qualsiasi dominio, specifica: +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)] @@ -80,7 +80,7 @@ public function handleList(): void Accesso tramite forward ----------------------- -A volte è utile limitare l'accesso a un presenter in modo che sia disponibile solo indirettamente, ad esempio utilizzando il metodo `forward()` o `switch()` da un altro presenter. In questo modo si proteggono, ad esempio, gli error-presenter, affinché non possano essere invocati dall'URL: +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)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -In pratica, è spesso necessario contrassegnare determinate view, alle quali si può accedere solo in base alla logica nel presenter. Cioè, di nuovo, affinché non possano essere aperte direttamente: +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 @@ -114,7 +114,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Azioni specifiche ----------------- -Puoi anche limitare che un certo codice, ad esempio la creazione di un componente, sia disponibile solo per azioni specifiche nel presenter: +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 @@ -126,15 +126,15 @@ class EditDeletePresenter extends Nette\Application\UI\Presenter } ``` -Nel caso di una singola azione, non è necessario scrivere un array: `#[Requires(actions: 'default')]` +Nel caso di una sola azione non serve scrivere un array: `#[Requires(actions: 'default')]` -Attributi personalizzati ------------------------- +Attributi propri +---------------- -Se vuoi usare l'attributo `#[Requires]` ripetutamente con le stesse impostazioni, puoi creare un tuo attributo personalizzato che erediterà `#[Requires]` e lo imposterà secondo le tue esigenze. +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. -Ad esempio, `#[SingleAction]` consentirà l'accesso solo tramite l'azione `default`: +Per esempio `#[SingleAction]` consente l'accesso solo tramite l'azione `default`: ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -Oppure `#[RestMethods]` consentirà l'accesso tramite tutti i metodi HTTP utilizzati per le API REST: +Oppure `#[RestMethods]` consentirà l'accesso con tutti i metodi HTTP usati per le API REST: ```php #[\Attribute] @@ -174,4 +174,4 @@ class ApiPresenter extends Nette\Application\UI\Presenter Conclusione ----------- -L'attributo `#[Requires]` ti offre grande flessibilità e controllo su come sono accessibili le tue pagine web. Utilizzando regole semplici ma potenti, puoi aumentare la sicurezza e il corretto funzionamento della tua applicazione. Come vedi, l'uso degli attributi in Nette può non solo facilitare il tuo lavoro, ma anche renderlo più sicuro. +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 index 8eb011319e..b779a4a8d2 100644 --- a/best-practices/it/composer.texy +++ b/best-practices/it/composer.texy @@ -1,12 +1,12 @@ -Composer: suggerimenti per l'uso -******************************** +Consigli per l'uso di Composer +****************************** <div class=perex> -Composer è uno strumento per la gestione delle dipendenze in PHP. Ci permette di elencare le librerie da cui dipende il nostro progetto e si occuperà di installarle e aggiornarle per noi. Vedremo: +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 -- il suo utilizzo in un progetto nuovo o esistente +- come usarlo in un progetto nuovo o esistente </div> @@ -14,31 +14,31 @@ Composer è uno strumento per la gestione delle dipendenze in PHP. Ci permette d Installazione ============= -Composer è un file `.phar` eseguibile, che si scarica e si installa nel seguente modo: +Composer è un file eseguibile `.phar` che scaricate e installate così. Windows ------- -Utilizzare l'installer ufficiale [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. +Usate l'installer ufficiale [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. Linux, macOS ------------ -Bastano 4 comandi, che possono essere copiati da [questa pagina |https://getcomposer.org/download/]. +Vi bastano 4 comandi, che potete copiare da [questa pagina |https://getcomposer.org/download/]. -Inoltre, inserendolo in una cartella che si trova nel `PATH` di sistema, Composer diventa accessibile globalmente: +Inoltre, copiandolo in una cartella che si trova nel `PATH` di sistema, Composer diventa accessibile globalmente: ```shell -$ mv ./composer.phar ~/bin/composer # o /usr/local/bin/composer +$ mv ./composer.phar ~/bin/composer # oppure /usr/local/bin/composer ``` -Utilizzo nel progetto -===================== +Uso nel progetto +================ -Per poter iniziare a usare Composer nel nostro progetto, è necessario solo il file `composer.json`. Questo descrive le dipendenze del nostro progetto e può anche contenere altri metadati. Un `composer.json` di base può quindi apparire così: +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 { @@ -48,17 +48,17 @@ Per poter iniziare a usare Composer nel nostro progetto, è necessario solo il f } ``` -Qui specifichiamo che la nostra applicazione (o libreria) richiede il pacchetto `nette/database` (il nome del pacchetto è composto dal nome dell'organizzazione e dal nome del progetto) e richiede una versione che corrisponda alla condizione `^3.0` (cioè la versione più recente 3.x.x). +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). -Quindi, con il file `composer.json` nella root del progetto, si esegue l'installazione: +Con il file `composer.json` nella radice del progetto lanciamo quindi: ```shell composer update ``` -Composer scaricherà Nette Database nella cartella `vendor/`. Inoltre, creerà il file `composer.lock`, che contiene informazioni su quali versioni esatte delle librerie ha installato. +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 genererà il file `vendor/autoload.php`, che possiamo semplicemente includere e iniziare a usare le librerie senza alcun lavoro aggiuntivo: +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'; @@ -67,52 +67,52 @@ $db = new Nette\Database\Connection('sqlite::memory:'); ``` -Aggiornamento dei pacchetti alle versioni più recenti -===================================================== +Aggiornare i pacchetti alle ultime versioni +=========================================== -L'aggiornamento delle librerie utilizzate alle versioni più recenti secondo le condizioni definite in `composer.json` è gestito dal comando `composer update`. Ad esempio, per la dipendenza `"nette/database": "^3.0"` installerà la versione più recente 3.x.x, ma non la versione 4. +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 le condizioni nel file `composer.json`, ad esempio a `"nette/database": "^4.1"`, in modo da poter installare la versione più recente, utilizzare il comando `composer require nette/database`. +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 utilizzati, sarebbe necessario elencarli tutti nella riga di comando, ad esempio: +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. Utilizzare quindi il semplice script "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, che lo farà al posto vostro: +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 ``` -Creazione di un nuovo progetto -============================== +Creare un nuovo progetto +======================== -È possibile creare un nuovo progetto Nette con un solo comando: +Un nuovo progetto Nette lo create con un unico comando: ```shell -composer create-project nette/web-project nome-progetto +composer create-project nette/web-project nome-del-progetto ``` -Come `nome-progetto` inserire il nome della directory per il proprio progetto e confermare. Composer scaricherà il repository `nette/web-project` da GitHub, che contiene già il file `composer.json`, e subito dopo Nette Framework. A questo punto, dovrebbe essere sufficiente solo [impostare i permessi delle directory |nette:troubleshooting#Impostazione dei permessi delle directory] di scrittura sulle cartelle `temp/` e `log/` e il progetto dovrebbe prendere vita. +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 si sa su quale versione di PHP verrà ospitato il progetto, non dimenticate di [impostarla |#Versione PHP]. +Se sapete su quale versione di PHP sarà ospitato il progetto, non dimenticate di [impostarla |#Versione di PHP]. -Versione PHP -============ +Versione di PHP +=============== -Composer installa sempre le versioni dei pacchetti compatibili con la versione di PHP attualmente in uso (meglio dire con la versione di PHP utilizzata nella riga di comando durante l'esecuzione di Composer). Che però probabilmente non è la stessa versione utilizzata dall'ambiente di hosting. Pertanto, è molto importante aggiungere al file `composer.json` l'informazione sulla versione di PHP nell'ambiente di hosting. Successivamente verranno installate solo le versioni dei pacchetti compatibili con l'ambiente di hosting. +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 specificare che il progetto verrà eseguito, ad esempio, su PHP 8.2.3, utilizzare il comando: +Per impostare per esempio che il progetto girerà su PHP 8.2.3 usate il comando: ```shell composer config platform.php 8.2.3 ``` -In questo modo la versione viene scritta nel file `composer.json`: +Così la versione verrà scritta nel file `composer.json`: ```js { @@ -124,13 +124,13 @@ In questo modo la versione viene scritta nel file `composer.json`: } ``` -Tuttavia, il numero di versione di PHP viene specificato anche in un altro punto del file, nella sezione `require`. Mentre il primo numero determina per quale versione verranno installati i pacchetti, il secondo numero indica per quale versione è scritta l'applicazione stessa. E in base ad esso, ad esempio, PhpStorm imposta il *PHP language level*. (Ovviamente non ha senso che queste versioni differiscano, quindi la doppia scrittura è un'imprecisione.) Questa versione si imposta con il comando: +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 ``` -O direttamente nel file `composer.json`: +Oppure direttamente nel file `composer.json`: ```js { @@ -144,51 +144,51 @@ O direttamente nel file `composer.json`: Ignorare la versione di PHP =========================== -I pacchetti di solito specificano sia la versione minima di PHP con cui sono compatibili, sia la più alta con cui sono testati. Se si intende utilizzare una versione di PHP ancora più recente, ad esempio per motivi di test, Composer rifiuterà di installare tale pacchetto. La soluzione è l'opzione `--ignore-platform-req=php+`, che fa sì che Composer ignori i limiti superiori della versione PHP richiesta. +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. -Messaggi falsi -============== +Segnalazioni false +================== -Durante l'aggiornamento dei pacchetti o la modifica dei numeri di versione, capita che si verifichi un conflitto. Un pacchetto ha requisiti che sono in conflitto con un altro e simili. Composer però a volte stampa falsi messaggi. Segnala un conflitto che in realtà non esiste. In tal caso, può essere utile eliminare il file `composer.lock` e riprovare. +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 è reale ed è necessario interpretarlo per capire cosa e come modificare. +Se il messaggio di errore persiste, allora è serio e bisogna leggerlo per capire cosa e come modificare. -Packagist.org - repository centrale -=================================== +Packagist.org - repository globale +================================== -[Packagist |https://packagist.org] è il repository principale in cui Composer cerca di trovare i pacchetti, a meno che non gli diciamo diversamente. Possiamo pubblicare qui anche i nostri pacchetti. +[Packagist |https://packagist.org] è il repository principale in cui Composer cerca i pacchetti per impostazione predefinita. Qui potete anche pubblicare i vostri pacchetti. -Cosa succede se non vogliamo usare il repository centrale? ----------------------------------------------------------- +E se non volessimo il repository centrale +----------------------------------------- -Se abbiamo applicazioni interne all'azienda, che semplicemente non possiamo ospitare pubblicamente, allora creiamo per esse un repository aziendale. +Se abbiamo in azienda applicazioni o librerie interne che non possono essere ospitate pubblicamente, possiamo crearci i nostri repository. -Maggiori informazioni sul tema dei repository [nella documentazione ufficiale |https://getcomposer.org/doc/05-repositories.md#repositories]. +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 da esso installate, che si avvia includendo il file `vendor/autoload.php`. +Una caratteristica fondamentale di Composer è che fornisce l'autoloading per tutte le classi che installa. Lo attivate includendo il file `vendor/autoload.php`. -Tuttavia, è possibile utilizzare Composer anche per caricare altre classi al di fuori della cartella `vendor`. La prima opzione è far sì che Composer esamini le cartelle e le sottocartelle definite, trovi tutte le classi e le includa nell'autoloader. Ciò si ottiene impostando `autoload > classmap` in `composer.json`: +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 cartella src/ e le sue sottocartelle + "src/", # include la directory src/ e le sue sottodirectory ] } } ``` -Successivamente, è necessario eseguire il comando `composer dumpautoload` ad ogni modifica e far rigenerare le tabelle di autoloading. Questo è estremamente scomodo ed è molto meglio affidare questo compito a [RobotLoader|robot-loader:], che esegue la stessa attività automaticamente in background e molto più velocemente. +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 opzione è rispettare [PSR-4|https://www.php-fig.org/psr/psr-4/]. In parole povere, si tratta di un sistema in cui i namespace e i nomi delle classi corrispondono alla struttura delle directory e ai nomi dei file, quindi ad esempio `App\Core\RouterFactory` sarà nel file `/path/to/App/Core/RouterFactory.php`. Esempio di configurazione: +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 { @@ -200,13 +200,13 @@ La seconda opzione è rispettare [PSR-4|https://www.php-fig.org/psr/psr-4/]. In } ``` -Come configurare esattamente il comportamento è descritto nella [documentazione di Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. +Come configurare questo comportamento lo trovate nella [documentazione di Composer |https://getcomposer.org/doc/04-schema.md#psr-4]. -Testare nuove versioni -====================== +Provare le versioni nuove +========================= -Si desidera testare una nuova versione di sviluppo di un pacchetto. Come fare? Innanzitutto, aggiungere al file `composer.json` questa coppia di opzioni, che permetterà di installare versioni di sviluppo dei pacchetti, ma ricorrerà ad essa solo nel caso in cui non esista alcuna combinazione di versioni stabili che soddisfi i requisiti: +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 { @@ -215,33 +215,33 @@ Si desidera testare una nuova versione di sviluppo di un pacchetto. Come fare? I } ``` -Inoltre, consigliamo di eliminare il file `composer.lock`, a volte infatti Composer rifiuta inspiegabilmente l'installazione e questo risolve il problema. +Consigliamo inoltre di cancellare il file `composer.lock`, perché a volte Composer rifiuta inspiegabilmente l'installazione e questo risolve il problema. -Supponiamo che si tratti del pacchetto `nette/utils` e che la nuova versione abbia il numero 4.0. Si installa con il comando: +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 è possibile installare una versione specifica, ad esempio 4.0.0-RC2: +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 è bloccato a una versione precedente (es. `^3.1`), allora è ideale aggiornare il pacchetto affinché funzioni con la nuova versione. Se però si vuole solo aggirare la limitazione e costringere Composer a installare la versione di sviluppo e fingere che sia una versione precedente (es. 3.1.6), si può usare la parola chiave `as`: +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" ``` -Chiamata di comandi -=================== +Richiamare comandi +================== -Tramite Composer è possibile chiamare comandi e script personalizzati pre-preparati, come se fossero comandi nativi di Composer. Per gli script che si trovano nella cartella `vendor/bin`, non è necessario specificare questa cartella. +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 nel file `composer.json` uno script che, utilizzando [Nette Tester|tester:], esegue i test: +Come esempio definiamo in `composer.json` uno script che usa [Nette Tester |tester:] per eseguire i test: ```js { @@ -251,19 +251,19 @@ Come esempio, definiamo nel file `composer.json` uno script che, utilizzando [Ne } ``` -Eseguiamo quindi i test con `composer tester`. Possiamo chiamare il comando anche se non siamo nella cartella principale del progetto, ma in una sottodirectory. +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. -Invia un ringraziamento -======================= +Mandate un grazie +================= -Vi mostreremo un trucco che farà piacere agli autori open source. In modo semplice, si darà una stella su GitHub alle librerie che il vostro progetto utilizza. Basta installare la libreria `symfony/thanks`: +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 eseguire: +E poi lanciare: ```shell composer thanks @@ -275,7 +275,7 @@ Provate! Configurazione ============== -Composer è strettamente legato allo strumento di versioning [Git |https://git-scm.com]. Se non è installato, è necessario indicare a Composer di non usarlo: +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 index 7a5868873c..9ac114e0de 100644 --- a/best-practices/it/creating-editing-form.texy +++ b/best-practices/it/creating-editing-form.texy @@ -1,16 +1,16 @@ -Modulo per la creazione e la modifica di un record -************************************************** +Form per creare e modificare un record +************************************** .[perex] -Come implementare correttamente l'aggiunta e la modifica di un record in Nette, utilizzando lo stesso modulo per entrambe le operazioni? +Come realizzare correttamente in Nette l'aggiunta e la modifica di un record usando per entrambe lo stesso form? -In molti casi i moduli per l'aggiunta e la modifica di un record sono gli stessi, differendo magari solo per l'etichetta sul pulsante. Mostreremo esempi di semplici presenter in cui utilizzeremo il modulo prima per aggiungere un record, poi per modificarlo e infine combineremo entrambe le soluzioni. +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. -Aggiunta di un record ---------------------- +Aggiungere un record +-------------------- -Esempio di un presenter utilizzato per aggiungere un record. Lasceremo il lavoro effettivo con il database alla classe `Facade`, il cui codice non è essenziale per l'esempio. +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 @@ -27,13 +27,13 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $form = new Form; - // ... aggiungiamo i campi del modulo ... + // ... aggiunta dei campi del form ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // aggiunta del record al database $this->flashMessage('Aggiunto con successo'); @@ -48,10 +48,10 @@ class RecordPresenter extends Nette\Application\UI\Presenter ``` -Modifica di un record ---------------------- +Modificare un record +-------------------- -Ora mostreremo come apparirebbe un presenter utilizzato per modificare un record: +Vediamo ora come apparirebbe un presenter per modificare un record: ```php @@ -81,21 +81,21 @@ class RecordPresenter extends Nette\Application\UI\Presenter protected function createComponentRecordForm(): Form { - // verifichiamo che l'azione sia 'edit' + // verifica che l'azione sia 'edit' if ($this->getAction() !== 'edit') { $this->error(); } $form = new Form; - // ... aggiungiamo i campi del modulo ... + // ... aggiunta dei campi del form ... $form->setDefaults($this->record); // impostazione dei valori predefiniti - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->update($this->record->id, $data); // aggiornamento del record $this->flashMessage('Aggiornato con successo'); @@ -104,9 +104,9 @@ class RecordPresenter extends Nette\Application\UI\Presenter } ``` -Nel metodo *action*, che viene eseguito all'inizio del [ciclo di vita del presenter |application:presenters#Ciclo di vita del presenter], verifichiamo l'esistenza del record e i permessi dell'utente per modificarlo. +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` in modo da averlo disponibile nel metodo `createComponentRecordForm()` per impostare i valori predefiniti e in `recordFormSucceeded()` per l'ID. Una soluzione alternativa sarebbe impostare i valori predefiniti direttamente in `actionEdit()` e ottenere il valore dell'ID, che fa parte dell'URL, utilizzando `getParameter('id')`: +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 @@ -119,7 +119,7 @@ Salviamo il record nella proprietà `$record` in modo da averlo disponibile nel $this->error(); } - // impostazione dei valori predefiniti del modulo + // impostazione dei valori predefiniti del form $this->getComponent('recordForm') ->setDefaults($record); } @@ -130,16 +130,15 @@ Salviamo il record nella proprietà `$record` in modo da averlo disponibile nel $this->facade->update($id, $data); // ... } -} ``` -Tuttavia, e questo dovrebbe essere **il punto chiave più importante dell'intero codice**, dobbiamo assicurarci durante la creazione del modulo che l'azione sia effettivamente `edit`. Altrimenti, la verifica nel metodo `actionEdit()` non verrebbe eseguita affatto! +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! -Stesso modulo per l'aggiunta e la modifica +Lo stesso form per aggiungere e modificare ------------------------------------------ -E ora uniamo entrambi i presenter in uno solo. Potremmo distinguere quale azione è in corso nel metodo `createComponentRecordForm()` e configurare il modulo di conseguenza, oppure possiamo lasciarlo direttamente ai metodi action e liberarci della condizione: +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 @@ -153,7 +152,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void @@ -168,31 +167,31 @@ class RecordPresenter extends Nette\Application\UI\Presenter $form = $this->getComponent('recordForm'); $form->setDefaults($record); // impostazione dei valori predefiniti - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form { - // verifichiamo che l'azione sia 'add' o 'edit' + // verifica che l'azione sia 'add' oppure 'edit' if (!in_array($this->getAction(), ['add', 'edit'])) { $this->error(); } $form = new Form; - // ... aggiungiamo i campi del modulo ... + // ... aggiunta dei campi del form ... return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // aggiunta del record al database $this->flashMessage('Aggiunto con successo'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); $this->facade->update($id, $data); // aggiornamento del record diff --git a/best-practices/it/dynamic-snippets.texy b/best-practices/it/dynamic-snippets.texy index e2c34a8080..850c70251b 100644 --- a/best-practices/it/dynamic-snippets.texy +++ b/best-practices/it/dynamic-snippets.texy @@ -1,7 +1,10 @@ Snippet dinamici **************** -Abbastanza spesso, durante lo sviluppo di applicazioni, sorge la necessità di eseguire operazioni AJAX, ad esempio, su singole righe di una tabella o elementi di un elenco. Come esempio, possiamo scegliere la visualizzazione di articoli, consentendo a ciascun utente loggato di scegliere una valutazione "mi piace/non mi piace". Il codice del presenter e del template corrispondente senza AJAX apparirà approssimativamente come segue (riporto le parti più importanti, il codice presume l'esistenza di un servizio per contrassegnare le valutazioni e ottenere la collezione di articoli - l'implementazione specifica non è importante ai fini di questo tutorial): +.[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 @@ -24,26 +27,26 @@ Template: <h2>{$article->title}</h2> <div class="content">{$article->content}</div> {if !$article->liked} - <a n:href="like! $article->id" class=ajax>mi piace</a> + <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> + <a n:href="unlike! $article->id" class=ajax>Non mi piace più</a> {/if} </article> ``` -Ajaxificazione -============== +Ajaxizzazione +============= -Ora dotiamo questa semplice applicazione di AJAX. La modifica della valutazione di un articolo non è così importante da richiedere un reindirizzamento, quindi idealmente dovrebbe avvenire tramite AJAX in background. Utilizzeremo lo [script di gestione degli addon |application:ajax#Naja] con la convenzione usuale che i link AJAX abbiano la classe CSS `ajax`. +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`. -Tuttavia, come farlo concretamente? Nette offre 2 percorsi: il percorso dei cosiddetti snippet dinamici e il percorso dei componenti. Entrambi hanno i loro pro e contro, quindi li mostreremo uno per uno. +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. -Il percorso degli snippet dinamici -================================== +La strada degli snippet dinamici +================================ -Uno snippet dinamico, nella terminologia Latte, significa un caso specifico di utilizzo del tag `{snippet}`, in cui viene utilizzata una variabile nel nome dello snippet. Tale snippet non può trovarsi ovunque nel template - deve essere racchiuso da uno snippet statico, cioè uno normale, o all'interno di `{snippetArea}`. Potremmo modificare il nostro template come segue. +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 @@ -53,18 +56,18 @@ Uno snippet dinamico, nella terminologia Latte, significa un caso specifico di u <div class="content">{$article->content}</div> {snippet article-{$article->id}} {if !$article->liked} - <a n:href="like! $article->id" class=ajax>mi piace</a> + <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> + <a n:href="unlike! $article->id" class=ajax>Non mi piace più</a> {/if} {/snippet} </article> {/snippet} ``` -Ogni articolo ora definisce uno snippet che ha l'ID dell'articolo nel nome. Tutti questi snippet sono poi racchiusi insieme da uno snippet chiamato `articlesContainer`. Se omettessimo questo snippet contenitore, Latte ci avviserebbe con un'eccezione. +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. -Ci resta da aggiungere il ridisegno nel presenter - basta ridisegnare il contenitore statico. +Non resta che aggiungere al presenter la logica di ridisegno: basta ridisegnare l'involucro statico. ```php public function handleLike(int $articleId): void @@ -72,18 +75,18 @@ 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 + // $this->redrawControl('article-' . $articleId); -- non necessario } else { $this->redirect('this'); } } ``` -Modifichiamo in modo simile anche il metodo gemello `handleUnlike()`, e AJAX è funzionante! +Modificate allo stesso modo il metodo `handleUnlike()` corrispondente e AJAX funziona! -La soluzione ha però un lato oscuro. Se esaminassimo più da vicino come avviene la richiesta AJAX, scopriremmo che, sebbene esternamente l'applicazione sembri efficiente (restituisce solo un singolo snippet per l'articolo dato), in realtà sul server ha renderizzato tutti gli snippet. Ha inserito lo snippet desiderato nel payload e ha scartato gli altri (ottenendoli quindi inutilmente anche dal database). +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 questo processo, dovremo intervenire dove passiamo la collezione `$articles` al template (diciamo nel metodo `renderDefault()`). Sfrutteremo il fatto che l'elaborazione dei segnali avviene prima dei metodi `render<Something>`: +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 @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -Ora, durante l'elaborazione del segnale, al template viene passato un array con un solo articolo - quello che vogliamo renderizzare e inviare nel payload al browser - invece della collezione con tutti gli articoli. `{foreach}` quindi verrà eseguito solo una volta e non verranno renderizzati snippet aggiuntivi. +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. -Il percorso dei componenti -========================== +La strada dei componenti +======================== -Un modo completamente diverso di risolvere il problema evita gli snippet dinamici. Il trucco sta nel trasferire l'intera logica in un componente separato - d'ora in poi non sarà il presenter a occuparsi dell'inserimento delle valutazioni, ma un `LikeControl` dedicato. La classe apparirà come segue (oltre a ciò, conterrà anche i metodi `render`, `handleUnlike` ecc.): +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 @@ -139,14 +142,14 @@ Template del componente: ```latte {snippet} {if !$article->liked} - <a n:href="like!" class=ajax>mi piace</a> + <a n:href="like!" class=ajax>Mi piace</a> {else} - <a n:href="unlike!" class=ajax>non mi piace più</a> + <a n:href="unlike!" class=ajax>Non mi piace più</a> {/if} {/snippet} ``` -Naturalmente, il template della vista cambierà e dovremo aggiungere una factory al presenter. Poiché creeremo il componente tante volte quanti sono gli articoli ottenuti dal database, utilizzeremo la classe [Multiplier|application:Multiplier] per la sua "moltiplicazione". +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() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -Il template della vista si riduce al minimo indispensabile (e completamente privo di snippet!): +Il template della vista si riduce al minimo indispensabile (e non contiene alcuno snippet!): ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ Il template della vista si riduce al minimo indispensabile (e completamente priv </article> ``` -Abbiamo quasi finito: l'applicazione ora funzionerà in modo AJAX. Anche qui dovremo ottimizzare l'applicazione, perché a causa dell'uso di Nette Database, durante l'elaborazione del segnale vengono caricati inutilmente tutti gli articoli dal database invece di uno solo. Il vantaggio, tuttavia, è che non verranno renderizzati, perché verrà renderizzato effettivamente solo il nostro componente. +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/editors-and-tools.texy b/best-practices/it/editors-and-tools.texy deleted file mode 100644 index 9386b93f8e..0000000000 --- a/best-practices/it/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editor e strumenti -****************** - -.[perex] -Potreste essere un programmatore esperto, ma solo con i buoni strumenti diventerete dei maestri. In questo capitolo troverete suggerimenti su strumenti, editor e plugin importanti. - - -Editor IDE -========== - -Consigliamo vivamente di utilizzare un IDE completo per lo sviluppo, come PhpStorm, NetBeans, VS Code, e non solo un editor di testo con supporto PHP. La differenza è davvero fondamentale. Non c'è motivo di accontentarsi di un semplice editor che colora la sintassi ma non raggiunge le capacità di un IDE di alto livello, che suggerisce con precisione, controlla gli errori, sa refattorizzare il codice e molto altro. Alcuni IDE sono a pagamento, altri addirittura gratuiti. - -**NetBeans IDE** ha il supporto per Nette, Latte e NEON già integrato. - -**PhpStorm**: installate questi plugin in `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: cercate nel marketplace il plugin "Nette Latte + Neon". - -Collegate anche Tracy all'editor. Quando viene visualizzata una pagina di errore, potrete cliccare sui nomi dei file e questi si apriranno nell'editor con il cursore sulla riga corrispondente. Leggete [come configurare il sistema|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan è uno strumento che rileva gli errori logici nel codice prima ancora di eseguirlo. - -Lo installiamo tramite Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Creiamo nel progetto il file di configurazione `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -E successivamente lo facciamo analizzare le classi nella cartella `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Troverete una documentazione esaustiva direttamente sul [sito web di PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] controlla ed eventualmente corregge alcuni errori formali nei vostri codici sorgente: - -- rimuove il [BOM |nette:glossary#BOM] -- controlla la validità dei template [Latte |latte:] -- controlla la validità dei file `.neon`, `.php` e `.json` -- controlla la presenza di [caratteri di controllo |nette:glossary#Caratteri di controllo] -- controlla se il file è codificato in UTF-8 -- controlla `/* @anotace */` scritti erroneamente (manca l'asterisco) -- rimuove il tag di chiusura `?>` dai file PHP -- rimuove gli spazi finali e le righe vuote alla fine del file -- normalizza i separatori di riga a quelli di sistema (se si specifica l'opzione `-l`) - - -Composer -======== - -[Composer |Composer] è uno strumento per la gestione delle dipendenze in PHP. Ci permette di dichiarare dipendenze arbitrariamente complesse tra le singole librerie e poi le installa per noi nel nostro progetto. - - -Requirements Checker -==================== - -Era uno strumento che testava l'ambiente di runtime del server e informava se (e in che misura) fosse possibile utilizzare il framework. Attualmente, Nette può essere utilizzato su qualsiasi server che abbia la versione minima richiesta di PHP. diff --git a/best-practices/it/form-reuse.texy b/best-practices/it/form-reuse.texy index ccd4423502..b2647d659b 100644 --- a/best-practices/it/form-reuse.texy +++ b/best-practices/it/form-reuse.texy @@ -1,16 +1,16 @@ -Riutilizzo dei moduli in più punti -********************************** +Riutilizzare i form in più punti +******************************** .[perex] -In Nette avete diverse opzioni per utilizzare lo stesso modulo in più punti senza duplicare il codice. In questo articolo mostreremo diverse soluzioni, comprese quelle che dovreste evitare. +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 per moduli -================== +Factory di form +=============== -Uno degli approcci fondamentali per utilizzare lo stesso componente in più punti è creare un metodo o una classe che genera questo componente e successivamente chiamare questo metodo in diversi punti dell'applicazione. Tale metodo o classe viene chiamato *factory*. Si prega di non confondere con il pattern di progettazione *factory method*, che descrive un modo specifico di utilizzare le factory e non è correlato a questo argomento. +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 costruirà un modulo di modifica: +Come esempio creiamo una factory che costruisce un form di modifica: ```php use Nette\Application\UI\Form; @@ -21,21 +21,21 @@ class FormFactory { $form = new Form; $form->addText('title', 'Titolo:'); - // qui vengono aggiunti altri campi del modulo - $form->addSubmit('send', 'Invia'); + // qui si aggiungono gli altri campi del form + $form->addSubmit('send', 'Salva'); return $form; } } ``` -Ora potete utilizzare questa factory in diversi punti della vostra applicazione, ad esempio nei presenter o nei componenti. E questo richiedendola come [dipendenza|dependency-injection:passing-dependencies]. Prima di tutto, quindi, registriamo la classe nel file di configurazione: +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 utilizziamo nel presenter: +E poi la usiamo nel presenter: ```php @@ -57,7 +57,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Potete estendere la factory dei moduli con altri metodi per creare altri tipi di moduli secondo le esigenze della vostra applicazione. E naturalmente possiamo aggiungere anche un metodo che crei un modulo base senza elementi, e che gli altri metodi utilizzeranno: +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 @@ -72,20 +72,20 @@ class FormFactory { $form = $this->createForm(); $form->addText('title', 'Titolo:'); - // qui vengono aggiunti altri campi del modulo - $form->addSubmit('send', 'Invia'); + // qui si aggiungono gli altri campi del form + $form->addSubmit('send', 'Salva'); return $form; } } ``` -Il metodo `createForm()` per ora non fa nulla di utile, ma questo cambierà rapidamente. +Il metodo `createForm()` non fa ancora nulla di utile, ma questo cambierà presto. Dipendenze della factory ======================== -Col tempo si scoprirà che abbiamo bisogno che i moduli siano multilingue. Ciò significa che a tutti i moduli dobbiamo impostare il cosiddetto [translator |forms:rendering#Traduzione]. A tal fine, modifichiamo la classe `FormFactory` in modo che accetti l'oggetto `Translator` come dipendenza nel costruttore e lo passiamo al modulo: +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; @@ -108,13 +108,13 @@ class FormFactory } ``` -Poiché il metodo `createForm()` viene chiamato anche dagli altri metodi che creano moduli specifici, è sufficiente impostare il translator solo lì. E abbiamo finito. Non è necessario modificare il codice di nessun presenter o componente, il che è fantastico. +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 modulo che volete utilizzare nella vostra applicazione. Questo approccio può aumentare la leggibilità del codice e facilitare la gestione dei moduli. Lasciamo che la `FormFactory` originale crei solo un modulo pulito con la configurazione di base (ad esempio con il supporto alle traduzioni) e per il modulo di modifica creiamo una nuova factory `EditFormFactory`. +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 @@ -144,39 +144,39 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - // qui vengono aggiunti altri campi del modulo - $form->addSubmit('send', 'Invia'); + // qui si aggiungono gli altri campi del form + $form->addSubmit('send', 'Salva'); return $form; } } ``` -È molto importante che la relazione tra le classi `FormFactory` e `EditFormFactory` sia realizzata tramite [composizione |nette:introduction-to-object-oriented-programming#Composizione], e non tramite [ereditarietà degli oggetti |nette:introduction-to-object-oriented-programming#Ereditarietà]: +È 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 -// ⛔ NON COSÌ! L'EREDITARIETÀ NON APPARTIENE QUI +// ⛔ NO! L'EREDITARIETÀ QUI NON C'ENTRA class EditFormFactory extends FormFactory { public function create(): Form { $form = parent::create(); $form->addText('title', 'Titolo:'); - // qui vengono aggiunti altri campi del modulo - $form->addSubmit('send', 'Invia'); + // qui si aggiungono gli altri campi del form + $form->addSubmit('send', 'Salva'); return $form; } } ``` -L'uso dell'ereditarietà sarebbe in questo caso del tutto controproducente. Incontrereste problemi molto rapidamente. Ad esempio, nel momento in cui voleste aggiungere parametri al metodo `create()`; PHP segnalerebbe un errore indicando che la sua firma differisce da quella del genitore. Oppure passando una dipendenza alla classe `EditFormFactory` tramite il costruttore. Si verificherebbe una situazione che chiamiamo [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. +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à]. +In generale è meglio preferire la [composizione all'ereditarietà |dependency-injection:faq#Perché si preferisce la composizione all'ereditarietà?]. -Gestione del modulo -=================== +Gestione del form +================= -La gestione del modulo, che viene chiamata dopo l'invio riuscito, può anche far parte della classe factory. Funzionerà passando i dati inviati al modello per l'elaborazione. Eventuali errori verranno [restituiti |forms:validation#Errori durante l Elaborazione] al modulo. Il modello nell'esempio seguente è rappresentato dalla classe `Facade`: +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 @@ -191,13 +191,13 @@ class EditFormFactory { $form = $this->formFactory->create(); $form->addText('title', 'Titolo:'); - // qui vengono aggiunti altri campi del modulo - $form->addSubmit('send', 'Invia'); - $form->onSuccess[] = [$this, 'processForm']; + // qui si aggiungono gli altri campi del form + $form->addSubmit('send', 'Salva'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // elaborazione dei dati inviati @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Tuttavia, lasceremo il reindirizzamento effettivo al presenter. Aggiungerà un altro gestore all'evento `onSuccess`, che eseguirà il reindirizzamento. Grazie a ciò, sarà possibile utilizzare il modulo in diversi presenter e reindirizzare a un luogo diverso in ciascuno di essi. +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 @@ -232,38 +232,38 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Questa soluzione sfrutta la proprietà dei moduli secondo cui, quando viene chiamato `addError()` sul modulo o su un suo elemento, il successivo gestore `onSuccess` non viene più chiamato. +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. -Ereditarietà dalla classe Form -============================== +Ereditare dalla classe Form +=========================== -Il modulo costruito non deve essere un discendente del modulo. In altre parole, non utilizzate questa soluzione: +Un form assemblato non dovrebbe essere un discendente della classe `Form`. In altre parole, non usate questa soluzione: ```php -// ⛔ NON COSÌ! L'EREDITARIETÀ NON APPARTIENE QUI +// ⛔ NO! L'EREDITARIETÀ QUI NON C'ENTRA class EditForm extends Form { public function __construct(Translator $translator) { parent::__construct(); $this->addText('title', 'Titolo:'); - // qui vengono aggiunti altri campi del modulo - $this->addSubmit('send', 'Invia'); + // qui si aggiungono gli altri campi del form + $this->addSubmit('send', 'Salva'); $this->setTranslator($translator); } } ``` -Invece di costruire il modulo nel costruttore, utilizzate una factory. +Invece di assemblare il form nel costruttore, usate una factory. -È necessario rendersi conto che la classe `Form` è principalmente uno strumento per costruire un modulo, ovvero un *form builder*. E il modulo costruito può essere considerato come il suo prodotto. Ma il prodotto non è un caso specifico del builder, non c'è tra loro una relazione *is a* che costituisce la base dell'ereditarietà. +È 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 modulo -===================== +Componente con form +=================== -Un approccio completamente diverso è la creazione di un [componente|application:components], che include un modulo. Questo offre nuove possibilità, ad esempio renderizzare il modulo in modo specifico, poiché il componente include anche un template. Oppure è possibile utilizzare i segnali per la comunicazione AJAX e il caricamento dinamico di informazioni nel modulo, ad esempio per i suggerimenti, ecc. +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 @@ -282,14 +282,14 @@ class EditControl extends Nette\Application\UI\Control { $form = new Form; $form->addText('title', 'Titolo:'); - // qui vengono aggiunti altri campi del modulo - $form->addSubmit('send', 'Invia'); - $form->onSuccess[] = [$this, 'processForm']; + // qui si aggiungono gli altri campi del form + $form->addSubmit('send', 'Salva'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // elaborazione dei dati inviati @@ -300,13 +300,13 @@ class EditControl extends Nette\Application\UI\Control return; } - // attivazione dell'evento + // richiamo dell'evento $this->onSave($this, $data); } } ``` -Creeremo anche una factory che produrrà questo componente. È sufficiente [scrivere la sua interfaccia |application:components#Componenti con dipendenze]: +Creiamo poi una factory che produrrà questo componente. Basta [definirne l'interfaccia |application:components#Componenti con dipendenze]: ```php interface EditControlFactory @@ -322,7 +322,7 @@ services: - EditControlFactory ``` -E ora possiamo richiedere la factory e utilizzarla nel presenter: +E ora possiamo farci passare la factory e usarla nel presenter: ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // o reindirizziamo al risultato della modifica, ad esempio: + // oppure redirect al risultato della modifica, per esempio: // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/it/inject-method-attribute.texy b/best-practices/it/inject-method-attribute.texy index d031886e4c..c76db495d8 100644 --- a/best-practices/it/inject-method-attribute.texy +++ b/best-practices/it/inject-method-attribute.texy @@ -2,17 +2,17 @@ Metodi e attributi inject ************************* .[perex] -In questo articolo ci concentreremo sui diversi modi di passare le dipendenze ai presenter nel framework Nette. Confronteremo il metodo preferito, che è il costruttore, con altre opzioni come i metodi e gli attributi `inject`. +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`. -Anche per i presenter vale che il passaggio delle dipendenze tramite il [costruttore |dependency-injection:passing-dependencies#Passaggio tramite costruttore] è il percorso preferito. Tuttavia, se si crea un antenato comune da cui ereditano altri presenter (ad es. `BasePresenter`), e questo antenato ha anch'esso delle dipendenze, si verifica un problema che chiamiamo [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Questo può essere aggirato utilizzando percorsi alternativi, che sono rappresentati dai metodi e dagli attributi (annotazioni) `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 della dipendenza tramite [setter |dependency-injection:passing-dependencies#Passaggio tramite setter]. Il nome di questi setter inizia con il prefisso `inject`. Nette DI chiama automaticamente i metodi così denominati subito dopo la creazione dell'istanza del presenter e passa loro tutte le dipendenze richieste. Devono quindi essere dichiarati come public. +È 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*()` possono essere considerati come una sorta di estensione del costruttore in più metodi. Grazie a ciò, `BasePresenter` può ricevere le dipendenze tramite un altro metodo e lasciare il costruttore libero per i suoi discendenti: +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 @@ -36,15 +36,15 @@ class MyPresenter extends BasePresenter } ``` -Un presenter può contenere un numero qualsiasi di metodi `inject*()` e ognuno può avere un numero qualsiasi di parametri. Sono ottimi anche nei casi in cui il presenter è [composto da trait |presenter-traits] e ognuno di essi richiede la propria dipendenza. +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 nella proprietà |dependency-injection:passing-dependencies#Impostazione di una variabile]. È sufficiente contrassegnare in quali variabili iniettare e Nette DI passerà automaticamente le dipendenze subito dopo la creazione dell'istanza del presenter. Per poterle inserire, è necessario dichiararle come public. +È 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. -Contrassegniamo le proprietà con un attributo: (in precedenza si usava l'annotazione `/** @inject */`) +Le proprietà si contrassegnano con un attributo: (in passato si usava l'annotazione `/** @inject */`) ```php use Nette\DI\Attributes\Inject; // questa riga è importante @@ -56,6 +56,6 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Il vantaggio di questo modo di passare le dipendenze era la forma di scrittura molto concisa. Tuttavia, con l'avvento della [constructor property promotion |https://blog.nette.org/it/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], sembra più facile utilizzare il costruttore. +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 svantaggi del passaggio delle dipendenze alle proprietà in generale: non abbiamo controllo sulle modifiche nella variabile e allo stesso tempo la variabile diventa parte dell'interfaccia pubblica della classe, il che è indesiderabile. +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 index 5b7992de4b..86252327f0 100644 --- a/best-practices/it/lets-create-contact-form.texy +++ b/best-practices/it/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -Creazione di un modulo di contatto -********************************** +Creiamo un form di contatto +*************************** .[perex] -Vediamo come creare un modulo di contatto in Nette, compreso l'invio di email. Allora, iniziamo! +Vediamo come creare in Nette un form di contatto, compreso l'invio per email dei dati inseriti. Cominciamo! -Prima di tutto, dobbiamo creare un nuovo progetto. Come farlo è spiegato nella pagina [Iniziare |nette:installation]. E poi possiamo iniziare a creare il modulo. +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 [modulo direttamente nel presenter |forms:in-presenter]. Possiamo utilizzare il `HomePresenter` pre-preparato. Aggiungeremo ad esso il componente `contactForm` che rappresenta il modulo. Lo faremo scrivendo nel codice il metodo factory `createComponentContactForm()`, che produrrà il componente: +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; @@ -18,39 +18,36 @@ class HomePresenter extends Presenter { $form = new Form; $form->addText('name', 'Nome:') - ->setRequired('Inserisci il nome'); + ->setRequired('Inserite il vostro nome'); $form->addEmail('email', 'E-mail:') - ->setRequired('Inserisci l\'e-mail'); - $form->addTextarea('message', 'Messaggio:') - ->setRequired('Inserisci il messaggio'); + ->setRequired('Inserite la vostra email'); + $form->addTextArea('message', 'Messaggio:') + ->setRequired('Inserite un messaggio'); $form->addSubmit('send', 'Invia'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // invio dell'email } } ``` -Come potete vedere, abbiamo creato due metodi. Il primo metodo `createComponentContactForm()` crea un nuovo modulo. Questo ha campi per nome, email e messaggio, che aggiungiamo con i metodi `addText()`, `addEmail()` e `addTextArea()`. Abbiamo anche aggiunto un pulsante per inviare il modulo. Ma cosa succede se l'utente non compila qualche campo? In tal caso, dovremmo fargli sapere che è un campo obbligatorio. Abbiamo ottenuto questo risultato con il metodo `setRequired()`. Infine, abbiamo aggiunto anche l'[evento |nette:glossary#Eventi] `onSuccess`, che si attiva se il modulo viene inviato con successo. Nel nostro caso, chiama il metodo `contactFormSucceeded`, che si occuperà dell'elaborazione del modulo inviato. Lo aggiungeremo al codice tra un momento. +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. -Faremo renderizzare il componente `contactForm` nel template `Home/default.latte`: +Rendiamo il componente `contactForm` nel template `Home/default.latte`: ```latte {block content} -<h1>Modulo di contatto</h1> +<h1>Form di contatto</h1> {control contactForm} ``` -Per l'invio effettivo dell'email, creeremo una nuova classe che chiameremo `ContactFacade` e la posizioneremo nel file `app/Model/ContactFacade.php`: +Per l'invio vero e proprio dell'email creeremo una nuova classe chiamata `ContactFacade` e la metteremo nel file `app/Model/ContactFacade.php`: ```php -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -66,9 +63,9 @@ class ContactFacade public function sendMessage(string $email, string $name, string $message): void { $mail = new Message; - $mail->addTo('admin@example.com') // la tua email + $mail->addTo('admin@example.com') // la vostra email ->setFrom($email, $name) - ->setSubject('Messaggio dal modulo di contatto') + ->setSubject('Messaggio dal form di contatto') ->setBody($message); $this->mailer->send($mail); @@ -76,9 +73,9 @@ class ContactFacade } ``` -Il metodo `sendMessage()` crea e invia l'email. Utilizza a tal fine il cosiddetto mailer, che si fa passare come dipendenza tramite il costruttore. Leggete di più sull'[invio di email |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:]. -Ora torniamo al presenter e completiamo il metodo `contactFormSucceeded()`. Questo chiamerà il metodo `sendMessage()` della classe `ContactFacade` e gli passerà i dati del modulo. E come otteniamo l'oggetto `ContactFacade`? Ce lo facciamo passare tramite il costruttore: +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; @@ -106,20 +103,20 @@ class HomePresenter extends Presenter } ``` -Dopo l'invio dell'email, mostreremo ancora all'utente il cosiddetto [flash message |application:components#Messaggi flash], confermando che il messaggio è stato inviato, e poi reindirizzeremo a un'altra pagina, in modo che non sia possibile inviare nuovamente il modulo tramite *refresh* nel browser. +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. -Bene, e se tutto funziona, dovreste essere in grado di inviare un'email dal vostro modulo di contatto. Congratulazioni! +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 un'email di testo semplice contenente solo il messaggio inviato dal modulo. Ma nell'email possiamo utilizzare HTML e renderne l'aspetto più attraente. Creeremo per essa un template in Latte, che scriveremo in `app/Model/contactEmail.latte`: +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 modulo di contatto + Messaggio dal form di contatto

    Nome: {$name}

    @@ -129,7 +126,7 @@ Per ora viene inviata un'email di testo semplice contenente solo il messaggio in ``` -Resta da modificare `ContactFacade` affinché utilizzi questo template. Nel costruttore richiederemo la classe `LatteFactory`, che sa produrre l'oggetto `Latte\Engine`, ovvero il [renderizzatore di template Latte |latte:develop#Come Renderizzare un Template]. Tramite il metodo `renderToString()` renderizzeremo il template in un file, il primo parametro è il percorso del template e il secondo sono le variabili. +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; @@ -156,7 +153,7 @@ class ContactFacade ]); $mail = new Message; - $mail->addTo('admin@example.com') // la tua email + $mail->addTo('admin@example.com') // la vostra email ->setFrom($email, $name) ->setHtmlBody($body); @@ -165,15 +162,15 @@ class ContactFacade } ``` -L'email HTML generata la passeremo quindi al metodo `setHtmlBody()` invece dell'originale `setBody()`. Inoltre, non dobbiamo specificare l'oggetto dell'email in `setSubject()`, perché la libreria lo prenderà dall'elemento `` del template. +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 `<title>` del template. Configurazione -------------- -Nel codice della classe `ContactFacade` è ancora hardcoded la nostra email di amministratore `admin@example.com`. Sarebbe meglio spostarla nel file di configurazione. Come fare? +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? -Prima modifichiamo la classe `ContactFacade` e sostituiamo la stringa con l'email con una variabile passata tramite il costruttore: +Per prima cosa modifichiamo la classe `ContactFacade` sostituendo la stringa con l'email con una variabile passata dal costruttore: ```php class ContactFacade @@ -197,25 +194,25 @@ class ContactFacade } ``` -E il secondo passo è specificare il valore di questa variabile nella configurazione. Nel file `app/config/services.neon` scriviamo: +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) ``` -Ed è fatto. Se ci fossero molte voci nella sezione `services` e aveste la sensazione che l'email si perda tra di esse, possiamo trasformarla in una variabile. Modifichiamo la scrittura in: +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 questa variabile: +E nel file `app/config/common.neon` definiamo questo parametro: ```neon parameters: adminEmail: admin@example.com ``` -Ed è fatto! +Ed è fatta! diff --git a/best-practices/it/microsites.texy b/best-practices/it/microsites.texy index 1efd628d35..8c09b66602 100644 --- a/best-practices/it/microsites.texy +++ b/best-practices/it/microsites.texy @@ -1,11 +1,11 @@ -Come scrivere micrositi +Come scrivere microsite *********************** -Immaginate di dover creare rapidamente un piccolo sito web per il prossimo evento della vostra azienda. Deve essere semplice, veloce e senza complicazioni inutili. Potreste pensare che per un progetto così piccolo non abbiate bisogno di un framework robusto. Ma cosa succederebbe se l'uso del framework Nette potesse semplificare e accelerare radicalmente questo processo? +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? -Dopotutto, anche nella creazione di siti web semplici, non volete rinunciare alla comodità. Non volete reinventare ciò che è già stato risolto una volta. Siate pure pigri e lasciatevi coccolare. Nette Framework può essere utilizzato egregiamente anche come micro framework. +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 tale microsito? Ad esempio, in modo che l'intero codice del sito web sia collocato in un unico file `index.php` nella cartella pubblica: +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 @@ -16,7 +16,7 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// crea il container DI basato sulla configurazione in config.neon +// creiamo il container DI in base alla configurazione in config.neon $configurator->addConfig(__DIR__ . '/../app/config.neon'); $container = $configurator->createContainer(); @@ -24,40 +24,40 @@ $container = $configurator->createContainer(); $router = new Nette\Application\Routers\RouteList; $container->addService('router', $router); -// route per l'URL https://example.com/ +// 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 o /de ecc. + // rileviamo la lingua del browser e reindirizziamo all'URL /en oppure /de ecc. $supportedLangs = ['en', 'de', 'cs']; $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); -// route per l'URL https://example.com/cs o https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // visualizziamo il template corrispondente, ad esempio ../templates/en.latte +// 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; }); -// avvia l'applicazione! +// avviamo l'applicazione! $container->getByType(Nette\Application\Application::class)->run(); ``` -Tutto il resto saranno template salvati nella cartella padre `/templates`. +Tutto il resto saranno template salvati nella directory superiore `/templates`. -Il codice PHP in `index.php` prima [prepara l'ambiente |bootstrap:], poi definisce le [route |application:routing#Routing dinamico con callback] e infine avvia l'applicazione. Il vantaggio è che il secondo parametro della funzione `addRoute()` può essere un callable, che viene eseguito dopo l'apertura della pagina corrispondente. +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 un microsito? ------------------------------------- +Perché usare Nette per i microsite? +----------------------------------- -- I programmatori che hanno provato [Tracy|tracy:] una volta, oggi non riescono a immaginare di programmare qualcosa senza di essa. -- Ma soprattutto utilizzerete il sistema di template [Latte|latte:], perché già da 2 pagine vorrete avere separati il [layout e il contenuto|latte:template-inheritance]. -- E sicuramente volete fare affidamento sull'[escaping automatico |latte:safety-first], affinché non si crei una vulnerabilità XSS -- Nette garantisce anche che, in caso di errore, non vengano mai visualizzati messaggi di errore PHP per programmatori, ma una pagina comprensibile per l'utente. -- Se volete ottenere feedback dagli utenti, ad esempio sotto forma di modulo di contatto, aggiungerete anche i [moduli|forms:] e il [database|database:]. -- Potete anche farvi [inviare facilmente via email|mail:] i moduli compilati. -- A volte potrebbe esservi utile il [caching|caching:], ad esempio se scaricate e visualizzate feed. +- 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. -Al giorno d'oggi, quando la velocità e l'efficienza sono fondamentali, è importante avere strumenti che vi permettano di ottenere risultati senza inutili ritardi. Nette framework vi offre proprio questo: sviluppo rapido, sicurezza e un'ampia gamma di strumenti, come Tracy e Latte, che semplificano il processo. Basta installare un paio di pacchetti Nette e costruire un tale microsito diventa improvvisamente un gioco da ragazzi. E sapete che non si nasconde nessuna falla di sicurezza da nessuna parte. +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 index 1f61f6b633..2edc1daf9d 100644 --- a/best-practices/it/pagination.texy +++ b/best-practices/it/pagination.texy @@ -1,10 +1,10 @@ -Paginazione dei risultati del database -************************************** +Paginare i risultati del database +********************************* .[perex] -Durante la creazione di applicazioni web, incontrerete molto spesso la necessità di limitare il numero di elementi visualizzati per pagina. +Sviluppando applicazioni web incontrerete spesso l'esigenza di limitare il numero di elementi elencati per pagina, tecnica nota come paginazione. -Partiamo dallo stato in cui visualizziamo tutti i dati senza paginazione. Per selezionare i dati dal database abbiamo la classe ArticleRepository, che oltre al costruttore contiene il metodo `findPublishedArticles`, che restituisce tutti gli articoli pubblicati ordinati in modo decrescente per data di pubblicazione. +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; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -Nel presenter, quindi, iniettiamo la classe del modello e nel metodo render richiediamo gli articoli pubblicati, che passiamo al template: +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; @@ -52,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -Nel template `default.latte` ci occupiamo quindi della visualizzazione degli articoli: +Il template `default.latte` si occuperà poi di elencare gli articoli: ```latte {block content} @@ -67,11 +67,11 @@ Nel template `default.latte` ci occupiamo quindi della visualizzazione degli art ``` -In questo modo sappiamo visualizzare tutti gli articoli, il che però inizia a creare problemi nel momento in cui il numero di articoli aumenta. In quel momento diventa utile implementare un meccanismo di paginazione. +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 garantirà che tutti gli articoli vengano divisi in più pagine e noi visualizzeremo solo gli articoli di una pagina corrente. Il numero totale di pagine e la divisione degli articoli verranno calcolati da [Paginator|utils:Paginator] stesso in base a quanti articoli abbiamo in totale e quanti articoli per pagina vogliamo visualizzare. +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 il metodo per ottenere gli articoli nella classe del repository in modo che possa restituirci solo gli articoli per una pagina. Aggiungiamo anche un metodo per determinare il numero totale di articoli nel database, che ci servirà per impostare il Paginator: +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; @@ -108,9 +108,9 @@ class ArticleRepository } ``` -Successivamente, ci dedichiamo alle modifiche del presenter. Al metodo render passeremo il numero della pagina attualmente visualizzata. Nel caso in cui questo numero non sia parte dell'URL, imposteremo il valore predefinito della prima pagina. +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). -Inoltre, estenderemo il metodo render con l'ottenimento dell'istanza di Paginator, la sua impostazione e la selezione degli articoli corretti per la visualizzazione nel template. HomePresenter dopo le modifiche apparirà così: +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; @@ -127,27 +127,27 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Otteniamo il numero totale di articoli pubblicati + // otteniamo il numero totale di articoli pubblicati $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Creiamo un'istanza di Paginator e la impostiamo + // creiamo e configuriamo l'istanza del Paginator $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // numero totale di articoli - $paginator->setItemsPerPage(10); // numero di elementi per pagina + $paginator->setItemCount($articlesCount); // numero totale di elementi + $paginator->setItemsPerPage(10); // elementi per pagina $paginator->setPage($page); // numero della pagina corrente - // Estraiamo dal database un sottoinsieme limitato di articoli secondo il calcolo del Paginator + // carichiamo dal database un insieme limitato di articoli secondo il calcolo del Paginator $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - // che passiamo al template + // li passiamo al template $this->template->articles = $articles; - // e anche il Paginator stesso per visualizzare le opzioni di paginazione + // e con essi anche il Paginator stesso, per mostrare i controlli di paginazione $this->template->paginator = $paginator; } } ``` -Il template ora itera solo sugli articoli di una pagina, ci basta aggiungere i link di paginazione: +Il template ora itera solo sugli articoli della pagina corrente. Basta aggiungere i link di paginazione: ```latte {block content} @@ -162,9 +162,9 @@ Il template ora itera solo sugli articoli di una pagina, ci basta aggiungere i l <div class="pagination"> {if !$paginator->isFirst()} - <a n:href="default, 1">Primo</a> + <a n:href="default, 1">Prima</a>  |  - <a n:href="default, $paginator->page-1">Precedente</a> + <a n:href="default, $paginator->getPage() - 1">Precedente</a>  |  {/if} @@ -172,17 +172,17 @@ Il template ora itera solo sugli articoli di una pagina, ci basta aggiungere i l {if !$paginator->isLast()}  |  - <a n:href="default, $paginator->getPage() + 1">Successivo</a> + <a n:href="default, $paginator->getPage() + 1">Successiva</a>  |  - <a n:href="default, $paginator->getPageCount()">Ultimo</a> + <a n:href="default, $paginator->getPageCount()">Ultima</a> {/if} </div> ``` -In questo modo abbiamo aggiunto alla pagina la possibilità di paginazione tramite Paginator. Nel caso in cui, invece di [Nette Database Core |database:sql-way] come livello di database utilizziamo [Nette Database Explorer |database:explorer], siamo in grado di implementare la paginazione anche senza l'uso di Paginator. La classe `Nette\Database\Table\Selection` infatti contiene il metodo [page |api:Nette\Database\Table\Selection::_page] con la logica di paginazione ereditata da Paginator. +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. -Il repository con questo metodo di implementazione apparirà così: +Con questo approccio il repository apparirà così: ```php namespace App\Model; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -Nel presenter non dobbiamo creare Paginator, useremo al suo posto il metodo della classe `Selection`, che ci restituisce il repository: +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; @@ -222,21 +222,21 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Estraiamo gli articoli pubblicati + // otteniamo gli articoli pubblicati $articles = $this->articleRepository->findPublishedArticles(); - // e inviamo al template solo una loro parte limitata secondo il calcolo del metodo page + // 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 anche i dati necessari per visualizzare le opzioni di paginazione + // e con essa anche i dati necessari a mostrare i controlli di paginazione $this->template->page = $page; $this->template->lastPage = $lastPage; } } ``` -Poiché ora non inviamo Paginator al template, modifichiamo la parte che visualizza i link di paginazione: +Poiché al template non passiamo più l'oggetto Paginator, dobbiamo modificare la parte che mostra i link di paginazione: ```latte {block content} @@ -251,7 +251,7 @@ Poiché ora non inviamo Paginator al template, modifichiamo la parte che visuali <div class="pagination"> {if $page > 1} - <a n:href="default, 1">Primo</a> + <a n:href="default, 1">Prima</a>  |  <a n:href="default, $page - 1">Precedente</a>  |  @@ -261,13 +261,13 @@ Poiché ora non inviamo Paginator al template, modifichiamo la parte che visuali {if $page < $lastPage}  |  - <a n:href="default, $page + 1">Successivo</a> + <a n:href="default, $page + 1">Successiva</a>  |  - <a n:href="default, $lastPage">Ultimo</a> + <a n:href="default, $lastPage">Ultima</a> {/if} </div> ``` -In questo modo abbiamo implementato il meccanismo di paginazione senza l'uso di Paginator. +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 index 61357ccd6b..e49d139737 100644 --- a/best-practices/it/passing-settings-to-presenters.texy +++ b/best-practices/it/passing-settings-to-presenters.texy @@ -1,10 +1,10 @@ -Passaggio delle impostazioni ai presenter -***************************************** +Passare impostazioni ai presenter +********************************* .[perex] -Avete bisogno di passare ai presenter argomenti che non sono oggetti (ad esempio, informazioni se è in esecuzione in modalità debug, percorsi di directory, ecc.) e che quindi non possono essere passati automaticamente tramite autowiring? La soluzione è incapsularli in un oggetto `Settings`. +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` rappresenta un modo molto semplice e allo stesso tempo utile per fornire informazioni sull'applicazione in esecuzione ai presenter. La sua forma specifica dipende esclusivamente dalle vostre esigenze particolari. Esempio: +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; @@ -12,7 +12,7 @@ namespace App; class Settings { public function __construct( - // da PHP 8.1 è possibile specificare readonly + // da PHP 8.1 si può usare readonly public bool $debugMode, public string $appDir, // e così via @@ -30,7 +30,7 @@ services: ) ``` -Quando un presenter avrà bisogno delle informazioni fornite da questo servizio, semplicemente le richiederà nel costruttore: +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 diff --git a/best-practices/it/post-links.texy b/best-practices/it/post-links.texy index 7eccd96552..6d69060c47 100644 --- a/best-practices/it/post-links.texy +++ b/best-practices/it/post-links.texy @@ -1,30 +1,30 @@ -Come utilizzare correttamente i link POST -***************************************** +Come usare correttamente i link POST +************************************ .[perex] -Nelle applicazioni web, specialmente nelle interfacce amministrative, dovrebbe essere una regola fondamentale che le azioni che modificano lo stato del server non vengano eseguite tramite il metodo HTTP GET. Come suggerisce il nome del metodo, GET dovrebbe servire solo per ottenere dati, non per modificarli. Per azioni come l'eliminazione di record, è preferibile utilizzare il metodo POST. Anche se l'ideale sarebbe il metodo DELETE, ma non può essere invocato senza JavaScript, quindi storicamente si usa POST. +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 farlo in pratica? Utilizzate questo semplice trucco. All'inizio del template, create un modulo ausiliario con l'identificatore `postForm`, che utilizzerete successivamente per i pulsanti di eliminazione: +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 modulo, potete utilizzare un pulsante `<button>` invece di un classico link `<a>`, che può essere stilizzato visivamente per assomigliare a un normale link. Ad esempio, il framework CSS Bootstrap offre le classi `btn btn-link` con cui potete ottenere che il pulsante non sia visivamente diverso dagli altri link. Tramite l'attributo `form="postForm"` lo colleghiamo al modulo pre-preparato: +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}">delete</button> - <!-- instead of <a n:href="delete $post->id">delete</a> --> + <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 sul link, ora verrà invocata l'azione `delete`. Per garantire che le richieste vengano accettate solo tramite il metodo POST e dallo stesso dominio (che è una difesa efficace contro gli attacchi CSRF), utilizzate l'attributo `#[Requires]`: +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; @@ -34,15 +34,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter #[Requires(methods: 'POST', sameOrigin: true)] public function actionDelete(int $id): void { - $this->facade->deletePost($id); // codice ipotetico che elimina il record + $this->facade->deletePost($id); // codice ipotetico per eliminare un record $this->redirect('default'); } } ``` -L'attributo esiste da Nette Application 3.2 e potete saperne di più sulle sue possibilità nella pagina [Come utilizzare l'attributo #Requires |attribute-requires]. +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()` utilizzaste il segnale `handleDelete()`, non è necessario specificare `sameOrigin: true`, perché i segnali hanno questa protezione impostata implicitamente: +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')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -Questo approccio non solo migliora la sicurezza della vostra applicazione, ma contribuisce anche al rispetto degli standard e delle pratiche web corrette. Utilizzando i metodi POST per le azioni che modificano lo stato, otterrete un'applicazione più robusta e sicura. +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 index 9fa07a6483..e301aadf4b 100644 --- a/best-practices/it/presenter-traits.texy +++ b/best-practices/it/presenter-traits.texy @@ -1,14 +1,14 @@ -Composizione dei presenter da trait -*********************************** +Comporre i presenter con i trait +******************************** .[perex] -Se abbiamo bisogno di implementare lo stesso codice in più presenter (ad esempio, verificare che l'utente sia loggato), possiamo inserire il codice in un antenato comune. La seconda opzione è creare [trait |nette:introduction-to-object-oriented-programming#Trait] specifici per uno scopo. +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 di questa soluzione è che ciascuno dei presenter può utilizzare esattamente i trait di cui ha effettivamente bisogno, mentre l'ereditarietà multipla non è possibile in PHP. +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, alla creazione del presenter, vengono chiamati progressivamente tutti i [metodi inject |inject-method-attribute#Metodi inject]. È solo necessario assicurarsi che il nome di ogni metodo inject sia unico. +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 il codice di inizializzazione agli eventi [onStartup o onRender |application:presenters#Eventi]. +I trait possono agganciare del codice di inizializzazione agli eventi [onStartup o onRender |application:presenters#Eventi]. Esempi: @@ -36,7 +36,7 @@ trait StandardTemplateFilters } ``` -Il presenter quindi utilizza semplicemente questi trait: +Il presenter poi si limita a usare questi trait: ```php class ArticlePresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/it/pretty-urls.texy b/best-practices/it/pretty-urls.texy new file mode 100644 index 0000000000..abee2b4ac7 --- /dev/null +++ b/best-practices/it/pretty-urls.texy @@ -0,0 +1,204 @@ +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 index abb5ee93e8..22e4afab3e 100644 --- a/best-practices/it/restore-request.texy +++ b/best-practices/it/restore-request.texy @@ -1,16 +1,16 @@ -Come tornare alla pagina precedente? -************************************ +Come tornare a una pagina precedente? +************************************* .[perex] -Cosa succede se un utente sta compilando un modulo e la sua sessione scade? Per evitare la perdita di dati, salviamo i dati nella sessione prima di reindirizzare alla pagina di login. In Nette, questo è un gioco da ragazzi. +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 può essere salvata nella sessione tramite il metodo `storeRequest()`, che restituisce il suo identificatore sotto forma di una breve stringa. Il metodo salva il nome del presenter corrente, la vista e i suoi parametri. Nel caso in cui sia stato inviato anche un modulo, verranno salvati anche i contenuti dei campi (ad eccezione dei file caricati). +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). -Il ripristino della richiesta viene eseguito dal metodo `restoreRequest($key)`, al quale passiamo l'identificatore ottenuto. Questo reindirizza al presenter e alla vista originali. Tuttavia, se la richiesta salvata contiene l'invio di un modulo, passerà al presenter originale tramite il metodo `forward()`, passerà al modulo i valori precedentemente compilati e lo farà renderizzare nuovamente. L'utente ha così la possibilità di inviare nuovamente il modulo e nessun dato andrà perso. +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. -È importante notare che `restoreRequest()` controlla se l'utente appena loggato è lo stesso che ha compilato originariamente il modulo. In caso contrario, scarta la richiesta e non fa nulla. +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. -Mostriamo tutto con un esempio. Abbiamo un presenter `AdminPresenter`, in cui si modificano i dati e nel cui metodo `startup()` verifichiamo se l'utente è loggato. In caso contrario, lo reindirizziamo a `SignPresenter`. Allo stesso tempo, salviamo la richiesta corrente e inviamo la sua chiave a `SignPresenter`. +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 @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Il presenter `SignPresenter` conterrà, oltre al modulo di login, anche un parametro persistente `$backlink`, in cui verrà scritta la chiave. Poiché il parametro è persistente, verrà trasmesso anche dopo l'invio del modulo di login. +`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 @@ -40,14 +40,14 @@ class SignPresenter extends Nette\Application\UI\Presenter protected function createComponentSignInForm() { $form = new Nette\Application\UI\Form; - // ... aggiungiamo i campi del modulo ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + // ... aggiunta dei campi del form ... + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { - // ... qui effettuiamo il login dell'utente ... + // ... qui l'utente viene connesso ... $this->restoreRequest($this->backlink); $this->redirect('Admin:'); @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -Al metodo `restoreRequest()` passiamo la chiave della richiesta salvata e questo reindirizza (o passa) al presenter originale. +Passiamo la chiave (`$this->backlink`) della richiesta salvata al metodo `restoreRequest()`. Esso reindirizza (o inoltra) poi l'utente al presenter e alla vista originali. -Tuttavia, se la chiave non è valida (ad esempio, non esiste più nella sessione), il metodo non fa nulla. Segue quindi la chiamata `$this->redirect('Admin:')`, che reindirizza a `AdminPresenter`. +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 index eab0e604fe..3365044dcf 100644 --- a/best-practices/ja/@home.texy +++ b/best-practices/ja/@home.texy @@ -1,24 +1,25 @@ -ガイドとベストプラクティス -************* +チュートリアルとベストプラクティス +***************** .[perex] -Netteのガイド、一般的なタスクの解決策、および*ベストプラクティス*。 +チュートリアル、よくある課題の解き方、そして Nette のベストプラクティス。 <div class=documentation> <div> -Netteアプリケーション -------------- -- [inject メソッドと属性 |inject-method-attribute] -- [トレイトからの Presenter の構成 |presenter-traits] -- [Presenter への設定の受け渡し |passing-settings-to-presenters] -- [前のページに戻る方法 |restore-request] +Nette Application +----------------- +- [inject メソッドとアトリビュート |inject-method-attribute] +- [トレイトによるプレゼンターの構成 |presenter-traits] +- [プレゼンターへの設定の受け渡し |passing-settings-to-presenters] +- [リクエストを復元する方法 |restore-request] - [データベース結果のページネーション |pagination] - [動的スニペット |dynamic-snippets] -- [#Requires 属性の使用方法 |attribute-requires] -- [POST リンクの正しい使用方法 |post-links] +- [#Requires アトリビュートの使い方 |attribute-requires] +- [POST リンクの正しい使い方 |post-links] +- [スラッグを使った読みやすい URL |pretty-urls] </div> <div> @@ -27,30 +28,29 @@ Netteアプリケーション フォーム ---- - [フォームの再利用 |form-reuse] -- [レコード作成および編集用フォーム |creating-editing-form] -- [お問い合わせフォームの作成 |lets-create-contact-form] -- [依存セレクトボックス |https://blog.nette.org/en/dependent-selectboxes-elegantly-in-nette-and-pure-js] +- [レコードの作成と編集のためのフォーム |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] -- [エディタとツールのヒント |editors-and-tools] +--- +- [設定ファイルの読み込み方 |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] @@ -61,9 +61,9 @@ Netteアプリケーション <div> -ビデオ +動画 --- -Poslední soboty の何百もの録画と Nette に関するビデオは、「Nette Framework Youtube チャンネル」:https://www.youtube.com/user/NetteFramework でまとめて見つけることができます。 +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 new file mode 100644 index 0000000000..2735f42d4d --- /dev/null +++ b/best-practices/ja/@left-menu.texy @@ -0,0 +1,34 @@ +チュートリアルとベストプラクティス +***************** +- [概要 |@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 index faa40c5409..f76b18d672 100644 --- a/best-practices/ja/@meta.texy +++ b/best-practices/ja/@meta.texy @@ -1,2 +1 @@ -{{sitename: ガイドとベストプラクティス}} -{{leftbar: www:@menu-common}} +{{sitename: チュートリアルとベストプラクティス}} diff --git a/best-practices/ja/attribute-requires.texy b/best-practices/ja/attribute-requires.texy index 5e374df7a0..de9e6c340a 100644 --- a/best-practices/ja/attribute-requires.texy +++ b/best-practices/ja/attribute-requires.texy @@ -1,31 +1,31 @@ -`#[Requires]` 属性の使用方法 -********************* +`#[Requires]` アトリビュートの使い方 +************************* .[perex] -Web アプリケーションを作成していると、アプリケーションの特定の部分へのアクセスを制限する必要性にしばしば直面します。一部のリクエストはフォーム(つまり POST メソッド)を使用してのみデータを送信できるようにしたり、AJAX コールのみにアクセスできるようにしたりしたい場合があります。Nette Framework 3.2 では、このような制限を非常にエレガントかつ明確に設定できる新しいツールが登場しました。それが `#[Requires]` 属性です。 +ウェブアプリケーションを書いていると、アプリケーションの一部へのアクセスを制限したくなる場面によく出会います。あるリクエストはフォーム経由(つまり POST メソッド)でしかデータを送れないようにしたい、あるいは AJAX の呼び出しからだけアクセスできるようにしたい、といった具合です。Nette Framework 3.2 では、そうした制限をきれいに分かりやすく設定できる新しい道具が導入されました。`#[Requires]` アトリビュートです。 -属性は、クラスまたはメソッドの定義の前に追加する PHP の特別なマークです。これは実際にはクラスであるため、以下の例が機能するためには、use 句を含める必要があります。 +アトリビュートとは、クラスやメソッドの定義の前に書く PHP の特別な印です。実体はクラスなので、以下の例を動かすには `use` 句が必要です。 ```php use Nette\Application\Attributes\Requires; ``` -`#[Requires]` 属性は、Presenter クラス自体、および以下のメソッドで使用できます。 +`#[Requires]` アトリビュートは、プレゼンターのクラス自体と、次のメソッドに使えます。 - `action<Action>()` - `render<View>()` - `handle<Signal>()` - `createComponent<Name>()` -最後の 2 つのメソッドはコンポーネントにも関連するため、属性はコンポーネントでも使用できます。 +最後の 2 つはコンポーネントにも当てはまるので、コンポーネントでもこのアトリビュートを使えます。 -属性が指定する条件が満たされない場合、HTTP エラー 4xx がスローされます。 +アトリビュートが定める条件が満たされない場合、HTTP 4xx のエラーが発生します。 HTTP メソッド --------- -アクセスが許可される HTTP メソッド(GET、POST など)を指定できます。たとえば、フォームの送信によるアクセスのみを許可したい場合は、次のように設定します。 +アクセスを許す HTTP メソッド(GET、POST など)を指定できます。たとえばフォームの送信でのみアクセスを許したいなら、次のようにします。 ```php class AdminPresenter extends Nette\Application\UI\Presenter @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -状態を変更するアクションになぜ GET ではなく POST を使用すべきか、そしてその方法については? [ガイドを読む |post-links]。 +状態を変える操作になぜ GET ではなく POST を使うべきなのか、そしてどう使うのかは、[ガイドをお読みください |post-links]。 -メソッドまたはメソッドの配列を指定できます。特別なケースは値 `'*'` で、これはすべてのメソッドを許可します。これは、Presenter が通常 [セキュリティ上の理由で許可されていません |application:presenters#HTTPメソッドのチェック]。 +メソッドはひとつでも、配列でも指定できます。特別なのは値 `'*'` で、すべてのメソッドを許します。プレゼンターは[安全のため既定ではそれを許しません |application:presenters#HTTP メソッドのチェック]。 -AJAX コール --------- +AJAX の呼び出し +---------- -Presenter またはメソッドを AJAX リクエストに対してのみ利用可能にしたい場合は、次を使用します。 +プレゼンターやメソッドを AJAX のリクエストからだけアクセスできるようにしたい場合は、次のようにします。 ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ class AjaxPresenter extends Nette\Application\UI\Presenter 同一オリジン ------ -セキュリティを強化するために、リクエストが同一ドメインから行われることを要求できます。これにより、[CSRF 脆弱性 |nette:vulnerability-protection#Cross-Site Request Forgery CSRF] を防ぐことができます。 +安全性を高めるために、リクエストが同じドメインから来ることを求められます。これは [CSRF 脆弱性 |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)]を防ぎます。 ```php #[Requires(sameOrigin: true)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -`handle<Signal>()` メソッドでは、同一ドメインからのアクセスが自動的に要求されます。したがって、逆に任意のドメインからのアクセスを許可したい場合は、次のように指定します。 +`handle<Signal>()` メソッドでは、同じドメインからのアクセスが自動的に要求されます。ですから、どのドメインからのアクセスも許したい場合は次のように指定します。 ```php #[Requires(sameOrigin: false)] @@ -77,10 +77,10 @@ public function handleList(): void ``` -フォワード経由のアクセス ------------- +forward 経由のアクセス +--------------- -Presenter へのアクセスを間接的にのみ、たとえば他の Presenter から `forward()` または `switch()` メソッドを使用してのみ利用可能にするように制限すると便利な場合があります。このようにして、たとえばエラー Presenter が URL から呼び出されるのを防ぎます。 +プレゼンターへのアクセスを制限し、たとえば別のプレゼンターからの `forward()` や `switch()` メソッド経由でのみ、つまり間接的にしか使えないようにすると便利なことがあります。エラー用のプレゼンターは、URL から呼び出されないようこうして守られています。 ```php #[Requires(forward: true)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -実際には、Presenter 内のロジックに基づいて初めてアクセスできる特定のビューを指定する必要があることがよくあります。つまり、再び、直接開くことができないようにするためです。 +実際には、プレゼンターのロジックにもとづいてのみアクセスできるビューに印を付ける必要がよくあります。これも直接開けないようにするためです。 ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -114,7 +114,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter 特定のアクション -------- -特定のコード、たとえばコンポーネントの作成などを、Presenter 内の特定のアクションに対してのみ利用可能にするように制限することもできます。 +コンポーネントの生成などのコードを、プレゼンターの特定のアクションでのみ使えるように制限することもできます。 ```php class EditDeletePresenter extends Nette\Application\UI\Presenter @@ -126,15 +126,15 @@ class EditDeletePresenter extends Nette\Application\UI\Presenter } ``` -単一のアクションの場合、配列を記述する必要はありません:`#[Requires(actions: 'default')]` +アクションがひとつなら配列を書く必要はありません: `#[Requires(actions: 'default')]` -カスタム属性 ------- +カスタムアトリビュート +----------- -`#[Requires]` 属性を同じ設定で繰り返し使用したい場合は、`#[Requires]` を継承し、必要に応じて設定する独自の属性を作成できます。 +同じ設定で `#[Requires]` アトリビュートを繰り返し使いたいなら、`#[Requires]` を継承して自分の必要に合わせて設定した独自のアトリビュートを作れます。 -たとえば、`#[SingleAction]` は `default` アクション経由でのみアクセスを許可します。 +たとえば `#[SingleAction]` は `default` アクションからのアクセスだけを許します。 ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -または、`#[RestMethods]` は REST API に使用されるすべての HTTP メソッド経由でのアクセスを許可します。 +あるいは `#[RestMethods]` は、REST API で使うすべての HTTP メソッドでのアクセスを許します。 ```php #[\Attribute] @@ -174,4 +174,4 @@ class ApiPresenter extends Nette\Application\UI\Presenter まとめ --- -`#[Requires]` 属性は、Web サイトへのアクセス方法について大きな柔軟性とコントロールを提供します。シンプルでありながら強力なルールを使用して、アプリケーションのセキュリティと適切な動作を向上させることができます。ご覧のとおり、Nette で属性を使用すると、作業が容易になるだけでなく、安全にもなります。 +`#[Requires]` アトリビュートは、ウェブページへのアクセスのされ方について大きな柔軟さと制御を与えてくれます。単純ながら強力な規則によって、アプリケーションの安全性と正しい動作を高められます。ご覧のとおり、Nette でアトリビュートを使うことは作業を簡単にするだけでなく、安全にもしてくれるのです。 diff --git a/best-practices/ja/composer.texy b/best-practices/ja/composer.texy index afd0df7f16..c485a72eaa 100644 --- a/best-practices/ja/composer.texy +++ b/best-practices/ja/composer.texy @@ -1,12 +1,12 @@ -Composer: 使用のヒント +Composer: 利用のヒント **************** <div class=perex> -ComposerはPHPの依存関係管理ツールです。プロジェクトが依存するライブラリをリストアップし、それらをインストールおよび更新してくれます。以下を示します: +Composer は PHP の依存関係を管理するための道具です。プロジェクトが依存するライブラリを宣言すると、そのインストールと更新を代わりに行ってくれます。ここでは次のことを学びます。 -- Composerのインストール方法 -- 新規または既存のプロジェクトでの使用方法 +- Composer のインストール方法 +- 新しいプロジェクトや既存のプロジェクトでの使い方 </div> @@ -14,31 +14,31 @@ ComposerはPHPの依存関係管理ツールです。プロジェクトが依存 インストール ====== -Composerは実行可能な `.phar` ファイルで、次の方法でダウンロードしてインストールします: +Composer は実行できる `.phar` ファイルで、次のようにダウンロードしてインストールします。 Windows ------- -公式インストーラ [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe] を使用してください。 +公式のインストーラ [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]を使います。 -Linux, macOS ------------- +Linux、macOS +----------- -[このページ |https://getcomposer.org/download/] からコピーできる4つのコマンドを実行するだけです。 +必要なのは 4 つのコマンドだけで、[このページ |https://getcomposer.org/download/]からコピーできます。 -さらに、システムの `PATH` にあるフォルダに配置することで、Composerはグローバルにアクセス可能になります: +さらに、システムの `PATH` に含まれるフォルダにコピーすれば、Composer をどこからでも使えるようになります。 ```shell $ mv ./composer.phar ~/bin/composer # または /usr/local/bin/composer ``` -プロジェクトでの使用 +プロジェクトでの利用 ========== -プロジェクトでComposerの使用を開始するには、`composer.json` ファイルのみが必要です。これはプロジェクトの依存関係を記述し、他のメタデータも含むことができます。基本的な `composer.json` は次のようになります: +プロジェクトで Composer を使い始めるのに必要なのは `composer.json` ファイルだけです。このファイルはプロジェクトの依存関係を記述し、ほかのメタデータを含むこともあります。最も単純な `composer.json` は次のようなものです。 ```js { @@ -48,17 +48,17 @@ $ mv ./composer.phar ~/bin/composer # または /usr/local/bin/composer } ``` -ここでは、アプリケーション(またはライブラリ)が `nette/database` パッケージ(パッケージ名は組織名とプロジェクト名で構成されます)を必要とし、条件 `^3.0` に一致するバージョン(つまり、最新のバージョン3)を要求していることを示しています。 +ここでは、私たちのアプリケーション(またはライブラリ)が `nette/database` パッケージを必要とし(パッケージ名はベンダー名とプロジェクト名から成ります)、`^3.0` というバージョン制約に合う版(つまり最新のバージョン 3)を求めている、と述べています。 -プロジェクトのルートに `composer.json` ファイルがあるので、インストールを実行します: +`composer.json` ファイルをプロジェクトのルートに置いたら、次を実行します。 ```shell composer update ``` -ComposerはNette Databaseを `vendor/` フォルダにダウンロードします。さらに、どのバージョンのライブラリを正確にインストールしたかに関する情報を含む `composer.lock` ファイルを作成します。 +Composer は Nette Database を `vendor/` ディレクトリにダウンロードします。あわせて `composer.lock` ファイルも作られ、どのライブラリのどのバージョンを正確にインストールしたかの情報が入ります。 -Composerは `vendor/autoload.php` ファイルを生成します。これを単純にインクルードするだけで、追加の作業なしにライブラリの使用を開始できます: +Composer は `vendor/autoload.php` ファイルを生成します。このファイルを読み込むだけで、余計な作業なしにライブラリのクラスを使い始められます。 ```php require __DIR__ . '/vendor/autoload.php'; @@ -67,20 +67,20 @@ $db = new Nette\Database\Connection('sqlite::memory:'); ``` -パッケージを最新バージョンに更新する -================== +パッケージを最新版に更新する +============== -`composer.json` で定義された条件に従って使用されているライブラリを最新バージョンに更新するには、`composer update` コマンドを使用します。たとえば、依存関係 `"nette/database": "^3.0"` の場合、最新のバージョン3.x.xをインストールしますが、バージョン4はインストールしません。 +`composer.json` で定めた制約に従って、使っているライブラリを最新版に更新するには `composer update` コマンドを使います。たとえば依存関係が `"nette/database": "^3.0"` なら、最新の 3.x.x 版がインストールされ、バージョン 4 はインストールされません。 -`composer.json` ファイル内の条件を、たとえば `"nette/database": "^4.1"` に更新して最新バージョンをインストールできるようにするには、`composer require nette/database` コマンドを使用します。 +`composer.json` ファイルの制約自体を、たとえば `"nette/database": "^4.1"` に更新して最新版をインストールできるようにするには、`composer require nette/database` コマンドを使います。 -使用されているすべてのNetteパッケージを更新するには、コマンドラインですべてをリストする必要があります。例: +使っているすべての Nette のパッケージを更新するには、コマンドラインにそれらをすべて並べる必要があります。たとえば次のようにです。 ```shell composer require nette/application nette/forms latte/latte tracy/tracy ... ``` -これは非実用的です。代わりに、簡単なスクリプト "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff を使用してください。これはあなたのためにそれを行います: +これは実用的ではありません。そこで、代わりにやってくれる簡単なスクリプト "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff を使ってください。 ```shell php composer-frontline.php @@ -90,29 +90,29 @@ php composer-frontline.php 新しいプロジェクトの作成 ============ -Netteで新しいプロジェクトを作成するには、単一のコマンドを使用します: +新しい Nette のプロジェクトは、ひとつのコマンドで作れます。 ```shell -composer create-project nette/web-project project-name +composer create-project nette/web-project name-of-the-project ``` -`project-name` として、プロジェクトのディレクトリ名を入力して確認します。ComposerはGitHubから `nette/web-project` リポジトリをダウンロードします。これにはすでに `composer.json` ファイルが含まれており、その後すぐにNette Frameworkをダウンロードします。あとは `temp/` および `log/` フォルダへの書き込み[権限を設定する |nette:troubleshooting#ディレクトリ権限の設定] だけで、プロジェクトは稼働するはずです。 +`name-of-the-project` をプロジェクトのディレクトリ名に置き換えて実行してください。Composer が GitHub から `nette/web-project` リポジトリ(すでに `composer.json` ファイルを含んでいます)をダウンロードし、続いて Nette Framework 自体をインストールします。あとは `temp/` と `log/` ディレクトリの[権限を設定する |nette:troubleshooting#ディレクトリの権限の設定]だけで、プロジェクトが動き出すはずです。 -プロジェクトがホストされるPHPのバージョンがわかっている場合は、[それを設定する |#PHPバージョン] ことを忘れないでください。 +プロジェクトを置くホスティングの PHP のバージョンが分かっているなら、必ず[それを設定してください |#PHP のバージョン]。 -PHPバージョン -======== +PHP のバージョン +========== -Composerは常に、現在使用しているPHPのバージョン(より正確には、Composerを実行するときにコマンドラインで使用されるPHPのバージョン)と互換性のあるパッケージのバージョンをインストールします。しかし、これはおそらくホスティングで使用しているバージョンと同じではありません。したがって、ホスティング上のPHPのバージョンに関する情報を `composer.json` ファイルに追加することが非常に重要です。その後、ホスティングと互換性のあるパッケージのバージョンのみがインストールされます。 +Composer は常に、今使っている PHP のバージョン(正確には Composer を実行したコマンドラインの PHP のバージョン)に合うパッケージの版をインストールします。これはウェブホスティングが使うバージョンと同じとは限りません。ですから、ホスティングの PHP のバージョンの情報を `composer.json` ファイルに足すことが決定的に重要です。そうすれば、ホスティングに合う版のパッケージだけがインストールされます。 -プロジェクトがたとえばPHP 8.2.3で実行されることを設定するには、次のコマンドを使用します: +たとえばプロジェクトが PHP 8.2.3 で動くと指定するには、次のコマンドを使います。 ```shell composer config platform.php 8.2.3 ``` -バージョンは `composer.json` ファイルに次のように書き込まれます: +バージョンは `composer.json` ファイルに次のように書かれます。 ```js { @@ -124,13 +124,13 @@ composer config platform.php 8.2.3 } ``` -ただし、PHPのバージョン番号はファイルの別の場所、つまり `require` セクションにも記載されています。最初の番号はどのバージョン用にパッケージがインストールされるかを決定し、2番目の番号はアプリケーション自体がどのバージョン用に書かれているかを示します。そして、たとえばPhpStormはそれに基づいて *PHP language level* を設定します。(もちろん、これらのバージョンが異なることは意味がないため、二重の記述は不注意です。)このバージョンは次のコマンドで設定します: +ただし PHP のバージョン番号は、このファイルの別の場所、`require` セクションにも書かれます。最初の番号がパッケージをインストールする際のバージョンを決めるのに対し、2 つめはアプリケーション自体がどのバージョン向けに書かれているかを示します。たとえば PhpStorm はこれを使って *PHP language level* を設定します。(もちろんこの 2 つが食い違うのは意味をなさないので、二重に書くことになっているのは設計上の見落としです。)このバージョンは次のコマンドで設定します。 ```shell composer require php 8.2.3 --no-update ``` -または `composer.json` ファイルで直接: +あるいは `composer.json` ファイルに直接書きます。 ```js { @@ -141,54 +141,54 @@ composer require php 8.2.3 --no-update ``` -PHPバージョンの無視 -=========== +PHP のバージョンを無視する +=============== -パッケージは通常、互換性のある最低PHPバージョンと、テストされた最高バージョンの両方を指定しています。さらに新しいPHPバージョンを使用する予定がある場合、たとえばテスト目的で、Composerはそのようなパッケージのインストールを拒否します。解決策は `--ignore-platform-req=php+` オプションです。これにより、Composerは要求されたPHPバージョンの上限を無視します。 +パッケージはふつう、対応する最も低い PHP のバージョンと、テスト済みの最も高いバージョンの両方を指定しています。テストなどのためにそれより新しい PHP のバージョンを使うつもりなら、Composer はそのパッケージのインストールを拒みます。その答えが `--ignore-platform-req=php+` オプションで、これを使うと Composer は要求される PHP バージョンの上限を無視します。 誤った報告 ===== -パッケージのアップグレードやバージョン番号の変更時に、競合が発生することがあります。あるパッケージには、別のパッケージと矛盾する要件があるなどです。しかし、Composerは時々誤った報告を表示することがあります。実際には存在しない競合を報告します。そのような場合は、`composer.lock` ファイルを削除して再試行すると役立ちます。 +パッケージを更新したりバージョン番号を変えたりすると、ときどき衝突が起こります。あるパッケージの要求が別のパッケージと衝突する、といった具合です。ただし Composer は、実際には存在しない衝突を報告することがあります。そうした場合は `composer.lock` ファイルを削除してやり直すとうまくいくことがあります。 -エラーメッセージが続く場合は、真剣に受け止められ、何とどのように修正する必要があるかを読み取る必要があります。 +それでもエラーメッセージが残るなら、それは本物です。何をどう直すべきか、メッセージを読んで理解する必要があります。 -Packagist.org - 中央リポジトリ -======================= +Packagist.org - グローバルなリポジトリ +=========================== -[Packagist |https://packagist.org] は、Composerが他に指示されない限りパッケージを検索しようとするメインリポジトリです。独自のパッケージをここで公開することもできます。 +[Packagist |https://packagist.org]は、Composer が既定でパッケージを探す主要なリポジトリです。ここで自分のパッケージを公開することもできます。 -中央リポジトリを使用したくない場合は? -------------------- +中央のリポジトリを使いたくない場合 +----------------- -社内アプリケーションがあり、単に公開でホストできない場合は、それらのために企業リポジトリを作成します。 +社内のアプリケーションやライブラリで公開できないものがあるなら、それ用に自分たちのリポジトリを作れます。 -リポジトリに関する詳細は、[公式ドキュメント |https://getcomposer.org/doc/05-repositories.md#repositories] で確認できます。 +リポジトリについて詳しくは[公式ドキュメント |https://getcomposer.org/doc/05-repositories.md#repositories]をご覧ください。 オートローディング ========= -Composerの重要な機能は、インストールされたすべてのクラスに対してオートローディングを提供することです。これは `vendor/autoload.php` ファイルをインクルードすることで開始します。 +Composer の重要な機能は、インストールしたすべてのクラスにオートローディングを提供することです。`vendor/autoload.php` ファイルを読み込めば有効になります。 -ただし、`vendor` フォルダ外の他のクラスをロードするためにComposerを使用することも可能です。最初のオプションは、Composerに定義されたフォルダとサブフォルダを検索させ、すべてのクラスを見つけてオートローダーに含めることです。これは、`composer.json` で `autoload > classmap` を設定することで実現できます: +さらに Composer を使って、`vendor/` ディレクトリの外のクラスを読み込むこともできます。ひとつめの方法は、指定したディレクトリとそのサブディレクトリを Composer に走査させ、見つけたすべてのクラスをオートローダーに登録させることです。そのためには `composer.json` の `autoload > classmap` を設定します。 ```js { "autoload": { "classmap": [ - "src/", # src/ フォルダとそのサブフォルダを含める + "src/", # src/ ディレクトリとそのサブディレクトリを含めます ] } } ``` -その後、変更があるたびに `composer dumpautoload` コマンドを実行し、オートロードテーブルを再生成する必要があります。これは非常に不便であり、このタスクを[RobotLoader|robot-loader:]に委ねる方がはるかに優れています。RobotLoaderは同じ操作をバックグラウンドで自動的に、はるかに高速に実行します。 +その場合、変更のたびに `composer dumpautoload` コマンドを実行してオートローディングの表を作り直す必要があります。これはきわめて面倒です。この仕事は [RobotLoader|robot-loader:]に任せるほうがずっと良く、同じことを自動的に、しかもはるかに速くバックグラウンドで行ってくれます。 -2番目のオプションは、[PSR-4|https://www.php-fig.org/psr/psr-4/] に従うことです。簡単に言えば、これは名前空間とクラス名がディレクトリ構造とファイル名に対応するシステムです。たとえば、`App\Core\RouterFactory` は `/path/to/App/Core/RouterFactory.php` ファイルにあります。設定例: +ふたつめの方法は [PSR-4 |https://www.php-fig.org/psr/psr-4/]に従うことです。簡単にいえば、名前空間とクラス名がディレクトリ構造とファイル名に対応するしくみで、たとえば `App\Core\RouterFactory` は `/path/to/App/Core/RouterFactory.php` ファイルに置かれます。設定の例です。 ```js { @@ -200,13 +200,13 @@ Composerの重要な機能は、インストールされたすべてのクラス } ``` -動作を正確に設定する方法については、[Composerドキュメント|https://getcomposer.org/doc/04-schema.md#psr-4] を参照してください。 +この振る舞いの設定方法について詳しくは [Composer のドキュメント |https://getcomposer.org/doc/04-schema.md#psr-4]をご覧ください。 -新しいバージョンのテスト -============ +新しいバージョンを試す +=========== -パッケージの新しい開発バージョンをテストしたいですか?どうすればよいでしょうか?まず、`composer.json` ファイルに次の2つのオプションを追加します。これにより、開発バージョンのパッケージをインストールできますが、要件を満たす安定バージョンの組み合わせが存在しない場合にのみ使用されます: +パッケージの新しい開発版を試したいですか。やり方を説明します。まず `composer.json` ファイルに次の 2 つのオプションを足します。これで開発版をインストールできるようになりますが、安定版の組み合わせで要求を満たせない場合にだけ Composer は開発版に頼ります。 ```js { @@ -215,21 +215,21 @@ Composerの重要な機能は、インストールされたすべてのクラス } ``` -さらに、`composer.lock` ファイルを削除することをお勧めします。Composerが理解できない理由でインストールを拒否することがあり、これが問題を解決します。 +あわせて `composer.lock` ファイルの削除もおすすめします。Composer が理由もはっきりしないままインストールを拒むことがあり、これで解決することがあるからです。 -パッケージが `nette/utils` で、新しいバージョンが4.0だとしましょう。次のコマンドでインストールします: +パッケージが `nette/utils` で、新しいバージョンが 4.0 だとしましょう。次のコマンドでインストールします。 ```shell composer require nette/utils:4.0.x-dev ``` -または、特定のバージョン、たとえば4.0.0-RC2をインストールできます: +あるいは特定のバージョン、たとえば 4.0.0-RC2 をインストールすることもできます。 ```shell composer require nette/utils:4.0.0-RC2 ``` -しかし、ライブラリが古いバージョン(例:`^3.1`)にロックされている別のパッケージに依存している場合、理想的にはパッケージを更新して新しいバージョンで動作するようにすることです。ただし、制限を回避してComposerに開発バージョンをインストールさせ、それが古いバージョン(例:3.1.6)であるかのように見せかけたい場合は、キーワード `as` を使用できます: +ただし別のパッケージがそのライブラリに依存していて、古いバージョン(たとえば `^3.1`)に固定されている場合、理想的な解はその依存元のパッケージを新しいバージョンで動くよう更新することです。とはいえ、制限を回避して、開発版を古いバージョン(たとえば 3.1.6)のふりをさせたまま Composer にインストールさせたいだけなら、`as` キーワードを使えます。 ```shell composer require nette/utils "4.0.x-dev as 3.1.6" @@ -239,9 +239,9 @@ composer require nette/utils "4.0.x-dev as 3.1.6" コマンドの呼び出し ========= -Composerを介して、Composerのネイティブコマンドであるかのように、独自の事前に準備されたコマンドやスクリプトを呼び出すことができます。`vendor/bin` フォルダにあるスクリプトの場合、このフォルダを指定する必要はありません。 +自分で定義したコマンドやスクリプトを、Composer 本来のコマンドであるかのように Composer 経由で呼び出せます。`vendor/bin` ディレクトリにあるスクリプトなら、そのパスを指定する必要もありません。 -例として、`composer.json` ファイルに、[Nette Tester|tester:] を使用してテストを実行するスクリプトを定義します: +例として、[Nette Tester |tester:]でテストを実行するスクリプトを `composer.json` に定義してみましょう。 ```js { @@ -251,31 +251,31 @@ Composerを介して、Composerのネイティブコマンドであるかのよ } ``` -次に、`composer tester` を使用してテストを実行します。プロジェクトのルートフォルダにいなくても、サブディレクトリのいずれかにいる場合でもコマンドを呼び出すことができます。 +あとは `composer tester` でテストを実行します。プロジェクトのルートディレクトリでなく、そのサブディレクトリにいてもこのコマンドを呼べます。 -感謝を送る -===== +感謝を伝える +====== -オープンソースの作者を喜ばせるトリックをお見せします。簡単な方法で、プロジェクトが使用しているライブラリにGitHubでスターを付けることができます。`symfony/thanks` ライブラリをインストールするだけです: +オープンソースの作者を喜ばせる小技を紹介します。プロジェクトが使っているライブラリに、GitHub で簡単にスターを付けられます。`symfony/thanks` ライブラリをインストールするだけです。 ```shell composer global require symfony/thanks ``` -そして実行します: +そして次を実行します。 ```shell composer thanks ``` -試してみてください! +試してみてください。 設定 -===== +=== -Composerはバージョン管理ツール [Git |https://git-scm.com] と密接に連携しています。インストールされていない場合は、Composerに使用しないように指示する必要があります: +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 index 806cea4a84..1a3c0827f7 100644 --- a/best-practices/ja/creating-editing-form.texy +++ b/best-practices/ja/creating-editing-form.texy @@ -2,15 +2,15 @@ ****************** .[perex] -Netteでレコードの追加と編集を正しく実装する方法は?両方に同じフォームを使用します。 +Nette でレコードの追加と編集を、同じフォームを使って正しく実装するにはどうすればよいでしょうか。 -多くの場合、レコードの追加と編集のためのフォームは同じであり、ボタンのラベルなどが異なるだけです。まずレコードを追加するためにフォームを使用し、次に編集のために使用し、最後に両方の解決策を組み合わせる簡単なPresenterの例を示します。 +多くの場合、レコードの追加と編集のフォームは同一で、違うのはボタンのラベルくらいです。ここでは簡単なプレゼンターの例を通して、まずフォームをレコードの追加に使い、次に編集に使い、最後にその 2 つをひとつにまとめます。 レコードの追加 ------- -レコードを追加するためのPresenterの例です。データベース自体の操作は`Facade`クラスに任せます。そのコードはこの例では重要ではありません。 +レコードを追加するプレゼンターの例です。実際のデータベースとのやり取りは `Facade` クラスに任せます。そのコードはこの例の本筋ではありません。 ```php @@ -27,16 +27,16 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $form = new Form; - // ... フォームコントロールを追加 ... + // ... フォームの項目を追加します ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // データベースへのレコード追加 - $this->flashMessage('正常に追加されました'); + $this->facade->add($data); // レコードをデータベースに追加します + $this->flashMessage('Successfully added'); $this->redirect('...'); } @@ -51,7 +51,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter レコードの編集 ------- -次に、レコードを編集するためのPresenterがどのようになるかを示します。 +次に、レコードを編集するプレゼンターがどうなるかを見てみましょう。 ```php @@ -70,10 +70,10 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // レコードの存在確認 - || !$this->facade->isEditAllowed(/*...*/) // 権限チェック + !$record // レコードの存在を確かめます + || !$this->facade->isEditAllowed(/*...*/) // 権限を確かめます ) { - $this->error(); // エラー 404 + $this->error(); // 404 エラー } $this->record = $record; @@ -81,32 +81,32 @@ class RecordPresenter extends Nette\Application\UI\Presenter protected function createComponentRecordForm(): Form { - // アクションが'edit'であることを確認 + // アクションが 'edit' であることを確かめます if ($this->getAction() !== 'edit') { $this->error(); } $form = new Form; - // ... フォームコントロールを追加 ... + // ... フォームの項目を追加します ... - $form->setDefaults($this->record); // デフォルト値の設定 - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->setDefaults($this->record); // 既定値を設定します + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->update($this->record->id, $data); // レコードの更新 - $this->flashMessage('正常に更新されました'); + $this->facade->update($this->record->id, $data); // レコードを更新します + $this->flashMessage('Successfully updated'); $this->redirect('...'); } } ``` -[presenterのライフサイクル |application:presenters#Presenterのライフサイクル] の最初に実行される *action* メソッドで、レコードの存在とユーザーがそれを編集する権限を確認します。 +[プレゼンターのライフサイクル |application:presenters#プレゼンターのライフサイクル]の初めに呼ばれる *action* メソッドで、レコードの存在とユーザーの編集権限を確かめます。 -レコードをプロパティ `$record` に保存して、デフォルト値を設定するために `createComponentRecordForm()` メソッドで、そしてIDのために `recordFormSucceeded()` で利用できるようにします。代替の解決策は、デフォルト値を直接 `actionEdit()` で設定し、URLの一部であるIDの値を `getParameter('id')` を使用して取得することです。 +レコードは `$record` プロパティに保存し、既定値の設定のために `createComponentRecordForm()` メソッドから、ID にアクセスするために `recordFormSucceeded()` から使えるようにしています。別の方法として、既定値を `actionEdit()` で直接設定し、ID の値(URL の一部)を `getParameter('id')` で取得することもできます。 ```php @@ -114,12 +114,12 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - // 存在確認と権限チェック + // 存在の確認と権限のチェック ) { $this->error(); } - // フォームのデフォルト値設定 + // フォームの既定値を設定します $this->getComponent('recordForm') ->setDefaults($record); } @@ -130,16 +130,15 @@ class RecordPresenter extends Nette\Application\UI\Presenter $this->facade->update($id, $data); // ... } -} ``` -しかし、そしてこれが **コード全体の最も重要なポイント** であるべきですが、フォームを作成する際には、アクションが実際に `edit` であることを確認する必要があります。そうでなければ、`actionEdit()` メソッドでの検証はまったく行われません! +ただし、そして**これがこのコード全体で最も大切な学び**ですが、フォームを作るときにアクションが本当に `edit` であることを確かめなければなりません。さもないと、`actionEdit()` メソッドでの確認がまったく行われないからです。 -追加と編集のための同じフォーム +追加と編集で同じフォームを使う --------------- -そして今、両方のPresenterを1つに結合します。`createComponentRecordForm()` メソッドでどのアクションかを区別し、それに応じてフォームを設定することもできますし、それを直接actionメソッドに任せて条件をなくすこともできます。 +では 2 つのプレゼンターをひとつにまとめましょう。`createComponentRecordForm()` メソッドの中でアクションを見分けてフォームを設定してもよいですし、それをアクションのメソッドに任せて条件のチェックをなくすこともできます。 ```php @@ -153,50 +152,50 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // レコードの存在確認 - || !$this->facade->isEditAllowed(/*...*/) // 権限チェック + !$record // レコードの存在を確かめます + || !$this->facade->isEditAllowed(/*...*/) // 権限を確かめます ) { - $this->error(); // エラー 404 + $this->error(); // 404 エラー } $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // デフォルト値の設定 - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->setDefaults($record); // 既定値を設定します + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form { - // アクションが'add'または'edit'であることを確認 + // アクションが 'add' か 'edit' であることを確かめます if (!in_array($this->getAction(), ['add', 'edit'])) { $this->error(); } $form = new Form; - // ... フォームコントロールを追加 ... + // ... フォームの項目を追加します ... return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // データベースへのレコード追加 - $this->flashMessage('正常に追加されました'); + $this->facade->add($data); // レコードをデータベースに追加します + $this->flashMessage('Successfully added'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // レコードの更新 - $this->flashMessage('正常に更新されました'); + $this->facade->update($id, $data); // レコードを更新します + $this->flashMessage('Successfully updated'); $this->redirect('...'); } } diff --git a/best-practices/ja/dynamic-snippets.texy b/best-practices/ja/dynamic-snippets.texy index eae6f47e34..b6ed42e44c 100644 --- a/best-practices/ja/dynamic-snippets.texy +++ b/best-practices/ja/dynamic-snippets.texy @@ -1,7 +1,10 @@ 動的スニペット ******* -アプリケーション開発において、テーブルの個々の行やリストの項目などに対してAJAX操作を実行する必要性がしばしば生じます。例として、記事のリストを表示し、ログインしたユーザーが各記事に対して「いいね/いいねしない」の評価を選択できるようにします。AJAXなしのPresenterと対応するテンプレートのコードは、おおよそ次のようになります(最も重要な部分を示します。コードは評価を記録し、記事のコレクションを取得するためのサービスの存在を前提としています - 具体的な実装はこのチュートリアルの目的には重要ではありません): +.[perex] +Latte の動的スニペットを使い、AJAX でページのうち実際に変わる部分だけ、たとえばリストの個々の項目だけを更新する方法です。 + +アプリケーションを開発していると、テーブルの行やリストの項目ごとに AJAX の操作を行いたくなることがよくあります。例として、記事の一覧を考えてみましょう。ログインしたユーザーは各記事に「いいね」や「よくない」の評価を付けられます。AJAX なしのプレゼンターのコードと対応するテンプレートは、次のようなものになります(関係の深い部分だけを示します。評価の処理と記事の取得を担うサービスがあると仮定しますが、その具体的な実装はこのガイドの本筋ではありません)。 ```php public function handleLike(int $articleId): void @@ -17,7 +20,7 @@ public function handleUnlike(int $articleId): void } ``` -テンプレート: +テンプレート: ```latte <article n:foreach="$articles as $article"> @@ -32,18 +35,18 @@ public function handleUnlike(int $articleId): void ``` -Ajax化 -===== +AJAX 化 +====== -では、この簡単なアプリケーションにAJAXを追加しましょう。記事の評価の変更はリダイレクトが必要なほど重要ではないため、理想的にはバックグラウンドでAJAXで行われるべきです。[アドオンのハンドリングスクリプト |application:ajax#Naja] を使用し、AJAXリンクにはCSSクラス `ajax` を付けるという通常の慣習に従います。 +では、この簡単なアプリケーションに AJAX の機能を足しましょう。記事の評価を変えることはページ全体のリダイレクトを要するほど重大ではないので、理想的にはバックグラウンドの AJAX で済ませたいところです。AJAX のリンクに CSS クラス `ajax` を付けるという一般的な決まりに従って、[アドオンのハンドラースクリプト |application:ajax#Naja]を使います。 -しかし、具体的にはどのようにすればよいでしょうか?Netteは2つの方法を提供します:いわゆる動的スニペットの方法とコンポーネントの方法です。どちらにも長所と短所があるため、それぞれを順番に見ていきます。 +しかし具体的にはどう実装すればよいでしょうか。Nette には 2 つのやり方があります。動的スニペットと、コンポーネントです。それぞれ長所と短所があるので、両方を見ていきましょう。 -動的スニペットの方法 -========== +動的スニペットのやり方 +=========== -動的スニペットとは、Latteの用語では、スニペット名に変数を使用する `{snippet}` タグの特定のユースケースを意味します。このようなスニペットはテンプレートのどこにでも配置できるわけではありません - 静的スニペット、つまり通常の、または `{snippetArea}` 内で囲まれている必要があります。私たちのテンプレートを次のように変更できます。 +Latte の用語でいう動的スニペットとは、スニペットの名前に変数を使う `{snippet}` タグの特別な使い方を指します。そうしたスニペットはテンプレートのどこにでも置けるわけではなく、静的な(通常の)スニペットで包むか、`{snippetArea}` の中に置く必要があります。テンプレートは次のように書き換えられます。 ```latte @@ -62,9 +65,9 @@ Ajax化 {/snippet} ``` -各記事は、記事IDを名前に含むスニペットを定義します。これらすべてのスニペットは、`articlesContainer` という名前の1つのスニペットでまとめてラップされます。このラッピングスニペットを省略すると、Latteは例外で警告します。 +各記事が、記事の ID を名前に含むスニペットを定義するようになりました。これらの動的スニペットはすべて、`articlesContainer` という名前の静的スニペットでまとめて包まれています。この外側のスニペットを省くと、Latte は例外を投げます。 -残っているのは、Presenterに再描画を追加することです - 静的なラッパーを再描画するだけで十分です。 +あとはプレゼンターに再描画のロジックを足すだけです。静的な包みを再描画すればよいのです。 ```php public function handleLike(int $articleId): void @@ -72,18 +75,18 @@ public function handleLike(int $articleId): void $this->ratingService->saveLike($articleId, $this->user->id); if ($this->isAjax()) { $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- 不要 + // $this->redrawControl('article-' . $articleId); -- 不要です } else { $this->redirect('this'); } } ``` -同様に、姉妹メソッド `handleUnlike()` も変更すれば、AJAXは機能します! +対応する `handleUnlike()` メソッドも同じように書き換えれば、AJAX が動きます。 -しかし、この解決策には1つの欠点があります。AJAXリクエストがどのように進行するかをさらに調査すると、アプリケーションは表面的には効率的に見える(特定の記事に対して1つのスニペットのみを返す)ものの、実際にはサーバー上で全てのスニペットを描画していることがわかります。目的のスニペットをペイロードに配置し、他のスニペットは破棄しました(したがって、それらもデータベースから不必要に取得しました)。 +ただし、この解には欠点があります。AJAX のリクエストをよく調べると、外からは効率よく見えても(特定の記事のスニペットひとつしか返しません)、サーバー側では実際に*すべての*スニペットを描いていることが分かります。必要なスニペットをペイロードに入れ、ほかは捨てているのです(つまり無駄に取得して描いています)。 -このプロセスを最適化するには、テンプレートに `$articles` コレクションを渡す場所(例えば `renderDefault()` メソッド内)に介入する必要があります。シグナルの処理が `render<Something>` メソッドの前に行われるという事実を利用します。 +これを最適化するには、`$articles` のコレクションがテンプレートに渡される場所(たとえば `renderDefault()` メソッド)に手を入れる必要があります。シグナルの処理が `render<Something>` メソッドより前に起こることを利用します。 ```php public function handleLike(int $articleId): void @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -これで、シグナルの処理中に、すべての記事を含むコレクションの代わりに、描画してペイロードでブラウザに送信したい1つの記事のみを含む配列がテンプレートに渡されます。したがって、`{foreach}` は一度だけ実行され、余分なスニペットは描画されません。 +これでシグナルの処理中は、記事のコレクション全体ではなく、該当する記事ひとつだけを含む配列がテンプレートに渡されます。描いてペイロードでブラウザに送ろうとしている、まさにその記事です。おかげで `{foreach}` ループは一度しか回らず、不要なスニペットは描かれません。 -コンポーネントの方法 -========== +コンポーネントのやり方 +=========== -全く異なる解決方法は、動的スニペットを回避します。トリックは、ロジック全体を特別なコンポーネントに移すことです - これからは、評価の入力はPresenterではなく、専用の `LikeControl` が担当します。クラスは次のようになります(それに加えて、`render`、`handleUnlike` などのメソッドも含まれます): +まったく別のやり方として、動的スニペットをそもそも使わない方法があります。仕掛けは、ロジック全体を独立したコンポーネントにまとめてしまうことです。プレゼンターが評価を扱う代わりに、専用の `LikeControl` がそれを担当します。クラスは次のようになります(`render`、`handleUnlike` などのメソッドも含みます)。 ```php class LikeControl extends Nette\Application\UI\Control @@ -134,7 +137,7 @@ class LikeControl extends Nette\Application\UI\Control } ``` -コンポーネントのテンプレート: +コンポーネントのテンプレート: ```latte {snippet} @@ -146,7 +149,7 @@ class LikeControl extends Nette\Application\UI\Control {/snippet} ``` -もちろん、ビューテンプレートが変更され、Presenterにファクトリを追加する必要があります。データベースから取得する記事の数だけコンポーネントを作成するため、その「増殖」には [Multiplier |application:Multiplier] クラスを使用します。 +当然ながらビューのテンプレートも変わり、プレゼンターにファクトリを足す必要があります。データベースから取得した記事ごとにこのコンポーネントのインスタンスを作るので、その生成を管理するために [Multiplier |application:Multiplier]クラスを使います。 ```php protected function createComponentLikeControl() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -ビューテンプレートは必要最小限に縮小されます(そして完全にスニペットなし!): +ビューのテンプレートは最小限にまで縮み、しかもスニペットが完全になくなります。 ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ protected function createComponentLikeControl() </article> ``` -ほぼ完了です:アプリケーションはこれでAJAXで動作します。ここでもアプリケーションを最適化する必要があります。なぜなら、Nette Databaseを使用しているため、シグナルの処理中にデータベースから1つではなく、すべての記事が不必要にロードされるからです。しかし、利点は、実際に私たちのコンポーネントだけがレンダリングされるため、それらの描画が行われないことです。 +これでほぼ完成です。アプリケーションは AJAX で動くようになります。ここでも最適化は必要で、Nette Database を使っているためにシグナルの処理が該当する記事だけでなくすべての記事を無駄に読み込んでしまいます。ただし利点として、描画されるのは特定のコンポーネントのインスタンスだけなので、無駄な描画は起こりません。 {{priority: -1}} diff --git a/best-practices/ja/editors-and-tools.texy b/best-practices/ja/editors-and-tools.texy deleted file mode 100644 index 36e72f6ea2..0000000000 --- a/best-practices/ja/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -エディタとツール -******** - -.[perex] -あなたは熟練したプログラマかもしれませんが、優れたツールがあってこそマスターになれます。この章では、重要なツール、エディタ、プラグインのヒントを紹介します。 - - -IDEエディタ -======= - -開発には、PhpStorm、NetBeans、VS Codeなどの本格的なIDEを使用することを強くお勧めします。単なるPHPサポート付きのテキストエディタだけではありません。違いは本当に決定的です。構文を色付けできるだけの単なるエディタで満足する理由はありません。それは、正確なヒントを提供し、エラーを監視し、コードをリファクタリングし、その他多くのことができるトップクラスのIDEの能力には及びません。一部のIDEは有料ですが、無料のものもあります。 - -**NetBeans IDE** は、Nette、Latte、NEONのサポートを組み込みで持っています。 - -**PhpStorm**: `Settings > Plugins > Marketplace` でこれらのプラグインをインストールしてください -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: マーケットプレイスで "Nette Latte + Neon" プラグインを見つけてください。 - -また、Tracyをエディタと連携させてください。エラーページが表示されたときに、ファイル名をクリックすると、エディタで該当する行にカーソルがある状態で開くことができます。[システムの設定方法|tracy:open-files-in-ide] を読んでください。 - - -PHPStan -======= - -PHPStanは、コードを実行する前に論理エラーを検出するツールです。 - -Composerを使用してインストールします: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -プロジェクトに設定ファイル `phpstan.neon` を作成します: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -そして、`app/` フォルダ内のクラスを分析させます: - -```shell -vendor/bin/phpstan analyse app -``` - -包括的なドキュメントは、[PHPStanのサイト |https://phpstan.org] で直接見つけることができます。 - - -Code Checker -============ - -[Code Checker|code-checker:] は、ソースコード内の一部の形式的なエラーをチェックし、場合によっては修正します: - -- [BOM |nette:glossary#BOM] を削除します -- [Latte |latte:] テンプレートの有効性をチェックします -- `.neon`、`.php`、`.json` ファイルの有効性をチェックします -- [制御文字 |nette:glossary#制御文字] の出現をチェックします -- ファイルがUTF-8でエンコードされているかチェックします -- 誤って書かれた `/* @anotace */` (アスタリスクが欠けている)をチェックします -- PHPファイルの終了タグ `?>` を削除します -- ファイルの末尾にある右側のスペースや不要な行を削除します -- `-l` オプションを指定した場合、行区切り文字をシステムのものに正規化します - - -Composer -======== - -[Composer |best-practices:composer] はPHPの依存関係管理ツールです。これにより、個々のライブラリの任意の複雑な依存関係を宣言し、それらをプロジェクトにインストールすることができます。 - - -Requirements Checker -==================== - -これは、サーバーの実行環境をテストし、フレームワークを使用できるかどうか(およびどの程度まで)を通知するツールでした。現在、Netteは最小限必要なPHPバージョンを持つすべてのサーバーで使用できます。 diff --git a/best-practices/ja/form-reuse.texy b/best-practices/ja/form-reuse.texy index ed1fa5507b..cc72357358 100644 --- a/best-practices/ja/form-reuse.texy +++ b/best-practices/ja/form-reuse.texy @@ -2,15 +2,15 @@ *************** .[perex] -Netteでは、コードを複製することなく、同じフォームを複数の場所で使用するためのいくつかのオプションがあります。この記事では、避けるべきものも含め、さまざまな解決策を紹介します。 +Nette には、同じフォームを複数の場所でコードを重複させずに再利用する方法がいくつかあります。この記事では、避けるべきものも含めてさまざまな解を扱います。 -フォームファクトリ -========= +フォームのファクトリ +========== -同じコンポーネントを複数の場所で使用するための基本的なアプローチの1つは、このコンポーネントを生成するメソッドまたはクラスを作成し、その後、アプリケーションのさまざまな場所でこのメソッドを呼び出すことです。このようなメソッドまたはクラスは *ファクトリ* と呼ばれます。ファクトリの特定の利用方法を説明するデザインパターン *factory method* と混同しないでください。これはこのトピックとは関係ありません。 +コンポーネントを複数の場所で再利用するための基本的なやり方は、そのコンポーネントを作るメソッドやクラスを用意することです。そのメソッドをアプリケーションのさまざまな場所から呼びます。こうしたメソッドやクラスを*ファクトリ*と呼びます。これを *factory method* デザインパターンと混同しないでください。あちらはファクトリの特定の使い方を述べたもので、この話題とは直接の関係がありません。 -例として、編集フォームを組み立てるファクトリを作成します。 +例として、編集フォームを組み立てるファクトリを作ってみましょう。 ```php use Nette\Application\UI\Form; @@ -21,21 +21,21 @@ class FormFactory { $form = new Form; $form->addText('title', 'タイトル:'); - // ここに他のフォームフィールドを追加します - $form->addSubmit('send', '送信'); + // ここにほかのフォーム項目を足します + $form->addSubmit('send', '保存'); return $form; } } ``` -これで、アプリケーションのさまざまな場所、たとえばPresenterやコンポーネントで、このファクトリを使用できます。それは、[依存関係として要求する|dependency-injection:passing-dependencies] ことによって行います。まず、クラスを設定ファイルに記述します。 +これでこのファクトリを、プレゼンターやコンポーネントなどアプリケーションのさまざまな場所で使えます。[依存関係として要求する |dependency-injection:passing-dependencies]ことでそうします。まず設定ファイルにクラスを登録します。 ```neon services: - FormFactory ``` -そして、Presenterで使用します。 +そしてプレゼンターで使います。 ```php @@ -57,7 +57,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -フォームファクトリを、アプリケーションのニーズに応じて他の種類のフォームを作成するための追加メソッドで拡張できます。そしてもちろん、要素のない基本フォームを作成するメソッドを追加し、他のメソッドがそれを利用することもできます。 +アプリケーションの必要に応じて、ほかの種類のフォームを作るメソッドをファクトリに足していけます。当然ながら、要素のない基本のフォームを作るメソッドを足して、ほかのメソッドからそれを使うこともできます。 ```php class FormFactory @@ -72,20 +72,20 @@ class FormFactory { $form = $this->createForm(); $form->addText('title', 'タイトル:'); - // ここに他のフォームフィールドを追加します - $form->addSubmit('send', '送信'); + // ここにほかのフォーム項目を足します + $form->addSubmit('send', '保存'); return $form; } } ``` -`createForm()` メソッドはまだ何も有用なことをしていませんが、それはすぐに変わります。 +`createForm()` メソッドはまだあまり役に立ちませんが、それはすぐに変わります。 ファクトリの依存関係 ========== -やがて、フォームが多言語対応である必要があることがわかります。これは、すべてのフォームにいわゆる [translator |forms:rendering#翻訳] を設定する必要があることを意味します。この目的のために、`FormFactory` クラスを変更して、コンストラクタで `Translator` オブジェクトを依存関係として受け入れ、それをフォームに渡します。 +やがてフォームを多言語対応にする必要が出てくるかもしれません。つまり、すべてのフォームに[トランスレーター |forms:rendering#翻訳]を設定するということです。そのためには `FormFactory` クラスを書き換え、コンストラクタで `Translator` オブジェクトを依存関係として受け取り、作ったフォームに渡すようにします。 ```php use Nette\Localization\Translator; @@ -108,13 +108,13 @@ class FormFactory } ``` -`createForm()` メソッドは特定のフォームを作成する他のメソッドからも呼び出されるため、translatorを設定するのはそのメソッドだけで十分です。そして完了です。Presenterやコンポーネントのコードを変更する必要はありません。これは素晴らしいことです。 +`createForm()` メソッドは個別のフォームを作るほかのメソッドからも呼ばれるので、ここでトランスレーターを設定すれば十分です。これで完了です。プレゼンターやコンポーネントのコードを直す必要はまったくなく、素晴らしいことです。 -複数のファクトリクラス -=========== +ファクトリのクラスを増やす +============= -あるいは、アプリケーションで使用したい各フォームに対して複数のクラスを作成することもできます。 このアプローチは、コードの可読性を向上させ、フォームの管理を容易にすることができます。元の `FormFactory` は、基本的な設定(たとえば翻訳サポート付き)を持つクリーンなフォームのみを作成するようにし、編集フォーム用に新しいファクトリ `EditFormFactory` を作成します。 +別の方法として、アプリケーションで使うフォームごとに独立したファクトリのクラスを作ることもできます。こうするとコードが読みやすくなり、フォームの管理も簡単になります。もとの `FormFactory` には(翻訳のサポートなど)基本的な設定を持つ素のフォームだけを作らせ、編集フォーム専用の新しいファクトリ `EditFormFactory` を作りましょう。 ```php class FormFactory @@ -133,7 +133,7 @@ class FormFactory } -// ✅ コンポジションの使用 +// ✅ コンポジションを使う class EditFormFactory { public function __construct( @@ -144,39 +144,39 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - // ここに他のフォームフィールドを追加します - $form->addSubmit('send', '送信'); + // ここにほかのフォーム項目を足します + $form->addSubmit('send', '保存'); return $form; } } ``` -`FormFactory` と `EditFormFactory` クラス間の関連付けが、[オブジェクト継承 |nette:introduction-to-object-oriented-programming#コンポジション] ではなく [コンポジション |nette:introduction-to-object-oriented-programming#継承] によって実現されることが非常に重要です。 +大事なのは、`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', '送信'); + // ここにほかのフォーム項目を足します + $form->addSubmit('send', '保存'); return $form; } } ``` -この場合に継承を使用することは、完全に逆効果になります。問題は非常に早く発生します。たとえば、`create()` メソッドにパラメータを追加したいと思ったとき、PHPはそのシグネチャが親のものと異なるとエラーを報告します。 または、コンストラクタを介して `EditFormFactory` クラスに依存関係を渡す場合。 [コンストラクタ地獄 |dependency-injection:passing-dependencies#コンストラクタ地獄] と呼ばれる状況が発生します。 +ここで継承を使うのはまったく逆効果です。すぐに問題にぶつかります。たとえば `create()` メソッドにパラメータを足したくなったら、シグネチャが親と違うので PHP がエラーを報告します。あるいは `EditFormFactory` クラスにコンストラクタで依存関係を渡す場合。これは[コンストラクタ地獄 |dependency-injection:passing-dependencies#コンストラクタ地獄]として知られる状態を招きます。 -一般的に、[継承よりもコンポジションを |dependency-injection:faq#なぜ継承よりもコンポジションが優先されるのですか] 優先する方が良いです。 +一般に、[継承よりコンポジションを優先する |dependency-injection:faq#なぜ継承よりコンポジションが好まれるのですか?]ほうが良いのです。 -フォームハンドラ -======== +フォームの処理 +======= -正常に送信された後に呼び出されるフォームハンドラも、ファクトリクラスの一部にすることができます。送信されたデータを処理のためにモデルに渡すように機能します。潜在的なエラーは、フォームに [返します |forms:validation#処理中のエラー] 。次の例のモデルは、`Facade` クラスによって表されます。 +送信が成功したときに呼ばれるフォームのハンドラも、ファクトリのクラスの一部にできます。ハンドラは送られたデータをモデル層に渡して処理させます。処理中のエラーはフォームに[戻されます |forms:validation#エラーの処理]。次の例では、モデルを `Facade` クラスが表しています。 ```php class EditFormFactory @@ -191,13 +191,13 @@ class EditFormFactory { $form = $this->formFactory->create(); $form->addText('title', 'タイトル:'); - // ここに他のフォームフィールドを追加します - $form->addSubmit('send', '送信'); - $form->onSuccess[] = [$this, 'processForm']; + // ここにほかのフォーム項目を足します + $form->addSubmit('send', '保存'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // 送信されたデータの処理 @@ -210,7 +210,7 @@ class EditFormFactory } ``` -ただし、リダイレクト自体はPresenterに任せます。Presenterは `onSuccess` イベントにリダイレクトを実行する別のハンドラを追加します。これにより、フォームを異なるPresenterで使用し、それぞれで異なる場所にリダイレクトすることが可能になります。 +ただしリダイレクトはプレゼンター自身に任せましょう。プレゼンターは `onSuccess` イベントにもうひとつハンドラを足し、そこでリダイレクトを行います。おかげで同じフォームをさまざまなプレゼンターで使え、成功時にはそれぞれ別の場所へリダイレクトできます。 ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -224,7 +224,7 @@ class MyPresenter extends Nette\Application\UI\Presenter { $form = $this->formFactory->create(); $form->onSuccess[] = function () { - $this->flashMessage('レコードが保存されました'); + $this->flashMessage('レコードを保存しました'); $this->redirect('Homepage:'); }; return $form; @@ -232,38 +232,38 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -この解決策は、フォームまたはその要素に対して `addError()` が呼び出されると、次の `onSuccess` ハンドラが呼び出されないというフォームのプロパティを利用します。 +この解は、フォームやその要素で `addError()` が呼ばれると、以降の `onSuccess` ハンドラが呼ばれないというフォームの性質を利用しています。 -Formクラスからの継承 -============ +Form クラスの継承 +=========== -組み立てられたフォームは、フォームの子孫であってはなりません。言い換えれば、この解決策を使用しないでください。 +組み立てたフォームは `Form` クラスの子孫であるべきではありません。言い換えると、次のやり方は避けてください。 ```php -// ⛔ これはダメ!継承はここには属しません +// ⛔ だめです! ここに継承は要りません class EditForm extends Form { public function __construct(Translator $translator) { parent::__construct(); - $this->addText('title', 'タイトル:'); // $form-> を $this-> に変更 - // ここに他のフォームフィールドを追加します - $this->addSubmit('send', '送信'); // $form-> を $this-> に変更 - $this->setTranslator($translator); // $form-> を $this-> に変更 + $this->addText('title', 'タイトル:'); + // ここにほかのフォーム項目を足します + $this->addSubmit('send', '保存'); + $this->setTranslator($translator); } } ``` -コンストラクタでフォームを組み立てる代わりに、ファクトリを使用してください。 +コンストラクタの中でフォームを組み立てる代わりに、ファクトリを使ってください。 -`Form` クラスは、主にフォームを組み立てるためのツール、つまり *フォームビルダー* であることを理解する必要があります。そして、組み立てられたフォームはその製品と見なすことができます。しかし、製品はビルダーの特定のケースではなく、それらの間には継承の基礎を形成する *is a* 関係はありません。 +大事なのは、`Form` クラスが第一にフォームを組み立てるための道具、つまり *form builder* であると認識することです。組み立てられたフォームは、その成果物と見なせます。しかし成果物は builder の一種ではありません。両者のあいだに、継承の土台となる *is a* の関係はないのです。 -フォームを持つコンポーネント -============== +フォームのコンポーネント +============ -まったく異なるアプローチは、フォームを含む [コンポーネント|application:components] の作成を表します。これにより、たとえば、コンポーネントにテンプレートも含まれているため、フォームを特定の方法でレンダリングするなど、新しい可能性が生まれます。 または、AJAX通信や、たとえばオートコンプリートなどのフォームへの情報の遅延読み込みにシグナルを利用できます。 +まったく別の方法として、フォームを包む[コンポーネント |application:components]を作ることもできます。これは新しい可能性を開きます。コンポーネントは自分のテンプレートを持つので、たとえばフォームを特定の形で描けます。あるいはシグナルを使って AJAX で通信し、候補の表示などのために情報を動的にフォームに読み込むこともできます。 ```php @@ -282,14 +282,14 @@ class EditControl extends Nette\Application\UI\Control { $form = new Form; $form->addText('title', 'タイトル:'); - // ここに他のフォームフィールドを追加します - $form->addSubmit('send', '送信'); - $form->onSuccess[] = [$this, 'processForm']; + // ここにほかのフォーム項目を足します + $form->addSubmit('send', '保存'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // 送信されたデータの処理 @@ -306,7 +306,7 @@ class EditControl extends Nette\Application\UI\Control } ``` -このコンポーネントを生成するファクトリも作成します。[そのインターフェースを記述する |application:components#依存関係を持つコンポーネント] だけで十分です。 +次に、このコンポーネントを作るファクトリを用意します。[インターフェースを定義する |application:components#依存関係を持つコンポーネント]だけで十分です。 ```php interface EditControlFactory @@ -315,14 +315,14 @@ interface EditControlFactory } ``` -そして、設定ファイルに追加します。 +そして設定ファイルに足します。 ```neon services: - EditControlFactory ``` -そして今、ファクトリを要求してPresenterで使用できます。 +これでファクトリを要求して、プレゼンターで使えます。 ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // または編集結果にリダイレクトします、例: + // あるいは編集結果へリダイレクトします。たとえば: // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/ja/inject-method-attribute.texy b/best-practices/ja/inject-method-attribute.texy index 7e85e1e486..c5127f5c21 100644 --- a/best-practices/ja/inject-method-attribute.texy +++ b/best-practices/ja/inject-method-attribute.texy @@ -1,18 +1,18 @@ -injectメソッドと属性 -************* +inject メソッドとアトリビュート +******************* .[perex] -この記事では、NetteフレームワークでPresenterに依存関係を渡すさまざまな方法に焦点を当てます。推奨される方法であるコンストラクタを、`inject`メソッドや属性などの他のオプションと比較します。 +この記事では、Nette フレームワークでプレゼンターに依存関係を渡すさまざまな方法を扱います。推奨される方法であるコンストラクタインジェクションと、`inject` メソッドやアトリビュートといった代替手段を比べていきます。 -Presenterについても、[コンストラクタ |dependency-injection:passing-dependencies#コンストラクタによる受け渡し] による依存関係の受け渡しが推奨される方法です。 しかし、他のPresenterが継承する共通の祖先(例:`BasePresenter`)を作成し、この祖先も依存関係を持っている場合、[コンストラクタ地獄 |dependency-injection:passing-dependencies#コンストラクタ地獄] と呼ばれる問題が発生します。 これは、injectメソッドと属性(アノテーション)という代替手段を使用して回避できます。 +プレゼンターでも、ほかのクラスと同じく、[コンストラクタ |dependency-injection:passing-dependencies#コンストラクタインジェクション]で依存関係を渡すのが推奨される方法です。しかし、ほかのプレゼンターが継承する共通の祖先(`BasePresenter` など)を作り、その祖先も依存関係を必要とする場合、[コンストラクタ地獄 |dependency-injection:passing-dependencies#コンストラクタ地獄]として知られる問題が生じます。これは別の方法、すなわち inject メソッドとアトリビュート(以前はアノテーション)で回避できます。 `inject*()` メソッド ================ -これは、[セッター |dependency-injection:passing-dependencies#セッターによる受け渡し] による依存関係の受け渡しの一形態です。これらのセッターの名前は、接頭辞 `inject` で始まります。 Nette DIは、このように名付けられたメソッドをPresenterインスタンスの作成直後に自動的に呼び出し、必要なすべての依存関係を渡します。したがって、publicとして宣言する必要があります。 +これは[セッター |dependency-injection:passing-dependencies#セッターインジェクション]による依存関係の受け渡しの一種です。セッターの名前は接頭辞 `inject` で始まらなければなりません。Nette DI は、プレゼンターのインスタンスを作った直後にこの名前のメソッドを自動的に呼び、必要な依存関係をすべて渡します。ですから public として宣言する必要があります。 -`inject*()` メソッドは、コンストラクタを複数のメソッドに拡張したものと考えることができます。これにより、`BasePresenter` は別のメソッドを介して依存関係を受け取り、コンストラクタをその子孫のために空けておくことができます。 +`inject*()` メソッドは、複数のメソッドに分割されたコンストラクタの延長と見なせます。これにより `BasePresenter` は別のメソッドで依存関係を受け取れるようになり、コンストラクタは子孫のために空けておけます。 ```php abstract class BasePresenter extends Nette\Application\UI\Presenter @@ -36,18 +36,18 @@ class MyPresenter extends BasePresenter } ``` -Presenterは任意の数の `inject*()` メソッドを持つことができ、各メソッドは任意の数のパラメータを持つことができます。これは、Presenterが [トレイトで構成されている |presenter-traits] 場合や、各トレイトが独自の依存関係を必要とする場合に非常に便利です。 +プレゼンターは `inject*()` メソッドをいくつでも持てますし、それぞれがいくつでもパラメータを取れます。この方法は、プレゼンターが[トレイトから構成されている |presenter-traits]場合で、各トレイトが独自の依存関係を必要とするときにも向いています。 -`Inject` 属性 -=========== +`Inject` アトリビュート +================ -これは、[プロパティへのインジェクション |dependency-injection:passing-dependencies#変数の設定による受け渡し] の一形態です。どの変数にインジェクトするかを指定するだけで、Nette DIはPresenterインスタンスの作成直後に依存関係を自動的に渡します。それらを挿入できるようにするには、publicとして宣言する必要があります。 +これは[プロパティへの注入 |dependency-injection:passing-dependencies#プロパティインジェクション]の一種です。注入したいプロパティに印を付けるだけで、Nette DI がプレゼンターのインスタンスを作った直後に依存関係を自動的に渡します。注入を可能にするため、これらのプロパティは public として宣言する必要があります。 -プロパティを属性でマークします:(以前はアノテーション `/** @inject */` が使用されていました) +プロパティにはアトリビュートで印を付けます(以前は `/** @inject */` アノテーションが使われていました)。 ```php -use Nette\DI\Attributes\Inject; // この行は重要です +use Nette\DI\Attributes\Inject; // この行が大事です class MyPresenter extends Nette\Application\UI\Presenter { @@ -56,6 +56,6 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -この依存関係の受け渡し方法の利点は、非常に簡潔な記述形式でした。しかし、[コンストラクタプロパティプロモーション |https://blog.nette.org/ja/php-8-0-new-features-overview#toc-constructor-property-promotion] の登場により、コンストラクタを使用する方が簡単に見えます。 +この受け渡し方法の利点は、構文がとても簡潔なことでした。しかし[コンストラクタのプロパティ昇格 |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 index e810ca3a2b..dc494337f1 100644 --- a/best-practices/ja/lets-create-contact-form.texy +++ b/best-practices/ja/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -お問い合わせフォームの作成 -************* +お問い合わせフォームを作ろう +************** .[perex] -Netteでお問い合わせフォームを作成し、メールで送信する方法を見ていきましょう。さあ、始めましょう! +Nette でお問い合わせフォームを作り、送られたデータをメールで送るところまでを見ていきましょう。さっそく始めます。 -まず、新しいプロジェクトを作成する必要があります。その方法は [はじめに |nette:installation] ページで説明されています。その後、フォームの作成を開始できます。 +まず新しいプロジェクトを作る必要があります。方法は[はじめに |nette:installation]のページで説明しています。それができたら、フォームを作り始められます。 -最も簡単な方法は、[Presenter内で直接フォームを作成する |forms:in-presenter] ことです。事前に準備された `HomePresenter` を使用できます。そこにフォームを表す `contactForm` コンポーネントを追加します。これを行うには、コードにコンポーネントを作成するファクトリメソッド `createComponentContactForm()` を記述します。 +最も簡単なのは、[プレゼンターの中に直接フォームを作る |forms:in-presenter]方法です。すでにある `HomePresenter` を使えます。フォームを表す `contactForm` というコンポーネントを追加しましょう。プレゼンターのコードに、そのコンポーネントを作るファクトリメソッド `createComponentContactForm()` を足します。 ```php use Nette\Application\UI\Form; @@ -18,26 +18,26 @@ class HomePresenter extends Presenter { $form = new Form; $form->addText('name', 'お名前:') - ->setRequired('名前を入力してください'); + ->setRequired('お名前を入力してください'); $form->addEmail('email', 'メールアドレス:') ->setRequired('メールアドレスを入力してください'); - $form->addTextarea('message', 'メッセージ:') + $form->addTextArea('message', 'メッセージ:') ->setRequired('メッセージを入力してください'); $form->addSubmit('send', '送信'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // メールの送信 } } ``` -ご覧のとおり、2つのメソッドを作成しました。最初のメソッド `createComponentContactForm()` は新しいフォームを作成します。このフォームには、名前、メール、メッセージのフィールドがあり、これらは `addText()`、`addEmail()`、`addTextArea()` メソッドで追加します。フォームを送信するためのボタンも追加しました。 しかし、ユーザーがフィールドを空欄にした場合はどうでしょうか?その場合、それが必須フィールドであることを知らせるべきです。これは `setRequired()` メソッドで実現しました。 最後に、フォームが正常に送信された場合にトリガーされる [イベント |nette:glossary#イベント] `onSuccess` も追加しました。私たちの場合、送信されたフォームの処理を担当する `contactFormSucceeded` メソッドを呼び出します。これはすぐにコードに追加します。 +ご覧のとおり、メソッドを 2 つ作りました。ひとつめの `createComponentContactForm()` は新しいフォームのインスタンスを作ります。名前、メールアドレス、メッセージの項目を、それぞれ `addText()`、`addEmail()`、`addTextArea()` メソッドで追加しています。送信ボタンも足しました。では、ユーザーが項目を空のままにしたらどうなるでしょうか。その場合は必須であることを伝えるべきです。それを `setRequired()` メソッドで実現しました。最後に `onSuccess` に[イベント |nette:glossary#イベント]ハンドラを結びつけています。これはフォームが正しく送信されたときに発火します。ここでは `contactFormSucceeded` メソッドを呼び、送られたデータの処理を担当させます。このメソッドはこのあと実装します。 -`contactForm` コンポーネントを `Home/default.latte` テンプレートでレンダリングさせます。 +`contactForm` コンポーネントを `Home/default.latte` テンプレートで描きましょう。 ```latte {block content} @@ -45,12 +45,9 @@ class HomePresenter extends Presenter {control contactForm} ``` -メールの送信自体については、`ContactFacade` という名前の新しいクラスを作成し、それを `app/Model/ContactFacade.php` ファイルに配置します。 +メールの送信そのもののために、`ContactFacade` という新しいクラスを作り、`app/Model/ContactFacade.php` に置きます。 ```php -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -76,9 +73,9 @@ class ContactFacade } ``` -`sendMessage()` メソッドはメールを作成して送信します。これには、コンストラクタを介して依存関係として渡される、いわゆるメーラーを使用します。[メールの送信 |mail:] について詳しく読んでください。 +`sendMessage()` メソッドがメールを作って送ります。そのために mailer サービスを使い、コンストラクタで依存関係として受け取っています。詳しくは[メールの送信 |mail:]をご覧ください。 -次に、Presenterに戻り、`contactFormSucceeded()` メソッドを完成させます。これは `ContactFacade` クラスの `sendMessage()` メソッドを呼び出し、フォームからのデータを渡します。そして、`ContactFacade` オブジェクトをどのように取得しますか?コンストラクタを介して渡してもらいます。 +ではプレゼンターに戻って `contactFormSucceeded()` メソッドを仕上げましょう。フォームから送られたデータを渡して、`ContactFacade` クラスの `sendMessage()` メソッドを呼びます。では `ContactFacade` オブジェクトはどうやって手に入れるのでしょうか。依存性注入を使い、コンストラクタで要求します。 ```php use App\Model\ContactFacade; @@ -100,22 +97,22 @@ class HomePresenter extends Presenter public function contactFormSucceeded(stdClass $data): void { $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('メッセージは送信されました'); + $this->flashMessage('メッセージを送信しました'); $this->redirect('this'); } } ``` -メールが送信された後、ユーザーにメッセージが送信されたことを確認する、いわゆる [フラッシュメッセージ |application:components#フラッシュメッセージ] を表示し、その後、ブラウザの *リフレッシュ* でフォームが繰り返し送信されるのを防ぐために現在のページにリダイレクトします。 +メールを送ったあと、送信を知らせる[フラッシュメッセージ |application:components#フラッシュメッセージ]をユーザーに表示します。それからリダイレクトして、ブラウザの再読み込みでフォームが再送信されないようにします。 -さて、すべてが機能していれば、お問い合わせフォームからメールを送信できるはずです。おめでとうございます! +ここまで正しく設定できていれば、お問い合わせフォームからメールを送れるはずです。おめでとうございます。 -HTMLメールテンプレート -------------- +HTML メールのテンプレート +--------------- -今のところ、フォームから送信されたメッセージのみを含むプレーンテキストのメールが送信されます。しかし、メールでHTMLを使用して、その外観をより魅力的にすることができます。そのためにLatteでテンプレートを作成し、それを `app/Model/contactEmail.latte` に記述します。 +今のところ、フォームから送られたメッセージだけを含むプレーンテキストのメールが送られます。しかしメールに HTML を使えば、見た目をより魅力的にできます。そのためのテンプレートを Latte で作り、`app/Model/contactEmail.latte` として保存します。 ```latte <html> @@ -129,7 +126,7 @@ HTMLメールテンプレート </html> ``` -残りは、このテンプレートを使用するように `ContactFacade` を変更することです。コンストラクタで、`Latte\Engine` オブジェクト、つまり [Latteテンプレートレンダラー |latte:develop#テンプレートをレンダリングする方法] を作成できる `LatteFactory` クラスを要求します。`renderToString()` メソッドを使用して、テンプレートを文字列にレンダリングします。最初のパラメータはテンプレートへのパス、2番目は変数です。 +あとは、このテンプレートを使うよう `ContactFacade` を書き換えるだけです。コンストラクタで `LatteFactory` クラスを要求します。これは [Latte のテンプレートレンダラー |latte:develop#テンプレートをレンダリングするには]である `Latte\Engine` オブジェクトを作れます。`renderToString()` メソッドでテンプレートを文字列に描きます。第 1 パラメータはテンプレートファイルへのパス、第 2 パラメータはそこに渡す変数の配列です。 ```php namespace App\Model; @@ -165,15 +162,15 @@ class ContactFacade } ``` -生成されたHTMLメールを、元の `setBody()` の代わりに `setHtmlBody()` メソッドに渡します。また、ライブラリがテンプレートの `<title>` 要素から件名を取得するため、`setSubject()` でメールの件名を指定する必要もありません。 +生成した HTML のメール本文は、もとの `setBody()` ではなく `setHtmlBody()` メソッドに渡します。`setSubject()` でメールの件名を指定する必要もありません。ライブラリがテンプレートの `<title>` 要素から自動的に取り出してくれるからです。 設定 ------------ +--- -`ContactFacade` クラスのコードには、まだ管理者メール `admin@example.com` がハードコーディングされています。これを設定ファイルに移動する方が良いでしょう。どうすればよいでしょうか? +`ContactFacade` クラスのコードには、管理者のメールアドレス `admin@example.com` がまだ直接書かれています。これは設定ファイルに移したほうがよいでしょう。どうすればよいでしょうか。 -まず、`ContactFacade` クラスを変更し、メールを含む文字列をコンストラクタで渡される変数に置き換えます。 +まず `ContactFacade` クラスを書き換え、直接書かれたメールアドレスの文字列を、コンストラクタで渡される変数に置き換えます。 ```php class ContactFacade @@ -197,25 +194,25 @@ class ContactFacade } ``` -そして2番目のステップは、設定でこの変数の値を指定することです。`config/services.neon` ファイル(または `app/config/services.neon`)に次のように記述します。 +次の段階は、この変数の値を設定で与えることです。`app/config/services.neon` ファイルに次を足します。 ```neon services: - App\Model\ContactFacade(adminEmail: admin@example.com) ``` -これで完了です。`services` セクションの項目が多く、メールがその中で見失われていると感じる場合は、それを変数にすることができます。記述を次のように変更します。 +これで完了です。`services` セクションに項目が多く、メールアドレスがその中で埋もれてしまうと感じるなら、パラメータにできます。次のように書き換えてください。 ```neon services: - App\Model\ContactFacade(adminEmail: %adminEmail%) ``` -そして、`config/common.neon` ファイル(または `app/config/common.neon`)でこのパラメータを定義します。 +そしてこのパラメータを `app/config/common.neon` ファイルで定義します。 ```neon parameters: adminEmail: admin@example.com ``` -これで完了です! +これで出来上がりです。 diff --git a/best-practices/ja/microsites.texy b/best-practices/ja/microsites.texy index f534524da8..5d47fd2b93 100644 --- a/best-practices/ja/microsites.texy +++ b/best-practices/ja/microsites.texy @@ -1,11 +1,11 @@ マイクロサイトの書き方 *********** -あなたの会社の次のイベントのために、すぐに小さなウェブサイトを作成する必要があると想像してみてください。それはシンプルで、速く、不必要な複雑さがないものでなければなりません。このような小さなプロジェクトには、堅牢なフレームワークは必要ないと思うかもしれません。しかし、Netteフレームワークを使用することで、このプロセスが大幅に簡素化され、高速化されるとしたらどうでしょうか? +会社の近々のイベントのために、小さなウェブサイトを手早く作る必要があるとしましょう。シンプルで、速く、余計な面倒がないものが求められます。こんな小さなプロジェクトに堅牢なフレームワークは要らない、と思うかもしれません。しかし、Nette Framework を使うことでその作業がむしろ簡単に、そして速くなるとしたらどうでしょうか。 -結局のところ、単純なウェブサイトを作成する場合でも、快適さを諦めたくはありません。すでに解決されたことを再発明したくはありません。怠惰になって、甘やかされてください。Nette Frameworkは、マイクロフレームワークとしても最適に使用できます。 +単純なウェブサイトを作るときでも、快適さを手放したくはありません。すでに解決済みのことを作り直したくもありません。遠慮なく怠けて、甘やかされてください。Nette Framework はマイクロフレームワークとしても優れています。 -そのようなマイクロサイトはどのように見えるでしょうか?たとえば、ウェブサイトのコード全体をパブリックフォルダ内の単一のファイル `index.php` に配置します。 +そんなマイクロサイトはどんな形になるでしょうか。たとえば、サイト全体のコードが公開ディレクトリの `index.php` ファイル 1 本に収まります。 ```php <?php @@ -16,48 +16,48 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// config.neonの設定に基づいてDIコンテナを作成 +// 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 などにリダイレクト + // ブラウザの言語を判定して URL /en や /de などにリダイレクトします $supportedLangs = ['en', 'de', 'cs']; $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); -// URL https://example.com/cs または https://example.com/en のルート -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // 対応するテンプレートを表示します、例:../templates/en.latte +// 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` に保存されたテンプレートになります。 +あとはすべて、親の `/templates` ディレクトリに置かれたテンプレートです。 -`index.php` のPHPコードは、まず [環境を準備し |bootstrap:]、次に [ルート |application:routing#コールバックによる動的ルーティング] を定義し、最後にアプリケーションを実行します。利点は、`addRoute()` 関数の2番目のパラメータが呼び出し可能であり、対応するページが開かれた後に実行されることです。 +`index.php` の PHP コードは、まず[環境を整え |bootstrap:]、次に[ルート |application:routing#コールバックによる動的なルーティング]を定義し、最後にアプリケーションを実行します。利点は、`addRoute()` 関数の第 2 パラメータに callable を渡せて、対応するページにアクセスされたときにそれが実行されることです。 -マイクロサイトにNetteを使用する理由 --------------------- +なぜマイクロサイトに Nette を使うのか +---------------------- -- [Tracy|tracy:] を試したことのあるプログラマは、今日、それなしで何かをプログラミングすることを想像できません。 -- しかし、何よりも、[Latte|latte:] テンプレートシステムを利用するでしょう。なぜなら、2ページ目から[レイアウトとコンテンツ|latte:template-inheritance] を分離したいからです。 -- そして、XSS脆弱性が発生しないように、[自動エスケープ |latte:safety-first] に頼りたいはずです。 -- Netteはまた、エラーが発生した場合にプログラマ向けのエラーメッセージPHPが表示されず、ユーザーフレンドリーなページが表示されることを保証します。 -- たとえばお問い合わせフォームの形でユーザーからのフィードバックを得たい場合は、[フォーム|forms:] と [データベース|database:] を追加するだけです。 -- 記入されたフォームを簡単に [メールで送信する|mail:] こともできます。 -- たとえばフィードを取得して表示する場合など、[キャッシュ|caching:] が役立つことがあります。 +- [Tracy|tracy:]を試したプログラマーは、今日それなしでコードを書くことをほとんど想像できません。 +- 何より [Latte|latte:]のテンプレートシステムの恩恵を受けられます。ページが 2 つしかなくても、[レイアウトと内容|latte:template-inheritance]は分けたくなるからです。 +- そして XSS 脆弱性を防ぐために、[自動エスケープ |latte:safety-first]に頼りたくなるはずです。 +- Nette は、エラーが起きたときに生の PHP のエラーメッセージが表示されないようにし、代わりに親しみやすいページを出します。 +- お問い合わせフォームなどでユーザーの声を集めたくなったら、[フォーム|forms:]と[データベース|database:]のサポートを簡単に足せます。 +- 記入されたフォームを[メールで送る|mail:]のも簡単です。 +- ときには[キャッシュ|caching:]が役立つこともあります。たとえばフィードを取得して表示する場合です。 -速度と効率が鍵となる今日の世界では、不必要な遅延なしに結果を達成できるツールを持つことが重要です。Nette frameworkはまさにそれを提供します - 迅速な開発、セキュリティ、そしてプロセスを簡素化するTracyやLatteなどの幅広いツール。いくつかのNetteパッケージをインストールするだけで、このようなマイクロサイトを構築するのは突然非常に簡単になります。そして、どこにもセキュリティホールが隠れていないことを知っています。 +速さと効率が決定的な今の時代、無駄な遅れなく結果にたどり着ける道具を持っていることは重要です。Nette Framework はまさにそれを提供します。素早い開発、安全性、そして作業を滑らかにする Tracy や Latte といった幅広い道具です。Nette のパッケージをいくつかインストールするだけで、こうしたマイクロサイトを作るのは驚くほど簡単になります。しかも隠れたセキュリティの穴がないと安心できます。 diff --git a/best-practices/ja/pagination.texy b/best-practices/ja/pagination.texy index c2524004bd..6320181781 100644 --- a/best-practices/ja/pagination.texy +++ b/best-practices/ja/pagination.texy @@ -2,9 +2,9 @@ ***************** .[perex] -Webアプリケーションを作成する際、ページに表示される項目数を制限するという要件に非常に頻繁に遭遇します。 +ウェブアプリケーションを開発していると、1 ページに並べる項目の数を制限したいという要求によく出会います。これはページネーションとして知られる手法です。 -ページネーションなしですべてのデータを表示する状態から始めます。データベースからデータを選択するために、コンストラクタに加えて、公開されたすべての記事を公開日の降順で返す `findPublishedArticles` メソッドを含む `ArticleRepository` クラスがあります。 +まずはページネーションなしですべてのデータを並べる状態から始めましょう。データベースからデータを取り出すために、`ArticleRepository` クラスがあります。コンストラクタのほかに、公開済みの記事を公開日の降順で返す `findPublishedArticles` メソッドを持っています。 ```php namespace App\Model; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -Presenterでは、モデルクラスをインジェクトし、renderメソッドで公開された記事を要求し、それをテンプレートに渡します。 +プレゼンターでは、このモデルのクラスを注入します。render メソッドで公開済みの記事を取得し、テンプレートに渡します。 ```php namespace App\Presentation\Home; @@ -52,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -`default.latte` テンプレートでは、記事の表示を担当します。 +`default.latte` テンプレートが記事の一覧を受け持ちます。 ```latte {block content} @@ -67,11 +67,11 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -この方法で、すべての記事を表示できますが、記事の数が増えると問題が発生し始めます。その時点で、ページネーションメカニズムの実装が役立ちます。 +これですべての記事を並べられますが、記事の数が増えると問題になります。そこでページネーションのしくみを実装すると役に立ちます。 -これにより、すべての記事がいくつかのページに分割され、現在の1ページの記​​事のみが表示されます。合計ページ数と記事の分割は、[Paginator |utils:Paginator] が、合計でいくつの記事があり、ページごとに表示したい記事の数に基づいて自動的に計算します。 +このしくみは、すべての記事をいくつかのページに分け、今選ばれているページに属する記事だけを表示します。ページの総数と記事の分け方は、記事の総数と 1 ページあたりの記事数をもとに [Paginator |utils:Paginator]ユーティリティが計算します。 -最初のステップでは、リポジトリクラスの記事取得メソッドを変更して、1ページの記事のみを返すようにします。また、Paginatorを設定するために必要なデータベース内の記事の総数を取得するメソッドを追加します。 +まず、リポジトリのクラスの記事取得メソッドを、1 ページ分の記事だけを返せるように書き換えます。あわせて、Paginator の設定に必要な、データベース内の記事の総数を得るメソッドも足します。 ```php namespace App\Model; @@ -99,7 +99,7 @@ class ArticleRepository } /** - * 公開された記事の総数を返します + * 公開済みの記事の総数を返します */ public function getPublishedArticlesCount(): int { @@ -108,9 +108,9 @@ class ArticleRepository } ``` -次に、Presenterの変更に取り掛かります。renderメソッドに現在表示されているページの番号を渡します。この番号がURLの一部でない場合、最初のページのデフォルト値を設定します。 +次にプレゼンターを書き換えます。現在のページ番号を `renderDefault` メソッドに渡します。この数が URL の一部でなければ、既定値の 1(最初のページ)を設定します。 -また、renderメソッドを拡張して、Paginatorインスタンスの取得、その設定、およびテンプレートで表示するための正しい記事の選択を行います。変更後のHomePresenterは次のようになります(Paginatorを使用する場合): +さらに render メソッドを拡張して、Paginator のインスタンスを作って設定し、テンプレートに表示する記事を適切に選びます。書き換えた `HomePresenter` は次のようになります。 ```php namespace App\Presentation\Home; @@ -127,27 +127,27 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // 公開された記事の総数を取得します + // 公開済みの記事の総数を取得します $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Paginatorのインスタンスを作成し、設定します + // Paginator のインスタンスを作って設定します $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // 記事の総数 - $paginator->setItemsPerPage(10); // ページあたりの項目数 + $paginator->setItemCount($articlesCount); // 項目の総数 + $paginator->setItemsPerPage(10); // 1 ページあたりの項目数 $paginator->setPage($page); // 現在のページ番号 - // Paginatorの計算に基づいてデータベースから記事の限定されたセットを取得します + // Paginator の計算にもとづいて、限られた数の記事をデータベースから取得します $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - // それをテンプレートに渡します + // テンプレートに渡します $this->template->articles = $articles; - // そして、ページネーションオプションを表示するためのPaginator自体も + // ページネーションの操作部品を表示するために Paginator 自体も渡します $this->template->paginator = $paginator; } } ``` -テンプレートはすでに1ページの記​​事のみを反復処理しているため、ページネーションリンクを追加するだけで済みます。 +テンプレートは現在のページの記事だけを反復するようになりました。あとはページネーションのリンクを足すだけです。 ```latte {block content} @@ -164,7 +164,7 @@ class HomePresenter extends Nette\Application\UI\Presenter {if !$paginator->isFirst()} <a n:href="default, 1">最初</a>  |  - <a n:href="default, $paginator->page-1">前へ</a> + <a n:href="default, $paginator->getPage() - 1">前へ</a>  |  {/if} @@ -180,9 +180,9 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -このようにして、Paginatorを使用してページネーションオプションをページに追加しました。データベース層として [Nette Database Core |database:sql-way] の代わりに [Nette Database Explorer |database:explorer] を使用する場合、Paginatorを使用せずにページネーションを実装することもできます。`Nette\Database\Table\Selection` クラスには、Paginatorから継承されたページネーションロジックを持つ [page() |api:Nette\Database\Table\Selection::page()] メソッドが含まれています。 +これで 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; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -Presenterでは、Paginatorを作成する必要はありません。代わりに、リポジトリが返す `Selection` クラスのメソッドを使用します。 +プレゼンターでは Paginator のインスタンスを作る必要がありません。代わりに、リポジトリが返す `Selection` オブジェクトの `page()` メソッドを使います。 ```php namespace App\Presentation\Home; @@ -222,21 +222,21 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // 公開された記事を取得します + // 公開済みの記事を取得します $articles = $this->articleRepository->findPublishedArticles(); - // そして、pageメソッドの計算に基づいて制限された部分のみをテンプレートに送信します + // page メソッドの計算で限られた分だけをテンプレートに渡します $lastPage = 0; $this->template->articles = $articles->page($page, 10, $lastPage); - // そして、ページネーションオプションを表示するために必要なデータも + // ページネーションの選択肢を表示するのに必要なデータも渡します $this->template->page = $page; $this->template->lastPage = $lastPage; } } ``` -テンプレートにPaginatorを送信しなくなったため、ページネーションリンクを表示する部分を変更します。 +Paginator オブジェクトをテンプレートに渡さなくなったので、ページネーションのリンクを表示する部分を調整する必要があります。 ```latte {block content} @@ -268,6 +268,6 @@ class HomePresenter extends Nette\Application\UI\Presenter </div> ``` -この方法で、Paginatorを使用せずにページネーションメカニズムを実装しました。 +こうして、Paginator ユーティリティを明示的に使わずにページネーションのしくみを実装できました。 {{priority: -1}} diff --git a/best-practices/ja/passing-settings-to-presenters.texy b/best-practices/ja/passing-settings-to-presenters.texy index 78fcc70158..8da3364e8a 100644 --- a/best-practices/ja/passing-settings-to-presenters.texy +++ b/best-practices/ja/passing-settings-to-presenters.texy @@ -1,10 +1,10 @@ -Presenterへの設定の受け渡し -****************** +プレゼンターへの設定の受け渡し +*************** .[perex] -Presenterにオブジェクトではない引数(デバッグモードで実行されているかどうかの情報、ディレクトリへのパスなど)を渡す必要があり、したがってautowiringを使用して自動的に渡すことができない場合はどうすればよいですか?解決策は、それらを`Settings`オブジェクトにカプセル化することです。 +オートワイヤリングでは自動的に渡せない、オブジェクトでない引数(デバッグモードを示すフラグ、ディレクトリのパスなど)をプレゼンターに渡す必要がありますか。その答えは、それらを専用の `Settings` オブジェクトにまとめることです。 -`Settings` サービスは、実行中のアプリケーションに関する情報をPresenterに提供するための非常に簡単で便利な方法を提供します。その具体的な形式は、完全にあなたの特定のニーズに依存します。例: +`Settings` サービスは、動いているアプリケーションについての情報をプレゼンターに届ける、とても単純ながら効果的な方法です。その具体的な構造は、あなたの必要に完全に委ねられています。例を挙げます。 ```php namespace App; @@ -12,7 +12,7 @@ namespace App; class Settings { public function __construct( - // PHP 8.1以降、readonlyを指定可能 + // PHP 8.1 以降は readonly が使えます public bool $debugMode, public string $appDir, // など @@ -20,7 +20,7 @@ class Settings } ``` -設定への登録例: +設定での登録の例です。 ```neon services: @@ -30,7 +30,7 @@ services: ) ``` -Presenterがこのサービスによって提供される情報を必要とする場合、単にコンストラクタでそれを要求します: +このサービスが提供する情報をプレゼンターが必要とするなら、コンストラクタでそれを要求するだけです。 ```php class MyPresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/ja/post-links.texy b/best-practices/ja/post-links.texy index 2d4877f097..622400350a 100644 --- a/best-practices/ja/post-links.texy +++ b/best-practices/ja/post-links.texy @@ -1,16 +1,16 @@ -POSTリンクの正しい使い方 -************** +POST リンクの正しい使い方 +*************** .[perex] -Webアプリケーション、特に管理インターフェースでは、サーバーの状態を変更するアクションはHTTPメソッドGETを介して実行されるべきではないという基本的なルールがあるべきです。メソッド名が示すように、GETはデータの取得にのみ使用されるべきであり、変更には使用されるべきではありません。 レコードの削除などのアクションには、POSTメソッドを使用する方が適しています。理想的にはDELETEメソッドですが、JavaScriptなしでは呼び出せないため、歴史的にPOSTが使用されています。 +ウェブアプリケーション、とくに管理画面では、サーバーの状態を変える操作を HTTP の GET メソッドで行わない、という基本の原則を守るべきです。名前が示すとおり、GET はデータの取得にだけ使うもので、変更に使うものではありません。レコードの削除のような操作には POST メソッドのほうが適しています。理想をいえば DELETE メソッドですが、JavaScript なしでは呼び出せません。だからこそ、こうした操作には歴史的に POST が使われてきました。 -実践的にはどうすればよいでしょうか?この簡単なトリックを利用してください。テンプレートの最初に、`postForm` という識別子を持つ補助フォームを作成し、それを削除ボタンに使用します。 +実際にはどう実装すればよいでしょうか。次の簡単な工夫を使います。レイアウトテンプレートの先頭に、ID が `postForm` の補助的なフォームを作ります。削除ボタンなどの操作では、このフォームを使います。 ```latte .{file:@layout.latte} <form method="post" id="postForm"></form> ``` -このフォームのおかげで、古典的なリンク `<a>` の代わりに、通常のリンクのように見えるように視覚的に調整できるボタン `<button>` を使用できます。たとえば、CSSフレームワークBootstrapは、ボタンが他のリンクと視覚的に区別されないようにするクラス `btn btn-link text-danger` を提供します(削除なので赤文字にする例)。属性 `form="postForm"` を使用して、事前に準備されたフォームにリンクします。`formaction` 属性で送信先URLを指定します。 +このフォームのおかげで、通常の `<a>` リンクの代わりに `<button>` を使えます。このボタンは普通のリンクのように見えるようスタイルを当てられます。たとえば Bootstrap の CSS フレームワークには `btn btn-link` クラスがあり、ボタンをほかのリンクと見分けがつかないようにできます。`form="postForm"` 属性を使って、ボタンを用意した補助フォームに結びつけます。 ```latte .{file:admin.latte} <table> @@ -18,13 +18,13 @@ Webアプリケーション、特に管理インターフェースでは、サ <td>{$post->title}</td> <td> <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">削除</button> - <!-- <a n:href="delete $post->id">delete</a> の代わりに --> + <!-- <a n:href="delete $post->id">削除</a> の代わり --> </td> </tr> </table> ``` -リンク(ボタン)をクリックすると、`delete` アクションが呼び出されます。リクエストがPOSTメソッドと同一ドメインからのみ受け入れられるようにするため(これはCSRF攻撃に対する効果的な防御策です)、`#[Requires]` 属性を使用します。 +このボタンをクリックすると `delete` アクションが呼ばれます。リクエストが POST メソッドでのみ、しかも同じドメインから来たものだけ受け付けられるようにするには(CSRF 攻撃への実効的な防御になります)、`#[Requires]` アトリビュートを使います。 ```php .{file:AdminPresenter.php} use Nette\Application\Attributes\Requires; @@ -34,15 +34,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter #[Requires(methods: 'POST', sameOrigin: true)] public function actionDelete(int $id): void { - $this->facade->deletePost($id); // レコードを削除する仮のコード + $this->facade->deletePost($id); // レコードを削除する架空のコード $this->redirect('default'); } } ``` -この属性はNette Application 3.2以降存在し、その可能性については [#Requires属性の使い方 |attribute-requires] ページで詳しく知ることができます。 +このアトリビュートは Nette Application 3.2 から使えます。その機能について詳しくは [#Requires アトリビュートの使い方 |attribute-requires]のページをご覧ください。 -`actionDelete()` アクションの代わりに `handleDelete()` シグナルを使用する場合、シグナルにはこの保護が暗黙的に設定されているため、`sameOrigin: true` を指定する必要はありません。 +`actionDelete()` アクションの代わりに `handleDelete()` シグナルを使っていた場合、`sameOrigin: true` を指定する必要はありません。シグナルではこの保護が既定で有効だからです。 ```php .{file:AdminPresenter.php} #[Requires(methods: 'POST')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -このアプローチは、アプリケーションのセキュリティを向上させるだけでなく、正しいWeb標準と実践の遵守にも貢献します。状態を変更するアクションにPOSTメソッドを利用することで、より堅牢で安全なアプリケーションを実現できます。 +この方法はアプリケーションの安全性を高めるだけでなく、ウェブの適切な標準と作法に沿うことにもつながります。状態を変える操作に POST メソッドを使えば、より堅牢で安全なアプリケーションになります。 diff --git a/best-practices/ja/presenter-traits.texy b/best-practices/ja/presenter-traits.texy index 3410b8424f..85e91e8a56 100644 --- a/best-practices/ja/presenter-traits.texy +++ b/best-practices/ja/presenter-traits.texy @@ -1,16 +1,16 @@ -トレイトからのPresenterの構成 -******************* +トレイトによるプレゼンターの構成 +**************** .[perex] -複数のPresenterで同じコードを実装する必要がある場合(例:ユーザーがログインしているかの検証)、コードを共通の祖先に配置することが考えられます。もう一つの選択肢は、単一目的の[トレイト |nette:introduction-to-object-oriented-programming#トレイト]を作成することです。 +同じ機能を複数のプレゼンターで実装する必要がある場合(ユーザーのログイン確認など)、共通の祖先にコードを置くのがよくあるやり方です。もうひとつの選択肢が、目的をひとつに絞った[トレイト |nette:introduction-to-object-oriented-programming#トレイト]を作ることです。 -この解決策の利点は、各Presenterが必要とするトレイトだけを使用できることです。一方、PHPでは多重継承は不可能です。 +トレイトを使う利点は、PHP が多重継承に対応していないこともあり、各プレゼンターが本当に必要なトレイトだけを取り込めることです。 -これらのトレイトは、Presenterが作成されるときに、すべての [injectメソッド |inject-method-attribute#inject メソッド] が順次呼び出されるという事実を利用できます。各injectメソッドの名前が一意であることを確認するだけで済みます。 +これらのトレイトは、プレゼンターのインスタンスが作られるときに[すべての inject メソッド |inject-method-attribute#inject*() メソッド]が順に呼ばれることを活かせます。使うすべてのトレイトとプレゼンター自身のあいだで、各 inject メソッドの名前が重ならないようにするだけです。 -トレイトは、[onStartup または onRender |application:presenters#イベント] イベントに初期化コードをフックすることができます。 +トレイトは [onStartup や onRender |application:presenters#イベント]のイベントに初期化のコードを結びつけられます。 -例: +例を挙げます。 ```php trait RequireLoggedUser @@ -36,7 +36,7 @@ trait StandardTemplateFilters } ``` -Presenterはこれらのトレイトを簡単に使用します: +あとはプレゼンターがこれらのトレイトを使うだけです。 ```php class ArticlePresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/ja/pretty-urls.texy b/best-practices/ja/pretty-urls.texy new file mode 100644 index 0000000000..2ecc54d02f --- /dev/null +++ b/best-practices/ja/pretty-urls.texy @@ -0,0 +1,204 @@ +スラッグを使った読みやすい 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 index 6a17b77503..8a0551c2ec 100644 --- a/best-practices/ja/restore-request.texy +++ b/best-practices/ja/restore-request.texy @@ -1,16 +1,16 @@ -前のページに戻る方法は? -************ +前のページに戻るには? +*********** .[perex] -ユーザーがフォームに入力中にログインセッションが切れたらどうしますか?データを失わないように、ログインページにリダイレクトする前にデータをセッションに保存します。Netteではこれは非常に簡単です。 +ユーザーがフォームに入力している最中にログインのセッションが切れたら、どうなるでしょうか。データを失わないよう、ログインページにリダイレクトする前に現在のリクエスト(フォームのデータを含みます)をセッションに保存できます。Nette では、これが驚くほど簡単です。 -現在のリクエストは `storeRequest()` メソッドを使用してセッションに保存でき、その識別子を短い文字列として返します。このメソッドは、現在のPresenterの名前、ビュー、およびそのパラメータを保存します。 フォームも送信された場合、フィールドの内容も保存されます(アップロードされたファイルを除く)。 +現在のリクエストは `storeRequest()` メソッドでセッションに保存できます。このメソッドは、保存したリクエストの一意な識別子(短い文字列)を返します。現在のプレゼンターの名前、そのビュー、パラメータが保存されます。リクエストの一部としてフォームが送信されていた場合は、フィールドに入力された値(アップロードされたファイルを除きます)も保存されます。 -リクエストの復元は `restoreRequest($key)` メソッドによって行われ、取得した識別子を渡します。これは元のPresenterとビューにリダイレクトします。ただし、保存されたリクエストにフォーム送信が含まれている場合、元のPresenterには `forward()` メソッドで移動し、以前に入力された値をフォームに渡し、再度レンダリングさせます。これにより、ユーザーはフォームを再度送信する機会があり、データは失われません。 +リクエストは `restoreRequest($key)` メソッドで復元します。先ほど得た識別子を渡してください。このメソッドはユーザーをもとのプレゼンターとビューに戻します。ただし保存されたリクエストにフォームの送信が含まれていた場合、`restoreRequest()` はリダイレクトの代わりに `forward()` メソッドを使います。以前に入力された値をフォームに戻し、もう一度描画できるようにします。おかげでユーザーは、入力したデータを失わずにフォームを再送信できます。 -重要なのは、`restoreRequest()` が新しくログインしたユーザーが最初にフォームに入力したユーザーと同じであるかどうかを確認することです。そうでない場合、リクエストは破棄され、何も行われません。 +重要なのは、`restoreRequest()` が、新しくログインしたユーザーがもとのフォームを送信したユーザーと同じかどうかを確認する点です。別のユーザーなら、保存されたリクエストは復元されず、メソッドは何もしません。安全性が高まります。 -例で説明しましょう。データを編集する `AdminPresenter` があり、その `startup()` メソッドでユーザーがログインしているかどうかを確認します。ログインしていない場合は、`SignPresenter` にリダイレクトします。同時に、現在のリクエストを保存し、そのキーを `SignPresenter` に送信します。 +例で見てみましょう。データを編集する `AdminPresenter` があるとします。その `startup()` メソッドはユーザーがログインしているかを確かめます。していなければ `SignPresenter` にリダイレクトします。同時に `storeRequest()` で現在のリクエストを保存し、そのキー(`$backlink`)を `SignPresenter` に渡します。 ```php class AdminPresenter extends Nette\Application\UI\Presenter @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -`SignPresenter` は、ログインフォームに加えて、キーが書き込まれる永続パラメータ `$backlink` も含みます。パラメータは永続的であるため、ログインフォームの送信後も転送されます。 +`SignPresenter` にはログインフォームのほかに、キーを保持する永続パラメータ `$backlink` があります。パラメータが永続なので、ログインフォームを送信したあとも値が保たれます。 ```php @@ -40,12 +40,12 @@ class SignPresenter extends Nette\Application\UI\Presenter protected function createComponentSignInForm() { $form = new Nette\Application\UI\Form; - // ... フォームコントロールを追加 ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + // ... フォームの項目を追加します ... + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { // ... ここでユーザーをログインさせます ... @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -保存されたリクエストのキーを `restoreRequest()` メソッドに渡し、元のPresenterにリダイレクト(または移動)します。 +保存したリクエストのキー(`$this->backlink`)を `restoreRequest()` メソッドに渡します。すると、ユーザーをもとのプレゼンターとビューにリダイレクト(または forward)します。 -ただし、キーが無効な場合(たとえば、セッションに存在しなくなった場合)、メソッドは何も行いません。したがって、`AdminPresenter` にリダイレクトする `$this->redirect('Admin:')` の呼び出しが続きます。 +ただしキーが正しくない場合(たとえばセッションから失効している場合)、メソッドは何もしません。ですから、続く `$this->redirect('Admin:')` の呼び出しがフォールバックとして働き、`AdminPresenter` のような既定のページにリダイレクトします。 {{priority: -1}} diff --git a/best-practices/pl/@home.texy b/best-practices/pl/@home.texy index 64470576e1..9019befb4e 100644 --- a/best-practices/pl/@home.texy +++ b/best-practices/pl/@home.texy @@ -1,24 +1,25 @@ -Przewodniki i dobre praktyki -**************************** +Poradniki i dobre praktyki +************************** .[perex] -Poradniki, rozwiązania częstych zadań i *dobre praktyki* dla Nette. +Poradniki, rozwiązania typowych zadań i dobre praktyki dla Nette. <div class=documentation> <div> -Aplikacje Nette ---------------- +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 wrócić do poprzedniej strony |restore-request] -- [Stronicowanie wyników bazy danych |pagination] +- [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ć linków POST |post-links] +- [Jak poprawnie używać odnośników POST |post-links] +- [Przyjazne adresy URL ze slugami |pretty-urls] </div> <div> @@ -26,9 +27,9 @@ Aplikacje Nette Formularze ---------- -- [Reużycie formularzy |form-reuse] -- [Formularz do tworzenia i edycji rekordu |creating-editing-form] -- [Tworzymy formularz kontaktowy |lets-create-contact-form] +- [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> @@ -38,11 +39,10 @@ Formularze Ogólne ------ - [Jak wczytać plik konfiguracyjny |bootstrap:] -- [Jak pisać mikro-strony |microsites] -- [Dlaczego Nette używa notacji PascalCase dla stałych? |https://blog.nette.org/pl/for-less-screaming-in-the-code] +- [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 dotyczące użycia |composer] -- [Wskazówki dotyczące edytorów i narzędzi |editors-and-tools] +- [Composer: wskazówki użycia |composer] - [Wprowadzenie do programowania obiektowego |nette:introduction-to-object-oriented-programming] </div> @@ -51,11 +51,11 @@ Ogólne Przykładowe rozwiązania ----------------------- -- [Przykłady Nette |https://github.com/nette-examples] +- [Nette examples |https://github.com/nette-examples] - [Doctrine & Nette |https://contributte.org/nettrine/] -- [Przykłady Contributte |https://contributte.org/examples.html] -- [Strona internetowa Doctrine ORM |https://github.com/MinecordNetwork/Website] -- [Szybki start |quickstart:] +- [Contributte examples |https://contributte.org/examples.html] +- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] +- [Quick start |quickstart:] </div> <div> @@ -63,7 +63,7 @@ Przykładowe rozwiązania Wideo ----- -Setki nagrań z Posledních sobot i filmów o Nette znajdziesz pod jednym dachem na "kanale Youtube Nette Frameworku":https://www.youtube.com/user/NetteFramework. +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 new file mode 100644 index 0000000000..7fd03c1ea8 --- /dev/null +++ b/best-practices/pl/@left-menu.texy @@ -0,0 +1,34 @@ +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 index 28fbec9e34..d812cd8006 100644 --- a/best-practices/pl/@meta.texy +++ b/best-practices/pl/@meta.texy @@ -1,2 +1 @@ {{sitename: Przewodniki i dobre praktyki}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/pl/attribute-requires.texy b/best-practices/pl/attribute-requires.texy index 750a41a034..8c854f39ab 100644 --- a/best-practices/pl/attribute-requires.texy +++ b/best-practices/pl/attribute-requires.texy @@ -2,30 +2,30 @@ Jak używać atrybutu `#[Requires]` ********************************* .[perex] -Kiedy piszesz aplikację internetową, często spotykasz się z potrzebą ograniczenia dostępu do określonych części Twojej aplikacji. Być może chcesz, aby niektóre żądania mogły wysyłać dane tylko za pomocą formularza (czyli metodą POST), lub aby były dostępne tylko dla wywołań AJAX. W Nette Framework 3.2 pojawiło się nowe narzędzie, które pozwoli Ci ustawić takie ograniczenia bardzo elegancko i przejrzyście: atrybut `#[Requires]`. +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 lub metody. Ponieważ jest to właściwie klasa, aby poniższe przykłady działały, konieczne jest podanie klauzuli use: +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; ``` -Atrybut `#[Requires]` możesz użyć przy samej klasie presentera, a także przy tych metodach: +Atrybutu `#[Requires]` możesz użyć na samej klasie presentera oraz na tych metodach: - `action<Action>()` - `render<View>()` - `handle<Signal>()` - `createComponent<Name>()` -Ostatnie dwie metody dotyczą również komponentów, więc atrybut możesz używać również przy nich. +Dwie ostatnie metody dotyczą także komponentów, więc możesz atrybutu używać również w nich. -Jeśli nie są spełnione warunki, które atrybut podaje, dojdzie do wywołania błędu HTTP 4xx. +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 itp.) są dozwolone dla dostępu. Na przykład, jeśli chcesz zezwolić na dostęp tylko przez wysłanie formularza, ustawisz: +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 @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Dlaczego powinieneś używać POST zamiast GET dla akcji zmieniających stan i jak to zrobić? [Przeczytaj poradnik |post-links]. +Dlaczego do akcji zmieniających stan używać POST zamiast GET i jak to robić? [Przeczytaj poradnik |post-links]. -Możesz podać metodę lub tablicę metod. Specjalnym przypadkiem jest wartość `'*'`, która zezwoli na wszystkie metody, czego standardowo presentery z [powodów bezpieczeństwa nie pozwalają |application:presenters#Kontrola metody HTTP]. +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łanie AJAX +Wywołania AJAX -------------- -Jeśli chcesz, aby presenter lub metoda była dostępna tylko dla żądań AJAX, użyj: +Jeśli chcesz, żeby presenter albo metoda były dostępne tylko dla żądań AJAX-owych, użyj: ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ class AjaxPresenter extends Nette\Application\UI\Presenter To samo pochodzenie ------------------- -Dla zwiększenia bezpieczeństwa możesz wymagać, aby żądanie zostało wykonane z tej samej domeny. Tym samym zapobiegniesz [podatności CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: +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)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -W przypadku metod `handle<Signal>()` dostęp z tej samej domeny jest wymagany automatycznie. Więc jeśli odwrotnie chcesz zezwolić na dostęp z dowolnej domeny, podaj: +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)] @@ -80,7 +80,7 @@ public function handleList(): void Dostęp przez forward -------------------- -Czasami przydatne jest ograniczenie dostępu do presentera tak, aby był dostępny tylko pośrednio, na przykład używając metody `forward()` lub `switch()` z innego presentera. W ten sposób na przykład chroni się error-presentery, aby nie można było ich wywołać z URL: +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)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -W praktyce często bywa potrzeba oznaczenia określonych widoków, do których można dostać się dopiero na podstawie logiki w presenterze. Czyli ponownie, aby nie można było ich otworzyć bezpośrednio: +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 @@ -114,7 +114,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Konkretne akcje --------------- -Możesz również ograniczyć, że określony kod, na przykład utworzenie komponentu, będzie dostępny tylko dla specyficznych akcji w presenterze: +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 @@ -126,15 +126,15 @@ class EditDeletePresenter extends Nette\Application\UI\Presenter } ``` -W przypadku jednej akcji nie trzeba zapisywać tablicy: `#[Requires(actions: 'default')]` +W przypadku jednej akcji nie trzeba pisać tablicy: `#[Requires(actions: 'default')]` Własne atrybuty --------------- -Jeśli chcesz użyć atrybutu `#[Requires]` wielokrotnie z tym samym ustawieniem, możesz stworzyć własny atrybut, który będzie dziedziczył `#[Requires]` i ustawi go według potrzeb. +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]` umożliwi dostęp tylko przez akcję `default`: +Na przykład `#[SingleAction]` zezwala na dostęp tylko przez akcję `default`: ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -Lub `#[RestMethods]` umożliwi dostęp przez wszystkie metody HTTP używane dla REST API: +Albo `#[RestMethods]` zezwoli na dostęp wszystkimi metodami HTTP używanymi w REST API: ```php #[\Attribute] @@ -171,7 +171,7 @@ class ApiPresenter extends Nette\Application\UI\Presenter ``` -Zakończenie ------------ +Podsumowanie +------------ -Atrybut `#[Requires]` daje Ci dużą elastyczność i kontrolę nad tym, jak dostępne są Twoje strony internetowe. Za pomocą prostych, ale potężnych reguł możesz zwiększyć bezpieczeństwo i prawidłowe funkcjonowanie Twojej aplikacji. Jak widzisz, użycie atrybutów w Nette może Twoją pracę nie tylko ułatwić, ale i zabezpieczyć. +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 index f8ccf099f8..377fa24df7 100644 --- a/best-practices/pl/composer.texy +++ b/best-practices/pl/composer.texy @@ -1,12 +1,12 @@ -Composer: wskazówki dotyczące użytkowania -***************************************** +Composer: wskazówki użycia +************************** <div class=perex> -Composer to narzędzie do zarządzania zależnościami w PHP. Umożliwia nam zdefiniowanie bibliotek, od których zależy nasz projekt, i będzie je za nas instalować oraz aktualizować. Pokażemy: +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 -- jego użycie w nowym lub istniejącym projekcie +- jak używać go w nowym albo istniejącym projekcie </div> @@ -14,31 +14,31 @@ Composer to narzędzie do zarządzania zależnościami w PHP. Umożliwia nam zde Instalacja ========== -Composer to plik wykonywalny `.phar`, który pobierzesz i zainstalujesz w następujący sposób: +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]. +Użyj oficjalnego instalatora [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. Linux, macOS ------------ -Wystarczą 4 polecenia, które skopiujesz z [tej strony |https://getcomposer.org/download/]. +Wystarczą 4 polecenia, które możesz skopiować z [tej strony |https://getcomposer.org/download/]. -Następnie, umieszczając go w folderze, który znajduje się w systemowym `PATH`, Composer stanie się dostępny globalnie: +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 # lub /usr/local/bin/composer +$ mv ./composer.phar ~/bin/composer # albo /usr/local/bin/composer ``` Użycie w projekcie ================== -Aby móc w swoim projekcie zacząć używać Composera, potrzebujesz tylko pliku `composer.json`. Opisuje on zależności naszego projektu i może również zawierać inne metadane. Podstawowy `composer.json` może więc wyglądać tak: +Ż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 { @@ -48,17 +48,17 @@ Aby móc w swoim projekcie zacząć używać Composera, potrzebujesz tylko pliku } ``` -Mówimy tutaj, że nasza aplikacja (lub biblioteka) wymaga pakietu `nette/database` (nazwa pakietu składa się z nazwy organizacji i nazwy projektu) i chce wersji, która odpowiada warunkowi `^3.0` (tj. najnowszej wersji 3). +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). -Mamy więc w katalogu głównym projektu plik `composer.json` i uruchamiamy instalację: +Mając więc plik `composer.json` w katalogu głównym projektu, uruchom: ```shell composer update ``` -Composer pobierze Nette Database do folderu `vendor/`. Następnie utworzy plik `composer.lock`, który zawiera informacje o tym, które wersje bibliotek dokładnie zainstalował. +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`, który możemy po prostu dołączyć i zacząć używać bibliotek bez żadnej dodatkowej pracy: +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'; @@ -70,17 +70,17 @@ $db = new Nette\Database\Connection('sqlite::memory:'); Aktualizacja pakietów do najnowszych wersji =========================================== -Za aktualizację używanych bibliotek do najnowszych wersji zgodnie z warunkami zdefiniowanymi w `composer.json` odpowiada polecenie `composer update`. Np. przy zależności `"nette/database": "^3.0"` zainstaluje najnowszą wersję 3.x.x, ale już nie wersję 4. +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. -Aby zaktualizować warunki w pliku `composer.json`, na przykład na `"nette/database": "^4.1"`, aby można było zainstalować najnowszą wersję, użyj polecenia `composer require nette/database`. +Ż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`. -Aby zaktualizować wszystkie używane pakiety Nette, trzeba by je wszystkie wymienić w wierszu poleceń, np.: +Ż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 ... ``` -Co jest niepraktyczne. Użyj dlatego prostego skryptu "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, który to zrobi za Ciebie: +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 @@ -90,29 +90,29 @@ php composer-frontline.php Tworzenie nowego projektu ========================= -Nowy projekt Nette utworzysz za pomocą jednego polecenia: +Nowy projekt Nette utworzysz jednym poleceniem: ```shell composer create-project nette/web-project nazwa-projektu ``` -Jako `nazwa-projektu` wstaw nazwę katalogu dla swojego projektu i potwierdź. Composer pobierze repozytorium `nette/web-project` z GitHubu, które już zawiera plik `composer.json`, a zaraz potem Nette Framework. Powinno już wystarczyć tylko [ustawić uprawnienia |nette:troubleshooting#Ustawianie uprawnień do katalogów] do zapisu w folderach `temp/` i `log/`, a projekt powinien ożyć. +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, nie zapomnij [jej ustawić |#Wersja PHP]. +Jeśli wiesz, na jakiej wersji PHP projekt będzie hostowany, koniecznie ją [ustaw |#Wersja PHP]. Wersja PHP ========== -Composer zawsze instaluje te wersje pakietów, które są kompatybilne z wersją PHP, której właśnie używasz (a raczej z wersją PHP używaną w wierszu poleceń podczas uruchamiania Composera). Co jednak najprawdopodobniej nie jest tą samą wersją, której używa Twój hosting. Dlatego bardzo ważne jest dodanie do pliku `composer.json` informacji o wersji PHP na hostingu. Wtedy będą instalowane tylko wersje pakietów kompatybilne z hostingiem. +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. -To, że projekt będzie działał na przykład na PHP 8.2.3, ustawimy poleceniem: +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 ``` -W ten sposób wersja zostanie zapisana do pliku `composer.json`: +Wersja zostanie zapisana w pliku `composer.json` w ten sposób: ```js { @@ -124,13 +124,13 @@ W ten sposób wersja zostanie zapisana do pliku `composer.json`: } ``` -Jednak numer wersji PHP podaje się jeszcze w innym miejscu pliku, a mianowicie w sekcji `require`. Podczas gdy pierwszy numer określa, dla jakiej wersji będą instalowane pakiety, drugi numer mówi, dla jakiej wersji jest napisana sama aplikacja. A według niego na przykład PhpStorm ustawia *PHP language level*. (Oczywiście nie ma sensu, aby te wersje się różniły, więc podwójny zapis jest niedopatrzeniem.) Tę wersję ustawisz poleceniem: +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 ``` -Lub bezpośrednio w pliku `composer.json`: +Albo bezpośrednio w pliku `composer.json`: ```js { @@ -144,51 +144,51 @@ Lub bezpośrednio w pliku `composer.json`: Ignorowanie wersji PHP ====================== -Pakiety zazwyczaj mają podaną zarówno najniższą wersję PHP, z którą są kompatybilne, jak i najwyższą, z którą są testowane. Jeśli zamierzasz używać wersji PHP jeszcze nowszej, na przykład w celu testowania, Composer odmówi zainstalowania takiego pakietu. Rozwiązaniem jest opcja `--ignore-platform-req=php+`, która spowoduje, że Composer będzie ignorować górne limity wymaganej 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 =================== -Podczas aktualizacji pakietów lub zmian numerów wersji zdarza się, że dochodzi do konfliktu. Jeden pakiet ma wymagania, które są sprzeczne z innym i podobnie. Composer jednak czasami wypisuje fałszywe komunikaty. Zgłasza konflikt, który realnie nie istnieje. W takim przypadku pomaga usunięcie pliku `composer.lock` i spróbowanie ponownie. +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 błędu nadal się pojawia, to jest on myśleny poważnie i trzeba z niego wyczytać, co i jak zmodyfikować. +Jeśli komunikat o błędzie nie znika, jest prawdziwy i trzeba go przeczytać, żeby zrozumieć, co i jak zmodyfikować. -Packagist.org - centralne repozytorium -====================================== +Packagist.org - globalne repozytorium +===================================== -[Packagist |https://packagist.org] to główne repozytorium, w którym Composer stara się wyszukiwać pakiety, jeśli mu nie powiemy inaczej. Możemy tutaj publikować również własne pakiety. +[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. -Co jeśli nie chcemy używać centralnego repozytorium? ----------------------------------------------------- +A co, jeśli nie chcemy centralnego repozytorium +----------------------------------------------- -Jeśli mamy wewnętrzne aplikacje firmowe, których po prostu nie możemy hostować publicznie, to stworzymy dla nich firmowe 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 na temat repozytoriów [w oficjalnej dokumentacji |https://getcomposer.org/doc/05-repositories.md#repositories]. +Więcej o repozytoriach przeczytasz w [oficjalnej dokumentacji |https://getcomposer.org/doc/05-repositories.md#repositories]. Autoloading =========== -Zasadniczą cechą Composera jest to, że zapewnia autoloading dla wszystkich przez niego zainstalowanych klas, który uruchamiasz przez dołączenie pliku `vendor/autoload.php`. +Kluczową cechą Composera jest to, że zapewnia autoloading wszystkich klas, które instaluje. Aktywujesz go, dołączając plik `vendor/autoload.php`. -Jednak możliwe jest używanie Composera również do ładowania innych klas spoza folderu `vendor`. Pierwszą możliwością jest pozwolenie Composerowi przeszukać zdefiniowane foldery i podfoldery, znaleźć wszystkie klasy i dołączyć je do autoloadera. Osiągniesz to ustawiając `autoload > classmap` w `composer.json`: +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/", # dołączy folder src/ i jego podfoldery + "src/", # włącza katalog src/ i jego podkatalogi ] } } ``` -Następnie przy każdej zmianie trzeba uruchomić polecenie `composer dumpautoload` i pozwolić na przegenerowanie tabel autoloadingu. To jest niezwykle niewygodne i znacznie lepiej jest powierzyć to zadanie [RobotLoaderowi|robot-loader:], który tę samą czynność wykonuje automatycznie w tle i znacznie szybciej. +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. -Drugą możliwością jest przestrzeganie [PSR-4|https://www.php-fig.org/psr/psr-4/]. Uproszczając, chodzi o system, w którym przestrzenie nazw i nazwy klas odpowiadają strukturze katalogów i nazwom plików, czyli np. `App\Core\RouterFactory` będzie w pliku `/path/to/App/Core/RouterFactory.php`. Przykład konfiguracji: +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 { @@ -200,13 +200,13 @@ Drugą możliwością jest przestrzeganie [PSR-4|https://www.php-fig.org/psr/psr } ``` -Jak dokładnie skonfigurować zachowanie dowiesz się w [dokumentacji Composera|https://getcomposer.org/doc/04-schema.md#psr-4]. +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. Jak to zrobić? Najpierw do pliku `composer.json` dodaj tę parę opcji, która pozwoli instalować wersje rozwojowe pakietów, jednak ucieknie się do tego tylko w przypadku, gdy nie istnieje żadna kombinacja stabilnych wersji, która spełniałaby wymagania: +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 { @@ -215,21 +215,21 @@ Chcesz przetestować nową wersję rozwojową pakietu. Jak to zrobić? Najpierw } ``` -Następnie zalecamy usunięcie pliku `composer.lock`, czasami bowiem Composer niezrozumiale odmawia instalacji i to rozwiązuje problem. +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` i nowa wersja ma numer 4.0. Zainstalujesz ją poleceniem: +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 ``` -Lub możesz zainstalować konkretną wersję, na przykład 4.0.0-RC2: +Albo możesz zainstalować konkretną wersję, na przykład 4.0.0-RC2: ```shell composer require nette/utils:4.0.0-RC2 ``` -Gdy jednak od biblioteki zależy inny pakiet, który jest zablokowany na starszej wersji (np. `^3.1`), to idealnie jest zaktualizować pakiet, aby działał z nową wersją. Jeśli jednak chcesz tylko obejść ograniczenie i zmusić Composera do zainstalowania wersji rozwojowej i udawania, że jest to wersja starsza (np. 3.1.6), możesz użyć słowa kluczowego `as`: +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" @@ -239,9 +239,9 @@ composer require nette/utils "4.0.x-dev as 3.1.6" Wywoływanie poleceń =================== -Przez Composer można wywoływać własne przygotowane polecenia i skrypty, jakby były to natywne polecenia Composera. W przypadku skryptów, które znajdują się w folderze `vendor/bin`, nie trzeba podawać tego folderu. +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 zdefiniujemy w pliku `composer.json` skrypt, który za pomocą [Nette Testera|tester:] uruchomi testy: +Jako przykład zdefiniujmy w `composer.json` skrypt, który za pomocą [Nette Testera |tester:] uruchamia testy: ```js { @@ -251,13 +251,13 @@ Jako przykład zdefiniujemy w pliku `composer.json` skrypt, który za pomocą [N } ``` -Testy następnie uruchomimy za pomocą `composer tester`. Polecenie możemy wywołać również w przypadku, gdy nie jesteśmy w folderze głównym projektu, ale w którymś podkatalogu. +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ękowania +Wyślij podziękowanie ==================== -Pokażemy Ci sztuczkę, którą ucieszysz autorów open source. W prosty sposób dasz na GitHubie gwiazdkę bibliotekom, których używa Twój projekt. Wystarczy zainstalować bibliotekę `symfony/thanks`: +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 @@ -269,13 +269,13 @@ A następnie uruchomić: composer thanks ``` -Spróbuj! +Wypróbuj! Konfiguracja ============ -Composer jest ściśle powiązany z narzędziem do wersjonowania [Git |https://git-scm.com]. Jeśli go nie masz zainstalowanego, trzeba powiedzieć Composerowi, aby go nie używał: +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 index d5aeaa3c1c..e5c3750b0a 100644 --- a/best-practices/pl/creating-editing-form.texy +++ b/best-practices/pl/creating-editing-form.texy @@ -2,15 +2,15 @@ Formularz do tworzenia i edycji rekordu *************************************** .[perex] -Jak poprawnie zaimplementować w Nette dodawanie i edycję rekordu, wykorzystując ten sam formularz do obu operacji? +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ę np. tylko etykietą na przycisku. Pokażemy przykłady prostych presenterów, gdzie formularz użyjemy najpierw do dodawania rekordu, potem do edycji, a na końcu połączymy oba rozwiązania. +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 dodawania rekordu. Samą pracę z bazą danych pozostawimy klasie `Facade`, której kod nie jest istotny dla przykładu. +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 @@ -29,11 +29,11 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... dodajemy pola formularza ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // dodanie rekordu do bazy danych $this->flashMessage('Pomyślnie dodano'); @@ -51,7 +51,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter Edycja rekordu -------------- -Teraz pokażemy, jak wyglądałby presenter służący do edycji rekordu: +Zobaczmy teraz, jak wyglądałby presenter służący do edycji rekordu: ```php @@ -70,8 +70,8 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // weryfikacja istnienia rekordu - || !$this->facade->isEditAllowed(/*...*/) // kontrola uprawnień + !$record // weryfikujemy istnienie rekordu + || !$this->facade->isEditAllowed(/*...*/) // sprawdzamy uprawnienia ) { $this->error(); // błąd 404 } @@ -81,7 +81,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter protected function createComponentRecordForm(): Form { - // sprawdzamy, czy akcja to 'edit' + // weryfikujemy, że akcja to 'edit' if ($this->getAction() !== 'edit') { $this->error(); } @@ -90,12 +90,12 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... dodajemy pola formularza ... - $form->setDefaults($this->record); // ustawienie wartości domyślnych - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->setDefaults($this->record); // ustawiamy wartości domyślne + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->update($this->record->id, $data); // aktualizacja rekordu $this->flashMessage('Pomyślnie zaktualizowano'); @@ -104,9 +104,9 @@ class RecordPresenter extends Nette\Application\UI\Presenter } ``` -W metodzie `actionEdit()`, która uruchamia się na samym początku [cyklu życia presentera |application:presenters#Cykl życia presentera], weryfikujemy istnienie rekordu i uprawnienia użytkownika do jego edycji. +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`, aby mieć go dostępnego w metodzie `createComponentRecordForm()` w celu ustawienia wartości domyślnych, oraz w `recordFormSucceeded()` ze względu na ID. Alternatywnym rozwiązaniem byłoby ustawienie wartości domyślnych bezpośrednio w `actionEdit()` i pobranie wartości ID, która jest częścią URL, za pomocą `getParameter('id')`: +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 @@ -114,12 +114,12 @@ Rekord zapisujemy do właściwości `$record`, aby mieć go dostępnego w metodz { $record = $this->facade->get($id); if ( - // weryfikacja istnienia i kontrola uprawnień + // weryfikujemy istnienie i sprawdzamy uprawnienia ) { $this->error(); } - // ustawienie wartości domyślnych formularza + // ustawiamy wartości domyślne formularza $this->getComponent('recordForm') ->setDefaults($record); } @@ -130,16 +130,15 @@ Rekord zapisujemy do właściwości `$record`, aby mieć go dostępnego w metodz $this->facade->update($id, $data); // ... } -} ``` -Jednakże, i to powinno być **najważniejszą lekcją całego kodu**, musimy podczas tworzenia formularza upewnić się, że akcja to rzeczywiście `edit`. W przeciwnym razie weryfikacja w metodzie `actionEdit()` w ogóle by nie została przeprowadzona! +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 --------------------------------------- -A teraz połączymy oba presentery w jeden. Albo moglibyśmy w metodzie `createComponentRecordForm()` rozróżnić, o którą akcję chodzi i na tej podstawie skonfigurować formularz, albo możemy to zostawić bezpośrednio metodom akcji i pozbyć się warunku: +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 @@ -153,27 +152,27 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // weryfikacja istnienia rekordu - || !$this->facade->isEditAllowed(/*...*/) // kontrola uprawnień + !$record // weryfikujemy istnienie rekordu + || !$this->facade->isEditAllowed(/*...*/) // sprawdzamy uprawnienia ) { $this->error(); // błąd 404 } $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // ustawienie wartości domyślnych - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->setDefaults($record); // ustawiamy wartości domyślne + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form { - // sprawdzamy, czy akcja to 'add' lub 'edit' + // weryfikujemy, że akcja to 'add' albo 'edit' if (!in_array($this->getAction(), ['add', 'edit'])) { $this->error(); } @@ -185,14 +184,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // dodanie rekordu do bazy danych $this->flashMessage('Pomyślnie dodano'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); $this->facade->update($id, $data); // aktualizacja rekordu diff --git a/best-practices/pl/dynamic-snippets.texy b/best-practices/pl/dynamic-snippets.texy index 67ac6ba7c0..3144061cd0 100644 --- a/best-practices/pl/dynamic-snippets.texy +++ b/best-practices/pl/dynamic-snippets.texy @@ -1,7 +1,10 @@ Dynamiczne snippety ******************* -Dość często podczas tworzenia aplikacji pojawia się potrzeba wykonywania operacji AJAX, na przykład na poszczególnych wierszach tabeli lub elementach listy. Jako przykład możemy wybrać listę artykułów, przy czym dla każdego z nich umożliwimy zalogowanemu użytkownikowi wybranie oceny "lubię/nie lubię". Kod presentera i odpowiadającego mu szablonu bez AJAX będzie wyglądał mniej więcej tak (podaję najważniejsze fragmenty, kod zakłada istnienie usługi do oznaczania ocen i pobierania kolekcji artykułów - konkretna implementacja nie jest ważna dla celów tego poradnika): +.[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 @@ -26,24 +29,24 @@ Szablon: {if !$article->liked} <a n:href="like! $article->id" class=ajax>lubię to</a> {else} - <a n:href="unlike! $article->id" class=ajax>już mi się to nie podoba</a> + <a n:href="unlike! $article->id" class=ajax>już tego nie lubię</a> {/if} </article> ``` -Ajaxizacja -========== +Ajaksowanie +=========== -Teraz wyposażmy tę prostą aplikację w AJAX. Zmiana oceny artykułu nie jest na tyle ważna, aby musiało dojść do przekierowania, dlatego idealnie powinna odbywać się za pomocą AJAX w tle. Wykorzystamy [skrypt obsługi z dodatków |application:ajax#Naja] ze zwyczajową konwencją, że linki AJAX mają klasę CSS `ajax`. +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`. -Jednak jak to zrobić konkretnie? Nette oferuje 2 ścieżki: ścieżkę tzw. dynamicznych snippetów i ścieżkę komponentów. Obie mają swoje zalety i wady, dlatego pokażemy je po kolei. +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. -Ścieżka dynamicznych snippetów -============================== +Sposób z dynamicznymi snippetami +================================ -Dynamiczny snippet w terminologii Latte oznacza specyficzny przypadek użycia znacznika `{snippet}`, gdzie w nazwie snippetu używana jest zmienna. Taki snippet nie może znajdować się w szablonie byle gdzie - musi być opakowany statycznym snippetem, tj. zwykłym, lub wewnątrz `{snippetArea}`. Nasz szablon moglibyśmy zmodyfikować w następujący sposób. +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 @@ -55,16 +58,16 @@ Dynamiczny snippet w terminologii Latte oznacza specyficzny przypadek użycia zn {if !$article->liked} <a n:href="like! $article->id" class=ajax>lubię to</a> {else} - <a n:href="unlike! $article->id" class=ajax>już mi się to nie podoba</a> + <a n:href="unlike! $article->id" class=ajax>już tego nie lubię</a> {/if} {/snippet} </article> {/snippet} ``` -Każdy artykuł definiuje teraz jeden snippet, który ma w nazwie ID artykułu. Wszystkie te snippety są następnie razem opakowane jednym snippetem o nazwie `articlesContainer`. Gdybyśmy pominęli ten opakowujący snippet, Latte poinformowałoby nas o tym wyjątkiem. +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 nam uzupełnić w prezenterze przerysowanie - wystarczy przerysować statyczną otoczkę. +Pozostaje już tylko dodać do presentera logikę przerysowywania: wystarczy przerysować statyczny wrapper. ```php public function handleLike(int $articleId): void @@ -72,18 +75,18 @@ public function handleLike(int $articleId): void $this->ratingService->saveLike($articleId, $this->user->id); if ($this->isAjax()) { $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- nie jest potrzebne + // $this->redrawControl('article-' . $articleId); -- niepotrzebne } else { $this->redirect('this'); } } ``` -Podobnie zmodyfikujemy również siostrzaną metodę `handleUnlike()`, i AJAX działa! +Analogicznie zmodyfikuj odpowiadającą metodę `handleUnlike()` i AJAX działa! -Rozwiązanie ma jednak jedną wadę. Gdybyśmy bardziej zbadali, jak przebiega żądanie AJAX, odkrylibyśmy, że chociaż na zewnątrz aplikacja wydaje się oszczędna (zwraca tylko jeden snippet dla danego artykułu), w rzeczywistości na serwerze wyrenderowała wszystkie snippety. Pożądany snippet umieściła w payloadzie, a pozostałe odrzuciła (całkowicie niepotrzebnie je również pobrała z bazy danych). +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). -Aby zoptymalizować ten proces, będziemy musieli interweniować tam, gdzie przekazujemy do szablonu kolekcję `$articles` (powiedzmy w metodzie `renderDefault()`). Wykorzystamy fakt, że przetwarzanie sygnałów odbywa się przed metodami `render<Something>`: +Ż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 @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -Teraz podczas przetwarzania sygnału do szablonu przekazywana jest zamiast kolekcji ze wszystkimi artykułami tylko tablica z jednym artykułem - tym, który chcemy wyrenderować i wysłać w payloadzie do przeglądarki. `{foreach}` przebiegnie więc tylko raz i żadne dodatkowe snippety się nie wyrenderują. +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. -Ścieżka komponentów -=================== +Sposób z komponentami +===================== -Zupełnie inny sposób rozwiązania unika dynamicznych snippetów. Sztuczka polega na przeniesieniu całej logiki do osobnego komponentu - od teraz o wprowadzanie ocen nie będzie dbał presenter, ale dedykowany `LikeControl`. Klasa będzie wyglądać następująco (oprócz tego będzie zawierać również metody `render`, `handleUnlike` itd.): +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 @@ -141,12 +144,12 @@ Szablon komponentu: {if !$article->liked} <a n:href="like!" class=ajax>lubię to</a> {else} - <a n:href="unlike!" class=ajax>już mi się to nie podoba</a> + <a n:href="unlike!" class=ajax>już tego nie lubię</a> {/if} {/snippet} ``` -Oczywiście zmieni nam się szablon widoku i do presentera będziemy musieli dodać fabrykę. Ponieważ komponent utworzymy tyle razy, ile artykułów pobierzemy z bazy danych, wykorzystamy do jego "rozmnożenia" klasę [Multiplier |application:Multiplier]. +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() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -Szablon widoku zmniejszy się do niezbędnego minimum (i całkowicie pozbawiony snippetów!): +Szablon widoku kurczy się do absolutnego minimum (i jest całkowicie wolny od snippetów!): ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ Szablon widoku zmniejszy się do niezbędnego minimum (i całkowicie pozbawiony </article> ``` -Mamy prawie gotowe: aplikacja teraz będzie działać AJAXowo. Również tutaj czeka nas optymalizacja aplikacji, ponieważ ze względu na użycie Nette Database podczas przetwarzania sygnału niepotrzebnie ładowane są wszystkie artykuły z bazy danych zamiast jednego. Zaletą jest jednak to, że nie dojdzie do ich renderowania, ponieważ wyrenderuje się rzeczywiście tylko nasz komponent. +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/editors-and-tools.texy b/best-practices/pl/editors-and-tools.texy deleted file mode 100644 index 7488231779..0000000000 --- a/best-practices/pl/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Edytory i narzędzia -******************* - -.[perex] -Możesz być biegłym programistą, ale dopiero z dobrymi narzędziami staniesz się mistrzem. W tym rozdziale znajdziesz wskazówki dotyczące ważnych narzędzi, edytorów i wtyczek. - - -Edytor IDE -========== - -Zdecydowanie zalecamy używanie do programowania pełnoprawnego IDE, takiego jak PhpStorm, NetBeans, VS Code, a nie tylko edytora tekstu z obsługą PHP. Różnica jest naprawdę zasadnicza. Nie ma powodu zadowalać się zwykłym edytorem, który co prawda potrafi kolorować składnię, ale nie dorównuje możliwościom zaawansowanego IDE, które precyzyjnie podpowiada, pilnuje błędów, potrafi refaktoryzować kod i wiele więcej. Niektóre IDE są płatne, inne nawet darmowe. - -**NetBeans IDE** ma wbudowane wsparcie dla Nette, Latte i NEON. - -**PhpStorm**: zainstaluj te wtyczki w `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: znajdź w marketplace wtyczkę "Nette Latte + Neon". - -Połącz również Tracy z edytorem. Podczas wyświetlania strony błędu będzie można kliknąć na nazwy plików, a te otworzą się w edytorze z kursorem na odpowiedniej linii. Przeczytaj, [jak skonfigurować system|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan to narzędzie, które wykrywa błędy logiczne w kodzie, zanim go uruchomisz. - -Zainstalujemy go za pomocą Composera: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Utworzymy w projekcie plik konfiguracyjny `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -A następnie zlecimy mu analizę klas w folderze `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Wyczerpującą dokumentację znajdziesz bezpośrednio na [stronie PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] sprawdza i ewentualnie poprawia niektóre błędy formalne w twoich kodach źródłowych: - -- usuwa [BOM |nette:glossary#BOM] -- sprawdza poprawność szablonów [Latte |latte:] -- sprawdza poprawność plików `.neon`, `.php` i `.json` -- sprawdza występowanie [znaków kontrolnych |nette:glossary#Znaki kontrolne] -- sprawdza, czy plik jest kodowany w UTF-8 -- sprawdza błędnie zapisane `/* @anotace */` (brakuje gwiazdki) -- usuwa kończące `?>` w plikach PHP -- usuwa spacje na końcu linii i zbędne linie na końcu pliku -- normalizuje separatory linii do systemowych (jeśli podasz opcję `-l`) - - -Composer -======== - -[Composer|best-practices:composer] to narzędzie do zarządzania zależnościami w PHP. Pozwala nam deklarować dowolnie złożone zależności poszczególnych bibliotek, a następnie instaluje je za nas w naszym projekcie. - - -Requirements Checker -==================== - -Było to narzędzie, które testowało środowisko uruchomieniowe serwera i informowało, czy (i w jakim stopniu) można używać frameworka. Obecnie Nette można używać na każdym serwerze, który ma minimalną wymaganą wersję PHP. diff --git a/best-practices/pl/form-reuse.texy b/best-practices/pl/form-reuse.texy index c4b5179419..a723016c5d 100644 --- a/best-practices/pl/form-reuse.texy +++ b/best-practices/pl/form-reuse.texy @@ -1,16 +1,16 @@ -Wielokrotne użycie formularzy w wielu miejscach -*********************************************** +Ponowne wykorzystanie formularzy w wielu miejscach +************************************************** .[perex] -W Nette masz do dyspozycji kilka opcji, jak użyć tego samego formularza w wielu miejscach i nie duplikować kodu. W tym artykule pokażemy różne rozwiązania, w tym te, których powinieneś unikać. +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 ================== -Jednym z podstawowych podejść do użycia tego samego komponentu w wielu miejscach jest utworzenie metody lub klasy, która generuje ten komponent, a następnie wywoływanie tej metody w różnych miejscach aplikacji. Taka metoda lub klasa nazywana jest *fabryką*. Proszę nie mylić z wzorcem projektowym *factory method*, który opisuje specyficzny sposób wykorzystania fabryk i nie jest związany z tym tematem. +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 stworzymy fabrykę, która będzie budować formularz edycyjny: +Jako przykład utwórzmy fabrykę budującą formularz edycyjny: ```php use Nette\Application\UI\Form; @@ -22,20 +22,20 @@ class FormFactory $form = new Form; $form->addText('title', 'Tytuł:'); // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Wyślij'); + $form->addSubmit('send', 'Zapisz'); return $form; } } ``` -Teraz możesz użyć tej fabryki w różnych miejscach w swojej aplikacji, na przykład w presenterach lub komponentach. A to tak, że [zażądamy jej jako zależności|dependency-injection:passing-dependencies]. Najpierw więc zapiszemy klasę do pliku konfiguracyjnego: +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żyjemy jej w prezenterze: +A potem użyj jej w presenterze: ```php @@ -57,7 +57,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Fabrykę formularzy możesz rozszerzyć o kolejne metody do tworzenia innych rodzajów formularzy zgodnie z potrzebami Twojej aplikacji. I oczywiście możemy dodać również metodę, która stworzy podstawowy formularz bez elementów, a tę będą wykorzystywać inne metody: +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 @@ -73,19 +73,19 @@ class FormFactory $form = $this->createForm(); $form->addText('title', 'Tytuł:'); // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Wyślij'); + $form->addSubmit('send', 'Zapisz'); return $form; } } ``` -Metoda `createForm()` na razie nie robi nic użytecznego, ale to się szybko zmieni. +Metoda `createForm()` nie robi jeszcze nic szczególnie pożytecznego, ale to się wkrótce zmieni. Zależności fabryki ================== -Z czasem okaże się, że potrzebujemy, aby formularze były wielojęzyczne. Oznacza to, że wszystkim formularzom musimy ustawić tzw. [translator |forms:rendering#Tłumaczenie]. W tym celu zmodyfikujemy klasę `FormFactory` tak, aby przyjmowała obiekt `Translator` jako zależność w konstruktorze, i przekażemy go formularzowi: +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; @@ -108,13 +108,13 @@ class FormFactory } ``` -Ponieważ metodę `createForm()` wywołują również inne metody tworzące specyficzne formularze, wystarczy translator ustawić tylko w niej. I gotowe. Nie ma potrzeby zmieniać kodu żadnego presentera ani komponentu, co jest świetne. +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. -Wiele klas fabryk -================= +Więcej klas fabryk +================== -Alternatywnie możesz utworzyć wiele klas dla każdego formularza, który chcesz użyć w swojej aplikacji. Takie podejście może zwiększyć czytelność kodu i ułatwić zarządzanie formularzami. Pierwotną `FormFactory` pozostawimy do tworzenia tylko czystego formularza z podstawową konfiguracją (na przykład ze wsparciem tłumaczeń), a dla formularza edycyjnego stworzymy nową fabrykę `EditFormFactory`. +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 @@ -145,16 +145,16 @@ class EditFormFactory { $form = $this->formFactory->create(); // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Wyślij'); + $form->addSubmit('send', 'Zapisz'); return $form; } } ``` -Bardzo ważne jest, aby powiązanie między klasami `FormFactory` i `EditFormFactory` było realizowane przez [kompozycję |nette:introduction-to-object-oriented-programming#Kompozycja], a nie przez [dziedziczenie obiektowe |nette:introduction-to-object-oriented-programming#Dziedziczenie]: +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 -// ⛔ TAK NIE! DZIEDZICZENIE TU NIE PASUJE +// ⛔ NIE! DZIEDZICZENIE TU NIE PASUJE class EditFormFactory extends FormFactory { public function create(): Form @@ -162,21 +162,21 @@ class EditFormFactory extends FormFactory $form = parent::create(); $form->addText('title', 'Tytuł:'); // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Wyślij'); + $form->addSubmit('send', 'Zapisz'); return $form; } } ``` -Użycie dziedziczenia byłoby w tym przypadku całkowicie kontrproduktywne. Na problemy napotkałbyś bardzo szybko. Na przykład w chwili, gdy chciałbyś dodać parametry do metody `create()`; PHP zgłosiłoby błąd, że jej sygnatura różni się od rodzicielskiej. Lub przy przekazywaniu zależności do klasy `EditFormFactory` przez konstruktor. Powstałaby sytuacja, którą nazywamy [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. +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]. -Ogólnie lepiej jest preferować [kompozycję nad dziedziczeniem |dependency-injection:faq#Dlaczego preferuje się kompozycję nad dziedziczeniem]. +Generalnie lepiej preferować [kompozycję nad dziedziczeniem |dependency-injection:faq#Dlaczego kompozycja jest preferowana nad dziedziczeniem?]. Obsługa formularza ================== -Obsługa formularza, która jest wywoływana po pomyślnym wysłaniu, może być również częścią klasy fabryki. Będzie działać tak, że przekaże wysłane dane do modelu w celu przetworzenia. Ewentualne błędy [przekazuje z powrotem |forms:validation#Błędy podczas przetwarzania] do formularza. Model w poniższym przykładzie reprezentuje klasa `Facade`: +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 @@ -192,12 +192,12 @@ class EditFormFactory $form = $this->formFactory->create(); $form->addText('title', 'Tytuł:'); // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Wyślij'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addSubmit('send', 'Zapisz'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // przetwarzanie wysłanych danych @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Samo przekierowanie pozostawimy jednak prezenterowi. Ten doda do zdarzenia `onSuccess` kolejny handler, który wykona przekierowanie. Dzięki temu będzie można użyć formularza w różnych prezenterach i w każdym przekierować gdzie indziej. +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 @@ -232,16 +232,16 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -To rozwiązanie wykorzystuje właściwość formularzy, że gdy na formularzu lub jego elemencie zostanie wywołane `addError()`, kolejne handlery `onSuccess` nie są już wywoływane. +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 od klasy Form -=========================== +Dziedziczenie po klasie Form +============================ -Zbudowany formularz nie powinien być potomkiem formularza. Innymi słowy, nie używaj tego rozwiązania: +Zbudowany formularz nie powinien być potomkiem klasy `Form`. Innymi słowy, unikaj takiego podejścia: ```php -// ⛔ TAK NIE! DZIEDZICZENIE TU NIE PASUJE +// ⛔ NIE! DZIEDZICZENIE TU NIE PASUJE class EditForm extends Form { public function __construct(Translator $translator) @@ -249,7 +249,7 @@ class EditForm extends Form parent::__construct(); $this->addText('title', 'Tytuł:'); // tutaj dodawane są kolejne pola formularza - $this->addSubmit('send', 'Wyślij'); + $this->addSubmit('send', 'Zapisz'); $this->setTranslator($translator); } } @@ -257,13 +257,13 @@ class EditForm extends Form Zamiast budować formularz w konstruktorze, użyj fabryki. -Należy zdać sobie sprawę, że klasa `Form` jest przede wszystkim narzędziem do budowania formularza, czyli *form builder*. A zbudowany formularz można rozumieć jako jej produkt. Jednak produkt nie jest specyficznym przypadkiem buildera, nie ma między nimi relacji *is a* stanowiącej podstawę dziedziczenia. +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 ======================= -Całkowicie inne podejście stanowi tworzenie [komponentu|application:components], którego częścią jest formularz. Daje to nowe możliwości, na przykład renderowanie formularza w specyficzny sposób, ponieważ częścią komponentu jest również szablon. Lub można wykorzystać sygnały do komunikacji AJAX i doładowywania informacji do formularza, na przykład do podpowiadania, itd. +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 @@ -283,13 +283,13 @@ class EditControl extends Nette\Application\UI\Control $form = new Form; $form->addText('title', 'Tytuł:'); // tutaj dodawane są kolejne pola formularza - $form->addSubmit('send', 'Wyślij'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addSubmit('send', 'Zapisz'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // przetwarzanie wysłanych danych @@ -306,7 +306,7 @@ class EditControl extends Nette\Application\UI\Control } ``` -Stworzymy jeszcze fabrykę, która będzie produkować ten komponent. Wystarczy [zapisać jej interfejs |application:components#Komponenty z zależnościami]: +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 @@ -315,14 +315,14 @@ interface EditControlFactory } ``` -I dodać do pliku konfiguracyjnego: +I dodać go do pliku konfiguracyjnego: ```neon services: - EditControlFactory ``` -A teraz już możemy zażądać fabryki i użyć jej w prezenterze: +Teraz możemy poprosić o fabrykę i użyć jej w presenterze: ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // lub przekierowujemy na wynik edycji, np.: + // albo przekierowanie na wynik edycji, np.: // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/pl/inject-method-attribute.texy b/best-practices/pl/inject-method-attribute.texy index a5889d2155..b21bb41ab2 100644 --- a/best-practices/pl/inject-method-attribute.texy +++ b/best-practices/pl/inject-method-attribute.texy @@ -2,17 +2,17 @@ Metody i atrybuty inject ************************ .[perex] -W tym artykule skupimy się na różnych sposobach przekazywania zależności do presenterów w frameworku Nette. Porównamy preferowany sposób, którym jest konstruktor, z innymi możliwościami, takimi jak metody i atrybuty `inject`. +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`. -Również dla presenterów obowiązuje zasada, że przekazywanie zależności za pomocą [konstruktora |dependency-injection:passing-dependencies#Przekazywanie przez konstruktor] jest preferowaną ścieżką. Jeśli jednak tworzysz wspólnego przodka, z którego dziedziczą inne presentery (np. `BasePresenter`), i ten przodek również ma zależności, pojawia się problem, który nazywamy [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Można go obejść za pomocą alternatywnych ścieżek, które stanowią metody i atrybuty (dawniej adnotacje) `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 [setter |dependency-injection:passing-dependencies#Przekazywanie przez setter]. Nazwa tych setterów zaczyna się prefiksem `inject`. Nette DI automatycznie wywołuje tak nazwane metody zaraz po utworzeniu instancji presentera i przekazuje im wszystkie wymagane zależności. Muszą być zatem zadeklarowane jako public. +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 uznać za pewnego rodzaju rozszerzenie konstruktora na wiele metod. Dzięki temu `BasePresenter` może przyjąć zależności przez inną metodę i pozostawić konstruktor wolny dla swoich potomków: +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 @@ -36,13 +36,13 @@ class MyPresenter extends BasePresenter } ``` -Metod `inject*()` presenter może zawierać dowolną liczbę, a każda może mieć dowolną liczbę parametrów. Świetnie sprawdzają się również w przypadkach, gdy presenter jest [złożony z traitów |presenter-traits], a każdy z nich wymaga własnej zależności. +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#Ustawienie właściwości]. Wystarczy oznaczyć, do których zmiennych ma nastąpić wstrzyknięcie, a Nette DI automatycznie przekaże zależności zaraz po utworzeniu instancji presentera. Aby mógł je wstawić, konieczne jest zadeklarowanie ich jako public. +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 */`) @@ -52,10 +52,10 @@ use Nette\DI\Attributes\Inject; // ta linia jest ważna class MyPresenter extends Nette\Application\UI\Presenter { #[Inject] - public Nette\Caching\Cache $cache; // Zmiana typu na Nette\Caching\Cache dla spójności z Cache + public Cache $cache; } ``` -Zaletą tego sposobu przekazywania zależności była bardzo oszczędna forma zapisu. Jednak wraz z pojawieniem się [constructor property promotion |https://blog.nette.org/pl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] wydaje się łatwiejsze użycie konstruktora. +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 przekazywanie zależności do właściwości ogólnie: nie mamy kontroli nad zmianami w zmiennej, a jednocześnie zmienna staje się częścią publicznego interfejsu klasy, co jest niepożądane. +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 index 4381796a10..a3e08a1aa3 100644 --- a/best-practices/pl/lets-create-contact-form.texy +++ b/best-practices/pl/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -Tworzymy formularz kontaktowy +Stwórzmy formularz kontaktowy ***************************** .[perex] -Zobaczymy, jak w Nette stworzyć formularz kontaktowy, w tym wysyłanie na e-mail. Zatem do dzieła! +Zobaczmy, jak w Nette utworzyć formularz kontaktowy wraz z wysyłaniem wpisanych danych e-mailem. No to do dzieła! -Najpierw musimy stworzyć nowy projekt. Jak to zrobić, wyjaśnia strona [Pierwsze kroki |nette:installation]. A potem już możemy zacząć tworzyć formularz. +Najpierw musimy utworzyć nowy projekt. Jak to zrobić, wyjaśnia strona [Pierwsze kroki |nette:installation]. Potem możemy zabrać się za tworzenie formularza. -Najprościej jest stworzyć [formularz bezpośrednio w prezenterze |forms:in-presenter]. Możemy wykorzystać przygotowany `HomePresenter`. Dodamy do niego komponent `contactForm` reprezentujący formularz. Zrobimy to tak, że do kodu wpiszemy metodę fabryczną `createComponentContactForm()`, która wyprodukuje komponent: +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; @@ -18,26 +18,26 @@ class HomePresenter extends Presenter { $form = new Form; $form->addText('name', 'Imię:') - ->setRequired('Proszę podać imię'); + ->setRequired('Podaj swoje imię'); $form->addEmail('email', 'E-mail:') - ->setRequired('Proszę podać e-mail'); - $form->addTextarea('message', 'Wiadomość:') - ->setRequired('Proszę wpisać wiadomość'); + ->setRequired('Podaj swój e-mail'); + $form->addTextArea('message', 'Wiadomość:') + ->setRequired('Wpisz wiadomość'); $form->addSubmit('send', 'Wyślij'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // wysłanie e-maila } } ``` -Jak widzisz, stworzyliśmy dwie metody. Pierwsza metoda `createComponentContactForm()` tworzy nowy formularz. Ma on pola na imię, e-mail i wiadomość, które dodajemy metodami `addText()`, `addEmail()` i `addTextArea()`. Dodaliśmy również przycisk do wysłania formularza. Ale co jeśli użytkownik nie wypełni jakiegoś pola? W takim przypadku powinniśmy go poinformować, że jest to pole obowiązkowe. Osiągnęliśmy to metodą `setRequired()`. Na koniec dodaliśmy również [zdarzenie |nette:glossary#Eventy zdarzenia] `onSuccess`, które uruchamia się, jeśli formularz zostanie pomyślnie wysłany i jest poprawny. W naszym przypadku wywołuje metodę `contactFormSucceeded`, która zajmie się przetwarzaniem wysłanego formularza. Uzupełnimy to w kodzie za chwilę. +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` wyrenderujemy w szablonie `Home/default.latte`: +Komponent `contactForm` wyrenderujmy w szablonie `Home/default.latte`: ```latte {block content} @@ -45,12 +45,9 @@ Komponent `contactForm` wyrenderujemy w szablonie `Home/default.latte`: {control contactForm} ``` -Do samego wysłania e-maila stworzymy nową klasę, którą nazwiemy `ContactFacade` i umieścimy ją w pliku `app/Model/ContactFacade.php`: +Do samego wysyłania e-maila utworzymy nową klasę o nazwie `ContactFacade` i umieścimy ją w pliku `app/Model/ContactFacade.php`: ```php -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -66,7 +63,7 @@ class ContactFacade public function sendMessage(string $email, string $name, string $message): void { $mail = new Message; - $mail->addTo('admin@example.com') // twój e-mail + $mail->addTo('admin@example.com') // Twój e-mail ->setFrom($email, $name) ->setSubject('Wiadomość z formularza kontaktowego') ->setBody($message); @@ -76,9 +73,9 @@ class ContactFacade } ``` -Metoda `sendMessage()` tworzy i wysyła e-mail. Wykorzystuje do tego tzw. mailer, który przyjmuje jako zależność przez konstruktor. Przeczytaj więcej o [wysyłaniu e-maili |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:]. -Teraz wrócimy do presentera i dokończymy metodę `contactFormSucceeded()`. Wywoła ona metodę `sendMessage()` klasy `ContactFacade` i przekaże jej dane z formularza. A jak uzyskać obiekt `ContactFacade`? Przyjmiemy go przez konstruktor: +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; @@ -106,16 +103,16 @@ class HomePresenter extends Presenter } ``` -Po wysłaniu e-maila jeszcze wyświetlimy użytkownikowi tzw. [flash message |application:components#Wiadomości flash], potwierdzającą, że wiadomość została wysłana, a następnie przekierujemy z powrotem na tę samą stronę, aby nie było możliwe ponowne wysłanie formularza za pomocą *refresh* w przeglądarce. +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. -Tak, i jeśli wszystko działa, powinieneś być w stanie wysłać e-mail z twojego formularza kontaktowego. Gratulacje! +No i jeśli wszystko jest ustawione poprawnie, powinieneś już móc wysłać e-mail ze swojego formularza kontaktowego. Gratulacje! -Szablon HTML e-maila --------------------- +Szablon e-maila w HTML +---------------------- -Na razie wysyłany jest prosty tekstowy e-mail zawierający tylko wiadomość wysłaną formularzem. W e-mailu możemy jednak wykorzystać HTML i uczynić jego wygląd bardziej atrakcyjnym. Stworzymy dla niego szablon w Latte, który zapiszemy w `app/Model/contactEmail.latte`: +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> @@ -129,7 +126,7 @@ Na razie wysyłany jest prosty tekstowy e-mail zawierający tylko wiadomość wy </html> ``` -Pozostaje zmodyfikować `ContactFacade`, aby używał tego szablonu. W konstruktorze zażądamy klasy `LatteFactory`, która potrafi stworzyć obiekt `Latte\Engine`, czyli [renderera szablonów Latte |latte:develop#Jak renderować szablon]. Za pomocą metody `renderToString()` wyrenderujemy szablon do stringa, pierwszym parametrem jest ścieżka do szablonu, a drugim są parametry. +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; @@ -156,7 +153,7 @@ class ContactFacade ]); $mail = new Message; - $mail->addTo('admin@example.com') // twój e-mail + $mail->addTo('admin@example.com') // Twój e-mail ->setFrom($email, $name) ->setHtmlBody($body); @@ -165,15 +162,15 @@ class ContactFacade } ``` -Wygenerowany e-mail HTML przekażemy następnie metodzie `setHtmlBody()` zamiast pierwotnej `setBody()`. Również nie musimy podawać tematu e-maila w `setSubject()`, ponieważ biblioteka pobierze go z elementu `<title>` szablonu. +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 `<title>` w szablonie. -Konfiguracja ------------- +Konfigurowanie +-------------- -W kodzie klasy `ContactFacade` nadal jest na sztywno zapisany nasz e-mail administratora `admin@example.com`. Lepiej byłoby przenieść go do pliku konfiguracyjnego. Jak to zrobić? +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 ciąg znaków z e-mailem zastąpimy zmienną przekazaną przez konstruktor: +Najpierw zmodyfikujemy klasę `ContactFacade` i zamiast zapisanego na sztywno ciągu z e-mailem użyjemy zmiennej przekazanej przez konstruktor: ```php class ContactFacade @@ -197,21 +194,21 @@ class ContactFacade } ``` -A drugim krokiem jest podanie wartości tej zmiennej w konfiguracji. Do pliku `app/config/services.neon` zapiszemy: +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` byłoby dużo i mielibyście wrażenie, że e-mail ginie wśród nich, możemy uczynić go parametrem. Zmodyfikujemy zapis na: +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 w pliku `app/config/common.neon` zdefiniujemy tę zmienną: +A ten parametr zdefiniujemy w pliku `app/config/common.neon`: ```neon parameters: diff --git a/best-practices/pl/microsites.texy b/best-practices/pl/microsites.texy index 72b1666dae..e88b04fc25 100644 --- a/best-practices/pl/microsites.texy +++ b/best-practices/pl/microsites.texy @@ -1,11 +1,11 @@ -Jak tworzyć mikro-strony -************************ +Jak pisać mikrostrony +********************* -Wyobraź sobie, że potrzebujesz szybko stworzyć małą stronę internetową na nadchodzące wydarzenie Twojej firmy. Ma być prosta, szybka i bez zbędnych komplikacji. Możesz pomyśleć, że do tak małego projektu nie potrzebujesz solidnego frameworka. Ale co jeśli użycie frameworka Nette może ten proces zasadniczo uprościć i przyspieszyć? +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ć? -Przecież nawet przy tworzeniu prostych stron internetowych nie chcesz rezygnować z wygody. Nie chcesz wymyślać tego, co już zostało raz rozwiązane. Bądź spokojnie leniwy i pozwól się rozpieszczać. Nette Framework można świetnie wykorzystać również jako micro framework. +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 taka mikrostroń może wyglądać? Na przykład tak, że cały kod strony umieścimy w jednym pliku `index.php` w folderze publicznym (`www`): +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 @@ -16,7 +16,7 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// utwórz kontener DI na podstawie konfiguracji w config.neon +// tworzymy kontener DI na podstawie konfiguracji w config.neon $configurator->addConfig(__DIR__ . '/../app/config.neon'); $container = $configurator->createContainer(); @@ -29,35 +29,35 @@ $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); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); // trasa dla URL https://example.com/cs lub https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { +$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; }); -// uruchom aplikację! +// uruchamiamy aplikację! $container->getByType(Nette\Application\Application::class)->run(); ``` -Wszystko pozostałe to szablony zapisane w nadrzędnym folderze `/templates`. +Wszystko pozostałe to będą szablony zapisane w nadrzędnym katalogu `/templates`. -Kod PHP w `index.php` najpierw [przygotowuje środowisko |bootstrap:], następnie definiuje [trasy |application:routing#Dynamiczne routowanie z callbackami] i na końcu uruchamia aplikację. Zaletą jest to, że drugi parametr funkcji `addRoute()` może być callable, który zostanie wykonany po otwarciu odpowiedniej strony. +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 mikrostroń? +Dlaczego używać Nette do mikrostron? ------------------------------------ -- Programiści, którzy kiedykolwiek wypróbowali [Tracy|tracy:], dziś nie wyobrażają sobie, że mogliby coś programować bez niej. -- Przede wszystkim jednak wykorzystasz system szablonów [Latte|latte:], ponieważ już od 2 stron będziesz chciał mieć oddzielony [layout i treść|latte:template-inheritance]. -- I zdecydowanie chcesz polegać na [automatycznym escapowaniu |latte:safety-first], aby nie powstała podatność XSS. -- Nette również zapewni, że w przypadku błędu nigdy nie pojawią się programistyczne komunikaty błędów PHP, ale zrozumiała dla użytkownika strona. -- Jeśli chcesz zbierać informacje zwrotne od użytkowników, na przykład w postaci formularza kontaktowego, to jeszcze dodasz [formularze|forms:] i [bazę danych|database:]. -- Wypełnione formularze możesz również łatwo [wysyłać e-mailem|mail:]. -- Czasami może przydać się [cache|caching:], na przykład jeśli pobierasz i wyświetlasz feedy. +- 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 dzisiejszych czasach, gdy szybkość i efektywność są kluczowe, ważne jest posiadanie narzędzi, które pozwolą Ci osiągnąć wyniki bez zbędnego opóźnienia. Nette framework oferuje właśnie to - szybki rozwój, bezpieczeństwo i szeroką gamę narzędzi, takich jak Tracy i Latte, które upraszczają proces. Wystarczy zainstalować kilka pakietów Nette, a zbudowanie takiej mikrostroń staje się nagle dziecinnie proste. I wiesz, że nigdzie nie kryje się żadna dziura bezpieczeństwa. +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 index b79dc18dce..54b4856b8b 100644 --- a/best-practices/pl/pagination.texy +++ b/best-practices/pl/pagination.texy @@ -1,10 +1,10 @@ -Paginacja wyników bazy danych -***************************** +Stronicowanie wyników z bazy danych +*********************************** .[perex] -Podczas tworzenia aplikacji internetowych bardzo często spotkasz się z wymogiem ograniczenia liczby wyświetlanych elementów na stronie. +Przy tworzeniu aplikacji webowych bardzo często napotkasz wymaganie, żeby ograniczyć liczbę wypisywanych pozycji na stronie. Technika ta nazywa się stronicowaniem. -Wyjdziemy ze stanu, w którym wyświetlamy wszystkie dane bez paginacji. Do wyboru danych z bazy danych mamy klasę `ArticleRepository`, która oprócz konstruktora zawiera metodę `findPublishedArticles`, zwracającą wszystkie opublikowane artykuły posortowane malejąco według daty publikacji. +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; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -W prezenterze następnie wstrzykujemy klasę modelu, a w metodzie render pobieramy opublikowane artykuły, które przekazujemy do szablonu: +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; @@ -52,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -W szablonie `default.latte` zajmujemy się następnie wyświetlaniem artykułów: +Szablon `default.latte` zajmie się już wypisaniem artykułów: ```latte {block content} @@ -67,11 +67,11 @@ W szablonie `default.latte` zajmujemy się następnie wyświetlaniem artykułów ``` -W ten sposób potrafimy wyświetlić wszystkie artykuły, co jednak zacznie sprawiać problemy w momencie, gdy liczba artykułów wzrośnie. W tym momencie przyda się implementacja mechanizmu paginacji. +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. -Zapewni on, że wszystkie artykuły zostaną podzielone na kilka stron, a my wyświetlimy tylko artykuły z jednej bieżącej strony. Całkowitą liczbę stron i podział artykułów obliczy [Paginator |utils:Paginator] sam na podstawie tego, ile artykułów mamy łącznie i ile artykułów na stronę chcemy wyświetlić. +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 metodę do pobierania artykułów w klasie repozytorium tak, aby potrafiła zwracać tylko artykuły dla jednej strony. Dodamy również metodę do sprawdzania całkowitej liczby artykułów w bazie danych, której będziemy potrzebować do ustawienia Paginatora: +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; @@ -99,7 +99,7 @@ class ArticleRepository } /** - * Zwraca całkowitą liczbę opublikowanych artykułów + * Zwraca łączną liczbę opublikowanych artykułów */ public function getPublishedArticlesCount(): int { @@ -108,9 +108,9 @@ class ArticleRepository } ``` -Następnie przystąpimy do modyfikacji presentera. Do metody render będziemy przekazywać numer aktualnie wyświetlanej strony jako parametr. W przypadku, gdy ten numer nie będzie częścią URL, ustawimy domyślną wartość pierwszej strony. +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). -Dalej rozszerzymy również metodę render o uzyskanie instancji Paginatora, jego ustawienie i wybór odpowiednich artykułów do wyświetlenia w szablonie. `HomePresenter` po modyfikacjach będzie wyglądał tak: +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; @@ -127,27 +127,27 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Sprawdzamy całkowitą liczbę opublikowanych artykułów + // pobieramy łączną liczbę opublikowanych artykułów $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Tworzymy instancję Paginatora i ustawiamy ją + // tworzymy i konfigurujemy instancję Paginatora $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // całkowita liczba artykułów - $paginator->setItemsPerPage(10); // liczba elementów na stronie - $paginator->setPage($page); // numer bieżącej strony + $paginator->setItemCount($articlesCount); // łączna liczba pozycji + $paginator->setItemsPerPage(10); // liczba pozycji na stronie + $paginator->setPage($page); // numer aktualnej strony - // Pobieramy z bazy danych ograniczony zestaw artykułów zgodnie z obliczeniami Paginatora + // pobieramy z bazy ograniczony zbiór artykułów według wyliczenia Paginatora $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - // który przekazujemy do szablonu + // przekazujemy je do szablonu $this->template->articles = $articles; - // a także sam Paginator do wyświetlania opcji paginacji + // a także sam Paginator do wyświetlenia opcji stronicowania $this->template->paginator = $paginator; } } ``` -Szablon już teraz iteruje tylko po artykułach jednej strony, wystarczy nam dodać linki paginacji: +Szablon iteruje teraz tylko po artykułach z aktualnej strony. Musimy jeszcze dodać odnośniki stronicowania: ```latte {block content} @@ -164,7 +164,7 @@ Szablon już teraz iteruje tylko po artykułach jednej strony, wystarczy nam dod {if !$paginator->isFirst()} <a n:href="default, 1">Pierwsza</a>  |  - <a n:href="default, $paginator->page-1">Poprzednia</a> + <a n:href="default, $paginator->getPage() - 1">Poprzednia</a>  |  {/if} @@ -180,9 +180,9 @@ Szablon już teraz iteruje tylko po artykułach jednej strony, wystarczy nam dod ``` -W ten sposób uzupełniliśmy stronę o możliwość paginacji za pomocą Paginatora. W przypadku, gdy zamiast [Nette Database Core |database:sql-way] jako warstwę bazodanową użyjemy [Nette Database Explorer |database:explorer], jesteśmy w stanie zaimplementować paginację również bez użycia Paginatora. Klasa `Nette\Database\Table\Selection` bowiem zawiera metodę [page() |api:Nette\Database\Table\Selection::page()], która implementuje logikę paginacji. +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. -Repozytorium przy tym sposobie implementacji będzie wyglądać tak: +Przy takim podejściu repozytorium będzie wyglądać tak: ```php namespace App\Model; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -W prezenterze nie musimy tworzyć Paginatora, użyjemy zamiast niego metody `page()` klasy `Selection`, którą zwraca repozytorium: +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; @@ -222,21 +222,21 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Pobieramy opublikowane artykuły + // pobieramy opublikowane artykuły $articles = $this->articleRepository->findPublishedArticles(); - // i do szablonu wysyłamy tylko ich część ograniczoną zgodnie z obliczeniami metody page + // i przekazujemy do szablonu tylko ich część ograniczoną wyliczeniem metody page $lastPage = 0; $this->template->articles = $articles->page($page, 10, $lastPage); - // a także potrzebne dane do wyświetlania opcji paginacji + // a także dane potrzebne do wyświetlenia opcji stronicowania $this->template->page = $page; $this->template->lastPage = $lastPage; } } ``` -Ponieważ do szablonu teraz nie wysyłamy obiektu Paginator, zmodyfikujemy część wyświetlającą linki paginacji: +Ponieważ do szablonu nie przekazujemy już obiektu Paginatora, musimy dostosować część wyświetlającą odnośniki stronicowania: ```latte {block content} @@ -268,6 +268,6 @@ Ponieważ do szablonu teraz nie wysyłamy obiektu Paginator, zmodyfikujemy czę </div> ``` -W ten sposób zaimplementowaliśmy mechanizm paginacji bez użycia Paginatora. +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 index 2c8ef4b1f7..722fc6b7f8 100644 --- a/best-practices/pl/passing-settings-to-presenters.texy +++ b/best-practices/pl/passing-settings-to-presenters.texy @@ -2,9 +2,9 @@ Przekazywanie ustawień do presenterów ************************************* .[perex] -Potrzebujesz przekazywać do presenterów argumenty, które nie są obiektami (np. informację, czy działa w trybie debugowania, ścieżki do katalogów itp.), a więc nie mogą być przekazane automatycznie za pomocą autowiringu? Rozwiązaniem jest zamknięcie ich w obiekcie `Settings`. +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` stanowi bardzo łatwy, a zarazem użyteczny sposób dostarczania informacji o działającej aplikacji presenterom. Jej konkretna postać zależy wyłącznie od Twoich konkretnych potrzeb. Przykład: +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; @@ -12,7 +12,7 @@ namespace App; class Settings { public function __construct( - // od PHP 8.1 można użyć readonly + // od PHP 8.1 można używać readonly public bool $debugMode, public string $appDir, // i tak dalej @@ -30,7 +30,7 @@ services: ) ``` -Gdy presenter będzie potrzebował informacji dostarczanych przez tę usługę, po prostu poprosi o nią w konstruktorze: +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 diff --git a/best-practices/pl/post-links.texy b/best-practices/pl/post-links.texy index 257edf12d6..a4f6d15cb3 100644 --- a/best-practices/pl/post-links.texy +++ b/best-practices/pl/post-links.texy @@ -1,16 +1,16 @@ -Jak poprawnie używać linków POST -******************************** +Jak poprawnie używać odnośników POST +************************************ .[perex] -W aplikacjach internetowych, zwłaszcza w interfejsach administracyjnych, podstawową zasadą powinno być, że akcje zmieniające stan serwera nie powinny być wykonywane za pomocą metody HTTP GET. Jak sama nazwa metody wskazuje, GET powinien służyć wyłącznie do pobierania danych, a nie do ich zmiany. Dla akcji takich jak na przykład usuwanie rekordów bardziej odpowiednie jest użycie metody POST. Chociaż idealna byłaby metoda DELETE, ale tej nie można wywołać bez JavaScriptu, dlatego historycznie używa się POST. +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 zrobić w praktyce? Wykorzystaj ten prosty trik. Na początku szablonu stworzysz pomocniczy formularz z identyfikatorem `postForm`, który następnie użyjesz do przycisków usuwania: +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 możesz zamiast klasycznego linku `<a>` użyć przycisku `<button>`, który można wizualnie dostosować tak, aby wyglądał jak zwykły link. Na przykład framework CSS Bootstrap oferuje klasy `btn btn-link`, dzięki którym osiągniesz to, że przycisk nie będzie wizualnie różnił się od innych linków. Za pomocą atrybutu `form="postForm"` powiążemy go z przygotowanym formularzem: +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> @@ -24,7 +24,7 @@ Dzięki temu formularzowi możesz zamiast klasycznego linku `<a>` użyć przycis </table> ``` -Po kliknięciu na link zostanie teraz wywołana akcja `delete`. Aby zapewnić, że żądania będą przyjmowane wyłącznie za pomocą metody POST i z tej samej domeny (co jest skuteczną obroną przed atakami CSRF), użyj atrybutu `#[Requires]`: +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; @@ -40,9 +40,9 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Atrybut istnieje od Nette Application 3.2, a więcej o jego możliwościach dowiesz się na stronie [Jak używać atrybutu #Requires |attribute-requires]. +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()`, nie jest konieczne podawanie `sameOrigin: true`, ponieważ sygnały mają tę ochronę ustawioną domyślnie: +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')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -Takie podejście nie tylko poprawia bezpieczeństwo Twojej aplikacji, ale także przyczynia się do przestrzegania prawidłowych standardów i praktyk internetowych. Wykorzystując metody POST do akcji zmieniających stan, osiągniesz bardziej solidną i bezpieczniejszą aplikację. +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 index d819871207..6a21bf1726 100644 --- a/best-practices/pl/presenter-traits.texy +++ b/best-practices/pl/presenter-traits.texy @@ -2,13 +2,13 @@ Składanie presenterów z traitów ******************************* .[perex] -Jeśli potrzebujemy w wielu presenterach zaimplementować ten sam kod (np. weryfikację, czy użytkownik jest zalogowany), można umieścić kod we wspólnym przodku. Drugą możliwością jest stworzenie jednofunkcyjnych [traitów |nette:introduction-to-object-oriented-programming#Traity]. +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ą tego rozwiązania jest to, że każdy z presenterów może użyć dokładnie tych traitów, których rzeczywiście potrzebuje, podczas gdy wielokrotne dziedziczenie nie jest możliwe w PHP. +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. -Te traity mogą wykorzystywać fakt, że przy tworzeniu presentera kolejno wywoływane są wszystkie [metody inject |inject-method-attribute#Metody inject]. Trzeba tylko dopilnować, aby nazwa każdej metody inject była unikalna. +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ą dołączyć kod inicjalizacyjny do zdarzeń [onStartup lub onRender |application:presenters#Zdarzenia]. +Traity mogą podpiąć kod inicjalizacyjny do zdarzeń [onStartup albo onRender |application:presenters#Zdarzenia]. Przykłady: @@ -36,7 +36,7 @@ trait StandardTemplateFilters } ``` -Presenter następnie po prostu używa tych traitów: +Presenter po prostu używa potem tych traitów: ```php class ArticlePresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/pl/pretty-urls.texy b/best-practices/pl/pretty-urls.texy new file mode 100644 index 0000000000..174fd176c6 --- /dev/null +++ b/best-practices/pl/pretty-urls.texy @@ -0,0 +1,204 @@ +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 index 4d279ee9bb..97875b6670 100644 --- a/best-practices/pl/restore-request.texy +++ b/best-practices/pl/restore-request.texy @@ -1,16 +1,16 @@ -Jak wrócić do poprzedniej strony? -********************************* +Jak wrócić do wcześniejszej strony? +*********************************** .[perex] -Co jeśli użytkownik wypełnia formularz i jego sesja wygaśnie? Aby nie stracił danych, przed przekierowaniem na stronę logowania zapiszemy żądanie w sesji. W Nette to bułka z masłem. +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. -Aktualne żądanie można zapisać w sesji za pomocą metody `storeRequest()`, która zwraca jego identyfikator w postaci krótkiego ciągu znaków. Metoda zapisuje nazwę aktualnego presentera, widoku i jego parametrów. W przypadku, gdy został również wysłany formularz, zapisywana jest także zawartość pól (z wyjątkiem przesłanych plików). +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). -Przywrócenie żądania wykonuje metoda `restoreRequest($key)`, której przekazujemy uzyskany identyfikator. Przekierowuje ona na pierwotny presenter i akcję. Jeśli jednak zapisane żądanie zawiera wysłanie formularza, przechodzi na pierwotny presenter metodą `forward()`, przekazuje formularzowi wcześniej wypełnione wartości i pozwala go ponownie wyrenderować. Użytkownik ma w ten sposób możliwość ponownego wysłania formularza i żadne dane się nie tracą. +Żą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. -Ważne jest, że `restoreRequest()` sprawdza, czy nowo zalogowany użytkownik jest tym samym, który pierwotnie wypełniał formularz. Jeśli nie, żądanie odrzuca i nic nie robi. +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żemy wszystko na przykładzie. Mamy presenter `AdminPresenter`, w którym edytuje się dane i w którego metodzie `startup()` weryfikujemy, czy użytkownik jest zalogowany. Jeśli nie, przekierowujemy go na `SignPresenter`. Jednocześnie zapisujemy aktualne żądanie i jego klucz wysyłamy do `SignPresenter`. +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 @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Presenter `SignPresenter` będzie oprócz formularza logowania zawierał również parametr persistentny `$backlink`, do którego zapisze się klucz. Ponieważ parametr jest persistentny, będzie przenoszony również po odesłaniu formularza logowania. +`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 @@ -41,11 +41,11 @@ class SignPresenter extends Nette\Application\UI\Presenter { $form = new Nette\Application\UI\Form; // ... dodajemy pola formularza ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { // ... tutaj logujemy użytkownika ... @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -Metodzie `restoreRequest()` przekazujemy klucz zapisanego żądania, a ona przekierowuje (lub przechodzi) na pierwotny presenter. +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 (na przykład już nie istnieje w sesji), metoda nic nie robi. Następuje więc wywołanie `$this->redirect('Admin:')`, które przekierowuje na `AdminPresenter`. +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/pt/@home.texy b/best-practices/pt/@home.texy deleted file mode 100644 index ff77ce2b7f..0000000000 --- a/best-practices/pt/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Guias e melhores práticas -************************* - -.[perex] -Guias, soluções para tarefas comuns e *melhores práticas* para Nette. - - -<div class=documentation> -<div> - - -Aplicação Nette ---------------- -- [Métodos e atributos inject |inject-method-attribute] -- [Composição de presenters a partir de traits |presenter-traits] -- [Passando configurações para presenters |passing-settings-to-presenters] -- [Como retornar a uma página anterior |restore-request] -- [Paginação de resultados do banco de dados |pagination] -- [Snippets dinâmicos |dynamic-snippets] -- [Como usar o atributo #Requires |attribute-requires] -- [Como usar corretamente links POST |post-links] - -</div> -<div> - - -Formulários ------------ -- [Reutilização de formulários |form-reuse] -- [Formulário para criar e editar registros |creating-editing-form] -- [Criando um formulário de contato |lets-create-contact-form] -- [Selectboxes dependentes |https://blog.nette.org/pt/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Geral ------ -- [Como carregar um arquivo de configuração |bootstrap:] -- [Como escrever microsites |microsites] -- [Por que o Nette usa a notação PascalCase para constantes? |https://blog.nette.org/pt/for-less-screaming-in-the-code] -- [Por que o Nette não usa o sufixo Interface? |https://blog.nette.org/pt/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: dicas de uso |composer] -- [Dicas sobre editores & ferramentas |editors-and-tools] -- [Introdução à programação orientada a objetos |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Solução de exemplo ------------------- -- [Exemplos Nette |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Exemplos Contributte |https://contributte.org/examples.html] -- [Site Doctrine ORM |https://github.com/MinecordNetwork/Website] -- [Início rápido |quickstart:] - -</div> -<div> - - -Vídeos ------- -Centenas de gravações dos Últimos Sábados e vídeos sobre Nette podem ser encontrados sob um mesmo teto no "Canal do Youtube Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/pt/@meta.texy b/best-practices/pt/@meta.texy deleted file mode 100644 index 1bf3200c6f..0000000000 --- a/best-practices/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Guias e melhores práticas}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/pt/attribute-requires.texy b/best-practices/pt/attribute-requires.texy deleted file mode 100644 index e128fa61b9..0000000000 --- a/best-practices/pt/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Como usar o atributo `#[Requires]` -********************************** - -.[perex] -Ao escrever uma aplicação web, você frequentemente encontrará a necessidade de restringir o acesso a certas partes da sua aplicação. Talvez você queira que algumas requisições possam enviar dados apenas através de um formulário (ou seja, pelo método POST), ou que sejam acessíveis apenas para chamadas AJAX. No Nette Framework 3.2, surgiu uma nova ferramenta que permite definir tais restrições de forma muito elegante e clara: o atributo `#[Requires]`. - -Um atributo é uma marca especial em PHP que você adiciona antes da definição de uma classe ou método. Como na verdade é uma classe, para que os exemplos a seguir funcionem, é necessário incluir a cláusula use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Você pode usar o atributo `#[Requires]` na própria classe do presenter e também nestes métodos: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Os dois últimos métodos também se aplicam a componentes, ou seja, você também pode usar o atributo neles. - -Se as condições especificadas pelo atributo não forem atendidas, um erro HTTP 4xx será lançado. - - -Métodos HTTP ------------- - -Você pode especificar quais métodos HTTP (como GET, POST, etc.) são permitidos para acesso. Por exemplo, se você quiser permitir o acesso apenas enviando um formulário, defina: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Por que você deve usar POST em vez de GET para ações que alteram o estado e como fazer isso? [Leia o tutorial |post-links]. - -Você pode especificar um método ou um array de métodos. Um caso especial é o valor `'*'`, que permite todos os métodos, o que os presenters normalmente não permitem por [razões de segurança |application:presenters#Verificação do método HTTP]. - - -Chamada AJAX ------------- - -Se você quiser que o presenter ou método esteja disponível apenas para requisições AJAX, use: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Mesma origem ------------- - -Para aumentar a segurança, você pode exigir que a requisição seja feita do mesmo domínio. Isso evita a [vulnerabilidade CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Para os métodos `handle<Signal>()`, o acesso do mesmo domínio é exigido automaticamente. Portanto, se, pelo contrário, você quiser permitir o acesso de qualquer domínio, especifique: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Acesso via forward ------------------- - -Às vezes, é útil restringir o acesso a um presenter para que ele esteja disponível apenas indiretamente, por exemplo, usando o método `forward()` ou `switch()` de outro presenter. Assim, por exemplo, protegem-se os error-presenters para que não possam ser chamados a partir da URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Na prática, muitas vezes é necessário marcar certas views às quais só se pode chegar com base na lógica do presenter. Ou seja, novamente, para que não possam ser abertas diretamente: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Ações específicas ------------------ - -Você também pode restringir que um determinado código, como a criação de um componente, esteja disponível apenas para ações específicas no presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -No caso de uma única ação, não é necessário escrever um array: `#[Requires(actions: 'default')]` - - -Atributos personalizados ------------------------- - -Se você quiser usar o atributo `#[Requires]` repetidamente com as mesmas configurações, pode criar seu próprio atributo que herdará `#[Requires]` e o configurará de acordo com as necessidades. - -Por exemplo, `#[SingleAction]` permitirá o acesso apenas através da ação `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Ou `#[RestMethods]` permitirá o acesso através de todos os métodos HTTP usados para APIs REST: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Conclusão ---------- - -O atributo `#[Requires]` oferece grande flexibilidade e controle sobre como suas páginas web são acessíveis. Usando regras simples, mas poderosas, você pode aumentar a segurança e o funcionamento correto da sua aplicação. Como você pode ver, o uso de atributos no Nette pode não apenas facilitar seu trabalho, mas também torná-lo mais seguro. diff --git a/best-practices/pt/composer.texy b/best-practices/pt/composer.texy deleted file mode 100644 index 0bbb245278..0000000000 --- a/best-practices/pt/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: dicas de uso -********************** - -<div class=perex> - -O Composer é uma ferramenta para gerenciamento de dependências em PHP. Ele nos permite listar as bibliotecas das quais nosso projeto depende e as instalará e atualizará para nós. Vamos mostrar: - -- como instalar o Composer -- seu uso em um projeto novo ou existente - -</div> - - -Instalação -========== - -O Composer é um arquivo `.phar` executável que você baixa e instala da seguinte maneira: - - -Windows -------- - -Use o instalador oficial [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Basta seguir 4 comandos que você pode copiar [desta página |https://getcomposer.org/download/]. - -Além disso, colocando-o em uma pasta que esteja no `PATH` do sistema, o Composer se torna acessível globalmente: - -```shell -$ mv ./composer.phar ~/bin/composer # ou /usr/local/bin/composer -``` - - -Uso no projeto -============== - -Para começar a usar o Composer em seu projeto, você só precisa do arquivo `composer.json`. Ele descreve as dependências do nosso projeto e também pode conter outros metadados. Um `composer.json` básico pode, portanto, parecer assim: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Aqui dizemos que nossa aplicação (ou biblioteca) requer o pacote `nette/database` (o nome do pacote consiste no nome da organização e no nome do projeto) e quer a versão que corresponde à condição `^3.0` (ou seja, a versão 3 mais recente). - -Temos, portanto, o arquivo `composer.json` na raiz do projeto e executamos a instalação: - -```shell -composer update -``` - -O Composer baixará o Nette Database para a pasta `vendor/`. Além disso, criará o arquivo `composer.lock`, que contém informações sobre quais versões exatas das bibliotecas ele instalou. - -O Composer gera o arquivo `vendor/autoload.php`, que podemos simplesmente incluir e começar a usar as bibliotecas sem qualquer trabalho adicional: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Atualização de pacotes para as versões mais recentes -==================================================== - -A atualização das bibliotecas usadas para as versões mais recentes, de acordo com as condições definidas em `composer.json`, é responsabilidade do comando `composer update`. Por exemplo, para a dependência `"nette/database": "^3.0"`, ele instalará a versão 3.x.x mais recente, mas não a versão 4. - -Para atualizar as condições no arquivo `composer.json`, por exemplo, para `"nette/database": "^4.1"`, para que seja possível instalar a versão mais recente, use o comando `composer require nette/database`. - -Para atualizar todos os pacotes Nette usados, seria necessário listá-los todos na linha de comando, por exemplo: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -O que é impraticável. Use, portanto, o script simples "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, que fará isso por você: - -```shell -php composer-frontline.php -``` - - -Criação de um novo projeto -========================== - -Você pode criar um novo projeto Nette com um único comando: - -```shell -composer create-project nette/web-project nome-do-projeto -``` - -Como `nome-do-projeto`, insira o nome do diretório para o seu projeto e confirme. O Composer baixará o repositório `nette/web-project` do GitHub, que já contém o arquivo `composer.json`, e logo depois o Nette Framework. Deve bastar apenas [definir as permissões |nette:troubleshooting#Configurando Permissões de Diretório] de escrita nas pastas `temp/` e `log/` e o projeto deve ganhar vida. - -Se você sabe em qual versão do PHP o projeto será hospedado, não se esqueça de [configurá-la |#Versão do PHP]. - - -Versão do PHP -============= - -O Composer sempre instala as versões dos pacotes que são compatíveis com a versão do PHP que você está usando atualmente (mais precisamente, com a versão do PHP usada na linha de comando ao executar o Composer). O que, no entanto, provavelmente não é a mesma versão que sua hospedagem usa. Por isso, é muito importante adicionar ao arquivo `composer.json` a informação sobre a versão do PHP na hospedagem. Depois disso, apenas as versões dos pacotes compatíveis com a hospedagem serão instaladas. - -Para definir que o projeto será executado, por exemplo, no PHP 8.2.3, usamos o comando: - -```shell -composer config platform.php 8.2.3 -``` - -Assim, a versão será escrita no arquivo `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -No entanto, o número da versão do PHP é especificado em outro local do arquivo, na seção `require`. Enquanto o primeiro número determina para qual versão os pacotes serão instalados, o segundo número diz para qual versão a própria aplicação foi escrita. E de acordo com ele, por exemplo, o PhpStorm define o *PHP language level*. (Claro, não faz sentido que essas versões sejam diferentes, então a dupla escrita é uma falha de design.) Você define esta versão com o comando: - -```shell -composer require php 8.2.3 --no-update -``` - -Ou diretamente no arquivo `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorar versão do PHP -===================== - -Os pacotes geralmente especificam tanto a versão mais baixa do PHP com a qual são compatíveis quanto a mais alta com a qual foram testados. Se você planeja usar uma versão do PHP ainda mais recente, talvez para fins de teste, o Composer se recusará a instalar tal pacote. A solução é a opção `--ignore-platform-req=php+`, que faz com que o Composer ignore os limites superiores da versão do PHP exigida. - - -Mensagens falsas -================ - -Ao atualizar pacotes ou alterar números de versão, pode ocorrer um conflito. Um pacote tem requisitos que estão em conflito com outro e assim por diante. Mas o Composer às vezes exibe uma mensagem falsa. Ele relata um conflito que realmente não existe. Nesse caso, ajuda excluir o arquivo `composer.lock` e tentar novamente. - -Se a mensagem de erro persistir, então ela é séria e é necessário ler nela o que e como ajustar. - - -Packagist.org - repositório central -=================================== - -[Packagist |https://packagist.org] é o repositório principal no qual o Composer tenta procurar pacotes, a menos que lhe digamos o contrário. Também podemos publicar nossos próprios pacotes aqui. - - -E se não quisermos usar o repositório central? ----------------------------------------------- - -Se tivermos aplicações internas da empresa que simplesmente não podemos hospedar publicamente, criaremos um repositório corporativo para elas. - -Mais sobre o tema de repositórios [na documentação oficial |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Uma característica fundamental do Composer é que ele fornece autoloading para todas as classes instaladas por ele, que você inicia incluindo o arquivo `vendor/autoload.php`. - -No entanto, é possível usar o Composer também para carregar outras classes fora da pasta `vendor`. A primeira opção é deixar o Composer pesquisar pastas e subpastas definidas, encontrar todas as classes e incluí-las no autoloader. Isso é alcançado definindo `autoload > classmap` em `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/" # inclui a pasta src/ e suas subpastas - ] - } -} -``` - -Posteriormente, é necessário executar o comando `composer dumpautoload` a cada alteração e deixar as tabelas de autoloading serem regeneradas. Isso é extremamente inconveniente e é muito melhor confiar esta tarefa ao [RobotLoader|robot-loader:], que realiza a mesma atividade automaticamente em segundo plano e muito mais rapidamente. - -A segunda opção é seguir o [PSR-4|https://www.php-fig.org/psr/psr-4/]. Simplificadamente, é um sistema onde namespaces e nomes de classes correspondem à estrutura de diretórios e nomes de arquivos, ou seja, por exemplo, `App\Core\RouterFactory` estará no arquivo `/path/to/App/Core/RouterFactory.php`. Exemplo de configuração: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # o namespace App\ está no diretório app/ - } - } -} -``` - -Como configurar exatamente o comportamento pode ser encontrado na [documentação do Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testando novas versões -====================== - -Você quer testar uma nova versão de desenvolvimento de um pacote. Como fazer isso? Primeiro, adicione este par de opções ao arquivo `composer.json`, que permite instalar versões de desenvolvimento de pacotes, mas recorrerá a isso apenas se não houver nenhuma combinação de versões estáveis que atenda aos requisitos: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true -} -``` - -Além disso, recomendamos excluir o arquivo `composer.lock`, às vezes o Composer inexplicavelmente se recusa a instalar e isso resolve o problema. - -Digamos que seja o pacote `nette/utils` e a nova versão tenha o número 4.0. Você a instala com o comando: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ou você pode instalar uma versão específica, por exemplo, 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Mas se outro pacote depender da biblioteca, que está bloqueada em uma versão mais antiga (por exemplo, `^3.1`), então o ideal é atualizar o pacote para que funcione com a nova versão. No entanto, se você quiser apenas contornar a restrição e forçar o Composer a instalar a versão de desenvolvimento e fingir que é uma versão mais antiga (por exemplo, 3.1.6), pode usar a palavra-chave `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Chamada de comandos -=================== - -Através do Composer, é possível chamar comandos e scripts próprios pré-preparados, como se fossem comandos nativos do Composer. Para scripts localizados na pasta `vendor/bin`, não é necessário especificar esta pasta. - -Como exemplo, definimos no arquivo `composer.json` um script que, usando o [Nette Tester|tester:], executa os testes: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Os testes são então executados usando `composer tester`. O comando pode ser chamado mesmo que não estejamos na pasta raiz do projeto, mas em algum subdiretório. - - -Envie agradecimentos -==================== - -Mostraremos um truque que agradará os autores de open source. De maneira simples, você pode dar uma estrela no GitHub às bibliotecas que seu projeto usa. Basta instalar a biblioteca `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -E depois executar: - -```shell -composer thanks -``` - -Experimente! - - -Configuração -============ - -O Composer está intimamente ligado à ferramenta de versionamento [Git |https://git-scm.com]. Se você não o tiver instalado, é necessário dizer ao Composer para não usá-lo: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/pt/creating-editing-form.texy b/best-practices/pt/creating-editing-form.texy deleted file mode 100644 index c08c92a039..0000000000 --- a/best-practices/pt/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Formulário para criar e editar um registro -****************************************** - -.[perex] -Como implementar corretamente a adição e edição de um registro no Nette, usando o mesmo formulário para ambos? - -Em muitos casos, os formulários para adicionar e editar um registro são os mesmos, diferindo talvez apenas no rótulo do botão. Mostraremos exemplos de presenters simples onde usaremos o formulário primeiro para adicionar um registro, depois para editar e, finalmente, combinaremos ambas as soluções. - - -Adicionar um registro ---------------------- - -Exemplo de um presenter usado para adicionar um registro. Deixaremos o trabalho real com o banco de dados para a classe `Facade`, cujo código não é essencial para a demonstração. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... adicionamos os campos do formulário ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // adiciona o registro ao banco de dados - $this->flashMessage('Adicionado com sucesso'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editar um registro ------------------- - -Agora mostraremos como seria um presenter usado para editar um registro: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verifica a existência do registro - || !$this->facade->isEditAllowed(/*...*/) // verifica a permissão - ) { - $this->error(); // erro 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // verificamos se a ação é 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... adicionamos os campos do formulário ... - - $form->setDefaults($this->record); // define os valores padrão - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // atualiza o registro - $this->flashMessage('Atualizado com sucesso'); - $this->redirect('...'); - } -} -``` - -No método `actionEdit`, que é executado logo no início do [ciclo de vida do presenter |application:presenters#Ciclo de vida do presenter], verificamos a existência do registro e a permissão do usuário para editá-lo. - -Armazenamos o registro na propriedade `$record` para tê-lo disponível no método `createComponentRecordForm()` para definir os valores padrão e em `recordFormSucceeded()` para o ID. Uma solução alternativa seria definir os valores padrão diretamente em `actionEdit()` e obter o valor do ID, que faz parte da URL, usando `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // verifica a existência e a permissão - ) { - $this->error(); - } - - // define os valores padrão do formulário - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -No entanto, e isso deve ser **o ponto mais importante de todo o código**, devemos garantir ao criar o formulário que a ação seja realmente `edit`. Caso contrário, a verificação no método `actionEdit()` não ocorreria de forma alguma! - - -O mesmo formulário para adicionar e editar ------------------------------------------- - -E agora combinamos ambos os presenters em um só. Poderíamos distinguir qual ação está sendo realizada no método `createComponentRecordForm()` e configurar o formulário de acordo, ou podemos deixar isso diretamente para os métodos de ação e nos livrar da condição: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verifica a existência do registro - || !$this->facade->isEditAllowed(/*...*/) // verifica a permissão - ) { - $this->error(); // erro 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // define os valores padrão - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // verificamos se a ação é 'add' ou 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... adicionamos os campos do formulário ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // adiciona o registro ao banco de dados - $this->flashMessage('Adicionado com sucesso'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // atualiza o registro - $this->flashMessage('Atualizado com sucesso'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/pt/dynamic-snippets.texy b/best-practices/pt/dynamic-snippets.texy deleted file mode 100644 index a77d207b02..0000000000 --- a/best-practices/pt/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Snippets dinâmicos -****************** - -Com bastante frequência, durante o desenvolvimento de aplicações, surge a necessidade de realizar operações AJAX, por exemplo, em linhas individuais de uma tabela ou itens de uma lista. Como exemplo, podemos escolher a exibição de artigos, onde permitimos que um usuário logado escolha a avaliação "gosto/não gosto" para cada um deles. O código do presenter e do template correspondente sem AJAX será aproximadamente o seguinte (apresento os trechos mais importantes, o código assume a existência de um serviço para marcar a avaliação e obter a coleção de artigos - a implementação específica não é importante para os fins deste tutorial): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Template: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Gosto disto</a> - {else} - <a n:href="unlike! $article->id" class=ajax>Já não gosto disto</a> - {/if} -</article> -``` - - -Ajaxificação -============ - -Vamos agora equipar esta aplicação simples com AJAX. A alteração da avaliação de um artigo não é tão importante a ponto de exigir um redirecionamento, e, portanto, idealmente, deveria ocorrer via AJAX em segundo plano. Usaremos o [script auxiliar dos add-ons |application:ajax#Naja] com a convenção usual de que os links AJAX têm a classe CSS `ajax`. - -Mas como fazer isso especificamente? O Nette oferece 2 caminhos: o caminho dos chamados snippets dinâmicos e o caminho dos componentes. Ambos têm seus prós e contras, e por isso vamos mostrá-los um por um. - - -Caminho dos snippets dinâmicos -============================== - -Um snippet dinâmico, na terminologia Latte, significa um caso específico de uso da tag `{snippet}`, onde uma variável é usada no nome do snippet. Tal snippet não pode estar em qualquer lugar no template - deve ser envolvido por um snippet estático, ou seja, um comum, ou dentro de `{snippetArea}`. Poderíamos modificar nosso template da seguinte forma. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Gosto disto</a> - {else} - <a n:href="unlike! $article->id" class=ajax>Já não gosto disto</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Cada artigo agora define um snippet que tem o ID do artigo em seu nome. Todos esses snippets são então agrupados em um único snippet chamado `articlesContainer`. Se omitíssemos este snippet envolvente, o Latte nos alertaria com uma exceção. - -Resta-nos adicionar o redesenho ao presenter - basta redesenhar o invólucro estático. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- não é necessário - } else { - $this->redirect('this'); - } -} -``` - -Modificamos de forma semelhante o método irmão `handleUnlike()`, e o AJAX está funcional! - -A solução, no entanto, tem um lado sombrio. Se investigássemos mais a fundo como a requisição AJAX ocorre, descobriríamos que, embora externamente a aplicação pareça econômica (retorna apenas um único snippet para o artigo em questão), na realidade, no servidor, ela renderizou todos os snippets. Ela colocou o snippet desejado no payload e descartou os outros (obtendo-os desnecessariamente do banco de dados também). - -Para otimizar este processo, teremos que intervir onde passamos a coleção `$articles` para o template (digamos, no método `renderDefault()`). Aproveitaremos o fato de que o processamento de sinais ocorre antes dos métodos `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Agora, ao processar o sinal, em vez de uma coleção com todos os artigos, apenas um array com um único artigo é passado para o template - aquele que queremos renderizar e enviar no payload para o navegador. O `{foreach}` então ocorrerá apenas uma vez e nenhum snippet extra será renderizado. - - -Caminho dos componentes -======================= - -Uma forma completamente diferente de solução evita os snippets dinâmicos. O truque consiste em transferir toda a lógica para um componente separado - a partir de agora, o presenter não será responsável pela inserção da avaliação, mas sim um `LikeControl` dedicado. A classe ficará assim (além disso, conterá também os métodos `render`, `handleUnlike`, etc.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Template do componente: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>Gosto disto</a> - {else} - <a n:href="unlike!" class=ajax>Já não gosto disto</a> - {/if} -{/snippet} -``` - -Claro, o template da view mudará e teremos que adicionar uma fábrica ao presenter. Como criaremos o componente tantas vezes quantos artigos obtivermos do banco de dados, usaremos a classe [application:Multiplier] para sua "multiplicação". - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -O template da view será reduzido ao mínimo necessário (e completamente livre de snippets!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Estamos quase lá: a aplicação agora funcionará com AJAX. Aqui também teremos que otimizar a aplicação, porque devido ao uso do Nette Database, ao processar o sinal, todos os artigos são carregados desnecessariamente do banco de dados em vez de apenas um. A vantagem, no entanto, é que eles não serão renderizados, pois apenas nosso componente será renderizado. - -{{priority: -1}} diff --git a/best-practices/pt/editors-and-tools.texy b/best-practices/pt/editors-and-tools.texy deleted file mode 100644 index 6d841136f7..0000000000 --- a/best-practices/pt/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editores & Ferramentas -********************** - -.[perex] -Você pode ser um programador habilidoso, mas é com boas ferramentas que você se torna um mestre. Neste capítulo, você encontrará dicas sobre ferramentas importantes, editores e plugins. - - -Editor IDE -========== - -Recomendamos fortemente o uso de um IDE completo para desenvolvimento, como PhpStorm, NetBeans, VS Code, e não apenas um editor de texto com suporte a PHP. A diferença é realmente fundamental. Não há razão para se contentar com um simples editor que, embora possa colorir a sintaxe, não atinge as capacidades de um IDE de ponta, que sugere com precisão, monitora erros, pode refatorar código e muito mais. Alguns IDEs são pagos, outros são até gratuitos. - -**NetBeans IDE** já vem com suporte integrado para Nette, Latte e NEON. - -**PhpStorm**: instale estes plugins em `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: encontre o plugin "Nette Latte + Neon" no marketplace. - -Conecte também o Tracy ao seu editor. Ao exibir uma página de erro, você poderá clicar nos nomes dos arquivos e eles serão abertos no editor com o cursor na linha correspondente. Leia [como configurar o sistema|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan é uma ferramenta que detecta erros lógicos no código antes mesmo de você executá-lo. - -Instalamos usando o Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Criamos um arquivo de configuração `phpstan.neon` no projeto: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -E, em seguida, deixamos que ele analise as classes na pasta `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Você encontrará documentação completa diretamente no [site do PHPStan |https://phpstan.org]. - - -Code Checker -============ - -O [Code Checker|code-checker:] verifica e, opcionalmente, corrige alguns erros formais em seus códigos-fonte: - -- remove [BOM |nette:glossary#BOM] -- verifica a validade dos templates [Latte |latte:] -- verifica a validade dos arquivos `.neon`, `.php` e `.json` -- verifica a ocorrência de [caracteres de controle |nette:glossary#Caracteres de controle] -- verifica se o arquivo está codificado em UTF-8 -- verifica `/* @anotações */` escritas incorretamente (falta um asterisco) -- remove `?>` de fechamento em arquivos PHP -- remove espaços em branco à direita e linhas desnecessárias no final do arquivo -- normaliza os separadores de linha para os do sistema (se você usar a opção `-l`) - - -Composer -======== - -[Composer|best-practices:composer] é uma ferramenta para gerenciamento de dependências em PHP. Permite declarar dependências arbitrariamente complexas de bibliotecas individuais e, em seguida, as instala para nós em nosso projeto. - - -Requirements Checker -==================== - -Era uma ferramenta que testava o ambiente de execução do servidor e informava se (e em que medida) o framework poderia ser usado. Atualmente, o Nette pode ser usado em qualquer servidor que tenha a versão mínima exigida do PHP. diff --git a/best-practices/pt/form-reuse.texy b/best-practices/pt/form-reuse.texy deleted file mode 100644 index a1c88ae4c5..0000000000 --- a/best-practices/pt/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Reutilização de formulários em vários lugares -********************************************* - -.[perex] -No Nette, você tem várias opções para usar o mesmo formulário em vários lugares sem duplicar o código. Neste artigo, mostraremos diferentes soluções, incluindo aquelas que você deve evitar. - - -Fábrica de formulários -====================== - -Uma das abordagens básicas para usar o mesmo componente em vários lugares é criar um método ou classe que gera esse componente e, em seguida, chamar esse método em diferentes lugares da aplicação. Tal método ou classe é chamado de *fábrica*. Por favor, não confunda com o padrão de projeto *factory method*, que descreve uma forma específica de usar fábricas e não está relacionado a este tópico. - -Como exemplo, criaremos uma fábrica que construirá um formulário de edição: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -Agora você pode usar esta fábrica em diferentes lugares da sua aplicação, por exemplo, em presenters ou componentes. E isso é feito [solicitando-a como dependência|dependency-injection:passing-dependencies]. Primeiro, registramos a classe no arquivo de configuração: - -```neon -services: - - FormFactory -``` - -E depois a usamos no presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // processamento dos dados enviados - }; - return $form; - } -} -``` - -Você pode estender a fábrica de formulários com outros métodos para criar outros tipos de formulários de acordo com as necessidades da sua aplicação. E, claro, também podemos adicionar um método que cria um formulário básico sem elementos, e os outros métodos o utilizarão: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -O método `createForm()` ainda não faz nada útil, mas isso mudará rapidamente. - - -Dependências da fábrica -======================= - -Com o tempo, percebe-se que precisamos que os formulários sejam multilíngues. Isso significa que precisamos definir o chamado [tradutor |forms:rendering#Tradução] para todos os formulários. Para isso, modificaremos a classe `FormFactory` para que ela aceite o objeto `Translator` como dependência no construtor e o passe para o formulário: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Como o método `createForm()` também é chamado por outros métodos que criam formulários específicos, basta definir o tradutor apenas nele. E está feito. Não é necessário alterar o código de nenhum presenter ou componente, o que é ótimo. - - -Múltiplas classes de fábrica -============================ - -Alternativamente, você pode criar várias classes para cada formulário que deseja usar em sua aplicação. Essa abordagem pode aumentar a legibilidade do código e facilitar o gerenciamento dos formulários. Deixaremos a `FormFactory` original criar apenas um formulário limpo com configuração básica (por exemplo, com suporte a traduções) e criaremos uma nova fábrica `EditFormFactory` para o formulário de edição. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ uso de composição -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -É muito importante que a relação entre as classes `FormFactory` e `EditFormFactory` seja realizada por [composição |nette:introduction-to-object-oriented-programming#Composição], e não por [herança de objetos |nette:introduction-to-object-oriented-programming#Herança]: - -```php -// ⛔ ASSIM NÃO! A HERANÇA NÃO PERTENCE AQUI -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -O uso de herança seria completamente contraproducente neste caso. Você encontraria problemas muito rapidamente. Por exemplo, no momento em que quisesse adicionar parâmetros ao método `create()`; o PHP relataria um erro de que sua assinatura difere da do pai. Ou ao passar dependências para a classe `EditFormFactory` através do construtor. Ocorreria uma situação que chamamos de [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -Em geral, é melhor [preferir composição em vez de herança |dependency-injection:faq#Por que a composição é preferida em relação à herança]. - - -Manipulação do formulário -========================= - -O manipulador do formulário, que é chamado após o envio bem-sucedido, também pode fazer parte da classe de fábrica. Ele funcionará passando os dados enviados para o modelo para processamento. Eventuais erros são [passados de volta |forms:validation#Erros durante o processamento] para o formulário. O modelo no exemplo a seguir é representado pela classe `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // processamento dos dados enviados - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -No entanto, deixaremos o redirecionamento em si para o presenter. Ele adicionará outro manipulador ao evento `onSuccess`, que realizará o redirecionamento. Graças a isso, será possível usar o formulário em diferentes presenters e redirecionar para um local diferente em cada um deles. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('O registro foi salvo'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Esta solução utiliza a propriedade dos formulários de que, quando `addError()` é chamado no formulário ou em seu elemento, o próximo manipulador `onSuccess` não é chamado. - - -Herança da classe Form -====================== - -Um formulário construído não deve ser um descendente da classe `Form`. Em outras palavras, não use esta solução: - -```php -// ⛔ ASSIM NÃO! A HERANÇA NÃO PERTENCE AQUI -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $this->addSubmit('send', 'Enviar'); - $this->setTranslator($translator); - } -} -``` - -Em vez de construir o formulário no construtor, use uma fábrica. - -É preciso perceber que a classe `Form` é, antes de tudo, uma ferramenta para construir um formulário, ou seja, um *form builder*. E o formulário construído pode ser entendido como seu produto. Mas o produto não é um caso específico do builder, não há entre eles uma relação *is a* que forma a base da herança. - - -Componente com formulário -========================= - -Uma abordagem completamente diferente é a criação de [componentes|application:components] que incluem um formulário. Isso oferece novas possibilidades, como renderizar o formulário de uma maneira específica, já que o componente também inclui um template. Ou é possível usar sinais para comunicação AJAX e carregamento de informações no formulário, por exemplo, para sugestões, etc. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // processamento dos dados enviados - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // dispara o evento - $this->onSave($this, $data); - } -} -``` - -Criaremos também uma fábrica que produzirá este componente. Basta [registrar sua interface |application:components#Componentes com dependências]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -E adicionar ao arquivo de configuração: - -```neon -services: - - EditControlFactory -``` - -E agora podemos solicitar a fábrica e usá-la no presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ou redirecionamos para o resultado da edição, por exemplo: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/pt/inject-method-attribute.texy b/best-practices/pt/inject-method-attribute.texy deleted file mode 100644 index c5b05410c2..0000000000 --- a/best-practices/pt/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Métodos e atributos inject -************************** - -.[perex] -Neste artigo, focaremos nas diferentes maneiras de passar dependências para presenters no framework Nette. Compararemos a forma preferida, que é o construtor, com outras opções, como métodos e atributos `inject`. - -Também para presenters, passar dependências usando o [construtor |dependency-injection:passing-dependencies#Passagem pelo construtor] é o caminho preferido. No entanto, se você criar um ancestral comum do qual outros presenters herdam (por exemplo, `BasePresenter`), e este ancestral também tiver dependências, ocorrerá um problema que chamamos de [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Isso pode ser contornado usando caminhos alternativos, que são os métodos e atributos (anotações) `inject`. - - -Métodos `inject*()` -=================== - -É uma forma de passar dependências por [setter |dependency-injection:passing-dependencies#Passagem por setter]. O nome desses setters começa com o prefixo `inject`. O Nette DI chama automaticamente métodos com esse nome logo após a criação da instância do presenter e passa a eles todas as dependências necessárias. Portanto, eles devem ser declarados como `public`. - -Os métodos `inject*()` podem ser considerados como uma extensão do construtor em vários métodos. Graças a isso, o `BasePresenter` pode receber dependências através de outro método e deixar o construtor livre para seus descendentes: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Um presenter pode conter qualquer número de métodos `inject*()` e cada um pode ter qualquer número de parâmetros. Eles também são ótimos em casos onde o presenter é [composto por traits |presenter-traits] e cada um deles requer sua própria dependência. - - -Atributos `Inject` -================== - -É uma forma de [injeção na propriedade |dependency-injection:passing-dependencies#Configuração de propriedade]. Basta marcar em quais propriedades injetar, e o Nette DI passa automaticamente as dependências logo após a criação da instância do presenter. Para poder inseri-las, é necessário declará-las como `public`. - -Marcamos as propriedades com um atributo: (anteriormente, usava-se a anotação `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // esta linha é importante - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -A vantagem dessa forma de passar dependências era a forma de escrita muito concisa. No entanto, com a chegada da [promoção de propriedades do construtor |https://blog.nette.org/pt/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], parece mais fácil usar o construtor. - -Por outro lado, essa forma sofre das mesmas desvantagens que a passagem de dependências para propriedades em geral: não temos controle sobre as alterações na variável e, ao mesmo tempo, a variável se torna parte da interface pública da classe, o que é indesejável. diff --git a/best-practices/pt/lets-create-contact-form.texy b/best-practices/pt/lets-create-contact-form.texy deleted file mode 100644 index f75fa749a8..0000000000 --- a/best-practices/pt/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Criando um formulário de contato -******************************** - -.[perex] -Vamos ver como criar um formulário de contato no Nette, incluindo o envio para e-mail. Então, vamos lá! - -Primeiro, precisamos criar um novo projeto. Como fazer isso é explicado na página [Começando |nette:installation]. E então podemos começar a criar o formulário. - -A maneira mais simples é criar o [formulário diretamente no presenter |forms:in-presenter]. Podemos usar o `HomePresenter` pré-preparado. Nele, adicionaremos o componente `contactForm` que representa o formulário. Faremos isso escrevendo o método de fábrica `createComponentContactForm()` no código, que produzirá o componente: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Nome:') - ->setRequired('Por favor, digite seu nome.'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Por favor, digite seu e-mail.'); - $form->addTextarea('message', 'Mensagem:') - ->setRequired('Por favor, digite sua mensagem.'); - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // envio de e-mail - } -} -``` - -Como você pode ver, criamos dois métodos. O primeiro método `createComponentContactForm()` cria um novo formulário. Ele tem campos para nome, e-mail e mensagem, que adicionamos com os métodos `addText()`, `addEmail()` e `addTextArea()`. Também adicionamos um botão para enviar o formulário. Mas e se o usuário não preencher algum campo? Nesse caso, devemos informá-lo de que é um campo obrigatório. Conseguimos isso com o método `setRequired()`. Finalmente, adicionamos também o [evento |nette:glossary#Eventos] `onSuccess`, que é acionado se o formulário for enviado com sucesso. No nosso caso, ele chama o método `contactFormSucceeded`, que cuidará do processamento do formulário enviado. Adicionaremos isso ao código em um momento. - -Deixaremos o componente `contactForm` ser renderizado no template `Home/default.latte`: - -```latte -{block content} -<h1>Formulário de Contato</h1> -{control contactForm} -``` - -Para o envio do e-mail em si, criaremos uma nova classe, que chamaremos de `ContactFacade` e a colocaremos no arquivo `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // seu e-mail - ->setFrom($email, $name) - ->setSubject('Mensagem do formulário de contato') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -O método `sendMessage()` cria e envia o e-mail. Ele usa o chamado mailer para isso, que ele recebe como dependência através do construtor. Leia mais sobre [envio de e-mails |mail:]. - -Agora voltaremos ao presenter e finalizaremos o método `contactFormSucceeded()`. Ele chamará o método `sendMessage()` da classe `ContactFacade` e passará os dados do formulário para ele. E como obtemos o objeto `ContactFacade`? Vamos recebê-lo através do construtor: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('A mensagem foi enviada'); - $this->redirect('this'); - } -} -``` - -Depois que o e-mail for enviado, ainda exibiremos ao usuário a chamada [flash message |application:components#Mensagens Flash], confirmando que a mensagem foi enviada, e depois redirecionaremos para a mesma página (usando `this`), para que não seja possível reenviar o formulário usando *refresh* no navegador. - - -Então, se tudo funcionar, você deve ser capaz de enviar um e-mail do seu formulário de contato. Parabéns! - - -Template HTML do e-mail ------------------------ - -Até agora, um e-mail de texto simples está sendo enviado, contendo apenas a mensagem enviada pelo formulário. Mas no e-mail, podemos usar HTML e tornar sua aparência mais atraente. Criaremos um template em Latte para ele, que escreveremos em `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Mensagem do formulário de contato - - -

    Nome: {$name}

    -

    E-mail: {$email}

    -

    Mensagem: {$message}

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

    Nume: {$name}

    -

    E-mail: {$email}

    -

    Mesaj: {$message}

    - - -``` - -Rămâne să modificăm `ContactFacade` pentru a utiliza acest șablon. În constructor vom solicita clasa `LatteFactory`, care poate produce obiectul `Latte\Engine`, adică [motorul de redare a șabloanelor Latte |latte:develop#Cum se randează un șablon]. Folosind metoda `renderToString()`, vom reda șablonul într-un șir, primul parametru este calea către șablon și al doilea sunt variabilele. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // emailul dvs. - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Emailul HTML generat îl vom transmite apoi metodei `setHtmlBody()` în locul celei originale `setBody()`. De asemenea, nu trebuie să specificăm subiectul emailului în `setSubject()`, deoarece biblioteca îl va prelua din elementul `` al șablonului. - - -Configurare ------------ - -În codul clasei `ContactFacade` este încă hardcodat emailul nostru de administrator `admin@example.com`. Ar fi mai bine să-l mutăm în fișierul de configurare. Cum facem asta? - -Mai întâi modificăm clasa `ContactFacade` și înlocuim șirul cu emailul cu o variabilă transmisă prin constructor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Și al doilea pas este specificarea valorii acestei variabile în configurație. În fișierul `app/config/services.neon` scriem: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Și gata. Dacă ar fi multe elemente în secțiunea `services` și ați avea senzația că emailul se pierde printre ele, îl putem transforma într-o variabilă. Modificăm înregistrarea la: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Și în fișierul `app/config/common.neon` definim această variabilă: - -```neon -parameters: - adminEmail: admin@example.com -``` - -Și am terminat! diff --git a/best-practices/ro/microsites.texy b/best-practices/ro/microsites.texy deleted file mode 100644 index 6c4441df8b..0000000000 --- a/best-practices/ro/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Cum să scrii micro-site-uri -*************************** - -Imaginați-vă că trebuie să creați rapid un mic site web pentru un eveniment viitor al companiei dvs. Trebuie să fie simplu, rapid și fără complicații inutile. Poate credeți că pentru un proiect atât de mic nu aveți nevoie de un framework robust. Dar ce se întâmplă dacă utilizarea framework-ului Nette poate simplifica și accelera fundamental acest proces? - -Chiar și la crearea site-urilor web simple, nu doriți să renunțați la confort. Nu doriți să reinventați ceea ce a fost deja rezolvat. Fiți liniștit leneș și lăsați-vă răsfățat. Nette Framework poate fi utilizat excelent și ca micro framework. - -Cum poate arăta un astfel de microsite? De exemplu, astfel încât întregul cod al site-ului să fie plasat într-un singur fișier `index.php` în directorul public: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// creează containerul DI pe baza configurației din config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// setăm rutarea -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// rută pentru URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // detectăm limba browserului și redirecționăm către URL /en sau /de etc. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// rută pentru URL https://example.com/cs sau https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // afișăm șablonul corespunzător, de exemplu ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// pornește aplicația! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Restul vor fi șabloane stocate în directorul părinte `/templates`. - -Codul PHP din `index.php` mai întâi [pregătește mediul |bootstrap:], apoi definește [rutele |application:routing#Rutare dinamică cu callback-uri] și în final pornește aplicația. Avantajul este că al doilea parametru al funcției `addRoute()` poate fi un callable, care se execută după deschiderea paginii corespunzătoare. - - -De ce să folosiți Nette pentru microsite-uri? ---------------------------------------------- - -- Programatorii care au încercat vreodată [Tracy |tracy:] nu își pot imagina astăzi că ar programa ceva fără ea. -- În primul rând, veți utiliza sistemul de șabloane [Latte |latte:], deoarece de la 2 pagini veți dori să aveți [layout-ul și conținutul separate |latte:template-inheritance]. -- Și cu siguranță doriți să vă bazați pe [escaparea automată |latte:safety-first], pentru a nu crea o vulnerabilitate XSS. -- Nette asigură, de asemenea, că în caz de eroare nu se vor afișa niciodată mesaje de eroare PHP pentru programatori, ci o pagină inteligibilă pentru utilizator. -- Dacă doriți să obțineți feedback de la utilizatori, de exemplu sub forma unui formular de contact, atunci veți adăuga și [formulare |forms:] și [bază de date |database:]. -- Formularele completate le puteți, de asemenea, [trimite ușor prin email |mail:]. -- Uneori vă poate fi utilă [cache-uirea |caching:], de exemplu dacă descărcați și afișați feed-uri. - -În zilele noastre, când viteza și eficiența sunt esențiale, este important să aveți instrumente care vă permit să obțineți rezultate fără întârzieri inutile. Nette framework vă oferă exact asta - dezvoltare rapidă, securitate și o gamă largă de instrumente, cum ar fi Tracy și Latte, care simplifică procesul. Este suficient să instalați câteva pachete Nette și construirea unui astfel de microsite devine brusc o joacă de copii. Și știți că nu se ascunde nicio gaură de securitate nicăieri. diff --git a/best-practices/ro/pagination.texy b/best-practices/ro/pagination.texy deleted file mode 100644 index fe8b8e2114..0000000000 --- a/best-practices/ro/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Paginarea rezultatelor bazei de date -************************************ - -.[perex] -La crearea aplicațiilor web, vă veți întâlni foarte des cu cerința de a limita numărul de elemente afișate pe pagină. - -Pornim de la starea în care afișăm toate datele fără paginare. Pentru selectarea datelor din baza de date avem clasa `ArticleRepository`, care, pe lângă constructor, conține metoda `findPublishedArticles`, ce returnează toate articolele publicate sortate descrescător după data publicării. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -În presenter injectăm apoi clasa model și în metoda render solicităm articolele publicate, pe care le transmitem șablonului: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -În șablonul `default.latte` ne ocupăm apoi de afișarea articolelor: - -```latte -{block content} -<h1>Articole</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -În acest mod putem afișa toate articolele, ceea ce însă începe să cauzeze probleme în momentul în care numărul articolelor crește. În acel moment devine utilă implementarea unui mecanism de paginare. - -Acesta asigură că toate articolele sunt împărțite în mai multe pagini și noi afișăm doar articolele unei pagini curente. Numărul total de pagini și împărțirea articolelor sunt calculate de [Paginator |utils:Paginator] singur, în funcție de câte articole avem în total și câte articole dorim să afișăm pe pagină. - -În primul pas, vom folosi obiectul `Paginator` în presenter pentru a calcula limita și offset-ul necesare pentru interogarea bazei de date. Clasa `ArticleRepository` nu necesită modificări dacă folosim `Nette\Database\Explorer`, deoarece putem aplica paginarea direct pe obiectul `Selection`. - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Returnează numărul total de articole publicate - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Ulterior, ne apucăm de modificările presenterului. În metoda render vom transmite numărul paginii afișate curent. Pentru cazul în care acest număr nu va face parte din URL, setăm valoarea implicită a primei pagini. - -Extindem, de asemenea, metoda render cu obținerea instanței Paginatorului, setarea sa și selectarea articolelor corecte pentru afișare în șablon. HomePresenter va arăta astfel după modificări: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Aflăm numărul total de articole publicate - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Creăm o instanță a Paginatorului și o setăm - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // numărul total de articole - $paginator->setItemsPerPage(10); // numărul de elemente pe pagină - $paginator->setPage($page); // numărul paginii curente - - // Extragem din baza de date un set limitat de articole conform calculului Paginatorului - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // pe care îl transmitem șablonului - $this->template->articles = $articles; - // și, de asemenea, Paginatorul însuși pentru afișarea opțiunilor de paginare - $this->template->paginator = $paginator; - } -} -``` - -Șablonul nostru iterează acum doar peste articolele unei singure pagini, este suficient să adăugăm linkurile de paginare: - -```latte -{block content} -<h1>Articole</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Prima</a> -  |  - <a n:href="default, $paginator->page-1">Anterioara</a> -  |  - {/if} - - Pagina {$paginator->getPage()} din {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Următoarea</a> -  |  - <a n:href="default, $paginator->getPageCount()">Ultima</a> - {/if} -</div> -``` - - -Astfel am completat pagina cu posibilitatea de paginare folosind `Paginator`. În cazul în care folosim [Nette Database Explorer |database:explorer], suntem capabili să implementăm paginarea și **fără a utiliza explicit** obiectul `Paginator` în presenter, deoarece clasa `Nette\Database\Table\Selection` conține metoda `page()` care încapsulează logica paginatorului. - -Repository-ul rămâne același ca în exemplul cu Explorer: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -În presenter nu trebuie să creăm Paginator, folosim în locul său metoda clasei `Selection`, pe care ne-o returnează repository-ul: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Extragem articolele publicate - $articles = $this->articleRepository->findPublishedArticles(); - - // și trimitem către șablon doar o parte din ele, limitată conform calculului metodei page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // și, de asemenea, datele necesare pentru afișarea opțiunilor de paginare - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Deoarece acum nu trimitem `Paginator` către șablon, modificăm partea care afișează linkurile de paginare pentru a folosi variabilele `$page` și `$lastPage`: - -```latte -{block content} -<h1>Articole</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Prima</a> -  |  - <a n:href="default, $page - 1">Anterioara</a> -  |  - {/if} - - Pagina {$page} din {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Următoarea</a> -  |  - <a n:href="default, $lastPage">Ultima</a> - {/if} -</div> -``` - -În acest mod am implementat mecanismul de paginare fără utilizarea Paginatorului. - -{{priority: -1}} diff --git a/best-practices/ro/passing-settings-to-presenters.texy b/best-practices/ro/passing-settings-to-presenters.texy deleted file mode 100644 index 0b0e2883ee..0000000000 --- a/best-practices/ro/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Transmiterea setărilor către presenteri -*************************************** - -.[perex] -Aveți nevoie să transmiteți argumente către presenteri care nu sunt obiecte (de ex. informația dacă rulează în modul debug, căi către directoare etc.) și, prin urmare, nu pot fi transmise automat prin autowiring? Soluția este să le încapsulați într-un obiect `Settings`. - -Serviciul `Settings` reprezintă o modalitate foarte ușoară și totuși utilă de a furniza informații despre aplicația care rulează către presenteri. Forma sa specifică depinde exclusiv de nevoile dvs. concrete. Exemplu: - -```php -namespace App; - -class Settings -{ - public function __construct( - // de la PHP 8.1 este posibil să specificați readonly - public bool $debugMode, - public string $appDir, - // și așa mai departe - ) {} -} -``` - -Exemplu de înregistrare în configurație: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Când presenterul va avea nevoie de informațiile furnizate de acest serviciu, pur și simplu îl va solicita în constructor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/ro/post-links.texy b/best-practices/ro/post-links.texy deleted file mode 100644 index 449a9de1c5..0000000000 --- a/best-practices/ro/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Cum să utilizați corect linkurile POST -************************************** - -.[perex] -În aplicațiile web, în special în interfețele administrative, ar trebui să fie o regulă de bază ca acțiunile care modifică starea serverului să nu fie efectuate prin metoda HTTP GET. După cum sugerează și numele metodei, GET ar trebui utilizat doar pentru obținerea datelor, nu pentru modificarea lor. Pentru acțiuni precum ștergerea înregistrărilor, este mai potrivită utilizarea metodei POST. Deși ideală ar fi metoda DELETE, aceasta nu poate fi invocată fără JavaScript, de aceea se folosește istoric POST. - -Cum se face acest lucru în practică? Utilizați acest truc simplu. La începutul șablonului, creați un formular auxiliar cu identificatorul `postForm`, pe care îl veți utiliza ulterior pentru butoanele de ștergere: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Datorită acestui formular, puteți utiliza un buton `<button>` în loc de linkul clasic `<a>`, care poate fi stilizat vizual pentru a arăta ca un link obișnuit. De exemplu, framework-ul CSS Bootstrap oferă clasele `btn btn-link` cu care puteți obține ca butonul să nu fie vizual diferit de alte linkuri. Folosind atributul `form="postForm"`, îl legați de formularul pre-pregătit: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button> - <!-- instead of <a n:href="delete $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -La click pe buton, se va invoca acum acțiunea `delete` prin metoda POST. Pentru a asigura că cererile sunt acceptate doar prin metoda POST și de pe același domeniu (ceea ce este o apărare eficientă împotriva atacurilor CSRF), utilizați atributul `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // cod ipotetic care șterge înregistrarea - $this->redirect('default'); - } -} -``` - -Atributul există de la Nette Application 3.2 și mai multe despre posibilitățile sale puteți afla pe pagina [Cum să utilizați atributul #Requires |attribute-requires]. - -Dacă ați utiliza semnalul `handleDelete()` în loc de acțiunea `actionDelete()`, nu este necesar să specificați `sameOrigin: true`, deoarece semnalele au această protecție setată implicit: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Această abordare nu numai că îmbunătățește securitatea aplicației dvs., dar contribuie și la respectarea standardelor și practicilor web corecte. Prin utilizarea metodelor POST pentru acțiunile care modifică starea, veți obține o aplicație mai robustă și mai sigură. diff --git a/best-practices/ro/presenter-traits.texy b/best-practices/ro/presenter-traits.texy deleted file mode 100644 index a074c21292..0000000000 --- a/best-practices/ro/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Compunerea presenterilor din trait-uri -************************************** - -.[perex] -Dacă avem nevoie să implementăm același cod în mai mulți presenteri (de ex. verificarea că utilizatorul este autentificat), o opțiune este plasarea codului într-un strămoș comun. A doua opțiune este crearea de [trait-uri |nette:introduction-to-object-oriented-programming#Trait-uri] cu un singur scop. - -Avantajul acestei soluții este că fiecare dintre presenteri poate folosi exact acele trait-uri de care are nevoie cu adevărat, în timp ce moștenirea multiplă nu este posibilă în PHP. - -Aceste trait-uri pot profita de faptul că la crearea presenterului se apelează succesiv toate [metodele inject |inject-method-attribute#Metode inject]. Este necesar doar să se asigure că numele fiecărei metode inject este unic pentru a evita conflictele. - -Trait-urile pot atașa cod de inițializare la evenimentele [onStartup sau onRender |application:presenters#Evenimente]. - -Exemple: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Presenterul apoi utilizează simplu aceste trait-uri: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/ro/restore-request.texy b/best-practices/ro/restore-request.texy deleted file mode 100644 index a8cee5857c..0000000000 --- a/best-practices/ro/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Cum să reveniți la pagina anterioară? -************************************* - -.[perex] -Ce se întâmplă dacă un utilizator completează un formular și sesiunea sa expiră? Pentru a nu pierde datele, înainte de a redirecționa către pagina de autentificare, salvăm cererea curentă în sesiune. În Nette, acest lucru este extrem de simplu. - -Cererea curentă poate fi salvată în sesiune folosind metoda `storeRequest()`, care returnează identificatorul său sub forma unui șir scurt. Metoda salvează numele presenterului curent, view-ul și parametrii săi. În cazul în care a fost trimis și un formular, se salvează și conținutul câmpurilor (cu excepția fișierelor încărcate). - -Restaurarea cererii se face prin metoda `restoreRequest($key)`, căreia îi transmitem identificatorul obținut. Aceasta redirecționează către presenterul și view-ul original. Dacă însă cererea salvată conține trimiterea unui formular, trece la presenterul original prin metoda `forward()`, transmite formularului valorile completate anterior și îl lasă să se redeseneze din nou. Astfel, utilizatorul are posibilitatea de a retrimite formularul și nu se pierd date. - -Important este că `restoreRequest()` verifică dacă utilizatorul nou autentificat este același cu cel care a completat inițial formularul. Dacă nu, cererea este abandonată și nu se face nimic. - -Vom ilustra totul cu un exemplu. Avem un presenter `AdminPresenter`, în care se editează date și în a cărui metodă `startup()` verificăm dacă utilizatorul este autentificat. Dacă nu este, îl redirecționăm către `SignPresenter`. În același timp, salvăm cererea curentă și trimitem cheia sa (`backlink`) către `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Presenterul `SignPresenter` va conține, pe lângă formularul de autentificare, și un parametru persistent `$backlink`, în care se va scrie cheia. Deoarece parametrul este persistent, acesta se va transmite și după trimiterea formularului de autentificare. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... adăugăm câmpurile formularului ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... aici autentificăm utilizatorul ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Metodei `restoreRequest()` îi transmitem cheia cererii salvate și aceasta redirecționează (sau trece) la presenterul original. - -Dacă însă cheia este invalidă (de exemplu, nu mai există în sesiune), metoda nu face nimic. Urmează deci apelul `$this->redirect('Admin:')`, care redirecționează către `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/ru/@home.texy b/best-practices/ru/@home.texy index 1ea3ce7e1e..623cb64fa2 100644 --- a/best-practices/ru/@home.texy +++ b/best-practices/ru/@home.texy @@ -2,23 +2,24 @@ ***************************** .[perex] -Руководства, решения частых задач и *лучшие практики* для Nette. +Руководства, решения типичных задач и лучшие практики для Nette. <div class=documentation> <div> -Приложения Nette ----------------- +Nette Application +----------------- - [Методы и атрибуты inject |inject-method-attribute] -- [Компоновка презентеров из трейтов |presenter-traits] +- [Составление презентеров из трейтов |presenter-traits] - [Передача настроек в презентеры |passing-settings-to-presenters] -- [Как вернуться на предыдущую страницу |restore-request] -- [Пагинация результатов базы данных |pagination] +- [Как восстановить запрос |restore-request] +- [Постраничный вывод из базы данных |pagination] - [Динамические сниппеты |dynamic-snippets] - [Как использовать атрибут #Requires |attribute-requires] - [Как правильно использовать POST-ссылки |post-links] +- [Красивые URL со слагами |pretty-urls] </div> <div> @@ -28,21 +29,20 @@ ----- - [Повторное использование форм |form-reuse] - [Форма для создания и редактирования записей |creating-editing-form] -- [Создаем контактную форму |lets-create-contact-form] -- [Зависимые селектбоксы |https://blog.nette.org/ru/dependent-selectboxes-elegantly-in-nette-and-pure-js] +- [Создадим форму обратной связи |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 записывает константы в 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] -- [Советы по редакторам и инструментам |editors-and-tools] - [Введение в объектно-ориентированное программирование |nette:introduction-to-object-oriented-programming] </div> @@ -52,9 +52,9 @@ Примеры решений --------------- - [Примеры Nette |https://github.com/nette-examples] -- [Doctrine и Nette |https://contributte.org/nettrine/] +- [Doctrine & Nette |https://contributte.org/nettrine/] - [Примеры Contributte |https://contributte.org/examples.html] -- [Сайт Doctrine ORM |https://github.com/MinecordNetwork/Website] +- [Сайт на Doctrine ORM |https://github.com/MinecordNetwork/Website] - [Быстрый старт |quickstart:] </div> @@ -63,7 +63,7 @@ Видео ----- -Сотни записей с Posledních sobot и видео о Nette вы найдете под одной крышей на "Youtube-канале Nette Framework":https://www.youtube.com/user/NetteFramework. +Сотни записей со встреч 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 new file mode 100644 index 0000000000..5f0b47ac07 --- /dev/null +++ b/best-practices/ru/@left-menu.texy @@ -0,0 +1,34 @@ +Руководства и лучшие практики +***************************** +- [Обзор |@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 index 6463960eed..cbad61a84f 100644 --- a/best-practices/ru/@meta.texy +++ b/best-practices/ru/@meta.texy @@ -1,2 +1 @@ {{sitename: Руководства и лучшие практики}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/ru/attribute-requires.texy b/best-practices/ru/attribute-requires.texy index 8fafce3e80..c7f07757f3 100644 --- a/best-practices/ru/attribute-requires.texy +++ b/best-practices/ru/attribute-requires.texy @@ -2,30 +2,30 @@ ************************************** .[perex] -При написании веб-приложения вы часто сталкиваетесь с необходимостью ограничить доступ к определенным частям вашего приложения. Возможно, вы хотите, чтобы некоторые запросы могли отправлять данные только с помощью формы (то есть методом POST), или чтобы они были доступны только для AJAX-вызовов. В Nette Framework 3.2 появился новый инструмент, который позволяет вам устанавливать такие ограничения очень элегантно и наглядно: атрибут `#[Requires]`. +Разрабатывая веб-приложение, вы часто сталкиваетесь с необходимостью ограничить доступ к некоторым его частям. Возможно, вы хотите, чтобы часть запросов могла отправлять данные только через форму (то есть методом POST) или была доступна только для AJAX-вызовов. В Nette Framework 3.2 появился новый инструмент, позволяющий задавать такие ограничения изящно и наглядно: атрибут `#[Requires]`. -Атрибут — это специальная метка в PHP, которую вы добавляете перед определением класса или метода. Поскольку это фактически класс, чтобы следующие примеры работали, необходимо указать клаузу use: +Атрибут - это особая пометка в PHP, которую вы добавляете перед определением класса или метода. Поскольку по сути это класс, для работы следующих примеров нужно подключить конструкцию `use`: ```php use Nette\Application\Attributes\Requires; ``` -Атрибут `#[Requires]` можно использовать у самого класса презентера, а также у следующих методов: +Атрибут `#[Requires]` можно использовать с самим классом презентера и с этими методами: - `action<Action>()` - `render<View>()` - `handle<Signal>()` - `createComponent<Name>()` -Последние два метода относятся и к компонентам, то есть атрибут можно использовать и у них. +Последние два метода относятся и к компонентам, поэтому атрибут можно использовать и с ними. -Если условия, указанные атрибутом, не выполнены, вызывается HTTP-ошибка 4xx. +Если условия, заданные атрибутом, не выполнены, вызывается HTTP-ошибка 4xx. -Методы HTTP +HTTP-методы ----------- -Вы можете указать, какие HTTP-методы (например, GET, POST и т. д.) разрешены для доступа. Например, если вы хотите разрешить доступ только путем отправки формы, установите: +Вы можете указать, какие HTTP-методы (такие как GET, POST и так далее) разрешены для доступа. Например, если вы хотите разрешить доступ только через отправку формы, задайте: ```php class AdminPresenter extends Nette\Application\UI\Presenter @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Почему следует использовать POST вместо GET для действий, изменяющих состояние, и как это сделать? [Прочитайте руководство |post-links]. +Почему для действий, меняющих состояние, стоит использовать POST вместо GET и как это сделать? [Прочитайте руководство |post-links]. -Вы можете указать метод или массив методов. Особым случаем является значение `'*'`, которое разрешает все методы, что стандартно презентеры по [соображениям безопасности не позволяют |application:presenters#Проверка HTTP-метода]. +Можно указать один метод или массив методов. Особый случай - значение `'*'`, разрешающее все методы, чего презентеры [по соображениям безопасности по умолчанию не допускают |application:presenters#Проверка HTTP-метода]. -AJAX-вызов ----------- +AJAX-вызовы +----------- -Если вы хотите, чтобы презентер или метод был доступен только для AJAX-запросов, используйте: +Если вы хотите, чтобы презентер или метод были доступны только для AJAX-запросов, используйте: ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ class AjaxPresenter extends Nette\Application\UI\Presenter Тот же источник --------------- -Для повышения безопасности вы можете требовать, чтобы запрос был сделан с того же домена. Это предотвратит [уязвимость CSRF |nette:vulnerability-protection#Межсайтовая подделка запроса CSRF]: +Ради повышения безопасности вы можете потребовать, чтобы запрос приходил с того же домена. Это предотвращает [уязвимость CSRF |nette:vulnerability-protection#Cross-Site Request Forgery (CSRF)]: ```php #[Requires(sameOrigin: true)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -У методов `handle<Signal>()` доступ с того же домена требуется автоматически. Так что если, наоборот, вы хотите разрешить доступ с любого домена, укажите: +Для методов `handle<Signal>()` доступ с того же домена требуется автоматически. Поэтому если вы хотите разрешить доступ с любого домена, укажите: ```php #[Requires(sameOrigin: false)] @@ -80,7 +80,7 @@ public function handleList(): void Доступ через forward -------------------- -Иногда полезно ограничить доступ к презентеру так, чтобы он был доступен только косвенно, например, с использованием метода `forward()` или `switch()` из другого презентера. Так, например, защищаются error-презентеры, чтобы их нельзя было вызвать из URL: +Иногда полезно ограничить доступ к презентеру так, чтобы он был доступен только косвенно, например через методы `forward()` или `switch()` из другого презентера. Именно так защищаются презентеры ошибок, чтобы их нельзя было вызвать из URL: ```php #[Requires(forward: true)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -На практике часто бывает необходимо пометить определенные представления, к которым можно получить доступ только на основе логики в презентере. То есть опять же, чтобы их нельзя было открыть напрямую: +На практике часто нужно пометить определённые представления, к которым можно попасть только по логике презентера. Опять же, чтобы их нельзя было открыть напрямую: ```php class ProductPresenter extends Nette\Application\UI\Presenter @@ -114,7 +114,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Конкретные действия ------------------- -Вы также можете ограничить, чтобы определенный код, например, создание компонента, был доступен только для специфических действий в презентере: +Вы можете также ограничить какой-то код, например создание компонента, чтобы он был доступен только для определённых действий презентера: ```php class EditDeletePresenter extends Nette\Application\UI\Presenter @@ -126,15 +126,15 @@ class EditDeletePresenter extends Nette\Application\UI\Presenter } ``` -В случае одного действия нет необходимости записывать массив: `#[Requires(actions: 'default')]` +Для одного действия массив писать не нужно: `#[Requires(actions: 'default')]` Собственные атрибуты -------------------- -Если вы хотите использовать атрибут `#[Requires]` повторно с теми же настройками, вы можете создать собственный атрибут, который будет наследовать `#[Requires]` и настроит его в соответствии с потребностями. +Если вы хотите многократно использовать атрибут `#[Requires]` с одними и теми же настройками, вы можете создать собственный атрибут, который наследует `#[Requires]` и настраивает его под ваши нужды. -Например, `#[SingleAction]` разрешит доступ только через действие `default`: +Например, `#[SingleAction]` разрешает доступ только через действие `default`: ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -Или `#[RestMethods]` разрешит доступ через все HTTP-методы, используемые для REST API: +А `#[RestMethods]` разрешит доступ всеми HTTP-методами, используемыми для REST API: ```php #[\Attribute] @@ -174,4 +174,4 @@ class ApiPresenter extends Nette\Application\UI\Presenter Заключение ---------- -Атрибут `#[Requires]` дает вам большую гибкость и контроль над тем, как доступны ваши веб-страницы. С помощью простых, но мощных правил вы можете повысить безопасность и правильное функционирование вашего приложения. Как видите, использование атрибутов в Nette может не только упростить вашу работу, но и обезопасить ее. +Атрибут `#[Requires]` даёт вам большую гибкость и контроль над тем, как обращаются к вашим страницам. С помощью простых, но мощных правил вы можете повысить безопасность и правильность работы приложения. Как видите, использование атрибутов в Nette может не только упростить вашу работу, но и защитить её. diff --git a/best-practices/ru/composer.texy b/best-practices/ru/composer.texy index b8f63ccad6..67f90286aa 100644 --- a/best-practices/ru/composer.texy +++ b/best-practices/ru/composer.texy @@ -3,10 +3,10 @@ Composer: советы по использованию <div class=perex> -Composer — это инструмент для управления зависимостями в PHP. Он позволяет нам перечислить библиотеки, от которых зависит наш проект, и будет устанавливать и обновлять их за нас. Мы покажем: +Composer - инструмент управления зависимостями в PHP. Он позволяет объявить библиотеки, от которых зависит ваш проект, и сам их установит и обновит. Мы узнаем: - как установить Composer -- его использование в новом или существующем проекте +- как использовать его в новом или существующем проекте </div> @@ -14,21 +14,21 @@ Composer — это инструмент для управления завис Установка ========= -Composer — это исполняемый файл `.phar`, который вы скачиваете и устанавливаете следующим образом: +Composer - исполняемый файл `.phar`, который вы скачиваете и устанавливаете следующим образом. Windows ------- -Используйте официальный установщик [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. +Воспользуйтесь официальным установщиком [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. Linux, macOS ------------ -Достаточно 4 команд, которые скопируйте с [этой страницы |https://getcomposer.org/download/]. +Вам понадобятся всего 4 команды, которые можно скопировать с [этой страницы |https://getcomposer.org/download/]. -Далее, поместив в папку, которая находится в системном `PATH`, Composer станет доступен глобально: +Кроме того, скопировав его в папку, входящую в системный `PATH`, вы сделаете Composer доступным глобально: ```shell $ mv ./composer.phar ~/bin/composer # или /usr/local/bin/composer @@ -38,7 +38,7 @@ $ mv ./composer.phar ~/bin/composer # или /usr/local/bin/composer Использование в проекте ======================= -Чтобы начать использовать Composer в своем проекте, вам нужен только файл `composer.json`. Он описывает зависимости нашего проекта и может также содержать другие метаданные. Базовый `composer.json` может выглядеть так: +Чтобы начать использовать Composer в своём проекте, вам нужен только файл `composer.json`. Этот файл описывает зависимости вашего проекта и может содержать другие метаданные. Простейший `composer.json` может выглядеть так: ```js { @@ -48,17 +48,17 @@ $ mv ./composer.phar ~/bin/composer # или /usr/local/bin/composer } ``` -Здесь мы говорим, что наше приложение (или библиотека) требует пакет `nette/database` (название пакета состоит из названия организации и названия проекта) и хочет версию, которая соответствует условию `^3.0` (т. е. последнюю версию 3). +Здесь мы говорим, что нашему приложению (или библиотеке) нужен пакет `nette/database` (имя пакета состоит из имени поставщика и имени проекта) и что нужна версия, отвечающая ограничению `^3.0` (то есть последняя версия 3). -Итак, у нас есть в корне проекта файл `composer.json`, и мы запускаем установку: +Итак, имея файл `composer.json` в корне проекта, выполните: ```shell composer update ``` -Composer скачает Nette Database в папку `vendor/`. Далее он создаст файл `composer.lock`, который содержит информацию о том, какие именно версии библиотек он установил. +Composer скачает Nette Database в каталог `vendor/`. Он также создаст файл `composer.lock`, в котором записано, какие именно версии библиотек были установлены. -Composer сгенерирует файл `vendor/autoload.php`, который мы можем просто включить и начать использовать библиотеки без какой-либо дополнительной работы: +Composer порождает файл `vendor/autoload.php`. Вы можете просто подключить этот файл и начать использовать классы библиотек без каких-либо дополнительных действий: ```php require __DIR__ . '/vendor/autoload.php'; @@ -70,17 +70,17 @@ $db = new Nette\Database\Connection('sqlite::memory:'); Обновление пакетов до последних версий ====================================== -За обновление используемых библиотек до последних версий в соответствии с условиями, определенными в `composer.json`, отвечает команда `composer update`. Например, для зависимости `"nette/database": "^3.0"` установит последнюю версию 3.x.x, но не версию 4. +Чтобы обновить используемые библиотеки до последних версий в рамках ограничений, заданных в `composer.json`, используйте команду `composer update`. Например, при зависимости `"nette/database": "^3.0"` он установит последнюю версию 3.x.x, но не версию 4. -Для обновления условий в файле `composer.json`, например, до `"nette/database": "^4.1"`, чтобы можно было установить последнюю версию, используйте команду `composer require nette/database`. +Чтобы обновить сами ограничения в файле `composer.json`, например до `"nette/database": "^4.1"`, разрешив установку последней версии, используйте команду `composer require nette/database`. -Для обновления всех используемых пакетов Nette потребовалось бы перечислить их все в командной строке, например: +Чтобы обновить все используемые пакеты Nette, вам пришлось бы перечислить их все в командной строке, например: ```shell composer require nette/application nette/forms latte/latte tracy/tracy ... ``` -Что непрактично. Используйте поэтому простой скрипт "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, который сделает это за вас: +Это непрактично. Поэтому воспользуйтесь простым скриптом "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, который сделает это за вас: ```shell php composer-frontline.php @@ -90,29 +90,29 @@ php composer-frontline.php Создание нового проекта ======================= -Новый проект на Nette создается с помощью одной команды: +Новый проект на Nette можно создать одной командой: ```shell -composer create-project nette/web-project nazev-projekta +composer create-project nette/web-project name-of-the-project ``` -В качестве `nazev-projekta` вставьте название каталога для своего проекта и подтвердите. Composer скачает репозиторий `nette/web-project` с GitHub, который уже содержит файл `composer.json`, и сразу после этого Nette Framework. Должно уже хватить только [установить права |nette:troubleshooting#Настройка прав доступа к каталогам] на запись в папки `temp/` и `log/`, и проект должен ожить. +Замените `name-of-the-project` на имя каталога вашего проекта и выполните команду. Composer скачает с GitHub репозиторий `nette/web-project`, в котором уже есть файл `composer.json`, а затем установит сам Nette Framework. Останется только [настроить права каталогов |nette:troubleshooting#Задание прав на каталоги] `temp/` и `log/`, и проект должен заработать. -Если вы знаете, на какой версии PHP будет хоститься проект, не забудьте [ее установить |#Версия PHP]. +Если вы знаете, на какой версии PHP будет размещён ваш проект, обязательно [задайте её |#Версия PHP]. Версия PHP ========== -Composer всегда устанавливает те версии пакетов, которые совместимы с версией PHP, которую вы сейчас используете (точнее, с версией PHP, используемой в командной строке при запуске Composer). Что, однако, скорее всего, не та же версия, которую использует ваш хостинг. Поэтому очень важно добавить в файл `composer.json` информацию о версии PHP на хостинге. После этого будут устанавливаться только версии пакетов, совместимые с хостингом. +Composer всегда устанавливает версии пакетов, совместимые с той версией PHP, которую вы сейчас используете (точнее, с версией PHP в командной строке, где запускается Composer). Она может отличаться от версии, которая используется на вашем хостинге. Поэтому принципиально важно добавить в файл `composer.json` сведения о версии PHP на хостинге. Тогда будут устанавливаться только версии пакетов, совместимые с хостингом. -То, что проект будет работать, например, на PHP 8.2.3, мы установим командой: +Например, чтобы указать, что проект будет работать на PHP 8.2.3, используйте команду: ```shell composer config platform.php 8.2.3 ``` -Так версия запишется в файл `composer.json`: +Версия запишется в файл `composer.json` вот так: ```js { @@ -124,7 +124,7 @@ composer config platform.php 8.2.3 } ``` -Однако номер версии PHP указывается еще в другом месте файла, а именно в секции `require`. В то время как первое число определяет, для какой версии будут устанавливаться пакеты, второе число говорит, для какой версии написано само приложение. И по нему, например, PhpStorm устанавливает `PHP language level`. (Конечно, нет смысла, чтобы эти версии различались, так что двойная запись — это недоработка.) Эту версию вы установите командой: +Однако номер версии PHP указывается в файле и в другом месте, в секции `require`. Первое число определяет версию, под которую устанавливаются пакеты, а второе - версию, под которую написано само приложение. Например, PhpStorm по нему выставляет *PHP language level*. (Разумеется, различаться этим версиям смысла нет, так что двойная запись - недосмотр.) Задайте эту версию командой: ```shell composer require php 8.2.3 --no-update @@ -144,69 +144,69 @@ composer require php 8.2.3 --no-update Игнорирование версии PHP ======================== -Пакеты обычно указывают как самую низкую версию PHP, с которой они совместимы, так и самую высокую, с которой они протестированы. Если вы собираетесь использовать версию PHP еще новее, например, для тестирования, Composer откажется устанавливать такой пакет. Решением является опция `--ignore-platform-req=php+`, которая заставит Composer игнорировать верхние пределы требуемой версии PHP. +Пакеты обычно указывают и самую низкую версию PHP, с которой они совместимы, и самую высокую, на которой они были протестированы. Если вы собираетесь использовать ещё более новую версию PHP, скажем ради тестирования, Composer откажется устанавливать такой пакет. Решение - параметр `--ignore-platform-req=php+`, который заставляет Composer игнорировать верхние границы требуемой версии PHP. Ложные сообщения ================ -При обновлении пакетов или изменении номеров версий случается, что возникает конфликт. Один пакет имеет требования, которые противоречат другому, и т. п. Composer, однако, иногда выводит ложные сообщения. Сообщает о конфликте, которого на самом деле нет. В таком случае поможет удалить файл `composer.lock` и попробовать снова. +При обновлении пакетов или изменении номеров версий иногда возникают конфликты. У одного пакета требования конфликтуют с другим и так далее. Однако иногда Composer выдаёт ложные сообщения. Он сообщает о конфликте, которого на самом деле нет. В таких случаях может помочь удаление файла `composer.lock` и повторная попытка. -Если сообщение об ошибке сохраняется, то оно серьезное, и нужно из него понять, что и как исправить. +Если сообщение об ошибке не исчезает, значит оно настоящее, и вам нужно его прочитать, чтобы понять, что и как изменить. -Packagist.org - центральный репозиторий -======================================= +Packagist.org - глобальный репозиторий +====================================== -[Packagist |https://packagist.org] — это главный репозиторий, в котором Composer пытается искать пакеты, если ему не скажут иначе. Здесь мы можем публиковать и собственные пакеты. +[Packagist |https://packagist.org] - главный репозиторий, в котором Composer по умолчанию ищет пакеты. Здесь вы можете публиковать и собственные пакеты. -Что делать, если мы не хотим использовать центральный репозиторий? ------------------------------------------------------------------- +А если нам не нужен центральный репозиторий +------------------------------------------- -Если у нас есть внутрифирменные приложения, которые мы просто не можем хостить публично, то мы создадим для них фирменный репозиторий. +Если внутри компании у нас есть приложения или библиотеки, которые нельзя размещать публично, мы можем создать для них собственные репозитории. -Больше на тему репозиториев [в официальной документации |https://getcomposer.org/doc/05-repositories.md#repositories]. +Подробнее о репозиториях читайте в [официальной документации |https://getcomposer.org/doc/05-repositories.md#repositories]. Автозагрузка ============ -Ключевой особенностью Composer является то, что он предоставляет автозагрузку для всех установленных им классов, которую вы запускаете, включив файл `vendor/autoload.php`. +Ключевая возможность Composer в том, что он обеспечивает автозагрузку всех устанавливаемых им классов. Вы включаете её подключением файла `vendor/autoload.php`. -Однако можно использовать Composer и для загрузки других классов и вне папки `vendor`. Первой возможностью является позволить Composer просканировать определенные папки и подпапки, найти все классы и включить их в автозагрузчик. Этого можно достичь, установив `autoload > classmap` в `composer.json`: +Однако Composer можно использовать и для загрузки других классов вне каталога `vendor/`. Первый вариант - позволить Composer просмотреть заданные каталоги и подкаталоги, найти все классы и включить их в автозагрузчик. Для этого задайте в `composer.json` секцию `autoload > classmap`: ```js { "autoload": { "classmap": [ - "src/", # включит папку src/ и ее подпапки + "src/", # включает каталог src/ и его подкаталоги ] } } ``` -Затем необходимо при каждом изменении запускать команду `composer dumpautoload` и позволить перегенерировать таблицы автозагрузки. Это крайне неудобно, и гораздо лучше доверить эту задачу [RobotLoader |robot-loader:], который ту же самую деятельность выполняет автоматически в фоновом режиме и гораздо быстрее. +После этого вам придётся после каждого изменения выполнять команду `composer dumpautoload`, чтобы перегенерировать таблицы автозагрузки. Это крайне неудобно. Куда лучше поручить эту задачу [RobotLoader|robot-loader:], который делает то же самое автоматически в фоне и намного быстрее. -Второй возможностью является соблюдение [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Упрощенно говоря, это система, когда пространства имен и названия классов соответствуют структуре каталогов и названиям файлов, то есть, например, `App\Core\RouterFactory` будет в файле `/path/to/App/Core/RouterFactory.php`. Пример конфигурации: +Второй вариант - придерживаться [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/ + "App\\": "app/" # пространство имён App\ находится в каталоге app/ } } } ``` -Как точно настроить поведение, вы узнаете в [документации Composer |https://getcomposer.org/doc/04-schema.md#psr-4]. +Подробности о настройке этого поведения см. в [документации Composer |https://getcomposer.org/doc/04-schema.md#psr-4]. Тестирование новых версий ========================= -Хотите протестировать новую разработочную версию пакета? Как это сделать? Сначала в файл `composer.json` добавьте эту пару опций, которая позволит устанавливать разработочные версии пакетов, однако прибегнет к этому только в случае, если не существует никакой комбинации стабильных версий, которая бы удовлетворяла требованиям: +Хотите протестировать новую разрабатываемую версию пакета? Вот как это сделать. Сначала добавьте в файл `composer.json` эту пару параметров. Она разрешает установку разрабатываемых версий, но Composer прибегнет к ним только тогда, когда ни одно сочетание стабильных версий не отвечает требованиям: ```js { @@ -215,21 +215,21 @@ Packagist.org - центральный репозиторий } ``` -Далее рекомендуем удалить файл `composer.lock`, иногда Composer необъяснимо отказывается от установки, и это решает проблему. +Мы также рекомендуем удалить файл `composer.lock`, потому что Composer иногда необъяснимо отказывается от установки, а это может решить проблему. -Допустим, это пакет `nette/utils`, и новая версия имеет номер 4.0. Установите ее командой: +Допустим, пакет - `nette/utils`, а новая версия - 4.0. Установите её командой: ```shell composer require nette/utils:4.0.x-dev ``` -Или вы можете установить конкретную версию, например, 4.0.0-RC2: +Или вы можете установить конкретную версию, например 4.0.0-RC2: ```shell composer require nette/utils:4.0.0-RC2 ``` -Но если от библиотеки зависит другой пакет, который заблокирован на старой версии (например, `^3.1`), то идеально обновить пакет, чтобы он работал с новой версией. Однако, если вы хотите просто обойти ограничение и заставить Composer установить разработочную версию и притвориться, что это старая версия (например, 3.1.6), вы можете использовать ключевое слово `as`: +Однако если от библиотеки зависит другой пакет и он привязан к более старой версии (например, `^3.1`), идеальным решением будет обновить этот зависимый пакет так, чтобы он работал с новой версией. Но если вы просто хотите обойти ограничение и заставить Composer установить разрабатываемую версию, притворившись, что это более старая версия (например, 3.1.6), вы можете использовать ключевое слово `as`: ```shell composer require nette/utils "4.0.x-dev as 3.1.6" @@ -239,9 +239,9 @@ composer require nette/utils "4.0.x-dev as 3.1.6" Вызов команд ============ -Через Composer можно вызывать собственные предопределенные команды и скрипты, как если бы это были нативные команды Composer. Для скриптов, которые находятся в папке `vendor/bin`, не нужно указывать эту папку. +Вы можете вызывать через Composer собственные заранее заданные команды и скрипты так, будто это его родные команды. Для скриптов, лежащих в каталоге `vendor/bin`, указывать этот путь не нужно. -В качестве примера определим в файле `composer.json` скрипт, который с помощью [Nette Tester |tester:] запустит тесты: +В качестве примера определим в `composer.json` скрипт, который использует [Nette Tester |tester:] для запуска тестов: ```js { @@ -251,19 +251,19 @@ composer require nette/utils "4.0.x-dev as 3.1.6" } ``` -Тесты затем запустим с помощью `composer tester`. Команду можно вызвать и в случае, если мы не находимся в корневой папке проекта, а в каком-либо подкаталоге. +Затем мы запускаем тесты командой `composer tester`. Команду можно вызвать, даже если вы находитесь не в корневом каталоге проекта, а в одном из его подкаталогов. -Отправьте благодарность -======================= +Скажите спасибо +=============== -Покажем вам трюк, которым вы порадуете авторов open source. Простым способом поставите на GitHub звездочку библиотекам, которые использует ваш проект. Достаточно установить библиотеку `symfony/thanks`: +Покажем приём, который порадует авторов открытого кода. Вы можете легко поставить на GitHub звёзды библиотекам, которые использует ваш проект. Достаточно установить библиотеку `symfony/thanks`: ```shell composer global require symfony/thanks ``` -А затем запустить: +А затем выполнить: ```shell composer thanks @@ -272,10 +272,10 @@ composer thanks Попробуйте! -Конфигурация -============ +Настройка +========= -Composer тесно связан с инструментом версионирования [Git |https://git-scm.com]. Если он у вас не установлен, нужно сказать Composer, чтобы он его не использовал: +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 index bc6b5b84e1..e059c579eb 100644 --- a/best-practices/ru/creating-editing-form.texy +++ b/best-practices/ru/creating-editing-form.texy @@ -2,15 +2,15 @@ ****************************************** .[perex] -Как правильно реализовать в Nette добавление и редактирование записи, используя одну и ту же форму для обеих операций? +Как правильно реализовать добавление и редактирование записи в Nette, используя для обоих случаев одну и ту же форму? -Во многих случаях формы для добавления и редактирования записей идентичны, отличаясь, возможно, только надписью на кнопке. Мы покажем примеры простых презентеров, где форма используется сначала для добавления записи, затем для редактирования, и, наконец, объединим оба решения. +Во многих случаях формы для добавления и редактирования записи одинаковы и различаются разве что подписью кнопки. Мы покажем примеры простых презентеров, где сначала используем форму для добавления записи, затем для её редактирования, а в конце объединим оба решения. Добавление записи ----------------- -Пример презентера, служащего для добавления записи. Саму работу с базой данных оставим классу `Facade`, код которого для примера не важен. +Пример презентера для добавления записи. Саму работу с базой данных оставим классу `Facade`, код которого для этого примера не важен. ```php @@ -29,14 +29,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... добавляем поля формы ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // добавление записи в базу данных - $this->flashMessage('Успешно добавлено'); + $this->facade->add($data); // добавляем запись в базу данных + $this->flashMessage('Successfully added'); $this->redirect('...'); } @@ -51,7 +51,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter Редактирование записи --------------------- -Теперь покажем, как выглядел бы презентер, служащий для редактирования записи: +Теперь посмотрим, как выглядел бы презентер для редактирования записи: ```php @@ -70,8 +70,8 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // проверка существования записи - || !$this->facade->isEditAllowed(/*...*/) // проверка прав доступа + !$record // проверяем существование записи + || !$this->facade->isEditAllowed(/*...*/) // проверяем права ) { $this->error(); // ошибка 404 } @@ -90,23 +90,23 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... добавляем поля формы ... - $form->setDefaults($this->record); // установка значений по умолчанию - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->setDefaults($this->record); // задаём значения по умолчанию + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->update($this->record->id, $data); // обновление записи - $this->flashMessage('Успешно обновлено'); + $this->facade->update($this->record->id, $data); // обновляем запись + $this->flashMessage('Successfully updated'); $this->redirect('...'); } } ``` -В методе *action*, который запускается сразу в начале [жизненного цикла презентера |application:presenters#Жизненный цикл презентера], мы проверяем существование записи и права пользователя на ее редактирование. +В методе *action*, вызываемом в начале [жизненного цикла презентера |application:presenters#Жизненный цикл презентера], мы проверяем существование записи и право пользователя её редактировать. -Запись сохраняем в свойстве `$record`, чтобы она была доступна в методе `createComponentRecordForm()` для установки значений по умолчанию и в `recordFormSucceeded()` для получения ID. Альтернативным решением было бы установить значения по умолчанию прямо в `actionEdit()` и получить значение ID, которое является частью URL, с помощью `getParameter('id')`: +Мы сохраняем запись в свойстве `$record`, благодаря чему она доступна в методе `createComponentRecordForm()` для задания значений по умолчанию и в `recordFormSucceeded()` для доступа к идентификатору. Альтернативное решение - задать значения по умолчанию прямо в `actionEdit()`, а значение ID (часть URL) получить через `getParameter('id')`: ```php @@ -114,12 +114,12 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - // проверка существования и прав доступа + // проверяем существование и права ) { $this->error(); } - // установка значений по умолчанию для формы + // задаём значения формы по умолчанию $this->getComponent('recordForm') ->setDefaults($record); } @@ -130,16 +130,15 @@ class RecordPresenter extends Nette\Application\UI\Presenter $this->facade->update($id, $data); // ... } -} ``` -Однако, и это должно быть **самым важным выводом из всего кода**, при создании формы мы должны убедиться, что действие действительно `edit`. Иначе проверка в методе `actionEdit()` вообще не выполнится! +Однако, и это должно стать **самым важным выводом из всего кода**, мы обязаны при создании формы убедиться, что действие действительно `edit`. Иначе проверка в методе `actionEdit()` вообще не выполнится! Одна форма для добавления и редактирования ------------------------------------------ -А теперь объединим оба презентера в один. Мы могли бы в методе `createComponentRecordForm()` различать, какое действие выполняется, и в соответствии с этим конфигурировать форму, или же мы можем оставить это непосредственно action-методам и избавиться от условия: +Теперь объединим оба презентера в один. Мы могли бы либо различать действие внутри метода `createComponentRecordForm()` и настраивать форму соответственно, либо переложить это на сами методы действий и избавиться от условной проверки: ```php @@ -153,22 +152,22 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // проверка существования записи - || !$this->facade->isEditAllowed(/*...*/) // проверка прав доступа + !$record // проверяем существование записи + || !$this->facade->isEditAllowed(/*...*/) // проверяем права ) { $this->error(); // ошибка 404 } $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // установка значений по умолчанию - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->setDefaults($record); // задаём значения по умолчанию + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form @@ -185,18 +184,18 @@ class RecordPresenter extends Nette\Application\UI\Presenter return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // добавление записи в базу данных - $this->flashMessage('Успешно добавлено'); + $this->facade->add($data); // добавляем запись в базу данных + $this->flashMessage('Successfully added'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // обновление записи - $this->flashMessage('Успешно обновлено'); + $this->facade->update($id, $data); // обновляем запись + $this->flashMessage('Successfully updated'); $this->redirect('...'); } } diff --git a/best-practices/ru/dynamic-snippets.texy b/best-practices/ru/dynamic-snippets.texy index 30a58602e3..65cd823467 100644 --- a/best-practices/ru/dynamic-snippets.texy +++ b/best-practices/ru/dynamic-snippets.texy @@ -1,7 +1,10 @@ Динамические сниппеты ********************* -Довольно часто при разработке приложений возникает необходимость выполнять AJAX-операции, например, над отдельными строками таблицы или элементами списка. В качестве примера можно выбрать вывод статей, при этом для каждой из них мы позволим авторизованному пользователю выбрать оценку "нравится/не нравится". Код презентера и соответствующего шаблона без AJAX будет выглядеть примерно так (привожу наиболее важные фрагменты, код предполагает существование сервиса для отметки оценок и получения коллекции статей - конкретная реализация для целей этого руководства не важна): +.[perex] +Как с помощью AJAX обновлять только те части страницы, которые действительно меняются, например отдельные элементы списка, используя динамические сниппеты Latte. + +Довольно часто при разработке приложения возникает необходимость выполнять AJAX-операции, например над отдельными строками таблицы или элементами списка. Для примера возьмём вывод статей, где вошедшие пользователи могут оценить каждую статью как "нравится" или "не нравится". Код презентера и соответствующий шаблон без AJAX выглядели бы примерно так (приводим самые важные части; код предполагает, что существует сервис для обработки оценок и получения статей - его конкретная реализация для этого руководства не важна): ```php public function handleLike(int $articleId): void @@ -24,26 +27,26 @@ public function handleUnlike(int $articleId): void <h2>{$article->title}</h2> <div class="content">{$article->content}</div> {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Мне нравится</a> + <a n:href="like! $article->id" class=ajax>I like it</a> {else} - <a n:href="unlike! $article->id" class=ajax>Мне больше не нравится</a> + <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`. +Теперь добавим в это простое приложение функциональность AJAX. Изменение оценки статьи не настолько важно, чтобы ради него перезагружать страницу целиком, поэтому в идеале оно должно происходить в фоне через AJAX. Мы воспользуемся [скриптом-обработчиком из дополнений |application:ajax#Naja] с общепринятым соглашением, что у AJAX-ссылок есть CSS-класс `ajax`. -Но как это сделать конкретно? Nette предлагает 2 пути: путь так называемых динамических сниппетов и путь компонентов. Оба имеют свои плюсы и минусы, поэтому мы рассмотрим их по очереди. +Но как именно это реализовать? Nette предлагает два подхода: динамические сниппеты и компоненты. У каждого есть свои плюсы и минусы, поэтому покажем оба. -Путь динамических сниппетов -=========================== +Способ с динамическими сниппетами +================================= -Динамический сниппет в терминологии Latte означает специфический случай использования тега `{snippet}`, когда в названии сниппета используется переменная. Такой сниппет не может находиться в шаблоне где угодно - он должен быть обернут статическим сниппетом, то есть обычным, или находиться внутри `{snippetArea}`. Наш шаблон можно было бы изменить следующим образом. +В терминологии Latte динамический сниппет - это особый случай использования тега `{snippet}`, когда в имени сниппета используется переменная. Такой сниппет нельзя поставить где угодно в шаблоне: он должен быть обёрнут статическим (обычным) сниппетом или находиться внутри `{snippetArea}`. Мы могли бы изменить наш шаблон так: ```latte @@ -53,18 +56,18 @@ public function handleUnlike(int $articleId): void <div class="content">{$article->content}</div> {snippet article-{$article->id}} {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Мне нравится</a> + <a n:href="like! $article->id" class=ajax>I like it</a> {else} - <a n:href="unlike! $article->id" class=ajax>Мне больше не нравится</a> + <a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a> {/if} {/snippet} </article> {/snippet} ``` -Каждая статья теперь определяет один сниппет, который содержит ID статьи в своем названии. Все эти сниппеты затем обернуты одним сниппетом с названием `articlesContainer`. Если бы мы пропустили этот обертывающий сниппет, Latte предупредил бы нас исключением. +Каждая статья теперь задаёт сниппет, в имя которого входит идентификатор статьи. Все эти динамические сниппеты вместе обёрнуты статическим сниппетом с именем `articlesContainer`. Если бы мы этот внешний сниппет опустили, Latte выбросил бы исключение. -Остается добавить в презентер перерисовку - достаточно перерисовать статическую обертку. +Остаётся добавить в презентер логику перерисовки - достаточно перерисовать статическую обёртку. ```php public function handleLike(int $articleId): void @@ -72,18 +75,18 @@ public function handleLike(int $articleId): void $this->ratingService->saveLike($articleId, $this->user->id); if ($this->isAjax()) { $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- не требуется + // $this->redrawControl('article-' . $articleId); -- не нужно } else { $this->redirect('this'); } } ``` -Аналогично изменим и сестринский метод `handleUnlike()`, и AJAX заработает! +Измените похожим образом и соответствующий метод `handleUnlike()`, и AJAX работает! -Однако у этого решения есть один недостаток. Если бы мы подробнее изучили, как происходит AJAX-запрос, мы бы обнаружили, что хотя внешне приложение выглядит экономно (возвращает только один сниппет для данной статьи), на самом деле на сервере оно отрисовало все сниппеты. Нужный сниппет оно поместило в payload, а остальные отбросило (совершенно зря, таким образом, также получив их из базы данных). +Однако у этого решения есть недостаток. Если присмотреться к AJAX-запросу, окажется, что снаружи приложение выглядит эффективным (возвращает только один сниппет конкретной статьи), но на стороне сервера отрисовывает *все* сниппеты. Оно кладёт нужный сниппет в payload, а остальные отбрасывает (то есть зря их получило и отрисовало). -Чтобы оптимизировать этот процесс, нам придется вмешаться там, где мы передаем коллекцию `$articles` в шаблон (скажем, в методе `renderDefault()`). Мы воспользуемся тем фактом, что обработка сигналов происходит перед методами `render<Something>`: +Чтобы это оптимизировать, нужно вмешаться там, где в шаблон передаётся коллекция `$articles` (скажем, в методе `renderDefault()`). Мы воспользуемся тем, что обработка сигнала происходит до методов `render<Something>`: ```php public function handleLike(int $articleId): void @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -Теперь при обработке сигнала в шаблон вместо коллекции со всеми статьями передается массив с одной единственной статьей - той, которую мы хотим отрисовать и отправить в payload в браузер. `{foreach}` таким образом выполнится только один раз, и никакие лишние сниппеты не будут отрисованы. +Теперь при обработке сигнала вместо всей коллекции статей в шаблон передаётся массив всего с одной нужной статьёй - той, которую мы собираемся отрисовать и отправить в payload в браузер. В результате цикл `{foreach}` выполняется только один раз, и лишние сниппеты не отрисовываются. -Путь компонентов -================ +Способ с компонентом +==================== -Совершенно другой способ решения избегает динамических сниппетов. Трюк заключается в переносе всей логики в отдельный компонент - теперь за ввод оценок будет отвечать не презентер, а выделенный `LikeControl`. Класс будет выглядеть следующим образом (кроме того, он будет содержать методы `render`, `handleUnlike` и т.д.): +Совершенно другой подход обходится без динамических сниппетов вообще. Хитрость в том, чтобы упаковать всю логику в отдельный компонент. Вместо того чтобы оценкой занимался презентер, ею будет управлять специальный `LikeControl`. Класс будет выглядеть так (в нём были бы также методы `render`, `handleUnlike` и другие): ```php class LikeControl extends Nette\Application\UI\Control @@ -139,14 +142,14 @@ class LikeControl extends Nette\Application\UI\Control ```latte {snippet} {if !$article->liked} - <a n:href="like!" class=ajax>Мне нравится</a> + <a n:href="like!" class=ajax>I like it</a> {else} - <a n:href="unlike!" class=ajax>Мне больше не нравится</a> + <a n:href="unlike!" class=ajax>I don't like it anymore</a> {/if} {/snippet} ``` -Конечно, нам придется изменить шаблон представления и добавить в презентер фабрику. Поскольку мы создадим компонент столько раз, сколько статей получим из базы данных, мы используем для его "размножения" класс [Multiplier |application:Multiplier]. +Разумеется, шаблон представления изменится, и нам понадобится добавить в презентер фабрику. Поскольку мы будем создавать экземпляр этого компонента для каждой статьи, полученной из базы данных, для управления их созданием мы используем класс [Multiplier |application:Multiplier]. ```php protected function createComponentLikeControl() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -Шаблон представления сократится до необходимого минимума (и будет полностью лишен сниппетов!): +Шаблон представления сокращается до минимума (и полностью обходится без сниппетов!): ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ protected function createComponentLikeControl() </article> ``` -Почти готово: приложение теперь будет работать с AJAX. Здесь нас также ждет оптимизация приложения, потому что из-за использования Nette Database при обработке сигнала из базы данных излишне загружаются все статьи вместо одной. Преимуществом, однако, является то, что их отрисовка не происходит, потому что рендерится действительно только наш компонент. +Мы почти закончили: теперь приложение работает с AJAX. И здесь тоже нужна оптимизация, потому что из-за использования Nette Database обработка сигнала зря загружает из базы все статьи вместо одной нужной. Преимущество, однако, в том, что лишней отрисовки не происходит: отрисовывается только конкретный экземпляр компонента. {{priority: -1}} diff --git a/best-practices/ru/editors-and-tools.texy b/best-practices/ru/editors-and-tools.texy deleted file mode 100644 index 7508f40017..0000000000 --- a/best-practices/ru/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Редакторы и инструменты -*********************** - -.[perex] -Вы можете быть опытным программистом, но только с хорошими инструментами вы станете мастером. В этой главе вы найдете советы по важным инструментам, редакторам и плагинам. - - -IDE редактор -============ - -Мы настоятельно рекомендуем использовать для разработки полноценную IDE, такую как PhpStorm, NetBeans, VS Code, а не просто текстовый редактор с поддержкой PHP. Разница действительно существенная. Нет причин довольствоваться простым редактором, который хоть и умеет подсвечивать синтаксис, но не достигает возможностей передовой IDE, которая точно подсказывает, отслеживает ошибки, умеет рефакторить код и многое другое. Некоторые IDE платные, другие даже бесплатные. - -**NetBeans IDE** имеет встроенную поддержку Nette, Latte и NEON. - -**PhpStorm**: установите эти плагины в `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: найдите в marketplace плагин "Nette Latte + Neon". - -Также свяжите Tracy с редактором. При отображении страницы ошибки можно будет кликнуть на имена файлов, и они откроются в редакторе с курсором на соответствующей строке. Прочтите, [как настроить систему|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan — это инструмент, который обнаруживает логические ошибки в коде до его запуска. - -Установим его с помощью Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Создадим в проекте конфигурационный файл `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -А затем позволим ему проанализировать классы в папке `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Исчерпывающую документацию вы найдете прямо на [сайте PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] проверяет и, при необходимости, исправляет некоторые формальные ошибки в ваших исходных кодах: - -- удаляет [BOM |nette:glossary#BOM] -- проверяет валидность шаблонов [Latte |latte:] -- проверяет валидность файлов `.neon`, `.php` и `.json` -- проверяет наличие [контрольных символов |nette:glossary#Управляющие символы] -- проверяет, закодирован ли файл в UTF-8 -- проверяет неправильно записанные `/* @anotace */` (отсутствует звездочка) -- удаляет завершающие `?>` в PHP файлах -- удаляет пробелы в конце строк и лишние строки в конце файла -- нормализует разделители строк к системным (если указана опция `-l`) - - -Composer -======== - -[Composer] — это инструмент для управления зависимостями в PHP. Он позволяет нам объявлять произвольно сложные зависимости отдельных библиотек и затем устанавливает их для нас в наш проект. - - -Requirements Checker -==================== - -Это был инструмент, который тестировал среду выполнения сервера и сообщал, можно ли (и в какой степени) использовать фреймворк. В настоящее время Nette можно использовать на любом сервере, имеющем минимально требуемую версию PHP. diff --git a/best-practices/ru/form-reuse.texy b/best-practices/ru/form-reuse.texy index 2c8dd3114c..74be343918 100644 --- a/best-practices/ru/form-reuse.texy +++ b/best-practices/ru/form-reuse.texy @@ -2,15 +2,15 @@ ************************************************ .[perex] -В Nette у вас есть несколько вариантов использования одной и той же формы в нескольких местах без дублирования кода. В этой статье мы рассмотрим различные решения, включая те, которых следует избегать. +Nette предлагает несколько способов использовать одну и ту же форму в нескольких местах, не дублируя код. Эта статья разбирает разные решения, в том числе те, которых стоит избегать. Фабрика форм ============ -Одним из основных подходов к использованию одного и того же компонента в нескольких местах является создание метода или класса, который генерирует этот компонент, и последующий вызов этого метода в разных частях приложения. Такой метод или класс называется *фабрикой*. Пожалуйста, не путайте с паттерном проектирования *factory method*, который описывает специфический способ использования фабрик и не связан с этой темой. +Основной подход к повторному использованию компонента в нескольких местах - создать метод или класс, который его порождает. Этот метод затем вызывается из разных мест приложения. Такой метод или класс называется *фабрикой*. Не путайте это с шаблоном проектирования *factory method*, который описывает конкретный способ использования фабрик и с этой темой напрямую не связан. -В качестве примера создадим фабрику, которая будет собирать форму редактирования: +Для примера создадим фабрику, собирающую форму редактирования: ```php use Nette\Application\UI\Form; @@ -20,22 +20,22 @@ class FormFactory public function createEditForm(): Form { $form = new Form; - $form->addText('title', 'Заголовок:'); - // здесь добавляются другие поля формы - $form->addSubmit('send', 'Отправить'); + $form->addText('title', 'Title:'); + // здесь добавляются остальные поля формы + $form->addSubmit('send', 'Save'); return $form; } } ``` -Теперь вы можете использовать эту фабрику в разных местах вашего приложения, например, в презентерах или компонентах. Для этого [запросим ее как зависимость|dependency-injection:passing-dependencies]. Сначала запишем класс в конфигурационный файл: +Теперь вы можете использовать эту фабрику в разных частях приложения, например в презентерах или компонентах. Делается это через [запрос её как зависимости |dependency-injection:passing-dependencies]. Сначала зарегистрируйте класс в конфигурационном файле: ```neon services: - FormFactory ``` -А затем используем ее в презентере: +Затем используйте её в презентере: ```php @@ -57,7 +57,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Фабрику форм можно расширить дополнительными методами для создания других видов форм в соответствии с потребностями вашего приложения. И, конечно, мы можем добавить метод, который создаст базовую форму без элементов, и этот метод будут использовать другие методы: +Вы можете расширять фабрику форм новыми методами, создающими другие виды форм по потребностям вашего приложения. И, разумеется, мы можем добавить метод, создающий базовую форму без элементов, которым затем воспользуются остальные методы: ```php class FormFactory @@ -71,21 +71,21 @@ class FormFactory public function createEditForm(): Form { $form = $this->createForm(); - $form->addText('title', 'Заголовок:'); - // здесь добавляются другие поля формы - $form->addSubmit('send', 'Отправить'); + $form->addText('title', 'Title:'); + // здесь добавляются остальные поля формы + $form->addSubmit('send', 'Save'); return $form; } } ``` -Метод `createForm()` пока не делает ничего полезного, но это быстро изменится. +Метод `createForm()` пока не делает ничего полезного, но скоро это изменится. Зависимости фабрики =================== -Со временем выяснится, что нам нужно, чтобы формы были многоязычными. Это означает, что всем формам нужно установить так называемый [translator |forms:rendering#Перевод]. Для этого изменим класс `FormFactory` так, чтобы он принимал объект `Translator` как зависимость в конструкторе, и передадим его форме: +Со временем может понадобиться, чтобы формы стали многоязычными. Это значит, что всем формам нужно задать [переводчик |forms:rendering#Перевод]. Чтобы этого добиться, измените класс `FormFactory` так, чтобы он принимал объект `Translator` как зависимость в конструкторе и передавал его создаваемой форме: ```php use Nette\Localization\Translator; @@ -108,13 +108,13 @@ class FormFactory } ``` -Поскольку метод `createForm()` вызывают и другие методы, создающие специфические формы, достаточно установить translator только в нем. И готово. Нет необходимости менять код какого-либо презентера или компонента, что замечательно. +Поскольку метод `createForm()` вызывается и другими методами, создающими конкретные формы, задать переводчик здесь достаточно. И готово. Менять код какого-либо презентера или компонента не нужно, и это прекрасно. -Несколько фабричных классов -=========================== +Несколько классов-фабрик +======================== -Альтернативно, вы можете создать несколько классов для каждой формы, которую хотите использовать в вашем приложении. Этот подход может повысить читаемость кода и упростить управление формами. Исходную `FormFactory` оставим создавать только чистую форму с базовой конфигурацией (например, с поддержкой переводов), а для формы редактирования создадим новую фабрику `EditFormFactory`. +Как вариант, вы можете создать отдельный класс-фабрику для каждой формы, которую собираетесь использовать в приложении. Такой подход может улучшить читаемость кода и упростить управление формами. Пусть исходная `FormFactory` создаёт только базовую форму с основными настройками (например, с поддержкой перевода), а для формы редактирования создадим новую фабрику `EditFormFactory`. ```php class FormFactory @@ -133,7 +133,7 @@ class FormFactory } -// ✅ использование композиции +// ✅ используем композицию class EditFormFactory { public function __construct( @@ -144,39 +144,39 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - // здесь добавляются другие поля формы - $form->addSubmit('send', 'Отправить'); + // здесь добавляются остальные поля формы + $form->addSubmit('send', 'Save'); return $form; } } ``` -Очень важно, чтобы связь между классами `FormFactory` и `EditFormFactory` была реализована [композицией |nette:introduction-to-object-oriented-programming#Композиция], а не [объектным наследованием |nette:introduction-to-object-oriented-programming#Наследование]: +Принципиально важно, что связь между классами `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', 'Отправить'); + $form->addText('title', 'Title:'); + // здесь добавляются остальные поля формы + $form->addSubmit('send', 'Save'); return $form; } } ``` -Использование наследования в этом случае было бы совершенно контрпродуктивным. Вы очень быстро столкнулись бы с проблемами. Например, в тот момент, когда вы захотели бы добавить параметры к методу `create()`; PHP выдал бы ошибку, что его сигнатура отличается от родительской. Или при передаче зависимости в класс `EditFormFactory` через конструктор. Возникла бы ситуация, которую мы называем [constructor hell |dependency-injection:passing-dependencies#Ад конструкторов]. +Наследование здесь было бы совершенно контрпродуктивно. Проблемы возникли бы очень быстро. Например, если бы вы захотели добавить параметры в метод `create()`, PHP выдал бы ошибку, потому что его сигнатура отличалась бы от родительской. Или при передаче зависимостей в класс `EditFormFactory` через конструктор. Это привело бы к тому, что известно как [ад конструкторов |dependency-injection:passing-dependencies#Ад конструкторов]. -В целом, лучше отдавать предпочтение [композиции перед наследованием |dependency-injection:faq#Почему композиция предпочтительнее наследования]. +Вообще лучше предпочитать [композицию наследованию |dependency-injection:faq#Почему композиция предпочтительнее наследования?]. Обработка формы =============== -Обработка формы, которая вызывается после успешной отправки, также может быть частью фабричного класса. Она будет работать так, что передаст отправленные данные модели для обработки. Возможные ошибки [передаст обратно |forms:validation#Ошибки при обработке] в форму. Модель в следующем примере представляет класс `Facade`: +Обработчик формы, вызываемый при успешной отправке, тоже может быть частью класса-фабрики. Он работает так, что передаёт отправленные данные на обработку в слой модели. Любые ошибки обработки передаются [обратно |forms:validation#Обработка ошибок] в форму. В следующем примере модель представлена классом `Facade`: ```php class EditFormFactory @@ -190,14 +190,14 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - $form->addText('title', 'Заголовок:'); - // здесь добавляются другие поля формы - $form->addSubmit('send', 'Отправить'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addText('title', 'Title:'); + // здесь добавляются остальные поля формы + $form->addSubmit('send', 'Save'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // обработка отправленных данных @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Само перенаправление оставим на презентере. Он добавит к событию `onSuccess` еще один обработчик, который выполнит перенаправление. Благодаря этому форму можно будет использовать в разных презентерах и в каждом перенаправлять в другое место. +А вот перенаправление пусть выполняет сам презентер. Он добавляет в событие `onSuccess` ещё один обработчик, который и перенаправляет. Благодаря этому форму можно использовать в разных презентерах, каждый из которых при успехе перенаправляет в своё место. ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -224,7 +224,7 @@ class MyPresenter extends Nette\Application\UI\Presenter { $form = $this->formFactory->create(); $form->onSuccess[] = function () { - $this->flashMessage('Запись была сохранена'); + $this->flashMessage('Record was saved'); $this->redirect('Homepage:'); }; return $form; @@ -232,24 +232,24 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Это решение использует свойство форм, что если над формой или ее элементом вызывается `addError()`, то следующий обработчик `onSuccess` уже не вызывается. +Это решение опирается на особенность форм: если у формы или у одного из её элементов вызван `addError()`, последующие обработчики `onSuccess` не вызываются. Наследование от класса Form =========================== -Собранная форма не должна быть потомком формы. Другими словами, не используйте это решение: +Собранная форма не должна быть потомком класса `Form`. Иными словами, избегайте такого подхода: ```php -// ⛔ ТАК НЕ НАДО! НАСЛЕДОВАНИЕ ЗДЕСЬ НЕУМЕСТНО +// ⛔ НЕТ! НАСЛЕДОВАНИЮ ЗДЕСЬ НЕ МЕСТО class EditForm extends Form { public function __construct(Translator $translator) { parent::__construct(); - $this->addText('title', 'Заголовок:'); - // здесь добавляются другие поля формы - $this->addSubmit('send', 'Отправить'); + $this->addText('title', 'Title:'); + // здесь добавляются остальные поля формы + $this->addSubmit('send', 'Save'); $this->setTranslator($translator); } } @@ -257,13 +257,13 @@ class EditForm extends Form Вместо сборки формы в конструкторе используйте фабрику. -Нужно понимать, что класс `Form` — это в первую очередь инструмент для сборки формы, то есть *form builder*. А собранную форму можно рассматривать как ее продукт. Но продукт не является специфическим случаем билдера, между ними нет связи *is a*, составляющей основу наследования. +Важно осознавать, что класс `Form` - это прежде всего инструмент для сборки форм, то есть *построитель форм*. Собранную форму можно считать его продуктом. Однако продукт не является разновидностью построителя; между ними нет отношения *является*, которое лежит в основе наследования. Компонент с формой ================== -Совершенно другой подход представляет собой создание [компонента|application:components], частью которого является форма. Это дает новые возможности, например, рендерить форму специфическим образом, поскольку частью компонента является и шаблон. Или можно использовать сигналы для AJAX-коммуникации и дозагрузки информации в форму, например, для подсказок и т.д. +Совершенно другой подход состоит в создании [компонента |application:components], который скрывает в себе форму. Это открывает новые возможности, например отрисовку формы особым образом, потому что у компонента есть собственный шаблон. Кроме того, можно использовать сигналы для AJAX-обмена и динамической подгрузки сведений в форму, например для подсказок и подобного. ```php @@ -281,15 +281,15 @@ class EditControl extends Nette\Application\UI\Control protected function createComponentForm(): Form { $form = new Form; - $form->addText('title', 'Заголовок:'); - // здесь добавляются другие поля формы - $form->addSubmit('send', 'Отправить'); - $form->onSuccess[] = [$this, 'processForm']; + $form->addText('title', 'Title:'); + // здесь добавляются остальные поля формы + $form->addSubmit('send', 'Save'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // обработка отправленных данных @@ -306,7 +306,7 @@ class EditControl extends Nette\Application\UI\Control } ``` -Еще создадим фабрику, которая будет производить этот компонент. Достаточно [записать ее интерфейс |application:components#Компоненты с зависимостями]: +Далее создадим фабрику, которая будет порождать этот компонент. Достаточно [определить её интерфейс |application:components#Компоненты с зависимостями]: ```php interface EditControlFactory @@ -315,14 +315,14 @@ interface EditControlFactory } ``` -И добавить в конфигурационный файл: +И добавить его в конфигурационный файл: ```neon services: - EditControlFactory ``` -А теперь уже можем запросить фабрику и использовать ее в презентере: +Теперь мы можем запросить фабрику и использовать её в презентере: ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // или перенаправим на результат редактирования, напр.: + // или перенаправление на результат редактирования, например: // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/ru/inject-method-attribute.texy b/best-practices/ru/inject-method-attribute.texy index 689ddbfa3c..c09a70b160 100644 --- a/best-practices/ru/inject-method-attribute.texy +++ b/best-practices/ru/inject-method-attribute.texy @@ -2,17 +2,17 @@ ************************ .[perex] -В этой статье мы рассмотрим различные способы передачи зависимостей в презентеры фреймворка Nette. Сравним предпочтительный способ, которым является конструктор, с другими возможностями, такими как методы и атрибуты `inject`. +Эта статья посвящена разным способам передачи зависимостей в презентеры фреймворка Nette. Мы сравним предпочтительный способ, внедрение через конструктор, с альтернативами в виде методов и атрибутов `inject`. -И для презентеров действует правило, что передача зависимостей с помощью [конструктора |dependency-injection:passing-dependencies#Передача через конструктор] является предпочтительным путем. Однако, если вы создаете общего предка, от которого наследуются другие презентеры (например, `BasePresenter`), и этот предок также имеет зависимости, возникает проблема, которую мы называем [constructor hell |dependency-injection:passing-dependencies#Ад конструкторов]. Ее можно обойти с помощью альтернативных путей, которые представляют собой методы и атрибуты (ранее аннотации) `inject`. +Для презентеров, как и для других классов, предпочтительным подходом является передача зависимостей через [конструктор |dependency-injection:passing-dependencies#Внедрение через конструктор]. Однако если вы создаёте общего предка, от которого наследуются другие презентеры (например, `BasePresenter`), и этому предку тоже нужны зависимости, может возникнуть проблема, известная как [ад конструкторов |dependency-injection:passing-dependencies#Ад конструкторов]. Её можно обойти альтернативными способами, а именно методами и атрибутами inject (раньше аннотациями). Методы `inject*()` ================== -Это форма передачи зависимости [сеттером |dependency-injection:passing-dependencies#Передача сеттером]. Название этих сеттеров начинается с префикса `inject`. Nette DI автоматически вызывает методы с таким названием сразу после создания экземпляра презентера и передает им все необходимые зависимости. Поэтому они должны быть объявлены как public. +Это разновидность передачи зависимостей через [сеттеры |dependency-injection:passing-dependencies#Внедрение через сеттер]. Имена таких сеттеров должны начинаться с префикса `inject`. Nette DI автоматически вызывает методы с такими именами сразу после создания экземпляра презентера и передаёт им все нужные зависимости. Поэтому они должны быть объявлены как public. -Методы `inject*()` можно рассматривать как своего рода расширение конструктора на несколько методов. Благодаря этому `BasePresenter` может принимать зависимости через другой метод и оставлять конструктор свободным для своих потомков: +Методы `inject*()` можно воспринимать как расширение конструктора, разбитое на несколько методов. Это позволяет `BasePresenter` получать свои зависимости отдельным методом, оставляя конструктор свободным для его потомков: ```php abstract class BasePresenter extends Nette\Application\UI\Presenter @@ -36,15 +36,15 @@ class MyPresenter extends BasePresenter } ``` -Презентер может содержать любое количество методов `inject*()`, и каждый может иметь любое количество параметров. Они отлично подходят также в случаях, когда презентер [составлен из трейтов |presenter-traits], и каждый из них требует свою собственную зависимость. +У презентера может быть сколько угодно методов `inject*()`, и каждый может принимать сколько угодно параметров. Такой подход хорошо подходит и для случаев, когда презентер [составлен из трейтов |presenter-traits] и каждому трейту нужны свои зависимости. Атрибуты `Inject` ================= -Это форма [инъекции в свойство |dependency-injection:passing-dependencies#Установка переменной]. Достаточно отметить, в какие свойства нужно инжектировать, и Nette DI автоматически передаст зависимости сразу после создания экземпляра презентера. Чтобы он мог их вставить, необходимо объявить их как public. +Это разновидность [внедрения в свойства |dependency-injection:passing-dependencies#Внедрение в свойство]. Достаточно пометить свойства, в которые нужно внедрить зависимости, и Nette DI автоматически передаст их сразу после создания экземпляра презентера. Чтобы внедрение было возможно, эти свойства должны быть объявлены как public. -Свойства помечаем атрибутом: (раньше использовалась аннотация `/** @inject */`) +Свойства помечаются атрибутом (раньше использовалась аннотация `/** @inject */`): ```php use Nette\DI\Attributes\Inject; // эта строка важна @@ -56,6 +56,6 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Преимуществом этого способа передачи зависимостей была очень лаконичная форма записи. Однако с появлением [constructor property promotion |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] кажется проще использовать конструктор. +Преимуществом этого способа передачи зависимостей была очень краткая запись. Однако с появлением [продвижения свойств конструктора |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 index 981a38df9b..f94b8b5113 100644 --- a/best-practices/ru/lets-create-contact-form.texy +++ b/best-practices/ru/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -Создаем контактную форму -************************ +Создадим форму обратной связи +***************************** .[perex] -Посмотрим, как в Nette создать контактную форму, включая отправку на email. Итак, приступим! +Разберём, как создать в Nette форму обратной связи, включая отправку введённых данных по электронной почте. Приступим! -Сначала нам нужно создать новый проект. Как это сделать, объясняется на странице [Начало работы |nette:installation]. А затем уже можно приступать к созданию формы. +Сначала нужно создать новый проект. Как это сделать, объясняет страница [Первые шаги |nette:installation]. Затем можно приступить к созданию формы. -Проще всего создать [форму прямо в презентере |forms:in-presenter]. Мы можем использовать готовый `HomePresenter`. В него добавим компонент `contactForm`, представляющий форму. Сделаем это так: запишем в код фабричный метод `createComponentContactForm()`, который создаст компонент: +Проще всего создать [форму прямо в презентере |forms:in-presenter]. Мы можем воспользоваться уже существующим `HomePresenter`. Мы добавим компонент с именем `contactForm`, представляющий нашу форму. Для этого добавим в код презентера фабричный метод `createComponentContactForm()`, который создаст компонент: ```php use Nette\Application\UI\Form; @@ -18,39 +18,36 @@ class HomePresenter extends Presenter { $form = new Form; $form->addText('name', 'Имя:') - ->setRequired('Введите имя'); + ->setRequired('Введите, пожалуйста, своё имя'); $form->addEmail('email', 'E-mail:') - ->setRequired('Введите e-mail'); - $form->addTextarea('message', 'Сообщение:') - ->setRequired('Введите сообщение'); + ->setRequired('Введите, пожалуйста, свой e-mail'); + $form->addTextArea('message', 'Сообщение:') + ->setRequired('Введите, пожалуйста, сообщение'); $form->addSubmit('send', 'Отправить'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { - // отправка email + // отправка письма } } ``` -Как видите, мы создали два метода. Первый метод `createComponentContactForm()` создает новую форму. У нее есть поля для имени, email и сообщения, которые мы добавляем методами `addText()`, `addEmail()` и `addTextArea()`. Также мы добавили кнопку для отправки формы. Но что, если пользователь не заполнит какое-то поле? В таком случае мы должны сообщить ему, что это обязательное поле. Этого мы добились с помощью метода `setRequired()`. Наконец, мы добавили также [событие |nette:glossary#События Events] `onSuccess`, которое сработает, если форма успешно отправлена. В нашем случае оно вызовет метод `contactFormSucceeded`, который позаботится об обработке отправленной формы. Это мы добавим в код через мгновение. +Как видите, мы создали два метода. Первый, `createComponentContactForm()`, создаёт новый экземпляр формы. В нём есть поля для имени, e-mail и сообщения, добавленные соответственно методами `addText()`, `addEmail()` и `addTextArea()`. Мы также добавили кнопку отправки. А что, если пользователь оставит поле пустым? В этом случае нам следует сообщить ему, что поле обязательно. Этого мы добились методом `setRequired()`. Наконец, мы привязали обработчик [события |nette:glossary#События] к `onSuccess`, который срабатывает при успешной отправке формы. В нашем случае он вызывает метод `contactFormSucceeded`, который займётся обработкой отправленных данных. Реализуем его чуть позже. -Компонент `contactForm` выведем в шаблоне `Home/default.latte`: +Отрисуем компонент `contactForm` в шаблоне `Home/default.latte`: ```latte {block content} -<h1>Контактная форма</h1> +<h1>Форма обратной связи</h1> {control contactForm} ``` -Для самой отправки email создадим новый класс, который назовем `ContactFacade` и разместим его в файле `app/Model/ContactFacade.php`: +Для самой отправки письма создадим новый класс с именем `ContactFacade` и поместим его в файл `app/Model/ContactFacade.php`: ```php -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -66,9 +63,9 @@ class ContactFacade public function sendMessage(string $email, string $name, string $message): void { $mail = new Message; - $mail->addTo('admin@example.com') // ваш email + $mail->addTo('admin@example.com') // ваш e-mail ->setFrom($email, $name) - ->setSubject('Сообщение из контактной формы') + ->setSubject('Сообщение из формы обратной связи') ->setBody($message); $this->mailer->send($mail); @@ -76,9 +73,9 @@ class ContactFacade } ``` -Метод `sendMessage()` создает и отправляет email. Для этого он использует так называемый mailer, который получает как зависимость через конструктор. Узнайте больше об [отправке email |mail:]. +Метод `sendMessage()` создаёт и отправляет письмо. Для этого он использует сервис отправки почты, который получает как зависимость через конструктор. Подробнее об [отправке писем |mail:]. -Теперь вернемся к презентеру и завершим метод `contactFormSucceeded()`. Он вызовет метод `sendMessage()` класса `ContactFacade` и передаст ему данные из формы. А как получить объект `ContactFacade`? Попросим передать его через конструктор: +Теперь вернёмся в презентер и допишем метод `contactFormSucceeded()`. Он вызовет метод `sendMessage()` класса `ContactFacade` и передаст данные, отправленные через форму. А как получить объект `ContactFacade`? Мы запросим его через конструктор с помощью внедрения зависимостей: ```php use App\Model\ContactFacade; @@ -100,26 +97,26 @@ class HomePresenter extends Presenter public function contactFormSucceeded(stdClass $data): void { $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Сообщение было отправлено'); + $this->flashMessage('Сообщение отправлено'); $this->redirect('this'); } } ``` -После того как email будет отправлен, мы еще покажем пользователю так называемое [flash-сообщение |application:components#Flash-сообщения], подтверждающее, что сообщение отправлено, а затем перенаправим на другую страницу, чтобы нельзя было повторно отправить форму с помощью *refresh* в браузере. +После отправки письма мы показываем пользователю [flash-сообщение |application:components#Flash-сообщения], подтверждающее отправку. Затем перенаправляем, чтобы форму нельзя было отправить повторно обновлением страницы в браузере. -Итак, если все работает, вы должны быть в состоянии отправить email из вашей контактной формы. Поздравляю! +Итак, если всё настроено правильно, теперь вы должны иметь возможность отправить письмо из своей формы обратной связи. Поздравляем! -HTML шаблон email ------------------ +HTML-шаблон письма +------------------ -Пока отправляется простой текстовый email, содержащий только сообщение, отправленное формой. Но в email мы можем использовать HTML и сделать его вид более привлекательным. Создадим для него шаблон в Latte, который запишем в `app/Model/contactEmail.latte`: +Сейчас отправляется обычное текстовое письмо, содержащее только сообщение, отправленное через форму. Однако мы можем использовать в письме HTML, чтобы его вид стал привлекательнее. Создадим для него шаблон на Latte и сохраним как `app/Model/contactEmail.latte`: ```latte <html> - <title>Сообщение из контактной формы + Сообщение из формы обратной связи

    Имя: {$name}

    @@ -129,7 +126,7 @@ HTML шаблон email ``` -Остается изменить `ContactFacade`, чтобы он использовал этот шаблон. В конструкторе запросим класс `LatteFactory`, который умеет создавать объект `Latte\Engine`, то есть [рендерер Latte шаблонов |latte:develop#Как отобразить шаблон]. С помощью метода `renderToString()` отрендерим шаблон в строку, первым параметром является путь к шаблону, а вторым — переменные. +Остаётся изменить `ContactFacade`, чтобы он использовал этот шаблон. В конструкторе мы запросим класс `LatteFactory`, который умеет создавать объект `Latte\Engine`, [отрисовщик шаблонов Latte |latte:develop#Как отрисовать шаблон]. Методом `renderToString()` мы отрисуем шаблон в строку. Первый параметр - путь к файлу шаблона, второй - массив переменных, которые в него передаются. ```php namespace App\Model; @@ -156,7 +153,7 @@ class ContactFacade ]); $mail = new Message; - $mail->addTo('admin@example.com') // ваш email + $mail->addTo('admin@example.com') // ваш e-mail ->setFrom($email, $name) ->setHtmlBody($body); @@ -165,15 +162,15 @@ class ContactFacade } ``` -Сгенерированный HTML email затем передадим методу `setHtmlBody()` вместо исходного `setBody()`. Также нам не нужно указывать тему email в `setSubject()`, потому что библиотека возьмет ее из элемента `` шаблона. +Получившееся HTML-содержимое письма мы затем передаём в метод `setHtmlBody()` вместо исходного `setBody()`. Тему письма через `setSubject()` указывать тоже не нужно, потому что библиотека автоматически берёт её из элемента `<title>` в шаблоне. -Конфигурация ------------- +Настройка +--------- -В коде класса `ContactFacade` все еще жестко прописан наш администраторский email `admin@example.com`. Было бы лучше перенести его в конфигурационный файл. Как это сделать? +В коде класса `ContactFacade` наш e-mail администратора `admin@example.com` по-прежнему прописан жёстко. Лучше было бы перенести его в конфигурационный файл. Как это сделать? -Сначала изменим класс `ContactFacade` и заменим строку с email переменной, переданной через конструктор: +Сначала изменим класс `ContactFacade`, заменив жёстко прописанную строку с e-mail переменной, передаваемой через конструктор: ```php class ContactFacade @@ -197,21 +194,21 @@ class ContactFacade } ``` -А вторым шагом является указание значения этой переменной в конфигурации. В файл `app/config/services.neon` запишем: +Второй шаг - задать значение этой переменной в конфигурации. В файл `app/config/services.neon` добавьте: ```neon services: - App\Model\ContactFacade(adminEmail: admin@example.com) ``` -И это все. Если бы записей в секции `services` было много и у вас было бы ощущение, что email среди них теряется, мы можем сделать из него переменную. Изменим запись на: +Вот и всё. Если в секции `services` много записей и вам кажется, что адрес электронной почты в них теряется, его можно превратить в параметр. Измените запись так: ```neon services: - App\Model\ContactFacade(adminEmail: %adminEmail%) ``` -А в файле `app/config/common.neon` определим эту переменную: +А сам параметр определите в файле `app/config/common.neon`: ```neon parameters: diff --git a/best-practices/ru/microsites.texy b/best-practices/ru/microsites.texy index c82e82b71a..d7bb6f89af 100644 --- a/best-practices/ru/microsites.texy +++ b/best-practices/ru/microsites.texy @@ -1,11 +1,11 @@ -Как писать микро-сайты -********************** +Как писать микросайты +********************* -Представьте, что вам нужно быстро создать небольшой сайт для предстоящего мероприятия вашей компании. Он должен быть простым, быстрым и без лишних сложностей. Возможно, вы думаете, что для такого маленького проекта вам не нужен надежный фреймворк. Но что, если использование фреймворка Nette может существенно упростить и ускорить этот процесс? +Представьте, что вам нужно быстро сделать небольшой сайт к предстоящему мероприятию вашей компании. Он должен быть простым, быстрым и без лишних усложнений. Вы можете подумать, что для такого маленького проекта мощный фреймворк не нужен. Но что, если использование Nette Framework на самом деле упростит и ускорит этот процесс? -Ведь даже при создании простых сайтов вы не хотите отказываться от удобства. Вы не хотите изобретать то, что уже было решено. Будьте спокойно ленивы и позвольте себя побаловать. Nette Framework можно отлично использовать и как микро-фреймворк. +Даже создавая простые сайты, вы не хотите жертвовать удобством. Вы не хотите заново изобретать то, что уже решено. Спокойно ленитесь и позвольте себя побаловать. Nette Framework отлично подходит и для использования в роли микрофреймворка. -Как может выглядеть такой микросайт? Например, так, что весь код сайта мы разместим в одном файле `index.php` в публичной папке: +Как может выглядеть такой микросайт? Например, весь код сайта может уместиться в один файл `index.php` в публичном каталоге: ```php <?php @@ -16,7 +16,7 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// создаем DI-контейнер на основе конфигурации в config.neon +// создаём DI-контейнер на основе конфигурации в config.neon $configurator->addConfig(__DIR__ . '/../app/config.neon'); $container = $configurator->createContainer(); @@ -26,15 +26,15 @@ $container->addService('router', $router); // маршрут для URL https://example.com/ $router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // определяем язык браузера и перенаправляем на URL /en или /de и т.д. + // определяем язык браузера и перенаправляем на URL /en или /de и так далее $supportedLangs = ['en', 'de', 'cs']; $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); // маршрут для URL https://example.com/cs или https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // отображаем соответствующий шаблон, например ../templates/en.latte +$router->addRoute('<lang cs|en|de>', function ($presenter, string $lang) { + // выводим подходящий шаблон, например ../templates/en.latte $template = $presenter->createTemplate() ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); return $template; @@ -44,20 +44,20 @@ $router->addRoute('<lang cs|en>', function ($presenter, string $lang) { $container->getByType(Nette\Application\Application::class)->run(); ``` -Все остальное будут шаблоны, сохраненные в родительской папке `/templates`. +Всё остальное будет шаблонами, лежащими в родительском каталоге `/templates`. -PHP-код в `index.php` сначала [подготавливает среду |bootstrap:], затем определяет [маршруты |application:routing#Динамическая маршрутизация с callback-функциями] и, наконец, запускает приложение. Преимущество в том, что второй параметр функции `addRoute()` может быть callable, который выполнится после открытия соответствующей страницы. +PHP-код в `index.php` сначала [настраивает окружение |bootstrap:], затем задаёт [маршруты |application:routing#Динамическая маршрутизация с callback-функциями] и наконец запускает приложение. Преимущество в том, что вторым параметром функции `addRoute()` может быть callable, который выполняется при обращении к соответствующей странице. -Зачем использовать Nette для микросайта? ----------------------------------------- +Зачем использовать Nette для микросайтов? +----------------------------------------- -- Программисты, которые когда-либо пробовали [Tracy|tracy:], сегодня не могут представить себе программирование без нее. -- Прежде всего, вы воспользуетесь системой шаблонов [Latte|latte:], потому что уже со 2 страниц вы захотите иметь разделенный [макет и контент|latte:template-inheritance]. -- И вы определенно хотите положиться на [автоматическое экранирование |latte:safety-first], чтобы не возникла уязвимость XSS -- Nette также гарантирует, что при ошибке никогда не отобразятся программистские сообщения об ошибках PHP, а пользователю понятная страница. -- Если вы хотите получать обратную связь от пользователей, например, в виде контактной формы, то вы еще добавите [формы|forms:] и [базу данных|database:]. -- Заполненные формы вы также можете легко [отправлять по email|mail:]. -- Иногда вам может пригодиться [кеширование|caching:], например, если вы скачиваете и отображаете фиды. +- Программисты, попробовавшие [Tracy|tracy:], сегодня едва ли представляют себе разработку без неё. +- Прежде всего вам пригодится система шаблонов [Latte|latte:], потому что даже при двух страницах вам захочется разделить [макет и содержимое|latte:template-inheritance]. +- И вы наверняка захотите положиться на [автоматическое экранирование |latte:safety-first], чтобы предотвратить XSS-уязвимости. +- Nette также следит за тем, чтобы при ошибке никогда не отображались сырые сообщения об ошибках PHP: вместо них показывается дружелюбная страница. +- Если вы хотите собирать отзывы пользователей, скажем через форму обратной связи, вы легко добавите поддержку [форм|forms:] и [базы данных|database:]. +- Заполненные формы можно легко [отправлять по электронной почте|mail:]. +- Иногда может пригодиться [кеширование|caching:], например при загрузке и выводе лент. -В наше время, когда скорость и эффективность являются ключевыми, важно иметь инструменты, которые позволят вам достигать результатов без лишних задержек. Фреймворк Nette предлагает именно это - быструю разработку, безопасность и широкий спектр инструментов, таких как Tracy и Latte, которые упрощают процесс. Достаточно установить несколько пакетов Nette, и создание такого микросайта становится совершенно простым делом. И вы знаете, что нигде не скрывается никакой дыры в безопасности. +В сегодняшнем быстром мире, где скорость и эффективность решают, жизненно важно иметь инструменты, позволяющие получать результат без лишних задержек. Nette Framework предлагает именно это - быструю разработку, безопасность и широкий набор инструментов вроде Tracy и Latte, которые упрощают процесс. Достаточно установить несколько пакетов Nette, и создание такого микросайта становится невероятно простым. И вы можете быть уверены, что скрытых уязвимостей в нём нет. diff --git a/best-practices/ru/pagination.texy b/best-practices/ru/pagination.texy index 15756e6d22..8929e32ffa 100644 --- a/best-practices/ru/pagination.texy +++ b/best-practices/ru/pagination.texy @@ -1,10 +1,10 @@ -Пагинация результатов базы данных -********************************* +Постраничный вывод результатов из базы данных +********************************************* .[perex] -При создании веб-приложений очень часто возникает требование ограничить количество выводимых элементов на странице. +Разрабатывая веб-приложения, вы часто сталкиваетесь с требованием ограничить количество выводимых элементов на странице - приёмом, известным как пагинация. -Начнем с состояния, когда мы выводим все данные без пагинации. Для выбора данных из базы данных у нас есть класс `ArticleRepository`, который, помимо конструктора, содержит метод `findPublishedArticles`, возвращающий все опубликованные статьи, отсортированные по убыванию даты публикации. +Начнём с состояния, когда мы выводим все данные без пагинации. Для выборки данных из базы у нас есть класс `ArticleRepository`. Помимо конструктора в нём есть метод `findPublishedArticles`, возвращающий все опубликованные статьи, отсортированные по убыванию даты публикации. ```php namespace App\Model; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -В презентере мы затем инжектируем класс модели и в методе рендеринга запрашиваем опубликованные статьи, которые передаем в шаблон: +Затем мы внедряем этот класс модели в презентер. В методе render получаем опубликованные статьи и передаём их в шаблон: ```php namespace App\Presentation\Home; @@ -52,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -В шаблоне `default.latte` затем позаботимся о выводе статей: +Шаблон `default.latte` затем позаботится о выводе статей: ```latte {block content} @@ -67,11 +67,11 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Таким образом, мы умеем выводить все статьи, что, однако, начнет вызывать проблемы, когда количество статей возрастет. В этот момент пригодится реализация механизма пагинации. +Так мы можем вывести все статьи, но это становится проблемой по мере роста их количества. Тогда и пригождается механизм пагинации. -Он обеспечит разделение всех статей на несколько страниц, и мы будем отображать только статьи текущей страницы. Общее количество страниц и распределение статей вычислит [Paginator |utils:Paginator] сам, исходя из того, сколько всего у нас статей и сколько статей мы хотим отображать на странице. +Этот механизм делит все статьи на несколько страниц, и мы показываем только те статьи, которые относятся к выбранной странице. Общее число страниц и распределение статей вычисляет вспомогательный класс [Paginator |utils:Paginator] на основе общего числа статей и желаемого числа статей на странице. -На первом шаге мы изменим метод получения статей в классе репозитория так, чтобы он мог возвращать только статьи для одной страницы. Также добавим метод для определения общего количества статей в базе данных, который нам понадобится для настройки Paginator: +На первом шаге изменим в классе репозитория метод получения статей так, чтобы он мог возвращать статьи только для одной страницы. Добавим также метод получения общего числа статей в базе, который нужен для настройки Paginator: ```php namespace App\Model; @@ -99,7 +99,7 @@ class ArticleRepository } /** - * Возвращает общее количество опубликованных статей + * Возвращает общее число опубликованных статей */ public function getPublishedArticlesCount(): int { @@ -108,9 +108,9 @@ class ArticleRepository } ``` -Затем приступим к изменениям в презентере. В метод рендеринга будем передавать номер текущей отображаемой страницы. На случай, если этот номер не будет частью URL, установим значение по умолчанию — первая страница. +Далее изменим презентер. Мы передадим в метод `renderDefault` номер текущей страницы. Если этого номера нет в URL, зададим значение по умолчанию 1 (первая страница). -Далее также расширим метод рендеринга получением экземпляра Paginator, его настройкой и выбором правильных статей для отображения в шаблоне. `HomePresenter` после изменений будет выглядеть так: +Мы также расширим метод render, чтобы создать и настроить экземпляр Paginator и выбрать подходящие статьи для вывода в шаблоне. Изменённый `HomePresenter` будет выглядеть так: ```php namespace App\Presentation\Home; @@ -127,27 +127,27 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Узнаем общее количество опубликованных статей + // Получаем общее число опубликованных статей $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Создадим экземпляр Paginator и настроим его + // Создаём и настраиваем экземпляр Paginator $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // общее количество статей - $paginator->setItemsPerPage(10); // количество элементов на странице + $paginator->setItemCount($articlesCount); // общее число элементов + $paginator->setItemsPerPage(10); // число элементов на странице $paginator->setPage($page); // номер текущей страницы - // Из базы данных извлечем ограниченное количество статей согласно расчету Paginator + // Получаем из базы ограниченный набор статей по расчёту Paginator $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - // которую передадим в шаблон + // передаём их в шаблон $this->template->articles = $articles; - // а также сам Paginator для отображения опций пагинации + // а также сам Paginator для вывода элементов пагинации $this->template->paginator = $paginator; } } ``` -Шаблон теперь уже итерирует только по статьям одной страницы, нам остается добавить ссылки пагинации: +Шаблон теперь обходит только статьи текущей страницы. Нам остаётся добавить ссылки пагинации: ```latte {block content} @@ -164,7 +164,7 @@ class HomePresenter extends Nette\Application\UI\Presenter {if !$paginator->isFirst()} <a n:href="default, 1">Первая</a>  |  - <a n:href="default, $paginator->page-1">Предыдущая</a> + <a n:href="default, $paginator->getPage() - 1">Предыдущая</a>  |  {/if} @@ -180,9 +180,9 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Таким образом, мы дополнили страницу возможностью пагинации с помощью Paginator. В случае, когда вместо [Nette Database Core |database:sql-way] в качестве слоя базы данных мы используем [Nette Database Explorer |database:explorer], мы можем реализовать пагинацию и без использования Paginator. Класс `Nette\Database\Table\Selection` содержит метод [page |api:Nette\Database\Table\Selection::_page] с логикой пагинации, взятой из Paginator. +На этом реализация пагинации с помощью 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; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -В презентере нам не нужно создавать Paginator, вместо него мы используем метод класса `Selection`, который возвращает репозиторий: +В презентере нам не нужно создавать экземпляр Paginator. Вместо этого мы используем метод `page()` объекта `Selection`, возвращённого из репозитория: ```php namespace App\Presentation\Home; @@ -222,21 +222,21 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Извлечем опубликованные статьи + // Получаем опубликованные статьи $articles = $this->articleRepository->findPublishedArticles(); - // и в шаблон отправим только их часть, ограниченную согласно расчету метода page + // и передаём в шаблон только их часть, ограниченную расчётом метода page $lastPage = 0; $this->template->articles = $articles->page($page, 10, $lastPage); - // а также необходимые данные для отображения опций пагинации + // а также данные, нужные для вывода элементов пагинации $this->template->page = $page; $this->template->lastPage = $lastPage; } } ``` -Поскольку в шаблон мы теперь не передаем Paginator, изменим часть, отображающую ссылки пагинации: +Поскольку объект Paginator в шаблон мы больше не передаём, нужно изменить часть, выводящую ссылки пагинации: ```latte {block content} @@ -268,6 +268,6 @@ class HomePresenter extends Nette\Application\UI\Presenter </div> ``` -Таким образом, мы реализовали механизм пагинации без использования Paginator. +Так мы реализовали механизм пагинации без явного использования вспомогательного класса Paginator. {{priority: -1}} diff --git a/best-practices/ru/passing-settings-to-presenters.texy b/best-practices/ru/passing-settings-to-presenters.texy index b65b1186c6..7e2dd4013f 100644 --- a/best-practices/ru/passing-settings-to-presenters.texy +++ b/best-practices/ru/passing-settings-to-presenters.texy @@ -2,9 +2,9 @@ ****************************** .[perex] -Вам нужно передавать в презентеры аргументы, которые не являются объектами (например, информацию о том, работает ли приложение в режиме отладки, пути к каталогам и т.д.), и поэтому не могут быть переданы автоматически с помощью autowiring? Решением является инкапсуляция их в объект `Settings`. +Нужно передать в презентеры не объектные аргументы (например, флаг режима отладки, пути к каталогам и подобное), которые нельзя передать автоматически через autowiring? Решение - упаковать их в отдельный объект `Settings`. -Сервис `Settings` представляет собой очень простой и в то же время полезный способ предоставления информации о работающем приложении презентерам. Его конкретный вид зависит исключительно от ваших конкретных потребностей. Пример: +Сервис `Settings` даёт очень простой, но действенный способ снабдить презентеры сведениями о работающем приложении. Его конкретная структура полностью зависит от ваших нужд. Пример: ```php namespace App; @@ -12,7 +12,7 @@ namespace App; class Settings { public function __construct( - // с PHP 8.1 можно указать readonly + // начиная с PHP 8.1 можно использовать readonly public bool $debugMode, public string $appDir, // и так далее @@ -30,7 +30,7 @@ services: ) ``` -Когда презентеру понадобится информация, предоставляемая этим сервисом, он просто запросит ее в конструкторе: +Когда презентеру нужны сведения, предоставляемые этим сервисом, он просто запрашивает его в своём конструкторе: ```php class MyPresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/ru/post-links.texy b/best-practices/ru/post-links.texy index 59daf4a9e5..6560e736a0 100644 --- a/best-practices/ru/post-links.texy +++ b/best-practices/ru/post-links.texy @@ -1,30 +1,30 @@ -Как правильно использовать POST ссылки +Как правильно использовать POST-ссылки ************************************** .[perex] -В веб-приложениях, особенно в административных интерфейсах, основным правилом должно быть то, что действия, изменяющие состояние сервера, не должны выполняться посредством HTTP-метода GET. Как следует из названия метода, GET должен служить только для получения данных, а не для их изменения. Для действий, таких как удаление записей, предпочтительнее использовать метод POST. Хотя идеальным был бы метод DELETE, но его нельзя вызвать без JavaScript, поэтому исторически используется POST. +В веб-приложениях, особенно в административных интерфейсах, основным правилом должно быть то, что действия, изменяющие состояние на сервере, не выполняются HTTP-методом GET. Как следует из названия, GET должен служить только для получения данных, а не для их изменения. Для действий вроде удаления записей уместнее метод POST. Идеальным был бы метод DELETE, но вызвать его без JavaScript нельзя, поэтому исторически для таких действий используется POST. -Как это сделать на практике? Используйте этот простой трюк. В начале шаблона создайте вспомогательную форму с идентификатором `postForm`, которую затем используете для кнопок удаления: +Как реализовать это на практике? Воспользуйтесь простым приёмом. В начале шаблона макета создайте вспомогательную форму с идентификатором `postForm`. Затем вы будете использовать её для действий вроде кнопок удаления: ```latte .{file:@layout.latte} <form method="post" id="postForm"></form> ``` -Благодаря этой форме вы можете вместо классической ссылки `<a>` использовать кнопку `<button>`, которую можно визуально стилизовать так, чтобы она выглядела как обычная ссылка. Например, CSS-фреймворк Bootstrap предлагает классы `btn btn-link`, с помощью которых вы добьетесь того, что кнопка не будет визуально отличаться от других ссылок. С помощью атрибута `form="postForm"` мы свяжем ее с подготовленной формой: +Благодаря этой форме вместо обычной ссылки `<a>` вы можете использовать `<button>`. Эту кнопку можно оформить так, чтобы она выглядела как обычная ссылка. Например, CSS-фреймворк Bootstrap предлагает классы `btn btn-link`, благодаря которым кнопка визуально не отличается от других ссылок. Атрибутом `form="postForm"` привяжите кнопку к подготовленной вспомогательной форме: ```latte .{file:admin.latte} <table> <tr n:foreach="$posts as $post"> <td>{$post->title}</td> <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">удалить</button> - <!-- вместо <a n:href="delete $post->id">удалить</a> --> + <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]`: +Нажатие на эту кнопку теперь вызывает действие `delete`. Чтобы запросы принимались только методом POST и только с того же домена (действенная защита от CSRF-атак), используйте атрибут `#[Requires]`: ```php .{file:AdminPresenter.php} use Nette\Application\Attributes\Requires; @@ -34,15 +34,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter #[Requires(methods: 'POST', sameOrigin: true)] public function actionDelete(int $id): void { - $this->facade->deletePost($id); // гипотетический код, удаляющий запись + $this->facade->deletePost($id); // гипотетический код удаления записи $this->redirect('default'); } } ``` -Атрибут существует с Nette Application 3.2, и больше о его возможностях вы узнаете на странице [Как использовать атрибут #Requires |attribute-requires]. +Этот атрибут доступен начиная с Nette Application 3.2. Подробнее о его возможностях можно узнать на странице [Как использовать атрибут #Requires |attribute-requires]. -Если бы вы вместо действия `actionDelete()` использовали сигнал `handleDelete()`, не нужно указывать `sameOrigin: true`, потому что сигналы имеют эту защиту, установленную по умолчанию: +Если вместо действия `actionDelete()` вы использовали сигнал `handleDelete()`, указывать `sameOrigin: true` не нужно, потому что у сигналов эта защита включена по умолчанию: ```php .{file:AdminPresenter.php} #[Requires(methods: 'POST')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -Этот подход не только улучшает безопасность вашего приложения, но и способствует соблюдению правильных веб-стандартов и практик. Используя методы POST для действий, изменяющих состояние, вы достигнете более надежного и безопасного приложения. +Такой подход не только повышает безопасность вашего приложения, но и способствует соблюдению правильных веб-стандартов и практик. Использование методов POST для действий, меняющих состояние, делает приложение надёжнее и безопаснее. diff --git a/best-practices/ru/presenter-traits.texy b/best-practices/ru/presenter-traits.texy index 96d90aef15..180271a230 100644 --- a/best-practices/ru/presenter-traits.texy +++ b/best-practices/ru/presenter-traits.texy @@ -1,14 +1,14 @@ -Компоновка презентеров из трейтов -********************************* +Составление презентеров из трейтов +********************************** .[perex] -Если нам нужно реализовать один и тот же код в нескольких презентерах (например, проверку, что пользователь авторизован), предлагается разместить код в общем предке. Вторая возможность — создание одноцелевых [трейтов |nette:introduction-to-object-oriented-programming#Трейты]. +Если вам нужно реализовать одну и ту же функциональность в нескольких презентерах (например, проверку входа пользователя), обычным подходом будет разместить код в общем предке. Другой вариант - создать узконаправленные [трейты |nette:introduction-to-object-oriented-programming#Трейты]. -Преимущество этого решения в том, что каждый из презентеров может использовать именно те трейты, которые ему действительно нужны, в то время как множественное наследование в PHP невозможно. +Преимущество трейтов в том, что каждый презентер может подключить только те трейты, которые ему действительно нужны, тем более что множественное наследование в PHP не поддерживается. -Эти трейты могут использовать тот факт, что при создании презентера последовательно вызываются все [inject методы |inject-method-attribute#Методы inject]. Необходимо только следить за тем, чтобы имя каждого inject метода было уникальным. +Эти трейты могут пользоваться тем, что все [методы inject |inject-method-attribute#Методы inject*()] вызываются друг за другом при создании экземпляра презентера. Нужно лишь позаботиться о том, чтобы имя каждого метода inject было уникальным среди всех используемых трейтов и самого презентера. -Трейты могут навешивать инициализационный код на события [onStartup или onRender |application:presenters#События]. +Трейты могут привязать код инициализации к событиям [onStartup или onRender |application:presenters#События]. Примеры: diff --git a/best-practices/ru/pretty-urls.texy b/best-practices/ru/pretty-urls.texy new file mode 100644 index 0000000000..4f0f0b0e73 --- /dev/null +++ b/best-practices/ru/pretty-urls.texy @@ -0,0 +1,204 @@ +Красивые 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 index 19f5c12df1..39d3b0a835 100644 --- a/best-practices/ru/restore-request.texy +++ b/best-practices/ru/restore-request.texy @@ -1,16 +1,16 @@ -Как вернуться к предыдущей странице? -************************************ +Как вернуться на предыдущую страницу? +************************************* .[perex] -Что, если пользователь заполняет форму, а его сессия истекает? Чтобы он не потерял данные, перед перенаправлением на страницу входа мы сохраним данные в сессию. В Nette это совершенно просто. +Что произойдёт, если пользователь заполняет форму, а его сессия входа истечёт? Чтобы не потерять данные, мы можем сохранить текущий запрос (включая данные формы) в сессию перед перенаправлением на страницу входа. В Nette это на удивление просто. -Текущий запрос можно сохранить в сессию с помощью метода `storeRequest()`, который возвращает его идентификатор в виде короткой строки. Метод сохраняет имя текущего презентера, представление и его параметры. В случае, если была отправлена и форма, сохраняется также содержимое полей (за исключением загруженных файлов). +Текущий запрос можно сохранить в сессию методом `storeRequest()`. Этот метод возвращает уникальный идентификатор (короткую строку) сохранённого запроса. Метод сохраняет имя текущего презентера, его представление и параметры. Если в составе запроса была отправлена форма, сохраняются и значения, введённые в поля (кроме загруженных файлов). -Восстановление запроса выполняет метод `restoreRequest($key)`, которому мы передаем полученный идентификатор. Он перенаправляет на исходный презентер и представление. Однако, если сохраненный запрос содержит отправку формы, он перейдет на исходный презентер методом `forward()`, передаст форме ранее заполненные значения и позволит ее снова отрисовать. Таким образом, пользователь имеет возможность повторно отправить форму, и никакие данные не теряются. +Запрос восстанавливается методом `restoreRequest($key)`, которому вы передаёте полученный ранее идентификатор. Этот метод перенаправляет пользователя обратно в исходные презентер и представление. Однако если сохранённый запрос содержал отправку формы, `restoreRequest()` вместо перенаправления использует метод `forward()`. Он передаёт в форму ранее заполненные значения и позволяет отрисовать её снова. Благодаря этому пользователь может отправить форму заново, не потеряв введённые данные. -Важно, что `restoreRequest()` проверяет, является ли вновь вошедший пользователь тем же, кто изначально заполнял форму. Если нет, запрос отбрасывается, и ничего не происходит. +Принципиально важно, что `restoreRequest()` проверяет, является ли только что вошедший пользователь тем же самым, который изначально отправил форму. Если пользователь другой, сохранённый запрос не восстанавливается, и метод ничего не делает, что повышает безопасность. -Покажем все на примере. Пусть у нас есть презентер `AdminPresenter`, в котором редактируются данные и в методе `startup()` которого проверяется, авторизован ли пользователь. Если нет, перенаправляем его на `SignPresenter`. Одновременно сохраняем текущий запрос и его ключ отправляем в `SignPresenter`. +Проиллюстрируем это примером. Представьте `AdminPresenter`, где редактируются данные. Его метод `startup()` проверяет, вошёл ли пользователь. Если нет, пользователь перенаправляется в `SignPresenter`. Одновременно мы сохраняем текущий запрос через `storeRequest()` и передаём его ключ (`$backlink`) в `SignPresenter`. ```php class AdminPresenter extends Nette\Application\UI\Presenter @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Презентер `SignPresenter` будет содержать, помимо формы для входа, также персистентный параметр `$backlink`, в который запишется ключ. Поскольку параметр персистентный, он будет передаваться и после отправки формы входа. +`SignPresenter` будет содержать, помимо формы входа, постоянный параметр `$backlink`, в котором хранится ключ. Поскольку параметр постоянный, его значение сохраняется и после отправки формы входа. ```php @@ -41,13 +41,13 @@ class SignPresenter extends Nette\Application\UI\Presenter { $form = new Nette\Application\UI\Form; // ... добавляем поля формы ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { - // ... здесь авторизуем пользователя ... + // ... здесь выполняем вход пользователя ... $this->restoreRequest($this->backlink); $this->redirect('Admin:'); @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -Методу `restoreRequest()` передаем ключ сохраненного запроса, и он перенаправляет (или переходит) на исходный презентер. +Мы передаём ключ (`$this->backlink`) сохранённого запроса в метод `restoreRequest()`. Он затем перенаправляет (или перебрасывает) пользователя обратно в исходные презентер и представление. -Однако, если ключ недействителен (например, его уже нет в сессии), метод ничего не делает. Затем следует вызов `$this->redirect('Admin:')`, который перенаправляет на `AdminPresenter`. +Однако если ключ недействителен (например, истёк в сессии), метод ничего не делает. Поэтому последующий вызов `$this->redirect('Admin:')` служит запасным вариантом и перенаправляет на страницу по умолчанию, например в `AdminPresenter`. {{priority: -1}} diff --git a/best-practices/sl/@home.texy b/best-practices/sl/@home.texy deleted file mode 100644 index a4cf0efbec..0000000000 --- a/best-practices/sl/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Navodila in postopki -******************** - -.[perex] -Navodila, rešitve pogostih nalog in *najboljše prakse* za Nette. - - -<div class=documentation> -<div> - - -Nette Aplikacije ----------------- -- [Metode in atributi inject |inject-method-attribute] -- [Sestavljanje presenterjev iz traitov |presenter-traits] -- [Posredovanje nastavitev v presenterje |passing-settings-to-presenters] -- [Kako se vrniti na prejšnjo stran |restore-request] -- [Strankanje rezultatov podatkovne baze |pagination] -- [Dinamični odrezki |dynamic-snippets] -- [Kako uporabljati atribut #Requires |attribute-requires] -- [Kako pravilno uporabljati POST povezave |post-links] - -</div> -<div> - - -Obrazci -------- -- [Ponovna uporaba obrazcev |form-reuse] -- [Obrazec za ustvarjanje in urejanje zapisa |creating-editing-form] -- [Ustvarjamo kontaktni obrazec |lets-create-contact-form] -- [Odvisni selectboxi |https://blog.nette.org/sl/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Splošno -------- -- [Kako naložiti konfiguracijsko datoteko |bootstrap:] -- [Kako pisati mikro-spletne strani |microsites] -- [Zakaj Nette uporablja PascalCase notacijo konstant? |https://blog.nette.org/sl/for-less-screaming-in-the-code] -- [Zakaj Nette ne uporablja pripone Interface? |https://blog.nette.org/sl/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: nasveti za uporabo |composer] -- [Nasveti za urejevalnike & orodja |editors-and-tools] -- [Uvod v objektno orientirano programiranje |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Primeri rešitev ---------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Videi ------ -Stotine posnetkov iz Poslednjih sobot in videov o Nette najdete pod eno streho na "Youtube kanalu Nette Frameworka":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/sl/@meta.texy b/best-practices/sl/@meta.texy deleted file mode 100644 index f58ad17850..0000000000 --- a/best-practices/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Navodila in postopki}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/sl/attribute-requires.texy b/best-practices/sl/attribute-requires.texy deleted file mode 100644 index 8fb0588c40..0000000000 --- a/best-practices/sl/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Kako uporabljati atribut `#[Requires]` -************************************** - -.[perex] -Ko pišete spletno aplikacijo, se pogosto srečate s potrebo po omejitvi dostopa do določenih delov vaše aplikacije. Morda želite, da lahko nekateri zahtevki pošiljajo podatke samo s pomočjo obrazca (torej z metodo POST), ali da so dostopni samo za AJAX klice. V Nette Frameworku 3.2 se je pojavilo novo orodje, ki vam omogoča takšne omejitve nastaviti zelo elegantno in pregledno: atribut `#[Requires]`. - -Atribut je posebna oznaka v PHP, ki jo dodate pred definicijo razreda ali metode. Ker gre pravzaprav za razred, da bi vam naslednji primeri delovali, je treba navesti klavzulo use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Atribut `#[Requires]` lahko uporabite pri samem razredu presenterja in tudi na teh metodah: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Zadnji dve metodi se nanašata tudi na komponente, torej atribut lahko uporabljate tudi pri njih. - -Če pogoji, ki jih atribut navaja, niso izpolnjeni, pride do sprožitve HTTP napake 4xx. - - -Metode HTTP ------------ - -Lahko specificirate, katere HTTP metode (kot GET, POST itd.) so za dostop dovoljene. Na primer, če želite dovoliti dostop samo s pošiljanjem obrazca, nastavite: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Zakaj bi morali uporabljati POST namesto GET za akcije, ki spreminjajo stanje, in kako to storiti? [Preberite navodilo |post-links]. - -Lahko navedete metodo ali polje metod. Poseben primer je vrednost `'*'`, ki dovoli vse metode, kar standardno presenterji iz [varnostnih razlogov ne dovoljujejo |application:presenters#Preverjanje HTTP metode]. - - -AJAX klici ----------- - -Če želite, da je presenter ali metoda dostopna samo za AJAX zahtevke, uporabite: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Isti izvor ----------- - -Za povečanje varnosti lahko zahtevate, da je zahtevek narejen iz iste domene. S tem preprečite [ranljivost CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Pri metodah `handle<Signal>()` je dostop iz iste domene zahtevan samodejno. Torej, če nasprotno želite dovoliti dostop iz katerekoli domene, navedite: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Dostop prek posredovanja ------------------------- - -Včasih je koristno omejiti dostop do presenterja tako, da je dostopen samo posredno, na primer z uporabo metode `forward()` ali `switch()` iz drugega presenterja. Tako se na primer ščitijo error-presenterji, da jih ni mogoče poklicati iz URL-ja: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -V praksi je pogosto treba označiti določene poglede (views), do katerih je mogoče priti šele na podlagi logike v presenterju. Torej spet, da jih ni mogoče odpreti neposredno: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Konkretne akcije ----------------- - -Lahko tudi omejite, da bo določena koda, na primer ustvarjanje komponente, dostopna samo za specifične akcije v presenterju: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -V primeru ene akcije ni treba zapisovati polja: `#[Requires(actions: 'default')]` - - -Lastni atributi ---------------- - -Če želite atribut `#[Requires]` uporabiti večkrat z isto nastavitvijo, si lahko ustvarite lasten atribut, ki bo dedoval `#[Requires]` in ga nastavil po potrebi. - -Na primer `#[SingleAction]` bo omogočil dostop samo prek akcije `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Ali `#[RestMethods]` bo omogočil dostop prek vseh HTTP metod, uporabljenih za REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Zaključek ---------- - -Atribut `#[Requires]` vam daje veliko fleksibilnosti in nadzora nad tem, kako so vaše spletne strani dostopne. S pomočjo preprostih, a močnih pravil lahko povečate varnost in pravilno delovanje vaše aplikacije. Kot vidite, lahko uporaba atributov v Nette vaše delo ne samo olajša, ampak tudi zavaruje. diff --git a/best-practices/sl/composer.texy b/best-practices/sl/composer.texy deleted file mode 100644 index bcfe1802c9..0000000000 --- a/best-practices/sl/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: nasveti za uporabo -**************************** - -<div class=perex> - -Composer je orodje za upravljanje odvisnosti v PHP. Omogoča nam, da naštejemo knjižnice, od katerih je naš projekt odvisen, in jih bo za nas nameščal in posodabljal. Pokazali bomo: - -- kako namestiti Composer -- njegovo uporabo v novem ali obstoječem projektu - -</div> - - -Namestitev -========== - -Composer je izvedljiva datoteka `.phar`, ki jo prenesete in namestite na naslednji način: - - -Windows -------- - -Uporabite uradni namestitveni program [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Dovolj so 4 ukazi, ki jih kopirate s [te strani |https://getcomposer.org/download/]. - -Nato z vstavitvijo v mapo, ki je v sistemskem `PATH`, postane Composer dostopen globalno: - -```shell -$ mv ./composer.phar ~/bin/composer # ali /usr/local/bin/composer -``` - - -Uporaba v projektu -================== - -Da bi lahko v svojem projektu začeli uporabljati Composer, potrebujete samo datoteko `composer.json`. Ta opisuje odvisnosti našega projekta in lahko vsebuje tudi druge metapodatke. Osnovni `composer.json` torej lahko izgleda takole: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Tukaj pravimo, da naša aplikacija (ali knjižnica) zahteva paket `nette/database` (ime paketa sestoji iz imena organizacije in imena projekta) in želi različico, ki ustreza pogoju `^3.0` (tj. najnovejšo različico 3). - -Imamo torej v korenu projekta datoteko `composer.json` in zaženemo namestitev: - -```shell -composer update -``` - -Composer bo prenesel Nette Database v mapo `vendor/`. Nato bo ustvaril datoteko `composer.lock`, ki vsebuje informacije o tem, katere različice knjižnic je točno namestil. - -Composer bo generiral datoteko `vendor/autoload.php`, ki jo lahko preprosto vključimo in začnemo uporabljati knjižnice brez kakršnegakoli dodatnega dela: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Posodabljanje paketov na najnovejše različice -============================================= - -Za posodabljanje uporabljenih knjižnic na najnovejše različice glede na pogoje, definirane v `composer.json`, skrbi ukaz `composer update`. Npr. pri odvisnosti `"nette/database": "^3.0"` bo namestil najnovejšo različico 3.x.x, vendar ne več različice 4. - -Za posodobitev pogojev v datoteki `composer.json`, na primer na `"nette/database": "^4.1"`, da bi bilo mogoče namestiti najnovejšo različico, uporabite ukaz `composer require nette/database`. - -Za posodobitev vseh uporabljenih paketov Nette bi bilo treba vse v ukazni vrstici našteti, npr.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Kar je nepraktično. Uporabite zato preprost skript "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, ki to stori za vas: - -```shell -php composer-frontline.php -``` - - -Ustvarjanje novega projekta -=========================== - -Nov projekt na Nette ustvarite s pomočjo enega samega ukaza: - -```shell -composer create-project nette/web-project ime-projekta -``` - -Kot `ime-projekta` vstavite ime mape za svoj projekt in potrdite. Composer bo prenesel repozitorij `nette/web-project` z GitHuba, ki že vsebuje datoteko `composer.json`, in takoj zatem Nette Framework. Moralo bi že zadostovati samo [nastaviti dovoljenja |nette:troubleshooting#Nastavitev pravic map] za pisanje v mape `temp/` in `log/` in projekt bi moral oživeti. - -Če veste, na kateri različici PHP bo projekt gostoval, ne pozabite [jo nastaviti |#Različica PHP]. - - -Različica PHP -============= - -Composer vedno namešča tiste različice paketov, ki so združljive z različico PHP, ki jo pravkar uporabljate (bolje rečeno z različico PHP, uporabljeno v ukazni vrstici pri zagonu Composerja). Kar pa najverjetneje ni ista različica, kot jo uporablja vaše gostovanje. Zato je zelo pomembno, da si v datoteko `composer.json` dodate informacijo o različici PHP na gostovanju. Nato se bodo nameščale samo različice paketov, združljive z gostovanjem. - -To, da bo projekt tekel na primer na PHP 8.2.3, nastavimo z ukazom: - -```shell -composer config platform.php 8.2.3 -``` - -Tako se različica zapiše v datoteko `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Vendar se številka različice PHP navaja še na drugem mestu datoteke, in sicer v sekciji `require`. Medtem ko prva številka določa, za katero različico se bodo nameščali paketi, druga številka pravi, za katero različico je napisana sama aplikacija. In po njej na primer PhpStorm nastavlja *PHP language level*. (Seveda nima smisla, da bi se te različice razlikovale, zato je dvojni zapis nedomišljenost.) To različico nastavite z ukazom: - -```shell -composer require php 8.2.3 --no-update -``` - -Ali neposredno v datoteki `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignoriranje različice PHP -========================= - -Paketi praviloma imajo navedeno tako najnižjo različico PHP, s katero so združljivi, kot tudi najvišjo, s katero so testirani. Če nameravate uporabljati še novejšo različico PHP, na primer zaradi testiranja, bo Composer zavrnil namestitev takšnega paketa. Rešitev je možnost `--ignore-platform-req=php+`, ki povzroči, da bo Composer ignoriral zgornje meje zahtevane različice PHP. - - -Lažna sporočila -=============== - -Pri nadgradnji paketov ali spremembah številk različic se zgodi, da pride do konflikta. En paket ima zahteve, ki so v nasprotju z drugim in podobno. Composer pa včasih izpisuje lažna sporočila. Poroča o konfliktu, ki realno ne obstaja. V takem primeru pomaga izbrisati datoteko `composer.lock` in poskusiti znova. - -Če sporočilo o napaki vztraja, potem je mišljeno resno in je treba iz njega razbrati, kaj in kako urediti. - - -Packagist.org - centralni repozitorij -===================================== - -[Packagist |https://packagist.org] je glavni repozitorij, v katerem Composer poskuša iskati pakete, če mu ne povemo drugače. Tukaj lahko objavimo tudi lastne pakete. - - -Kaj če ne želimo uporabljati centralnega repozitorija? ------------------------------------------------------- - -Če imamo znotrajpodjetniške aplikacije, ki jih preprosto ne moremo gostovati javno, si zanje ustvarimo podjetniški repozitorij. - -Več na temo repozitorijev [v uradni dokumentaciji |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Samodejno nalaganje -=================== - -Ključna lastnost Composerja je, da zagotavlja samodejno nalaganje za vse z njim nameščene razrede, ki ga zaženete z vključitvijo datoteke `vendor/autoload.php`. - -Vendar je mogoče uporabljati Composer tudi za nalaganje drugih razredov izven mape `vendor`. Prva možnost je, da pustite Composerju preiskati definirane mape in podmape, najti vse razrede in jih vključiti v samodejni nalagalnik. To dosežete z nastavitvijo `autoload > classmap` v `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # vključi mapo src/ in njene podmape - ] - } -} -``` - -Nato je treba ob vsaki spremembi zagnati ukaz `composer dumpautoload` in pustiti, da se tabele samodejnega nalaganja ponovno generirajo. To je izjemno neprijetno in veliko bolje je to nalogo zaupati [RobotLoaderju|robot-loader:], ki isto dejavnost izvaja samodejno v ozadju in veliko hitreje. - -Druga možnost je upoštevati [PSR-4|https://www.php-fig.org/psr/psr-4/]. Poenostavljeno rečeno gre za sistem, kjer imenski prostori in imena razredov ustrezajo strukturi map in imenom datotek, torej npr. `App\Core\RouterFactory` bo v datoteki `/path/to/App/Core/RouterFactory.php`. Primer konfiguracije: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # imenski prostor App\ je v mapi app/ - } - } -} -``` - -Kako natančno konfigurirati obnašanje, boste izvedeli v [dokumentaciji Composerja|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testiranje novih različic -========================= - -Želite preizkusiti novo razvojno različico paketa. Kako to storiti? Najprej v datoteko `composer.json` dodajte ta par možnosti, ki dovoli nameščanje razvojnih različic paketov, vendar se k temu zateče samo v primeru, da ne obstaja nobena kombinacija stabilnih različic, ki bi ustrezala zahtevam: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Nato priporočamo izbris datoteke `composer.lock`, včasih namreč Composer nerazumljivo zavrne namestitev in to težavo reši. - -Recimo, da gre za paket `nette/utils` in nova različica ima številko 4.0. Namestite jo z ukazom: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ali pa lahko namestite konkretno različico, na primer 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Ko pa je od knjižnice odvisen drug paket, ki je zaklenjen na starejšo različico (npr. `^3.1`), je idealno paket posodobiti, da bo deloval z novo različico. Če pa želite omejitev samo zaobiti in prisiliti Composer, da namesti razvojno različico in se pretvarja, da gre za starejšo različico (npr. 3.1.6), lahko uporabite ključno besedo `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Klicanje ukazov -=============== - -Prek Composerja lahko kličete lastne vnaprej pripravljene ukaze in skripte, kot da bi šlo za izvorne ukaze Composerja. Pri skriptih, ki se nahajajo v mapi `vendor/bin`, ni treba te mape navajati. - -Kot primer si definiramo v datoteki `composer.json` skript, ki s pomočjo [Nette Testerja|tester:] zažene teste: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Teste nato zaženemo s pomočjo `composer tester`. Ukaz lahko pokličemo tudi v primeru, da nismo v korenski mapi projekta, ampak v katerem od poddirektorijev. - - -Pošljite zahvalo -================ - -Pokazali vam bomo trik, s katerim boste razveselili avtorje odprte kode. Na preprost način boste na GitHubu dali zvezdico knjižnicam, ki jih vaš projekt uporablja. Dovolj je namestiti knjižnico `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -In nato zagnati: - -```shell -composer thanks -``` - -Poskusite! - - -Konfiguracija -============= - -Composer je tesno povezan z orodjem za verzioniranje [Git |https://git-scm.com]. Če ga nimate nameščenega, je treba Composerju povedati, naj ga ne uporablja: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/sl/creating-editing-form.texy b/best-practices/sl/creating-editing-form.texy deleted file mode 100644 index 650812cb3a..0000000000 --- a/best-practices/sl/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Obrazec za ustvarjanje in urejanje zapisa -***************************************** - -.[perex] -Kako v Nette pravilno implementirati dodajanje in urejanje zapisa, pri čemer za oboje uporabimo isti obrazec? - -V mnogih primerih so obrazci za dodajanje in urejanje zapisa enaki, razlikujejo se morda le po napisu na gumbu. Prikazali bomo primere preprostih presenterjev, kjer bomo obrazec najprej uporabili za dodajanje zapisa, nato za urejanje in na koncu obe rešitvi združili. - - -Dodajanje zapisa ----------------- - -Primer presenterja, ki služi za dodajanje zapisa. Samo delo s podatkovno bazo bomo prepustili razredu `Facade`, katerega koda za prikaz ni bistvena. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... dodamo polja obrazca ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // dodajanje zapisa v podatkovno bazo - $this->flashMessage('Uspešno dodano'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Urejanje zapisa ---------------- - -Zdaj si poglejmo, kako bi izgledal presenter, ki služi za urejanje zapisa: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // preverjanje obstoja zapisa - || !$this->facade->isEditAllowed(/*...*/) // preverjanje dovoljenj - ) { - $this->error(); // napaka 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // preverimo, da je akcija 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... dodamo polja obrazca ... - - $form->setDefaults($this->record); // nastavitev privzetih vrednosti - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // posodobitev zapisa - $this->flashMessage('Uspešno posodobljeno'); - $this->redirect('...'); - } -} -``` - -V metodi *action*, ki se zažene takoj na začetku [življenjskega cikla presenterja |application:presenters#Življenjski cikel presenterja], preverimo obstoj zapisa in dovoljenje uporabnika za urejanje. - -Zapis shranimo v lastnost `$record`, da ga imamo na voljo v metodi `createComponentRecordForm()` za nastavitev privzetih vrednosti in v `recordFormSucceeded()` zaradi ID-ja. Alternativna rešitev bi bila nastavitev privzetih vrednosti neposredno v `actionEdit()` in pridobitev vrednosti ID, ki je del URL-ja, s pomočjo `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // preverjanje obstoja in preverjanje dovoljenj - ) { - $this->error(); - } - - // nastavitev privzetih vrednosti obrazca - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Vendar pa, in to bi moralo biti **najpomembnejše spoznanje celotne kode**, se moramo pri ustvarjanju obrazca prepričati, da je akcija resnično `edit`. Ker sicer preverjanje v metodi `actionEdit()` sploh ne bi potekalo! - - -Isti obrazec za dodajanje in urejanje -------------------------------------- - -In zdaj oba presenterja združimo v enega. Ali bi lahko v metodi `createComponentRecordForm()` razlikovali, za katero akcijo gre, in glede na to konfigurirali obrazec, ali pa to prepustimo neposredno action-metodam in se znebimo pogoja: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // preverjanje obstoja zapisa - || !$this->facade->isEditAllowed(/*...*/) // preverjanje dovoljenj - ) { - $this->error(); // napaka 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // nastavitev privzetih vrednosti - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // preverimo, da je akcija 'add' ali 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... dodamo polja obrazca ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // dodajanje zapisa v podatkovno bazo - $this->flashMessage('Uspešno dodano'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // posodobitev zapisa - $this->flashMessage('Uspešno posodobljeno'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/sl/dynamic-snippets.texy b/best-practices/sl/dynamic-snippets.texy deleted file mode 100644 index 1f5704cd9c..0000000000 --- a/best-practices/sl/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Dinamični snippeti -****************** - -Precej pogosto se pri razvoju aplikacij pojavi potreba po izvajanju AJAX operacij, na primer nad posameznimi vrsticami tabele ali elementi seznama. Za primer lahko izberemo izpis člankov, pri čemer pri vsakem od njih prijavljenemu uporabniku omogočimo izbiro ocene "všeč mi je/ni mi všeč". Koda presenterja in ustrezne predloge brez AJAX-a bo izgledala približno takole (navajam najpomembnejše odseke, koda predvideva obstoj storitve za označevanje ocen in pridobivanje zbirke člankov - konkretna implementacija za namene tega navodila ni pomembna): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Predloga: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>všeč mi je</a> - {else} - <a n:href="unlike! $article->id" class=ajax>ni mi več všeč</a> - {/if} -</article> -``` - - -Ajaxizacija -=========== - -Zdaj pa opremimo to preprosto aplikacijo z AJAX-om. Sprememba ocene članka ni tako pomembna, da bi moralo priti do preusmeritve, zato bi idealno morala potekati z AJAX-om v ozadju. Uporabili bomo [pomožni skript iz dodatkov |application:ajax#Naja] z običajno konvencijo, da imajo AJAX povezave CSS razred `ajax`. - -Vendar kako to storiti konkretno? Nette ponuja 2 poti: pot t.i. dinamičnih snippetov in pot komponent. Obe imata svoje prednosti in slabosti, zato si ju bomo ogledali eno za drugo. - - -Pot dinamičnih snippetov -======================== - -Dinamični snippet v terminologiji Latte pomeni specifičen primer uporabe značke `{snippet}`, kjer je v imenu snippeta uporabljena spremenljivka. Takšen snippet se v predlogi ne more nahajati kjerkoli - mora biti ovit s statičnim snippetom, tj. običajnim, ali znotraj `{snippetArea}`. Našo predlogo bi lahko prilagodili na naslednji način. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>všeč mi je</a> - {else} - <a n:href="unlike! $article->id" class=ajax>ni mi več všeč</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Vsak članek zdaj definira en snippet, ki ima v imenu ID članka. Vsi ti snippeti so nato skupaj zaviti v en snippet z imenom `articlesContainer`. Če bi ta ovojni snippet izpustili, bi nas Latte na to opozoril z izjemo. - -Ostane nam še, da v presenter dodamo ponovno izrisovanje - dovolj je ponovno izrisati statični ovoj. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- ni potrebno - } else { - $this->redirect('this'); - } -} -``` - -Podobno prilagodimo tudi sestrsko metodo `handleUnlike()`, in AJAX deluje! - -Rešitev pa ima eno senčno stran. Če bi podrobneje preučili, kako poteka AJAX zahteva, bi ugotovili, da čeprav se aplikacija navzven zdi varčna (vrne samo en sam snippet za določen članek), je v resnici na strežniku izrisala vse snippete. Želeni snippet nam je postavila v payload, ostale pa zavrgla (popolnoma nepotrebno jih je torej tudi pridobila iz podatkovne baze). - -Da bi ta proces optimizirali, bomo morali poseči tja, kjer v predlogo posredujemo zbirko `$articles` (recimo v metodi `renderDefault()`). Izkoristili bomo dejstvo, da obdelava signalov poteka pred metodami `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Zdaj se pri obdelavi signala v predlogo namesto zbirke z vsemi članki posreduje le polje z enim samim člankom - tistim, ki ga želimo izrisati in poslati v payloadu v brskalnik. `{foreach}` se torej izvede samo enkrat in nobeni dodatni snippeti se ne izrišejo. - - -Pot komponent -============= - -Popolnoma drugačen način reševanja se izogne dinamičnim snippetom. Trik je v prenosu celotne logike v posebno komponento - za vnos ocen ne bo več skrbel presenter, temveč namenska `LikeControl`. Razred bo izgledal takole (poleg tega bo vseboval tudi metode `render`, `handleUnlike` itd.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Predloga komponente: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>všeč mi je</a> - {else} - <a n:href="unlike!" class=ajax>ni mi več všeč</a> - {/if} -{/snippet} -``` - -Seveda se nam bo spremenila predloga pogleda (view) in v presenter bomo morali dodati tovarno. Ker bomo komponento ustvarili tolikokrat, kolikor člankov pridobimo iz podatkovne baze, bomo za njeno "razmnoževanje" uporabili razred [Multiplier |application:multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Predloga pogleda (view) se zmanjša na nujni minimum (in je popolnoma brez snippetov!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Skoraj smo končali: aplikacija bo zdaj delovala AJAX-ovsko. Tudi tukaj nas čaka optimizacija aplikacije, saj se zaradi uporabe Nette Database pri obdelavi signala nepotrebno naložijo vsi članki iz podatkovne baze namesto enega. Prednost pa je, da ne pride do njihovega izrisovanja, ker se dejansko izriše samo naša komponenta. - -{{priority: -1}} diff --git a/best-practices/sl/editors-and-tools.texy b/best-practices/sl/editors-and-tools.texy deleted file mode 100644 index 9ac5d2ff3c..0000000000 --- a/best-practices/sl/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Urejevalniki & orodja -********************* - -.[perex] -Lahko ste spreten programer, vendar šele z dobrimi orodji postanete mojster. V tem poglavju boste našli nasvete za pomembna orodja, urejevalnike in vtičnike. - - -IDE urejevalnik -=============== - -Vsekakor priporočamo, da za razvoj uporabljate polnopravno IDE, kot so na primer PhpStorm, NetBeans, VS Code, in ne le urejevalnika besedil s podporo za PHP. Razlika je resnično bistvena. Ni razloga, da bi se zadovoljili zgolj z urejevalnikom, ki sicer zna obarvati sintakso, vendar ne dosega zmožnosti vrhunskega IDE-ja, ki natančno predlaga, preverja napake, zna refaktorirati kodo in še veliko več. Nekateri IDE-ji so plačljivi, drugi celo brezplačni. - -**NetBeans IDE** ima podporo za Nette, Latte in NEON že vgrajeno. - -**PhpStorm**: namestite te vtičnike v `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: v tržnici (marketplace) poiščite vtičnik "Nette Latte + Neon". - -Povežite tudi Tracy z urejevalnikom. Pri prikazu strani z napako bo potem mogoče klikniti na imena datotek, ki se bodo odprla v urejevalniku s kazalcem na ustrezni vrstici. Preberite, [kako konfigurirati sistem|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan je orodje, ki odkrije logične napake v kodi, preden jo zaženete. - -Namestimo ga s pomočjo Composerja: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -V projektu ustvarimo konfiguracijsko datoteko `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Nato pustimo, da analizira razrede v mapi `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Izčrpno dokumentacijo najdete neposredno na [straneh PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] preveri in po potrebi popravi nekatere formalne napake v vaši izvorni kodi: - -- odstranjuje [BOM |nette:glossary#BOM] -- preverja veljavnost predlog [Latte |latte:] -- preverja veljavnost datotek `.neon`, `.php` in `.json` -- preverja pojav [kontrolnih znakov |nette:glossary#Kontrolni znaki] -- preverja, ali je datoteka kodirana v UTF-8 -- preverja napačno zapisane `/* @anotacije */` (manjka zvezdica) -- odstranjuje zaključno oznako `?>` pri PHP datotekah -- odstranjuje presledke na desni strani in nepotrebne vrstice na koncu datoteke -- normalizira ločila vrstic na sistemska (če navedete možnost `-l`) - - -Composer -======== - -[Composer |best-practices:composer] je orodje za upravljanje odvisnosti v PHP. Omogoča nam deklariranje poljubno zapletenih odvisnosti posameznih knjižnic in jih nato za nas namesti v naš projekt. - - -Requirements Checker -==================== - -To je bilo orodje, ki je testiralo izvajalno okolje strežnika in obveščalo, ali (in v kolikšni meri) je mogoče ogrodje uporabljati. Trenutno je Nette mogoče uporabljati na vsakem strežniku, ki ima minimalno zahtevano različico PHP. diff --git a/best-practices/sl/form-reuse.texy b/best-practices/sl/form-reuse.texy deleted file mode 100644 index 4a388acd07..0000000000 --- a/best-practices/sl/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Ponovna uporaba obrazcev na več mestih -************************************** - -.[perex] -V Nette imate na voljo več možnosti, kako uporabiti isti obrazec na več mestih in ne podvajati kode. V tem članku si bomo ogledali različne rešitve, vključno s tistimi, ki se jim morate izogibati. - - -Tovarna obrazcev -================ - -Eden od osnovnih pristopov k uporabi iste komponente na več mestih je ustvarjanje metode ali razreda, ki to komponento generira, in nato klicanje te metode na različnih mestih aplikacije. Takšni metodi ali razredu pravimo *tovarna*. Prosimo, ne zamenjujte z oblikovalskim vzorcem *factory method*, ki opisuje specifičen način uporabe tovarn in ni povezan s to temo. - -Kot primer bomo ustvarili tovarno, ki bo sestavljala urejevalni obrazec: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Zdaj lahko to tovarno uporabite na različnih mestih v vaši aplikaciji, na primer v presenterjih ali komponentah. In sicer tako, da jo [zahtevamo kot odvisnost|dependency-injection:passing-dependencies]. Najprej torej razred zapišemo v konfiguracijsko datoteko: - -```neon -services: - - FormFactory -``` - -Nato jo uporabimo v presenterju: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // obdelava poslanih podatkov - }; - return $form; - } -} -``` - -Tovarno obrazcev lahko razširite z dodatnimi metodami za ustvarjanje drugih vrst obrazcev glede na potrebe vaše aplikacije. In seveda lahko dodamo tudi metodo, ki ustvari osnovni obrazec brez elementov, in to bodo uporabljale druge metode: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Metoda `createForm()` zaenkrat ne počne ničesar uporabnega, vendar se bo to hitro spremenilo. - - -Odvisnosti tovarne -================== - -Sčasoma se bo izkazalo, da potrebujemo, da so obrazci večjezični. To pomeni, da moramo vsem obrazcem nastaviti t.i. [prevajalnik |forms:rendering#Prevajanje]. V ta namen bomo prilagodili razred `FormFactory`, da bo sprejemal objekt `Translator` kot odvisnost v konstruktorju, in ga posredovali obrazcu: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Ker metodo `createForm()` kličejo tudi druge metode, ki ustvarjajo specifične obrazce, je dovolj, da prevajalnik nastavimo samo v njej. In končali smo. Ni treba spreminjati kode nobenega presenterja ali komponente, kar je odlično. - - -Več tovarniških razredov -======================== - -Alternativno lahko ustvarite več razredov za vsak obrazec, ki ga želite uporabiti v svoji aplikaciji. Ta pristop lahko poveča berljivost kode in olajša upravljanje obrazcev. Prvotno `FormFactory` bomo pustili, da ustvarja samo čist obrazec z osnovno konfiguracijo (na primer s podporo za prevode), za urejevalni obrazec pa bomo ustvarili novo tovarno `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ uporaba kompozicije -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Zelo pomembno je, da je povezava med razredoma `FormFactory` in `EditFormFactory` realizirana s [kompozicijo |nette:introduction-to-object-oriented-programming#Kompozicija], ne pa z [objektnim dedovanjem |nette:introduction-to-object-oriented-programming#Dedovanje]: - -```php -// ⛔ TAKOLE NE! SEM DEDOVANJE NE SPADA -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Uporaba dedovanja bi bila v tem primeru popolnoma kontraproduktivna. Na težave bi naleteli zelo hitro. Na primer v trenutku, ko bi želeli metodi `create()` dodati parametre; PHP bi javil napako, da se njena signatura razlikuje od starševske. Ali pri posredovanju odvisnosti v razred `EditFormFactory` prek konstruktorja. Nastala bi situacija, ki ji pravimo [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -Na splošno je bolje dati prednost [kompoziciji pred dedovanjem |dependency-injection:faq#Zakaj se daje prednost kompoziciji pred dedovanjem]. - - -Obdelava obrazca -================ - -Obdelava obrazca, ki se pokliče po uspešnem pošiljanju, je lahko tudi del tovarniškega razreda. Delovala bo tako, da bo poslana podatke posredovala modelu v obdelavo. Morebitne napake [posreduje nazaj |forms:validation#Napake pri obdelavi] v obrazec. Model v naslednjem primeru predstavlja razred `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // obdelava poslanih podatkov - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Samo preusmeritev pa bomo prepustili presenterju. Ta bo dogodku `onSuccess` dodal še en handler, ki bo izvedel preusmeritev. Zaradi tega bo mogoče obrazec uporabiti v različnih presenterjih in v vsakem preusmeriti drugam. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Zapis je bil shranjen'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Ta rešitev izkorišča lastnost obrazcev, da ko se nad obrazcem ali njegovim elementom pokliče `addError()`, se naslednji handler `onSuccess` ne pokliče več. - - -Dedovanje od razreda Form -========================= - -Sestavljen obrazec ne sme biti potomec obrazca. Z drugimi besedami, ne uporabljajte te rešitve: - -```php -// ⛔ TAKOLE NE! SEM DEDOVANJE NE SPADA -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $this->addSubmit('send', 'Pošlji'); - $this->setTranslator($translator); - } -} -``` - -Namesto sestavljanja obrazca v konstruktorju uporabite tovarno. - -Treba se je zavedati, da je razred `Form` v prvi vrsti orodje za sestavljanje obrazca, torej *form builder*. In sestavljen obrazec lahko razumemo kot njen produkt. Vendar produkt ni specifičen primer graditelja (builder), med njimi ni povezave *is a*, ki tvori osnovo dedovanja. - - -Komponenta z obrazcem -===================== - -Popolnoma drugačen pristop predstavlja ustvarjanje [komponente|application:components], katere del je obrazec. To daje nove možnosti, na primer izrisovanje obrazca na specifičen način, saj je del komponente tudi predloga. Ali pa je mogoče uporabiti signale za AJAX komunikacijo in nalaganje informacij v obrazec, na primer za predlaganje itd. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // obdelava poslanih podatkov - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // sprožitev dogodka - $this->onSave($this, $data); - } -} -``` - -Ustvarili bomo še tovarno, ki bo izdelovala to komponento. Dovolj je [zapisati njen vmesnik |application:components#Komponente z odvisnostmi]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -In dodati v konfiguracijsko datoteko: - -```neon -services: - - EditControlFactory -``` - -In zdaj lahko že zahtevamo tovarno in jo uporabimo v presenterju: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ali preusmerimo na rezultat urejanja, npr.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/sl/inject-method-attribute.texy b/best-practices/sl/inject-method-attribute.texy deleted file mode 100644 index e09c5e2471..0000000000 --- a/best-practices/sl/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Metode in atributi inject -************************* - -.[perex] -V tem članku se bomo osredotočili na različne načine posredovanja odvisnosti v presenterje v ogrodju Nette. Primerjali bomo prednostni način, ki je konstruktor, z drugimi možnostmi, kot so metode in atributi `inject`. - -Tudi za presenterje velja, da je posredovanje odvisnosti s pomočjo [konstruktorja |dependency-injection:passing-dependencies#Predajanje s konstruktorjem] prednostna pot. Če pa ustvarjate skupnega prednika, od katerega dedujejo drugi presenterji (npr. `BasePresenter`), in ta prednik ima tudi odvisnosti, nastane problem, ki mu pravimo [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Temu se lahko izognemo z alternativnimi potmi, ki jih predstavljajo metode in atributi (anotacije) `inject`. - - -Metode `inject*()` -================== - -Gre za obliko posredovanja odvisnosti s [setterjem |dependency-injection:passing-dependencies#Predajanje s setterjem]. Ime teh setterjev se začne s predpono `inject`. Nette DI tako poimenovane metode samodejno pokliče takoj po ustvarjanju instance presenterja in jim posreduje vse zahtevane odvisnosti. Zato morajo biti deklarirane kot public. - -Metode `inject*()` lahko štejemo za nekakšno razširitev konstruktorja v več metod. Zahvaljujoč temu lahko `BasePresenter` prevzame odvisnosti prek druge metode in pusti konstruktor prost za svoje potomce: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Presenter lahko vsebuje poljubno število metod `inject*()` in vsaka lahko ima poljubno število parametrov. Odlično se obnesejo tudi v primerih, ko je presenter [sestavljen iz lastnosti (trait) |presenter-traits] in vsaka od njih zahteva svojo odvisnost. - - -Atributi `Inject` -================= - -Gre za obliko [injiciranja v lastnost |dependency-injection:passing-dependencies#Nastavitev spremenljivke]. Dovolj je označiti, v katere spremenljivke naj se injicira, in Nette DI samodejno posreduje odvisnosti takoj po ustvarjanju instance presenterja. Da jih lahko vstavi, jih je treba deklarirati kot public. - -Lastnosti označimo z atributom: (prej se je uporabljala anotacija `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // ta vrstica je pomembna - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Prednost tega načina posredovanja odvisnosti je bila zelo varčna oblika zapisa. Vendar pa se z uvedbo [constructor property promotion |https://blog.nette.org/sl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] zdi lažje uporabiti konstruktor. - -Nasprotno pa ta način trpi za enakimi pomanjkljivostmi kot posredovanje odvisnosti v lastnosti (properties) na splošno: nimamo nadzora nad spremembami v spremenljivki in hkrati spremenljivka postane del javnega vmesnika razreda, kar je nezaželeno. diff --git a/best-practices/sl/lets-create-contact-form.texy b/best-practices/sl/lets-create-contact-form.texy deleted file mode 100644 index ed3d4baf0b..0000000000 --- a/best-practices/sl/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Ustvarjamo kontaktni obrazec -**************************** - -.[perex] -Pogledali si bomo, kako v Nette ustvariti kontaktni obrazec, vključno s pošiljanjem na e-pošto. Pa začnimo! - -Najprej moramo ustvariti nov projekt. Kako to storiti, pojasnjuje stran [Začenjamo |nette:installation]. Nato pa lahko že začnemo z ustvarjanjem obrazca. - -Najenostavneje je ustvariti [obrazec neposredno v presenterju |forms:in-presenter]. Lahko uporabimo vnaprej pripravljen `HomePresenter`. Vanjo dodamo komponento `contactForm`, ki predstavlja obrazec. To storimo tako, da v kodo zapišemo tovarniško metodo `createComponentContactForm()`, ki bo komponento izdelala: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Ime:') - ->setRequired('Vnesite ime'); - $form->addEmail('email', 'E-pošta:') - ->setRequired('Vnesite e-pošto'); - $form->addTextarea('message', 'Sporočilo:') - ->setRequired('Vnesite sporočilo'); - $form->addSubmit('send', 'Pošlji'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, stdClass $data): void - { - // pošiljanje e-pošte - } -} -``` - -Kot vidite, smo ustvarili dve metodi. Prva metoda `createComponentContactForm()` ustvari nov obrazec. Ta ima polja za ime, e-pošto in sporočilo, ki jih dodajamo z metodami `addText()`, `addEmail()` in `addTextArea()`. Dodali smo tudi gumb za pošiljanje obrazca. Kaj pa, če uporabnik ne izpolni katerega od polj? V takem primeru bi mu morali sporočiti, da je to obvezno polje. To smo dosegli z metodo `setRequired()`. Na koncu smo dodali tudi [dogodek |nette:glossary#Dogodki eventi] `onSuccess`, ki se sproži, če je obrazec uspešno poslan. V našem primeru pokliče metodo `contactFormSucceeded`, ki poskrbi za obdelavo poslanega obrazca. To bomo v kodo dodali čez trenutek. - -Komponento `contactForm` bomo pustili izrisati v predlogi `Home/default.latte`: - -```latte -{block content} -<h1>Kontaktni obrazec</h1> -{control contactForm} -``` - -Za samo pošiljanje e-pošte bomo ustvarili nov razred, ki ga bomo poimenovali `ContactFacade` in ga postavili v datoteko `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // vaša e-pošta - ->setFrom($email, $name) - ->setSubject('Sporočilo iz kontaktnega obrazca') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Metoda `sendMessage()` ustvari in pošlje e-pošto. Za to uporablja t.i. mailer, ki si ga pusti posredovati kot odvisnost prek konstruktorja. Preberite več o [pošiljanju e-pošte |mail:]. - -Zdaj se vrnemo nazaj k presenterju in dokončamo metodo `contactFormSucceeded()`. Ta pokliče metodo `sendMessage()` razreda `ContactFacade` in ji posreduje podatke iz obrazca. In kako pridobimo objekt `ContactFacade`? Pustimo si ga posredovati s konstruktorjem: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Sporočilo je bilo poslano'); - $this->redirect('this'); - } -} -``` - -Ko je e-pošta poslana, uporabniku prikažemo še t.i. [flash sporočilo |application:components#Flash sporočila], ki potrjuje, da je bilo sporočilo poslano, nato pa preusmerimo na naslednjo stran, da obrazca ni mogoče ponovno poslati s pomočjo *refresh* v brskalniku. - - -Tako, in če vse deluje, bi morali biti sposobni poslati e-pošto iz vašega kontaktnega obrazca. Čestitam! - - -HTML predloga e-pošte ---------------------- - -Zaenkrat se pošilja navadno besedilno e-sporočilo, ki vsebuje samo sporočilo, poslano z obrazcem. V e-pošti pa lahko uporabimo HTML in naredimo njen videz privlačnejši. Zanjo bomo ustvarili predlogo v Latte, ki jo bomo zapisali v `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Sporočilo iz kontaktnega obrazca - - -

    Ime: {$name}

    -

    E-pošta: {$email}

    -

    Sporočilo: {$message}

    - - -``` - -Ostane še prilagoditi `ContactFacade`, da bo uporabljal to predlogo. V konstruktorju bomo zahtevali razred `LatteFactory`, ki zna izdelati objekt `Latte\Engine`, torej [izrisovalnik Latte predlog |latte:develop#Kako izrisati predlogo]. S pomočjo metode `renderToString()` bomo predlogo izrisali v datoteko, prvi parameter je pot do predloge, drugi pa so spremenljivke. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // vaša e-pošta - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Generirano HTML e-pošto nato posredujemo metodi `setHtmlBody()` namesto prvotni `setBody()`. Prav tako nam ni treba navajati zadeve e-pošte v `setSubject()`, ker si jo bo knjižnica vzela iz elementa `` predloge. - - -Konfiguracija -------------- - -V kodi razreda `ContactFacade` je še vedno trdo kodiran naš administratorski e-naslov `admin@example.com`. Bolje bi bilo, da ga premaknemo v konfiguracijsko datoteko. Kako to storiti? - -Najprej prilagodimo razred `ContactFacade` in niz z e-pošto nadomestimo s spremenljivko, posredovano s konstruktorjem: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Drugi korak pa je navedba vrednosti te spremenljivke v konfiguraciji. V datoteko `app/config/services.neon` zapišemo: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -In to je to. Če bi bilo elementov v odseku `services` veliko in bi imeli občutek, da se e-pošta med njimi izgublja, jo lahko naredimo za spremenljivko. Prilagodimo zapis na: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -In v datoteki `app/config/common.neon` definiramo to spremenljivko: - -```neon -parameters: - adminEmail: admin@example.com -``` - -In končano! diff --git a/best-practices/sl/microsites.texy b/best-practices/sl/microsites.texy deleted file mode 100644 index fd4a5be3f2..0000000000 --- a/best-practices/sl/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Kako pisati mikro-spletna mesta -******************************* - -Predstavljajte si, da morate hitro ustvariti majhno spletno mesto za prihajajoči dogodek vašega podjetja. Mora biti preprosto, hitro in brez nepotrebnih zapletov. Morda mislite, da za tako majhen projekt ne potrebujete robustnega ogrodja. Kaj pa, če lahko uporaba ogrodja Nette ta proces bistveno poenostavi in pospeši? - -Saj se tudi pri ustvarjanju preprostih spletnih mest nočete odreči udobju. Nočete izumljati tistega, kar je bilo že enkrat rešeno. Bodite mirno leni in se pustite razvajati. Nette Framework lahko odlično uporabite tudi kot mikro ogrodje. - -Kako lahko izgleda takšno mikro-spletno mesto? Na primer tako, da celotno kodo spletnega mesta postavimo v eno samo datoteko `index.php` v javni mapi: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// ustvari DI vsebnik na podlagi konfiguracije v config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// nastavimo usmerjanje (routing) -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// pot za URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // zaznamo jezik brskalnika in preusmerimo na URL /en ali /de itd. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// pot za URL https://example.com/cs ali https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // prikažemo ustrezno predlogo, na primer ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// zaženi aplikacijo! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Vse ostalo bodo predloge, shranjene v nadrejeni mapi `/templates`. - -PHP koda v `index.php` najprej [pripravi okolje |bootstrap:], nato definira [poti (route) |application:routing#Dinamično usmerjanje s povratnimi klici] in na koncu zažene aplikacijo. Prednost je, da je lahko drugi parameter funkcije `addRoute()` callable, ki se izvede po odprtju ustrezne strani. - - -Zakaj uporabljati Nette za mikro-spletna mesta? ------------------------------------------------ - -- Programerji, ki so kdaj preizkusili [Tracy|tracy:], si danes ne predstavljajo, da bi kaj programirali brez nje. -- Predvsem pa boste izkoristili sistem predlog [Latte|latte:], saj boste že od 2 strani želeli imeti ločeno [postavitev in vsebino|latte:template-inheritance]. -- In zagotovo se želite zanesti na [samodejno ubežanje znakov |latte:safety-first], da ne nastane ranljivost XSS -- Nette bo tudi zagotovil, da se ob napaki nikoli ne prikažejo programerska sporočila o napakah PHP, temveč uporabniku razumljiva stran. -- Če želite pridobivati povratne informacije od uporabnikov, na primer v obliki kontaktnega obrazca, boste dodali še [obrazce|forms:] in [podatkovno bazo|database:]. -- Izpolnjene obrazce si lahko prav tako enostavno [pošiljate po e-pošti|mail:]. -- Včasih vam lahko koristi [predpomnjenje|caching:], na primer če prenašate in prikazujete vire (feeds). - -V današnjem času, ko sta hitrost in učinkovitost ključnega pomena, je pomembno imeti orodja, ki vam omogočajo doseganje rezultatov brez nepotrebnega odlašanja. Ogrodje Nette vam ponuja prav to - hiter razvoj, varnost in široko paleto orodij, kot sta Tracy in Latte, ki poenostavljajo proces. Dovolj je namestiti nekaj Nette paketov in zgraditi takšno mikro-spletno mesto je naenkrat povsem enostavno. In veste, da se nikjer ne skriva nobena varnostna luknja. diff --git a/best-practices/sl/pagination.texy b/best-practices/sl/pagination.texy deleted file mode 100644 index f543008252..0000000000 --- a/best-practices/sl/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Stranskanje rezultatov podatkovne baze -************************************** - -.[perex] -Pri ustvarjanju spletnih aplikacij se zelo pogosto srečate z zahtevo po omejitvi števila izpisanih postavk na strani. - -Izhajali bomo iz stanja, ko izpisujemo vse podatke brez stranskanja. Za izbiro podatkov iz podatkovne baze imamo razred ArticleRepository, ki poleg konstruktorja vsebuje metodo `findPublishedArticles`, ki vrača vse objavljene članke, razvrščene padajoče po datumu objave. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -V presenterju si nato injiciramo modelni razred in v render metodi zahtevamo objavljene članke, ki jih posredujemo v predlogo: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -V predlogi `default.latte` se nato poskrbimo za izpis člankov: - -```latte -{block content} -<h1>Članki</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Na ta način znamo izpisati vse članke, kar pa začne povzročati težave v trenutku, ko število člankov naraste. V tem trenutku pride prav implementacija mehanizma za stranskanje. - -Ta zagotovi, da se vsi članki razdelijo na več strani in mi prikažemo samo članke ene trenutne strani. Skupno število strani in razdelitev člankov si izračuna [utils:Paginator] sam glede na to, koliko člankov skupaj imamo in koliko člankov na stran želimo prikazati. - -V prvem koraku si prilagodimo metodo za pridobivanje člankov v razredu repozitorija tako, da nam zna vračati samo članke za eno stran. Dodamo tudi metodo za ugotavljanje skupnega števila člankov v podatkovni bazi, ki jo bomo potrebovali za nastavitev Paginatorja: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Vrača skupno število objavljenih člankov - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Nato se lotimo prilagoditev presenterja. V render metodo bomo posredovali številko trenutno prikazane strani. Za primer, ko ta številka ne bo del URL-ja, nastavimo privzeto vrednost prve strani. - -Nadalje render metodo razširimo še s pridobivanjem instance Paginatorja, njegovo nastavitvijo in izbiro pravilnih člankov za prikaz v predlogi. HomePresenter bo po prilagoditvah izgledal takole: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Ugotovimo skupno število objavljenih člankov - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Izdelamo instanco Paginatorja in jo nastavimo - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // skupno število člankov - $paginator->setItemsPerPage(10); // število postavk na stran - $paginator->setPage($page); // številka trenutne strani - - // Iz podatkovne baze izvlečemo omejeno množico člankov glede na izračun Paginatorja - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // ki jo posredujemo v predlogo - $this->template->articles = $articles; - // in tudi sam Paginator za prikaz možnosti stranskanja - $this->template->paginator = $paginator; - } -} -``` - -Predloga nam že zdaj iterira samo nad članki ene strani, dodati moramo le še povezave za stranskanje: - -```latte -{block content} -<h1>Članki</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Prva</a> -  |  - <a n:href="default, $paginator->page-1">Prejšnja</a> -  |  - {/if} - - Stran {$paginator->getPage()} od {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Naslednja</a> -  |  - <a n:href="default, $paginator->getPageCount()">Zadnja</a> - {/if} -</div> -``` - - -Tako smo stran dopolnili z možnostjo stranskanja s pomočjo Paginatorja. V primeru, ko namesto [Nette Database Core |database:sql-way] kot podatkovno plast uporabimo [Nette Database Explorer |database:explorer], smo sposobni implementirati stranskanje tudi brez uporabe Paginatorja. Razred `Nette\Database\Table\Selection` namreč vsebuje metodo [page |api:Nette\Database\Table\Selection::page] z logiko stranskanja, prevzeto iz Paginatorja. - -Repozitorij bo pri tem načinu implementacije izgledal takole: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -V presenterju nam ni treba ustvarjati Paginatorja, namesto njega uporabimo metodo razreda `Selection`, ki nam jo vrača repozitorij: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Izvlečemo objavljene članke - $articles = $this->articleRepository->findPublishedArticles(); - - // in v predlogo pošljemo samo njihov del, omejen glede na izračun metode page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // in tudi potrebne podatke za prikaz možnosti stranskanja - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Ker v predlogo zdaj ne pošiljamo Paginatorja, prilagodimo del, ki prikazuje povezave za stranskanje: - -```latte -{block content} -<h1>Članki</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Prva</a> -  |  - <a n:href="default, $page - 1">Prejšnja</a> -  |  - {/if} - - Stran {$page} od {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Naslednja</a> -  |  - <a n:href="default, $lastPage">Zadnja</a> - {/if} -</div> -``` - -Na ta način smo implementirali mehanizem za stranskanje brez uporabe Paginatorja. - -{{priority: -1}} diff --git a/best-practices/sl/passing-settings-to-presenters.texy b/best-practices/sl/passing-settings-to-presenters.texy deleted file mode 100644 index 8ced4b88f2..0000000000 --- a/best-practices/sl/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Posredovanje nastavitev v presenterje -************************************* - -.[perex] -Ali morate v presenterje posredovati argumente, ki niso objekti (npr. informacijo, ali teče v načinu za odpravljanje napak, poti do map itd.), in jih torej ni mogoče samodejno posredovati s pomočjo autowiringa? Rešitev je, da jih zapakirate v objekt `Settings`. - -Storitev `Settings` predstavlja zelo enostaven in hkrati uporaben način za zagotavljanje informacij o tekoči aplikaciji presenterjem. Njena konkretna oblika je odvisna izključno od vaših specifičnih potreb. Primer: - -```php -namespace App; - -class Settings -{ - public function __construct( - // od PHP 8.1 je mogoče navesti readonly - public bool $debugMode, - public string $appDir, - // in tako naprej - ) {} -} -``` - -Primer registracije v konfiguraciji: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Ko bo presenter potreboval informacije, ki jih zagotavlja ta storitev, jo bo preprosto zahteval v konstruktorju: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/sl/post-links.texy b/best-practices/sl/post-links.texy deleted file mode 100644 index 0b99158d78..0000000000 --- a/best-practices/sl/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Kako pravilno uporabljati POST povezave -*************************************** - -.[perex] -V spletnih aplikacijah, zlasti v administrativnih vmesnikih, bi moralo biti osnovno pravilo, da se akcije, ki spreminjajo stanje strežnika, ne izvajajo prek metode HTTP GET. Kot pove že ime metode, bi moral GET služiti samo za pridobivanje podatkov, ne pa za njihovo spreminjanje. Za akcije, kot je na primer brisanje zapisov, je primernejša uporaba metode POST. Čeprav bi bila idealna metoda DELETE, je te brez JavaScripta ni mogoče izvesti, zato se zgodovinsko uporablja POST. - -Kako to storiti v praksi? Uporabite ta preprost trik. Na začetku predloge si ustvarite pomožni obrazec z identifikatorjem `postForm`, ki ga nato uporabite za gumbe za brisanje: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Zahvaljujoč temu obrazcu lahko namesto klasične povezave `<a>` uporabite gumb `<button>`, ki ga lahko vizualno prilagodite tako, da izgleda kot običajna povezava. Na primer, CSS ogrodje Bootstrap ponuja razreda `btn btn-link`, s katerima dosežete, da gumb ne bo vizualno drugačen od ostalih povezav. Z atributom `form="postForm"` ga povežemo z vnaprej pripravljenim obrazcem: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">izbriši</button> - <!-- namesto <a n:href="delete $post->id">izbriši</a> --> - </td> - </tr> -</table> -``` - -Ob kliku na povezavo se zdaj izvede akcija `delete`. Za zagotovitev, da bodo zahteve sprejete samo prek metode POST in iz iste domene (kar je učinkovita obramba pred napadi CSRF), uporabite atribut `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hipotetična koda, ki briše zapis - $this->redirect('default'); - } -} -``` - -Atribut obstaja od Nette Application 3.2 in več o njegovih možnostih boste izvedeli na strani [Kako uporabljati atribut #Requires |attribute-requires]. - -Če bi namesto akcije `actionDelete()` uporabljali signal `handleDelete()`, ni treba navajati `sameOrigin: true`, ker imajo signali to zaščito nastavljeno implicitno: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Ta pristop ne samo izboljšuje varnost vaše aplikacije, ampak tudi prispeva k spoštovanju pravilnih spletnih standardov in praks. Z uporabo metod POST za akcije, ki spreminjajo stanje, dosežete bolj robustno in varnejšo aplikacijo. diff --git a/best-practices/sl/presenter-traits.texy b/best-practices/sl/presenter-traits.texy deleted file mode 100644 index 894c0add0e..0000000000 --- a/best-practices/sl/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Sestavljanje presenterjev iz lastnosti (trait) -********************************************** - -.[perex] -Če moramo v več presenterjih implementirati isto kodo (npr. preverjanje, ali je uporabnik prijavljen), se ponuja možnost, da kodo postavimo v skupnega prednika. Druga možnost je ustvarjanje namensko usmerjenih [lastnosti (trait) |nette:introduction-to-object-oriented-programming#Lastnosti Traits]. - -Prednost te rešitve je, da lahko vsak od presenterjev uporabi točno tiste lastnosti (traits), ki jih dejansko potrebuje, medtem ko večkratno dedovanje v PHP ni mogoče. - -Te lastnosti (traits) lahko izkoristijo dejstvo, da se ob ustvarjanju presenterja postopoma pokličejo vse [inject metode |inject-method-attribute#Metode inject]. Paziti je treba le, da je ime vsake inject metode edinstveno. - -Lastnosti (traits) lahko pripnejo inicializacijsko kodo na dogodke [onStartup ali onRender |application:presenters#Dogodki]. - -Primeri: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Presenter nato te lastnosti (traits) preprosto uporabi: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/sl/restore-request.texy b/best-practices/sl/restore-request.texy deleted file mode 100644 index 6f81a2d371..0000000000 --- a/best-practices/sl/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Kako se vrniti na prejšnjo stran? -********************************* - -.[perex] -Kaj če uporabnik izpolnjuje obrazec in mu poteče prijava? Da ne bi izgubil podatkov, pred preusmeritvijo na prijavno stran podatke shranimo v sejo (session). V Nette je to povsem enostavno. - -Trenutno zahtevo lahko shranite v sejo s pomočjo metode `storeRequest()`, ki vrne njen identifikator v obliki kratkega niza. Metoda shrani ime trenutnega presenterja, pogled (view) in njegove parametre. V primeru, da je bil poslan tudi obrazec, se shrani tudi vsebina polj (z izjemo naloženih datotek). - -Obnovitev zahteve izvede metoda `restoreRequest($key)`, ki ji posredujemo pridobljeni identifikator. Ta preusmeri na prvotni presenter in pogled. Če pa shranjena zahteva vsebuje pošiljanje obrazca, na prvotni presenter preide z metodo `forward()`, obrazcu posreduje prej izpolnjene vrednosti in ga pusti ponovno izrisati. Uporabnik ima tako možnost obrazec ponovno poslati in nobeni podatki se ne izgubijo. - -Pomembno je, da `restoreRequest()` preveri, ali je novo prijavljeni uporabnik isti, kot tisti, ki je obrazec prvotno izpolnjeval. Če ne, zahtevo zavrže in ne naredi ničesar. - -Poglejmo si vse na primeru. Imejmo presenter `AdminPresenter`, v katerem se urejajo podatki in v njegovi metodi `startup()` preverjamo, ali je uporabnik prijavljen. Če ni, ga preusmerimo na `SignPresenter`. Hkrati shranimo trenutno zahtevo in njen ključ pošljemo v `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Presenter `SignPresenter` bo poleg obrazca za prijavo vseboval tudi persistentni parameter `$backlink`, v katerega se zapiše ključ. Ker je parameter persistenten, se bo prenašal tudi po pošiljanju prijavnega obrazca. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... dodamo polja obrazca ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... tukaj uporabnika prijavimo ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Metodi `restoreRequest()` posredujemo ključ shranjene zahteve in ta preusmeri (ali preide) na prvotni presenter. - -Če pa je ključ neveljaven (na primer že ne obstaja v seji), metoda ne naredi ničesar. Sledi torej klic `$this->redirect('Admin:')`, ki preusmeri na `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/tr/@home.texy b/best-practices/tr/@home.texy index c8b90b9e40..8c1c1f139d 100644 --- a/best-practices/tr/@home.texy +++ b/best-practices/tr/@home.texy @@ -1,24 +1,25 @@ -Kılavuzlar ve yöntemler -*********************** +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*. +Nette için kılavuzlar, sık karşılaşılan görevlerin çözümleri ve en iyi uygulamalar. <div class=documentation> <div> -Nette Uygulaması ----------------- -- [Inject metotları ve nitelikleri |inject-method-attribute] -- [Trait'lerden presenter oluşturma |presenter-traits] -- [Ayarları presenter'lara geçirme |passing-settings-to-presenters] -- [Önceki sayfaya nasıl dönülür |restore-request] +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 niteliği nasıl kullanılır |attribute-requires] -- [POST bağlantıları nasıl doğru kullanılır |post-links] +- [#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> @@ -26,10 +27,10 @@ Nette Uygulaması Formlar ------- -- [Formların yeniden kullanımı |form-reuse] +- [Formları yeniden kullanma |form-reuse] - [Kayıt oluşturma ve düzenleme formu |creating-editing-form] -- [İletişim formu oluşturuyoruz |lets-create-contact-form] -- [Bağımlı seçme kutuları |https://blog.nette.org/tr/dependent-selectboxes-elegantly-in-nette-and-pure-js] +- [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> @@ -38,11 +39,10 @@ Formlar Genel ----- - [Yapılandırma dosyası nasıl yüklenir |bootstrap:] -- [Mikro web siteleri nasıl yazılır |microsites] -- [Nette neden PascalCase sabit gösterimini kullanıyor? |https://blog.nette.org/tr/for-less-screaming-in-the-code] +- [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] -- [Düzenleyiciler ve araçlar için ipuçları |editors-and-tools] - [Nesne yönelimli programlamaya giriş |nette:introduction-to-object-oriented-programming] </div> @@ -54,7 +54,7 @@ Genel - [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] +- [Doctrine ORM web sitesi |https://github.com/MinecordNetwork/Website] - [Hızlı başlangıç |quickstart:] </div> @@ -63,7 +63,7 @@ Genel Videolar -------- -Poslední soboty'den yüzlerce kayıt ve Nette hakkındaki videoları tek bir çatı altında "Nette Framework Youtube kanalında":https://www.youtube.com/user/NetteFramework bulabilirsiniz. +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 new file mode 100644 index 0000000000..2cbf528cde --- /dev/null +++ b/best-practices/tr/@left-menu.texy @@ -0,0 +1,34 @@ +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 index 7f915d9a01..e2ab9e0d40 100644 --- a/best-practices/tr/@meta.texy +++ b/best-practices/tr/@meta.texy @@ -1,2 +1 @@ -{{sitename: Kılavuzlar ve yöntemler}} -{{leftbar: www:@menu-common}} +{{sitename: Kılavuzlar ve En İyi Uygulamalar}} diff --git a/best-practices/tr/attribute-requires.texy b/best-practices/tr/attribute-requires.texy index 0fe1f082f5..26f6caed3e 100644 --- a/best-practices/tr/attribute-requires.texy +++ b/best-practices/tr/attribute-requires.texy @@ -1,31 +1,31 @@ -`#[Requires]` Niteliği Nasıl Kullanılır -*************************************** +`#[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 sık karşılaşırsınız. Belki bazı isteklerin yalnızca bir form kullanarak (yani POST metoduyla) veri gönderebilmesini veya yalnızca AJAX çağrıları için erişilebilir olmasını istersiniz. Nette Framework 3.2'de, bu tür kısıtlamaları çok zarif ve anlaşılır bir şekilde ayarlamanıza olanak tanıyan yeni bir araç ortaya çıktı: `#[Requires]` niteliği. +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. -Nitelik, bir sınıf veya metot tanımının önüne eklediğiniz PHP'deki özel bir işarettir. Aslında bir sınıf olduğu için, aşağıdaki örneklerin çalışması için use ifadesini belirtmek gerekir: +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]` niteliğini presenter sınıfının kendisinde ve ayrıca şu metotlarda kullanabilirsiniz: +`#[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şenlerle de ilgilidir, yani niteliği onlarda da kullanabilirsiniz. +Son iki metot bileşenler için de geçerlidir, dolayısıyla attribute'u onlarla da kullanabilirsiniz. -Niteliğin belirttiği koşullar karşılanmazsa, bir HTTP 4xx hatası tetiklenir. +Attribute'un belirttiği koşullar sağlanmazsa bir HTTP 4xx hatası tetiklenir. HTTP Metotları -------------- -Erişim için hangi HTTP metotlarının (GET, POST vb. gibi) izinli olduğunu belirleyebilirsiniz. Örneğin, yalnızca bir form göndererek erişime izin vermek istiyorsanız, şunu ayarlarsınız: +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 @@ -37,15 +37,15 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Durumu değiştiren eylemler için neden GET yerine POST kullanmalısınız ve bunu nasıl yapmalısınız? [Kılavuzu okuyun |post-links]. +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 veya metot dizisi belirtebilirsiniz. Özel bir durum, tüm metotlara izin veren `'*'` değeridir, ki bu presenter'ların standart olarak [güvenlik nedenleriyle izin vermediği |application:presenters#HTTP Metodu Kontrolü] bir durumdur. +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 veya metodun yalnızca AJAX istekleri için kullanılabilir olmasını istiyorsanız, şunu kullanın: +Bir presenter'ın ya da metodun yalnızca AJAX istekleriyle erişilebilir olmasını istiyorsanız şunu kullanın: ```php #[Requires(ajax: true)] @@ -58,7 +58,7 @@ 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 güvenlik açığını |nette:vulnerability-protection#Cross-Site Request Forgery CSRF] önler: +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)] @@ -67,7 +67,7 @@ class SecurePresenter extends Nette\Application\UI\Presenter } ``` -`handle<Signal>()` metotlarında, aynı alan adından erişim otomatik olarak zorunlu kılınır. Dolayısıyla tam tersine, herhangi bir alan adından erişime izin vermek istiyorsanız, şunu belirtin: +`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)] @@ -80,7 +80,7 @@ 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()` veya `switch()` metodunu kullanarak kullanılabilir olacak şekilde kısıtlamak yararlıdır. Örneğin error-presenter'lar bu şekilde korunur, böylece URL'den çağrılamazlar: +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)] @@ -89,7 +89,7 @@ class ForwardedPresenter extends Nette\Application\UI\Presenter } ``` -Pratikte, presenter'daki mantığa dayalı olarak erişilebilen belirli view'leri işaretlemek genellikle gereklidir. Yani yine, doğrudan açılamamaları için: +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 @@ -114,7 +114,7 @@ class ProductPresenter extends Nette\Application\UI\Presenter Belirli Eylemler ---------------- -Ayrıca, belirli bir kodun, örneğin bir bileşenin oluşturulmasının, yalnızca presenter'daki belirli eylemler için kullanılabilir olmasını da kısıtlayabilirsiniz: +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 @@ -126,15 +126,15 @@ class EditDeletePresenter extends Nette\Application\UI\Presenter } ``` -Tek bir eylem durumunda, bir dizi yazmaya gerek yoktur: `#[Requires(actions: 'default')]` +Tek bir eylem söz konusuysa dizi yazmaya gerek yoktur: `#[Requires(actions: 'default')]` -Özel Nitelikler ---------------- +Özel Attribute'lar +------------------ -`#[Requires]` niteliğini aynı ayarlarla tekrar tekrar kullanmak istiyorsanız, `#[Requires]`'ı miras alan ve onu ihtiyaçlara göre ayarlayan kendi niteliğinizi oluşturabilirsiniz. +`#[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]` yalnızca `default` eylemi aracılığıyla erişime izin verir: +Örneğin `#[SingleAction]`, erişime yalnızca `default` eylemi üzerinden izin verir: ```php #[\Attribute] @@ -152,7 +152,7 @@ class SingleActionPresenter extends Nette\Application\UI\Presenter } ``` -Veya `#[RestMethods]`, REST API için kullanılan tüm HTTP metotları aracılığıyla erişime izin verir: +Ya da `#[RestMethods]`, REST API'de kullanılan tüm HTTP metotlarıyla erişime izin verir: ```php #[\Attribute] @@ -174,4 +174,4 @@ class ApiPresenter extends Nette\Application\UI\Presenter Sonuç ----- -`#[Requires]` niteliği, web sayfalarınızın nasıl erişilebilir olduğu konusunda size büyük esneklik ve kontrol sağlar. Basit ama güçlü kurallar kullanarak uygulamanızın güvenliğini ve doğru çalışmasını artırabilirsiniz. Gördüğünüz gibi, Nette'de nitelikleri kullanmak işinizi yalnızca kolaylaştırmakla kalmaz, aynı zamanda güvence altına da alabilir. +`#[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 index c0eb5fb788..798a79ef77 100644 --- a/best-practices/tr/composer.texy +++ b/best-practices/tr/composer.texy @@ -1,12 +1,12 @@ -Composer: Kullanım İpuçları -*************************** +Composer Kullanım İpuçları +************************** <div class=perex> -Composer, PHP'de bağımlılıkları yönetmek için bir araçtır. Projemizin bağlı olduğu kütüphaneleri listelememize olanak tanır ve bunları bizim için kurar ve günceller. Şunları göstereceğiz: +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 veya mevcut bir projede kullanımı +- yeni ya da var olan bir projede nasıl kullanılır </div> @@ -14,31 +14,31 @@ Composer, PHP'de bağımlılıkları yönetmek için bir araçtır. Projemizin b Kurulum ======= -Composer, aşağıdaki şekilde indirip kuracağınız çalıştırılabilir bir `.phar` dosyasıdır: +Composer, indirip aşağıdaki gibi kuracağınız çalıştırılabilir bir `.phar` dosyasıdır. Windows ------- -Resmi [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe] yükleyicisini kullanın. +Resmi kurulum programını kullanın: [Composer-Setup.exe|https://getcomposer.org/Composer-Setup.exe]. Linux, macOS ------------ -[Bu sayfadan |https://getcomposer.org/download/] kopyalayacağınız sadece 4 komut yeterlidir. +Tek gereken, [bu sayfadan |https://getcomposer.org/download/] kopyalayabileceğiniz 4 komut. -Ayrıca, sistem `PATH`'inde bulunan bir klasöre yerleştirerek, Composer genel olarak erişilebilir hale gelir: +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 # veya /usr/local/bin/composer +$ mv ./composer.phar ~/bin/composer # ya da /usr/local/bin/composer ``` Projede Kullanım ================ -Projemizde Composer kullanmaya başlamak için yalnızca `composer.json` dosyasına ihtiyacımız var. Bu dosya projemizin bağımlılıklarını tanımlar ve ayrıca ek meta veriler içerebilir. Temel bir `composer.json` dosyası şöyle görünebilir: +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 { @@ -48,17 +48,17 @@ Projemizde Composer kullanmaya başlamak için yalnızca `composer.json` dosyas } ``` -Burada uygulamamızın (veya kütüphanemizin) `nette/database` paketini (paket adı kuruluş adı ve proje adından oluşur) gerektirdiğini ve `^3.0` koşuluna uyan sürümü (yani en son 3 sürümünü) istediğini söylüyoruz. +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. -Yani projenin kökünde `composer.json` dosyamız var ve kurulumu başlatıyoruz: +`composer.json` dosyası proje kökündeyken şunu çalıştırın: ```shell composer update ``` -Composer, Nette Database'i `vendor/` klasörüne indirecektir. Ayrıca, tam olarak hangi kütüphane sürümlerini kurduğu hakkında bilgi içeren `composer.lock` dosyasını oluşturacaktır. +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, `vendor/autoload.php` dosyasını oluşturur, bunu basitçe dahil edebilir ve başka herhangi bir iş yapmadan kütüphaneleri kullanmaya başlayabiliriz: +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'; @@ -70,17 +70,17 @@ $db = new Nette\Database\Connection('sqlite::memory:'); Paketleri En Son Sürümlere Güncelleme ===================================== -Kullanılan kütüphaneleri `composer.json`'da tanımlanan koşullara göre en son sürümlere güncellemek `composer update` komutunun sorumluluğundadır. Örneğin, `"nette/database": "^3.0"` bağımlılığı için en son 3.x.x sürümünü kurar, ancak 4 sürümünü kurmaz. +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. -En son sürümü kurabilmek için `composer.json` dosyasındaki koşulları örneğin `"nette/database": "^4.1"` olarak güncellemek için `composer require nette/database` komutunu kullanın. +`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 listelemek gerekirdi, örn.: +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ğildir. Bu yüzden bunu sizin için yapacak basit "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff betiğini kullanın: +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 @@ -90,29 +90,29 @@ php composer-frontline.php Yeni Proje Oluşturma ==================== -Nette üzerinde yeni bir proje tek bir komutla oluşturulur: +Tek bir komutla yeni bir Nette projesi oluşturabilirsiniz: ```shell -composer create-project nette/web-project proje-adi +composer create-project nette/web-project projenin-adi ``` -`proje-adi` olarak projeniz için dizin adını girin ve onaylayın. Composer, zaten `composer.json` dosyasını içeren `nette/web-project` deposunu GitHub'dan indirecek ve hemen ardından Nette Framework'ü indirecektir. Artık yalnızca `temp/` ve `log/` klasörlerine yazma [izinlerini ayarlamak |nette:troubleshooting#Dizin İzinlerini Ayarlama] yeterli olmalı ve proje canlanmalıdır. +`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. -Projenin hangi PHP sürümünde barındırılacağını biliyorsanız, [onu ayarlamayı |#PHP Sürümü] unutmayın. +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 kullandığınız PHP sürümüyle uyumlu paket sürümlerini kurar (daha doğrusu Composer'ı çalıştırırken komut satırında kullanılan PHP sürümüyle). Ancak bu muhtemelen barındırma hizmetinizin kullandığı sürümle aynı değildir. Bu nedenle, barındırmadaki PHP sürümü hakkındaki bilgiyi `composer.json` dosyasına eklemek çok önemlidir. Ardından yalnızca barındırma ile uyumlu paket sürümleri kurulacaktır. +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. -Projenin örneğin PHP 8.2.3 üzerinde çalışacağını şu komutla ayarlarız: +Ö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 şu şekilde yazılır: +Sürüm `composer.json` dosyasına şöyle yazılır: ```js { @@ -124,13 +124,13 @@ Sürüm `composer.json` dosyasına şu şekilde yazılır: } ``` -Ancak, PHP sürüm numarası dosyanın başka bir yerinde, `require` bölümünde de belirtilir. İlk sayı, paketlerin hangi sürüm için kurulacağını belirlerken, ikinci sayı uygulamanın kendisinin hangi sürüm için yazıldığını söyler. Ve örneğin PhpStorm, *PHP dil seviyesini* buna göre ayarlar. (Elbette bu sürümlerin farklı olmasının bir anlamı yoktur, bu yüzden çift kayıt düşüncesizliktir.) Bu sürümü şu komutla ayarlarsınız: +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 ``` -Veya doğrudan `composer.json` dosyasında: +Ya da doğrudan `composer.json` dosyasında: ```js { @@ -141,54 +141,54 @@ Veya doğrudan `composer.json` dosyasında: ``` -PHP Sürümünü Yoksayma -===================== +PHP Sürümünü Yok Sayma +====================== -Paketler genellikle hem uyumlu oldukları en düşük PHP sürümünü hem de test edildikleri en yüksek sürümü belirtirler. Henüz daha yeni bir PHP sürümü kullanmayı planlıyorsanız, örneğin test amacıyla, Composer böyle bir paketi kurmayı reddedecektir. Çözüm, Composer'ın gerekli PHP sürümünün üst sınırlarını yoksaymasına neden olan `--ignore-platform-req=php+` seçeneğidir. +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. -Yanıltıcı Bildirimler -===================== +Yanlış Bildirimler +================== -Paketleri yükseltirken veya sürüm numaralarını değiştirirken çakışmalar meydana gelebilir. Bir paket, başka bir paketle çelişen gereksinimlere sahip olabilir vb. Ancak Composer bazen yanıltıcı bildirimler yazdırır. Gerçekte var olmayan bir çakışma bildirir. Bu durumda, `composer.lock` dosyasını silmek ve tekrar denemek yardımcı olur. +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ı devam ederse, ciddiye alınmalı ve neyin nasıl ayarlanacağını anlamak için okunmalıdır. +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 - Merkezi Depo -============================ +Packagist.org - Genel Depo +========================== -[Packagist |https://packagist.org], Composer'ın aksi belirtilmedikçe paketleri aramaya çalıştığı ana depodur. Burada kendi paketlerimizi de yayınlayabiliriz. +[Packagist |https://packagist.org], Composer'ın paketleri varsayılan olarak aradığı ana depodur. Kendi paketlerinizi de burada yayımlayabilirsiniz. -Merkezi Depoyu Kullanmak İstemezsek Ne Olur? --------------------------------------------- +Ya Merkezi Depoyu İstemiyorsak +------------------------------ -Şirket içi uygulamalarımız varsa ve bunları kamuya açık olarak barındıramıyorsak, onlar için bir şirket deposu oluştururuz. +Ş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 fazla bilgi [resmi belgelerde |https://getcomposer.org/doc/05-repositories.md#repositories]. +Depolar hakkında daha fazlasını [resmi belgelerde |https://getcomposer.org/doc/05-repositories.md#repositories] okuyun. -Otomatik Yükleme (Autoloading) -============================== +Autoloading +=========== -Composer'ın temel bir özelliği, kurduğu tüm sınıflar için otomatik yükleme sağlamasıdır; bunu `vendor/autoload.php` dosyasını dahil ederek başlatırsınız. +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` klasörü dışındaki diğer sınıfları yüklemek için de kullanmak mümkündür. İlk seçenek, Composer'ın tanımlanmış klasörleri ve alt klasörleri taramasını, tüm sınıfları bulmasını ve bunları otomatik yükleyiciye dahil etmesini sağlamaktır. Bunu `composer.json`'da `autoload > classmap` ayarlayarak başarırsınız: +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/ klasörünü ve alt klasörlerini dahil eder + "src/", # src/ dizinini ve alt dizinlerini kapsar ] } } ``` -Ardından, her değişiklikte `composer dumpautoload` komutunu çalıştırmak ve otomatik yükleme tablolarını yeniden oluşturmak gerekir. Bu son derece zahmetlidir ve bu görevi aynı işlemi arka planda otomatik olarak ve çok daha hızlı gerçekleştiren [RobotLoader|robot-loader:]'a devretmek çok daha iyidir. +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/]'e uymaktır. Basitçe ifade etmek gerekirse, bu, 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, yani örn. `App\Core\RouterFactory`, `/path/to/App/Core/RouterFactory.php` dosyasında olacaktır. Yapılandırma örneği: +İ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 { @@ -200,13 +200,13 @@ Ardından, her değişiklikte `composer dumpautoload` komutunu çalıştırmak v } ``` -Davranışın tam olarak nasıl yapılandırılacağını [Composer belgelerinde|https://getcomposer.org/doc/04-schema.md#psr-4] öğrenebilirsiniz. +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 Test Etme -======================== +Yeni Sürümleri Deneme +===================== -Bir paketin yeni bir geliştirme sürümünü test etmek istiyorsunuz. Nasıl yapılır? Öncelikle `composer.json` dosyasına, paketlerin geliştirme sürümlerinin kurulmasına izin veren, ancak yalnızca gereksinimleri karşılayan kararlı sürüm kombinasyonu yoksa buna başvuran şu çift seçeneği ekleyin: +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 { @@ -215,33 +215,33 @@ Bir paketin yeni bir geliştirme sürümünü test etmek istiyorsunuz. Nasıl ya } ``` -Ayrıca `composer.lock` dosyasını silmenizi öneririz, bazen Composer anlaşılmaz bir şekilde kurulumu reddeder ve bu sorunu çözer. +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ün numarası 4.0. Şu komutla kurarsınız: +Diyelim ki paket `nette/utils` ve yeni sürüm 4.0. Şu komutla kurun: ```shell composer require nette/utils:4.0.x-dev ``` -Veya belirli bir sürümü kurabilirsiniz, örneğin 4.0.0-RC2: +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 kütüphaneye daha eski bir sürüme kilitlenmiş başka bir paket bağlıysa (örn. `^3.1`), o zaman paketi yeni sürümle çalışacak şekilde güncellemek idealdir. Ancak yalnızca kısıtlamayı aşmak ve Composer'ı geliştirme sürümünü kurmaya ve daha eski bir sürüm (örn. 3.1.6) gibi davranmaya zorlamak istiyorsanız, `as` anahtar kelimesini kullanabilirsiniz: +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" ``` -Komutları Çağırma -================= +Komut Çağırma +============= -Composer aracılığıyla, sanki yerel Composer komutlarıymış gibi kendi önceden hazırlanmış komutlarınızı ve betiklerinizi çağırabilirsiniz. `vendor/bin` klasöründe bulunan betikler için bu klasörü belirtmeye gerek yoktur. +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` dosyasında [Nette Tester|tester:] kullanarak testleri çalıştıran bir betik tanımlayalım: +Örnek olarak, `composer.json` içinde testleri çalıştırmak üzere [Nette Tester |tester:] kullanan bir betik tanımlayalım: ```js { @@ -251,13 +251,13 @@ Composer aracılığıyla, sanki yerel Composer komutlarıymış gibi kendi önc } ``` -Testleri daha sonra `composer tester` kullanarak çalıştırırız. Komutu, projenin kök klasöründe olmasak bile, bazı alt dizinlerde olsak bile çağırabiliriz. +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 ================= -Size açık kaynak yazarlarını memnun edecek bir numara göstereceğiz. Projenizin kullandığı kütüphanelere GitHub'da basit bir şekilde yıldız verebilirsiniz. Sadece `symfony/thanks` kütüphanesini kurmanız yeterlidir: +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 @@ -275,7 +275,7 @@ Deneyin! Yapılandırma ============ -Composer, [Git |https://git-scm.com] sürüm kontrol aracıyla yakından bağlantılıdır. Eğer kurulu değilse, Composer'a onu kullanmamasını söylemeniz gerekir: +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 index 0274a0e122..6c96ad2d41 100644 --- a/best-practices/tr/creating-editing-form.texy +++ b/best-practices/tr/creating-editing-form.texy @@ -2,15 +2,15 @@ Kayıt Oluşturma ve Düzenleme Formu ********************************** .[perex] -Nette'de, her ikisi için de aynı formu kullanarak bir kaydın eklenmesini ve düzenlenmesini nasıl doğru bir şekilde uygulayabiliriz? +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? -Birçok durumda, kayıt ekleme ve düzenleme formları aynıdır, belki sadece düğme üzerindeki etiket farklıdır. Formu önce kayıt eklemek, sonra düzenlemek için kullanacağımız ve son olarak her iki çözümü birleştireceğimiz basit presenter örneklerini göstereceğiz. +Ç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 hizmet eden bir presenter örneği. Veritabanıyla olan asıl işi, kodu gösterim için önemli olmayan `Facade` sınıfına bırakacağız. +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 @@ -27,15 +27,15 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $form = new Form; - // ... form alanlarını ekleyin ... + // ... form alanlarını ekle ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // veritabanına kayıt ekleme + $this->facade->add($data); // kaydı veritabanına ekle $this->flashMessage('Başarıyla eklendi'); $this->redirect('...'); } @@ -51,7 +51,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter Kayıt Düzenleme --------------- -Şimdi bir kaydı düzenlemeye hizmet eden presenter'ın nasıl görüneceğini göstereceğiz: +Şimdi kayıt düzenlemeye yarayan bir presenter'ın nasıl görüneceğine bakalım: ```php @@ -70,10 +70,10 @@ class RecordPresenter extends Nette\Application\UI\Presenter { $record = $this->facade->get($id); if ( - !$record // kaydın varlığının doğrulanması - || !$this->facade->isEditAllowed(/*...*/) // yetki kontrolü + !$record // kaydın varlığını doğrula + || !$this->facade->isEditAllowed(/*...*/) // izinleri denetle ) { - $this->error(); // hata 404 + $this->error(); // 404 hatası } $this->record = $record; @@ -81,32 +81,32 @@ class RecordPresenter extends Nette\Application\UI\Presenter protected function createComponentRecordForm(): Form { - // eylemin 'edit' olduğunu doğrulayın + // eylemin 'edit' olduğunu doğrula if ($this->getAction() !== 'edit') { $this->error(); } $form = new Form; - // ... form alanlarını ekleyin ... + // ... form alanlarını ekle ... - $form->setDefaults($this->record); // varsayılan değerlerin ayarlanması - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->setDefaults($this->record); // varsayılan değerleri ayarla + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { - $this->facade->update($this->record->id, $data); // kaydın güncellenmesi + $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 Yaşam Döngüsü] hemen başında çalışan *action* metodunda, kaydın varlığını ve kullanıcının onu düzenleme iznini doğrularız. +[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 için `recordFormSucceeded()` metodunda kullanılabilir olur. Alternatif bir çözüm, varsayılan değerleri doğrudan `actionEdit()` içinde ayarlamak ve URL'nin bir parçası olan ID değerini `getParameter('id')` kullanarak almaktır: +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 @@ -114,12 +114,12 @@ Kaydı `$record` özelliğinde saklarız, böylece varsayılan değerleri ayarla { $record = $this->facade->get($id); if ( - // varlığın doğrulanması ve yetki kontrolü + // varlığı doğrula ve izinleri denetle ) { $this->error(); } - // formun varsayılan değerlerini ayarlama + // formun varsayılan değerlerini ayarla $this->getComponent('recordForm') ->setDefaults($record); } @@ -130,16 +130,15 @@ Kaydı `$record` özelliğinde saklarız, böylece varsayılan değerleri ayarla $this->facade->update($id, $data); // ... } -} ``` -Ancak, ve bu **tüm kodun en önemli çıkarımı olmalı**, formu oluştururken eylemin gerçekten `edit` olduğundan emin olmalıyız. Aksi takdirde, `actionEdit()` metodundaki doğrulama hiç gerçekleşmezdi! +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 için Aynı Form +Ekleme ve Düzenleme İçin Aynı Form ---------------------------------- -Ve şimdi her iki presenter'ı tek bir tanede birleştireceğiz. Ya `createComponentRecordForm()` metodunda hangi eylemin söz konusu olduğunu ayırt edebilir ve formu buna göre yapılandırabiliriz ya da bunu doğrudan action metotlarına bırakıp koşuldan kurtulabiliriz: +Ş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 @@ -153,49 +152,49 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void { $record = $this->facade->get($id); if ( - !$record // kaydın varlığının doğrulanması - || !$this->facade->isEditAllowed(/*...*/) // yetki kontrolü + !$record // kaydın varlığını doğrula + || !$this->facade->isEditAllowed(/*...*/) // izinleri denetle ) { - $this->error(); // hata 404 + $this->error(); // 404 hatası } $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // varsayılan değerlerin ayarlanması - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->setDefaults($record); // varsayılan değerleri ayarla + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form { - // eylemin 'add' veya 'edit' olduğunu doğrulayın + // eylemin 'add' ya da 'edit' olduğunu doğrula if (!in_array($this->getAction(), ['add', 'edit'])) { $this->error(); } $form = new Form; - // ... form alanlarını ekleyin ... + // ... form alanlarını ekle ... return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { - $this->facade->add($data); // veritabanına kayıt ekleme + $this->facade->add($data); // kaydı veritabanına ekle $this->flashMessage('Başarıyla eklendi'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // kaydın güncellenmesi + $this->facade->update($id, $data); // kaydı güncelle $this->flashMessage('Başarıyla güncellendi'); $this->redirect('...'); } diff --git a/best-practices/tr/dynamic-snippets.texy b/best-practices/tr/dynamic-snippets.texy index c805aeedb5..77edc5fcf3 100644 --- a/best-practices/tr/dynamic-snippets.texy +++ b/best-practices/tr/dynamic-snippets.texy @@ -1,7 +1,10 @@ Dinamik Snippet'ler ******************* -Uygulama geliştirirken, örneğin bir tablonun tek tek satırları veya bir listenin öğeleri üzerinde AJAX işlemleri yapma ihtiyacı oldukça sık ortaya çıkar. Örnek olarak, makalelerin bir listesini seçebiliriz, burada her makale için giriş yapmış kullanıcının "beğen/beğenme" derecelendirmesini seçmesine izin veririz. AJAX olmadan presenter ve ilgili şablonun kodu yaklaşık olarak aşağıdaki gibi görünecektir (en önemli bölümleri listeliyorum, kod derecelendirmeleri işaretlemek ve makale koleksiyonunu almak için bir servisin varlığını varsayar - belirli uygulama bu kılavuzun amaçları için önemli değildir): +.[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 @@ -24,26 +27,26 @@ public function handleUnlike(int $articleId): void <h2>{$article->title}</h2> <div class="content">{$article->content}</div> {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Beğen</a> + <a n:href="like! $article->id" class=ajax>Beğendim</a> {else} - <a n:href="unlike! $article->id" class=ajax>Beğenmekten Vazgeç</a> + <a n:href="unlike! $article->id" class=ajax>Artık beğenmiyorum</a> {/if} </article> ``` -AJAXlaştırma -============ +AJAX'laştırma +============= -Şimdi bu basit uygulamayı AJAX ile donatalım. Bir makalenin derecelendirmesini değiştirmek, bir yönlendirme gerektirecek kadar önemli değildir ve bu nedenle ideal olarak arka planda AJAX ile gerçekleşmelidir. [Eklentilerden yardımcı betiği |application:ajax#Naja] AJAX bağlantılarının `ajax` CSS sınıfına sahip olduğu olağan kuralıyla kullanacağız. +Ş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. -Ancak, bunu tam olarak nasıl yapacağız? Nette 2 yol sunar: dinamik snippet'ler yolu ve bileşenler yolu. Her ikisinin de artıları ve eksileri vardır, bu yüzden onları birer birer göstereceğiz. +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 bir snippet, snippet adında bir değişkenin kullanıldığı `{snippet}` etiketinin özel bir kullanım durumunu ifade eder. Böyle bir snippet şablonda herhangi bir yerde bulunamaz - statik bir snippet, yani sıradan bir snippet veya `{snippetArea}` içinde sarmalanmalıdır. Şablonumuzu aşağıdaki gibi değiştirebiliriz. +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 @@ -53,18 +56,18 @@ Latte terminolojisinde dinamik bir snippet, snippet adında bir değişkenin kul <div class="content">{$article->content}</div> {snippet article-{$article->id}} {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Beğen</a> + <a n:href="like! $article->id" class=ajax>Beğendim</a> {else} - <a n:href="unlike! $article->id" class=ajax>Beğenmekten Vazgeç</a> + <a n:href="unlike! $article->id" class=ajax>Artık beğenmiyorum</a> {/if} {/snippet} </article> {/snippet} ``` -Her makale şimdi adında makale ID'si bulunan bir snippet tanımlar. Tüm bu snippet'ler daha sonra `articlesContainer` adlı tek bir snippet ile birlikte sarmalanır. Bu sarmalayıcı snippet'i atlarsak, Latte bizi bir istisna ile uyaracaktır. +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 presenter'a yeniden çizimi eklemek kalıyor - sadece statik sarmalayıcıyı yeniden çizmek yeterlidir. +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 @@ -72,18 +75,18 @@ public function handleLike(int $articleId): void $this->ratingService->saveLike($articleId, $this->user->id); if ($this->isAjax()) { $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- gerekli değil + // $this->redrawControl('article-' . $articleId); -- gerekmiyor } else { $this->redirect('this'); } } ``` -Benzer şekilde, kardeş metot `handleUnlike()`'ı da değiştiririz ve AJAX işlevseldir! +İlgili `handleUnlike()` metodunu da benzer şekilde değiştirin; AJAX çalışıyor! -Ancak çözümün bir dezavantajı var. AJAX isteğinin nasıl ilerlediğini daha fazla incelersek, uygulamanın dışarıdan verimli görünmesine rağmen (belirli makale için yalnızca tek bir snippet döndürür), aslında sunucuda tüm snippet'leri oluşturduğunu fark ederiz. İstenen snippet'i payload'a yerleştirdi ve diğerlerini attı (bu nedenle onları veritabanından tamamen gereksiz yere aldı). +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). -Bu süreci optimize etmek için, `$articles` koleksiyonunu şablona ilettiğimiz yere müdahale etmemiz gerekecek (diyelim ki `renderDefault()` metodunda). Sinyal işlemenin `render<Something>` metotlarından önce gerçekleştiği gerçeğinden yararlanacağız: +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 @@ -106,13 +109,13 @@ public function renderDefault(): void } ``` -Şimdi, sinyal işlenirken, tüm makaleleri içeren koleksiyon yerine şablona yalnızca tek bir makale içeren bir dizi iletilir - yani, tarayıcıya payload'da oluşturmak ve göndermek istediğimiz makale. `{foreach}` bu nedenle yalnızca bir kez çalışır ve fazladan snippet oluşturulmaz. +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 ============ -Tamamen farklı bir çözüm yaklaşımı dinamik snippet'lerden kaçınır. Hile, tüm mantığı özel bir bileşene aktarmaktır - bundan sonra derecelendirme girişi presenter tarafından değil, özel bir `LikeControl` tarafından yönetilecektir. Sınıf aşağıdaki gibi görünecektir (ayrıca `render`, `handleUnlike` vb. metotları da içerecektir): +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 @@ -134,19 +137,19 @@ class LikeControl extends Nette\Application\UI\Control } ``` -Bileşen şablonu: +Bileşenin şablonu: ```latte {snippet} {if !$article->liked} - <a n:href="like!" class=ajax>Beğen</a> + <a n:href="like!" class=ajax>Beğendim</a> {else} - <a n:href="unlike!" class=ajax>Beğenmekten Vazgeç</a> + <a n:href="unlike!" class=ajax>Artık beğenmiyorum</a> {/if} {/snippet} ``` -Tabii ki, görünüm şablonumuz değişecek ve presenter'a bir fabrika eklememiz gerekecek. Bileşeni veritabanından aldığımız makale sayısı kadar oluşturacağımız için, onu "çoğaltmak" için [application:Multiplier] sınıfını kullanacağız. +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() @@ -158,7 +161,7 @@ protected function createComponentLikeControl() } ``` -Görünüm şablonu gerekli minimuma indirildi (ve tamamen snippet'lerden arındırıldı!): +View'ın şablonu en aza iner (ve snippet'lerden tümüyle arınır!): ```latte <article n:foreach="$articles as $article"> @@ -168,6 +171,6 @@ Görünüm şablonu gerekli minimuma indirildi (ve tamamen snippet'lerden arınd </article> ``` -Neredeyse bitti: uygulama artık AJAX ile çalışacak. Burada da uygulamayı optimize etmemiz gerekiyor, çünkü Nette Database kullanımı nedeniyle, sinyal işlenirken veritabanından tüm makaleler gereksiz yere yüklenir, oysa sadece bir tanesi yeterlidir. Ancak avantajı, bunların oluşturulmamasıdır, çünkü gerçekten sadece bizim bileşenimiz oluşturulur. +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/editors-and-tools.texy b/best-practices/tr/editors-and-tools.texy deleted file mode 100644 index e1822a4311..0000000000 --- a/best-practices/tr/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editörler & Araçlar -******************* - -.[perex] -Yetenekli bir programcı olabilirsiniz, ancak ancak iyi araçlarla bir usta olursunuz. Bu bölümde önemli araçlar, editörler ve eklentiler hakkında ipuçları bulacaksınız. - - -IDE Editörü -=========== - -Geliştirme için kesinlikle PhpStorm, NetBeans, VS Code gibi tam özellikli bir IDE kullanmanızı öneririz, sadece PHP desteği olan bir metin editörü değil. Fark gerçekten çok büyük. Sadece sözdizimini renklendirebilen, ancak tam olarak ipucu veren, hataları kontrol eden, kodu yeniden düzenleyebilen ve çok daha fazlasını yapabilen birinci sınıf bir IDE'nin yeteneklerine ulaşamayan bir editörle yetinmek için hiçbir neden yok. Bazı IDE'ler ücretlidir, diğerleri ise ücretsizdir. - -**NetBeans IDE** Nette, Latte ve NEON desteği zaten yerleşiktir. - -**PhpStorm**: `Settings > Plugins > Marketplace` bölümünden şu eklentileri yükleyin -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: marketplace'te "Nette Latte + Neon" eklentisini bulun. - -Ayrıca Tracy'yi editörünüzle bağlayın. Hata sayfası görüntülendiğinde, dosya adlarına tıklayabilir ve bunlar editörde ilgili satırda imleçle açılır. [Sistemi nasıl yapılandıracağınızı |tracy:open-files-in-ide] okuyun. - - -PHPStan -======= - -PHPStan, kodu çalıştırmadan önce mantıksal hataları ortaya çıkaran bir araçtır. - -Composer kullanarak kurarız: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Projede `phpstan.neon` yapılandırma dosyasını oluştururuz: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Ve ardından `app/` klasöründeki sınıfları analiz etmesini sağlarız: - -```shell -vendor/bin/phpstan analyse app -``` - -Kapsamlı belgeleri doğrudan [PHPStan web sitesinde |https://phpstan.org] bulabilirsiniz. - - -Code Checker -============ - -[Code Checker|code-checker:] kaynak kodlarınızdaki bazı biçimsel hataları kontrol eder ve gerekirse düzeltir: - -- [BOM |nette:glossary#BOM] kaldırır -- [Latte |latte:] şablonlarının geçerliliğini kontrol eder -- `.neon`, `.php` ve `.json` dosyalarının geçerliliğini kontrol eder -- [kontrol karakterlerinin |nette:glossary#Kontrol Karakterleri] varlığını kontrol eder -- dosyanın UTF-8 olarak kodlanıp kodlanmadığını kontrol eder -- yanlış yazılmış `/* @anotace */` (yıldız eksik) kontrol eder -- PHP dosyalarındaki kapanış `?>` etiketini kaldırır -- dosya sonundaki sağdaki boşlukları ve gereksiz satırları kaldırır -- satır ayırıcılarını sistem varsayılanlarına normalleştirir (`-l` seçeneğini belirtirseniz) - - -Composer -======== - -[Composer |Composer] PHP'de bağımlılıkları yönetmek için bir araçtır. Bireysel kütüphanelerin keyfi olarak karmaşık bağımlılıklarını bildirmemize ve ardından bunları projemize kurmamıza olanak tanır. - - -Requirements Checker -==================== - -Sunucunun çalışma zamanı ortamını test eden ve framework'ün kullanılıp kullanılamayacağını (ve ne ölçüde) bildiren bir araçtı. Şu anda Nette, minimum gerekli PHP sürümüne sahip her sunucuda kullanılabilir. diff --git a/best-practices/tr/form-reuse.texy b/best-practices/tr/form-reuse.texy index 224e9f00d7..ff42e35b06 100644 --- a/best-practices/tr/form-reuse.texy +++ b/best-practices/tr/form-reuse.texy @@ -1,16 +1,16 @@ -Formların Birden Fazla Yerde Yeniden Kullanımı -********************************************** +Formları Birden Çok Yerde Yeniden Kullanma +****************************************** .[perex] -Nette'de, aynı formu birden fazla yerde kullanmak ve kodu tekrarlamamak için çeşitli seçenekleriniz vardır. Bu makalede, kaçınmanız gerekenler de dahil olmak üzere farklı çözümleri göstereceğiz. +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 Fabrikası -============== +Form Factory'si +=============== -Aynı bileşeni birden fazla yerde kullanmanın temel yaklaşımlarından biri, bu bileşeni üreten bir metot veya sınıf oluşturmak ve ardından bu metodu uygulamanın farklı yerlerinde çağırmaktır. Böyle bir metoda veya sınıfa *fabrika* denir. Lütfen fabrikaların belirli bir kullanım şeklini tanımlayan ve bu konuyla ilgili olmayan *factory method* tasarım deseniyle karıştırmayın. +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 oluşturacak bir fabrika yaratacağız: +Örnek olarak, bir düzenleme formu kuran bir factory oluşturalım: ```php use Nette\Application\UI\Form; @@ -21,21 +21,21 @@ class FormFactory { $form = new Form; $form->addText('title', 'Başlık:'); - // buraya diğer form alanları eklenir - $form->addSubmit('send', 'Gönder'); + // diğer form alanları buraya eklenir + $form->addSubmit('send', 'Kaydet'); return $form; } } ``` -Şimdi bu fabrikayı uygulamanızın farklı yerlerinde, örneğin presenter'larda veya bileşenlerde kullanabilirsiniz. Bunu [bağımlılık olarak talep edeceğiz |dependency-injection:passing-dependencies]. Önce sınıfı yapılandırma dosyasına yazarız: +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 ``` -Ve sonra onu presenter'da kullanırız: +Sonra onu bir presenter'da kullanın: ```php @@ -57,7 +57,7 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Form fabrikasını, uygulamanızın ihtiyaçlarına göre diğer tür formları oluşturmak için ek metotlarla genişletebilirsiniz. Ve tabii ki, öğeler olmadan temel bir form oluşturan ve diğer metotların kullanacağı bir metot da ekleyebiliriz: +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 @@ -72,20 +72,20 @@ class FormFactory { $form = $this->createForm(); $form->addText('title', 'Başlık:'); - // buraya diğer form alanları eklenir - $form->addSubmit('send', 'Gönder'); + // diğer form alanları buraya eklenir + $form->addSubmit('send', 'Kaydet'); return $form; } } ``` -`createForm()` metodu henüz yararlı bir şey yapmıyor, ancak bu hızla değişecek. +`createForm()` metodu henüz pek yararlı bir şey yapmıyor, ama bu yakında değişecek. -Fabrika Bağımlılıkları +Factory Bağımlılıkları ====================== -Zamanla, formların çok dilli olması gerektiği ortaya çıkacaktır. Bu, tüm formlara sözde [çevirmeni |forms:rendering#Çeviri] ayarlamamız gerektiği anlamına gelir. Bu amaçla, `FormFactory` sınıfını yapıcıda `Translator` nesnesini bir bağımlılık olarak kabul edecek şekilde değiştiririz ve onu forma iletiriz: +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; @@ -108,13 +108,13 @@ class FormFactory } ``` -`createForm()` metodu diğer belirli formları oluşturan metotlar tarafından da çağrıldığı için, çevirmeni yalnızca bu metotta ayarlamak yeterlidir. Ve işimiz bitti. Herhangi bir presenter veya bileşenin kodunu değiştirmeye gerek yok, ki bu harika. +`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. -Birden Fazla Fabrika Sınıfı -=========================== +Daha Fazla Factory Sınıfı +========================= -Alternatif olarak, uygulamanızda kullanmak istediğiniz her form için birden fazla sınıf oluşturabilirsiniz. Bu yaklaşım, kod okunabilirliğini artırabilir ve form yönetimini kolaylaştırabilir. Orijinal `FormFactory`'yi yalnızca temel yapılandırmaya sahip (örneğin çeviri desteği ile) temiz bir form oluşturmak için bırakırız ve düzenleme formu için yeni bir `EditFormFactory` fabrikası oluştururuz. +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 @@ -144,39 +144,39 @@ class EditFormFactory public function create(): Form { $form = $this->formFactory->create(); - // buraya diğer form alanları eklenir - $form->addSubmit('send', 'Gönder'); + // diğer form alanları buraya eklenir + $form->addSubmit('send', 'Kaydet'); return $form; } } ``` -`FormFactory` ve `EditFormFactory` sınıfları arasındaki bağın [nesne kalıtımı |nette:introduction-to-object-oriented-programming#Kompozisyon] yerine [kompozisyon |nette:introduction-to-object-oriented-programming#Kalıtım] ile gerçekleştirilmesi çok önemlidir: +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 -// ⛔ BU ŞEKİLDE DEĞİL! KALITIM BURAYA AİT DEĞİL +// ⛔ HAYIR! KALITIMIN BURADA YERİ YOK class EditFormFactory extends FormFactory { public function create(): Form { $form = parent::create(); $form->addText('title', 'Başlık:'); - // buraya diğer form alanları eklenir - $form->addSubmit('send', 'Gönder'); + // diğer form alanları buraya eklenir + $form->addSubmit('send', 'Kaydet'); return $form; } } ``` -Bu durumda kalıtım kullanmak tamamen verimsiz olurdu. Çok hızlı bir şekilde sorunlarla karşılaşırdınız. Örneğin, `create()` metoduna parametreler eklemek istediğinizde; PHP, imzasının ebeveyninden farklı olduğuna dair bir hata bildirirdi. Veya `EditFormFactory` sınıfına yapıcı aracılığıyla bir bağımlılık iletirken. [Yapıcı cehennemi |dependency-injection:passing-dependencies#Constructor Hell] dediğimiz bir durum ortaya çıkardı. +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, [kalıtım yerine kompozisyonu |dependency-injection:faq#Neden Kalıtım Yerine Kompozisyon Tercih Edilir] tercih etmek daha iyidir. +Genel olarak [kompozisyonu kalıtıma yeğlemek |dependency-injection:faq#Kompozisyon neden kalıtıma yeğlenir?] daha iyidir. -Form İşleme -=========== +Formun İşlenmesi +================ -Başarılı bir gönderimden sonra çağrılan form işleyicisi de fabrika sınıfının bir parçası olabilir. Gönderilen verileri işlenmek üzere modele ileterek çalışacaktır. Olası hataları forma [geri iletir |forms:validation#İşleme Sırasındaki Hatalar]. Aşağıdaki örnekte model, `Facade` sınıfı tarafından temsil edilmektedir: +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 @@ -191,13 +191,13 @@ class EditFormFactory { $form = $this->formFactory->create(); $form->addText('title', 'Başlık:'); - // buraya diğer form alanları eklenir - $form->addSubmit('send', 'Gönder'); - $form->onSuccess[] = [$this, 'processForm']; + // diğer form alanları buraya eklenir + $form->addSubmit('send', 'Kaydet'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // gönderilen verilerin işlenmesi @@ -210,7 +210,7 @@ class EditFormFactory } ``` -Ancak yönlendirmeyi presenter'a bırakacağız. `onSuccess` olayına yönlendirmeyi gerçekleştirecek başka bir işleyici ekleyecektir. Bu sayede formu farklı presenter'larda kullanmak ve her birinde farklı bir yere yönlendirmek mümkün olacaktır. +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 @@ -232,38 +232,38 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Bu çözüm, form veya öğesi üzerinde `addError()` çağrıldığında sonraki `onSuccess` işleyicisinin çağrılmaması özelliğini kullanır. +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 Kalıtım Alma -============================ +Form Sınıfından Türetme +======================= -Oluşturulan form, formun bir alt sınıfı olmamalıdır. Başka bir deyişle, bu çözümü kullanmayın: +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 -// ⛔ BU ŞEKİLDE DEĞİL! KALITIM BURAYA AİT DEĞİL +// ⛔ HAYIR! KALITIMIN BURADA YERİ YOK class EditForm extends Form { public function __construct(Translator $translator) { parent::__construct(); $this->addText('title', 'Başlık:'); - // buraya diğer form alanları eklenir - $this->addSubmit('send', 'Gönder'); + // diğer form alanları buraya eklenir + $this->addSubmit('send', 'Kaydet'); $this->setTranslator($translator); } } ``` -Formu yapıcıda oluşturmak yerine bir fabrika kullanın. +Formu yapıcının içinde kurmak yerine bir factory kullanın. -`Form` sınıfının öncelikle bir form oluşturma aracı, yani bir *form oluşturucu* olduğu unutulmamalıdır. Ve oluşturulan form, onun bir ürünü olarak düşünülebilir. Ancak ürün, oluşturucunun özel bir durumu değildir, aralarında kalıtımın temelini oluşturan bir *is a* ilişkisi yoktur. +Ş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 İçeren Bileşen -=================== +Form Bileşeni +============= -Tamamen farklı bir yaklaşım, form içeren bir [bileşen |application:components] oluşturmaktır. Bu, örneğin formu belirli bir şekilde oluşturmak gibi yeni olanaklar sunar, çünkü bileşenin bir parçası olarak bir şablon da bulunur. Veya AJAX iletişimi ve forma bilgi yükleme, örneğin öneriler için sinyaller kullanılabilir, vb. +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 @@ -282,14 +282,14 @@ class EditControl extends Nette\Application\UI\Control { $form = new Form; $form->addText('title', 'Başlık:'); - // buraya diğer form alanları eklenir - $form->addSubmit('send', 'Gönder'); - $form->onSuccess[] = [$this, 'processForm']; + // diğer form alanları buraya eklenir + $form->addSubmit('send', 'Kaydet'); + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // gönderilen verilerin işlenmesi @@ -300,13 +300,13 @@ class EditControl extends Nette\Application\UI\Control return; } - // olayın tetiklenmesi + // olayın çağrılması $this->onSave($this, $data); } } ``` -Bu bileşeni üretecek bir fabrika da oluşturacağız. Sadece [arayüzünü yazmanız |application:components#Bağımlılıklara Sahip Bileşenler] yeterlidir: +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 @@ -315,14 +315,14 @@ interface EditControlFactory } ``` -Ve yapılandırma dosyasına ekleyin: +Ve onu yapılandırma dosyasına ekleyin: ```neon services: - EditControlFactory ``` -Ve şimdi fabrikayı talep edebilir ve presenter'da kullanabiliriz: +Artık factory'yi isteyip presenter'da kullanabiliriz: ```php class MyPresenter extends Nette\Application\UI\Presenter @@ -338,7 +338,7 @@ class MyPresenter extends Nette\Application\UI\Presenter $control->onSave[] = function (EditControl $control, $data) { $this->redirect('this'); - // veya düzenleme sonucuna yönlendiririz, örn.: + // ya da düzenleme sonucuna yönlendir, örneğin: // $this->redirect('detail', ['id' => $data->id]); }; diff --git a/best-practices/tr/inject-method-attribute.texy b/best-practices/tr/inject-method-attribute.texy index 95d08e2e06..52689032c5 100644 --- a/best-practices/tr/inject-method-attribute.texy +++ b/best-practices/tr/inject-method-attribute.texy @@ -1,18 +1,18 @@ -Inject Metotları ve Nitelikleri -******************************* +Inject Metotları ve Attribute'ları +********************************** .[perex] -Bu makalede, Nette framework'ünde presenter'lara bağımlılıkları iletmenin farklı yollarına odaklanacağız. Tercih edilen yöntem olan yapıcıyı, inject metotları ve nitelikleri gibi diğer seçeneklerle karşılaştıracağız. +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'lar için de bağımlılıkların [yapıcı |dependency-injection:passing-dependencies#Yapıcı ile İletme] aracılığıyla iletilmesinin tercih edilen yol olduğu geçerlidir. Ancak, diğer presenter'ların miras aldığı ortak bir ata sınıf (örneğin `BasePresenter`) oluşturuyorsanız ve bu ata sınıfın da bağımlılıkları varsa, [yapıcı cehennemi |dependency-injection:passing-dependencies#Constructor Hell] dediğimiz bir sorun ortaya çıkar. Bu, inject metotları ve nitelikleri (eski adıyla anotasyonlar) olan alternatif yollarla aşılabilir. +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ığın [ayarlayıcı |dependency-injection:passing-dependencies#Setter ile İletme] ile iletilmesinin bir şeklidir. Bu ayarlayıcıların adı `inject` önekiyle başlar. Nette DI, bu şekilde adlandırılan metotları presenter örneği oluşturulduktan hemen sonra otomatik olarak çağırır ve onlara tüm gerekli bağımlılıkları iletir. Bu nedenle `public` olarak bildirilmelidirler. +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 fazla metoda genişletilmiş bir türü olarak kabul edilebilir. Bu sayede `BasePresenter`, bağımlılıkları başka bir metot aracılığıyla alabilir ve yapıcıyı alt sınıfları için serbest bırakabilir: +`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 @@ -36,18 +36,18 @@ class MyPresenter extends BasePresenter } ``` -Bir presenter, keyfi sayıda `inject*()` metodu içerebilir ve her biri keyfi sayıda parametreye sahip olabilir. Bu, presenter'ın [trait'lerden oluştuğunda |presenter-traits] ve her birinin kendi bağımlılığını gerektirdiği durumlarda da harika çalışır. +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` Nitelikleri -==================== +`Inject` Attribute'ları +======================= -Bu, [özelliğe enjekte etme |dependency-injection:passing-dependencies#Değişken Ayarlayarak] şeklidir. Hangi değişkenlere enjekte edileceğini belirtmek yeterlidir ve Nette DI, presenter örneği oluşturulduktan hemen sonra bağımlılıkları otomatik olarak iletir. Bunları ekleyebilmesi için `public` olarak bildirilmelidirler. +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. -Özellikleri nitelikle işaretleriz: (daha önce `/** @inject */` anotasyonu kullanılıyordu) +Özellikler bir attribute ile işaretlenir: (eskiden `/** @inject */` açıklaması kullanılıyordu) ```php -use Nette\DI\Attributes\Inject; // bu satır önemlidir +use Nette\DI\Attributes\Inject; // bu satır önemli class MyPresenter extends Nette\Application\UI\Presenter { @@ -56,6 +56,6 @@ class MyPresenter extends Nette\Application\UI\Presenter } ``` -Bu bağımlılık iletme yönteminin avantajı, çok kısa bir yazım şekli olmasıydı. Ancak, [constructor property promotion |https://blog.nette.org/tr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] 'ın gelişiyle, yapıcıyı kullanmak daha kolay görünüyor. +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. -Tersine, bu yöntem, genel olarak özelliklere bağımlılık iletmeyle aynı dezavantajlara sahiptir: değişken üzerindeki değişiklikler üzerinde kontrolümüz yoktur ve aynı zamanda değişken, sınıfın genel arayüzünün bir parçası haline gelir, ki bu istenmeyen bir durumdur. +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 index 042f486703..bf686c2288 100644 --- a/best-practices/tr/lets-create-contact-form.texy +++ b/best-practices/tr/lets-create-contact-form.texy @@ -1,12 +1,12 @@ -İletişim Formu Oluşturma -************************ +Bir İletişim Formu Oluşturalım +****************************** .[perex] -Nette'de e-postaya gönderme dahil bir iletişim formunun nasıl oluşturulacağına bir göz atacağız. Öyleyse başlayalım! +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! -Öncelikle yeni bir proje oluşturmamız gerekiyor. Bunun nasıl yapılacağını [Başlarken |nette:installation] sayfası açıklıyor. Ve sonra formu oluşturmaya başlayabiliriz. +Ö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 kolay yol, [doğrudan presenter içinde form |forms:in-presenter] oluşturmaktır. Önceden hazırlanmış `HomePresenter`'ı kullanabiliriz. Ona formu temsil eden `contactForm` bileşenini ekleyeceğiz. Bunu, bileşeni üreten `createComponentContactForm()` fabrika metodunu koda yazarak yapacağız: +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; @@ -17,27 +17,27 @@ class HomePresenter extends Presenter protected function createComponentContactForm(): Form { $form = new Form; - $form->addText('name', 'İsim:') - ->setRequired('İsminizi girin'); + $form->addText('name', 'Ad:') + ->setRequired('Lütfen adınızı girin'); $form->addEmail('email', 'E-posta:') - ->setRequired('E-postanızı girin'); - $form->addTextarea('message', 'Mesaj:') - ->setRequired('Mesajınızı girin'); + ->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']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // e-posta gönderimi } } ``` -Gördüğünüz gibi, iki metot oluşturduk. İlk metot `createComponentContactForm()` yeni bir form oluşturur. Bu formda isim, e-posta ve mesaj için `addText()`, `addEmail()` ve `addTextArea()` metotlarıyla eklediğimiz alanlar bulunur. Ayrıca formu göndermek için bir düğme ekledik. Peki ya kullanıcı bir alanı doldurmazsa? Bu durumda, bunun zorunlu bir alan olduğunu ona bildirmeliyiz. Bunu `setRequired()` metoduyla başardık. Son olarak, form başarıyla gönderildiğinde tetiklenecek olan [olay |nette:glossary#Olaylar Events] `onSuccess`'ı da ekledik. Bizim durumumuzda, gönderilen formu işlemekle ilgilenecek olan `contactFormSucceeded` metodunu çağırır. Bunu bir an sonra koda ekleyeceğiz. +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 oluşturulmasını sağlayacağız: +`contactForm` bileşenini `Home/default.latte` şablonunda render edelim: ```latte {block content} @@ -45,12 +45,9 @@ Gördüğünüz gibi, iki metot oluşturduk. İlk metot `createComponentContactF {control contactForm} ``` -E-postanın kendisini göndermek için `ContactFacade` adını vereceğimiz yeni bir sınıf oluşturacağız ve onu `app/Model/ContactFacade.php` dosyasına yerleştireceğiz: +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 -<?php -declare(strict_types=1); - namespace App\Model; use Nette\Mail\Mailer; @@ -76,9 +73,9 @@ class ContactFacade } ``` -`sendMessage()` metodu bir e-posta oluşturur ve gönderir. Bunun için yapıcı aracılığıyla bir bağımlılık olarak aldığı sözde mailer'ı kullanır. [E-posta gönderme |mail:] hakkında daha fazla bilgi edinin. +`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 geri dönelim ve `contactFormSucceeded()` metodunu tamamlayalım. Bu metot, `ContactFacade` sınıfının `sendMessage()` metodunu çağıracak ve ona formdan gelen verileri iletecektir. Peki `ContactFacade` nesnesini nasıl elde ederiz? Yapıcı aracılığıyla bize iletilmesini sağlayacağız: +Ş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; @@ -106,30 +103,30 @@ class HomePresenter extends Presenter } ``` -E-posta gönderildikten sonra, kullanıcıya mesajın gönderildiğini onaylayan sözde bir [flash mesajı |application:components#Flash Mesajları] göstereceğiz ve ardından formun tarayıcıda *yenile* ile tekrar tekrar gönderilmesini önlemek için bir sonraki sayfaya yönlendireceğiz. +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. -İşte bu kadar, her şey çalışıyorsa, iletişim formunuzdan bir e-posta gönderebilmelisiniz. Tebrikler! +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 ana kadar, yalnızca form tarafından gönderilen mesajı içeren düz metin bir e-posta gönderiliyor. Ancak e-postada HTML kullanabilir ve görünümünü daha çekici hale getirebiliriz. Bunun için Latte'de bir şablon oluşturacağız ve onu `app/Model/contactEmail.latte` dosyasına yazacağız: +Ş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 + İletişim formundan mesaj -

    İsim: {$name}

    +

    Ad: {$name}

    E-posta: {$email}

    Mesaj: {$message}

    ``` -Geriye `ContactFacade`'i bu şablonu kullanacak şekilde düzenlemek kalıyor. Yapıcıda, `Latte\Engine` nesnesini, yani [Latte şablon oluşturucuyu |latte:develop#Bir Şablon Nasıl Oluşturulur] üretebilen `LatteFactory` sınıfını talep edeceğiz. `renderToString()` metoduyla şablonu bir dosyaya oluşturacağız, ilk parametre şablonun yolu ve ikincisi değişkenlerdir. +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; @@ -165,15 +162,15 @@ class ContactFacade } ``` -Oluşturulan HTML e-postayı daha sonra orijinal `setBody()` yerine `setHtmlBody()` metoduna ileteceğiz. Ayrıca `setSubject()` içinde e-posta konusunu belirtmemize gerek yok, çünkü kütüphane onu şablonun `` öğesinden alacaktır. +Ü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 `<title>` elemanından otomatik olarak çıkarır. Yapılandırma ------------ -`ContactFacade` sınıfının kodunda, yönetici e-postamız `admin@example.com` hala sabit kodlanmıştır. Onu yapılandırma dosyasına taşımak daha iyi olurdu. Bunu nasıl yaparız? +`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ı düzenleriz ve e-posta içeren karakter dizisini yapıcı tarafından iletilen bir değişkenle değiştiririz: +Ö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 @@ -197,25 +194,25 @@ class ContactFacade } ``` -Ve ikinci adım, bu değişkenin değerini yapılandırmada belirtmektir. `app/config/services.neon` dosyasına şunu yazarız: +İ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 işte bu kadar. `services` bölümündeki öğelerin sayısı çok fazlaysa ve e-postanın aralarında kaybolduğunu düşünüyorsanız, onu bir değişkene dönüştürebiliriz. Yazımı şu şekilde düzenleriz: +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 `app/config/common.neon` dosyasında bu değişkeni tanımlarız: +Ve bu parametreyi `app/config/common.neon` dosyasında tanımlayın: ```neon parameters: adminEmail: admin@example.com ``` -Ve işimiz bitti! +Ve iş tamam! diff --git a/best-practices/tr/microsites.texy b/best-practices/tr/microsites.texy index fca2a1ebeb..e2bb5836fa 100644 --- a/best-practices/tr/microsites.texy +++ b/best-practices/tr/microsites.texy @@ -1,11 +1,11 @@ -Mikro web siteleri nasıl yazılır -******************************** +Mikro Siteler Nasıl Yazılır +*************************** -Şirketinizin yaklaşan bir etkinliği için hızlı bir şekilde küçük bir web sitesi oluşturmanız gerektiğini hayal edin. Basit, hızlı ve gereksiz karmaşıklıklar olmadan olmalı. Böyle küçük bir proje için sağlam bir framework'e ihtiyacınız olmadığını düşünebilirsiniz. Peki ya Nette framework kullanmak bu süreci temelden basitleştirip hızlandırabilirse? +Ş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? -Sonuçta, basit web siteleri oluştururken bile rahatlıktan vazgeçmek istemezsiniz. Bir kez çözülmüş olanı yeniden icat etmek istemezsiniz. Tembel olmaktan çekinmeyin ve şımartılmaya izin verin. Nette Framework, bir mikro framework olarak da mükemmel bir şekilde kullanılabilir. +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, web sitesinin tüm kodunu genel klasördeki tek bir `index.php` dosyasına yerleştirerek: +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 @@ -16,25 +16,25 @@ $configurator = new Nette\Bootstrap\Configurator; $configurator->enableTracy(__DIR__ . '/../log'); $configurator->setTempDirectory(__DIR__ . '/../temp'); -// config.neon içindeki yapılandırmaya göre DI konteynerini oluştur +// config.neon içindeki yapılandırmaya göre DI container oluştur $configurator->addConfig(__DIR__ . '/../app/config.neon'); $container = $configurator->createContainer(); -// yönlendirmeyi ayarla +// 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 algıla ve /en veya /de vb. URL'ye yönlendir + // 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); - $presenter->redirectUrl("/$lang"); + return $presenter->redirectUrl("/$lang"); }); -// https://example.com/cs veya https://example.com/en URL'si için rota -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // ilgili şablonu göster, örneğin ../templates/en.latte +// 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; @@ -44,20 +44,20 @@ $router->addRoute('<lang cs|en>', function ($presenter, string $lang) { $container->getByType(Nette\Application\Application::class)->run(); ``` -Geri kalan her şey, üst klasör `/templates` içinde saklanan şablonlar olacaktır. +Geri kalan her şey, üst `/templates` dizininde saklanan şablonlar olacak. -`index.php` içindeki PHP kodu önce [ortamı hazırlar |bootstrap:], ardından [rotaları tanımlar |application:routing#Geri Çağrılarla Dinamik Yönlendirme] ve son olarak uygulamayı çalıştırır. Avantajı, `addRoute()` fonksiyonunun ikinci parametresinin, ilgili sayfa açıldığında yürütülecek bir callable olabilmesidir. +`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. -Neden mikro siteler için Nette kullanmalı? ------------------------------------------- +Mikro Siteler İçin Neden Nette? +------------------------------- -- [Tracy|tracy:]'yi bir kez deneyen programcılar, bugün onsuz bir şey programlamayı hayal edemezler. -- Ama her şeyden önce, şablonlama sistemi [Latte|latte:]'yi kullanacaksınız, çünkü 2 sayfadan itibaren [düzeni ve içeriği|latte:template-inheritance] ayırmak isteyeceksiniz. -- Ve kesinlikle XSS güvenlik açığı oluşmaması için [otomatik kaçışa |latte:safety-first] güvenmek istersiniz. -- Nette ayrıca bir hata durumunda programcı hata mesajlarının PHP'de asla gösterilmemesini, bunun yerine kullanıcı dostu bir sayfanın gösterilmesini sağlar. -- Kullanıcılardan geri bildirim almak istiyorsanız, örneğin bir iletişim formu şeklinde, o zaman [formları|forms:] ve [veritabanını|database:] da eklersiniz. -- Doldurulmuş formları kolayca [e-posta ile gönderebilirsiniz|mail:]. -- Bazen [önbelleğe alma|caching:] işinize yarayabilir, örneğin beslemeleri indirip görüntülüyorsanız. +- [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 ve verimliliğin anahtar olduğu günümüzde, gereksiz gecikmeler olmadan sonuçlara ulaşmanızı sağlayan araçlara sahip olmak önemlidir. Nette framework size tam da bunu sunar - hızlı geliştirme, güvenlik ve süreci basitleştiren Tracy ve Latte gibi geniş bir araç yelpazesi. Sadece birkaç Nette paketi yükleyin ve böyle bir mikro site oluşturmak birdenbire çocuk oyuncağı haline gelir. Ve hiçbir yerde gizli bir güvenlik açığı olmadığını bilirsiniz. +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 index 92509706a7..db396606d7 100644 --- a/best-practices/tr/pagination.texy +++ b/best-practices/tr/pagination.texy @@ -1,10 +1,10 @@ -Veritabanı sonuçlarını sayfalama +Veritabanı Sonuçlarını Sayfalama ******************************** .[perex] -Web uygulamaları geliştirirken, sayfada görüntülenen öğe sayısını sınırlama gereksinimiyle çok sık karşılaşırsınız. +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 sayfalama olmadan listelediğimiz durumdan başlayalım. Veritabanından veri seçmek için, yapıcıya ek olarak, yayınlanan tüm makaleleri yayın tarihine göre azalan sırada döndüren `findPublishedArticles` metodunu içeren bir `ArticleRepository` sınıfımız var. +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; @@ -30,7 +30,7 @@ class ArticleRepository } ``` -Presenter'da model sınıfını enjekte ederiz ve render metodunda yayınlanan makaleleri talep ederiz, bunları şablona iletiriz: +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; @@ -52,7 +52,7 @@ class HomePresenter extends Nette\Application\UI\Presenter } ``` -`default.latte` şablonunda makalelerin listelenmesini sağlarız: +`default.latte` şablonu da makaleleri listelemeyi üstlenir: ```latte {block content} @@ -67,11 +67,11 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Bu şekilde tüm makaleleri listeleyebiliriz, ancak makale sayısı arttığında bu sorun yaratmaya başlar. Bu noktada bir sayfalama mekanizması uygulamak faydalı olacaktır. +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, tüm makalelerin birkaç sayfaya bölünmesini ve yalnızca geçerli bir sayfanın makalelerini görüntülememizi sağlar. Toplam sayfa sayısı ve makalelerin dağılımı, toplamda kaç makalemiz olduğuna ve sayfa başına kaç makale görüntülemek istediğimize bağlı olarak [utils:Paginator | utils:Paginator] tarafından hesaplanır. +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, depodaki makaleleri almak için metodu, yalnızca bir sayfa için makaleleri döndürebilecek şekilde değiştiririz. Ayrıca, Paginator'u ayarlamak için ihtiyaç duyacağımız veritabanındaki toplam makale sayısını bulmak için bir metot ekleriz: +İ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; @@ -99,7 +99,7 @@ class ArticleRepository } /** - * Yayınlanan toplam makale sayısını döndürür + * Yayımlanmış makalelerin toplam sayısını döndürür */ public function getPublishedArticlesCount(): int { @@ -108,9 +108,9 @@ class ArticleRepository } ``` -Ardından, presenter'ı düzenlemeye başlarız. Render metoduna, görüntülenen geçerli sayfanın numarasını ileteceğiz. Bu numaranın URL'nin bir parçası olmadığı durumlar için, ilk sayfanın varsayılan değerini ayarlarız. +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 Paginator örneğini almak, ayarlamak ve şablonda görüntülenecek doğru makaleleri seçmek için genişletiriz. HomePresenter, düzenlemelerden sonra şöyle görünecektir: +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; @@ -127,27 +127,27 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Yayınlanan toplam makale sayısını bulalım + // Yayımlanmış makalelerin toplam sayısını al $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - // Paginator örneğini oluşturalım ve ayarlayalım + // Paginator örneğini oluştur ve yapılandır $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // toplam makale sayısı - $paginator->setItemsPerPage(10); // sayfa başına öğe sayısı + $paginator->setItemCount($articlesCount); // toplam öğe sayısı + $paginator->setItemsPerPage(10); // sayfa başına öğe $paginator->setPage($page); // geçerli sayfa numarası - // Veritabanından Paginator hesaplamasına göre sınırlı bir makale kümesi çekelim + // Paginator'ın hesabına göre veritabanından sınırlı bir makale kümesi getir $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - // bunu şablona iletelim + // onları şablona aktar $this->template->articles = $articles; - // ve ayrıca sayfalama seçeneklerini görüntülemek için Paginator'ın kendisini + // ve sayfalama denetimlerini göstermek için Paginator'ın kendisini de $this->template->paginator = $paginator; } } ``` -Şablonumuz artık yalnızca bir sayfanın makaleleri üzerinde yineleniyor, sadece sayfalama bağlantılarını eklememiz gerekiyor: +Ş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} @@ -164,7 +164,7 @@ class HomePresenter extends Nette\Application\UI\Presenter {if !$paginator->isFirst()} <a n:href="default, 1">İlk</a>  |  - <a n:href="default, $paginator->page-1">Önceki</a> + <a n:href="default, $paginator->getPage() - 1">Önceki</a>  |  {/if} @@ -180,9 +180,9 @@ class HomePresenter extends Nette\Application\UI\Presenter ``` -Bu şekilde, Paginator kullanarak sayfaya sayfalama yeteneği ekledik. Veritabanı katmanı olarak [Nette Database Core |database:sql-way] yerine [Nette Database Explorer |database:explorer] kullanırsak, Paginator kullanmadan da sayfalama uygulayabiliriz. `Nette\Database\Table\Selection` sınıfı, Paginator'dan alınan sayfalama mantığına sahip [page |api:Nette\Database\Table\Selection::_page] metodunu içerir. +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 uygulama yöntemiyle depo şöyle görünecektir: +Bu yaklaşımda repository şöyle görünür: ```php namespace App\Model; @@ -205,7 +205,7 @@ class ArticleRepository } ``` -Presenter'da Paginator oluşturmamıza gerek yok, bunun yerine deponun döndürdüğü `Selection` sınıfının metodunu kullanırız: +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; @@ -222,21 +222,21 @@ class HomePresenter extends Nette\Application\UI\Presenter public function renderDefault(int $page = 1): void { - // Yayınlanan makaleleri çekelim + // Yayımlanmış makaleleri getir $articles = $this->articleRepository->findPublishedArticles(); - // ve şablona yalnızca page metodunun hesaplamasına göre sınırlanmış bir kısmını gönderelim + // 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 ayrıca sayfalama seçeneklerini görüntülemek için gerekli verileri + // ve sayfalama seçeneklerini göstermek için gereken verileri de $this->template->page = $page; $this->template->lastPage = $lastPage; } } ``` -Şimdi şablona Paginator göndermediğimiz için, sayfalama bağlantılarını gösteren kısmı düzenleriz: +Ş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} @@ -268,6 +268,6 @@ class HomePresenter extends Nette\Application\UI\Presenter </div> ``` -Bu şekilde, Paginator kullanmadan sayfalama mekanizmasını uyguladık. +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 index b71ddc741d..bab7e1227c 100644 --- a/best-practices/tr/passing-settings-to-presenters.texy +++ b/best-practices/tr/passing-settings-to-presenters.texy @@ -1,10 +1,10 @@ -Ayarları presenter'lara iletme -****************************** +Presenter'lara Ayar Aktarma +*************************** .[perex] -Presenter'lara nesne olmayan argümanları (örneğin, hata ayıklama modunda çalışıp çalışmadığı bilgisi, dizin yolları vb.) iletmeniz gerekiyor ve bu nedenle otomatik kablolama (autowiring) ile otomatik olarak iletilemiyorlar mı? Çözüm, bunları bir `Settings` nesnesine sarmaktır. +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` hizmeti, çalışan uygulama hakkındaki bilgileri presenter'lara sağlamanın çok kolay ve aynı zamanda kullanışlı bir yoludur. Somut biçimi tamamen özel ihtiyaçlarınıza bağlıdır. Örnek: +`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; @@ -12,7 +12,7 @@ namespace App; class Settings { public function __construct( - // PHP 8.1'den itibaren readonly belirtilebilir + // PHP 8.1'den beri readonly kullanılabilir public bool $debugMode, public string $appDir, // vb. @@ -20,7 +20,7 @@ class Settings } ``` -Yapılandırmaya kayıt örneği: +Yapılandırmada kaydetme örneği: ```neon services: @@ -30,7 +30,7 @@ services: ) ``` -Presenter bu hizmet tarafından sağlanan bilgilere ihtiyaç duyduğunda, yapıcıda basitçe ister: +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 diff --git a/best-practices/tr/post-links.texy b/best-practices/tr/post-links.texy index 4d4ca45666..9dee9339a5 100644 --- a/best-practices/tr/post-links.texy +++ b/best-practices/tr/post-links.texy @@ -1,16 +1,16 @@ -POST bağlantıları nasıl doğru kullanılır -**************************************** +POST Bağlantıları Doğru Şekilde Nasıl Kullanılır +************************************************ .[perex] -Web uygulamalarında, özellikle yönetim arayüzlerinde, sunucu durumunu değiştiren eylemlerin HTTP GET metodu aracılığıyla gerçekleştirilmemesi temel bir kural olmalıdır. Metodun adından da anlaşılacağı gibi, GET yalnızca veri almak için kullanılmalı, değiştirmek için değil. Kayıt silme gibi eylemler için POST metodunu kullanmak daha uygundur. İdeal olan DELETE metodu olsa da, JavaScript olmadan çağrılamaz, bu nedenle tarihsel olarak POST kullanılır. +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. -Pratikte nasıl yapılır? Bu basit hileyi kullanın. Şablonun başında, `postForm` tanımlayıcısına sahip yardımcı bir form oluşturursunuz, bunu daha sonra silme düğmeleri için kullanırsınız: +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, klasik bir `<a>` bağlantısı yerine, görsel olarak normal bir bağlantı gibi görünecek şekilde ayarlanabilen bir `<button>` düğmesi kullanabilirsiniz. Örneğin, Bootstrap CSS framework'ü, düğmenin diğer bağlantılardan görsel olarak farklı olmamasını sağlayan `btn btn-link` sınıflarını sunar. `form="postForm"` niteliğini kullanarak onu önceden hazırlanmış formla ilişkilendiririz: +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> @@ -24,7 +24,7 @@ Bu form sayesinde, klasik bir `<a>` bağlantısı yerine, görsel olarak normal </table> ``` -Bağlantıya tıklandığında, şimdi `delete` eylemi çağrılır. İsteklerin yalnızca POST metodu aracılığıyla ve aynı etki alanından kabul edilmesini sağlamak için (bu, CSRF saldırılarına karşı etkili bir savunmadır), `#[Requires]` niteliğini kullanın: +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; @@ -40,9 +40,9 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -Nitelik Nette Application 3.2'den beri mevcuttur ve yetenekleri hakkında daha fazla bilgiyi [Requires niteliği nasıl kullanılır |attribute-requires] sayfasında bulabilirsiniz. +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, sinyallerin bu koruması örtük olarak ayarlandığından `sameOrigin: true` belirtmek gerekli değildir: +`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')] @@ -53,4 +53,4 @@ public function handleDelete(int $id): void } ``` -Bu yaklaşım yalnızca uygulamanızın güvenliğini artırmakla kalmaz, aynı zamanda doğru web standartlarına ve uygulamalarına uymaya da katkıda bulunur. Durumu değiştiren eylemler için POST yöntemlerini kullanarak daha sağlam ve güvenli bir uygulama elde edersiniz. +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 index a75c15f500..abcbe39854 100644 --- a/best-practices/tr/presenter-traits.texy +++ b/best-practices/tr/presenter-traits.texy @@ -1,14 +1,14 @@ -Presenter'ları trait'lerden oluşturma -************************************* +Presenter'ları Trait'lerden Kurma +********************************* .[perex] -Birden fazla presenter'da aynı kodu uygulamamız gerekiyorsa (örneğin, kullanıcının oturum açıp açmadığını doğrulamak), kodu ortak bir ataya yerleştirmek bir seçenektir. İkinci seçenek, tek amaçlı [trait'ler |nette:introduction-to-object-oriented-programming#Traitler] oluşturmaktır. +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. -Bu çözümün avantajı, her presenter'ın yalnızca gerçekten ihtiyaç duyduğu trait'leri kullanabilmesidir, oysa PHP'de çoklu kalıtım mümkün değildir. +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 oluşturulduğunda tüm [inject metotlarının |inject-method-attribute#inject Metotları] sırayla çağrılması gerçeğinden yararlanabilir. Yalnızca her inject metodunun adının benzersiz olduğundan emin olmak gerekir. +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 veya onRender |application:presenters#Olaylar] olaylarına bağlayabilir. +Trait'ler, başlatma kodunu [onStartup ya da onRender |application:presenters#Olaylar] olaylarına bağlayabilir. Örnekler: @@ -36,7 +36,7 @@ trait StandardTemplateFilters } ``` -Presenter daha sonra bu trait'leri basitçe kullanır: +Presenter ise yalnızca bu trait'leri kullanır: ```php class ArticlePresenter extends Nette\Application\UI\Presenter diff --git a/best-practices/tr/pretty-urls.texy b/best-practices/tr/pretty-urls.texy new file mode 100644 index 0000000000..41fa193089 --- /dev/null +++ b/best-practices/tr/pretty-urls.texy @@ -0,0 +1,204 @@ +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 index 0316b1cc68..384ddfc3f4 100644 --- a/best-practices/tr/restore-request.texy +++ b/best-practices/tr/restore-request.texy @@ -1,16 +1,16 @@ -Önceki bir sayfaya nasıl dönülür? -********************************* +Önceki Sayfaya Nasıl Dönülür? +***************************** .[perex] -Bir kullanıcı bir form doldururken oturumu sona ererse ne olur? Verilerini kaybetmemek için, oturum açma sayfasına yönlendirmeden önce verileri oturumda saklarız. Nette'de bu çocuk oyuncağıdır. +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()` metodu kullanılarak oturumda saklanabilir, bu metot isteğin tanımlayıcısını kısa bir dize olarak döndürür. Metot, geçerli presenter'ın adını, görünümünü ve parametrelerini saklar. Bir form da gönderildiyse, alanların içeriği de saklanır (yüklenen dosyalar hariç). +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. -İsteğin geri yüklenmesi, elde edilen tanımlayıcıyı ilettiğimiz `restoreRequest($key)` metodu tarafından gerçekleştirilir. Bu metot, orijinal presenter'a ve görünüme yönlendirir. Ancak, saklanan istek bir form gönderimi içeriyorsa, orijinal presenter'a `forward()` metoduyla geçer, forma daha önce doldurulan değerleri iletir ve yeniden oluşturulmasını sağlar. Bu şekilde kullanıcı formu tekrar gönderme fırsatına sahip olur ve hiçbir veri kaybolmaz. +İ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. -Önemli olan, `restoreRequest()` metodunun yeni oturum açan kullanıcının formu başlangıçta dolduranla aynı olup olmadığını kontrol etmesidir. Değilse, isteği atar ve hiçbir şey yapmaz. +Ç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. -Her şeyi bir örnekle gösterelim. Verilerin düzenlendiği ve `startup()` metodunda kullanıcının oturum açıp açmadığını doğruladığımız bir `AdminPresenter`'ımız olsun. Değilse, onu `SignPresenter`'a yönlendiririz. Aynı zamanda geçerli isteği saklarız ve anahtarını `SignPresenter`'a göndeririz. +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 @@ -26,7 +26,7 @@ class AdminPresenter extends Nette\Application\UI\Presenter } ``` -`SignPresenter`, oturum açma formuna ek olarak, anahtarın yazılacağı kalıcı bir `$backlink` parametresi de içerecektir. Parametre kalıcı olduğu için, oturum açma formu gönderildikten sonra da aktarılacaktır. +`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 @@ -40,14 +40,14 @@ class SignPresenter extends Nette\Application\UI\Presenter protected function createComponentSignInForm() { $form = new Nette\Application\UI\Form; - // ... form alanlarını ekleyin ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + // ... form alanlarını ekle ... + $form->onSuccess[] = $this->signInFormSuceeded(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSuceeded($form) { - // ... burada kullanıcıyı oturum açtırın ... + // ... kullanıcının oturumunu burada aç ... $this->restoreRequest($this->backlink); $this->redirect('Admin:'); @@ -55,8 +55,8 @@ class SignPresenter extends Nette\Application\UI\Presenter } ``` -`restoreRequest()` metoduna saklanan isteğin anahtarını iletiriz ve o, orijinal presenter'a yönlendirir (veya geçer). +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, artık oturumda mevcut değilse), metot hiçbir şey yapmaz. Bu nedenle, `AdminPresenter`'a yönlendiren `$this->redirect('Admin:')` çağrısı takip 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/best-practices/uk/@home.texy b/best-practices/uk/@home.texy deleted file mode 100644 index f9d3510c2b..0000000000 --- a/best-practices/uk/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Посібники та практики -********************* - -.[perex] -Посібники, рішення поширених завдань та *best practices* для Nette. - - -<div class=documentation> -<div> - - -Застосунки Nette ----------------- -- [Методи та атрибути inject |inject-method-attribute] -- [Складання презентерів з трейтів |presenter-traits] -- [Передача налаштувань до презентерів |passing-settings-to-presenters] -- [Як повернутися до попередньої сторінки |restore-request] -- [Пагінація результатів бази даних |pagination] -- [Динамічні сніпети |dynamic-snippets] -- [Як використовувати атрибут #Requires |attribute-requires] -- [Як правильно використовувати POST-посилання |post-links] - -</div> -<div> - - -Форми ------ -- [Повторне використання форм |form-reuse] -- [Форма для створення та редагування запису |creating-editing-form] -- [Створюємо контактну форму |lets-create-contact-form] -- [Залежні селектбокси |https://blog.nette.org/uk/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Загальне --------- -- [Як завантажити конфігураційний файл |bootstrap:] -- [Як писати мікро-сайти |microsites] -- [Чому Nette використовує PascalCase нотацію констант? |https://blog.nette.org/uk/for-less-screaming-in-the-code] -- [Чому Nette не використовує суфікс Interface? |https://blog.nette.org/uk/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: поради щодо використання |composer] -- [Поради щодо редакторів та інструментів |editors-and-tools] -- [Вступ до об'єктно-орієнтованого програмування |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Приклади рішень ---------------- -- [Nette examples |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Contributte examples |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Швидкий старт |quickstart:] - -</div> -<div> - - -Відео ------ -Сотні записів з Posledních sobot та відео про Nette ви знайдете під одним дахом на "Youtube каналі Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/uk/@meta.texy b/best-practices/uk/@meta.texy deleted file mode 100644 index 5ad8bb9a6b..0000000000 --- a/best-practices/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Посібники та практики}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/uk/attribute-requires.texy b/best-practices/uk/attribute-requires.texy deleted file mode 100644 index f0d8d4a2d1..0000000000 --- a/best-practices/uk/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Як використовувати атрибут `#[Requires]` -**************************************** - -.[perex] -Під час написання веб-додатку часто виникає потреба обмежити доступ до певних частин вашого додатку. Можливо, ви хочете, щоб деякі запити могли надсилати дані лише за допомогою форми (тобто методом POST), або щоб вони були доступні лише для AJAX-викликів. У Nette Framework 3.2 з'явився новий інструмент, який дозволяє встановити такі обмеження дуже елегантно та зрозуміло: атрибут `#[Requires]`. - -Атрибут — це спеціальна позначка в PHP, яку ви додаєте перед визначенням класу або методу. Оскільки це фактично клас, щоб наступні приклади працювали, необхідно вказати оператор use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Атрибут `#[Requires]` можна використовувати для самого класу presenter'а, а також для таких методів: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Останні два методи стосуються також компонентів, отже, атрибут можна використовувати і для них. - -Якщо умови, зазначені в атрибуті, не виконані, буде викликано HTTP-помилку 4xx. - - -Методи HTTP ------------ - -Ви можете вказати, які HTTP-методи (наприклад, GET, POST тощо) дозволені для доступу. Наприклад, якщо ви хочете дозволити доступ лише шляхом надсилання форми, встановіть: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Чому слід використовувати POST замість GET для дій, що змінюють стан, і як це зробити? [Прочитайте інструкцію |post-links]. - -Ви можете вказати метод або масив методів. Особливим випадком є значення `'*'`, яке дозволяє всі методи, що зазвичай presenter'и [з міркувань безпеки не дозволяють |application:presenters#Перевірка HTTP-методу]. - - -AJAX-виклики ------------- - -Якщо ви хочете, щоб presenter або метод був доступний лише для AJAX-запитів, використовуйте: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Те саме походження ------------------- - -Для підвищення безпеки ви можете вимагати, щоб запит надходив з того самого домену. Це запобігає [вразливості CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Для методів `handle<Signal>()` доступ з того самого домену вимагається автоматично. Тому, якщо ви, навпаки, хочете дозволити доступ з будь-якого домену, вкажіть: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Доступ через forward --------------------- - -Іноді корисно обмежити доступ до presenter'а так, щоб він був доступний лише опосередковано, наприклад, за допомогою методу `forward()` або `switch()` з іншого presenter'а. Таким чином, наприклад, захищаються error-presenter'и, щоб їх не можна було викликати з URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -На практиці часто буває необхідно позначити певні views, до яких можна отримати доступ лише на основі логіки в presenter'і. Тобто, знову ж таки, щоб їх не можна було відкрити безпосередньо: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Конкретні дії -------------- - -Ви також можете обмежити, щоб певний код, наприклад, створення компонента, був доступний лише для специфічних дій у presenter'і: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -У випадку однієї дії не потрібно записувати масив: `#[Requires(actions: 'default')]` - - -Власні атрибути ---------------- - -Якщо ви хочете використовувати атрибут `#[Requires]` повторно з тими самими налаштуваннями, ви можете створити власний атрибут, який успадковуватиме `#[Requires]` і налаштує його відповідно до потреб. - -Наприклад, `#[SingleAction]` дозволить доступ лише через дію `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Або `#[RestMethods]` дозволить доступ через усі HTTP-методи, що використовуються для REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Висновок --------- - -Атрибут `#[Requires]` надає вам велику гнучкість і контроль над тим, як доступні ваші веб-сторінки. За допомогою простих, але потужних правил ви можете підвищити безпеку та правильне функціонування вашого додатку. Як бачите, використання атрибутів у Nette може не тільки полегшити вашу роботу, але й зробити її безпечнішою. diff --git a/best-practices/uk/composer.texy b/best-practices/uk/composer.texy deleted file mode 100644 index a237caf401..0000000000 --- a/best-practices/uk/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: поради щодо використання -********************************** - -<div class=perex> - -Composer — це інструмент для керування залежностями в PHP. Він дозволяє нам перерахувати бібліотеки, від яких залежить наш проект, і буде встановлювати та оновлювати їх за нас. Ми покажемо: - -- як встановити Composer -- його використання в новому або існуючому проекті - -</div> - - -Встановлення -============ - -Composer — це виконуваний файл `.phar`, який ви завантажуєте та встановлюєте наступним чином: - - -Windows -------- - -Використовуйте офіційний інсталятор [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Достатньо 4 команд, які ви можете скопіювати з [цієї сторінки |https://getcomposer.org/download/]. - -Потім, розмістивши його в папці, яка знаходиться в системному `PATH`, Composer стане доступним глобально: - -```shell -$ mv ./composer.phar ~/bin/composer # або /usr/local/bin/composer -``` - - -Використання в проекті -====================== - -Щоб почати використовувати Composer у своєму проекті, вам потрібен лише файл `composer.json`. Він описує залежності вашого проекту і може також містити інші метадані. Базовий `composer.json` може виглядати так: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Тут ми вказуємо, що наш додаток (або бібліотека) вимагає пакет `nette/database` (назва пакета складається з назви організації та назви проекту) і хоче версію, яка відповідає умові `^3.0` (тобто найновішу версію 3). - -Отже, у нас є файл `composer.json` у корені проекту, і ми запускаємо встановлення: - -```shell -composer update -``` - -Composer завантажить Nette Database у папку `vendor/`. Потім він створить файл `composer.lock`, який містить інформацію про те, які саме версії бібліотек він встановив. - -Composer згенерує файл `vendor/autoload.php`, який ми можемо просто підключити і почати використовувати бібліотеки без будь-якої додаткової роботи: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Оновлення пакетів до останніх версій -==================================== - -Оновлення використовуваних бібліотек до останніх версій відповідно до умов, визначених у `composer.json`, здійснюється командою `composer update`. Наприклад, для залежності `"nette/database": "^3.0"` буде встановлена остання версія 3.x.x, але не версія 4. - -Для оновлення умов у файлі `composer.json`, наприклад, до `"nette/database": "^4.1"`, щоб можна було встановити останню версію, використовуйте команду `composer require nette/database`. - -Для оновлення всіх використовуваних пакетів Nette необхідно було б перерахувати їх усі в командному рядку, наприклад: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Що непрактично. Тому використовуйте простий скрипт "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, який зробить це за вас: - -```shell -php composer-frontline.php -``` - - -Створення нового проекту -======================== - -Новий проект на Nette створюється за допомогою однієї команди: - -```shell -composer create-project nette/web-project nazev-projektu -``` - -Як `nazev-projektu` введіть назву каталогу для свого проекту та підтвердіть. Composer завантажить репозиторій `nette/web-project` з GitHub, який вже містить файл `composer.json`, а потім одразу Nette Framework. Повинно бути достатньо лише [встановити права |nette:troubleshooting#Налаштування прав доступу до каталогів] на запис у папки `temp/` та `log/`, і проект має запрацювати. - -Якщо ви знаєте, на якій версії PHP буде розміщено проект, не забудьте [її встановити |#Версія PHP]. - - -Версія PHP -========== - -Composer завжди встановлює ті версії пакетів, які сумісні з версією PHP, яку ви зараз використовуєте (точніше, з версією PHP, що використовується в командному рядку під час запуску Composer). Однак це, швидше за все, не та сама версія, яку використовує ваш хостинг. Тому дуже важливо додати до файлу `composer.json` інформацію про версію PHP на хостингу. Після цього будуть встановлюватися лише ті версії пакетів, які сумісні з хостингом. - -Те, що проект працюватиме, наприклад, на PHP 8.2.3, встановлюється командою: - -```shell -composer config platform.php 8.2.3 -``` - -Таким чином версія запишеться у файл `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Однак номер версії PHP вказується ще в одному місці файлу, а саме в секції `require`. У той час як перше число визначає, для якої версії будуть встановлюватися пакети, друге число вказує, для якої версії написаний сам додаток. І за ним, наприклад, PhpStorm встановлює *PHP language level*. (Звичайно, немає сенсу, щоб ці версії відрізнялися, тому подвійний запис є недоліком.) Цю версію встановлюють командою: - -```shell -composer require php 8.2.3 --no-update -``` - -Або безпосередньо у файлі `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ігнорування версії PHP -====================== - -Пакети зазвичай вказують як найнижчу версію PHP, з якою вони сумісні, так і найвищу, з якою вони протестовані. Якщо ви збираєтеся використовувати ще новішу версію PHP, наприклад, для тестування, Composer відмовиться встановлювати такий пакет. Рішенням є опція `--ignore-platform-req=php+`, яка змусить Composer ігнорувати верхні межі необхідної версії PHP. - - -Хибні повідомлення -================== - -Під час оновлення пакетів або зміни номерів версій трапляється, що виникає конфлікт. Один пакет має вимоги, які суперечать іншому, і так далі. Однак Composer іноді видає хибні повідомлення. Він повідомляє про конфлікт, якого насправді не існує. У такому випадку допоможе видалити файл `composer.lock` і спробувати ще раз. - -Якщо повідомлення про помилку залишається, то воно серйозне, і потрібно з нього зрозуміти, що і як виправити. - - -Packagist.org - центральний репозиторій -======================================= - -[Packagist |https://packagist.org] — це головний репозиторій, у якому Composer намагається шукати пакети, якщо йому не вказано інше. Ми також можемо публікувати тут власні пакети. - - -Що робити, якщо ми не хочемо використовувати центральний репозиторій? ---------------------------------------------------------------------- - -Якщо у нас є внутрішньокорпоративні додатки, які ми просто не можемо розміщувати публічно, то ми створимо для них корпоративний репозиторій. - -Більше на тему репозиторіїв [в офіційній документації |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Автозавантаження -================ - -Ключовою особливістю Composer є те, що він забезпечує автозавантаження для всіх встановлених ним класів, яке ви запускаєте, підключивши файл `vendor/autoload.php`. - -Однак можна використовувати Composer і для завантаження інших класів поза папкою `vendor`. Перший варіант — дозволити Composer просканувати визначені папки та підпапки, знайти всі класи та включити їх до автозавантажувача. Цього можна досягти, налаштувавши `autoload > classmap` у `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # включить папку src/ та її підпапки - ] - } -} -``` - -Після цього необхідно при кожній зміні запускати команду `composer dumpautoload` і перегенерувати таблиці автозавантаження. Це надзвичайно незручно, і набагато краще доручити це завдання [RobotLoader|robot-loader:], який виконує ту саму дію автоматично у фоновому режимі та набагато швидше. - -Другий варіант — дотримуватися [PSR-4|https://www.php-fig.org/psr/psr-4/]. Спрощено кажучи, це система, де простори імен та назви класів відповідають структурі каталогів та назвам файлів, тобто, наприклад, `App\Core\RouterFactory` буде знаходитись у файлі `/path/to/App/Core/RouterFactory.php`. Приклад конфігурації: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # простір імен App\ знаходиться в каталозі app/ - } - } -} -``` - -Як саме налаштувати поведінку, ви дізнаєтеся в [документації Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Тестування нових версій -======================= - -Ви хочете протестувати нову розробницьку версію пакета. Як це зробити? Спочатку додайте до файлу `composer.json` цю пару опцій, яка дозволить встановлювати розробницькі версії пакетів, але вдасться до цього лише в тому випадку, якщо не існує жодної комбінації стабільних версій, яка б задовольняла вимогам: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Далі рекомендуємо видалити файл `composer.lock`, іноді Composer незрозуміло відмовляється від встановлення, і це вирішує проблему. - -Припустимо, йдеться про пакет `nette/utils`, і нова версія має номер 4.0. Встановіть її командою: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Або ви можете встановити конкретну версію, наприклад, 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Але якщо від бібліотеки залежить інший пакет, який заблокований на старішій версії (наприклад, `^3.1`), то ідеально оновити цей пакет, щоб він працював з новою версією. Якщо ж ви хочете просто обійти обмеження і змусити Composer встановити розробницьку версію, видаючи її за старішу (наприклад, 3.1.6), ви можете використати ключове слово `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Виклик команд -============= - -Через Composer можна викликати власні підготовлені команди та скрипти, ніби це нативні команди Composer. Для скриптів, що знаходяться в папці `vendor/bin`, не потрібно вказувати цю папку. - -Як приклад, визначимо в файлі `composer.json` скрипт, який за допомогою [Nette Tester|tester:] запустить тести: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Тести потім запустимо за допомогою `composer tester`. Команду можна викликати, навіть якщо ми не знаходимося в кореневій папці проекту, а в якомусь підкаталозі. - - -Надішліть подяку -================ - -Ми покажемо вам трюк, яким ви порадуєте авторів open source. Простим способом ви поставите зірочку на GitHub бібліотекам, які використовує ваш проект. Достатньо встановити бібліотеку `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -А потім запустити: - -```shell -composer thanks -``` - -Спробуйте! - - -Конфігурація -============ - -Composer тісно пов'язаний з інструментом версіонування [Git |https://git-scm.com]. Якщо він у вас не встановлений, потрібно сказати Composer, щоб він його не використовував: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/uk/creating-editing-form.texy b/best-practices/uk/creating-editing-form.texy deleted file mode 100644 index 415142d3ae..0000000000 --- a/best-practices/uk/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Форма для створення та редагування запису -***************************************** - -.[perex] -Як правильно реалізувати в Nette додавання та редагування запису, використовуючи для обох операцій одну й ту саму форму? - -У багатьох випадках форми для додавання та редагування запису однакові, відрізняючись, наприклад, лише написом на кнопці. Ми покажемо приклади простих презентерів, де форму спочатку використаємо для додавання запису, потім для редагування, і нарешті об'єднаємо обидва рішення. - - -Додавання запису ----------------- - -Приклад презентера, що служить для додавання запису. Саму роботу з базою даних залишимо класу `Facade`, код якого не є суттєвим для прикладу. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... додамо поля форми ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // додавання запису до бази даних - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Редагування запису ------------------- - -Тепер покажемо, як виглядав би презентер, що служить для редагування запису: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // перевірка існування запису - || !$this->facade->isEditAllowed(/*...*/) // перевірка прав доступу - ) { - $this->error(); // помилка 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // перевіримо, що дія є 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... додамо поля форми ... - - $form->setDefaults($this->record); // встановлення значень за замовчуванням - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // оновлення запису - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -У методі *action*, який запускається на самому початку [життєвого циклу презентера |application:presenters#Життєвий цикл презентера], ми перевіряємо існування запису та права користувача на його редагування. - -Запис ми зберігаємо у властивості `$record`, щоб мати до нього доступ у методі `createComponentRecordForm()` для встановлення значень за замовчуванням, та в `recordFormSucceeded()` для отримання ID. Альтернативним рішенням було б встановити значення за замовчуванням безпосередньо в `actionEdit()` та отримати значення ID, яке є частиною URL, за допомогою `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // перевірка існування та прав доступу - ) { - $this->error(); - } - - // встановлення значень за замовчуванням форми - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Однак, і це має бути **найважливішим висновком усього коду**, ми повинні при створенні форми переконатися, що дія дійсно є `edit`. Бо інакше перевірка в методі `actionEdit()` взагалі не відбудеться! - - -Однакова форма для додавання та редагування -------------------------------------------- - -А тепер об'єднаємо обидва презентери в один. Ми могли б у методі `createComponentRecordForm()` розрізняти, про яку дію йдеться, і відповідно конфігурувати форму, або ж можемо залишити це безпосередньо для action-методів і позбутися умови: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - public function actionAdd(): void - { - $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // перевірка існування запису - || !$this->facade->isEditAllowed(/*...*/) // перевірка прав доступу - ) { - $this->error(); // помилка 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // встановлення значень за замовчуванням - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // перевіримо, що дія є 'add' або 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... додамо поля форми ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // додавання запису до бази даних - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // оновлення запису - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/uk/dynamic-snippets.texy b/best-practices/uk/dynamic-snippets.texy deleted file mode 100644 index 3a8f9f5f63..0000000000 --- a/best-practices/uk/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Динамічні сніпети -***************** - -Досить часто під час розробки додатків виникає потреба виконувати AJAX-операції, наприклад, над окремими рядками таблиці або елементами списку. Для прикладу можемо взяти виведення статей, причому для кожної з них дозволимо зареєстрованому користувачеві вибрати оцінку "подобається/не подобається". Код презентера та відповідного шаблону без AJAX виглядатиме приблизно так (наводжу найважливіші фрагменти, код розраховує на існування сервісу для позначення оцінок та отримання колекції статей - конкретна реалізація не важлива для цілей цього посібника): - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - $this->redirect('this'); -} - -public function handleUnlike(int $articleId): void -{ - $this->ratingService->removeLike($articleId, $this->user->id); - $this->redirect('this'); -} -``` - -Шаблон: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>{* це мені подобається *}</a> - {else} - <a n:href="unlike! $article->id" class=ajax>{* мені це вже не подобається *}</a> - {/if} -</article> -``` - - -Аяксифікація -============ - -Тепер давайте оснастимо цей простий додаток AJAX. Зміна оцінки статті не настільки важлива, щоб вимагати перенаправлення, тому ідеально було б, щоб вона відбувалася за допомогою AJAX у фоновому режимі. Ми використаємо [скрипт обробки з доповнень |application:ajax#Naja] зі звичайною конвенцією, що AJAX-посилання мають CSS-клас `ajax`. - -Однак, як це зробити конкретно? Nette пропонує 2 шляхи: шлях так званих динамічних сніпетів та шлях компонентів. Обидва мають свої переваги та недоліки, тому ми розглянемо їх по черзі. - - -Шлях динамічних сніпетів -======================== - -Динамічний сніпет в термінології Latte означає специфічний випадок використання тегу `{snippet}`, коли в назві сніпета використовується змінна. Такий сніпет не може знаходитися будь-де в шаблоні - він повинен бути обгорнутий статичним сніпетом, тобто звичайним, або всередині `{snippetArea}`. Наш шаблон можна було б змінити наступним чином. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>{* це мені подобається *}</a> - {else} - <a n:href="unlike! $article->id" class=ajax>{* мені це вже не подобається *}</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Кожна стаття тепер визначає один сніпет, який має в назві ID статті. Всі ці сніпети потім разом обгорнуті одним сніпетом з назвою `articlesContainer`. Якби ми пропустили цей обгортаючий сніпет, Latte повідомить нас про це винятком. - -Залишається додати до презентера перемальовування - достатньо перемалювати статичну обгортку. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- не потрібно - } else { - $this->redirect('this'); - } -} -``` - -Аналогічно змінимо і сестринський метод `handleUnlike()`, і AJAX запрацює! - -Однак рішення має один недолік. Якщо ми детальніше дослідимо, як відбувається AJAX-запит, то виявимо, що хоча зовні додаток виглядає економним (повертає лише один єдиний сніпет для даної статті), насправді на сервері він відрендерив усі сніпети. Потрібний сніпет він помістив у payload, а решту відкинув (отже, також абсолютно марно отримав їх із бази даних). - -Щоб оптимізувати цей процес, нам доведеться втрутитися там, де ми передаємо колекцію `$articles` до шаблону (скажімо, в методі `renderDefault()`). Ми скористаємося тим фактом, що обробка сигналів відбувається перед методами `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Тепер при обробці сигналу до шаблону передається замість колекції з усіма статтями лише масив з єдиною статтею - тією, яку ми хочемо відрендерити та надіслати в payload до браузера. `{foreach}` таким чином пройде лише один раз, і жодних зайвих сніпетів не відрендериться. - - -Шлях компонентів -================ - -Абсолютно інший спосіб вирішення уникає динамічних сніпетів. Трюк полягає в перенесенні всієї логіки в окремий компонент - відтепер про введення оцінки дбатиме не презентер, а спеціалізований `LikeControl`. Клас виглядатиме наступним чином (крім того, він міститиме також методи `render`, `handleUnlike` тощо): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private Article $article, - ) { - } - - public function handleLike(): void - { - $this->ratingService->saveLike($this->article->id, $this->presenter->user->id); - if ($this->presenter->isAjax()) { - $this->redrawControl(); - } else { - $this->presenter->redirect('this'); - } - } -} -``` - -Шаблон компонента: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>{* це мені подобається *}</a> - {else} - <a n:href="unlike!" class=ajax>{* мені це вже не подобається *}</a> - {/if} -{/snippet} -``` - -Звичайно, шаблон view зміниться, і нам доведеться додати до презентера фабрику. Оскільки ми створимо компонент стільки разів, скільки статей отримаємо з бази даних, ми використаємо для його "розмноження" клас [application:Multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Шаблон view зменшиться до необхідного мінімуму (і повністю позбавиться сніпетів!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Майже готово: додаток тепер працюватиме за допомогою AJAX. Тут також нас чекає оптимізація додатку, оскільки через використання Nette Database при обробці сигналу марно завантажуються всі статті з бази даних замість однієї. Перевагою, однак, є те, що їх рендеринг не відбудеться, оскільки відрендериться дійсно лише наш компонент. - -{{priority: -1}} diff --git a/best-practices/uk/editors-and-tools.texy b/best-practices/uk/editors-and-tools.texy deleted file mode 100644 index 86380ff279..0000000000 --- a/best-practices/uk/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Редактори та інструменти -************************ - -.[perex] -Ви можете бути вправним програмістом, але лише з хорошими інструментами ви станете майстром. У цьому розділі ви знайдете поради щодо важливих інструментів, редакторів та плагінів. - - -IDE редактор -============ - -Ми наполегливо рекомендуємо використовувати для розробки повноцінне IDE, таке як PhpStorm, NetBeans, VS Code, а не просто текстовий редактор з підтримкою PHP. Різниця справді суттєва. Немає причин задовольнятися простим редактором, який хоч і вміє підсвічувати синтаксис, але не досягає можливостей топового IDE, яке точно підказує, відстежує помилки, вміє рефакторити код та багато іншого. Деякі IDE платні, інші навіть безкоштовні. - -**NetBeans IDE** має вбудовану підтримку Nette, Latte та NEON. - -**PhpStorm**: встановіть ці плагіни в `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: знайдіть у marketplace плагін "Nette Latte + Neon". - -Також зв'яжіть Tracy з редактором. При відображенні сторінки помилки можна буде клікнути на імена файлів, і вони відкриються в редакторі з курсором на відповідному рядку. Прочитайте, [як налаштувати систему|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan — це інструмент, який виявляє логічні помилки в коді ще до його запуску. - -Встановимо його за допомогою Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Створимо в проекті конфігураційний файл `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -А потім запустимо аналіз класів у папці `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Вичерпну документацію ви знайдете безпосередньо на [сайті PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] перевіряє та, за потреби, виправляє деякі формальні помилки у ваших вихідних кодах: - -- видаляє [BOM |nette:glossary#BOM] -- перевіряє валідність шаблонів [Latte |latte:] -- перевіряє валідність файлів `.neon`, `.php` та `.json` -- перевіряє наявність [контрольних символів |nette:glossary#Керуючі символи] -- перевіряє, чи файл закодований у UTF-8 -- перевіряє помилково записані `/* @anotace */` (відсутня зірочка) -- видаляє завершальний `?>` у PHP файлах -- видаляє пробіли в кінці рядків та зайві рядки в кінці файлу -- нормалізує роздільники рядків до системних (якщо вказати опцію `-l`) - - -Composer -======== - -[Composer | Composer] — це інструмент для керування залежностями в PHP. Він дозволяє нам декларувати довільно складні залежності окремих бібліотек, а потім встановлює їх для нас у наш проект. - - -Requirements Checker -==================== - -Це був інструмент, який тестував середовище виконання сервера та інформував, чи (і якою мірою) можна використовувати фреймворк. На даний момент Nette можна використовувати на будь-якому сервері, який має мінімально необхідну версію PHP. diff --git a/best-practices/uk/form-reuse.texy b/best-practices/uk/form-reuse.texy deleted file mode 100644 index d75d20642b..0000000000 --- a/best-practices/uk/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Повторне використання форм у кількох місцях -******************************************* - -.[perex] -У Nette у вас є кілька варіантів використання однієї й тієї ж форми в кількох місцях без дублювання коду. У цій статті ми розглянемо різні рішення, включно з тими, яких слід уникати. - - -Фабрика форм -============ - -Одним з основних підходів до використання одного й того ж компонента в кількох місцях є створення методу або класу, який генерує цей компонент, і подальше викликання цього методу в різних місцях програми. Такий метод або клас називається *фабрикою*. Будь ласка, не плутайте з патерном проектування *factory method*, який описує специфічний спосіб використання фабрик і не пов'язаний з цією темою. - -Як приклад, створимо фабрику, яка буде збирати форму редагування: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Тепер ви можете використовувати цю фабрику в різних місцях вашої програми, наприклад, у презентерах або компонентах. Це робиться шляхом [запрошення її як залежності|dependency-injection:passing-dependencies]. Спочатку запишемо клас у конфігураційний файл: - -```neon -services: - - FormFactory -``` - -А потім використаємо її в презентері: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // обробка надісланих даних - }; - return $form; - } -} -``` - -Фабрику форм можна розширити додатковими методами для створення інших типів форм відповідно до потреб вашої програми. І, звичайно, ми можемо додати метод, який створить базову форму без елементів, і цей метод будуть використовувати інші методи: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Метод `createForm()` поки що не робить нічого корисного, але це швидко зміниться. - - -Залежності фабрики -================== - -З часом виявиться, що нам потрібно, щоб форми були багатомовними. Це означає, що всім формам потрібно встановити так званий [translator |forms:rendering#Переклад]. Для цього ми змінимо клас `FormFactory` так, щоб він приймав об'єкт `Translator` як залежність у конструкторі, і передамо його формі: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Оскільки метод `createForm()` викликають і інші методи, що створюють специфічні форми, достатньо встановити translator лише в ньому. І все готово. Не потрібно змінювати код жодного презентера чи компонента, що чудово. - - -Кілька фабричних класів -======================= - -Альтернативно, ви можете створити кілька класів для кожної форми, яку хочете використовувати у вашій програмі. Цей підхід може підвищити читабельність коду та полегшити керування формами. Оригінальну `FormFactory` залишимо створювати лише чисту форму з базовою конфігурацією (наприклад, з підтримкою перекладів), а для форми редагування створимо нову фабрику `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ використання композиції -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Дуже важливо, щоб зв'язок між класами `FormFactory` та `EditFormFactory` був реалізований [композицією |nette:introduction-to-object-oriented-programming#Композиція], а не [об'єктною спадковістю |nette:introduction-to-object-oriented-programming#Успадкування]: - -```php -// ⛔ ТАК НЕ РОБИТИ! ТУТ СПАДКУВАННЯ НЕ ДО РЕЧІ -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Використання спадковості в цьому випадку було б абсолютно контрпродуктивним. Ви б дуже швидко зіткнулися з проблемами. Наприклад, коли б ви захотіли додати параметри до методу `create()`; PHP повідомив би про помилку, що його сигнатура відрізняється від батьківської. Або при передачі залежності до класу `EditFormFactory` через конструктор. Виникла б ситуація, яку ми називаємо [constructor hell |dependency-injection:passing-dependencies#Пекло конструкторів]. - -Загалом, краще надавати перевагу [композиції перед спадковістю |dependency-injection:faq#Чому композиції надається перевага перед успадкуванням]. - - -Обробка форми -============= - -Обробник форми, який викликається після успішного надсилання, також може бути частиною фабричного класу. Він працюватиме так, що передасть надіслані дані моделі для обробки. Можливі помилки [передасть назад |forms:validation#Помилки під час обробки] до форми. Модель у наступному прикладі представляє клас `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // обробка надісланих даних - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Однак саме перенаправлення ми залишимо на презентері. Він додасть до події `onSuccess` ще один обробник, який виконає перенаправлення. Завдяки цьому форму можна буде використовувати в різних презентерах і в кожному перенаправляти в інше місце. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Запис було збережено'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Це рішення використовує властивість форм, що коли над формою або її елементом викликається `addError()`, наступний обробник `onSuccess` вже не викликається. - - -Спадкування від класу Form -========================== - -Скомпонована форма не повинна бути нащадком форми. Іншими словами, не використовуйте це рішення: - -```php -// ⛔ ТАК НЕ РОБИТИ! ТУТ СПАДКУВАННЯ НЕ ДО РЕЧІ -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $this->addSubmit('send', 'Надіслати'); - $this->setTranslator($translator); - } -} -``` - -Замість того, щоб збирати форму в конструкторі, використовуйте фабрику. - -Потрібно усвідомити, що клас `Form` є насамперед інструментом для побудови форми, тобто *form builder*. А зібрану форму можна розглядати як її продукт. Однак продукт не є специфічним випадком білдера, між ними немає зв'язку *is a*, що лежить в основі спадковості. - - -Компонент з формою -================== - -Абсолютно інший підхід представляє створення [компонента|application:components], частиною якого є форма. Це дає нові можливості, наприклад, рендерити форму специфічним чином, оскільки частиною компонента є і шаблон. Або можна використовувати сигнали для AJAX-комунікації та дозавантаження інформації у форму, наприклад, для підказок тощо. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // обробка надісланих даних - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // виклик події - $this->onSave($this, $data); - } -} -``` - -Ще створимо фабрику, яка буде виробляти цей компонент. Достатньо [записати її інтерфейс |application:components#Компоненти із залежностями]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -І додати до конфігураційного файлу: - -```neon -services: - - EditControlFactory -``` - -А тепер вже можемо запросити фабрику та використати її в презентері: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // або перенаправляємо на результат редагування, напр.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/uk/inject-method-attribute.texy b/best-practices/uk/inject-method-attribute.texy deleted file mode 100644 index 8e64ef27ab..0000000000 --- a/best-practices/uk/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Методи та атрибути inject -************************* - -.[perex] -У цій статті ми розглянемо різні способи передачі залежностей у презентери у фреймворку Nette. Ми порівняємо бажаний спосіб, яким є конструктор, з іншими варіантами, такими як методи та атрибути `inject`. - -Навіть для презентерів передача залежностей за допомогою [конструктора |dependency-injection:passing-dependencies#Передача конструктором] є бажаним шляхом. Однак, якщо ви створюєте спільного предка, від якого успадковуються інші презентери (наприклад, `BasePresenter`), і цей предок також має залежності, виникає проблема, яку ми називаємо [constructor hell |dependency-injection:passing-dependencies#Пекло конструкторів]. Її можна обійти за допомогою альтернативних шляхів, якими є методи та атрибути (анотації) `inject`. - - -Методи `inject*()` -================== - -Це форма передачі залежності [сеттером |dependency-injection:passing-dependencies#Передача сеттером]. Назва цих сеттерів починається з префікса `inject`. Nette DI автоматично викликає методи з такою назвою одразу після створення екземпляра презентера та передає їм усі необхідні залежності. Тому вони повинні бути оголошені як public. - -Методи `inject*()` можна вважати своєрідним розширенням конструктора на кілька методів. Завдяки цьому `BasePresenter` може приймати залежності через інший метод і залишати конструктор вільним для своїх нащадків: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Презентер може містити будь-яку кількість методів `inject*()`, і кожен може мати будь-яку кількість параметрів. Це також чудово підходить у випадках, коли презентер [складається з трейтів |presenter-traits], і кожен з них вимагає власної залежності. - - -Атрибути `Inject` -================= - -Це форма [ін'єкції у властивість |dependency-injection:passing-dependencies#Встановленням змінної]. Достатньо позначити, в які змінні слід ін'єктувати, і Nette DI автоматично передасть залежності одразу після створення екземпляра презентера. Щоб їх можна було вставити, необхідно оголосити їх як public. - -Властивості позначимо атрибутом: (раніше використовувалася анотація `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // цей рядок важливий - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Перевагою цього способу передачі залежностей була дуже лаконічна форма запису. Однак з появою [constructor property promotion |https://blog.nette.org/uk/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] простіше використовувати конструктор. - -Навпаки, цей спосіб страждає тими ж недоліками, що й передача залежності у властивості загалом: ми не маємо контролю над змінами в змінній, і водночас змінна стає частиною публічного інтерфейсу класу, що є небажаним. diff --git a/best-practices/uk/lets-create-contact-form.texy b/best-practices/uk/lets-create-contact-form.texy deleted file mode 100644 index dda0437ab6..0000000000 --- a/best-practices/uk/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Створюємо контактну форму -************************* - -.[perex] -Розглянемо, як у Nette створити контактну форму, включно з надсиланням на електронну пошту. Отже, до справи! - -Спочатку потрібно створити новий проект. Як це зробити, пояснюється на сторінці [Починаємо |nette:installation]. А потім вже можемо почати створювати форму. - -Найпростіше створити [форму безпосередньо в презентері |forms:in-presenter]. Можемо використати заготовлений `HomePresenter`. До нього додамо компонент `contactForm`, що представляє форму. Зробимо це так: запишемо в код фабричний метод `createComponentContactForm()`, який створить компонент: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', "Ім'я:") - ->setRequired("Введіть ім'я"); - $form->addEmail('email', 'E-mail:') - ->setRequired('Введіть e-mail'); - $form->addTextarea('message', 'Повідомлення:') - ->setRequired('Введіть повідомлення'); - $form->addSubmit('send', 'Надіслати'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // надсилання email - } -} -``` - -Як бачите, ми створили два методи. Перший метод `createComponentContactForm()` створює нову форму. Вона має поля для імені, email та повідомлення, які ми додаємо методами `addText()`, `addEmail()` та `addTextArea()`. Також ми додали кнопку для надсилання форми. Але що, якщо користувач не заповнить якесь поле? У такому випадку ми повинні повідомити йому, що це обов'язкове поле. Цього ми досягли за допомогою методу `setRequired()`. Нарешті, ми також додали [подію |nette:glossary#Події události] `onSuccess`, яка спрацює, якщо форма успішно надіслана. У нашому випадку вона викличе метод `contactFormSucceeded`, який подбає про обробку надісланої форми. Це ми доповнимо в код за мить. - -Компонент `contactForm` виведемо в шаблоні `Home/default.latte`: - -```latte -{block content} -<h1>Контактна форма</h1> -{control contactForm} -``` - -Для самого надсилання email створимо новий клас, який назвемо `ContactFacade` і розмістимо його у файлі `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // ваш email - ->setFrom($email, $name) - ->setSubject('Повідомлення з контактної форми') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Метод `sendMessage()` створює та надсилає email. Для цього він використовує так званий mailer, який отримує як залежність через конструктор. Дізнайтеся більше про [надсилання електронних листів |mail:]. - -Тепер повернемося до презентера і завершимо метод `contactFormSucceeded()`. Він викличе метод `sendMessage()` класу `ContactFacade` і передасть йому дані з форми. А як отримати об'єкт `ContactFacade`? Отримаємо його через конструктор: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Повідомлення було надіслано'); - $this->redirect('this'); - } -} -``` - -Після надсилання email ми ще покажемо користувачеві так зване [flash-повідомлення |application:components#Flash-повідомлення], що підтверджує надсилання повідомлення, а потім перенаправимо на наступну сторінку, щоб не можна було повторно надіслати форму за допомогою *refresh* у браузері. - - -Отже, якщо все працює, ви повинні мати можливість надіслати email з вашої контактної форми. Вітаю! - - -HTML-шаблон електронного листа ------------------------------- - -Поки що надсилається простий текстовий email, що містить лише повідомлення, надіслане формою. Але в email ми можемо використовувати HTML і зробити його вигляд привабливішим. Створимо для нього шаблон у Latte, який запишемо до `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Повідомлення з контактної форми - - -

    Ім'я: {$name}

    -

    E-mail: {$email}

    -

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

    - - -``` - -Залишилося змінити `ContactFacade`, щоб він використовував цей шаблон. У конструкторі ми запросимо клас `LatteFactory`, який вміє створювати об'єкт `Latte\Engine`, тобто [рендер шаблонів Latte |latte:develop#Як відобразити шаблон]. За допомогою методу `renderToString()` ми відрендеримо шаблон у файл, першим параметром є шлях до шаблону, а другим – змінні. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // ваш email - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Згенерований HTML email потім передамо методу `setHtmlBody()` замість початкового `setBody()`. Також нам не потрібно вказувати тему email у `setSubject()`, оскільки бібліотека візьме її з елемента `` шаблону. - - -Конфігурація ------------- - -У коді класу `ContactFacade` все ще жорстко прописаний наш адміністраторський email `admin@example.com`. Було б краще перенести його до конфігураційного файлу. Як це зробити? - -Спочатку змінимо клас `ContactFacade` і рядок з email замінимо змінною, переданою конструктором: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -А другим кроком є вказівка значення цієї змінної в конфігурації. До файлу `app/config/services.neon` запишемо: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -І все. Якщо елементів у секції `services` буде багато і ви відчуватимете, що email серед них губиться, ми можемо зробити з нього змінну. Змінимо запис на: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -А у файлі `app/config/common.neon` визначимо цю змінну: - -```neon -parameters: - adminEmail: admin@example.com -``` - -І готово! diff --git a/best-practices/uk/microsites.texy b/best-practices/uk/microsites.texy deleted file mode 100644 index 19744c4b86..0000000000 --- a/best-practices/uk/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Як створювати мікросайти -************************ - -Уявіть, що вам потрібно швидко створити невеликий веб-сайт для майбутньої події вашої компанії. Це має бути просто, швидко і без зайвих ускладнень. Можливо, ви думаєте, що для такого маленького проекту вам не потрібен потужний фреймворк. Але що, якщо використання фреймворку Nette може суттєво спростити та прискорити цей процес? - -Адже навіть при створенні простих веб-сайтів ви не хочете відмовлятися від зручності. Ви не хочете вигадувати те, що вже було одного разу вирішено. Будьте спокійно лінивими і дозвольте себе побалувати. Nette Framework можна чудово використовувати і як мікрофреймворк. - -Як може виглядати такий мікросайт? Наприклад, так, що весь код сайту ми розмістимо в єдиному файлі `index.php` у публічній папці: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// створи DI-контейнер на основі конфігурації в config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// налаштуємо маршрутизацію -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// маршрут для URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // визначаємо мову браузера та перенаправляємо на URL /en або /de тощо. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// маршрут для URL https://example.com/cs або https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // відобразимо відповідний шаблон, наприклад ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// запустіть додаток! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Все інше будуть шаблони, збережені в батьківській папці `/templates`. - -PHP-код в `index.php` спочатку [підготує середовище |bootstrap:], потім визначає [маршрути |application:routing#Динамічна маршрутизація з callback-функціями] і нарешті запускає додаток. Перевагою є те, що другий параметр функції `addRoute()` може бути callable, який виконається після відкриття відповідної сторінки. - - -Чому варто використовувати Nette для мікросайтів? -------------------------------------------------- - -- Програмісти, які колись спробували [Tracy|tracy:], сьогодні не уявляють, як програмувати без неї. -- Перш за все, ви скористаєтеся системою шаблонів [Latte|latte:], оскільки вже з 2 сторінок вам захочеться мати розділений [макет та вміст|latte:template-inheritance]. -- І ви точно хочете покладатися на [автоматичне екранування |latte:safety-first], щоб не виникла вразливість XSS. -- Nette також гарантує, що при помилці ніколи не відобразяться повідомлення про помилки PHP для програмістів, а зрозуміла для користувача сторінка. -- Якщо ви хочете отримувати зворотній зв'язок від користувачів, наприклад, у вигляді контактної форми, то ще додасте [форми|forms:] та [базу даних|database:]. -- Заповнені форми ви також можете легко [надсилати електронною поштою|mail:]. -- Іноді вам може знадобитися [кешування|caching:], наприклад, якщо ви завантажуєте та відображаєте стрічки новин. - -У наш час, коли швидкість та ефективність є ключовими, важливо мати інструменти, які дозволять вам досягти результатів без зайвих затримок. Фреймворк Nette пропонує саме це - швидку розробку, безпеку та широкий спектр інструментів, таких як Tracy та Latte, які спрощують процес. Достатньо встановити кілька пакетів Nette, і створення такого мікросайту раптом стає зовсім простою справою. І ви знаєте, що ніде не ховається жодна дірка в безпеці. diff --git a/best-practices/uk/pagination.texy b/best-practices/uk/pagination.texy deleted file mode 100644 index e16f7e4ad4..0000000000 --- a/best-practices/uk/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Пагінація результатів бази даних -******************************** - -.[perex] -При створенні веб-додатків дуже часто виникає вимога обмежити кількість виведених елементів на сторінці. - -Почнемо зі стану, коли ми виводимо всі дані без пагінації. Для вибору даних з бази даних у нас є клас ArticleRepository, який, крім конструктора, містить метод `findPublishedArticles`, що повертає всі опубліковані статті, відсортовані за спаданням дати публікації. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -У презентері ми потім ін'єктуємо клас моделі, а в методі render запитуємо опубліковані статті, які передаємо до шаблону: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -У шаблоні `default.latte` ми потім подбаємо про виведення статей: - -```latte -{block content} -<h1>Статті</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Таким чином ми можемо вивести всі статті, що, однак, почне створювати проблеми, коли кількість статей зросте. У цей момент стане в нагоді реалізація механізму пагінації. - -Він забезпечить, що всі статті будуть розділені на кілька сторінок, і ми відобразимо лише статті однієї поточної сторінки. Загальну кількість сторінок та розподіл статей обчислить [utils:Paginator] сам, залежно від того, скільки статей у нас загалом і скільки статей на сторінку ми хочемо відобразити. - -На першому кроці ми змінимо метод для отримання статей у класі репозиторію так, щоб він міг повертати лише статті для однієї сторінки. Також додамо метод для визначення загальної кількості статей у базі даних, який нам знадобиться для налаштування Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC - LIMIT ? - OFFSET ?', - new \DateTime, $limit, $offset, - ); - } - - /** - * Повертає загальну кількість опублікованих статей - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Потім перейдемо до змін у презентері. У метод render ми будемо передавати номер поточної відображуваної сторінки. У випадку, якщо цей номер не буде частиною URL, встановимо значення за замовчуванням першої сторінки. - -Далі також розширимо метод render отриманням екземпляра Paginator, його налаштуванням та вибором правильних статей для відображення в шаблоні. HomePresenter після змін виглядатиме так: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // З'ясуємо загальну кількість опублікованих статей - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Створимо екземпляр Paginator і налаштуємо його - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // загальна кількість статей - $paginator->setItemsPerPage(10); // кількість елементів на сторінці - $paginator->setPage($page); // номер поточної сторінки - - // З бази даних витягнемо обмежену множину статей згідно з розрахунком Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // яку передамо до шаблону - $this->template->articles = $articles; - // а також сам Paginator для відображення опцій пагінації - $this->template->paginator = $paginator; - } -} -``` - -Шаблон тепер уже ітерує лише над статтями однієї сторінки, нам залишається додати посилання для пагінації: - -```latte -{block content} -<h1>Статті</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Перша</a> -  |  - <a n:href="default, $paginator->page-1">Попередня</a> -  |  - {/if} - - Сторінка {$paginator->getPage()} з {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Наступна</a> -  |  - <a n:href="default, $paginator->getPageCount()">Остання</a> - {/if} -</div> -``` - - -Таким чином ми доповнили сторінку можливістю пагінації за допомогою Paginator. У випадку, коли замість [Nette Database Core |database:sql-way] як шар бази даних використовується [Nette Database Explorer |database:explorer], ми можемо реалізувати пагінацію і без використання Paginator. Клас `Nette\Database\Table\Selection` містить метод [page |api:Nette\Database\Table\Selection::_page] з логікою пагінації, взятою з Paginator. - -Репозиторій при такому способі реалізації виглядатиме так: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -У презентері нам не потрібно створювати Paginator, замість нього ми використаємо метод класу `Selection`, який повертає репозиторій: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Витягнемо опубліковані статті - $articles = $this->articleRepository->findPublishedArticles(); - - // а до шаблону надішлемо лише їх частину, обмежену згідно з розрахунком методу page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // а також необхідні дані для відображення опцій пагінації - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Оскільки до шаблону ми тепер не надсилаємо Paginator, змінимо частину, що відображає посилання пагінації: - -```latte -{block content} -<h1>Статті</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Перша</a> -  |  - <a n:href="default, $page - 1">Попередня</a> -  |  - {/if} - - Сторінка {$page} з {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Наступна</a> -  |  - <a n:href="default, $lastPage">Остання</a> - {/if} -</div> -``` - -Таким чином ми реалізували механізм пагінації без використання Paginator. - -{{priority: -1}} diff --git a/best-practices/uk/passing-settings-to-presenters.texy b/best-practices/uk/passing-settings-to-presenters.texy deleted file mode 100644 index eab473dbda..0000000000 --- a/best-practices/uk/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Передача налаштувань у презентери -********************************* - -.[perex] -Вам потрібно передавати в презентери аргументи, які не є об'єктами (наприклад, інформацію про те, чи працює додаток у режимі налагодження, шляхи до каталогів тощо), і тому їх не можна передати автоматично за допомогою autowiring? Рішенням є інкапсуляція їх в об'єкт `Settings`. - -Сервіс `Settings` представляє дуже простий, але корисний спосіб надання інформації про запущений додаток презентерам. Його конкретна форма залежить виключно від ваших конкретних потреб. Приклад: - -```php -namespace App; - -class Settings -{ - public function __construct( - // від PHP 8.1 можна вказати readonly - public bool $debugMode, - public string $appDir, - // і так далі - ) {} -} -``` - -Приклад реєстрації в конфігурації: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Коли презентеру знадобиться інформація, що надається цим сервісом, він просто запросить її в конструкторі: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/uk/post-links.texy b/best-practices/uk/post-links.texy deleted file mode 100644 index d0c67a0506..0000000000 --- a/best-practices/uk/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Як правильно використовувати POST-посилання -******************************************* - -.[perex] -У веб-додатках, особливо в адміністративних інтерфейсах, основним правилом має бути те, що дії, які змінюють стан сервера, не повинні виконуватися за допомогою HTTP-методу GET. Як випливає з назви методу, GET повинен використовуватися лише для отримання даних, а не для їх зміни. Для дій, таких як видалення записів, краще використовувати метод POST. Хоча ідеальним був би метод DELETE, але його не можна викликати без JavaScript, тому історично використовується POST. - -Як це зробити на практиці? Використовуйте цей простий трюк. На початку шаблону створіть допоміжну форму з ідентифікатором `postForm`, яку потім використовуйте для кнопок видалення: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Завдяки цій формі ви можете замість класичного посилання `<a>` використовувати кнопку `<button>`, яку можна візуально стилізувати так, щоб вона виглядала як звичайне посилання. Наприклад, CSS-фреймворк Bootstrap пропонує класи `btn btn-link`, за допомогою яких ви досягнете того, що кнопка не буде візуально відрізнятися від інших посилань. За допомогою атрибута `form="postForm"` ми пов'яжемо її з підготовленою формою: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">видалити</button> - <!-- замість <a n:href="delete $post->id">видалити</a> --> - </td> - </tr> -</table> -``` - -При натисканні на посилання тепер викликається дія `delete`. Щоб гарантувати, що запити будуть прийматися лише за допомогою методу POST і з того ж домену (що є ефективним захистом від CSRF-атак), використовуйте атрибут `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // гіпотетичний код, що видаляє запис - $this->redirect('default'); - } -} -``` - -Атрибут існує з Nette Application 3.2, і більше про його можливості ви дізнаєтеся на сторінці [Як використовувати атрибут #Requires |attribute-requires]. - -Якби ви замість дії `actionDelete()` використовували сигнал `handleDelete()`, не потрібно вказувати `sameOrigin: true`, оскільки сигнали мають цей захист встановлений неявно: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Цей підхід не тільки покращує безпеку вашого додатку, але й сприяє дотриманню правильних веб-стандартів та практик. Використовуючи методи POST для дій, що змінюють стан, ви досягнете більш надійного та безпечного додатку. diff --git a/best-practices/uk/presenter-traits.texy b/best-practices/uk/presenter-traits.texy deleted file mode 100644 index 3b4ce8a1cb..0000000000 --- a/best-practices/uk/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Компонування презентерів із трейтів -*********************************** - -.[perex] -Якщо нам потрібно реалізувати однаковий код у кількох презентерах (наприклад, перевірка, чи користувач увійшов у систему), пропонується розмістити код у спільному предку. Другим варіантом є створення одноцільових [трейтів |nette:introduction-to-object-oriented-programming#Трейди]. - -Перевага цього рішення полягає в тому, що кожен з презентерів може використовувати саме ті трейти, які йому дійсно потрібні, тоді як множинне успадкування в PHP неможливе. - -Ці трейти можуть використовувати той факт, що при створенні презентера послідовно викликаються всі [inject-методи |inject-method-attribute#Методи inject]. Потрібно лише переконатися, що назва кожного inject-методу є унікальною. - -Трейт може навішувати ініціалізаційний код на події [onStartup або onRender |application:presenters#Події]. - -Приклади: - -```php -trait RequireLoggedUser -{ - public function injectRequireLoggedUser(): void - { - $this->onStartup[] = function () { - if (!$this->getUser()->isLoggedIn()) { - $this->redirect('Sign:in', $this->storeRequest()); - } - }; - } -} - -trait StandardTemplateFilters -{ - public function injectStandardTemplateFilters(TemplateBuilder $builder): void - { - $this->onRender[] = function () use ($builder) { - $builder->setupTemplate($this->template); - }; - } -} -``` - -Презентер потім просто використовує ці трейти: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/uk/restore-request.texy b/best-practices/uk/restore-request.texy deleted file mode 100644 index 63446e2b95..0000000000 --- a/best-practices/uk/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Як повернутися на попередню сторінку? -************************************* - -.[perex] -Що робити, якщо користувач заповнює форму, а його сесія закінчується? Щоб дані не були втрачені, перед перенаправленням на сторінку входу ми збережемо дані в сесії. У Nette це зовсім просто. - -Поточний запит можна зберегти в сесії за допомогою методу `storeRequest()`, який поверне його ідентифікатор у вигляді короткого рядка. Метод зберігає назву поточного презентера, view та його параметри. У випадку, якщо була надіслана форма, також зберігається вміст полів (за винятком завантажених файлів). - -Відновлення запиту виконує метод `restoreRequest($key)`, якому ми передаємо отриманий ідентифікатор. Він перенаправляє на початковий презентер та view. Однак, якщо збережений запит містить надсилання форми, на початковий презентер він перейде методом `forward()`, передасть формі раніше заповнені значення і дозволить її знову відрендерити. Таким чином, користувач має можливість повторно надіслати форму, і жодні дані не втрачаються. - -Важливо, що `restoreRequest()` перевіряє, чи новозареєстрований користувач є тим самим, хто спочатку заповнював форму. Якщо ні, запит відкидається, і нічого не відбувається. - -Покажемо все на прикладі. Маємо презентер `AdminPresenter`, в якому редагуються дані і в методі `startup()` якого перевіряється, чи користувач увійшов у систему. Якщо ні, перенаправляємо його на `SignPresenter`. Водночас зберігаємо поточний запит і його ключ надсилаємо до `SignPresenter`. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -Презентер `SignPresenter` міститиме, крім форми для входу, також персистентний параметр `$backlink`, до якого запишеться ключ. Оскільки параметр є персистентним, він передаватиметься і після надсилання форми входу. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... додамо поля форми ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... тут користувача авторизуємо ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Методу `restoreRequest()` ми передаємо ключ збереженого запиту, і він перенаправляє (або переходить) на початковий презентер. - -Однак, якщо ключ недійсний (наприклад, вже не існує в сесії), метод нічого не робить. Тому далі йде виклик `$this->redirect('Admin:')`, який перенаправляє на `AdminPresenter`. - -{{priority: -1}} diff --git a/bootstrap/bg/@home.texy b/bootstrap/bg/@home.texy deleted file mode 100644 index 3f9b20f4a1..0000000000 --- a/bootstrap/bg/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Настройваме отделните компоненти на Nette с помощта на конфигурационни файлове. Ще ви покажем как да зареждате тези файлове. - -.[tip] -Ако използвате целия framework, не е необходимо да правите нищо повече. В проекта имате подготвена директория `config/` за конфигурационните файлове и зареждането им се управлява от [зареждащото устройство на приложението |application:bootstrapping#Конфигурация на DI контейнера]. Тази статия е за потребители, които използват само една библиотека на Nette и искат да използват възможностите на конфигурационните файлове. - -Конфигурационните файлове обикновено се записват във [формат NEON|neon:format] и най-добре се редактират в [редактори с неговата поддръжка |best-practices:editors-and-tools#IDE редактор]. Могат да се разглеждат като ръководства за **създаване и конфигуриране** на обекти. Следователно, резултатът от зареждането на конфигурацията ще бъде така наречената фабрика, която е обект, който по заявка ще ни създаде други обекти, които искаме да използваме. Например връзка с база данни и т.н. - -Тази фабрика се нарича още *dependency injection контейнер* (DI container) и ако се интересувате от подробности, прочетете главата за [dependency injection |dependency-injection:]. - -Зареждането на конфигурацията и създаването на контейнера се извършва от класа [api:Nette\Bootstrap\Configurator], така че първо ще инсталираме неговия пакет `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -И създаваме инстанция на класа `Configurator`. Тъй като генерираният DI контейнер ще се кешира на диска, е необходимо да се зададе пътят до директорията, където ще се съхранява: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -В Linux или macOS задайте на директорията `temp/` [права за запис |nette:troubleshooting#Настройка на правата на директориите]. - -И стигаме до самите конфигурационни файлове. Зареждаме ги с помощта на `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Ако искаме да добавим повече конфигурационни файлове, можем да извикаме функцията `addConfig()` няколко пъти. Ако във файловете се появят елементи със същите ключове, те ще бъдат презаписани (или в случай на масиви [обединени |dependency-injection:configuration#Сливане]). По-късно вмъкнатият файл има по-висок приоритет от предишния. - -Последната стъпка е създаването на DI контейнера: - -```php -$container = $configurator->createContainer(); -``` - -И той вече ще ни създаде желаните обекти. Ако например използвате конфигурация за [Nette Database|database:configuration], можете да го помолите да създаде връзки с базата данни: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// или -$explorer = $container->getByType(Nette\Database\Explorer::class); -// или при създаване на повече връзки -$db = $container->getByName('database.main.connection'); -``` - -И сега вече можете да работите с базата данни! - - -Режим на разработка срещу производствен режим ---------------------------------------------- - -В режим на разработка контейнерът се актуализира автоматично при всяка промяна на конфигурационните файлове. В производствен режим се генерира само веднъж и промените не се проверяват. Режимът на разработка е насочен към максимално удобство на програмиста, докато производственият режим е насочен към производителност и реално внедряване. - -Изборът на режим се извършва чрез автоматично откриване, така че обикновено не е необходимо да конфигурирате или превключвате ръчно. Режимът е разработващ, ако приложението се изпълнява на localhost (т.е. IP адрес `127.0.0.1` или `::1`) и няма налично прокси (т.е. негов HTTP хедър). В противен случай работи в производствен режим. - -Ако искаме да разрешим режима на разработка и в други случаи, например за програмисти, достъпващи от конкретен IP адрес, използваме `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// може да се зададе и масив от IP адреси -``` - -Определено препоръчваме да комбинирате IP адрес с cookie. В cookie `nette-debug` съхраняваме таен токен, например `secret1234`, и по този начин активираме режима на разработка за програмисти, достъпващи от конкретен IP адрес и едновременно имащи споменатия токен в cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Можем също така да изключим напълно режима на разработка, дори за localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Параметри ---------- - -В конфигурационните файлове можете да използвате и параметри, които се дефинират [в секцията `parameters` |dependency-injection:configuration#Параметри]. - -Те могат да бъдат вмъкнати и отвън с помощта на метода `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Параметърът `projectId` може да бъде рефериран в конфигурацията чрез запис `%projectId%`. diff --git a/bootstrap/bg/@meta.texy b/bootstrap/bg/@meta.texy deleted file mode 100644 index 794cbc8522..0000000000 --- a/bootstrap/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/cs/@home.texy b/bootstrap/cs/@home.texy index 9e3f2acaa6..1265c7c451 100644 --- a/bootstrap/cs/@home.texy +++ b/bootstrap/cs/@home.texy @@ -5,9 +5,9 @@ Nette Bootstrap Jednotlivé součásti Nette nastavujeme pomocí konfiguračních souborů. Ukážeme si, jak tyto soubory načítat. .[tip] -Pokud používate celý framework, není potřeba nic dalšího dělat. V projektu máte pro konfigurační soubory předpřipravený adresář `config/` a jejich načítání má na starosti [zavaděč aplikace |application: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ů. +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 |best-practices:editors-and-tools#IDE editor]. Lze je chápat jako návody, jak **vytvářet a konfigurovat** objekty. Tedy výsledkem načtení konfigurace bude tzv. továrna, což je objekt, který nám na požádání vytvoří další objekty, které chceme používat. Například databázové spojení apod. +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:]. @@ -58,7 +58,7 @@ Vývojářský vs produkční režim Ve vývojářském režimu se kontejner automaticky aktualizuje při každé změně konfiguračních souborů. V produkčním režimu se vygeneruje jen jednou a změny se nekontrolují. Vývojářský je tedy zaměřen na maximální pohodlí programátora, produkční na výkon a ostré nasazení. -Volba režimu se provádí autodetekcí, takže obvykle není potřeba nic konfigurovat nebo ručně přepínat. Režim je vývojářský tehdy, pokud je aplikace spuštěna na localhostu (tj. IP adresa `127.0.0.1` nebo `::1`) a není přitomna proxy (tj. její HTTP hlavička). Jinak běží v produkčním režimu. +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()`: @@ -83,7 +83,7 @@ $configurator->setDebugMode(false); Parametry --------- -V konfiguračním souborech můžete používat také parametry, které se definují [v sekci `parameters` |dependency-injection:configuration#Parametry]. +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()`: @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -Na parametr `projectId` se lze v konfiguraci odkázat zápisem `%projectId%`. +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]. diff --git a/bootstrap/cs/@left-menu.texy b/bootstrap/cs/@left-menu.texy new file mode 100644 index 0000000000..636e2ded56 --- /dev/null +++ b/bootstrap/cs/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 08edde785b..462d9add80 100644 --- a/bootstrap/cs/@meta.texy +++ b/bootstrap/cs/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokumentace}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/cs/upgrading.texy b/bootstrap/cs/upgrading.texy new file mode 100644 index 0000000000..240cf0e828 --- /dev/null +++ b/bootstrap/cs/upgrading.texy @@ -0,0 +1,18 @@ +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` diff --git a/bootstrap/de/@home.texy b/bootstrap/de/@home.texy index 8f49650c62..4ee3487d2a 100644 --- a/bootstrap/de/@home.texy +++ b/bootstrap/de/@home.texy @@ -2,78 +2,78 @@ Nette Bootstrap *************** .[perex] -Einzelne Nette-Komponenten werden über Konfigurationsdateien eingerichtet. Wir zeigen Ihnen, wie Sie diese Dateien laden. +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, müssen Sie nichts weiter tun. In Ihrem Projekt gibt es ein vorbereitetes Verzeichnis `config/` für Konfigurationsdateien, und deren Laden wird vom [Anwendungs-Bootstrap |application:bootstrapping#Konfiguration des DI-Containers] übernommen. Dieser Artikel richtet sich an Benutzer, die nur eine Nette-Bibliothek verwenden und die Möglichkeiten der Konfigurationsdateien nutzen möchten. +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 normalerweise im [NEON-Format|neon:format] geschrieben und am besten in [Editoren mit NEON-Unterstützung |best-practices:editors-and-tools#IDE-Editor] bearbeitet. Sie können als Anleitungen zum **Erstellen und Konfigurieren** von Objekten verstanden werden. Das Ergebnis des Ladens der Konfiguration ist also eine sogenannte Factory, ein Objekt, das auf Anfrage weitere Objekte erstellt, die wir verwenden möchten. Zum Beispiel eine Datenbankverbindung usw. +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. -Dieser Factory wird auch *Dependency Injection Container* (DI-Container) genannt. Wenn Sie an Details interessiert sind, lesen Sie das Kapitel über [Dependency Injection |dependency-injection:]. +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 Erstellen des Containers übernimmt die Klasse [api:Nette\Bootstrap\Configurator]. Installieren wir also zuerst ihr Paket `nette/bootstrap`: +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 wir erstellen eine Instanz der Klasse `Configurator`. Da der generierte DI-Container auf der Festplatte zwischengespeichert wird, ist es notwendig, den Pfad zum Verzeichnis festzulegen, in dem er gespeichert werden soll: +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'); ``` -Unter Linux oder macOS setzen Sie für das Verzeichnis `temp/` [Schreibberechtigungen |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. +Setzen Sie unter Linux oder macOS für das Verzeichnis `temp/` die [Schreibrechte |nette:troubleshooting#Einstellung der Verzeichnisberechtigungen]. -Und wir kommen zu den Konfigurationsdateien selbst. Wir laden sie mit `addConfig()`: +Nun kommen wir zu den Konfigurationsdateien selbst. Wir laden sie mit `addConfig()`: ```php $configurator->addConfig(__DIR__ . '/database.neon'); ``` -Wenn wir mehrere Konfigurationsdateien hinzufügen möchten, können wir die Funktion `addConfig()` mehrmals aufrufen. Wenn in den Dateien Elemente mit denselben Schlüsseln vorkommen, werden sie überschrieben (oder im Falle von Arrays [zusammengeführt |dependency-injection:configuration#Zusammenführen]). Eine später eingefügte Datei hat eine höhere Priorität als die vorherige. +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 die Erstellung des DI-Containers: +Der letzte Schritt ist das Erzeugen des DI-Containers: ```php $container = $configurator->createContainer(); ``` -Und dieser erstellt uns dann die gewünschten Objekte. Wenn Sie beispielsweise die Konfiguration für [Nette Database|database:configuration] verwenden, können Sie ihn bitten, Datenbankverbindungen zu erstellen: +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 Erstellen mehrerer Verbindungen +// oder beim Erzeugen mehrerer Verbindungen $db = $container->getByName('database.main.connection'); ``` -Und jetzt können Sie mit der Datenbank arbeiten! +Und schon können Sie mit der Datenbank arbeiten! Entwicklungs- vs. Produktionsmodus ---------------------------------- -Im Entwicklungsmodus wird der Container bei jeder Änderung der Konfigurationsdateien automatisch aktualisiert. Im Produktionsmodus wird er nur einmal generiert, und Änderungen werden nicht überprüft. Der Entwicklungsmodus ist also auf maximalen Komfort für den Programmierer ausgerichtet, der Produktionsmodus auf Leistung und den Live-Einsatz. +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 Modusauswahl erfolgt durch Autoerkennung, sodass normalerweise keine Konfiguration oder manuelles Umschalten erforderlich ist. Der Modus ist der Entwicklungsmodus, wenn die Anwendung auf Localhost (d.h. IP-Adresse `127.0.0.1` oder `::1`) ausgeführt wird und kein Proxy vorhanden ist (d.h. dessen HTTP-Header). Andernfalls läuft sie im Produktionsmodus. +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. -Wenn wir den Entwicklungsmodus auch in anderen Fällen aktivieren möchten, zum Beispiel für Programmierer, die von einer bestimmten IP-Adresse zugreifen, verwenden wir `setDebugMode()`: +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 kann auch ein Array von IP-Adressen angegeben werden +// es lässt sich auch ein Array von IP-Adressen angeben ``` -Wir empfehlen dringend, die IP-Adresse mit einem Cookie zu kombinieren. Wir speichern ein geheimes Token, z.B. `secret1234`, im Cookie `nette-debug` und aktivieren auf diese Weise den Entwicklungsmodus für Programmierer, die von einer bestimmten IP-Adresse zugreifen und gleichzeitig das erwähnte Token im Cookie haben: +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 deaktivieren, sogar für Localhost: +Wir können den Entwicklungsmodus auch vollständig abschalten, sogar für localhost: ```php $configurator->setDebugMode(false); @@ -83,9 +83,9 @@ $configurator->setDebugMode(false); Parameter --------- -In Konfigurationsdateien können Sie auch Parameter verwenden, die [im Abschnitt `parameters` |dependency-injection:configuration#Parameter] definiert sind. +In den Konfigurationsdateien können Sie auch Parameter verwenden, die [im Abschnitt `parameters` |dependency-injection:configuration#Parameter] definiert werden. -Sie können auch von außen über die Methode `addDynamicParameters()` eingefügt werden: +Sie lassen sich auch von außen einfügen, und zwar mit der Methode `addDynamicParameters()`: ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -Auf den Parameter `projectId` kann in der Konfiguration mit der Notation `%projectId%` verwiesen werden. +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 new file mode 100644 index 0000000000..800f23ec5f --- /dev/null +++ b/bootstrap/de/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 2cf383a5cf..b3b806b2ca 100644 --- a/bootstrap/de/@meta.texy +++ b/bootstrap/de/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokumentation}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/de/upgrading.texy b/bootstrap/de/upgrading.texy new file mode 100644 index 0000000000..286f6b6e03 --- /dev/null +++ b/bootstrap/de/upgrading.texy @@ -0,0 +1,18 @@ +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/el/@home.texy b/bootstrap/el/@home.texy deleted file mode 100644 index 8b4cf46446..0000000000 --- a/bootstrap/el/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Τα διάφορα μέρη του Nette διαμορφώνονται χρησιμοποιώντας αρχεία διαμόρφωσης. Θα δείξουμε πώς να φορτώνετε αυτά τα αρχεία. - -.[tip] -Αν χρησιμοποιείτε ολόκληρο το framework, δεν χρειάζεται να κάνετε τίποτα άλλο. Στο έργο σας, έχετε έναν προετοιμασμένο κατάλογο `config/` για αρχεία διαμόρφωσης, και η φόρτωσή τους αναλαμβάνεται από τον [φορτωτή της εφαρμογής |application:bootstrapping#Διαμόρφωση του DI Container]. Αυτό το άρθρο είναι για χρήστες που χρησιμοποιούν μόνο μία βιβλιοθήκη Nette και θέλουν να εκμεταλλευτούν τις δυνατότητες των αρχείων διαμόρφωσης. - -Τα αρχεία διαμόρφωσης συνήθως γράφονται σε [μορφή NEON |neon:format] και επεξεργάζονται καλύτερα σε [editors με υποστήριξη για αυτό |best-practices:editors-and-tools#IDE editor]. Μπορούν να θεωρηθούν ως οδηγίες για το πώς να **δημιουργείτε και να διαμορφώνετε** αντικείμενα. Έτσι, το αποτέλεσμα της φόρτωσης της διαμόρφωσης θα είναι ένα λεγόμενο factory, το οποίο είναι ένα αντικείμενο που, κατόπιν αιτήματος, θα δημιουργήσει άλλα αντικείμενα που θέλουμε να χρησιμοποιήσουμε. Για παράδειγμα, συνδέσεις βάσης δεδομένων κ.λπ. - -Αυτό το factory ονομάζεται επίσης *dependency injection container* (DI container) και αν σας ενδιαφέρουν οι λεπτομέρειες, διαβάστε το κεφάλαιο για το [dependency injection |dependency-injection:]. - -Η φόρτωση της διαμόρφωσης και η δημιουργία του container αναλαμβάνεται από την κλάση [api:Nette\Bootstrap\Configurator], οπότε πρώτα θα εγκαταστήσουμε το πακέτο της `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -Και θα δημιουργήσουμε ένα στιγμιότυπο της κλάσης `Configurator`. Επειδή ο παραγόμενος DI container θα αποθηκευτεί προσωρινά στον δίσκο, είναι απαραίτητο να ορίσουμε τη διαδρομή προς τον κατάλογο όπου θα αποθηκευτεί: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Σε Linux ή macOS, ορίστε [δικαιώματα εγγραφής |nette:troubleshooting#Ρύθμιση δικαιωμάτων καταλόγου] στον κατάλογο `temp/`. - -Και φτάνουμε στα ίδια τα αρχεία διαμόρφωσης. Τα φορτώνουμε χρησιμοποιώντας το `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Αν θέλουμε να προσθέσουμε περισσότερα αρχεία διαμόρφωσης, μπορούμε να καλέσουμε τη συνάρτηση `addConfig()` πολλές φορές. Αν εμφανιστούν στοιχεία με τα ίδια κλειδιά στα αρχεία, θα αντικατασταθούν (ή στην περίπτωση πινάκων [θα συγχωνευθούν |dependency-injection:configuration#Συγχώνευση]). Το αρχείο που εισάγεται αργότερα έχει υψηλότερη προτεραιότητα από το προηγούμενο. - -Το τελευταίο βήμα είναι η δημιουργία του DI container: - -```php -$container = $configurator->createContainer(); -``` - -Και αυτός θα δημιουργήσει για εμάς τα απαιτούμενα αντικείμενα. Για παράδειγμα, αν χρησιμοποιείτε τη διαμόρφωση για το [Nette Database |database:configuration], μπορείτε να του ζητήσετε να δημιουργήσει συνδέσεις βάσης δεδομένων: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// ή -$explorer = $container->getByType(Nette\Database\Explorer::class); -// ή κατά τη δημιουργία πολλαπλών συνδέσεων -$db = $container->getByName('database.main.connection'); -``` - -Και τώρα μπορείτε να εργαστείτε με τη βάση δεδομένων! - - -Κατάσταση ανάπτυξης vs παραγωγής --------------------------------- - -Στην κατάσταση ανάπτυξης, ο container ενημερώνεται αυτόματα κάθε φορά που αλλάζουν τα αρχεία διαμόρφωσης. Στην κατάσταση παραγωγής, δημιουργείται μόνο μία φορά και οι αλλαγές δεν ελέγχονται. Η κατάσταση ανάπτυξης επικεντρώνεται στην μέγιστη άνεση του προγραμματιστή, ενώ η κατάσταση παραγωγής στην απόδοση και την πραγματική ανάπτυξη. - -Η επιλογή της κατάστασης γίνεται μέσω αυτόματης ανίχνευσης, οπότε συνήθως δεν χρειάζεται να διαμορφώσετε ή να αλλάξετε κάτι χειροκίνητα. Η κατάσταση είναι ανάπτυξης εάν η εφαρμογή εκτελείται σε localhost (δηλ. διεύθυνση IP `127.0.0.1` ή `::1`) και δεν υπάρχει proxy (δηλ. η κεφαλίδα HTTP του). Διαφορετικά, εκτελείται σε κατάσταση παραγωγής. - -Αν θέλουμε να ενεργοποιήσουμε την κατάσταση ανάπτυξης και σε άλλες περιπτώσεις, για παράδειγμα για προγραμματιστές που έχουν πρόσβαση από μια συγκεκριμένη διεύθυνση IP, χρησιμοποιούμε το `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// μπορεί να δοθεί και ένας πίνακας διευθύνσεων IP -``` - -Συνιστούμε ανεπιφύλακτα τον συνδυασμό της διεύθυνσης IP με ένα cookie. Στο cookie `nette-debug` αποθηκεύουμε ένα μυστικό token, π.χ. `secret1234`, και με αυτόν τον τρόπο ενεργοποιούμε την κατάσταση ανάπτυξης για προγραμματιστές που έχουν πρόσβαση από μια συγκεκριμένη διεύθυνση IP και ταυτόχρονα έχουν το αναφερόμενο token στο cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Μπορούμε επίσης να απενεργοποιήσουμε εντελώς την κατάσταση ανάπτυξης, ακόμη και για localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Παράμετροι ----------- - -Στα αρχεία διαμόρφωσης μπορείτε επίσης να χρησιμοποιήσετε παραμέτρους, οι οποίες ορίζονται [στην ενότητα `parameters` |dependency-injection:configuration#Παράμετροι]. - -Μπορούν επίσης να εισαχθούν από έξω χρησιμοποιώντας τη μέθοδο `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Στην παράμετρο `projectId` μπορεί να γίνει αναφορά στη διαμόρφωση με τη σύνταξη `%projectId%`. diff --git a/bootstrap/el/@meta.texy b/bootstrap/el/@meta.texy deleted file mode 100644 index a09ce5fe0d..0000000000 --- a/bootstrap/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/en/@home.texy b/bootstrap/en/@home.texy index 0e80ab16c2..af370ff707 100644 --- a/bootstrap/en/@home.texy +++ b/bootstrap/en/@home.texy @@ -7,7 +7,7 @@ Individual Nette components are configured using configuration files. We will sh .[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 |best-practices:editors-and-tools#IDE Editor]. They can be thought of as instructions on how to **create and configure** objects. Thus, the result of loading a configuration will be a so-called factory, which is an object that creates on demand other objects you want to use. For example, a database connection, etc. +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:]. @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -The `projectId` parameter can be referenced in the configuration using the `%projectId%` notation. +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. diff --git a/bootstrap/en/@left-menu.texy b/bootstrap/en/@left-menu.texy new file mode 100644 index 0000000000..fa06cbfede --- /dev/null +++ b/bootstrap/en/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 91205786e5..42471908b0 100644 --- a/bootstrap/en/@meta.texy +++ b/bootstrap/en/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Documentation}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/en/upgrading.texy b/bootstrap/en/upgrading.texy new file mode 100644 index 0000000000..03b81d17f5 --- /dev/null +++ b/bootstrap/en/upgrading.texy @@ -0,0 +1,18 @@ +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` diff --git a/bootstrap/es/@home.texy b/bootstrap/es/@home.texy index 25321bada6..bd94f66c99 100644 --- a/bootstrap/es/@home.texy +++ b/bootstrap/es/@home.texy @@ -2,37 +2,37 @@ Nette Bootstrap *************** .[perex] -Configuramos los componentes individuales de Nette usando archivos de configuración. Mostraremos cómo cargar estos archivos. +Los distintos componentes de Nette se configuran con archivos de configuración. Le mostraremos cómo cargar esos archivos. .[tip] -Si está utilizando todo el framework, no necesita hacer nada más. En su proyecto, tiene un directorio `config/` preparado para archivos de configuración, y la [carga de la aplicación |application:bootstrapping#Configuración del contenedor DI] se encarga de cargarlos. Este artículo es para usuarios que utilizan solo una librería de Nette y desean aprovechar las capacidades de los archivos de configuración. +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 generalmente se escriben en [formato NEON|neon:format] y se editan mejor en [editores con soporte para él |best-practices:editors-and-tools#Editor IDE]. Pueden verse como instrucciones sobre cómo **crear y configurar** objetos. Por lo tanto, el resultado de cargar la configuración será una llamada fábrica, que es un objeto que creará otros objetos que queremos usar bajo demanda. Por ejemplo, una conexión a la base de datos, etc. +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. -Esta fábrica también se llama *contenedor de inyección de dependencias* (contenedor DI) y si está interesado en los detalles, lea el capítulo sobre [inyección de dependencias |dependency-injection:]. +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:]. -La carga de la configuración y la creación del contenedor son manejadas por la clase [api:Nette\Bootstrap\Configurator], así que primero instalaremos su paquete `nette/bootstrap`: +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`. Dado que el contenedor DI generado se almacenará en caché en el disco, es necesario establecer la ruta al directorio donde se guardará: +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#Configuración de permisos de directorio] para el directorio `temp/`. +En Linux o macOS, establezca los [permisos de escritura |nette:troubleshooting#Establecer los permisos de los directorios] del directorio `temp/`. -Y llegamos a los propios archivos de configuración. Los cargamos usando `addConfig()`: +Ahora llegamos a los archivos de configuración en sí. Los cargamos con `addConfig()`: ```php $configurator->addConfig(__DIR__ . '/database.neon'); ``` -Si queremos agregar más archivos de configuración, podemos llamar a la función `addConfig()` varias veces. Si aparecen elementos con las mismas claves en los archivos, se sobrescribirán (o se [fusionarán |dependency-injection:configuration#Fusión] en el caso de arrays). El archivo incluido más tarde tiene mayor prioridad que el anterior. +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: @@ -40,40 +40,40 @@ El último paso es crear el contenedor DI: $container = $configurator->createContainer(); ``` -Y este ya creará los objetos requeridos para nosotros. Por ejemplo, si está utilizando la configuración para [Nette Database|database:configuration], puede pedirle que cree conexiones a la base de datos: +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 múltiples conexiones +// o, al crear varias conexiones $db = $container->getByName('database.main.connection'); ``` -¡Y ahora ya puede trabajar con la base de datos! +¡Y ya puede trabajar con la base de datos! -Modo de desarrollo vs producción --------------------------------- +Modo de desarrollo frente a modo de producción +---------------------------------------------- -En el modo de desarrollo, el contenedor se actualiza automáticamente cada vez que cambian los archivos de configuración. En el modo de producción, se genera solo una vez y no se verifican los cambios. Por lo tanto, el modo de desarrollo se centra en la máxima comodidad del programador, mientras que el modo de producción se centra en el rendimiento y el despliegue en vivo. +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. -La selección del modo se realiza mediante autodetección, por lo que generalmente no es necesario configurar nada ni cambiar manualmente. El modo es de desarrollo si la aplicación se ejecuta en localhost (es decir, dirección IP `127.0.0.1` o `::1`) y no hay proxy presente (es decir, su cabecera HTTP). De lo contrario, se ejecuta en modo de producción. +El modo se 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 habilitar el modo de desarrollo también en otros casos, por ejemplo, para programadores que acceden desde una dirección IP específica, usamos `setDebugMode()`: +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 especificar un array de direcciones IP +// también se puede indicar un array de direcciones IP ``` -Recomendamos encarecidamente combinar la dirección IP con una cookie. Guardaremos un token secreto, por ejemplo, `secret1234`, en la cookie `nette-debug`, y de esta manera activaremos el modo de desarrollo para los programadores que acceden desde una dirección IP específica y que también tienen el token mencionado en la cookie: +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 completamente el modo de desarrollo, incluso para localhost: +También podemos desactivar por completo el modo de desarrollo, incluso para localhost: ```php $configurator->setDebugMode(false); @@ -83,9 +83,9 @@ $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]. +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 el exterior utilizando el método `addDynamicParameters()`: +También se pueden insertar desde fuera con el método `addDynamicParameters()`: ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -Se puede hacer referencia al parámetro `projectId` en la configuración usando la notación `%projectId%`. +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 new file mode 100644 index 0000000000..cbd167179a --- /dev/null +++ b/bootstrap/es/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 25d506cde9..3798d9cda4 100644 --- a/bootstrap/es/@meta.texy +++ b/bootstrap/es/@meta.texy @@ -1,2 +1 @@ -{{sitename: Nette Documentación}} -{{leftbar: nette:@menu-topics}} +{{sitename: Documentación de Nette}} diff --git a/bootstrap/es/upgrading.texy b/bootstrap/es/upgrading.texy new file mode 100644 index 0000000000..ea7ece7df8 --- /dev/null +++ b/bootstrap/es/upgrading.texy @@ -0,0 +1,18 @@ +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 index 25f662ac6c..b83da75f53 100644 --- a/bootstrap/fr/@home.texy +++ b/bootstrap/fr/@home.texy @@ -2,78 +2,78 @@ Nette Bootstrap *************** .[perex] -Les composants individuels de Nette sont configurés à l'aide de fichiers de configuration. Nous allons montrer comment charger ces fichiers. +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 le framework complet, vous n'avez rien d'autre à faire. Votre projet dispose d'un répertoire `config/` préparé pour les fichiers de configuration, et leur chargement est géré par [le chargeur de l'application |application:bootstrapping#Configuration du Conteneur DI]. Cet article s'adresse aux utilisateurs qui n'utilisent qu'une seule bibliothèque Nette et souhaitent profiter des fonctionnalités des fichiers de configuration. +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 sont généralement écrits au [format NEON |neon:format] et sont mieux édités dans des [éditeurs qui le prennent en charge |best-practices:editors-and-tools#Éditeur IDE]. Ils peuvent être considérés comme des instructions sur la façon de **créer et configurer** des objets. Ainsi, le résultat du chargement de la configuration sera une soi-disant factory, qui est un objet qui créera d'autres objets que nous voulons utiliser à la demande. Par exemple, une connexion à une base de données, etc. +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 factory est également appelée *conteneur d'injection de dépendances* (conteneur DI), et si vous êtes intéressé par les détails, lisez le chapitre sur [l'injection de dépendances |dependency-injection:]. +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 gérés par la classe [api:Nette\Bootstrap\Configurator], nous allons donc d'abord installer son paquet `nette/bootstrap` : +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 ``` -Et nous créons une instance de la classe `Configurator`. Comme le conteneur DI généré sera mis en cache sur le disque, il est nécessaire de définir le chemin d'accès au répertoire où il sera stocké : +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, définissez les [droits d'écriture |nette:troubleshooting#Configuration des permissions de répertoire] pour le répertoire `temp/`. +Sous Linux ou macOS, donnez les [droits d'écriture |nette:troubleshooting#Régler les permissions des répertoires] au répertoire `temp/`. -Et nous arrivons aux fichiers de configuration eux-mêmes. Nous les chargeons en utilisant `addConfig()` : +Venons-en aux fichiers de configuration eux-mêmes. Nous les chargeons avec `addConfig()` : ```php $configurator->addConfig(__DIR__ . '/database.neon'); ``` -Si nous voulons ajouter plusieurs fichiers de configuration, nous pouvons appeler la fonction `addConfig()` plusieurs fois. Si des éléments avec les mêmes clés apparaissent dans les fichiers, ils seront écrasés (ou dans le cas de tableaux, [fusionnés |dependency-injection:configuration#Fusion]). Un fichier inclus plus tard a une priorité plus élevée que le précédent. +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 consiste à créer le conteneur DI : +La dernière étape est la création du conteneur DI : ```php $container = $configurator->createContainer(); ``` -Et il créera les objets requis pour nous. Par exemple, si vous utilisez la configuration pour [Nette Database |database:configuration], vous pouvez lui demander de créer des connexions à la base de données : +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); -// ou +// or $explorer = $container->getByType(Nette\Database\Explorer::class); -// ou lors de la création de plusieurs connexions +// or when creating multiple connections $db = $container->getByName('database.main.connection'); ``` -Et maintenant vous pouvez travailler avec la base de données ! +Et vous voilà prêt à travailler avec la base de données ! -Mode développeur vs mode production ------------------------------------ +Mode développement et mode production +------------------------------------- -En mode développeur, le conteneur est automatiquement mis à jour à chaque fois que les fichiers de configuration sont modifiés. En mode production, il n'est généré qu'une seule fois et les modifications ne sont pas vérifiées. Le mode développeur est donc axé sur le confort maximal du programmeur, tandis que le mode production est axé sur la performance et le déploiement en 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. -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. Le mode est développeur si l'application est exécutée sur localhost (c'est-à-dire l'adresse IP `127.0.0.1` ou `::1`) et qu'aucun proxy n'est présent (c'est-à-dire son en-tête HTTP). Sinon, elle s'exécute en mode production. +Le 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éveloppeur dans d'autres cas, par exemple pour les programmeurs accédant depuis une adresse IP spécifique, nous utilisons `setDebugMode()` : +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'); -// vous pouvez également spécifier un tableau d'adresses IP +// an array of IP addresses can also be specified ``` -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éveloppeur pour les programmeurs accédant depuis une adresse IP spécifique et ayant également le jeton mentionné dans le cookie : +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 également désactiver complètement le mode développeur, même pour localhost : +Nous pouvons aussi désactiver complètement le mode développement, y compris sur localhost : ```php $configurator->setDebugMode(false); @@ -83,9 +83,9 @@ $configurator->setDebugMode(false); Paramètres ---------- -Dans les fichiers de configuration, vous pouvez également utiliser des paramètres, qui sont définis [dans la section `parameters` |dependency-injection:configuration#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 insérés de l'extérieur en utilisant la méthode `addDynamicParameters()` : +Ils peuvent également être injectés de l'extérieur par la méthode `addDynamicParameters()` : ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -Le paramètre `projectId` peut être référencé dans la configuration en utilisant la notation `%projectId%`. +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 new file mode 100644 index 0000000000..610ccd39e0 --- /dev/null +++ b/bootstrap/fr/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 95ec8a4ef6..72ae4b8db8 100644 --- a/bootstrap/fr/@meta.texy +++ b/bootstrap/fr/@meta.texy @@ -1,2 +1 @@ {{sitename: Documentation Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/fr/upgrading.texy b/bootstrap/fr/upgrading.texy new file mode 100644 index 0000000000..281e99fa41 --- /dev/null +++ b/bootstrap/fr/upgrading.texy @@ -0,0 +1,18 @@ +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/hu/@home.texy b/bootstrap/hu/@home.texy deleted file mode 100644 index 86a0ca10df..0000000000 --- a/bootstrap/hu/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -A Nette egyes részeit konfigurációs fájlok segítségével állítjuk be. Megmutatjuk, hogyan kell ezeket a fájlokat betölteni. - -.[tip] -Ha a teljes keretrendszert használja, nincs szükség további teendőkre. A projektben van egy előkészített `config/` könyvtár a konfigurációs fájlok számára, és ezek betöltéséért az [alkalmazás betöltő |application:bootstrapping#DI konténer konfigurálása] felelős. Ez a cikk azoknak a felhasználóknak szól, akik csak egy Nette könyvtárat használnak, és ki szeretnék használni a konfigurációs fájlok lehetőségeit. - -A konfigurációs fájlokat általában [NEON formátumban|neon:format] írják, és a legjobban [az azt támogató szerkesztőkben |best-practices:editors-and-tools#IDE szerkesztő] lehet szerkeszteni. Útmutatóként foghatók fel, hogyan **hozzunk létre és konfiguráljunk** objektumokat. Tehát a konfiguráció betöltésének eredménye egy úgynevezett factory lesz, ami egy olyan objektum, amely kérésre létrehozza számunkra a használni kívánt további objektumokat. Például adatbázis-kapcsolatokat stb. - -Ezt a factory-t *dependency injection konténernek* (DI konténer) is nevezik, és ha érdeklik a részletek, olvassa el a [dependency injection |dependency-injection:] fejezetet. - -A konfiguráció betöltését és a konténer létrehozását az [api:Nette\Bootstrap\Configurator] osztály végzi, ezért először telepítjük a `nette/bootstrap` csomagját: - -```shell -composer require nette/bootstrap -``` - -És létrehozunk egy `Configurator` osztály példányt. Mivel a generált DI konténer a lemezre lesz gyorsítótárazva, meg kell adni annak a könyvtárnak az elérési útját, ahová menteni fogja: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Linuxon vagy macOS-en állítson be [írási jogokat |nette:troubleshooting#Könyvtárjogosultságok beállítása] a `temp/` könyvtárnak. - -És elérkeztünk magukhoz a konfigurációs fájlokhoz. Ezeket az `addConfig()` segítségével töltjük be: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Ha több konfigurációs fájlt szeretnénk hozzáadni, többször is meghívhatjuk az `addConfig()` függvényt. Ha a fájlokban azonos kulcsú elemek jelennek meg, azok felülíródnak (vagy tömbök esetén [összevonódnak |dependency-injection:configuration#Összefésülés]). A később hozzáadott fájl magasabb prioritással rendelkezik, mint az előző. - -Az utolsó lépés a DI konténer létrehozása: - -```php -$container = $configurator->createContainer(); -``` - -És ez már létrehozza számunkra a kívánt objektumokat. Ha például a [Nette Database|database:configuration] konfigurációját használja, kérheti tőle adatbázis-kapcsolatok létrehozását: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// vagy -$explorer = $container->getByType(Nette\Database\Explorer::class); -// vagy több kapcsolat létrehozásakor -$db = $container->getByName('database.main.connection'); -``` - -És most már dolgozhat az adatbázissal! - - -Fejlesztői vs. éles üzemmód ---------------------------- - -Fejlesztői módban a konténer automatikusan frissül minden konfigurációs fájl módosításakor. Éles (produkciós) módban csak egyszer generálódik, és a változásokat nem ellenőrzi. A fejlesztői mód tehát a programozó maximális kényelmére összpontosít, az éles mód a teljesítményre és az éles bevetésre. - -Az üzemmód kiválasztása automatikus felismeréssel történik, így általában nincs szükség semmit konfigurálni vagy manuálisan váltani. Az üzemmód fejlesztői, ha az alkalmazás localhoston fut (azaz IP-cím `127.0.0.1` vagy `::1`), és nincs jelen proxy (azaz annak HTTP fejléce). Ellenkező esetben éles módban fut. - -Ha engedélyezni szeretnénk a fejlesztői módot más esetekben is, például egy adott IP-címről hozzáférő programozók számára, használjuk a `setDebugMode()` metódust: - -```php -$configurator->setDebugMode('23.75.345.200'); -// megadható IP-címek tömbje is -``` - -Mindenképpen javasoljuk az IP-cím és egy cookie kombinálását. A `nette-debug` cookie-ba mentsünk egy titkos tokent, pl. `secret1234`, és így aktiváljuk a fejlesztői módot az adott IP-címről hozzáférő és a cookie-ban említett tokennel rendelkező programozók számára: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -A fejlesztői módot teljesen ki is kapcsolhatjuk, még localhost esetén is: - -```php -$configurator->setDebugMode(false); -``` - - -Paraméterek ------------ - -A konfigurációs fájlokban paramétereket is használhat, amelyeket [a `parameters` szekcióban |dependency-injection:configuration#Paraméterek] definiálunk. - -Ezeket kívülről is beilleszthetjük az `addDynamicParameters()` metódussal: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -A `projectId` paraméterre a konfigurációban a `%projectId%` jelöléssel hivatkozhatunk. diff --git a/bootstrap/hu/@meta.texy b/bootstrap/hu/@meta.texy deleted file mode 100644 index c00a2158aa..0000000000 --- a/bootstrap/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette dokumentáció}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/it/@home.texy b/bootstrap/it/@home.texy index 72a8b4d1f0..15ff03669c 100644 --- a/bootstrap/it/@home.texy +++ b/bootstrap/it/@home.texy @@ -2,78 +2,78 @@ Nette Bootstrap *************** .[perex] -Le singole parti di Nette vengono impostate tramite file di configurazione. Vediamo come caricare questi file. +I singoli componenti di Nette si configurano con i file di configurazione. Mostreremo come caricare questi file. .[tip] -Se si utilizza l'intero framework, non è necessario fare altro. Nel progetto è presente una directory `config/` preimpostata per i file di configurazione, e il loro caricamento è gestito dal [bootloader dell'applicazione |application:bootstrapping#Configurazione del Container DI]. Questo articolo è per gli utenti che utilizzano solo una libreria Nette e vogliono sfruttare le possibilità dei file di configurazione. +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 sono solitamente scritti in [formato NEON|neon:format] e si modificano al meglio negli [editor con supporto per esso |best-practices:editors-and-tools#Editor IDE]. Possono essere visti come istruzioni su come **creare e configurare** oggetti. Quindi, il risultato del caricamento della configurazione sarà una cosiddetta factory, che è un oggetto che, su richiesta, ci creerà altri oggetti che vogliamo utilizzare. Ad esempio, connessioni al database, ecc. +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 è anche chiamata *dependency injection container* (container DI) e se sei interessato ai dettagli, leggi il capitolo sulla [dependency injection |dependency-injection:]. +Questa factory si chiama anche *dependency injection container* (container DI) e, se vi interessano i dettagli, leggete il capitolo sulla [dependency injection |dependency-injection:]. -Il caricamento della configurazione e la creazione del container sono gestiti dalla classe [api:Nette\Bootstrap\Configurator], quindi installiamo prima il suo pacchetto `nette/bootstrap`: +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à memorizzato nella cache su disco, è necessario impostare il percorso della directory in cui verrà salvato: +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, imposta i [permessi di scrittura |nette:troubleshooting#Impostazione dei permessi delle directory] per la directory `temp/`. +Su Linux o macOS impostate i [permessi di scrittura |nette:troubleshooting#Impostazione dei permessi delle directory] per la directory `temp/`. -E arriviamo ai file di configurazione stessi. Li carichiamo usando `addConfig()`: +Ora arriviamo ai file di configurazione veri e propri. Li carichiamo con `addConfig()`: ```php $configurator->addConfig(__DIR__ . '/database.neon'); ``` -Se vogliamo aggiungere più file di configurazione, possiamo chiamare la funzione `addConfig()` più volte. Se nei file compaiono elementi con le stesse chiavi, verranno sovrascritti (o, nel caso degli array, [uniti |dependency-injection:configuration#Unione]). Il file inserito successivamente ha una priorità maggiore rispetto al precedente. +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 è la creazione del container DI: +L'ultimo passo è creare il container DI: ```php $container = $configurator->createContainer(); ``` -E questo ci creerà già gli oggetti richiesti. Se, ad esempio, utilizzi la configurazione per [Nette Database|database:configuration], puoi chiedergli di creare le connessioni al database: +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 creando più connessioni +// oppure quando si creano più connessioni $db = $container->getByName('database.main.connection'); ``` -E ora puoi già lavorare con il database! +E ora potete lavorare con il database! -Modalità sviluppatore vs produzione ------------------------------------ +Modalità di sviluppo e modalità produzione +------------------------------------------ -In modalità sviluppatore, il container si aggiorna automaticamente ad ogni modifica dei file di configurazione. In modalità produzione, viene generato solo una volta e le modifiche non vengono controllate. La modalità sviluppatore è quindi focalizzata sulla massima comodità del programmatore, la modalità produzione sulle prestazioni e sulla distribuzione in 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 tramite autodetect, quindi di solito non è necessario configurare nulla o passare manualmente. La modalità è sviluppatore se l'applicazione viene eseguita su localhost (cioè indirizzo IP `127.0.0.1` o `::1`) e non è presente una proxy (cioè la sua intestazione HTTP). Altrimenti, viene eseguita in modalità 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 abilitare la modalità sviluppatore anche in altri casi, ad esempio per i programmatori che accedono da un indirizzo IP specifico, usiamo `setDebugMode()`: +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'); -// è possibile specificare anche un array di indirizzi IP +// si può indicare anche un array di indirizzi IP ``` -Raccomandiamo vivamente di combinare l'indirizzo IP con un cookie. Nel cookie `nette-debug` salviamo un token segreto, ad esempio `secret1234`, e in questo modo attiviamo la modalità sviluppatore per i programmatori che accedono da un indirizzo IP specifico e che hanno anche il token menzionato nel cookie: +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 disabilitare completamente la modalità sviluppatore, anche per localhost: +Possiamo anche disattivare del tutto la modalità di sviluppo, perfino per localhost: ```php $configurator->setDebugMode(false); @@ -83,9 +83,9 @@ $configurator->setDebugMode(false); Parametri --------- -Nei file di configurazione è possibile utilizzare anche parametri, che vengono definiti [nella sezione `parameters` |dependency-injection:configuration#Parametri]. +Nei file di configurazione potete usare anche i parametri, che si definiscono [nella sezione `parameters` |dependency-injection:configuration#Parametri]. -Possono anche essere inseriti dall'esterno tramite il metodo `addDynamicParameters()`: +Si possono anche inserire dall'esterno con il metodo `addDynamicParameters()`: ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -Al parametro `projectId` si può fare riferimento nella configurazione tramite la notazione `%projectId%`. +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 new file mode 100644 index 0000000000..24db87db11 --- /dev/null +++ b/bootstrap/it/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 9d19e7312c..4647d0c8a2 100644 --- a/bootstrap/it/@meta.texy +++ b/bootstrap/it/@meta.texy @@ -1,2 +1 @@ {{sitename: Documentazione Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/it/upgrading.texy b/bootstrap/it/upgrading.texy new file mode 100644 index 0000000000..fb280b0119 --- /dev/null +++ b/bootstrap/it/upgrading.texy @@ -0,0 +1,18 @@ +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 index 80ea76d567..c7cea8d524 100644 --- a/bootstrap/ja/@home.texy +++ b/bootstrap/ja/@home.texy @@ -2,78 +2,78 @@ Nette Bootstrap *************** .[perex] -Nette の個々のコンポーネントは設定ファイルを使用して設定します。これらのファイルを読み込む方法を示します。 +Nette の個々のコンポーネントは設定ファイルで設定します。ここではそのファイルの読み込み方をお見せします。 .[tip] -フレームワーク全体を使用している場合、追加の作業は必要ありません。プロジェクトには設定ファイル用の `config/` ディレクトリが用意されており、それらの読み込みは[アプリケーションブートローダー |application:bootstrapping#DIコンテナの設定]が担当します。 この記事は、Nette のライブラリを 1 つだけ使用し、設定ファイルの機能を利用したいユーザー向けです。 +フレームワーク全体を使っているなら、ほかに何もする必要はありません。あなたのプロジェクトには設定ファイル用の `config/` のディレクトリが用意されていて、その読み込みは[アプリケーションの立ち上げ役 |application:bootstrapping#DI コンテナの設定]が受け持ちます。この記事は、Nette のライブラリをひとつだけ使っていて、設定ファイルを活かしたい人のためのものです。 -設定ファイルは通常[NEON 形式|neon:format]で記述され、[サポートされているエディタ |best-practices:editors-and-tools#IDEエディタ]で編集するのが最適です。これらはオブジェクトを**作成および設定**する方法の指示として理解できます。したがって、設定の読み込み結果はいわゆるファクトリであり、これはリクエストに応じて使用したい他のオブジェクト(データベース接続など)を作成するオブジェクトです。 +設定ファイルはふつう [NEON 形式|neon:format]で書かれ、[それに対応したエディタ |tools:ide]で編集するのがいちばんです。設定ファイルは、オブジェクトを**どう作ってどう整えるか**の指示だと考えられます。ですから設定を読み込んだ結果はいわゆるファクトリになります。これは、あなたが使いたいほかのオブジェクトを求めに応じて作るオブジェクトです。たとえばデータベース接続などです。 -このファクトリは*依存関係注入コンテナ*(DI コンテナ)とも呼ばれ、詳細に興味がある場合は[依存関係注入 |dependency-injection:]に関する章をお読みください。 +このファクトリは *dependency injection コンテナ*(DI コンテナ)とも呼ばれます。詳しいことに関心があるなら、[dependency injection |dependency-injection:]の章をご覧ください。 -設定の読み込みとコンテナの作成は[api:Nette\Bootstrap\Configurator]クラスが行うため、まずそのパッケージ `nette/bootstrap` をインストールします: +設定の読み込みとコンテナの生成は [api:Nette\Bootstrap\Configurator]クラスが受け持つので、まずその `nette/bootstrap` のパッケージを入れます。 ```shell composer require nette/bootstrap ``` -そして `Configurator` クラスのインスタンスを作成します。生成されたDIコンテナはディスクにキャッシュされるため、保存先のディレクトリパスを設定する必要があります: +そして `Configurator` クラスのインスタンスを作ります。作られる DI コンテナはディスクに蓄えられるので、それを保存するディレクトリへのパスを設定する必要があります。 ```php $configurator = new Nette\Bootstrap\Configurator; $configurator->setTempDirectory(__DIR__ . '/temp'); ``` -LinuxまたはmacOSでは、`temp/` ディレクトリに[書き込み権限を設定 |nette:troubleshooting#ディレクトリ権限の設定]してください。 +Linux や macOS では、`temp/` のディレクトリに[書き込みの権限 |nette:troubleshooting#ディレクトリの権限の設定]を与えてください。 -そして、設定ファイル自体に移ります。これらは `addConfig()` を使用して読み込みます: +ではいよいよ設定ファイルそのものです。`addConfig()` で読み込みます。 ```php $configurator->addConfig(__DIR__ . '/database.neon'); ``` -複数の設定ファイルを追加したい場合は、`addConfig()` 関数を複数回呼び出すことができます。ファイル内に同じキーを持つ要素が現れた場合、それらは上書きされます(または配列の場合は[マージされます |dependency-injection:configuration#マージ])。後から読み込まれたファイルは前のファイルよりも高い優先度を持ちます。 +設定ファイルをもっと足したいなら、`addConfig()` の関数を何度でも呼べます。ファイルに同じキーの要素が現れると、それは上書きされます(配列の場合は[併合されます |dependency-injection:configuration#統合])。あとから足したファイルのほうが、前のものより優先されます。 -最後のステップはDIコンテナの作成です: +最後の一歩は DI コンテナを作ることです。 ```php $container = $configurator->createContainer(); ``` -そして、それは必要なオブジェクトを作成します。たとえば、[Nette Database|database:configuration]の設定を使用している場合、データベース接続の作成をリクエストできます: +これで望むオブジェクトが作られます。たとえば [Nette Database|database:configuration]の設定を使っているなら、データベース接続を作ってもらえます。 ```php $db = $container->getByType(Nette\Database\Connection::class); // または $explorer = $container->getByType(Nette\Database\Explorer::class); -// または複数の接続を作成する場合 +// あるいは複数の接続を作る場合 $db = $container->getByName('database.main.connection'); ``` -これでデータベースを操作できます! +これでデータベースを扱えます。 -開発モード vs プロダクションモード -------------------- +開発モードと本番モード +----------- -開発モードでは、設定ファイルが変更されるたびにコンテナが自動的に更新されます。プロダクションモードでは、一度だけ生成され、変更はチェックされません。 したがって、開発モードはプログラマの最大限の利便性に焦点を当てており、プロダクションモードはパフォーマンスと本番展開に焦点を当てています。 +開発モードでは、設定ファイルが変わるたびにコンテナが自動的に更新されます。本番モードでは一度だけ作られ、変更は確かめられません。ですから開発モードはプログラマーの心地よさを最大にすることを、本番モードは性能と本番への配置を目指しています。 -モードの選択は自動検出によって行われるため、通常は何も設定したり手動で切り替えたりする必要はありません。アプリケーションがlocalhost(つまりIPアドレス `127.0.0.1` または `::1`)で実行され、プロキシが存在しない(つまりそのHTTPヘッダーがない)場合、モードは開発モードになります。それ以外の場合はプロダクションモードで実行されます。 +モードの選択は自動の判別で行われるので、ふつうは何かを設定したり手で切り替えたりする必要はありません。アプリケーションが localhost(つまり IP アドレス `127.0.0.1` または `::1`)で動いていて、プロキシ(つまりその HTTP ヘッダー)がないときが開発モードです。そうでなければ本番モードで動きます。 -特定のIPアドレスからアクセスするプログラマなど、他の場合に開発モードを有効にしたい場合は、`setDebugMode()` を使用します: +たとえば特定の IP アドレスからアクセスするプログラマーのように、ほかの場合にも開発モードを有効にしたいなら、`setDebugMode()` を使います。 ```php $configurator->setDebugMode('23.75.345.200'); -// IPアドレスの配列も指定できます +// IP アドレスの配列も指定できます ``` -IPアドレスとCookieを組み合わせることを強くお勧めします。`nette-debug` Cookieに秘密のトークン(例:`secret1234`)を保存し、この方法で特定のIPアドレスからアクセスし、かつCookieに言及されたトークンを持つプログラマに対して開発モードを有効にします: +IP アドレスとクッキーを組み合わせることを強くおすすめします。`nette-debug` のクッキーに秘密のトークン、たとえば `secret1234` を入れておきます。こうすれば、そのトークンをクッキーに持つ、特定の IP アドレスからアクセスするプログラマーにだけ開発モードを有効にできます。 ```php $configurator->setDebugMode('secret1234@23.75.345.200'); ``` -localhostに対しても、開発モードを完全に無効にすることもできます: +localhost も含めて、開発モードを完全に切ることもできます。 ```php $configurator->setDebugMode(false); @@ -83,9 +83,9 @@ $configurator->setDebugMode(false); パラメータ ----- -設定ファイルでは、[`parameters` セクション |dependency-injection:configuration#パラメータ]で定義されるパラメータも使用できます。 +設定ファイルではパラメータも使えます。それは [`parameters` の区画 |dependency-injection:configuration#パラメータ]で定めます。 -これらは `addDynamicParameters()` メソッドを使用して外部から挿入することもできます: +`addDynamicParameters()` メソッドで外から入れることもできます。 ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -パラメータ `projectId` は、設定内で `%projectId%` と記述することで参照できます。 +`remoteIp` のパラメータは、設定の中で `%remoteIp%` の書き方で参照できます。 + + +新しい版へ上げるなら、[アップグレード |upgrading]のページをご覧ください。 diff --git a/bootstrap/ja/@left-menu.texy b/bootstrap/ja/@left-menu.texy new file mode 100644 index 0000000000..a14c57e9d4 --- /dev/null +++ b/bootstrap/ja/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 7d67dcb7b8..43b85f3cac 100644 --- a/bootstrap/ja/@meta.texy +++ b/bootstrap/ja/@meta.texy @@ -1,2 +1 @@ -{{sitename: Nette ドキュメンテーション}} -{{leftbar: nette:@menu-topics}} +{{sitename: Nette ドキュメント}} diff --git a/bootstrap/ja/upgrading.texy b/bootstrap/ja/upgrading.texy new file mode 100644 index 0000000000..7d422f5d3a --- /dev/null +++ b/bootstrap/ja/upgrading.texy @@ -0,0 +1,18 @@ +アップグレード +******* + + +バージョン 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 index 1af4e4578f..2ccc9134fc 100644 --- a/bootstrap/meta.json +++ b/bootstrap/meta.json @@ -1,5 +1,6 @@ { "version": "3.x", "repo": "nette/bootstrap", - "composer": "nette/bootstrap" + "composer": "nette/bootstrap", + "api": "https://api.nette.org/bootstrap/" } diff --git a/bootstrap/pl/@home.texy b/bootstrap/pl/@home.texy index 5089994f3b..4f20b60939 100644 --- a/bootstrap/pl/@home.texy +++ b/bootstrap/pl/@home.texy @@ -2,37 +2,37 @@ Nette Bootstrap *************** .[perex] -Poszczególne części Nette ustawiamy za pomocą plików konfiguracyjnych. Pokażemy, jak te pliki wczytywać. +Poszczególne komponenty Nette konfiguruje się za pomocą plików konfiguracyjnych. Pokażemy, jak te pliki wczytać. .[tip] -Jeśli używasz całego frameworka, nie trzeba nic więcej robić. W projekcie masz przygotowany katalog `config/` na pliki konfiguracyjne, a ich wczytywaniem zajmuje się [bootloader aplikacji |application:bootstrapping#Konfiguracja kontenera DI]. Ten artykuł jest dla użytkowników, którzy używają tylko jednej biblioteki Nette i chcą wykorzystać możliwości plików konfiguracyjnych. +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 zazwyczaj zapisuje się w [formacie NEON|neon:format] i najlepiej edytuje się je w [edytorach z jego obsługą |best-practices:editors-and-tools#Edytor IDE]. Można je rozumieć jako instrukcje, jak **tworzyć i konfigurować** obiekty. Zatem wynikiem wczytania konfiguracji będzie tzw. fabryka, czyli obiekt, który na żądanie utworzy nam kolejne obiekty, których chcemy używać. Na przykład połączenie z bazą danych itp. +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. -Ta fabryka nazywana jest również *kontenerem dependency injection* (kontenerem DI), a jeśli interesują Cię szczegóły, przeczytaj rozdział o [dependency injection |dependency-injection:]. +Fabrykę tę nazywa się też *kontenerem dependency injection* (kontenerem DI), a jeśli interesują Cię szczegóły, przeczytaj rozdział o [dependency injection |dependency-injection:]. -Wczytanie konfiguracji i utworzenie kontenera zapewnia klasa [api:Nette\Bootstrap\Configurator], więc najpierw zainstalujemy jej pakiet `nette/bootstrap`: +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 tworzymy instancję klasy `Configurator`. Ponieważ wygenerowany kontener DI będzie buforowany na dysku, konieczne jest ustawienie ścieżki do katalogu, w którym będzie przechowywany: +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'); ``` -Na Linuksie lub macOS ustaw katalogowi `temp/` [uprawnienia do zapisu |nette:troubleshooting#Ustawianie uprawnień do katalogów]. +W Linuksie albo macOS ustaw katalogowi `temp/` [uprawnienia do zapisu |nette:troubleshooting#Ustawienie uprawnień do katalogów]. -Dochodzimy do samych plików konfiguracyjnych. Wczytujemy je za pomocą `addConfig()`: +Teraz przechodzimy do samych plików konfiguracyjnych. Wczytujemy je za pomocą `addConfig()`: ```php $configurator->addConfig(__DIR__ . '/database.neon'); ``` -Jeśli chcemy dodać więcej plików konfiguracyjnych, możemy wywołać funkcję `addConfig()` wielokrotnie. Jeśli w plikach pojawią się elementy o tych samych kluczach, zostaną one nadpisane (lub w przypadku tablic [scalane |dependency-injection:configuration#Łączenie]). Później wczytany plik ma wyższy priorytet niż poprzedni. +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: @@ -40,40 +40,40 @@ Ostatnim krokiem jest utworzenie kontenera DI: $container = $configurator->createContainer(); ``` -A ten już utworzy nam żądane obiekty. Jeśli na przykład używasz konfiguracji dla [Nette Database|database:configuration], możesz go poprosić o utworzenie połączeń z bazą danych: +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); -// lub +// albo $explorer = $container->getByType(Nette\Database\Explorer::class); -// lub przy tworzeniu wielu połączeń +// albo przy tworzeniu wielu połączeń $db = $container->getByName('database.main.connection'); ``` -I teraz możesz już pracować z bazą danych! +I teraz możesz pracować z bazą danych! -Tryb deweloperski vs produkcyjny --------------------------------- +Tryb deweloperski a produkcyjny +------------------------------- -W trybie deweloperskim kontener jest automatycznie aktualizowany przy każdej zmianie plików konfiguracyjnych. W trybie produkcyjnym generowany jest tylko raz, a zmiany nie są sprawdzane. Tryb deweloperski jest więc ukierunkowany na maksymalny komfort programisty, tryb produkcyjny na wydajność i wdrożenie produkcyjne. +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ę poprzez autodetekcję, więc zazwyczaj nie ma potrzeby niczego konfigurować ani ręcznie przełączać. Tryb jest deweloperski, jeśli aplikacja jest uruchomiona na localhost (tj. adres IP `127.0.0.1` lub `::1`) i nie ma obecnego proxy (tj. jego nagłówka HTTP). W przeciwnym razie działa w trybie 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 również w innych przypadkach, na przykład dla programistów łączących się z określonego adresu IP, używamy `setDebugMode()`: +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 również podać tablicę adresów IP +// można podać też tablicę adresów IP ``` -Zdecydowanie zalecamy łączenie adresu IP z ciasteczkiem. W ciasteczku `nette-debug` zapiszemy tajny token, np. `secret1234`, i w ten sposób aktywujemy tryb deweloperski dla programistów łączących się z określonego adresu IP i jednocześnie posiadających w ciasteczku wspomniany token: +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 również całkowicie wyłączyć, nawet dla localhost: +Tryb deweloperski możemy też całkowicie wyłączyć, nawet dla localhosta: ```php $configurator->setDebugMode(false); @@ -83,9 +83,9 @@ $configurator->setDebugMode(false); Parametry --------- -W plikach konfiguracyjnych możesz również używać parametrów, które są definiowane [w sekcji `parameters` |dependency-injection:configuration#Parametry]. +W plikach konfiguracyjnych możesz też używać parametrów definiowanych [w sekcji `parameters` |dependency-injection:configuration#Parametry]. -Można je również wstawiać z zewnątrz za pomocą metody `addDynamicParameters()`: +Można je też wstawić z zewnątrz metodą `addDynamicParameters()`: ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -Do parametru `projectId` można się odwołać w konfiguracji zapisem `%projectId%`. +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 new file mode 100644 index 0000000000..e5a94dddb2 --- /dev/null +++ b/bootstrap/pl/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 08f2227fb5..61ac92d1af 100644 --- a/bootstrap/pl/@meta.texy +++ b/bootstrap/pl/@meta.texy @@ -1,2 +1 @@ {{sitename: Dokumentacja Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/pl/upgrading.texy b/bootstrap/pl/upgrading.texy new file mode 100644 index 0000000000..d71d8cc454 --- /dev/null +++ b/bootstrap/pl/upgrading.texy @@ -0,0 +1,18 @@ +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/pt/@home.texy b/bootstrap/pt/@home.texy deleted file mode 100644 index 0344fc188a..0000000000 --- a/bootstrap/pt/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Os componentes individuais do Nette são configurados usando arquivos de configuração. Mostraremos como carregar esses arquivos. - -.[tip] -Se você estiver usando todo o framework, não há necessidade de fazer mais nada. No seu projeto, você tem um diretório `config/` pré-preparado para arquivos de configuração, e o carregamento deles é responsabilidade do [carregador da aplicação |application:bootstrapping#Configuração do contêiner de DI]. Este artigo é para usuários que usam apenas uma biblioteca Nette e desejam aproveitar as opções dos arquivos de configuração. - -Os arquivos de configuração são geralmente escritos no [formato NEON|neon:format] e são melhor editados em [editores com suporte a ele |best-practices:editors-and-tools#Editor IDE]. Eles podem ser entendidos como instruções sobre como **criar e configurar** objetos. Ou seja, o resultado do carregamento da configuração será uma chamada fábrica, que é um objeto que, sob demanda, criará outros objetos que queremos usar. Por exemplo, uma conexão de banco de dados, etc. - -Essa fábrica também é chamada de *contêiner de injeção de dependência* (contêiner DI) e, se você estiver interessado em detalhes, leia o capítulo sobre [injeção de dependência |dependency-injection:]. - -O carregamento da configuração e a criação do contêiner são feitos pela classe [api:Nette\Bootstrap\Configurator], então primeiro instalaremos seu pacote `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -E criamos uma instância da classe `Configurator`. Como o contêiner DI gerado será armazenado em cache no disco, é necessário definir o caminho para o diretório onde ele será salvo: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -No Linux ou macOS, defina as [permissões de escrita |nette:troubleshooting#Configurando Permissões de Diretório] para o diretório `temp/`. - -E chegamos aos próprios arquivos de configuração. Nós os carregamos usando `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Se quisermos adicionar mais arquivos de configuração, podemos chamar a função `addConfig()` várias vezes. Se elementos com as mesmas chaves aparecerem nos arquivos, eles serão sobrescritos (ou, no caso de arrays, [mesclados |dependency-injection:configuration#Mesclagem]). O arquivo inserido posteriormente tem prioridade maior que o anterior. - -O último passo é criar o contêiner de DI: - -```php -$container = $configurator->createContainer(); -``` - -E ele já criará os objetos necessários para nós. Por exemplo, se você estiver usando a configuração para [Nette Database|database:configuration], pode pedir a ele para criar conexões de banco de dados: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// ou -$explorer = $container->getByType(Nette\Database\Explorer::class); -// ou ao criar múltiplas conexões -$db = $container->getByName('database.main.connection'); -``` - -E agora você já pode trabalhar com o banco de dados! - - -Modo de desenvolvimento vs. produção ------------------------------------- - -No modo de desenvolvimento, o contêiner é atualizado automaticamente sempre que os arquivos de configuração são alterados. No modo de produção, ele é gerado apenas uma vez e as alterações não são verificadas. O modo de desenvolvimento é, portanto, focado no máximo conforto do programador, enquanto o modo de produção é focado no desempenho e na implantação em produção. - -A seleção do modo é feita por autodetecção, portanto, geralmente não é necessário configurar nada ou alternar manualmente. O modo é de desenvolvimento se a aplicação for executada em localhost (ou seja, endereço IP `127.0.0.1` ou `::1`) e não houver proxy presente (ou seja, seu cabeçalho HTTP). Caso contrário, ele é executado no modo de produção. - -Se quisermos habilitar o modo de desenvolvimento também em outros casos, por exemplo, para programadores acessando de um endereço IP específico, usamos `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// também pode ser especificado um array de endereços IP -``` - -Recomendamos enfaticamente combinar o endereço IP com um cookie. Armazenamos um token secreto no cookie `nette-debug`, por exemplo, `secret1234`, e dessa forma ativamos o modo de desenvolvimento para programadores acessando de um endereço IP específico e também tendo o token mencionado no cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Também podemos desativar completamente o modo de desenvolvimento, mesmo para localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parâmetros ----------- - -Nos arquivos de configuração, você também pode usar parâmetros, que são definidos [na seção `parameters` |dependency-injection:configuration#Parâmetros]. - -Eles também podem ser inseridos de fora usando o método `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -O parâmetro `projectId` pode ser referenciado na configuração usando a notação `%projectId%`. diff --git a/bootstrap/pt/@meta.texy b/bootstrap/pt/@meta.texy deleted file mode 100644 index e2566bcb44..0000000000 --- a/bootstrap/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentação Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/ro/@home.texy b/bootstrap/ro/@home.texy deleted file mode 100644 index 16606ee9e5..0000000000 --- a/bootstrap/ro/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Componentele individuale Nette sunt configurate folosind fișiere de configurare. Vom arăta cum să încărcați aceste fișiere. - -.[tip] -Dacă utilizați întregul framework, nu este nevoie să faceți nimic altceva. În proiect aveți un director `config/` pregătit pentru fișierele de configurare, iar încărcarea lor este gestionată de [încărcătorul aplicației |application:bootstrapping#Configurarea containerului DI]. Acest articol este pentru utilizatorii care folosesc doar o singură bibliotecă Nette și doresc să profite de posibilitățile fișierelor de configurare. - -Fișierele de configurare sunt de obicei scrise în [formatul NEON|neon:format] și cel mai bine se editează în [editori cu suport pentru acesta |best-practices:editors-and-tools#Editor IDE]. Ele pot fi înțelese ca instrucțiuni despre cum să **creați și configurați** obiecte. Prin urmare, rezultatul încărcării configurației va fi așa-numita fabrică (factory), care este un obiect ce ne va crea la cerere alte obiecte pe care dorim să le folosim. De exemplu, conexiuni la baze de date etc. - -Această fabrică se mai numește și *dependency injection container* (container DI) și, dacă sunteți interesat de detalii, citiți capitolul despre [dependency injection |dependency-injection:]. - -Încărcarea configurației și crearea containerului sunt gestionate de clasa [api:Nette\Bootstrap\Configurator], așa că mai întâi vom instala pachetul său `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -Și vom crea o instanță a clasei `Configurator`. Deoarece containerul DI generat va fi stocat în cache pe disc, este necesar să setați calea către directorul unde va fi salvat: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Pe Linux sau macOS, setați [drepturi de scriere |nette:troubleshooting#Setarea permisiunilor pentru directoare] pentru directorul `temp/`. - -Și ajungem la fișierele de configurare în sine. Le încărcăm folosind `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Dacă dorim să adăugăm mai multe fișiere de configurare, putem apela funcția `addConfig()` de mai multe ori. Dacă în fișiere apar elemente cu aceleași chei, acestea vor fi suprascrise (sau, în cazul array-urilor, [combinate |dependency-injection:configuration#Combinare]). Fișierul încărcat ulterior are prioritate mai mare decât cel anterior. - -Ultimul pas este crearea containerului DI: - -```php -$container = $configurator->createContainer(); -``` - -Și acesta ne va crea obiectele solicitate. De exemplu, dacă utilizați configurația pentru [Nette Database|database:configuration], îi puteți cere să creeze conexiuni la baza de date: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// sau -$explorer = $container->getByType(Nette\Database\Explorer::class); -// sau la crearea mai multor conexiuni -$db = $container->getByName('database.main.connection'); -``` - -Și acum puteți lucra cu baza de date! - - -Mod dezvoltator vs mod producție --------------------------------- - -În modul dezvoltator, containerul se actualizează automat la fiecare modificare a fișierelor de configurare. În modul producție, se generează o singură dată și modificările nu sunt verificate. Modul dezvoltator este, prin urmare, axat pe confortul maxim al programatorului, iar modul producție pe performanță și implementare live. - -Selectarea modului se face prin autodetecție, deci de obicei nu este nevoie să configurați sau să comutați manual nimic. Modul este dezvoltator dacă aplicația este rulată pe localhost (adică adresa IP `127.0.0.1` sau `::1`) și nu este prezent un proxy (adică antetul său HTTP). Altfel, rulează în modul producție. - -Dacă dorim să activăm modul dezvoltator și în alte cazuri, de exemplu pentru programatorii care accesează de la o anumită adresă IP, folosim `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// se poate specifica și un array de adrese IP -``` - -Recomandăm cu tărie combinarea adresei IP cu un cookie. Vom stoca un token secret în cookie-ul `nette-debug`, de exemplu `secret1234`, și astfel vom activa modul dezvoltator pentru programatorii care accesează de la o anumită adresă IP și au în același timp tokenul menționat în cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Putem, de asemenea, să dezactivăm complet modul dezvoltator, chiar și pentru localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parametri ---------- - -În fișierele de configurare puteți utiliza și parametri, care sunt definiți [în secțiunea `parameters` |dependency-injection:configuration#Parametri]. - -Aceștia pot fi, de asemenea, inserați din exterior folosind metoda `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Parametrul `projectId` poate fi referențiat în configurație prin notația `%projectId%`. diff --git a/bootstrap/ro/@meta.texy b/bootstrap/ro/@meta.texy deleted file mode 100644 index 6554692600..0000000000 --- a/bootstrap/ro/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentație Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/ru/@home.texy b/bootstrap/ru/@home.texy index ec99937fd8..e7c34492a1 100644 --- a/bootstrap/ru/@home.texy +++ b/bootstrap/ru/@home.texy @@ -2,78 +2,78 @@ Nette Bootstrap *************** .[perex] -Отдельные компоненты Nette настраиваются с помощью конфигурационных файлов. Мы покажем вам, как загружать эти файлы. +Отдельные компоненты Nette настраиваются через конфигурационные файлы. Мы покажем, как эти файлы загрузить. .[tip] -Если вы используете весь фреймворк, вам не нужно ничего делать. В проекте у вас есть подготовленный каталог `config/` для конфигурационных файлов, и их загрузкой занимается [загрузчик приложения |application:bootstrapping#Конфигурация DI-контейнера]. Эта статья предназначена для пользователей, которые используют только одну библиотеку Nette и хотят воспользоваться возможностями конфигурационных файлов. +Если вы используете весь фреймворк, ничего больше делать не нужно. У вашего проекта есть подготовленный каталог `config/` для конфигурационных файлов, а их загрузкой занимается [загрузчик приложения |application:bootstrapping#Конфигурация DI-контейнера]. Эта статья для тех, кто использует только одну библиотеку Nette и хочет воспользоваться конфигурационными файлами. -Конфигурационные файлы обычно пишутся в [формате NEON|neon:format] и лучше всего редактируются в [редакторах с его поддержкой |best-practices:editors-and-tools#IDE редактор]. Их можно рассматривать как инструкции по **созданию и настройке** объектов. Таким образом, результатом загрузки конфигурации будет так называемая фабрика, то есть объект, который по запросу создаст для нас другие объекты, которые мы хотим использовать. Например, соединение с базой данных и т. д. +Конфигурационные файлы обычно записываются в [формате NEON|neon:format], и лучше всего править их в [редакторах, которые его поддерживают |tools:ide]. Их можно понимать как указания, как **создавать и настраивать** объекты. Так что результатом загрузки конфигурации будет так называемая фабрика - объект, который по требованию создаёт другие объекты, которые вы хотите использовать. Например, соединение с базой данных и т. п. -Эта фабрика также называется *контейнером внедрения зависимостей* (DI container), и если вас интересуют подробности, прочитайте главу о [внедрении зависимостей |dependency-injection:]. +Эту фабрику называют ещё *контейнером внедрения зависимостей* (DI-контейнером), и если вас интересуют подробности, прочитайте главу о [внедрении зависимостей |dependency-injection:]. -Загрузку конфигурации и создание контейнера выполняет класс [api:Nette\Bootstrap\Configurator], поэтому сначала установим его пакет `nette/bootstrap`: +Загрузкой конфигурации и созданием контейнера занимается класс [api:Nette\Bootstrap\Configurator], поэтому сначала мы установим его пакет `nette/bootstrap`: ```shell composer require nette/bootstrap ``` -И создадим экземпляр класса `Configurator`. Поскольку сгенерированный DI-контейнер будет кешироваться на диск, необходимо указать путь к каталогу, где он будет храниться: +И создадим экземпляр класса `Configurator`. Поскольку порождённый DI-контейнер будет кешироваться на диск, нужно задать путь к каталогу, куда он будет сохраняться: ```php $configurator = new Nette\Bootstrap\Configurator; $configurator->setTempDirectory(__DIR__ . '/temp'); ``` -В Linux или macOS установите для каталога `temp/` [права на запись |nette:troubleshooting#Настройка прав доступа к каталогам]. +В Linux или macOS задайте каталогу `temp/` [права на запись |nette:troubleshooting#Задание прав на каталоги]. -И мы подходим к самим конфигурационным файлам. Мы загружаем их с помощью `addConfig()`: +Теперь перейдём к самим конфигурационным файлам. Мы загружаем их через `addConfig()`: ```php $configurator->addConfig(__DIR__ . '/database.neon'); ``` -Если мы хотим добавить несколько конфигурационных файлов, мы можем вызвать функцию `addConfig()` несколько раз. Если в файлах появятся элементы с одинаковыми ключами, они будут перезаписаны (или в случае массивов [объединены |dependency-injection:configuration#Слияние]). Файл, вставленный позже, имеет более высокий приоритет, чем предыдущий. +Если вы хотите добавить больше конфигурационных файлов, функцию `addConfig()` можно вызвать несколько раз. Если в файлах окажутся элементы с одинаковыми ключами, они будут перезаписаны (или [объединены |dependency-injection:configuration#Слияние] в случае массивов). Файл, добавленный позже, имеет более высокий приоритет, чем предыдущий. -Последний шаг — создание DI-контейнера: +Последний шаг - создание DI-контейнера: ```php $container = $configurator->createContainer(); ``` -И он уже создаст для нас нужные объекты. Например, если вы используете конфигурацию для [Nette Database|database:configuration], вы можете попросить его создать соединения с базой данных: +И он создаст за нас нужные объекты. Например, если вы используете конфигурацию для [Nette Database|database:configuration], вы можете попросить его создать соединения с базой данных: ```php $db = $container->getByType(Nette\Database\Connection::class); -// или +// либо $explorer = $container->getByType(Nette\Database\Explorer::class); -// или при создании нескольких соединений +// либо при создании нескольких соединений $db = $container->getByName('database.main.connection'); ``` И теперь вы можете работать с базой данных! -Режим разработки vs production ------------------------------- +Режим разработки и продакшн-режим +--------------------------------- -В режиме разработки контейнер автоматически обновляется при каждом изменении конфигурационных файлов. В production-режиме он генерируется только один раз, и изменения не проверяются. Таким образом, режим разработки ориентирован на максимальное удобство программиста, а production — на производительность и развертывание. +В режиме разработки контейнер автоматически обновляется всякий раз, когда меняются конфигурационные файлы. В продакшн-режиме он порождается только один раз, и изменения не проверяются. Так что режим разработки нацелен на максимальное удобство программиста, а продакшн-режим - на производительность и боевое развёртывание. -Выбор режима осуществляется автоматически, поэтому обычно нет необходимости что-либо настраивать или переключать вручную. Режим является режимом разработки, если приложение запущено на localhost (т. е. IP-адрес `127.0.0.1` или `::1`) и отсутствует прокси (т. е. его HTTP-заголовок). В противном случае оно работает в production-режиме. +Выбор режима выполняется автоопределением, так что обычно ничего настраивать и переключать вручную не нужно. Режим считается разработческим, если приложение запущено на localhost (то есть IP-адрес `127.0.0.1` или `::1`) и при этом нет прокси (то есть его HTTP-заголовка). Иначе работает продакшн-режим. -Если мы хотим включить режим разработки и в других случаях, например, для программистов, обращающихся с определенного IP-адреса, мы используем `setDebugMode()`: +Если мы хотим включить режим разработки и в других случаях, например для программистов, заходящих с определённого IP-адреса, используйте `setDebugMode()`: ```php $configurator->setDebugMode('23.75.345.200'); -// можно также указать массив IP-адресов +// можно указать и массив IP-адресов ``` -Мы настоятельно рекомендуем сочетать IP-адрес с cookie. В cookie `nette-debug` мы сохраняем секретный токен, например, `secret1234`, и таким образом активируем режим разработки для программистов, обращающихся с определенного IP-адреса и одновременно имеющих указанный токен в cookie: +Мы настоятельно рекомендуем сочетать IP-адрес с cookie. В cookie `nette-debug` сохраните секретный токен, например `secret1234`. Так вы включите режим разработки для программистов, заходящих с определённого IP-адреса и имеющих в cookie этот токен: ```php $configurator->setDebugMode('secret1234@23.75.345.200'); ``` -Мы также можем полностью отключить режим разработки, даже для localhost: +Режим разработки можно и полностью отключить, даже для localhost: ```php $configurator->setDebugMode(false); @@ -83,9 +83,9 @@ $configurator->setDebugMode(false); Параметры --------- -В конфигурационных файлах вы также можете использовать параметры, которые определяются [в разделе `parameters` |dependency-injection:configuration#Параметры]. +В конфигурационных файлах можно использовать и параметры, которые определяются [в секции `parameters` |dependency-injection:configuration#Параметры]. -Их также можно вставлять извне с помощью метода `addDynamicParameters()`: +Их можно вставить и снаружи методом `addDynamicParameters()`: ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -На параметр `projectId` можно ссылаться в конфигурации с помощью записи `%projectId%`. +На параметр `remoteIp` можно сослаться в конфигурации записью `%remoteIp%`. + + +Если вы переходите на более новую версию, посмотрите страницу [обновления |upgrading]. diff --git a/bootstrap/ru/@left-menu.texy b/bootstrap/ru/@left-menu.texy new file mode 100644 index 0000000000..8b44f1f22d --- /dev/null +++ b/bootstrap/ru/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 61577d6323..7f329adfce 100644 --- a/bootstrap/ru/@meta.texy +++ b/bootstrap/ru/@meta.texy @@ -1,2 +1 @@ {{sitename: Документация Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/ru/upgrading.texy b/bootstrap/ru/upgrading.texy new file mode 100644 index 0000000000..7972806304 --- /dev/null +++ b/bootstrap/ru/upgrading.texy @@ -0,0 +1,18 @@ +Обновление +********** + + +Обновление до версии 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/sl/@home.texy b/bootstrap/sl/@home.texy deleted file mode 100644 index 76641a1cce..0000000000 --- a/bootstrap/sl/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Posamezne komponente Nette nastavljamo s pomočjo konfiguracijskih datotek. Pokazali bomo, kako te datoteke nalagati. - -.[tip] -Če uporabljate celotno ogrodje, ni treba storiti ničesar dodatnega. V projektu imate za konfiguracijske datoteke pripravljen imenik `config/` in za njihovo nalaganje skrbi [zavajalec aplikacije |application:bootstrapping#Konfiguracija DI vsebnika]. Ta članek je za uporabnike, ki uporabljajo samo eno knjižnico Nette in želijo izkoristiti možnosti konfiguracijskih datotek. - -Konfiguracijske datoteke se običajno pišejo v [formatu NEON|neon:format] in se najbolje urejajo v [urejevalnikih z njegovo podporo |best-practices:editors-and-tools#IDE urejevalnik]. Lahko jih razumemo kot navodila, kako **ustvarjati in konfigurirati** objekte. Torej bo rezultat nalaganja konfiguracije tako imenovana tovarna, kar je objekt, ki nam na zahtevo ustvari druge objekte, ki jih želimo uporabljati. Na primer povezavo s podatkovno bazo itd. - -Tej tovarni se tudi reče *dependency injection vsebnik* (DI vsebnik) in če vas zanimajo podrobnosti, preberite poglavje o [dependency injection |dependency-injection:]. - -Nalaganje konfiguracije in ustvarjanje vsebnika opravi razred [api:Nette\Bootstrap\Configurator], zato najprej namestimo njegov paket `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -In ustvarimo instanco razreda `Configurator`. Ker se bo generirani DI vsebnik predpomnil na disk, je treba nastaviti pot do imenika, kamor se bo shranjeval: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Na Linuxu ali macOS nastavite imeniku `temp/` [pravice za pisanje |nette:troubleshooting#Nastavitev pravic map]. - -In pridemo do samih konfiguracijskih datotek. Te naložimo s pomočjo `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Če želimo dodati več konfiguracijskih datotek, lahko funkcijo `addConfig()` pokličemo večkrat. Če se v datotekah pojavijo elementi z enakimi ključi, bodo prepisani (ali v primeru polj [združeni |dependency-injection:configuration#Združevanje]). Kasneje vstavljena datoteka ima višjo prioriteto kot prejšnja. - -Zadnji korak je ustvarjanje DI vsebnika: - -```php -$container = $configurator->createContainer(); -``` - -In ta nam bo že ustvaril zahtevane objekte. Če na primer uporabljate konfiguracijo za [Nette Database|database:configuration], ga lahko prosite za ustvarjanje povezav s podatkovno bazo: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// ali -$explorer = $container->getByType(Nette\Database\Explorer::class); -// ali pri ustvarjanju več povezav -$db = $container->getByName('database.main.connection'); -``` - -In zdaj lahko že delate s podatkovno bazo! - - -Razvojni vs produkcijski način ------------------------------- - -V razvojnem načinu se vsebnik samodejno posodablja ob vsaki spremembi konfiguracijskih datotek. V produkcijskem načinu se generira samo enkrat in spremembe se ne preverjajo. Razvojni je torej usmerjen v maksimalno udobje programerja, produkcijski pa v zmogljivost in ostro uvajanje. - -Izbira načina se izvaja s samodejnim zaznavanjem, zato običajno ni treba ničesar konfigurirati ali ročno preklapljati. Način je razvojni takrat, ko je aplikacija zagnana na localhostu (tj. IP naslov `127.0.0.1` ali `::1`) in ni prisotna proxy (tj. njena HTTP glava). Sicer teče v produkcijskem načinu. - -Če želimo razvojni način omogočiti tudi v drugih primerih, na primer programerjem, ki dostopajo iz določenega IP naslova, uporabimo `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// lahko se navede tudi polje IP naslovov -``` - -Vsekakor priporočamo kombiniranje IP naslova s piškotkom. V piškotek `nette-debug` shranimo skrivni žeton, npr. `secret1234`, in na ta način aktiviramo razvojni način za programerje, ki dostopajo iz določenega IP naslova in hkrati imajo v piškotku omenjeni žeton: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Razvojni način lahko tudi popolnoma izklopimo, tudi za localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parametri ---------- - -V konfiguracijskih datotekah lahko uporabljate tudi parametre, ki se definirajo [v sekciji `parameters` |dependency-injection:configuration#Parametri]. - -Lahko jih vstavljate tudi od zunaj s pomočjo metode `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Na parameter `projectId` se lahko v konfiguraciji sklicujete z zapisom `%projectId%`. diff --git a/bootstrap/sl/@meta.texy b/bootstrap/sl/@meta.texy deleted file mode 100644 index 282883a3d6..0000000000 --- a/bootstrap/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Dokumentacija}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/tr/@home.texy b/bootstrap/tr/@home.texy index 732122fed8..0f97f854c5 100644 --- a/bootstrap/tr/@home.texy +++ b/bootstrap/tr/@home.texy @@ -2,78 +2,78 @@ Nette Bootstrap *************** .[perex] -Nette'nin bireysel bileşenlerini yapılandırma dosyaları kullanarak ayarlıyoruz. Bu dosyaların nasıl yükleneceğini göstereceğiz. +Tek tek Nette bileşenleri yapılandırma dosyalarıyla ayarlanır. Bu dosyaların nasıl yükleneceğini göstereceğiz. .[tip] -Eğer tüm framework'ü kullanıyorsanız, başka bir şey yapmanıza gerek yoktur. Projenizde yapılandırma dosyaları için önceden hazırlanmış bir `config/` dizini bulunur ve bunların yüklenmesinden [uygulama yükleyicisi |application:bootstrapping#DI Konteyner Yapılandırması] sorumludur. Bu makale, yalnızca bir Nette kütüphanesi kullanan ve yapılandırma dosyalarının olanaklarından yararlanmak isteyen kullanıcılar içindir. +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 formatında|neon:format] yazılır ve en iyi şekilde [destekleyen düzenleyicilerde |best-practices:editors-and-tools#IDE Editörü] düzenlenir. Bunları, nesnelerin **nasıl oluşturulacağı ve yapılandırılacağı** konusunda talimatlar olarak düşünebiliriz. Yani, yapılandırmanın yüklenmesinin sonucu, istek üzerine kullanmak istediğimiz diğer nesneleri (örneğin, veritabanı bağlantısı vb.) oluşturacak olan fabrika olarak adlandırılan bir nesne olacaktır. +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 fabrikaya aynı zamanda *dependency injection konteyneri* (DI konteyneri) denir ve ayrıntılarla ilgileniyorsanız, [dependency injection |dependency-injection:] bölümünü okuyun. +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üklenmesi ve konteynerin oluşturulması [api:Nette\Bootstrap\Configurator] sınıfı tarafından gerçekleştirilir, bu nedenle önce `nette/bootstrap` paketini kuracağız: +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şturacağız. Oluşturulan DI konteyneri diske önbelleğe alınacağından, kaydedileceği dizinin yolunu ayarlamak gerekir: +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 veya macOS'ta, `temp/` dizinine [yazma izinleri |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. +Linux ya da macOS'ta `temp/` dizini için [yazma izinlerini |nette:troubleshooting#Dizin İzinlerini Ayarlama] ayarlayın. -Ve yapılandırma dosyalarına geliyoruz. Bunları `addConfig()` kullanarak yükleyeceğiz: +Ş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 istiyorsak, `addConfig()` fonksiyonunu birden çok kez çağırabiliriz. Dosyalarda aynı anahtarlara sahip öğeler görünürse, bunlar üzerine yazılır (veya diziler durumunda [birleştirilir |dependency-injection:configuration#Birleştirme]). Daha sonra eklenen dosya, öncekinden daha yüksek önceliğe sahiptir. +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 konteynerini oluşturmaktır: +Son adım DI container'ı oluşturmaktır: ```php $container = $configurator->createContainer(); ``` -Ve bu bize istenen nesneleri oluşturacaktır. Örneğin, [Nette Database|database:configuration] için yapılandırma kullanıyorsanız, veritabanı bağlantıları oluşturmasını isteyebilirsiniz: +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); -// veya +// ya da $explorer = $container->getByType(Nette\Database\Explorer::class); -// veya birden fazla bağlantı oluştururken +// ya da birden çok bağlantı oluşturulurken $db = $container->getByName('database.main.connection'); ``` -Ve şimdi veritabanıyla çalışabilirsiniz! +Ve artık veritabanıyla çalışabilirsiniz! -Geliştirme vs Üretim Modu -------------------------- +Geliştirme Kipi ile Üretim Kipi +------------------------------- -Geliştirme modunda, konteyner yapılandırma dosyaları her değiştiğinde otomatik olarak güncellenir. Üretim modunda, yalnızca bir kez oluşturulur ve değişiklikler kontrol edilmez. Bu nedenle geliştirme modu, programcının maksimum rahatlığına odaklanırken, üretim modu performansa ve canlı dağıtıma odaklanır. +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. -Mod seçimi otomatik algılama ile yapılır, bu nedenle genellikle bir şey yapılandırmaya veya manuel olarak değiştirmeye gerek yoktur. Uygulama localhost'ta (yani IP adresi `127.0.0.1` veya `::1`) çalıştırılıyorsa ve bir proxy mevcut değilse (yani HTTP başlığı yoksa) mod geliştirme modudur. Aksi takdirde, üretim modunda çalışı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 modunu diğer durumlarda da etkinleştirmek istiyorsak, örneğin belirli bir IP adresinden erişen programcılar için `setDebugMode()` kullanırız: +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'); -// IP adresleri dizisi de belirtilebilir +// bir IP adresleri dizisi de belirtilebilir ``` -Kesinlikle bir IP adresini bir çerezle birleştirmenizi öneririz. `nette-debug` çerezine gizli bir belirteç, örneğin `secret1234` kaydedeceğiz ve bu şekilde belirli bir IP adresinden erişen ve aynı zamanda çerezde belirtilen belirtece sahip olan programcılar için geliştirme modunu etkinleştireceğiz: +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 modunu localhost için bile tamamen devre dışı bırakabiliriz: +Geliştirme kipini, localhost için bile tümüyle kapatabiliriz: ```php $configurator->setDebugMode(false); @@ -85,7 +85,7 @@ Parametreler Yapılandırma dosyalarında, [`parameters` bölümünde |dependency-injection:configuration#Parametreler] tanımlanan parametreleri de kullanabilirsiniz. -Bunlar ayrıca `addDynamicParameters()` yöntemi kullanılarak dışarıdan da eklenebilir: +Onlar `addDynamicParameters()` metoduyla dışarıdan da eklenebilir: ```php $configurator->addDynamicParameters([ @@ -93,4 +93,7 @@ $configurator->addDynamicParameters([ ]); ``` -`projectId` parametresine yapılandırmada `%projectId%` yazılarak başvurulabilir. +`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 new file mode 100644 index 0000000000..858f95fcdf --- /dev/null +++ b/bootstrap/tr/@left-menu.texy @@ -0,0 +1,13 @@ +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 index e5c5cea355..8dfe82f311 100644 --- a/bootstrap/tr/@meta.texy +++ b/bootstrap/tr/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokümantasyonu}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/tr/upgrading.texy b/bootstrap/tr/upgrading.texy new file mode 100644 index 0000000000..1af0aaba79 --- /dev/null +++ b/bootstrap/tr/upgrading.texy @@ -0,0 +1,18 @@ +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/bootstrap/uk/@home.texy b/bootstrap/uk/@home.texy deleted file mode 100644 index 1320bdf9fa..0000000000 --- a/bootstrap/uk/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Окремі компоненти Nette налаштовуються за допомогою конфігураційних файлів. Ми покажемо, як завантажувати ці файли. - -.[tip] -Якщо ви використовуєте весь фреймворк, нічого додаткового робити не потрібно. У проекті є підготовлений каталог `config/` для конфігураційних файлів, а за їх завантаження відповідає [завантажувач застосунку |application:bootstrapping#Конфігурація DI-контейнера]. Ця стаття призначена для користувачів, які використовують лише одну бібліотеку Nette і хочуть скористатися можливостями конфігураційних файлів. - -Конфігураційні файли зазвичай записуються у [форматі NEON|neon:format] і найкраще редагуються в [редакторах з його підтримкою |best-practices:editors-and-tools#IDE редактор]. Їх можна розглядати як інструкції щодо **створення та конфігурації** об'єктів. Отже, результатом завантаження конфігурації буде так звана фабрика, тобто об'єкт, який за запитом створить для нас інші об'єкти, які ми хочемо використовувати. Наприклад, з'єднання з базою даних тощо. - -Ця фабрика також називається *dependency injection контейнером* (DI container), і якщо вас цікавлять подробиці, прочитайте розділ про [dependency injection |dependency-injection:]. - -Завантаження конфігурації та створення контейнера забезпечує клас [api:Nette\Bootstrap\Configurator], тому спочатку встановимо його пакет `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -І створимо екземпляр класу `Configurator`. Оскільки згенерований DI-контейнер буде кешуватися на диск, необхідно вказати шлях до каталогу, де він буде зберігатися: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -На Linux або macOS встановіть для каталогу `temp/` [права на запис |nette:troubleshooting#Налаштування прав доступу до каталогів]. - -І ми підходимо до самих конфігураційних файлів. Їх завантажуємо за допомогою `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Якщо ми хочемо додати більше конфігураційних файлів, можемо викликати функцію `addConfig()` кілька разів. Якщо у файлах з'являться елементи з однаковими ключами, вони будуть перезаписані (або у випадку масивів [об'єднані |dependency-injection:configuration#Об єднання]). Файл, вставлений пізніше, має вищий пріоритет, ніж попередній. - -Останнім кроком є створення DI-контейнера: - -```php -$container = $configurator->createContainer(); -``` - -І він уже створить для нас необхідні об'єкти. Наприклад, якщо ви використовуєте конфігурацію для [Nette Database|database:configuration], ви можете попросити його створити з'єднання з базою даних: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// або -$explorer = $container->getByType(Nette\Database\Explorer::class); -// або при створенні кількох з'єднань -$db = $container->getByName('database.main.connection'); -``` - -І тепер ви можете працювати з базою даних! - - -Режим розробки проти робочого режиму ------------------------------------- - -У режимі розробки контейнер автоматично оновлюється при кожній зміні конфігураційних файлів. У робочому режимі він генерується лише один раз, і зміни не перевіряються. Отже, режим розробки орієнтований на максимальну зручність програміста, а робочий — на швидкодію та розгортання. - -Вибір режиму здійснюється автовизначенням, тому зазвичай не потрібно нічого конфігурувати або вручну перемикати. Режим є розробницьким, якщо застосунок запущено на localhost (тобто IP-адреса `127.0.0.1` або `::1`) і немає проксі (тобто його HTTP-заголовка). В іншому випадку він працює в робочому режимі. - -Якщо ми хочемо увімкнути режим розробки і в інших випадках, наприклад, для програмістів, які підключаються з конкретної IP-адреси, використовуємо `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// можна також вказати масив IP-адрес -``` - -Ми наполегливо рекомендуємо поєднувати IP-адресу з cookie. У cookie `nette-debug` збережемо секретний токен, наприклад, `secret1234`, і таким чином активуємо режим розробки для програмістів, які підключаються з конкретної IP-адреси і водночас мають зазначений токен у cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Режим розробки можна також повністю вимкнути, навіть для localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Параметри ---------- - -У конфігураційних файлах ви також можете використовувати параметри, які визначаються [у секції `parameters` |dependency-injection:configuration#Параметри]. - -Їх також можна вставляти ззовні за допомогою методу `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -На параметр `projectId` можна посилатися в конфігурації записом `%projectId%`. diff --git a/bootstrap/uk/@meta.texy b/bootstrap/uk/@meta.texy deleted file mode 100644 index 083a8ab9f7..0000000000 --- a/bootstrap/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документація Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/bg/@home.texy b/caching/bg/@home.texy deleted file mode 100644 index 8c04a7e659..0000000000 --- a/caching/bg/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Кешът ускорява вашето приложение, като съхранява данни, които са били трудно получени веднъж, за бъдеща употреба. Ще ви покажем: - -- как да използвате кеша -- как да промените хранилището -- как правилно да инвалидирате кеша - -</div> - -Използването на кеша в Nette е много лесно, като същевременно покрива и много напреднали нужди. Той е проектиран за производителност и 100% устойчивост. В основата му ще намерите адаптери за най-често срещаните бекенд хранилища. Позволява инвалидация, базирана на тагове, изтичане на времето, има защита срещу cache stampede и др. - - -Инсталация -========== - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Основна употреба -================ - -Централният елемент на работата с кеша е обектът [api:Nette\Caching\Cache]. Създаваме негова инстанция и предаваме на конструктора така нареченото хранилище като параметър. Това е обект, представляващ мястото, където данните ще се съхраняват физически (база данни, Memcached, файлове на диска, ...). Достъп до хранилището получаваме, като го поискаме чрез [dependency injection |dependency-injection:passing-dependencies] с тип `Nette\Caching\Storage`. Всичко съществено ще научите в [раздела Хранилища |#Хранилища]. - -.[warning] -Във версия 3.0 интерфейсът все още имаше префикс `I`, така че името беше `Nette\Caching\IStorage`. Освен това константите на класа `Cache` бяха написани с главни букви, така че например `Cache::EXPIRE` вместо `Cache::Expire`. - -За следващите примери да предположим, че имаме създаден псевдоним `Cache` и в променливата `$storage` - хранилище. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // инстанция на Nette\Caching\Storage -``` - -Кешът е всъщност *key–value store*, тоест четем и записваме данни под ключове, точно както при асоциативните масиви. Приложенията се състоят от редица независими части и ако всички те използват едно хранилище (представете си една директория на диска), рано или късно ще възникне колизия на ключове. Nette Framework решава проблема, като разделя цялото пространство на именни пространства (поддиректории). Всяка част от програмата използва свое пространство с уникално име и вече не може да възникне колизия. - -Името на пространството се указва като втори параметър на конструктора на класа Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Сега можем да използваме обекта `$cache` за четене и запис в кеша. За двете цели се използва методът `load()`. Първият аргумент е ключът, а вторият е PHP callback, който се извиква, когато ключът не е намерен в кеша. Callback генерира стойността, връща я и тя се записва в кеша: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // сложно изчисление - return $computedValue; -}); -``` - -Ако вторият параметър не е указан `$value = $cache->load($key)`, ще се върне `null`, ако елементът не е в кеша. - -.[tip] -Страхотно е, че в кеша могат да се съхраняват всякакви сериализуеми структури, не само низове. Същото важи дори и за ключовете. - -Изтриваме елемент от кеша с метода `remove()`: - -```php -$cache->remove($key); -``` - -Записването на елемент в кеша може да се извърши и с метода `$cache->save($key, $value, array $dependencies = [])`. Предпочитаният начин обаче е горепосоченият чрез `load()`. - - -Мемоизация -========== - -Мемоизацията означава кеширане на резултата от извикване на функция или метод, така че да можете да го използвате следващия път, без да изчислявате същото нещо отново и отново. - -Методи и функции могат да бъдат извиквани мемоизирано с помощта на `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Функцията `gethostbyaddr()` се извиква само веднъж за всеки параметър `$ip`, а следващия път стойността се връща от кеша. - -Също така е възможно да се създаде мемоизирана обвивка над метод или функция, която може да бъде извикана по-късно: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // изчислява за първи път -$result = $memoizedFactorial(5); // втори път от кеша -``` - - -Изтичане & инвалидация -====================== - -При съхраняването в кеш е необходимо да се реши въпросът кога по-рано съхранените данни стават невалидни. Nette Framework предлага механизъм за ограничаване на валидността на данните или за тяхното контролирано изтриване (в терминологията на framework-а „инвалидиране“). - -Валидността на данните се задава в момента на записване чрез третия параметър на метода `save()`, напр.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Или чрез параметъра `$dependencies`, предаден по референция към callback-а на метода `load()`, напр.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Или чрез 3-тия параметър в метода `load()`, напр: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -В следващите примери ще предположим втория вариант и следователно съществуването на променливата `$dependencies`. - - -Изтичане --------- - -Най-простото изтичане е времевият лимит. По този начин съхраняваме данни в кеша с валидност 20 минути: - -```php -// приема също брой секунди или UNIX timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Ако искаме да удължим срока на валидност при всяко четене, това може да се постигне по следния начин, но внимавайте, режийните разходи на кеша ще се увеличат: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Удобна е възможността данните да изтекат в момента, в който се промени файл или някой от няколко файла. Това може да се използва например при съхраняване на данни, възникнали при обработката на тези файлове, в кеша. Използвайте абсолютни пътища. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// или -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Можем да накараме елемент в кеша да изтече в момента, в който изтече друг елемент (или някой от няколко други). Това може да се използва, когато съхраняваме в кеша например цяла HTML страница и под други ключове нейните фрагменти. Щом фрагментът се промени, цялата страница се инвалидира. Ако фрагментите са съхранени под ключове напр. `frag1` и `frag2`, използваме: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Изтичането може да се контролира и с помощта на персонализирани функции или статични методи, които при всяко четене решават дали елементът е все още валиден. По този начин например можем да накараме елемент да изтече винаги, когато се промени версията на PHP. Създаваме функция, която сравнява текущата версия с параметъра, и при записване добавяме към зависимостите масив във формат `[име на функция, ...аргументи]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // изтече, когато checkPhpVersion(...) === false -]; -``` - -Всички критерии, разбира се, могат да се комбинират. Кешът тогава изтича, когато поне един критерий не е изпълнен. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Инвалидация чрез тагове ------------------------ - -Много полезен инструмент за инвалидация са така наречените тагове. Към всеки елемент в кеша можем да присвоим списък с тагове, които са произволни низове. Да вземем например HTML страница със статия и коментари, която ще кешираме. При записване посочваме таговете: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Да се преместим в администрацията. Тук намираме форма за редактиране на статия. Заедно със записването на статията в базата данни извикваме командата `clean()`, която изтрива от кеша елементи според тага: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -По същия начин, на мястото на добавяне на нов коментар (или редактиране на коментар) не забравяме да инвалидираме съответния таг: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Какво постигнахме с това? Че HTML кешът ще се инвалидира (изтрива), когато статията или коментарите се променят. Когато се редактира статия с ID = 10, се извършва принудителна инвалидация на тага `article/10` и HTML страницата, която носи посочения таг, се изтрива от кеша. Същото се случва и при вмъкване на нов коментар под съответната статия. - -.[note] -Таговете изискват така наречения [#Journal]. - - -Инвалидация чрез приоритет --------------------------- - -На отделните елементи в кеша можем да зададем приоритет, с който ще може да ги изтриваме, когато например кешът надхвърли определен размер: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Изтриваме всички елементи с приоритет равен или по-малък от 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Приоритетите изискват така наречения [#Journal]. - - -Изтриване на кеша ------------------ - -Параметърът `Cache::All` изтрива всичко: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Групово четене -============== - -За групово четене и запис в кеша се използва методът `bulkLoad()`, на който предаваме масив от ключове и получаваме масив от стойности: - -```php -$values = $cache->bulkLoad($keys); -``` - -Методът `bulkLoad()` работи подобно на `load()` и с втория параметър callback, на който се предава ключът на генерирания елемент: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // сложно изчисление - return $computedValue; -}); -``` - - -Използване с PSR-16 .{data-version:3.3.1} -========================================= - -За използване на Nette Cache с интерфейса PSR-16 можете да използвате адаптера `PsrCacheAdapter`. Той позволява безпроблемна интеграция между Nette Cache и всеки код или библиотека, която очаква PSR-16 съвместим кеш. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Сега можете да използвате `$psrCache` като PSR-16 кеш: - -```php -$psrCache->set('key', 'value', 3600); // съхранява стойността за 1 час -$value = $psrCache->get('key', 'default'); -``` - -Адаптерът поддържа всички методи, дефинирани в PSR-16, включително `getMultiple()`, `setMultiple()` и `deleteMultiple()`. - - -Кеширане на изхода -================== - -Много елегантно може да се улавя и кешира изходът: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // изписваме данни - - $capture->end(); // записваме изхода в кеша -} -``` - -В случай, че изходът вече е съхранен в кеша, методът `capture()` го изписва и връща `null`, така че условието не се изпълнява. В противен случай започва да улавя изхода и връща обект `$capture`, с помощта на който накрая записваме изписаните данни в кеша. - -.[note] -Във версия 3.0 методът се наричаше `$cache->start()`. - - -Кеширане в Latte -================ - -Кеширането в шаблоните [Latte|latte:] е много лесно, достатъчно е част от шаблона да се обвие в тагове `{cache}...{/cache}`. Кешът се инвалидира автоматично в момента, в който се промени изходният шаблон (включително евентуални включени шаблони вътре в кеш блока). Таговете `{cache}` могат да се влагат един в друг и когато вложен блок се инвалидира (например с таг), се инвалидира и родителският блок. - -В тага е възможно да се посочат ключове, към които ще се обвърже кешът (тук променливата `$id`) и да се зададе изтичане и [тагове за инвалидация |#Инвалидация чрез тагове] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Всички елементи са незадължителни, така че не е необходимо да посочваме нито изтичане, нито тагове, нито дори ключове. - -Използването на кеша може да бъде обусловено и с помощта на `if` - съдържанието тогава ще се кешира само ако условието е изпълнено: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Хранилища -========= - -Хранилището е обект, представляващ мястото, където данните се съхраняват физически. Можем да използваме база данни, сървър Memcached или най-достъпното хранилище, което са файлове на диска. - -|----------------- -| Хранилище | Описание -|----------------- -| [#FileStorage] | хранилище по подразбиране със съхранение във файлове на диска -| [#MemcachedStorage] | използва `Memcached` сървър -| [#MemoryStorage] | данните са временно в паметта -| [#SQLiteStorage] | данните се съхраняват в SQLite база данни -| [#DevNullStorage] | данните не се съхраняват, подходящо за тестване - -Достъп до обекта на хранилището получавате, като го поискате чрез [dependency injection |dependency-injection:passing-dependencies] с тип `Nette\Caching\Storage`. Като хранилище по подразбиране Nette предоставя обект FileStorage, съхраняващ данни в поддиректория `cache` в директорията за [временни файлове |application:bootstrapping#Временни файлове]. - -Можете да промените хранилището в конфигурацията: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Записва кеша във файлове на диска. Хранилището `Nette\Caching\Storages\FileStorage` е много добре оптимизирано за производителност и преди всичко осигурява пълна атомарност на операциите. Какво означава това? Че при използване на кеша не може да се случи да прочетем файл, който все още не е напълно записан от друг поток, или някой да го изтрие "под носа ни". Използването на кеша е напълно безопасно. - -Това хранилище има и вградена важна функция, която предотвратява екстремно нарастване на използването на CPU в момента, когато кешът се изтрие или все още не е загрят (т.е. създаден). Това е превенция срещу "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Случва се в един момент да се съберат по-голям брой едновременни заявки, които искат от кеша едно и също нещо (например резултат от скъпа SQL заявка) и тъй като то не е в кеша, всички процеси започват да изпълняват същата SQL заявка. Натоварването се умножава и дори може да се случи нито един поток да не успее да отговори в рамките на времевия лимит, кешът да не се създаде и приложението да се срине. За щастие, кешът в Nette работи така, че при повече едновременни заявки за един елемент, той се генерира само от първия поток, останалите чакат и след това използват генерирания резултат. - -Пример за създаване на FileStorage: - -```php -// хранилището ще бъде директория '/path/to/temp' на диска -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Сървърът [Memcached|https://memcached.org] е високопроизводителна система за съхранение в разпределена памет, чийто адаптер е `Nette\Caching\Storages\MemcachedStorage`. В конфигурацията посочваме IP адрес и порт, ако се различават от стандартния 11211. - -.[caution] -Изисква PHP разширение `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` е хранилище, което съхранява данни в PHP масив, и следователно те се губят с прекратяването на заявката. - - -SQLiteStorage -------------- - -Базата данни SQLite и адаптерът `Nette\Caching\Storages\SQLiteStorage` предлагат начин за съхраняване на кеша в един файл на диска. В конфигурацията посочваме пътя до този файл. - -.[caution] -Изисква PHP разширения `pdo` и `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Специална имплементация на хранилище е `Nette\Caching\Storages\DevNullStorage`, което всъщност изобщо не съхранява данни. Подходящо е за тестване, когато искаме да елиминираме влиянието на кеша. - - -Използване на кеша в кода -========================= - -При използване на кеша в кода имаме два начина да го направим. Първият е да поискаме хранилището чрез [dependency injection |dependency-injection:passing-dependencies] и да създадем обект `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Втората възможност е директно да поискаме обект `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Обектът `Cache` след това се създава директно в конфигурацията по следния начин: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette съхранява тагове и приоритети в така наречения journal. Стандартно за това се използва SQLite и файл `journal.s3db` и **се изискват PHP разширения `pdo` и `pdo_sqlite`.** - -Можете да промените journal-а в конфигурацията: - -```neon -services: - cache.journal: MyJournal -``` - - -DI Сървиси -========== - -Тези сървиси се добавят към DI контейнера: - -| Име | Тип | Описание -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | хранилище - - -Изключване на кеша -================== - -Една от възможностите за изключване на кеша в приложението е да се зададе като хранилище [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Тази настройка не влияе на кеширането на шаблони в Latte или DI контейнера, тъй като тези библиотеки не използват услугите на nette/caching и управляват кеша си самостоятелно. Техният кеш впрочем [не е необходимо да се изключва |nette:troubleshooting#Как да изключите кеша по време на разработка] в режим на разработка. diff --git a/caching/bg/@meta.texy b/caching/bg/@meta.texy deleted file mode 100644 index 794cbc8522..0000000000 --- a/caching/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/cs/@home.texy b/caching/cs/@home.texy index 17ddcaa49f..79c370bad2 100644 --- a/caching/cs/@home.texy +++ b/caching/cs/@home.texy @@ -27,7 +27,7 @@ composer require nette/caching Základní použití ================ -Středobodem práce s cache neboli mezipamětí představuje objekt [api:Nette\Caching\Cache]. Vytvoříme si jeho instanci a jako parametr předáme konstruktoru tzv. úložiště. Což je objekt reprezentující místo, kam se budou data fyzicky ukládat (databáze, Memcached, soubory na disku, ...). K úložišti se dostaneme tak, že si jej necháme předat pomocí [dependency injection |dependency-injection:passing-dependencies] s typem `Nette\Caching\Storage`. Vše podstatné se dozvíte v [části Úložiště |#Úložiště]. +Středobodem práce s cache neboli mezipamětí je objekt [api:Nette\Caching\Cache]. Vytvoříme si jeho instanci a jako parametr předáme konstruktoru tzv. úložiště. Což je objekt reprezentující místo, kam se budou data fyzicky ukládat (databáze, Memcached, soubory na disku, ...). K úložišti se dostaneme tak, že si jej necháme předat pomocí [dependency injection |dependency-injection:passing-dependencies] s typem `Nette\Caching\Storage`. Vše podstatné se dozvíte v [části Úložiště |#Úložiště]. .[warning] Ve verzi 3.0 mělo rozhraní ještě prefix `I`, takže název byl `Nette\Caching\IStorage`. A dále konstanty třídy `Cache` byly psané velkými písmeny, takže třeba `Cache::EXPIRE` místo `Cache::Expire`. @@ -40,7 +40,7 @@ use Nette\Caching\Cache; $storage = /* ... */; // instance of Nette\Caching\Storage ``` -Cache je vlastně *key–value store*, tedy data čteme a zapisujeme pod klíči stejně jako u asociativních polí. Aplikace se skládají z řady nezávislých částí a pokud všechny budou používat jedno úložiště (představte si jeden adresář na disku), dříve nebo později by došlo ke kolizi klíčů. Nette Framework problém řeší tak, že celý prostor rozděluje na jmenné prostory (podadresáře). Každá část programu pak používá svůj prostor s unikátním názvem a k žádné kolizi již dojít nemůže. +Cache je vlastně *key-value store*, tedy data čteme a zapisujeme pod klíči stejně jako u asociativních polí. Aplikace se skládají z řady nezávislých částí a pokud všechny budou používat jedno úložiště (představte si jeden adresář na disku), dříve nebo později by došlo ke kolizi klíčů. Nette Framework problém řeší tak, že celý prostor rozděluje na jmenné prostory (podadresáře). Každá část programu pak používá svůj prostor s unikátním názvem a k žádné kolizi již dojít nemůže. Název prostoru uvedeme jako druhý parametr konstruktoru třídy Cache: @@ -48,6 +48,12 @@ Název prostoru uvedeme jako druhý parametr konstruktoru třídy Cache: $cache = new Cache($storage, 'Full Html Pages'); ``` +V případě potřeby lze z existující instance odvodit novou cache ve vnořeném podprostoru pomocí metody `derive()`: + +```php +$subCache = $cache->derive('Images'); +``` + Nyní můžeme pomocí objektu `$cache` z mezipaměti číst a zapisovat do ní. K obojímu slouží metoda `load()`. Prvním argumentem je klíč a druhým PHP callback, který se zavolá, když klíč není nalezen v cache. Callback hodnotu vygeneruje, vrátí a ta se uloží do cache: ```php @@ -68,7 +74,7 @@ Položku z mezipaměti vymažeme metodou `remove()`: $cache->remove($key); ``` -Uložit položku do mezipaměti lze také metodou `$cache->save($key, $value, array $dependencies = [])`. Preferovaná je nicméně výše uvedený způsob pomocí `load()`. +Uložit položku do mezipaměti lze také metodou `$cache->save($key, $data, ?array $dependencies = null)`. Preferovaný je nicméně výše uvedený způsob pomocí `load()`. Memoizace @@ -102,9 +108,9 @@ $result = $memoizedFactorial(5); // podruhé z cache Expirace & invalidace ===================== -S ukládáním do cache je potřeba řešit otázku, kdy se dříve uložená data stanou neplatná. Nette Framework nabízí mechanismus, jak omezit platnost dat nebo je řízeně mazat (v terminologii frameworku „invalidovat“). +S ukládáním do cache je potřeba řešit otázku, kdy se dříve uložená data stanou neplatná. Nette Framework nabízí mechanismus, jak omezit platnost dat nebo je řízeně mazat (v terminologii frameworku "invalidovat"). -Platnost dat se nastavuje v okamžiku ukládání a to pomocí třetího parametru metody `save()`, např.: +Platnost dat se nastavuje v okamžiku ukládání, a to pomocí třetího parametru metody `save()`, např.: ```php $cache->save($key, $value, [ @@ -121,11 +127,11 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -Nebo pomocí 3. parametru v metodě `load()`, např: +Nebo pomocí 3. parametru v metodě `load()`, např.: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` @@ -162,7 +168,7 @@ Můžeme nechat položku v cache vyexpirovat ve chvíli, kdy vyexpiruje jiná po $dependencies[Cache::Items] = ['frag1', 'frag2']; ``` -Expiraci lze řídit i pomocí vlastních funkcí nebo statických metod, které vždy při čtení rozhodnou, zda je položka ještě platná. Takto třeba můžeme nechat položku vyexpirovat vždy, když se změní verze PHP. Vytvoříme funkci, která porovná aktuální verzi s parameterem, a při ukládání přidáme mezi závislosti pole ve tvaru `[nazev funkce, ...argumenty]`: +Expiraci lze řídit i pomocí vlastních funkcí nebo statických metod, které vždy při čtení rozhodnou, zda je položka ještě platná. Takto třeba můžeme nechat položku vyexpirovat vždy, když se změní verze PHP. Vytvoříme funkci, která porovná aktuální verzi s parametrem, a při ukládání přidáme mezi závislosti pole ve tvaru `[nazev funkce, ...argumenty]`: ```php function checkPhpVersion($ver): bool @@ -186,13 +192,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; Invalidace pomocí tagů ---------------------- -Velmi užitečným invalidačním nástrojem jsou tzv. tagy. Každé položce v cache můžeme přiřadit seznam tagů, což jsou libovolné řetězce. Mějme třeba HTML stránku s článkem a komentáři, kterou budeme cachovat. Při ukládání specifikujeme tagy: +Velmi užitečným invalidačním nástrojem jsou tzv. tagy. Každé položce v cache můžeme přiřadit seznam tagů, což jsou libovolné řetězce. Mějme třeba HTML stránku s článkem a komentáři, kterou budeme cachovat. Při ukládání uvedeme tagy: ```php $dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; ``` -Přesuňme se do administrace. Tady najdeme formulář pro editaci článku. Společně s uložením článku do databáze zavoláme příkaz `clean()`, který smaže z cache položky dle tagu: +Přesuňme se do administrace. Tady najdeme formulář pro editaci článku. Společně s uložením článku do databáze zavoláme metodu `clean()`, která smaže z cache položky dle tagu: ```php $cache->clean([ @@ -265,11 +271,20 @@ $values = $cache->bulkLoad($keys, function ($key, &$dependencies) { }); ``` +Naopak pro hromadný zápis více položek najednou slouží metoda `bulkSave()`, které předáme pole dvojic `klíč => hodnota` a volitelně závislosti: + +```php +$cache->bulkSave([ + $key1 => $value1, + $key2 => $value2, +], [Cache::Expire => '20 minutes']); +``` + Použití s PSR-16 .{data-version:3.3.1} ====================================== -Pro použití Nette Cache s rozhraním PSR-16 můžete využít adaptér `PsrCacheAdapter`. Umožňuje bezešvou integraci mezi Nette Cache a jakýmkoli kódem nebo knihovnou, která očekává PSR-16 kompatibilní cache. +Pro použití Nette Cache s rozhraním PSR-16 můžete využít adaptér `PsrCacheAdapter`. Umožňuje hladkou integraci mezi Nette Cache a jakýmkoli kódem nebo knihovnou, která očekává PSR-16 kompatibilní cache. ```php $psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); @@ -282,7 +297,7 @@ $psrCache->set('key', 'value', 3600); // uloží hodnotu na 1 hodinu $value = $psrCache->get('key', 'default'); ``` -Adaptér podporuje všechny metody definované v PSR-16, včetně `getMultiple()`, `setMultiple()`, a `deleteMultiple()`. +Adaptér podporuje všechny metody definované v PSR-16, včetně `getMultiple()`, `setMultiple()` a `deleteMultiple()`. Cachování výstupu @@ -293,13 +308,13 @@ Velmi elegantně lze zachytávat a cachovat výstup: ```php if ($capture = $cache->capture($key)) { - echo ... // vypisujeme data + // echo ... vypisujeme data $capture->end(); // uložíme výstup do cache } ``` -V případě, že výstup už je v cache uložen, tak ho metoda `capture()` vypíše a vrátí `null`, tedy podmínka se nevykoná. V opačném případě začne výstup zachytávat a vrátí objekt `$capture`, pomocí něhož nakonec vypsaná data uložíme do cache. +V případě, že výstup už je v cache uložen, metoda `capture()` ho vypíše a vrátí `null`, tedy podmínka se nevykoná. V opačném případě začne výstup zachytávat a vrátí objekt `$capture`, pomocí něhož nakonec vypsaná data uložíme do cache. .[note] Ve verzi 3.0 se metoda jmenovala `$cache->start()`. @@ -308,9 +323,9 @@ Ve verzi 3.0 se metoda jmenovala `$cache->start()`. Cachování v Latte ================= -Cachování v šablonách [Latte|latte:] je velmi snadné, stačí část šablony obalit značkami `{cache}...{/cache}`. Cache se automaticky invaliduje ve chvíli, kdy se změní zdrojová šablona (včetně případných inkludovaných šablon uvnitř bloku cache). Značky `{cache}` lze vnořovat do sebe a když se vnořený blok zneplatní (například tagem), zneplatní se i blok nadřazený. +Cachování v šablonách [Latte|latte:] je velmi snadné, stačí část šablony obalit značkami `{cache}...{/cache}`. Cache se automaticky invaliduje ve chvíli, kdy se změní zdrojová šablona (včetně případných vložených šablon uvnitř bloku cache). Značky `{cache}` lze vnořovat do sebe a když se vnořený blok zneplatní (například tagem), zneplatní se i blok nadřazený. -Ve značce je možné uvést klíče, na které se bude cache vázat (zde proměnná `$id`) a nastavit expiraci a [tagy pro zneplatnění |#Invalidace pomocí tagů] +Ve značce je možné uvést klíče, na které se bude cache vázat (zde proměnná `$id`), nastavit expiraci a [tagy pro zneplatnění |#Invalidace pomocí tagů]. ```latte {cache $id, expire: '20 minutes', tags: [tag1, tag2]} @@ -320,7 +335,7 @@ Ve značce je možné uvést klíče, na které se bude cache vázat (zde promě Všechny položky jsou volitelné, takže nemusíme uvádět ani expiraci, ani tagy, nakonec ani klíče. -Použití cache lze také podmínit pomocí `if` - obsah se pak bude cachovat pouze bude-li splněna podmínka: +Použití cache lze také podmínit pomocí `if` - obsah se pak bude cachovat, pouze pokud bude splněna podmínka: ```latte {cache $id, if: !$form->isSubmitted()} @@ -358,7 +373,7 @@ FileStorage Zapisuje cache do souborů na disku. Úložiště `Nette\Caching\Storages\FileStorage` je velmi dobře optimalizované pro výkon a především zajišťuje plnou atomicitu operací. Co to znamená? Že při použití cache se nemůže stát, že přečteme soubor, který ještě není jiným vláknem kompletně zapsaný, nebo že by vám jej někdo "pod rukama" smazal. Použití cache je tedy zcela bezpečné. -Toto úložiště má také vestavěnou důležitou funkci, která brání před extrémním nárůstem využití CPU ve chvíli, kdy se cache smaže nebo ještě není zahřátá (tj. vytvořená). Jedná se o prevenci před "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Stává se, že v jednu chvíli se sejde větší počet souběžných požadavků, které chtějí z cache stejnou věc (např. výsledek drahého SQL dotazu) a protože v mezipaměti není, začnou všechny procesy vykonávat stejný SQL dotaz. Vytížení se tak násobí a může se dokonce stát, že žádné vlákno nestihne odpovědět v časovém limitu, cache se nevytvoří a aplikace zkolabuje. Naštěstí cache v Nette funguje tak, že při více souběžných požadavcích na jednu položku ji generuje pouze první vlákno, ostatní čekají a následně využíjí vygenerovaný výsledek. +Toto úložiště má také vestavěnou důležitou funkci, která brání před extrémním nárůstem využití CPU ve chvíli, kdy se cache smaže nebo ještě není zahřátá (tj. vytvořená). Jedná se o prevenci před "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Stává se, že v jednu chvíli se sejde větší počet souběžných požadavků, které chtějí z cache stejnou věc (např. výsledek drahého SQL dotazu) a protože v mezipaměti není, začnou všechny procesy vykonávat stejný SQL dotaz. Vytížení se tak násobí a může se dokonce stát, že žádné vlákno nestihne odpovědět v časovém limitu, cache se nevytvoří a aplikace zkolabuje. Naštěstí cache v Nette funguje tak, že při více souběžných požadavcích na jednu položku ji generuje pouze první vlákno, ostatní čekají a následně využijí vygenerovaný výsledek. Příklad vytvoření FileStorage: @@ -385,7 +400,7 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` je úložiště, která data ukládá do PHP pole, a tedy se s ukončením požadavku ztratí. +`Nette\Caching\Storages\MemoryStorage` je úložiště, které data ukládá do PHP pole, a tedy se s ukončením požadavku ztratí. SQLiteStorage @@ -481,4 +496,7 @@ services: cache.storage: Nette\Caching\Storages\DevNullStorage ``` -Toto nastavení nemá vliv na kešování šablon v Latte nebo DI kontejeru, protože tyto knihovny nevyužívají služeb nette/caching a spravují si cache samostatně. Jejich cache ostatně [není potřeba |nette:troubleshooting#Jak vypnout cache během vývoje] ve vývojářském režimu vypínat. +Toto nastavení nemá vliv na kešování šablon v Latte nebo DI kontejneru, protože tyto knihovny nevyužívají služeb nette/caching a spravují si cache samostatně. Jejich cache ostatně [není potřeba |nette:troubleshooting#Jak vypnout cache během vývoje] ve vývojářském režimu vypínat. + + +Pokud aktualizujete balíček na novější verzi, podívejte se na stránku [upgrade|upgrading]. diff --git a/caching/cs/@left-menu.texy b/caching/cs/@left-menu.texy new file mode 100644 index 0000000000..d24d2ac574 --- /dev/null +++ b/caching/cs/@left-menu.texy @@ -0,0 +1,13 @@ +Nette Caching +************* +- [Ú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/caching/cs/@meta.texy b/caching/cs/@meta.texy index 08edde785b..462d9add80 100644 --- a/caching/cs/@meta.texy +++ b/caching/cs/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokumentace}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/cs/upgrading.texy b/caching/cs/upgrading.texy new file mode 100644 index 0000000000..198a5953be --- /dev/null +++ b/caching/cs/upgrading.texy @@ -0,0 +1,20 @@ +Upgrade +******* + + +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)` diff --git a/caching/de/@home.texy b/caching/de/@home.texy index d2c2c5186a..518f599a11 100644 --- a/caching/de/@home.texy +++ b/caching/de/@home.texy @@ -3,21 +3,21 @@ Nette Caching <div class=perex> -Der Cache beschleunigt Ihre Anwendung, indem er einmal aufwendig abgerufene Daten für die zukünftige Verwendung speichert. Wir zeigen Ihnen: +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 man den Cache verwendet -- wie man den Speicher (Storage) ändert -- wie man den Cache korrekt invalidiert +- 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 dennoch auch sehr fortgeschrittene Anforderungen ab. Er ist auf Leistung und 100%ige Stabilität ausgelegt. Standardmäßig finden Sie Adapter für die gängigsten Backend-Speicher. Er ermöglicht die auf Tags basierende Invalidierung, Zeitablauf, Schutz vor Cache Stampede usw. +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 ============ -Sie können die Bibliothek mit dem Werkzeug [Composer|best-practices:composer] herunterladen und installieren: +Laden Sie das Paket mit [Composer|best-practices:composer] herunter und installieren Sie es: ```shell composer require nette/caching @@ -27,12 +27,12 @@ composer require nette/caching Grundlegende Verwendung ======================= -Der zentrale Punkt der Arbeit mit dem Cache oder Zwischenspeicher ist das Objekt [api:Nette\Caching\Cache]. Wir erstellen eine Instanz davon und übergeben dem Konstruktor als Parameter einen sogenannten Speicher (Storage). Dies ist ein Objekt, das den Ort darstellt, an dem die Daten physisch gespeichert werden (Datenbank, Memcached, Dateien auf der Festplatte, ...). Zum Speicher gelangen wir, indem wir ihn uns mittels [Dependency Injection |dependency-injection:passing-dependencies] mit dem Typ `Nette\Caching\Storage` übergeben lassen. Alles Wesentliche erfahren Sie im [Abschnitt Speicher |#Speicher Storage]. +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 war also `Nette\Caching\IStorage`. Außerdem wurden die Konstanten der Klasse `Cache` großgeschrieben, also z.B. `Cache::EXPIRE` statt `Cache::Expire`. +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` erstellt haben und der Speicher in der Variablen `$storage` vorhanden ist. +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; @@ -40,15 +40,21 @@ use Nette\Caching\Cache; $storage = /* ... */; // Instanz von Nette\Caching\Storage ``` -Der Cache ist eigentlich ein *Key-Value-Store*, das heißt, wir lesen und schreiben Daten unter Schlüsseln, genau wie bei assoziativen Arrays. Anwendungen bestehen aus einer Reihe unabhängiger Teile, und wenn alle denselben Speicher verwenden (stellen Sie sich ein Verzeichnis auf der Festplatte vor), würde es früher oder später zu Schlüsselkollisionen kommen. Das Nette Framework löst dieses Problem, indem es den gesamten Speicherplatz in Namensräume (Unterverzeichnisse) aufteilt. Jeder Teil des Programms verwendet dann seinen eigenen Namensraum mit einem eindeutigen Namen, und es kann keine Kollision mehr auftreten. +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 Namensraums geben wir als zweiten Parameter des Konstruktors der Cache-Klasse an: +Den Namen des Raums geben wir als zweiten Parameter des Konstruktors der Klasse `Cache` an: ```php $cache = new Cache($storage, 'Full Html Pages'); ``` -Jetzt können wir mit dem Objekt `$cache` aus dem Cache lesen und schreiben. Für beides dient die Methode `load()`. Das erste Argument ist der Schlüssel und das zweite ein PHP-Callback, der aufgerufen wird, wenn der Schlüssel nicht im Cache gefunden wird. Der Callback generiert den Wert, gibt ihn zurück und dieser wird im Cache gespeichert: +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) { @@ -57,34 +63,34 @@ $value = $cache->load($key, function () use ($key) { }); ``` -Wenn wir den zweiten Parameter nicht angeben `$value = $cache->load($key)`, wird `null` zurückgegeben, wenn das Element nicht im Cache vorhanden ist. +Wenn wir den zweiten Parameter weglassen (`$value = $cache->load($key)`), gibt `load()` `null` zurück, falls der Eintrag nicht im Cache liegt. .[tip] -Das Tolle ist, dass Sie beliebige serialisierbare Strukturen im Cache speichern können, nicht nur Strings. Und dasselbe gilt sogar für die Schlüssel. +Praktisch ist, dass sich beliebige serialisierbare Strukturen im Cache ablegen lassen, nicht nur Strings. Und dasselbe gilt sogar für die Schlüssel. -Ein Element aus dem Cache löschen wir mit der Methode `remove()`: +Einen Eintrag löschen wir mit der Methode `remove()` aus dem Cache: ```php $cache->remove($key); ``` -Ein Element kann auch mit der Methode `$cache->save($key, $value, array $dependencies = [])` im Cache gespeichert werden. Die bevorzugte Methode ist jedoch die oben beschriebene Verwendung von `load()`. +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 Cachen des Ergebnisses eines Funktions- oder Methodenaufrufs, sodass Sie es beim nächsten Mal verwenden können, ohne dasselbe erneut berechnen zu müssen. +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 können memoisiert mit `call(callable $callback, ...$args)` aufgerufen werden: +Methoden und Funktionen lassen sich mit `call(callable $callback, ...$args)` memoisiert aufrufen: ```php $result = $cache->call('gethostbyaddr', $ip); ``` -Die Funktion `gethostbyaddr()` wird somit für jeden Parameter `$ip` nur einmal aufgerufen, und beim nächsten Mal wird der Wert aus dem Cache zurückgegeben. +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 für eine Methode oder Funktion zu erstellen, der später aufgerufen werden kann: +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) @@ -94,7 +100,7 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // berechnet beim ersten Mal +$result = $memoizedFactorial(5); // berechnet es beim ersten Mal $result = $memoizedFactorial(5); // beim zweiten Mal aus dem Cache ``` @@ -102,9 +108,9 @@ $result = $memoizedFactorial(5); // beim zweiten Mal aus dem Cache Ablauf & Invalidierung ====================== -Beim Speichern im Cache muss die Frage geklärt werden, wann die zuvor gespeicherten Daten ungültig werden. Das Nette Framework bietet einen Mechanismus, um die Gültigkeit von Daten zu begrenzen oder sie kontrolliert zu löschen (in der Terminologie des Frameworks „invalidieren“). +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 zum Zeitpunkt des Speicherns festgelegt, und zwar mit dem dritten Parameter der Methode `save()`, z.B.: +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, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -Oder mithilfe des Parameters `$dependencies`, der per Referenz an den Callback der Methode `load()` übergeben wird, z.B.: +Oder über den Parameter `$dependencies`, der dem Callback der Methode `load()` per Referenz übergeben wird, z. B.: ```php $value = $cache->load($key, function (&$dependencies) { @@ -121,34 +127,34 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -Oder mithilfe des 3. Parameters in der Methode `load()`, z.B: +Oder über den 3. Parameter der Methode `load()`, z. B.: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -In den weiteren Beispielen gehen wir von der zweiten Variante und somit der Existenz der Variablen `$dependencies` aus. +In den folgenden Beispielen gehen wir von der zweiten Variante aus, also von der Existenz der Variable `$dependencies`. -Ablauf (Expiration) -------------------- +Ablauf +------ -Der einfachste Ablauf ist ein Zeitlimit. So speichern wir Daten für 20 Minuten im Cache: +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 die Anzahl der Sekunden oder einen UNIX-Zeitstempel +// akzeptiert auch eine Anzahl von Sekunden oder einen UNIX-Timestamp $dependencies[Cache::Expire] = '20 minutes'; ``` -Wenn wir die Gültigkeitsdauer bei jedem Lesevorgang verlängern möchten (Sliding Expiration), kann dies wie folgt erreicht werden, aber Vorsicht, der Overhead des Caches steigt dadurch: +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; ``` -Eine praktische Möglichkeit besteht darin, die Daten verfallen zu lassen, wenn sich eine Datei oder eine von mehreren Dateien ändert. Dies kann beispielsweise beim Speichern von Daten im Cache genutzt werden, die durch die Verarbeitung dieser Dateien entstanden sind. Verwenden Sie absolute Pfade. +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'; @@ -156,13 +162,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; $dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; ``` -Wir können ein Element im Cache verfallen lassen, wenn ein anderes Element (oder eines von mehreren anderen) verfällt. Dies kann nützlich sein, wenn wir beispielsweise eine ganze HTML-Seite im Cache speichern und ihre Fragmente unter anderen Schlüsseln ablegen. Sobald sich ein Fragment ändert, wird die gesamte Seite invalidiert. Wenn die Fragmente beispielsweise unter den Schlüsseln `frag1` und `frag2` gespeichert sind, verwenden wir: +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 kann auch über benutzerdefinierte Funktionen oder statische Methoden gesteuert werden, die bei jedem Lesevorgang entscheiden, ob das Element noch gültig ist. Auf diese Weise können wir beispielsweise ein Element immer dann verfallen lassen, wenn sich die PHP-Version ändert. Wir erstellen eine Funktion, die die aktuelle Version mit dem Parameter vergleicht, und beim Speichern fügen wir unter den Abhängigkeiten ein Array im Format `[Funktionsname, ...Argumente]` hinzu: +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 @@ -171,11 +177,11 @@ function checkPhpVersion($ver): bool } $dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // verfallen lassen, wenn checkPhpVersion(...) === false ist + ['checkPhpVersion', PHP_VERSION_ID] // ablaufen lassen, wenn checkPhpVersion(...) === false ]; ``` -Natürlich können alle Kriterien kombiniert werden. Der Cache verfällt dann, wenn mindestens ein Kriterium nicht erfüllt ist. +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'; @@ -186,13 +192,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; Invalidierung mittels Tags -------------------------- -Ein sehr nützliches Invalidierungswerkzeug sind die sogenannten Tags. Jedem Element im Cache können wir eine Liste von Tags zuweisen, bei denen es sich um beliebige Strings handelt. Nehmen wir zum Beispiel eine HTML-Seite mit einem Artikel und Kommentaren, die wir cachen möchten. Beim Speichern geben wir die Tags an: +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 zur Administration. Hier finden wir ein Formular zur Bearbeitung des Artikels. Zusammen mit dem Speichern des Artikels in der Datenbank rufen wir den Befehl `clean()` auf, der die Cache-Elemente entsprechend dem Tag löscht: +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([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -Ebenso vergessen wir an der Stelle, an der ein neuer Kommentar hinzugefügt (oder ein Kommentar bearbeitet) wird, nicht, den entsprechenden Tag zu invalidieren: +Ebenso vergessen wir dort, wo ein neuer Kommentar hinzugefügt (oder ein Kommentar bearbeitet) wird, nicht, den entsprechenden Tag zu invalidieren: ```php $cache->clean([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -Was haben wir damit erreicht? Dass unser HTML-Cache immer dann invalidiert (gelöscht) wird, wenn sich der Artikel oder die Kommentare ändern. Wenn der Artikel mit der ID = 10 bearbeitet wird, wird der Tag `article/10` zwangsweise invalidiert, und die HTML-Seite, die diesen Tag trägt, wird aus dem Cache gelöscht. Dasselbe geschieht beim Einfügen eines neuen Kommentars unter dem entsprechenden Artikel. +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 das sogenannte [#journal]. +Tags erfordern ein sogenanntes [#Journal]. Invalidierung mittels Priorität ------------------------------- -Einzelnen Elementen im Cache können wir eine Priorität zuweisen, mit der sie gelöscht werden können, wenn der Cache beispielsweise eine bestimmte Größe überschreitet: +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; ``` -Wir löschen alle Elemente mit einer Priorität von 100 oder weniger: +So löschen wir alle Einträge mit einer Priorität gleich oder kleiner als 100: ```php $cache->clean([ @@ -232,7 +238,7 @@ $cache->clean([ ``` .[note] -Prioritäten erfordern das sogenannte [#journal]. +Prioritäten erfordern ein sogenanntes [#Journal]. Löschen des Caches @@ -247,16 +253,16 @@ $cache->clean([ ``` -Massenlesen (Bulk Read) -======================= +Massenlesen +=========== -Für das Massenlesen und -schreiben in den Cache dient die Methode `bulkLoad()`, der wir ein Array von Schlüsseln übergeben und ein Array von Werten erhalten: +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 dem zweiten Parameter, einem Callback, dem der Schlüssel des generierten Elements übergeben wird: +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) { @@ -265,11 +271,20 @@ $values = $cache->bulkLoad($keys, function ($key, &$dependencies) { }); ``` +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} =========================================== -Zur Verwendung von Nette Cache mit der PSR-16-Schnittstelle können Sie den Adapter `Nette\Bridges\Psr\PsrCacheAdapter` nutzen. Er ermöglicht eine nahtlose Integration zwischen Nette Cache und jedem Code oder jeder Bibliothek, die einen PSR-16-kompatiblen Cache erwartet. +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); @@ -288,18 +303,18 @@ Der Adapter unterstützt alle in PSR-16 definierten Methoden, einschließlich `g Caching der Ausgabe =================== -Die Ausgabe kann sehr elegant abgefangen und gecached werden: +Sehr elegant lässt sich die Ausgabe abfangen und cachen: ```php if ($capture = $cache->capture($key)) { - echo ... // wir geben Daten aus + // echo ... wir geben Daten aus - $capture->end(); // wir speichern die Ausgabe im Cache + $capture->end(); // die Ausgabe im Cache speichern } ``` -Falls die Ausgabe bereits im Cache gespeichert ist, gibt die Methode `capture()` sie aus und gibt `null` zurück, sodass die Bedingung nicht ausgeführt wird. Andernfalls beginnt sie mit dem Abfangen der Ausgabe und gibt das Objekt `$capture` zurück, mit dessen Hilfe wir die ausgegebenen Daten schließlich 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()`. @@ -308,9 +323,9 @@ In Version 3.0 hieß die Methode `$cache->start()`. Caching in Latte ================ -Das Caching in [Latte|latte:]-Templates ist sehr einfach, es genügt, einen Teil des Templates mit den Tags `{cache}...{/cache}` zu umschließen. Der Cache wird automatisch invalidiert, sobald sich das Quelltemplate ändert (einschließlich eventuell eingebundener Templates innerhalb des Cache-Blocks). Die `{cache}`-Tags können ineinander verschachtelt werden, und wenn ein verschachtelter Block ungültig wird (z. B. durch ein Tag), wird auch der übergeordnete Block ungültig. +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 können Schlüssel angegeben werden, an die der Cache gebunden wird (hier die Variable `$id`), sowie der Ablauf und [Tags zur Invalidierung |#Invalidierung mittels Tags] eingestellt werden. +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]} @@ -318,9 +333,9 @@ Im Tag können Schlüssel angegeben werden, an die der Cache gebunden wird (hier {/cache} ``` -Alle Parameter sind optional, sodass wir weder den Ablauf noch die Tags und letztendlich nicht einmal die Schlüssel angeben müssen. +Alle Angaben sind optional, wir müssen also weder die Ablaufzeit noch die Tags noch überhaupt die Schlüssel angeben. -Die Verwendung des Caches kann auch mit `if` bedingt werden - der Inhalt wird dann nur gecached, wenn die Bedingung erfüllt ist: +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()} @@ -329,23 +344,23 @@ Die Verwendung des Caches kann auch mit `if` bedingt werden - der Inhalt wird da ``` -Speicher (Storage) -================== +Storages +======== -Ein Speicher (Storage) ist ein Objekt, das den Ort darstellt, an dem Daten physisch gespeichert werden. Wir können eine Datenbank, einen Memcached-Server oder den am leichtesten verfügbaren Speicher verwenden, nämlich Dateien auf der Festplatte. +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. -|----------------- -| Speicher | Beschreibung -|----------------- -| [#FileStorage] | Standardspeicher mit Speicherung in Dateien auf der Festplatte -| [#MemcachedStorage] | verwendet einen `Memcached`-Server -| [#MemoryStorage] | Daten werden temporär im Speicher gehalten -| [#SQLiteStorage] | Daten werden in einer SQLite-Datenbank gespeichert -| [#DevNullStorage] | Daten werden nicht gespeichert, geeignet zum Testen +|---------------------- +| 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. -Zum Speicherobjekt gelangen Sie, indem Sie es sich mittels [Dependency Injection |dependency-injection:passing-dependencies] mit dem Typ `Nette\Caching\Storage` übergeben lassen. Als Standardspeicher stellt Nette das `FileStorage`-Objekt bereit, das Daten im Unterordner `cache` im Verzeichnis für [temporäre Dateien |application:bootstrapping#Temporäre Dateien] speichert. +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. -Den Speicher können Sie in der Konfiguration ändern: +Das Storage können Sie in der Konfiguration ändern: ```neon services: @@ -356,14 +371,14 @@ services: FileStorage ----------- -Schreibt den Cache in Dateien auf der Festplatte. Der Speicher `Nette\Caching\Storages\FileStorage` ist sehr gut für die Leistung optimiert und gewährleistet vor allem die volle Atomizität der Operationen. Was bedeutet das? Dass bei Verwendung des Caches nicht passieren kann, dass wir eine Datei lesen, die von einem anderen Thread noch nicht vollständig geschrieben wurde, oder dass sie jemand „unter den Händen“ löscht. Die Verwendung des Caches ist also absolut sicher. +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. -Dieser Speicher verfügt auch über eine wichtige integrierte Funktion, die einen extremen Anstieg der CPU-Auslastung verhindert, wenn der Cache gelöscht wird oder noch nicht „aufgewärmt“ (d.h. erstellt) ist. Dies ist eine Prävention gegen den "Cache Stampede":https://en.wikipedia.org/wiki/Cache_stampede. Es kommt vor, dass zu einem Zeitpunkt eine größere Anzahl gleichzeitiger Anfragen eingeht, die dasselbe Element aus dem Cache abrufen möchten (z. B. das Ergebnis einer teuren SQL-Abfrage), und da es nicht im Cache vorhanden ist, beginnen alle Prozesse, dieselbe SQL-Abfrage auszuführen. Die Auslastung vervielfacht sich, und es kann sogar vorkommen, dass kein Thread innerhalb des Zeitlimits antworten kann, der Cache nicht erstellt wird und die Anwendung zusammenbricht. Glücklicherweise funktioniert der Cache in Nette so, dass bei mehreren gleichzeitigen Anfragen für ein Element nur der erste Thread dieses generiert, die anderen warten und anschließend das generierte Ergebnis verwenden. +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 die Erstellung von `FileStorage`: +Beispiel für das Erzeugen eines `FileStorage`: ```php -// Der Speicher wird das Verzeichnis '/path/to/temp' auf der Festplatte sein +// das Storage ist das Verzeichnis '/path/to/temp' auf der Festplatte $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); ``` @@ -371,7 +386,7 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -Der [Memcached|https://memcached.org]-Server ist ein hochleistungsfähiges verteiltes In-Memory-Speichersystem, dessen Adapter `Nette\Caching\Storages\MemcachedStorage` ist. In der Konfiguration geben wir die IP-Adresse und den Port an, falls dieser vom Standardport 11211 abweicht. +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`. @@ -385,13 +400,13 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` ist ein Speicher, der Daten in einem PHP-Array ablegt und somit mit dem Ende der Anfrage verloren gehen. +`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 SQLite-Datenbank und der Adapter `Nette\Caching\Storages\SQLiteStorage` bieten eine Möglichkeit, den Cache in einer einzigen Datei auf der Festplatte zu speichern. In der Konfiguration geben wir den Pfad zu dieser Datei an. +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`. @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -Eine spezielle Implementierung des Speichers ist `Nette\Caching\Storages\DevNullStorage`, das tatsächlich überhaupt keine Daten speichert. Es eignet sich daher zum Testen, wenn wir den Einfluss des Caches eliminieren möchten. +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 gibt es zwei Möglichkeiten. Die erste besteht darin, sich den Speicher mittels [Dependency Injection |dependency-injection:passing-dependencies] übergeben zu lassen und ein `Cache`-Objekt zu erstellen: +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; @@ -427,7 +442,7 @@ class ClassOne } ``` -Die zweite Möglichkeit besteht darin, sich direkt das `Cache`-Objekt übergeben zu lassen: +Die zweite Möglichkeit ist, uns gleich das Objekt `Cache` übergeben zu lassen: ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -Das `Cache`-Objekt wird dann direkt in der Konfiguration auf diese Weise erstellt: +Das Objekt `Cache` wird dann direkt in der Konfiguration auf diese Weise erzeugt: ```neon services: @@ -450,7 +465,7 @@ services: Journal ======= -Nette speichert Tags und Prioritäten im sogenannten Journal. Standardmäßig wird dafür SQLite und die Datei `journal.s3db` verwendet, und **es sind die PHP-Erweiterungen `pdo` und `pdo_sqlite` erforderlich.** +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: @@ -460,25 +475,28 @@ services: ``` -DI-Dienste -========== +DI-Services +=========== -Diese Dienste werden dem DI-Container hinzugefügt: +Diese Services werden dem DI-Container hinzugefügt: -| Name | Typ | Beschreibung +| Name | Typ | Beschreibung |---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Journal -| `cache.storage` | [api:Nette\Caching\Storage] | Speicher +| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Storage des Cache-Journals +| `cache.storage` | [api:Nette\Caching\Storage] | Primäres Cache-Storage -Deaktivieren des Caches -======================= +Cache abschalten +================ -Eine Möglichkeit, den Cache in der Anwendung zu deaktivieren, besteht darin, [#DevNullStorage] als Speicher festzulegen: +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 von Templates in Latte oder des DI-Containers, da diese Bibliotheken die Dienste von `nette/caching` nicht nutzen und ihren Cache selbst verwalten. Ihr Cache muss im Entwicklermodus übrigens [nicht deaktiviert werden |nette:troubleshooting#Wie schaltet man den Cache während der Entwicklung aus]. +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 new file mode 100644 index 0000000000..15db5d0a89 --- /dev/null +++ b/caching/de/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 2cf383a5cf..b3b806b2ca 100644 --- a/caching/de/@meta.texy +++ b/caching/de/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokumentation}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/de/upgrading.texy b/caching/de/upgrading.texy new file mode 100644 index 0000000000..b9955c6376 --- /dev/null +++ b/caching/de/upgrading.texy @@ -0,0 +1,20 @@ +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/el/@home.texy b/caching/el/@home.texy deleted file mode 100644 index a9c9f774d5..0000000000 --- a/caching/el/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Η Cache επιταχύνει την εφαρμογή σας αποθηκεύοντας δεδομένα που αποκτήθηκαν με κόπο μία φορά για μελλοντική χρήση. Θα δείξουμε: - -- πώς να χρησιμοποιήσετε την cache -- πώς να αλλάξετε την αποθήκη -- πώς να ακυρώσετε σωστά την cache - -</div> - -Η χρήση της cache στο Nette είναι πολύ εύκολη, ενώ καλύπτει και πολύ προηγμένες ανάγκες. Είναι σχεδιασμένη για απόδοση και 100% ανθεκτικότητα. Στη βάση θα βρείτε προσαρμογείς για τις πιο συνηθισμένες αποθήκες backend. Επιτρέπει την ακύρωση βάσει tags, την λήξη βάσει χρόνου, έχει προστασία έναντι cache stampede κ.λπ. - - -Εγκατάσταση -=========== - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Βασική Χρήση -============ - -Ο πυρήνας της εργασίας με την cache, ή την προσωρινή μνήμη, είναι το αντικείμενο [api:Nette\Caching\Cache]. Δημιουργούμε ένα στιγμιότυπό του και περνάμε στον κατασκευαστή την λεγόμενη αποθήκη ως παράμετρο. Αυτό είναι ένα αντικείμενο που αντιπροσωπεύει τον τόπο όπου τα δεδομένα θα αποθηκευτούν φυσικά (βάση δεδομένων, Memcached, αρχεία στον δίσκο, ...). Έχουμε πρόσβαση στην αποθήκη αφήνοντάς την να περάσει μέσω [dependency injection |dependency-injection:passing-dependencies] με τον τύπο `Nette\Caching\Storage`. Όλα τα απαραίτητα θα τα μάθετε στην [ενότητα Αποθήκες |#Αποθήκες]. - -.[warning] -Στην έκδοση 3.0, το interface είχε ακόμα το πρόθεμα `I`, οπότε το όνομα ήταν `Nette\Caching\IStorage`. Επιπλέον, οι σταθερές της κλάσης `Cache` γράφονταν με κεφαλαία γράμματα, οπότε για παράδειγμα `Cache::EXPIRE` αντί για `Cache::Expire`. - -Για τα παρακάτω παραδείγματα, ας υποθέσουμε ότι έχουμε δημιουργήσει ένα alias `Cache` και στην μεταβλητή `$storage` την αποθήκη. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // στιγμιότυπο του Nette\Caching\Storage -``` - -Η cache είναι στην πραγματικότητα ένα *key–value store*, δηλαδή διαβάζουμε και γράφουμε δεδομένα υπό κλειδιά, όπως και με τους συσχετιστικούς πίνακες. Οι εφαρμογές αποτελούνται από μια σειρά ανεξάρτητων τμημάτων και αν όλα χρησιμοποιούσαν μία αποθήκη (φανταστείτε έναν κατάλογο στον δίσκο), αργά ή γρήγορα θα προέκυπτε σύγκρουση κλειδιών. Το Nette Framework λύνει το πρόβλημα χωρίζοντας ολόκληρο τον χώρο σε namespaces (υποκαταλόγους). Κάθε τμήμα του προγράμματος χρησιμοποιεί τότε τον δικό του χώρο με ένα μοναδικό όνομα και δεν μπορεί πλέον να υπάρξει καμία σύγκρουση. - -Το όνομα του χώρου αναφέρεται ως η δεύτερη παράμετρος του κατασκευαστή της κλάσης Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Τώρα μπορούμε να χρησιμοποιήσουμε το αντικείμενο `$cache` για να διαβάσουμε και να γράψουμε στην προσωρινή μνήμη. Η μέθοδος `load()` χρησιμοποιείται και για τα δύο. Το πρώτο όρισμα είναι το κλειδί και το δεύτερο είναι ένα PHP callback που καλείται όταν το κλειδί δεν βρίσκεται στην cache. Το callback παράγει την τιμή, την επιστρέφει και αποθηκεύεται στην cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // απαιτητικός υπολογισμός - return $computedValue; -}); -``` - -Αν η δεύτερη παράμετρος δεν καθοριστεί `$value = $cache->load($key)`, θα επιστραφεί `null` αν το στοιχείο δεν υπάρχει στην cache. - -.[tip] -Είναι υπέροχο που οποιαδήποτε σειριοποιήσιμη δομή μπορεί να αποθηκευτεί στην cache, όχι μόνο συμβολοσειρές. Και το ίδιο ισχύει ακόμη και για τα κλειδιά. - -Ένα στοιχείο διαγράφεται από την προσωρινή μνήμη χρησιμοποιώντας τη μέθοδο `remove()`: - -```php -$cache->remove($key); -``` - -Η αποθήκευση ενός στοιχείου στην προσωρινή μνήμη μπορεί επίσης να γίνει με τη μέθοδο `$cache->save($key, $value, array $dependencies = [])`. Ωστόσο, προτιμάται η παραπάνω μέθοδος χρησιμοποιώντας το `load()`. - - -Memoization -=========== - -Memoization σημαίνει την προσωρινή αποθήκευση του αποτελέσματος μιας κλήσης συνάρτησης ή μεθόδου, ώστε να μπορείτε να το χρησιμοποιήσετε την επόμενη φορά χωρίς να υπολογίζετε ξανά το ίδιο πράγμα. - -Μέθοδοι και συναρτήσεις μπορούν να κληθούν με memoization χρησιμοποιώντας το `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Η συνάρτηση `gethostbyaddr()` καλείται έτσι μόνο μία φορά για κάθε παράμετρο `$ip`, και την επόμενη φορά η τιμή επιστρέφεται από την cache. - -Είναι επίσης δυνατό να δημιουργηθεί ένα memoized wrapper γύρω από μια μέθοδο ή συνάρτηση που μπορεί να κληθεί αργότερα: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // υπολογίζει την πρώτη φορά -$result = $memoizedFactorial(5); // τη δεύτερη φορά από την cache -``` - - -Λήξη & Ακύρωση -============== - -Με την αποθήκευση στην cache, είναι απαραίτητο να αντιμετωπιστεί το ζήτημα του πότε τα προηγουμένως αποθηκευμένα δεδομένα καθίστανται άκυρα. Το Nette Framework προσφέρει έναν μηχανισμό για τον περιορισμό της εγκυρότητας των δεδομένων ή την ελεγχόμενη διαγραφή τους (στην ορολογία του framework "ακύρωση"). - -Η εγκυρότητα των δεδομένων ορίζεται τη στιγμή της αποθήκευσης χρησιμοποιώντας την τρίτη παράμετρο της μεθόδου `save()`, π.χ.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Ή χρησιμοποιώντας την παράμετρο `$dependencies` που περνιέται με αναφορά στο callback της μεθόδου `load()`, π.χ.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ή χρησιμοποιώντας την 3η παράμετρο στη μέθοδο `load()`, π.χ: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -Στα επόμενα παραδείγματα, θα υποθέσουμε τη δεύτερη παραλλαγή και συνεπώς την ύπαρξη της μεταβλητής `$dependencies`. - - -Λήξη ----- - -Η απλούστερη λήξη είναι ένα χρονικό όριο. Έτσι αποθηκεύουμε δεδομένα στην cache με ισχύ 20 λεπτών: - -```php -// δέχεται επίσης τον αριθμό των δευτερολέπτων ή UNIX timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Αν θέλαμε να παρατείνουμε την περίοδο ισχύος με κάθε ανάγνωση, αυτό μπορεί να επιτευχθεί ως εξής, αλλά προσέξτε, το overhead της cache αυξάνεται: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Είναι χρήσιμη η δυνατότητα να λήξουν τα δεδομένα τη στιγμή που αλλάζει ένα αρχείο ή κάποιο από τα περισσότερα αρχεία. Αυτό μπορεί να χρησιμοποιηθεί, για παράδειγμα, κατά την αποθήκευση δεδομένων που προκύπτουν από την επεξεργασία αυτών των αρχείων στην cache. Χρησιμοποιήστε απόλυτες διαδρομές. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// ή -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Μπορούμε να αφήσουμε ένα στοιχείο στην cache να λήξει τη στιγμή που λήγει ένα άλλο στοιχείο (ή κάποιο από τα περισσότερα άλλα). Αυτό μπορεί να χρησιμοποιηθεί όταν αποθηκεύουμε, για παράδειγμα, ολόκληρη τη σελίδα HTML στην cache και τα τμήματά της κάτω από άλλα κλειδιά. Μόλις αλλάξει ένα τμήμα, ακυρώνεται ολόκληρη η σελίδα. Αν έχουμε αποθηκεύσει τα τμήματα κάτω από κλειδιά π.χ. `frag1` και `frag2`, χρησιμοποιούμε: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Η λήξη μπορεί επίσης να ελεγχθεί χρησιμοποιώντας προσαρμοσμένες συναρτήσεις ή στατικές μεθόδους, οι οποίες αποφασίζουν πάντα κατά την ανάγνωση αν το στοιχείο είναι ακόμα έγκυρο. Έτσι, για παράδειγμα, μπορούμε να αφήσουμε ένα στοιχείο να λήξει κάθε φορά που αλλάζει η έκδοση της PHP. Δημιουργούμε μια συνάρτηση που συγκρίνει την τρέχουσα έκδοση με την παράμετρο, και κατά την αποθήκευση προσθέτουμε μεταξύ των εξαρτήσεων έναν πίνακα της μορφής `[όνομα συνάρτησης, ...ορίσματα]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // λήξη όταν checkPhpVersion(...) === false -]; -``` - -Όλα τα κριτήρια μπορούν φυσικά να συνδυαστούν. Η cache θα λήξει τότε όταν τουλάχιστον ένα κριτήριο δεν πληρείται. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Ακύρωση με χρήση tags ---------------------- - -Ένα πολύ χρήσιμο εργαλείο ακύρωσης είναι τα λεγόμενα tags. Μπορούμε να αντιστοιχίσουμε σε κάθε στοιχείο της cache μια λίστα από tags, που είναι οποιεσδήποτε συμβολοσειρές. Ας υποθέσουμε ότι έχουμε μια HTML σελίδα με ένα άρθρο και σχόλια, την οποία θα αποθηκεύσουμε στην cache. Κατά την αποθήκευση, καθορίζουμε τα tags: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Ας μεταφερθούμε στη διαχείριση. Εδώ βρίσκουμε μια φόρμα για την επεξεργασία του άρθρου. Μαζί με την αποθήκευση του άρθρου στη βάση δεδομένων, καλούμε την εντολή `clean()`, η οποία διαγράφει από την cache τα στοιχεία σύμφωνα με το tag: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Ομοίως, στο σημείο προσθήκης νέου σχολίου (ή επεξεργασίας σχολίου), δεν παραλείπουμε να ακυρώσουμε το σχετικό tag: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Τι πετύχαμε με αυτό; Ότι η HTML cache μας θα ακυρώνεται (διαγράφεται) κάθε φορά που αλλάζει το άρθρο ή τα σχόλια. Όταν επεξεργάζεται το άρθρο με ID = 10, γίνεται αναγκαστική ακύρωση του tag `article/10` και η HTML σελίδα που φέρει το εν λόγω tag διαγράφεται από την cache. Το ίδιο συμβαίνει κατά την εισαγωγή νέου σχολίου κάτω από το σχετικό άρθρο. - -.[note] -Τα tags απαιτούν το λεγόμενο [#Journal]. - - -Ακύρωση με χρήση προτεραιότητας -------------------------------- - -Μπορούμε να ορίσουμε μια προτεραιότητα για μεμονωμένα στοιχεία στην cache, με βάση την οποία θα μπορούν να διαγραφούν όταν, για παράδειγμα, η cache υπερβεί ένα συγκεκριμένο μέγεθος: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Διαγράφουμε όλα τα στοιχεία με προτεραιότητα ίση ή μικρότερη από 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Οι προτεραιότητες απαιτούν το λεγόμενο [#Journal]. - - -Διαγραφή της cache ------------------- - -Η παράμετρος `Cache::All` διαγράφει τα πάντα: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Μαζική ανάγνωση -=============== - -Για μαζικές αναγνώσεις και εγγραφές στην cache χρησιμοποιείται η μέθοδος `bulkLoad()`, στην οποία περνάμε έναν πίνακα κλειδιών και λαμβάνουμε έναν πίνακα τιμών: - -```php -$values = $cache->bulkLoad($keys); -``` - -Η μέθοδος `bulkLoad()` λειτουργεί παρόμοια με το `load()` και με τη δεύτερη παράμετρο callback, στην οποία περνιέται το κλειδί του παραγόμενου στοιχείου: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // απαιτητικός υπολογισμός - return $computedValue; -}); -``` - - -Χρήση με PSR-16 .{data-version:3.3.1} -===================================== - -Για να χρησιμοποιήσετε την Nette Cache με το interface PSR-16, μπορείτε να χρησιμοποιήσετε τον προσαρμογέα `PsrCacheAdapter`. Επιτρέπει την απρόσκοπτη ενσωμάτωση μεταξύ της Nette Cache και οποιουδήποτε κώδικα ή βιβλιοθήκης που αναμένει μια cache συμβατή με PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Τώρα μπορείτε να χρησιμοποιήσετε το `$psrCache` ως PSR-16 cache: - -```php -$psrCache->set('key', 'value', 3600); // αποθηκεύει την τιμή για 1 ώρα -$value = $psrCache->get('key', 'default'); -``` - -Ο προσαρμογέας υποστηρίζει όλες τις μεθόδους που ορίζονται στο PSR-16, συμπεριλαμβανομένων των `getMultiple()`, `setMultiple()`, και `deleteMultiple()`. - - -Caching εξόδου -============== - -Μπορείτε να συλλάβετε και να αποθηκεύσετε στην cache την έξοδο πολύ κομψά: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // εκτυπώνουμε δεδομένα - - $capture->end(); // αποθηκεύουμε την έξοδο στην cache -} -``` - -Σε περίπτωση που η έξοδος είναι ήδη αποθηκευμένη στην cache, η μέθοδος `capture()` την εκτυπώνει και επιστρέφει `null`, οπότε η συνθήκη δεν εκτελείται. Διαφορετικά, αρχίζει να συλλαμβάνει την έξοδο και επιστρέφει το αντικείμενο `$capture`, με το οποίο τελικά αποθηκεύουμε τα εκτυπωμένα δεδομένα στην cache. - -.[note] -Στην έκδοση 3.0, η μέθοδος ονομαζόταν `$cache->start()`. - - -Caching στο Latte -================= - -Το caching στα πρότυπα [Latte |latte:] είναι πολύ εύκολο, αρκεί να περιβάλλετε ένα μέρος του προτύπου με τα tags `{cache}...{/cache}`. Η cache ακυρώνεται αυτόματα τη στιγμή που αλλάζει το πρότυπο προέλευσης (συμπεριλαμβανομένων τυχόν ενσωματωμένων προτύπων εντός του μπλοκ cache). Τα tags `{cache}` μπορούν να ενσωματωθούν το ένα μέσα στο άλλο, και όταν ένα ενσωματωμένο μπλοκ ακυρωθεί (για παράδειγμα, με ένα tag), ακυρώνεται και το γονικό μπλοκ. - -Στο tag είναι δυνατό να αναφερθούν κλειδιά στα οποία θα συνδεθεί η cache (εδώ η μεταβλητή `$id`) και να οριστεί η λήξη και τα [tags για ακύρωση |#Ακύρωση με χρήση tags] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Όλα τα στοιχεία είναι προαιρετικά, οπότε δεν χρειάζεται να καθορίσουμε ούτε λήξη, ούτε tags, ούτε καν κλειδιά. - -Η χρήση της cache μπορεί επίσης να εξαρτηθεί από συνθήκη χρησιμοποιώντας το `if` - το περιεχόμενο θα αποθηκευτεί στην cache μόνο αν η συνθήκη πληρείται: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Αποθήκες -======== - -Μια αποθήκη είναι ένα αντικείμενο που αντιπροσωπεύει τον τόπο όπου τα δεδομένα αποθηκεύονται φυσικά. Μπορούμε να χρησιμοποιήσουμε μια βάση δεδομένων, έναν διακομιστή Memcached, ή την πιο προσιτή αποθήκη, που είναι τα αρχεία στον δίσκο. - -|----------------- -| Αποθήκη | Περιγραφή -|----------------- -| [#FileStorage] | προεπιλεγμένη αποθήκη με αποθήκευση σε αρχεία στον δίσκο -| [#MemcachedStorage] | χρησιμοποιεί τον διακομιστή `Memcached` -| [#MemoryStorage] | τα δεδομένα είναι προσωρινά στη μνήμη -| [#SQLiteStorage] | τα δεδομένα αποθηκεύονται σε βάση δεδομένων SQLite -| [#DevNullStorage] | τα δεδομένα δεν αποθηκεύονται, κατάλληλο για testing - -Μπορείτε να αποκτήσετε πρόσβαση στο αντικείμενο αποθήκης αφήνοντάς το να περάσει μέσω [dependency injection |dependency-injection:passing-dependencies] με τον τύπο `Nette\Caching\Storage`. Ως προεπιλεγμένη αποθήκη, το Nette παρέχει το αντικείμενο FileStorage που αποθηκεύει δεδομένα στον υποκατάλογο `cache` στον κατάλογο για [προσωρινά αρχεία |application:bootstrapping#Προσωρινά Αρχεία]. - -Μπορείτε να αλλάξετε την αποθήκη στη διαμόρφωση: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Γράφει την cache σε αρχεία στον δίσκο. Η αποθήκη `Nette\Caching\Storages\FileStorage` είναι πολύ καλά βελτιστοποιημένη για απόδοση και κυρίως εξασφαλίζει πλήρη ατομικότητα των λειτουργιών. Τι σημαίνει αυτό; Ότι κατά τη χρήση της cache, δεν μπορεί να συμβεί να διαβάσουμε ένα αρχείο που δεν έχει ακόμη γραφτεί πλήρως από άλλο νήμα, ή να το διαγράψει κάποιος "κάτω από τα χέρια μας". Η χρήση της cache είναι επομένως απολύτως ασφαλής. - -Αυτή η αποθήκη έχει επίσης ενσωματωμένη μια σημαντική λειτουργία που εμποδίζει την ακραία αύξηση της χρήσης της CPU τη στιγμή που η cache διαγράφεται ή δεν έχει ακόμη θερμανθεί (δηλ. δημιουργηθεί). Πρόκειται για πρόληψη έναντι του "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Συμβαίνει ότι σε μία στιγμή συγκεντρώνεται μεγαλύτερος αριθμός ταυτόχρονων αιτημάτων που θέλουν το ίδιο πράγμα από την cache (π.χ. το αποτέλεσμα ενός ακριβού ερωτήματος SQL) και επειδή δεν υπάρχει στην προσωρινή μνήμη, όλες οι διεργασίες αρχίζουν να εκτελούν το ίδιο ερώτημα SQL. Η φόρτωση έτσι πολλαπλασιάζεται και μπορεί ακόμη και να συμβεί καμία διεργασία να μην προλάβει να απαντήσει εντός του χρονικού ορίου, η cache να μην δημιουργηθεί και η εφαρμογή να καταρρεύσει. Ευτυχώς, η cache στο Nette λειτουργεί έτσι ώστε κατά τη διάρκεια πολλαπλών ταυτόχρονων αιτημάτων για ένα στοιχείο, το παράγει μόνο το πρώτο νήμα, τα υπόλοιπα περιμένουν και στη συνέχεια χρησιμοποιούν το παραγόμενο αποτέλεσμα. - -Παράδειγμα δημιουργίας FileStorage: - -```php -// η αποθήκη θα είναι ο κατάλογος '/path/to/temp' στον δίσκο -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Ο διακομιστής [Memcached |https://memcached.org] είναι ένα σύστημα αποθήκευσης υψηλής απόδοσης σε κατανεμημένη μνήμη, του οποίου ο προσαρμογέας είναι ο `Nette\Caching\Storages\MemcachedStorage`. Στη διαμόρφωση, αναφέρουμε τη διεύθυνση IP και τη θύρα, αν διαφέρει από την προεπιλεγμένη 11211. - -.[caution] -Απαιτεί την επέκταση PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -Το `Nette\Caching\Storages\MemoryStorage` είναι μια αποθήκη που αποθηκεύει δεδομένα σε έναν πίνακα PHP, και επομένως χάνονται με τον τερματισμό του αιτήματος. - - -SQLiteStorage -------------- - -Η βάση δεδομένων SQLite και ο προσαρμογέας `Nette\Caching\Storages\SQLiteStorage` προσφέρουν έναν τρόπο αποθήκευσης της cache σε ένα μόνο αρχείο στον δίσκο. Στη διαμόρφωση, αναφέρουμε τη διαδρομή προς αυτό το αρχείο. - -.[caution] -Απαιτεί τις επεκτάσεις PHP `pdo` και `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Μια ειδική υλοποίηση αποθήκης είναι η `Nette\Caching\Storages\DevNullStorage`, η οποία στην πραγματικότητα δεν αποθηκεύει καθόλου δεδομένα. Είναι επομένως κατάλληλη για testing, όταν θέλουμε να εξαλείψουμε την επίδραση της cache. - - -Χρήση της cache στον κώδικα -=========================== - -Κατά τη χρήση της cache στον κώδικα, έχουμε δύο τρόπους για να το κάνουμε. Ο πρώτος είναι να αφήσουμε την αποθήκη να περάσει μέσω [dependency injection |dependency-injection:passing-dependencies] και να δημιουργήσουμε ένα αντικείμενο `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Η δεύτερη επιλογή είναι να αφήσουμε το αντικείμενο `Cache` να περάσει απευθείας: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Το αντικείμενο `Cache` δημιουργείται στη συνέχεια απευθείας στη διαμόρφωση με αυτόν τον τρόπο: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Το Nette αποθηκεύει τα tags και τις προτεραιότητες στο λεγόμενο journal. Για αυτό χρησιμοποιείται συνήθως το SQLite και το αρχείο `journal.s3db` και **απαιτούνται οι επεκτάσεις PHP `pdo` και `pdo_sqlite`.** - -Μπορείτε να αλλάξετε το journal στη διαμόρφωση: - -```neon -services: - cache.journal: MyJournal -``` - - -Υπηρεσίες DI -============ - -Αυτές οι υπηρεσίες προστίθενται στον DI container: - -| Όνομα | Τύπος | Περιγραφή -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | αποθήκη - - -Απενεργοποίηση της cache -======================== - -Μία από τις επιλογές για την απενεργοποίηση της cache στην εφαρμογή είναι να ορίσετε ως αποθήκη την [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Αυτή η ρύθμιση δεν επηρεάζει το caching των προτύπων στο Latte ή τον DI container, καθώς αυτές οι βιβλιοθήκες δεν χρησιμοποιούν τις υπηρεσίες nette/caching και διαχειρίζονται την cache τους ανεξάρτητα. Εξάλλου, η cache τους [δεν χρειάζεται να απενεργοποιηθεί |nette:troubleshooting#Πώς να απενεργοποιήσετε την cache κατά την ανάπτυξη] στη λειτουργία ανάπτυξης. diff --git a/caching/el/@meta.texy b/caching/el/@meta.texy deleted file mode 100644 index a09ce5fe0d..0000000000 --- a/caching/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/en/@home.texy b/caching/en/@home.texy index d8a7533643..922d42f91f 100644 --- a/caching/en/@home.texy +++ b/caching/en/@home.texy @@ -3,7 +3,7 @@ Nette Caching <div class=perex> -Cache speeds up your application by storing data that was once computationally expensive to retrieve, allowing for faster access in the future. We will cover: +A cache speeds up your application by storing data that was once computationally expensive to retrieve, allowing for faster access in the future. We will cover: - how to use the cache - how to change the storage backend @@ -48,6 +48,12 @@ Specify the namespace name as the second argument to the `Cache` class construct $cache = new Cache($storage, 'Full Html Pages'); ``` +If needed, you can derive a new cache scoped to a sub-namespace from an existing instance using the `derive()` method: + +```php +$subCache = $cache->derive('Images'); +``` + Now, we can use the `$cache` object to read from and write to the cache. The `load()` method serves both purposes. The first argument is the key, and the second is a PHP callback that gets invoked if the key is not found in the cache. The callback generates the value, returns it, and the `load()` method caches it: ```php @@ -68,7 +74,7 @@ To delete an item from the cache, use the `remove()` method: $cache->remove($key); ``` -You can also save an item to the cache using the `$cache->save($key, $value, array $dependencies = [])` method. However, the `load()` approach shown above is generally preferred. +You can also save an item to the cache using the `$cache->save($key, $data, ?array $dependencies = null)` method. However, the `load()` approach shown above is generally preferred. Memoization @@ -125,7 +131,7 @@ Or by using the 3rd parameter of the `load()` method itself, e.g.: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` @@ -250,7 +256,7 @@ $cache->clean([ Bulk Reading ============ -For bulk reading and writing to the cache, use the `bulkLoad()` method. Pass it an array of keys, and it returns an array of corresponding values: +For bulk reading from and writing to the cache, use the `bulkLoad()` method. Pass it an array of keys, and it returns an array of corresponding values: ```php $values = $cache->bulkLoad($keys); @@ -265,6 +271,15 @@ $values = $cache->bulkLoad($keys, function ($key, &$dependencies) { }); ``` +Conversely, to write multiple items at once, use the `bulkSave()` method, which takes an array of `key => value` pairs and optional dependencies: + +```php +$cache->bulkSave([ + $key1 => $value1, + $key2 => $value2, +], [Cache::Expire => '20 minutes']); +``` + Using with PSR-16 .{data-version:3.3.1} ======================================= @@ -293,7 +308,7 @@ Output can be captured and cached very elegantly: ```php if ($capture = $cache->capture($key)) { - echo ... // printing some data + // echo ... printing some data $capture->end(); // save the output to the cache } @@ -320,7 +335,7 @@ Within the tag, you can specify keys to which the cache entry will be bound (her All these parameters are optional, so you don't need to specify expiration, tags, or even keys. -The use of caching can also be made conditional using `if` – the content will only be cached if the condition is met: +The use of caching can also be made conditional using `if` - the content will only be cached if the condition is met: ```latte {cache $id, if: !$form->isSubmitted()} @@ -450,7 +465,7 @@ services: Journal ======= -Nette stores tags and priorities information in a so-called journal. By default, SQLite is used for this purpose via the file `journal.s3db`, and **the `pdo` and `pdo_sqlite` PHP extensions are required.** +Nette stores tag and priority information in a so-called journal. By default, SQLite is used for this purpose via the file `journal.s3db`, and **the `pdo` and `pdo_sqlite` PHP extensions are required.** You can change the journal implementation in the configuration: @@ -481,4 +496,7 @@ services: cache.storage: Nette\Caching\Storages\DevNullStorage ``` -This setting does not affect the caching of Latte templates or the DI container, as these libraries do not utilize `nette/caching` services and manage their caches independently. Furthermore, their caches [do not typically need to be disabled |nette:troubleshooting#How to Disable Cache During Development] during development mode. +This setting does not affect the caching of Latte templates or the DI container, as these libraries do not utilize `nette/caching` services and manage their caches independently. Furthermore, their caches [do not typically need to be disabled |nette:troubleshooting#How to Disable Cache During Development] in development mode. + + +If you are upgrading to a newer version, see the [upgrading] page. diff --git a/caching/en/@left-menu.texy b/caching/en/@left-menu.texy new file mode 100644 index 0000000000..18196494d3 --- /dev/null +++ b/caching/en/@left-menu.texy @@ -0,0 +1,13 @@ +Nette Caching +************* +- [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/caching/en/@meta.texy b/caching/en/@meta.texy index 91205786e5..42471908b0 100644 --- a/caching/en/@meta.texy +++ b/caching/en/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Documentation}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/en/upgrading.texy b/caching/en/upgrading.texy new file mode 100644 index 0000000000..5547b32483 --- /dev/null +++ b/caching/en/upgrading.texy @@ -0,0 +1,20 @@ +Upgrading +********* + + +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 diff --git a/caching/es/@home.texy b/caching/es/@home.texy index ce53dca55f..437b6a72ac 100644 --- a/caching/es/@home.texy +++ b/caching/es/@home.texy @@ -3,7 +3,7 @@ Nette Caching <div class=perex> -La caché acelera su aplicación al guardar datos obtenidos con esfuerzo una vez para su uso futuro. Mostraremos: +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 @@ -11,13 +11,13 @@ La caché acelera su aplicación al guardar datos obtenidos con esfuerzo una vez </div> -Usar la caché en Nette es muy fácil, pero cubre incluso necesidades muy avanzadas. Está diseñada para el rendimiento y una resistencia del 100%. En su núcleo, encontrará adaptadores para los almacenamientos backend más comunes. Permite la invalidación basada en etiquetas, la expiración por tiempo, tiene protección contra la estampida de caché, etc. +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 =========== -Puede descargar e instalar la librería usando [Composer|best-practices:composer]: +Descargue e instale el paquete con [Composer|best-practices:composer]: ```shell composer require nette/caching @@ -27,12 +27,12 @@ composer require nette/caching Uso básico ========== -El centro del trabajo con la caché es el objeto [api:Nette\Caching\Cache]. Creamos su instancia y pasamos el llamado almacenamiento como parámetro al constructor. Este es un objeto que representa el lugar donde los datos se almacenarán físicamente (base de datos, Memcached, archivos en disco, ...). Accedemos al almacenamiento pidiéndolo mediante [inyección de dependencias |dependency-injection:passing-dependencies] con el tipo `Nette\Caching\Storage`. Aprenderá todo lo esencial en la [sección Almacenamientos |#Almacenamientos]. +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 tenía el prefijo `I`, por lo que el nombre era `Nette\Caching\IStorage`. Además, las constantes de la clase `Cache` estaban escritas en mayúsculas, así que, por ejemplo, `Cache::EXPIRE` en lugar de `Cache::Expire`. +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`. -Para los siguientes ejemplos, supongamos que tenemos un alias `Cache` creado y el almacenamiento en la variable `$storage`. +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; @@ -40,15 +40,21 @@ use Nette\Caching\Cache; $storage = /* ... */; // instancia de Nette\Caching\Storage ``` -La caché es básicamente un *key-value store*, lo que significa que leemos y escribimos datos bajo claves al igual que con los arrays asociativos. Las aplicaciones consisten en varias partes independientes, y si todas usaran un solo almacenamiento (imagine un directorio en el disco), tarde o temprano ocurriría una colisión de claves. Nette Framework resuelve este problema dividiendo todo el espacio en espacios de nombres (subdirectorios). Cada parte del programa luego usa su propio espacio con un nombre único, y no puede ocurrir ninguna colisión. +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. -Especificamos el nombre del espacio como el segundo parámetro del constructor de la clase Cache: +Indique el nombre del espacio de nombres como segundo argumento del constructor de la clase `Cache`: ```php $cache = new Cache($storage, 'Full Html Pages'); ``` -Ahora podemos usar el objeto `$cache` para leer y escribir en la caché. El método `load()` sirve para ambos propósitos. El primer argumento es la clave y el segundo es un callback de PHP, que se llama cuando la clave no se encuentra en la caché. El callback genera el valor, lo devuelve y se almacena en la caché: +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) { @@ -57,34 +63,34 @@ $value = $cache->load($key, function () use ($key) { }); ``` -Si no especificamos el segundo parámetro `$value = $cache->load($key)`, devuelve `null` si el elemento no está en la caché. +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 bueno es que se pueden almacenar en la caché cualquier estructura serializable, no solo cadenas. Y lo mismo se aplica incluso a las claves. +Lo estupendo es que se pueden cachear todas las estructuras serializables, no solo cadenas. Lo mismo vale para las claves. -Eliminamos un elemento de la caché usando el método `remove()`: +Para borrar un elemento de la caché, use el método `remove()`: ```php $cache->remove($key); ``` -También es posible guardar un elemento en la caché usando el método `$cache->save($key, $value, array $dependencies = [])`. Sin embargo, se prefiere el método mencionado anteriormente usando `load()`. +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 significa almacenar en caché el resultado de una llamada a una función o método para que pueda usarlo la próxima vez sin calcular lo mismo una y otra vez. +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. -Se pueden llamar a métodos y funciones de forma memoizada usando `call(callable $callback, ...$args)`: +Los métodos y las funciones se pueden llamar de forma memoizada con `call(callable $callback, ...$args)`: ```php $result = $cache->call('gethostbyaddr', $ip); ``` -La función `gethostbyaddr()` se llamará solo una vez para cada parámetro `$ip`, y la próxima vez se devolverá el valor de la caché. +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 sobre un método o función que se puede llamar más tarde: +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) @@ -94,17 +100,17 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // calcula la primera vez -$result = $memoizedFactorial(5); // la segunda vez desde la caché +$result = $memoizedFactorial(5); // la primera vez lo calcula +$result = $memoizedFactorial(5); // la segunda vez lo devuelve de la caché ``` -Expiración e Invalidación +Expiración e invalidación ========================= -Al almacenar en caché, es necesario abordar la cuestión de cuándo los datos previamente almacenados se vuelven inválidos. Nette Framework ofrece un mecanismo para limitar la validez de los datos o eliminarlos de forma controlada (en la terminología del framework, "invalidar"). +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 en el momento del almacenamiento utilizando el tercer parámetro del método `save()`, por ejemplo: +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, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -O usando el parámetro `$dependencies` pasado por referencia al callback del método `load()`, por ejemplo: +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) { @@ -121,34 +127,34 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -O usando el tercer parámetro en el método `load()`, por ejemplo: +O con el tercer parámetro del propio método `load()`, p. ej.: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -En los siguientes ejemplos, asumiremos la segunda variante y, por lo tanto, la existencia de la variable `$dependencies`. +En los ejemplos siguientes usaremos la segunda variante, con la variable `$dependencies` dentro del callback. Expiración ---------- -La expiración más simple es un límite de tiempo. Así es como almacenamos en caché datos válidos durante 20 minutos: +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 -// también acepta número de segundos o timestamp UNIX +// acepta también un número de segundos o una marca de tiempo UNIX $dependencies[Cache::Expire] = '20 minutes'; ``` -Si quisiéramos extender el período de validez con cada lectura, se puede lograr de la siguiente manera, pero tenga cuidado, la sobrecarga de la caché aumentará: +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 dejar que los datos expiren cuando cambia un archivo o uno de varios archivos. Esto se puede usar, por ejemplo, al almacenar en caché datos resultantes del procesamiento de estos archivos. Use rutas absolutas. +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'; @@ -156,13 +162,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; $dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; ``` -Podemos hacer que un elemento de la caché expire cuando otro elemento (o uno de varios otros) expire. Esto se puede usar cuando almacenamos en caché, por ejemplo, una página HTML completa y sus fragmentos bajo otras claves. Tan pronto como cambia un fragmento, toda la página se invalida. Si tenemos fragmentos almacenados bajo claves como `frag1` y `frag2`, usamos: +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 mediante funciones personalizadas o métodos estáticos, que deciden cada vez que se lee si el elemento sigue siendo válido. De esta manera, por ejemplo, podemos hacer que un elemento expire siempre que cambie la versión de PHP. Creamos una función que compara la versión actual con un parámetro, y al guardar, agregamos un array con el formato `[nombre de la función, ...argumentos]` entre las dependencias: +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 @@ -175,7 +181,7 @@ $dependencies[Cache::Callbacks] = [ ]; ``` -Por supuesto, todos los criterios se pueden combinar. La caché expirará cuando al menos un criterio no se cumpla. +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'; @@ -183,16 +189,16 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; ``` -Invalidación mediante etiquetas -------------------------------- +Invalidación con etiquetas +-------------------------- -Una herramienta de invalidación muy útil son las llamadas etiquetas. Podemos asignar una lista de etiquetas, que son cadenas arbitrarias, a cada elemento de la caché. Por ejemplo, tengamos una página HTML con un artículo y comentarios que almacenaremos en caché. Al guardar, especificamos las 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 a la administración. Aquí encontramos un formulario para editar el artículo. Junto con guardar el artículo en la base de datos, llamamos al comando `clean()`, que elimina elementos de la caché según la etiqueta: +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([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -Del mismo modo, en el lugar de agregar un nuevo comentario (o editar un comentario), no olvidemos invalidar la etiqueta correspondiente: +Del mismo modo, al añadir un comentario nuevo (o al editarlo) tenemos que acordarnos de invalidar la etiqueta correspondiente: ```php $cache->clean([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -¿Qué hemos logrado con esto? Que nuestra caché HTML se invalidará (eliminará) cada vez que cambie el artículo o los comentarios. Cuando se edita un artículo con ID = 10, se fuerza la invalidación de la etiqueta `article/10` y la página HTML que lleva esa etiqueta se elimina de la caché. Lo mismo ocurre al insertar un nuevo comentario bajo el artículo correspondiente. +¿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 el llamado [#Journal]. +Las etiquetas requieren un [#Journal]. -Invalidación mediante prioridad -------------------------------- +Invalidación por prioridad +-------------------------- -Podemos establecer una prioridad para los elementos individuales en la caché, que se puede usar para eliminarlos cuando, por ejemplo, la caché exceda un cierto tamaño: +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; ``` -Eliminaremos todos los elementos con una prioridad igual o menor que 100: +Para borrar todos los elementos con una prioridad igual o menor que 100: ```php $cache->clean([ @@ -235,10 +241,10 @@ $cache->clean([ Las prioridades requieren el llamado [#Journal]. -Limpiar la caché ----------------- +Vaciar la caché +--------------- -El parámetro `Cache::All` elimina todo: +El parámetro `Cache::All` lo borra todo: ```php $cache->clean([ @@ -250,13 +256,13 @@ $cache->clean([ Lectura masiva ============== -Para la lectura y escritura masiva en la caché, se utiliza el método `bulkLoad()`, al que pasamos un array de claves y obtenemos un array de valores: +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 manera similar a `load()` también con el segundo parámetro callback, al que se pasa la clave del elemento generado: +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) { @@ -265,52 +271,61 @@ $values = $cache->bulkLoad($keys, function ($key, &$dependencies) { }); ``` +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 la interfaz PSR-16, puede utilizar el adaptador `PsrCacheAdapter`. Permite una integración perfecta entre Nette Cache y cualquier código o librería que espere una caché compatible con PSR-16. +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: +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 admite todos los métodos definidos en PSR-16, incluidos `getMultiple()`, `setMultiple()` y `deleteMultiple()`. +El adaptador soporta todos los métodos definidos en PSR-16, incluidos `getMultiple()`, `setMultiple()` y `deleteMultiple()`. -Almacenamiento en caché de la salida -==================================== +Cachear la salida +================= -La salida se puede capturar y almacenar en caché de forma muy elegante: +La salida se puede capturar y cachear de forma muy elegante: ```php if ($capture = $cache->capture($key)) { - echo ... // imprimimos datos + // echo ... imprime algunos datos - $capture->end(); // guardamos la salida en la caché + $capture->end(); // guarda la salida en la caché } ``` -Si la salida ya está almacenada en la caché, el método `capture()` la imprimirá y devolverá `null`, por lo que la condición no se ejecutará. De lo contrario, comenzará a capturar la salida y devolverá el objeto `$capture`, con el que finalmente guardaremos los datos impresos 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, el método se llamaba `$cache->start()`. +En la versión 3.0 este método se llamaba `$cache->start()`. -Almacenamiento en caché en Latte -================================ +Cachear en Latte +================ -El almacenamiento en caché en las plantillas [Latte|latte:] es muy fácil, simplemente envuelva una parte de la plantilla con las etiquetas `{cache}...{/cache}`. La caché se invalida automáticamente cuando cambia la plantilla de origen (incluidas las plantillas incluidas dentro del bloque de caché). Las etiquetas `{cache}` se pueden anidar, y cuando un bloque anidado se invalida (por ejemplo, por una etiqueta), el bloque padre también se invalida. +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. -En la etiqueta, es posible especificar claves a las que se vinculará la caché (aquí la variable `$id`) y establecer la expiración y las [etiquetas para la invalidación |#Invalidación mediante etiquetas]. +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]} @@ -318,9 +333,9 @@ En la etiqueta, es posible especificar claves a las que se vinculará la caché {/cache} ``` -Todos los elementos son opcionales, por lo que no tenemos que especificar ni la expiración, ni las etiquetas, ni siquiera las claves. +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 usando `if`: el contenido se almacenará en caché solo si se cumple la condición: +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()} @@ -332,20 +347,20 @@ El uso de la caché también se puede condicionar usando `if`: el contenido se a Almacenamientos =============== -Un almacenamiento es un objeto que representa el lugar donde se almacenan físicamente los datos. Podemos usar una base de datos, un servidor Memcached o el almacenamiento más accesible, que son archivos en disco. +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 con guardado en archivos en disco -| [#MemcachedStorage] | utiliza un servidor `Memcached` -| [#MemoryStorage] | los datos están temporalmente en memoria -| [#SQLiteStorage] | los datos se guardan en una base de datos SQLite -| [#DevNullStorage] | los datos no se guardan, adecuado para pruebas +|---------------------- +| [#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. -Accede al objeto de almacenamiento pidiéndolo mediante [inyección de dependencias |dependency-injection:passing-dependencies] con el tipo `Nette\Caching\Storage`. Como almacenamiento predeterminado, Nette proporciona el objeto FileStorage que guarda los datos en la subcarpeta `cache` en el directorio para [archivos temporales |application:bootstrapping#Archivos temporales]. +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 en la configuración: +Puede cambiar el almacenamiento predeterminado en la configuración: ```neon services: @@ -356,14 +371,14 @@ services: FileStorage ----------- -Escribe la caché en archivos en disco. El almacenamiento `Nette\Caching\Storages\FileStorage` está muy bien optimizado para el rendimiento y, sobre todo, garantiza la atomicidad completa de las operaciones. ¿Qué significa eso? Que al usar la caché, no puede suceder que leamos un archivo que otro hilo aún no ha escrito por completo, o que alguien lo elimine "debajo de nuestras manos". Por lo tanto, el uso de la caché es completamente seguro. +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 también tiene una función importante incorporada que evita un aumento extremo del uso de la CPU cuando se borra la caché o aún no se ha calentado (es decir, creado). Esta es una prevención contra la "estampida de caché":https://en.wikipedia.org/wiki/Cache_stampede. Sucede que en un momento dado, un gran número de solicitudes concurrentes llegan queriendo lo mismo de la caché (por ejemplo, el resultado de una consulta SQL costosa) y como no está en la memoria caché, todos los procesos comienzan a ejecutar la misma consulta SQL. La carga se multiplica y puede incluso suceder que ningún hilo logre responder dentro del límite de tiempo, la caché no se crea y la aplicación colapsa. Afortunadamente, la caché en Nette funciona de tal manera que cuando hay múltiples solicitudes concurrentes para un elemento, solo el primer hilo lo genera, los demás esperan y luego usan el resultado generado. +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 FileStorage: +Ejemplo de creación de un `FileStorage`: ```php -// el almacenamiento será el directorio '/path/to/temp' en el disco +// el almacenamiento será el directorio '/path/to/temp' del disco $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); ``` @@ -371,10 +386,10 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -El servidor [Memcached|https://memcached.org] es un sistema de almacenamiento en memoria distribuida de alto rendimiento, cuyo adaptador es `Nette\Caching\Storages\MemcachedStorage`. En la configuración, especificamos la dirección IP y el puerto, si es diferente del estándar 11211. +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 PHP `memcached`. +Requiere la extensión de PHP `memcached`. ```neon services: @@ -385,16 +400,16 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` es un almacenamiento que guarda los datos en un array PHP y, por lo tanto, se pierden al finalizar la solicitud. +`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 y el adaptador `Nette\Caching\Storages\SQLiteStorage` ofrecen una forma de almacenar la caché en un solo archivo en el disco. En la configuración, especificamos la ruta a este archivo. +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 PHP `pdo` y `pdo_sqlite`. +Requiere las extensiones de PHP `pdo` y `pdo_sqlite`. ```neon services: @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -Una implementación especial de almacenamiento es `Nette\Caching\Storages\DevNullStorage`, que en realidad no guarda datos en absoluto. Por lo tanto, es adecuado para pruebas cuando queremos eliminar la influencia de la caché. +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. -Uso de la caché en el código -============================ +Usar la caché en el código +========================== -Al usar la caché en el código, tenemos dos formas de hacerlo. La primera es que nos pasen el almacenamiento mediante [inyección de dependencias |dependency-injection:passing-dependencies] y creemos un objeto `Cache`: +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; @@ -427,7 +442,7 @@ class ClassOne } ``` -La segunda opción es que nos pasen directamente el objeto `Cache`: +La segunda opción es pedir directamente el objeto `Cache`: ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -El objeto `Cache` se crea luego directamente en la configuración de esta manera: +El objeto `Cache` hay que definirlo entonces en la configuración, por ejemplo así: ```neon services: @@ -450,9 +465,9 @@ services: Journal ======= -Nette guarda las etiquetas y prioridades en el llamado journal. Por defecto, se utiliza SQLite y el archivo `journal.s3db` para esto, y **se requieren las extensiones PHP `pdo` y `pdo_sqlite`.** +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 el journal en la configuración: +Puede cambiar la implementación del journal en la configuración: ```neon services: @@ -463,22 +478,25 @@ services: Servicios DI ============ -Estos servicios se agregan al contenedor DI: +Estos servicios se añaden al contenedor DI: -| Nombre | Tipo | Descripción +| Nombre | Tipo | Descripción |---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | almacenamiento +| `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 de las formas de desactivar la caché en la aplicación es establecer el almacenamiento en [#DevNullStorage]: +Una forma de desactivar el cacheo en su aplicación es establecer el almacenamiento a [#DevNullStorage]: ```neon services: cache.storage: Nette\Caching\Storages\DevNullStorage ``` -Esta configuración no afecta el almacenamiento en caché de plantillas en Latte o el contenedor DI, ya que estas librerías no utilizan los servicios de nette/caching y gestionan su caché de forma independiente. Además, su caché [no necesita ser desactivada |nette:troubleshooting#Cómo desactivar la caché durante el desarrollo] en el modo de desarrollo. +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 new file mode 100644 index 0000000000..d775b39d5e --- /dev/null +++ b/caching/es/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 25d506cde9..3798d9cda4 100644 --- a/caching/es/@meta.texy +++ b/caching/es/@meta.texy @@ -1,2 +1 @@ -{{sitename: Nette Documentación}} -{{leftbar: nette:@menu-topics}} +{{sitename: Documentación de Nette}} diff --git a/caching/es/upgrading.texy b/caching/es/upgrading.texy new file mode 100644 index 0000000000..f2789edd82 --- /dev/null +++ b/caching/es/upgrading.texy @@ -0,0 +1,20 @@ +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 index f0dabcdc54..0c3d1f9a4d 100644 --- a/caching/fr/@home.texy +++ b/caching/fr/@home.texy @@ -3,21 +3,21 @@ Nette Caching <div class=perex> -Le cache accélère votre application en stockant les données coûteuses à obtenir pour une utilisation ultérieure. Nous allons vous montrer : +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 le stockage +- comment changer de stockage - comment invalider correctement le cache </div> -L'utilisation du cache est très facile dans Nette, tout en couvrant des besoins très avancés. Il est conçu pour la performance et une résilience à 100%. Par défaut, vous trouverez des adaptateurs pour les stockages backend les plus courants. Il permet l'invalidation basée sur les tags, l'expiration temporelle, dispose d'une protection contre le cache stampede, etc. +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 ============ -La bibliothèque peut être téléchargée et installée en utilisant l'outil [Composer|best-practices:composer] : +Téléchargez et installez le paquet à l'aide de [Composer|best-practices:composer] : ```shell composer require nette/caching @@ -27,64 +27,70 @@ composer require nette/caching Utilisation de base =================== -Le cœur du travail avec le cache est l'objet [api:Nette\Caching\Cache]. Nous créons son instance et passons au constructeur ce qu'on appelle un stockage. C'est un objet représentant l'endroit où les données seront physiquement stockées (base de données, Memcached, fichiers sur disque, ...). Nous accédons au stockage en le faisant passer via [l'injection de dépendances |dependency-injection:passing-dependencies] avec le type `Nette\Caching\Storage`. Vous trouverez tout ce qui est essentiel dans la [section Stockages |#Stockages]. +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 avait encore le préfixe `I`, donc le nom était `Nette\Caching\IStorage`. De plus, les constantes de la classe `Cache` étaient écrites en majuscules, donc par exemple `Cache::EXPIRE` au lieu de `Cache::Expire`. +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`. -Pour les exemples suivants, supposons que nous avons créé un alias `Cache` et que la variable `$storage` contient le stockage. +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 de Nette\Caching\Storage +$storage = /* ... */; // instance of Nette\Caching\Storage ``` -Le cache est en fait un *key–value store*, c'est-à-dire que nous lisons et écrivons des données sous des clés, tout comme avec les tableaux associatifs. Les applications sont composées de nombreuses parties indépendantes, et si toutes utilisaient un seul stockage (imaginez un seul répertoire sur le disque), tôt ou tard, une collision de clés se produirait. Le framework Nette résout ce problème en divisant tout l'espace en espaces de noms (sous-répertoires). Chaque partie du programme utilise alors son propre espace avec un nom unique, et aucune collision ne peut plus se produire. +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. -Nous spécifions le nom de l'espace comme deuxième paramètre du constructeur de la classe Cache : +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'); ``` -Maintenant, nous pouvons utiliser l'objet `$cache` pour lire et écrire dans le cache. La méthode `load()` est utilisée pour les deux. Le premier argument est la clé et le second est un callback PHP, qui est appelé lorsque la clé n'est pas trouvée dans le cache. Le callback génère la valeur, la retourne et elle est stockée dans le cache : +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 = /* ... */; // calcul coûteux + $computedValue = /* ... */; // expensive computation return $computedValue; }); ``` -Si le deuxième paramètre n'est pas fourni `$value = $cache->load($key)`, `null` est retourné si l'élément n'est pas dans le cache. +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] -Ce qui est génial, c'est que vous pouvez stocker n'importe quelle structure sérialisable dans le cache, pas seulement des chaînes de caractères. Et la même chose s'applique même aux clés. +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. -Nous supprimons un élément du cache en utilisant la méthode `remove()` : +Pour supprimer un élément du cache, utilisez la méthode `remove()` : ```php $cache->remove($key); ``` -Vous pouvez également enregistrer un élément dans le cache en utilisant la méthode `$cache->save($key, $value, array $dependencies = [])`. Cependant, la méthode préférée est celle mentionnée ci-dessus en utilisant `load()`. +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. -Memoization +Mémoïsation =========== -La mémoïsation signifie la mise en cache du résultat d'un appel de fonction ou de méthode afin que vous puissiez l'utiliser la prochaine fois sans recalculer la même chose encore et encore. +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 les fonctions peuvent être appelées de manière mémoïsée en utilisant `call(callable $callback, ...$args)` : +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()` ne sera appelée qu'une seule fois pour chaque paramètre `$ip`, et la prochaine fois, la valeur sera retournée depuis le cache. +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 un wrapper mémoïsé autour d'une méthode ou d'une fonction, qui peut être appelé plus tard : +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) @@ -94,17 +100,17 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // calcule la première fois -$result = $memoizedFactorial(5); // la deuxième fois depuis le cache +$result = $memoizedFactorial(5); // calculates it the first time +$result = $memoizedFactorial(5); // returns from cache the second time ``` -Expiration & Invalidation -========================= +Expiration et invalidation +========================== -Lors de la mise en cache, il est nécessaire de résoudre la question de savoir quand les données précédemment stockées deviennent invalides. Le framework Nette propose un mécanisme pour limiter la validité des données ou les supprimer de manière contrôlée (dans la terminologie du framework, "invalider"). +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 est définie au moment de l'enregistrement à l'aide du troisième paramètre de la méthode `save()`, par exemple : +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, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -Ou en utilisant le paramètre `$dependencies` passé par référence au callback de la méthode `load()`, par exemple : +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) { @@ -121,48 +127,48 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -Ou en utilisant le 3ème paramètre de la méthode `load()`, par exemple : +Ou encore par le 3e paramètre de la méthode `load()` elle-même, par ex. : ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -Dans les exemples suivants, nous supposerons la deuxième variante et donc l'existence de la variable `$dependencies`. +Dans les exemples qui suivent, nous partirons de la deuxième variante, avec la variable `$dependencies` dans le callback. Expiration ---------- -L'expiration la plus simple est une limite de temps. Voici comment nous mettons en cache les données avec une validité de 20 minutes : +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 -// accepte également le nombre de secondes ou un timestamp UNIX +// accepts number of seconds or a UNIX timestamp as well $dependencies[Cache::Expire] = '20 minutes'; ``` -Si nous voulions prolonger la période de validité à chaque lecture, cela peut être réalisé comme suit, mais attention, la surcharge du cache augmentera : +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 pratique est de laisser les données expirer lorsqu'un fichier ou l'un des multiples fichiers change. Cela peut être utilisé, par exemple, lors de la mise en cache de données résultant du traitement de ces fichiers. Utilisez des chemins absolus. +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'; -// ou +// or $dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; ``` -Nous pouvons laisser un élément du cache expirer lorsqu'un autre élément (ou l'un de plusieurs autres) expire. Cela peut être utilisé lorsque nous mettons en cache, par exemple, une page HTML entière et ses fragments sous d'autres clés. Dès qu'un fragment change, toute la page est invalidée. Si nous avons des fragments stockés sous les clés `frag1` et `frag2`, par exemple, nous utilisons : +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 également être contrôlée à l'aide de fonctions personnalisées ou de méthodes statiques, qui décident à chaque lecture si l'élément est toujours valide. De cette façon, nous pouvons laisser un élément expirer chaque fois que la version de PHP change. Nous créons une fonction qui compare la version actuelle avec un paramètre, et lors de l'enregistrement, nous ajoutons un tableau de la forme `[nom de la fonction, ...arguments]` aux dépendances : +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 @@ -171,11 +177,11 @@ function checkPhpVersion($ver): bool } $dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expire quand checkPhpVersion(...) === false + ['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false ]; ``` -Bien sûr, tous les critères peuvent être combinés. Le cache expirera alors si au moins un critère n'est pas rempli. +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'; @@ -186,13 +192,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; Invalidation par tags --------------------- -Les tags sont un outil d'invalidation très utile. Nous pouvons assigner une liste de tags, qui sont des chaînes arbitraires, à chaque élément du cache. Par exemple, supposons que nous ayons une page HTML avec un article et des commentaires que nous allons mettre en cache. Lors de l'enregistrement, nous spécifions les 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 à l'administration. Ici, nous trouvons un formulaire pour éditer l'article. En même temps que l'enregistrement de l'article dans la base de données, nous appelons la commande `clean()`, qui supprime les éléments du cache par tag : +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([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -De même, à l'endroit où un nouveau commentaire est ajouté (ou un commentaire est édité), nous n'oublions pas d'invalider le tag correspondant : +De même, à l'ajout d'un nouveau commentaire (ou à sa modification), nous devons penser à invalider le tag correspondant : ```php $cache->clean([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -Qu'avons-nous accompli ? Que notre cache HTML sera invalidé (supprimé) chaque fois que l'article ou les commentaires changent. Lorsqu'un article avec l'ID = 10 est édité, le tag `article/10` est invalidé de force, et la page HTML portant ce tag est supprimée du cache. La même chose se produit lors de l'insertion d'un nouveau commentaire sous l'article correspondant. +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 ce qu'on appelle un [#Journal]. +Les tags nécessitent un [#Journal]. Invalidation par priorité ------------------------- -Nous pouvons définir une priorité pour les éléments individuels dans le cache, qui peut être utilisée pour les supprimer lorsque, par exemple, le cache dépasse une certaine taille : +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; ``` -Nous supprimons tous les éléments avec une priorité égale ou inférieure à 100 : +Pour supprimer tous les éléments dont la priorité est inférieure ou égale à 100 : ```php $cache->clean([ @@ -235,10 +241,10 @@ $cache->clean([ Les priorités nécessitent ce qu'on appelle un [#Journal]. -Suppression du cache --------------------- +Vider le cache +-------------- -Le paramètre `Cache::All` supprime tout : +Le paramètre `Cache::All` vide tout : ```php $cache->clean([ @@ -250,67 +256,76 @@ $cache->clean([ Lecture en masse ================ -Pour lire et écrire en masse dans le cache, utilisez la méthode `bulkLoad()`, à laquelle nous passons un tableau de clés et obtenons un tableau de valeurs : +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 de manière similaire à `load()` avec le deuxième paramètre callback, auquel la clé de l'élément généré est passée : +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 = /* ... */; // calcul coûteux basé sur $key + $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 l'interface PSR-16, vous pouvez utiliser l'adaptateur `PsrCacheAdapter`. Il permet une intégration transparente entre Nette Cache et tout code ou bibliothèque qui attend un cache compatible PSR-16. +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 maintenant utiliser `$psrCache` comme un cache PSR-16 : +Vous pouvez désormais utiliser `$psrCache` comme un cache PSR-16 standard : ```php -$psrCache->set('key', 'value', 3600); // stocke la valeur pendant 1 heure +$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 dans PSR-16, y compris `getMultiple()`, `setMultiple()` et `deleteMultiple()`. +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 de manière très élégante : +La sortie peut être capturée et mise en cache très élégamment : ```php if ($capture = $cache->capture($key)) { - echo ... // affichage des données + // echo ... printing some data - $capture->end(); // enregistre la sortie dans le cache + $capture->end(); // save the output to the cache } ``` -Si la sortie est déjà stockée dans le cache, la méthode `capture()` l'affiche et renvoie `null`, donc la condition n'est pas exécutée. Sinon, elle commence à capturer la sortie et renvoie l'objet `$capture`, que nous utilisons pour finalement enregistrer les données affichées dans le 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, la méthode s'appelait `$cache->start()`. +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'envelopper une partie du template avec les balises `{cache}...{/cache}`. Le cache est automatiquement invalidé lorsque le template source change (y compris les templates inclus dans le bloc de cache). Les balises `{cache}` peuvent être imbriquées, et lorsqu'un bloc imbriqué est invalidé (par exemple, par un tag), le bloc parent est également invalidé. +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 la balise, il est possible de spécifier les clés auxquelles le cache sera lié (ici la variable `$id`) et de définir l'expiration et les [tags pour l'invalidation |#Invalidation par tags]. +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]} @@ -318,9 +333,9 @@ Dans la balise, il est possible de spécifier les clés auxquelles le cache sera {/cache} ``` -Tous les paramètres sont facultatifs, nous n'avons donc pas besoin de spécifier l'expiration, les tags ou même les clés. +Tous ces paramètres sont facultatifs : vous n'avez à indiquer ni l'expiration, ni les tags, ni même les clés. -L'utilisation du cache peut également être conditionnée à l'aide de `if` - le contenu ne sera alors mis en cache que si la condition est remplie : +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()} @@ -332,20 +347,20 @@ L'utilisation du cache peut également être conditionnée à l'aide de `if` - l Stockages ========= -Un stockage est un objet représentant l'endroit où les données sont physiquement stockées. Nous pouvons utiliser une base de données, un serveur Memcached ou le stockage le plus accessible, qui sont des fichiers sur disque. +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 avec enregistrement dans des fichiers sur disque -| [#MemcachedStorage]| utilise un serveur `Memcached` -| [#MemoryStorage] | les données sont temporairement en mémoire -| [#SQLiteStorage] | les données sont stockées dans une base de données SQLite -| [#DevNullStorage] | les données ne sont pas stockées, adapté aux tests +|---------------------- +| 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 accédez à l'objet de stockage en le faisant passer via [l'injection de dépendances |dependency-injection:passing-dependencies] avec le type `Nette\Caching\Storage`. Nette fournit par défaut l'objet `FileStorage`, qui stocke les données dans le sous-répertoire `cache` du répertoire des [fichiers temporaires |application:bootstrapping#Fichiers Temporaires]. +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 modifier le stockage dans la configuration : +Vous pouvez changer le stockage par défaut dans la configuration : ```neon services: @@ -356,14 +371,14 @@ services: FileStorage ----------- -Écrit le cache dans des fichiers sur disque. Le stockage `Nette\Caching\Storages\FileStorage` est très bien optimisé pour les performances et garantit surtout une atomicité complète des opérations. Qu'est-ce que cela signifie ? Que lors de l'utilisation du cache, il ne peut pas arriver que nous lisions un fichier qui n'a pas encore été complètement écrit par un autre thread, ou qu'il soit supprimé "sous nos yeux". L'utilisation du cache est donc totalement sûre. +É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 dispose également d'une fonction intégrée importante qui empêche une augmentation extrême de l'utilisation du processeur lorsque le cache est supprimé ou n'est pas encore chaud (c'est-à-dire créé). C'est une prévention contre le "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Il arrive qu'un grand nombre de requêtes simultanées arrivent en même temps, demandant la même chose au cache (par exemple, le résultat d'une requête SQL coûteuse), et comme il n'est pas dans le cache, tous les processus commencent à exécuter la même requête SQL. La charge se multiplie ainsi et il peut même arriver qu'aucun thread ne parvienne à répondre dans le délai imparti, que le cache ne soit pas créé et que l'application plante. Heureusement, le cache de Nette fonctionne de telle manière que lors de plusieurs requêtes simultanées pour un même élément, seul le premier thread le génère, les autres attendent et utilisent ensuite le résultat généré. +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 de `FileStorage` : +Exemple de création d'un `FileStorage` : ```php -// le stockage sera le répertoire '/path/to/temp' sur le disque +// the storage will be the directory '/path/to/temp' on disk $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); ``` @@ -371,7 +386,7 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -Le serveur [Memcached |https://memcached.org] est un système de stockage distribué en mémoire très performant, dont l'adaptateur est `Nette\Caching\Storages\MemcachedStorage`. Dans la configuration, nous spécifions l'adresse IP et le port, s'ils diffèrent du port standard 11211. +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`. @@ -385,13 +400,13 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` est un stockage qui enregistre les données dans un tableau PHP, et donc elles sont perdues à la fin de la requête. +`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 et l'adaptateur `Nette\Caching\Storages\SQLiteStorage` offrent un moyen de stocker le cache dans un seul fichier sur disque. Dans la configuration, nous spécifions le chemin d'accès à ce fichier. +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`. @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -Une implémentation spéciale de stockage est `Nette\Caching\Storages\DevNullStorage`, qui en réalité ne stocke aucune donnée. Il est donc adapté aux tests lorsque nous voulons éliminer l'influence du cache. +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 ================================= -Lors de l'utilisation du cache dans le code, nous avons deux façons de procéder. La première consiste à recevoir le stockage via [l'injection de dépendances |dependency-injection:passing-dependencies] et à créer un objet `Cache` : +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; @@ -427,7 +442,7 @@ class ClassOne } ``` -La deuxième option est de recevoir directement l'objet `Cache` : +La seconde consiste à se faire passer directement l'objet `Cache` : ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -L'objet `Cache` est ensuite créé directement dans la configuration de cette manière : +L'objet `Cache` doit alors être défini dans la configuration, par exemple ainsi : ```neon services: @@ -450,9 +465,9 @@ services: Journal ======= -Nette stocke les tags et les priorités dans ce qu'on appelle un journal. Par défaut, SQLite et le fichier `journal.s3db` sont utilisés pour cela, et **les extensions PHP `pdo` et `pdo_sqlite` sont requises.** +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 modifier le journal dans la configuration : +Vous pouvez changer l'implémentation du journal dans la configuration : ```neon services: @@ -465,20 +480,23 @@ Services DI Ces services sont ajoutés au conteneur DI : -| Nom | Type | Description -|-----------------|------------------------------------------|---------------------------------------------------------- -| `cache.journal` | `Nette\Caching\Storages\Journal` | Le journal utilisé pour les tags et priorités (par défaut SQLiteJournal) -| `cache.storage` | `Nette\Caching\Storage` | Le stockage de cache par défaut (par défaut FileStorage) +| 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ésactivation du cache -====================== +Désactiver le cache +=================== -Une des options pour désactiver le cache dans l'application est de définir le stockage sur [#DevNullStorage] : +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 paramètre n'affecte pas la mise en cache des templates dans Latte ou du conteneur DI, car ces bibliothèques n'utilisent pas les services nette/caching et gèrent leur propre cache indépendamment. D'ailleurs, leur cache [n'a pas besoin d'être désactivé |nette:troubleshooting#Comment désactiver le cache pendant le développement] en mode développeur. +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 new file mode 100644 index 0000000000..36b3c82411 --- /dev/null +++ b/caching/fr/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 95ec8a4ef6..72ae4b8db8 100644 --- a/caching/fr/@meta.texy +++ b/caching/fr/@meta.texy @@ -1,2 +1 @@ {{sitename: Documentation Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/fr/upgrading.texy b/caching/fr/upgrading.texy new file mode 100644 index 0000000000..1d6e687fce --- /dev/null +++ b/caching/fr/upgrading.texy @@ -0,0 +1,20 @@ +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/hu/@home.texy b/caching/hu/@home.texy deleted file mode 100644 index 25518b3721..0000000000 --- a/caching/hu/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -A Cache felgyorsítja az alkalmazást azáltal, hogy az egyszer nehezen megszerzett adatokat elmenti a későbbi felhasználásra. Megmutatjuk: - -- hogyan használjuk a cache-t -- hogyan változtassuk meg a tárolót -- hogyan érvénytelenítsük helyesen a cache-t - -</div> - -A cache használata a Nette-ben nagyon egyszerű, miközben nagyon fejlett igényeket is lefed. Teljesítményre és 100%-os ellenállóságra tervezték. Alapból adaptereket talál a leggyakoribb háttértárolókhoz. Lehetővé teszi a tag-ek alapján történő érvénytelenítést, az időbeli lejárást, védelmet nyújt a cache stampede ellen stb. - - -Telepítés -========= - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/caching -``` - - -Alapvető használat -================== - -A cache-sel vagy gyorsítótárral való munka középpontjában az [api:Nette\Caching\Cache] objektum áll. Létrehozunk egy példányt belőle, és paraméterként átadjuk a konstruktornak az úgynevezett tárolót. Ez egy olyan objektum, amely azt a helyet képviseli, ahol az adatok fizikailag tárolódnak (adatbázis, Memcached, fájlok a lemezen, ...). A tárolóhoz úgy juthatunk hozzá, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével kérjük át a `Nette\Caching\Storage` típussal. Minden lényegeset megtudhat a [Tárolók szakaszban |#Tárolók]. - -.[warning] -A 3.0-s verzióban az interfésznek még volt `I` előtagja, tehát a neve `Nette\Caching\IStorage` volt. Továbbá a `Cache` osztály konstansai nagybetűkkel voltak írva, tehát például `Cache::EXPIRE` a `Cache::Expire` helyett. - -A következő példákhoz feltételezzük, hogy létrehoztunk egy `Cache` aliast, és a `$storage` változóban van a tároló. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -A cache valójában egy *key–value store*, tehát az adatokat kulcsok alatt olvassuk és írjuk, ugyanúgy, mint az asszociatív tömböknél. Az alkalmazások számos független részből állnak, és ha mindegyik ugyanazt a tárolót használná (képzeljünk el egyetlen könyvtárat a lemezen), előbb-utóbb kulcsütközés következne be. A Nette Framework ezt a problémát úgy oldja meg, hogy az egész teret névtérekre (alkönyvtárakra) osztja. Minden programrész ezután a saját, egyedi nevű terét használja, és így már nem fordulhat elő ütközés. - -A névtér nevét a Cache osztály konstruktorának második paramétereként adjuk meg: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Most már a `$cache` objektum segítségével olvashatunk a gyorsítótárból és írhatunk bele. Mindkettőre a `load()` metódus szolgál. Az első argumentum a kulcs, a második pedig egy PHP callback, amely akkor hívódik meg, ha a kulcs nem található a cache-ben. A callback generálja az értéket, visszaadja, és az elmentődik a cache-be: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // költséges számítás - return $computedValue; -}); -``` - -Ha a második paramétert nem adjuk meg `$value = $cache->load($key)`, akkor `null`-t ad vissza, ha az elem nincs a cache-ben. - -.[tip] -Nagyszerű, hogy a cache-be bármilyen szerializálható struktúrát tárolhatunk, nem csak stringeket. És ugyanez igaz még a kulcsokra is. - -Az elemet a gyorsítótárból a `remove()` metódussal töröljük: - -```php -$cache->remove($key); -``` - -Elemet a gyorsítótárba a `$cache->save($key, $value, array $dependencies = [])` metódussal is menthetünk. Azonban a fentebb bemutatott `load()` használata preferált. - - -Memoizáció -========== - -A memoizáció egy függvény vagy metódus hívásának eredményének gyorsítótárazását jelenti, hogy legközelebb újra felhasználhassuk anélkül, hogy újra kiszámítanánk ugyanazt. - -Metódusokat és függvényeket memoizáltan hívhatunk a `call(callable $callback, ...$args)` segítségével: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -A `gethostbyaddr()` függvény így minden `$ip` paraméterre csak egyszer hívódik meg, és legközelebb már a cache-ből adódik vissza az érték. - -Lehetőség van arra is, hogy egy memoizált burkolót hozzunk létre egy metódus vagy függvény köré, amelyet később hívhatunk meg: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // először kiszámítja -$result = $memoizedFactorial(5); // másodszor a cache-ből -``` - - -Lejárat & érvénytelenítés -========================= - -A cache-be való mentéskor felmerül a kérdés, hogy a korábban elmentett adatok mikor válnak érvénytelenné. A Nette Framework egy mechanizmust kínál az adatok érvényességének korlátozására vagy azok irányított törlésére (a keretrendszer terminológiájában „érvénytelenítésére”). - -Az adatok érvényességét a mentéskor állítjuk be a `save()` metódus harmadik paraméterével, pl.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Vagy a `load()` metódus callbackjének referenciaként átadott `$dependencies` paraméterével, pl.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Vagy a `load()` metódus 3. paraméterével, pl.: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -A további példákban a második változatot feltételezzük, és így a `$dependencies` változó létezését. - - -Lejárat -------- - -A legegyszerűbb lejárat az időkorlát. Így 20 perces érvényességgel mentünk adatokat a cache-be: - -```php -// elfogadja a másodpercek számát vagy UNIX timestamp-et is -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Ha minden olvasással meg szeretnénk hosszabbítani az érvényességi időt, azt a következőképpen érhetjük el, de vigyázat, a cache rezsije ezzel megnő: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Ügyes lehetőség, hogy az adatokat akkor járassuk le, amikor egy fájl vagy több fájl közül valamelyik megváltozik. Ezt például akkor használhatjuk, ha ezeknek a fájloknak a feldolgozásából származó adatokat mentjük a cache-be. Használjon abszolút elérési utakat. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// vagy -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Lejárathatunk egy elemet a cache-ben akkor, amikor egy másik elem (vagy több másik közül valamelyik) lejár. Ezt akkor használhatjuk, ha például egy egész HTML oldalt mentünk a cache-be, és más kulcsok alatt annak töredékeit. Amint egy töredék megváltozik, az egész oldal érvénytelenné válik. Ha a töredékeket pl. `frag1` és `frag2` kulcsok alatt tároljuk, használjuk ezt: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -A lejáratot saját függvényekkel vagy statikus metódusokkal is vezérelhetjük, amelyek minden olvasáskor eldöntik, hogy az elem még érvényes-e. Így például lejárathatunk egy elemet mindig, amikor a PHP verziója megváltozik. Létrehozunk egy függvényt, amely összehasonlítja az aktuális verziót a paraméterrel, és a mentéskor hozzáadjuk a függőségek közé a `[függvény neve, ...argumentumok]` formátumú tömböt: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // járjon le, ha checkPhpVersion(...) === false -]; -``` - -Természetesen minden kritérium kombinálható. A cache akkor jár le, ha legalább egy kritérium nem teljesül. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Érvénytelenítés tag-ekkel -------------------------- - -Nagyon hasznos érvénytelenítő eszközök az úgynevezett tag-ek. Minden cache-beli elemhez hozzárendelhetünk egy tag-listát, amelyek tetszőleges stringek. Legyen például egy HTML oldalunk egy cikkel és hozzászólásokkal, amelyet gyorsítótárazni fogunk. Mentéskor megadjuk a tag-eket: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Lépjünk át az adminisztrációba. Itt találunk egy űrlapot a cikk szerkesztéséhez. A cikk adatbázisba mentésével együtt meghívjuk a `clean()` parancsot, amely törli a cache-ből az elemeket a tag alapján: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Ugyanígy az új hozzászólás hozzáadásának (vagy egy hozzászólás szerkesztésének) helyén ne felejtsük el érvényteleníteni a megfelelő tag-et: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Mit értünk el ezzel? Azt, hogy a HTML cache érvénytelenné válik (törlődik), amikor a cikk vagy a hozzászólások megváltoznak. Ha egy 10-es ID-jú cikket szerkesztünk, akkor kényszerített érvénytelenítés történik az `article/10` tag-re, és a HTML oldal, amely ezt a tag-et hordozza, törlődik a cache-ből. Ugyanez történik egy új hozzászólás beszúrásakor a megfelelő cikk alá. - -.[note] -A tag-ekhez úgynevezett [#Journal] szükséges. - - -Érvénytelenítés prioritással ----------------------------- - -Az egyes cache-elemekhez beállíthatunk prioritást, amellyel törölhetjük őket, ha például a cache meghalad egy bizonyos méretet: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Töröljük az összes elemet, amelyek prioritása 100 vagy annál kisebb: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -A prioritásokhoz úgynevezett [#Journal] szükséges. - - -Cache törlése -------------- - -A `Cache::All` paraméter mindent töröl: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Tömeges olvasás -=============== - -A cache-ből való tömeges olvasásra és írásra a `bulkLoad()` metódus szolgál, amelynek átadunk egy kulcstömböt, és egy értéktömböt kapunk vissza: - -```php -$values = $cache->bulkLoad($keys); -``` - -A `bulkLoad()` metódus hasonlóan működik, mint a `load()`, a második paraméter callbackkel is, amelynek átadódik a generált elem kulcsa: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // költséges számítás - return $computedValue; -}); -``` - - -Használat PSR-16-tal .{data-version:3.3.1} -========================================== - -A Nette Cache PSR-16 interfésszel való használatához használhatja a `PsrCacheAdapter` adaptert. Lehetővé teszi a zökkenőmentes integrációt a Nette Cache és bármely olyan kód vagy könyvtár között, amely PSR-16 kompatibilis cache-t vár. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Most már használhatja a `$psrCache`-t PSR-16 cache-ként: - -```php -$psrCache->set('key', 'value', 3600); // 1 órára menti az értéket -$value = $psrCache->get('key', 'default'); -``` - -Az adapter támogatja az összes PSR-16-ban definiált metódust, beleértve a `getMultiple()`, `setMultiple()` és `deleteMultiple()` metódusokat is. - - -Kimenet gyorsítótárazása -======================== - -Nagyon elegánsan lehet a kimenetet elfogni és gyorsítótárazni: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // kiírjuk az adatokat - - $capture->end(); // elmentjük a kimenetet a cache-be -} -``` - -Abban az esetben, ha a kimenet már a cache-ben van, a `capture()` metódus kiírja azt és `null`-t ad vissza, tehát a feltétel nem teljesül. Ellenkező esetben elkezdi a kimenet elfogását és visszaadja a `$capture` objektumot, amelynek segítségével végül elmentjük a kiírt adatokat a cache-be. - -.[note] -A 3.0-s verzióban a metódus neve `$cache->start()` volt. - - -Gyorsítótárazás Latte-ban -========================= - -A sablonokban való gyorsítótárazás a [Latte|latte:]-ban nagyon egyszerű, csak a sablon egy részét kell `{cache}...{/cache}` tagekkel körbevenni. A cache automatikusan érvénytelenné válik, amikor a forrás sablon megváltozik (beleértve az esetlegesen beillesztett sablonokat a cache blokkon belül). A `{cache}` tagek egymásba ágyazhatók, és ha egy beágyazott blokk érvénytelenné válik (például egy tag miatt), akkor a fölérendelt blokk is érvénytelenné válik. - -A tagben megadhatók kulcsok, amelyekhez a cache kötődni fog (itt a `$id` változó), és beállítható a lejárat és a [címkék az érvénytelenítéshez |#Érvénytelenítés tag-ekkel]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Minden elem opcionális, így nem kell megadnunk sem a lejáratot, sem a címkéket, végül még a kulcsokat sem. - -A cache használata feltételhez is köthető az `if` segítségével - a tartalom csak akkor lesz gyorsítótárazva, ha a feltétel teljesül: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Tárolók -======= - -A tároló egy objektum, amely azt a helyet képviseli, ahol az adatok fizikailag tárolódnak. Használhatunk adatbázist, Memcached szervert, vagy a leginkább elérhető tárolót, ami a lemezen lévő fájlok. - -|----------------- -| Tároló | Leírás -|----------------- -| [#FileStorage] | alapértelmezett tároló, amely a lemezen lévő fájlokba ment -| [#MemcachedStorage] | `Memcached` szervert használ -| [#MemoryStorage] | az adatok ideiglenesen a memóriában vannak -| [#SQLiteStorage] | az adatok SQLite adatbázisba mentődnek -| [#DevNullStorage] | az adatok nem mentődnek, tesztelésre alkalmas - -A tároló objektumhoz úgy juthat hozzá, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével kéri át a `Nette\Caching\Storage` típussal. Alapértelmezett tárolóként a Nette a FileStorage objektumot biztosítja, amely az adatokat az [ideiglenes fájlok |application:bootstrapping#Ideiglenes fájlok] könyvtárában lévő `cache` alkönyvtárba menti. - -A tárolót a konfigurációban módosíthatja: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -A cache-t fájlokba írja a lemezen. A `Nette\Caching\Storages\FileStorage` tároló nagyon jól optimalizált a teljesítményre, és mindenekelőtt biztosítja a műveletek teljes atomicitását. Mit jelent ez? Azt, hogy a cache használatakor nem fordulhat elő, hogy olyan fájlt olvassunk be, amelyet egy másik szál még nem írt ki teljesen, vagy hogy valaki "a kezünk alól" törölje azt. A cache használata tehát teljesen biztonságos. - -Ez a tároló egy fontos beépített funkcióval is rendelkezik, amely megakadályozza a CPU extrém kihasználtságának növekedését abban a pillanatban, amikor a cache törlődik vagy még nincs felmelegítve (azaz létrehozva). Ez a "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede elleni védelem. Előfordul, hogy egy időben több párhuzamos kérés érkezik, amelyek ugyanazt a dolgot akarják a cache-ből (pl. egy drága SQL lekérdezés eredményét), és mivel az nincs a gyorsítótárban, minden folyamat ugyanazt az SQL lekérdezést kezdi el végrehajtani. A terhelés így megsokszorozódik, és akár az is előfordulhat, hogy egyetlen szál sem tud válaszolni az időkorláton belül, a cache nem jön létre, és az alkalmazás összeomlik. Szerencsére a Nette cache úgy működik, hogy több párhuzamos kérés esetén egy elemre csak az első szál generálja azt, a többiek várnak, majd felhasználják a generált eredményt. - -Példa a FileStorage létrehozására: - -```php -// a tároló a '/path/to/temp' könyvtár lesz a lemezen -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -A [Memcached|https://memcached.org] szerver egy nagy teljesítményű, elosztott memóriában történő tárolási rendszer, amelynek adaptere a `Nette\Caching\Storages\MemcachedStorage`. A konfigurációban megadjuk az IP-címet és a portot, ha az eltér a standard 11211-től. - -.[caution] -Szükséges a `memcached` PHP kiterjesztés. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -A `Nette\Caching\Storages\MemoryStorage` egy olyan tároló, amely az adatokat egy PHP tömbben tárolja, és így a kérés befejeztével elvesznek. - - -SQLiteStorage -------------- - -Az SQLite adatbázis és az `Nette\Caching\Storages\SQLiteStorage` adapter lehetőséget kínál a cache egyetlen fájlba történő mentésére a lemezen. A konfigurációban megadjuk ennek a fájlnak az elérési útját. - -.[caution] -Szükséges a `pdo` és `pdo_sqlite` PHP kiterjesztés. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -A tároló speciális implementációja a `Nette\Caching\Storages\DevNullStorage`, amely valójában egyáltalán nem tárol adatokat. Így tesztelésre alkalmas, amikor ki akarjuk küszöbölni a cache hatását. - - -Cache használata a kódban -========================= - -A cache kódban való használatakor kétféleképpen járhatunk el. Az első az, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével átkérjük a tárolót, és létrehozunk egy `Cache` objektumot: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -A második lehetőség az, hogy közvetlenül a `Cache` objektumot kérjük át: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -A `Cache` objektumot ezután közvetlenül a konfigurációban hozzuk létre ezzel a módszerrel: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -A Nette a címkéket és prioritásokat az úgynevezett journalba menti. Alapértelmezés szerint ehhez SQLite-ot és a `journal.s3db` fájlt használja, és **szükséges a `pdo` és `pdo_sqlite` PHP kiterjesztés.** - -A journalt a konfigurációban módosíthatja: - -```neon -services: - cache.journal: MyJournal -``` - - -DI szolgáltatások -================= - -Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez: - -| Név | Típus | Leírás -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | tároló - - -Cache kikapcsolása -================== - -Az alkalmazásban a cache kikapcsolásának egyik módja, ha a [#DevNullStorage]-t állítjuk be tárolóként: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Ez a beállítás nincs hatással a sablonok gyorsítótárazására a Latte-ban vagy a DI konténerben, mivel ezek a könyvtárak nem használják a nette/caching szolgáltatásait, és önállóan kezelik a cache-t. Egyébként a cache-üket [nem szükséges kikapcsolni |nette:troubleshooting#Hogyan kapcsoljuk ki a cache-t fejlesztés közben] fejlesztői módban. diff --git a/caching/hu/@meta.texy b/caching/hu/@meta.texy deleted file mode 100644 index c00a2158aa..0000000000 --- a/caching/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette dokumentáció}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/it/@home.texy b/caching/it/@home.texy index 3b9e0dba0b..371a69ab1b 100644 --- a/caching/it/@home.texy +++ b/caching/it/@home.texy @@ -3,88 +3,94 @@ Nette Caching <div class=perex> -La cache accelera la vostra applicazione salvando i dati ottenuti con fatica una volta per un uso futuro. Vedremo: +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 lo storage +- come cambiare il backend di storage - come invalidare correttamente la cache </div> -L'uso della cache in Nette è molto semplice, ma copre anche esigenze molto avanzate. È progettato per le prestazioni e una resistenza del 100%. Di base, si trovano adattatori per gli storage di backend più comuni. Supporta l'invalidazione basata su tag, la scadenza temporale, ha protezione contro il cache stampede, ecc. +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 ============= -Potete scaricare e installare la libreria utilizzando lo strumento [Composer|best-practices:composer]: +Il pacchetto si scarica e si installa con [Composer|best-practices:composer]: ```shell composer require nette/caching ``` -Utilizzo di base -================ +Uso di base +=========== -Il fulcro del lavoro con la cache è l'oggetto [api:Nette\Caching\Cache]. Creiamo la sua istanza e passiamo al costruttore il cosiddetto storage come parametro. Questo è un oggetto che rappresenta il luogo in cui i dati verranno fisicamente salvati (database, Memcached, file su disco, ...). Possiamo accedere allo storage facendocelo passare tramite [dependency injection |dependency-injection:passing-dependencies] con il tipo `Nette\Caching\Storage`. Troverete tutto l'essenziale nella [sezione Storage |#Storage]. +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 il nome era `Nette\Caching\IStorage`. Inoltre, le costanti della classe `Cache` erano scritte in maiuscolo, quindi ad esempio `Cache::EXPIRE` invece di `Cache::Expire`. +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 creato un alias `Cache` e di avere lo storage nella variabile `$storage`. +Per gli esempi seguenti supponiamo di avere un alias `Cache` e un'istanza di storage nella variabile `$storage`. ```php use Nette\Caching\Cache; -$storage = /* ... */; // instance of Nette\Caching\Storage +$storage = /* ... */; // istanza di Nette\Caching\Storage ``` -La cache è essenzialmente un *key-value store*, quindi leggiamo e scriviamo dati sotto chiavi proprio come con gli array associativi. Le applicazioni sono composte da una serie di parti indipendenti e se tutte utilizzassero un unico storage (immaginate una singola directory su disco), prima o poi si verificherebbe una collisione di chiavi. Nette Framework risolve il problema dividendo l'intero spazio in namespace (sottodirectory). Ogni parte del programma utilizza quindi il proprio spazio con un nome univoco e non può più verificarsi alcuna collisione. +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 dello spazio è specificato come secondo parametro del costruttore della classe Cache: +Il nome del namespace si indica come secondo argomento del costruttore della classe `Cache`: ```php $cache = new Cache($storage, 'Full Html Pages'); ``` -Ora possiamo usare l'oggetto `$cache` per leggere e scrivere dalla cache. A tal fine serve il metodo `load()`. Il primo argomento è la chiave e il secondo è un callback PHP che viene chiamato quando la chiave non viene trovata nella cache. Il callback genera il valore, lo restituisce e viene salvato nella cache: +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 complesso + $computedValue = /* ... */; // calcolo costoso return $computedValue; }); ``` -Se il secondo parametro non viene specificato `$value = $cache->load($key)`, verrà restituito `null` se l'elemento non è nella cache. +Se il secondo parametro viene omesso (`$value = $cache->load($key)`), `load()` restituisce `null` se l'elemento non si trova nella cache. .[tip] -Fantastico è che nella cache è possibile salvare qualsiasi struttura serializzabile, non devono essere solo stringhe. E lo stesso vale anche per le chiavi. +È ottimo che si possano mettere in cache non solo stringhe, ma qualsiasi struttura serializzabile. Lo stesso vale per le chiavi. -L'elemento dalla cache viene cancellato con il metodo `remove()`: +Per cancellare un elemento dalla cache serve il metodo `remove()`: ```php $cache->remove($key); ``` -È anche possibile salvare un elemento nella cache con il metodo `$cache->save($key, $value, array $dependencies = [])`. Tuttavia, è preferibile il metodo sopra menzionato che utilizza `load()`. +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 significa memorizzare nella cache il risultato di una chiamata a una funzione o a un metodo, in modo da poterlo utilizzare la prossima volta senza calcolare nuovamente la stessa cosa. +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. -È possibile chiamare metodi e funzioni in modo memoizzato usando `call(callable $callback, ...$args)`: +Metodi e funzioni si possono chiamare in modo memoizzato con `call(callable $callback, ...$args)`: ```php $result = $cache->call('gethostbyaddr', $ip); ``` -La funzione `gethostbyaddr()` viene chiamata solo una volta per ogni parametro `$ip` e la prossima volta verrà restituito il valore dalla cache. +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. -È anche possibile creare un wrapper memoizzato su un metodo o una funzione che può essere chiamato in seguito: +Si può anche creare un wrapper memoizzato attorno a un metodo o a una funzione, da chiamare in seguito: ```php function factorial($num) @@ -94,17 +100,17 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // calcola la prima volta -$result = $memoizedFactorial(5); // la seconda volta dalla cache +$result = $memoizedFactorial(5); // la prima volta lo calcola +$result = $memoizedFactorial(5); // la seconda volta lo restituisce dalla cache ``` -Scadenza & Invalidazione +Scadenza e invalidazione ======================== -Con il salvataggio nella cache, è necessario risolvere la questione di quando i dati precedentemente salvati diventano non validi. Nette Framework offre un meccanismo per limitare la validità dei dati o cancellarli in modo controllato (nella terminologia del framework "invalidare"). +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 viene impostata al momento del salvataggio tramite il terzo parametro del metodo `save()`, ad esempio: +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, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -Oppure tramite il parametro `$dependencies` passato per riferimento al callback del metodo `load()`, ad esempio: +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) { @@ -121,34 +127,34 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -Oppure tramite il 3° parametro nel metodo `load()`, ad esempio: +Oppure usando il terzo parametro del metodo `load()` stesso, per esempio: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -Negli esempi successivi, assumeremo la seconda variante e quindi l'esistenza della variabile `$dependencies`. +Negli esempi seguenti supporremo la seconda variante, quella che usa la variabile `$dependencies` dentro il callback. Scadenza -------- -La scadenza più semplice è un limite di tempo. In questo modo salviamo nella cache i dati con validità di 20 minuti: +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 o il timestamp UNIX +// accetta anche il numero di secondi oppure un timestamp UNIX $dependencies[Cache::Expire] = '20 minutes'; ``` -Se volessimo estendere il periodo di validità ad ogni lettura, è possibile farlo nel modo seguente, ma attenzione, il sovraccarico della cache aumenterà: +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; ``` -È utile la possibilità di far scadere i dati nel momento in cui cambia un file o uno dei più file. Questo può essere utilizzato, ad esempio, per salvare nella cache i dati derivanti dall'elaborazione di questi file. Utilizzare percorsi assoluti. +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'; @@ -156,13 +162,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; $dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; ``` -Possiamo far scadere un elemento nella cache nel momento in cui scade un altro elemento (o uno dei più altri). Questo può essere utilizzato quando salviamo nella cache, ad esempio, un'intera pagina HTML e, sotto altre chiavi, i suoi frammenti. Non appena un frammento cambia, l'intera pagina viene invalidata. Se abbiamo i frammenti salvati sotto le chiavi, ad esempio `frag1` e `frag2`, useremo: +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 può essere controllata anche tramite funzioni personalizzate o metodi statici, che decidono sempre alla lettura se l'elemento è ancora valido. In questo modo, ad esempio, possiamo far scadere l'elemento ogni volta che cambia la versione di PHP. Creiamo una funzione che confronta la versione attuale con il parametro e, durante il salvataggio, aggiungiamo tra le dipendenze un array nella forma `[nome funzione, ...argomenti]`: +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 @@ -175,7 +181,7 @@ $dependencies[Cache::Callbacks] = [ ]; ``` -Ovviamente è possibile combinare tutti i criteri. La cache scadrà quindi quando almeno un criterio non è soddisfatto. +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'; @@ -186,13 +192,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; Invalidazione tramite tag ------------------------- -Uno strumento di invalidazione molto utile sono i cosiddetti tag. A ogni elemento nella cache possiamo assegnare un elenco di tag, che sono stringhe arbitrarie. Supponiamo di avere una pagina HTML con un articolo e commenti, che memorizzeremo nella cache. Durante il salvataggio, specifichiamo i 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"]; ``` -Spostiamoci nell'amministrazione. Qui troviamo un form per modificare l'articolo. Insieme al salvataggio dell'articolo nel database, chiamiamo il comando `clean()`, che elimina gli elementi dalla cache in base al tag: +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([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -Allo stesso modo, nel punto di aggiunta di un nuovo commento (o modifica di un commento), non dimentichiamo di invalidare il tag corrispondente: +Allo stesso modo, quando aggiungiamo un nuovo commento (o ne modifichiamo uno), dobbiamo ricordarci di invalidare il tag corrispondente: ```php $cache->clean([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -Cosa abbiamo ottenuto con questo? Che la nostra cache HTML verrà invalidata (cancellata) ogni volta che l'articolo o i commenti cambiano. Quando viene modificato l'articolo con ID = 10, viene forzata l'invalidazione del tag `article/10` e la pagina HTML che porta il tag specificato viene eliminata dalla cache. Lo stesso accade quando viene inserito un nuovo commento sotto l'articolo corrispondente. +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 il cosiddetto [#Journal]. +I tag richiedono un [#Journal]. -Invalidazione tramite priorità ------------------------------- +Invalidazione per priorità +-------------------------- -Ai singoli elementi nella cache possiamo impostare una priorità, tramite la quale sarà possibile eliminarli, ad esempio, quando la cache supera una certa dimensione: +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; ``` -Eliminiamo tutti gli elementi con priorità pari o inferiore a 100: +Per cancellare tutti gli elementi con priorità minore o uguale a 100: ```php $cache->clean([ @@ -235,8 +241,8 @@ $cache->clean([ Le priorità richiedono il cosiddetto [#Journal]. -Cancellazione della cache -------------------------- +Svuotare la cache +----------------- Il parametro `Cache::All` cancella tutto: @@ -247,70 +253,79 @@ $cache->clean([ ``` -Lettura di massa -================ +Lettura in blocco +================= -Per la lettura e la scrittura di massa nella cache serve il metodo `bulkLoad()`, al quale passiamo un array di chiavi e otteniamo un array di valori: +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()` anche con il secondo parametro callback, al quale viene passata la chiave dell'elemento generato: +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 complesso + $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']); +``` -Utilizzo con PSR-16 .{data-version:3.3.1} -========================================= -Per utilizzare Nette Cache con l'interfaccia PSR-16, è possibile utilizzare l'adattatore `PsrCacheAdapter`. Consente un'integrazione senza soluzione di continuità tra Nette Cache e qualsiasi codice o libreria che si aspetta una cache compatibile con PSR-16. +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 è possibile usare `$psrCache` come cache PSR-16: +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, inclusi `getMultiple()`, `setMultiple()` e `deleteMultiple()`. +L'adattatore supporta tutti i metodi definiti in PSR-16, compresi `getMultiple()`, `setMultiple()` e `deleteMultiple()`. -Caching dell'output -=================== +Cache dell'output +================= -È possibile catturare e memorizzare nella cache l'output in modo molto elegante: +L'output si può catturare e mettere in cache in modo molto elegante: ```php if ($capture = $cache->capture($key)) { - echo ... // stampiamo i dati + // echo ... stampa di alcuni dati - $capture->end(); // salviamo l'output nella cache + $capture->end(); // salva l'output nella cache } ``` -Nel caso in cui l'output sia già memorizzato nella cache, il metodo `capture()` lo stamperà e restituirà `null`, quindi la condizione non verrà eseguita. In caso contrario, inizierà a catturare l'output e restituirà l'oggetto `$capture`, tramite il quale infine salveremo i dati stampati 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, il metodo si chiamava `$cache->start()`. +Nella versione 3.0 questo metodo si chiamava `$cache->start()`. -Caching in Latte -================ +Cache in Latte +============== -Il caching nei template [Latte|latte:] è molto semplice, basta racchiudere una parte del template con i tag `{cache}...{/cache}`. La cache viene invalidata automaticamente nel momento in cui cambia il template sorgente (inclusi eventuali template inclusi all'interno del blocco cache). I tag `{cache}` possono essere annidati e quando un blocco annidato viene invalidato (ad esempio tramite un tag), viene invalidato anche il blocco genitore. +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. -Nel tag è possibile specificare le chiavi a cui la cache sarà legata (qui la variabile `$id`) e impostare la scadenza e i [tag per l'invalidazione |#Invalidazione tramite tag]. +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]} @@ -318,9 +333,9 @@ Nel tag è possibile specificare le chiavi a cui la cache sarà legata (qui la v {/cache} ``` -Tutti gli elementi sono opzionali, quindi non dobbiamo specificare né la scadenza, né i tag, e infine nemmeno le chiavi. +Tutti questi parametri sono facoltativi, quindi non dovete indicare né la scadenza, né i tag, né le chiavi. -L'uso della cache può anche essere condizionato tramite `if` - il contenuto verrà quindi memorizzato nella cache solo se la condizione è soddisfatta: +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()} @@ -332,20 +347,20 @@ L'uso della cache può anche essere condizionato tramite `if` - il contenuto ver Storage ======= -Lo storage è un oggetto che rappresenta il luogo in cui i dati vengono fisicamente salvati. Possiamo usare un database, un server Memcached, o lo storage più accessibile, che sono i file su disco. +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 con salvataggio in file su disco -| [#MemcachedStorage] | utilizza il server `Memcached` -| [#MemoryStorage] | i dati sono temporaneamente in memoria -| [#SQLiteStorage] | i dati vengono salvati in un database SQLite -| [#DevNullStorage] | i dati non vengono salvati, adatto per i test +|---------------------- +| [#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. -Si accede all'oggetto storage facendoselo passare tramite [dependency injection |dependency-injection:passing-dependencies] con il tipo `Nette\Caching\Storage`. Come storage predefinito, Nette fornisce l'oggetto FileStorage che salva i dati nella sottodirectory `cache` nella directory per i [file temporanei |application:bootstrapping#File Temporanei]. +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]. -È possibile modificare lo storage nella configurazione: +Lo storage predefinito lo potete cambiare nella configurazione: ```neon services: @@ -356,11 +371,11 @@ services: FileStorage ----------- -Scrive la cache in file su disco. Lo storage `Nette\Caching\Storages\FileStorage` è molto ben ottimizzato per le prestazioni e soprattutto garantisce la piena atomicità delle operazioni. Cosa significa? Che quando si utilizza la cache, non può accadere di leggere un file che non è ancora stato completamente scritto da un altro thread, o che qualcuno ve lo cancelli "sotto il naso". L'uso della cache è quindi completamente sicuro. +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 ha anche una funzione importante integrata che previene un aumento estremo dell'utilizzo della CPU nel momento in cui la cache viene cancellata o non è ancora "calda" (cioè creata). Si tratta di una prevenzione contro il "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Succede che, in un dato momento, un gran numero di richieste simultanee arrivino, chiedendo la stessa cosa dalla cache (ad esempio, il risultato di una costosa query SQL) e poiché non è nella cache, tutti i processi iniziano a eseguire la stessa query SQL. Il carico si moltiplica e può persino accadere che nessun thread riesca a rispondere entro il limite di tempo, la cache non viene creata e l'applicazione collassa. Fortunatamente, la cache in Nette funziona in modo tale che, in caso di più richieste simultanee per un singolo elemento, viene generato solo dal primo thread, gli altri aspettano e successivamente utilizzano il risultato generato. +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 FileStorage: +Esempio di creazione di un `FileStorage`: ```php // lo storage sarà la directory '/path/to/temp' su disco @@ -371,7 +386,7 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -Il server [Memcached|https://memcached.org] è un sistema di memorizzazione distribuita ad alte prestazioni, il cui adattatore è `Nette\Caching\Storages\MemcachedStorage`. Nella configurazione, specifichiamo l'indirizzo IP e la porta, se diversa dalla standard 11211. +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`. @@ -385,13 +400,13 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` è uno storage che salva i dati in un array PHP, e quindi si perdono alla fine della richiesta. +`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 e l'adattatore `Nette\Caching\Storages\SQLiteStorage` offrono un modo per salvare la cache in un unico file su disco. Nella configurazione, specifichiamo il percorso di questo file. +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`. @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -Un'implementazione speciale dello storage è `Nette\Caching\Storages\DevNullStorage`, che in realtà non salva affatto i dati. È quindi adatto per i test, quando vogliamo eliminare l'influenza della cache. +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. -Utilizzo della cache nel codice -=============================== +Usare la cache nel codice +========================= -Quando si utilizza la cache nel codice, abbiamo due modi per farlo. Il primo è farsi passare lo storage tramite [dependency injection |dependency-injection:passing-dependencies] e creare l'oggetto `Cache`: +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; @@ -427,7 +442,7 @@ class ClassOne } ``` -La seconda opzione è farsi passare direttamente l'oggetto `Cache`: +La seconda possibilità è farsi passare direttamente l'oggetto `Cache`: ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -L'oggetto `Cache` viene quindi creato direttamente nella configurazione in questo modo: +L'oggetto `Cache` va poi definito nella configurazione, per esempio così: ```neon services: @@ -450,9 +465,9 @@ services: Journal ======= -Nette salva i tag e le priorità nel cosiddetto journal. Standardmente, per questo viene utilizzato SQLite e il file `journal.s3db` e **sono richieste le estensioni PHP `pdo` e `pdo_sqlite`.** +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`.** -È possibile modificare il journal nella configurazione: +L'implementazione del journal la potete cambiare nella configurazione: ```neon services: @@ -463,22 +478,25 @@ services: Servizi DI ========== -Questi servizi vengono aggiunti al container DI: +Al container DI vengono aggiunti questi servizi: -| Nome | Tipo | Descrizione +| Nome | Tipo | Descrizione |---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | storage +| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Lo storage del journal della cache +| `cache.storage` | [api:Nette\Caching\Storage] | Lo storage principale della cache -Disattivazione della cache -========================== +Disattivare la cache +==================== -Una delle opzioni per disattivare la cache nell'applicazione è impostare [#DevNullStorage] come storage: +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 in Latte o del container DI, poiché queste librerie non utilizzano i servizi di nette/caching e gestiscono la cache autonomamente. La loro cache, del resto, [non è necessario disattivare |nette:troubleshooting#Come disattivare la cache durante lo sviluppo] in modalità sviluppatore. +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 new file mode 100644 index 0000000000..80fd21c6c6 --- /dev/null +++ b/caching/it/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 9d19e7312c..4647d0c8a2 100644 --- a/caching/it/@meta.texy +++ b/caching/it/@meta.texy @@ -1,2 +1 @@ {{sitename: Documentazione Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/it/upgrading.texy b/caching/it/upgrading.texy new file mode 100644 index 0000000000..aace8026cb --- /dev/null +++ b/caching/it/upgrading.texy @@ -0,0 +1,20 @@ +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 index 50ae8d3d98..5108136eb1 100644 --- a/caching/ja/@home.texy +++ b/caching/ja/@home.texy @@ -3,36 +3,36 @@ Nette Caching <div class=perex> -キャッシュ は、一度取得に時間のかかったデータを次回の使用のために保存することで、アプリケーションを高速化します。以下を示します: +キャッシュは、かつて取り出すのに計算の手間がかかったデータを保存しておき、次からは速く取り出せるようにして、アプリケーションを速くします。ここでは次のことを扱います。 -- キャッシュの使用方法 -- ストレージの変更方法 -- キャッシュを正しく無効化する方法 +- キャッシュの使い方 +- 保管の仕組みの変え方 +- キャッシュを正しく無効にする方法 </div> -Netteでのキャッシュの使用は非常に簡単ですが、非常に高度なニーズにも対応しています。パフォーマンスと100%の耐障害性を考慮して設計されています。基本的には、最も一般的なバックエンドストレージ用のアダプタが含まれています。タグベースの無効化、時間ベースの有効期限、キャッシュスタンピードからの保護などをサポートしています。 +Nette でのキャッシュの使い方はごく分かりやすく、それでいて洗練された必要にも応えます。性能と 100% の堅牢さを目指して作られています。よく使われる保管の仕組みのアダプタが備わっています。タグによる無効化、時間による期限切れ、キャッシュスタンピードへの守りなどに対応しています。 インストール ====== -[Composer|best-practices:composer]を使用してライブラリをダウンロードし、インストールします: +パッケージは [Composer|best-practices:composer]でダウンロードしてインストールします。 ```shell composer require nette/caching ``` -基本的な使用法 -======= +基本の使い方 +====== -キャッシュ(またはメモリキャッシュ)の操作の中心は[api:Nette\Caching\Cache]オブジェクトです。そのインスタンスを作成し、コンストラクタにいわゆるストレージをパラメータとして渡します。これは、データが物理的に保存される場所(データベース、Memcached、ディスク上のファイルなど)を表すオブジェクトです。ストレージには、`Nette\Caching\Storage` 型で[dependency injection |dependency-injection:passing-dependencies]を使用して渡してもらうことでアクセスできます。必要なことはすべて[ストレージのセクション |#ストレージ]で説明します。 +キャッシュを扱う中心の要素は [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` でした。 +バージョン 3.0 では、このインターフェースにはまだ `I` の接頭辞が付いていて、名前は `Nette\Caching\IStorage` でした。さらに `Cache` クラスの定数は大文字で書かれていて、たとえば `Cache::Expire` ではなく `Cache::EXPIRE` でした。 -以下の例では、エイリアス `Cache` が作成され、変数 `$storage` にストレージが含まれていると仮定します。 +以下の例では、別名 `Cache` があり、`$storage` の変数に保管のインスタンスが入っているものとします。 ```php use Nette\Caching\Cache; @@ -40,51 +40,57 @@ use Nette\Caching\Cache; $storage = /* ... */; // Nette\Caching\Storage のインスタンス ``` -キャッシュは実際には *key-valueストア* であり、連想配列のようにキーを使用してデータを読み書きします。アプリケーションは多数の独立した部分で構成されており、すべてが1つのストレージ(ディスク上の1つのディレクトリを想像してください)を使用すると、遅かれ早かれキーの衝突が発生します。Nette Frameworkはこの問題を、スペース全体を名前空間(サブディレクトリ)に分割することで解決します。プログラムの各部分は、一意の名前を持つ独自のスペースを使用するため、衝突は発生しません。 +キャッシュは要するに *キーと値の保管場所* で、連想配列のようにキーでデータを読み書きします。アプリケーションは独立した複数の部分から成ります。すべての部分がひとつの保管場所(ディスクのひとつのディレクトリを想像してください)を使えば、遅かれ早かれキーの衝突が起きます。Nette Framework はこれを、保管の空間を名前空間(考え方としては下位のディレクトリのようなもの)に分けることで解決します。アプリケーションのそれぞれの部分は、一意の名前を持つ自分の名前空間の中で働くので、衝突は起きません。 -スペース名はCacheクラスのコンストラクタの2番目のパラメータとして指定します: +名前空間の名前は `Cache` クラスのコンストラクタの第 2 引数で指定します。 ```php $cache = new Cache($storage, 'Full Html Pages'); ``` -これで、`$cache` オブジェクトを使用してキャッシュから読み書きできます。両方の操作には `load()` メソッドを使用します。最初の引数はキーで、2番目の引数はキーがキャッシュに見つからない場合に呼び出されるPHPコールバックです。コールバックは値を生成して返し、キャッシュに保存されます: +必要なら、既存のインスタンスから `derive()` メソッドで下位の名前空間に絞った新しいキャッシュを作れます。 + +```php +$subCache = $cache->derive('Images'); +``` + +これで `$cache` のオブジェクトでキャッシュを読み書きできます。`load()` メソッドが両方の役目を果たします。第 1 引数はキー、第 2 引数はそのキーがキャッシュに見つからないときに呼ばれる PHP のコールバックです。コールバックが値を作って返し、`load()` メソッドがそれを蓄えます。 ```php $value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // 時間のかかる計算 + $computedValue = /* ... */; // 手間のかかる計算 return $computedValue; }); ``` -2番目のパラメータを指定しない場合 `$value = $cache->load($key)`、キャッシュにアイテムがない場合は `null` が返されます。 +第 2 パラメータを省くと(`$value = $cache->load($key)`)、その項目がキャッシュに見つからなければ `load()` は `null` を返します。 .[tip] -素晴らしいことに、キャッシュにはシリアライズ可能な任意の構造を保存でき、文字列だけである必要はありません。そして、これはキーにも当てはまります(ただし、通常は文字列または単純な配列が推奨されます)。 +文字列だけでなく、直列化できるどんな構造も蓄えられるのはうれしいところです。キーについても同じです。 -キャッシュからアイテムを削除するには `remove()` メソッドを使用します: +キャッシュから項目を消すには `remove()` メソッドを使います。 ```php $cache->remove($key); ``` -キャッシュにアイテムを保存するには `$cache->save($key, $value, array $dependencies = [])` メソッドも使用できます。ただし、上記で説明した `load()` を使用する方法が推奨されます。 +項目は `$cache->save($key, $data, ?array $dependencies = null)` メソッドでも保存できます。とはいえ、上で見た `load()` のやり方のほうがふつうは好まれます。 メモ化 === -メモ化とは、関数やメソッドの呼び出し結果をキャッシュして、同じことを何度も計算することなく次回使用できるようにすることです。 +メモ化とは、関数やメソッドの呼び出しの結果を蓄えて、次に同じ引数で呼ばれたときに計算し直さず蓄えた結果を返すことです。 -メソッドや関数は `call(callable $callback, ...$args)` を使用してメモ化して呼び出すことができます: +メソッドと関数は `call(callable $callback, ...$args)` でメモ化して呼べます。 ```php $result = $cache->call('gethostbyaddr', $ip); ``` -`gethostbyaddr()` 関数は、各 `$ip` パラメータに対して一度だけ呼び出され、次回はキャッシュから値が返されます。 +こうすると `gethostbyaddr()` の関数は、`$ip` の引数の値ごとに一度だけ呼ばれます。同じ `$ip` での次の呼び出しは、蓄えられた値を返します。 -後で呼び出すことができるメソッドや関数のメモ化されたラッパーを作成することも可能です: +メソッドや関数のメモ化された包みを作って、あとで呼ぶこともできます。 ```php function factorial($num) @@ -94,17 +100,17 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // 初回計算 -$result = $memoizedFactorial(5); // 2回目はキャッシュから +$result = $memoizedFactorial(5); // 一度めは計算します +$result = $memoizedFactorial(5); // 二度めはキャッシュから返します ``` -有効期限と無効化 +期限切れと無効化 ======== -キャッシュに保存する際には、以前に保存されたデータがいつ無効になるかという問題を解決する必要があります。Nette Frameworkは、データの有効期間を制限したり、制御された方法で削除(フレームワークの用語では「無効化」)したりするメカニズムを提供します。 +キャッシュを使うなら、前に保存したデータがいつ古くなるかという問題に向き合う必要があります。Nette Framework は、データの有効さを制限したり、はっきり消したりするしくみを備えています(フレームワークの言い方では「無効化」と呼びます)。 -データの有効期間は、保存時に `save()` メソッドの3番目のパラメータを使用して設定します。例: +データの有効さは保存のときに決めます。ふつうは `save()` メソッドの第 3 パラメータを使います。たとえば次のようにです。 ```php $cache->save($key, $value, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -または、`load()` メソッドのコールバックに参照渡しされる `$dependencies` パラメータを使用します。例: +あるいは、`load()` メソッドのコールバックに参照で渡される `$dependencies` のパラメータで決められます。たとえば次のようにです。 ```php $value = $cache->load($key, function (&$dependencies) { @@ -121,34 +127,34 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -または、`load()` メソッドの3番目のパラメータを使用します。例: +あるいは `load()` メソッド自身の第 3 パラメータでも決められます。たとえば次のようにです。 ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -以下の例では、2番目のバリアントを想定し、したがって変数 `$dependencies` が存在すると仮定します。 +以下の例では、コールバックの中で `$dependencies` の変数を使う 2 つめの形を前提にします。 -有効期限 +期限切れ ---- -最も単純な有効期限は時間制限です。このようにして、20分間有効なデータをキャッシュに保存します: +いちばん単純な期限切れの形は時間の制限です。次はデータを 20 分の有効期間で蓄えます。 ```php -// 秒数またはUNIXタイムスタンプも受け付けます +// 秒数や UNIX タイムスタンプも受け取ります $dependencies[Cache::Expire] = '20 minutes'; ``` -読み取りごとに有効期間を延長したい場合は、次のように実現できますが、注意してください。これによりキャッシュのオーバーヘッドが増加します: +読むたびに有効期間を延ばしたいなら(スライド式の期限切れ)、次のようにできます。ただしキャッシュの手間が増えることに注意してください。 ```php $dependencies[Cache::Sliding] = true; ``` -ファイルまたは複数のファイルのいずれかが変更されたときにデータを期限切れにするオプションは便利です。これは、たとえば、これらのファイルを処理して生成されたデータをキャッシュに保存する場合に使用できます。絶対パスを使用してください。 +特定のファイル、あるいはいくつかのファイルのどれかが変わったときにデータを期限切れにするのも便利な選択肢です。たとえばそれらのファイルを処理して得たデータを蓄えるときに役立ちます。絶対パスを使ってください。 ```php $dependencies[Cache::Files] = '/path/to/data.yaml'; @@ -156,13 +162,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; $dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; ``` -別のアイテム(または複数の他のアイテムのいずれか)が期限切れになったときに、キャッシュ内のアイテムを期限切れにすることができます。これは、たとえば、HTMLページ全体をキャッシュし、そのフラグメントを他のキーの下に保存する場合に使用できます。フラグメントが変更されると、ページ全体が無効になります。フラグメントがキー `frag1` と `frag2` の下に保存されている場合、次のように使用します: +ほかの特定の項目(あるいはいくつかのうちのどれか)が期限切れになったときに、キャッシュの項目を期限切れにできます。たとえば HTML のページ全体とその断片を違うキーで蓄えるときに役立ちます。断片が変わったら、ページ全体を無効にすべきです。断片が `frag1` と `frag2` のキーで蓄えられているなら、次のようにします。 ```php $dependencies[Cache::Items] = ['frag1', 'frag2']; ``` -有効期限は、独自の関数または静的メソッドを使用して制御することもできます。これらの関数またはメソッドは、読み取り時にアイテムがまだ有効かどうかを常に決定します。このようにして、たとえば、PHPのバージョンが変更されたときに常にアイテムを期限切れにすることができます。現在のバージョンをパラメータと比較する関数を作成し、保存時に依存関係の間に `[関数名, ...引数]` の形式の配列を追加します: +期限切れは独自の関数や静的メソッドでも決められます。それらは読むたびに呼ばれ、その項目がまだ有効かを判断します。たとえば PHP のバージョンが変わるたびに項目を期限切れにできます。今のバージョンをパラメータと比べる関数を作り、保存のときに `[関数名, ...引数]` の形の配列を依存関係に足します。 ```php function checkPhpVersion($ver): bool @@ -171,11 +177,11 @@ function checkPhpVersion($ver): bool } $dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // checkPhpVersion(...) === false の場合に期限切れにする + ['checkPhpVersion', PHP_VERSION_ID] // checkPhpVersion(...) === false のとき期限切れ ]; ``` -もちろん、すべての基準を組み合わせることができます。キャッシュは、少なくとも1つの基準が満たされない場合に期限切れになります。 +もちろん、これらの基準はすべて組み合わせられます。少なくともひとつの基準が満たされなくなると、そのキャッシュの項目は期限切れになります。 ```php $dependencies[Cache::Expire] = '20 minutes'; @@ -186,13 +192,13 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; タグによる無効化 -------- -非常に便利な無効化ツールは、いわゆるタグです。キャッシュ内の各アイテムに、任意の文字列であるタグのリストを割り当てることができます。たとえば、記事とコメントを含むHTMLページがあり、それをキャッシュするとします。保存時にタグを指定します: +タグはとても役に立つ無効化のしくみです。キャッシュに蓄えるそれぞれの項目に、タグ(好きな文字列)の一覧を割り当てられます。たとえば、記事とそのコメントを表示する HTML のページを蓄えたいとしましょう。保存のときに、それに合うタグを指定します。 ```php $dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; ``` -管理画面に移動しましょう。ここに記事を編集するためのフォームがあります。記事をデータベースに保存すると同時に、タグに従ってキャッシュからアイテムを削除する `clean()` コマンドを呼び出します: +では管理画面へ移りましょう。ここには記事を編集するフォームがあります。記事をデータベースへ保存するのと合わせて、`clean()` メソッドを呼んでタグをもとに蓄えた項目を消します。 ```php $cache->clean([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -同様に、新しいコメントを追加する場所(またはコメントを編集する場所)で、適切なタグを無効化することを忘れないでください: +同じように、新しいコメントを足す(あるいは編集する)ときも、対応するタグを無効にするのを忘れてはいけません。 ```php $cache->clean([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -これで何が達成されたのでしょうか?記事やコメントが変更されるたびにHTMLキャッシュが無効化(削除)されるようになります。ID = 10の記事が編集されると、タグ `article/10` が強制的に無効化され、指定されたタグを持つHTMLページがキャッシュから削除されます。同じことが、対応する記事の下に新しいコメントが挿入された場合にも発生します。 +これで何が実現したのでしょうか。私たちの HTML のキャッシュは、それに結び付いた記事やそのコメントが変わるたびに無効化(削除)されるようになりました。ID = 10 の記事を編集すると、タグ `article/10` が無効になり、このタグを持つ蓄えられた HTML のページが消されます。その記事の下に新しいコメントが足されたときも同じです。 .[note] -タグにはいわゆる[#Journal]が必要です。 +タグには[ジャーナル |#ジャーナル]が要ります。 優先度による無効化 --------- -キャッシュ内の個々のアイテムに優先度を設定できます。これを使用して、たとえばキャッシュが特定のサイズを超えた場合にアイテムを削除できます: +キャッシュの項目ごとに優先度を割り当てられます。おかげで、たとえばキャッシュがある大きさの制限を超えたときに、制御された形で消せます。 ```php $dependencies[Cache::Priority] = 50; ``` -優先度が100以下のすべてのアイテムを削除します: +優先度が 100 以下のすべての項目を消すには次のようにします。 ```php $cache->clean([ @@ -232,13 +238,13 @@ $cache->clean([ ``` .[note] -優先度にはいわゆる[#Journal]が必要です。 +優先度にはいわゆる[ジャーナル |#ジャーナル]が要ります。 -キャッシュの削除 --------- +キャッシュを空にする +---------- -パラメータ `Cache::All` はすべてを削除します: +`Cache::All` のパラメータはすべてを空にします。 ```php $cache->clean([ @@ -247,70 +253,79 @@ $cache->clean([ ``` -一括読み取り +まとめて読む ====== -キャッシュへの一括読み取りおよび書き込みには `bulkLoad()` メソッドを使用します。これにキーの配列を渡し、値の配列を取得します: +キャッシュをまとめて読み書きするには `bulkLoad()` メソッドを使います。キーの配列を渡すと、それに対応する値の配列が返ります。 ```php $values = $cache->bulkLoad($keys); ``` -`bulkLoad()` メソッドは `load()` と同様に、生成されるアイテムのキーを渡される2番目のパラメータコールバックでも機能します: +`bulkLoad()` メソッドは `load()` と同じように働き、第 2 パラメータのコールバックも受け取ります。このコールバックは、作られる項目のキーを受け取ります。 ```php $values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // 時間のかかる計算 + $computedValue = /* ... */; // 手間のかかる計算 return $computedValue; }); ``` +逆に複数の項目を一度に書くには `bulkSave()` メソッドを使います。これは `キー => 値` の組の配列と、省略できる依存関係を受け取ります。 + +```php +$cache->bulkSave([ + $key1 => $value1, + $key2 => $value2, +], [Cache::Expire => '20 minutes']); +``` -PSR-16との使用 .{data-version:3.3.1} -================================ -PSR-16インターフェースでNette Cacheを使用するには、`PsrCacheAdapter` アダプタを使用できます。これにより、Nette CacheとPSR-16互換キャッシュを期待する任意のコードまたはライブラリとの間でシームレスな統合が可能になります。 +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キャッシュとして使用できます: +これで `$psrCache` を標準の PSR-16 のキャッシュとして使えます。 ```php -$psrCache->set('key', 'value', 3600); // 値を1時間保存します +$psrCache->set('key', 'value', 3600); // 値を 1 時間蓄えます $value = $psrCache->get('key', 'default'); ``` -アダプタは、`getMultiple()`、`setMultiple()`、`deleteMultiple()` を含む、PSR-16で定義されているすべてのメソッドをサポートしています。 +このアダプタは、`getMultiple()`、`setMultiple()`、`deleteMultiple()` も含めて PSR-16 が定めるすべてのメソッドに対応しています。 -出力のキャッシュ -======== +出力の蓄え +===== -出力をキャプチャしてキャッシュすることは非常にエレガントに行えます: +出力はとても優雅に捕まえて蓄えられます。 ```php if ($capture = $cache->capture($key)) { - echo ... // データを出力します + // echo ... なんらかのデータを出力します - $capture->end(); // 出力をキャッシュに保存します + $capture->end(); // 出力をキャッシュへ保存します } ``` -出力がすでにキャッシュに保存されている場合、`capture()` メソッドはそれを表示して `null` を返し、したがって条件は実行されません。それ以外の場合、出力のキャプチャを開始し、最終的に表示されたデータをキャッシュに保存するために使用される `$capture` オブジェクトを返します。 +出力がすでにキャッシュにあれば、`capture()` メソッドはそれを出力して `null` を返すので、`if` の中身は飛ばされます。そうでなければ出力の蓄えを始め、`$capture` のオブジェクトを返します。捕まえたデータは、その `end()` メソッドで最後にキャッシュへ保存します。 .[note] -バージョン3.0では、メソッド名は `$cache->start()` でした。 +バージョン 3.0 では、このメソッドの名前は `$cache->start()` でした。 -Latteでのキャッシュ -============ +Latte でのキャッシュ +============= -[Latte|latte:]テンプレートでのキャッシュは非常に簡単です。テンプレートの一部を `{cache}...{/cache}` タグで囲むだけです。ソーステンプレート(キャッシュブロック内のインクルードされたテンプレートを含む)が変更されると、キャッシュは自動的に無効化されます。`{cache}` タグはネストでき、ネストされたブロックが無効化されると(たとえばタグによって)、親ブロックも無効化されます。 +[Latte|latte:]のテンプレートでのキャッシュはとても簡単です。蓄えたいテンプレートの部分を `{cache}...{/cache}` のタグで包むだけです。もとのテンプレートのファイルが変わると(蓄えられたかたまりの中で取り込まれたテンプレートも含みます)、キャッシュは自動的に無効になります。`{cache}` のタグは入れ子にできます。入れ子のかたまりが(たとえばタグで)無効になると、その親のかたまりも無効になります。 -タグ内では、キャッシュがバインドされるキー(ここでは変数 `$id`)を指定し、有効期限と[無効化タグ |#タグによる無効化]を設定できます。 +タグの中では、そのキャッシュの項目を結び付けるキー(ここでは変数 `$id`)を指定したり、有効期限を決めたり、[無効化のタグ |#タグによる無効化]を定めたりできます。 ```latte {cache $id, expire: '20 minutes', tags: [tag1, tag2]} @@ -318,9 +333,9 @@ Latteでのキャッシュ {/cache} ``` -すべてのパラメータはオプションなので、有効期限もタグも、最終的にはキーも指定する必要はありません。 +これらのパラメータはすべて省略できるので、有効期限もタグもキーも指定しなくてかまいません。 -キャッシュの使用は `if` を使用して条件付きにすることもできます - コンテンツは条件が満たされた場合にのみキャッシュされます: +キャッシュを使うかどうかを `if` で条件にもできます。条件が満たされたときにだけ中身が蓄えられます。 ```latte {cache $id, if: !$form->isSubmitted()} @@ -329,23 +344,23 @@ Latteでのキャッシュ ``` -ストレージ -===== +保管の仕組み +====== -ストレージは、データが物理的に保存される場所を表すオブジェクトです。データベース、Memcachedサーバー、または最も利用しやすいストレージであるディスク上のファイルを使用できます。 +保管の仕組みは、データが実際に置かれる場所を表すオブジェクトです。データベース、Memcached のサーバー、あるいはもっとも手近な保管場所であるディスクのファイルを使えます。 -|----------------- -| ストレージ | 説明 -|----------------- -| [#FileStorage] | ディスク上のファイルに保存するデフォルトのストレージ -| [#MemcachedStorage] | `Memcached` サーバーを使用します -| [#MemoryStorage] | データは一時的にメモリに保存されます -| [#SQLiteStorage] | データはSQLiteデータベースに保存されます -| [#DevNullStorage] | データは保存されません、テストに適しています +|---------------------- +| 保管の仕組み | 説明 +|---------------------- +| [#FileStorage] | 既定の保管の仕組み。キャッシュをディスクのファイルに保存します。 +| [#MemcachedStorage] | 保管に `Memcached` のサーバーを使います。 +| [#MemoryStorage] | データを一時的にメモリに置きます(リクエストの終わりに失われます)。 +| [#SQLiteStorage] | データを SQLite のデータベースのファイルに置きます。 +| [#DevNullStorage] | データは実際には保存されません。テストに役立ちます。 -ストレージオブジェクトには、`Nette\Caching\Storage` 型で[dependency injection |dependency-injection:passing-dependencies]を使用して渡してもらうことでアクセスできます。デフォルトのストレージとして、Netteは[一時ファイル |application:bootstrapping#一時ファイル]用のディレクトリの `cache` サブディレクトリにデータを保存するFileStorageオブジェクトを提供します。 +保管のオブジェクトは [dependency injection |dependency-injection:passing-dependencies]で `Nette\Caching\Storage` の型を求めて手に入れます。既定では、Nette は[一時ファイル |application:bootstrapping#一時ファイル]のディレクトリの中の `cache` の下位のディレクトリにデータを置く `FileStorage` オブジェクトを与えます。 -ストレージは設定で変更できます: +既定の保管の仕組みは設定で変えられます。 ```neon services: @@ -356,14 +371,14 @@ services: FileStorage ----------- -キャッシュをディスク上のファイルに書き込みます。`Nette\Caching\Storages\FileStorage` ストレージはパフォーマンスに非常に最適化されており、特に操作の完全な原子性を保証します。これはどういう意味でしょうか?キャッシュを使用する場合、別のスレッドによってまだ完全に書き込まれていないファイルを読み取ったり、誰かが「手元で」削除したりすることはできません。したがって、キャッシュの使用は完全に安全です。 +キャッシュの項目をディスクのファイルに書きます。`Nette\Caching\Storages\FileStorage` の保管の仕組みは性能のために念入りに最適化されていて、そして何より操作の完全なアトミック性を保証します。それはどういうことでしょうか。このキャッシュを使っているとき、ほかのスレッドがまだ完全に書き終えていないファイルを読んでしまうことも、読んでいる最中に誰かがそれを消してしまうことも起こりえません。ですからこの保管の仕組みを使うのは完全に安全です。 -このストレージには、キャッシュが削除されたり、まだウォームアップされていない(つまり作成されていない)ときにCPU使用率が極端に増加するのを防ぐ重要な機能も組み込まれています。これは「キャッシュスタンピード」:https://en.wikipedia.org/wiki/Cache_stampede に対する予防策です。 同時に多数の同時リクエストが発生し、それらがすべてキャッシュから同じもの(たとえば、高価なSQLクエリの結果)を要求し、キャッシュにないため、すべてのプロセスが同じSQLクエリを実行し始めることがあります。 これにより負荷が倍増し、どのスレッドも時間制限内に応答できず、キャッシュが作成されず、アプリケーションがクラッシュすることさえあります。 幸いなことに、Netteのキャッシュは、1つのアイテムに対して複数の同時リクエストがある場合、最初のスレッドのみがそれを生成し、他のスレッドは待機し、その後生成された結果を使用するように機能します。 +この保管の仕組みには、キャッシュが消されたときや、まだ「冷たい」(つまりまだ作られていない)ときに CPU の使用が跳ね上がるのを防ぐ、大事な組み込みの機能もあります。これは "キャッシュスタンピード":https://en.wikipedia.org/wiki/Cache_stampede への守りとして知られています。これは、同時に走る複数のリクエストが同じキャッシュの項目(たとえば手間のかかる SQL のクエリの結果)を同時に求めたときに起こります。その項目がそのときキャッシュになければ、これらのプロセスがすべて同じ手間のかかる処理(SQL のクエリなど)を始めてしまうかもしれません。これはサーバーの負荷を何倍にもし、どのスレッドも制限時間の中で応えられず、キャッシュも作られず、アプリケーションが落ちることさえあります。幸い Nette のキャッシュはこれに対処します。同じ項目への同時のリクエストが複数あるとき、それを作るのは最初のスレッドだけです。ほかのスレッドは待って、そのあと最初のスレッドが作った結果を使います。 -FileStorageの作成例: +`FileStorage` を作る例です。 ```php -// ストレージはディスク上の '/path/to/temp' ディレクトリになります +// 保管場所はディスクの '/path/to/temp' のディレクトリになります $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); ``` @@ -371,10 +386,10 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -[Memcached|https://memcached.org]サーバーは、高性能な分散メモリキャッシュシステムであり、そのアダプタは `Nette\Caching\Storages\MemcachedStorage` です。設定では、標準の11211と異なる場合はIPアドレスとポートを指定します。 +[Memcached |https://memcached.org]のサーバーは、高い性能を持つ分散型のメモリのオブジェクトのキャッシュのしくみです。Nette でのそのアダプタは `Nette\Caching\Storages\MemcachedStorage` です。設定では、サーバーの IP アドレスと、標準の 11211 と違うならポートを指定します。 .[caution] -PHP拡張機能 `memcached` が必要です。 +PHP の `memcached` 拡張が要ります。 ```neon services: @@ -385,16 +400,16 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` は、データをPHP配列に保存するストレージであり、したがってリクエストの終了とともに失われます。 +`Nette\Caching\Storages\MemoryStorage` は、データを PHP の配列の中に持つ保管の仕組みです。ですからリクエストが終わるとデータは失われます。 SQLiteStorage ------------- -SQLiteデータベースと `Nette\Caching\Storages\SQLiteStorage` アダプタは、キャッシュをディスク上の単一ファイルに保存する方法を提供します。設定では、このファイルへのパスを指定します。 +SQLite のデータベースと `Nette\Caching\Storages\SQLiteStorage` のアダプタは、ディスクのひとつのファイルの中にデータを蓄える方法を与えます。設定ではそのデータベースのファイルへのパスを指定します。 .[caution] -PHP拡張機能 `pdo` および `pdo_sqlite` が必要です。 +PHP の `pdo` と `pdo_sqlite` の拡張が要ります。 ```neon services: @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -ストレージの特別な実装は `Nette\Caching\Storages\DevNullStorage` であり、実際にはデータをまったく保存しません。したがって、キャッシュの影響を排除したいテストに適しています。 +特別な保管の実装が `Nette\Caching\Storages\DevNullStorage` で、これは実際にはデータを何も保存しません。ですからキャッシュの影響をなくしたいテストの用途に向いています。 -コードでのキャッシュの使用 -============= +コードでキャッシュを使う +============ -コードでキャッシュを使用する場合、2つの方法があります。1つ目は、[dependency injection |dependency-injection:passing-dependencies]を使用してストレージを渡し、`Cache` オブジェクトを作成する方法です: +コードでキャッシュを使うやり方は主に 2 つあります。ひとつめは、[dependency injection |dependency-injection:passing-dependencies]で保管のオブジェクトを手に入れて、`Cache` のオブジェクトを自分で作ることです。 ```php use Nette; @@ -427,7 +442,7 @@ class ClassOne } ``` -2番目のオプションは、`Cache` オブジェクトを直接渡してもらうことです: +もうひとつは、`Cache` のオブジェクトを直接求めることです。 ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -`Cache` オブジェクトは、次のように設定で直接作成されます: +その場合、`Cache` のオブジェクトは設定で定めなければなりません。たとえば次のようにです。 ```neon services: @@ -447,12 +462,12 @@ services: ``` -Journal -======= +ジャーナル +===== -Netteはタグと優先度をいわゆるジャーナルに保存します。標準では、SQLiteとファイル `journal.s3db` が使用され、**PHP拡張機能 `pdo` と `pdo_sqlite` が必要です。** +Nette はタグと優先度の情報を、いわゆるジャーナルに保存します。既定では `journal.s3db` のファイルを通して SQLite が使われ、**PHP の `pdo` と `pdo_sqlite` の拡張が要ります**。 -ジャーナルは設定 (`config/services.neon`) で変更できます: +ジャーナルの実装は設定で変えられます。 ```neon services: @@ -460,25 +475,28 @@ services: ``` -DIサービス -====== +DI のサービス +======== -これらのサービスはDIコンテナに追加されます: +DI コンテナには次のサービスが足されます。 -| 名前 | 型 | 説明 +| 名前 | 型 | 説明 |---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | キャッシュのジャーナル (タグや優先度管理用) -| `cache.storage` | [api:Nette\Caching\Storage] | デフォルトのキャッシュストレージ +| `cache.journal` | [api:Nette\Caching\Storages\Journal] | キャッシュのジャーナルの保管 +| `cache.storage` | [api:Nette\Caching\Storage] | 主なキャッシュの保管 -キャッシュの無効化 -========= +キャッシュを切る +======== -アプリケーションでキャッシュを無効にする1つの方法は、ストレージとして[#DevNullStorage]を設定することです: +アプリケーションでキャッシュを切るひとつの方法は、保管の仕組みを [#DevNullStorage]にすることです。 ```neon services: cache.storage: Nette\Caching\Storages\DevNullStorage ``` -この設定は、LatteのテンプレートやDIコンテナのキャッシュには影響しません。これらのライブラリはnette/cachingサービスを使用せず、独自のキャッシュを管理するためです。また、開発モードでは[これらのキャッシュを無効にする必要はありません |nette:troubleshooting#開発中にキャッシュを無効にする方法は]。 +この設定は、Latte のテンプレートや DI コンテナのキャッシュには影響しません。これらのライブラリは `nette/caching` のサービスを使わず、自分でキャッシュを管理するからです。しかも開発モードでは、それらのキャッシュを[ふつう切る必要はありません |nette:troubleshooting#開発中にキャッシュを切るには]。 + + +新しい版へ上げるなら、[アップグレード |upgrading]のページをご覧ください。 diff --git a/caching/ja/@left-menu.texy b/caching/ja/@left-menu.texy new file mode 100644 index 0000000000..f536857307 --- /dev/null +++ b/caching/ja/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 7d67dcb7b8..43b85f3cac 100644 --- a/caching/ja/@meta.texy +++ b/caching/ja/@meta.texy @@ -1,2 +1 @@ -{{sitename: Nette ドキュメンテーション}} -{{leftbar: nette:@menu-topics}} +{{sitename: Nette ドキュメント}} diff --git a/caching/ja/upgrading.texy b/caching/ja/upgrading.texy new file mode 100644 index 0000000000..d3d13ba310 --- /dev/null +++ b/caching/ja/upgrading.texy @@ -0,0 +1,20 @@ +アップグレード +******* + + +バージョン 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 index 8f948e0cbd..00b14c1a59 100644 --- a/caching/meta.json +++ b/caching/meta.json @@ -1,5 +1,6 @@ { "version": "3.x", "repo": "nette/caching", - "composer": "nette/caching" + "composer": "nette/caching", + "api": "https://api.nette.org/caching/" } diff --git a/caching/pl/@home.texy b/caching/pl/@home.texy index 61fdbc3275..df503eda45 100644 --- a/caching/pl/@home.texy +++ b/caching/pl/@home.texy @@ -3,21 +3,21 @@ Nette Caching <div class=perex> -Cache przyspieszy twoją aplikację, przechowując raz uzyskane dane do przyszłego użytku. Pokażemy: +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 -- jak poprawnie unieważniać cache +- jak zmienić magazyn zaplecza +- jak poprawnie inwalidować cache </div> -Korzystanie z cache w Nette jest bardzo łatwe, a jednocześnie obejmuje bardzo zaawansowane potrzeby. Został zaprojektowany z myślą o wydajności i 100% odporności. W standardzie znajdziesz adaptery do najczęstszych magazynów backendowych. Umożliwia unieważnianie oparte na tagach, wygaśnięcie czasowe, posiada ochronę przed zjawiskiem *cache stampede* itp. +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 ========== -Bibliotekę pobierzesz i zainstalujesz za pomocą narzędzia [Composer|best-practices:composer]: +Pobierz i zainstaluj pakiet za pomocą [Composera|best-practices:composer]: ```shell composer require nette/caching @@ -27,12 +27,12 @@ composer require nette/caching Podstawowe użycie ================= -Centrum pracy z cache, czyli pamięcią podręczną, stanowi obiekt [api:Nette\Caching\Cache]. Tworzymy jego instancję i jako parametr przekazujemy konstruktorowi tzw. magazyn (storage). Jest to obiekt reprezentujący miejsce, gdzie dane będą fizycznie przechowywane (baza danych, Memcached, pliki na dysku, ...). Do magazynu dostaniemy się, prosząc o jego przekazanie za pomocą [dependency injection |dependency-injection:passing-dependencies] z typem `Nette\Caching\Storage`. Wszystko, co istotne, dowiesz się w [sekcji Magazyny |#Magazyny]. +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 prefiks `I`, więc nazwa brzmiała `Nette\Caching\IStorage`. Ponadto stałe klasy `Cache` były pisane wielkimi literami, więc na przykład `Cache::EXPIRE` zamiast `Cache::Expire`. +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`. -W poniższych przykładach zakładamy, że mamy utworzony alias `Cache` i w zmiennej `$storage` magazyn. +Dla poniższych przykładów załóżmy, że mamy alias `Cache` i instancję magazynu w zmiennej `$storage`. ```php use Nette\Caching\Cache; @@ -40,51 +40,57 @@ use Nette\Caching\Cache; $storage = /* ... */; // instancja Nette\Caching\Storage ``` -Cache jest właściwie *key–value store*, czyli dane odczytujemy i zapisujemy pod kluczami, tak jak w tablicach asocjacyjnych. Aplikacje składają się z wielu niezależnych części i jeśli wszystkie będą używać jednego magazynu (wyobraź sobie jeden katalog na dysku), wcześniej czy później doszłoby do kolizji kluczy. Nette Framework rozwiązuje ten problem, dzieląc całą przestrzeń na przestrzenie nazw (podkatalogi). Każda część programu używa wtedy swojej przestrzeni z unikalną nazwą i żadna kolizja już nie może wystąpić. +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 podajemy jako drugi parametr konstruktora klasy Cache: +Nazwę przestrzeni nazw podaj jako drugi argument konstruktora klasy `Cache`: ```php $cache = new Cache($storage, 'Full Html Pages'); ``` -Teraz możemy za pomocą obiektu `$cache` odczytywać i zapisywać do pamięci podręcznej. Do obu służy metoda `load()`. Pierwszym argumentem jest klucz, a drugim PHP callback, który jest wywoływany, gdy klucz nie zostanie znaleziony w cache. Callback generuje wartość, zwraca ją, a ta jest zapisywana w cache: +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 obliczenia + $computedValue = /* ... */; // kosztowne obliczenie return $computedValue; }); ``` -Jeśli drugi parametr nie zostanie podany `$value = $cache->load($key)`, zwrócony zostanie `null`, jeśli elementu nie ma w cache. +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 w cache można przechowywać dowolne serializowalne struktury, nie muszą to być tylko ciągi znaków. To samo dotyczy nawet kluczy. +Świetne jest to, że buforować można dowolne struktury dające się serializować, nie tylko ciągi. To samo dotyczy kluczy. -Element z pamięci podręcznej usuwamy metodą `remove()`: +Do usunięcia pozycji z cache służy metoda `remove()`: ```php $cache->remove($key); ``` -Zapisać element do pamięci podręcznej można również metodą `$cache->save($key, $value, array $dependencies = [])`. Preferowany jest jednak powyższy sposób za pomocą `load()`. +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 oznacza buforowanie wyniku wywołania funkcji lub metody, aby móc go użyć następnym razem bez ponownego obliczania tej samej rzeczy. +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. -Memoizowanie można wywoływać dla metod i funkcji za pomocą `call(callable $callback, ...$args)`: +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()` zostanie wywołana dla każdego parametru `$ip` tylko raz, a następnym razem zostanie zwrócona wartość z cache. +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żliwe jest również utworzenie memoizowanego opakowania nad metodą lub funkcją, które można wywołać później: +Można też utworzyć memoizowany wrapper wokół metody albo funkcji, który można potem wywoływać: ```php function factorial($num) @@ -94,17 +100,17 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // oblicza po raz pierwszy -$result = $memoizedFactorial(5); // po raz drugi z cache +$result = $memoizedFactorial(5); // za pierwszym razem liczy +$result = $memoizedFactorial(5); // za drugim zwraca z cache ``` -Wygaśnięcie i unieważnienie -=========================== +Wygasanie i inwalidacja +======================= -Przy zapisywaniu do cache trzeba rozwiązać kwestię, kiedy wcześniej zapisane dane stają się nieaktualne. Nette Framework oferuje mechanizm, jak ograniczyć ważność danych lub je kontrolowanie usuwać (w terminologii frameworka „unieważniać“). +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 zapisywania za pomocą trzeciego parametru metody `save()`, np.: +Ważność danych ustawia się w momencie zapisu, zwykle trzecim parametrem metody `save()`, np.: ```php $cache->save($key, $value, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -Lub za pomocą parametru `$dependencies` przekazywanego przez referencję do callbacku metody `load()`, np.: +Alternatywnie można ustawić ją parametrem `$dependencies` przekazywanym przez referencję do callbacku w metodzie `load()`, np.: ```php $value = $cache->load($key, function (&$dependencies) { @@ -121,48 +127,48 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -Lub za pomocą trzeciego parametru w metodzie `load()`, np.: +Albo za pomocą 3. parametru samej metody `load()`, np.: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -W kolejnych przykładach będziemy zakładać drugą opcję, a więc istnienie zmiennej `$dependencies`. +W poniższych przykładach będziemy zakładać drugi wariant, wykorzystujący zmienną `$dependencies` wewnątrz callbacku. -Wygaśnięcie ------------ +Wygasanie +--------- -Najprostszym wygaśnięciem jest limit czasowy. W ten sposób zapisujemy dane do cache z ważnością 20 minut: +Najprostszą formą wygasania jest limit czasowy. To buforuje dane z ważnością 20 minut: ```php -// akceptuje również liczbę sekund lub timestamp UNIX +// przyjmuje też liczbę sekund albo uniksowy timestamp $dependencies[Cache::Expire] = '20 minutes'; ``` -Jeśli chcielibyśmy przedłużyć okres ważności przy każdym odczycie, można to osiągnąć w następujący sposób, ale uwaga, narzut cache przez to wzrośnie: +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; ``` -Przydatna jest możliwość wygaśnięcia danych w momencie zmiany pliku lub jednego z wielu plików. Można to wykorzystać na przykład przy zapisywaniu do cache danych powstałych w wyniku przetwarzania tych plików. Używaj ścieżek absolutnych. +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'; -// lub +// albo $dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; ``` -Możemy pozwolić, aby element w cache wygasł w momencie, gdy wygaśnie inny element (lub jeden z wielu innych). Można to wykorzystać, gdy przechowujemy w cache na przykład całą stronę HTML, a pod innymi kluczami jej fragmenty. Gdy fragment się zmieni, unieważniona zostanie cała strona. Jeśli fragmenty mamy zapisane pod kluczami np. `frag1` i `frag2`, użyjemy: +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']; ``` -Wygaśnięcie można również kontrolować za pomocą własnych funkcji lub metod statycznych, które zawsze przy odczycie decydują, czy element jest jeszcze ważny. W ten sposób możemy na przykład pozwolić, aby element wygasł zawsze, gdy zmieni się wersja PHP. Tworzymy funkcję, która porównuje aktualną wersję z parametrem, a przy zapisywaniu dodajemy do zależności tablicę w formacie `[nazwa funkcji, ...argumenty]`: +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 @@ -171,11 +177,11 @@ function checkPhpVersion($ver): bool } $dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // wygaśnij, gdy checkPhpVersion(...) === false + ['checkPhpVersion', PHP_VERSION_ID] // wygaśnie, gdy checkPhpVersion(...) === false ]; ``` -Wszystkie kryteria można oczywiście łączyć. Cache wygaśnie wtedy, gdy przynajmniej jedno kryterium nie jest spełnione. +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'; @@ -183,16 +189,16 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; ``` -Unieważnianie za pomocą tagów ------------------------------ +Inwalidacja tagami +------------------ -Bardzo użytecznym narzędziem do unieważniania są tzw. tagi. Każdemy elementowi w cache możemy przypisać listę tagów, które są dowolnymi ciągami znaków. Załóżmy, że mamy stronę HTML z artykułem i komentarzami, którą będziemy buforować. Przy zapisywaniu określamy tagi: +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 do administracji. Tutaj znajdziemy formularz do edycji artykułu. Wraz z zapisaniem artykułu do bazy danych wywołamy polecenie `clean()`, które usunie z cache elementy według tagu: +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([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -Podobnie, w miejscu dodawania nowego komentarza (lub edycji komentarza) nie zapomnijmy unieważnić odpowiedniego tagu: +Podobnie przy dodawaniu nowego komentarza (albo edycji istniejącego) musimy pamiętać o zinwalidowaniu odpowiedniego tagu: ```php $cache->clean([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -Co przez to osiągnęliśmy? Że nasza pamięć podręczna HTML będzie unieważniana (usuwana), gdy tylko zmieni się artykuł lub komentarze. Kiedy edytowany jest artykuł o ID = 10, następuje wymuszone unieważnienie tagu `article/10`, a strona HTML, która nosi ten tag, zostanie usunięta z cache. To samo nastąpi przy wstawieniu nowego komentarza pod odpowiedni artykuł. +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ą tzw. [#Journal]. +Tagi wymagają [dziennika |#Dziennik]. -Unieważnianie za pomocą priorytetu ----------------------------------- +Inwalidacja po priorytecie +-------------------------- -Poszczególnym elementom w cache możemy ustawić priorytet, za pomocą którego będzie można je usuwać, gdy na przykład cache przekroczy określoną wielkość: +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; ``` -Usuniemy wszystkie elementy o priorytecie równym lub mniejszym niż 100: +Żeby usunąć wszystkie pozycje o priorytecie równym 100 albo mniejszym: ```php $cache->clean([ @@ -232,13 +238,13 @@ $cache->clean([ ``` .[note] -Priorytety wymagają tzw. [#Journal]. +Priorytety wymagają tak zwanego [dziennika |#Dziennik]. Czyszczenie cache ----------------- -Parametr `Cache::All` usuwa wszystko: +Parametr `Cache::All` czyści wszystko: ```php $cache->clean([ @@ -250,67 +256,76 @@ $cache->clean([ Odczyt masowy ============= -Do masowego odczytu i zapisu do cache służy metoda `bulkLoad()`, której przekazujemy tablicę kluczy i otrzymujemy tablicę wartości: +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 jak `load()` również z drugim parametrem callbackiem, któremu przekazywany jest klucz generowanego elementu: +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 obliczenia + $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} ===================================== -Aby użyć Nette Cache z interfejsem PSR-16, możesz wykorzystać adapter `PsrCacheAdapter`. Umożliwia on bezproblemową integrację między Nette Cache a dowolnym kodem lub biblioteką, która oczekuje cache zgodnego z PSR-16. +Ż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 cache PSR-16: +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 obsługuje wszystkie metody zdefiniowane w PSR-16, w tym `getMultiple()`, `setMultiple()` i `deleteMultiple()`. +Adapter wspiera wszystkie metody zdefiniowane w PSR-16, wraz z `getMultiple()`, `setMultiple()` i `deleteMultiple()`. Buforowanie wyjścia =================== -Bardzo elegancko można przechwytywać i buforować wyjście: +Wyjście można bardzo elegancko przechwycić i zbuforować: ```php if ($capture = $cache->capture($key)) { - echo ... // wypisujemy dane + // echo ... wypisanie jakichś danych - $capture->end(); // zapisujemy wyjście do cache + $capture->end(); // zapisuje wyjście do cache } ``` -W przypadku, gdy wyjście jest już zapisane w cache, metoda `capture()` je wypisuje i zwraca `null`, więc warunek `if` się nie wykona. W przeciwnym razie zaczyna przechwytywać wyjście i zwraca obiekt `$capture`, za pomocą którego ostatecznie zapisujemy wypisane dane 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 nazywała się `$cache->start()`. +W wersji 3.0 metoda ta nazywała się `$cache->start()`. Buforowanie w Latte =================== -Buforowanie w szablonach [Latte|latte:] jest bardzo łatwe, wystarczy część szablonu otoczyć znacznikami `{cache}...{/cache}`. Cache jest automatycznie unieważniany w momencie zmiany szablonu źródłowego (w tym ewentualnych dołączonych szablonów wewnątrz bloku cache). Znaczniki `{cache}` można zagnieżdżać w sobie, a gdy zagnieżdżony blok zostanie unieważniony (na przykład przez tag), unieważniony zostanie również blok nadrzędny. +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. -W znaczniku można podać klucze, do których będzie powiązany cache (tutaj zmienna `$id`) i ustawić wygaśnięcie oraz [tagi do unieważnienia |#Unieważnianie za pomocą tagów]. +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]} @@ -318,9 +333,9 @@ W znaczniku można podać klucze, do których będzie powiązany cache (tutaj zm {/cache} ``` -Wszystkie parametry są opcjonalne, więc nie musimy podawać ani wygaśnięcia, ani tagów, ani nawet kluczy. +Wszystkie te parametry są opcjonalne, więc nie musisz podawać ani wygaśnięcia, ani tagów, ani nawet kluczy. -Użycie cache można również uzależnić za pomocą `if` - zawartość będzie wtedy buforowana tylko wtedy, gdy warunek zostanie spełniony: +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()} @@ -332,20 +347,20 @@ Użycie cache można również uzależnić za pomocą `if` - zawartość będzie Magazyny ======== -Magazyn to obiekt reprezentujący miejsce, gdzie dane są fizycznie przechowywane. Możemy użyć bazy danych, serwera Memcached lub najłatwiej dostępnego magazynu, jakim są pliki na dysku. +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] | domyślny magazyn z zapisem do plików na dysku -| [#MemcachedStorage] | wykorzystuje serwer `Memcached` -| [#MemoryStorage] | dane są tymczasowo w pamięci -| [#SQLiteStorage] | dane są zapisywane do bazy danych SQLite -| [#DevNullStorage] | dane nie są zapisywane, odpowiednie do testowania +|---------------------- +| [#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. -Do obiektu magazynu dostaniesz się, prosząc o jego przekazanie za pomocą [dependency injection |dependency-injection:passing-dependencies] z typem `Nette\Caching\Storage`. Jako domyślny magazyn Nette dostarcza obiekt `FileStorage` zapisujący dane do podkatalogu `cache` w katalogu dla [plików tymczasowych |application:bootstrapping#Pliki tymczasowe]. +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]. -Zmienić magazyn można w konfiguracji: +Domyślny magazyn możesz zmienić w konfiguracji: ```neon services: @@ -356,11 +371,11 @@ services: FileStorage ----------- -Zapisuje cache do plików na dysku. Magazyn `Nette\Caching\Storages\FileStorage` jest bardzo dobrze zoptymalizowany pod kątem wydajności i przede wszystkim zapewnia pełną atomowość operacji. Co to oznacza? Że podczas używania cache nie może się zdarzyć, że odczytamy plik, który jeszcze nie został w całości zapisany przez inny wątek, lub że ktoś nam go "pod ręką" usunie. Użycie cache jest więc całkowicie bezpieczne. +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. -Ten magazyn ma również wbudowaną ważną funkcję, która zapobiega ekstremalnemu wzrostowi zużycia procesora w momencie, gdy cache zostanie usunięty lub jeszcze nie jest rozgrzany (tj. utworzony). Jest to prewencja przed zjawiskiem "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Zdarza się, że w jednym momencie pojawi się większa liczba równoczesnych żądań, które chcą z cache tej samej rzeczy (np. wyniku kosztownego zapytania SQL), a ponieważ w pamięci podręcznej jej nie ma, wszystkie procesy zaczynają wykonywać to samo zapytanie SQL. Obciążenie się w ten sposób mnoży i może nawet dojść do sytuacji, że żaden wątek nie zdąży odpowiedzieć w limicie czasowym, cache się nie utworzy, a aplikacja ulegnie awarii. Na szczęście cache w Nette działa tak, że przy wielu równoczesnych żądaniach o jeden element generuje go tylko pierwszy wątek, pozostałe czekają, a następnie wykorzystują wygenerowany wynik. +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: +Przykład utworzenia `FileStorage`: ```php // magazynem będzie katalog '/path/to/temp' na dysku @@ -371,7 +386,7 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -Serwer [Memcached|https://memcached.org] to wysokowydajny system przechowywania w rozproszonej pamięci, którego adapterem jest `Nette\Caching\Storages\MemcachedStorage`. W konfiguracji podajemy adres IP i port, jeśli różni się od standardowego 11211. +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`. @@ -385,13 +400,13 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` to magazyn, który przechowuje dane w tablicy PHP, a więc znikają one wraz z zakończeniem żądania. Jest przydatny głównie do testów lub w specyficznych przypadkach, gdy cache ma żyć tylko przez czas trwania jednego żądania. +`Nette\Caching\Storages\MemoryStorage` to magazyn trzymający dane w tablicy PHP. Dane giną więc, gdy żądanie się kończy. SQLiteStorage ------------- -Baza danych SQLite i adapter `Nette\Caching\Storages\SQLiteStorage` oferują sposób na przechowywanie cache w jednym pliku na disku. W konfiguracji podajemy ścieżkę do tego pliku. +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`. @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -Specjalną implementacją magazynu jest `Nette\Caching\Storages\DevNullStorage`, który w rzeczywistości w ogóle nie przechowuje danych. Jest więc odpowiedni do testowania, gdy chcemy wyeliminować wpływ cache. +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życie cache w kodzie -===================== +Używanie cache w kodzie +======================= -Przy używaniu cache w kodzie mamy dwa sposoby. Pierwszy z nich polega na tym, że prosimy o przekazanie magazynu za pomocą [dependency injection |dependency-injection:passing-dependencies] i tworzymy obiekt `Cache`: +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; @@ -427,7 +442,7 @@ class ClassOne } ``` -Druga możliwość polega na tym, że prosimy o przekazanie od razu obiektu `Cache`: +Druga możliwość to poproszenie bezpośrednio o obiekt `Cache`: ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -Obiekt `Cache` jest następnie tworzony bezpośrednio w konfiguracji w ten sposób: +Obiekt `Cache` trzeba wtedy zdefiniować w konfiguracji, na przykład tak: ```neon services: @@ -447,12 +462,12 @@ services: ``` -Journal -======= +Dziennik +======== -Nette przechowuje tagi i priorytety w tzw. journalu. Standardowo używa się do tego SQLite i pliku `journal.s3db` i **wymagane są rozszerzenia PHP `pdo` i `pdo_sqlite`.** +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`.** -Zmienić journal można w konfiguracji: +Implementację dziennika możesz zmienić w konfiguracji: ```neon services: @@ -463,22 +478,25 @@ services: Usługi DI ========= -Te usługi są dodawane do kontenera DI: +Do kontenera DI dodawane są te usługi: -| Nazwa | Typ | Opis +| Nazwa | Typ | Opis |---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | magazyn +| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Magazyn dziennika cache +| `cache.storage` | [api:Nette\Caching\Storage] | Główny magazyn cache Wyłączenie cache ================ -Jedną z możliwości wyłączenia cache w aplikacji jest ustawienie jako magazynu [#DevNullStorage]: +Jednym ze sposobów wyłączenia buforowania w Twojej aplikacji jest ustawienie magazynu zaplecza na [#DevNullStorage]: ```neon services: cache.storage: Nette\Caching\Storages\DevNullStorage ``` -To ustawienie nie ma wpływu na buforowanie szablonów w Latte ani kontenera DI, ponieważ te biblioteki nie korzystają z usług `nette/caching` i zarządzają swoją pamięcią podręczną samodzielnie. Ich cache zresztą [nie trzeba wyłączać |nette:troubleshooting#Jak wyłączyć cache podczas developmentu] w trybie deweloperskim. +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 new file mode 100644 index 0000000000..4aafd59703 --- /dev/null +++ b/caching/pl/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 08f2227fb5..61ac92d1af 100644 --- a/caching/pl/@meta.texy +++ b/caching/pl/@meta.texy @@ -1,2 +1 @@ {{sitename: Dokumentacja Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/pl/upgrading.texy b/caching/pl/upgrading.texy new file mode 100644 index 0000000000..1c10c21580 --- /dev/null +++ b/caching/pl/upgrading.texy @@ -0,0 +1,20 @@ +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/pt/@home.texy b/caching/pt/@home.texy deleted file mode 100644 index 82c8cbb29f..0000000000 --- a/caching/pt/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -A Cache acelera sua aplicação armazenando dados obtidos com dificuldade uma vez para uso futuro. Mostraremos: - -- como usar a cache -- como alterar o armazenamento -- como invalidar corretamente a cache - -</div> - -Usar a cache no Nette é muito fácil, mas cobre até mesmo as necessidades mais avançadas. É projetado para desempenho e 100% de resiliência. Basicamente, você encontrará adaptadores para os armazenamentos de backend mais comuns. Permite invalidação baseada em tags, expiração por tempo, tem proteção contra cache stampede, etc. - - -Instalação -========== - -Faça o download e instale a biblioteca usando o [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Uso Básico -========== - -O centro do trabalho com a cache é o objeto [api:Nette\Caching\Cache]. Criamos sua instância e passamos o chamado armazenamento como parâmetro para o construtor. Este é um objeto que representa o local onde os dados serão fisicamente armazenados (banco de dados, Memcached, arquivos em disco, ...). Acessamos o armazenamento pedindo que ele seja passado usando [injeção de dependência |dependency-injection:passing-dependencies] com o tipo `Nette\Caching\Storage`. Tudo o essencial pode ser encontrado na [seção Armazenamentos |#Armazenamentos]. - -.[warning] -Na versão 3.0, a interface ainda tinha o prefixo `I`, então o nome era `Nette\Caching\IStorage`. Além disso, as constantes da classe `Cache` eram escritas em maiúsculas, como `Cache::EXPIRE` em vez de `Cache::Expire`. - -Para os exemplos a seguir, suponha que temos um alias `Cache` criado e o armazenamento na variável `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -A cache é na verdade um *key–value store*, ou seja, lemos e escrevemos dados sob chaves, assim como em arrays associativos. As aplicações consistem em várias partes independentes e, se todas usassem um único armazenamento (imagine um único diretório no disco), mais cedo ou mais tarde ocorreria uma colisão de chaves. O Nette Framework resolve o problema dividindo todo o espaço em namespaces (subdiretórios). Cada parte do programa então usa seu próprio espaço com um nome único e nenhuma colisão pode ocorrer. - -O nome do espaço é especificado como o segundo parâmetro do construtor da classe Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Agora podemos usar o objeto `$cache` para ler e escrever na cache. O método `load()` serve para ambos. O primeiro argumento é a chave e o segundo é um callback PHP, que é chamado quando a chave não é encontrada na cache. O callback gera o valor, retorna-o e ele é armazenado na cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // cálculo intensivo - return $computedValue; -}); -``` - -Se o segundo parâmetro não for especificado `$value = $cache->load($key)`, `null` será retornado se o item não estiver na cache. - -.[tip] -O bom é que qualquer estrutura serializável pode ser armazenada na cache, não precisa ser apenas strings. E o mesmo se aplica até mesmo às chaves. - -Removemos um item da cache usando o método `remove()`: - -```php -$cache->remove($key); -``` - -Também é possível salvar um item na cache usando o método `$cache->save($key, $value, array $dependencies = [])`. No entanto, o método preferido é o mencionado acima usando `load()`. - - -Memoização -========== - -Memoização significa armazenar em cache o resultado de uma chamada de função ou método para que você possa usá-lo na próxima vez sem calcular a mesma coisa repetidamente. - -Métodos e funções podem ser chamados com memoização usando `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -A função `gethostbyaddr()` será chamada apenas uma vez para cada parâmetro `$ip` e, na próxima vez, o valor da cache será retornado. - -Também é possível criar um invólucro memoizado em torno de um método ou função que pode ser chamado posteriormente: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // calcula pela primeira vez -$result = $memoizedFactorial(5); // pela segunda vez, da cache -``` - - -Expiração & Invalidação -======================= - -Ao armazenar em cache, é necessário resolver a questão de quando os dados armazenados anteriormente se tornam inválidos. O Nette Framework oferece um mecanismo para limitar a validade dos dados ou excluí-los de forma controlada (na terminologia do framework, "invalidar"). - -A validade dos dados é definida no momento do armazenamento usando o terceiro parâmetro do método `save()`, por exemplo: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Ou usando o parâmetro `$dependencies` passado por referência para o callback do método `load()`, por exemplo: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ou usando o 3º parâmetro no método `load()`, que define as dependências se o item for gerado: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -Nos exemplos a seguir, assumiremos a segunda variante e, portanto, a existência da variável `$dependencies`. - - -Expiração ---------- - -A expiração mais simples é um limite de tempo. Desta forma, armazenamos dados na cache com validade de 20 minutos: - -```php -// também aceita número de segundos ou timestamp UNIX -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Se quisermos estender o período de validade a cada leitura, isso pode ser alcançado da seguinte forma, mas atenção, a sobrecarga da cache aumentará: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Uma opção útil é deixar os dados expirarem quando um arquivo ou um de vários arquivos for alterado. Isso pode ser usado, por exemplo, ao armazenar na cache dados gerados pelo processamento desses arquivos. Use caminhos absolutos. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// ou -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Podemos deixar um item na cache expirar quando outro item (ou um de vários outros) expirar. Isso pode ser usado quando armazenamos, por exemplo, uma página HTML inteira na cache e seus fragmentos sob outras chaves. Assim que um fragmento muda, a página inteira é invalidada. Se tivermos fragmentos armazenados sob chaves como `frag1` e `frag2`, usamos: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -A expiração também pode ser controlada usando funções personalizadas ou métodos estáticos, que sempre decidem na leitura se o item ainda é válido. Desta forma, por exemplo, podemos deixar um item expirar sempre que a versão do PHP mudar. Criamos uma função que compara a versão atual com um parâmetro e, ao salvar, adicionamos um array no formato `[callable, ...argumentos]` entre as dependências: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expirar quando checkPhpVersion(...) === false -]; -``` - -Todos os critérios podem, obviamente, ser combinados. A cache então expirará quando pelo menos um critério não for atendido. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidação usando tags ------------------------ - -Uma ferramenta de invalidação muito útil são as chamadas tags. Podemos atribuir uma lista de tags a cada item na cache, que são strings arbitrárias. Por exemplo, tenhamos uma página HTML com um artigo e comentários que iremos armazenar em cache. Ao salvar, especificamos as tags: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Vamos para a administração. Aqui encontramos um formulário para editar o artigo. Juntamente com o salvamento do artigo no banco de dados, chamamos o comando `clean()`, que exclui itens da cache por tag: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Da mesma forma, no local de adição de um novo comentário (ou edição de um comentário), não nos esquecemos de invalidar a tag apropriada: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -O que alcançamos com isso? Que nossa cache HTML será invalidada (excluída) sempre que o artigo ou os comentários forem alterados. Quando um artigo com ID = 123 é editado, a tag `article/123` é invalidada à força e a página HTML que carrega a tag mencionada é excluída da cache. O mesmo acontece ao inserir um novo comentário sob o artigo relevante. - -.[note] -Tags requerem o chamado [#Journal]. - - -Invalidação usando prioridade ------------------------------ - -Podemos definir uma prioridade para itens individuais na cache, que pode ser usada para excluí-los quando, por exemplo, a cache exceder um determinado tamanho: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Excluiremos todos os itens com prioridade igual ou menor que 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Prioridades requerem o chamado [#Journal]. - - -Limpar a cache --------------- - -O parâmetro `Cache::All` exclui tudo: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Leitura em massa -================ - -Para leituras e escritas em massa na cache, usamos o método `bulkLoad()`, ao qual passamos um array de chaves e obtemos um array de valores (chave => valor): - -```php -$values = $cache->bulkLoad($keys); -``` - -O método `bulkLoad()` funciona de forma semelhante a `load()`, também com o segundo parâmetro callback, ao qual é passada a chave do item gerado: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // cálculo intensivo - return $computedValue; -}); -``` - - -Uso com PSR-16 .{data-version:3.3.1} -==================================== - -Para usar a Nette Cache com a interface PSR-16, você pode utilizar o adaptador `PsrCacheAdapter`. Ele permite uma integração perfeita entre a Nette Cache e qualquer código ou biblioteca que espera uma cache compatível com PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Agora você pode usar `$psrCache` como uma cache PSR-16: - -```php -$psrCache->set('key', 'value', 3600); // armazena o valor por 1 hora -$value = $psrCache->get('key', 'default'); -``` - -O adaptador suporta todos os métodos definidos em PSR-16, incluindo `getMultiple()`, `setMultiple()` e `deleteMultiple()`. Note que namespaces e dependências complexas (tags, prioridade, etc.) do Nette Cache não são diretamente expostos pela interface PSR-16. - - -Armazenamento em cache da saída -=============================== - -É muito elegante capturar e armazenar em cache a saída: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // imprimimos os dados - - $capture->end(); // salvamos a saída na cache -} -``` - -Caso a saída já esteja armazenada na cache, o método `capture()` a imprimirá e retornará `null`, portanto a condição não será executada. Caso contrário, ele começará a capturar a saída e retornará o objeto `$capture`, com o qual finalmente salvamos os dados impressos na cache. - -.[note] -Na versão 3.0, o método era chamado `$cache->start()`. - - -Armazenamento em cache no Latte -=============================== - -Armazenar em cache nos templates [Latte|latte:] é muito fácil, basta envolver a parte do template com as tags `{cache}...{/cache}`. A cache é invalidada automaticamente quando o template de origem é alterado (incluindo quaisquer templates incluídos dentro do bloco de cache). As tags `{cache}` podem ser aninhadas e, quando um bloco aninhado é invalidado (por exemplo, por uma tag), o bloco pai também é invalidado. - -Na tag, é possível especificar as chaves às quais a cache estará vinculada (aqui a variável `$id`) e definir a expiração e as [tags para invalidação |#Invalidação usando tags]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Todos os itens são opcionais, portanto não precisamos especificar nem a expiração, nem as tags, e finalmente nem as chaves. - -O uso da cache também pode ser condicionado usando `if` - o conteúdo será então armazenado em cache apenas se a condição for atendida: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Armazenamentos -============== - -Um armazenamento é um objeto que representa o local onde os dados são fisicamente armazenados. Podemos usar um banco de dados, um servidor Memcached ou o armazenamento mais acessível, que são arquivos em disco. - -|--------------------- |------------------------------------------------------- -| Armazenamento | Descrição -|--------------------- |------------------------------------------------------- -| [#FileStorage] | Armazenamento padrão, salva em arquivos no disco. -| [#MemcachedStorage] | Utiliza um servidor [Memcached|https://memcached.org]. -| [#MemoryStorage] | Os dados ficam temporariamente na memória (por requisição). -| [#SQLiteStorage] | Os dados são salvos em um banco de dados SQLite. -| [#DevNullStorage] | Os dados não são salvos, útil para testes. - -Você acessa o objeto de armazenamento padrão pedindo que ele seja passado usando [injeção de dependência |dependency-injection:passing-dependencies] com o tipo `Nette\Caching\Storage`. Como armazenamento padrão, o Nette fornece o objeto `FileStorage` que armazena dados no subdiretório `cache` no diretório para [arquivos temporários |application:bootstrapping#Arquivos temporários]. - -Você pode alterar o armazenamento na configuração: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Grava a cache em arquivos no disco. O armazenamento `Nette\Caching\Storages\FileStorage` é muito bem otimizado para desempenho e, acima de tudo, garante total atomicidade das operações. O que isso significa? Que ao usar a cache, não pode acontecer de lermos um arquivo que ainda não foi completamente escrito por outro processo, ou que alguém o exclua "enquanto estamos usando". O uso da cache é, portanto, completamente seguro. - -Este armazenamento também possui uma função importante integrada que evita um aumento extremo no uso da CPU quando a cache é excluída ou ainda não está aquecida (ou seja, criada). Esta é uma prevenção contra o "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Acontece que, em um determinado momento, um número maior de requisições simultâneas chega, querendo a mesma coisa da cache (por exemplo, o resultado de uma consulta SQL cara) e, como não está na cache, todos os processos começam a executar a mesma consulta SQL. A carga é assim multiplicada e pode até acontecer que nenhum processo consiga responder dentro do limite de tempo, a cache não seja criada e a aplicação entre em colapso. Felizmente, a cache no Nette funciona de forma que, com várias requisições simultâneas para um item, ele é gerado apenas pelo primeiro processo, os outros esperam e então usam o resultado gerado. - -Exemplo de criação manual de FileStorage (geralmente feito via DI): - -```php -// o armazenamento será o diretório '/path/to/temp' no disco -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -O servidor [Memcached|https://memcached.org] é um sistema de armazenamento em memória distribuída de alto desempenho, cujo adaptador é `Nette\Caching\Storages\MemcachedStorage`. Na configuração, especificamos o endereço IP e a porta, se for diferente do padrão 11211. - -.[caution] -Requer a extensão PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` é um armazenamento que guarda dados em um array PHP e, portanto, são perdidos com o término da requisição. - - -SQLiteStorage -------------- - -O banco de dados SQLite e o adaptador `Nette\Caching\Storages\SQLiteStorage` oferecem uma maneira de armazenar a cache em um único arquivo no disco. Na configuração, especificamos o caminho para este arquivo. - -.[caution] -Requer as extensões PHP `pdo` e `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Uma implementação especial de armazenamento é `Nette\Caching\Storages\DevNullStorage`, que na verdade não armazena dados. É, portanto, adequado para testes ou para desativar completamente a cache. - - -Uso da cache no código -====================== - -Ao usar a cache no código, temos duas maneiras de fazer isso. A primeira é pedir que o armazenamento seja passado usando [injeção de dependência |dependency-injection:passing-dependencies] e criar o objeto `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -A segunda opção é pedir que o objeto `Cache` seja passado diretamente: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -O objeto `Cache` é então criado diretamente na configuração desta forma: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette armazena tags e prioridades no chamado journal. Por padrão, o SQLite e o arquivo `journal.s3db` são usados para isso e **são necessárias as extensões PHP `pdo` e `pdo_sqlite`.** - -Você pode alterar o journal na configuração: - -```neon -services: - cache.journal: MyJournal -``` - - -Serviços DI -=========== - -Estes serviços são adicionados ao contêiner DI: - -| Nome | Tipo | Descrição -|-----------------|------------------------------------------|--------------------------------------------------- -| `cache.storage` | `Nette\Caching\Storage` | O serviço de armazenamento de cache padrão (geralmente FileStorage). -| `cache.journal` | `Nette\Caching\Storages\Journal` | O journal padrão (geralmente SQLiteJournal). - - -Desativar a cache -================= - -Uma das opções para desativar a cache na aplicação é definir o armazenamento como [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Esta configuração não afeta o armazenamento em cache de templates no Latte ou no contêiner DI, pois essas bibliotecas não usam os serviços nette/caching e gerenciam sua própria cache de forma independente. Afinal, a cache delas [não precisa ser desativada |nette:troubleshooting#Como desativar o cache durante o desenvolvimento] no modo de desenvolvimento. diff --git a/caching/pt/@meta.texy b/caching/pt/@meta.texy deleted file mode 100644 index e2566bcb44..0000000000 --- a/caching/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentação Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/ro/@home.texy b/caching/ro/@home.texy deleted file mode 100644 index 2bff23a31e..0000000000 --- a/caching/ro/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Cache-ul accelerează aplicația dvs. salvând datele obținute cu efort pentru utilizare ulterioară. Vom arăta: - -- cum să utilizați cache-ul -- cum să schimbați stocarea -- cum să invalidați corect cache-ul - -</div> - -Utilizarea cache-ului în Nette este foarte ușoară, acoperind în același timp și nevoi foarte avansate. Este proiectat pentru performanță și rezistență 100%. În mod implicit, veți găsi adaptoare pentru cele mai comune stocări backend. Permite invalidarea bazată pe tag-uri, expirarea în timp, are protecție împotriva cache stampede etc. - - -Instalare -========= - -Descărcați și instalați biblioteca folosind [Composer |best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Utilizare de bază -================= - -Centrul lucrului cu cache-ul este obiectul [Cache |api:Nette\Caching\Cache]. Creăm o instanță a acestuia și îi transmitem constructorului așa-numita stocare (storage). Acesta este un obiect care reprezintă locul unde datele vor fi stocate fizic (bază de date, Memcached, fișiere pe disc, ...). Ajungem la stocare lăsându-ne să o primim prin [dependency injection |dependency-injection:passing-dependencies] cu tipul `Nette\Caching\Storage`. Veți afla tot ce este esențial în [secțiunea Stocări |#Stocări]. - -.[warning] -În versiunea 3.0, interfața avea încă prefixul `I`, deci numele era `Nette\Caching\IStorage`. De asemenea, constantele clasei `Cache` erau scrise cu majuscule, deci, de exemplu, `Cache::EXPIRE` în loc de `Cache::Expire`. - -Pentru următoarele exemple, presupunem că avem un alias `Cache` creat și stocarea în variabila `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instanță de Nette\Caching\Storage -``` - -Cache-ul este de fapt un *key–value store*, adică citim și scriem date sub chei la fel ca în array-urile asociative. Aplicațiile sunt compuse din mai multe părți independente și dacă toate ar folosi o singură stocare (imaginați-vă un singur director pe disc), mai devreme sau mai târziu ar apărea o coliziune de chei. Nette Framework rezolvă problema împărțind întregul spațiu în spații de nume (subdirectoare). Fiecare parte a programului folosește apoi propriul spațiu cu un nume unic și nu mai poate apărea nicio coliziune. - -Numele spațiului îl specificăm ca al doilea parametru al constructorului clasei Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Acum putem folosi obiectul `$cache` pentru a citi și scrie în cache. Pentru ambele servește metoda `load()`. Primul argument este cheia și al doilea este un callback PHP, care este apelat atunci când cheia nu este găsită în cache. Callback-ul generează valoarea, o returnează și aceasta este salvată în cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // calcul costisitor - return $computedValue; -}); -``` - -Dacă nu specificăm al doilea parametru `$value = $cache->load($key)`, se va returna `null` dacă elementul nu este în cache. - -.[tip] -Este grozav că în cache pot fi stocate orice structuri serializabile, nu trebuie să fie doar șiruri de caractere. Și același lucru este valabil chiar și pentru chei. - -Elementul din cache îl ștergem cu metoda `remove()`: - -```php -$cache->remove($key); -``` - -Salvarea unui element în cache se poate face și cu metoda `$cache->save($key, $value, array $dependencies = [])`. Cu toate acestea, metoda preferată este cea menționată mai sus, folosind `load()`, deoarece gestionează atomic generarea și salvarea datelor. - - -Memoizare -========= - -Memoizarea înseamnă stocarea în cache a rezultatului apelării unei funcții sau metode, astfel încât să îl puteți utiliza data viitoare fără a calcula același lucru din nou și din nou. - -Metodele și funcțiile pot fi apelate memoizat folosind `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Funcția `gethostbyaddr()` va fi astfel apelată pentru fiecare parametru `$ip` o singură dată, iar data viitoare se va returna valoarea din cache. - -De asemenea, este posibil să creați un wrapper memoizat peste o metodă sau funcție, care poate fi apelat ulterior: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // calculează prima dată -$result = $memoizedFactorial(5); // a doua oară din cache -``` - - -Expirare & invalidare -===================== - -Cu stocarea în cache, trebuie rezolvată problema când datele stocate anterior devin invalide. Nette Framework oferă un mecanism pentru a limita validitatea datelor sau pentru a le șterge controlat (în terminologia framework-ului „a invalida”). - -Validitatea datelor se setează în momentul salvării, folosind al treilea parametru al metodei `save()`, de exemplu: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Sau folosind parametrul `$dependencies` transmis prin referință la callback-ul metodei `load()`, de exemplu: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Sau folosind al treilea parametru în metoda `load()`, de exemplu: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -În următoarele exemple, vom presupune a doua variantă și, prin urmare, existența variabilei `$dependencies`. - - -Expirare --------- - -Cea mai simplă expirare este limita de timp. Astfel stocăm date în cache cu o valabilitate de 20 de minute: - -```php -// acceptă și numărul de secunde sau timestamp UNIX -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Dacă am dori să prelungim perioada de valabilitate la fiecare citire, se poate realiza astfel, dar atenție, costul cache-ului va crește: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Este utilă posibilitatea de a lăsa datele să expire în momentul în care se modifică un fișier sau unul dintre mai multe fișiere. Acest lucru poate fi utilizat, de exemplu, la stocarea în cache a datelor rezultate din procesarea acestor fișiere. Utilizați căi absolute. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// sau -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Putem lăsa un element din cache să expire în momentul în care expiră un alt element (sau unul dintre mai multe altele). Acest lucru poate fi utilizat atunci când stocăm în cache, de exemplu, o întreagă pagină HTML și sub alte chei fragmentele sale. De îndată ce un fragment se modifică, întreaga pagină este invalidată. Dacă fragmentele sunt stocate sub cheile, de exemplu, `frag1` și `frag2`, folosim: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Expirarea poate fi controlată și prin funcții proprii sau metode statice, care decid întotdeauna la citire dacă elementul este încă valid. Astfel, de exemplu, putem lăsa un element să expire ori de câte ori se schimbă versiunea PHP. Creăm o funcție care compară versiunea curentă cu parametrul și, la salvare, adăugăm între dependențe un array de forma `[nume functie, ...argumente]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expiră când checkPhpVersion(...) === false -]; -``` - -Toate criteriile pot fi, desigur, combinate. Cache-ul va expira atunci când cel puțin un criteriu nu este îndeplinit. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidare prin tag-uri ------------------------ - -Un instrument de invalidare foarte util sunt așa-numitele tag-uri. Fiecărui element din cache îi putem atribui la salvare o listă de tag-uri, care sunt șiruri de caractere arbitrare. Să avem, de exemplu, o pagină HTML cu un articol și comentarii, pe care o vom stoca în cache. La salvare, specificăm tag-urile: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Să ne mutăm în administrare. Aici găsim un formular pentru editarea articolului. Împreună cu salvarea articolului în baza de date, vom apela comanda `clean()`, care va șterge din cache elementele conform tag-ului: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -La fel, în locul adăugării unui nou comentariu (sau editării unui comentariu), nu vom omite invalidarea tag-ului corespunzător: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Ce am obținut prin asta? Că cache-ul nostru HTML se va invalida (șterge) ori de câte ori se modifică articolul sau comentariile. Când se editează articolul cu ID = 10, se va forța invalidarea tag-ului `article/10` și pagina HTML care poartă tag-ul menționat se va șterge din cache. Același lucru se întâmplă la inserarea unui nou comentariu sub articolul respectiv. - -.[note] -Tag-urile necesită așa-numitul [#Journal]. - - -Invalidare prin prioritate --------------------------- - -Fiecărui element din cache îi putem seta o prioritate, cu ajutorul căreia va fi posibil să le ștergem, de exemplu, când cache-ul depășește o anumită dimensiune: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Ștergem toate elementele cu prioritate egală sau mai mică de 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Prioritățile necesită așa-numitul [#Journal]. - - -Ștergerea cache-ului --------------------- - -Parametrul `Cache::All` șterge tot: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Citire în masă -============== - -Pentru citirea și scrierea în masă în cache servește metoda `bulkLoad()`, căreia îi transmitem un array de chei și obținem un array de valori: - -```php -$values = $cache->bulkLoad($keys); -``` - -Metoda `bulkLoad()` funcționează similar cu `load()`, inclusiv cu al doilea parametru callback, căruia i se transmite cheia elementului generat: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // calcul costisitor - return $computedValue; -}); -``` - - -Utilizare cu PSR-16 .{data-version:3.3.1} -========================================= - -Pentru a utiliza Nette Cache cu interfața PSR-16 (Simple Cache), puteți folosi adaptorul `Nette\Bridges\Psr\PsrCacheAdapter`. Acesta permite integrarea fără probleme între Nette Cache (`Nette\Caching\Storage`) și orice cod sau bibliotecă care așteaptă un cache compatibil PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Acum puteți utiliza `$psrCache` ca un cache PSR-16: - -```php -$psrCache->set('key', 'value', 3600); // salvează valoarea pentru 1 oră -$value = $psrCache->get('key', 'default'); -``` - -Adaptorul suportă toate metodele definite în PSR-16, inclusiv `getMultiple()`, `setMultiple()` și `deleteMultiple()`. - - -Stocarea în cache a ieșirii -=========================== - -Se poate captura și stoca în cache ieșirea foarte elegant: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // afișăm date - - $capture->end(); // salvăm ieșirea în cache -} -``` - -În cazul în care ieșirea este deja stocată în cache, metoda `capture()` o va afișa și va returna `null`, deci condiția nu se va executa. În caz contrar, va începe să captureze ieșirea și va returna obiectul `$capture`, cu ajutorul căruia vom salva în final datele afișate în cache. - -.[note] -În versiunea 3.0, metoda se numea `$cache->start()`. - - -Stocarea în cache în Latte -========================== - -Stocarea în cache în șabloanele [Latte |latte:] este foarte ușoară, este suficient să încadrați o parte a șablonului cu tag-urile `{cache}...{/cache}`. Cache-ul se invalidează automat în momentul în care se modifică șablonul sursă (inclusiv eventualele șabloane incluse în interiorul blocului `{cache}`). Tag-urile `{cache}` pot fi imbricate, iar când un bloc imbricat devine invalid (de exemplu, printr-un tag), blocul părinte devine și el invalid. - -În tag se pot specifica chei suplimentare de care va depinde cache-ul (aici variabila `$id`), se poate seta expirarea și [tag-urile pentru invalidare |#Invalidare prin tag-uri]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Toate elementele sunt opționale, deci nu trebuie să specificăm nici expirarea, nici tag-urile, și nici măcar cheile. - -Utilizarea cache-ului poate fi, de asemenea, condiționată folosind `if` - conținutul va fi stocat în cache doar dacă condiția este îndeplinită: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Stocări -======= - -Stocarea este un obiect care reprezintă locul unde datele sunt stocate fizic. Putem folosi o bază de date, un server Memcached sau cea mai accesibilă stocare, care sunt fișierele pe disc. - -|----------------- -| Stocare | Descriere -|----------------- -| [#FileStorage] | stocare implicită cu salvare în fișiere pe disc -| [#MemcachedStorage] | utilizează serverul `Memcached` -| [#MemoryStorage] | datele sunt temporar în memorie -| [#SQLiteStorage] | datele se salvează într-o bază de date SQLite -| [#DevNullStorage] | datele nu se salvează, potrivit pentru testare - -La obiectul de stocare ajungeți lăsându-vă să vi-l transmită prin [dependency injection |dependency-injection:passing-dependencies] cu tipul `Nette\Caching\Storage`. Ca stocare implicită, Nette oferă obiectul `FileStorage` care salvează datele în subdirectorul `cache` din directorul pentru [fișiere temporare |application:bootstrapping#Fișiere temporare]. - -Puteți schimba stocarea implicită în configurația `services.neon`: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Scrie cache-ul în fișiere pe disc. Stocarea `Nette\Caching\Storages\FileStorage` este foarte bine optimizată pentru performanță și, mai presus de toate, asigură atomicitatea completă a operațiunilor. Ce înseamnă asta? Că la utilizarea cache-ului nu se poate întâmpla să citim un fișier care nu a fost încă scris complet de un alt fir de execuție, sau ca cineva să ni-l șteargă „sub nas”. Utilizarea cache-ului este, prin urmare, complet sigură. - -Această stocare are, de asemenea, o funcție importantă încorporată, care previne creșterea extremă a utilizării CPU în momentul în care cache-ul este șters sau nu este încă încălzit (adică creat) - fenomen cunoscut sub numele de "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Se întâmplă ca, la un moment dat, să apară un număr mai mare de cereri concurente care doresc același lucru din cache (de exemplu, rezultatul unei interogări SQL costisitoare) și, deoarece nu se află în cache, toate procesele încep să execute aceeași interogare SQL. Sarcina se multiplică astfel și se poate chiar întâmpla ca niciun fir de execuție să nu reușească să răspundă în limita de timp, cache-ul să nu se creeze și aplicația să se prăbușească. Din fericire, cache-ul din Nette (cu `FileStorage`) funcționează astfel încât, în cazul mai multor cereri concurente pentru un singur element, acesta este generat doar de primul fir de execuție, celelalte așteaptă și apoi utilizează rezultatul generat. - -Exemplu de creare a FileStorage: - -```php -// stocarea va fi directorul '/path/to/temp' pe disc -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Serverul [Memcached |https://memcached.org] este un sistem de înaltă performanță pentru stocarea în memorie distribuită, al cărui adaptor este `Nette\Caching\Storages\MemcachedStorage`. În configurație specificăm adresa IP și portul, dacă diferă de cel standard 11211. - -.[caution] -Necesită extensia PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` este o stocare care salvează datele într-un array PHP și, prin urmare, se pierd la terminarea cererii. - - -SQLiteStorage -------------- - -Baza de date SQLite și adaptorul `Nette\Caching\Storages\SQLiteStorage` oferă o modalitate de a stoca cache-ul într-un singur fișier pe disc. În configurație specificăm calea către acest fișier. - -.[caution] -Necesită extensiile PHP `pdo` și `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -O implementare specială a stocării este `Nette\Caching\Storages\DevNullStorage`, care de fapt nu stochează deloc datele. Este astfel potrivită pentru testare, când dorim să eliminăm influența cache-ului. - - -Utilizarea cache-ului în cod -============================ - -La utilizarea cache-ului în cod, avem două moduri de a proceda. Primul este să ne lăsăm să primim stocarea prin [dependency injection |dependency-injection:passing-dependencies] și să creăm obiectul `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -A doua opțiune este să ne lăsăm să primim direct obiectul `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Obiectul `Cache` este apoi creat direct în configurație în acest mod: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette stochează tag-urile și prioritățile în așa-numitul journal. În mod standard, se utilizează SQLite și fișierul `journal.s3db` și **sunt necesare extensiile PHP `pdo` și `pdo_sqlite`.** - -Puteți schimba journal-ul în configurație: - -```neon -services: - cache.journal: MyJournal -``` - - -Servicii DI -=========== - -Aceste servicii sunt adăugate implicit în containerul DI de către extensia `nette/caching`: - -| Nume | Tip | Descriere -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | stocare - - -Dezactivarea cache-ului -======================= - -Una dintre opțiunile pentru a dezactiva *efectiv* cache-ul gestionat de `nette/caching` în aplicație este să setați ca stocare implicită [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Această setare nu afectează stocarea în cache a șabloanelor în Latte sau a containerului DI, deoarece aceste biblioteci nu utilizează serviciile nette/caching și își gestionează cache-ul independent. Cache-ul lor, de altfel, [nu trebuie dezactivat |nette:troubleshooting#Cum să dezactivați cache-ul în timpul dezvoltării] în modul dezvoltator. diff --git a/caching/ro/@meta.texy b/caching/ro/@meta.texy deleted file mode 100644 index 6554692600..0000000000 --- a/caching/ro/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentație Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/ru/@home.texy b/caching/ru/@home.texy index 784e86d219..e98ef446bf 100644 --- a/caching/ru/@home.texy +++ b/caching/ru/@home.texy @@ -3,36 +3,36 @@ Nette Caching <div class=perex> -Кеш ускоряет ваше приложение, сохраняя данные, полученные с трудом один раз, для последующего использования. Мы покажем вам: +Кеш ускоряет ваше приложение, сохраняя данные, получение которых однажды обошлось дорого, и позволяя быстрее обращаться к ним в дальнейшем. Мы разберём: - как использовать кеш -- как изменить хранилище -- как правильно инвалидировать кеш +- как сменить хранилище +- как правильно сбрасывать кеш </div> -Использование кеша в Nette очень просто, при этом оно покрывает даже очень продвинутые потребности. Он разработан для производительности и 100% отказоустойчивости. В основе вы найдете адаптеры для самых распространенных бэкенд-хранилищ. Позволяет инвалидацию на основе тегов, истечение срока действия по времени, имеет защиту от cache stampede и т. д. +Использовать кеш в Nette очень просто, и при этом он покрывает продуманные потребности кеширования. Он рассчитан на производительность и стопроцентную надёжность. В нём есть адаптеры для самых распространённых хранилищ. Он поддерживает сброс по тегам, истечение по времени, защиту от cache stampede и не только. Установка ========= -Скачать и установить библиотеку можно с помощью [Composer|best-practices:composer]: +Скачайте и установите пакет с помощью [Composer|best-practices:composer]: ```shell composer require nette/caching ``` -Основное использование -====================== +Основы использования +==================== -Центром работы с кешем является объект [api:Nette\Caching\Cache]. Мы создаем его экземпляр и передаем конструктору так называемое хранилище. Это объект, представляющий место, где данные будут физически храниться (база данных, Memcached, файлы на диске, ...). К хранилищу мы получаем доступ, запросив его с помощью [внедрения зависимостей |dependency-injection:passing-dependencies] с типом `Nette\Caching\Storage`. Все существенное вы узнаете в [разделе Хранилища |#Хранилища]. +Основной элемент для работы с кешем - объект [api:Nette\Caching\Cache]. Мы создаём его экземпляр, передавая в конструктор объект хранилища. Этот объект хранилища представляет физическое место, где будут храниться данные (база данных, Memcached, файлы на диске и т. д.). Обычно вы получаете объект хранилища через [внедрение зависимостей |dependency-injection:passing-dependencies], запросив тип `Nette\Caching\Storage`. Самое важное вы узнаете в разделе [Хранилища |#Хранилища]. .[warning] -В версии 3.0 интерфейс еще имел префикс `I`, поэтому название было `Nette\Caching\IStorage`. А также константы класса `Cache` были написаны заглавными буквами, например, `Cache::EXPIRE` вместо `Cache::Expire`. +В версии 3.0 у интерфейса ещё была приставка `I`, так что имя было `Nette\Caching\IStorage`. Кроме того, константы класса `Cache` записывались прописными буквами, например `Cache::EXPIRE` вместо `Cache::Expire`. -Для следующих примеров предположим, что у нас есть созданный псевдоним `Cache` и в переменной `$storage` хранилище. +В следующих примерах будем считать, что у нас есть псевдоним `Cache` и экземпляр хранилища в переменной `$storage`. ```php use Nette\Caching\Cache; @@ -40,51 +40,57 @@ use Nette\Caching\Cache; $storage = /* ... */; // экземпляр Nette\Caching\Storage ``` -Кеш — это, по сути, *key–value store*, то есть мы читаем и записываем данные под ключами так же, как в ассоциативных массивах. Приложения состоят из ряда независимых частей, и если все они будут использовать одно хранилище (представьте себе один каталог на диске), рано или поздно произойдет коллизия ключей. Nette Framework решает эту проблему, разделяя все пространство на пространства имен (подкаталоги). Каждая часть программы затем использует свое пространство с уникальным именем, и коллизий больше не происходит. +Кеш - это по сути *хранилище ключ-значение*, то есть мы читаем и пишем данные по ключам, как в ассоциативном массиве. Приложения состоят из нескольких независимых частей. Если бы все части использовали одно хранилище (представьте себе один каталог на диске), рано или поздно возникло бы столкновение ключей. Nette Framework решает это разделением пространства хранилища на пространства имён (по сути на подкаталоги). Каждая часть приложения тогда работает в собственном пространстве имён с уникальным именем, и никаких столкновений возникнуть не может. -Имя пространства указывается в качестве второго параметра конструктора класса Cache: +Имя пространства имён укажите вторым аргументом конструктора класса `Cache`: ```php $cache = new Cache($storage, 'Full Html Pages'); ``` -Теперь мы можем с помощью объекта `$cache` читать и записывать в кеш. Для обоих действий служит метод `load()`. Первым аргументом является ключ, а вторым — PHP-callback, который вызывается, если ключ не найден в кеше. Callback генерирует значение, возвращает его, и оно сохраняется в кеше: +При необходимости от существующего экземпляра можно вывести новый кеш, ограниченный подпространством имён, методом `derive()`: + +```php +$subCache = $cache->derive('Images'); +``` + +Теперь мы можем использовать объект `$cache` для чтения из кеша и записи в него. Обеим задачам служит метод `load()`. Первый аргумент - ключ, а второй - PHP-callback, который вызывается, если ключ в кеше не найден. Callback порождает значение, возвращает его, а метод `load()` его кеширует: ```php $value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // сложный расчет + $computedValue = /* ... */; // дорогое вычисление return $computedValue; }); ``` -Если второй параметр не указан `$value = $cache->load($key)`, вернется `null`, если элемент отсутствует в кеше. +Если второй параметр опущен (`$value = $cache->load($key)`), `load()` возвращает `null`, когда элемента в кеше нет. .[tip] -Здорово, что в кеш можно сохранять любые сериализуемые структуры, не обязательно только строки. И то же самое относится даже к ключам. +Прекрасно, что кешировать можно любые сериализуемые структуры, а не только строки. То же относится и к ключам. -Элемент из кеша удаляется методом `remove()`: +Чтобы удалить элемент из кеша, используйте метод `remove()`: ```php $cache->remove($key); ``` -Сохранить элемент в кеше можно также методом `$cache->save($key, $value, array $dependencies = [])`. Однако предпочтительным является вышеуказанный способ с использованием `load()`. +Сохранить элемент в кеш можно и методом `$cache->save($key, $data, ?array $dependencies = null)`. Однако подход с `load()`, показанный выше, обычно предпочтительнее. Мемоизация ========== -Мемоизация означает кеширование результата вызова функции или метода, чтобы вы могли использовать его в следующий раз, не вычисляя то же самое снова и снова. +Мемоизация состоит в кешировании результата вызова функции или метода, так что при следующем вызове с теми же аргументами вместо повторного вычисления возвращается закешированный результат. -Мемоизированно можно вызывать методы и функции с помощью `call(callable $callback, ...$args)`: +Методы и функции можно вызывать мемоизированно с помощью `call(callable $callback, ...$args)`: ```php $result = $cache->call('gethostbyaddr', $ip); ``` -Функция `gethostbyaddr()` таким образом вызывается для каждого параметра `$ip` только один раз, а в следующий раз уже возвращается значение из кеша. +Функция `gethostbyaddr()` тем самым вызывается только один раз для каждого уникального аргумента `$ip`. Последующие вызовы с тем же `$ip` вернут закешированное значение. -Также можно создать мемоизированную обертку над методом или функцией, которую можно вызвать позже: +Можно создать и мемоизированную обёртку вокруг метода или функции, которую затем вызывать: ```php function factorial($num) @@ -94,17 +100,17 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // вычисляет в первый раз -$result = $memoizedFactorial(5); // во второй раз из кеша +$result = $memoizedFactorial(5); // в первый раз вычисляет +$result = $memoizedFactorial(5); // во второй раз возвращает из кеша ``` -Истечение срока действия и инвалидация -====================================== +Истечение и сброс +================= -При сохранении в кеш необходимо решить вопрос, когда ранее сохраненные данные станут недействительными. Nette Framework предлагает механизм для ограничения срока действия данных или их управляемого удаления (в терминологии фреймворка — «инвалидации»). +При использовании кеширования нужно решить вопрос о том, когда ранее сохранённые данные становятся недействительными. Nette Framework предоставляет механизмы для ограничения срока действия данных или их явного удаления (на языке фреймворка это называется "инвалидацией"). -Срок действия данных устанавливается в момент сохранения с помощью третьего параметра метода `save()`, например: +Срок действия данных задаётся в момент сохранения, обычно третьим параметром метода `save()`, например: ```php $cache->save($key, $value, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -Или с помощью параметра `$dependencies`, передаваемого по ссылке в callback метода `load()`, например: +Как вариант, его можно задать через параметр `$dependencies`, передаваемый по ссылке в callback метода `load()`, например: ```php $value = $cache->load($key, function (&$dependencies) { @@ -121,48 +127,48 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -Или с помощью 3-го параметра в методе `load()`, например: +Либо через 3-й параметр самого метода `load()`, например: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -В следующих примерах мы будем предполагать второй вариант и, следовательно, существование переменной `$dependencies`. +В следующих примерах будем исходить из второго варианта, использующего переменную `$dependencies` внутри callback'а. -Истечение срока действия ------------------------- +Истечение +--------- -Самое простое истечение срока действия — это временной лимит. Таким образом, мы сохраняем данные в кеше на 20 минут: +Простейший вид истечения - ограничение по времени. Так данные кешируются со сроком действия 20 минут: ```php -// принимает также количество секунд или UNIX timestamp +// принимает и количество секунд, и метку времени 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`, используем: +Мы можем сделать так, чтобы элемент кеша истёк, когда истечёт другой конкретный элемент (или один из нескольких). Это удобно, когда мы кешируем, например, целую HTML-страницу и её фрагменты под разными ключами. Когда фрагмент меняется, вся страница должна быть сброшена. Если фрагменты сохранены под ключами вроде `frag1` и `frag2`, используйте: ```php $dependencies[Cache::Items] = ['frag1', 'frag2']; ``` -Истечение срока действия можно контролировать и с помощью пользовательских функций или статических методов, которые при каждом чтении решают, действителен ли еще элемент. Таким образом, мы можем, например, сделать так, чтобы элемент истек всегда, когда изменяется версия PHP. Создадим функцию, которая сравнивает текущую версию с параметром, и при сохранении добавим в зависимости массив вида `[имя функции, ...аргументы]`: +Истечением можно управлять и с помощью собственных функций или статических методов. Они вызываются при каждом чтении, чтобы определить, действителен ли элемент ещё. Например, мы можем сделать так, чтобы элемент истекал всякий раз при смене версии PHP. Создайте функцию, которая сравнивает текущую версию с параметром, и при сохранении добавьте в зависимости массив вида `[имя функции, ...аргументы]`: ```php function checkPhpVersion($ver): bool @@ -171,11 +177,11 @@ function checkPhpVersion($ver): bool } $dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // истекает, когда checkPhpVersion(...) === false + ['checkPhpVersion', PHP_VERSION_ID] // истечёт, когда checkPhpVersion(...) === false ]; ``` -Все критерии, конечно, можно комбинировать. Кеш тогда истечет, когда хотя бы один критерий не выполнен. +Естественно, все эти условия можно сочетать. Элемент кеша истекает, если перестало выполняться хотя бы одно из них. ```php $dependencies[Cache::Expire] = '20 minutes'; @@ -183,16 +189,16 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; ``` -Инвалидация с помощью тегов ---------------------------- +Сброс по тегам +-------------- -Очень полезным инструментом инвалидации являются так называемые теги. Каждому элементу в кеше мы можем присвоить список тегов, которые являются произвольными строками. Допустим, у нас есть HTML-страница со статьей и комментариями, которую мы будем кешировать. При сохранении указываем теги: +Теги дают очень удобный механизм сброса. Каждому сохранённому в кеш элементу мы можем присвоить список тегов (произвольных строк). Например, допустим, у нас есть HTML-страница, показывающая статью и комментарии к ней, которую мы хотим закешировать. При сохранении мы указываем соответствующие теги: ```php $dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; ``` -Перейдем в админку. Здесь мы найдем форму для редактирования статьи. Вместе с сохранением статьи в базу данных вызовем команду `clean()`, которая удалит из кеша элементы по тегу: +Теперь перейдём в административную часть. Здесь у нас есть форма редактирования статей. Вместе с сохранением статьи в базу данных мы вызываем метод `clean()`, чтобы удалить закешированные элементы по их тегу: ```php $cache->clean([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -Точно так же в месте добавления нового комментария (или редактирования комментария) не забудем инвалидировать соответствующий тег: +Точно так же при добавлении нового комментария (или его редактировании) мы должны не забыть сбросить соответствующий тег: ```php $cache->clean([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -Чего мы этим достигли? Того, что наш HTML-кеш будет инвалидироваться (удаляться) всякий раз, когда изменяется статья или комментарии. При редактировании статьи с ID = 10 произойдет принудительная инвалидация тега `article/10`, и HTML-страница, несущая указанный тег, будет удалена из кеша. То же самое произойдет при добавлении нового комментария к соответствующей статье. +Чего мы добились? Наш HTML-кеш теперь будет сбрасываться (удаляться) всякий раз, когда меняется связанная статья или комментарии к ней. При редактировании статьи с ID = 10 сбрасывается тег `article/10`, и закешированная HTML-страница, несущая этот тег, удаляется. То же самое происходит при добавлении нового комментария под соответствующей статьёй. .[note] -Теги требуют так называемый [#Journal]. +Теги требуют [журнала |#Журнал]. -Инвалидация с помощью приоритета --------------------------------- +Сброс по приоритету +------------------- -Отдельным элементам в кеше мы можем установить приоритет, с помощью которого их можно будет удалять, например, когда кеш превысит определенный размер: +Отдельным элементам кеша можно присвоить приоритеты. Это позволяет управляемо удалять их, например когда кеш превышает определённый предел размера: ```php $dependencies[Cache::Priority] = 50; ``` -Удалим все элементы с приоритетом, равным или меньшим 100: +Чтобы удалить все элементы с приоритетом, равным 100 или меньше: ```php $cache->clean([ @@ -232,13 +238,13 @@ $cache->clean([ ``` .[note] -Приоритеты требуют так называемый [журнал |#Journal]. +Приоритеты требуют так называемого [журнала |#Журнал]. Очистка кеша ------------ -Параметр `Cache::All` удаляет все: +Параметр `Cache::All` очищает всё: ```php $cache->clean([ @@ -250,67 +256,76 @@ $cache->clean([ Массовое чтение =============== -Для массового чтения и записи в кеш служит метод `bulkLoad()`, которому мы передаем массив ключей и получаем массив значений: +Для массового чтения из кеша и записи в него служит метод `bulkLoad()`. Передайте ему массив ключей, и он вернёт массив соответствующих значений: ```php $values = $cache->bulkLoad($keys); ``` -Метод `bulkLoad()` работает аналогично `load()` и со вторым параметром-callback'ом, которому передается ключ генерируемого элемента: +Метод `bulkLoad()` работает похоже на `load()` и тоже принимает вторым параметром callback. Этот callback получает ключ порождаемого элемента: ```php $values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // сложный расчет + $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-совместимый кеш. +Чтобы использовать Nette Cache с интерфейсом PSR-16, можно воспользоваться `PsrCacheAdapter`. Он даёт бесшовную интеграцию между Nette Cache и любым кодом или библиотекой, ожидающей реализацию кеша, совместимую с PSR-16. ```php $psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); ``` -Теперь вы можете использовать `$psrCache` как PSR-16 кеш: +Теперь вы можете использовать `$psrCache` как обычный кеш PSR-16: ```php $psrCache->set('key', 'value', 3600); // сохраняет значение на 1 час $value = $psrCache->get('key', 'default'); ``` -Адаптер поддерживает все методы, определенные в PSR-16, включая `getMultiple()`, `setMultiple()` и `deleteMultiple()`. +Адаптер поддерживает все методы, определённые в PSR-16, включая `getMultiple()`, `setMultiple()` и `deleteMultiple()`. Кеширование вывода ================== -Очень элегантно можно перехватывать и кешировать вывод: +Вывод можно очень изящно перехватить и закешировать: ```php if ($capture = $cache->capture($key)) { - echo ... // выводим данные + // echo ... выводим какие-то данные $capture->end(); // сохраняем вывод в кеш } ``` -В случае, если вывод уже сохранен в кеше, метод `capture()` выведет его и вернет `null`, то есть условие не выполнится. В противном случае он начнет перехватывать вывод и вернет объект `$capture`, с помощью которого мы в конечном итоге сохраним выведенные данные в кеш. +Если вывод уже есть в кеше, метод `capture()` его выводит и возвращает `null`, так что блок условия `if` пропускается. Иначе он начинает буферизовать вывод и возвращает объект `$capture`, которым вы в конце сохраняете перехваченные данные в кеш через его метод `end()`. .[note] -В версии 3.0 метод назывался `$cache->start()`. +В версии 3.0 этот метод назывался `$cache->start()`. Кеширование в Latte =================== -Кеширование в шаблонах [Latte|latte:] очень просто, достаточно обернуть часть шаблона тегами `{cache}...{/cache}`. Кеш автоматически инвалидируется в момент изменения исходного шаблона (включая возможные включенные шаблоны внутри блока cache). Теги `{cache}` можно вкладывать друг в друга, и когда вложенный блок становится недействительным (например, по тегу), недействительным становится и родительский блок. +Кеширование в шаблонах [Latte|latte:] совсем просто. Достаточно обернуть часть шаблона, которую вы хотите закешировать, тегами `{cache}...{/cache}`. Кеш автоматически сбрасывается всякий раз, когда меняется исходный файл шаблона (включая любые шаблоны, подключённые внутри кешируемого блока). Теги `{cache}` можно вкладывать. Когда сбрасывается вложенный блок (например, по тегу), сбрасывается и родительский блок. -В теге можно указать ключи, к которым будет привязан кеш (здесь переменная `$id`), и установить срок действия и [теги для инвалидации |#Инвалидация с помощью тегов]. +Внутри тега можно указать ключи, к которым будет привязана запись кеша (здесь переменная `$id`), задать время истечения и определить [теги сброса |#Сброс по тегам]. ```latte {cache $id, expire: '20 minutes', tags: [tag1, tag2]} @@ -318,9 +333,9 @@ if ($capture = $cache->capture($key)) { {/cache} ``` -Все параметры необязательны, поэтому мы можем не указывать ни срок действия, ни теги, ни даже ключи. +Все эти параметры необязательны, так что указывать ни истечение, ни теги, ни даже ключи не обязательно. -Использование кеша также можно сделать условным с помощью `if` - содержимое тогда будет кешироваться только при выполнении условия: +Использование кеширования можно сделать и условным через `if`: содержимое будет закешировано, только если условие выполнено: ```latte {cache $id, if: !$form->isSubmitted()} @@ -332,20 +347,20 @@ if ($capture = $cache->capture($key)) { Хранилища ========= -Хранилище — это объект, представляющий место, где данные физически хранятся. Мы можем использовать базу данных, сервер Memcached или самое доступное хранилище — файлы на диске. +Хранилище - объект, представляющий физическое место, где хранятся данные. Мы можем использовать базу данных, сервер Memcached или самое доступное хранилище: файлы на диске. -|----------------- +|---------------------- | Хранилище | Описание -|----------------- -| [#FileStorage] | хранилище по умолчанию с сохранением в файлы на диск -| [#MemcachedStorage] | использует сервер `Memcached` -| [#MemoryStorage] | данные временно хранятся в памяти -| [#SQLiteStorage] | данные сохраняются в базу данных SQLite -| [#DevNullStorage] | данные не сохраняются, подходит для тестирования +|---------------------- +| [#FileStorage] | Хранилище по умолчанию, сохраняет кеш в файлы на диске. +| [#MemcachedStorage] | Для хранения использует сервер `Memcached`. +| [#MemoryStorage] | Данные временно хранятся в памяти (теряются в конце запроса). +| [#SQLiteStorage] | Данные хранятся в файле базы данных SQLite. +| [#DevNullStorage] | Данные на самом деле не хранятся; полезно для тестирования. -К объекту хранилища вы получаете доступ, запросив его с помощью [внедрения зависимостей |dependency-injection:passing-dependencies] с типом `Nette\Caching\Storage`. В качестве хранилища по умолчанию Nette предоставляет объект `FileStorage`, сохраняющий данные в подкаталог `cache` в каталоге для [временных файлов |application:bootstrapping#Временные файлы]. +Объект хранилища вы получаете через [внедрение зависимостей |dependency-injection:passing-dependencies], запросив тип `Nette\Caching\Storage`. По умолчанию Nette предоставляет объект `FileStorage`, который хранит данные в подкаталоге `cache` внутри каталога для [временных файлов |application:bootstrapping#Временные файлы]. -Изменить хранилище можно в конфигурации: +Изменить хранилище по умолчанию можно в конфигурации: ```neon services: @@ -356,11 +371,11 @@ services: FileStorage ----------- -Записывает кеш в файлы на диске. Хранилище `Nette\Caching\Storages\FileStorage` очень хорошо оптимизировано для производительности и, прежде всего, обеспечивает полную атомарность операций. Что это значит? Что при использовании кеша не может случиться так, что мы прочитаем файл, который еще не полностью записан другим потоком, или что кто-то удалит его "под рукой". Использование кеша, таким образом, полностью безопасно. +Записывает записи кеша в файлы на диске. Хранилище `Nette\Caching\Storages\FileStorage` сильно оптимизировано по производительности и, что принципиально важно, обеспечивает полную атомарность операций. Что это значит? При использовании кеша не может случиться, что вы прочитаете файл, который другой поток ещё не дописал до конца, или что кто-то удалит его, пока вы читаете. Поэтому использование этого хранилища кеша совершенно безопасно. -Это хранилище также имеет встроенную важную функцию, которая предотвращает экстремальный рост использования ЦП в момент, когда кеш удаляется или еще не прогрет (т. е. не создан). Это предотвращение "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Бывает, что в один момент сходится большое количество одновременных запросов, которые хотят из кеша одно и то же (например, результат дорогого SQL-запроса), и поскольку в кеше его нет, все процессы начинают выполнять один и тот же SQL-запрос. Нагрузка таким образом умножается, и может даже случиться так, что ни один поток не успеет ответить в течение временного лимита, кеш не создастся, и приложение рухнет. К счастью, кеш в Nette работает так, что при нескольких одновременных запросах к одному элементу его генерирует только первый поток, остальные ждут и затем используют сгенерированный результат. +В этом хранилище есть и важная встроенная возможность, предотвращающая чрезмерный всплеск нагрузки на процессор, когда кеш очищен или ещё "холодный" (то есть ещё не создан). Это защита от так называемого "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Он возникает, когда несколько одновременных запросов разом просят один и тот же элемент кеша (например, результат дорогого SQL-запроса). Если элемента в кеше в этот момент нет, все эти процессы могли бы начать выполнять одну и ту же дорогую операцию (тот же SQL-запрос). Это многократно увеличивает нагрузку на сервер, и может даже случиться, что ни один поток не успеет ответить в отведённое время, кеш не создастся, а приложение рухнет. К счастью, кеш Nette с этим справляется: при нескольких одновременных запросах на один и тот же элемент порождает его только первый поток. Остальные потоки ждут и затем используют результат, порождённый первым. -Пример создания FileStorage: +Пример создания `FileStorage`: ```php // хранилищем будет каталог '/path/to/temp' на диске @@ -371,10 +386,10 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -Сервер [Memcached|https://memcached.org] — это высокопроизводительная система хранения в распределенной памяти, адаптером для которой является `Nette\Caching\Storages\MemcachedStorage`. В конфигурации укажем IP-адрес и порт, если он отличается от стандартного 11211. +Сервер [Memcached |https://memcached.org] - высокопроизводительная распределённая система кеширования объектов в памяти. Его адаптер в Nette - `Nette\Caching\Storages\MemcachedStorage`. В конфигурации укажите IP-адрес сервера и порт, если он отличается от стандартного 11211. .[caution] -Требуется расширение PHP `memcached`. +Требует PHP-расширения `memcached`. ```neon services: @@ -385,16 +400,16 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage` — это хранилище, которое сохраняет данные в массив PHP, и, следовательно, они теряются при завершении запроса. +`Nette\Caching\Storages\MemoryStorage` - хранилище, которое держит данные в массиве PHP. Соответственно, данные теряются в конце запроса. SQLiteStorage ------------- -База данных SQLite и адаптер `Nette\Caching\Storages\SQLiteStorage` предлагают способ хранения кеша в одном файле на диске. В конфигурации укажем путь к этому файлу. +База данных SQLite вместе с адаптером `Nette\Caching\Storages\SQLiteStorage` даёт способ кешировать данные в одном файле на диске. В конфигурации указывается путь к этому файлу базы данных. .[caution] -Требуются расширения PHP `pdo` и `pdo_sqlite`. +Требует PHP-расширений `pdo` и `pdo_sqlite`. ```neon services: @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -Специальной реализацией хранилища является `Nette\Caching\Storages\DevNullStorage`, которое на самом деле вообще не сохраняет данные. Оно подходит для тестирования, когда мы хотим исключить влияние кеша. +Особая реализация хранилища - `Nette\Caching\Storages\DevNullStorage`, которая на самом деле никаких данных не хранит. Поэтому она подходит для тестирования, когда вы хотите исключить влияние кеширования. Использование кеша в коде ========================= -При использовании кеша в коде у нас есть два способа. Первый из них заключается в том, что мы запрашиваем хранилище с помощью [внедрения зависимостей |dependency-injection:passing-dependencies] и создаем объект `Cache`: +При использовании кеширования в своём коде есть два основных подхода. Первый - получить объект хранилища через [внедрение зависимостей |dependency-injection:passing-dependencies] и затем создать объект `Cache` самому: ```php use Nette; @@ -427,7 +442,7 @@ class ClassOne } ``` -Второй вариант — запросить сразу объект `Cache`: +Второй вариант - запросить объект `Cache` напрямую: ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -Объект `Cache` затем создается непосредственно в конфигурации следующим образом: +Объект `Cache` тогда нужно определить в конфигурации, например так: ```neon services: @@ -447,12 +462,12 @@ services: ``` -Journal -======= +Журнал +====== -Nette сохраняет теги и приоритеты в так называемый журнал. По умолчанию для этого используется SQLite и файл `journal.s3db`, и **требуются расширения PHP `pdo` и `pdo_sqlite`.** +Сведения о тегах и приоритетах Nette хранит в так называемом журнале. По умолчанию для этого используется SQLite через файл `journal.s3db`, и **требуются PHP-расширения `pdo` и `pdo_sqlite`**. -Изменить журнал можно в конфигурации: +Изменить реализацию журнала можно в конфигурации: ```neon services: @@ -465,20 +480,23 @@ services: Эти сервисы добавляются в DI-контейнер: -| Название | Тип | Описание +| Имя | Тип | Описание |---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | журнал -| `cache.storage` | [api:Nette\Caching\Storage] | хранилище +| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Хранилище журнала кеша +| `cache.storage` | [api:Nette\Caching\Storage] | Основное хранилище кеша Отключение кеша =============== -Одним из способов отключения кеша в приложении является установка в качестве хранилища [#DevNullStorage]: +Один из способов отключить кеширование в приложении - задать в качестве хранилища [#DevNullStorage]: ```neon services: cache.storage: Nette\Caching\Storages\DevNullStorage ``` -Эта настройка не влияет на кеширование шаблонов в Latte или DI-контейнера, поскольку эти библиотеки не используют сервисы nette/caching и управляют своим кешем самостоятельно. Их кеш, впрочем, [нет необходимости |nette:troubleshooting#Как отключить кеш во время разработки] отключать в режиме разработки. +Эта настройка не влияет на кеширование шаблонов Latte или DI-контейнера, потому что эти библиотеки не используют сервисы `nette/caching` и управляют своим кешем самостоятельно. Кроме того, их кеш [обычно не нужно отключать |nette:troubleshooting#Как отключить кеш при разработке?] в режиме разработки. + + +Если вы переходите на более новую версию, посмотрите страницу [обновления |upgrading]. diff --git a/caching/ru/@left-menu.texy b/caching/ru/@left-menu.texy new file mode 100644 index 0000000000..251951e6cd --- /dev/null +++ b/caching/ru/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 61577d6323..7f329adfce 100644 --- a/caching/ru/@meta.texy +++ b/caching/ru/@meta.texy @@ -1,2 +1 @@ {{sitename: Документация Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/ru/upgrading.texy b/caching/ru/upgrading.texy new file mode 100644 index 0000000000..d013c0ef2e --- /dev/null +++ b/caching/ru/upgrading.texy @@ -0,0 +1,20 @@ +Обновление +********** + + +Обновление до версии 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/sl/@home.texy b/caching/sl/@home.texy deleted file mode 100644 index f87e7a5f6e..0000000000 --- a/caching/sl/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Predpomnilnik pospeši vašo aplikacijo tako, da enkrat težko pridobljene podatke shrani za naslednjo uporabo. Pokazali bomo: - -- kako uporabljati predpomnilnik -- kako spremeniti shrambo -- kako pravilno invalidirati predpomnilnik - -</div> - -Uporaba predpomnilnika je v Nette zelo enostavna, hkrati pa pokriva tudi zelo napredne potrebe. Zasnovan je za zmogljivost in 100% odpornost. V osnovi najdete adapterje za najpogostejše zaledne shrambe. Omogoča invalidacijo, temelječo na značkah, časovni potek, ima zaščito pred cache stampede itd. - - -Namestitev -========== - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Osnovna uporaba -=============== - -Središče dela s predpomnilnikom predstavlja objekt [api:Nette\Caching\Cache]. Ustvarimo si njegovo instanco in kot parameter konstruktorju posredujemo t.i. shrambo. To je objekt, ki predstavlja mesto, kamor se bodo podatki fizično shranjevali (podatkovna baza, Memcached, datoteke na disku, ...). Do shrambe pridemo tako, da si jo pustimo posredovati s pomočjo [dependency injection |dependency-injection:passing-dependencies] s tipom `Nette\Caching\Storage`. Vse bistveno boste izvedeli v [odseku Shrambe |#Shrambe]. - -.[warning] -V različici 3.0 je imel vmesnik še predpono `I`, zato je bilo ime `Nette\Caching\IStorage`. Poleg tega so bile konstante razreda `Cache` zapisane z velikimi črkami, torej na primer `Cache::EXPIRE` namesto `Cache::Expire`. - -Za naslednje primere predpostavimo, da imamo ustvarjen alias `Cache` in v spremenljivki `$storage` shrambo. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -Predpomnilnik je pravzaprav *key–value store*, torej podatke beremo in zapisujemo pod ključi enako kot pri asociativnih poljih. Aplikacije so sestavljene iz vrste neodvisnih delov in če bi vsi uporabljali eno shrambo (predstavljajte si en imenik na disku), bi prej ali slej prišlo do kolizije ključev. Nette Framework problem rešuje tako, da celoten prostor deli na imenske prostore (podimenike). Vsak del programa nato uporablja svoj prostor z edinstvenim imenom in do nobene kolizije več ne more priti. - -Ime prostora navedemo kot drugi parameter konstruktorja razreda Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Zdaj lahko s pomočjo objekta `$cache` iz predpomnilnika beremo in vanj zapisujemo. Za oboje služi metoda `load()`. Prvi argument je ključ in drugi PHP povratni klic (callback), ki se pokliče, ko ključ ni najden v predpomnilniku. Povratni klic vrednost generira, vrne in ta se shrani v predpomnilnik: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // zahteven izračun - return $computedValue; -}); -``` - -Če drugega parametra ne navedemo `$value = $cache->load($key)`, se vrne `null`, če elementa v predpomnilniku ni. - -.[tip] -Odlično je, da lahko v predpomnilnik shranjujemo kakršnekoli serializabilne strukture, ni nujno, da so to samo nizi. In enako velja celo za ključe. - -Element iz predpomnilnika izbrišemo z metodo `remove()`: - -```php -$cache->remove($key); -``` - -Shranjevanje elementa v predpomnilnik je mogoče tudi z metodo `$cache->save($key, $value, array $dependencies = [])`. Vendar je prednostni zgoraj navedeni način s pomočjo `load()`. - - -Memoizacija -=========== - -Memoizacija pomeni predpomnjenje rezultata klica funkcije ali metode, da ga lahko uporabite naslednjič brez ponovnega izračunavanja iste stvari. - -Memoizirano lahko kličemo metode in funkcije s pomočjo `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Funkcija `gethostbyaddr()` se tako pokliče za vsak parameter `$ip` samo enkrat in naslednjič se že vrne vrednost iz predpomnilnika. - -Prav tako je mogoče ustvariti memoiziran ovoj nad metodo ali funkcijo, ki ga lahko kličemo kasneje: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // prvič izračuna -$result = $memoizedFactorial(5); // drugič iz predpomnilnika -``` - - -Potek & invalidacija -==================== - -Pri shranjevanju v predpomnilnik je treba rešiti vprašanje, kdaj prej shranjeni podatki postanejo neveljavni. Nette Framework ponuja mehanizem, kako omejiti veljavnost podatkov ali jih nadzorovano brisati (v terminologiji ogrodja "invalidirati"). - -Veljavnost podatkov se nastavi v trenutku shranjevanja in sicer s pomočjo tretjega parametra metode `save()`, npr.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Ali s pomočjo parametra `$dependencies`, posredovanega z referenco v povratni klic metode `load()`, npr.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ali s pomočjo 3. parametra v metodi `load()`, npr: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -V nadaljnjih primerih bomo predpostavljali drugo varianto in torej obstoj spremenljivke `$dependencies`. - - -Potek ------ - -Najenostavnejši potek predstavlja časovna omejitev. Tako shranimo v predpomnilnik podatke z veljavnostjo 20 minut: - -```php -// sprejema tudi število sekund ali UNIX časovni žig -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Če bi želeli podaljšati dobo veljavnosti z vsakim branjem, lahko to dosežemo na naslednji način, vendar pozor, režija predpomnilnika se s tem poveča: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Priročna je možnost, da podatki potečejo v trenutku, ko se spremeni datoteka ali katera od več datotek. To lahko izkoristimo na primer pri shranjevanju podatkov, nastalih z obdelavo teh datotek, v predpomnilnik. Uporabljajte absolutne poti. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// ali -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Element v predpomnilniku lahko pustimo poteči v trenutku, ko poteče drug element (ali kateri od več drugih). To lahko izkoristimo takrat, ko v predpomnilnik shranjujemo na primer celotno HTML stran in pod drugimi ključi njene fragmente. Takoj ko se fragment spremeni, se invalidira celotna stran. Če imamo fragmente shranjene pod ključi npr. `frag1` in `frag2`, uporabimo: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Potek lahko nadzorujemo tudi s pomočjo lastnih funkcij ali statičnih metod, ki vedno ob branju odločijo, ali je element še veljaven. Tako lahko na primer pustimo element poteči vedno, ko se spremeni različica PHP. Ustvarimo funkcijo, ki primerja trenutno različico s parametrom, in pri shranjevanju dodamo med odvisnosti polje v obliki `[ime funkcije, ...argumenti]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // poteče, ko checkPhpVersion(...) === false -]; -``` - -Vsa merila je seveda mogoče kombinirati. Predpomnilnik potem poteče, ko vsaj eno merilo ni izpolnjeno. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidacija s pomočjo značk ----------------------------- - -Zelo uporabno orodje za invalidacijo so t.i. značke. Vsakemu elementu v predpomnilniku lahko ob shranjevanju dodelimo seznam značk, ki so poljubni nizi. Imejmo na primer HTML stran s člankom in komentarji, ki jo bomo predpomnili. Pri shranjevanju specificiramo značke: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Premaknimo se v administracijo. Tu najdemo obrazec za urejanje članka. Skupaj s shranjevanjem članka v podatkovno bazo pokličemo ukaz `clean()`, ki izbriše iz predpomnilnika elemente glede na značko: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Enako tako na mestu dodajanja novega komentarja (ali urejanja komentarja) ne pozabimo invalidirati ustrezne značke: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Kaj smo s tem dosegli? Da se nam bo HTML predpomnilnik invalidiral (brisal), kadarkoli se spremeni članek ali komentarji. Ko se ureja članek z ID = 10, pride do prisilne invalidacije značke `article/10` in HTML stran, ki nosi navedeno značko, se izbriše iz predpomnilnika. Enako se zgodi pri vstavljanju novega komentarja pod ustrezen članek. - -.[note] -Značke zahtevajo t.i. [#Dnevnik Journal]. - - -Invalidacija s pomočjo prioritete ---------------------------------- - -Posameznim elementom v predpomnilniku lahko nastavimo prioriteto, s pomočjo katere jih bo mogoče brisati, ko na primer predpomnilnik preseže določeno velikost: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Izbrišemo vse elemente s prioriteto enako ali manjšo od 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Prioritete zahtevajo t.i. [#Dnevnik Journal]. - - -Brisanje predpomnilnika ------------------------ - -Parameter `Cache::All` izbriše vse: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Množično branje -=============== - -Za množično branje in pisanje v predpomnilnik služi metoda `bulkLoad()`, kateri posredujemo polje ključev in dobimo polje vrednosti: - -```php -$values = $cache->bulkLoad($keys); -``` - -Metoda `bulkLoad()` deluje podobno kot `load()` tudi z drugim parametrom povratnim klicem, kateremu se posreduje ključ generiranega elementa: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // zahteven izračun - return $computedValue; -}); -``` - - -Uporaba s PSR-16 .{data-version:3.3.1} -====================================== - -Za uporabo Nette Cache z vmesnikom PSR-16 lahko uporabite adapter `PsrCacheAdapter`. Omogoča brezšivno integracijo med Nette Cache in katerokoli kodo ali knjižnico, ki pričakuje PSR-16 združljiv predpomnilnik. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Zdaj lahko uporabljate `$psrCache` kot PSR-16 predpomnilnik: - -```php -$psrCache->set('key', 'value', 3600); // shrani vrednost za 1 uro -$value = $psrCache->get('key', 'default'); -``` - -Adapter podpira vse metode, definirane v PSR-16, vključno z `getMultiple()`, `setMultiple()` in `deleteMultiple()`. - - -Predpomnjenje izpisa -==================== - -Zelo elegantno lahko zajamemo in predpomnimo izpis: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // izpisujemo podatke - - $capture->end(); // shranimo izpis v predpomnilnik -} -``` - -V primeru, da je izpis že shranjen v predpomnilniku, ga metoda `capture()` izpiše in vrne `null`, torej se pogoj ne izvede. V nasprotnem primeru začne zajemati izpis in vrne objekt `$capture`, s pomočjo katerega na koncu izpisane podatke shranimo v predpomnilnik. - -.[note] -V različici 3.0 se je metoda imenovala `$cache->start()`. - - -Predpomnjenje v Latte -===================== - -Predpomnjenje v predlogah [Latte|latte:] je zelo enostavno, dovolj je, da del predloge ovijemo z značkami `{cache}...{/cache}`. Predpomnilnik se samodejno invalidira v trenutku, ko se spremeni izvorna predloga (vključno z morebitnimi vključenimi predlogami znotraj bloka cache). Značke `{cache}` lahko gnezdijo ena v drugo in ko se vgnezden blok razveljavi (na primer z značko), se razveljavi tudi nadrejeni blok. - -V znački je mogoče navesti ključe, na katere bo predpomnilnik vezan (tu spremenljivka `$id`) in nastaviti potek ter [značke za razveljavitev |#Invalidacija s pomočjo značk] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Vsi elementi so neobvezni, zato nam ni treba navajati niti poteka, niti značk, na koncu niti ključev. - -Uporabo predpomnilnika lahko tudi pogojimo s pomočjo `if` - vsebina se bo potem predpomnila samo, če bo pogoj izpolnjen: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Shrambe -======= - -Shramba je objekt, ki predstavlja mesto, kamor se podatki fizično shranjujejo. Lahko uporabimo podatkovno bazo, strežnik Memcached ali najdostopnejšo shrambo, kar so datoteke na disku. - -|----------------- -| Shramba | Opis -|----------------- -| [#FileStorage] | privzeta shramba s shranjevanjem v datoteke na disk -| [#MemcachedStorage] | uporablja `Memcached` strežnik -| [#MemoryStorage] | podatki so začasno v pomnilniku -| [#SQLiteStorage] | podatki se shranjujejo v SQLite podatkovno bazo -| [#DevNullStorage] | podatki se ne shranjujejo, primerno za testiranje - -Do objekta shrambe pridete tako, da si ga pustite posredovati s pomočjo [dependency injection |dependency-injection:passing-dependencies] s tipom `Nette\Caching\Storage`. Kot privzeto shrambo Nette ponuja objekt FileStorage, ki shranjuje podatke v podimenik `cache` v imeniku za [začasne datoteke |application:bootstrapping#Začasne datoteke]. - -Shrambo lahko spremenite v konfiguraciji: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Zapisuje predpomnilnik v datoteke na disku. Shramba `Nette\Caching\Storages\FileStorage` je zelo dobro optimizirana za zmogljivost in predvsem zagotavlja polno atomičnost operacij. Kaj to pomeni? Da se pri uporabi predpomnilnika ne more zgoditi, da bi prebrali datoteko, ki še ni bila popolnoma zapisana s strani druge niti, ali da bi vam jo kdo "pod roko" izbrisal. Uporaba predpomnilnika je torej popolnoma varna. - -Ta shramba ima tudi vgrajeno pomembno funkcijo, ki preprečuje ekstremno povečanje uporabe CPU v trenutku, ko se predpomnilnik izbriše ali še ni ogret (tj. ustvarjen). Gre za preprečevanje "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Zgodi se, da se v enem trenutku zbere večje število sočasnih zahtev, ki želijo iz predpomnilnika isto stvar (npr. rezultat drage SQL poizvedbe) in ker v predpomnilniku ni, začnejo vsi procesi izvajati isto SQL poizvedbo. Obremenitev se tako množi in lahko se celo zgodi, da nobena nit ne uspe odgovoriti v časovni omejitvi, predpomnilnik se ne ustvari in aplikacija propade. Na srečo predpomnilnik v Nette deluje tako, da pri več sočasnih zahtevah za en element ga generira samo prva nit, ostale čakajo in nato uporabijo generirani rezultat. - -Primer ustvarjanja FileStorage: - -```php -// shramba bo imenik '/path/to/temp' na disku -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Strežnik [Memcached|https://memcached.org] je visoko zmogljiv sistem shranjevanja v porazdeljenem pomnilniku, katerega adapter je `Nette\Caching\Storages\MemcachedStorage`. V konfiguraciji navedemo IP naslov in vrata, če se razlikujejo od standardnih 11211. - -.[caution] -Zahteva PHP razširitev `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` je shramba, ki podatke shranjuje v PHP polje, in se torej z zaključkom zahteve izgubijo. - - -SQLiteStorage -------------- - -Podatkovna baza SQLite in adapter `Nette\Caching\Storages\SQLiteStorage` ponujata način, kako shranjevati predpomnilnik v eno datoteko na disku. V konfiguraciji navedemo pot do te datoteke. - -.[caution] -Zahteva PHP razširitvi `pdo` in `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Posebna implementacija shrambe je `Nette\Caching\Storages\DevNullStorage`, ki dejansko podatkov sploh ne shranjuje. Je tako primerna za testiranje, ko želimo eliminirati vpliv predpomnilnika. - - -Uporaba predpomnilnika v kodi -============================= - -Pri uporabi predpomnilnika v kodi imamo dva načina, kako to storiti. Prvi je ta, da si pustimo posredovati s pomočjo [dependency injection |dependency-injection:passing-dependencies] shrambo in ustvarimo objekt `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Druga možnost je, da si pustimo neposredno posredovati objekt `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Objekt `Cache` se potem ustvari neposredno v konfiguraciji na ta način: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Dnevnik (Journal) -================= - -Nette si značke in prioritete shranjuje v t.i. dnevnik (journal). Standardno se za to uporablja SQLite in datoteka `journal.s3db` ter **zahtevata se PHP razširitvi `pdo` in `pdo_sqlite`.** - -Dnevnik lahko spremenite v konfiguraciji: - -```neon -services: - cache.journal: MyJournal -``` - - -Storitve DI -=========== - -Te storitve se dodajo v DI vsebnik: - -| Ime | Tip | Opis -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | dnevnik -| `cache.storage` | [api:Nette\Caching\Storage] | shramba - - -Izklop predpomnilnika -===================== - -Ena od možnosti, kako izklopiti predpomnilnik v aplikaciji, je nastaviti kot shrambo [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Ta nastavitev nima vpliva na predpomnjenje predlog v Latte ali DI vsebnika, ker te knjižnice ne uporabljajo storitev nette/caching in si upravljajo predpomnilnik samostojno. Njihovega predpomnilnika sicer [ni treba |nette:troubleshooting#Kako izklopiti predpomnilnik med razvojem] v razvojnem načinu izklapljati. diff --git a/caching/sl/@meta.texy b/caching/sl/@meta.texy deleted file mode 100644 index 282883a3d6..0000000000 --- a/caching/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Dokumentacija}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/tr/@home.texy b/caching/tr/@home.texy index a14067b97e..e74c679329 100644 --- a/caching/tr/@home.texy +++ b/caching/tr/@home.texy @@ -3,21 +3,21 @@ Nette Caching <div class=perex> -Önbellek, bir kez zorlukla elde edilen verileri bir sonraki kullanım için saklayarak uygulamanızı hızlandırır. Göstereceğiz: +Ö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: -- önbellek nasıl kullanılır -- depolama nasıl değiştirilir -- önbellek nasıl doğru bir şekilde geçersiz kılınır +- ö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 önbellek kullanımı çok kolaydır, ancak çok gelişmiş ihtiyaçları bile karşılar. Performans ve %100 dayanıklılık için tasarlanmıştır. Temelde en yaygın arka uç depolama alanları için adaptörler bulacaksınız. Etiket tabanlı geçersizleştirmeyi, zaman aşımını destekler, önbellek izdihamına karşı koruması vardır vb. +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 ======= -Kütüphaneyi [Composer|best-practices:composer] aracını kullanarak indirip kurabilirsiniz: +Paketi [Composer|best-practices:composer] kullanarak indirin ve kurun: ```shell composer require nette/caching @@ -27,12 +27,12 @@ composer require nette/caching Temel Kullanım ============== -Önbellekle çalışmanın merkezi noktası [api:Nette\Caching\Cache] nesnesidir. Bir örneğini oluştururuz ve kurucuya parametre olarak depolama adı verilen bir nesne geçiririz. Bu, verilerin fiziksel olarak depolanacağı yeri (veritabanı, Memcached, diskteki dosyalar, ...) temsil eden bir nesnedir. Depolamaya, `Nette\Caching\Storage` türüyle [dependency injection |dependency-injection:passing-dependencies] kullanarak geçirmemizi isteyerek erişiriz. Tüm önemli bilgileri [Depolama bölümünde |#Depolama] bulacaksınız. +Ö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] -Sürüm 3.0'da, arayüzün hala `I` öneki vardı, bu nedenle adı `Nette\Caching\IStorage` idi. Ayrıca, `Cache` sınıfının sabitleri büyük harflerle yazılmıştı, örneğin `Cache::Expire` yerine `Cache::EXPIRE`. +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` takma adını oluşturduğumuzu ve `$storage` değişkeninde bir depolama alanına sahip olduğumuzu varsayalım. +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; @@ -40,15 +40,21 @@ use Nette\Caching\Cache; $storage = /* ... */; // Nette\Caching\Storage örneği ``` -Önbellek aslında bir *anahtar-değer deposudur*, yani verileri ilişkisel dizilerde olduğu gibi anahtarlar altında okur ve yazarız. Uygulamalar bir dizi bağımsız bölümden oluşur ve hepsi tek bir depolama alanı kullanırsa (diskte tek bir dizin düşünün), er ya da geç anahtar çakışmaları meydana gelir. Nette Framework, tüm alanı ad alanlarına (alt dizinlere) bölerek sorunu çözer. Programın her bölümü daha sonra benzersiz bir ada sahip kendi alanını kullanır ve artık çakışma olmaz. +Ö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. -Alan adını Cache sınıfının kurucusunun ikinci parametresi olarak belirtiriz: +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'); ``` -Şimdi `$cache` nesnesini kullanarak önbellekten okuyabilir ve ona yazabiliriz. Her ikisi için de `load()` yöntemi kullanılır. İlk argüman anahtardır ve ikincisi, anahtar önbellekte bulunamadığında çağrılan bir PHP geri çağrısıdır. Geri çağrı değeri oluşturur, döndürür ve önbelleğe kaydedilir: +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) { @@ -57,34 +63,34 @@ $value = $cache->load($key, function () use ($key) { }); ``` -İkinci parametreyi belirtmezsek `$value = $cache->load($key)`, öğe önbellekte yoksa `null` döndürülür. +İkinci parametre atlanırsa (`$value = $cache->load($key)`), öğe önbellekte bulunamadığında `load()` metodu `null` döndürür. .[tip] -Harika olan şey, önbelleğe herhangi bir serileştirilebilir yapının kaydedilebilmesidir, yalnızca dizeler olması gerekmez. Ve aynı şey anahtarlar için bile geçerlidir. +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. -Öğeyi önbellekten `remove()` yöntemiyle sileriz: +Bir öğeyi önbellekten silmek için `remove()` metodunu kullanın: ```php $cache->remove($key); ``` -Bir öğeyi önbelleğe kaydetmek için `$cache->save($key, $value, array $dependencies = [])` yöntemi de kullanılabilir. Ancak, yukarıda belirtilen `load()` yöntemini kullanmak tercih edilir. +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. -Memoizasyon +Memoization =========== -Memoizasyon, bir fonksiyon veya metodun çağrısının sonucunu önbelleğe almak anlamına gelir, böylece aynı şeyi tekrar tekrar hesaplamadan bir dahaki sefere kullanabilirsiniz. +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 edilebilir: +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 `$ip` parametresi için yalnızca bir kez çağrılır ve bir dahaki sefere değer önbellekten döndürülür. +`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. -Ayrıca, daha sonra çağrılabilecek bir metot veya fonksiyon üzerinde memoize edilmiş bir sarmalayıcı oluşturmak da mümkündü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) @@ -94,17 +100,17 @@ function factorial($num) $memoizedFactorial = $cache->wrap('factorial'); -$result = $memoizedFactorial(5); // ilk kez hesaplar -$result = $memoizedFactorial(5); // ikinci kez önbellekten +$result = $memoizedFactorial(5); // ilk seferde hesaplar +$result = $memoizedFactorial(5); // ikinci seferde önbellekten döndürür ``` -Sona Erme & Geçersizleştirme -============================ +Süre Dolması ve Geçersiz Kılma +============================== -Önbelleğe kaydetme ile birlikte, daha önce kaydedilen verilerin ne zaman geçersiz hale geleceği sorusunu çözmek gerekir. Nette Framework, verilerin geçerliliğini sınırlamak veya kontrollü bir şekilde silmek (framework terminolojisinde "geçersiz kılmak") için bir mekanizma sunar. +Ö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). -Verilerin geçerliliği, kaydetme anında `save()` yönteminin üçüncü parametresi kullanılarak ayarlanır, örneğin: +Verinin geçerliliği kaydetme sırasında, genellikle `save()` metodunun üçüncü parametresiyle ayarlanır; örneğin: ```php $cache->save($key, $value, [ @@ -112,7 +118,7 @@ $cache->save($key, $value, [ ]); ``` -Veya `load()` yönteminin geri çağrısına referansla iletilen `$dependencies` parametresi kullanılarak, örneğin: +Alternatif olarak, `load()` metodundaki callback'e başvuruyla aktarılan `$dependencies` parametresiyle ayarlanabilir; örneğin: ```php $value = $cache->load($key, function (&$dependencies) { @@ -121,48 +127,48 @@ $value = $cache->load($key, function (&$dependencies) { }); ``` -Veya `load()` yöntemindeki 3. parametre kullanılarak, örneğin: +Ya da `load()` metodunun kendi 3. parametresi kullanılarak; örneğin: ```php $value = $cache->load($key, function () { - return ...; + return /* ... */; }, [Cache::Expire => '20 minutes']); ``` -Sonraki örneklerde, ikinci varyantı ve dolayısıyla `$dependencies` değişkeninin varlığını varsayacağız. +Aşağıdaki örneklerde, callback içinde `$dependencies` değişkenini kullanan ikinci çeşidi varsayacağız. -Sona Erme ---------- +Süre Dolması +------------ -En basit sona erme, bir zaman sınırıdır. Bu şekilde verileri 20 dakika geçerlilik süresiyle önbelleğe kaydederiz: +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ı veya UNIX zaman damgasını da kabul eder +// saniye sayısını ya da bir UNIX zaman damgasını da kabul eder $dependencies[Cache::Expire] = '20 minutes'; ``` -Her okumada geçerlilik süresini uzatmak istersek, bunu aşağıdaki gibi yapabiliriz, ancak dikkatli olun, önbellek ek yükü artacaktır: +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; ``` -Bir dosya veya birden fazla dosyadan herhangi biri değiştiğinde verilerin süresinin dolmasına izin verme seçeneği kullanışlıdır. Bu, örneğin bu dosyaların işlenmesinden kaynaklanan verileri önbelleğe kaydederken kullanılabilir. Mutlak yolları kullanın. +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'; -// veya +// ya da $dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; ``` -Bir öğenin süresinin başka bir öğenin (veya birden fazla öğeden herhangi birinin) süresi dolduğunda dolmasına izin verebiliriz. Bu, örneğin tüm bir HTML sayfasını önbelleğe kaydettiğimizde ve parçalarını başka anahtarlar altında sakladığımızda kullanılabilir. Parça değiştiğinde, tüm sayfa geçersiz kılınır. Parçaları örneğin `frag1` ve `frag2` anahtarları altında sakladıysak, şunu kullanırız: +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']; ``` -Sona erme, her okumada öğenin hala geçerli olup olmadığına karar veren özel fonksiyonlar veya statik metotlar kullanılarak da kontrol edilebilir. Bu şekilde, örneğin PHP sürümü değiştiğinde öğenin süresinin dolmasına izin verebiliriz. Mevcut sürümü parametreyle karşılaştıran bir fonksiyon oluştururuz ve kaydederken bağımlılıklar arasına `[fonksiyon adı, ...argümanlar]` şeklinde bir dizi ekleriz: +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 @@ -175,7 +181,7 @@ $dependencies[Cache::Callbacks] = [ ]; ``` -Tüm kriterler elbette birleştirilebilir. Önbellek daha sonra en az bir kriter karşılanmadığında sona erer. +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'; @@ -183,16 +189,16 @@ $dependencies[Cache::Files] = '/path/to/data.yaml'; ``` -Etiketlerle Geçersizleştirme ----------------------------- +Etiketlerle Geçersiz Kılma +-------------------------- -Çok kullanışlı bir geçersizleştirme aracı etiketlerdir. Önbellekteki her öğeye, herhangi bir dize olabilen bir etiket listesi atayabiliriz. Örneğin, önbelleğe alacağımız bir makale ve yorumları içeren bir HTML sayfamız olsun. Kaydederken etiketleri belirtiriz: +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"]; ``` -Yönetim paneline geçelim. Burada makaleyi düzenlemek için bir form bulacağız. Makaleyi veritabanına kaydetmekle birlikte, etikete göre önbellekten öğeleri silen `clean()` komutunu çağıracağız: +Ş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([ @@ -200,7 +206,7 @@ $cache->clean([ ]); ``` -Benzer şekilde, yeni bir yorum ekleme (veya bir yorumu düzenleme) yerinde, ilgili etiketi geçersiz kılmayı unutmayacağız: +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([ @@ -208,22 +214,22 @@ $cache->clean([ ]); ``` -Bununla ne başardık? Makale veya yorumlar değiştiğinde HTML önbelleğimizin geçersiz kılınmasını (silinmesini) sağladık. ID = 10 olan bir makale düzenlendiğinde, `article/10` etiketinin zorunlu geçersizleştirilmesi gerçekleşir ve belirtilen etiketi taşıyan HTML sayfası önbellekten silinir. Aynı şey, ilgili makalenin altına yeni bir yorum eklendiğinde de olur. +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 [#Journal] gerektirir. +Etiketler bir [#Journal] gerektirir. -Öncelikle Geçersizleştirme --------------------------- +Önceliğe Göre Geçersiz Kılma +---------------------------- -Önbellekteki bireysel öğelere bir öncelik ayarlayabiliriz, bu sayede örneğin önbellek belirli bir boyutu aştığında bunları silebiliriz: +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; ``` -100'e eşit veya daha düşük önceliğe sahip tüm öğeleri sileceğiz: +Önceliği 100'e eşit ya da ondan küçük olan tüm öğeleri silmek için: ```php $cache->clean([ @@ -232,13 +238,13 @@ $cache->clean([ ``` .[note] -Öncelikler [#Journal] gerektirir. +Öncelikler [#Journal] denen şeyi gerektirir. -Önbelleği Silme ---------------- +Önbelleği Temizleme +------------------- -`Cache::All` parametresi her şeyi siler: +`Cache::All` parametresi her şeyi temizler: ```php $cache->clean([ @@ -250,13 +256,13 @@ $cache->clean([ Toplu Okuma =========== -Önbelleğe toplu okuma ve yazma işlemleri için `bulkLoad()` yöntemi kullanılır, buna anahtar dizisini geçiririz ve değer dizisini alırız: +Ö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()` yöntemi, oluşturulan öğenin anahtarını alan ikinci bir geri çağırma parametresiyle `load()` yöntemine benzer şekilde çalışır: +`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) { @@ -265,52 +271,61 @@ $values = $cache->bulkLoad($keys, function ($key, &$dependencies) { }); ``` +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 PSR-16 arayüzüyle kullanmak için `PsrCacheAdapter` adaptörünü kullanabilirsiniz. Nette Cache ile PSR-16 uyumlu bir önbellek bekleyen herhangi bir kod veya kütüphane arasında sorunsuz entegrasyon sağlar. +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); ``` -Şimdi `$psrCache`'i PSR-16 önbelleği olarak kullanabilirsiniz: +Artık `$psrCache` nesnesini standart bir PSR-16 önbelleği olarak kullanabilirsiniz: ```php -$psrCache->set('key', 'value', 3600); // değeri 1 saatliğine kaydeder +$psrCache->set('key', 'value', 3600); // değeri 1 saat saklar $value = $psrCache->get('key', 'default'); ``` -Adaptör, `getMultiple()`, `setMultiple()` ve `deleteMultiple()` dahil olmak üzere PSR-16'da tanımlanan tüm yöntemleri destekler. +Adaptör, `getMultiple()`, `setMultiple()` ve `deleteMultiple()` dahil PSR-16'da tanımlı tüm metotları destekler. -Çıktıyı Önbelleğe Alma -====================== +Çıktı Önbellekleme +================== -Çıktıyı yakalamak ve önbelleğe almak çok zarif bir şekilde yapılabilir: +Çıktı çok zarif biçimde yakalanıp önbelleğe alınabilir: ```php if ($capture = $cache->capture($key)) { - echo ... // verileri yazdırıyoruz + // echo ... bir miktar veri yazdırılıyor - $capture->end(); // çıktıyı önbelleğe kaydediyoruz + $capture->end(); // çıktıyı önbelleğe kaydet } ``` -Çıktı zaten önbellekteyse, `capture()` yöntemi onu yazdırır ve `null` döndürür, bu nedenle koşul yürütülmez. Aksi takdirde, çıktıyı yakalamaya başlar ve sonunda yazdırılan verileri önbelleğe kaydettiğimiz `$capture` nesnesini döndürür. +Çı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] -Sürüm 3.0'da yöntemin adı `$cache->start()` idi. +3.0 sürümünde bu metodun adı `$cache->start()` idi. -Latte'de Önbelleğe Alma -======================= +Latte'de Önbellekleme +===================== -[Latte|latte:] şablonlarında önbelleğe alma çok kolaydır, şablonun bir bölümünü `{cache}...{/cache}` etiketleriyle sarmak yeterlidir. Kaynak şablon değiştiğinde (önbellek bloğu içindeki dahil edilen şablonlar dahil) önbellek otomatik olarak geçersiz kılınır. `{cache}` etiketleri iç içe yerleştirilebilir ve iç içe geçmiş bir blok geçersiz kılındığında (örneğin bir etiketle), üst blok da geçersiz kılınır. +[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. -Etikette, önbelleğin bağlanacağı anahtarları (burada `$id` değişkeni) belirtebilir ve sona erme süresini ve [geçersizleştirme etiketlerini |#Etiketlerle Geçersizleştirme] ayarlayabilirsiniz. +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]} @@ -318,9 +333,9 @@ Etikette, önbelleğin bağlanacağı anahtarları (burada `$id` değişkeni) be {/cache} ``` -Tüm öğeler isteğe bağlıdır, bu nedenle ne sona erme süresini ne de etiketleri, hatta anahtarları bile belirtmemiz gerekmez. +Tüm bu parametreler isteğe bağlıdır, dolayısıyla süre dolmasını, etiketleri, hatta anahtarları belirtmeniz gerekmez. -Önbellek kullanımı ayrıca `if` kullanılarak koşullandırılabilir - içerik yalnızca koşul karşılanırsa önbelleğe alınır: +Ö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()} @@ -329,23 +344,23 @@ Tüm öğeler isteğe bağlıdır, bu nedenle ne sona erme süresini ne de etike ``` -Depolama -======== +Depolar +======= -Depolama, verilerin fiziksel olarak depolandığı yeri temsil eden bir nesnedir. Bir veritabanı, Memcached sunucusu veya en erişilebilir depolama alanı olan diskteki dosyaları kullanabiliriz. +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. -|----------------- -| Depolama | Açıklama -|----------------- -| [#FileStorage] | diske dosyalara kaydeden varsayılan depolama -| [#MemcachedStorage] | `Memcached` sunucusunu kullanır -| [#MemoryStorage] | veriler geçici olarak bellekte tutulur -| [#SQLiteStorage] | veriler SQLite veritabanına kaydedilir -| [#DevNullStorage] | veriler kaydedilmez, test için uygundur +|---------------------- +| 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. -Depolama nesnesine, `Nette\Caching\Storage` türüyle [dependency injection |dependency-injection:passing-dependencies] kullanarak geçirmemizi isteyerek erişirsiniz. Nette, varsayılan depolama olarak verileri [geçici dosyalar |application:bootstrapping#Geçici Dosyalar] dizinindeki `cache` alt dizinine kaydeden bir FileStorage nesnesi sağlar. +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. -Depolamayı yapılandırmada değiştirebilirsiniz: +Varsayılan depoyu yapılandırmada değiştirebilirsiniz: ```neon services: @@ -356,14 +371,14 @@ services: FileStorage ----------- -Önbelleği diskteki dosyalara yazar. `Nette\Caching\Storages\FileStorage` depolama alanı, performans için çok iyi optimize edilmiştir ve özellikle işlemlerin tam atomikliğini sağlar. Bu ne anlama geliyor? Önbelleği kullanırken, başka bir iş parçacığı tarafından henüz tamamen yazılmamış bir dosyayı okumanız veya birinin onu "ellerinizin altından" silmesi mümkün değildir. Bu nedenle önbellek kullanımı tamamen güvenlidir. +Ö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 depolama alanı ayrıca, önbellek silindiğinde veya henüz ısınmadığında (yani oluşturulmadığında) CPU kullanımında aşırı artışı önleyen önemli bir yerleşik işleve sahiptir. Bu, "önbellek izdihamı":https://en.wikipedia.org/wiki/Cache_stampede önlemesidir. Bazen, aynı anda daha fazla sayıda eşzamanlı istek, önbellekten aynı şeyi (örneğin pahalı bir SQL sorgusunun sonucu) ister ve önbellekte olmadığı için tüm işlemler aynı SQL sorgusunu yürütmeye başlar. Yük böylece katlanır ve hatta hiçbir iş parçacığının zaman sınırında yanıt verememesi, önbelleğin oluşturulmaması ve uygulamanın çökmesi bile olabilir. Neyse ki, Nette'deki önbellek, bir öğe için birden fazla eşzamanlı istek olduğunda, onu yalnızca ilk iş parçacığının oluşturduğu, diğerlerinin beklediği ve ardından oluşturulan sonucu kullandığı şekilde çalışır. +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: +`FileStorage` oluşturma örneği: ```php -// depolama alanı diskteki '/path/to/temp' dizini olacak +// depo, diskteki '/path/to/temp' dizini olacak $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); ``` @@ -371,10 +386,10 @@ $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); MemcachedStorage ---------------- -[Memcached|https://memcached.org] sunucusu, adaptörü `Nette\Caching\Storages\MemcachedStorage` olan yüksek performanslı bir dağıtılmış bellek depolama sistemidir. Yapılandırmada, standart 11211'den farklıysa IP adresini ve bağlantı noktasını belirtiriz. +[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] -PHP `memcached` uzantısı gerektirir. +`memcached` PHP uzantısını gerektirir. ```neon services: @@ -385,16 +400,16 @@ services: MemoryStorage ------------- -`Nette\Caching\Storages\MemoryStorage`, verileri bir PHP dizisinde saklayan ve bu nedenle istek sona erdiğinde kaybolan bir depolama alanıdır. +`Nette\Caching\Storages\MemoryStorage`, veriyi bir PHP dizisinin içinde tutan bir depodur. Dolayısıyla istek bittiğinde veri yiter. SQLiteStorage ------------- -SQLite veritabanı ve `Nette\Caching\Storages\SQLiteStorage` adaptörü, önbelleği diskteki tek bir dosyaya kaydetmenin bir yolunu sunar. Yapılandırmada bu dosyanın yolunu belirtiriz. +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] -PHP `pdo` ve `pdo_sqlite` uzantılarını gerektirir. +`pdo` ve `pdo_sqlite` PHP uzantılarını gerektirir. ```neon services: @@ -405,13 +420,13 @@ services: DevNullStorage -------------- -Depolamanın özel bir uygulaması, aslında verileri hiç saklamayan `Nette\Caching\Storages\DevNullStorage`'dır. Bu nedenle, önbelleğin etkisini ortadan kaldırmak istediğimizde test için uygundur. +Ö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 Önbellek Kullanımı +Kodda Önbelleği Kullanma ======================== -Kodda önbellek kullanırken, bunu yapmanın iki yolu vardır. Birincisi, [dependency injection |dependency-injection:passing-dependencies] kullanarak depolamayı geçirmemizi istemek ve bir `Cache` nesnesi oluşturmaktır: +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; @@ -427,7 +442,7 @@ class ClassOne } ``` -İkinci seçenek, doğrudan bir `Cache` nesnesi geçirmemizi istemektir: +İkinci seçenek, doğrudan `Cache` nesnesini istemektir: ```php class ClassTwo @@ -439,7 +454,7 @@ class ClassTwo } ``` -`Cache` nesnesi daha sonra doğrudan yapılandırmada şu şekilde oluşturulur: +`Cache` nesnesi o zaman yapılandırmada, örneğin şöyle tanımlanmalıdır: ```neon services: @@ -450,9 +465,9 @@ services: Journal ======= -Nette, etiketleri ve öncelikleri journal adı verilen bir yerde saklar. Standart olarak bunun için SQLite ve `journal.s3db` dosyası kullanılır ve **PHP `pdo` ve `pdo_sqlite` uzantıları gereklidir.** +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'ı yapılandırmada değiştirebilirsiniz: +Journal gerçekleştirimini yapılandırmada değiştirebilirsiniz: ```neon services: @@ -463,22 +478,25 @@ services: DI Servisleri ============= -Bu servisler DI konteynerine eklenir: +DI container'a şu servisler eklenir: -| Ad | Tür | Açıklama +| Ad | Tip | Açıklama |---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | depolama +| `cache.journal` | [api:Nette\Caching\Storages\Journal] | Önbellek journal deposu +| `cache.storage` | [api:Nette\Caching\Storage] | Birincil önbellek deposu -Önbelleği Devre Dışı Bırakma -============================ +Önbelleği Kapatma +================= -Uygulamada önbelleği devre dışı bırakmanın bir yolu, depolama olarak [#DevNullStorage] ayarlamaktır: +Uygulamanızda önbelleklemeyi kapatmanın bir yolu, depo arka ucunu [#DevNullStorage] olarak ayarlamaktır: ```neon services: cache.storage: Nette\Caching\Storages\DevNullStorage ``` -Bu ayarın Latte'deki şablonların veya DI konteynerinin önbelleğe alınması üzerinde bir etkisi yoktur, çünkü bu kütüphaneler nette/caching servislerini kullanmaz ve kendi önbelleklerini yönetirler. Ayrıca, geliştirme modunda [önbelleklerini devre dışı bırakmaya gerek yoktur |nette:troubleshooting#Geliştirme Sırasında Önbellek Nasıl Kapatılır]. +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 new file mode 100644 index 0000000000..c1ed2c3be6 --- /dev/null +++ b/caching/tr/@left-menu.texy @@ -0,0 +1,13 @@ +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 index e5c5cea355..8dfe82f311 100644 --- a/caching/tr/@meta.texy +++ b/caching/tr/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokümantasyonu}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/tr/upgrading.texy b/caching/tr/upgrading.texy new file mode 100644 index 0000000000..b6ae5cb6db --- /dev/null +++ b/caching/tr/upgrading.texy @@ -0,0 +1,20 @@ +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/caching/uk/@home.texy b/caching/uk/@home.texy deleted file mode 100644 index 14e19a212f..0000000000 --- a/caching/uk/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Кеш прискорить ваш застосунок, зберігаючи дані, отримані з великими витратами, для майбутнього використання. Ми покажемо: - -- як використовувати кеш -- як змінити сховище -- як правильно інвалідувати кеш - -</div> - -Використання кешу в Nette дуже просте, водночас воно покриває навіть дуже складні потреби. Він розроблений для продуктивності та 100% стійкості. В основі ви знайдете адаптери для найпоширеніших бекенд-сховищ. Дозволяє інвалідацію на основі тегів, часову експірацію, має захист від cache stampede тощо. - - -Встановлення -============ - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Базове використання -=================== - -Центром роботи з кешем є об'єкт [api:Nette\Caching\Cache]. Створимо його екземпляр і передамо конструктору так зване сховище. Це об'єкт, що представляє місце, де дані будуть фізично зберігатися (база даних, Memcached, файли на диску, ...). До сховища можна отримати доступ, попросивши передати його за допомогою [dependency injection |dependency-injection:passing-dependencies] з типом `Nette\Caching\Storage`. Все важливе ви дізнаєтеся в [розділі Сховища |#Сховища]. - -.[warning] -У версії 3.0 інтерфейс ще мав префікс `I`, тому назва була `Nette\Caching\IStorage`. Також константи класу `Cache` були написані великими літерами, наприклад, `Cache::EXPIRE` замість `Cache::Expire`. - -Для наступних прикладів припустимо, що ми створили псевдонім `Cache` і маємо сховище у змінній `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // екземпляр Nette\Caching\Storage -``` - -Кеш — це, по суті, *key–value store*, тобто ми читаємо та записуємо дані за ключами так само, як у асоціативних масивах. Застосунки складаються з низки незалежних частин, і якщо всі вони будуть використовувати одне сховище (уявіть собі один каталог на диску), рано чи пізно виникне колізія ключів. Nette Framework вирішує цю проблему, розділяючи весь простір на простори імен (підкаталоги). Кожна частина програми використовує свій простір з унікальною назвою, і колізій більше не виникає. - -Назву простору вказуємо як другий параметр конструктора класу Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Тепер за допомогою об'єкта `$cache` ми можемо читати з кешу та записувати в нього. Для обох дій служить метод `load()`. Першим аргументом є ключ, а другим — PHP callback, який викликається, якщо ключ не знайдено в кеші. Callback генерує значення, повертає його, і воно зберігається в кеші: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // складне обчислення - return $computedValue; -}); -``` - -Якщо другий параметр не вказано `$value = $cache->load($key)`, повернеться `null`, якщо елемент відсутній у кеші. - -.[tip] -Чудово те, що в кеш можна зберігати будь-які серіалізовані структури, не обов'язково лише рядки. Те саме стосується навіть ключів. - -Елемент з кешу видаляємо методом `remove()`: - -```php -$cache->remove($key); -``` - -Зберегти елемент у кеші можна також методом `$cache->save($key, $value, array $dependencies = [])`. Однак перевага надається вищезгаданому способу за допомогою `load()`. - - -Мемоізація -========== - -Мемоізація означає кешування результату виклику функції або методу, щоб ви могли використовувати його наступного разу без повторного обчислення того самого. - -Мемоізовано можна викликати методи та функції за допомогою `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Функція `gethostbyaddr()` таким чином викликається для кожного параметра `$ip` лише один раз, а наступного разу повертається значення з кешу. - -Також можна створити мемоізовану обгортку над методом або функцією, яку можна викликати пізніше: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // обчислює вперше -$result = $memoizedFactorial(5); // вдруге з кешу -``` - - -Експірація та інвалідація -========================= - -При зберіганні даних у кеші необхідно вирішувати питання, коли раніше збережені дані стануть недійсними. Nette Framework пропонує механізм для обмеження терміну дії даних або їх керованого видалення (в термінології фреймворку — «інвалідації»). - -Термін дії даних встановлюється в момент збереження за допомогою третього параметра методу `save()`, наприклад: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Або за допомогою параметра `$dependencies`, переданого за посиланням до callback-функції методу `load()`, наприклад: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Або за допомогою 3-го параметра в методі `load()`, наприклад: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -У наступних прикладах ми будемо припускати другий варіант і, отже, існування змінної `$dependencies`. - - -Експірація ----------- - -Найпростіша експірація — це часовий ліміт. Таким чином ми зберігаємо дані в кеші з терміном дії 20 хвилин: - -```php -// приймає також кількість секунд або UNIX timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Якщо ми хочемо продовжити термін дії при кожному читанні, це можна зробити наступним чином, але будьте обережні, накладні витрати кешу при цьому зростуть: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Зручною є можливість дозволити даним закінчитися в момент зміни файлу або одного з кількох файлів. Це можна використовувати, наприклад, при зберіганні в кеші даних, отриманих в результаті обробки цих файлів. Використовуйте абсолютні шляхи. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// або -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Ми можемо дозволити елементу в кеші закінчитися в момент, коли закінчується інший елемент (або один з кількох інших). Це можна використовувати, наприклад, коли ми зберігаємо в кеші цілу HTML-сторінку, а під іншими ключами — її фрагменти. Як тільки фрагмент змінюється, вся сторінка інвалідується. Якщо фрагменти збережені під ключами, наприклад, `frag1` та `frag2`, використовуємо: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Експірацію можна контролювати також за допомогою власних функцій або статичних методів, які завжди при читанні вирішують, чи є елемент ще дійсним. Таким чином, наприклад, ми можемо дозволити елементу закінчитися щоразу, коли змінюється версія PHP. Створимо функцію, яка порівнює поточну версію з параметром, і при збереженні додамо серед залежностей масив у форматі `[назва функції, ...аргументи]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // закінчити, коли checkPhpVersion(...) === false -]; -``` - -Всі критерії, звичайно, можна комбінувати. Кеш тоді закінчується, коли принаймні один критерій не виконується. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Інвалідація за допомогою тегів ------------------------------- - -Дуже корисним інструментом інвалідації є так звані теги. Кожному елементу в кеші ми можемо призначити список тегів, які є довільними рядками. Наприклад, маємо HTML-сторінку зі статтею та коментарями, яку будемо кешувати. При збереженні вказуємо теги: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Перейдемо до адміністративної частини. Тут знайдемо форму для редагування статті. Разом зі збереженням статті в базу даних викличемо команду `clean()`, яка видалить з кешу елементи за тегом: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Так само в місці додавання нового коментаря (або редагування коментаря) не забудемо інвалідувати відповідний тег: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Чого ми цим досягли? Що наш HTML-кеш буде інвалідуватися (видалятися), коли змінюється стаття або коментарі. При редагуванні статті з ID = 10 відбувається примусова інвалідація тегу `article/10`, і HTML-сторінка, яка несе цей тег, видаляється з кешу. Те саме відбувається при додаванні нового коментаря до відповідної статті. - -.[note] -Теги вимагають так званого [#Journal]. - - -Інвалідація за допомогою пріоритету ------------------------------------ - -Окремим елементам у кеші ми можемо встановити пріоритет, за допомогою якого їх можна буде видаляти, наприклад, коли кеш перевищить певний розмір: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Видалимо всі елементи з пріоритетом, рівним або меншим за 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Пріоритети вимагають так званого [#Journal]. - - -Видалення кешу --------------- - -Параметр `Cache::All` видаляє все: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Масове читання -============== - -Для масового читання та запису в кеш служить метод `bulkLoad()`, якому ми передаємо масив ключів і отримуємо масив значень: - -```php -$values = $cache->bulkLoad($keys); -``` - -Метод `bulkLoad()` працює подібно до `load()` і з другим параметром callback, якому передається ключ генерованого елемента: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // складне обчислення - return $computedValue; -}); -``` - - -Використання з PSR-16 .{data-version:3.3.1} -=========================================== - -Для використання Nette Cache з інтерфейсом PSR-16 ви можете скористатися адаптером `PsrCacheAdapter`. Він дозволяє безшовну інтеграцію між Nette Cache та будь-яким кодом або бібліотекою, яка очікує PSR-16 сумісний кеш. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Тепер ви можете використовувати `$psrCache` як PSR-16 кеш: - -```php -$psrCache->set('key', 'value', 3600); // зберігає значення на 1 годину -$value = $psrCache->get('key', 'default'); -``` - -Адаптер підтримує всі методи, визначені в PSR-16, включаючи `getMultiple()`, `setMultiple()` та `deleteMultiple()`. - - -Кешування виводу -================ - -Дуже елегантно можна перехоплювати та кешувати вивід: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // виводимо дані - - $capture->end(); // зберігаємо вивід у кеш -} -``` - -У випадку, якщо вивід вже збережено в кеші, метод `capture()` виведе його і поверне `null`, отже умова не виконається. В іншому випадку він почне перехоплювати вивід і поверне об'єкт `$capture`, за допомогою якого ми врешті-решт збережемо виведені дані в кеш. - -.[note] -У версії 3.0 метод називався `$cache->start()`. - - -Кешування в Latte -================= - -Кешування в шаблонах [Latte|latte:] дуже просте, достатньо частину шаблону обернути тегами `{cache}...{/cache}`. Кеш автоматично інвалідується в момент, коли змінюється вихідний шаблон (включаючи можливі включені шаблони всередині блоку кешу). Теги `{cache}` можна вкладати один в одного, і коли вкладений блок стає недійсним (наприклад, за допомогою тегу), батьківський блок також стає недійсним. - -У тегу можна вказати ключі, до яких буде прив'язаний кеш (тут змінна `$id`), і встановити термін дії та [теги для інвалідації |#Інвалідація за допомогою тегів]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Усі параметри є необов'язковими, тому ми не повинні вказувати ні термін дії, ні теги, ні навіть ключі. - -Використання кешу також можна обумовити за допомогою `if` - вміст тоді буде кешуватися лише за умови виконання умови: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Сховища -======= - -Сховище — це об'єкт, що представляє місце, де дані фізично зберігаються. Ми можемо використовувати базу даних, сервер Memcached або найдоступніше сховище — файли на диску. - -|----------------- -| Сховище | Опис -|----------------- -| [#FileStorage] | сховище за замовчуванням зі збереженням у файли на диску -| [#MemcachedStorage] | використовує сервер `Memcached` -| [#MemoryStorage] | дані тимчасово зберігаються в пам'яті -| [#SQLiteStorage] | дані зберігаються в базі даних SQLite -| [#DevNullStorage] | дані не зберігаються, підходить для тестування - -До об'єкта сховища можна отримати доступ, попросивши передати його за допомогою [dependency injection |dependency-injection:passing-dependencies] з типом `Nette\Caching\Storage`. Як сховище за замовчуванням Nette надає об'єкт FileStorage, що зберігає дані в підкаталозі `cache` в каталозі для [тимчасових файлів |application:bootstrapping#Тимчасові файли]. - -Змінити сховище можна в конфігурації: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Записує кеш у файли на диску. Сховище `Nette\Caching\Storages\FileStorage` дуже добре оптимізоване для продуктивності і, перш за все, забезпечує повну атомарність операцій. Що це означає? Що при використанні кешу не може статися так, що ми прочитаємо файл, який ще не повністю записаний іншим потоком, або що хтось його "під руками" видалить. Використання кешу, таким чином, є абсолютно безпечним. - -Це сховище також має вбудовану важливу функцію, яка запобігає екстремальному зростанню використання ЦП у момент, коли кеш видаляється або ще не прогрітий (тобто створений). Це запобігання "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Стається так, що в один момент збігається велика кількість одночасних запитів, які хочуть отримати з кешу одну й ту саму річ (наприклад, результат дорогого SQL-запиту), і оскільки в кеші її немає, всі процеси починають виконувати той самий SQL-запит. Навантаження таким чином множиться, і може навіть статися, що жоден потік не встигне відповісти в часовому ліміті, кеш не створиться, і застосунок звалиться. На щастя, кеш у Nette працює так, що при кількох одночасних запитах на один елемент його генерує лише перший потік, інші чекають і потім використовують згенерований результат. - -Приклад створення FileStorage: - -```php -// сховищем буде каталог '/path/to/temp' на диску -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Сервер [Memcached|https://memcached.org] — це високопродуктивна система зберігання в розподіленій пам'яті, адаптером якої є `Nette\Caching\Storages\MemcachedStorage`. У конфігурації вказуємо IP-адресу та порт, якщо він відрізняється від стандартного 11211. - -.[caution] -Вимагає PHP-розширення `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` — це сховище, яке зберігає дані в масиві PHP, і тому вони втрачаються після завершення запиту. - - -SQLiteStorage -------------- - -База даних SQLite та адаптер `Nette\Caching\Storages\SQLiteStorage` пропонують спосіб зберігання кешу в одному файлі на диску. У конфігурації вказуємо шлях до цього файлу. - -.[caution] -Вимагає PHP-розширень `pdo` та `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Спеціальною реалізацією сховища є `Nette\Caching\Storages\DevNullStorage`, яке насправді взагалі не зберігає дані. Тому воно підходить для тестування, коли ми хочемо усунути вплив кешу. - - -Використання кешу в коді -======================== - -При використанні кешу в коді є два способи це зробити. Перший полягає в тому, що ми просимо передати сховище за допомогою [dependency injection |dependency-injection:passing-dependencies] і створюємо об'єкт `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Другий варіант — ми просимо передати об'єкт `Cache` безпосередньо: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Об'єкт `Cache` потім створюється безпосередньо в конфігурації таким чином: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette зберігає теги та пріоритети у так званому журналі. Стандартно для цього використовується SQLite та файл `journal.s3db`, і **вимагаються PHP-розширення `pdo` та `pdo_sqlite`.** - -Змінити журнал можна в конфігурації: - -```neon -services: - cache.journal: MyJournal -``` - - -Сервіси DI -========== - -Ці сервіси додаються до DI-контейнера: - -| Назва | Тип | Опис -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | журнал -| `cache.storage` | [api:Nette\Caching\Storage] | сховище - - -Вимкнення кешу -============== - -Одним із способів вимкнути кеш у застосунку є встановлення [#DevNullStorage] як сховища: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Це налаштування не впливає на кешування шаблонів у Latte або DI-контейнера, оскільки ці бібліотеки не використовують сервіси nette/caching і керують своїм кешем самостійно. Їхній кеш, до речі, [не потрібно |nette:troubleshooting#Як вимкнути кеш під час розробки] вимикати в режимі розробки. diff --git a/caching/uk/@meta.texy b/caching/uk/@meta.texy deleted file mode 100644 index 083a8ab9f7..0000000000 --- a/caching/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документація Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/code-checker/bg/@home.texy b/code-checker/bg/@home.texy deleted file mode 100644 index ab284bfe7e..0000000000 --- a/code-checker/bg/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Инструментът [Code Checker |https://github.com/nette/code-checker] проверява и евентуално коригира някои от формалните грешки във вашия изходен код. - - -Инсталация -========== - -Code Checker не трябва да се добавя към зависимостите, а да се инсталира като проект. - -```shell -composer create-project nette/code-checker -``` - -Или го инсталирайте глобално с помощта на: - -```shell -composer global require nette/code-checker -``` - -и се уверете, че вашата глобална директория `vendor/bin` е в [променливата на средата $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Употреба -======== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Без параметри проверява текущата директория в режим само за четене, с параметъра `-f` коригира файловете. - -Преди да се запознаете с него, определено първо архивирайте файловете си. - -За по-лесно стартиране можем да създадем файл `code.bat`: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -Какво прави всичко това? -======================== - -- премахва [BOM |nette:glossary#BOM] -- проверява валидността на [Latte |latte:] шаблони -- проверява валидността на файлове `.neon`, `.php` и `.json` -- проверява за наличието на [контролни знаци |nette:glossary#Контролни знаци] -- проверява дали файлът е кодиран в UTF-8 -- проверява неправилно записани `/* @anotace */` (липсва звездичка) -- премахва завършващия `?>` при PHP файлове -- премахва десните интервали и излишните редове в края на файла -- нормализира разделителите на редове до системните (ако посочите опцията `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/cs/@home.texy b/code-checker/cs/@home.texy deleted file mode 100644 index a4d62f10d6..0000000000 --- a/code-checker/cs/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Nástroj [Code Checker |https://github.com/nette/code-checker] zkontroluje a případně opraví některé z formálních chyb ve vašich zdrojových kódech. - - -Instalace -========= - -Code Checker byste neměli přidávat do závislostí, ale instalovat jako projekt. - -```shell -composer create-project nette/code-checker -``` - -Nebo jej nainstalujte globálně pomocí: - -```shell -composer global require nette/code-checker -``` - -a ujistěte se, že váš globální adresář `vendor/bin` je v [proměnné prostředí $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Použití -======= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Bez parametrů zkontroluje aktuální adresář v read-only režimu, s parametrem `-f` opravuje soubory. - -Než se s ním seznámíte, určitě si soubory nejdřív zazálohujte. - -Pro snadnější spouštění si můžeme vytvořit soubor `code.bat`: - -```shell -php cesta_k_Nette_tools\Code-Checker\code-checker %* -``` - - -Co všechno dělá? -================ - -- odstraňuje [BOM |nette:glossary#BOM] -- kontroluje validitu [Latte |latte:] šablon -- kontroluje validitu souborů `.neon`, `.php` a `.json` -- kontroluje výskyt [kontrolních znaků |nette:glossary#Kontrolní znaky] -- kontroluje, zda je soubor kódován v UTF-8 -- kontroluje chybně zapsané `/* @anotace */` (chybí hvězdička) -- odstraňuje ukončovací `?>` u PHP souborů -- odstraňuje pravostranné mezery a zbytečné řádky na konci souboru -- normalizuje oddělovače řádků na systémové (pokud uvedete volbu `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/de/@home.texy b/code-checker/de/@home.texy deleted file mode 100644 index 598b407465..0000000000 --- a/code-checker/de/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Das Werkzeug [Code Checker |https://github.com/nette/code-checker] überprüft und korrigiert gegebenenfalls einige formale Fehler in Ihrem Quellcode. - - -Installation -============ - -Code Checker sollten Sie nicht zu Ihren Abhängigkeiten hinzufügen, sondern als Projekt installieren. - -```shell -composer create-project nette/code-checker -``` - -Oder installieren Sie es global mit: - -```shell -composer global require nette/code-checker -``` - -und stellen Sie sicher, dass Ihr globales Verzeichnis `vendor/bin` in der [Umgebungsvariablen $PATH |https://getcomposer.org/doc/03-cli.md#global] enthalten ist. - - -Verwendung -========== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Zu scannender Ordner oder Datei (Standard: aktuelles Verzeichnis) - -i | --ignore <mask> Zu ignorierende Dateien - -f | --fix Korrigiert Dateien - -l | --eol Konvertiert Zeilenumbruchzeichen - --no-progress Keine Fortschrittspunkte anzeigen - --strict-types Prüft, ob die PHP 7.0-Direktive strict_types aktiviert ist -``` - -Ohne Parameter prüft es das aktuelle Verzeichnis im schreibgeschützten Modus, mit dem Parameter `-f` korrigiert es die Dateien. - -Bevor Sie sich damit vertraut machen, sichern Sie unbedingt zuerst Ihre Dateien. - -Für einen einfacheren Start können wir eine Datei `code.bat` erstellen: - -```shell -php pfad_zu_Nette_tools\Code-Checker\code-checker %* -``` - - -Was macht es alles? -=================== - -- entfernt das [BOM |nette:glossary#BOM] -- prüft die Gültigkeit von [Latte |latte:]-Templates -- prüft die Gültigkeit von `.neon`-, `.php`- und `.json`-Dateien -- prüft das Vorkommen von [Steuerzeichen |nette:glossary#Steuerzeichen] -- prüft, ob die Datei in UTF-8 kodiert ist -- prüft falsch geschriebene `/* @anotace */` (fehlendes Sternchen) -- entfernt das schließende `?>` bei PHP-Dateien -- entfernt Leerzeichen am Zeilenende und unnötige Leerzeilen am Dateiende -- normalisiert Zeilentrennzeichen auf Systemstandard (wenn Sie die Option `-l` angeben) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/el/@home.texy b/code-checker/el/@home.texy deleted file mode 100644 index de1f6401a0..0000000000 --- a/code-checker/el/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Το εργαλείο [Code Checker |https://github.com/nette/code-checker] ελέγχει και ενδεχομένως διορθώνει ορισμένα από τα τυπικά σφάλματα στους πηγαίους κώδικές σας. - - -Εγκατάσταση -=========== - -Δεν πρέπει να προσθέσετε το Code Checker στις εξαρτήσεις, αλλά να το εγκαταστήσετε ως έργο. - -```shell -composer create-project nette/code-checker -``` - -Ή εγκαταστήστε το καθολικά χρησιμοποιώντας: - -```shell -composer global require nette/code-checker -``` - -και βεβαιωθείτε ότι ο καθολικός σας κατάλογος `vendor/bin` βρίσκεται στη [μεταβλητή περιβάλλοντος $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Χρήση -===== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Χωρίς παραμέτρους ελέγχει τον τρέχοντα κατάλογο σε κατάσταση μόνο ανάγνωσης, με την παράμετρο `-f` διορθώνει τα αρχεία. - -Πριν εξοικειωθείτε μαζί του, φροντίστε να δημιουργήσετε αντίγραφα ασφαλείας των αρχείων σας πρώτα. - -Για ευκολότερη εκτέλεση, μπορούμε να δημιουργήσουμε ένα αρχείο `code.bat`: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -Τι κάνει; -========= - -- αφαιρεί το [BOM |nette:glossary#BOM] -- ελέγχει την εγκυρότητα των templates [Latte |latte:] -- ελέγχει την εγκυρότητα των αρχείων `.neon`, `.php` και `.json` -- ελέγχει την παρουσία [χαρακτήρων ελέγχου |nette:glossary#Control characters] -- ελέγχει εάν το αρχείο είναι κωδικοποιημένο σε UTF-8 -- ελέγχει για λανθασμένα γραμμένα `/* @anotace */` (λείπει ο αστερίσκος) -- αφαιρεί το τελικό `?>` από τα αρχεία PHP -- αφαιρεί τα δεξιά κενά και τις περιττές γραμμές στο τέλος του αρχείου -- κανονικοποιεί τους διαχωριστές γραμμών σε συστήματος (εάν δώσετε την επιλογή `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/en/@home.texy b/code-checker/en/@home.texy deleted file mode 100644 index 2bbaba1c33..0000000000 --- a/code-checker/en/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -The tool called [Code Checker |https://github.com/nette/code-checker] checks and optionally repairs some of the formal errors in your source code. - - -Installation -============ - -Code Checker should be installed as a project, not added as a dependency. - -```shell -composer create-project nette/code-checker -``` - -Or install it globally via: - -```shell -composer global require nette/code-checker -``` - -and make sure your global vendor binaries directory is in [your `$PATH` environment variable|https://getcomposer.org/doc/03-cli.md#global]. - - -Usage -===== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Without parameters, it checks the current working directory in read-only mode; with the `-f` parameter, it fixes files. - -Before you get familiar with the tool, be sure to back up your files first. - -You can create a batch file, e.g., `code.bat`, for easier execution of Code Checker under Windows: - -```shell -php path_to\Nette_tools\Code-Checker\code-checker %* -``` - - -What Does Code Checker Do? -========================== - -- removes [BOM |nette:glossary#BOM] -- checks the validity of [Latte |latte:] templates -- checks the validity of `.neon`, `.php`, and `.json` files -- checks for [control characters |nette:glossary#Control Characters] -- checks whether the file is encoded in UTF-8 -- checks for misspelled `/* @annotations */` (missing second asterisk) -- removes PHP ending tags `?>` in PHP files -- removes trailing whitespace and unnecessary blank lines from the end of a file -- normalizes line endings to the system default (with the `-l` parameter) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/es/@home.texy b/code-checker/es/@home.texy deleted file mode 100644 index c6770a3010..0000000000 --- a/code-checker/es/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -La herramienta [Code Checker |https://github.com/nette/code-checker] comprueba y, opcionalmente, corrige algunos de los errores formales en sus códigos fuente. - - -Instalación -=========== - -No debe agregar Code Checker a las dependencias, sino instalarlo como un proyecto. - -```shell -composer create-project nette/code-checker -``` - -O instálelo globalmente usando: - -```shell -composer global require nette/code-checker -``` - -y asegúrese de que su directorio global `vendor/bin` esté en la [variable de entorno $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Uso -=== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Sin parámetros, comprueba el directorio actual en modo de solo lectura, con el parámetro `-f` corrige los archivos. - -Antes de familiarizarse con él, asegúrese de hacer una copia de seguridad de sus archivos primero. - -Para facilitar la ejecución, podemos crear un archivo `code.bat`: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -¿Qué hace todo esto? -==================== - -- elimina el [BOM |nette:glossary#BOM] -- comprueba la validez de las plantillas [Latte |latte:] -- comprueba la validez de los archivos `.neon`, `.php` y `.json` -- comprueba la presencia de [caracteres de control |nette:glossary#Caracteres de control] -- comprueba si el archivo está codificado en UTF-8 -- comprueba las `/* @anotaciones */` escritas incorrectamente (falta el asterisco) -- elimina el cierre `?>` de los archivos PHP -- elimina los espacios finales y las líneas innecesarias al final del archivo -- normaliza los separadores de línea a los del sistema (si especifica la opción `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/fr/@home.texy b/code-checker/fr/@home.texy deleted file mode 100644 index 13c595ee8c..0000000000 --- a/code-checker/fr/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -L'outil [Code Checker |https://github.com/nette/code-checker] vérifie et corrige éventuellement certaines erreurs formelles dans vos codes sources. - - -Installation -============ - -Code Checker ne doit pas être ajouté aux dépendances, mais installé en tant que projet. - -```shell -composer create-project nette/code-checker -``` - -Ou installez-le globalement en utilisant : - -```shell -composer global require nette/code-checker -``` - -et assurez-vous que votre répertoire global `vendor/bin` est dans [la variable d'environnement $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Utilisation -=========== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Dossier ou fichier à analyser (par défaut : répertoire courant) - -i | --ignore <mask> Fichiers à ignorer - -f | --fix Corrige les fichiers - -l | --eol Convertit les caractères de nouvelle ligne - --no-progress N'affiche pas les points de progression - --strict-types Vérifie si la directive PHP 7.0 strict_types est activée -``` - -Sans paramètres, il vérifie le répertoire actuel en mode lecture seule, avec le paramètre `-f`, il corrige les fichiers. - -Avant de vous familiariser avec lui, assurez-vous de sauvegarder d'abord vos fichiers. - -Pour faciliter l'exécution, nous pouvons créer un fichier `code.bat` : - -```shell -php chemin_vers_Nette_tools\Code-Checker\code-checker %* -``` - - -Que fait-il ? -============= - -- supprime le [BOM |nette:glossary#BOM] -- vérifie la validité des templates [Latte |latte:] -- vérifie la validité des fichiers `.neon`, `.php` et `.json` -- vérifie la présence de [caractères de contrôle |nette:glossary#Caractères de contrôle] -- vérifie si le fichier est encodé en UTF-8 -- vérifie les `/* @annotations */` mal écrites (étoile manquante) -- supprime le `?>` de fin des fichiers PHP -- supprime les espaces de fin et les lignes vides inutiles à la fin du fichier -- normalise les séparateurs de lignes en séparateurs système (si vous spécifiez l'option `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/hu/@home.texy b/code-checker/hu/@home.texy deleted file mode 100644 index e676665b2f..0000000000 --- a/code-checker/hu/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -A [Code Checker |https://github.com/nette/code-checker] eszköz ellenőrzi és szükség esetén kijavítja a forráskódjaiban található néhány formai hibát. - - -Telepítés -========= - -A Code Checkert nem szabad a függőségekhez hozzáadni, hanem projektként kell telepíteni. - -```shell -composer create-project nette/code-checker -``` - -Vagy telepítse globálisan a következővel: - -```shell -composer global require nette/code-checker -``` - -és győződjön meg róla, hogy a globális `vendor/bin` könyvtára benne van a [$PATH környezeti változóban |https://getcomposer.org/doc/03-cli.md#global]. - - -Használat -========= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Szkennelendő mappa vagy fájl (alapértelmezett: aktuális könyvtár) - -i | --ignore <mask> Figyelmen kívül hagyandó fájlok - -f | --fix Javítja a fájlokat - -l | --eol Újsor karakterek konvertálása - --no-progress Ne jelenítse meg a folyamatjelző pontokat - --strict-types Ellenőrzi, hogy a PHP 7.0 strict_types direktíva engedélyezve van-e -``` - -Paraméterek nélkül az aktuális könyvtárat ellenőrzi read-only módban, a `-f` paraméterrel javítja a fájlokat. - -Mielőtt megismerkedne vele, mindenképpen készítsen biztonsági másolatot a fájlokról. - -A könnyebb indítás érdekében létrehozhatunk egy `code.bat` fájlt: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -Mit csinál pontosan? -==================== - -- eltávolítja a [BOM |nette:glossary#BOM]-ot -- ellenőrzi a [Latte |latte:] sablonok érvényességét -- ellenőrzi a `.neon`, `.php` és `.json` fájlok érvényességét -- ellenőrzi a [vezérlőkarakterek |nette:glossary#Vezérlő karakterek] előfordulását -- ellenőrzi, hogy a fájl UTF-8 kódolású-e -- ellenőrzi a hibásan írt `/* @anotace */` (hiányzik a csillag) -- eltávolítja a záró `?>` taget a PHP fájlokból -- eltávolítja a jobb oldali szóközöket és a felesleges sorokat a fájl végéről -- normalizálja a sorelválasztókat a rendszer alapértelmezettjére (ha megadja a `-l` opciót) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/it/@home.texy b/code-checker/it/@home.texy deleted file mode 100644 index 4529024b15..0000000000 --- a/code-checker/it/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Lo strumento [Code Checker |https://github.com/nette/code-checker] controlla e, se necessario, corregge alcuni degli errori formali nei vostri codici sorgente. - - -Installazione -============= - -Code Checker non dovrebbe essere aggiunto alle dipendenze, ma installato come progetto. - -```shell -composer create-project nette/code-checker -``` - -Oppure installatelo globalmente tramite: - -```shell -composer global require nette/code-checker -``` - -e assicuratevi che la vostra directory globale `vendor/bin` sia nella [variabile d'ambiente $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Utilizzo -======== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Senza parametri, controlla la directory corrente in modalità di sola lettura, con il parametro `-f` corregge i file. - -Prima di familiarizzare con esso, assicuratevi di eseguire il backup dei file. - -Per un avvio più semplice, possiamo creare un file `code.bat`: - -```shell -php percorso_a_Nette_tools\Code-Checker\code-checker %* -``` - - -Cosa fa? -======== - -- rimuove il [BOM |nette:glossary#BOM] -- controlla la validità dei template [Latte |latte:] -- controlla la validità dei file `.neon`, `.php` e `.json` -- controlla la presenza di [caratteri di controllo |nette:glossary#Caratteri di controllo] -- controlla se il file è codificato in UTF-8 -- controlla le annotazioni `/* @anotace */` scritte male (manca l'asterisco) -- rimuove i tag di chiusura `?>` dai file PHP -- rimuove gli spazi finali e le righe vuote alla fine del file -- normalizza i separatori di riga a quelli di sistema (se si specifica l'opzione `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/ja/@home.texy b/code-checker/ja/@home.texy deleted file mode 100644 index b01ce40115..0000000000 --- a/code-checker/ja/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -[Code Checker |https://github.com/nette/code-checker]ツールは、ソースコード内のいくつかの形式的なエラーをチェックし、必要に応じて修正します。 - - -インストール -====== - -Code Checkerは依存関係に追加するのではなく、プロジェクトとしてインストールする必要があります。 - -```shell -composer create-project nette/code-checker -``` - -または、次のようにグローバルにインストールします: - -```shell -composer global require nette/code-checker -``` - -そして、グローバルな `vendor/bin` ディレクトリが[$PATH 環境変数 |https://getcomposer.org/doc/03-cli.md#global]に含まれていることを確認してください。 - - -使用法 -=== - -``` -Usage: php code-checker [options] - -Options: - -d <path> スキャンするフォルダまたはファイル(デフォルト:現在のディレクトリ) - -i | --ignore <mask> 無視するファイル - -f | --fix ファイルを修正 - -l | --eol 改行文字を変換 - --no-progress 進捗ドットを表示しない - --strict-types PHP 7.0 ディレクティブ strict_types が有効かどうかをチェック -``` - -パラメータなしでは、現在のディレクトリを読み取り専用モードでチェックします。パラメータ `-f` を指定すると、ファイルを修正します。 - -慣れる前に、必ずファイルをバックアップしてください。 - -簡単に実行するために、`code.bat` ファイルを作成できます: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -何をするのですか? -========= - -- [BOM |nette:glossary#BOM]を削除します -- [Latte |latte:] テンプレートの有効性をチェックします -- `.neon`、`.php`、`.json` ファイルの有効性をチェックします -- [制御文字 |nette:glossary#制御文字]の出現をチェックします -- ファイルがUTF-8でエンコードされているかチェックします -- 誤って記述された `/* @anotation */`(アスタリスクが欠けている)をチェックします -- PHP ファイルの末尾の `?>` を削除します -- ファイルの末尾の右側のスペースと不要な行を削除します -- (オプション `-l` を指定した場合)改行文字をシステム標準に正規化します - -{{leftbar: www:@menu-common}} diff --git a/code-checker/meta.json b/code-checker/meta.json deleted file mode 100644 index 7bfea0012f..0000000000 --- a/code-checker/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "3.x", - "repo": "nette/code-checker", - "composer": "nette/code-checker" -} diff --git a/code-checker/pl/@home.texy b/code-checker/pl/@home.texy deleted file mode 100644 index 23a99c3807..0000000000 --- a/code-checker/pl/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Narzędzie [Code Checker |https://github.com/nette/code-checker] sprawdza i ewentualnie naprawia niektóre formalne błędy w Twoich kodach źródłowych. - - -Instalacja -========== - -Code Checker nie powinien być dodawany do zależności, ale instalowany jako projekt. - -```shell -composer create-project nette/code-checker -``` - -Lub zainstaluj go globalnie za pomocą: - -```shell -composer global require nette/code-checker -``` - -i upewnij się, że twój globalny katalog `vendor/bin` jest w [zmiennej środowiskowej $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Użycie -====== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Katalog lub plik do skanowania (domyślnie: bieżący katalog) - -i | --ignore <mask> Pliki do ignorowania - -f | --fix Naprawia pliki - -l | --eol Konwertuj znaki nowej linii - --no-progress Nie pokazuj kropek postępu - --strict-types Sprawdza, czy dyrektywa PHP 7.0 strict_types jest włączona -``` - -Bez parametrów sprawdza bieżący katalog w trybie tylko do odczytu, z parametrem `-f` naprawia pliki. - -Zanim się z nim zapoznasz, na pewno najpierw zrób kopię zapasową plików. - -Dla łatwiejszego uruchamiania możemy utworzyć plik `code.bat`: - -```shell -php sciezka_do_Nette_tools\Code-Checker\code-checker %* -``` - - -Co wszystko robi? -================= - -- usuwa [BOM |nette:glossary#BOM] -- sprawdza poprawność szablonów [Latte |latte:] -- sprawdza poprawność plików `.neon`, `.php` i `.json` -- sprawdza występowanie [znaków kontrolnych |nette:glossary#Znaki kontrolne] -- sprawdza, czy plik jest zakodowany w UTF-8 -- sprawdza błędnie zapisane `/* @adnotacje */` (brakuje gwiazdki) -- usuwa kończące `?>` w plikach PHP -- usuwa prawostronne spacje i zbędne linie na końcu pliku -- normalizuje separatory linii do systemowych (jeśli podasz opcję `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/pt/@home.texy b/code-checker/pt/@home.texy deleted file mode 100644 index c50e0603c1..0000000000 --- a/code-checker/pt/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -A ferramenta [Code Checker |https://github.com/nette/code-checker] verifica e, opcionalmente, corrige alguns dos erros formais nos seus códigos-fonte. - - -Instalação -========== - -Você não deve adicionar o Code Checker às suas dependências, mas instalá-lo como um projeto. - -```shell -composer create-project nette/code-checker -``` - -Ou instale-o globalmente usando: - -```shell -composer global require nette/code-checker -``` - -e certifique-se de que seu diretório global `vendor/bin` esteja na [variável de ambiente $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Uso -=== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Pasta ou arquivo para escanear (padrão: diretório atual) - -i | --ignore <mask> Arquivos a ignorar - -f | --fix Corrige arquivos - -l | --eol Converte caracteres de nova linha - --no-progress Não mostrar pontos de progresso - --strict-types Verifica se a diretiva strict_types do PHP 7.0 está habilitada -``` - -Sem parâmetros, verifica o diretório atual no modo somente leitura; com o parâmetro `-f`, corrige os arquivos. - -Antes de se familiarizar com ele, certifique-se de fazer backup dos seus arquivos primeiro. - -Para facilitar a execução, podemos criar um arquivo `code.bat`: - -```shell -php caminho_para_Nette_tools\Code-Checker\code-checker %* -``` - - -O que ele faz? -============== - -- remove o [BOM |nette:glossary#BOM] -- verifica a validade dos templates [Latte |latte:] -- verifica a validade dos arquivos `.neon`, `.php` e `.json` -- verifica a ocorrência de [caracteres de controle |nette:glossary#Caracteres de controle] -- verifica se o arquivo está codificado em UTF-8 -- verifica `/* @anotações */` mal escritas (falta asterisco) -- remove `?>` de fechamento em arquivos PHP -- remove espaços em branco à direita e linhas desnecessárias no final do arquivo -- normaliza os separadores de linha para o padrão do sistema (se você usar a opção `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/ro/@home.texy b/code-checker/ro/@home.texy deleted file mode 100644 index c73570354d..0000000000 --- a/code-checker/ro/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Instrumentul [Code Checker |https://github.com/nette/code-checker] verifică și, eventual, corectează unele dintre erorile formale din codurile dvs. sursă. - - -Instalare -========= - -Code Checker nu ar trebui adăugat la dependențe, ci instalat ca proiect. - -```shell -composer create-project nette/code-checker -``` - -Sau instalați-l global folosind: - -```shell -composer global require nette/code-checker -``` - -și asigurați-vă că directorul dvs. global `vendor/bin` se află în [variabila de mediu $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Utilizare -========= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Director sau fișier de scanat (implicit: directorul curent) - -i | --ignore <mask> Fișiere de ignorat - -f | --fix Corectează fișierele - -l | --eol Convertește caracterele de sfârșit de linie - --no-progress Nu afișa punctele de progres - --strict-types Verifică dacă directiva PHP 7.0 strict_types este activată -``` - -Fără parametri, verifică directorul curent în modul read-only, cu parametrul `-f` corectează fișierele. - -Înainte de a vă familiariza cu el, asigurați-vă că faceți mai întâi o copie de rezervă a fișierelor. - -Pentru o rulare mai ușoară, putem crea un fișier `code.bat`: - -```shell -php cale_catre_Nette_tools\Code-Checker\code-checker %* -``` - - -Ce face? -======== - -- elimină [BOM |nette:glossary#BOM] -- verifică validitatea șabloanelor [Latte |latte:] -- verifică validitatea fișierelor `.neon`, `.php` și `.json` -- verifică prezența [caracterelor de control |nette:glossary#Caractere de control] -- verifică dacă fișierul este codificat în UTF-8 -- verifică `/* @adnotari */` scrise incorect (lipsește asteriscul) -- elimină `?>` de la sfârșitul fișierelor PHP -- elimină spațiile de la sfârșitul rândului și rândurile goale inutile de la sfârșitul fișierului -- normalizează separatorii de rând la cei de sistem (dacă specificați opțiunea `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/ru/@home.texy b/code-checker/ru/@home.texy deleted file mode 100644 index 911ba624aa..0000000000 --- a/code-checker/ru/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Инструмент [Code Checker |https://github.com/nette/code-checker] проверяет и при необходимости исправляет некоторые формальные ошибки в ваших исходных кодах. - - -Установка -========= - -Code Checker не следует добавлять в зависимости, а устанавливать как проект. - -```shell -composer create-project nette/code-checker -``` - -Или установите его глобально с помощью: - -```shell -composer global require nette/code-checker -``` - -и убедитесь, что ваш глобальный каталог `vendor/bin` находится в [переменной окружения $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Использование -============= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Без параметров проверяет текущий каталог в режиме только для чтения, с параметром `-f` исправляет файлы. - -Прежде чем ознакомиться с ним, обязательно сделайте резервную копию файлов. - -Для более легкого запуска можно создать файл `code.bat`: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -Что он делает? -============== - -- удаляет [BOM |nette:glossary#BOM] -- проверяет валидность шаблонов [Latte |latte:] -- проверяет валидность файлов `.neon`, `.php` и `.json` -- проверяет наличие [управляющих символов |nette:glossary#Управляющие символы] -- проверяет, закодирован ли файл в UTF-8 -- проверяет неправильно записанные `/* @anotace */` (отсутствует звездочка) -- удаляет завершающий `?>` у PHP-файлов -- удаляет пробелы в конце строк и лишние строки в конце файла -- нормализует разделители строк до системных (если указана опция `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/sl/@home.texy b/code-checker/sl/@home.texy deleted file mode 100644 index 27cbc98ca0..0000000000 --- a/code-checker/sl/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Orodje [Code Checker |https://github.com/nette/code-checker] preveri in po potrebi popravi nekatere formalne napake v vaših izvornih kodah. - - -Namestitev -========== - -Code Checkerja ne bi smeli dodajati med odvisnosti, ampak ga namestiti kot projekt. - -```shell -composer create-project nette/code-checker -``` - -Ali pa ga namestite globalno s pomočjo: - -```shell -composer global require nette/code-checker -``` - -in se prepričajte, da je vaš globalni imenik `vendor/bin` v [okoljski spremenljivki $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Uporaba -======= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> Files to ignore - -f | --fix Fixes files - -l | --eol Convert newline characters - --no-progress Do not show progress dots - --strict-types Checks whether PHP 7.0 directive strict_types is enabled -``` - -Brez parametrov preveri trenutni imenik v načinu samo za branje, s parametrom `-f` popravlja datoteke. - -Preden se z njim seznanite, si vsekakor najprej varnostno kopirajte datoteke. - -Za lažje zaganjanje si lahko ustvarimo datoteko `code.bat`: - -```shell -php pot_do_Nette_tools\Code-Checker\code-checker %* -``` - - -Kaj vse počne? -============== - -- odstranjuje [BOM |nette:glossary#BOM] -- preverja veljavnost [Latte |latte:] predlog -- preverja veljavnost datotek `.neon`, `.php` in `.json` -- preverja pojav [kontrolnih znakov |nette:glossary#Kontrolni znaki] -- preverja, ali je datoteka kodirana v UTF-8 -- preverja napačno zapisane `/* @anotace */` (manjka zvezdica) -- odstranjuje zaključne `?>` pri PHP datotekah -- odstranjuje desne presledke in nepotrebne vrstice na koncu datoteke -- normalizira ločila vrstic na sistemske (če navedete opcijo `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/tr/@home.texy b/code-checker/tr/@home.texy deleted file mode 100644 index ea2e1ecb22..0000000000 --- a/code-checker/tr/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -[Kod Denetleyicisi |https://github.com/nette/code-checker] aracı, kaynak kodlarınızdaki bazı biçimsel hataları kontrol eder ve gerekirse düzeltir. - - -Kurulum -======= - -Kod Denetleyicisini bağımlılıklara eklememeli, bir proje olarak kurmalısınız. - -```shell -composer create-project nette/code-checker -``` - -Veya küresel olarak kurun: - -```shell -composer global require nette/code-checker -``` - -ve küresel `vendor/bin` dizininizin [$PATH ortam değişkeninde |https://getcomposer.org/doc/03-cli.md#global] olduğundan emin olun. - - -Kullanım -======== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Taranacak klasör veya dosya (varsayılan: geçerli dizin) - -i | --ignore <mask> Yoksayılacak dosyalar - -f | --fix Dosyaları düzeltir - -l | --eol Yeni satır karakterlerini dönüştürür - --no-progress İlerleme noktalarını gösterme - --strict-types PHP 7.0 direktifi strict_types'ın etkin olup olmadığını kontrol eder -``` - -Parametresiz olarak geçerli dizini salt okunur modda kontrol eder, `-f` parametresiyle dosyaları düzeltir. - -Tanışmadan önce dosyalarınızı mutlaka yedekleyin. - -Daha kolay çalıştırmak için bir `code.bat` dosyası oluşturabiliriz: - -```shell -php nette_araçlarının_yolu\Code-Checker\code-checker %* -``` - - -Ne yapar? -========= - -- [BOM |nette:glossary#BOM] kaldırır -- [Latte |latte:] şablonlarının geçerliliğini kontrol eder -- `.neon`, `.php` ve `.json` dosyalarının geçerliliğini kontrol eder -- [Kontrol karakterlerinin |nette:glossary#Kontrol Karakterleri] varlığını kontrol eder -- Dosyanın UTF-8 olarak kodlanıp kodlanmadığını kontrol eder -- Yanlış yazılmış `/* @anotace */` (yıldız eksik) kontrol eder -- PHP dosyalarındaki kapanış `?>` etiketini kaldırır -- Sağdaki boşlukları ve dosyanın sonundaki gereksiz satırları kaldırır -- Satır ayırıcılarını sistem varsayılanına normalleştirir (`-l` seçeneğini belirtirseniz) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/uk/@home.texy b/code-checker/uk/@home.texy deleted file mode 100644 index 261ea7550c..0000000000 --- a/code-checker/uk/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Інструмент [Code Checker |https://github.com/nette/code-checker] перевіряє та, за потреби, виправляє деякі формальні помилки у ваших вихідних кодах. - - -Встановлення -============ - -Code Checker не слід додавати до залежностей, а встановлювати як проект. - -```shell -composer create-project nette/code-checker -``` - -Або встановіть його глобально за допомогою: - -```shell -composer global require nette/code-checker -``` - -і переконайтеся, що ваш глобальний каталог `vendor/bin` знаходиться у [змінній середовища $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Використання -============ - -``` -Usage: php code-checker [options] - -Options: - -d <path> Папка або файл для сканування (за замовчуванням: поточний каталог) - -i | --ignore <mask> Файли, які слід ігнорувати - -f | --fix Виправляє файли - -l | --eol Перетворює символи нового рядка - --no-progress Не показувати точки прогресу - --strict-types Перевіряє, чи увімкнена директива PHP 7.0 strict_types -``` - -Без параметрів перевіряє поточний каталог у режимі лише для читання, з параметром `-f` виправляє файли. - -Перш ніж ознайомитися з ним, обов'язково зробіть резервну копію файлів. - -Для полегшення запуску можна створити файл `code.bat`: - -```shell -php шлях_до_Nette_tools\Code-Checker\code-checker %* -``` - - -Що він робить? -============== - -- видаляє [BOM |nette:glossary#BOM] -- перевіряє валідність [Latte |latte:] шаблонів -- перевіряє валідність файлів `.neon`, `.php` та `.json` -- перевіряє наявність [керуючих символів |nette:glossary#Керуючі символи] -- перевіряє, чи файл закодований у UTF-8 -- перевіряє неправильно записані `/* @anotace */` (відсутня зірочка) -- видаляє завершальний `?>` у PHP файлах -- видаляє пробіли в кінці рядка та зайві рядки в кінці файлу -- нормалізує роздільники рядків до системних (якщо вказано опцію `-l`) - -{{leftbar: www:@menu-common}} diff --git a/command-line/cs/@home.texy b/command-line/cs/@home.texy new file mode 100644 index 0000000000..e05b2f1810 --- /dev/null +++ b/command-line/cs/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +Odlehčená knihovna pro tvorbu konzolových aplikací v PHP. Zpracuje přepínače, volby i poziční argumenty a pomůže vám vytvářet barevný výstup do terminálu s podporou ANSI. + +Instalace: + +```shell +composer require nette/command-line +``` + +Vyžaduje PHP verze 8.2 a podporuje PHP až do verze 8.5. + + +Zpracování argumentů příkazové řádky +==================================== + +Každý konzolový skript musí umět zpracovat argumenty jako `--verbose`, `-o output.txt` nebo prostá jména souborů. Třída [api:Nette\CommandLine\Parser] nabízí nejrychlejší způsob, jak začít: stačí napsat text nápovědy a parser z něj definice voleb sám vyextrahuje: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +A je to. Parser pozná, že `--verbose` je přepínač, `--output` vyžaduje hodnotu a `--format` má nepovinnou hodnotu s výchozí hodnotou `json`. Text nápovědy tak zůstává v souladu se skutečnými definicemi voleb. + +Metoda `parse()` vrací asociativní pole. Klíče přesně odpovídají názvům voleb tak, jak jste je definovali, včetně pomlček: + +```php +[ + '--help' => true, // nebo null, pokud nebylo použito + '--verbose' => null, + '--output' => 'file.txt', // nebo null, pokud nebylo použito + '--format' => 'json', // výchozí hodnota z (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +Ve výchozím nastavení čte `parse()` z `$_SERVER['argv']`. Můžete předat vlastní pole, což se hodí při testování: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Syntaxe textu nápovědy +---------------------- + +Parser extrahuje definice voleb z formátovaného textu nápovědy podle těchto pravidel: + +| `--verbose` | přepínač (bez hodnoty) +| `-v, --verbose` | přepínač s krátkým aliasem +| `--output <file>`| volba s povinnou hodnotou +| `--format [type]`| volba s nepovinnou hodnotou +| `(default: json)`| nastaví výchozí hodnotu +| `<path>...` | opakovatelná volba + +Každý řádek definuje jednu volbu. Názvy voleb musí být od popisu odděleny alespoň dvěma mezerami. + + +Další konfigurace +----------------- + +Některá nastavení nelze v textu nápovědy vyjádřit. Předejte je jako pole ve druhém parametru, kde klíčem je název volby: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Dostupné klíče: + +| `Parser::Repeatable` | sbírá více hodnot do pole +| `Parser::RealPath` | ověří, že soubor existuje, a převede cestu na absolutní +| `Parser::Normalizer` | transformační funkce `fn($value) => ...` +| `Parser::Default` | výchozí hodnota (totéž jako `(default: x)` v textu nápovědy) +| `Parser::Enum` | pole povolených hodnot + + +Plynulé API +=========== + +Když potřebujete nad definicemi voleb větší kontrolu, použijte plynulé API s metodami `addSwitch()`, `addOption()` a `addArgument()`. Tento přístup vám zpřístupní všechny možnosti, včetně normalizátorů, výčtů a přesného řízení každého parametru: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +Stejně jako u `addFromHelp()` můžete metodě `parse()` předat vlastní pole pro testování: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Přepínače, volby a argumenty +---------------------------- + +Existují tři typy vstupů z příkazové řádky: + +**Přepínače** jsou příznaky bez hodnoty, jako `--verbose` nebo `-v`. Když jsou přítomné, vyhodnotí se jako `true`, jinak jako `null`: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (nepoužito) → null +``` + +**Volby** přijímají hodnoty, jako `--output file.txt`. Hodnotu lze oddělit mezerou nebo znakem `=`: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → vyhodí výjimku (hodnota je povinná) +// (nepoužito) → null +``` + +Všimněte si, že samotná volba je vždy nepovinná - její nepoužití vrátí `null`. Pokud ji ale použijete, je hodnota ve výchozím nastavení povinná. Nastavením `optionalValue: true` povolíte volbu i bez hodnoty (pak se vyhodnotí jako `true`): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (nepoužito) → null +``` + +Když se stejná volba použije vícekrát bez `repeatable: true`, vyhraje poslední hodnota: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**Argumenty** jsou poziční hodnoty bez pomlček. Ve výchozím nastavení jsou povinné. Nastavením `optional: true` je učiníte nepovinnými: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (nepoužito) → vyhodí výjimku + +$parser->addArgument('output', optional: true); +// (nepoužito) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (nepoužito) → 'out.txt' +``` + +Pomocí `fallback` určíte hodnotu, která se použije, když nepovinná volba nebo argument nejsou zadány. U voleb s `optionalValue: true` platí, že použití volby bez hodnoty se stále vyhodnotí jako `true`, zatímco výchozí hodnota se použije pouze tehdy, když volba není přítomna vůbec: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (volba použita bez hodnoty) +// (nepoužito) → 'json' (výchozí hodnota) +``` + +Argumenty se mohou na příkazové řádce objevit kdekoli - nemusí následovat až za volbami: + +```php +// všechny tyto zápisy jsou rovnocenné: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Omezení hodnot pomocí výčtu +--------------------------- + +Omezte přijímané hodnoty na konkrétní množinu: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → vyhodí "Value of option --format must be json, or xml, or csv." +``` + + +Opakovatelné volby +------------------ + +Nastavením `repeatable: true` shromáždíte více hodnot do pole: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (nepoužito) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Transformace hodnot +------------------- + +Pomocí `normalizer` transformujete zpracovanou hodnotu: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (celé číslo) +``` + +Pro ověření cesty k souboru použijte vestavěný `normalizeRealPath`: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → vyhodí "File path 'missing.ini' not found." +``` + + +Kombinace obou přístupů +----------------------- + +Když potřebujete normalizátory jen pro některé volby, můžete `addFromHelp()` zkombinovat s plynulými metodami: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Ošetření chyb +------------- + +Parser vyhodí `\Exception` při neplatném vstupu: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Běžné chybové zprávy: + +| `Option --output requires argument.` | volba použita bez povinné hodnoty +| `Unknown option --foo.` | neznámá volba +| `Missing required argument <file>.` | nezadán povinný argument +| `Unexpected parameter foo.` | přebývající poziční argument +| `Value of option --format must be json, or xml.` | hodnota není ve výčtu + +Metodou `isEmpty()` zjistíte, zda nebyly zadány vůbec žádné argumenty příkazové řádky (tj. uživatel spustil pouze `script.php` bez čehokoli za ním): + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Obsluha --help a --version +-------------------------- + +Když má váš skript povinné argumenty, spuštění `script.php --help` by normálně selhalo, protože chybí povinný argument. Použijte `parseOnly()`, abyste informativní volby zkontrolovali jako první: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // povinný + +// Nejprve zkontrolujeme informativní volby (bez validace, bez výjimek) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Teprve teď provedeme plné zpracování s validací +$args = $parser->parse(); +``` + +Metoda `parseOnly()`: +- zpracuje pouze zadané volby a vše ostatní ignoruje, +- respektuje aliasy (`-h` → `--help`), +- nikdy nevyhodí výjimku, +- vrátí `null` pro volby, které nebyly použity. + + +Barevný výstup +============== + +Třída [api:Nette\CommandLine\Console] obalí text ANSI kódy barev, takže váš výstup v terminálu vynikne: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +Barva se zadává jako `'popředí'` nebo `'popředí/pozadí'`. Dostupné barvy jsou: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` a `yellow`. + +Barvy se zapnou automaticky jen tehdy, když je výstup podporuje. Metoda `color()` vrátí při vypnutých barvách prostý řetězec, takže je vždy bezpečné ji volat. Chování můžete nastavit i ručně: + +```php +$console->useColors(false); // vypne barvy +$console->useColors(true); // vynutí barvy +``` + + +Detekce terminálu +----------------- + +Dvě statické metody vám pomohou rozhodnout, zda použít funkce určené jen pro terminál. `detectColors()` vrátí `false`, když je nastavená proměnná prostředí [NO_COLOR |https://no-color.org] nebo když výstup není konzolový terminál; proměnná `FORCE_COLOR` kontrolu terminálu přebije: + +```php +if (Console::detectColors()) { + // terminál podporuje ANSI barvy +} +``` + +`detectTerminal()` vám řekne, zda je výstupem interaktivní terminál (TTY). To se hodí pro automatické vypnutí funkcí, které dávají smysl jen v terminálu, jako jsou ukazatele průběhu, přepisování řádků nebo interaktivní dotazy: + +```php +if (Console::detectTerminal()) { + // výstup jde do interaktivního terminálu, ne do souboru či roury +} +``` + + +Kompletní příklad +================= + +Tady je skript konvertoru souborů z praxe, který kombinuje `Parser` a `Console`: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// Obsloužíme --help ještě před validací (vyhneme se chybě "missing argument") +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... zde následuje logika konverze ... + +echo "Done!\n"; +``` + +Skript přijímá příkazy jako: + +- `convert input.txt` - konverze s výchozím nastavením +- `convert -v --format xml input.txt` - podrobný výstup, formát XML +- `convert -o result.txt input.txt` - určení výstupního souboru +- `convert --help` - zobrazení nápovědy (funguje i bez vstupního souboru) + + +{{sitename: Nette Dokumentace}} diff --git a/command-line/cs/@left-menu.texy b/command-line/cs/@left-menu.texy new file mode 100644 index 0000000000..ff9d0cc8a7 --- /dev/null +++ b/command-line/cs/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Úvod |@home] + + +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/command-line/de/@home.texy b/command-line/de/@home.texy new file mode 100644 index 0000000000..8df481975b --- /dev/null +++ b/command-line/de/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +Eine leichtgewichtige Bibliothek zum Erstellen von Kommandozeilenanwendungen in PHP. Sie verarbeitet Schalter, Optionen und positionelle Argumente und hilft Ihnen, farbige Terminalausgaben mit ANSI-Unterstützung zu erzeugen. + +Installation: + +```shell +composer require nette/command-line +``` + +Sie erfordert PHP in der Version 8.2 und unterstützt PHP bis 8.5. + + +Verarbeitung von Kommandozeilenargumenten +========================================= + +Jedes Konsolenskript muss Argumente wie `--verbose`, `-o output.txt` oder schlichte Dateinamen verarbeiten können. Die Klasse [api:Nette\CommandLine\Parser] bietet den schnellsten Einstieg: Sie schreiben einfach den Hilfetext, und der Parser extrahiert die Definitionen der Optionen selbst daraus: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +Das war's. Der Parser erkennt, dass `--verbose` ein Schalter ist, `--output` einen Wert verlangt und `--format` einen optionalen Wert mit `json` als Standardwert hat. Ihr Hilfetext bleibt so mit den tatsächlichen Definitionen der Optionen im Einklang. + +Die Methode `parse()` gibt ein assoziatives Array zurück. Die Schlüssel entsprechen genau den Namen der Optionen, wie Sie sie definiert haben, einschließlich der Bindestriche: + +```php +[ + '--help' => true, // oder null, wenn nicht verwendet + '--verbose' => null, + '--output' => 'file.txt', // oder null, wenn nicht verwendet + '--format' => 'json', // Standardwert aus (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +Standardmäßig liest `parse()` aus `$_SERVER['argv']`. Sie können ein eigenes Array übergeben, was sich beim Testen anbietet: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Syntax des Hilfetexts +--------------------- + +Der Parser extrahiert die Definitionen der Optionen nach diesen Regeln aus dem formatierten Hilfetext: + +| `--verbose` | Schalter (ohne Wert) +| `-v, --verbose` | Schalter mit kurzem Alias +| `--output <file>`| Option mit erforderlichem Wert +| `--format [type]`| Option mit optionalem Wert +| `(default: json)`| setzt den Standardwert +| `<path>...` | wiederholbare Option + +Jede Zeile definiert eine Option. Die Namen der Optionen müssen von der Beschreibung durch mindestens zwei Leerzeichen getrennt sein. + + +Weitere Konfiguration +--------------------- + +Manche Einstellungen lassen sich im Hilfetext nicht ausdrücken. Übergeben Sie sie als Array im zweiten Parameter, wobei der Schlüssel der Name der Option ist: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Verfügbare Schlüssel: + +| `Parser::Repeatable` | sammelt mehrere Werte in einem Array +| `Parser::RealPath` | prüft, ob die Datei existiert, und wandelt den Pfad in einen absoluten um +| `Parser::Normalizer` | Transformationsfunktion `fn($value) => ...` +| `Parser::Default` | Standardwert (dasselbe wie `(default: x)` im Hilfetext) +| `Parser::Enum` | Array erlaubter Werte + + +Fluent API +========== + +Wenn Sie mehr Kontrolle über die Definitionen der Optionen brauchen, verwenden Sie das Fluent API mit den Methoden `addSwitch()`, `addOption()` und `addArgument()`. Dieser Ansatz eröffnet Ihnen alle Möglichkeiten, einschließlich Normalizer, Enums und der genauen Steuerung jedes Parameters: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +Wie bei `addFromHelp()` können Sie der Methode `parse()` zum Testen ein eigenes Array übergeben: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Schalter, Optionen und Argumente +-------------------------------- + +Es gibt drei Arten von Eingaben auf der Kommandozeile: + +**Schalter** sind Flags ohne Wert, etwa `--verbose` oder `-v`. Sind sie vorhanden, werden sie als `true` ausgewertet, sonst als `null`: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (nicht verwendet) → null +``` + +**Optionen** nehmen Werte entgegen, etwa `--output file.txt`. Der Wert lässt sich durch ein Leerzeichen oder durch `=` trennen: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → wirft eine Exception (Wert erforderlich) +// (nicht verwendet) → null +``` + +Beachten Sie, dass die Option selbst immer optional ist - wird sie nicht verwendet, kommt `null` zurück. Wird sie aber verwendet, ist der Wert standardmäßig erforderlich. Mit `optionalValue: true` erlauben Sie die Option auch ohne Wert (sie wird dann als `true` ausgewertet): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (nicht verwendet) → null +``` + +Wird dieselbe Option ohne `repeatable: true` mehrfach verwendet, gewinnt der letzte Wert: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**Argumente** sind positionelle Werte ohne Bindestriche. Standardmäßig sind sie erforderlich. Mit `optional: true` machen Sie sie optional: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (nicht verwendet) → wirft eine Exception + +$parser->addArgument('output', optional: true); +// (nicht verwendet) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (nicht verwendet) → 'out.txt' +``` + +Mit `fallback` legen Sie den Wert fest, der verwendet wird, wenn eine optionale Option oder ein optionales Argument nicht angegeben wird. Bei Optionen mit `optionalValue: true` gilt: Die Verwendung der Option ohne Wert wird weiterhin als `true` ausgewertet, während der Standardwert nur dann greift, wenn die Option überhaupt nicht vorhanden ist: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (Option ohne Wert verwendet) +// (nicht verwendet) → 'json' (Standardwert) +``` + +Argumente können auf der Kommandozeile an beliebiger Stelle stehen - sie müssen nicht erst hinter den Optionen kommen: + +```php +// alle diese Schreibweisen sind gleichwertig: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Werte mit einem Enum einschränken +--------------------------------- + +Beschränken Sie die akzeptierten Werte auf eine bestimmte Menge: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → wirft "Value of option --format must be json, or xml, or csv." +``` + + +Wiederholbare Optionen +---------------------- + +Mit `repeatable: true` sammeln Sie mehrere Werte in einem Array: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (nicht verwendet) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Werte transformieren +-------------------- + +Mit `normalizer` transformieren Sie den verarbeiteten Wert: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (Integer) +``` + +Zum Prüfen eines Dateipfads verwenden Sie das eingebaute `normalizeRealPath`: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → wirft "File path 'missing.ini' not found." +``` + + +Kombination beider Ansätze +-------------------------- + +Wenn Sie Normalizer nur für einige Optionen brauchen, können Sie `addFromHelp()` mit den Fluent-Methoden kombinieren: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Fehlerbehandlung +---------------- + +Der Parser wirft bei ungültiger Eingabe `\Exception`: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Häufige Fehlermeldungen: + +| `Option --output requires argument.` | Option ohne den erforderlichen Wert verwendet +| `Unknown option --foo.` | unbekannte Option +| `Missing required argument <file>.` | erforderliches Argument nicht angegeben +| `Unexpected parameter foo.` | überzähliges positionelles Argument +| `Value of option --format must be json, or xml.` | Wert nicht im Enum + +Mit der Methode `isEmpty()` stellen Sie fest, ob überhaupt keine Argumente auf der Kommandozeile angegeben wurden (der Benutzer also nur `script.php` ohne irgendetwas dahinter aufgerufen hat): + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Umgang mit --help und --version +------------------------------- + +Wenn Ihr Skript erforderliche Argumente hat, würde der Aufruf `script.php --help` normalerweise fehlschlagen, weil das erforderliche Argument fehlt. Verwenden Sie `parseOnly()`, um die informativen Optionen zuerst zu prüfen: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // erforderlich + +// Zuerst prüfen wir die informativen Optionen (ohne Validierung, ohne Exceptions) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Erst jetzt die vollständige Verarbeitung mit Validierung +$args = $parser->parse(); +``` + +Die Methode `parseOnly()`: +- verarbeitet nur die angegebenen Optionen und ignoriert alles Übrige, +- respektiert Aliase (`-h` → `--help`), +- wirft niemals eine Exception, +- gibt `null` für Optionen zurück, die nicht verwendet wurden. + + +Farbige Ausgabe +=============== + +Die Klasse [api:Nette\CommandLine\Console] umhüllt den Text mit ANSI-Farbcodes, sodass Ihre Ausgabe im Terminal hervorsticht: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +Die Farbe wird als `'Vordergrund'` oder `'Vordergrund/Hintergrund'` angegeben. Verfügbare Farben sind: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` und `yellow`. + +Farben werden nur dann automatisch eingeschaltet, wenn die Ausgabe sie unterstützt. Die Methode `color()` gibt bei abgeschalteten Farben einen einfachen String zurück, ihr Aufruf ist also immer sicher. Sie können das Verhalten auch von Hand festlegen: + +```php +$console->useColors(false); // schaltet die Farben ab +$console->useColors(true); // erzwingt die Farben +``` + + +Erkennung des Terminals +----------------------- + +Zwei statische Methoden helfen Ihnen bei der Entscheidung, ob Sie Funktionen nutzen, die nur im Terminal Sinn ergeben. `detectColors()` gibt `false` zurück, wenn die Umgebungsvariable [NO_COLOR |https://no-color.org] gesetzt ist oder wenn die Ausgabe kein Konsolenterminal ist; die Variable `FORCE_COLOR` überstimmt die Prüfung des Terminals: + +```php +if (Console::detectColors()) { + // das Terminal unterstützt ANSI-Farben +} +``` + +`detectTerminal()` sagt Ihnen, ob die Ausgabe ein interaktives Terminal (ein TTY) ist. Das eignet sich zum automatischen Abschalten von Funktionen, die nur in einem Terminal Sinn ergeben, etwa Fortschrittsanzeigen, das Überschreiben von Zeilen oder interaktive Rückfragen: + +```php +if (Console::detectTerminal()) { + // die Ausgabe geht an ein interaktives Terminal, nicht in eine Datei oder Pipe +} +``` + + +Vollständiges Beispiel +====================== + +Hier ist ein Skript zur Dateikonvertierung aus der Praxis, das `Parser` und `Console` kombiniert: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// --help noch vor der Validierung behandeln (vermeidet den Fehler "missing argument") +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... hier folgt die Logik der Konvertierung ... + +echo "Done!\n"; +``` + +Das Skript akzeptiert Befehle wie: + +- `convert input.txt` - Konvertierung mit den Standardeinstellungen +- `convert -v --format xml input.txt` - ausführliche Ausgabe, Format XML +- `convert -o result.txt input.txt` - Angabe der Ausgabedatei +- `convert --help` - Anzeige der Hilfe (funktioniert auch ohne Eingabedatei) + + +{{sitename: Nette Dokumentation}} diff --git a/command-line/de/@left-menu.texy b/command-line/de/@left-menu.texy new file mode 100644 index 0000000000..eaf312fc19 --- /dev/null +++ b/command-line/de/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Übersicht |@home] + + +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/command-line/en/@home.texy b/command-line/en/@home.texy new file mode 100644 index 0000000000..6c072a6d6b --- /dev/null +++ b/command-line/en/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +A lightweight library for building command-line applications in PHP. It parses switches, options, and positional arguments, and helps you produce colorful terminal output with ANSI support. + +Installation: + +```shell +composer require nette/command-line +``` + +It requires PHP version 8.2 and supports PHP up to 8.5. + + +Parsing Command-Line Arguments +============================== + +Every CLI script needs to handle arguments like `--verbose`, `-o output.txt`, or plain file names. The [api:Nette\CommandLine\Parser] class offers the fastest way to get started: just write your help text and let the parser extract option definitions from it: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +That's it. The parser understands that `--verbose` is a switch, `--output` requires a value, and `--format` has an optional value with `json` as its fallback. Your help text stays in sync with the actual option definitions. + +The `parse()` method returns an associative array. Keys match option names exactly as defined, including the dashes: + +```php +[ + '--help' => true, // or null if not used + '--verbose' => null, + '--output' => 'file.txt', // or null if not used + '--format' => 'json', // fallback from (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +By default, `parse()` reads from `$_SERVER['argv']`. You can pass a custom array, which is handy for testing: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Help Text Syntax +---------------- + +The parser extracts option definitions from formatted help text according to these rules: + +| `--verbose` | Switch (no value) +| `-v, --verbose` | Switch with short alias +| `--output <file>`| Option with required value +| `--format [type]`| Option with optional value +| `(default: json)`| Sets the fallback value +| `<path>...` | Repeatable option + +Each line defines one option. Option names must be separated from their descriptions by at least two spaces. + + +Additional Configuration +------------------------ + +Some settings can't be expressed in the help text. Pass an array as the second parameter, keyed by option name: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Available keys: + +| `Parser::Repeatable` | Collect multiple values into an array +| `Parser::RealPath` | Validate that the file exists and resolve it to an absolute path +| `Parser::Normalizer` | Transform function `fn($value) => ...` +| `Parser::Default` | Fallback value (same as `(default: x)` in the help text) +| `Parser::Enum` | Array of allowed values + + +Fluent API +========== + +When you need more control over the option definitions, use the fluent API with the `addSwitch()`, `addOption()`, and `addArgument()` methods. This approach gives you access to all features, including normalizers, enums, and precise control over each parameter: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +As with `addFromHelp()`, you can pass a custom array to `parse()` for testing: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Switches, Options, and Arguments +-------------------------------- + +There are three types of command-line inputs: + +**Switches** are flags without values, like `--verbose` or `-v`. They parse as `true` when present, `null` when absent: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (not used) → null +``` + +**Options** accept values, like `--output file.txt`. The value can be separated by a space or by `=`: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → throws an exception (value required) +// (not used) → null +``` + +Note that the option itself is always optional - not using it returns `null`. However, when it is used, the value is required by default. Set `optionalValue: true` to allow the option without a value (it then parses as `true`): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (not used) → null +``` + +When the same option is used multiple times without `repeatable: true`, the last value wins: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**Arguments** are positional values without dashes. By default they are required. Set `optional: true` to make them optional: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (not used) → throws an exception + +$parser->addArgument('output', optional: true); +// (not used) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (not used) → 'out.txt' +``` + +Use `fallback` to specify the value used when an optional option or argument is not provided. For options with `optionalValue: true`, note that using the option without a value still parses as `true`, while the fallback is used only when the option is not present at all: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (option used without a value) +// (not used) → 'json' (fallback) +``` + +Arguments can appear anywhere on the command line - they don't have to come after the options: + +```php +// all of these are equivalent: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Restricting Values with Enum +---------------------------- + +Limit the accepted values to a specific set: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → throws "Value of option --format must be json, or xml, or csv." +``` + + +Repeatable Options +------------------ + +Set `repeatable: true` to collect multiple values into an array: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (not used) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Transforming Values +------------------- + +Use a `normalizer` to transform the parsed value: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (integer) +``` + +For file path validation, use the built-in `normalizeRealPath`: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → throws "File path 'missing.ini' not found." +``` + + +Mixing Both Approaches +---------------------- + +You can combine `addFromHelp()` with the fluent methods when you need normalizers for only some of the options: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Error Handling +-------------- + +The parser throws `\Exception` for invalid input: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Common error messages: + +| `Option --output requires argument.` | Option used without its required value +| `Unknown option --foo.` | Unrecognized option +| `Missing required argument <file>.` | Required argument not provided +| `Unexpected parameter foo.` | Extra positional argument +| `Value of option --format must be json, or xml.` | Value not in the enum + +Use `isEmpty()` to check whether no command-line arguments were provided at all (i.e. the user ran just `script.php` with nothing after it): + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Handling --help and --version +----------------------------- + +When your script has required arguments, running `script.php --help` would normally fail because the required argument is missing. Use `parseOnly()` to check for info options first: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // required + +// First, check the info options (no validation, no exceptions) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Now do the full parsing with validation +$args = $parser->parse(); +``` + +The `parseOnly()` method: +- parses only the specified options, ignoring everything else, +- respects aliases (`-h` → `--help`), +- never throws exceptions, +- returns `null` for options that weren't used. + + +Colorful Output +=============== + +The [api:Nette\CommandLine\Console] class wraps text in ANSI color codes so your output stands out in the terminal: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +The color is given as `'foreground'` or `'foreground/background'`. Available colors are: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive`, and `yellow`. + +Colors are enabled automatically only when the output supports them. The `color()` method returns a plain string when colors are disabled, so it's always safe to call. You can force the behavior manually: + +```php +$console->useColors(false); // disable colors +$console->useColors(true); // force colors on +``` + + +Detecting the Terminal +---------------------- + +Two static methods help you decide whether to use terminal-only features. `detectColors()` returns `false` when the [NO_COLOR |https://no-color.org] environment variable is set, or when the output isn't a CLI terminal; the `FORCE_COLOR` variable overrides the terminal check: + +```php +if (Console::detectColors()) { + // the terminal supports ANSI colors +} +``` + +`detectTerminal()` tells you whether the output is an interactive terminal (a TTY). This is useful for auto-disabling features that only make sense in a terminal, such as progress indicators, line-rewriting output, or interactive prompts: + +```php +if (Console::detectTerminal()) { + // output goes to an interactive terminal, not a file or pipe +} +``` + + +Complete Example +================ + +Here's a real-world file converter script combining `Parser` and `Console`: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// Handle --help before validation (avoids the "missing argument" error) +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... conversion logic here ... + +echo "Done!\n"; +``` + +The script accepts commands like: + +- `convert input.txt` - convert with the defaults +- `convert -v --format xml input.txt` - verbose, XML format +- `convert -o result.txt input.txt` - specify the output file +- `convert --help` - show the help (works even without the input file) + + +{{sitename: Nette Documentation}} diff --git a/command-line/en/@left-menu.texy b/command-line/en/@left-menu.texy new file mode 100644 index 0000000000..57788fce72 --- /dev/null +++ b/command-line/en/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Overview |@home] + + +Further Reading +*************** +- [Nette Documentation |nette:] +- [Nette Application |application:how-it-works] +- [Utilities |utils:] +- [Best practices |best-practices:] +- [Troubleshooting |nette:troubleshooting] diff --git a/command-line/es/@home.texy b/command-line/es/@home.texy new file mode 100644 index 0000000000..e9e1aef437 --- /dev/null +++ b/command-line/es/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +Una biblioteca ligera para construir aplicaciones de línea de comandos en PHP. Analiza los conmutadores, las opciones y los argumentos posicionales, y le ayuda a producir una salida de terminal con colores y soporte de ANSI. + +Instalación: + +```shell +composer require nette/command-line +``` + +Requiere PHP en la versión 8.2 y soporta PHP hasta la 8.5. + + +Analizar los argumentos de la línea de comandos +=============================================== + +Todo script de CLI necesita tratar argumentos como `--verbose`, `-o output.txt` o simples nombres de archivo. La clase [api:Nette\CommandLine\Parser] ofrece la forma más rápida de empezar: basta con escribir su texto de ayuda y dejar que el parser extraiga de él las definiciones de las opciones: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +Eso es todo. El parser entiende que `--verbose` es un conmutador, que `--output` requiere un valor y que `--format` tiene un valor opcional con `json` como valor de reserva. Su texto de ayuda se mantiene sincronizado con las definiciones reales de las opciones. + +El método `parse()` devuelve un array asociativo. Las claves coinciden exactamente con los nombres de las opciones tal como se definieron, guiones incluidos: + +```php +[ + '--help' => true, // o null si no se usó + '--verbose' => null, + '--output' => 'file.txt', // o null si no se usó + '--format' => 'json', // valor de reserva de (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +De forma predeterminada, `parse()` lee de `$_SERVER['argv']`. Puede pasarle un array propio, lo que resulta práctico para las pruebas: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Sintaxis del texto de ayuda +--------------------------- + +El parser extrae las definiciones de las opciones del texto de ayuda formateado según estas reglas: + +| `--verbose` | Conmutador (sin valor) +| `-v, --verbose` | Conmutador con alias corto +| `--output <file>`| Opción con valor obligatorio +| `--format [type]`| Opción con valor opcional +| `(default: json)`| Establece el valor de reserva +| `<path>...` | Opción repetible + +Cada línea define una opción. Los nombres de las opciones tienen que estar separados de sus descripciones por al menos dos espacios. + + +Configuración adicional +----------------------- + +Algunos ajustes no se pueden expresar en el texto de ayuda. Pase un array como segundo parámetro, con los nombres de las opciones como claves: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Claves disponibles: + +| `Parser::Repeatable` | Recoge varios valores en un array +| `Parser::RealPath` | Verifica que el archivo existe y lo resuelve a una ruta absoluta +| `Parser::Normalizer` | Función de transformación `fn($value) => ...` +| `Parser::Default` | Valor de reserva (lo mismo que `(default: x)` en el texto de ayuda) +| `Parser::Enum` | Array de valores permitidos + + +API fluida +========== + +Cuando necesite más control sobre las definiciones de las opciones, use la API fluida con los métodos `addSwitch()`, `addOption()` y `addArgument()`. Este enfoque le da acceso a todas las funciones, incluidos los normalizadores, los enums y el control preciso de cada parámetro: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +Igual que con `addFromHelp()`, puede pasarle a `parse()` un array propio para hacer pruebas: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Conmutadores, opciones y argumentos +----------------------------------- + +Hay tres tipos de entradas de línea de comandos: + +Los **conmutadores** son banderas sin valor, como `--verbose` o `-v`. Se analizan como `true` cuando están presentes y como `null` cuando faltan: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (sin usar) → null +``` + +Las **opciones** aceptan valores, como `--output file.txt`. El valor se puede separar con un espacio o con `=`: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → lanza una excepción (valor obligatorio) +// (sin usar) → null +``` + +Tenga en cuenta que la propia opción siempre es opcional: si no se usa, devuelve `null`. Pero, cuando se usa, el valor es obligatorio de forma predeterminada. Establezca `optionalValue: true` para permitir la opción sin valor (entonces se analiza como `true`): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (sin usar) → null +``` + +Cuando la misma opción se usa varias veces sin `repeatable: true`, gana el último valor: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +Los **argumentos** son valores posicionales sin guiones. De forma predeterminada son obligatorios. Establezca `optional: true` para hacerlos opcionales: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (sin usar) → lanza una excepción + +$parser->addArgument('output', optional: true); +// (sin usar) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (sin usar) → 'out.txt' +``` + +Use `fallback` para indicar el valor que se usa cuando no se proporciona una opción o un argumento opcionales. En las opciones con `optionalValue: true`, tenga en cuenta que usar la opción sin valor sigue analizándose como `true`, mientras que el valor de reserva se usa solo cuando la opción no aparece en absoluto: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (opción usada sin valor) +// (sin usar) → 'json' (valor de reserva) +``` + +Los argumentos pueden aparecer en cualquier sitio de la línea de comandos, no tienen por qué ir detrás de las opciones: + +```php +// todas estas formas son equivalentes: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Restringir los valores con enum +------------------------------- + +Limite los valores aceptados a un conjunto concreto: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → lanza "Value of option --format must be json, or xml, or csv." +``` + + +Opciones repetibles +------------------- + +Establezca `repeatable: true` para recoger varios valores en un array: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (sin usar) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Transformar los valores +----------------------- + +Use un `normalizer` para transformar el valor analizado: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (entero) +``` + +Para verificar rutas de archivo, use el `normalizeRealPath` integrado: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → lanza "File path 'missing.ini' not found." +``` + + +Combinar los dos enfoques +------------------------- + +Puede combinar `addFromHelp()` con los métodos fluidos cuando necesite normalizadores solo para algunas de las opciones: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Tratamiento de los errores +-------------------------- + +El parser lanza `\Exception` cuando la entrada no es válida: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Mensajes de error habituales: + +| `Option --output requires argument.` | Opción usada sin su valor obligatorio +| `Unknown option --foo.` | Opción no reconocida +| `Missing required argument <file>.` | Argumento obligatorio no proporcionado +| `Unexpected parameter foo.` | Argumento posicional de más +| `Value of option --format must be json, or xml.` | Valor que no está en el enum + +Use `isEmpty()` para comprobar si no se proporcionó ningún argumento de línea de comandos (es decir, si el usuario ejecutó solo `script.php` sin nada detrás): + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Tratar --help y --version +------------------------- + +Cuando su script tiene argumentos obligatorios, ejecutar `script.php --help` fallaría normalmente porque falta el argumento obligatorio. Use `parseOnly()` para comprobar antes las opciones informativas: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // obligatorio + +// Primero comprueba las opciones informativas (sin validación, sin excepciones) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Ahora hace el análisis completo con validación +$args = $parser->parse(); +``` + +El método `parseOnly()`: +- analiza solo las opciones indicadas e ignora todo lo demás, +- respeta los alias (`-h` → `--help`), +- nunca lanza excepciones, +- devuelve `null` para las opciones que no se usaron. + + +Salida con colores +================== + +La clase [api:Nette\CommandLine\Console] envuelve el texto en códigos de color ANSI para que su salida destaque en la terminal: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +El color se indica como `'primer plano'` o `'primer plano/fondo'`. Los colores disponibles son: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` y `yellow`. + +Los colores se activan automáticamente solo cuando la salida los soporta. El método `color()` devuelve una cadena simple cuando los colores están desactivados, así que llamarlo siempre es seguro. Puede forzar el comportamiento a mano: + +```php +$console->useColors(false); // desactiva los colores +$console->useColors(true); // fuerza los colores +``` + + +Detectar la terminal +-------------------- + +Dos métodos estáticos le ayudan a decidir si usar funciones exclusivas de la terminal. `detectColors()` devuelve `false` cuando está establecida la variable de entorno [NO_COLOR |https://no-color.org], o cuando la salida no es una terminal de CLI; la variable `FORCE_COLOR` anula la comprobación de la terminal: + +```php +if (Console::detectColors()) { + // la terminal soporta colores ANSI +} +``` + +`detectTerminal()` le dice si la salida es una terminal interactiva (una TTY). Es útil para desactivar automáticamente las funciones que solo tienen sentido en una terminal, como los indicadores de progreso, la salida que reescribe líneas o las preguntas interactivas: + +```php +if (Console::detectTerminal()) { + // la salida va a una terminal interactiva, no a un archivo ni a una tubería +} +``` + + +Ejemplo completo +================ + +Aquí tiene un script real de conversión de archivos que combina `Parser` y `Console`: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// Trata --help antes de la validación (evita el error de "falta el argumento") +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... aquí va la lógica de conversión ... + +echo "Done!\n"; +``` + +El script acepta comandos como: + +- `convert input.txt`: convierte con los valores predeterminados +- `convert -v --format xml input.txt`: modo detallado, formato XML +- `convert -o result.txt input.txt`: indica el archivo de salida +- `convert --help`: muestra la ayuda (funciona incluso sin el archivo de entrada) + + +{{sitename: Documentación de Nette}} diff --git a/command-line/es/@left-menu.texy b/command-line/es/@left-menu.texy new file mode 100644 index 0000000000..b71e62ec0b --- /dev/null +++ b/command-line/es/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Introducción |@home] + + +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/command-line/fr/@home.texy b/command-line/fr/@home.texy new file mode 100644 index 0000000000..338105c63b --- /dev/null +++ b/command-line/fr/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +Une bibliothèque légère pour construire des applications en ligne de commande en PHP. Elle analyse les commutateurs, les options et les arguments positionnels, et vous aide à produire une sortie colorée dans le terminal grâce à la prise en charge d'ANSI. + +Installation : + +```shell +composer require nette/command-line +``` + +Elle nécessite PHP 8.2 et prend en charge PHP jusqu'à la version 8.5. + + +Analyse des arguments de la ligne de commande +============================================= + +Tout script CLI doit traiter des arguments comme `--verbose`, `-o output.txt` ou de simples noms de fichiers. La classe [api:Nette\CommandLine\Parser] offre le démarrage le plus rapide : écrivez votre texte d'aide et laissez l'analyseur en extraire les définitions des options : + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +C'est tout. L'analyseur comprend que `--verbose` est un commutateur, que `--output` exige une valeur et que `--format` a une valeur facultative dont `json` est le repli. Votre texte d'aide reste ainsi synchronisé avec les définitions réelles des options. + +La méthode `parse()` renvoie un tableau associatif. Les clés correspondent exactement aux noms des options tels que définis, tirets compris : + +```php +[ + '--help' => true, // or null if not used + '--verbose' => null, + '--output' => 'file.txt', // or null if not used + '--format' => 'json', // fallback from (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +Par défaut, `parse()` lit `$_SERVER['argv']`. Vous pouvez lui passer votre propre tableau, ce qui est pratique pour les tests : + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Syntaxe du texte d'aide +----------------------- + +L'analyseur extrait les définitions des options d'un texte d'aide mis en forme selon ces règles : + +| `--verbose` | Commutateur (sans valeur) +| `-v, --verbose` | Commutateur avec alias court +| `--output <file>`| Option à valeur obligatoire +| `--format [type]`| Option à valeur facultative +| `(default: json)`| Définit la valeur de repli +| `<path>...` | Option répétable + +Chaque ligne définit une option. Les noms des options doivent être séparés de leur description par au moins deux espaces. + + +Configuration supplémentaire +---------------------------- + +Certains réglages ne peuvent pas s'exprimer dans le texte d'aide. Passez un tableau en second paramètre, indexé par nom d'option : + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Clés disponibles : + +| `Parser::Repeatable` | Rassembler plusieurs valeurs dans un tableau +| `Parser::RealPath` | Vérifier que le fichier existe et le résoudre en chemin absolu +| `Parser::Normalizer` | Fonction de transformation `fn($value) => ...` +| `Parser::Default` | Valeur de repli (équivaut à `(default: x)` dans le texte d'aide) +| `Parser::Enum` | Tableau des valeurs autorisées + + +API fluide +========== + +Quand vous voulez plus de contrôle sur les définitions des options, utilisez l'API fluide avec les méthodes `addSwitch()`, `addOption()` et `addArgument()`. Cette approche donne accès à toutes les fonctionnalités, y compris les normalisateurs, les enums et le réglage fin de chaque paramètre : + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +Comme avec `addFromHelp()`, vous pouvez passer votre propre tableau à `parse()` pour les tests : + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Commutateurs, options et arguments +---------------------------------- + +Il existe trois types d'entrées en ligne de commande : + +**Les commutateurs** sont des drapeaux sans valeur, comme `--verbose` ou `-v`. Ils valent `true` quand ils sont présents, `null` sinon : + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (not used) → null +``` + +**Les options** acceptent une valeur, comme `--output file.txt`. La valeur peut être séparée par une espace ou par `=` : + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → throws an exception (value required) +// (not used) → null +``` + +Notez que l'option elle-même est toujours facultative : ne pas l'utiliser renvoie `null`. En revanche, quand elle est utilisée, la valeur est obligatoire par défaut. Mettez `optionalValue: true` pour autoriser l'option sans valeur (elle vaut alors `true`) : + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (not used) → null +``` + +Quand la même option est utilisée plusieurs fois sans `repeatable: true`, c'est la dernière valeur qui l'emporte : + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**Les arguments** sont des valeurs positionnelles sans tirets. Ils sont obligatoires par défaut. Mettez `optional: true` pour les rendre facultatifs : + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (not used) → throws an exception + +$parser->addArgument('output', optional: true); +// (not used) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (not used) → 'out.txt' +``` + +Utilisez `fallback` pour indiquer la valeur employée quand une option ou un argument facultatif n'est pas fourni. Pour les options avec `optionalValue: true`, notez qu'utiliser l'option sans valeur donne toujours `true`, tandis que le repli ne sert que si l'option est totalement absente : + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (option used without a value) +// (not used) → 'json' (fallback) +``` + +Les arguments peuvent apparaître n'importe où sur la ligne de commande, ils n'ont pas à suivre les options : + +```php +// all of these are equivalent: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Restreindre les valeurs avec enum +--------------------------------- + +Limitez les valeurs acceptées à un ensemble donné : + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → throws "Value of option --format must be json, or xml, or csv." +``` + + +Options répétables +------------------ + +Mettez `repeatable: true` pour rassembler plusieurs valeurs dans un tableau : + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (not used) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Transformer les valeurs +----------------------- + +Utilisez un `normalizer` pour transformer la valeur analysée : + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (integer) +``` + +Pour valider un chemin de fichier, utilisez le `normalizeRealPath` intégré : + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → throws "File path 'missing.ini' not found." +``` + + +Combiner les deux approches +--------------------------- + +Vous pouvez combiner `addFromHelp()` avec les méthodes fluides quand vous n'avez besoin de normalisateurs que pour certaines options : + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Gestion des erreurs +------------------- + +L'analyseur lève une `\Exception` en cas d'entrée invalide : + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Messages d'erreur courants : + +| `Option --output requires argument.` | Option utilisée sans sa valeur obligatoire +| `Unknown option --foo.` | Option non reconnue +| `Missing required argument <file>.` | Argument obligatoire non fourni +| `Unexpected parameter foo.` | Argument positionnel en trop +| `Value of option --format must be json, or xml.` | Valeur absente de l'enum + +Utilisez `isEmpty()` pour savoir si aucun argument n'a été fourni du tout (c'est-à-dire si l'utilisateur a lancé `script.php` sans rien après) : + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Traiter --help et --version +--------------------------- + +Quand votre script a des arguments obligatoires, lancer `script.php --help` échouerait normalement, faute de l'argument obligatoire. Utilisez `parseOnly()` pour vérifier d'abord les options d'information : + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // required + +// First, check the info options (no validation, no exceptions) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Now do the full parsing with validation +$args = $parser->parse(); +``` + +La méthode `parseOnly()` : +- n'analyse que les options indiquées, en ignorant tout le reste, +- respecte les alias (`-h` → `--help`), +- ne lève jamais d'exception, +- renvoie `null` pour les options non utilisées. + + +Sortie colorée +============== + +La classe [api:Nette\CommandLine\Console] enveloppe le texte dans des codes de couleur ANSI, pour que votre sortie ressorte dans le terminal : + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +La couleur s'indique sous la forme `'premier plan'` ou `'premier plan/arrière-plan'`. Les couleurs disponibles sont : `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` et `yellow`. + +Les couleurs ne s'activent automatiquement que si la sortie les prend en charge. La méthode `color()` renvoie une chaîne brute quand les couleurs sont désactivées, on peut donc toujours l'appeler sans risque. Vous pouvez forcer le comportement à la main : + +```php +$console->useColors(false); // disable colors +$console->useColors(true); // force colors on +``` + + +Détecter le terminal +-------------------- + +Deux méthodes statiques vous aident à décider s'il faut employer des fonctionnalités propres au terminal. `detectColors()` renvoie `false` quand la variable d'environnement [NO_COLOR |https://no-color.org] est définie, ou quand la sortie n'est pas un terminal CLI ; la variable `FORCE_COLOR` court-circuite ce test : + +```php +if (Console::detectColors()) { + // the terminal supports ANSI colors +} +``` + +`detectTerminal()` vous dit si la sortie est un terminal interactif (un TTY). C'est utile pour désactiver automatiquement les fonctionnalités qui n'ont de sens que dans un terminal, comme les indicateurs de progression, la réécriture de ligne ou les invites interactives : + +```php +if (Console::detectTerminal()) { + // output goes to an interactive terminal, not a file or pipe +} +``` + + +Exemple complet +=============== + +Voici un script de conversion de fichiers tiré du monde réel, combinant `Parser` et `Console` : + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// Handle --help before validation (avoids the "missing argument" error) +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... conversion logic here ... + +echo "Done!\n"; +``` + +Le script accepte des commandes comme : + +- `convert input.txt` - conversion avec les valeurs par défaut +- `convert -v --format xml input.txt` - mode détaillé, format XML +- `convert -o result.txt input.txt` - indiquer le fichier de sortie +- `convert --help` - afficher l'aide (fonctionne même sans le fichier d'entrée) + + +{{sitename: Documentation Nette}} diff --git a/command-line/fr/@left-menu.texy b/command-line/fr/@left-menu.texy new file mode 100644 index 0000000000..78d9a2a02e --- /dev/null +++ b/command-line/fr/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Introduction |@home] + + +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/command-line/it/@home.texy b/command-line/it/@home.texy new file mode 100644 index 0000000000..08d60a12fc --- /dev/null +++ b/command-line/it/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +Una libreria leggera per costruire applicazioni da riga di comando in PHP. Analizza switch, opzioni e argomenti posizionali e vi aiuta a produrre output colorato nel terminale con il supporto ANSI. + +Installazione: + +```shell +composer require nette/command-line +``` + +Richiede PHP versione 8.2 e supporta PHP fino alla 8.5. + + +Analisi degli argomenti da riga di comando +========================================== + +Ogni script CLI deve gestire argomenti come `--verbose`, `-o output.txt` oppure semplici nomi di file. La classe [api:Nette\CommandLine\Parser] offre il modo più rapido di cominciare: basta scrivere il vostro testo di aiuto e lasciare che il parser ne ricavi le definizioni delle opzioni: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +Ecco fatto. Il parser capisce che `--verbose` è uno switch, che `--output` richiede un valore e che `--format` ha un valore facoltativo con `json` come ripiego. Il vostro testo di aiuto resta allineato alle definizioni reali delle opzioni. + +Il metodo `parse()` restituisce un array associativo. Le chiavi corrispondono esattamente ai nomi delle opzioni come sono definiti, trattini compresi: + +```php +[ + '--help' => true, // oppure null se non è stata usata + '--verbose' => null, + '--output' => 'file.txt', // oppure null se non è stata usata + '--format' => 'json', // ripiego da (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +Per impostazione predefinita `parse()` legge da `$_SERVER['argv']`. Potete passare un array vostro, il che torna comodo per i test: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Sintassi del testo di aiuto +--------------------------- + +Il parser ricava le definizioni delle opzioni dal testo di aiuto formattato secondo queste regole: + +| `--verbose` | Switch (senza valore) +| `-v, --verbose` | Switch con alias breve +| `--output <file>`| Opzione con valore obbligatorio +| `--format [type]`| Opzione con valore facoltativo +| `(default: json)`| Imposta il valore di ripiego +| `<path>...` | Opzione ripetibile + +Ogni riga definisce un'opzione. I nomi delle opzioni devono essere separati dalle loro descrizioni da almeno due spazi. + + +Configurazione aggiuntiva +------------------------- + +Alcune impostazioni non si possono esprimere nel testo di aiuto. Passate come secondo parametro un array con chiavi corrispondenti ai nomi delle opzioni: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Chiavi disponibili: + +| `Parser::Repeatable` | Raccoglie più valori in un array +| `Parser::RealPath` | Verifica che il file esista e lo risolve in un percorso assoluto +| `Parser::Normalizer` | Funzione di trasformazione `fn($value) => ...` +| `Parser::Default` | Valore di ripiego (uguale a `(default: x)` nel testo di aiuto) +| `Parser::Enum` | Array dei valori consentiti + + +API fluent +========== + +Quando vi serve più controllo sulle definizioni delle opzioni, usate l'API fluent con i metodi `addSwitch()`, `addOption()` e `addArgument()`. Questo approccio vi dà accesso a tutte le funzionalità, compresi normalizzatori, enum e controllo preciso su ogni parametro: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +Come con `addFromHelp()`, potete passare a `parse()` un array vostro per i test: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Switch, opzioni e argomenti +--------------------------- + +Ci sono tre tipi di input da riga di comando: + +Gli **switch** sono flag senza valore, come `--verbose` oppure `-v`. Vengono analizzati come `true` quando sono presenti, come `null` quando mancano: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (non usato) → null +``` + +Le **opzioni** accettano valori, come `--output file.txt`. Il valore si può separare con uno spazio oppure con `=`: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → lancia un'eccezione (valore obbligatorio) +// (non usata) → null +``` + +Notate che l'opzione in sé è sempre facoltativa: non usarla restituisce `null`. Quando però viene usata, il valore è obbligatorio per impostazione predefinita. Impostate `optionalValue: true` per permettere l'opzione senza valore (viene allora analizzata come `true`): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (non usata) → null +``` + +Quando la stessa opzione viene usata più volte senza `repeatable: true`, vince l'ultimo valore: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +Gli **argomenti** sono valori posizionali senza trattini. Per impostazione predefinita sono obbligatori. Impostate `optional: true` per renderli facoltativi: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (non usato) → lancia un'eccezione + +$parser->addArgument('output', optional: true); +// (non usato) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (non usato) → 'out.txt' +``` + +Usate `fallback` per indicare il valore usato quando un'opzione o un argomento facoltativo non viene fornito. Per le opzioni con `optionalValue: true` tenete presente che usare l'opzione senza valore viene comunque analizzato come `true`, mentre il ripiego si usa solo quando l'opzione non è presente affatto: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (opzione usata senza valore) +// (non usata) → 'json' (ripiego) +``` + +Gli argomenti possono comparire in qualsiasi punto della riga di comando, non devono per forza venire dopo le opzioni: + +```php +// tutte queste forme sono equivalenti: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Limitare i valori con enum +-------------------------- + +Limitate i valori accettati a un insieme determinato: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → lancia "Value of option --format must be json, or xml, or csv." +``` + + +Opzioni ripetibili +------------------ + +Impostate `repeatable: true` per raccogliere più valori in un array: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (non usata) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Trasformare i valori +-------------------- + +Usate un `normalizer` per trasformare il valore analizzato: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (intero) +``` + +Per validare i percorsi dei file usate il `normalizeRealPath` integrato: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → lancia "File path 'missing.ini' not found." +``` + + +Combinare i due approcci +------------------------ + +Potete combinare `addFromHelp()` con i metodi fluent quando vi servono i normalizzatori solo per alcune opzioni: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Gestione degli errori +--------------------- + +Il parser lancia `\Exception` per gli input non validi: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Messaggi di errore più frequenti: + +| `Option --output requires argument.` | Opzione usata senza il valore obbligatorio +| `Unknown option --foo.` | Opzione non riconosciuta +| `Missing required argument <file>.` | Argomento obbligatorio non fornito +| `Unexpected parameter foo.` | Argomento posizionale in più +| `Value of option --format must be json, or xml.` | Valore non presente nell'enum + +Usate `isEmpty()` per verificare se non è stato fornito alcun argomento da riga di comando (cioè se l'utente ha lanciato solo `script.php` senza nulla dopo): + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Gestire --help e --version +-------------------------- + +Quando il vostro script ha argomenti obbligatori, lanciare `script.php --help` fallirebbe normalmente perché manca l'argomento obbligatorio. Usate `parseOnly()` per controllare prima le opzioni informative: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // obbligatorio + +// prima controlliamo le opzioni informative (nessuna validazione, nessuna eccezione) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// ora facciamo l'analisi completa con la validazione +$args = $parser->parse(); +``` + +Il metodo `parseOnly()`: +- analizza solo le opzioni indicate, ignorando tutto il resto, +- rispetta gli alias (`-h` → `--help`), +- non lancia mai eccezioni, +- restituisce `null` per le opzioni che non sono state usate. + + +Output colorato +=============== + +La classe [api:Nette\CommandLine\Console] racchiude il testo nei codici colore ANSI, così il vostro output spicca nel terminale: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +Il colore si indica come `'primo piano'` oppure `'primo piano/sfondo'`. I colori disponibili sono: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` e `yellow`. + +I colori si attivano automaticamente solo quando l'output li supporta. Il metodo `color()` restituisce una stringa semplice quando i colori sono disattivati, quindi si può sempre chiamare senza rischi. Il comportamento lo potete forzare a mano: + +```php +$console->useColors(false); // disattiva i colori +$console->useColors(true); // forza i colori +``` + + +Rilevare il terminale +--------------------- + +Due metodi statici vi aiutano a decidere se usare funzionalità legate al terminale. `detectColors()` restituisce `false` quando è impostata la variabile d'ambiente [NO_COLOR |https://no-color.org], oppure quando l'output non è un terminale CLI; la variabile `FORCE_COLOR` ha la precedenza sul controllo del terminale: + +```php +if (Console::detectColors()) { + // il terminale supporta i colori ANSI +} +``` + +`detectTerminal()` vi dice se l'output è un terminale interattivo (un TTY). Torna utile per disattivare automaticamente le funzionalità che hanno senso solo in un terminale, come gli indicatori di avanzamento, l'output che riscrive la riga o le richieste interattive: + +```php +if (Console::detectTerminal()) { + // l'output va a un terminale interattivo, non a un file o a una pipe +} +``` + + +Esempio completo +================ + +Ecco uno script reale di conversione file che combina `Parser` e `Console`: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// gestiamo --help prima della validazione (evita l'errore "missing argument") +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... qui la logica di conversione ... + +echo "Done!\n"; +``` + +Lo script accetta comandi come: + +- `convert input.txt` - conversione con i valori predefiniti +- `convert -v --format xml input.txt` - modalità verbose, formato XML +- `convert -o result.txt input.txt` - indica il file di output +- `convert --help` - mostra l'aiuto (funziona anche senza il file di input) + + +{{sitename: Documentazione Nette}} diff --git a/command-line/it/@left-menu.texy b/command-line/it/@left-menu.texy new file mode 100644 index 0000000000..3366a83f9f --- /dev/null +++ b/command-line/it/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Panoramica |@home] + + +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/command-line/ja/@home.texy b/command-line/ja/@home.texy new file mode 100644 index 0000000000..8948d945c8 --- /dev/null +++ b/command-line/ja/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +PHP でコマンドラインのアプリケーションを作るための軽いライブラリです。スイッチ、オプション、位置の引数を読み解き、ANSI に対応した色付きの端末への出力を作るのを助けます。 + +インストール: + +```shell +composer require nette/command-line +``` + +PHP のバージョン 8.2 が要り、PHP 8.5 まで対応しています。 + + +コマンドラインの引数を読み解く +=============== + +どの CLI のスクリプトも、`--verbose`、`-o output.txt`、あるいはただのファイル名といった引数を扱う必要があります。[api:Nette\CommandLine\Parser]クラスは、いちばん手早く始める方法を差し出します。ヘルプの文を書けば、そこからオプションの定義を読み取ってくれるのです。 + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +これだけです。この読み取り器は、`--verbose` がスイッチで、`--output` が値を求め、`--format` は省略できる値を持ち、`json` が代わりの値であることを理解します。ヘルプの文は、実際のオプションの定義と食い違いません。 + +`parse()` メソッドは連想配列を返します。キーは、ダッシュも含めて定義したとおりのオプションの名前と一致します。 + +```php +[ + '--help' => true, // 使われなければ null + '--verbose' => null, + '--output' => 'file.txt', // 使われなければ null + '--format' => 'json', // (default: json) からの代わりの値 + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +既定では `parse()` は `$_SERVER['argv']` から読みます。独自の配列も渡せて、テストに便利です。 + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +ヘルプの文の書き方 +--------- + +読み取り器は、整えられたヘルプの文から次の決まりに沿ってオプションの定義を取り出します。 + +| `--verbose` | スイッチ(値なし) +| `-v, --verbose` | 短い別名の付いたスイッチ +| `--output <file>`| 値が必須のオプション +| `--format [type]`| 値を省略できるオプション +| `(default: json)`| 代わりの値を決めます +| `<path>...` | 繰り返せるオプション + +それぞれの行がひとつのオプションを定めます。オプションの名前は、説明と少なくとも空白 2 つで分けなければなりません。 + + +追加の設定 +----- + +ヘルプの文では表せない設定もあります。第 2 パラメータに、オプションの名前をキーにした配列を渡します。 + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +使えるキーです。 + +| `Parser::Repeatable` | 複数の値を配列に集めます +| `Parser::RealPath` | ファイルが存在するかを確かめ、絶対パスに解決します +| `Parser::Normalizer` | 変換の関数 `fn($value) => ...` +| `Parser::Default` | 代わりの値(ヘルプの文の `(default: x)` と同じ) +| `Parser::Enum` | 許される値の配列 + + +fluent な API +============ + +オプションの定義をもっと思いどおりにしたいなら、`addSwitch()`、`addOption()`、`addArgument()` のメソッドによる fluent な API を使います。このやり方なら、正規化器、enum、そしてそれぞれのパラメータの細かな制御も含め、すべての機能を使えます。 + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +`addFromHelp()` と同じく、テストのために `parse()` へ独自の配列を渡せます。 + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +スイッチ、オプション、引数 +------------- + +コマンドラインの入力には 3 つの種類があります。 + +**スイッチ** は `--verbose` や `-v` のような値のない旗です。あれば `true`、なければ `null` と読み解かれます。 + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (使われない) → null +``` + +**オプション** は `--output file.txt` のように値を受け取ります。値は空白でも `=` でも区切れます。 + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → 例外を投げます(値が必須) +// (使われない) → null +``` + +オプションそのものはいつも省略できて、使わなければ `null` が返ることに注意してください。とはいえ使うなら、既定では値が必須です。値なしのオプションを許すには `optionalValue: true` にします(その場合 `true` と読み解かれます)。 + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (使われない) → null +``` + +同じオプションが `repeatable: true` なしで何度も使われた場合、最後の値が勝ちます。 + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**引数** はダッシュのない位置の値です。既定では必須です。省略できるようにするには `optional: true` にします。 + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (使われない) → 例外を投げます + +$parser->addArgument('output', optional: true); +// (使われない) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (使われない) → 'out.txt' +``` + +省略できるオプションや引数が渡されなかったときに使う値は `fallback` で決めます。`optionalValue: true` のオプションでは、値なしでそのオプションを使うとやはり `true` と読み解かれ、fallback はそのオプションがまったくないときにだけ使われることに注意してください。 + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true(値なしでオプションを使った場合) +// (使われない) → 'json'(fallback) +``` + +引数はコマンドラインのどこにでも書けます。オプションのうしろでなくてもかまいません。 + +```php +// 次はどれも同じです: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +enum で値を制限する +------------ + +受け入れる値を決まった一そろいに限れます。 + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → "Value of option --format must be json, or xml, or csv." を投げます +``` + + +繰り返せるオプション +---------- + +`repeatable: true` にすると、複数の値を配列に集めます。 + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (使われない) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +値を変換する +------ + +読み解いた値を変えるには `normalizer` を使います。 + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42(整数) +``` + +ファイルのパスの検証には、組み込みの `normalizeRealPath` を使います。 + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → "File path 'missing.ini' not found." を投げます +``` + + +両方のやり方を混ぜる +---------- + +一部のオプションにだけ正規化器が必要なら、`addFromHelp()` と fluent なメソッドを組み合わせられます。 + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +エラーの処理 +------ + +読み取り器は、正しくない入力に対して `\Exception` を投げます。 + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +よくあるエラーのメッセージです。 + +| `Option --output requires argument.` | 必須の値なしでオプションが使われました +| `Unknown option --foo.` | 知らないオプション +| `Missing required argument <file>.` | 必須の引数が渡されていません +| `Unexpected parameter foo.` | 余分な位置の引数 +| `Value of option --format must be json, or xml.` | 値が enum にありません + +コマンドラインの引数がまったく渡されなかったか(つまり利用者が `script.php` のうしろに何も付けずに走らせたか)は `isEmpty()` で調べられます。 + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +--help と --version を扱う +---------------------- + +スクリプトに必須の引数があると、`script.php --help` を走らせても必須の引数がないのでふつうは失敗します。まず情報のオプションを調べるには `parseOnly()` を使います。 + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // 必須 + +// まず情報のオプションを調べます(検証も例外もありません) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// そのあと検証付きの完全な読み取りを行います +$args = $parser->parse(); +``` + +`parseOnly()` メソッドは次のように働きます。 +- 指定されたオプションだけを読み解き、ほかはすべて無視します +- 別名を認めます(`-h` → `--help`) +- 例外を決して投げません +- 使われなかったオプションには `null` を返します + + +色付きの出力 +====== + +[api:Nette\CommandLine\Console]クラスは、出力が端末で目立つように、文を ANSI の色のコードで包みます。 + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +色は `'前景'` か `'前景/背景'` の形で渡します。使える色は `black`、`gray`、`silver`、`white`、`navy`、`blue`、`green`、`lime`、`teal`、`aqua`、`maroon`、`red`、`purple`、`fuchsia`、`olive`、`yellow` です。 + +色は、出力がそれに対応しているときにだけ自動的に有効になります。色が切られているとき `color()` メソッドはただの文字列を返すので、いつでも安全に呼べます。振る舞いは手で決められます。 + +```php +$console->useColors(false); // 色を切ります +$console->useColors(true); // 色を強制します +``` + + +端末を見分ける +------- + +端末でだけ意味のある機能を使うかどうかを決めるのに、2 つの静的メソッドが役立ちます。`detectColors()` は、[NO_COLOR |https://no-color.org]の環境変数が設定されているときや、出力が CLI の端末でないときに `false` を返します。`FORCE_COLOR` の変数は端末の判別を上書きします。 + +```php +if (Console::detectColors()) { + // 端末は ANSI の色に対応しています +} +``` + +`detectTerminal()` は、出力が対話的な端末(TTY)かどうかを教えてくれます。進み具合の表示、行を書き直す出力、対話的な問いかけのように、端末でだけ意味のある機能を自動的に切るのに役立ちます。 + +```php +if (Console::detectTerminal()) { + // 出力はファイルやパイプではなく、対話的な端末へ向かっています +} +``` + + +完全な例 +==== + +`Parser` と `Console` を組み合わせた、実際に使えるファイルの変換のスクリプトです。 + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// 検証の前に --help を扱います(「引数がない」のエラーを避けます) +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... ここに変換の論理 ... + +echo "Done!\n"; +``` + +このスクリプトは次のようなコマンドを受け取ります。 + +- `convert input.txt` - 既定の設定で変換します +- `convert -v --format xml input.txt` - 詳しい出力、XML の形式 +- `convert -o result.txt input.txt` - 出力のファイルを指定します +- `convert --help` - ヘルプを表示します(入力のファイルがなくても働きます) + + +{{sitename: Nette ドキュメント}} diff --git a/command-line/ja/@left-menu.texy b/command-line/ja/@left-menu.texy new file mode 100644 index 0000000000..46613227aa --- /dev/null +++ b/command-line/ja/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [概要 |@home] + + +関連情報 +**** +- [Nette ドキュメント |nette:] +- [Nette Application |application:how-it-works] +- [Utilities |utils:] +- [ベストプラクティス |best-practices:] +- [トラブルシューティング |nette:troubleshooting] diff --git a/command-line/meta.json b/command-line/meta.json new file mode 100644 index 0000000000..8548be517a --- /dev/null +++ b/command-line/meta.json @@ -0,0 +1,5 @@ +{ + "version": "1.x", + "repo": "nette/command-line", + "composer": "nette/command-line" +} diff --git a/command-line/pl/@home.texy b/command-line/pl/@home.texy new file mode 100644 index 0000000000..fdebc06732 --- /dev/null +++ b/command-line/pl/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +Lekka biblioteka do budowania aplikacji wiersza poleceń w PHP. Parsuje przełączniki, opcje i argumenty pozycyjne oraz pomaga tworzyć kolorowe wyjście terminala ze wsparciem ANSI. + +Instalacja: + +```shell +composer require nette/command-line +``` + +Wymaga PHP w wersji 8.2 i wspiera PHP do 8.5. + + +Parsowanie argumentów wiersza poleceń +===================================== + +Każdy skrypt CLI musi obsłużyć argumenty w rodzaju `--verbose`, `-o output.txt` albo zwykłe nazwy plików. Klasa [api:Nette\CommandLine\Parser] daje najszybszy sposób na start: wystarczy napisać tekst pomocy i pozwolić parserowi wyciągnąć z niego definicje opcji: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +I to wszystko. Parser rozumie, że `--verbose` to przełącznik, `--output` wymaga wartości, a `--format` ma wartość opcjonalną z `json` jako wartością zapasową. Twój tekst pomocy pozostaje zsynchronizowany z faktycznymi definicjami opcji. + +Metoda `parse()` zwraca tablicę asocjacyjną. Klucze odpowiadają dokładnie nazwom opcji tak, jak zostały zdefiniowane, wraz z myślnikami: + +```php +[ + '--help' => true, // albo null, jeśli nieużyte + '--verbose' => null, + '--output' => 'file.txt', // albo null, jeśli nieużyte + '--format' => 'json', // wartość zapasowa z (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +Domyślnie `parse()` czyta z `$_SERVER['argv']`. Możesz przekazać własną tablicę, co przydaje się przy testowaniu: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Składnia tekstu pomocy +---------------------- + +Parser wyciąga definicje opcji ze sformatowanego tekstu pomocy według tych reguł: + +| `--verbose` | Przełącznik (bez wartości) +| `-v, --verbose` | Przełącznik z krótkim aliasem +| `--output <file>`| Opcja z wymaganą wartością +| `--format [type]`| Opcja z wartością opcjonalną +| `(default: json)`| Ustawia wartość zapasową +| `<path>...` | Opcja powtarzalna + +Każda linia definiuje jedną opcję. Nazwy opcji muszą być oddzielone od swoich opisów co najmniej dwiema spacjami. + + +Dodatkowa konfiguracja +---------------------- + +Niektórych ustawień nie da się wyrazić w tekście pomocy. Przekaż jako drugi parametr tablicę kluczowaną nazwą opcji: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Dostępne klucze: + +| `Parser::Repeatable` | Zbiera wiele wartości do tablicy +| `Parser::RealPath` | Weryfikuje, że plik istnieje, i rozwiązuje go do ścieżki absolutnej +| `Parser::Normalizer` | Funkcja przekształcająca `fn($value) => ...` +| `Parser::Default` | Wartość zapasowa (to samo co `(default: x)` w tekście pomocy) +| `Parser::Enum` | Tablica dozwolonych wartości + + +Interfejs płynny +================ + +Gdy potrzebujesz większej kontroli nad definicjami opcji, użyj interfejsu płynnego z metodami `addSwitch()`, `addOption()` i `addArgument()`. To podejście daje Ci dostęp do wszystkich funkcji, wraz z normalizatorami, enumami i precyzyjną kontrolą nad każdym parametrem: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +Podobnie jak przy `addFromHelp()` możesz przekazać do `parse()` własną tablicę na potrzeby testowania: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Przełączniki, opcje i argumenty +------------------------------- + +Są trzy typy wejść wiersza poleceń: + +**Przełączniki** to flagi bez wartości, jak `--verbose` czy `-v`. Parsują się jako `true`, gdy są obecne, i `null`, gdy ich nie ma: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (nieużyte) → null +``` + +**Opcje** przyjmują wartości, jak `--output file.txt`. Wartość można oddzielić spacją albo znakiem `=`: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → rzuca wyjątek (wartość wymagana) +// (nieużyte) → null +``` + +Zwróć uwagę, że sama opcja jest zawsze opcjonalna: jej nieużycie zwraca `null`. Gdy jednak zostanie użyta, wartość jest domyślnie wymagana. Ustaw `optionalValue: true`, żeby dopuścić opcję bez wartości (parsuje się wtedy jako `true`): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (nieużyte) → null +``` + +Gdy ta sama opcja użyta jest wielokrotnie bez `repeatable: true`, wygrywa ostatnia wartość: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**Argumenty** to wartości pozycyjne bez myślników. Domyślnie są wymagane. Ustaw `optional: true`, żeby uczynić je opcjonalnymi: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (nieużyte) → rzuca wyjątek + +$parser->addArgument('output', optional: true); +// (nieużyte) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (nieużyte) → 'out.txt' +``` + +Za pomocą `fallback` podajesz wartość używaną, gdy opcjonalna opcja albo argument nie zostaną podane. Przy opcjach z `optionalValue: true` zwróć uwagę, że użycie opcji bez wartości nadal parsuje się jako `true`, a wartość zapasowa używana jest tylko wtedy, gdy opcji w ogóle nie ma: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (opcja użyta bez wartości) +// (nieużyte) → 'json' (wartość zapasowa) +``` + +Argumenty mogą pojawić się w wierszu poleceń w dowolnym miejscu, nie muszą występować po opcjach: + +```php +// wszystkie te warianty są równoważne: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Ograniczanie wartości enumem +---------------------------- + +Ogranicz przyjmowane wartości do konkretnego zbioru: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → rzuca "Value of option --format must be json, or xml, or csv." +``` + + +Opcje powtarzalne +----------------- + +Ustaw `repeatable: true`, żeby zbierać wiele wartości do tablicy: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (nieużyte) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Przekształcanie wartości +------------------------ + +Użyj `normalizer`, żeby przekształcić sparsowaną wartość: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (liczba całkowita) +``` + +Do walidacji ścieżek plików użyj wbudowanego `normalizeRealPath`: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → rzuca "File path 'missing.ini' not found." +``` + + +Łączenie obu podejść +-------------------- + +Możesz połączyć `addFromHelp()` z metodami płynnymi, gdy potrzebujesz normalizatorów tylko dla niektórych opcji: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Obsługa błędów +-------------- + +Parser rzuca `\Exception` przy nieprawidłowym wejściu: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Typowe komunikaty o błędach: + +| `Option --output requires argument.` | Opcja użyta bez wymaganej wartości +| `Unknown option --foo.` | Nierozpoznana opcja +| `Missing required argument <file>.` | Nie podano wymaganego argumentu +| `Unexpected parameter foo.` | Nadmiarowy argument pozycyjny +| `Value of option --format must be json, or xml.` | Wartość spoza enuma + +Użyj `isEmpty()`, żeby sprawdzić, czy w ogóle nie podano żadnych argumentów wiersza poleceń (czyli użytkownik uruchomił sam `script.php` bez niczego dalej): + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Obsługa --help i --version +-------------------------- + +Gdy Twój skrypt ma wymagane argumenty, uruchomienie `script.php --help` normalnie by zawiodło, bo brakuje wymaganego argumentu. Użyj `parseOnly()`, żeby najpierw sprawdzić opcje informacyjne: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // wymagany + +// Najpierw sprawdzamy opcje informacyjne (bez walidacji, bez wyjątków) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Teraz przeprowadzamy pełne parsowanie z walidacją +$args = $parser->parse(); +``` + +Metoda `parseOnly()`: +- parsuje tylko podane opcje, ignorując całą resztę, +- respektuje aliasy (`-h` → `--help`), +- nigdy nie rzuca wyjątków, +- zwraca `null` dla opcji, które nie zostały użyte. + + +Kolorowe wyjście +================ + +Klasa [api:Nette\CommandLine\Console] opakowuje tekst w kody kolorów ANSI, żeby Twoje wyjście wyróżniało się w terminalu: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +Kolor podaje się jako `'pierwszy plan'` albo `'pierwszy plan/tło'`. Dostępne kolory to: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` i `yellow`. + +Kolory włączane są automatycznie tylko wtedy, gdy wyjście je wspiera. Metoda `color()` zwraca przy wyłączonych kolorach zwykły ciąg, więc jej wywołanie jest zawsze bezpieczne. Zachowanie możesz wymusić ręcznie: + +```php +$console->useColors(false); // wyłącza kolory +$console->useColors(true); // wymusza włączenie kolorów +``` + + +Wykrywanie terminala +-------------------- + +Dwie metody statyczne pomagają Ci zdecydować, czy używać funkcji dostępnych tylko w terminalu. `detectColors()` zwraca `false`, gdy ustawiona jest zmienna środowiskowa [NO_COLOR |https://no-color.org] albo gdy wyjście nie jest terminalem CLI; zmienna `FORCE_COLOR` nadpisuje sprawdzenie terminala: + +```php +if (Console::detectColors()) { + // terminal wspiera kolory ANSI +} +``` + +`detectTerminal()` mówi Ci, czy wyjście jest interaktywnym terminalem (TTY). Przydaje się to do automatycznego wyłączania funkcji, które mają sens tylko w terminalu, jak wskaźniki postępu, wyjście przepisujące linie czy interaktywne pytania: + +```php +if (Console::detectTerminal()) { + // wyjście trafia do interaktywnego terminala, a nie do pliku czy potoku +} +``` + + +Kompletny przykład +================== + +Oto rzeczywisty skrypt konwertujący pliki, łączący `Parser` i `Console`: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// Obsługujemy --help przed walidacją (unikamy błędu "missing argument") +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... tutaj logika konwersji ... + +echo "Done!\n"; +``` + +Skrypt przyjmuje polecenia takie jak: + +- `convert input.txt` - konwersja z wartościami domyślnymi +- `convert -v --format xml input.txt` - tryb verbose, format XML +- `convert -o result.txt input.txt` - podanie pliku wyjściowego +- `convert --help` - wyświetlenie pomocy (działa nawet bez pliku wejściowego) + + +{{sitename: Dokumentacja Nette}} diff --git a/command-line/pl/@left-menu.texy b/command-line/pl/@left-menu.texy new file mode 100644 index 0000000000..30345e4de4 --- /dev/null +++ b/command-line/pl/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Przegląd |@home] + + +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/command-line/ru/@home.texy b/command-line/ru/@home.texy new file mode 100644 index 0000000000..567a53620a --- /dev/null +++ b/command-line/ru/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +Лёгкая библиотека для создания приложений командной строки на PHP. Она разбирает переключатели, параметры и позиционные аргументы и помогает выводить в терминал цветной текст с поддержкой ANSI. + +Установка: + +```shell +composer require nette/command-line +``` + +Она требует PHP версии 8.2 и поддерживает PHP вплоть до 8.5. + + +Разбор аргументов командной строки +================================== + +Любому CLI-скрипту нужно обрабатывать аргументы вроде `--verbose`, `-o output.txt` или просто имена файлов. Класс [api:Nette\CommandLine\Parser] даёт самый быстрый способ начать: просто напишите текст справки и дайте парсеру извлечь из него определения параметров: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +Вот и всё. Парсер понимает, что `--verbose` - переключатель, `--output` требует значения, а у `--format` значение необязательно и по умолчанию равно `json`. Ваш текст справки остаётся согласованным с фактическими определениями параметров. + +Метод `parse()` возвращает ассоциативный массив. Ключи в точности совпадают с именами параметров, как они определены, включая дефисы: + +```php +[ + '--help' => true, // либо null, если не использован + '--verbose' => null, + '--output' => 'file.txt', // либо null, если не использован + '--format' => 'json', // значение из (default: json) + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +По умолчанию `parse()` читает из `$_SERVER['argv']`. Можно передать собственный массив, что удобно для тестирования: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Синтаксис текста справки +------------------------ + +Парсер извлекает определения параметров из отформатированного текста справки по таким правилам: + +| `--verbose` | Переключатель (без значения) +| `-v, --verbose` | Переключатель с коротким синонимом +| `--output <file>`| Параметр с обязательным значением +| `--format [type]`| Параметр с необязательным значением +| `(default: json)`| Задаёт значение по умолчанию +| `<path>...` | Повторяемый параметр + +Каждая строка определяет один параметр. Имена параметров должны отделяться от описаний хотя бы двумя пробелами. + + +Дополнительная настройка +------------------------ + +Некоторые настройки в тексте справки не выразить. Передайте вторым параметром массив с ключами по именам параметров: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Доступные ключи: + +| `Parser::Repeatable` | Собирать несколько значений в массив +| `Parser::RealPath` | Проверить, что файл существует, и привести путь к абсолютному +| `Parser::Normalizer` | Преобразующая функция `fn($value) => ...` +| `Parser::Default` | Значение по умолчанию (то же, что `(default: x)` в тексте справки) +| `Parser::Enum` | Массив допустимых значений + + +Текучий API +=========== + +Когда вам нужно больше контроля над определениями параметров, используйте текучий API с методами `addSwitch()`, `addOption()` и `addArgument()`. Такой подход даёт доступ ко всем возможностям, включая нормализаторы, перечисления и точное управление каждым параметром: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +Как и в случае `addFromHelp()`, в `parse()` для тестирования можно передать собственный массив: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Переключатели, параметры и аргументы +------------------------------------ + +Есть три вида ввода в командной строке: + +**Переключатели** - флаги без значений вроде `--verbose` или `-v`. При наличии они разбираются как `true`, при отсутствии - как `null`: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (не использован) → null +``` + +**Параметры** принимают значения, например `--output file.txt`. Значение может отделяться пробелом или знаком `=`: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → выбросит исключение (значение обязательно) +// (не использован) → null +``` + +Обратите внимание, что сам параметр всегда необязателен: если его не использовать, вернётся `null`. Однако когда он использован, значение по умолчанию обязательно. Задайте `optionalValue: true`, чтобы разрешить параметр без значения (тогда он разбирается как `true`): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (не использован) → null +``` + +Когда один и тот же параметр использован несколько раз без `repeatable: true`, побеждает последнее значение: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**Аргументы** - позиционные значения без дефисов. По умолчанию они обязательны. Задайте `optional: true`, чтобы сделать их необязательными: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (не использован) → выбросит исключение + +$parser->addArgument('output', optional: true); +// (не использован) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (не использован) → 'out.txt' +``` + +Через `fallback` задаётся значение, используемое, когда необязательный параметр или аргумент не указан. У параметров с `optionalValue: true` учтите, что использование параметра без значения всё равно разбирается как `true`, а fallback используется, только когда параметра нет вовсе: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (параметр использован без значения) +// (не использован) → 'json' (fallback) +``` + +Аргументы могут находиться в командной строке где угодно, им не обязательно идти после параметров: + +```php +// всё это равносильно: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Ограничение значений перечислением +---------------------------------- + +Ограничьте принимаемые значения определённым набором: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → выбросит "Value of option --format must be json, or xml, or csv." +``` + + +Повторяемые параметры +--------------------- + +Задайте `repeatable: true`, чтобы собирать несколько значений в массив: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (не использован) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Преобразование значений +----------------------- + +Используйте `normalizer`, чтобы преобразовать разобранное значение: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (целое число) +``` + +Для проверки пути к файлу используйте встроенный `normalizeRealPath`: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → выбросит "File path 'missing.ini' not found." +``` + + +Сочетание обоих подходов +------------------------ + +`addFromHelp()` можно сочетать с текучими методами, когда нормализаторы нужны лишь для некоторых параметров: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Обработка ошибок +---------------- + +При некорректном вводе парсер выбрасывает `\Exception`: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Частые сообщения об ошибках: + +| `Option --output requires argument.` | Параметр использован без обязательного значения +| `Unknown option --foo.` | Нераспознанный параметр +| `Missing required argument <file>.` | Не указан обязательный аргумент +| `Unexpected parameter foo.` | Лишний позиционный аргумент +| `Value of option --format must be json, or xml.` | Значение не входит в перечисление + +Используйте `isEmpty()`, чтобы проверить, не были ли аргументы командной строки вообще не заданы (то есть пользователь запустил просто `script.php` и ничего после него): + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +Обработка --help и --version +---------------------------- + +Когда у вашего скрипта есть обязательные аргументы, запуск `script.php --help` обычно завершился бы ошибкой, потому что обязательный аргумент отсутствует. Используйте `parseOnly()`, чтобы сначала проверить информационные параметры: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // обязательный + +// Сначала проверяем информационные параметры (без проверок, без исключений) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Теперь выполняем полный разбор с проверками +$args = $parser->parse(); +``` + +Метод `parseOnly()`: +- разбирает только указанные параметры, всё остальное игнорирует, +- учитывает синонимы (`-h` → `--help`), +- никогда не выбрасывает исключений, +- возвращает `null` для неиспользованных параметров. + + +Цветной вывод +============= + +Класс [api:Nette\CommandLine\Console] оборачивает текст в цветовые коды ANSI, чтобы ваш вывод выделялся в терминале: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +Цвет задаётся как `'передний план'` или `'передний план/фон'`. Доступные цвета: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` и `yellow`. + +Цвета включаются автоматически, только если вывод их поддерживает. При отключённых цветах метод `color()` возвращает обычную строку, так что вызывать его всегда безопасно. Поведение можно задать и вручную: + +```php +$console->useColors(false); // отключить цвета +$console->useColors(true); // принудительно включить цвета +``` + + +Определение терминала +--------------------- + +Два статических метода помогают решить, использовать ли возможности, доступные только в терминале. `detectColors()` возвращает `false`, когда задана переменная окружения [NO_COLOR |https://no-color.org] либо когда вывод не является CLI-терминалом; переменная `FORCE_COLOR` перебивает проверку терминала: + +```php +if (Console::detectColors()) { + // терминал поддерживает цвета ANSI +} +``` + +`detectTerminal()` говорит, является ли вывод интерактивным терминалом (TTY). Это удобно для автоматического отключения возможностей, которые имеют смысл только в терминале: индикаторов выполнения, вывода с перезаписью строки или интерактивных вопросов: + +```php +if (Console::detectTerminal()) { + // вывод идёт в интерактивный терминал, а не в файл или канал +} +``` + + +Полный пример +============= + +Вот настоящий скрипт-конвертер файлов, сочетающий `Parser` и `Console`: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// Обрабатываем --help до проверок (избегаем ошибки "missing argument") +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... здесь логика преобразования ... + +echo "Done!\n"; +``` + +Скрипт принимает команды вроде: + +- `convert input.txt` - преобразовать со значениями по умолчанию +- `convert -v --format xml input.txt` - подробный вывод, формат XML +- `convert -o result.txt input.txt` - указать файл вывода +- `convert --help` - показать справку (работает даже без входного файла) + + +{{sitename: Документация Nette}} diff --git a/command-line/ru/@left-menu.texy b/command-line/ru/@left-menu.texy new file mode 100644 index 0000000000..32cca385ee --- /dev/null +++ b/command-line/ru/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Обзор |@home] + + +Дополнительные материалы +************************ +- [Документация Nette |nette:] +- [Nette Application |application:how-it-works] +- [Utilities |utils:] +- [Лучшие практики |best-practices:] +- [Устранение неполадок |nette:troubleshooting] diff --git a/command-line/tr/@home.texy b/command-line/tr/@home.texy new file mode 100644 index 0000000000..149c43341e --- /dev/null +++ b/command-line/tr/@home.texy @@ -0,0 +1,445 @@ +Nette Command-Line +****************** + +.[perex] +PHP'de komut satırı uygulamaları kurmak için hafif bir kütüphane. Anahtarları, seçenekleri ve konumsal argümanları ayrıştırır ve ANSI desteğiyle renkli terminal çıktısı üretmenize yardım eder. + +Kurulum: + +```shell +composer require nette/command-line +``` + +PHP 8.2 sürümünü gerektirir ve PHP 8.5'e dek destekler. + + +Komut Satırı Argümanlarını Ayrıştırma +===================================== + +Her CLI betiğinin `--verbose`, `-o output.txt` ya da düz dosya adları gibi argümanları ele alması gerekir. [api:Nette\CommandLine\Parser] sınıfı başlamanın en hızlı yolunu sunar: yardım metninizi yazın ve ayrıştırıcının seçenek tanımlarını ondan çıkarmasına izin verin: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser->addFromHelp(' + -h, --help Show this help + -v, --verbose Enable verbose mode + -o, --output <file> Output file + -f, --format [type] Output format (default: json) + -I, --include <path>... Include paths + --dry-run Show what would be done +'); + +$args = $parser->parse(); +``` + +Hepsi bu. Ayrıştırıcı, `--verbose` seçeneğinin bir anahtar olduğunu, `--output` seçeneğinin bir değer gerektirdiğini ve `--format` seçeneğinin `json` yedekli isteğe bağlı bir değeri olduğunu anlar. Yardım metniniz gerçek seçenek tanımlarıyla eşzamanlı kalır. + +`parse()` metodu ilişkisel bir dizi döndürür. Anahtarlar, tirelerle birlikte, tanımlandıkları gibi seçenek adlarıyla tam olarak eşleşir: + +```php +[ + '--help' => true, // ya da kullanılmadıysa null + '--verbose' => null, + '--output' => 'file.txt', // ya da kullanılmadıysa null + '--format' => 'json', // (default: json) yedeği + '--include' => ['src', 'lib'], + '--dry-run' => null, +] +``` + +`parse()` varsayılan olarak `$_SERVER['argv']` içinden okur. Sınamalarda kullanışlı olan özel bir dizi aktarabilirsiniz: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt']); +``` + + +Yardım Metni Sözdizimi +---------------------- + +Ayrıştırıcı, biçimlendirilmiş yardım metninden seçenek tanımlarını şu kurallara göre çıkarır: + +| `--verbose` | Anahtar (değersiz) +| `-v, --verbose` | Kısa alias'lı anahtar +| `--output <file>`| Zorunlu değerli seçenek +| `--format [type]`| İsteğe bağlı değerli seçenek +| `(default: json)`| Yedek değeri belirler +| `<path>...` | Yinelenebilir seçenek + +Her satır bir seçenek tanımlar. Seçenek adları, açıklamalarından en az iki boşlukla ayrılmalıdır. + + +Ek Yapılandırma +--------------- + +Bazı ayarlar yardım metninde ifade edilemez. İkinci parametre olarak, seçenek adına göre anahtarlanmış bir dizi aktarın: + +```php +$parser->addFromHelp(' + -c, --config <file> Configuration file + -I, --include <path> Include path + -n, --count <num> Number of iterations +', [ + '--config' => [ + Parser::RealPath => true, + ], + '--include' => [ + Parser::Repeatable => true, + ], + '--count' => [ + Parser::Normalizer => fn($v) => (int) $v, + ], +]); +``` + +Kullanılabilir anahtarlar: + +| `Parser::Repeatable` | Birden çok değeri bir dizide toplar +| `Parser::RealPath` | Dosyanın var olduğunu doğrular ve onu mutlak yola çözer +| `Parser::Normalizer` | Dönüştürme fonksiyonu `fn($value) => ...` +| `Parser::Default` | Yedek değer (yardım metnindeki `(default: x)` ile aynı) +| `Parser::Enum` | İzin verilen değerlerden oluşan dizi + + +Akıcı API +========= + +Seçenek tanımları üzerinde daha fazla denetime gereksinim duyduğunuzda, `addSwitch()`, `addOption()` ve `addArgument()` metotlarıyla akıcı API'yi kullanın. Bu yaklaşım; normalizer'lar, enum'lar ve her parametre üzerinde kesin denetim dahil tüm özelliklere erişim verir: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addSwitch('--verbose', '-v') + ->addOption('--output', '-o') + ->addArgument('file'); + +$args = $parser->parse(); +``` + +`addFromHelp()` metodunda olduğu gibi, sınama için `parse()` metoduna özel bir dizi aktarabilirsiniz: + +```php +$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']); +``` + + +Anahtarlar, Seçenekler ve Argümanlar +------------------------------------ + +Üç tip komut satırı girdisi vardır: + +**Anahtarlar**, `--verbose` ya da `-v` gibi değersiz bayraklardır. Bulunduklarında `true`, bulunmadıklarında `null` olarak ayrıştırılırlar: + +```php +$parser->addSwitch('--verbose', '-v'); +// --verbose → true +// -v → true +// (kullanılmadı) → null +``` + +**Seçenekler**, `--output file.txt` gibi değer kabul eder. Değer bir boşlukla ya da `=` ile ayrılabilir: + +```php +$parser->addOption('--output', '-o'); +// --output file.txt → 'file.txt' +// --output=file.txt → 'file.txt' +// -o file.txt → 'file.txt' +// --output → istisna fırlatır (değer gerekli) +// (kullanılmadı) → null +``` + +Seçeneğin kendisinin her zaman isteğe bağlı olduğuna dikkat edin; onu kullanmamak `null` döndürür. Ancak kullanıldığında değer varsayılan olarak zorunludur. Seçeneğe değersiz izin vermek için `optionalValue: true` ayarlayın (o zaman `true` olarak ayrıştırılır): + +```php +$parser->addOption('--format', '-f', optionalValue: true); +// --format json → 'json' +// --format → true +// (kullanılmadı) → null +``` + +Aynı seçenek `repeatable: true` olmadan birden çok kez kullanıldığında son değer kazanır: + +```php +$parser->addOption('--output', '-o'); +// -o first.txt -o second.txt → 'second.txt' +``` + +**Argümanlar**, tiresiz konumsal değerlerdir. Varsayılan olarak zorunludurlar. Onları isteğe bağlı kılmak için `optional: true` ayarlayın: + +```php +$parser->addArgument('input'); +// script.php file.txt → 'file.txt' +// (kullanılmadı) → istisna fırlatır + +$parser->addArgument('output', optional: true); +// (kullanılmadı) → null + +$parser->addArgument('output', optional: true, fallback: 'out.txt'); +// (kullanılmadı) → 'out.txt' +``` + +İsteğe bağlı bir seçenek ya da argüman verilmediğinde kullanılacak değeri belirtmek için `fallback` kullanın. `optionalValue: true` olan seçeneklerde, seçeneği değersiz kullanmanın yine `true` olarak ayrıştırıldığını, yedeğin ise yalnızca seçenek hiç bulunmadığında kullanıldığını unutmayın: + +```php +$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json'); +// --format xml → 'xml' +// --format → true (seçenek değersiz kullanıldı) +// (kullanılmadı) → 'json' (yedek) +``` + +Argümanlar komut satırının herhangi bir yerinde bulunabilir; seçeneklerden sonra gelmeleri gerekmez: + +```php +// bunların hepsi eşdeğerdir: +// script.php --verbose input.txt +// script.php input.txt --verbose +``` + + +Değerleri Enum ile Kısıtlama +---------------------------- + +Kabul edilen değerleri belirli bir kümeyle sınırlayın: + +```php +$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']); +// --format yaml → "Value of option --format must be json, or xml, or csv." fırlatır +``` + + +Yinelenebilir Seçenekler +------------------------ + +Birden çok değeri bir dizide toplamak için `repeatable: true` ayarlayın: + +```php +$parser->addOption('--include', '-I', repeatable: true); +// -I src -I lib → ['src', 'lib'] +// (kullanılmadı) → [] + +$parser->addArgument('files', optional: true, repeatable: true); +// a.txt b.txt → ['a.txt', 'b.txt'] +``` + + +Değerleri Dönüştürme +-------------------- + +Ayrıştırılan değeri dönüştürmek için bir `normalizer` kullanın: + +```php +$parser->addOption('--count', normalizer: fn($v) => (int) $v); +// --count 42 → 42 (tam sayı) +``` + +Dosya yolu doğrulaması için yerleşik `normalizeRealPath` kullanın: + +```php +$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...)); +// --config app.ini → '/full/path/to/app.ini' +// --config missing.ini → "File path 'missing.ini' not found." fırlatır +``` + + +İki Yaklaşımı Karıştırma +------------------------ + +Normalizer'lara yalnızca bazı seçenekler için gereksinim duyduğunuzda `addFromHelp()` metodunu akıcı metotlarla birleştirebilirsiniz: + +```php +$parser + ->addFromHelp(' + -v, --verbose Enable verbose mode + -q, --quiet Suppress output + ') + ->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...)) + ->addArgument('input'); +``` + + +Hata Ele Alma +------------- + +Ayrıştırıcı geçersiz girdi için `\Exception` fırlatır: + +```php +use Nette\CommandLine\Parser; + +$parser = new Parser; +$parser + ->addOption('--output', '-o') + ->addArgument('file'); + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} +``` + +Yaygın hata mesajları: + +| `Option --output requires argument.` | Seçenek, zorunlu değeri olmadan kullanıldı +| `Unknown option --foo.` | Tanınmayan seçenek +| `Missing required argument <file>.` | Zorunlu argüman verilmedi +| `Unexpected parameter foo.` | Fazladan konumsal argüman +| `Value of option --format must be json, or xml.` | Değer enum'da yok + +Hiç komut satırı argümanı verilmediğini (yani kullanıcının ardında hiçbir şey olmadan yalnızca `script.php` çalıştırdığını) denetlemek için `isEmpty()` kullanın: + +```php +if ($parser->isEmpty()) { + $parser->help(); + exit; +} +``` + + +--help ve --version Ele Alma +---------------------------- + +Betiğinizin zorunlu argümanları varsa, `script.php --help` çalıştırmak normalde zorunlu argüman eksik olduğu için başarısız olurdu. Önce bilgi seçeneklerini denetlemek için `parseOnly()` kullanın: + +```php +$parser = new Parser; +$parser + ->addSwitch('--help', '-h') + ->addSwitch('--version', '-V') + ->addArgument('input'); // zorunlu + +// Önce bilgi seçeneklerini denetle (doğrulama yok, istisna yok) +$info = $parser->parseOnly(['--help', '--version']); + +if ($info['--help']) { + $parser->help(); + exit; +} + +if ($info['--version']) { + echo "1.0.0\n"; + exit; +} + +// Şimdi doğrulamayla tam ayrıştırmayı yap +$args = $parser->parse(); +``` + +`parseOnly()` metodu: +- yalnızca belirtilen seçenekleri ayrıştırır, geri kalan her şeyi yok sayar, +- alias'lara saygı gösterir (`-h` → `--help`), +- asla istisna fırlatmaz, +- kullanılmayan seçenekler için `null` döndürür. + + +Renkli Çıktı +============ + +[api:Nette\CommandLine\Console] sınıfı, çıktınızın terminalde öne çıkması için metni ANSI renk kodlarıyla sarar: + +```php +use Nette\CommandLine\Console; + +$console = new Console; +echo $console->color('red', 'Error!') . "\n"; +echo $console->color('white/blue', 'White text on blue background') . "\n"; +``` + +Renk `'ön plan'` ya da `'ön plan/arka plan'` olarak verilir. Kullanılabilir renkler şunlardır: `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` ve `yellow`. + +Renkler yalnızca çıktı onları desteklediğinde otomatik etkinleşir. `color()` metodu, renkler kapalıyken düz bir dize döndürür, dolayısıyla onu çağırmak her zaman güvenlidir. Davranışı elle zorlayabilirsiniz: + +```php +$console->useColors(false); // renkleri kapat +$console->useColors(true); // renkleri zorla aç +``` + + +Terminali Algılama +------------------ + +İki statik metot, yalnızca terminale özgü özellikleri kullanıp kullanmayacağınıza karar vermenize yardım eder. `detectColors()`, [NO_COLOR |https://no-color.org] ortam değişkeni ayarlıysa ya da çıktı bir CLI terminali değilse `false` döndürür; `FORCE_COLOR` değişkeni terminal denetimini geçersiz kılar: + +```php +if (Console::detectColors()) { + // terminal ANSI renklerini destekliyor +} +``` + +`detectTerminal()`, çıktının etkileşimli bir terminal (bir TTY) olup olmadığını söyler. Bu; ilerleme göstergeleri, satır yeniden yazan çıktı ya da etkileşimli sorular gibi yalnızca terminalde anlamlı olan özellikleri otomatik kapatmak için yararlıdır: + +```php +if (Console::detectTerminal()) { + // çıktı bir dosyaya ya da boruya değil, etkileşimli bir terminale gidiyor +} +``` + + +Eksiksiz Örnek +============== + +İşte `Parser` ile `Console` sınıflarını birleştiren, gerçek dünyadan bir dosya dönüştürme betiği: + +```php +#!/usr/bin/env php +<?php +use Nette\CommandLine\Parser; + +require __DIR__ . '/vendor/autoload.php'; + +$parser = new Parser; +$parser + ->addFromHelp(' + -h, --help Show this help + -v, --verbose Show detailed output + -n, --dry-run Show what would be done + -f, --format [type] Output format (default: json) + -o, --output <file> Output file + ', [ + '--format' => [ + Parser::Enum => ['json', 'xml', 'csv'], + ], + ]) + ->addArgument('input', normalizer: Parser::normalizeRealPath(...)); + +// --help seçeneğini doğrulamadan önce ele al ("eksik argüman" hatasından kaçınır) +if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) { + echo "Usage: convert [options] <input>\n\n"; + $parser->help(); + exit; +} + +try { + $args = $parser->parse(); +} catch (\Exception $e) { + fwrite(STDERR, "Error: {$e->getMessage()}\n"); + exit(1); +} + +if ($args['--verbose']) { + echo "Converting {$args['input']} to {$args['--format']}...\n"; +} + +if ($args['--dry-run']) { + echo "Dry run: no changes made.\n"; + exit; +} + +// ... dönüştürme mantığı burada ... + +echo "Done!\n"; +``` + +Betik şu gibi komutları kabul eder: + +- `convert input.txt` - varsayılanlarla dönüştür +- `convert -v --format xml input.txt` - ayrıntılı, XML biçimi +- `convert -o result.txt input.txt` - çıktı dosyasını belirt +- `convert --help` - yardımı göster (girdi dosyası olmadan bile çalışır) + + +{{sitename: Nette Dokümantasyonu}} diff --git a/command-line/tr/@left-menu.texy b/command-line/tr/@left-menu.texy new file mode 100644 index 0000000000..f01a897def --- /dev/null +++ b/command-line/tr/@left-menu.texy @@ -0,0 +1,12 @@ +Nette Command-Line +****************** +- [Genel bakış |@home] + + +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/bg/@home.texy b/component-model/bg/@home.texy deleted file mode 100644 index 2209fed3e8..0000000000 --- a/component-model/bg/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Компонентен модел -***************** - -.[perex] -Важно понятие в Nette е компонентът. В страниците вмъкваме [визуални интерактивни компоненти |application:components], компоненти са и формите или всички техни елементи. Основните два класа, от които всички тези компоненти наследяват, са част от пакета `nette/component-model` и имат за цел да създават дървовидна йерархия на компоненти. - - -Component -========= -[api:Nette\ComponentModel\Component] е общият предтеча на всички компоненти. Съдържа методи `getName()`, връщащ името на компонента, и метод `getParent()`, връщащ неговия родител. И двете могат да бъдат зададени с метода `setParent()` - първият параметър е родителят, а вторият - името на компонента. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Търси в йерархията нагоре обект от желания клас или интерфейс. Например `$component->lookup(Nette\Application\UI\Presenter::class)` връща презентер, ако компонентът е свързан с него, дори през няколко нива. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Връща така наречения път, който е низ, получен чрез свързване на имената на всички компоненти по пътя между текущия и търсения компонент. Така например `$component->lookupPath(Nette\Application\UI\Presenter::class)` връща уникален идентификатор на компонента спрямо презентера. - - -Container -========= -[api:Nette\ComponentModel\Container] е родителският компонент, т.е. компонент, съдържащ наследници и така образуващ дървовидна структура. Разполага с методи за лесно добавяне, получаване и премахване на обекти. Той е предтеча например на формата или на класовете `Control` и `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Връща компонент. При опит за получаване на недефиниран наследник се извиква фабриката `createComponent($name)`. Методът `createComponent($name)` извиква в текущия компонент метода `createComponent<име на компонента>` и като параметър му предава името на компонента. Създаденият компонент след това се добавя към текущия компонент като негов наследник. Тези методи наричаме фабрики за компоненти и могат да бъдат имплементирани от наследниците на класа `Container`. - - -getComponents(): array .[method] --------------------------------- -Връща преките наследници като масив. Ключовете съдържат имената на тези компоненти. Забележка: във версия 3.0.x методът връщаше итератор вместо масив и първият му параметър определяше дали компонентите да се обхождат в дълбочина, а вторият представляваше типов филтър. Тези параметри са deprecated. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Получава цялата йерархия на компоненти, включително всички вложени подчинени компоненти, като индексиран масив. Търсенето се извършва първо в дълбочина. - - -Наблюдение на предците -====================== - -Компонентният модел на Nette позволява много динамична работа с дървото (можем да премахваме, преместваме, добавяме компоненти), затова би било грешка да се разчита, че след създаването на компонента веднага (в конструктора) е известен родителят, родителят на родителя и т.н. Най-често родителят изобщо не е известен при създаването. - -Как да разберем кога компонентът е бил прикрепен към дървото на презентера? Наблюдението на промяната на родителя не е достатъчно, тъй като към презентера може да е бил прикрепен например родителят на родителя. Помага методът [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Всеки компонент може да наблюдава произволен брой класове и интерфейси. Прикрепването или откачането се съобщава чрез извикване на callback `$attached`, респ. `$detached`, и предаване на обекта на наблюдавания клас. - -За по-добро разбиране, пример: класът `UploadControl`, представляващ формулярен елемент за качване на файлове в Nette Forms, трябва да зададе на формата атрибут `enctype` на стойност `multipart/form-data`. В момента на създаване на обекта обаче той може да не е прикрепен към никаква форма. В кой момент тогава да се модифицира формата? Решението е просто - в конструктора се иска наблюдение: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -и щом формата е налична, се извиква callback. (Преди това вместо него се използваха общите методи `attached`, респ. `detached`). diff --git a/component-model/bg/@meta.texy b/component-model/bg/@meta.texy deleted file mode 100644 index 794cbc8522..0000000000 --- a/component-model/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/cs/@home.texy b/component-model/cs/@home.texy index 7aa17b402f..c0b12941fe 100644 --- a/component-model/cs/@home.texy +++ b/component-model/cs/@home.texy @@ -7,22 +7,32 @@ Důležitým pojmem v Nette je komponenta. Do stránek vkládáme [vizuální in Component ========= -[api:Nette\ComponentModel\Component] je společným předkem všech komponent. Obsahuje metody `getName()` vracející název kompoenty a metodu `getParent()` vracející jejího rodiče. Obojí lze nastavit metodou `setParent()` - první parametr je rodič a druhý název komponenty. +[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. Obojí lze nastavit metodou `setParent()` - první parametr je rodič a druhý název komponenty. -lookup(string $type): ?Component .[method] ------------------------------------------- -Vyhledá v hierarchii směrem nahoru objekt požadované třídy nebo rozhraní. Například `$component->lookup(Nette\Application\UI\Presenter::class)` vrací presenter, pokud je k němu, i přes několik úrovní, komponenta připojena. +lookup(?string $type, bool $throw=true): ?Component .[method] +------------------------------------------------------------- +Vyhledá v hierarchii směrem nahoru objekt požadované třídy nebo rozhraní. Například `$component->lookup(Nette\Application\UI\Presenter::class)` vrací presenter, pokud je k němu, i přes několik úrovní, komponenta připojena. 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): ?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. +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`. +[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(Component $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. + + +removeComponent(Component $component): void .[method] +----------------------------------------------------- +Odebere potomka z kontejneru. getComponent(string $name): ?Component .[method] @@ -30,13 +40,13 @@ getComponent(string $name): ?Component .[method] Vrací komponentu. Při pokusu o získání nedefinovaného potomka je zavolána továrna `createComponent($name)`. Metoda `createComponent($name)` zavolá v aktuální komponentě metodu `createComponent<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`. -getComponents(): array .[method] --------------------------------- -Vrací přímé potomky jako pole. Klíče obsahují názvy těchto komponent. Poznámka: ve verzi 3.0.x metoda namísto pole vracela iterátor a její první parametr určoval, zda se mají komponenty procházet do hloubky, a druhý představoval typový filtr. Tyto parametry jsou deprecated. +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(): array .[method]{data-version:3.1.0} -------------------------------------------------------- +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. @@ -45,7 +55,9 @@ Monitorování předků Komponentový model Nette umožňuje velmi dynamickou práci se stromem (komponenty můžeme vyjímat, přesouvat, přidávat), proto by byla chyba se spoléhat na to, že po vytvoření komponenty je hned (v konstruktoru) znám rodič, rodič rodiče atd. Většinou totiž rodič při vytvoření vůbec známý není. -Jak poznat, kdy byla komponenta připojena do stromu presenteru? Sledovat změnu rodiče nestačí, protože k presenteru mohl být připojen třeba rodič rodiče. Pomůže metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Každá komponenta může monitorovat libovolný počet tříd a rozhraní. Připojení nebo odpojení je ohlášeno zavoláním callbacku `$attached` resp. `$detached`, a předáním objektu sledované třídy. +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)`. + +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. Callbacky jsou navíc deduplikované, takže se stejný callback nikdy nezavolá dvakrát pro tentýž objekt. 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: @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -a jakmile je formulář k dispozici, zavolá se callback. (Dříve se místo něj používala společná metoda `attached` resp. `detached`). +a jakmile je formulář k dispozici, zavolá se callback. + + +Pokud aktualizujete balíček na novější verzi, podívejte se na stránku [upgrade|upgrading]. diff --git a/component-model/cs/@left-menu.texy b/component-model/cs/@left-menu.texy new file mode 100644 index 0000000000..a64de8aa8c --- /dev/null +++ b/component-model/cs/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 08edde785b..462d9add80 100644 --- a/component-model/cs/@meta.texy +++ b/component-model/cs/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokumentace}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/cs/upgrading.texy b/component-model/cs/upgrading.texy new file mode 100644 index 0000000000..aef1820db1 --- /dev/null +++ b/component-model/cs/upgrading.texy @@ -0,0 +1,20 @@ +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. diff --git a/component-model/de/@home.texy b/component-model/de/@home.texy index 680880c8f3..0e6ab23b3e 100644 --- a/component-model/de/@home.texy +++ b/component-model/de/@home.texy @@ -2,52 +2,64 @@ Komponentenmodell ***************** .[perex] -Ein wichtiger Begriff in Nette ist die Komponente. Wir fügen [visuelle interaktive Komponenten |application:components] in Seiten ein, auch Formulare oder alle ihre Elemente sind Komponenten. Die beiden Basisklassen, von denen alle diese Komponenten erben, sind Teil des Pakets `nette/component-model` und haben die Aufgabe, eine Baumhierarchie von Komponenten zu erstellen. +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. Es enthält die Methoden `getName()`, die den Namen der Komponente zurückgibt, und die Methode `getParent()`, die ihren Elternteil zurückgibt. Beides kann mit der Methode `setParent()` eingestellt werden - der erste Parameter ist der Elternteil und der zweite der Komponentenname. +[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): ?Component .[method] ------------------------------------------- -Sucht in der Hierarchie nach oben nach einem Objekt der gewünschten Klasse oder Schnittstelle. Zum Beispiel gibt `$component->lookup(Nette\Application\UI\Presenter::class)` den Presenter zurück, wenn die Komponente, auch über mehrere Ebenen hinweg, mit ihm verbunden ist. +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): ?string .[method] -------------------------------------------- -Gibt den sogenannten Pfad zurück, eine Zeichenkette, die durch die Verkettung der Namen aller Komponenten auf dem Weg zwischen der aktuellen und der gesuchten Komponente entsteht. Zum Beispiel gibt `$component->lookupPath(Nette\Application\UI\Presenter::class)` einen eindeutigen Bezeichner der Komponente relativ zum Presenter zurück. +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 Elternkomponente, d.h. eine Komponente, die Nachkommen enthält und somit eine Baumstruktur bildet. Sie verfügt über Methoden zum einfachen Hinzufügen, Abrufen und Entfernen von Objekten. Sie ist beispielsweise der Vorfahre von Formularen oder den Klassen `Control` und `Presenter`. +[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 die Komponente zurück. Beim Versuch, einen undefinierten Nachkommen abzurufen, wird die Factory `createComponent($name)` aufgerufen. Die Methode `createComponent($name)` ruft in der aktuellen Komponente die Methode `createComponent<Komponentenname>` auf und übergibt ihr den Komponentennamen als Parameter. Die erstellte Komponente wird dann der aktuellen Komponente als ihr Nachkomme hinzugefügt. Diese Methoden nennen wir Komponentenfabriken und sie können von Nachkommen der Klasse `Container` implementiert werden. +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(): array .[method] --------------------------------- -Gibt die direkten Nachkommen als Array zurück. Die Schlüssel enthalten die Namen dieser Komponenten. Hinweis: In Version 3.0.x gab die Methode anstelle eines Arrays einen Iterator zurück, und ihr erster Parameter bestimmte, ob die Komponenten in die Tiefe durchlaufen werden sollten, und der zweite stellte einen Typfilter dar. Diese Parameter sind deprecated. +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(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Ruft die gesamte Komponenten-Hierarchie einschließlich aller verschachtelten untergeordneten Komponenten als indiziertes Array ab. Die Suche erfolgt zuerst in die Tiefe. +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 ermöglicht eine sehr dynamische Arbeit mit dem Baum (Komponenten können entfernt, verschoben, hinzugefügt werden), daher wäre es ein Fehler, sich darauf zu verlassen, dass nach dem Erstellen einer Komponente sofort (im Konstruktor) der Elternteil, der Elternteil des Elternteils usw. bekannt ist. Meistens ist der Elternteil zum Zeitpunkt der Erstellung überhaupt nicht bekannt. +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 erkennt man, wann eine Komponente in den Presenter-Baum eingehängt wurde? Die Änderung des Elternteils zu beobachten reicht nicht aus, da möglicherweise der Elternteil des Elternteils mit dem Presenter verbunden wurde. Die Methode [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] hilft dabei. Jede Komponente kann eine beliebige Anzahl von Klassen und Schnittstellen überwachen. Das Einhängen oder Aushängen wird durch den Aufruf des Callbacks `$attached` bzw. `$detached` gemeldet, wobei das Objekt der überwachten Klasse übergeben wird. +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. -Zum besseren Verständnis ein Beispiel: Die Klasse `UploadControl`, die ein Formularelement für den Datei-Upload in Nette Forms darstellt, muss das Attribut `enctype` des Formulars auf den Wert `multipart/form-data` setzen. Zum Zeitpunkt der Objekterstellung muss sie jedoch nicht mit einem Formular verbunden sein. Wann soll das Formular also modifiziert werden? Die Lösung ist einfach - im Konstruktor wird die Überwachung angefordert: +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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -und sobald das Formular verfügbar ist, wird der Callback aufgerufen. (Früher wurde stattdessen die gemeinsame Methode `attached` bzw. `detached` verwendet). +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 new file mode 100644 index 0000000000..d31c9dc429 --- /dev/null +++ b/component-model/de/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 2cf383a5cf..b3b806b2ca 100644 --- a/component-model/de/@meta.texy +++ b/component-model/de/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokumentation}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/de/upgrading.texy b/component-model/de/upgrading.texy new file mode 100644 index 0000000000..08cde92699 --- /dev/null +++ b/component-model/de/upgrading.texy @@ -0,0 +1,20 @@ +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/el/@home.texy b/component-model/el/@home.texy deleted file mode 100644 index b38ccc1262..0000000000 --- a/component-model/el/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Μοντέλο Component -***************** - -.[perex] -Ένας σημαντικός όρος στο Nette είναι το component. Στις σελίδες εισάγουμε [οπτικά διαδραστικά components |application:components], τα components είναι επίσης φόρμες ή όλα τα στοιχεία τους. Οι δύο βασικές κλάσεις από τις οποίες κληρονομούν όλα αυτά τα components αποτελούν μέρος του πακέτου `nette/component-model` και έχουν ως αποστολή τη δημιουργία μιας ιεραρχικής δενδροειδούς δομής components. - - -Component -========= -Η [api:Nette\ComponentModel\Component] είναι ο κοινός πρόγονος όλων των components. Περιέχει τις μεθόδους `getName()` που επιστρέφει το όνομα του component και τη μέθοδο `getParent()` που επιστρέφει τον γονέα του. Και τα δύο μπορούν να οριστούν με τη μέθοδο `setParent()` - η πρώτη παράμετρος είναι ο γονέας και η δεύτερη το όνομα του component. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Αναζητά στην ιεραρχία προς τα πάνω ένα αντικείμενο της ζητούμενης κλάσης ή interface. Για παράδειγμα, το `$component->lookup(Nette\Application\UI\Presenter::class)` επιστρέφει τον presenter, εάν το component είναι συνδεδεμένο με αυτόν, ακόμη και μέσω πολλών επιπέδων. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Επιστρέφει τη λεγόμενη διαδρομή, η οποία είναι μια συμβολοσειρά που δημιουργείται από τη συνένωση των ονομάτων όλων των components στη διαδρομή μεταξύ του τρέχοντος και του αναζητούμενου component. Έτσι, π.χ., το `$component->lookupPath(Nette\Application\UI\Presenter::class)` επιστρέφει ένα μοναδικό αναγνωριστικό του component σε σχέση με τον presenter. - - -Container -========= -Η [api:Nette\ComponentModel\Container] είναι το γονικό component, δηλ. ένα component που περιέχει απογόνους και σχηματίζει έτσι μια δενδροειδή δομή. Διαθέτει μεθόδους για εύκολη προσθήκη, ανάκτηση και αφαίρεση αντικειμένων. Είναι ο πρόγονος, για παράδειγμα, της φόρμας ή των κλάσεων `Control` και `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Επιστρέφει το component. Κατά την προσπάθεια ανάκτησης ενός μη ορισμένου απογόνου, καλείται το factory `createComponent($name)`. Η μέθοδος `createComponent($name)` καλεί στο τρέχον component τη μέθοδο `createComponent<όνομα_component>` και της περνά ως παράμετρο το όνομα του component. Το δημιουργημένο component προστίθεται στη συνέχεια στο τρέχον component ως απόγονός του. Αυτές οι μέθοδοι ονομάζονται factories component και μπορούν να υλοποιηθούν από απογόνους της κλάσης `Container`. - - -getComponents(): array .[method] --------------------------------- -Επιστρέφει τους άμεσους απογόνους ως πίνακα. Τα κλειδιά περιέχουν τα ονόματα αυτών των components. Σημείωση: στην έκδοση 3.0.x η μέθοδος επέστρεφε έναν iterator αντί για πίνακα και η πρώτη της παράμετρος καθόριζε αν τα components έπρεπε να διασχιστούν σε βάθος, και η δεύτερη αντιπροσώπευε ένα φίλτρο τύπου. Αυτές οι παράμετροι είναι deprecated. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Ανακτά ολόκληρη την ιεραρχία των components, συμπεριλαμβανομένων όλων των ενσωματωμένων θυγατρικών components, ως ευρετηριασμένο πίνακα. Η αναζήτηση γίνεται πρώτα σε βάθος. - - -Παρακολούθηση προγόνων -====================== - -Το μοντέλο component του Nette επιτρέπει πολύ δυναμική εργασία με το δέντρο (μπορούμε να αφαιρούμε, να μετακινούμε, να προσθέτουμε components), επομένως θα ήταν λάθος να βασιζόμαστε στο γεγονός ότι μετά τη δημιουργία ενός component είναι αμέσως γνωστός ο γονέας, ο γονέας του γονέα κ.λπ. (στον κατασκευαστή). Τις περισσότερες φορές, ο γονέας δεν είναι καθόλου γνωστός κατά τη δημιουργία. - -Πώς να αναγνωρίσετε πότε ένα component συνδέθηκε στο δέντρο του presenter; Η παρακολούθηση της αλλαγής του γονέα δεν αρκεί, γιατί μπορεί να έχει συνδεθεί στον presenter ο γονέας του γονέα, για παράδειγμα. Η μέθοδος [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] βοηθάει. Κάθε component μπορεί να παρακολουθεί οποιονδήποτε αριθμό κλάσεων και interfaces. Η σύνδεση ή η αποσύνδεση ανακοινώνεται με την κλήση του callback `$attached` ή `$detached` αντίστοιχα, και την παράδοση του αντικειμένου της παρακολουθούμενης κλάσης. - -Για καλύτερη κατανόηση, ένα παράδειγμα: η κλάση `UploadControl`, που αντιπροσωπεύει ένα στοιχείο φόρμας για την αποστολή αρχείων στο Nette Forms, πρέπει να ορίσει το attribute `enctype` της φόρμας στην τιμή `multipart/form-data`. Κατά τη στιγμή της δημιουργίας του αντικειμένου, όμως, μπορεί να μην είναι συνδεδεμένο με καμία φόρμα. Πότε λοιπόν πρέπει να τροποποιηθεί η φόρμα; Η λύση είναι απλή - στον κατασκευαστή ζητείται η παρακολούθηση: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -και μόλις η φόρμα είναι διαθέσιμη, καλείται το callback. (Παλαιότερα, χρησιμοποιούνταν αντί αυτού η κοινή μέθοδος `attached` ή `detached`). diff --git a/component-model/el/@meta.texy b/component-model/el/@meta.texy deleted file mode 100644 index a09ce5fe0d..0000000000 --- a/component-model/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/en/@home.texy b/component-model/en/@home.texy index ec18f76c32..fb18149cfd 100644 --- a/component-model/en/@home.texy +++ b/component-model/en/@home.texy @@ -10,19 +10,29 @@ Component [api:Nette\ComponentModel\Component] is the common ancestor of all components. It contains the `getName()` method returning the name of the component and the `getParent()` method returning its parent. Both can be set using the `setParent()` method - the first parameter is the parent, and the second is the component name. -lookup(string $type): ?Component .[method] ------------------------------------------- -Searches up the hierarchy for an object of the desired class or interface. For example, `$component->lookup(Nette\Application\UI\Presenter::class)` returns the presenter if the component is connected to it, even across several levels. +lookup(?string $type, bool $throw=true): ?Component .[method] +------------------------------------------------------------- +Searches up the hierarchy for an object of the desired class or interface. For example, `$component->lookup(Nette\Application\UI\Presenter::class)` returns 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): ?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. +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`. +[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(Component $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. + + +removeComponent(Component $component): void .[method] +----------------------------------------------------- +Removes a child component from the container. getComponent(string $name): ?Component .[method] @@ -30,13 +40,13 @@ getComponent(string $name): ?Component .[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`. -getComponents(): array .[method] --------------------------------- -Returns direct descendants as an array. The keys contain the names of these components. Note: in version 3.0.x, the method returned an iterator instead of an array, and its first parameter specified whether to iterate through the components in depth, and the second represented a type filter. These parameters are deprecated. +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(): array .[method]{data-version:3.1.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. @@ -45,7 +55,9 @@ 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 to find out when a component has been added to the presenter tree? Keeping track of the parent change is not enough, because the parent of the parent could have been attached to the presenter, for example. The [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()] method can help. Each component can monitor any number of classes and interfaces. Connection or disconnection is announced by calling the callbacks `$attached` and `$detached`, respectively, and passing the object of the monitored class. +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)`. + +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. Callbacks are also deduplicated, so the same callback is never called twice for the same object. 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: @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -and as soon as the form becomes available, the callback is invoked. (Previously, the common methods `attached` and `detached` were used for this purpose.) +and as soon as the form becomes available, the callback is invoked. + + +If you are upgrading to a newer version, see the [upgrading] page. diff --git a/component-model/en/@left-menu.texy b/component-model/en/@left-menu.texy new file mode 100644 index 0000000000..37693b5d78 --- /dev/null +++ b/component-model/en/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 91205786e5..42471908b0 100644 --- a/component-model/en/@meta.texy +++ b/component-model/en/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Documentation}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/en/upgrading.texy b/component-model/en/upgrading.texy new file mode 100644 index 0000000000..02c9795461 --- /dev/null +++ b/component-model/en/upgrading.texy @@ -0,0 +1,20 @@ +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. diff --git a/component-model/es/@home.texy b/component-model/es/@home.texy index 515782fc79..3320fd550d 100644 --- a/component-model/es/@home.texy +++ b/component-model/es/@home.texy @@ -2,52 +2,64 @@ Modelo de componentes ********************* .[perex] -Un concepto importante en Nette es el componente. Insertamos [componentes interactivos visuales |application:components] en las páginas, los formularios o todos sus elementos también son componentes. Las dos clases base de las que heredan todos estos componentes forman parte del paquete `nette/component-model` y su propósito es crear una jerarquía de componentes en forma de árbol. +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 ancestro común de todos los componentes. Contiene los métodos `getName()` que devuelven 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 es el nombre del componente. +[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): ?Component .[method] ------------------------------------------- -Busca un objeto de la clase o interfaz requerida hacia arriba en la jerarquía. Por ejemplo, `$component->lookup(Nette\Application\UI\Presenter::class)` devuelve el presenter si el componente está adjunto a él, incluso a través de varios niveles. +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): ?string .[method] -------------------------------------------- -Devuelve la llamada ruta, que es una cadena creada concatenando los nombres de todos los componentes en la ruta entre el componente actual y el buscado. Así, por ejemplo, `$component->lookupPath(Nette\Application\UI\Presenter::class)` devuelve un identificador único del componente en relación con el presenter. +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, un componente que contiene hijos y, por lo tanto, forma una estructura de árbol. Dispone de métodos para agregar, obtener y eliminar objetos fácilmente. Es el ancestro, por ejemplo, del formulario o de las clases `Control` y `Presenter`. +[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. Al intentar obtener un hijo indefinido, se llama a la fábrica `createComponent($name)`. El método `createComponent($name)` llama al método `createComponent<nombre del componente>` en el componente actual y le pasa el nombre del componente como parámetro. El componente creado se agrega luego al componente actual como su hijo. Llamamos a estos métodos fábricas de componentes y pueden ser implementados por descendientes de la clase `Container`. +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(): array .[method] --------------------------------- -Devuelve los hijos directos como un array. Las claves contienen los nombres de estos componentes. Nota: en la versión 3.0.x, el método devolvía un iterador en lugar de un array, y su primer parámetro determinaba si los componentes debían recorrerse en profundidad, y el segundo representaba un filtro de tipo. Estos parámetros están obsoletos. +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(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Obtiene toda la jerarquía de componentes, incluidos todos los subcomponentes anidados, como un array indexado. La búsqueda va primero en profundidad. +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. -Monitorización de ancestros +Monitorizar los antecesores =========================== -El modelo de componentes de Nette permite un trabajo muy dinámico con el árbol (podemos eliminar, mover, agregar componentes), por lo que sería un error confiar en que después de crear un componente, el padre, el padre del padre, etc., se conozcan inmediatamente (en el constructor). En la mayoría de los casos, el padre no se conoce en absoluto en el momento de la creación. +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 saber cuándo se adjuntó un componente al árbol del presenter? Observar el cambio del padre no es suficiente, porque el padre del padre podría haber sido adjuntado al presenter, por ejemplo. El método [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] ayuda. Cada componente puede monitorizar cualquier número de clases e interfaces. El adjunto o desadjunto se anuncia llamando al callback `$attached` o `$detached`, respectivamente, y pasando el objeto de la clase monitorizada. +¿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)`. -Para una mejor comprensión, un ejemplo: la clase `UploadControl`, que representa el elemento de formulario para la carga de archivos en Nette Forms, debe establecer el atributo `enctype` del formulario en el valor `multipart/form-data`. Sin embargo, en el momento de la creación del objeto, es posible que no esté adjunto a ningún formulario. ¿En qué momento, entonces, modificar el formulario? La solución es simple: en el constructor, solicite la monitorización: +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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -y tan pronto como el formulario esté disponible, se llama al callback. (Anteriormente, se usaba el método común `attached` o `detached` en su lugar). +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 new file mode 100644 index 0000000000..574ccef237 --- /dev/null +++ b/component-model/es/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 25d506cde9..3798d9cda4 100644 --- a/component-model/es/@meta.texy +++ b/component-model/es/@meta.texy @@ -1,2 +1 @@ -{{sitename: Nette Documentación}} -{{leftbar: nette:@menu-topics}} +{{sitename: Documentación de Nette}} diff --git a/component-model/es/upgrading.texy b/component-model/es/upgrading.texy new file mode 100644 index 0000000000..8857811e0c --- /dev/null +++ b/component-model/es/upgrading.texy @@ -0,0 +1,20 @@ +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 index 706b8f46ce..8205b27905 100644 --- a/component-model/fr/@home.texy +++ b/component-model/fr/@home.texy @@ -1,53 +1,65 @@ -Modèle de composant -******************* +Modèle de composants +******************** .[perex] -Un concept important dans Nette est le composant. Nous insérons des [composants interactifs visuels |application:components] dans les pages ; les formulaires ou tous leurs éléments sont également des composants. Les deux classes de base dont tous ces composants héritent font partie du paquet `nette/component-model` et leur rôle est de créer une hiérarchie arborescente de composants. +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 les méthodes `getName()` retournant le nom du composant et la méthode `getParent()` retournant son parent. Les deux peuvent être définis à l'aide de la méthode `setParent()` - le premier paramètre est le parent et le second est le nom du composant. +[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): ?Component .[method] ------------------------------------------- -Recherche dans la hiérarchie vers le haut un objet de la classe ou de l'interface demandée. Par exemple, `$component->lookup(Nette\Application\UI\Presenter::class)` retourne le presenter, si le composant y est attaché, même à travers plusieurs niveaux. +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): ?string .[method] -------------------------------------------- -Retourne ce qu'on appelle le chemin, qui est une chaîne formée en joignant les noms de tous les composants sur le chemin entre le composant actuel et le composant recherché. Ainsi, par exemple, `$component->lookupPath(Nette\Application\UI\Presenter::class)` retourne l'identifiant unique du composant par rapport au presenter. +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 un composant contenant des enfants et formant ainsi une structure arborescente. Il dispose de méthodes pour ajouter, obtenir et supprimer facilement des objets. C'est l'ancêtre, par exemple, du formulaire ou des classes `Control` et `Presenter`. +[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] ------------------------------------------------ -Retourne un composant. Lors d'une tentative d'obtention d'un enfant non défini, la factory `createComponent($name)` est appelée. La méthode `createComponent($name)` appelle la méthode `createComponent<NomDuComposant>` dans le composant actuel et lui passe le nom du composant en paramètre. Le composant créé est ensuite ajouté au composant actuel en tant qu'enfant. Nous appelons ces méthodes des factories de composants, et elles peuvent être implémentées par les descendants de la classe `Container`. +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(): array .[method] --------------------------------- -Retourne les enfants directs sous forme de tableau. Les clés contiennent les noms de ces composants. Note : dans la version 3.0.x, la méthode retournait un itérateur au lieu d'un tableau, et son premier paramètre déterminait si les composants devaient être parcourus en profondeur, et le second représentait un filtre de type. Ces paramètres sont obsolètes. +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(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Obtient toute la hiérarchie des composants, y compris tous les composants enfants imbriqués, sous forme de tableau indexé. La recherche se fait d'abord en profondeur. +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 composant de Nette permet un travail très dynamique avec l'arborescence (nous pouvons retirer, déplacer, ajouter des composants), il serait donc erroné de supposer qu'après la création d'un composant, le parent, le parent du parent, etc., sont immédiatement connus (dans le constructeur). La plupart du temps, le parent n'est pas du tout connu lors de la création. +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 savoir quand un composant a été attaché à l'arborescence du presenter ? Surveiller le changement de parent ne suffit pas, car le parent du parent, par exemple, aurait pu être attaché au presenter. La méthode [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] aide. Chaque composant peut surveiller n'importe quel nombre de classes et d'interfaces. L'attachement ou le détachement est signalé en appelant le callback `$attached` ou `$detached`, respectivement, et en passant l'objet de la classe surveillée. +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)`. -Pour une meilleure compréhension, voici un exemple : la classe `UploadControl`, représentant l'élément de formulaire pour le téléchargement de fichiers dans Nette Forms, doit définir l'attribut `enctype` du formulaire sur `multipart/form-data`. Cependant, au moment de la création de l'objet, il se peut qu'il ne soit attaché à aucun formulaire. À quel moment alors modifier le formulaire ? La solution est simple - dans le constructeur, demandez la surveillance : +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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -et dès que le formulaire est disponible, le callback est appelé. (Auparavant, les méthodes communes `attached` ou `detached` étaient utilisées à la place). +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 new file mode 100644 index 0000000000..a325edaeb4 --- /dev/null +++ b/component-model/fr/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 95ec8a4ef6..72ae4b8db8 100644 --- a/component-model/fr/@meta.texy +++ b/component-model/fr/@meta.texy @@ -1,2 +1 @@ {{sitename: Documentation Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/fr/upgrading.texy b/component-model/fr/upgrading.texy new file mode 100644 index 0000000000..c51378a805 --- /dev/null +++ b/component-model/fr/upgrading.texy @@ -0,0 +1,20 @@ +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/hu/@home.texy b/component-model/hu/@home.texy deleted file mode 100644 index 5ea05ef999..0000000000 --- a/component-model/hu/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Komponens modell -**************** - -.[perex] -A Nette fontos fogalma a komponens. Az oldalakra [vizuális interaktív komponenseket |application:components] illesztünk be, komponensek az űrlapok vagy azok összes eleme is. A két alapvető osztály, amelyektől ezek a komponensek öröklődnek, a `nette/component-model` csomag részét képezik, és feladatuk a komponensek fa hierarchiájának létrehozása. - - -Component -========= -Az [api:Nette\ComponentModel\Component] az összes komponens közös őse. Tartalmazza a `getName()` metódust, amely visszaadja a komponens nevét, és a `getParent()` metódust, amely visszaadja a szülőjét. Mindkettőt a `setParent()` metódussal lehet beállítani - az első paraméter a szülő, a második a komponens neve. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Felkeresi a hierarchiában felfelé a kívánt osztály vagy interfész objektumát. Például a `$component->lookup(Nette\Application\UI\Presenter::class)` visszaadja a presentert, ha a komponens hozzá van csatolva, akár több szinten keresztül is. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Visszaadja az úgynevezett utat, amely egy string, ami az aktuális és a keresett komponens közötti útvonalon lévő összes komponens nevének összekapcsolásával jön létre. Tehát pl. a `$component->lookupPath(Nette\Application\UI\Presenter::class)` visszaadja a komponens egyedi azonosítóját a presenterhez képest. - - -Container -========= -Az [api:Nette\ComponentModel\Container] a szülő komponens, azaz a leszármazottakat tartalmazó komponens, amely fa struktúrát alkot. Metódusokkal rendelkezik az objektumok egyszerű hozzáadásához, lekéréséhez és eltávolításához. Például az űrlap vagy a `Control` és `Presenter` osztályok őse. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Visszaadja a komponenst. Egy nem definiált leszármazott lekérésekor a `createComponent($name)` factory hívódik meg. A `createComponent($name)` metódus meghívja az aktuális komponensben a `createComponent<komponens neve>` metódust, és paraméterként átadja neki a komponens nevét. A létrehozott komponens ezután hozzáadódik az aktuális komponenshez annak leszármazottjaként. Ezeket a metódusokat komponens factory-knak nevezzük, és a `Container` osztály leszármazottai implementálhatják őket. - - -getComponents(): array .[method] --------------------------------- -Visszaadja a közvetlen leszármazottakat tömbként. A kulcsok ezeknek a komponenseknek a neveit tartalmazzák. Megjegyzés: a 3.0.x verzióban a metódus tömb helyett iterátort adott vissza, és az első paramétere határozta meg, hogy a komponenseket mélységében kell-e bejárni, a második pedig egy típus szűrőt jelentett. Ezek a paraméterek elavultak. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Lekéri a teljes komponens hierarchiát, beleértve az összes beágyazott alárendelt komponenst is, indexelt tömbként. A keresés először mélységében történik. - - -Ősök monitorozása -================= - -A Nette komponens modellje nagyon dinamikus munkát tesz lehetővé a fával (komponenseket kivehetünk, áthelyezhetünk, hozzáadhatunk), ezért hiba lenne arra támaszkodni, hogy a komponens létrehozása után azonnal (a konstruktorban) ismert a szülő, a szülő szülője stb. Legtöbbször ugyanis a szülő a létrehozáskor egyáltalán nem ismert. - -Hogyan lehet tudni, mikor csatlakozott a komponens a presenter fájához? A szülő változásának figyelése nem elegendő, mert a presenterhez például a szülő szülője is csatlakozhatott. Segít a [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] metódus. Minden komponens tetszőleges számú osztályt és interfészt monitorozhat. A csatlakozást vagy leválasztást a `$attached`, illetve `$detached` callback meghívása jelzi, átadva a figyelt osztály objektumát. - -A jobb megértés érdekében egy példa: az `UploadControl` osztály, amely a Nette Forms fájlfeltöltési űrlap elemét képviseli, be kell állítania az űrlap `enctype` attribútumát `multipart/form-data` értékre. Az objektum létrehozásakor azonban nem feltétlenül kell csatlakoznia semmilyen űrlaphoz. Melyik pillanatban kell tehát módosítani az űrlapot? A megoldás egyszerű - a konstruktorban kérjük a monitorozást: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -és amint az űrlap elérhetővé válik, a callback meghívódik. (Korábban helyette a közös `attached`, illetve `detached` metódust használták). diff --git a/component-model/hu/@meta.texy b/component-model/hu/@meta.texy deleted file mode 100644 index c00a2158aa..0000000000 --- a/component-model/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette dokumentáció}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/it/@home.texy b/component-model/it/@home.texy index 77d7b09509..fe9dc4a7f6 100644 --- a/component-model/it/@home.texy +++ b/component-model/it/@home.texy @@ -1,53 +1,65 @@ -Modello a componenti -******************** +Component Model +*************** .[perex] -Un concetto importante in Nette è il componente. Inseriamo nelle pagine [componenti visivi interattivi |application:components], i form sono componenti, così come tutti i loro elementi. Le due classi base da cui tutti questi componenti ereditano fanno parte del pacchetto `nette/component-model` e hanno il compito di creare una gerarchia ad albero di componenti. +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 i metodi `getName()` che restituisce il nome del componente e il metodo `getParent()` che restituisce il suo genitore. Entrambi possono essere impostati con il metodo `setParent()` - il primo parametro è il genitore e il secondo il nome del componente. +[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): ?Component .[method] ------------------------------------------- -Cerca nella gerarchia verso l'alto un oggetto della classe o interfaccia richiesta. Ad esempio, `$component->lookup(Nette\Application\UI\Presenter::class)` restituisce il presenter, se il componente è collegato ad esso, anche attraverso diversi livelli. +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): ?string .[method] -------------------------------------------- -Restituisce il cosiddetto percorso, che è una stringa creata concatenando i nomi di tutti i componenti nel percorso tra il componente corrente e quello cercato. Quindi, ad esempio, `$component->lookupPath(Nette\Application\UI\Presenter::class)` restituisce l'identificatore univoco del componente rispetto al presenter. +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è un componente che contiene discendenti e forma così una struttura ad albero. Dispone di metodi per aggiungere, ottenere e rimuovere facilmente oggetti. È l'antenato, ad esempio, del form o delle classi `Control` e `Presenter`. +[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. Quando si tenta di ottenere un discendente non definito, viene chiamata la factory `createComponent($name)`. Il metodo `createComponent($name)` chiama nel componente corrente il metodo `createComponent<nomeComponente>` e gli passa come parametro il nome del componente. Il componente creato viene quindi aggiunto al componente corrente come suo discendente. Questi metodi sono chiamati factory di componenti e possono essere implementati dai discendenti della classe `Container`. +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(): array .[method] --------------------------------- -Restituisce i discendenti diretti come array. Le chiavi contengono i nomi di questi componenti. Nota: nella versione 3.0.x il metodo restituiva un iteratore invece di un array e il suo primo parametro specificava se i componenti dovevano essere attraversati in profondità, e il secondo rappresentava un filtro di tipo. Questi parametri sono deprecati. +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(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Ottiene l'intera gerarchia dei componenti, inclusi tutti i componenti figli annidati, come un array indicizzato. La ricerca va prima in profondità. +getComponentTree(): list<IComponent> .[method] +---------------------------------------------- +Ottiene l'intera gerarchia dei componenti, compresi tutti i componenti figli annidati, come array indicizzato. La ricerca è in profondità. -Monitoraggio degli antenati -=========================== +Sorvegliare gli antenati +======================== -Il modello a componenti di Nette consente un lavoro molto dinamico con l'albero (possiamo rimuovere, spostare, aggiungere componenti), quindi sarebbe un errore fare affidamento sul fatto che dopo la creazione del componente sia immediatamente noto (nel costruttore) il genitore, il genitore del genitore, ecc. Di solito, infatti, il genitore non è affatto noto al momento della creazione. +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 sapere quando un componente è stato collegato all'albero del presenter? Monitorare il cambiamento del genitore non è sufficiente, perché al presenter potrebbe essere stato collegato, ad esempio, il genitore del genitore. Il metodo [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] aiuta. Ogni componente può monitorare un numero qualsiasi di classi e interfacce. L'allegamento o lo scollegamento viene segnalato chiamando il callback `$attached` rispettivamente `$detached`, e passando l'oggetto della classe monitorata. +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)`. -Per una migliore comprensione, un esempio: la classe `UploadControl`, che rappresenta l'elemento del form per l'upload di file in Nette Forms, deve impostare l'attributo `enctype` del form sul valore `multipart/form-data`. Al momento della creazione dell'oggetto, tuttavia, potrebbe non essere collegata a nessun form. In quale momento, quindi, modificare il form? La soluzione è semplice: nel costruttore si richiede il monitoraggio: +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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -e non appena il form è disponibile, viene chiamato il callback. (In passato, al suo posto venivano usati i metodi comuni `attached` rispettivamente `detached`). +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 new file mode 100644 index 0000000000..d14767c2db --- /dev/null +++ b/component-model/it/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 9d19e7312c..4647d0c8a2 100644 --- a/component-model/it/@meta.texy +++ b/component-model/it/@meta.texy @@ -1,2 +1 @@ {{sitename: Documentazione Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/it/upgrading.texy b/component-model/it/upgrading.texy new file mode 100644 index 0000000000..189a793eaf --- /dev/null +++ b/component-model/it/upgrading.texy @@ -0,0 +1,20 @@ +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 index 6f62c85626..6e004510e1 100644 --- a/component-model/ja/@home.texy +++ b/component-model/ja/@home.texy @@ -1,53 +1,65 @@ -コンポーネントモデル -********** +Component Model +*************** .[perex] -Netteにおける重要な概念はコンポーネントです。ページには[視覚的なインタラクティブコンポーネント |application:components]を挿入し、フォームやすべてのフォーム要素もコンポーネントです。これらすべてのコンポーネントが継承する基本的な2つのクラスは、`nette/component-model` パッケージの一部であり、コンポーネントの木構造階層を作成する役割を担っています。 +Nette の大事な考え方のひとつがコンポーネントです。ページには[目に見える対話的なコンポーネント |application:components]を差し込みます。フォームとそのすべての要素もコンポーネントです。これらすべてのコンポーネントが継承する 2 つの基本のクラスは `nette/component-model` のパッケージの一部で、コンポーネントの木の階層を作ることを受け持ちます。 Component ========= -[api:Nette\ComponentModel\Component]は、すべてのコンポーネントの共通の祖先です。コンポーネントの名前を返す `getName()` メソッドと、その親を返す `getParent()` メソッドを含みます。両方は `setParent()` メソッドで設定できます - 最初のパラメータは親で、2番目のパラメータはコンポーネントの名前です。 +[api:Nette\ComponentModel\Component]はすべてのコンポーネントの共通の祖先です。コンポーネントの名前を返す `getName()` メソッドと、その親を返す `getParent()` メソッドを持ちます。どちらも `setParent()` メソッドで設定できます。第 1 パラメータが親、第 2 パラメータがコンポーネントの名前です。 -lookup(string $type): ?Component .[method] ------------------------------------------- -階層を上方向に検索し、要求されたクラスまたはインターフェースのオブジェクトを見つけます。たとえば、`$component->lookup(Nette\Application\UI\Presenter::class)` は、コンポーネントが(数レベルを介してでも)Presenterに接続されている場合、Presenterを返します。 +lookup(?string $type, bool $throw=true): ?Component .[method] +------------------------------------------------------------- +階層を上へたどって、目当てのクラスやインターフェースのオブジェクトを探します。たとえば `$component->lookup(Nette\Application\UI\Presenter::class)` は、そのコンポーネントがプレゼンターにつながっていれば、何段か上でもそのプレゼンターを返します。合うオブジェクトが見つからなければ例外を投げます。代わりに `null` を返させたいなら、第 2 引数に `false` を渡します。`$type` に `null` を渡すと、このメソッドは木のいちばん上のコンポーネント、つまり親のない根を探します。 -lookupPath(string $type): ?string .[method] -------------------------------------------- -いわゆるパスを返します。これは、現在のコンポーネントと検索対象のコンポーネントの間のパスにあるすべてのコンポーネントの名前を結合して作成された文字列です。したがって、たとえば `$component->lookupPath(Nette\Application\UI\Presenter::class)` は、Presenterに対するコンポーネントの一意の識別子を返します。 +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` クラスの祖先です。 +[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` クラスの子孫で実装できます。 +コンポーネントを返します。定義されていない子を取り出そうとすると、ファクトリメソッド `createComponent($name)` が呼ばれます。`createComponent($name)` メソッドは、今のコンポーネントの `createComponent<コンポーネントの名前>` というメソッドを、コンポーネントの名前をパラメータとして渡して呼びます。作られたコンポーネントは、そのあと今のコンポーネントの子として足されます。これらのメソッドをコンポーネントのファクトリと呼び、`Container` を継承したクラスで実装できます。 -getComponents(): array .[method] --------------------------------- -直接の子を配列として返します。キーにはこれらのコンポーネントの名前が含まれます。注:バージョン3.0.xでは、このメソッドは配列の代わりにイテレータを返し、最初のパラメータはコンポーネントを深く走査するかどうかを指定し、2番目のパラメータは型フィルタを表していました。これらのパラメータは非推奨です。 +getComponents(): IComponent[] .[method] +--------------------------------------- +直接の子孫を配列として返します。キーにはそれらのコンポーネントの名前が入ります。部分木全体を再帰的に取り出すには `getComponentTree()` を、型で絞るなら `array_filter()` と組み合わせて使ってください。(古い版で知られていた `$deep` と `$filterType` のパラメータは、バージョン 4.0 で取り除かれました。) -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -すべてのネストされた子コンポーネントを含む完全なコンポーネント階層をインデックス付き配列として取得します。検索は最初に深さ優先で行われます。 +getComponentTree(): list<IComponent> .[method] +---------------------------------------------- +入れ子のすべての子のコンポーネントも含めた、コンポーネントの階層全体を添字の配列として取り出します。探索は深さ優先です。 -祖先の監視 -===== +祖先を見張る +====== -Netteコンポーネントモデルは、ツリーとの非常に動的な作業を可能にします(コンポーネントを削除、移動、追加できます)。したがって、コンポーネントが作成された直後(コンストラクタ内)に親、親の親などがわかっていると頼るのは間違いです。ほとんどの場合、作成時に親はまったくわかりません。 +Nette のコンポーネントのしくみは木をとても動的に扱えるので(コンポーネントを取り除いたり、移したり、足したりできます)、コンポーネントを作った直後(コンストラクタの中)に親や親の親などが分かっていると当てにするのは誤りです。ふつう、コンポーネントが作られる時点で親はまったく分かっていません。 -コンポーネントがPresenterツリーに接続されたことをいつ知るにはどうすればよいですか?親の変更を監視するだけでは不十分です。たとえば、親の親がPresenterに接続されている可能性があるためです。メソッド[monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]が役立ちます。各コンポーネントは、任意の数のクラスとインターフェースを監視できます。接続または切断は、コールバック `$attached` または `$detached` を呼び出し、監視対象クラスのオブジェクトを渡すことによって通知されます。 +では、コンポーネントは自分がプレゼンターの下(あるいは、ある型のほかの祖先の下)に取り付けられた瞬間をどう知るのでしょうか。直接の親を見張るだけでは足りません。つながりは木のもっと上で、たとえば親の親が取り付けられたときに起こるかもしれないからです。そのために [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()]メソッドがあります。コンポーネントは、クラスやインターフェース `$type` の祖先が木の中で自分の上に現れたとき、あるいはそこから消えたときに知らせてほしいと申し出ます。コンポーネントはいくつでも型を見張れます。`$attached` のコールバックは合う祖先がつながったときに発火し、その祖先を引数として受け取ります。`$detached` は切り離されたときに発火します。見張るのは `unmonitor($type)` でやめられます。 -よりよく理解するための例:Nette Formsのファイルアップロード用のフォーム要素を表す `UploadControl` クラスは、フォームの `enctype` 属性を `multipart/form-data` に設定する必要があります。しかし、オブジェクトが作成された時点では、どのフォームにも接続されていない可能性があります。では、どの時点でフォームを変更すればよいでしょうか?解決策は簡単です - コンストラクタで監視を要求します: +知らせは木の構造に沿って届きます。取り付けのときは祖先が子孫より先に知らされるので(上から下へ)、親が共有の状態を先に整えたり、子自身のコールバックが走る前にその子を取り除いたりできます。取り外しのときは順序が逆で、子孫が先に知らされます。コールバックの重複も取り除かれるので、同じオブジェクトについて同じコールバックが二度呼ばれることはありません。この振る舞いの理由は、[バージョン 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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -そして、フォームが利用可能になるとすぐに、コールバックが呼び出されます。(以前は、代わりに共通のメソッド `attached` または `detached` が使用されていました)。 +そしてフォームが使えるようになったとたん、そのコールバックが呼ばれます。 + + +新しい版へ上げるなら、[アップグレード |upgrading]のページをご覧ください。 diff --git a/component-model/ja/@left-menu.texy b/component-model/ja/@left-menu.texy new file mode 100644 index 0000000000..eb3baffa8e --- /dev/null +++ b/component-model/ja/@left-menu.texy @@ -0,0 +1,13 @@ +コンポーネントモデル +********** +- [概要 |@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 index 7d67dcb7b8..43b85f3cac 100644 --- a/component-model/ja/@meta.texy +++ b/component-model/ja/@meta.texy @@ -1,2 +1 @@ -{{sitename: Nette ドキュメンテーション}} -{{leftbar: nette:@menu-topics}} +{{sitename: Nette ドキュメント}} diff --git a/component-model/ja/upgrading.texy b/component-model/ja/upgrading.texy new file mode 100644 index 0000000000..601fd0da99 --- /dev/null +++ b/component-model/ja/upgrading.texy @@ -0,0 +1,20 @@ +アップグレード +******* + + +バージョン 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 index 245d650e59..eed25f2df7 100644 --- a/component-model/meta.json +++ b/component-model/meta.json @@ -1,5 +1,6 @@ { - "version": "3.x", + "version": "4.x", "repo": "nette/component-model", - "composer": "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 index b20f94a1f3..7391e0af14 100644 --- a/component-model/pl/@home.texy +++ b/component-model/pl/@home.texy @@ -2,52 +2,64 @@ Model komponentów ***************** .[perex] -Ważnym pojęciem w Nette jest komponent. Do stron wstawiamy [wizualne komponenty interaktywne |application:components], komponentami są również formularze lub wszystkie ich elementy. Podstawowe dwie klasy, od których dziedziczą wszystkie te komponenty, są częścią pakietu `nette/component-model` i mają za zadanie tworzyć hierarchię drzewa komponentów. +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] jest wspólnym przodkiem wszystkich komponentów. Zawiera metody `getName()` zwracającą nazwę komponentu i metodę `getParent()` zwracającą jego rodzica. Oboje można ustawić metodą `setParent()` - pierwszy parametr to rodzic, a drugi nazwa komponentu. +[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): ?Component .[method] ------------------------------------------- -Wyszukuje w hierarchii w górę obiekt żądanej klasy lub interfejsu. Na przykład `$component->lookup(Nette\Application\UI\Presenter::class)` zwraca presenter, jeśli komponent jest do niego dołączony, nawet przez kilka poziomów. +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): ?string .[method] -------------------------------------------- -Zwraca tzw. ścieżkę, czyli ciąg znaków powstały przez połączenie nazw wszystkich komponentów na ścieżce między bieżącym a szukanym komponentem. Zatem np. `$component->lookupPath(Nette\Application\UI\Presenter::class)` zwraca unikalny identyfikator komponentu względem presentera. +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] jest komponentem nadrzędnym, tj. komponentem zawierającym potomków i tworzącym w ten sposób strukturę drzewa. Dysponuje metodami do łatwego dodawania, pobierania i usuwania obiektów. Jest przodkiem na przykład formularza czy klas `Control` i `Presenter`. +[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. Przy próbie uzyskania niezdefiniowanego potomka jest wywoływana fabryka `createComponent($name)`. Metoda `createComponent($name)` wywołuje w bieżącym komponencie metodę `createComponent<nazwa komponentu>` i jako parametr przekazuje jej nazwę komponentu. Utworzony komponent jest następnie dodawany do bieżącego komponentu jako jego potomek. Te metody nazywamy fabrykami komponentów i mogą je implementować potomkowie klasy `Container`. +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(): array .[method] --------------------------------- -Zwraca bezpośrednich potomków jako tablicę. Klucze zawierają nazwy tych komponentów. Uwaga: w wersji 3.0.x metoda zamiast tablicy zwracała iterator, a jej pierwszy parametr określał, czy komponenty mają być przeglądane wgłąb, a drugi reprezentował filtr typów. Te parametry są przestarzałe. +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(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Pobiera całą hierarchię komponentów, w tym wszystkie zagnieżdżone komponenty podrzędne, jako tablicę indeksowaną. Przeszukiwanie odbywa się najpierw wgłąb. +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 umożliwia bardzo dynamiczną pracę z drzewem (komponenty możemy usuwać, przenosić, dodawać), dlatego błędem byłoby polegać na tym, że po utworzeniu komponentu od razu (w konstruktorze) znany jest rodzic, rodzic rodzica itd. Zazwyczaj bowiem rodzic przy tworzeniu w ogóle nie jest znany. +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 rozpoznać, kiedy komponent został dołączony do drzewa presentera? Śledzenie zmiany rodzica nie wystarczy, ponieważ do presentera mógł zostać dołączony na przykład rodzic rodzica. Pomocna jest metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Każdy komponent może monitorować dowolną liczbę klas i interfejsów. Dołączenie lub odłączenie jest sygnalizowane wywołaniem callbacku `$attached` lub `$detached`, i przekazaniem obiektu śledzonej klasy. +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)`. -Dla lepszego zrozumienia przykład: klasa `UploadControl`, reprezentująca element formularza do przesyłania plików w Nette Forms, musi ustawić atrybut `enctype` formularza na wartość `multipart/form-data`. W momencie tworzenia obiektu nie musi być jednak dołączona do żadnego formularza. W którym momencie więc zmodyfikować formularz? Rozwiązanie jest proste - w konstruktorze żąda się monitorowania: +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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -a gdy formularz jest dostępny, wywoływany jest callback. (Wcześniej zamiast niego używano wspólnej metody `attached` lub `detached`). +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 new file mode 100644 index 0000000000..7d68b3a733 --- /dev/null +++ b/component-model/pl/@left-menu.texy @@ -0,0 +1,13 @@ +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 index 08f2227fb5..61ac92d1af 100644 --- a/component-model/pl/@meta.texy +++ b/component-model/pl/@meta.texy @@ -1,2 +1 @@ {{sitename: Dokumentacja Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/pl/upgrading.texy b/component-model/pl/upgrading.texy new file mode 100644 index 0000000000..a89b507636 --- /dev/null +++ b/component-model/pl/upgrading.texy @@ -0,0 +1,20 @@ +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/pt/@home.texy b/component-model/pt/@home.texy deleted file mode 100644 index 2e57ace611..0000000000 --- a/component-model/pt/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Modelo de Componente -******************** - -.[perex] -Um conceito importante no Nette é o componente. Inserimos [componentes interativos visuais |application:components] nas páginas, formulários são componentes, assim como todos os seus elementos. As duas classes base das quais todos esses componentes herdam fazem parte do pacote `nette/component-model` e têm a tarefa de criar uma hierarquia de componentes em árvore. - - -Component -========= -[api:Nette\ComponentModel\Component] é o ancestral comum de todos os componentes. Contém os métodos `getName()` que retorna o nome do componente e `getParent()` que retorna seu pai. Ambos podem ser definidos usando o método `setParent()` - o primeiro parâmetro é o pai e o segundo é o nome do componente. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Procura na hierarquia para cima um objeto da classe ou interface solicitada. Por exemplo, `$component->lookup(Nette\Application\UI\Presenter::class)` retorna o presenter, se o componente estiver anexado a ele, mesmo através de vários níveis. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Retorna o chamado caminho, que é uma string formada pela concatenação dos nomes de todos os componentes no caminho entre o componente atual e o componente procurado. Assim, por exemplo, `$component->lookupPath(Nette\Application\UI\Presenter::class)` retorna um identificador único do componente em relação ao presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] é o componente pai, ou seja, um componente que contém descendentes e forma assim uma estrutura em árvore. Possui métodos para fácil adição, obtenção e remoção de objetos. É o ancestral, por exemplo, do formulário ou das classes `Control` e `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Retorna um componente. Ao tentar obter um descendente indefinido, a fábrica `createComponent($name)` é chamada. O método `createComponent($name)` chama o método `createComponent<nome do componente>` no componente atual e passa o nome do componente como parâmetro. O componente criado é então adicionado ao componente atual como seu descendente. Chamamos esses métodos de fábricas de componentes e eles podem ser implementados por descendentes da classe `Container`. - - -getComponents(): array .[method] --------------------------------- -Retorna os descendentes diretos como um array. As chaves contêm os nomes desses componentes. Nota: na versão 3.0.x, o método retornava um iterador em vez de um array, e seu primeiro parâmetro determinava se os componentes deveriam ser percorridos em profundidade, e o segundo representava um filtro de tipo. Esses parâmetros estão obsoletos. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Obtém toda a hierarquia de componentes, incluindo todos os componentes filhos aninhados, como um array indexado. A busca é feita primeiro em profundidade. - - -Monitoramento de Ancestrais -=========================== - -O modelo de componente Nette permite um trabalho muito dinâmico com a árvore (podemos remover, mover, adicionar componentes), portanto seria um erro confiar que, após a criação de um componente, o pai, o pai do pai, etc., sejam imediatamente conhecidos (no construtor). Na maioria das vezes, o pai não é conhecido durante a criação. - -Como saber quando um componente foi anexado à árvore do presenter? Observar a mudança do pai não é suficiente, porque o pai do pai pode ter sido anexado ao presenter, por exemplo. O método [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] ajuda. Cada componente pode monitorar qualquer número de classes e interfaces. A anexação ou desanexação é sinalizada chamando o callback `$attached` ou `$detached`, respectivamente, e passando o objeto da classe monitorada. - -Para melhor compreensão, um exemplo: a classe `UploadControl`, que representa o elemento de formulário para upload de arquivos no Nette Forms, precisa definir o atributo `enctype` do formulário para o valor `multipart/form-data`. No entanto, no momento da criação do objeto, ele pode não estar anexado a nenhum formulário. Em que momento, então, modificar o formulário? A solução é simple - no construtor, solicita-se o monitoramento: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -e assim que o formulário estiver disponível, o callback é chamado. (Anteriormente, os métodos comuns `attached` e `detached` eram usados em seu lugar). diff --git a/component-model/pt/@meta.texy b/component-model/pt/@meta.texy deleted file mode 100644 index e2566bcb44..0000000000 --- a/component-model/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentação Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/ro/@home.texy b/component-model/ro/@home.texy deleted file mode 100644 index 6bf15157b2..0000000000 --- a/component-model/ro/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Modelul de componente -********************* - -.[perex] -Un concept important în Nette este componenta. În pagini inserăm [componente vizuale interactive |application:components], componente sunt și formularele sau toate elementele lor. Cele două clase de bază, din care moștenesc toate aceste componente, fac parte din pachetul `nette/component-model` și au rolul de a crea o ierarhie arborescentă de componente. - - -Component -========= -[api:Nette\ComponentModel\Component] este strămoșul comun al tuturor componentelor. Conține metodele `getName()` care returnează numele componentei și metoda `getParent()` care returnează părintele său. Ambele pot fi setate cu metoda `setParent()` - primul parametru este părintele și al doilea este numele componentei. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Caută în ierarhie în sus un obiect de clasa sau interfața dorită. De exemplu, `$component->lookup(Nette\Application\UI\Presenter::class)` returnează presenter-ul, dacă componenta este atașată la acesta, chiar și prin mai multe niveluri. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Returnează așa-numita cale, care este un șir de caractere format prin concatenarea numelor tuturor componentelor de pe calea dintre componenta curentă și cea căutată. Deci, de exemplu, `$component->lookupPath(Nette\Application\UI\Presenter::class)` returnează un identificator unic al componentei față de presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] este componenta părinte, adică o componentă care conține descendenți și formează astfel o structură arborescentă. Dispune de metode pentru adăugarea, obținerea și eliminarea ușoară a obiectelor. Este strămoșul, de exemplu, al formularului sau al claselor `Control` și `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Returnează componenta. La încercarea de a obține un descendent nedefinit, este apelată fabrica `createComponent($name)`. Metoda `createComponent($name)` apelează în componenta curentă metoda `createComponent<nume componenta>` și îi transmite ca parametru numele componentei. Componenta creată este apoi adăugată la componenta curentă ca descendent al acesteia. Aceste metode le numim fabrici de componente și pot fi implementate de descendenții clasei `Container`. - - -getComponents(): array .[method] --------------------------------- -Returnează descendenții direcți ca array. Cheile conțin numele acestor componente. Notă: în versiunea 3.0.x, metoda returna un iterator în loc de array, iar primul său parametru specifica dacă componentele trebuie parcurse în adâncime, iar al doilea reprezenta un filtru de tip. Acești parametri sunt depreciați. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Obține întreaga ierarhie de componente, inclusiv toate componentele subordonate imbricate, ca un array indexat. Căutarea se face mai întâi în adâncime. - - -Monitorizarea strămoșilor -========================= - -Modelul de componente Nette permite o muncă foarte dinamică cu arborele (putem elimina, muta, adăuga componente), de aceea ar fi o greșeală să ne bazăm pe faptul că, după crearea componentei, părintele, părintele părintelui etc. sunt imediat cunoscuți (în constructor). De obicei, părintele nu este deloc cunoscut la creare. - -Cum să aflăm când a fost componenta atașată la arborele presenter-ului? Urmărirea schimbării părintelui nu este suficientă, deoarece la presenter ar fi putut fi atașat, de exemplu, părintele părintelui. Ajută metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Fiecare componentă poate monitoriza orice număr de clase și interfețe. Atașarea sau detașarea este anunțată prin apelarea callback-ului `$attached` respectiv `$detached`, și transmiterea obiectului clasei monitorizate. - -Pentru o mai bună înțelegere, un exemplu: clasa `UploadControl`, reprezentând elementul de formular pentru încărcarea fișierelor în Nette Forms, trebuie să seteze atributul `enctype` al formularului la valoarea `multipart/form-data`. Dar în momentul creării obiectului, este posibil să nu fie atașată la niciun formular. În ce moment, deci, să modificăm formularul? Soluția este simplă - în constructor se solicită monitorizarea: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -și de îndată ce formularul este disponibil, se apelează callback-ul. (Anterior, în locul său se folosea metoda comună `attached` respectiv `detached`). diff --git a/component-model/ro/@meta.texy b/component-model/ro/@meta.texy deleted file mode 100644 index 6554692600..0000000000 --- a/component-model/ro/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentație Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/ru/@home.texy b/component-model/ru/@home.texy index 2b89ffd9fb..c4a42dd817 100644 --- a/component-model/ru/@home.texy +++ b/component-model/ru/@home.texy @@ -2,52 +2,64 @@ ******************* .[perex] -Важным понятием в Nette является компонент. В страницы мы вставляем [визуальные интерактивные компоненты |application:components], компонентами также являются формы или все их элементы. Два базовых класса, от которых наследуются все эти компоненты, являются частью пакета `nette/component-model` и отвечают за создание древовидной иерархии компонентов. +Важное понятие в Nette - компонент. В страницы мы вставляем [визуальные интерактивные компоненты |application:components]; компонентами являются и формы, и все их элементы. Два базовых класса, от которых наследуются все эти компоненты, входят в пакет `nette/component-model` и отвечают за создание иерархии дерева компонентов. Component ========= -[api:Nette\ComponentModel\Component] является общим предком всех компонентов. Он содержит методы `getName()`, возвращающий имя компонента, и метод `getParent()`, возвращающий его родителя. Оба можно установить методом `setParent()` - первый параметр - родитель, а второй - имя компонента. +[api:Nette\ComponentModel\Component] - общий предок всех компонентов. У него есть метод `getName()`, возвращающий имя компонента, и метод `getParent()`, возвращающий его родителя. Оба можно задать методом `setParent()`: первый параметр - родитель, второй - имя компонента. -lookup(string $type): ?Component .[method] ------------------------------------------- -Ищет в иерархии вверх объект требуемого класса или интерфейса. Например, `$component->lookup(Nette\Application\UI\Presenter::class)` возвращает презентер, если компонент присоединен к нему, даже через несколько уровней. +lookup(?string $type, bool $throw=true): ?Component .[method] +------------------------------------------------------------- +Ищет вверх по иерархии объект нужного класса или интерфейса. Например, `$component->lookup(Nette\Application\UI\Presenter::class)` возвращает презентер, если компонент к нему подключён, пусть даже через несколько уровней. Если подходящий объект не найден, метод выбрасывает исключение; передайте вторым аргументом `false`, чтобы вместо этого вернулся `null`. Если в качестве `$type` передать `null`, метод ищет самый верхний компонент в дереве, то есть корень без родителя. -lookupPath(string $type): ?string .[method] -------------------------------------------- -Возвращает так называемый путь, который представляет собой строку, образованную соединением имен всех компонентов на пути между текущим и искомым компонентом. Так, например, `$component->lookupPath(Nette\Application\UI\Presenter::class)` возвращает уникальный идентификатор компонента относительно презентера. +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`. +[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`. +Возвращает компонент. Попытка получить неопределённого потомка вызывает фабричный метод `createComponent($name)`. Метод `createComponent($name)` вызывает в текущем компоненте метод `createComponent<имя компонента>`, передавая имя компонента параметром. Созданный компонент затем добавляется в текущий компонент как его потомок. Эти методы мы называем фабриками компонентов, и их можно реализовать в классах, унаследованных от `Container`. -getComponents(): array .[method] --------------------------------- -Возвращает прямых потомков в виде массива. Ключи содержат имена этих компонентов. Примечание: в версии 3.0.x метод вместо массива возвращал итератор, и его первый параметр определял, следует ли проходить компоненты вглубь, а второй представлял собой фильтр по типу. Эти параметры устарели. +getComponents(): IComponent[] .[method] +--------------------------------------- +Возвращает непосредственных потомков массивом; в ключах находятся имена этих компонентов. Чтобы получить всё поддерево рекурсивно, используйте `getComponentTree()`, при необходимости в сочетании с `array_filter()` для фильтрации по типу. (Параметры `$deep` и `$filterType`, известные по прежним версиям, в версии 4.0 удалены.) -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Получает всю иерархию компонентов, включая все вложенные дочерние компоненты, в виде индексированного массива. Поиск идет сначала вглубь. +getComponentTree(): list<IComponent> .[method] +---------------------------------------------- +Получает всю иерархию компонентов, включая все вложенные компоненты-потомки, в виде индексированного массива. Обход идёт в глубину. -Мониторинг предков -================== +Наблюдение за предками +====================== -Компонентная модель Nette позволяет очень динамично работать с деревом (компоненты можно извлекать, перемещать, добавлять), поэтому было бы ошибкой полагаться на то, что после создания компонента сразу (в конструкторе) известен родитель, родитель родителя и т. д. В большинстве случаев родитель при создании вообще неизвестен. +Компонентная модель Nette позволяет очень динамично работать с деревом (мы можем удалять, перемещать, добавлять компоненты), поэтому было бы ошибкой полагаться на то, что сразу после создания компонента (в конструкторе) известны родитель, родитель родителя и т. д. Обычно при создании компонента родитель вообще неизвестен. -Как узнать, когда компонент был присоединен к дереву презентера? Отслеживать изменение родителя недостаточно, так как к презентеру мог быть присоединен, например, родитель родителя. Поможет метод [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Каждый компонент может отслеживать любое количество классов и интерфейсов. Присоединение или отсоединение сообщается вызовом callback-функции `$attached` соответственно `$detached`, и передачей объекта отслеживаемого класса. +Как компоненту узнать момент, когда он окажется под презентером или под любым другим предком заданного типа? Следить за непосредственным родителем недостаточно, потому что соединение может произойти выше по дереву, например когда присоединяется родитель родителя. Для этого и служит метод [monitor($type, $attached, $detached) |api:Nette\ComponentModel\Component::monitor()]: компонент объявляет, что хочет получать уведомление всякий раз, когда над ним в дереве появляется предок класса или интерфейса `$type` либо исчезает из него. Компонент может наблюдать за сколькими угодно типами; callback `$attached` срабатывает, когда подходящий предок подключается, и получает этого предка аргументом, а `$detached` срабатывает при отключении. Наблюдение можно снова прекратить через `unmonitor($type)`. -Для лучшего понимания пример: класс `UploadControl`, представляющий элемент формы для загрузки файлов в Nette Forms, должен установить атрибут `enctype` формы на значение `multipart/form-data`. Однако в момент создания объекта он может не быть присоединен ни к какой форме. В какой момент тогда модифицировать форму? Решение простое - в конструкторе запрашивается мониторинг: +Уведомления следуют структуре дерева. При присоединении предок уведомляется раньше своих потомков (сверху вниз), так что родитель может сначала подготовить общее состояние или даже удалить потомка до того, как выполнится собственный 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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -и как только форма становится доступной, вызывается callback. (Раньше вместо него использовался общий метод `attached` соответственно `detached`). +и как только форма станет доступна, callback будет вызван. + + +Если вы переходите на более новую версию, посмотрите страницу [обновления |upgrading]. diff --git a/component-model/ru/@left-menu.texy b/component-model/ru/@left-menu.texy new file mode 100644 index 0000000000..b7d69c476a --- /dev/null +++ b/component-model/ru/@left-menu.texy @@ -0,0 +1,13 @@ +Модель компонентов +****************** +- [Обзор |@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 index 61577d6323..7f329adfce 100644 --- a/component-model/ru/@meta.texy +++ b/component-model/ru/@meta.texy @@ -1,2 +1 @@ {{sitename: Документация Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/ru/upgrading.texy b/component-model/ru/upgrading.texy new file mode 100644 index 0000000000..62eec079e9 --- /dev/null +++ b/component-model/ru/upgrading.texy @@ -0,0 +1,20 @@ +Обновление +********** + + +Обновление до версии 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/sl/@home.texy b/component-model/sl/@home.texy deleted file mode 100644 index cc2bd1f48c..0000000000 --- a/component-model/sl/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Komponentni model -***************** - -.[perex] -Pomemben pojem v Nette je komponenta. V strani vstavljamo [vizualne interaktivne komponente |application:components], komponente so tudi obrazci ali vsi njihovi elementi. Osnovna dva razreda, od katerih vse te komponente dedujejo, sta del paketa `nette/component-model` in imata nalogo ustvarjati drevesno hierarhijo komponent. - - -Component -========= -[api:Nette\ComponentModel\Component] je skupni prednik vseh komponent. Vsebuje metodi `getName()`, ki vrača ime komponente, in metodo `getParent()`, ki vrača njenega starša. Oboje lahko nastavimo z metodo `setParent()` - prvi parameter je starš in drugi ime komponente. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -V hierarhiji navzgor poišče objekt zahtevanega razreda ali vmesnika. Na primer `$component->lookup(Nette\Application\UI\Presenter::class)` vrne presenter, če je komponenta nanj, tudi preko več nivojev, priključena. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Vrača t.i. pot, kar je niz, nastal s spajanjem imen vseh komponent na poti med trenutno in iskano komponento. Torej npr. `$component->lookupPath(Nette\Application\UI\Presenter::class)` vrača edinstven identifikator komponente glede na presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] je starševska komponenta, tj. komponenta, ki vsebuje potomce in tako tvori drevesno strukturo. Ima metode za enostavno dodajanje, pridobivanje in odstranjevanje objektov. Je prednik na primer obrazca ali razredov `Control` in `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Vrača komponento. Pri poskusu pridobivanja nedefiniranega potomca se pokliče tovarna `createComponent($name)`. Metoda `createComponent($name)` v trenutni komponenti pokliče metodo `createComponent<ime komponente>` in ji kot parameter posreduje ime komponente. Ustvarjena komponenta se nato doda v trenutno komponento kot njen potomec. Tem metodam rečemo tovarne komponent in jih lahko implementirajo potomci razreda `Container`. - - -getComponents(): array .[method] --------------------------------- -Vrača neposredne potomce kot polje. Ključi vsebujejo imena teh komponent. Opomba: v različici 3.0.x je metoda namesto polja vračala iterator in njen prvi parameter je določal, ali naj se komponente prehajajo v globino, drugi pa je predstavljal tipski filter. Ti parametri so zastareli. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Pridobi celotno hierarhijo komponent, vključno z vsemi gnezdenimi podrejenimi komponentami, kot indeksirano polje. Iskanje gre najprej v globino. - - -Spremljanje prednikov -===================== - -Komponentni model Nette omogoča zelo dinamično delo z drevesom (komponente lahko odstranjujemo, premikamo, dodajamo), zato bi bila napaka zanašati se na to, da je po ustvarjanju komponente takoj (v konstruktorju) znan starš, starš starša itd. Večinoma namreč starš ob ustvarjanju sploh ni znan. - -Kako ugotoviti, kdaj je bila komponenta priključena v drevo presenterja? Spremljanje spremembe starša ni dovolj, saj je bil lahko k presenterju priključen na primer starš starša. Pomaga metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Vsaka komponenta lahko spremlja poljubno število razredov in vmesnikov. Priključitev ali odklop je sporočen s klicem povratnega klica `$attached` oz. `$detached`, in posredovanjem objekta spremljanega razreda. - -Za boljše razumevanje primer: razred `UploadControl`, ki predstavlja obrazčevni element za nalaganje datotek v Nette Forms, mora obrazcu nastaviti atribut `enctype` na vrednost `multipart/form-data`. V času ustvarjanja objekta pa ni nujno, da je priključen na kakršenkoli obrazec. V katerem trenutku torej modificirati obrazec? Rešitev je enostavna - v konstruktorju se zahteva spremljanje: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -in takoj ko je obrazec na voljo, se pokliče povratni klic. (Prej se je namesto njega uporabljala skupna metoda `attached` oz. `detached`). diff --git a/component-model/sl/@meta.texy b/component-model/sl/@meta.texy deleted file mode 100644 index 282883a3d6..0000000000 --- a/component-model/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Dokumentacija}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/tr/@home.texy b/component-model/tr/@home.texy index 8b8015cab4..35ee741b3c 100644 --- a/component-model/tr/@home.texy +++ b/component-model/tr/@home.texy @@ -2,52 +2,64 @@ Bileşen Modeli ************** .[perex] -Nette'de önemli bir kavram bileşendir. Sayfalara [görsel etkileşimli bileşenler |application:components] ekleriz, formlar veya tüm öğeleri de bileşenlerdir. Tüm bu bileşenlerin miras aldığı temel iki sınıf, `nette/component-model` paketinin bir parçasıdır ve bileşenlerin ağaç hiyerarşisini oluşturmaktan sorumludur. +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()` yöntemini ve ebeveynini döndüren `getParent()` yöntemini içerir. Her ikisi de `setParent()` yöntemiyle ayarlanabilir - ilk parametre ebeveyn, ikincisi bileşenin adıdır. +[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): ?Component .[method] ------------------------------------------- -Hiyerarşide yukarı doğru istenen sınıf veya arayüzün nesnesini arar. Örneğin, `$component->lookup(Nette\Application\UI\Presenter::class)`, bileşen ona birkaç seviye üzerinden bile bağlıysa presenter'ı döndürü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): ?string .[method] -------------------------------------------- -Yol adı verilen, geçerli ve aranan bileşen arasındaki yoldaki tüm bileşenlerin adlarının birleştirilmesiyle oluşan bir dize döndürür. 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. +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], ebeveyn bileşendir, yani alt öğeleri içeren ve böylece bir ağaç yapısı oluşturan bir bileşendir. Nesneleri kolayca eklemek, almak ve kaldırmak için yöntemlere sahiptir. Örneğin formun veya `Control` ve `Presenter` sınıflarının atasıdır. +[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] ------------------------------------------------ -Bileşeni döndürür. Tanımlanmamış bir alt öğeyi almaya çalışırken, `createComponent($name)` fabrikası çağrılır. `createComponent($name)` yöntemi, geçerli bileşende `createComponent<bileşen adı>` yöntemini çağırır ve parametre olarak bileşenin adını geçirir. Oluşturulan bileşen daha sonra geçerli bileşene alt öğesi olarak eklenir. Bu yöntemlere bileşen fabrikaları diyoruz ve `Container` sınıfının alt sınıfları tarafından uygulanabilirler. +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(): array .[method] --------------------------------- -Doğrudan alt öğeleri bir dizi olarak döndürür. Anahtarlar bu bileşenlerin adlarını içerir. Not: 3.0.x sürümünde, yöntem bir dizi yerine bir yineleyici döndürüyordu ve ilk parametresi bileşenlerin derinlemesine taranıp taranmayacağını belirtiyordu ve ikincisi bir tür filtresi temsil ediyordu. Bu parametreler kullanımdan kaldırılmıştır. +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(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Tüm iç içe geçmiş alt bileşenler dahil olmak üzere tüm bileşen hiyerarşisini dizinlenmiş bir dizi olarak alır. Arama önce derinlemesine yapılır. +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 olanak tanır (bileşenleri kaldırabilir, taşıyabilir, ekleyebiliriz), bu nedenle bir bileşen oluşturulduktan sonra ebeveynin, ebeveynin ebeveyninin vb. hemen (kurucuda) bilindiğine güvenmek bir hata olur. Çoğu zaman, ebeveyn oluşturma sırasında hiç bilinmez. +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şenin presenter ağacına ne zaman bağlandığını nasıl anlarız? Ebeveyn değişikliğini izlemek yeterli değildir, çünkü örneğin ebeveynin ebeveyni presenter'a bağlanmış olabilir. [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] yöntemi yardımcı olur. Her bileşen, herhangi bir sayıda sınıfı ve arayüzü izleyebilir. Bağlanma veya ayrılma, `$attached` veya `$detached` geri çağrısının çağrılmasıyla ve izlenen sınıfın nesnesinin geçirilmesiyle bildirilir. +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. -Daha iyi anlamak için bir örnek: Nette Forms'daki dosya yükleme form öğesini temsil eden `UploadControl` sınıfı, formun `enctype` niteliğini `multipart/form-data` değerine ayarlamalıdır. Ancak nesne oluşturulduğunda herhangi bir forma bağlı olmayabilir. Öyleyse formu hangi noktada değiştirmeli? Çözüm basittir - kurucuda izleme istenir: +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 @@ -64,4 +76,7 @@ class UploadControl extends Nette\Forms\Controls\BaseControl } ``` -ve form kullanılabilir olduğunda, geri çağrı çağrılır. (Daha önce bunun yerine ortak `attached` veya `detached` yöntemi kullanılıyordu). +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 new file mode 100644 index 0000000000..836f507021 --- /dev/null +++ b/component-model/tr/@left-menu.texy @@ -0,0 +1,13 @@ +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 index e5c5cea355..8dfe82f311 100644 --- a/component-model/tr/@meta.texy +++ b/component-model/tr/@meta.texy @@ -1,2 +1 @@ {{sitename: Nette Dokümantasyonu}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/tr/upgrading.texy b/component-model/tr/upgrading.texy new file mode 100644 index 0000000000..f5b61e3e6d --- /dev/null +++ b/component-model/tr/upgrading.texy @@ -0,0 +1,20 @@ +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/component-model/uk/@home.texy b/component-model/uk/@home.texy deleted file mode 100644 index 34b27ba85e..0000000000 --- a/component-model/uk/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Компонентна модель -****************** - -.[perex] -Важливим поняттям у Nette є компонент. На сторінки ми вставляємо [візуальні інтерактивні компоненти |application:components], компонентами є також форми або всі їхні елементи. Основні два класи, від яких успадковуються всі ці компоненти, є частиною пакету `nette/component-model` і мають на меті створення ієрархії компонентів у вигляді дерева. - - -Component -========= -[api:Nette\ComponentModel\Component] є спільним предком усіх компонентів. Він містить методи `getName()`, що повертає назву компонента, та метод `getParent()`, що повертає його батька. Обидва можна встановити методом `setParent()` - перший параметр - батько, а другий - назва компонента. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Шукає в ієрархії вгору об'єкт потрібного класу або інтерфейсу. Наприклад, `$component->lookup(Nette\Application\UI\Presenter::class)` повертає presenter, якщо компонент приєднаний до нього, навіть через кілька рівнів. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Повертає так званий шлях, який є рядком, утвореним з'єднанням імен усіх компонентів на шляху між поточним та шуканим компонентом. Так, наприклад, `$component->lookupPath(Nette\Application\UI\Presenter::class)` повертає унікальний ідентифікатор компонента відносно presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] є батьківським компонентом, тобто компонентом, що містить нащадків і таким чином утворює деревоподібну структуру. Він має методи для легкого додавання, отримання та видалення об'єктів. Він є предком, наприклад, форми або класів `Control` та `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Повертає компонент. При спробі отримати невизначеного нащадка викликається фабрика `createComponent($name)`. Метод `createComponent($name)` викликає в поточному компоненті метод `createComponent<назва компонента>` і передає йому як параметр назву компонента. Створений компонент потім додається до поточного компонента як його нащадок. Ці методи називаються фабриками компонентів і можуть бути реалізовані нащадками класу `Container`. - - -getComponents(): array .[method] --------------------------------- -Повертає прямих нащадків у вигляді масиву. Ключі містять назви цих компонентів. Примітка: у версії 3.0.x метод повертав ітератор замість масиву, а його перший параметр визначав, чи слід проходити компоненти в глибину, а другий представляв фільтр типів. Ці параметри є застарілими. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Отримує всю ієрархію компонентів, включаючи всі вкладені дочірні компоненти, у вигляді індексованого масиву. Пошук спочатку йде в глибину. - - -Моніторинг предків -================== - -Компонентна модель Nette дозволяє дуже динамічно працювати з деревом (компоненти можна видаляти, переміщати, додавати), тому було б помилкою покладатися на те, що після створення компонента відразу (в конструкторі) відомий батько, батько батька і т.д. Зазвичай батько при створенні взагалі не відомий. - -Як дізнатися, коли компонент був приєднаний до дерева presenter? Спостерігати за зміною батька недостатньо, оскільки до presenter міг бути приєднаний, наприклад, батько батька. Допоможе метод [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Кожен компонент може моніторити будь-яку кількість класів та інтерфейсів. Приєднання або від'єднання повідомляється викликом callback-функції `$attached` або `$detached` відповідно, і передачею об'єкта відстежуваного класу. - -Для кращого розуміння приклад: клас `UploadControl`, що представляє елемент форми для завантаження файлів у Nette Forms, повинен встановити атрибут `enctype` форми на значення `multipart/form-data`. Однак у момент створення об'єкта він може не бути приєднаним до жодної форми. В який момент тоді модифікувати форму? Рішення просте - в конструкторі запитується моніторинг: - -```php -class UploadControl extends Nette\Forms\Controls\BaseControl -{ - public function __construct($label) - { - $this->monitor(Nette\Forms\Form::class, function ($form): void { - $form->setHtmlAttribute('enctype', 'multipart/form-data'); - }); - // ... - } - - // ... -} -``` - -і як тільки форма стає доступною, викликається callback. (Раніше замість нього використовувалися спільні методи `attached` або `detached`). diff --git a/component-model/uk/@meta.texy b/component-model/uk/@meta.texy deleted file mode 100644 index 083a8ab9f7..0000000000 --- a/component-model/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документація Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/contributing/bg/@home.texy b/contributing/bg/@home.texy deleted file mode 100644 index 628390697b..0000000000 --- a/contributing/bg/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Станете сътрудник на Nette -************************** - -.[perex] -Научете как можете да се включите в нашия open source проект. Овладейте процедурите за принос към изходния код и документацията и станете част от общността на разработчиците, които активно участват в подобряването на Nette. - - -**Код** - -- [Как да допринесем към кода? |code] -- [Стандарт за кодиране |coding-standard] - -**Документация** - -- [Как да допринесем към документацията? |documentation] -- [Синтаксис на документацията |syntax] -- "Редактор за предварителен преглед":https://editor.nette.org diff --git a/contributing/bg/@left-menu.texy b/contributing/bg/@left-menu.texy deleted file mode 100644 index 111ad4339d..0000000000 --- a/contributing/bg/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Код -*** -- [Как да допринесем към кода? |code] -- [Стандарт за кодиране |coding-standard] - -Документация -************ -- [Как да допринесем към документацията? |documentation] -- [Синтаксис на документацията |syntax] -- "Редактор за предварителен преглед":https://editor.nette.org diff --git a/contributing/bg/code.texy b/contributing/bg/code.texy deleted file mode 100644 index 9e609453a3..0000000000 --- a/contributing/bg/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Как да допринесете към кода -*************************** - -.[perex] -Подготвяте се да допринесете към Nette Framework и трябва да се ориентирате в правилата и процедурите? Този наръчник за начинаещи ще ви покаже стъпка по стъпка как ефективно да допринасяте към кода, да работите с хранилища и да внедрявате промени. - - -Процедура -========= - -За да допринесете към кода, е необходимо да имате акаунт в [GitHub|https://github.com] и да сте запознати с основите на работа със системата за контрол на версиите Git. Ако не владеете работата с Git, можете да разгледате ръководството [git - the simple guide |https://rogerdudler.github.io/git-guide/] и евентуално да използвате някой от многото [графични клиенти |https://git-scm.com/downloads/guis]. - - -Подготовка на средата и хранилището ------------------------------------ - -1) В GitHub си създайте [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] на хранилището на [пакета |www:packages], който се готвите да промените -2) [Клонирайте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] това хранилище на своя компютър -3) Инсталирайте зависимостите, включително [Nette Tester |tester:], с командата `composer install` -4) Проверете дали тестовете работят, като стартирате `composer tester` -5) Създайте си [#нов branch] базиран на последната издадена версия - - -Внедряване на собствени промени -------------------------------- - -Сега можете да направите своите собствени промени в кода: - -1) програмирайте желаните промени и не забравяйте тестовете -2) уверете се, че тестовете преминават успешно, с помощта на `composer tester` -3) проверете дали кодът отговаря на [стандарта за кодиране |#Стандарти за кодиране] -4) запазете промените (commit) с описание в [този формат |#Описание на commit] - -Можете да създадете няколко commit-а, по един за всяка логическа стъпка. Всеки commit трябва да бъде смислен сам по себе си. - - -Изпращане на промените ----------------------- - -След като сте доволни от промените, можете да ги изпратите: - -1) изпратете (push) промените в GitHub към вашия fork -2) оттам ги изпратете към Nette хранилището, като създадете [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) посочете в описанието [достатъчно информация |#Описание на pull request] - - -Обработване на забележките --------------------------- - -Вашите commit-и сега ще бъдат видени и от други. Обичайно е да получите коментари със забележки: - -1) следете предложените корекции -2) обработете ги като нови commit-и или ги [обединете с предишните |https://help.github.com/en/github/using-git/about-git-rebase] -3) отново изпратете commit-ите в GitHub и те автоматично ще се появят в pull request-а - -Никога не създавайте нов pull request за корекция на съществуващ. - - -Документация ------------- - -Ако сте променили функционалност или сте добавили нова, не забравяйте да я [добавите и в документацията |documentation]. - - -Нов branch -========== - -Ако е възможно, правете промените спрямо последната издадена версия, т.е. последния таг в дадения branch. За таг `v3.2.1` ще създадете branch с тази команда: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Стандарти за кодиране -===================== - -Вашият код трябва да отговаря на [стандарта за кодиране |coding-standard], използван в Nette Framework. За проверка и корекция на кода е наличен автоматичен инструмент. Може да бъде инсталиран чрез Composer **глобално** във ваша избрана папка: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Сега трябва да можете да стартирате инструмента в терминала. С първата команда ще проверите, а с втората и ще коригирате кода в папките `src` и `tests` в текущата директория: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Описание на commit -================== - -В Nette темите на commit-ите имат формат: `Presenter: fixed AJAX detection [Closes #69]` - -- област, последвана от двоеточие -- целта на commit-а в минало време; ако е възможно, започнете с думата: "added" (добавена нова функционалност), "fixed" (корекция), "refactored" (промяна в кода без промяна на поведението), "changed", "removed" -- ако commit-ът нарушава обратната съвместимост, добавете "BC break" -- евентуална връзка към issue tracker като `(#123)` или `[Closes #69]` -- след темата може да последва един празен ред и след това по-подробно описание, включително например връзки към форума - - -Описание на pull request -======================== - -При създаване на pull request интерфейсът на GitHub ще ви позволи да въведете заглавие и описание. Посочете описателно заглавие и в описанието предоставете колкото се може повече информация за причините за вашата промяна. - -Ще се покаже и заглавие, където да посочите дали става въпрос за нова функция или корекция на грешка и дали може да настъпи нарушаване на обратната съвместимост (BC break). Ако има свързан проблем (issue), посочете го, за да бъде затворен след одобрение на pull request-а. - -``` -- bug fix / new feature? <!-- #issue номера, ако има --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- силно приветствано, вижте https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/bg/coding-standard.texy b/contributing/bg/coding-standard.texy deleted file mode 100644 index a6371c01dd..0000000000 --- a/contributing/bg/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Стандарт за кодиране -******************** - -.[perex] -Този документ описва правилата и препоръките за разработка на Nette. При допринасяне на код към Nette трябва да ги спазвате. Най-лесният начин да го направите е да имитирате съществуващия код. Целта е целият код да изглежда така, сякаш е написан от един човек. - -Стандартът за кодиране на Nette отговаря на [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/], с две основни изключения: за отстъп използва [#табулатори вместо интервали] и за [константи на класове използва PascalCase|https://blog.nette.org/bg/for-less-screaming-in-the-code]. - - -Общи правила -============ - -- Всеки PHP файл трябва да съдържа `declare(strict_types=1)` -- Два празни реда се използват за разделяне на методи за по-добра четливост. -- Причината за използване на shut-up оператора трябва да бъде документирана: `@mkdir($dir); // @ - директорията може да съществува`. -- Ако се използва оператор за сравнение със слабо типизиране (т.е. `==`, `!=`, ...), намерението трябва да бъде документирано: `// == приема null` -- В един файл `exceptions.php` можете да запишете няколко изключения. -- При интерфейсите не се специфицира видимостта на методите, тъй като те винаги са публични. -- Всяко свойство, върната стойност и параметър трябва да имат посочен тип. Обратно, при `final` константи никога не посочваме тип, тъй като той е очевиден. -- За ограждане на низ трябва да се използват единични кавички, с изключение на случаите, когато самият литерал съдържа апострофи. - - -Конвенции за именуване -====================== - -- Не използвайте съкращения, освен ако цялото име не е твърде дълго. -- При двубуквени съкращения използвайте главни букви, при по-дълги съкращения PascalCase/camelCase. -- За име на клас използвайте съществително име или словосъчетание. -- Имената на класовете трябва да съдържат не само спецификата (`Array`), но и общността (`ArrayIterator`). Изключение са PHP атрибутите. -- "Константите на класове и енумите трябва да използват PascalCaps":https://blog.nette.org/bg/for-less-screaming-in-the-code. -- "Интерфейсите и абстрактните класове не трябва да съдържат префикси или суфикси":https://blog.nette.org/bg/prefixes-and-suffixes-do-not-belong-in-interface-names като `Abstract`, `Interface` или `I`. - - -Обвиване и скоби -================ - -Стандартът за кодиране на Nette отговаря на PSR-12 (респ. PER Coding Style), в някои точки го допълва или променя: - -- arrow функциите се пишат без интервал преди скобата, т.е. `fn($a) => $b`. -- не се изисква празен ред между различните типове `use` импортиращи изрази. -- типът на връщаната стойност на функция/метод и началната фигурна скоба винаги са на отделни редове: - -```php - public function find( - string $dir, - array $options, - ): array - { - // тяло на метода - } -``` - -Началната фигурна скоба на отделен ред е важна за визуалното разделяне на сигнатурата на функцията/метода от тялото. Ако сигнатурата е на един ред, разделянето е ясно (изображение вляво), ако е на няколко реда, в PSR сигнатурата и тялото се сливат (в средата), докато в стандарта на Nette те продължават да бъдат разделени (вдясно): - -[* new-line-after.webp *] - - -Документационни блокове (phpDoc) -================================ - -Основно правило: Никога не дублирайте никаква информация в сигнатурата, като тип на параметър или тип на връщаната стойност, без добавена стойност. - -Документационен блок за дефиниция на клас: - -- Започва с описание на класа. -- Следва празен ред. -- Следват анотации `@property` (или `@property-read`, `@property-write`), една след друга. Синтаксисът е: анотация, интервал, тип, интервал, `$име`. -- Следват анотации `@method`, една след друга. Синтаксисът е: анотация, интервал, тип на връщаната стойност, интервал, име(тип $param, ...). -- Анотацията `@author` се пропуска. Авторството се съхранява в историята на изходния код. -- Могат да се използват анотации `@internal` или `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Документационен блок за свойство, който съдържа само анотация `@var`, трябва да бъде едноредов: - -```php -/** @var string[] */ -private array $name; -``` - -Документационен блок за дефиниция на метод: - -- Започва с кратко описание на метода. -- Без празен ред. -- Анотации `@param` на отделни редове. -- Анотация `@return`. -- Анотации `@throws`, една след друга. -- Могат да се използват анотации `@internal` или `@deprecated`. - -След всяка анотация следва един интервал, с изключение на `@param`, след която за по-добра четливост следват два интервала. - -```php -/** - * Намира файл в директория. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Табулатори вместо интервали -=========================== - -Табулаторите имат няколко предимства пред интервалите: - -- размерът на отстъпа може да се персонализира в редакторите и в "уеб":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- не налагат на кода предпочитанията на потребителя за размера на отстъпа, така че кодът е по-преносим -- могат да се напишат с едно натискане на клавиш (навсякъде, не само в редактори, които превръщат табулаторите в интервали) -- отстъпването е тяхната цел -- уважават нуждите на колегите със зрителни увреждания и незрящите - -Чрез използването на табулатори в нашите проекти позволяваме персонализиране на ширината, което може да изглежда като излишно за повечето хора, но за хората със зрителни увреждания е необходимо. - -За незрящите програмисти, които използват брайлови дисплеи, всеки интервал представлява една брайлова клетка. Така че, ако отстъпът по подразбиране е 4 интервала, отстъпът от 3-то ниво губи 12 ценни брайлови клетки още преди началото на кода. На 40-клетъчен дисплей, който се използва най-често при лаптопи, това е повече от една четвърт от наличните клетки, които са пропилени без никаква информация. - - -{{priority: -1}} diff --git a/contributing/bg/documentation.texy b/contributing/bg/documentation.texy deleted file mode 100644 index 4f03aca70a..0000000000 --- a/contributing/bg/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Как да допринесете към документацията -************************************* - -.[perex] -Допринасянето към документацията е една от най-полезните дейности, тъй като помагате на другите да разберат framework-а. - - -Как да пишем? -------------- - -Документацията е предназначена предимно за хора, които се запознават с темата. Затова трябва да отговаря на няколко важни точки: - -- Започнете от простото и общото. Към по-напредналите теми преминете едва накрая -- Опитайте се да обясните нещата възможно най-добре. Опитайте например първо да обясните темата на колега -- Посочвайте само тази информация, която потребителят действително трябва да знае по дадената тема -- Проверете дали вашата информация е наистина вярна. Тествайте всеки код -- Бъдете кратки - това, което напишете, съкратете наполовина. А след това спокойно още веднъж -- Пестете всякакви видове подчертавания, от удебелен шрифт до рамки като `.[note]` -- В кодовете спазвайте [Стандарта за кодиране |coding-standard] - -Освойте също [синтаксиса |syntax]. За преглед на статията по време на писането й можете да използвате [редактор с преглед |https://editor.nette.org/]. - - -Езикови версии --------------- - -Основният език е английският, така че вашите промени трябва да бъдат на чешки и английски. Ако английският не е вашата силна страна, използвайте [DeepL Translator |https://www.deepl.com/translator] и другите ще проверят текста ви. - -Преводът на други езици ще бъде извършен автоматично след одобрение и финализиране на вашата корекция. - - -Тривиални корекции ------------------- - -За да допринесете към документацията, е необходимо да имате акаунт в [GitHub|https://github.com]. - -Най-лесният начин да направите дребна промяна в документацията е да използвате връзките в края на всяка страница: - -- *Покажи в GitHub* отваря изходния вид на дадената страница в GitHub. След това е достатъчно да натиснете бутона `E` и можете да започнете да редактирате (необходимо е да сте влезли в GitHub) -- *Отвори преглед* отваря редактор, където веднага виждате и крайния визуален вид - -Тъй като [редакторът с преглед |https://editor.nette.org/] няма възможност да запазва промените директно в GitHub, е необходимо след завършване на корекциите да копирате изходния текст в клипборда (с бутона *Copy to clipboard*) и след това да го поставите в редактора в GitHub. Под полето за редактиране има формуляр за изпращане. Тук не забравяйте да обобщите накратко и да обясните причината за вашата корекция. След изпращане се създава т.нар. pull request (PR), който може да бъде редактиран допълнително. - - -По-големи корекции ------------------- - -По-подходящо, отколкото да използвате интерфейса на GitHub, е да сте запознати с основите на работа със системата за контрол на версиите Git. Ако не владеете работата с Git, можете да разгледате ръководството [git - the simple guide |https://rogerdudler.github.io/git-guide/] и евентуално да използвате някой от многото [графични клиенти |https://git-scm.com/downloads/guis]. - -Редактирайте документацията по този начин: - -1) В GitHub си създайте [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] на хранилището [nette/docs |https://github.com/nette/docs] -2) [Клонирайте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] това хранилище на своя компютър -3) След това в [съответния branch |#Структура на документацията] направете промените -4) Проверете за излишни интервали в текста с помощта на инструмента [Code-Checker |code-checker:] -4) Запазете промените (commit) -6) Ако сте доволни от промените, изпратете ги (push) в GitHub към вашия fork -7) Оттам ги изпратете към хранилището `nette/docs`, като създадете [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) - -Обичайно е да получавате коментари със забележки. Следете предложените промени и ги обработете. Добавете предложените промени като нови commit-и и отново ги изпратете в GitHub. Никога не създавайте нов pull request заради корекция на съществуващ pull request. - - -Структура на документацията ---------------------------- - -Цялата документация е разположена в GitHub в хранилището [nette/docs |https://github.com/nette/docs]. Текущата версия е в `master`, по-старите версии са разположени в branch-ове като `doc-3.x`, `doc-2.x`. - -Съдържанието на всеки branch се разделя на основни папки, представляващи отделните области на документацията. Например `application/` отговаря на https://doc.nette.org/bg/application, `latte/` отговаря на https://latte.nette.org и т.н. Всяка такава папка съдържа подпапки, представляващи езиковите версии (`cs`, `en`, `bg`, ...) и евентуално подпапка `files` с изображения, които могат да бъдат вмъквани в страниците на документацията. diff --git a/contributing/bg/syntax.texy b/contributing/bg/syntax.texy deleted file mode 100644 index 23db082cf4..0000000000 --- a/contributing/bg/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Синтаксис на документацията -*************************** - -Документацията използва Markdown & [синтаксис на Texy |https://texy.info/cs/syntax] с някои разширения. - - -Връзки -====== - -За вътрешни връзки се използва запис в квадратни скоби `[връзка |odkaz]`. И това е или във формата с вертикална черта `[текст на връзката |цел на връзката]`, или съкратено `[текст на връзката]`, ако целта е същата като текста (след трансформация в малки букви и тирета): - -- `[Page name]` -> `<a href="/bg/page-name">Page name</a>` -- `[текст на връзка |Page name]` -> `<a href="/bg/page-name">текст на връзка</a>` - -Можем да правим връзки към друга езикова версия или към друга секция. Под секция се разбира Nette библиотека (напр. `forms`, `latte` и др.) или специални секции като `best-practices`, `quickstart` и т.н.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (същата секция, друг език) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/bg/page-name">Page name</a>` (друга секция, същия език) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (друга секция и език) - -С помощта на `#` е възможно също така да се насочи към конкретно заглавие на страницата. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (заглавие на текущата страница) -- `[Page name#Heading]` -> `<a href="/bg/page-name#toc-heading">Page name</a>` - -Връзка към началната страница на секцията: (`@home` е специален израз за началната страница на секцията) - -- `[текст на връзка |@home]` -> `<a href="/bg/">текст на връзка</a>` -- `[текст на връзка |tracy:]` -> `<a href="//tracy.nette.org/bg/">текст на връзка</a>` - - -Връзки към API документацията ------------------------------ - -Винаги посочвайте само с този запис: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Използвайте напълно квалифицирани имена само при първото споменаване. За следващи връзки използвайте опростено име: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Връзки към PHP документацията ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Изходен код -=========== - -Блокът с код започва с <code>```lang</code> и завършва с <code>```</code>. Поддържаните езици са `php`, `latte`, `neon`, `html`, `css`, `js` и `sql`. За отстъп винаги използвайте табулатори. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Можете също така да посочите името на файла като <code>```php .{file: ArrayTest.php}</code> и блокът с код ще се рендира по този начин: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Заглавия -======== - -Най-високото заглавие (т.е. името на страницата) подчертайте със звездички (`***`). За разделяне на секции използвайте знаци за равенство (`===`). Заглавията от по-ниско ниво подчертавайте със знаци за равенство (`===`) и след това с тирета (`---`): - -``` -MVC Приложения & презентери -*************************** -... - - -Създаване на връзки -=================== -... - - -Връзки в шаблоните ------------------- -... -``` - - -Рамки и стилове -=============== - -Perex обозначаваме с клас `.[perex]` .[perex] - -Бележка обозначаваме с клас `.[note]` .[note] - -Съвет обозначаваме с клас `.[tip]` .[tip] - -Предупреждение обозначаваме с клас `.[caution]` .[caution] - -По-силно предупреждение обозначаваме с клас `.[warning]` .[warning] - -Номер на версия `.{data-version:2.4.10}` .{data-version:2.4.10} - -Записвайте класовете преди реда: - -``` -.[perex] -Това е perex. -``` - -Моля, имайте предвид, че рамки като `.[tip]` "привличат" очите, следователно се използват за подчертаване, а не за по-малко съществена информация. Затова използвайте ги максимално пестеливо. - - -Съдържание -========== - -Съдържанието (връзките в дясното меню) се генерира автоматично за всички страници, чийто размер надхвърля 4 000 байта, като това поведение по подразбиране може да бъде променено с помощта на [мета таг |#Мета тагове] `{{toc}}`. Текстът, формиращ съдържанието, се взема стандартно директно от текста на заглавията, но с помощта на модификатора `.{toc}` е възможно да се покаже в съдържанието друг текст, което е полезно главно за по-дълги заглавия. - -``` - - -Дълго и интелигентно заглавие .{toc: Произволен друг текст, показан в съдържанието} -=================================================================================== -``` - - -Мета тагове -=========== - -- настройка на собствено име на страницата (в `<title>` и навигацията тип "хлебни трохи") `{{title: Друго име}}` -- пренасочване `{{redirect: pla:cs}}` - виж [#връзки] -- принудително `{{toc}}` или забрана `{{toc: no}}` на автоматичното съдържание (кутийка с връзки към отделните заглавия) - -{{priority: -1}} diff --git a/contributing/cs/@left-menu.texy b/contributing/cs/@left-menu.texy index 7358695b3f..cdd44bf231 100644 --- a/contributing/cs/@left-menu.texy +++ b/contributing/cs/@left-menu.texy @@ -8,3 +8,11 @@ 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 index 39dba729b8..3296695c5c 100644 --- a/contributing/cs/code.texy +++ b/contributing/cs/code.texy @@ -14,8 +14,8 @@ Pro přispívání do kódu je nezbytné mít účet na [GitHub|https://github.c 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] repositáře [balíčku |www:packages], který se chystáte upravit -2) tento repositář [naklonujete |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svůj počítač +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 @@ -40,7 +40,7 @@ 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 repositáře vytvořením [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) +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] @@ -75,18 +75,7 @@ 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 opravu kódu je k dispozici automatický nástroj. Lze jej nainstalovat přes Composer **globálně** do vámi zvolené složky: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Nyní byste měli mít možnost spustit nástroj v terminálu. Prvním příkazem zkontrolujete a druhým i opravíte kód ve složkách `src` a `tests` v aktuálním adresáři: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` +Váš kód musí splňovat [coding standard] používaný v Nette Framework. Pro kontrolu a automatickou opravu použijte nástroj [Nette Coding Standard |tools:coding-standard], kde najdete i návod na instalaci a použití. Popis komitu @@ -106,7 +95,7 @@ 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 specifikujte, 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. +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 --> diff --git a/contributing/cs/coding-standard.texy b/contributing/cs/coding-standard.texy index d9e3cc8c85..c78b5a9ce8 100644 --- a/contributing/cs/coding-standard.texy +++ b/contributing/cs/coding-standard.texy @@ -2,20 +2,23 @@ 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 . +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 [Nette Coding Standard |tools:coding-standard], takže je nemusíte hlídat ručně. + 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`. +- 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. -- U rozhraní se nespecifikuje viditelnost metod, protože jsou vždy veřejné. +- 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. @@ -31,8 +34,8 @@ Pojmenovací konvence - "Rozhraní a abstraktní třídy by neměly obsahovat předpony nebo přípony":https://blog.nette.org/cs/predpony-a-pripony-do-nazvu-rozhrani-nepatri jako `Abstract`, `Interface` nebo `I`. -Wrapping and Braces -=================== +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: @@ -109,6 +112,23 @@ public function find(string $dir, array $options): array ``` +Globální funkce a konstanty +=========================== + +Globální funkce a konstanty se píší bez úvodního zpětného lomítka, tedy `count($arr)` nikoliv `\count($arr)`. Pro funkce, které umí PHP optimalizovat, uvedeme na začátku souboru `use function`, aby je kompilátor mohl přeložit efektivněji. Jedná se zejména o funkce jako `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` aj. Funkce se uvádějí na jednom řádku, aby úvodní blok importů nebyl zbytečně velký: + +```php +use Nette; +use function count, is_array, is_scalar, sprintf; +``` + +Výjimečně takto uvádíme i konstanty, u kterých může znalost hodnoty posloužit kompilátoru: + +```php +use const PHP_OS_FAMILY; +``` + + Tabulátory místo mezer ====================== @@ -122,7 +142,7 @@ Tabulátory mají oproti mezerám několik výhod: Používáním tabulátorů v našich projektech umožňujeme přizpůsobení šířky, které se může většině lidí zdát jako zbytečnost, ale pro lidi se zrakovým postižením je nezbytné. -Pro nevidomé programátory, kteří používají braillské displeje, představuje každá mezera jednu braillskou buňkou. Pokud je tedy výchozí odsazení 4 mezery, odsazení 3. úrovně plýtvá 12 cennými braillskými buňkami ještě před začátkem kódu. Na 40buňkovém displeji, který se u notebooků používá nejčastěji, je to více než čtvrtina dostupných buněk, které jsou promrhány bez jakékoliv informace. +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}} diff --git a/contributing/cs/documentation.texy b/contributing/cs/documentation.texy index c02587a359..b644eee0a9 100644 --- a/contributing/cs/documentation.texy +++ b/contributing/cs/documentation.texy @@ -49,13 +49,13 @@ Vhodnější, než využít rozhraní GitHubu, je být obeznámen se základy pr 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] repositáře [nette/docs |https://github.com/nette/docs] -2) tento repositář [naklonujete |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svůj počítač +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) zkontroluje přebytečné mezery v textu pomocí nástroje [Code-Checker |code-checker:] -4) změny uložte (commitněte) +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 repositáře `nette/docs` vytvořením [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) +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. @@ -63,6 +63,6 @@ Je běžné, že budete dostávat komentáře s připomínkami. Sledujte navrhov Struktura dokumentace --------------------- -Celá dokumentace je umístěna na GitHubu v repositář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`. +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. diff --git a/contributing/cs/syntax.texy b/contributing/cs/syntax.texy index ad8f3ea4c1..1d1754024b 100644 --- a/contributing/cs/syntax.texy +++ b/contributing/cs/syntax.texy @@ -1,7 +1,7 @@ Dokumentační syntax ******************* -Dokumentace používá Markdown & [Texy syntaxi |https://texy.info/cs/syntax] s některými rozšířeními. +Dokumentace používá Markdown & [Texy syntaxi |https://texy.nette.org/syntax] s některými rozšířeními. Odkazy @@ -12,9 +12,9 @@ Pro interní odkazy se používá zápis v hranatých závorkách `[odkaz]`. A t - `[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: +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á sekci, jiný jazyk) +- `[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) @@ -116,13 +116,13 @@ Třídy zapisujte před řádkem: Tohle je perex. ``` -Uvědomte si prosím, že rámečky jako `.[tip]` "tahají" oči, tudíž se používají pro zdůraznění, nikoliv pro méně podstatné informace. Proto jejich používám maximálně šetřte. +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 bytů, přičemž toho výchozí chování je možné upravit pomocí [#meta značky] `{{toc}}`. Text tvořící obsah se bere standardně přímo z textu nadpisů, ale pomocí modifikátoru `.{toc}` je možné zobrazit v obsahu jiný text, což se hodí hlavně pro delší nadpisy. +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. ``` @@ -138,5 +138,6 @@ 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}} diff --git a/contributing/de/@home.texy b/contributing/de/@home.texy index 37bab1fa5d..c4fcfeb89a 100644 --- a/contributing/de/@home.texy +++ b/contributing/de/@home.texy @@ -1,17 +1,17 @@ -Werden Sie ein Nette-Mitwirkender -********************************* +Machen Sie bei Nette mit +************************ .[perex] -Erfahren Sie, wie Sie sich an unserem Open-Source-Projekt beteiligen können. Erlernen Sie die Verfahren zur Mitwirkung am Quellcode und an der Dokumentation und werden Sie Teil der Entwicklergemeinschaft, die aktiv an der Verbesserung von Nette mitwirkt. +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** -- [Wie kann man zum Code beitragen? |code] -- [Codierungsstandard |coding-standard] +- [Zum Code beitragen |code] +- [Coding Standard |coding-standard] **Dokumentation** -- [Wie kann man zur Dokumentation beitragen? |documentation] +- [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 index a807ebcaf2..53e21f1303 100644 --- a/contributing/de/@left-menu.texy +++ b/contributing/de/@left-menu.texy @@ -1,10 +1,18 @@ Code **** -- [Wie man zum Code beiträgt? |code] -- [Codierungsstandard |coding-standard] +- [Zum Code beitragen |code] +- [Coding Standard |coding-standard] Dokumentation ************* -- [Wie man zur Dokumentation beiträgt? |documentation] +- [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 index b26457ddb5..f718bf8024 100644 --- a/contributing/de/code.texy +++ b/contributing/de/code.texy @@ -1,117 +1,106 @@ -Wie man zum Code beiträgt -************************* +Zum Code beitragen +****************** .[perex] -Sie möchten zum Nette Framework beitragen und benötigen eine Orientierung über die Regeln und Verfahren? Dieser Leitfaden für Anfänger zeigt Ihnen Schritt für Schritt, wie Sie effektiv zum Code beitragen, mit Repositories arbeiten und Änderungen implementieren können. +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 zum Code beizutragen, ist es unerlässlich, ein Konto auf [GitHub|https://github.com] zu haben und mit den Grundlagen der Arbeit mit dem Versionskontrollsystem Git vertraut zu sein. Wenn Sie nicht mit Git vertraut sind, können Sie sich den Leitfaden [git - the simple guide |https://rogerdudler.github.io/git-guide/] ansehen und gegebenenfalls einen der vielen [grafischen Clients |https://git-scm.com/downloads/guis] nutzen. +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 bearbeiten möchten. -2) [Klonen |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] Sie dieses Repository auf Ihren Computer. -3) Installieren Sie die Abhängigkeiten, einschließlich des [Nette Testers |tester:], mit dem Befehl `composer install`. -4) Überprüfen Sie, ob die Tests funktionieren, indem Sie `composer tester` ausführen. -5) Erstellen Sie einen [neuen Branch |#Neuer Branch] basierend auf der letzten veröffentlichten Version. +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 ---------------------------------- -Nun können Sie Ihre eigenen Codeänderungen vornehmen: +Jetzt können Sie Ihre eigenen Anpassungen am Code vornehmen: -1) Programmieren Sie die gewünschten Änderungen und vergessen Sie nicht die Tests. -2) Stellen Sie sicher, dass die Tests erfolgreich verlaufen, indem Sie `composer tester` verwenden. -3) Überprüfen Sie, ob der Code dem [Codierungsstandard |#Coding Standards] entspricht. -4) Speichern (committen) Sie die Änderungen mit einer Beschreibung in [diesem Format |#Commit-Beschreibung]. +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 erstellen, einen für jeden logischen Schritt. Jeder Commit sollte für sich genommen sinnvoll sein. +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 senden: +Sobald Sie mit den Änderungen zufrieden sind, können Sie sie einsenden: -1) Senden (pushen) Sie die Änderungen auf GitHub in Ihren Fork. -2) Senden Sie sie von dort an 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. +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 werden nun auch von anderen gesehen. Es ist üblich, dass Sie Kommentare mit Anmerkungen erhalten: +Ihre Commits sind nun für andere sichtbar. Üblicherweise erhalten Sie Kommentare mit Vorschlägen: -1) Verfolgen Sie die vorgeschlagenen Änderungen. -2) Arbeiten Sie sie als neue Commits ein oder [fügen Sie sie mit den vorherigen zusammen |https://help.github.com/en/github/using-git/about-git-rebase]. -3) Senden Sie die Commits erneut auf GitHub, und sie erscheinen automatisch im Pull Request. +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 bearbeiten. +Erstellen Sie niemals einen neuen Pull Request, um einen bestehenden zu ändern. Dokumentation ------------- -Wenn Sie die Funktionalität geändert oder eine neue hinzugefügt haben, vergessen Sie nicht, sie auch [zur Dokumentation hinzuzufügen |documentation]. +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 ============ -Wenn möglich, führen Sie Änderungen gegenüber der letzten veröffentlichten Version durch, d.h. dem letzten Tag in diesem Branch. Für den Tag `v3.2.1` erstellen Sie einen Branch mit diesem Befehl: +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 neuer_branch_name v3.2.1 +git checkout -b new_branch_name v3.2.1 ``` -Coding Standards -================ +Coding Standard +=============== -Ihr Code muss dem [Codierungsstandard |Coding Standard] entsprechen, der im Nette Framework verwendet wird. Zur Überprüfung und Korrektur des Codes steht ein automatisches Werkzeug zur Verfügung. Es kann über Composer **global** in einem von Ihnen gewählten Ordner installiert werden: - -```shell -composer create-project nette/coding-standard /pfad/zu/nette-coding-standard -``` - -Nun sollten Sie das Werkzeug im Terminal starten können. Mit dem ersten Befehl überprüfen Sie und mit dem zweiten korrigieren Sie den Code in den Ordnern `src` und `tests` im aktuellen Verzeichnis: - -```shell -/pfad/zu/nette-coding-standard/ecs check -/pfad/zu/nette-coding-standard/ecs check --fix -``` +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 Format: `Presenter: fixed AJAX detection [Closes #69]` +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 einem der folgenden Worte: `added` (neue Funktion hinzugefügt), `fixed` (Fehlerbehebung), `refactored` (Codeänderung ohne Verhaltensänderung), `changed`, `removed` -- Wenn der Commit die Abwärtskompatibilität bricht, fügen Sie `BC break` hinzu -- Eine optionale Verknüpfung zum Issue Tracker wie `(#123)` oder `[Closes #69]` -- Nach dem Betreff kann eine Leerzeile folgen und dann eine detailliertere Beschreibung, einschließlich z.B. Links zum Forum +- 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 ermöglicht Ihnen die GitHub-Oberfläche die Eingabe eines Titels und einer Beschreibung. Geben Sie einen aussagekräftigen Titel an und liefern Sie in der Beschreibung so viele Informationen wie möglich über die Gründe für Ihre Änderung. +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. -Es wird auch eine Kopfzeile angezeigt, in der Sie angeben, ob es sich um eine neue Funktion oder eine Fehlerbehebung handelt und ob die Abwärtskompatibilität beeinträchtigt werden könnte (BC break). Wenn ein zugehöriges Problem (Issue) vorhanden ist, verweisen Sie darauf, damit es nach Genehmigung des Pull Requests geschlossen wird. +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/de/writing --> +- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> ``` diff --git a/contributing/de/coding-standard.texy b/contributing/de/coding-standard.texy index 6109abb5c7..db83c6d9ef 100644 --- a/contributing/de/coding-standard.texy +++ b/contributing/de/coding-standard.texy @@ -1,44 +1,47 @@ -Codierungsstandard -****************** +Coding Standard +*************** .[perex] -Dieses Dokument beschreibt die Regeln und Empfehlungen für die Entwicklung von Nette. Wenn Sie Code zu Nette beitragen, müssen Sie diese einhalten. Der einfachste Weg, dies zu tun, ist, den vorhandenen Code nachzuahmen. Ziel ist es, dass der gesamte Code so aussieht, als wäre er von einer einzigen Person geschrieben worden. +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 Codierungsstandard entspricht dem [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] mit zwei Hauptausnahmen: Er verwendet [#Tabulatoren statt Leerzeichen] für die Einrückung und [PascalCase für Klassenkonstanten|https://blog.nette.org/de/fuer-weniger-geschrei-im-code]. +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. -- Zwei Leerzeilen werden verwendet, um Methoden zur besseren Lesbarkeit voneinander zu trennen. -- Der Grund für die Verwendung des Shut-up-Operators (`@`) muss dokumentiert werden: `@mkdir($dir); // @ - Verzeichnis kann bereits existieren`. -- Wenn ein schwach typisierter Vergleichsoperator verwendet wird (d. h. `==`, `!=`, ...), muss die Absicht dokumentiert werden: `// == null akzeptieren` -- In eine einzige Datei `exceptions.php` können mehrere Ausnahmeklassen geschrieben werden. -- Bei Schnittstellen wird die Sichtbarkeit von Methoden nicht angegeben, da sie immer `public` sind. -- Jede Eigenschaft, jeder Rückgabewert und jeder Parameter muss einen Typ angegeben haben. Bei `final` Konstanten geben wir den Typ jedoch nie an, da er offensichtlich ist. -- Zum Begrenzen von Zeichenketten sollten einfache Anführungszeichen verwendet werden, es sei denn, das Literal selbst enthält Apostrophe. +- 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 ====================== -- Verwenden Sie keine Abkürzungen, es sei denn, der vollständige Name ist zu lang. -- Verwenden Sie bei zweibuchstabigen Abkürzungen Großbuchstaben (z.B. `IO`), bei längeren Abkürzungen PascalCase oder camelCase (z.B. `XmlRpc`). -- Verwenden Sie für den Klassennamen ein Substantiv oder eine Wortgruppe. -- Klassennamen müssen nicht nur die Spezifität (`Array`), sondern auch die Allgemeinheit (`ArrayIterator`) enthalten. Ausnahmen sind PHP-Attribute. -- [Klassenkonstanten und Enums sollten PascalCase verwenden |https://blog.nette.org/de/fuer-weniger-geschrei-im-code]. -- [Schnittstellen und abstrakte Klassen sollten keine Präfixe oder Suffixe enthalten |https://blog.nette.org/de/praefixe-und-suffixe-gehoeren-nicht-in-interface-namen] wie `Abstract`, `Interface` oder `I`. +- 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 Codierungsstandard entspricht PSR-12 (bzw. PER Coding Style), ergänzt oder modifiziert ihn jedoch in einigen Punkten: +Der Nette Coding Standard entspricht PSR-12 (bzw. dem PER Coding Style), präzisiert oder verändert ihn aber in einigen Punkten: -- Pfeilfunktionen werden ohne Leerzeichen vor der öffnenden Klammer geschrieben, d.h. `fn($a) => $b` -- Es ist keine Leerzeile zwischen verschiedenen Typen von `use`-Importanweisungen erforderlich. -- Der Rückgabetyp einer Funktion/Methode und die öffnende geschweifte Klammer stehen immer auf separaten Zeilen: +- 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( @@ -46,11 +49,11 @@ Der Nette Codierungsstandard entspricht PSR-12 (bzw. PER Coding Style), ergänzt array $options, ): array { - // Methodenkörper + // Rumpf der Methode } ``` -Die öffnende geschweifte Klammer auf einer separaten Zeile ist wichtig für die visuelle Trennung der Signatur der Funktion/Methode vom Körper. Wenn die Signatur auf einer Zeile steht, ist die Trennung deutlich (Bild links). Wenn sie auf mehreren Zeilen steht, verschmelzen in PSR Signatur und Körper (Mitte), während sie im Nette-Standard weiterhin getrennt sind (rechts): +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 *] @@ -58,20 +61,20 @@ Die öffnende geschweifte Klammer auf einer separaten Zeile ist wichtig für die Dokumentationsblöcke (phpDoc) ============================= -Hauptregel: Duplizieren Sie niemals Informationen aus der Signatur (wie Parametertyp oder Rückgabetyp) im Docblock, es sei denn, Sie fügen zusätzliche Informationen hinzu. +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 der Beschreibung der Klasse. -- Gefolgt von einer Leerzeile. -- Gefolgt von `@property`-Annotationen (oder `@property-read`, `@property-write`), eine nach der anderen. Syntax: Annotation, Leerzeichen, Typ, Leerzeichen, `$name`. -- Gefolgt von `@method`-Annotationen, eine nach der anderen. Syntax: Annotation, Leerzeichen, Rückgabetyp, Leerzeichen, `methodName(Typ $param, ...)`. -- Die `@author`-Annotation wird weggelassen. Die Autorschaft wird in der Quellcode-Historie gespeichert. -- Die Annotationen `@internal` oder `@deprecated` können verwendet werden. +- 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 /** - * Repräsentiert einen Teil einer MIME-Nachricht. + * MIME message part. * * @property string $encoding * @property-read array $headers @@ -80,7 +83,7 @@ Dokumentationsblock für eine Klassendefinition: */ ``` -Ein Dokumentationsblock für eine Eigenschaft, der nur die Annotation `@var` enthält, sollte einzeilig sein: +Ein Dokumentationsblock für eine Property, der nur die Annotation `@var` enthält, soll einzeilig sein: ```php /** @var string[] */ @@ -89,18 +92,18 @@ private array $name; Dokumentationsblock für eine Methodendefinition: -- Beginnt mit einer kurzen Beschreibung der Methode. -- Keine Leerzeile danach. -- `@param`-Annotationen, jede in einer eigenen Zeile. -- `@return`-Annotation. -- `@throws`-Annotationen, eine nach der anderen. -- Die Annotationen `@internal` oder `@deprecated` können verwendet werden. +- 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 -Auf jede Annotation folgt ein Leerzeichen, mit Ausnahme von `@param`, auf das zur besseren Lesbarkeit zwei Leerzeichen folgen. +Nach jeder Annotation folgt ein Leerzeichen, außer bei `@param`, nach der zur besseren Lesbarkeit zwei Leerzeichen folgen. ```php /** - * Findet eine Datei in einem Verzeichnis. + * Finds a file in directory. * @param string[] $options * @return string[] * @throws DirectoryNotFoundException @@ -109,20 +112,37 @@ 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 kann in Editoren und im [Web |https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size] angepasst werden. -- Sie zwingen dem Code nicht die vom Benutzer bevorzugte Einrückungsgröße auf, sodass der Code besser portierbar ist. -- Sie können mit einem einzigen Tastendruck geschrieben werden (überall, nicht nur in Editoren, die Tabulatoren in Leerzeichen umwandeln). -- Einrückung ist ihr Zweck. -- Sie respektieren die Bedürfnisse von sehbehinderten und blinden Kollegen. +- 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 -Durch die Verwendung von Tabulatoren in unseren Projekten ermöglichen wir die Anpassung der Breite, was den meisten Menschen als unnötig erscheinen mag, aber für Menschen mit Sehbehinderungen unerlässlich ist. +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, stellt jedes Leerzeichen eine Braillezelle dar. Wenn also die Standardeinrückung 4 Leerzeichen beträgt, verschwendet die Einrückung der 3. Ebene 12 wertvolle Braillezellen, noch bevor der Code beginnt. Auf einem 40-Zellen-Display, das bei Laptops am häufigsten verwendet wird, ist das mehr als ein Viertel der verfügbaren Zellen, die ohne jegliche Information verschwendet werden. +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 index 1461cd05c5..62fdf843b6 100644 --- a/contributing/de/documentation.texy +++ b/contributing/de/documentation.texy @@ -1,68 +1,68 @@ -Wie man zur Dokumentation beiträgt -********************************** +Zur Dokumentation beitragen +*************************** .[perex] -Beiträge zur Dokumentation sind eine der lohnendsten Tätigkeiten, da Sie anderen helfen, das Framework zu verstehen. +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 richtet sich vor allem an Personen, die sich mit dem Thema vertraut machen. Daher sollte sie mehrere wichtige Punkte erfüllen: +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 dem Einfachen und Allgemeinen. Gehen Sie erst am Ende zu fortgeschritteneren Themen über. -- Versuchen Sie, die Sache so gut wie möglich zu erklären. Versuchen Sie zum Beispiel, das Thema zuerst einem Kollegen zu erklären. -- Geben Sie nur die Informationen an, die der Benutzer tatsächlich zum jeweiligen Thema wissen muss. -- Überprüfen Sie, ob Ihre Informationen tatsächlich wahr sind. Testen Sie jeden Code. -- Seien Sie prägnant - kürzen Sie, was Sie schreiben, auf die Hälfte. Und dann ruhig noch einmal. -- Sparen Sie mit Hervorhebungen aller Art, von Fettdruck bis hin zu Rahmen wie `.[note]`. -- Halten Sie sich in den Codebeispielen an den [Codierungsstandard |Coding Standard]. +- 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 auch mit der [Syntax |Syntax] vertraut. Für die Vorschau eines Artikels während des Schreibens können Sie den [Editor mit Vorschau |https://editor.nette.org/] verwenden. +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 --------------- -Die Hauptsprache ist Englisch. Ihre Änderungen sollten daher idealerweise sowohl auf Tschechisch als auch auf Englisch erfolgen. Wenn Englisch nicht Ihre Stärke ist, verwenden Sie den [DeepL Translator |https://www.deepl.com/translator] und andere werden den Text für Sie überprüfen. +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 andere Sprachen erfolgt automatisch nach Genehmigung und Feinabstimmung Ihrer Änderung. +Die Übersetzung in die anderen Sprachen erfolgt automatisch, nachdem Ihre Änderung angenommen und abgeschlossen ist. Triviale Änderungen ------------------- -Um zur Dokumentation beizutragen, ist ein Konto auf [GitHub|https://github.com] erforderlich. +Um zur Dokumentation beizutragen, brauchen Sie ein Konto auf [GitHub |https://github.com]. -Der einfachste Weg, eine kleine Änderung in der Dokumentation vorzunehmen, ist die Verwendung der Links am Ende jeder Seite: +Am einfachsten nehmen Sie eine kleine Änderung in der Dokumentation über die Links am Ende jeder Seite vor: -- *Auf GitHub anzeigen* öffnet die Quellcodedatei der jeweiligen Seite auf GitHub. Drücken Sie dann einfach die Taste `E` und Sie können mit der Bearbeitung beginnen (Sie müssen bei GitHub angemeldet sein). -- *Vorschau öffnen* öffnet den Editor, in dem Sie auch gleich die resultierende visuelle Darstellung sehen. +- *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 [Editor mit Vorschau |https://editor.nette.org/] keine Möglichkeit hat, Änderungen direkt auf GitHub zu speichern, müssen Sie nach Abschluss der Bearbeitung den Quelltext in die Zwischenablage kopieren (mit der Schaltfläche *Copy to clipboard*) und ihn dann in den Editor auf GitHub einfügen. Unter dem Bearbeitungsfeld befindet sich ein Formular zum Senden. Vergessen Sie hier nicht, den Grund für Ihre Änderung kurz zusammenzufassen und zu erklären. Nach dem Senden entsteht ein sogenannter Pull Request (PR), der weiter bearbeitet werden kann. +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 ------------------ -Besser als die Nutzung der GitHub-Oberfläche ist es, mit den Grundlagen der Arbeit mit dem Versionskontrollsystem Git vertraut zu sein. Wenn Sie nicht mit Git vertraut sind, können Sie sich den Leitfaden [git - the simple guide |https://rogerdudler.github.io/git-guide/] ansehen und gegebenenfalls einen der vielen [grafischen Clients |https://git-scm.com/downloads/guis] nutzen. +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. -Bearbeiten Sie die Dokumentation auf diese Weise: +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 |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] Sie dieses Repository auf Ihren Computer. -3) Nehmen Sie dann im [entsprechenden Branch |#Struktur der Dokumentation] die Änderungen vor. -4) Überprüfen Sie überflüssige Leerzeichen im Text mit dem Werkzeug [Code-Checker |code-checker:]. +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, senden (pushen) Sie sie auf GitHub in Ihren Fork. -7) Senden Sie sie von dort an das Repository `nette/docs`, indem Sie einen [Pull Request|https://help.github.com/articles/creating-a-pull-request] (PR) erstellen. +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. -Es ist üblich, dass Sie Kommentare mit Anmerkungen erhalten. Verfolgen Sie die vorgeschlagenen Änderungen und arbeiten Sie sie ein. Fügen Sie die vorgeschlagenen Änderungen als neue Commits hinzu und senden Sie sie erneut auf GitHub. Erstellen Sie niemals einen neuen Pull Request, um einen bestehenden Pull Request zu bearbeiten. +Ü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 befindet sich auf GitHub im Repository [nette/docs |https://github.com/nette/docs]. Die aktuelle Version befindet sich im `master`-Branch, ältere Versionen befinden sich in Branches wie `doc-3.x`, `doc-2.x`. +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 die einzelnen Bereiche der Dokumentation repräsentieren. Zum Beispiel entspricht `application/` https://doc.nette.org/de/application, `latte/` entspricht https://latte.nette.org usw. Jeder dieser Ordner enthält Unterordner, die die Sprachversionen (`cs`, `en`, ...) darstellen, und gegebenenfalls einen Unterordner `files` mit Bildern, die in die Seiten der Dokumentation eingefügt werden können. +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 index 64257e971f..c06fe5c4e5 100644 --- a/contributing/de/syntax.texy +++ b/contributing/de/syntax.texy @@ -1,45 +1,45 @@ Dokumentationssyntax ******************** -Die Dokumentation verwendet Markdown & [Texy-Syntax |https://texy.info/de/syntax] mit einigen Erweiterungen. +Die Dokumentation verwendet Markdown & die [Texy-Syntax |https://texy.nette.org/syntax] mit einigen Erweiterungen. Links ===== -Für interne Links wird die Notation in eckigen Klammern `[...]` verwendet. Entweder in der Form mit einem senkrechten Strich `[Linktext |Linkziel]` oder verkürzt `[Linktext als Ziel]`, wenn der Linktext mit dem Ziel übereinstimmt (nach Umwandlung in Kleinbuchstaben und Ersetzung von Leerzeichen durch Bindestriche): +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="/de/page-name">Page name</a>` -- `[Linktext |Page name]` -> `<a href="/de/page-name">Linktext</a>` +- `[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 spezielle Abschnitte wie `best-practices`, `quickstart` usw.: +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/de/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 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 `#` ist es auch möglich, auf eine bestimmte Überschrift auf der Seite zu zielen. +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="/de/page-name#toc-heading">Page name</a>` +- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` -Link zur Startseite des Abschnitts: (`@home` ist ein spezieller Ausdruck für die Startseite des Abschnitts) +Link auf die Startseite des Abschnitts: (`@home` ist ein besonderer Ausdruck für die Startseite des Abschnitts) -- `[Linktext |@home]` -> `<a href="/de/">Linktext</a>` -- `[Linktext |tracy:]` -> `<a href="//tracy.nette.org/de/">Linktext</a>` +- `[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 immer nur diese Notation: +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 vollqualifizierte Namen nur bei der ersten Erwähnung. Für weitere Links verwenden Sie den vereinfachten Namen: +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()] @@ -53,7 +53,7 @@ Links zur PHP-Dokumentation Quellcode ========= -Ein Codeblock beginnt mit <code>```sprache</code> und endet mit <code>```</code>. Unterstützte Sprachen sind `php`, `latte`, `neon`, `html`, `css`, `js` und `sql`. Verwenden Sie für die Einrückung immer Tabulatoren. +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 @@ -63,7 +63,7 @@ Ein Codeblock beginnt mit <code>```sprache</code> und endet mit <cod ``` ``` -Sie können auch den Dateinamen als <code>```php .{file: ArrayTest.php}</code> angeben und der Codeblock wird auf diese Weise gerendert: +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) @@ -75,7 +75,7 @@ public function renderPage($id) Überschriften ============= -Die oberste Überschrift (also der Seitentitel) wird mit Sternchen unterstrichen. Zur Trennung von Abschnitten verwenden Sie Gleichheitszeichen. Überschriften unterstreichen Sie mit Gleichheitszeichen und dann mit Bindestrichen: +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 @@ -83,8 +83,8 @@ MVC-Anwendungen & Presenter ... -Linkerstellung -============== +Erstellen von Links +=================== ... @@ -97,37 +97,37 @@ Links in Templates Rahmen und Stile ================ -Der Perex (Einleitungstext) wird mit der Klasse `.[perex]` gekennzeichnet. .[perex] +Den Perex kennzeichnen wir mit der Klasse `.[perex]` .[perex] -Eine Anmerkung wird mit der Klasse `.[note]` gekennzeichnet. .[note] +Einen Hinweis kennzeichnen wir mit der Klasse `.[note]` .[note] -Ein Tipp wird mit der Klasse `.[tip]` gekennzeichnet. .[tip] +Einen Tipp kennzeichnen wir mit der Klasse `.[tip]` .[tip] -Eine Warnung wird mit der Klasse `.[caution]` gekennzeichnet. .[caution] +Eine Warnung kennzeichnen wir mit der Klasse `.[caution]` .[caution] -Eine stärkere Warnung wird mit der Klasse `.[warning]` gekennzeichnet. .[warning] +Eine eindringlichere Warnung kennzeichnen wir mit der Klasse `.[warning]` .[warning] -Versionsnummer `.{data-version:2.4.10}` .{data-version:2.4.10} +Die Versionsnummer `.{data-version:2.4.10}` .{data-version:2.4.10} -Schreiben Sie Klassen vor die Zeile, zu der sie gehören: +Die Klassen schreiben Sie vor die Zeile, für die sie gelten: ``` .[perex] -Dies ist der Perex. +Das ist der Perex. ``` -Bitte beachten Sie, dass Rahmen wie `.[tip]` die Aufmerksamkeit auf sich ziehen. Verwenden Sie sie daher zur Hervorhebung wichtiger Informationen und nicht für Nebensächlichkeiten. Gehen Sie äußerst sparsam damit um. +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 ================== -Das Inhaltsverzeichnis (Links im rechten Menü) wird automatisch für alle Seiten generiert, deren Größe 4.000 Byte überschreitet. Dieses Standardverhalten kann mit dem [Meta-Tag |#Meta-Tags] `{{toc}}` angepasst werden. Der Text für das Inhaltsverzeichnis wird standardmäßig direkt aus den Überschriften übernommen. Mit dem Modifikator `.{toc}` kann jedoch ein anderer Text angezeigt werden, was besonders bei längeren Überschriften nützlich ist. +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: Beliebiger anderer Text für das Inhaltsverzeichnis} +Lange und intelligente Überschrift .{toc: Ein anderer im Inhaltsverzeichnis angezeigter Text} ============================================================================================= ``` @@ -135,8 +135,9 @@ Lange und intelligente Überschrift .{toc: Beliebiger anderer Text für das Inha Meta-Tags ========= -- Einstellung eines benutzerdefinierten Seitentitels (im `<title>`-Tag und in der Breadcrumb-Navigation): `{{title: Anderer Titel}}` +- Einen eigenen Seitentitel setzen (in `<title>` und in der Brotkrumennavigation): `{{title: Another name}}` - Weiterleitung: `{{redirect: pla:cs}}` - siehe [#Links] -- Erzwingen `{{toc}}` oder Deaktivieren `{{toc: no}}` des automatischen Inhaltsverzeichnisses (Box mit Links zu einzelnen Überschriften) +- 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/el/@home.texy b/contributing/el/@home.texy deleted file mode 100644 index a3a78869e2..0000000000 --- a/contributing/el/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Γίνετε συνεισφέρων στο Nette -**************************** - -.[perex] -Μάθετε πώς μπορείτε να συμμετάσχετε στο open source έργο μας. Εξοικειωθείτε με τις διαδικασίες συνεισφοράς στον πηγαίο κώδικα και την τεκμηρίωση και γίνετε μέλος της κοινότητας των προγραμματιστών που συμμετέχουν ενεργά στη βελτίωση του Nette. - - -**Κώδικας** - -- [Πώς να συνεισφέρετε στον κώδικα; |code] -- [Πρότυπο κωδικοποίησης |coding-standard] - -**Τεκμηρίωση** - -- [Πώς να συνεισφέρετε στην τεκμηρίωση; |documentation] -- [Σύνταξη τεκμηρίωσης |syntax] -- "Επεξεργαστής προεπισκόπησης":https://editor.nette.org diff --git a/contributing/el/@left-menu.texy b/contributing/el/@left-menu.texy deleted file mode 100644 index 8c9742c5a3..0000000000 --- a/contributing/el/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Κώδικας -******* -- [Πώς να συνεισφέρετε στον κώδικα; |code] -- [Πρότυπο κωδικοποίησης |coding-standard] - -Τεκμηρίωση -********** -- [Πώς να συνεισφέρετε στην τεκμηρίωση; |documentation] -- [Σύνταξη τεκμηρίωσης |syntax] -- "Επεξεργαστής προεπισκόπησης":https://editor.nette.org diff --git a/contributing/el/code.texy b/contributing/el/code.texy deleted file mode 100644 index e6c6cc8513..0000000000 --- a/contributing/el/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Πώς να συνεισφέρετε στον κώδικα -******************************* - -.[perex] -Ετοιμάζεστε να συνεισφέρετε στο Nette Framework και χρειάζεστε καθοδήγηση σχετικά με τους κανόνες και τις διαδικασίες; Αυτός ο οδηγός για αρχάριους θα σας δείξει βήμα προς βήμα πώς να συνεισφέρετε αποτελεσματικά στον κώδικα, να εργάζεστε με αποθετήρια (repositories) και να υλοποιείτε αλλαγές. - - -Διαδικασία -========== - -Για να συνεισφέρετε στον κώδικα, είναι απαραίτητο να έχετε λογαριασμό στο [GitHub |https://github.com] και να είστε εξοικειωμένοι με τα βασικά του συστήματος ελέγχου εκδόσεων Git. Αν δεν γνωρίζετε πώς να χρησιμοποιείτε το Git, μπορείτε να ανατρέξετε στον οδηγό [git - the simple guide |https://rogerdudler.github.io/git-guide/] και ενδεχομένως να χρησιμοποιήσετε έναν από τους πολλούς [γραφικούς clients |https://git-scm.com/downloads/guis]. - - -Προετοιμασία περιβάλλοντος και αποθετηρίου ------------------------------------------- - -1) Στο GitHub, δημιουργήστε ένα [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] του αποθετηρίου του [πακέτου |www:packages] που πρόκειται να τροποποιήσετε. -2) [Κλωνοποιήστε |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] αυτό το αποθετήριο στον υπολογιστή σας. -3) Εγκαταστήστε τις εξαρτήσεις, συμπεριλαμβανομένου του [Nette Tester |tester:], χρησιμοποιώντας την εντολή `composer install`. -4) Ελέγξτε ότι οι δοκιμές λειτουργούν, εκτελώντας το `composer tester`. -5) Δημιουργήστε έναν [νέο κλάδο |#Νέος Κλάδος] βασισμένο στην τελευταία δημοσιευμένη έκδοση. - - -Υλοποίηση των δικών σας αλλαγών -------------------------------- - -Τώρα μπορείτε να πραγματοποιήσετε τις δικές σας τροποποιήσεις στον κώδικα: - -1) Προγραμματίστε τις απαιτούμενες αλλαγές και μην ξεχάσετε τις δοκιμές. -2) Βεβαιωθείτε ότι οι δοκιμές εκτελούνται επιτυχώς, χρησιμοποιώντας το `composer tester`. -3) Ελέγξτε αν ο κώδικας πληροί τα [#πρότυπα κωδικοποίησης]. -4) Αποθηκεύστε τις αλλαγές (commit) με περιγραφή σε [αυτή τη μορφή |#Περιγραφή του Commit]. - -Μπορείτε να δημιουργήσετε πολλαπλά commits, ένα για κάθε λογικό βήμα. Κάθε commit θα πρέπει να έχει νόημα από μόνο του. - - -Υποβολή των αλλαγών -------------------- - -Μόλις είστε ικανοποιημένοι με τις αλλαγές, μπορείτε να τις υποβάλετε: - -1) Στείλτε (push) τις αλλαγές στο GitHub στο δικό σας fork. -2) Από εκεί, υποβάλετέ τις στο αποθετήριο του Nette δημιουργώντας ένα [pull request |https://help.github.com/articles/creating-a-pull-request] (PR). -3) Παρέχετε [επαρκείς πληροφορίες |#Περιγραφή του Pull Request] στην περιγραφή. - - -Ενσωμάτωση σχολίων ------------------- - -Τα commits σας θα είναι πλέον ορατά και σε άλλους. Είναι σύνηθες να λαμβάνετε σχόλια με παρατηρήσεις: - -1) Παρακολουθήστε τις προτεινόμενες τροποποιήσεις. -2) Ενσωματώστε τις ως νέα commits ή [συγχωνεύστε τα με τα προηγούμενα |https://help.github.com/en/github/using-git/about-git-rebase]. -3) Στείλτε ξανά τα commits στο GitHub, και θα εμφανιστούν αυτόματα στο pull request. - -Ποτέ μην δημιουργείτε νέο pull request για την τροποποίηση ενός υπάρχοντος. - - -Τεκμηρίωση ----------- - -Αν αλλάξατε τη λειτουργικότητα ή προσθέσατε νέα, μην ξεχάσετε να την [προσθέσετε και στην τεκμηρίωση |documentation]. - - -Νέος Κλάδος -=========== - -Αν είναι δυνατόν, πραγματοποιήστε τις αλλαγές έναντι της τελευταίας δημοσιευμένης έκδοσης, δηλαδή του τελευταίου tag στον συγκεκριμένο κλάδο. Για το tag `v3.2.1`, δημιουργείτε έναν κλάδο με αυτή την εντολή: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Πρότυπα Κωδικοποίησης -===================== - -Ο κώδικάς σας πρέπει να πληροί τα [πρότυπα κωδικοποίησης |coding-standard] που χρησιμοποιούνται στο Nette Framework. Για τον έλεγχο και τη διόρθωση του κώδικα είναι διαθέσιμο ένα αυτόματο εργαλείο. Μπορεί να εγκατασταθεί μέσω Composer **global** στον φάκελο της επιλογής σας: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Τώρα θα πρέπει να μπορείτε να εκτελέσετε το εργαλείο στο τερματικό. Με την πρώτη εντολή ελέγχετε και με τη δεύτερη διορθώνετε τον κώδικα στους φακέλους `src` και `tests` στον τρέχοντα κατάλογο: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Περιγραφή του Commit -==================== - -Στο Nette, τα θέματα των commits έχουν τη μορφή: `Presenter: fixed AJAX detection [Closes #69]` - -- Περιοχή ακολουθούμενη από άνω και κάτω τελεία. -- Σκοπός του commit σε παρελθοντικό χρόνο, αν είναι δυνατόν, ξεκινήστε με τη λέξη: "added (προστέθηκε νέα δυνατότητα)", "fixed (διόρθωση)", "refactored (αλλαγή στον κώδικα χωρίς αλλαγή συμπεριφοράς)", "changed", "removed". -- Αν το commit διακόπτει την προς τα πίσω συμβατότητα, προσθέστε "BC break". -- Πιθανή σύνδεση με το issue tracker όπως `(#123)` ή `[Closes #69]`. -- Μετά το θέμα μπορεί να ακολουθεί μία κενή γραμμή και στη συνέχεια λεπτομερέστερη περιγραφή, συμπεριλαμβανομένων, για παράδειγμα, συνδέσμων στο φόρουμ. - - -Περιγραφή του Pull Request -========================== - -Κατά τη δημιουργία ενός pull request, η διεπαφή του GitHub σας επιτρέπει να εισάγετε έναν τίτλο και μια περιγραφή. Δώστε έναν περιεκτικό τίτλο και στην περιγραφή παρέχετε όσο το δυνατόν περισσότερες πληροφορίες σχετικά με τους λόγους της αλλαγής σας. - -Θα εμφανιστεί επίσης μια επικεφαλίδα, όπου θα καθορίσετε αν πρόκειται για νέα λειτουργία ή διόρθωση σφάλματος και αν μπορεί να προκύψει παραβίαση της προς τα πίσω συμβατότητας (BC break). Αν υπάρχει σχετικό πρόβλημα (issue), αναφερθείτε σε αυτό, ώστε να κλείσει μετά την έγκριση του pull request. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/el/coding-standard.texy b/contributing/el/coding-standard.texy deleted file mode 100644 index 6454785629..0000000000 --- a/contributing/el/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Πρότυπο Κωδικοποίησης -********************* - -.[perex] -Αυτό το έγγραφο περιγράφει τους κανόνες και τις συστάσεις για την ανάπτυξη του Nette. Κατά τη συνεισφορά κώδικα στο Nette, πρέπει να τους τηρείτε. Ο ευκολότερος τρόπος για να το κάνετε αυτό είναι να μιμηθείτε τον υπάρχοντα κώδικα. Στόχος είναι όλος ο κώδικας να φαίνεται σαν να τον έγραψε ένα άτομο. - -Το Nette Coding Standard αντιστοιχεί στο [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] με δύο κύριες εξαιρέσεις: για την εσοχή χρησιμοποιεί [tabs αντί για κενά |#Tabulators αντί για Κενά] και για τις [σταθερές κλάσεων χρησιμοποιεί PascalCase |https://blog.nette.org/el/for-less-screaming-in-the-code]. - - -Γενικοί Κανόνες -=============== - -- Κάθε αρχείο PHP πρέπει να περιέχει `declare(strict_types=1)`. -- Δύο κενές γραμμές χρησιμοποιούνται για τον διαχωρισμό μεθόδων για καλύτερη αναγνωσιμότητα. -- Ο λόγος χρήσης του τελεστή σίγασης (@) πρέπει να τεκμηριώνεται: `@mkdir($dir); // @ - ο κατάλογος μπορεί ήδη να υπάρχει`. -- Αν χρησιμοποιείται τελεστής σύγκρισης με αδύναμη τυποποίηση (δηλ. `==`, `!=`, ...), πρέπει να τεκμηριώνεται η πρόθεση: `// == αποδοχή null`. -- Σε ένα αρχείο `exceptions.php` μπορείτε να γράψετε πολλαπλές εξαιρέσεις. -- Στα interfaces δεν καθορίζεται η ορατότητα των μεθόδων, επειδή είναι πάντα public. -- Κάθε ιδιότητα, τιμή επιστροφής και παράμετρος πρέπει να έχει δηλωμένο τύπο. Αντίθετα, στις τελικές σταθερές δεν δηλώνουμε ποτέ τον τύπο, επειδή είναι προφανής. -- Για τον οριοθέτηση ενός string θα πρέπει να χρησιμοποιούνται απλά εισαγωγικά, εκτός από τις περιπτώσεις όπου το ίδιο το literal περιέχει αποστρόφους. - - -Συμβάσεις Ονοματοδοσίας -======================= - -- Μην χρησιμοποιείτε συντομογραφίες, εκτός αν το πλήρες όνομα είναι πολύ μεγάλο. -- Για διγράμματες συντομογραφίες χρησιμοποιήστε κεφαλαία γράμματα, για μεγαλύτερες συντομογραφίες PascalCase/camelCase. -- Για το όνομα της κλάσης χρησιμοποιήστε ουσιαστικό ή φράση ουσιαστικού. -- Τα ονόματα των κλάσεων πρέπει να περιέχουν όχι μόνο την εξειδίκευση (`Array`), αλλά και τη γενικότητα (`ArrayIterator`). Εξαίρεση αποτελούν τα attributes της γλώσσας PHP. -- "Οι σταθερές κλάσεων και τα enums πρέπει να χρησιμοποιούν PascalCase":https://blog.nette.org/el/for-less-screaming-in-the-code. -- "Τα Interfaces και οι abstract κλάσεις δεν πρέπει να περιέχουν προθέματα ή επιθήματα":https://blog.nette.org/el/prefixes-and-suffixes-do-not-belong-in-interface-names όπως `Abstract`, `Interface` ή `I`. - - -Αναδίπλωση και Άγκιστρα -======================= - -Το Nette Coding Standard αντιστοιχεί στο PSR-12 (ή PER Coding Style), σε ορισμένα σημεία το συμπληρώνει ή το τροποποιεί: - -- Οι arrow functions γράφονται χωρίς κενό πριν την παρένθεση, δηλ. `fn($a) => $b`. -- Δεν απαιτείται κενή γραμμή μεταξύ διαφορετικών τύπων `use` import statements. -- Ο τύπος επιστροφής της συνάρτησης/μεθόδου και το αρχικό άγκιστρο `{` είναι πάντα σε ξεχωριστές γραμμές: - -```php - public function find( - string $dir, - array $options, - ): array - { - // σώμα της μεθόδου - } -``` - -Το αρχικό άγκιστρο σε ξεχωριστή γραμμή είναι σημαντικό για τον οπτικό διαχωρισμό της υπογραφής της συνάρτησης/μεθόδου από το σώμα. Αν η υπογραφή είναι σε μία γραμμή, ο διαχωρισμός είναι σαφής (εικόνα αριστερά). Αν είναι σε πολλές γραμμές, στο PSR οι υπογραφές και το σώμα συγχωνεύονται (μέση), ενώ στο πρότυπο Nette παραμένουν διαχωρισμένα (δεξιά): - -[* new-line-after.webp *] - - -Μπλοκ Τεκμηρίωσης (phpDoc) -========================== - -Κύριος κανόνας: Ποτέ μην επαναλαμβάνετε καμία πληροφορία που υπάρχει ήδη στην υπογραφή, όπως τον τύπο παραμέτρου ή τον τύπο επιστροφής, χωρίς να προσθέτετε αξία. - -Μπλοκ τεκμηρίωσης για ορισμό κλάσης: - -- Ξεκινά με την περιγραφή της κλάσης. -- Ακολουθεί μια κενή γραμμή. -- Ακολουθούν οι annotations `@property` (ή `@property-read`, `@property-write`), μία μετά την άλλη. Η σύνταξη είναι: annotation, κενό, τύπος, κενό, `$name`. -- Ακολουθούν οι annotations `@method`, μία μετά την άλλη. Η σύνταξη είναι: annotation, κενό, τύπος επιστροφής, κενό, `name(type $param, ...)` . -- Η annotation `@author` παραλείπεται. Η πατρότητα διατηρείται στην ιστορία του πηγαίου κώδικα. -- Μπορούν να χρησιμοποιηθούν οι annotations `@internal` ή `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Το μπλοκ τεκμηρίωσης για μια ιδιότητα, που περιέχει μόνο την annotation `@var`, θα πρέπει να είναι σε μία γραμμή: - -```php -/** @var string[] */ -private array $name; -``` - -Μπλοκ τεκμηρίωσης για ορισμό μεθόδου: - -- Ξεκινά με μια σύντομη περιγραφή της μεθόδου. -- Καμία κενή γραμμή. -- Annotations `@param` σε ξεχωριστές γραμμές. -- Annotation `@return`. -- Annotations `@throws`, μία μετά την άλλη. -- Μπορούν να χρησιμοποιηθούν οι annotations `@internal` ή `@deprecated`. - -Μετά από κάθε annotation ακολουθεί ένα κενό, εκτός από το `@param`, μετά το οποίο για καλύτερη αναγνωσιμότητα ακολουθούν δύο κενά. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulators αντί για Κενά -======================== - -Οι tabulators έχουν αρκετά πλεονεκτήματα έναντι των κενών: - -- Το μέγεθος της εσοχής μπορεί να προσαρμοστεί στους επεξεργαστές κειμένου και στον "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size. -- Δεν επιβάλλουν στον κώδικα την προτίμηση του χρήστη για το μέγεθος της εσοχής, οπότε ο κώδικας είναι πιο φορητός. -- Μπορούν να γραφτούν με ένα πάτημα πλήκτρου (οπουδήποτε, όχι μόνο σε επεξεργαστές που μετατρέπουν τους tabulators σε κενά). -- Η εσοχή είναι ο σκοπός τους. -- Σέβονται τις ανάγκες των συναδέλφων με προβλήματα όρασης και των τυφλών. - -Χρησιμοποιώντας tabulators στα έργα μας, επιτρέπουμε την προσαρμογή του πλάτους, η οποία μπορεί να φαίνεται περιττή στους περισσότερους ανθρώπους, αλλά είναι απαραίτητη για άτομα με προβλήματα όρασης. - -Για τους τυφλούς προγραμματιστές που χρησιμοποιούν οθόνες Braille, κάθε κενό αντιπροσωπεύει ένα κελί Braille. Αν λοιπόν η προεπιλεγμένη εσοχή είναι 4 κενά, η εσοχή 3ου επιπέδου σπαταλά 12 πολύτιμα κελιά Braille πριν καν αρχίσει ο κώδικας. Σε μια οθόνη 40 κελιών, η οποία χρησιμοποιείται συχνότερα σε φορητούς υπολογιστές, αυτό είναι περισσότερο από το ένα τέταρτο των διαθέσιμων κελιών που σπαταλούνται χωρίς καμία πληροφορία. - - -{{priority: -1}} diff --git a/contributing/el/documentation.texy b/contributing/el/documentation.texy deleted file mode 100644 index 28e360e165..0000000000 --- a/contributing/el/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Πώς να Συνεισφέρετε στην Τεκμηρίωση -*********************************** - -.[perex] -Η συνεισφορά στην τεκμηρίωση είναι μία από τις πιο ωφέλιμες δραστηριότητες, καθώς βοηθάτε άλλους να κατανοήσουν το framework. - - -Πώς να Γράφετε; ---------------- - -Η τεκμηρίωση προορίζεται κυρίως για άτομα που εξοικειώνονται με το θέμα. Επομένως, θα πρέπει να πληροί αρκετά σημαντικά σημεία: - -- Ξεκινήστε από το απλό και το γενικό. Προχωρήστε σε πιο προχωρημένα θέματα μόνο στο τέλος. -- Προσπαθήστε να εξηγήσετε το θέμα όσο το δυνατόν καλύτερα. Δοκιμάστε, για παράδειγμα, να εξηγήσετε πρώτα το θέμα σε έναν συνάδελφο. -- Αναφέρετε μόνο τις πληροφορίες που ο χρήστης πραγματικά χρειάζεται να γνωρίζει για το συγκεκριμένο θέμα. -- Επαληθεύστε ότι οι πληροφορίες σας είναι όντως αληθείς. Δοκιμάστε κάθε κώδικα. -- Να είστε συνοπτικοί - ό,τι γράψετε, συντομεύστε το στο μισό. Και μετά, αν θέλετε, ξανά. -- Χρησιμοποιήστε με φειδώ τα στοιχεία έμφασης κάθε είδους, από έντονα γράμματα μέχρι πλαίσια όπως `.[note]`. -- Στον κώδικα, τηρήστε τα [Πρότυπα Κωδικοποίησης |coding-standard]. - -Εξοικειωθείτε επίσης με τη [σύνταξη |syntax]. Για προεπισκόπηση του άρθρου κατά τη συγγραφή του, μπορείτε να χρησιμοποιήσετε τον [επεξεργαστή με προεπισκόπηση |https://editor.nette.org/]. - - -Γλωσσικές Εκδόσεις ------------------- - -Η κύρια γλώσσα είναι τα Αγγλικά. Οι αλλαγές σας θα πρέπει ιδανικά να γίνονται και στα Αγγλικά. Αν τα Αγγλικά δεν είναι το δυνατό σας σημείο, χρησιμοποιήστε τον [DeepL Translator |https://www.deepl.com/translator] και οι άλλοι θα ελέγξουν το κείμενό σας. - -Η μετάφραση στις άλλες γλώσσες θα γίνει αυτόματα μετά την έγκριση και την τελειοποίηση της τροποποίησής σας. - - -Ασήμαντες Τροποποιήσεις ------------------------ - -Για να συνεισφέρετε στην τεκμηρίωση, είναι απαραίτητο να έχετε λογαριασμό στο [GitHub |https://github.com]. - -Ο ευκολότερος τρόπος για να κάνετε μια μικρή αλλαγή στην τεκμηρίωση είναι να χρησιμοποιήσετε τους συνδέσμους στο τέλος κάθε σελίδας: - -- Το *Εμφάνιση στο GitHub* ανοίγει την πηγαία μορφή της συγκεκριμένης σελίδας στο GitHub. Στη συνέχεια, αρκεί να πατήσετε το κουμπί `E` και μπορείτε να αρχίσετε την επεξεργασία (πρέπει να είστε συνδεδεμένοι στο GitHub). -- Το *Άνοιγμα προεπισκόπησης* ανοίγει τον επεξεργαστή, όπου βλέπετε αμέσως και την τελική οπτική μορφή. - -Επειδή ο [επεξεργαστής με προεπισκόπηση |https://editor.nette.org/] δεν έχει τη δυνατότητα αποθήκευσης αλλαγών απευθείας στο GitHub, είναι απαραίτητο μετά την ολοκλήρωση των τροποποιήσεων να αντιγράψετε το πηγαίο κείμενο στο πρόχειρο (με το κουμπί *Copy to clipboard*) και στη συνέχεια να το επικολλήσετε στον επεξεργαστή στο GitHub. Κάτω από το πεδίο επεξεργασίας υπάρχει μια φόρμα για την υποβολή. Εδώ μην ξεχάσετε να συνοψίσετε και να εξηγήσετε σύντομα τον λόγο της τροποποίησής σας. Μετά την υποβολή δημιουργείται ένα λεγόμενο pull request (PR), το οποίο μπορεί να επεξεργαστεί περαιτέρω. - - -Μεγαλύτερες Τροποποιήσεις -------------------------- - -Πιο κατάλληλο από τη χρήση της διεπαφής του GitHub, είναι να είστε εξοικειωμένοι με τα βασικά της εργασίας με το σύστημα ελέγχου εκδόσεων Git. Αν δεν γνωρίζετε πώς να χρησιμοποιείτε το Git, μπορείτε να δείτε τον οδηγό [git - the simple guide |https://rogerdudler.github.io/git-guide/] και ενδεχομένως να χρησιμοποιήσετε έναν από τους πολλούς [γραφικούς clients |https://git-scm.com/downloads/guis]. - -Τροποποιήστε την τεκμηρίωση με αυτόν τον τρόπο: - -1) Στο GitHub, δημιουργήστε ένα [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] του αποθετηρίου [nette/docs |https://github.com/nette/docs]. -2) [Κλωνοποιήστε |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] αυτό το αποθετήριο στον υπολογιστή σας. -3) Στη συνέχεια, στον [κατάλληλο κλάδο |#Δομή της Τεκμηρίωσης] πραγματοποιήστε τις αλλαγές. -4) Ελέγξτε για περιττά κενά στο κείμενο χρησιμοποιώντας το εργαλείο [Code-Checker |code-checker:]. -5) Αποθηκεύστε τις αλλαγές (commit). -6) Αν είστε ικανοποιημένοι με τις αλλαγές, στείλτε (push) τις στο GitHub στο δικό σας fork. -7) Από εκεί, υποβάλετέ τις στο αποθετήριο `nette/docs` δημιουργώντας ένα [pull request |https://help.github.com/articles/creating-a-pull-request] (PR). - -Είναι σύνηθες να λαμβάνετε σχόλια με παρατηρήσεις. Παρακολουθήστε τις προτεινόμενες αλλαγές και ενσωματώστε τις. Προσθέστε τις προτεινόμενες αλλαγές ως νέα commits και στείλτε τις ξανά στο GitHub. Ποτέ μην δημιουργείτε νέο pull request για την τροποποίηση ενός υπάρχοντος pull request. - - -Δομή της Τεκμηρίωσης --------------------- - -Ολόκληρη η τεκμηρίωση βρίσκεται στο GitHub στο αποθετήριο [nette/docs |https://github.com/nette/docs]. Η τρέχουσα έκδοση βρίσκεται στον κλάδο `master`, ενώ οι παλαιότερες εκδόσεις βρίσκονται σε κλάδους όπως `doc-3.x`, `doc-2.x`. - -Το περιεχόμενο κάθε κλάδου χωρίζεται σε κύριους φακέλους που αντιπροσωπεύουν τις επιμέρους ενότητες της τεκμηρίωσης. Για παράδειγμα, το `application/` αντιστοιχεί στο https://doc.nette.org/cs/application, το `latte/` αντιστοιχεί στο https://latte.nette.org κ.λπ. Κάθε τέτοιος φάκελος περιέχει υποφακέλους που αντιπροσωπεύουν τις γλωσσικές εκδόσεις (`cs`, `en`, ...) και ενδεχομένως τον υποφάκελο `files` με εικόνες, τις οποίες είναι δυνατόν να εισαγάγετε στις σελίδες της τεκμηρίωσης. diff --git a/contributing/el/syntax.texy b/contributing/el/syntax.texy deleted file mode 100644 index 8727317430..0000000000 --- a/contributing/el/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Σύνταξη Τεκμηρίωσης -******************* - -Η τεκμηρίωση χρησιμοποιεί Markdown & [σύνταξη Texy |https://texy.info/cs/syntax] με ορισμένες επεκτάσεις. - - -Σύνδεσμοι -========= - -Για εσωτερικούς συνδέσμους χρησιμοποιείται η γραφή σε αγκύλες `[σύνδεσμος]`. Αυτό μπορεί να γίνει είτε με κάθετη γραμμή `[κείμενο συνδέσμου |στόχος συνδέσμου]`, είτε συντομευμένα `[κείμενο συνδέσμου]`, αν ο στόχος είναι ίδιος με το κείμενο (μετά από μετατροπή σε πεζά γράμματα και παύλες): - -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` - -Μπορούμε να συνδέσουμε σε άλλη γλωσσική έκδοση ή σε άλλη ενότητα. Ενότητα νοείται η βιβλιοθήκη Nette (π.χ. `forms`, `latte`, κ.λπ.) ή ειδικές ενότητες όπως `best-practices`, `quickstart` κ.λπ.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (ίδια ενότητα, άλλη γλώσσα) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (άλλη ενότητα, ίδια γλώσσα) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (άλλη ενότητα και γλώσσα) - -Με τη χρήση του `#` είναι επίσης δυνατό να στοχεύσουμε σε μια συγκεκριμένη επικεφαλίδα στη σελίδα. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (επικεφαλίδα στην τρέχουσα σελίδα) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Σύνδεσμος στην αρχική σελίδα της ενότητας: (`@home` είναι μια ειδική έκφραση για την αρχική σελίδα της ενότητας) - -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Σύνδεσμοι στην Τεκμηρίωση API ------------------------------ - -Πάντα να τους αναφέρετε μόνο χρησιμοποιώντας αυτή τη γραφή: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Χρησιμοποιήστε πλήρως προσδιορισμένα ονόματα μόνο στην πρώτη αναφορά. Για επόμενους συνδέσμους χρησιμοποιήστε το απλοποιημένο όνομα: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Σύνδεσμοι στην Τεκμηρίωση PHP ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Πηγαίος Κώδικας -=============== - -Ένα μπλοκ κώδικα ξεκινά με <code>```lang</code> και τελειώνει με <code>```</code>. Οι υποστηριζόμενες γλώσσες είναι `php`, `latte`, `neon`, `html`, `css`, `js` και `sql`. Για την εσοχή χρησιμοποιείτε πάντα tabulators. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Μπορείτε επίσης να αναφέρετε το όνομα του αρχείου ως <code>```php .{file: ArrayTest.php}</code> και το μπλοκ κώδικα θα αποδοθεί με αυτόν τον τρόπο: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Επικεφαλίδες -============ - -Την υψηλότερη επικεφαλίδα (δηλαδή τον τίτλο της σελίδας) υπογραμμίστε την με αστερίσκους (`***`). Για τον διαχωρισμό ενοτήτων χρησιμοποιήστε ίσον (`===`). Τις υπόλοιπες επικεφαλίδες υπογραμμίστε τις με ίσον (`===`) και στη συνέχεια με παύλες (`---`): - -``` -Εφαρμογές MVC & Presenters -************************** -... - - -Δημιουργία Συνδέσμων -==================== -... - - -Σύνδεσμοι στα Templates ------------------------ -... -``` - - -Πλαίσια και Στυλ -================ - -Το perex το επισημαίνουμε με την κλάση `.[perex]` .[perex] - -Τη σημείωση την επισημαίνουμε με την κλάση `.[note]` .[note] - -Τη συμβουλή την επισημαίνουμε με την κλάση `.[tip]` .[tip] - -Την προειδοποίηση την επισημαίνουμε με την κλάση `.[caution]` .[caution] - -Μια πιο έντονη προειδοποίηση την επισημαίνουμε με την κλάση `.[warning]` .[warning] - -Αριθμός έκδοσης `.{data-version:2.4.10}` .{data-version:2.4.10} - -Γράψτε τις κλάσεις πριν από τη γραμμή: - -``` -.[perex] -Αυτό είναι το perex. -``` - -Παρακαλούμε λάβετε υπόψη ότι τα πλαίσια όπως το `.[tip]` "τραβούν" τα μάτια, επομένως χρησιμοποιούνται για έμφαση, όχι για λιγότερο σημαντικές πληροφορίες. Γι' αυτό χρησιμοποιήστε τα με τη μέγιστη φειδώ. - - -Πίνακας Περιεχομένων -==================== - -Ο πίνακας περιεχομένων (σύνδεσμοι στο δεξί μενού) δημιουργείται αυτόματα για όλες τις σελίδες των οποίων το μέγεθος υπερβαίνει τα 4.000 bytes. Αυτή η προεπιλεγμένη συμπεριφορά μπορεί να τροποποιηθεί χρησιμοποιώντας τα [#meta tags] `{{toc}}`. Το κείμενο που αποτελεί τα περιεχόμενα λαμβάνεται συνήθως απευθείας από το κείμενο των επικεφαλίδων, αλλά με τον τροποποιητή `.{toc}` είναι δυνατό να εμφανιστεί στα περιεχόμενα διαφορετικό κείμενο, πράγμα που είναι χρήσιμο κυρίως για μακροσκελείς επικεφαλίδες. - -``` - - -Μακροσκελής και Έξυπνη Επικεφαλίδα .{toc: Οποιοδήποτε άλλο κείμενο εμφανίζεται στα περιεχόμενα} -=============================================================================================== -``` - - -Meta Tags -========= - -- Ορισμός προσαρμοσμένου τίτλου σελίδας (στο `<title>` και στην πλοήγηση breadcrumb) `{{title: Άλλος τίτλος}}` -- Ανακατεύθυνση `{{redirect: pla:cs}}` - βλ. [#Σύνδεσμοι] -- Επιβολή `{{toc}}` ή απενεργοποίηση `{{toc: no}}` του αυτόματου πίνακα περιεχομένων (πλαίσιο με συνδέσμους στις επιμέρους επικεφαλίδες) - -{{priority: -1}} diff --git a/contributing/en/@left-menu.texy b/contributing/en/@left-menu.texy index 967653c863..3e010b2217 100644 --- a/contributing/en/@left-menu.texy +++ b/contributing/en/@left-menu.texy @@ -8,3 +8,11 @@ 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 index d99f100765..415f50effe 100644 --- a/contributing/en/code.texy +++ b/contributing/en/code.texy @@ -75,18 +75,7 @@ git checkout -b new_branch_name v3.2.1 Coding Standards ================ -Your code must meet the [coding standard] used in the Nette Framework. An automatic tool is available for checking and fixing the code. You can install it **globally** via Composer into a folder of your choice: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Now you should be able to run the tool in the terminal. The first command checks, and the second one fixes the code in the `src` and `tests` folders in the current directory: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` +Your code must meet the [coding standard] used in the Nette Framework. To check and automatically fix your code, use the [Nette Coding Standard |tools:coding-standard] tool, where you will also find installation and usage instructions. Commit Description diff --git a/contributing/en/coding-standard.texy b/contributing/en/coding-standard.texy index e98f821d4d..5698953407 100644 --- a/contributing/en/coding-standard.texy +++ b/contributing/en/coding-standard.texy @@ -6,6 +6,9 @@ This document describes the rules and recommendations for developing Nette. When 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 the [Nette Coding Standard |tools:coding-standard] tool, so you don't have to check them by hand. + General Rules ============= @@ -13,8 +16,8 @@ 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 weak 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` +- 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 @@ -26,7 +29,7 @@ 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`). The exception are PHP attributes +- 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` @@ -109,6 +112,23 @@ public function find(string $dir, array $options): array ``` +Global Functions and Constants +============================== + +Global functions and constants are written without a leading backslash, i.e., `count($arr)` not `\count($arr)`. For functions that PHP can optimize, add `use function` at the beginning of the file so the compiler can translate them more efficiently. These include functions like `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf`, etc. Functions are listed on a single line to keep the import block compact: + +```php +use Nette; +use function count, is_array, is_scalar, sprintf; +``` + +Occasionally, we also import constants whose value knowledge may help the compiler: + +```php +use const PHP_OS_FAMILY; +``` + + Tabs Instead of Spaces ====================== diff --git a/contributing/en/documentation.texy b/contributing/en/documentation.texy index 3d2353444b..e89c20c648 100644 --- a/contributing/en/documentation.texy +++ b/contributing/en/documentation.texy @@ -14,7 +14,7 @@ Documentation is primarily intended for people who are new to the topic. Therefo - 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. +- 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. @@ -52,7 +52,7 @@ 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 |code-checker:] tool. +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). diff --git a/contributing/en/syntax.texy b/contributing/en/syntax.texy index a04894a8b9..1c6e98f712 100644 --- a/contributing/en/syntax.texy +++ b/contributing/en/syntax.texy @@ -1,7 +1,7 @@ Documentation Syntax ******************** -Documentation uses Markdown & [Texy syntax |https://texy.info/en/syntax] with several enhancements. +Documentation uses Markdown & [Texy syntax |https://texy.nette.org/syntax] with several enhancements. Links @@ -138,5 +138,6 @@ 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}} diff --git a/contributing/es/@home.texy b/contributing/es/@home.texy index ce1d46d7d1..e19fdf2f2b 100644 --- a/contributing/es/@home.texy +++ b/contributing/es/@home.texy @@ -1,17 +1,17 @@ -Conviértase en un contribuyente de Nette -**************************************** +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 la documentación, y forme parte de la comunidad de desarrolladores que participan activamente en la mejora de Nette. +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** -- [¿Cómo contribuir al código? |code] -- [Estándar de codificación |coding-standard] +- [Contribuir al código |code] +- [Estándares de codificación |coding-standard] **Documentación** -- [¿Cómo contribuir a la documentación? |documentation] -- [Sintaxis de documentación |syntax] +- [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 index ac70975ecc..153ac072de 100644 --- a/contributing/es/@left-menu.texy +++ b/contributing/es/@left-menu.texy @@ -1,10 +1,18 @@ Código ****** -- [¿Cómo contribuir al código? |code] -- [Estándar de codificación |coding-standard] +- [Contribuir al código |code] +- [Estándares de codificación |coding-standard] Documentación ************* -- [¿Cómo contribuir a la documentación? |documentation] -- [Sintaxis de documentación |syntax] +- [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 index 3fdf50969b..7b1d52da51 100644 --- a/contributing/es/code.texy +++ b/contributing/es/code.texy @@ -1,112 +1,101 @@ -Cómo contribuir al código -************************* +Contribuir al código +******************** .[perex] -¿Te preparas para contribuir a Nette Framework y necesitas orientarte sobre las reglas y procedimientos? Esta guía para principiantes te mostrará paso a paso cómo contribuir eficazmente al código, trabajar con repositorios e implementar cambios. +¿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 al código, es esencial tener una cuenta en [GitHub|https://github.com] y estar familiarizado con los fundamentos del trabajo con el sistema de control de versiones Git. Si no dominas Git, puedes consultar la guía [git - the simple guide |https://rogerdudler.github.io/git-guide/] y, opcionalmente, utilizar alguno de los muchos [clientes gráficos |https://git-scm.com/downloads/guis]. +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]. -Preparación del entorno y del repositorio ------------------------------------------ +Preparar el entorno y el repositorio +------------------------------------ -1) en GitHub, crea un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] del repositorio del [paquete |www:packages] que vas a modificar -2) [clona |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] este repositorio en tu ordenador -3) instala las dependencias, incluido [Nette Tester |tester:], mediante el comando `composer install` -4) comprueba que las pruebas funcionan ejecutando `composer tester` -5) crea una [#nueva rama] basada en la última versión publicada +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 -Implementación de tus propios cambios -------------------------------------- +Implementar sus propios cambios +------------------------------- -Ahora puedes realizar tus propias modificaciones en el código: +Ahora puede hacer sus propios ajustes en el código: -1) programa los cambios deseados y no olvides las pruebas -2) asegúrate de que las pruebas se ejecutan correctamente usando `composer tester` -3) comprueba que el código cumple con los [#estándares de codificación] -4) guarda los cambios (haz commit) con una descripción en [este formato |#Descripción del commit] +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] -Puedes crear varios commits, uno para cada paso lógico. Cada commit debe tener sentido por sí solo. +Puede crear varios commits, uno por cada paso lógico. Cada commit debería tener sentido por sí mismo. -Envío de cambios ----------------- +Enviar los cambios +------------------ -Una vez que estés satisfecho con los cambios, puedes enviarlos: +Cuando esté satisfecho con los cambios, puede enviarlos: -1) envía (haz push) los cambios a GitHub a tu fork -2) desde allí, envíalos al repositorio de Nette creando una [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) proporciona [suficiente información |#Descripción de la pull request] en la descripción +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] -Incorporación de comentarios ----------------------------- +Incorporar los comentarios +-------------------------- -Tus commits ahora serán visibles para otros. Es común recibir comentarios con sugerencias: +Sus commits son ahora visibles para los demás. Es habitual recibir comentarios con sugerencias: -1) sigue las modificaciones propuestas -2) incorpóralas como nuevos commits o [combínalas con los anteriores |https://help.github.com/en/github/using-git/about-git-rebase] -3) vuelve a enviar los commits a GitHub y aparecerán automáticamente en la pull request +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 crees una nueva pull request para modificar una existente. +Nunca cree un pull request nuevo para modificar uno existente. Documentación ------------- -Si has cambiado la funcionalidad o añadido una nueva, no olvides [añadirla también a la documentación |documentation]. +Si ha cambiado una funcionalidad o ha añadido una nueva, no se olvide de [añadirla también a la documentación|documentation]. -Nueva rama +Rama nueva ========== -Si es posible, realiza los cambios sobre la última versión publicada, es decir, la última etiqueta en la rama correspondiente. Para la etiqueta `v3.2.1`, crea una rama con este comando: +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_nueva_rama v3.2.1 +git checkout -b nombre_de_la_rama v3.2.1 ``` Estándares de codificación ========================== -Tu código debe cumplir con el [estándar de codificación |coding-standard] utilizado en Nette Framework. Hay disponible una herramienta automática para comprobar y corregir el código. Se puede instalar a través de Composer **globalmente** en la carpeta que elijas: - -```shell -composer create-project nette/coding-standard /ruta/a/nette-coding-standard -``` - -Ahora deberías poder ejecutar la herramienta en la terminal. El primer comando comprueba y el segundo también corrige el código en las carpetas `src` y `tests` del directorio actual: - -```shell -/ruta/a/nette-coding-standard/ecs check -/ruta/a/nette-coding-standard/ecs check --fix -``` +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 formato: `Presenter: fixed AJAX detection [Closes #69]` +En Nette, los asuntos de los commits tienen el siguiente formato: `Presenter: fixed AJAX detection [Closes #69]` -- Área seguida de dos puntos. -- Propósito del commit en tiempo pasado; si es posible, comienza con una palabra como: "added (nueva característica añadida)", "fixed (corrección)", "refactored (cambio en el código sin cambio de comportamiento)", changed, removed. -- Si el commit rompe la compatibilidad hacia atrás, añade "BC break". -- Posible vínculo con el issue tracker como `(#123)` o `[Closes #69]`. -- Después del asunto puede seguir una línea vacía y luego una descripción más detallada, incluyendo, por ejemplo, enlaces al foro. +- 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 de la pull request -============================== +Descripción del pull request +============================ -Al crear una pull request, la interfaz de GitHub te permitirá introducir un título y una descripción. Proporciona un título descriptivo y en la descripción ofrece la mayor cantidad de información posible sobre las razones de tu cambio. +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. -También se mostrará un encabezado donde especificarás si se trata de una nueva función o una corrección de error y si puede haber una ruptura de la compatibilidad hacia atrás (BC break). Si hay un problema relacionado (issue), enlaza a él para que se cierre después de la aprobación de la pull request. +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 --> diff --git a/contributing/es/coding-standard.texy b/contributing/es/coding-standard.texy index 8bb007f4cc..cc96d1a1b2 100644 --- a/contributing/es/coding-standard.texy +++ b/contributing/es/coding-standard.texy @@ -2,43 +2,46 @@ Estándar de codificación ************************ .[perex] -Este documento describe las reglas y recomendaciones para el desarrollo de Nette. Al contribuir con código a Nette, debes seguirlas. La forma más sencilla de hacerlo es imitar el código existente. El objetivo es que todo el código parezca escrito por una sola persona. +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 corresponde al [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] con dos excepciones principales: utiliza [#tabuladores en lugar de espacios] para la indentación y utiliza [PascalCase para las constantes de clase |https://blog.nette.org/es/for-less-screaming-in-the-code]. +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 debe contener `declare(strict_types=1)`. -- Se utilizan dos líneas vacías para separar métodos para una mejor legibilidad. -- La razón para usar el operador de silencio (`@`) debe documentarse: `@mkdir($dir); // @ - el directorio puede existir`. -- Si se utiliza un operador de comparación de tipo débil (es decir, `==`, `!=`, ...), la intención debe documentarse: `// == aceptar null`. -- Puedes escribir múltiples excepciones en un solo archivo `exceptions.php`. -- La visibilidad de los métodos no se especifica para las interfaces, ya que siempre son públicas. -- Cada propiedad, valor de retorno y parámetro debe tener un tipo especificado. Por el contrario, nunca especificamos el tipo para las constantes `final`, ya que es obvio. -- Se deben usar comillas simples para delimitar cadenas, excepto en los casos en que el literal mismo contenga apóstrofes. +- 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 nomenclatura -============================ +Convenciones de nombres +======================= -- No uses abreviaturas a menos que el nombre completo sea demasiado largo. -- Usa mayúsculas para acrónimos de dos letras, PascalCase/camelCase para acrónimos más largos. -- Usa un sustantivo o una frase nominal para el nombre de la clase. -- Los nombres de las clases deben contener no solo la especificidad (`Array`), sino también la generalidad (`ArrayIterator`). La excepción son los atributos del lenguaje PHP. -- [Las constantes de clase y los enums deben usar PascalCaps |https://blog.nette.org/es/for-less-screaming-in-the-code]. -- [Las interfaces y las clases abstractas no deben contener prefijos o sufijos |https://blog.nette.org/es/prefixes-and-suffixes-do-not-belong-in-interface-names] como `Abstract`, `Interface` o `I`. +- 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` -Envoltura y llaves -================== +Saltos de línea y llaves +======================== -El Estándar de Codificación de Nette corresponde a PSR-12 (o PER Coding Style), en algunos puntos lo complementa o modifica: +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 de flecha se escriben sin espacio antes del paréntesis, es decir, `fn($a) => $b`. -- No se requiere una línea vacía entre diferentes tipos de declaraciones de importación `use`. -- El tipo de retorno de la función/método y la llave de apertura siempre están en líneas separadas: +- 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( @@ -50,7 +53,7 @@ El Estándar de Codificación de Nette corresponde a PSR-12 (o PER Coding Style) } ``` -La llave de apertura en una línea separada es importante para la separación visual de la firma de la función/método del cuerpo. Si la firma está en una línea, la separación es clara (imagen izquierda); si está en varias líneas, en PSR las firmas y el cuerpo se fusionan (medio), mientras que en el estándar de Nette siguen separados (derecha): +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 *] @@ -58,20 +61,20 @@ La llave de apertura en una línea separada es importante para la separación vi Bloques de documentación (phpDoc) ================================= -Regla principal: Nunca dupliques ninguna información en la firma, como el tipo de parámetro o el tipo de retorno, sin valor añadido. +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 para la definición de clase: +Bloque de documentación de la definición de una clase: -- Comienza con la descripción de la clase. -- Sigue una línea vacía. -- Siguen las anotaciones `@property` (o `@property-read`, `@property-write`), una tras otra. La sintaxis es: anotación, espacio, tipo, espacio, `$nombre`. -- Siguen las anotaciones `@method`, una tras otra. La sintaxis es: 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`. +- 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 /** - * Parte del mensaje MIME. + * MIME message part. * * @property string $encoding * @property-read array $headers @@ -80,27 +83,27 @@ Bloque de documentación para la definición de clase: */ ``` -El bloque de documentación para una propiedad, que contiene solo la anotación `@var`, debe ser de una sola línea: +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 para la definición de método: +Bloque de documentación de la definición de un método: -- Comienza con una breve descripción del método. -- Sin línea vacía. -- Anotaciones `@param` en líneas individuales. -- Anotación `@return`. -- Anotaciones `@throws`, una tras otra. -- Se pueden usar las anotaciones `@internal` o `@deprecated`. +- 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` -Cada anotación va seguida de un espacio, excepto `@param`, que va seguida de dos espacios para una mejor legibilidad. +A cada anotación le sigue un espacio, salvo a `@param`, a la que le siguen dos espacios para mejorar la legibilidad. ```php /** - * Encuentra un archivo en el directorio. + * Finds a file in directory. * @param string[] $options * @return string[] * @throws DirectoryNotFoundException @@ -109,20 +112,37 @@ 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 del espaciado se puede personalizar 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 del usuario sobre el tamaño de la indentación, por lo que el código es más portátil. -- Se pueden escribir con una sola pulsación de tecla (en cualquier lugar, no solo en editores que convierten tabuladores en espacios). -- La indentación es su propósito. -- Respetan las necesidades de los compañeros con discapacidad visual y ciegos. +- 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 la personalización del ancho, lo que puede parecer innecesario para la mayoría de las personas, pero es esencial para las personas con discapacidad visual. +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 pantallas Braille, cada espacio representa una celda Braille. Por lo tanto, si la indentación predeterminada es de 4 espacios, la indentación de tercer nivel desperdicia 12 valiosas celdas Braille incluso antes de que comience el código. En una pantalla de 40 celdas, que es la más utilizada en portátiles, esto es más de una cuarta parte de las celdas disponibles que se desperdician sin ninguna información. +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 index f3e0e63004..16d5e84c16 100644 --- a/contributing/es/documentation.texy +++ b/contributing/es/documentation.texy @@ -1,68 +1,68 @@ -Cómo contribuir a la documentación -********************************** +Contribuir a la documentación +***************************** .[perex] -Contribuir a la documentación es una de las actividades más gratificantes, ya que ayudas a otros a comprender el framework. +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á destinada principalmente a personas que se están familiarizando con el tema. Por lo tanto, debe cumplir varios puntos importantes: +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: -- Comienza por lo simple y general. Pasa a temas más avanzados solo al final. -- Intenta explicar el asunto lo mejor posible. Por ejemplo, intenta explicar primero el tema a un colega. -- Proporciona solo la información que el usuario realmente necesita saber sobre el tema dado. -- Verifica que tu información sea realmente veraz. Prueba cada fragmento de código. -- Sé conciso: reduce a la mitad lo que escribas. Y luego, si es necesario, hazlo de nuevo. -- Ahorra en todo tipo de resaltados, desde negrita hasta recuadros como `.[note]`. -- En los códigos, sigue el [Estándar de codificación |coding-standard]. +- 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. -Adopta también la [sintaxis |syntax]. Para previsualizar el artículo mientras lo escribes, puedes usar el [editor con vista previa |https://editor.nette.org/]. +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 lingüísticas ----------------------- +Versiones de idioma +------------------- -El idioma principal es el inglés. Si el inglés no es tu punto fuerte, usa [DeepL Translator |https://www.deepl.com/translator] y otros revisarán tu texto. Los cambios deben enviarse en inglés. +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 otros idiomas se realizará automáticamente después de la aprobación y ajuste de tu modificación. +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, es esencial tener una cuenta en [GitHub |https://github.com]. +Para contribuir a la documentación necesita tener una cuenta en [GitHub |https://github.com]. -La forma más sencilla de realizar un pequeño cambio en la documentación es utilizar los enlaces al final de cada página: +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: -- *Mostrar en GitHub* abre la versión fuente de la página dada en GitHub. Luego, simplemente presiona el botón `E` y puedes comenzar a editar (es necesario haber iniciado sesión en GitHub). -- *Abrir vista previa* abre el editor, donde puedes ver directamente la apariencia visual resultante. +- *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. -Dado que el [editor con vista previa |https://editor.nette.org/] no tiene la opción de guardar cambios directamente en GitHub, es necesario, después de completar las ediciones, copiar el texto fuente al portapapeles (con el botón *Copy to clipboard*) y luego pegarlo en el editor de GitHub. Debajo del campo de edición hay un formulario para enviar. Aquí, no olvides resumir brevemente y explicar el motivo de tu modificación. Después de enviar, se crea una llamada pull request (PR), que se puede editar más. +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 mayores ------------------ +Ediciones más grandes +--------------------- -Más apropiado que usar la interfaz de GitHub es estar familiarizado con los fundamentos del trabajo con el sistema de control de versiones Git. Si no dominas Git, puedes consultar la guía [git - the simple guide |https://rogerdudler.github.io/git-guide/] y, opcionalmente, utilizar alguno de los muchos [clientes gráficos |https://git-scm.com/downloads/guis]. +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. -Modifica la documentación de esta manera: +Edite la documentación así: -1) en GitHub, crea 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) [clona |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] este repositorio en tu ordenador -3) luego, en la [rama apropiada |#Estructura de la documentación], realiza los cambios -4) comprueba si hay espacios sobrantes en el texto usando la herramienta [Code-Checker |code-checker:] -5) guarda los cambios (haz commit) -6) si estás satisfecho con los cambios, envíalos (haz push) a GitHub a tu fork -7) desde allí, envíalos al repositorio `nette/docs` creando una [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) +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 común recibir comentarios con sugerencias. Sigue los cambios propuestos e incorpóralos. Añade los cambios propuestos como nuevos commits y vuelve a enviarlos a GitHub. Nunca crees una nueva pull request para modificar una pull request existente. +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 se encuentra en GitHub en el repositorio [nette/docs |https://github.com/nette/docs]. La versión actual está en `master`, las versiones anteriores se encuentran en ramas como `doc-3.x`, `doc-2.x`. +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 áreas individuales de la documentación. Por ejemplo, `application/` corresponde a https://doc.nette.org/es/application, `latte/` corresponde a https://latte.nette.org, etc. Cada una de estas carpetas contiene subcarpetas que representan las versiones lingüísticas (`cs`, `en`, ...) y, opcionalmente, una subcarpeta `files` con imágenes que se pueden insertar en las páginas de la documentación. +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 index 97d8316c1c..d984921ab8 100644 --- a/contributing/es/syntax.texy +++ b/contributing/es/syntax.texy @@ -1,29 +1,29 @@ Sintaxis de la documentación **************************** -La documentación utiliza Markdown y la [sintaxis Texy |https://texy.info/cs/syntax] con algunas extensiones. +La documentación usa Markdown y la [sintaxis de Texy |https://texy.nette.org/syntax] con varias mejoras. Enlaces ======= -Para los enlaces internos se utiliza la notación entre corchetes `[enlace]`. Ya sea en la forma con barra vertical `[texto del enlace |destino del enlace]`, o de forma abreviada `[texto del enlace]`, si el destino es idéntico al texto (después de la transformación a minúsculas y guiones): +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): -- `[Page name |Page name]` -> `<a href="/es/page-name">Page name</a>` -- `[texto del enlace |Page name]` -> `<a href="/es/page-name">texto del enlace</a>` +- `[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 lingüística o a otra sección. Por sección se entiende una librería de Nette (p. ej., `forms`, `latte`, etc.) o secciones especiales como `best-practices`, `quickstart`, etc.: +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:Page name |cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (misma sección, diferente idioma) -- `[tracy:Page name |tracy:Page name]` -> `<a href="//tracy.nette.org/es/page-name">Page name</a>` (diferente sección, mismo idioma) -- `[tracy:cs:Page name |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (diferente sección e idioma) +- `[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) -Usando `#` también es posible apuntar a un encabezado específico en la página. +También es posible apuntar a un encabezado concreto de la página con `#`. -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (encabezado en la página actual) -- `[Page name#Heading |Page name#Heading]` -> `<a href="/es/page-name#toc-heading">Page name</a>` +- `[#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 de inicio de la sección: (`@home` es una expresión especial para la página de inicio de la sección) +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>` @@ -32,14 +32,14 @@ Enlace a la página de inicio de la sección: (`@home` es una expresión especia Enlaces a la documentación de la API ------------------------------------ -Siempre indíquelos únicamente mediante esta notación: +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 nombres completamente calificados solo en la primera mención. Para enlaces posteriores, use el nombre simplificado: +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()] @@ -53,7 +53,7 @@ Enlaces a la documentación de PHP Código fuente ============= -Un bloque de código comienza con <code>```lang</code> y termina con <code>```</code>. Los lenguajes admitidos son `php`, `latte`, `neon`, `html`, `css`, `js` y `sql`. Para la indentación, use siempre tabuladores. +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 @@ -63,7 +63,7 @@ Un bloque de código comienza con <code>```lang</code> y termina con ``` ``` -También puede especificar el nombre del archivo como <code>```php .{file: ArrayTest.php}</code> y el bloque de código se renderizará de esta manera: +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) @@ -75,21 +75,21 @@ public function renderPage($id) Encabezados =========== -El encabezado más alto (es decir, el nombre de la página) debe subrayarse con asteriscos (`***`). Para separar secciones, use signos de igual (`===`). Subraye los encabezados de nivel inferior con signos de igual (`===`) y luego con guiones (`---`): +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 (`-`): ``` -Aplicaciones MVC y presenters +MVC Applications & Presenters ***************************** ... -Creación de enlaces -=================== +Link Creation +============= ... -Enlaces en plantillas ---------------------- +Links in Templates +------------------ ... ``` @@ -97,46 +97,47 @@ Enlaces en plantillas Recuadros y estilos =================== -Marcamos el perex con la clase `.[perex]` .[perex] +Perex marcado con la clase `.[perex]` .[perex] -Marcamos una nota con la clase `.[note]` .[note] +Nota marcada con la clase `.[note]` .[note] -Marcamos un consejo con la clase `.[tip]` .[tip] +Consejo marcado con la clase `.[tip]` .[tip] -Marcamos una advertencia con la clase `.[caution]` .[caution] +Precaución marcada con la clase `.[caution]` .[caution] -Marcamos una advertencia más fuerte con la clase `.[warning]` .[warning] +Advertencia fuerte marcada con la clase `.[warning]` .[warning] Número de versión `.{data-version:2.4.10}` .{data-version:2.4.10} -Escriba las clases antes de la línea: +Las clases se escriben antes de la línea a la que se aplican: ``` .[perex] -Este es el perex. +Esto es el perex. ``` -Tenga en cuenta que los recuadros como `.[tip]` "atraen" la vista, por lo que se utilizan para enfatizar, no para información menos importante. Por lo tanto, úselos con la máxima moderación. +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 (enlaces en el menú derecho) se genera automáticamente para todas las páginas cuyo tamaño supere los 4000 bytes, aunque este comportamiento predeterminado se puede modificar mediante la [metaetiqueta |#Metaetiquetas] `{{toc}}`. El texto que forma la tabla de contenidos se toma por defecto directamente del texto de los encabezados, pero mediante el modificador `.{toc}` es posible mostrar un texto diferente en la tabla de contenidos, lo cual es útil principalmente para encabezados más largos. +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: Cualquier otro texto mostrado en la tabla de contenidos} -============================================================================================== +Encabezado largo e inteligente .{toc: Un texto distinto para la TOC} +==================================================================== ``` Metaetiquetas ============= -- establecer un título de página personalizado (en `<title>` y navegación de migas de pan) `{{title: Otro título}}` -- redirección `{{redirect: pla:cs}}` - ver [#enlaces] -- forzar `{{toc}}` o deshabilitar `{{toc: no}}` la tabla de contenidos automática (recuadro con enlaces a encabezados individuales) +- 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/fr/@home.texy b/contributing/fr/@home.texy index 9abf985f3b..b530df7923 100644 --- a/contributing/fr/@home.texy +++ b/contributing/fr/@home.texy @@ -2,16 +2,16 @@ Devenez contributeur de Nette ***************************** .[perex] -Découvrez comment vous pouvez vous impliquer dans notre projet open source. Apprenez les procédures pour contribuer au code source et à la documentation et devenez membre de la communauté de développeurs qui participent activement à l'amélioration de Nette. +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** -- [Comment contribuer au code ? |code] -- [Standard de codage |coding-standard] +- [Contribuer au code |code] +- [Standards de codage |coding-standard] **Documentation** -- [Comment contribuer à la documentation ? |documentation] +- [Contribuer à la documentation |documentation] - [Syntaxe de la documentation |syntax] -- "Éditeur de prévisualisation":https://editor.nette.org +- "Éditeur d'aperçu":https://editor.nette.org diff --git a/contributing/fr/@left-menu.texy b/contributing/fr/@left-menu.texy index ca09218bbd..7dad1d7512 100644 --- a/contributing/fr/@left-menu.texy +++ b/contributing/fr/@left-menu.texy @@ -1,10 +1,18 @@ Code **** -- [Comment contribuer au code ? |code] -- [Standard de codage |coding-standard] +- [Contribuer au code |code] +- [Standards de codage |coding-standard] Documentation ************* -- [Comment contribuer à la documentation ? |documentation] +- [Contribuer à la documentation |documentation] - [Syntaxe de la documentation |syntax] -- "Éditeur de prévisualisation":https://editor.nette.org +- "É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 index 39c6b6bf19..f8002bacff 100644 --- a/contributing/fr/code.texy +++ b/contributing/fr/code.texy @@ -1,57 +1,57 @@ -Comment contribuer au code -************************** +Contribuer au code +****************** .[perex] -Vous êtes sur le point de contribuer à Nette Framework et avez besoin de vous familiariser avec les règles et procédures ? Ce guide pour débutants vous montrera étape par étape comment contribuer efficacement au code, travailler avec les dépôts et implémenter des changements. +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 nécessaire d'avoir un compte sur [GitHub|https://github.com] et d'être familiarisé avec les bases du travail avec le système de contrôle de version Git. Si vous ne maîtrisez pas Git, vous pouvez consulter le guide [git - le guide simple |https://rogerdudler.github.io/git-guide/] ou utiliser l'un des nombreux [clients graphiques |https://git-scm.com/downloads/guis]. +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 vous apprêtez à 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, y compris [Nette Tester |tester:], à l'aide de la commande `composer install`. -4) Vérifiez que les tests fonctionnent en exécutant `composer tester`. -5) Créez une [#nouvelle branche] basée sur la dernière version publiée. +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 -Implémentation de vos propres changements ------------------------------------------ +Mise en œuvre de vos modifications +---------------------------------- -Vous pouvez maintenant effectuer vos propres modifications de code : +Vous pouvez maintenant apporter vos propres modifications au code : -1) Programmez les changements souhaités et n'oubliez pas les tests. -2) Assurez-vous que les tests réussissent en utilisant `composer tester`. -3) Vérifiez que le code respecte le [standard de codage |#Standards de codage]. -4) Enregistrez (commitez) les changements avec une description dans [ce format |#Description du commit]. +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 pour chaque étape logique. Chaque commit doit être autonome et avoir un sens. +Vous pouvez créer plusieurs commits, un par étape logique. Chaque commit doit avoir du sens à lui seul. -Envoi des changements ---------------------- +Soumission des modifications +---------------------------- -Une fois que vous êtes satisfait des changements, vous pouvez les envoyer : +Une fois satisfait de vos modifications, vous pouvez les soumettre : -1) Envoyez (pushez) les changements sur GitHub vers votre fork. -2) De là, envoyez-les au dépôt Nette en créant une [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). -3) Fournissez [suffisamment d'informations |#Description de la pull request] dans la description. +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 -Intégration des commentaires ----------------------------- +Prise en compte des retours +--------------------------- -Vos commits seront désormais visibles par les autres. Il est courant de recevoir des commentaires avec des remarques : +Vos commits sont désormais visibles par les autres. Il est courant de recevoir des commentaires avec des suggestions : -1) Suivez les modifications suggérées. -2) Intégrez-les comme 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. +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. @@ -59,54 +59,43 @@ Ne créez jamais une nouvelle pull request pour modifier une pull request exista Documentation ------------- -Si vous avez modifié une fonctionnalité ou en avez ajouté une nouvelle, n'oubliez pas de l'[ajouter à la documentation |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, effectuez les changements par rapport à la dernière version publiée, c'est-à-dire le dernier tag dans la branche concernée. Pour le tag `v3.2.1`, vous créez une branche avec cette commande : +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 nom_nouvelle_branche v3.2.1 +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. Un outil automatique est disponible pour vérifier et corriger le code. Il peut être installé via Composer **globalement** dans le dossier de votre choix : - -```shell -composer create-project nette/coding-standard /chemin/vers/nette-coding-standard -``` - -Vous devriez maintenant pouvoir exécuter l'outil dans le terminal. La première commande vérifie et la seconde corrige également le code dans les dossiers `src` et `tests` du répertoire courant : - -```shell -/chemin/vers/nette-coding-standard/ecs check -/chemin/vers/nette-coding-standard/ecs check --fix -``` +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 : `Presenter: fixed AJAX detection [Closes #69]` +Dans Nette, les sujets des commits ont le format suivant : `Presenter: fixed AJAX detection [Closes #69]` -- la zone suivie de deux-points -- l'objectif du commit au passé, si possible, commencez par le mot : "added (nouvelle fonctionnalité ajoutée)", "fixed (correction)", "refactored (modification du code sans changement de comportement)", changed, removed -- si le commit rompt la compatibilité ascendante, ajoutez "BC break" -- une éventuelle liaison avec le suivi des problèmes comme `(#123)` ou `[Closes #69]` -- après le sujet, une ligne vide peut suivre, puis une description plus détaillée incluant par exemple des liens vers le forum +- 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 ============================== -Lors de la création d'une pull request, l'interface GitHub vous permettra de saisir un titre et une description. Donnez un titre concis et dans la description, fournissez autant d'informations que possible sur les raisons de votre changement. +À 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. -Un en-tête s'affichera également, où vous spécifierez s'il s'agit d'une nouvelle fonctionnalité ou d'une correction de bug et si cela peut entraîner une rupture de compatibilité ascendante (BC break). S'il existe un problème lié (issue), référencez-le afin qu'il soit fermé après l'approbation de la pull request. +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 --> diff --git a/contributing/fr/coding-standard.texy b/contributing/fr/coding-standard.texy index e9d450f247..7fb99402bd 100644 --- a/contributing/fr/coding-standard.texy +++ b/contributing/fr/coding-standard.texy @@ -2,43 +2,46 @@ Standard de codage ****************** .[perex] -Ce document décrit les règles et recommandations pour le développement de Nette. Lorsque vous contribuez au code de Nette, vous devez les respecter. La manière la plus simple de le faire est d'imiter le code existant. L'objectif est que tout le code ait l'air d'avoir été écrit par une seule personne. +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 standard de codage Nette correspond au [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] avec deux exceptions principales : il utilise des [#Tabulations au lieu d'espaces] pour l'indentation et [utilise PascalCase pour les constantes de classe |https://blog.nette.org/fr/for-less-screaming-in-the-code]. +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 sont utilisées pour séparer les méthodes pour une meilleure lisibilité. -- La raison de l'utilisation de l'opérateur silence (`@`) doit être documentée : `@mkdir($dir); // @ - le répertoire peut exister`. -- Si un opérateur de comparaison faiblement typé est utilisé (c.-à-d. `==`, `!=`, ...), l'intention doit être documentée : `// == accepter null` -- Vous pouvez écrire plusieurs exceptions dans un seul fichier `exceptions.php`. -- La visibilité des méthodes n'est pas spécifiée pour les interfaces, car elles sont toujours publiques. -- Chaque propriété, valeur de retour et paramètre doit avoir un type spécifié. Inversement, pour les constantes `final`, nous ne spécifions jamais le type, car il est évident. -- Les guillemets simples (`'`) doivent être utilisés pour délimiter les chaînes de caractères, sauf lorsque le littéral lui-même contient des apostrophes. +- 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 ====================== -- N'utilisez pas d'abréviations, sauf si le nom complet est trop long. -- Utilisez des majuscules pour les abréviations de deux lettres, pascal/camel pour les abréviations plus longues. -- Utilisez un nom ou une expression nominale pour le nom de la classe. -- Les noms de classe doivent contenir non seulement la spécificité (`Array`), mais aussi la généralité (`ArrayIterator`). Les attributs du langage PHP font exception. -- "Les constantes de classe et les énumérations doivent utiliser PascalCaps":https://blog.nette.org/fr/for-less-screaming-in-the-code. -- "Les interfaces et les classes abstraites ne doivent pas contenir de préfixes ou de suffixes":https://blog.nette.org/fr/prefixes-and-suffixes-do-not-belong-in-interface-names comme `Abstract`, `Interface` ou `I`. +- É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 standard de codage Nette correspond à PSR-12 (resp. PER Coding Style), le complète ou le modifie sur certains points : +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.-à-d. `fn($a) => $b` -- une ligne vide n'est pas requise entre différents types d'instructions d'importation `use` -- le type de retour de la fonction/méthode et l'accolade ouvrante sont toujours sur des lignes séparées : +- 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( @@ -46,11 +49,11 @@ Le standard de codage Nette correspond à PSR-12 (resp. PER Coding Style), le co array $options, ): array { - // corps de la méthode + // method body } ``` -L'accolade ouvrante sur une ligne séparée est importante pour la séparation visuelle de la signature de la fonction/méthode du corps. Si la signature est sur une seule ligne, la séparation est claire (image de gauche), si elle est sur plusieurs lignes, dans PSR les signatures et le corps se confondent (au milieu), tandis que dans le standard Nette, ils restent séparés (à droite) : +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 *] @@ -58,20 +61,20 @@ L'accolade ouvrante sur une ligne séparée est importante pour la séparation v Blocs de documentation (phpDoc) =============================== -Règle principale : Ne dupliquez jamais aucune information déjà présente dans la signature, comme le type de paramètre ou le type de retour, sans apporter une valeur ajoutée (par exemple, une description plus détaillée du type). +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 pour la définition de classe : +Bloc de documentation d'une définition de classe : -- Commence par la description de la classe. -- Suivi d'une ligne vide. -- Suivi des annotations `@property` (ou `@property-read`, `@property-write`), une par ligne. La syntaxe est : annotation, espace, type, espace, `$nom`. -- Suivi des annotations `@method`, une par ligne. La syntaxe est : 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. +- 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 /** - * Partie de message MIME. + * MIME message part. * * @property string $encoding * @property-read array $headers @@ -80,27 +83,27 @@ Bloc de documentation pour la définition de classe : */ ``` -Un bloc de documentation pour une propriété, qui ne contient que l'annotation `@var`, doit être sur une seule ligne : +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 pour la définition de méthode : +Bloc de documentation d'une définition de méthode : -- Commence par une brève description de la méthode. -- Pas de ligne vide. -- Annotations `@param` sur des lignes individuelles. -- Annotation `@return`. -- Annotations `@throws`, une par une. -- Les annotations `@internal` ou `@deprecated` peuvent être utilisées. +- 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'un espace, à l'exception de `@param`, qui est suivie de deux espaces pour une meilleure lisibilité. +Chaque annotation est suivie d'une espace, sauf `@param`, qui est suivie de deux espaces pour une meilleure lisibilité. ```php /** - * Trouve un fichier dans le répertoire. + * Finds a file in directory. * @param string[] $options * @return string[] * @throws DirectoryNotFoundException @@ -109,20 +112,37 @@ 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 présentent plusieurs avantages par rapport aux espaces : +Les tabulations ont plusieurs avantages sur les espaces : -- la taille de l'indentation peut être personnalisée 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 de l'utilisateur en matière de taille d'indentation, de sorte que le code est plus portable -- elles peuvent être écrites en une seule touche (partout, pas seulement dans les éditeurs qui transforment les tabulations en espaces) -- l'indentation est leur fonction première -- elles respectent les besoins des collègues malvoyants et aveugles +- 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 une personnalisation de la largeur, ce qui peut sembler superflu pour la plupart des gens, mais est essentiel pour les personnes ayant une déficience visuelle. +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 des afficheurs braille, chaque espace représente une cellule braille. Ainsi, si l'indentation par défaut est de 4 espaces, une indentation de 3ème niveau gaspille 12 précieuses cellules braille avant même le début du code. Sur un afficheur de 40 cellules, qui est le plus couramment utilisé sur les ordinateurs portables, cela représente plus d'un quart des cellules disponibles gaspillées sans aucune information. +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 index 87d2fa6640..669bd7149f 100644 --- a/contributing/fr/documentation.texy +++ b/contributing/fr/documentation.texy @@ -1,68 +1,68 @@ -Comment contribuer à la documentation -************************************* +Contribuer à la documentation +***************************** .[perex] -Contribuer à la documentation est l'une des activités les plus enrichissantes, car vous aidez les autres à comprendre le framework. +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 est principalement destinée aux personnes qui découvrent le sujet. Par conséquent, elle doit respecter plusieurs points importants : +La documentation s'adresse avant tout à des personnes qui découvrent le sujet. Elle doit donc satisfaire plusieurs points importants : -- Commencez par le simple et le général. N'abordez les sujets plus avancés qu'à la fin. -- Essayez d'expliquer les choses le mieux possible. Essayez par exemple d'expliquer d'abord le sujet à un collègue. -- Ne fournissez que les informations dont l'utilisateur a réellement besoin sur le sujet donné. -- Vérifiez que vos informations sont réellement exactes. Testez chaque exemple de code. -- Soyez concis - réduisez de moitié ce que vous écrivez. Et puis n'hésitez pas à le refaire. -- Économisez toutes sortes de mises en évidence, du texte en gras aux cadres comme `.[note]`. -- Respectez le [Standard de codage |Coding Standard] dans les exemples de code. +- 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. -Maîtrisez également la [syntaxe |syntax]. Pour prévisualiser l'article pendant son écriture, vous pouvez utiliser l'[éditeur avec aperçu |https://editor.nette.org/]. +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 ---------------------- -La langue principale est l'anglais. Vos modifications devraient donc idéalement être apportées aux versions anglaise et tchèque de la documentation. Si l'anglais n'est pas votre point fort, utilisez [DeepL Translator |https://www.deepl.com/translator] et les autres vérifieront votre texte. +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 sera effectuée automatiquement après approbation et finalisation de votre modification. +La traduction dans les autres langues se fera automatiquement une fois votre modification approuvée et finalisée. -Modifications triviales ------------------------ +Petites modifications +--------------------- -Pour contribuer à la documentation, il est nécessaire d'avoir un compte sur [GitHub|https://github.com]. +Pour contribuer à la documentation, vous devez avoir un compte sur [GitHub |https://github.com]. -La manière la plus simple d'apporter une petite modification à la documentation est d'utiliser les liens à la fin de chaque page : +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 donnée sur GitHub. Ensuite, il suffit d'appuyer sur le bouton `E` pour commencer à éditer (il faut être connecté à GitHub). -- *Ouvrir l'aperçu* ouvre l'éditeur, où vous voyez directement le rendu visuel final. +- *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 avec aperçu |https://editor.nette.org/] n'a pas la possibilité d'enregistrer les modifications directement sur GitHub, il est nécessaire, après avoir terminé les modifications, de copier le texte source dans le presse-papiers (bouton *Copy to clipboard*) puis de le coller dans l'éditeur sur GitHub. Sous le champ d'édition se trouve un formulaire d'envoi. N'oubliez pas d'y 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 être éditée ultérieurement. +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 d'utiliser l'interface GitHub, il est préférable d'être familiarisé avec les bases du travail avec le système de contrôle de version Git. Si vous ne maîtrisez pas Git, vous pouvez consulter le guide [git - le guide simple |https://rogerdudler.github.io/git-guide/] ou utiliser l'un des nombreux [clients graphiques |https://git-scm.com/downloads/guis]. +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 de cette manière : +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) Ensuite, dans la [branche appropriée |#Structure de la documentation], effectuez les modifications. -4) Vérifiez les espaces superflus dans le texte à l'aide de l'outil [Code-Checker |code-checker:]. -5) Enregistrez (commitez) les changements. -6) Si vous êtes satisfait des changements, envoyez-les (pushez) sur GitHub vers votre fork. -7) De là, envoyez-les au dépôt `nette/docs` en créant une [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). +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 que vous receviez des commentaires avec des remarques. Suivez les modifications suggérées et intégrez-les. Ajoutez les modifications suggérées comme de nouveaux commits et renvoyez-les sur GitHub. Ne créez jamais une nouvelle pull request pour modifier une pull request existante. +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 est hébergée 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 sont situées dans des branches comme `doc-3.x`, `doc-2.x`. +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 divisé en dossiers principaux représentant les différentes sections de la documentation. Par exemple, `application/` correspond à https://doc.nette.org/fr/application, `latte/` correspond à https://latte.nette.org, etc. Chacun de ces dossiers contient des sous-dossiers représentant les versions linguistiques (`cs`, `en`, `fr`, ...) et éventuellement un sous-dossier `files` avec des images, qui peuvent être insérées dans les pages de la documentation. +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 index 29c724851e..200f39bdef 100644 --- a/contributing/fr/syntax.texy +++ b/contributing/fr/syntax.texy @@ -1,51 +1,51 @@ Syntaxe de la documentation *************************** -La documentation utilise Markdown & la [syntaxe Texy |https://texy.info/cs/syntax] avec quelques extensions. +La documentation utilise Markdown et la [syntaxe Texy |https://texy.nette.org/syntax] avec quelques enrichissements. Liens ===== -Pour les liens internes, on utilise la notation entre crochets `[...]`. Soit sous la forme avec une barre verticale `[texte du lien |cible du lien]`, soit en abrégé `[Cible du lien]` si la cible est identique au texte (après transformation en minuscules et tirets) : +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) : -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` +- `[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 lier à une autre version linguistique ou à une autre section. Une section désigne une bibliothèque Nette (par ex. `forms`, `latte`, etc.) ou des sections spéciales comme `best-practices`, `quickstart` etc. : +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:Page name]` -> `<a href="/cs/page-name">Page name</a>` (même section, langue différente) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (section différente, même langue) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (section et langue différentes) +- `[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) -Avec `#`, il est également possible de cibler un titre spécifique sur la page. +Il est également possible de viser un titre précis de la page à l'aide de `#`. -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (titre sur la page actuelle) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` +- `[#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 une expression spéciale pour la page d'accueil de la section) +Lien vers la page d'accueil de la section : (`@home` est un terme spécial désignant la page d'accueil de la section) -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` +- `[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 API -------------------------------- +Liens vers la documentation de l'API +------------------------------------ -Utilisez toujours uniquement cette notation : +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] -Utilisez les noms pleinement qualifiés uniquement lors de la première mention. Pour les liens suivants, utilisez le nom simplifié : +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 PHP -------------------------------- +Liens vers la documentation de PHP +---------------------------------- - `[php:substr]` -> [php:substr] @@ -53,7 +53,7 @@ Liens vers la documentation PHP Code source =========== -Un bloc de code commence par <code>```lang</code> et se termine par <code>```</code>. Les langues prises en charge sont `php`, `latte`, `neon`, `html`, `css`, `js` et `sql`. Utilisez toujours des tabulations pour l'indentation. +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 @@ -63,7 +63,7 @@ Un bloc de code commence par <code>```lang</code> et se termine par ``` ``` -Vous pouvez également indiquer un nom de fichier comme <code>```php .{file: ArrayTest.php}</code> et le bloc de code sera rendu de cette manière : +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) @@ -75,68 +75,69 @@ public function renderPage($id) Titres ====== -Le titre le plus élevé (c'est-à-dire le nom de la page) doit être souligné par des étoiles. Pour séparer les sections, utilisez des signes égal. Soulignez les titres avec des signes égal, puis avec des tirets : +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 (`-`) : ``` -Applications MVC & Presenters +MVC Applications & Presenters ***************************** ... -Création de liens -================= +Link Creation +============= ... -Liens dans les templates ------------------------- +Links in Templates +------------------ ... ``` -Cadres et styles -================ +Encadrés et styles +================== -Le perex est marqué avec la classe `.[perex]` .[perex] +Perex marqué par la classe `.[perex]` .[perex] -Une note est marquée avec la classe `.[note]` .[note] +Note marquée par la classe `.[note]` .[note] -Un conseil est marqué avec la classe `.[tip]` .[tip] +Astuce marquée par la classe `.[tip]` .[tip] -Un avertissement est marqué avec la classe `.[caution]` .[caution] +Avertissement marqué par la classe `.[caution]` .[caution] -Un avertissement plus important est marqué avec la classe `.[warning]` .[warning] +Avertissement fort marqué par la classe `.[warning]` .[warning] Numéro de version `.{data-version:2.4.10}` .{data-version:2.4.10} -Écrivez les classes avant la ligne : +Les classes s'écrivent avant la ligne à laquelle elles s'appliquent : ``` .[perex] -Ceci est l'introduction. +This is the perex. ``` -Veuillez noter que les cadres comme `.[tip]` attirent l'attention, ils sont donc utilisés pour mettre en évidence des informations importantes, et non pour des détails secondaires. Par conséquent, utilisez-les avec parcimonie. +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 ================== -La table des matières (liens dans le menu de droite) est générée automatiquement pour toutes les pages dont la taille dépasse 4 000 octets. Ce comportement par défaut peut être modifié à l'aide de la [Balise méta |#Balises méta] `{{toc}}`. Le texte affiché dans la table des matières est pris par défaut directement dans le texte des titres. Cependant, à l'aide du modificateur `.{toc}`, il est possible d'afficher un texte différent, ce qui est particulièrement utile pour les titres plus longs. +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. ``` -Titre long et potentiellement complexe .{toc: Titre court pour la table des matières} -===================================================================================== +Long and Intelligent Heading .{toc: A Different Text for TOC} +============================================================= ``` -Balises méta +Méta-balises ============ -- définir le titre de la page personnalisée (dans `<title>` et le fil d'Ariane) `{{title : Autre titre}}`` -- redirection `{{redirect : pla:cs}}` - voir [#Liens] -- forcer `{{toc}}` ou désactiver `{{toc : no}}` le contenu automatique (boîte avec des liens vers les titres individuels) +- 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/hu/@home.texy b/contributing/hu/@home.texy deleted file mode 100644 index ea18ed53fa..0000000000 --- a/contributing/hu/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Legyen Ön is Nette hozzájáruló -****************************** - -.[perex] -Tudja meg, hogyan vehet részt nyílt forráskódú projektünkben. Sajátítsa el a forráskódhoz és a dokumentációhoz való hozzájárulás eljárásait, és váljon a Nette fejlesztését aktívan segítő fejlesztői közösség részévé. - - -**Kód** - -- [Hogyan járulhat hozzá a kódhoz? |code] -- [Kódolási szabvány |coding-standard] - -**Dokumentáció** - -- [Hogyan járulhat hozzá a dokumentációhoz? |documentation] -- [Dokumentációs szintaxis |syntax] -- "Előnézeti szerkesztő":https://editor.nette.org diff --git a/contributing/hu/@left-menu.texy b/contributing/hu/@left-menu.texy deleted file mode 100644 index 0775d11f06..0000000000 --- a/contributing/hu/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Kód -*** -- [Hogyan járulhat hozzá a kódhoz? |code] -- [Kódolási szabvány |coding-standard] - -Dokumentáció -************ -- [Hogyan járulhat hozzá a dokumentációhoz? |documentation] -- [Dokumentációs szintaxis |syntax] -- "Előnézeti szerkesztő":https://editor.nette.org diff --git a/contributing/hu/code.texy b/contributing/hu/code.texy deleted file mode 100644 index 97a520de2b..0000000000 --- a/contributing/hu/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Hogyan járuljunk hozzá a kódhoz -******************************* - -.[perex] -Készülsz hozzájárulni a Nette Frameworkhöz, és szükséged van eligazodásra a szabályokban és eljárásokban? Ez a kezdőknek szóló útmutató lépésről lépésre megmutatja, hogyan járulhatsz hozzá hatékonyan a kódhoz, hogyan dolgozz a repository-kkal és hogyan implementáld a változtatásokat. - - -Eljárás -======= - -A kódhoz való hozzájáruláshoz elengedhetetlen egy [GitHub |https://github.com] fiók és a Git verziókezelő rendszer alapjainak ismerete. Ha nem ismered a Git használatát, megnézheted a [git - the simple guide |https://rogerdudler.github.io/git-guide/] útmutatót, és esetleg használhatod a számos [grafikus kliens |https://git-scm.com/downloads/guis] egyikét. - - -Környezet és repository előkészítése ------------------------------------- - -1) a GitHubon hozz létre egy [forkot |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] annak a [csomagnak |www:packages] a repository-jából, amelyet módosítani készülsz -2) ezt a repository-t [klónozd |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] a számítógépedre -3) telepítsd a függőségeket, beleértve a [Nette Testert |tester:] is, a `composer install` paranccsal -4) ellenőrizd, hogy a tesztek működnek-e, a `composer tester` futtatásával -5) hozz létre egy [új ágat |#Új ág] az utolsó kiadott verzió alapján - - -Saját változtatások implementálása ----------------------------------- - -Most végrehajthatod a saját kódmódosításaidat: - -1) programozd le a kívánt változtatásokat, és ne feledkezz meg a tesztekről -2) győződj meg róla, hogy a tesztek sikeresen lefutnak, a `composer tester` segítségével -3) ellenőrizd, hogy a kód megfelel-e a [kódolási szabványnak |#Kódolási szabványok] -4) mentsd el a változtatásokat (commitold) egy leírással [ebben a formátumban |#Commit leírása] - -Létrehozhatsz több commitot, egyet minden logikai lépéshez. Minden commitnak önmagában értelmesnek kell lennie. - - -Változtatások elküldése ------------------------ - -Amint elégedett vagy a változtatásokkal, elküldheted őket: - -1) küldd el (pushold) a változtatásokat a GitHubra a saját forkodba -2) onnan küldd el őket a Nette repository-ba egy [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) létrehozásával -3) adj meg a leírásban [elegendő információt |#Pull request leírása] - - -Észrevételek beépítése ----------------------- - -A commitjaidat most már mások is látni fogják. Gyakori, hogy észrevételeket tartalmazó kommenteket kapsz: - -1) kövesd nyomon a javasolt módosításokat -2) építsd be őket új commitokként, vagy [olvaszd össze őket a korábbiakkal |https://help.github.com/en/github/using-git/about-git-rebase] -3) küldd el újra a commitokat a GitHubra, és automatikusan megjelennek a pull requestben - -Soha ne hozz létre új pull requestet egy meglévő módosítása miatt. - - -Dokumentáció ------------- - -Ha megváltoztattad a funkcionalitást vagy újat adtál hozzá, ne felejtsd el [hozzáadni a dokumentációhoz |documentation] is. - - -Új ág -===== - -Ha lehetséges, a változtatásokat az utolsó kiadott verzióhoz képest végezd, azaz az adott ág utolsó tagjéhez. A `v3.2.1` taghez ezzel a paranccsal hozhatsz létre ágat: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Kódolási szabványok -=================== - -A kódodnak meg kell felelnie a Nette Frameworkben használt [kódolási szabványnak |coding-standard]. A kód ellenőrzésére és javítására rendelkezésre áll egy automatikus eszköz. Telepíthető a Composer segítségével **globálisan** egy általad választott mappába: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Most már képesnek kell lenned futtatni az eszközt a terminálban. Az első parancs ellenőrzi, a második pedig javítja is a kódot az `src` és `tests` mappákban az aktuális könyvtárban: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Commit leírása -============== - -A Nette-ben a commit tárgyak formátuma: `Presenter: fixed AJAX detection [Closes #69]` - -- terület, amelyet kettőspont követ -- a commit célja múlt időben, ha lehetséges, kezdődjön a következő szavakkal: `added` (új funkció hozzáadva), `fixed` (javítás), `refactored` (kódváltozás viselkedésváltozás nélkül), `changed`, `removed` -- ha a commit megszakítja a visszamenőleges kompatibilitást, add hozzá a "BC break" jelzést -- esetleges kapcsolat az issue trackerrel, mint `(#123)` vagy `[Closes #69]` -- a tárgy után következhet egy üres sor, majd részletesebb leírás, beleértve például a fórumra mutató linkeket - - -Pull request leírása -==================== - -Pull request létrehozásakor a GitHub felülete lehetővé teszi egy név és leírás megadását. Adj meg egy kifejező nevet, és a leírásban adj meg minél több információt a változtatásod okairól. - -Megjelenik egy fejléc is, ahol meg kell adnod, hogy új funkcióról vagy hibajavításról van-e szó, és hogy okozhat-e visszamenőleges kompatibilitási törést (BC break). Ha van kapcsolódó probléma (issue), hivatkozz rá, hogy a pull request jóváhagyása után lezárásra kerüljön. - -``` -- bug fix / new feature? <!-- #issue számok, ha vannak --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- nagyon szívesen látjuk, lásd https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/hu/coding-standard.texy b/contributing/hu/coding-standard.texy deleted file mode 100644 index 48c1ec05cb..0000000000 --- a/contributing/hu/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Kódolási szabvány -***************** - -.[perex] -Ez a dokumentum leírja a Nette fejlesztésére vonatkozó szabályokat és ajánlásokat. Amikor kódot járulsz hozzá a Nette-hez, be kell tartanod őket. Ennek legegyszerűbb módja a meglévő kód utánzása. A lényeg az, hogy minden kód úgy nézzen ki, mintha egyetlen ember írta volna. - -A Nette Kódolási Szabvány megfelel a [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/]-nak, két fő kivétellel: a behúzáshoz [tabulátorokat használ szóközök helyett |#Tabulátorok szóközök helyett], és az "osztály konstansokhoz PascalCase-t használ":https://blog.nette.org/hu/for-less-screaming-in-the-code. - - -Általános szabályok -=================== - -- Minden PHP fájlnak tartalmaznia kell a `declare(strict_types=1)` deklarációt. -- Két üres sort használunk a metódusok elválasztására a jobb olvashatóság érdekében. -- A shut-up operátor használatának okát dokumentálni kell: `@mkdir($dir); // @ - a könyvtár létezhet`. -- Ha gyengén típusos összehasonlító operátort használunk (pl. `==`, `!=`, ...), a szándékot dokumentálni kell: `// == elfogadja a null-t` -- Egy `exceptions.php` fájlba több kivételt is írhatsz. -- Az interfészeknél nem adjuk meg a metódusok láthatóságát, mivel azok mindig public-ok. -- Minden property-nek, visszatérési értéknek és paraméternek meg kell adni a típusát. Ezzel szemben a final konstansoknál soha nem adjuk meg a típust, mert az nyilvánvaló. -- A stringek határolására aposztrófokat kell használni, kivéve, ha maga a literál tartalmaz aposztrófokat. - - -Elnevezési konvenciók -===================== - -- Ne használj rövidítéseket, hacsak a teljes név nem túl hosszú. -- Kétbetűs rövidítéseknél használj nagybetűket, hosszabb rövidítéseknél pascal/camel case-t. -- Az osztály nevéhez használj főnevet vagy szókapcsolatot. -- Az osztályneveknek nemcsak a specifikusságot (`Array`), hanem az általánosságot (`ArrayIterator`) is tartalmazniuk kell. Kivételt képeznek a PHP nyelvi attribútumok. -- "Az osztály konstansoknak és enumoknak PascalCaps-t kell használniuk":https://blog.nette.org/hu/for-less-screaming-in-the-code. -- "Az interfészeknek és absztrakt osztályoknak nem szabad előtagokat vagy utótagokat tartalmazniuk":https://blog.nette.org/hu/prefixes-and-suffixes-do-not-belong-in-interface-names, mint például `Abstract`, `Interface` vagy `I`. - - -Tördelés és zárójelek -===================== - -A Nette Kódolási Szabvány megfelel a PSR-12-nek (illetve a PER Coding Style-nak), néhány pontban kiegészíti vagy módosítja azt: - -- az arrow függvényeket szóköz nélkül írjuk a zárójel előtt, azaz `fn($a) => $b` -- nem szükséges üres sor a különböző típusú `use` import utasítások között -- a függvény/metódus visszatérési típusa és a nyitó kapcsos zárójel mindig külön sorokban vannak: - -```php - public function find( - string $dir, - array $options, - ): array - { - // metódus törzse - } -``` - -A nyitó kapcsos zárójel külön sorban fontos a függvény/metódus szignatúrájának és törzsének vizuális elválasztásához. Ha a szignatúra egy sorban van, az elválasztás egyértelmű (bal oldali kép), ha több sorban van, a PSR-ben a szignatúra és a törzs egybefolyik (középen), míg a Nette szabványban továbbra is elkülönülnek (jobbra): - -[* new-line-after.webp *] - - -Dokumentációs blokkok (phpDoc) -============================== - -Fő szabály: Soha ne duplikálj semmilyen információt a szignatúrában, mint például a paraméter típusa vagy a visszatérési típus, hozzáadott érték nélkül. - -Dokumentációs blokk egy osztály definíciójához: - -- Az osztály leírásával kezdődik. -- Üres sor következik. -- Az `@property` (vagy `@property-read`, `@property-write`) annotációk következnek, egymás után. Szintaxis: annotáció, szóköz, típus, szóköz, $név. -- Az `@method` annotációk következnek, egymás után. Szintaxis: annotáció, szóköz, visszatérési típus, szóköz, név(típus $param, ...). -- Az `@author` annotációt kihagyjuk. A szerzőiséget a forráskód története őrzi meg. -- Használhatók az `@internal` vagy `@deprecated` annotációk. - -```php -/** - * MIME üzenet rész. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Egy property dokumentációs blokkja, amely csak az `@var` annotációt tartalmazza, egysoros legyen: - -```php -/** @var string[] */ -private array $name; -``` - -Dokumentációs blokk egy metódus definíciójához: - -- Rövid metódusleírással kezdődik. -- Nincs üres sor. -- Az `@param` annotációk külön sorokban. -- Az `@return` annotáció. -- Az `@throws` annotációk, egymás után. -- Használhatók az `@internal` vagy `@deprecated` annotációk. - -Minden annotációt egy szóköz követ, kivéve az `@param`-ot, amelyet a jobb olvashatóság érdekében két szóköz követ. - -```php -/** - * Fájlt keres egy könyvtárban. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulátorok szóközök helyett -============================ - -A tabulátoroknak számos előnyük van a szóközökkel szemben: - -- a behúzás mérete a szerkesztőkben és a "weben":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size testreszabható -- nem erőltetik a kódra a felhasználó behúzásméret-preferenciáját, így a kód jobban hordozható -- egyetlen billentyűleütéssel írhatók (bárhol, nem csak azokban a szerkesztőkben, amelyek a tabulátorokat szóközökre cserélik) -- a behúzás a céljuk -- tiszteletben tartják a látássérült és vak kollégák igényeit - -A tabulátorok használatával projektjeinkben lehetővé tesszük a szélesség testreszabását, ami a legtöbb ember számára feleslegesnek tűnhet, de a látássérültek számára elengedhetetlen. - -A Braille-kijelzőket használó vak programozók számára minden szóköz egy Braille-cellát jelent. Tehát ha az alapértelmezett behúzás 4 szóköz, a 3. szintű behúzás 12 értékes Braille-cellát pazarol el még a kód kezdete előtt. Egy 40 cellás kijelzőn, amelyet a laptopoknál leggyakrabban használnak, ez a rendelkezésre álló cellák több mint negyede, amelyet információ nélkül pazarolnak el. - - -{{priority: -1}} diff --git a/contributing/hu/documentation.texy b/contributing/hu/documentation.texy deleted file mode 100644 index 7ca946186c..0000000000 --- a/contributing/hu/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Hogyan járuljunk hozzá a dokumentációhoz -**************************************** - -.[perex] -A dokumentációhoz való hozzájárulás az egyik leghasznosabb tevékenység, mivel segít másoknak megérteni a keretrendszert. - - -Hogyan írjunk? --------------- - -A dokumentáció elsősorban azoknak szól, akik most ismerkednek a témával. Ezért több fontos pontnak kell megfelelnie: - -- Kezdje az egyszerűtől és általánostól. Csak a végén térjen át a haladóbb témákra. -- Próbálja meg a lehető legjobban elmagyarázni a dolgot. Például próbálja meg először elmagyarázni a témát egy kollégának. -- Csak azokat az információkat közölje, amelyekre a felhasználónak valóban szüksége van az adott témához. -- Ellenőrizze, hogy az információi valóban igazak-e. Minden kódot teszteljen le. -- Legyen tömör - amit ír, rövidítse le a felére. Aztán nyugodtan még egyszer. -- Takarékoskodjon mindenféle kiemeléssel, a félkövér betűktől az olyan keretekig, mint a `.[note]`. -- A kódokban tartsa be a [Kódolási Szabványt |Coding Standard]. - -Sajátítsa el a [szintaxist |syntax] is. A cikk írása közbeni előnézethez használhatja az [előnézeti szerkesztőt |https://editor.nette.org/]. - - -Nyelvi változatok ------------------ - -Az elsődleges nyelv az angol, tehát a változtatásainak csehül és angolul is meg kell lenniük. Ha az angol nem az erőssége, használja a [DeepL Translator |https://www.deepl.com/translator]-t, és a többiek ellenőrzik a szövegét. - -A többi nyelvre történő fordítás automatikusan megtörténik a módosítás jóváhagyása és finomítása után. - - -Apróbb módosítások ------------------- - -A dokumentációhoz való hozzájáruláshoz elengedhetetlen egy [GitHub |https://github.com] fiók. - -A legegyszerűbb módja egy apróbb változtatás végrehajtásának a dokumentációban az, ha kihasználja az egyes oldalak végén található linkeket: - -- *Megjelenítés GitHubon* megnyitja az adott oldal forráskódját a GitHubon. Ezután elég megnyomni az `E` gombot, és elkezdheti a szerkesztést (szükséges bejelentkezni a GitHubra). -- *Előnézet megnyitása* megnyitja a szerkesztőt, ahol rögtön láthatja a végső vizuális megjelenést is. - -Mivel az [előnézeti szerkesztő |https://editor.nette.org/] nem tudja közvetlenül a GitHubra menteni a változtatásokat, a módosítások befejezése után a forrásszöveget a vágólapra kell másolni (a *Copy to clipboard* gombbal), majd beilleszteni a GitHub szerkesztőjébe. A szerkesztőmező alatt található az elküldési űrlap. Itt ne felejtse el röviden összefoglalni és elmagyarázni a módosítás okát. Az elküldés után létrejön egy úgynevezett pull request (PR), amelyet tovább lehet szerkeszteni. - - -Nagyobb módosítások -------------------- - -A GitHub felületének használata helyett célszerűbb tisztában lenni a Git verziókezelő rendszer alapjaival. Ha nem ismeri a Git használatát, megnézheti a [git - the simple guide |https://rogerdudler.github.io/git-guide/] útmutatót, és esetleg használhatja a számos [grafikus kliens |https://git-scm.com/downloads/guis] egyikét. - -A dokumentációt a következő módon szerkessze: - -1) a GitHubon hozzon létre egy [forkot |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] a [nette/docs |https://github.com/nette/docs] repository-ból -2) ezt a repository-t [klónozza |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] a számítógépére -3) ezután a [megfelelő ágban |#Dokumentáció struktúrája] végezze el a változtatásokat -4) ellenőrizze a felesleges szóközöket a szövegben a [Code-Checker |code-checker:] eszközzel -5) mentse el a változtatásokat (commitolja) -6) ha elégedett a változtatásokkal, küldje el (pusholja) őket a GitHubra a saját forkjába -7) onnan küldje el őket a `nette/docs` repository-ba egy [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) létrehozásával - -Gyakori, hogy észrevételeket tartalmazó kommenteket fog kapni. Kövesse nyomon a javasolt változtatásokat, és építse be őket. A javasolt változtatásokat adja hozzá új commitokként, és küldje el újra a GitHubra. Soha ne hozzon létre új pull requestet egy pull request módosítása miatt. - - -Dokumentáció struktúrája ------------------------- - -Az egész dokumentáció a GitHubon található a [nette/docs |https://github.com/nette/docs] repository-ban. Az aktuális verzió a master ágban van, a régebbi verziók olyan ágakban találhatók, mint a `doc-3.x`, `doc-2.x`. - -Minden ág tartalma fő mappákra oszlik, amelyek a dokumentáció egyes területeit képviselik. Például az `application/` megfelel a https://doc.nette.org/hu/application címnek, a `latte/` megfelel a https://latte.nette.org címnek stb. Minden ilyen mappa tartalmaz almappákat, amelyek a nyelvi változatokat (`hu`, `en`, ...) képviselik, és esetleg egy `files` almappát képekkel, amelyeket be lehet illeszteni a dokumentáció oldalaira. diff --git a/contributing/hu/syntax.texy b/contributing/hu/syntax.texy deleted file mode 100644 index 66b8b50260..0000000000 --- a/contributing/hu/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Dokumentációs szintaxis -*********************** - -A dokumentáció Markdown & [Texy szintaxist |https://texy.info/cs/syntax] használ néhány kiterjesztéssel. - - -Linkek -====== - -Belső linkekhez szögletes zárójelekben `[link |odkaz]` írásmódot használunk. Vagy függőleges vonallal elválasztott formában `[link szövege |link célja]`, vagy rövidítve `[link szövege]`, ha a cél megegyezik a szöveggel (kisbetűssé és kötőjelessé alakítás után): - -- `[Page name]` -> `<a href="/hu/page-name">Page name</a>` -- `[link szövege |Page name]` -> `<a href="/hu/page-name">link szövege</a>` - -Hivatkozhatunk más nyelvi változatra vagy más szekcióra. Szekció alatt Nette könyvtárat értünk (pl. `forms`, `latte`, stb.) vagy speciális szekciókat, mint `best-practices`, `quickstart` stb.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (ugyanaz a szekció, más nyelv) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/hu/page-name">Page name</a>` (más szekció, ugyanaz a nyelv) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (más szekció és más nyelv) - -A `#` segítségével egy adott címsorra is lehet célozni az oldalon. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (címsor az aktuális oldalon) -- `[Page name#Heading]` -> `<a href="/hu/page-name#toc-heading">Page name</a>` - -Link a szekció kezdőoldalára: (`@home` egy speciális kifejezés a szekció kezdőoldalára) - -- `[link szövege |@home]` -> `<a href="/hu/">link szövege</a>` -- `[link szövege |tracy:]` -> `<a href="//tracy.nette.org/hu/">link szövege</a>` - - -Linkek az API dokumentációba ----------------------------- - -Mindig csak ezzel az írásmóddal adjuk meg: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Teljesen minősített neveket csak az első említéskor használjunk. További hivatkozásokhoz használjunk egyszerűsített nevet: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Linkek a PHP dokumentációba ---------------------------- - -- `[php:substr]` -> [php:substr] - - -Forráskód -========= - -A kódblokk <code>```lang</code>-gal kezdődik és <code>```</code>-gal végződik. Támogatott nyelvek: `php`, `latte`, `neon`, `html`, `css`, `js` és `sql`. A behúzáshoz mindig tabulátorokat használjunk. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Megadhatja a fájlnevet is, mint <code>```php .{file: ArrayTest.php}</code>, és a kódblokk így fog megjelenni: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Címsorok -======== - -A legfelső címsort (azaz az oldal nevét) csillagokkal húzza alá. A szekciók elválasztásához használjon egyenlőségjeleket. A címsorokat egyenlőségjelekkel, majd kötőjelekkel húzza alá: - -``` -MVC Alkalmazások & presenterek -****************************** -... - - -Linkek létrehozása -================== -... - - -Linkek sablonokban ------------------- -... -``` - - -Keretek és stílusok -=================== - -A perexet a `.[perex]` osztállyal jelöljük. .[perex] - -A megjegyzést a `.[note]` osztállyal jelöljük. .[note] - -A tippet a `.[tip]` osztállyal jelöljük. .[tip] - -A figyelmeztetést a `.[caution]` osztállyal jelöljük. .[caution] - -Az erősebb figyelmeztetést a `.[warning]` osztállyal jelöljük. .[warning] - -Verziószám `.{data-version:2.4.10}` .{data-version:2.4.10} - -Az osztályokat a sor elé írja: - -``` -.[perex] -Ez a perex. -``` - -Kérjük, vegye figyelembe, hogy az olyan keretek, mint a `.[tip]`, "vonzzák" a szemet, ezért kiemelésre használják őket, nem pedig kevésbé fontos információkra. Ezért használatukkal maximálisan takarékoskodjon. - - -Tartalomjegyzék -=============== - -A tartalomjegyzék (linkek a jobb oldali menüben) automatikusan generálódik minden olyan oldalhoz, amelynek mérete meghaladja a 4000 bájtot, de ez az alapértelmezett viselkedés módosítható a [#Meta tagek] `{{toc}}` segítségével. A tartalomjegyzéket alkotó szöveg alapértelmezés szerint közvetlenül a címsorok szövegéből származik, de a `.{toc}` módosítóval lehetőség van más szöveg megjelenítésére a tartalomjegyzékben, ami különösen hosszabb címsorok esetén hasznos. - -``` - - -Hosszú és intelligens címsor .{toc: Tetszőleges más szöveg a tartalomjegyzékben} -================================================================================ -``` - - -Meta tagek -========== - -- saját oldalnév beállítása (a `<title>`-ben és a morzsamenüben) `{{title: Másik név}}` -- átirányítás `{{redirect: pla:cs}}` - lásd [#Linkek] -- az automatikus tartalomjegyzék (a linkeket tartalmazó doboz az egyes címsorokra) kényszerítése `{{toc}}` vagy letiltása `{{toc: no}}` - -{{priority: -1}} diff --git a/contributing/it/@home.texy b/contributing/it/@home.texy index 797897d6e6..fa01dea43f 100644 --- a/contributing/it/@home.texy +++ b/contributing/it/@home.texy @@ -2,16 +2,16 @@ Diventa un contributore di Nette ******************************** .[perex] -Scopri come puoi partecipare al nostro progetto open source. Impara le procedure per contribuire al codice sorgente e alla documentazione e diventa parte della comunità di sviluppatori che partecipano attivamente al miglioramento di Nette. +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** -- [Come contribuire al codice? |code] +- [Contribuire al codice |code] - [Standard di codifica |coding-standard] **Documentazione** -- [Come contribuire alla documentazione? |documentation] +- [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 index 3c77da0d39..9ab88a7be8 100644 --- a/contributing/it/@left-menu.texy +++ b/contributing/it/@left-menu.texy @@ -1,10 +1,18 @@ Codice ****** -- [Come contribuire al codice? |code] +- [Contribuire al codice |code] - [Standard di codifica |coding-standard] Documentazione ************** -- [Come contribuire alla documentazione? |documentation] +- [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 index 7a6e6363b1..cb1d18a645 100644 --- a/contributing/it/code.texy +++ b/contributing/it/code.texy @@ -1,117 +1,106 @@ -Come contribuire al codice -************************** +Contribuire al codice +********************* .[perex] -State pensando di contribuire a Nette Framework e avete bisogno di orientarvi tra le regole e le procedure? Questa guida per principianti vi mostrerà passo dopo passo come contribuire efficacemente al codice, lavorare con i repository e implementare le modifiche. +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] ed essere familiari con le basi del lavoro con il sistema di versionamento Git. Se non conoscete il lavoro con Git, potete consultare la guida [git - the simple guide |https://rogerdudler.github.io/git-guide/] ed eventualmente utilizzare uno dei tanti [client grafici |https://git-scm.com/downloads/guis]. +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 intendete 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, incluso [Nette Tester |tester:], tramite il comando `composer install` -4) verificate che i test funzionino eseguendo `composer tester` -5) create un [#nuovo ramo] basato sull'ultima versione rilasciata +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 -Implementazione delle proprie modifiche ---------------------------------------- +Realizzazione delle vostre modifiche +------------------------------------ -Ora potete apportare le vostre modifiche al codice: +Ora potete fare le vostre modifiche al codice: -1) programmate le modifiche richieste e non dimenticate i test -2) assicuratevi che i test vengano eseguiti con successo tramite `composer tester` -3) verificate che il codice soddisfi lo [#standard di codifica] -4) salvate (committate) le modifiche con una descrizione in [questo formato |#Descrizione del commit] +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 passaggio logico. Ogni commit dovrebbe avere senso da solo. +Potete creare più commit, uno per ogni passo logico. Ogni commit dovrebbe avere senso da solo. Invio delle modifiche --------------------- -Una volta soddisfatti delle modifiche, potete inviarle: +Quando siete soddisfatti delle modifiche, potete inviarle: -1) inviate (push) le modifiche su GitHub al vostro fork -2) da lì, inviatele al repository Nette creando una [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) fornite nella descrizione [informazioni sufficienti |#Descrizione della pull request] +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] -Integrazione dei commenti -------------------------- +Incorporare i feedback +---------------------- -I vostri commit ora saranno visibili anche agli altri. È comune ricevere commenti con suggerimenti: +I vostri commit sono ora visibili agli altri. È normale ricevere commenti con dei suggerimenti: -1) seguite le modifiche proposte -2) integrateli come nuovi commit o [uniteli ai precedenti |https://help.github.com/en/github/using-git/about-git-rebase] -3) inviate nuovamente i commit su GitHub e appariranno automaticamente nella pull request +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 modificare una esistente. +Non create mai una nuova pull request per modificarne una esistente. Documentazione -------------- -Se avete modificato la funzionalità o ne avete aggiunta una nuova, non dimenticate di [aggiungerla anche alla documentazione |documentation]. +Se avete cambiato una funzionalità o ne avete aggiunta una nuova, non dimenticate di [aggiungerla anche alla documentazione|documentation]. Nuovo ramo ========== -Se possibile, apportate le modifiche rispetto all'ultima versione rilasciata, ovvero l'ultimo tag nel ramo corrispondente. Per il tag `v3.2.1`, create un ramo con questo comando: +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 new_branch_name v3.2.1 +git checkout -b nome_nuovo_ramo v3.2.1 ``` Standard di codifica ==================== -Il vostro codice deve soddisfare lo [standard di codifica |coding standard] utilizzato in Nette Framework. Per controllare e correggere il codice è disponibile uno strumento automatico. Può essere installato tramite Composer **globalmente** nella cartella da voi scelta: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Ora dovreste essere in grado di eseguire lo strumento nel terminale. Con il primo comando controllerete e con il secondo correggerete anche il codice nelle cartelle `src` e `tests` nella directory corrente: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` +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, gli oggetti dei commit hanno il formato: `Presenter: fixed AJAX detection [Closes #69]` +In Nette il soggetto dei commit ha questo formato: `Presenter: fixed AJAX detection [Closes #69]` -- area seguita da due punti -- scopo del commit al passato, se possibile, iniziate con la parola: `added` (nuova funzionalità aggiunta), `fixed` (correzione), `refactored` (modifica del codice senza modifica del comportamento), `changed`, `removed` -- se il commit interrompe la compatibilità all'indietro, aggiungete "BC break" -- eventuale collegamento all'issue tracker come `(#123)` o `[Closes #69]` -- dopo l'oggetto può seguire una riga vuota e poi una descrizione più dettagliata, inclusi ad esempio link al forum +- 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 ============================== -Durante la creazione di una pull request, l'interfaccia di GitHub vi consentirà di inserire un titolo e una descrizione. Fornite un titolo conciso e nella descrizione fornite quante più informazioni possibili sui motivi della vostra modifica. +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. -Verrà visualizzata anche un'intestazione in cui specificare se si tratta di una nuova funzionalità o di una correzione di bug e se può verificarsi un'interruzione della compatibilità all'indietro (BC break). Se è disponibile un problema correlato (issue), fatevi riferimento in modo che venga chiuso dopo l'approvazione della pull request. +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. ``` -- correzione bug / nuova funzionalità? <!-- #numeri issue, se presenti --> -- BC break? sì/no -- doc PR: nette/docs#? <!-- molto gradito, vedi https://nette.org/en/writing --> +- bug fix / new feature? <!-- #issue numbers, if any --> +- BC break? yes/no +- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> ``` diff --git a/contributing/it/coding-standard.texy b/contributing/it/coding-standard.texy index ad904f5bb0..dc2a4592bc 100644 --- a/contributing/it/coding-standard.texy +++ b/contributing/it/coding-standard.texy @@ -2,43 +2,46 @@ Standard di codifica ******************** .[perex] -Questo documento descrive le regole e le raccomandazioni per lo sviluppo di Nette. Quando contribuite con codice a Nette, dovete seguirle. Il modo più semplice per farlo è imitare il codice esistente. L'obiettivo è che tutto il codice sembri scritto da una sola persona. +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. -Lo Standard di Codifica Nette corrisponde a [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] con due eccezioni principali: utilizza [#tabulazioni invece di spazi] per l'indentazione e [PascalCase per le costanti di classe|https://blog.nette.org/it/for-less-screaming-in-the-code]. +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)` -- Due righe vuote vengono utilizzate per separare i metodi per una migliore leggibilità. -- Il motivo dell'uso dell'operatore shut-up deve essere documentato: `@mkdir($dir); // @ - la directory potrebbe esistere`. -- Se viene utilizzato un operatore di confronto debolmente tipizzato (cioè `==`, `!=`, ...), l'intenzione deve essere documentata: `// == accetta null` -- In un unico file `exceptions.php` è possibile scrivere più eccezioni. -- Per le interfacce non viene specificata la visibilità dei metodi, poiché sono sempre pubblici. -- Ogni proprietà, valore di ritorno e parametro deve avere un tipo specificato. Al contrario, per le costanti finali non specifichiamo mai il tipo, poiché è ovvio. -- Per delimitare una stringa dovrebbero essere usate le virgolette singole, ad eccezione dei casi in cui il letterale stesso contiene apostrofi. +- 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 ============================ -- Non utilizzate abbreviazioni, a meno che il nome completo non sia troppo lungo. -- Per le abbreviazioni di due lettere utilizzate lettere maiuscole, per le abbreviazioni più lunghe pascal/camel case. -- Per il nome di una classe utilizzate un sostantivo o una frase nominale. -- I nomi delle classi devono contenere non solo la specificità (`Array`), ma anche la generalità (`ArrayIterator`). Fanno eccezione gli attributi del linguaggio PHP. -- "Le costanti di classe e gli enum dovrebbero usare PascalCaps":https://blog.nette.org/it/for-less-screaming-in-the-code. -- "Le interfacce e le classi astratte non dovrebbero contenere prefissi o suffissi":https://blog.nette.org/it/prefixes-and-suffixes-do-not-belong-in-interface-names come `Abstract`, `Interface` o `I`. +- 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 graffe -========================= +A capo e parentesi +================== -Lo Standard di Codifica Nette corrisponde a PSR-12 (risp. PER Coding Style), in alcuni punti lo completa o lo modifica: +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` -- non è richiesta una riga vuota tra diversi tipi di `use` import statements -- il tipo di ritorno della funzione/metodo e la parentesi graffa di apertura sono sempre su righe separate: +- 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( @@ -50,7 +53,7 @@ Lo Standard di Codifica Nette corrisponde a PSR-12 (risp. PER Coding Style), in } ``` -La parentesi graffa di apertura su una riga separata è importante per la separazione visiva della firma della funzione/metodo dal corpo. Se la firma è su una riga, la separazione è chiara (immagine a sinistra), se è su più righe, in PSR la firma e il corpo si fondono (al centro), mentre nello standard Nette rimangono separati (a destra): +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 *] @@ -58,20 +61,20 @@ La parentesi graffa di apertura su una riga separata è importante per la separa Blocchi di documentazione (phpDoc) ================================== -Regola principale: Non duplicare mai alcuna informazione nella firma, come il tipo di parametro o il tipo di ritorno, senza un valore aggiunto. +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` (o `@property-read`, `@property-write`), una dopo l'altra. La sintassi è: annotazione, spazio, tipo, spazio, $nome. -- Seguono le annotazioni `@method`, una dopo l'altra. La sintassi è: annotazione, spazio, tipo di ritorno, spazio, nome(tipo $param, ...). -- L'annotazione `@author` viene omessa. L'autorialità viene conservata nella cronologia del codice sorgente. -- Possono essere utilizzate le annotazioni `@internal` o `@deprecated`. +- 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 /** - * Parte del messaggio MIME. + * MIME message part. * * @property string $encoding * @property-read array $headers @@ -80,7 +83,7 @@ Blocco di documentazione per la definizione di una classe: */ ``` -Un blocco di documentazione per una proprietà, che contiene solo l'annotazione `@var`, dovrebbe essere su una sola riga: +Un blocco di documentazione di una proprietà che contiene solo l'annotazione `@var` dovrebbe stare su una sola riga: ```php /** @var string[] */ @@ -89,18 +92,18 @@ private array $name; Blocco di documentazione per la definizione di un metodo: -- Inizia con una breve descrizione del metodo. -- Nessuna riga vuota. -- Annotazioni `@param` su righe separate. -- Annotazione `@return`. -- Annotazioni `@throws`, una dopo l'altra. -- Possono essere utilizzate le annotazioni `@internal` o `@deprecated`. +- 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 segue uno spazio, ad eccezione di `@param`, dopo la quale seguono due spazi per una migliore leggibilità. +Dopo ogni annotazione c'è uno spazio, tranne dopo `@param`, dopo la quale ce ne sono due per una migliore leggibilità. ```php /** - * Trova un file nella directory. + * Finds a file in directory. * @param string[] $options * @return string[] * @throws DirectoryNotFoundException @@ -109,20 +112,37 @@ 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 può essere personalizzata 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, quindi il codice è più portabile -- possono essere scritte con un solo tasto (ovunque, non solo negli editor che trasformano le tabulazioni in spazi) -- l'indentazione è il loro scopo -- rispettano le esigenze dei colleghi ipovedenti e non vedenti +- 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 -Utilizzando le tabulazioni nei nostri progetti, consentiamo la personalizzazione della larghezza, che può sembrare superflua alla maggior parte delle persone, ma è essenziale per le persone con disabilità visive. +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 utilizzano display Braille, ogni spazio rappresenta una cella Braille. Quindi, se l'indentazione predefinita è di 4 spazi, l'indentazione di 3° livello spreca 12 preziose celle Braille prima ancora che inizi il codice. Su un display a 40 celle, che è il più comune per i notebook, questo rappresenta più di un quarto delle celle disponibili sprecate senza alcuna informazione. +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 index 93d0597b18..3d8bc2dbb6 100644 --- a/contributing/it/documentation.texy +++ b/contributing/it/documentation.texy @@ -1,68 +1,68 @@ -Come contribuire alla documentazione -************************************ +Contribuire alla documentazione +******************************* .[perex] -Contribuire alla documentazione è una delle attività più gratificanti, poiché aiutate gli altri a comprendere il framework. +Contribuire alla documentazione è una delle attività più preziose, perché aiuta gli altri a capire il framework. Come scrivere? -------------- -La documentazione è destinata principalmente alle persone che si avvicinano all'argomento. Pertanto, dovrebbe soddisfare diversi punti importanti: +La documentazione è pensata anzitutto per chi si avvicina all'argomento per la prima volta. Dovrebbe quindi soddisfare alcuni punti importanti: -- Iniziate dal semplice e generale. Passate ad argomenti più avanzati solo alla fine. -- Cercate di spiegare la cosa nel miglior modo possibile. Provate, ad esempio, a spiegare prima l'argomento a un collega. -- Fornite solo le informazioni di cui l'utente ha effettivamente bisogno per l'argomento specifico. -- Verificate che le vostre informazioni siano effettivamente vere. Testate ogni codice. -- Siate concisi - dimezzate ciò che scrivete. E poi, se necessario, ancora una volta. -- Risparmiate sugli evidenziatori di ogni tipo, dal grassetto alle cornici come `.[note]`. -- Nel codice, rispettate lo [Standard di codifica |Coding Standard]. +- 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 visualizzare l'anteprima dell'articolo durante la scrittura, potete utilizzare l'[editor con anteprima |https://editor.nette.org/]. +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 le vostre modifiche dovrebbero essere sia in ceco che in inglese. Se l'inglese non è il vostro forte, utilizzate [DeepL Translator |https://www.deepl.com/translator] e gli altri controlleranno il testo per voi. +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 verrà eseguita automaticamente dopo l'approvazione e la messa a punto della vostra modifica. +La traduzione nelle altre lingue avverrà automaticamente dopo che la vostra modifica sarà approvata e finalizzata. -Modifiche triviali ------------------- +Modifiche minime +---------------- -Per contribuire alla documentazione è indispensabile avere un account su [GitHub|https://github.com]. +Per contribuire alla documentazione dovete avere un account su [GitHub |https://github.com]. -Il modo più semplice per apportare una piccola modifica alla documentazione è utilizzare i link alla fine di ogni pagina: +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 data su GitHub. Successivamente, è sufficiente premere il pulsante `E` e potete iniziare a modificare (è necessario essere loggati su GitHub). -- *Apri anteprima* apre l'editor, dove vedete subito anche l'aspetto visivo risultante. +- *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 con anteprima |https://editor.nette.org/] non ha la possibilità di salvare le modifiche direttamente su GitHub, è necessario, dopo aver completato le modifiche, copiare il testo sorgente negli appunti (con il pulsante *Copy to clipboard*) e quindi incollarlo nell'editor su GitHub. Sotto il campo di modifica c'è un modulo per l'invio. Qui non dimenticate di riassumere brevemente e spiegare il motivo della vostra modifica. Dopo l'invio, verrà creata una cosiddetta pull request (PR), che potrà essere ulteriormente modificata. +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ù grandi --------------------- +Modifiche più ampie +------------------- -Più appropriato che utilizzare l'interfaccia di GitHub, è essere familiari con le basi del lavoro con il sistema di versionamento Git. Se non conoscete il lavoro con Git, potete consultare la guida [git - the simple guide |https://rogerdudler.github.io/git-guide/] ed eventualmente utilizzare uno dei tanti [client grafici |https://git-scm.com/downloads/guis]. +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 in questo modo: +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) successivamente, nel [ramo appropriato |#Struttura della documentazione], apportate le modifiche -4) controllate gli spazi superflui nel testo tramite lo strumento [Code-Checker |code-checker:] -4) salvate (committate) le modifiche -6) se siete soddisfatti delle modifiche, inviatele (push) su GitHub al vostro fork -7) da lì, inviatele al repository `nette/docs` creando una [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) +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). -È comune ricevere commenti con suggerimenti. Seguite le modifiche proposte e integrateli. Aggiungete le modifiche proposte come nuovi commit e inviatele nuovamente su GitHub. Non create mai una nuova pull request per modificare una pull request esistente. +È 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 ------------------------------ -L'intera documentazione si trova su GitHub nel repository [nette/docs |https://github.com/nette/docs]. La versione attuale è nel master, le versioni precedenti si trovano in rami come `doc-3.x`, `doc-2.x`. +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. Ad esempio, `application/` corrisponde a https://doc.nette.org/cs/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 immagini che possono essere inserite nelle pagine della documentazione. +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 index ef704f10e4..05a5f68fa2 100644 --- a/contributing/it/syntax.texy +++ b/contributing/it/syntax.texy @@ -1,51 +1,51 @@ Sintassi della documentazione ***************************** -La documentazione utilizza Markdown e la [sintassi Texy |https://texy.info/cs/syntax] con alcune estensioni. +La documentazione usa Markdown e la [sintassi di Texy |https://texy.nette.org/syntax] con alcune aggiunte. Link ==== -Per i link interni si utilizza la notazione tra parentesi quadre `[link]`. E questo o nella forma con la barra verticale `[testo del link |destinazione del link]`, o abbreviata `[testo del link]`, se la destinazione è identica al testo (dopo la trasformazione in minuscolo e trattini): +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): -- `[Page name |Page name]` -> `<a href="/it/page-name">Page name</a>` -- `[testo del link |Page name]` -> `<a href="/it/page-name">testo del link</a>` +- `[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 creare link a un'altra versione linguistica o a un'altra sezione. Per sezione si intende una libreria Nette (ad es. `forms`, `latte`, ecc.) o sezioni speciali come `best-practices`, `quickstart` ecc.: +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:Page name |cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (stessa sezione, lingua diversa) -- `[tracy:Page name |tracy:Page name]` -> `<a href="//tracy.nette.org/it/page-name">Page name</a>` (sezione diversa, stessa lingua) -- `[tracy:cs:Page name |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (sezione e lingua diverse) +- `[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) -Tramite `#` è anche possibile puntare a un titolo specifico sulla pagina. +Con `#` si può puntare anche a un'intestazione specifica della pagina. -- `[Titolo |#Heading]` -> `<a href="#toc-heading">Titolo</a>` (titolo sulla pagina corrente) -- `[Page name#Heading |Page name#Heading]` -> `<a href="/it/page-name#toc-heading">Page name</a>` +- `[#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 iniziale della sezione: (`@home` è un'espressione speciale per la home page della sezione) +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 API ----------------------------- +Link alla documentazione dell'API +--------------------------------- -Indicare sempre solo utilizzando questa notazione: +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] -Utilizzate i nomi completamente qualificati solo alla prima menzione. Per i link successivi, utilizzate il nome semplificato: +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 PHP ----------------------------- +Link alla documentazione di PHP +------------------------------- - `[php:substr]` -> [php:substr] @@ -53,7 +53,7 @@ Link alla documentazione PHP Codice sorgente =============== -Un blocco di codice inizia con <code>```lang</code> e termina con <code>```</code>. Le lingue supportate sono `php`, `latte`, `neon`, `html`, `css`, `js` e `sql`. Per l'indentazione utilizzate sempre le tabulazioni. +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 @@ -63,7 +63,7 @@ Un blocco di codice inizia con <code>```lang</code> e termina con <c ``` ``` -Potete anche specificare il nome del file come <code>```php .{file: ArrayTest.php}</code> e il blocco di codice verrà renderizzato in questo modo: +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) @@ -72,71 +72,72 @@ public function renderPage($id) ``` -Titoli -====== +Intestazioni +============ -Il titolo più alto (cioè il nome della pagina) sottolineatelo con asterischi. Per separare le sezioni utilizzate gli uguali. Sottolineate i titoli con gli uguali e poi con i trattini: +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 (`-`): ``` -Applicazioni MVC e presenter -**************************** +MVC Applications & Presenters +***************************** ... -Creazione di link -================= +Link Creation +============= ... -Link nei template ------------------ +Links in Templates +------------------ ... ``` -Cornici e stili -=============== +Riquadri e stili +================ -Il perex lo contrassegniamo con la classe `.[perex]` .[perex] +Perex contrassegnato con la classe `.[perex]` .[perex] -Una nota la contrassegniamo con la classe `.[note]` .[note] +Nota contrassegnata con la classe `.[note]` .[note] -Un suggerimento lo contrassegniamo con la classe `.[tip]` .[tip] +Suggerimento contrassegnato con la classe `.[tip]` .[tip] -Un avvertimento lo contrassegniamo con la classe `.[caution]` .[caution] +Avvertenza contrassegnata con la classe `.[caution]` .[caution] -Un avvertimento più forte lo contrassegniamo con la classe `.[warning]` .[warning] +Avviso forte contrassegnato con la classe `.[warning]` .[warning] Numero di versione `.{data-version:2.4.10}` .{data-version:2.4.10} -Scrivete le classi prima della riga: +Le classi si scrivono prima della riga a cui si riferiscono: ``` .[perex] Questo è il perex. ``` -Si prega di notare che le cornici come `.[tip]` "attirano" gli occhi, quindi vengono utilizzate per enfatizzare, non per informazioni meno importanti. Pertanto, utilizzateli con la massima parsimonia. +Tenete presente che i riquadri come `.[tip]` attirano l'attenzione e andrebbero quindi usati per evidenziare informazioni importanti, non dettagli secondari. Usateli con parsimonia. -Contenuto -========= +Indice +====== -Il contenuto (link nel menu a destra) viene generato automaticamente per tutte le pagine la cui dimensione supera i 4.000 byte, tuttavia questo comportamento predefinito può essere modificato tramite il [#meta tag] `{{toc}}`. Il testo che forma il contenuto viene preso standard direttamente dal testo dei titoli, ma tramite il modificatore `.{toc}` è possibile visualizzare nel contenuto un testo diverso, il che è utile soprattutto per i titoli più lunghi. +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. ``` -Titolo lungo e intelligente .{toc: Qualsiasi altro testo visualizzato nel contenuto} -==================================================================================== +Intestazione lunga e intelligente .{toc: Un testo diverso per l'indice} +======================================================================= ``` Meta tag ======== -- impostazione di un nome di pagina personalizzato (in `<title>` e nella navigazione breadcrumb) `{{title: Altro nome}}` -- reindirizzamento `{{redirect: pla:cs}}` - [vedi #link |#Link] -- forzatura `{{toc}}` o disabilitazione `{{toc: no}}` del contenuto automatico (riquadro con link ai singoli titoli) +- 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 index 9be5c11a00..cb9d9cb30b 100644 --- a/contributing/ja/@home.texy +++ b/contributing/ja/@home.texy @@ -1,17 +1,17 @@ -Netteの貢献者になる +Nette に貢献しよう ************ .[perex] -私たちのオープンソースプロジェクトにどのように参加できるかをご覧ください。ソースコードとドキュメントへの貢献の手順を学び、Netteの改善に積極的に参加している開発者コミュニティの一員になりましょう。 +私たちのオープンソースのプロジェクトにどう参加できるかをご紹介します。ソースコードとドキュメントに貢献する手順を学び、Nette を良くしていく開発者の集まりの一員になってください。 **コード** -- [コードに貢献する方法は? |code] -- [コーディング標準 |coding-standard] +- [コードへの貢献 |code] +- [コーディング規約 |coding-standard] **ドキュメント** -- [ドキュメントに貢献する方法は? |documentation] -- [ドキュメントの構文 |syntax] +- [ドキュメントへの貢献 |documentation] +- [ドキュメントの記法 |syntax] - "プレビューエディタ":https://editor.nette.org diff --git a/contributing/ja/@left-menu.texy b/contributing/ja/@left-menu.texy index cebe170ec6..92c36a8576 100644 --- a/contributing/ja/@left-menu.texy +++ b/contributing/ja/@left-menu.texy @@ -1,10 +1,18 @@ コード *** -- [コードに貢献する方法は? |code] -- [コーディング標準 |coding-standard] +- [コードへの貢献 |code] +- [コーディング規約 |coding-standard] ドキュメント ****** -- [ドキュメントに貢献する方法は? |documentation] -- [ドキュメントの構文 |syntax] +- [ドキュメントへの貢献 |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 index b068678c3d..d0bbc02a5e 100644 --- a/contributing/ja/code.texy +++ b/contributing/ja/code.texy @@ -1,71 +1,71 @@ -コードへの貢献方法 -********* +コードへの貢献 +******* .[perex] -Netteフレームワークに貢献しようとしていて、ルールや手順を理解する必要がありますか?この初心者向けガイドでは、コードに効果的に貢献し、リポジトリで作業し、変更を実装する方法をステップバイステップで示します。 +Nette Framework に貢献しようと考えていて、その決まりと手順を知りたいですか。この初めての方への案内が、コードを効果的に貢献し、リポジトリを扱い、変更を実装するまでの手順を導きます。 手順 -====== +=== -コードに貢献するには、[GitHub|https://github.com] アカウントを持ち、Gitバージョン管理システムの基本に精通している必要があります。Gitの操作に慣れていない場合は、[git - the simple guide |https://rogerdudler.github.io/git-guide/] ガイドを参照したり、多くの [グラフィカルクライアント |https://git-scm.com/downloads/guis] のいずれかを利用したりできます。 +コードを貢献するには、[GitHub|https://github.com]のアカウントを持ち、Git のバージョン管理システムの基本を身につけていることが欠かせません。Git に馴染みがないなら、[git - the simple guide|https://rogerdudler.github.io/git-guide/]を見て、たくさんある[図の操作のクライアント|https://git-scm.com/downloads/guis]のどれかを使うことを考えてみてください。 -環境とリポジトリの準備 ------------ +環境とリポジトリを整える +------------ -1) GitHubで、編集する [パッケージ |www:packages] のリポジトリの [フォーク |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] を作成します -2) このリポジトリを自分のコンピュータに [クローン |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] します -3) `composer install` コマンドを使用して、[Nette Tester |tester:] を含む依存関係をインストールします -4) `composer tester` を実行してテストが機能することを確認します -5) 最新のリリースバージョンに基づいて [#新しいブランチ] を作成します +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) 望む変更を実装します。テストも忘れずに +2) `composer tester` でテストがうまく通ることを確かめます +3) コードが[コーディング規約 |#コーディング規約]に合っているかを確かめます +4) [この形式 |#コミットの説明]の説明とともに変更を保存(コミット)します -各論理ステップごとに1つのコミットを作成できます。各コミットは単独で意味があるべきです。 +コミットは複数作れます。論理的な一歩ごとにひとつです。それぞれのコミットは、それだけで意味の通るものにすべきです。 -変更の送信 +変更を送る ----- -変更に満足したら、送信できます: +変更に満足したら、それを送れます。 -1) 変更をGitHubの自分のフォークに送信(プッシュ)します -2) そこから、[プルリクエスト |https://help.github.com/articles/creating-a-pull-request] (PR) を作成してNetteリポジトリに送信します -3) 説明に [十分な情報 |#プルリクエストの説明] を記載します +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に送信すると、自動的にプルリクエストに表示されます +1) 提案された変更を追いかけます +2) それを新しいコミットとして取り入れるか、[前のコミットと合わせます|https://help.github.com/en/github/using-git/about-git-rebase] +3) コミットを GitHub へ送り直すと、pull request に自動的に現れます -既存のプルリクエストを修正するために新しいプルリクエストを作成しないでください。 +既存の pull request を直すために、新しい pull request を作っては決していけません。 ドキュメント ------ -機能性を変更したり、新しい機能を追加したりした場合は、それを [ドキュメントに追加 |documentation] することも忘れないでください。 +機能を変えたり新しく足したりしたなら、[ドキュメントに足す|documentation]のも忘れないでください。 新しいブランチ ======= -可能であれば、最新のリリースバージョン、つまり特定のブランチの最新のタグに対して変更を行ってください。タグ `v3.2.1` の場合、次のコマンドでブランチを作成します: +できるなら、最新のリリース版、つまりブランチの最後のタグに対して変更を加えてください。タグ `v3.2.1` なら、次のコマンドでブランチを作ります。 ```shell git checkout -b new_branch_name v3.2.1 @@ -75,43 +75,32 @@ git checkout -b new_branch_name v3.2.1 コーディング規約 ======== -あなたのコードは、Netteフレームワークで使用されている [コーディング規約 |coding standard] に準拠する必要があります。コードのチェックと修正には自動ツールが利用可能です。Composerを介して、選択したフォルダに **グローバルに** インストールできます: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -これで、ターミナルでツールを実行できるようになるはずです。最初のコマンドでチェックし、2番目のコマンドで現在のディレクトリの `src` および `tests` フォルダ内のコードを修正します: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` +あなたのコードは、Nette Framework で使われている[コーディング規約|coding-standard]に合っていなければなりません。コードを確かめて自動的に直すには、[Nette Coding Standard |tools:coding-standard]の道具を使ってください。そこにはインストールと使い方の説明もあります。 コミットの説明 ======= -Netteでは、コミットの件名は次の形式です:`Presenter: fixed AJAX detection [Closes #69]` +Nette では、コミットの主題は次の形式です。`Presenter: fixed AJAX detection [Closes #69]` -- コロンが続く領域 -- 可能であれば過去形のコミットの目的、可能であれば次の単語で始めます:"added .(新しい機能の追加)", "fixed .(修正)", "refactored .(動作を変更しないコードの変更)", changed, removed -- コミットが後方互換性を壊す場合は、"BC break" を追加します -- `(#123)` や `[Closes #69]` のような課題トラッカーへの可能な関連付け -- 件名の後には、1行の空行が続き、その後、フォーラムへのリンクなどを含む詳細な説明が続くことがあります +- 領域とそれに続くコロン +- コミットの目的を過去形で。できれば「added(新機能)」「fixed(修正)」「refactored(振る舞いを変えないコードの変更)」「changed」「removed」のような語で始めます +- そのコミットが後方互換を壊すなら「BC break」を足します +- 課題の管理へのリンクがあれば、`(#123)` や `[Closes #69]` のように書きます +- 主題のあとには空行をひとつ置いて、フォーラムへのリンクなどを含むより詳しい説明を書けます -プルリクエストの説明 -========== +Pull request の説明 +================ -プルリクエストを作成する際、GitHubインターフェースではタイトルと説明を入力できます。わかりやすいタイトルを付け、説明には変更の理由についてできるだけ多くの情報を提供してください。 +pull request を作るとき、GitHub の画面で題名と説明を入れられます。題名は簡潔にして、説明にはその変更の理由についてできるだけ多くの情報を書いてください。 -また、ヘッダーも表示され、それが新機能なのかバグ修正なのか、後方互換性の破壊(BC break)が発生する可能性があるかどうかを指定します。関連する問題(issue)がある場合は、プルリクエストが承認された後に閉じられるようにリンクしてください。 +また、それが新機能か不具合の修正か、そして後方互換の問題(BC break)を起こしうるかを見出しに書いてください。関連する課題があればリンクしてください。そうすれば pull request が受け入れられたときにそれが閉じられます。 ``` -- bug fix / new feature? <!-- #issue番号、もしあれば --> +- bug fix / new feature? <!-- #issue numbers, if any --> - BC break? yes/no -- doc PR: nette/docs#? <!-- 大歓迎、https://nette.org/en/writing を参照 --> +- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> ``` diff --git a/contributing/ja/coding-standard.texy b/contributing/ja/coding-standard.texy index ab7a4b103e..0b6c7f5a3a 100644 --- a/contributing/ja/coding-standard.texy +++ b/contributing/ja/coding-standard.texy @@ -2,43 +2,46 @@ ******** .[perex] -このドキュメントでは、Nette開発のためのルールと推奨事項について説明します。Netteにコードを貢献する際には、これらを遵守する必要があります。最も簡単な方法は、既存のコードを模倣することです。 目標は、すべてのコードが一人の人間によって書かれたかのように見えるようにすることです。 +この文書は 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] します。 +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); // @ - ディレクトリは存在する可能性があります`。 -- 弱い型付けの比較演算子(例:`==`, `!=`, ...)を使用する場合、意図を文書化する必要があります:`// == nullを受け入れる` -- 複数の例外を1つの `exceptions.php` ファイルに記述できます。 -- インターフェースではメソッドの可視性を指定しません。常にpublicだからです。 -- 各プロパティ、戻り値、パラメータには型を指定する必要があります。逆に、final定数には型を記述しません。明らかだからです。 -- 文字列リテラル自体にアポストロフィが含まれていない限り、文字列を囲むには単一引用符を使用する必要があります。 +全般の決まり +====== +- すべての PHP のファイルには `declare(strict_types=1)` を入れます +- 読みやすさのために、メソッドのあいだは空行 2 つで区切ります +- 黙らせる演算子(`@`)を使う理由は書き残さなければなりません。`@mkdir($dir); // @ - directory may exist` +- 緩い型の比較の演算子(つまり `==`、`!=` など)を使うなら、その意図を書き残さなければなりません。`// == to accept null` +- 例外のクラスは `exceptions.php` という名前のひとつのファイルに複数書けますし、enum は `enums.php` に複数書けます +- インターフェースではメソッドの可視性を書きません。いつも public だからです +- すべてのプロパティ、戻り値、パラメータには型を指定しなければなりません。逆に final の定数には型を決して書きません。自明だからです +- 文字列は単一引用符で囲むべきです。ただしその文字列自身がアポストロフィを含む場合は別です -命名規則 -==== -- 全体が長すぎない限り、略語を使用しないでください。 -- 2文字の略語には大文字を使用し、長い略語にはpascal/camelケースを使用します。 -- クラス名には名詞または名詞句を使用します。 -- クラス名には、具体性(`Array`)だけでなく、一般性(`ArrayIterator`)も含める必要があります。PHP言語属性は例外です。 -- "クラス定数とenumはPascalCapsを使用する必要があります":https://blog.nette.org/en/for-less-screaming-in-the-code。 -- "インターフェースと抽象クラスには、`Abstract`、`Interface`、`I`のような接頭辞や接尾辞を含めるべきではありません":https://blog.nette.org/en/prefixes-and-suffixes-do-not-belong-in-interface-names。 +名前の付け方 +====== +- 完全な名前が長すぎる場合を除き、省略形は避けます +- 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)に準拠しており、いくつかの点で補足または変更されています: +改行と波かっこ +======= + +Nette のコーディング規約は PSR-12(または PER Coding Style)に沿っていますが、いくつかの点でそれを細かく定めたり変えたりしています。 -- アロー関数は括弧の前にスペースを入れずに記述します。つまり `fn($a) => $b` -- 異なるタイプの `use` インポートステートメントの間に空行は必要ありません -- 関数/メソッドの戻り値の型と開始波括弧は常に別々の行に記述します: +- アロー関数はかっこの前に空白を入れずに書きます。つまり `fn($a) => $b` です +- 種類の違う `use` の import の文のあいだに空行は要りません +- 関数やメソッドの戻り値の型と、開く波かっこは、いつも別々の行に置きます。 ```php public function find( @@ -46,32 +49,32 @@ Netteコーディング規約はPSR-12(またはPER Coding Style)に準拠 array $options, ): array { - // メソッド本体 + // method body } ``` -開始波括弧を別々の行に置くことは、関数/メソッドのシグネチャと本体を視覚的に区別するために重要です。シグネチャが1行の場合、区別は明確です(左の画像)。複数行の場合、PSRではシグネチャと本体が混ざり合いますが(中央)、Nette標準では引き続き区別されます(右): +開く波かっこを別の行に置くのは、関数やメソッドの見出しを本体から目で分けるために大事です。見出しが 1 行なら区切りははっきりしています(左の図)。複数行なら、PSR では見出しと本体が溶け合ってしまいますが(真ん中)、Nette の規約では分かれたままです(右)。 [* new-line-after.webp *] -ドキュメンテーションブロック (phpDoc) -======================= +文書のかたまり(phpDoc) +=============== -主なルール:付加価値なしに、パラメータの型や戻り値の型など、シグネチャ内の情報を決して複製しないでください。 +いちばんの決まりはこれです。**価値を足さずに**、パラメータの型や戻り値の型のような見出しの情報を**重ねて書かないこと**。 -クラス定義のドキュメンテーションブロック: +クラスの定義の文書のかたまりです。 -- クラスの説明で始まります。 -- 空行が続きます。 -- `@property` (または `@property-read`, `@property-write`)アノテーションが続きます。構文は:アノテーション、スペース、型、スペース、$名前。 -- `@method` アノテーションが続きます。構文は:アノテーション、スペース、戻り値の型、スペース、名前(型 $param, ...)。 -- `@author` アノテーションは省略されます。作者情報はソースコードの履歴に保存されます。 -- `@internal` または `@deprecated` アノテーションを使用できます。 +- クラスの説明から始めます +- 続いて空行 +- 続いて `@property`(あるいは `@property-read`、`@property-write`)のアノテーションを 1 行にひとつずつ。書き方は、アノテーション、空白、型、空白、`$name` +- 続いて `@method` のアノテーションを 1 行にひとつずつ。書き方は、アノテーション、空白、戻り値の型、空白、`name(type $param, ...)` +- `@author` のアノテーションは書きません。誰が書いたかはソースコードの履歴に残ります +- `@internal` や `@deprecated` のアノテーションは使えます ```php /** - * MIMEメッセージパート。 + * MIME message part. * * @property string $encoding * @property-read array $headers @@ -80,27 +83,27 @@ Netteコーディング規約はPSR-12(またはPER Coding Style)に準拠 */ ``` -`@var` アノテーションのみを含むプロパティのドキュメンテーションブロックは、1行であるべきです: +`@var` のアノテーションだけを含むプロパティの文書のかたまりは、1 行に書くべきです。 ```php /** @var string[] */ private array $name; ``` -メソッド定義のドキュメンテーションブロック: +メソッドの定義の文書のかたまりです。 -- メソッドの簡単な説明で始まります。 -- 空行はありません。 -- `@param` アノテーションは個別の行に記述します。 -- `@return` アノテーション。 -- `@throws` アノテーションは個別の行に記述します。 -- `@internal` または `@deprecated` アノテーションを使用できます。 +- メソッドの短い説明から始めます +- 空行は入れません +- `@param` のアノテーションを 1 行にひとつずつ +- `@return` のアノテーション +- `@throws` のアノテーションを 1 行にひとつずつ +- `@internal` や `@deprecated` のアノテーションは使えます -各アノテーションの後には1つのスペースが続きますが、`@param` の後には読みやすさ向上のために2つのスペースが続きます。 +どのアノテーションのうしろにも空白をひとつ置きます。ただし `@param` だけは、読みやすさのために空白 2 つを置きます。 ```php /** - * ディレクトリ内のファイルを検索します。 + * Finds a file in directory. * @param string[] $options * @return string[] * @throws DirectoryNotFoundException @@ -109,20 +112,37 @@ 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] で調整できます -- ユーザーのインデントサイズの好みをコードに強制しないため、コードの移植性が向上します -- 1回のキーストロークで入力できます(タブをスペースに変換するエディタだけでなく、どこでも) -- インデントはその目的です -- 視覚障害のある同僚や盲目の同僚のニーズを尊重します +- 字下げの大きさをエディタでも "ウェブ":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size でも変えられます +- 書き手の好みの字下げの大きさをコードに押し付けないので、コードが持ち運びやすくなります +- ひと打ちで入力できます(タブを空白に変えるエディタに限らず、どこでもです) +- 字下げこそがその役目です +- 目の不自由な仲間や見えない仲間の必要に応えます -私たちのプロジェクトでタブを使用することにより、幅のカスタマイズが可能になります。これはほとんどの人にとっては不要に見えるかもしれませんが、視覚障害のある人々にとっては不可欠です。 +プロジェクトでタブを使うことで幅を変えられるようにしています。多くの人には要らないことに見えるかもしれませんが、目の不自由な人には欠かせません。 -点字ディスプレイを使用する盲目のプログラマにとって、各スペースは1つの点字セルを表します。したがって、デフォルトのインデントが4スペースの場合、第3レベルのインデントはコードが始まる前に12個の貴重な点字セルを浪費します。 ノートパソコンで最も一般的に使用される40セルのディスプレイでは、利用可能なセルの4分の1以上が情報なしで浪費されます。 +点字ディスプレイを使う目の見えないプログラマーにとって、空白ひとつは点字のマスひとつです。ですから既定の字下げが空白 4 つなら、3 段めの字下げは、コードが始まる前に貴重な点字のマスを 12 個も無駄にします。ノートパソコンでもっともよくある 40 マスのディスプレイなら、それは使えるマスの 4 分の 1 以上を、何の情報も与えずに無駄にすることになります。 {{priority: -1}} diff --git a/contributing/ja/documentation.texy b/contributing/ja/documentation.texy index 052c6485fd..c332519c91 100644 --- a/contributing/ja/documentation.texy +++ b/contributing/ja/documentation.texy @@ -1,68 +1,68 @@ -ドキュメントへの貢献方法 -************ +ドキュメントへの貢献 +********** .[perex] -ドキュメントへの貢献は、他の人がフレームワークを理解するのを助けるため、最も有益な活動の1つです。 +ドキュメントへの貢献はもっとも価値ある活動のひとつです。ほかの人がフレームワークを理解する助けになるからです。 -書き方 ---- +どう書くか +----- -ドキュメントは主に、トピックに慣れていない人々を対象としています。したがって、いくつかの重要な点を満たす必要があります: +ドキュメントは主に、その話題にはじめて触れる人のためのものです。ですからいくつかの大事な点を満たすべきです。 -- 簡単で一般的なことから始めます。より高度なトピックには最後に進みます -- 物事をできるだけよく説明するように努めます。たとえば、まず同僚にトピックを説明してみてください -- ユーザーが特定のトピックについて実際に知る必要がある情報のみを提供します -- あなたの情報が本当に真実であることを確認します。すべてのコードをテストします -- 簡潔に - 書いたものを半分に短縮します。そして、必要であればもう一度 -- 太字から `.[note]` のようなボックスまで、あらゆる種類の強調表示を控えめに使用します -- コードでは [コーディング規約 |Coding Standard] を遵守します +- 単純で一般的な考え方から始めます。より進んだ話題へ進むのは最後にします。 +- できるだけ分かりやすく説明するよう努めます。たとえば、まず同僚に説明してみてください。 +- その話題で利用者が本当に必要とする情報だけを書きます。 +- 書いた情報が正しいかを確かめます。すべてのコードを試してください。 +- 簡潔にします。書いたものを半分に削ってください。そしてもう一度やっても構いません。 +- 強調は控えめに使います。太字から `.[note]` のような囲みまで同じです。 +- コードの例では[コーディング規約|coding-standard]に従ってください。 -また、[構文 |syntax] を習得してください。執筆中に記事をプレビューするには、[プレビュー付きエディタ |https://editor.nette.org/] を使用できます。 +[記法 |syntax]も学んでください。書きながら記事を下見するには、[プレビューエディタ |https://editor.nette.org/]を使えます。 -言語バージョン -------- +言語版 +--- -主要言語は英語です。したがって、あなたの変更はチェコ語と英語の両方であるべきです。英語が得意でない場合は、[DeepL Translator |https://www.deepl.com/translator] を使用し、他の人がテキストをチェックします。 +英語が主の言語なので、変更は英語であるのが理想です。英語が得意でないなら [DeepL 翻訳 |https://www.deepl.com/translator]を使ってください。ほかの人が文を見直します。 -他の言語への翻訳は、あなたの修正が承認され、微調整された後に自動的に行われます。 +ほかの言語への翻訳は、あなたの変更が受け入れられて確定したあとに自動的に行われます。 -簡単な編集 ------ +ごく小さな修正 +------- -ドキュメントに貢献するには、[GitHub|https://github.com] アカウントが必要です。 +ドキュメントに貢献するには、[GitHub |https://github.com]のアカウントが要ります。 -ドキュメントに小さな変更を加える最も簡単な方法は、各ページの最後にあるリンクを利用することです: +ドキュメントに小さな変更を加えるいちばん簡単な方法は、各ページの終わりのリンクを使うことです。 -- *GitHubで表示* は、GitHub上の特定のページのソース形式を開きます。その後、`E` ボタンを押すだけで編集を開始できます(GitHubにログインしている必要があります) -- *プレビューを開く* はエディタを開き、最終的な視覚的な外観もすぐに確認できます +- *Show on GitHub* は GitHub でそのページのもとの版を開きます。あとは `E` のキーを押せば編集を始められます(GitHub にログインしている必要があります)。 +- *Open preview* はエディタを開き、最終的な見た目をすぐに確かめられます。 -[プレビュー付きエディタ |https://editor.nette.org/] には変更を直接GitHubに保存するオプションがないため、編集が完了したら、ソーステキストをクリップボードにコピーし(*クリップボードにコピー* ボタンを使用)、それをGitHubのエディタに貼り付ける必要があります。編集フィールドの下には送信フォームがあります。ここで、修正の理由を簡単に要約して説明することを忘れないでください。送信後、いわゆるプルリクエスト(PR)が作成され、さらに編集できます。 +[プレビューエディタ |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] のいずれかを利用したりできます。 +GitHub の画面だけに頼るより、Git のバージョン管理システムの基本を身につけておくほうがよいでしょう。Git に馴染みがないなら、[git - the simple guide |https://rogerdudler.github.io/git-guide/]を見て、たくさんある[図の操作のクライアント |https://git-scm.com/downloads/guis]のどれかを使うことを考えてみてください。 -ドキュメントを次のように編集します: +ドキュメントは次のように編集します。 -1) GitHubで、[nette/docs |https://github.com/nette/docs] リポジトリの [フォーク |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] を作成します -2) このリポジトリを自分のコンピュータに [クローン |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] します -3) 次に、[関連するブランチ |#ドキュメントの構造] で変更を行います -4) [Code-Checker |code-checker:] ツールを使用して、テキスト内の余分なスペースをチェックします -4) 変更を保存(コミット)します -6) 変更に満足したら、GitHubの自分のフォークに送信(プッシュ)します -7) そこから、[プルリクエスト |https://help.github.com/articles/creating-a-pull-request] (PR) を作成して `nette/docs` リポジトリに送信します +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に送信します。プルリクエストの修正のために新しいプルリクエストを作成しないでください。 +提案のコメントを受け取るのはよくあることです。提案された変更を追いかけて取り入れてください。提案された変更は新しいコミットとして足し、GitHub へまた push します。既存の pull request を直すためだけに、新しい pull request を作っては決していけません。 ドキュメントの構造 --------- -ドキュメント全体は、GitHubの [nette/docs |https://github.com/nette/docs] リポジトリにあります。現在のバージョンはマスターにあり、古いバージョンは `doc-3.x`、`doc-2.x` のようなブランチにあります。 +ドキュメント全体は GitHub の [nette/docs |https://github.com/nette/docs]のリポジトリにあります。今の版は `master` のブランチに、古い版は `doc-3.x`、`doc-2.x` のようなブランチにあります。 -各ブランチの内容は、ドキュメントの個々の領域を表す主要なフォルダに分割されます。たとえば、`application/` は https://doc.nette.org/cs/application に対応し、`latte/` は https://latte.nette.org に対応します。これらの各フォルダには、言語バージョン(`cs`、`en`、...)を表すサブフォルダと、オプションでドキュメントページに挿入できる画像を含む `files` サブフォルダが含まれています。 +それぞれのブランチの中身は、ドキュメントの領域ごとの主なフォルダに分かれています。たとえば `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 index 4ee286ef64..bd6dfd14bc 100644 --- a/contributing/ja/syntax.texy +++ b/contributing/ja/syntax.texy @@ -1,51 +1,51 @@ -ドキュメント構文 -******** +ドキュメントの記法 +********* -ドキュメントはMarkdownと [Texy構文 |https://texy.info/cs/syntax] を使用し、いくつかの拡張機能があります。 +ドキュメントは Markdown と [Texy の記法 |https://texy.nette.org/syntax]に、いくつかの拡張を加えたものを使います。 リンク === -内部リンクには角括弧 `[]` を使用します。これは、パイプ記号 `[リンクテキスト |リンクターゲット]` を使用する形式、またはターゲットがテキストと同じ場合(小文字とハイフンに変換後)の省略形 `[リンクテキスト |元のリンクテキスト]` のいずれかです。 +内部のリンクには角かっこの書き方 `[link]` を使います。これは縦棒を使う形 `[リンクの文 |リンクの行き先]`、あるいは行き先が文と同じ場合(小文字とハイフンへ変換したうえで)の短い形 `[リンクの文]` です。 -- `[Page name |Page name]` -> `<a href="/en/page-name">Page name</a>` +- `[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`などの特別なセクションを意味します。 +別の言語版や別の区画へもリンクできます。区画とは Nette のライブラリ(たとえば `forms`、`latte` など)や、`best-practices`、`quickstart` などの特別な区画のことです。 -- `[cs:Page name |cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (同じセクション、異なる言語) -- `[tracy:Page name |tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (異なるセクション、同じ言語) -- `[tracy:cs:Page name |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (異なるセクションと言語) +- `[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>` (現在のページのヘッダー) +- `[#Heading]` -> `<a href="#toc-heading">Heading</a>`(今のページの見出し) - `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` -セクションの開始ページへのリンク:(`@home` はセクションのホームページの特別な表現です) +区画のトップページへのリンクです(`@home` は区画のトップページを表す特別な語です)。 - `[link text |@home]` -> `<a href="/en/">link text</a>` - `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` -APIドキュメントへのリンク --------------- +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 のドキュメントへのリンク +---------------- - `[php:substr]` -> [php:substr] @@ -53,7 +53,7 @@ PHPドキュメントへのリンク ソースコード ====== -コードブロックは <code>```lang</code> で始まり、<code>```</code> で終わります。サポートされている言語は `php`、`latte`、`neon`、`html`、`css`、`js`、`sql` です。インデントには常にタブを使用してください。 +コードのかたまりは <code>```lang</code> で始まり <code>```</code> で終わります。対応している言語は `php`、`latte`、`neon`、`html`、`css`、`js`、`sql` です。字下げにはいつもタブを使ってください。 ``` ```php @@ -63,7 +63,7 @@ PHPドキュメントへのリンク ``` ``` -ファイル名を <code>```php .{file: ArrayTest.php}</code> のように指定することもでき、コードブロックはこのようにレンダリングされます: +<code>```php .{file: ArrayTest.php}</code> のようにファイル名も指定でき、そのコードのかたまりは次のように描かれます。 ```php .{file: ArrayTest.php} public function renderPage($id) @@ -75,68 +75,69 @@ public function renderPage($id) 見出し === -最上位の見出し(つまりページタイトル)はアスタリスクで下線を引きます。セクションを区切るには等号を使用します。見出しには等号、次にハイフンで下線を引きます: +いちばん上の見出し(ページの名前)はアスタリスク(`*`)で下線を引きます。節を分けるには等号(`=`)を使います。見出しはまず等号(`=`)で、次にハイフン(`-`)で下線を引きます。 ``` -MVCアプリケーションとPresenter -********************* +MVC Applications & Presenters +***************************** ... -リンクの作成 -====== +Link Creation +============= ... -テンプレート内のリンク ------------ +Links in Templates +------------------ ... ``` -ボックスとスタイル -========= +囲みとスタイル +======= -Perexは `.[perex]` クラスでマークします .[perex] +クラス `.[perex]` で印を付けた perex .[perex] -注釈は `.[note]` クラスでマークします .[note] +クラス `.[note]` で印を付けた note .[note] -ヒントは `.[tip]` クラスでマークします .[tip] +クラス `.[tip]` で印を付けた tip .[tip] -注意は `.[caution]` クラスでマークします .[caution] +クラス `.[caution]` で印を付けた caution .[caution] -より強い警告は `.[warning]` クラスでマークします .[warning] +クラス `.[warning]` で印を付けた強い警告 .[warning] バージョン番号 `.{data-version:2.4.10}` .{data-version:2.4.10} -クラスを行の前に記述します: +クラスは、それが当たる行の前に書くべきです。 ``` .[perex] -これはペレックスです。 +これが perex です。 ``` -`.[tip]` のようなボックスは目を引くため、重要でない情報ではなく、強調のために使用されることに注意してください。したがって、その使用は最大限に控えてください。 +`.[tip]` のような囲みは目を引くので、大事な情報を強調するのに使うべきで、あまり重要でない細部には使わないでください。控えめに使ってください。 目次 -===== +=== -目次(右側のメニューのリンク)は、サイズが4,000バイトを超えるすべてのページに対して自動的に生成されます。このデフォルトの動作は、[#メタタグ] `{{toc}}` を使用して変更できます。目次を構成するテキストは、通常、見出しのテキストから直接取得されますが、`{toc}` 修飾子を使用すると、目次に異なるテキストを表示できます。これは、特に長い見出しに便利です。 +目次(右の欄のリンク)は、大きさが 4,000 バイトを超えるすべてのページに自動的に作られます。この既定の振る舞いは `{{toc}}` の[メタタグ |#メタタグ]で変えられます。目次の文は既定では見出しからそのまま取られますが、`.{toc}` の修飾子で違う文を表示させられます。長い見出しに便利です。 ``` -長くて賢い見出し .{toc: 目次に表示される任意の他のテキスト} -================================== +Long and Intelligent Heading .{toc: A Different Text for TOC} +============================================================= ``` メタタグ ==== -- カスタムページタイトル(`<title>` とパンくずナビゲーション内)の設定 `{{title: 別のタイトル}}` -- リダイレクト `{{redirect: pla:cs}}` - [#リンク] を参照 -- 自動目次(個々の見出しへのリンクを含むボックス)の強制 `{{toc}}` または無効化 `{{toc: no}}` +- ページの題名を独自に決めます(`<title>` とパンくずで)。`{{title: Another name}}` +- リダイレクト。`{{redirect: pla:cs}}` - [#リンク]をご覧ください +- 自動の目次(見出しへのリンクの囲み)を強いる `{{toc}}` か、切る `{{toc: no}}`。 +- 左のメニューを設定する `{{leftbar: utils:@left-menu}}` か、切る `{{leftbar: no}}`。 {{priority: -1}} diff --git a/contributing/pl/@home.texy b/contributing/pl/@home.texy index fe566dfdef..afd1a1164b 100644 --- a/contributing/pl/@home.texy +++ b/contributing/pl/@home.texy @@ -2,16 +2,16 @@ Zostań kontrybutorem Nette ************************** .[perex] -Dowiedz się, jak możesz zaangażować się w nasz projekt open source. Opanuj procedury dotyczące wnoszenia wkładu w kod źródłowy i dokumentację i stań się częścią społeczności programistów, którzy aktywnie uczestniczą w ulepszaniu Nette. +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** -- [Jak wnieść wkład w kod? |code] -- [Standard kodowania |coding-standard] +- [Kontrybuowanie do kodu |code] +- [Standardy kodowania |coding-standard] **Dokumentacja** -- [Jak wnieść wkład w dokumentację? |documentation] +- [Kontrybuowanie do dokumentacji |documentation] - [Składnia dokumentacji |syntax] -- "Edytor podglądu":https://editor.nette.org +- "Edytor z podglądem":https://editor.nette.org diff --git a/contributing/pl/@left-menu.texy b/contributing/pl/@left-menu.texy index da4eb76448..ce68b1225b 100644 --- a/contributing/pl/@left-menu.texy +++ b/contributing/pl/@left-menu.texy @@ -1,10 +1,18 @@ Kod *** -- [Jak wnieść wkład w kod? |code] -- [Standard kodowania |coding-standard] +- [Jak współtworzyć kod |code] +- [Standardy kodowania |coding-standard] Dokumentacja ************ -- [Jak wnieść wkład w dokumentację? |documentation] +- [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 index 7546159296..cf48768c04 100644 --- a/contributing/pl/code.texy +++ b/contributing/pl/code.texy @@ -1,117 +1,106 @@ -Jak współtworzyć kod -******************** +Kontrybuowanie do kodu +********************** .[perex] -Zamierzasz współtworzyć Nette Framework i potrzebujesz zorientować się w zasadach i procedurach? Ten przewodnik dla początkujących krok po kroku pokaże Ci, jak efektywnie współtworzyć kod, pracować z repozytoriami i implementować zmiany. +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 ========= -Aby współtworzyć kod, niezbędne jest posiadanie konta na [GitHub|https://github.com] i znajomość podstaw pracy z systemem kontroli wersji Git. Jeśli nie znasz pracy z Gitem, możesz zapoznać się z przewodnikiem [git - the simple guide |https://rogerdudler.github.io/git-guide/] i ewentualnie skorzystać z jednego z wielu [klientów graficznych |https://git-scm.com/downloads/guis]. +Ż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) to repozytorium [sklonujesz |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na swój komputer -3) zainstaluj zależności, w tym [Nette Testera |tester:], za pomocą polecenia `composer install` -4) sprawdź, czy testy działają, uruchamiając `composer tester` -5) utwórz [#nową gałąź] opartą na ostatniej wydanej wersji +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 -Implementacja własnych zmian ----------------------------- +Wprowadzanie własnych zmian +--------------------------- -Teraz możesz wprowadzić własne modyfikacje kodu: +Teraz możesz wprowadzać własne poprawki w kodzie: -1) zaprogramuj wymagane zmiany i nie zapomnij o testach -2) upewnij się, że testy przechodzą pomyślnie, za pomocą `composer tester` -3) sprawdź, czy kod spełnia [standard kodowania |#Standardy kodowania] -4) zmiany zapisz (commituj) z opisem w [tym formacie |#Opis commita] +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ć kilka commitów, jeden dla każdego logicznego kroku. Każdy commit powinien być sensowny samodzielnie. +Możesz utworzyć wiele commitów, po jednym na każdy logiczny krok. Każdy commit powinien mieć sens sam w sobie. -Wysyłanie zmian ---------------- +Zgłaszanie zmian +---------------- -Gdy będziesz zadowolony ze zmian, możesz je wysłać: +Gdy jesteś zadowolony ze zmian, możesz je zgłosić: -1) wyślij (pushnij) zmiany na GitHub do swojego forka -2) stamtąd wyślij je do repozytorium Nette, tworząc [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) podaj w opisie [wystarczająco informacji |#Opis pull requesta] +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] -Wprowadzanie uwag ------------------ +Uwzględnianie uwag +------------------ -Twoje commity teraz zobaczą również inni. Jest to normalne, że otrzymasz komentarze z uwagami: +Twoje commity są teraz widoczne dla innych. Często dostaje się komentarze z sugestiami: -1) śledź proponowane modyfikacje -2) wprowadź je jako nowe commity lub [połącz z poprzednimi |https://help.github.com/en/github/using-git/about-git-rebase] -3) ponownie wyślij commity na GitHub, a automatycznie pojawią się w pull requeście +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 w celu modyfikacji istniejącego. +Nigdy nie twórz nowego pull requesta, żeby zmodyfikować istniejący. Dokumentacja ------------ -Jeśli zmieniłeś funkcjonalność lub dodałeś nową, nie zapomnij jej również [dodać do dokumentacji |documentation]. +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 ostatniej wydanej wersji, tj. ostatniego tagu w danej gałęzi. Dla tagu `v3.2.1` utworzysz gałąź tym poleceniem: +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 new_branch_name v3.2.1 +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 kontroli i poprawy kodu dostępne jest automatyczne narzędzie. Można je zainstalować za pomocą Composera **globalnie** w wybranym przez siebie folderze: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Teraz powinieneś móc uruchomić narzędzie w terminalu. Pierwszym poleceniem sprawdzisz, a drugim również poprawisz kod w folderach `src` i `tests` w bieżącym katalogu: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` +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 commita +Opis commitu ============ -W Nette tematy commitów mają format: `Presenter: fixed AJAX detection [Closes #69]` +W Nette tematy commitów mają następujący format: `Presenter: fixed AJAX detection [Closes #69]` -- obszar, po którym następuje dwukropek -- cel commita w czasie przeszłym, jeśli to możliwe, zacznij od słowa: "added (dodana nowa właściwość)", "fixed (poprawka)", "refactored (zmiana w kodzie bez zmiany zachowania)", changed, removed -- jeśli commit przerywa kompatybilność wsteczną, dodaj "BC break" -- ewentualne powiązanie z issue trackerem, jak `(#123)` lub `[Closes #69]` -- po temacie może nastąpić jedna wolna linia, a następnie bardziej szczegółowy opis, w tym np. linki do forum +- 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 ================== -Podczas tworzenia pull requesta interfejs GitHubu pozwoli Ci wprowadzić tytuł i opis. Podaj zwięzły tytuł, a w opisie dostarcz jak najwięcej informacji o powodach Twojej zmiany. +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. -Wyświetli się również nagłówek, w którym określ, czy jest to nowa funkcja, czy poprawka błędu i czy może dojść do naruszenia kompatybilności wstecznej (BC break). Jeśli istnieje powiązany problem (issue), odwołaj się do niego, aby został zamknięty po zatwierdzeniu pull requesta. +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? <!-- #numery issue, jeśli istnieją --> +- bug fix / new feature? <!-- #issue numbers, if any --> - BC break? yes/no -- doc PR: nette/docs#? <!-- bardzo mile widziane, zobacz https://nette.org/en/writing --> +- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> ``` diff --git a/contributing/pl/coding-standard.texy b/contributing/pl/coding-standard.texy index 0d2237d70b..c4eccea1f9 100644 --- a/contributing/pl/coding-standard.texy +++ b/contributing/pl/coding-standard.texy @@ -2,43 +2,46 @@ Standard kodowania ****************** .[perex] -Ten dokument opisuje zasady i zalecenia dotyczące rozwoju Nette. Przy współtworzeniu kodu do Nette musisz ich przestrzegać. Najprostszym sposobem, aby to zrobić, jest naśladowanie istniejącego kodu. Chodzi o to, aby cały kod wyglądał, jakby napisała go jedna osoba. +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 [#tabulatory zamiast spacji] i dla [stałych klas używa PascalCase|https://blog.nette.org/pl/for-less-screaming-in-the-code]. +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. -Ogólne zasady + +Reguły ogólne ============= - Każdy plik PHP musi zawierać `declare(strict_types=1)` -- Dwie puste linie są używane do oddzielenia metod dla lepszej czytelności. -- Powód użycia operatora wyciszenia musi być udokumentowany: `@mkdir($dir); // @ - katalog może istnieć`. -- Jeśli używany jest operator porównania słabo typizowanego (tj. `==`, `!=`, ...), musi być udokumentowany zamiar: `// == akceptuj null` -- Do jednego pliku `exceptions.php` możesz zapisać wiele wyjątków. -- W interfejsach nie określa się widoczności metod, ponieważ są zawsze publiczne. -- Każda właściwość, wartość zwracana i parametr musi mieć podany typ. Natomiast przy stałych finalnych typu nigdy nie podajemy, ponieważ jest oczywisty. -- Do ograniczenia ciągu znaków należy używać pojedynczych cudzysłowów, z wyjątkiem przypadków, gdy sam literał zawiera apostrofy. +- 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 nazewnictwa -===================== +Konwencje nazewnicze +==================== -- Nie używaj skrótów, chyba że pełna nazwa jest zbyt długa. -- W przypadku dwuliterowych skrótów używaj wielkich liter, w przypadku dłuższych skrótów pascal/camel. -- Dla nazwy klasy używaj rzeczownika lub wyrażenia rzeczownikowego. -- Nazwy klas muszą zawierać nie tylko specyficzność (`Array`), ale także ogólność (`ArrayIterator`). Wyjątkiem są atrybuty języka PHP. -- "Stałe klas i enumy powinny używać PascalCaps":https://blog.nette.org/pl/for-less-screaming-in-the-code. -- "Interfejsy i klasy abstrakcyjne nie powinny zawierać prefiksów ani sufiksów":https://blog.nette.org/pl/prefixes-and-suffixes-do-not-belong-in-interface-names jak `Abstract`, `Interface` lub `I`. +- 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` -Zawijanie i nawiasy klamrowe -============================ +Łamanie linii i nawiasy +======================= -Standard kodowania Nette odpowiada PSR-12 (resp. PER Coding Style), w niektórych punktach go uzupełnia lub modyfikuje: +Standard kodowania Nette odpowiada PSR-12 (albo PER Coding Style), ale w niektórych punktach go doprecyzowuje albo modyfikuje: -- funkcje strzałkowe pisze się bez spacji przed nawiasem, tj. `fn($a) => $b` -- nie wymaga się pustej linii między różnymi typami instrukcji importu `use` -- typ zwracany funkcji/metody i otwierający nawias klamrowy są zawsze na osobnych liniach: +- 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( @@ -46,11 +49,11 @@ Standard kodowania Nette odpowiada PSR-12 (resp. PER Coding Style), w niektóryc array $options, ): array { - // ciało metody + // treść metody } ``` -Otwierający nawias klamrowy na osobnej linii jest ważny dla wizualnego oddzielenia sygnatury funkcji/metody od ciała. Jeśli sygnatura jest na jednej linii, oddzielenie jest wyraźne (rysunek po lewej), jeśli jest na wielu liniach, w PSR sygnatury i ciała zlewają się (w środku), podczas gdy w standardzie Nette są nadal oddzielone (po prawej): +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 *] @@ -58,20 +61,20 @@ Otwierający nawias klamrowy na osobnej linii jest ważny dla wizualnego oddziel Bloki dokumentacyjne (phpDoc) ============================= -Główna zasada: Nigdy nie duplikuj żadnych informacji w sygnaturze, takich jak typ parametru lub typ zwracany, bez dodanej wartości. +Główna reguła: **nigdy nie duplikuj** żadnej informacji z sygnatury, jak typ parametru czy typ zwracany, bez wnoszenia wartości. -Blok dokumentacyjny dla definicji klasy: +Blok dokumentacyjny definicji klasy: -- Zaczyna się opisem klasy. -- Następuje pusta linia. -- Następują adnotacje `@property` (lub `@property-read`, `@property-write`), jedna po drugiej. Składnia to: adnotacja, spacja, typ, spacja, $nazwa. -- Następują adnotacje `@method`, jedna po drugiej. Składnia to: adnotacja, spacja, typ zwracany, spacja, nazwa(typ $param, ...). -- Adnotacja `@author` jest pomijana. Autorstwo jest przechowywane w historii kodu źródłowego. -- Można użyć adnotacji `@internal` lub `@deprecated`. +- 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 /** - * Część wiadomości MIME. + * MIME message part. * * @property string $encoding * @property-read array $headers @@ -80,27 +83,27 @@ Blok dokumentacyjny dla definicji klasy: */ ``` -Blok dokumentacyjny dla właściwości, który zawiera tylko adnotację `@var`, powinien być jednoliniowy: +Blok dokumentacyjny właściwości zawierający tylko adnotację `@var` powinien być w jednej linii: ```php /** @var string[] */ private array $name; ``` -Blok dokumentacyjny dla definicji metody: +Blok dokumentacyjny definicji metody: -- Zaczyna się krótkim opisem metody. -- Brak pustej linii. -- Adnotacje `@param` w osobnych liniach. -- Adnotacja `@return`. -- Adnotacje `@throws`, jedna po drugiej. -- Można użyć adnotacji `@internal` lub `@deprecated`. +- 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 dla lepszej czytelności następują dwie spacje. +Po każdej adnotacji następuje jedna spacja, z wyjątkiem `@param`, po której następują dwie spacje dla lepszej czytelności. ```php /** - * Znajduje plik w katalogu. + * Finds a file in directory. * @param string[] $options * @return string[] * @throws DirectoryNotFoundException @@ -109,20 +112,37 @@ 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ą w porównaniu ze spacjami kilka zalet: +Tabulatory mają nad spacjami kilka zalet: -- rozmiar wcięcia można dostosować w edytorach i na "webie":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- nie narzucają kodowi preferencji użytkownika co do rozmiaru wcięcia, dzięki czemu kod jest lepiej przenośny -- można je napisać jednym naciśnięciem klawisza (wszędzie, nie tylko w edytorach, które zamieniają tabulatory na spacje) -- wcięcie jest ich celem -- szanują potrzeby kolegów z wadami wzroku i niewidomych +- 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 tabulatorów w naszych projektach, 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. +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, którzy używają monitorów brajlowskich, każda spacja stanowi 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 jeszcze przed rozpoczęciem kodu. Na 40-komórkowym monitorze, który jest najczęściej używany w laptopach, to ponad ćwierć dostępnych komórek, które są marnowane bez żadnej informacji. +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 index d34ecbffc2..4d12a28dc2 100644 --- a/contributing/pl/documentation.texy +++ b/contributing/pl/documentation.texy @@ -1,68 +1,68 @@ -Jak współtworzyć dokumentację -***************************** +Kontrybuowanie do dokumentacji +****************************** .[perex] -Współtworzenie dokumentacji jest jedną z najbardziej wartościowych czynności, ponieważ pomagasz innym zrozumieć framework. +Kontrybuowanie do dokumentacji to jedna z najcenniejszych czynności, bo pomaga innym zrozumieć framework. Jak pisać? ---------- -Dokumentacja jest przeznaczona przede wszystkim dla osób, które zapoznają się z tematem. Dlatego powinna spełniać kilka ważnych punktów: +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: -- Zacznij od prostego i ogólnego. Do bardziej zaawansowanych tematów przejdź dopiero na końcu -- Staraj się jak najlepiej wyjaśnić sprawę. Spróbuj na przykład najpierw wyjaśnić temat koledze -- Podawaj tylko te informacje, które użytkownik rzeczywiście potrzebuje wiedzieć na dany temat -- Sprawdź, czy twoje informacje są rzeczywiście prawdziwe. Każdy kod przetestuj -- Bądź zwięzły - to, co napiszesz, skróć o połowę. A potem spokojnie jeszcze raz -- Oszczędzaj na wszelkiego rodzaju wyróżnieniach, od pogrubienia po ramki jak `.[note]` -- W kodach przestrzegaj [Standard kodowania |Coding Standard] +- 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]. -Opanuj również [składnia |syntax]. Do podglądu artykułu podczas jego pisania możesz użyć [edytor z podglądem |https://editor.nette.org/]. +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 --------------- -Podstawowym językiem jest angielski, Twoje zmiany powinny więc być w języku czeskim i angielskim. Jeśli angielski nie jest Twoją mocną stroną, użyj [DeepL Translator |https://www.deepl.com/translator], a inni sprawdzą Twój tekst. +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 inne języki zostanie wykonane automatycznie po zatwierdzeniu i dopracowaniu Twojej modyfikacji. +Tłumaczenie na pozostałe języki zostanie wykonane automatycznie po zatwierdzeniu i sfinalizowaniu Twojej poprawki. -Trywialne poprawki ------------------- +Drobne poprawki +--------------- -Aby współtworzyć dokumentację, niezbędne jest posiadanie konta na [GitHub|https://github.com]. +Żeby kontrybuować do dokumentacji, musisz mieć konto na [GitHubie |https://github.com]. -Najprostszym sposobem na wprowadzenie drobnej zmiany w dokumentacji jest skorzystanie z linków na końcu każdej strony: +Najprostszym sposobem na wprowadzenie drobnej zmiany w dokumentacji jest użycie odnośników na końcu każdej strony: -- *Ukaž na GitHubu* otworzy źródłową postać danej strony na GitHubie. Następnie wystarczy nacisnąć przycisk `E` i można zacząć edytować (konieczne jest bycie zalogowanym na GitHubie) -- *Otevři náhled* otworzy edytor, w którym od razu widzisz również wynikowy wygląd wizualny +- *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 ma możliwości zapisywania zmian bezpośrednio na GitHubie, konieczne jest po zakończeniu edycji skopiowanie tekstu źródłowego do schowka (przyciskiem *Copy to clipboard*), a następnie wklejenie go do edytora na GitHubie. Pod polem edycyjnym znajduje się formularz do wysłania. Tutaj nie zapomnij krótko podsumować i wyjaśnić powód swojej modyfikacji. Po wysłaniu powstanie tzw. pull request (PR), który można dalej edytować. +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 ---------------- -Bardziej odpowiednie niż korzystanie z interfejsu GitHubu jest zapoznanie się z podstawami pracy z systemem kontroli wersji Git. Jeśli nie znasz pracy z Gitem, możesz zapoznać się z przewodnikiem [git - the simple guide |https://rogerdudler.github.io/git-guide/] i ewentualnie skorzystać z jednego z wielu [klientów graficznych |https://git-scm.com/downloads/guis]. +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ę modyfikuj w ten sposób: +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) to repozytorium [sklonujesz |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na swój komputer -3) następnie w [odpowiedniej gałęzi |#Struktura dokumentacji] wprowadź zmiany -4) sprawdź zbędne spacje w tekście za pomocą narzędzia [Code-Checker |code-checker:] -4) zmiany zapisz (commituj) -6) jeśli jesteś zadowolony ze zmian, wyślij (pushnij) je na GitHub do swojego forka -7) stamtąd wyślij je do repozytorium `nette/docs`, tworząc [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) +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). -Jest to normalne, że będziesz otrzymywać komentarze z uwagami. Śledź proponowane zmiany i wprowadź je. Proponowane zmiany dodaj jako nowe commity i ponownie wyślij na GitHub. Nigdy nie twórz nowego pull requesta w celu modyfikacji istniejącego pull requesta. +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, starsze wersje znajdują się w gałęziach takich jak `doc-3.x`, `doc-2.x`. +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 dzieli się na główne foldery reprezentujące poszczególne obszary dokumentacji. Na przykład `application/` odpowiada https://doc.nette.org/cs/application, `latte/` odpowiada https://latte.nette.org itd. Każdy taki folder zawiera podfoldery reprezentujące wersje językowe (`cs`, `en`, ...) oraz ewentualnie podfolder `files` z obrazkami, które można wstawiać do stron w dokumentacji. +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 index 0090da8000..037631e352 100644 --- a/contributing/pl/syntax.texy +++ b/contributing/pl/syntax.texy @@ -1,51 +1,51 @@ Składnia dokumentacji ********************* -Dokumentacja używa składni Markdown & [składni Texy |https://texy.info/cs/syntax] z niektórymi rozszerzeniami. +Dokumentacja używa Markdowna i [składni Texy |https://texy.nette.org/syntax] z kilkoma rozszerzeniami. -Linki -===== +Odnośniki +========= -Do linków wewnętrznych używa się zapisu w nawiasach kwadratowych `[link |odkaz]`. I to albo w postaci z pionową kreską `[tekst linku |cíl odkazu]`, albo skróconej `[tekst linku |text odkazu]`, jeśli cel jest zgodny z tekstem (po transformacji na małe litery i myślniki): +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): -- `[Page name |Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[tekst linku |Page name]` -> `<a href="/en/page-name">link text</a>` +- `[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 lub do innej sekcji. Sekcją rozumie się bibliotekę Nette (np. `forms`, `latte`, itp.) lub specjalne sekcje jak `best-practices`, `quickstart` itd.: +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:Page name |cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (ta sama sekcja, inny język) -- `[tracy:Page name |tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (inna sekcja, ten sam język) -- `[tracy:cs:Page name |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (inna sekcja i język) +- `[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) -Za pomocą `#` można również celować w konkretny nagłówek na stronie. +Można też wskazać konkretny nagłówek na stronie za pomocą `#`. -- `[Heading |#Heading]` -> `<a href="#toc-heading">Heading</a>` (nagłówek na bieżącej stronie) -- `[Page name#Heading |Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` +- `[#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>` -Link do strony głównej sekcji: (`@home` to specjalne wyrażenie dla strony głównej sekcji) +Odnośnik do strony głównej sekcji: (`@home` to specjalne określenie strony głównej sekcji) -- `[tekst linku |@home]` -> `<a href="/en/">link text</a>` -- `[tekst linku |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` +- `[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>` -Linki do dokumentacji API -------------------------- +Odnośniki do dokumentacji API +----------------------------- -Zawsze podawaj tylko za pomocą tego zapisu: +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] -Pełne kwalifikowane nazwy używaj tylko przy pierwszej wzmiance. Do kolejnych linków użyj uproszczonej nazwy: +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()] -Linki do dokumentacji PHP -------------------------- +Odnośniki do dokumentacji PHP +----------------------------- - `[php:substr]` -> [php:substr] @@ -53,7 +53,7 @@ Linki do dokumentacji PHP Kod źródłowy ============ -Blok kodu zaczyna się od <code>```lang</code> i kończy <code>```</code>. Obsługiwane języki to `php`, `latte`, `neon`, `html`, `css`, `js` i `sql`. Do wcięć zawsze używaj tabulatorów. +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 @@ -63,7 +63,7 @@ Blok kodu zaczyna się od <code>```lang</code> i kończy <code>` ``` ``` -Możesz również podać nazwę pliku jako <code>```php .{file: ArrayTest.php}</code>, a blok kodu zostanie wyrenderowany w ten sposób: +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) @@ -75,20 +75,20 @@ public function renderPage($id) Nagłówki ======== -Najwyższy nagłówek (czyli tytuł strony) podkreśl gwiazdkami. Do oddzielenia sekcji używaj znaków równości. Nagłówki podkreślaj znakami równości, a następnie myślnikami: +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 (`-`): ``` -Aplikacje MVC i presentery -************************** +MVC Applications & Presenters +***************************** ... -Tworzenie linków -================ +Link Creation +============= ... -Linki w szablonach +Links in Templates ------------------ ... ``` @@ -97,46 +97,47 @@ Linki w szablonach Ramki i style ============= -Perex oznaczamy klasą `.[perex]` .[perex] +Perex oznaczony klasą `.[perex]` .[perex] -Notatkę oznaczamy klasą `.[note]` .[note] +Notatka oznaczona klasą `.[note]` .[note] -Wskazówkę oznaczamy klasą `.[tip]` .[tip] +Wskazówka oznaczona klasą `.[tip]` .[tip] -Ostrzeżenie oznaczamy klasą `.[caution]` .[caution] +Uwaga oznaczona klasą `.[caution]` .[caution] -Mocniejsze ostrzeżenie oznaczamy klasą `.[warning]` .[warning] +Mocne ostrzeżenie oznaczone klasą `.[warning]` .[warning] Numer wersji `.{data-version:2.4.10}` .{data-version:2.4.10} -Klasy zapisuj przed linią: +Klasy należy zapisywać przed linią, której dotyczą: ``` .[perex] To jest perex. ``` -Proszę pamiętać, że ramki takie jak `.[tip]` "przyciągają" wzrok, dlatego używa się ich do podkreślenia, a nie do mniej istotnych informacji. Dlatego ich użyciem maksymalnie oszczędzaj. +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 (linki w prawym menu) jest automatycznie generowany dla wszystkich stron, których rozmiar przekroczy 4 000 bajtów, przy czym to domyślne zachowanie można zmodyfikować za pomocą [#znaczniki meta] `{{toc}}`. Tekst tworzący spis treści jest standardowo brany bezpośrednio z tekstu nagłówków, ale za pomocą modyfikatora `.{toc}` można wyświetlić w spisie treści inny tekst, co przydaje się głównie przy dłuższych nagłówkach. +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. ``` -Długi i inteligentny nagłówek .{toc: Dowolny inny tekst wyświetlany w spisie treści} -==================================================================================== +Long and Intelligent Heading .{toc: A Different Text for TOC} +============================================================= ``` -Znaczniki meta -============== +Metatagi +======== -- ustawienie własnego tytułu strony (w `<title>` i nawigacji okruszkowej) `{{title: Inny tytuł}}` -- przekierowanie `{{redirect: pla:cs}}` - zobacz [#linki] -- wymuszenie `{{toc}}` lub zakazanie `{{toc: no}}` automatycznego spisu treści (ramka z linkami do poszczególnych nagłówków) +- 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/pt/@home.texy b/contributing/pt/@home.texy deleted file mode 100644 index a1e6b6466d..0000000000 --- a/contributing/pt/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Torne-se um contribuidor do Nette -********************************* - -.[perex] -Descubra como você pode se envolver em nosso projeto de código aberto. Aprenda os procedimentos para contribuir com o código-fonte e a documentação e faça parte da comunidade de desenvolvedores que participam ativamente no aprimoramento do Nette. - - -**Código** - -- [Como contribuir para o código? |code] -- [Padrão de codificação |coding-standard] - -**Documentação** - -- [Como contribuir para a documentação? |documentation] -- [Sintaxe da documentação |syntax] -- "Editor de pré-visualização":https://editor.nette.org diff --git a/contributing/pt/@left-menu.texy b/contributing/pt/@left-menu.texy deleted file mode 100644 index bca571a642..0000000000 --- a/contributing/pt/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Código -****** -- [Como contribuir para o código? |code] -- [Padrão de codificação |coding-standard] - -Documentação -************ -- [Como contribuir para a documentação? |documentation] -- [Sintaxe da documentação |syntax] -- "Editor de pré-visualização":https://editor.nette.org diff --git a/contributing/pt/code.texy b/contributing/pt/code.texy deleted file mode 100644 index 2a264be5cf..0000000000 --- a/contributing/pt/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Como contribuir para o código -***************************** - -.[perex] -Você está prestes a contribuir para o Nette Framework e precisa se orientar sobre as regras e procedimentos? Este guia para iniciantes mostrará passo a passo como contribuir eficazmente para o código, trabalhar com repositórios e implementar alterações. - - -Procedimento -============ - -Para contribuir para o código, é essencial ter uma conta no [GitHub|https://github.com] e estar familiarizado com os fundamentos do trabalho com o sistema de controle de versão Git. Se você não domina o trabalho com o Git, pode consultar o guia [git - the simple guide |https://rogerdudler.github.io/git-guide/] e, opcionalmente, usar um dos muitos [clientes gráficos |https://git-scm.com/downloads/guis]. - - -Preparação do ambiente e do repositório ---------------------------------------- - -1) No GitHub, crie um [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] do repositório do [pacote |www:packages] que você pretende modificar. -2) [Clone |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] este repositório para o seu computador. -3) Instale as dependências, incluindo o [Nette Tester |tester:], utilizando o comando `composer install`. -4) Verifique se os testes funcionam executando `composer tester`. -5) Crie um [#novo branch] baseado na última versão lançada. - - -Implementação das suas próprias alterações ------------------------------------------- - -Agora você pode fazer as suas próprias modificações no código: - -1) Implemente as alterações desejadas e não se esqueça dos testes. -2) Certifique-se de que os testes são executados com sucesso, utilizando `composer tester`. -3) Verifique se o código cumpre os [#padrões de codificação]. -4) Salve (commit) as alterações com uma descrição [neste formato |#Descrição do commit]. - -Você pode criar vários commits, um para cada passo lógico. Cada commit deve ser significativo por si só. - - -Envio das alterações --------------------- - -Assim que estiver satisfeito com as alterações, pode enviá-las: - -1) Envie (push) as alterações para o GitHub no seu fork. -2) A partir daí, envie-as para o repositório Nette criando um [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). -3) Forneça [informações suficientes |#Descrição do pull request] na descrição. - - -Incorporação de comentários ---------------------------- - -Os seus commits serão agora vistos por outros. É comum receber comentários com sugestões: - -1) Acompanhe as modificações propostas. -2) Incorpore-as como novos commits ou [faça rebase com os anteriores |https://help.github.com/en/github/using-git/about-git-rebase]. -3) Envie novamente os commits para o GitHub e eles aparecerão automaticamente no pull request. - -Nunca crie um novo pull request para modificar um existente. - - -Documentação ------------- - -Se você alterou a funcionalidade ou adicionou uma nova, não se esqueça de a [adicionar também à documentação |documentation]. - - -Novo branch -=========== - -Se possível, faça as alterações em relação à última versão lançada, ou seja, a última tag no branch correspondente. Para a tag `v3.2.1`, crie um branch com este comando: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Padrões de Codificação -====================== - -O seu código deve cumprir os [padrões de codificação |coding-standard] utilizados no Nette Framework. Existe uma ferramenta automática disponível para verificar e corrigir o código. Pode ser instalada via Composer **globalmente** na pasta da sua escolha: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Agora você deve conseguir executar a ferramenta no terminal. O primeiro comando verifica e o segundo também corrige o código nas pastas `src` e `tests` no diretório atual: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Descrição do commit -=================== - -No Nette, os assuntos dos commits têm o formato: `Presenter: fixed AJAX detection [Closes #69]` - -- área seguida por dois pontos -- propósito do commit no tempo passado; se possível, comece com uma palavra como: "added" (nova funcionalidade adicionada), "fixed" (correção), "refactored" (alteração no código sem alteração de comportamento), "changed", "removed" -- se o commit quebrar a compatibilidade retroativa, adicione "BC break" -- possível vínculo com o gestor de issues como `(#123)` ou `[Closes #69]` -- após o assunto, pode seguir uma linha em branco e depois uma descrição mais detalhada, incluindo, por exemplo, links para o fórum - - -Descrição do pull request -========================= - -Ao criar um pull request, a interface do GitHub permitirá que você insira um título e uma descrição. Forneça um título conciso e, na descrição, forneça o máximo de informações possível sobre os motivos da sua alteração. - -Também será exibido um cabeçalho onde você especifica se é uma nova funcionalidade ou correção de erro e se pode haver quebra de compatibilidade retroativa (BC break). Se houver um problema relacionado (issue), crie um link para ele para que seja fechado após a aprovação do pull request. - -``` -- bug fix / new feature? <!-- #números das issues, se houver --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- altamente bem-vindo, veja https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/pt/coding-standard.texy b/contributing/pt/coding-standard.texy deleted file mode 100644 index fcc737696b..0000000000 --- a/contributing/pt/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Padrões de Codificação -********************** - -.[perex] -Este documento descreve as regras e recomendações para o desenvolvimento do Nette. Ao contribuir com código para o Nette, você deve segui-las. A forma mais fácil de o fazer é imitar o código existente. O objetivo é fazer com que todo o código pareça ter sido escrito por uma única pessoa. - -Os Padrões de Codificação Nette correspondem ao [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] com duas exceções principais: utiliza [#tabulações em vez de espaços] para indentação e utiliza [PascalCase para constantes de classe|https://blog.nette.org/en/less-noise-in-code]. - - -Regras gerais -============= - -- Cada arquivo PHP deve conter `declare(strict_types=1)` -- Duas linhas em branco são usadas para separar métodos para melhor legibilidade. -- O motivo para usar o operador shut-up (@) deve ser documentado: `@mkdir($dir); // @ - o diretório pode já existir`. -- Se for usado um operador de comparação de tipo fraco (ou seja, `==`, `!=`, ...), a intenção deve ser documentada: `// == aceita null` -- Você pode escrever várias exceções num único arquivo `exceptions.php`. -- A visibilidade do método não é especificada para interfaces, pois são sempre públicas. -- Cada propriedade, valor de retorno e parâmetro deve ter um tipo especificado. Por outro lado, nunca especificamos o tipo para constantes finais (`final const`), pois é óbvio. -- Aspas simples devem ser usadas para delimitar strings, exceto quando o próprio literal contém apóstrofos. - - -Convenções de Nomenclatura -========================== - -- Não use abreviações, a menos que o nome completo seja muito longo. -- Use letras maiúsculas para abreviações de duas letras, Pascal/CamelCase para abreviações mais longas. -- Use um substantivo ou frase nominal para o nome da classe. -- Os nomes das classes devem conter não apenas a especificidade (`Array`), mas também a generalidade (`ArrayIterator`). Exceções são atributos da linguagem PHP. -- "Constantes de classe e enums devem usar PascalCaps":https://blog.nette.org/en/less-noise-in-code. -- "Interfaces e classes abstratas não devem conter prefixos ou sufixos":https://blog.nette.org/pt/prefixes-and-suffixes-do-not-belong-in-interface-names como `Abstract`, `Interface` ou `I`. - - -Quebra de Linha e Chaves -======================== - -Os Padrões de Codificação Nette correspondem ao PSR-12 (ou PER Coding Style), em alguns pontos complementam-no ou modificam-no: - -- arrow functions são escritas sem espaço antes do parêntese, ou seja, `fn($a) => $b` -- não é necessária uma linha em branco entre diferentes tipos de declarações de importação `use` -- o tipo de retorno da função/método e a chave de abertura `{` estão sempre em linhas separadas: - -```php - public function find( - string $dir, - array $options, - ): array - { - // corpo do método - } -``` - -A chave de abertura `{` numa linha separada é importante para a separação visual da assinatura da função/método do corpo. Se a assinatura estiver numa única linha, a separação é clara (imagem à esquerda). Se estiver em várias linhas, no PSR as assinaturas e o corpo fundem-se (meio), enquanto no padrão Nette permanecem separados (direita): - -[* new-line-after.webp *] - - -Blocos de Documentação (phpDoc) -=============================== - -Regra principal: Nunca duplique informações da assinatura, como o tipo do parâmetro ou o tipo de retorno, sem adicionar valor (por exemplo, uma descrição). - -Bloco de documentação para definição de classe: - -- Começa com a descrição da classe. -- Seguido por uma linha em branco. -- Seguem-se as anotações `@property` (ou `@property-read`, `@property-write`), uma por linha. A sintaxe é: anotação, espaço, tipo, espaço, `$nome`. -- Seguem-se as anotações `@method`, uma por linha. A sintaxe é: anotação, espaço, tipo de retorno, espaço, `nome(tipo $param, ...)`. -- A anotação `@author` é omitida. A autoria é mantida no histórico do código-fonte. -- Podem ser usadas as anotações `@internal` ou `@deprecated`. - -```php -/** - * Parte da mensagem MIME. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Um bloco de documentação para uma propriedade, que contém apenas a anotação `@var`, deve ser de linha única: - -```php -/** @var string[] */ -private array $name; -``` - -Bloco de documentação para definição de método: - -- Começa com uma breve descrição do método. -- Sem linha em branco entre a descrição e as anotações. -- Anotações `@param`, uma por linha. -- Anotação `@return`. -- Anotações `@throws`, uma por linha. -- Podem ser usadas as anotações `@internal` ou `@deprecated`. - -Cada anotação (`@return`, `@throws`, etc.) é seguida por um espaço. A exceção é `@param`, que é seguida por dois espaços para melhor legibilidade. - -```php -/** - * Encontra um arquivo no diretório. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulações em Vez de Espaços -============================ - -As tabulações têm várias vantagens sobre os espaços: - -- o tamanho do recuo pode ser ajustado em editores e na "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- não impõem a preferência de tamanho de indentação do utilizador ao código, tornando o código mais portátil -- podem ser digitadas com um único toque de tecla (em qualquer lugar, não apenas em editores que convertem tabulações em espaços) -- a indentação é o seu propósito -- respeitam as necessidades de colegas com deficiência visual e cegos - -Ao usar tabulações nos nossos projetos, permitimos o ajuste da largura, o que pode parecer supérfluo para a maioria das pessoas, mas é essencial para pessoas com deficiência visual. - -Para programadores cegos que usam displays Braille, cada espaço representa uma célula Braille. Portanto, se a indentação padrão for de 4 espaços, uma indentação de 3º nível desperdiça 12 valiosas células Braille antes mesmo do início do código. Num display de 40 células, que é o mais comum em portáteis, isso representa mais de um quarto das células disponíveis sendo desperdiçadas sem qualquer informação. - - -{{priority: -1}} diff --git a/contributing/pt/documentation.texy b/contributing/pt/documentation.texy deleted file mode 100644 index 4cf9f50cbb..0000000000 --- a/contributing/pt/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Como Contribuir para a Documentação -*********************************** - -.[perex] -Contribuir para a documentação é uma das atividades mais gratificantes, pois ajuda outros a entender o framework. - - -Como Escrever? --------------- - -A documentação destina-se principalmente a pessoas que estão a familiarizar-se com o tópico. Portanto, deve cumprir vários pontos importantes: - -- Comece pelo simples e geral. Avance para tópicos mais complexos apenas no final. -- Tente explicar o assunto da melhor forma possível. Por exemplo, tente explicar primeiro o tópico a um colega. -- Forneça apenas as informações que o utilizador realmente precisa saber sobre o tópico em questão. -- Verifique se as suas informações são realmente verdadeiras. Teste cada trecho de código. -- Seja conciso - reduza o que escreveu pela metade. E depois, se necessário, novamente. -- Use com moderação todos os tipos de destaque, desde negrito até caixas como `.[note]`. -- No código, siga os [Padrões de Codificação |coding-standard]. - -Aprenda também a [sintaxe |syntax]. Para pré-visualizar o artigo enquanto o escreve, pode usar o [editor com pré-visualização |https://editor.nette.org/]. - - -Versões de Idioma ------------------ - -O idioma principal é o inglês. As suas alterações devem ser, portanto, em inglês. Se o inglês não for o seu forte, use o [DeepL Translator |https://www.deepl.com/translator] e outros irão rever o seu texto. - -A tradução para outros idiomas será feita automaticamente após a aprovação e ajuste da sua modificação. - - -Modificações Triviais ---------------------- - -Para contribuir para a documentação, é essencial ter uma conta no [GitHub|https://github.com]. - -A forma mais fácil de fazer uma pequena alteração na documentação é usar os links no final de cada página: - -- *Mostrar no GitHub* abre a versão do código-fonte da página no GitHub. Depois, basta pressionar o botão `E` para começar a editar (é necessário estar autenticado no GitHub). -- *Abrir pré-visualização* abre o editor, onde pode ver imediatamente a aparência visual resultante. - -Como o [editor com pré-visualização |https://editor.nette.org/] não tem a opção de guardar alterações diretamente no GitHub, é necessário, após concluir as edições, copiar o texto fonte para a área de transferência (botão *Copy to clipboard*) e depois colá-lo no editor do GitHub. Abaixo do campo de edição existe um formulário para envio. Não se esqueça de resumir brevemente e explicar o motivo da sua modificação. Após o envio, é criado um chamado pull request (PR), que pode ser editado posteriormente. - - -Modificações Maiores --------------------- - -Mais adequado do que usar a interface do GitHub é estar familiarizado com os fundamentos do trabalho com o sistema de controlo de versões Git. Se não domina o trabalho com o Git, pode consultar o guia [git - the simple guide |https://rogerdudler.github.io/git-guide/] e, opcionalmente, usar um dos muitos [clientes gráficos |https://git-scm.com/downloads/guis]. - -Edite a documentação desta forma: - -1) No GitHub, crie um [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] do repositório [nette/docs |https://github.com/nette/docs]. -2) [Clone |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] este repositório para o seu computador. -3) Em seguida, no [branch apropriado |#Estrutura da Documentação], faça as alterações. -4) Verifique se há espaços em branco extras no texto usando a ferramenta [Code-Checker |code-checker:]. -5) Salve (commit) as alterações. -6) Se estiver satisfeito com as alterações, envie-as (push) para o GitHub no seu fork. -7) A partir daí, envie-as para o repositório `nette/docs` criando um [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -É comum receber comentários com sugestões. Acompanhe as alterações propostas e incorpore-as. Adicione as alterações propostas como novos commits e envie novamente para o GitHub. Nunca crie um novo pull request para modificar um pull request existente. - - -Estrutura da Documentação -------------------------- - -Toda a documentação está localizada no GitHub no repositório [nette/docs |https://github.com/nette/docs]. A versão atual está no branch `master`, versões mais antigas estão localizadas em branches como `doc-3.x`, `doc-2.x`. - -O conteúdo de cada branch é dividido em pastas principais que representam as diferentes áreas da documentação. Por exemplo, `application/` corresponde a `https://doc.nette.org/pt/application`, `latte/` corresponde a `https://latte.nette.org`, etc. Cada uma destas pastas contém subpastas que representam as versões de idioma (`pt`, `en`, `cs`, ...) e, opcionalmente, a subpasta `files` com imagens que podem ser inseridas nas páginas da documentação. diff --git a/contributing/pt/syntax.texy b/contributing/pt/syntax.texy deleted file mode 100644 index c820a98389..0000000000 --- a/contributing/pt/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Sintaxe da Documentação -*********************** - -A documentação usa Markdown e a [sintaxe Texy |https://texy.info/cs/syntax] com algumas extensões. - - -Links -===== - -Para links internos, utiliza-se a notação em colchetes `[...]`. Seja na forma com barra vertical `[texto do link |destino do link]`, ou abreviada `[texto do link]`, se o destino for idêntico ao texto (após transformação para minúsculas e hífens): - -- `[Page name|Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[texto do link |Page name]` -> `<a href="/en/page-name">link text</a>` - -Podemos criar links para uma versão de idioma diferente ou para uma seção diferente. Uma seção significa uma biblioteca Nette (por exemplo, `forms`, `latte`, etc.) ou seções especiais como `best-practices`, `quickstart`, etc.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (mesma seção, idioma diferente) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (seção diferente, mesmo idioma) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (seção e idioma diferentes) - -Usando `#`, também é possível direcionar para um título específico na página. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (título na página atual) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Link para a página inicial da seção: (`@home` é uma expressão especial para a página inicial da seção) - -- `[texto do link |@home]` -> `<a href="/en/">link text</a>` -- `[texto do link |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Links para a Documentação da API --------------------------------- - -Utilize sempre apenas esta notação: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Use nomes totalmente qualificados apenas na primeira menção. Para links subsequentes, use o nome simplificado: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Links para a Documentação do PHP --------------------------------- - -- `[php:substr]` -> [php:substr] - - -Código-Fonte -============ - -Um bloco de código começa com ` ```lang ` e termina com ` ``` `. Os idiomas suportados são `php`, `latte`, `neon`, `html`, `css`, `js` e `sql`. Use sempre tabulações para a indentação. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Também pode especificar o nome do arquivo como ` ```php .{file: ArrayTest.php} ` e o bloco de código será renderizado desta forma: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Títulos -======= - -Sublinhe o título mais alto (ou seja, o nome da página) com asteriscos (`***`). Use sinais de igual (`===`) para separar secções principais. Sublinhe os títulos de nível inferior com sinais de igual (`===`) e depois com hífens (`---`): - -``` -Aplicações MVC & Presenters -*************************** -... - - -Criação de Links -================ -... - - -Links em Templates ------------------- -... -``` - - -Caixas e Estilos -================ - -Marcamos o perex com a classe `.[perex]` .[perex] - -Marcamos uma nota com a classe `.[note]` .[note] - -Marcamos uma dica com a classe `.[tip]` .[tip] - -Marcamos um aviso com a classe `.[caution]` .[caution] - -Marcamos um aviso mais forte com a classe `.[warning]` .[warning] - -Número da versão `.{data-version:2.4.10}` .{data-version:2.4.10} - -Escreva as classes antes da linha: - -``` -.[perex] -Este é o perex. -``` - -Por favor, esteja ciente de que caixas como `.[tip]` chamam a atenção, portanto, são usadas para enfatizar, e não para informações menos importantes. Use-as com moderação. - - -Sumário -======= - -O sumário (links no menu direito) é gerado automaticamente para todas as páginas cujo tamanho exceda 4 000 bytes. Este comportamento padrão pode ser modificado usando a [meta tag |#Meta Tags] `{{toc}}`. O texto que forma o sumário é retirado por padrão diretamente do texto dos títulos, mas usando o modificador `.{toc}`, é possível exibir um texto diferente no sumário, o que é útil principalmente para títulos mais longos. - -``` - - -Título longo e inteligente .{toc: Qualquer outro texto exibido no sumário} -========================================================================== -``` - - -Meta Tags -========= - -- definir um título de página personalizado (em `<title>` e na navegação breadcrumb) `{{title: Outro título}}` -- redirecionamento `{{redirect: pla:cs}}` - veja [#Links] -- forçar `{{toc}}` ou desabilitar `{{toc: no}}` o sumário automático (caixa com links para títulos individuais) - -{{priority: -1}} diff --git a/contributing/ro/@home.texy b/contributing/ro/@home.texy deleted file mode 100644 index aa47120625..0000000000 --- a/contributing/ro/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Deveniți un contribuitor Nette -****************************** - -.[perex] -Aflați cum vă puteți implica în proiectul nostru open source. Însușiți-vă procedurile pentru contribuția la codul sursă și documentație și deveniți parte a comunității de dezvoltatori care participă activ la îmbunătățirea Nette. - - -**Cod** - -- [Cum să contribuiți la cod? |code] -- [Standard de codificare |coding-standard] - -**Documentație** - -- [Cum să contribuiți la documentație? |documentation] -- [Sintaxa documentației |syntax] -- "Editor de previzualizare":https://editor.nette.org diff --git a/contributing/ro/@left-menu.texy b/contributing/ro/@left-menu.texy deleted file mode 100644 index ddea62842c..0000000000 --- a/contributing/ro/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Cod -*** -- [Cum să contribuiți la cod? |code] -- [Standard de codificare |coding-standard] - -Documentație -************ -- [Cum să contribuiți la documentație? |documentation] -- [Sintaxa documentației |syntax] -- "Editor de previzualizare":https://editor.nette.org diff --git a/contributing/ro/code.texy b/contributing/ro/code.texy deleted file mode 100644 index 16fade07ae..0000000000 --- a/contributing/ro/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Cum să contribuiți la cod -************************* - -.[perex] -Vă pregătiți să contribuiți la Nette Framework și aveți nevoie să vă orientați în reguli și proceduri? Acest ghid pentru începători vă va arăta pas cu pas cum să contribuiți eficient la cod, să lucrați cu depozite și să implementați modificări. - - -Procedura -========= - -Pentru a contribui la cod este necesar să aveți un cont pe [GitHub |https://github.com] și să fiți familiarizat cu elementele de bază ale lucrului cu sistemul de versionare Git. Dacă nu stăpâniți lucrul cu Git, puteți consulta ghidul [git - the simple guide |https://rogerdudler.github.io/git-guide/] și eventual să utilizați unul dintre multele [clienți grafici |https://git-scm.com/downloads/guis]. - - -Pregătirea mediului și a depozitului ------------------------------------- - -1) pe GitHub creați un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] al depozitului [pachetului |www:packages], pe care urmează să-l modificați -2) [clonați |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] acest depozit pe computerul dvs. -3) instalați dependențele, inclusiv [Nette Tester |tester:], folosind comanda `composer install` -4) verificați dacă testele funcționează, rulând `composer tester` -5) creați o [nouă ramură |#Ramură nouă] bazată pe ultima versiune lansată - - -Implementarea propriilor modificări ------------------------------------ - -Acum puteți efectua propriile modificări de cod: - -1) programați modificările dorite și nu uitați de teste -2) asigurați-vă că testele rulează cu succes, folosind `composer tester` -3) verificați dacă codul respectă [standardul de codificare |#Standarde de codificare] -4) salvați modificările (commit) cu o descriere în [acest format |#Descrierea commit-ului] - -Puteți crea mai multe commit-uri, unul pentru fiecare pas logic. Fiecare commit ar trebui să aibă sens de sine stătător. - - -Trimiterea modificărilor ------------------------- - -Odată ce sunteți mulțumit de modificări, le puteți trimite: - -1) trimiteți (push) modificările pe GitHub în fork-ul dvs. -2) de acolo le trimiteți către depozitul Nette creând un [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) -3) furnizați în descriere [suficiente informații |#Descrierea pull request-ului] - - -Incorporarea comentariilor --------------------------- - -Commit-urile dvs. vor fi acum vizibile și pentru alții. Este obișnuit să primiți comentarii cu observații: - -1) urmăriți modificările propuse -2) încorporați-le ca noi commit-uri sau [combinați-le cu cele anterioare |https://help.github.com/en/github/using-git/about-git-rebase] -3) retrimiteți commit-urile pe GitHub și acestea vor apărea automat în pull request - -Nu creați niciodată un nou pull request pentru a modifica unul existent. - - -Documentație ------------- - -Dacă ați modificat funcționalitatea sau ați adăugat una nouă, nu uitați să o [adăugați și în documentație |documentation]. - - -Ramură nouă -=========== - -Dacă este posibil, efectuați modificările față de ultima versiune lansată, adică ultimul tag din ramura respectivă. Pentru tag-ul `v3.2.1` creați o ramură cu această comandă: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Standarde de codificare -======================= - -Codul dvs. trebuie să respecte [standardul de codificare |coding-standard] utilizat în Nette Framework. Pentru verificarea și corectarea codului este disponibil un instrument automat. Acesta poate fi instalat prin Composer **global** în directorul ales de dvs.: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Acum ar trebui să puteți rula instrumentul în terminal. Prima comandă verifică și a doua corectează codul din directoarele `src` și `tests` din directorul curent: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Descrierea commit-ului -====================== - -În Nette, subiectele commit-urilor au formatul: `Presenter: fixed AJAX detection [Closes #69]` - -- zona urmată de două puncte -- scopul commit-ului la timpul trecut, dacă este posibil, începeți cu cuvântul: "added .(proprietate nouă adăugată)", "fixed .(corecție)", "refactored .(modificare în cod fără schimbarea comportamentului)", changed, removed -- dacă commit-ul întrerupe compatibilitatea inversă, adăugați "BC break" -- eventuală legătură cu issue tracker-ul precum `(#123)` sau `[Closes #69]` -- după subiect poate urma o linie goală și apoi o descriere mai detaliată, inclusiv, de exemplu, linkuri către forum - - -Descrierea pull request-ului -============================ - -La crearea unui pull request, interfața GitHub vă permite să introduceți un titlu și o descriere. Furnizați un titlu descriptiv și în descriere oferiți cât mai multe informații despre motivele modificării dvs. - -Se va afișa și un antet, unde specificați dacă este vorba despre o nouă funcție sau o corecție de eroare și dacă poate apărea o întrerupere a compatibilității inverse (BC break). Dacă există o problemă (issue) asociată, faceți referire la ea, astfel încât să fie închisă după aprobarea pull request-ului. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/ro/coding-standard.texy b/contributing/ro/coding-standard.texy deleted file mode 100644 index a933882b59..0000000000 --- a/contributing/ro/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Standard de codificare -********************** - -.[perex] -Acest document descrie regulile și recomandările pentru dezvoltarea Nette. Atunci când contribuiți cu cod la Nette, trebuie să le respectați. Cea mai simplă modalitate de a face acest lucru este să imitați codul existent. Ideea este ca tot codul să arate ca și cum ar fi fost scris de o singură persoană. - -Standardul de codificare Nette corespunde [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] cu două excepții principale: pentru indentare folosește [#Tabulatori în loc de spații] în loc de spații și pentru [constantele de clasă folosește PascalCase |https://blog.nette.org/en/a-bit-less-screaming-in-code]. - - -Reguli generale -=============== - -- Fiecare fișier PHP trebuie să conțină `declare(strict_types=1)` -- Două rânduri goale sunt folosite pentru a separa metodele pentru o mai bună lizibilitate. -- Motivul utilizării operatorului shut-up trebuie documentat: `@mkdir($dir); // @ - directorul poate exista`. -- Dacă este utilizat un operator de comparație slab tipizat (adică `==`, `!=`, ...), intenția trebuie documentată: `// == acceptă null` -- Într-un singur fișier `exceptions.php` puteți scrie mai multe excepții. -- Pentru interfețe nu se specifică vizibilitatea metodelor, deoarece sunt întotdeauna publice. -- Fiecare proprietate, valoare returnată și parametru trebuie să aibă tipul specificat. În schimb, la constantele finale nu specificăm niciodată tipul, deoarece este evident. -- Pentru delimitarea șirurilor de caractere ar trebui folosite ghilimele simple, cu excepția cazurilor în care literalul însuși conține apostrofuri. - - -Convenții de denumire -===================== - -- Nu utilizați abrevieri, cu excepția cazului în care numele complet este prea lung. -- Pentru abrevierile de două litere utilizați majuscule, pentru abrevierile mai lungi pascal/camel case. -- Pentru numele clasei utilizați un substantiv sau o sintagmă. -- Numele claselor trebuie să conțină nu numai specificitatea (`Array`), ci și generalitatea (`ArrayIterator`). Excepție fac atributele limbajului PHP. -- "Constantele de clasă și enum-urile ar trebui să utilizeze PascalCaps":https://blog.nette.org/en/a-bit-less-screaming-in-code. -- "Interfețele și clasele abstracte nu ar trebui să conțină prefixe sau sufixe":https://blog.nette.org/ro/prefixes-and-suffixes-do-not-belong-in-interface-names precum `Abstract`, `Interface` sau `I`. - - -Wrapping and Braces -=================== - -Standardul de codificare Nette corespunde PSR-12 (respectiv PER Coding Style), în unele puncte îl completează sau îl modifică: - -- funcțiile arrow se scriu fără spațiu înainte de paranteză, adică `fn($a) => $b` -- nu se cere un rând gol între diferite tipuri de `use` import statements -- tipul returnat al funcției/metodei și acolada de deschidere sunt întotdeauna pe rânduri separate: - -```php - public function find( - string $dir, - array $options, - ): array - { - // corpul metodei - } -``` - -Acolada de deschidere pe un rând separat este importantă pentru separarea vizuală a semnăturii funcției/metodei de corp. Dacă semnătura este pe un singur rând, separarea este clară (imaginea din stânga), dacă este pe mai multe rânduri, în PSR semnăturile și corpurile se contopesc (mijloc), în timp ce în standardul Nette sunt în continuare separate (dreapta): - -[* new-line-after.webp *] - - -Blocuri de documentație (phpDoc) -================================ - -Regula principală: Nu duplicați niciodată informații în semnătură, cum ar fi tipul parametrului sau tipul returnat, fără valoare adăugată. - -Blocul de documentație pentru definirea clasei: - -- Începe cu descrierea clasei. -- Urmează un rând gol. -- Urmează adnotările `@property` (sau `@property-read`, `@property-write`), una după alta. Sintaxa este: adnotare, spațiu, tip, spațiu, $nume. -- Urmează adnotările `@method`, una după alta. Sintaxa este: adnotare, spațiu, tip returnat, spațiu, nume(tip $param, ...). -- Adnotarea `@author` se omite. Autoritatea este păstrată în istoricul codului sursă. -- Se pot utiliza adnotările `@internal` sau `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Blocul de documentație pentru o proprietate, care conține doar adnotarea `@var`, ar trebui să fie pe un singur rând: - -```php -/** @var string[] */ -private array $name; -``` - -Blocul de documentație pentru definirea metodei: - -- Începe cu o scurtă descriere a metodei. -- Niciun rând gol. -- Adnotările `@param` pe rânduri separate. -- Adnotarea `@return`. -- Adnotările `@throws`, una după alta. -- Se pot utiliza adnotările `@internal` sau `@deprecated`. - -După fiecare adnotare urmează un singur spațiu, cu excepția `@param`, după care, pentru o mai bună lizibilitate, urmează două spații. - -```php -/** - * Găsește un fișier în director. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulatori în loc de spații -=========================== - -Tabulatorii au mai multe avantaje față de spații: - -- dimensiunea indentării poate fi personalizată în editoare și pe "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- nu impun codului preferința utilizatorului privind dimensiunea indentării, astfel încât codul este mai portabil -- pot fi scrise cu o singură apăsare de tastă (oriunde, nu doar în editoarele care transformă tabulatorii în spații) -- indentarea este scopul lor -- respectă nevoile colegilor cu deficiențe de vedere și nevăzători - -Utilizând tabulatori în proiectele noastre, permitem personalizarea lățimii, ceea ce poate părea o inutilitate pentru majoritatea oamenilor, dar este esențială pentru persoanele cu deficiențe de vedere. - -Pentru programatorii nevăzători care utilizează afișaje Braille, fiecare spațiu reprezintă o celulă Braille. Deci, dacă indentarea implicită este de 4 spații, indentarea de nivel 3 irosește 12 celule Braille valoroase chiar înainte de începutul codului. Pe un afișaj de 40 de celule, care este cel mai frecvent utilizat la laptopuri, aceasta reprezintă mai mult de un sfert din celulele disponibile, care sunt irosite fără nicio informație. - - -{{priority: -1}} diff --git a/contributing/ro/documentation.texy b/contributing/ro/documentation.texy deleted file mode 100644 index c51ce18e62..0000000000 --- a/contributing/ro/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Cum să contribuiți la documentație -********************************** - -.[perex] -Contribuția la documentație este una dintre cele mai benefice activități, deoarece îi ajutați pe alții să înțeleagă framework-ul. - - -Cum să scrieți? ---------------- - -Documentația este destinată în principal persoanelor care se familiarizează cu subiectul. Prin urmare, ar trebui să îndeplinească câteva puncte importante: - -- Începeți de la simplu și general. Treceți la subiecte mai avansate abia la sfârșit. -- Încercați să explicați lucrul cât mai bine posibil. Încercați, de exemplu, să explicați mai întâi subiectul unui coleg. -- Furnizați doar informațiile de care utilizatorul are nevoie cu adevărat pentru subiectul respectiv. -- Verificați dacă informațiile dvs. sunt într-adevăr adevărate. Testați fiecare cod. -- Fiți concis - scurtați ceea ce scrieți la jumătate. Și apoi, dacă este necesar, încă o dată. -- Economisiți evidențiatoarele de orice fel, de la text îngroșat la cadre precum `.[note]`. -- În coduri respectați [Coding Standard |coding-standard]. - -Însușiți-vă și [sintaxa |syntax]. Pentru previzualizarea articolului în timpul scrierii, puteți utiliza [editorul cu previzualizare |https://editor.nette.org/]. - - -Versiuni lingvistice --------------------- - -Limba principală este engleza, modificările dvs. ar trebui deci să fie și în engleză. Dacă engleza nu este punctul dvs. forte, utilizați [DeepL Translator |https://www.deepl.com/translator] și ceilalți vă vor verifica textul. - -Traducerea în celelalte limbi va fi efectuată automat după aprobarea și finisarea modificării dvs. - - -Modificări triviale -------------------- - -Pentru a contribui la documentație este necesar să aveți un cont pe [GitHub |https://github.com]. - -Cel mai simplu mod de a efectua o mică modificare în documentație este să utilizați linkurile de la sfârșitul fiecărei pagini: - -- *Arată pe GitHub* deschide forma sursă a paginii respective pe GitHub. Apoi este suficient să apăsați butonul `E` și puteți începe editarea (este necesar să fiți autentificat pe GitHub). -- *Deschide previzualizarea* deschide editorul, unde vedeți imediat și forma vizuală finală. - -Deoarece [editorul cu previzualizare |https://editor.nette.org/] nu are posibilitatea de a salva modificările direct pe GitHub, este necesar ca după finalizarea modificărilor să copiați textul sursă în clipboard (cu butonul *Copy to clipboard*) și apoi să-l lipiți în editorul de pe GitHub. Sub câmpul de editare se află formularul de trimitere. Aici nu uitați să rezumați pe scurt și să explicați motivul modificării dvs. După trimitere se creează așa-numitul pull request (PR), care poate fi editat ulterior. - - -Modificări mai mari -------------------- - -Mai potrivit decât utilizarea interfeței GitHub este să fiți familiarizat cu elementele de bază ale lucrului cu sistemul de versionare Git. Dacă nu stăpâniți lucrul cu Git, puteți consulta ghidul [git - the simple guide |https://rogerdudler.github.io/git-guide/] și eventual să utilizați unul dintre multele [clienți grafici |https://git-scm.com/downloads/guis]. - -Modificați documentația în acest mod: - -1) pe GitHub creați un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] al depozitului [nette/docs |https://github.com/nette/docs] -2) [clonați |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] acest depozit pe computerul dvs. -3) apoi în [ramura corespunzătoare |#Structura documentației] efectuați modificările -4) verificați spațiile în exces din text folosind instrumentul [Code-Checker |code-checker:] -4) salvați modificările (commit) -6) dacă sunteți mulțumit de modificări, trimiteți-le (push) pe GitHub în fork-ul dvs. -7) de acolo le trimiteți către depozitul `nette/docs` creând un [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) - -Este obișnuit să primiți comentarii cu observații. Urmăriți modificările propuse și încorporați-le. Adăugați modificările propuse ca noi commit-uri și retrimiteți-le pe GitHub. Nu creați niciodată un nou pull request pentru a modifica unul existent. - - -Structura documentației ------------------------ - -Întreaga documentație este găzduită pe GitHub în depozitul [nette/docs |https://github.com/nette/docs]. Versiunea curentă este în master, versiunile mai vechi sunt plasate în ramuri precum `doc-3.x`, `doc-2.x`. - -Conținutul fiecărei ramuri este împărțit în directoare principale reprezentând domeniile individuale ale documentației. De exemplu, `application/` corespunde https://doc.nette.org/ro/application, `latte/` corespunde https://latte.nette.org etc. Fiecare dintre aceste directoare conține subdirectoare reprezentând versiunile lingvistice (`cs`, `en`, ...) și eventual subdirectorul `files` cu imagini, care pot fi inserate în paginile din documentație. diff --git a/contributing/ro/syntax.texy b/contributing/ro/syntax.texy deleted file mode 100644 index dd1a95cf97..0000000000 --- a/contributing/ro/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Sintaxa documentației -********************* - -Documentația utilizează Markdown & [sintaxa Texy |https://texy.info/en/syntax] cu unele extensii. - - -Linkuri -======= - -Pentru linkurile interne se utilizează notația în paranteze drepte `[link]`. Fie în forma cu bară verticală `[text link |țintă link]`, fie prescurtat `[text link]`, dacă ținta este identică cu textul (după transformarea în litere mici și cratime): - -- `[Page name]` -> `<a href="/ro/page-name">Page name</a>` -- `[text link |Page name]` -> `<a href="/ro/page-name">text link</a>` - -Putem face link către o altă versiune lingvistică sau către o altă secțiune. Prin secțiune se înțelege o bibliotecă Nette (de ex. `forms`, `latte`, etc.) sau secțiuni speciale precum `best-practices`, `quickstart` etc.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (aceeași secțiune, altă limbă) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/ro/page-name">Page name</a>` (altă secțiune, aceeași limbă) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (altă secțiune și limbă) - -Folosind `#` este de asemenea posibil să țintim un anumit titlu de pe pagină. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (titlu pe pagina curentă) -- `[Page name#Heading]` -> `<a href="/ro/page-name#toc-heading">Page name</a>` - -Link către pagina de start a secțiunii: (`@home` este o expresie specială pentru pagina de start a secțiunii) - -- `[link text |@home]` -> `<a href="/ro/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/ro/">link text</a>` - - -Linkuri către documentația API ------------------------------- - -Specificați întotdeauna doar folosind această notație: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Utilizați nume complet calificate doar la prima mențiune. Pentru linkurile ulterioare utilizați numele simplificat: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Linkuri către documentația PHP ------------------------------- - -- `[php:substr]` -> [php:substr] - - -Cod sursă -========= - -Blocul de cod începe cu <code>```lang</code> și se termină cu <code>```</code>. Limbajele suportate sunt `php`, `latte`, `neon`, `html`, `css`, `js` și `sql`. Pentru indentare utilizați întotdeauna tabulatori. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Puteți specifica și numele fișierului ca <code>```php .{file: ArrayTest.php}</code> și blocul de cod se va reda în acest mod: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Titluri -======= - -Titlul cel mai înalt (adică numele paginii) subliniați-l cu asteriscuri. Pentru separarea secțiunilor utilizați semne de egal. Subliniați titlurile cu semne de egal și apoi cu cratime: - -``` -Aplicații MVC & presenteri -************************** -... - - -Crearea linkurilor -================== -... - - -Linkuri în șabloane -------------------- -... -``` - - -Cadre și stiluri -================ - -Perexul îl marcăm cu clasa `.[perex]` .[perex] - -Nota o marcăm cu clasa `.[note]` .[note] - -Sfatul îl marcăm cu clasa `.[tip]` .[tip] - -Avertismentul îl marcăm cu clasa `.[caution]` .[caution] - -Avertismentul mai accentuat îl marcăm cu clasa `.[warning]` .[warning] - -Numărul versiunii `.{data-version:2.4.10}` .{data-version:2.4.10} - -Scrieți clasele înainte de rând: - -``` -.[perex] -Acesta este perexul. -``` - -Vă rugăm să rețineți că cadrele precum `.[tip]` "atrag" ochii, deci se utilizează pentru accentuare, nu pentru informații mai puțin importante. Prin urmare, utilizați-le cu maximă economie. - - -Cuprins -======= - -Cuprinsul (linkurile din meniul din dreapta) este generat automat pentru toate paginile a căror dimensiune depășește 4 000 de octeți, acest comportament implicit putând fi modificat folosind [#Meta tag-uri] `{{toc}}`. Textul care formează cuprinsul este preluat standard direct din textul titlurilor, dar folosind modificatorul `.{toc}` este posibil să se afișeze în cuprins un alt text, ceea ce este util în special pentru titlurile mai lungi. - -``` - - -Titlu lung și inteligent .{toc: Orice alt text afișat în cuprins} -================================================================= -``` - - -Meta tag-uri -============ - -- setarea unui nume personalizat pentru pagină (în `<title>` și navigarea breadcrumb) `{{title: Alt nume}}` -- redirecționare `{{redirect: pla:cs}}` - vezi [#Linkuri] -- forțarea `{{toc}}` sau interzicerea `{{toc: no}}` cuprinsului automat (căsuța cu linkuri către titlurile individuale) - -{{priority: -1}} diff --git a/contributing/ru/@home.texy b/contributing/ru/@home.texy index 2714687ce7..256a114e1e 100644 --- a/contributing/ru/@home.texy +++ b/contributing/ru/@home.texy @@ -1,17 +1,17 @@ -Станьте контрибьютором Nette -**************************** +Станьте участником Nette +************************ .[perex] -Узнайте, как вы можете принять участие в нашем open source проекте. Освойте процедуры внесения вклада в исходный код и документацию и станьте частью сообщества разработчиков, активно участвующих в совершенствовании Nette. +Узнайте, как вы можете принять участие в нашем открытом проекте. Познакомьтесь с порядком внесения вклада в исходный код и документацию и станьте частью сообщества разработчиков, которые активно участвуют в улучшении Nette. **Код** -- [Как внести вклад в код? |code] -- [Стандарт кодирования |coding-standard] +- [Вклад в код |code] +- [Стандарты кодирования |coding-standard] **Документация** -- [Как внести вклад в документацию? |documentation] +- [Вклад в документацию |documentation] - [Синтаксис документации |syntax] -- "Редактор предварительного просмотра":https://editor.nette.org +- "Редактор с предпросмотром":https://editor.nette.org diff --git a/contributing/ru/@left-menu.texy b/contributing/ru/@left-menu.texy index 97c6e23d70..18544a5ab0 100644 --- a/contributing/ru/@left-menu.texy +++ b/contributing/ru/@left-menu.texy @@ -1,10 +1,18 @@ Код *** -- [Как внести вклад в код? |code] -- [Стандарт кодирования |coding-standard] +- [Как внести вклад в код |code] +- [Стандарты кодирования |coding-standard] Документация ************ -- [Как внести вклад в документацию? |documentation] +- [Как внести вклад в документацию |documentation] - [Синтаксис документации |syntax] -- "Редактор предварительного просмотра":https://editor.nette.org +- "Редактор предпросмотра":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 index b6ed03248e..cd82375ec0 100644 --- a/contributing/ru/code.texy +++ b/contributing/ru/code.texy @@ -1,71 +1,71 @@ -Как внести вклад в код -********************** +Вклад в код +*********** .[perex] -Вы собираетесь внести свой вклад в Nette Framework и вам нужно разобраться в правилах и процедурах? Это руководство для начинающих шаг за шагом покажет вам, как эффективно вносить вклад в код, работать с репозиториями и внедрять изменения. +Собираетесь внести вклад в Nette Framework и вам нужно познакомиться с правилами и порядком работы? Это руководство для начинающих шаг за шагом проведёт вас через то, как эффективно вносить код, работать с репозиториями и реализовывать изменения. -Процедура -========= +Порядок работы +============== -Для внесения вклада в код необходимо иметь учетную запись на [GitHub|https://github.com] и быть знакомым с основами работы с системой контроля версий Git. Если вы не владеете работой с Git, вы можете ознакомиться с руководством [git - простое руководство |https://rogerdudler.github.io/git-guide/] и, при необходимости, использовать один из множества [графических клиентов |https://git-scm.com/downloads/guis]. +Чтобы вносить код, необходимо иметь учётную запись на [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) На 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) Реализуйте нужные изменения и не забудьте о тестах +2) Убедитесь, что тесты успешно проходят, командой `composer tester` +3) Проверьте, отвечает ли код [стандартам кодирования |#Стандарты кодирования] +4) Сохраните (закоммитьте) изменения с описанием в [таком формате |#Описание коммита] -Вы можете создать несколько коммитов, по одному для каждого логического шага. Каждый коммит должен быть осмысленным сам по себе. +Вы можете создать несколько коммитов, по одному на каждый логический шаг. Каждый коммит должен быть осмысленным сам по себе. Отправка изменений ------------------ -Как только вы будете удовлетворены изменениями, вы можете их отправить: +Когда изменения вас устроят, вы можете их отправить: -1) отправьте (push) изменения на GitHub в ваш форк -2) оттуда отправьте их в репозиторий Nette, создав [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) укажите в описании [достаточно информации |#Описание pull request] +1) Отправьте изменения на 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 +1) Следите за предлагаемыми изменениями +2) Вносите их новыми коммитами или [объединяйте с предыдущими|https://help.github.com/en/github/using-git/about-git-rebase] +3) Снова отправьте коммиты на GitHub, и они автоматически появятся в pull request -Никогда не создавайте новый pull request для изменения существующего. +Никогда не создавайте новый pull request, чтобы изменить существующий. Документация ------------ -Если вы изменили функциональность или добавили новую, не забудьте также [добавить ее в документацию |documentation]. +Если вы изменили функциональность или добавили новую, не забудьте [добавить её и в документацию|documentation]. Новая ветка =========== -Если возможно, вносите изменения относительно последней выпущенной версии, т.е. последнего тега в данной ветке. Для тега `v3.2.1` вы создадите ветку этой командой: +По возможности вносите изменения против последней выпущенной версии, то есть последнего тега в ветке. Для тега `v3.2.1` создайте ветку такой командой: ```shell git checkout -b new_branch_name v3.2.1 @@ -75,38 +75,27 @@ git checkout -b new_branch_name v3.2.1 Стандарты кодирования ===================== -Ваш код должен соответствовать [стандарту кодирования |coding standard], используемому в Nette Framework. Для проверки и исправления кода доступен автоматический инструмент. Его можно установить через Composer **глобально** в выбранную вами папку: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Теперь вы должны иметь возможность запустить инструмент в терминале. Первой командой вы проверите, а второй — исправите код в папках `src` и `tests` в текущем каталоге: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` +Ваш код должен отвечать [стандарту кодирования|coding-standard], используемому в Nette Framework. Для проверки и автоматического исправления кода воспользуйтесь инструментом [Nette Coding Standard |tools:coding-standard], там же вы найдёте указания по установке и использованию. Описание коммита ================ -В Nette темы коммитов имеют формат: `Presenter: fixed AJAX detection [Closes #69]` +В Nette у темы коммита такой формат: `Presenter: fixed AJAX detection [Closes #69]` -- область, за которой следует двоеточие -- цель коммита в прошедшем времени, если возможно, начните со слова: "added .(добавлено новое свойство)", "fixed .(исправление)", "refactored .(изменение в коде без изменения поведения)", changed, removed -- если коммит нарушает обратную совместимость, добавьте "BC break" -- возможная связь с трекером issue, например `(#123)` или `[Closes #69]` -- за темой может следовать одна пустая строка, а затем более подробное описание, включая, например, ссылки на форум +- область, за ней двоеточие +- назначение коммита в прошедшем времени; по возможности начинайте словами: "added (новая возможность)", "fixed (исправление)", "refactored (изменение кода без изменения поведения)", "changed", "removed" +- если коммит ломает обратную совместимость, добавьте "BC break" +- любая ссылка на трекер задач, например `(#123)` или `[Closes #69]` +- после темы может идти пустая строка, а за ней более подробное описание, включающее, например, ссылки на форум Описание pull request ===================== -При создании pull request интерфейс GitHub позволит вам указать название и описание. Укажите информативное название и в описании предоставьте как можно больше информации о причинах вашего изменения. +При создании pull request интерфейс GitHub позволит вам ввести заголовок и описание. Дайте краткий заголовок и приведите в описании как можно больше сведений о причинах вашего изменения. -Также отобразится заголовок, где укажите, является ли это новой функцией или исправлением ошибки, и может ли произойти нарушение обратной совместимости (BC break). Если есть связанная проблема (issue), ссылайтесь на нее, чтобы она была закрыта после одобрения pull request. +Укажите в шапке и то, идёт ли речь о новой возможности или об исправлении ошибки и может ли это вызвать проблемы с обратной совместимостью (BC break). Если есть связанная задача, сошлитесь на неё, чтобы она закрылась при одобрении pull request. ``` - bug fix / new feature? <!-- #issue numbers, if any --> diff --git a/contributing/ru/coding-standard.texy b/contributing/ru/coding-standard.texy index 2a4a960d9f..671da0c6a1 100644 --- a/contributing/ru/coding-standard.texy +++ b/contributing/ru/coding-standard.texy @@ -2,43 +2,46 @@ ******************** .[perex] -Этот документ описывает правила и рекомендации для разработки Nette. При внесении вклада в код Nette вы должны их соблюдать. Самый простой способ сделать это — подражать существующему коду. Цель в том, чтобы весь код выглядел так, как будто его написал один человек. +Этот документ описывает правила и рекомендации для разработки Nette. Внося код в Nette, вы обязаны им следовать. Проще всего добиться этого, подражая существующему коду. Цель в том, чтобы весь код выглядел так, будто его написал один человек. -Стандарт кодирования Nette соответствует [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] с двумя основными исключениями: для отступов он использует [#Табуляции вместо пробелов] и для [констант классов использует PascalCase|https://blog.nette.org/ru/for-less-screaming-in-the-code]. +Стандарт кодирования 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); // @ - каталог может существовать`. -- Если используется оператор сравнения со слабой типизацией (т.е. `==`, `!=`, ...), намерение должно быть задокументировано: `// == принять null` -- В один файл `exceptions.php` можно записать несколько исключений. -- У интерфейсов не указывается видимость методов, так как они всегда публичные. -- Каждое свойство, возвращаемое значение и параметр должны иметь указанный тип. Напротив, у финальных констант тип никогда не указываем, так как он очевиден. -- Для обрамления строки следует использовать одинарные кавычки, за исключением случаев, когда сам литерал содержит апострофы. +- Для разделения методов ради лучшей читаемости используются две пустые строки +- Причина использования оператора подавления (`@`) должна быть задокументирована: `@mkdir($dir); // @ - directory may exist` +- Если используется оператор нестрогого сравнения (то есть `==`, `!=`, ...), намерение должно быть задокументировано: `// == to accept null` +- Несколько классов исключений можно записать в один файл с именем `exceptions.php`, а несколько перечислений - в `enums.php` +- У интерфейсов видимость методов не указывается, потому что они всегда публичные +- У каждого свойства, возвращаемого значения и параметра должен быть указан тип. И наоборот, у финальных констант тип мы никогда не указываем, потому что он очевиден +- Для ограничения строк следует использовать одинарные кавычки, кроме случаев, когда сам литерал содержит апострофы Соглашения об именовании ======================== -- Не используйте сокращения, если полное имя не слишком длинное. -- Для двухбуквенных аббревиатур используйте заглавные буквы, для более длинных аббревиатур — PascalCase/camelCase. -- Для имени класса используйте существительное или словосочетание. -- Имена классов должны содержать не только специфичность (`Array`), но и общность (`ArrayIterator`). Исключением являются атрибуты языка PHP. -- "Константы классов и перечисления должны использовать PascalCaps":https://blog.nette.org/ru/for-less-screaming-in-the-code. -- "Интерфейсы и абстрактные классы не должны содержать префиксы или суффиксы":https://blog.nette.org/ru/prefixes-and-suffixes-do-not-belong-in-interface-names, такие как `Abstract`, `Interface` или `I`. +- Избегайте сокращений, если только полное имя не окажется чрезмерным +- Для двухбуквенных сокращений используйте прописные буквы, а для более длинных - 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), в некоторых пунктах он его дополняет или изменяет: +Стандарт кодирования Nette соответствует PSR-12 (или PER Coding Style), но в некоторых пунктах уточняет или меняет его: -- стрелочные функции пишутся без пробела перед скобкой, т.е. `fn($a) => $b` -- не требуется пустая строка между различными типами импортов `use` -- возвращаемый тип функции/метода и открывающая фигурная скобка всегда находятся на отдельных строках: +- Стрелочные функции пишутся без пробела перед скобкой, то есть `fn($a) => $b` +- Пустая строка между разными видами импортов `use` не требуется +- Возвращаемый тип функции или метода и открывающая фигурная скобка всегда находятся на разных строках: ```php public function find( @@ -50,7 +53,7 @@ } ``` -Открывающая фигурная скобка на отдельной строке важна для визуального разделения сигнатуры функции/метода от тела. Если сигнатура находится на одной строке, разделение очевидно (рисунок слева), если на нескольких строках, в PSR сигнатуры и тела сливаются (посередине), в то время как в стандарте Nette они остаются разделенными (справа): +Открывающая фигурная скобка на отдельной строке важна для визуального отделения сигнатуры функции или метода от тела. Если сигнатура на одной строке, разделение очевидно (изображение слева). Если она на нескольких строках, в PSR сигнатура и тело сливаются (посередине), а в стандарте Nette остаются разделёнными (справа): [* new-line-after.webp *] @@ -58,16 +61,16 @@ Блоки документации (phpDoc) =========================== -Основное правило: Никогда не дублируйте никакую информацию в сигнатуре, такую как тип параметра или возвращаемый тип, без добавленной ценности. +Главное правило: **никогда не дублируйте** сведения из сигнатуры, такие как тип параметра или возвращаемый тип, если это не добавляет ценности. -Блок документации для определения класса: +Блок документации к определению класса: -- Начинается с описания класса. -- Следует пустая строка. -- Следуют аннотации `@property` (или `@property-read`, `@property-write`), одна за другой. Синтаксис: аннотация, пробел, тип, пробел, $имя. -- Следуют аннотации `@method`, одна за другой. Синтаксис: аннотация, пробел, возвращаемый тип, пробел, имя(тип $param, ...). -- Аннотация `@author` опускается. Авторство сохраняется в истории исходного кода. -- Можно использовать аннотации `@internal` или `@deprecated`. +- Начинается с описания класса +- Дальше пустая строка +- Дальше аннотации `@property` (или `@property-read`, `@property-write`), по одной на строку. Синтаксис: аннотация, пробел, тип, пробел, `$имя` +- Дальше аннотации `@method`, по одной на строку. Синтаксис: аннотация, пробел, возвращаемый тип, пробел, `имя(тип $параметр, ...)` +- Аннотация `@author` опускается. Авторство хранится в истории исходного кода +- Можно использовать аннотации `@internal` или `@deprecated` ```php /** @@ -80,23 +83,23 @@ */ ``` -Блок документации для свойства, содержащий только аннотацию `@var`, должен быть однострочным: +Блок документации к свойству, содержащий только аннотацию `@var`, должен быть на одной строке: ```php /** @var string[] */ private array $name; ``` -Блок документации для определения метода: +Блок документации к определению метода: -- Начинается с краткого описания метода. -- Нет пустой строки. -- Аннотации `@param` по отдельным строкам. -- Аннотация `@return`. -- Аннотации `@throws`, одна за другой. -- Можно использовать аннотации `@internal` или `@deprecated`. +- Начинается с краткого описания метода +- Без пустой строки +- Аннотации `@param`, по одной на строку +- Аннотация `@return` +- Аннотации `@throws`, по одной на строку +- Можно использовать аннотации `@internal` или `@deprecated` -За каждой аннотацией следует один пробел, за исключением `@param`, за которой для лучшей читаемости следуют два пробела. +За каждой аннотацией следует один пробел, кроме `@param`, за которой ради лучшей читаемости следуют два пробела. ```php /** @@ -109,20 +112,37 @@ 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 -- они не навязывают коду предпочтение пользователя в размере отступа, поэтому код лучше переносим -- их можно написать одним нажатием клавиши (где угодно, не только в редакторах, которые заменяют табуляции на пробелы) -- отступы — это их смысл -- они уважают потребности коллег с нарушениями зрения и незрячих +- Размер отступа настраивается в редакторах и в "вебе":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size +- Они не навязывают коду предпочтения автора по размеру отступа, благодаря чему код становится переносимее +- Их можно ввести одним нажатием клавиши (где угодно, а не только в редакторах, которые преобразуют табуляции в пробелы) +- Отступ - их прямое назначение +- Они учитывают нужды слабовидящих и слепых коллег -Используя табуляции в наших проектах, мы позволяем настраивать ширину, что большинству людей может показаться излишеством, но для людей с нарушениями зрения это необходимо. +Используя в наших проектах табуляции, мы даём возможность настроить ширину, что большинству людей может показаться ненужным, но для людей с нарушениями зрения это принципиально важно. -Для незрячих программистов, использующих дисплеи Брайля, каждый пробел представляет собой одну ячейку Брайля. Если, таким образом, отступ по умолчанию составляет 4 пробела, отступ 3-го уровня тратит 12 ценных ячеек Брайля еще до начала кода. На 40-ячеечном дисплее, который чаще всего используется в ноутбуках, это более четверти доступных ячеек, которые тратятся без какой-либо информации. +Для слепых программистов, использующих брайлевские дисплеи, каждый пробел означает одну брайлевскую ячейку. Так что если отступ по умолчанию равен 4 пробелам, отступ 3-го уровня тратит 12 ценных брайлевских ячеек ещё до начала кода. На дисплее из 40 ячеек, самом распространённом для ноутбуков, это больше четверти доступных ячеек, потраченных впустую и не несущих никаких сведений. {{priority: -1}} diff --git a/contributing/ru/documentation.texy b/contributing/ru/documentation.texy index dab56db27d..28182374e3 100644 --- a/contributing/ru/documentation.texy +++ b/contributing/ru/documentation.texy @@ -1,68 +1,68 @@ -Как внести вклад в документацию -******************************* +Вклад в документацию +******************** .[perex] -Внесение вклада в документацию — одно из самых полезных занятий, поскольку вы помогаете другим понять фреймворк. +Вклад в документацию - одно из самых ценных занятий, потому что он помогает другим понять фреймворк. Как писать? ----------- -Документация предназначена в первую очередь для людей, которые знакомятся с темой. Поэтому она должна соответствовать нескольким важным пунктам: +Документация в первую очередь предназначена для людей, которые в теме новички. Поэтому она должна отвечать нескольким важным пунктам: -- Начните с простого и общего. К более сложным темам переходите только в конце -- Старайтесь объяснить вещь как можно лучше. Попробуйте, например, сначала объяснить тему коллеге -- Приводите только ту информацию, которая действительно нужна пользователю по данной теме -- Убедитесь, что ваша информация действительно верна. Каждый код протестируйте -- Будьте краткими - то, что вы напишете, сократите наполовину. А потом, возможно, еще раз -- Экономьте на выделениях всех видов, от жирного шрифта до рамок типа `.[note]` -- В коде соблюдайте [Стандарт кодирования |Coding Standard] +- Начинайте с простых и общих понятий. К более продвинутым темам переходите только в конце. +- Старайтесь объяснить тему как можно понятнее. Попробуйте, например, сначала объяснить её коллеге. +- Приводите только те сведения, которые пользователю по данной теме действительно нужны. +- Проверяйте, что ваши сведения верны. Проверяйте каждый кусок кода. +- Будьте кратки: сократите написанное вдвое. А потом смело сделайте это ещё раз. +- Используйте выделение умеренно, от жирного текста до блоков вроде `.[note]`. +- В примерах кода придерживайтесь [стандарта кодирования|coding-standard]. -Освойте также [синтаксис |syntax]. Для предварительного просмотра статьи во время ее написания вы можете использовать [редактор с предпросмотром |https://editor.nette.org/]. +Изучите также [синтаксис |syntax]. Чтобы просматривать статью прямо при написании, можно воспользоваться [редактором с предпросмотром |https://editor.nette.org/]. Языковые версии --------------- -Основным языком является английский, поэтому ваши изменения должны быть на английском. Если английский не является вашей сильной стороной, используйте [DeepL Translator |https://www.deepl.com/translator], и другие проверят ваш текст. +Английский - основной язык, так что ваши изменения в идеале должны быть на английском. Если английский не ваша сильная сторона, воспользуйтесь [переводчиком DeepL |https://www.deepl.com/translator], а другие ваш текст проверят. -Перевод на другие языки будет выполнен автоматически после утверждения и доработки вашего изменения. +Перевод на другие языки будет выполнен автоматически после того, как ваша правка будет одобрена и завершена. -Незначительные правки ---------------------- +Мелкие правки +------------- -Для внесения вклада в документацию необходимо иметь учетную запись на [GitHub|https://github.com]. +Чтобы вносить вклад в документацию, вам нужна учётная запись на [GitHub |https://github.com]. -Самый простой способ внести небольшое изменение в документацию — использовать ссылки в конце каждой страницы: +Проще всего внести небольшое изменение в документацию с помощью ссылок в конце каждой страницы: -- *Показать на GitHub* откроет исходную версию данной страницы на GitHub. Затем достаточно нажать кнопку `E` и можно начинать редактировать (необходимо быть авторизованным на GitHub) -- *Открыть предпросмотр* откроет редактор, где вы сразу увидите и итоговый визуальный вид +- *Показать на GitHub* открывает исходную версию страницы на GitHub. Дальше достаточно нажать клавишу `E`, чтобы начать редактирование (нужно быть авторизованным на GitHub). +- *Открыть предпросмотр* открывает редактор, в котором вы сразу видите итоговый внешний вид. -Поскольку [редактор с предпросмотром |https://editor.nette.org/] не имеет возможности сохранять изменения прямо на GitHub, необходимо после завершения правок скопировать исходный текст в буфер обмена (кнопкой *Copy to clipboard*), а затем вставить его в редактор на GitHub. Под полем редактирования находится форма для отправки. Здесь не забудьте кратко изложить и объяснить причину вашей правки. После отправки создается так называемый pull request (PR), который можно дальше редактировать. +Поскольку [редактор с предпросмотром |https://editor.nette.org/] не может сохранять изменения прямо на GitHub, после окончания правок нужно скопировать исходный текст в буфер обмена (кнопкой *Copy to clipboard*), а затем вставить его в редактор на GitHub. Под полем редактирования находится форма отправки. Здесь не забудьте кратко изложить и объяснить причину вашей правки. После отправки создаётся pull request (PR), который можно править дальше. Более крупные правки -------------------- -Более подходящим, чем использование интерфейса GitHub, является знакомство с основами работы с системой контроля версий Git. Если вы не владеете работой с Git, вы можете ознакомиться с руководством [git - простое руководство |https://rogerdudler.github.io/git-guide/] и, при необходимости, использовать один из множества [графических клиентов |https://git-scm.com/downloads/guis]. +Вместо того чтобы полагаться только на интерфейс 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 |code-checker:] -4) сохраните изменения (сделайте коммит) -6) если вы удовлетворены изменениями, отправьте (push) их на GitHub в ваш форк -7) оттуда отправьте их в репозиторий `nette/docs`, создав [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) +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 для изменения существующего pull request. +Обычно вы получаете замечания с предложениями. Следите за предлагаемыми изменениями и вносите их. Добавляйте предложенные изменения новыми коммитами и снова отправляйте их на GitHub. Никогда не создавайте новый pull request только для того, чтобы изменить существующий. Структура документации ---------------------- -Вся документация размещена на GitHub в репозитории [nette/docs |https://github.com/nette/docs]. Актуальная версия находится в ветке master, старые версии размещены в ветках, таких как `doc-3.x`, `doc-2.x`. +Вся документация находится на GitHub в репозитории [nette/docs |https://github.com/nette/docs]. Текущая версия - в ветке `master`, а более старые версии находятся в ветках вроде `doc-3.x`, `doc-2.x`. -Содержимое каждой ветки делится на основные папки, представляющие отдельные области документации. Например, `application/` соответствует https://doc.nette.org/ru/application, `latte/` соответствует https://latte.nette.org и т.д. Каждая такая папка содержит подпапки, представляющие языковые версии (`en`, `ru`, ...), и, возможно, подпапку `files` с изображениями, которые можно вставлять на страницы документации. +Содержимое каждой ветки разделено на основные папки, представляющие отдельные области документации. Например, `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 index ad1bbc8511..db403799e0 100644 --- a/contributing/ru/syntax.texy +++ b/contributing/ru/syntax.texy @@ -1,45 +1,45 @@ Синтаксис документации ********************** -Документация использует синтаксис Markdown и [синтаксис Texy |https://texy.info/cs/syntax] с некоторыми расширениями. +Документация использует Markdown и [синтаксис Texy |https://texy.nette.org/syntax] с несколькими дополнениями. Ссылки ====== -Для внутренних ссылок используется запись в квадратных скобках `[ссылка |odkaz]`. Либо в виде с вертикальной чертой `[текст ссылки |цель ссылки]`, либо сокращенно `[текст ссылки |text odkazu]`, если цель совпадает с текстом (после преобразования в нижний регистр и дефисы): +Для внутренних ссылок используется запись в квадратных скобках `[link]`. Это либо вид с вертикальной чертой `[текст ссылки |цель ссылки]`, либо сокращённый вид `[текст ссылки]`, если цель совпадает с текстом (после преобразования в строчные буквы и дефисы): -- `[Page name]` -> `<a href="/ru/page-name">Page name</a>` -- `[текст ссылки |Page name]` -> `<a href="/ru/page-name">текст ссылки</a>` +- `[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` и т.д.: +Мы можем сослаться на другую языковую версию или другой раздел. Раздел означает библиотеку 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/ru/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="/ru/page-name#toc-heading">Page name</a>` +- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` -Ссылка на главную страницу раздела: (`@home` — это специальное выражение для домашней страницы раздела) +Ссылка на главную страницу раздела: (`@home` - особое обозначение главной страницы раздела) -- `[текст ссылки |@home]` -> `<a href="/ru/">текст ссылки</a>` -- `[текст ссылки |tracy:]` -> `<a href="//tracy.nette.org/ru/">текст ссылки</a>` +- `[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()] @@ -63,7 +63,7 @@ ``` ``` -Вы также можете указать имя файла как <code>```php .{file: ArrayTest.php}</code>, и блок кода будет отображен следующим образом: +Можно указать и имя файла как <code>```php .{file: ArrayTest.php}</code>, и блок кода отрисуется так: ```php .{file: ArrayTest.php} public function renderPage($id) @@ -75,68 +75,69 @@ public function renderPage($id) Заголовки ========= -Самый верхний заголовок (т.е. название страницы) подчеркните звездочками. Для разделения секций используйте знаки равенства. Заголовки подчеркивайте знаками равенства, а затем дефисами: +Самый верхний заголовок (имя страницы) подчёркивайте звёздочками (`*`). Для разделения секций используйте знаки равенства (`=`). Заголовки подчёркивайте сначала знаками равенства (`=`), а затем дефисами (`-`): ``` -MVC Приложения и презентеры -*************************** +MVC Applications & Presenters +***************************** ... -Создание ссылок -=============== +Link Creation +============= ... -Ссылки в шаблонах ------------------ +Links in Templates +------------------ ... ``` -Рамки и стили +Блоки и стили ============= -Вступление обозначаем классом `.[perex]` .[perex] +Перекс, помеченный классом `.[perex]` .[perex] -Примечание обозначаем классом `.[note]` .[note] +Замечание, помеченное классом `.[note]` .[note] -Совет обозначаем классом `.[tip]` .[tip] +Совет, помеченный классом `.[tip]` .[tip] -Предостережение обозначаем классом `.[caution]` .[caution] +Предостережение, помеченное классом `.[caution]` .[caution] -Серьезное предупреждение обозначаем классом `.[warning]` .[warning] +Строгое предупреждение, помеченное классом `.[warning]` .[warning] Номер версии `.{data-version:2.4.10}` .{data-version:2.4.10} -Классы записывайте перед строкой: +Классы следует писать перед строкой, к которой они относятся: ``` .[perex] -Это вступление. +Это перекс. ``` -Пожалуйста, учтите, что рамки типа `.[tip]` "притягивают" взгляд, поэтому они используются для выделения, а не для менее важной информации. Поэтому максимально экономьте их использование. +Учтите, что блоки вроде `.[tip]` привлекают внимание и поэтому должны использоваться для выделения важных сведений, а не менее значимых подробностей. Используйте их умеренно. Содержание ========== -Содержание (ссылки в правом меню) генерируется автоматически для всех страниц, размер которых превышает 4 000 байт, при этом это поведение по умолчанию можно изменить с помощью [#Мета-теги] `{{toc}}`. Текст, составляющий содержание, берется стандартно прямо из текста заголовков, но с помощью модификатора `.{toc}` можно отобразить в содержании другой текст, что полезно в основном для длинных заголовков. +Содержание (ссылки в правой колонке) порождается автоматически для всех страниц размером более 4000 байт. Это поведение по умолчанию можно изменить [метатегом |#Метатеги] `{{toc}}`. Текст для содержания по умолчанию берётся прямо из заголовков, но можно вывести другой текст модификатором `.{toc}`, что полезно для более длинных заголовков. ``` -Длинный и умный заголовок .{toc: Любой другой текст, отображаемый в содержании} -=============================================================================== +Long and Intelligent Heading .{toc: A Different Text for TOC} +============================================================= ``` -Мета-теги -========= +Метатеги +======== -- установка собственного названия страницы (в `<title>` и хлебных крошках) `{{title: Другое название}}` -- перенаправление `{{redirect: pla:cs}}` - см. [#Ссылки] -- принудительное `{{toc}}` или запрет `{{toc: no}}` автоматического содержания (блок со ссылками на отдельные заголовки) +- Задать собственный заголовок страницы (в `<title>` и хлебных крошках): `{{title: Another name}}` +- Перенаправление: `{{redirect: pla:cs}}` - см. [#Ссылки] +- Принудительно включить `{{toc}}` или отключить `{{toc: no}}` автоматическое содержание (блок со ссылками на заголовки). +- Задать левое меню `{{leftbar: utils:@left-menu}}` или отключить его `{{leftbar: no}}`. {{priority: -1}} diff --git a/contributing/sl/@home.texy b/contributing/sl/@home.texy deleted file mode 100644 index 00b95b7b0d..0000000000 --- a/contributing/sl/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Postanite prispevalec k Nette -***************************** - -.[perex] -Ugotovite, kako se lahko vključite v naš odprtokodni projekt. Osvojite postopke za prispevanje k izvorni kodi in dokumentaciji ter postanite del skupnosti razvijalcev, ki aktivno sodelujejo pri izboljševanju Nette. - - -**Koda** - -- [Kako prispevati h kodi? |code] -- [Standard kodiranja |coding-standard] - -**Dokumentacija** - -- [Kako prispevati k dokumentaciji? |documentation] -- [Sintaksa dokumentacije |syntax] -- "Predogledni urejevalnik":https://editor.nette.org diff --git a/contributing/sl/@left-menu.texy b/contributing/sl/@left-menu.texy deleted file mode 100644 index 87eefc02fa..0000000000 --- a/contributing/sl/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Koda -**** -- [Kako prispevati h kodi? |code] -- [Standard kodiranja |coding-standard] - -Dokumentacija -************* -- [Kako prispevati k dokumentaciji? |documentation] -- [Sintaksa dokumentacije |syntax] -- "Predogledni urejevalnik":https://editor.nette.org diff --git a/contributing/sl/code.texy b/contributing/sl/code.texy deleted file mode 100644 index 6bd373891b..0000000000 --- a/contributing/sl/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Kako prispevati h kodi -********************** - -.[perex] -Se pripravljate prispevati k Nette Frameworku in potrebujete orientacijo glede pravil in postopkov? Ta vodnik za začetnike vam bo korak za korakom pokazal, kako učinkovito prispevati h kodi, delati z repozitoriji in implementirati spremembe. - - -Postopek -======== - -Za prispevanje h kodi je nujno imeti račun na [GitHub|https://github.com] in biti seznanjen z osnovami dela z verzijskim sistemom Git. Če ne obvladate dela z Gitom, si lahko ogledate vodnik [git - the simple guide |https://rogerdudler.github.io/git-guide/] in po potrebi uporabite katerega od mnogih [grafičnih klientov |https://git-scm.com/downloads/guis]. - - -Priprava okolja in repozitorija -------------------------------- - -1) na GitHubu si ustvarite [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] repozitorija [paketa |www:packages], ki ga nameravate urejati -2) ta repozitorij [klonirajte |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svoj računalnik -3) namestite odvisnosti, vključno z [Nette Testerjem |tester:], z ukazom `composer install` -4) preverite, ali testi delujejo, z zagonom `composer tester` -5) ustvarite si [novo vejo |#Nova veja], ki temelji na zadnji izdani različici - - -Implementacija lastnih sprememb -------------------------------- - -Zdaj lahko izvedete svoje lastne prilagoditve kode: - -1) sprogramirajte zahtevane spremembe in ne pozabite na teste -2) prepričajte se, da testi uspešno potekajo, z uporabo `composer tester` -3) preverite, ali koda ustreza [standardom kodiranja |#Standardi kodiranja] -4) spremembe shranite (commitnite) z opisom v [tem formatu |#Opis commita] - -Lahko ustvarite več commitov, enega za vsak logični korak. Vsak commit bi moral biti smiseln sam po sebi. - - -Pošiljanje sprememb -------------------- - -Ko boste s spremembami zadovoljni, jih lahko pošljete: - -1) pošljite (pushnite) spremembe na GitHub v vaš fork -2) od tam jih pošljite v Nette repozitorij z ustvarjanjem [pull requesta|https://help.github.com/articles/creating-a-pull-request] (PR) -3) v opisu navedite [dovolj informacij |#Opis pull requesta] - - -Vključevanje pripomb --------------------- - -Vaše commite bodo zdaj videli tudi drugi. Običajno je, da boste prejeli komentarje s pripombami: - -1) spremljajte predlagane prilagoditve -2) vključite jih kot nove commite ali jih [združite s prejšnjimi |https://help.github.com/en/github/using-git/about-git-rebase] -3) ponovno pošljite commite na GitHub in samodejno se bodo pojavili v pull requestu - -Nikoli ne ustvarjajte novega pull requesta zaradi urejanja obstoječega. - - -Dokumentacija -------------- - -Če ste spremenili funkcionalnost ali dodali novo, je ne pozabite tudi [dodati v dokumentacijo |documentation]. - - -Nova veja -========= - -Če je mogoče, izvajajte spremembe glede na zadnjo izdano različico, tj. zadnjo oznako (tag) v dani veji. Za oznako `v3.2.1` ustvarite vejo s tem ukazom: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Standardi kodiranja -=================== - -Vaša koda mora ustrezati [standardu kodiranja |coding standard], ki se uporablja v Nette Frameworku. Za preverjanje in popravljanje kode je na voljo samodejno orodje. Lahko ga namestite prek Composerja **globalno** v mapo po vaši izbiri: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Zdaj bi morali imeti možnost zagnati orodje v terminalu. S prvim ukazom preverite in z drugim tudi popravite kodo v mapah `src` in `tests` v trenutnem imeniku: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Opis commita -============ - -V Nette imajo predmeti commitov format: `Presenter: fixed AJAX detection [Closes #69]` - -- področje, ki mu sledi dvopičje -- namen commita v preteklem času, če je mogoče, začnite z besedo: »added« (dodana nova lastnost), »fixed« (popravek), »refactored« (sprememba v kodi brez spremembe obnašanja), changed, removed -- če commit prekine povratno združljivost, dodajte »BC break« -- morebitna povezava z issue trackerjem kot `(#123)` ali `[Closes #69]` -- za subjektom lahko sledi ena prosta vrstica in nato podrobnejši opis, vključno na primer s povezavami na forum - - -Opis pull requesta -================== - -Pri ustvarjanju pull requesta vam vmesnik GitHub omogoča vnos naslova in opisa. Navedite jedrnat naslov in v opisu podajte čim več informacij o razlogih za vašo spremembo. - -Prikazala se bo tudi glava, kjer določite, ali gre za novo funkcijo ali popravek napake in ali lahko pride do prekinitve povratne združljivosti (BC break). Če obstaja povezan problem (issue), se nanj sklicujte, da bo zaprt po odobritvi pull requesta. - -``` -- bug fix / new feature? <!-- #številke issue-jev, če obstajajo --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- zelo dobrodošlo, glej https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/sl/coding-standard.texy b/contributing/sl/coding-standard.texy deleted file mode 100644 index 8b4e2ffb2b..0000000000 --- a/contributing/sl/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Standard kodiranja -****************** - -.[perex] -Ta dokument opisuje pravila in priporočila za razvoj Nette. Pri prispevanju kode k Nette jih morate upoštevati. Najlažji način za to je posnemanje obstoječe kode. Gre za to, da vsa koda izgleda, kot da jo je napisala ena oseba. - -Nette Coding Standard ustreza [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] z dvema glavnima izjemama: za zamikanje uporablja [zavihke namesto presledkov |#Zavihki namesto presledkov] in za [konstante razredov uporablja PascalCase|https://blog.nette.org/sl/for-less-screaming-in-the-code]. - - -Splošna pravila -=============== - -- Vsaka PHP datoteka mora vsebovati `declare(strict_types=1)` -- Dve prazni vrstici se uporabljata za ločevanje metod za boljšo berljivost. -- Razlog za uporabo operatorja za utišanje (shut-up operator) mora biti dokumentiran: `@mkdir($dir); // @ - mapa lahko obstaja`. -- Če je uporabljen šibko tipiziran primerjalni operator (tj. `==`, `!=`, ...), mora biti namen dokumentiran: `// == sprejmi null` -- V eno datoteko `exceptions.php` lahko zapišete več izjem. -- Pri vmesnikih se ne določa vidnost metod, ker so vedno javne. -- Vsaka lastnost, vračana vrednost in parameter morajo imeti naveden tip. Nasprotno pa pri končnih konstantah tipa nikoli ne navajamo, ker je očiten. -- Za omejevanje niza naj se uporabljajo enojni narekovaji, razen v primerih, ko sam literal vsebuje apostrofe. - - -Poimenovalne konvencije -======================= - -- Ne uporabljajte okrajšav, razen če je celotno ime predolgo. -- Pri dvočrkovnih okrajšavah uporabljajte velike črke, pri daljših okrajšavah pascal/camel. -- Za ime razreda uporabljajte samostalnik ali besedno zvezo. -- Imena razredov morajo vsebovati ne samo specifičnost (`Array`), ampak tudi splošnost (`ArrayIterator`). Izjema so atributi jezika PHP. -- "Konstante razredov in enumeracije naj uporabljajo PascalCaps":https://blog.nette.org/sl/for-less-screaming-in-the-code. -- "Vmesniki in abstraktni razredi ne smejo vsebovati predpon ali pripon":https://blog.nette.org/sl/prefixes-and-suffixes-do-not-belong-in-interface-names kot `Abstract`, `Interface` ali `I`. - - -Oblikovanje in oklepaji -======================= - -Nette Coding Standard ustreza PSR-12 (oz. PER Coding Style), v nekaterih točkah ga dopolnjuje ali spreminja: - -- puščične funkcije se pišejo brez presledka pred oklepajem, tj. `fn($a) => $b` -- ne zahteva se prazna vrstica med različnimi tipi `use` import stavkov -- vračani tip funkcije/metode in začetni zaviti oklepaj sta vedno na ločenih vrsticah: - -```php - public function find( - string $dir, - array $options, - ): array - { - // telo metode - } -``` - -Začetni zaviti oklepaj na ločeni vrstici je pomemben za vizualno ločevanje signature funkcije/metode od telesa. Če je signatura na eni vrstici, je ločitev očitna (slika levo), če je na več vrsticah, se v PSR signaturi in telesi zlivata (sredina), medtem ko sta v Nette standardu še naprej ločeni (desno): - -[* new-line-after.webp *] - - -Bloki dokumentacije (phpDoc) -============================ - -Glavno pravilo: Nikoli ne podvajajte nobenih informacij v signaturi, kot je tip parametra ali vračani tip, brez dodane vrednosti. - -Dokumentacijski blok za definicijo razreda: - -- Začne se z opisom razreda. -- Sledi prazna vrstica. -- Sledijo anotacije `@property` (ali `@property-read`, `@property-write`), ena za drugo. Sintaksa je: anotacija, presledek, tip, presledek, $ime. -- Sledijo anotacije `@method`, ena za drugo. Sintaksa je: anotacija, presledek, vračani tip, presledek, ime(tip $param, ...). -- Anotacija `@author` se izpušča. Avtorstvo se hrani v zgodovini izvorne kode. -- Lahko se uporabita anotaciji `@internal` ali `@deprecated`. - -```php -/** - * MIME del sporočila. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Dokumentacijski blok za lastnost, ki vsebuje samo anotacijo `@var`, bi moral biti enovrstičen: - -```php -/** @var string[] */ -private array $name; -``` - -Dokumentacijski blok za definicijo metode: - -- Začne se s kratkim opisom metode. -- Brez prazne vrstice. -- Anotacije `@param` po posameznih vrsticah. -- Anotacija `@return`. -- Anotacije `@throws`, ena za drugo. -- Lahko se uporabita anotaciji `@internal` ali `@deprecated`. - -Vsaki anotaciji sledi en presledek, z izjemo `@param`, za katero za boljšo berljivost sledita dva presledka. - -```php -/** - * Najde datoteko v imeniku. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Zavihki namesto presledkov -========================== - -Zavihki imajo v primerjavi s presledki več prednosti: - -- velikost zamika je mogoče prilagoditi v urejevalnikih in na "spletu":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- kodi ne vsiljujejo uporabnikove preference glede velikosti zamika, zato je koda bolje prenosljiva -- lahko jih napišemo z enim pritiskom tipke (kjerkoli, ne samo v urejevalnikih, ki spreminjajo zavihke v presledke) -- zamikanje je njihov namen -- spoštujejo potrebe slabovidnih in slepih kolegov - -Z uporabo zavihkov v naših projektih omogočamo prilagajanje širine, kar se večini ljudi morda zdi nepotrebno, vendar je za ljudi z okvaro vida nujno. - -Za slepe programerje, ki uporabljajo braillove zaslone, vsak presledek predstavlja eno braillovo celico. Če je torej privzeti zamik 4 presledki, zamik 3. stopnje zapravi 12 dragocenih braillovih celic, še preden se koda začne. Na 40-celičnem zaslonu, ki se najpogosteje uporablja pri prenosnikih, je to več kot četrtina razpoložljivih celic, ki so zapravljene brez kakršnekoli informacije. - - -{{priority: -1}} diff --git a/contributing/sl/documentation.texy b/contributing/sl/documentation.texy deleted file mode 100644 index 8ae7191d0c..0000000000 --- a/contributing/sl/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Kako prispevati k dokumentaciji -******************************* - -.[perex] -Prispevanje k dokumentaciji je ena najbolj koristnih dejavnosti, saj pomagate drugim razumeti ogrodje. - - -Kako pisati? ------------- - -Dokumentacija je namenjena predvsem ljudem, ki se s temo seznanjajo. Zato bi morala izpolnjevati nekaj pomembnih točk: - -- Začnite s preprostim in splošnim. K naprednejšim temam preidite šele na koncu. -- Poskusite stvar čim bolje pojasniti. Poskusite na primer temo najprej pojasniti kolegu. -- Navajajte samo tiste informacije, ki jih uporabnik dejansko potrebuje vedeti o dani temi. -- Preverite, ali so vaše informacije resnično pravilne. Vsako kodo preizkusite. -- Bodite jedrnati - kar napišete, skrajšajte na polovico. In potem mirno še enkrat. -- Varčujte z vsemi vrstami poudarkov, od krepke pisave do okvirjev kot `.[note]`. -- V kodah upoštevajte [Standard kodiranja |Coding Standard]. - -Osvojite tudi [sintakso |syntax]. Za predogled članka med pisanjem lahko uporabite [urejevalnik s predogledom |https://editor.nette.org/]. - - -Jezikovne različice -------------------- - -Primarni jezik je angleščina, zato bi morale biti vaše spremembe najprej v angleščini. Če angleščina ni vaša močna stran, uporabite [DeepL Translator |https://www.deepl.com/translator] in drugi vam bodo besedilo preverili. - -Prevod v ostale jezike bo izveden samodejno po odobritvi in dodelavi vaše prilagoditve. - - -Manjše prilagoditve -------------------- - -Za prispevanje k dokumentaciji je nujno imeti račun na [GitHub|https://github.com]. - -Najlažji način za manjšo spremembo v dokumentaciji je uporaba povezav na koncu vsake strani: - -- *Pokaži na GitHubu* odpre izvorno obliko dane strani na GitHubu. Nato samo pritisnite gumb `E` in lahko začnete urejati (potrebno je biti prijavljen na GitHubu). -- *Odpri predogled* odpre urejevalnik, kjer takoj vidite tudi končno vizualno podobo. - -Ker [urejevalnik s predogledom |https://editor.nette.org/] nima možnosti shranjevanja sprememb neposredno na GitHub, je treba po končanem urejanju izvorni tekst kopirati v odložišče (z gumbom *Copy to clipboard*) in ga nato prilepiti v urejevalnik na GitHubu. Pod urejevalnim poljem je obrazec za pošiljanje. Tukaj ne pozabite na kratko povzeti in pojasniti razlog vaše prilagoditve. Po pošiljanju nastane t.i. pull request (PR), ki ga je mogoče nadalje urejati. - - -Večje prilagoditve ------------------- - -Primernejše kot uporaba vmesnika GitHub je biti seznanjen z osnovami dela z verzijskim sistemom Git. Če ne obvladate dela z Gitom, si lahko ogledate vodnik [git - the simple guide |https://rogerdudler.github.io/git-guide/] in po potrebi uporabite katerega od mnogih [grafičnih klientov |https://git-scm.com/downloads/guis]. - -Dokumentacijo urejajte na ta način: - -1) na GitHubu si ustvarite [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] repozitorija [nette/docs |https://github.com/nette/docs] -2) ta repozitorij [klonirajte |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svoj računalnik -3) nato v [ustrezni veji |#Struktura dokumentacije] izvedite spremembe -4) preverite odvečne presledke v besedilu z orodjem [Code-Checker |code-checker:] -4) spremembe shranite (commitnite) -6) če ste s spremembami zadovoljni, jih pošljite (pushnite) na GitHub v vaš fork -7) od tam jih pošljite v repozitorij `nette/docs` z ustvarjanjem [pull requesta|https://help.github.com/articles/creating-a-pull-request] (PR) - -Običajno je, da boste prejemali komentarje s pripombami. Spremljajte predlagane spremembe in jih vključite. Predlagane spremembe dodajte kot nove commite in ponovno pošljite na GitHub. Nikoli ne ustvarjajte novega pull requesta zaradi urejanja pull requesta. - - -Struktura dokumentacije ------------------------ - -Celotna dokumentacija je nameščena na GitHubu v repozitoriju [nette/docs |https://github.com/nette/docs]. Trenutna različica je v veji `master`, starejše različice so nameščene v vejah kot `doc-3.x`, `doc-2.x`. - -Vsebina vsake veje se deli na glavne mape, ki predstavljajo posamezna področja dokumentacije. Na primer `application/` ustreza https://doc.nette.org/sl/application, `latte/` ustreza https://latte.nette.org/sl itd. Vsaka ta mapa vsebuje podmape, ki predstavljajo jezikovne različice (`sl`, `en`, ...) in po potrebi podmapo `files` s slikami, ki jih je mogoče vstavljati na strani v dokumentaciji. diff --git a/contributing/sl/syntax.texy b/contributing/sl/syntax.texy deleted file mode 100644 index 8486f3ea0e..0000000000 --- a/contributing/sl/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Sintaksa dokumentacije -********************** - -Dokumentacija uporablja Markdown & [sintakso Texy |https://texy.info/sl/syntax] z nekaterimi razširitvami. - - -Povezave -======== - -Za notranje povezave se uporablja zapis v oglatih oklepajih `[povezava]`. In sicer bodisi v obliki z navpičnico `[besedilo povezave |cilj povezave]`, bodisi skrajšano `[besedilo povezave]`, če je cilj enak besedilu (po pretvorbi v male črke in pomišljaje): - -- `[Ime strani |Page name]` -> `<a href="/en/page-name">Ime strani</a>` -- `[besedilo povezave |Page name]` -> `<a href="/en/page-name">besedilo povezave</a>` - -Povezujemo lahko v drugo jezikovno različico ali v drugo sekcijo. Sekcija pomeni Nette knjižnico (npr. `forms`, `latte`, ipd.) ali posebne sekcije kot `best-practices`, `quickstart` itd.: - -- `[cs:Ime strani |cs:Page name]` -> `<a href="/cs/page-name">cs:Ime strani</a>` (ista sekcija, drug jezik) -- `[tracy:Ime strani |tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">tracy:Ime strani</a>` (druga sekcija, isti jezik) -- `[tracy:cs:Ime strani |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">tracy:cs:Ime strani</a>` (druga sekcija in jezik) - -S pomočjo `#` je mogoče tudi ciljati na določen naslov na strani. - -- `[Naslov |#Heading]` -> `<a href="#toc-heading">Naslov</a>` (naslov na trenutni strani) -- `[Ime strani#Naslov |Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Ime strani#Naslov</a>` - -Povezava na uvodno stran sekcije: (`@home` je poseben izraz za domačo stran sekcije) - -- `[besedilo povezave |@home]` -> `<a href="/en/">besedilo povezave</a>` -- `[besedilo povezave |tracy:]` -> `<a href="//tracy.nette.org/en/">besedilo povezave</a>` - - -Povezave do API dokumentacije ------------------------------ - -Vedno navajajte samo s tem zapisom: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Popolnoma kvalificirana imena uporabljajte samo ob prvi omembi. Za nadaljnje povezave uporabite poenostavljeno ime: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Povezave do PHP dokumentacije ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Izvorna koda -============ - -Blok kode se začne z <code>```lang</code> in konča z <code>```</code>. Podprti jeziki so `php`, `latte`, `neon`, `html`, `css`, `js` in `sql`. Za zamikanje vedno uporabljajte zavihke. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Lahko tudi navedete ime datoteke kot <code>```php .{file: ArrayTest.php}</code> in blok kode se bo izrisal na ta način: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Naslovi -======= - -Najvišji naslov (torej ime strani) podčrtajte z zvezdicami. Za ločevanje sekcij uporabljajte enačaje. Naslove podčrtajte z enačaji in nato s pomišljaji: - -``` -MVC Aplikacije & presenterji -**************************** -... - - -Ustvarjanje povezav -=================== -... - - -Povezave v predlogah --------------------- -... -``` - - -Okvirji in stili -================ - -Perex označimo z razredom `.[perex]` - -Opombo označimo z razredom `.[note]` - -Nasvet označimo z razredom `.[tip]` - -Opozorilo označimo z razredom `.[caution]` - -Močnejše opozorilo označimo z razredom `.[warning]` - -Številko različice `.{data-version:2.4.10}` - -Razrede zapišite pred vrstico: - -``` -.[perex] -To je perex. -``` - -Zavedajte se prosim, da okvirji kot `.[tip]` »pritegnejo« oči, zato se uporabljajo za poudarjanje, ne pa za manj pomembne informacije. Zato z njihovo uporabo maksimalno varčujte. - - -Vsebina -======= - -Vsebina (povezave v desnem meniju) je samodejno generirana za vse strani, katerih velikost presega 4.000 bajtov, pri čemer je to privzeto obnašanje mogoče prilagoditi s pomočjo [meta oznak |#Meta značke] `{{toc}}`. Besedilo, ki tvori vsebino, se standardno vzame neposredno iz besedila naslovov, vendar je s pomočjo modifikatorja `.{toc}` mogoče v vsebini prikazati drugo besedilo, kar je koristno predvsem za daljše naslove. - -``` - - -Dolg in inteligenten naslov .{toc: Poljubno drugo besedilo, prikazano v vsebini} -================================================================================ -``` - - -Meta značke -=========== - -- nastavitev lastnega imena strani (v `<title>` in drobtinicah) `{{title: Drugo ime}}` -- preusmeritev `{{redirect: pla:cs}}` - glej [#povezave] -- vsiljenje `{{toc}}` ali prepoved `{{toc: no}}` samodejne vsebine (okvirček s povezavami na posamezne naslove) - -{{priority: -1}} diff --git a/contributing/tr/@home.texy b/contributing/tr/@home.texy index 837975c5b5..be73a4a3d4 100644 --- a/contributing/tr/@home.texy +++ b/contributing/tr/@home.texy @@ -1,17 +1,17 @@ -Nette'ye Katkıda Bulunan Olun -***************************** +Nette'e Katkıda Bulunun +*********************** .[perex] -Açık kaynak projemize nasıl katılabileceğinizi öğrenin. Kaynak koduna ve dokümantasyona katkıda bulunma prosedürlerini öğrenin ve Nette'yi geliştirmeye aktif olarak katılan geliştiriciler topluluğunun bir parçası olun. +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 nasıl katkıda bulunulur? |code] -- [Kodlama standardı |coding-standard] +- [Koda katkıda bulunma |code] +- [Kodlama standartları |coding-standard] -**Dokümantasyon** +**Belgeler** -- [Dokümantasyona nasıl katkıda bulunulur? |documentation] -- [Dokümantasyon sözdizimi |syntax] +- [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 index 471105271a..0a20b30b47 100644 --- a/contributing/tr/@left-menu.texy +++ b/contributing/tr/@left-menu.texy @@ -1,10 +1,18 @@ Kod *** -- [Koda nasıl katkıda bulunulur? |code] -- [Kodlama standardı |coding-standard] +- [Koda katkıda bulunma |code] +- [Kodlama standartları |coding-standard] -Dokümantasyon -************* -- [Dokümantasyona nasıl katkıda bulunulur? |documentation] -- [Dokümantasyon sözdizimi |syntax] +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 index c9cecd6add..b0bb188db3 100644 --- a/contributing/tr/code.texy +++ b/contributing/tr/code.texy @@ -1,71 +1,71 @@ -Koda nasıl katkıda bulunulur -**************************** +Koda Katkıda Bulunma +******************** .[perex] -Nette Framework'e katkıda bulunmaya hazırlanıyor ve kurallar ve prosedürler hakkında bilgiye mi ihtiyacınız var? Yeni başlayanlar için bu kılavuz, koda etkili bir şekilde nasıl katkıda bulunacağınızı, depolarla nasıl çalışacağınızı ve değişiklikleri nasıl uygulayacağınızı adım adım gösterecektir. +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. -Prosedür -======== +Yöntem +====== -Koda katkıda bulunmak için [GitHub|https://github.com] üzerinde bir hesabınızın olması ve Git sürüm kontrol sistemi ile çalışmanın temellerine aşina olmanız gerekir. Git ile çalışmayı bilmiyorsanız, [Git - basit kılavuz |https://rogerdudler.github.io/git-guide/] kılavuzuna bakabilir ve gerekirse birçok [grafik istemciden biri |https://git-scm.com/downloads/guis]ni kullanabilirsiniz. +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 depo hazırlığı ------------------------ +Ortamı ve Depoyu Hazırlama +-------------------------- -1) GitHub'da, düzenlemeyi planladığınız [paketin |www:packages] deposunun bir [forkunu |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] oluşturun -2) Bu depoyu bilgisayarınıza [klonlayın |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] -3) `composer install` komutunu kullanarak [Nette Tester |tester:] dahil olmak üzere bağımlılıkları yükleyin -4) `composer tester` komutunu çalıştırarak testlerin çalıştığını kontrol edin -5) Son yayınlanan sürüme dayalı [yeni bir dal |#Yeni dal] oluşturun +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 uygulama ---------------------------------- +Kendi Değişikliklerinizi Gerçekleştirme +--------------------------------------- -Şimdi kendi kod değişikliklerinizi yapabilirsiniz: +Artık kendi kod düzenlemelerinizi yapabilirsiniz: -1) Gerekli değişiklikleri programlayın ve testleri unutmayın -2) `composer tester` kullanarak testlerin başarıyla geçtiğinden emin olun -3) Kodun [kodlama standardına |#Kodlama Standartları] uygun olup olmadığını kontrol edin -4) Değişiklikleri [bu formatta |#Commit açıklaması] bir açıklama ile kaydedin (commit edin) +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 birkaç commit oluşturabilirsiniz. Her commit kendi başına anlamlı olmalıdır. +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 gönderme ------------------------ +Değişiklikleri Sunma +-------------------- -Değişikliklerden memnun olduğunuzda, gönderebilirsiniz: +Değişikliklerden memnun olduğunuzda onları sunabilirsiniz: -1) Değişiklikleri GitHub'daki fork'unuza gönderin (push) -2) Oradan, bir [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) oluşturarak Nette deposuna gönderin -3) Açıklamada [yeterli bilgi |#Pull request açıklaması] sağlayın +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 dahil etme -------------------------- +Geri Bildirimi Uygulama +----------------------- -Commit'leriniz artık başkaları tarafından görülecektir. Yorumlarla geri bildirim almanız yaygındır: +Commit'leriniz artık başkalarınca görülebilir. Öneriler içeren yorumlar almak olağandır: -1) Önerilen değişiklikleri takip edin -2) Bunları yeni commit'ler olarak dahil edin veya [öncekilerle birleştirin |https://help.github.com/en/github/using-git/about-git-rebase] -3) Commit'leri tekrar GitHub'a gönderin, otomatik olarak pull request'te görüneceklerdir +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 -Mevcut bir pull request'i düzenlemek için asla yeni bir pull request oluşturmayın. +Var olan bir pull request'i değiştirmek için asla yeni bir pull request oluşturmayın. -Dokümantasyon -------------- +Belgeler +-------- -İşlevselliği değiştirdiyseniz veya yeni bir tane eklediyseniz, bunu [dokümantasyona eklemeyi |documentation] de unutmayın. +Bir işlevselliği değiştirdiyseniz ya da yenisini eklediyseniz, onu [belgelere de eklemeyi|documentation] unutmayın. -Yeni dal +Yeni Dal ======== -Mümkünse, değişiklikleri son yayınlanan sürüme, yani ilgili daldaki son etikete göre yapın. `v3.2.1` etiketi için şu komutla bir dal oluşturursunuz: +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 @@ -75,38 +75,27 @@ git checkout -b new_branch_name v3.2.1 Kodlama Standartları ==================== -Kodunuz, Nette Framework'te kullanılan [kodlama standardı |coding standard]na uymalıdır. Kodu kontrol etmek ve düzeltmek için otomatik bir araç mevcuttur. Composer aracılığıyla seçtiğiniz bir klasöre **global olarak** kurulabilir: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Şimdi aracı terminalde çalıştırabilmelisiniz. İlk komut kontrol eder ve ikinci komut geçerli dizindeki `src` ve `tests` klasörlerindeki kodu düzeltir: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` +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ı +Commit Açıklaması ================= -Nette'de commit konularının formatı şöyledir: `Presenter: fixed AJAX detection [Closes #69]` +Nette'de commit konuları şu biçimdedir: `Presenter: fixed AJAX detection [Closes #69]` -- İki nokta üst üste ile takip edilen alan -- Mümkünse geçmiş zamanda commit'in amacı, şu kelimelerle başlayın: "added .(yeni özellik eklendi)", "fixed .(hata düzeltildi)", "refactored .(davranış değişikliği olmadan kod yeniden düzenlendi)", changed, removed -- Commit geriye dönük uyumluluğu bozarsa, "BC break" ekleyin -- `(#123)` veya `[Closes #69]` gibi isteğe bağlı sorun izleyici bağlantısı -- Konudan sonra bir boş satır ve ardından forum bağlantıları gibi daha ayrıntılı bir açıklama gelebilir +- 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ı +Pull Request Açıklaması ======================= -Bir pull request oluştururken, GitHub arayüzü bir başlık ve açıklama girmenize izin verir. Açıklayıcı bir başlık girin ve açıklama bölümünde değişikliğinizin nedenleri hakkında mümkün olduğunca fazla bilgi sağlayın. +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, bunun yeni bir özellik mi yoksa bir hata düzeltmesi mi olduğunu ve geriye dönük uyumluluğun bozulup bozulmayacağını (BC break) belirttiğiniz bir başlık da görünecektir. İlgili bir sorun (issue) varsa, pull request onaylandıktan sonra kapatılması için ona bağlantı verin. +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 --> diff --git a/contributing/tr/coding-standard.texy b/contributing/tr/coding-standard.texy index c932e44d9b..f527f40ee4 100644 --- a/contributing/tr/coding-standard.texy +++ b/contributing/tr/coding-standard.texy @@ -1,44 +1,47 @@ -Kodlama standardı +Kodlama Standardı ***************** .[perex] -Bu belge, Nette geliştirme için kuralları ve önerileri açıklar. Nette'ye kod katkısında bulunurken bunlara uymalısınız. Bunu yapmanın en kolay yolu, mevcut kodu taklit etmektir. Amaç, tüm kodun tek bir kişi tarafından yazılmış gibi görünmesidir. +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 istisna dışında [PSR-12 Genişletilmiş Kodlama Stili |https://www.php-fig.org/psr/psr-12/] ile uyumludur: girinti için [#boşluklar yerine sekmeler] kullanır ve "sınıf sabitleri için PascalCase kullanır":https://blog.nette.org/tr/for-less-screaming-in-the-code. +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 + +Genel Kurallar ============== - Her PHP dosyası `declare(strict_types=1)` içermelidir -- Daha iyi okunabilirlik için metotları ayırmak için iki boş satır kullanılır. -- Susturma operatörünün (@) kullanım nedeni belgelenmelidir: `@mkdir($dir); // @ - dizin mevcut olabilir`. -- Zayıf tipli bir karşılaştırma operatörü (yani `==`, `!=`, ...) kullanılıyorsa, amaç belgelenmelidir: `// == null kabul et` -- Tek bir `exceptions.php` dosyasına birden fazla istisna yazabilirsiniz. -- Arayüzler için metot görünürlüğü belirtilmez, çünkü her zaman public'tirler. -- Her özellik, dönüş değeri ve parametre için bir tip belirtilmelidir. Tersine, nihai sabitler için tipi asla belirtmeyiz, çünkü açıktır. -- Bir karakter dizisini sınırlamak için, değişmez değerin kendisi kesme işareti içermediği sürece tek tırnak işaretleri kullanılmalıdır. +- 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ı +Adlandırma Kuralları ==================== -- Tam ad çok uzun olmadıkça kısaltmalar kullanmayın. -- İki harfli kısaltmalar için büyük harfler, daha uzun kısaltmalar için pascal/camel case kullanın. -- Sınıf adı için bir isim veya isim tamlaması kullanın. -- Sınıf adları yalnızca özgüllüğü (`Array`) değil, aynı zamanda genelliği de (`ArrayIterator`) içermelidir. PHP dil nitelikleri bir istisnadır. -- "Sınıf sabitleri ve enumlar PascalCaps kullanmalıdır":https://blog.nette.org/tr/for-less-screaming-in-the-code. -- "Arayüzler ve soyut sınıflar, Abstract, Interface veya I gibi önekler veya sonekler içermemelidir":https://blog.nette.org/tr/prefixes-and-suffixes-do-not-belong-in-interface-names. +- 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` -Sarma ve Parantezler -==================== +Satır Sarma ve Parantezler +========================== -Nette Kodlama Standardı, PSR-12 (veya PER Kodlama Stili) ile uyumludur, bazı noktalarda onu tamamlar veya değiştirir: +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 ifadeleri türleri arasında boş bir satır gerekli değildir -- fonksiyon/metot dönüş tipi ve açılış küme parantezi her zaman ayrı satırlardadır: +- 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( @@ -46,32 +49,32 @@ Nette Kodlama Standardı, PSR-12 (veya PER Kodlama Stili) ile uyumludur, bazı n array $options, ): array { - // metot gövdesi + // method body } ``` -Ayrı bir satırdaki açılış küme parantezi, fonksiyon/metot imzasını gövdeden görsel olarak ayırmak için önemlidir. İmza tek bir satırdaysa, ayırma açıktır (soldaki resim), birden çok satırdaysa, PSR'de imzalar ve gövde birleşir (ortada), Nette standardında ise ayrı kalırlar (sağda): +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 *] -Belgelendirme blokları (phpDoc) -=============================== +Belge Blokları (phpDoc) +======================= -Ana kural: Ek bir değer olmadan parametre tipi veya dönüş tipi gibi imzadaki hiçbir bilgiyi asla tekrarlamayın. +Ana kural: Parametre tipi ya da dönüş tipi gibi imza bilgilerini, değer katmadan **asla yinelemeyin**. -Sınıf tanımı için belgelendirme bloğu: +Bir sınıf tanımı için belge bloğu: -- Sınıfın bir açıklamasıyla başlar. -- Ardından boş bir satır gelir. -- Ardından `@property` (veya `@property-read`, `@property-write`) ek açıklamaları gelir, birbiri ardına. Sözdizimi: ek açıklama, boşluk, tip, boşluk, $name. -- Ardından `@method` ek açıklamaları gelir, birbiri ardına. Sözdizimi: ek açıklama, boşluk, dönüş tipi, boşluk, name(tip $param, ...). -- `@author` ek açıklaması atlanır. Yazarlık, kaynak kodu geçmişinde tutulur. -- `@internal` veya `@deprecated` ek açıklamaları kullanılabilir. +- 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 mesaj bölümü. + * MIME message part. * * @property string $encoding * @property-read array $headers @@ -80,27 +83,27 @@ Sınıf tanımı için belgelendirme bloğu: */ ``` -Yalnızca `@var` ek açıklamasını içeren bir özellik için belgelendirme bloğu tek satırlık olmalıdır: +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; ``` -Metot tanımı için belgelendirme bloğu: +Bir metot tanımı için belge bloğu: -- Metodun kısa bir açıklamasıyla başlar. -- Boş satır yok. -- Ayrı satırlarda `@param` ek açıklamaları. -- `@return` ek açıklaması. -- `@throws` ek açıklamaları, birbiri ardına. -- `@internal` veya `@deprecated` ek açıklamaları kullanılabilir. +- 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 ek açıklamayı bir boşluk takip eder, `@param` hariç, daha iyi okunabilirlik için bunu iki boşluk takip eder. +Her açıklamadan sonra bir boşluk gelir; daha iyi okunabilirlik için iki boşluk gelen `@param` bunun dışındadır. ```php /** - * Dizinde bir dosya bulur. + * Finds a file in directory. * @param string[] $options * @return string[] * @throws DirectoryNotFoundException @@ -109,20 +112,37 @@ public function find(string $dir, array $options): array ``` -Boşluklar yerine sekmeler -========================= +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: -- girinti boyutu düzenleyicilerde ve "web'de":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size ayarlanabilir -- kodun kullanıcının girinti boyutu tercihini zorlamazlar, bu nedenle kod daha taşınabilirdir -- tek bir tuş vuruşuyla yazılabilirler (sadece sekmeleri boşluklara dönüştüren düzenleyicilerde değil, her yerde) -- girintileme onların amacıdır -- görme engelli ve kör meslektaşlarımızın ihtiyaçlarına saygı duyarlar +- 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 sekmeler kullanarak, çoğu insan için gereksiz görünebilecek genişlik ayarlamasına izin veriyoruz, ancak görme engelli insanlar için bu zorunludur. +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 ekranları kullanan kör programcılar için her boşluk bir Braille hücresini temsil eder. Bu nedenle, varsayılan girinti 4 boşluksa, 3. seviye girinti, kod başlamadan önce 12 değerli Braille hücresini boşa harcar. Dizüstü bilgisayarlarda en sık kullanılan 40 hücreli bir ekranda, bu, herhangi bir bilgi olmadan boşa harcanan mevcut hücrelerin dörtte birinden fazlasıdır. +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 index b9efcf74e4..fb728d7442 100644 --- a/contributing/tr/documentation.texy +++ b/contributing/tr/documentation.texy @@ -1,68 +1,68 @@ -Dokümantasyona nasıl katkıda bulunulur -************************************** +Belgelere Katkıda Bulunma +************************* .[perex] -Dokümantasyona katkıda bulunmak, başkalarının framework'ü anlamasına yardımcı olduğunuz için en faydalı faaliyetlerden biridir. +Belgelere katkıda bulunmak, başkalarının framework'ü anlamasına yardım ettiği için en değerli etkinliklerden biridir. -Nasıl yazılır? +Nasıl Yazmalı? -------------- -Dokümantasyon öncelikle konuyla yeni tanışan kişilere yöneliktir. Bu nedenle birkaç önemli noktayı karşılamalıdır: +Belgeler öncelikle konuya yeni olan kişiler içindir. Bu yüzden birkaç önemli noktayı karşılamalıdır: -- Basit ve genelden başlayın. Daha gelişmiş konulara ancak sonunda geçin -- Konuyu olabildiğince iyi açıklamaya çalışın. Örneğin, konuyu önce bir meslektaşınıza açıklamayı deneyin -- Yalnızca kullanıcının konu hakkında gerçekten bilmesi gereken bilgileri sağlayın -- Bilgilerinizin gerçekten doğru olduğunu doğrulayın. Her kodu test edin -- Kısa ve öz olun - yazdıklarınızı yarıya indirin. Ve sonra gerekirse bir kez daha -- Kalın yazıdan `.[note]` gibi çerçevelere kadar her türlü vurgulayıcıdan tasarruf edin -- Kodlarda [Kodlama Standardı |Coding Standard]na uyun +- 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özdizimi |syntax]ni öğrenin. Yazarken makaleyi önizlemek için [önizlemeli düzenleyiciyi |https://editor.nette.org/] kullanabilirsiniz. +Ayrıca [sözdizimini |syntax] öğrenin. Yazarken makaleyi önizlemek için [önizleme düzenleyicisini |https://editor.nette.org/] kullanabilirsiniz. -Dil sürümleri +Dil Sürümleri ------------- -Birincil dil İngilizce'dir, bu nedenle değişiklikleriniz hem Çekçe hem de İngilizce olmalıdır. İngilizce güçlü yanınız değilse, [DeepL Translator |https://www.deepl.com/translator] kullanın ve diğerleri metninizi kontrol edecektir. +İ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 onaylandıktan ve ince ayar yapıldıktan sonra otomatik olarak yapılacaktır. +Diğer dillere çeviri, düzenlemeniz onaylanıp sonlandırıldıktan sonra otomatik yapılacak. -Önemsiz düzenlemeler +Önemsiz Düzenlemeler -------------------- -Dokümantasyona katkıda bulunmak için [GitHub|https://github.com] üzerinde bir hesabınızın olması gerekir. +Belgelere katkıda bulunmak için [GitHub |https://github.com] üzerinde bir hesabınızın olması gerekir. -Dokümantasyonda küçük bir değişiklik yapmanın en kolay yolu, her sayfanın sonundaki bağlantıları kullanmaktır: +Belgelerde küçük bir değişiklik yapmanın en kolay yolu, her sayfanın sonundaki bağlantıları kullanmaktır: -- *GitHub'da göster* ilgili sayfanın kaynak sürümünü GitHub'da açar. Ardından `E` düğmesine basmanız yeterlidir ve düzenlemeye başlayabilirsiniz (GitHub'da oturum açmış olmanız gerekir) -- *Önizlemeyi aç* düzenleyiciyi açar, burada sonuçtaki görsel görünümü de hemen görürsünüz +- *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. -[Önizlemeli düzenleyici |https://editor.nette.org/] değişiklikleri doğrudan GitHub'a kaydetme seçeneğine sahip olmadığından, düzenlemeyi bitirdikten sonra kaynak metni panoya kopyalamak (*Copy to clipboard* düğmesiyle) ve ardından GitHub'daki düzenleyiciye yapıştırmak gerekir. Düzenleme alanının altında gönderme formu bulunur. Burada düzenlemenizin nedenini kısaca özetlemeyi ve açıklamayı unutmayın. Gönderdikten sonra, daha fazla düzenlenebilen bir pull request (PR) oluşturulur. +[Ö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 +Daha Büyük Düzenlemeler ----------------------- -GitHub arayüzünü kullanmak yerine, Git sürüm kontrol sistemi ile çalışmanın temellerine aşina olmak daha uygundur. Git ile çalışmayı bilmiyorsanız, [Git - basit kılavuz |https://rogerdudler.github.io/git-guide/] kılavuzuna bakabilir ve gerekirse birçok [grafik istemciden biri |https://git-scm.com/downloads/guis]ni kullanabilirsiniz. +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. -Dokümantasyonu şu şekilde düzenleyin: +Belgeleri şöyle düzenleyin: -1) GitHub'da [nette/docs |https://github.com/nette/docs] deposunun bir [forkunu |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] oluşturun -2) Bu depoyu bilgisayarınıza [klonlayın |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] -3) Ardından [ilgili dalda |#Dokümantasyon yapısı] değişiklikleri yapın -4) [Code-Checker |code-checker:] aracını kullanarak metindeki fazla boşlukları kontrol edin -5) Değişiklikleri kaydedin (commit) -6) Değişikliklerden memnunsanız, bunları GitHub'daki 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 gönderin +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. -Yorumlarla geri bildirim almanız yaygındır. Önerilen değişiklikleri takip edin ve dahil edin. Önerilen değişiklikleri yeni commit'ler olarak ekleyin ve tekrar GitHub'a gönderin. Bir pull request'i düzenlemek için asla yeni bir pull request oluşturmayın. +Ö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. -Dokümantasyon yapısı --------------------- +Belgelerin Yapısı +----------------- -Tüm dokümantasyon GitHub'da [nette/docs |https://github.com/nette/docs] deposunda bulunur. Geçerli sürüm master dalındadır, eski sürümler `doc-3.x`, `doc-2.x` gibi dallarda bulunur. +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, dokümantasyonun ayrı alanlarını temsil eden ana klasörlere ayrılmıştır. Örneğin, `application/` https://doc.nette.org/cs/application adresine karşılık gelir, `latte/` 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 dokümantasyon sayfalarına eklenebilen resimleri içeren `files` alt klasörünü içerir. +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 index 650cfe37eb..f126215b10 100644 --- a/contributing/tr/syntax.texy +++ b/contributing/tr/syntax.texy @@ -1,51 +1,51 @@ -Dokümantasyon sözdizimi -*********************** +Belge Sözdizimi +*************** -Dokümantasyon, bazı uzantılarla birlikte Markdown & [Texy sözdizimini |https://texy.info/cs/syntax] kullanır. +Belgeler, birkaç geliştirmeyle birlikte Markdown ve [Texy sözdizimini |https://texy.nette.org/syntax] kullanır. Bağlantılar =========== -Dahili bağlantılar için köşeli parantez `[bağlantı]` gösterimi kullanılır. Ya dikey çizgi ile `[bağlantı metni |bağlantı hedefi]` şeklinde ya da hedef metinle aynıysa (küçük harflere ve tirelere dönüştürüldükten sonra) kısaltılmış olarak `[bağlantı metni | Orijinal bağlantı metni]` şeklinde: +İç 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ı | Page name]` -> `<a href="/tr/page-name">Sayfa adı</a>` -- `[bağlantı metni |Page name]` -> `<a href="/tr/page-name">bağlantı metni</a>` +- `[Sayfa adı]` -> `<a href="/tr/sayfa-adi">Sayfa adı</a>` +- `[bağlantı metni |Sayfa adı]` -> `<a href="/tr/sayfa-adi">bağlantı metni</a>` -Farklı bir dil sürümüne veya farklı bir bölüme bağlantı verebiliriz. Bölüm, bir Nette kütüphanesi (örneğin `forms`, `latte`, vb.) veya `best-practices`, `quickstart` gibi özel bölümler anlamına gelir: +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ı | cs:Page name]` -> `<a href="/cs/page-name">Sayfa adı</a>` (aynı bölüm, farklı dil) -- `[tracy:Sayfa adı | tracy:Page name]` -> `<a href="//tracy.nette.org/tr/page-name">Sayfa adı</a>` (farklı bölüm, aynı dil) -- `[tracy:cs:Sayfa adı | tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Sayfa adı</a>` (farklı bölüm ve dil) +- `[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ığa da hedef belirlemek mümkündür. +`#` kullanarak sayfadaki belirli bir başlığı hedeflemek de mümkündür. -- `[#Heading]` -> `<a href="#toc-heading">Başlık</a>` (geçerli sayfadaki başlık) -- `[Page name#Heading]` -> `<a href="/tr/page-name#toc-heading">Sayfa adı</a>` +- `[#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 giriş sayfasına bağlantı: (`@home`, bölümün ana sayfası için özel bir terimdir) +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 dokümantasyonuna bağlantılar --------------------------------- +API Belgelerine Bağlantılar +--------------------------- -Her zaman yalnızca bu gösterimi kullanarak belirtin: +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 bahsedişte kullanın. Sonraki bağlantılar için basitleştirilmiş adı kullanın: +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 dokümantasyonuna bağlantılar --------------------------------- +PHP Belgelerine Bağlantılar +--------------------------- - `[php:substr]` -> [php:substr] @@ -53,7 +53,7 @@ PHP dokümantasyonuna bağlantılar Kaynak Kodu =========== -Kod bloğu <code>```lang</code> ile başlar ve <code>```</code> ile biter. Desteklenen diller `php`, `latte`, `neon`, `html`, `css`, `js` ve `sql`'dir. Girintileme için her zaman sekmeleri kullanın. +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 @@ -63,7 +63,7 @@ Kod bloğu <code>```lang</code> ile başlar ve <code>``` ``` ``` -Ayrıca dosya adını <code>```php .{file: ArrayTest.php}</code> olarak belirtebilirsiniz ve kod bloğu şu şekilde oluşturulur: +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) @@ -75,68 +75,69 @@ public function renderPage($id) Başlıklar ========= -En üst başlığın (yani sayfa adının) altını yıldızlarla çizin. Bölümleri ayırmak için eşittir işaretleri kullanın. Başlıkların altını eşittir işaretleri ve ardından tirelerle çizin: +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 Uygulamaları & presenterlar -******************************* +MVC Applications & Presenters +***************************** ... -Bağlantı oluşturma -================== +Link Creation +============= ... -Şablonlardaki bağlantılar -------------------------- +Links in Templates +------------------ ... ``` -Çerçeveler ve stiller -===================== +Kutular ve Stiller +================== -Perex'i `.[perex]` sınıfıyla işaretleriz .[perex] +`.[perex]` sınıfıyla işaretlenmiş perex .[perex] -Notu `.[note]` sınıfıyla işaretleriz .[note] +`.[note]` sınıfıyla işaretlenmiş not .[note] -İpucunu `.[tip]` sınıfıyla işaretleriz .[tip] +`.[tip]` sınıfıyla işaretlenmiş ipucu .[tip] -Uyarıyı `.[caution]` sınıfıyla işaretleriz .[caution] +`.[caution]` sınıfıyla işaretlenmiş dikkat .[caution] -Daha güçlü bir uyarıyı `.[warning]` sınıfıyla işaretleriz .[warning] +`.[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ı satırdan önce yazın: +Sınıflar, uygulandıkları satırın önüne yazılmalıdır: ``` .[perex] Bu perex'tir. ``` -Lütfen `.[tip]` gibi çerçevelerin gözleri "çektiğini" unutmayın, bu nedenle daha az önemli bilgiler için değil, vurgulamak için kullanılırlar. Bu nedenle, kullanımlarını en aza indirin. +`.[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 =========== -İçindekiler (sağ menüdeki bağlantılar), boyutu 4.000 baytı aşan tüm sayfalar için otomatik olarak oluşturulur ve bu varsayılan davranış [#Meta etiketleri] `{{toc}}` kullanılarak ayarlanabilir. İçeriği oluşturan metin standart olarak doğrudan başlıkların metninden alınır, ancak `.{toc}` değiştiricisi kullanılarak içerikte farklı bir metin görüntülenebilir, bu özellikle daha uzun başlıklar için kullanışlıdır. +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: İçerikte gösterilen herhangi bir başka metin} -========================================================================== +Uzun ve Akıllı Başlık .{toc: TOC için farklı bir metin} +======================================================= ``` -Meta etiketleri +Meta Etiketleri =============== -- özel sayfa başlığı ayarlama (`<title>` ve içerik haritası navigasyonunda) `{{title: Başka bir başlık}}` -- yönlendirme `{{redirect: pla:cs}}` - bkz. [#Bağlantılar] -- otomatik içeriği (tek tek başlıklara bağlantılar içeren kutu) zorlama `{{toc}}` veya devre dışı bırakma `{{toc: no}}` +- Ö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/contributing/uk/@home.texy b/contributing/uk/@home.texy deleted file mode 100644 index befb74c6c5..0000000000 --- a/contributing/uk/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Станьте контриб'ютором Nette -**************************** - -.[perex] -Дізнайтеся, як ви можете долучитися до нашого open source проекту. Ознайомтеся з процедурами внесення внеску у вихідний код та документацію та станьте частиною спільноти розробників, які активно беруть участь у вдосконаленні Nette. - - -**Код** - -- [Як зробити внесок у код? |code] -- [Стандарт кодування |coding-standard] - -**Документація** - -- [Як зробити внесок у документацію? |documentation] -- [Синтаксис документації |syntax] -- "Редактор попереднього перегляду":https://editor.nette.org diff --git a/contributing/uk/@left-menu.texy b/contributing/uk/@left-menu.texy deleted file mode 100644 index 6a23fcbcb1..0000000000 --- a/contributing/uk/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Код -*** -- [Як зробити внесок у код? |code] -- [Стандарт кодування |coding-standard] - -Документація -************ -- [Як зробити внесок у документацію? |documentation] -- [Синтаксис документації |syntax] -- "Редактор попереднього перегляду":https://editor.nette.org diff --git a/contributing/uk/code.texy b/contributing/uk/code.texy deleted file mode 100644 index 5e0e04479c..0000000000 --- a/contributing/uk/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Як зробити внесок у код -*********************** - -.[perex] -Ви збираєтеся зробити внесок у Nette Framework і вам потрібно розібратися в правилах та процедурах? Цей посібник для початківців крок за кроком покаже вам, як ефективно робити внесок у код, працювати з репозиторіями та впроваджувати зміни. - - -Процедура -========= - -Для того, щоб зробити внесок у код, необхідно мати обліковий запис на [GitHub|https://github.com] та бути знайомим з основами роботи з системою контролю версій Git. Якщо ви не володієте роботою з Git, можете ознайомитися з посібником [git - the simple guide |https://rogerdudler.github.io/git-guide/] та, за потреби, скористатися одним з багатьох [графічних клієнтів |https://git-scm.com/downloads/guis]. - - -Підготовка середовища та репозиторію ------------------------------------- - -1) на GitHub створіть [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] репозиторію [пакета |www:packages], який ви збираєтеся змінити -2) цей репозиторій [клонуєте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] на свій комп'ютер -3) встановіть залежності, включно з [Nette Tester |tester:], за допомогою команди `composer install` -4) перевірте, чи працюють тести, запустивши `composer tester` -5) створіть [нову гілку |#Нова гілка] на основі останньої випущеної версії - - -Реалізація власних змін ------------------------ - -Тепер ви можете внести свої власні зміни до коду: - -1) запрограмуйте необхідні зміни та не забудьте про тести -2) переконайтеся, що тести проходять успішно, за допомогою `composer tester` -3) перевірте, чи код відповідає [стандарту кодування |#Стандарти кодування] -4) збережіть зміни (зробіть коміт) з описом у [цьому форматі |#Опис коміту] - -Ви можете створити кілька комітів, по одному для кожного логічного кроку. Кожен коміт повинен бути осмисленим сам по собі. - - -Надсилання змін ---------------- - -Як тільки ви будете задоволені змінами, можете їх надіслати: - -1) надішліть (push) зміни на GitHub у ваш форк -2) звідти надішліть їх до репозиторію Nette, створивши [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) надайте в описі [достатньо інформації |#Опис pull request] - - -Врахування зауважень --------------------- - -Ваші коміти тепер побачать і інші. Зазвичай ви отримуватимете коментарі із зауваженнями: - -1) слідкуйте за запропонованими змінами -2) врахуйте їх як нові коміти або [об'єднайте з попередніми |https://help.github.com/en/github/using-git/about-git-rebase] -3) знову надішліть коміти на GitHub, і вони автоматично з'являться в pull request - -Ніколи не створюйте новий pull request для зміни існуючого. - - -Документація ------------- - -Якщо ви змінили функціональність або додали нову, не забудьте також [додати це до документації |documentation]. - - -Нова гілка -========== - -Якщо це можливо, вносьте зміни щодо останньої випущеної версії, тобто останнього тегу в даній гілці. Для тегу `v3.2.1` ви створите гілку цією командою: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Стандарти кодування -=================== - -Ваш код повинен відповідати [стандарту кодування |Coding Standard], що використовується в Nette Framework. Для перевірки та виправлення коду доступний автоматичний інструмент. Його можна встановити через Composer **глобально** у вибрану вами папку: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Тепер ви повинні мати можливість запустити інструмент у терміналі. Першою командою ви перевірите, а другою – виправите код у папках `src` та `tests` у поточному каталозі: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Опис коміту -=========== - -У Nette теми комітів мають формат: `Presenter: виправлено виявлення AJAX [Closes #69]` - -- область, за якою слідує двокрапка -- мета коміту в минулому часі, якщо можливо, почніть зі слова: `added` (додано нову властивість), `fixed` (виправлення), `refactored` (зміна в коді без зміни поведінки), `changed`, `removed` -- якщо коміт порушує зворотну сумісність, додайте "BC break" -- можливий зв'язок з трекером проблем, як `(#123)` або `[Closes #69]` -- за темою може слідувати один порожній рядок, а потім детальніший опис, включно з, наприклад, посиланнями на форум - - -Опис pull request -================= - -При створенні pull request інтерфейс GitHub дозволить вам ввести назву та опис. Вкажіть змістовну назву, а в описі надайте якомога більше інформації про причини вашої зміни. - -Також відобразиться заголовок, де вкажіть, чи це нова функція, чи виправлення помилки, і чи може відбутися порушення зворотної сумісності (BC break). Якщо є пов'язана проблема (issue), посилайтеся на неї, щоб її було закрито після схвалення pull request. - -``` -- виправлення помилки / нова функція? <!-- #номери issue, якщо є --> -- BC break? так/ні -- doc PR: nette/docs#? <!-- дуже вітається, див. https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/uk/coding-standard.texy b/contributing/uk/coding-standard.texy deleted file mode 100644 index 20e5a5e9a9..0000000000 --- a/contributing/uk/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Стандарт кодування -****************** - -.[perex] -Цей документ описує правила та рекомендації для розробки Nette. При внесенні коду до Nette ви повинні їх дотримуватися. Найпростіший спосіб зробити це – наслідувати існуючий код. Мета полягає в тому, щоб весь код виглядав так, ніби його написала одна людина. - -Стандарт кодування Nette відповідає [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] з двома основними винятками: для відступів він використовує [#табуляції замість пробілів] та для [констант класів використовує PascalCase|https://blog.nette.org/uk/for-less-screaming-in-the-code]. - - -Загальні правила -================ - -- Кожен файл PHP повинен містити `declare(strict_types=1)` -- Два порожні рядки використовуються для розділення методів для кращої читабельності. -- Причина використання оператора приглушення помилок (`@`) повинна бути задокументована: `@mkdir($dir); // @ - каталог може існувати`. -- Якщо використовується оператор порівняння зі слабкою типізацією (тобто `==`, `!=`, ...), намір повинен бути задокументований: `// == прийняти null` -- В один файл `exceptions.php` можна записати кілька винятків. -- Для інтерфейсів не вказується видимість методів, оскільки вони завжди публічні. -- Кожна властивість, повернене значення та параметр повинні мати вказаний тип. Навпаки, для фінальних констант тип ніколи не вказуємо, оскільки він очевидний. -- Для обмеження рядка слід використовувати одинарні лапки (`'`), за винятком випадків, коли сам літерал містить апострофи. - - -Угоди про іменування -==================== - -- Не використовуйте скорочення, якщо повна назва не надто довга. -- Для дволітерних скорочень використовуйте великі літери, для довших скорочень – Pascal/camelCase. -- Для назви класу використовуйте іменник або словосполучення. -- Назви класів повинні містити не лише специфічність (`Array`), але й загальність (`ArrayIterator`). Винятком є атрибути мови PHP. -- "Константи класів та enum-и повинні використовувати PascalCase":https://blog.nette.org/uk/for-less-screaming-in-the-code. -- "Інтерфейси та абстрактні класи не повинні містити префіксів або суфіксів":https://blog.nette.org/uk/prefixes-and-suffixes-do-not-belong-in-interface-names як `Abstract`, `Interface` або `I`. - - -Перенесення та фігурні дужки -============================ - -Стандарт кодування Nette відповідає PSR-12 (або PER Coding Style), в деяких пунктах доповнює або змінює його: - -- стрілкові функції пишуться без пробілу перед дужкою, тобто `fn($a) => $b` -- не вимагається порожній рядок між різними типами імпортів `use` -- тип повернення функції/методу та початкова фігурна дужка завжди знаходяться на окремих рядках: - -```php - public function find( - string $dir, - array $options, - ): array - { - // тіло методу - } -``` - -Початкова фігурна дужка на окремому рядку важлива для візуального розділення сигнатури функції/методу від тіла. Якщо сигнатура знаходиться на одному рядку, розділення очевидне (зображення зліва), якщо на кількох рядках, у PSR сигнатури та тіла зливаються (посередині), тоді як у стандарті Nette вони залишаються розділеними (праворуч): - -[* new-line-after.webp *] - - -Блоки документації (phpDoc) -=========================== - -Основне правило: Ніколи не дублюйте жодної інформації в сигнатурі, такої як тип параметра або тип повернення, без доданої вартості. - -Блок документації для визначення класу: - -- Починається з опису класу. -- Слідує порожній рядок. -- Слідують анотації `@property` (або `@property-read`, `@property-write`), одна за одною. Синтаксис: анотація, пробіл, тип, пробіл, `$ім'я`. -- Слідують анотації `@method`, одна за одною. Синтаксис: анотація, пробіл, тип повернення, пробіл, ім'я(тип $param, ...). -- Анотація `@author` пропускається. Авторство зберігається в історії вихідного коду. -- Можна використовувати анотації `@internal` або `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Блок документації для властивості, який містить лише анотацію `@var`, повинен бути однорядковим: - -```php -/** @var string[] */ -private array $name; -``` - -Блок документації для визначення методу: - -- Починається з короткого опису методу. -- Жодного порожнього рядка. -- Анотації `@param` по окремих рядках. -- Анотація `@return`. -- Анотації `@throws`, одна за одною. -- Можна використовувати анотації `@internal` або `@deprecated`. - -За кожною анотацією слідує один пробіл, за винятком `@param`, за якою для кращої читабельності слідують два пробіли. - -```php -/** - * Знаходить файл у каталозі. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Табуляції замість пробілів -========================== - -Табуляції мають кілька переваг перед пробілами: - -- розмір відступу можна налаштувати в редакторах та на `"веб":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size` -- не нав'язують коду переваги користувача щодо розміру відступу, тому код краще переноситься -- їх можна написати одним натисканням клавіші (будь-де, не тільки в редакторах, які перетворюють табуляції на пробіли) -- відступи – це їхній сенс -- поважають потреби колег з вадами зору та незрячих - -Використовуючи табуляції в наших проектах, ми дозволяємо налаштовувати ширину, що може здатися більшості людей зайвим, але для людей з вадами зору є необхідним. - -Для незрячих програмістів, які використовують брайлівські дисплеї, кожен пробіл представляє одну брайлівську комірку. Якщо стандартний відступ становить 4 пробіли, відступ 3-го рівня марнує 12 цінних брайлівських комірок ще до початку коду. На 40-комірковому дисплеї, який найчастіше використовується для ноутбуків, це більше чверті доступних комірок, які марнуються без будь-якої інформації. - - -{{priority: -1}} diff --git a/contributing/uk/documentation.texy b/contributing/uk/documentation.texy deleted file mode 100644 index 95b53fa278..0000000000 --- a/contributing/uk/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Як зробити внесок у документацію -******************************** - -.[perex] -Внесок у документацію є однією з найкорисніших діяльностей, оскільки ви допомагаєте іншим зрозуміти фреймворк. - - -Як писати? ----------- - -Документація призначена насамперед для людей, які знайомляться з темою. Тому вона повинна відповідати кільком важливим пунктам: - -- Починайте з простого та загального. До більш складних тем переходьте лише наприкінці. -- Намагайтеся пояснити річ якомога краще. Спробуйте, наприклад, спочатку пояснити тему колезі. -- Наводьте лише ту інформацію, яка дійсно потрібна користувачеві для даної теми. -- Перевірте, чи ваша інформація дійсно правдива. Кожен код протестуйте. -- Будьте лаконічними - те, що напишете, скоротіть наполовину. А потім, можливо, ще раз. -- Економте на виділеннях усіх видів, від жирного шрифту до рамок типу `.[note]`. -- У кодах дотримуйтесь [стандарту кодування |Coding Standard]. - -Освойте також [синтаксис |syntax]. Для попереднього перегляду статті під час її написання можете використовувати [редактор з попереднім переглядом |https://editor.nette.org/]. - - -Мовні версії ------------- - -Основною мовою є англійська, тому ваші зміни повинні бути як чеською, так і англійською. Якщо англійська не є вашою сильною стороною, використовуйте [DeepL Translator |https://www.deepl.com/translator], а інші перевірять ваш текст. - -Переклад на інші мови буде виконано автоматично після схвалення та доопрацювання вашої правки. - - -Тривіальні правки ------------------ - -Для внесення внеску в документацію необхідно мати обліковий запис на [GitHub|https://github.com]. - -Найпростіший спосіб внести невелику зміну в документацію – скористатися посиланнями в кінці кожної сторінки: - -- *Показати на GitHub* відкриє вихідний код даної сторінки на GitHub. Потім достатньо натиснути кнопку `E`, і ви можете почати редагувати (необхідно бути авторизованим на GitHub). -- *Відкрити попередній перегляд* відкриє редактор, де ви одразу побачите і кінцевий візуальний вигляд. - -Оскільки [редактор з попереднім переглядом |https://editor.nette.org/] не має можливості зберігати зміни безпосередньо на GitHub, необхідно після завершення редагування скопіювати вихідний текст у буфер обміну (кнопкою *Copy to clipboard*), а потім вставити його в редактор на GitHub. Під полем редагування є форма для надсилання. Тут не забудьте коротко підсумувати та пояснити причину вашої правки. Після надсилання створюється так званий pull request (PR), який можна далі редагувати. - - -Більші правки -------------- - -Більш доцільно, ніж використовувати інтерфейс GitHub, бути знайомим з основами роботи з системою контролю версій Git. Якщо ви не володієте роботою з Git, можете ознайомитися з посібником [git - the simple guide |https://rogerdudler.github.io/git-guide/] та, за потреби, скористатися одним з багатьох [графічних клієнтів |https://git-scm.com/downloads/guis]. - -Документацію редагуйте таким чином: - -1) на GitHub створіть [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] репозиторію [nette/docs |https://github.com/nette/docs] -2) цей репозиторій [клонуєте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] на свій комп'ютер -3) потім у [відповідній гілці |#Структура документації] внесіть зміни -4) перевірте зайві пробіли в тексті за допомогою інструменту [Code-Checker |code-checker:] -4) збережіть зміни (зробіть коміт) -6) якщо ви задоволені змінами, надішліть (push) їх на GitHub у ваш форк -7) звідти надішліть їх до репозиторію `nette/docs`, створивши [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) - -Зазвичай ви отримуватимете коментарі із зауваженнями. Слідкуйте за запропонованими змінами та враховуйте їх. Запропоновані зміни додайте як нові коміти та знову надішліть на GitHub. Ніколи не створюйте новий pull request для зміни існуючого pull request. - - -Структура документації ----------------------- - -Вся документація розміщена на GitHub у репозиторії [nette/docs |https://github.com/nette/docs]. Поточна версія знаходиться в гілці `master`, старіші версії розміщені в гілках, таких як `doc-3.x`, `doc-2.x`. - -Вміст кожної гілки поділяється на основні папки, що представляють окремі області документації. Наприклад, `application/` відповідає https://doc.nette.org/cs/application, `latte/` відповідає https://latte.nette.org тощо. Кожна така папка містить підпапки, що представляють мовні версії (`cs`, `en`, ...), та, за потреби, підпапку `files` із зображеннями, які можна вставляти на сторінки документації. diff --git a/contributing/uk/syntax.texy b/contributing/uk/syntax.texy deleted file mode 100644 index de3c97be92..0000000000 --- a/contributing/uk/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Синтаксис документації -********************** - -Документація використовує Markdown та [синтаксис Texy |https://texy.info/cs/syntax] з деякими розширеннями. - - -Посилання -========= - -Для внутрішніх посилань використовується запис у квадратних дужках `[посилання]`. Це може бути або у формі з вертикальною рискою `[текст посилання |ціль посилання]`, або скорочено `[текст посилання]`, якщо ціль збігається з текстом (після перетворення на малі літери та дефіси): - -- `[Назва сторінки |Page name]` -> `<a href="/uk/page-name">Назва сторінки</a>` -- `[текст посилання |Page name]` -> `<a href="/uk/page-name">текст посилання</a>` - -Ми можемо посилатися на іншу мовну версію або інший розділ. Розділом вважається бібліотека Nette (наприклад, `forms`, `latte` тощо) або спеціальні розділи, такі як `best-practices`, `quickstart` тощо: - -- `[cs:Назва сторінки |cs:Page name]` -> `<a href="/cs/page-name">Назва сторінки</a>` (той самий розділ, інша мова) -- `[tracy:Назва сторінки |tracy:Page name]` -> `<a href="//tracy.nette.org/uk/page-name">Назва сторінки</a>` (інший розділ, та сама мова) -- `[tracy:cs:Назва сторінки |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Назва сторінки</a>` (інший розділ та мова) - -За допомогою `#` також можна націлитися на конкретний заголовок на сторінці. - -- `[Заголовок |#Heading]` -> `<a href="#toc-heading">Заголовок</a>` (заголовок на поточній сторінці) -- `[Назва сторінки#Заголовок |Page name#Heading]` -> `<a href="/uk/page-name#toc-heading">Назва сторінки</a>` - -Посилання на головну сторінку розділу: (`@home` – це спеціальний вираз для домашньої сторінки розділу) - -- `[текст посилання |@home]` -> `<a href="/uk/">текст посилання</a>` -- `[текст посилання |tracy:]` -> `<a href="//tracy.nette.org/uk/">текст посилання</a>` - - -Посилання на документацію API ------------------------------ - -Завжди вказуйте лише за допомогою цього запису: - -- `[api:Nette\SmartObject]` -> [api:Nette\SmartObject] -- `[api:Nette\Forms\Form::setTranslator()]` -> [api:Nette\Forms\Form::setTranslator()] -- `[api:Nette\Forms\Form::$onSubmit]` -> [api:Nette\Forms\Form::$onSubmit] -- `[api:Nette\Forms\Form::Required]` -> [api:Nette\Forms\Form::Required] - -Повністю кваліфіковані назви використовуйте лише при першій згадці. Для подальших посилань використовуйте спрощену назву: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Посилання на документацію PHP ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Вихідний код -============ - -Блок коду починається з <code>```lang</code> і закінчується <code>```</code>. Підтримувані мови: `php`, `latte`, `neon`, `html`, `css`, `js` та `sql`. Для відступів завжди використовуйте табуляції. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Ви також можете вказати ім'я файлу як <code>```php .{file: ArrayTest.php}</code>, і блок коду буде відрендерено таким чином: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Заголовки -========= - -Найвищий заголовок (тобто назву сторінки) підкресліть зірочками. Для розділення секцій використовуйте знаки рівності. Заголовки підкреслюйте знаками рівності, а потім дефісами: - -``` -MVC Додатки & презентери -************************ -... - - -Створення посилань -================== -... - - -Посилання в шаблонах --------------------- -... -``` - - -Рамки та стилі -============== - -Перекс позначимо класом `.[perex]` .[perex] - -Примітку позначимо класом `.[note]` .[note] - -Пораду позначимо класом `.[tip]` .[tip] - -Застереження позначимо класом `.[caution]` .[caution] - -Більш сильне застереження позначимо класом `.[warning]` .[warning] - -Номер версії `.{data-version:2.4.10}` .{data-version:2.4.10} - -Класи записуйте перед рядком: - -``` -.[perex] -Це перекс. -``` - -Будь ласка, усвідомте, що рамки, такі як `.[tip]`, "притягують" очі, тому їх використовують для підкреслення, а не для менш важливої інформації. Тому максимально економте їх використання. - - -Зміст -===== - -Зміст (посилання в правому меню) генерується автоматично для всіх сторінок, розмір яких перевищує 4 000 байт, причому цю стандартну поведінку можна змінити за допомогою [#Метатеги] `{{toc}}`. Текст, що утворює зміст, стандартно береться безпосередньо з тексту заголовків, але за допомогою модифікатора `.{toc}` можна відобразити в змісті інший текст, що особливо корисно для довших заголовків. - -``` - - -Довгий та розумний заголовок .{toc: Будь-який інший текст, відображений у змісті} -================================================================================= -``` - - -Метатеги -======== - -- встановлення власної назви сторінки (у `<title>` та навігаційному ланцюжку) `{{title: Інша назва}}` -- перенаправлення `{{redirect: pla:cs}}` - див. [#Посилання] -- примусове `{{toc}}` або заборона `{{toc: no}}` автоматичного змісту (блок з посиланнями на окремі заголовки) - -{{priority: -1}} diff --git a/database/bg/@home.texy b/database/bg/@home.texy deleted file mode 100644 index 7eccf1677b..0000000000 --- a/database/bg/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Поддържани бази данни -===================== - -Nette поддържа следните бази данни: - -|* Сървър на база данни |* DSN име |* Поддръжка в Core |* Поддръжка в Explorer -| MySQL (>= 5.1) | mysql | ДА | ДА -| PostgreSQL (>= 9.0) | pgsql | ДА | ДА -| Sqlite 3 (>= 3.8) | sqlite | ДА | ДА -| Oracle | oci | ДА | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ДА | ДА -| MS SQL (PDO_DBLIB) | mssql | ДА | - -| ODBC | odbc | ДА | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database значително улеснява извличането на данни от базата данни, без да е необходимо да се пишат SQL заявки. Изпълнява ефективни заявки и не прехвърля излишни данни.}} diff --git a/database/bg/@left-menu.texy b/database/bg/@left-menu.texy deleted file mode 100644 index 928b1f73ea..0000000000 --- a/database/bg/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Въведение |guide] -- [SQL достъп |sql way] -- [Explorer |Explorer] -- [Трансакции |transactions] -- [Изключения |exceptions] -- [Рефлексия |reflection] -- [Мапинг |mapping] -- [Конфигурация |configuration] -- [Рискове за сигурността |security] -- [Надграждане |en:upgrading] diff --git a/database/bg/@meta.texy b/database/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/database/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/database/bg/configuration.texy b/database/bg/configuration.texy deleted file mode 100644 index 68d49c5011..0000000000 --- a/database/bg/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Конфигурация на базата данни -**************************** - -.[perex] -Преглед на конфигурационните опции за Nette Database. - -Ако не използвате целия framework, а само тази библиотека, прочетете [как да заредите конфигурацията|bootstrap:]. - - -Една връзка ------------ - -Конфигурация на една връзка към база данни: - -```neon -database: - # DSN, единственият задължителен ключ - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Създава сървисите `Nette\Database\Connection` и `Nette\Database\Explorer`, които обикновено предаваме чрез [autowiring |dependency-injection:autowiring], или чрез връзка към [тяхното име |#DI Сървиси]. - -Други настройки: - -```neon -database: - # покажи панела на базата данни в Tracy Bar? - debugger: ... # (bool) по подразбиране е true - - # покажи EXPLAIN на заявките в Tracy Bar? - explain: ... # (bool) по подразбиране е true - - # разреши autowiring за тази връзка? - autowired: ... # (bool) по подразбиране е true при първата връзка - - # конвенции за таблици: discovered, static или име на клас - conventions: discovered # (string) по подразбиране е 'discovered' - - options: - # свързване към базата данни само когато е необходимо? - lazy: ... # (bool) по подразбиране е false - - # PHP клас на драйвера на базата данни - driverClass: # (string) - - # само MySQL: задава sql_mode - sqlmode: # (string) - - # само MySQL: задава SET NAMES - charset: # (string) по подразбиране е 'utf8mb4' - - # само MySQL: преобразува TINYINT(1) в bool - convertBoolean: # (bool) по подразбиране е false - - # връща колони с дата като immutable обекти (от версия 3.2.1) - newDateTime: # (bool) по подразбиране е false - - # само Oracle и SQLite: формат за съхранение на дата - formatDateTime: # (string) по подразбиране е 'U' -``` - -В ключа `options` могат да се посочват други опции, които ще намерите в [документацията на PDO драйверите |https://www.php.net/manual/en/pdo.drivers.php], като например: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Множество връзки ----------------- - -В конфигурацията можем да дефинираме и множество връзки към бази данни, като ги разделим на именувани секции: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring е включен само за сървисите от първата секция. Това може да се промени с помощта на `autowired: false` или `autowired: true`. - - -DI Сървиси ----------- - -Тези сървиси се добавят към DI контейнера, където `###` представлява името на връзката: - -| Име | Тип | Описание -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | връзка с базата данни -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |database:explorer] - - -Ако дефинираме само една връзка, имената на сървисите ще бъдат `database.default.connection` и `database.default.explorer`. Ако дефинираме повече връзки, както в примера по-горе, имената ще отговарят на секциите, т.е. `database.main.connection`, `database.main.explorer` и след това `database.another.connection` и `database.another.explorer`. - -Сървисите, които не са autowired, предаваме изрично чрез връзка към тяхното име: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/bg/exceptions.texy b/database/bg/exceptions.texy deleted file mode 100644 index 92a7983581..0000000000 --- a/database/bg/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Изключения -********** - -Nette Database използва йерархия от изключения. Основният клас е `Nette\Database\DriverException`, който наследява от `PDOException` и предоставя разширени възможности за работа с грешки в базата данни: - -- Методът `getDriverCode()` връща кода на грешката от драйвера на базата данни -- Методът `getSqlState()` връща SQLSTATE кода -- Методите `getQueryString()` и `getParameters()` позволяват да се получи оригиналната заявка и нейните параметри - -От `DriverException` наследяват следните специализирани изключения: - -- `ConnectionException` - сигнализира за неуспешно свързване към сървъра на базата данни -- `ConstraintViolationException` - основен клас за нарушаване на ограниченията на базата данни, от който наследяват: - - `ForeignKeyConstraintViolationException` - нарушаване на външен ключ - - `NotNullConstraintViolationException` - нарушаване на ограничението NOT NULL - - `UniqueConstraintViolationException` - нарушаване на уникалността на стойността - - -Пример за прихващане на изключение `UniqueConstraintViolationException`, което възниква, когато се опитваме да вмъкнем потребител с имейл, който вече съществува в базата данни (при условие, че колоната email има уникален индекс). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Потребител с този имейл вече съществува.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Възникна грешка при регистрацията: ' . $e->getMessage(); -} -``` diff --git a/database/bg/explorer.texy b/database/bg/explorer.texy deleted file mode 100644 index b8d1c4a799..0000000000 --- a/database/bg/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer предлага интуитивен и ефективен начин за работа с базата данни. Той автоматично се грижи за релациите между таблиците и оптимизацията на заявките, така че можете да се съсредоточите върху своето приложение. Работи веднага без настройка. Ако се нуждаете от пълен контрол над SQL заявките, можете да използвате [SQL достъп |database:sql-way]. - -- Работата с данни е естествена и лесно разбираема -- Генерира оптимизирани SQL заявки, които зареждат само необходимите данни -- Позволява лесен достъп до свързани данни без необходимост от писане на JOIN заявки -- Работи незабавно без каквато и да е конфигурация или генериране на ентитита - -</div> - - -С Explorer започвате с извикване на метода `table()` на обекта [api:Nette\Database\Explorer] (подробности за връзката ще намерите в главата [Връзка и конфигурация |database:configuration]): - -```php -$books = $explorer->table('book'); // 'book' е името на таблицата -``` - -Методът връща обект [Selection |api:Nette\Database\Table\Selection], който представлява SQL заявка. Към този обект можем да навързваме други методи за филтриране и сортиране на резултатите. Заявката се съставя и изпълнява едва в момента, когато започнем да изискваме данни. Например, чрез преминаване през цикъл `foreach`. Всеки ред е представен от обект [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // извеждане на колона 'title' - echo $book->author_id; // извеждане на колона 'author_id' -} -``` - -Explorer значително улеснява работата с [#релации между таблици]. Следващият пример показва колко лесно можем да изведем данни от свързани таблици (книги и техните автори). Обърнете внимание, че не е необходимо да пишем никакви JOIN заявки, Nette ги създава за нас: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Книга: ' . $book->title; - echo 'Автор: ' . $book->author->name; // създава JOIN към таблица 'author' -} -``` - -Nette Database Explorer оптимизира заявките, за да бъдат възможно най-ефективни. Горепосоченият пример ще изпълни само две SELECT заявки, независимо дали обработваме 10 или 10 000 книги. - -Освен това Explorer следи кои колони се използват в кода и зарежда от базата данни само тях, като по този начин спестява допълнителна производителност. Това поведение е напълно автоматично и адаптивно. Ако по-късно промените кода и започнете да използвате други колони, Explorer автоматично ще промени заявките. Не е необходимо нищо да настройвате, нито да мислите кои колони ще ви трябват - оставете това на Nette. - - -Филтриране и сортиране -====================== - -Класът `Selection` предоставя методи за филтриране и сортиране на избора на данни. - -.[language-php] -| `where($condition, ...$params)` | Добавя условие WHERE. Множество условия се свързват с оператор AND -| `whereOr(array $conditions)` | Добавя група условия WHERE, свързани с оператор OR -| `wherePrimary($value)` | Добавя условие WHERE по първичен ключ -| `order($columns, ...$params)` | Задава сортиране ORDER BY -| `select($columns, ...$params)` | Специфицира колоните, които трябва да бъдат заредени -| `limit($limit, $offset = null)` | Ограничава броя на редовете (LIMIT) и опционално задава OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Задава пагиниране -| `group($columns, ...$params)` | Групира редове (GROUP BY) -| `having($condition, ...$params)` | Добавя условие HAVING за филтриране на групирани редове - -Методите могат да се навързват (т.нар. [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -В тези методи можете също да използвате специална нотация за достъп до [данни от свързани таблици |#Заявки през свързани таблици]. - - -Екраниране и идентификатори ---------------------------- - -Методите автоматично екранират параметрите и ограждат идентификаторите (имената на таблици и колони) с кавички, като по този начин предотвратяват SQL injection. За правилното функциониране е необходимо да се спазват няколко правила: - -- Ключовите думи, имената на функции, процедури и т.н. пишете с **главни букви**. -- Имената на колони и таблици пишете с **малки букви**. -- Низовете винаги вмъквайте чрез **параметри**. - -```php -where('name = ' . $name); // КРИТИЧНА УЯЗВИМОСТ: SQL injection -where('name LIKE "%search%"'); // ГРЕШНО: усложнява автоматичното ограждане с кавички -where('name LIKE ?', '%search%'); // ПРАВИЛНО: стойност, вмъкната чрез параметър - -where('name like ?', $name); // ГРЕШНО: генерира: `name` `like` ? -where('name LIKE ?', $name); // ПРАВИЛНО: генерира: `name` LIKE ? -where('LOWER(name) = ?', $value);// ПРАВИЛНО: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Филтрира резултатите с помощта на условия WHERE. Силната му страна е интелигентната работа с различни типове стойности и автоматичният избор на SQL оператори. - -Основно използване: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Благодарение на автоматичното откриване на подходящи оператори не е необходимо да се занимаваме с различни специални случаи. Nette ги решава за нас: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// може да се използва и заместващ въпросителен знак без оператор: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Методът правилно обработва и отрицателни условия и празни масиви: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- нищо не намира -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- намира всичко -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- намира всичко -// $table->where('NOT id ?', $ids); Внимание - този синтаксис не се поддържа -``` - -Като параметър можем да предадем и резултат от друга таблица - създава се подзаявка: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Условията можем да предадем и като масив, чиито елементи се свързват с AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -В масива можем да използваме двойки ключ => стойност и Nette отново автоматично избира правилните оператори: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -В масива можем да комбинираме SQL изрази със заместващи въпросителни знаци и множество параметри. Това е подходящо за комплексни условия с точно дефинирани оператори: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // два параметъра предаваме като масив -]); -``` - -Многократното извикване на `where()` автоматично свързва условията с AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Подобно на `where()` добавя условия, но с тази разлика, че ги свързва с OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -И тук можем да използваме по-комплексни изрази: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Добавя условие за първичния ключ на таблицата: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Ако таблицата има композитен първичен ключ (напр. `foo_id`, `bar_id`), предаваме го като масив: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Определя реда, в който ще бъдат върнати редовете. Можем да сортираме по една или повече колони, в низходящ или възходящ ред, или по собствен израз: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Специфицира колоните, които трябва да бъдат върнати от базата данни. По подразбиране Nette Database Explorer връща само тези колони, които реално се използват в кода. Методът `select()` така използваме в случаите, когато трябва да върнем специфични изрази: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Псевдонимите, дефинирани с `AS`, след това са достъпни като свойства на обекта ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // достъп до псевдонима -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Ограничава броя на върнатите редове (LIMIT) и опционално позволява да се зададе offset: - -```php -$table->limit(10); // LIMIT 10 (връща първите 10 реда) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -За пагиниране е по-подходящо да се използва методът `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Улеснява пагинирането на резултатите. Приема номер на страницата (изчисляван от 1) и брой елементи на страница. Опционално може да се предаде референция към променлива, в която ще се съхрани общият брой страници: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Общо страници: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Групира редове според зададените колони (GROUP BY). Обикновено се използва във връзка с агрегатни функции: - -```php -// Преброява броя на продуктите във всяка категория -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Задава условие за филтриране на групирани редове (HAVING). Може да се използва във връзка с метода `group()` и агрегатни функции: - -```php -// Намира категории, които имат повече от 100 продукта -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Четене на данни -=============== - -За четене на данни от базата данни имаме на разположение няколко полезни метода: - -.[language-php] -| `foreach ($table as $key => $row)` | Итерира през всички редове, `$key` е стойността на първичния ключ, `$row` е обект ActiveRow -| `$row = $table->get($key)` | Връща един ред според първичния ключ -| `$row = $table->fetch()` | Връща текущия ред и премества указателя към следващия -| `$array = $table->fetchPairs()` | Създава асоциативен масив от резултатите -| `$array = $table->fetchAll()` | Връща всички редове като масив -| `count($table)` | Връща броя на редовете в обекта Selection - -Обектът [ActiveRow |api:Nette\Database\Table\ActiveRow] е предназначен само за четене. Това означава, че не може да се променят стойностите на неговите свойства. Това ограничение гарантира консистенцията на данните и предотвратява неочаквани странични ефекти. Данните се зареждат от базата данни и всяка промяна трябва да бъде извършена изрично и контролирано. - - -`foreach` - итерация през всички редове ---------------------------------------- - -Най-лесният начин да изпълните заявка и да получите редове е чрез итерация в цикъл `foreach`. Автоматично стартира SQL заявка. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key е стойността на първичния ключ, $book е ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Изпълнява SQL заявка и връща ред според първичния ключ, или `null`, ако не съществува. - -```php -$book = $explorer->table('book')->get(123); // връща ActiveRow с ID 123 или null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Връща ред и премества вътрешния указател към следващия. Ако вече не съществуват други редове, връща `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Връща резултатите като асоциативен масив. Първият аргумент определя името на колоната, която ще се използва като ключ в масива, вторият аргумент определя името на колоната, която ще се използва като стойност: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ако посочим само първия параметър, стойността ще бъде целият ред, т.е. обект `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -В случай на дублиращи се ключове се използва стойността от последния ред. При използване на `null` като ключ масивът ще бъде индексиран числово от нула (тогава не възникват колизии): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Алтернативно можете като параметър да посочите callback, който за всеки ред ще връща или самата стойност, или двойка ключ-стойност. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Първа книга (Ян Новак)', ...] - -// Callback може също да връща масив с двойка ключ & стойност: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Първа книга' => 'Ян Новак', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Връща всички редове като асоциативен масив от обекти `ActiveRow`, където ключовете са стойностите на първичните ключове. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Методът `count()` без параметър връща броя на редовете в обекта `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // алтернатива -``` - -Внимание, `count()` с параметър извършва агрегатна функция COUNT в базата данни, вижте по-долу. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Преобразува обект `ActiveRow` в асоциативен масив, където ключовете са имената на колоните, а стойностите са съответните данни. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray ще бъде ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Агрегиране -========== - -Класът `Selection` предоставя методи за лесно извършване на агрегатни функции (COUNT, SUM, MIN, MAX, AVG и т.н.). - -.[language-php] -| `count($expr)` | Преброява броя на редовете -| `min($expr)` | Връща минималната стойност в колоната -| `max($expr)` | Връща максималната стойност в колоната -| `sum($expr)` | Връща сумата на стойностите в колоната -| `aggregation($function)` | Позволява да се извърши произволна агрегатна функция. Напр. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Изпълнява SQL заявка с функцията COUNT и връща резултата. Методът се използва за установяване колко реда отговарят на определено условие: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Внимание, [#count()] без параметър само връща броя на редовете в обекта `Selection`. - - -min(string $expr) и max(string $expr) .[method] ------------------------------------------------ - -Методите `min()` и `max()` връщат минималната и максималната стойност в специфицираната колона или израз: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Връща сумата на стойностите в специфицираната колона или израз: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- - -Позволява да се извърши произволна агрегатна функция. - -```php -// средна цена на продуктите в категория -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// свързва етикетите на продукта в един низ -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Ако трябва да агрегираме резултати, които вече сами по себе си са произлезли от някаква агрегатна функция и групиране (напр. `SUM(стойност)` върху групирани редове), като втори аргумент посочваме агрегатната функция, която трябва да се приложи върху тези междинни резултати: - -```php -// Изчислява общата цена на продуктите на склад за отделните категории и след това сумира тези цени заедно. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -В този пример първо изчисляваме общата цена на продуктите във всяка категория (`SUM(price * stock) AS category_total`) и групираме резултатите по `category_id`. След това използваме `aggregation('SUM(category_total)', 'SUM')` за сумиране на тези междинни суми `category_total`. Вторият аргумент `'SUM'` казва, че върху междинните резултати трябва да се приложи функцията SUM. - - -Insert, Update & Delete -======================= - -Nette Database Explorer опростява вмъкването, актуализирането и изтриването на данни. Всички посочени методи в случай на грешка изхвърлят изключение `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Вмъква нови записи в таблицата. - -**Вмъкване на един запис:** - -Новият запис предаваме като асоциативен масив или iterable обект (например ArrayHash, използван във [формите |forms:]), където ключовете отговарят на имената на колоните в таблицата. - -Ако таблицата има дефиниран първичен ключ, методът връща обект `ActiveRow`, който се презарежда от базата данни, за да се отразят евентуалните промени, извършени на ниво база данни (тригери, стойности по подразбиране на колони, изчисления на auto-increment колони). По този начин се гарантира консистенцията на данните и обектът винаги съдържа актуалните данни от базата данни. Ако няма еднозначен първичен ключ, връща предадените данни под формата на масив. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row е инстанция на ActiveRow и съдържа пълните данни на вмъкнатия ред, -// включително автоматично генерираното ID и евентуалните промени, извършени от тригери -echo $row->id; // Извежда ID на нововмъкнатия потребител -echo $row->created_at; // Извежда времето на създаване, ако е зададено от тригер -``` - -**Вмъкване на няколко записа едновременно:** - -Методът `insert()` позволява да се вмъкнат няколко записа с една SQL заявка. В този случай връща броя на вмъкнатите редове. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows ще бъде 2 -``` - -Като параметър може също да се предаде обект `Selection` с избор на данни. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Вмъкване на специални стойности:** - -Като стойности можем да предаваме и файлове, обекти DateTime или SQL литерали: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // преобразува в база данни формат - 'avatar' => fopen('image.jpg', 'rb'), // вмъква бинарно съдържание на файла - 'uuid' => $explorer::literal('UUID()'), // извиква функцията UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Актуализира редове в таблицата според зададения филтър. Връща броя на действително променените редове. - -Променяните колони предаваме като асоциативен масив или iterable обект (например ArrayHash, използван във [формите |forms:]), където ключовете отговарят на имената на колоните в таблицата: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -За промяна на числови стойности можем да използваме операторите `+=` и `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // увеличава стойността на колоната 'points' с 1 - 'coins-=' => 1, // намалява стойността на колоната 'coins' с 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Изтрива редове от таблицата според зададения филтър. Връща броя на изтритите редове. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -При извикване на `update()` и `delete()` не забравяйте с помощта на `where()` да специфицирате редовете, които трябва да се променят/изтрият. Ако `where()` не използвате, операцията ще се извърши върху цялата таблица! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Актуализира данни в реда на базата данни, представен от обекта `ActiveRow`. Като параметър приема iterable с данни, които трябва да се актуализират (ключовете са имената на колоните). За промяна на числови стойности можем да използваме операторите `+=` и `-=`: - -След извършване на актуализацията `ActiveRow` автоматично се презарежда от базата данни, за да се отразят евентуалните промени, извършени на ниво база данни (напр. тригери). Методът връща `true` само ако е настъпила действителна промяна на данните. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // увеличаваме броя на показванията -]); -echo $article->views; // Извежда текущия брой показвания -``` - -Този метод актуализира само един конкретен ред в базата данни. За масова актуализация на повече редове използвайте метода [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Изтрива реда от базата данни, който е представен от обекта `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Изтрива книга с ID 1 -``` - -Този метод изтрива само един конкретен ред в базата данни. За масово изтриване на повече редове използвайте метода [#Selection::delete()]. - - -Релации между таблици -===================== - -В релационните бази данни данните са разделени на няколко таблици и са взаимно свързани с помощта на външни ключове. Nette Database Explorer предлага революционен начин за работа с тези релации - без писане на JOIN заявки и без необходимост от каквото и да е конфигуриране или генериране. - -За илюстрация на работата с релации ще използваме примерна база данни с книги ([ще я намерите в GitHub |https://github.com/nette-examples/books]). В базата данни имаме таблици: - -- `author` - писатели и преводачи (колони `id`, `name`, `web`, `born`) -- `book` - книги (колони `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - етикети (колони `id`, `name`) -- `book_tag` - свързваща таблица между книги и етикети (колони `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Структура на базата данни, използвана в примерите - -В нашия пример с база данни за книги намираме няколко типа връзки (въпреки че моделът е опростен спрямо реалността): - -- Едно към много (1:N) – всяка книга **има един** автор, авторът може да напише **няколко** книги. -- Нула към много (0:N) – книгата **може да има** преводач, преводачът може да преведе **няколко** книги. -- Нула към едно (0:1) – книгата **може да има** следващ том. -- Много към много (M:N) – книгата **може да има няколко** етикета и етикетът може да бъде присвоен на **няколко** книги. - -В тези връзки винаги съществува родителска и дъщерна таблица. Например във връзката между автор и книга таблицата `author` е родителска, а `book` е дъщерна - можем да си го представим така, че книгата винаги "принадлежи" на някой автор. Това се проявява и в структурата на базата данни: дъщерната таблица `book` съдържа външен ключ `author_id`, който сочи към родителската таблица `author`. - -Ако трябва да изведем книгите, включително имената на техните автори, имаме две възможности. Или да получим данните с една SQL заявка с помощта на JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Или да заредим данните на две стъпки - първо книгите, а след това техните автори - и след това да ги съберем в PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id на авторите на получените книги -``` - -Вторият подход всъщност е по-ефективен, въпреки че това може да е изненадващо. Данните се зареждат само веднъж и могат да бъдат по-добре използвани в кеша. Точно по този начин работи Nette Database Explorer - всичко решава под повърхността и ви предлага елегантно API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'заглавие: ' . $book->title; - echo 'написано от: ' . $book->author->name; // $book->author е запис от таблица 'author' - echo 'преведено от: ' . $book->translator?->name; -} -``` - - -Достъп до родителска таблица ----------------------------- - -Достъпът до родителската таблица е пряк. Става въпрос за връзки като *книгата има автор* или *книгата може да има преводач*. Свързаният запис получаваме чрез свойството на обекта ActiveRow - неговото име отговаря на името на колоната с външния ключ без суфикса `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // намира автора според колоната author_id -echo $book->translator?->name; // намира преводача според translator_id -``` - -Когато достъпим свойството `$book->author`, Explorer в таблицата `book` търси колона, чието име съдържа низа `author` (т.е. `author_id`). Според стойността в тази колона зарежда съответния запис от таблицата `author` и го връща като `ActiveRow`. Подобно работи и `$book->translator`, който използва колоната `translator_id`. Тъй като колоната `translator_id` може да съдържа `null`, използваме в кода оператора `?->`. - -Алтернативен път предлага методът `ref()`, който приема два аргумента, името на целевата таблица и името на свързващата колона, и връща инстанция на `ActiveRow` или `null`: - -```php -echo $book->ref('author', 'author_id')->name; // връзка към автора -echo $book->ref('author', 'translator_id')->name; // връзка към преводача -``` - -Методът `ref()` е подходящ, ако не може да се използва достъп чрез свойство, тъй като таблицата съдържа колона със същото име (т.е. `author`). В останалите случаи се препоръчва използването на достъп чрез свойство, който е по-четлив. - -Explorer автоматично оптимизира заявките към базата данни. Когато преминаваме през книгите в цикъл и достъпваме техните свързани записи (автори, преводачи), Explorer не генерира заявка за всяка книга поотделно. Вместо това изпълнява само една SELECT заявка за всеки тип връзка, като по този начин значително намалява натоварването на базата данни. Например: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Този код ще извика само тези три светкавични заявки към базата данни: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id от колоната author_id на избраните книги -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id от колоната translator_id на избраните книги -``` - -.[note] -Логиката за намиране на свързващата колона е дадена от имплементацията на [Conventions |api:Nette\Database\Conventions]. Препоръчваме използването на [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], които анализират външните ключове и позволяват лесно да се работи със съществуващите връзки между таблиците. - - -Достъп до дъщерна таблица -------------------------- - -Достъпът до дъщерната таблица работи в обратна посока. Сега питаме *какви книги е написал този автор* или *превел този преводач*. За този тип заявка използваме метода `related()`, който връща `Selection` със свързаните записи. Нека разгледаме пример: - -```php -$author = $explorer->table('author')->get(1); - -// Извежда всички книги от автора -foreach ($author->related('book.author_id') as $book) { - echo "Написал: $book->title"; -} - -// Извежда всички книги, които авторът е превел -foreach ($author->related('book.translator_id') as $book) { - echo "Превел: $book->title"; -} -``` - -Методът `related()` приема описанието на връзката като един аргумент с точкова нотация или като два отделни аргумента: - -```php -$author->related('book.translator_id'); // един аргумент -$author->related('book', 'translator_id'); // два аргумента -``` - -Explorer може автоматично да открие правилната свързваща колона въз основа на името на родителската таблица. В този случай се свързва чрез колоната `book.author_id`, тъй като името на изходната таблица е `author`: - -```php -$author->related('book'); // използва book.author_id -``` - -Ако съществуват няколко възможни връзки, Explorer ще изхвърли изключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Методът `related()` можем, разбира се, да използваме и при преминаване през повече записи в цикъл и Explorer и в този случай автоматично оптимизира заявките: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' написал:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Този код ще генерира само две светкавични SQL заявки: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id на избраните автори -``` - - -Връзка Много към много ----------------------- - -За връзка много към много (M:N) е необходимо съществуването на свързваща таблица (в нашия случай `book_tag`), която съдържа две колони с външни ключове (`book_id`, `tag_id`). Всяка от тези колони сочи към първичния ключ на една от свързваните таблици. За получаване на свързаните данни първо получаваме записите от свързващата таблица с помощта на `related('book_tag')` и след това продължаваме към целевите данни: - -```php -$book = $explorer->table('book')->get(1); -// извежда имената на етикетите, присвоени към книгата -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // извежда името на етикета през свързващата таблица -} - -$tag = $explorer->table('tag')->get(1); -// или обратно: извежда имената на книгите, означени с този етикет -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // извежда името на книгата -} -``` - -Explorer отново оптимизира SQL заявките до ефективна форма: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id на избраните книги -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id на етикетите, намерени в book_tag -``` - - -Заявки през свързани таблици ----------------------------- - -В методите `where()`, `select()`, `order()` и `group()` можем да използваме специални нотации за достъп до колони от други таблици. Explorer автоматично създава необходимите JOIN-ове. - -**Точкова нотация** (`родителска_таблица.колона`) се използва за връзка 1:N от гледна точка на дъщерната таблица: - -```php -$books = $explorer->table('book'); - -// Намира книги, чийто автор има име, започващо с 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Сортира книгите по името на автора низходящо -$books->order('author.name DESC'); - -// Извежда името на книгата и името на автора -$books->select('book.title, author.name'); -``` - -**Нотация с двоеточие** (`:дъщерна_таблица.колона`) се използва за връзка 1:N от гледна точка на родителската таблица: - -```php -$authors = $explorer->table('author'); - -// Намира автори, които са написали книга с 'PHP' в заглавието -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Преброява броя на книгите за всеки автор -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -В горепосочения пример с нотация с двоеточие (`:book.title`) не е специфицирана колоната с външния ключ. Explorer автоматично открива правилната колона въз основа на името на родителската таблица. В този случай се свързва чрез колоната `book.author_id`, тъй като името на изходната таблица е `author`. Ако съществуват няколко възможни връзки, Explorer ще изхвърли изключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Свързващата колона може да бъде изрично посочена в скоби: - -```php -// Намира автори, които са превели книга с 'PHP' в заглавието -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Нотациите могат да се навързват за достъп през няколко таблици: - -```php -// Намира автори на книги, означени с етикета 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Разширяване на условията за JOIN --------------------------------- - -Методът `joinWhere()` разширява условията, които се посочват при свързване на таблици в SQL след ключовата дума `ON`. - -Да предположим, че искаме да намерим книги, преведени от конкретен преводач: - -```php -// Намира книги, преведени от преводач на име 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -В условието `joinWhere()` можем да използваме същите конструкции като в метода `where()` - оператори, заместващи въпросителни знаци, масиви от стойности или SQL изрази. - -За по-сложни заявки с повече JOIN-ове можем да дефинираме псевдоними на таблици: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Обърнете внимание, че докато методът `where()` добавя условия към клаузата `WHERE`, методът `joinWhere()` разширява условията в клаузата `ON` при свързване на таблици. diff --git a/database/bg/guide.texy b/database/bg/guide.texy deleted file mode 100644 index e4775b3ff2..0000000000 --- a/database/bg/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database е мощно и елегантно ниво за работа с бази данни за PHP с акцент върху простотата и интелигентните функции. Предлага два начина за работа с базата данни - [Explorer |Explorer] за бърза разработка на приложения или [SQL достъп |SQL way] за директна работа със заявки. - -<div class="grid gap-3"> -<div> - - -[SQL достъп |SQL way] -===================== -- Безопасни параметризирани заявки -- Прецизен контрол върху формата на SQL заявките -- Когато пишете сложни заявки с разширени функции -- Оптимизирате производителността с помощта на специфични SQL функции - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Разработвате бързо без писане на SQL -- Интуитивна работа с релациите между таблиците -- Ще оцените автоматичната оптимизация на заявките -- Подходящо за бърза и удобна работа с базата данни - -</div> - -</div> - - -Инсталация -========== - -Можете да изтеглите и инсталирате библиотеката с помощта на инструмента [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Поддържани бази данни -===================== - -Nette Database поддържа следните бази данни: - -|* Сървър на база данни |* DSN име |* Поддръжка в Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | ДА -| PostgreSQL (>= 9.0) | pgsql | ДА -| Sqlite 3 (>= 3.8) | sqlite | ДА -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ДА -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Два подхода към базата данни -============================ - -Nette Database ви дава избор: можете или да пишете SQL заявки директно (SQL достъп), или да ги оставите да се генерират автоматично (Explorer). Нека видим как двата подхода решават едни и същи задачи: - -[SQL достъп|sql way] - SQL заявки - -```php -// вмъкване на запис -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// получаване на записи: автори на книги -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// изход (не е оптимален, генерира N допълнителни заявки) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Автор $author->name е написал $author->books_count книги:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer достъп|explorer] - автоматично генериране на SQL - -```php -// вмъкване на запис -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// получаване на записи: автори на книги -$authors = $database->table('authors') - ->where('active', 1); - -// изход (автоматично генерира само 2 оптимизирани заявки) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Автор $author->name е написал {$books->count()} книги:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Explorer достъпът генерира и оптимизира SQL заявките автоматично. В дадения пример SQL достъпът ще генерира N+1 заявки (една за авторите и след това по една за книгите на всеки автор), докато Explorer автоматично оптимизира заявките и изпълнява само две - една за авторите и една за всички техни книги. - -Двата подхода могат да се комбинират свободно в приложението според нуждите. - - -Свързване и конфигурация -======================== - -За да се свържете с базата данни, е достатъчно да създадете инстанция на класа [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Параметърът `$dsn` (data source name) е същият, [както се използва от PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], напр. `mysql:host=127.0.0.1;dbname=test`. В случай на неуспех, хвърля изключение `Nette\Database\ConnectionException`. - -Въпреки това, по-удобен начин предлага [конфигурацията на приложението |configuration], където е достатъчно да добавите секция `database` и ще се създадат необходимите обекти, както и панелът за база данни в лентата на [Tracy |tracy:] . - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -След това [получаваме обекта на връзката като сървис от DI контейнера |dependency-injection:passing-dependencies], напр.: - -```php -class Model -{ - public function __construct( - // или Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Повече информация за [конфигурацията на базата данни |configuration]. - - -Ръчно създаване на Explorer ---------------------------- - -Ако не използвате Nette DI контейнер, можете да създадете инстанция на `Nette\Database\Explorer` ръчно: - -```php -// свързване с базата данни -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// хранилище за кеш, имплементира Nette\Caching\Storage, напр.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// грижи се за рефлексията на структурата на базата данни -$structure = new Nette\Database\Structure($connection, $storage); -// дефинира правила за мапиране на имената на таблици, колони и външни ключове -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Управление на връзката -====================== - -При създаване на обект `Connection` автоматично се осъществява връзка. Ако искате да отложите връзката, използвайте lazy режим - можете да го включите в [конфигурацията |configuration], като зададете `lazy: true`, или по следния начин: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -За управление на връзката използвайте методите `connect()`, `disconnect()` и `reconnect()`. -- `connect()` създава връзка, ако все още не съществува, като може да хвърли изключение `Nette\Database\ConnectionException`. -- `disconnect()` прекъсва текущата връзка с базата данни. -- `reconnect()` извършва прекъсване и последващо повторно свързване с базата данни. Този метод също може да хвърли изключение `Nette\Database\ConnectionException`. - -Освен това можете да следите събитията, свързани с връзката, с помощта на събитието `onConnect`, което е масив от callback-ове, които се извикват след установяване на връзка с базата данни. - -```php -// изпълнява се след свързване с базата данни -$database->onConnect[] = function($database) { - echo "Свързано с базата данни"; -}; -``` - - -Tracy Debug Bar -=============== - -Ако използвате [Tracy |tracy:], автоматично се активира панелът Database в Debug лентата, който показва всички изпълнени заявки, техните параметри, времето за изпълнение и мястото в кода, където са били извикани. - -[* db-panel.webp *] diff --git a/database/bg/mapping.texy b/database/bg/mapping.texy deleted file mode 100644 index c56310b380..0000000000 --- a/database/bg/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Преобразуване на типове -*********************** - -.[perex] -Nette Database автоматично преобразува стойностите, върнати от базата данни, в съответните PHP типове. - - -Дата и час ----------- - -Данните за време се преобразуват в обекти `Nette\Utils\DateTime`. Ако искате данните за време да се преобразуват в immutable обекти `Nette\Database\DateTime`, задайте опцията `newDateTime: true` в [конфигурацията |configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -В случай на MySQL, преобразува типа данни `TIME` в обекти `DateInterval`. - - -Булеви стойности ----------------- - -Булевите стойности автоматично се преобразуват в `true` или `false`. При MySQL се преобразува `TINYINT(1)`, ако зададем `convertBoolean: true` в [конфигурацията |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Числови стойности ------------------ - -Числовите стойности се преобразуват в `int` или `float` според типа на колоната в базата данни: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Персонализирана нормализация ----------------------------- - -С помощта на метода `setRowNormalizer(?callable $normalizer)` можете да зададете персонализирана функция за трансформиране на редовете от базата данни. Това е полезно например за автоматично преобразуване на типове данни. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // тук се извършва преобразуването на типове - return $row; -}); -``` diff --git a/database/bg/reflection.texy b/database/bg/reflection.texy deleted file mode 100644 index 496557ba0e..0000000000 --- a/database/bg/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Рефлексия на структурата -************************ - -.{data-version:3.2.1} -Nette Database предоставя инструменти за интроспекция на структурата на базата данни с помощта на класа [api:Nette\Database\Reflection]. Тя позволява получаване на информация за таблици, колони, индекси и външни ключове. Можете да използвате рефлексията за генериране на схеми, създаване на гъвкави приложения, работещи с база данни, или общи инструменти за бази данни. - -Получаваме обекта на рефлексията от инстанцията на връзката с базата данни: - -```php -$reflection = $database->getReflection(); -``` - - -Получаване на таблици ---------------------- - -Readonly свойството `$reflection->tables` съдържа асоциативен масив на всички таблици в базата данни: - -```php -// Извеждане на имената на всички таблици -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Налични са още два метода: - -```php -// Проверка за съществуване на таблица -if ($reflection->hasTable('users')) { - echo "Таблицата users съществува"; -} - -// Връща обект на таблицата; ако не съществува, хвърля изключение -$table = $reflection->getTable('users'); -``` - - -Информация за таблицата ------------------------ - -Таблицата е представена от обект [Table|api:Nette\Database\Reflection\Table], който предоставя следните readonly свойства: - -- `$name: string` – име на таблицата -- `$view: bool` – дали е изглед -- `$fullName: ?string` – пълно име на таблицата, включително схемата (ако съществува) -- `$columns: array<string, Column>` – асоциативен масив от колоните на таблицата -- `$indexes: Index[]` – масив от индексите на таблицата -- `$primaryKey: ?Index` – първичен ключ на таблицата или null -- `$foreignKeys: ForeignKey[]` – масив от външните ключове на таблицата - - -Колони ------- - -Свойството `columns` на таблицата предоставя асоциативен масив от колони, където ключът е името на колоната, а стойността е инстанция на [Column|api:Nette\Database\Reflection\Column] със следните свойства: - -- `$name: string` – име на колоната -- `$table: ?Table` – референция към таблицата на колоната -- `$nativeType: string` – нативен тип данни на базата данни -- `$size: ?int` – размер/дължина на типа -- `$nullable: bool` – дали колоната може да съдържа NULL -- `$default: mixed` – стойност по подразбиране на колоната -- `$autoIncrement: bool` – дали колоната е auto-increment -- `$primary: bool` – дали е част от първичния ключ -- `$vendor: array` – допълнителни метаданни, специфични за дадената система за бази данни - -```php -foreach ($table->columns as $name => $column) { - echo "Колона: $name\n"; - echo "Тип: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Да' : 'Не') . "\n"; -} -``` - - -Индекси -------- - -Свойството `indexes` на таблицата предоставя масив от индекси, където всеки индекс е инстанция на [Index|api:Nette\Database\Reflection\Index] със следните свойства: - -- `$columns: Column[]` – масив от колони, образуващи индекса -- `$unique: bool` – дали индексът е уникален -- `$primary: bool` – дали е първичен ключ -- `$name: ?string` – име на индекса - -Първичният ключ на таблицата може да бъде получен с помощта на свойството `primaryKey`, което връща или обект `Index`, или `null` в случай, че таблицата няма първичен ключ. - -```php -// Извеждане на индекси -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Индекс" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Колони: $columns\n"; - echo " Unique: " . ($index->unique ? 'Да' : 'Не') . "\n"; -} - -// Извеждане на първичния ключ -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Първичен ключ: $columns\n"; -} -``` - - -Външни ключове --------------- - -Свойството `foreignKeys` на таблицата предоставя масив от външни ключове, където всеки външен ключ е инстанция на [ForeignKey|api:Nette\Database\Reflection\ForeignKey] със следните свойства: - -- `$foreignTable: Table` – реферирана таблица -- `$localColumns: Column[]` – масив от локални колони -- `$foreignColumns: Column[]` – масив от реферирани колони -- `$name: ?string` – име на външния ключ - -```php -// Извеждане на външни ключове -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/bg/security.texy b/database/bg/security.texy deleted file mode 100644 index 8cd001caa2..0000000000 --- a/database/bg/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Рискове за сигурността -********************** - -<div class=perex> - -Базата данни често съдържа чувствителни данни и позволява извършването на опасни операции. За безопасна работа с Nette Database е ключово: - -- Да се разбира разликата между безопасно и опасно API -- Да се използват параметризирани заявки -- Да се валидират правилно входните данни - -</div> - - -Какво е SQL Injection? -====================== - -SQL инжекцията е най-сериозният риск за сигурността при работа с база данни. Възниква, когато необработен вход от потребител стане част от SQL заявка. Нападателят може да вмъкне собствени SQL команди и по този начин: -- Да получи неоторизиран достъп до данни -- Да модифицира или изтрие данни в базата данни -- Да заобиколи автентикацията - -```php -// ❌ ОПАСЕН КОД - уязвим към SQL инжекция -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Нападателят може да въведе например стойност: ' OR '1'='1 -// Резултатната заявка ще бъде: SELECT * FROM users WHERE name = '' OR '1'='1' -// Което ще върне всички потребители -``` - -Същото се отнася и за Database Explorer: - -```php -// ❌ ОПАСЕН КОД - уязвим към SQL инжекция -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Параметризирани заявки -====================== - -Основната защита срещу SQL инжекция са параметризираните заявки. Nette Database предлага няколко начина за тяхното използване. - -Най-простият начин е използването на **заместващи въпросителни знаци**: - -```php -// ✅ Безопасна параметризирана заявка -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Безопасно условие в Explorer -$table->where('name = ?', $name); -``` - -Това важи за всички други методи в [Database Explorer|explorer], които позволяват вмъкване на изрази със заместващи въпросителни знаци и параметри. - -За командите INSERT, UPDATE или клаузата WHERE можем да предадем стойности в масив: - -```php -// ✅ Безопасен INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Безопасен INSERT в Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Валидация на стойностите на параметрите -======================================= - -Параметризираните заявки са основният градивен елемент за безопасна работа с базата данни. Въпреки това, стойностите, които вмъкваме в тях, трябва да преминат през няколко нива на проверка: - - -Проверка на типа ----------------- - -**Най-важното е да се гарантира правилният тип данни на параметрите** - това е необходимо условие за безопасното използване на Nette Database. Базата данни предполага, че всички входни данни имат правилния тип данни, съответстващ на дадената колона. - -Например, ако `$name` в предишните примери неочаквано беше масив вместо низ, Nette Database щеше да се опита да вмъкне всички негови елементи в SQL заявката, което би довело до грешка. Затова **никога не използвайте** невалидирани данни от `$_GET`, `$_POST` или `$_COOKIE` директно в заявките към базата данни. - - -Проверка на формата -------------------- - -На второ ниво проверяваме формата на данните - например дали низовете са в UTF-8 кодиране и тяхната дължина съответства на дефиницията на колоната, или дали числовите стойности са в допустимия диапазон за дадения тип данни на колоната. - -На това ниво на валидация можем частично да разчитаме и на самата база данни - много бази данни ще отхвърлят невалидни данни. Въпреки това, поведението може да варира, някои могат тихо да скъсят дълги низове или да отрежат числа извън диапазона. - - -Домейн проверка ---------------- - -Третото ниво представляват логически проверки, специфични за вашето приложение. Например, проверка дали стойностите от select полетата съответстват на предлаганите опции, дали числата са в очаквания диапазон (напр. възраст 0-150 години) или дали взаимните зависимости между стойностите имат смисъл. - - -Препоръчителни начини за валидация ----------------------------------- - -- Използвайте [Nette Forms|forms:], които автоматично осигуряват правилната валидация на всички входове -- Използвайте [Presenters|application:] и посочвайте типовете данни за параметрите в методите `action*()` и `render*()` -- Или реализирайте собствен слой за валидация с помощта на стандартни PHP инструменти като `filter_var()` - - -Безопасна работа с колони -========================= - -В предишната секция показахме как правилно да валидираме стойностите на параметрите. При използване на масиви в SQL заявки обаче трябва да обърнем същото внимание и на техните ключове. - -```php -// ❌ ОПАСЕН КОД - ключовете в масива не са обработени -$database->query('INSERT INTO users', $_POST); -``` - -При командите INSERT и UPDATE това е критична грешка в сигурността - нападателят може да вмъкне или промени всяка колона в базата данни. Може например да зададе `is_admin = 1` или да вмъкне произволни данни в чувствителни колони (т.нар. Mass Assignment Vulnerability). - -В условията WHERE е още по-опасно, тъй като те могат да съдържат оператори: - -```php -// ❌ ОПАСЕН КОД - ключовете в масива не са обработени -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// изпълнява заявка WHERE (`salary` > 100000) -``` - -Нападателят може да използва този подход за систематично откриване на заплатите на служителите. Започва например със заявка за заплати над 100 000, след това под 50 000 и чрез постепенно стесняване на диапазона може да разкрие приблизителните заплати на всички служители. Този тип атака се нарича SQL enumeration. - -Методите `where()` и `whereOr()` са още [много по-гъвкави |explorer#where] и поддържат SQL изрази в ключовете и стойностите, включително оператори и функции. Това дава възможност на нападателя да извърши SQL инжекция: - -```php -// ❌ ОПАСЕН КОД - нападателят може да вмъкне собствен SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// изпълнява заявка WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Тази атака прекратява първоначалното условие с помощта на `0)`, добавя собствена `SELECT` команда с помощта на `UNION`, за да получи чувствителни данни от таблицата `users`, и затваря синтактично правилната заявка с помощта на `WHERE (1)`. - - -Бял списък на колони --------------------- - -За безопасна работа с имената на колони се нуждаем от механизъм, който да гарантира, че потребителят може да работи само с разрешени колони и не може да добавя собствени. Можем да се опитаме да открием и блокираме опасни имена на колони (черен списък), но този подход е ненадежден - нападателят винаги може да измисли нов начин да запише опасно име на колона, който не сме предвидили. - -Затова е много по-безопасно да обърнем логиката и да дефинираме изричен списък с разрешени колони (бял списък): - -```php -// Колони, които потребителят може да редактира -$allowedColumns = ['name', 'email', 'active']; - -// Премахваме всички неразрешени колони от входа -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Сега можем безопасно да използваме в заявки, като например: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Динамични идентификатори -======================== - -За динамични имена на таблици и колони използвайте заместващия символ `?name`. Той осигурява правилното екраниране на идентификаторите според синтаксиса на дадената база данни (напр. с помощта на обратни кавички в MySQL): - -```php -// ✅ Безопасно използване на доверени идентификатори -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Резултат в MySQL: SELECT `name` FROM `users` -``` - -Важно: използвайте символа `?name` само за доверени стойности, дефинирани в кода на приложението. За стойности от потребителя използвайте отново [бял списък |#Бял списък на колони]. В противен случай се излагате на рискове за сигурността: - -```php -// ❌ ОПАСНО - никога не използвайте вход от потребител -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/bg/sql-way.texy b/database/bg/sql-way.texy deleted file mode 100644 index 6833ea9af8..0000000000 --- a/database/bg/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL достъп -********** - -.[perex] -Nette Database предлага два начина: можете да пишете SQL заявки сами (SQL достъп) или да ги оставите да се генерират автоматично (вижте [Explorer |explorer]). SQL достъпът ви дава пълен контрол над заявките, като същевременно гарантира тяхното безопасно изграждане. - -.[note] -Подробности за свързването и конфигурацията на базата данни можете да намерите в глава [Свързване и конфигурация |guide#Свързване и конфигурация]. - - -Основно запитване -================= - -За запитвания към базата данни се използва методът `query()`. Той връща обект [ResultSet |api:Nette\Database\ResultSet], който представлява резултата от заявката. В случай на неуспех, методът [хвърля изключение |exceptions]. Можем да обходим резултата от заявката с помощта на цикъл `foreach` или да използваме някоя от [помощните функции |#Получаване на данни]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -За безопасно вмъкване на стойности в SQL заявки използваме параметризирани заявки. Nette Database ги прави максимално прости - достатъчно е да добавите запетая и стойност след SQL заявката: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -При повече параметри имате две опции за запис. Можете или да "вмъквате" параметри в SQL заявката: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Или първо да напишете цялата SQL заявка и след това да добавите всички параметри: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Защита от SQL injection -======================= - -Защо е важно да се използват параметризирани заявки? Защото те ви защитават от атака, наречена SQL injection, при която нападателят може да вмъкне собствени SQL команди и по този начин да получи или повреди данни в базата данни. - -.[warning] -**Никога не вмъквайте променливи директно в SQL заявката!** Винаги използвайте параметризирани заявки, които ви защитават от SQL injection. - -```php -// ❌ ОПАСЕН КОД - уязвим към SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Безопасна параметризирана заявка -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Запознайте се с [възможните рискове за сигурността |security]. - - -Техники за запитване -==================== - - -Условия WHERE -------------- - -Можете да запишете условията WHERE като асоциативен масив, където ключовете са имената на колоните, а стойностите са данните за сравнение. Nette Database автоматично избира най-подходящия SQL оператор според типа на стойността. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -В ключа можете също изрично да посочите оператора за сравнение: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // използва оператор > - 'name LIKE' => '%John%', // използва оператор LIKE - 'email NOT LIKE' => '%example.com%', // използва оператор NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette автоматично обработва специални случаи като `null` стойности или масиви. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // използва оператор = - 'category_id' => [1, 2, 3], // използва IN - 'description' => null, // използва IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -За отрицателни условия използвайте оператора `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // използва оператор <> - 'category_id NOT' => [1, 2, 3], // използва NOT IN - 'description NOT' => null, // използва IS NOT NULL - 'id' => [], // пропуска се -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -За свързване на условия се използва операторът `AND`. Това може да се промени с помощта на [заместващия символ ?or |#Подсказки за изграждане на SQL]. - - -Правила ORDER BY ----------------- - -Сортирането `ORDER BY` може да се запише с помощта на масив. В ключовете посочваме колоните, а стойността ще бъде булева променлива, определяща дали да се сортира възходящо: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // възходящо - 'name' => false, // низходящо -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Вмъкване на данни (INSERT) --------------------------- - -За вмъкване на записи се използва SQL инструкцията `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Методът `getInsertId()` връща ID на последния вмъкнат ред. При някои бази данни (напр. PostgreSQL) е необходимо да се посочи като параметър името на последователността, от която трябва да се генерира ID, с помощта на `$database->getInsertId($sequenceId)`. - -Като параметри можем да предаваме и [#специални стойности] като файлове, обекти DateTime или enum типове. - -Вмъкване на няколко записа наведнъж: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Многократното INSERT е много по-бързо, тъй като се изпълнява една единствена заявка към базата данни, вместо много отделни. - -**Предупреждение за сигурност:** Никога не използвайте невалидирани данни като `$values`. Запознайте се с [възможните рискове |security#Безопасна работа с колони]. - - -Актуализация на данни (UPDATE) ------------------------------- - -За актуализация на записи се използва SQL инструкцията `UPDATE`. - -```php -// Актуализация на един запис -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Броят на засегнатите редове се връща от `$result->getRowCount()`. - -За UPDATE можем да използваме операторите `+=` и `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // инкрементиране на login_count -], 1); -``` - -Пример за вмъкване или редактиране на запис, ако вече съществува. Ще използваме техниката `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Забележете, че Nette Database разпознава в какъв контекст на SQL инструкцията вмъкваме параметъра с масив и съответно изгражда SQL кода от него. Така от първия масив е изградил `(id, name, year) VALUES (123, 'Jim', 1978)`, докато втория е преобразувал във формата `name = 'Jim', year = 1978`. Разглеждаме това по-подробно в секцията [#Подсказки за изграждане на SQL]. - - -Изтриване на данни (DELETE) ---------------------------- - -За изтриване на записи се използва SQL инструкцията `DELETE`. Пример за получаване на броя на изтритите редове: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Подсказки за изграждане на SQL ------------------------------- - -Подсказката е специален placeholder в SQL заявката, който указва как стойността на параметъра трябва да се преобразува в SQL израз: - -| Подсказка | Описание | Автоматично се използва -|-----------|-------------------------------------------------|----------------------------- -| `?name` | използва се за вмъкване на име на таблица или колона | - -| `?values` | генерира `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | генерира присвояване `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | свързва условията в масива с оператор `AND` | `WHERE ?`, `HAVING ?` -| `?or` | свързва условията в масива с оператор `OR` | - -| `?order` | генерира клауза `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -За динамично вмъкване на имена на таблици и колони в заявката се използва placeholder-ът `?name`. Nette Database се грижи за правилното обработване на идентификаторите според конвенциите на дадената база данни (напр. затваряне в обратни кавички в MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (в MySQL) -``` - -**Внимание:** използвайте символа `?name` само за имена на таблици и колони от валидирани входове, в противен случай се излагате на [риск за сигурността |security#Динамични идентификатори]. - -Обикновено не е необходимо да се посочват другите подсказки, тъй като Nette използва интелигентно автоматично откриване при съставянето на SQL заявката (вижте третата колона на таблицата). Но можете да ги използвате например в ситуация, когато искате да свържете условията с `OR` вместо с `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Специални стойности -------------------- - -Освен обичайните скаларни типове (string, int, bool), можете да предавате като параметри и специални стойности: - -- файлове: `fopen('image.gif', 'r')` вмъква бинарното съдържание на файла -- дата и час: обекти `DateTime` се преобразуват в база данни формат -- enum типове: инстанции на `enum` се преобразуват в тяхната стойност -- SQL литерали: създадени с помощта на `Connection::literal('NOW()')` се вмъкват директно в заявката - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -При бази данни, които нямат нативна поддръжка за типа данни `datetime` (като SQLite и Oracle), `DateTime` се преобразува в стойност, определена в [конфигурацията на базата данни |configuration] чрез елемента `formatDateTime` (стойността по подразбиране е `U` - unix timestamp). - - -SQL литерали ------------- - -В някои случаи трябва да посочите директно SQL код като стойност, който обаче не трябва да се разбира като низ и да се екранира. За това служат обектите от класа `Nette\Database\SqlLiteral`. Те се създават от метода `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Или алтернативно: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL литералите могат да съдържат параметри: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Благодарение на което можем да създаваме интересни комбинации: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Получаване на данни -=================== - - -Кратки пътища за SELECT заявки ------------------------------- - -За опростяване на извличането на данни `Connection` предлага няколко кратки пътя, които комбинират извикването на `query()` със следващо `fetch*()`. Тези методи приемат същите параметри като `query()`, т.е. SQL заявка и незадължителни параметри. Пълно описание на методите `fetch*()` ще намерите [по-долу |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Изпълнява заявка и връща първия ред като обект `Row` -| `fetchAll($sql, ...$params): array` | Изпълнява заявка и връща всички редове като масив от обекти `Row` -| `fetchPairs($sql, ...$params): array` | Изпълнява заявка и връща асоциативен масив, където първата колона представлява ключ, а втората - стойност -| `fetchField($sql, ...$params): mixed` | Изпълнява заявка и връща стойността на първото поле от първия ред -| `fetchList($sql, ...$params): ?array` | Изпълнява заявка и връща първия ред като индексиран масив - -Пример: - -```php -// fetchField() - връща стойността на първата клетка -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - итерация през редове --------------------------------- - -След изпълнение на заявката се връща обект [ResultSet|api:Nette\Database\ResultSet], който позволява обхождане на резултатите по няколко начина. Най-лесният начин да изпълните заявка и да получите редовете е чрез итерация в цикъл `foreach`. Този начин е най-икономичен откъм памет, тъй като връща данните постепенно и не ги съхранява всички наведнъж в паметта. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` може да се итерира само веднъж. Ако трябва да итерирате многократно, първо трябва да заредите данните в масив, например с помощта на метода `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Връща ред като обект `Row`. Ако няма повече редове, връща `null`. Премества вътрешния указател към следващия ред. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // зарежда първия ред -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Връща всички останали редове от `ResultSet` като масив от обекти `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // зарежда всички редове -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Връща резултатите като асоциативен масив. Първият аргумент определя името на колоната, която ще се използва като ключ в масива, вторият аргумент определя името на колоната, която ще се използва като стойност: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ако посочим само първия параметър, стойността ще бъде целият ред, т.е. обект `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -В случай на дублиращи се ключове, се използва стойността от последния ред. При използване на `null` като ключ, масивът ще бъде индексиран числово от нула (тогава не възникват колизии): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Алтернативно, можете да посочите като параметър callback, който за всеки ред ще връща или самата стойност, или двойка ключ-стойност. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback може също да връща масив с двойка ключ & стойност: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Връща стойността на първото поле от текущия ред. Ако няма повече редове, връща `null`. Премества вътрешния указател към следващия ред. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // зарежда името от първия ред -``` - - -fetchList(): ?array .[method] ------------------------------ - -Връща ред като индексиран масив. Ако няма повече редове, връща `null`. Премества вътрешния указател към следващия ред. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Връща броя на засегнатите редове от последната заявка `UPDATE` или `DELETE`. За `SELECT` това е броят на върнатите редове, но той може да не е известен - в такъв случай методът връща `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Връща броя на колоните в `ResultSet`. - - -Информация за заявките -====================== - -За целите на дебъгването можем да получим информация за последната изпълнена заявка: - -```php -echo $database->getLastQueryString(); // извежда SQL заявката - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // извежда SQL заявката -echo $result->getTime(); // извежда времето за изпълнение в секунди -``` - -За показване на резултата като HTML таблица може да се използва: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet предлага информация за типовете на колоните: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column е тип $type->type"; // напр. 'id е тип int' -} -``` - - -Логване на заявки ------------------ - -Можем да реализираме собствено логване на заявки. Събитието `onQuery` е масив от callback-ове, които се извикват след всяка изпълнена заявка: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Заявка: ' . $result->getQueryString()); - $logger->info('Време: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Голям резултатен набор: ' . $result->getRowCount() . ' реда'); - } -}; -``` diff --git a/database/bg/transactions.texy b/database/bg/transactions.texy deleted file mode 100644 index dd532f0a4a..0000000000 --- a/database/bg/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Транзакции -********** - -.[perex] -Транзакциите гарантират, че или всички операции в рамките на трансакцията ще бъдат изпълнени, или нито една няма да бъде изпълнена. Те са полезни за осигуряване на консистентност на данните при по-сложни операции. - -Най-лесният начин за използване на транзакции изглежда така: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Можете да запишете същото много по-елегантно с помощта на метода `transaction()`. Той приема като параметър callback, който изпълнява в транзакция. Ако callback-ът премине без изключение, транзакцията се потвърждава автоматично. Ако възникне изключение, транзакцията се отменя (rollback) и изключението се разпространява по-нататък. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Методът `transaction()` може също да връща стойности: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // връща броя на актуализираните редове -}); -``` diff --git a/database/cs/@home.texy b/database/cs/@home.texy index 58162d133d..c989fd1820 100644 --- a/database/cs/@home.texy +++ b/database/cs/@home.texy @@ -1,5 +1,3 @@ - - Podporované databáze ==================== @@ -16,6 +14,5 @@ Nette podporuje následující databáze: - {{maintitle: Nette Database - awesome database layer for PHP}} {{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/database/cs/@left-menu.texy b/database/cs/@left-menu.texy index 6cb0892efa..f96b60ebe1 100644 --- a/database/cs/@left-menu.texy +++ b/database/cs/@left-menu.texy @@ -6,7 +6,16 @@ Nette Database - [Transakce |transactions] - [Výjimky |exceptions] - [Reflexe |reflection] -- [Mapování |mapping] +- [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/configuration.texy b/database/cs/configuration.texy index 021ee3cd11..726fb15291 100644 --- a/database/cs/configuration.texy +++ b/database/cs/configuration.texy @@ -4,7 +4,7 @@ Konfigurace databáze .[perex] Přehled konfiguračních voleb pro Nette Database. -Pokud nepoužívate celý framework, ale jen tuto knihovnu, přečtěte si, [jak konfiguraci načíst|bootstrap:]. +Pokud nepoužíváte 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í je true + debugger: ... # (bool) výchozí zapnuto při aktivní Tracy # zobrazit EXPLAIN dotazů v Tracy Bar? explain: ... # (bool) výchozí je true diff --git a/database/cs/exceptions.texy b/database/cs/exceptions.texy index e78dd0108d..f06d44a561 100644 --- a/database/cs/exceptions.texy +++ b/database/cs/exceptions.texy @@ -10,11 +10,14 @@ Nette Database používá hierarchii výjimek. Základní třídou je `Nette\Dat 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). diff --git a/database/cs/explorer.texy b/database/cs/explorer.texy index 24044ef827..5138d5e97a 100644 --- a/database/cs/explorer.texy +++ b/database/cs/explorer.texy @@ -19,7 +19,7 @@ S Explorerem začnete voláním metody `table()` objektu [api:Nette\Database\Exp $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,7 +56,7 @@ 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, &$total = null)` | Nastaví stránkování +| `page($page, $itemsPerPage, &$numOfPages = 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ů @@ -68,7 +68,7 @@ V těchto metodách můžete také používat speciální notaci pro přístup k 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: +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: - 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 +112,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 -- nalezene vše -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- nalezene vše +$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('NOT id ?', $ids); Pozor - tato syntaxe není podporovaná ``` @@ -263,7 +263,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); +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); echo "Celkem stránek: $numOfPages"; ``` @@ -303,7 +303,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áti všechny řádky jako pole +| `$array = $table->fetchAll()` | Vrátí 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 +312,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ím v cyklu `foreach`. Automaticky spouští SQL dotaz. +Nejsnazší způsob, jak vykonat dotaz a získat řádky, je iterování v cyklu `foreach`. Automaticky spustí SQL dotaz. ```php $books = $explorer->table('book'); @@ -359,7 +359,7 @@ $authors = $explorer->table('author')->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Pokud uvedeme pouze první parametr, bude hodnotou celý řadek, tedy objekt `ActiveRow`: +Pokud uvedeme pouze první parametr, bude hodnotou celý řádek, tedy objekt `ActiveRow`: ```php $authors = $explorer->table('author')->fetchPairs('id'); @@ -444,7 +444,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á k 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á ke zjištění, kolik řádků odpovídá určité podmínce: ```php $count = $table->count('*'); // SELECT COUNT(*) FROM `table` @@ -457,7 +457,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 ve specifikovaném sloupci nebo výrazu: +Metody `min()` a `max()` vrací minimální a maximální hodnotu v zadaném sloupci nebo výrazu: ```php // SELECT MAX(`price`) FROM `products` WHERE `active` = 1 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -Vrací součet hodnot ve specifikovaném sloupci nebo výrazu: +Vrací součet hodnot v zadaném sloupci nebo výrazu: ```php // SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 @@ -478,8 +478,8 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- Umožňuje provést libovolnou agregační funkci. @@ -510,7 +510,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ě 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ě chyby vyhodí výjimku `Nette\Database\DriverException`. Selection::insert(iterable $data) .[method] @@ -522,7 +522,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 jednoznačný primární klíč nemá, vrací předaná data ve formě pole. +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`. ```php $row = $explorer->table('users')->insert([ @@ -621,7 +621,7 @@ $count = $explorer->table('users') ``` .[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! +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! ActiveRow::update(iterable $data): bool .[method] @@ -629,7 +629,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 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 tehdy, pokud došlo ke skutečné změně dat. ```php $article = $explorer->table('article')->get(1); @@ -642,10 +642,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() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -Smaže řádek z databáze, který je reprezentován objektem `ActiveRow`. +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. ```php $book = $explorer->table('book')->get(1); @@ -671,10 +671,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 +716,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 je doporučeno 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 doporučujeme 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 +751,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 +805,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 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: +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: ```php $book = $explorer->table('book')->get(1); @@ -833,7 +833,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 k 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 ke 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,7 +863,7 @@ $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]. +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]. Spojovací sloupec lze explicitně uvést v závorce: diff --git a/database/cs/guide.texy b/database/cs/guide.texy index f70849ed6c..68bde63e38 100644 --- a/database/cs/guide.texy +++ b/database/cs/guide.texy @@ -25,7 +25,7 @@ Nette Database je výkonná a elegantní databázová vrstva pro PHP s důrazem - Vyvíjíte rychle bez psaní SQL - Intuitivní práce s relacemi mezi tabulkami - Oceníte automatickou optimalizaci dotazů -- Vhodné pro rychlou a pohodlnout práci s databází +- Vhodné pro rychlou a pohodlnou práci s databází </div> @@ -196,7 +196,7 @@ $database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => tru Pro správu připojení využijte metody `connect()`, `disconnect()` a `reconnect()`. - `connect()` vytvoří připojení, pokud ještě neexistuje, přičemž může vyvolat výjimku `Nette\Database\ConnectionException`. - `disconnect()` odpojí aktuální připojení k databázi. -- `reconnect()` provede odpojení a následné znovu připojení k databázi. Tato metoda může rovněž vyvolat výjimku `Nette\Database\ConnectionException`. +- `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í. @@ -207,6 +207,8 @@ $database->onConnect[] = function($database) { }; ``` +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 =============== diff --git a/database/cs/mapping.texy b/database/cs/mapping.texy deleted file mode 100644 index 07bf97e6a9..0000000000 --- a/database/cs/mapping.texy +++ /dev/null @@ -1,55 +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 převádí datový typ `TIME` na objekty `DateInterval`. - - -Booleovské hodnoty ------------------- - -Booleovské hodnoty jsou automaticky převedeny na `true` nebo `false`. U MySQL se převádí `TINYINT(1)` pokud nastavíme v [konfiguraci|configuration] `convertBoolean`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Číselné hodnoty ---------------- - -Číselné hodnoty jsou převedeny na `int` nebo `float` podle typu sloupce v databázi: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Vlastní normalizace -------------------- - -Pomocí metody `setRowNormalizer(?callable $normalizer)` můžete nastavit vlastní funkci pro transformaci řádků z databáze. To se hodí například pro automatický převod datových typů. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // tady proběhne konverze typů - return $row; -}); -``` diff --git a/database/cs/reflection.texy b/database/cs/reflection.texy index 43a83b88d3..1242dcb163 100644 --- a/database/cs/reflection.texy +++ b/database/cs/reflection.texy @@ -41,13 +41,14 @@ 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 +- `$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 @@ -55,15 +56,16 @@ 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 +- `$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) { @@ -79,10 +81,10 @@ 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 +- `$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íč. @@ -108,10 +110,10 @@ 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 +- `$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íčů diff --git a/database/cs/security.texy b/database/cs/security.texy index 89077c54a2..f651e315b5 100644 --- a/database/cs/security.texy +++ b/database/cs/security.texy @@ -83,7 +83,7 @@ Typová kontrola **Nejdůležitější je zajistit správný datový typ parametrů** - to je nutná podmínka pro bezpečné použití Nette Database. Databáze předpokládá, že všechna vstupní data mají správný datový typ odpovídající danému sloupci. -Například pokud by `$name` v předchozích příkladech bylo neočekávaně pole místo řetězce, Nette Database by se pokusilo vložit všechny jeho prvky do SQL dotazu, což by vedlo k chybě. Proto **nikdy nepoužívejte** nevalidovaná data z `$_GET`, `$_POST` nebo `$_COOKIE` přímo v databázových dotazech. +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 @@ -118,7 +118,7 @@ V předchozí sekci jsme si ukázali, jak správně validovat hodnoty parametrů $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: @@ -129,7 +129,7 @@ $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: @@ -140,7 +140,7 @@ $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ů diff --git a/database/cs/sql-way.texy b/database/cs/sql-way.texy index 5838a7478c..83bcba1534 100644 --- a/database/cs/sql-way.texy +++ b/database/cs/sql-way.texy @@ -77,7 +77,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -V klíči můžete také explicitně specifikovat operátor pro porovnání: +V klíči můžete také explicitně uvést operátor pro porovnání: ```php $database->query('SELECT * FROM users WHERE', [ @@ -88,7 +88,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' ``` -Nette automaticky ošetřuje speciální případy jako `null` hodnoty nebo pole. +Nette automaticky ošetřuje speciální případy jako hodnoty `null` nebo pole. ```php $database->query('SELECT * FROM products WHERE', [ @@ -103,12 +103,12 @@ Pro negativní podmínky použijte operátor `NOT`: ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // použije operátor <> + '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 + 'id NOT' => [], // vynechá se ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// 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]. @@ -163,7 +163,7 @@ Vícenásobný INSERT je mnohem rychlejší, protože se provede jediný databá Aktualizace dat (UPDATE) ------------------------ -Pro aktualizacizáznamů se používá SQL příkaz `UPDATE`. +Pro aktualizaci záznamů se používá SQL příkaz `UPDATE`. ```php // Aktualizace jednoho záznamu @@ -198,7 +198,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`. Podrobě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`. Podrobněji se tomu věnujeme v části [#Hinty pro sestavování SQL]. Mazání dat (DELETE) @@ -219,7 +219,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 pro vložení názvu tabulky nebo sloupce | - +| `?name` | použije se pro vložení názvu tabulky či 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 ?` @@ -254,7 +254,7 @@ Speciální hodnoty Kromě běžných skalárních typů (string, int, bool) můžete jako parametry předávat i speciální hodnoty: - soubory: `fopen('image.gif', 'r')` vloží binární obsah souboru -- datum a čas: objekty `DateTime` se převedou na databázový formát +- datum a čas: objekty `DateTimeInterface` 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 @@ -273,7 +273,7 @@ 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 tomuto 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 tomu slouží objekty třídy `Nette\Database\SqlLiteral`. Vytváří je metoda `Connection::literal()`. ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -Nebo alternativě: +Nebo alternativně: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Díky čemuž můžeme vytvářet zajímavé kombinace: +Díky tomu můžeme vytvářet zajímavé kombinace: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -326,11 +326,11 @@ 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` +| `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í asocitivní pole, kde první sloupec představuje klíč a druhý hodnotu +| `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 vrací první řádek jako indexované pole +| `fetchList($sql, ...$params): ?array` | Provede dotaz a vrátí první řádek jako indexované pole nebo `null` Příklad: @@ -344,7 +344,7 @@ $count = $database->query('SELECT COUNT(*) FROM articles') `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. +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. ```php $result = $database->query('SELECT * FROM users'); @@ -455,7 +455,7 @@ $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`. +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`. getColumnCount(): ?int .[method] @@ -491,7 +491,7 @@ $result = $database->query('SELECT * FROM articles'); $types = $result->getColumnTypes(); foreach ($types as $column => $type) { - echo "$column je typu $type->type"; // např. 'id je typu int' + echo "$column je typu $type"; // např. 'id je typu int' } ``` diff --git a/database/cs/transactions.texy b/database/cs/transactions.texy index e91070c2fb..442d34e196 100644 --- a/database/cs/transactions.texy +++ b/database/cs/transactions.texy @@ -33,6 +33,8 @@ $database->transaction(function ($database) use ($id) { }); ``` +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 diff --git a/database/cs/type-conversion.texy b/database/cs/type-conversion.texy new file mode 100644 index 0000000000..aed7eadadc --- /dev/null +++ b/database/cs/type-conversion.texy @@ -0,0 +1,55 @@ +Konverze typů +************* + +.[perex] +Nette Database automaticky konvertuje hodnoty vrácené z databáze na odpovídající PHP typy. + + +Datum a čas +----------- + +Časové údaje jsou převáděny na objekty `Nette\Utils\DateTime`. Pokud chcete, aby byly časové údaje převáděny na immutable objekty `Nette\Database\DateTime`, nastavte v [konfiguraci|configuration] volbu `newDateTime` na true. + +```php +$row = $database->fetch('SELECT created_at FROM articles'); +echo $row->created_at instanceof DateTime; // true +echo $row->created_at->format('j. n. Y'); +``` + +V případě MySQL 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; +}); +``` diff --git a/database/cs/upgrading.texy b/database/cs/upgrading.texy index 20bb26d94d..692d783db9 100644 --- a/database/cs/upgrading.texy +++ b/database/cs/upgrading.texy @@ -2,8 +2,8 @@ Upgrade ******* -Přechod z verze 3.1 na 3.2 -========================== +Upgrade na verzi 3.2 +==================== Minimální požadovaná verze PHP je 8.1. @@ -11,4 +11,26 @@ Kód byl pečlivě vyladěn pro PHP 8.1. Byly doplněny všechny nové typehinty - 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 +- 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` diff --git a/database/de/@home.texy b/database/de/@home.texy index 45a96f08ed..464cd5e695 100644 --- a/database/de/@home.texy +++ b/database/de/@home.texy @@ -1,21 +1,18 @@ - - Unterstützte Datenbanken ======================== -Nette unterstützt die folgenden Datenbanken: - -|* Datenbankserver |* DSN-Name |* Unterstützung im Core |* Unterstützung im Explorer -| 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 | - +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 - awesome database layer for PHP}} -{{description: Nette Database vereinfacht das Abrufen von Daten aus der Datenbank erheblich, ohne dass SQL-Abfragen geschrieben werden müssen. Es stellt effiziente Abfragen und überträgt keine unnötigen Daten.}} +{{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 index a174fb97e9..e9dc1a243f 100644 --- a/database/de/@left-menu.texy +++ b/database/de/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** -- [Einführung |guide] -- [SQL-Zugriff |sql way] -- [Explorer |Explorer] -- [Transaktionen |transactions] -- [Ausnahmen |exceptions] -- [Reflexion |reflection] -- [Mapping |mapping] -- [Konfiguration |configuration] +- [Erste Schritte |guide] +- [SQL-Weg|sql-way] +- [Explorer|explorer] +- [Transaktionen|transactions] +- [Exceptions|exceptions] +- [Reflection|reflection] +- [Typkonvertierung |type-conversion] +- [Konfiguration|configuration] - [Sicherheitsrisiken |security] -- [Upgrade |en:upgrading] +- [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/configuration.texy b/database/de/configuration.texy index f624f1bbb5..10e35eec1c 100644 --- a/database/de/configuration.texy +++ b/database/de/configuration.texy @@ -4,7 +4,7 @@ Datenbankkonfiguration .[perex] Übersicht der Konfigurationsoptionen für Nette Database. -Wenn Sie nicht das gesamte Framework verwenden, sondern nur diese Bibliothek, lesen Sie, [wie die Konfiguration geladen wird|bootstrap:]. +Wenn Sie nicht das gesamte Framework, sondern nur diese Bibliothek verwenden, lesen Sie, [wie man die Konfiguration lädt |bootstrap:]. Einzelne Verbindung @@ -14,33 +14,33 @@ Konfiguration einer einzelnen Datenbankverbindung: ```neon database: - # DSN, einziger Pflichtschlüssel + # DSN, der einzige Pflichtschlüssel dsn: "sqlite:%appDir%/Model/demo.db" user: ... password: ... ``` -Erstellt die Dienste `Nette\Database\Connection` und `Nette\Database\Explorer`, die wir normalerweise über [Autowiring |dependency-injection:autowiring] übergeben, oder durch Verweis auf [ihren Namen |#DI-Dienste]. +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: - # Datenbankpanel in der Tracy Bar anzeigen? - debugger: ... # (bool) Standard ist true + # das Datenbank-Panel in der Tracy Bar anzeigen? + debugger: ... # (bool) standardmäßig an, wenn Tracy aktiv ist - # EXPLAIN von Abfragen in der Tracy Bar anzeigen? - explain: ... # (bool) Standard ist true + # EXPLAIN der Queries in der Tracy Bar anzeigen? + explain: ... # (bool) Standardwert ist true - # Autowiring für diese Verbindung zulassen? - autowired: ... # (bool) Standard ist true bei der ersten Verbindung + # Autowiring für diese Verbindung aktivieren? + autowired: ... # (bool) Standardwert ist true für die erste Verbindung - # Tabellenkonventionen: discovered, static oder Klassenname - conventions: discovered # (string) Standard ist 'discovered' + # Tabellenkonventionen: discovered, static oder ein Klassenname + conventions: discovered # (string) Standardwert ist 'discovered' options: - # Erst verbinden, wenn nötig? - lazy: ... # (bool) Standard ist false + # erst dann mit der Datenbank verbinden, wenn es nötig ist? + lazy: ... # (bool) Standardwert ist false # PHP-Klasse des Datenbanktreibers driverClass: # (string) @@ -49,19 +49,19 @@ database: sqlmode: # (string) # nur MySQL: setzt SET NAMES - charset: # (string) Standard ist 'utf8mb4' + charset: # (string) Standardwert ist 'utf8mb4' - # nur MySQL: konvertiert TINYINT(1) in bool - convertBoolean: # (bool) Standard ist false + # nur MySQL: wandelt TINYINT(1) in bool um + convertBoolean: # (bool) Standardwert ist false - # gibt Datumsspalten als unveränderliche Objekte zurück (ab Version 3.2.1) - newDateTime: # (bool) Standard 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 von Datum/Uhrzeit - formatDateTime: # (string) Standard ist 'U' + # nur Oracle und SQLite: Format zum Speichern des Datums + formatDateTime: # (string) Standardwert ist 'U' ``` -Im Schlüssel `options` können weitere Optionen angegeben werden, die Sie in der [Dokumentation der PDO-Treiber |https://www.php.net/manual/en/pdo.drivers.php] finden, wie zum Beispiel: +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: @@ -73,7 +73,7 @@ database: Mehrere Verbindungen -------------------- -In der Konfiguration können wir auch mehrere Datenbankverbindungen definieren, indem wir sie in benannte Abschnitte unterteilen: +In der Konfiguration können wir mehrere Datenbankverbindungen definieren, indem wir sie in benannte Abschnitte aufteilen: ```neon database: @@ -86,23 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -Autowiring ist nur für Dienste aus dem ersten Abschnitt aktiviert. Dies kann mit `autowired: false` oder `autowired: true` geändert werden. +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-Dienste ----------- +DI-Services +----------- -Diese Dienste werden dem DI-Container hinzugefügt, wobei `###` den Namen der Verbindung darstellt: +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] | Verbindung zur Datenbank -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] +| 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 Dienstnamen `database.default.connection` und `database.default.explorer`. Wenn wir mehrere Verbindungen wie im obigen Beispiel definieren, entsprechen die Namen den Abschnitten, d.h. `database.main.connection`, `database.main.explorer` und weiter `database.another.connection` und `database.another.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 automatisch verdrahtete Dienste übergeben wir explizit durch Verweis auf ihren Namen: +Nicht autowirete Services übergeben wir explizit über einen Verweis auf ihren Namen: ```neon services: diff --git a/database/de/exceptions.texy b/database/de/exceptions.texy index 9dff39187f..7e7a726168 100644 --- a/database/de/exceptions.texy +++ b/database/de/exceptions.texy @@ -1,22 +1,25 @@ -Ausnahmen -********* +Exceptions +********** -Nette Database verwendet eine Hierarchie von Ausnahmeklassen. Die Basisklasse ist `Nette\Database\DriverException`, die von `PDOException` erbt und erweiterte Möglichkeiten zur Fehlerbehandlung bei Datenbankfehlern bietet: +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()` ermöglichen es, die ursprüngliche Abfrage und ihre Parameter abzurufen. +- Die Methoden `getQueryString()` und `getParameters()` erlauben es, die ursprüngliche Query und ihre Parameter abzurufen. -Von `DriverException` erben die folgenden spezialisierten Ausnahmeklassen: +Die Klasse `DriverException` wird von den folgenden spezialisierten Exceptions erweitert: -- `ConnectionException` - signalisiert einen Verbindungsfehler zum Datenbankserver. -- `ConstraintViolationException` - Basisklasse für Verletzungen von Datenbankbeschränkungen, von der erben: - - `ForeignKeyConstraintViolationException` - Verletzung eines Fremdschlüssels. - - `NotNullConstraintViolationException` - Verletzung einer NOT NULL-Beschränkung. - - `UniqueConstraintViolationException` - Verletzung der Eindeutigkeit eines Wertes. +- `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. - -Beispiel für das Abfangen der Ausnahme `UniqueConstraintViolationException`, die auftritt, wenn versucht wird, einen Benutzer mit einer E-Mail-Adresse einzufügen, die bereits in der Datenbank vorhanden ist (vorausgesetzt, die `email`-Spalte hat einen UNIQUE-Index). +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 { diff --git a/database/de/explorer.texy b/database/de/explorer.texy index 0d7f2546fd..1b9ec271c2 100644 --- a/database/de/explorer.texy +++ b/database/de/explorer.texy @@ -3,23 +3,23 @@ Database Explorer <div class=perex> -Der Explorer bietet eine intuitive und effiziente Methode zur Arbeit mit der Datenbank. Er kümmert sich automatisch um Beziehungen zwischen Tabellen und die Optimierung von Abfragen, sodass Sie sich auf Ihre Anwendung konzentrieren können. Er funktioniert sofort ohne Konfiguration. Wenn Sie die volle Kontrolle über SQL-Abfragen benötigen, können Sie den [SQL-Zugriff |SQL way] nutzen. +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 -- Generiert optimierte SQL-Abfragen, die nur die benötigten Daten laden -- Ermöglicht einfachen Zugriff auf verwandte Daten ohne die Notwendigkeit, JOIN-Abfragen zu schreiben -- Funktioniert sofort ohne jegliche Konfiguration oder Generierung von Entitäten +- 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 zur Verbindung finden Sie im Kapitel [Verbindung und Konfiguration |guide#Verbindung und Konfiguration]): +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-Abfrage repräsentiert. An dieses Objekt können weitere Methoden zur Filterung und Sortierung der Ergebnisse angehängt werden. Die Abfrage wird erst zusammengestellt und ausgeführt, wenn Sie Daten anfordern, beispielsweise durch Iteration mit einer `foreach`-Schleife. Jede Zeile wird durch ein Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] repräsentiert: +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) { @@ -28,59 +28,59 @@ foreach ($books as $book) { } ``` -Der Explorer erleichtert die Arbeit mit [#Beziehungen zwischen Tabellen] erheblich. Das folgende Beispiel zeigt, wie einfach Sie Daten aus verknüpften Tabellen (Bücher und ihre Autoren) ausgeben können. Beachten Sie, dass Sie keine JOIN-Abfragen schreiben müssen; Nette erstellt sie für Sie: +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; // erstellt JOIN zur Tabelle 'author' + echo 'Autor: ' . $book->author->name; // erzeugt einen JOIN auf die Tabelle 'author' } ``` -Nette Database Explorer optimiert Abfragen, um sie so effizient wie möglich zu gestalten. Das obige Beispiel führt nur zwei SELECT-Abfragen aus, unabhängig davon, ob Sie 10 oder 10.000 Bücher verarbeiten. +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. -Zusätzlich verfolgt der Explorer, welche Spalten im Code verwendet werden, und lädt nur diese aus der Datenbank, wodurch weitere Leistung eingespart wird. Dieses Verhalten ist vollständig automatisch und adaptiv. Wenn Sie später den Code ändern und weitere Spalten verwenden, passt der Explorer die Abfragen automatisch an. Sie müssen nichts einstellen oder darüber nachdenken, welche Spalten Sie benötigen werden - überlassen Sie das Nette. +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` bietet Methoden zur Filterung und Sortierung der Datenauswahl. +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 dem AND-Operator verknüpft -| `whereOr(array $conditions)` | Fügt eine Gruppe von WHERE-Bedingungen hinzu, die mit dem OR-Operator verknüpft sind -| `wherePrimary($value)` | Fügt eine WHERE-Bedingung nach dem Primärschlüssel hinzu -| `order($columns, ...$params)` | Legt die ORDER BY-Sortierung fest -| `select($columns, ...$params)` | Spezifiziert die Spalten, die geladen werden sollen -| `limit($limit, $offset = null)` | Begrenzt die Anzahl der Zeilen (LIMIT) und setzt optional den OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Legt die Paginierung fest -| `group($columns, ...$params)` | Gruppiert Zeilen (GROUP BY) -| `having($condition, ...$params)` | Fügt eine HAVING-Bedingung zur Filterung gruppierter Zeilen hinzu +| `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 | -Methoden können verkettet werden (sog. [Fluent Interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. +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 auch spezielle Notationen für den Zugriff auf [Daten aus verwandten Tabellen |#Abfragen über verwandte Tabellen] verwenden. +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 ----------------------- -Methoden escapen automatisch Parameter und setzen Bezeichner (Tabellen- und Spaltennamen) in Anführungszeichen, wodurch SQL-Injection verhindert wird. Für die korrekte Funktion müssen einige Regeln beachtet werden: +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: -- Schlüsselwörter, Funktionsnamen, Prozedurnamen usw. **großschreiben**. -- Spalten- und Tabellennamen **kleinschreiben**. -- Zeichenketten immer über **Parameter** einfügen. +- 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 SCHWACHSTELLE: SQL-Injection -where('name LIKE "%search%"'); // FALSCH: erschwert das automatische Setzen von Anführungszeichen -where('name LIKE ?', '%search%'); // RICHTIG: Wert über Parameter eingefügt +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: generiert: `name` `like` ? -where('name LIKE ?', $name); // RICHTIG: generiert: `name` LIKE ? +where('name like ?', $name); // FALSCH: erzeugt: `name` `like` ? +where('name LIKE ?', $name); // RICHTIG: erzeugt: `name` LIKE ? where('LOWER(name) = ?', $value);// RICHTIG: LOWER(`name`) = ? ``` @@ -88,7 +88,7 @@ where('LOWER(name) = ?', $value);// RICHTIG: LOWER(`name`) = ? where(string|array $condition, ...$parameters): static .[method] ---------------------------------------------------------------- -Filtert Ergebnisse anhand von WHERE-Bedingungen. Ihre Stärke liegt in der intelligenten Verarbeitung verschiedener Wertetypen und der automatischen Wahl von SQL-Operatoren. +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: @@ -98,13 +98,13 @@ $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 geeigneter Operatoren müssen Sie sich nicht um verschiedene Sonderfälle kümmern. Nette löst sie für Sie: +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) -// Es kann auch ein Fragezeichen-Platzhalter ohne Operator verwendet werden: +// Sie können auch das Fragezeichen ohne Operator verwenden: $table->where('id ?', 1); // WHERE `id` = 1 ``` @@ -114,10 +114,10 @@ Die Methode verarbeitet auch negative Bedingungen und leere Arrays korrekt: $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 +// $table->where('NOT id ?', $ids); // ACHTUNG: Diese Syntax wird nicht unterstützt ``` -Als Parameter können Sie auch das Ergebnis aus einer anderen Tabelle übergeben - es wird eine Unterabfrage erstellt: +Als Parameter können Sie auch das Ergebnis einer anderen Tabellenabfrage übergeben - dabei entsteht eine Unterabfrage: ```php // WHERE `id` IN (SELECT `id` FROM `tableName`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -Bedingungen können Sie auch als Array übergeben, dessen Elemente mit AND verknüpft werden: +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`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -Im Array können Sie Schlüssel-Wert-Paare verwenden, und Nette wählt wieder automatisch die richtigen Operatoren: +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)) @@ -147,23 +147,23 @@ $table->where([ ]); ``` -Im Array können Sie SQL-Ausdrücke mit Fragezeichen-Platzhaltern und mehreren Parametern kombinieren. Dies ist geeignet für komplexe Bedingungen mit genau definierten Operatoren: +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 als Array übergeben + 'ROUND(score, ?) > ?' => [2, 75.5], // zwei Parameter werden als Array übergeben ]); ``` -Mehrfache Aufrufe von `where()` verknüpfen Bedingungen automatisch mit AND. +Mehrfache Aufrufe von `where()` verknüpfen die Bedingungen automatisch mit AND. whereOr(array $parameters): static .[method] -------------------------------------------- -Fügt ähnlich wie `where()` Bedingungen hinzu, jedoch mit dem Unterschied, dass sie mit OR verknüpft werden: +Fügt ähnlich wie `where()` Bedingungen hinzu, verknüpft sie aber mit OR: ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -Auch hier können Sie komplexere Ausdrücke verwenden: +Auch hier lassen sich komplexere Ausdrücke verwenden: ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -214,7 +214,7 @@ $table->wherePrimary([ 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, in aufsteigender oder absteigender Reihenfolge oder nach einem eigenen Ausdruck: +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` @@ -227,14 +227,14 @@ $table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC select(string $columns, ...$parameters): static .[method] --------------------------------------------------------- -Spezifiziert die Spalten, die aus der Datenbank zurückgegeben werden sollen. Standardmäßig gibt Nette Database Explorer nur die Spalten zurück, die tatsächlich im Code verwendet werden. Die Methode `select()` verwenden Sie daher in Fällen, in denen Sie spezifische Ausdrücke zurückgeben müssen: +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'); ``` -Aliase, die mit `AS` definiert wurden, sind dann als Eigenschaften des ActiveRow-Objekts verfügbar: +Mit `AS` definierte Aliase sind dann als Properties des `ActiveRow`-Objekts verfügbar: ```php foreach ($table as $row) { @@ -246,32 +246,32 @@ foreach ($table as $row) { limit(?int $limit, ?int $offset = null): static .[method] --------------------------------------------------------- -Begrenzt die Anzahl der zurückgegebenen Zeilen (LIMIT) und ermöglicht optional die Einstellung eines Offsets: +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 besser, die Methode `page()` zu verwenden. +Für die Paginierung ist es sinnvoller, die Methode `page()` zu verwenden. page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] ------------------------------------------------------------------------- -Erleichtert die Paginierung von Ergebnissen. Akzeptiert die Seitennummer (beginnend bei 1) und die Anzahl der Elemente pro Seite. Optional kann eine Referenz auf eine Variable übergeben werden, in der die Gesamtzahl der Seiten gespeichert wird: +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); -echo "Gesamtzahl der Seiten: $numOfPages"; +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); +echo "Seiten insgesamt: $numOfPages"; ``` group(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Gruppiert Zeilen nach den angegebenen Spalten (GROUP BY). Wird normalerweise in Verbindung mit Aggregationsfunktionen verwendet: +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 @@ -283,7 +283,7 @@ $table->select('category_id, COUNT(*) AS count') having(string $having, ...$parameters): static .[method] -------------------------------------------------------- -Legt eine Bedingung zur Filterung gruppierter Zeilen fest (HAVING). Kann in Verbindung mit der Methode `group()` und Aggregationsfunktionen verwendet werden: +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 @@ -296,23 +296,23 @@ $table->select('category_id, COUNT(*) AS count') Daten lesen =========== -Zum Lesen von Daten aus der Datenbank stehen Ihnen mehrere nützliche Methoden zur Verfügung: +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 Zeile nach dem Primärschlüssel zurück -| `$row = $table->fetch()` | Gibt die aktuelle Zeile zurück und bewegt den Zeiger zur nächsten -| `$array = $table->fetchPairs()` | Erstellt ein assoziatives Array aus den Ergebnissen -| `$array = $table->fetchAll()` | Gibt alle Zeilen als Array zurück -| `count($table)` | Gibt die Anzahl der Zeilen im Selection-Objekt zurück +| `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 bedeutet, dass die Werte seiner Eigenschaften nicht geändert werden können. Diese Einschränkung gewährleistet die Datenkonsistenz und verhindert unerwartete Nebeneffekte. Daten werden aus der Datenbank geladen, und jede Änderung sollte explizit und kontrolliert erfolgen. +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 Abfrage auszuführen und Zeilen zu erhalten, ist die Iteration in einer `foreach`-Schleife. Sie startet automatisch die SQL-Abfrage. +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'); @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -Führt eine SQL-Abfrage aus und gibt eine Zeile nach dem Primärschlüssel zurück, oder `null`, wenn sie nicht existiert. +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 ID 123 oder null zurück +$book = $explorer->table('book')->get(123); // gibt ActiveRow mit der ID 123 oder null zurück if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -Gibt eine Zeile zurück und bewegt den internen Zeiger zur nächsten. Wenn keine weiteren Zeilen mehr existieren, gibt sie `null` zurück. +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'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Gibt 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 bestimmt den Namen der Spalte, die als Wert verwendet wird: +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 `ActiveRow`-Objekt: +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. Bei Verwendung von `null` als Schlüssel wird das Array numerisch von Null indiziert (dann treten keine Kollisionen auf): +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'); @@ -377,17 +377,17 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternativ können Sie als Parameter einen Callback angeben, der für jede Zeile entweder den Wert selbst oder ein Schlüssel-Wert-Paar zurückgibt. +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 (Jan Novák)', ...] +// ['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' => 'Jan Novák', ...] +// ['Erstes Buch' => 'John Novak', ...] ``` @@ -405,7 +405,7 @@ $allBooks = $explorer->table('book')->fetchAll(); count(): int .[method] ---------------------- -Die Methode `count()` ohne Parameter gibt die Anzahl der Zeilen im `Selection`-Objekt zurück: +Die Methode `count()` ohne Parameter gibt die Anzahl der Zeilen im Objekt `Selection` zurück: ```php $table->where('category', 1); @@ -413,51 +413,51 @@ $count = $table->count(); $count = count($table); // Alternative ``` -Achtung: `count()` mit einem Parameter führt eine Aggregationsfunktion `COUNT()` in der Datenbank aus, siehe unten. +Achtung: `count()` mit einem Parameter führt die Aggregatfunktion COUNT in der Datenbank aus, siehe unten. ActiveRow::toArray(): array .[method] ------------------------------------- -Konvertiert das `ActiveRow`-Objekt in ein assoziatives Array, wobei die Schlüssel die Spaltennamen und die Werte die entsprechenden Daten sind. +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 wird sein: ['id' => 1, 'title' => '...', 'author_id' => ..., ...] +// $bookArray ist ['id' => 1, 'title' => '...', 'author_id' => ..., ...] ``` Aggregation =========== -Die Klasse `Selection` bietet Methoden zur einfachen Durchführung von Aggregationsfunktionen (COUNT, SUM, MIN, MAX, AVG usw.). +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 Minimalwert in der Spalte zurück -| `max($expr)` | Gibt den Maximalwert in der Spalte zurück -| `sum($expr)` | Gibt die Summe der Werte in der Spalte zurück -| `aggregation($function)` | Ermöglicht die Durchführung einer beliebigen Aggregationsfunktion. Z. B. `AVG()`, `GROUP_CONCAT()` +| `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-Abfrage mit der `COUNT`-Funktion aus und gibt das Ergebnis zurück. Die Methode wird verwendet, um festzustellen, wie viele Zeilen einer bestimmten Bedingung entsprechen: +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 `Selection`-Objekt zurück. +Achtung: [#count()] ohne Parameter gibt nur die Anzahl der Zeilen im Objekt `Selection` zurück. -min(string $expr) und max(string $expr) .[method] +min(string $expr) and max(string $expr) .[method] ------------------------------------------------- -Die Methoden `min()` und `max()` geben den minimalen bzw. maximalen Wert in der angegebenen Spalte oder dem Ausdruck zurück: +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 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -Gibt die Summe der Werte in der angegebenen Spalte oder dem Ausdruck zurück: +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 @@ -478,33 +478,33 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -Ermöglicht die Durchführung einer beliebigen Aggregationsfunktion. +Erlaubt die Ausführung einer beliebigen Aggregatfunktion. ```php -// Durchschnittlicher Preis der Produkte in einer Kategorie +// Durchschnittspreis der Produkte in einer Kategorie $avgPrice = $products->where('category_id', 1) ->aggregation('AVG(price)'); -// Verbindet Produkt-Tags zu einer Zeichenkette +// 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 bereits selbst aus einer Aggregationsfunktion und Gruppierung hervorgegangen sind (z. B. `SUM(wert)` über gruppierte Zeilen), geben wir als zweites Argument die Aggregationsfunktion an, die auf diese Zwischenergebnisse angewendet werden soll: +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 einzelne Kategorien und summiert dann diese Preise. +// 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'` gibt an, dass die `SUM`-Funktion auf die Zwischenergebnisse angewendet werden soll. +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 @@ -520,9 +520,9 @@ Fügt neue Datensätze in die Tabelle ein. **Einfügen eines einzelnen Datensatzes:** -Den neuen Datensatz übergeben wir als assoziatives Array oder iterable Objekt (zum Beispiel `ArrayHash`, das in [Formularen |forms:] verwendet wird), wobei die Schlüssel den Spaltennamen in der Tabelle entsprechen. +Ü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 (Trigger, Standardwerte von Spalten, Berechnungen von Auto-Increment-Spalten) zu berücksichtigen. Dadurch wird die Datenkonsistenz gewährleistet und das Objekt enthält immer die aktuellen Daten aus der Datenbank. Wenn es keinen eindeutigen Primärschlüssel gibt, gibt sie die übergebenen Daten in Form eines Arrays zurück. +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([ @@ -530,14 +530,14 @@ $row = $explorer->table('users')->insert([ '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 generierten ID und eventueller durch Trigger vorgenommener Änderungen +// 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, falls sie durch einen Trigger gesetzt wurde +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()` ermöglicht das Einfügen mehrerer Datensätze mit einer einzigen SQL-Abfrage. In diesem Fall gibt sie die Anzahl der eingefügten Zeilen zurück. +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([ @@ -551,10 +551,10 @@ $insertedRows = $explorer->table('users')->insert([ ], ]); // INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows wird 2 sein +// $insertedRows ist 2 ``` -Als Parameter kann auch ein `Selection`-Objekt mit einer Datenauswahl übergeben werden. +Als Parameter lässt sich auch ein `Selection`-Objekt mit einer Datenauswahl übergeben. ```php $newUsers = $explorer->table('potential_users') @@ -566,13 +566,13 @@ $insertedRows = $explorer->table('users')->insert($newUsers); **Einfügen spezieller Werte:** -Als Werte können wir auch Dateien, DateTime-Objekte oder SQL-Literale übergeben: +Als Werte können wir auch Dateien, `DateTime`-Objekte oder SQL-Literale übergeben: ```php $explorer->table('users')->insert([ 'name' => 'John', - 'created_at' => new DateTime, // konvertiert in Datenbankformat - 'avatar' => fopen('image.jpg', 'rb'), // fügt binären Inhalt der Datei ein + '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 ]); ``` @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -Aktualisiert Zeilen in der Tabelle gemäß dem angegebenen Filter. Gibt die Anzahl der tatsächlich geänderten Zeilen zurück. +Aktualisiert die Zeilen der Tabelle nach dem angegebenen Filter. Gibt die Anzahl der tatsächlich geänderten Zeilen zurück. -Die zu ändernden Spalten übergeben wir als assoziatives Array oder iterable Objekt (zum Beispiel `ArrayHash`, das in [Formularen |forms:] verwendet wird), wobei die Schlüssel den Spaltennamen in der Tabelle entsprechen: +Ü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') @@ -595,7 +595,7 @@ $affected = $explorer->table('users') // UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Zur Änderung numerischer Werte können Sie die Operatoren `+=` und `-=` verwenden: +Zum Ändern numerischer Werte können Sie die Operatoren `+=` und `-=` verwenden: ```php $explorer->table('users') @@ -611,7 +611,7 @@ $explorer->table('users') Selection::delete(): int .[method] ---------------------------------- -Löscht Zeilen aus der Tabelle gemäß dem angegebenen Filter. Gibt die Anzahl der gelöschten Zeilen zurück. +Löscht Zeilen aus der Tabelle nach dem angegebenen Filter. Gibt die Anzahl der gelöschten Zeilen zurück. ```php $count = $explorer->table('users') @@ -621,31 +621,31 @@ $count = $explorer->table('users') ``` .[caution] -Vergessen Sie beim Aufrufen von `update()` und `delete()` nicht, mit `where()` die Zeilen anzugeben, die geändert bzw. gelöscht werden sollen. Wenn Sie `where()` nicht verwenden, wird die Operation auf die gesamte Tabelle angewendet! +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 Daten in der Datenbankzeile, die durch das `ActiveRow`-Objekt repräsentiert wird. Als Parameter akzeptiert es ein Iterable mit Daten, die aktualisiert werden sollen (Schlüssel sind Spaltennamen). Zur Änderung numerischer Werte können Sie die Operatoren `+=` und `-=` verwenden: +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 Durchführung der Aktualisierung wird `ActiveRow` automatisch aus der Datenbank neu geladen, um eventuelle Änderungen auf Datenbankebene (z. B. Trigger) zu berücksichtigen. Die Methode gibt `true` zurück, nur wenn tatsächlich Daten geändert wurden. +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öhen die Anzahl der Ansichten + 'views += 1', // erhöht die Anzahl der Aufrufe ]); -echo $article->views; // Gibt die aktuelle Anzahl der Ansichten aus +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() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -Löscht die Zeile aus der Datenbank, die durch das `ActiveRow`-Objekt repräsentiert wird. +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); @@ -658,47 +658,47 @@ Diese Methode löscht nur eine bestimmte Zeile in der Datenbank. Für das Massen Beziehungen zwischen Tabellen ============================= -In relationalen Datenbanken sind Daten auf mehrere Tabellen verteilt und über Fremdschlüssel miteinander verbunden. Nette Database Explorer bietet eine revolutionäre Möglichkeit, mit diesen Beziehungen zu arbeiten - ohne JOIN-Abfragen zu schreiben und ohne die Notwendigkeit, etwas zu konfigurieren oder zu generieren. +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 das Beispiel einer Buchdatenbank ([finden Sie auf GitHub |https://github.com/nette-examples/books]). In der Datenbank haben wir folgende Tabellen: +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` - Schlagwörter (Spalten `id`, `name`) -- `book_tag` - Verknüpfungstabelle zwischen Büchern und Schlagwörtern (Spalten `book_id`, `tag_id`) +- `tag` - Tags (Spalten `id`, `name`) +- `book_tag` - Verknüpfungstabelle zwischen Büchern und Tags (Spalten `book_id`, `tag_id`) -[* db-schema-1-.webp *] *** Datenbankstruktur, die in den Beispielen verwendet wird .<> +[* db-schema-1-.webp *] *** Struktur der in den Beispielen verwendeten Datenbank .<> -In unserem Beispiel der Buchdatenbank finden wir verschiedene Arten von Beziehungen (obwohl das Modell im Vergleich zur Realität vereinfacht ist): +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 einen** weiteren Teil haben -- Many-to-many M:N – ein Buch **kann mehrere** Schlagwörter haben und ein Schlagwort kann **mehreren** Büchern zugeordnet sein +- **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 und eine untergeordnete Tabelle. Zum Beispiel ist in der Beziehung zwischen Autor und Buch die Tabelle `author` übergeordnet und `book` untergeordnet - man kann sich vorstellen, dass ein Buch immer einem Autor "gehört". Dies spiegelt sich auch in der Datenbankstruktur wider: Die untergeordnete Tabelle `book` enthält den Fremdschlüssel `author_id`, der auf die übergeordnete Tabelle `author` verweist. +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 einschließlich der Namen ihrer Autoren auflisten müssen, haben wir zwei Möglichkeiten. Entweder erhalten wir die Daten mit einer einzigen SQL-Abfrage mittels `LEFT JOIN`: +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 +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 und dann ihre Autoren - und fügen sie dann in PHP zusammen: +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 abgerufenen Bücher +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs der Autoren der ausgewählten Bücher ``` -Der zweite Ansatz ist tatsächlich effizienter, auch wenn das überraschend sein mag. Die Daten werden nur einmal pro Tabelle geladen und können besser im Cache genutzt werden. Genau auf diese Weise arbeitet Nette Database Explorer - alles wird unter der Haube gelöst und Ihnen wird eine elegante API geboten: +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 'Titel: ' . $book->title; - echo 'geschrieben von: ' . $book->author->name; // $book->author ist der Datensatz aus der Tabelle 'author' - echo 'übersetzt von: ' . $book->translator?->name; + 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; } ``` @@ -706,26 +706,26 @@ foreach ($books as $book) { Zugriff auf die übergeordnete Tabelle ------------------------------------- -Der Zugriff auf die übergeordnete Tabelle ist unkompliziert. Es handelt sich um Beziehungen wie *ein Buch hat einen Autor* oder *ein Buch kann einen Übersetzer haben*. Den zugehörigen Datensatz erhalten wir über eine Eigenschaft des `ActiveRow`-Objekts. Der Name der Eigenschaft entspricht dem Namen der Spalte mit dem Fremdschlüssel, jedoch ohne das Suffix `_id`: +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 über die Spalte author_id -echo $book->translator?->name; // findet den Übersetzer über translator_id (nullsafe) +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 Sie auf die Eigenschaft `$book->author` zugreifen, sucht der Explorer in der Tabelle `book` nach einer Spalte, deren Name auf `author` endet und auf `_id` endet (also `author_id`). Anhand des Wertes in dieser Spalte lädt er den entsprechenden Datensatz aus der Tabelle `author` und gibt ihn als `ActiveRow` zurück. Ähnlich funktioniert auch `$book->translator`, das die Spalte `translator_id` verwendet. Da die Spalte `translator_id` `NULL` enthalten kann, verwenden wir im Code den Nullsafe-Operator `?->`. +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 akzeptiert: den Namen der Zieltabelle und optional den Namen der Verbindungspalte. Sie gibt eine Instanz von `ActiveRow` oder `null` zurück: +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 der Name der Beziehung nicht eindeutig aus dem Spaltennamen abgeleitet werden kann oder wenn Sie eine explizite Steuerung bevorzugen. +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 Datenbankabfragen automatisch. Wenn Sie Bücher in einer Schleife durchlaufen und auf ihre zugehörigen Datensätze (Autoren, Übersetzer) zugreifen, generiert der Explorer nicht für jedes Buch eine separate Abfrage. Stattdessen führt er nur eine `SELECT`-Abfrage pro referenzierter Tabelle durch, was die Datenbanklast erheblich reduziert. Zum Beispiel: +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'); @@ -736,22 +736,22 @@ foreach ($books as $book) { } ``` -Dieser Code führt nur diese drei blitzschnellen Abfragen an die Datenbank aus: +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)); -- id aus der Spalte author_id der ausgewählten Bücher -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id aus der Spalte translator_id der ausgewählten Bücher +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 zur Erkennung der Beziehung basiert auf den [Conventions |api:Nette\Database\Conventions]. Wir empfehlen die Verwendung von [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], die Fremdschlüssel analysiert und die einfache Arbeit mit bestehenden Beziehungen zwischen Tabellen ermöglicht. +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. Nun fragen wir *welche Bücher hat dieser Autor geschrieben* oder *welche Bücher hat dieser Übersetzer übersetzt*. Für diesen Abfragetyp verwenden wir die Methode `related()`, die eine `Selection` mit den zugehörigen Datensätzen zurückgibt. Sehen wir uns ein Beispiel an: +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); @@ -767,90 +767,90 @@ foreach ($author->related('book.translator_id') as $book) { } ``` -Die Methode `related()` akzeptiert die Beschreibung der Beziehung als ein Argument mit Punktnotation (`Zieltabelle.Fremdschlüsselspalte`) oder als zwei separate Argumente (`Zieltabelle`, `Fremdschlüsselspalte`): +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 +$author->related('book', 'translator_id'); // zwei Argumente ``` -Der Explorer kann die korrekte Verbindungspalte oft automatisch erkennen, wenn sie den Konventionen folgt (z.B. `zieltabelle_id`). In diesem Fall würde die Verbindung über die Spalte `book.author_id` erfolgen, da der Name der Quelltabelle `author` ist und die Zieltabelle `book` heißt: +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 Beziehungen bestehen oder die Spalte nicht den Konventionen entspricht, wirft der Explorer eine [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Wenn mehrere mögliche Verknüpfungen existieren, wirft der Explorer eine [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Die Methode `related()` können Sie natürlich auch beim Durchlaufen mehrerer Datensätze in einer Schleife verwenden, und der Explorer optimiert auch in diesem Fall die Abfragen automatisch: +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 . ' hat geschrieben:'; + echo $author->name . ' schrieb:'; foreach ($author->related('book') as $book) { echo $book->title; } } ``` -Dieser Code generiert nur zwei blitzschnelle SQL-Abfragen: +Dieser Code erzeugt nur zwei blitzschnelle SQL-Queries: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id der ausgewählten Autoren +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) ist die Existenz einer Verknüpfungstabelle erforderlich (in unserem Fall `book_tag`), die zwei Spalten mit Fremdschlüsseln (`book_id`, `tag_id`) enthält. Jede dieser Spalten verweist auf den Primärschlüssel einer der verbundenen Tabellen. Um die zugehörigen Daten zu erhalten, holen wir zuerst die Datensätze aus der Verknüpfungstabelle mit `related('book_tag')` und fahren dann mit den Zieldaten fort: +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 zugewiesenen Tags aus +// 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 gekennzeichneten Bücher aus +// 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 Namen des Buches aus + echo $bookTag->book->title; // gibt den Titel des Buches aus } ``` -Der Explorer optimiert die SQL-Abfragen wieder in eine effiziente Form: +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, ...)); -- id der ausgewählten Bücher -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id der in book_tag gefundenen Tags +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 verbundenen Tabellen zuzugreifen. Der Explorer erstellt automatisch die erforderlichen JOINs. +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** (`referenzierte_tabelle.spalte`) wird für Many-to-One- oder One-to-One-Beziehungen verwendet (Zugriff auf die übergeordnete Tabelle): +**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 Autorname mit 'Jon' beginnt +// Findet Bücher, deren Autorenname mit 'Jon' beginnt $books->where('author.name LIKE ?', 'Jon%'); -// Sortiert Bücher nach Autorennamen absteigend +// 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 die 1:N-Beziehung aus Sicht der übergeordneten Tabelle verwendet: +**Doppelpunktnotation** (`:untergeordnete_tabelle.spalte`) wird für 1:N-Beziehungen aus Sicht der übergeordneten Tabelle verwendet: ```php $authors = $explorer->table('author'); @@ -858,24 +858,24 @@ $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 für jeden Autor +// 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 Spalte mit dem Fremdschlüssel nicht angegeben. Der Explorer erkennt automatisch die richtige Spalte anhand des Namens der übergeordneten Tabelle. In diesem Fall wird über die Spalte `book.author_id` verbunden, da der Name der Quelltabelle `author` ist. Wenn mehrere mögliche Verbindungen bestehen, wirft der Explorer eine [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +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 Verbindungspalte kann explizit in Klammern angegeben werden: +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%'); ``` -Notationen können für den Zugriff über mehrere Tabellen verkettet werden: +Die Notationen lassen sich verketten, um über mehrere Tabellen hinweg auf Daten zuzugreifen: ```php -// Findet Autoren von Büchern, die mit dem Tag 'PHP' gekennzeichnet sind +// Findet die Autoren von Büchern, die mit dem Tag 'PHP' markiert sind $authors->where(':book:book_tag.tag.name', 'PHP') ->group('author.id'); ``` @@ -884,20 +884,20 @@ $authors->where(':book:book_tag.tag.name', 'PHP') Erweiterung der Bedingungen für JOIN ------------------------------------ -Die Methode `joinWhere()` erweitert die Bedingungen, die in der `ON`-Klausel beim Verknüpfen von Tabellen in SQL angegeben werden. +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 möchten Bücher finden, die von einem bestimmten Übersetzer übersetzt wurden, und wir möchten die Bedingung direkt in den `JOIN` einbauen: +Nehmen wir an, wir wollen Bücher finden, die von einem bestimmten Übersetzer übersetzt wurden: ```php -// Findet Bücher, die vom Übersetzer namens 'David' übersetzt wurden +// 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 `joinWhere()`-Bedingung können wir dieselben Konstrukte wie in der `where()`-Methode verwenden - Operatoren, Fragezeichen-Platzhalter, Wertearrays oder SQL-Ausdrücke. +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 Abfragen mit mehreren JOINs können wir Tabellenaliase definieren: +Für komplexere Queries mit mehreren JOINs können Sie Tabellenaliase definieren: ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // AND (`book_author`.`born` < 1950) ``` -Beachten Sie den Unterschied: Während die `where()`-Methode Bedingungen zur `WHERE`-Klausel hinzufügt, erweitert die `joinWhere()`-Methode die Bedingungen in der `ON`-Klausel beim Verknüpfen von Tabellen. +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 index 26e4b8c297..c0a27e0f8d 100644 --- a/database/de/guide.texy +++ b/database/de/guide.texy @@ -2,30 +2,30 @@ Nette Database ************** .[perex] -Nette Database ist eine leistungsstarke und elegante Datenbankschicht für PHP mit Schwerpunkt auf Einfachheit und intelligenten Funktionen. Sie bietet zwei Möglichkeiten zur Arbeit mit der Datenbank: den [Explorer] für eine schnelle Anwendungsentwicklung oder den [SQL-Zugriff |SQL way] für die direkte Arbeit mit Abfragen. +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-Zugriff |SQL way] -====================== -- Sichere parametrisierte Abfragen -- Präzise Kontrolle über die Form der SQL-Abfragen -- Ideal für komplexe Abfragen mit erweiterten Funktionen -- Optimierung der Leistung mithilfe spezifischer SQL-Funktionen +[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] -========== -- Schnelle Entwicklung ohne manuelles Schreiben von SQL +[Explorer |explorer] +==================== +- Schnell entwickeln, ohne SQL zu schreiben - Intuitive Arbeit mit Beziehungen zwischen Tabellen -- Automatische Optimierung von Abfragen -- Geeignet für schnelle und bequeme Arbeit mit der Datenbank +- Automatische Optimierung der Queries nutzen +- Geeignet für schnelles und bequemes Arbeiten mit der Datenbank </div> @@ -35,7 +35,7 @@ Nette Database ist eine leistungsstarke und elegante Datenbankschicht für PHP m Installation ============ -Die Bibliothek wird mit dem Werkzeug [Composer|best-practices:composer] heruntergeladen und installiert: +Die Bibliothek laden und installieren Sie mit [Composer |best-practices:composer]: ```shell composer require nette/database @@ -47,23 +47,23 @@ Unterstützte Datenbanken Nette Database unterstützt die folgenden Datenbanken: -|* Datenbankserver |* DSN-Name |* Unterstützung in Explorer -|---------------------|-------------|----------------------- -| 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 +|* 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 gibt Ihnen die Wahl: Sie können entweder SQL-Abfragen direkt schreiben (SQL-Zugriff) oder sie automatisch generieren lassen (Explorer). Sehen wir uns an, wie beide Ansätze dieselben Aufgaben lösen: +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-Zugriff|sql way] - Manuelle SQL-Abfragen +[SQL-Weg |sql-way] - SQL-Queries ```php // Einfügen eines Datensatzes @@ -73,7 +73,7 @@ $database->query('INSERT INTO books', [ 'published_at' => new DateTime, ]); -// Abrufen von Datensätzen: Aktive Autoren und ihre Buchanzahl +// Laden von Datensätzen: Autoren der Bücher $result = $database->query(' SELECT authors.*, COUNT(books.id) AS books_count FROM authors @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// Ausgabe (nicht optimal, generiert N weitere Abfragen - N+1 Problem) +// Ausgabe (nicht optimal, erzeugt N weitere Queries) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -98,7 +98,7 @@ foreach ($result as $author) { } ``` -[Explorer-Zugriff|explorer] - Automatische Generierung von SQL +[Explorer-Weg |explorer] - automatische SQL-Generierung ```php // Einfügen eines Datensatzes @@ -108,13 +108,13 @@ $database->table('books')->insert([ 'published_at' => new DateTime, ]); -// Abrufen von Datensätzen: Aktive Autoren +// Laden von Datensätzen: Autoren der Bücher $authors = $database->table('authors') - ->where('active', true); + ->where('active', 1); -// Ausgabe (automatisch optimiert, generiert nur 2 Abfragen) +// Ausgabe (erzeugt automatisch nur 2 optimierte Queries) foreach ($authors as $author) { - $books = $author->related('books') // Holt zugehörige Bücher + $books = $author->related('books') ->order('published_at DESC'); echo "Autor $author->name hat {$books->count()} Bücher geschrieben:\n"; @@ -125,23 +125,23 @@ foreach ($authors as $author) { } ``` -Der Explorer-Zugriff generiert und optimiert SQL-Abfragen automatisch. Im gezeigten Beispiel generiert der SQL-Zugriff N+1 Abfragen (eine für Autoren und dann eine für die Bücher jedes Autors), während der Explorer die Abfragen automatisch optimiert und nur zwei durchführt - eine für Autoren und eine für alle ihre Bücher auf einmal. +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 Ansätze können in der Anwendung nach Bedarf beliebig kombiniert werden. +Beide Zugänge lassen sich in der Anwendung nach Bedarf beliebig kombinieren. Verbindung und Konfiguration ============================ -Um eine Verbindung zur Datenbank herzustellen, genügt es, eine Instanz der Klasse [api:Nette\Database\Connection] zu erstellen: +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], z.B. `mysql:host=127.0.0.1;dbname=test`. Im Fehlerfall wird eine Ausnahme `Nette\Database\ConnectionException` ausgelöst. +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. -Ein eleganterer Weg ist jedoch die Verwendung der [Anwendungskonfiguration |configuration]. Fügen Sie einfach einen `database`-Abschnitt hinzu, und die erforderlichen Objekte (Connection und Explorer) werden erstellt. Außerdem wird ein Datenbankpanel in der [Tracy |tracy:] Debug-Leiste angezeigt. +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: @@ -150,7 +150,7 @@ database: password: password ``` -Danach erhalten Sie das Verbindungsobjekt oder den Explorer [als Dienst aus dem DI-Container |dependency-injection:passing-dependencies], z.B. über Constructor Injection: +Das Verbindungsobjekt lässt sich danach [als Service aus dem DI-Container beziehen |dependency-injection:passing-dependencies], z. B.: ```php class Model @@ -163,22 +163,22 @@ class Model } ``` -Mehr Informationen zur [Datenbankkonfiguration|configuration]. +Mehr Informationen zur [Datenbankkonfiguration |configuration]. Manuelle Erstellung des Explorers --------------------------------- -Wenn Sie keinen Nette DI-Container verwenden, können Sie die Instanz `Nette\Database\Explorer` manuell erstellen: +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 Cache, implementiert Nette\Caching\Storage, z.B.: +// 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 Regeln für das Mapping von Tabellen-, Spalten- und Fremdschlüsselnamen +// 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); ``` @@ -187,30 +187,32 @@ $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $ Verbindungsverwaltung ===================== -Beim Erstellen des `Connection`-Objekts wird die Verbindung automatisch hergestellt. Wenn Sie die Verbindung verzögern möchten, verwenden Sie den Lazy-Modus - diesen aktivieren Sie in der [Konfiguration|configuration] durch Setzen von `lazy`, oder so: +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 nutzen Sie die Methoden `connect()`, `disconnect()` und `reconnect()`. -- `connect()` stellt die Verbindung her, falls sie noch nicht existiert, und kann eine Ausnahme `Nette\Database\ConnectionException` auslösen. +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()` führt eine Trennung und anschließende erneute Verbindung zur Datenbank durch. Diese Methode kann ebenfalls eine Ausnahme `Nette\Database\ConnectionException` auslösen. +- `reconnect()` trennt die Verbindung und baut sie anschließend neu auf. Auch diese Methode kann eine `Nette\Database\ConnectionException` werfen. -Darüber hinaus können Sie Ereignisse im Zusammenhang mit der Verbindung über das Ereignis `onConnect` verfolgen, ein Array von Callbacks, die nach dem Aufbau der Verbindung zur Datenbank aufgerufen werden. +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 -// wird nach der Verbindung zur Datenbank ausgeführt +// läuft nach dem Verbinden mit der Datenbank $database->onConnect[] = function($database) { - echo "Verbunden mit der Datenbank"; + 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 automatisch das Database-Panel in der Debug-Bar aktiviert, das alle ausgeführten Abfragen, ihre Parameter, die Ausführungszeit und den Ort im Code anzeigt, an dem sie aufgerufen wurden. +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/mapping.texy b/database/de/mapping.texy deleted file mode 100644 index ba543d9207..0000000000 --- a/database/de/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Typkonvertierung -**************** - -.[perex] -Nette Database konvertiert automatisch Werte, die aus der Datenbank zurückgegeben werden, in die entsprechenden PHP-Typen. - - -Datum und Uhrzeit ------------------ - -Zeitangaben werden in `Nette\Utils\DateTime`-Objekte konvertiert. Wenn Sie möchten, dass Zeitangaben in unveränderliche `Nette\Database\DateTime`-Objekte konvertiert 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 `DateInterval`-Objekte konvertiert. - - -Boolesche Werte ---------------- - -Boolesche Werte werden automatisch in `true` oder `false` konvertiert. Bei MySQL wird `TINYINT(1)` konvertiert, 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` konvertiert: - -```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 Transformation von Zeilen aus der Datenbank festlegen. Dies ist nützlich, zum Beispiel für die automatische Konvertierung von Datentypen. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // hier findet die Typkonvertierung statt - return $row; -}); -``` diff --git a/database/de/reflection.texy b/database/de/reflection.texy index e98fbe8bc8..d8b6a0a727 100644 --- a/database/de/reflection.texy +++ b/database/de/reflection.texy @@ -2,9 +2,9 @@ Strukturreflexion ***************** .{data-version:3.2.1} -Nette Database bietet Werkzeuge zur Introspektion der Datenbankstruktur mithilfe der Klasse [api:Nette\Database\Reflection]. Sie ermöglicht das Abrufen von Informationen über Tabellen, Spalten, Indizes und Fremdschlüssel. Die Reflexion können Sie zur Generierung von Schemata, zur Erstellung flexibler Anwendungen, die mit der Datenbank arbeiten, oder für allgemeine Datenbankwerkzeuge nutzen. +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 wir aus der Instanz der Datenbankverbindung: +Das Reflexionsobjekt erhalten Sie von der Instanz der Datenbankverbindung: ```php $reflection = $database->getReflection(); @@ -14,7 +14,7 @@ $reflection = $database->getReflection(); Abrufen von Tabellen -------------------- -Die readonly-Eigenschaft `$reflection->tables` enthält ein assoziatives Array aller Tabellen in der Datenbank: +Die Readonly-Property `$reflection->tables` enthält ein assoziatives Array aller Tabellen der Datenbank: ```php // Ausgabe der Namen aller Tabellen @@ -23,15 +23,15 @@ foreach ($reflection->tables as $name => $table) { } ``` -Es stehen noch zwei weitere Methoden zur Verfügung: +Zwei weitere Methoden stehen zur Verfügung: ```php -// Überprüfung der Existenz einer Tabelle +// Prüfung, ob eine Tabelle existiert if ($reflection->hasTable('users')) { - echo "Tabelle users existiert"; + echo "Die Tabelle users existiert"; } -// Gibt das Tabellenobjekt zurück; wenn es nicht existiert, wird eine Ausnahme ausgelöst +// Gibt das Tabellenobjekt zurück; wirft eine Exception, wenn es nicht existiert $table = $reflection->getTable('users'); ``` @@ -39,31 +39,33 @@ $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-Eigenschaften bereitstellt: +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 eine Ansicht 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 +- `$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 Eigenschaft `columns` der Tabelle liefert ein assoziatives Array von Spalten, wobei der Schlüssel der Spaltenname und der Wert eine Instanz von [Column|api:Nette\Database\Reflection\Column] mit diesen Eigenschaften ist: +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 kann -- `$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, die spezifisch für das jeweilige Datenbanksystem sind +- `$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) { @@ -77,14 +79,14 @@ foreach ($table->columns as $name => $column) { Indizes ------- -Die Eigenschaft `indexes` der Tabelle liefert ein Array von Indizes, wobei jeder Index eine Instanz von [Index|api:Nette\Database\Reflection\Index] mit diesen Eigenschaften ist: +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 den Primärschlüssel handelt -- `$name: ?string` – Name des Index +- `$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 -Der Primärschlüssel der Tabelle kann über die Eigenschaft `primaryKey` abgerufen werden, die entweder ein `Index`-Objekt oder `null` zurückgibt, falls die Tabelle keinen Primärschlüssel hat. +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 @@ -92,7 +94,7 @@ 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 " Unique: " . ($index->unique ? 'Ja' : 'Nein') . "\n"; + echo " Eindeutig: " . ($index->unique ? 'Ja' : 'Nein') . "\n"; } // Ausgabe des Primärschlüssels @@ -106,12 +108,12 @@ if ($primaryKey = $table->primaryKey) { Fremdschlüssel -------------- -Die Eigenschaft `foreignKeys` der Tabelle liefert ein Array von Fremdschlüsseln, wobei jeder Fremdschlüssel eine Instanz von [ForeignKey|api:Nette\Database\Reflection\ForeignKey] mit diesen Eigenschaften ist: +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` – referenzierte Tabelle -- `$localColumns: Column[]` – Array der lokalen Spalten -- `$foreignColumns: Column[]` – Array der referenzierten Spalten -- `$name: ?string` – Name des Fremdschlüssels +- `$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 diff --git a/database/de/security.texy b/database/de/security.texy index 5d2c61e725..2f07e36e34 100644 --- a/database/de/security.texy +++ b/database/de/security.texy @@ -3,68 +3,68 @@ Sicherheitsrisiken <div class=perex> -Datenbanken enthalten oft sensible Daten und ermöglichen die Durchführung gefährlicher Operationen. Für die sichere Arbeit mit Nette Database ist es entscheidend: +Datenbanken enthalten oft sensible Daten und erlauben gefährliche Operationen. Für die sichere Arbeit mit Nette Database ist es entscheidend: -- Den Unterschied zwischen sicherer und unsicherer API zu verstehen -- Parametrisierte Abfragen zu verwenden -- Eingabedaten korrekt zu validieren +- den Unterschied zwischen sicheren und unsicheren APIs zu verstehen +- parametrisierte Queries zu verwenden +- Eingabedaten richtig zu validieren </div> -Was ist SQL Injection? +Was ist SQL-Injection? ====================== -SQL Injection ist das schwerwiegendste Sicherheitsrisiko bei der Arbeit mit Datenbanken. Es entsteht, wenn unbehandelte Benutzereingaben Teil einer SQL-Abfrage werden. Ein Angreifer kann eigene SQL-Befehle einschleusen und dadurch: -- Unberechtigten Zugriff auf Daten erlangen -- Daten in der Datenbank modifizieren oder löschen -- Authentifizierung umgehen +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 -// ❌ UNSICHERER CODE - anfällig für SQL-Injection +// ❌ GEFÄHRLICHER CODE - anfällig für SQL-Injection $database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); -// Ein Angreifer kann beispielsweise den Wert eingeben: ' OR '1'='1 -// Die resultierende Abfrage lautet dann: SELECT * FROM users WHERE name = '' OR '1'='1' -// Was alle Benutzer zurückgibt +// 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 auch für den Database Explorer: +Dasselbe gilt für den Database Explorer: ```php -// ❌ UNSICHERER CODE - anfällig für SQL-Injection +// ❌ GEFÄHRLICHER CODE - anfällig für SQL-Injection $table->where('name = ' . $_GET['name']); $table->where("name = '$_GET[name]'"); ``` -Parametrisierte Abfragen -======================== +Parametrisierte Queries +======================= -Die grundlegende Verteidigung gegen SQL-Injection sind parametrisierte Abfragen. Nette Database bietet mehrere Möglichkeiten, sie zu verwenden. +Die grundlegende Abwehr gegen SQL-Injection sind parametrisierte Queries. Nette Database bietet mehrere Wege, sie zu verwenden. -Der einfachste Weg ist die Verwendung von **Fragezeichen-Platzhaltern**: +Am einfachsten sind **Fragezeichen-Platzhalter**: ```php -// ✅ Sichere parametrisierte Abfrage +// ✅ Sichere parametrisierte Query $database->query('SELECT * FROM users WHERE name = ?', $name); // ✅ Sichere Bedingung im Explorer $table->where('name = ?', $name); ``` -Dies gilt für alle weiteren Methoden im [Database Explorer|explorer], die das Einfügen von Ausdrücken mit Fragezeichen-Platzhaltern und Parametern ermöglichen. +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-, UPDATE-Befehle oder die WHERE-Klausel können wir Werte in einem Array übergeben: +Für INSERT- und UPDATE-Befehle oder die WHERE-Klausel können wir die Werte in einem Array übergeben: ```php -// ✅ Sicherer INSERT +// ✅ Sicheres INSERT $database->query('INSERT INTO users', [ 'name' => $name, 'email' => $email, ]); -// ✅ Sicherer INSERT im Explorer +// ✅ Sicheres INSERT im Explorer $table->insert([ 'name' => $name, 'email' => $email, @@ -75,89 +75,89 @@ $table->insert([ Validierung von Parameterwerten =============================== -Parametrisierte Abfragen sind der grundlegende Baustein für die sichere Arbeit mit Datenbanken. Dennoch müssen die Werte, die Sie als Parameter übergeben, sorgfältig validiert werden, um andere Arten von Fehlern und potenziellen Problemen zu vermeiden. +Parametrisierte Queries sind der Grundstein sicherer Datenbankarbeit. Die Werte, die wir in sie einsetzen, müssen jedoch mehrere Prüfebenen durchlaufen: Typkontrolle ------------ -**Stellen Sie sicher, dass Parameter den erwarteten Datentyp haben.** Nette Database versucht zwar, Typen zu konvertieren, aber die Übergabe eines völlig falschen Typs (z.B. ein Array, wo ein String erwartet wird) kann zu Fehlern oder unerwartetem Verhalten führen. +**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. -Wenn beispielsweise `$name` in den vorherigen Beispielen unerwartet ein Array anstelle einer Zeichenkette wäre, könnte dies einen Fehler auslösen. Verwenden Sie daher **niemals** unvalidierte Rohdaten aus `$_GET`, `$_POST`, `$_COOKIE` oder anderen externen Quellen direkt in Datenbankabfragen. +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 und Wertebereichs-Kontrolle ------------------------------------ +Formale Kontrolle +----------------- -Überprüfen Sie, ob die Daten das erwartete Format haben (z.B. gültige E-Mail-Adresse, UTF-8-Kodierung) und ob Werte innerhalb zulässiger Grenzen liegen (z.B. Länge einer Zeichenkette, Wertebereich einer Zahl). +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. -Obwohl die Datenbank selbst einige dieser Prüfungen durchführen kann (z.B. durch Spaltentypen und Constraints), ist es besser, ungültige Daten bereits in der Anwendung abzufangen. Das Verhalten der Datenbank bei ungültigen Daten kann variieren (Fehler, stilles Abschneiden, etc.). +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änen-/Logik-Kontrolle ------------------------- +Domänenspezifische Kontrolle +---------------------------- -Die dritte Ebene stellen logische Kontrollen dar, die spezifisch für Ihre Anwendung sind. Zum Beispiel die Überprüfung, ob Werte aus Select-Boxen den angebotenen Optionen entsprechen, ob Zahlen im erwarteten Bereich liegen (z. B. Alter 0-150 Jahre) oder ob gegenseitige Abhängigkeiten zwischen Werten sinnvoll sind. +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 eine robuste Validierung für Benutzereingaben bieten. -- Nutzen Sie Type Hints in [Presentern|application:] für `action*()` und `render*()` Methodenparameter. -- Implementieren Sie eine eigene Validierungsschicht mit PHP-Funktionen wie `filter_var()`, `mb_strlen()`, regulären Ausdrücken oder spezialisierten Validierungsbibliotheken. +- 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 Spaltennamen (Bezeichnern) -============================================= +Sichere Arbeit mit Spalten +========================== -Während Parameterwerte durch Parametrisierung geschützt sind, müssen **Spalten- und Tabellennamen (Bezeichner)**, wenn sie dynamisch sind, anders behandelt werden. Sie können nicht direkt durch Fragezeichen-Platzhalter ersetzt werden. +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 -// ❌ UNSICHERER CODE - Schlüssel im Array sind nicht behandelt +// ❌ GEFÄHRLICHER CODE - die Schlüssel im Array sind nicht geprüft $database->query('INSERT INTO users', $_POST); ``` -Bei INSERT- und UPDATE-Befehlen ist dies ein kritischer Sicherheitsfehler - ein Angreifer kann jede beliebige Spalte in die Datenbank einfügen oder ändern. Er könnte sich beispielsweise `is_admin = 1` setzen oder beliebige Daten in sensible Spalten einfügen (sog. Mass Assignment Vulnerability). +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 dies noch gefährlicher, da sie Operatoren enthalten können: +In WHERE-Bedingungen ist es noch gefährlicher, weil sie Operatoren enthalten können: ```php -// ❌ UNSICHERER CODE - Schlüssel im Array sind nicht behandelt +// ❌ 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 Abfrage WHERE (`salary` > 100000) aus +// führt die Query WHERE (`salary` > 100000) aus ``` -Ein Angreifer kann diesen Ansatz nutzen, um systematisch die Gehälter von Mitarbeitern zu ermitteln. Er beginnt beispielsweise mit einer Abfrage nach Gehältern über 100.000, dann unter 50.000 und durch schrittweise Eingrenzung des Bereichs kann er die ungefähren Gehälter aller Mitarbeiter aufdecken. Diese Art von Angriff wird als SQL-Enumeration bezeichnet. +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 noch [viel flexibler |explorer#where] und unterstützen in Schlüsseln und Werten SQL-Ausdrücke einschließlich Operatoren und Funktionen. Dies gibt einem Angreifer die Möglichkeit, eine SQL-Injection durchzuführen: +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 -// ❌ UNSICHERER CODE - Angreifer kann eigenes SQL einschleusen +// ❌ 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 Abfrage WHERE (0) UNION SELECT name, salary FROM users WHERE (1) aus +// führt die Query WHERE (0) UNION SELECT name, salary FROM users WHERE (1) aus ``` -Dieser Angriff beendet die ursprüngliche Bedingung mit `0)`, fügt mit `UNION` ein eigenes `SELECT` hinzu, um sensible Daten aus der Tabelle `users` zu erhalten, und schließt die syntaktisch korrekte Abfrage mit `WHERE (1)` ab. +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 benötigen wir einen Mechanismus, der sicherstellt, dass der Benutzer nur mit erlaubten Spalten arbeiten kann und keine eigenen hinzufügen kann. Wir könnten versuchen, gefährliche Spaltennamen zu erkennen und zu blockieren (Blacklist), aber dieser Ansatz ist unzuverlässig - ein Angreifer kann immer einen neuen Weg finden, einen gefährlichen Spaltennamen zu schreiben, den wir nicht vorhergesehen haben. +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. -Daher ist es viel sicherer, die Logik umzukehren und eine explizite Liste erlaubter Spalten zu definieren (Whitelist): +Deshalb ist es viel sicherer, die Logik umzudrehen und eine explizite Liste erlaubter Spalten zu definieren (Whitelist): ```php -// Spalten, die der Benutzer bearbeiten darf +// Spalten, die der Benutzer ändern darf $allowedColumns = ['name', 'email', 'active']; -// Wir entfernen alle nicht erlaubten Spalten aus der Eingabe -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // Flip für O(1) lookup +// Alle unerlaubten Spalten aus der Eingabe entfernen +$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); -// ✅ Nun können wir sicher in Abfragen verwenden, wie zum Beispiel: +// ✅ Jetzt sicher in Queries verwendbar, etwa: $database->query('INSERT INTO users', $filteredData); $table->update($filteredData); $table->where($filteredData); @@ -167,7 +167,7 @@ $table->where($filteredData); Dynamische Bezeichner ===================== -Für dynamische Tabellen- und Spaltennamen verwenden Sie den Platzhalter `?name`. Dieser stellt das korrekte Escaping von Bezeichnern gemäß der Syntax der jeweiligen Datenbank sicher (z. B. durch Backticks in MySQL): +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 @@ -177,9 +177,9 @@ $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 Anwendungscode definiert sind. Für Werte vom Benutzer verwenden Sie wieder eine [Whitelist |#Whitelist für Spalten]. Andernfalls setzen Sie sich Sicherheitsrisiken aus: +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 -// ❌ UNSICHER - verwenden Sie niemals Benutzereingaben +// ❌ 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 index 3eb97494c8..7caf3c74eb 100644 --- a/database/de/sql-way.texy +++ b/database/de/sql-way.texy @@ -1,17 +1,17 @@ -SQL-Zugriff -*********** +SQL-Weg +******* .[perex] -Nette Database bietet zwei Wege: Sie können SQL-Abfragen selbst schreiben (SQL-Zugriff) oder sie automatisch generieren lassen (siehe [Explorer |explorer]). Der SQL-Zugriff gibt Ihnen die volle Kontrolle über die Abfragen und gewährleistet gleichzeitig deren sichere Erstellung. +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 Verbindung und Konfiguration der Datenbank finden Sie im Kapitel [Verbindung und Konfiguration |guide#Verbindung und Konfiguration]. +Details zur Datenbankverbindung und -konfiguration finden Sie im Kapitel [Verbindung und Konfiguration |guide#Verbindung und Konfiguration]. Grundlegende Abfragen ===================== -Für Abfragen an die Datenbank dient die Methode `query()`. Sie gibt ein [ResultSet |api:Nette\Database\ResultSet]-Objekt zurück, das das Ergebnis der Abfrage repräsentiert. Im Fehlerfall löst die Methode eine [Ausnahme aus|exceptions]. Das Ergebnis der Abfrage kann mit einer `foreach`-Schleife durchlaufen werden, oder es können einige der [Hilfsfunktionen |#Daten abrufen] verwendet werden. +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'); @@ -22,19 +22,19 @@ foreach ($result as $row) { } ``` -Für das sichere Einfügen von Werten in SQL-Abfragen verwenden wir parametrisierte Abfragen. Nette Database macht diese maximal einfach – fügen Sie einfach ein Komma und den Wert nach der SQL-Abfrage hinzu: +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 Schreibmöglichkeiten. Entweder können Sie die SQL-Abfrage mit Parametern „durchsetzen“: +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 schreiben Sie zuerst die gesamte SQL-Abfrage und fügen dann alle Parameter an: +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); @@ -44,16 +44,16 @@ $database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); Schutz vor SQL-Injection ======================== -Warum ist es wichtig, parametrisierte Abfragen zu verwenden? Weil sie Sie vor einem Angriff namens SQL-Injection schützen, bei dem ein Angreifer eigene SQL-Befehle einschleusen und so Daten in der Datenbank gewinnen oder beschädigen könnte. +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] -**Fügen Sie Variablen niemals direkt in eine SQL-Abfrage ein!** Verwenden Sie immer parametrisierte Abfragen, die Sie vor SQL-Injection schützen. +**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 Abfrage +// ✅ Sichere parametrisierte Query $database->query('SELECT * FROM users WHERE name = ?', $name); ``` @@ -67,7 +67,7 @@ Abfragetechniken WHERE-Bedingungen ----------------- -WHERE-Bedingungen können als assoziatives Array geschrieben werden, wobei die Schlüssel Spaltennamen und die Werte Daten zum Vergleich sind. Nette Database wählt automatisch den am besten geeigneten SQL-Operator basierend auf dem Werttyp aus. +`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', [ @@ -77,47 +77,47 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -Im Schlüssel können Sie auch explizit einen Operator für den Vergleich angeben: +Sie können den Vergleichsoperator auch explizit im Schlüssel angeben: ```php $database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // verwendet Operator > - 'name LIKE' => '%John%', // verwendet Operator LIKE - 'email NOT LIKE' => '%example.com%', // verwendet Operator NOT LIKE + '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 automatisch Sonderfälle wie `null`-Werte oder Arrays. +Nette behandelt Sonderfälle wie `null`-Werte oder Arrays automatisch. ```php $database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // verwendet Operator = + '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 `NOT`-Operator: +Für negative Bedingungen verwenden Sie den Operator `NOT`: ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // verwendet Operator <> + 'name NOT' => 'Laptop', // verwendet den Operator != 'category_id NOT' => [1, 2, 3], // verwendet NOT IN 'description NOT' => null, // verwendet IS NOT NULL - 'id' => [], // wird ausgelassen + 'id NOT' => [], // übersprungen ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -Zum Verknüpfen von Bedingungen wird der `AND`-Operator verwendet. Dies kann mit dem [Platzhalter ?or |#SQL-Erstellungs-Hinweise] geändert werden. +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`-Sortierung kann mithilfe eines Arrays angegeben werden. In den Schlüsseln geben wir die Spalten an, und der Wert ist ein Boolean, der angibt, ob aufsteigend sortiert werden soll: +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', [ @@ -142,9 +142,9 @@ $database->query('INSERT INTO users ?', $values); $userId = $database->getInsertId(); ``` -Die Methode `getInsertId()` gibt die ID der zuletzt eingefügten Zeile zurück. Bei einigen Datenbanken (z. B. PostgreSQL) muss der Name der Sequenz, aus der die ID generiert werden soll, als Parameter mit `$database->getInsertId($sequenceId)` angegeben werden. +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 wir auch [#Spezielle Werte] wie Dateien, DateTime-Objekte oder Enum-Typen übergeben. +Als Parameter können Sie auch [#Spezielle Werte] wie Dateien, DateTime-Objekte oder Enum-Typen übergeben. Einfügen mehrerer Datensätze auf einmal: @@ -155,9 +155,9 @@ $database->query('INSERT INTO users ?', [ ]); ``` -Ein mehrfacher INSERT ist viel schneller, da nur eine Datenbankabfrage anstelle vieler einzelner ausgeführt wird. +Ein mehrfaches INSERT ist deutlich schneller, weil nur eine einzige Datenbankabfrage ausgeführt wird statt vieler einzelner. -**Sicherheitshinweis:** Verwenden Sie niemals unvalidierte Daten als `$values`. Machen Sie sich mit den [möglichen Risiken |security#Sichere Arbeit mit Spaltennamen Bezeichnern] vertraut. +**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) @@ -173,17 +173,17 @@ $values = [ $result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); ``` -Die Anzahl der betroffenen Zeilen wird von `$result->getRowCount()` zurückgegeben. +Die Anzahl der betroffenen Zeilen gibt `$result->getRowCount()` zurück. -Für UPDATE können wir die Operatoren `+=` und `-=` verwenden: +Bei `UPDATE` können wir die Operatoren `+=` und `-=` verwenden: ```php $database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // Inkrementierung von login_count + 'login_count+=' => 1, // erhöht login_count ], 1); ``` -Beispiel für das Einfügen oder Ändern eines Datensatzes, falls er bereits existiert. Wir verwenden die Technik `ON DUPLICATE KEY UPDATE`: +Beispiel für das Einfügen oder Aktualisieren eines Datensatzes, wenn er bereits existiert. Wir verwenden die Technik `ON DUPLICATE KEY UPDATE`: ```php $values = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Beachten Sie, dass Nette Database erkennt, in welchem Kontext des SQL-Befehls wir den Parameter mit dem Array einfügen, und daraus den SQL-Code entsprechend erstellt. So wurde aus dem ersten Array `(id, name, year) VALUES (123, 'Jim', 1978)` erstellt, während das zweite in die Form `name = 'Jim', year = 1978` umgewandelt wurde. Wir gehen darauf im Abschnitt [#SQL-Erstellungs-Hinweise] näher ein. +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 zur Ermittlung der Anzahl gelöschter Zeilen: +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) @@ -212,21 +212,21 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -SQL-Erstellungs-Hinweise ------------------------- +Hinweise zur SQL-Konstruktion +----------------------------- -Ein Hinweis ist ein spezieller Platzhalter in einer SQL-Abfrage, der angibt, wie der Wert des Parameters in einen SQL-Ausdruck umgeschrieben werden soll: +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 -|-----------|-------------------------------------------------------|----------------------------- -| `?name` | Wird zum Einfügen von Tabellen- oder Spaltennamen verwendet | - -| `?values` | Generiert `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | Generiert Zuweisung `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | Verknüpft Bedingungen im Array mit dem `AND`-Operator | `WHERE ?`, `HAVING ?` -| `?or` | Verknüpft Bedingungen im Array mit dem `OR`-Operator | - -| `?order` | Generiert `ORDER BY`-Klausel | `ORDER BY ?`, `GROUP BY ?` +| 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 ?` -Für das dynamische Einfügen von Tabellen- und Spaltennamen in die Abfrage dient der Platzhalter `?name`. Nette Database kümmert sich um die korrekte Behandlung von Bezeichnern gemäß den Konventionen der jeweiligen Datenbank (z. B. das Einschließen in Backticks in MySQL). +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'; @@ -235,9 +235,9 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); // SELECT `name` FROM `users` WHERE id = 1 (in MySQL) ``` -**Warnung:** Verwenden Sie das Symbol `?name` nur für Tabellen- und Spaltennamen aus validierten Eingaben, andernfalls setzen Sie sich einem [Sicherheitsrisiko |security#Dynamische Bezeichner] aus. +**Achtung:** Verwenden Sie den Platzhalter `?name` nur für validierte Tabellen- und Spaltennamen. Sonst riskieren Sie [Sicherheitslücken |security#Dynamische Bezeichner]. -Andere Hinweise müssen normalerweise nicht angegeben werden, da Nette beim Erstellen der SQL-Abfrage eine intelligente Autoerkennung verwendet (siehe dritte Spalte der Tabelle). Sie können ihn jedoch beispielsweise in einer Situation verwenden, in der Sie Bedingungen mit `OR` anstelle von `AND` verknüpfen möchten: +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', [ @@ -251,29 +251,29 @@ $database->query('SELECT * FROM users WHERE ?or', [ Spezielle Werte --------------- -Neben den üblichen skalaren Typen (String, Int, Bool) können Sie auch spezielle Werte als Parameter übergeben: +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 Uhrzeit: `DateTime`-Objekte werden in das Datenbankformat konvertiert -- Enum-Typen: `enum`-Instanzen werden in ihren Wert konvertiert -- SQL-Literale: Erstellt mit `Connection::literal('NOW()')` werden direkt in die Abfrage eingefügt +- 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 DateTime, + 'published_at' => new DateTimeImmutable, // oder new DateTime 'content' => fopen('image.png', 'r'), 'state' => Status::Draft, ]); ``` -Bei Datenbanken, die keine native Unterstützung für den Datentyp `datetime` haben (wie SQLite und Oracle), wird `DateTime` in den Wert konvertiert, der in der [Datenbankkonfiguration|configuration] durch den Eintrag `formatDateTime` festgelegt ist (Standardwert ist `U` – Unix-Timestamp). +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 einigen Fällen müssen Sie SQL-Code direkt als Wert angeben, der jedoch nicht als Zeichenkette verstanden und maskiert werden soll. Dafür dienen Objekte der Klasse `Nette\Database\SqlLiteral`. Sie werden durch die Methode `Connection::literal()` erstellt. +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', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -Oder alternativ: +Alternativ: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Dadurch können wir interessante Kombinationen erstellen: +Das erlaubt interessante Kombinationen: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -324,13 +324,13 @@ Daten abrufen Abkürzungen für SELECT-Abfragen ------------------------------- -Zur Vereinfachung des Datenabrufs bietet `Connection` mehrere Abkürzungen, die den Aufruf von `query()` mit dem anschließenden `fetch*()` kombinieren. Diese Methoden akzeptieren die gleichen Parameter wie `query()`, d.h. die SQL-Abfrage und optionale Parameter. Eine vollständige Beschreibung der `fetch*()`-Methoden finden Sie [unten |#fetch]. +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 Abfrage aus und gibt die erste Zeile als `Row`-Objekt zurück -| `fetchAll($sql, ...$params): array` | Führt die Abfrage aus und gibt alle Zeilen als Array von `Row`-Objekten zurück -| `fetchPairs($sql, ...$params): array` | Führt die Abfrage aus und gibt ein assoziatives Array zurück, wobei die erste Spalte den Schlüssel und die zweite den Wert darstellt -| `fetchField($sql, ...$params): mixed` | Führt die Abfrage aus und gibt den Wert des ersten Feldes der ersten Zeile zurück -| `fetchList($sql, ...$params): ?array` | Führt die Abfrage aus und gibt die erste Zeile als indiziertes Array zurück +| `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: @@ -344,7 +344,7 @@ $count = $database->query('SELECT COUNT(*) FROM articles') `foreach` - Iteration über Zeilen --------------------------------- -Nach Ausführung der Abfrage wird ein [ResultSet|api:Nette\Database\ResultSet]-Objekt zurückgegeben, das es ermöglicht, die Ergebnisse auf verschiedene Arten zu durchlaufen. Der einfachste Weg, eine Abfrage auszuführen und Zeilen zu erhalten, ist die Iteration in einer `foreach`-Schleife. Diese Methode ist am speicherschonendsten, da die Daten schrittweise zurückgegeben und nicht auf einmal im Speicher abgelegt werden. +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'); @@ -357,13 +357,13 @@ foreach ($result as $row) { ``` .[note] -`ResultSet` kann nur einmal iteriert werden. Wenn Sie wiederholt iterieren müssen, müssen Sie die Daten zuerst in ein Array laden, zum Beispiel mit der Methode `fetchAll()`. +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 keine weiteren Zeilen existieren, wird `null` zurückgegeben. Verschiebt den internen Zeiger auf die nächste Zeile. +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'); @@ -377,7 +377,7 @@ if ($row) { fetchAll(): array .[method] --------------------------- -Gibt alle verbleibenden Zeilen aus dem `ResultSet` als Array von `Row`-Objekten zurück. +Gibt alle verbleibenden Zeilen des `ResultSet` als Array von `Row`-Objekten zurück. ```php $result = $database->query('SELECT * FROM users'); @@ -391,7 +391,7 @@ foreach ($rows as $row) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Gibt die Ergebnisse als assoziatives Array zurück. Das erste Argument gibt den Namen der Spalte an, die als Schlüssel im Array verwendet wird, das zweite Argument gibt den Namen der Spalte an, die als Wert verwendet wird: +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'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Wenn wir nur den ersten Parameter angeben, ist der Wert die gesamte Zeile, d.h. das `Row`-Objekt: +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. Bei Verwendung von `null` als Schlüssel wird das Array numerisch ab Null indiziert (dann treten keine Kollisionen auf): +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'); @@ -417,7 +417,7 @@ $names = $result->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternativ können Sie einen Callback als Parameter angeben, der für jede Zeile entweder den Wert selbst oder ein Schlüssel-Wert-Paar zurückgibt. +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'); @@ -433,7 +433,7 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -Gibt den Wert des ersten Feldes der aktuellen Zeile zurück. Wenn keine weiteren Zeilen existieren, wird `null` zurückgegeben. Verschiebt den internen Zeiger auf die nächste Zeile. +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'); @@ -444,7 +444,7 @@ $name = $result->fetchField(); // lädt den Namen aus der ersten Zeile fetchList(): ?array .[method] ----------------------------- -Gibt eine Zeile als indiziertes Array zurück. Wenn keine weiteren Zeilen existieren, wird `null` zurückgegeben. Verschiebt den internen Zeiger auf die nächste Zeile. +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'); @@ -455,7 +455,7 @@ $row = $result->fetchList(); // ['John', 'john@example.com'] getRowCount(): ?int .[method] ----------------------------- -Gibt die Anzahl der von der letzten `UPDATE`- oder `DELETE`-Abfrage betroffenen Zeilen zurück. Bei `SELECT` ist dies die Anzahl der zurückgegebenen Zeilen, diese ist jedoch möglicherweise nicht bekannt – in diesem Fall gibt die Methode `null` zurück. +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] @@ -467,31 +467,31 @@ Gibt die Anzahl der Spalten im `ResultSet` zurück. Informationen zu Abfragen ========================= -Zu Debugging-Zwecken können wir Informationen über die zuletzt ausgeführte Abfrage abrufen: +Zu Debugging-Zwecken können wir Informationen über die zuletzt ausgeführte Query erhalten: ```php -echo $database->getLastQueryString(); // gibt die SQL-Abfrage aus +echo $database->getLastQueryString(); // gibt die SQL-Query aus $result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // gibt die SQL-Abfrage aus +echo $result->getQueryString(); // gibt die SQL-Query aus echo $result->getTime(); // gibt die Ausführungszeit in Sekunden aus ``` -Zur Anzeige des Ergebnisses als HTML-Tabelle kann verwendet werden: +Um das Ergebnis als HTML-Tabelle anzuzeigen, können Sie verwenden: ```php $result = $database->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSet bietet Informationen zu Spaltentypen: +`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->type"; // z.B. 'id ist vom Typ int' + echo "$column ist vom Typ $type"; // z. B. 'id ist vom Typ int' } ``` @@ -499,7 +499,7 @@ foreach ($types as $column => $type) { Abfrage-Protokollierung ----------------------- -Wir können eine eigene Abfrage-Protokollierung implementieren. Das Ereignis `onQuery` ist ein Array von Callbacks, die nach jeder ausgeführten Abfrage aufgerufen werden: +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) { diff --git a/database/de/transactions.texy b/database/de/transactions.texy index dc8a1ba004..dcf52d2e21 100644 --- a/database/de/transactions.texy +++ b/database/de/transactions.texy @@ -2,9 +2,9 @@ Transaktionen ************* .[perex] -Transaktionen garantieren, dass entweder alle Operationen innerhalb der Transaktion ausgeführt werden oder keine. Sie sind nützlich, um die Datenkonsistenz bei komplexeren Operationen sicherzustellen. +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. -Die einfachste Art, Transaktionen zu verwenden, sieht so aus: +Am einfachsten sieht die Verwendung von Transaktionen so aus: ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -Viel eleganter können Sie dasselbe mit der Methode `transaction()` schreiben. Sie akzeptiert einen Callback als Parameter, der innerhalb der Transaktion ausgeführt wird. Wenn der Callback ohne Ausnahme durchläuft, wird die Transaktion automatisch bestätigt (commit). Wenn eine Ausnahme auftritt, wird die Transaktion zurückgerollt (rollback) und die Ausnahme weitergegeben. +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) { @@ -33,6 +33,8 @@ $database->transaction(function ($database) use ($id) { }); ``` +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 diff --git a/database/de/type-conversion.texy b/database/de/type-conversion.texy new file mode 100644 index 0000000000..b32109dae0 --- /dev/null +++ b/database/de/type-conversion.texy @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000000..0460eb483a --- /dev/null +++ b/database/de/upgrading.texy @@ -0,0 +1,36 @@ +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/el/@home.texy b/database/el/@home.texy deleted file mode 100644 index ff1b599908..0000000000 --- a/database/el/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Υποστηριζόμενες βάσεις δεδομένων -================================ - -Το Nette υποστηρίζει τις ακόλουθες βάσεις δεδομένων: - -|* Διακομιστής βάσης δεδομένων |* Όνομα DSN |* Υποστήριξη στον Core |* Υποστήριξη στον Explorer -| MySQL (>= 5.1) | mysql | ΝΑΙ | ΝΑΙ -| PostgreSQL (>= 9.0) | pgsql | ΝΑΙ | ΝΑΙ -| Sqlite 3 (>= 3.8) | sqlite | ΝΑΙ | ΝΑΙ -| Oracle | oci | ΝΑΙ | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ΝΑΙ | ΝΑΙ -| MS SQL (PDO_DBLIB) | mssql | ΝΑΙ | - -| ODBC | odbc | ΝΑΙ | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Η Nette Database απλοποιεί σημαντικά την ανάκτηση δεδομένων από τη βάση δεδομένων χωρίς την ανάγκη γραφής ερωτημάτων SQL. Θέτει αποτελεσματικά ερωτήματα και δεν μεταφέρει περιττά δεδομένα.}} diff --git a/database/el/@left-menu.texy b/database/el/@left-menu.texy deleted file mode 100644 index 2bfe03617b..0000000000 --- a/database/el/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Εισαγωγή |guide] -- [Πρόσβαση SQL |sql way] -- [Explorer] -- [Συναλλαγές |transactions] -- [Εξαιρέσεις |exceptions] -- [Reflection |reflection] -- [Αντιστοίχιση |mapping] -- [Διαμόρφωση |configuration] -- [Κίνδυνοι ασφαλείας |security] -- [Αναβάθμιση |en:upgrading] diff --git a/database/el/@meta.texy b/database/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/database/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/database/el/configuration.texy b/database/el/configuration.texy deleted file mode 100644 index 221c114176..0000000000 --- a/database/el/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Διαμόρφωση βάσης δεδομένων -************************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette Database. - -Αν δεν χρησιμοποιείτε ολόκληρο το framework, αλλά μόνο αυτή τη βιβλιοθήκη, διαβάστε [πώς να φορτώσετε τη διαμόρφωση |bootstrap:]. - - -Μία σύνδεση ------------ - -Διαμόρφωση μιας σύνδεσης βάσης δεδομένων: - -```neon -database: - # DSN, το μοναδικό υποχρεωτικό κλειδί - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Δημιουργεί τις υπηρεσίες `Nette\Database\Connection` και `Nette\Database\Explorer`, τις οποίες συνήθως περνάμε με [autowiring |dependency-injection:autowiring], ή με αναφορά στο [όνομά τους |#Υπηρεσίες DI]. - -Περαιτέρω ρυθμίσεις: - -```neon -database: - # εμφάνιση του πίνακα database στο Tracy Bar; - debugger: ... # (bool) προεπιλογή είναι true - - # εμφάνιση EXPLAIN των queries στο Tracy Bar; - explain: ... # (bool) προεπιλογή είναι true - - # ενεργοποίηση autowiring για αυτή τη σύνδεση; - autowired: ... # (bool) προεπιλογή είναι true στην πρώτη σύνδεση - - # συμβάσεις πινάκων: discovered, static ή όνομα κλάσης - conventions: discovered # (string) προεπιλογή είναι 'discovered' - - options: - # σύνδεση στη βάση δεδομένων μόνο όταν χρειάζεται; - lazy: ... # (bool) προεπιλογή είναι false - - # PHP κλάση του database driver - driverClass: # (string) - - # μόνο MySQL: ορίζει το sql_mode - sqlmode: # (string) - - # μόνο MySQL: ορίζει το SET NAMES - charset: # (string) προεπιλογή είναι 'utf8mb4' - - # μόνο MySQL: μετατρέπει το TINYINT(1) σε bool - convertBoolean: # (bool) προεπιλογή είναι false - - # επιστρέφει στήλες με ημερομηνία ως immutable αντικείμενα (από την έκδοση 3.2.1) - newDateTime: # (bool) προεπιλογή είναι false - - # μόνο Oracle και SQLite: μορφή για αποθήκευση ημερομηνίας - formatDateTime: # (string) προεπιλογή είναι 'U' -``` - -Στο κλειδί `options` μπορούν να αναφερθούν και άλλες επιλογές, τις οποίες θα βρείτε στην [τεκμηρίωση των PDO drivers |https://www.php.net/manual/en/pdo.drivers.php], όπως για παράδειγμα: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Πολλαπλές συνδέσεις -------------------- - -Στη διαμόρφωση μπορούμε να ορίσουμε και πολλαπλές συνδέσεις βάσης δεδομένων χωρίζοντάς τις σε ονομασμένες ενότητες: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Το Autowiring είναι ενεργοποιημένο μόνο για τις υπηρεσίες από την πρώτη ενότητα. Μπορεί να αλλάξει χρησιμοποιώντας `autowired: false` ή `autowired: true`. - - -Υπηρεσίες DI ------------- - -Αυτές οι υπηρεσίες προστίθενται στο DI container, όπου το `###` αντιπροσωπεύει το όνομα της σύνδεσης: - -| Όνομα | Τύπος | Περιγραφή -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | σύνδεση με τη βάση δεδομένων -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Αν ορίσουμε μόνο μία σύνδεση, τα ονόματα των υπηρεσιών θα είναι `database.default.connection` και `database.default.explorer`. Αν ορίσουμε πολλαπλές συνδέσεις όπως στο παραπάνω παράδειγμα, τα ονόματα θα αντιστοιχούν στις ενότητες, δηλ. `database.main.connection`, `database.main.explorer` και επιπλέον `database.another.connection` και `database.another.explorer`. - -Τις μη-autowired υπηρεσίες τις περνάμε ρητά με αναφορά στο όνομά τους: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/el/exceptions.texy b/database/el/exceptions.texy deleted file mode 100644 index 9948c5f939..0000000000 --- a/database/el/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Εξαιρέσεις -********** - -Το Nette Database χρησιμοποιεί μια ιεραρχία εξαιρέσεων. Η βασική κλάση είναι η `Nette\Database\DriverException`, η οποία κληρονομεί από την `PDOException` και παρέχει διευρυμένες δυνατότητες για την εργασία με σφάλματα βάσης δεδομένων: - -- Η μέθοδος `getDriverCode()` επιστρέφει τον κωδικό σφάλματος από τον οδηγό (driver) της βάσης δεδομένων. -- Η μέθοδος `getSqlState()` επιστρέφει τον κωδικό SQLSTATE. -- Οι μέθοδοι `getQueryString()` και `getParameters()` επιτρέπουν την απόκτηση του αρχικού ερωτήματος (query) και των παραμέτρων του. - -Από την `DriverException` κληρονομούν οι ακόλουθες εξειδικευμένες εξαιρέσεις: - -- `ConnectionException` - σηματοδοτεί αποτυχία σύνδεσης στον διακομιστή της βάσης δεδομένων. -- `ConstraintViolationException` - βασική κλάση για παραβίαση περιορισμών βάσης δεδομένων, από την οποία κληρονομούν: - - `ForeignKeyConstraintViolationException` - παραβίαση ξένου κλειδιού. - - `NotNullConstraintViolationException` - παραβίαση περιορισμού NOT NULL. - - `UniqueConstraintViolationException` - παραβίαση μοναδικότητας τιμής. - - -Παράδειγμα σύλληψης της εξαίρεσης `UniqueConstraintViolationException`, η οποία προκύπτει όταν προσπαθούμε να εισαγάγουμε έναν χρήστη με email που υπάρχει ήδη στη βάση δεδομένων (υποθέτοντας ότι η στήλη `email` έχει μοναδικό ευρετήριο - unique index). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Υπάρχει ήδη χρήστης με αυτό το email.'; // User with this email already exists. - -} catch (Nette\Database\DriverException $e) { - echo 'Παρουσιάστηκε σφάλμα κατά την εγγραφή: ' . $e->getMessage(); // An error occurred during registration: -} -``` diff --git a/database/el/explorer.texy b/database/el/explorer.texy deleted file mode 100644 index f09cee5fdd..0000000000 --- a/database/el/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Ο Explorer προσφέρει έναν διαισθητικό και αποτελεσματικό τρόπο εργασίας με τη βάση δεδομένων. Φροντίζει αυτόματα για τις σχέσεις μεταξύ των πινάκων και τη βελτιστοποίηση των ερωτημάτων (queries), ώστε να μπορείτε να επικεντρωθείτε στην εφαρμογή σας. Λειτουργεί αμέσως χωρίς καμία ρύθμιση. Αν χρειάζεστε πλήρη έλεγχο των ερωτημάτων SQL, μπορείτε να χρησιμοποιήσετε την [προσέγγιση SQL |sql-way]. - -- Η εργασία με τα δεδομένα είναι φυσική και εύκολα κατανοητή. -- Παράγει βελτιστοποιημένα ερωτήματα SQL που φορτώνουν μόνο τα απαραίτητα δεδομένα. -- Επιτρέπει εύκολη πρόσβαση σε σχετιζόμενα δεδομένα χωρίς την ανάγκη γραφής ερωτημάτων JOIN. -- Λειτουργεί άμεσα χωρίς καμία διαμόρφωση ή παραγωγή οντοτήτων (entities). - -</div> - - -Με τον Explorer ξεκινάτε καλώντας τη μέθοδο `table()` του αντικειμένου [api:Nette\Database\Explorer] (λεπτομέρειες για τη σύνδεση θα βρείτε στο κεφάλαιο [Σύνδεση και Διαμόρφωση |guide#Σύνδεση και Διαμόρφωση]): - -```php -$books = $explorer->table('book'); // 'book' είναι το όνομα του πίνακα -``` - -Η μέθοδος επιστρέφει ένα αντικείμενο [Selection |api:Nette\Database\Table\Selection], το οποίο αντιπροσωπεύει ένα ερώτημα SQL. Σε αυτό το αντικείμενο μπορούμε να συνδέσουμε περαιτέρω μεθόδους για φιλτράρισμα και ταξινόμηση των αποτελεσμάτων. Το ερώτημα συντάσσεται και εκτελείται μόνο τη στιγμή που αρχίζουμε να ζητάμε δεδομένα, για παράδειγμα, με τη διέλευση ενός βρόχου `foreach`. Κάθε γραμμή αντιπροσωπεύεται από ένα αντικείμενο [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // εμφάνιση της στήλης 'title' - echo $book->author_id; // εμφάνιση της στήλης 'author_id' -} -``` - -Ο Explorer διευκολύνει θεμελιωδώς την εργασία με τις [#σχέσεις μεταξύ πινάκων]. Το ακόλουθο παράδειγμα δείχνει πόσο εύκολα μπορούμε να εμφανίσουμε δεδομένα από συνδεδεμένους πίνακες (βιβλία και οι συγγραφείς τους). Παρατηρήστε ότι δεν χρειάζεται να γράψουμε κανένα ερώτημα JOIN, το Nette τα δημιουργεί για εμάς: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Βιβλίο: ' . $book->title; // Book: - echo 'Συγγραφέας: ' . $book->author->name; // δημιουργεί JOIN στον πίνακα 'author' // Author: -} -``` - -Το Nette Database Explorer βελτιστοποιεί τα ερωτήματα ώστε να είναι όσο το δυνατόν πιο αποτελεσματικά. Το παραπάνω παράδειγμα εκτελεί μόνο δύο ερωτήματα SELECT, ανεξάρτητα από το αν επεξεργαζόμαστε 10 ή 10.000 βιβλία. - -Επιπλέον, ο Explorer παρακολουθεί ποιες στήλες χρησιμοποιούνται στον κώδικα και φορτώνει από τη βάση δεδομένων μόνο αυτές, εξοικονομώντας έτσι περαιτέρω απόδοση. Αυτή η συμπεριφορά είναι πλήρως αυτόματη και προσαρμοστική. Αν αργότερα τροποποιήσετε τον κώδικα και αρχίσετε να χρησιμοποιείτε άλλες στήλες, ο Explorer προσαρμόζει αυτόματα τα ερωτήματα. Δεν χρειάζεται να ρυθμίσετε τίποτα, ούτε να σκεφτείτε ποιες στήλες θα χρειαστείτε - αφήστε το στο Nette. - - -Φιλτράρισμα και Ταξινόμηση -========================== - -Η κλάση `Selection` παρέχει μεθόδους για το φιλτράρισμα και την ταξινόμηση της επιλογής δεδομένων. - -.[language-php] -| `where($condition, ...$params)` | Προσθέτει συνθήκη WHERE. Πολλαπλές συνθήκες συνδέονται με τον τελεστή AND -| `whereOr(array $conditions)` | Προσθέτει μια ομάδα συνθηκών WHERE συνδεδεμένων με τον τελεστή OR -| `wherePrimary($value)` | Προσθέτει συνθήκη WHERE βάσει του πρωτεύοντος κλειδιού -| `order($columns, ...$params)` | Ορίζει την ταξινόμηση ORDER BY -| `select($columns, ...$params)` | Καθορίζει τις στήλες που πρέπει να φορτωθούν -| `limit($limit, $offset = null)` | Περιορίζει τον αριθμό των γραμμών (LIMIT) και προαιρετικά ορίζει το OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Ορίζει τη σελίδωση -| `group($columns, ...$params)` | Ομαδοποιεί τις γραμμές (GROUP BY) -| `having($condition, ...$params)` | Προσθέτει συνθήκη HAVING για το φιλτράρισμα των ομαδοποιημένων γραμμών - -Οι μέθοδοι μπορούν να αλυσιδωθούν (το λεγόμενο [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -Σε αυτές τις μεθόδους μπορείτε επίσης να χρησιμοποιείτε ειδική σημειογραφία για την πρόσβαση σε [δεδομένα από σχετικούς πίνακες |#Ερωτήματα μέσω Σχετικών Πινάκων]. - - -Escaping και Αναγνωριστικά --------------------------- - -Οι μέθοδοι κάνουν αυτόματα escaping τις παραμέτρους και περικλείουν σε εισαγωγικά τα αναγνωριστικά (ονόματα πινάκων και στηλών), αποτρέποντας έτσι το SQL injection. Για τη σωστή λειτουργία, είναι απαραίτητο να τηρούνται ορισμένοι κανόνες: - -- Λέξεις-κλειδιά, ονόματα συναρτήσεων, διαδικασιών κ.λπ. γράφονται με **κεφαλαία γράμματα**. -- Ονόματα στηλών και πινάκων γράφονται με **μικρά γράμματα**. -- Τα strings πάντα εισάγονται μέσω **παραμέτρων**. - -```php -where('name = ' . $name); // ΚΡΙΣΙΜΗ ΕΥΠΑΘΕΙΑ: SQL injection -where('name LIKE "%search%"'); // ΛΑΘΟΣ: περιπλέκει την αυτόματη περικλείωση σε εισαγωγικά -where('name LIKE ?', '%search%'); // ΣΩΣΤΟ: η τιμή εισάγεται μέσω παραμέτρου - -where('name like ?', $name); // ΛΑΘΟΣ: παράγει: `name` `like` ? -where('name LIKE ?', $name); // ΣΩΣΤΟ: παράγει: `name` LIKE ? -where('LOWER(name) = ?', $value);// ΣΩΣΤΟ: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Φιλτράρει τα αποτελέσματα χρησιμοποιώντας συνθήκες WHERE. Η ισχυρή της πλευρά είναι η έξυπνη διαχείριση διαφόρων τύπων τιμών και η αυτόματη επιλογή τελεστών SQL. - -Βασική χρήση: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Χάρη στην αυτόματη ανίχνευση των κατάλληλων τελεστών, δεν χρειάζεται να ασχολούμαστε με διάφορες ειδικές περιπτώσεις. Το Nette τις λύνει για εμάς: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// μπορεί να χρησιμοποιηθεί και το placeholder ερωτηματικό (?) χωρίς τελεστή: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Η μέθοδος επεξεργάζεται σωστά και τις αρνητικές συνθήκες και τους κενούς πίνακες: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- τίποτα δεν θα βρεθεί -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- όλα θα βρεθούν -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- όλα θα βρεθούν -// $table->where('NOT id ?', $ids); Προσοχή - αυτή η σύνταξη δεν υποστηρίζεται -``` - -Ως παράμετρο μπορούμε να περάσουμε επίσης το αποτέλεσμα από έναν άλλο πίνακα - θα δημιουργηθεί ένα υποερώτημα (subquery): - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Τις συνθήκες μπορούμε να τις περάσουμε επίσης ως πίνακα, τα στοιχεία του οποίου συνδέονται με AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -Στον πίνακα μπορούμε να χρησιμοποιήσουμε ζεύγη κλειδί => τιμή και το Nette πάλι επιλέγει αυτόματα τους σωστούς τελεστές: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -Στον πίνακα μπορούμε να συνδυάσουμε εκφράσεις SQL με placeholders ερωτηματικά (?) και πολλαπλές παραμέτρους. Αυτό είναι κατάλληλο για πολύπλοκες συνθήκες με ακριβώς καθορισμένους τελεστές: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // δύο παραμέτρους τις περνάμε ως πίνακα -]); -``` - -Οι πολλαπλές κλήσεις `where()` συνδέουν αυτόματα τις συνθήκες με AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Παρόμοια με το `where()`, προσθέτει συνθήκες, αλλά με τη διαφορά ότι τις συνδέει με OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Και εδώ μπορούμε να χρησιμοποιήσουμε πιο πολύπλοκες εκφράσεις: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Προσθέτει συνθήκη για το πρωτεύον κλειδί του πίνακα: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Αν ο πίνακας έχει σύνθετο πρωτεύον κλειδί (π.χ. `foo_id`, `bar_id`), το περνάμε ως πίνακα: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Καθορίζει τη σειρά με την οποία θα επιστραφούν οι γραμμές. Μπορούμε να ταξινομήσουμε με βάση μία ή περισσότερες στήλες, σε φθίνουσα ή αύξουσα σειρά, ή με βάση μια δική μας έκφραση: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Καθορίζει τις στήλες που θα επιστραφούν από τη βάση δεδομένων. Από προεπιλογή, το Nette Database Explorer επιστρέφει μόνο τις στήλες που χρησιμοποιούνται πραγματικά στον κώδικα. Τη μέθοδο `select()` τη χρησιμοποιούμε λοιπόν σε περιπτώσεις όπου χρειαζόμαστε να επιστρέψουμε συγκεκριμένες εκφράσεις: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Τα ψευδώνυμα (aliases) που ορίζονται με `AS` είναι στη συνέχεια διαθέσιμα ως ιδιότητες του αντικειμένου ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // πρόσβαση στο alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Περιορίζει τον αριθμό των επιστρεφόμενων γραμμών (LIMIT) και προαιρετικά επιτρέπει τον ορισμό της μετατόπισης (offset): - -```php -$table->limit(10); // LIMIT 10 (επιστρέφει τις πρώτες 10 γραμμές) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Για σελίδωση, είναι προτιμότερο να χρησιμοποιήσετε τη μέθοδο `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Διευκολύνει τη σελίδωση των αποτελεσμάτων. Δέχεται τον αριθμό της σελίδας (μετρώντας από το 1) και τον αριθμό των στοιχείων ανά σελίδα. Προαιρετικά, μπορεί να περάσει μια αναφορά σε μια μεταβλητή, στην οποία θα αποθηκευτεί ο συνολικός αριθμός σελίδων: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Συνολικά σελίδες: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Ομαδοποιεί τις γραμμές με βάση τις καθορισμένες στήλες (GROUP BY). Χρησιμοποιείται συνήθως σε συνδυασμό με συναρτήσεις συγκέντρωσης (aggregation functions): - -```php -// Υπολογίζει τον αριθμό των προϊόντων σε κάθε κατηγορία -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Ορίζει συνθήκη για το φιλτράρισμα των ομαδοποιημένων γραμμών (HAVING). Μπορεί να χρησιμοποιηθεί σε συνδυασμό με τη μέθοδο `group()` και συναρτήσεις συγκέντρωσης: - -```php -// Βρίσκει κατηγορίες που έχουν περισσότερα από 100 προϊόντα -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Ανάγνωση Δεδομένων -================== - -Για την ανάγνωση δεδομένων από τη βάση δεδομένων έχουμε στη διάθεσή μας αρκετές χρήσιμες μεθόδους: - -.[language-php] -| `foreach ($table as $key => $row)` | Επαναλαμβάνεται σε όλες τις γραμμές, το `$key` είναι η τιμή του πρωτεύοντος κλειδιού, το `$row` είναι αντικείμενο ActiveRow -| `$row = $table->get($key)` | Επιστρέφει μία γραμμή με βάση το πρωτεύον κλειδί -| `$row = $table->fetch()` | Επιστρέφει την τρέχουσα γραμμή και μετακινεί τον δείκτη στην επόμενη -| `$array = $table->fetchPairs()` | Δημιουργεί έναν συσχετιστικό πίνακα από τα αποτελέσματα -| `$array = $table->fetchAll()` | Επιστρέφει όλες τις γραμμές ως πίνακα -| `count($table)` | Επιστρέφει τον αριθμό των γραμμών στο αντικείμενο Selection - -Το αντικείμενο [ActiveRow |api:Nette\Database\Table\ActiveRow] προορίζεται μόνο για ανάγνωση. Αυτό σημαίνει ότι δεν μπορούν να αλλάξουν οι τιμές των ιδιοτήτων του. Αυτός ο περιορισμός εξασφαλίζει τη συνέπεια των δεδομένων και αποτρέπει απροσδόκητες παρενέργειες. Τα δεδομένα φορτώνονται από τη βάση δεδομένων και οποιαδήποτε αλλαγή θα πρέπει να γίνεται ρητά και ελεγχόμενα. - - -`foreach` - Επανάληψη σε Όλες τις Γραμμές ------------------------------------------ - -Ο ευκολότερος τρόπος για να εκτελέσετε ένα ερώτημα και να λάβετε γραμμές είναι με την επανάληψη σε έναν βρόχο `foreach`. Εκτελεί αυτόματα το ερώτημα SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // το $key είναι η τιμή του πρωτεύοντος κλειδιού, το $book είναι ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Εκτελεί το ερώτημα SQL και επιστρέφει τη γραμμή με βάση το πρωτεύον κλειδί, ή `null`, αν δεν υπάρχει. - -```php -$book = $explorer->table('book')->get(123); // επιστρέφει ActiveRow με ID 123 ή null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Επιστρέφει μια γραμμή και μετακινεί τον εσωτερικό δείκτη στην επόμενη. Αν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Επιστρέφει τα αποτελέσματα ως συσχετιστικό πίνακα. Το πρώτο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως κλειδί στον πίνακα, το δεύτερο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως τιμή: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Αν δώσουμε μόνο την πρώτη παράμετρο, η τιμή θα είναι ολόκληρη η γραμμή, δηλαδή το αντικείμενο `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Σε περίπτωση διπλότυπων κλειδιών, χρησιμοποιείται η τιμή από την τελευταία γραμμή. Κατά τη χρήση του `null` ως κλειδί, ο πίνακας θα ευρετηριαστεί αριθμητικά από το μηδέν (τότε δεν προκύπτουν συγκρούσεις): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Εναλλακτικά, μπορείτε να δώσετε ως παράμετρο ένα callback, το οποίο θα επιστρέφει για κάθε γραμμή είτε την ίδια την τιμή, είτε ένα ζεύγος κλειδί-τιμή. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Πρώτο βιβλίο (Γιάννης Νοβάκ)', ...] - -// Το Callback μπορεί επίσης να επιστρέφει έναν πίνακα με ένα ζεύγος κλειδί & τιμή: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Πρώτο βιβλίο' => 'Γιάννης Νοβάκ', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Επιστρέφει όλες τις γραμμές ως συσχετιστικό πίνακα αντικειμένων `ActiveRow`, όπου τα κλειδιά είναι οι τιμές των πρωτευόντων κλειδιών. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Η μέθοδος `count()` χωρίς παράμετρο επιστρέφει τον αριθμό των γραμμών στο αντικείμενο `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // εναλλακτική -``` - -Προσοχή, το `count()` με παράμετρο εκτελεί τη συνάρτηση συγκέντρωσης COUNT στη βάση δεδομένων. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Μετατρέπει το αντικείμενο `ActiveRow` σε συσχετιστικό πίνακα, όπου τα κλειδιά είναι τα ονόματα των στηλών και οι τιμές είναι τα αντίστοιχα δεδομένα. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// το $bookArray θα είναι ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Συγκέντρωση -=========== - -Η κλάση `Selection` παρέχει μεθόδους για εύκολη εκτέλεση συναρτήσεων συγκέντρωσης (COUNT, SUM, MIN, MAX, AVG κ.λπ.). - -.[language-php] -| `count($expr)` | Μετρά τον αριθμό των γραμμών -| `min($expr)` | Επιστρέφει την ελάχιστη τιμή στη στήλη -| `max($expr)` | Επιστρέφει τη μέγιστη τιμή στη στήλη -| `sum($expr)` | Επιστρέφει το άθροισμα των τιμών στη στήλη -| `aggregation($function)` | Επιτρέπει την εκτέλεση οποιασδήποτε συνάρτησης συγκέντρωσης. Π.χ. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Εκτελεί ένα ερώτημα SQL με τη συνάρτηση COUNT και επιστρέφει το αποτέλεσμα. Η μέθοδος χρησιμοποιείται για να διαπιστωθεί πόσες γραμμές αντιστοιχούν σε μια συγκεκριμένη συνθήκη: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Προσοχή, η μέθοδος [#count()] χωρίς παράμετρο επιστρέφει απλώς τον αριθμό των γραμμών στο αντικείμενο `Selection`. - - -min(string $expr) και max(string $expr) .[method] -------------------------------------------------- - -Οι μέθοδοι `min()` και `max()` επιστρέφουν την ελάχιστη και τη μέγιστη τιμή στην καθορισμένη στήλη ή έκφραση: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Επιστρέφει το άθροισμα των τιμών στην καθορισμένη στήλη ή έκφραση: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- - -Επιτρέπει την εκτέλεση οποιασδήποτε συνάρτησης συγκέντρωσης. - -```php -// μέση τιμή προϊόντων στην κατηγορία -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// συνδέει τις ετικέτες του προϊόντος σε ένα string -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Αν χρειαζόμαστε να συγκεντρώσουμε αποτελέσματα που ήδη προέκυψαν από κάποια συνάρτηση συγκέντρωσης και ομαδοποίηση (π.χ. `SUM(value)` σε ομαδοποιημένες γραμμές), ως δεύτερο όρισμα δίνουμε τη συνάρτηση συγκέντρωσης που πρέπει να εφαρμοστεί σε αυτά τα ενδιάμεσα αποτελέσματα: - -```php -// Υπολογίζει τη συνολική τιμή των προϊόντων στο απόθεμα για μεμονωμένες κατηγορίες και στη συνέχεια αθροίζει αυτές τις τιμές μαζί. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Σε αυτό το παράδειγμα, πρώτα υπολογίζουμε τη συνολική τιμή των προϊόντων σε κάθε κατηγορία (`SUM(price * stock) AS category_total`) και ομαδοποιούμε τα αποτελέσματα με βάση το `category_id`. Στη συνέχεια, χρησιμοποιούμε το `aggregation('SUM(category_total)', 'SUM')` για να αθροίσουμε αυτά τα ενδιάμεσα αθροίσματα `category_total`. Το δεύτερο όρισμα `'SUM'` δηλώνει ότι πρέπει να εφαρμοστεί η συνάρτηση SUM στα ενδιάμεσα αποτελέσματα. - - -Εισαγωγή, Ενημέρωση & Διαγραφή -============================== - -Το Nette Database Explorer απλοποιεί την εισαγωγή, την ενημέρωση και τη διαγραφή δεδομένων. Όλες οι αναφερόμενες μέθοδοι, σε περίπτωση σφάλματος, θα προκαλέσουν την εξαίρεση `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Εισάγει νέες εγγραφές στον πίνακα. - -**Εισαγωγή μιας εγγραφής:** - -Τη νέα εγγραφή την περνάμε ως συσχετιστικό πίνακα ή iterable αντικείμενο (για παράδειγμα ArrayHash που χρησιμοποιείται στις [φόρμες |forms:]), όπου τα κλειδιά αντιστοιχούν στα ονόματα των στηλών στον πίνακα. - -Αν ο πίνακας έχει ορισμένο πρωτεύον κλειδί, η μέθοδος επιστρέφει ένα αντικείμενο `ActiveRow`, το οποίο επαναφορτώνεται από τη βάση δεδομένων, ώστε να ληφθούν υπόψη τυχόν αλλαγές που έγιναν σε επίπεδο βάσης δεδομένων (triggers, προεπιλεγμένες τιμές στηλών, υπολογισμοί auto-increment στηλών). Έτσι εξασφαλίζεται η συνέπεια των δεδομένων και το αντικείμενο περιέχει πάντα τα τρέχοντα δεδομένα από τη βάση δεδομένων. Αν δεν έχει μοναδικό πρωτεύον κλειδί, επιστρέφει τα παραδοθέντα δεδομένα με τη μορφή πίνακα. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// το $row είναι παρουσία του ActiveRow και περιέχει τα πλήρη δεδομένα της εισαχθείσας γραμμής, -// συμπεριλαμβανομένου του αυτόματα παραγόμενου ID και τυχόν αλλαγών που έγιναν από triggers -echo $row->id; // Εμφανίζει το ID του νέου εισαχθέντος χρήστη -echo $row->created_at; // Εμφανίζει τον χρόνο δημιουργίας, αν έχει οριστεί από trigger -``` - -**Εισαγωγή πολλαπλών εγγραφών ταυτόχρονα:** - -Η μέθοδος `insert()` επιτρέπει την εισαγωγή πολλαπλών εγγραφών με ένα μόνο ερώτημα SQL. Σε αυτή την περίπτωση, επιστρέφει τον αριθμό των εισαχθέντων γραμμών. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// το $insertedRows θα είναι 2 -``` - -Ως παράμετρο μπορεί επίσης να περάσει ένα αντικείμενο `Selection` με επιλογή δεδομένων. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Εισαγωγή ειδικών τιμών:** - -Ως τιμές μπορούμε να περάσουμε και αρχεία, αντικείμενα DateTime ή SQL literals: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // μετατρέπει σε μορφή βάσης δεδομένων - 'avatar' => fopen('image.jpg', 'rb'), // εισάγει το δυαδικό περιεχόμενο του αρχείου - 'uuid' => $explorer::literal('UUID()'), // καλεί τη συνάρτηση UUID() της βάσης δεδομένων -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Ενημερώνει γραμμές στον πίνακα σύμφωνα με το καθορισμένο φίλτρο. Επιστρέφει τον αριθμό των γραμμών που πραγματικά άλλαξαν. - -Τις στήλες που αλλάζουν τις περνάμε ως συσχετιστικό πίνακα ή iterable αντικείμενο (για παράδειγμα ArrayHash που χρησιμοποιείται στις [φόρμες |forms:]), όπου τα κλειδιά αντιστοιχούν στα ονόματα των στηλών στον πίνακα: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Για την αλλαγή αριθμητικών τιμών μπορούμε να χρησιμοποιήσουμε τους τελεστές `+=` και `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // αυξάνει την τιμή της στήλης 'points' κατά 1 - 'coins-=' => 1, // μειώνει την τιμή της στήλης 'coins' κατά 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Διαγράφει γραμμές από τον πίνακα σύμφωνα με το καθορισμένο φίλτρο. Επιστρέφει τον αριθμό των διαγραμμένων γραμμών. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Κατά την κλήση `update()` και `delete()`, μην ξεχάσετε να καθορίσετε με το `where()` τις γραμμές που πρέπει να τροποποιηθούν/διαγραφούν. Αν δεν χρησιμοποιήσετε το `where()`, η λειτουργία θα εκτελεστεί σε ολόκληρο τον πίνακα! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Ενημερώνει τα δεδομένα στη γραμμή της βάσης δεδομένων που αντιπροσωπεύεται από το αντικείμενο `ActiveRow`. Ως παράμετρο δέχεται ένα iterable με τα δεδομένα που πρέπει να ενημερωθούν (τα κλειδιά είναι τα ονόματα των στηλών). Για την αλλαγή αριθμητικών τιμών μπορούμε να χρησιμοποιήσουμε τους τελεστές `+=` και `-=`: - -Μετά την εκτέλεση της ενημέρωσης, το `ActiveRow` επαναφορτώνεται αυτόματα από τη βάση δεδομένων, ώστε να ληφθούν υπόψη τυχόν αλλαγές που έγιναν σε επίπεδο βάσης δεδομένων (π.χ. triggers). Η μέθοδος επιστρέφει `true` μόνο αν έγινε πραγματική αλλαγή δεδομένων. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // αυξάνουμε τον αριθμό προβολών -]); -echo $article->views; // Εμφανίζει τον τρέχοντα αριθμό προβολών -``` - -Αυτή η μέθοδος ενημερώνει μόνο μία συγκεκριμένη γραμμή στη βάση δεδομένων. Για μαζική ενημέρωση πολλαπλών γραμμών χρησιμοποιήστε τη μέθοδο [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Διαγράφει τη γραμμή από τη βάση δεδομένων, η οποία αντιπροσωπεύεται από το αντικείμενο `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Διαγράφει το βιβλίο με ID 1 -``` - -Αυτή η μέθοδος διαγράφει μόνο μία συγκεκριμένη γραμμή στη βάση δεδομένων. Για μαζική διαγραφή πολλαπλών γραμμών χρησιμοποιήστε τη μέθοδο [#Selection::delete()]. - - -Σχέσεις μεταξύ Πινάκων -====================== - -Σε σχεσιακές βάσεις δεδομένων, τα δεδομένα χωρίζονται σε πολλούς πίνακες και συνδέονται μεταξύ τους με ξένα κλειδιά. Το Nette Database Explorer φέρνει έναν επαναστατικό τρόπο εργασίας με αυτές τις σχέσεις - χωρίς να γράφετε ερωτήματα JOIN και χωρίς την ανάγκη να διαμορφώνετε ή να παράγετε οτιδήποτε. - -Για την απεικόνιση της εργασίας με τις σχέσεις θα χρησιμοποιήσουμε ένα παράδειγμα βάσης δεδομένων βιβλίων ([μπορείτε να το βρείτε στο GitHub |https://github.com/nette-examples/books]). Στη βάση δεδομένων έχουμε τους πίνακες: - -- `author` - συγγραφείς και μεταφραστές (στήλες `id`, `name`, `web`, `born`) -- `book` - βιβλία (στήλες `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - ετικέτες (στήλες `id`, `name`) -- `book_tag` - πίνακας σύνδεσης μεταξύ βιβλίων και ετικετών (στήλες `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Δομή της βάσης δεδομένων .<> - -Στο παράδειγμά μας της βάσης δεδομένων βιβλίων βρίσκουμε διάφορους τύπους σχέσεων (αν και το μοντέλο είναι απλοποιημένο σε σχέση με την πραγματικότητα): - -- One-to-many (1:N) – κάθε βιβλίο **έχει έναν** συγγραφέα, ο συγγραφέας μπορεί να γράψει **πολλά** βιβλία. -- Zero-to-many (0:N) – το βιβλίο **μπορεί να έχει** μεταφραστή, ο μεταφραστής μπορεί να μεταφράσει **πολλά** βιβλία. -- Zero-to-one (0:1) – το βιβλίο **μπορεί να έχει** επόμενο τόμο. -- Many-to-many (M:N) – το βιβλίο **μπορεί να έχει πολλές** ετικέτες και η ετικέτα μπορεί να αντιστοιχιστεί σε **πολλά** βιβλία. - -Σε αυτές τις σχέσεις υπάρχει πάντα ένας γονικός (parent) και ένας παιδικός (child) πίνακας. Για παράδειγμα, στη σχέση μεταξύ συγγραφέα και βιβλίου, ο πίνακας `author` είναι γονικός και ο `book` παιδικός - μπορούμε να το φανταστούμε έτσι ώστε το βιβλίο πάντα "ανήκει" σε κάποιον συγγραφέα. Αυτό εκδηλώνεται και στη δομή της βάσης δεδομένων: ο παιδικός πίνακας `book` περιέχει το ξένο κλειδί `author_id`, το οποίο αναφέρεται στον γονικό πίνακα `author`. - -Αν χρειαζόμαστε να εμφανίσουμε τα βιβλία συμπεριλαμβανομένων των ονομάτων των συγγραφέων τους, έχουμε δύο δυνατότητες. Είτε θα λάβουμε τα δεδομένα με ένα μόνο ερώτημα SQL χρησιμοποιώντας JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Είτε θα φορτώσουμε τα δεδομένα σε δύο βήματα - πρώτα τα βιβλία και μετά τους συγγραφείς τους - και στη συνέχεια θα τα συνθέσουμε στην PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- ids των συγγραφέων των ληφθέντων βιβλίων -``` - -Η δεύτερη προσέγγιση είναι στην πραγματικότητα πιο αποτελεσματική, αν και αυτό μπορεί να προκαλεί έκπληξη. Τα δεδομένα φορτώνονται μόνο μία φορά και μπορούν να αξιοποιηθούν καλύτερα στην cache. Ακριβώς με αυτόν τον τρόπο λειτουργεί το Nette Database Explorer - λύνει τα πάντα κάτω από την επιφάνεια και σας προσφέρει ένα κομψό API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'τίτλος: ' . $book->title; - echo 'γράφτηκε από: ' . $book->author->name; // το $book->author είναι η εγγραφή από τον πίνακα 'author' - echo 'μεταφράστηκε από: ' . $book->translator?->name; -} -``` - - -Πρόσβαση στον Γονικό Πίνακα ---------------------------- - -Η πρόσβαση στον γονικό πίνακα είναι άμεση. Πρόκειται για σχέσεις όπως *το βιβλίο έχει συγγραφέα* ή *το βιβλίο μπορεί να έχει μεταφραστή*. Την σχετιζόμενη εγγραφή την λαμβάνουμε μέσω της ιδιότητας του αντικειμένου ActiveRow - το όνομά της αντιστοιχεί στο όνομα της στήλης με το ξένο κλειδί, αφαιρώντας το `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // βρίσκει τον συγγραφέα με βάση τη στήλη author_id -echo $book->translator?->name; // βρίσκει τον μεταφραστή με βάση τη στήλη translator_id -``` - -Όταν αποκτούμε πρόσβαση στην ιδιότητα `$book->author`, ο Explorer στον πίνακα `book` αναζητά μια στήλη της οποίας το όνομα περιέχει το string `author` (δηλαδή `author_id`). Με βάση την τιμή σε αυτή τη στήλη, φορτώνει την αντίστοιχη εγγραφή από τον πίνακα `author` και την επιστρέφει ως `ActiveRow`. Παρόμοια λειτουργεί και το `$book->translator`, το οποίο χρησιμοποιεί τη στήλη `translator_id`. Επειδή η στήλη `translator_id` μπορεί να περιέχει `null`, χρησιμοποιούμε στον κώδικα τον τελεστή nullsafe `?->`. - -Μια εναλλακτική οδό προσφέρει η μέθοδος `ref()`, η οποία δέχεται δύο ορίσματα, το όνομα του πίνακα προορισμού και το όνομα της στήλης σύνδεσης, και επιστρέφει μια παρουσία `ActiveRow` ή `null`: - -```php -echo $book->ref('author', 'author_id')->name; // σχέση με τον συγγραφέα -echo $book->ref('author', 'translator_id')->name; // σχέση με τον μεταφραστή -``` - -Η μέθοδος `ref()` είναι χρήσιμη αν δεν μπορεί να χρησιμοποιηθεί η πρόσβαση μέσω ιδιότητας, επειδή ο πίνακας περιέχει στήλη με το ίδιο όνομα (δηλ. `author`). Στις υπόλοιπες περιπτώσεις, συνιστάται η χρήση της πρόσβασης μέσω ιδιότητας, η οποία είναι πιο ευανάγνωστη. - -Ο Explorer βελτιστοποιεί αυτόματα τα ερωτήματα της βάσης δεδομένων. Όταν διατρέχουμε τα βιβλία σε έναν βρόχο και αποκτούμε πρόσβαση στις σχετιζόμενες εγγραφές τους (συγγραφείς, μεταφραστές), ο Explorer δεν παράγει ένα ερώτημα για κάθε βιβλίο ξεχωριστά. Αντ' αυτού, εκτελεί μόνο ένα SELECT για κάθε τύπο σχέσης, μειώνοντας έτσι σημαντικά το φορτίο της βάσης δεδομένων. Για παράδειγμα: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Αυτός ο κώδικας θα καλέσει μόνο αυτά τα τρία αστραπιαία ερωτήματα στη βάση δεδομένων: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id από τη στήλη author_id των επιλεγμένων βιβλίων -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id από τη στήλη translator_id των επιλεγμένων βιβλίων -``` - -.[note] -Η λογική εύρεσης της στήλης σύνδεσης καθορίζεται από την υλοποίηση των [Conventions |api:Nette\Database\Conventions]. Συνιστούμε τη χρήση των [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], οι οποίες αναλύουν τα ξένα κλειδιά και επιτρέπουν την εύκολη εργασία με τις υπάρχουσες σχέσεις μεταξύ των πινάκων. - - -Πρόσβαση στον Παιδικό Πίνακα ----------------------------- - -Η πρόσβαση στον παιδικό πίνακα λειτουργεί με την αντίστροφη κατεύθυνση. Τώρα ρωτάμε *ποια βιβλία έγραψε αυτός ο συγγραφέας* ή *μετέφρασε αυτός ο μεταφραστής*. Για αυτόν τον τύπο ερωτήματος χρησιμοποιούμε τη μέθοδο `related()`, η οποία επιστρέφει ένα `Selection` με τις σχετιζόμενες εγγραφές. Ας δούμε ένα παράδειγμα: - -```php -$author = $explorer->table('author')->get(1); - -// Εμφανίζει όλα τα βιβλία του συγγραφέα -foreach ($author->related('book.author_id') as $book) { - echo "Έγραψε: $book->title"; -} - -// Εμφανίζει όλα τα βιβλία που μετέφρασε ο συγγραφέας -foreach ($author->related('book.translator_id') as $book) { - echo "Μετέφρασε: $book->title"; -} -``` - -Η μέθοδος `related()` δέχεται την περιγραφή της σύνδεσης ως ένα όρισμα με σημειογραφία τελείας ή ως δύο ξεχωριστά ορίσματα: - -```php -$author->related('book.translator_id'); // ένα όρισμα -$author->related('book', 'translator_id'); // δύο ορίσματα -``` - -Ο Explorer μπορεί να ανιχνεύσει αυτόματα τη σωστή στήλη σύνδεσης με βάση το όνομα του γονικού πίνακα. Σε αυτή την περίπτωση, η σύνδεση γίνεται μέσω της στήλης `book.author_id`, επειδή το όνομα του πίνακα πηγής είναι `author`: - -```php -$author->related('book'); // χρησιμοποιεί το book.author_id -``` - -Αν υπήρχαν περισσότερες πιθανές συνδέσεις, ο Explorer θα προκαλούσε την εξαίρεση [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Τη μέθοδο `related()` μπορούμε φυσικά να τη χρησιμοποιήσουμε και κατά τη διέλευση πολλαπλών εγγραφών σε έναν βρόχο και ο Explorer και σε αυτή την περίπτωση βελτιστοποιεί αυτόματα τα ερωτήματα: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' έγραψε:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Αυτός ο κώδικας θα παράγει μόνο δύο αστραπιαία ερωτήματα SQL: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id των επιλεγμένων συγγραφέων -``` - - -Σχέση Many-to-Many ------------------- - -Για τη σχέση many-to-many (M:N) είναι απαραίτητη η ύπαρξη ενός πίνακα σύνδεσης (στην περίπτωσή μας `book_tag`), ο οποίος περιέχει δύο στήλες με ξένα κλειδιά (`book_id`, `tag_id`). Κάθε μία από αυτές τις στήλες αναφέρεται στο πρωτεύον κλειδί ενός από τους συνδεόμενους πίνακες. Για να λάβουμε τα σχετιζόμενα δεδομένα, πρώτα λαμβάνουμε τις εγγραφές από τον πίνακα σύνδεσης χρησιμοποιώντας το `related('book_tag')` και στη συνέχεια συνεχίζουμε στα δεδομένα προορισμού: - -```php -$book = $explorer->table('book')->get(1); -// εμφανίζει τα ονόματα των ετικετών που έχουν αντιστοιχιστεί στο βιβλίο -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // εμφανίζει το όνομα της ετικέτας μέσω του πίνακα σύνδεσης -} - -$tag = $explorer->table('tag')->get(1); -// ή αντίστροφα: εμφανίζει τα ονόματα των βιβλίων που έχουν επισημανθεί με αυτή την ετικέτα -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // εμφανίζει το όνομα του βιβλίου -} -``` - -Ο Explorer πάλι βελτιστοποιεί τα ερωτήματα SQL σε αποτελεσματική μορφή: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id των επιλεγμένων βιβλίων -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id των ετικετών που βρέθηκαν στο book_tag -``` - - -Ερωτήματα μέσω Σχετικών Πινάκων -------------------------------- - -Στις μεθόδους `where()`, `select()`, `order()` και `group()` μπορούμε να χρησιμοποιούμε ειδικές σημειογραφίες για την πρόσβαση σε στήλες από άλλους πίνακες. Ο Explorer δημιουργεί αυτόματα τα απαραίτητα JOINs. - -**Σημειογραφία τελείας** (`parent_table.column`) χρησιμοποιείται για τη σχέση 1:N από την οπτική γωνία του παιδικού πίνακα: - -```php -$books = $explorer->table('book'); - -// Βρίσκει βιβλία των οποίων ο συγγραφέας έχει όνομα που αρχίζει από 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Ταξινομεί τα βιβλία με βάση το όνομα του συγγραφέα φθίνουσα -$books->order('author.name DESC'); - -// Εμφανίζει τον τίτλο του βιβλίου και το όνομα του συγγραφέα -$books->select('book.title, author.name'); -``` - -**Σημειογραφία άνω και κάτω τελείας** (`:child_table.column`) χρησιμοποιείται για τη σχέση 1:N από την οπτική γωνία του γονικού πίνακα: - -```php -$authors = $explorer->table('author'); - -// Βρίσκει συγγραφείς που έγραψαν βιβλίο με 'PHP' στον τίτλο -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Μετρά τον αριθμό των βιβλίων για κάθε συγγραφέα -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -Στο παραπάνω παράδειγμα με τη σημειογραφία άνω και κάτω τελείας (`:book.title`) δεν καθορίζεται η στήλη με το ξένο κλειδί. Ο Explorer ανιχνεύει αυτόματα τη σωστή στήλη με βάση το όνομα του γονικού πίνακα. Σε αυτή την περίπτωση, η σύνδεση γίνεται μέσω της στήλης `book.author_id`, επειδή το όνομα του πίνακα πηγής είναι `author`. Αν υπήρχαν περισσότερες πιθανές συνδέσεις, ο Explorer θα προκαλούσε την εξαίρεση [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Η στήλη σύνδεσης μπορεί να δηλωθεί ρητά σε παρένθεση: - -```php -// Βρίσκει συγγραφείς που μετέφρασαν βιβλίο με 'PHP' στον τίτλο -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Οι σημειογραφίες μπορούν να αλυσιδωθούν για πρόσβαση μέσω πολλαπλών πινάκων: - -```php -// Βρίσκει συγγραφείς βιβλίων που έχουν επισημανθεί με την ετικέτα 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Επέκταση Συνθηκών για JOIN --------------------------- - -Η μέθοδος `joinWhere()` επεκτείνει τις συνθήκες που αναφέρονται κατά τη σύνδεση πινάκων στο SQL μετά τη λέξη-κλειδί `ON`. - -Ας υποθέσουμε ότι θέλουμε να βρούμε βιβλία που μεταφράστηκαν από έναν συγκεκριμένο μεταφραστή: - -```php -// Βρίσκει βιβλία που μεταφράστηκαν από τον μεταφραστή με όνομα 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -Στη συνθήκη `joinWhere()` μπορούμε να χρησιμοποιούμε τις ίδιες κατασκευές όπως στη μέθοδο `where()` - τελεστές, placeholders ερωτηματικά (?), πίνακες τιμών ή εκφράσεις SQL. - -Για πιο πολύπλοκα ερωτήματα με πολλαπλά JOINs, μπορούμε να ορίσουμε ψευδώνυμα (aliases) πινάκων: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Παρατηρήστε ότι ενώ η μέθοδος `where()` προσθέτει συνθήκες στην πρόταση `WHERE`, η μέθοδος `joinWhere()` επεκτείνει τις συνθήκες στην πρόταση `ON` κατά τη σύνδεση των πινάκων. diff --git a/database/el/guide.texy b/database/el/guide.texy deleted file mode 100644 index c3515aba11..0000000000 --- a/database/el/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Το Nette Database είναι ένα ισχυρό και κομψό επίπεδο βάσης δεδομένων για PHP με έμφαση στην απλότητα και τις έξυπνες λειτουργίες. Προσφέρει δύο τρόπους εργασίας με τη βάση δεδομένων - [Explorer |Explorer] για γρήγορη ανάπτυξη εφαρμογών, ή [πρόσβαση SQL |SQL way] για άμεση εργασία με ερωτήματα. - -<div class="grid gap-3"> -<div> - - -[Πρόσβαση SQL |SQL way] -======================= -- Ασφαλή παραμετροποιημένα ερωτήματα -- Ακριβής έλεγχος της μορφής των ερωτημάτων SQL -- Όταν γράφετε σύνθετα ερωτήματα με προηγμένες λειτουργίες -- Βελτιστοποιείτε την απόδοση χρησιμοποιώντας συγκεκριμένες λειτουργίες SQL - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Αναπτύσσετε γρήγορα χωρίς να γράφετε SQL -- Διαισθητική εργασία με σχέσεις μεταξύ πινάκων -- Εκτιμάτε την αυτόματη βελτιστοποίηση ερωτημάτων -- Κατάλληλο για γρήγορη και άνετη εργασία με τη βάση δεδομένων - -</div> - -</div> - - -Εγκατάσταση -=========== - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Υποστηριζόμενες Βάσεις Δεδομένων -================================ - -Το Nette Database υποστηρίζει τις ακόλουθες βάσεις δεδομένων: - -|* Διακομιστής Βάσης Δεδομένων |* Όνομα DSN |* Υποστήριξη στον Explorer -|-----------------------------|-------------|-------------------------- -| MySQL (>= 5.1) | mysql | ΝΑΙ -| PostgreSQL (>= 9.0) | pgsql | ΝΑΙ -| Sqlite 3 (>= 3.8) | sqlite | ΝΑΙ -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ΝΑΙ -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Δύο Προσεγγίσεις στη Βάση Δεδομένων -=================================== - -Το Nette Database σας δίνει μια επιλογή: μπορείτε είτε να γράψετε απευθείας ερωτήματα SQL (πρόσβαση SQL), είτε να τα αφήσετε να δημιουργηθούν αυτόματα (Explorer). Ας δούμε πώς και οι δύο προσεγγίσεις επιλύουν τις ίδιες εργασίες: - -[Πρόσβαση SQL|sql way] - Ερωτήματα SQL - -```php -// εισαγωγή εγγραφής -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// λήψη εγγραφών: συγγραφείς βιβλίων -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// έξοδος (δεν είναι βέλτιστη, δημιουργεί N+1 ερωτήματα) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Ο συγγραφέας $author->name έγραψε $author->books_count βιβλία:\n"; // Author $author->name wrote $author->books_count books:\n - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Πρόσβαση Explorer|explorer] - Αυτόματη δημιουργία SQL - -```php -// εισαγωγή εγγραφής -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// λήψη εγγραφών: συγγραφείς βιβλίων -$authors = $database->table('authors') - ->where('active', 1); - -// έξοδος (δημιουργεί αυτόματα μόνο 2 βελτιστοποιημένα ερωτήματα) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Ο συγγραφέας $author->name έγραψε {$books->count()} βιβλία:\n"; // Author $author->name wrote {$books->count()} books:\n - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Η προσέγγιση Explorer δημιουργεί και βελτιστοποιεί αυτόματα τα ερωτήματα SQL. Στο παραπάνω παράδειγμα, η πρόσβαση SQL δημιουργεί N+1 ερωτήματα (ένα για τους συγγραφείς και στη συνέχεια ένα για τα βιβλία κάθε συγγραφέα), ενώ ο Explorer βελτιστοποιεί αυτόματα τα ερωτήματα και εκτελεί μόνο δύο - ένα για τους συγγραφείς και ένα για όλα τα βιβλία τους. - -Και οι δύο προσεγγίσεις μπορούν να συνδυαστούν ελεύθερα στην εφαρμογή ανάλογα με τις ανάγκες. - - -Σύνδεση και Διαμόρφωση -====================== - -Για να συνδεθείτε στη βάση δεδομένων, απλώς δημιουργήστε μια παρουσία της κλάσης [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Η παράμετρος `$dsn` (data source name) είναι η ίδια [που χρησιμοποιεί το PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], π.χ. `mysql:host=127.0.0.1;dbname=test`. Σε περίπτωση αποτυχίας, θα προκαλέσει μια εξαίρεση `Nette\Database\ConnectionException`. - -Ωστόσο, ένας πιο βολικός τρόπος προσφέρεται από τη [διαμόρφωση εφαρμογής |configuration], όπου απλά προσθέτετε την ενότητα `database` και δημιουργούνται τα απαραίτητα αντικείμενα καθώς και ο πίνακας της βάσης δεδομένων στη γραμμή [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Στη συνέχεια, [λαμβάνουμε το αντικείμενο σύνδεσης ως υπηρεσία από το DI container |dependency-injection:passing-dependencies], π.χ.: - -```php -class Model -{ - public function __construct( - // ή Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Περισσότερες πληροφορίες σχετικά με τη [διαμόρφωση της βάσης δεδομένων|configuration]. - - -Χειροκίνητη Δημιουργία του Explorer ------------------------------------ - -Εάν δεν χρησιμοποιείτε το Nette DI container, μπορείτε να δημιουργήσετε χειροκίνητα την παρουσία `Nette\Database\Explorer`: - -```php -// σύνδεση στη βάση δεδομένων -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// αποθήκη για την cache, υλοποιεί το Nette\Caching\Storage, π.χ.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// φροντίζει για την αντανάκλαση της δομής της βάσης δεδομένων -$structure = new Nette\Database\Structure($connection, $storage); -// ορίζει κανόνες για την αντιστοίχιση ονομάτων πινάκων, στηλών και ξένων κλειδιών -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Διαχείριση Σύνδεσης -=================== - -Κατά τη δημιουργία του αντικειμένου `Connection`, η σύνδεση πραγματοποιείται αυτόματα. Εάν θέλετε να καθυστερήσετε τη σύνδεση, χρησιμοποιήστε τη λειτουργία lazy - την ενεργοποιείτε στη [διαμόρφωση|configuration] ορίζοντας το `lazy: true`, ή ως εξής: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Για τη διαχείριση της σύνδεσης, χρησιμοποιήστε τις μεθόδους `connect()`, `disconnect()` και `reconnect()`. -- `connect()`: δημιουργεί τη σύνδεση, εάν δεν υπάρχει ήδη. Μπορεί να προκαλέσει εξαίρεση `Nette\Database\ConnectionException`. -- `disconnect()`: αποσυνδέει την τρέχουσα σύνδεση με τη βάση δεδομένων. -- `reconnect()`: πραγματοποιεί αποσύνδεση και στη συνέχεια επανασύνδεση με τη βάση δεδομένων. Αυτή η μέθοδος μπορεί επίσης να προκαλέσει εξαίρεση `Nette\Database\ConnectionException`. - -Επιπλέον, μπορείτε να παρακολουθείτε τα συμβάντα που σχετίζονται με τη σύνδεση χρησιμοποιώντας το συμβάν `onConnect`, το οποίο είναι ένας πίνακας callbacks που καλούνται μετά την εγκατάσταση της σύνδεσης με τη βάση δεδομένων. - -```php -// εκτελείται μετά τη σύνδεση στη βάση δεδομένων -$database->onConnect[] = function($database) { - echo "Συνδεθήκατε στη βάση δεδομένων"; // Connected to the database -}; -``` - - -Tracy Debug Bar -=============== - -Εάν χρησιμοποιείτε το [Tracy |tracy:], ενεργοποιείται αυτόματα ο πίνακας Database στη γραμμή Debug, ο οποίος εμφανίζει όλα τα εκτελεσμένα ερωτήματα, τις παραμέτρους τους, τον χρόνο εκτέλεσης και το σημείο στον κώδικα όπου κλήθηκαν. - -[* db-panel.webp *] diff --git a/database/el/mapping.texy b/database/el/mapping.texy deleted file mode 100644 index 0ab89d43e2..0000000000 --- a/database/el/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Μετατροπή Τύπων -*************** - -.[perex] -Το Nette Database μετατρέπει αυτόματα τις τιμές που επιστρέφονται από τη βάση δεδομένων στους αντίστοιχους τύπους PHP. - - -Ημερομηνία και Ώρα ------------------- - -Οι χρονικές τιμές μετατρέπονται σε αντικείμενα `Nette\Utils\DateTime`. Εάν θέλετε οι χρονικές τιμές να μετατρέπονται σε αμετάβλητα (immutable) αντικείμενα `DateTimeImmutable`, ορίστε την επιλογή `newDateTime: true` στη [διαμόρφωση|configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -Στην περίπτωση της MySQL, ο τύπος δεδομένων `TIME` μετατρέπεται σε αντικείμενα `DateInterval`. - - -Boolean Τιμές -------------- - -Οι boolean τιμές μετατρέπονται αυτόματα σε `true` ή `false`. Στην MySQL, μετατρέπεται ο τύπος `TINYINT(1)` εάν ορίσουμε `convertBoolean: true` στη [διαμόρφωση|configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Αριθμητικές Τιμές ------------------ - -Οι αριθμητικές τιμές μετατρέπονται σε `int` ή `float` ανάλογα με τον τύπο της στήλης στη βάση δεδομένων: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Προσαρμοσμένη Κανονικοποίηση ----------------------------- - -Χρησιμοποιώντας τη μέθοδο `setRowNormalizer(?callable $normalizer)`, μπορείτε να ορίσετε μια προσαρμοσμένη συνάρτηση για τη μετατροπή των γραμμών από τη βάση δεδομένων. Αυτό είναι χρήσιμο, για παράδειγμα, για την αυτόματη μετατροπή τύπων δεδομένων. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // εδώ γίνεται η μετατροπή τύπων - return $row; -}); -``` diff --git a/database/el/reflection.texy b/database/el/reflection.texy deleted file mode 100644 index 909276b244..0000000000 --- a/database/el/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Αντανάκλαση Δομής -***************** - -.{data-version:3.2.1} -Το Nette Database παρέχει εργαλεία για την ενδοσκόπηση (introspection) της δομής της βάσης δεδομένων χρησιμοποιώντας την κλάση [api:Nette\Database\Structure]. Αυτή επιτρέπει τη λήψη πληροφοριών σχετικά με πίνακες, στήλες, ευρετήρια (indexes) και ξένα κλειδιά (foreign keys). Μπορείτε να χρησιμοποιήσετε την αντανάκλαση (reflection) για τη δημιουργία σχημάτων (schemas), τη δημιουργία ευέλικτων εφαρμογών που λειτουργούν με τη βάση δεδομένων ή γενικών εργαλείων βάσης δεδομένων. - -Λαμβάνουμε το αντικείμενο αντανάκλασης από την παρουσία της σύνδεσης με τη βάση δεδομένων: - -```php -$reflection = $database->getReflection(); -``` - - -Λήψη Πινάκων ------------- - -Η ιδιότητα readonly `$reflection->tables` περιέχει έναν συσχετιστικό πίνακα όλων των πινάκων στη βάση δεδομένων: - -```php -// Εμφάνιση ονομάτων όλων των πινάκων -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Υπάρχουν δύο ακόμη διαθέσιμες μέθοδοι: - -```php -// Έλεγχος ύπαρξης πίνακα -if ($reflection->hasTable('users')) { - echo "Ο πίνακας users υπάρχει"; // Table users exists -} - -// Επιστρέφει το αντικείμενο του πίνακα. αν δεν υπάρχει, προκαλεί εξαίρεση -$table = $reflection->getTable('users'); -``` - - -Πληροφορίες για τον Πίνακα --------------------------- - -Ο πίνακας αντιπροσωπεύεται από το αντικείμενο [Table|api:Nette\Database\Reflection\Table], το οποίο παρέχει τις ακόλουθες ιδιότητες readonly: - -- `$name: string` – όνομα του πίνακα -- `$view: bool` – εάν πρόκειται για προβολή (view) -- `$fullName: ?string` – πλήρες όνομα του πίνακα συμπεριλαμβανομένου του σχήματος (εάν υπάρχει) -- `$columns: array<string, Column>` – συσχετιστικός πίνακας στηλών του πίνακα -- `$indexes: Index[]` – πίνακας ευρετηρίων του πίνακα -- `$primaryKey: ?Index` – πρωτεύον κλειδί του πίνακα ή null -- `$foreignKeys: ForeignKey[]` – πίνακας ξένων κλειδιών του πίνακα - - -Στήλες ------- - -Η ιδιότητα `columns` του πίνακα παρέχει έναν συσχετιστικό πίνακα στηλών, όπου το κλειδί είναι το όνομα της στήλης και η τιμή είναι μια παρουσία [Column|api:Nette\Database\Reflection\Column] με τις ακόλουθες ιδιότητες: - -- `$name: string` – όνομα της στήλης -- `$table: ?Table` – αναφορά στον πίνακα της στήλης -- `$nativeType: string` – εγγενής τύπος δεδομένων της βάσης δεδομένων -- `$size: ?int` – μέγεθος/μήκος του τύπου -- `$nullable: bool` – εάν η στήλη μπορεί να περιέχει NULL -- `$default: mixed` – προεπιλεγμένη τιμή της στήλης -- `$autoIncrement: bool` – εάν η στήλη είναι auto-increment -- `$primary: bool` – εάν αποτελεί μέρος του πρωτεύοντος κλειδιού -- `$vendor: array` – πρόσθετα μεταδεδομένα ειδικά για το συγκεκριμένο σύστημα βάσης δεδομένων - -```php -foreach ($table->columns as $name => $column) { - echo "Στήλη: $name\n"; // Column: - echo "Τύπος: {$column->nativeType}\n"; // Type: - echo "Nullable: " . ($column->nullable ? 'Ναι' : 'Όχι') . "\n"; // Nullable: Yes / No -} -``` - - -Ευρετήρια ---------- - -Η ιδιότητα `indexes` του πίνακα παρέχει έναν πίνακα ευρετηρίων, όπου κάθε ευρετήριο είναι μια παρουσία [Index|api:Nette\Database\Reflection\Index] με τις ακόλουθες ιδιότητες: - -- `$columns: Column[]` – πίνακας στηλών που αποτελούν το ευρετήριο -- `$unique: bool` – εάν το ευρετήριο είναι μοναδικό -- `$primary: bool` – εάν πρόκειται για πρωτεύον κλειδί -- `$name: ?string` – όνομα του ευρετηρίου - -Το πρωτεύον κλειδί του πίνακα μπορεί να ληφθεί χρησιμοποιώντας την ιδιότητα `primaryKey`, η οποία επιστρέφει είτε ένα αντικείμενο `Index`, είτε `null` στην περίπτωση που ο πίνακας δεν έχει πρωτεύον κλειδί. - -```php -// Εμφάνιση ευρετηρίων -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Ευρετήριο" . ($index->name ? " {$index->name}" : '') . ":\n"; // Index - echo " Στήλες: $columns\n"; // Columns: - echo " Μοναδικό: " . ($index->unique ? 'Ναι' : 'Όχι') . "\n"; // Unique: Yes / No -} - -// Εμφάνιση πρωτεύοντος κλειδιού -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Πρωτεύον κλειδί: $columns\n"; // Primary key: -} -``` - - -Ξένα κλειδιά ------------- - -Η ιδιότητα `foreignKeys` του πίνακα παρέχει έναν πίνακα ξένων κλειδιών, όπου κάθε ξένο κλειδί είναι μια παρουσία [ForeignKey|api:Nette\Database\Reflection\ForeignKey] με τις ακόλουθες ιδιότητες: - -- `$foreignTable: Table` – ο πίνακας στον οποίο γίνεται αναφορά -- `$localColumns: Column[]` – πίνακας τοπικών στηλών -- `$foreignColumns: Column[]` – πίνακας στηλών στις οποίες γίνεται αναφορά -- `$name: ?string` – όνομα του ξένου κλειδιού - -```php -// Εμφάνιση ξένων κλειδιών -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/el/security.texy b/database/el/security.texy deleted file mode 100644 index 8fda175c50..0000000000 --- a/database/el/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Κίνδυνοι Ασφαλείας -****************** - -<div class=perex> - -Η βάση δεδομένων συχνά περιέχει ευαίσθητα δεδομένα και επιτρέπει την εκτέλεση επικίνδυνων λειτουργιών. Για την ασφαλή εργασία με το Nette Database είναι κρίσιμο: - -- Να κατανοήσετε τη διαφορά μεταξύ ασφαλούς και μη ασφαλούς API -- Να χρησιμοποιείτε παραμετροποιημένα ερωτήματα -- Να επικυρώνετε σωστά τα δεδομένα εισόδου - -</div> - - -Τι είναι το SQL Injection; -========================== - -Το SQL injection είναι ο σοβαρότερος κίνδυνος ασφαλείας κατά την εργασία με τη βάση δεδομένων. Προκύπτει όταν η μη επεξεργασμένη είσοδος από τον χρήστη γίνεται μέρος ενός ερωτήματος SQL. Ο εισβολέας μπορεί να εισάγει δικές του εντολές SQL και έτσι: -- Να αποκτήσει μη εξουσιοδοτημένη πρόσβαση σε δεδομένα -- Να τροποποιήσει ή να διαγράψει δεδομένα στη βάση δεδομένων -- Να παρακάμψει τον έλεγχο ταυτότητας - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ευάλωτος σε SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Ο εισβολέας μπορεί να εισάγει για παράδειγμα την τιμή: ' OR '1'='1 -// Το τελικό ερώτημα θα είναι: SELECT * FROM users WHERE name = '' OR '1'='1' -// Το οποίο επιστρέφει όλους τους χρήστες -``` - -Το ίδιο ισχύει και για το Database Explorer: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ευάλωτος σε SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Παραμετροποιημένα Ερωτήματα -=========================== - -Η βασική άμυνα κατά του SQL injection είναι τα παραμετροποιημένα ερωτήματα. Το Nette Database προσφέρει διάφορους τρόπους χρήσης τους. - -Ο απλούστερος τρόπος είναι η χρήση **placeholders ερωτηματικών (?)**: - -```php -// ✅ Ασφαλές παραμετροποιημένο ερώτημα -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Ασφαλής συνθήκη στο Explorer -$table->where('name = ?', $name); -``` - -Αυτό ισχύει για όλες τις άλλες μεθόδους στο [Database Explorer|explorer], που επιτρέπουν την εισαγωγή εκφράσεων με placeholders ερωτηματικά και παραμέτρους. - -Για εντολές INSERT, UPDATE ή τη ρήτρα WHERE, μπορούμε να περάσουμε τις τιμές σε έναν πίνακα: - -```php -// ✅ Ασφαλές INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Ασφαλές INSERT στο Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Επικύρωση Τιμών Παραμέτρων -========================== - -Τα παραμετροποιημένα ερωτήματα είναι ο θεμελιώδης λίθος της ασφαλούς εργασίας με τη βάση δεδομένων. Ωστόσο, οι τιμές που εισάγουμε σε αυτά πρέπει να περάσουν από διάφορα επίπεδα ελέγχου: - - -Έλεγχος Τύπου -------------- - -**Το πιο σημαντικό είναι να διασφαλιστεί ο σωστός τύπος δεδομένων των παραμέτρων** - αυτό είναι απαραίτητη προϋπόθεση για την ασφαλή χρήση του Nette Database. Η βάση δεδομένων υποθέτει ότι όλα τα δεδομένα εισόδου έχουν τον σωστό τύπο δεδομένων που αντιστοιχεί στη συγκεκριμένη στήλη. - -Για παράδειγμα, εάν το `$name` στα προηγούμενα παραδείγματα ήταν απροσδόκητα ένας πίνακας αντί για μια συμβολοσειρά, το Nette Database θα προσπαθούσε να εισάγει όλα τα στοιχεία του στο ερώτημα SQL, οδηγώντας σε σφάλμα. Επομένως, **ποτέ μην χρησιμοποιείτε** μη επικυρωμένα δεδομένα από `$_GET`, `$_POST` ή `$_COOKIE` απευθείας σε ερωτήματα βάσης δεδομένων. - - -Έλεγχος Μορφής --------------- - -Στο δεύτερο επίπεδο, ελέγχουμε τη μορφή των δεδομένων - για παράδειγμα, εάν οι συμβολοσειρές είναι σε κωδικοποίηση UTF-8 και το μήκος τους αντιστοιχεί στον ορισμό της στήλης, ή εάν οι αριθμητικές τιμές βρίσκονται εντός του επιτρεπόμενου εύρους για τον συγκεκριμένο τύπο δεδομένων της στήλης. - -Σε αυτό το επίπεδο επικύρωσης, μπορούμε εν μέρει να βασιστούμε και στην ίδια τη βάση δεδομένων - πολλές βάσεις δεδομένων απορρίπτουν μη έγκυρα δεδομένα. Ωστόσο, η συμπεριφορά μπορεί να διαφέρει, κάποιες μπορεί να περικόψουν σιωπηλά μακριές συμβολοσειρές ή να κόψουν αριθμούς εκτός εύρους. - - -Έλεγχος τομέα -------------- - -Το τρίτο επίπεδο αντιπροσωπεύουν οι λογικοί έλεγχοι που είναι ειδικοί για την εφαρμογή σας. Για παράδειγμα, η επαλήθευση ότι οι τιμές από τα select boxes αντιστοιχούν στις προσφερόμενες επιλογές, ότι οι αριθμοί βρίσκονται στο αναμενόμενο εύρος (π.χ. ηλικία 0-150 ετών) ή ότι οι αμοιβαίες εξαρτήσεις μεταξύ των τιμών έχουν νόημα. - - -Συνιστώμενοι Τρόποι Επικύρωσης ------------------------------- - -- Χρησιμοποιήστε [Nette Forms|forms:], που εξασφαλίζουν αυτόματα τη σωστή επικύρωση όλων των εισόδων -- Χρησιμοποιήστε [Presenters|application:] και δηλώστε τους τύπους δεδομένων για τις παραμέτρους στις μεθόδους `action*()` και `render*()` -- Ή υλοποιήστε το δικό σας επίπεδο επικύρωσης χρησιμοποιώντας τυπικά εργαλεία PHP όπως το `filter_var()` - - -Ασφαλής Εργασία με Στήλες -========================= - -Στην προηγούμενη ενότητα, δείξαμε πώς να επικυρώνουμε σωστά τις τιμές των παραμέτρων. Ωστόσο, κατά τη χρήση πινάκων σε ερωτήματα SQL, πρέπει να δώσουμε την ίδια προσοχή και στα κλειδιά τους. - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - τα κλειδιά στον πίνακα δεν έχουν υποστεί επεξεργασία -$database->query('INSERT INTO users', $_POST); -``` - -Στις εντολές INSERT και UPDATE, αυτό αποτελεί κρίσιμο σφάλμα ασφαλείας - ο εισβολέας μπορεί να εισάγει ή να αλλάξει οποιαδήποτε στήλη στη βάση δεδομένων. Θα μπορούσε, για παράδειγμα, να ορίσει `is_admin = 1` ή να εισάγει αυθαίρετα δεδομένα σε ευαίσθητες στήλες (η λεγόμενη Mass Assignment Vulnerability). - -Στις συνθήκες WHERE, είναι ακόμη πιο επικίνδυνο, επειδή μπορεί να περιέχουν τελεστές: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - τα κλειδιά στον πίνακα δεν έχουν υποστεί επεξεργασία -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// εκτελεί το ερώτημα WHERE (`salary` > 100000) -``` - -Ο εισβολέας μπορεί να χρησιμοποιήσει αυτή την προσέγγιση για να ανακαλύψει συστηματικά τους μισθούς των υπαλλήλων. Μπορεί να ξεκινήσει, για παράδειγμα, με ένα ερώτημα για μισθούς πάνω από 100.000, στη συνέχεια κάτω από 50.000, και με σταδιακή στένωση του εύρους, μπορεί να αποκαλύψει τους κατά προσέγγιση μισθούς όλων των υπαλλήλων. Αυτός ο τύπος επίθεσης ονομάζεται SQL enumeration. - -Οι μέθοδοι `where()` και `whereOr()` είναι ακόμη [πολύ πιο ευέλικτες |explorer#where] και υποστηρίζουν εκφράσεις SQL στα κλειδιά και τις τιμές, συμπεριλαμβανομένων τελεστών και συναρτήσεων. Αυτό δίνει στον εισβολέα τη δυνατότητα να εκτελέσει SQL injection: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ο εισβολέας μπορεί να εισάγει δικό του SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// εκτελεί το ερώτημα WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Αυτή η επίθεση τερματίζει την αρχική συνθήκη χρησιμοποιώντας `0)`, προσαρτά το δικό της `SELECT` χρησιμοποιώντας `UNION` για να αποκτήσει ευαίσθητα δεδομένα από τον πίνακα `users` και κλείνει το συντακτικά σωστό ερώτημα χρησιμοποιώντας `WHERE (1)`. - - -Whitelist Στηλών ----------------- - -Για την ασφαλή εργασία με ονόματα στηλών, χρειαζόμαστε έναν μηχανισμό που να διασφαλίζει ότι ο χρήστης μπορεί να εργαστεί μόνο με τις επιτρεπόμενες στήλες και δεν μπορεί να προσθέσει δικές του. Θα μπορούσαμε να προσπαθήσουμε να ανιχνεύσουμε και να μπλοκάρουμε επικίνδυνα ονόματα στηλών (blacklist), αλλά αυτή η προσέγγιση είναι αναξιόπιστη - ο εισβολέας μπορεί πάντα να βρει έναν νέο τρόπο να γράψει ένα επικίνδυνο όνομα στήλης που δεν είχαμε προβλέψει. - -Επομένως, είναι πολύ πιο ασφαλές να αντιστρέψουμε τη λογική και να ορίσουμε μια ρητή λίστα επιτρεπόμενων στηλών (whitelist): - -```php -// Στήλες που μπορεί να επεξεργαστεί ο χρήστης -$allowedColumns = ['name', 'email', 'active']; - -// Φιλτράρουμε τα δεδομένα εισόδου για να κρατήσουμε μόνο τα επιτρεπόμενα κλειδιά -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Τώρα μπορούμε να τα χρησιμοποιήσουμε με ασφάλεια σε ερωτήματα, όπως: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Δυναμικά Αναγνωριστικά -====================== - -Για δυναμικά ονόματα πινάκων και στηλών, χρησιμοποιήστε το placeholder `?name`. Αυτό εξασφαλίζει τη σωστή διαφυγή (escaping) των αναγνωριστικών σύμφωνα με τη σύνταξη της συγκεκριμένης βάσης δεδομένων (π.χ. χρησιμοποιώντας ανάποδα εισαγωγικά `` ` `` στην MySQL): - -```php -// ✅ Ασφαλής χρήση αξιόπιστων αναγνωριστικών -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Αποτέλεσμα στην MySQL: SELECT `name` FROM `users` -``` - -Σημαντικό: χρησιμοποιήστε το σύμβολο `?name` μόνο για αξιόπιστες τιμές που ορίζονται στον κώδικα της εφαρμογής. Για τιμές από τον χρήστη, χρησιμοποιήστε ξανά τη [whitelist |#Whitelist Στηλών]. Διαφορετικά, εκτίθεστε σε κινδύνους ασφαλείας: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟ - ποτέ μην χρησιμοποιείτε είσοδο από τον χρήστη -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/el/sql-way.texy b/database/el/sql-way.texy deleted file mode 100644 index 1cfab7b050..0000000000 --- a/database/el/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Πρόσβαση SQL -************ - -.[perex] -Η Nette Database προσφέρει δύο τρόπους: μπορείτε να γράψετε μόνοι σας ερωτήματα SQL (πρόσβαση SQL) ή να τα αφήσετε να δημιουργηθούν αυτόματα (βλ. [Explorer |explorer]). Η πρόσβαση SQL σάς δίνει πλήρη έλεγχο των ερωτημάτων, εξασφαλίζοντας ταυτόχρονα την ασφαλή σύνταξή τους. - -.[note] -Λεπτομέρειες σχετικά με τη σύνδεση και τη διαμόρφωση της βάσης δεδομένων θα βρείτε στο κεφάλαιο [Σύνδεση και διαμόρφωση |guide#Σύνδεση και Διαμόρφωση]. - - -Βασικά ερωτήματα -================ - -Για την υποβολή ερωτημάτων στη βάση δεδομένων, χρησιμοποιείται η μέθοδος `query()`. Αυτή επιστρέφει ένα αντικείμενο [ResultSet |api:Nette\Database\ResultSet], το οποίο αντιπροσωπεύει το αποτέλεσμα του ερωτήματος. Σε περίπτωση αποτυχίας, η μέθοδος [προκαλεί εξαίρεση |exceptions]. Μπορούμε να διατρέξουμε το αποτέλεσμα του ερωτήματος χρησιμοποιώντας έναν βρόχο `foreach` ή να χρησιμοποιήσουμε κάποια από τις [βοηθητικές συναρτήσεις |#Λήψη δεδομένων]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Για την ασφαλή εισαγωγή τιμών σε ερωτήματα SQL, χρησιμοποιούμε παραμετροποιημένα ερωτήματα. Η Nette Database τα καθιστά εξαιρετικά απλά - αρκεί να προσθέσετε ένα κόμμα και την τιμή μετά το ερώτημα SQL: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Με περισσότερες παραμέτρους, έχετε δύο επιλογές σύνταξης. Μπορείτε είτε να "διανθίσετε" το ερώτημα SQL με παραμέτρους: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Ή να γράψετε πρώτα ολόκληρο το ερώτημα SQL και στη συνέχεια να επισυνάψετε όλες τις παραμέτρους: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Προστασία από SQL injection -=========================== - -Γιατί είναι σημαντικό να χρησιμοποιείτε παραμετροποιημένα ερωτήματα; Επειδή σας προστατεύουν από την επίθεση που ονομάζεται SQL injection, κατά την οποία ο εισβολέας θα μπορούσε να εισάγει δικές του εντολές SQL και έτσι να αποκτήσει ή να καταστρέψει δεδομένα στη βάση δεδομένων. - -.[warning] -**Ποτέ μην εισάγετε μεταβλητές απευθείας στο ερώτημα SQL!** Πάντα να χρησιμοποιείτε παραμετροποιημένα ερωτήματα, τα οποία σας προστατεύουν από το SQL injection. - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ευάλωτος σε SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Ασφαλές παραμετροποιημένο ερώτημα -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Ενημερωθείτε για τους [πιθανούς κινδύνους ασφαλείας |security]. - - -Τεχνικές ερωτημάτων -=================== - - -Συνθήκες WHERE --------------- - -Μπορείτε να γράψετε τις συνθήκες WHERE ως έναν συσχετιστικό πίνακα (associative array), όπου τα κλειδιά είναι τα ονόματα των στηλών και οι τιμές είναι τα δεδομένα για σύγκριση. Η Nette Database επιλέγει αυτόματα τον καταλληλότερο τελεστή SQL ανάλογα με τον τύπο της τιμής. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Στο κλειδί, μπορείτε επίσης να καθορίσετε ρητά τον τελεστή για σύγκριση: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // χρησιμοποιεί τον τελεστή > - 'name LIKE' => '%John%', // χρησιμοποιεί τον τελεστή LIKE - 'email NOT LIKE' => '%example.com%', // χρησιμοποιεί τον τελεστή NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Το Nette χειρίζεται αυτόματα ειδικές περιπτώσεις όπως τιμές `null` ή πίνακες. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // χρησιμοποιεί τον τελεστή = - 'category_id' => [1, 2, 3], // χρησιμοποιεί το IN - 'description' => null, // χρησιμοποιεί το IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Για αρνητικές συνθήκες, χρησιμοποιήστε τον τελεστή `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // χρησιμοποιεί τον τελεστή <> - 'category_id NOT' => [1, 2, 3], // χρησιμοποιεί το NOT IN - 'description NOT' => null, // χρησιμοποιεί το IS NOT NULL - 'id' => [], // παραλείπεται -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Για τη σύνδεση συνθηκών, χρησιμοποιείται ο τελεστής `AND`. Αυτό μπορεί να αλλάξει χρησιμοποιώντας το [placeholder ?or |#Hints για τη σύνταξη SQL]. - - -Κανόνες ORDER BY ----------------- - -Η ταξινόμηση `ORDER BY` μπορεί να γραφτεί χρησιμοποιώντας έναν πίνακα. Στα κλειδιά, αναφέρουμε τις στήλες και η τιμή θα είναι μια boolean τιμή που καθορίζει εάν θα ταξινομηθεί αύξουσα: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // αύξουσα - 'name' => false, // φθίνουσα -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Εισαγωγή δεδομένων (INSERT) ---------------------------- - -Για την εισαγωγή εγγραφών, χρησιμοποιείται η εντολή SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Η μέθοδος `getInsertId()` επιστρέφει το ID της τελευταίας εισαχθείσας γραμμής. Σε ορισμένες βάσεις δεδομένων (π.χ. PostgreSQL), είναι απαραίτητο να καθορίσετε ως παράμετρο το όνομα της ακολουθίας (sequence) από την οποία θα δημιουργηθεί το ID χρησιμοποιώντας `$database->getInsertId($sequenceId)`. - -Ως παραμέτρους μπορούμε επίσης να περάσουμε [#Ειδικές τιμές] όπως αρχεία, αντικείμενα DateTime ή τύπους enum. - -Εισαγωγή πολλαπλών εγγραφών ταυτόχρονα: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Η πολλαπλή INSERT είναι πολύ ταχύτερη, επειδή εκτελείται ένα μόνο ερώτημα βάσης δεδομένων, αντί για πολλά μεμονωμένα. - -**Προειδοποίηση ασφαλείας:** Ποτέ μην χρησιμοποιείτε μη επικυρωμένα δεδομένα ως `$values`. Ενημερωθείτε για τους [πιθανούς κινδύνους |security#Ασφαλής Εργασία με Στήλες]. - - -Ενημέρωση δεδομένων (UPDATE) ----------------------------- - -Για την ενημέρωση εγγραφών, χρησιμοποιείται η εντολή SQL `UPDATE`. - -```php -// Ενημέρωση μίας εγγραφής -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Ο αριθμός των επηρεασμένων γραμμών επιστρέφεται από το `$result->getRowCount()`. - -Για το UPDATE, μπορούμε να χρησιμοποιήσουμε τους τελεστές `+=` και `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // αύξηση του login_count -], 1); -``` - -Παράδειγμα εισαγωγής ή τροποποίησης εγγραφής, εάν υπάρχει ήδη. Χρησιμοποιούμε την τεχνική `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Παρατηρήστε ότι η Nette Database αναγνωρίζει σε ποιο πλαίσιο της εντολής SQL εισάγουμε την παράμετρο με τον πίνακα και ανάλογα συνθέτει τον κώδικα SQL. Έτσι, από τον πρώτο πίνακα συνέθεσε `(id, name, year) VALUES (123, 'Jim', 1978)`, ενώ τον δεύτερο τον μετέτρεψε στη μορφή `name = 'Jim', year = 1978`. Αυτό το εξετάζουμε λεπτομερέστερα στην ενότητα [#Hints για τη σύνταξη SQL]. - - -Διαγραφή δεδομένων (DELETE) ---------------------------- - -Για τη διαγραφή εγγραφών, χρησιμοποιείται η εντολή SQL `DELETE`. Παράδειγμα με λήψη του αριθμού των διαγραμμένων γραμμών: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Hints για τη σύνταξη SQL ------------------------- - -Ένα hint είναι ένα ειδικό placeholder στο ερώτημα SQL που λέει πώς πρέπει να μεταγραφεί η τιμή της παραμέτρου σε έκφραση SQL: - -| Hint | Περιγραφή | Χρησιμοποιείται αυτόματα -|-----------|-------------------------------------------------|----------------------------- -| `?name` | χρησιμοποιείται για την εισαγωγή ονόματος πίνακα ή στήλης | - -| `?values` | δημιουργεί `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | δημιουργεί ανάθεση `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | συνδέει συνθήκες στον πίνακα με τον τελεστή `AND` | `WHERE ?`, `HAVING ?` -| `?or` | συνδέει συνθήκες στον πίνακα με τον τελεστή `OR` | - -| `?order` | δημιουργεί τη ρήτρα `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Για τη δυναμική εισαγωγή ονομάτων πινάκων και στηλών στο ερώτημα, χρησιμοποιείται το placeholder `?name`. Η Nette Database φροντίζει για τη σωστή επεξεργασία των αναγνωριστικών σύμφωνα με τις συμβάσεις της συγκεκριμένης βάσης δεδομένων (π.χ. κλείσιμο σε ανάποδα εισαγωγικά `` ` `` στην MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (στην MySQL) -``` - -**Προειδοποίηση:** χρησιμοποιήστε το σύμβολο `?name` μόνο για ονόματα πινάκων και στηλών από επικυρωμένες εισόδους, διαφορετικά εκτίθεστε σε [κίνδυνο ασφαλείας |security#Δυναμικά Αναγνωριστικά]. - -Τα υπόλοιπα hints συνήθως δεν χρειάζεται να αναφέρονται, καθώς το Nette χρησιμοποιεί έξυπνη αυτόματη ανίχνευση κατά τη σύνθεση του ερωτήματος SQL (βλ. τρίτη στήλη του πίνακα). Αλλά μπορείτε να το χρησιμοποιήσετε, για παράδειγμα, σε μια κατάσταση όπου θέλετε να συνδέσετε συνθήκες χρησιμοποιώντας `OR` αντί για `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Ειδικές τιμές -------------- - -Εκτός από τους συνήθεις σκαλωτούς τύπους (string, int, bool), μπορείτε να περάσετε και ειδικές τιμές ως παραμέτρους: - -- αρχεία: `fopen('image.gif', 'r')` εισάγει το δυαδικό περιεχόμενο του αρχείου -- ημερομηνία και ώρα: τα αντικείμενα `DateTime` μετατρέπονται στη μορφή της βάσης δεδομένων -- τύποι enum: οι παρουσίες `enum` μετατρέπονται στην τιμή τους -- SQL literals: δημιουργημένα με `Connection::literal('NOW()')` εισάγονται απευθείας στο ερώτημα - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Σε βάσεις δεδομένων που δεν έχουν εγγενή υποστήριξη για τον τύπο δεδομένων `datetime` (όπως SQLite και Oracle), το `DateTime` μετατρέπεται στην τιμή που καθορίζεται στη [διαμόρφωση της βάσης δεδομένων |configuration] με την επιλογή `formatDateTime` (η προεπιλεγμένη τιμή είναι `U` - unix timestamp). - - -SQL Literals ------------- - -Σε ορισμένες περιπτώσεις, πρέπει να αναφέρετε απευθείας κώδικα SQL ως τιμή, ο οποίος όμως δεν πρέπει να θεωρηθεί ως συμβολοσειρά και να υποστεί escaping. Για αυτό χρησιμεύουν τα αντικείμενα της κλάσης `Nette\Database\SqlLiteral`. Τα δημιουργεί η μέθοδος `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Ή εναλλακτικά: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Τα SQL literals μπορούν να περιέχουν παραμέτρους: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Χάρη σε αυτό, μπορούμε να δημιουργήσουμε ενδιαφέροντες συνδυασμούς: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Λήψη δεδομένων -============== - - -Συντομεύσεις για ερωτήματα SELECT ---------------------------------- - -Για την απλοποίηση της ανάκτησης δεδομένων, το `Connection` προσφέρει αρκετές συντομεύσεις που συνδυάζουν την κλήση `query()` με την ακόλουθη `fetch*()`. Αυτές οι μέθοδοι δέχονται τις ίδιες παραμέτρους με το `query()`, δηλαδή το ερώτημα SQL και προαιρετικές παραμέτρους. Μια πλήρης περιγραφή των μεθόδων `fetch*()` βρίσκεται [παρακάτω |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Εκτελεί το ερώτημα και επιστρέφει την πρώτη γραμμή ως αντικείμενο `Row` -| `fetchAll($sql, ...$params): array` | Εκτελεί το ερώτημα και επιστρέφει όλες τις γραμμές ως πίνακα αντικειμένων `Row` -| `fetchPairs($sql, ...$params): array` | Εκτελεί το ερώτημα και επιστρέφει έναν συσχετιστικό πίνακα, όπου η πρώτη στήλη αντιπροσωπεύει το κλειδί και η δεύτερη την τιμή -| `fetchField($sql, ...$params): mixed` | Εκτελεί το ερώτημα και επιστρέφει την τιμή του πρώτου πεδίου από την πρώτη γραμμή -| `fetchList($sql, ...$params): ?array` | Εκτελεί το ερώτημα και επιστρέφει την πρώτη γραμμή ως αριθμημένο πίνακα - -Παράδειγμα: - -```php -// fetchField() - επιστρέφει την τιμή του πρώτου κελιού -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - επανάληψη μέσω γραμμών ----------------------------------- - -Μετά την εκτέλεση του ερωτήματος, επιστρέφεται ένα αντικείμενο [ResultSet |api:Nette\Database\ResultSet], το οποίο επιτρέπει την περιήγηση στα αποτελέσματα με διάφορους τρόπους. Ο ευκολότερος τρόπος για να εκτελέσετε ένα ερώτημα και να λάβετε τις γραμμές είναι με επανάληψη σε έναν βρόχο `foreach`. Αυτός ο τρόπος είναι ο πιο αποδοτικός από πλευράς μνήμης, καθώς επιστρέφει τα δεδομένα σταδιακά και δεν τα αποθηκεύει όλα στη μνήμη ταυτόχρονα. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -Το `ResultSet` μπορεί να επαναληφθεί μόνο μία φορά. Εάν χρειάζεται να επαναλάβετε πολλές φορές, πρέπει πρώτα να φορτώσετε τα δεδομένα σε έναν πίνακα, για παράδειγμα χρησιμοποιώντας τη μέθοδο `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Επιστρέφει μια γραμμή ως αντικείμενο `Row`. Εάν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. Μετακινεί τον εσωτερικό δείκτη στην επόμενη γραμμή. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // φορτώνει την πρώτη γραμμή -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Επιστρέφει όλες τις υπόλοιπες γραμμές από το `ResultSet` ως πίνακα αντικειμένων `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // φορτώνει όλες τις γραμμές -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Επιστρέφει τα αποτελέσματα ως συσχετιστικό πίνακα. Το πρώτο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως κλειδί στον πίνακα, το δεύτερο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως τιμή: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Εάν αναφέρουμε μόνο την πρώτη παράμετρο, η τιμή θα είναι ολόκληρη η γραμμή, δηλαδή το αντικείμενο `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Σε περίπτωση διπλότυπων κλειδιών, χρησιμοποιείται η τιμή από την τελευταία γραμμή. Κατά τη χρήση `null` ως κλειδί, ο πίνακας θα αριθμηθεί αριθμητικά από το μηδέν (τότε δεν προκύπτουν συγκρούσεις): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Εναλλακτικά, μπορείτε να δώσετε ως παράμετρο ένα callback, το οποίο θα επιστρέφει για κάθε γραμμή είτε την ίδια την τιμή, είτε ένα ζεύγος κλειδιού-τιμής. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Το callback μπορεί επίσης να επιστρέψει έναν πίνακα με ένα ζεύγος κλειδιού & τιμής: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Επιστρέφει την τιμή του πρώτου πεδίου από την τρέχουσα γραμμή. Εάν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. Μετακινεί τον εσωτερικό δείκτη στην επόμενη γραμμή. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // φορτώνει το όνομα από την πρώτη γραμμή -``` - - -fetchList(): ?array .[method] ------------------------------ - -Επιστρέφει μια γραμμή ως αριθμημένο πίνακα. Εάν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. Μετακινεί τον εσωτερικό δείκτη στην επόμενη γραμμή. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Επιστρέφει τον αριθμό των επηρεασμένων γραμμών από το τελευταίο ερώτημα `UPDATE` ή `DELETE`. Για το `SELECT`, είναι ο αριθμός των επιστρεφόμενων γραμμών, αλλά αυτός μπορεί να μην είναι γνωστός - σε αυτή την περίπτωση, η μέθοδος επιστρέφει `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Επιστρέφει τον αριθμό των στηλών στο `ResultSet`. - - -Πληροφορίες για τα ερωτήματα -============================ - -Για σκοπούς εντοπισμού σφαλμάτων, μπορούμε να λάβουμε πληροφορίες σχετικά με το τελευταίο εκτελεσμένο ερώτημα: - -```php -echo $database->getLastQueryString(); // εκτυπώνει το ερώτημα SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // εκτυπώνει το ερώτημα SQL -echo $result->getTime(); // εκτυπώνει τον χρόνο εκτέλεσης σε δευτερόλεπτα -``` - -Για την εμφάνιση του αποτελέσματος ως πίνακα HTML, μπορείτε να χρησιμοποιήσετε: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -Το ResultSet προσφέρει πληροφορίες σχετικά με τους τύπους των στηλών: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column είναι τύπου $type->type"; // π.χ. 'id είναι τύπου int' -} -``` - - -Καταγραφή ερωτημάτων --------------------- - -Μπορούμε να υλοποιήσουμε τη δική μας καταγραφή ερωτημάτων. Το συμβάν `onQuery` είναι ένας πίνακας callbacks που καλούνται μετά από κάθε εκτελεσμένο ερώτημα: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/el/transactions.texy b/database/el/transactions.texy deleted file mode 100644 index ffdd9514af..0000000000 --- a/database/el/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Συναλλαγές (Transactions) -************************* - -.[perex] -Οι συναλλαγές εγγυώνται ότι είτε όλες οι λειτουργίες εντός της συναλλαγής θα εκτελεστούν, είτε καμία. Είναι χρήσιμες για τη διασφάλιση της συνέπειας των δεδομένων κατά τη διάρκεια πιο σύνθετων λειτουργιών. - -Ο απλούστερος τρόπος χρήσης συναλλαγών μοιάζει με αυτό: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Μπορείτε να γράψετε το ίδιο πράγμα πολύ πιο κομψά χρησιμοποιώντας τη μέθοδο `transaction()`. Δέχεται μια επανάκληση (callback) ως παράμετρο, την οποία εκτελεί σε μια συναλλαγή. Εάν η επανάκληση εκτελεστεί χωρίς εξαίρεση, η συναλλαγή επιβεβαιώνεται αυτόματα (commit). Εάν προκύψει εξαίρεση, η συναλλαγή ακυρώνεται (rollback) και η εξαίρεση διαδίδεται περαιτέρω. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Η μέθοδος `transaction()` μπορεί επίσης να επιστρέψει τιμές: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // επιστρέφει τον αριθμό των ενημερωμένων γραμμών -}); -``` diff --git a/database/en/@home.texy b/database/en/@home.texy index 7d7452d8d1..330104209f 100644 --- a/database/en/@home.texy +++ b/database/en/@home.texy @@ -1,5 +1,3 @@ - - Supported Databases =================== diff --git a/database/en/@left-menu.texy b/database/en/@left-menu.texy index f865f8027e..1ccf8834ff 100644 --- a/database/en/@left-menu.texy +++ b/database/en/@left-menu.texy @@ -6,7 +6,16 @@ Nette Database - [Transactions] - [Exceptions] - [Reflection] -- [Mapping] +- [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/configuration.texy b/database/en/configuration.texy index 8a11a1cbcb..003eba4b26 100644 --- a/database/en/configuration.texy +++ b/database/en/configuration.texy @@ -27,7 +27,7 @@ Other settings: ```neon database: # show the database panel in Tracy Bar? - debugger: ... # (bool) defaults to true + debugger: ... # (bool) defaults to on if Tracy is active # show query EXPLAIN in Tracy Bar? explain: ... # (bool) defaults to true diff --git a/database/en/exceptions.texy b/database/en/exceptions.texy index d3780dcacc..650eaabb55 100644 --- a/database/en/exceptions.texy +++ b/database/en/exceptions.texy @@ -9,12 +9,15 @@ Nette Database uses an exception hierarchy. The base class is `Nette\Database\Dr The `DriverException` class is extended by the following specialized exceptions: -- `ConnectionException` – indicates a failure to connect to the database server. -- `ConstraintViolationException` – the base class for database constraint violations, from which the following exceptions inherit: - - `ForeignKeyConstraintViolationException` – violation of a foreign key constraint. - - `NotNullConstraintViolationException` – violation of a NOT NULL constraint. - - `UniqueConstraintViolationException` – violation of a uniqueness constraint. - +- `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): diff --git a/database/en/explorer.texy b/database/en/explorer.texy index dc5e7ef7af..60002a1dc8 100644 --- a/database/en/explorer.texy +++ b/database/en/explorer.texy @@ -41,7 +41,7 @@ foreach ($books as $book) { 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. +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 @@ -56,7 +56,7 @@ The `Selection` class provides methods for filtering and sorting data selections | `order($columns, ...$params)` | Sets sorting with ORDER BY | | `select($columns, ...$params)` | Specifies which columns to fetch | | `limit($limit, $offset = null)` | Limits the number of rows (LIMIT) and optionally sets OFFSET | -| `page($page, $itemsPerPage, &$total = null)` | Sets pagination | +| `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 | @@ -98,7 +98,7 @@ $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: +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 @@ -263,7 +263,7 @@ Facilitates pagination of results. It accepts the page number (starting from 1) ```php $numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); echo "Total pages: $numOfPages"; ``` @@ -466,8 +466,8 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr): int .[method] --------------------------------- +sum(string $expr): mixed .[method] +---------------------------------- Returns the sum of values in the specified column or expression: @@ -522,7 +522,7 @@ Inserts new records into the table. Pass the new record as an associative array or iterable object (like `ArrayHash` used in [forms |forms:]), where keys correspond to column names in the table. -If the table has a defined primary key, the method returns an `ActiveRow` object, which is reloaded from the database to reflect any changes made at the database level (triggers, default column values, auto-increment column calculations). This ensures data consistency, and the object always contains the current data from the database. If it doesn't have a unique primary key, it returns the passed data as an array. +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([ @@ -658,25 +658,25 @@ This method deletes only one specific row in the database. For bulk deletion of 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. +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`) +- `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. +- **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`. +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: @@ -684,14 +684,14 @@ If we need to list books including their authors' names, we have two options. Ei 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: +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: +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'); @@ -706,7 +706,7 @@ foreach ($books as $book) { 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: +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); @@ -895,7 +895,7 @@ $books = $explorer->table('book') // 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. +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: diff --git a/database/en/guide.texy b/database/en/guide.texy index 545eb1b656..51562d638d 100644 --- a/database/en/guide.texy +++ b/database/en/guide.texy @@ -125,7 +125,7 @@ foreach ($authors as $author) { } ``` -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. +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. @@ -207,6 +207,8 @@ $database->onConnect[] = function($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 =============== diff --git a/database/en/reflection.texy b/database/en/reflection.texy index c8fdafd987..31662529e3 100644 --- a/database/en/reflection.texy +++ b/database/en/reflection.texy @@ -41,13 +41,14 @@ Table Information A table is represented by the [Table|api:Nette\Database\Reflection\Table] object, which provides the following readonly properties: -- `$name: string` – name of the table -- `$view: bool` – whether it is a view -- `$fullName: ?string` – full name of the table including schema (if exists) -- `$columns: array<string, Column>` – associative array of table columns -- `$indexes: Index[]` – array of table indexes -- `$primaryKey: ?Index` – primary key of the table or null -- `$foreignKeys: ForeignKey[]` – array of table foreign keys +- `$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 @@ -55,15 +56,16 @@ 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 +- `$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) { @@ -79,10 +81,10 @@ 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 +- `$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. @@ -108,10 +110,10 @@ 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 +- `$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 diff --git a/database/en/security.texy b/database/en/security.texy index 2632e630a1..ac9c42bcda 100644 --- a/database/en/security.texy +++ b/database/en/security.texy @@ -81,7 +81,7 @@ Parameterized queries are the cornerstone of secure database work. However, the 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. +**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. @@ -89,9 +89,9 @@ For example, if `$name` in the previous examples were unexpectedly an array inst 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. +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. +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 @@ -118,7 +118,7 @@ In the previous section, we showed how to properly validate parameter values. Ho $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). +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: @@ -146,7 +146,7 @@ This attack terminates the original condition using `0)`, appends its own `SELEC 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. +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): diff --git a/database/en/sql-way.texy b/database/en/sql-way.texy index f447354e3b..a87292d684 100644 --- a/database/en/sql-way.texy +++ b/database/en/sql-way.texy @@ -103,12 +103,12 @@ For negative conditions, use the `NOT` operator: ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // uses the <> operator + 'name NOT' => 'Laptop', // uses the != operator 'category_id NOT' => [1, 2, 3], // uses NOT IN 'description NOT' => null, // uses IS NOT NULL - 'id' => [], // skipped + 'id NOT' => [], // skipped ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// 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]. @@ -237,7 +237,7 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); **Warning:** Only use the `?name` placeholder for validated table and column names. Otherwise, you risk [security vulnerabilities |security#Dynamic Identifiers]. -Other hints usually do not need to be specified, as Nette uses smart autodetection when constructing the SQL query (see the third column of the table). But you can use it, for example, in a situation where you want to join conditions using `OR` instead of `AND`: +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', [ @@ -254,9 +254,9 @@ Special Values In addition to common scalar types (string, int, bool), you can also pass special values as parameters: - files: `fopen('image.gif', 'r')` inserts the binary content of the file -- date and time: `DateTime` and `DateTimeImmutable` objects are converted to the database format +- date and time: `DateTimeInterface` objects are converted to the database format - enum types: instances of `enum` are converted to their value -- SQL literals: created using `Connection::literal('NOW()')` are inserted directly into the query +- SQL literals: those created using `Connection::literal('NOW()')` are inserted directly into the query ```php $database->query('INSERT INTO articles ?', [ @@ -491,7 +491,7 @@ $result = $database->query('SELECT * FROM articles'); $types = $result->getColumnTypes(); foreach ($types as $column => $type) { - echo "$column is of type $type->type"; // e.g., 'id is of type int' + echo "$column is of type $type"; // e.g., 'id is of type int' } ``` diff --git a/database/en/transactions.texy b/database/en/transactions.texy index 36b455a663..bb404cead2 100644 --- a/database/en/transactions.texy +++ b/database/en/transactions.texy @@ -33,6 +33,8 @@ $database->transaction(function ($database) use ($id) { }); ``` +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 diff --git a/database/en/mapping.texy b/database/en/type-conversion.texy similarity index 100% rename from database/en/mapping.texy rename to database/en/type-conversion.texy diff --git a/database/en/upgrading.texy b/database/en/upgrading.texy index 7bf5fa7f43..700bbc41ba 100644 --- a/database/en/upgrading.texy +++ b/database/en/upgrading.texy @@ -2,8 +2,8 @@ Upgrading ********* -Migrating from 3.1 to 3.2 -========================= +Upgrading to Version 3.2 +======================== The minimum required PHP version is 8.1. @@ -11,4 +11,26 @@ The code has been carefully tuned for PHP 8.1. All new type hints for methods an - MySQL: zero date `0000-00-00` is returned as `null` - MySQL: decimal without decimal places is returned as int instead of float -- The `time` type is returned as a `DateTimeImmutable` object with the date set to `0001-01-01` instead of the current date +- 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` diff --git a/database/es/@home.texy b/database/es/@home.texy index 847f8e4d76..4b02660296 100644 --- a/database/es/@home.texy +++ b/database/es/@home.texy @@ -1,21 +1,18 @@ - - Bases de datos compatibles ========================== -Nette soporta las siguientes 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Í | - +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 radicalmente la obtención de datos de la base de datos sin necesidad de escribir consultas SQL. Realiza consultas eficientes y no transfiere datos innecesarios.}} +{{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 index 334a1f0ecd..cb91994186 100644 --- a/database/es/@left-menu.texy +++ b/database/es/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** -- [Introducción |guide] -- [Acceso SQL |sql way] -- [Explorer |Explorer] -- [Transacciones |transactions] -- [Excepciones |exceptions] -- [Reflexión |reflection] -- [Mapeo |mapping] -- [Configuración |configuration] +- [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 |en:upgrading] +- [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 index 1670b124ad..3798d9cda4 100644 --- a/database/es/@meta.texy +++ b/database/es/@meta.texy @@ -1 +1 @@ -{{sitename: Nette Documentación}} +{{sitename: Documentación de Nette}} diff --git a/database/es/configuration.texy b/database/es/configuration.texy index 17f31d5bf7..ab1b60004b 100644 --- a/database/es/configuration.texy +++ b/database/es/configuration.texy @@ -2,13 +2,13 @@ Configuración de la base de datos ********************************* .[perex] -Resumen de las opciones de configuración para Nette Database. +Resumen de las opciones de configuración de Nette Database. -Si no está utilizando todo el framework, sino sólo esta librería, lea [cómo cargar la configuración|bootstrap:]. +Si no usa el framework entero, sino solo esta biblioteca, lea [cómo cargar la configuración|bootstrap:]. -Una conexión ------------- +Una sola conexión +----------------- Configuración de una única conexión a la base de datos: @@ -20,48 +20,48 @@ database: password: ... ``` -Crea los servicios `Nette\Database\Connection` y `Nette\Database\Explorer`, que normalmente pasamos mediante [autowiring |dependency-injection:autowiring], o por referencia a [su nombre |#Servicios DI]. +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 base de datos en Tracy Bar? - debugger: ... # (bool) por defecto es true + # ¿mostrar el panel de la base de datos en la Tracy Bar? + debugger: ... # (bool) activado de forma predeterminada si Tracy está activa - # ¿mostrar EXPLAIN de las consultas en Tracy Bar? - explain: ... # (bool) por defecto es true + # ¿mostrar el EXPLAIN de las consultas en la Tracy Bar? + explain: ... # (bool) el valor predeterminado es true - # ¿habilitar autowiring para esta conexión? - autowired: ... # (bool) por defecto es true para la primera conexión + # ¿activar el autowiring para esta conexión? + autowired: ... # (bool) true de forma predeterminada para la primera conexión - # convenciones de tabla: discovered, static o nombre de clase - conventions: discovered # (string) por defecto es 'discovered' + # 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 sea necesario? - lazy: ... # (bool) por defecto es false + # ¿conectarse a la base de datos solo cuando haga falta? + lazy: ... # (bool) el valor predeterminado es false - # Clase PHP del controlador de base de datos + # 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) por defecto es 'utf8mb4' + charset: # (string) el valor predeterminado es 'utf8mb4' # solo MySQL: convierte TINYINT(1) a bool - convertBoolean: # (bool) por defecto es false + convertBoolean: # (bool) el valor predeterminado es false - # devuelve columnas de fecha como objetos inmutables (desde la versión 3.2.1) - newDateTime: # (bool) por defecto 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 almacenar la fecha - formatDateTime: # (string) por defecto es 'U' + # solo Oracle y SQLite: formato para guardar la fecha + formatDateTime: # (string) el valor predeterminado es 'U' ``` -En la clave `options`, puede especificar otras opciones que se encuentran en la [documentación de los controladores PDO |https://www.php.net/manual/en/pdo.drivers.php], como por ejemplo: +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: @@ -70,10 +70,10 @@ database: ``` -Múltiples conexiones --------------------- +Varias conexiones +----------------- -En la configuración, también podemos definir múltiples conexiones de base de datos dividiéndolas en secciones con nombre: +En la configuración podemos definir varias conexiones a bases de datos repartiéndolas en secciones con nombre: ```neon database: @@ -86,7 +86,7 @@ database: dsn: 'sqlite::memory:' ``` -El autowiring solo está habilitado para los servicios de la primera sección. Esto se puede cambiar usando `autowired: false` o `autowired: true`. +El autowiring está activado solo para los servicios de la primera sección. Se puede cambiar con `autowired: false` o `autowired: true`. Servicios DI @@ -94,15 +94,15 @@ 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 con la base de datos -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] +| 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 solo una conexión, los nombres de los servicios serán `database.default.connection` y `database.default.explorer`. Si definimos múltiples 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`. +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`. -Pasamos los servicios no autowired explícitamente por referencia a su nombre: +Los servicios que no están en el autowiring se pasan explícitamente refiriéndose a su nombre: ```neon services: diff --git a/database/es/exceptions.texy b/database/es/exceptions.texy index 67ced3eb4d..78b8f4ae28 100644 --- a/database/es/exceptions.texy +++ b/database/es/exceptions.texy @@ -1,22 +1,25 @@ Excepciones *********** -Nette Database utiliza una jerarquía de excepciones. La clase base es `Nette\Database\DriverException`, que hereda de `PDOException` y proporciona opciones extendidas para trabajar con errores de la base de datos: +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 +- 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. -De `DriverException` heredan las siguientes excepciones especializadas: +La clase `DriverException` la extienden las siguientes excepciones especializadas: -- `ConnectionException` - señala un fallo de conexión al servidor de base de datos -- `ConstraintViolationException` - clase base para violaciones de restricciones de la base de datos, de la cual heredan: - - `ForeignKeyConstraintViolationException` - violación de clave foránea - - `NotNullConstraintViolationException` - violación de restricción NOT NULL - - `UniqueConstraintViolationException` - violación de unicidad de valor +- `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. - -Ejemplo de captura de excepción `UniqueConstraintViolationException`, que ocurre cuando intentamos insertar un usuario con un correo electrónico que ya existe en la base de datos (asumiendo que la columna `email` tiene un índice único). +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 { @@ -26,9 +29,9 @@ try { 'password' => $hashedPassword, ]); } catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'El usuario con este correo electrónico ya existe.'; + echo 'A user with this email already exists.'; } catch (Nette\Database\DriverException $e) { - echo 'Ocurrió un error durante el registro: ' . $e->getMessage(); + echo 'An error occurred during registration: ' . $e->getMessage(); } ``` diff --git a/database/es/explorer.texy b/database/es/explorer.texy index a7d283a7e0..d9f3e7a984 100644 --- a/database/es/explorer.texy +++ b/database/es/explorer.texy @@ -3,92 +3,92 @@ Database Explorer <div class=perex> -Explorer ofrece una forma intuitiva y eficiente de trabajar con la base de datos. Se encarga automáticamente de las relaciones entre tablas y la optimización de consultas, para que puedas concentrarte en tu aplicación. Funciona inmediatamente sin configuración. Si necesitas control total sobre las consultas SQL, puedes utilizar el [acceso SQL |sql-way]. +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 datos es natural y fácil de entender. -- Genera consultas SQL optimizadas que cargan solo los datos necesarios. -- Permite un fácil acceso a los datos relacionados sin necesidad de escribir consultas JOIN. -- Funciona instantáneamente sin ninguna configuración o generación de entidades. +- 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> -Empiezas con Explorer llamando al método `table()` del objeto [api:Nette\Database\Explorer] (los detalles de la conexión se pueden encontrar en el capítulo [Conexión y configuración |guide#Conexión y configuración]): +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 podemos encadenar otros métodos para filtrar y ordenar los resultados. La consulta se construye y ejecuta solo cuando empezamos a solicitar datos. Por ejemplo, iterando con un bucle `foreach`. Cada fila está representada por un objeto [ActiveRow |api:Nette\Database\Table\ActiveRow]: +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; // Salida de la columna 'title' - echo $book->author_id; // Salida de la columna 'author_id' + echo $book->title; // imprime la columna 'title' + echo $book->author_id; // imprime la columna 'author_id' } ``` -Explorer facilita fundamentalmente el trabajo con [#relaciones entre tablas]. El siguiente ejemplo muestra lo fácil que es mostrar datos de tablas relacionadas (libros y sus autores). Ten en cuenta que no necesitamos escribir ninguna consulta JOIN, Nette las crea por nosotros: +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 'Libro: ' . $book->title; - echo 'Autor: ' . $book->author->name; // Crea un JOIN a la tabla 'author' + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // crea un JOIN con la tabla 'author' } ``` -Nette Database Explorer optimiza las consultas para que sean lo más eficientes posible. El ejemplo anterior ejecuta solo dos consultas SELECT, independientemente de si procesamos 10 o 10,000 libros. +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 rastrea qué columnas se utilizan en el código y carga solo esas desde la base de datos, ahorrando así rendimiento adicional. Este comportamiento es completamente automático y adaptativo. Si más tarde modificas el código y comienzas a usar columnas adicionales, Explorer ajustará automáticamente las consultas. No necesitas configurar nada, ni pensar qué columnas necesitarás - déjaselo a Nette. +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` proporciona métodos para filtrar y ordenar la selección de datos. +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. Múltiples condiciones se unen con el operador AND -| `whereOr(array $conditions)` | Añade un grupo de condiciones WHERE unidas por el operador OR -| `wherePrimary($value)` | Añade una condición WHERE por clave primaria -| `order($columns, ...$params)` | Establece el orden ORDER BY -| `select($columns, ...$params)` | Especifica las columnas que se deben cargar -| `limit($limit, $offset = null)` | Limita el número de filas (LIMIT) y opcionalmente establece OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Establece la paginación -| `group($columns, ...$params)` | Agrupa las filas (GROUP BY) -| `having($condition, ...$params)` | Añade una condición HAVING para filtrar filas agrupadas +| `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(...)`. +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 puedes usar notación especial para acceder a [datos de tablas relacionadas |#Consultas a través de tablas relacionadas]. +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]. -Escape e identificadores ------------------------- +Escapado e identificadores +-------------------------- -Los métodos escapan automáticamente los parámetros y entrecomillan los identificadores (nombres de tablas y columnas), previniendo así la inyección SQL. Para un funcionamiento correcto, es necesario seguir algunas reglas: +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: -- Escribe las palabras clave, nombres de funciones, procedimientos, etc., en **MAYÚSCULAS**. -- Escribe los nombres de columnas y tablas en **minúsculas**. -- Siempre inserta cadenas a través de **parámetros**. +- 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 = ' . $name); // VULNERABILIDAD CRÍTICA: inyección SQL where('name LIKE "%search%"'); // MAL: complica el entrecomillado automático -where('name LIKE ?', '%search%'); // CORRECTO: valor insertado mediante parámetro +where('name LIKE ?', '%search%'); // BIEN: el valor se pasa como parámetro where('name like ?', $name); // MAL: genera: `name` `like` ? -where('name LIKE ?', $name); // CORRECTO: genera: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORRECTO: LOWER(`name`) = ? +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 usando condiciones WHERE. Su punto fuerte es el manejo inteligente de diferentes tipos de valores y la elección automática de operadores SQL. +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: @@ -98,26 +98,26 @@ $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 de operadores apropiados, no tenemos que lidiar con varios casos especiales. Nette los resuelve por nosotros: +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 se puede usar un signo de interrogación de marcador de posición sin operador: +// También puede usar el marcador ? sin operador: $table->where('id ?', 1); // WHERE `id` = 1 ``` -El método también maneja correctamente condiciones negativas y arrays vacíos: +El método trata correctamente las condiciones negativas y los arrays vacíos: ```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- No encontrará nada -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- Encontrará todo -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- Encontrará todo -// $table->where('NOT id ?', $ids); Atención - esta sintaxis no es compatible +$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 ``` -También podemos pasar el resultado de otra tabla como parámetro - se creará una subconsulta: +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`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -También podemos pasar las condiciones como un array, cuyos elementos se unirán usando AND: +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`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -En el array podemos usar pares clave => valor y Nette elegirá automáticamente los operadores correctos de nuevo: +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)) @@ -147,23 +147,23 @@ $table->where([ ]); ``` -En el array podemos combinar expresiones SQL con marcadores de posición de signo de interrogación y múltiples parámetros. Esto es adecuado para condiciones complejas con operadores definidos con precisión: +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], // Pasamos dos parámetros como un array + 'ROUND(score, ?) > ?' => [2, 75.5], // los dos parámetros se pasan como array ]); ``` -Múltiples llamadas a `where()` unen automáticamente las condiciones usando AND. +Varias llamadas a `where()` combinan las condiciones automáticamente con AND. whereOr(array $parameters): static .[method] -------------------------------------------- -Similar a `where()`, añade condiciones, pero con la diferencia de que las une usando OR: +Parecido a `where()`, añade condiciones, pero las combina con OR: ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -Aquí también podemos usar expresiones más complejas: +Aquí también se pueden usar expresiones más complejas: ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -187,7 +187,7 @@ $table->whereOr([ wherePrimary(mixed $key): static .[method] ------------------------------------------ -Añade una condición para la clave primaria de la tabla: +Añade una condición sobre la clave primaria de la tabla: ```php // WHERE `id` = 123 @@ -197,7 +197,7 @@ $table->wherePrimary(123); $table->wherePrimary([1, 2, 3]); ``` -Si la tabla tiene una clave primaria compuesta (por ejemplo, `foo_id`, `bar_id`), la pasamos como un array: +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 @@ -214,7 +214,7 @@ $table->wherePrimary([ order(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Especifica el orden en que se devolverán las filas. Podemos ordenar por una o más columnas, en orden descendente o ascendente, o por una expresión personalizada: +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` @@ -227,18 +227,18 @@ $table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC select(string $columns, ...$parameters): static .[method] --------------------------------------------------------- -Especifica las columnas que deben devolverse de la base de datos. Por defecto, Nette Database Explorer devuelve solo aquellas columnas que se utilizan realmente en el código. Por lo tanto, usamos el método `select()` en casos donde necesitamos devolver expresiones específicas: +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 usando `AS` están entonces disponibles como propiedades del objeto ActiveRow: +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 + echo $row->formatted_date; // acceso al alias } ``` @@ -246,35 +246,35 @@ foreach ($table as $row) { limit(?int $limit, ?int $offset = null): static .[method] --------------------------------------------------------- -Limita el número de filas devueltas (LIMIT) y opcionalmente permite establecer un offset: +Limita el número de filas devueltas (LIMIT) y opcionalmente permite fijar un desplazamiento: ```php -$table->limit(10); // LIMIT 10 (Devuelve las primeras 10 filas) +$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()`. +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 resultados. Acepta el número de página (contando desde 1) y el número de elementos por página. Opcionalmente, se puede pasar una referencia a una variable donde se almacenará el número total de páginas: +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); -echo "Total de páginas: $numOfPages"; +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); +echo "Total pages: $numOfPages"; ``` group(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Agrupa las filas por las columnas especificadas (GROUP BY). Se utiliza generalmente junto con funciones de agregación: +Agrupa las filas según las columnas indicadas (GROUP BY). Se usa normalmente junto con funciones de agregación: ```php -// Calcula el número de productos en cada categoría +// Cuenta el número de productos de cada categoría $table->select('category_id, COUNT(*) AS count') ->group('category_id'); ``` @@ -283,36 +283,36 @@ $table->select('category_id, COUNT(*) AS count') having(string $having, ...$parameters): static .[method] -------------------------------------------------------- -Establece la condición para filtrar filas agrupadas (HAVING). Se puede usar junto con el método `group()` y funciones de agregación: +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 categorías que tienen más de 100 productos +// Encuentra las categorías que tienen más de 100 productos $table->select('category_id, COUNT(*) AS count') ->group('category_id') ->having('count > ?', 100); ``` -Lectura de datos -================ +Leer los datos +============== -Para leer datos de la base de datos, tenemos varios métodos útiles disponibles: +Para leer datos de la base de datos hay disponibles varios métodos útiles: .[language-php] -| `foreach ($table as $key => $row)` | Itera sobre todas las filas, `$key` es el valor de la clave primaria, `$row` es un objeto ActiveRow -| `$row = $table->get($key)` | Devuelve una fila por clave primaria -| `$row = $table->fetch()` | Devuelve la fila actual y mueve 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 un array -| `count($table)` | Devuelve el número de filas en el objeto Selection +| `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] está destinado solo para lectura. Esto significa que no se pueden cambiar los valores de sus propiedades. Esta restricción asegura la consistencia de los datos y previene efectos secundarios inesperados. Los datos se cargan desde la base de datos y cualquier cambio debe realizarse de forma explícita y controlada. +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` - iteración sobre todas las filas -------------------------------------------- +`foreach`: recorrer todas las filas +----------------------------------- -La forma más fácil de ejecutar una consulta y obtener filas es iterando en un bucle `foreach`. Ejecuta automáticamente la consulta SQL. +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'); @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -Ejecuta la consulta SQL y devuelve la fila por clave primaria, o `null` si no existe. +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 +$book = $explorer->table('book')->get(123); // devuelve ActiveRow con ID 123 o null if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -Devuelve una fila y mueve el puntero interno a la siguiente. Si no existen más filas, devuelve `null`. +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'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Devuelve los resultados como un array asociativo. El primer argumento especifica el nombre de la columna que se usará como clave en el array, el segundo argumento especifica el nombre de la columna que se usará como valor: +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 especificamos el primer parámetro, el valor será la fila completa, es decir, el objeto `ActiveRow`: +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 utiliza el valor de la última fila. Al usar `null` como clave, el array se indexará numéricamente desde cero (entonces no ocurren colisiones): +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'); @@ -377,24 +377,24 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternativamente, puedes pasar un callback como parámetro, que devolverá el valor en sí, o un par clave-valor para cada fila. +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})"); -// ['Primer libro (Jan Novák)', ...] +// ['First Book (John Novak)', ...] -// El callback también puede devolver un array con un par clave & valor: +// 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]); -// ['Primer libro' => 'Jan Novák', ...] +// ['First Book' => 'John Novak', ...] ``` fetchAll(): array .[method] --------------------------- -Devuelve todas las filas como un array asociativo de objetos `ActiveRow`, donde las claves son los valores de las claves primarias. +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(); @@ -405,21 +405,21 @@ $allBooks = $explorer->table('book')->fetchAll(); count(): int .[method] ---------------------- -El método `count()` sin parámetro devuelve el número de filas en el objeto `Selection`: +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 +$count = count($table); // alternativa ``` -Atención, `count()` con un parámetro realiza la función de agregación `COUNT` en la base de datos, ver más abajo. +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, donde las claves son los nombres de las columnas y los valores son los datos correspondientes. +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); @@ -431,33 +431,33 @@ $bookArray = $book->toArray(); Agregación ========== -La clase `Selection` proporciona métodos para realizar fácilmente funciones de agregación (COUNT, SUM, MIN, MAX, AVG, etc.). +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 en la columna -| `max($expr)` | Devuelve el valor máximo en la columna -| `sum($expr)` | Devuelve la suma de los valores en la columna -| `aggregation($function)` | Permite ejecutar cualquier función de agregación. Por ejemplo, `AVG()`, `GROUP_CONCAT()` +| `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 se utiliza para determinar cuántas filas coinciden con una condición específica: +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` ``` -Atención, [#count()] sin parámetro solo devuelve el número de filas en el objeto `Selection`. +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 máximo en la columna o expresión especificada: +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 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -Devuelve la suma de los valores en la columna o expresión especificada: +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 @@ -478,51 +478,51 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- Permite ejecutar cualquier función de agregación. ```php -// Precio promedio de los productos en la categoría +// precio medio de los productos de una categoría $avgPrice = $products->where('category_id', 1) ->aggregation('AVG(price)'); -// Concatena las etiquetas del producto en una sola cadena +// 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 provienen de alguna función de agregación y agrupación (por ejemplo, `SUM(valor)` sobre filas agrupadas), especificamos la función de agregación que se aplicará a estos resultados intermedios como segundo argumento: +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 para cada categoría y luego suma estos precios. +// 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, primero calculamos el precio total de los productos en cada categoría (`SUM(price * stock) AS category_total`) y agrupamos los resultados por `category_id`. Luego usamos `aggregation('SUM(category_total)', 'SUM')` para sumar estos subtotales `category_total`. El segundo argumento `'SUM'` indica que la función `SUM` debe aplicarse a los resultados intermedios. +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. -Insertar, Actualizar y Eliminar -=============================== +Insert, Update y Delete +======================= -Nette Database Explorer simplifica la inserción, actualización y eliminación de datos. Todos los métodos mencionados lanzarán una excepción `Nette\Database\DriverException` en caso de error. +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 nuevos registros en la tabla. +Inserta registros nuevos en la tabla. **Insertar un solo registro:** -Pasamos el nuevo registro como un array asociativo o un objeto iterable (por ejemplo, `ArrayHash` usado en [formularios |forms:]), donde las claves corresponden a los nombres de las columnas en la tabla. +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 una clave primaria definida, el método devuelve un objeto `ActiveRow`, que se recarga desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (disparadores, valores de columna predeterminados, cálculos de columnas autoincrementales). Esto asegura la consistencia de los datos y el objeto siempre contiene los datos actuales de la base de datos. Si no tiene una clave primaria única, devuelve los datos pasados en forma de array. +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([ @@ -530,14 +530,14 @@ $row = $explorer->table('users')->insert([ 'email' => 'john.doe@example.com', ]); // $row es una instancia de ActiveRow y contiene los datos completos de la fila insertada, -// incluyendo el ID generado automáticamente y cualquier cambio realizado por disparadores +// 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 está establecida por un disparador +echo $row->created_at; // Imprime la hora de creación si la estableció un trigger ``` -**Insertar múltiples registros a la vez:** +**Insertar varios registros a la vez:** -El método `insert()` permite insertar múltiples registros usando una sola consulta SQL. En este caso, devuelve el número de filas insertadas. +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([ @@ -554,7 +554,7 @@ $insertedRows = $explorer->table('users')->insert([ // $insertedRows será 2 ``` -También se puede pasar un objeto `Selection` con una selección de datos como parámetro. +Como parámetro también se puede pasar un objeto `Selection` con una selección de datos. ```php $newUsers = $explorer->table('potential_users') @@ -566,14 +566,14 @@ $insertedRows = $explorer->table('users')->insert($newUsers); **Insertar valores especiales:** -También podemos pasar archivos, objetos `DateTime` o literales SQL como valores: +También podemos pasar como valores archivos, objetos `DateTime` o literales SQL: ```php $explorer->table('users')->insert([ 'name' => 'John', - 'created_at' => new DateTime, // Convierte al formato de base de datos - 'avatar' => fopen('image.jpg', 'rb'), // Inserta el contenido binario del archivo - 'uuid' => $explorer::literal('UUID()'), // Llama a la función UUID() + '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() ]); ``` @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -Actualiza las filas en la tabla según el filtro especificado. Devuelve el número de filas realmente cambiadas. +Actualiza las filas de la tabla según el filtro indicado. Devuelve el número de filas realmente modificadas. -Pasamos las columnas a cambiar como un array asociativo o un objeto iterable (por ejemplo, `ArrayHash` usado en [formularios |forms:]), donde las claves corresponden a los nombres de las columnas en la tabla: +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') @@ -595,14 +595,14 @@ $affected = $explorer->table('users') // UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Para cambiar valores numéricos, podemos usar los operadores `+=` y `-=`: +Para cambiar valores numéricos puede usar los operadores `+=` y `-=`: ```php $explorer->table('users') ->where('id', 10) ->update([ - 'points+=' => 1, // Incrementa el valor de la columna 'points' en 1 - 'coins-=' => 1, // Decrementa el valor de la columna 'coins' en 1 + '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 ``` @@ -611,7 +611,7 @@ $explorer->table('users') Selection::delete(): int .[method] ---------------------------------- -Elimina filas de la tabla según el filtro especificado. Devuelve el número de filas eliminadas. +Borra las filas de la tabla según el filtro indicado. Devuelve el número de filas borradas. ```php $count = $explorer->table('users') @@ -621,111 +621,111 @@ $count = $explorer->table('users') ``` .[caution] -Al llamar a `update()` y `delete()`, no olvides especificar las filas a modificar/eliminar usando `where()`. Si no usas `where()`, ¡la operación se realizará en toda la tabla! +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 en la fila de la base de datos representada por el objeto `ActiveRow`. Acepta un iterable con los datos a actualizar como parámetro (las claves son los nombres de las columnas). Para cambiar valores numéricos, podemos usar los operadores `+=` y `-=`: +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 `-=`: -Después de realizar la actualización, `ActiveRow` se recarga automáticamente desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (por ejemplo, disparadores). El método devuelve `true` solo si los datos realmente cambiaron. +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', // Incrementamos el número de vistas + 'views += 1', // incrementa el contador de visitas ]); -echo $article->views; // Imprime el número actual de vistas +echo $article->views; // Imprime el contador de visitas actual ``` -Este método actualiza solo una fila específica en la base de datos. Para la actualización masiva de múltiples filas, usa el método [#Selection::update()]. +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() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -Elimina la fila de la base de datos representada por el objeto `ActiveRow`. +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(); // Elimina el libro con ID 1 +$book->delete(); // Borra el libro con ID 1 ``` -Este método elimina solo una fila específica en la base de datos. Para la eliminación masiva de múltiples filas, usa el método [#Selection::delete()]. +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 dividen en múltiples tablas y se interconectan mediante claves foráneas. Nette Database Explorer introduce una forma revolucionaria de trabajar con estas relaciones, sin escribir consultas JOIN y sin necesidad de configurar o generar nada. +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 relaciones, usaremos un ejemplo de base de datos de libros ([puedes encontrarlo en GitHub |https://github.com/nette-examples/books]). En la base de datos tenemos tablas: +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`) +- `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 utilizada en los ejemplos *** +[* db-schema-1-.webp *] *** Estructura de la base de datos usada en los ejemplos .<> -En nuestro ejemplo de base de datos de libros, encontramos varios tipos de relaciones (aunque el modelo está simplificado en comparación con la realidad): +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** un traductor, un traductor puede traducir **varios** libros. -- Cero a uno (0:1) – un libro **puede tener** una secuela. -- Muchos a muchos (M:N) – un libro **puede tener varias** etiquetas y una etiqueta puede asignarse a **varios** libros. +- **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 autor y libro, la tabla `author` es la padre y `book` es la hija - podemos imaginarlo como que un libro siempre "pertenece" a algún autor. Esto también se refleja en la estructura de la base de datos: la tabla hija `book` contiene la clave foránea `author_id`, que hace referencia a la tabla padre `author`. +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 libros incluyendo los nombres de sus autores, tenemos dos opciones. O bien obtenemos los datos con una única consulta SQL usando JOIN: +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 +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -O cargamos los datos en dos pasos - primero los libros y luego sus autores - y luego los ensamblamos en PHP: +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 de los autores de los libros obtenidos +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 cargan solo una vez y pueden utilizarse mejor en la caché. Así es exactamente como funciona Nette Database Explorer: resuelve todo bajo el capó y te ofrece una API elegante: +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 'título: ' . $book->title; - echo 'escrito por: ' . $book->author->name; // $book->author es un registro de la tabla 'author' - echo 'traducido por: ' . $book->translator?->name; + 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; } ``` -Acceso a la tabla padre ------------------------ +Acceder a la tabla padre +------------------------ -El acceso a la tabla padre es directo. Se trata de relaciones como *un libro tiene un autor* o *un libro puede tener un traductor*. Obtenemos el registro relacionado a través de una propiedad del objeto `ActiveRow`; su nombre corresponde al nombre de la columna de clave foránea sin `_id`: +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 al autor por la columna author_id -echo $book->translator?->name; // Encuentra al traductor por translator_id +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 ``` -Cuando accedemos a la propiedad `$book->author`, Explorer busca en la tabla `book` una columna cuyo nombre contenga la cadena `author` (es decir, `author_id`). Según el valor en esta columna, carga el registro correspondiente de la tabla `author` y lo devuelve como `ActiveRow`. De manera similar funciona `$book->translator`, que utiliza la columna `translator_id`. Dado que la columna `translator_id` puede contener `null`, usamos el operador `?->` en el código. +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 `?->`. -Una ruta alternativa la 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`: +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 +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 a través de la propiedad porque la tabla contiene una columna con el mismo nombre (es decir, `author`). En otros casos, se recomienda usar el acceso a través de la propiedad, que es más legible. +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 iteramos sobre 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 un SELECT para cada tipo de relación, reduciendo significativamente la carga de la base de datos. Por ejemplo: +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'); @@ -736,168 +736,168 @@ foreach ($books as $book) { } ``` -Este código solo llama a estas tres consultas rápidas a la base de datos: +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 de la columna author_id de los libros seleccionados -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- IDs de la columna translator_id de los libros seleccionados +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 viene dada por la implementación de [Conventions |api:Nette\Database\Conventions]. Recomendamos usar [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analiza las claves foráneas y permite trabajar fácilmente con las relaciones existentes entre tablas. +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. -Acceso a la tabla hija ----------------------- +Acceder a la tabla hija +----------------------- -El acceso a la tabla hija funciona en la dirección opuesta. Ahora preguntamos *qué libros escribió este autor* o *tradujo este traductor*. Para este tipo de consulta, usamos el método `related()`, que devuelve una `Selection` con registros relacionados. Veamos un ejemplo: +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 "Escrito por: $book->title"; + echo "Wrote: $book->title"; } -// Imprime todos los libros que el autor tradujo +// Imprime todos los libros traducidos por el autor foreach ($author->related('book.translator_id') as $book) { - echo "Traducido por: $book->title"; + echo "Translated: $book->title"; } ``` -El método `related()` acepta la descripción de la unión como un argumento con notación de puntos o como dos argumentos separados: +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 argumento -$author->related('book', 'translator_id'); // Dos argumentos +$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 basándose en el nombre de la tabla padre. En este caso, la unión se realiza a través de la columna `book.author_id`, porque el nombre de la tabla de origen es `author`: +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 +$author->related('book'); // usa book.author_id ``` -Si hubiera múltiples uniones posibles, Explorer lanzaría una excepción [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Si existen varias uniones posibles, Explorer lanzará una [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Por supuesto, también podemos usar el método `related()` al iterar sobre múltiples registros en un bucle, y Explorer también optimiza automáticamente las consultas en este caso: +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 . ' escribió:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { echo $book->title; } } ``` -Este código genera solo dos consultas SQL rápidas: +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 de los autores seleccionados +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors ``` -Relación Muchos a Muchos +Relación muchos a muchos ------------------------ -Para una relación muchos a muchos (M:N), se requiere una tabla de unión (en nuestro caso `book_tag`), que contiene dos columnas con claves foráneas (`book_id`, `tag_id`). Cada una de estas columnas hace referencia a la clave primaria de una de las tablas conectadas. Para obtener los datos relacionados, primero obtenemos los registros de la tabla de unión usando `related('book_tag')` y luego continuamos hacia los datos de destino: +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 +// 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 + 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 viceversa: imprime los nombres de los libros etiquetados con esta etiqueta +// 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 + echo $bookTag->book->title; // imprime el título del libro } ``` -Explorer optimiza de nuevo las consultas SQL a una forma eficiente: +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 de los libros seleccionados -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- IDs de las etiquetas encontradas en book_tag +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 ``` -Consultas a través de tablas relacionadas +Consultar a través de tablas relacionadas ----------------------------------------- -En los métodos `where()`, `select()`, `order()` y `group()`, podemos usar notaciones especiales para acceder a columnas de otras tablas. Explorer crea automáticamente los `JOIN` necesarios. +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 de puntos** (`tabla_padre.columna`) se utiliza para la relación 1:N desde la perspectiva de la tabla hija: +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 libros cuyo autor tiene un nombre que empieza por 'Jon' +// Encuentra los libros cuyo autor tiene un nombre que empieza por 'Jon' $books->where('author.name LIKE ?', 'Jon%'); -// Ordena los libros por nombre de autor en orden descendente +// 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 de dos puntos** (`:tabla_hija.columna`) se utiliza para la relación 1:N desde la perspectiva de la tabla padre: +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 autores que escribieron un libro con 'PHP' en el título +// 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 para cada autor +// 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 notación de dos puntos (`:book.title`), no se especifica la columna de clave foránea. Explorer detecta automáticamente la columna correcta basándose en el nombre de la tabla padre. En este caso, la unión se realiza a través de la columna `book.author_id`, porque el nombre de la tabla de origen es `author`. Si hubiera múltiples uniones posibles, Explorer lanzaría una excepción [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +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 especificar explícitamente entre paréntesis: +La columna de unión se puede indicar explícitamente entre paréntesis: ```php -// Encuentra autores que tradujeron un libro con 'PHP' en el título +// 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 través de múltiples tablas: +Las notaciones se pueden encadenar para acceder a datos de varias tablas: ```php -// Encuentra autores de libros etiquetados con 'PHP' +// Encuentra los autores de los libros etiquetados con 'PHP' $authors->where(':book:book_tag.tag.name', 'PHP') ->group('author.id'); ``` -Extensión de condiciones para JOIN ----------------------------------- +Ampliar las condiciones del JOIN +-------------------------------- -El método `joinWhere()` extiende las condiciones que se especifican al unir tablas en SQL después de la palabra clave `ON`. +El método `joinWhere()` amplía las condiciones indicadas al unir tablas en SQL tras la palabra clave `ON`. -Supongamos que queremos encontrar libros traducidos por un traductor específico: +Digamos que queremos encontrar los libros traducidos por un traductor concreto: ```php -// Encuentra libros traducidos por el traductor llamado 'David' +// 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 `joinWhere()`, podemos usar las mismas construcciones que en el método `where()` - operadores, marcadores de posición de signo de interrogación, arrays de valores o expresiones SQL. +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 múltiples `JOIN`, podemos definir alias de tabla: +Para consultas más complejas con varios JOIN puede definir alias de tablas: ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // AND (`book_author`.`born` < 1950) ``` -Ten en cuenta que mientras el método `where()` añade condiciones a la cláusula `WHERE`, el método `joinWhere()` extiende las condiciones en la cláusula `ON` al unir tablas. +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 index bf912ef423..4ba6820b71 100644 --- a/database/es/guide.texy +++ b/database/es/guide.texy @@ -2,18 +2,18 @@ Nette Database ************** .[perex] -Nette Database es una capa de base de datos potente y elegante para PHP con énfasis en la simplicidad y funciones inteligentes. Ofrece dos formas de trabajar con la base de datos: [Explorer |explorer] para el desarrollo rápido de aplicaciones, o [acceso SQL |sql-way] para el trabajo directo con consultas. +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> -[Acceso SQL |sql-way] +[Enfoque SQL|sql-way] ===================== -- Consultas parametrizadas seguras -- Control preciso sobre la forma de las consultas SQL -- Cuando escribe consultas complejas con funciones avanzadas -- Optimiza el rendimiento utilizando funciones SQL específicas +- 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> @@ -22,10 +22,10 @@ Nette Database es una capa de base de datos potente y elegante para PHP con énf [Explorer |explorer] ==================== -- Desarrolla rápidamente sin escribir SQL -- Trabajo intuitivo con relaciones entre tablas -- Apreciará la optimización automática de consultas -- Adecuado para un trabajo rápido y cómodo con la base de datos +- 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> @@ -35,45 +35,45 @@ Nette Database es una capa de base de datos potente y elegante para PHP con énf Instalación =========== -Descarga e instala la librería usando la herramienta [Composer |best-practices:composer]: +Descargue e instale la biblioteca con [Composer|best-practices:composer]: ```shell composer require nette/database ``` -Bases de datos soportadas -========================= +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 | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SÍ -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - +|* 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 la base de datos -================================== +Dos enfoques para trabajar con la base de datos +=============================================== -Nette Database te da una opción: puedes escribir consultas SQL directamente (acceso SQL), o dejar que se generen automáticamente (Explorer). Veamos cómo ambos enfoques resuelven las mismas tareas: +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: -[Acceso SQL |sql-way] - Consultas SQL +[Enfoque SQL|sql-way] - consultas SQL ```php -// insertar registro +// Inserta un registro $database->query('INSERT INTO books', [ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// obtener registros: autores de libros +// Obtiene registros: autores de libros $result = $database->query(' SELECT authors.*, COUNT(books.id) AS books_count FROM authors @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// listado (no óptimo, genera N consultas adicionales) +// Muestra (no es óptimo, genera N consultas adicionales) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -90,7 +90,7 @@ foreach ($result as $author) { ORDER BY published_at DESC ', $author->id); - echo "Autor $author->name escribió $author->books_count libros:\n"; + echo "Author $author->name has written $author->books_count books:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -98,26 +98,26 @@ foreach ($result as $author) { } ``` -[Enfoque Explorer |explorer] - generación automática de SQL +[Enfoque Explorer|explorer] - generación automática de SQL ```php -// insertar registro +// Inserta un registro $database->table('books')->insert([ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// obtener registros: autores de libros +// Obtiene registros: autores de libros $authors = $database->table('authors') ->where('active', 1); -// listado (genera automáticamente solo 2 consultas optimizadas) +// Muestra (genera automáticamente solo 2 consultas optimizadas) foreach ($authors as $author) { $books = $author->related('books') ->order('published_at DESC'); - echo "Autor $author->name escribió {$books->count()} libros:\n"; + echo "Author $author->name has written {$books->count()} books:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -125,23 +125,23 @@ foreach ($authors as $author) { } ``` -El enfoque Explorer genera y optimiza las consultas SQL automáticamente. En el ejemplo dado, el acceso SQL generará N+1 consultas (una para los autores y luego una para los libros de cada autor), mientras que Explorer optimiza automáticamente las consultas y realiza solo dos: una para los autores y otra para todos sus libros. +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 la aplicación según sea necesario. +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, simplemente crea una instancia de la clase [api:Nette\Database\Connection]: +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 utiliza PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], por ejemplo, `mysql:host=127.0.0.1;dbname=test`. En caso de fallo, lanza una excepción `Nette\Database\ConnectionException`. +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`. -Sin embargo, una forma más conveniente es ofrecida por la [configuración de la aplicación |configuration], donde simplemente necesitas agregar la sección `database` y se crearán los objetos necesarios, así como el panel de base de datos en la barra [Tracy |tracy:]. +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: @@ -150,7 +150,7 @@ database: password: password ``` -Luego, [obtenemos el objeto de conexión como servicio del contenedor DI |dependency-injection:passing-dependencies], por ejemplo: +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 @@ -163,22 +163,22 @@ class Model } ``` -Más información sobre la [configuración de la base de datos |configuration]. +Más información sobre la [configuración de la base de datos|configuration]. Creación manual de Explorer --------------------------- -Si no utilizas el contenedor Nette DI, puedes crear manualmente una instancia de `Nette\Database\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 para caché, implementa Nette\Caching\Storage, por ejemplo: -$storage = new Nette\Caching\Storages\FileStorage('/ruta/a/directorio/temp'); -// se encarga de la reflexión de la estructura de la base de datos +// 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 reglas para mapear nombres de tablas, columnas y claves foráneas +// 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); ``` @@ -187,30 +187,32 @@ $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $ Gestión de la conexión ====================== -Al crear un objeto `Connection`, la conexión se establece automáticamente. Si deseas posponer la conexión, utiliza el modo lazy; puedes activarlo en la [configuración |configuration] estableciendo `lazy: true`, o de esta manera: +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, utiliza los métodos `connect()`, `disconnect()` y `reconnect()`. -- `connect()` crea una conexión si aún no existe, y puede lanzar una excepción `Nette\Database\ConnectionException`. -- `disconnect()` desconecta la conexión actual a la base de datos. -- `reconnect()` realiza una desconexión y luego una reconexión a la base de datos. Este método también puede lanzar una excepción `Nette\Database\ConnectionException`. +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, puedes monitorear los eventos relacionados con la conexión utilizando el evento `onConnect`, que es un array de callbacks que se llaman después de establecer una conexión con la base de datos. +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 después de conectarse a la base de datos +// se ejecuta tras conectar con la base de datos $database->onConnect[] = function($database) { - echo "Conectado a la base de datos"; + 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. -Tracy Debug Bar -=============== -Si utilizas [Tracy |tracy:], el panel Database se activa automáticamente en la barra de depuración, mostrando todas las consultas ejecutadas, sus parámetros, el tiempo de ejecución y la ubicación en el código donde fueron llamadas. +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/mapping.texy b/database/es/mapping.texy deleted file mode 100644 index 615c68b2e5..0000000000 --- a/database/es/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversión de tipos -******************* - -.[perex] -Nette Database convierte automáticamente los valores devueltos de la base de datos a los tipos PHP correspondientes. - - -Fecha y hora ------------- - -Los datos de tiempo se convierten en objetos `Nette\Utils\DateTime`. Si desea que los datos de tiempo se conviertan en objetos inmutables `Nette\Database\DateTime`, establezca la opción `newDateTime` en true 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, convierte el tipo de dato `TIME` en objetos `DateInterval`. - - -Valores booleanos ------------------ - -Los valores booleanos se convierten automáticamente a `true` o `false`. Para MySQL, `TINYINT(1)` se convierte si establecemos `convertBoolean: true` 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 columna en la base de datos: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // double (o float) -``` - - -Normalización personalizada ---------------------------- - -Usando el método `setRowNormalizer(?callable $normalizer)`, puedes establecer una función personalizada para transformar las filas de la base de datos. Esto es útil, por ejemplo, para la conversión automática de tipos de datos. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // aquí ocurre la conversión de tipos - return $row; -}); -``` diff --git a/database/es/reflection.texy b/database/es/reflection.texy index f948bdbf74..1f761d220a 100644 --- a/database/es/reflection.texy +++ b/database/es/reflection.texy @@ -2,74 +2,76 @@ Reflexión de la estructura ************************** .{data-version:3.2.1} -Nette Database proporciona herramientas para la introspección de la estructura de la base de datos utilizando la clase [api:Nette\Database\Reflection\Reflection]. Permite obtener información sobre tablas, columnas, índices y claves foráneas. Puedes utilizar la reflexión para generar esquemas, crear aplicaciones flexibles que trabajen con la base de datos o herramientas generales de base de datos. +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. -Obtenemos el objeto de reflexión de la instancia de conexión a la base 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(); ``` -Obtención de tablas -------------------- +Obtener las tablas +------------------ -La propiedad de solo lectura `$reflection->tables` contiene un array asociativo de todas las tablas en la base de datos: +La propiedad de solo lectura `$reflection->tables` contiene un array asociativo de todas las tablas de la base de datos: ```php -// Listado de nombres de todas las tablas +// Lista los nombres de todas las tablas foreach ($reflection->tables as $name => $table) { echo $name . "\n"; } ``` -También hay dos métodos disponibles: +Hay disponibles otros dos métodos: ```php -// Verificar la existencia de una tabla +// Comprueba la existencia de una tabla if ($reflection->hasTable('users')) { - echo "La tabla users existe"; + echo "Table users exists"; } -// Devuelve el objeto de la tabla; si no existe, lanza una excepción +// Devuelve el objeto de la tabla; lanza una excepción si no existe $table = $reflection->getTable('users'); ``` -Información sobre la tabla --------------------------- +Información de la tabla +----------------------- -La tabla está representada por el objeto [Table |api:Nette\Database\Reflection\Table], que proporciona las siguientes propiedades de solo lectura: +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 incluyendo el esquema (si existe) -- `$columns: array<string, Column>` – array asociativo de columnas de la tabla -- `$indexes: Index[]` – array de índices de la tabla -- `$primaryKey: ?Index` – clave primaria de la tabla o `null` -- `$foreignKeys: ForeignKey[]` – array de claves foráneas de la tabla +- `$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 proporciona un array asociativo de columnas, donde la clave es el nombre de la columna y el valor es una instancia de [Column |api:Nette\Database\Reflection\Column] con estas propiedades: +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 de dato nativo de la base de datos -- `$size: ?int` – tamañ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 auto-increment -- `$primary: bool` – si es parte de la clave primaria -- `$vendor: array` – metadatos adicionales específicos del sistema de base de datos +- `$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 "Columna: $name\n"; - echo "Tipo: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Sí' : 'No') . "\n"; + echo "Column: $name\n"; + echo "Type: {$column->nativeType}\n"; + echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; } ``` @@ -77,28 +79,28 @@ foreach ($table->columns as $name => $column) { Índices ------- -La propiedad `indexes` de la tabla proporciona un array de índices, donde cada índice es una instancia de [Index |api:Nette\Database\Reflection\Index] con estas propiedades: +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 columnas que forman el índice -- `$unique: bool` – si el índice es único -- `$primary: bool` – si es la clave primaria -- `$name: ?string` – nombre del índice +- `$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 usando la propiedad `primaryKey`, que devuelve un objeto `Index` o `null` si la tabla no tiene clave primaria. +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 -// Listado de índices +// Lista los índices foreach ($table->indexes as $index) { $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Índice" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Columnas: $columns\n"; - echo " Único: " . ($index->unique ? 'Sí' : 'No') . "\n"; + echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; + echo " Columns: $columns\n"; + echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; } -// Listado de la clave primaria +// Lista la clave primaria if ($primaryKey = $table->primaryKey) { $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Clave primaria: $columns\n"; + echo "Primary Key: $columns\n"; } ``` @@ -106,15 +108,15 @@ if ($primaryKey = $table->primaryKey) { Claves foráneas --------------- -La propiedad `foreignKeys` de la tabla proporciona un array de claves foráneas, donde cada clave foránea es una instancia de [ForeignKey |api:Nette\Database\Reflection\ForeignKey] con estas propiedades: +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` – tabla referenciada -- `$localColumns: Column[]` – array de columnas locales -- `$foreignColumns: Column[]` – array de columnas referenciadas -- `$name: ?string` – nombre de la clave foránea +- `$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 -// Listado de claves foráneas +// 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)); diff --git a/database/es/security.texy b/database/es/security.texy index b72f09669a..46d4b0550c 100644 --- a/database/es/security.texy +++ b/database/es/security.texy @@ -3,36 +3,36 @@ Riesgos de seguridad <div class=perex> -La base de datos a menudo contiene datos sensibles y permite realizar operaciones peligrosas. Para trabajar de forma segura con Nette Database es clave: +Las bases de datos contienen a menudo datos sensibles y permiten realizar operaciones peligrosas. Para trabajar con Nette Database de forma segura es crucial: -- Comprender la diferencia entre API segura y peligrosa -- Usar consultas parametrizadas -- Validar correctamente los datos de entrada +- entender la diferencia entre las API seguras y las inseguras +- usar consultas parametrizadas +- validar correctamente los datos de entrada </div> -¿Qué es SQL Injection? -====================== +¿Qué es la inyección SQL? +========================= -SQL injection es el riesgo de seguridad más grave al trabajar con bases de datos. Ocurre cuando la entrada no tratada del usuario se convierte en parte de una consulta SQL. Un atacante puede insertar sus propios comandos SQL y así: -- Obtener acceso no autorizado a los datos -- Modificar o eliminar datos en la base de datos -- Omitir la autenticación +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 inyección SQL +// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL $database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); -// Un atacante puede introducir, por ejemplo, el valor: ' OR '1'='1 -// La consulta resultante será: SELECT * FROM users WHERE name = '' OR '1'='1' -// Lo que devolverá todos los usuarios +// 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 se aplica a Database Explorer: +Lo mismo vale para Database Explorer: ```php -// ❌ CÓDIGO PELIGROSO - vulnerable a inyección SQL +// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL $table->where('name = ' . $_GET['name']); $table->where("name = '$_GET[name]'"); ``` @@ -41,9 +41,9 @@ $table->where("name = '$_GET[name]'"); Consultas parametrizadas ======================== -La defensa básica contra la inyección SQL son las consultas parametrizadas. Nette Database ofrece varias formas de usarlas. +La defensa fundamental contra la inyección SQL son las consultas parametrizadas. Nette Database ofrece varias formas de usarlas. -La forma más sencilla es usar **signos de interrogación como marcadores de posición**: +La más sencilla es usar **marcadores con signo de interrogación**: ```php // ✅ Consulta parametrizada segura @@ -53,9 +53,9 @@ $database->query('SELECT * FROM users WHERE name = ?', $name); $table->where('name = ?', $name); ``` -Esto se aplica a todos los demás métodos en [Database Explorer |explorer] que permiten insertar expresiones con marcadores de posición y parámetros. +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`, `UPDATE` o la cláusula `WHERE`, podemos pasar los valores en un array: +Para los comandos INSERT y UPDATE o para la cláusula WHERE podemos pasar los valores en un array: ```php // ✅ INSERT seguro @@ -72,66 +72,66 @@ $table->insert([ ``` -Validación de valores de parámetros -=================================== +Validación de los valores de los parámetros +=========================================== -Las consultas parametrizadas son la piedra angular del trabajo seguro con bases de datos. Sin embargo, los valores que insertamos en ellas deben pasar por varios niveles de control: +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: -Control de tipo ---------------- +Comprobación del tipo +--------------------- -**Lo más importante es asegurar el tipo de dato correcto de los parámetros** - esta es una condición necesaria para el uso seguro de Nette Database. La base de datos asume que todos los datos de entrada tienen el tipo de dato correcto correspondiente a la columna dada. +**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 llevaría a un error. Por lo tanto, **nunca uses** datos no validados de `$_GET`, `$_POST` o `$_COOKIE` directamente en las consultas de base de datos. +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. -Control de formato ------------------- +Validación del formato +---------------------- -En el segundo nivel, verificamos 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 la columna. +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. -En este nivel de validación, podemos confiar parcialmente en la propia base de datos: muchas bases de datos rechazarán datos no válidos. Sin embargo, el comportamiento puede variar, algunas pueden truncar silenciosamente cadenas largas o recortar números fuera de rango. +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. -Control de dominio ------------------- +Validación específica del dominio +--------------------------------- -El tercer nivel son los controles lógicos específicos de tu aplicación. Por ejemplo, verificar que los valores de los select boxes correspondan a las opciones ofrecidas, que los números estén en el rango esperado (por ejemplo, edad 0-150 años) o que las dependencias mutuas entre los valores tengan sentido. +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 ---------------------------------- -- Usa [Nette Forms |forms:], que aseguran automáticamente la validación correcta de todas las entradas. -- Usa [Presenters |application:] e indica los tipos de datos para los parámetros en los métodos `action*()` y `render*()`. -- O implementa tu propia capa de validación usando herramientas estándar de PHP como `filter_var()`. +- 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 columnas -=========================== +Trabajo seguro con las columnas +=============================== -En la sección anterior, mostramos cómo validar correctamente los valores de los parámetros. Sin embargo, al usar arrays en consultas SQL, debemos prestar la misma atención a sus claves. +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 en el array no están tratadas +// ❌ CÓDIGO PELIGROSO - las claves del array no están saneadas $database->query('INSERT INTO users', $_POST); ``` -Para los comandos `INSERT` y `UPDATE`, este es un error de seguridad fundamental: un atacante puede insertar o cambiar cualquier columna en la base de datos. Podría, por ejemplo, establecer `is_admin = 1` o insertar datos arbitrarios en columnas sensibles (la llamada Vulnerabilidad de Asignación Masiva). +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, ya que pueden contener operadores: +En las condiciones WHERE es aún más peligroso, porque pueden contener operadores: ```php -// ❌ CÓDIGO PELIGROSO - las claves en el array no están tratadas +// ❌ 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 averiguar sistemáticamente los salarios de los empleados. Comenzará, por ejemplo, con una consulta sobre salarios superiores a 100.000, luego inferiores a 50.000, y reduciendo gradualmente el rango, puede descubrir los salarios aproximados de todos los empleados. Este tipo de ataque se llama enumeración SQL. +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 aún [mucho más flexibles |explorer#where] y admiten expresiones SQL, incluidos operadores y funciones, en claves y valores. Esto le da al atacante la posibilidad de realizar una inyecció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 @@ -140,24 +140,24 @@ $table->where($_POST); // ejecuta la consulta WHERE (0) UNION SELECT name, salary FROM users WHERE (1) ``` -Este ataque finaliza la condición original con `0)`, adjunta su propio `SELECT` usando `UNION` para obtener datos sensibles de la tabla `users` y cierra la consulta sintácticamente correcta con `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 de forma segura con los nombres de las columnas, necesitamos un mecanismo que garantice que el usuario solo pueda trabajar con las columnas permitidas y no pueda agregar las suyas propias. Podríamos intentar detectar y bloquear nombres de columnas peligrosos (lista negra), pero este enfoque no es fiable: un atacante siempre puede encontrar una nueva forma de escribir un nombre de columna peligroso que no previmos. +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 lo tanto, es mucho más seguro invertir la lógica y definir una lista explícita de columnas permitidas (lista blanca): +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 editar +// Columnas que el usuario puede modificar $allowedColumns = ['name', 'email', 'active']; -// Eliminamos todas las columnas no permitidas de la entrada -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // Use array_flip for keys +// Elimina de la entrada todas las columnas no autorizadas +$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); -// ✅ Ahora podemos usar $filteredData de forma segura en consultas, como por ejemplo: +// ✅ Ahora es seguro usarlo en consultas, como: $database->query('INSERT INTO users', $filteredData); $table->update($filteredData); $table->where($filteredData); @@ -167,19 +167,19 @@ $table->where($filteredData); Identificadores dinámicos ========================= -Para nombres dinámicos de tablas y columnas, usa el marcador de posición `?name`. Esto asegura el escape correcto de los identificadores según la sintaxis de la base de datos dada (por ejemplo, usando comillas invertidas en MySQL): +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 confiables definidos en la aplicación +// ✅ 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: usa el símbolo `?name` solo para valores confiables definidos en el código de la aplicación. Para valores del usuario, usa nuevamente la [lista blanca |#Lista blanca de columnas]. De lo contrario, te expones a riesgos de seguridad: +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 uses la entrada del usuario para nombres de columnas/tablas +// ❌ 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 index 9f8a66af5a..1cf378fa36 100644 --- a/database/es/sql-way.texy +++ b/database/es/sql-way.texy @@ -1,17 +1,17 @@ -Acceso SQL -********** +Enfoque SQL +*********** .[perex] -Nette Database ofrece dos vías: puede escribir consultas SQL usted mismo (acceso SQL), o dejar que se generen automáticamente (consulte [Explorer |explorer]). El acceso SQL le da control total sobre las consultas y al mismo tiempo asegura su construcción segura. +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 configuración de la base de datos se pueden encontrar en el capítulo [Conexión y configuración |guide#Conexión y configuración]. +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 utiliza el método `query()`. Devuelve un objeto [ResultSet |api:Nette\Database\ResultSet], que representa el resultado de la consulta. En caso de fallo, el método [lanza una excepción |exceptions]. Podemos recorrer el resultado de la consulta usando un bucle `foreach`, o usar una de las [funciones auxiliares |#Obtención de datos]. +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'); @@ -22,32 +22,32 @@ foreach ($result as $row) { } ``` -Para insertar valores de forma segura en consultas SQL, usamos consultas parametrizadas. Nette Database las hace extremadamente simples: solo agregue una coma y el valor después de la consulta SQL: +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 múltiples parámetros, tiene dos opciones de sintaxis. Puede "intercalar" la consulta SQL con parámetros: +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 toda la consulta SQL y luego adjuntar todos los parámetros: +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 inyección SQL -=============================== +Protección contra la inyección SQL +================================== -¿Por qué es importante usar consultas parametrizadas? Porque lo protegen de un ataque llamado inyección SQL, en el que un atacante podría introducir sus propios comandos SQL y así obtener o dañar datos en la base de datos. +¿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 la consulta SQL!** Siempre use consultas parametrizadas, que lo protegerán de la inyección SQL. +**¡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 @@ -67,7 +67,7 @@ Técnicas de consulta Condiciones WHERE ----------------- -Puede escribir condiciones WHERE como un array asociativo, donde las claves son los nombres de las columnas y los valores son los datos para la comparación. Nette Database selecciona automáticamente el operador SQL más adecuado según el tipo de valor. +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', [ @@ -77,7 +77,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -También puede especificar explícitamente el operador de comparación en la clave: +También puede indicar explícitamente el operador de comparación en la clave: ```php $database->query('SELECT * FROM users WHERE', [ @@ -88,7 +88,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' ``` -Nette maneja automáticamente casos especiales como valores `null` o arrays. +Nette gestiona automáticamente los casos especiales, como los valores `null` o los arrays. ```php $database->query('SELECT * FROM products WHERE', [ @@ -99,25 +99,25 @@ $database->query('SELECT * FROM products WHERE', [ // WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL ``` -Para condiciones negativas, use el operador `NOT`: +Para las condiciones negativas use el operador `NOT`: ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // usa el operador <> + 'name NOT' => 'Laptop', // usa el operador != 'category_id NOT' => [1, 2, 3], // usa NOT IN 'description NOT' => null, // usa IS NOT NULL - 'id' => [], // se omite + 'id NOT' => [], // se omite ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -Para unir condiciones, se utiliza el operador `AND`. Esto se puede cambiar usando el [marcador de posición ?or |#Pistas para construir SQL]. +De forma predeterminada, las condiciones se unen con el operador `AND`. Se puede cambiar con el [marcador ?or |#Marcadores para construir SQL]. -Reglas ORDER BY ---------------- +Reglas de ORDER BY +------------------ -La ordenación `ORDER BY` se puede escribir usando un array. En las claves, especificamos las columnas y el valor será un booleano que indica si ordenar ascendentemente: +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', [ @@ -128,10 +128,10 @@ $database->query('SELECT id FROM author ORDER BY', [ ``` -Inserción de datos (INSERT) ---------------------------- +Insertar datos (INSERT) +----------------------- -Para insertar registros, se utiliza el comando SQL `INSERT`. +Para insertar registros se usa el comando SQL `INSERT`. ```php $values = [ @@ -142,11 +142,11 @@ $database->query('INSERT INTO users ?', $values); $userId = $database->getInsertId(); ``` -El método `getInsertId()` devuelve el ID de la última fila insertada. Para algunas bases de datos (por ejemplo, PostgreSQL), es necesario especificar como parámetro el nombre de la secuencia desde la cual se debe generar el ID usando `$database->getInsertId($sequenceId)`. +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)`. -También podemos pasar [#Valores especiales] como parámetros, como archivos, objetos DateTime o tipos enumerados. +Como parámetros también puede pasar [#Valores especiales], como archivos, objetos DateTime o tipos enum. -Insertar múltiples registros a la vez: +Insertar varios registros a la vez: ```php $database->query('INSERT INTO users ?', [ @@ -155,18 +155,18 @@ $database->query('INSERT INTO users ?', [ ]); ``` -El INSERT múltiple es mucho más rápido porque se ejecuta una única consulta a la base de datos, en lugar de muchas individuales. +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. -**Advertencia de seguridad:** Nunca use datos no validados como `$values`. Familiarícese con los [posibles riesgos |security#Trabajo seguro con columnas]. +**Nota de seguridad:** nunca use datos sin validar como `$values`. Familiarícese con los [posibles riesgos |security#Trabajo seguro con las columnas]. -Actualización de datos (UPDATE) -------------------------------- +Actualizar datos (UPDATE) +------------------------- -Para actualizar registros, se utiliza el comando SQL `UPDATE`. +Para actualizar registros se usa el comando SQL `UPDATE`. ```php -// Actualizar un registro +// Actualiza un solo registro $values = [ 'name' => 'John Smith', ]; @@ -175,7 +175,7 @@ $result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); El número de filas afectadas lo devuelve `$result->getRowCount()`. -Para UPDATE, podemos usar los operadores `+=` y `-=`: +En `UPDATE` podemos usar los operadores `+=` y `-=`: ```php $database->query('UPDATE users SET ? WHERE id = ?', [ @@ -183,7 +183,7 @@ $database->query('UPDATE users SET ? WHERE id = ?', [ ], 1); ``` -Ejemplo de inserción o modificación de un registro si ya existe. Usamos la técnica `ON DUPLICATE KEY UPDATE`: +Ejemplo de insertar o actualizar un registro si ya existe. Usamos la técnica `ON DUPLICATE KEY UPDATE`: ```php $values = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Observe que Nette Database reconoce en qué contexto del comando SQL insertamos el parámetro con el array 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ó a la forma `name = 'Jim', year = 1978`. Nos ocupamos de esto con más detalle en la sección [#Pistas para construir SQL]. +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]. -Eliminación de datos (DELETE) ------------------------------ +Borrar datos (DELETE) +--------------------- -Para eliminar registros, se utiliza el comando SQL `DELETE`. Ejemplo con obtención del número de filas eliminadas: +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) @@ -212,21 +212,21 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -Pistas para construir SQL -------------------------- +Marcadores para construir SQL +----------------------------- -Una pista es un marcador de posición especial en una consulta SQL que indica cómo se debe reescribir el valor del parámetro en una expresión 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: -| Pista | Descripción | Se usa automáticamente -|-----------|---------------------------------------------------|----------------------------- -| `?name` | se usa para insertar el nombre de tabla o columna | - -| `?values` | genera `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | genera la asignación `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | une condiciones en el array con el operador `AND` | `WHERE ?`, `HAVING ?` -| `?or` | une condiciones en el array con el operador `OR` | - -| `?order` | genera la cláusula `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` +| 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 ?` -Para la inserción dinámica de nombres de tablas y columnas en la consulta, se utiliza el marcador de posición `?name`. Nette Database se encarga del tratamiento correcto de los identificadores según las convenciones de la base de datos dada (por ejemplo, encerrándolos entre comillas invertidas en MySQL). +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'; @@ -235,9 +235,9 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); // SELECT `name` FROM `users` WHERE id = 1 (en MySQL) ``` -**Advertencia:** use el símbolo `?name` solo para nombres de tablas y columnas de entradas validadas, de lo contrario se expone a un [riesgo de seguridad |security#Identificadores dinámicos]. +**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]. -Otras pistas generalmente no necesitan ser especificadas, ya que Nette utiliza una detección automática inteligente al construir la consulta SQL (ver la tercera columna de la tabla). Pero puede usarla, por ejemplo, en una situación en la que desee unir condiciones usando `OR` en lugar de `AND`: +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', [ @@ -251,29 +251,29 @@ $database->query('SELECT * FROM users WHERE ?or', [ Valores especiales ------------------ -Además de los tipos escalares comunes (string, int, bool), también puede pasar valores especiales como parámetros: +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 `DateTime` se convierten al formato de la base de datos -- tipos enumerados: las instancias de `enum` se convierten a su valor -- literales SQL: creados usando `Connection::literal('NOW()')` se insertan directamente en la consulta +- 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 DateTime, + 'published_at' => new DateTimeImmutable, // o new DateTime 'content' => fopen('image.png', 'r'), 'state' => Status::Draft, ]); ``` -Para bases de datos que no tienen soporte nativo para el tipo de dato `datetime` (como SQLite y Oracle), `DateTime` se convierte al valor especificado en la [configuración de la base de datos |configuration] por la entrada `formatDateTime` (el valor predeterminado es `U` - timestamp unix). +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 especificar directamente código SQL como valor, que no debe entenderse como una cadena y escaparse. Para esto sirven los objetos de la clase `Nette\Database\SqlLiteral`. Son creados por el método `Connection::literal()`. +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', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Gracias a lo cual podemos crear combinaciones interesantes: +Eso permite combinaciones interesantes: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -317,20 +317,20 @@ $result = $database->query('SELECT * FROM users WHERE', [ ``` -Obtención de datos -================== +Obtener los datos +================= -Atajos para consultas SELECT ----------------------------- +Atajos para las consultas SELECT +-------------------------------- -Para simplificar la recuperación de datos, `Connection` ofrece varios atajos que combinan la llamada a `query()` con la siguiente `fetch*()`. Estos métodos aceptan los mismos parámetros que `query()`, es decir, la consulta SQL y parámetros opcionales. Una descripción completa de los métodos `fetch*()` se puede encontrar [más abajo |#fetch]. +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 un objeto `Row` -| `fetchAll($sql, ...$params): array` | Ejecuta la consulta y devuelve todas las filas como un array de objetos `Row` -| `fetchPairs($sql, ...$params): array` | Ejecuta la consulta y devuelve un array asociativo, donde la primera columna representa la clave y la segunda el valor -| `fetchField($sql, ...$params): mixed` | Ejecuta la consulta y devuelve el valor del primer campo de la primera fila -| `fetchList($sql, ...$params): ?array` | Ejecuta la consulta y devuelve la primera fila como un array indexado +| `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: @@ -341,10 +341,10 @@ $count = $database->query('SELECT COUNT(*) FROM articles') ``` -`foreach` - iteración sobre filas ---------------------------------- +`foreach`: recorrer las filas +----------------------------- -Después de ejecutar la 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 filas es iterando en un bucle `foreach`. Este método es el más eficiente en cuanto a memoria, ya que devuelve los datos gradualmente y no los almacena todos en la memoria a la vez. +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'); @@ -357,13 +357,13 @@ foreach ($result as $row) { ``` .[note] -`ResultSet` solo se puede iterar una vez. Si necesita iterar repetidamente, primero debe cargar los datos en un array, por ejemplo, usando el método `fetchAll()`. +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 un objeto `Row`. Si no hay más filas, devuelve `null`. Mueve el puntero interno a la siguiente fila. +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'); @@ -377,7 +377,7 @@ if ($row) { fetchAll(): array .[method] --------------------------- -Devuelve todas las filas restantes del `ResultSet` como un array de objetos `Row`. +Devuelve todas las filas restantes del `ResultSet` como array de objetos `Row`. ```php $result = $database->query('SELECT * FROM users'); @@ -391,7 +391,7 @@ foreach ($rows as $row) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Devuelve los resultados como un array asociativo. El primer argumento especifica el nombre de la columna que se usará como clave en el array, el segundo argumento especifica el nombre de la columna que se usará como valor: +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'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Si solo especificamos el primer parámetro, el valor será la fila completa, es decir, el objeto `Row`: +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 utiliza el valor de la última fila. Al usar `null` como clave, el array se indexará numéricamente desde cero (entonces no hay colisiones): +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'); @@ -417,14 +417,14 @@ $names = $result->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternativamente, puede pasar un callback como parámetro, que devolverá para cada fila ya sea el valor en sí, o un par clave-valor. +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 & valor: +// 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, ...] ``` @@ -433,7 +433,7 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -Devuelve el valor del primer campo de la fila actual. Si no hay más filas, devuelve `null`. Mueve el puntero interno a la siguiente fila. +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'); @@ -444,7 +444,7 @@ $name = $result->fetchField(); // carga el nombre de la primera fila fetchList(): ?array .[method] ----------------------------- -Devuelve una fila como un array indexado. Si no hay más filas, devuelve `null`. Mueve el puntero interno a la siguiente fila. +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'); @@ -455,19 +455,19 @@ $row = $result->fetchList(); // ['John', 'john@example.com'] getRowCount(): ?int .[method] ----------------------------- -Devuelve el número de filas afectadas por la última consulta `UPDATE` o `DELETE`. Para `SELECT`, es el número de filas devueltas, pero este puede no ser conocido; en tal caso, el método devuelve `null`. +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 en el `ResultSet`. +Devuelve el número de columnas del `ResultSet`. -Información sobre consultas -=========================== +Información sobre la consulta +============================= -Para fines de depuración, podemos obtener información sobre la última consulta ejecutada: +Para depurar podemos obtener información sobre la última consulta ejecutada: ```php echo $database->getLastQueryString(); // imprime la consulta SQL @@ -477,37 +477,37 @@ echo $result->getQueryString(); // imprime la consulta SQL echo $result->getTime(); // imprime el tiempo de ejecución en segundos ``` -Para mostrar el resultado como una tabla HTML, se puede usar: +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 columnas: +`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 es de tipo $type->type"; // por ejemplo, 'id es de tipo int' + echo "$column is of type $type"; // p. ej. 'id is of type int' } ``` -Registro de consultas ---------------------- +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('Consulta: ' . $result->getQueryString()); - $logger->info('Tiempo: ' . $result->getTime()); + $logger->info('Query: ' . $result->getQueryString()); + $logger->info('Time: ' . $result->getTime()); if ($result->getRowCount() > 1000) { - $logger->warning('Conjunto de resultados grande: ' . $result->getRowCount() . ' filas'); + $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); } }; ``` diff --git a/database/es/transactions.texy b/database/es/transactions.texy index 2b32093f77..f1640331a1 100644 --- a/database/es/transactions.texy +++ b/database/es/transactions.texy @@ -2,9 +2,9 @@ Transacciones ************* .[perex] -Las transacciones garantizan que todas las operaciones dentro de una transacción se ejecuten o que ninguna se ejecute. Son útiles para asegurar la consistencia de los datos en operaciones más complejas. +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 la siguiente: +La forma más sencilla de usar transacciones es esta: ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -Puede escribir lo mismo de forma mucho más elegante usando el método `transaction()`. Acepta una devolución de llamada como parámetro, que ejecuta dentro de la transacción. Si la devolución de llamada se ejecuta sin excepciones, la transacción se confirma automáticamente (commit). Si ocurre una excepción, la transacción se cancela (rollback) y la excepción se propaga. +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) { @@ -33,6 +33,8 @@ $database->transaction(function ($database) use ($id) { }); ``` +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 diff --git a/database/es/type-conversion.texy b/database/es/type-conversion.texy new file mode 100644 index 0000000000..25e3dd8ebc --- /dev/null +++ b/database/es/type-conversion.texy @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000000..ac811ee6a0 --- /dev/null +++ b/database/es/upgrading.texy @@ -0,0 +1,36 @@ +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/fr/@home.texy b/database/fr/@home.texy index f05b61a24f..071520fd83 100644 --- a/database/fr/@home.texy +++ b/database/fr/@home.texy @@ -1,21 +1,18 @@ - - Bases de données prises en charge ================================= -Nette prend en charge les bases de données suivantes : - -|* Serveur de base de données |* Nom DSN |* Support dans Core |* Support dans 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 | - +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 - awesome database layer for PHP}} -{{description: Nette Database simplifie considérablement l'obtention de données de la base de données sans avoir à écrire de requêtes SQL. Il exécute des requêtes efficaces et ne transfère pas de données inutiles.}} +{{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 index bf2b375e4d..941ea60671 100644 --- a/database/fr/@left-menu.texy +++ b/database/fr/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** -- [Introduction |guide] -- [Accès SQL |sql way] -- [Explorer |Explorer] -- [Transactions |transactions] -- [Exceptions |exceptions] -- [Réflexion |reflection] -- [Mapping |mapping] -- [Configuration |configuration] +- [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 |en:upgrading] +- [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/configuration.texy b/database/fr/configuration.texy index db34cc8f3c..b90d0e9d24 100644 --- a/database/fr/configuration.texy +++ b/database/fr/configuration.texy @@ -2,66 +2,66 @@ Configuration de la base de données *********************************** .[perex] -Aperçu des options de configuration pour Nette Database. +Aperçu des options de configuration de Nette Database. -Si vous n'utilisez pas l'ensemble du framework, mais uniquement cette bibliothèque, lisez [comment charger la configuration|bootstrap:]. +Si vous n'utilisez pas tout le framework, mais seulement cette bibliothèque, lisez [comment charger la configuration|bootstrap:]. Connexion unique ---------------- -Configuration d'une connexion unique à la base de données : +Configuration d'une seule connexion à la base de données : ```neon database: - # DSN, seule clé obligatoire + # DSN, la seule clé obligatoire dsn: "sqlite:%appDir%/Model/demo.db" user: ... password: ... ``` -Crée les services `Nette\Database\Connection` et `Nette\Database\Explorer`, que nous transmettons généralement par [autowiring |dependency-injection:autowiring], ou par référence à [leur nom |#Services DI]. +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 paramètres : +Autres réglages : ```neon database: - # afficher le panneau de base de données dans la barre Tracy ? - debugger: ... # (bool) la valeur par défaut est true + # afficher le panneau de la base de données dans la Tracy Bar ? + debugger: ... # (bool) activé par défaut si Tracy est actif - # afficher EXPLAIN des requêtes dans la barre Tracy ? - explain: ... # (bool) la valeur par défaut est true + # afficher l'EXPLAIN des requêtes dans la Tracy Bar ? + explain: ... # (bool) true par défaut - # autoriser l'autowiring pour cette connexion ? - autowired: ... # (bool) la valeur par défaut est true pour la première connexion + # activer l'autowiring pour cette connexion ? + autowired: ... # (bool) true par défaut pour la première connexion - # conventions de table : discovered, static ou nom de classe - conventions: discovered # (string) la valeur par défaut est 'discovered' + # conventions de tables : discovered, static, ou nom de classe + conventions: discovered # (string) 'discovered' par défaut options: - # se connecter à la base de données uniquement lorsque c'est nécessaire ? - lazy: ... # (bool) la valeur par défaut est false + # ne se connecter à la base que lorsque c'est nécessaire ? + lazy: ... # (bool) false par défaut - # classe PHP du pilote de base de données + # classe du driver de base de données PHP driverClass: # (string) - # uniquement MySQL : définit sql_mode + # MySQL uniquement : définit sql_mode sqlmode: # (string) - # uniquement MySQL : définit SET NAMES - charset: # (string) la valeur par défaut est 'utf8mb4' + # MySQL uniquement : définit SET NAMES + charset: # (string) 'utf8mb4' par défaut - # uniquement MySQL : convertit TINYINT(1) en bool - convertBoolean: # (bool) la valeur par défaut est false + # 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) la valeur par défaut est false + newDateTime: # (bool) false par défaut - # uniquement Oracle et SQLite : format pour stocker la date - formatDateTime: # (string) la valeur par défaut est 'U' + # Oracle et SQLite uniquement : format de stockage de la date + formatDateTime: # (string) 'U' par défaut ``` -Dans la clé `options`, vous pouvez spécifier d'autres options que vous trouverez dans la [documentation des pilotes PDO |https://www.php.net/manual/en/pdo.drivers.php], comme par exemple : +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: @@ -73,7 +73,7 @@ database: Connexions multiples -------------------- -Dans la configuration, nous pouvons également définir plusieurs connexions de base de données en les divisant en sections nommées : +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: @@ -86,7 +86,7 @@ database: dsn: 'sqlite::memory:' ``` -L'autowiring n'est activé que pour les services de la première section. Cela peut être modifié à l'aide de `autowired: false` ou `autowired: true`. +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 @@ -94,15 +94,15 @@ 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] +| 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` et ensuite `database.another.connection` et `database.another.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 transmettons explicitement les services non autowirés par référence à leur nom : +Nous passons les services non autowirés explicitement, en nous référant à leur nom : ```neon services: diff --git a/database/fr/exceptions.texy b/database/fr/exceptions.texy index a6f5aad0b3..6fa8ef384a 100644 --- a/database/fr/exceptions.texy +++ b/database/fr/exceptions.texy @@ -1,22 +1,25 @@ Exceptions ********** -Nette Database utilise une hiérarchie d'exceptions. La classe de base est `Nette\Database\DriverException`, qui hérite de `PDOException` et offre des fonctionnalités étendues pour travailler avec les erreurs de base de données : +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 spécifique au pilote de base de données. -- La méthode `getSqlState()` renvoie le code SQLSTATE standard. -- Les méthodes `getQueryString()` et `getParameters()` permettent d'obtenir la requête SQL d'origine et ses paramètres. +- 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. -Les exceptions spécialisées suivantes héritent de `DriverException` : +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 -- `ConstraintViolationException` - classe de base pour la violation des contraintes de base de données, dont héritent : - - `ForeignKeyConstraintViolationException` - violation de clé étrangère - - `NotNullConstraintViolationException` - violation de contrainte NOT NULL - - `UniqueConstraintViolationException` - violation de l'unicité de la valeur +- `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. - -Exemple de capture de l'exception `UniqueConstraintViolationException`, qui se produit lorsque nous essayons d'insérer un utilisateur avec un e-mail qui existe déjà dans la base de données (en supposant que la colonne `email` a un index unique). +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 { @@ -29,6 +32,6 @@ try { echo 'Un utilisateur avec cet e-mail existe déjà.'; } catch (Nette\Database\DriverException $e) { - echo 'Une erreur s\'est produite lors de l\'inscription : ' . $e->getMessage(); + echo 'Une erreur est survenue lors de l\'inscription : ' . $e->getMessage(); } ``` diff --git a/database/fr/explorer.texy b/database/fr/explorer.texy index c1862f2c42..01cb9e92f6 100644 --- a/database/fr/explorer.texy +++ b/database/fr/explorer.texy @@ -3,94 +3,94 @@ Database Explorer <div class=perex> -Explorer offre un moyen intuitif et efficace de travailler avec votre base de données. Il gère automatiquement les relations entre les tables et l'optimisation des requêtes, vous permettant de vous concentrer sur votre application. Il fonctionne immédiatement sans aucune configuration. Si vous avez besoin d'un contrôle total sur vos requêtes SQL, vous pouvez utiliser [l'approche SQL |SQL way]. +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 -- Génère des requêtes SQL optimisées qui ne chargent que les données nécessaires -- Permet un accès facile aux données liées sans avoir à écrire de requêtes JOIN -- Fonctionne immédiatement sans aucune configuration ni génération d'entités +- 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> -Vous commencez avec Explorer en appelant la méthode `table()` sur l'objet [api:Nette\Database\Explorer] (les détails sur la connexion se trouvent dans le chapitre [Connexion et configuration |guide#Connexion et configuration]) : +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. Vous pouvez enchaîner d'autres méthodes sur cet objet pour filtrer et trier les résultats. La requête est construite et exécutée seulement au moment où vous commencez à demander des données, par exemple, en parcourant une boucle `foreach`. Chaque ligne est représentée par un objet [ActiveRow |api:Nette\Database\Table\ActiveRow] : +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' + echo $book->title; // affiche la colonne 'title' + echo $book->author_id; // affiche la colonne 'author_id' } ``` -Explorer facilite considérablement le travail avec les [#relations entre les tables]. L'exemple suivant montre avec quelle facilité vous pouvez afficher les données de tables liées (livres et leurs auteurs). Notez que vous n'avez pas besoin d'écrire de requêtes JOIN ; Nette les crée pour vous : +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 sur la table 'author' + echo 'Auteur : ' . $book->author->name; // crée un JOIN vers la table 'author' } ``` -Nette Database Explorer optimise les requêtes pour qu'elles soient aussi efficaces que possible. L'exemple ci-dessus n'exécute que deux requêtes SELECT, que vous traitiez 10 ou 10 000 livres. +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 surveille les colonnes utilisées dans votre code et ne charge que celles-ci depuis la base de données, économisant ainsi des ressources. Ce comportement est entièrement automatique et adaptatif. Si vous modifiez ultérieurement votre code et commencez à utiliser d'autres colonnes, Explorer ajuste automatiquement les requêtes. Vous n'avez rien à configurer ni à vous soucier des colonnes dont vous aurez besoin – laissez Nette s'en charger. +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. -Filtrage et tri -=============== +Filtrer et trier +================ -La classe `Selection` fournit des méthodes pour filtrer et trier la sélection de données. +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 liées par l'opérateur AND -| `whereOr(array $conditions)` | Ajoute un groupe de conditions WHERE liées par l'opérateur OR -| `wherePrimary($value)` | Ajoute une condition WHERE basée sur la clé primaire -| `order($columns, ...$params)` | Définit le tri ORDER BY -| `select($columns, ...$params)` | Spécifie les colonnes à charger -| `limit($limit, $offset = null)` | Limite le nombre de lignes (LIMIT) et définit éventuellement OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Définit la pagination -| `group($columns, ...$params)` | Regroupe les lignes (GROUP BY) -| `having($condition, ...$params)` | Ajoute une condition HAVING pour filtrer les lignes groupées +| `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 enchaînées (ce qu'on appelle une [interface fluide |nette:introduction-to-object-oriented-programming#Interfaces fluides]) : `$table->where(...)->order(...)->limit(...)`. +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 également utiliser une notation spéciale pour accéder aux [données des tables liées |#Interrogation via les tables liées]. +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 protègent les identifiants (noms de tables et de colonnes), prévenant ainsi les injections SQL. Pour un fonctionnement correct, il est nécessaire de respecter quelques règles : +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 SQL, noms de fonctions, procédures, etc. en **MAJUSCULES**. +- É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**. -- Insérez toujours les chaînes de caractères via des **paramètres**. +- Passez toujours les chaînes par des **paramètres**. ```php -where('name = ' . $name); // ⚠️ VULNÉRABILITÉ CRITIQUE : injection SQL -where('name LIKE "%search%"'); // ❌ MAUVAIS : complique la protection automatique des identifiants -where('name LIKE ?', '%search%'); // ✅ CORRECT : valeur insérée via un paramètre +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` ? (mot-clé en minuscule) -where('name LIKE ?', $name); // ✅ CORRECT : génère : `name` LIKE ? -where('LOWER(name) = ?', $value);// ✅ CORRECT : génère : LOWER(`name`) = ? +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 sa gestion intelligente des différents types de valeurs et dans le choix automatique des opérateurs SQL appropriés. +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. -Utilisation de base : +Usage de base : ```php $table->where('id', $value); // WHERE `id` = 123 @@ -98,26 +98,26 @@ $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 des opérateurs appropriés, vous n'avez pas besoin de gérer différents cas spéciaux. Nette s'en charge pour vous : +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 un placeholder (?) sans opérateur : +// Vous pouvez aussi utiliser le placeholder ? sans opérateur : $table->where('id ?', 1); // WHERE `id` = 1 ``` -La méthode gère correctement également les conditions négatives et les tableaux vides : +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 supportée +// $table->where('NOT id ?', $ids); // ATTENTION : cette syntaxe n'est pas prise en charge ``` -Comme paramètre, vous pouvez également passer le résultat d'une autre table (`Selection`) – une sous-requête sera créée : +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`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -Vous pouvez également passer les conditions sous forme de tableau associatif, dont les éléments seront liés par l'opérateur AND : +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`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -Dans le tableau, vous pouvez utiliser des paires `clé => valeur`, et Nette choisira à nouveau automatiquement les opérateurs corrects : +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)) @@ -147,23 +147,23 @@ $table->where([ ]); ``` -Dans le tableau, vous pouvez combiner des expressions SQL avec des placeholders (`?`) et plusieurs paramètres. C'est utile pour des conditions complexes avec des opérateurs définis précisément : +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 passés comme tableau + 'ROUND(score, ?) > ?' => [2, 75.5], // deux paramètres sont passés sous forme de tableau ]); ``` -Les appels multiples à `where()` lient automatiquement les conditions avec l'opérateur AND. +Des appels répétés à `where()` combinent automatiquement les conditions par AND. whereOr(array $parameters): static .[method] -------------------------------------------- -Similaire à `where()`, cette méthode ajoute des conditions, mais les lie avec l'opérateur OR : +Comme `where()`, cette méthode ajoute des conditions, mais les combine par OR : ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -Ici aussi, vous pouvez utiliser des expressions plus complexes : +Des expressions plus complexes peuvent aussi être utilisées ici : ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -187,7 +187,7 @@ $table->whereOr([ wherePrimary(mixed $key): static .[method] ------------------------------------------ -Ajoute une condition pour la clé primaire de la table. +Ajoute une condition sur la clé primaire de la table : ```php // WHERE `id` = 123 @@ -197,7 +197,7 @@ $table->wherePrimary(123); $table->wherePrimary([1, 2, 3]); ``` -Si la table a une clé primaire composite (par ex. `foo_id`, `bar_id`), passez-la comme un tableau associatif : +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 @@ -214,7 +214,7 @@ $table->wherePrimary([ order(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Détermine l'ordre dans lequel les lignes seront retournées. Nous pouvons trier par une ou plusieurs colonnes, par ordre décroissant ou croissant, ou selon une expression personnalisée : +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` @@ -227,14 +227,14 @@ $table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC select(string $columns, ...$parameters): static .[method] --------------------------------------------------------- -Spécifie les colonnes à retourner de la base de données. Par défaut, Nette Database Explorer ne retourne que les colonnes réellement utilisées dans le code. Nous utilisons donc la méthode `select()` dans les cas où nous avons besoin de retourner des expressions spécifiques : +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 de `AS` sont alors accessibles comme propriétés de l'objet `ActiveRow` : +Les alias définis à l'aide d'`AS` sont ensuite accessibles comme propriétés de l'objet `ActiveRow` : ```php foreach ($table as $row) { @@ -246,24 +246,24 @@ foreach ($table as $row) { limit(?int $limit, ?int $offset = null): static .[method] --------------------------------------------------------- -Limite le nombre de lignes retournées (LIMIT) et permet éventuellement de définir un offset : +Limite le nombre de lignes renvoyées (LIMIT) et permet éventuellement de fixer un décalage : ```php -$table->limit(10); // LIMIT 10 (retourne les 10 premières lignes) +$table->limit(10); // LIMIT 10 (renvoie les 10 premières lignes) $table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` -Pour la pagination, il est préférable d'utiliser la méthode `page()`. +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. Accepte le numéro de page (compté à partir de 1) et le nombre d'éléments par page. Il est possible de passer en option une référence à une variable dans laquelle sera stocké le nombre total de pages : +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); +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); echo "Nombre total de pages : $numOfPages"; ``` @@ -271,7 +271,7 @@ echo "Nombre total de pages : $numOfPages"; group(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Regroupe les lignes selon les colonnes spécifiées (GROUP BY). Est généralement utilisé en conjonction avec des fonctions d'agrégation : +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 @@ -283,7 +283,7 @@ $table->select('category_id, COUNT(*) AS count') having(string $having, ...$parameters): static .[method] -------------------------------------------------------- -Définit une condition pour filtrer les lignes groupées (HAVING). Peut être utilisé en conjonction avec la méthode `group()` et les fonctions d'agrégation : +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 @@ -293,31 +293,31 @@ $table->select('category_id, COUNT(*) AS count') ``` -Lecture des données -=================== +Lire les données +================ -Pour lire les données de la base de données, vous disposez de plusieurs méthodes utiles : +Pour lire les données de la base, plusieurs méthodes utiles sont disponibles : .[language-php] -| `foreach ($table as $key => $row)` | Itère sur toutes les lignes. `$key` est la valeur de la clé primaire, `$row` est l'objet `ActiveRow`. -| `$row = $table->get($key)` | Retourne une seule ligne identifiée par sa clé primaire. -| `$row = $table->fetch()` | Retourne la ligne suivante du résultat. -| `$array = $table->fetchPairs($key = null, $value = null)` | Crée un tableau associatif à partir des résultats. -| `$array = $table->fetchAll()` | Retourne toutes les lignes sous forme de tableau d'objets `ActiveRow`. -| `count($table)` | Retourne le nombre de lignes dans l'objet `Selection` (si déjà chargées) ou exécute `COUNT(*)` si non chargées. +| `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 conçu pour la lecture seule. Cela signifie que vous ne pouvez pas modifier directement les valeurs de ses propriétés. Cette restriction garantit la cohérence des données et empêche les effets secondaires inattendus. Les données sont chargées depuis la base de données, et toute modification doit être effectuée explicitement (par exemple via la méthode `update()`). +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` - itération sur toutes les lignes -------------------------------------------- +`foreach` - parcourir toutes les lignes +--------------------------------------- -La manière la plus simple d'exécuter une requête et d'obtenir les lignes est d'itérer sur l'objet `Selection` avec une boucle `foreach`. Cela déclenche automatiquement l'exécution de la requête SQL. +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 contient la valeur de la clé primaire, $book est un objet ActiveRow + // $key est la valeur de la clé primaire, $book est un ActiveRow echo "$book->title ({$book->author->name})"; } ``` @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -Exécute la requête SQL pour récupérer une seule ligne par sa clé primaire et la retourne sous forme d'objet `ActiveRow`, ou `null` si la ligne n'existe pas. +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); // Retourne ActiveRow avec l'ID 123 ou null +$book = $explorer->table('book')->get(123); // renvoie l'ActiveRow d'ID 123 ou null if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -Retourne la ligne suivante du jeu de résultats sous forme d'objet `ActiveRow` et déplace le pointeur interne sur la suivante. S'il n'y a plus de lignes, retourne `null`. +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'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Retourne les résultats sous forme de tableau associatif. Le premier argument `$key` spécifie le nom de la colonne à utiliser comme clé dans le tableau. Le second argument `$value` spécifie le nom de la colonne à utiliser comme valeur : +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 vous ne spécifiez que le premier paramètre `$key`, la valeur de chaque élément du tableau sera la ligne entière (objet `ActiveRow`) : +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 dupliquées, la valeur de la dernière ligne écrasera les précédentes. Si vous utilisez `null` comme `$key`, le tableau sera indexé numériquement à partir de zéro (évitant ainsi les collisions) : +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'); @@ -377,24 +377,24 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternativement, vous pouvez passer un callback comme unique paramètre. Ce callback sera appelé pour chaque ligne et devra retourner soit la valeur à ajouter au tableau, soit une paire `[clé, valeur]`. +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})"); -// ['Premier livre (Jan Novák)', ...] +// ['First Book (John Novak)', ...] -// Le callback peut aussi retourner un tableau [clé, valeur] : +// Le callback peut aussi renvoyer un tableau formant une paire clé & valeur : $titles = $explorer->table('book') ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Premier livre' => 'Jan Novák', ...] +// ['First Book' => 'John Novak', ...] ``` fetchAll(): array .[method] --------------------------- -Retourne toutes les lignes du résultat sous forme de tableau d'objets `ActiveRow`. Les clés du tableau sont les valeurs des clés primaires des lignes. +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(); @@ -405,21 +405,21 @@ $allBooks = $explorer->table('book')->fetchAll(); count(): int .[method] ---------------------- -La méthode `count()` sans argument retourne le nombre de lignes dans l'objet `Selection` (si les données ont déjà été chargées) ou exécute une requête `SELECT COUNT(*)` pour obtenir le nombre total de lignes correspondant aux conditions définies : +La méthode `count()` sans paramètre renvoie le nombre de lignes de l'objet `Selection` : ```php -$selection = $explorer->table('book')->where('available', true); -$count = $selection->count(); -$count = count($selection); // Alternative, même comportement +$table->where('category', 1); +$count = $table->count(); +$count = count($table); // variante ``` -Attention, `count()` avec un argument exécute une fonction d'agrégation `COUNT()` dans la base de données. +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 PHP standard, où les clés sont les noms des colonnes et les valeurs sont les données correspondantes. +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); @@ -431,33 +431,33 @@ $bookArray = $book->toArray(); Agrégation ========== -La classe `Selection` fournit des méthodes pour effectuer facilement des fonctions d'agrégation SQL (COUNT, SUM, MIN, MAX, AVG, etc.). +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 correspondant à l'expression. -| `min($expr)` | Retourne la valeur minimale de la colonne/expression. -| `max($expr)` | Retourne la valeur maximale de la colonne/expression. -| `sum($expr)` | Retourne la somme des valeurs de la colonne/expression. -| `aggregation($function, $groupFunction = null)` | Permet d'effectuer une fonction d'agrégation SQL arbitraire (ex: `AVG()`, `GROUP_CONCAT()`). +| `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 retourne le résultat. La méthode est utilisée pour compter le nombre de lignes correspondant à une condition ou une expression : +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` ``` -Attention, [#count()] sans argument retourne le nombre de lignes dans l'objet `Selection` (si déjà chargé) ou exécute `COUNT(*)` si les données ne sont pas chargées. +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()` retournent la valeur minimale et maximale dans la colonne ou l'expression spécifiée pour les lignes sélectionnées : +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 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -Retourne la somme des valeurs dans la colonne ou l'expression spécifiée pour les lignes sélectionnées : +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 @@ -478,24 +478,24 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -Permet d'exécuter n'importe quelle fonction d'agrégation SQL. +Permet d'exécuter n'importe quelle fonction d'agrégation. ```php -// prix moyen des produits dans la catégorie +// prix moyen des produits d'une catégorie $avgPrice = $products->where('category_id', 1) ->aggregation('AVG(price)'); -// concatène les étiquettes du produit en une seule chaîne +// 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 vous avez besoin d'agréger des résultats qui proviennent déjà eux-mêmes d'une fonction d'agrégation et d'un regroupement (par ex., calculer la somme de `SUM(valeur)` sur des lignes groupées), spécifiez comme deuxième argument `$groupFunction` la fonction d'agrégation à appliquer à ces résultats intermédiaires : +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. @@ -504,40 +504,40 @@ $totalPrice = $products->select('category_id, SUM(price * stock) AS category_tot ->aggregation('SUM(category_total)', 'SUM'); ``` -Dans cet exemple, nous calculons d'abord le prix total des produits dans chaque catégorie (`SUM(price * stock) AS category_total`) et regroupons les résultats par `category_id`. Ensuite, nous utilisons `aggregation('SUM(category_total)', 'SUM')` pour additionner ces sous-totaux `category_total`. Le deuxième argument `'SUM'` indique que la fonction `SUM` doit être appliquée aux résultats intermédiaires (`category_total`). +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. -Insertion, Mise à jour & Suppression -==================================== +Insert, Update & Delete +======================= -Nette Database Explorer simplifie l'insertion, la mise à jour et la suppression de données. Toutes les méthodes mentionnées lèvent une exception `Nette\Database\DriverException` en cas d'erreur de base de données. +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 un ou plusieurs nouveaux enregistrements dans la table. +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 (par exemple, `Nette\Utils\ArrayHash` utilisé par les [formulaires |forms:]), où les clés correspondent aux noms des colonnes de la table. +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 retourne un objet `ActiveRow` représentant la ligne insérée. Cet objet est rechargé depuis la base de données pour refléter les éventuelles modifications effectuées au niveau de la base de données (triggers, valeurs par défaut, auto-incrément). Cela garantit la cohérence des données. Si la table n'a pas de clé primaire unique, la méthode retourne les données transmises sous forme de tableau. +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 de ActiveRow et contient les données complètes de la ligne insérée, -// y compris l'ID généré automatiquement (si applicable) et les éventuelles modifications par triggers. +// $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 définie par un trigger ou une valeur par défaut +echo $row->created_at; // Affiche l'heure de création si elle est définie par un trigger ``` **Insertion de plusieurs enregistrements à la fois :** -Passez un tableau de tableaux associatifs ou d'objets itérables. La méthode `insert()` exécute alors une seule requête SQL pour insérer toutes les lignes. Dans ce cas, elle retourne le nombre de lignes insérées. +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([ @@ -551,10 +551,10 @@ $insertedRows = $explorer->table('users')->insert([ ], ]); // INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows sera 2 +// $insertedRows vaudra 2 ``` -Comme paramètre, on peut également passer un objet `Selection` avec une sélection de données. +Un objet `Selection` contenant une sélection de données peut aussi être passé en paramètre. ```php $newUsers = $explorer->table('potential_users') @@ -564,16 +564,16 @@ $newUsers = $explorer->table('potential_users') $insertedRows = $explorer->table('users')->insert($newUsers); ``` -**Insertion de valeurs spéciales :** +**Insertion de valeurs particulières :** -Comme valeurs, nous pouvons également passer des fichiers, des objets DateTime ou des littéraux SQL : +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, // Converti au format de base de données - 'avatar' => fopen('image.jpg', 'rb'), // Insère le contenu binaire du fichier - 'uuid' => $explorer::literal('UUID()'), // Appelle la fonction SQL UUID() + '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() ]); ``` @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -Met à jour les lignes de la table correspondant au filtre défini précédemment (par `where()`). Retourne le nombre de lignes réellement modifiées. +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 (par exemple, `ArrayHash` des [formulaires |forms:]), où les clés correspondent aux noms des colonnes : +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') @@ -595,14 +595,14 @@ $affected = $explorer->table('users') // UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Pour incrémenter ou décrémenter des valeurs numériques, vous pouvez utiliser les opérateurs `+=` et `-=` dans les clés du tableau : +Pour modifier des valeurs numériques, vous pouvez utiliser les opérateurs `+=` et `-=` : ```php $explorer->table('users') ->where('id', 10) ->update([ - 'points+=' => 1, // augmente la valeur de la colonne 'points' de 1 - 'coins-=' => 1, // diminue la valeur de la colonne 'coins' de 1 + '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 ``` @@ -611,7 +611,7 @@ $explorer->table('users') Selection::delete(): int .[method] ---------------------------------- -Supprime les lignes de la table selon le filtre spécifié. Retourne le nombre de lignes supprimées. +Supprime les lignes de la table selon le filtre indiqué. Renvoie le nombre de lignes supprimées. ```php $count = $explorer->table('users') @@ -621,111 +621,111 @@ $count = $explorer->table('users') ``` .[caution] -Lors de l'appel de `update()` et `delete()`, n'oubliez pas de spécifier les lignes à modifier/supprimer à l'aide de `where()`. Si vous n'utilisez pas `where()`, l'opération s'effectuera sur toute la table ! +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 de base de données représentée par l'objet `ActiveRow`. Accepte comme paramètre un itérable (tableau associatif ou objet) avec les données à mettre à jour (les clés sont les noms des colonnes). Pour incrémenter/décrémenter des valeurs numériques, utilisez les opérateurs `+=` et `-=` : +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 l'exécution de la mise à jour, les propriétés de l'objet `ActiveRow` sont automatiquement mises à jour avec les nouvelles valeurs (elles sont rechargées depuis la base de données pour refléter d'éventuels triggers). La méthode retourne `true` si une modification a réellement eu lieu, `false` sinon. +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', // augmentons le nombre de vues + 'views += 1', // incrémente le nombre de vues ]); -echo $article->views; // Affiche le nombre actuel de vues +echo $article->views; // Affiche le nombre de vues actuel ``` -Cette méthode met à jour uniquement la ligne spécifique représentée par l'objet `ActiveRow`. Pour une mise à jour en masse de plusieurs lignes, utilisez la méthode [#`Selection::update()`]. +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() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -Supprime la ligne de la base de données représentée par l'objet `ActiveRow`. +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 avec l'ID 1 +$book->delete(); // Supprime le livre d'ID 1 ``` -Cette méthode supprime uniquement la ligne spécifique représentée par l'objet `ActiveRow`. Pour une suppression en masse de plusieurs lignes, utilisez la méthode [#`Selection::delete()`]. +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 dans plusieurs tables et reliées entre elles par des clés étrangères. Nette Database Explorer apporte une manière révolutionnaire de travailler avec ces relations - sans écrire de requêtes JOIN et sans avoir besoin de configurer ou de générer quoi que ce soit. +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 l'exemple d'une base de données de livres ([vous le trouverez sur GitHub |https://github.com/nette-examples/books]). Dans la base de données, nous avons les tables : +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` - écrivains et traducteurs (colonnes `id`, `name`, `web`, `born`) -- `book` - livres (colonnes `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - étiquettes (colonnes `id`, `name`) -- `book_tag` - table de liaison entre les livres et les étiquettes (colonnes `book_id`, `tag_id`) +- `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 *** +[* db-schema-1-.webp *] *** Structure de la base de données utilisée dans les exemples .<> -Dans notre exemple de base de données de livres, nous trouvons plusieurs types de relations (bien que le modèle soit simplifié par rapport à la réalité) : +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** un tome suivant -- Plusieurs-à-plusieurs M:N – un livre **peut avoir plusieurs** étiquettes et une étiquette peut être attribuée à **plusieurs** livres +- **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 existe toujours une table parente et une table enfant. Par exemple, dans la relation entre l'auteur et le livre, la table `author` est parente et `book` est enfant - on peut imaginer que le livre "appartient" toujours à un auteur. Cela se reflète également dans la structure de la base de données : la table enfant `book` contient une clé étrangère `author_id`, qui référence la table parente `author`. +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 d'afficher les livres y compris les noms de leurs auteurs, nous avons deux options. Soit nous obtenons les données avec une seule requête SQL à l'aide de JOIN : +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 +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Soit nous chargeons les données en deux étapes - d'abord les livres, puis leurs auteurs - et ensuite nous les assemblons en PHP : +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 obtenus +SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs des auteurs des livres sélectionnés ``` -La deuxième approche est en fait plus efficace, même si cela peut être surprenant. Les données sont chargées une seule fois et peuvent être mieux utilisées dans le cache. C'est précisément de cette manière que fonctionne Nette Database Explorer - tout est géré en arrière-plan et vous offre une API élégante : +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 l'enregistrement de la table 'author' + echo 'écrit par : ' . $book->author->name; // $book->author est un enregistrement de la table 'author' echo 'traduit par : ' . $book->translator?->name; } ``` -Accès à la table parente (Relation N:1) ---------------------------------------- +Accéder à la table parente +-------------------------- -Accéder à la table parente (la table référencée par une clé étrangère) est très simple. Cela correspond aux relations comme "un livre a un auteur" ou "un livre peut avoir un traducteur". Vous obtenez l'enregistrement lié via une propriété dynamique de l'objet `ActiveRow`. Le nom de cette propriété correspond au nom de la colonne contenant la clé étrangère, sans le suffixe `_id` (par convention) : +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 selon la colonne author_id -echo $book->translator?->name; // trouve le traducteur selon translator_id +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 ``` -Lorsque vous accédez à la propriété `$book->author`, Explorer recherche dans la table `book` une colonne dont le nom est dérivé de `author` (généralement `author_id`). Il utilise la valeur de cette colonne pour charger l'enregistrement correspondant dans la table `author` et le retourne sous forme d'objet `ActiveRow`. Le même mécanisme s'applique pour `$book->translator` via la colonne `translator_id`. Comme la colonne `translator_id` peut contenir `null`, nous utilisons l'opérateur `?->` dans le code. +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 alternative est la méthode `ref()`, qui prend deux arguments : le nom de la table cible et (optionnellement) le nom de la colonne de liaison. Elle retourne l'instance `ActiveRow` ou `null` : +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; // liaison à l'auteur -echo $book->ref('author', 'translator_id')->name; // liaison au traducteur +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 si le nom de la propriété dynamique entre en conflit avec un nom de colonne existant dans la table (`author` dans cet exemple). Sinon, l'accès via la propriété est généralement plus lisible et recommandé. +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. Lorsque vous parcourez des livres dans une boucle et accédez à leurs enregistrements liés (auteurs, traducteurs), Explorer ne génère pas une requête distincte pour chaque livre. Au lieu de cela, il effectue **une seule requête SELECT par type de relation** pour récupérer tous les enregistrements liés nécessaires en une seule fois (technique connue sous le nom de "eager loading" implicite), réduisant considérablement la charge de la base de données : +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'); @@ -736,22 +736,22 @@ foreach ($books as $book) { } ``` -Ce code n'exécutera que trois requêtes rapides, quel que soit le nombre de livres : +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)); -- id de la colonne author_id des livres sélectionnés -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id de la colonne translator_id des livres sélectionnés +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 détection de la colonne de liaison est gérée par l'implémentation des [Conventions |api:Nette\Database\Conventions]. Nous recommandons l'utilisation de [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], qui analyse les clés étrangères définies dans votre base de données et permet de travailler facilement avec les relations existantes. +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ès à la table enfant ------------------------ +Accéder à la table enfant +------------------------- -L'accès à la table enfant fonctionne dans le sens inverse. Nous demandons maintenant *quels livres cet auteur a-t-il écrits* ou *traduits*. Pour ce type de requête, nous utilisons la méthode `related()`, qui retourne une `Selection` avec les enregistrements liés. Regardons un exemple : +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); @@ -761,28 +761,28 @@ foreach ($author->related('book.author_id') as $book) { echo "A écrit : $book->title"; } -// Affiche tous les livres que l'auteur a traduits +// 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 liaison comme un seul argument avec la notation par points ou comme deux arguments séparés : +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 argument +$author->related('book.translator_id'); // un seul argument $author->related('book', 'translator_id'); // deux arguments ``` -Explorer peut détecter automatiquement la colonne de liaison correcte en fonction du nom de la table parente. Dans ce cas, la liaison se fait via la colonne `book.author_id`, car le nom de la table source est `author` : +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 ``` -S'il existait plusieurs liaisons possibles, Explorer lèverait une exception [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +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()` également lors du parcours de plusieurs enregistrements dans une boucle, et Explorer optimise également automatiquement les requêtes dans ce cas : +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'); @@ -794,53 +794,53 @@ foreach ($authors as $author) { } ``` -Ce code ne génère que deux requêtes SQL rapides : +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)); -- id des auteurs sélectionnés +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs des auteurs sélectionnés ``` -Relation Plusieurs-à-plusieurs +Relation plusieurs-à-plusieurs ------------------------------ -Pour une relation plusieurs-à-plusieurs (M:N), l'existence d'une table de liaison est nécessaire (dans notre cas `book_tag`), qui contient deux colonnes avec des clés étrangères (`book_id`, `tag_id`). Chacune de ces colonnes référence la clé primaire de l'une des tables liées. Pour obtenir les données liées, nous obtenons d'abord les enregistrements de la table de liaison à l'aide de `related('book_tag')` et continuons ensuite vers les données cibles : +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 étiquettes attribuées au livre +// affiche les noms des tags attribués au livre foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // affiche le nom de l'étiquette via la table de liaison + echo $bookTag->tag->name; // affiche le nom du tag via la table de jonction } $tag = $explorer->table('tag')->get(1); -// ou inversement : affiche les noms des livres marqués avec cette étiquette +// 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 nom du livre + echo $bookTag->book->title; // affiche le titre du livre } ``` -Explorer optimise à nouveau les requêtes SQL en une forme efficace : +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, ...)); -- id des livres sélectionnés -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id des étiquettes trouvées dans book_tag +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 ``` -Interrogation via les tables liées ----------------------------------- +Requêter à travers les tables liées +----------------------------------- -Dans les méthodes `where()`, `select()`, `order()` et `group()`, nous pouvons utiliser des notations spéciales pour accéder aux colonnes d'autres tables. Explorer crée automatiquement les JOIN nécessaires. +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. -**Notation par points** (`table_parente.colonne`) est utilisée pour la relation 1:N du point de vue de la table enfant : +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 l'auteur a un nom commençant par 'Jon' +// 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 @@ -850,54 +850,54 @@ $books->order('author.name DESC'); $books->select('book.title, author.name'); ``` -**Notation par deux-points** (`:table_enfant.colonne`) est utilisée pour la relation 1:N du point de vue de la table parente : +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 qui ont écrit un livre avec 'PHP' dans le titre +// Trouve les auteurs ayant écrit un livre avec 'PHP' dans le titre $authors->where(':book.title LIKE ?', '%PHP%'); -// Compte le nombre de livres pour chaque auteur +// 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 avec la clé étrangère n'est pas spécifiée. Explorer détecte automatiquement la colonne correcte en fonction du nom de la table parente. Dans ce cas, la liaison se fait via la colonne `book.author_id`, car le nom de la table source est `author`. S'il existait plusieurs liaisons possibles, Explorer lèverait une exception [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +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 liaison peut être explicitement indiquée entre parenthèses : +La colonne de jointure peut être indiquée explicitement entre parenthèses : ```php -// Trouve les auteurs qui ont traduit un livre avec 'PHP' dans le titre +// Trouve les auteurs ayant traduit un livre avec 'PHP' dans le titre $authors->where(':book(translator_id).title LIKE ?', '%PHP%'); ``` -Les notations peuvent être enchaînées pour accéder via plusieurs tables : +Les notations peuvent être chaînées pour accéder aux données à travers plusieurs tables : ```php -// Trouve les auteurs de livres marqués avec l'étiquette 'PHP' +// Trouve les auteurs de livres portant le tag 'PHP' $authors->where(':book:book_tag.tag.name', 'PHP') ->group('author.id'); ``` -Extension des conditions pour JOIN ----------------------------------- +Étendre les conditions du JOIN +------------------------------ -La méthode `joinWhere()` étend les conditions qui sont spécifiées lors de la liaison des tables en SQL après le mot-clé `ON`. +La méthode `joinWhere()` étend les conditions indiquées lors de la jointure des tables en SQL, après le mot-clé `ON`. -Supposons que nous voulions trouver les livres traduits par un traducteur spécifique : +Disons que nous voulons trouver les livres traduits par un traducteur précis : ```php -// Trouve les livres traduits par le traducteur nommé 'David' +// 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 `joinWhere()`, nous pouvons utiliser les mêmes constructions que dans la méthode `where()` - opérateurs, points d'interrogation, tableaux de valeurs ou expressions SQL. +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 des requêtes plus complexes avec plusieurs JOIN, nous pouvons définir des alias de tables : +Pour les requêtes plus complexes comportant plusieurs JOINs, vous pouvez définir des alias de tables : ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // 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 dans la clause `ON` lors de la liaison des tables. +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 index 3bed959c52..77f062dcca 100644 --- a/database/fr/guide.texy +++ b/database/fr/guide.texy @@ -2,30 +2,30 @@ Nette Database ************** .[perex] -Nette Database est une couche de base de données puissante et élégante pour PHP, mettant l'accent sur la simplicité et les fonctionnalités intelligentes. Elle offre deux façons de travailler avec la base de données - [Explorer |Explorer] pour un développement rapide d'applications, ou [l'accès SQL |SQL way] pour travailler directement avec les requêtes. +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> -[Accès SQL |SQL way] -==================== -- Requêtes paramétrées sécurisées -- Contrôle précis sur la forme des requêtes SQL -- Lorsque vous écrivez des requêtes complexes avec des fonctionnalités avancées -- Vous optimisez les performances en utilisant des fonctions SQL spécifiques +[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] +[Explorer |explorer] ==================== -- Vous développez rapidement sans écrire de SQL -- Travail intuitif avec les relations entre les tables -- Vous apprécierez l'optimisation automatique des requêtes -- Convient pour un travail rapide et confortable avec la base de données +- 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> @@ -35,45 +35,45 @@ Nette Database est une couche de base de données puissante et élégante pour P Installation ============ -Téléchargez et installez la bibliothèque à l'aide de l'[outil Composer |best-practices:composer] : +Téléchargez et installez la bibliothèque à l'aide de [Composer|best-practices:composer] : ```shell composer require nette/database ``` -Bases de données supportées -=========================== +Bases de données prises en charge +================================= -Nette Database supporte les bases de données suivantes : +Nette Database prend en charge les bases de données suivantes : -|* Serveur de base de données |* Nom DSN |* Support dans Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | OUI -| PostgreSQL (>= 9.0) | pgsql | OUI -| Sqlite 3 (>= 3.8) | sqlite | OUI -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | OUI -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - +|* 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 de la base de données -==================================== +Deux approches du travail avec la base de données +================================================= -Nette Database vous donne le choix : écrire directement des requêtes SQL (accès SQL) ou laisser Explorer les générer automatiquement. Voyons comment les deux approches résolvent les mêmes tâches : +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 : -[Accès SQL|sql way] - Requêtes SQL +[Approche SQL|sql-way] - requêtes SQL ```php -// Insertion d'un enregistrement +// Insère un enregistrement $database->query('INSERT INTO books', [ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// Récupération des auteurs actifs avec le nombre de livres +// Récupère des enregistrements : les auteurs des livres $result = $database->query(' SELECT authors.*, COUNT(books.id) AS books_count FROM authors @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// Affichage (problème N+1 : génère N requêtes supplémentaires pour les livres) +// Affichage (pas optimal, génère N requêtes supplémentaires) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -98,21 +98,21 @@ foreach ($result as $author) { } ``` -[Approche Explorer|explorer] - Génération automatique de SQL +[Approche Explorer|explorer] - génération automatique du SQL ```php -// Insertion d'un enregistrement +// Insère un enregistrement $database->table('books')->insert([ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// Récupération des auteurs actifs +// Récupère des enregistrements : les auteurs des livres $authors = $database->table('authors') ->where('active', 1); -// Affichage (optimisé : génère seulement 2 requêtes au total) +// Affichage (génère automatiquement seulement 2 requêtes optimisées) foreach ($authors as $author) { $books = $author->related('books') ->order('published_at DESC'); @@ -125,9 +125,9 @@ foreach ($authors as $author) { } ``` -L'approche Explorer génère et optimise automatiquement les requêtes SQL. Dans l'exemple ci-dessus, l'accès SQL souffre du problème "N+1" (une requête pour les auteurs, puis une requête par auteur pour ses livres), tandis qu'Explorer optimise cela en seulement deux requêtes au total : une pour les auteurs et une pour tous leurs livres associés. +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. -Vous pouvez combiner librement les deux approches dans votre application selon vos besoins. +Les deux approches peuvent être librement combinées dans votre application, selon les besoins. Connexion et configuration @@ -139,9 +139,9 @@ Pour vous connecter à la base de données, il suffit de créer une instance de $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]. En cas d'échec de connexion, une exception `Nette\Database\ConnectionException` est levée. +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`. -Cependant, la méthode recommandée est d'utiliser la [configuration de l'application |configuration] (fichier NEON). Ajoutez simplement une section `database`, et Nette DI créera automatiquement les services nécessaires (`Connection` et `Explorer`), ainsi que le panneau de base de données dans la barre de débogage [Tracy |tracy:]. +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: @@ -150,7 +150,7 @@ database: password: password ``` -Ensuite, vous [obtenez l'objet de connexion ou l'Explorer en tant que service via l'injection de dépendances |dependency-injection:passing-dependencies] : +L'objet de connexion peut ensuite être [obtenu comme service depuis le conteneur DI |dependency-injection:passing-dependencies], par exemple : ```php class Model @@ -163,22 +163,22 @@ class Model } ``` -Consultez la section sur la [configuration de la base de données |configuration] pour plus de détails. +Plus d'informations sur la [configuration de la base de données|configuration]. Création manuelle de l'Explorer ------------------------------- -Si vous n'utilisez pas Nette DI, vous pouvez créer manuellement une instance de `Nette\Database\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 pour le cache, implémente Nette\Caching\Storage, par ex. : +// stockage du cache, implémente Nette\Caching\Storage, par exemple : $storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// s'occupe de la réflexion de la structure de la base de données +// 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 mappage des noms de tables, colonnes et clés étrangères +// 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); ``` @@ -187,18 +187,18 @@ $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $ Gestion de la connexion ======================= -Lors de la création de l'objet `Connection`, la connexion est établie automatiquement. Si vous souhaitez différer la connexion, utilisez le mode lazy - activez-le dans la [configuration |configuration] en définissant `lazy`, ou comme ceci : +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 la connexion si elle n'existe pas encore, et peut lever une exception `Nette\Database\ConnectionException`. -- `disconnect()` déconnecte la connexion actuelle à la base de données. -- `reconnect()` effectue une déconnexion puis une reconnexion à la base de données. Cette méthode peut également lever une exception `Nette\Database\ConnectionException`. +- `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`. -De plus, vous pouvez surveiller les événements liés à la connexion en utilisant l'événement `onConnect`, qui est un tableau de callbacks appelés après l'établissement de la connexion à la base de données. +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 @@ -207,10 +207,12 @@ $database->onConnect[] = function($database) { }; ``` +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 s'active automatiquement dans la barre de débogage, affichant toutes les requêtes exécutées, leurs paramètres, leur temps d'exécution et l'endroit dans le code où elles ont été appelées. +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/mapping.texy b/database/fr/mapping.texy deleted file mode 100644 index 2ef1d96e1b..0000000000 --- a/database/fr/mapping.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 en types PHP correspondants. - - -Date et heure -------------- - -Les données temporelles sont converties en objets `Nette\Utils\DateTime`. Si vous souhaitez que les données temporelles soient converties en objets immuables `Nette\Database\DateTime`, définissez l'option `newDateTime` sur true 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 définissons `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 `float` selon le type de 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 ---------------------------- - -Avec la méthode `setRowNormalizer(?callable $normalizer)`, vous pouvez définir votre propre fonction pour transformer les lignes de la base de données. Ceci est utile, par exemple, pour la conversion automatique des types de données. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // la conversion de type a lieu ici - return $row; -}); -``` diff --git a/database/fr/reflection.texy b/database/fr/reflection.texy index c594ffc8e1..9fbee322a8 100644 --- a/database/fr/reflection.texy +++ b/database/fr/reflection.texy @@ -2,36 +2,36 @@ Réflexion de la structure ************************* .{data-version:3.2.1} -Nette Database fournit des outils pour l'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 flexibles travaillant avec la base de données ou des outils de base de données généraux. +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. -Nous obtenons l'objet de réflexion à partir de l'instance de connexion à la base de données : +L'objet de réflexion s'obtient depuis l'instance de connexion à la base de données : ```php $reflection = $database->getReflection(); ``` -Obtention des tables +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 des noms de toutes les tables +// Liste les noms de toutes les tables foreach ($reflection->tables as $name => $table) { echo $name . "\n"; } ``` -Deux autres méthodes sont également disponibles : +Deux méthodes supplémentaires sont disponibles : ```php -// Vérification de l'existence de la table +// Vérifie l'existence d'une table if ($reflection->hasTable('users')) { echo "La table users existe"; } -// Renvoie l'objet table ; lève une exception s'il n'existe pas +// Renvoie l'objet de la table ; lève une exception si elle n'existe pas $table = $reflection->getTable('users'); ``` @@ -39,31 +39,33 @@ $table = $reflection->getTable('users'); Informations sur la table ------------------------- -La table est représentée par l'objet [Table|api:Nette\Database\Reflection\Table], qui fournit les propriétés en lecture seule suivantes : +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 incluant le schéma (si existant) -- `$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 +- `$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 des colonnes, où la clé est le nom de la colonne et la valeur est une instance de [Column|api:Nette\Database\Reflection\Column] avec ces propriétés : +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 de base de données natif -- `$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 spécifiques au système de base de données donné +- `$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) { @@ -79,15 +81,15 @@ 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 formant l'index -- `$unique: bool` – si l'index est unique -- `$primary: bool` – s'il s'agit de la clé primaire -- `$name: ?string` – nom de l'index +- `$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 peut être obtenue à l'aide de la propriété `primaryKey`, qui renvoie soit un objet `Index`, soit `null` si la table n'a pas de clé primaire. +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 des index +// 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"; @@ -95,7 +97,7 @@ foreach ($table->indexes as $index) { echo " Unique : " . ($index->unique ? 'Oui' : 'Non') . "\n"; } -// Liste de la clé primaire +// Liste la clé primaire if ($primaryKey = $table->primaryKey) { $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); echo "Clé primaire : $columns\n"; @@ -108,13 +110,13 @@ 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` – 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 +- `$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 des clés étrangères +// 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)); diff --git a/database/fr/security.texy b/database/fr/security.texy index fea9dda5c7..c65a5ca9e5 100644 --- a/database/fr/security.texy +++ b/database/fr/security.texy @@ -3,11 +3,11 @@ Risques de sécurité <div class=perex> -La base de données contient souvent des données sensibles et permet d'effectuer des opérations dangereuses. Pour travailler en toute sécurité avec Nette Database, il est crucial de : +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écurisée et non sécurisée -- Utiliser des requêtes paramétrées -- Valider correctement les données d'entrée +- 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> @@ -15,17 +15,17 @@ La base de données contient souvent des données sensibles et permet d'effectue Qu'est-ce que l'injection SQL ? =============================== -L'injection SQL est le risque de sécurité le plus grave lors du travail avec une base de données. Elle se produit lorsque l'entrée non traitée d'un utilisateur fait partie d'une requête SQL. Un attaquant peut 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 de données -- Contourner l'authentification +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]'"); -// L'attaquant peut entrer une valeur comme : ' OR '1'='1 -// La requête résultante sera : SELECT * FROM users WHERE name = '' OR '1'='1' +// 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 ``` @@ -41,30 +41,30 @@ $table->where("name = '$_GET[name]'"); Requêtes paramétrées ==================== -La défense de base contre l'injection SQL consiste à utiliser des requêtes paramétrées. Nette Database offre plusieurs façons de les utiliser. +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 méthode la plus simple consiste à utiliser des **points d'interrogation comme placeholders** : +La plus simple est d'utiliser des **points d'interrogation comme placeholders** : ```php -// ✅ Requête paramétrée sécurisée +// ✅ Requête paramétrée sûre $database->query('SELECT * FROM users WHERE name = ?', $name); -// ✅ Condition sécurisée dans l'Explorer +// ✅ Condition sûre dans Explorer $table->where('name = ?', $name); ``` -Cela s'applique à 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. +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écurisé +// ✅ INSERT sûr $database->query('INSERT INTO users', [ 'name' => $name, 'email' => $email, ]); -// ✅ INSERT sécurisé dans l'Explorer +// ✅ INSERT sûr dans Explorer $table->insert([ 'name' => $name, 'email' => $email, @@ -75,89 +75,89 @@ $table->insert([ Validation des valeurs des paramètres ===================================== -Les requêtes paramétrées sont la pierre angulaire d'un travail sécurisé avec la base de données. Cependant, les valeurs que nous y insérons doivent passer par plusieurs niveaux de contrôles : +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 de type +Contrôle du type ---------------- -**Le plus important est d'assurer le type de données correct des paramètres** - c'est une condition nécessaire pour une utilisation sécurisée de Nette Database. La base de données suppose que toutes les données d'entrée ont le type de données correct correspondant à la colonne donnée. +**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. -Par exemple, si `$name` dans les exemples précédents était de manière inattendue un tableau au lieu d'une chaîne, Nette Database tenterait d'insérer tous ses éléments dans la requête SQL, ce qui entraînerait une erreur. Par conséquent, **n'utilisez jamais** de données non validées de `$_GET`, `$_POST` ou `$_COOKIE` directement dans les requêtes de base de données. +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. -Contrôle de format ------------------- +Validation du format +-------------------- -Au deuxième niveau, nous vérifions le format des données - par exemple, si les chaînes sont en encodage UTF-8 et si leur longueur correspond à la définition de la colonne, ou si les valeurs numériques sont dans la plage autorisée pour le type de données de la colonne donnée. +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 également compter en partie sur la base de données elle-même - de nombreuses bases de données refuseront les données non valides. Cependant, le comportement peut varier, certaines peuvent tronquer silencieusement les longues chaînes ou couper les nombres hors plage. +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. -Contrôle de domaine -------------------- +Validation propre au domaine +---------------------------- -Le troisième niveau représente les contrôles logiques spécifiques à votre application. Par exemple, vérifier que les valeurs des listes déroulantes correspondent aux options proposées, que les nombres sont dans la plage attendue (par exemple, âge 0-150 ans) ou que les dépendances mutuelles entre les valeurs ont un sens. +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 assurent automatiquement la validation correcte de toutes les entrées -- Utilisez les [Presenters |application:] et spécifiez les types de données pour les paramètres dans les méthodes `action*()` et `render*()` -- Ou implémentez votre propre couche de validation en utilisant des outils PHP standard comme `filter_var()` +- 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 toute sécurité avec les colonnes -============================================== +Travailler en sécurité avec les colonnes +======================================== -Dans la section précédente, nous avons montré comment valider correctement les valeurs des paramètres. Cependant, lors de l'utilisation de tableaux dans les requêtes SQL, nous devons accorder la même attention à leurs clés. +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 traitées +// ❌ CODE DANGEREUX - les clés du tableau ne sont pas assainies $database->query('INSERT INTO users', $_POST); ``` -Pour les commandes INSERT et UPDATE, il s'agit d'une faille de sécurité critique - un attaquant peut insérer ou modifier n'importe quelle colonne dans la base de données. Il pourrait, par exemple, définir `is_admin = 1` ou insérer des données arbitraires dans des colonnes sensibles (vulnérabilité dite Mass Assignment). +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 traitées +// ❌ 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 utiliser cette approche pour découvrir systématiquement les salaires des employés. Il commence, par exemple, par une requête sur les salaires supérieurs à 100 000, puis inférieurs à 50 000, et en réduisant progressivement la plage, il peut révéler les salaires approximatifs de tous les employés. Ce type d'attaque est appelé énumération SQL. +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 encore [beaucoup plus flexibles |explorer#where] et supportent dans les clés et les valeurs des expressions SQL incluant des opérateurs et des fonctions. Cela donne à l'attaquant la possibilité d'effectuer une injection 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 injecter son propre SQL +// ❌ 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 avec `0)`, ajoute son propre `SELECT` en utilisant `UNION` pour obtenir des données sensibles de la table `users` et ferme la requête syntaxiquement correcte avec `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 toute sécurité avec les noms de colonnes, nous avons besoin d'un mécanisme qui garantit que l'utilisateur ne peut travailler qu'avec les colonnes autorisées et ne peut pas ajouter les siennes. 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 peut toujours trouver une nouvelle façon d'écrire un nom de colonne dangereux que nous n'avions pas prévu. +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é. -Par conséquent, il est beaucoup plus sûr d'inverser la logique et de définir une liste explicite des colonnes autorisées (liste blanche) : +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 peut modifier +// Colonnes que l'utilisateur a le droit de modifier $allowedColumns = ['name', 'email', 'active']; -// Supprimer toutes les colonnes non autorisées de l'entrée +// Supprime de l'entrée toutes les colonnes non autorisées $filteredData = array_intersect_key($userData, array_flip($allowedColumns)); -// ✅ Maintenant, nous pouvons l'utiliser en toute sécurité dans les requêtes, telles que : +// ✅ Utilisation désormais sûre dans les requêtes, par exemple : $database->query('INSERT INTO users', $filteredData); $table->update($filteredData); $table->where($filteredData); @@ -167,19 +167,19 @@ $table->where($filteredData); Identifiants dynamiques ======================= -Pour les noms de tables et de colonnes dynamiques, utilisez le placeholder `?name`. Cela garantit un échappement correct des identifiants selon la syntaxe de la base de données donnée (par exemple, en utilisant des backticks en MySQL) : +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 -// ✅ Utilisation sécurisée d'identifiants fiables +// ✅ 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 : utilisez le symbole `?name` uniquement pour les valeurs fiables définies dans le code de l'application. Pour les valeurs provenant de l'utilisateur, utilisez à nouveau la [liste blanche |#Liste blanche de colonnes]. Sinon, vous vous exposez à des risques de sécurité : +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 utilisateur +// ❌ 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 index 5d815deebe..cc7469635f 100644 --- a/database/fr/sql-way.texy +++ b/database/fr/sql-way.texy @@ -1,17 +1,17 @@ -Accès SQL -********* +Approche SQL +************ .[perex] -Nette Database offre deux approches : vous pouvez écrire vous-même des requêtes SQL (accès SQL), ou les laisser générer automatiquement (voir [Explorer |explorer]). L'accès SQL vous donne un contrôle total sur les requêtes tout en assurant leur construction sécurisée. +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 sur la connexion et la configuration de la base de données se trouvent dans le chapitre [Connexion et configuration |guide#Connexion et configuration]. +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 ================ -Pour interroger la base de données, utilisez la méthode `query()`. Elle renvoie un objet [ResultSet |api:Nette\Database\ResultSet] qui représente le résultat de la requête. En cas d'échec, la méthode [lève une exception |exceptions]. Nous pouvons parcourir le résultat de la requête à l'aide d'une boucle `foreach`, ou utiliser l'une des [fonctions d'aide |#Obtention des données]. +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'); @@ -22,19 +22,19 @@ foreach ($result as $row) { } ``` -Pour insérer des valeurs en toute sécurité dans les requêtes SQL, nous utilisons des requêtes paramétrées. Nette Database les rend extrêmement simples - il suffit d'ajouter une virgule et la valeur après la requête SQL : +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 options d'écriture. Vous pouvez soit "intercaler" les paramètres dans la requête SQL : +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 la requête SQL complète, puis joindre tous les paramètres : +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); @@ -44,30 +44,30 @@ $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 contre une attaque appelée injection SQL, où un attaquant pourrait injecter ses propres commandes SQL et ainsi obtenir ou endommager des données dans la base de données. +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 la requête SQL !** Utilisez toujours des requêtes paramétrées, qui vous protègent contre l'injection SQL. +**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écurisée +// ✅ Requête paramétrée sûre $database->query('SELECT * FROM users WHERE name = ?', $name); ``` -Familiarisez-vous avec les [risques de sécurité possibles |security]. +Prenez connaissance des [risques de sécurité possibles |security]. -Techniques de requête -===================== +Techniques de requêtage +======================= Conditions WHERE ---------------- -Les conditions WHERE peuvent être écrites sous forme de tableau associatif, où les clés sont les noms des colonnes et les valeurs sont les données à comparer. Nette Database choisit automatiquement l'opérateur SQL le plus approprié en fonction du type de valeur. +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', [ @@ -77,7 +77,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -Vous pouvez également spécifier explicitement l'opérateur de comparaison dans la clé : +Vous pouvez aussi indiquer explicitement l'opérateur de comparaison dans la clé : ```php $database->query('SELECT * FROM users WHERE', [ @@ -88,7 +88,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' ``` -Nette gère automatiquement les cas spéciaux comme les valeurs `null` ou les tableaux. +Nette gère automatiquement les cas particuliers comme les valeurs `null` ou les tableaux. ```php $database->query('SELECT * FROM products WHERE', [ @@ -103,21 +103,21 @@ Pour les conditions négatives, utilisez l'opérateur `NOT` : ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // utilise l'opérateur <> + 'name NOT' => 'Laptop', // utilise l'opérateur != 'category_id NOT' => [1, 2, 3], // utilise NOT IN 'description NOT' => null, // utilise IS NOT NULL - 'id' => [], // sera omis + 'id NOT' => [], // ignoré ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -Pour joindre des conditions, l'opérateur `AND` est utilisé. Cela peut être modifié à l'aide du [placeholder ?or |#Conseils pour la construction SQL]. +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 --------------- -Le tri `ORDER BY` peut être écrit à l'aide d'un tableau. Dans les clés, nous indiquons les colonnes et la valeur sera un booléen indiquant s'il faut trier par ordre croissant : +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', [ @@ -128,10 +128,10 @@ $database->query('SELECT id FROM author ORDER BY', [ ``` -Insertion de données (INSERT) ------------------------------ +Insérer des données (INSERT) +---------------------------- -Pour insérer des enregistrements, la commande SQL `INSERT` est utilisée. +La commande SQL `INSERT` sert à insérer des enregistrements. ```php $values = [ @@ -142,11 +142,11 @@ $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 est nécessaire de spécifier en paramètre le nom de la séquence à partir de laquelle l'ID doit être généré à l'aide de `$database->getInsertId($sequenceId)`. +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)`. -Nous pouvons également passer des [#valeurs spéciales] comme paramètres, telles que des fichiers, des objets DateTime ou des types enum. +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 à la fois : +Insertion de plusieurs enregistrements d'un coup : ```php $database->query('INSERT INTO users ?', [ @@ -155,18 +155,18 @@ $database->query('INSERT INTO users ?', [ ]); ``` -L'INSERT multiple est beaucoup plus rapide car une seule requête de base de données est exécutée, au lieu de plusieurs requêtes individuelles. +Un INSERT multiple est bien plus rapide, car une seule requête est exécutée au lieu de nombreuses requêtes individuelles. -**Avertissement de sécurité :** N'utilisez jamais de données non validées comme `$values`. Familiarisez-vous avec les [risques possibles |security#Travailler en toute sécurité avec les colonnes]. +**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]. -Mise à jour des données (UPDATE) --------------------------------- +Mettre à jour des données (UPDATE) +---------------------------------- -Pour mettre à jour des enregistrements, la commande SQL `UPDATE` est utilisée. +La commande SQL `UPDATE` sert à mettre à jour des enregistrements. ```php -// Mise à jour d'un seul enregistrement +// Met à jour un seul enregistrement $values = [ 'name' => 'John Smith', ]; @@ -175,15 +175,15 @@ $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 `-=` : +Pour `UPDATE`, nous pouvons utiliser les opérateurs `+=` et `-=` : ```php $database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrémentation de login_count + 'login_count+=' => 1, // incrémente login_count ], 1); ``` -Exemple d'insertion ou de modification d'un enregistrement s'il existe déjà. Nous utilisons la technique `ON DUPLICATE KEY UPDATE` : +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 = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Notez que Nette Database reconnaît le contexte dans lequel le paramètre avec le tableau est inséré dans la commande SQL et construit le code SQL en conséquence. Ainsi, à partir du premier tableau, il a construit `(id, name, year) VALUES (123, 'Jim', 1978)`, tandis que le second a été converti sous la forme `name = 'Jim', year = 1978`. Nous en discutons plus en détail dans la section [#Conseils pour la construction SQL]. +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]. -Suppression de données (DELETE) -------------------------------- +Supprimer des données (DELETE) +------------------------------ -Pour supprimer des enregistrements, la commande SQL `DELETE` est utilisée. Exemple d'obtention du nombre de lignes supprimées : +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) @@ -212,32 +212,32 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -Conseils pour la construction SQL ---------------------------------- +Indications de construction SQL +------------------------------- -Un hint est un placeholder spécial dans la requête SQL qui indique comment la valeur du paramètre doit être réécrite en expression 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 : -| Hint | Description | S'applique automatiquement +| Indication | Description | Utilisée automatiquement pour |-----------|-------------------------------------------------|----------------------------- -| `?name` | utilisé pour insérer le nom de la table ou de la colonne | - -| `?values` | génère `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | génère l'affectation `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | joint les conditions du tableau avec l'opérateur `AND` | `WHERE ?`, `HAVING ?` -| `?or` | joint les conditions du tableau avec l'opérateur `OR` | - -| `?order` | génère la clause `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` +| `?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 ?` -Pour insérer dynamiquement des noms de tables et de colonnes dans la requête, le placeholder `?name` est utilisé. Nette Database s'occupe du traitement correct des identifiants selon les conventions de la base de données donnée (par exemple, en les entourant de backticks en MySQL). +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 (en MySQL) +// SELECT `name` FROM `users` WHERE id = 1 (dans MySQL) ``` -**Avertissement :** utilisez le symbole `?name` uniquement pour les noms de tables et de colonnes provenant d'entrées validées, sinon vous vous exposez à un [risque de sécurité |security#Identifiants dynamiques]. +**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 hints n'ont généralement pas besoin d'être spécifiés, car Nette utilise une autodétection intelligente lors de la composition de la requête SQL (voir la troisième colonne du tableau). Mais vous pouvez l'utiliser, par exemple, dans une situation où vous souhaitez joindre des conditions à l'aide de `OR` au lieu de `AND` : +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', [ @@ -251,29 +251,29 @@ $database->query('SELECT * FROM users WHERE ?or', [ Valeurs spéciales ----------------- -En plus des types scalaires courants (string, int, bool), vous pouvez également passer des valeurs spéciales comme paramètres : +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 `DateTime` sont convertis au format de la base de données -- types enum : les instances `enum` sont converties en leur valeur -- littéraux SQL : créés à l'aide de `Connection::literal('NOW()')` sont insérés directement dans la requête +- 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 DateTime, + '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 support natif pour le type de données `datetime` (comme SQLite et Oracle), `DateTime` est converti en une valeur spécifiée dans la [configuration de la base de données |configuration] par l'élément `formatDateTime` (la valeur par défaut est `U` - timestamp unix). +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 spécifier directement du code SQL comme valeur, mais celui-ci ne doit pas être interprété comme une chaîne et échappé. C'est à cela que servent les objets de la classe `Nette\Database\SqlLiteral`. Ils sont créés par la méthode `Connection::literal()`. +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', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -Ou alternativement : +Ou bien : ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Grâce à cela, nous pouvons créer des combinaisons intéressantes : +Cela permet des combinaisons intéressantes : ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -317,20 +317,20 @@ $result = $database->query('SELECT * FROM users WHERE', [ ``` -Obtention des données +Récupérer les données ===================== Raccourcis pour les requêtes SELECT ----------------------------------- -Pour simplifier la récupération des données, `Connection` propose plusieurs raccourcis qui combinent l'appel de `query()` avec le `fetch*()` suivant. Ces méthodes acceptent les mêmes paramètres que `query()`, c'est-à-dire la requête SQL et les paramètres optionnels. Une description complète des méthodes `fetch*()` se trouve [ci-dessous |#fetch]. +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` -| `fetchAll($sql, ...$params): array` | Exécute la requête et renvoie toutes les lignes comme tableau d'objets `Row` -| `fetchPairs($sql, ...$params): array` | Exécute la requête et renvoie un tableau associatif, où la première colonne représente la clé et la seconde la valeur -| `fetchField($sql, ...$params): mixed` | Exécute la requête et renvoie la valeur du premier champ de la première ligne -| `fetchList($sql, ...$params): ?array` | Exécute la requête et renvoie la première ligne comme tableau indexé +| `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 : @@ -341,10 +341,10 @@ $count = $database->query('SELECT COUNT(*) FROM articles') ``` -`foreach` - itération sur les lignes ------------------------------------- +`foreach` - parcourir les lignes +-------------------------------- -Après l'exécution de la requête, un objet [ResultSet|api:Nette\Database\ResultSet] est renvoyé, permettant de parcourir les résultats de plusieurs manières. La manière la plus simple d'exécuter une requête et d'obtenir les lignes est d'itérer dans une boucle `foreach`. Cette méthode est la plus économe en mémoire car elle renvoie les données progressivement et ne les stocke pas toutes en mémoire en même temps. +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'); @@ -357,17 +357,17 @@ foreach ($result as $row) { ``` .[note] -`ResultSet` ne peut être itéré qu'une seule fois. Si vous avez besoin d'itérer plusieurs fois, vous devez d'abord charger les données dans un tableau, par exemple à l'aide de la méthode `fetchAll()`. +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 comme objet `Row`. S'il n'y a plus de lignes, renvoie `null`. Déplace le pointeur interne à la ligne suivante. +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(); // lit la première ligne +$row = $result->fetch(); // charge la première ligne if ($row) { echo $row->name; } @@ -381,7 +381,7 @@ Renvoie toutes les lignes restantes du `ResultSet` sous forme de tableau d'objet ```php $result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // lit toutes les lignes +$rows = $result->fetchAll(); // charge toutes les lignes foreach ($rows as $row) { echo $row->name; } @@ -391,7 +391,7 @@ foreach ($rows as $row) { 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 spécifie le nom de la colonne à utiliser comme clé dans le tableau, le second argument spécifie le nom de la colonne à utiliser comme valeur : +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'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Si nous ne spécifions que le premier paramètre, la valeur sera la ligne entière, c'est-à-dire l'objet `Row` : +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 dupliquées, la valeur de la dernière ligne est utilisée. En utilisant `null` comme clé, le tableau sera indexé numériquement à partir de zéro (il n'y a alors pas de collisions) : +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'); @@ -417,14 +417,14 @@ $names = $result->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternativement, vous pouvez spécifier un callback comme paramètre, qui renverra pour chaque ligne soit la valeur elle-même, soit une paire clé-valeur. +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 également renvoyer un tableau avec une paire clé & valeur : +// 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, ...] ``` @@ -433,18 +433,18 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -Renvoie la valeur du premier champ de la ligne actuelle. S'il n'y a plus de lignes, renvoie `null`. Déplace le pointeur interne à la ligne suivante. +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(); // lit le nom de la première ligne +$name = $result->fetchField(); // charge le name de la première ligne ``` fetchList(): ?array .[method] ----------------------------- -Renvoie une ligne comme tableau indexé. S'il n'y a plus de lignes, renvoie `null`. Déplace le pointeur interne à la ligne suivante. +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'); @@ -455,17 +455,17 @@ $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 `SELECT`, c'est le nombre de lignes renvoyées, mais celui-ci peut ne pas être connu - dans ce cas, la méthode renvoie `null`. +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 dans le `ResultSet`. +Renvoie le nombre de colonnes du `ResultSet`. -Informations sur les requêtes -============================= +Informations sur la requête +=========================== À des fins de débogage, nous pouvons obtenir des informations sur la dernière requête exécutée : @@ -477,21 +477,21 @@ 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, on peut utiliser : +Pour afficher le résultat sous forme de tableau HTML, vous pouvez utiliser : ```php $result = $database->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSet fournit des informations sur les types de colonnes : +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->type"; // par ex. 'id est de type int' + echo "$column est de type $type"; // par ex. 'id est de type int' } ``` @@ -499,7 +499,7 @@ foreach ($types as $column => $type) { Journalisation des requêtes --------------------------- -Nous pouvons implémenter notre propre journalisation des requêtes. L'événement `onQuery` est un tableau de callbacks qui sont appelés après chaque requête exécutée : +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) { diff --git a/database/fr/transactions.texy b/database/fr/transactions.texy index 7dce777453..62c0067f22 100644 --- a/database/fr/transactions.texy +++ b/database/fr/transactions.texy @@ -2,9 +2,9 @@ Transactions ************ .[perex] -Les transactions garantissent que soit toutes les opérations au sein d'une transaction sont exécutées, soit aucune ne l'est. Elles sont utiles pour assurer la cohérence des données lors d'opérations plus complexes. +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 manière la plus simple d'utiliser les transactions ressemble à ceci : +La façon la plus simple d'utiliser les transactions ressemble à ceci : ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -Vous pouvez écrire la même chose de manière beaucoup plus élégante en utilisant la méthode `transaction()`. Elle accepte un callback en paramètre, qu'elle exécute dans une transaction. Si le callback se déroule sans exception, la transaction est automatiquement validée (commit). Si une exception se produit, la transaction est annulée (rollback) et l'exception est propagé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) { @@ -33,11 +33,13 @@ $database->transaction(function ($database) use ($id) { }); ``` -La méthode `transaction()` peut également retourner des valeurs : +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(); // retourne le nombre de lignes mises à jour + return $result->getRowCount(); // renvoie le nombre de lignes mises à jour }); ``` diff --git a/database/fr/type-conversion.texy b/database/fr/type-conversion.texy new file mode 100644 index 0000000000..87c9b75f66 --- /dev/null +++ b/database/fr/type-conversion.texy @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000000..fb78b20053 --- /dev/null +++ b/database/fr/upgrading.texy @@ -0,0 +1,36 @@ +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/hu/@home.texy b/database/hu/@home.texy deleted file mode 100644 index fc880e7b35..0000000000 --- a/database/hu/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Támogatott adatbázisok -====================== - -A Nette a következő adatbázisokat támogatja: - -|* Adatbázis szerver |* DSN név |* Támogatás a Core-ban |* Támogatás az Explorerben -| MySQL (>= 5.1) | mysql | IGEN | IGEN -| PostgreSQL (>= 9.0) | pgsql | IGEN | IGEN -| Sqlite 3 (>= 3.8) | sqlite | IGEN | IGEN -| Oracle | oci | IGEN | - -| MS SQL (PDO_SQLSRV) | sqlsrv | IGEN | IGEN -| MS SQL (PDO_DBLIB) | mssql | IGEN | - -| ODBC | odbc | IGEN | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: A Nette Database jelentősen leegyszerűsíti az adatok lekérdezését az adatbázisból SQL lekérdezések írása nélkül. Hatékony lekérdezéseket tesz és nem továbbít felesleges adatokat.}} diff --git a/database/hu/@left-menu.texy b/database/hu/@left-menu.texy deleted file mode 100644 index 6bf3ca54b9..0000000000 --- a/database/hu/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Bevezetés |guide] -- [SQL hozzáférés |sql way] -- [Explorer |Explorer] -- [Tranzakciók |transactions] -- [Kivételek |exceptions] -- [Reflexió |reflection] -- [Leképezés |mapping] -- [Konfiguráció |configuration] -- [Biztonsági kockázatok |security] -- [Frissítés |en:upgrading] diff --git a/database/hu/@meta.texy b/database/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/database/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/database/hu/configuration.texy b/database/hu/configuration.texy deleted file mode 100644 index 7e539ca546..0000000000 --- a/database/hu/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Adatbázis konfiguráció -********************** - -.[perex] -A Nette Database konfigurációs lehetőségeinek áttekintése. - -Ha nem a teljes keretrendszert használja, csak ezt a könyvtárat, olvassa el, [hogyan töltse be a konfigurációt |bootstrap:]. - - -Egy kapcsolat -------------- - -Egy adatbázis-kapcsolat konfigurációja: - -```neon -database: - # DSN, az egyetlen kötelező kulcs - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Létrehozza a `Nette\Database\Connection` és `Nette\Database\Explorer` szolgáltatásokat, amelyeket általában [autowiringgel |dependency-injection:autowiring] adunk át, vagy hivatkozással a [nevükre |#DI Szolgáltatások]. - -További beállítások: - -```neon -database: - # megjelenítse az adatbázis panelt a Tracy Bar-ban? - debugger: ... # (bool) alapértelmezett true - - # megjelenítse az EXPLAIN lekérdezéseket a Tracy Bar-ban? - explain: ... # (bool) alapértelmezett true - - # engedélyezze az autowiringet ehhez a kapcsolathoz? - autowired: ... # (bool) alapértelmezett true az első kapcsolatnál - - # tábla konvenciók: discovered, static vagy osztálynév - conventions: discovered # (string) alapértelmezett 'discovered' - - options: - # csak akkor csatlakozzon az adatbázishoz, amikor szükséges? - lazy: ... # (bool) alapértelmezett false - - # PHP adatbázis-illesztőprogram osztálya - driverClass: # (string) - - # csak MySQL: beállítja az sql_mode-ot - sqlmode: # (string) - - # csak MySQL: beállítja a SET NAMES-t - charset: # (string) alapértelmezett 'utf8mb4' - - # csak MySQL: a TINYINT(1)-et bool-ra konvertálja - convertBoolean: # (bool) alapértelmezett false - - # a dátum oszlopokat immutable objektumokként adja vissza (3.2.1 verziótól) - newDateTime: # (bool) alapértelmezett false - - # csak Oracle és SQLite: dátum tárolási formátuma - formatDateTime: # (string) alapértelmezett 'U' -``` - -Az `options` kulcsban további opciókat adhat meg, amelyeket a [PDO illesztőprogramok dokumentációjában |https://www.php.net/manual/en/pdo.drivers.php] talál, például: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Több kapcsolat --------------- - -A konfigurációban több adatbázis-kapcsolatot is definiálhatunk, elnevezett szekciókra osztva: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Az autowiring csak az első szekcióból származó szolgáltatásoknál van bekapcsolva. Ezt meg lehet változtatni az `autowired: false` vagy `autowired: true` segítségével. - - -DI Szolgáltatások ------------------ - -Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez, ahol a `###` a kapcsolat nevét jelöli: - -| Név | Típus | Leírás -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | adatbázis-kapcsolat -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Ha csak egy kapcsolatot definiálunk, a szolgáltatások nevei `database.default.connection` és `database.default.explorer` lesznek. Ha több kapcsolatot definiálunk, mint a fenti példában, a nevek megfelelnek a szekcióknak, azaz `database.main.connection`, `database.main.explorer`, valamint `database.another.connection` és `database.another.explorer`. - -A nem autowire-olt szolgáltatásokat explicit módon, a nevükre való hivatkozással adjuk át: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/hu/exceptions.texy b/database/hu/exceptions.texy deleted file mode 100644 index 790fc06199..0000000000 --- a/database/hu/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Kivételek -********* - -A Nette Database kivétel-hierarchiát használ. Az alaposztály a `Nette\Database\DriverException`, amely a `PDOException`-ből öröklődik, és kibővített lehetőségeket biztosít az adatbázis-hibák kezelésére: - -- A `getDriverCode()` metódus visszaadja az adatbázis-driver hibakódját. -- A `getSqlState()` metódus visszaadja az SQLSTATE kódot. -- A `getQueryString()` és `getParameters()` metódusok lehetővé teszik az eredeti lekérdezés és paramétereinek lekérését. - -A `DriverException`-ből a következő specializált kivételek öröklődnek: - -- `ConnectionException` - jelzi az adatbázis-szerverhez való csatlakozás sikertelenségét. -- `ConstraintViolationException` - alaposztály az adatbázis-korlátozások megsértéséhez, amelyből öröklődnek: - - `ForeignKeyConstraintViolationException` - idegen kulcs megsértése. - - `NotNullConstraintViolationException` - NOT NULL korlátozás megsértése. - - `UniqueConstraintViolationException` - érték egyediségének megsértése. - - -Példa a `UniqueConstraintViolationException` kivétel elkapására, amely akkor következik be, ha olyan e-mail címmel próbálunk meg felhasználót beszúrni, amely már létezik az adatbázisban (feltéve, hogy az email oszlopnak egyedi indexe van). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Már létezik felhasználó ezzel az e-mail címmel.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Hiba történt a regisztráció során: ' . $e->getMessage(); -} -``` diff --git a/database/hu/explorer.texy b/database/hu/explorer.texy deleted file mode 100644 index 5b46016d0c..0000000000 --- a/database/hu/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Az Explorer intuitív és hatékony módot kínál az adatbázissal való munkára. Automatikusan gondoskodik a táblák közötti kapcsolatokról és a lekérdezések optimalizálásáról, így Ön az alkalmazására koncentrálhat. Azonnal működik beállítás nélkül. Ha teljes kontrollra van szüksége az SQL lekérdezések felett, használhatja az [SQL megközelítést |SQL way]. - -- Az adatokkal való munka természetes és könnyen érthető. -- Optimalizált SQL lekérdezéseket generál, amelyek csak a szükséges adatokat töltik be. -- Lehetővé teszi a kapcsolódó adatokhoz való könnyű hozzáférést JOIN lekérdezések írása nélkül. -- Azonnal működik bármilyen konfiguráció vagy entitásgenerálás nélkül. - -</div> - - -Az Explorerrel a [api:Nette\Database\Explorer] objektum `table()` metódusának meghívásával kezdhet (a csatlakozás részleteit a [Csatlakozás és konfiguráció |guide#Csatlakozás és konfiguráció] fejezetben találja): - -```php -$books = $explorer->table('book'); // 'book' a tábla neve -``` - -A metódus egy [Selection |api:Nette\Database\Table\Selection] objektumot ad vissza, amely egy SQL lekérdezést képvisel. Erre az objektumra további metódusokat láncolhatunk az eredmények szűrésére és rendezésére. A lekérdezés csak akkor áll össze és fut le, amikor elkezdjük kérni az adatokat. Például egy `foreach` ciklussal történő bejáráskor. Minden sort egy [ActiveRow |api:Nette\Database\Table\ActiveRow] objektum képvisel: - -```php -foreach ($books as $book) { - echo $book->title; // a 'title' oszlop kiírása - echo $book->author_id; // az 'author_id' oszlop kiírása -} -``` - -Az Explorer alapvetően megkönnyíti a [táblák közötti kapcsolatokkal |#Kapcsolatok a táblák között] való munkát. A következő példa bemutatja, milyen könnyen tudunk adatokat kiírni összekapcsolt táblákból (könyvek és szerzőik). Figyelje meg, hogy nem kell semmilyen JOIN lekérdezést írnunk, a Nette létrehozza őket helyettünk: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Könyv: ' . $book->title; - echo 'Szerző: ' . $book->author->name; // JOIN-t hoz létre az 'author' táblára -} -``` - -A Nette Database Explorer optimalizálja a lekérdezéseket, hogy a lehető leghatékonyabbak legyenek. A fenti példa csak két SELECT lekérdezést hajt végre, függetlenül attól, hogy 10 vagy 10 000 könyvet dolgozunk fel. - -Ráadásul az Explorer figyeli, hogy mely oszlopokat használják a kódban, és csak azokat tölti be az adatbázisból, ezzel további teljesítményt takarítva meg. Ez a viselkedés teljesen automatikus és adaptív. Ha később módosítja a kódot, és elkezd további oszlopokat használni, az Explorer automatikusan módosítja a lekérdezéseket. Nem kell semmit beállítania, sem azon gondolkodnia, mely oszlopokra lesz szüksége - bízza ezt a Nette-re. - - -Szűrés és rendezés -================== - -A `Selection` osztály metódusokat biztosít az adatok kiválasztásának szűrésére és rendezésére. - -.[language-php] -| `where($condition, ...$params)` | WHERE feltételt ad hozzá. Több feltétel AND operátorral van összekötve. -| `whereOr(array $conditions)` | OR operátorral összekötött WHERE feltételek csoportját adja hozzá. -| `wherePrimary($value)` | WHERE feltételt ad hozzá az elsődleges kulcs alapján. -| `order($columns, ...$params)` | Beállítja az ORDER BY rendezést. -| `select($columns, ...$params)` | Meghatározza a betöltendő oszlopokat. -| `limit($limit, $offset = null)` | Korlátozza a sorok számát (LIMIT) és opcionálisan beállítja az OFFSET-et. -| `page($page, $itemsPerPage, &$total = null)` | Beállítja a lapozást. -| `group($columns, ...$params)` | Csoportosítja a sorokat (GROUP BY). -| `having($condition, ...$params)` | HAVING feltételt ad hozzá a csoportosított sorok szűréséhez. - -A metódusok láncolhatók (ún. [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfészek]): `$table->where(...)->order(...)->limit(...)`. - -Ezekben a metódusokban speciális jelölést is használhat a [kapcsolódó táblákból származó adatokhoz |#Lekérdezés kapcsolódó táblákon keresztül] való hozzáféréshez. - - -Escapelés és azonosítók ------------------------ - -A metódusok automatikusan escapelik a paramétereket és idézőjelek közé teszik az azonosítókat (tábla- és oszlopneveket), ezzel megakadályozva az SQL injectiont. A helyes működéshez néhány szabályt be kell tartani: - -- A kulcsszavakat, függvényneveket, eljárásneveket stb. **nagybetűkkel** írja. -- Az oszlop- és táblaneveket **kisbetűkkel** írja. -- A stringeket mindig **paramétereken** keresztül adja át. - -```php -where('name = ' . $name); // KRITIKUS SEBEZHETŐSÉG: SQL injection -where('name LIKE "%search%"'); // ROSSZ: bonyolítja az automatikus idézőjelezést -where('name LIKE ?', '%search%'); // HELYES: érték paraméteren keresztül átadva - -where('name like ?', $name); // ROSSZ: generálja: `name` `like` ? -where('name LIKE ?', $name); // HELYES: generálja: `name` LIKE ? -where('LOWER(name) = ?', $value);// HELYES: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Szűri az eredményeket WHERE feltételekkel. Erőssége az intelligens munka különböző típusú értékekkel és az SQL operátorok automatikus kiválasztása. - -Alapvető használat: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -A megfelelő operátorok automatikus felismerésének köszönhetően nem kell különböző speciális esetekkel foglalkoznunk. A Nette megoldja őket helyettünk: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// operátor nélküli helyettesítő kérdőjelet is használhatunk: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -A metódus helyesen kezeli a negált feltételeket és az üres tömböket is: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- semmit sem talál -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- mindent megtalál -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- mindent megtalál -// $table->where('NOT id ?', $ids); Figyelem - ez a szintaxis nem támogatott -``` - -Paraméterként átadhatunk egy másik tábla eredményét is - al-lekérdezés jön létre: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -A feltételeket tömbként is átadhatjuk, amelynek elemei AND-del lesznek összekötve: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -A tömbben használhatunk kulcs => érték párokat, és a Nette ismét automatikusan kiválasztja a megfelelő operátorokat: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -A tömbben kombinálhatunk SQL kifejezéseket helyettesítő kérdőjelekkel és több paraméterrel. Ez alkalmas komplex feltételekhez pontosan definiált operátorokkal: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // két paramétert tömbként adunk át -]); -``` - -A `where()` többszöri hívása automatikusan AND-del köti össze a feltételeket. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Hasonlóan a `where()`-hez, feltételeket ad hozzá, de azzal a különbséggel, hogy OR-ral köti össze őket: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Itt is használhatunk komplexebb kifejezéseket: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Feltételt ad hozzá a tábla elsődleges kulcsához: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Ha a táblának összetett elsődleges kulcsa van (pl. `foo_id`, `bar_id`), tömbként adjuk át: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Meghatározza a sorok visszaadási sorrendjét. Rendezhetünk egy vagy több oszlop szerint, csökkenő vagy növekvő sorrendben, vagy saját kifejezés szerint: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Meghatározza az adatbázisból visszaadandó oszlopokat. Alapértelmezés szerint a Nette Database Explorer csak azokat az oszlopokat adja vissza, amelyeket ténylegesen használnak a kódban. A `select()` metódust olyan esetekben használjuk, amikor specifikus kifejezéseket kell visszaadnunk: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Az `AS` segítségével definiált aliasok ezután elérhetők az ActiveRow objektum tulajdonságaként: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // hozzáférés az aliashoz -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Korlátozza a visszaadott sorok számát (LIMIT), és opcionálisan lehetővé teszi az offset beállítását: - -```php -$table->limit(10); // LIMIT 10 (az első 10 sort adja vissza) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Lapozáshoz célszerűbb a `page()` metódust használni. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Megkönnyíti az eredmények lapozását. Elfogadja az oldal számát (1-től számolva) és az oldalankénti elemek számát. Opcionálisan átadható egy referencia egy változóra, amelybe az oldalak teljes száma kerül mentésre: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Összesen oldalak: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Csoportosítja a sorokat a megadott oszlopok szerint (GROUP BY). Általában aggregáló függvényekkel együtt használják: - -```php -// Megszámolja a termékek számát minden kategóriában -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Feltételt állít be a csoportosított sorok szűréséhez (HAVING). Használható a `group()` metódussal és aggregáló függvényekkel együtt: - -```php -// Megtalálja azokat a kategóriákat, amelyek több mint 100 termékkel rendelkeznek -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Adatok olvasása -=============== - -Az adatok adatbázisból történő olvasásához számos hasznos metódus áll rendelkezésre: - -.[language-php] -| `foreach ($table as $key => $row)` | Iterál az összes soron, `$key` az elsődleges kulcs értéke, `$row` egy ActiveRow objektum -| `$row = $table->get($key)` | Visszaad egy sort az elsődleges kulcs alapján -| `$row = $table->fetch()` | Visszaadja az aktuális sort és a mutatót a következőre lépteti -| `$array = $table->fetchPairs()` | Asszociatív tömböt hoz létre az eredményekből -| `$array = $table->fetchAll()` | Visszaadja az összes sort tömbként -| `count($table)` | Visszaadja a sorok számát a Selection objektumban - -Az [ActiveRow |api:Nette\Database\Table\ActiveRow] objektum csak olvasásra szolgál. Ez azt jelenti, hogy nem lehet módosítani a tulajdonságainak értékeit. Ez a korlátozás biztosítja az adatok konzisztenciáját és megakadályozza a váratlan mellékhatásokat. Az adatok az adatbázisból töltődnek be, és bármilyen változtatást explicit módon és ellenőrzötten kell végrehajtani. - - -`foreach` - iteráció az összes soron ------------------------------------- - -A legegyszerűbb módja a lekérdezés végrehajtásának és a sorok megszerzésének a `foreach` ciklussal történő iterálás. Automatikusan elindítja az SQL lekérdezést. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key az elsődleges kulcs értéke, $book egy ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Végrehajtja az SQL lekérdezést és visszaadja a sort az elsődleges kulcs alapján, vagy `null`-t, ha nem létezik. - -```php -$book = $explorer->table('book')->get(123); // visszaadja az ActiveRow-t 123 ID-vel vagy null-t -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Visszaadja a sort és a belső mutatót a következőre lépteti. Ha már nincsenek további sorok, `null`-t ad vissza. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Visszaadja az eredményeket asszociatív tömbként. Az első argumentum határozza meg annak az oszlopnak a nevét, amely kulcsként lesz használva a tömbben, a második argumentum pedig annak az oszlopnak a nevét, amely értékként lesz használva: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ha csak az első paramétert adjuk meg, az érték az egész sor lesz, azaz az `ActiveRow` objektum: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Duplikált kulcsok esetén az utolsó sor értéke lesz használva. Ha `null`-t használunk kulcsként, a tömb numerikusan lesz indexelve nullától kezdve (ekkor nem történik ütközés): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatívaként megadhat egy callbacket paraméterként, amely minden sorhoz vagy magát az értéket, vagy egy kulcs-érték párt ad vissza. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Első könyv (János Novak)', ...] - -// A callback visszaadhat egy tömböt is kulcs & érték párral: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Első könyv' => 'János Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Visszaadja az összes sort `ActiveRow` objektumok asszociatív tömbjeként, ahol a kulcsok az elsődleges kulcsok értékei. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -A `count()` metódus paraméter nélkül visszaadja a sorok számát a `Selection` objektumban: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternatíva -``` - -Figyelem, a `count()` paraméterrel aggregáló COUNT függvényt hajt végre az adatbázisban, lásd alább. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Átalakítja az `ActiveRow` objektumot asszociatív tömbbé, ahol a kulcsok az oszlopnevek, az értékek pedig a megfelelő adatok. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray lesz ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Aggregáció -========== - -A `Selection` osztály metódusokat biztosít az aggregáló függvények (COUNT, SUM, MIN, MAX, AVG stb.) egyszerű végrehajtásához. - -.[language-php] -| `count($expr)` | Megszámolja a sorok számát -| `min($expr)` | Visszaadja a minimális értéket egy oszlopban -| `max($expr)` | Visszaadja a maximális értéket egy oszlopban -| `sum($expr)` | Visszaadja az értékek összegét egy oszlopban -| `aggregation($function)` | Lehetővé teszi tetszőleges aggregáló függvény végrehajtását. Pl. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Végrehajt egy SQL lekérdezést a COUNT függvénnyel és visszaadja az eredményt. A metódus arra használatos, hogy megállapítsuk, hány sor felel meg egy bizonyos feltételnek: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Figyelem, a [#count()] paraméter nélkül csak a sorok számát adja vissza a `Selection` objektumban. - - -min(string $expr) és max(string $expr) .[method] ------------------------------------------------- - -A `min()` és `max()` metódusok visszaadják a minimális és maximális értéket a megadott oszlopban vagy kifejezésben: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Visszaadja az értékek összegét a megadott oszlopban vagy kifejezésben: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- - -Lehetővé teszi tetszőleges aggregáló függvény végrehajtását. - -```php -// termékek átlagos ára egy kategóriában -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// összekapcsolja a termék címkéit egyetlen stringgé -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Ha olyan eredményeket kell aggregálnunk, amelyek már maguk is valamilyen aggregáló függvényből és csoportosításból származnak (pl. `SUM(érték)` csoportosított sorokon keresztül), második argumentumként megadjuk azt az aggregáló függvényt, amelyet ezekre a köztes eredményekre kell alkalmazni: - -```php -// Kiszámítja a raktáron lévő termékek teljes árát az egyes kategóriákra, majd összeadja ezeket az árakat. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Ebben a példában először kiszámítjuk a termékek teljes árát minden kategóriában (`SUM(price * stock) AS category_total`), és csoportosítjuk az eredményeket a `category_id` szerint. Ezután az `aggregation('SUM(category_total)', 'SUM')` segítségével összeadjuk ezeket a `category_total` köztes összegeket. A második argumentum `'SUM'` azt mondja, hogy a köztes eredményekre a SUM függvényt kell alkalmazni. - - -Beszúrás, Frissítés és Törlés -============================= - -A Nette Database Explorer leegyszerűsíti az adatok beszúrását, frissítését és törlését. Minden említett metódus kivételt `Nette\Database\DriverException` dob hiba esetén. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Új rekordokat szúr be a táblába. - -**Egy rekord beszúrása:** - -Az új rekordot asszociatív tömbként vagy iterable objektumként (például az [űrlapokban |forms:] használt ArrayHash) adjuk át, ahol a kulcsok megfelelnek a tábla oszlopneveinek. - -Ha a táblának definiált elsődleges kulcsa van, a metódus egy `ActiveRow` objektumot ad vissza, amely újra betöltődik az adatbázisból, hogy figyelembe vegye az adatbázis szintjén végrehajtott esetleges változásokat (triggerek, oszlopok alapértelmezett értékei, auto-increment oszlopok számításai). Ez biztosítja az adatok konzisztenciáját, és az objektum mindig az aktuális adatokat tartalmazza az adatbázisból. Ha nincs egyértelmű elsődleges kulcsa, a átadott adatokat tömb formájában adja vissza. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row egy ActiveRow példány, és tartalmazza a beszúrt sor teljes adatait, -// beleértve az automatikusan generált ID-t és a triggerek által végrehajtott esetleges változásokat -echo $row->id; // Kiírja az újonnan beszúrt felhasználó ID-ját -echo $row->created_at; // Kiírja a létrehozás idejét, ha trigger állította be -``` - -**Több rekord beszúrása egyszerre:** - -A `insert()` metódus lehetővé teszi több rekord beszúrását egyetlen SQL lekérdezéssel. Ebben az esetben a beszúrt sorok számát adja vissza. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows értéke 2 lesz -``` - -Paraméterként átadható egy `Selection` objektum is adatkiválasztással. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Speciális értékek beszúrása:** - -Értékként átadhatunk fájlokat, DateTime objektumokat vagy SQL literálokat is: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // adatbázis formátumra konvertálja - 'avatar' => fopen('image.jpg', 'rb'), // beszúrja a fájl bináris tartalmát - 'uuid' => $explorer::literal('UUID()'), // meghívja az UUID() függvényt -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Frissíti a tábla sorait a megadott szűrő szerint. Visszaadja a ténylegesen megváltozott sorok számát. - -A módosítandó oszlopokat asszociatív tömbként vagy iterable objektumként (például az [űrlapokban |forms:] használt ArrayHash) adjuk át, ahol a kulcsok megfelelnek a tábla oszlopneveinek: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Numerikus értékek módosításához használhatjuk a `+=` és `-=` operátorokat: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // növeli a 'points' oszlop értékét 1-gyel - 'coins-=' => 1, // csökkenti a 'coins' oszlop értékét 1-gyel - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Törli a tábla sorait a megadott szűrő szerint. Visszaadja a törölt sorok számát. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Az `update()` és `delete()` hívásakor ne felejtse el a `where()` segítségével megadni a módosítandó/törlendő sorokat. Ha nem használja a `where()`-t, a művelet az egész táblán végrehajtódik! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Frissíti az adatokat az `ActiveRow` objektum által képviselt adatbázis-sorban. Paraméterként egy iterable-t fogad el a frissítendő adatokkal (a kulcsok az oszlopnevek). Numerikus értékek módosításához használhatjuk a `+=` és `-=` operátorokat: - -A frissítés végrehajtása után az `ActiveRow` automatikusan újra betöltődik az adatbázisból, hogy figyelembe vegye az adatbázis szintjén végrehajtott esetleges változásokat (pl. triggerek). A metódus csak akkor ad vissza true-t, ha tényleges adatváltozás történt. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // növeljük a megtekintések számát -]); -echo $article->views; // Kiírja az aktuális megtekintések számát -``` - -Ez a metódus csak egyetlen konkrét sort frissít az adatbázisban. Több sor tömeges frissítéséhez használja a [#Selection::update()] metódust. - - -ActiveRow::delete() .[method] ------------------------------ - -Törli az adatbázisból azt a sort, amelyet az `ActiveRow` objektum képvisel. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Törli az 1-es ID-jű könyvet -``` - -Ez a metódus csak egyetlen konkrét sort töröl az adatbázisból. Több sor tömeges törléséhez használja a [#Selection::delete()] metódust. - - -Kapcsolatok a táblák között -=========================== - -Relációs adatbázisokban az adatok több táblára vannak osztva, és idegen kulcsok segítségével kapcsolódnak egymáshoz. A Nette Database Explorer forradalmi módot kínál ezekkel a kapcsolatokkal való munkára - JOIN lekérdezések írása és bármi konfigurálása vagy generálása nélkül. - -A kapcsolatokkal való munka illusztrálására egy könyvadatbázis példáját használjuk ([megtalálható a GitHubon |https://github.com/nette-examples/books]). Az adatbázisban a következő táblák vannak: - -- `author` - írók és fordítók (oszlopok: `id`, `name`, `web`, `born`) -- `book` - könyvek (oszlopok: `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - címkék (oszlopok: `id`, `name`) -- `book_tag` - kapcsolótábla a könyvek és címkék között (oszlopok: `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** A példákban használt adatbázis struktúra *** - -A könyvadatbázis példánkban több típusú kapcsolatot találunk (bár a modell egyszerűsített a valósághoz képest): - -- Egy-a-többhöz 1:N – minden könyvnek **egy** szerzője van, egy szerző **több** könyvet írhat -- Nulla-a-többhöz 0:N – egy könyvnek **lehet** fordítója, egy fordító **több** könyvet fordíthat -- Nulla-az-egyhez 0:1 – egy könyvnek **lehet** folytatása -- Több-a-többhöz M:N – egy könyvnek **több** címkéje lehet, és egy címke **több** könyvhöz rendelhető - -Ezekben a kapcsolatokban mindig van egy szülő és egy gyermek tábla. Például a szerző és a könyv közötti kapcsolatban az `author` tábla a szülő, a `book` pedig a gyermek - elképzelhetjük úgy, hogy a könyv mindig "tartozik" valamilyen szerzőhöz. Ez megmutatkozik az adatbázis struktúrájában is: a gyermek `book` tábla tartalmaz egy `author_id` idegen kulcsot, amely a szülő `author` táblára hivatkozik. - -Ha ki kell listáznunk a könyveket a szerzőik nevével együtt, két lehetőségünk van. Vagy egyetlen SQL lekérdezéssel szerezzük meg az adatokat JOIN segítségével: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Vagy két lépésben töltjük be az adatokat - először a könyveket, majd a szerzőiket - és utána PHP-ban összerakjuk őket: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- a megszerzett könyvek szerzőinek id-jai -``` - -A második megközelítés valójában hatékonyabb, bár ez meglepő lehet. Az adatok csak egyszer töltődnek be, és jobban felhasználhatók a cache-ben. Pontosan így működik a Nette Database Explorer - mindent a felszín alatt old meg, és elegáns API-t kínál Önnek: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'cím: ' . $book->title; - echo 'írta: ' . $book->author->name; // $book->author egy rekord az 'author' táblából - echo 'fordította: ' . $book->translator?->name; -} -``` - - -Hozzáférés a szülő táblához ---------------------------- - -A szülő táblához való hozzáférés egyszerű. Olyan kapcsolatokról van szó, mint *a könyvnek van szerzője* vagy *a könyvnek lehet fordítója*. A kapcsolódó rekordot az ActiveRow objektum property-jén keresztül érjük el - a neve megegyezik az idegen kulcsot tartalmazó oszlop nevével `id` nélkül: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // megtalálja a szerzőt az author_id oszlop alapján -echo $book->translator?->name; // megtalálja a fordítót a translator_id alapján -``` - -Amikor hozzáférünk a `$book->author` property-hez, az Explorer a `book` táblában keres egy oszlopot, amelynek neve tartalmazza az `author` stringet (tehát `author_id`). Az ebben az oszlopban lévő érték alapján betölti a megfelelő rekordot az `author` táblából, és `ActiveRow`-ként adja vissza. Hasonlóan működik a `$book->translator` is, amely a `translator_id` oszlopot használja. Mivel a `translator_id` oszlop tartalmazhat `null`-t, a kódban a `?->` operátort használjuk. - -Alternatív utat kínál a `ref()` metódus, amely két argumentumot fogad el, a cél tábla nevét és a kapcsoló oszlop nevét, és egy `ActiveRow` példányt vagy `null`-t ad vissza: - -```php -echo $book->ref('author', 'author_id')->name; // kapcsolat a szerzővel -echo $book->ref('author', 'translator_id')->name; // kapcsolat a fordítóval -``` - -A `ref()` metódus akkor hasznos, ha nem lehet a property-n keresztüli hozzáférést használni, mert a tábla tartalmaz egy azonos nevű oszlopot (azaz `author`). Más esetekben a property-n keresztüli hozzáférés használata javasolt, amely olvashatóbb. - -Az Explorer automatikusan optimalizálja az adatbázis-lekérdezéseket. Amikor ciklusban járjuk be a könyveket, és hozzáférünk a kapcsolódó rekordjaikhoz (szerzők, fordítók), az Explorer nem generál lekérdezést minden egyes könyvhöz külön. Ehelyett csak egy SELECT-et hajt végre minden kapcsolattípushoz, ezzel jelentősen csökkentve az adatbázis terhelését. Például: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Ez a kód csak ezt a három villámgyors lekérdezést hívja meg az adatbázisba: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id-k a kiválasztott könyvek author_id oszlopából -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id-k a kiválasztott könyvek translator_id oszlopából -``` - -.[note] -A kapcsoló oszlop megtalálásának logikáját a [Conventions |api:Nette\Database\Conventions] implementációja határozza meg. Javasoljuk a [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions] használatát, amely elemzi az idegen kulcsokat, és lehetővé teszi a táblák közötti meglévő kapcsolatokkal való egyszerű munkát. - - -Hozzáférés a gyermek táblához ------------------------------ - -A gyermek táblához való hozzáférés fordított irányban működik. Most azt kérdezzük, *milyen könyveket írt ez a szerző* vagy *fordított ez a fordító*. Ehhez a lekérdezéstípushoz a `related()` metódust használjuk, amely egy `Selection`-t ad vissza a kapcsolódó rekordokkal. Nézzünk egy példát: - -```php -$author = $explorer->table('author')->get(1); - -// Kiírja a szerző összes könyvét -foreach ($author->related('book.author_id') as $book) { - echo "Írta: $book->title"; -} - -// Kiírja az összes könyvet, amelyet a szerző fordított -foreach ($author->related('book.translator_id') as $book) { - echo "Fordította: $book->title"; -} -``` - -A `related()` metódus a kapcsolat leírását egyetlen argumentumként pont-jelöléssel vagy két különálló argumentumként fogadja el: - -```php -$author->related('book.translator_id'); // egy argumentum -$author->related('book', 'translator_id'); // két argumentum -``` - -Az Explorer képes automatikusan felismerni a helyes kapcsoló oszlopot a szülő tábla neve alapján. Ebben az esetben a `book.author_id` oszlopon keresztül kapcsolódik, mivel a forrástábla neve `author`: - -```php -$author->related('book'); // a book.author_id-t használja -``` - -Ha több lehetséges kapcsolat létezne, az Explorer [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] kivételt dob. - -A `related()` metódust természetesen használhatjuk több rekord ciklusban történő bejárásakor is, és az Explorer ebben az esetben is automatikusan optimalizálja a lekérdezéseket: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' írta:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Ez a kód csak két villámgyors SQL lekérdezést generál: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- a kiválasztott szerzők id-jai -``` - - -Több-a-többhöz kapcsolat ------------------------- - -A több-a-többhöz (M:N) kapcsolathoz szükség van egy kapcsolótábla létezésére (esetünkben `book_tag`), amely két idegen kulcsot tartalmazó oszlopot (`book_id`, `tag_id`) tartalmaz. Ezen oszlopok mindegyike az összekapcsolt táblák egyikének elsődleges kulcsára hivatkozik. A kapcsolódó adatok megszerzéséhez először a kapcsolótábla rekordjait szerezzük meg a `related('book_tag')` segítségével, majd tovább haladunk a céladatokhoz: - -```php -$book = $explorer->table('book')->get(1); -// kiírja a könyvhöz rendelt címkék neveit -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // kiírja a címke nevét a kapcsolótáblán keresztül -} - -$tag = $explorer->table('tag')->get(1); -// vagy fordítva: kiírja az ezzel a címkével megjelölt könyvek neveit -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // kiírja a könyv nevét -} -``` - -Az Explorer ismét optimalizálja az SQL lekérdezéseket hatékony formába: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- a kiválasztott könyvek id-jai -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- a book_tag-ban talált címkék id-jai -``` - - -Lekérdezés kapcsolódó táblákon keresztül ----------------------------------------- - -A `where()`, `select()`, `order()` és `group()` metódusokban speciális jelöléseket használhatunk más táblák oszlopaihoz való hozzáféréshez. Az Explorer automatikusan létrehozza a szükséges JOIN-okat. - -**Pont-jelölés** (`szülő_tábla.oszlop`) a gyermek tábla szemszögéből nézett 1:N kapcsolathoz használatos: - -```php -$books = $explorer->table('book'); - -// Megtalálja azokat a könyveket, amelyek szerzőjének neve 'Jon'-nal kezdődik -$books->where('author.name LIKE ?', 'Jon%'); - -// Rendezi a könyveket a szerző neve szerint csökkenő sorrendben -$books->order('author.name DESC'); - -// Kiírja a könyv címét és a szerző nevét -$books->select('book.title, author.name'); -``` - -**Kettőspont-jelölés** (`:gyermek_tábla.oszlop`) a szülő tábla szemszögéből nézett 1:N kapcsolathoz használatos: - -```php -$authors = $explorer->table('author'); - -// Megtalálja azokat a szerzőket, akik 'PHP'-t tartalmazó című könyvet írtak -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Megszámolja a könyvek számát minden szerzőhöz -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -A fenti példában a kettőspont-jelöléssel (`:book.title`) nincs megadva az idegen kulcs oszlopa. Az Explorer automatikusan felismeri a helyes oszlopot a szülő tábla neve alapján. Ebben az esetben a `book.author_id` oszlopon keresztül kapcsolódik, mivel a forrástábla neve `author`. Ha több lehetséges kapcsolat létezne, az Explorer [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] kivételt dob. - -A kapcsoló oszlopot explicit módon meg lehet adni zárójelben: - -```php -// Megtalálja azokat a szerzőket, akik 'PHP'-t tartalmazó című könyvet fordítottak -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -A jelölések láncolhatók több táblán keresztüli hozzáféréshez: - -```php -// Megtalálja a 'PHP' címkével megjelölt könyvek szerzőit -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -JOIN feltételek bővítése ------------------------- - -A `joinWhere()` metódus kibővíti azokat a feltételeket, amelyeket a táblák összekapcsolásakor az SQL-ben az `ON` kulcsszó után adunk meg. - -Tegyük fel, hogy egy adott fordító által fordított könyveket szeretnénk megtalálni: - -```php -// Megtalálja a 'David' nevű fordító által fordított könyveket -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -A `joinWhere()` feltételben ugyanazokat a konstrukciókat használhatjuk, mint a `where()` metódusban - operátorokat, helyettesítő kérdőjeleket, értékek tömbjét vagy SQL kifejezéseket. - -Összetettebb lekérdezésekhez több JOIN-nal definiálhatunk tábla aliasokat: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Figyelje meg, hogy míg a `where()` metódus feltételeket ad hozzá a `WHERE` záradékhoz, a `joinWhere()` metódus kibővíti a feltételeket az `ON` záradékban a táblák összekapcsolásakor. diff --git a/database/hu/guide.texy b/database/hu/guide.texy deleted file mode 100644 index b7e2e44944..0000000000 --- a/database/hu/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -A Nette Database egy erőteljes és elegáns adatbázis réteg PHP számára, hangsúlyt fektetve az egyszerűségre és az okos funkciókra. Kétféle módot kínál az adatbázissal való munkára - [Explorer |Explorer] az alkalmazások gyors fejlesztéséhez, vagy [SQL megközelítés |SQL way] a lekérdezésekkel való közvetlen munkához. - -<div class="grid gap-3"> -<div> - - -[SQL megközelítés |SQL way] -=========================== -- Biztonságos paraméterezett lekérdezések -- Pontos ellenőrzés az SQL lekérdezések formája felett -- Amikor komplex lekérdezéseket ír haladó funkciókkal -- Optimalizálja a teljesítményt specifikus SQL funkciók segítségével - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Gyorsan fejleszthet SQL írása nélkül -- Intuitív munka a táblák közötti kapcsolatokkal -- Értékelni fogja a lekérdezések automatikus optimalizálását -- Alkalmas gyors és kényelmes adatbázis-kezelésre - -</div> - -</div> - - -Telepítés -========= - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/database -``` - - -Támogatott adatbázisok -====================== - -A Nette Database a következő adatbázisokat támogatja: - -|* Adatbázis szerver |* DSN név |* Támogatás az Explorerben -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | IGEN -| PostgreSQL (>= 9.0) | pgsql | IGEN -| Sqlite 3 (>= 3.8) | sqlite | IGEN -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | IGEN -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Két megközelítés az adatbázishoz -================================ - -A Nette Database választási lehetőséget kínál: vagy közvetlenül írhat SQL lekérdezéseket (SQL megközelítés), vagy hagyhatja, hogy automatikusan generálódjanak (Explorer). Nézzük meg, hogyan oldják meg mindkét megközelítéssel ugyanazokat a feladatokat: - -[SQL megközelítés|sql way] - SQL lekérdezések - -```php -// rekord beszúrása -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// rekordok lekérése: könyvek szerzői -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// listázás (nem optimális, N további lekérdezést generál) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Szerző $author->name írt $author->books_count könyvet:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer megközelítés |explorer] - automatikus SQL generálás - -```php -// rekord beszúrása -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// rekordok lekérése: könyvek szerzői -$authors = $database->table('authors') - ->where('active', 1); - -// listázás (automatikusan csak 2 optimalizált lekérdezést generál) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Szerző $author->name írt {$books->count()} könyvet:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Az Explorer megközelítés automatikusan generálja és optimalizálja az SQL lekérdezéseket. A megadott példában az SQL megközelítés N+1 lekérdezést generál (egyet a szerzőkhöz, majd egyet minden szerző könyveihez), míg az Explorer automatikusan optimalizálja a lekérdezéseket, és csak kettőt hajt végre - egyet a szerzőkhöz és egyet az összes könyvükhöz. - -Mindkét megközelítés tetszés szerint kombinálható az alkalmazásban, igény szerint. - - -Csatlakozás és konfiguráció -=========================== - -Az adatbázishoz való csatlakozáshoz elegendő létrehozni egy [api:Nette\Database\Connection] osztálypéldányt: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -A `$dsn` (data source name) paraméter ugyanaz, [amit a PDO használ |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], pl. `host=127.0.0.1;dbname=test`. Hiba esetén `Nette\Database\ConnectionException` kivételt dob. - -Azonban egy ügyesebb módszert kínál az [alkalmazáskonfiguráció |configuration], ahová elegendő hozzáadni egy `database` szekciót, és létrejönnek a szükséges objektumok, valamint az adatbázis panel a [Tracy |tracy:] sávban. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Ezután a kapcsolat objektumot [szolgáltatásként kapjuk meg a DI konténerből |dependency-injection:passing-dependencies], pl.: - -```php -class Model -{ - public function __construct( - // vagy Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -További információk az [adatbázis konfigurációjáról |configuration]. - - -Explorer manuális létrehozása ------------------------------ - -Ha nem használja a Nette DI konténert, manuálisan is létrehozhat egy `Nette\Database\Explorer` példányt: - -```php -// csatlakozás az adatbázishoz -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// tároló a cache-hez, implementálja a Nette\Caching\Storage-ot, pl.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// gondoskodik az adatbázis struktúra reflexiójáról -$structure = new Nette\Database\Structure($connection, $storage); -// definiálja a táblanevek, oszlopnevek és idegen kulcsok leképezési szabályait -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Kapcsolatkezelés -================ - -A `Connection` objektum létrehozásakor a csatlakozás automatikusan megtörténik. Ha késleltetni szeretné a csatlakozást, használja a lazy módot - ezt a [konfigurációban |configuration] a `lazy` beállításával, vagy így kapcsolhatja be: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -A kapcsolat kezeléséhez használja a `connect()`, `disconnect()` és `reconnect()` metódusokat. -- `connect()` létrehozza a kapcsolatot, ha még nem létezik, és `Nette\Database\ConnectionException` kivételt dobhat. -- `disconnect()` megszakítja az aktuális adatbázis-kapcsolatot. -- `reconnect()` megszakítja, majd újra csatlakoztatja az adatbázishoz. Ez a metódus szintén `Nette\Database\ConnectionException` kivételt dobhat. - -Ezenkívül figyelheti a csatlakozással kapcsolatos eseményeket az `onConnect` esemény segítségével, amely egy callback tömb, amely az adatbázissal való kapcsolat létrejötte után hívódik meg. - -```php -// az adatbázishoz való csatlakozás után fut le -$database->onConnect[] = function($database) { - echo "Csatlakozva az adatbázishoz"; -}; -``` - - -Tracy Debug Bar -=============== - -Ha [Tracy-t |tracy:] használ, a Database panel automatikusan aktiválódik a Debug sávban, amely megjeleníti az összes végrehajtott lekérdezést, azok paramétereit, végrehajtási idejét és a kódban való meghívásuk helyét. - -[* db-panel.webp *] diff --git a/database/hu/mapping.texy b/database/hu/mapping.texy deleted file mode 100644 index 1e96f83fbb..0000000000 --- a/database/hu/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Típuskonverzió -************** - -.[perex] -A Nette Database automatikusan konvertálja az adatbázisból visszaadott értékeket a megfelelő PHP típusokra. - - -Dátum és idő ------------- - -Az időadatok `Nette\Utils\DateTime` objektumokká konvertálódnak. Ha azt szeretné, hogy az időadatok immutable `Nette\Database\DateTime` objektumokká konvertálódjanak, állítsa a `newDateTime` opciót true-ra a [konfigurációban |configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('Y. n. j.'); -``` - -MySQL esetén a `TIME` adattípust `DateInterval` objektumokká konvertálja. - - -Logikai értékek ---------------- - -A logikai értékek automatikusan `true`-ra vagy `false`-ra konvertálódnak. MySQL esetén a `TINYINT(1)` konvertálódik, ha a [konfigurációban |configuration] beállítjuk a `convertBoolean`-t. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Numerikus értékek ------------------ - -A numerikus értékek `int`-re vagy `float`-ra konvertálódnak az adatbázis oszlopának típusa szerint: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Egyéni normalizálás -------------------- - -A `setRowNormalizer(?callable $normalizer)` metódussal beállíthat egy egyéni funkciót az adatbázisból származó sorok átalakítására. Ez hasznos lehet például az adattípusok automatikus konvertálásához. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // itt történik a típuskonverzió - return $row; -}); -``` diff --git a/database/hu/reflection.texy b/database/hu/reflection.texy deleted file mode 100644 index ad380ba29b..0000000000 --- a/database/hu/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Struktúra reflexió -****************** - -.{data-version:3.2.1} -A Nette Database eszközöket biztosít az adatbázis struktúrájának introspekciójához a [api:Nette\Database\Reflection] osztály segítségével. Ez lehetővé teszi információk lekérését táblákról, oszlopokról, indexekről és idegen kulcsokról. A reflexiót használhatja sémák generálásához, rugalmas, adatbázissal dolgozó alkalmazások létrehozásához vagy általános adatbázis-eszközök készítéséhez. - -A reflexiós objektumot az adatbázis-kapcsolat példányából kapjuk meg: - -```php -$reflection = $database->getReflection(); -``` - - -Táblák lekérése ---------------- - -A `$reflection->tables` readonly property tartalmazza az adatbázis összes táblájának asszociatív tömbjét: - -```php -// Az összes tábla nevének kiírása -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Két további metódus is rendelkezésre áll: - -```php -// Tábla létezésének ellenőrzése -if ($reflection->hasTable('users')) { - echo "A users tábla létezik"; -} - -// Visszaadja a tábla objektumot; ha nem létezik, kivételt dob -$table = $reflection->getTable('users'); -``` - - -Információ a tábláról ---------------------- - -A táblát egy [Table|api:Nette\Database\Reflection\Table] objektum reprezentálja, amely a következő readonly property-ket biztosítja: - -- `$name: string` – tábla neve -- `$view: bool` – hogy nézetről van-e szó -- `$fullName: ?string` – a tábla teljes neve, beleértve a sémát (ha létezik) -- `$columns: array<string, Column>` – a tábla oszlopainak asszociatív tömbje -- `$indexes: Index[]` – a tábla indexeinek tömbje -- `$primaryKey: ?Index` – a tábla elsődleges kulcsa vagy null -- `$foreignKeys: ForeignKey[]` – a tábla idegen kulcsainak tömbje - - -Oszlopok --------- - -A tábla `columns` property-je az oszlopok asszociatív tömbjét adja meg, ahol a kulcs az oszlop neve, az érték pedig egy [Column|api:Nette\Database\Reflection\Column] példány a következő property-kkel: - -- `$name: string` – oszlop neve -- `$table: ?Table` – referencia az oszlop táblájára -- `$nativeType: string` – natív adatbázis típus -- `$size: ?int` – a típus mérete/hossza -- `$nullable: bool` – hogy az oszlop tartalmazhat-e NULL-t -- `$default: mixed` – az oszlop alapértelmezett értéke -- `$autoIncrement: bool` – hogy az oszlop auto-increment-e -- `$primary: bool` – hogy része-e az elsődleges kulcsnak -- `$vendor: array` – további, az adott adatbázis-rendszerre specifikus metaadatok - -```php -foreach ($table->columns as $name => $column) { - echo "Oszlop: $name\n"; - echo "Típus: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Igen' : 'Nem') . "\n"; -} -``` - - -Indexek -------- - -A tábla `indexes` property-je az indexek tömbjét adja meg, ahol minden index egy [Index|api:Nette\Database\Reflection\Index] példány a következő property-kkel: - -- `$columns: Column[]` – az indexet alkotó oszlopok tömbje -- `$unique: bool` – hogy az index egyedi-e -- `$primary: bool` – hogy elsődleges kulcsról van-e szó -- `$name: ?string` – az index neve - -A tábla elsődleges kulcsát a `primaryKey` property segítségével lehet lekérni, amely vagy egy `Index` objektumot ad vissza, vagy `null`-t, ha a táblának nincs elsődleges kulcsa. - -```php -// Indexek kiírása -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Oszlopok: $columns\n"; - echo " Unique: " . ($index->unique ? 'Igen' : 'Nem') . "\n"; -} - -// Elsődleges kulcs kiírása -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Elsődleges kulcs: $columns\n"; -} -``` - - -Idegen kulcsok --------------- - -A tábla `foreignKeys` property-je az idegen kulcsok tömbjét adja meg, ahol minden idegen kulcs egy [ForeignKey|api:Nette\Database\Reflection\ForeignKey] példány a következő property-kkel: - -- `$foreignTable: Table` – a hivatkozott tábla -- `$localColumns: Column[]` – a helyi oszlopok tömbje -- `$foreignColumns: Column[]` – a hivatkozott oszlopok tömbje -- `$name: ?string` – az idegen kulcs neve - -```php -// Idegen kulcsok kiírása -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/hu/security.texy b/database/hu/security.texy deleted file mode 100644 index 1922a972d3..0000000000 --- a/database/hu/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Biztonsági kockázatok -********************* - -<div class=perex> - -Az adatbázis gyakran tartalmaz érzékeny adatokat és lehetővé teszi veszélyes műveletek végrehajtását. A Nette Database biztonságos használatához kulcsfontosságú: - -- Megérteni a különbséget a biztonságos és a nem biztonságos API között -- Paraméterezett lekérdezéseket használni -- Helyesen validálni a bemeneti adatokat - -</div> - - -Mi az SQL Injection? -==================== - -Az SQL injection a legkomolyabb biztonsági kockázat az adatbázisokkal való munka során. Akkor keletkezik, ha a felhasználótól származó, nem kezelt bemenet az SQL lekérdezés részévé válik. A támadó saját SQL parancsokat illeszthet be, és ezzel: -- Jogosulatlan hozzáférést szerezhet az adatokhoz -- Módosíthatja vagy törölheti az adatokat az adatbázisban -- Megkerülheti az authentikációt - -```php -// ❌ VESZÉLYES KÓD - sebezhető az SQL injection-nel szemben -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// A támadó például megadhatja a következő értéket: ' OR '1'='1 -// Az eredményül kapott lekérdezés ez lesz: SELECT * FROM users WHERE name = '' OR '1'='1' -// Ami visszaadja az összes felhasználót -``` - -Ugyanez vonatkozik a [Database Explorer |explorer]-re is: - -```php -// ❌ VESZÉLYES KÓD - sebezhető az SQL injection-nel szemben -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Paraméterezett lekérdezések -=========================== - -Az SQL injection elleni alapvető védekezés a paraméterezett lekérdezések használata. A Nette Database több módszert is kínál ezek használatára. - -A legegyszerűbb módszer a **kérdőjeles helyettesítők** használata: - -```php -// ✅ Biztonságos paraméterezett lekérdezés -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Biztonságos feltétel az Explorerben -$table->where('name = ?', $name); -``` - -Ez érvényes minden további metódusra a [Database Explorerben |explorer], amelyek lehetővé teszik kifejezések beillesztését kérdőjeles helyettesítőkkel és paraméterekkel. - -Az INSERT, UPDATE parancsokhoz vagy a WHERE záradékhoz az értékeket tömbben adhatjuk át: - -```php -// ✅ Biztonságos INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Biztonságos INSERT az Explorerben -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Paraméterértékek validálása -=========================== - -A paraméterezett lekérdezések a biztonságos adatbázis-kezelés alapkövei. Azonban az értékeknek, amelyeket beléjük illesztünk, több ellenőrzési szinten kell átesniük: - - -Típusellenőrzés ---------------- - -**A legfontosabb a paraméterek helyes adattípusának biztosítása** - ez szükséges feltétele a Nette Database biztonságos használatának. Az adatbázis feltételezi, hogy minden bemeneti adat helyes adattípussal rendelkezik, amely megfelel az adott oszlopnak. - -Például, ha az előző példákban a `$name` váratlanul egy tömb lenne egy string helyett, a Nette Database megpróbálná az összes elemét beilleszteni az SQL lekérdezésbe, ami hibához vezetne. Ezért **soha ne használjon** validálatlan adatokat a `$_GET`, `$_POST` vagy `$_COOKIE` tömbökből közvetlenül az adatbázis lekérdezésekben. - - -Formátumellenőrzés ------------------- - -A második ellenőrzési szinten az adatok formátumát ellenőrizzük - például, hogy a stringek UTF-8 kódolásúak-e, és hosszuk megfelel-e az oszlop definíciójának, vagy hogy a numerikus értékek az adott oszlop adattípusához megengedett tartományban vannak-e. - -Ezen a validálási szinten részben magára az adatbázisra is támaszkodhatunk - sok adatbázis elutasítja az érvénytelen adatokat. Azonban a viselkedés eltérő lehet, némelyik csendben levághatja a hosszú stringeket, vagy a tartományon kívüli számokat. - - -Domain ellenőrzés ------------------ - -A harmadik szint az alkalmazásspecifikus logikai ellenőrzéseket jelenti. Például annak ellenőrzése, hogy a select boxokból származó értékek megfelelnek-e a kínált lehetőségeknek, hogy a számok a várt tartományban vannak-e (pl. életkor 0-150 év), vagy hogy az értékek közötti kölcsönös függőségek értelmesek-e. - - -Ajánlott validálási módszerek ------------------------------ - -- Használjon [Nette Űrlapokat |forms:], amelyek automatikusan biztosítják az összes bemenet helyes validálását -- Használjon [Presentereket |application:] és adja meg az adattípusokat a paramétereknél az `action*()` és `render*()` metódusokban -- Vagy implementáljon saját validálási réteget standard PHP eszközökkel, mint például a `filter_var()` - - -Biztonságos munka az oszlopokkal -================================ - -Az előző szakaszban megmutattuk, hogyan kell helyesen validálni a paraméterértékeket. Azonban az SQL lekérdezésekben tömbök használatakor ugyanolyan figyelmet kell fordítanunk a kulcsaikra is. - -```php -// ❌ VESZÉLYES KÓD - a tömb kulcsai nincsenek kezelve -$database->query('INSERT INTO users', $_POST); -``` - -Az INSERT és UPDATE parancsoknál ez alapvető biztonsági hiba - a támadó bármilyen oszlopot beilleszthet vagy módosíthat az adatbázisban. Például beállíthatná az `is_admin = 1`-et, vagy tetszőleges adatokat illeszthetne be érzékeny oszlopokba (ún. Mass Assignment Vulnerability). - -A WHERE feltételekben ez még veszélyesebb, mivel operátorokat tartalmazhatnak: - -```php -// ❌ VESZÉLYES KÓD - a tömb kulcsai nincsenek kezelve -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// végrehajtja a WHERE (`salary` > 100000) lekérdezést -``` - -A támadó ezt a megközelítést használhatja a munkavállalók fizetésének szisztematikus kiderítésére. Például elkezdheti a 100 000 feletti fizetések lekérdezésével, majd az 50 000 alattiakkal, és a tartomány fokozatos szűkítésével felfedheti az összes munkavállaló hozzávetőleges fizetését. Ezt a támadástípust SQL enumeration-nek nevezik. - -A `where()` és `whereOr()` metódusok még [sokkal rugalmasabbak |explorer#where], és támogatják az SQL kifejezéseket, beleértve az operátorokat és függvényeket a kulcsokban és értékekben. Ez lehetőséget ad a támadónak SQL injection végrehajtására: - -```php -// ❌ VESZÉLYES KÓD - a támadó saját SQL-t illeszthet be -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// végrehajtja a WHERE (0) UNION SELECT name, salary FROM users WHERE (1) lekérdezést -``` - -Ez a támadás lezárja az eredeti feltételt a `0)` segítségével, saját `SELECT`-et csatol a `UNION` segítségével, hogy érzékeny adatokat szerezzen a `users` táblából, és szintaktikailag helyes lekérdezést zár le a `WHERE (1)` segítségével. - - -Oszlopok Whitelistje --------------------- - -Az oszlopnevekkel való biztonságos munkához szükségünk van egy mechanizmusra, amely biztosítja, hogy a felhasználó csak az engedélyezett oszlopokkal dolgozhasson, és ne tudjon sajátokat hozzáadni. Megpróbálhatnánk észlelni és blokkolni a veszélyes oszlopneveket (blacklist), de ez a megközelítés megbízhatatlan - a támadó mindig kitalálhat egy új módszert a veszélyes oszlopnév beírására, amit nem láttunk előre. - -Ezért sokkal biztonságosabb megfordítani a logikát, és explicit módon definiálni az engedélyezett oszlopok listáját (whitelist): - -```php -// Oszlopok, amelyeket a felhasználó módosíthat -$allowedColumns = ['name', 'email', 'active']; - -// Eltávolítjuk az összes nem engedélyezett oszlopot a bemenetből -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Most már biztonságosan használhatjuk a lekérdezésekben, például: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dinamikus azonosítók -==================== - -Dinamikus tábla- és oszlopnevekhez használja a `?name` helyettesítő szimbólumot. Ez biztosítja az azonosítók helyes escapelését az adott adatbázis szintaxisa szerint (pl. backtickek használatával MySQL-ben): - -```php -// ✅ Megbízható azonosítók biztonságos használata -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Eredmény MySQL-ben: SELECT `name` FROM `users` -``` - -Fontos: a `?name` szimbólumot csak az alkalmazás kódjában definiált, megbízható értékekhez használja. Felhasználótól származó értékekhez használja újra a [whitelistet |#Oszlopok Whitelistje]. Ellenkező esetben biztonsági kockázatoknak teszi ki magát: - -```php -// ❌ VESZÉLYES - soha ne használjon felhasználói bemenetet -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/hu/sql-way.texy b/database/hu/sql-way.texy deleted file mode 100644 index ff318344a3..0000000000 --- a/database/hu/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL megközelítés -**************** - -.[perex] -A Nette Database két utat kínál: írhat SQL lekérdezéseket saját maga (SQL megközelítés), vagy hagyhatja, hogy automatikusan generálódjanak (lásd [Explorer |explorer]). Az SQL megközelítés teljes ellenőrzést biztosít a lekérdezések felett, miközben garantálja azok biztonságos összeállítását. - -.[note] -Az adatbázis csatlakozásának és konfigurálásának részleteit a [Csatlakozás és konfiguráció |guide#Csatlakozás és konfiguráció] fejezetben találja. - - -Alapvető lekérdezés -=================== - -Az adatbázis lekérdezéséhez a `query()` metódus szolgál. Ez egy [ResultSet |api:Nette\Database\ResultSet] objektumot ad vissza, amely a lekérdezés eredményét reprezentálja. Hiba esetén a metódus [kivételt dob|exceptions]. A lekérdezés eredményét `foreach` ciklussal járhatjuk be, vagy használhatunk néhány [segédfüggvényt |#Adatlekérés]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Az értékek biztonságos beillesztéséhez az SQL lekérdezésekbe paraméterezett lekérdezéseket használunk. A Nette Database ezt maximálisan egyszerűvé teszi - elegendő az SQL lekérdezés után egy vesszőt és az értéket hozzáadni: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Több paraméter esetén kétféle írásmód lehetséges. Vagy "átszőheti" az SQL lekérdezést paraméterekkel: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Vagy először megírhatja a teljes SQL lekérdezést, majd csatolhatja az összes paramétert: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Védelem az SQL injection ellen -============================== - -Miért fontos paraméterezett lekérdezéseket használni? Mert megvédenek az SQL injection nevű támadástól, amely során a támadó saját SQL parancsokat csempészhetne be, és ezzel adatokat szerezhetne vagy károsíthatna az adatbázisban. - -.[warning] -**Soha ne illesszen be változókat közvetlenül az SQL lekérdezésbe!** Mindig használjon paraméterezett lekérdezéseket, amelyek megvédenek az SQL injection ellen. - -```php -// ❌ VESZÉLYES KÓD - sebezhető az SQL injection-nel szemben -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Biztonságos paraméterezett lekérdezés -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Ismerkedjen meg a [lehetséges biztonsági kockázatokkal |security]. - - -Lekérdezési technikák -===================== - - -WHERE feltételek ----------------- - -A WHERE feltételeket asszociatív tömbként írhatja le, ahol a kulcsok az oszlopnevek, az értékek pedig az összehasonlítandó adatok. A Nette Database automatikusan kiválasztja a legmegfelelőbb SQL operátort az érték típusa alapján. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -A kulcsban explicit módon is megadhatja az összehasonlítási operátort: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // a > operátort használja - 'name LIKE' => '%John%', // a LIKE operátort használja - 'email NOT LIKE' => '%example.com%', // a NOT LIKE operátort használja -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -A Nette automatikusan kezeli a speciális eseteket, mint a `null` értékek vagy tömbök. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // az = operátort használja - 'category_id' => [1, 2, 3], // az IN-t használja - 'description' => null, // az IS NULL-t használja -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Negatív feltételekhez használja a `NOT` operátort: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // a <> operátort használja - 'category_id NOT' => [1, 2, 3], // a NOT IN-t használja - 'description NOT' => null, // az IS NOT NULL-t használja - 'id' => [], // kihagyja -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -A feltételek összekapcsolásához az `AND` operátor használatos. Ezt a [?or helyettesítő karakterrel |#SQL összeállítási tippek] lehet megváltoztatni. - - -ORDER BY szabályok ------------------- - -Az `ORDER BY` rendezést tömb segítségével lehet leírni. A kulcsokban az oszlopokat adjuk meg, az érték pedig egy logikai érték lesz, amely meghatározza, hogy növekvő sorrendben kell-e rendezni: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // növekvő - 'name' => false, // csökkenő -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Adatbeszúrás (INSERT) ---------------------- - -Rekordok beszúrásához az `INSERT` SQL parancsot használjuk. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -A `getInsertId()` metódus visszaadja az utoljára beszúrt sor ID-jét. Néhány adatbázisnál (pl. PostgreSQL) paraméterként meg kell adni annak a szekvenciának a nevét, amelyből az ID-t generálni kell a `$database->getInsertId($sequenceId)` segítségével. - -Paraméterként átadhatunk [#Speciális értékek] is, mint például fájlokat, DateTime objektumokat vagy enum típusokat. - -Több rekord beszúrása egyszerre: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -A többszörös INSERT sokkal gyorsabb, mert egyetlen adatbázis-lekérdezés hajtódik végre, sok különálló helyett. - -**Biztonsági figyelmeztetés:** Soha ne használjon validálatlan adatokat `$values`-ként. Ismerkedjen meg a [lehetséges kockázatokkal |security#Biztonságos munka az oszlopokkal]. - - -Adatfrissítés (UPDATE) ----------------------- - -Rekordok frissítéséhez az `UPDATE` SQL parancsot használjuk. - -```php -// Egy rekord frissítése -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Az érintett sorok számát a `$result->getRowCount()` adja vissza. - -Az UPDATE-hez használhatjuk a `+=` és `-=` operátorokat: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // a login_count inkrementálása -], 1); -``` - -Példa egy rekord beszúrására vagy módosítására, ha már létezik. Az `ON DUPLICATE KEY UPDATE` technikát használjuk: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Figyelje meg, hogy a Nette Database felismeri, milyen kontextusban illesztjük be a tömböt tartalmazó paramétert az SQL parancsba, és ennek megfelelően állítja össze belőle az SQL kódot. Tehát az első tömbből `(id, name, year) VALUES (123, 'Jim', 1978)`-t állított össze, míg a másodikat `name = 'Jim', year = 1978` formára alakította át. Részletesebben ezzel az [#SQL összeállítási tippek] részben foglalkozunk. - - -Adattörlés (DELETE) -------------------- - -Rekordok törléséhez a `DELETE` SQL parancsot használjuk. Példa a törölt sorok számának lekérésével: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -SQL összeállítási tippek ------------------------- - -A hint egy speciális helyettesítő karakter az SQL lekérdezésben, amely megmondja, hogyan kell a paraméter értékét SQL kifejezéssé átírni: - -| Hint | Leírás | Automatikusan használva -|-----------|-------------------------------------------------|----------------------------- -| `?name` | tábla vagy oszlop nevének beillesztésére használja | - -| `?values` | `(key, ...) VALUES (value, ...)`-t generál | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | `key = value, ...` hozzárendelést generál | `SET ?`, `KEY UPDATE ?` -| `?and` | a tömb feltételeit `AND` operátorral köti össze | `WHERE ?`, `HAVING ?` -| `?or` | a tömb feltételeit `OR` operátorral köti össze | - -| `?order` | `ORDER BY` záradékot generál | `ORDER BY ?`, `GROUP BY ?` - -Táblák és oszlopok nevének dinamikus beillesztéséhez a lekérdezésbe a `?name` helyettesítő karakter szolgál. A Nette Database gondoskodik az azonosítók helyes kezeléséről az adott adatbázis konvenciói szerint (pl. backtickekbe zárás MySQL-ben). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (MySQL-ben) -``` - -**Figyelmeztetés:** a `?name` szimbólumot csak validált bemenetekből származó tábla- és oszlopnevekhez használja, különben [biztonsági kockázatnak |security#Dinamikus azonosítók] teszi ki magát. - -A többi hintet általában nem szükséges megadni, mivel a Nette okos automatikus felismerést használ az SQL lekérdezés összeállításakor (lásd a táblázat harmadik oszlopát). De használhatja például olyan helyzetben, amikor a feltételeket `OR` helyett `AND`-del szeretné összekötni: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Speciális értékek ------------------ - -A szokásos skalár típusokon (string, int, bool) kívül speciális értékeket is átadhat paraméterként: - -- fájlok: `fopen('image.gif', 'r')` beilleszti a fájl bináris tartalmát -- dátum és idő: a `DateTime` objektumok adatbázis formátumra konvertálódnak -- enum típusok: az `enum` példányok értékükre konvertálódnak -- SQL literálok: a `Connection::literal('NOW()')` segítségével létrehozottak közvetlenül beillesztődnek a lekérdezésbe - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Azoknál az adatbázisoknál, amelyek nem rendelkeznek natív támogatással a `datetime` adattípushoz (mint a SQLite és az Oracle), a `DateTime` az [adatbázis konfigurációjában|configuration] a `formatDateTime` tétellel meghatározott értékre konvertálódik (az alapértelmezett érték `U` - unix timestamp). - - -SQL literálok -------------- - -Néhány esetben szükség van arra, hogy értékként közvetlenül SQL kódot adjunk meg, amelyet azonban nem szabad stringként értelmezni és escapelni. Erre szolgálnak a `Nette\Database\SqlLiteral` osztály objektumai. Ezeket a `Connection::literal()` metódus hozza létre. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Vagy alternatívaként: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Az SQL literálok tartalmazhatnak paramétereket: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Ennek köszönhetően érdekes kombinációkat hozhatunk létre: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Adatlekérés -=========== - - -Rövidítések SELECT lekérdezésekhez ----------------------------------- - -Az adatbetöltés egyszerűsítésére a `Connection` több rövidítést kínál, amelyek kombinálják a `query()` hívást a következő `fetch*()` hívásokkal. Ezek a metódusok ugyanazokat a paramétereket fogadják el, mint a `query()`, azaz az SQL lekérdezést és az opcionális paramétereket. A `fetch*()` metódusok teljes leírását [alább |#fetch] találja. - -| `fetch($sql, ...$params): ?Row` | Végrehajtja a lekérdezést és visszaadja az első sort `Row` objektumként -| `fetchAll($sql, ...$params): array` | Végrehajtja a lekérdezést és visszaadja az összes sort `Row` objektumok tömbjeként -| `fetchPairs($sql, ...$params): array` | Végrehajtja a lekérdezést és visszaad egy asszociatív tömböt, ahol az első oszlop a kulcs, a második az érték -| `fetchField($sql, ...$params): mixed` | Végrehajtja a lekérdezést és visszaadja az első sor első mezőjének értékét -| `fetchList($sql, ...$params): ?array` | Végrehajtja a lekérdezést és visszaadja az első sort indexelt tömbként - -Példa: - -```php -// fetchField() - visszaadja az első cella értékét -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iteráció a sorokon ------------------------------- - -A lekérdezés végrehajtása után egy [ResultSet|api:Nette\Database\ResultSet] objektumot kapunk vissza, amely lehetővé teszi az eredmények több módon történő bejárását. A legegyszerűbb módja a lekérdezés végrehajtásának és a sorok lekérésének a `foreach` ciklussal történő iterálás. Ez a módszer a memóriatakarékosabb, mivel az adatokat fokozatosan adja vissza, és nem tárolja őket egyszerre a memóriában. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -A `ResultSet`-et csak egyszer lehet iterálni. Ha ismételten kell iterálni, először be kell tölteni az adatokat egy tömbbe, például a `fetchAll()` metódussal. - - -fetch(): ?Row .[method] ------------------------ - -Visszaad egy sort `Row` objektumként. Ha nincs több sor, `null`-t ad vissza. A belső mutatót a következő sorra mozgatja. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // betölti az első sort -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Visszaadja a `ResultSet`-ből az összes fennmaradó sort `Row` objektumok tömbjeként. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // betölti az összes sort -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Visszaadja az eredményeket asszociatív tömbként. Az első argumentum határozza meg az oszlop nevét, amely a tömb kulcsaként lesz használva, a második argumentum határozza meg az oszlop nevét, amely értékként lesz használva: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ha csak az első paramétert adjuk meg, az érték a teljes sor lesz, azaz egy `Row` objektum: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Duplikált kulcsok esetén az utolsó sor értéke lesz használva. Ha `null`-t használunk kulcsként, a tömb numerikusan lesz indexelve nullától kezdve (ekkor nem történik ütközés): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatívaként megadhat egy callbacket paraméterként, amely minden sorhoz vagy magát az értéket, vagy egy kulcs-érték párt ad vissza. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// A callback visszaadhat egy tömböt is kulcs & érték párral: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Visszaadja az aktuális sor első mezőjének értékét. Ha nincs több sor, `null`-t ad vissza. A belső mutatót a következő sorra mozgatja. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // betölti a nevet az első sorból -``` - - -fetchList(): ?array .[method] ------------------------------ - -Visszaad egy sort indexelt tömbként. Ha nincs több sor, `null`-t ad vissza. A belső mutatót a következő sorra mozgatja. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Visszaadja az utolsó `UPDATE` vagy `DELETE` lekérdezés által érintett sorok számát. `SELECT` esetén ez a visszaadott sorok száma, de ez nem mindig ismert - ebben az esetben a metódus `null`-t ad vissza. - - -getColumnCount(): ?int .[method] --------------------------------- - -Visszaadja az oszlopok számát a `ResultSet`-ben. - - -Információk a lekérdezésekről -============================= - -Debuggolási célokra lekérhetjük az utoljára végrehajtott lekérdezés információit: - -```php -echo $database->getLastQueryString(); // kiírja az SQL lekérdezést - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // kiírja az SQL lekérdezést -echo $result->getTime(); // kiírja a végrehajtási időt másodpercben -``` - -Az eredmény HTML táblázatként való megjelenítéséhez használható: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -A ResultSet információkat kínál az oszloptípusokról: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column típusa $type->type"; // pl. 'id típusa int' -} -``` - - -Lekérdezések naplózása ----------------------- - -Implementálhatunk saját lekérdezés-naplózást. Az `onQuery` esemény egy callback tömb, amely minden végrehajtott lekérdezés után meghívódik: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Lekérdezés: ' . $result->getQueryString()); - $logger->info('Idő: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Nagy eredményhalmaz: ' . $result->getRowCount() . ' sor'); - } -}; -``` diff --git a/database/hu/transactions.texy b/database/hu/transactions.texy deleted file mode 100644 index accc0cc112..0000000000 --- a/database/hu/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Tranzakciók -*********** - -.[perex] -A tranzakciók garantálják, hogy a tranzakción belüli összes művelet végrehajtásra kerül, vagy egyik sem. Hasznosak az adatok konzisztenciájának biztosítására összetettebb műveletek során. - -A tranzakciók használatának legegyszerűbb módja a következő: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Ugyanezt sokkal elegánsabban is megírhatja a `transaction()` metódussal. Paraméterként egy callbacket fogad el, amelyet a tranzakcióban hajt végre. Ha a callback kivétel nélkül lefut, a tranzakció automatikusan megerősítésre kerül. Ha kivétel történik, a tranzakció visszavonásra kerül (rollback), és a kivétel tovább terjed. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -A `transaction()` metódus értékeket is visszaadhat: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // visszaadja a frissített sorok számát -}); -``` diff --git a/database/it/@home.texy b/database/it/@home.texy index 97f1e1e9db..0c342c6b51 100644 --- a/database/it/@home.texy +++ b/database/it/@home.texy @@ -1,21 +1,18 @@ - - Database supportati =================== -Nette supporta i seguenti 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Ì | - +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 - awesome database layer for PHP}} -{{description: Nette Database semplifica notevolmente l'ottenimento di dati dal database senza la necessità di scrivere query SQL. Esegue query efficienti e non trasferisce dati inutili.}} +{{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 index 58127bb77b..ffec3672aa 100644 --- a/database/it/@left-menu.texy +++ b/database/it/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** -- [Introduzione |guide] -- [Approccio SQL |sql way] -- [Explorer |Explorer] -- [Transazioni |transactions] -- [Eccezioni |exceptions] -- [Riflessione |reflection] -- [Mappatura |mapping] -- [Configurazione |configuration] -- [Rischi per la sicurezza |security] -- [Aggiornamento |en:upgrading] +- [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/configuration.texy b/database/it/configuration.texy index 543819e346..976d2054fd 100644 --- a/database/it/configuration.texy +++ b/database/it/configuration.texy @@ -2,9 +2,9 @@ Configurazione del database *************************** .[perex] -Panoramica delle opzioni di configurazione per Nette Database. +Panoramica delle opzioni di configurazione di Nette Database. -Se non utilizzate l'intero framework, ma solo questa libreria, leggete [come caricare la configurazione|bootstrap:]. +Se non usate tutto il framework, ma solo questa libreria, leggete [come caricare la configurazione|bootstrap:]. Connessione singola @@ -14,54 +14,54 @@ Configurazione di una singola connessione al database: ```neon database: - # DSN, unica chiave obbligatoria + # DSN, l'unica chiave obbligatoria dsn: "sqlite:%appDir%/Model/demo.db" user: ... password: ... ``` -Crea i servizi `Nette\Database\Connection` e `Nette\Database\Explorer`, che di solito passiamo tramite [autowiring |dependency-injection:autowiring], oppure tramite riferimento al [loro nome |#Servizi DI]. +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: - # visualizzare il pannello del database nella Tracy Bar? - debugger: ... # (bool) il default è true + # mostrare il pannello del database nella Tracy Bar? + debugger: ... # (bool) predefinito è attivo se Tracy è attiva - # visualizzare EXPLAIN delle query nella Tracy Bar? - explain: ... # (bool) il default è true + # mostrare l'EXPLAIN della query nella Tracy Bar? + explain: ... # (bool) predefinito true # abilitare l'autowiring per questa connessione? - autowired: ... # (bool) il default è true per la prima connessione + autowired: ... # (bool) predefinito true per la prima connessione - # convenzioni delle tabelle: discovered, static o nome della classe - conventions: discovered # (string) il default è 'discovered' + # convenzioni delle tabelle: discovered, static oppure il nome di una classe + conventions: discovered # (string) predefinito 'discovered' options: - # connettersi al database solo quando necessario? - lazy: ... # (bool) il default è false + # connettersi al database solo quando serve? + lazy: ... # (bool) predefinito false - # Classe PHP del driver del database + # classe del driver di database PHP driverClass: # (string) # solo MySQL: imposta sql_mode sqlmode: # (string) # solo MySQL: imposta SET NAMES - charset: # (string) il default è 'utf8mb4' + charset: # (string) predefinito 'utf8mb4' # solo MySQL: converte TINYINT(1) in bool - convertBoolean: # (bool) il default è false + convertBoolean: # (bool) predefinito false - # restituisce le colonne data come oggetti immutabili (dalla versione 3.2.1) - newDateTime: # (bool) il default è false + # restituisce le colonne di data come oggetti immutabili (dalla versione 3.2.1) + newDateTime: # (bool) predefinito false - # solo Oracle e SQLite: formato per la memorizzazione della data - formatDateTime: # (string) il default è 'U' + # solo Oracle e SQLite: formato per salvare la data + formatDateTime: # (string) predefinito 'U' ``` -Nella chiave `options` è possibile specificare altre opzioni che trovate nella [documentazione dei driver PDO |https://www.php.net/manual/en/pdo.drivers.php], come ad esempio: +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: @@ -73,7 +73,7 @@ database: Connessioni multiple -------------------- -Nella configurazione possiamo definire anche più connessioni al database dividendole in sezioni denominate: +Nella configurazione possiamo definire più connessioni al database dividendole in sezioni con un nome: ```neon database: @@ -86,23 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -L'autowiring è abilitato solo per i servizi della prima sezione. È possibile modificarlo tramite `autowired: false` o `autowired: true`. +L'autowiring è attivo solo per i servizi della prima sezione. Lo si può cambiare con `autowired: false` oppure `autowired: true`. Servizi DI ---------- -Questi servizi vengono aggiunti al container DI, dove `###` rappresenta il nome della connessione: +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] +| Nome | Tipo | Descrizione +|---------------------------|---------------------------------|--------------------------- +| `database.###.connection` | [api:Nette\Database\Connection] | connessione al database +| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] -Se definiamo solo una 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 inoltre `database.another.connection` e `database.another.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 li passiamo esplicitamente tramite riferimento al loro nome: +I servizi non autowired si passano esplicitamente facendo riferimento al loro nome: ```neon services: diff --git a/database/it/exceptions.texy b/database/it/exceptions.texy index f7ebeb69c1..bc6531f582 100644 --- a/database/it/exceptions.texy +++ b/database/it/exceptions.texy @@ -1,22 +1,25 @@ Eccezioni ********* -Nette Database utilizza una gerarchia di eccezioni. La classe base è `Nette\Database\DriverException`, che eredita da `PDOException` e fornisce funzionalità estese per la gestione degli errori del database: +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 dal driver del database -- Il metodo `getSqlState()` restituisce il codice SQLSTATE -- I metodi `getQueryString()` e `getParameters()` consentono di ottenere la query originale e i suoi parametri +- 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. -Da `DriverException` ereditano le seguenti eccezioni specializzate: +Dalla classe `DriverException` derivano queste eccezioni specializzate: -- `ConnectionException` - segnala un fallimento della connessione al server del database -- `ConstraintViolationException` - classe base per la violazione dei vincoli del database, da cui ereditano: - - `ForeignKeyConstraintViolationException` - violazione della chiave esterna - - `NotNullConstraintViolationException` - violazione del vincolo NOT NULL - - `UniqueConstraintViolationException` - violazione dell'unicità del valore +- `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. - -Esempio di cattura dell'eccezione `UniqueConstraintViolationException`, che si verifica quando cerchiamo di inserire un utente con un'email che esiste già nel database (presupponendo che la colonna email abbia un indice univoco). +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 { @@ -26,9 +29,9 @@ try { 'password' => $hashedPassword, ]); } catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Esiste già un utente con questa email.'; + echo 'Un utente con questa email esiste già.'; } catch (Nette\Database\DriverException $e) { - echo 'Si è verificato un errore durante la registrazione: ' . $e->getMessage(); + echo 'Durante la registrazione si è verificato un errore: ' . $e->getMessage(); } ``` diff --git a/database/it/explorer.texy b/database/it/explorer.texy index 155338a3f9..508447a718 100644 --- a/database/it/explorer.texy +++ b/database/it/explorer.texy @@ -3,81 +3,81 @@ Database Explorer <div class=perex> -Explorer offre un modo intuitivo ed efficiente di lavorare con il database. Si occupa automaticamente delle relazioni tra le tabelle e dell'ottimizzazione delle query, così potete concentrarvi sulla vostra applicazione. Funziona immediatamente senza alcuna impostazione. Se avete bisogno del pieno controllo sulle query SQL, potete utilizzare l'[approccio SQL |SQL way]. +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]. -- Il lavoro con i dati è naturale e facile da capire +- Lavorare con i dati è naturale e facile da capire - Genera query SQL ottimizzate che caricano solo i dati necessari -- Permette un facile accesso ai dati correlati senza la necessità di scrivere query JOIN -- Funziona immediatamente senza alcuna configurazione o generazione di entità +- Permette un accesso semplice ai dati collegati senza dover scrivere query JOIN +- Funziona subito, senza alcuna configurazione né generazione di entità </div> -Con Explorer iniziate chiamando il metodo `table()` dell'oggetto [api:Nette\Database\Explorer] (dettagli sulla connessione li trovate nel capitolo [Connessione e configurazione |guide#Connessione e configurazione]): +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 possiamo concatenare altri metodi per filtrare e ordinare i risultati. La query viene costruita ed eseguita solo nel momento in cui iniziamo a richiedere i dati. Ad esempio, iterando con un ciclo `foreach`. Ogni riga è rappresentata da un oggetto [ActiveRow |api:Nette\Database\Table\ActiveRow]: +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 della colonna 'title' - echo $book->author_id; // stampa della colonna 'author_id' + echo $book->title; // stampa la colonna 'title' + echo $book->author_id; // stampa la colonna 'author_id' } ``` -Explorer semplifica notevolmente il lavoro con le [#relazioni tra tabelle]. L'esempio seguente mostra quanto sia facile visualizzare i dati da tabelle correlate (libri e i loro autori). Notate che non dobbiamo scrivere alcuna query JOIN, Nette le crea per noi: +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 sulla tabella 'author' + echo 'Autore: ' . $book->author->name; // crea un JOIN alla tabella 'author' } ``` -Nette Database Explorer ottimizza le query affinché siano il più efficienti possibile. L'esempio sopra esegue solo due query SELECT, indipendentemente dal fatto che stiamo elaborando 10 o 10.000 libri. +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. -Inoltre, Explorer tiene traccia di quali colonne vengono utilizzate nel codice e carica dal database solo quelle, risparmiando ulteriore performance. Questo comportamento è completamente automatico e adattivo. Se successivamente modificate il codice e iniziate a utilizzare altre colonne, Explorer adatterà automaticamente le query. Non dovete impostare nulla, né pensare a quali colonne vi serviranno - lasciate fare a Nette. +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` fornisce metodi per filtrare e ordinare la selezione dei dati. +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 sono unite dall'operatore AND -| `whereOr(array $conditions)` | Aggiunge un gruppo di condizioni WHERE unite dall'operatore OR -| `wherePrimary($value)` | Aggiunge una condizione WHERE in base alla chiave primaria -| `order($columns, ...$params)` | Imposta l'ordinamento ORDER BY -| `select($columns, ...$params)` | Specifica le colonne da caricare -| `limit($limit, $offset = null)` | Limita il numero di righe (LIMIT) e opzionalmente imposta OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Imposta la paginazione -| `group($columns, ...$params)` | Raggruppa le righe (GROUP BY) -| `having($condition, ...$params)` | Aggiunge una condizione HAVING per filtrare le righe raggruppate +| `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 possono essere concatenati (il cosiddetto [fluent interface |nette:introduction-to-object-oriented-programming#Interfacce fluenti]): `$table->where(...)->order(...)->limit(...)`. +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 anche utilizzare una notazione speciale per accedere ai [dati da tabelle correlate |#Interrogazione tramite tabelle correlate]. +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 racchiudono tra virgolette gli identificatori (nomi di tabelle e colonne), prevenendo così SQL injection. Per un corretto funzionamento è necessario rispettare alcune regole: +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 le parole chiave, i nomi di funzioni, procedure ecc. in **maiuscolo**. - Scrivete i nomi di colonne e tabelle in **minuscolo**. -- Inserite sempre le stringhe tramite **parametri**. +- Passate sempre le stringhe tramite **parametri**. ```php -where('name = ' . $name); // VULNERABILITÀ CRITICA: SQL injection -where('name LIKE "%search%"'); // SBAGLIato: complica l'inserimento automatico delle virgolette -where('name LIKE ?', '%search%'); // CORRETTO: valore inserito tramite parametro +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 ? @@ -88,9 +88,9 @@ where('LOWER(name) = ?', $value);// CORRETTO: LOWER(`name`) = ? where(string|array $condition, ...$parameters): static .[method] ---------------------------------------------------------------- -Filtra i risultati tramite condizioni WHERE. Il suo punto di forza è il lavoro intelligente con diversi tipi di valori e la scelta automatica degli operatori SQL. +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 base: +Uso di base: ```php $table->where('id', $value); // WHERE `id` = 123 @@ -98,13 +98,13 @@ $table->where('id > ?', $value); // WHERE `id` > 123 $table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' ``` -Grazie al rilevamento automatico degli operatori appropriati, non dobbiamo gestire diversi casi speciali. Nette li risolve per noi: +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) -// è possibile utilizzare anche il placeholder punto interrogativo senza operatore: +// potete usare anche il segnaposto ? senza operatore: $table->where('id ?', 1); // WHERE `id` = 1 ``` @@ -114,10 +114,10 @@ Il metodo gestisce correttamente anche le condizioni negative e gli array vuoti: $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 +// $table->where('NOT id ?', $ids); // ATTENZIONE: questa sintassi non è supportata ``` -Come parametro possiamo passare anche il risultato di un'altra tabella - verrà creata una sottoquery: +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`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -Possiamo passare le condizioni anche come array, i cui elementi verranno uniti tramite AND: +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`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -Nell'array possiamo utilizzare coppie chiave => valore e Nette sceglierà nuovamente automaticamente gli operatori corretti: +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)) @@ -147,23 +147,23 @@ $table->where([ ]); ``` -Nell'array possiamo combinare espressioni SQL con placeholder punto interrogativo e più parametri. Questo è adatto per condizioni complesse con operatori definiti con precisione: +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], // passiamo due parametri come array + 'ROUND(score, ?) > ?' => [2, 75.5], // i due parametri si passano come array ]); ``` -Chiamate multiple a `where()` uniscono automaticamente le condizioni tramite AND. +Più chiamate a `where()` uniscono automaticamente le condizioni con AND. whereOr(array $parameters): static .[method] -------------------------------------------- -Simile a `where()`, aggiunge condizioni, ma con la differenza che le unisce tramite OR: +Analogamente a `where()` aggiunge delle condizioni, ma le unisce con OR: ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -Anche qui possiamo utilizzare espressioni più complesse: +Anche qui si possono usare espressioni più complesse: ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -187,7 +187,7 @@ $table->whereOr([ wherePrimary(mixed $key): static .[method] ------------------------------------------ -Aggiunge una condizione per la chiave primaria della tabella: +Aggiunge una condizione sulla chiave primaria della tabella: ```php // WHERE `id` = 123 @@ -197,7 +197,7 @@ $table->wherePrimary(123); $table->wherePrimary([1, 2, 3]); ``` -Se la tabella ha una chiave primaria composita (ad es. `foo_id`, `bar_id`), la passiamo come array: +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 @@ -214,7 +214,7 @@ $table->wherePrimary([ order(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Determina l'ordine in cui verranno restituite le righe. Possiamo ordinare per una o più colonne, in ordine ascendente o discendente, o secondo un'espressione personalizzata: +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` @@ -227,14 +227,14 @@ $table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC select(string $columns, ...$parameters): static .[method] --------------------------------------------------------- -Specifica le colonne che devono essere restituite dal database. Per impostazione predefinita, Nette Database Explorer restituisce solo le colonne che vengono effettivamente utilizzate nel codice. Il metodo `select()` viene quindi utilizzato nei casi in cui è necessario restituire espressioni specifiche: +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 tramite `AS` sono quindi disponibili come proprietà dell'oggetto ActiveRow: +Gli alias definiti con `AS` sono poi accessibili come proprietà dell'oggetto `ActiveRow`: ```php foreach ($table as $row) { @@ -246,24 +246,24 @@ foreach ($table as $row) { limit(?int $limit, ?int $offset = null): static .[method] --------------------------------------------------------- -Limita il numero di righe restituite (LIMIT) e opzionalmente consente di impostare un offset: +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 è preferibile utilizzare il metodo `page()`. +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 (contato da 1) e il numero di elementi per pagina. Opzionalmente è possibile passare un riferimento a una variabile in cui verrà memorizzato il numero totale di pagine: +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); +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); echo "Pagine totali: $numOfPages"; ``` @@ -271,10 +271,10 @@ echo "Pagine totali: $numOfPages"; group(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Raggruppa le righe in base alle colonne specificate (GROUP BY). Viene utilizzato solitamente in combinazione con funzioni di aggregazione: +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 +// conta il numero di prodotti in ogni categoria $table->select('category_id, COUNT(*) AS count') ->group('category_id'); ``` @@ -283,41 +283,41 @@ $table->select('category_id, COUNT(*) AS count') having(string $having, ...$parameters): static .[method] -------------------------------------------------------- -Imposta una condizione per filtrare le righe raggruppate (HAVING). Può essere utilizzata in combinazione con il metodo `group()` e le funzioni di aggregazione: +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 +// trova le categorie che hanno più di 100 prodotti $table->select('category_id, COUNT(*) AS count') ->group('category_id') ->having('count > ?', 100); ``` -Lettura dei dati -================ +Leggere i dati +============== -Per leggere i dati dal database abbiamo a disposizione diversi metodi utili: +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` è l'oggetto ActiveRow -| `$row = $table->get($key)` | Restituisce una riga in base alla chiave primaria -| `$row = $table->fetch()` | Restituisce la riga corrente e sposta il puntatore alla 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 +| `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] è destinato solo alla lettura. Ciò significa che non è possibile modificare i valori delle sue proprietà. Questa limitazione garantisce la coerenza dei dati e previene effetti collaterali imprevisti. I dati vengono caricati dal database e qualsiasi modifica dovrebbe essere eseguita esplicitamente e in modo controllato. +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 per eseguire una query e ottenere le righe è iterare in un ciclo `foreach`. Avvia automaticamente la query SQL. +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 è ActiveRow + // $key è il valore della chiave primaria, $book è un ActiveRow echo "$book->title ({$book->author->name})"; } ``` @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -Esegue la query SQL e restituisce la riga in base alla chiave primaria, o `null` se non esiste. +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 ActiveRow con ID 123 o null +$book = $explorer->table('book')->get(123); // restituisce l'ActiveRow con ID 123 oppure null if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -Restituisce una riga e sposta il puntatore interno alla successiva. Se non esistono più altre righe, restituisce `null`. +Restituisce la riga corrente e sposta il puntatore interno a quella successiva. Se non esistono altre righe, restituisce `null`. ```php $books = $explorer->table('book'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Restituisce i risultati come array associativo. Il primo argomento specifica il nome della colonna che verrà utilizzata come chiave nell'array, il secondo argomento specifica il nome della colonna che verrà utilizzata come valore: +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 specifichiamo solo il primo parametro, il valore sarà l'intera riga, ovvero l'oggetto `ActiveRow`: +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, verrà utilizzato il valore dell'ultima riga. Utilizzando `null` come chiave, l'array sarà indicizzato numericamente da zero (quindi non si verificano collisioni): +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'); @@ -377,17 +377,17 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -In alternativa, potete specificare come parametro un callback, che per ogni riga restituirà o il valore stesso, o una coppia chiave-valore. +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 (Jan Novák)', ...] +// ['Primo libro (John Novak)', ...] -// Il callback può anche restituire un array con una coppia chiave & valore: +// 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' => 'Jan Novák', ...] +// ['Primo libro' => 'John Novak', ...] ``` @@ -405,7 +405,7 @@ $allBooks = $explorer->table('book')->fetchAll(); count(): int .[method] ---------------------- -Il metodo `count()` senza parametro restituisce il numero di righe nell'oggetto `Selection`: +Il metodo `count()` senza parametri restituisce il numero di righe nell'oggetto `Selection`: ```php $table->where('category', 1); @@ -413,13 +413,13 @@ $count = $table->count(); $count = count($table); // alternativa ``` -Attenzione, `count()` con parametro esegue la funzione di aggregazione COUNT nel database, vedi sotto. +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 sono i dati corrispondenti. +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); @@ -431,33 +431,33 @@ $bookArray = $book->toArray(); Aggregazione ============ -La classe `Selection` fornisce metodi per eseguire facilmente funzioni di aggregazione (COUNT, SUM, MIN, MAX, AVG ecc.). +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 nella colonna -| `max($expr)` | Restituisce il valore massimo nella colonna -| `sum($expr)` | Restituisce la somma dei valori nella colonna -| `aggregation($function)` | Permette di eseguire qualsiasi funzione di aggregazione. Ad esempio, `AVG()`, `GROUP_CONCAT()` +| `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 viene utilizzato per determinare quante righe corrispondono a una determinata condizione: +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 parametro restituisce solo il numero di righe nell'oggetto `Selection`. +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 nella colonna o espressione specificata: +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 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -Restituisce la somma dei valori nella colonna o espressione specificata: +Restituisce la somma dei valori della colonna o dell'espressione indicata: ```php // SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 @@ -478,39 +478,39 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -Permette di eseguire qualsiasi funzione di aggregazione. +Permette di eseguire una funzione di aggregazione qualsiasi. ```php -// prezzo medio dei prodotti nella categoria +// prezzo medio dei prodotti di una categoria $avgPrice = $products->where('category_id', 1) ->aggregation('AVG(price)'); -// unisce le etichette del prodotto in un'unica stringa +// unisce i tag dei prodotti in un'unica stringa $tags = $products->where('id', 1) ->aggregation('GROUP_CONCAT(tag.name) AS tags') ->fetch() ->tags; ``` -Se abbiamo bisogno di aggregare risultati che sono già essi stessi derivati da qualche funzione di aggregazione e raggruppamento (ad es. `SUM(valore)` su righe raggruppate), come secondo argomento specifichiamo la funzione di aggregazione che deve essere applicata a questi risultati intermedi: +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 ogni categoria e poi somma questi prezzi insieme. +// 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, prima calcoliamo il prezzo totale dei prodotti in ogni categoria (`SUM(price * stock) AS category_total`) e raggruppiamo i risultati per `category_id`. Poi utilizziamo `aggregation('SUM(category_total)', 'SUM')` per sommare questi subtotali `category_total`. Il secondo argomento `'SUM'` dice che la funzione SUM deve essere applicata ai risultati intermedi. +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 & Delete +Insert, Update e Delete ======================= -Nette Database Explorer semplifica l'inserimento, l'aggiornamento e l'eliminazione dei dati. Tutti i metodi menzionati lanciano un'eccezione `Nette\Database\DriverException` in caso di errore. +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] @@ -520,9 +520,9 @@ Inserisce nuovi record nella tabella. **Inserimento di un singolo record:** -Passiamo il nuovo record come array associativo o oggetto iterabile (ad esempio ArrayHash utilizzato nei [moduli |forms:]), dove le chiavi corrispondono ai nomi delle colonne nella tabella. +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 tenere conto di eventuali modifiche apportate a livello di database (trigger, valori predefiniti delle colonne, calcoli delle colonne auto-increment). Questo garantisce la coerenza dei dati e l'oggetto contiene sempre i dati attuali dal database. Se non ha una chiave primaria univoca, restituisce i dati passati sotto forma di array. +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([ @@ -530,14 +530,14 @@ $row = $explorer->table('users')->insert([ 'email' => 'john.doe@example.com', ]); // $row è un'istanza di ActiveRow e contiene i dati completi della riga inserita, -// incluso l'ID generato automaticamente e eventuali modifiche apportate dai trigger -echo $row->id; // Stampa l'ID dell'utente appena inserito -echo $row->created_at; // Stampa l'ora di creazione, se impostata da un trigger +// 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 contemporaneamente:** +**Inserimento di più record in una volta:** -Il metodo `insert()` consente di inserire più record tramite un'unica query SQL. In questo caso, restituisce il numero di righe inserite. +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([ @@ -554,7 +554,7 @@ $insertedRows = $explorer->table('users')->insert([ // $insertedRows sarà 2 ``` -Come parametro è possibile passare anche un oggetto `Selection` con una selezione di dati. +Come parametro si può passare anche un oggetto `Selection` con una selezione di dati. ```php $newUsers = $explorer->table('potential_users') @@ -566,7 +566,7 @@ $insertedRows = $explorer->table('users')->insert($newUsers); **Inserimento di valori speciali:** -Come valori possiamo passare anche file, oggetti DateTime o letterali SQL: +Come valori possiamo passare anche file, oggetti `DateTime` o letterali SQL: ```php $explorer->table('users')->insert([ @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -Aggiorna le righe nella tabella secondo il filtro specificato. Restituisce il numero di righe effettivamente modificate. +Aggiorna le righe della tabella secondo il filtro indicato. Restituisce il numero di righe effettivamente modificate. -Passiamo le colonne da modificare come array associativo o oggetto iterabile (ad esempio ArrayHash utilizzato nei [moduli |forms:]), dove le chiavi corrispondono ai nomi delle colonne nella tabella: +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') @@ -595,14 +595,14 @@ $affected = $explorer->table('users') // UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Per modificare i valori numerici possiamo utilizzare gli operatori `+=` e `-=`: +Per modificare i valori numerici potete usare gli operatori `+=` e `-=`: ```php $explorer->table('users') ->where('id', 10) ->update([ - 'points+=' => 1, // aumenta il valore della colonna 'points' di 1 - 'coins-=' => 1, // diminuisce il valore della colonna 'coins' di 1 + '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 ``` @@ -611,7 +611,7 @@ $explorer->table('users') Selection::delete(): int .[method] ---------------------------------- -Elimina le righe dalla tabella secondo il filtro specificato. Restituisce il numero di righe eliminate. +Cancella le righe dalla tabella secondo il filtro indicato. Restituisce il numero di righe cancellate. ```php $count = $explorer->table('users') @@ -621,111 +621,111 @@ $count = $explorer->table('users') ``` .[caution] -Quando si chiamano `update()` e `delete()`, non dimenticate di specificare tramite `where()` le righe che devono essere modificate/eliminate. Se non si utilizza `where()`, l'operazione verrà eseguita sull'intera tabella! +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`. Come parametro accetta un iterabile con i dati da aggiornare (le chiavi sono i nomi delle colonne). Per modificare i valori numerici possiamo utilizzare gli operatori `+=` e `-=`: +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'esecuzione dell'aggiornamento, `ActiveRow` viene automaticamente ricaricato dal database per tenere conto di eventuali modifiche apportate a livello di database (ad es. trigger). Il metodo restituisce true solo se si è verificata una modifica effettiva dei dati. +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', // aumentiamo il numero di visualizzazioni + 'views += 1', // incrementa il numero di visualizzazioni ]); -echo $article->views; // Stampa il numero attuale di visualizzazioni +echo $article->views; // stampa il numero attuale di visualizzazioni ``` -Questo metodo aggiorna solo una riga specifica nel database. Per l'aggiornamento massivo di più righe, utilizzate il metodo [#Selection::update()]. +Questo metodo aggiorna una sola riga concreta del database. Per l'aggiornamento massivo di più righe usate il metodo [#Selection::update()]. -ActiveRow::delete() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -Elimina la riga dal database rappresentata dall'oggetto `ActiveRow`. +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(); // Elimina il libro con ID 1 +$book->delete(); // cancella il libro con ID 1 ``` -Questo metodo elimina solo una riga specifica nel database. Per l'eliminazione massiva di più righe, utilizzate il metodo [#Selection::delete()]. +Questo metodo cancella una sola riga concreta del database. Per la cancellazione massiva di più righe usate il metodo [#Selection::delete()]. -Relazioni tra tabelle -===================== +Relazioni tra le tabelle +======================== -Nei database relazionali, i dati sono divisi in più tabelle e collegati tra loro tramite chiavi esterne. Nette Database Explorer introduce un modo rivoluzionario per lavorare con queste relazioni - senza scrivere query JOIN e senza la necessità di configurare o generare nulla. +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 illustrare il lavoro con le relazioni, utilizzeremo un esempio di database di libri ([lo trovate su GitHub |https://github.com/nette-examples/books]). Nel database abbiamo le tabelle: +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` - etichette (colonne `id`, `name`) -- `book_tag` - tabella di collegamento tra libri ed etichette (colonne `book_id`, `tag_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 utilizzata negli esempi *** +[* db-schema-1-.webp *] *** Struttura del database usata negli esempi .<> -Nel nostro esempio di database di libri troviamo diversi tipi di relazioni (sebbene il modello sia semplificato rispetto alla realtà): +Nel nostro database di libri di esempio troviamo diversi tipi di relazione (anche se il modello è semplificato rispetto alla realtà): -- One-to-many 1:N – ogni libro **ha un** autore, un autore può scrivere **diversi** libri -- Zero-to-many 0:N – un libro **può avere** un traduttore, un traduttore può tradurre **diversi** libri -- Zero-to-one 0:1 – un libro **può avere** un seguito -- Many-to-many M:N – un libro **può avere diverse** etichette e un'etichetta può essere assegnata a **diversi** libri +- **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 esiste sempre una tabella padre e una figlia. Ad esempio, nella relazione tra autore e libro, la tabella `author` è padre e `book` è figlia - possiamo immaginarlo come se il libro "appartenesse" sempre a qualche autore. Questo si riflette anche nella struttura del database: la tabella figlia `book` contiene la chiave esterna `author_id`, che fa riferimento alla tabella padre `author`. +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 abbiamo bisogno di visualizzare i libri inclusi i nomi dei loro autori, abbiamo due possibilità. O otteniamo i dati con un'unica query SQL tramite JOIN: +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 +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Oppure carichiamo i dati in due passaggi - prima i libri e poi i loro autori - e poi li assembliamo in PHP: +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 ottenuti +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 solo una volta e possono essere utilizzati meglio nella cache. È proprio in questo modo che lavora Nette Database Explorer - risolve tutto sotto il cofano e vi offre un'API elegante: +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 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author è un record della tabella 'author' - echo 'translated by: ' . $book->translator?->name; + 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 padre --------------------------- +Accesso alla tabella genitore +----------------------------- -L'accesso alla tabella padre è diretto. Si tratta di relazioni come *il libro ha un autore* o *il libro può avere un traduttore*. Otteniamo il record correlato tramite la proprietà dell'oggetto ActiveRow - il suo nome corrisponde al nome della colonna con la chiave esterna senza `id`: +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 a translator_id +echo $book->translator?->name; // trova il traduttore in base alla colonna translator_id ``` -Quando accediamo alla proprietà `$book->author`, Explorer cerca nella tabella `book` una colonna il cui nome contiene la stringa `author` (cioè `author_id`). In base al valore in questa colonna, carica il record corrispondente dalla tabella `author` e lo restituisce come `ActiveRow`. Funziona in modo simile anche `$book->translator`, che utilizza la colonna `translator_id`. Poiché la colonna `translator_id` può contenere `null`, utilizziamo nel codice l'operatore `?->`. +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 percorso alternativo è offerto dal 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` o `null`: +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()` è utile se non è possibile utilizzare l'accesso tramite proprietà, perché la tabella contiene una colonna con lo stesso nome (cioè `author`). Negli altri casi è consigliato utilizzare l'accesso tramite proprietà, che è più leggibile. +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 iteriamo sui libri in un ciclo e accediamo ai loro record correlati (autori, traduttori), Explorer non genera una query per ogni libro separatamente. Invece, esegue solo un SELECT per ogni tipo di relazione, riducendo significativamente il carico sul database. Ad esempio: +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'); @@ -736,53 +736,53 @@ foreach ($books as $book) { } ``` -Questo codice chiamerà solo queste tre velocissime query al database: +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 +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 di ricerca della colonna di collegamento è data dall'implementazione di [Conventions |api:Nette\Database\Conventions]. Consigliamo l'uso di [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], che analizza le chiavi esterne e consente di lavorare semplicemente con le relazioni esistenti tra le tabelle. +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* o *ha tradotto questo traduttore*. Per questo tipo di query utilizziamo il metodo `related()`, che restituisce una `Selection` con i record correlati. Vediamo un esempio: +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 +// 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 +// 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 della connessione come un unico argomento con notazione a punti o come due argomenti separati: +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 è in grado di rilevare automaticamente la colonna di collegamento corretta in base al nome della tabella padre. In questo caso, si collega tramite la colonna `book.author_id`, poiché il nome della tabella di origine è `author`: +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ù connessioni possibili, Explorer lancerebbe un'eccezione [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Se esistessero più collegamenti possibili, Explorer lancia una [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Il metodo `related()` può ovviamente essere utilizzato anche iterando su più record in un ciclo e Explorer anche in questo caso ottimizza automaticamente le query: +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'); @@ -794,18 +794,18 @@ foreach ($authors as $author) { } ``` -Questo codice genererà solo due velocissime query SQL: +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 +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ID degli autori selezionati ``` -Relazione Many-to-many ----------------------- +Relazione molti a molti +----------------------- -Per la relazione many-to-many (M:N) è necessaria l'esistenza di una tabella di collegamento (nel nostro caso `book_tag`), che contiene due colonne con chiavi esterne (`book_id`, `tag_id`). Ciascuna di queste colonne fa riferimento alla chiave primaria di una delle tabelle collegate. Per ottenere i dati correlati, otteniamo prima i record dalla tabella di collegamento tramite `related('book_tag')` e poi proseguiamo verso i dati di destinazione: +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); @@ -815,89 +815,89 @@ foreach ($book->related('book_tag') as $bookTag) { } $tag = $explorer->table('tag')->get(1); -// o viceversa: stampa i nomi dei libri contrassegnati da questo tag +// 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 nuovamente le query SQL in una forma efficiente: +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 +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 ``` -Interrogazione tramite tabelle correlate ----------------------------------------- +Interrogare attraverso le tabelle collegate +------------------------------------------- -Nei metodi `where()`, `select()`, `order()` e `group()` possiamo utilizzare notazioni speciali per accedere alle colonne di altre tabelle. Explorer creerà automaticamente i JOIN necessari. +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 punti** (`tabella_padre.colonna`) viene utilizzata per la relazione 1:N dal punto di vista della tabella figlia: +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 autore ha un nome che inizia per 'Jon' +// 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 +// 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 +// stampa il titolo del libro e il nome dell'autore $books->select('book.title, author.name'); ``` -La **notazione a due punti** (`:tabella_figlia.colonna`) viene utilizzata per la relazione 1:N dal punto di vista della tabella padre: +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 +// trova gli autori che hanno scritto un libro con 'PHP' nel titolo $authors->where(':book.title LIKE ?', '%PHP%'); -// Conta il numero di libri per ogni autore +// 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 è specificata la colonna con la chiave esterna. Explorer rileva automaticamente la colonna corretta in base al nome della tabella padre. In questo caso, si collega tramite la colonna `book.author_id`, poiché il nome della tabella di origine è `author`. Se esistessero più connessioni possibili, Explorer lancerebbe un'eccezione [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +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 può essere specificata esplicitamente tra parentesi: +La colonna di collegamento si può indicare esplicitamente tra parentesi: ```php -// Trova gli autori che hanno tradotto un libro con 'PHP' nel titolo +// trova gli autori che hanno tradotto un libro con 'PHP' nel titolo $authors->where(':book(translator_id).title LIKE ?', '%PHP%'); ``` -Le notazioni possono essere concatenate per accedere tramite più tabelle: +Le notazioni si possono concatenare per accedere ai dati attraverso più tabelle: ```php -// Trova gli autori dei libri contrassegnati con il tag 'PHP' +// trova gli autori dei libri contrassegnati con il tag 'PHP' $authors->where(':book:book_tag.tag.name', 'PHP') ->group('author.id'); ``` -Estensione delle condizioni per JOIN ------------------------------------- +Estendere le condizioni per JOIN +-------------------------------- -Il metodo `joinWhere()` estende le condizioni che vengono specificate durante il collegamento delle tabelle in SQL dopo la parola chiave `ON`. +Il metodo `joinWhere()` estende le condizioni indicate quando si collegano le tabelle in SQL dopo la parola chiave `ON`. -Supponiamo di voler trovare i libri tradotti da un traduttore specifico: +Diciamo che vogliamo trovare i libri tradotti da un certo traduttore: ```php -// Trova i libri tradotti dal traduttore di nome 'David' +// 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 `joinWhere()` possiamo utilizzare le stesse costruzioni del metodo `where()` - operatori, placeholder punto interrogativo, array di valori o espressioni SQL. +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, possiamo definire alias di tabelle: +Per query più complesse con più JOIN potete definire degli alias per le tabelle: ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // 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` durante il collegamento delle tabelle. +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 index 946c94506a..83fe3e156c 100644 --- a/database/it/guide.texy +++ b/database/it/guide.texy @@ -2,30 +2,30 @@ Nette Database ************** .[perex] -Nette Database è un layer di database potente ed elegante per PHP, con un'enfasi sulla semplicità e sulle funzionalità intelligenti. Offre due modi per lavorare con il database: [Explorer |Explorer] per lo sviluppo rapido di applicazioni, o [l'accesso SQL |SQL way] per lavorare direttamente con le query. +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> -[Accesso SQL |SQL way] -====================== -- Query parametrizzate sicure -- Controllo preciso sulla forma delle query SQL -- Quando si scrivono query complesse con funzionalità avanzate -- Ottimizzazione delle performance utilizzando funzioni SQL specifiche +[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] +[Explorer |explorer] ==================== -- Sviluppo rapido senza scrivere SQL -- Lavoro intuitivo con le relazioni tra tabelle -- Apprezzerete l'ottimizzazione automatica delle query -- Adatto per un lavoro rapido e comodo con il database +- 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> @@ -35,7 +35,7 @@ Nette Database è un layer di database potente ed elegante per PHP, con un'enfas Installazione ============= -Scarica e installa la libreria utilizzando lo strumento [Composer|best-practices:composer]: +La libreria si scarica e si installa con [Composer|best-practices:composer]: ```shell composer require nette/database @@ -45,25 +45,25 @@ composer require nette/database Database supportati =================== -Nette Database supporta i seguenti database: +Nette Database supporta questi database: -|* Server Database |* Nome DSN |* Supporto in Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | SÌ -| PostgreSQL (>= 9.0) | pgsql | SÌ -| Sqlite 3 (>= 3.8) | sqlite | SÌ -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SÌ -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - +|* 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 database -======================== +Due approcci al lavoro con il database +====================================== -Nette Database ti dà una scelta: puoi scrivere direttamente le query SQL (accesso SQL) o lasciarle generare automaticamente (Explorer). Vediamo come entrambi gli approcci risolvono gli stessi compiti: +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: -[Accesso SQL|sql way] - Query SQL +[Approccio SQL|sql-way] - query SQL ```php // inserimento di un record @@ -73,7 +73,7 @@ $database->query('INSERT INTO books', [ 'published_at' => new DateTime, ]); -// recupero dei record: autori dei libri +// ottenimento dei record: autori dei libri $result = $database->query(' SELECT authors.*, COUNT(books.id) AS books_count FROM authors @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// output (non ottimale, genera N query aggiuntive) +// visualizzazione (non ottimale, genera N query aggiuntive) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -90,7 +90,7 @@ foreach ($result as $author) { ORDER BY published_at DESC ', $author->id); - echo "Autore $author->name ha scritto $author->books_count libri:\n"; + echo "L'autore $author->name ha scritto $author->books_count libri:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -98,7 +98,7 @@ foreach ($result as $author) { } ``` -[Approccio Explorer|explorer] - generazione automatica di SQL +[Approccio Explorer|explorer] - generazione automatica dell'SQL ```php // inserimento di un record @@ -108,16 +108,16 @@ $database->table('books')->insert([ 'published_at' => new DateTime, ]); -// recupero dei record: autori dei libri +// ottenimento dei record: autori dei libri $authors = $database->table('authors') ->where('active', 1); -// output (genera automaticamente solo 2 query ottimizzate) +// visualizzazione (genera automaticamente solo 2 query ottimizzate) foreach ($authors as $author) { $books = $author->related('books') ->order('published_at DESC'); - echo "Autore $author->name ha scritto {$books->count()} libri:\n"; + echo "L'autore $author->name ha scritto {$books->count()} libri:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -125,23 +125,23 @@ foreach ($authors as $author) { } ``` -L'approccio Explorer genera e ottimizza automaticamente le query SQL. Nell'esempio fornito, l'accesso SQL genera N+1 query (una per gli autori e poi una per i libri di ciascun autore), mentre Explorer ottimizza automaticamente le query ed ne esegue solo due: una per gli autori e una per tutti i loro libri. +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. -Entrambi gli approcci possono essere combinati liberamente nell'applicazione secondo necessità. +I due approcci si possono combinare liberamente nella vostra applicazione secondo le necessità. Connessione e configurazione ============================ -Per connettersi al database, è sufficiente creare un'istanza della classe [api:Nette\Database\Connection]: +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 [quello utilizzato da PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], ad esempio `host=127.0.0.1;dbname=test`. In caso di fallimento, lancia un'eccezione `Nette\Database\ConnectionException`. +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`. -Tuttavia, un modo più pratico è offerto dalla [configurazione dell'applicazione |configuration], dove è sufficiente aggiungere la sezione `database` e verranno creati gli oggetti necessari e anche il pannello del database nella barra di [Tracy |tracy:]. +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: @@ -150,13 +150,13 @@ database: password: password ``` -Successivamente, [otteniamo come servizio dal container DI |dependency-injection:passing-dependencies] l'oggetto della connessione, ad esempio: +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( - // o Nette\Database\Explorer + // oppure Nette\Database\Explorer private Nette\Database\Connection $database, ) { } @@ -166,19 +166,19 @@ class Model Maggiori informazioni sulla [configurazione del database|configuration]. -Creazione manuale di Explorer ------------------------------ +Creazione manuale dell'Explorer +------------------------------- -Se non si utilizza il container Nette DI, è possibile creare manualmente un'istanza di `Nette\Database\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 per la cache, implementa Nette\Caching\Storage, ad esempio: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// si occupa della riflessione della struttura del database +// 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 la mappatura dei nomi di tabelle, colonne e chiavi esterne +// 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); ``` @@ -187,18 +187,18 @@ $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $ Gestione della connessione ========================== -Quando viene creato un oggetto `Connection`, la connessione viene stabilita automaticamente. Se si desidera posticipare la connessione, utilizzare la modalità lazy - questa può essere abilitata nella [configurazione|configuration] impostando `lazy` su true, o in questo modo: +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, utilizzare i metodi `connect()`, `disconnect()` e `reconnect()`. -- `connect()` crea una connessione se non esiste già, e può lanciare un'eccezione `Nette\Database\ConnectionException`. -- `disconnect()` disconnette la connessione corrente al database. -- `reconnect()` esegue la disconnessione e la successiva riconnessione al database. Questo metodo può anche lanciare un'eccezione `Nette\Database\ConnectionException`. +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`. -Inoltre, è possibile monitorare gli eventi associati alla connessione utilizzando l'evento `onConnect`, che è un array di callback che vengono chiamati dopo aver stabilito una connessione al database. +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 @@ -207,10 +207,12 @@ $database->onConnect[] = function($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 utilizzi [Tracy |tracy:], il pannello Database viene attivato automaticamente nella Debug Bar, mostrando tutte le query eseguite, i loro parametri, il tempo di esecuzione e la posizione nel codice in cui sono state chiamate. +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/mapping.texy b/database/it/mapping.texy deleted file mode 100644 index aa8c9b5d82..0000000000 --- a/database/it/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversione dei tipi -******************** - -.[perex] -Nette Database converte automaticamente i valori restituiti dal database nei tipi PHP corrispondenti. - - -Data e ora ----------- - -I dati temporali vengono convertiti in oggetti `Nette\Utils\DateTime`. Se si desidera che i dati temporali vengano convertiti in oggetti immutabili `Nette\Database\DateTime`, impostare l'opzione `newDateTime` su true nella [configurazione|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'); -``` - -Nel caso di MySQL, il tipo di dati `TIME` viene convertito in oggetti `DateInterval`. - - -Valori booleani ---------------- - -I valori booleani vengono automaticamente convertiti in `true` o `false`. Per MySQL, `TINYINT(1)` viene convertito se impostiamo `convertBoolean` nella [configurazione|configuration]. - -```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` in base al tipo di colonna nel database: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalizzazione personalizzata ------------------------------- - -Utilizzando il metodo `setRowNormalizer(?callable $normalizer)` è possibile impostare una funzione personalizzata per trasformare le righe dal database. Questo è utile, ad esempio, per la conversione automatica dei tipi di dati. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // qui avviene la conversione dei tipi - return $row; -}); -``` diff --git a/database/it/reflection.texy b/database/it/reflection.texy index b608b59dde..e3a6036b31 100644 --- a/database/it/reflection.texy +++ b/database/it/reflection.texy @@ -1,10 +1,10 @@ -Riflessione della struttura -*************************** +Reflection della struttura +************************** .{data-version:3.2.1} -Nette Database fornisce strumenti per l'introspezione della struttura del database utilizzando la classe [api:Nette\Database\Reflection]. Ciò consente di ottenere informazioni su tabelle, colonne, indici e chiavi esterne. È possibile utilizzare la riflessione per generare schemi, creare applicazioni flessibili che lavorano con il database o strumenti di database generici. +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. -Otteniamo l'oggetto di riflessione dall'istanza della connessione al database: +L'oggetto reflection si ottiene dall'istanza della connessione al database: ```php $reflection = $database->getReflection(); @@ -14,24 +14,24 @@ $reflection = $database->getReflection(); Ottenere le tabelle ------------------- -La proprietà readonly `$reflection->tables` contiene un array associativo di tutte le tabelle nel database: +La proprietà readonly `$reflection->tables` contiene un array associativo di tutte le tabelle del database: ```php -// Elenca i nomi di tutte le tabelle +// elenco dei nomi di tutte le tabelle foreach ($reflection->tables as $name => $table) { echo $name . "\n"; } ``` -Sono disponibili anche due metodi: +Sono disponibili altri due metodi: ```php -// Verifica l'esistenza della tabella +// verifica dell'esistenza di una tabella if ($reflection->hasTable('users')) { echo "La tabella users esiste"; } -// Restituisce l'oggetto tabella; se non esiste, lancia un'eccezione +// restituisce l'oggetto della tabella; se non esiste lancia un'eccezione $table = $reflection->getTable('users'); ``` @@ -39,31 +39,33 @@ $table = $reflection->getTable('users'); Informazioni sulla tabella -------------------------- -La tabella è rappresentata dall'oggetto [Table|api:Nette\Database\Reflection\Table], che fornisce le seguenti proprietà readonly: +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 incluso 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 o null -- `$foreignKeys: ForeignKey[]` – array delle chiavi esterne della tabella +- `$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 fornisce 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à: +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 di dato 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 per il sistema di database dato +- `$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) { @@ -77,25 +79,25 @@ foreach ($table->columns as $name => $column) { Indici ------ -La proprietà `indexes` della tabella fornisce un array di indici, dove ogni indice è un'istanza di [Index|api:Nette\Database\Reflection\Index] con queste proprietà: +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 di colonne che compongono l'indice -- `$unique: bool` – se l'indice è univoco -- `$primary: bool` – se è una chiave primaria -- `$name: ?string` – nome dell'indice +- `$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 può essere ottenuta utilizzando la proprietà `primaryKey`, che restituisce un oggetto `Index` o `null` nel caso in cui la tabella non abbia una chiave primaria. +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 +// 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 " Unique: " . ($index->unique ? 'Sì' : 'No') . "\n"; + echo " Univoco: " . ($index->unique ? 'Sì' : 'No') . "\n"; } -// Elenco della chiave primaria +// elenco della chiave primaria if ($primaryKey = $table->primaryKey) { $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); echo "Chiave primaria: $columns\n"; @@ -106,15 +108,15 @@ if ($primaryKey = $table->primaryKey) { Chiavi esterne -------------- -La proprietà `foreignKeys` della tabella fornisce un array di chiavi esterne, dove ogni chiave esterna è un'istanza di [ForeignKey|api:Nette\Database\Reflection\ForeignKey] con queste proprietà: +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` – tabella referenziata -- `$localColumns: Column[]` – array di colonne locali -- `$foreignColumns: Column[]` – array di colonne referenziate -- `$name: ?string` – nome della chiave esterna +- `$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 +// 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)); diff --git a/database/it/security.texy b/database/it/security.texy index b22398b8eb..6fb5dc6902 100644 --- a/database/it/security.texy +++ b/database/it/security.texy @@ -1,32 +1,32 @@ -Rischi per la sicurezza -*********************** +Rischi di sicurezza +******************* <div class=perex> -Il database contiene spesso dati sensibili e consente di eseguire operazioni pericolose. Per lavorare in sicurezza con Nette Database è fondamentale: +I database contengono spesso dati sensibili e permettono di eseguire operazioni pericolose. Per lavorare in sicurezza con Nette Database è fondamentale: -- Comprendere la differenza tra API sicure e non sicure -- Utilizzare query parametrizzate -- Validare correttamente i dati di input +- Capire la differenza tra API sicure e insicure +- Usare query parametrizzate +- Validare correttamente i dati in ingresso </div> -Cos'è SQL Injection? -==================== +Che cos'è la SQL injection? +=========================== -SQL injection è il rischio di sicurezza più grave quando si lavora con un database. Si verifica quando l'input non trattato di un utente diventa parte di una query SQL. Un attaccante può inserire i propri comandi SQL e quindi: -- Ottenere accesso non autorizzato ai dati +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 -- Bypassare l'autenticazione +- Aggirare l'autenticazione ```php // ❌ CODICE PERICOLOSO - vulnerabile a SQL injection $database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); -// L'attaccante può inserire ad esempio il valore: ' OR '1'='1 -// La query risultante sarà: SELECT * FROM users WHERE name = '' OR '1'='1' -// Che restituirà tutti gli utenti +// 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: @@ -41,21 +41,21 @@ $table->where("name = '$_GET[name]'"); Query parametrizzate ==================== -La difesa fondamentale contro SQL injection sono le query parametrizzate. Nette Database offre diversi modi per utilizzarle. +La difesa fondamentale contro la SQL injection sono le query parametrizzate. Nette Database offre diversi modi per usarle. -Il modo più semplice è utilizzare **placeholder a punto interrogativo**: +Il più semplice è usare i **segnaposto con punto interrogativo**: ```php -// ✅ Query parametrizzata sicura +// ✅ query parametrizzata sicura $database->query('SELECT * FROM users WHERE name = ?', $name); -// ✅ Condizione sicura in Explorer +// ✅ condizione sicura in Explorer $table->where('name = ?', $name); ``` -Questo vale per tutti gli altri metodi in [Database Explorer|explorer], che consentono di inserire espressioni con placeholder a punto interrogativo e parametri. +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 la clausola WHERE, possiamo passare i valori in un array: +Per i comandi INSERT, UPDATE o per la clausola WHERE possiamo passare i valori in un array: ```php // ✅ INSERT sicuro @@ -75,89 +75,89 @@ $table->insert([ Validazione dei valori dei parametri ==================================== -Le query parametrizzate sono la pietra angolare del lavoro sicuro con il database. Tuttavia, i valori che inseriamo in esse devono passare attraverso diversi livelli di controllo: +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 del tipo +Controllo dei tipi ------------------ -**La cosa più importante è garantire il tipo di dato corretto dei parametri** - questa è una condizione necessaria per l'uso sicuro di Nette Database. Il database presuppone che tutti i dati di input abbiano il tipo di dato corretto corrispondente alla colonna data. +**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. -Ad esempio, se `$name` negli esempi precedenti fosse inaspettatamente un array invece di una stringa, Nette Database tenterebbe di inserire tutti i suoi elementi nella query SQL, il che porterebbe a un errore. Pertanto, **non utilizzare mai** dati non validati da `$_GET`, `$_POST` o `$_COOKIE` direttamente nelle query del database. +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. -Controllo del formato ---------------------- +Validazione del formato +----------------------- -Al secondo livello, controlliamo il formato dei dati - ad esempio, se le stringhe sono in codifica UTF-8 e la loro lunghezza corrisponde alla definizione della colonna, o se i valori numerici rientrano nell'intervallo consentito per il tipo di dato della colonna. +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 parzialmente fare affidamento anche sul database stesso: molti database rifiuteranno dati non validi. Tuttavia, il comportamento può variare, alcuni potrebbero silenziosamente troncare stringhe lunghe o tagliare numeri fuori intervallo. +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. -Controllo del dominio ---------------------- +Validazione specifica del dominio +--------------------------------- -Il terzo livello rappresenta i controlli logici specifici della tua applicazione. Ad esempio, verificare che i valori delle caselle di selezione corrispondano alle opzioni offerte, che i numeri siano nell'intervallo previsto (ad es. età 0-150 anni) o che le dipendenze reciproche tra i valori abbiano senso. +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. -Metodi di validazione consigliati ---------------------------------- +Modi consigliati di validare +---------------------------- -- Utilizzare [Nette Forms|forms:], che garantiscono automaticamente la corretta validazione di tutti gli input -- Utilizzare i [Presenter|application:] e specificare i tipi di dati per i parametri nei metodi `action*()` e `render*()` -- Oppure implementare un proprio layer di validazione utilizzando strumenti PHP standard come `filter_var()` +- 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. Tuttavia, quando si utilizzano array nelle query SQL, dobbiamo prestare la stessa attenzione anche alle loro chiavi. +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 nell'array non sono trattate +// ❌ CODICE PERICOLOSO - le chiavi dell'array non sono trattate $database->query('INSERT INTO users', $_POST); ``` -Nei comandi INSERT e UPDATE, questo è un errore di sicurezza critico: un attaccante può inserire o modificare qualsiasi colonna nel database. Potrebbe, ad esempio, impostare `is_admin = 1` o inserire dati arbitrari in colonne sensibili (la cosiddetta Mass Assignment Vulnerability). +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 operatori: +Nelle condizioni WHERE è ancora più pericoloso, perché possono contenere degli operatori: ```php -// ❌ CODICE PERICOLOSO - le chiavi nell'array non sono trattate +// ❌ 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) ``` -Un attaccante può utilizzare questo approccio per scoprire sistematicamente gli stipendi dei dipendenti. Inizia, ad esempio, con una query sugli stipendi superiori a 100.000, poi inferiori a 50.000 e restringendo gradualmente l'intervallo, può rivelare gli stipendi approssimativi di tutti i dipendenti. Questo tipo di attacco è chiamato SQL enumeration. +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 ancora [molto più flessibili |explorer#where] e supportano espressioni SQL nelle chiavi e nei valori, inclusi operatori e funzioni. Ciò dà all'attaccante la possibilità di eseguire SQL injection: +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 il proprio SQL +// ❌ 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 termina la condizione originale con `0)`, aggiunge il proprio `SELECT` utilizzando `UNION` per ottenere dati sensibili dalla tabella `users` e chiude la query sintatticamente corretta con `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, abbiamo bisogno di 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 questo approccio è inaffidabile: un attaccante può sempre trovare un nuovo modo per scrivere un nome di colonna pericoloso che non avevamo previsto. +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. -Pertanto, è molto più sicuro invertire la logica e definire un elenco esplicito di colonne consentite (whitelist): +È perciò molto più sicuro ribaltare la logica e definire un elenco esplicito di colonne consentite (whitelist): ```php -// Colonne che l'utente può modificare +// colonne che l'utente può modificare $allowedColumns = ['name', 'email', 'active']; -// Rimuoviamo tutte le colonne non consentite dall'input +// rimuoviamo dall'input tutte le colonne non autorizzate $filteredData = array_intersect_key($userData, array_flip($allowedColumns)); -// ✅ Ora possiamo usarlo in sicurezza nelle query, come ad esempio: +// ✅ ora si può usare in sicurezza nelle query, per esempio: $database->query('INSERT INTO users', $filteredData); $table->update($filteredData); $table->where($filteredData); @@ -167,19 +167,19 @@ $table->where($filteredData); Identificatori dinamici ======================= -Per nomi dinamici di tabelle e colonne, utilizzare il placeholder `?name`. Questo garantisce il corretto escaping degli identificatori secondo la sintassi del database dato (ad esempio, utilizzando i backtick in MySQL): +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 +// ✅ uso sicuro di identificatori affidabili $table = 'users'; $column = 'name'; $database->query('SELECT ?name FROM ?name', $column, $table); -// Risultato in MySQL: SELECT `name` FROM `users` +// risultato in MySQL: SELECT `name` FROM `users` ``` -Importante: utilizzare il simbolo `?name` solo per valori affidabili definiti nel codice dell'applicazione. Per i valori provenienti dall'utente, utilizzare nuovamente la [whitelist |#Whitelist delle colonne]. Altrimenti, ci si espone a rischi per la sicurezza: +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 utilizzare mai l'input dell'utente +// ❌ 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 index 0b9bf6030a..11c2c6bbcc 100644 --- a/database/it/sql-way.texy +++ b/database/it/sql-way.texy @@ -1,17 +1,17 @@ -Accesso SQL -*********** +Approccio SQL +************* .[perex] -Nette Database offre due approcci: puoi scrivere query SQL da solo (accesso SQL), oppure puoi farle generare automaticamente (vedi [Explorer |explorer]). L'accesso SQL ti dà il pieno controllo sulle query, garantendo al contempo la loro costruzione sicura. +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 si trovano nel capitolo [Connessione e configurazione |guide#Connessione e configurazione]. +I dettagli sulla connessione e la configurazione del database li trovate nel capitolo [Connessione e configurazione |guide#Connessione e configurazione]. -Query di base -============= +Interrogazione di base +====================== -Per interrogare il database, si usa il metodo `query()`. Questo restituisce un oggetto [ResultSet |api:Nette\Database\ResultSet], che rappresenta il risultato della query. In caso di fallimento, il metodo [lancia un'eccezione|exceptions]. Possiamo scorrere il risultato della query usando un ciclo `foreach`, oppure usare una delle [funzioni ausiliarie |#Recupero dati]. +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'); @@ -22,52 +22,52 @@ foreach ($result as $row) { } ``` -Per inserire in modo sicuro i valori nelle query SQL, usiamo query parametrizzate. Nette Database le rende estremamente semplici: basta aggiungere una virgola e il valore dopo la query SQL: +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, hai due opzioni di scrittura. Puoi "intervallare" la query SQL con i parametri: +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 l'intera query SQL e poi aggiungere tutti i parametri: +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 SQL injection -=============================== +Protezione contro la SQL injection +================================== -Perché è importante usare query parametrizzate? Perché ti proteggono da un attacco chiamato SQL injection, in cui un attaccante potrebbe inserire i propri comandi SQL e quindi ottenere o danneggiare i dati nel database. +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 inserire mai variabili direttamente nella query SQL!** Usa sempre query parametrizzate, che ti proteggono da SQL injection. +**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 +// ✅ query parametrizzata sicura $database->query('SELECT * FROM users WHERE name = ?', $name); ``` -Familiarizza con i [possibili rischi per la sicurezza |security]. +Prendete confidenza con i [possibili rischi di sicurezza |security]. -Tecniche di query -================= +Tecniche di interrogazione +========================== Condizioni WHERE ---------------- -Puoi scrivere le condizioni WHERE come un array associativo, dove le chiavi sono i nomi delle colonne e i valori sono i dati per il confronto. Nette Database seleziona automaticamente l'operatore SQL più appropriato in base al tipo di valore. +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', [ @@ -77,7 +77,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -Nella chiave puoi anche specificare esplicitamente l'operatore per il confronto: +Nella chiave potete anche indicare esplicitamente l'operatore di confronto: ```php $database->query('SELECT * FROM users WHERE', [ @@ -88,7 +88,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' ``` -Nette gestisce automaticamente casi speciali come valori `null` o array. +Nette gestisce automaticamente i casi particolari come i valori `null` o gli array. ```php $database->query('SELECT * FROM products WHERE', [ @@ -99,39 +99,39 @@ $database->query('SELECT * FROM products WHERE', [ // WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL ``` -Per le condizioni negative, usa l'operatore `NOT`: +Per le condizioni negative usate l'operatore `NOT`: ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // usa l'operatore <> + 'name NOT' => 'Laptop', // usa l'operatore != 'category_id NOT' => [1, 2, 3], // usa NOT IN 'description NOT' => null, // usa IS NOT NULL - 'id' => [], // viene omesso + 'id NOT' => [], // viene saltato ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -Per unire le condizioni si usa l'operatore `AND`. Questo può essere cambiato usando il [segnaposto ?or |#Hint per la costruzione di SQL]. +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 --------------- -L'ordinamento `ORDER BY` può essere scritto usando un array. Nelle chiavi indichiamo le colonne e il valore sarà un booleano che determina se ordinare in modo ascendente: +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, // ascendente - 'name' => false, // discendente + 'id' => true, // crescente + 'name' => false, // decrescente ]); // SELECT id FROM author ORDER BY `id`, `name` DESC ``` -Inserimento dati (INSERT) -------------------------- +Inserimento dei dati (INSERT) +----------------------------- -Per inserire record si usa l'istruzione SQL `INSERT`. +Per inserire i record serve il comando SQL `INSERT`. ```php $values = [ @@ -142,11 +142,11 @@ $database->query('INSERT INTO users ?', $values); $userId = $database->getInsertId(); ``` -Il metodo `getInsertId()` restituisce l'ID dell'ultima riga inserita. Per alcuni database (ad es. PostgreSQL), è necessario specificare come parametro il nome della sequenza da cui generare l'ID tramite `$database->getInsertId($sequenceId)`. +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 possiamo passare anche [#valori speciali] come file, oggetti DateTime o tipi enum. +Come parametri si possono passare anche [#Valori speciali], per esempio file, oggetti DateTime o tipi enum. -Inserimento di più record contemporaneamente: +Inserimento di più record in una volta: ```php $database->query('INSERT INTO users ?', [ @@ -155,27 +155,27 @@ $database->query('INSERT INTO users ?', [ ]); ``` -L'INSERT multiplo è molto più veloce perché viene eseguita una singola query al database, invece di molte query individuali. +L'INSERT multiplo è molto più veloce, perché viene eseguita una sola query al database invece di tante singole. -**Avviso di sicurezza:** Non usare mai dati non validati come `$values`. Familiarizza con i [possibili rischi |security#Lavoro sicuro con le colonne]. +**Nota di sicurezza:** non usate mai dati non validati come `$values`. Prendete confidenza con i [possibili rischi |security#Lavoro sicuro con le colonne]. -Aggiornamento dati (UPDATE) ---------------------------- +Aggiornamento dei dati (UPDATE) +------------------------------- -Per aggiornare i record si usa l'istruzione SQL `UPDATE`. +Per aggiornare i record serve il comando SQL `UPDATE`. ```php -// Aggiornamento di un singolo record +// aggiornamento di un singolo record $values = [ 'name' => 'John Smith', ]; $result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); ``` -Il numero di righe interessate viene restituito da `$result->getRowCount()`. +Il numero di righe interessate lo restituisce `$result->getRowCount()`. -Per UPDATE possiamo usare gli operatori `+=` e `-=`: +Per l'`UPDATE` possiamo usare gli operatori `+=` e `-=`: ```php $database->query('UPDATE users SET ? WHERE id = ?', [ @@ -183,7 +183,7 @@ $database->query('UPDATE users SET ? WHERE id = ?', [ ], 1); ``` -Esempio di inserimento o modifica di un record, se esiste già. Usiamo la tecnica `ON DUPLICATE KEY UPDATE`: +Esempio di inserimento o aggiornamento di un record se esiste già. Usiamo la tecnica `ON DUPLICATE KEY UPDATE`: ```php $values = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Nota che Nette Database riconosce in quale contesto dell'istruzione SQL viene inserito il parametro con l'array e costruisce il codice SQL di conseguenza. Quindi dal primo array ha costruito `(id, name, year) VALUES (123, 'Jim', 1978)`, mentre il secondo lo ha convertito nella forma `name = 'Jim', year = 1978`. Ne parliamo più dettagliatamente nella sezione [#Hint per la costruzione di SQL]. +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 dati (DELETE) ---------------------------- +Cancellazione dei dati (DELETE) +------------------------------- -Per cancellare i record si usa l'istruzione SQL `DELETE`. Esempio con ottenimento del numero di righe cancellate: +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) @@ -212,21 +212,21 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -Hint per la costruzione di SQL ------------------------------- +Suggerimenti per la costruzione dell'SQL +---------------------------------------- -Un hint è un segnaposto speciale nella query SQL che indica come il valore del parametro deve essere riscritto nell'espressione SQL: +Un suggerimento è un segnaposto particolare nella query SQL che determina come il valore del parametro debba essere convertito in un'espressione SQL: -| Hint | Descrizione | Utilizzato automaticamente +| Suggerimento | Descrizione | Usato automaticamente per |-----------|-------------------------------------------------|----------------------------- -| `?name` | usa per inserire il nome della tabella o della colonna | - -| `?values` | genera `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | genera l'assegnazione `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | unisce le condizioni nell'array con l'operatore `AND` | `WHERE ?`, `HAVING ?` -| `?or` | unisce le condizioni nell'array con l'operatore `OR` | - -| `?order` | genera la clausola `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` +| `?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 ?` -Per l'inserimento dinamico di nomi di tabelle e colonne nella query, si usa il segnaposto `?name`. Nette Database si occupa della corretta gestione degli identificatori secondo le convenzioni del database specifico (ad es. racchiudendoli tra backtick in MySQL). +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'; @@ -235,9 +235,9 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); // SELECT `name` FROM `users` WHERE id = 1 (in MySQL) ``` -**Avviso:** usa il simbolo `?name` solo per nomi di tabelle e colonne provenienti da input validati, altrimenti ti esponi a un [rischio per la sicurezza |security#Identificatori dinamici]. +**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 hint di solito non devono essere specificati, poiché Nette utilizza un'intelligente rilevazione automatica durante la composizione della query SQL (vedi la terza colonna della tabella). Ma puoi usarlo, ad esempio, in una situazione in cui vuoi unire le condizioni usando `OR` invece di `AND`: +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', [ @@ -251,29 +251,29 @@ $database->query('SELECT * FROM users WHERE ?or', [ Valori speciali --------------- -Oltre ai comuni tipi scalari (string, int, bool), puoi passare valori speciali come parametri: +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 `DateTime` vengono convertiti nel formato del database -- tipi enum: le istanze `enum` vengono convertite nel loro valore +- 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' => 'My Article', - 'published_at' => new DateTime, + 'title' => 'Il mio articolo', + 'published_at' => new DateTimeImmutable, // oppure new DateTime 'content' => fopen('image.png', 'r'), 'state' => Status::Draft, ]); ``` -Per i database che non hanno supporto nativo per il tipo di dati `datetime` (come SQLite e Oracle), `DateTime` viene convertito nel valore specificato nella [configurazione del database|configuration] tramite la voce `formatDateTime` (il valore predefinito è `U` - timestamp unix). +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, è necessario specificare direttamente il codice SQL come valore, che però non deve essere interpretato come stringa ed escapato. A questo servono gli oggetti della classe `Nette\Database\SqlLiteral`. Li crea il metodo `Connection::literal()`. +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', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -O alternativamente: +Oppure: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Grazie a ciò possiamo creare combinazioni interessanti: +Il che permette combinazioni interessanti: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -317,20 +317,20 @@ $result = $database->query('SELECT * FROM users WHERE', [ ``` -Recupero dati -============= +Ottenere i dati +=============== -Scorciatoie per query SELECT ----------------------------- +Scorciatoie per le query SELECT +------------------------------- -Per semplificare il recupero dei dati, `Connection` offre diverse scorciatoie che combinano la chiamata `query()` con il successivo `fetch*()`. Questi metodi accettano gli stessi parametri di `query()`, ovvero la query SQL e parametri opzionali. Una descrizione completa dei metodi `fetch*()` si trova [sotto |#fetch]. +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` -| `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, dove la prima colonna rappresenta la chiave e la seconda il valore -| `fetchField($sql, ...$params): mixed` | Esegue la query e restituisce il valore del primo campo della prima riga -| `fetchList($sql, ...$params): ?array` | Esegue la query e restituisce la prima riga come array indicizzato +| `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: @@ -344,7 +344,7 @@ $count = $database->query('SELECT COUNT(*) FROM articles') `foreach` - iterazione sulle righe ---------------------------------- -Dopo l'esecuzione della query, viene restituito un oggetto [ResultSet|api:Nette\Database\ResultSet], che consente di scorrere i risultati in diversi modi. Il modo più semplice per eseguire una query e ottenere le righe è iterando in un ciclo `foreach`. Questo metodo è il più efficiente in termini di memoria, poiché restituisce i dati gradualmente e non li memorizza tutti in memoria contemporaneamente. +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'); @@ -357,13 +357,13 @@ foreach ($result as $row) { ``` .[note] -`ResultSet` può essere iterato solo una volta. Se è necessario iterare ripetutamente, è necessario prima caricare i dati in un array, ad esempio utilizzando il metodo `fetchAll()`. +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 ci sono più righe, restituisce `null`. Sposta il puntatore interno alla riga successiva. +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'); @@ -377,7 +377,7 @@ if ($row) { fetchAll(): array .[method] --------------------------- -Restituisce tutte le righe rimanenti dal `ResultSet` come un array di oggetti `Row`. +Restituisce tutte le righe rimanenti del `ResultSet` come array di oggetti `Row`. ```php $result = $database->query('SELECT * FROM users'); @@ -391,7 +391,7 @@ foreach ($rows as $row) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Restituisce i risultati come un array associativo. Il primo argomento specifica il nome della colonna da utilizzare come chiave nell'array, il secondo argomento specifica il nome della colonna da utilizzare come valore: +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'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Se specifichiamo solo il primo parametro, il valore sarà l'intera riga, ovvero un oggetto `Row`: +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 utilizzato il valore dell'ultima riga. Utilizzando `null` come chiave, l'array sarà indicizzato numericamente a partire da zero (quindi non si verificano collisioni): +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'); @@ -417,14 +417,14 @@ $names = $result->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -In alternativa, puoi specificare come parametro un callback, che per ogni riga restituirà o il valore stesso, o una coppia chiave-valore. +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 una coppia chiave & valore: +// 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, ...] ``` @@ -433,7 +433,7 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -Restituisce il valore del primo campo della riga corrente. Se non ci sono più righe, restituisce `null`. Sposta il puntatore interno alla riga successiva. +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'); @@ -444,7 +444,7 @@ $name = $result->fetchField(); // carica il nome dalla prima riga fetchList(): ?array .[method] ----------------------------- -Restituisce una riga come array indicizzato. Se non ci sono più righe, restituisce `null`. Sposta il puntatore interno alla riga successiva. +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'); @@ -455,19 +455,19 @@ $row = $result->fetchList(); // ['John', 'john@example.com'] getRowCount(): ?int .[method] ----------------------------- -Restituisce il numero di righe interessate dall'ultima query `UPDATE` o `DELETE`. Per `SELECT`, è il numero di righe restituite, ma questo potrebbe non essere noto - in tal caso il metodo restituirà `null`. +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 nel `ResultSet`. +Restituisce il numero di colonne del `ResultSet`. Informazioni sulle query ======================== -A scopo di debugging, possiamo ottenere informazioni sull'ultima query eseguita: +Per il debugging possiamo ottenere informazioni sull'ultima query eseguita: ```php echo $database->getLastQueryString(); // stampa la query SQL @@ -477,21 +477,21 @@ echo $result->getQueryString(); // stampa la query SQL echo $result->getTime(); // stampa il tempo di esecuzione in secondi ``` -Per visualizzare il risultato come tabella HTML, si può usare: +Per mostrare il risultato come tabella HTML potete usare: ```php $result = $database->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSet offre informazioni sui tipi di colonna: +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->type"; // ad es. 'id è di tipo int' + echo "$column è di tipo $type"; // per esempio 'id è di tipo int' } ``` @@ -499,15 +499,15 @@ foreach ($types as $column => $type) { Logging delle query ------------------- -Possiamo implementare il nostro logging delle query personalizzato. L'evento `onQuery` è un array di callback che vengono chiamati dopo ogni query eseguita: +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('Time: ' . $result->getTime()); + $logger->info('Tempo: ' . $result->getTime()); if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); + $logger->warning('Risultato di grandi dimensioni: ' . $result->getRowCount() . ' righe'); } }; ``` diff --git a/database/it/transactions.texy b/database/it/transactions.texy index b737f2bbb2..b0755210c8 100644 --- a/database/it/transactions.texy +++ b/database/it/transactions.texy @@ -2,9 +2,9 @@ Transazioni *********** .[perex] -Le transazioni garantiscono che tutte le operazioni all'interno di una transazione vengano eseguite, oppure nessuna di esse. Sono utili per garantire la coerenza dei dati durante operazioni complesse. +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 per utilizzare le transazioni è il seguente: +Il modo più semplice di usare le transazioni è questo: ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -Potete scrivere la stessa cosa in modo molto più elegante usando il metodo `transaction()`. Accetta un callback come parametro, che esegue all'interno della transazione. Se il callback viene eseguito senza eccezioni, la transazione viene confermata automaticamente. Se si verifica un'eccezione, la transazione viene annullata (rollback) e l'eccezione si propaga ulteriormente. +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) { @@ -33,11 +33,13 @@ $database->transaction(function ($database) use ($id) { }); ``` +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 aggiornate + return $result->getRowCount(); // restituisce il numero di righe modificate }); ``` diff --git a/database/it/type-conversion.texy b/database/it/type-conversion.texy new file mode 100644 index 0000000000..970c33d09e --- /dev/null +++ b/database/it/type-conversion.texy @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000000..6ee4337c59 --- /dev/null +++ b/database/it/upgrading.texy @@ -0,0 +1,36 @@ +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 index 965d3ecec8..0b8efd210e 100644 --- a/database/ja/@home.texy +++ b/database/ja/@home.texy @@ -1,21 +1,18 @@ +対応しているデータベース +============ +次のデータベースサーバーに対応しています。 -サポートされているデータベース -=============== +|* データベースサーバー |* 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 | あり | - -Netteは以下のデータベースをサポートしています: -|* データベースサーバー |* 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 | はい | - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Databaseは、SQLクエリを記述する必要なく、データベースからデータを取得するプロセスを大幅に簡素化します。効率的なクエリを実行し、不要なデータを転送しません。}} +{{maintitle: Nette Database - PHP のための素晴らしいデータベース層}} +{{description: Nette Database は SQL のクエリを書かずにデータベースからデータを取り出す作業を大幅に簡単にします。効率のよいクエリを実行し、要らないデータは転送しません。}} diff --git a/database/ja/@left-menu.texy b/database/ja/@left-menu.texy index 4b7d6758d8..a0b66b94d9 100644 --- a/database/ja/@left-menu.texy +++ b/database/ja/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** - [はじめに |guide] -- [SQL アクセス |sql way] -- [Explorer |Explorer] -- [トランザクション |transactions] -- [例外 |exceptions] -- [リフレクション |reflection] -- [マッピング |mapping] -- [設定 |configuration] +- [SQL アプローチ|sql-way] +- [Explorer|explorer] +- [トランザクション|transactions] +- [例外|exceptions] +- [リフレクション|reflection] +- [型変換 |type-conversion] +- [設定|configuration] - [セキュリティリスク |security] -- [アップグレード |en:upgrading] +- [アップグレード|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 index d3c41dc3d7..43b85f3cac 100644 --- a/database/ja/@meta.texy +++ b/database/ja/@meta.texy @@ -1 +1 @@ -{{sitename: Nette ドキュメンテーション}} +{{sitename: Nette ドキュメント}} diff --git a/database/ja/configuration.texy b/database/ja/configuration.texy index d97526880e..c20a9b2c69 100644 --- a/database/ja/configuration.texy +++ b/database/ja/configuration.texy @@ -1,67 +1,67 @@ -データベース設定 -******** +データベースの設定 +********* .[perex] -Nette Databaseの設定オプションの概要。 +Nette Database の設定オプションの一覧です。 -フレームワーク全体ではなく、このライブラリのみを使用している場合は、[設定を読み込む方法|bootstrap:] を読んでください。 +フレームワーク全体ではなくこのライブラリだけを使っているなら、[設定の読み込み方|bootstrap:]をご覧ください。 -単一接続 ----- +ひとつの接続 +------ -単一のデータベース接続の設定: +ひとつのデータベース接続を設定します。 ```neon database: - # DSN、唯一の必須キー + # DSN。唯一の必須のキーです dsn: "sqlite:%appDir%/Model/demo.db" user: ... password: ... ``` -`Nette\Database\Connection` と `Nette\Database\Explorer` サービスを作成します。これらは通常、[autowiring |dependency-injection:autowiring] によって渡されるか、[その名前 |#DI サービス] への参照によって渡されます。 +これで `Nette\Database\Connection` と `Nette\Database\Explorer` のサービスが作られ、ふつうは[オートワイヤリング |dependency-injection:autowiring]で、あるいは[その名前 |#DI のサービス]を指定して渡されます。 -その他の設定: +そのほかの設定です。 ```neon database: - # Tracy Bar にデータベースパネルを表示しますか? - debugger: ... # (bool) デフォルトはtrue + # Tracy Bar にデータベースのパネルを表示しますか + debugger: ... # (bool) Tracy が動いていれば既定でオン - # Tracy Bar にクエリの EXPLAIN を表示しますか? - explain: ... # (bool) デフォルトはtrue + # Tracy Bar にクエリの EXPLAIN を表示しますか + explain: ... # (bool) 既定は true - # この接続に対して autowiring を許可しますか? - autowired: ... # (bool) 最初の接続ではデフォルトでtrue + # この接続でオートワイヤリングを有効にしますか + autowired: ... # (bool) 最初の接続では既定で true - # テーブルの命名規則: discovered, static またはクラス名 - conventions: discovered # (string) デフォルトは 'discovered' + # テーブルの規約: discovered、static、またはクラス名 + conventions: discovered # (string) 既定は 'discovered' options: - # データベースへの接続は必要になったときのみ行いますか? - lazy: ... # (bool) デフォルトはfalse + # 必要になったときだけデータベースに接続しますか + lazy: ... # (bool) 既定は false - # PHP データベースドライバクラス + # PHP のデータベースドライバのクラス driverClass: # (string) - # MySQL のみ: sql_mode を設定 + # MySQL のみ: sql_mode を設定します sqlmode: # (string) - # MySQL のみ: SET NAMES を設定 - charset: # (string) デフォルトは 'utf8mb4' + # MySQL のみ: SET NAMES を設定します + charset: # (string) 既定は 'utf8mb4' - # MySQL のみ: TINYINT(1) を bool に変換 - convertBoolean: # (bool) デフォルトはfalse + # MySQL のみ: TINYINT(1) を bool に変換します + convertBoolean: # (bool) 既定は false - # 日付カラムを immutable オブジェクトとして返します (バージョン 3.2.1 以降) - newDateTime: # (bool) デフォルトはfalse + # 日付の列を変更できないオブジェクトとして返します(バージョン 3.2.1 以降) + newDateTime: # (bool) 既定は false - # Oracle と SQLite のみ: 日付の保存形式 - formatDateTime: # (string) デフォルトは 'U' + # Oracle と SQLite のみ: 日付を保存する書式 + formatDateTime: # (string) 既定は 'U' ``` -`options` キーには、[PDO ドライバのドキュメント |https://www.php.net/manual/en/pdo.drivers.php] に記載されているその他のオプションを指定できます。例: +`options` キーには [PDO ドライバのドキュメント |https://www.php.net/manual/en/pdo.drivers.php]にあるほかのオプションも書けます。たとえば次のようにです。 ```neon database: @@ -70,10 +70,10 @@ database: ``` -複数接続 ----- +複数の接続 +----- -設定では、名前付きセクションに分割することで、複数のデータベース接続を定義することもできます: +設定では、名前の付いた区画に分けて複数のデータベース接続を定義できます。 ```neon database: @@ -86,23 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -Autowiringは最初のセクションのサービスに対してのみ有効です。これは `autowired: false` または `autowired: true` を使用して変更できます。 +オートワイヤリングが有効になるのは最初の区画のサービスだけです。これは `autowired: false` や `autowired: true` で変えられます。 -DI サービス -------- +DI のサービス +-------- -これらのサービスはDIコンテナに追加されます。ここで `###` は接続名を表します: +DI コンテナには次のサービスが足されます。`###` は接続の名前を表します。 -| 名前 | 型 | 説明 -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | データベース接続 -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] +| 名前 | 型 | 説明 +|---------------------------|---------------------------------|--------------------------- +| `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` です。 +接続をひとつだけ定義した場合、サービスの名前は `database.default.connection` と `database.default.explorer` になります。上の例のように複数の接続を定義した場合、名前は区画に対応して `database.main.connection`、`database.main.explorer`、さらに `database.another.connection`、`database.another.explorer` になります。 -Autowiringされていないサービスは、その名前への参照によって明示的に渡します: +オートワイヤリングされないサービスは、その名前を指定してはっきり渡します。 ```neon services: diff --git a/database/ja/exceptions.texy b/database/ja/exceptions.texy index e2764b7d45..b8f06f6ea9 100644 --- a/database/ja/exceptions.texy +++ b/database/ja/exceptions.texy @@ -1,22 +1,25 @@ 例外 -******* +*** -Nette Databaseは例外の階層を使用します。基本クラスは `Nette\Database\DriverException` で、これは `PDOException` を継承し、データベースエラーを処理するための拡張機能を提供します: +Nette Database は例外の階層を使います。基底クラスは `Nette\Database\DriverException` で、`PDOException` を継承し、データベースのエラーを扱うための機能を強化します。 -- `getDriverCode()` メソッドは、データベースドライバからのエラーコードを返します -- `getSqlState()` メソッドは、SQLSTATEコードを返します -- `getQueryString()` および `getParameters()` メソッドは、元のクエリとそのパラメータを取得できます +- `getDriverCode()` メソッドはデータベースドライバのエラーコードを返します。 +- `getSqlState()` メソッドは SQLSTATE のコードを返します。 +- `getQueryString()` と `getParameters()` メソッドで、もとのクエリとそのパラメータを取り出せます。 -`DriverException` から、次の特殊な例外が継承されます: +`DriverException` クラスは、次の特化した例外に継承されます。 -- `ConnectionException` - データベースサーバーへの接続失敗を示します -- `ConstraintViolationException` - データベース制約違反の基本クラスで、以下が継承されます: - - `ForeignKeyConstraintViolationException` - 外部キー制約違反 - - `NotNullConstraintViolationException` - NOT NULL制約違反 - - `UniqueConstraintViolationException` - 値の一意性制約違反 +- `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` 例外をキャッチする例。これは、データベースに既に存在するメールアドレスを持つユーザーを挿入しようとしたときに発生します(メールカラムに一意インデックスがあると仮定)。 +次の例は `UniqueConstraintViolationException` の捕まえ方を示します。これはデータベースにすでにあるメールアドレスでユーザーを挿入しようとしたときに起きます(`email` 列に一意インデックスがあるものとします)。 ```php try { @@ -26,9 +29,9 @@ try { 'password' => $hashedPassword, ]); } catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'このメールアドレスのユーザーは既に存在します。'; + echo 'このメールアドレスのユーザーはすでに存在します。'; } catch (Nette\Database\DriverException $e) { - echo '登録中にエラーが発生しました: ' . $e->getMessage(); + echo '登録の途中でエラーが起きました: ' . $e->getMessage(); } ``` diff --git a/database/ja/explorer.texy b/database/ja/explorer.texy index dc272d1b52..369e2c668c 100644 --- a/database/ja/explorer.texy +++ b/database/ja/explorer.texy @@ -3,94 +3,94 @@ Database Explorer <div class=perex> -Explorerは、データベースを扱うための直感的で効率的な方法を提供します。テーブル間のリレーションやクエリの最適化を自動的に処理するため、アプリケーションに集中できます。設定なしですぐに機能します。SQLクエリを完全に制御する必要がある場合は、[SQLアクセス |SQL way] を利用できます。 +Explorer はデータベースを直感的に、しかも効率よく扱う方法を提供します。テーブルどうしの関係を自動的に扱い、クエリを最適化するので、あなたはアプリケーションの論理に集中できます。設定なしですぐに動きます。SQL のクエリを完全に思いどおりにしたいなら、[SQL アプローチ |SQL way]を使えます。 -- データの操作は自然で理解しやすい -- 必要なデータのみを読み込む最適化されたSQLクエリを生成 -- JOINクエリを記述することなく、関連データに簡単にアクセス可能 -- 設定やエンティティの生成なしで即座に機能 +- データの扱いが自然で分かりやすい +- 必要なデータだけを取ってくる最適化された SQL のクエリを生成する +- JOIN のクエリを書かずに関連するデータへ簡単にアクセスできる +- 設定もエンティティの生成もなしにすぐ動く </div> -Explorerの使用は、[api:Nette\Database\Explorer] オブジェクトの `table()` メソッドを呼び出すことから始めます(接続の詳細については、[接続と設定 |guide#接続と設定] の章を参照してください): +Explorer を使う仕事は、[api:Nette\Database\Explorer]オブジェクトの `table()` メソッドを呼ぶところから始まります(データベース接続の設定については [接続と設定 |guide#接続と設定]をご覧ください)。 ```php -$books = $explorer->table('book'); // 'book' はテーブル名 +$books = $explorer->table('book'); // 'book' はテーブルの名前です ``` -このメソッドは、SQLクエリを表す [Selection |api:Nette\Database\Table\Selection] オブジェクトを返します。このオブジェクトにさらにメソッドを連鎖させて、結果をフィルタリングおよびソートできます。クエリは、データを要求し始めたときにのみ構築および実行されます。たとえば、`foreach` ループで反復処理する場合です。各行は [ActiveRow |api:Nette\Database\Table\ActiveRow] オブジェクトによって表されます: +このメソッドは 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' カラムの出力 + echo $book->title; // 'title' 列を出力します + echo $book->author_id; // 'author_id' 列を出力します } ``` -Explorerは、[#テーブル間のリレーション] の操作を大幅に簡素化します。次の例は、関連するテーブル(書籍とその著者)からデータを簡単に表示する方法を示しています。JOINクエリを記述する必要がないことに注意してください。Netteがそれらを自動的に作成します: +Explorer は[テーブルどうしの関係 |#テーブルどうしの関係]を扱う仕事を大いに簡単にします。次の例は、関連するテーブル(本とその著者)のデータをどれだけ簡単に出力できるかを示します。JOIN のクエリを書く必要がないことに注目してください。Nette がそれを生成してくれます。 ```php $books = $explorer->table('book'); foreach ($books as $book) { - echo '書籍: ' . $book->title; - echo '著者: ' . $book->author->name; // 'author' テーブルへのJOINを作成 + echo 'Book: ' . $book->title; + echo 'Author: ' . $book->author->name; // 'author' テーブルへの JOIN を作ります } ``` -Nette Database Explorerは、クエリを可能な限り効率的に最適化します。上記の例では、処理する書籍が10冊であろうと10,000冊であろうと、2つのSELECTクエリのみを実行します。 +Nette Database Explorer は効率が最大になるようにクエリを最適化します。上の例は、本を 10 冊扱おうと 10,000 冊扱おうと、SELECT のクエリを 2 つしか実行しません。 -さらに、Explorerはコード内で使用されているカラムを追跡し、データベースからそれらのみを読み込むことで、さらなるパフォーマンスを節約します。この動作は完全に自動的で適応的です。後でコードを変更して追加のカラムを使用し始めると、Explorerは自動的にクエリを調整します。何も設定する必要はなく、どのカラムが必要になるかを考える必要もありません - それはNetteに任せてください。 +さらに Explorer は、コードでどの列が使われているかを追い、データベースからはそれだけを取ってきて、いっそう性能を節約します。この振る舞いは完全に自動で、状況に合わせて変わります。あとからコードを変えて別の列を使うようにすれば、Explorer はクエリを自動的に合わせます。何かを設定したり、どの列が要るかを考えたりする必要はありません。それは Nette に任せてください。 -フィルタリングとソート -=========== +絞り込みと並べ替え +========= -`Selection` クラスは、データ選択のフィルタリングとソートのためのメソッドを提供します。 +`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, &$total = null)` | ページネーションを設定します -| `group($columns, ...$params)` | 行をグループ化します(GROUP BY) -| `having($condition, ...$params)` | グループ化された行をフィルタリングするためのHAVING条件を追加します +| `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 interface |nette:introduction-to-object-oriented-programming#Fluent Interface]):`$table->where(...)->order(...)->limit(...)`。 +メソッドはつなげて書けます(いわゆる [fluent インターフェース |nette:introduction-to-object-oriented-programming#fluent インターフェース])。`$table->where(...)->order(...)->limit(...)` のようにです。 -これらのメソッドでは、[関連テーブルのデータ |#関連テーブルを介したクエリ] にアクセスするための特別な表記法を使用することもできます。 +これらのメソッドでは、[関連するテーブルのデータ |#関連するテーブルを通した問い合わせ]にアクセスする特別な書き方も使えます。 エスケープと識別子 --------- -メソッドはパラメータを自動的にエスケープし、識別子(テーブル名とカラム名)を引用符で囲むことで、SQLインジェクションを防ぎます。正しく機能させるためには、いくつかのルールに従う必要があります: +これらのメソッドはパラメータを自動的にエスケープし、識別子(テーブル名と列名)を引用符で囲むので、SQL インジェクションを防ぎます。正しく動くようにするには、いくつかの決まりを守る必要があります。 -- キーワード、関数名、プロシージャ名などは **大文字** で記述します。 -- カラム名とテーブル名は **小文字** で記述します。 -- 文字列は常に **パラメータ** を介して代入します。 +- キーワード、関数名、プロシージャ名などは**大文字**で書きます。 +- 列名とテーブル名は**小文字**で書きます。 +- 文字列はいつも**パラメータ**として渡します。 ```php -where('name = ' . $name); // 致命的な脆弱性: SQLインジェクション -where('name LIKE "%search%"'); // 悪い例: 自動引用符付けを複雑にする -where('name LIKE ?', '%search%'); // 正しい例: パラメータ経由で値が代入される +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('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演算子を自動的に選択することです。 +WHERE の条件で結果を絞り込みます。その強みは、さまざまな型の値を賢く扱い、ふさわしい SQL の演算子を自動的に選ぶところにあります。 -基本的な使用法: +基本の使い方です。 ```php $table->where('id', $value); // WHERE `id` = 123 @@ -98,26 +98,26 @@ $table->where('id > ?', $value); // WHERE `id` > 123 $table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' ``` -適切な演算子の自動検出のおかげで、さまざまな特殊なケースに対処する必要はありません。Netteがそれらを処理します: +ふさわしい演算子が自動的に見分けられるので、さまざまな特別な場合を自分で扱う必要はありません。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); 注意 - この構文はサポートされていません +$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`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -条件を配列として渡すこともでき、その要素はANDで結合されます: +条件は配列でも渡せます。その要素は AND でつながれます。 ```php // WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -配列では、キー => 値のペアを使用でき、Netteは再び正しい演算子を自動的に選択します: +配列では キー => 値 の組も使え、Nette はやはり正しい演算子を自動的に選びます。 ```php // WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) @@ -147,23 +147,23 @@ $table->where([ ]); ``` -配列では、プレースホルダー疑問符と複数のパラメータを持つSQL式を組み合わせることができます。これは、正確に定義された演算子を持つ複雑な条件に適しています: +配列の中では、SQL の式をプレースホルダや複数のパラメータと組み合わせられます。演算子をきっちり定めたい込み入った条件に向いています。 ```php // WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) $table->where([ 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // 2つのパラメータを配列として渡します + 'ROUND(score, ?) > ?' => [2, 75.5], // 2 つのパラメータを配列で渡します ]); ``` -`where()` の複数回の呼び出しは、条件を自動的にANDで結合します。 +`where()` を何度も呼ぶと、条件は自動的に AND でつながれます。 whereOr(array $parameters): static .[method] -------------------------------------------- -`where()` と同様に条件を追加しますが、ORで結合する点が異なります: +`where()` と同じように条件を足しますが、OR でつなぎます。 ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -ここでも、より複雑な式を使用できます: +ここでもより込み入った式を使えます。 ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -187,7 +187,7 @@ $table->whereOr([ wherePrimary(mixed $key): static .[method] ------------------------------------------ -テーブルの主キーの条件を追加します: +テーブルの主キーに対する条件を足します。 ```php // WHERE `id` = 123 @@ -197,7 +197,7 @@ $table->wherePrimary(123); $table->wherePrimary([1, 2, 3]); ``` -テーブルに複合主キー(例:`foo_id`, `bar_id`)がある場合は、配列として渡します: +テーブルが複合主キー(たとえば `foo_id`、`bar_id`)を持つなら、配列で渡します。 ```php // WHERE `foo_id` = 1 AND `bar_id` = 5 @@ -214,7 +214,7 @@ $table->wherePrimary([ order(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -行が返される順序を指定します。1つまたは複数のカラム、昇順または降順、またはカスタム式でソートできます: +行が返される順序を指定します。ひとつまたは複数の列で、昇順または降順に、あるいは独自の式に従って並べ替えられます。 ```php $table->order('created'); // ORDER BY `created` @@ -227,18 +227,18 @@ $table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC select(string $columns, ...$parameters): static .[method] --------------------------------------------------------- -データベースから返すカラムを指定します。デフォルトでは、Nette Database Explorerはコードで実際に使用されるカラムのみを返します。`select()` メソッドは、特定の式を返す必要がある場合に使用します: +データベースから返される列を指定します。既定では、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オブジェクトのプロパティとして利用できます: +`AS` で定義した別名は、そのあと `ActiveRow` オブジェクトのプロパティとして使えます。 ```php foreach ($table as $row) { - echo $row->formatted_date; // エイリアスへのアクセス + echo $row->formatted_date; // 別名にアクセスします } ``` @@ -246,35 +246,35 @@ foreach ($table as $row) { limit(?int $limit, ?int $offset = null): static .[method] --------------------------------------------------------- -返される行数を制限し(LIMIT)、オプションでオフセットを設定できます: +返される行の数を制限し(LIMIT)、必要なら開始位置も設定できます。 ```php -$table->limit(10); // LIMIT 10 (最初の10行を返す) +$table->limit(10); // LIMIT 10(最初の 10 行を返します) $table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` -ページネーションには、`page()` メソッドを使用する方が適しています。 +ページ分けには `page()` メソッドのほうが向いています。 page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] ------------------------------------------------------------------------- -結果のページネーションを容易にします。ページ番号(1から数える)とページあたりの項目数を受け取ります。オプションで、合計ページ数が格納される変数への参照を渡すことができます: +結果のページ分けを楽にします。ページ番号(1 から始まります)と 1 ページあたりの項目の数を受け取ります。必要なら、ページの総数が入る変数への参照も渡せます。 ```php $numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "合計ページ数: $numOfPages"; +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); +echo "Total pages: $numOfPages"; ``` group(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -指定されたカラムに基づいて行をグループ化します(GROUP BY)。通常、集計関数と組み合わせて使用されます: +指定した列に従って行をまとめます(GROUP BY)。ふつうは集約関数と組み合わせて使います。 ```php -// 各カテゴリの製品数をカウント +// カテゴリごとの商品の数を数えます $table->select('category_id, COUNT(*) AS count') ->group('category_id'); ``` @@ -283,41 +283,41 @@ $table->select('category_id, COUNT(*) AS count') having(string $having, ...$parameters): static .[method] -------------------------------------------------------- -グループ化された行をフィルタリングするための条件(HAVING)を設定します。`group()` メソッドと集計関数と組み合わせて使用できます: +まとめた行を絞り込む条件を設定します(HAVING)。`group()` メソッドや集約関数と組み合わせて使えます。 ```php -// 100以上の製品を持つカテゴリを検索 +// 商品が 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)` | 主キーに基づいて1行を返します -| `$row = $table->fetch()` | 現在の行を返し、ポインタを次に進めます -| `$array = $table->fetchPairs()` | 結果から連想配列を作成します -| `$array = $table->fetchAll()` | 全行を配列として返します -| `count($table)` | Selectionオブジェクト内の行数を返します +| `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] オブジェクトは読み取り専用です。つまり、そのプロパティの値を変更することはできません。この制限により、データの整合性が保証され、予期しない副作用が防止されます。データはデータベースから読み込まれ、変更は明示的かつ制御された方法で行われるべきです。 +[ActiveRow |api:Nette\Database\Table\ActiveRow]オブジェクトは読み取り専用です。つまりそのプロパティの値は変えられません。この制限はデータの整合性を守り、思いがけない副作用を防ぎます。データはデータベースから読み込まれるものであり、変更ははっきりと、しかも制御された形で行うべきだからです。 -`foreach` - 全行の反復処理 -------------------- +`foreach` - すべての行を回す +-------------------- -クエリを実行して行を取得する最も簡単な方法は、`foreach` ループで反復処理することです。SQLクエリを自動的に実行します。 +クエリを実行して行を取り出すいちばん簡単な方法は、`foreach` のループで回すことです。SQL のクエリは自動的に実行されます。 ```php $books = $explorer->table('book'); foreach ($books as $key => $book) { - // $keyは主キーの値、$bookはActiveRow + // $key は主キーの値、$book は ActiveRow です echo "$book->title ({$book->author->name})"; } ``` @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -SQLクエリを実行し、主キーに基づいて行を返します。存在しない場合は `null` を返します。 +SQL のクエリを実行し、主キーで行を返します。存在しなければ `null` を返します。 ```php -$book = $explorer->table('book')->get(123); // ID 123のActiveRowまたはnullを返します +$book = $explorer->table('book')->get(123); // ID 123 の ActiveRow か null を返します if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -行を返し、内部ポインタを次に進めます。これ以上行がない場合は `null` を返します。 +今の行を返し、内部のポインタを次へ進めます。行がもうなければ `null` を返します。 ```php $books = $explorer->table('book'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -結果を連想配列として返します。最初の引数は配列のキーとして使用されるカラム名を指定し、2番目の引数は値として使用されるカラム名を指定します: +結果を連想配列として返します。第 1 引数は配列のキーとして使う列の名前、第 2 引数は値として使う列の名前を指定します。 ```php $authors = $explorer->table('author')->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -最初のパラメータのみを指定した場合、値は行全体、つまり `ActiveRow` オブジェクトになります: +第 1 パラメータだけを渡すと、値は行全体、つまり `ActiveRow` オブジェクトになります。 ```php $authors = $explorer->table('author')->fetchPairs('id'); // [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -キーが重複する場合、最後の行の値が使用されます。キーとして `null` を使用すると、配列はゼロから始まる数値インデックスになります(この場合、衝突は発生しません): +キーが重なった場合は最後の行の値が使われます。キーに `null` を使うと、配列はゼロから始まる添字になります(そうすれば衝突は起きません)。 ```php $authors = $explorer->table('author')->fetchPairs(null, 'name'); @@ -377,17 +377,17 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -あるいは、パラメータとしてコールバックを指定することもできます。これは、各行に対して値自体、またはキーと値のペアのいずれかを返します。 +代わりにパラメータとしてコールバックを渡せます。それは行ごとにひとつの値か、キーと値の組を返します。 ```php $titles = $explorer->table('book') ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['最初の本 (Jan Novák)', ...] +// ['First Book (John Novak)', ...] -// コールバックはキーと値のペアを持つ配列を返すこともできます: +// コールバックはキーと値の組の配列を返すこともできます: $titles = $explorer->table('book') ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['最初の本' => 'Jan Novák', ...] +// ['First Book' => 'John Novak', ...] ``` @@ -405,21 +405,21 @@ $allBooks = $explorer->table('book')->fetchAll(); count(): int .[method] ---------------------- -パラメータなしの `count()` メソッドは、`Selection` オブジェクト内の行数を返します: +パラメータなしの `count()` メソッドは `Selection` オブジェクトの行の数を返します。 ```php $table->where('category', 1); $count = $table->count(); -$count = count($table); // 代替 +$count = count($table); // 別の書き方 ``` -注意:パラメータ付きの `count()` は、データベースで集計関数COUNTを実行します。下記参照。 +注意: パラメータ付きの `count()` はデータベースで COUNT の集約関数を実行します。下をご覧ください。 ActiveRow::toArray(): array .[method] ------------------------------------- -`ActiveRow` オブジェクトを連想配列に変換します。キーはカラム名、値は対応するデータです。 +`ActiveRow` オブジェクトを連想配列に変えます。キーは列の名前、値は対応するデータです。 ```php $book = $explorer->table('book')->get(1); @@ -428,36 +428,36 @@ $bookArray = $book->toArray(); ``` -集計 -======== +集約 +=== -`Selection` クラスは、集計関数(COUNT、SUM、MIN、MAX、AVGなど)を簡単に実行するためのメソッドを提供します。 +`Selection` クラスは、集約関数(COUNT、SUM、MIN、MAX、AVG など)を簡単に実行するメソッドを提供します。 .[language-php] -| `count($expr)` | 行数をカウントします -| `min($expr)` | カラム内の最小値を返します -| `max($expr)` | カラム内の最大値を返します -| `sum($expr)` | カラム内の値の合計を返します -| `aggregation($function)` | 任意の集計関数を実行できます。例: `AVG()`, `GROUP_CONCAT()` +| `count($expr)` | 行の数を数えます | +| `min($expr)` | 列の最小値を返します | +| `max($expr)` | 列の最大値を返します | +| `sum($expr)` | 列の値の合計を返します | +| `aggregation($function)` | `AVG()` や `GROUP_CONCAT()` など、任意の集約関数を使えます | count(string $expr): int .[method] ---------------------------------- -COUNT関数を使用してSQLクエリを実行し、結果を返します。このメソッドは、特定の条件に一致する行数を調べるために使用されます: +COUNT 関数を使う SQL のクエリを実行し、その結果を返します。ある条件に合う行がいくつあるかを調べるのに使います。 ```php $count = $table->count('*'); // SELECT COUNT(*) FROM `table` $count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` ``` -注意:パラメータなしの [#count()] は、`Selection` オブジェクト内の行数を返すだけです。 +注意: パラメータなしの [#count()]は `Selection` オブジェクトの行の数を返すだけです。 -min(string $expr) a max(string $expr) .[method] +min(string $expr) と max(string $expr) .[method] ----------------------------------------------- -`min()` および `max()` メソッドは、指定されたカラムまたは式の最小値と最大値を返します: +`min()` と `max()` メソッドは、指定した列や式の最小値と最大値を返します。 ```php // SELECT MAX(`price`) FROM `products` WHERE `active` = 1 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -指定されたカラムまたは式の値の合計を返します: +指定した列や式の値の合計を返します。 ```php // SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 @@ -478,39 +478,39 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -任意の集計関数を実行できます。 +任意の集約関数を実行できます。 ```php -// カテゴリ内の製品の平均価格 +// カテゴリの商品の平均価格 $avgPrice = $products->where('category_id', 1) ->aggregation('AVG(price)'); -// 製品のタグを1つの文字列に結合します +// 商品のタグをひとつの文字列につなぎます $tags = $products->where('id', 1) ->aggregation('GROUP_CONCAT(tag.name) AS tags') ->fetch() ->tags; ``` -既に何らかの集計関数とグループ化から生じた結果(例:グループ化された行に対する `SUM(値)`)を集計する必要がある場合、2番目の引数として、これらの中間結果に適用する集計関数を指定します: +すでに何らかの集約関数とまとめの結果になっているもの(たとえばまとめた行に対する `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関数を適用することを示します。 +この例では、まずカテゴリごとに商品の合計金額を計算し(`SUM(price * stock) AS category_total`)、結果を `category_id` でまとめます。そのあと `aggregation('SUM(category_total)', 'SUM')` でこれらの中間の合計 `category_total` を足し合わせます。第 2 引数の `'SUM'` は、中間の結果に SUM 関数を当てることを指定しています。 -Insert, Update & Delete -======================= +挿入、更新、削除 +======== -Nette Database Explorerは、データの挿入、更新、削除を簡素化します。記載されているすべてのメソッドは、`Nette\Database\DriverException` 例外をスローします。 +Nette Database Explorer はデータの挿入、更新、削除を簡単にします。ここで挙げるメソッドはすべて、エラーの場合に `Nette\Database\DriverException` を投げます。 Selection::insert(iterable $data) .[method] @@ -518,26 +518,26 @@ Selection::insert(iterable $data) .[method] テーブルに新しいレコードを挿入します。 -**単一レコードの挿入:** +**ひとつのレコードを挿入する:** -新しいレコードを連想配列またはiterableオブジェクト(たとえば [フォーム |forms:] で使用されるArrayHash)として渡します。キーはテーブルのカラム名に対応します。 +新しいレコードを連想配列か、反復できるオブジェクト([フォーム |forms:]で使われる `ArrayHash` など)として渡します。キーはテーブルの列の名前に対応します。 -テーブルに主キーが定義されている場合、メソッドはデータベースから再読み込みされた `ActiveRow` オブジェクトを返します。これにより、データベースレベルで行われた変更(トリガー、カラムのデフォルト値、自動インクリメントカラムの計算)が反映されます。これにより、データの整合性が保証され、オブジェクトは常にデータベースからの最新データを含みます。一意の主キーがない場合は、渡されたデータを配列形式で返します。 +テーブルに主キーが定義されていれば、このメソッドは `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; // トリガーによって設定されている場合、作成時間を出力します +// $row は ActiveRow のインスタンスで、挿入された行の完全なデータを持ちます。 +// 自動的に生成された ID や、トリガーによる変更も含みます +echo $row->id; // 新しく挿入されたユーザーの ID を出力します +echo $row->created_at; // トリガーで設定されていれば作成時刻を出力します ``` -**複数のレコードを一度に挿入:** +**複数のレコードを一度に挿入する:** -`insert()` メソッドを使用すると、単一のSQLクエリで複数のレコードを挿入できます。この場合、挿入された行数を返します。 +`insert()` メソッドは、ひとつの SQL のクエリで複数のレコードを挿入できます。その場合は挿入された行の数を返します。 ```php $insertedRows = $explorer->table('users')->insert([ @@ -554,7 +554,7 @@ $insertedRows = $explorer->table('users')->insert([ // $insertedRows は 2 になります ``` -パラメータとして、データ選択を持つ `Selection` オブジェクトを渡すこともできます。 +パラメータとして、データを選び出した `Selection` オブジェクトも渡せます。 ```php $newUsers = $explorer->table('potential_users') @@ -564,16 +564,16 @@ $newUsers = $explorer->table('potential_users') $insertedRows = $explorer->table('users')->insert($newUsers); ``` -**特殊な値の挿入:** +**特別な値を挿入する:** -値として、ファイル、DateTimeオブジェクト、またはSQLリテラルを渡すこともできます: +値としてファイル、`DateTime` オブジェクト、SQL のリテラルも渡せます。 ```php $explorer->table('users')->insert([ 'name' => 'John', - 'created_at' => new DateTime, // データベース形式に変換します - 'avatar' => fopen('image.jpg', 'rb'), // ファイルのバイナリコンテンツを挿入します - 'uuid' => $explorer::literal('UUID()'), // UUID() 関数を呼び出します + 'created_at' => new DateTime, // データベースの書式に変換します + 'avatar' => fopen('image.jpg', 'rb'), // ファイルの中身をバイナリとして入れます + 'uuid' => $explorer::literal('UUID()'), // UUID() 関数を呼びます ]); ``` @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -指定されたフィルタに従ってテーブル内の行を更新します。実際に変更された行数を返します。 +指定した絞り込みに従ってテーブルの行を更新します。実際に変わった行の数を返します。 -変更するカラムを連想配列またはiterableオブジェクト(たとえば [フォーム |forms:] で使用されるArrayHash)として渡します。キーはテーブルのカラム名に対応します: +変える列を連想配列か、反復できるオブジェクト([フォーム |forms:]で使われる `ArrayHash` など)として渡します。キーはテーブルの列の名前に対応します。 ```php $affected = $explorer->table('users') @@ -595,14 +595,14 @@ $affected = $explorer->table('users') // 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減らします + 'points+=' => 1, // 'points' 列の値を 1 増やします + 'coins-=' => 1, // 'coins' 列の値を 1 減らします ]); // UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` @@ -611,7 +611,7 @@ $explorer->table('users') Selection::delete(): int .[method] ---------------------------------- -指定されたフィルタに従ってテーブルから行を削除します。削除された行数を返します。 +指定した絞り込みに従ってテーブルの行を削除します。削除された行の数を返します。 ```php $count = $explorer->table('users') @@ -621,83 +621,83 @@ $count = $explorer->table('users') ``` .[caution] -`update()` および `delete()` を呼び出すときは、`where()` を使用して変更/削除する行を指定することを忘れないでください。`where()` を使用しない場合、操作はテーブル全体に対して実行されます! +`update()` や `delete()` を呼ぶときは、変更/削除する行を `where()` で指定するのを忘れないでください。`where()` を使わないと、その操作はテーブル全体に対して行われます。 ActiveRow::update(iterable $data): bool .[method] ------------------------------------------------- -`ActiveRow` オブジェクトによって表されるデータベース行のデータを更新します。パラメータとして、更新するデータを含むiterable(キーはカラム名)を受け取ります。数値の値を変更するには、`+=` および `-=` 演算子を使用できます: +`ActiveRow` オブジェクトが表すデータベースの行のデータを更新します。更新するデータを持つ反復できるものを受け取ります(キーは列の名前です)。数値を変えるには `+=` と `-=` の演算子を使えます。 -更新を実行した後、`ActiveRow` はデータベースから自動的に再読み込みされ、データベースレベルで行われた変更(例:トリガー)が反映されます。メソッドは、データが実際に変更された場合にのみtrueを返します。 +更新のあと、`ActiveRow` はデータベースの側で起きた変更(トリガーなど)を映すために自動的に読み直されます。このメソッドは、実際にデータが変わったときにだけ `true` を返します。 ```php $article = $explorer->table('article')->get(1); $article->update([ - 'views += 1', // 表示回数を増やします + 'views += 1', // 閲覧数を増やします ]); -echo $article->views; // 現在の表示回数を出力します +echo $article->views; // 今の閲覧数を出力します ``` -このメソッドは、データベース内の特定の1行のみを更新します。複数の行を一括更新するには、[#Selection::update()] メソッドを使用します。 +このメソッドはデータベースの特定の 1 行だけを更新します。複数の行をまとめて更新するには [#Selection::update()]メソッドを使ってください。 -ActiveRow::delete() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -`ActiveRow` オブジェクトによって表されるデータベースから行を削除します。 +`ActiveRow` オブジェクトが表す行をデータベースから削除します。削除された行の数を返します。それは 1 のはずです。 ```php $book = $explorer->table('book')->get(1); -$book->delete(); // ID 1の書籍を削除します +$book->delete(); // ID 1 の本を削除します ``` -このメソッドは、データベース内の特定の1行のみを削除します。複数の行を一括削除するには、[#Selection::delete()] メソッドを使用します。 +このメソッドはデータベースの特定の 1 行だけを削除します。複数の行をまとめて削除するには [#Selection::delete()]メソッドを使ってください。 -テーブル間のリレーション -============ +テーブルどうしの関係 +========== -リレーショナルデータベースでは、データは複数のテーブルに分割され、外部キーを使用して相互にリンクされています。Nette Database Explorerは、これらのリレーションを操作するための革新的な方法を提供します - JOINクエリを記述したり、何かを設定したり生成したりする必要はありません。 +リレーショナルデータベースでは、データは複数のテーブルに分けられ、外部キーで結び付けられます。Nette Database Explorer は、この関係を扱う画期的な方法を提供します。JOIN のクエリを書く必要も、何かを設定したり生成したりする必要もありません。 -リレーションの操作を説明するために、書籍データベースの例を使用します([GitHubで見つけることができます |https://github.com/nette-examples/books])。データベースには次のテーブルがあります: +関係の扱いを説明するために、本のデータベースの例を使います([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`) +- `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 *] *** データベース構造 .<> +[* db-schema-1-.webp *] *** 例で使うデータベースの構造 .<> -書籍データベースの例では、いくつかのタイプの関係が見つかります(モデルは現実よりも単純化されていますが): +この本のデータベースの例には、いくつかの種類の関係があります(現実に比べると簡略にしてあります)。 -- One-to-many 1:N – 各書籍には **1人の** 著者がおり、著者は **複数の** 書籍を書くことができます -- Zero-to-many 0:N – 書籍には翻訳者が **いる場合があり**、翻訳者は **複数の** 書籍を翻訳できます -- Zero-to-one 0:1 – 書籍には続編が **ある場合があります** -- Many-to-many M:N – 書籍には **複数の** タグがあり、タグは **複数の** 書籍に割り当てることができます +- **1 対多(1:N)** - それぞれの本には著者が**ひとり**います。著者は**複数**の本を書けます。 +- **0 対多(0:N)** - 本には翻訳者が**いることがあります**。翻訳者は**複数**の本を訳せます。 +- **0 対 1(0:1)** - 本には続編が**あることがあります**。 +- **多対多(M:N)** - 本には**いくつかの**タグを付けられ、タグは**いくつもの**本に付けられます。 -これらの関係では、常に親テーブルと子テーブルが存在します。たとえば、著者と書籍の関係では、`author` テーブルが親で、`book` テーブルが子です - 書籍は常に何らかの著者に「属している」と考えることができます。これはデータベースの構造にも反映されています:子テーブル `book` には、親テーブル `author` を参照する外部キー `author_id` が含まれています。 +こうした関係にはいつも**親テーブル**と**子テーブル**があります。たとえば著者と本の関係では、`author` テーブルが親で `book` テーブルが子です。本はいつも著者に「属している」と考えると分かりやすいでしょう。これはデータベースの構造にも表れています。子テーブル `book` は、親テーブル `author` を参照する外部キー `author_id` を持ちます。 -著者名を含む書籍をリストする必要がある場合、2つの選択肢があります。JOINを使用して単一のSQLクエリでデータを取得する: +本を著者の名前とともに並べたいなら、やり方は 2 つあります。JOIN を使ってひとつの SQL のクエリでデータを取ってくるか、 ```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -または、データを2段階で読み込む - まず書籍、次にその著者 - そしてPHPでそれらを組み立てる: +あるいは 2 段階でデータを取ってきて、まず本、次にその著者を取り、そのあと PHP で組み合わせるかです。 ```sql SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- 取得した書籍の著者ID +SELECT * FROM author WHERE id IN (1, 2, 3); -- 選ばれた本の著者の ID ``` -2番目のアプローチは、驚くべきかもしれませんが、実際にはより効率的です。データは一度だけ読み込まれ、キャッシュでより良く利用できます。Nette Database Explorerはこの方法で動作します - すべてを内部で処理し、エレガントなAPIを提供します: +意外に思えるかもしれませんが、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 'written by: ' . $book->author->name; // $book->author は 'author' テーブルのレコードです echo 'translated by: ' . $book->translator?->name; } ``` @@ -706,26 +706,26 @@ foreach ($books as $book) { 親テーブルへのアクセス ----------- -親テーブルへのアクセスは簡単です。これは *書籍には著者がいる* または *書籍には翻訳者がいる場合がある* のような関係です。関連するレコードは、ActiveRowオブジェクトのプロパティを介して取得します - その名前は、`id` を除いた外部キーのカラム名に対応します: +親テーブルへのアクセスは分かりやすいものです。*本には著者がいる*、*本には翻訳者がいることがある* といった関係です。関連するレコードは ActiveRow オブジェクトのプロパティで取り出せます。その名前は、外部キーの列の名前から `_id` の接尾辞を取ったものに対応します。 ```php $book = $explorer->table('book')->get(1); -echo $book->author->name; // author_id カラムに基づいて著者を見つけます -echo $book->translator?->name; // translator_id に基づいて翻訳者を見つけます +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` を含む可能性があるため、コードで `?->` 演算子を使用します。 +`$book->author` プロパティにアクセスすると、Explorer は `book` テーブルの中から、名前に `author` という文字列を含む列(つまり `author_id`)を探します。その列の値をもとに `author` テーブルの対応するレコードを読み込み、`ActiveRow` として返します。同じように `$book->translator` は `translator_id` 列を使います。`translator_id` 列は `null` を持てるので、コードでは nullsafe 演算子 `?->` を使っています。 -代替の方法として、`ref()` メソッドがあります。これは、ターゲットテーブルの名前と結合カラムの名前の2つの引数を受け取り、`ActiveRow` インスタンスまたは `null` を返します: +別のやり方として `ref()` メソッドがあります。これは 2 つの引数、つまり対象のテーブルの名前とつなぐ列の名前を受け取り、`ActiveRow` のインスタンスか `null` を返します。 ```php -echo $book->ref('author', 'author_id')->name; // 著者へのリレーション -echo $book->ref('author', 'translator_id')->name; // 翻訳者へのリレーション +echo $book->ref('author', 'author_id')->name; // 著者への関係 +echo $book->ref('author', 'translator_id')->name; // 翻訳者への関係 ``` -`ref()` メソッドは、テーブルに同じ名前のカラム(つまり `author`)が含まれているためにプロパティアクセスを使用できない場合に便利です。その他の場合、読みやすいプロパティアクセスを使用することをお勧めします。 +`ref()` メソッドは、たとえばテーブルに同じ名前の列(つまり `author`)があってプロパティによるアクセスが使えない場合に役立ちます。そのほかの場合は、読みやすさのためにプロパティによるアクセスをおすすめします。 -Explorerはデータベースクエリを自動的に最適化します。ループで書籍を反復処理し、それらの関連レコード(著者、翻訳者)にアクセスする場合、Explorerは各書籍に対して個別にクエリを生成しません。代わりに、各リレーションタイプに対して1つのSELECTのみを実行し、データベースの負荷を大幅に削減します。たとえば: +Explorer はデータベースのクエリを自動的に最適化します。ループで本を回しながら関連するレコード(著者、翻訳者)にアクセスしても、Explorer は本ごとにクエリを作ったりしません。代わりに**関係の種類ごとに SELECT のクエリをひとつだけ**実行し、データベースの負荷を大きく減らします。たとえば次のようにです。 ```php $books = $explorer->table('book'); @@ -736,168 +736,168 @@ foreach ($books as $book) { } ``` -このコードは、データベースに対してこれら3つの高速なクエリのみを呼び出します: +このコードはデータベースに対して、次の 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 +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] の使用をお勧めします。これは外部キーを分析し、テーブル間の既存のリレーションを簡単に操作できます。 +つなぐ列を見つける論理は [Conventions |api:Nette\Database\Conventions]の実装が決めます。外部キーを解析し、テーブルどうしの既存の関係を簡単に扱える [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions]をおすすめします。 子テーブルへのアクセス ----------- -子テーブルへのアクセスは逆方向に機能します。今度は *この著者が書いた書籍は何か* または *この翻訳者が翻訳した書籍は何か* を尋ねます。このタイプのクエリには、関連レコードを持つ `Selection` を返す `related()` メソッドを使用します。例を見てみましょう: +子テーブルへのアクセスは逆向きに働きます。今度は *この著者はどの本を書いたか*、*この翻訳者はどの本を訳したか* と尋ねます。この種の問い合わせには `related()` メソッドを使います。これは関連するレコードを持つ `Selection` を返します。例を見てみましょう。 ```php $author = $explorer->table('author')->get(1); -// 著者のすべての書籍を出力します +// その著者のすべての本を出力します foreach ($author->related('book.author_id') as $book) { - echo "執筆: $book->title"; + echo "Wrote: $book->title"; } -// 著者が翻訳したすべての書籍を出力します +// その著者が訳したすべての本を出力します foreach ($author->related('book.translator_id') as $book) { - echo "翻訳: $book->title"; + echo "Translated: $book->title"; } ``` -`related()` メソッドは、ドット表記の単一引数として、または2つの個別の引数として結合の説明を受け入れます: +`related()` メソッドは、つなぎ方の指定をドットの書き方でひとつの引数として、あるいは 2 つの別々の引数として受け取ります。 ```php -$author->related('book.translator_id'); // 1つの引数 -$author->related('book', 'translator_id'); // 2つの引数 +$author->related('book.translator_id'); // 引数ひとつ +$author->related('book', 'translator_id'); // 引数 2 つ ``` -Explorerは、親テーブルの名前に基づいて正しい結合カラムを自動的に検出できます。この場合、ソーステーブルの名前が `author` であるため、`book.author_id` カラムを介して結合されます: +Explorer は親テーブルの名前をもとに、正しいつなぎの列を自動的に見つけられます。この場合、もとのテーブルの名前が `author` なので `book.author_id` 列でつなぎます。 ```php -$author->related('book'); // book.author_id を使用します +$author->related('book'); // book.author_id を使います ``` -複数の可能な結合が存在する場合、Explorerは [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] 例外をスローします。 +つなぎ方の候補が複数あると、Explorer は [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]を投げます。 -`related()` メソッドは、もちろん、ループで複数のレコードを反復処理する場合にも使用でき、Explorerはこの場合でもクエリを自動的に最適化します: +もちろん `related()` メソッドは、ループで複数のレコードを回しながらも使えます。その場合も Explorer はクエリを自動的に最適化します。 ```php $authors = $explorer->table('author'); foreach ($authors as $author) { - echo $author->name . ' 執筆:'; + echo $author->name . ' wrote:'; foreach ($author->related('book') as $book) { echo $book->title; } } ``` -このコードは、2つの高速なSQLクエリのみを生成します: +このコードはごく速い SQL のクエリを 2 つだけ生みます。 ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- 選択された著者のID +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- 選ばれた著者の ID ``` -多対多リレーション ---------- +多対多の関係 +------ -多対多(M:N)リレーションには、2つの外部キーカラム(`book_id`、`tag_id`)を含む関連テーブル(この場合は `book_tag`)が必要です。これらのカラムのそれぞれは、リンクされたテーブルの1つの主キーを参照します。関連データを取得するには、まず `related('book_tag')` を使用して関連テーブルからレコードを取得し、次にターゲットデータに進みます: +多対多(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; // 関連テーブルを介してタグの名前を出力します + echo $bookTag->tag->name; // 中間テーブル経由でタグの名前を出力します } $tag = $explorer->table('tag')->get(1); -// または逆: このタグでマークされた書籍の名前を出力します +// あるいは逆向きに: そのタグが付いた本の名前を出力します foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // 書籍の名前を出力します + echo $bookTag->book->title; // 本のタイトルを出力します } ``` -Explorerは再びSQLクエリを効率的な形式に最適化します: +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 +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を自動的に作成します。 +`where()`、`select()`、`order()`、`group()` メソッドでは、ほかのテーブルの列にアクセスする特別な書き方を使えます。Explorer は必要な JOIN を自動的に作ります。 -**ドット表記** (`親テーブル.カラム`) は、子テーブルの観点からの1:N関係に使用されます: +**ドットの書き方**(`親テーブル.列`)は、子テーブルから見た 1:N の関係に使います。 ```php $books = $explorer->table('book'); -// 著者の名前が 'Jon' で始まる書籍を見つけます +// 著者の名前が 'Jon' で始まる本を見つけます $books->where('author.name LIKE ?', 'Jon%'); -// 著者の名前で書籍を降順にソートします +// 本を著者の名前の降順に並べます $books->order('author.name DESC'); -// 書籍のタイトルと著者の名前を出力します +// 本のタイトルと著者の名前を出力します $books->select('book.title, author.name'); ``` -**コロン表記** (`:子テーブル.カラム`) は、親テーブルの観点からの1:N関係に使用されます: +**コロンの書き方**(`:子テーブル.列`)は、親テーブルから見た 1:N の関係に使います。 ```php $authors = $explorer->table('author'); -// タイトルに 'PHP' を含む書籍を書いた著者を見つけます +// タイトルに '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] 例外をスローします。 +上のコロンの書き方の例(`:book.title`)では、外部キーの列を指定していません。Explorer は親テーブルの名前をもとに正しい列を自動的に見つけます。この場合、もとのテーブルの名前が `author` なので `book.author_id` 列でつなぎます。つなぎ方の候補が複数あると、Explorer は [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]を投げます。 -結合カラムは括弧内に明示的に指定できます: +つなぐ列はかっこの中ではっきり指定できます。 ```php -// タイトルに 'PHP' を含む書籍を翻訳した著者を見つけます +// タイトルに 'PHP' を含む本を訳した著者を見つけます $authors->where(':book(translator_id).title LIKE ?', '%PHP%'); ``` -表記法は、複数のテーブルを介してアクセスするために連鎖させることができます: +書き方はつなげられるので、複数のテーブルをまたいでデータにアクセスできます。 ```php -// 'PHP' タグでマークされた書籍の著者を見つけます +// 'PHP' のタグが付いた本の著者を見つけます $authors->where(':book:book_tag.tag.name', 'PHP') ->group('author.id'); ``` -JOIN条件の拡張 ---------- +JOIN の条件を広げる +------------ -`joinWhere()` メソッドは、SQLでテーブルを結合する際に `ON` キーワードの後に指定される条件を拡張します。 +`joinWhere()` メソッドは、SQL でテーブルをつなぐときの `ON` キーワードのうしろの条件を広げます。 -特定の翻訳者によって翻訳された書籍を見つけたいとしましょう: +ある翻訳者が訳した本を見つけたいとしましょう。 ```php -// 'David' という名前の翻訳者によって翻訳された書籍を見つけます +// '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式。 +`joinWhere()` の条件では、`where()` メソッドと同じ書き方、つまり演算子、プレースホルダ、値の配列、SQL の式を使えます。 -複数のJOINを持つより複雑なクエリの場合、テーブルエイリアスを定義できます: +JOIN が複数ある込み入ったクエリでは、テーブルの別名を定義できます。 ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // AND (`book_author`.`born` < 1950) ``` -`where()` メソッドが `WHERE` 句に条件を追加するのに対し、`joinWhere()` メソッドはテーブルを結合する際の `ON` 句の条件を拡張することに注意してください。 +`where()` メソッドが `WHERE` の句に条件を足すのに対し、`joinWhere()` メソッドはテーブルをつなぐときの `ON` の句の条件を広げることに注意してください。 diff --git a/database/ja/guide.texy b/database/ja/guide.texy index 26fd499ed4..09c66eb5b7 100644 --- a/database/ja/guide.texy +++ b/database/ja/guide.texy @@ -2,30 +2,30 @@ Nette Database ************** .[perex] -Nette Databaseは、シンプルさとスマートな機能に重点を置いた、PHP向けの強力でエレガントなデータベース層です。データベースを操作する2つの方法を提供します - アプリケーションの迅速な開発のための[Explorer |Explorer]、またはクエリを直接操作するための[SQLアクセス |SQL way]。 +Nette Database は PHP のための強力で優雅なデータベース層で、単純さと気の利いた機能を大切にしています。データベースを扱う方法を 2 つ用意しています。素早くアプリケーションを開発するための [Explorer |explorer]と、クエリを直接組み立てる [SQL アプローチ |SQL way]です。 <div class="grid gap-3"> <div> -[SQLアクセス |SQL way] -================== -- 安全なパラメータ化クエリ -- SQLクエリの形式に対する正確な制御 -- 高度な機能を持つ複雑なクエリを作成する場合 -- 特定のSQL機能を使用してパフォーマンスを最適化する場合 +[SQL アプローチ|sql-way] +=================== +- 安全でパラメータ化されたクエリ +- SQL のクエリの構造を細かく制御 +- 進んだ機能を使う込み入ったクエリを書くとき +- 特定の SQL の関数を使って性能を最適化 </div> <div> -[Explorer |Explorer] +[Explorer |explorer] ==================== -- SQLを書かずに迅速に開発 -- テーブル間のリレーションを直感的に操作 -- クエリの自動最適化を評価 -- データベースを迅速かつ快適に操作するのに適しています +- SQL を書かずに素早く開発 +- テーブルどうしの関係を直感的に扱う +- クエリの自動的な最適化の恩恵を受ける +- 速く快適にデータベースを扱うのに向く </div> @@ -35,45 +35,45 @@ Nette Databaseは、シンプルさとスマートな機能に重点を置いた インストール ====== -ライブラリは[Composer|best-practices:composer]ツールを使用してダウンロードおよびインストールします: +ライブラリは [Composer|best-practices:composer]でダウンロードしてインストールします。 ```shell composer require nette/database ``` -サポートされているデータベース -=============== +対応しているデータベース +============ -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 | - +|* データベースサーバー |* 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つのアプローチ -================ +データベースを扱う 2 つのやり方 +================= -Nette Databaseは選択肢を提供します:SQLクエリを直接記述する(SQLアクセス)か、自動的に生成させる(Explorer)かです。両方のアプローチが同じタスクをどのように解決するかを見てみましょう: +Nette Database は選択肢を与えます。SQL のクエリを直接書く(SQL アプローチ)か、自動的に生成させる(Explorer)かです。同じ仕事を両方のやり方でどう片付けるか見てみましょう。 -[SQLアクセス |sql way] - SQLクエリ +[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 @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// 出力 (最適ではない、N個の追加クエリを生成する) +// 表示します(最適ではなく、N 個の追加のクエリを生みます) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -90,7 +90,7 @@ foreach ($result as $author) { ORDER BY published_at DESC ', $author->id); - echo "著者 $author->name は $author->books_count 冊の本を書きました:\n"; + echo "Author $author->name has written $author->books_count books:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -98,26 +98,26 @@ foreach ($result as $author) { } ``` -[Explorerアクセス |explorer] - SQLの自動生成 +[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つのクエリのみを生成) +// 表示します(自動的に最適化された 2 つのクエリだけを生みます) foreach ($authors as $author) { $books = $author->related('books') ->order('published_at DESC'); - echo "著者 $author->name は {$books->count()} 冊の本を書きました:\n"; + echo "Author $author->name has written {$books->count()} books:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -125,23 +125,23 @@ foreach ($authors as $author) { } ``` -ExplorerアクセスはSQLクエリを自動的に生成および最適化します。上記の例では、SQLアクセスはN+1個のクエリ(著者用に1つ、各著者の本用に1つ)を生成しますが、Explorerはクエリを自動的に最適化し、2つだけ実行します - 著者用に1つ、すべての本用に1つです。 +Explorer のやり方は SQL のクエリを自動的に生成して最適化します。上の例では、SQL アプローチが N+1 個のクエリ(著者に 1 つ、そして著者ごとに本のクエリが 1 つずつ)を生むのに対し、Explorer はクエリを自動的に最適化して 2 つだけ、つまり著者に 1 つと、そのすべての本に 1 つを実行します。 -両方のアプローチは、必要に応じてアプリケーション内で自由に組み合わせることができます。 +どちらのやり方も、必要に応じてアプリケーションの中で自由に組み合わせられます。 接続と設定 ===== -データベースに接続するには、[api:Nette\Database\Connection]クラスのインスタンスを作成するだけです: +データベースに接続するには、[api:Nette\Database\Connection]クラスのインスタンスを作るだけです。 ```php $database = new Nette\Database\Connection($dsn, $user, $password); ``` -パラメータ `$dsn`(データソース名)は、[PDOが使用するもの |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters]と同じです。例:`host=127.0.0.1;dbname=test`。失敗した場合、`Nette\Database\ConnectionException`例外をスローします。 +`$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:]バーのデータベースパネルが作成されます。 +とはいえ、もっと便利な方法を[アプリケーションの設定 |configuration]が用意しています。そこに `database` の区画を足すだけです。これで必要なオブジェクトが作られ、[Tracy |tracy:]のバーにデータベースのパネルも現れます。 ```neon database: @@ -150,7 +150,7 @@ database: password: password ``` -その後、接続オブジェクトを[DIコンテナからサービスとして取得 |dependency-injection:passing-dependencies]します。例: +そのあと接続のオブジェクトは [DI コンテナからサービスとして受け取れます |dependency-injection:passing-dependencies]。たとえば次のようにです。 ```php class Model @@ -163,54 +163,56 @@ class Model } ``` -[データベース設定 |configuration]の詳細については、こちらをご覧ください。 +詳しくは[データベースの設定|configuration]をご覧ください。 -Explorerの手動作成 -------------- +Explorer を手で作る +-------------- -Nette DIコンテナを使用しない場合は、`Nette\Database\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 を実装、例: +// キャッシュの保管場所。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`を設定するか、次のようにして有効にします: +`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`例外をスローする可能性があります。 +接続を管理するには `connect()`、`disconnect()`、`reconnect()` メソッドを使います。 +- `connect()` はまだ接続がなければ接続を作り、`Nette\Database\ConnectionException` を投げることがあります。 +- `disconnect()` は今のデータベース接続を切ります。 +- `reconnect()` は接続を切ってから、データベースに接続し直します。このメソッドも `Nette\Database\ConnectionException` を投げることがあります。 -さらに、`onConnect`イベントを使用して接続に関連するイベントを監視できます。これは、データベースとの接続が確立された後に呼び出されるコールバックの配列です。 +さらに `onConnect` イベントで接続にまつわる出来事を見張れます。これはデータベースに接続したあとに呼ばれるコールバックの配列です。 ```php -// データベースへの接続後に実行されます +// データベースに接続したあとに実行されます $database->onConnect[] = function($database) { - echo "データベースに接続しました"; + echo "Connected to the database"; }; ``` +`onQuery` イベントも同じように働きます。これは実行されたクエリごとに(そしてクエリが失敗したときにも)呼ばれるコールバックの配列で、ログや性能の計測に役立ちます。 + -Tracyデバッグバー -=========== +Tracy のデバッグバー +============= -[Tracy |tracy:]を使用している場合、デバッグバーにデータベースパネルが自動的にアクティブになり、実行されたすべてのクエリ、そのパラメータ、実行時間、およびコード内で呼び出された場所が表示されます。 +[Tracy |tracy:]を使っていれば、デバッグバーの Database のパネルが自動的に有効になります。そこには実行されたすべてのクエリ、そのパラメータ、実行にかかった時間、そしてそれが呼ばれたコードの場所が表示されます。 [* db-panel.webp *] diff --git a/database/ja/mapping.texy b/database/ja/mapping.texy deleted file mode 100644 index 7894b02a82..0000000000 --- a/database/ja/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -型変換 -*** - -.[perex] -Nette Databaseは、データベースから返された値を対応するPHP型に自動的に変換します。 - - -日付と時刻 ------ - -時間データは`Nette\Utils\DateTime`オブジェクトに変換されます。時間データを不変の`Nette\Database\DateTime`オブジェクトに変換したい場合は、[設定 |configuration]で`newDateTime`オプションをtrueに設定します。 - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('Y年n月j日'); -``` - -MySQLの場合、データ型`TIME`は`DateInterval`オブジェクトに変換されます。 - - -ブール値 ----- - -ブール値は自動的に`true`または`false`に変換されます。[設定 |configuration]で`convertBoolean`を設定すると、MySQLでは`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/reflection.texy b/database/ja/reflection.texy index d7fa9dc883..c188254a50 100644 --- a/database/ja/reflection.texy +++ b/database/ja/reflection.texy @@ -1,10 +1,10 @@ -構造リフレクション -********* +構造のリフレクション +********** .{data-version:3.2.1} -Nette Databaseは、[api:Nette\Database\Reflection]クラスを使用してデータベース構造をイントロスペクションするためのツールを提供します。これにより、テーブル、カラム、インデックス、および外部キーに関する情報を取得できます。リフレクションを使用して、スキーマの生成、データベースを操作する柔軟なアプリケーションの作成、または一般的なデータベースツールの作成を行うことができます。 +Nette Database は [api:Nette\Database\Reflection]クラスで、データベースの構造を調べる道具を提供します。テーブル、列、インデックス、外部キーの情報を取り出せます。リフレクションはスキーマの生成、データベースを扱う柔軟なアプリケーション、汎用のデータベース向けの道具などに使えます。 -リフレクションオブジェクトは、データベース接続インスタンスから取得します: +リフレクションのオブジェクトはデータベース接続のインスタンスから取り出します。 ```php $reflection = $database->getReflection(); @@ -14,62 +14,64 @@ $reflection = $database->getReflection(); テーブルの取得 ------- -読み取り専用プロパティ `$reflection->tables` には、データベース内のすべてのテーブルの連想配列が含まれています: +読み取り専用のプロパティ `$reflection->tables` には、データベースのすべてのテーブルの連想配列が入っています。 ```php -// すべてのテーブル名の出力 +// すべてのテーブルの名前を並べます foreach ($reflection->tables as $name => $table) { echo $name . "\n"; } ``` -さらに2つのメソッドが利用可能です: +さらに 2 つのメソッドが使えます。 ```php -// テーブルの存在確認 +// テーブルが存在するかを調べます if ($reflection->hasTable('users')) { - echo "テーブル users は存在します"; + echo "Table users exists"; } -// テーブルオブジェクトを返します。存在しない場合は例外をスローします +// テーブルのオブジェクトを返します。存在しなければ例外を投げます $table = $reflection->getTable('users'); ``` -テーブル情報 ------- +テーブルの情報 +------- -テーブルは、以下の読み取り専用プロパティを提供する[Table|api:Nette\Database\Reflection\Table]オブジェクトによって表されます: +テーブルは [Table|api:Nette\Database\Reflection\Table]オブジェクトが表し、次の読み取り専用のプロパティを持ちます。 -- `$name: string` – テーブル名 -- `$view: bool` – ビューであるかどうか -- `$fullName: ?string` – スキーマを含む完全なテーブル名(存在する場合) -- `$columns: array<string, Column>` – テーブルのカラムの連想配列 -- `$indexes: Index[]` – テーブルのインデックスの配列 -- `$primaryKey: ?Index` – テーブルの主キーまたはnull -- `$foreignKeys: ForeignKey[]` – テーブルの外部キーの配列 +- `$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]インスタンスであるカラムの連想配列を提供します: +テーブルの `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` – 特定のデータベースシステムに固有の追加メタデータ +- `$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 "カラム: $name\n"; - echo "型: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'はい' : 'いいえ') . "\n"; + echo "Column: $name\n"; + echo "Type: {$column->nativeType}\n"; + echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; } ``` @@ -77,28 +79,28 @@ foreach ($table->columns as $name => $column) { インデックス ------ -テーブルの`indexes`プロパティは、各インデックスが以下のプロパティを持つ[Index|api:Nette\Database\Reflection\Index]インスタンスであるインデックスの配列を提供します: +テーブルの `indexes` プロパティはインデックスの配列を返します。それぞれのインデックスは次のプロパティを持つ [Index|api:Nette\Database\Reflection\Index]のインスタンスです。 -- `$columns: Column[]` – インデックスを構成するカラムの配列 -- `$unique: bool` – インデックスが一意であるかどうか -- `$primary: bool` – 主キーであるかどうか -- `$name: ?string` – インデックス名 +- `$columns: Column[]` - インデックスを構成する列の配列 +- `$unique: bool` - インデックスが一意かどうか +- `$primary: bool` - 主キーかどうか +- `$name: ?string` - インデックスの名前 -テーブルの主キーは`primaryKey`プロパティを使用して取得でき、これは`Index`オブジェクトまたはテーブルに主キーがない場合は`null`を返します。 +テーブルの主キーは `primaryKey` プロパティで取り出せます。これは `Index` オブジェクトか、テーブルに主キーがなければ `null` を返します。 ```php -// インデックスの出力 +// インデックスを並べます foreach ($table->indexes as $index) { $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "インデックス" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " カラム: $columns\n"; - echo " Unique: " . ($index->unique ? 'はい' : 'いいえ') . "\n"; + 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 "主キー: $columns\n"; + echo "Primary Key: $columns\n"; } ``` @@ -106,15 +108,15 @@ if ($primaryKey = $table->primaryKey) { 外部キー ---- -テーブルの`foreignKeys`プロパティは、各外部キーが以下のプロパティを持つ[ForeignKey|api:Nette\Database\Reflection\ForeignKey]インスタンスである外部キーの配列を提供します: +テーブルの `foreignKeys` プロパティは外部キーの配列を返します。それぞれの外部キーは次のプロパティを持つ [ForeignKey|api:Nette\Database\Reflection\ForeignKey]のインスタンスです。 -- `$foreignTable: Table` – 参照されるテーブル -- `$localColumns: Column[]` – ローカルカラムの配列 -- `$foreignColumns: Column[]` – 参照されるカラムの配列 -- `$name: ?string` – 外部キー名 +- `$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)); diff --git a/database/ja/security.texy b/database/ja/security.texy index cf61e7c60a..72d96480af 100644 --- a/database/ja/security.texy +++ b/database/ja/security.texy @@ -3,36 +3,36 @@ <div class=perex> -データベースには機密データが含まれていることが多く、危険な操作を実行できます。Nette Databaseを安全に使用するためには、以下が重要です: +データベースには機微なデータが入っていることが多く、危険な操作も行えます。Nette Database を安全に使うには、次のことが欠かせません。 -- 安全なAPIと危険なAPIの違いを理解する -- パラメータ化されたクエリを使用する -- 入力データを正しく検証する +- 安全な API と危険な API の違いを理解する +- パラメータ化されたクエリを使う +- 入力データをきちんと検証する </div> -SQLインジェクションとは? -============== +SQL インジェクションとは何か +================ -SQLインジェクションは、データベースを操作する上で最も深刻なセキュリティリスクです。これは、ユーザーからの未処理の入力がSQLクエリの一部になったときに発生します。攻撃者は独自のSQLコマンドを挿入し、それによって: -- データへの不正アクセスを取得する -- データベース内のデータを変更または削除する -- 認証を回避する +SQL インジェクションはデータベースを扱ううえでもっとも深刻なセキュリティリスクです。これは検査されていないユーザーの入力が SQL のクエリの一部になったときに起こります。攻撃者は自分の SQL のコマンドを差し込んで、次のことができてしまいます。 +- データへの許されないアクセスを得る +- データベースのデータを書き換えたり消したりする +- 認証をすり抜ける ```php -// ❌ 危険なコード - SQLインジェクションに対して脆弱 +// ❌ 危険なコード - SQL インジェクションに対して脆弱です $database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); -// 攻撃者は例えば次の値を入力できます: ' OR '1'='1 -// 結果のクエリは次のようになります: SELECT * FROM users WHERE name = '' OR '1'='1' -// これによりすべてのユーザーが返されます +// 攻撃者は次のような値を入れるかもしれません: ' OR '1'='1 +// できあがるクエリは: SELECT * FROM users WHERE name = '' OR '1'='1' +// これはすべてのユーザーを返します ``` -これはDatabase Explorerにも当てはまります: +同じことが Database Explorer にも当てはまります。 ```php -// ❌ 危険なコード - SQLインジェクションに対して脆弱 +// ❌ 危険なコード - SQL インジェクションに対して脆弱です $table->where('name = ' . $_GET['name']); $table->where("name = '$_GET[name]'"); ``` @@ -41,30 +41,30 @@ $table->where("name = '$_GET[name]'"); パラメータ化されたクエリ ============ -SQLインジェクションに対する基本的な防御策は、パラメータ化されたクエリです。Nette Databaseは、それらを使用するためのいくつかの方法を提供します。 +SQL インジェクションに対する基本の守りはパラメータ化されたクエリです。Nette Database はその使い方をいくつか用意しています。 -最も簡単な方法は、**疑問符プレースホルダ**を使用することです: +もっとも単純なのは**疑問符のプレースホルダ**を使うことです。 ```php // ✅ 安全なパラメータ化されたクエリ $database->query('SELECT * FROM users WHERE name = ?', $name); -// ✅ Explorerでの安全な条件 +// ✅ Explorer での安全な条件 $table->where('name = ?', $name); ``` -これは、疑問符プレースホルダとパラメータを含む式を挿入できる[Database Explorer|explorer]の他のすべてのメソッドに適用されます。 +これは、疑問符のプレースホルダとパラメータで式を入れられる [Database Explorer|explorer]のほかのすべてのメソッドにも当てはまります。 -INSERT、UPDATEコマンド、またはWHERE句の場合、値を配列で渡すことができます: +INSERT、UPDATE のコマンドや WHERE 句では、値を配列で渡せます。 ```php -// ✅ 安全なINSERT +// ✅ 安全な INSERT $database->query('INSERT INTO users', [ 'name' => $name, 'email' => $email, ]); -// ✅ Explorerでの安全なINSERT +// ✅ Explorer での安全な INSERT $table->insert([ 'name' => $name, 'email' => $email, @@ -72,114 +72,114 @@ $table->insert([ ``` -パラメータ値の検証 -========= +パラメータの値の検証 +========== -パラメータ化されたクエリは、データベースを安全に操作するための基本的な構成要素です。ただし、それらに挿入する値は、いくつかのレベルのチェックを通過する必要があります: +パラメータ化されたクエリは、安全にデータベースを扱うための土台です。とはいえ、そこに入れる値はいくつかの段階の検査を経なければなりません。 -型チェック ------ +型の検査 +---- -**最も重要なのは、パラメータの正しいデータ型を保証することです** - これはNette Databaseを安全に使用するための必須条件です。データベースは、すべての入力データが特定のカラムに対応する正しいデータ型を持っていることを前提としています。 +**もっとも大事なのはパラメータのデータ型が正しいことを確かめること**です。これは Nette Database を安全に使うために欠かせない条件です。データベースは、入力データがすべてその列に対応する正しいデータ型を持っていると前提にしています。 -たとえば、前の例で `$name` が文字列ではなく予期せず配列であった場合、Nette Databaseはそのすべての要素をSQLクエリに挿入しようとし、エラーが発生します。したがって、**決して** `$_GET`、`$_POST`、または `$_COOKIE` からの未検証のデータをデータベースクエリで直接使用しないでください。 +たとえば前の例の `$name` が思いがけず文字列ではなく配列だったとすると、Nette Database はその要素をすべて SQL のクエリに入れようとして、エラーになります。ですから `$_GET`、`$_POST`、`$_COOKIE` からの検証していないデータを、データベースのクエリで直接**決して使わないでください**。 -フォーマットチェック ----------- +書式の検証 +----- -第2レベルでは、データのフォーマットをチェックします - たとえば、文字列がUTF-8エンコーディングであり、その長さがカラム定義に対応しているか、または数値が特定のカラムデータ型で許可されている範囲内にあるかどうか。 +2 つめの段階ではデータの書式を確かめます。たとえば文字列が UTF-8 の文字コードで、長さが列の定義に合っているか、数値がその列のデータ型で許される範囲に収まっているか、といったことです。 -このレベルの検証では、データベース自体にも部分的に依存できます - 多くのデータベースは無効なデータを拒否します。ただし、動作は異なる場合があり、一部は長い文字列を黙って切り捨てたり、範囲外の数値を切り捨てたりする場合があります。 +この段階の検証はデータベース自身にある程度は任せられます。多くのデータベースは正しくないデータを拒みます。とはいえ振る舞いはさまざまで、長い文字列を黙って切り詰めたり、範囲の外の数を丸めたりするものもあります。 -ドメインチェック --------- +ドメインに固有の検証 +---------- -第3レベルは、アプリケーション固有の論理チェックを表します。たとえば、セレクトボックスの値が提供されたオプションに対応していること、数値が期待される範囲内にあること(例:年齢0〜150歳)、または値間の相互依存関係が意味をなすことの検証。 +3 つめの段階は、あなたのアプリケーションに固有の論理的な検査です。たとえば選択肢の値が提示されたものと一致するか、数が思ったとおりの範囲にあるか(年齢 0〜150 歳など)、値どうしの依存関係が筋の通ったものかを確かめます。 -推奨される検証方法 ---------- +おすすめの検証の方法 +---------- -- すべての入力の正しい検証を自動的に保証する[Nette Forms |forms:]を使用します -- [Presenters |application:]を使用し、`action*()`および`render*()`メソッドのパラメータにデータ型を指定します -- または、`filter_var()`などの標準的なPHPツールを使用して独自の検証層を実装します +- すべての入力の適切な検証を自動的に受け持つ [Nette Forms|forms:]を使います。 +- [プレゼンター|application:]を使い、`action*()` と `render*()` メソッドのパラメータにデータ型を指定します。 +- あるいは `filter_var()` のような PHP の標準の道具で独自の検証の層を作ります。 -カラムの安全な操作 -========= +列を安全に扱う +======= -前のセクションでは、パラメータ値を正しく検証する方法を示しました。ただし、SQLクエリで配列を使用する場合、そのキーにも同じ注意を払う必要があります。 +前の節では、パラメータの値をきちんと検証する方法を見ました。しかし SQL のクエリで配列を使うときは、そのキーにも同じだけ気を配らなければなりません。 ```php -// ❌ 危険なコード - 配列内のキーが処理されていません +// ❌ 危険なコード - 配列のキーが検査されていません $database->query('INSERT INTO users', $_POST); ``` -INSERTおよびUPDATEコマンドの場合、これは重大なセキュリティエラーです - 攻撃者はデータベース内の任意のカラムを挿入または変更できます。たとえば、`is_admin = 1` を設定したり、機密カラムに任意のデータを挿入したりできます(いわゆるマスアサインメント脆弱性)。 +INSERT と UPDATE のコマンドでは、これは致命的なセキュリティの欠陥です。攻撃者はデータベースのどの列でも挿入したり書き換えたりできてしまいます。たとえば `is_admin = 1` を設定したり、機微な列に好きなデータを入れたりできます(いわゆる Mass Assignment Vulnerability)。 -WHERE条件では、演算子を含めることができるため、さらに危険です: +WHERE の条件ではさらに危険です。演算子を含められるからです。 ```php -// ❌ 危険なコード - 配列内のキーが処理されていません +// ❌ 危険なコード - 配列のキーが検査されていません $_POST['salary >'] = 100000; $database->query('SELECT * FROM users WHERE', $_POST); -// クエリ WHERE (`salary` > 100000) を実行します +// クエリ WHERE (`salary` > 100000) が実行されます ``` -攻撃者はこのアプローチを使用して、従業員の給与を体系的に特定できます。たとえば、100,000を超える給与のクエリから始め、次に50,000未満のクエリを行い、範囲を徐々に狭めることで、すべての従業員のおおよその給与を明らかにすることができます。このタイプの攻撃はSQL列挙と呼ばれます。 +攻撃者はこのやり方で従業員の給与を順に探り当てられます。たとえばまず 100,000 より上の給与を問い合わせ、次に 50,000 より下を問い合わせ、範囲を少しずつ狭めていくことで、全員のおおよその給与を暴けます。この種の攻撃を SQL 列挙と呼びます。 -`where()`および`whereOr()`メソッドは、[さらに柔軟 |explorer#where]であり、キーと値に演算子や関数を含むSQL式をサポートしています。これにより、攻撃者はSQLインジェクションを実行できます: +`where()` と `whereOr()` メソッドは[さらにずっと柔軟で |explorer#where()]、キーと値の中で演算子や関数を含む SQL の式に対応しています。これは攻撃者に SQL インジェクションの余地を与えます。 ```php -// ❌ 危険なコード - 攻撃者は独自のSQLを挿入できます +// ❌ 危険なコード - 攻撃者が自分の SQL を差し込めます $_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; $table->where($_POST); -// クエリ WHERE (0) UNION SELECT name, salary FROM users WHERE (1) を実行します +// クエリ WHERE (0) UNION SELECT name, salary FROM users WHERE (1) が実行されます ``` -この攻撃は、`0)`を使用して元の条件を終了し、`UNION`を使用して独自の`SELECT`を追加して`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ではバッククォートを使用): +テーブルや列の名前を動的に扱うには `?name` のプレースホルダを使います。これはそのデータベースの構文に従って識別子をきちんとエスケープします(MySQL ならバッククォートを使います)。 ```php -// ✅ 信頼できる識別子の安全な使用 +// ✅ 信頼できる識別子の安全な使い方 $table = 'users'; $column = 'name'; $database->query('SELECT ?name FROM ?name', $column, $table); -// MySQLでの結果: SELECT `name` FROM `users` +// MySQL での結果: SELECT `name` FROM `users` ``` -重要:シンボル `?name` は、アプリケーションコードで定義された信頼できる値にのみ使用してください。ユーザーからの値には、再び[ホワイトリスト |#カラムのホワイトリスト]を使用してください。そうしないと、セキュリティリスクにさらされます: +大事な点として、`?name` の記号はアプリケーションのコードで定義した信頼できる値にだけ使ってください。ユーザーから来る値には、やはり[ホワイトリスト |#列のホワイトリスト]を使います。さもないとセキュリティリスクにさらされます。 ```php -// ❌ 危険 - ユーザーからの入力は絶対に使用しないでください +// ❌ 危険 - ユーザーの入力を決して使わないでください $database->query('SELECT ?name FROM users', $_GET['column']); ``` diff --git a/database/ja/sql-way.texy b/database/ja/sql-way.texy index 7f2385f573..13bf64ffd6 100644 --- a/database/ja/sql-way.texy +++ b/database/ja/sql-way.texy @@ -1,17 +1,17 @@ -SQLアクセス -******* +SQL アプローチ +********* .[perex] -Nette Databaseは2つの方法を提供します:SQLクエリを自分で記述する(SQLアクセス)、または自動的に生成させる([Explorer |explorer]を参照)。SQLアクセスはクエリを完全に制御でき、同時に安全な構築を保証します。 +Nette Database は 2 つのやり方を用意しています。SQL のクエリを自分で書くか(SQL アプローチ)、自動的に生成させるか([Explorer |explorer]をご覧ください)です。SQL アプローチはクエリを完全に思いどおりにしつつ、それが安全に組み立てられることを保証します。 .[note] -データベース接続と設定の詳細については、[接続と設定 |guide#接続と設定]の章を参照してください。 +データベースへの接続と設定の詳しい話は [接続と設定 |guide#接続と設定]の章にあります。 -基本的なクエリ -======= +基本のクエリ +====== -データベースにクエリを実行するには、`query()`メソッドを使用します。これは、クエリの結果を表す[ResultSet |api:Nette\Database\ResultSet]オブジェクトを返します。失敗した場合、メソッドは[例外をスローします|exceptions]。 クエリの結果は`foreach`ループを使用して反復処理するか、[ヘルパー関数 |#データの取得]のいずれかを使用できます。 +データベースへの問い合わせには `query()` メソッドを使います。これはクエリの結果を表す [ResultSet |api:Nette\Database\ResultSet]オブジェクトを返します。クエリが失敗すると、このメソッドは[例外を投げます|exceptions]。クエリの結果は `foreach` のループで回せますし、[補助のメソッド |#データの取り出し]のどれかを使えます。 ```php $result = $database->query('SELECT * FROM users'); @@ -22,52 +22,52 @@ foreach ($result as $row) { } ``` -SQLクエリに値を安全に挿入するには、パラメータ化されたクエリを使用します。Nette Databaseはこれを最大限に簡単にします - SQLクエリの後にカンマと値を追加するだけです: +SQL のクエリに値を安全に入れるには、パラメータ化されたクエリを使います。Nette Database ではこれがきわめて簡単で、SQL のクエリのうしろにコンマと値を足すだけです。 ```php $database->query('SELECT * FROM users WHERE name = ?', $name); ``` -複数のパラメータがある場合、2つの記述方法があります。SQLクエリにパラメータを「散りばめる」ことができます: +パラメータが複数あるときは 2 通りの書き方があります。SQL のクエリとパラメータを交互に並べられます。 ```php $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); ``` -または、まず完全なSQLクエリを記述し、次にすべてのパラメータを追加します: +あるいは先に SQL のクエリ全体を書いて、そのあとにすべてのパラメータを並べられます。 ```php $database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); ``` -SQLインジェクションからの保護 -================ +SQL インジェクションからの保護 +================= -パラメータ化されたクエリを使用することが重要なのはなぜでしょうか? なぜなら、SQLインジェクションと呼ばれる攻撃から保護してくれるからです。この攻撃では、攻撃者が独自のSQLコマンドを挿入し、それによってデータベース内のデータを取得または破損させる可能性があります。 +なぜパラメータ化されたクエリを使うことが大事なのでしょうか。それは SQL インジェクションと呼ばれる攻撃から守ってくれるからです。この攻撃では、攻撃者が自分の SQL のコマンドを差し込んで、データベースのデータにアクセスしたり壊したりできてしまいます。 .[warning] -**変数をSQLクエリに直接挿入しないでください!** SQLインジェクションから保護するために、常にパラメータ化されたクエリを使用してください。 +**変数を SQL のクエリに直接入れては決していけません。** いつもパラメータ化されたクエリを使ってください。それが SQL インジェクションから守ってくれます。 ```php -// ❌ 危険なコード - SQLインジェクションに対して脆弱 +// ❌ 危険なコード - SQL インジェクションに対して脆弱です $database->query("SELECT * FROM users WHERE name = '$name'"); // ✅ 安全なパラメータ化されたクエリ $database->query('SELECT * FROM users WHERE name = ?', $name); ``` -[潜在的なセキュリティリスクについて理解してください |security]。 +[起こりうるセキュリティリスク |security]も知っておいてください。 -クエリ技術 -===== +クエリの書き方 +======= -WHERE条件 -------- +WHERE の条件 +--------- -WHERE条件は連想配列として記述でき、キーはカラム名、値は比較データです。Nette Databaseは、値の型に基づいて最適なSQL演算子を自動的に選択します。 +`WHERE` の条件は連想配列で書けます。キーは列の名前、値は比較するデータです。Nette Database は値の型をもとに、いちばんふさわしい SQL の演算子を自動的に選びます。 ```php $database->query('SELECT * FROM users WHERE', [ @@ -77,47 +77,47 @@ $database->query('SELECT * FROM users WHERE', [ // 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 を使用 + '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`値や配列などの特殊なケースを自動的に処理します。 +Nette は `null` の値や配列といった特別な場合も自動的に扱います。 ```php $database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // 演算子 = を使用 - 'category_id' => [1, 2, 3], // IN を使用 - 'description' => null, // IS NULL を使用 + '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` を使用します: +否定の条件には `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' => [], // 省略されます + '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 +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -条件を結合するには演算子 `AND` が使用されます。これは[プレースホルダ ?or |#SQL構築のヒント]を使用して変更できます。 +既定では条件は `AND` 演算子でつながれます。これは [?or のプレースホルダ |#SQL の組み立てのヒント]で変えられます。 -ORDER BYルール ------------ +ORDER BY の規則 +------------ -`ORDER BY`ソートは配列を使用して記述できます。キーにカラムを指定し、値は昇順でソートするかどうかを示すブール値になります: +`ORDER BY` の句は配列で書けます。キーに列を指定し、真偽値で昇順(`true`)か降順(`false`)かを示します。 ```php $database->query('SELECT id FROM author ORDER BY', [ @@ -128,10 +128,10 @@ $database->query('SELECT id FROM author ORDER BY', [ ``` -データの挿入 (INSERT) ---------------- +データの挿入(INSERT) +-------------- -レコードを挿入するには、SQLコマンド `INSERT` を使用します。 +レコードの挿入には SQL の `INSERT` コマンドを使います。 ```php $values = [ @@ -142,11 +142,11 @@ $database->query('INSERT INTO users ?', $values); $userId = $database->getInsertId(); ``` -`getInsertId()`メソッドは、最後に挿入された行のIDを返します。一部のデータベース(例:PostgreSQL)では、`$database->getInsertId($sequenceId)`を使用してIDを生成するシーケンス名をパラメータとして指定する必要があります。 +`getInsertId()` メソッドは最後に挿入された行の ID を返します。データベースによっては(PostgreSQL など)、ID を生成するシーケンスの名前をパラメータとして `$database->getInsertId($sequenceId)` のように指定する必要があります。 -パラメータとして、ファイル、DateTimeオブジェクト、または列挙型などの[#特別な値]を渡すこともできます。 +パラメータとしては[特別な値 |#特別な値]、たとえばファイル、DateTime オブジェクト、enum 型も渡せます。 -複数のレコードを一度に挿入する: +複数のレコードを一度に挿入します。 ```php $database->query('INSERT INTO users ?', [ @@ -155,35 +155,35 @@ $database->query('INSERT INTO users ?', [ ]); ``` -複数INSERTは、多くの個別のクエリではなく単一のデータベースクエリが実行されるため、はるかに高速です。 +複数レコードの INSERT はずっと速くなります。多くの個別のクエリの代わりに、データベースのクエリが 1 つだけ実行されるからです。 -**セキュリティ警告:** `$values`として検証されていないデータを使用しないでください。[潜在的なリスクについて理解してください |security#カラムの安全な操作]。 +**セキュリティに関する注意:** 検証していないデータを `$values` として決して使わないでください。[起こりうるリスク |security#列を安全に扱う]を知っておいてください。 -データの更新 (UPDATE) ---------------- +データの更新(UPDATE) +-------------- -レコードを更新するには、SQLコマンド `UPDATE` を使用します。 +レコードの更新には SQL の `UPDATE` コマンドを使います。 ```php -// 1つのレコードの更新 +// ひとつのレコードを更新します $values = [ 'name' => 'John Smith', ]; $result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); ``` -影響を受けた行数は `$result->getRowCount()` で返されます。 +影響を受けた行の数は `$result->getRowCount()` が返します。 -UPDATEには演算子 `+=` および `-=` を使用できます: +`UPDATE` では `+=` と `-=` の演算子を使えます。 ```php $database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // login_count をインクリメント + 'login_count+=' => 1, // login_count を増やします ], 1); ``` -レコードが存在する場合は挿入、存在しない場合は更新する例。`ON DUPLICATE KEY UPDATE`テクニックを使用します: +レコードがすでにあれば更新し、なければ挿入する例です。`ON DUPLICATE KEY UPDATE` の手法を使います。 ```php $values = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // 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構築のヒント]セクションで詳しく説明します。 +Nette Database が、配列のパラメータが SQL のコマンドのどの文脈で使われているかを見分けて、それに応じた SQL のコードを組み立てていることに注目してください。最初の配列からは `(id, name, year) VALUES (123, 'Jim', 1978)` を組み立て、2 つめは `name = 'Jim', year = 1978` の形に変えました。これは [#SQL の組み立てのヒント]の節で詳しく説明します。 -データの削除 (DELETE) ---------------- +データの削除(DELETE) +-------------- -レコードを削除するには、SQLコマンド `DELETE` を使用します。削除された行数を取得する例: +レコードの削除には SQL の `DELETE` コマンドを使います。消された行の数を得る例です。 ```php $count = $database->query('DELETE FROM users WHERE id = ?', 1) @@ -212,32 +212,32 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -SQL構築のヒント ---------- +SQL の組み立てのヒント +------------- -ヒントは、SQLクエリ内の特別なプレースホルダーであり、パラメータ値を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` | テーブルや列の名前を入れるのに使います | - +| `?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ではバッククォートで囲む)。 +`?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の場合) +// SELECT `name` FROM `users` WHERE id = 1 (MySQL の場合) ``` -**警告:** シンボル `?name` は、検証された入力からのテーブル名とカラム名にのみ使用してください。そうしないと、[セキュリティリスクにさらされます |security#動的識別子]。 +**注意:** `?name` のプレースホルダは検証済みのテーブル名と列名にだけ使ってください。さもないと[セキュリティ上の弱点 |security#動的な識別子]を招きます。 -他のヒントは通常、NetteがSQLクエリを構築する際に賢い自動検出を使用するため(表の3番目の列を参照)、指定する必要はありません。ただし、たとえば `AND` の代わりに `OR` を使用して条件を結合したい場合などに使用できます: +ほかのヒントはふつう指定する必要はありません。Nette は SQL のクエリを組み立てるときに賢く自動判別するからです(表の 3 列めをご覧ください)。とはいえ、たとえば条件を `AND` ではなく `OR` でつなぎたい場面で使えます。 ```php $database->query('SELECT * FROM users WHERE ?or', [ @@ -251,29 +251,29 @@ $database->query('SELECT * FROM users WHERE ?or', [ 特別な値 ---- -通常のスカラ型(string、int、bool)に加えて、パラメータとして特別な値を渡すこともできます: +よくあるスカラーの型(string、int、bool)のほかに、パラメータとして特別な値も渡せます。 -- ファイル:`fopen('image.gif', 'r')` はファイルのバイナリコンテンツを挿入します -- 日付と時刻:`DateTime`オブジェクトはデータベース形式に変換されます -- 列挙型:`enum`インスタンスはその値に変換されます -- SQLリテラル:`Connection::literal('NOW()')`を使用して作成されたものは、クエリに直接挿入されます +- ファイル: `fopen('image.gif', 'r')` はファイルの中身をバイナリとして入れます +- 日付と時刻: `DateTimeInterface` のオブジェクトはデータベースの書式に変換されます +- enum 型: `enum` のインスタンスはその値に変換されます +- SQL のリテラル: `Connection::literal('NOW()')` で作られ、そのままクエリに入ります ```php $database->query('INSERT INTO articles ?', [ 'title' => 'My Article', - 'published_at' => new DateTime, + 'published_at' => new DateTimeImmutable, // または new DateTime 'content' => fopen('image.png', 'r'), 'state' => Status::Draft, ]); ``` -`datetime`データ型をネイティブにサポートしていないデータベース(SQLiteやOracleなど)の場合、`DateTime`は[データベース設定|configuration]の`formatDateTime`項目で指定された値(デフォルト値は`U` - Unixタイムスタンプ)に変換されます。 +`datetime` のデータ型を本来は持たないデータベース(SQLite や Oracle など)では、`DateTime` と `DateTimeImmutable` のオブジェクトは、[データベースの設定|configuration]の `formatDateTime` の項目で指定された値に変換されます(既定値は `U`、つまり Unix タイムスタンプです)。 -SQLリテラル -------- +SQL のリテラル +--------- -場合によっては、値として直接SQLコードを指定する必要がありますが、これは文字列として解釈されず、エスケープされるべきではありません。この目的のために、`Nette\Database\SqlLiteral`クラスのオブジェクトが使用されます。これらは`Connection::literal()`メソッドによって作成されます。 +生の SQL のコードを値として渡し、文字列として扱われたりエスケープされたりしないようにしたい場合があります。そのために `Nette\Database\SqlLiteral` クラスのオブジェクトを使います。これは `Connection::literal()` メソッドで作ります。 ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -または代替案: +あるいは次のようにも書けます。 ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -293,7 +293,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) ``` -SQLリテラルにはパラメータを含めることができます: +SQL のリテラルはパラメータを含められます。 ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -これにより、興味深い組み合わせを作成できます: +これで面白い組み合わせが作れます。 ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -317,22 +317,22 @@ $result = $database->query('SELECT * FROM users WHERE', [ ``` -データの取得 -====== +データの取り出し +======== -SELECTクエリのショートカット ------------------ +SELECT のクエリの近道 +-------------- -データ取得を簡略化するために、`Connection`は`query()`の呼び出しとそれに続く`fetch*()`を組み合わせたいくつかのショートカットを提供します。これらのメソッドは`query()`と同じパラメータ、つまりSQLクエリとオプションのパラメータを受け入れます。`fetch*()`メソッドの完全な説明は[以下 |#fetch]にあります。 +データの取り出しを簡単にするために、`Connection` は `query()` の呼び出しと、そのあとの `fetch*()` の呼び出しをひとつにまとめた近道をいくつか用意しています。これらのメソッドは `query()` と同じパラメータ、つまり SQL のクエリと省略できるパラメータを受け取ります。`fetch*()` メソッドの詳しい説明は[下 |#fetch()]にあります。 -| `fetch($sql, ...$params): ?Row` | クエリを実行し、最初の行を`Row`オブジェクトとして返します -| `fetchAll($sql, ...$params): array` | クエリを実行し、すべての行を`Row`オブジェクトの配列として返します -| `fetchPairs($sql, ...$params): array` | クエリを実行し、最初のカラムがキー、2番目のカラムが値である連想配列を返します -| `fetchField($sql, ...$params): mixed` | クエリを実行し、最初の行の最初のフィールドの値を返します -| `fetchList($sql, ...$params): ?array` | クエリを実行し、最初の行をインデックス付き配列として返します +| `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() - 最初のセルの値を返します @@ -341,10 +341,10 @@ $count = $database->query('SELECT COUNT(*) FROM articles') ``` -`foreach` - 行の反復処理 +`foreach` - 行を順に回す ------------------ -クエリを実行した後、[ResultSet|api:Nette\Database\ResultSet]オブジェクトが返され、これにより結果をいくつかの方法で反復処理できます。クエリを実行して行を取得する最も簡単な方法は、`foreach`ループで反復処理することです。この方法は、データを段階的に返し、一度にメモリに保存しないため、メモリ効率が最も高くなります。 +クエリを実行すると [ResultSet|api:Nette\Database\ResultSet]オブジェクトが返され、結果をいくつかの方法で回せます。クエリを実行して行を取り出すいちばん簡単な方法は、`foreach` のループで回すことです。この方法はメモリをもっとも節約します。データを 1 行ずつ取り出し、結果全体を一度にメモリへ読み込まないからです。 ```php $result = $database->query('SELECT * FROM users'); @@ -357,13 +357,13 @@ foreach ($result as $row) { ``` .[note] -`ResultSet`は一度しか反復処理できません。繰り返し反復処理する必要がある場合は、まず`fetchAll()`メソッドなどを使用してデータを配列に読み込む必要があります。 +`ResultSet` は一度しか回せません。何度も回す必要があるなら、まず `fetchAll()` メソッドなどでデータを配列に読み込まなければなりません。 fetch(): ?Row .[method] ----------------------- -行を`Row`オブジェクトとして返します。これ以上行がない場合は`null`を返します。内部ポインタを次の行に進めます。 +行を `Row` オブジェクトとして返します。行がもうなければ `null` を返します。内部のポインタを次の行へ進めます。 ```php $result = $database->query('SELECT * FROM users'); @@ -377,7 +377,7 @@ if ($row) { fetchAll(): array .[method] --------------------------- -`ResultSet`から残りのすべての行を`Row`オブジェクトの配列として返します。 +`ResultSet` に残っているすべての行を `Row` オブジェクトの配列として返します。 ```php $result = $database->query('SELECT * FROM users'); @@ -391,7 +391,7 @@ foreach ($rows as $row) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -結果を連想配列として返します。最初の引数は配列のキーとして使用されるカラム名を指定し、2番目の引数は値として使用されるカラム名を指定します: +結果を連想配列として返します。第 1 引数はキーとして使う列、第 2 引数は値として使う列を指定します。 ```php $result = $database->query('SELECT id, name FROM users'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -最初のパラメータのみを指定した場合、値は行全体、つまり`Row`オブジェクトになります: +第 1 パラメータ(`$key`)だけを渡すと、行全体(`Row` オブジェクト)が値として使われます。 ```php $rows = $result->fetchPairs('id'); // [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] ``` -キーが重複する場合、最後の行の値が使用されます。キーとして`null`を使用すると、配列はゼロから数値でインデックス付けされます(衝突は発生しません): +キーが重なった場合は最後の行の値が使われます。キーに `null` を使うと、ゼロから始まる添字の配列になり、キーの衝突が起きません。 ```php $names = $result->fetchPairs(null, 'name'); @@ -417,14 +417,14 @@ $names = $result->fetchPairs(null, 'name'); 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, ...] ``` @@ -433,18 +433,18 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -現在の行の最初のフィールドの値を返します。これ以上行がない場合は`null`を返します。内部ポインタを次の行に進めます。 +今の行の最初の列の値を返します。行がもうなければ `null` を返します。内部のポインタを次の行へ進めます。 ```php $result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // 最初の行から名前を読み込みます +$name = $result->fetchField(); // 最初の行から name を読み込みます ``` fetchList(): ?array .[method] ----------------------------- -行をインデックス付き配列として返します。これ以上行がない場合は`null`を返します。内部ポインタを次の行に進めます。 +行を添字の配列として返します。行がもうなければ `null` を返します。内部のポインタを次の行へ進めます。 ```php $result = $database->query('SELECT name, email FROM users'); @@ -455,51 +455,51 @@ $row = $result->fetchList(); // ['John', 'john@example.com'] getRowCount(): ?int .[method] ----------------------------- -最後の`UPDATE`または`DELETE`クエリによって影響を受けた行数を返します。`SELECT`の場合、これは返された行数ですが、これは不明な場合があり、その場合メソッドは`null`を返します。 +直前の `UPDATE` または `DELETE` のクエリで影響を受けた行の数を返します。`SELECT` のクエリでは結果の行の数を返します。ただしこれは常に分かるとは限らず、その場合このメソッドは `null` を返します。 getColumnCount(): ?int .[method] -------------------------------- -`ResultSet`内のカラム数を返します。 +`ResultSet` の列の数を返します。 -クエリ情報 -===== +クエリの情報 +====== -デバッグ目的で、最後に実行されたクエリに関する情報を取得できます: +デバッグのために、最後に実行されたクエリの情報を取り出せます。 ```php -echo $database->getLastQueryString(); // SQLクエリを出力します +echo $database->getLastQueryString(); // SQL のクエリを出力します $result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // SQLクエリを出力します -echo $result->getTime(); // 実行時間を秒単位で出力します +echo $result->getQueryString(); // SQL のクエリを出力します +echo $result->getTime(); // 実行にかかった時間を秒で出力します ``` -結果をHTMLテーブルとして表示するには、次を使用できます: +結果を HTML の表として表示するには次のようにします。 ```php $result = $database->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSetはカラムの型に関する情報を提供します: +`ResultSet` は列の型の情報も提供します。 ```php $result = $database->query('SELECT * FROM articles'); $types = $result->getColumnTypes(); foreach ($types as $column => $type) { - echo "$column は型 $type->type です"; // 例:'id は型 int です' + echo "$column is of type $type"; // たとえば 'id is of type int' } ``` -クエリのロギング --------- +クエリのログ +------ -独自のクエリロギングを実装できます。イベント`onQuery`は、実行された各クエリの後に呼び出されるコールバックの配列です: +クエリのログを独自に作れます。`onQuery` イベントは、実行されたクエリごとに呼ばれるコールバックの配列です。 ```php $database->onQuery[] = function ($database, $result) use ($logger) { diff --git a/database/ja/transactions.texy b/database/ja/transactions.texy index b30405b524..d74fc82da4 100644 --- a/database/ja/transactions.texy +++ b/database/ja/transactions.texy @@ -2,9 +2,9 @@ ******** .[perex] -トランザクションは、トランザクション内のすべての操作が実行されるか、または何も実行されないかのいずれかを保証します。これらは、より複雑な操作でデータの整合性を確保するのに役立ちます。 +トランザクションは、その中のすべての操作が実行されるか、まったく実行されないかのどちらかであることを保証します。込み入った操作でデータの整合性を守るのに役立ちます。 -トランザクションを使用する最も簡単な方法は次のようになります: +トランザクションのもっとも単純な使い方は次のようになります。 ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -`transaction()` メソッドを使用すると、同じことをはるかにエレガントに記述できます。パラメータとしてコールバックを受け取り、それをトランザクション内で実行します。コールバックが例外なく実行されると、トランザクションは自動的にコミットされます。例外が発生した場合、トランザクションはキャンセル(ロールバック)され、例外はさらに伝播されます。 +同じことを `transaction()` メソッドでずっと優雅にできます。これはトランザクションの中で実行されるコールバックを受け取ります。コールバックが例外を投げずに終われば、トランザクションは自動的にコミットされます。例外が起きればトランザクションはロールバックされ、例外はさらに上へ伝わります。 ```php $database->transaction(function ($database) use ($id) { @@ -33,11 +33,13 @@ $database->transaction(function ($database) use ($id) { }); ``` -`transaction()` メソッドは値を返すこともできます: +`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(); // 更新された行数を返します + return $result->getRowCount(); // 更新された行の数を返します }); ``` diff --git a/database/ja/type-conversion.texy b/database/ja/type-conversion.texy new file mode 100644 index 0000000000..bafc96e51e --- /dev/null +++ b/database/ja/type-conversion.texy @@ -0,0 +1,55 @@ +型変換 +*** + +.[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 new file mode 100644 index 0000000000..9372b3ff3f --- /dev/null +++ b/database/ja/upgrading.texy @@ -0,0 +1,36 @@ +アップグレード +******* + + +バージョン 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 index 248e88956e..3a8408cd13 100644 --- a/database/meta.json +++ b/database/meta.json @@ -1,5 +1,6 @@ { - "version": "4.0", + "version": "4.x", "repo": "nette/database", - "composer": "nette/database" + "composer": "nette/database", + "api": "https://api.nette.org/database/" } diff --git a/database/pl/@home.texy b/database/pl/@home.texy index 4093fa6893..bdb1bffaee 100644 --- a/database/pl/@home.texy +++ b/database/pl/@home.texy @@ -1,21 +1,18 @@ - - Obsługiwane bazy danych ======================= -Nette obsługuje następujące bazy danych: - -|* Serwer bazy danych |* Nazwa DSN |* Obsługa w Core |* Obsługa w 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 | - +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 - awesome database layer for PHP}} -{{description: Nette Database w znaczący sposób upraszcza pobieranie danych z bazy danych bez konieczności pisania zapytań SQL. Składa efektywne zapytania i nie przesyła zbędnych danych.}} +{{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 index 635d04210c..0469ba34f8 100644 --- a/database/pl/@left-menu.texy +++ b/database/pl/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** -- [Wprowadzenie |guide] -- [Podejście SQL |sql way] -- [Explorer |Explorer] -- [Transakcje |transactions] -- [Wyjątki |exceptions] -- [Refleksja |reflection] -- [Mapowanie |mapping] -- [Konfiguracja |configuration] +- [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 |en:upgrading] +- [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/configuration.texy b/database/pl/configuration.texy index 3ab8140579..ed4eff1875 100644 --- a/database/pl/configuration.texy +++ b/database/pl/configuration.texy @@ -4,7 +4,7 @@ Konfiguracja bazy danych .[perex] Przegląd opcji konfiguracyjnych dla Nette Database. -Jeśli nie używasz całego frameworka, ale tylko tej biblioteki, przeczytaj, [jak wczytać konfigurację|bootstrap:]. +Jeśli nie używasz całego frameworku, tylko tej biblioteki, przeczytaj, [jak wczytać konfigurację|bootstrap:]. Jedno połączenie @@ -14,35 +14,35 @@ Konfiguracja jednego połączenia z bazą danych: ```neon database: - # DSN, jedyny wymagany klucz + # DSN, jedyny obowiązkowy klucz dsn: "sqlite:%appDir%/Model/demo.db" user: ... password: ... ``` -Tworzy usługi `Nette\Database\Connection` i `Nette\Database\Explorer`, które zazwyczaj przekazujemy przez [autowiring |dependency-injection:autowiring], ewentualnie przez odwołanie do [ich nazwy |#Usługi DI]. +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]. -Dalsze ustawienia: +Pozostałe ustawienia: ```neon database: - # wyświetlić panel bazy danych w Tracy Bar? - debugger: ... # (bool) domyślnie true + # pokazać panel bazy danych w Tracy Bar? + debugger: ... # (bool) domyślnie włączone, jeśli Tracy jest aktywne - # wyświetlić EXPLAIN zapytań w Tracy Bar? + # 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 lub nazwa klasy + # konwencje tabel: discovered, static albo nazwa klasy conventions: discovered # (string) domyślnie 'discovered' options: - # łączyć się z bazą danych dopiero w razie potrzeby? + # łączyć się z bazą danych dopiero wtedy, gdy jest potrzebna? lazy: ... # (bool) domyślnie false - # klasa PHP sterownika bazy danych + # klasa sterownika bazy danych w PHP driverClass: # (string) # tylko MySQL: ustawia sql_mode @@ -52,16 +52,16 @@ database: charset: # (string) domyślnie 'utf8mb4' # tylko MySQL: konwertuje TINYINT(1) na bool - convertBoolean: # (bool) domyślnie false + convertBoolean: # (bool) domyślnie false - # zwraca kolumny z datą jako obiekty immutable (od wersji 3.2.1) + # zwraca kolumny dat jako obiekty niezmienne (od wersji 3.2.1) newDateTime: # (bool) domyślnie false - # tylko Oracle i SQLite: format przechowywania daty + # tylko Oracle i SQLite: format zapisu daty formatDateTime: # (string) domyślnie 'U' ``` -W kluczu `options` można podawać inne opcje, które znajdziesz w [dokumentacji sterowników PDO |https://www.php.net/manual/en/pdo.drivers.php], takie jak: +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: @@ -73,7 +73,7 @@ database: Wiele połączeń -------------- -W konfiguracji możemy zdefiniować również wiele połączeń z bazą danych, dzieląc je na nazwane sekcje: +W konfiguracji możemy zdefiniować wiele połączeń z bazą danych, dzieląc je na nazwane sekcje: ```neon database: @@ -86,23 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -Autowiring jest włączony tylko dla usług z pierwszej sekcji. Można to zmienić za pomocą `autowired: false` lub `autowired: true`. +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 --------- -Te usługi są dodawane do kontenera DI, gdzie `###` reprezentuje nazwę połączenia: +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] +| 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 definiujemy tylko jedno połączenie, nazwy usług będą `database.default.connection` i `database.default.explorer`. Jeśli definiujemy więcej połączeń jak w przykładzie powyżej, nazwy będą odpowiadać sekcjom, tj. `database.main.connection`, `database.main.explorer` oraz `database.another.connection` i `database.another.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 bez autowiringu przekazujemy jawnie przez odwołanie do ich nazwy: +Usługi spoza autowiringu przekazujemy jawnie, odwołując się do ich nazwy: ```neon services: diff --git a/database/pl/exceptions.texy b/database/pl/exceptions.texy index 0883717760..3d63166710 100644 --- a/database/pl/exceptions.texy +++ b/database/pl/exceptions.texy @@ -1,22 +1,25 @@ Wyjątki ******* -Nette Database używa hierarchii wyjątków. Podstawową klasą jest `Nette\Database\DriverException`, która dziedziczy z `PDOException` i zapewnia rozszerzone możliwości pracy z błędami bazy danych: +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 od sterownika bazy danych -- Metoda `getSqlState()` zwraca kod SQLSTATE -- Metody `getQueryString()` i `getParameters()` umożliwiają uzyskanie pierwotnego zapytania i jego parametrów +- 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. -Z `DriverException` dziedziczą następujące wyspecjalizowane wyjątki: +Klasę `DriverException` rozszerzają następujące wyspecjalizowane wyjątki: -- `ConnectionException` - sygnalizuje niepowodzenie połączenia z serwerem bazy danych -- `ConstraintViolationException` - podstawowa klasa dla naruszenia ograniczeń bazy danych, z której dziedziczą: - - `ForeignKeyConstraintViolationException` - naruszenie klucza obcego - - `NotNullConstraintViolationException` - naruszenie ograniczenia NOT NULL - - `UniqueConstraintViolationException` - naruszenie unikalności wartości +- `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. - -Przykład przechwytywania wyjątku `UniqueConstraintViolationException`, który występuje, gdy próbujemy wstawić użytkownika z adresem e-mail, który już istnieje w bazie danych (zakładając, że kolumna email ma unikalny indeks). +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 { @@ -26,9 +29,9 @@ try { 'password' => $hashedPassword, ]); } catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Użytkownik o tym adresie e-mail już istnieje.'; + echo 'Użytkownik z tym e-mailem już istnieje.'; } catch (Nette\Database\DriverException $e) { - echo 'Wystąpił błąd podczas rejestracji: ' . $e->getMessage(); + echo 'Podczas rejestracji wystąpił błąd: ' . $e->getMessage(); } ``` diff --git a/database/pl/explorer.texy b/database/pl/explorer.texy index 5541a0e211..e2960c972e 100644 --- a/database/pl/explorer.texy +++ b/database/pl/explorer.texy @@ -3,32 +3,32 @@ Database Explorer <div class=perex> -Explorer oferuje intuicyjny i efektywny sposób pracy z bazą danych. Dba automatycznie o relacje między tabelami i optymalizację zapytań, dzięki czemu możesz skupić się na swojej aplikacji. Działa od razu bez konieczności ustawiania. Jeśli potrzebujesz pełnej kontroli nad zapytaniami SQL, możesz wykorzystać [dostęp SQL |SQL way]. +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, które pobierają tylko potrzebne dane -- Umożliwia łatwy dostęp do powiązanych danych bez konieczności pisania zapytań JOIN -- Działa natychmiast bez jakiejkolwiek konfiguracji czy generowania encji +- 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> -Z Explorerem zaczniesz od wywołania metody `table()` obiektu [api:Nette\Database\Explorer] (szczegóły dotyczące połączenia znajdziesz w rozdziale [Połączenie i konfiguracja |guide#Połączenie i konfiguracja]): +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żemy dołączać kolejne metody do filtrowania i sortowania wyników. Zapytanie jest budowane i uruchamiane dopiero w momencie, gdy zaczynamy żądać danych. Na przykład przez przechodzenie pętlą `foreach`. Każdy wiersz jest reprezentowany przez obiekt [ActiveRow |api:Nette\Database\Table\ActiveRow]: +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; // wypisanie kolumny 'title' - echo $book->author_id; // wypisanie kolumny 'author_id' + echo $book->title; // wypisuje kolumnę 'title' + echo $book->author_id; // wypisuje kolumnę 'author_id' } ``` -Explorer zasadniczo ułatwia pracę z [relacjami między tabelami |#Relacje między tabelami]. Poniższy przykład pokazuje, jak łatwo możemy wypisać dane z powiązanych tabel (książki i ich autorzy). Zauważ, że nie musimy pisać żadnych zapytań JOIN, Nette stworzy je za nas: +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'); @@ -39,45 +39,45 @@ foreach ($books as $book) { } ``` -Nette Database Explorer optymalizuje zapytania, aby były jak najbardziej efektywne. Powyższy przykład wykona tylko dwa zapytania SELECT, niezależnie od tego, czy przetwarzamy 10 czy 10 000 książek. +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. -Dodatkowo Explorer śledzi, które kolumny są używane w kodzie, i pobiera z bazy danych tylko te, oszczędzając tym samym dodatkową wydajność. To zachowanie jest w pełni automatyczne i adaptacyjne. Jeśli później zmodyfikujesz kod i zaczniesz używać innych kolumn, Explorer automatycznie dostosuje zapytania. Nie musisz niczego ustawiać ani zastanawiać się, które kolumny będziesz potrzebować - zostaw to Nette. +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` dostarcza metody do filtrowania i sortowania wyboru danych. +Klasa `Selection` udostępnia metody do filtrowania i sortowania wyboru danych. .[language-php] -| `where($condition, ...$params)` | Dodaje warunek WHERE. Wiele warunków jest łączonych operatorem AND -| `whereOr(array $conditions)` | Dodaje grupę warunków WHERE połączonych operatorem OR -| `wherePrimary($value)` | Dodaje warunek WHERE według klucza podstawowego -| `order($columns, ...$params)` | Ustawia sortowanie ORDER BY -| `select($columns, ...$params)` | Określa kolumny, które mają zostać załadowane -| `limit($limit, $offset = null)` | Ogranicza liczbę wierszy (LIMIT) i opcjonalnie ustawia OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Ustawia paginację -| `group($columns, ...$params)` | Grupuje wiersze (GROUP BY) -| `having($condition, ...$params)` | Dodaje warunek HAVING do filtrowania zgrupowanych wierszy +| `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ć łańcuchowo (tzw. [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. +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 również używać specjalnej notacji do dostępu do [danych z powiązanych tabel |#Zapytania przez powiązane tabele]. +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 w ten sposób SQL injection. Dla poprawnego działania konieczne jest przestrzeganie kilku zasad: +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 itp. pisz **wielkimi literami**. +- Słowa kluczowe, nazwy funkcji, procedur itd. pisz **wielkimi literami**. - Nazwy kolumn i tabel pisz **małymi literami**. -- Ciągi znaków zawsze wstawiaj przez **parametry**. +- Ciągi zawsze przekazuj przez **parametry**. ```php -where('name = ' . $name); // KRYTYCZNA PODATNOŚĆ: SQL injection +where('name = ' . $name); // KRYTYCZNA LUKA: SQL injection where('name LIKE "%search%"'); // ŹLE: komplikuje automatyczne cytowanie -where('name LIKE ?', '%search%'); // POPRAWNIE: wartość wstawiona przez parametr +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 ? @@ -88,7 +88,7 @@ where('LOWER(name) = ?', $value);// POPRAWNIE: LOWER(`name`) = ? where(string|array $condition, ...$parameters): static .[method] ---------------------------------------------------------------- -Filtruje wyniki za pomocą warunków WHERE. Jej mocną stroną jest inteligentna praca z różnymi typami wartości i automatyczny wybór operatorów SQL. +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: @@ -98,26 +98,26 @@ $table->where('id > ?', $value); // WHERE `id` > 123 $table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' ``` -Dzięki automatycznej detekcji odpowiednich operatorów nie musimy zajmować się różnymi specjalnymi przypadkami. Nette rozwiąże je za nas: +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żna również użyć znaku zapytania bez operatora: +// Możesz też użyć zastępnika ? bez operatora: $table->where('id ?', 1); // WHERE `id` = 1 ``` -Metoda poprawnie przetwarza również warunki negatywne i puste tablice: +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 obsługiwana +// $table->where('NOT id ?', $ids); // UWAGA: ta składnia nie jest wspierana ``` -Jako parametr możemy przekazać również wynik z innej tabeli - utworzy się podzapytanie: +Jako parametr możesz przekazać także wynik zapytania z innej tabeli, tworząc podzapytanie: ```php // WHERE `id` IN (SELECT `id` FROM `tableName`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -Warunki możemy przekazać również jako tablicę, której elementy zostaną połączone za pomocą AND: +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`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -W tablicy możemy użyć par klucz => wartość, a Nette ponownie automatycznie wybierze odpowiednie operatory: +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)) @@ -147,7 +147,7 @@ $table->where([ ]); ``` -W tablicy możemy łączyć wyrażenia SQL ze znakami zapytania i wieloma parametrami. Jest to odpowiednie dla złożonych warunków z precyzyjnie zdefiniowanymi operatorami: +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) @@ -157,13 +157,13 @@ $table->where([ ]); ``` -Wielokrotne wywołanie `where()` automatycznie łączy warunki za pomocą AND. +Wielokrotne wywołania `where()` automatycznie łączą warunki operatorem AND. whereOr(array $parameters): static .[method] -------------------------------------------- -Podobnie jak `where()` dodaje warunki, ale z tą różnicą, że łączy je za pomocą OR: +Podobnie jak `where()` dodaje warunki, ale łączy je operatorem OR: ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -Również tutaj możemy użyć bardziej złożonych wyrażeń: +Można tu również używać bardziej złożonych wyrażeń: ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -187,7 +187,7 @@ $table->whereOr([ wherePrimary(mixed $key): static .[method] ------------------------------------------ -Dodaje warunek dla klucza podstawowego tabeli: +Dodaje warunek na klucz główny tabeli: ```php // WHERE `id` = 123 @@ -197,7 +197,7 @@ $table->wherePrimary(123); $table->wherePrimary([1, 2, 3]); ``` -Jeśli tabela ma złożony klucz podstawowy (np. `foo_id`, `bar_id`), przekażemy go jako tablicę: +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 @@ -214,7 +214,7 @@ $table->wherePrimary([ order(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Określa kolejność, w jakiej będą zwracane wiersze. Możemy sortować według jednej lub więcej kolumn, w porządku malejącym lub rosnącym, lub według własnego wyrażenia: +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` @@ -227,14 +227,14 @@ $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ą rzeczywiście używane w kodzie. Metodę `select()` używamy więc w przypadkach, gdy potrzebujemy zwrócić specyficzne wyrażenia: +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 za pomocą `AS` są następnie dostępne jako właściwości obiektu ActiveRow: +Aliasy zdefiniowane przez `AS` są potem dostępne jako właściwości obiektu `ActiveRow`: ```php foreach ($table as $row) { @@ -253,17 +253,17 @@ $table->limit(10); // LIMIT 10 (zwraca pierwsze 10 wierszy) $table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` -Do paginacji bardziej odpowiednie jest użycie metody `page()`. +Do stronicowania bardziej odpowiednia jest metoda `page()`. page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] ------------------------------------------------------------------------- -Ułatwia paginację wyników. Przyjmuje numer strony (liczony od 1) i liczbę elementów na stronę. Opcjonalnie można przekazać referencję do zmiennej, do której zostanie zapisana całkowita liczba stron: +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); +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); echo "Łącznie stron: $numOfPages"; ``` @@ -271,10 +271,10 @@ echo "Łącznie stron: $numOfPages"; group(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Grupuje wiersze według podanych kolumn (GROUP BY). Używa się jej zazwyczaj w połączeniu z funkcjami agregującymi: +Grupuje wiersze według podanych kolumn (GROUP BY). Używa się jej zwykle w połączeniu z funkcjami agregującymi: ```php -// Oblicza liczbę produktów w każdej kategorii +// Liczy liczbę produktów w każdej kategorii $table->select('category_id, COUNT(*) AS count') ->group('category_id'); ``` @@ -283,7 +283,7 @@ $table->select('category_id, COUNT(*) AS count') having(string $having, ...$parameters): static .[method] -------------------------------------------------------- -Ustawia warunek do filtrowania zgrupowanych wierszy (HAVING). Można ją użyć w połączeniu z metodą `group()` i funkcjami agregującymi: +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 @@ -296,28 +296,28 @@ $table->select('category_id, COUNT(*) AS count') Odczyt danych ============= -Do odczytu danych z bazy danych mamy do dyspozycji kilka użytecznych metod: +Do odczytu danych z bazy dostępnych jest kilka przydatnych metod: .[language-php] -| `foreach ($table as $key => $row)` | Iteruje po wszystkich wierszach, `$key` to wartość klucza podstawowego, `$row` to obiekt ActiveRow -| `$row = $table->get($key)` | Zwraca jeden wiersz według klucza podstawowego -| `$row = $table->fetch()` | Zwraca bieżący wiersz i przesuwa wskaźnik na następny -| `$array = $table->fetchPairs()` | Tworzy tablicę asocjacyjną z wyników -| `$array = $table->fetchAll()` | Zwraca wszystkie wiersze jako tablicę -| `count($table)` | Zwraca liczbę wierszy w obiekcie Selection +| `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 przeznaczony tylko do odczytu. Oznacza to, że nie można zmieniać wartości jego właściwości. To ograniczenie zapewnia spójność danych i zapobiega nieoczekiwanym efektom ubocznym. Dane są ładowane z bazy danych, a jakakolwiek zmiana powinna być przeprowadzona jawnie i kontrolowanie. +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` - iteracja po wszystkich wierszach --------------------------------------------- +`foreach` - iterowanie przez wszystkie wiersze +---------------------------------------------- -Najprostszy sposób na wykonanie zapytania i uzyskanie wierszy to iteracja w pętli `foreach`. Automatycznie uruchamia zapytanie SQL. +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 podstawowego, $book to ActiveRow + // $key to wartość klucza głównego, $book to ActiveRow echo "$book->title ({$book->author->name})"; } ``` @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -Wykonuje zapytanie SQL i zwraca wiersz według klucza podstawowego, lub `null`, jeśli nie istnieje. +Wykonuje zapytanie SQL i zwraca wiersz według klucza głównego albo `null`, jeśli nie istnieje. ```php -$book = $explorer->table('book')->get(123); // zwróci ActiveRow o ID 123 lub null +$book = $explorer->table('book')->get(123); // zwraca ActiveRow o ID 123 albo null if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -Zwraca wiersz i przesuwa wewnętrzny wskaźnik na następny. Jeśli nie ma już kolejnych wierszy, zwraca `null`. +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'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Zwraca wyniki jako tablicę asocjacyjną. Pierwszy argument określa nazwę kolumny, która zostanie użyta jako klucz w tablicy, drugi argument określa nazwę kolumny, która zostanie użyta jako wartość: +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 podamy tylko pierwszy parametr, wartością będzie cały wiersz, czyli obiekt `ActiveRow`: +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 duplikujących się kluczy użyta zostanie wartość z ostatniego wiersza. Przy użyciu `null` jako klucza tablica będzie indeksowana numerycznie od zera (wtedy do kolizji nie dochodzi): +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'); @@ -377,24 +377,24 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternatywnie możesz jako parametr podać callback, który dla każdego wiersza będzie zwracał albo samą wartość, albo parę klucz-wartość. +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})"); -// ['Pierwsza książka (Jan Nowak)', ...] +// ['First Book (John Novak)', ...] -// Callback może również zwracać tablicę z parą klucz & wartość: +// 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]); -// ['Pierwsza książka' => 'Jan Nowak', ...] +// ['First Book' => 'John Novak', ...] ``` fetchAll(): array .[method] --------------------------- -Zwraca wszystkie wiersze jako tablicę asocjacyjną obiektów `ActiveRow`, gdzie klucze są wartościami kluczy podstawowych. +Zwraca wszystkie wiersze jako tablicę asocjacyjną obiektów `ActiveRow`, gdzie kluczami są wartości kluczy głównych. ```php $allBooks = $explorer->table('book')->fetchAll(); @@ -413,13 +413,13 @@ $count = $table->count(); $count = count($table); // alternatywa ``` -Uwaga, `count()` z parametrem wykonuje funkcję agregującą COUNT w bazie danych, zobacz poniżej. +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 klucze są nazwami kolumn, a wartości odpowiadającymi danymi. +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); @@ -431,33 +431,33 @@ $bookArray = $book->toArray(); Agregacja ========= -Klasa `Selection` dostarcza metody do łatwego wykonywania funkcji agregujących (COUNT, SUM, MIN, MAX, AVG itd.). +Klasa `Selection` udostępnia metody do łatwego wykonywania funkcji agregujących (COUNT, SUM, MIN, MAX, AVG itd.). .[language-php] -| `count($expr)` | Oblicza 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)` | Umożliwia wykonanie dowolnej funkcji agregującej. Np. `AVG()`, `GROUP_CONCAT()` +| `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. Metoda jest używana do sprawdzenia, ile wierszy odpowiada określonemu warunkowi: +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 tylko zwraca liczbę wierszy w obiekcie `Selection`. +Uwaga: [#count()] bez parametru zwraca tylko liczbę wierszy w obiekcie `Selection`. -min(string $expr) a max(string $expr) .[method] +min(string $expr) i max(string $expr) .[method] ----------------------------------------------- -Metody `min()` i `max()` zwracają minimalną i maksymalną wartość w określonej kolumnie lub wyrażeniu: +Metody `min()` i `max()` zwracają minimalną i maksymalną wartość w podanej kolumnie albo wyrażeniu: ```php // SELECT MAX(`price`) FROM `products` WHERE `active` = 1 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -Zwraca sumę wartości w określonej kolumnie lub wyrażeniu: +Zwraca sumę wartości w podanej kolumnie albo wyrażeniu: ```php // SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 @@ -478,51 +478,51 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -Umożliwia wykonanie dowolnej funkcji agregującej. +Pozwala wykonać dowolną funkcję agregującą. ```php // średnia cena produktów w kategorii $avgPrice = $products->where('category_id', 1) ->aggregation('AVG(price)'); -// łączy etykiety produktu w jeden ciąg +// łączy tagi produktu w jeden ciąg $tags = $products->where('id', 1) ->aggregation('GROUP_CONCAT(tag.name) AS tags') ->fetch() ->tags; ``` -Jeśli potrzebujemy agregować wyniki, które już same w sobie powstały z jakiejś funkcji agregującej i grupowania (np. `SUM(wartość)` przez zgrupowane wiersze), jako drugi argument podajemy funkcję agregującą, która ma być zastosowana do tych wyników pośrednich: +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 -// Oblicza całkowitą cenę produktów w magazynie dla poszczególnych kategorii, a następnie sumuje te ceny. +// 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 obliczamy całkowitą cenę produktów w każdej kategorii (`SUM(price * stock) AS category_total`) i grupujemy wyniki według `category_id`. Następnie używamy `aggregation('SUM(category_total)', 'SUM')` do zsumowania tych sum pośrednich `category_total`. Drugi argument `'SUM'` mówi, że na wyniki pośrednie ma być zastosowana funkcja 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 & Delete +Insert, Update i Delete ======================= -Nette Database Explorer upraszcza wstawianie, aktualizację i usuwanie danych. Wszystkie podane metody w przypadku błędu wyrzucą wyjątek `Nette\Database\DriverException`. +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 nowe rekordy do tabeli. +Wstawia do tabeli nowe rekordy. -**Wstawianie jednego rekordu:** +**Wstawienie jednego rekordu:** -Nowy rekord przekazujemy jako tablicę asocjacyjną lub obiekt iterable (na przykład ArrayHash używany w [formularzach |forms:]), gdzie klucze odpowiadają nazwom kolumn w tabeli. +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 podstawowy, metoda zwraca obiekt `ActiveRow`, który jest ponownie ładowany z bazy danych, aby uwzględnić ewentualne zmiany dokonane na poziomie bazy danych (triggery, wartości domyślne kolumn, obliczenia kolumn auto-increment). Zapewnia to spójność danych, a obiekt zawsze zawiera aktualne dane z bazy danych. Jeśli jednoznaczny klucz podstawowy nie istnieje, zwraca przekazane dane w formie tablicy. +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([ @@ -530,14 +530,14 @@ $row = $explorer->table('users')->insert([ 'email' => 'john.doe@example.com', ]); // $row jest instancją ActiveRow i zawiera kompletne dane wstawionego wiersza, -// w tym automatycznie generowane ID i ewentualne zmiany dokonane przez triggery +// 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 jest ustawiony przez trigger +echo $row->created_at; // Wypisuje czas utworzenia, jeśli ustawia go trigger ``` -**Wstawianie wielu rekordów naraz:** +**Wstawienie wielu rekordów naraz:** -Metoda `insert()` umożliwia wstawienie wielu rekordów za pomocą jednego zapytania SQL. W tym przypadku zwraca liczbę wstawionych wierszy. +Metoda `insert()` pozwala wstawić wiele rekordów jednym zapytaniem SQL. W takim przypadku zwraca liczbę wstawionych wierszy. ```php $insertedRows = $explorer->table('users')->insert([ @@ -554,7 +554,7 @@ $insertedRows = $explorer->table('users')->insert([ // $insertedRows będzie 2 ``` -Jako parametr można również przekazać obiekt `Selection` z wyborem danych. +Jako parametr można przekazać także obiekt `Selection` z wyborem danych. ```php $newUsers = $explorer->table('potential_users') @@ -564,9 +564,9 @@ $newUsers = $explorer->table('potential_users') $insertedRows = $explorer->table('users')->insert($newUsers); ``` -**Wstawianie specjalnych wartości:** +**Wstawianie wartości specjalnych:** -Jako wartości możemy przekazywać również pliki, obiekty DateTime lub literały SQL: +Jako wartości możemy przekazać także pliki, obiekty `DateTime` albo literały SQL: ```php $explorer->table('users')->insert([ @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -Aktualizuje wiersze w tabeli według podanego filtra. Zwraca liczbę rzeczywiście zmienionych wierszy. +Aktualizuje wiersze w tabeli według podanego filtra. Zwraca liczbę faktycznie zmienionych wierszy. -Zmieniane kolumny przekazujemy jako tablicę asocjacyjną lub obiekt iterable (na przykład ArrayHash używany w [formularzach |forms:]), gdzie klucze odpowiadają nazwom kolumn w tabeli: +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') @@ -595,7 +595,7 @@ $affected = $explorer->table('users') // UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Do zmiany wartości liczbowych możemy użyć operatorów `+=` i `-=`: +Do zmiany wartości liczbowych możesz użyć operatorów `+=` i `-=`: ```php $explorer->table('users') @@ -621,20 +621,20 @@ $count = $explorer->table('users') ``` .[caution] -Podczas wywoływania `update()` i `delete()` nie zapomnij za pomocą `where()` określić wierszy, które mają zostać zmodyfikowane/usunięte. Jeśli `where()` nie zostanie użyte, operacja zostanie przeprowadzona na całej tabeli! +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`. Jako parametr przyjmuje iterable z danymi, które mają zostać zaktualizowane (klucze są nazwami kolumn). Do zmiany wartości liczbowych możemy użyć operatorów `+=` i `-=`: +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 ładowany z bazy danych, aby uwzględnić ewentualne zmiany dokonane na poziomie bazy danych (np. triggery). Metoda zwraca true tylko jeśli doszło do rzeczywistej zmiany danych. +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ększamy liczbę wyświetleń + 'views += 1', // zwiększa liczbę wyświetleń ]); echo $article->views; // Wypisuje aktualną liczbę wyświetleń ``` @@ -642,63 +642,63 @@ 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() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -Usuwa wiersz z bazy danych, który jest reprezentowany przez obiekt `ActiveRow`. +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 usunięcia wielu wierszy użyj metody [#Selection::delete()]. +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 relacyjnych bazach danych dane są podzielone na wiele tabel i wzajemnie powiązane za pomocą kluczy obcych. Nette Database Explorer wprowadza rewolucyjny sposób pracy z tymi relacjami - bez pisania zapytań JOIN i konieczności cokolwiek konfigurować czy generować. +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 ilustracji pracy z relacjami użyjemy przykładu bazy danych książek ([znajdziesz go na GitHubie |https://github.com/nette-examples/books]). W bazie danych mamy tabele: +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` - etykiety (kolumny `id`, `name`) -- `book_tag` - tabela łącząca między książkami a etykietami (kolumny `book_id`, `tag_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żyta w przykładach *** +[* db-schema-1-.webp *] *** Struktura bazy danych używanej w przykładach .<> -W naszym przykładzie bazy danych książek znajdziemy kilka typów relacji (chociaż model jest uproszczony w porównaniu do rzeczywistości): +W naszej przykładowej bazie książek znajdziemy kilka typów relacji (choć model jest uproszczony względem rzeczywistości): -- One-to-many 1:N – każda książka **ma jednego** autora, autor może napisać **kilka** książek -- Zero-to-many 0:N – książka **może mieć** tłumacza, tłumacz może przetłumaczyć **kilka** książek -- Zero-to-one 0:1 – książka **może mieć** kolejną część -- Many-to-many M:N – książka **może mieć kilka** tagów, a tag może być przypisany **kilku** książkom +- **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 podrzędna. Na przykład w relacji między autorem a książką tabela `author` jest nadrzędna, a `book` podrzędna - możemy to sobie wyobrazić tak, że książka zawsze "należy" do jakiegoś autora. Przejawia się to również w strukturze bazy danych: podrzędna tabela `book` zawiera klucz obcy `author_id`, który odnosi się do nadrzędnej tabeli `author`. +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 imionami ich autorów, mamy dwie możliwości. Albo uzyskamy dane jednym zapytaniem SQL za pomocą JOIN: +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 +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Albo załadujemy dane w dwóch krokach - najpierw książki, a potem ich autorów - a następnie złożymy je w PHP: +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 pobranych książek +SELECT * FROM author WHERE id IN (1, 2, 3); -- ID autorów z wybranych książek ``` -Drugie podejście jest w rzeczywistości bardziej efektywne, choć może to być zaskakujące. Dane są ładowane tylko raz i mogą być lepiej wykorzystane w cache. Właśnie w ten sposób działa Nette Database Explorer - wszystko rozwiązuje pod powierzchnią i oferuje Ci eleganckie API: +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 'tytuł: ' . $book->title; - echo 'napisane przez: ' . $book->author->name; // $book->author to rekord z tabeli 'author' - echo 'przetłumaczone przez: ' . $book->translator?->name; + echo 'title: ' . $book->title; + echo 'written by: ' . $book->author->name; // $book->author to rekord z tabeli 'author' + echo 'translated by: ' . $book->translator?->name; } ``` @@ -706,26 +706,26 @@ foreach ($books as $book) { Dostęp do tabeli nadrzędnej --------------------------- -Dostęp do tabeli nadrzędnej jest prosty. Chodzi o relacje takie jak *książka ma autora* lub *książka może mieć tłumacza*. Powiązany rekord uzyskujemy przez właściwość obiektu ActiveRow - jej nazwa odpowiada nazwie kolumny z kluczem obcym bez `id`: +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 według kolumny author_id -echo $book->translator?->name; // znajduje tłumacza według translator_id +echo $book->author->name; // znajduje autora na podstawie kolumny author_id +echo $book->translator?->name; // znajduje tłumacza na podstawie kolumny translator_id ``` -Gdy uzyskujemy dostęp do właściwości `$book->author`, Explorer w tabeli `book` szuka kolumny, której nazwa zawiera ciąg `author` (czyli `author_id`). Według wartości w tej kolumnie ładuje odpowiedni rekord z tabeli `author` i zwraca go jako `ActiveRow`. Podobnie działa `$book->translator`, który wykorzystuje kolumnę `translator_id`. Ponieważ kolumna `translator_id` może zawierać `null`, użyjemy w kodzie operatora `?->`. +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 `?->`. -Alternatywną ścieżkę oferuje metoda `ref()`, która przyjmuje dwa argumenty, nazwę tabeli docelowej i nazwę kolumny łączącej, i zwraca instancję `ActiveRow` lub `null`: +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ę, jeśli nie można użyć dostępu przez właściwość, ponieważ tabela zawiera kolumnę o tej samej nazwie (tj. `author`). W pozostałych przypadkach zaleca się używanie dostępu przez właściwość, który jest bardziej czytelny. +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. Kiedy przechodzimy przez książki w pętli i uzyskujemy dostęp do ich powiązanych rekordów (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 znacznie zmniejsza obciążenie bazy danych. Na przykład: +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'); @@ -736,22 +736,22 @@ foreach ($books as $book) { } ``` -Ten kod wywoła tylko te trzy błyskawiczne zapytania do bazy danych: +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 +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] -Logika wyszukiwania kolumny łączącej jest określona przez implementację [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. +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* lub *przetłumaczył ten tłumacz*. Do tego typu zapytania używamy metody `related()`, która zwraca `Selection` z powiązanymi rekordami. Spójrzmy na przykład: +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); @@ -761,28 +761,28 @@ foreach ($author->related('book.author_id') as $book) { echo "Napisał: $book->title"; } -// Wypisuje wszystkie książki, które autor przetłumaczył +// 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ą lub jako dwa osobne argumenty: +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ć poprawną kolumnę łączącą na podstawie nazwy tabeli nadrzędnej. W tym przypadku łączy się przez kolumnę `book.author_id`, ponieważ nazwa tabeli źródłowej to `author`: +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żyje book.author_id +$author->related('book'); // używa book.author_id ``` -Gdyby istniało więcej możliwych połączeń, Explorer wyrzuci wyjątek [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Jeśli istnieje wiele możliwych połączeń, Explorer rzuci [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Metodę `related()` możemy oczywiście użyć również przy przechodzeniu przez wiele rekordów w pętli, a Explorer również w tym przypadku automatycznie optymalizuje zapytania: +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'); @@ -798,14 +798,14 @@ 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 +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ID wybranych autorów ``` -Relacja Many-to-many --------------------- +Relacja wiele do wielu +---------------------- -Dla relacji many-to-many (M:N) potrzebna jest istnienie tabeli łączącej (w naszym przypadku `book_tag`), która zawiera dwie kolumny z kluczami obcymi (`book_id`, `tag_id`). Każda z tych kolumn odnosi się do klucza podstawowego jednej z łączonych tabel. Aby uzyskać powiązane dane, najpierw uzyskujemy rekordy z tabeli łączącej za pomocą `related('book_tag')`, a następnie przechodzimy do danych docelowych: +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); @@ -815,42 +815,42 @@ foreach ($book->related('book_tag') as $bookTag) { } $tag = $explorer->table('tag')->get(1); -// lub odwrotnie: wypisuje nazwy książek oznaczonych tym tagiem +// albo odwrotnie: wypisuje nazwy książek oznaczonych tym tagiem foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // wypisuje nazwę książki + echo $bookTag->book->title; // wypisuje tytuł książki } ``` -Explorer ponownie optymalizuje zapytania SQL do efektywnej postaci: +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 +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żemy używać specjalnych notacji do dostępu do kolumn z innych tabel. Explorer automatycznie utworzy potrzebne JOINy. +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_nadrzędna.kolumna`) jest używana dla relacji 1:N z perspektywy tabeli podrzędnej: +**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 imię zaczynające się na 'Jon' +// Znajduje książki, których autor ma nazwisko zaczynające się od 'Jon' $books->where('author.name LIKE ?', 'Jon%'); -// Sortuje książki według imienia autora malejąco +// Sortuje książki według nazwiska autora malejąco $books->order('author.name DESC'); -// Wypisuje tytuł książki i imię autora +// Wypisuje tytuł książki i nazwisko autora $books->select('book.title, author.name'); ``` -**Notacja dwukropkowa** (`:tabela_podrzędna.kolumna`) jest używana dla relacji 1:N z perspektywy tabeli nadrzędnej: +**Notacja z dwukropkiem** (`:tabela_podrzedna.kolumna`) używana jest dla relacji 1:N z perspektywy tabeli nadrzędnej: ```php $authors = $explorer->table('author'); @@ -858,21 +858,21 @@ $authors = $explorer->table('author'); // Znajduje autorów, którzy napisali książkę z 'PHP' w tytule $authors->where(':book.title LIKE ?', '%PHP%'); -// Oblicza liczbę książek dla każdego autora +// 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ą dwukropkową (`:book.title`) nie jest określona kolumna z kluczem obcym. Explorer automatycznie wykrywa poprawną kolumnę na podstawie nazwy tabeli nadrzędnej. W tym przypadku łączy się przez kolumnę `book.author_id`, ponieważ nazwa tabeli źródłowej to `author`. Gdyby istniało więcej możliwych połączeń, Explorer wyrzuci wyjątek [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +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 nawiasie: +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%'); ``` -Notacje można łączyć łańcuchowo dla dostępu przez wiele tabel: +Zapisy można łączyć w łańcuch, żeby sięgać po dane w wielu tabelach: ```php // Znajduje autorów książek oznaczonych tagiem 'PHP' @@ -881,12 +881,12 @@ $authors->where(':book:book_tag.tag.name', 'PHP') ``` -Rozszerzenie warunków dla JOIN +Rozszerzanie warunków dla JOIN ------------------------------ -Metoda `joinWhere()` rozszerza warunki, które podaje się przy łączeniu tabel w SQL za słowem kluczowym `ON`. +Metoda `joinWhere()` rozszerza warunki podawane przy łączeniu tabel w SQL po słowie kluczowym `ON`. -Załóżmy, że chcemy znaleźć książki przetłumaczone przez konkretnego tłumacza: +Powiedzmy, że chcemy znaleźć książki przetłumaczone przez konkretnego tłumacza: ```php // Znajduje książki przetłumaczone przez tłumacza o imieniu 'David' @@ -895,9 +895,9 @@ $books = $explorer->table('book') // LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') ``` -W warunku `joinWhere()` możemy używać tych samych konstrukcji co w metodzie `where()` - operatorów, znaków zapytania, tablic wartości czy wyrażeń SQL. +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 JOINami możemy zdefiniować aliasy tabel: +Dla bardziej złożonych zapytań z wieloma JOIN-ami możesz zdefiniować aliasy tabel: ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // AND (`book_author`.`born` < 1950) ``` -Zauważ, że podczas gdy metoda `where()` dodaje warunki do klauzuli `WHERE`, metoda `joinWhere()` rozszerza warunki w klauzuli `ON` podczas łączenia tabel. +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 index e3e5d284ef..237f8469a0 100644 --- a/database/pl/guide.texy +++ b/database/pl/guide.texy @@ -2,30 +2,30 @@ Nette Database ************** .[perex] -Nette Database to wydajna i elegancka warstwa bazodanowa dla PHP z naciskiem na prostotę i inteligentne funkcje. Oferuje dwa sposoby pracy z bazą danych - [Explorer |Explorer] dla szybkiego rozwoju aplikacji, lub [Dostęp SQL |SQL way] dla bezpośredniej pracy z zapytaniami. +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> -[Dostęp SQL |SQL way] -===================== -- Bezpieczne sparametryzowane zapytania -- Dokładna kontrola nad formą zapytań SQL +[Podejście SQL|sql-way] +======================= +- Bezpieczne, parametryzowane zapytania +- Precyzyjna kontrola nad strukturą zapytania SQL - Gdy piszesz złożone zapytania z zaawansowanymi funkcjami -- Optymalizujesz wydajność za pomocą specyficznych funkcji SQL +- Optymalizacja wydajności za pomocą konkretnych funkcji SQL </div> <div> -[Explorer |Explorer] +[Explorer |explorer] ==================== -- Rozwijasz szybko bez pisania SQL +- Szybkie tworzenie bez pisania SQL - Intuicyjna praca z relacjami między tabelami -- Docenisz automatyczną optymalizację zapytań -- Odpowiednie do szybkiej i wygodnej pracy z bazą danych +- Korzyść z automatycznej optymalizacji zapytań +- Odpowiedni do szybkiej i wygodnej pracy z bazą danych </div> @@ -35,7 +35,7 @@ Nette Database to wydajna i elegancka warstwa bazodanowa dla PHP z naciskiem na Instalacja ========== -Bibliotekę można pobrać i zainstalować za pomocą narzędzia [Composer|best-practices:composer]: +Pobierz i zainstaluj bibliotekę za pomocą [Composera|best-practices:composer]: ```shell composer require nette/database @@ -47,33 +47,33 @@ Obsługiwane bazy danych Nette Database obsługuje następujące bazy danych: -|* Serwer bazy danych |* Nazwa DSN |* Wsparcie w Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | TAK -| PostgreSQL (>= 9.0) | pgsql | TAK -| Sqlite 3 (>= 3.8) | sqlite | TAK -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | TAK -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - +|* 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 bazy danych -============================ +Dwa podejścia do pracy z bazą danych +==================================== -Nette Database daje Ci wybór: możesz pisać zapytania SQL bezpośrednio (dostęp SQL) lub pozwolić na ich automatyczne generowanie (Explorer). Zobaczmy, jak oba podejścia rozwiązują te same zadania: +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: -[Dostęp SQL|sql way] - Zapytania SQL +[Podejście SQL|sql-way] - zapytania SQL ```php -// wstawienie rekordu +// Wstawienie rekordu $database->query('INSERT INTO books', [ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// pobranie rekordów: autorzy książek +// Pobranie rekordów: autorzy książek $result = $database->query(' SELECT authors.*, COUNT(books.id) AS books_count FROM authors @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// wypisanie (nie jest optymalne, generuje N dodatkowych zapytań) +// Wypisanie (nieoptymalne, generuje N dodatkowych zapytań) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -101,18 +101,18 @@ foreach ($result as $author) { [Podejście Explorer|explorer] - automatyczne generowanie SQL ```php -// wstawienie rekordu +// Wstawienie rekordu $database->table('books')->insert([ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// pobranie rekordów: autorzy książek +// Pobranie rekordów: autorzy książek $authors = $database->table('authors') ->where('active', 1); -// wypisanie (automatycznie generuje tylko 2 zoptymalizowane zapytania) +// Wypisanie (automatycznie generuje tylko 2 zoptymalizowane zapytania) foreach ($authors as $author) { $books = $author->related('books') ->order('published_at DESC'); @@ -125,23 +125,23 @@ foreach ($authors as $author) { } ``` -Podejście Explorer generuje i optymalizuje zapytania SQL automatycznie. W podanym przykładzie podejście SQL wygeneruje N+1 zapytań (jedno dla autorów, a następnie jedno dla książek każdego autora), podczas gdy Explorer automatycznie optymalizuje zapytania i wykonuje tylko dwa - jedno dla autorów i jedno dla wszystkich ich książek. +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 dowolnie łączyć w aplikacji w zależności od potrzeb. +Oba podejścia można w aplikacji swobodnie łączyć według potrzeb. Połączenie i konfiguracja ========================= -Aby połączyć się z bazą danych, wystarczy utworzyć instancję klasy [api:Nette\Database\Connection]: +Ż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, [jakiego używa PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], np. `host=127.0.0.1;dbname=test`. W przypadku niepowodzenia rzuca wyjątek `Nette\Database\ConnectionException`. +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`. -Jednak wygodniejszy sposób oferuje [konfiguracja aplikacji |configuration], gdzie wystarczy dodać sekcję `database`, a zostaną utworzone potrzebne obiekty oraz panel bazy danych w pasku [Tracy |tracy:]. +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: @@ -150,13 +150,13 @@ database: password: password ``` -Następnie obiekt połączenia [uzyskujemy jako usługę z kontenera DI |dependency-injection:passing-dependencies], np.: +Obiekt połączenia można potem [uzyskać jako usługę z kontenera DI |dependency-injection:passing-dependencies], np.: ```php class Model { public function __construct( - // lub Nette\Database\Explorer + // albo Nette\Database\Explorer private Nette\Database\Connection $database, ) { } @@ -166,15 +166,15 @@ class Model Więcej informacji o [konfiguracji bazy danych|configuration]. -Ręczne tworzenie Explorera --------------------------- +Ręczne utworzenie Explorera +--------------------------- -Jeśli nie używasz kontenera Nette DI, możesz utworzyć instancję `Nette\Database\Explorer` ręcznie: +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 dla cache, implementuje Nette\Caching\Storage, np.: +// 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); @@ -187,18 +187,18 @@ $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $ Zarządzanie połączeniem ======================= -Podczas tworzenia obiektu `Connection` połączenie jest nawiązywane automatycznie. Jeśli chcesz odłożyć połączenie, użyj trybu lazy - włączysz go w [konfiguracji|configuration] ustawiając `lazy`, lub w ten sposób: +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 użyj metod `connect()`, `disconnect()` i `reconnect()`. -- `connect()` tworzy połączenie, jeśli jeszcze nie istnieje, przy czym może rzucić wyjątek `Nette\Database\ConnectionException`. +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ć wyjątek `Nette\Database\ConnectionException`. +- `reconnect()` wykonuje rozłączenie i ponowne połączenie z bazą danych. Ta metoda również może rzucić `Nette\Database\ConnectionException`. -Ponadto możesz śledzić 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. +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 @@ -207,10 +207,12 @@ $database->onConnect[] = function($database) { }; ``` +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. -Pasek Debugowania Tracy -======================= -Jeśli używasz [Tracy |tracy:], panel Database w pasku Debugowania aktywuje się automatycznie, wyświetlając wszystkie wykonane zapytania, ich parametry, czas wykonania oraz miejsce w kodzie, gdzie zostały wywołane. +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/mapping.texy b/database/pl/mapping.texy deleted file mode 100644 index 4eea807bb4..0000000000 --- a/database/pl/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Konwersja typów -*************** - -.[perex] -Nette Database automatycznie konwertuje wartości zwrócone z bazy danych na odpowiednie typy PHP. - - -Data i czas ------------ - -Dane czasowe są konwertowane na obiekty `Nette\Utils\DateTime`. Jeśli chcesz, aby dane czasowe były konwertowane na niemutowalne 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 konwertuje typ danych `TIME` na obiekty `DateInterval`. - - -Wartości logiczne ------------------ - -Wartości logiczne są automatycznie konwertowane na `true` lub `false`. W MySQL konwertuje się `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 są konwertowane na `int` lub `float` w zależności od typu 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 -------------------- - -Za pomocą metody `setRowNormalizer(?callable $normalizer)` możesz ustawić własną funkcję do transformacji wierszy z bazy danych. Jest to przydatne na przykład do automatycznej konwersji typów danych. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // tutaj następuje konwersja typów - return $row; -}); -``` diff --git a/database/pl/reflection.texy b/database/pl/reflection.texy index 0f09769793..cd295b4f26 100644 --- a/database/pl/reflection.texy +++ b/database/pl/reflection.texy @@ -2,7 +2,7 @@ Refleksja struktury ******************* .{data-version:3.2.1} -Nette Database dostarcza narzędzi do introspekcji struktury bazy danych za pomocą klasy [api:Nette\Database\Reflection]. Umożliwia ona uzyskiwanie informacji o tabelach, kolumnach, indeksach i kluczach obcych. Refleksję można wykorzystać do generowania schematów, tworzenia elastycznych aplikacji pracujących z bazą danych lub ogólnych narzędzi bazodanowych. +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: @@ -39,31 +39,33 @@ $table = $reflection->getTable('users'); Informacje o tabeli ------------------- -Tabela jest reprezentowana przez obiekt [Table|api:Nette\Database\Reflection\Table], który udostępnia następujące właściwości readonly: +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 podstawowy tabeli lub null -- `$foreignKeys: ForeignKey[]` – tablica kluczy obcych tabeli +- `$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 następującymi właściwościami: +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` – referencja do tabeli kolumny -- `$nativeType: string` – natywny typ bazodanowy -- `$size: ?int` – rozmiar/długość typu -- `$nullable: bool` – czy kolumna może zawierać NULL -- `$default: mixed` – domyślna wartość kolumny -- `$autoIncrement: bool` – czy kolumna jest auto-increment -- `$primary: bool` – czy jest częścią klucza podstawowego -- `$vendor: array` – dodatkowe metadane specyficzne dla danego systemu bazodanowego +- `$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) { @@ -77,14 +79,14 @@ foreach ($table->columns as $name => $column) { Indeksy ------- -Właściwość `indexes` tabeli udostępnia tablicę indeksów, gdzie każdy indeks jest instancją [Index|api:Nette\Database\Reflection\Index] z następującymi właściwościami: +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 to klucz podstawowy -- `$name: ?string` – nazwa indeksu +- `$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 podstawowy tabeli można uzyskać za pomocą właściwości `primaryKey`, która zwraca obiekt `Index` lub `null` w przypadku, gdy tabela nie ma klucza podstawowego. +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 @@ -95,10 +97,10 @@ foreach ($table->indexes as $index) { echo " Unikalny: " . ($index->unique ? 'Tak' : 'Nie') . "\n"; } -// Wypisanie klucza podstawowego +// Wypisanie klucza głównego if ($primaryKey = $table->primaryKey) { $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Klucz podstawowy: $columns\n"; + echo "Klucz główny: $columns\n"; } ``` @@ -106,12 +108,12 @@ if ($primaryKey = $table->primaryKey) { 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 następującymi właściwościami: +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 referencyjna -- `$localColumns: Column[]` – tablica kolumn lokalnych -- `$foreignColumns: Column[]` – tablica kolumn referencyjnych -- `$name: ?string` – nazwa klucza obcego +- `$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 diff --git a/database/pl/security.texy b/database/pl/security.texy index 2a6aba415e..8b2a4e9956 100644 --- a/database/pl/security.texy +++ b/database/pl/security.texy @@ -1,35 +1,35 @@ -Ryzyka bezpieczeństwa -********************* +Zagrożenia bezpieczeństwa +************************* <div class=perex> -Baza danych często zawiera wrażliwe dane i umożliwia wykonywanie niebezpiecznych operacji. Dla bezpiecznej pracy z Nette Database kluczowe jest: +Bazy danych często zawierają wrażliwe dane i pozwalają wykonywać niebezpieczne operacje. Dla bezpiecznej pracy z Nette Database kluczowe jest: -- Zrozumienie różnicy między bezpiecznym a niebezpiecznym API -- Używanie sparametryzowanych zapytań -- Poprawna walidacja danych wejściowych +- Zrozumieć różnicę między bezpiecznym a niebezpiecznym API +- Używać zapytań parametryzowanych +- Poprawnie walidować dane wejściowe </div> -Co to jest SQL Injection? -========================= +Czym jest SQL injection? +======================== -SQL injection jest najpoważniejszym ryzykiem bezpieczeństwa podczas pracy z bazą danych. Powstaje, gdy nieprzetworzone dane wejściowe od użytkownika stają się częścią zapytania SQL. Atakujący może wstrzyknąć własne polecenia SQL i tym samym: +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ć lub usunąć dane w bazie danych -- Ominąć uwierzytelnianie +- 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 podać na przykład wartość: ' OR '1'='1 -// Wynikowe zapytanie będzie wtedy: SELECT * FROM users WHERE name = '' OR '1'='1' +// 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 Explorer: +To samo dotyczy Database Explorera: ```php // ❌ NIEBEZPIECZNY KOD - podatny na SQL injection @@ -38,24 +38,24 @@ $table->where("name = '$_GET[name]'"); ``` -Zapytania sparametryzowane -========================== +Zapytania parametryzowane +========================= -Podstawową obroną przed SQL injection są zapytania sparametryzowane. Nette Database oferuje kilka sposobów ich użycia. +Podstawową obroną przed SQL injection są zapytania parametryzowane. Nette Database oferuje kilka sposobów ich użycia. -Najprostszym sposobem jest użycie **symboli zastępczych (placeholderów) w postaci znaków zapytania**: +Najprostszy sposób to użycie **zastępników w postaci znaku zapytania**: ```php -// ✅ Bezpieczne zapytanie sparametryzowane +// ✅ Bezpieczne zapytanie parametryzowane $database->query('SELECT * FROM users WHERE name = ?', $name); // ✅ Bezpieczny warunek w Explorerze $table->where('name = ?', $name); ``` -Dotyczy to wszystkich innych metod w [Database Explorer|explorer], które umożliwiają wstawianie wyrażeń z symbolami zastępczymi i parametrami. +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 lub klauzuli WHERE możemy przekazać wartości w tablicy: +Dla poleceń INSERT, UPDATE albo klauzuli WHERE możemy przekazać wartości w tablicy: ```php // ✅ Bezpieczny INSERT @@ -75,89 +75,89 @@ $table->insert([ Walidacja wartości parametrów ============================= -Zapytania sparametryzowane są podstawowym elementem bezpiecznej pracy z bazą danych. Jednak wartości, które do nich wstawiamy, muszą przejść przez kilka poziomów kontroli: +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** - jest to warunek konieczny do bezpiecznego używania Nette Database. Baza danych zakłada, że wszystkie dane wejściowe mają poprawny typ danych odpowiadający danej kolumnie. +**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. -Na przykład, jeśli `$name` w poprzednich przykładach byłoby niespodziewanie tablicą zamiast stringiem, Nette Database próbowałoby wstawić wszystkie jej elementy do zapytania SQL, co doprowadziłoby do błędu. Dlatego **nigdy nie używaj** niezweryfikowanych danych z `$_GET`, `$_POST` lub `$_COOKIE` bezpośrednio w zapytaniach bazodanowych. +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. -Kontrola formatu ----------------- +Walidacja formatu +----------------- -Na drugim poziomie kontrolujemy format danych - na przykład, czy ciągi znaków są w kodowaniu UTF-8 i ich długość odpowiada definicji kolumny, lub czy wartości liczbowe mieszczą się w dozwolonym zakresie dla danego typu danych kolumny. +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 danych odrzuci nieprawidłowe dane. Jednak zachowanie może się różnić, niektóre mogą cicho skrócić długie ciągi znaków lub przyciąć liczby spoza zakresu. +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. -Kontrola domenowa ------------------ +Walidacja specyficzna dla domeny +-------------------------------- -Trzeci poziom stanowią kontrole logiczne specyficzne dla Twojej aplikacji. Na przykład weryfikacja, czy wartości z pól wyboru odpowiadają oferowanym opcjom, czy liczby mieszczą się w oczekiwanym zakresie (np. wiek 0-150 lat) lub czy wzajemne zależności między wartościami mają sens. +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 [Formularzy Nette|forms:], które automatycznie zapewniają poprawną walidację wszystkich danych wejściowych -- Używaj [Presenterów|application:] i podawaj typy danych dla parametrów w metodach `action*()` i `render*()` -- Lub zaimplementuj własną warstwę walidacji za pomocą standardowych narzędzi PHP, takich jak `filter_var()` +- 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. Jednak przy użyciu tablic w zapytaniach SQL musimy poświęcić taką samą uwagę ich kluczom. +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ą sprawdzane +// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są oczyszczone $database->query('INSERT INTO users', $_POST); ``` -W przypadku poleceń INSERT i UPDATE jest to fundamentalny błąd bezpieczeństwa - atakujący może wstawić lub zmienić dowolną kolumnę w bazie danych. Mógłby na przykład ustawić `is_admin = 1` lub wstawić dowolne dane do wrażliwych kolumn (tzw. Mass Assignment Vulnerability). +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 bardziej niebezpieczne, ponieważ mogą zawierać operatory: +W warunkach WHERE jest to jeszcze niebezpieczniejsze, bo mogą one zawierać operatory: ```php -// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są sprawdzane +// ❌ NIEBEZPIECZNY KOD - klucze w tablicy nie są oczyszczone $_POST['salary >'] = 100000; $database->query('SELECT * FROM users WHERE', $_POST); -// wykonuje zapytanie WHERE (`salary` > 100000) +// wykona zapytanie WHERE (`salary` > 100000) ``` -Atakujący może wykorzystać to podejście do systematycznego odkrywania wynagrodzeń pracowników. Zacznie na przykład od zapytania o wynagrodzenia powyżej 100 000, następnie poniżej 50 000 i stopniowo zawężając zakres, może odkryć przybliżone wynagrodzenia wszystkich pracowników. Ten typ ataku nazywa się SQL enumeration. +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ą jeszcze [znacznie bardziej elastyczne |explorer#where] i obsługują w kluczach i wartościach wyrażenia SQL, w tym operatory i funkcje. Daje to atakującemu możliwość przeprowadzenia SQL injection: +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 wstrzyknąć własny SQL +// ❌ NIEBEZPIECZNY KOD - atakujący może wstawić własny SQL $_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; $table->where($_POST); -// wykonuje zapytanie WHERE (0) UNION SELECT name, salary FROM users WHERE (1) +// wykona zapytanie WHERE (0) UNION SELECT name, salary FROM users WHERE (1) ``` -Ten atak kończy pierwotny warunek za pomocą `0)`, dołącza własne `SELECT` za pomocą `UNION`, aby uzyskać wrażliwe dane z tabeli `users` i zamyka składniowo poprawne zapytanie za pomocą `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)`. -Biała lista kolumn ------------------- +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 próbować wykrywać i blokować niebezpieczne nazwy kolumn (czarna lista), ale to podejście jest zawodne - atakujący zawsze może wymyślić nowy sposób zapisu niebezpiecznej nazwy kolumny, którego nie przewidzieliśmy. +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 bezpieczniejsze jest odwrócenie logiki i zdefiniowanie jawnej listy dozwolonych kolumn (biała lista): +Dlatego znacznie bezpieczniej jest odwrócić logikę i zdefiniować jawną listę dozwolonych kolumn (whitelistę): ```php -// Kolumny, które użytkownik może edytować +// Kolumny, które użytkownik może modyfikować $allowedColumns = ['name', 'email', 'active']; -// Usuwamy wszystkie niedozwolone kolumny z danych wejściowych -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // array_flip for PHP < 8.1 +// Usuwamy z wejścia wszystkie nieuprawnione kolumny +$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); -// ✅ Teraz możemy bezpiecznie używać w zapytaniach, na przykład: +// ✅ Teraz bezpiecznie użyjemy w zapytaniach, na przykład: $database->query('INSERT INTO users', $filteredData); $table->update($filteredData); $table->where($filteredData); @@ -167,7 +167,7 @@ $table->where($filteredData); Dynamiczne identyfikatory ========================= -Dla dynamicznych nazw tabel i kolumn użyj symbolu zastępczego `?name`. Zapewni on poprawne escapowanie identyfikatorów zgodnie ze składnią danej bazy danych (np. za pomocą odwrotnych apostrofów w MySQL): +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 @@ -177,9 +177,9 @@ $database->query('SELECT ?name FROM ?name', $column, $table); // Wynik w MySQL: SELECT `name` FROM `users` ``` -Ważne: symbol `?name` używaj tylko dla zaufanych wartości zdefiniowanych w kodzie aplikacji. Dla wartości od użytkownika użyj ponownie [białej listy |#Biała lista kolumn]. W przeciwnym razie narażasz się na ryzyko bezpieczeństwa: +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 danych wejściowych od użytkownika +// ❌ 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 index 860a12a8f2..077bfb5094 100644 --- a/database/pl/sql-way.texy +++ b/database/pl/sql-way.texy @@ -1,17 +1,17 @@ -Dostęp SQL -********** +Podejście SQL +************* .[perex] -Nette Database oferuje dwie ścieżki: możesz pisać zapytania SQL samodzielnie (dostęp SQL) lub pozwolić na ich automatyczne generowanie (zobacz [Explorer |explorer]). Dostęp SQL daje Ci pełną kontrolę nad zapytaniami, jednocześnie zapewniając ich bezpieczne tworzenie. +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]. +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 wykonywania zapytań do bazy danych służy metoda `query()`. Zwraca ona obiekt [ResultSet |api:Nette\Database\ResultSet], który reprezentuje wynik zapytania. W przypadku niepowodzenia metoda [rzuci wyjątek|exceptions]. Wynik zapytania możemy przeglądać za pomocą pętli `foreach` lub użyć jednej z [funkcji pomocniczych |#Pobieranie danych]. +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'); @@ -22,19 +22,19 @@ foreach ($result as $row) { } ``` -Do bezpiecznego wstawiania wartości do zapytań SQL używamy zapytań sparametryzowanych. Nette Database czyni je maksymalnie prostymi - wystarczy dodać przecinek i wartość po zapytaniu SQL: +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 zapisu. Możesz albo "przeplatać" zapytanie SQL parametrami: +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 napisać najpierw całe zapytanie SQL, a następnie dołączyć wszystkie parametry: +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); @@ -44,20 +44,20 @@ $database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); Ochrona przed SQL injection =========================== -Dlaczego ważne jest używanie zapytań sparametryzowanych? Ponieważ chronią Cię przed atakiem zwanym SQL injection, w którym atakujący mógłby podrzucić własne polecenia SQL i tym samym uzyskać lub uszkodzić dane w bazie danych. +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ń sparametryzowanych, które chronią Cię przed SQL injection. +**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 sparametryzowane +// ✅ Bezpieczne zapytanie parametryzowane $database->query('SELECT * FROM users WHERE name = ?', $name); ``` -Zapoznaj się z [możliwymi ryzykami bezpieczeństwa |security]. +Zapoznaj się z [potencjalnymi zagrożeniami bezpieczeństwa |security]. Techniki zapytań @@ -67,7 +67,7 @@ Techniki zapytań Warunki WHERE ------------- -Warunki WHERE możesz zapisać jako tablicę asocjacyjną, gdzie klucze to nazwy kolumn, a wartości to dane do porównania. Nette Database automatycznie wybierze najodpowiedniejszy operator SQL w zależności od typu wartości. +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', [ @@ -77,7 +77,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -W kluczu możesz również jawnie określić operator do porównania: +Operator porównania możesz też podać jawnie w kluczu: ```php $database->query('SELECT * FROM users WHERE', [ @@ -88,7 +88,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' ``` -Nette automatycznie obsługuje specjalne przypadki, takie jak wartości `null` lub tablice. +Nette automatycznie obsługuje przypadki szczególne, jak wartości `null` czy tablice. ```php $database->query('SELECT * FROM products WHERE', [ @@ -103,21 +103,21 @@ Dla warunków negatywnych użyj operatora `NOT`: ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // użyje operatora <> + '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' => [], // zostanie pominięte + 'id NOT' => [], // pominięte ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -Do łączenia warunków używa się operatora `AND`. Można to zmienić za pomocą [symbolu zastępczego ?or |#Wskazówki dotyczące tworzenia SQL]. +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 --------------- -Sortowanie `ORDER BY` można zapisać za pomocą tablicy. W kluczach podajemy kolumny, a wartością będzie boolean określający, czy sortować rosnąco: +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', [ @@ -131,7 +131,7 @@ $database->query('SELECT id FROM author ORDER BY', [ Wstawianie danych (INSERT) -------------------------- -Do wstawiania rekordów używa się polecenia SQL `INSERT`. +Do wstawiania rekordów służy polecenie SQL `INSERT`. ```php $values = [ @@ -142,11 +142,11 @@ $database->query('INSERT INTO users ?', $values); $userId = $database->getInsertId(); ``` -Metoda `getInsertId()` zwraca ID ostatnio wstawionego wiersza. W niektórych bazach danych (np. PostgreSQL) konieczne jest podanie jako parametru nazwy sekwencji, z której ma być generowane ID za pomocą `$database->getInsertId($sequenceId)`. +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żemy przekazywać również [#wartości specjalne], takie jak pliki, obiekty DateTime lub typy wyliczeniowe. +Jako parametry możesz przekazać także [#Wartości specjalne], jak pliki, obiekty DateTime czy typy enum. -Wstawienie wielu rekordów naraz: +Wstawianie wielu rekordów naraz: ```php $database->query('INSERT INTO users ?', [ @@ -155,15 +155,15 @@ $database->query('INSERT INTO users ?', [ ]); ``` -Wielokrotny INSERT jest znacznie szybszy, ponieważ wykonuje się jedno zapytanie do bazy danych, zamiast wielu pojedynczych. +INSERT wielu rekordów jest znacznie szybszy, bo wykonywane jest tylko jedno zapytanie do bazy zamiast wielu pojedynczych. -**Ostrzeżenie dotyczące bezpieczeństwa:** Nigdy nie używaj jako `$values` niezweryfikowanych danych. Zapoznaj się z [możliwymi ryzykami |security#Bezpieczna praca z kolumnami]. +**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 używa się polecenia SQL `UPDATE`. +Do aktualizacji rekordów służy polecenie SQL `UPDATE`. ```php // Aktualizacja jednego rekordu @@ -173,17 +173,17 @@ $values = [ $result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); ``` -Liczbę zmienionych wierszy zwraca `$result->getRowCount()`. +Liczbę dotkniętych wierszy zwraca `$result->getRowCount()`. -Dla UPDATE możemy wykorzystać operatory `+=` i `-=`: +Przy `UPDATE` możemy użyć operatorów `+=` i `-=`: ```php $database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // inkrementacja login_count + 'login_count+=' => 1, // zwiększa login_count ], 1); ``` -Przykład wstawienia lub aktualizacji rekordu, jeśli już istnieje. Użyjemy techniki `ON DUPLICATE KEY UPDATE`: +Przykład wstawienia albo aktualizacji rekordu, jeśli już istnieje. Użyjemy techniki `ON DUPLICATE KEY UPDATE`: ```php $values = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Zauważ, że Nette Database rozpoznaje, w jakim kontekście polecenia SQL wstawiamy parametr z tablicą i odpowiednio tworzy z niego kod SQL. Tak więc z pierwszej tablicy utworzył `(id, name, year) VALUES (123, 'Jim', 1978)`, podczas gdy drugą przekształcił do postaci `name = 'Jim', year = 1978`. Szczegółowiej omówimy to w części [#Wskazówki dotyczące tworzenia SQL]. +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 używa się polecenia SQL `DELETE`. Przykład z uzyskaniem liczby usuniętych wierszy: +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) @@ -212,21 +212,21 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -Wskazówki dotyczące tworzenia SQL ---------------------------------- +Wskazówki składania SQL +----------------------- -Wskazówka (hint) to specjalny symbol zastępczy w zapytaniu SQL, który mówi, jak wartość parametru ma zostać przepisana na wyrażenie 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 | Używa się automatycznie +| Wskazówka | Opis | Automatycznie używana dla |-----------|-------------------------------------------------|----------------------------- -| `?name` | używa do wstawienia nazwy tabeli lub kolumny | - -| `?values` | generuje `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | generuje przypisanie `key = value, ...` | `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 ?` +| `?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 ?` -Do dynamicznego wstawiania nazw tabel i kolumn do zapytania służy symbol zastępczy `?name`. Nette Database zadba o poprawne przetworzenie identyfikatorów zgodnie z konwencjami danej bazy danych (np. zamknięcie w odwrotnych apostrofach w MySQL). +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'; @@ -235,9 +235,9 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); // SELECT `name` FROM `users` WHERE id = 1 (w MySQL) ``` -**Ostrzeżenie:** symbol `?name` używaj tylko dla nazw tabel i kolumn z zweryfikowanych danych wejściowych, w przeciwnym razie narażasz się na [ryzyko bezpieczeństwa |security#Dynamiczne identyfikatory]. +**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ć, ponieważ Nette używa inteligentnej autodetekcji podczas składania zapytania SQL (zobacz trzecią kolumnę tabeli). Ale możesz jej użyć na przykład w sytuacji, gdy chcesz połączyć warunki za pomocą `OR` zamiast `AND`: +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', [ @@ -251,29 +251,29 @@ $database->query('SELECT * FROM users WHERE ?or', [ Wartości specjalne ------------------ -Oprócz zwykłych typów skalarnych (string, int, bool) możesz przekazywać jako parametry również 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 `DateTime` są konwertowane na format bazodanowy -- typy wyliczeniowe: instancje `enum` są konwertowane na ich wartość -- literały SQL: utworzone za pomocą `Connection::literal('NOW()')` są wstawiane bezpośrednio do zapytania +- 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 DateTime, + 'published_at' => new DateTimeImmutable, // albo new DateTime 'content' => fopen('image.png', 'r'), 'state' => Status::Draft, ]); ``` -W bazach danych, które nie mają natywnego wsparcia dla typu danych `datetime` (jak SQLite i Oracle), `DateTime` jest konwertowany na wartość określoną w [konfiguracji bazy danych|configuration] za pomocą opcji `formatDateTime` (domyślna wartość to `U` - unix timestamp). +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 musisz podać jako wartość bezpośrednio kod SQL, który nie powinien być traktowany jako ciąg znaków i escapowany. Do tego służą obiekty klasy `Nette\Database\SqlLiteral`. Tworzy je metoda `Connection::literal()`. +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', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -Lub alternatywnie: +Alternatywnie: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Dzięki czemu możemy tworzyć ciekawe kombinacje: +Pozwala to na ciekawe kombinacje: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -324,13 +324,13 @@ Pobieranie danych Skróty dla zapytań SELECT ------------------------- -Aby uprościć pobieranie danych, `Connection` oferuje kilka skrótów, które łączą wywołanie `query()` z następującym `fetch*()`. Metody te przyjmują te same parametry co `query()`, czyli zapytanie SQL i opcjonalne parametry. Pełny opis metod `fetch*()` znajdziesz [poniżej |#fetch]. +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` -| `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ą, gdzie pierwsza kolumna reprezentuje klucz, a druga wartość -| `fetchField($sql, ...$params): mixed` | Wykonuje zapytanie i zwraca wartość pierwszej komórki z pierwszego wiersza -| `fetchList($sql, ...$params): ?array` | Wykonuje zapytanie i zwraca pierwszy wiersz jako tablicę indeksowaną +| `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: @@ -341,10 +341,10 @@ $count = $database->query('SELECT COUNT(*) FROM articles') ``` -`foreach` - iteracja po wierszach ---------------------------------- +`foreach` - iterowanie po wierszach +----------------------------------- -Po wykonaniu zapytania zwracany jest obiekt [ResultSet|api:Nette\Database\ResultSet], który umożliwia przeglądanie wyników na kilka sposobów. Najłatwiejszym sposobem wykonania zapytania i pobrania wierszy jest iteracja w pętli `foreach`. Ten sposób jest najbardziej oszczędny pod względem pamięci, ponieważ zwraca dane stopniowo i nie przechowuje ich wszystkich w pamięci naraz. +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'); @@ -357,13 +357,13 @@ foreach ($result as $row) { ``` .[note] -`ResultSet` można iterować tylko raz. Jeśli potrzebujesz iterować wielokrotnie, musisz najpierw załadować dane do tablicy, na przykład za pomocą metody `fetchAll()`. +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ż więcej wierszy, zwraca `null`. Przesuwa wewnętrzny wskaźnik na następny wiersz. +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'); @@ -391,7 +391,7 @@ foreach ($rows as $row) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Zwraca wyniki jako tablicę asocjacyjną. Pierwszy argument określa nazwę kolumny, która zostanie użyta jako klucz w tablicy, drugi argument określa nazwę kolumny, która zostanie użyta jako wartość: +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'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Jeśli podamy tylko pierwszy parametr, wartością będzie cały wiersz, czyli obiekt `Row`: +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żyta zostanie wartość z ostatniego wiersza. Przy użyciu `null` jako klucza, tablica będzie indeksowana numerycznie od zera (wtedy nie dochodzi do kolizji): +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'); @@ -417,14 +417,14 @@ $names = $result->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternatywnie możesz podać jako parametr callback, który dla każdego wiersza zwróci albo samą wartość, albo parę klucz-wartość. +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 również zwracać tablicę z parą klucz & wartość: +// 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, ...] ``` @@ -433,18 +433,18 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -Zwraca wartość pierwszej komórki z bieżącego wiersza. Jeśli nie ma już więcej wierszy, zwraca `null`. Przesuwa wewnętrzny wskaźnik na następny wiersz. +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 imię z pierwszego wiersza +$name = $result->fetchField(); // wczytuje name z pierwszego wiersza ``` fetchList(): ?array .[method] ----------------------------- -Zwraca wiersz jako tablicę indeksowaną. Jeśli nie ma już więcej wierszy, zwraca `null`. Przesuwa wewnętrzny wskaźnik na następny wiersz. +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'); @@ -455,7 +455,7 @@ $row = $result->fetchList(); // ['John', 'john@example.com'] getRowCount(): ?int .[method] ----------------------------- -Zwraca liczbę zmienionych wierszy przez ostatnie zapytanie `UPDATE` lub `DELETE`. Dla `SELECT` jest to liczba zwróconych wierszy, ale ta może nie być znana - w takim przypadku metoda zwróci `null`. +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] @@ -464,10 +464,10 @@ getColumnCount(): ?int .[method] Zwraca liczbę kolumn w `ResultSet`. -Informacje o zapytaniach -======================== +Informacje o zapytaniu +====================== -Do celów debugowania możemy uzyskać informacje o ostatnim wykonanym zapytaniu: +Na potrzeby debugowania możemy uzyskać informacje o ostatnio wykonanym zapytaniu: ```php echo $database->getLastQueryString(); // wypisuje zapytanie SQL @@ -477,21 +477,21 @@ echo $result->getQueryString(); // wypisuje zapytanie SQL echo $result->getTime(); // wypisuje czas wykonania w sekundach ``` -Do wyświetlenia wyniku jako tabeli HTML można użyć: +Żeby wyświetlić wynik jako tabelę HTML, możesz użyć: ```php $result = $database->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSet oferuje informacje o typach kolumn: +`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->type"; // np. 'id jest typu int' + echo "$column jest typu $type"; // np. 'id jest typu int' } ``` @@ -499,15 +499,15 @@ foreach ($types as $column => $type) { Logowanie zapytań ----------------- -Możemy zaimplementować własne logowanie zapytań. Zdarzenie `onQuery` jest tablicą callbacków, które są wywoływane po każdym wykonanym zapytaniu: +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('Zapytanie: ' . $result->getQueryString()); - $logger->info('Czas: ' . $result->getTime()); + $logger->info('Query: ' . $result->getQueryString()); + $logger->info('Time: ' . $result->getTime()); if ($result->getRowCount() > 1000) { - $logger->warning('Duży zestaw wyników: ' . $result->getRowCount() . ' wierszy'); + $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); } }; ``` diff --git a/database/pl/transactions.texy b/database/pl/transactions.texy index 37e8dde5cb..3c6d8243ad 100644 --- a/database/pl/transactions.texy +++ b/database/pl/transactions.texy @@ -2,9 +2,9 @@ Transakcje ********** .[perex] -Transakcje gwarantują, że albo wszystkie operacje w ramach transakcji zostaną wykonane, albo żadna z nich nie zostanie wykonana. Są one przydatne do zapewnienia spójności danych podczas bardziej złożonych operacji. +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 następująco: +Najprostszy sposób użycia transakcji wygląda tak: ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -Znacznie bardziej elegancko można to samo zapisać za pomocą metody `transaction()`. Jako parametr przyjmuje ona callback, który wykonuje w transakcji. Jeśli callback przebiegnie bez wyjątku, transakcja jest automatycznie zatwierdzana. Jeśli wystąpi wyjątek, transakcja jest anulowana (rollback), a wyjątek jest propagowany dalej. +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) { @@ -33,7 +33,9 @@ $database->transaction(function ($database) use ($id) { }); ``` -Metoda `transaction()` może również zwracać wartości: +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) { diff --git a/database/pl/type-conversion.texy b/database/pl/type-conversion.texy new file mode 100644 index 0000000000..a57a4cc47a --- /dev/null +++ b/database/pl/type-conversion.texy @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000000..a8241f91de --- /dev/null +++ b/database/pl/upgrading.texy @@ -0,0 +1,36 @@ +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/pt/@home.texy b/database/pt/@home.texy deleted file mode 100644 index dc4308eeae..0000000000 --- a/database/pt/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Bancos de dados suportados -========================== - -Nette suporta os seguintes bancos de dados: - -|* Servidor de banco de dados |* Nome DSN |* Suporte no Core |* Suporte no Explorer -| MySQL (>= 5.1) | mysql | SIM | SIM -| PostgreSQL (>= 9.0) | pgsql | SIM | SIM -| Sqlite 3 (>= 3.8) | sqlite | SIM | SIM -| Oracle | oci | SIM | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SIM | SIM -| MS SQL (PDO_DBLIB) | mssql | SIM | - -| ODBC | odbc | SIM | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database simplifica significativamente a obtenção de dados do banco de dados sem a necessidade de escrever consultas SQL. Ele faz consultas eficientes e não transfere dados desnecessários.}} diff --git a/database/pt/@left-menu.texy b/database/pt/@left-menu.texy deleted file mode 100644 index b1a45ab3c0..0000000000 --- a/database/pt/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Introdução |guide] -- [Acesso SQL |sql way] -- [Explorer |Explorer] -- [Transações |transactions] -- [Exceções |exceptions] -- [Reflexão |reflection] -- [Mapeamento |mapping] -- [Configuração |configuration] -- [Riscos de segurança |security] -- [Atualização |en:upgrading] diff --git a/database/pt/@meta.texy b/database/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/database/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/database/pt/configuration.texy b/database/pt/configuration.texy deleted file mode 100644 index b327ef196d..0000000000 --- a/database/pt/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Configuração do banco de dados -****************************** - -.[perex] -Visão geral das opções de configuração para Nette Database. - -Se você não estiver usando o framework completo, mas apenas esta biblioteca, leia [como carregar a configuração|bootstrap:]. - - -Conexão única -------------- - -Configuração de uma única conexão de banco de dados: - -```neon -database: - # DSN, a única chave obrigatória - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Cria os serviços `Nette\Database\Connection` e `Nette\Database\Explorer`, que geralmente passamos por [autowiring |dependency-injection:autowiring], ou por referência ao [seu nome |#Serviços DI]. - -Outras configurações: - -```neon -database: - # exibir o painel do banco de dados na Tracy Bar? - debugger: ... # (bool) padrão é true - - # exibir EXPLAIN das consultas na Tracy Bar? - explain: ... # (bool) padrão é true - - # permitir autowiring para esta conexão? - autowired: ... # (bool) padrão é true na primeira conexão - - # convenções de tabela: discovered, static ou nome da classe - conventions: discovered # (string) padrão é 'discovered' - - options: - # conectar ao banco de dados apenas quando necessário? - lazy: ... # (bool) padrão é false - - # classe PHP do driver do banco de dados - driverClass: # (string) - - # apenas MySQL: define sql_mode - sqlmode: # (string) - - # apenas MySQL: define SET NAMES - charset: # (string) padrão é 'utf8mb4' - - # apenas MySQL: converte TINYINT(1) para bool - convertBoolean: # (bool) padrão é false - - # retorna colunas de data como objetos imutáveis (desde a versão 3.2.1) - newDateTime: # (bool) padrão é false - - # apenas Oracle e SQLite: formato para armazenar data - formatDateTime: # (string) padrão é 'U' -``` - -Na chave `options`, você pode especificar outras opções encontradas na [documentação dos drivers PDO |https://www.php.net/manual/en/pdo.drivers.php], como por exemplo: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Múltiplas conexões ------------------- - -Na configuração, também podemos definir várias conexões de banco de dados dividindo-as em seções nomeadas: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -O autowiring está habilitado apenas para os serviços da primeira seção. Isso pode ser alterado usando `autowired: false` ou `autowired: true`. - - -Serviços DI ------------ - -Estes serviços são adicionados ao contêiner de DI, onde `###` representa o nome da conexão: - -| Nome | Tipo | Descrição -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | conexão com o banco de dados -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Se definirmos apenas uma conexão, os nomes dos serviços serão `database.default.connection` e `database.default.explorer`. Se definirmos várias conexões como no exemplo acima, os nomes corresponderão às seções, ou seja, `database.main.connection`, `database.main.explorer` e também `database.another.connection` e `database.another.explorer`. - -Passamos serviços não autowired explicitamente por referência ao seu nome: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/pt/exceptions.texy b/database/pt/exceptions.texy deleted file mode 100644 index 1882eaf2d8..0000000000 --- a/database/pt/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Exceções -******** - -O Nette Database utiliza uma hierarquia de exceções. A classe base é `Nette\Database\DriverException`, que herda de `PDOException` e fornece opções estendidas para trabalhar com erros do banco de dados: - -- O método `getDriverCode()` retorna o código de erro do driver do banco de dados -- O método `getSqlState()` retorna o código SQLSTATE -- Os métodos `getQueryString()` e `getParameters()` permitem obter a consulta original e os seus parâmetros - -As seguintes exceções especializadas herdam de `DriverException`: - -- `ConnectionException` - sinaliza falha na conexão com o servidor de banco de dados -- `ConstraintViolationException` - classe base para violação de restrições do banco de dados, da qual herdam: - - `ForeignKeyConstraintViolationException` - violação de chave estrangeira - - `NotNullConstraintViolationException` - violação da restrição NOT NULL - - `UniqueConstraintViolationException` - violação da unicidade do valor - - -Exemplo de captura da exceção `UniqueConstraintViolationException`, que ocorre quando tentamos inserir um utilizador com um e-mail que já existe no banco de dados (assumindo que a coluna `email` tenha um índice único). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Um utilizador com este e-mail já existe.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Ocorreu um erro durante o registo: ' . $e->getMessage(); -} -``` diff --git a/database/pt/explorer.texy b/database/pt/explorer.texy deleted file mode 100644 index 714a4d7719..0000000000 --- a/database/pt/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -O Explorer oferece uma forma intuitiva e eficiente de trabalhar com o banco de dados. Ele trata automaticamente das relações entre tabelas e da otimização de consultas, para que você possa se concentrar na sua aplicação. Funciona imediatamente sem configuração. Se precisar de controle total sobre as consultas SQL, pode utilizar o [acesso SQL |sql-way]. - -- O trabalho com dados é natural e fácil de entender -- Gera consultas SQL otimizadas que carregam apenas os dados necessários -- Permite acesso fácil a dados relacionados sem a necessidade de escrever consultas JOIN -- Funciona imediatamente sem qualquer configuração ou geração de entidades - -</div> - - -Começa-se com o Explorer chamando o método `table()` do objeto [api:Nette\Database\Explorer] (detalhes sobre a conexão podem ser encontrados no capítulo [Conexão e configuração |guide#Conexão e configuração]): - -```php -$books = $explorer->table('book'); // 'book' é o nome da tabela -``` - -O método retorna um objeto [Selection |api:Nette\Database\Table\Selection], que representa uma consulta SQL. A este objeto, podemos encadear outros métodos para filtrar e ordenar os resultados. A consulta é construída e executada apenas quando começamos a solicitar os dados, por exemplo, percorrendo um ciclo `foreach`. Cada linha é representada por um objeto [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // exibe a coluna 'title' - echo $book->author_id; // exibe a coluna 'author_id' -} -``` - -O Explorer facilita fundamentalmente o trabalho com [#relações entre tabelas]. O exemplo seguinte mostra como podemos facilmente exibir dados de tabelas relacionadas (livros e seus autores). Note que não precisamos escrever nenhuma consulta JOIN, o Nette cria-as por nós: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Livro: ' . $book->title; - echo 'Autor: ' . $book->author->name; // cria JOIN na tabela 'author' -} -``` - -O Nette Database Explorer otimiza as consultas para serem o mais eficientes possível. O exemplo acima executa apenas duas consultas SELECT, independentemente de estarmos a processar 10 ou 10 000 livros. - -Além disso, o Explorer monitoriza quais colunas são usadas no código e carrega do banco de dados apenas essas, economizando ainda mais desempenho. Este comportamento é totalmente automático e adaptativo. Se modificar o código posteriormente e começar a usar outras colunas, o Explorer ajustará automaticamente as consultas. Não precisa de configurar nada, nem pensar em quais colunas precisará - deixe isso para o Nette. - - -Filtragem e Ordenação -===================== - -A classe `Selection` fornece métodos para filtrar e ordenar a seleção de dados. - -.[language-php] -| `where($condition, ...$params)` | Adiciona uma condição WHERE. Múltiplas condições são unidas pelo operador AND -| `whereOr(array $conditions)` | Adiciona um grupo de condições WHERE unidas pelo operador OR -| `wherePrimary($value)` | Adiciona uma condição WHERE pela chave primária -| `order($columns, ...$params)` | Define a ordenação ORDER BY -| `select($columns, ...$params)` | Especifica as colunas que devem ser carregadas -| `limit($limit, $offset = null)` | Limita o número de linhas (LIMIT) e opcionalmente define OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Define a paginação -| `group($columns, ...$params)` | Agrupa linhas (GROUP BY) -| `having($condition, ...$params)` | Adiciona uma condição HAVING para filtrar linhas agrupadas - -Os métodos podem ser encadeados (a chamada [fluent interface |nette:introduction-to-object-oriented-programming#Interfaces Fluentes]): `$table->where(...)->order(...)->limit(...)`. - -Nestes métodos, também pode usar notação especial para aceder a [dados de tabelas relacionadas |#Consulta através de tabelas relacionadas]. - - -Escaping e Identificadores --------------------------- - -Os métodos escapam automaticamente os parâmetros e colocam aspas nos identificadores (nomes de tabelas e colunas), prevenindo assim a injeção de SQL. Para o funcionamento correto, é necessário seguir algumas regras: - -- Palavras-chave, nomes de funções, procedimentos, etc., escreva em **MAIÚSCULAS**. -- Nomes de colunas e tabelas escreva em **minúsculas**. -- Strings sempre insira através de **parâmetros**. - -```php -where('name = ' . $name); // VULNERABILIDADE CRÍTICA: injeção de SQL -where('name LIKE "%search%"'); // ERRADO: complica o quoting automático -where('name LIKE ?', '%search%'); // CORRETO: valor inserido via parâmetro - -where('name like ?', $name); // ERRADO: gera: `name` `like` ? -where('name LIKE ?', $name); // CORRETO: gera: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORRETO: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtra os resultados usando condições WHERE. A sua força reside no trabalho inteligente com diferentes tipos de valores e na escolha automática de operadores SQL. - -Uso básico: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Graças à deteção automática de operadores apropriados, não precisamos de lidar com vários casos especiais. O Nette resolve-os por nós: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// também é possível usar o placeholder de interrogação sem operador: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -O método também processa corretamente condições negativas e arrays vazios: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- não encontra nada -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- encontra tudo -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- encontra tudo -// $table->where('NOT id ?', $ids); Atenção - esta sintaxe não é suportada -``` - -Como parâmetro, também podemos passar o resultado de outra tabela - será criada uma subconsulta: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -As condições também podem ser passadas como um array, cujos itens são unidos por AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -No array, podemos usar pares chave => valor e o Nette escolherá novamente, de forma automática, os operadores corretos: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -No array, podemos combinar expressões SQL com placeholders de interrogação e múltiplos parâmetros. Isto é adequado para condições complexas com operadores definidos com precisão: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // dois parâmetros passados como array -]); -``` - -Chamadas múltiplas de `where()` unem automaticamente as condições com AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Semelhante a `where()`, adiciona condições, mas com a diferença de que as une usando OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Aqui também podemos usar expressões mais complexas: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Adiciona uma condição para a chave primária da tabela: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Se a tabela tiver uma chave primária composta (por exemplo, `foo_id`, `bar_id`), passamo-la como um array: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Determina a ordem em que as linhas serão retornadas. Podemos ordenar por uma ou mais colunas, em ordem ascendente ou descendente, ou por uma expressão personalizada: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Especifica as colunas que devem ser retornadas do banco de dados. Por padrão, o Nette Database Explorer retorna apenas as colunas que são realmente usadas no código. O método `select()` é, portanto, usado nos casos em que precisamos retornar expressões específicas: - -```php -// SELECT *, DATE_FORMAT(`created_at`, ?) AS formatted_date -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Os aliases definidos usando `AS` ficam então disponíveis como propriedades do objeto ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // acesso ao alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limita o número de linhas retornadas (LIMIT) e opcionalmente permite definir um offset: - -```php -$table->limit(10); // LIMIT 10 (retorna as primeiras 10 linhas) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Para paginação, é mais adequado usar o método `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilita a paginação dos resultados. Aceita o número da página (contado a partir de 1) e o número de itens por página. Opcionalmente, pode-se passar uma referência a uma variável na qual o número total de páginas será armazenado: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Total de páginas: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Agrupa linhas de acordo com as colunas especificadas (GROUP BY). É geralmente usado em conjunto com funções de agregação: - -```php -// Conta o número de produtos em cada categoria -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Define uma condição para filtrar linhas agrupadas (HAVING). Pode ser usado em conjunto com o método `group()` e funções de agregação: - -```php -// Encontra categorias que têm mais de 100 produtos -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Leitura de Dados -================ - -Para ler dados do banco de dados, temos vários métodos úteis disponíveis: - -.[language-php] -| `foreach ($table as $key => $row)` | Itera sobre todas as linhas, `$key` é o valor da chave primária, `$row` é o objeto ActiveRow -| `$row = $table->get($key)` | Retorna uma única linha pela chave primária -| `$row = $table->fetch()` | Retorna a linha atual e move o ponteiro para a próxima -| `$array = $table->fetchPairs()` | Cria um array associativo a partir dos resultados -| `$array = $table->fetchAll()` | Retorna todas as linhas como um array -| `count($table)` | Retorna o número de linhas no objeto Selection - -O objeto [ActiveRow |api:Nette\Database\Table\ActiveRow] destina-se apenas à leitura. Isto significa que não é possível alterar os valores das suas propriedades. Esta restrição garante a consistência dos dados e evita efeitos colaterais inesperados. Os dados são carregados do banco de dados e qualquer alteração deve ser feita explicitamente e de forma controlada. - - -`foreach` - Iteração Sobre Todas as Linhas ------------------------------------------- - -A forma mais fácil de executar uma consulta e obter linhas é iterando num ciclo `foreach`. Ele executa automaticamente a consulta SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key é o valor da chave primária, $book é ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Executa a consulta SQL e retorna a linha pela chave primária, ou `null` se não existir. - -```php -$book = $explorer->table('book')->get(123); // retorna ActiveRow com ID 123 ou null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Retorna uma linha e move o ponteiro interno para a próxima. Se não houver mais linhas, retorna `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Retorna os resultados como um array associativo. O primeiro argumento especifica o nome da coluna que será usada como chave no array, o segundo argumento especifica o nome da coluna que será usada como valor: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Se especificarmos apenas o primeiro parâmetro, o valor será a linha inteira, ou seja, o objeto `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Em caso de chaves duplicadas, o valor da última linha será usado. Ao usar `null` como chave, o array será indexado numericamente a partir de zero (neste caso, não ocorrem colisões): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativamente, pode fornecer um callback como parâmetro, que retornará para cada linha ou o próprio valor, ou um par chave-valor. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Primeiro livro (Jan Novák)', ...] - -// O callback também pode retornar um array com o par chave & valor: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Primeiro livro' => 'Jan Novák', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Retorna todas as linhas como um array associativo de objetos `ActiveRow`, onde as chaves são os valores das chaves primárias. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -O método `count()` sem parâmetro retorna o número de linhas no objeto `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativa -``` - -Atenção, `count()` com parâmetro executa a função de agregação COUNT no banco de dados. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Converte o objeto `ActiveRow` num array associativo, onde as chaves são os nomes das colunas e os valores são os dados correspondentes. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray será ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregação -========= - -A classe `Selection` fornece métodos para executar facilmente funções de agregação (COUNT, SUM, MIN, MAX, AVG, etc.). - -.[language-php] -| `count($expr)` | Conta o número de linhas -| `min($expr)` | Retorna o valor mínimo na coluna -| `max($expr)` | Retorna o valor máximo na coluna -| `sum($expr)` | Retorna a soma dos valores na coluna -| `aggregation($function)` | Permite executar qualquer função de agregação. Ex: `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Executa uma consulta SQL com a função COUNT e retorna o resultado. O método é usado para descobrir quantas linhas correspondem a uma determinada condição: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Atenção, `count()` sem parâmetro apenas retorna o número de linhas no objeto `Selection`, veja [#count()]. - - -min(string $expr) e max(string $expr) .[method] ------------------------------------------------ - -Os métodos `min()` e `max()` retornam o valor mínimo e máximo na coluna ou expressão especificada: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Retorna a soma dos valores na coluna ou expressão especificada: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- - -Permite executar qualquer função de agregação. - -```php -// preço médio dos produtos na categoria -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// concatena as tags do produto em uma única string -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Se precisarmos agregar resultados que já resultaram de alguma função de agregação e agrupamento (por exemplo, `SUM(valor)` sobre linhas agrupadas), como segundo argumento, especificamos a função de agregação que deve ser aplicada a esses resultados intermediários: - -```php -// Calcula o preço total dos produtos em estoque para categorias individuais e, em seguida, soma esses preços. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Neste exemplo, primeiro calculamos o preço total dos produtos em cada categoria (`SUM(price * stock) AS category_total`) e agrupamos os resultados por `category_id`. Em seguida, usamos `aggregation('SUM(category_total)', 'SUM')` para somar esses subtotais `category_total`. O segundo argumento `'SUM'` diz que a função SUM deve ser aplicada aos resultados intermediários. - - -Inserir, Atualizar & Excluir -============================ - -O Nette Database Explorer simplifica a inserção, atualização e exclusão de dados. Todos os métodos listados abaixo lançarão uma exceção `Nette\Database\DriverException` em caso de erro. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Insere novos registros na tabela. - -**Inserindo um único registro:** - -Passamos o novo registro como um array associativo ou objeto iterável (por exemplo, ArrayHash usado em [formulários |forms:]), onde as chaves correspondem aos nomes das colunas na tabela. - -Se a tabela tiver uma chave primária definida, o método retorna um objeto `ActiveRow`, que é recarregado do banco de dados para refletir quaisquer alterações feitas no nível do banco de dados (gatilhos, valores padrão de colunas, cálculos de colunas auto-increment). Isso garante a consistência dos dados e o objeto sempre contém os dados atuais do banco de dados. Se não houver uma chave primária única, ele retorna os dados passados na forma de um array. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row é uma instância de ActiveRow e contém os dados completos da linha inserida, -// incluindo o ID gerado automaticamente e quaisquer alterações feitas por gatilhos -echo $row->id; // Exibe o ID do usuário recém-inserido -echo $row->created_at; // Exibe a hora de criação, se definida por um gatilho -``` - -**Inserindo múltiplos registros de uma vez:** - -O método `insert()` permite inserir vários registros usando uma única consulta SQL. Neste caso, retorna o número de linhas inseridas. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows será 2 -``` - -Como parâmetro, também pode ser passado um objeto `Selection` com uma seleção de dados. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Inserindo valores especiais:** - -Como valores, também podemos passar arquivos, objetos DateTime ou literais SQL: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // converte para formato de banco de dados - 'avatar' => fopen('image.jpg', 'rb'), // insere o conteúdo binário do arquivo - 'uuid' => $explorer::literal('UUID()'), // chama a função UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Atualiza linhas na tabela de acordo com o filtro especificado. Retorna o número de linhas realmente alteradas. - -Passamos as colunas a serem alteradas como um array associativo ou objeto iterável (por exemplo, ArrayHash usado em [formulários |forms:]), onde as chaves correspondem aos nomes das colunas na tabela: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Para alterar valores numéricos, podemos usar os operadores `+=` e `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // aumenta o valor da coluna 'points' em 1 - 'coins-=' => 1, // diminui o valor da coluna 'coins' em 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Exclui linhas da tabela de acordo com o filtro especificado. Retorna o número de linhas excluídas. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Ao chamar `update()` e `delete()`, não se esqueça de especificar as linhas a serem modificadas/excluídas usando `where()`. Se `where()` não for usado, a operação será realizada em toda a tabela! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Atualiza os dados na linha do banco de dados representada pelo objeto `ActiveRow`. Como parâmetro, aceita um iterável com os dados a serem atualizados (as chaves são os nomes das colunas). Para alterar valores numéricos, podemos usar os operadores `+=` e `-=`: - -Após a execução da atualização, o `ActiveRow` é automaticamente recarregado do banco de dados para refletir quaisquer alterações feitas no nível do banco de dados (por exemplo, gatilhos). O método retorna true apenas se houve uma alteração real nos dados. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // aumentamos o número de visualizações -]); -echo $article->views; // Exibe o número atual de visualizações -``` - -Este método atualiza apenas uma linha específica no banco de dados. Para atualização em massa de várias linhas, use o método [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Exclui a linha do banco de dados representada pelo objeto `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Exclui o livro com ID 1 -``` - -Este método exclui apenas uma linha específica no banco de dados. Para exclusão em massa de várias linhas, use o método [#Selection::delete()]. - - -Relações entre tabelas -====================== - -Em bancos de dados relacionais, os dados são divididos em várias tabelas e interligados por chaves estrangeiras. O Nette Database Explorer traz uma maneira revolucionária de trabalhar com essas relações - sem escrever consultas JOIN e sem a necessidade de configurar ou gerar nada. - -Para ilustrar o trabalho com relações, usaremos o exemplo de um banco de dados de livros ([você pode encontrá-lo no GitHub |https://github.com/nette-examples/books]). No banco de dados, temos as tabelas: - -- `author` - escritores e tradutores (colunas `id`, `name`, `web`, `born`) -- `book` - livros (colunas `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - tags (colunas `id`, `name`) -- `book_tag` - tabela de ligação entre livros e tags (colunas `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Estrutura do banco de dados usada nos exemplos *** - -Em nosso exemplo de banco de dados de livros, encontramos vários tipos de relacionamentos (embora o modelo seja simplificado em comparação com a realidade): - -- Um-para-muitos 1:N – cada livro **tem um** autor, um autor pode escrever **vários** livros -- Zero-para-muitos 0:N – um livro **pode ter** um tradutor, um tradutor pode traduzir **vários** livros -- Zero-para-um 0:1 – um livro **pode ter** uma sequência -- Muitos-para-muitos M:N – um livro **pode ter várias** tags e uma tag pode ser atribuída a **vários** livros - -Nesses relacionamentos, sempre existe uma tabela pai e uma tabela filho. Por exemplo, no relacionamento entre autor e livro, a tabela `author` é a pai e `book` é a filho - podemos imaginar que o livro sempre "pertence" a algum autor. Isso também se reflete na estrutura do banco de dados: a tabela filho `book` contém a chave estrangeira `author_id`, que referencia a tabela pai `author`. - -Se precisarmos listar os livros incluindo os nomes de seus autores, temos duas opções. Ou obtemos os dados com uma única consulta SQL usando JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Ou carregamos os dados em duas etapas - primeiro os livros e depois seus autores - e depois os montamos em PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- ids dos autores dos livros obtidos -``` - -A segunda abordagem é, na verdade, mais eficiente, embora possa ser surpreendente. Os dados são carregados apenas uma vez e podem ser melhor utilizados no cache. É precisamente desta forma que o Nette Database Explorer funciona - ele resolve tudo nos bastidores e oferece uma API elegante: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'título: ' . $book->title; - echo 'escrito por: ' . $book->author->name; // $book->author é o registro da tabela 'author' - echo 'traduzido por: ' . $book->translator?->name; -} -``` - - -Acesso à tabela pai -------------------- - -O acesso à tabela pai é direto. Trata-se de relacionamentos como *livro tem um autor* ou *livro pode ter um tradutor*. Obtemos o registro relacionado através da propriedade do objeto ActiveRow - seu nome corresponde ao nome da coluna com a chave estrangeira sem `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // encontra o autor pela coluna author_id -echo $book->translator?->name; // encontra o tradutor pela coluna translator_id -``` - -Quando acessamos a propriedade `$book->author`, o Explorer procura na tabela `book` por uma coluna cujo nome contenha a string `author` (ou seja, `author_id`). Com base no valor nesta coluna, ele carrega o registro correspondente da tabela `author` e o retorna como `ActiveRow`. Da mesma forma funciona `$book->translator`, que usa a coluna `translator_id`. Como a coluna `translator_id` pode conter `null`, usamos o operador `?->` no código. - -Um caminho alternativo é oferecido pelo método `ref()`, que aceita dois argumentos, o nome da tabela de destino e o nome da coluna de ligação, e retorna uma instância de `ActiveRow` ou `null`: - -```php -echo $book->ref('author', 'author_id')->name; // relação com o autor -echo $book->ref('author', 'translator_id')->name; // relação com o tradutor -``` - -O método `ref()` é útil se o acesso via propriedade não puder ser usado porque a tabela contém uma coluna com o mesmo nome (ou seja, `author`). Nos outros casos, recomenda-se usar o acesso via propriedade, que é mais legível. - -O Explorer otimiza automaticamente as consultas ao banco de dados. Quando percorremos os livros em um loop e acessamos seus registros relacionados (autores, tradutores), o Explorer não gera uma consulta para cada livro separadamente. Em vez disso, ele executa apenas um SELECT para cada tipo de relacionamento, reduzindo significativamente a carga no banco de dados. Por exemplo: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Este código chamará apenas estas três consultas rápidas ao banco de dados: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id da coluna author_id dos livros selecionados -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id da coluna translator_id dos livros selecionados -``` - -.[note] -A lógica para encontrar a coluna de ligação é dada pela implementação de [Conventions |api:Nette\Database\Conventions]. Recomendamos o uso de [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analisa as chaves estrangeiras e permite trabalhar facilmente com os relacionamentos existentes entre as tabelas. - - -Acesso à tabela filho ---------------------- - -O acesso à tabela filho funciona na direção oposta. Agora perguntamos *quais livros este autor escreveu* ou *este tradutor traduziu*. Para este tipo de consulta, usamos o método `related()`, que retorna uma `Selection` com os registros relacionados. Vejamos um exemplo: - -```php -$author = $explorer->table('author')->get(1); - -// Exibe todos os livros do autor -foreach ($author->related('book.author_id') as $book) { - echo "Escreveu: $book->title"; -} - -// Exibe todos os livros que o autor traduziu -foreach ($author->related('book.translator_id') as $book) { - echo "Traduziu: $book->title"; -} -``` - -O método `related()` aceita a descrição da ligação como um único argumento com notação de ponto ou como dois argumentos separados: - -```php -$author->related('book.translator_id'); // um argumento -$author->related('book', 'translator_id'); // dois argumentos -``` - -O Explorer pode detectar automaticamente a coluna de ligação correta com base no nome da tabela pai. Neste caso, a ligação é feita através da coluna `book.author_id`, porque o nome da tabela de origem é `author`: - -```php -$author->related('book'); // usa book.author_id -``` - -Se existissem várias ligações possíveis, o Explorer lançaria uma exceção [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -O método `related()` pode, obviamente, ser usado também ao percorrer vários registros em um loop, e o Explorer, neste caso, também otimiza automaticamente as consultas: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' escreveu:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Este código gerará apenas duas consultas SQL rápidas: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id dos autores selecionados -``` - - -Relacionamento Muitos-para-Muitos ---------------------------------- - -Para o relacionamento muitos-para-muitos (M:N), é necessária a existência de uma tabela de ligação (no nosso caso `book_tag`), que contém duas colunas com chaves estrangeiras (`book_id`, `tag_id`). Cada uma dessas colunas referencia a chave primária de uma das tabelas interligadas. Para obter os dados relacionados, primeiro obtemos os registros da tabela de ligação usando `related('book_tag')` e, em seguida, prosseguimos para os dados de destino: - -```php -$book = $explorer->table('book')->get(1); -// exibe os nomes das tags atribuídas ao livro -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // exibe o nome da tag através da tabela de ligação -} - -$tag = $explorer->table('tag')->get(1); -// ou o inverso: exibe os nomes dos livros marcados com esta tag -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // exibe o nome do livro -} -``` - -O Explorer novamente otimiza as consultas SQL para uma forma eficiente: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id dos livros selecionados -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id das tags encontradas em book_tag -``` - - -Consulta através de tabelas relacionadas ----------------------------------------- - -Nos métodos `where()`, `select()`, `order()` e `group()`, podemos usar notações especiais para acessar colunas de outras tabelas. O Explorer cria automaticamente os JOINs necessários. - -**Notação de ponto** (`tabela_pai.coluna`) é usada para o relacionamento 1:N do ponto de vista da tabela filho: - -```php -$books = $explorer->table('book'); - -// Encontra livros cujo autor tem nome começando com 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Ordena os livros pelo nome do autor em ordem decrescente -$books->order('author.name DESC'); - -// Exibe o título do livro e o nome do autor -$books->select('book.title, author.name'); -``` - -**Notação de dois pontos** (`:tabela_filho.coluna`) é usada para o relacionamento 1:N do ponto de vista da tabela pai: - -```php -$authors = $explorer->table('author'); - -// Encontra autores que escreveram um livro com 'PHP' no título -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Conta o número de livros para cada autor -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -No exemplo acima com notação de dois pontos (`:book.title`), a coluna com a chave estrangeira não é especificada. O Explorer detecta automaticamente a coluna correta com base no nome da tabela pai. Neste caso, a ligação é feita através da coluna `book.author_id`, porque o nome da tabela de origem é `author`. Se existissem várias ligações possíveis, o Explorer lançaria uma exceção [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -A coluna de ligação pode ser explicitamente especificada entre parênteses: - -```php -// Encontra autores que traduziram um livro com 'PHP' no título -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -As notações podem ser encadeadas para acesso através de múltiplas tabelas: - -```php -// Encontra autores de livros marcados com a tag 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Extensão de condições para JOIN -------------------------------- - -O método `joinWhere()` estende as condições que são especificadas ao ligar tabelas em SQL após a palavra-chave `ON`. - -Digamos que queremos encontrar livros traduzidos por um tradutor específico: - -```php -// Encontra livros traduzidos pelo tradutor chamado 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -Na condição `joinWhere()`, podemos usar as mesmas construções que no método `where()` - operadores, placeholders de interrogação, arrays de valores ou expressões SQL. - -Para consultas mais complexas com múltiplos JOINs, podemos definir aliases de tabela: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Observe que, enquanto o método `where()` adiciona condições à cláusula `WHERE`, o método `joinWhere()` estende as condições na cláusula `ON` ao ligar tabelas. diff --git a/database/pt/guide.texy b/database/pt/guide.texy deleted file mode 100644 index 366588d064..0000000000 --- a/database/pt/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database é uma camada de banco de dados poderosa e elegante para PHP com ênfase na simplicidade e recursos inteligentes. Oferece duas formas de trabalhar com o banco de dados - [Explorer |explorer] para desenvolvimento rápido de aplicações, ou [Acesso SQL |sql-way] para trabalho direto com consultas. - -<div class="grid gap-3"> -<div> - - -[Acesso SQL |sql-way] -===================== -- Consultas parametrizadas seguras -- Controle preciso sobre a forma das consultas SQL -- Quando você escreve consultas complexas com recursos avançados -- Otimiza o desempenho usando funções SQL específicas - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Desenvolve rapidamente sem escrever SQL -- Trabalho intuitivo com relações entre tabelas -- Você apreciará a otimização automática de consultas -- Adequado para trabalho rápido e confortável com o banco de dados - -</div> - -</div> - - -Instalação -========== - -A biblioteca pode ser baixada e instalada usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Bancos de dados suportados -========================== - -Nette Database suporta os seguintes bancos de dados: - -|* Servidor de banco de dados |* Nome DSN |* Suporte no Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | SIM -| PostgreSQL (>= 9.0) | pgsql | SIM -| Sqlite 3 (>= 3.8) | sqlite | SIM -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SIM -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Duas abordagens ao banco de dados -================================= - -Nette Database oferece uma escolha: você pode escrever consultas SQL diretamente (acesso SQL) ou deixá-las serem geradas automaticamente (Explorer). Vejamos como ambas as abordagens resolvem as mesmas tarefas: - -[Acesso SQL|sql-way] - Consultas SQL - -```php -// inserção de registro -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obtenção de registros: autores de livros -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// listagem (não otimizada, gera N consultas adicionais) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Autor $author->name escreveu $author->books_count livros:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Acesso Explorer|explorer] - Geração automática de SQL - -```php -// inserção de registro -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obtenção de registros: autores de livros -$authors = $database->table('authors') - ->where('active', 1); - -// listagem (gera automaticamente apenas 2 consultas otimizadas) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Autor $author->name escreveu {$books->count()} livros:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -A abordagem Explorer gera e otimiza consultas SQL automaticamente. No exemplo fornecido, a abordagem SQL gera N+1 consultas (uma para os autores e depois uma para os livros de cada autor), enquanto o Explorer otimiza automaticamente as consultas e executa apenas duas - uma para os autores e uma para todos os seus livros. - -Ambas as abordagens podem ser combinadas livremente na aplicação conforme necessário. - - -Conexão e configuração -====================== - -Para conectar ao banco de dados, basta criar uma instância da classe [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -O parâmetro `$dsn` (data source name) é o mesmo [que o PDO usa |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], por exemplo, `host=127.0.0.1;dbname=test`. Em caso de falha, lança a exceção `Nette\Database\ConnectionException`. - -No entanto, uma maneira mais conveniente é oferecida pela [configuração da aplicação |configuration], onde basta adicionar a seção `database` e os objetos necessários serão criados, assim como o painel do banco de dados na barra [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Depois, o objeto de conexão [pode ser obtido como um serviço do contêiner DI |dependency-injection:passing-dependencies], por exemplo: - -```php -class Model -{ - public function __construct( - // ou Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Mais informações sobre a [configuração do banco de dados|configuration]. - - -Criação manual do Explorer --------------------------- - -Se você não usa o contêiner Nette DI, pode criar a instância `Nette\Database\Explorer` manualmente: - -```php -// conexão com o banco de dados -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// armazenamento para cache, implementa Nette\Caching\Storage, por exemplo: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// cuida da reflexão da estrutura do banco de dados -$structure = new Nette\Database\Structure($connection, $storage); -// define regras para mapear nomes de tabelas, colunas e chaves estrangeiras -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Gerenciamento de conexão -======================== - -Ao criar o objeto `Connection`, a conexão é estabelecida automaticamente. Se você deseja adiar a conexão, use o modo lazy - ative-o na [configuração|configuration] definindo `lazy` como `true`, ou assim: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Para gerenciar a conexão, use os métodos `connect()`, `disconnect()` e `reconnect()`. -- `connect()` cria a conexão se ela ainda não existir, podendo lançar a exceção `Nette\Database\ConnectionException`. -- `disconnect()` desconecta a conexão atual com o banco de dados. -- `reconnect()` desconecta e, em seguida, reconecta ao banco de dados. Este método também pode lançar a exceção `Nette\Database\ConnectionException`. - -Além disso, você pode monitorar eventos relacionados à conexão usando o evento `onConnect`, que é um array de callbacks chamados após o estabelecimento da conexão com o banco de dados. - -```php -// ocorre após a conexão com o banco de dados -$database->onConnect[] = function($database) { - echo "Conectado ao banco de dados"; -}; -``` - - -Tracy Debug Bar -=============== - -Se você usa [Tracy |tracy:], o painel Database é ativado automaticamente na Debug Bar, exibindo todas as consultas executadas, seus parâmetros, tempo de execução e o local no código onde foram chamadas. - -[* db-panel.webp *] diff --git a/database/pt/mapping.texy b/database/pt/mapping.texy deleted file mode 100644 index d6c415f5ed..0000000000 --- a/database/pt/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversão de tipos -****************** - -.[perex] -Nette Database converte automaticamente os valores retornados do banco de dados para os tipos PHP correspondentes. - - -Data e hora ------------ - -Os dados de tempo são convertidos em objetos `Nette\Utils\DateTime`. Se você deseja que os dados de tempo sejam convertidos em objetos imutáveis `Nette\Database\DateTime`, defina a opção `newDateTime` como `true` na [configuração|configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -No caso do MySQL, o tipo de dados `TIME` é convertido em objetos `DateInterval`. - - -Valores booleanos ------------------ - -Os valores booleanos são automaticamente convertidos para `true` ou `false`. No MySQL, `TINYINT(1)` é convertido se definirmos `convertBoolean` como `true` na [configuração|configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Valores numéricos ------------------ - -Os valores numéricos são convertidos para `int` ou `float` de acordo com o tipo da coluna no banco de dados: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalização personalizada --------------------------- - -Usando o método `setRowNormalizer(?callable $normalizer)`, você pode definir uma função personalizada para transformar linhas do banco de dados. Isso é útil, por exemplo, para a conversão automática de tipos de dados. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // a conversão de tipos ocorre aqui - return $row; -}); -``` diff --git a/database/pt/reflection.texy b/database/pt/reflection.texy deleted file mode 100644 index fba8302010..0000000000 --- a/database/pt/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Reflexão da estrutura -********************* - -.{data-version:3.2.1} -Nette Database fornece ferramentas para introspecção da estrutura do banco de dados usando a classe [api:Nette\Database\Reflection]. Ela permite obter informações sobre tabelas, colunas, índices e chaves estrangeiras. Você pode usar a reflexão para gerar esquemas, criar aplicações flexíveis que trabalham com o banco de dados ou ferramentas gerais de banco de dados. - -Obtemos o objeto de reflexão da instância de conexão com o banco de dados: - -```php -$reflection = $database->getReflection(); -``` - - -Obtenção de tabelas -------------------- - -A propriedade readonly `$reflection->tables` contém um array associativo de todas as tabelas no banco de dados: - -```php -// Listagem dos nomes de todas as tabelas -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Existem mais dois métodos disponíveis: - -```php -// Verificação da existência da tabela -if ($reflection->hasTable('users')) { - echo "A tabela users existe"; -} - -// Retorna o objeto da tabela; se não existir, lança uma exceção -$table = $reflection->getTable('users'); -``` - - -Informações sobre a tabela --------------------------- - -A tabela é representada pelo objeto [Table|api:Nette\Database\Reflection\Table], que fornece as seguintes propriedades readonly: - -- `$name: string` – nome da tabela -- `$view: bool` – se é uma view -- `$fullName: ?string` – nome completo da tabela incluindo o esquema (se existir) -- `$columns: array<string, Column>` – array associativo das colunas da tabela -- `$indexes: Index[]` – array de índices da tabela -- `$primaryKey: ?Index` – chave primária da tabela ou null -- `$foreignKeys: ForeignKey[]` – array de chaves estrangeiras da tabela - - -Colunas -------- - -A propriedade `columns` da tabela fornece um array associativo de colunas, onde a chave é o nome da coluna e o valor é uma instância de [Column|api:Nette\Database\Reflection\Column] com estas propriedades: - -- `$name: string` – nome da coluna -- `$table: ?Table` – referência à tabela da coluna -- `$nativeType: string` – tipo de dados nativo do banco de dados -- `$size: ?int` – tamanho/comprimento do tipo -- `$nullable: bool` – se a coluna pode conter NULL -- `$default: mixed` – valor padrão da coluna -- `$autoIncrement: bool` – se a coluna é auto-increment -- `$primary: bool` – se faz parte da chave primária -- `$vendor: array` – metadados adicionais específicos do sistema de banco de dados - -```php -foreach ($table->columns as $name => $column) { - echo "Coluna: $name\n"; - echo "Tipo: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Sim' : 'Não') . "\n"; -} -``` - - -Índices -------- - -A propriedade `indexes` da tabela fornece um array de índices, onde cada índice é uma instância de [Index|api:Nette\Database\Reflection\Index] com estas propriedades: - -- `$columns: Column[]` – array de colunas que formam o índice -- `$unique: bool` – se o índice é único -- `$primary: bool` – se é a chave primária -- `$name: ?string` – nome do índice - -A chave primária da tabela pode ser obtida usando a propriedade `primaryKey`, que retorna ou um objeto `Index`, ou `null` caso a tabela não tenha chave primária. - -```php -// Listagem de índices -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Índice" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Colunas: $columns\n"; - echo " Unique: " . ($index->unique ? 'Sim' : 'Não') . "\n"; -} - -// Listagem da chave primária -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Chave primária: $columns\n"; -} -``` - - -Chaves estrangeiras -------------------- - -A propriedade `foreignKeys` da tabela fornece um array de chaves estrangeiras, onde cada chave estrangeira é uma instância de [ForeignKey|api:Nette\Database\Reflection\ForeignKey] com estas propriedades: - -- `$foreignTable: Table` – tabela referenciada -- `$localColumns: Column[]` – array de colunas locais -- `$foreignColumns: Column[]` – array de colunas referenciadas -- `$name: ?string` – nome da chave estrangeira - -```php -// Listagem de chaves estrangeiras -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/pt/security.texy b/database/pt/security.texy deleted file mode 100644 index e225154bed..0000000000 --- a/database/pt/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Riscos de segurança -******************* - -<div class=perex> - -O banco de dados frequentemente contém dados sensíveis e permite a execução de operações perigosas. Para trabalhar com segurança com Nette Database, é crucial: - -- Compreender a diferença entre API segura e insegura -- Usar consultas parametrizadas -- Validar corretamente os dados de entrada - -</div> - - -O que é SQL Injection? -====================== - -SQL injection é o risco de segurança mais grave ao trabalhar com um banco de dados. Ocorre quando uma entrada não tratada do usuário se torna parte de uma consulta SQL. Um invasor pode inserir seus próprios comandos SQL e, assim: -- Obter acesso não autorizado aos dados -- Modificar ou excluir dados no banco de dados -- Contornar a autenticação - -```php -// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// O invasor pode inserir, por exemplo, o valor: ' OR '1'='1 -// A consulta resultante será: SELECT * FROM users WHERE name = '' OR '1'='1' -// O que retorna todos os usuários -``` - -O mesmo se aplica ao Database Explorer: - -```php -// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Consultas parametrizadas -======================== - -A defesa básica contra SQL injection são as consultas parametrizadas. Nette Database oferece várias maneiras de usá-las. - -A maneira mais simples é usar **placeholders de interrogação**: - -```php -// ✅ Consulta parametrizada segura -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Condição segura no Explorer -$table->where('name = ?', $name); -``` - -Isso se aplica a todos os outros métodos no [Database Explorer|explorer] que permitem inserir expressões com placeholders de interrogação e parâmetros. - -Para comandos INSERT, UPDATE ou a cláusula WHERE, podemos passar valores em um array: - -```php -// ✅ INSERT seguro -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ INSERT seguro no Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validação dos valores dos parâmetros -==================================== - -Consultas parametrizadas são o pilar fundamental do trabalho seguro com bancos de dados. No entanto, os valores que inserimos nelas devem passar por vários níveis de verificação: - - -Verificação de tipo -------------------- - -**O mais importante é garantir o tipo de dados correto dos parâmetros** - esta é uma condição necessária para o uso seguro do Nette Database. O banco de dados assume que todos os dados de entrada têm o tipo de dados correto correspondente à coluna específica. - -Por exemplo, se `$name` nos exemplos anteriores fosse inesperadamente um array em vez de uma string, o Nette Database tentaria inserir todos os seus elementos na consulta SQL, o que levaria a um erro. Portanto, **nunca use** dados não validados de `$_GET`, `$_POST` ou `$_COOKIE` diretamente em consultas de banco de dados. - - -Verificação de formato ----------------------- - -No segundo nível, verificamos o formato dos dados - por exemplo, se as strings estão na codificação UTF-8 e seu comprimento corresponde à definição da coluna, ou se os valores numéricos estão dentro do intervalo permitido para o tipo de dados da coluna. - -Neste nível de validação, podemos confiar parcialmente no próprio banco de dados - muitos bancos de dados rejeitarão dados inválidos. No entanto, o comportamento pode variar, alguns podem truncar silenciosamente strings longas ou cortar números fora do intervalo. - - -Verificação de domínio ----------------------- - -O terceiro nível representa verificações lógicas específicas da sua aplicação. Por exemplo, verificar se os valores das caixas de seleção correspondem às opções oferecidas, se os números estão no intervalo esperado (por exemplo, idade 0-150 anos) ou se as dependências mútuas entre os valores fazem sentido. - - -Métodos de validação recomendados ---------------------------------- - -- Use [Nette Forms |forms:], que garantem automaticamente a validação correta de todas as entradas -- Use [Presenters |application:] e especifique os tipos de dados para os parâmetros nos métodos `action*()` e `render*()` -- Ou implemente sua própria camada de validação usando ferramentas PHP padrão como `filter_var()` - - -Trabalho seguro com colunas -=========================== - -Na seção anterior, mostramos como validar corretamente os valores dos parâmetros. No entanto, ao usar arrays em consultas SQL, devemos prestar a mesma atenção às suas chaves. - -```php -// ❌ CÓDIGO PERIGOSO - as chaves no array não são tratadas -$database->query('INSERT INTO users', $_POST); -``` - -Para comandos INSERT e UPDATE, isso é uma falha de segurança crítica - um invasor pode inserir ou alterar qualquer coluna no banco de dados. Ele poderia, por exemplo, definir `is_admin = 1` ou inserir dados arbitrários em colunas sensíveis (a chamada Mass Assignment Vulnerability). - -Nas condições WHERE, é ainda mais perigoso, pois podem conter operadores: - -```php -// ❌ CÓDIGO PERIGOSO - as chaves no array não são tratadas -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// executa a consulta WHERE (`salary` > 100000) -``` - -Um invasor pode usar essa abordagem para descobrir sistematicamente os salários dos funcionários. Ele pode começar, por exemplo, com uma consulta por salários acima de 100.000, depois abaixo de 50.000 e, estreitando gradualmente o intervalo, pode revelar os salários aproximados de todos os funcionários. Esse tipo de ataque é chamado de SQL enumeration. - -Os métodos `where()` e `whereOr()` são ainda [muito mais flexíveis |explorer#where] e suportam expressões SQL, incluindo operadores e funções, nas chaves e valores. Isso dá ao invasor a possibilidade de realizar SQL injection: - -```php -// ❌ CÓDIGO PERIGOSO - o invasor pode inserir seu próprio SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// executa a consulta WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Este ataque encerra a condição original com `0)`, anexa seu próprio `SELECT` usando `UNION` para obter dados sensíveis da tabela `users` e fecha a consulta sintaticamente correta com `WHERE (1)`. - - -Whitelist de colunas --------------------- - -Para trabalhar com segurança com nomes de colunas, precisamos de um mecanismo que garanta que o usuário só possa trabalhar com colunas permitidas e não possa adicionar as suas próprias. Poderíamos tentar detectar e bloquear nomes de colunas perigosos (blacklist), mas essa abordagem não é confiável - um invasor sempre pode encontrar uma nova maneira de escrever um nome de coluna perigoso que não previmos. - -Portanto, é muito mais seguro inverter a lógica e definir uma lista explícita de colunas permitidas (whitelist): - -```php -// Colunas que o usuário pode editar -$allowedColumns = ['name', 'email', 'active']; - -// Removemos todas as colunas não permitidas da entrada -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Agora podemos usar com segurança em consultas, como por exemplo: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Identificadores dinâmicos -========================= - -Para nomes dinâmicos de tabelas e colunas, use o placeholder `?name`. Isso garante o escape correto dos identificadores de acordo com a sintaxe do banco de dados específico (por exemplo, usando crases no MySQL): - -```php -// ✅ Uso seguro de identificadores confiáveis -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Resultado no MySQL: SELECT `name` FROM `users` -``` - -Importante: use o símbolo `?name` apenas para valores confiáveis definidos no código da aplicação. Para valores do usuário, use novamente a [whitelist |#Whitelist de colunas]. Caso contrário, você se expõe a riscos de segurança: - -```php -// ❌ PERIGOSO - nunca use entrada do usuário -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/pt/sql-way.texy b/database/pt/sql-way.texy deleted file mode 100644 index 11652311b3..0000000000 --- a/database/pt/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Acesso SQL -********** - -.[perex] -A Nette Database oferece dois caminhos: você pode escrever consultas SQL você mesmo (acesso SQL), ou deixá-las serem geradas automaticamente (veja [Explorer |explorer]). O acesso SQL dá a você controle total sobre as consultas, garantindo ao mesmo tempo sua construção segura. - -.[note] -Detalhes sobre conexão e configuração do banco de dados podem ser encontrados no capítulo [Conexão e configuração |guide#Conexão e configuração]. - - -Consultas básicas -================= - -Para consultar o banco de dados, use o método `query()`. Ele retorna um objeto [ResultSet |api:Nette\Database\ResultSet], que representa o resultado da consulta. Em caso de falha, o método [lança uma exceção|exceptions]. Podemos percorrer o resultado da consulta usando um loop `foreach`, ou usar uma das [funções auxiliares |#Obtenção de dados]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Para inserir valores com segurança em consultas SQL, usamos consultas parametrizadas. A Nette Database torna isso o mais simples possível - basta adicionar uma vírgula e o valor após a consulta SQL: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Com múltiplos parâmetros, você tem duas opções de escrita. Você pode "intercalar" a consulta SQL com parâmetros: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Ou escrever a consulta SQL inteira primeiro e depois anexar todos os parâmetros: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Proteção contra SQL injection -============================= - -Por que é importante usar consultas parametrizadas? Porque elas protegem você contra um ataque chamado SQL injection, no qual um invasor poderia injetar seus próprios comandos SQL e, assim, obter ou danificar dados no banco de dados. - -.[warning] -**Nunca insira variáveis diretamente na consulta SQL!** Sempre use consultas parametrizadas, que protegem você contra SQL injection. - -```php -// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Consulta parametrizada segura -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Familiarize-se com os [possíveis riscos de segurança |security]. - - -Técnicas de consulta -==================== - - -Condições WHERE ---------------- - -As condições WHERE podem ser escritas como um array associativo, onde as chaves são os nomes das colunas e os valores são os dados para comparação. A Nette Database seleciona automaticamente o operador SQL mais apropriado com base no tipo de valor. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Na chave, você também pode especificar explicitamente o operador para comparação: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // usa o operador > - 'name LIKE' => '%John%', // usa o operador LIKE - 'email NOT LIKE' => '%example.com%', // usa o operador NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette trata automaticamente casos especiais como valores `null` ou arrays. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // usa o operador = - 'category_id' => [1, 2, 3], // usa IN - 'description' => null, // usa IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Para condições negativas, use o operador `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // usa o operador <> - 'category_id NOT' => [1, 2, 3], // usa NOT IN - 'description NOT' => null, // usa IS NOT NULL - 'id' => [], // será omitido -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Para combinar condições, o operador `AND` é usado. Isso pode ser alterado usando o [placeholder ?or |#Dicas para construir SQL]. - - -Regras ORDER BY ---------------- - -A ordenação `ORDER BY` pode ser escrita usando um array. Nas chaves, especificamos as colunas e o valor será um booleano indicando se a ordenação é ascendente: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // ascendente - 'name' => false, // descendente -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Inserção de dados (INSERT) --------------------------- - -Para inserir registros, usa-se o comando SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -O método `getInsertId()` retorna o ID da última linha inserida. Em alguns bancos de dados (por exemplo, PostgreSQL), é necessário especificar como parâmetro o nome da sequência da qual o ID deve ser gerado usando `$database->getInsertId($sequenceId)`. - -Como parâmetros, também podemos passar [#valores especiais] como arquivos, objetos DateTime ou tipos enumerados. - -Inserção de múltiplos registros de uma vez: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Um INSERT múltiplo é muito mais rápido porque uma única consulta ao banco de dados é executada, em vez de muitas individuais. - -**Aviso de segurança:** Nunca use dados não validados como `$values`. Familiarize-se com os [possíveis riscos |security#Trabalho seguro com colunas]. - - -Atualização de dados (UPDATE) ------------------------------ - -Para atualizar registros, usa-se o comando SQL `UPDATE`. - -```php -// Atualização de um único registro -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -O número de linhas afetadas é retornado por `$result->getRowCount()`. - -Para UPDATE, podemos usar os operadores `+=` e `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrementa login_count -], 1); -``` - -Exemplo de inserção ou atualização de um registro, se ele já existir. Usamos a técnica `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Observe que a Nette Database reconhece em qual contexto do comando SQL o parâmetro com o array é inserido e constrói o código SQL a partir dele de acordo. Assim, do primeiro array, ele construiu `(id, name, year) VALUES (123, 'Jim', 1978)`, enquanto o segundo foi convertido para a forma `name = 'Jim', year = 1978`. Discutimos isso em mais detalhes na seção [#Dicas para construir SQL]. - - -Exclusão de dados (DELETE) --------------------------- - -Para excluir registros, usa-se o comando SQL `DELETE`. Exemplo com obtenção do número de linhas excluídas: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Dicas para construir SQL ------------------------- - -Uma dica é um placeholder especial na consulta SQL que diz como o valor do parâmetro deve ser reescrito em uma expressão SQL: - -| Dica | Descrição | Usado automaticamente -|-----------|-------------------------------------------------|----------------------------- -| `?name` | usa para inserir nome da tabela ou coluna | - -| `?values` | gera `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | gera atribuição `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | combina condições no array com o operador `AND` | `WHERE ?`, `HAVING ?` -| `?or` | combina condições no array com o operador `OR` | - -| `?order` | gera a cláusula `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Para inserir dinamicamente nomes de tabelas e colunas na consulta, use o placeholder `?name`. A Nette Database cuida do tratamento correto dos identificadores de acordo com as convenções do banco de dados específico (por exemplo, envolvendo em crases no MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (no MySQL) -``` - -**Aviso:** use o símbolo `?name` apenas para nomes de tabelas e colunas de entradas validadas, caso contrário, você se expõe a um [risco de segurança |security#Identificadores dinâmicos]. - -Outras dicas geralmente não precisam ser especificadas, pois Nette usa detecção automática inteligente ao montar a consulta SQL (veja a terceira coluna da tabela). Mas você pode usá-la, por exemplo, em uma situação em que deseja combinar condições usando `OR` em vez de `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Valores especiais ------------------ - -Além dos tipos escalares comuns (string, int, bool), você também pode passar valores especiais como parâmetros: - -- arquivos: `fopen('image.gif', 'r')` insere o conteúdo binário do arquivo -- data e hora: objetos `DateTime` são convertidos para o formato do banco de dados -- tipos enumerados: instâncias `enum` são convertidas para seu valor -- literais SQL: criados com `Connection::literal('NOW()')` são inseridos diretamente na consulta - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Para bancos de dados que não têm suporte nativo para o tipo de dados `datetime` (como SQLite e Oracle), `DateTime` é convertido para o valor especificado na [configuração do banco de dados|configuration] pelo item `formatDateTime` (o valor padrão é `U` - timestamp Unix). - - -Literais SQL ------------- - -Em alguns casos, você precisa especificar diretamente o código SQL como um valor, que não deve ser entendido como uma string e escapado. Para isso, servem os objetos da classe `Nette\Database\SqlLiteral`. Eles são criados pelo método `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Ou alternativamente: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Literais SQL podem conter parâmetros: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Graças a isso, podemos criar combinações interessantes: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Obtenção de dados -================= - - -Atalhos para consultas SELECT ------------------------------ - -Para simplificar a recuperação de dados, `Connection` oferece vários atalhos que combinam a chamada `query()` com a subsequente `fetch*()`. Esses métodos aceitam os mesmos parâmetros que `query()`, ou seja, a consulta SQL e parâmetros opcionais. Uma descrição completa dos métodos `fetch*()` pode ser encontrada [abaixo |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Executa a consulta e retorna a primeira linha como um objeto `Row` -| `fetchAll($sql, ...$params): array` | Executa a consulta e retorna todas as linhas como um array de objetos `Row` -| `fetchPairs($sql, ...$params): array` | Executa a consulta e retorna um array associativo, onde a primeira coluna representa a chave e a segunda o valor -| `fetchField($sql, ...$params): mixed` | Executa a consulta e retorna o valor do primeiro campo da primeira linha -| `fetchList($sql, ...$params): ?array` | Executa a consulta e retorna a primeira linha como um array indexado - -Exemplo: - -```php -// fetchField() - retorna o valor da primeira célula -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iteração sobre linhas ---------------------------------- - -Após a execução da consulta, é retornado um objeto [ResultSet|api:Nette\Database\ResultSet], que permite percorrer os resultados de várias maneiras. A maneira mais fácil de executar uma consulta e obter linhas é iterando em um loop `foreach`. Este método é o mais eficiente em termos de memória, pois retorna os dados gradualmente e não os armazena na memória de uma vez. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` só pode ser iterado uma vez. Se precisar iterar repetidamente, você deve primeiro carregar os dados em um array, por exemplo, usando o método `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Retorna a linha como um objeto `Row`. Se não houver mais linhas, retorna `null`. Move o ponteiro interno para a próxima linha. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // carrega a primeira linha -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Retorna todas as linhas restantes do `ResultSet` como um array de objetos `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // carrega todas as linhas -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Retorna os resultados como um array associativo. O primeiro argumento especifica o nome da coluna a ser usada como chave no array, o segundo argumento especifica o nome da coluna a ser usada como valor: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Se especificarmos apenas o primeiro parâmetro, o valor será a linha inteira, ou seja, o objeto `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Em caso de chaves duplicadas, o valor da última linha é usado. Ao usar `null` como chave, o array será indexado numericamente a partir de zero (então não ocorrem colisões): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativamente, você pode fornecer um callback como parâmetro, que retornará para cada linha ou o próprio valor, ou um par chave-valor. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// O callback também pode retornar um array com um par chave & valor: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Retorna o valor do primeiro campo da linha atual. Se não houver mais linhas, retorna `null`. Move o ponteiro interno para a próxima linha. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // carrega o nome da primeira linha -``` - - -fetchList(): ?array .[method] ------------------------------ - -Retorna a linha como um array indexado. Se não houver mais linhas, retorna `null`. Move o ponteiro interno para a próxima linha. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Retorna o número de linhas afetadas pela última consulta `UPDATE` ou `DELETE`. Para `SELECT`, é o número de linhas retornadas, mas isso pode não ser conhecido - nesse caso, o método retorna `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Retorna o número de colunas no `ResultSet`. - - -Informações sobre consultas -=========================== - -Para fins de depuração, podemos obter informações sobre a última consulta executada: - -```php -echo $database->getLastQueryString(); // imprime a consulta SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // imprime a consulta SQL -echo $result->getTime(); // imprime o tempo de execução em segundos -``` - -Para exibir o resultado como uma tabela HTML, pode-se usar: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet oferece informações sobre os tipos das colunas: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column é do tipo $type->type"; // por exemplo, 'id é do tipo int' -} -``` - - -Registro de consultas ---------------------- - -Podemos implementar nosso próprio registro de consultas. O evento `onQuery` é um array de callbacks que são chamados após cada consulta executada: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/pt/transactions.texy b/database/pt/transactions.texy deleted file mode 100644 index eaa93de03e..0000000000 --- a/database/pt/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Transações -********** - -.[perex] -As transações garantem que todas as operações dentro de uma transação sejam executadas ou nenhuma delas seja executada. Elas são úteis para garantir a consistência dos dados em operações mais complexas. - -A maneira mais simples de usar transações é assim: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Você pode escrever a mesma coisa de forma muito mais elegante usando o método `transaction()`. Ele recebe um callback como parâmetro, que executa dentro da transação. Se o callback for executado sem exceção, a transação é automaticamente confirmada. Se ocorrer uma exceção, a transação é cancelada (rollback) e a exceção é propagada. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -O método `transaction()` também pode retornar valores: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // retorna o número de linhas atualizadas -}); -``` diff --git a/database/ro/@home.texy b/database/ro/@home.texy deleted file mode 100644 index e6e3ddb3ab..0000000000 --- a/database/ro/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Baze de date suportate -====================== - -Nette suportă următoarele baze de date: - -|* Server bază de date |* Nume DSN |* Suport în Core |* Suport în Explorer -| MySQL (>= 5.1) | mysql | DA | DA -| PostgreSQL (>= 9.0) | pgsql | DA | DA -| Sqlite 3 (>= 3.8) | sqlite | DA | DA -| Oracle | oci | DA | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA | DA -| MS SQL (PDO_DBLIB) | mssql | DA | - -| ODBC | odbc | DA | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database simplifică semnificativ recuperarea datelor din baza de date fără a fi nevoie să scrieți interogări SQL. Execută interogări eficiente și nu transferă date inutile.}} diff --git a/database/ro/@left-menu.texy b/database/ro/@left-menu.texy deleted file mode 100644 index 7e37400986..0000000000 --- a/database/ro/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Introducere |guide] -- [Abordare SQL |sql way] -- [Explorer |Explorer] -- [Tranzacții |transactions] -- [Excepții |exceptions] -- [Reflecție |reflection] -- [Mapare |mapping] -- [Configurație |configuration] -- [Riscuri de securitate |security] -- [Actualizare |en:upgrading] diff --git a/database/ro/@meta.texy b/database/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/database/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/database/ro/configuration.texy b/database/ro/configuration.texy deleted file mode 100644 index c1948e2cb1..0000000000 --- a/database/ro/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Configurarea bazei de date -************************** - -.[perex] -Prezentare generală a opțiunilor de configurare pentru Nette Database. - -Dacă nu utilizați întregul framework, ci doar această bibliotecă, citiți [cum să încărcați configurația |bootstrap:]. - - -O singură conexiune -------------------- - -Configurarea unei singure conexiuni la baza de date: - -```neon -database: - # DSN, singura cheie obligatorie - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Creează serviciile `Nette\Database\Connection` și `Nette\Database\Explorer`, pe care de obicei le transmitem prin [autowiring |dependency-injection:autowiring], eventual prin referință la [numele lor |#Servicii DI]. - -Alte setări: - -```neon -database: - # afișează panoul database în Tracy Bar? - debugger: ... # (bool) implicit este true - - # afișează EXPLAIN pentru interogări în Tracy Bar? - explain: ... # (bool) implicit este true - - # permite autowiring pentru această conexiune? - autowired: ... # (bool) implicit este true la prima conexiune - - # convenții pentru tabele: discovered, static sau numele clasei - conventions: discovered # (string) implicit este 'discovered' - - options: - # conectare la baza de date doar când este necesar? - lazy: ... # (bool) implicit este false - - # clasa PHP a driverului bazei de date - driverClass: # (string) - - # doar MySQL: setează sql_mode - sqlmode: # (string) - - # doar MySQL: setează SET NAMES - charset: # (string) implicit este 'utf8mb4' - - # doar MySQL: convertește TINYINT(1) la bool - convertBoolean: # (bool) implicit este false - - # returnează coloanele cu dată ca obiecte imutabile (de la versiunea 3.2.1) - newDateTime: # (bool) implicit este false - - # doar Oracle și SQLite: format pentru stocarea datei - formatDateTime: # (string) implicit este 'U' -``` - -În cheia `options` se pot specifica și alte opțiuni, pe care le găsiți în [documentația driverelor PDO |https://www.php.net/manual/en/pdo.drivers.php], cum ar fi: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Mai multe conexiuni -------------------- - -În configurație putem defini și mai multe conexiuni la baza de date împărțindu-le în secțiuni numite: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring-ul este activat doar pentru serviciile din prima secțiune. Acest lucru poate fi schimbat folosind `autowired: false` sau `autowired: true`. - - -Servicii DI ------------ - -Aceste servicii sunt adăugate în containerul DI, unde `###` reprezintă numele conexiunii: - -| Nume | Tip | Descriere -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | conexiune la baza de date -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Dacă definim doar o singură conexiune, numele serviciilor vor fi `database.default.connection` și `database.default.explorer`. Dacă definim mai multe conexiuni ca în exemplul de mai sus, numele vor corespunde secțiunilor, adică `database.main.connection`, `database.main.explorer` și apoi `database.another.connection` și `database.another.explorer`. - -Serviciile ne-autowired le transmitem explicit prin referință la numele lor: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/ro/exceptions.texy b/database/ro/exceptions.texy deleted file mode 100644 index 7a2084e0d6..0000000000 --- a/database/ro/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Excepții -******** - -Nette Database utilizează o ierarhie de excepții. Clasa de bază este `Nette\Database\DriverException`, care moștenește din `PDOException` și oferă posibilități extinse pentru lucrul cu erorile bazei de date: - -- Metoda `getDriverCode()` returnează codul de eroare de la driverul bazei de date -- Metoda `getSqlState()` returnează codul SQLSTATE -- Metodele `getQueryString()` și `getParameters()` permit obținerea interogării originale și a parametrilor săi - -Din `DriverException` moștenesc următoarele excepții specializate: - -- `ConnectionException` - semnalează eșecul conexiunii la serverul bazei de date -- `ConstraintViolationException` - clasa de bază pentru încălcarea constrângerilor bazei de date, din care moștenesc: - - `ForeignKeyConstraintViolationException` - încălcarea cheii străine - - `NotNullConstraintViolationException` - încălcarea constrângerii NOT NULL - - `UniqueConstraintViolationException` - încălcarea unicității valorii - - -Exemplu de capturare a excepției `UniqueConstraintViolationException`, care apare atunci când încercăm să inserăm un utilizator cu un email care există deja în baza de date (presupunând că coloana email are un index unic). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Utilizatorul cu acest email există deja.'; - -} catch (Nette\Database\DriverException $e) { - echo 'A apărut o eroare la înregistrare: ' . $e->getMessage(); -} -``` diff --git a/database/ro/explorer.texy b/database/ro/explorer.texy deleted file mode 100644 index f7cb938248..0000000000 --- a/database/ro/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer oferă o modalitate intuitivă și eficientă de a lucra cu baza de date. Se ocupă automat de legăturile dintre tabele și de optimizarea interogărilor, astfel încât să vă puteți concentra pe aplicația dvs. Funcționează imediat fără configurare. Dacă aveți nevoie de control total asupra interogărilor SQL, puteți utiliza [abordarea SQL |sql-way]. - -- Lucrul cu datele este natural și ușor de înțeles -- Generează interogări SQL optimizate, care încarcă doar datele necesare -- Permite accesul facil la datele conexe fără a fi nevoie să scrieți interogări JOIN -- Funcționează imediat fără nicio configurare sau generare de entități - -</div> - - -Cu Explorer începeți prin apelarea metodei `table()` a obiectului [api:Nette\Database\Explorer] (detalii despre conectare găsiți în capitolul [Conectare și configurare |guide#Conectare și configurare]): - -```php -$books = $explorer->table('book'); // 'book' este numele tabelei -``` - -Metoda returnează obiectul [Selection |api:Nette\Database\Table\Selection], care reprezintă o interogare SQL. Pe acest obiect putem înlănțui alte metode pentru filtrarea și sortarea rezultatelor. Interogarea se construiește și se execută abia în momentul în care începem să solicităm date. De exemplu, prin parcurgerea cu ciclul `foreach`. Fiecare rând este reprezentat de obiectul [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // afișarea coloanei 'title' - echo $book->author_id; // afișarea coloanei 'author_id' -} -``` - -Explorer facilitează în mod fundamental lucrul cu [legăturile dintre tabele |#Relații între tabele]. Următorul exemplu arată cât de ușor putem afișa date din tabele legate (cărți și autorii lor). Observați că nu trebuie să scriem nicio interogare JOIN, Nette le creează pentru noi: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Carte: ' . $book->title; - echo 'Autor: ' . $book->author->name; // creează JOIN pe tabela 'author' -} -``` - -Nette Database Explorer optimizează interogările pentru a fi cât mai eficiente. Exemplul de mai sus execută doar două interogări SELECT, indiferent dacă procesăm 10 sau 10 000 de cărți. - -În plus, Explorer urmărește ce coloane sunt utilizate în cod și încarcă din baza de date doar acelea, economisind astfel performanță suplimentară. Acest comportament este complet automat și adaptiv. Dacă modificați ulterior codul și începeți să utilizați alte coloane, Explorer ajustează automat interogările. Nu trebuie să setați nimic, nici să vă gândiți ce coloane veți avea nevoie - lăsați asta pe seama Nette. - - -Filtrare și sortare -=================== - -Clasa `Selection` oferă metode pentru filtrarea și sortarea selecției de date. - -.[language-php] -| `where($condition, ...$params)` | Adaugă condiția WHERE. Mai multe condiții sunt legate cu operatorul AND -| `whereOr(array $conditions)` | Adaugă un grup de condiții WHERE legate cu operatorul OR -| `wherePrimary($value)` | Adaugă condiția WHERE după cheia primară -| `order($columns, ...$params)` | Setează sortarea ORDER BY -| `select($columns, ...$params)` | Specifică coloanele care trebuie încărcate -| `limit($limit, $offset = null)` | Limitează numărul de rânduri (LIMIT) și opțional setează OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Setează paginarea -| `group($columns, ...$params)` | Grupează rândurile (GROUP BY) -| `having($condition, ...$params)` | Adaugă condiția HAVING pentru filtrarea rândurilor grupate - -Metodele pot fi înlănțuite (așa-numitul [fluent interface |nette:introduction-to-object-oriented-programming#Interfețe fluente]): `$table->where(...)->order(...)->limit(...)`. - -În aceste metode puteți utiliza și notația specială pentru accesarea [datelor din tabelele conexe |#Interogarea prin tabele asociate]. - - -Escapare și identificatori --------------------------- - -Metodele escapează automat parametrii și încadrează identificatorii (numele tabelelor și coloanelor) în ghilimele, prevenind astfel SQL injection. Pentru funcționarea corectă este necesar să respectați câteva reguli: - -- Cuvintele cheie, numele funcțiilor, procedurilor etc. scrieți-le cu **majuscule**. -- Numele coloanelor și tabelelor scrieți-le cu **litere mici**. -- Șirurile de caractere introduceți-le întotdeauna prin **parametri**. - -```php -where('name = ' . $name); // VULNERABILITATE CRITICĂ: SQL injection -where('name LIKE "%search%"'); // GREȘIT: complică încadrarea automată în ghilimele -where('name LIKE ?', '%search%'); // CORECT: valoare introdusă prin parametru - -where('name like ?', $name); // GREȘIT: generează: `name` `like` ? -where('name LIKE ?', $name); // CORECT: generează: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORECT: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtrează rezultatele folosind condiții WHERE. Punctul său forte este lucrul inteligent cu diferite tipuri de valori și alegerea automată a operatorilor SQL. - -Utilizare de bază: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Datorită detectării automate a operatorilor potriviți, nu trebuie să ne ocupăm de diverse cazuri speciale. Nette le rezolvă pentru noi: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// se poate utiliza și semnul de întrebare substituent fără operator: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Metoda procesează corect și condițiile negative și array-urile goale: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- nu găsește nimic -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- găsește tot -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- găsește tot -// $table->where('NOT id ?', $ids); Atenție - această sintaxă nu este suportată -``` - -Ca parametru putem transmite și rezultatul dintr-o altă tabelă - se va crea o subinterogare: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Condițiile le putem transmite și ca array, ale cărui elemente se vor uni cu AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -În array putem folosi perechi cheie => valoare și Nette alege din nou automat operatorii corecți: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -În array putem combina expresii SQL cu semne de întrebare substituente și mai mulți parametri. Acest lucru este potrivit pentru condiții complexe cu operatori definiți precis: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // doi parametri îi transmitem ca array -]); -``` - -Apelurile multiple ale `where()` leagă automat condițiile cu AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Similar cu `where()`, adaugă condiții, dar cu diferența că le leagă cu OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Și aici putem folosi expresii mai complexe: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Adaugă condiția pentru cheia primară a tabelei: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Dacă tabela are o cheie primară compozită (de ex. `foo_id`, `bar_id`), o transmitem ca array: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Determină ordinea în care vor fi returnate rândurile. Putem sorta după una sau mai multe coloane, în ordine descrescătoare sau crescătoare, sau după o expresie proprie: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Specifică coloanele care trebuie returnate din baza de date. În mod implicit, Nette Database Explorer returnează doar acele coloane care sunt utilizate efectiv în cod. Metoda `select()` o folosim deci în cazurile în care avem nevoie să returnăm expresii specifice: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Aliasurile definite cu `AS` sunt apoi disponibile ca proprietăți ale obiectului ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // acces la alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limitează numărul de rânduri returnate (LIMIT) și opțional permite setarea unui offset: - -```php -$table->limit(10); // LIMIT 10 (returnează primele 10 rânduri) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Pentru paginare este mai potrivită utilizarea metodei `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilitează paginarea rezultatelor. Acceptă numărul paginii (numărat de la 1) și numărul de elemente pe pagină. Opțional, se poate transmite o referință la o variabilă în care se va stoca numărul total de pagini: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Total pagini: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Grupează rândurile după coloanele specificate (GROUP BY). Se utilizează de obicei în combinație cu funcții de agregare: - -```php -// Calculează numărul de produse din fiecare categorie -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Setează condiția pentru filtrarea rândurilor grupate (HAVING). Poate fi utilizată în combinație cu metoda `group()` și funcții de agregare: - -```php -// Găsește categoriile care au mai mult de 100 de produse -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Citirea datelor -=============== - -Pentru citirea datelor din baza de date avem la dispoziție câteva metode utile: - -.[language-php] -| `foreach ($table as $key => $row)` | Iterează peste toate rândurile, `$key` este valoarea cheii primare, `$row` este obiectul ActiveRow -| `$row = $table->get($key)` | Returnează un rând după cheia primară -| `$row = $table->fetch()` | Returnează rândul curent și mută pointerul la următorul -| `$array = $table->fetchPairs()` | Creează un array asociativ din rezultate -| `$array = $table->fetchAll()` | Returnează toate rândurile ca array -| `count($table)` | Returnează numărul de rânduri din obiectul Selection - -Obiectul [ActiveRow |api:Nette\Database\Table\ActiveRow] este destinat doar citirii. Acest lucru înseamnă că nu se pot modifica valorile proprietăților sale. Această limitare asigură consistența datelor și previne efectele secundare neașteptate. Datele sunt încărcate din baza de date și orice modificare ar trebui efectuată explicit și controlat. - - -`foreach` - iterare peste toate rândurile ------------------------------------------ - -Cel mai simplu mod de a executa o interogare și de a obține rândurile este iterarea într-un ciclu `foreach`. Lansează automat interogarea SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key este valoarea cheii primare, $book este ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Execută interogarea SQL și returnează rândul după cheia primară, sau `null`, dacă nu există. - -```php -$book = $explorer->table('book')->get(123); // returnează ActiveRow cu ID 123 sau null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Returnează rândul și mută pointerul intern la următorul. Dacă nu mai există alte rânduri, returnează `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Returnează rezultatele ca array asociativ. Primul argument specifică numele coloanei care se va utiliza ca cheie în array, al doilea argument specifică numele coloanei care se va utiliza ca valoare: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Dacă specificăm doar primul parametru, valoarea va fi întregul rând, adică obiectul `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -În cazul cheilor duplicate, se va utiliza valoarea din ultimul rând. La utilizarea `null` ca cheie, array-ul va fi indexat numeric de la zero (atunci nu apar coliziuni): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativ, puteți specifica ca parametru un callback, care va returna pentru fiecare rând fie valoarea însăși, fie perechea cheie-valoare. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Prima carte (Jan Novák)', ...] - -// Callback-ul poate returna și un array cu perechea cheie & valoare: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Prima carte' => 'Jan Novák', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Returnează toate rândurile ca array asociativ de obiecte `ActiveRow`, unde cheile sunt valorile cheilor primare. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Metoda `count()` fără parametru returnează numărul de rânduri din obiectul `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativă -``` - -Atenție, `count()` cu parametru execută funcția de agregare COUNT în baza de date. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Convertește obiectul `ActiveRow` într-un array asociativ, unde cheile sunt numele coloanelor și valorile sunt datele corespunzătoare. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray va fi ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregace -======== - -Clasa `Selection` oferă metode pentru executarea ușoară a funcțiilor de agregare (COUNT, SUM, MIN, MAX, AVG etc.). - -.[language-php] -| `count($expr)` | Numără numărul de rânduri -| `min($expr)` | Returnează valoarea minimă din coloană -| `max($expr)` | Returnează valoarea maximă din coloană -| `sum($expr)` | Returnează suma valorilor din coloană -| `aggregation($function)` | Permite executarea oricărei funcții de agregare. De ex. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Execută interogarea SQL cu funcția COUNT și returnează rezultatul. Metoda se utilizează pentru a afla câte rânduri corespund unei anumite condiții: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Atenție, [#count()] fără parametru returnează doar numărul de rânduri din obiectul `Selection`. - - -min(string $expr) și max(string $expr) .[method] ------------------------------------------------- - -Metodele `min()` și `max()` returnează valoarea minimă și maximă din coloana sau expresia specificată: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Returnează suma valorilor din coloana sau expresia specificată: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- - -Permite executarea oricărei funcții de agregare. - -```php -// prețul mediu al produselor din categorie -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// unește etichetele produsului într-un singur șir -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Dacă avem nevoie să agregăm rezultate care deja provin dintr-o funcție de agregare și grupare (de ex. `SUM(valoare)` peste rândurile grupate), ca al doilea argument specificăm funcția de agregare care trebuie aplicată acestor rezultate intermediare: - -```php -// Calculează prețul total al produselor din stoc pentru fiecare categorie și apoi adună aceste prețuri. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -În acest exemplu, mai întâi calculăm prețul total al produselor din fiecare categorie (`SUM(price * stock) AS category_total`) și grupăm rezultatele după `category_id`. Apoi folosim `aggregation('SUM(category_total)', 'SUM')` pentru a aduna aceste sume intermediare `category_total`. Al doilea argument `'SUM'` spune că funcția SUM trebuie aplicată rezultatelor intermediare. - - -Insert, Update & Delete -======================= - -Nette Database Explorer simplifică inserarea, actualizarea și ștergerea datelor. Toate metodele menționate aruncă excepția `Nette\Database\DriverException` în caz de eroare. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Inserează înregistrări noi în tabelă. - -**Inserarea unei singure înregistrări:** - -Transmitem noua înregistrare ca array asociativ sau obiect iterabil (de exemplu, ArrayHash utilizat în [formulare |forms:]), unde cheile corespund numelor coloanelor din tabelă. - -Dacă tabela are o cheie primară definită, metoda returnează un obiect `ActiveRow`, care este reîncărcat din baza de date pentru a reflecta eventualele modificări efectuate la nivelul bazei de date (triggere, valori implicite ale coloanelor, calcule ale coloanelor auto-increment). Astfel se asigură consistența datelor și obiectul conține întotdeauna datele actuale din baza de date. Dacă nu are o cheie primară unică, returnează datele transmise sub formă de array. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row este o instanță ActiveRow și conține datele complete ale rândului inserat, -// inclusiv ID-ul generat automat și eventualele modificări efectuate de triggere -echo $row->id; // Afișează ID-ul utilizatorului nou inserat -echo $row->created_at; // Afișează timpul creării, dacă este setat de un trigger -``` - -**Inserarea mai multor înregistrări deodată:** - -Metoda `insert()` permite inserarea mai multor înregistrări printr-o singură interogare SQL. În acest caz, returnează numărul de rânduri inserate. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows va fi 2 -``` - -Ca parametru se poate transmite și un obiect `Selection` cu selecția de date. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Inserarea valorilor speciale:** - -Ca valori putem transmite și fișiere, obiecte DateTime sau literali SQL: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // convertește la formatul bazei de date - 'avatar' => fopen('image.jpg', 'rb'), // inserează conținutul binar al fișierului - 'uuid' => $explorer::literal('UUID()'), // apelează funcția UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Actualizează rândurile din tabelă conform filtrului specificat. Returnează numărul de rânduri efectiv modificate. - -Coloanele modificate le transmitem ca array asociativ sau obiect iterabil (de exemplu, ArrayHash utilizat în [formulare |forms:]), unde cheile corespund numelor coloanelor din tabelă: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Pentru modificarea valorilor numerice putem folosi operatorii `+=` și `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // crește valoarea coloanei 'points' cu 1 - 'coins-=' => 1, // scade valoarea coloanei 'coins' cu 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Șterge rândurile din tabelă conform filtrului specificat. Returnează numărul de rânduri șterse. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -La apelarea `update()` și `delete()`, nu uitați să specificați rândurile care trebuie modificate/șterse folosind `where()`. Dacă nu utilizați `where()`, operația se va efectua pe întreaga tabelă! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Actualizează datele din rândul bazei de date reprezentat de obiectul `ActiveRow`. Ca parametru acceptă un iterabil cu datele care trebuie actualizate (cheile sunt numele coloanelor). Pentru modificarea valorilor numerice putem folosi operatorii `+=` și `-=`: - -După efectuarea actualizării, `ActiveRow` se reîncarcă automat din baza de date pentru a reflecta eventualele modificări efectuate la nivelul bazei de date (de ex. triggere). Metoda returnează true doar dacă a avut loc o modificare efectivă a datelor. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // creștem numărul de vizualizări -]); -echo $article->views; // Afișează numărul curent de vizualizări -``` - -Această metodă actualizează doar un singur rând specific din baza de date. Pentru actualizarea în masă a mai multor rânduri, utilizați metoda [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Șterge rândul din baza de date, care este reprezentat de obiectul `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Șterge cartea cu ID 1 -``` - -Această metodă șterge doar un singur rând specific din baza de date. Pentru ștergerea în masă a mai multor rânduri, utilizați metoda [#Selection::delete()]. - - -Relații între tabele -==================== - -În bazele de date relaționale, datele sunt împărțite în mai multe tabele și interconectate prin chei străine. Nette Database Explorer aduce o modalitate revoluționară de a lucra cu aceste legături - fără a scrie interogări JOIN și fără a fi nevoie să configurați sau să generați ceva. - -Pentru a ilustra lucrul cu legăturile, vom folosi exemplul bazei de date de cărți ([îl găsiți pe GitHub |https://github.com/nette-examples/books]). În baza de date avem tabelele: - -- `author` - scriitori și traducători (coloane `id`, `name`, `web`, `born`) -- `book` - cărți (coloane `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - etichete (coloane `id`, `name`) -- `book_tag` - tabelă de legătură între cărți și etichete (coloane `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Structura bazei de date folosită în exemple .<> - -În exemplul nostru de bază de date de cărți găsim mai multe tipuri de relații (deși modelul este simplificat față de realitate): - -- One-to-many 1:N – fiecare carte **are un** autor, autorul poate scrie **mai multe** cărți -- Zero-to-many 0:N – cartea **poate avea** un traducător, traducătorul poate traduce **mai multe** cărți -- Zero-to-one 0:1 – cartea **poate avea** o continuare -- Many-to-many M:N – cartea **poate avea mai multe** etichete și o etichetă poate fi atribuită **mai multor** cărți - -În aceste relații există întotdeauna o tabelă părinte și una copil. De exemplu, în relația dintre autor și carte, tabela `author` este părinte și `book` este copil - ne putem imagina că o carte "aparține" întotdeauna unui autor. Acest lucru se reflectă și în structura bazei de date: tabela copil `book` conține cheia străină `author_id`, care face referire la tabela părinte `author`. - -Dacă avem nevoie să afișăm cărțile inclusiv numele autorilor lor, avem două opțiuni. Fie obținem datele printr-o singură interogare SQL folosind JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Fie încărcăm datele în doi pași - mai întâi cărțile și apoi autorii lor - și apoi le asamblăm în PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id-urile autorilor cărților obținute -``` - -A doua abordare este de fapt mai eficientă, deși poate fi surprinzător. Datele sunt încărcate o singură dată și pot fi utilizate mai bine în cache. Exact în acest mod lucrează Nette Database Explorer - rezolvă totul sub capotă și vă oferă o API elegantă: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'titlu: ' . $book->title; - echo 'scris de: ' . $book->author->name; // $book->author este înregistrarea din tabela 'author' - echo 'tradus de: ' . $book->translator?->name; -} -``` - - -Accesul la tabela părinte -------------------------- - -Accesul la tabela părinte este direct. Este vorba despre relații precum *cartea are un autor* sau *cartea poate avea un traducător*. Obținem înregistrarea asociată prin proprietatea obiectului ActiveRow - numele său corespunde numelui coloanei cu cheia străină fără `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // găsește autorul după coloana author_id -echo $book->translator?->name; // găsește traducătorul după translator_id -``` - -Când accesăm proprietatea `$book->author`, Explorer caută în tabela `book` o coloană al cărei nume conține șirul `author` (adică `author_id`). După valoarea din această coloană, încarcă înregistrarea corespunzătoare din tabela `author` și o returnează ca `ActiveRow`. Similar funcționează și `$book->translator`, care utilizează coloana `translator_id`. Deoarece coloana `translator_id` poate conține `null`, folosim în cod operatorul `?->`. - -O cale alternativă o oferă metoda `ref()`, care acceptă doi argumente, numele tabelei țintă și numele coloanei de legătură, și returnează o instanță `ActiveRow` sau `null`: - -```php -echo $book->ref('author', 'author_id')->name; // legătura cu autorul -echo $book->ref('author', 'translator_id')->name; // legătura cu traducătorul -``` - -Metoda `ref()` este utilă dacă nu se poate utiliza accesul prin proprietate, deoarece tabela conține o coloană cu același nume (adică `author`). În celelalte cazuri, se recomandă utilizarea accesului prin proprietate, care este mai lizibil. - -Explorer optimizează automat interogările bazei de date. Când parcurgem cărțile într-un ciclu și accesăm înregistrările lor asociate (autori, traducători), Explorer nu generează o interogare pentru fiecare carte în parte. În schimb, execută doar un singur SELECT pentru fiecare tip de legătură, reducând semnificativ sarcina bazei de date. De exemplu: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Acest cod apelează doar aceste trei interogări fulgerătoare în baza de date: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id din coloana author_id a cărților selectate -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id din coloana translator_id a cărților selectate -``` - -.[note] -Logica de căutare a coloanei de legătură este dată de implementarea [Conventions |api:Nette\Database\Conventions]. Recomandăm utilizarea [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], care analizează cheile străine și permite lucrul simplu cu relațiile existente între tabele. - - -Accesul la tabela copil ------------------------ - -Accesul la tabela copil funcționează în direcția opusă. Acum întrebăm *ce cărți a scris acest autor* sau *a tradus acest traducător*. Pentru acest tip de interogare folosim metoda `related()`, care returnează `Selection` cu înregistrările asociate. Să vedem un exemplu: - -```php -$author = $explorer->table('author')->get(1); - -// Afișează toate cărțile autorului -foreach ($author->related('book.author_id') as $book) { - echo "A scris: $book->title"; -} - -// Afișează toate cărțile pe care autorul le-a tradus -foreach ($author->related('book.translator_id') as $book) { - echo "A tradus: $book->title"; -} -``` - -Metoda `related()` acceptă descrierea legăturii ca un singur argument cu notație cu punct sau ca doi argumente separate: - -```php -$author->related('book.translator_id'); // un argument -$author->related('book', 'translator_id'); // doi argumente -``` - -Explorer poate detecta automat coloana de legătură corectă pe baza numelui tabelei părinte. În acest caz, se leagă prin coloana `book.author_id`, deoarece numele tabelei sursă este `author`: - -```php -$author->related('book'); // utilizează book.author_id -``` - -Dacă ar exista mai multe legături posibile, Explorer ar arunca excepția [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Metoda `related()` o putem folosi, desigur, și la parcurgerea mai multor înregistrări într-un ciclu și Explorer optimizează automat interogările și în acest caz: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' a scris:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Acest cod generează doar două interogări SQL fulgerătoare: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id-urile autorilor selectați -``` - - -Legătura Many-to-many ---------------------- - -Pentru legătura many-to-many (M:N) este necesară existența unei tabele de legătură (în cazul nostru `book_tag`), care conține două coloane cu chei străine (`book_id`, `tag_id`). Fiecare dintre aceste coloane face referire la cheia primară a uneia dintre tabelele legate. Pentru a obține datele asociate, mai întâi obținem înregistrările din tabela de legătură folosind `related('book_tag')` și apoi continuăm către datele țintă: - -```php -$book = $explorer->table('book')->get(1); -// afișează numele etichetelor atribuite cărții -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // afișează numele etichetei prin tabela de legătură -} - -$tag = $explorer->table('tag')->get(1); -// sau invers: afișează numele cărților etichetate cu această etichetă -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // afișează numele cărții -} -``` - -Explorer optimizează din nou interogările SQL într-o formă eficientă: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id-urile cărților selectate -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id-urile etichetelor găsite în book_tag -``` - - -Interogarea prin tabele asociate --------------------------------- - -În metodele `where()`, `select()`, `order()` și `group()` putem folosi notații speciale pentru accesarea coloanelor din alte tabele. Explorer creează automat JOIN-urile necesare. - -**Notația cu punct** (`tabela_parinte.coloana`) se utilizează pentru relația 1:N din perspectiva tabelei copil: - -```php -$books = $explorer->table('book'); - -// Găsește cărțile al căror autor are numele începând cu 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Sortează cărțile după numele autorului descrescător -$books->order('author.name DESC'); - -// Afișează titlul cărții și numele autorului -$books->select('book.title, author.name'); -``` - -**Notația cu două puncte** (`:tabela_copil.coloana`) se utilizează pentru relația 1:N din perspectiva tabelei părinte: - -```php -$authors = $explorer->table('author'); - -// Găsește autorii care au scris o carte cu 'PHP' în titlu -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Calculează numărul de cărți pentru fiecare autor -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -În exemplul de mai sus cu notația cu două puncte (`:book.title`), nu este specificată coloana cu cheia străină. Explorer detectează automat coloana corectă pe baza numelui tabelei părinte. În acest caz, se leagă prin coloana `book.author_id`, deoarece numele tabelei sursă este `author`. Dacă ar exista mai multe legături posibile, Explorer ar arunca excepția [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Coloana de legătură poate fi specificată explicit în paranteză: - -```php -// Găsește autorii care au tradus o carte cu 'PHP' în titlu -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Notațiile pot fi înlănțuite pentru accesul prin mai multe tabele: - -```php -// Găsește autorii cărților etichetate cu 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Extinderea condițiilor pentru JOIN ----------------------------------- - -Metoda `joinWhere()` extinde condițiile care se specifică la legarea tabelelor în SQL după cuvântul cheie `ON`. - -Să presupunem că dorim să găsim cărțile traduse de un anumit traducător: - -```php -// Găsește cărțile traduse de traducătorul numit 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -În condiția `joinWhere()` putem folosi aceleași construcții ca în metoda `where()` - operatori, semne de întrebare substituente, array-uri de valori sau expresii SQL. - -Pentru interogări mai complexe cu mai multe JOIN-uri, putem defini aliasuri pentru tabele: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Observați că, în timp ce metoda `where()` adaugă condiții în clauza `WHERE`, metoda `joinWhere()` extinde condițiile în clauza `ON` la legarea tabelelor. diff --git a/database/ro/guide.texy b/database/ro/guide.texy deleted file mode 100644 index f315aff423..0000000000 --- a/database/ro/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database este un strat de baze de date puternic și elegant pentru PHP, cu accent pe simplitate și funcții inteligente. Oferă două moduri de a lucra cu baza de date - [Explorer |explorer] pentru dezvoltarea rapidă a aplicațiilor sau [abordarea SQL |sql-way] pentru lucrul direct cu interogări. - -<div class="grid gap-3"> -<div> - - -[Abordarea SQL |sql-way] -======================== -- Interogări parametrizate sigure -- Control precis asupra formei interogărilor SQL -- Când scrieți interogări complexe cu funcții avansate -- Optimizați performanța folosind funcții SQL specifice - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Dezvoltați rapid fără a scrie SQL -- Lucru intuitiv cu relațiile dintre tabele -- Apreciați optimizarea automată a interogărilor -- Potrivit pentru lucrul rapid și confortabil cu baza de date - -</div> - -</div> - - -Instalare -========= - -Descărcați și instalați biblioteca folosind [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Baze de date suportate -====================== - -Nette Database suportă următoarele baze de date: - -|* Server de baze de date |* Nume DSN |* Suport în Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | DA -| PostgreSQL (>= 9.0) | pgsql | DA -| Sqlite 3 (>= 3.8) | sqlite | DA -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Două abordări ale bazei de date -=============================== - -Nette Database vă oferă o alegere: puteți fie să scrieți interogări SQL direct (abordarea SQL), fie să le lăsați generate automat (Explorer). Să vedem cum ambele abordări rezolvă aceleași sarcini: - -[Abordarea SQL|sql-way] - Interogări SQL - -```php -// inserarea unei înregistrări -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obținerea înregistrărilor: autorii cărților -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// listare (nu este optimă, generează N interogări suplimentare) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Autorul $author->name a scris $author->books_count cărți:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Abordarea Explorer|explorer] - generare automată SQL - -```php -// inserarea unei înregistrări -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obținerea înregistrărilor: autorii cărților -$authors = $database->table('authors') - ->where('active', 1); - -// listare (generează automat doar 2 interogări optimizate) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Autorul $author->name a scris {$books->count()} cărți:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Abordarea Explorer generează și optimizează interogările SQL automat. În exemplul dat, abordarea SQL generează N+1 interogări (una pentru autori și apoi una pentru cărțile fiecărui autor), în timp ce Explorer optimizează automat interogările și execută doar două - una pentru autori și una pentru toate cărțile lor. - -Ambele abordări pot fi combinate liber în aplicație după cum este necesar. - - -Conectare și configurare -======================== - -Pentru a vă conecta la baza de date, trebuie doar să creați o instanță a clasei [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Parametrul `$dsn` (data source name) este același [ca cel utilizat de PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], de ex. `mysql:host=127.0.0.1;dbname=test`. În caz de eșec, aruncă o excepție `Nette\Database\ConnectionException`. - -Cu toate acestea, o modalitate mai convenabilă este oferită de [configurația aplicației |configuration], unde trebuie doar să adăugați secțiunea `database` și se vor crea obiectele necesare, precum și panoul bazei de date în bara [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Apoi, [obținem obiectul conexiunii ca serviciu din containerul DI |dependency-injection:passing-dependencies], de exemplu: - -```php -class Model -{ - public function __construct( - // sau Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Mai multe informații despre [configurarea bazei de date |configuration]. - - -Crearea manuală a Explorer --------------------------- - -Dacă nu utilizați containerul Nette DI, puteți crea manual o instanță `Nette\Database\Explorer`: - -```php -// conectare la baza de date -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// stocare pentru cache, implementează Nette\Caching\Storage, de ex.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// se ocupă de reflexia structurii bazei de date -$structure = new Nette\Database\Structure($connection, $storage); -// definește reguli pentru maparea numelor de tabele, coloane și chei străine -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Gestionarea conexiunii -====================== - -La crearea obiectului `Connection`, conexiunea se stabilește automat. Dacă doriți să amânați conexiunea, utilizați modul lazy - îl activați în [configurație |configuration] setând `lazy: true`, sau astfel: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Pentru gestionarea conexiunii, utilizați metodele `connect()`, `disconnect()` și `reconnect()`. -- `connect()` creează conexiunea dacă nu există deja, putând arunca o excepție `Nette\Database\ConnectionException`. -- `disconnect()` deconectează conexiunea curentă la baza de date. -- `reconnect()` efectuează deconectarea și apoi reconectarea la baza de date. Această metodă poate arunca, de asemenea, o excepție `Nette\Database\ConnectionException`. - -În plus, puteți monitoriza evenimentele legate de conexiune folosind evenimentul `onConnect`, care este un array de callback-uri care sunt apelate după stabilirea conexiunii cu baza de date. - -```php -// se execută după conectarea la baza de date -$database->onConnect[] = function($database) { - echo "Conectat la baza de date"; -}; -``` - - -Bara de depanare Tracy -====================== - -Dacă utilizați [Tracy |tracy:], panoul Database se activează automat în bara de depanare, afișând toate interogările executate, parametrii lor, timpul de execuție și locul din cod unde au fost apelate. - -[* db-panel.webp *] diff --git a/database/ro/mapping.texy b/database/ro/mapping.texy deleted file mode 100644 index e64374fb06..0000000000 --- a/database/ro/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversia tipurilor -******************* - -.[perex] -Nette Database convertește automat valorile returnate din baza de date în tipurile PHP corespunzătoare. - - -Data și ora ------------ - -Datele de timp sunt convertite în obiecte `Nette\Utils\DateTime`. Dacă doriți ca datele de timp să fie convertite în obiecte imuabile `Nette\Database\DateTime`, setați opțiunea `newDateTime` la true în [configurație|configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -În cazul MySQL, convertește tipul de date `TIME` în obiecte `DateInterval`. - - -Valori booleene ---------------- - -Valorile booleene sunt convertite automat în `true` sau `false`. Pentru MySQL, se convertește `TINYINT(1)` dacă setăm `convertBoolean: true` în [configurație |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Valori numerice ---------------- - -Valorile numerice sunt convertite în `int` sau `float` în funcție de tipul coloanei din baza de date: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalizare personalizată -------------------------- - -Folosind metoda `setRowNormalizer(?callable $normalizer)`, puteți seta o funcție personalizată pentru transformarea rândurilor din baza de date. Acest lucru este util, de exemplu, pentru conversia automată a tipurilor de date. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // aici are loc conversia tipurilor - return $row; -}); -``` diff --git a/database/ro/reflection.texy b/database/ro/reflection.texy deleted file mode 100644 index 33121e9002..0000000000 --- a/database/ro/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Reflexia structurii -******************* - -.{data-version:3.2.1} -Nette Database oferă instrumente pentru introspecția structurii bazei de date folosind clasa [api:Nette\Database\Structure]. Aceasta permite obținerea de informații despre tabele, coloane, indecși și chei străine. Puteți utiliza reflexia pentru a genera scheme, a crea aplicații flexibile care lucrează cu baza de date sau instrumente generale pentru baze de date. - -Obținem obiectul structurii din instanța conexiunii la baza de date: - -```php -$reflection = $database->getReflection(); -``` - - -Obținerea tabelelor -------------------- - -Metoda `getTables()` returnează un array cu informații despre toate tabelele: - -```php -// Listarea numelor tuturor tabelelor -foreach ($structure->getTables() as $table) { - echo $table['name'] . "\n"; -} -``` - -Sunt disponibile încă două metode: - -```php -// Verificarea existenței tabelului -if ($reflection->hasTable('users')) { - echo "Tabelul users există"; -} - -// Returnează obiectul tabelului; dacă nu există, aruncă o excepție -$table = $reflection->getTable('users'); -``` - - -Informații despre tabel ------------------------ - -Tabelul este reprezentat de obiectul [Table|api:Nette\Database\Reflection\Table], care oferă următoarele proprietăți readonly: - -- `$name: string` – numele tabelului -- `$view: bool` – dacă este o vizualizare -- `$fullName: ?string` – numele complet al tabelului, inclusiv schema (dacă există) -- `$columns: array<string, Column>` – array asociativ al coloanelor tabelului -- `$indexes: Index[]` – array de indecși ai tabelului -- `$primaryKey: ?Index` – cheia primară a tabelului sau null -- `$foreignKeys: ForeignKey[]` – array de chei străine ale tabelului - - -Coloane -------- - -Proprietatea `columns` a tabelului oferă un array asociativ de coloane, unde cheia este numele coloanei și valoarea este o instanță [Column|api:Nette\Database\Reflection\Column] cu aceste proprietăți: - -- `$name: string` – numele coloanei -- `$table: ?Table` – referință la tabelul coloanei -- `$nativeType: string` – tipul de date nativ al bazei de date -- `$size: ?int` – dimensiunea/lungimea tipului -- `$nullable: bool` – dacă coloana poate conține NULL -- `$default: mixed` – valoarea implicită a coloanei -- `$autoIncrement: bool` – dacă coloana este auto-increment -- `$primary: bool` – dacă face parte din cheia primară -- `$vendor: array` – metadate suplimentare specifice sistemului de baze de date respectiv - -```php -foreach ($table->columns as $name => $column) { - echo "Coloană: $name\n"; - echo "Tip: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Da' : 'Nu') . "\n"; -} -``` - - -Indecși -------- - -Proprietatea `indexes` a tabelului oferă un array de indecși, unde fiecare index este o instanță [Index|api:Nette\Database\Reflection\Index] cu aceste proprietăți: - -- `$columns: Column[]` – array de coloane care formează indexul -- `$unique: bool` – dacă indexul este unic -- `$primary: bool` – dacă este cheia primară -- `$name: ?string` – numele indexului - -Cheia primară a tabelului poate fi obținută folosind proprietatea `primaryKey`, care returnează fie un obiect `Index`, fie `null` în cazul în care tabelul nu are cheie primară. - -```php -// Listarea indecșilor -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Coloane: $columns\n"; - echo " Unic: " . ($index->unique ? 'Da' : 'Nu') . "\n"; -} - -// Listarea cheii primare -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Cheie primară: $columns\n"; -} -``` - - -Chei străine ------------- - -Proprietatea `foreignKeys` a tabelului oferă un array de chei străine, unde fiecare cheie străină este o instanță [ForeignKey|api:Nette\Database\Reflection\ForeignKey] cu aceste proprietăți: - -- `$foreignTable: Table` – tabelul referit -- `$localColumns: Column[]` – array de coloane locale -- `$foreignColumns: Column[]` – array de coloane referite -- `$name: ?string` – numele cheii străine - -```php -// Listarea cheilor străine -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/ro/security.texy b/database/ro/security.texy deleted file mode 100644 index fc616b8c1c..0000000000 --- a/database/ro/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Riscuri de securitate -********************* - -<div class=perex> - -Baza de date conține adesea date sensibile și permite efectuarea de operațiuni periculoase. Pentru a lucra în siguranță cu Nette Database, este esențial să: - -- Înțelegeți diferența dintre API-ul sigur și cel nesigur -- Utilizați interogări parametrizate -- Validați corect datele de intrare - -</div> - - -Ce este SQL Injection? -====================== - -SQL injection este cel mai grav risc de securitate atunci când lucrați cu o bază de date. Apare atunci când intrarea nesanitizată de la utilizator devine parte a unei interogări SQL. Atacatorul poate introduce propriile comenzi SQL și astfel: -- Obține acces neautorizat la date -- Modifică sau șterge datele din baza de date -- Ocolește autentificarea - -```php -// ❌ COD PERICULOS - vulnerabil la SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Atacatorul poate introduce, de exemplu, valoarea: ' OR '1'='1 -// Interogarea rezultată va fi: SELECT * FROM users WHERE name = '' OR '1'='1' -// Ceea ce returnează toți utilizatorii -``` - -Același lucru este valabil și pentru Database Explorer: - -```php -// ❌ COD PERICULOS - vulnerabil la SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Interogări parametrizate -======================== - -Apărarea de bază împotriva SQL injection sunt interogările parametrizate. Nette Database oferă mai multe moduri de a le utiliza. - -Cel mai simplu mod este utilizarea **semnelor de întrebare placeholder**: - -```php -// ✅ Interogare parametrizată sigură -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Condiție sigură în Explorer -$table->where('name = ?', $name); -``` - -Acest lucru este valabil pentru toate celelalte metode din [Database Explorer |explorer], care permit inserarea de expresii cu semne de întrebare placeholder și parametri. - -Pentru comenzile INSERT, UPDATE sau clauza WHERE, putem transmite valorile într-un array: - -```php -// ✅ INSERT sigur -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ INSERT sigur în Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validarea valorilor parametrilor -================================ - -Interogările parametrizate sunt piatra de temelie a lucrului sigur cu baza de date. Cu toate acestea, valorile pe care le introducem în ele trebuie să treacă prin mai multe niveluri de control: - - -Controlul tipului ------------------ - -**Cel mai important este să se asigure tipul corect de date al parametrilor** - aceasta este o condiție necesară pentru utilizarea sigură a Nette Database. Baza de date presupune că toate datele de intrare au tipul de date corect corespunzător coloanei respective. - -De exemplu, dacă `$name` din exemplele anterioare ar fi în mod neașteptat un array în loc de un șir, Nette Database ar încerca să insereze toate elementele sale în interogarea SQL, ceea ce ar duce la o eroare. Prin urmare, **nu utilizați niciodată** date nevalidate din `$_GET`, `$_POST` sau `$_COOKIE` direct în interogările bazei de date. - - -Controlul formatului --------------------- - -La al doilea nivel, verificăm formatul datelor - de exemplu, dacă șirurile sunt în codificare UTF-8 și lungimea lor corespunde definiției coloanei, sau dacă valorile numerice se încadrează în intervalul permis pentru tipul de date al coloanei respective. - -La acest nivel de validare, ne putem baza parțial și pe baza de date însăși - multe baze de date vor refuza datele nevalide. Cu toate acestea, comportamentul poate varia, unele pot scurta în tăcere șirurile lungi sau pot trunchia numerele în afara intervalului. - - -Controlul domeniului --------------------- - -Al treilea nivel constă în controale logice specifice aplicației dvs. De exemplu, verificarea faptului că valorile din casetele de selecție corespund opțiunilor oferite, că numerele se încadrează în intervalul așteptat (de exemplu, vârsta 0-150 de ani) sau că dependențele reciproce dintre valori au sens. - - -Metode de validare recomandate ------------------------------- - -- Utilizați [Formulare Nette |forms:], care asigură automat validarea corectă a tuturor intrărilor -- Utilizați [Presentere |application:presenters] și specificați tipurile de date pentru parametrii din metodele `action*()` și `render*()` -- Sau implementați propriul strat de validare folosind instrumente PHP standard precum `filter_var()` - - -Lucrul sigur cu coloanele -========================= - -În secțiunea anterioară, am arătat cum să validăm corect valorile parametrilor. Cu toate acestea, atunci când folosim array-uri în interogările SQL, trebuie să acordăm aceeași atenție și cheilor lor. - -```php -// ❌ COD PERICULOS - cheile din array nu sunt tratate -$database->query('INSERT INTO users', $_POST); -``` - -Pentru comenzile INSERT și UPDATE, aceasta este o eroare de securitate fundamentală - atacatorul poate introduce sau modifica orice coloană în baza de date. Ar putea, de exemplu, să seteze `is_admin = 1` sau să introducă date arbitrare în coloane sensibile (așa-numita Mass Assignment Vulnerability). - -În condițiile WHERE, este și mai periculos, deoarece pot conține operatori: - -```php -// ❌ COD PERICULOS - cheile din array nu sunt tratate -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// execută interogarea WHERE (`salary` > 100000) -``` - -Atacatorul poate folosi această abordare pentru a descoperi sistematic salariile angajaților. De exemplu, începe cu o interogare pentru salarii peste 100.000, apoi sub 50.000 și, prin restrângerea treptată a intervalului, poate descoperi salariile aproximative ale tuturor angajaților. Acest tip de atac se numește SQL enumeration. - -Metodele `where()` și `whereOr()` sunt și [mult mai flexibile |explorer#where] și suportă expresii SQL în chei și valori, inclusiv operatori și funcții. Acest lucru îi oferă atacatorului posibilitatea de a efectua SQL injection: - -```php -// ❌ COD PERICULOS - atacatorul poate introduce propriul SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// execută interogarea WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Acest atac încheie condiția originală folosind `0)`, adaugă propriul `SELECT` folosind `UNION` pentru a obține date sensibile din tabelul `users` și închide interogarea sintactic corectă folosind `WHERE (1)`. - - -Lista albă a coloanelor ------------------------ - -Pentru a lucra în siguranță cu numele coloanelor, avem nevoie de un mecanism care să asigure că utilizatorul poate lucra doar cu coloanele permise și nu poate adăuga propriile coloane. Am putea încerca să detectăm și să blocăm numele de coloane periculoase (lista neagră), dar această abordare nu este fiabilă - atacatorul poate găsi întotdeauna o nouă modalitate de a scrie un nume de coloană periculos pe care nu l-am prevăzut. - -Prin urmare, este mult mai sigur să inversăm logica și să definim o listă explicită de coloane permise (lista albă): - -```php -// Coloane pe care utilizatorul le poate modifica -$allowedColumns = ['name', 'email', 'active']; - -// Eliminăm toate coloanele nepermise din intrare -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Acum putem folosi în siguranță în interogări, cum ar fi: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Identificatori dinamici -======================= - -Pentru numele dinamice de tabele și coloane, utilizați substituentul `?name`. Acesta asigură escaparea corectă a identificatorilor conform sintaxei bazei de date respective (de exemplu, folosind ghilimele inverse în MySQL): - -```php -// ✅ Utilizare sigură a identificatorilor de încredere -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Rezultat în MySQL: SELECT `name` FROM `users` -``` - -Important: utilizați simbolul `?name` numai pentru valori de încredere definite în codul aplicației. Pentru valorile de la utilizator, utilizați din nou [lista albă |#Lista albă a coloanelor]. Altfel, vă expuneți riscurilor de securitate: - -```php -// ❌ PERICULOS - nu utilizați niciodată intrarea de la utilizator -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/ro/sql-way.texy b/database/ro/sql-way.texy deleted file mode 100644 index 3276671a8d..0000000000 --- a/database/ro/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Abordarea SQL -************* - -.[perex] -Nette Database oferă două abordări: puteți scrie interogări SQL singur (abordarea SQL) sau le puteți lăsa generate automat (vezi [Explorer |explorer]). Abordarea SQL vă oferă control complet asupra interogărilor și, în același timp, asigură construirea lor în siguranță. - -.[note] -Detalii despre conectarea și configurarea bazei de date găsiți în capitolul [Conectare și configurare |guide#Conectare și configurare]. - - -Interogare de bază -================== - -Pentru interogarea bazei de date se folosește metoda `query()`. Aceasta returnează un obiect [ResultSet |api:Nette\Database\ResultSet], care reprezintă rezultatul interogării. În caz de eșec, metoda [aruncă o excepție |exceptions]. Putem parcurge rezultatul interogării folosind bucla `foreach` sau putem folosi una dintre [funcțiile auxiliare |#Obținerea datelor]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Pentru inserarea sigură a valorilor în interogările SQL, folosim interogări parametrizate. Nette Database le face extrem de simple - trebuie doar să adăugați o virgulă și valoarea după interogarea SQL: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Pentru mai mulți parametri, aveți două opțiuni de scriere. Fie puteți "intercala" interogarea SQL cu parametri: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Fie scrieți mai întâi întreaga interogare SQL și apoi adăugați toți parametrii: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Protecție împotriva SQL injection -================================= - -De ce este important să folosim interogări parametrizate? Deoarece vă protejează împotriva atacului numit SQL injection, în care un atacator ar putea introduce propriile comenzi SQL și astfel să obțină sau să deterioreze datele din baza de date. - -.[warning] -**Nu introduceți niciodată variabile direct în interogarea SQL!** Folosiți întotdeauna interogări parametrizate, care vă protejează împotriva SQL injection. - -```php -// ❌ COD PERICULOS - vulnerabil la SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Interogare parametrizată sigură -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Familiarizați-vă cu [posibilele riscuri de securitate |security]. - - -Tehnici de interogare -===================== - - -Condiții WHERE --------------- - -Condițiile WHERE pot fi scrise ca un array asociativ, unde cheile sunt numele coloanelor și valorile sunt datele pentru comparație. Nette Database selectează automat operatorul SQL cel mai potrivit în funcție de tipul valorii. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -În cheie, puteți specifica explicit și operatorul pentru comparație: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // folosește operatorul > - 'name LIKE' => '%John%', // folosește operatorul LIKE - 'email NOT LIKE' => '%example.com%', // folosește operatorul NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette tratează automat cazurile speciale precum valorile `null` sau array-urile. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // folosește operatorul = - 'category_id' => [1, 2, 3], // folosește IN - 'description' => null, // folosește IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Pentru condiții negative, utilizați operatorul `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // folosește operatorul <> - 'category_id NOT' => [1, 2, 3], // folosește NOT IN - 'description NOT' => null, // folosește IS NOT NULL - 'id' => [], // se omite -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Pentru combinarea condițiilor se folosește operatorul `AND`. Acest lucru poate fi schimbat folosind [substituentul ?or |#Indicații pentru construirea SQL]. - - -Reguli ORDER BY ---------------- - -Sortarea `ORDER BY` poate fi scrisă folosind un array. În chei specificăm coloanele, iar valoarea va fi un boolean care determină dacă se sortează ascendent: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // ascendent - 'name' => false, // descendent -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Inserarea datelor (INSERT) --------------------------- - -Pentru inserarea înregistrărilor se folosește comanda SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Metoda `getInsertId()` returnează ID-ul ultimului rând inserat. Pentru unele baze de date (de ex. PostgreSQL), este necesar să specificați ca parametru numele secvenței din care trebuie generat ID-ul folosind `$database->getInsertId($sequenceId)`. - -Ca parametri putem transmite și [#valori speciale] precum fișiere, obiecte DateTime sau tipuri enum. - -Inserarea mai multor înregistrări simultan: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -INSERT-ul multiplu este mult mai rapid, deoarece se execută o singură interogare la baza de date, în loc de multe interogări individuale. - -**Avertisment de securitate:** Nu utilizați niciodată date nevalidate ca `$values`. Familiarizați-vă cu [posibilele riscuri |security#Lucrul sigur cu coloanele]. - - -Actualizarea datelor (UPDATE) ------------------------------ - -Pentru actualizarea înregistrărilor se folosește comanda SQL `UPDATE`. - -```php -// Actualizarea unei singure înregistrări -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Numărul de rânduri afectate este returnat de `$result->getRowCount()`. - -Pentru UPDATE putem folosi operatorii `+=` și `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrementarea login_count -], 1); -``` - -Exemplu de inserare sau modificare a unei înregistrări, dacă aceasta există deja. Folosim tehnica `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Observați că Nette Database recunoaște în ce context al comenzii SQL este inserat parametrul cu array-ul și, în funcție de aceasta, construiește codul SQL din el. Astfel, din primul array a construit `(id, name, year) VALUES (123, 'Jim', 1978)`, în timp ce al doilea l-a convertit în forma `name = 'Jim', year = 1978`. Detaliem acest aspect în secțiunea [#Indicații pentru construirea SQL]. - - -Ștergerea datelor (DELETE) --------------------------- - -Pentru ștergerea înregistrărilor se folosește comanda SQL `DELETE`. Exemplu cu obținerea numărului de rânduri șterse: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Indicații pentru construirea SQL --------------------------------- - -O indicație este un substituent special în interogarea SQL care specifică modul în care valoarea parametrului trebuie rescrisă într-o expresie SQL: - -| Indicație | Descriere | Se utilizează automat -|-----------|-------------------------------------------------|----------------------------- -| `?name` | se utilizează pentru inserarea numelui tabelului sau coloanei | - -| `?values` | generează `(cheie, ...) VALUES (valoare, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | generează atribuirea `cheie = valoare, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | combină condițiile din array cu operatorul `AND` | `WHERE ?`, `HAVING ?` -| `?or` | combină condițiile din array cu operatorul `OR` | - -| `?order` | generează clauza `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Pentru inserarea dinamică a numelor de tabele și coloane în interogare se folosește substituentul `?name`. Nette Database se ocupă de tratarea corectă a identificatorilor conform convențiilor bazei de date respective (de ex. încadrarea în ghilimele inverse în MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (în MySQL) -``` - -**Avertisment:** utilizați simbolul `?name` numai pentru numele de tabele și coloane din intrări validate, altfel vă expuneți unui [risc de securitate |security#Identificatori dinamici]. - -Celelalte indicații de obicei nu trebuie specificate, deoarece Nette folosește o autodetecție inteligentă la construirea interogării SQL (vezi a treia coloană a tabelului). Dar le puteți utiliza, de exemplu, într-o situație în care doriți să combinați condițiile folosind `OR` în loc de `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Valori speciale ---------------- - -Pe lângă tipurile scalare obișnuite (string, int, bool), puteți transmite ca parametri și valori speciale: - -- fișiere: `fopen('image.gif', 'r')` inserează conținutul binar al fișierului -- data și ora: obiectele `DateTime` sunt convertite în formatul bazei de date -- tipuri enum: instanțele `enum` sunt convertite în valoarea lor -- literali SQL: creați folosind `Connection::literal('NOW()')` sunt inserați direct în interogare - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Pentru bazele de date care nu au suport nativ pentru tipul de date `datetime` (precum SQLite și Oracle), `DateTime` este convertit în valoarea specificată în [configurația bazei de date |configuration] prin elementul `formatDateTime` (valoarea implicită este `U` - timestamp unix). - - -Literali SQL ------------- - -În unele cazuri, trebuie să specificați direct cod SQL ca valoare, care însă nu trebuie interpretat ca șir și escapat. Pentru aceasta se folosesc obiectele clasei `Nette\Database\SqlLiteral`. Acestea sunt create de metoda `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Sau alternativ: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Literalii SQL pot conține parametri: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Datorită cărora putem crea combinații interesante: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Obținerea datelor -================= - - -Scurtături pentru interogări SELECT ------------------------------------ - -Pentru a simplifica încărcarea datelor, `Connection` oferă câteva scurtături care combină apelul `query()` cu următorul `fetch*()`. Aceste metode acceptă aceiași parametri ca `query()`, adică interogarea SQL și parametrii opționali. O descriere completă a metodelor `fetch*()` găsiți [mai jos |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Execută interogarea și returnează primul rând ca obiect `Row` -| `fetchAll($sql, ...$params): array` | Execută interogarea și returnează toate rândurile ca array de obiecte `Row` -| `fetchPairs($sql, ...$params): array` | Execută interogarea și returnează un array asociativ, unde prima coloană reprezintă cheia și a doua valoarea -| `fetchField($sql, ...$params): mixed` | Execută interogarea și returnează valoarea primului câmp din primul rând -| `fetchList($sql, ...$params): ?array` | Execută interogarea și returnează primul rând ca array indexat - -Exemplu: - -```php -// fetchField() - returnează valoarea primei celule -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iterarea prin rânduri ---------------------------------- - -După executarea interogării, se returnează obiectul [ResultSet|api:Nette\Database\ResultSet], care permite parcurgerea rezultatelor în mai multe moduri. Cel mai simplu mod de a executa o interogare și de a obține rânduri este prin iterarea într-o buclă `foreach`. Această metodă este cea mai eficientă din punct de vedere al memoriei, deoarece returnează datele treptat și nu le stochează pe toate în memorie simultan. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` poate fi iterat o singură dată. Dacă aveți nevoie să iterați în mod repetat, trebuie mai întâi să încărcați datele într-un array, de exemplu folosind metoda `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Returnează un rând ca obiect `Row`. Dacă nu mai există alte rânduri, returnează `null`. Mută pointerul intern la următorul rând. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // încarcă primul rând -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Returnează toate rândurile rămase din `ResultSet` ca un array de obiecte `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // încarcă toate rândurile -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Returnează rezultatele ca un array asociativ. Primul argument specifică numele coloanei care va fi folosită ca cheie în array, al doilea argument specifică numele coloanei care va fi folosită ca valoare: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Dacă specificăm doar primul parametru, valoarea va fi întregul rând, adică obiectul `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -În cazul cheilor duplicate, se va folosi valoarea din ultimul rând. La utilizarea `null` ca cheie, array-ul va fi indexat numeric începând de la zero (atunci nu apar coliziuni): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativ, puteți specifica ca parametru un callback care va returna pentru fiecare rând fie valoarea însăși, fie o pereche cheie-valoare. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback-ul poate returna și un array cu perechea cheie & valoare: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Returnează valoarea primului câmp din rândul curent. Dacă nu mai există alte rânduri, returnează `null`. Mută pointerul intern la următorul rând. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // încarcă numele din primul rând -``` - - -fetchList(): ?array .[method] ------------------------------ - -Returnează un rând ca array indexat. Dacă nu mai există alte rânduri, returnează `null`. Mută pointerul intern la următorul rând. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Returnează numărul de rânduri afectate de ultima interogare `UPDATE` sau `DELETE`. Pentru `SELECT`, este numărul de rânduri returnate, dar acesta poate să nu fie cunoscut - în acest caz, metoda returnează `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Returnează numărul de coloane din `ResultSet`. - - -Informații despre interogări -============================ - -În scopuri de depanare, putem obține informații despre ultima interogare executată: - -```php -echo $database->getLastQueryString(); // afișează interogarea SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // afișează interogarea SQL -echo $result->getTime(); // afișează timpul de execuție în secunde -``` - -Pentru a afișa rezultatul ca tabel HTML, se poate folosi: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet oferă informații despre tipurile coloanelor: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column este de tip $type->type"; // de ex. 'id este de tip int' -} -``` - - -Logarea interogărilor ---------------------- - -Putem implementa propria logare a interogărilor. Evenimentul `onQuery` este un array de callback-uri care sunt apelate după fiecare interogare executată: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Query: ' . $result->getQueryString()); - $logger->info('Time: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); - } -}; -``` diff --git a/database/ro/transactions.texy b/database/ro/transactions.texy deleted file mode 100644 index 86fa0b3bf8..0000000000 --- a/database/ro/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Tranzacții -********** - -.[perex] -Tranzacțiile garantează că fie toate operațiunile din cadrul tranzacției sunt efectuate, fie niciuna nu este efectuată. Acestea sunt utile pentru a asigura consistența datelor în cazul operațiunilor mai complexe. - -Cel mai simplu mod de a utiliza tranzacțiile arată astfel: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Puteți scrie același lucru mult mai elegant folosind metoda `transaction()`. Aceasta acceptă un callback ca parametru, pe care îl execută în cadrul tranzacției. Dacă callback-ul se execută fără excepții, tranzacția este confirmată automat. Dacă apare o excepție, tranzacția este anulată (rollback), iar excepția este propagată mai departe. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Metoda `transaction()` poate returna și valori: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // returnează numărul de rânduri actualizate -}); -``` diff --git a/database/ru/@home.texy b/database/ru/@home.texy index 0927782214..412e6353f6 100644 --- a/database/ru/@home.texy +++ b/database/ru/@home.texy @@ -1,21 +1,18 @@ - - Поддерживаемые базы данных ========================== -Nette поддерживает следующие базы данных: - -|* Сервер базы данных |* Имя DSN |* Поддержка в Core |* Поддержка в Explorer -| MySQL (>= 5.1) | mysql | ДА | ДА -| PostgreSQL (>= 9.0) | pgsql | ДА | ДА -| Sqlite 3 (>= 3.8) | sqlite | ДА | ДА -| Oracle | oci | ДА | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ДА | ДА -| MS SQL (PDO_DBLIB) | mssql | ДА | - -| ODBC | odbc | ДА | - +Поддерживаются такие серверы баз данных: +|* Сервер базы данных |* Имя DSN |* Поддержка Core |* Поддержка Explorer +| MySQL (>= 5.1) | mysql | ДА | ДА +| PostgreSQL (>= 9.0) | pgsql | ДА | ДА +| Sqlite 3 (>= 3.8) | sqlite | ДА | ДА +| Oracle | oci | ДА | - +| MS SQL (PDO_SQLSRV) | sqlsrv | ДА | ДА +| MS SQL (PDO_DBLIB) | mssql | ДА | - +| ODBC | odbc | ДА | - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database существенно упрощает получение данных из базы данных без необходимости писать SQL-запросы. Он выполняет эффективные запросы и не передает лишние данные.}} +{{maintitle: Nette Database - прекрасный слой работы с базой данных для PHP}} +{{description: Nette Database значительно упрощает получение данных из базы без написания SQL-запросов. Он выполняет эффективные запросы и не передаёт лишних данных.}} diff --git a/database/ru/@left-menu.texy b/database/ru/@left-menu.texy index 91864ddc35..655f71354f 100644 --- a/database/ru/@left-menu.texy +++ b/database/ru/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** -- [Введение |guide] -- [SQL-подход |sql way] -- [Explorer |Explorer] -- [Транзакции |transactions] -- [Исключения |exceptions] -- [Рефлексия |reflection] -- [Маппинг |mapping] -- [Конфигурация |configuration] +- [Первые шаги |guide] +- [SQL-подход|sql-way] +- [Explorer|explorer] +- [Транзакции|transactions] +- [Исключения|exceptions] +- [Рефлексия|reflection] +- [Преобразование типов |type-conversion] +- [Конфигурация|configuration] - [Риски безопасности |security] -- [Обновление |en:upgrading] +- [Обновление|upgrading] + + +Дополнительные материалы +************************ +- [Документация Nette |nette:] +- [Nette Application |application:how-it-works] +- [Utilities |utils:] +- [Лучшие практики |best-practices:] +- [Устранение неполадок |nette:troubleshooting] diff --git a/database/ru/configuration.texy b/database/ru/configuration.texy index 58dd6654f1..b6250e6811 100644 --- a/database/ru/configuration.texy +++ b/database/ru/configuration.texy @@ -2,15 +2,15 @@ ************************ .[perex] -Обзор опций конфигурации для Nette Database. +Обзор параметров конфигурации Nette Database. -Если вы не используете весь фреймворк, а только эту библиотеку, прочитайте, [как загрузить конфигурацию|bootstrap:]. +Если вы используете не весь фреймворк, а только эту библиотеку, прочитайте, [как загрузить конфигурацию|bootstrap:]. Одно соединение --------------- -Конфигурация одного соединения с базой данных: +Настройка одного соединения с базой данных: ```neon database: @@ -20,48 +20,48 @@ database: password: ... ``` -Создает сервисы `Nette\Database\Connection` и `Nette\Database\Explorer`, которые обычно передаются с помощью [autowiring |dependency-injection:autowiring], либо по ссылке на [их имя |#Сервисы DI]. +Так создаются сервисы `Nette\Database\Connection` и `Nette\Database\Explorer`, которые обычно передаются через [autowiring |dependency-injection:autowiring] или по ссылке на [их имя |#Сервисы DI]. Другие настройки: ```neon database: - # отображать панель базы данных в Tracy Bar? - debugger: ... # (bool) по умолчанию true + # показывать панель базы данных в Tracy Bar? + debugger: ... # (bool) по умолчанию включено, если Tracy активна - # отображать EXPLAIN запросов в Tracy Bar? + # показывать EXPLAIN запроса в Tracy Bar? explain: ... # (bool) по умолчанию true - # разрешить autowiring для этого соединения? - autowired: ... # (bool) по умолчанию true у первого соединения + # включить autowiring для этого соединения? + autowired: ... # (bool) по умолчанию true для первого соединения - # конвенции таблиц: discovered, static или имя класса + # соглашения о таблицах: discovered, static или имя класса conventions: discovered # (string) по умолчанию 'discovered' options: - # подключаться к базе данных только когда это необходимо? + # подключаться к базе данных только при необходимости? lazy: ... # (bool) по умолчанию false - # PHP класс драйвера базы данных + # класс драйвера базы данных PHP driverClass: # (string) - # только MySQL: устанавливает sql_mode + # только MySQL: задаёт sql_mode sqlmode: # (string) - # только MySQL: устанавливает SET NAMES + # только MySQL: задаёт SET NAMES charset: # (string) по умолчанию 'utf8mb4' # только MySQL: преобразует TINYINT(1) в bool - convertBoolean: # (bool) по умолчанию false + convertBoolean: # (bool) по умолчанию false - # возвращает столбцы с датой как immutable объекты (с версии 3.2.1) + # возвращает столбцы с датой как неизменяемые объекты (начиная с версии 3.2.1) newDateTime: # (bool) по умолчанию false - # только Oracle и SQLite: формат для сохранения даты + # только Oracle и SQLite: формат хранения даты formatDateTime: # (string) по умолчанию 'U' ``` -В ключе `options` можно указывать другие опции, которые вы найдете в [документации драйверов PDO |https://www.php.net/manual/en/pdo.drivers.php], например: +Ключ `options` может содержать и другие параметры из [документации драйвера PDO |https://www.php.net/manual/en/pdo.drivers.php], например: ```neon database: @@ -73,7 +73,7 @@ database: Несколько соединений -------------------- -В конфигурации мы можем определить и несколько соединений с базой данных, разделив их на именованные секции: +В конфигурации мы можем определить несколько соединений с базой данных, разделив их на именованные секции: ```neon database: @@ -86,23 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -Autowiring включен только для сервисов из первой секции. Это можно изменить с помощью `autowired: false` или `autowired: true`. +Autowiring включён только для сервисов из первой секции. Это можно изменить через `autowired: false` или `autowired: true`. Сервисы DI ---------- -Эти сервисы добавляются в DI-контейнер, где `###` представляет имя соединения: +Эти сервисы добавляются в DI-контейнер, где `###` обозначает имя соединения: -| Название | Тип | Описание -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | соединение с базой данных -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] +| Имя | Тип | Описание +|---------------------------|---------------------------------|--------------------------- +| `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`. +Если мы определяем только одно соединение, именами сервисов будут `database.default.connection` и `database.default.explorer`. Если мы определяем несколько соединений, как в примере выше, имена будут соответствовать секциям, то есть `database.main.connection`, `database.main.explorer`, а также `database.another.connection` и `database.another.explorer`. -Неавтовайренные сервисы передаем явно по ссылке на их имя: +Сервисы, не участвующие в autowiring, мы передаём явно, ссылаясь на их имя: ```neon services: diff --git a/database/ru/exceptions.texy b/database/ru/exceptions.texy index 58fdc27104..7aa8625103 100644 --- a/database/ru/exceptions.texy +++ b/database/ru/exceptions.texy @@ -1,22 +1,25 @@ Исключения ********** -Nette Database использует иерархию исключений. Базовым классом является `Nette\Database\DriverException`, который наследует от `PDOException` и предоставляет расширенные возможности для работы с ошибками базы данных: +Nette Database использует иерархию исключений. Базовый класс - `Nette\Database\DriverException`, который расширяет `PDOException` и даёт дополнительные возможности для работы с ошибками базы данных: -- Метод `getDriverCode()` возвращает код ошибки от драйвера базы данных -- Метод `getSqlState()` возвращает код SQLSTATE -- Методы `getQueryString()` и `getParameters()` позволяют получить исходный запрос и его параметры +- Метод `getDriverCode()` возвращает код ошибки от драйвера базы данных. +- Метод `getSqlState()` возвращает код SQLSTATE. +- Методы `getQueryString()` и `getParameters()` позволяют получить исходный запрос и его параметры. -От `DriverException` наследуют следующие специализированные исключения: +Класс `DriverException` расширяют следующие специализированные исключения: -- `ConnectionException` - сигнализирует о сбое подключения к серверу базы данных -- `ConstraintViolationException` - базовый класс для нарушений ограничений базы данных, от которого наследуют: - - `ForeignKeyConstraintViolationException` - нарушение внешнего ключа - - `NotNullConstraintViolationException` - нарушение ограничения NOT NULL - - `UniqueConstraintViolationException` - нарушение уникальности значения +- `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, который уже существует в базе данных (при условии, что столбец email имеет уникальный индекс). +Следующий пример показывает, как перехватить `UniqueConstraintViolationException`, которое возникает при попытке вставить пользователя с адресом электронной почты, уже существующим в базе данных (при условии, что у столбца `email` есть уникальный индекс): ```php try { @@ -26,9 +29,9 @@ try { 'password' => $hashedPassword, ]); } catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Пользователь с этим email уже существует.'; + echo 'A user with this email already exists.'; } catch (Nette\Database\DriverException $e) { - echo 'Произошла ошибка при регистрации: ' . $e->getMessage(); + echo 'An error occurred during registration: ' . $e->getMessage(); } ``` diff --git a/database/ru/explorer.texy b/database/ru/explorer.texy index 1f82982676..2a2f27de84 100644 --- a/database/ru/explorer.texy +++ b/database/ru/explorer.texy @@ -3,45 +3,45 @@ Database Explorer <div class=perex> -Explorer предлагает интуитивно понятный и эффективный способ работы с базой данных. Он автоматически заботится о связях между таблицами и оптимизации запросов, так что вы можете сосредоточиться на своем приложении. Работает сразу без настройки. Если вам нужен полный контроль над SQL-запросами, вы можете использовать [SQL-подход |SQL way]. +Explorer предлагает интуитивный и эффективный способ работы с базой данных. Он сам заботится о связях между таблицами и оптимизирует запросы, так что вы можете сосредоточиться на логике приложения. Работает сразу, без всякой настройки. Если вам нужен полный контроль над SQL-запросами, воспользуйтесь [SQL-подходом |SQL way]. -- Работа с данными естественна и легко понятна -- Генерирует оптимизированные SQL-запросы, которые загружают только необходимые данные -- Обеспечивает легкий доступ к связанным данным без необходимости писать JOIN-запросы -- Работает мгновенно без какой-либо конфигурации или генерации сущностей +- Работа с данными естественна и понятна +- Порождает оптимизированные SQL-запросы, которые получают только нужные данные +- Даёт лёгкий доступ к связанным данным без написания JOIN-запросов +- Работает сразу, без всякой настройки и порождения сущностей </div> -С Explorer вы начинаете с вызова метода `table()` объекта [api:Nette\Database\Explorer] (подробности о подключении см. в главе [Подключение и конфигурация |guide#Подключение и конфигурация]): +Работа с 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]: +Метод возвращает объект [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' + echo $book->title; // выводит столбец 'title' + echo $book->author_id; // выводит столбец 'author_id' } ``` -Explorer существенно упрощает работу со [связями между таблицами |#Связи между таблицами]. Следующий пример показывает, как легко можно вывести данные из связанных таблиц (книги и их авторы). Обратите внимание, что нам не нужно писать никаких JOIN-запросов, Nette создаст их за нас: +Explorer сильно упрощает работу со [связями между таблицами |#Связи между таблицами]. Следующий пример показывает, как легко вывести данные из связанных таблиц (книги и их авторы). Обратите внимание, что писать JOIN-запросы не нужно, Nette порождает их за нас: ```php $books = $explorer->table('book'); foreach ($books as $book) { echo 'Книга: ' . $book->title; - echo 'Автор: ' . $book->author->name; // создаст JOIN к таблице 'author' + echo 'Автор: ' . $book->author->name; // создаёт JOIN с таблицей 'author' } ``` -Nette Database Explorer оптимизирует запросы, чтобы они были максимально эффективными. Вышеуказанный пример выполнит только два SELECT-запроса, независимо от того, обрабатываем ли мы 10 или 10 000 книг. +Nette Database Explorer оптимизирует запросы так, чтобы они были максимально эффективными. Приведённый выше пример выполняет всего два SELECT-запроса независимо от того, обрабатываем ли мы 10 или 10 000 книг. -Кроме того, Explorer отслеживает, какие столбцы используются в коде, и загружает из базы данных только их, тем самым экономя дополнительную производительность. Это поведение полностью автоматическое и адаптивное. Если вы позже измените код и начнете использовать другие столбцы, Explorer автоматически изменит запросы. Вам не нужно ничего настраивать или думать о том, какие столбцы вам понадобятся - оставьте это Nette. +Кроме того, Explorer отслеживает, какие столбцы используются в коде, и получает из базы данных только их, что даёт дополнительный выигрыш в производительности. Это поведение полностью автоматическое и адаптивное. Если позже вы измените код и станете использовать другие столбцы, Explorer сам подстроит запросы. Ничего не нужно настраивать и не нужно думать о том, какие столбцы понадобятся, оставьте это Nette. Фильтрация и сортировка @@ -50,47 +50,47 @@ Nette Database Explorer оптимизирует запросы, чтобы он Класс `Selection` предоставляет методы для фильтрации и сортировки выборки данных. .[language-php] -| `where($condition, ...$params)` | Добавляет условие WHERE. Несколько условий соединяются оператором AND -| `whereOr(array $conditions)` | Добавляет группу условий WHERE, соединенных оператором OR -| `wherePrimary($value)` | Добавляет условие WHERE по первичному ключу -| `order($columns, ...$params)` | Устанавливает сортировку ORDER BY -| `select($columns, ...$params)` | Указывает столбцы, которые должны быть загружены -| `limit($limit, $offset = null)` | Ограничивает количество строк (LIMIT) и опционально устанавливает OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Устанавливает пагинацию -| `group($columns, ...$params)` | Группирует строки (GROUP BY) -| `having($condition, ...$params)` | Добавляет условие HAVING для фильтрации сгруппированных строк +| `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 для фильтрации сгруппированных строк | -Методы можно вызывать цепочкой (так называемый [fluent interface |nette:introduction-to-object-oriented-programming#Текучие интерфейсы Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. +Методы можно объединять в цепочку (так называемый [текучий интерфейс |nette:introduction-to-object-oriented-programming#Текучие интерфейсы]): `$table->where(...)->order(...)->limit(...)`. -В этих методах вы также можете использовать специальную нотацию для доступа к [данным из связанных таблиц |#Запросы через связанные таблицы]. +В этих методах можно также использовать особые обозначения для доступа к [данным из связанных таблиц |#Запросы через связанные таблицы]. Экранирование и идентификаторы ------------------------------ -Методы автоматически экранируют параметры и заключают идентификаторы (имена таблиц и столбцов) в кавычки, тем самым предотвращая SQL-инъекции. Для правильной работы необходимо соблюдать несколько правил: +Методы автоматически экранируют параметры и заключают идентификаторы (имена таблиц и столбцов) в кавычки, что предотвращает SQL injection. Чтобы всё работало правильно, нужно соблюдать несколько правил: -- Ключевые слова, имена функций, процедур и т.д. пишите **заглавными буквами**. +- Ключевые слова, имена функций, процедур и т. п. пишите **заглавными буквами**. - Имена столбцов и таблиц пишите **строчными буквами**. - Строки всегда передавайте через **параметры**. ```php -where('name = ' . $name); // КРИТИЧЕСКАЯ УЯЗВИМОСТЬ: SQL-инъекция -where('name LIKE "%search%"'); // НЕПРАВИЛЬНО: усложняет автоматическое заключение в кавычки -where('name LIKE ?', '%search%'); // ПРАВИЛЬНО: значение передано через параметр +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('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-операторов. +Фильтрует результаты с помощью условий WHERE. Его сила в том, что он умно обрабатывает разные типы значений и сам выбирает подходящие SQL-операторы. -Основное использование: +Базовое использование: ```php $table->where('id', $value); // WHERE `id` = 123 @@ -98,26 +98,26 @@ $table->where('id > ?', $value); // WHERE `id` > 123 $table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' ``` -Благодаря автоматическому определению подходящих операторов нам не нужно решать различные специальные случаи. Nette решит их за нас: +Благодаря автоматическому определению подходящего оператора вам не нужно разбираться с разными особыми случаями, 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); Внимание - этот синтаксис не поддерживается +$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`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -Условия можно передать также в виде массива, элементы которого соединяются с помощью AND: +Условия можно передать и массивом, элементы которого объединяются оператором AND: ```php // WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -В массиве можно использовать пары ключ => значение, и Nette снова автоматически выберет правильные операторы: +В массиве можно использовать пары ключ => значение, и Nette снова сам выберет верные операторы: ```php // WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) @@ -147,23 +147,23 @@ $table->where([ ]); ``` -В массиве можно комбинировать SQL-выражения с заполнителями в виде вопросительных знаков и несколькими параметрами. Это подходит для сложных условий с точно определенными операторами: +В массиве можно сочетать SQL-выражения с подстановками и несколькими параметрами. Это удобно для сложных условий с точно заданными операторами: ```php // WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) $table->where([ 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // два параметра передаем как массив + 'ROUND(score, ?) > ?' => [2, 75.5], // два параметра передаются массивом ]); ``` -Множественные вызовы `where()` автоматически соединяют условия с помощью AND. +Многократные вызовы `where()` автоматически объединяют условия оператором AND. whereOr(array $parameters): static .[method] -------------------------------------------- -Подобно `where()` добавляет условия, но с тем отличием, что соединяет их с помощью OR: +Похож на `where()`: тоже добавляет условия, но объединяет их оператором OR: ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -Здесь также можно использовать более сложные выражения: +И здесь можно использовать более сложные выражения: ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -187,7 +187,7 @@ $table->whereOr([ wherePrimary(mixed $key): static .[method] ------------------------------------------ -Добавляет условие для первичного ключа таблицы: +Добавляет условие по первичному ключу таблицы: ```php // WHERE `id` = 123 @@ -197,7 +197,7 @@ $table->wherePrimary(123); $table->wherePrimary([1, 2, 3]); ``` -Если таблица имеет составной первичный ключ (например, `foo_id`, `bar_id`), передаем его как массив: +Если у таблицы составной первичный ключ (например, `foo_id`, `bar_id`), передайте его массивом: ```php // WHERE `foo_id` = 1 AND `bar_id` = 5 @@ -214,7 +214,7 @@ $table->wherePrimary([ order(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Определяет порядок, в котором будут возвращены строки. Можно сортировать по одному или нескольким столбцам, в убывающем или возрастающем порядке, или по собственному выражению: +Задаёт порядок, в котором возвращаются строки. Сортировать можно по одному или нескольким столбцам, по возрастанию или по убыванию, а также по собственному выражению: ```php $table->order('created'); // ORDER BY `created` @@ -227,18 +227,18 @@ $table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC select(string $columns, ...$parameters): static .[method] --------------------------------------------------------- -Указывает столбцы, которые должны быть возвращены из базы данных. По умолчанию Nette Database Explorer возвращает только те столбцы, которые реально используются в коде. Метод `select()` мы используем в случаях, когда нужно вернуть специфические выражения: +Указывает столбцы, которые нужно вернуть из базы данных. По умолчанию 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: +Псевдонимы, заданные через `AS`, затем доступны как свойства объекта `ActiveRow`: ```php foreach ($table as $row) { - echo $row->formatted_date; // доступ к псевдониму + echo $row->formatted_date; // обращение к псевдониму } ``` @@ -246,24 +246,24 @@ foreach ($table as $row) { limit(?int $limit, ?int $offset = null): static .[method] --------------------------------------------------------- -Ограничивает количество возвращаемых строк (LIMIT) и опционально позволяет установить смещение: +Ограничивает количество возвращаемых строк (LIMIT) и при необходимости позволяет задать смещение: ```php -$table->limit(10); // LIMIT 10 (вернет первые 10 строк) +$table->limit(10); // LIMIT 10 (вернёт первые 10 строк) $table->limit(10, 20); // LIMIT 10 OFFSET 20 ``` -Для пагинации удобнее использовать метод `page()`. +Для постраничного вывода уместнее использовать метод `page()`. page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] ------------------------------------------------------------------------- -Упрощает пагинацию результатов. Принимает номер страницы (считая от 1) и количество элементов на страницу. Опционально можно передать ссылку на переменную, в которую будет сохранено общее количество страниц: +Облегчает постраничный вывод результатов. Принимает номер страницы (начиная с 1) и количество элементов на странице. Необязательно можно передать ссылку на переменную, в которую будет записано общее количество страниц: ```php $numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); echo "Всего страниц: $numOfPages"; ``` @@ -271,10 +271,10 @@ echo "Всего страниц: $numOfPages"; group(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Группирует строки по указанным столбцам (GROUP BY). Обычно используется в сочетании с агрегатными функциями: +Группирует строки по указанным столбцам (GROUP BY). Обычно используется вместе с агрегатными функциями: ```php -// Подсчитывает количество продуктов в каждой категории +// Подсчитывает количество товаров в каждой категории $table->select('category_id, COUNT(*) AS count') ->group('category_id'); ``` @@ -283,10 +283,10 @@ $table->select('category_id, COUNT(*) AS count') having(string $having, ...$parameters): static .[method] -------------------------------------------------------- -Устанавливает условие для фильтрации сгруппированных строк (HAVING). Можно использовать в сочетании с методом `group()` и агрегатными функциями: +Задаёт условие для фильтрации сгруппированных строк (HAVING). Его можно использовать вместе с методом `group()` и агрегатными функциями: ```php -// Находит категории, в которых более 100 продуктов +// Находит категории, в которых больше 100 товаров $table->select('category_id, COUNT(*) AS count') ->group('category_id') ->having('count > ?', 100); @@ -296,23 +296,23 @@ $table->select('category_id, COUNT(*) AS count') Чтение данных ============= -Для чтения данных из базы данных у нас есть несколько полезных методов: +Для чтения данных из базы доступно несколько полезных методов: .[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 +| `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] предназначен только для чтения. Это означает, что нельзя изменять значения его свойств. Это ограничение обеспечивает консистентность данных и предотвращает неожиданные побочные эффекты. Данные загружаются из базы данных, и любое изменение должно быть выполнено явно и контролируемо. +Объект [ActiveRow |api:Nette\Database\Table\ActiveRow] доступен только для чтения. Это значит, что менять значения его свойств нельзя. Такое ограничение обеспечивает согласованность данных и предотвращает неожиданные побочные эффекты. Данные загружаются из базы, и любые изменения должны выполняться явно и контролируемо. -`foreach` - итерация по всем строкам ------------------------------------- +`foreach` - обход всех строк +---------------------------- -Самый простой способ выполнить запрос и получить строки — это итерация в цикле `foreach`. Автоматически запускает SQL-запрос. +Проще всего выполнить запрос и получить строки обходом через цикл `foreach`. Он автоматически выполнит SQL-запрос. ```php $books = $explorer->table('book'); @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -Выполняет SQL-запрос и возвращает строку по первичному ключу, или `null`, если она не существует. +Выполняет SQL-запрос и возвращает строку по первичному ключу либо `null`, если такой строки нет. ```php -$book = $explorer->table('book')->get(123); // вернет ActiveRow с ID 123 или null +$book = $explorer->table('book')->get(123); // вернёт ActiveRow с ID 123 или null if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -Возвращает строку и перемещает внутренний указатель на следующую. Если больше нет строк, возвращает `null`. +Возвращает текущую строку и сдвигает внутренний указатель на следующую. Если строк больше нет, возвращает `null`. ```php $books = $explorer->table('book'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { 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`: +Если задан только первый параметр, значением будет вся строка, то есть объект `ActiveRow`: ```php $authors = $explorer->table('author')->fetchPairs('id'); // [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] ``` -В случае дублирующихся ключей используется значение из последней строки. При использовании `null` в качестве ключа массив будет индексирован численно с нуля (тогда коллизий не происходит): +При повторяющихся ключах используется значение из последней строки. Если в качестве ключа указать `null`, массив будет пронумерован с нуля (тогда коллизий не возникает): ```php $authors = $explorer->table('author')->fetchPairs(null, 'name'); @@ -377,24 +377,24 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Альтернативно, вы можете указать в качестве параметра callback, который для каждой строки будет возвращать либо само значение, либо пару ключ-значение. +Как вариант, параметром можно передать callback, который для каждой строки вернёт либо само значение, либо пару ключ - значение. ```php $titles = $explorer->table('book') ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Первая книга (Ян Новак)', ...] +// ['First Book (John Novak)', ...] -// Callback может также возвращать массив с парой ключ & значение: +// Callback может вернуть и массив с парой ключ и значение: $titles = $explorer->table('book') ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Первая книга' => 'Ян Новак', ...] +// ['First Book' => 'John Novak', ...] ``` fetchAll(): array .[method] --------------------------- -Возвращает все строки как ассоциативный массив объектов `ActiveRow`, где ключами являются значения первичных ключей. +Возвращает все строки ассоциативным массивом объектов `ActiveRow`, где ключами служат значения первичного ключа. ```php $allBooks = $explorer->table('book')->fetchAll(); @@ -410,16 +410,16 @@ count(): int .[method] ```php $table->where('category', 1); $count = $table->count(); -$count = count($table); // альтернатива +$count = count($table); // вариант ``` -Внимание, `count()` с параметром выполняет агрегатную функцию COUNT в базе данных, см. ниже. +Замечание: `count()` с параметром выполняет в базе данных агрегатную функцию COUNT, см. ниже. ActiveRow::toArray(): array .[method] ------------------------------------- -Преобразует объект `ActiveRow` в ассоциативный массив, где ключами являются имена столбцов, а значениями — соответствующие данные. +Преобразует объект `ActiveRow` в ассоциативный массив, где ключи - имена столбцов, а значения - соответствующие данные. ```php $book = $explorer->table('book')->get(1); @@ -431,33 +431,33 @@ $bookArray = $book->toArray(); Агрегация ========= -Класс `Selection` предоставляет методы для легкого выполнения агрегатных функций (COUNT, SUM, MIN, MAX, AVG и т.д.). +Класс `Selection` предоставляет методы для удобного выполнения агрегатных функций (COUNT, SUM, MIN, MAX, AVG и т. д.). .[language-php] -| `count($expr)` | Подсчитывает количество строк -| `min($expr)` | Возвращает минимальное значение в столбце -| `max($expr)` | Возвращает максимальное значение в столбце -| `sum($expr)` | Возвращает сумму значений в столбце -| `aggregation($function)` | Позволяет выполнить любую агрегатную функцию. Напр. `AVG()`, `GROUP_CONCAT()` +| `count($expr)` | Подсчитывает количество строк | +| `min($expr)` | Возвращает наименьшее значение в столбце | +| `max($expr)` | Возвращает наибольшее значение в столбце | +| `sum($expr)` | Возвращает сумму значений в столбце | +| `aggregation($function)` | Позволяет выполнить любую агрегатную функцию, например `AVG()` или `GROUP_CONCAT()` | count(string $expr): int .[method] ---------------------------------- -Выполняет SQL-запрос с функцией COUNT и возвращает результат. Метод используется для определения, сколько строк соответствует определенному условию: +Выполняет SQL-запрос с функцией COUNT и возвращает результат. Метод используется, чтобы узнать, сколько строк соответствует определённому условию: ```php $count = $table->count('*'); // SELECT COUNT(*) FROM `table` $count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` ``` -Внимание, [#count()] без параметра только возвращает количество строк в объекте `Selection`. +Замечание: [#count()] без параметра лишь возвращает количество строк в объекте `Selection`. min(string $expr) и max(string $expr) .[method] ----------------------------------------------- -Методы `min()` и `max()` возвращают минимальное и максимальное значение в указанном столбце или выражении: +Методы `min()` и `max()` возвращают наименьшее и наибольшее значение в указанном столбце или выражении: ```php // SELECT MAX(`price`) FROM `products` WHERE `active` = 1 @@ -466,8 +466,8 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- Возвращает сумму значений в указанном столбце или выражении: @@ -478,66 +478,66 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +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(значение)` по сгруппированным строкам), в качестве второго аргумента указываем агрегатную функцию, которая должна быть применена к этим промежуточным результатам: +Если нам нужно агрегировать результаты, которые сами уже получены агрегатной функцией и группировкой (например, `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. +В этом примере мы сначала вычисляем суммарную цену товаров в каждой категории (`SUM(price * stock) AS category_total`) и группируем результаты по `category_id`. Затем через `aggregation('SUM(category_total)', 'SUM')` складываем эти промежуточные суммы `category_total`. Второй аргумент `'SUM'` указывает, что к промежуточным результатам нужно применить функцию SUM. -Insert, Update & Delete -======================= +Вставка, изменение и удаление +============================= -Nette Database Explorer упрощает вставку, обновление и удаление данных. Все указанные методы в случае ошибки выбрасывают исключение `Nette\Database\DriverException`. +Nette Database Explorer упрощает вставку, изменение и удаление данных. Все упомянутые методы в случае ошибки выбрасывают `Nette\Database\DriverException`. Selection::insert(iterable $data) .[method] ------------------------------------------- -Вставляет новые записи в таблицу. +Вставляет в таблицу новые записи. **Вставка одной записи:** -Новую запись передаем как ассоциативный массив или итерируемый объект (например, ArrayHash, используемый в [формах |forms:]), где ключи соответствуют именам столбцов в таблице. +Новую запись передайте ассоциативным массивом или итерируемым объектом (например, `ArrayHash`, который используется в [формах |forms:]), где ключи соответствуют именам столбцов таблицы. -Если в таблице определен первичный ключ, метод возвращает объект `ActiveRow`, который перезагружается из базы данных, чтобы учесть возможные изменения, выполненные на уровне базы данных (триггеры, значения по умолчанию столбцов, вычисления автоинкрементных столбцов). Этим обеспечивается консистентность данных, и объект всегда содержит актуальные данные из базы данных. Если однозначного первичного ключа нет, возвращает переданные данные в виде массива. +Если у таблицы задан первичный ключ, метод возвращает объект `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; // Выведет время создания, если оно установлено триггером +// $row - экземпляр ActiveRow, содержащий полные данные вставленной строки, +// включая автоматически порождённый ID и любые изменения, сделанные триггерами +echo $row->id; // Выведет ID только что вставленного пользователя +echo $row->created_at; // Выведет время создания, если его задаёт триггер ``` -**Вставка нескольких записей одновременно:** +**Вставка нескольких записей сразу:** -Метод `insert()` позволяет вставить несколько записей с помощью одного SQL-запроса. В этом случае возвращает количество вставленных строк. +Метод `insert()` позволяет вставить несколько записей одним SQL-запросом. В этом случае он возвращает количество вставленных строк. ```php $insertedRows = $explorer->table('users')->insert([ @@ -554,7 +554,7 @@ $insertedRows = $explorer->table('users')->insert([ // $insertedRows будет 2 ``` -В качестве параметра можно также передать объект `Selection` с выборкой данных. +Параметром можно передать и объект `Selection` с выборкой данных. ```php $newUsers = $explorer->table('potential_users') @@ -564,15 +564,15 @@ $newUsers = $explorer->table('potential_users') $insertedRows = $explorer->table('users')->insert($newUsers); ``` -**Вставка специальных значений:** +**Вставка особых значений:** -В качестве значений можно передавать и файлы, объекты DateTime или SQL-литералы: +В качестве значений можно передавать и файлы, объекты `DateTime` или SQL-литералы: ```php $explorer->table('users')->insert([ 'name' => 'John', 'created_at' => new DateTime, // преобразует в формат базы данных - 'avatar' => fopen('image.jpg', 'rb'), // вставит бинарное содержимое файла + 'avatar' => fopen('image.jpg', 'rb'), // вставит двоичное содержимое файла 'uuid' => $explorer::literal('UUID()'), // вызовет функцию UUID() ]); ``` @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -Обновляет строки в таблице согласно указанному фильтру. Возвращает количество действительно измененных строк. +Изменяет строки в таблице согласно заданному фильтру. Возвращает количество действительно изменённых строк. -Изменяемые столбцы передаем как ассоциативный массив или итерируемый объект (например, ArrayHash, используемый в [формах |forms:]), где ключи соответствуют именам столбцов в таблице: +Изменяемые столбцы передайте ассоциативным массивом или итерируемым объектом (например, `ArrayHash`, который используется в [формах |forms:]), где ключи соответствуют именам столбцов таблицы: ```php $affected = $explorer->table('users') @@ -601,8 +601,8 @@ $affected = $explorer->table('users') $explorer->table('users') ->where('id', 10) ->update([ - 'points+=' => 1, // увеличит значение столбца 'points' на 1 - 'coins-=' => 1, // уменьшит значение столбца 'coins' на 1 + 'points+=' => 1, // увеличивает значение столбца 'points' на 1 + 'coins-=' => 1, // уменьшает значение столбца 'coins' на 1 ]); // UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 ``` @@ -611,7 +611,7 @@ $explorer->table('users') Selection::delete(): int .[method] ---------------------------------- -Удаляет строки из таблицы согласно указанному фильтру. Возвращает количество удаленных строк. +Удаляет строки из таблицы согласно заданному фильтру. Возвращает количество удалённых строк. ```php $count = $explorer->table('users') @@ -621,31 +621,31 @@ $count = $explorer->table('users') ``` .[caution] -При вызове `update()` и `delete()` не забудьте с помощью `where()` указать строки, которые нужно изменить/удалить. Если `where()` не использовать, операция будет выполнена над всей таблицей! +При вызове `update()` или `delete()` не забудьте с помощью `where()` указать строки, которые нужно изменить или удалить. Если `where()` не использовать, операция выполнится над всей таблицей! ActiveRow::update(iterable $data): bool .[method] ------------------------------------------------- -Обновляет данные в строке базы данных, представленной объектом `ActiveRow`. В качестве параметра принимает итерируемый объект с данными, которые нужно обновить (ключи — имена столбцов). Для изменения числовых значений можно использовать операторы `+=` и `-=`: +Изменяет данные в строке базы данных, представленной объектом `ActiveRow`. Принимает итерируемую структуру с данными для изменения (ключи - имена столбцов). Для изменения числовых значений можно использовать операторы `+=` и `-=`: -После выполнения обновления `ActiveRow` автоматически перезагружается из базы данных, чтобы учесть возможные изменения, выполненные на уровне базы данных (например, триггеры). Метод возвращает true только если произошло действительное изменение данных. +После выполнения изменения `ActiveRow` автоматически перезагружается из базы данных, чтобы отразить изменения, сделанные на уровне базы (например, триггерами). Метод возвращает `true` только тогда, когда данные действительно изменились. ```php $article = $explorer->table('article')->get(1); $article->update([ - 'views += 1', // увеличим количество просмотров + 'views += 1', // увеличивает счётчик просмотров ]); echo $article->views; // Выведет текущее количество просмотров ``` -Этот метод обновляет только одну конкретную строку в базе данных. Для массового обновления нескольких строк используйте метод [#Selection::update()]. +Этот метод изменяет только одну конкретную строку в базе данных. Для массового изменения нескольких строк используйте метод [#Selection::update()]. -ActiveRow::delete() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -Удаляет строку из базы данных, которая представлена объектом `ActiveRow`. +Удаляет строку базы данных, представленную объектом `ActiveRow`. Возвращает количество удалённых строк, которое должно быть равно 1. ```php $book = $explorer->table('book')->get(1); @@ -658,47 +658,47 @@ $book->delete(); // Удалит книгу с ID 1 Связи между таблицами ===================== -В реляционных базах данных данные разделены на несколько таблиц и взаимосвязаны с помощью внешних ключей. Nette Database Explorer предлагает революционный способ работы с этими связями - без написания JOIN-запросов и необходимости что-либо конфигурировать или генерировать. +В реляционных базах данных данные разделены на несколько таблиц и связаны между собой внешними ключами. Nette Database Explorer предлагает революционный способ работы с этими связями: без написания JOIN-запросов и без необходимости что-либо настраивать или порождать. -Для иллюстрации работы со связями используем пример базы данных книг ([найдете его на GitHub |https://github.com/nette-examples/books]). В базе данных у нас есть таблицы: +Для демонстрации работы со связями воспользуемся примером базы данных книг ([найдёте её на 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 *] *** Структура базы данных .<> +[* db-schema-1-.webp *] *** Структура базы данных, используемой в примерах .<> -В нашем примере базы данных книг мы находим несколько типов отношений (хотя модель упрощена по сравнению с реальностью): +В нашем примере базы данных книг мы находим несколько типов связей (хотя модель упрощена по сравнению с реальностью): -- Один-ко-многим 1:N – каждая книга **имеет одного** автора, автор может написать **несколько** книг -- Ноль-ко-многим 0:N – книга **может иметь** переводчика, переводчик может перевести **несколько** книг -- Ноль-к-одному 0:1 – книга **может иметь** продолжение -- Многие-ко-многим M:N – книга **может иметь несколько** тегов, и тег может быть присвоен **нескольким** книгам +- **Один ко многим (1:N)** - у каждой книги **есть один** автор, автор может написать **несколько** книг. +- **Ноль ко многим (0:N)** - у книги **может быть** переводчик, переводчик может перевести **несколько** книг. +- **Ноль к одному (0:1)** - у книги **может быть** продолжение. +- **Многие ко многим (M:N)** - у книги **может быть несколько** тегов, а тег может быть присвоен **нескольким** книгам. -В этих отношениях всегда существует родительская и дочерняя таблица. Например, в отношении между автором и книгой таблица `author` является родительской, а `book` — дочерней - можно представить это так, что книга всегда "принадлежит" какому-то автору. Это проявляется и в структуре базы данных: дочерняя таблица `book` содержит внешний ключ `author_id`, который ссылается на родительскую таблицу `author`. +В этих связях всегда есть **родительская таблица** и **дочерняя таблица**. Например, в связи между авторами и книгами таблица `author` родительская, а таблица `book` дочерняя: можно представить себе, что книга всегда "принадлежит" какому-то автору. Это отражено и в структуре базы данных: дочерняя таблица `book` содержит внешний ключ `author_id`, ссылающийся на родительскую таблицу `author`. -Если нам нужно вывести книги, включая имена их авторов, у нас есть два варианта. Либо получить данные одним SQL-запросом с помощью JOIN: +Если нам нужно вывести книги вместе с именами их авторов, у нас есть две возможности. Либо получить данные одним SQL-запросом с использованием JOIN: ```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Либо загрузить данные в два этапа - сначала книги, а затем их авторов - и потом собрать их в PHP: +Либо получить данные в два шага - сначала книги, потом их авторов - и затем собрать их в PHP: ```sql SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id авторов полученных книг +SELECT * FROM author WHERE id IN (1, 2, 3); -- ID авторов выбранных книг ``` -Второй подход на самом деле более эффективен, хотя это может показаться удивительным. Данные загружаются только один раз и могут быть лучше использованы в кеше. Именно таким образом работает Nette Database Explorer - все решает под капотом и предлагает вам элегантный API: +Второй подход на самом деле **эффективнее**, хотя это может показаться неожиданным. Данные получаются только один раз и лучше используются в кеше. Именно так работает 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; + echo 'написал: ' . $book->author->name; // $book->author - запись из таблицы 'author' + echo 'перевёл: ' . $book->translator?->name; } ``` @@ -706,26 +706,26 @@ foreach ($books as $book) { Доступ к родительской таблице ----------------------------- -Доступ к родительской таблице прост. Речь идет об отношениях типа *книга имеет автора* или *книга может иметь переводчика*. Связанную запись получаем через свойство объекта ActiveRow - его имя соответствует имени столбца с внешним ключом без суффикса `_id`: +Доступ к родительской таблице прост. Речь о связях вроде *у книги есть автор* или *у книги может быть переводчик*. Связанную запись получаем через свойство объекта ActiveRow, имя которого соответствует имени столбца внешнего ключа без суффикса `_id`: ```php $book = $explorer->table('book')->get(1); -echo $book->author->name; // найдет автора по столбцу author_id -echo $book->translator?->name; // найдет переводчика по translator_id +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 оператор `?->`. +При обращении к свойству `$book->author` Explorer ищет в таблице `book` столбец, имя которого содержит строку `author` (то есть `author_id`). По значению в этом столбце он загружает соответствующую запись из таблицы `author` и возвращает её как `ActiveRow`. Точно так же `$book->translator` использует столбец `translator_id`. Поскольку столбец `translator_id` может содержать `null`, в коде мы используем nullsafe-оператор `?->`. -Альтернативный путь предлагает метод `ref()`, который принимает два аргумента: имя целевой таблицы и имя связующего столбца, и возвращает экземпляр `ActiveRow` или `null`: +Другой подход предлагает метод `ref()`, который принимает два аргумента - имя целевой таблицы и имя связующего столбца - и возвращает экземпляр `ActiveRow` или `null`: ```php echo $book->ref('author', 'author_id')->name; // связь с автором echo $book->ref('author', 'translator_id')->name; // связь с переводчиком ``` -Метод `ref()` удобен, если нельзя использовать доступ через свойство, потому что таблица содержит столбец с таким же именем (т.е. `author`). В остальных случаях рекомендуется использовать доступ через свойство, который более читабелен. +Метод `ref()` пригодится, если нельзя использовать обращение через свойство, например потому что таблица содержит столбец с таким же именем (то есть `author`). В остальных случаях для лучшей читаемости рекомендуется использовать обращение через свойство. -Explorer автоматически оптимизирует запросы к базе данных. Когда мы проходим по книгам в цикле и обращаемся к их связанным записям (авторам, переводчикам), Explorer не генерирует запрос для каждой книги отдельно. Вместо этого он выполняет только один SELECT для каждого типа связи, тем самым значительно снижая нагрузку на базу данных. Например: +Explorer автоматически оптимизирует запросы к базе данных. Когда мы обходим книги в цикле и обращаемся к их связанным записям (авторам, переводчикам), Explorer не порождает запрос для каждой книги отдельно. Вместо этого он выполняет только **один SELECT-запрос для каждого типа связи**, что существенно снижает нагрузку на базу данных. Например: ```php $books = $explorer->table('book'); @@ -736,53 +736,53 @@ foreach ($books as $book) { } ``` -Этот код вызовет только эти три молниеносных запроса к базе данных: +Этот код выполнит к базе данных всего три молниеносных запроса: ```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 выбранных книг +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], которые анализируют внешние ключи и позволяют легко работать с существующими отношениями между таблицами. +Логика поиска связующего столбца задаётся реализацией [Conventions |api:Nette\Database\Conventions]. Мы рекомендуем использовать [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], которая анализирует внешние ключи и позволяет легко работать с существующими связями между таблицами. Доступ к дочерней таблице ------------------------- -Доступ к дочерней таблице работает в обратном направлении. Теперь мы спрашиваем, *какие книги написал этот автор* или *перевел этот переводчик*. Для этого типа запроса мы используем метод `related()`, который возвращает `Selection` со связанными записями. Посмотрим на пример: +Доступ к дочерней таблице работает в обратном направлении. Теперь мы спрашиваем, *какие книги написал этот автор* или *какие книги перевёл этот переводчик*. Для такого запроса служит метод `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"; + echo "Перевёл: $book->title"; } ``` -Метод `related()` принимает описание соединения как один аргумент с точечной нотацией или как два отдельных аргумента: +Метод `related()` принимает описание связи одним аргументом с точечной записью либо двумя отдельными аргументами: ```php $author->related('book.translator_id'); // один аргумент $author->related('book', 'translator_id'); // два аргумента ``` -Explorer может автоматически определить правильный связующий столбец на основе имени родительской таблицы. В данном случае соединение происходит через столбец `book.author_id`, поскольку имя исходной таблицы — `author`: +Explorer умеет автоматически определить верный связующий столбец по имени родительской таблицы. В данном случае он соединяет через столбец `book.author_id`, потому что имя исходной таблицы - `author`: ```php $author->related('book'); // использует book.author_id ``` -Если бы существовало несколько возможных соединений, Explorer выбросил бы исключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. +Если возможных соединений несколько, Explorer выбросит [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. -Метод `related()` можно, конечно, использовать и при прохождении нескольких записей в цикле, и Explorer и в этом случае автоматически оптимизирует запросы: +Метод `related()` мы можем, разумеется, использовать и при обходе нескольких записей в цикле, и Explorer автоматически оптимизирует запросы и в этом случае: ```php $authors = $explorer->table('author'); @@ -794,53 +794,53 @@ foreach ($authors as $author) { } ``` -Этот код сгенерирует только два молниеносных SQL-запроса: +Этот код породит всего два молниеносных SQL-запроса: ```sql SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id выбранных авторов +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ID выбранных авторов ``` -Связь Many-to-many ------------------- +Связь многие ко многим +---------------------- -Для связи многие-ко-многим (M:N) необходимо наличие связующей таблицы (в нашем случае `book_tag`), которая содержит два столбца с внешними ключами (`book_id`, `tag_id`). Каждый из этих столбцов ссылается на первичный ключ одной из связываемых таблиц. Для получения связанных данных сначала получаем записи из связующей таблицы с помощью `related('book_tag')`, а затем переходим к целевым данным: +Для связи многие ко многим (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; // выведет название тега через связующую таблицу + echo $bookTag->tag->name; // выводит имя тега через связующую таблицу } $tag = $explorer->table('tag')->get(1); -// или наоборот: выведет названия книг, отмеченных этим тегом +// или наоборот: выводит названия книг, помеченных этим тегом foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // выведет название книги + echo $bookTag->book->title; // выводит название книги } ``` -Explorer снова оптимизирует SQL-запросы до эффективной формы: +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 +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'ы. +В методах `where()`, `select()`, `order()` и `group()` можно использовать особые обозначения для доступа к столбцам из других таблиц. Explorer автоматически создаст нужные JOIN. -**Точечная нотация** (`родительская_таблица.столбец`) используется для отношения 1:N с точки зрения дочерней таблицы: +**Точечная запись** (`родительская_таблица.столбец`) используется для связи 1:N с точки зрения дочерней таблицы: ```php $books = $explorer->table('book'); -// Находит книги, автор которых имеет имя, начинающееся на 'Jon' +// Находит книги, имя автора которых начинается на 'Jon' $books->where('author.name LIKE ?', 'Jon%'); // Сортирует книги по имени автора по убыванию @@ -850,7 +850,7 @@ $books->order('author.name DESC'); $books->select('book.title, author.name'); ``` -**Двоеточная нотация** (`:дочерняя_таблица.столбец`) используется для отношения 1:N с точки зрения родительской таблицы: +**Запись с двоеточием** (`:дочерняя_таблица.столбец`) используется для связи 1:N с точки зрения родительской таблицы: ```php $authors = $explorer->table('author'); @@ -858,12 +858,12 @@ $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]. +В примере выше с записью через двоеточие (`:book.title`) столбец внешнего ключа не указан. Explorer автоматически определит верный столбец по имени родительской таблицы. В данном случае он соединяет через столбец `book.author_id`, потому что имя исходной таблицы - `author`. Если возможных соединений несколько, Explorer выбросит [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. Связующий столбец можно явно указать в скобках: @@ -872,10 +872,10 @@ $authors->select('*, COUNT(:book.id) AS book_count') $authors->where(':book(translator_id).title LIKE ?', '%PHP%'); ``` -Нотации можно объединять в цепочку для доступа через несколько таблиц: +Записи можно объединять в цепочку и обращаться к данным через несколько таблиц: ```php -// Находит авторов книг, отмеченных тегом 'PHP' +// Находит авторов книг, помеченных тегом 'PHP' $authors->where(':book:book_tag.tag.name', 'PHP') ->group('author.id'); ``` @@ -884,20 +884,20 @@ $authors->where(':book:book_tag.tag.name', 'PHP') Расширение условий для JOIN --------------------------- -Метод `joinWhere()` расширяет условия, которые указываются при соединении таблиц в SQL за ключевым словом `ON`. +Метод `joinWhere()` расширяет условия, задаваемые при соединении таблиц в SQL после ключевого слова `ON`. -Допустим, мы хотим найти книги, переведенные конкретным переводчиком: +Допустим, мы хотим найти книги, переведённые определённым переводчиком: ```php -// Находит книги, переведенные переводчиком по имени 'David' +// Находит книги, переведённые переводчиком по имени '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-выражения. +В условии `joinWhere()` можно использовать те же конструкции, что и в методе `where()`: операторы, подстановки, массивы значений или SQL-выражения. -Для более сложных запросов с несколькими JOIN'ами мы можем определить псевдонимы таблиц: +Для более сложных запросов с несколькими JOIN можно задать псевдонимы таблиц: ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // AND (`book_author`.`born` < 1950) ``` -Обратите внимание, что в то время как метод `where()` добавляет условия в клаузулу `WHERE`, метод `joinWhere()` расширяет условия в клаузуле `ON` при соединении таблиц. +Обратите внимание, что если метод `where()` добавляет условия в конструкцию `WHERE`, то метод `joinWhere()` расширяет условия в конструкции `ON` при соединении таблиц. diff --git a/database/ru/guide.texy b/database/ru/guide.texy index 2e644aa3d5..b6d7567f79 100644 --- a/database/ru/guide.texy +++ b/database/ru/guide.texy @@ -2,29 +2,29 @@ Nette Database ************** .[perex] -Nette Database — это мощный и элегантный слой базы данных для PHP, ориентированный на простоту и интеллектуальные функции. Он предлагает два способа работы с базой данных — [Explorer |Explorer] для быстрой разработки приложений или [SQL-подход |SQL way] для прямой работы с запросами. +Nette Database - мощный и изящный слой работы с базой данных для PHP, нацеленный на простоту и продуманные возможности. Он предлагает два способа работы с базой: [Explorer |explorer] для быстрой разработки приложений или [SQL-подход |SQL way] для прямой работы с запросами. <div class="grid gap-3"> <div> -[SQL-подход |SQL way] -===================== +[SQL-подход|sql-way] +==================== - Безопасные параметризованные запросы -- Точный контроль над формой SQL-запросов -- Когда вы пишете сложные запросы с расширенными функциями -- Оптимизация производительности с помощью специфических функций SQL +- Точный контроль над структурой SQL-запроса +- Когда нужно писать сложные запросы с продвинутыми функциями +- Оптимизация производительности через конкретные функции SQL </div> <div> -[Explorer |Explorer] +[Explorer |explorer] ==================== - Быстрая разработка без написания SQL -- Интуитивно понятная работа с отношениями между таблицами -- Вы оцените автоматическую оптимизацию запросов +- Интуитивная работа со связями между таблицами +- Выигрыш от автоматической оптимизации запросов - Подходит для быстрой и удобной работы с базой данных </div> @@ -35,7 +35,7 @@ Nette Database — это мощный и элегантный слой базы Установка ========= -Вы можете скачать и установить библиотеку с помощью инструмента [Composer|best-practices:composer]: +Скачайте и установите библиотеку с помощью [Composer|best-practices:composer]: ```shell composer require nette/database @@ -47,33 +47,33 @@ 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 | - +|* Сервер базы данных |* Имя 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). Давайте посмотрим, как оба подхода решают одни и те же задачи: +Nette Database даёт вам выбор: вы можете либо писать SQL-запросы напрямую (SQL-подход), либо позволить порождать их автоматически (Explorer). Посмотрим, как оба подхода справляются с одними и теми же задачами: -[SQL-подход|sql way] - SQL-запросы +[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 @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// вывод (не оптимально, генерирует N+1 запросов) +// Вывод (не оптимально, порождает N дополнительных запросов) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -90,7 +90,7 @@ foreach ($result as $author) { ORDER BY published_at DESC ', $author->id); - echo "Автор $author->name написал $author->books_count книг:\n"; + echo "Author $author->name has written $author->books_count books:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -98,26 +98,26 @@ foreach ($result as $author) { } ``` -[Подход Explorer|explorer] - автоматическая генерация SQL +[Подход 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 оптимизированных запроса) +// Вывод (автоматически порождает всего 2 оптимизированных запроса) foreach ($authors as $author) { $books = $author->related('books') ->order('published_at DESC'); - echo "Автор $author->name написал {$books->count()} книг:\n"; + echo "Author $author->name has written {$books->count()} books:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -125,23 +125,23 @@ foreach ($authors as $author) { } ``` -Подход Explorer автоматически генерирует и оптимизирует SQL-запросы. В приведенном примере SQL-подход генерирует N+1 запросов (один для авторов, а затем по одному для книг каждого автора), в то время как Explorer автоматически оптимизирует запросы и выполняет только два — один для авторов и один для всех их книг. +Подход Explorer порождает и оптимизирует SQL-запросы автоматически. В примере выше SQL-подход порождает N+1 запросов (один для авторов и затем по одному для книг каждого автора), тогда как Explorer автоматически оптимизирует запросы и выполняет только два: один для авторов и один для всех их книг. -Оба подхода можно свободно комбинировать в приложении по мере необходимости. +Оба подхода можно свободно сочетать в приложении по мере надобности. -Подключение и конфигурация -========================== +Соединение и настройка +====================== -Для подключения к базе данных достаточно создать экземпляр класса [api:Nette\Database\Connection]: +Чтобы подключиться к базе данных, достаточно создать экземпляр класса [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`. +Параметр `$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:] . +Однако более удобный способ предлагает [конфигурация приложения |configuration], где достаточно добавить секцию `database`. Так создаются нужные объекты, а также панель базы данных в панели [Tracy |tracy:]. ```neon database: @@ -150,7 +150,7 @@ database: password: password ``` -Затем объект соединения [можно получить как сервис из DI-контейнера |dependency-injection:passing-dependencies], например: +Затем объект соединения можно [получить как сервис из DI-контейнера |dependency-injection:passing-dependencies], например: ```php class Model @@ -163,22 +163,22 @@ class Model } ``` -Больше информации о [конфигурации базы данных|configuration]. +Подробнее о [конфигурации базы данных|configuration]. -Ручное создание Explorer ------------------------- +Создание 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, например: +// хранилище кеша, реализующее 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); ``` @@ -187,30 +187,32 @@ $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $ Управление соединением ====================== -При создании объекта `Connection` подключение происходит автоматически. Если вы хотите отложить подключение, используйте ленивый режим — его можно включить в [конфигурации|configuration], установив `lazy: true`, или следующим образом: +При создании объекта `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`. +Для управления соединением служат методы `connect()`, `disconnect()` и `reconnect()`. +- `connect()` создаёт соединение, если его ещё нет, и может выбросить `Nette\Database\ConnectionException`. +- `disconnect()` разрывает текущее соединение с базой данных. +- `reconnect()` выполняет разрыв и последующее повторное подключение к базе данных. Этот метод тоже может выбросить `Nette\Database\ConnectionException`. -Кроме того, вы можете отслеживать события, связанные с подключением, с помощью события `onConnect` — это массив обратных вызовов, которые вызываются после установления соединения с базой данных. +Кроме того, вы можете следить за событиями, связанными с соединением, через событие `onConnect`, которое представляет собой массив callback-функций, вызываемых после установки соединения с базой данных. ```php // выполняется после подключения к базе данных $database->onConnect[] = function($database) { - echo "Подключено к базе данных"; + echo "Connected to the database"; }; ``` +Похожим образом работает событие `onQuery`: это массив callback-функций, вызываемых после каждого выполненного запроса (и когда запрос завершается ошибкой), что удобно для логирования или профилирования. -Tracy Debug Bar -=============== -Если вы используете [Tracy |tracy:], панель Database в Debug Bar активируется автоматически. Она отображает все выполненные запросы, их параметры, время выполнения и место в коде, где они были вызваны. +Панель отладки Tracy +==================== + +Если вы используете [Tracy |tracy:], панель Database в Debug Bar включается автоматически. Она показывает все выполненные запросы, их параметры, время выполнения и место в коде, откуда они были вызваны. [* db-panel.webp *] diff --git a/database/ru/mapping.texy b/database/ru/mapping.texy deleted file mode 100644 index 5a8b3c76bf..0000000000 --- a/database/ru/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Преобразование типов -******************** - -.[perex] -Nette Database автоматически преобразует значения, возвращаемые из базы данных, в соответствующие типы PHP. - - -Дата и время ------------- - -Временные данные преобразуются в объекты `Nette\Utils\DateTime`. Если вы хотите, чтобы временные данные преобразовывались в неизменяемые объекты `Nette\Database\DateTime`, установите в [конфигурации|configuration] опцию `newDateTime` в `true`. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -В случае MySQL тип данных `TIME` преобразуется в объекты `DateInterval`. - - -Логические значения -------------------- - -Логические значения автоматически преобразуются в `true` или `false`. В MySQL преобразуется `TINYINT(1)`, если мы установим в [конфигурации|configuration] `convertBoolean: true`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Числовые значения ------------------ - -Числовые значения преобразуются в `int` или `float` в зависимости от типа столбца в базе данных: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Пользовательская нормализация ------------------------------ - -С помощью метода `setRowNormalizer(?callable $normalizer)` вы можете установить собственную функцию для преобразования строк из базы данных. Это полезно, например, для автоматического преобразования типов данных. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // здесь происходит преобразование типов - return $row; -}); -``` diff --git a/database/ru/reflection.texy b/database/ru/reflection.texy index 6a13f25280..efdc70b4e5 100644 --- a/database/ru/reflection.texy +++ b/database/ru/reflection.texy @@ -2,9 +2,9 @@ ******************* .{data-version:3.2.1} -Nette Database предоставляет инструменты для интроспекции структуры базы данных с помощью класса [api:Nette\Database\Reflection]. Он позволяет получать информацию о таблицах, столбцах, индексах и внешних ключах. Рефлексию можно использовать для генерации схем, создания гибких приложений, работающих с базой данных, или общих инструментов для работы с базами данных. +Nette Database предоставляет инструменты для исследования структуры базы данных через класс [api:Nette\Database\Reflection]. Он позволяет получать сведения о таблицах, столбцах, индексах и внешних ключах. Рефлексию можно использовать для порождения схем, создания гибких приложений, работающих с базой данных, или для общих инструментов работы с базами. -Объект рефлексии можно получить из экземпляра подключения к базе данных: +Объект рефлексии получают из экземпляра соединения с базой данных: ```php $reflection = $database->getReflection(); @@ -14,62 +14,64 @@ $reflection = $database->getReflection(); Получение таблиц ---------------- -Свойство только для чтения `$reflection->tables` содержит ассоциативный массив всех таблиц в базе данных: +Свойство только для чтения `$reflection->tables` содержит ассоциативный массив всех таблиц базы данных: ```php -// Вывод имен всех таблиц +// Вывод имён всех таблиц foreach ($reflection->tables as $name => $table) { echo $name . "\n"; } ``` -Доступны еще два метода: +Доступны ещё два метода: ```php // Проверка существования таблицы if ($reflection->hasTable('users')) { - echo "Таблица 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[]` – массив внешних ключей таблицы +- `$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] со следующими свойствами: +Свойство таблицы `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` – дополнительные метаданные, специфичные для данной системы баз данных +- `$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 "Столбец: $name\n"; - echo "Тип: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Да' : 'Нет') . "\n"; + echo "Column: $name\n"; + echo "Type: {$column->nativeType}\n"; + echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; } ``` @@ -77,28 +79,28 @@ foreach ($table->columns as $name => $column) { Индексы ------- -Свойство `indexes` таблицы предоставляет массив индексов, где каждый индекс является экземпляром [Index|api:Nette\Database\Reflection\Index] со следующими свойствами: +Свойство таблицы `indexes` предоставляет массив индексов, где каждый индекс - экземпляр [Index|api:Nette\Database\Reflection\Index] с такими свойствами: -- `$columns: Column[]` – массив столбцов, составляющих индекс -- `$unique: bool` – является ли индекс уникальным -- `$primary: bool` – является ли первичным ключом -- `$name: ?string` – имя индекса +- `$columns: Column[]` - массив столбцов, образующих индекс +- `$unique: bool` - является ли индекс уникальным +- `$primary: bool` - является ли он первичным ключом +- `$name: ?string` - имя индекса -Первичный ключ таблицы можно получить с помощью свойства `primaryKey`, которое возвращает либо объект `Index`, либо `null` в случае, если таблица не имеет первичного ключа. +Первичный ключ таблицы можно получить через свойство `primaryKey`, которое возвращает либо объект `Index`, либо `null`, если у таблицы нет первичного ключа. ```php // Вывод индексов foreach ($table->indexes as $index) { $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Индекс" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Столбцы: $columns\n"; - echo " Unique: " . ($index->unique ? 'Да' : 'Нет') . "\n"; + 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 "Первичный ключ: $columns\n"; + echo "Primary Key: $columns\n"; } ``` @@ -106,12 +108,12 @@ if ($primaryKey = $table->primaryKey) { Внешние ключи ------------- -Свойство `foreignKeys` таблицы предоставляет массив внешних ключей, где каждый внешний ключ является экземпляром [ForeignKey|api:Nette\Database\Reflection\ForeignKey] со следующими свойствами: +Свойство таблицы `foreignKeys` предоставляет массив внешних ключей, где каждый внешний ключ - экземпляр [ForeignKey|api:Nette\Database\Reflection\ForeignKey] с такими свойствами: -- `$foreignTable: Table` – таблица, на которую ссылается ключ -- `$localColumns: Column[]` – массив локальных столбцов -- `$foreignColumns: Column[]` – массив столбцов, на которые ссылается ключ -- `$name: ?string` – имя внешнего ключа +- `$foreignTable: Table` - таблица, на которую он ссылается +- `$localColumns: Column[]` - массив локальных столбцов +- `$foreignColumns: Column[]` - массив столбцов, на которые идёт ссылка +- `$name: string` - имя внешнего ключа ```php // Вывод внешних ключей diff --git a/database/ru/security.texy b/database/ru/security.texy index 3178bed6e0..226caa5aba 100644 --- a/database/ru/security.texy +++ b/database/ru/security.texy @@ -3,36 +3,36 @@ <div class=perex> -База данных часто содержит конфиденциальные данные и позволяет выполнять опасные операции. Для безопасной работы с Nette Database ключевыми являются: +Базы данных часто содержат конфиденциальные данные и позволяют выполнять опасные операции. Для безопасной работы с Nette Database принципиально важно: -- Понимание разницы между безопасным и небезопасным API -- Использование параметризованных запросов -- Правильная валидация входных данных +- Понимать разницу между безопасным и небезопасным API +- Использовать параметризованные запросы +- Правильно проверять входные данные </div> -Что такое SQL Injection? +Что такое SQL injection? ======================== -SQL Injection — это самый серьезный риск безопасности при работе с базой данных. Он возникает, когда необработанные входные данные от пользователя становятся частью SQL-запроса. Злоумышленник может внедрить собственные SQL-команды и тем самым: -- Получить несанкционированный доступ к данным -- Изменить или удалить данные в базе данных -- Обойти аутентификацию +SQL injection - самый серьёзный риск безопасности при работе с базами данных. Она возникает, когда неочищенный пользовательский ввод становится частью SQL-запроса. Злоумышленник может вставить собственные SQL-команды и тем самым: +- получить несанкционированный доступ к данным +- изменить или удалить данные в базе +- обойти аутентификацию ```php -// ❌ НЕБЕЗОПАСНЫЙ КОД - уязвимый для SQL-инъекций +// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection $database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); -// Злоумышленник может ввести, например, значение: ' OR '1'='1 -// Результирующий запрос будет: SELECT * FROM users WHERE name = '' OR '1'='1' -// Что вернет всех пользователей +// Злоумышленник может ввести значение вроде: ' OR '1'='1 +// Получившийся запрос будет: SELECT * FROM users WHERE name = '' OR '1'='1' +// Он вернёт всех пользователей ``` -То же самое относится и к Database Explorer: +То же относится и к Database Explorer: ```php -// ❌ НЕБЕЗОПАСНЫЙ КОД - уязвимый для SQL-инъекций +// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection $table->where('name = ' . $_GET['name']); $table->where("name = '$_GET[name]'"); ``` @@ -41,9 +41,9 @@ $table->where("name = '$_GET[name]'"); Параметризованные запросы ========================= -Основной защитой от SQL Injection являются параметризованные запросы. Nette Database предлагает несколько способов их использования. +Основная защита от SQL injection - параметризованные запросы. Nette Database предлагает несколько способов их использовать. -Самый простой способ — использовать **заполнители в виде вопросительных знаков**: +Проще всего использовать **подстановки в виде вопросительных знаков**: ```php // ✅ Безопасный параметризованный запрос @@ -53,18 +53,18 @@ $database->query('SELECT * FROM users WHERE name = ?', $name); $table->where('name = ?', $name); ``` -Это относится ко всем другим методам в [Database Explorer|explorer], которые позволяют вставлять выражения с заполнителями в виде вопросительных знаков и параметрами. +Это относится ко всем остальным методам [Database Explorer|explorer], позволяющим вставлять выражения с вопросительными знаками и параметрами. -Для команд INSERT, UPDATE или условия WHERE мы можем передавать значения в массиве: +Для команд INSERT, UPDATE или для условия WHERE мы можем передать значения массивом: ```php -// ✅ Безопасная вставка INSERT +// ✅ Безопасный INSERT $database->query('INSERT INTO users', [ 'name' => $name, 'email' => $email, ]); -// ✅ Безопасная вставка INSERT в Explorer +// ✅ Безопасный INSERT в Explorer $table->insert([ 'name' => $name, 'email' => $email, @@ -72,92 +72,92 @@ $table->insert([ ``` -Валидация значений параметров -============================= +Проверка значений параметров +============================ -Параметризованные запросы являются основой безопасной работы с базой данных. Однако значения, которые мы в них вставляем, должны проходить несколько уровней проверок: +Параметризованные запросы - краеугольный камень безопасной работы с базой данных. Однако значения, которые мы в них вставляем, должны пройти несколько уровней проверок: -Проверка типов --------------- +Проверка типа +------------- -**Самое важное — обеспечить правильный тип данных параметров** — это необходимое условие для безопасного использования Nette Database. База данных предполагает, что все входные данные имеют правильный тип данных, соответствующий данному столбцу. +**Самое важное - обеспечить правильный тип данных параметров**: это необходимое условие безопасного использования Nette Database. База данных исходит из того, что все входные данные имеют правильный тип, соответствующий данному столбцу. -Например, если бы `$name` в предыдущих примерах неожиданно оказался массивом вместо строки, Nette Database попыталась бы вставить все его элементы в SQL-запрос, что привело бы к ошибке. Поэтому **никогда не используйте** невалидированные данные из `$_GET`, `$_POST` или `$_COOKIE` непосредственно в запросах к базе данных. +Например, если бы `$name` в предыдущих примерах неожиданно оказалась массивом вместо строки, Nette Database попытался бы вставить все его элементы в SQL-запрос, что привело бы к ошибке. Поэтому **никогда не используйте** непроверенные данные из `$_GET`, `$_POST` или `$_COOKIE` напрямую в запросах к базе. Проверка формата ---------------- -На втором уровне мы проверяем формат данных — например, находятся ли строки в кодировке UTF-8 и соответствует ли их длина определению столбца, или находятся ли числовые значения в допустимом диапазоне для данного типа данных столбца. +На втором уровне мы проверяем формат данных: например, что строки в кодировке UTF-8 и их длина соответствует описанию столбца или что числовые значения находятся в допустимом диапазоне для типа данных столбца. -На этом уровне валидации мы можем частично положиться и на саму базу данных — многие базы данных отклонят невалидные данные. Однако поведение может отличаться, некоторые могут молча обрезать длинные строки или усекать числа вне диапазона. +На этом уровне проверки мы можем частично положиться на саму базу данных: многие базы отклонят некорректные данные. Однако поведение может различаться: некоторые молча обрежут длинные строки или числа вне диапазона. -Проверка домена ---------------- +Проверка, свойственная предметной области +----------------------------------------- -Третий уровень представляют логические проверки, специфичные для вашего приложения. Например, проверка того, соответствуют ли значения из выпадающих списков предлагаемым вариантам, находятся ли числа в ожидаемом диапазоне (например, возраст 0–150 лет) или имеют ли смысл взаимные зависимости между значениями. +Третий уровень - логические проверки, специфичные для вашего приложения. Например, проверка, что значения из выпадающих списков соответствуют предложенным вариантам, что числа находятся в ожидаемом диапазоне (например, возраст 0-150 лет) или что взаимные зависимости между значениями осмысленны. -Рекомендуемые способы валидации -------------------------------- +Рекомендуемые способы проверки +------------------------------ -- Используйте [Nette Forms|forms:], которые автоматически обеспечивают правильную валидацию всех входных данных -- Используйте [Presenters|application:] и указывайте типы данных для параметров в методах `action*()` и `render*()` -- Или реализуйте собственный слой валидации с помощью стандартных инструментов PHP, таких как `filter_var()` +- Используйте [Nette Forms|forms:], которые автоматически обеспечивают правильную проверку всех вводимых данных. +- Используйте [презентеры|application:] и указывайте типы данных параметров в методах `action*()` и `render*()`. +- Или реализуйте собственный слой проверки стандартными средствами PHP вроде `filter_var()`. Безопасная работа со столбцами ============================== -В предыдущем разделе мы показали, как правильно валидировать значения параметров. Однако при использовании массивов в SQL-запросах необходимо уделять такое же внимание и их ключам. +В предыдущем разделе мы показали, как правильно проверять значения параметров. Однако, используя массивы в SQL-запросах, мы должны с тем же вниманием отнестись и к их ключам. ```php -// ❌ НЕБЕЗОПАСНЫЙ КОД - ключи в массиве не обработаны +// ❌ ОПАСНЫЙ КОД - ключи в массиве не очищены $database->query('INSERT INTO users', $_POST); ``` -В командах INSERT и UPDATE это серьезная ошибка безопасности — злоумышленник может вставить или изменить любой столбец в базе данных. Он мог бы, например, установить `is_admin = 1` или вставить произвольные данные в конфиденциальные столбцы (так называемая Уязвимость массового присваивания). +Для команд INSERT и UPDATE это критическая брешь в безопасности: злоумышленник может вставить или изменить любой столбец базы данных. Он мог бы, например, задать `is_admin = 1` или вставить произвольные данные в конфиденциальные столбцы (так называемая Mass Assignment Vulnerability). -В условиях WHERE это еще опаснее, так как они могут содержать операторы: +В условиях WHERE это ещё опаснее, потому что они могут содержать операторы: ```php -// ❌ НЕБЕЗОПАСНЫЙ КОД - ключи в массиве не обработаны +// ❌ ОПАСНЫЙ КОД - ключи в массиве не очищены $_POST['salary >'] = 100000; $database->query('SELECT * FROM users WHERE', $_POST); // выполняет запрос WHERE (`salary` > 100000) ``` -Злоумышленник может использовать этот подход для систематического выяснения зарплат сотрудников. Например, он начнет с запроса зарплат выше 100 000, затем ниже 50 000 и, постепенно сужая диапазон, сможет раскрыть приблизительные зарплаты всех сотрудников. Этот тип атаки называется Перечисление SQL. +Злоумышленник может таким способом планомерно выяснить зарплаты сотрудников. Он может начать, например, с запроса о зарплатах выше 100 000, затем ниже 50 000 и, постепенно сужая диапазон, раскрыть примерные зарплаты всех сотрудников. Такой вид атаки называется SQL enumeration. -Методы `where()` и `whereOr()` [гораздо более гибки |explorer#where] и поддерживают в ключах и значениях SQL-выражения, включая операторы и функции. Это дает злоумышленнику возможность выполнить SQL-инъекцию: +Методы `where()` и `whereOr()` [ещё гораздо гибче |explorer#where()] и поддерживают SQL-выражения, включая операторы и функции, в ключах и значениях. Это даёт злоумышленнику возможность выполнить SQL injection: ```php -// ❌ НЕБЕЗОПАСНЫЙ КОД - злоумышленник может внедрить собственный SQL +// ❌ ОПАСНЫЙ КОД - злоумышленник может вставить собственный 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)`. +Эта атака завершает исходное условие через `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); @@ -167,7 +167,7 @@ $table->where($filteredData); Динамические идентификаторы =========================== -Для динамических имен таблиц и столбцов используйте заполнитель `?name`. Он обеспечит правильное экранирование идентификаторов в соответствии с синтаксисом данной базы данных (например, с помощью обратных кавычек в MySQL): +Для динамических имён таблиц и столбцов используйте подстановку `?name`. Она обеспечивает правильное экранирование идентификаторов по синтаксису данной базы данных (например, обратными кавычками в MySQL): ```php // ✅ Безопасное использование доверенных идентификаторов @@ -177,9 +177,9 @@ $database->query('SELECT ?name FROM ?name', $column, $table); // Результат в MySQL: SELECT `name` FROM `users` ``` -Важно: символ `?name` используйте только для доверенных значений, определенных в коде приложения. Для значений от пользователя снова используйте [белый список |#Белый список столбцов]. В противном случае вы подвергаетесь рискам безопасности: +Важно: используйте обозначение `?name` только для доверенных значений, заданных в коде приложения. Для значений от пользователя снова используйте [белый список |#Белый список столбцов]. Иначе вы подвергаете себя рискам безопасности: ```php -// ❌ НЕБЕЗОПАСНО - никогда не используйте ввод пользователя +// ❌ ОПАСНО - никогда не используйте пользовательский ввод $database->query('SELECT ?name FROM users', $_GET['column']); ``` diff --git a/database/ru/sql-way.texy b/database/ru/sql-way.texy index 4da96b74e6..8e26e2e91e 100644 --- a/database/ru/sql-way.texy +++ b/database/ru/sql-way.texy @@ -2,16 +2,16 @@ SQL-подход ********** .[perex] -Nette Database предлагает два пути: вы можете писать SQL-запросы сами (SQL-подход) или позволить генерировать их автоматически (см. [Explorer |explorer]). SQL-подход дает вам полный контроль над запросами и при этом обеспечивает их безопасное построение. +Nette Database предлагает два способа работы: вы можете писать SQL-запросы сами (SQL-подход) или позволить порождать их автоматически (см. [Explorer |explorer]). SQL-подход даёт вам полный контроль над запросами и при этом обеспечивает их безопасное построение. .[note] -Подробности о подключении и конфигурации базы данных можно найти в главе [Подключение и конфигурация |guide#Подключение и конфигурация]. +Подробности о соединении с базой данных и его настройке можно найти в главе [Соединение и настройка |guide#Соединение и настройка]. -Базовые запросы +Основы запросов =============== -Для выполнения запросов к базе данных используется метод `query()`. Он возвращает объект [ResultSet |api:Nette\Database\ResultSet], который представляет результат запроса. В случае сбоя метод [выбрасывает исключение |exceptions]. Результат запроса можно перебирать с помощью цикла `foreach` или использовать одну из [вспомогательных функций |#Получение данных]. +Для запросов к базе данных служит метод `query()`. Он возвращает объект [ResultSet |api:Nette\Database\ResultSet], представляющий результат запроса. Если запрос не удаётся, метод [выбрасывает исключение|exceptions]. Результат запроса можно обойти циклом `foreach` или воспользоваться одним из [вспомогательных методов |#Получение данных]. ```php $result = $database->query('SELECT * FROM users'); @@ -22,35 +22,35 @@ foreach ($result as $row) { } ``` -Для безопасной вставки значений в SQL-запросы мы используем параметризованные запросы. Nette Database делает их максимально простыми — достаточно добавить запятую и значение после SQL-запроса: +Чтобы безопасно вставлять значения в SQL-запросы, используйте параметризованные запросы. Nette Database делает это исключительно просто: достаточно добавить после SQL-запроса запятую и значение: ```php $database->query('SELECT * FROM users WHERE name = ?', $name); ``` -При наличии нескольких параметров у вас есть два варианта записи. Вы можете либо «перемежать» SQL-запрос параметрами: +При нескольких параметрах у вас два варианта. Вы можете чередовать SQL-запрос и параметры: ```php $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); ``` -Либо сначала написать весь SQL-запрос, а затем добавить все параметры: +Или сначала написать весь SQL-запрос, а затем добавить все параметры: ```php $database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); ``` -Защита от SQL Injection +Защита от SQL injection ======================= -Почему важно использовать параметризованные запросы? Потому что они защищают вас от атаки под названием SQL Injection, при которой злоумышленник мог бы подставить собственные SQL-команды и тем самым получить или повредить данные в базе данных. +Почему важно использовать параметризованные запросы? Потому что они защищают вас от атаки под названием SQL injection, при которой злоумышленник может подсунуть собственные SQL-команды и тем самым получить доступ к данным в базе или повредить их. .[warning] -**Никогда не вставляйте переменные непосредственно в SQL-запрос!** Всегда используйте параметризованные запросы, которые защитят вас от SQL Injection. +**Никогда не вставляйте переменные прямо в SQL-запрос!** Всегда используйте параметризованные запросы, которые защищают вас от SQL injection. ```php -// ❌ ОПАСНЫЙ КОД - уязвимый для SQL-инъекций +// ❌ ОПАСНЫЙ КОД - уязвим для SQL injection $database->query("SELECT * FROM users WHERE name = '$name'"); // ✅ Безопасный параметризованный запрос @@ -60,14 +60,14 @@ $database->query('SELECT * FROM users WHERE name = ?', $name); Ознакомьтесь с [возможными рисками безопасности |security]. -Техники запросов -================ +Приёмы построения запросов +========================== Условия WHERE ------------- -Условия WHERE можно записать как ассоциативный массив, где ключи — это имена столбцов, а значения — данные для сравнения. Nette Database автоматически выберет наиболее подходящий SQL-оператор в зависимости от типа значения. +Условия `WHERE` можно записать ассоциативным массивом, где ключи - имена столбцов, а значения - данные для сравнения. Nette Database автоматически выбирает наиболее подходящий оператор SQL по типу значения. ```php $database->query('SELECT * FROM users WHERE', [ @@ -77,7 +77,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -В ключе также можно явно указать оператор для сравнения: +Оператор сравнения можно указать в ключе и явно: ```php $database->query('SELECT * FROM users WHERE', [ @@ -88,7 +88,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' ``` -Nette автоматически обрабатывает особые случаи, такие как значения `null` или массивы. +Nette автоматически обрабатывает особые случаи вроде значений `null` или массивов. ```php $database->query('SELECT * FROM products WHERE', [ @@ -103,21 +103,21 @@ $database->query('SELECT * FROM products WHERE', [ ```php $database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // использует оператор <> + 'name NOT' => 'Laptop', // использует оператор != 'category_id NOT' => [1, 2, 3], // использует NOT IN 'description NOT' => null, // использует IS NOT NULL - 'id' => [], // пропускается + 'id NOT' => [], // пропускается ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -Для объединения условий используется оператор `AND`. Это можно изменить с помощью [плейсхолдера ?or |#Подсказки для построения SQL]. +По умолчанию условия объединяются оператором `AND`. Это можно изменить [подсказкой ?or |#Подсказки построения SQL]. Правила ORDER BY ---------------- -Сортировку `ORDER BY` можно записать с помощью массива. В ключах указываем столбцы, а значением будет boolean, определяющий, сортировать ли по возрастанию: +Конструкцию `ORDER BY` можно записать массивом. Столбцы указывайте в ключах, а логическим значением обозначайте порядок: по возрастанию (`true`) или по убыванию (`false`): ```php $database->query('SELECT id FROM author ORDER BY', [ @@ -131,7 +131,7 @@ $database->query('SELECT id FROM author ORDER BY', [ Вставка данных (INSERT) ----------------------- -Для вставки записей используется SQL-команда `INSERT`. +Для вставки записей служит команда SQL `INSERT`. ```php $values = [ @@ -142,11 +142,11 @@ $database->query('INSERT INTO users ?', $values); $userId = $database->getInsertId(); ``` -Метод `getInsertId()` возвращает ID последней вставленной строки. В некоторых базах данных (например, PostgreSQL) необходимо в качестве параметра указать имя последовательности, из которой должен генерироваться ID, с помощью `$database->getInsertId($sequenceId)`. +Метод `getInsertId()` возвращает ID последней вставленной записи. Для некоторых баз данных (например, PostgreSQL) нужно указать параметром имя последовательности, из которой должен порождаться ID: `$database->getInsertId($sequenceId)`. -В качестве параметров можно передавать и [#специальные значения] такие как файлы, объекты DateTime или перечисляемые типы. +Параметрами можно передавать и [особые значения |#Особые значения], такие как файлы, объекты DateTime или типы enum. -Вставка нескольких записей одновременно: +Вставка нескольких записей сразу: ```php $database->query('INSERT INTO users ?', [ @@ -155,15 +155,15 @@ $database->query('INSERT INTO users ?', [ ]); ``` -Множественная вставка INSERT намного быстрее, так как выполняется один запрос к базе данных вместо множества отдельных. +Многозаписный INSERT намного быстрее, потому что выполняется всего один запрос к базе вместо множества отдельных. -**Предупреждение о безопасности:** Никогда не используйте в качестве `$values` невалидированные данные. Ознакомьтесь с [возможными рисками |security#Безопасная работа со столбцами]. +**Замечание о безопасности:** никогда не используйте непроверенные данные в качестве `$values`. Ознакомьтесь с [возможными рисками |security#Безопасная работа со столбцами]. Обновление данных (UPDATE) -------------------------- -Для обновления записей используется SQL-команда `UPDATE`. +Для обновления записей служит команда SQL `UPDATE`. ```php // Обновление одной записи @@ -173,17 +173,17 @@ $values = [ $result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); ``` -Количество затронутых строк возвращает `$result->getRowCount()`. +Число затронутых записей возвращает `$result->getRowCount()`. -Для UPDATE можно использовать операторы `+=` и `-=`: +Для `UPDATE` мы можем использовать операторы `+=` и `-=`: ```php $database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // инкремент login_count + 'login_count+=' => 1, // увеличивает login_count ], 1); ``` -Пример вставки или обновления записи, если она уже существует. Используем технику `ON DUPLICATE KEY UPDATE`: +Пример вставки или обновления записи, если она уже существует. Мы используем приём `ON DUPLICATE KEY UPDATE`: ```php $values = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Обратите внимание, что Nette Database распознает, в каком контексте SQL-команды вставляется параметр с массивом, и в соответствии с этим составляет из него SQL-код. Так, из первого массива он составил `(id, name, year) VALUES (123, 'Jim', 1978)`, в то время как второй преобразовал в вид `name = 'Jim', year = 1978`. Подробнее об этом мы поговорим в разделе [#Подсказки для построения SQL]. +Обратите внимание, что Nette Database распознаёт контекст, в котором в SQL-команде используется параметр-массив, и строит SQL-код соответственно. Так, из первого массива он построил `(id, name, year) VALUES (123, 'Jim', 1978)`, а второй преобразовал в вид `name = 'Jim', year = 1978`. Подробнее об этом в разделе [#Подсказки построения SQL]. Удаление данных (DELETE) ------------------------ -Для удаления записей используется SQL-команда `DELETE`. Пример с получением количества удаленных строк: +Для удаления записей служит команда SQL `DELETE`. Пример получения числа удалённых записей: ```php $count = $database->query('DELETE FROM users WHERE id = ?', 1) @@ -212,21 +212,21 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -Подсказки для построения SQL ----------------------------- +Подсказки построения SQL +------------------------ -Подсказка — это специальный плейсхолдер в SQL-запросе, который указывает, как значение параметра должно быть преобразовано в 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` | Служит для вставки имён таблиц или столбцов | - +| `?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). +Подстановка `?name` служит для динамической вставки имён таблиц и столбцов в запрос. Nette Database заботится о правильном экранировании идентификаторов по соглашениям базы данных (например, о заключении в обратные кавычки в MySQL). ```php $table = 'users'; @@ -235,9 +235,9 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); // SELECT `name` FROM `users` WHERE id = 1 (в MySQL) ``` -**Предупреждение:** символ `?name` используйте только для имен таблиц и столбцов из валидированных входных данных, иначе вы подвергаетесь [риску безопасности |security#Динамические идентификаторы]. +**Внимание:** используйте подстановку `?name` только для проверенных имён таблиц и столбцов. Иначе вы рискуете [уязвимостями безопасности |security#Динамические идентификаторы]. -Остальные подсказки обычно указывать не нужно, так как Nette использует умное автоопределение при составлении SQL-запроса (см. третий столбец таблицы). Но вы можете использовать его, например, в ситуации, когда хотите объединить условия с помощью `OR` вместо `AND`: +Остальные подсказки обычно указывать не нужно, потому что при построении SQL-запроса Nette использует умное автоопределение (см. третий столбец таблицы). Но вы можете применить их, например, в ситуации, когда хотите объединить условия через `OR` вместо `AND`: ```php $database->query('SELECT * FROM users WHERE ?or', [ @@ -248,32 +248,32 @@ $database->query('SELECT * FROM users WHERE ?or', [ ``` -Специальные значения --------------------- +Особые значения +--------------- -Кроме обычных скалярных типов (string, int, bool), в качестве параметров можно передавать и специальные значения: +Помимо обычных скалярных типов (string, int, bool) параметрами можно передавать и особые значения: -- файлы: `fopen('image.gif', 'r')` вставляет бинарное содержимое файла -- дата и время: объекты `DateTime` преобразуются в формат базы данных -- перечисляемые типы: экземпляры `enum` преобразуются в их значение -- SQL-литералы: созданные с помощью `Connection::literal('NOW()')` вставляются непосредственно в запрос +- файлы: `fopen('image.gif', 'r')` вставляет двоичное содержимое файла +- дату и время: объекты `DateTimeInterface` преобразуются в формат базы данных +- типы enum: экземпляры `enum` преобразуются в своё значение +- литералы SQL: созданные через `Connection::literal('NOW()')` вставляются в запрос напрямую ```php $database->query('INSERT INTO articles ?', [ 'title' => 'My Article', - 'published_at' => new DateTime, + 'published_at' => new DateTimeImmutable, // или new DateTime 'content' => fopen('image.png', 'r'), 'state' => Status::Draft, ]); ``` -В базах данных, которые не имеют нативной поддержки типа данных `datetime` (например, SQLite и Oracle), `DateTime` преобразуется в значение, указанное в [конфигурации базы данных |configuration] в элементе `formatDateTime` (значение по умолчанию — `U` - unix timestamp). +Для баз данных без нативной поддержки типа `datetime` (таких как SQLite и Oracle) объекты `DateTime` и `DateTimeImmutable` преобразуются в значение, заданное в [конфигурации базы данных|configuration] параметром `formatDateTime` (значение по умолчанию - `U`, то есть Unix-время). -SQL-литералы +Литералы SQL ------------ -В некоторых случаях необходимо указать в качестве значения непосредственно SQL-код, который, однако, не должен восприниматься как строка и экранироваться. Для этого служат объекты класса `Nette\Database\SqlLiteral`. Их создает метод `Connection::literal()`. +В некоторых случаях вам нужно передать значением сырой SQL-код, который не должен восприниматься как строка и экранироваться. Для этого служат объекты класса `Nette\Database\SqlLiteral`. Их создаёт метод `Connection::literal()`. ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -Или альтернативно: +Как вариант: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -293,7 +293,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) ``` -SQL-литералы могут содержать параметры: +Литералы SQL могут содержать параметры: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Благодаря чему мы можем создавать интересные комбинации: +Это позволяет строить интересные сочетания: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -324,13 +324,13 @@ $result = $database->query('SELECT * FROM users WHERE', [ Сокращения для запросов SELECT ------------------------------ -Для упрощения извлечения данных `Connection` предлагает несколько сокращений, которые объединяют вызов `query()` с последующим `fetch*()`. Эти методы принимают те же параметры, что и `query()`, то есть SQL-запрос и необязательные параметры. Полное описание методов `fetch*()` вы найдете [ниже |#fetch]. +Чтобы упростить получение данных, `Connection` предлагает несколько сокращений, объединяющих вызов `query()` с последующим вызовом `fetch*()`. Эти методы принимают те же параметры, что и `query()`, то есть SQL-запрос и необязательные параметры. Полное описание методов `fetch*()` можно найти [ниже |#fetch()]. -| `fetch($sql, ...$params): ?Row` | Выполняет запрос и возвращает первую строку как объект `Row` -| `fetchAll($sql, ...$params): array` | Выполняет запрос и возвращает все строки как массив объектов `Row` -| `fetchPairs($sql, ...$params): array` | Выполняет запрос и возвращает ассоциативный массив, где первый столбец представляет ключ, а второй — значение -| `fetchField($sql, ...$params): mixed` | Выполняет запрос и возвращает значение первого поля из первой строки -| `fetchList($sql, ...$params): ?array` | Выполняет запрос и возвращает первую строку как индексированный массив +| `fetch($sql, ...$params): ?Row` | Выполняет запрос и возвращает первую запись как объект `Row` или `null`. +| `fetchAll($sql, ...$params): array` | Выполняет запрос и возвращает все записи как массив объектов `Row`. +| `fetchPairs($sql, ...$params): array` | Выполняет запрос и возвращает ассоциативный массив (пары ключ => значение). +| `fetchField($sql, ...$params): mixed` | Выполняет запрос и возвращает значение первого столбца первой записи. +| `fetchList($sql, ...$params): ?array` | Выполняет запрос и возвращает первую запись как индексированный массив или `null`. Пример: @@ -341,10 +341,10 @@ $count = $database->query('SELECT COUNT(*) FROM articles') ``` -`foreach` - итерация по строкам -------------------------------- +`foreach` - обход записей +------------------------- -После выполнения запроса возвращается объект [ResultSet |api:Nette\Database\ResultSet], который позволяет перебирать результаты несколькими способами. Самый простой способ выполнить запрос и получить строки — это итерация в цикле `foreach`. Этот способ наиболее экономичен по памяти, так как возвращает данные постепенно и не сохраняет их все сразу в памяти. +После выполнения запроса возвращается объект [ResultSet|api:Nette\Database\ResultSet], позволяющий обходить результаты несколькими способами. Проще всего выполнить запрос и получить записи обходом в цикле `foreach`. Этот способ наиболее экономен по памяти, потому что получает данные запись за записью и не загружает весь результат в память сразу. ```php $result = $database->query('SELECT * FROM users'); @@ -357,17 +357,17 @@ foreach ($result as $row) { ``` .[note] -`ResultSet` можно итерировать только один раз. Если вам нужно итерировать повторно, сначала необходимо загрузить данные в массив, например, с помощью метода `fetchAll()`. +`ResultSet` можно обойти только один раз. Если вам нужен повторный обход, сначала загрузите данные в массив, например методом `fetchAll()`. fetch(): ?Row .[method] ----------------------- -Возвращает строку как объект `Row`. Если больше нет строк, возвращает `null`. Перемещает внутренний указатель на следующую строку. +Возвращает запись как объект `Row`. Если записей больше нет, возвращает `null`. Сдвигает внутренний указатель на следующую запись. ```php $result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // считывает первую строку +$row = $result->fetch(); // загружает первую запись if ($row) { echo $row->name; } @@ -377,11 +377,11 @@ if ($row) { fetchAll(): array .[method] --------------------------- -Возвращает все оставшиеся строки из `ResultSet` как массив объектов `Row`. +Возвращает все оставшиеся записи из `ResultSet` как массив объектов `Row`. ```php $result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // считывает все строки +$rows = $result->fetchAll(); // загружает все записи foreach ($rows as $row) { echo $row->name; } @@ -391,7 +391,7 @@ foreach ($rows as $row) { fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] --------------------------------------------------------------------------------------- -Возвращает результаты как ассоциативный массив. Первый аргумент указывает имя столбца, который будет использоваться в качестве ключа в массиве, второй аргумент указывает имя столбца, который будет использоваться в качестве значения: +Возвращает результат как ассоциативный массив. Первый аргумент задаёт столбец, который используется как ключи, а второй - столбец, используемый как значения: ```php $result = $database->query('SELECT id, name FROM users'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Если указать только первый параметр, значением будет вся строка, то есть объект `Row`: +Если задан только первый параметр (`$key`), значением будет вся запись (объект `Row`): ```php $rows = $result->fetchPairs('id'); // [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] ``` -В случае дублирующихся ключей используется значение из последней строки. При использовании `null` в качестве ключа массив будет индексирован численно с нуля (тогда коллизий не происходит): +При повторяющихся ключах используется значение из последней записи. Использование `null` в качестве ключа даёт массив с числовой индексацией (начиная с нуля), что предотвращает столкновения ключей: ```php $names = $result->fetchPairs(null, 'name'); @@ -417,14 +417,14 @@ $names = $result->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Альтернативно, в качестве параметра можно указать callback, который будет для каждой строки возвращать либо само значение, либо пару ключ-значение. +Как вариант, вы можете передать callback, обрабатывающий каждую запись. Callback может вернуть одно значение или пару ключ-значение. ```php $result = $database->query('SELECT * FROM users'); $items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); // ['1 - John', '2 - Jane', ...] -// Callback также может возвращать массив с парой ключ & значение: +// Callback может вернуть и массив с парой ключ и значение: $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); // ['John' => 46, 'Jane' => 21, ...] ``` @@ -433,18 +433,18 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -Возвращает значение первого поля из текущей строки. Если больше нет строк, возвращает `null`. Перемещает внутренний указатель на следующую строку. +Возвращает значение первого столбца текущей записи. Если записей больше нет, возвращает `null`. Сдвигает внутренний указатель на следующую запись. ```php $result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // считывает имя из первой строки +$name = $result->fetchField(); // загружает name из первой записи ``` fetchList(): ?array .[method] ----------------------------- -Возвращает строку как индексированный массив. Если больше нет строк, возвращает `null`. Перемещает внутренний указатель на следующую строку. +Возвращает запись как индексированный массив. Если записей больше нет, возвращает `null`. Сдвигает внутренний указатель на следующую запись. ```php $result = $database->query('SELECT name, email FROM users'); @@ -455,19 +455,19 @@ $row = $result->fetchList(); // ['John', 'john@example.com'] getRowCount(): ?int .[method] ----------------------------- -Возвращает количество затронутых строк последним запросом `UPDATE` или `DELETE`. Для `SELECT` это количество возвращенных строк, но оно может быть неизвестно — в таком случае метод вернет `null`. +Возвращает число затронутых записей от последнего запроса `UPDATE` или `DELETE`. Для запросов `SELECT` возвращает число записей в результате. Однако оно не всегда бывает известно, и в этом случае метод возвращает `null`. getColumnCount(): ?int .[method] -------------------------------- -Возвращает количество столбцов в `ResultSet`. +Возвращает число столбцов в `ResultSet`. -Информация о запросах -===================== +Сведения о запросе +================== -Для отладочных целей можно получить информацию о последнем выполненном запросе: +Ради отладки мы можем получить сведения о последнем выполненном запросе: ```php echo $database->getLastQueryString(); // выводит SQL-запрос @@ -477,21 +477,21 @@ echo $result->getQueryString(); // выводит SQL-запрос echo $result->getTime(); // выводит время выполнения в секундах ``` -Для отображения результата в виде HTML-таблицы можно использовать: +Чтобы вывести результат в виде HTML-таблицы, можно использовать: ```php $result = $database->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSet предлагает информацию о типах столбцов: +`ResultSet` предоставляет сведения о типах столбцов: ```php $result = $database->query('SELECT * FROM articles'); $types = $result->getColumnTypes(); foreach ($types as $column => $type) { - echo "$column имеет тип $type->type"; // например, 'id имеет тип int' + echo "$column is of type $type"; // например, 'id is of type int' } ``` @@ -499,15 +499,15 @@ foreach ($types as $column => $type) { Логирование запросов -------------------- -Мы можем реализовать собственное логирование запросов. Событие `onQuery` — это массив callback-функций, которые вызываются после каждого выполненного запроса: +Мы можем реализовать собственное логирование запросов. Событие `onQuery` - массив callback-функций, вызываемых после каждого выполненного запроса: ```php $database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Запрос: ' . $result->getQueryString()); - $logger->info('Время: ' . $result->getTime()); + $logger->info('Query: ' . $result->getQueryString()); + $logger->info('Time: ' . $result->getTime()); if ($result->getRowCount() > 1000) { - $logger->warning('Большой набор результатов: ' . $result->getRowCount() . ' строк'); + $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); } }; ``` diff --git a/database/ru/transactions.texy b/database/ru/transactions.texy index 67eacd4dff..7792cb35ac 100644 --- a/database/ru/transactions.texy +++ b/database/ru/transactions.texy @@ -2,9 +2,9 @@ ********** .[perex] -Транзакции гарантируют, что либо все операции в рамках транзакции будут выполнены, либо ни одна из них не будет выполнена. Они полезны для обеспечения согласованности данных при более сложных операциях. +Транзакции гарантируют, что либо будут выполнены все операции внутри транзакции, либо ни одна. Они полезны для обеспечения согласованности данных при сложных операциях. -Самый простой способ использования транзакций выглядит следующим образом: +Простейший способ использовать транзакции выглядит так: ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -Гораздо элегантнее то же самое можно записать с помощью метода `transaction()`. В качестве параметра он принимает колбэк, который выполняется в транзакции. Если колбэк выполняется без исключения, транзакция автоматически подтверждается. Если возникает исключение, транзакция отменяется (rollback), а исключение распространяется дальше. +Того же результата можно добиться куда изящнее методом `transaction()`. Он принимает callback, который выполняется внутри транзакции. Если callback отрабатывает без исключения, транзакция автоматически фиксируется. Если возникает исключение, транзакция откатывается, а исключение распространяется дальше. ```php $database->transaction(function ($database) use ($id) { @@ -33,11 +33,13 @@ $database->transaction(function ($database) use ($id) { }); ``` -Метод `transaction()` также может возвращать значения: +Вызовы `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(); // возвращает количество обновленных строк + return $result->getRowCount(); // возвращает число обновлённых записей }); ``` diff --git a/database/ru/type-conversion.texy b/database/ru/type-conversion.texy new file mode 100644 index 0000000000..0865e740ac --- /dev/null +++ b/database/ru/type-conversion.texy @@ -0,0 +1,55 @@ +Преобразование типов +******************** + +.[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 new file mode 100644 index 0000000000..4b13b13831 --- /dev/null +++ b/database/ru/upgrading.texy @@ -0,0 +1,36 @@ +Обновление +********** + + +Обновление до версии 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/sl/@home.texy b/database/sl/@home.texy deleted file mode 100644 index 5c0ffe069e..0000000000 --- a/database/sl/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Podprte podatkovne baze -======================= - -Nette podpira naslednje podatkovne baze: - -|* Strežnik podatkovne baze |* Ime DSN |* Podpora v Core |* Podpora v Explorer -| MySQL (>= 5.1) | mysql | DA | DA -| PostgreSQL (>= 9.0) | pgsql | DA | DA -| Sqlite 3 (>= 3.8) | sqlite | DA | DA -| Oracle | oci | DA | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA | DA -| MS SQL (PDO_DBLIB) | mssql | DA | - -| ODBC | odbc | DA | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database bistveno poenostavlja pridobivanje podatkov iz podatkovne baze brez potrebe po pisanju SQL poizvedb. Postavlja učinkovite poizvedbe in ne prenaša nepotrebnih podatkov.}} diff --git a/database/sl/@left-menu.texy b/database/sl/@left-menu.texy deleted file mode 100644 index 280bbea034..0000000000 --- a/database/sl/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Uvod |guide] -- [SQL pristop |sql way] -- [Explorer |Explorer] -- [Transakcije |transactions] -- [Izjeme |exceptions] -- [Refleksija |reflection] -- [Preslikava |mapping] -- [Konfiguracija |configuration] -- [Varnostna tveganja |security] -- [Nadgradnja |en:upgrading] diff --git a/database/sl/@meta.texy b/database/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/database/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/database/sl/configuration.texy b/database/sl/configuration.texy deleted file mode 100644 index 1fd404c6d1..0000000000 --- a/database/sl/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Konfiguracija podatkovne baze -***************************** - -.[perex] -Pregled konfiguracijskih možnosti za Nette Database. - -Če ne uporabljate celotnega ogrodja, ampak samo to knjižnico, preberite, [kako naložiti konfiguracijo|bootstrap:]. - - -Ena povezava ------------- - -Konfiguracija ene podatkovne povezave: - -```neon -database: - # DSN, edini obvezni ključ - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Ustvari storitvi `Nette\Database\Connection` in `Nette\Database\Explorer`, ki si jih običajno posredujemo z [autowiringom |dependency-injection:autowiring], ali pa s sklicem na [njihovo ime |#Storitve DI]. - -Druge nastavitve: - -```neon -database: - # prikazati ploščo podatkovne baze v Tracy Bar? - debugger: ... # (bool) privzeto je true - - # prikazati EXPLAIN poizvedb v Tracy Bar? - explain: ... # (bool) privzeto je true - - # dovoliti autowiring za to povezavo? - autowired: ... # (bool) privzeto je true pri prvi povezavi - - # konvencije tabel: discovered, static ali ime razreda - conventions: discovered # (string) privzeto je 'discovered' - - options: - # povezati se s podatkovno bazo šele, ko je potrebno? - lazy: ... # (bool) privzeto je false - - # PHP razred gonilnika podatkovne baze - driverClass: # (string) - - # samo MySQL: nastavi sql_mode - sqlmode: # (string) - - # samo MySQL: nastavi SET NAMES - charset: # (string) privzeto je 'utf8mb4' - - # samo MySQL: pretvori TINYINT(1) v bool - convertBoolean: # (bool) privzeto je false - - # vrača stolpce z datumom kot nespremenljive objekte (od različice 3.2.1) - newDateTime: # (bool) privzeto je false - - # samo Oracle in SQLite: format za shranjevanje datuma - formatDateTime: # (string) privzeto je 'U' -``` - -V ključu `options` lahko navajate druge možnosti, ki jih najdete v [dokumentaciji gonilnikov PDO |https://www.php.net/manual/en/pdo.drivers.php], kot na primer: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Več povezav ------------ - -V konfiguraciji lahko definiramo tudi več podatkovnih povezav z razdelitvijo na poimenovane sekcije: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring je vklopljen samo pri storitvah iz prve sekcije. To lahko spremenite s pomočjo `autowired: false` ali `autowired: true`. - - -Storitve DI ------------ - -Te storitve se dodajo v DI vsebnik, kjer `###` predstavlja ime povezave: - -| Ime | Tip | Opis -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | povezava s podatkovno bazo -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Če definiramo samo eno povezavo, bosta imeni storitev `database.default.connection` in `database.default.explorer`. Če definiramo več povezav kot v zgornjem primeru, bodo imena ustrezala sekcijam, tj. `database.main.connection`, `database.main.explorer` in naprej `database.another.connection` ter `database.another.explorer`. - -Ne-autowirane storitve posredujemo eksplicitno s sklicem na njihovo ime: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/sl/exceptions.texy b/database/sl/exceptions.texy deleted file mode 100644 index 07d3bd8871..0000000000 --- a/database/sl/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Izjeme -****** - -Nette Database uporablja hierarhijo izjem. Osnovni razred je `Nette\Database\DriverException`, ki deduje iz `PDOException` in nudi razširjene možnosti za delo z napakami podatkovne baze: - -- Metoda `getDriverCode()` vrača kodo napake od gonilnika podatkovne baze -- Metoda `getSqlState()` vrača kodo SQLSTATE -- Metodi `getQueryString()` in `getParameters()` omogočata pridobitev prvotne poizvedbe in njenih parametrov - -Iz `DriverException` dedujejo naslednje specializirane izjeme: - -- `ConnectionException` - signalizira neuspeh povezave s podatkovnim strežnikom -- `ConstraintViolationException` - osnovni razred za kršitve podatkovnih omejitev, iz katerega dedujejo: - - `ForeignKeyConstraintViolationException` - kršitev tujega ključa - - `NotNullConstraintViolationException` - kršitev omejitve NOT NULL - - `UniqueConstraintViolationException` - kršitev edinstvenosti vrednosti - - -Primer lovljenja izjeme `UniqueConstraintViolationException`, ki nastane, ko poskušamo vstaviti uporabnika z e-pošto, ki že obstaja v podatkovni bazi (ob predpostavki, da ima stolpec email edinstven indeks). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Uporabnik s tem e-naslovom že obstaja.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Pri registraciji je prišlo do napake: ' . $e->getMessage(); -} -``` diff --git a/database/sl/explorer.texy b/database/sl/explorer.texy deleted file mode 100644 index 1be13e409f..0000000000 --- a/database/sl/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer ponuja intuitiven in učinkovit način dela s podatkovno bazo. Samodejno skrbi za relacije med tabelami in optimizacijo poizvedb, tako da se lahko osredotočite na svojo aplikacijo. Deluje takoj brez nastavljanja. Če potrebujete popoln nadzor nad SQL poizvedbami, lahko uporabite [SQL pristop |SQL way]. - -- Delo s podatki je naravno in enostavno razumljivo -- Generira optimizirane SQL poizvedbe, ki nalagajo samo potrebne podatke -- Omogoča enostaven dostop do povezanih podatkov brez potrebe po pisanju JOIN poizvedb -- Deluje takoj brez kakršnekoli konfiguracije ali generiranja entitet - -</div> - - -Z Explorerjem začnete s klicem metode `table()` objekta [api:Nette\Database\Explorer] (podrobnosti o povezavi najdete v poglavju [Povezava in konfiguracija |guide#Povezava in konfiguracija]): - -```php -$books = $explorer->table('book'); // 'book' je ime tabele -``` - -Metoda vrača objekt [Selection |api:Nette\Database\Table\Selection], ki predstavlja SQL poizvedbo. Na ta objekt lahko navezujemo nadaljnje metode za filtriranje in razvrščanje rezultatov. Poizvedba se sestavi in zažene šele v trenutku, ko začnemo zahtevati podatke. Na primer s prehajanjem skozi zanko `foreach`. Vsaka vrstica je predstavljena z objektom [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // izpis stolpca 'title' - echo $book->author_id; // izpis stolpca 'author_id' -} -``` - -Explorer bistveno olajša delo s [povezavami med tabelami |#Povezave med tabelami]. Naslednji primer prikazuje, kako enostavno lahko izpišemo podatke iz povezanih tabel (knjige in njihovi avtorji). Opazite, da nam ni treba pisati nobenih JOIN poizvedb, Nette jih ustvari za nas: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Knjiga: ' . $book->title; - echo 'Avtor: ' . $book->author->name; // ustvari JOIN na tabelo 'author' -} -``` - -Nette Database Explorer optimizira poizvedbe, da so čim bolj učinkovite. Zgornji primer izvede samo dve SELECT poizvedbi, ne glede na to, ali obdelujemo 10 ali 10.000 knjig. - -Poleg tega Explorer spremlja, kateri stolpci se v kodi uporabljajo, in nalaga iz podatkovne baze samo te, s čimer prihrani dodatno zmogljivost. To obnašanje je popolnoma samodejno in prilagodljivo. Če kasneje prilagodite kodo in začnete uporabljati druge stolpce, Explorer samodejno prilagodi poizvedbe. Ničesar vam ni treba nastavljati, niti razmišljati o tem, katere stolpce boste potrebovali - prepustite to Nette. - - -Filtriranje in razvrščanje -========================== - -Razred `Selection` ponuja metode za filtriranje in razvrščanje izbora podatkov. - -.[language-php] -| `where($condition, ...$params)` | Doda pogoj WHERE. Več pogojev je povezanih z operatorjem AND -| `whereOr(array $conditions)` | Doda skupino pogojev WHERE, povezanih z operatorjem OR -| `wherePrimary($value)` | Doda pogoj WHERE po primarnem ključu -| `order($columns, ...$params)` | Nastavi razvrščanje ORDER BY -| `select($columns, ...$params)` | Določi stolpce, ki naj se naložijo -| `limit($limit, $offset = null)` | Omeji število vrstic (LIMIT) in po želji nastavi OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Nastavi stranskanje -| `group($columns, ...$params)` | Združi vrstice (GROUP BY) -| `having($condition, ...$params)` | Doda pogoj HAVING za filtriranje združenih vrstic - -Metode lahko verižimo (t.i. [fluent interface |nette:introduction-to-object-oriented-programming#Tekoči vmesniki]): `$table->where(...)->order(...)->limit(...)`. - -V teh metodah lahko uporabljate tudi posebno notacijo za dostop do [podatkov iz povezanih tabel |#Poizvedovanje prek povezanih tabel]. - - -Ubežanje znakov in identifikatorji ----------------------------------- - -Metode samodejno ubežijo parametre in navajajo identifikatorje (imena tabel in stolpcev), s čimer preprečujejo SQL injection. Za pravilno delovanje je treba upoštevati nekaj pravil: - -- Ključne besede, imena funkcij, procedur ipd. pišite **z velikimi črkami**. -- Imena stolpcev in tabel pišite **z malimi črkami**. -- Nize vedno vstavljajte prek **parametrov**. - -```php -where('name = ' . $name); // KRITIČNA RANLJIVOST: SQL injection -where('name LIKE "%search%"'); // NAPAKA: otežuje samodejno navajanje -where('name LIKE ?', '%search%'); // PRAVILNO: vrednost vstavljena prek parametra - -where('name like ?', $name); // NAPAKA: generira: `name` `like` ? -where('name LIKE ?', $name); // PRAVILNO: generira: `name` LIKE ? -where('LOWER(name) = ?', $value);// PRAVILNO: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtrira rezultate s pomočjo pogojev WHERE. Njena močna stran je inteligentno delo z različnimi tipi vrednosti in samodejna izbira SQL operatorjev. - -Osnovna uporaba: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Zahvaljujoč samodejnemu zaznavanju ustreznih operatorjev nam ni treba reševati različnih posebnih primerov. Nette jih reši za nas: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// lahko se uporabi tudi nadomestni vprašaj brez operatorja: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Metoda pravilno obdela tudi negativne pogoje in prazno polje: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- ničesar ne najde -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- najde vse -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- najde vse -// $table->where('NOT id ?', $ids); Pozor - ta sintaksa ni podprta -``` - -Kot parameter lahko posredujemo tudi rezultat iz druge tabele - ustvari se podpoizvedba: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Pogoje lahko posredujemo tudi kot polje, katerega elementi se združijo s pomočjo AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -V polju lahko uporabimo pare ključ => vrednost in Nette spet samodejno izbere pravilne operatorje: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -V polju lahko kombiniramo SQL izraze z nadomestnimi vprašaji in več parametri. To je primerno za kompleksne pogoje z natančno določenimi operatorji: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // dva parametra posredujemo kot polje -]); -``` - -Večkratni klic `where()` pogoje samodejno združuje s pomočjo AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Podobno kot `where()` dodaja pogoje, vendar s to razliko, da jih združuje s pomočjo OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Tudi tukaj lahko uporabimo kompleksnejše izraze: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Doda pogoj za primarni ključ tabele: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Če ima tabela sestavljen primarni ključ (npr. `foo_id`, `bar_id`), ga posredujemo kot polje: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Določa vrstni red, v katerem bodo vrnjene vrstice. Lahko razvrščamo po enem ali več stolpcih, v padajočem ali naraščajočem vrstnem redu, ali po lastnem izrazu: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Določa stolpce, ki naj se vrnejo iz podatkovne baze. V privzetem stanju Nette Database Explorer vrača samo tiste stolpce, ki se dejansko uporabijo v kodi. Metodo `select()` tako uporabljamo v primerih, ko moramo vrniti specifične izraze: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Aliasi, definirani s pomočjo `AS`, so nato dostopni kot lastnosti objekta ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // dostop do aliasa -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Omejuje število vrnjenih vrstic (LIMIT) in po želji omogoča nastavitev odmika (offset): - -```php -$table->limit(10); // LIMIT 10 (vrne prvih 10 vrstic) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Za stranskanje je primernejša uporaba metode `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Olajša stranskanje rezultatov. Sprejme številko strani (šteto od 1) in število postavk na stran. Po želji lahko posredujemo referenco na spremenljivko, v katero se shrani skupno število strani: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Skupaj strani: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Združuje vrstice po navedenih stolpcih (GROUP BY). Uporablja se običajno v povezavi z agregatnimi funkcijami: - -```php -// Prešteje število izdelkov v vsaki kategoriji -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Nastavi pogoj za filtriranje združenih vrstic (HAVING). Lahko se uporablja v povezavi z metodo `group()` in agregatnimi funkcijami: - -```php -// Najde kategorije, ki imajo več kot 100 izdelkov -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Branje podatkov -=============== - -Za branje podatkov iz podatkovne baze imamo na voljo več uporabnih metod: - -.[language-php] -| `foreach ($table as $key => $row)` | Iterira čez vse vrstice, `$key` je vrednost primarnega ključa, `$row` je objekt ActiveRow -| `$row = $table->get($key)` | Vrne eno vrstico po primarnem ključu -| `$row = $table->fetch()` | Vrne trenutno vrstico in premakne kazalec na naslednjo -| `$array = $table->fetchPairs()` | Ustvari asociativno polje iz rezultatov -| `$array = $table->fetchAll()` | Vrne vse vrstice kot polje -| `count($table)` | Vrne število vrstic v objektu Selection - -Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je namenjen samo za branje. To pomeni, da ni mogoče spreminjati vrednosti njegovih lastnosti. Ta omejitev zagotavlja doslednost podatkov in preprečuje nepričakovane stranske učinke. Podatki se nalagajo iz podatkovne baze in vsaka sprememba bi morala biti izvedena eksplicitno in nadzorovano. - - -`foreach` - iteracija čez vse vrstice -------------------------------------- - -Najlažji način za izvedbo poizvedbe in pridobitev vrstic je iteriranje v zanki `foreach`. Samodejno zažene SQL poizvedbo. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key je vrednost primarnega ključa, $book je ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Izvede SQL poizvedbo in vrne vrstico po primarnem ključu, ali `null`, če ne obstaja. - -```php -$book = $explorer->table('book')->get(123); // vrne ActiveRow z ID 123 ali null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Vrne vrstico in premakne notranji kazalec na naslednjo. Če ni več vrstic, vrne `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Vrne rezultate kot asociativno polje. Prvi argument določa ime stolpca, ki se uporabi kot ključ v polju, drugi argument določa ime stolpca, ki se uporabi kot vrednost: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Če navedemo samo prvi parameter, bo vrednost celotna vrstica, torej objekt `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -V primeru podvojenih ključev se uporabi vrednost iz zadnje vrstice. Pri uporabi `null` kot ključa bo polje indeksirano numerično od nič (takrat do kolizij ne pride): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativno lahko kot parameter navedete povratni klic (callback), ki bo za vsako vrstico vračal bodisi samo vrednost, bodisi par ključ-vrednost. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Prva knjiga (Jan Novak)', ...] - -// Callback lahko vrne tudi polje s parom ključ & vrednost: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Prva knjiga' => 'Jan Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Vrne vse vrstice kot asociativno polje objektov `ActiveRow`, kjer so ključi vrednosti primarnih ključev. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Metoda `count()` brez parametra vrne število vrstic v objektu `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativa -``` - -Pozor, `count()` s parametrom izvaja agregatno funkcijo COUNT v podatkovni bazi, glej spodaj. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Pretvori objekt `ActiveRow` v asociativno polje, kjer so ključi imena stolpcev in vrednosti ustrezni podatki. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray bo ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregacija -========== - -Razred `Selection` ponuja metode za enostavno izvajanje agregatnih funkcij (COUNT, SUM, MIN, MAX, AVG itd.). - -.[language-php] -| `count($expr)` | Prešteje število vrstic -| `min($expr)` | Vrne minimalno vrednost v stolpcu -| `max($expr)` | Vrne maksimalno vrednost v stolpcu -| `sum($expr)` | Vrne vsoto vrednosti v stolpcu -| `aggregation($function)` | Omogoča izvedbo poljubne agregatne funkcije. Npr. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Izvede SQL poizvedbo s funkcijo COUNT in vrne rezultat. Metoda se uporablja za ugotavljanje, koliko vrstic ustreza določenemu pogoju: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Pozor, [#count()] brez parametra samo vrača število vrstic v objektu `Selection`. - - -min(string $expr) a max(string $expr) .[method] ------------------------------------------------ - -Metodi `min()` in `max()` vračata minimalno in maksimalno vrednost v določenem stolpcu ali izrazu: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Vrne vsoto vrednosti v določenem stolpcu ali izrazu: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- - -Omogoča izvedbo poljubne agregatne funkcije. - -```php -// povprečna cena izdelkov v kategoriji -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// združi oznake izdelka v en niz -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Če moramo agregirati rezultate, ki so že sami po sebi nastali iz neke agregatne funkcije in združevanja (npr. `SUM(vrednost)` čez združene vrstice), kot drugi argument navedemo agregatno funkcijo, ki naj se uporabi na teh vmesnih rezultatih: - -```php -// Izračuna skupno ceno izdelkov na zalogi za posamezne kategorije in nato sešteje te cene skupaj. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -V tem primeru najprej izračunamo skupno ceno izdelkov v vsaki kategoriji (`SUM(price * stock) AS category_total`) in združimo rezultate po `category_id`. Nato uporabimo `aggregation('SUM(category_total)', 'SUM')` za seštevanje teh vmesnih vsot `category_total`. Drugi argument `'SUM'` pove, da naj se na vmesne rezultate uporabi funkcija SUM. - - -Insert, Update & Delete -======================= - -Nette Database Explorer poenostavlja vstavljanje, posodabljanje in brisanje podatkov. Vse navedene metode v primeru napake vržejo izjemo `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Vstavi nove zapise v tabelo. - -**Vstavljanje enega zapisa:** - -Nov zapis posredujemo kot asociativno polje ali iterable objekt (na primer ArrayHash, ki se uporablja v [obrazcih |forms:]), kjer ključi ustrezajo imenom stolpcev v tabeli. - -Če ima tabela definiran primarni ključ, metoda vrne objekt `ActiveRow`, ki se ponovno naloži iz podatkovne baze, da se upoštevajo morebitne spremembe, izvedene na ravni podatkovne baze (sprožilci, privzete vrednosti stolpcev, izračuni auto-increment stolpcev). S tem je zagotovljena doslednost podatkov in objekt vedno vsebuje aktualne podatke iz podatkovne baze. Če enoličnega primarnega ključa nima, vrne posredovane podatke v obliki polja. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row je instanca ActiveRow in vsebuje celotne podatke vstavljene vrstice, -// vključno s samodejno generiranim ID-jem in morebitnimi spremembami, izvedenimi s sprožilci (triggerji) -echo $row->id; // Izpiše ID novo vstavljenega uporabnika -echo $row->created_at; // Izpiše čas ustvarjanja, če je nastavljen s sprožilcem -``` - -**Vstavljanje več zapisov hkrati:** - -Metoda `insert()` omogoča vstavljanje več zapisov z eno samo SQL poizvedbo. V tem primeru vrne število vstavljenih vrstic. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows bo 2 -``` - -Kot parameter lahko posredujemo tudi objekt `Selection` z izborom podatkov. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Vstavljanje posebnih vrednosti:** - -Kot vrednosti lahko posredujemo tudi datoteke, objekte DateTime ali SQL literale: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // pretvori v format podatkovne baze - 'avatar' => fopen('image.jpg', 'rb'), // vstavi binarno vsebino datoteke - 'uuid' => $explorer::literal('UUID()'), // pokliče funkcijo UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Posodobi vrstice v tabeli po navedenem filtru. Vrne število dejansko spremenjenih vrstic. - -Spremenjene stolpce posredujemo kot asociativno polje ali iterable objekt (na primer ArrayHash, ki se uporablja v [obrazcih |forms:]), kjer ključi ustrezajo imenom stolpcev v tabeli: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Za spremembo številskih vrednosti lahko uporabimo operatorja `+=` in `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // poveča vrednost stolpca 'points' za 1 - 'coins-=' => 1, // zmanjša vrednost stolpca 'coins' za 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Briše vrstice iz tabele po navedenem filtru. Vrne število izbrisanih vrstic. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Pri klicu `update()` in `delete()` ne pozabite s pomočjo `where()` določiti vrstic, ki naj se uredijo/izbrišejo. Če `where()` ne uporabite, se operacija izvede na celotni tabeli! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Posodobi podatke v podatkovni vrstici, ki jo predstavlja objekt `ActiveRow`. Kot parameter sprejme iterable s podatki, ki naj se posodobijo (ključi so imena stolpcev). Za spremembo številskih vrednosti lahko uporabimo operatorja `+=` in `-=`: - -Po izvedbi posodobitve se `ActiveRow` samodejno ponovno naloži iz podatkovne baze, da se upoštevajo morebitne spremembe, izvedene na ravni podatkovne baze (npr. sprožilci). Metoda vrne true samo, če je prišlo do dejanske spremembe podatkov. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // povečamo število prikazov -]); -echo $article->views; // Izpiše trenutno število prikazov -``` - -Ta metoda posodobi samo eno določeno vrstico v podatkovni bazi. Za množično posodabljanje več vrstic uporabite metodo [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Izbriše vrstico iz podatkovne baze, ki jo predstavlja objekt `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Izbriše knjigo z ID 1 -``` - -Ta metoda briše samo eno določeno vrstico v podatkovni bazi. Za množično brisanje več vrstic uporabite metodo [#Selection::delete()]. - - -Povezave med tabelami -===================== - -V relacijskih podatkovnih bazah so podatki razdeljeni na več tabel in medsebojno povezani s pomočjo tujih ključev. Nette Database Explorer prinaša revolucionaren način dela s temi povezavami - brez pisanja JOIN poizvedb in potrebe po kakršnikoli konfiguraciji ali generiranju. - -Za ilustracijo dela s povezavami bomo uporabili primer podatkovne baze knjig ([najdete ga na GitHubu |https://github.com/nette-examples/books]). V podatkovni bazi imamo tabele: - -- `author` - pisatelji in prevajalci (stolpci `id`, `name`, `web`, `born`) -- `book` - knjige (stolpci `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - oznake (stolpci `id`, `name`) -- `book_tag` - povezovalna tabela med knjigami in oznakami (stolpci `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Struktura podatkovne baze .<> - -V našem primeru podatkovne baze knjig najdemo več tipov odnosov (čeprav je model poenostavljen v primerjavi z realnostjo): - -- Ena-proti-mnogo 1:N – vsaka knjiga **ima enega** avtorja, avtor lahko napiše **več** knjig -- Nič-proti-mnogo 0:N – knjiga **lahko ima** prevajalca, prevajalec lahko prevede **več** knjig -- Nič-proti-ena 0:1 – knjiga **lahko ima** naslednji del -- Mnogo-proti-mnogo M:N – knjiga **lahko ima več** oznak in oznaka je lahko dodeljena **več** knjigam - -V teh odnosih vedno obstaja nadrejena in podrejena tabela. Na primer v odnosu med avtorjem in knjigo je tabela `author` nadrejena in `book` podrejena - lahko si predstavljamo, da knjiga vedno »pripada« nekemu avtorju. To se odraža tudi v strukturi podatkovne baze: podrejena tabela `book` vsebuje tuji ključ `author_id`, ki se nanaša na nadrejeno tabelo `author`. - -Če moramo izpisati knjige vključno z imeni njihovih avtorjev, imamo dve možnosti. Ali podatke pridobimo z eno samo SQL poizvedbo s pomočjo JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Ali pa podatke naložimo v dveh korakih - najprej knjige in nato njihove avtorje - in jih nato v PHP sestavimo: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id-ji avtorjev pridobljenih knjig -``` - -Drugi pristop je dejansko učinkovitejši, čeprav se to morda zdi presenetljivo. Podatki so naloženi samo enkrat in jih je mogoče bolje izkoristiti v predpomnilniku. Prav na ta način deluje Nette Database Explorer - vse rešuje pod površjem in vam ponuja eleganten API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author je zapis iz tabele 'author' - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Dostop do nadrejene tabele --------------------------- - -Dostop do nadrejene tabele je neposreden. Gre za odnose kot *knjiga ima avtorja* ali *knjiga lahko ima prevajalca*. Povezan zapis pridobimo prek lastnosti objekta ActiveRow - njeno ime ustreza imenu stolpca s tujim ključem brez `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // najde avtorja po stolpcu author_id -echo $book->translator?->name; // najde prevajalca po stolpcu translator_id -``` - -Ko dostopamo do lastnosti `$book->author`, Explorer v tabeli `book` išče stolpec, katerega ime vsebuje niz `author` (torej `author_id`). Po vrednosti v tem stolpcu naloži ustrezen zapis iz tabele `author` in ga vrne kot `ActiveRow`. Podobno deluje tudi `$book->translator`, ki uporabi stolpec `translator_id`. Ker stolpec `translator_id` lahko vsebuje `null`, v kodi uporabimo operator `?->`. - -Alternativno pot ponuja metoda `ref()`, ki sprejme dva argumenta, ime ciljne tabele in ime povezovalnega stolpca, ter vrne instanco `ActiveRow` ali `null`: - -```php -echo $book->ref('author', 'author_id')->name; // povezava na avtorja -echo $book->ref('author', 'translator_id')->name; // povezava na prevajalca -``` - -Metoda `ref()` je koristna, če ni mogoče uporabiti dostopa prek lastnosti, ker tabela vsebuje stolpec z istim imenom (tj. `author`). V ostalih primerih je priporočljivo uporabljati dostop prek lastnosti, ki je bolj berljiv. - -Explorer samodejno optimizira podatkovne poizvedbe. Ko prehajamo skozi knjige v zanki in dostopamo do njihovih povezanih zapisov (avtorjev, prevajalcev), Explorer ne generira poizvedbe za vsako knjigo posebej. Namesto tega izvede samo en SELECT za vsak tip povezave, s čimer bistveno zmanjša obremenitev podatkovne baze. Na primer: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Ta koda pokliče samo te tri bliskovite poizvedbe v podatkovno bazo: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id iz stolpca author_id izbranih knjig -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id iz stolpca translator_id izbranih knjig -``` - -.[note] -Logika iskanja povezovalnega stolpca je določena z implementacijo [Conventions |api:Nette\Database\Conventions]. Priporočamo uporabo [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], ki analizira tuje ključe in omogoča enostavno delo z obstoječimi relacijami med tabelami. - - -Dostop do podrejene tabele --------------------------- - -Dostop do podrejene tabele deluje v obratni smeri. Zdaj se sprašujemo *katere knjige je napisal ta avtor* ali *prevedel ta prevajalec*. Za ta tip poizvedbe uporabljamo metodo `related()`, ki vrne `Selection` s povezanimi zapisi. Poglejmo si primer: - -```php -$author = $explorer->table('author')->get(1); - -// Izpiše vse knjige avtorja -foreach ($author->related('book.author_id') as $book) { - echo "Napisal: $book->title"; -} - -// Izpiše vse knjige, ki jih je avtor prevedel -foreach ($author->related('book.translator_id') as $book) { - echo "Prevedel: $book->title"; -} -``` - -Metoda `related()` sprejme opis povezave kot en argument s pikčasto notacijo ali kot dva ločena argumenta: - -```php -$author->related('book.translator_id'); // en argument -$author->related('book', 'translator_id'); // dva argumenta -``` - -Explorer zna samodejno zaznati pravilen povezovalni stolpec na podlagi imena nadrejene tabele. V tem primeru se povezuje prek stolpca `book.author_id`, ker je ime izvorne tabele `author`: - -```php -$author->related('book'); // uporabi book.author_id -``` - -Če bi obstajalo več možnih povezav, Explorer vrže izjemo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Metodo `related()` lahko seveda uporabimo tudi pri prehajanju skozi več zapisov v zanki in Explorer tudi v tem primeru samodejno optimizira poizvedbe: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' napisal:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Ta koda generira samo dve bliskoviti SQL poizvedbi: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id izbranih avtorjev -``` - - -Povezava Mnogo-proti-mnogo --------------------------- - -Za povezavo mnogo-proti-mnogo (M:N) je potrebna obstoj povezovalne tabele (v našem primeru `book_tag`), ki vsebuje dva stolpca s tujima ključema (`book_id`, `tag_id`). Vsak od teh stolpcev se nanaša na primarni ključ ene od povezanih tabel. Za pridobitev povezanih podatkov najprej pridobimo zapise iz povezovalne tabele s pomočjo `related('book_tag')` in nato nadaljujemo k ciljnim podatkom: - -```php -$book = $explorer->table('book')->get(1); -// izpiše imena oznak, dodeljenih knjigi -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // izpiše ime oznake prek povezovalne tabele -} - -$tag = $explorer->table('tag')->get(1); -// ali obratno: izpiše imena knjig, označenih s to oznako -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // izpiše ime knjige -} -``` - -Explorer spet optimizira SQL poizvedbe v učinkovito obliko: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id izbranih knjig -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id oznak, najdenih v book_tag -``` - - -Poizvedovanje prek povezanih tabel ----------------------------------- - -V metodah `where()`, `select()`, `order()` in `group()` lahko uporabljamo posebne notacije za dostop do stolpcev iz drugih tabel. Explorer samodejno ustvari potrebne JOINe. - -**Pikčasta notacija** (`nadrejena_tabela.stolpec`) se uporablja za odnos 1:N z vidika podrejene tabele: - -```php -$books = $explorer->table('book'); - -// Najde knjige, katerih avtor ima ime, ki se začne na 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Razvrsti knjige po imenu avtorja padajoče -$books->order('author.name DESC'); - -// Izpiše naslov knjige in ime avtorja -$books->select('book.title, author.name'); -``` - -**Dvopična notacija** (`:podrejena_tabela.stolpec`) se uporablja za odnos 1:N z vidika nadrejene tabele: - -```php -$authors = $explorer->table('author'); - -// Najde avtorje, ki so napisali knjigo s 'PHP' v naslovu -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Prešteje število knjig za vsakega avtorja -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -V zgornjem primeru z dvopično notacijo (`:book.title`) ni določen stolpec s tujim ključem. Explorer samodejno zazna pravilen stolpec na podlagi imena nadrejene tabele. V tem primeru se povezuje prek stolpca `book.author_id`, ker je ime izvorne tabele `author`. Če bi obstajalo več možnih povezav, Explorer vrže izjemo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Povezovalni stolpec lahko eksplicitno navedemo v oklepaju: - -```php -// Najde avtorje, ki so prevedli knjigo s 'PHP' v naslovu -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Notacije lahko verižimo za dostop prek več tabel: - -```php -// Najde avtorje knjig, označenih z oznako 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Razširitev pogojev za JOIN --------------------------- - -Metoda `joinWhere()` razširja pogoje, ki se navajajo pri povezovanju tabel v SQL za ključno besedo `ON`. - -Recimo, da želimo najti knjige, prevedene s strani določenega prevajalca: - -```php -// Najde knjige, prevedene s strani prevajalca z imenom 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -V pogoju `joinWhere()` lahko uporabljamo enake konstrukcije kot v metodi `where()` - operatorje, nadomestne vprašaje, polja vrednosti ali SQL izraze. - -Za kompleksnejše poizvedbe z več JOINi lahko definiramo aliase tabel: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Opazite, da medtem ko metoda `where()` dodaja pogoje v klavzulo `WHERE`, metoda `joinWhere()` razširja pogoje v klavzuli `ON` pri povezovanju tabel. diff --git a/database/sl/guide.texy b/database/sl/guide.texy deleted file mode 100644 index 4ef0012d4d..0000000000 --- a/database/sl/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database je zmogljiva in elegantna podatkovna plast za PHP s poudarkom na preprostosti in pametnih funkcijah. Ponuja dva načina dela z bazo podatkov - [Explorer] za hiter razvoj aplikacij ali [SQL pristop |SQL way] za neposredno delo s poizvedbami. - -<div class="grid gap-3"> -<div> - - -[SQL pristop |SQL way] -====================== -- Varne parametrizirane poizvedbe -- Natančen nadzor nad obliko SQL poizvedb -- Ko pišete kompleksne poizvedbe z naprednimi funkcijami -- Optimizirate zmogljivost s specifičnimi SQL funkcijami - -</div> - -<div> - - -[Explorer] -========== -- Hitro razvijate brez pisanja SQL -- Intuitivno delo z relacijami med tabelami -- Cenili boste samodejno optimizacijo poizvedb -- Primerno za hitro in udobno delo z bazo podatkov - -</div> - -</div> - - -Namestitev -========== - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Podprte podatkovne baze -======================= - -Nette Database podpira naslednje podatkovne baze: - -|* Podatkovni strežnik |* Ime DSN |* Podpora v Explorerju -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | DA -| PostgreSQL (>= 9.0) | pgsql | DA -| Sqlite 3 (>= 3.8) | sqlite | DA -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Dva pristopa k bazi podatkov -============================ - -Nette Database vam daje izbiro: lahko pišete SQL poizvedbe neposredno (SQL pristop) ali pa jih pustite samodejno generirati (Explorer). Poglejmo, kako oba pristopa rešujeta enake naloge: - -[SQL pristop|sql way] - SQL poizvedbe - -```php -// vstavljanje zapisa -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// pridobivanje zapisov: avtorji knjig -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// izpis (ni optimalno, generira N dodatnih poizvedb) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Avtor $author->name je napisal $author->books_count knjig:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Pristop Explorer|explorer] - samodejno generiranje SQL - -```php -// vstavljanje zapisa -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// pridobivanje zapisov: avtorji knjig -$authors = $database->table('authors') - ->where('active', 1); - -// izpis (samodejno generira samo 2 optimizirani poizvedbi) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Avtor $author->name je napisal {$books->count()} knjig:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Pristop Explorer samodejno generira in optimizira SQL poizvedbe. V navedenem primeru SQL pristop generira N+1 poizvedb (eno za avtorje in nato eno za knjige vsakega avtorja), medtem ko Explorer samodejno optimizira poizvedbe in izvede samo dve - eno za avtorje in eno za vse njihove knjige. - -Oba pristopa lahko v aplikaciji poljubno kombinirate po potrebi. - - -Povezava in konfiguracija -========================= - -Za povezavo z bazo podatkov zadostuje ustvariti instanco razreda [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Parameter `$dsn` (data source name) je enak, [kot ga uporablja PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], npr. `host=127.0.0.1;dbname=test`. V primeru napake vrže izjemo `Nette\Database\ConnectionException`. - -Vendar pa spretnejši način ponuja [konfiguracija aplikacije |configuration], kamor zadostuje dodati sekcijo `database` in ustvarijo se potrebni objekti ter tudi podatkovna plošča v [Tracy |tracy:] baru. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Nato objekt povezave [pridobimo kot storitev iz DI vsebnika |dependency-injection:passing-dependencies], npr.: - -```php -class Model -{ - public function __construct( - // ali Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Več informacij o [konfiguraciji baze podatkov|configuration]. - - -Ročno ustvarjanje Explorerja ----------------------------- - -Če ne uporabljate Nette DI vsebnika, lahko instanco `Nette\Database\Explorer` ustvarite ročno: - -```php -// povezava z bazo podatkov -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// shramba za predpomnilnik, implementira Nette\Caching\Storage, npr.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// skrbi za refleksijo strukture baze podatkov -$structure = new Nette\Database\Structure($connection, $storage); -// definira pravila za preslikavo imen tabel, stolpcev in tujih ključev -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Upravljanje povezave -==================== - -Pri ustvarjanju objekta `Connection` se samodejno vzpostavi povezava. Če želite povezavo odložiti, uporabite lazy način - tega vklopite v [konfiguraciji|configuration] z nastavitvijo `lazy` ali takole: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Za upravljanje povezave uporabite metode `connect()`, `disconnect()` in `reconnect()`. -- `connect()` ustvari povezavo, če še ne obstaja, pri čemer lahko vrže izjemo `Nette\Database\ConnectionException`. -- `disconnect()` prekine trenutno povezavo z bazo podatkov. -- `reconnect()` izvede prekinitev in nato ponovno vzpostavitev povezave z bazo podatkov. Ta metoda lahko prav tako vrže izjemo `Nette\Database\ConnectionException`. - -Poleg tega lahko spremljate dogodke, povezane s povezavo, z uporabo dogodka `onConnect`, ki je polje povratnih klicev (callback), ki se pokličejo po vzpostavitvi povezave z bazo podatkov. - -```php -// izvede se po povezavi z bazo podatkov -$database->onConnect[] = function($database) { - echo "Povezano z bazo podatkov"; -}; -``` - - -Tracy Debug Bar -=============== - -Če uporabljate [Tracy |tracy:], se samodejno aktivira plošča Database v Debug baru, ki prikazuje vse izvedene poizvedbe, njihove parametre, čas izvedbe in mesto v kodi, kjer so bile poklicane. - -[* db-panel.webp *] diff --git a/database/sl/mapping.texy b/database/sl/mapping.texy deleted file mode 100644 index 132f47367e..0000000000 --- a/database/sl/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Pretvorba tipov -*************** - -.[perex] -Nette Database samodejno pretvarja vrednosti, vrnjene iz baze podatkov, v ustrezne PHP tipe. - - -Datum in čas ------------- - -Časovni podatki se pretvorijo v objekte `Nette\Utils\DateTime`. Če želite, da se časovni podatki pretvorijo v nespremenljive objekte `Nette\Database\DateTime`, nastavite v [konfiguraciji|configuration] možnost `newDateTime` na true. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -V primeru MySQL pretvarja podatkovni tip `TIME` v objekte `DateInterval`. - - -Booleove vrednosti ------------------- - -Booleove vrednosti se samodejno pretvorijo v `true` ali `false`. Pri MySQL se pretvarja `TINYINT(1)`, če nastavimo v [konfiguraciji|configuration] `convertBoolean`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Številske vrednosti -------------------- - -Številske vrednosti se pretvorijo v `int` ali `float` glede na tip stolpca v bazi podatkov: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Lastna normalizacija --------------------- - -Z metodo `setRowNormalizer(?callable $normalizer)` lahko nastavite lastno funkcijo za transformacijo vrstic iz baze podatkov. To je koristno na primer za samodejno pretvorbo podatkovnih tipov. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // tukaj poteka pretvorba tipov - return $row; -}); -``` diff --git a/database/sl/reflection.texy b/database/sl/reflection.texy deleted file mode 100644 index a5d4cb846a..0000000000 --- a/database/sl/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Refleksija strukture -******************** - -.{data-version:3.2.1} -Nette Database ponuja orodja za introspekcijo strukture baze podatkov z uporabo razreda [api:Nette\Database\Reflection]. Ta omogoča pridobivanje informacij o tabelah, stolpcih, indeksih in tujih ključih. Refleksijo lahko uporabite za generiranje shem, ustvarjanje fleksibilnih aplikacij, ki delajo z bazo podatkov, ali splošnih orodij za baze podatkov. - -Objekt refleksije pridobimo iz instance povezave z bazo podatkov: - -```php -$reflection = $database->getReflection(); -``` - - -Pridobivanje tabel ------------------- - -Readonly lastnost `$reflection->tables` vsebuje asociativno polje vseh tabel v bazi podatkov: - -```php -// Izpis imen vseh tabel -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Na voljo sta še dve metodi: - -```php -// Preverjanje obstoja tabele -if ($reflection->hasTable('users')) { - echo "Tabela users obstaja"; -} - -// Vrne objekt tabele; če ne obstaja, vrže izjemo -$table = $reflection->getTable('users'); -``` - - -Informacije o tabeli --------------------- - -Tabela je predstavljena z objektom [Table|api:Nette\Database\Reflection\Table], ki ponuja naslednje readonly lastnosti: - -- `$name: string` – ime tabele -- `$view: bool` – ali gre za pogled (view) -- `$fullName: ?string` – polno ime tabele, vključno s shemo (če obstaja) -- `$columns: array<string, Column>` – asociativno polje stolpcev tabele -- `$indexes: Index[]` – polje indeksov tabele -- `$primaryKey: ?Index` – primarni ključ tabele ali null -- `$foreignKeys: ForeignKey[]` – polje tujih ključev tabele - - -Stolpci -------- - -Lastnost `columns` tabele ponuja asociativno polje stolpcev, kjer je ključ ime stolpca in vrednost instanca [Column|api:Nette\Database\Reflection\Column] s temi lastnostmi: - -- `$name: string` – ime stolpca -- `$table: ?Table` – referenca na tabelo stolpca -- `$nativeType: string` – nativni podatkovni tip baze podatkov -- `$size: ?int` – velikost/dolžina tipa -- `$nullable: bool` – ali lahko stolpec vsebuje NULL -- `$default: mixed` – privzeta vrednost stolpca -- `$autoIncrement: bool` – ali je stolpec auto-increment -- `$primary: bool` – ali je del primarnega ključa -- `$vendor: array` – dodatni metapodatki, specifični za dani sistem baze podatkov - -```php -foreach ($table->columns as $name => $column) { - echo "Stolpec: $name\n"; - echo "Tip: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Da' : 'Ne') . "\n"; -} -``` - - -Indeksi -------- - -Lastnost `indexes` tabele ponuja polje indeksov, kjer je vsak indeks instanca [Index|api:Nette\Database\Reflection\Index] s temi lastnostmi: - -- `$columns: Column[]` – polje stolpcev, ki tvorijo indeks -- `$unique: bool` – ali je indeks unikaten -- `$primary: bool` – ali gre za primarni ključ -- `$name: ?string` – ime indeksa - -Primarni ključ tabele lahko pridobimo z lastnostjo `primaryKey`, ki vrne bodisi objekt `Index` ali `null` v primeru, da tabela nima primarnega ključa. - -```php -// Izpis indeksov -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Indeks" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Stolpci: $columns\n"; - echo " Unique: " . ($index->unique ? 'Da' : 'Ne') . "\n"; -} - -// Izpis primarnega ključa -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primarni ključ: $columns\n"; -} -``` - - -Tuji ključi ------------ - -Lastnost `foreignKeys` tabele ponuja polje tujih ključev, kjer je vsak tuji ključ instanca [ForeignKey|api:Nette\Database\Reflection\ForeignKey] s temi lastnostmi: - -- `$foreignTable: Table` – referencirana tabela -- `$localColumns: Column[]` – polje lokalnih stolpcev -- `$foreignColumns: Column[]` – polje referenciranih stolpcev -- `$name: ?string` – ime tujega ključa - -```php -// Izpis tujih ključev -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/sl/security.texy b/database/sl/security.texy deleted file mode 100644 index 9dda20fdc4..0000000000 --- a/database/sl/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Varnostna tveganja -****************** - -<div class=perex> - -Baza podatkov pogosto vsebuje občutljive podatke in omogoča izvajanje nevarnih operacij. Za varno delo z Nette Database je ključno: - -- Razumeti razliko med varnim in nevarnim API-jem -- Uporabljati parametrizirane poizvedbe -- Pravilno validirati vhodne podatke - -</div> - - -Kaj je SQL Injection? -===================== - -SQL injection je najresnejše varnostno tveganje pri delu z bazo podatkov. Nastane, ko neobdelan vnos uporabnika postane del SQL poizvedbe. Napadalec lahko vstavi lastne SQL ukaze in s tem: -- Pridobi nepooblaščen dostop do podatkov -- Spremeni ali izbriše podatke v bazi podatkov -- Obide avtentikacijo - -```php -// ❌ NEVARNA KODA - ranljiva za SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Napadalec lahko vnese na primer vrednost: ' OR '1'='1 -// Rezultatna poizvedba bo potem: SELECT * FROM users WHERE name = '' OR '1'='1' -// Kar vrne vse uporabnike -``` - -Enako velja tudi za Database Explorer: - -```php -// ❌ NEVARNA KODA - ranljiva za SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Parametrizirane poizvedbe -========================= - -Osnovna obramba pred SQL injection so parametrizirane poizvedbe. Nette Database ponuja več načinov njihove uporabe. - -Najenostavnejši način je uporaba **nadomestnih vprašajev**: - -```php -// ✅ Varna parametrizirana poizvedba -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Varen pogoj v Explorerju -$table->where('name = ?', $name); -``` - -To velja za vse druge metode v [Database Explorer|explorer], ki omogočajo vstavljanje izrazov z nadomestnimi vprašaji in parametri. - -Za ukaze INSERT, UPDATE ali klavzulo WHERE lahko vrednosti posredujemo v polju: - -```php -// ✅ Varen INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Varen INSERT v Explorerju -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validacija vrednosti parametrov -=============================== - -Parametrizirane poizvedbe so osnovni gradnik varnega dela z bazo podatkov. Vendar pa morajo vrednosti, ki jih vstavljamo vanje, preiti več ravni preverjanj: - - -Tipska kontrola ---------------- - -**Najpomembnejše je zagotoviti pravilen podatkovni tip parametrov** - to je nujen pogoj za varno uporabo Nette Database. Baza podatkov predpostavlja, da imajo vsi vhodni podatki pravilen podatkovni tip, ki ustreza danemu stolpcu. - -Na primer, če bi bil `$name` v prejšnjih primerih nepričakovano polje namesto niza, bi Nette Database poskusila vstaviti vse njegove elemente v SQL poizvedbo, kar bi povzročilo napako. Zato **nikoli ne uporabljajte** nevalidiranih podatkov iz `$_GET`, `$_POST` ali `$_COOKIE` neposredno v poizvedbah baze podatkov. - - -Formatna kontrola ------------------ - -Na drugi ravni preverjamo format podatkov - na primer, ali so nizi v UTF-8 kodiranju in njihova dolžina ustreza definiciji stolpca, ali pa so številske vrednosti v dovoljenem obsegu za dani podatkovni tip stolpca. - -Pri tej ravni validacije se lahko delno zanesemo tudi na samo bazo podatkov - mnoge baze podatkov zavrnejo nevalidne podatke. Vendar pa se obnašanje lahko razlikuje, nekatere lahko dolge nize tiho skrajšajo ali števila izven obsega obrežejo. - - -Domenska kontrola ------------------ - -Tretjo raven predstavljajo logične kontrole, specifične za vašo aplikacijo. Na primer preverjanje, da vrednosti iz izbirnih polj ustrezajo ponujenim možnostim, da so števila v pričakovanem obsegu (npr. starost 0-150 let) ali da medsebojne odvisnosti med vrednostmi imajo smisel. - - -Priporočeni načini validacije ------------------------------ - -- Uporabljajte [Nette Obrazce|forms:], ki samodejno zagotovijo pravilno validacijo vseh vnosov -- Uporabljajte [Presenterje|application:] in navedite pri parametrih v `action*()` in `render*()` metodah podatkovne tipe -- Ali implementirajte lastno validacijsko plast z uporabo standardnih PHP orodij, kot je `filter_var()` - - -Varno delo s stolpci -==================== - -V prejšnjem odseku smo si ogledali, kako pravilno validirati vrednosti parametrov. Pri uporabi polj v SQL poizvedbah pa moramo enako pozornost posvetiti tudi njihovim ključem. - -```php -// ❌ NEVARNA KODA - niso obdelani ključi v polju -$database->query('INSERT INTO users', $_POST); -``` - -Pri ukazih INSERT in UPDATE je to temeljna varnostna napaka - napadalec lahko v bazo podatkov vstavi ali spremeni kateri koli stolpec. Lahko bi si na primer nastavil `is_admin = 1` ali vstavil poljubne podatke v občutljive stolpce (t.i. Mass Assignment Vulnerability). - -V pogojih WHERE je to še nevarnejše, saj lahko vsebujejo operatorje: - -```php -// ❌ NEVARNA KODA - niso obdelani ključi v polju -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// izvede poizvedbo WHERE (`salary` > 100000) -``` - -Napadalec lahko ta pristop izkoristi za sistematično ugotavljanje plač zaposlenih. Začne na primer s poizvedbo o plačah nad 100.000, nato pod 50.000 in s postopnim zoževanjem obsega lahko odkrije približne plače vseh zaposlenih. Ta tip napada se imenuje SQL enumeration. - -Metodi `where()` in `whereOr()` sta še [veliko bolj fleksibilni |explorer#where] in podpirata v ključih in vrednostih SQL izraze, vključno z operatorji in funkcijami. To daje napadalcu možnost izvedbe SQL injection: - -```php -// ❌ NEVARNA KODA - napadalec lahko vstavi lasten SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// izvede poizvedbo WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Ta napad zaključi prvotni pogoj z `0)`, priključi lasten `SELECT` z `UNION`, da pridobi občutljive podatke iz tabele `users`, in zaključi sintaktično pravilno poizvedbo z `WHERE (1)`. - - -Beli seznam stolpcev --------------------- - -Za varno delo z imeni stolpcev potrebujemo mehanizem, ki zagotavlja, da lahko uporabnik dela samo z dovoljenimi stolpci in ne more dodati lastnih. Lahko bi poskusili zaznati in blokirati nevarna imena stolpcev (črni seznam), vendar je ta pristop nezanesljiv - napadalec lahko vedno najde nov način, kako zapisati nevarno ime stolpca, ki ga nismo predvideli. - -Zato je veliko varneje obrniti logiko in definirati ekspliciten seznam dovoljenih stolpcev (beli seznam): - -```php -// Stolpci, ki jih lahko uporabnik ureja -$allowedColumns = ['name', 'email', 'active']; - -// Odstranimo vse nedovoljene stolpce iz vnosa -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // array_flip for performance - -// ✅ Zdaj lahko varno uporabimo v poizvedbah, kot na primer: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dinamični identifikatorji -========================= - -Za dinamična imena tabel in stolpcev uporabite nadomestni znak `?name`. Ta zagotavlja pravilno ubežanje identifikatorjev glede na sintakso dane baze podatkov (npr. z uporabo povratnih narekovajev v MySQL): - -```php -// ✅ Varna uporaba zaupanja vrednih identifikatorjev -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Rezultat v MySQL: SELECT `name` FROM `users` -``` - -Pomembno: simbol `?name` uporabljajte samo za zaupanja vredne vrednosti, definirane v kodi aplikacije. Za vrednosti od uporabnika ponovno uporabite [beli seznam |#Beli seznam stolpcev]. Sicer se izpostavljate varnostnim tveganjem: - -```php -// ❌ NEVARNO - nikoli ne uporabljajte vnosa od uporabnika -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/sl/sql-way.texy b/database/sl/sql-way.texy deleted file mode 100644 index e8857452ba..0000000000 --- a/database/sl/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL pristop -*********** - -.[perex] -Nette Database ponuja dve poti: lahko pišete SQL poizvedbe sami (SQL pristop) ali pa jih pustite samodejno generirati (glej [Explorer |explorer]). SQL pristop vam daje popoln nadzor nad poizvedbami in hkrati zagotavlja njihovo varno sestavljanje. - -.[note] -Podrobnosti o povezavi in konfiguraciji podatkovne baze najdete v poglavju [Povezava in konfiguracija |guide#Povezava in konfiguracija]. - - -Osnovno poizvedovanje -===================== - -Za poizvedovanje v podatkovni bazi služi metoda `query()`. Ta vrne objekt [ResultSet |api:Nette\Database\ResultSet], ki predstavlja rezultat poizvedbe. V primeru napake metoda [vrže izjemo|exceptions]. Rezultat poizvedbe lahko prehajamo z zanko `foreach` ali uporabimo katero od [pomožnih funkcij |#Pridobivanje podatkov]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Za varno vstavljanje vrednosti v SQL poizvedbe uporabljamo parametrizirane poizvedbe. Nette Database jih naredi maksimalno preproste - zadostuje, da za SQL poizvedbo dodamo vejico in vrednost: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Pri več parametrih imate dve možnosti zapisa. Lahko SQL poizvedbo "prepletate" s parametri: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Ali pa najprej napišete celotno SQL poizvedbo in nato priključite vse parametre: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Zaščita pred SQL injection -========================== - -Zakaj je pomembno uporabljati parametrizirane poizvedbe? Ker vas ščitijo pred napadom, imenovanim SQL injection, pri katerem bi napadalec lahko podtaknil lastne SQL ukaze in s tem pridobil ali poškodoval podatke v podatkovni bazi. - -.[warning] -**Nikoli ne vstavljajte spremenljivk neposredno v SQL poizvedbo!** Vedno uporabljajte parametrizirane poizvedbe, ki vas ščitijo pred SQL injection. - -```php -// ❌ NEVARNA KODA - ranljiva za SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Varna parametrizirana poizvedba -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Seznanite se z [možnimi varnostnimi tveganji |security]. - - -Tehnike poizvedovanja -===================== - - -Pogoji WHERE ------------- - -Pogoje WHERE lahko zapišete kot asociativno polje, kjer so ključi imena stolpcev in vrednosti podatki za primerjavo. Nette Database samodejno izbere najprimernejši SQL operator glede na tip vrednosti. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -V ključu lahko tudi eksplicitno določite operator za primerjavo: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // uporabi operator > - 'name LIKE' => '%John%', // uporabi operator LIKE - 'email NOT LIKE' => '%example.com%', // uporabi operator NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette samodejno obravnava posebne primere, kot so `null` vrednosti ali polja. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // uporabi operator = - 'category_id' => [1, 2, 3], // uporabi IN - 'description' => null, // uporabi IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Za negativne pogoje uporabite operator `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // uporabi operator <> - 'category_id NOT' => [1, 2, 3], // uporabi NOT IN - 'description NOT' => null, // uporabi IS NOT NULL - 'id' => [], // izpusti se -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Za združevanje pogojev se uporablja operator `AND`. To lahko spremenite z uporabo [nadomestnega znaka ?or |#Namigi za sestavljanje SQL]. - - -Pravila ORDER BY ----------------- - -Razvrščanje `ORDER BY` lahko zapišemo z uporabo polja. V ključih navedemo stolpce, vrednost pa bo boolean, ki določa, ali razvrščati naraščajoče: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // naraščajoče - 'name' => false, // padajoče -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Vstavljanje podatkov (INSERT) ------------------------------ - -Za vstavljanje zapisov se uporablja SQL ukaz `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Metoda `getInsertId()` vrne ID zadnje vstavljene vrstice. Pri nekaterih podatkovnih bazah (npr. PostgreSQL) je treba kot parameter določiti ime sekvence, iz katere naj se ID generira z uporabo `$database->getInsertId($sequenceId)`. - -Kot parametre lahko posredujemo tudi [#Posebne vrednosti] kot so datoteke, objekti DateTime ali naštevni tipi. - -Vstavljanje več zapisov hkrati: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Večkratni INSERT je veliko hitrejši, ker se izvede ena sama poizvedba podatkovne baze namesto mnogih posameznih. - -**Varnostno opozorilo:** Nikoli ne uporabljajte kot `$values` nevalidiranih podatkov. Seznanite se z [možnimi tveganji |security#Varno delo s stolpci]. - - -Posodabljanje podatkov (UPDATE) -------------------------------- - -Za posodabljanje zapisov se uporablja SQL ukaz `UPDATE`. - -```php -// Posodobitev enega zapisa -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Število prizadetih vrstic vrne `$result->getRowCount()`. - -Za UPDATE lahko uporabimo operatorja `+=` in `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // inkrementacija login_count -], 1); -``` - -Primer vstavljanja ali urejanja zapisa, če že obstaja. Uporabimo tehniko `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Opazite, da Nette Database prepozna, v kakšnem kontekstu SQL ukaza vstavljamo parameter s poljem in glede na to iz njega sestavi SQL kodo. Tako je iz prvega polja sestavil `(id, name, year) VALUES (123, 'Jim', 1978)`, medtem ko je drugega pretvoril v obliko `name = 'Jim', year = 1978`. Podrobneje se temu posvečamo v delu [#Namigi za sestavljanje SQL]. - - -Brisanje podatkov (DELETE) --------------------------- - -Za brisanje zapisov se uporablja SQL ukaz `DELETE`. Primer s pridobivanjem števila izbrisanih vrstic: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Namigi za sestavljanje SQL --------------------------- - -Namig je poseben nadomestni znak v SQL poizvedbi, ki pove, kako naj se vrednost parametra prepiše v SQL izraz: - -| Namig | Opis | Samodejno se uporabi -|-----------|-------------------------------------------------|----------------------------- -| `?name` | uporabi za vstavljanje imena tabele ali stolpca | - -| `?values` | generira `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | generira prirejanje `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | združi pogoje v polju z operatorjem `AND` | `WHERE ?`, `HAVING ?` -| `?or` | združi pogoje v polju z operatorjem `OR` | - -| `?order` | generira klavzulo `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Za dinamično vstavljanje imen tabel in stolpcev v poizvedbo služi nadomestni znak `?name`. Nette Database poskrbi za pravilno obdelavo identifikatorjev glede na konvencije dane podatkovne baze (npr. zapiranje v povratne narekovaje v MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (v MySQL) -``` - -**Opozorilo:** simbol `?name` uporabljajte samo za imena tabel in stolpcev iz validiranih vnosov, sicer se izpostavljate [varnostnemu tveganju |security#Dinamični identifikatorji]. - -Drugih namigov običajno ni treba navajati, saj Nette pri sestavljanju SQL poizvedbe uporablja pametno samodejno zaznavanje (glej tretji stolpec tabele). Lahko pa ga uporabite na primer v situaciji, ko želite združiti pogoje z `OR` namesto `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Posebne vrednosti ------------------ - -Poleg običajnih skalarnih tipov (string, int, bool) lahko kot parametre posredujete tudi posebne vrednosti: - -- datoteke: `fopen('image.gif', 'r')` vstavi binarno vsebino datoteke -- datum in čas: objekti `DateTime` se pretvorijo v format podatkovne baze -- naštevni tipi: instance `enum` se pretvorijo v njihovo vrednost -- SQL literali: ustvarjeni z `Connection::literal('NOW()')` se vstavijo neposredno v poizvedbo - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Pri podatkovnih bazah, ki nimajo nativne podpore za podatkovni tip `datetime` (kot SQLite in Oracle), se `DateTime` pretvori v vrednost, določeno v [konfiguraciji podatkovne baze|configuration] z vnosom `formatDateTime` (privzeta vrednost je `U` - unix timestamp). - - -SQL literali ------------- - -V nekaterih primerih morate kot vrednost navesti neposredno SQL kodo, ki pa se ne sme razumeti kot niz in ubežati. Za to služijo objekti razreda `Nette\Database\SqlLiteral`. Ustvarja jih metoda `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Ali alternativno: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL literali lahko vsebujejo parametre: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Zaradi česar lahko ustvarjamo zanimive kombinacije: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Pridobivanje podatkov -===================== - - -Bližnjice za SELECT poizvedbe ------------------------------ - -Za poenostavitev nalaganja podatkov `Connection` ponuja več bližnjic, ki kombinirajo klic `query()` z naslednjim `fetch*()`. Te metode sprejemajo enake parametre kot `query()`, torej SQL poizvedbo in neobvezne parametre. Popoln opis metod `fetch*()` najdete [spodaj |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Izvede poizvedbo in vrne prvo vrstico kot objekt `Row` -| `fetchAll($sql, ...$params): array` | Izvede poizvedbo in vrne vse vrstice kot polje objektov `Row` -| `fetchPairs($sql, ...$params): array` | Izvede poizvedbo in vrne asociativno polje, kjer prvi stolpec predstavlja ključ in drugi vrednost -| `fetchField($sql, ...$params): mixed` | Izvede poizvedbo in vrne vrednost prvega polja iz prve vrstice -| `fetchList($sql, ...$params): ?array` | Izvede poizvedbo in vrne prvo vrstico kot indeksirano polje - -Primer: - -```php -// fetchField() - vrne vrednost prve celice -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iteracija čez vrstice ---------------------------------- - -Po izvedbi poizvedbe se vrne objekt [ResultSet|api:Nette\Database\ResultSet], ki omogoča prehajanje rezultatov na več načinov. Najlažji način za izvedbo poizvedbe in pridobitev vrstic je iteracija v zanki `foreach`. Ta način je pomnilniško najbolj varčen, saj vrača podatke postopoma in jih ne shranjuje vseh hkrati v pomnilnik. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` je mogoče iterirati samo enkrat. Če potrebujete iterirati večkrat, morate najprej naložiti podatke v polje, na primer z metodo `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Vrne vrstico kot objekt `Row`. Če ni več vrstic, vrne `null`. Premakne notranji kazalec na naslednjo vrstico. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // naloži prvo vrstico -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Vrne vse preostale vrstice iz `ResultSet` kot polje objektov `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // naloži vse vrstice -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Vrne rezultate kot asociativno polje. Prvi argument določa ime stolpca, ki se uporabi kot ključ v polju, drugi argument določa ime stolpca, ki se uporabi kot vrednost: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Če navedemo samo prvi parameter, bo vrednost celotna vrstica, torej objekt `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -V primeru podvojenih ključev se uporabi vrednost iz zadnje vrstice. Pri uporabi `null` kot ključa bo polje indeksirano numerično od nič (potem do kolizij ne pride): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativno lahko kot parameter navedete povratni klic (callback), ki bo za vsako vrstico vrnil bodisi samo vrednost ali par ključ-vrednost. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback lahko vrne tudi polje s parom ključ & vrednost: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Vrne vrednost prvega polja iz trenutne vrstice. Če ni več vrstic, vrne `null`. Premakne notranji kazalec na naslednjo vrstico. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // naloži ime iz prve vrstice -``` - - -fetchList(): ?array .[method] ------------------------------ - -Vrne vrstico kot indeksirano polje. Če ni več vrstic, vrne `null`. Premakne notranji kazalec na naslednjo vrstico. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Vrne število prizadetih vrstic zadnje poizvedbe `UPDATE` ali `DELETE`. Za `SELECT` je to število vrnjenih vrstic, vendar to morda ni znano - v takem primeru metoda vrne `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Vrne število stolpcev v `ResultSet`. - - -Informacije o poizvedbah -======================== - -Za namene razhroščevanja lahko pridobimo informacije o zadnji izvedeni poizvedbi: - -```php -echo $database->getLastQueryString(); // izpiše SQL poizvedbo - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // izpiše SQL poizvedbo -echo $result->getTime(); // izpiše čas izvedbe v sekundah -``` - -Za prikaz rezultata kot HTML tabele lahko uporabimo: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet ponuja informacije o tipih stolpcev: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column je tipa $type->type"; // npr. 'id je tipa int' -} -``` - - -Dnevniško beleženje poizvedb ----------------------------- - -Lahko implementiramo lastno dnevniško beleženje poizvedb. Dogodek `onQuery` je polje povratnih klicev (callback), ki se pokličejo po vsaki izvedeni poizvedbi: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Poizvedba: ' . $result->getQueryString()); - $logger->info('Čas: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Velik nabor rezultatov: ' . $result->getRowCount() . ' vrstic'); - } -}; -``` diff --git a/database/sl/transactions.texy b/database/sl/transactions.texy deleted file mode 100644 index 4183ae8e2c..0000000000 --- a/database/sl/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Transakcije -*********** - -.[perex] -Transakcije zagotavljajo, da se bodisi izvedejo vse operacije znotraj transakcije ali pa se ne izvede nobena. Uporabne so za zagotavljanje skladnosti podatkov pri bolj zapletenih operacijah. - -Najenostavnejši način uporabe transakcij je videti takole: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Veliko bolj elegantno lahko isto zapišete z metodo `transaction()`. Kot parameter sprejme povratni klic, ki ga izvede v transakciji. Če povratni klic poteka brez izjeme, se transakcija samodejno potrdi. Če pride do izjeme, se transakcija prekliče (rollback) in izjema se širi naprej. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Metoda `transaction()` lahko tudi vrača vrednosti: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // vrne število posodobljenih vrstic -}); -``` diff --git a/database/tr/@home.texy b/database/tr/@home.texy index 7700df2b84..43782badfb 100644 --- a/database/tr/@home.texy +++ b/database/tr/@home.texy @@ -1,21 +1,18 @@ - - Desteklenen Veritabanları ========================= -Nette aşağıdaki veritabanlarını destekler: - -|* Veritabanı sunucusu |* DSN adı |* Core'da Destek |* Explorer'da Destek -| 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 | - +Ş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 - awesome database layer for PHP}} -{{description: Nette Database, SQL sorguları yazmaya gerek kalmadan veritabanından veri almayı önemli ölçüde basitleştirir. Etkili sorgular yapar ve gereksiz verileri aktarmaz.}} +{{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 index 5a4cd8355f..afd037e071 100644 --- a/database/tr/@left-menu.texy +++ b/database/tr/@left-menu.texy @@ -1,12 +1,21 @@ Nette Database ************** -- [Giriş |guide] -- [SQL Yaklaşımı |sql way] -- [Explorer |Explorer] -- [İşlemler |transactions] -- [İstisnalar |exceptions] -- [Yansıma |reflection] -- [Eşleme |mapping] -- [Yapılandırma |configuration] -- [Güvenlik Riskleri |security] -- [Yükseltme |en:upgrading] +- [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/configuration.texy b/database/tr/configuration.texy index eb6b6d98b6..85005e3216 100644 --- a/database/tr/configuration.texy +++ b/database/tr/configuration.texy @@ -1,16 +1,16 @@ -Veritabanı yapılandırması +Veritabanı Yapılandırması ************************* .[perex] -Nette Database için yapılandırma seçeneklerine genel bakış. +Nette Database'in yapılandırma seçeneklerine genel bakış. -Tüm framework'ü değil de yalnızca bu kütüphaneyi kullanıyorsanız, [yapılandırmanın nasıl yükleneceğini|bootstrap:] okuyun. +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 Bağlantı ------------ -Tek bir veritabanı bağlantısının yapılandırılması: +Tek bir veritabanı bağlantısını yapılandırın: ```neon database: @@ -20,29 +20,29 @@ database: password: ... ``` -Genellikle [otomatik kablolama |dependency-injection:autowiring] ile ilettiğimiz `Nette\Database\Connection` ve `Nette\Database\Explorer` servislerini oluşturur veya [adlarına |#DI Servisleri] bir referansla. +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: - # Tracy Bar'da veritabanı panelini göster? - debugger: ... # (bool) varsayılan true'dur + # veritabanı paneli Tracy Bar'da gösterilsin mi? + debugger: ... # (bool) Tracy etkinse varsayılan olarak açık - # Tracy Bar'da sorguların EXPLAIN'ini göster? - explain: ... # (bool) varsayılan true'dur + # sorgunun EXPLAIN çıktısı Tracy Bar'da gösterilsin mi? + explain: ... # (bool) varsayılan true - # Bu bağlantı için otomatik kablolamaya izin ver? - autowired: ... # (bool) ilk bağlantı için varsayılan true'dur + # bu bağlantı için autowiring açık olsun mu? + autowired: ... # (bool) ilk bağlantı için varsayılan true - # tablo kuralları: discovered, static veya sınıf adı + # 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? - lazy: ... # (bool) varsayılan false'dur + # veritabanına yalnızca gerektiğinde bağlanılsın mı? + lazy: ... # (bool) varsayılan false - # Veritabanı sürücüsü PHP sınıfı + # PHP veritabanı sürücüsü sınıfı driverClass: # (string) # yalnızca MySQL: sql_mode ayarlar @@ -51,17 +51,17 @@ database: # yalnızca MySQL: SET NAMES ayarlar charset: # (string) varsayılan 'utf8mb4' - # yalnızca MySQL: TINYINT(1)'i bool'a dönüştürür - convertBoolean: # (bool) varsayılan false'dur + # yalnızca MySQL: TINYINT(1) değerini bool'a dönüştürür + convertBoolean: # (bool) varsayılan false - # tarih içeren sütunları değişmez nesneler olarak döndürür (sürüm 3.2.1'den itibaren) - newDateTime: # (bool) varsayılan false'dur + # 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: tarih kaydetme formatı + # yalnızca Oracle ve SQLite: tarihi saklama biçimi formatDateTime: # (string) varsayılan 'U' ``` -`options` anahtarında, [PDO sürücü belgelerinde |https://www.php.net/manual/en/pdo.drivers.php] bulabileceğiniz diğer seçenekleri belirtebilirsiniz, örneğin: +`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: @@ -70,10 +70,10 @@ database: ``` -Çoklu bağlantılar ------------------ +Birden Çok Bağlantı +------------------- -Yapılandırmada, adlandırılmış bölümlere ayırarak birden fazla veritabanı bağlantısı da tanımlayabiliriz: +Yapılandırmada, adlandırılmış bölümlere ayırarak birden çok veritabanı bağlantısı tanımlayabiliriz: ```neon database: @@ -86,23 +86,23 @@ database: dsn: 'sqlite::memory:' ``` -Otomatik kablolama yalnızca ilk bölümdeki servisler için etkindir. Bu, `autowired: false` veya `autowired: true` kullanılarak değiştirilebilir. +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 ------------- -Bu servisler DI konteynerine eklenir, burada `###` bağlantı adını temsil eder: +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] +| 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` olacaktır. Yukarıdaki örnekte olduğu gibi birden fazla bağlantı tanımlarsak, adlar bölümlere karşılık gelir, yani `database.main.connection`, `database.main.explorer` ve ayrıca `database.another.connection` ve `database.another.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`. -Otomatik olarak kablolanmayan servisleri adlarına açık bir referansla iletiriz: +Autowiring'e girmeyen servisleri, adlarına başvurarak açıkça aktarırız: ```neon services: diff --git a/database/tr/exceptions.texy b/database/tr/exceptions.texy index ec399a0309..01d97a0ecb 100644 --- a/database/tr/exceptions.texy +++ b/database/tr/exceptions.texy @@ -1,22 +1,25 @@ İstisnalar ********** -Nette Database bir istisna hiyerarşisi kullanır. Temel sınıf `Nette\Database\DriverException`'dır, bu sınıf `PDOException`'dan miras alır ve veritabanı hatalarıyla çalışmak için genişletilmiş yetenekler sağlar: +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 hata kodunu döndürür -- `getSqlState()` metodu, SQLSTATE kodunu döndürür -- `getQueryString()` ve `getParameters()` metotları, orijinal sorguyu ve parametrelerini almanızı sağlar +- `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`'dan aşağıdaki özel istisnalar miras alır: +`DriverException` sınıfı şu özelleşmiş istisnalarla genişletilir: -- `ConnectionException` - veritabanı sunucusuna bağlantı hatasını belirtir -- `ConstraintViolationException` - veritabanı kısıtlamalarının ihlali için temel sınıf, bundan miras alanlar: - - `ForeignKeyConstraintViolationException` - yabancı anahtar ihlali - - `NotNullConstraintViolationException` - NOT NULL kısıtlaması ihlali - - `UniqueConstraintViolationException` - değer benzersizliği ihlali +- `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. - -Veritabanında zaten var olan bir e-postaya sahip bir kullanıcı eklemeye çalıştığımızda oluşan `UniqueConstraintViolationException` istisnasını yakalama örneği (e-posta sütununun benzersiz bir dizine sahip olduğu varsayılarak). +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 { @@ -26,7 +29,7 @@ try { 'password' => $hashedPassword, ]); } catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Bu e-posta adresine sahip bir kullanıcı zaten var.'; + 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 index 6fb3f323fa..506e757bf5 100644 --- a/database/tr/explorer.texy +++ b/database/tr/explorer.texy @@ -3,92 +3,92 @@ Database Explorer <div class=perex> -Explorer, veritabanıyla çalışmak için sezgisel ve etkili bir yol sunar. Tablolar arasındaki ilişkileri ve sorgu optimizasyonunu otomatik olarak halleder, böylece uygulamanıza odaklanabilirsiniz. Ayarlama yapmadan hemen çalışır. SQL sorguları üzerinde tam kontrol sahibi olmanız gerekiyorsa, [SQL yaklaşımını |SQL way] kullanabilirsiniz. +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 gerekli verileri yükleyen optimize edilmiş SQL sorguları oluşturur -- JOIN sorguları yazmaya gerek kalmadan ilgili verilere kolay erişim sağlar -- Herhangi bir yapılandırma veya varlık oluşturma olmadan anında çalışı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 [api:Nette\Database\Explorer] nesnesinin `table()` metodunu çağırarak başlarsınız (bağlantı ayrıntıları [Bağlantı ve yapılandırma |guide#Bağlantı ve Yapılandırma] bölümünde bulunabilir): +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' tablo adıdır +$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 ek metotlar zincirleyebiliriz. Sorgu, veri talep etmeye başladığımızda oluşturulur ve yürütülür. Örneğin, bir `foreach` döngüsüyle. Her satır bir [ActiveRow |api:Nette\Database\Table\ActiveRow] nesnesiyle temsil edilir: +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 yazdır - echo $book->author_id; // 'author_id' sütununu yazdır + echo $book->title; // 'title' sütununu çıktılar + echo $book->author_id; // 'author_id' sütununu çıktılar } ``` -Explorer, [tablolar arasındaki ilişkilerle |#Tablolar arasındaki ilişkiler] çalışmayı önemli ölçüde kolaylaştırır. Aşağıdaki örnek, ilişkili tablolardan (kitaplar ve yazarları) verilerin ne kadar kolay listelenebileceğini gösterir. Herhangi bir JOIN sorgusu yazmamıza gerek olmadığına dikkat edin, Nette bunları bizim için oluşturur: +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 JOIN oluşturur + echo 'Yazar: ' . $book->author->name; // 'author' tablosuna bir JOIN oluşturur } ``` -Nette Database Explorer, sorguları mümkün olduğunca verimli olacak şekilde optimize eder. Yukarıdaki örnek, 10 veya 10.000 kitap işliyor olsak da yalnızca iki SELECT sorgusu gerçekleştirir. +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. -Ek olarak, Explorer kodda hangi sütunların kullanıldığını izler ve veritabanından yalnızca bunları yükleyerek daha fazla performans tasarrufu sağlar. Bu davranış tamamen otomatik ve uyarlanabilirdir. Daha sonra kodu değiştirir ve ek sütunlar kullanmaya başlarsanız, Explorer sorguları otomatik olarak ayarlar. Hiçbir şey ayarlamanıza veya hangi sütunlara ihtiyacınız olacağını düşünmenize gerek yok - bırakın Nette halletsin. +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 +Filtreleme ve Sıralama ====================== -`Selection` sınıfı, veri seçimini filtrelemek ve sıralamak için metotlar sağlar. +`Selection` sınıfı, veri seçimlerini filtrelemek ve sıralamak için metotlar sunar. .[language-php] -| `where($condition, ...$params)` | WHERE koşulu ekler. Birden çok koşul AND operatörü ile birleştirilir -| `whereOr(array $conditions)` | OR operatörü ile birleştirilmiş bir WHERE koşulları grubu ekler -| `wherePrimary($value)` | Birincil anahtara göre WHERE koşulu ekler -| `order($columns, ...$params)` | ORDER BY sıralamasını ayarlar -| `select($columns, ...$params)` | Yüklenecek sütunları belirtir -| `limit($limit, $offset = null)` | Satır sayısını sınırlar (LIMIT) ve isteğe bağlı olarak OFFSET ayarlar -| `page($page, $itemsPerPage, &$total = null)` | Sayfalamayı ayarlar -| `group($columns, ...$params)` | Satırları gruplar (GROUP BY) -| `having($condition, ...$params)` | Gruplanmış satırları filtrelemek için HAVING koşulu ekler +| `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 (sözde [akıcı arayüz |nette:introduction-to-object-oriented-programming#Akıcı Arayüzler Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. +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 tablolardaki verilere |#İlişkili tablolar üzerinden sorgulama] erişmek için özel bir gösterim de kullanabilirsiniz. +Bu metotlarda, [ilişkili tablolardan gelen verilere |#İlişkili Tablolar Üzerinden Sorgulama] erişmek için özel yazımlar da kullanabilirsiniz. -Kaçış ve tanımlayıcılar ------------------------ +Kaçışlama ve Tanımlayıcılar +--------------------------- -Metotlar parametreleri otomatik olarak kaçar ve tanımlayıcıları (tablo ve sütun adları) tırnak içine alır, böylece SQL injection'u önler. Doğru çalışması için birkaç kurala uymak gerekir: +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 kelimeleri, fonksiyon adlarını, prosedürleri vb. **büyük harflerle** yazın. -- Sütun ve tablo adlarını **küçük harflerle** yazın. -- Karakter dizilerini her zaman **parametreler** aracılığıyla ekleyin. +- 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 GÜVENLİK AÇIĞI: SQL injection -where('name LIKE "%search%"'); // YANLIŞ: otomatik tırnak içine almayı zorlaştırır -where('name LIKE ?', '%search%'); // DOĞRU: parametre aracılığıyla eklenen değer +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Ş: `name` `like` ? oluşturur -where('name LIKE ?', $name); // DOĞRU: `name` LIKE ? oluşturur -where('LOWER(name) = ?', $value);// DOĞRU: LOWER(`name`) = ? oluşturur +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üçlü yanı, farklı değer tipleriyle akıllıca çalışması ve SQL operatörlerini otomatik olarak seçmesidir. +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: @@ -98,26 +98,26 @@ $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 olarak algılanması sayesinde, farklı özel durumlarla uğraşmamıza gerek kalmaz. Nette bunları bizim için halleder: +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ör olmadan yer tutucu soru işareti de kullanılabilir: +// Operatörsüz ? yer tutucusunu da kullanabilirsiniz: $table->where('id ?', 1); // WHERE `id` = 1 ``` -Metot, negatif koşulları ve boş dizileri de doğru şekilde işler: +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); Dikkat - bu sözdizimi desteklenmiyor +// $table->where('NOT id ?', $ids); // UYARI: Bu söz dizimi desteklenmiyor ``` -Parametre olarak başka bir tablodan sonuç da iletebiliriz - bir alt sorgu oluşturulur: +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`) @@ -127,7 +127,7 @@ $table->where('id', $explorer->table($tableName)); $table->where('id', $explorer->table($tableName)->select('col')); ``` -Koşulları, öğeleri AND ile birleştirilecek bir dizi olarak da iletebiliriz: +Koşullar, öğeleri AND ile birleştirilen bir dizi olarak da aktarılabilir: ```php // WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) @@ -137,7 +137,7 @@ $table->where([ ]); ``` -Dizide anahtar => değer çiftleri kullanabiliriz ve Nette yine doğru operatörleri otomatik olarak seçer: +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)) @@ -147,23 +147,23 @@ $table->where([ ]); ``` -Dizide, yer tutucu soru işaretleri ve birden çok parametre içeren SQL ifadelerini birleştirebiliriz. Bu, tam olarak tanımlanmış operatörlere sahip karmaşık koşullar için uygundur: +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 parametreyi dizi olarak iletiriz + 'ROUND(score, ?) > ?' => [2, 75.5], // iki parametre dizi olarak aktarılır ]); ``` -Birden çok `where()` çağrısı, koşulları otomatik olarak AND ile birleştirir. +`where()` metodunun birden çok çağrısı, koşulları otomatik olarak AND ile birleştirir. whereOr(array $parameters): static .[method] -------------------------------------------- -`where()` gibi koşullar ekler, ancak farkı bunları OR kullanarak birleştirmesidir: +`where()` metoduna benzer şekilde koşul ekler, ama onları OR ile birleştirir: ```php // WHERE (`status` = 'active') OR (`deleted` = 1) @@ -173,7 +173,7 @@ $table->whereOr([ ]); ``` -Burada da daha karmaşık ifadeler kullanabiliriz: +Burada daha karmaşık ifadeler de kullanılabilir: ```php // WHERE (`price` > 1000) OR (`price_with_tax` > 1500) @@ -197,7 +197,7 @@ $table->wherePrimary(123); $table->wherePrimary([1, 2, 3]); ``` -Tablonun bileşik bir birincil anahtarı varsa (örneğin `foo_id`, `bar_id`), bunu bir dizi olarak iletiriz: +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 @@ -214,7 +214,7 @@ $table->wherePrimary([ order(string $columns, ...$parameters): static .[method] -------------------------------------------------------- -Satırların döndürüleceği sırayı belirler. Bir veya daha fazla sütuna göre, azalan veya artan sırada veya özel bir ifadeye göre sıralayabiliriz: +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` @@ -227,14 +227,14 @@ $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. Varsayılan olarak, Nette Database Explorer yalnızca kodda gerçekten kullanılan sütunları döndürür. Bu nedenle `select()` metodunu, belirli ifadeleri döndürmemiz gereken durumlarda kullanırız: +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 adlar daha sonra ActiveRow nesnesinin özellikleri olarak kullanılabilir: +`AS` ile tanımlanan takma adlara sonra `ActiveRow` nesnesinin özellikleri olarak erişilir: ```php foreach ($table as $row) { @@ -246,7 +246,7 @@ foreach ($table as $row) { 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 ofset ayarlamaya izin verir: +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) @@ -259,12 +259,12 @@ 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şlayarak sayılır) ve sayfa başına öğe sayısını kabul eder. İsteğe bağlı olarak, toplam sayfa sayısının saklanacağı bir değişkene referans iletilebilir: +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); -echo "Toplam sayfa sayısı: $numOfPages"; +$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); +echo "Total pages: $numOfPages"; ``` @@ -293,31 +293,31 @@ $table->select('category_id, COUNT(*) AS count') ``` -Veri okuma +Veri Okuma ========== -Veritabanından veri okumak için birkaç kullanışlı metodumuz var: +Veritabanından veri okumak için çeşitli kullanışlı metotlar vardır: .[language-php] -| `foreach ($table as $key => $row)` | Tüm satırlar üzerinde yinelenir, `$key` birincil anahtar değeridir, `$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 bir sonrakine taşır -| `$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 +| `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 yalnızca okuma amaçlıdır. Bu, özelliklerinin değerlerini değiştiremeyeceğiniz anlamına gelir. Bu kısıtlama, veri tutarlılığını sağlar ve beklenmedik yan etkileri önler. Veriler veritabanından yüklenir ve herhangi bir değişiklik açıkça ve kontrollü bir şekilde yapılmalıdı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 üzerinde yineleme ------------------------------------------- +`foreach` - Tüm Satırları Dolaşma +--------------------------------- -Bir sorguyu yürütmenin ve satırları almanın en kolay yolu, bir `foreach` döngüsünde yinelemektir. SQL sorgusunu otomatik olarak çalıştırır. +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ğeridir, $book ActiveRow'dur + // $key birincil anahtar değeri, $book bir ActiveRow echo "$book->title ({$book->author->name})"; } ``` @@ -326,10 +326,10 @@ foreach ($books as $key => $book) { get($key): ?ActiveRow .[method] ------------------------------- -SQL sorgusunu yürütür ve birincil anahtara göre satırı veya mevcut değilse `null` döndürür. +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 123 olan ActiveRow'u veya null döndürür +$book = $explorer->table('book')->get(123); // ID'si 123 olan ActiveRow'u ya da null döndürür if ($book) { echo $book->title; } @@ -339,7 +339,7 @@ if ($book) { fetch(): ?ActiveRow .[method] ----------------------------- -Bir satır döndürür ve dahili işaretçiyi bir sonrakine taşır. Başka satır yoksa `null` döndürür. +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'); @@ -352,21 +352,21 @@ while ($book = $books->fetch()) { 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ütun adını belirtir, ikinci argüman değer olarak kullanılacak sütun adını belirtir: +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 parametreyi belirtirsek, değer tüm satır, yani `ActiveRow` nesnesi olacaktır: +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, ...), ...] ``` -Yinelenen anahtarlar durumunda, son satırdan gelen değer kullanılır. Anahtar olarak `null` kullanıldığında, dizi sıfırdan başlayarak sayısal olarak dizine eklenir (o zaman çakışma olmaz): +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'); @@ -377,24 +377,24 @@ $authors = $explorer->table('author')->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternatif olarak, parametre olarak her satır için ya değerin kendisini ya da bir anahtar-değer çiftini döndürecek bir geri arama (callback) belirtebilirsiniz. +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})"); -// ['İlk kitap (Jan Novák)', ...] +// ['First Book (John Novak)', ...] -// Geri arama ayrıca bir anahtar & değer çifti içeren bir dizi de döndürebilir: +// 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]); -// ['İlk kitap' => 'Jan Novák', ...] +// ['First Book' => 'John Novak', ...] ``` fetchAll(): array .[method] --------------------------- -Tüm satırları, anahtarların birincil anahtar değerleri olduğu `ActiveRow` nesnelerinin ilişkisel bir dizisi olarak döndürür. +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(); @@ -413,51 +413,51 @@ $count = $table->count(); $count = count($table); // alternatif ``` -Dikkat, parametreli `count()` veritabanında COUNT toplama fonksiyonunu gerçekleştirir, aşağıya bakın. +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ı ve değerlerin karşılık gelen veriler olduğu ilişkisel bir diziye dönüştürür. +`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 ['id' => 1, 'title' => '...', 'author_id' => ..., ...] olacaktır +// $bookArray şu olur: ['id' => 1, 'title' => '...', 'author_id' => ..., ...] ``` Toplama ======= -`Selection` sınıfı, toplama fonksiyonlarını (COUNT, SUM, MIN, MAX, AVG vb.) kolayca gerçekleştirmek için metotlar sağlar. +`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)` | Sütundaki minimum değeri döndürür -| `max($expr)` | Sütundaki maksimum değeri döndürür -| `sum($expr)` | Sütundaki değerlerin toplamını döndürür -| `aggregation($function)` | Herhangi bir toplama fonksiyonunun gerçekleştirilmesini sağlar. Örn. `AVG()`, `GROUP_CONCAT()` +| `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 gerçekleştirir ve sonucu döndürür. Metot, belirli bir koşula kaç satırın karşılık geldiğini bulmak için kullanılır: +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` ``` -Dikkat, [#count()] parametresiz yalnızca `Selection` nesnesindeki satır sayısını döndürür. +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ütun veya ifadedeki minimum ve maksimum değeri döndürür: +`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 @@ -466,10 +466,10 @@ $maxPrice = $products->where('active', true) ``` -sum(string $expr) .[method] ---------------------------- +sum(string $expr): mixed .[method] +---------------------------------- -Belirtilen sütun veya ifadedeki değerlerin toplamını döndürür: +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 @@ -478,39 +478,39 @@ $totalPrice = $products->where('active', true) ``` -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- +aggregation(string $function, ?string $groupFunction = null): mixed .[method] +----------------------------------------------------------------------------- -Herhangi bir toplama fonksiyonunun gerçekleştirilmesini sağlar. +Herhangi bir toplama fonksiyonunu çalıştırmaya olanak tanır. ```php -// kategorideki ürünlerin ortalama fiyatı +// bir kategorideki ürünlerin ortalama fiyatı $avgPrice = $products->where('category_id', 1) ->aggregation('AVG(price)'); -// ürün etiketlerini tek bir karakter dizisinde birleştirir +// ü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 fonksiyonu ve gruplamadan (örneğin, gruplanmış satırlar üzerinde `SUM(değer)`) kaynaklanan sonuçları toplamamız gerekiyorsa, ikinci argüman olarak bu ara sonuçlara uygulanacak toplama fonksiyonunu belirtiriz: +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 -// Her kategori için stoktaki ürünlerin toplam fiyatını hesaplar ve ardından bu fiyatları toplar. +// 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ı hesaplarız (`SUM(price * stock) AS category_total`) ve sonuçları `category_id`'ye göre gruplarız. Ardından, bu ara toplamları `category_total` toplamak için `aggregation('SUM(category_total)', 'SUM')` kullanırız. İkinci argüman `'SUM'`, ara sonuçlara SUM fonksiyonunun uygulanması gerektiğini söyler. +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. -Ekleme, Güncelleme ve Silme -=========================== +Insert, Update ve Delete +======================== -Nette Database Explorer, veri eklemeyi, güncellemeyi ve silmeyi basitleştirir. Belirtilen tüm metotlar, bir hata durumunda `Nette\Database\DriverException` istisnası fırlatır. +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] @@ -520,24 +520,24 @@ Tabloya yeni kayıtlar ekler. **Tek bir kayıt ekleme:** -Yeni kaydı, anahtarların tablodaki sütun adlarına karşılık geldiği ilişkisel bir dizi veya yinelenebilir bir nesne (örneğin, [formlarda |forms:] kullanılan ArrayHash) olarak iletiriz. +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ımlanmış bir birincil anahtarı varsa, metot veritabanı düzeyinde yapılan olası değişiklikleri (tetikleyiciler, varsayılan sütun değerleri, otomatik artan sütun hesaplamaları) yansıtmak için veritabanından yeniden yüklenen bir `ActiveRow` nesnesi döndürür. Bu, veri tutarlılığını sağlar ve nesne her zaman veritabanındaki güncel verileri içerir. Benzersiz bir birincil anahtarı yoksa, iletilen verileri dizi biçiminde döndürür. +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, ActiveRow örneğidir ve eklenen satırın tam verilerini içerir, -// otomatik olarak oluşturulan ID ve tetikleyiciler tarafından yapılan olası değişiklikler dahil -echo $row->id; // Yeni eklenen kullanıcının ID'sini yazdırır -echo $row->created_at; // Tetikleyici tarafından ayarlandıysa oluşturma zamanını yazdırır +// $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 ``` -**Aynı anda birden çok kayıt ekleme:** +**Birden çok kaydı tek seferde ekleme:** -`insert()` metodu, tek bir SQL sorgusu kullanarak birden çok kayıt eklemeye izin verir. Bu durumda, eklenen satır sayısını döndürür. +`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([ @@ -551,10 +551,10 @@ $insertedRows = $explorer->table('users')->insert([ ], ]); // INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows 2 olacaktır +// $insertedRows değeri 2 olur ``` -Parametre olarak, veri seçimi içeren bir `Selection` nesnesi de iletilebilir. +Parametre olarak, veri seçimi içeren bir `Selection` nesnesi de aktarılabilir. ```php $newUsers = $explorer->table('potential_users') @@ -566,12 +566,12 @@ $insertedRows = $explorer->table('users')->insert($newUsers); **Özel değerler ekleme:** -Değer olarak dosyaları, DateTime nesnelerini veya SQL değişmezlerini de iletebiliriz: +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ı formatına dönüştürür + '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 ]); @@ -581,9 +581,9 @@ $explorer->table('users')->insert([ Selection::update(iterable $data): int .[method] ------------------------------------------------ -Belirtilen filtreye göre tablodaki satırları günceller. Gerçekte değiştirilen satır sayısını döndürür. +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ın tablodaki sütun adlarına karşılık geldiği ilişkisel bir dizi veya yinelenebilir bir nesne (örneğin, [formlarda |forms:] kullanılan ArrayHash) olarak iletiriz: +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') @@ -595,7 +595,7 @@ $affected = $explorer->table('users') // UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 ``` -Sayısal değerleri değiştirmek için `+=` ve `-=` operatörlerini kullanabiliriz: +Sayısal değerleri değiştirmek için `+=` ve `-=` operatörlerini kullanabilirsiniz: ```php $explorer->table('users') @@ -611,7 +611,7 @@ $explorer->table('users') Selection::delete(): int .[method] ---------------------------------- -Belirtilen filtreye göre tablodan satırları siler. Silinen satır sayısını döndürür. +Tablodaki satırları belirtilen filtreye göre siler. Silinen satır sayısını döndürür. ```php $count = $explorer->table('users') @@ -621,77 +621,77 @@ $count = $explorer->table('users') ``` .[caution] -`update()` ve `delete()` çağırırken, `where()` kullanarak değiştirilecek/silinecek satırları belirtmeyi unutmayın. `where()` kullanmazsanız, işlem tüm tablo üzerinde gerçekleştirilir! +`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` nesnesi tarafından temsil edilen veritabanı satırındaki verileri günceller. Parametre olarak, güncellenecek verileri içeren yinelenebilir bir değer alır (anahtarlar sütun adlarıdır). Sayısal değerleri değiştirmek için `+=` ve `-=` operatörlerini kullanabiliriz: +`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 olası değişiklikleri (örneğin tetikleyiciler) yansıtmak için otomatik olarak veritabanından yeniden yüklenir. Metot, yalnızca verilerde gerçek bir değişiklik yapıldıysa true döndürür. +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ız + 'views += 1', // görüntülenme sayısını artırır ]); -echo $article->views; // Geçerli görüntülenme sayısını yazdırır +echo $article->views; // Geçerli görüntülenme sayısını çıktılar ``` -Bu metot, veritabanındaki yalnızca belirli bir satırı günceller. Birden çok satırı toplu olarak güncellemek için [#Selection::update()] metodunu kullanın. +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() .[method] ------------------------------ +ActiveRow::delete(): int .[method] +---------------------------------- -`ActiveRow` nesnesi tarafından temsil edilen satırı veritabanından siler. +`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 1 olan kitabı siler +$book->delete(); // ID'si 1 olan kitabı siler ``` -Bu metot, veritabanındaki yalnızca belirli bir satırı siler. Birden çok satırı toplu olarak silmek için [#Selection::delete()] metodunu kullanın. +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ındaki ilişkiler -============================= +Tablolar Arası İlişkiler +======================== -İlişkisel veritabanlarında, veriler birden çok tabloya bölünür ve yabancı anahtarlar kullanılarak birbirine bağlanır. Nette Database Explorer, bu ilişkilerle çalışmak için devrim niteliğinde bir yol sunar - JOIN sorguları yazmadan ve herhangi bir şeyi yapılandırmaya veya oluşturmaya gerek kalmadan. +İ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 bir kitap veritabanı örneği kullanacağız ([GitHub'da bulabilirsiniz |https://github.com/nette-examples/books]). Veritabanında şu tablolarımız var: +İ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 (sütunlar `id`, `name`, `web`, `born`) -- `book` - kitaplar (sütunlar `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - etiketler (sütunlar `id`, `name`) -- `book_tag` - kitaplar ve etiketler arasındaki ilişki tablosu (sütunlar `book_id`, `tag_id`) +- `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 *] *** Veritabanı yapısı .<> +[* db-schema-1-.webp *] *** Örneklerde kullanılan veritabanı yapısı .<> -Kitap veritabanı örneğimizde, birkaç ilişki tipi buluruz (model gerçekliğe göre basitleştirilmiş olsa da): +Ö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 **birkaç** kitap yazabilir -- Sıfıra çok 0:N – kitabın bir çevirmeni **olabilir**, bir çevirmen **birkaç** kitap çevirebilir -- Sıfıra bir 0:1 – kitabın bir devamı **olabilir** -- Çoka çok M:N – kitabın **birkaç** etiketi olabilir ve bir etiket **birkaç** kitaba atanabilir +- **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 ve bir alt tablo bulunur. Örneğin, yazar ve kitap arasındaki ilişkide, `author` tablosu üst, `book` tablosu alttır - bir kitabın her zaman bir yazara "ait olduğunu" düşünebiliriz. Bu, veritabanı yapısında da kendini gösterir: alt `book` tablosu, üst `author` tablosuna başvuran `author_id` yabancı anahtarını içerir. +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. -Yazarlarının adları da dahil olmak üzere kitapları listelememiz gerekiyorsa, iki seçeneğimiz vardır. Ya verileri JOIN kullanarak tek bir SQL sorgusuyla alırız: +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 +SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id; ``` -Ya da verileri iki adımda yükleriz - önce kitapları, sonra yazarlarını - ve sonra bunları PHP'de birleştiririz: +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); -- alınan kitapların yazar kimlikleri +SELECT * FROM author WHERE id IN (1, 2, 3); -- seçilen kitapların yazar ID'leri ``` -İkinci yaklaşım, şaşırtıcı olsa da aslında daha verimlidir. Veriler yalnızca bir kez yüklenir ve önbellekte daha iyi kullanılabilir. Nette Database Explorer tam olarak bu şekilde çalışır - her şeyi perde arkasında halleder ve size zarif bir API sunar: +İ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'); @@ -703,29 +703,29 @@ foreach ($books as $book) { ``` -Üst tabloya erişim +Üst Tabloya Erişim ------------------ -Üst tabloya erişim basittir. Bunlar *kitabın bir yazarı var* veya *kitabın bir çevirmeni olabilir* gibi ilişkilerdir. İlgili kaydı ActiveRow nesnesinin özelliği aracılığıyla alırız - adı, `id` olmadan yabancı anahtar sütununun adına karşılık gelir: +Ü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'ye göre çevirmeni bulur +echo $book->translator?->name; // translator_id sütununa göre çevirmeni bulur ``` -`$book->author` özelliğine eriştiğimizde, Explorer `book` tablosunda adı `author` karakter dizisini içeren bir sütun arar (yani `author_id`). Bu sütundaki değere göre, `author` tablosundan karşılık gelen kaydı yükler ve `ActiveRow` olarak döndürür. Benzer şekilde, `translator_id` sütununu kullanan `$book->translator` da çalışır. `translator_id` sütunu `null` içerebileceğinden, kodda `?->` operatörünü kullanırız. +`$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. -Alternatif bir yol, iki argüman alan `ref()` metodudur: hedef tablo adı ve birleştirme sütunu adı ve bir `ActiveRow` örneği veya `null` döndürür: +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; // yazara bağlantı -echo $book->ref('author', 'translator_id')->name; // çevirmene bağlantı +echo $book->ref('author', 'author_id')->name; // yazarla ilişki +echo $book->ref('author', 'translator_id')->name; // çevirmenle ilişki ``` -`ref()` metodu, tablo aynı ada sahip bir sütun içerdiği için (yani `author`) özellik üzerinden erişim kullanılamadığında kullanışlıdır. Diğer durumlarda, daha okunabilir olan özellik üzerinden erişim kullanılması önerilir. +`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 optimize eder. Bir döngüde kitapları dolaşırken ve ilgili kayıtlarına (yazarlar, çevirmenler) erişirken, Explorer her kitap için ayrı bir sorgu oluşturmaz. Bunun yerine, her ilişki tipi için yalnızca bir SELECT gerçekleştirir, bu da veritabanı yükünü önemli ölçüde azaltır. Örneğin: +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'); @@ -736,121 +736,121 @@ foreach ($books as $book) { } ``` -Bu kod, veritabanına yalnızca şu üç yıldırım hızında sorguyu çağırır: +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ütunundan kimlikler -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- seçilen kitapların translator_id sütunundan kimlikler +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ığı, [Conventions |api:Nette\Database\Conventions] uygulaması tarafından belirlenir. Yabancı anahtarları analiz eden ve mevcut tablo ilişkileriyle kolayca çalışmanıza olanak tanıyan [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions] kullanmanızı öneririz. +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 ------------------ -Alt tabloya erişim ters yönde çalışır. Şimdi *bu yazar hangi kitapları yazdı* veya *bu çevirmen hangi kitapları çevirdi* diye soruyoruz. Bu tür bir sorgu için, ilgili kayıtlarla bir `Selection` döndüren `related()` metodunu kullanırız. Bir örneğe bakalım: +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ı yazdırır +// 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ı yazdırır +// Yazarın çevirdiği tüm kitapları çıktılar foreach ($author->related('book.translator_id') as $book) { echo "Çevirdi: $book->title"; } ``` -`related()` metodu, birleştirmeyi nokta gösterimiyle tek bir argüman olarak veya iki ayrı argüman olarak kabul eder: +`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, üst tablonun adına göre doğru birleştirme sütununu otomatik olarak algılayabilir. Bu durumda, kaynak tablonun adı `author` olduğu için `book.author_id` sütunu üzerinden birleştirme yapılır: +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 fazla olası bağlantı varsa, Explorer bir [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] istisnası fırlatı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 ve Explorer bu durumda da sorguları otomatik olarak optimize eder: +`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 . ' yazdı:'; + echo $author->name . ' şunları yazdı:'; foreach ($author->related('book') as $book) { echo $book->title; } } ``` -Bu kod yalnızca iki yıldırım hızında SQL sorgusu oluşturur: +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 kimlikleri +SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- seçilen yazarların ID'leri ``` -Çoka çok ilişki +Çoka Çok İlişki --------------- -Çoka çok (M:N) ilişkisi için bir ilişki tablosunun (bizim durumumuzda `book_tag`) varlığı gereklidir, bu tablo iki yabancı anahtar sütunu (`book_id`, `tag_id`) içerir. Bu sütunların her biri, birleştirilen tablolardan birinin birincil anahtarına başvurur. İlgili verileri almak için önce `related('book_tag')` kullanarak ilişki tablosundan kayıtları alırız ve ardından hedef verilere devam ederiz: +Ç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ı yazdırır +// kitaba atanan etiketlerin adlarını çıktılar foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // ilişki tablosu üzerinden etiketin adını yazdırır + echo $bookTag->tag->name; // etiket adını bağlantı tablosu üzerinden çıktılar } $tag = $explorer->table('tag')->get(1); -// veya tersi: bu etiketle işaretlenmiş kitapların adlarını yazdırır +// 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 adını yazdırır + echo $bookTag->book->title; // kitabın başlığını çıktılar } ``` -Explorer yine SQL sorgularını verimli bir forma optimize eder: +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 kimlikleri -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- book_tag'de bulunan etiketlerin kimlikleri +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 +İlişkili Tablolar Üzerinden Sorgulama ------------------------------------- -`where()`, `select()`, `order()` ve `group()` metotlarında, diğer tablolardaki sütunlara erişmek için özel gösterimler kullanabiliriz. Explorer gerekli JOIN'leri otomatik olarak oluşturur. +`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 gösterimi** (`üst_tablo.sütun`), alt tablo açısından 1:N ilişkisi için kullanılır: +**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ı 'Jon' ile başlayan kitapları bulur +// 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'); -// Kitap adını ve yazar adını yazdırır +// Kitabın başlığını ve yazarın adını çıktılar $books->select('book.title, author.name'); ``` -**İki nokta üst üste gösterimi** (`:alt_tablo.sütun`), üst tablo açısından 1:N ilişkisi için kullanılır: +**İ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'); @@ -858,12 +858,12 @@ $authors = $explorer->table('author'); // Başlığında 'PHP' geçen bir kitap yazan yazarları bulur $authors->where(':book.title LIKE ?', '%PHP%'); -// Her yazar için kitap sayısını sayar +// Her yazarın kitap sayısını sayar $authors->select('*, COUNT(:book.id) AS book_count') ->group('author.id'); ``` -Yukarıdaki örnekte iki nokta üst üste gösterimi (`:book.title`) ile yabancı anahtar sütunu belirtilmemiştir. Explorer, üst tablonun adına göre doğru sütunu otomatik olarak algılar. Bu durumda, kaynak tablonun adı `author` olduğu için `book.author_id` sütunu üzerinden birleştirme yapılır. Birden fazla olası bağlantı varsa, Explorer bir [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] istisnası fırlatır. +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: @@ -872,32 +872,32 @@ Birleştirme sütunu parantez içinde açıkça belirtilebilir: $authors->where(':book(translator_id).title LIKE ?', '%PHP%'); ``` -Gösterimler, birden çok tablo üzerinden erişim için zincirlenebilir: +Yazımlar, birden çok tabloya yayılan veriye erişmek için zincirlenebilir: ```php -// 'PHP' etiketiyle işaretlenmiş kitapların yazarlarını bulur +// 'PHP' etiketli kitapların yazarlarını bulur $authors->where(':book:book_tag.tag.name', 'PHP') ->group('author.id'); ``` -JOIN koşullarını genişletme +JOIN Koşullarını Genişletme --------------------------- -`joinWhere()` metodu, SQL'de tabloları birleştirirken `ON` anahtar kelimesinden sonra belirtilen koşulları genişletir. +`joinWhere()` metodu, SQL'de tabloları birleştirirken `ON` anahtar sözcüğünden sonra belirtilen koşulları genişletir. -Belirli bir çevirmen tarafından çevrilen kitapları bulmak istediğimizi varsayalım: +Diyelim ki belirli bir çevirmenin çevirdiği kitapları bulmak istiyoruz: ```php -// 'David' adlı çevirmen tarafından çevrilen kitapları bulur +// '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()` metodunda olduğu gibi aynı yapıları kullanabiliriz - operatörler, yer tutucu soru işaretleri, değer dizileri veya SQL ifadeleri. +`joinWhere()` koşulunda, `where()` metodundakiyle aynı yapıları kullanabilirsiniz: operatörler, yer tutucular, değer dizileri ya da SQL ifadeleri. -Daha karmaşık, birden çok JOIN içeren sorgular için tablo takma adları tanımlayabiliriz: +Birden çok JOIN içeren daha karmaşık sorgularda tablo takma adları tanımlayabilirsiniz: ```php $tags = $explorer->table('tag') @@ -909,4 +909,4 @@ $tags = $explorer->table('tag') // AND (`book_author`.`born` < 1950) ``` -`where()` metodunun koşulları `WHERE` yan tümcesine eklerken, `joinWhere()` metodunun tabloları birleştirirken `ON` yan tümcesindeki koşulları genişlettiğine dikkat edin. +`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 index cf0dd5e69e..5b6a06755e 100644 --- a/database/tr/guide.texy +++ b/database/tr/guide.texy @@ -2,78 +2,78 @@ Nette Database ************** .[perex] -Nette Database, basitliğe ve akıllı özelliklere odaklanan, PHP için güçlü ve zarif bir veritabanı katmanıdır. Veritabanıyla çalışmak için iki yol sunar - hızlı uygulama geliştirme için [Explorer |Explorer] veya sorgularla doğrudan çalışmak için [SQL yaklaşımı |SQL way]. +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 yaklaşımı |SQL way] -======================== -- Güvenli parametreli sorgular -- SQL sorgularının şekli üzerinde hassas kontrol -- Gelişmiş özelliklere sahip karmaşık sorgular yazdığınızda -- Belirli SQL fonksiyonlarını kullanarak performansı optimize ettiğinizde +[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] +[Explorer |explorer] ==================== -- SQL yazmadan hızlı geliştirme yaparsınız -- Tablolar arasındaki ilişkilerle sezgisel çalışma -- Otomatik sorgu optimizasyonunu takdir edersiniz -- Veritabanıyla hızlı ve rahat çalışmak için uygundur +- 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 / Yükleme -================= +Kurulum +======= -Kütüphaneyi [Composer |best-practices:composer] aracıyla indirip kurabilirsiniz: +Kütüphaneyi [Composer|best-practices:composer] ile indirip kurun: ```shell composer require nette/database ``` -Desteklenen veritabanları +Desteklenen Veritabanları ========================= -Nette Database aşağıdaki veritabanlarını destekler: +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 | - -| MS SQL (PDO_SQLSRV) | sqlsrv | EVET -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - +|* 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ına iki yaklaşım -========================= +Veritabanı Çalışmasına İki Yaklaşım +=================================== -Nette Database size bir seçenek sunar: SQL sorgularını doğrudan yazabilir (SQL yaklaşımı) veya otomatik olarak oluşturulmalarını sağlayabilirsiniz (Explorer). Her iki yaklaşımın da aynı görevleri nasıl çözdüğüne bakalı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 yaklaşımı |sql way] - SQL sorguları +[SQL yolu|sql-way] - SQL sorguları ```php -// kayıt ekleme +// Kayıt ekleme $database->query('INSERT INTO books', [ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// kayıtları alma: kitap yazarları +// Kayıtları alma: kitap yazarları $result = $database->query(' SELECT authors.*, COUNT(books.id) AS books_count FROM authors @@ -82,7 +82,7 @@ $result = $database->query(' GROUP BY authors.id '); -// çıktı (optimal değil, N tane daha sorgu üretir) +// Gösterim (ideal değil, N ek sorgu üretir) foreach ($result as $author) { $books = $database->query(' SELECT * FROM books @@ -90,7 +90,7 @@ foreach ($result as $author) { ORDER BY published_at DESC ', $author->id); - echo "Yazar $author->name, $author->books_count kitap yazdı:\n"; + echo "$author->name adlı yazar $author->books_count kitap yazmış:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -98,26 +98,26 @@ foreach ($result as $author) { } ``` -[Explorer yaklaşımı |explorer] - otomatik SQL oluşturma +[Explorer yolu|explorer] - otomatik SQL üretimi ```php -// kayıt ekleme +// Kayıt ekleme $database->table('books')->insert([ 'author_id' => $authorId, 'title' => $bookData->title, 'published_at' => new DateTime, ]); -// kayıtları alma: kitap yazarları +// Kayıtları alma: kitap yazarları $authors = $database->table('authors') ->where('active', 1); -// çıktı (otomatik olarak sadece 2 optimize edilmiş sorgu üretir) +// Gösterim (otomatik olarak yalnızca 2 iyileştirilmiş sorgu üretir) foreach ($authors as $author) { $books = $author->related('books') ->order('published_at DESC'); - echo "Yazar $author->name, {$books->count()} kitap yazdı:\n"; + echo "$author->name adlı yazar {$books->count()} kitap yazmış:\n"; foreach ($books as $book) { echo "- $book->title\n"; @@ -125,23 +125,23 @@ foreach ($authors as $author) { } ``` -Explorer yaklaşımı SQL sorgularını otomatik olarak oluşturur ve optimize eder. Yukarıdaki örnekte, SQL yaklaşımı N+1 sorgu üretir (biri yazarlar için ve sonra her yazarın kitapları için bir tane), Explorer ise sorguları otomatik olarak optimize eder ve yalnızca iki tane yürütür - biri yazarlar için ve biri tüm kitapları için. +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. -Her iki yaklaşım da uygulamada ihtiyaca göre serbestçe birleştirilebilir. +İki yaklaşım uygulamanızda ihtiyaca göre serbestçe birleştirilebilir. Bağlantı ve Yapılandırma ======================== -Veritabanına bağlanmak için [api:Nette\Database\Connection] sınıfının bir örneğini oluşturmanız yeterlidir: +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` (veri kaynağı adı) parametresi, [PDO'nun kullandığı |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters] ile aynıdır, örn. `host=127.0.0.1;dbname=test`. Başarısızlık durumunda `Nette\Database\ConnectionException` istisnası fırlatır. +`$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, [uygulama yapılandırması |configuration] daha kullanışlı bir yol sunar; buraya sadece `database` bölümünü eklemeniz yeterlidir ve gerekli nesneler oluşturulur ve ayrıca [Tracy |tracy:] çubuğunda veritabanı paneli de oluşturulur. +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: @@ -150,35 +150,35 @@ database: password: password ``` -Daha sonra bağlantı nesnesini [DI konteynerinden bir servis olarak alırız |dependency-injection:passing-dependencies], örn.: +Bağlantı nesnesi sonra [DI container'dan servis olarak alınabilir |dependency-injection:passing-dependencies], örneğin: ```php class Model { public function __construct( - // veya Nette\Database\Explorer + // ya da Nette\Database\Explorer private Nette\Database\Connection $database, ) { } } ``` -[Veritabanı yapılandırması |configuration] hakkında daha fazla bilgi. +[Veritabanı yapılandırması|configuration] hakkında daha fazla bilgi. -Manuel Explorer Oluşturma +Explorer'ı Elle Oluşturma ------------------------- -Nette DI konteynerini kullanmıyorsanız, `Nette\Database\Explorer` örneğini manuel olarak oluşturabilirsiniz: +Nette DI container kullanmıyorsanız, `Nette\Database\Explorer` örneğini elle oluşturabilirsiniz: ```php -// veritabanına bağlanma +// veritabanı bağlantısı $connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// önbellek için depolama, Nette\Caching\Storage uygular, örn.: +// ö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 yansımasıyla ilgilenir +// veritabanı yapısının reflection'ını üstlenir $structure = new Nette\Database\Structure($connection, $storage); -// tablo, sütun ve yabancı anahtar adlarının eşleştirilmesi için kuralları tanımlar +// 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); ``` @@ -187,30 +187,32 @@ $explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $ Bağlantı Yönetimi ================= -`Connection` nesnesi oluşturulduğunda bağlantı otomatik olarak kurulur. Bağlantıyı ertelemek isterseniz, lazy modunu kullanın - bunu [yapılandırmada |configuration] `lazy` olarak ayarlayarak veya şu şekilde etkinleştirin: +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 mevcut değilse bir bağlantı oluşturur ve `Nette\Database\ConnectionException` istisnası fırlatabilir. -- `disconnect()` veritabanına olan mevcut bağlantıyı keser. -- `reconnect()` bağlantıyı keser ve ardından veritabanına yeniden bağlanır. Bu metot da `Nette\Database\ConnectionException` istisnası fırlatabilir. +- `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, veritabanıyla bağlantı kurulduktan sonra çağrılacak geri arama (callback) dizisi olan `onConnect` olayını kullanarak bağlantıyla ilişkili olayları izleyebilirsiniz. +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 "Veritabanına bağlandı"; + 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 çubuğunda otomatik olarak bir Veritabanı paneli etkinleştirilir; bu panel, yürütülen tüm sorguları, parametrelerini, yürütme sürelerini ve kodda çağrıldıkları yeri gösterir. +[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/mapping.texy b/database/tr/mapping.texy deleted file mode 100644 index 5ec4aef48e..0000000000 --- a/database/tr/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Tip Dönüşümü -************ - -.[perex] -Nette Database, veritabanından döndürülen değerleri otomatik olarak karşılık gelen PHP tiplerine dönüştürür. - - -Tarih ve Saat -------------- - -Zaman verileri `Nette\Utils\DateTime` nesnelerine dönüştürülür. Zaman verilerinin değişmez (immutable) `Nette\Database\DateTime` nesnelerine dönüştürülmesini istiyorsanız, [yapılandırmada |configuration] `newDateTime` seçeneğini true olarak ayarlayı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 durumunda, `TIME` veri tipi `DateInterval` nesnelerine dönüştürülür. - - -Boolean Değerler ----------------- - -Boolean değerler otomatik olarak `true` veya `false` değerlerine dönüştürülür. MySQL için, [yapılandırmada |configuration] `convertBoolean` ayarını yaparsak `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 tipine göre `int` veya `float` değerlerine 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)` metodunu kullanarak veritabanından gelen satırları dönüştürmek için kendi fonksiyonunuzu ayarlayabilirsiniz. Bu, örneğin veri tiplerinin otomatik dönüşümü için kullanışlıdır. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // burada tip dönüşümü gerçekleşir - return $row; -}); -``` diff --git a/database/tr/reflection.texy b/database/tr/reflection.texy index a17ac24d83..8681b068bc 100644 --- a/database/tr/reflection.texy +++ b/database/tr/reflection.texy @@ -1,10 +1,10 @@ -Yapı Yansıması -************** +Yapı Reflection'ı +***************** .{data-version:3.2.1} -Nette Database, [api:Nette\Database\Reflection] sınıfını kullanarak veritabanı yapısının iç gözlemi için araçlar sağlar. Bu, tablolar, sütunlar, indeksler ve yabancı anahtarlar hakkında bilgi almanızı sağlar. Yansımayı şemalar oluşturmak, veritabanıyla çalışan esnek uygulamalar oluşturmak veya genel veritabanı araçları oluşturmak için kullanabilirsiniz. +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. -Yansıma nesnesini veritabanı bağlantı örneğinden alırız: +Reflection nesnesi, veritabanı bağlantısı örneğinden alınır: ```php $reflection = $database->getReflection(); @@ -17,59 +17,61 @@ Tabloları Alma Salt okunur `$reflection->tables` özelliği, veritabanındaki tüm tabloların ilişkisel bir dizisini içerir: ```php -// Tüm tablo adlarının çıktısı +// Tüm tabloların adlarını listeleme foreach ($reflection->tables as $name => $table) { echo $name . "\n"; } ``` -Ayrıca iki metot daha mevcuttur: +İki metot daha vardır: ```php -// Tablonun varlığını kontrol etme +// Bir tablonun varlığını denetleme if ($reflection->hasTable('users')) { - echo "users tablosu mevcut"; + echo "Table users exists"; } -// Tablo nesnesini döndürür; mevcut değilse istisna fırlatır +// Tablo nesnesini döndürür; yoksa istisna fırlatır $table = $reflection->getTable('users'); ``` -Tablo Bilgileri ---------------- +Tablo Bilgisi +------------- -Tablo, aşağıdaki salt okunur özellikleri sağlayan bir [Table |api:Nette\Database\Reflection\Table] nesnesiyle temsil edilir: +Bir tablo, şu salt okunur özellikleri sunan [Table|api:Nette\Database\Reflection\Table] nesnesiyle temsil edilir: -- `$name: string` – tablo adı -- `$view: bool` – bir görünüm olup olmadığı -- `$fullName: ?string` – şema dahil tam tablo adı (varsa) -- `$columns: array<string, Column>` – tablo sütunlarının ilişkisel dizisi -- `$indexes: Index[]` – tablo indeksleri dizisi -- `$primaryKey: ?Index` – tablonun birincil anahtarı veya null -- `$foreignKeys: ForeignKey[]` – tablonun yabancı anahtarları dizisi +- `$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ı ve değerin aşağıdaki özelliklere sahip bir [Column |api:Nette\Database\Reflection\Column] örneği olduğu ilişkisel bir sütun dizisi sağlar: +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ütun adı -- `$table: ?Table` – sütunun tablosuna referans -- `$nativeType: string` – yerel veritabanı tipi -- `$size: ?int` – tipin 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 otomatik artan olup olmadığı -- `$primary: bool` – birincil anahtarın bir parçası olup olmadığı -- `$vendor: array` – belirli veritabanı sistemine özgü ek meta veri +- `$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 "Sütun: $name\n"; - echo "Tip: {$column->nativeType}\n"; - echo "Null olabilir: " . ($column->nullable ? 'Evet' : 'Hayır') . "\n"; + echo "Column: $name\n"; + echo "Type: {$column->nativeType}\n"; + echo "Nullable: " . ($column->nullable ? 'Yes' : 'No') . "\n"; } ``` @@ -77,28 +79,28 @@ foreach ($table->columns as $name => $column) { İndeksler --------- -Tablonun `indexes` özelliği, her indeksin aşağıdaki özelliklere sahip bir [Index |api:Nette\Database\Reflection\Index] örneği olduğu bir indeks dizisi sağlar: +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 dizisi -- `$unique: bool` – indeksin benzersiz olup olmadığı -- `$primary: bool` – birincil anahtar olup olmadığı -- `$name: ?string` – indeks adı +- `$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ı, ya bir `Index` nesnesi ya da tablonun birincil anahtarı olmaması durumunda `null` döndüren `primaryKey` özelliği kullanılarak elde edilebilir. +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 -// İndekslerin çıktısı +// İndeksleri listeleme foreach ($table->indexes as $index) { $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "İndeks" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Sütunlar: $columns\n"; - echo " Benzersiz: " . ($index->unique ? 'Evet' : 'Hayır') . "\n"; + echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; + echo " Columns: $columns\n"; + echo " Unique: " . ($index->unique ? 'Yes' : 'No') . "\n"; } -// Birincil anahtarın çıktısı +// Birincil anahtarı listeleme if ($primaryKey = $table->primaryKey) { $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Birincil anahtar: $columns\n"; + echo "Primary Key: $columns\n"; } ``` @@ -106,15 +108,15 @@ if ($primaryKey = $table->primaryKey) { Yabancı Anahtarlar ------------------ -Tablonun `foreignKeys` özelliği, her yabancı anahtarın aşağıdaki özelliklere sahip bir [ForeignKey |api:Nette\Database\Reflection\ForeignKey] örneği olduğu bir yabancı anahtar dizisi sağlar: +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 dizisi -- `$foreignColumns: Column[]` – başvurulan sütunlar dizisi -- `$name: ?string` – yabancı anahtar adı +- `$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ın çıktısı +// 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)); diff --git a/database/tr/security.texy b/database/tr/security.texy index 3db61769e9..d76581655b 100644 --- a/database/tr/security.texy +++ b/database/tr/security.texy @@ -3,11 +3,11 @@ Güvenlik Riskleri <div class=perex> -Veritabanı genellikle hassas veriler içerir ve tehlikeli işlemler yapılmasına izin verir. Nette Database ile güvenli çalışmak için şunlar önemlidir: +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 tehlikeli API arasındaki farkı anlamak +- Güvenli ve güvensiz API'ler arasındaki farkı anlamak - Parametreli sorgular kullanmak -- Giriş verilerini doğru şekilde doğrulamak +- Girdi verisini düzgün doğrulamak </div> @@ -15,24 +15,24 @@ Veritabanı genellikle hassas veriler içerir ve tehlikeli işlemler yapılması SQL Injection Nedir? ==================== -SQL injection, veritabanıyla çalışırken en ciddi güvenlik riskidir. Kullanıcıdan gelen işlenmemiş girdinin SQL sorgusunun bir parçası haline gelmesiyle ortaya çıkar. Saldırgan kendi SQL deyimlerini ekleyebilir ve böylece: +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 veya silebilir -- Kimlik doğrulamasını atlatabilir +- Veritabanındaki verileri değiştirebilir ya da silebilir +- Kimlik doğrulamayı atlayabilir ```php -// ❌ TEHLİKELİ KOD - SQL injection'a karşı savunmasız +// ❌ TEHLİKELİ KOD - SQL injection'a açık $database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); -// Saldırgan örneğin şu değeri girebilir: ' OR '1'='1 -// Sonuç sorgusu şöyle olur: SELECT * FROM users WHERE name = '' OR '1'='1' +// 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ı durum Database Explorer için de geçerlidir: +Aynısı Database Explorer için de geçerlidir: ```php -// ❌ TEHLİKELİ KOD - SQL injection'a karşı savunmasız +// ❌ TEHLİKELİ KOD - SQL injection'a açık $table->where('name = ' . $_GET['name']); $table->where("name = '$_GET[name]'"); ``` @@ -41,9 +41,9 @@ $table->where("name = '$_GET[name]'"); Parametreli Sorgular ==================== -SQL injection'a karşı temel savunma parametreli sorgulardır. Nette Database bunların kullanımı için birkaç yol sunar. +SQL injection'a karşı temel savunma parametreli sorgulardır. Nette Database onları kullanmanın çeşitli yollarını sunar. -En basit yol **yer tutucu soru işaretlerini** kullanmaktır: +En basit yol, **soru işareti yer tutucuları** kullanmaktır: ```php // ✅ Güvenli parametreli sorgu @@ -53,9 +53,9 @@ $database->query('SELECT * FROM users WHERE name = ?', $name); $table->where('name = ?', $name); ``` -Bu, yer tutucu soru işaretleri ve parametrelerle ifadeler eklemeye izin veren [Database Explorer |explorer]'daki diğer tüm metotlar için geçerlidir. +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 deyimleri veya WHERE yan tümcesi için değerleri bir dizi içinde iletebiliriz: +INSERT, UPDATE komutlarında ya da WHERE yan tümcesinde değerleri bir dizide aktarabiliriz: ```php // ✅ Güvenli INSERT @@ -75,89 +75,89 @@ $table->insert([ Parametre Değerlerinin Doğrulanması =================================== -Parametreli sorgular, veritabanıyla güvenli çalışmanın temel yapı taşıdır. Ancak, bunlara eklediğimiz değerlerin birkaç kontrol seviyesinden geçmesi gerekir: +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: -Tip Kontrolü +Tür Denetimi ------------ -**En önemlisi, parametrelerin doğru veri tipini sağlamaktır** - bu, Nette Database'in güvenli kullanımı için gerekli bir koşuldur. Veritabanı, tüm giriş verilerinin ilgili sütuna karşılık gelen doğru veri tipine sahip olduğunu varsayar. +**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` beklenmedik bir şekilde bir karakter dizisi yerine bir dizi olsaydı, Nette Database tüm öğelerini SQL sorgusuna eklemeye çalışır ve bu da hataya yol açardı. Bu nedenle, **asla** `$_GET`, `$_POST` veya `$_COOKIE`'den gelen doğrulanmamış verileri doğrudan veritabanı sorgularında kullanmayın. +Ö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**. -Format Kontrolü ---------------- +Biçim Doğrulaması +----------------- -İkinci seviyede, verinin formatını kontrol ederiz - örneğin, karakter dizilerinin UTF-8 kodlamasında olup olmadığını ve uzunluklarının sütun tanımına uygun olup olmadığını veya sayısal değerlerin ilgili sütun veri tipi için izin verilen aralıkta olup olmadığını kontrol ederiz. +İ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 seviyesinde, kısmen veritabanının kendisine de güvenebiliriz - birçok veritabanı geçersiz verileri reddeder. Ancak, davranış farklılık gösterebilir, bazıları uzun karakter dizilerini sessizce kısaltabilir veya aralık dışındaki sayıları kırpabilir. +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. -Alan Kontrolü -------------- +Alana Özgü Doğrulama +-------------------- -Üçüncü seviye, uygulamanıza özgü mantıksal kontrolleri temsil eder. Örneğin, seçim kutularından gelen değerlerin sunulan seçeneklere karşılık geldiğini, sayıların beklenen aralıkta olduğunu (örn. yaş 0-150 yıl) veya değerler arasındaki karşılıklı bağımlılıkların anlamlı olduğunu doğrulamak. +Üçü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 doğru doğrulamasını otomatik olarak sağlayan [Nette Formları |forms:] kullanın -- [Presenter'ları |application:] kullanın ve `action*()` ve `render*()` metotlarındaki parametreler için veri tiplerini belirtin -- Veya `filter_var()` gibi standart PHP araçlarını kullanarak kendi doğrulama katmanınızı uygulayın +- 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ğerlerini nasıl doğru bir şekilde doğrulayacağımızı gösterdik. Ancak, SQL sorgularında dizileri kullanırken, anahtarlarına da aynı özeni göstermeliyiz. +Ö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 işlenmemiş +// ❌ TEHLİKELİ KOD - dizideki anahtarlar temizlenmemiş $database->query('INSERT INTO users', $_POST); ``` -INSERT ve UPDATE deyimlerinde bu kritik bir güvenlik açığıdır - saldırgan veritabanına herhangi bir sütunu ekleyebilir veya değiştirebilir. Örneğin, `is_admin = 1` ayarlayabilir veya hassas sütunlara rastgele veriler ekleyebilir (Mass Assignment Vulnerability olarak adlandırılır). +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örler içerebilirler: +WHERE koşullarında bu daha da tehlikelidir; çünkü operatör içerebilirler: ```php -// ❌ TEHLİKELİ KOD - dizideki anahtarlar işlenmemiş +// ❌ TEHLİKELİ KOD - dizideki anahtarlar temizlenmemiş $_POST['salary >'] = 100000; $database->query('SELECT * FROM users WHERE', $_POST); -// WHERE (`salary` > 100000) sorgusunu yürütür +// WHERE (`salary` > 100000) sorgusunu çalıştırır ``` -Saldırgan bu yaklaşımı çalışanların maaşlarını sistematik olarak keşfetmek için kullanabilir. Örneğin, 100.000'in üzerindeki maaşlar için bir sorguyla başlar, sonra 50.000'in altındakilerle ve aralığı kademeli olarak daraltarak tüm çalışanların yaklaşık maaşlarını ortaya çıkarabilir. Bu tür saldırıya SQL enumeration denir. +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 ve değerlerde operatörler ve fonksiyonlar dahil olmak üzere SQL ifadelerini destekler. Bu, saldırgana SQL injection yapma imkanı verir: +`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 yürütür +// WHERE (0) UNION SELECT name, salary FROM users WHERE (1) sorgusunu çalıştırır ``` -Bu saldırı, orijinal koşulu `0)` ile sonlandırır, `users` tablosundan hassas verileri almak için `UNION` kullanarak kendi `SELECT`'ini ekler ve `WHERE (1)` kullanarak sözdizimsel olarak doğru bir sorgu kapatı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ütunlarını ekleyememesini sağlayan bir mekanizmaya ihtiyacımız var. Tehlikeli sütun adlarını tespit etmeye ve engellemeye çalışabiliriz (kara liste), ancak bu yaklaşım güvenilmezdir - saldırgan her zaman tehlikeli bir sütun adını öngörmediğimiz yeni bir şekilde yazabilir. +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 nedenle, mantığı tersine çevirmek ve izin verilen sütunların açık bir listesini tanımlamak (beyaz liste) çok daha güvenlidir: +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 düzenleyebileceği sütunlar +// Kullanıcının değiştirmesine izin verilen sütunlar $allowedColumns = ['name', 'email', 'active']; -// Girdiden tüm izin verilmeyen sütunları kaldırırız +// İzin verilmeyen tüm sütunları girdiden çıkar $filteredData = array_intersect_key($userData, array_flip($allowedColumns)); -// ✅ Şimdi sorgularda güvenle kullanabiliriz, örneğin: +// ✅ Artık sorgularda güvenle kullanılabilir, örneğin: $database->query('INSERT INTO users', $filteredData); $table->update($filteredData); $table->where($filteredData); @@ -167,19 +167,19 @@ $table->where($filteredData); Dinamik Tanımlayıcılar ====================== -Dinamik tablo ve sütun adları için `?name` yer tutucu sembolünü kullanın. Bu, tanımlayıcıların ilgili veritabanının sözdizimine göre (örn. MySQL'de geri tırnaklar kullanarak) doğru bir şekilde kaçışını sağ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'deki sonuç: SELECT `name` FROM `users` +// MySQL'de sonuç: SELECT `name` FROM `users` ``` -Önemli: `?name` sembolünü yalnızca uygulama kodunda tanımlanan güvenilir değerler için kullanın. Kullanıcıdan gelen değerler için tekrar [beyaz listeyi |#Sütun Beyaz Listesi] kullanın. Aksi takdirde güvenlik risklerine maruz kalırsınız: +Ö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İ - asla kullanıcı girdisini kullanmayın +// ❌ 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 index 83e4f51e1e..53ac83b0ed 100644 --- a/database/tr/sql-way.texy +++ b/database/tr/sql-way.texy @@ -1,17 +1,17 @@ -SQL Yaklaşımı -************* +SQL Yolu +******** .[perex] -Nette Database iki yol sunar: SQL sorgularını kendiniz yazabilir (SQL yaklaşımı) veya otomatik olarak oluşturulmalarını sağlayabilirsiniz (bkz. [Explorer |explorer]). SQL yaklaşımı size sorgular üzerinde tam kontrol sağlarken, güvenli bir şekilde oluşturulmalarını da garanti eder. +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ıyla ilgili ayrıntıları [Bağlantı ve Yapılandırma |guide#Bağlantı ve Yapılandırma] bölümünde bulabilirsiniz. +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ına sorgu yapmak için `query()` metodu kullanılır. Bu metot, sorgu sonucunu temsil eden bir [ResultSet |api:Nette\Database\ResultSet] nesnesi döndürür. Başarısızlık durumunda metot [istisna fırlatır |exceptions]. Sorgu sonucunu `foreach` döngüsüyle gezebilir veya [yardımcı fonksiyonlardan |#Veri Alma] birini kullanabiliriz. +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'); @@ -22,42 +22,42 @@ foreach ($result as $row) { } ``` -SQL sorgularına güvenli bir şekilde değer eklemek için parametreli sorgular kullanırız. Nette Database bunları son derece basit hale getirir - SQL sorgusundan sonra virgül ve değeri eklemeniz yeterlidir: +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 fazla parametre olduğunda iki yazım seçeneğiniz vardır. SQL sorgusunu parametrelerle "serpiştirebilirsiniz": +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); ``` -Veya önce tüm SQL sorgusunu yazıp sonra tüm parametreleri ekleyebilirsiniz: +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'dan Korunma -========================= +SQL Injection'a Karşı Koruma +============================ -Parametreli sorguları kullanmak neden önemlidir? Çünkü sizi, saldırganın kendi SQL deyimlerini ekleyerek veritabanındaki verilere erişebileceği veya zarar verebileceği SQL injection adlı saldırıdan korurlar. +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] -**Asla değişkenleri doğrudan SQL sorgusuna eklemeyin!** Sizi SQL injection'dan koruyan parametreli sorguları her zaman kullanın. +**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 karşı savunmasız +// ❌ 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 riskleri |security] hakkında bilgi edinin. +[Olası güvenlik risklerini |security] öğrenin. Sorgulama Teknikleri @@ -67,7 +67,7 @@ Sorgulama Teknikleri WHERE Koşulları --------------- -WHERE koşullarını, anahtarların sütun adları ve değerlerin karşılaştırma için veriler olduğu ilişkisel bir dizi olarak yazabilirsiniz. Nette Database, değerin tipine göre en uygun SQL operatörünü otomatik olarak seçer. +`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', [ @@ -77,7 +77,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `name` = 'John' AND `active` = 1 ``` -Anahtarda karşılaştırma için operatörü açıkça belirtebilirsiniz: +Karşılaştırma operatörünü anahtarda açıkça da belirtebilirsiniz: ```php $database->query('SELECT * FROM users WHERE', [ @@ -88,7 +88,7 @@ $database->query('SELECT * FROM users WHERE', [ // WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' ``` -Nette, `null` değerleri veya diziler gibi özel durumları otomatik olarak işler. +Nette, `null` değerler ya da diziler gibi özel durumları otomatik olarak ele alır. ```php $database->query('SELECT * FROM products WHERE', [ @@ -99,30 +99,30 @@ $database->query('SELECT * FROM products WHERE', [ // WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL ``` -Negatif koşullar için `NOT` operatörünü kullanın: +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 + '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' => [], // atlanır + 'id NOT' => [], // atlanır ]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL +// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL ``` -Koşulları birleştirmek için `AND` operatörü kullanılır. Bu, [?or yer tutucu sembolü |#SQL Oluşturma İpuçları] kullanılarak değiştirilebilir. +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` sıralaması bir dizi kullanılarak yazılabilir. Anahtarlarda sütunları belirtiriz ve değer, artan sırada sıralanıp sıralanmayacağını belirleyen bir boolean olur: +`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 sırada - 'name' => false, // azalan sırada + 'id' => true, // artan + 'name' => false, // azalan ]); // SELECT id FROM author ORDER BY `id`, `name` DESC ``` @@ -131,7 +131,7 @@ $database->query('SELECT id FROM author ORDER BY', [ Veri Ekleme (INSERT) -------------------- -Kayıt eklemek için `INSERT` SQL deyimi kullanılır. +Kayıt eklemek için SQL `INSERT` komutu kullanılır. ```php $values = [ @@ -142,11 +142,11 @@ $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 (örn. PostgreSQL), ID'nin oluşturulacağı sıra adını `$database->getInsertId($sequenceId)` kullanarak parametre olarak belirtmek gerekir. +`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 veya enum tipleri gibi [#özel değerler] de iletebiliriz. +Parametre olarak dosyalar, DateTime nesneleri ya da enum türleri gibi [#Özel Değerler] de aktarabilirsiniz. -Aynı anda birden fazla kayıt ekleme: +Birden çok kaydı tek seferde ekleme: ```php $database->query('INSERT INTO users ?', [ @@ -155,35 +155,35 @@ $database->query('INSERT INTO users ?', [ ]); ``` -Çoklu INSERT çok daha hızlıdır, çünkü birçok tekil sorgu yerine tek bir veritabanı sorgusu yürütülür. +Ç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 uyarısı:** Asla `$values` olarak doğrulanmamış verileri kullanmayın. [Olası riskler |security#Sütunlarla Güvenli Çalışma] hakkında bilgi edinin. +**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 `UPDATE` SQL deyimi kullanılır. +Kayıtları güncellemek için SQL `UPDATE` komutu kullanılır. ```php -// Tek bir kaydı güncelleme +// Tek bir kaydı güncelle $values = [ 'name' => 'John Smith', ]; $result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); ``` -Etkilenen satır sayısı `$result->getRowCount()` tarafından döndürülür. +Etkilenen satır sayısını `$result->getRowCount()` döndürür. -UPDATE için `+=` ve `-=` operatörlerini kullanabiliriz: +`UPDATE` içinde `+=` ve `-=` operatörlerini kullanabiliriz: ```php $database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // login_count'u artır + 'login_count+=' => 1, // login_count değerini artır ], 1); ``` -Zaten varsa bir kaydı ekleme veya düzenleme örneği. `ON DUPLICATE KEY UPDATE` tekniğini kullanıyoruz: +Kayıt zaten varsa güncelleyen, yoksa ekleyen bir örnek. `ON DUPLICATE KEY UPDATE` tekniğini kullanıyoruz: ```php $values = [ @@ -198,13 +198,13 @@ $database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', // ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 ``` -Nette Database'in, parametreyi dizi ile SQL deyiminin hangi bağlamına eklediğini tanıdığına ve buna göre SQL kodunu oluşturduğuna dikkat edin. Yani ilk diziden `(id, name, year) VALUES (123, 'Jim', 1978)` oluştururken, ikincisini `name = 'Jim', year = 1978` şekline dönüştürdü. Buna [#SQL Oluşturma İpuçları] bölümünde daha ayrıntılı olarak değiniyoruz. +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 `DELETE` SQL deyimi kullanılır. Silinen satır sayısını alma örneği: +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) @@ -212,21 +212,21 @@ $count = $database->query('DELETE FROM users WHERE id = ?', 1) ``` -SQL Oluşturma İpuçları ----------------------- +SQL Kurma İpuçları +------------------ -İpucu, parametre değerinin SQL ifadesine nasıl çevrileceğini belirten SQL sorgusundaki özel bir yer tutucu semboldür: +İ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 olarak kullanılır +| İpucu | Açıklama | Otomatik kullanıldığı yer |-----------|-------------------------------------------------|----------------------------- -| `?name` | tablo veya sütun adı eklemek için kullanılır | - -| `?values` | `(key, ...) VALUES (value, ...)` üretir | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | `key = value, ...` atamasını üretir | `SET ?`, `KEY UPDATE ?` -| `?and` | dizideki koşulları `AND` operatörüyle birleştirir | `WHERE ?`, `HAVING ?` -| `?or` | dizideki koşulları `OR` operatörüyle birleştirir | - +| `?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 ?` -Tablo ve sütun adlarını sorguya dinamik olarak eklemek için `?name` yer tutucu sembolü kullanılır. Nette Database, tanımlayıcıların ilgili veritabanının kurallarına göre (örn. MySQL'de geri tırnak içine alma) doğru bir şekilde işlenmesini sağlar. +`?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'; @@ -235,9 +235,9 @@ $database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); // SELECT `name` FROM `users` WHERE id = 1 (MySQL'de) ``` -**Uyarı:** `?name` sembolünü yalnızca doğrulanmış girdilerden gelen tablo ve sütun adları için kullanın, aksi takdirde [güvenlik riskine |security#Dinamik Tanımlayıcılar] maruz kalırsınız. +**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 oluştururken akıllı otomatik algılama kullanır (tablonun üçüncü sütununa bakın). Ancak, örneğin koşulları `AND` yerine `OR` ile birleştirmek istediğiniz bir durumda kullanabilirsiniz: +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', [ @@ -251,29 +251,29 @@ $database->query('SELECT * FROM users WHERE ?or', [ Özel Değerler ------------- -Normal skaler tiplerin (string, int, bool) yanı sıra, parametre olarak özel değerler de iletebilirsiniz: +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: `DateTime` nesneleri veritabanı formatına dönüştürülür -- enum tipleri: `enum` örnekleri değerlerine dönüştürülür -- SQL literalleri: `Connection::literal('NOW()')` ile oluşturulanlar doğrudan sorguya eklenir +- 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 DateTime, + 'published_at' => new DateTimeImmutable, // ya da new DateTime 'content' => fopen('image.png', 'r'), 'state' => Status::Draft, ]); ``` -`datetime` veri tipi için yerel desteği olmayan veritabanlarında (SQLite ve Oracle gibi), `DateTime` [veritabanı yapılandırmasındaki |configuration] `formatDateTime` öğesiyle belirtilen değere dönüştürülür (varsayılan değer `U` - unix zaman damgasıdır). +`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 Literalleri ---------------- +SQL Sabit Değerleri +------------------- -Bazı durumlarda, değer olarak doğrudan SQL kodu belirtmeniz gerekir, ancak bu kodun bir karakter dizisi olarak anlaşılmaması ve kaçış yapılmaması gerekir. Bunun için `Nette\Database\SqlLiteral` sınıfının nesneleri kullanılır. Bunları `Connection::literal()` metodu oluşturur. +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', [ @@ -283,7 +283,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) ``` -Veya alternatif olarak: +Alternatif olarak: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -293,7 +293,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) ``` -SQL literalleri parametreler içerebilir: +SQL sabit değerleri parametre içerebilir: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -303,7 +303,7 @@ $result = $database->query('SELECT * FROM users WHERE', [ // SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) ``` -Bu sayede ilginç kombinasyonlar oluşturabiliriz: +Bu, ilginç bileşimlere olanak tanır: ```php $result = $database->query('SELECT * FROM users WHERE', [ @@ -321,16 +321,16 @@ Veri Alma ========= -SELECT Sorguları için Kısayollar +SELECT Sorguları İçin Kısayollar -------------------------------- -Veri alımını basitleştirmek için `Connection`, `query()` çağrısını ardından `fetch*()` ile birleştiren birkaç kısayol sunar. Bu metotlar, `query()` ile aynı parametreleri kabul eder, yani SQL sorgusu ve isteğe bağlı parametreler. `fetch*()` metotlarının tam açıklaması [aşağıda |#fetch] bulunabilir. +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 yürütür ve ilk satırı `Row` nesnesi olarak döndürür -| `fetchAll($sql, ...$params): array` | Sorguyu yürütür ve tüm satırları `Row` nesneleri dizisi olarak döndürür -| `fetchPairs($sql, ...$params): array` | Sorguyu yürütür ve ilk sütunun anahtar, ikinci sütunun değer olduğu ilişkisel bir dizi döndürür -| `fetchField($sql, ...$params): mixed` | Sorguyu yürütür ve ilk satırdaki ilk hücrenin değerini döndürür -| `fetchList($sql, ...$params): ?array` | Sorguyu yürütür ve ilk satırı indeksli bir dizi olarak döndürür +| `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: @@ -344,7 +344,7 @@ $count = $database->query('SELECT COUNT(*) FROM articles') `foreach` - Satırlar Üzerinde Yineleme -------------------------------------- -Sorgu yürütüldükten sonra, sonuçları birkaç şekilde gezmenizi sağlayan bir [ResultSet |api:Nette\Database\ResultSet] nesnesi döndürülür. Bir sorguyu yürütmenin ve satırları almanın en kolay yolu `foreach` döngüsünde yinelemektir. Bu yöntem bellek açısından en verimli olanıdır, çünkü verileri kademeli olarak döndürür ve hepsini aynı anda bellekte saklamaz. +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'); @@ -357,13 +357,13 @@ foreach ($result as $row) { ``` .[note] -`ResultSet` yalnızca bir kez yinelenebilir. Tekrar tekrar yinelemeniz gerekiyorsa, önce verileri bir diziye yüklemeniz gerekir, örneğin `fetchAll()` metodunu kullanarak. +`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] ----------------------- -Satırı `Row` nesnesi olarak döndürür. Başka satır yoksa `null` döndürür. Dahili göstericiyi bir sonraki satıra taşır. +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'); @@ -377,7 +377,7 @@ if ($row) { fetchAll(): array .[method] --------------------------- -`ResultSet`'ten kalan tüm satırları `Row` nesneleri dizisi olarak döndürür. +`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'); @@ -391,7 +391,7 @@ foreach ($rows as $row) { 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ütun adını belirtir, ikinci argüman değer olarak kullanılacak sütun adını belirtir: +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'); @@ -399,14 +399,14 @@ $names = $result->fetchPairs('id', 'name'); // [1 => 'John Doe', 2 => 'Jane Doe', ...] ``` -Yalnızca ilk parametreyi belirtirsek, değer tüm satır, yani `Row` nesnesi olacaktır: +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'), ...] ``` -Yinelenen anahtarlar durumunda, son satırdaki değer kullanılır. Anahtar olarak `null` kullanıldığında, dizi sıfırdan başlayarak sayısal olarak indekslenir (o zaman çakışma olmaz): +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'); @@ -417,14 +417,14 @@ $names = $result->fetchPairs(null, 'name'); fetchPairs(Closure $callback): array .[method] ---------------------------------------------- -Alternatif olarak, parametre olarak her satır için ya değeri ya da anahtar-değer çiftini döndürecek bir geri arama (callback) belirtebilirsiniz. +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', ...] -// Geri arama ayrıca anahtar & değer çifti içeren bir dizi döndürebilir: +// 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, ...] ``` @@ -433,18 +433,18 @@ $names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); fetchField(): mixed .[method] ----------------------------- -Geçerli satırdaki ilk hücrenin değerini döndürür. Başka satır yoksa `null` döndürür. Dahili göstericiyi bir sonraki satıra taşır. +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ırdan adı yükler +$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 yoksa `null` döndürür. Dahili göstericiyi bir sonraki satıra taşır. +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'); @@ -455,59 +455,59 @@ $row = $result->fetchList(); // ['John', 'john@example.com'] getRowCount(): ?int .[method] ----------------------------- -Son `UPDATE` veya `DELETE` sorgusundan etkilenen satır sayısını döndürür. `SELECT` için bu, döndürülen satır sayısıdır, ancak bu bilinmeyebilir - bu durumda metot `null` döndürür. +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`'teki sütun sayısını döndürür. +`ResultSet` içindeki sütun sayısını döndürür. -Sorgu Bilgileri -=============== +Sorgu Bilgisi +============= -Hata ayıklama amacıyla, son yürütülen sorgu hakkında bilgi alabiliriz: +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(); // yürütme süresini saniye cinsinden yazdırır +echo $result->getTime(); // çalışma süresini saniye cinsinden yazdırır ``` -Sonucu HTML tablosu olarak görüntülemek için şunu kullanabilirsiniz: +Sonucu bir HTML tablosu olarak göstermek için şunu kullanabilirsiniz: ```php $result = $database->query('SELECT * FROM articles'); $result->dump(); ``` -ResultSet, sütun tipleri hakkında bilgi sunar: +`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 tipi $type->type"; // örn. 'id tipi int' + echo "$column is of type $type"; // örneğin 'id is of type int' } ``` -Sorgu Günlüklemesi ------------------- +Sorgu Günlükleme +---------------- -Kendi sorgu günlüklememizi uygulayabiliriz. `onQuery` olayı, her yürütülen sorgudan sonra çağrılacak geri arama (callback) dizisidir: +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('Sorgu: ' . $result->getQueryString()); - $logger->info('Süre: ' . $result->getTime()); + $logger->info('Query: ' . $result->getQueryString()); + $logger->info('Time: ' . $result->getTime()); if ($result->getRowCount() > 1000) { - $logger->warning('Büyük sonuç kümesi: ' . $result->getRowCount() . ' satır'); + $logger->warning('Large result set: ' . $result->getRowCount() . ' rows'); } }; ``` diff --git a/database/tr/transactions.texy b/database/tr/transactions.texy index 296291f458..b71d4d4f87 100644 --- a/database/tr/transactions.texy +++ b/database/tr/transactions.texy @@ -1,10 +1,10 @@ -İşlemler (Transactions) -*********************** +Transaction'lar +*************** .[perex] -İşlemler (Transactions), işlem içindeki tüm operasyonların ya gerçekleştirilmesini ya da hiçbirinin gerçekleştirilmemesini garanti eder. Daha karmaşık operasyonlarda veri tutarlılığını sağlamak için kullanışlıdırlar. +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. -İşlemleri kullanmanın en basit yolu şöyledir: +Transaction kullanmanın en basit yolu şöyle görünür: ```php $database->beginTransaction(); @@ -21,7 +21,7 @@ try { } ``` -Aynı şeyi `transaction()` metodunu kullanarak çok daha zarif bir şekilde yazabilirsiniz. Parametre olarak, işlem içinde yürüteceği bir geri arama (callback) kabul eder. Geri arama bir istisna olmadan çalışırsa, işlem otomatik olarak onaylanır (commit). Bir istisna oluşursa, işlem iptal edilir (rollback) ve istisna daha da yayılır. +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) { @@ -33,7 +33,9 @@ $database->transaction(function ($database) use ($id) { }); ``` -`transaction()` metodu ayrıca değerler de döndürebilir: +`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) { diff --git a/database/tr/type-conversion.texy b/database/tr/type-conversion.texy new file mode 100644 index 0000000000..11efe21987 --- /dev/null +++ b/database/tr/type-conversion.texy @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000000..85eee5b017 --- /dev/null +++ b/database/tr/upgrading.texy @@ -0,0 +1,36 @@ +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/database/uk/@home.texy b/database/uk/@home.texy deleted file mode 100644 index 9cd3b93223..0000000000 --- a/database/uk/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Підтримувані бази даних -======================= - -Nette підтримує наступні бази даних: - -|* Сервер бази даних |* Ім'я DSN |* Підтримка в Core |* Підтримка в Explorer -| MySQL (>= 5.1) | mysql | ТАК | ТАК -| PostgreSQL (>= 9.0) | pgsql | ТАК | ТАК -| Sqlite 3 (>= 3.8) | sqlite | ТАК | ТАК -| Oracle | oci | ТАК | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ТАК | ТАК -| MS SQL (PDO_DBLIB) | mssql | ТАК | - -| ODBC | odbc | ТАК | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database суттєво спрощує отримання даних з бази даних без необхідності писати SQL-запити. Вона виконує ефективні запити та не передає зайвих даних.}} diff --git a/database/uk/@left-menu.texy b/database/uk/@left-menu.texy deleted file mode 100644 index f8a09afcd7..0000000000 --- a/database/uk/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Вступ |guide] -- [SQL-доступ |sql way] -- [Explorer |Explorer] -- [Транзакції |transactions] -- [Винятки |exceptions] -- [Рефлексія |reflection] -- [Мапування |mapping] -- [Конфігурація |configuration] -- [Ризики безпеки |security] -- [Оновлення |en:upgrading] diff --git a/database/uk/@meta.texy b/database/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/database/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/database/uk/configuration.texy b/database/uk/configuration.texy deleted file mode 100644 index 60db622a55..0000000000 --- a/database/uk/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Конфігурація бази даних -*********************** - -.[perex] -Огляд параметрів конфігурації для Nette Database. - -Якщо ви не використовуєте весь фреймворк, а лише цю бібліотеку, прочитайте, [як завантажити конфігурацію|bootstrap:]. - - -Одне з'єднання --------------- - -Конфігурація одного з'єднання з базою даних: - -```neon -database: - # DSN, єдиний обов'язковий ключ - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Створює сервіси `Nette\Database\Connection` та `Nette\Database\Explorer`, які зазвичай передаються за допомогою [autowiring |dependency-injection:autowiring], або посиланням на [їхню назву |#Сервіси DI]. - -Інші налаштування: - -```neon -database: - # відображати панель бази даних у Tracy Bar? - debugger: ... # (bool) за замовчуванням true - - # відображати EXPLAIN запитів у Tracy Bar? - explain: ... # (bool) за замовчуванням true - - # дозволити autowiring для цього з'єднання? - autowired: ... # (bool) за замовчуванням true для першого з'єднання - - # конвенції таблиць: discovered, static або ім'я класу - conventions: discovered # (string) за замовчуванням 'discovered' - - options: - # підключатися до бази даних лише коли це необхідно? - lazy: ... # (bool) за замовчуванням false - - # PHP клас драйвера бази даних - driverClass: # (string) - - # лише MySQL: встановлює sql_mode - sqlmode: # (string) - - # лише MySQL: встановлює SET NAMES - charset: # (string) за замовчуванням 'utf8mb4' - - # лише MySQL: перетворює TINYINT(1) на bool - convertBoolean: # (bool) за замовчуванням false - - # повертає стовпці з датою як immutable об'єкти (з версії 3.2.1) - newDateTime: # (bool) за замовчуванням false - - # лише Oracle та SQLite: формат для зберігання дати - formatDateTime: # (string) за замовчуванням 'U' -``` - -У ключі `options` можна вказувати інші параметри, які ви знайдете в [документації драйверів PDO |https://www.php.net/manual/en/pdo.drivers.php], наприклад: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Кілька з'єднань ---------------- - -У конфігурації ми можемо визначити і кілька з'єднань з базою даних, розділивши їх на іменовані секції: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring увімкнено лише для сервісів з першої секції. Це можна змінити за допомогою `autowired: false` або `autowired: true`. - - -Сервіси DI ----------- - -Ці сервіси додаються до DI-контейнера, де `###` представляє назву з'єднання: - -| Назва | Тип | Опис -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | з'єднання з базою даних -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Якщо ми визначаємо лише одне з'єднання, назви сервісів будуть `database.default.connection` та `database.default.explorer`. Якщо ми визначаємо кілька з'єднань, як у прикладі вище, назви будуть відповідати секціям, тобто `database.main.connection`, `database.main.explorer`, а також `database.another.connection` та `database.another.explorer`. - -Сервіси без autowiring передаються явно за посиланням на їхню назву: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/uk/exceptions.texy b/database/uk/exceptions.texy deleted file mode 100644 index 3d1642573e..0000000000 --- a/database/uk/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Винятки -******* - -Nette Database використовує ієрархію винятків. Базовим класом є `Nette\Database\DriverException`, який успадковує від `PDOException` і надає розширені можливості для роботи з помилками бази даних: - -- Метод `getDriverCode()` повертає код помилки від драйвера бази даних -- Метод `getSqlState()` повертає код SQLSTATE -- Методи `getQueryString()` та `getParameters()` дозволяють отримати початковий запит та його параметри - -Від `DriverException` успадковуються наступні спеціалізовані винятки: - -- `ConnectionException` - сигналізує про збій підключення до сервера бази даних -- `ConstraintViolationException` - базовий клас для порушення обмежень бази даних, від якого успадковуються: - - `ForeignKeyConstraintViolationException` - порушення зовнішнього ключа - - `NotNullConstraintViolationException` - порушення обмеження NOT NULL - - `UniqueConstraintViolationException` - порушення унікальності значення - - -Приклад перехоплення винятку `UniqueConstraintViolationException`, який виникає, коли ми намагаємося вставити користувача з email, який вже існує в базі даних (за умови, що стовпець email має унікальний індекс). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Користувач з цим email вже існує.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Сталася помилка під час реєстрації: ' . $e->getMessage(); -} -``` diff --git a/database/uk/explorer.texy b/database/uk/explorer.texy deleted file mode 100644 index 164032ccda..0000000000 --- a/database/uk/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer пропонує інтуїтивно зрозумілий та ефективний спосіб роботи з базою даних. Він автоматично дбає про зв'язки між таблицями та оптимізацію запитів, тож ви можете зосередитися на своєму додатку. Працює одразу без налаштувань. Якщо вам потрібен повний контроль над SQL-запитами, ви можете скористатися [SQL-підходом |SQL way]. - -- Робота з даними є природною та легкою для розуміння -- Генерує оптимізовані SQL-запити, які завантажують лише необхідні дані -- Дозволяє легко отримати доступ до пов'язаних даних без необхідності писати JOIN-запити -- Працює одразу без будь-якої конфігурації чи генерації сутностей - -</div> - - -З Explorer ви починаєте, викликаючи метод `table()` об'єкта [api:Nette\Database\Explorer] (деталі підключення див. у розділі [Підключення та конфігурація |guide#Підключення та конфігурація]): - -```php -$books = $explorer->table('book'); // 'book' - назва таблиці -``` - -Метод повертає об'єкт [Selection |api:Nette\Database\Table\Selection], який представляє SQL-запит. До цього об'єкта можна додавати інші методи для фільтрації та сортування результатів. Запит складається та виконується лише тоді, коли ми починаємо запитувати дані. Наприклад, проходячи циклом `foreach`. Кожен рядок представлений об'єктом [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // виведення стовпця 'title' - echo $book->author_id; // виведення стовпця 'author_id' -} -``` - -Explorer суттєво спрощує роботу зі [зв'язками між таблицями |#Зв язки між таблицями]. Наступний приклад показує, як легко ми можемо вивести дані з пов'язаних таблиць (книги та їхні автори). Зверніть увагу, що нам не потрібно писати жодних JOIN-запитів, Nette створить їх за нас: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Книга: ' . $book->title; - echo 'Автор: ' . $book->author->name; // створить JOIN на таблицю 'author' -} -``` - -Nette Database Explorer оптимізує запити, щоб вони були максимально ефективними. Вищезгаданий приклад виконає лише два SELECT-запити, незалежно від того, чи обробляємо ми 10 чи 10 000 книг. - -Крім того, Explorer відстежує, які стовпці використовуються в коді, і завантажує з бази даних лише їх, тим самим заощаджуючи додаткову продуктивність. Ця поведінка повністю автоматична та адаптивна. Якщо ви пізніше зміните код і почнете використовувати інші стовпці, Explorer автоматично змінить запити. Вам не потрібно нічого налаштовувати або думати про те, які стовпці вам знадобляться - залиште це Nette. - - -Фільтрація та сортування -======================== - -Клас `Selection` надає методи для фільтрації та сортування вибірки даних. - -.[language-php] -| `where($condition, ...$params)` | Додає умову WHERE. Кілька умов об'єднуються оператором AND -| `whereOr(array $conditions)` | Додає групу умов WHERE, об'єднаних оператором OR -| `wherePrimary($value)` | Додає умову WHERE за первинним ключем -| `order($columns, ...$params)` | Встановлює сортування ORDER BY -| `select($columns, ...$params)` | Вказує стовпці, які потрібно завантажити -| `limit($limit, $offset = null)` | Обмежує кількість рядків (LIMIT) та опціонально встановлює OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Встановлює пагінацію -| `group($columns, ...$params)` | Групує рядки (GROUP BY) -| `having($condition, ...$params)` | Додає умову HAVING для фільтрації згрупованих рядків - -Методи можна ланцюжком (так званий [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -У цих методах ви також можете використовувати спеціальну нотацію для доступу до [даних з пов'язаних таблиць |#Запити через пов язані таблиці]. - - -Екранування та ідентифікатори ------------------------------ - -Методи автоматично екранують параметри та беруть у лапки ідентифікатори (назви таблиць та стовпців), тим самим запобігаючи SQL injection. Для правильної роботи необхідно дотримуватися кількох правил: - -- Ключові слова, назви функцій, процедур тощо пишіть **великими літерами**. -- Назви стовпців та таблиць пишіть **малими літерами**. -- Рядки завжди підставляйте через **параметри**. - -```php -where('name = ' . $name); // КРИТИЧНА ВРАЗЛИВІСТЬ: SQL injection -where('name LIKE "%search%"'); // ПОГАНО: ускладнює автоматичне взяття в лапки -where('name LIKE ?', '%search%'); // ПРАВИЛЬНО: значення підставлене через параметр - -where('name like ?', $name); // ПОГАНО: згенерує: `name` `like` ? -where('name LIKE ?', $name); // ПРАВИЛЬНО: згенерує: `name` LIKE ? -where('LOWER(name) = ?', $value);// ПРАВИЛЬНО: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Фільтрує результати за допомогою умов WHERE. Її сильною стороною є інтелектуальна робота з різними типами значень та автоматичний вибір SQL-операторів. - -Базове використання: - -```php -$table->where('id', $value); // WHERE `id` = 123 -$table->where('id > ?', $value); // WHERE `id` > 123 -$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow' -``` - -Завдяки автоматичному визначенню відповідних операторів нам не потрібно розбиратися з різними спеціальними випадками. Nette вирішить їх за нас: - -```php -$table->where('id', 1); // WHERE `id` = 1 -$table->where('id', null); // WHERE `id` IS NULL -$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3) -// можна використовувати і знак питання без оператора: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Метод правильно обробляє також заперечні умови та порожні масиви: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- нічого не знайде -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- знайде все -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- знайде все -// $table->where('NOT id ?', $ids); Увага - ця синтаксична конструкція не підтримується -``` - -Як параметр ми можемо передати також результат з іншої таблиці - створиться підзапит: - -```php -// WHERE `id` IN (SELECT `id` FROM `tableName`) -$table->where('id', $explorer->table($tableName)); - -// WHERE `id` IN (SELECT `col` FROM `tableName`) -$table->where('id', $explorer->table($tableName)->select('col')); -``` - -Умови ми можемо передати також як масив, елементи якого об'єднаються за допомогою AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -У масиві ми можемо використовувати пари ключ => значення, і Nette знову автоматично вибере правильні оператори: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -У масиві ми можемо комбінувати SQL-вирази зі знаками питання та кількома параметрами. Це зручно для складних умов з точно визначеними операторами: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // два параметри передаємо як масив -]); -``` - -Багаторазовий виклик `where()` автоматично об'єднує умови за допомогою AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Подібно до `where()`, додає умови, але з тією різницею, що об'єднує їх за допомогою OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Тут також можна використовувати складніші вирази: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Додає умову для первинного ключа таблиці: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Якщо таблиця має складений первинний ключ (наприклад, `foo_id`, `bar_id`), передаємо його як масив: - -```php -// WHERE `foo_id` = 1 AND `bar_id` = 5 -$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch(); - -// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3)) -$table->wherePrimary([ - ['foo_id' => 1, 'bar_id' => 5], - ['foo_id' => 2, 'bar_id' => 3], -])->fetchAll(); -``` - - -order(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Визначає порядок, у якому будуть повернені рядки. Можна сортувати за одним або кількома стовпцями, у спадному чи зростаючому порядку, або за власним виразом: - -```php -$table->order('created'); // ORDER BY `created` -$table->order('created DESC'); // ORDER BY `created` DESC -$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created` -$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC -``` - - -select(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Вказує стовпці, які потрібно повернути з бази даних. За замовчуванням Nette Database Explorer повертає лише ті стовпці, які реально використовуються в коді. Метод `select()` ми використовуємо у випадках, коли потрібно повернути специфічні вирази: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Аліаси, визначені за допомогою `AS`, потім доступні як властивості об'єкта ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // доступ до аліасу -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Обмежує кількість повернутих рядків (LIMIT) та опціонально дозволяє встановити зсув (offset): - -```php -$table->limit(10); // LIMIT 10 (поверне перші 10 рядків) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Для пагінації краще використовувати метод `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Спрощує пагінацію результатів. Приймає номер сторінки (рахується з 1) та кількість елементів на сторінку. Опціонально можна передати посилання на змінну, в яку буде збережено загальну кількість сторінок: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Всього сторінок: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Групує рядки за вказаними стовпцями (GROUP BY). Зазвичай використовується у поєднанні з агрегатними функціями: - -```php -// Рахує кількість продуктів у кожній категорії -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Встановлює умову для фільтрації згрупованих рядків (HAVING). Можна використовувати у поєднанні з методом `group()` та агрегатними функціями: - -```php -// Знаходить категорії, які мають більше 100 продуктів -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Читання даних -============= - -Для читання даних з бази даних у нас є кілька корисних методів: - -.[language-php] -| `foreach ($table as $key => $row)` | Ітерує по всіх рядках, `$key` - значення первинного ключа, `$row` - об'єкт ActiveRow -| `$row = $table->get($key)` | Повертає один рядок за первинним ключем -| `$row = $table->fetch()` | Повертає поточний рядок і переміщує вказівник на наступний -| `$array = $table->fetchPairs()` | Створює асоціативний масив з результатів -| `$array = $table->fetchAll()` | Повертає всі рядки як масив -| `count($table)` | Повертає кількість рядків в об'єкті Selection - -Об'єкт [ActiveRow |api:Nette\Database\Table\ActiveRow] призначений лише для читання. Це означає, що не можна змінювати значення його властивостей. Це обмеження забезпечує консистенцію даних та запобігає неочікуваним побічним ефектам. Дані завантажуються з бази даних, і будь-яка зміна повинна бути виконана явно та контрольовано. - - -`foreach` - ітерація по всіх рядках ------------------------------------ - -Найпростіший спосіб виконати запит і отримати рядки – це ітерація в циклі `foreach`. Автоматично запускає SQL-запит. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key - значення первинного ключа, $book - ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Виконує SQL-запит і повертає рядок за первинним ключем, або `null`, якщо він не існує. - -```php -$book = $explorer->table('book')->get(123); // поверне ActiveRow з ID 123 або null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Повертає рядок і переміщує внутрішній вказівник на наступний. Якщо більше немає рядків, повертає `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Повертає результати як асоціативний масив. Перший аргумент визначає назву стовпця, який буде використовуватися як ключ у масиві, другий аргумент визначає назву стовпця, який буде використовуватися як значення: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Якщо вказати лише перший параметр, значенням буде весь рядок, тобто об'єкт `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -У випадку дублювання ключів використовується значення з останнього рядка. При використанні `null` як ключа, масив буде індексований чисельно з нуля (тоді колізій не виникає): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Альтернативно, ви можете вказати як параметр callback, який для кожного рядка повертатиме або саме значення, або пару ключ-значення. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Перша книга (Ян Новак)', ...] - -// Callback може також повертати масив з парою ключ & значення: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Перша книга' => 'Ян Новак', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Повертає всі рядки як асоціативний масив об'єктів `ActiveRow`, де ключами є значення первинних ключів. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Метод `count()` без параметра повертає кількість рядків в об'єкті `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // альтернатива -``` - -Увага, `count()` з параметром виконує агрегатну функцію COUNT у базі даних, див. нижче. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Перетворює об'єкт `ActiveRow` на асоціативний масив, де ключами є назви стовпців, а значеннями – відповідні дані. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray буде ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Агрегація -========= - -Клас `Selection` надає методи для легкого виконання агрегатних функцій (COUNT, SUM, MIN, MAX, AVG тощо). - -.[language-php] -| `count($expr)` | Рахує кількість рядків -| `min($expr)` | Повертає мінімальне значення у стовпці -| `max($expr)` | Повертає максимальне значення у стовпці -| `sum($expr)` | Повертає суму значень у стовпці -| `aggregation($function)` | Дозволяє виконати будь-яку агрегатну функцію. Напр. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Виконує SQL-запит з функцією COUNT і повертає результат. Метод використовується для визначення, скільки рядків відповідає певній умові: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Увага, [#count()] без параметра лише повертає кількість рядків в об'єкті `Selection`. - - -min(string $expr) a max(string $expr) .[method] ------------------------------------------------ - -Методи `min()` та `max()` повертають мінімальне та максимальне значення у вказаному стовпці або виразі: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Повертає суму значень у вказаному стовпці або виразі: - -```php -// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1 -$totalPrice = $products->where('active', true) - ->sum('price * items_in_stock'); -``` - - -aggregation(string $function, ?string $groupFunction = null) .[method] ----------------------------------------------------------------------- - -Дозволяє виконати будь-яку агрегатну функцію. - -```php -// середня ціна продуктів у категорії -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// об'єднує теги продукту в один рядок -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Якщо нам потрібно агрегувати результати, які вже самі по собі виникли з якоїсь агрегатної функції та групування (наприклад, `SUM(значення)` за згрупованими рядками), як другий аргумент вкажемо агрегатну функцію, яка має бути застосована до цих проміжних результатів: - -```php -// Розраховує загальну вартість продуктів на складі для окремих категорій, а потім підсумовує ці ціни разом. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -У цьому прикладі ми спочатку розраховуємо загальну вартість продуктів у кожній категорії (`SUM(price * stock) AS category_total`) та групуємо результати за `category_id`. Потім використовуємо `aggregation('SUM(category_total)', 'SUM')` для підсумовування цих проміжних сум `category_total`. Другий аргумент `'SUM'` вказує, що до проміжних результатів має бути застосована функція SUM. - - -Insert, Update & Delete -======================= - -Nette Database Explorer спрощує вставку, оновлення та видалення даних. Усі наведені методи у випадку помилки викидають виняток `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Вставляє нові записи до таблиці. - -**Вставка одного запису:** - -Новий запис передаємо як асоціативний масив або iterable об'єкт (наприклад, ArrayHash, що використовується у [формах |forms:]), де ключі відповідають назвам стовпців у таблиці. - -Якщо таблиця має визначений первинний ключ, метод повертає об'єкт `ActiveRow`, який перезавантажується з бази даних, щоб врахувати можливі зміни, внесені на рівні бази даних (тригери, значення за замовчуванням стовпців, обчислення auto-increment стовпців). Це забезпечує консистенцію даних, і об'єкт завжди містить актуальні дані з бази даних. Якщо однозначного первинного ключа немає, повертає передані дані у вигляді масиву. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row є екземпляром ActiveRow і містить повні дані вставленого рядка, -// включно з автоматично згенерованим ID та можливими змінами, внесеними тригерами -echo $row->id; // Виведе ID новоствореного користувача -echo $row->created_at; // Виведе час створення, якщо встановлено тригером -``` - -**Вставка кількох записів одночасно:** - -Метод `insert()` дозволяє вставити кілька записів за допомогою одного SQL-запиту. У цьому випадку повертає кількість вставлених рядків. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows буде 2 -``` - -Як параметр можна також передати об'єкт `Selection` з вибіркою даних. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Вставка спеціальних значень:** - -Як значення ми можемо передавати також файли, об'єкти DateTime або SQL-літерали: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // перетворює на формат бази даних - 'avatar' => fopen('image.jpg', 'rb'), // вставляє бінарний вміст файлу - 'uuid' => $explorer::literal('UUID()'), // викликає функцію UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Оновлює рядки в таблиці відповідно до вказаного фільтра. Повертає кількість фактично змінених рядків. - -Змінювані стовпці передаємо як асоціативний масив або iterable об'єкт (наприклад, ArrayHash, що використовується у [формах |forms:]), де ключі відповідають назвам стовпців у таблиці: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Для зміни числових значень можна використовувати оператори `+=` та `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // збільшить значення стовпця 'points' на 1 - 'coins-=' => 1, // зменшить значення стовпця 'coins' на 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Видаляє рядки з таблиці відповідно до вказаного фільтра. Повертає кількість видалених рядків. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -При виклику `update()` та `delete()` не забудьте за допомогою `where()` вказати рядки, які потрібно змінити/видалити. Якщо `where()` не використовувати, операція буде виконана над усією таблицею! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Оновлює дані в рядку бази даних, представленому об'єктом `ActiveRow`. Як параметр приймає iterable з даними, які потрібно оновити (ключі - назви стовпців). Для зміни числових значень можна використовувати оператори `+=` та `-=`: - -Після виконання оновлення `ActiveRow` автоматично перезавантажується з бази даних, щоб врахувати можливі зміни, внесені на рівні бази даних (наприклад, тригери). Метод повертає true лише якщо відбулася фактична зміна даних. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // збільшимо кількість переглядів -]); -echo $article->views; // Виведе поточну кількість переглядів -``` - -Цей метод оновлює лише один конкретний рядок у базі даних. Для масового оновлення кількох рядків використовуйте метод [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Видаляє рядок з бази даних, який представлений об'єктом `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Видалить книгу з ID 1 -``` - -Цей метод видаляє лише один конкретний рядок у базі даних. Для масового видалення кількох рядків використовуйте метод [#Selection::delete()]. - - -Зв'язки між таблицями -===================== - -У реляційних базах даних дані розділені на кілька таблиць і взаємопов'язані за допомогою зовнішніх ключів. Nette Database Explorer пропонує революційний спосіб роботи з цими зв'язками - без написання JOIN-запитів та необхідності щось конфігурувати чи генерувати. - -Для ілюстрації роботи зі зв'язками використаємо приклад бази даних книг ([знайдете його на GitHub |https://github.com/nette-examples/books]). У базі даних маємо таблиці: - -- `author` - письменники та перекладачі (стовпці `id`, `name`, `web`, `born`) -- `book` - книги (стовпці `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - теги (стовпці `id`, `name`) -- `book_tag` - таблиця зв'язку між книгами та тегами (стовпці `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Структура бази даних, що використовується в прикладах *** - -У нашому прикладі бази даних книг знайдемо кілька типів зв'язків (хоча модель спрощена порівняно з реальністю): - -- One-to-many 1:N – кожна книга **має одного** автора, автор може написати **кілька** книг -- Zero-to-many 0:N – книга **може мати** перекладача, перекладач може перекласти **кілька** книг -- Zero-to-one 0:1 – книга **може мати** наступну частину -- Many-to-many M:N – книга **може мати кілька** тегів, а тег може бути присвоєний **кільком** книгам - -У цих зв'язках завжди існує батьківська та дочірня таблиця. Наприклад, у зв'язку між автором та книгою таблиця `author` є батьківською, а `book` - дочірньою. Ми можемо уявити це так, що книга завжди "належить" якомусь автору. Це проявляється і в структурі бази даних: дочірня таблиця `book` містить зовнішній ключ `author_id`, який посилається на батьківську таблицю `author`. - -Якщо нам потрібно вивести книги разом з іменами їхніх авторів, у нас є два варіанти. Або отримати дані одним SQL-запитом за допомогою JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Або завантажити дані у два кроки - спочатку книги, а потім їхніх авторів - і потім зібрати їх у PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id авторів отриманих книг -``` - -Другий підхід насправді ефективніший, хоча це може здатися дивним. Дані завантажуються лише один раз і можуть бути краще використані в кеші. Саме таким чином працює Nette Database Explorer - все вирішує під капотом і пропонує вам елегантний API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author - це запис з таблиці 'author' - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Доступ до батьківської таблиці ------------------------------- - -Доступ до батьківської таблиці є прямолінійним. Йдеться про зв'язки типу *книга має автора* або *книга може мати перекладача*. Пов'язаний запис отримуємо через властивість об'єкта ActiveRow - її назва відповідає назві стовпця із зовнішнім ключем без `id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // знайде автора за стовпцем author_id -echo $book->translator?->name; // знайде перекладача за translator_id -``` - -Коли ми звертаємося до властивості `$book->author`, Explorer у таблиці `book` шукає стовпець, назва якого містить рядок `author` (тобто `author_id`). За значенням у цьому стовпці він завантажує відповідний запис з таблиці `author` і повертає його як `ActiveRow`. Подібно працює і `$book->translator`, який використовує стовпець `translator_id`. Оскільки стовпець `translator_id` може містити `null`, ми використовуємо в коді оператор `?->`. - -Альтернативний шлях пропонує метод `ref()`, який приймає два аргументи: назву цільової таблиці та назву сполучного стовпця, і повертає екземпляр `ActiveRow` або `null`: - -```php -echo $book->ref('author', 'author_id')->name; // зв'язок з автором -echo $book->ref('author', 'translator_id')->name; // зв'язок з перекладачем -``` - -Метод `ref()` корисний, якщо не можна використати доступ через властивість, оскільки таблиця містить стовпець з такою ж назвою (тобто `author`). В інших випадках рекомендується використовувати доступ через властивість, який є більш читабельним. - -Explorer автоматично оптимізує запити до бази даних. Коли ми проходимо книги в циклі та звертаємося до їхніх пов'язаних записів (авторів, перекладачів), Explorer не генерує запит для кожної книги окремо. Замість цього він виконує лише один SELECT для кожного типу зв'язку, тим самим значно знижуючи навантаження на базу даних. Наприклад: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Цей код викличе лише ці три блискавичні запити до бази даних: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id зі стовпця author_id вибраних книг -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id зі стовпця translator_id вибраних книг -``` - -.[note] -Логіка пошуку сполучного стовпця визначається реалізацією [Conventions |api:Nette\Database\Conventions]. Рекомендуємо використовувати [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], які аналізують зовнішні ключі та дозволяють легко працювати з існуючими зв'язками між таблицями. - - -Доступ до дочірньої таблиці ---------------------------- - -Доступ до дочірньої таблиці працює у зворотному напрямку. Тепер ми запитуємо *які книги написав цей автор* або *переклав цей перекладач*. Для цього типу запиту ми використовуємо метод `related()`, який повертає `Selection` з пов'язаними записами. Розглянемо приклад: - -```php -$author = $explorer->table('author')->get(1); - -// Виведе всі книги автора -foreach ($author->related('book.author_id') as $book) { - echo "Написав: $book->title"; -} - -// Виведе всі книги, які автор переклав -foreach ($author->related('book.translator_id') as $book) { - echo "Переклав: $book->title"; -} -``` - -Метод `related()` приймає опис з'єднання як один аргумент з точковою нотацією або як два окремі аргументи: - -```php -$author->related('book.translator_id'); // один аргумент -$author->related('book', 'translator_id'); // два аргументи -``` - -Explorer може автоматично визначити правильний сполучний стовпець на основі назви батьківської таблиці. У цьому випадку з'єднання відбувається через стовпець `book.author_id`, оскільки назва вихідної таблиці - `author`: - -```php -$author->related('book'); // використовує book.author_id -``` - -Якщо існує кілька можливих з'єднань, Explorer викине виняток [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Метод `related()` можна, звичайно, використовувати і при проходженні кількох записів у циклі, і Explorer і в цьому випадку автоматично оптимізує запити: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' написав:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Цей код згенерує лише два блискавичні SQL-запити: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id вибраних авторів -``` - - -Зв'язок Many-to-many --------------------- - -Для зв'язку many-to-many (M:N) необхідна наявність таблиці зв'язку (у нашому випадку `book_tag`), яка містить два стовпці із зовнішніми ключами (`book_id`, `tag_id`). Кожен з цих стовпців посилається на первинний ключ однієї з пов'язуваних таблиць. Для отримання пов'язаних даних спочатку отримуємо записи з таблиці зв'язку за допомогою `related('book_tag')`, а далі переходимо до цільових даних: - -```php -$book = $explorer->table('book')->get(1); -// виведе назви тегів, присвоєних книзі -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // виведе назву тегу через таблицю зв'язку -} - -$tag = $explorer->table('tag')->get(1); -// або навпаки: виведе назви книг, позначених цим тегом -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // виведе назву книги -} -``` - -Explorer знову оптимізує SQL-запити до ефективної форми: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id вибраних книг -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id тегів, знайдених у book_tag -``` - - -Запити через пов'язані таблиці ------------------------------- - -У методах `where()`, `select()`, `order()` та `group()` ми можемо використовувати спеціальні нотації для доступу до стовпців з інших таблиць. Explorer автоматично створить необхідні JOIN-и. - -**Точкова нотація** (`батьківська_таблиця.стовпець`) використовується для зв'язку 1:N з точки зору дочірньої таблиці: - -```php -$books = $explorer->table('book'); - -// Знаходить книги, автор яких має ім'я, що починається на 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Сортує книги за іменем автора за спаданням -$books->order('author.name DESC'); - -// Виводить назву книги та ім'я автора -$books->select('book.title, author.name'); -``` - -**Двокрапкова нотація** (`:дочірня_таблиця.стовпець`) використовується для зв'язку 1:N з точки зору батьківської таблиці: - -```php -$authors = $explorer->table('author'); - -// Знаходить авторів, які написали книгу з 'PHP' у назві -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Рахує кількість книг для кожного автора -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -У вищезгаданому прикладі з двокрапковою нотацією (`:book.title`) не вказано стовпець із зовнішнім ключем. Explorer автоматично визначає правильний стовпець на основі назви батьківської таблиці. У цьому випадку з'єднання відбувається через стовпець `book.author_id`, оскільки назва вихідної таблиці - `author`. Якщо існує кілька можливих з'єднань, Explorer викине виняток [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Сполучний стовпець можна явно вказати в дужках: - -```php -// Знаходить авторів, які переклали книгу з 'PHP' у назві -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Нотації можна ланцюжком для доступу через кілька таблиць: - -```php -// Знаходить авторів книг, позначених тегом 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Розширення умов для JOIN ------------------------- - -Метод `joinWhere()` розширює умови, які вказуються при з'єднанні таблиць у SQL за ключовим словом `ON`. - -Припустимо, ми хочемо знайти книги, перекладені конкретним перекладачем: - -```php -// Знаходить книги, перекладені перекладачем на ім'я 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -В умові `joinWhere()` ми можемо використовувати ті ж конструкції, що й у методі `where()` - оператори, знаки питання, масиви значень або SQL-вирази. - -Для складніших запитів з кількома JOIN-ами ми можемо визначити аліаси таблиць: - -```php -$tags = $explorer->table('tag') - ->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950) - ->alias(':book_tag.book.author', 'book_author'); -// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id` -// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id` -// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id` -// AND (`book_author`.`born` < 1950) -``` - -Зверніть увагу, що тоді як метод `where()` додає умови до клаузули `WHERE`, метод `joinWhere()` розширює умови в клаузулі `ON` при з'єднанні таблиць. diff --git a/database/uk/guide.texy b/database/uk/guide.texy deleted file mode 100644 index e192adf548..0000000000 --- a/database/uk/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database — це потужний та елегантний шар бази даних для PHP з акцентом на простоту та розумні функції. Він пропонує два способи роботи з базою даних — [Explorer |Explorer] для швидкої розробки додатків або [SQL підхід |SQL way] для прямої роботи з запитами. - -<div class="grid gap-3"> -<div> - - -[SQL підхід |SQL way] -===================== -- Безпечні параметризовані запити -- Точний контроль над формою SQL-запитів -- Коли ви пишете складні запити з розширеними функціями -- Оптимізуєте продуктивність за допомогою специфічних функцій SQL - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Розробляєте швидко, не пишучи SQL -- Інтуїтивна робота з відношеннями між таблицями -- Оціните автоматичну оптимізацію запитів -- Підходить для швидкої та зручної роботи з базою даних - -</div> - -</div> - - -Встановлення -============ - -Завантажте та встановіть бібліотеку за допомогою інструмента [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Підтримувані бази даних -======================= - -Nette Database підтримує наступні бази даних: - -|* Сервер бази даних |* Ім'я DSN |* Підтримка в Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | ТАК -| PostgreSQL (>= 9.0) | pgsql | ТАК -| Sqlite 3 (>= 3.8) | sqlite | ТАК -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ТАК -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Два підходи до бази даних -========================= - -Nette Database надає вам вибір: ви можете або писати SQL-запити безпосередньо (SQL підхід), або дозволити генерувати їх автоматично (Explorer). Давайте подивимося, як обидва підходи вирішують однакові завдання: - -[SQL підхід|sql way] - SQL-запити - -```php -// вставка запису -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// отримання записів: автори книг -$result = $database->query(' - SELECT authors.*, COUNT(books.id) AS books_count - FROM authors - LEFT JOIN books ON authors.id = books.author_id - WHERE authors.active = 1 - GROUP BY authors.id -'); - -// виведення (не оптимально, генерує N додаткових запитів) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Автор $author->name написав $author->books_count книг:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer підхід|explorer] - автоматичне генерування SQL - -```php -// вставка запису -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// отримання записів: автори книг -$authors = $database->table('authors') - ->where('active', 1); - -// виведення (автоматично генерує лише 2 оптимізовані запити) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Автор $author->name написав {$books->count()} книг:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Підхід Explorer генерує та оптимізує SQL-запити автоматично. У наведеному прикладі SQL підхід генерує N+1 запитів (один для авторів, а потім один для книг кожного автора), тоді як Explorer автоматично оптимізує запити та виконує лише два - один для авторів та один для всіх їхніх книг. - -Обидва підходи можна вільно комбінувати в додатку за потреби. - - -Підключення та конфігурація -=========================== - -Для підключення до бази даних достатньо створити екземпляр класу [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Параметр `$dsn` (data source name) такий самий, [який використовує PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], наприклад `mysql:host=127.0.0.1;dbname=test`. У разі збою викидається виняток `Nette\Database\ConnectionException`. - -Однак, зручніший спосіб пропонує [конфігурація програми |configuration], куди достатньо додати секцію `database`, і будуть створені необхідні об'єкти, а також панель бази даних у [Tracy |tracy:] барі. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Потім об'єкт з'єднання [отримаємо як сервіс з DI-контейнера |dependency-injection:passing-dependencies], наприклад: - -```php -class Model -{ - public function __construct( - // або Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Більше інформації про [конфігурацію бази даних|configuration]. - - -Ручне створення Explorer ------------------------- - -Якщо ви не використовуєте Nette DI-контейнер, ви можете створити екземпляр `Nette\Database\Explorer` вручну: - -```php -// підключення до бази даних -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// сховище для кешу, реалізує Nette\Caching\Storage, наприклад: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// відповідає за рефлексію структури бази даних -$structure = new Nette\Database\Structure($connection, $storage); -// визначає правила для відображення назв таблиць, стовпців та зовнішніх ключів -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Управління підключенням -======================= - -При створенні об'єкта `Connection` підключення відбувається автоматично. Якщо ви хочете відкласти підключення, використовуйте режим lazy - його можна увімкнути в [конфігурації|configuration], встановивши `lazy: true`, або так: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Для управління підключенням використовуйте методи `connect()`, `disconnect()` та `reconnect()`. -- `connect()` створює підключення, якщо його ще немає, при цьому може викликати виняток `Nette\Database\ConnectionException`. -- `disconnect()` відключає поточне підключення до бази даних. -- `reconnect()` виконує відключення та подальше повторне підключення до бази даних. Цей метод також може викликати виняток `Nette\Database\ConnectionException`. - -Крім того, ви можете відстежувати події, пов'язані з підключенням, за допомогою події `onConnect`, яка є масивом колбеків, що викликаються після встановлення з'єднання з базою даних. - -```php -// виконується після підключення до бази даних -$database->onConnect[] = function($database) { - echo "Підключено до бази даних"; -}; -``` - - -Tracy Debug Bar -=============== - -Якщо ви використовуєте [Tracy |tracy:], автоматично активується панель Database в Debug барі, яка відображає всі виконані запити, їхні параметри, час виконання та місце в коді, де вони були викликані. - -[* db-panel.webp *] diff --git a/database/uk/mapping.texy b/database/uk/mapping.texy deleted file mode 100644 index c8bcebb200..0000000000 --- a/database/uk/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Перетворення типів -****************** - -.[perex] -Nette Database автоматично перетворює значення, повернуті з бази даних, на відповідні типи PHP. - - -Дата та час ------------ - -Часові дані перетворюються на об'єкти `Nette\Utils\DateTime`. Якщо ви хочете, щоб часові дані перетворювалися на незмінні об'єкти `Nette\Database\DateTime`, встановіть у [конфігурації|configuration] опцію `newDateTime: true`. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('j. n. Y'); -``` - -У випадку MySQL перетворює тип даних `TIME` на об'єкти `DateInterval`. - - -Булеві значення ---------------- - -Булеві значення автоматично перетворюються на `true` або `false`. У MySQL перетворюється `TINYINT(1)`, якщо ми встановимо в [конфігурації|configuration] `convertBoolean: true`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Числові значення ----------------- - -Числові значення перетворюються на `int` або `float` відповідно до типу стовпця в базі даних: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Власна нормалізація -------------------- - -За допомогою методу `setRowNormalizer(?callable $normalizer)` ви можете встановити власну функцію для трансформації рядків з бази даних. Це корисно, наприклад, для автоматичного перетворення типів даних. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // тут відбувається перетворення типів - return $row; -}); -``` diff --git a/database/uk/reflection.texy b/database/uk/reflection.texy deleted file mode 100644 index 703664abb8..0000000000 --- a/database/uk/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Рефлексія структури -******************* - -.{data-version:3.2.1} -Nette Database надає інструменти для інтроспекції структури бази даних за допомогою класу [api:Nette\Database\Reflection]. Вона дозволяє отримувати інформацію про таблиці, стовпці, індекси та зовнішні ключі. Рефлексію можна використовувати для генерації схем, створення гнучких додатків, що працюють з базою даних, або загальних інструментів для роботи з базами даних. - -Об'єкт рефлексії отримуємо з екземпляра підключення до бази даних: - -```php -$reflection = $database->getReflection(); -``` - - -Отримання таблиць ------------------ - -Readonly властивість `$reflection->tables` містить асоціативний масив усіх таблиць у базі даних: - -```php -// Виведення назв усіх таблиць -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Доступні ще два методи: - -```php -// Перевірка існування таблиці -if ($reflection->hasTable('users')) { - echo "Таблиця users існує"; -} - -// Повертає об'єкт таблиці; якщо не існує, викидає виняток -$table = $reflection->getTable('users'); -``` - - -Інформація про таблицю ----------------------- - -Таблиця представлена об'єктом [Table|api:Nette\Database\Reflection\Table], який надає наступні readonly властивості: - -- `$name: string` – назва таблиці -- `$view: bool` – чи є це представленням (view) -- `$fullName: ?string` – повна назва таблиці, включаючи схему (якщо існує) -- `$columns: array<string, Column>` – асоціативний масив стовпців таблиці -- `$indexes: Index[]` – масив індексів таблиці -- `$primaryKey: ?Index` – первинний ключ таблиці або null -- `$foreignKeys: ForeignKey[]` – масив зовнішніх ключів таблиці - - -Стовпці -------- - -Властивість `columns` таблиці надає асоціативний масив стовпців, де ключем є назва стовпця, а значенням - екземпляр [Column|api:Nette\Database\Reflection\Column] з такими властивостями: - -- `$name: string` – назва стовпця -- `$table: ?Table` – посилання на таблицю стовпця -- `$nativeType: string` – нативний тип даних бази даних -- `$size: ?int` – розмір/довжина типу -- `$nullable: bool` – чи може стовпець містити NULL -- `$default: mixed` – значення за замовчуванням стовпця -- `$autoIncrement: bool` – чи є стовпець автоінкрементним -- `$primary: bool` – чи є частиною первинного ключа -- `$vendor: array` – додаткові метадані, специфічні для даної системи бази даних - -```php -foreach ($table->columns as $name => $column) { - echo "Стовпець: $name\n"; - echo "Тип: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Так' : 'Ні') . "\n"; -} -``` - - -Індекси -------- - -Властивість `indexes` таблиці надає масив індексів, де кожен індекс є екземпляром [Index|api:Nette\Database\Reflection\Index] з такими властивостями: - -- `$columns: Column[]` – масив стовпців, що утворюють індекс -- `$unique: bool` – чи є індекс унікальним -- `$primary: bool` – чи є це первинним ключем -- `$name: ?string` – назва індексу - -Первинний ключ таблиці можна отримати за допомогою властивості `primaryKey`, яка повертає або об'єкт `Index`, або `null` у випадку, якщо таблиця не має первинного ключа. - -```php -// Виведення індексів -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Індекс" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Стовпці: $columns\n"; - echo " Unique: " . ($index->unique ? 'Так' : 'Ні') . "\n"; -} - -// Виведення первинного ключа -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Первинний ключ: $columns\n"; -} -``` - - -Зовнішні ключі --------------- - -Властивість `foreignKeys` таблиці надає масив зовнішніх ключів, де кожен зовнішній ключ є екземпляром [ForeignKey|api:Nette\Database\Reflection\ForeignKey] з такими властивостями: - -- `$foreignTable: Table` – таблиця, на яку посилається ключ -- `$localColumns: Column[]` – масив локальних стовпців -- `$foreignColumns: Column[]` – масив стовпців, на які посилається ключ -- `$name: ?string` – назва зовнішнього ключа - -```php -// Виведення зовнішніх ключів -foreach ($table->foreignKeys as $fk) { - $localCols = implode(', ', array_map(fn($col) => $col->name, $fk->localColumns)); - $foreignCols = implode(', ', array_map(fn($col) => $col->name, $fk->foreignColumns)); - - echo "FK" . ($fk->name ? " {$fk->name}" : '') . ":\n"; - echo " $localCols -> {$fk->foreignTable->name}($foreignCols)\n"; -} -``` diff --git a/database/uk/security.texy b/database/uk/security.texy deleted file mode 100644 index cc04ec9783..0000000000 --- a/database/uk/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Ризики безпеки -************** - -<div class=perex> - -База даних часто містить конфіденційні дані та дозволяє виконувати небезпечні операції. Для безпечної роботи з Nette Database ключовим є: - -- Розуміти різницю між безпечним та небезпечним API -- Використовувати параметризовані запити -- Правильно валідувати вхідні дані - -</div> - - -Що таке SQL Injection? -====================== - -SQL injection є найсерйознішим ризиком безпеки при роботі з базою даних. Він виникає, коли необроблені вхідні дані від користувача стають частиною SQL-запиту. Зловмисник може вставити власні SQL-команди і таким чином: -- Отримати несанкціонований доступ до даних -- Змінити або видалити дані в базі даних -- Обійти автентифікацію - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкції -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Зловмисник може ввести, наприклад, значення: ' OR '1'='1 -// Кінцевий запит буде: SELECT * FROM users WHERE name = '' OR '1'='1' -// Що поверне всіх користувачів -``` - -Те саме стосується і Database Explorer: - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкції -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Параметризовані запити -====================== - -Основний захист від SQL injection - це параметризовані запити. Nette Database пропонує кілька способів їх використання. - -Найпростіший спосіб - використання **заповнювачів-знаків питання**: - -```php -// ✅ Безпечний параметризований запит -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Безпечна умова в Explorer -$table->where('name = ?', $name); -``` - -Це стосується всіх інших методів у [Database Explorer|explorer], які дозволяють вставляти вирази з заповнювачами-знаками питання та параметрами. - -Для команд INSERT, UPDATE або умови WHERE ми можемо передати значення в масиві: - -```php -// ✅ Безпечний INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Безпечний INSERT в Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Валідація значень параметрів -============================ - -Параметризовані запити є основним будівельним блоком безпечної роботи з базою даних. Однак значення, які ми в них вставляємо, повинні пройти кілька рівнів перевірок: - - -Перевірка типу --------------- - -**Найважливіше - забезпечити правильний тип даних параметрів** - це необхідна умова для безпечного використання Nette Database. База даних передбачає, що всі вхідні дані мають правильний тип даних, що відповідає даному стовпцю. - -Наприклад, якби `$name` у попередніх прикладах було несподівано масивом замість рядка, Nette Database спробувала б вставити всі його елементи в SQL-запит, що призвело б до помилки. Тому **ніколи не використовуйте** невалідовані дані з `$_GET`, `$_POST` або `$_COOKIE` безпосередньо в запитах до бази даних. - - -Перевірка формату ------------------ - -На другому рівні ми перевіряємо формат даних - наприклад, чи є рядки в кодуванні UTF-8 та чи їхня довжина відповідає визначенню стовпця, або чи є числові значення в дозволеному діапазоні для даного типу даних стовпця. - -На цьому рівні валідації ми можемо частково покладатися і на саму базу даних - багато баз даних відхилять невалідовані дані. Однак поведінка може відрізнятися, деякі можуть тихо скоротити довгі рядки або обрізати числа поза діапазоном. - - -Доменна перевірка ------------------ - -Третій рівень представляють логічні перевірки, специфічні для вашого додатка. Наприклад, перевірка, що значення з select box відповідають запропонованим варіантам, що числа знаходяться в очікуваному діапазоні (наприклад, вік 0-150 років) або що взаємні залежності між значеннями мають сенс. - - -Рекомендовані способи валідації -------------------------------- - -- Використовуйте [Nette Forms|forms:], які автоматично забезпечать правильну валідацію всіх вхідних даних -- Використовуйте [Presenters|application:] та вказуйте у параметрів в `action*()` та `render*()` методах типи даних -- Або реалізуйте власний шар валідації за допомогою стандартних інструментів PHP, таких як `filter_var()` - - -Безпечна робота зі стовпцями -============================ - -У попередньому розділі ми показали, як правильно валідувати значення параметрів. Однак при використанні масивів у SQL-запитах ми повинні приділяти таку ж увагу і їхнім ключам. - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - не оброблені ключі в масиві -$database->query('INSERT INTO users', $_POST); -``` - -У командах INSERT та UPDATE це є критичною помилкою безпеки - зловмисник може вставити або змінити будь-який стовпець у базі даних. Він міг би, наприклад, встановити `is_admin = 1` або вставити будь-які дані в конфіденційні стовпці (так звана Mass Assignment Vulnerability). - -В умовах WHERE це ще небезпечніше, оскільки вони можуть містити оператори: - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - не оброблені ключі в масиві -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// виконає запит WHERE (`salary` > 100000) -``` - -Зловмисник може використати цей підхід для систематичного з'ясування зарплат співробітників. Наприклад, почне із запиту на зарплати понад 100 000, потім менше 50 000 і поступовим звуженням діапазону може виявити приблизні зарплати всіх співробітників. Цей тип атаки називається SQL enumeration. - -Методи `where()` та `whereOr()` є ще [набагато гнучкішими |explorer#where] і підтримують у ключах та значеннях SQL-вирази, включаючи оператори та функції. Це дає зловмиснику можливість здійснити SQL-ін'єкцію: - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - зловмисник може вставити власний SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// виконає запит WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Ця атака завершує початкову умову за допомогою `0)`, приєднує власний `SELECT` за допомогою `UNION` для отримання конфіденційних даних з таблиці `users` та закриває синтаксично правильний запит за допомогою `WHERE (1)`. - - -Білий список стовпців ---------------------- - -Для безпечної роботи з назвами стовпців нам потрібен механізм, який забезпечить, що користувач може працювати лише з дозволеними стовпцями і не може додати власні. Ми могли б спробувати виявляти та блокувати небезпечні назви стовпців (чорний список), але цей підхід ненадійний - зловмисник завжди може придумати новий спосіб записати небезпечну назву стовпця, який ми не передбачили. - -Тому набагато безпечніше змінити логіку і визначити явний список дозволених стовпців (білий список): - -```php -// Стовпці, які користувач може редагувати -$allowedColumns = ['name', 'email', 'active']; - -// Видалимо всі недозволені стовпці з вхідних даних -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Тепер можна безпечно використовувати в запитах, наприклад: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Динамічні ідентифікатори -======================== - -Для динамічних назв таблиць та стовпців використовуйте заповнювач `?name`. Він забезпечить правильне екранування ідентифікаторів відповідно до синтаксису даної бази даних (наприклад, за допомогою зворотних апострофів у MySQL): - -```php -// ✅ Безпечне використання довірених ідентифікаторів -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Результат у MySQL: SELECT `name` FROM `users` -``` - -Важливо: символ `?name` використовуйте лише для довірених значень, визначених у коді програми. Для значень від користувача використовуйте знову [білий список |#Білий список стовпців]. Інакше ви наражаєтеся на ризики безпеки: - -```php -// ❌ НЕБЕЗПЕЧНО - ніколи не використовуйте вхідні дані від користувача -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/uk/sql-way.texy b/database/uk/sql-way.texy deleted file mode 100644 index 3565ac16c4..0000000000 --- a/database/uk/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL підхід -********** - -.[perex] -Nette Database пропонує два шляхи: ви можете писати SQL-запити самостійно (SQL підхід), або дозволити генерувати їх автоматично (див. [Explorer |explorer]). SQL підхід дає вам повний контроль над запитами і при цьому забезпечує їх безпечне формування. - -.[note] -Деталі щодо підключення та конфігурації бази даних знайдете в розділі [Підключення та конфігурація |guide#Підключення та конфігурація]. - - -Базові запити -============= - -Для запитів до бази даних служить метод `query()`. Він повертає об'єкт [ResultSet |api:Nette\Database\ResultSet], який представляє результат запиту. У разі невдачі метод [викине виняток|exceptions]. Результат запиту можна перебирати за допомогою циклу `foreach`, або використати одну з [допоміжних функцій |#Отримання даних]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Для безпечного вставлення значень у SQL-запити використовуємо параметризовані запити. Nette Database робить їх максимально простими - достатньо після SQL-запиту додати кому та значення: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -При використанні кількох параметрів у вас є два варіанти запису. Ви можете "розбавляти" SQL-запит параметрами: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Або написати спочатку весь SQL-запит, а потім додати всі параметри: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Захист від SQL injection -======================== - -Чому важливо використовувати параметризовані запити? Тому що вони захищають вас від атаки під назвою SQL injection, під час якої зловмисник міг би підсунути власні SQL-команди і таким чином отримати або пошкодити дані в базі даних. - -.[warning] -**Ніколи не вставляйте змінні безпосередньо в SQL-запит!** Завжди використовуйте параметризовані запити, які захистять вас від SQL injection. - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Безпечний параметризований запит -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Ознайомтеся з [можливими ризиками безпеки |security]. - - -Техніки запитів -=============== - - -Умови WHERE ------------ - -Умови WHERE можна записати як асоціативний масив, де ключі - це назви стовпців, а значення - дані для порівняння. Nette Database автоматично вибере найбільш відповідний SQL-оператор залежно від типу значення. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -У ключі можна також явно вказати оператор для порівняння: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // використовує оператор > - 'name LIKE' => '%John%', // використовує оператор LIKE - 'email NOT LIKE' => '%example.com%', // використовує оператор NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette автоматично обробляє спеціальні випадки, такі як значення `null` або масиви. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // використовує оператор = - 'category_id' => [1, 2, 3], // використовує IN - 'description' => null, // використовує IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Для негативних умов використовуйте оператор `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // використовує оператор <> - 'category_id NOT' => [1, 2, 3], // використовує NOT IN - 'description NOT' => null, // використовує IS NOT NULL - 'id' => [], // пропускається -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Для об'єднання умов використовується оператор `AND`. Це можна змінити за допомогою [заповнювача ?or |#Підказки для побудови SQL]. - - -Правила ORDER BY ----------------- - -Сортування `ORDER BY` можна записати за допомогою масиву. У ключах вказуємо стовпці, а значенням буде boolean, що визначає, чи сортувати за зростанням: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // за зростанням - 'name' => false, // за спаданням -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Вставка даних (INSERT) ----------------------- - -Для вставки записів використовується SQL-команда `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Метод `getInsertId()` повертає ID останнього вставленого рядка. У деяких базах даних (наприклад, PostgreSQL) необхідно як параметр вказати назву послідовності, з якої має генеруватися ID, за допомогою `$database->getInsertId($sequenceId)`. - -Як параметри можна передавати і [#Спеціальні значення], такі як файли, об'єкти DateTime або перелічувані типи. - -Вставка кількох записів одночасно: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Багаторазовий INSERT набагато швидший, оскільки виконується єдиний запит до бази даних замість багатьох окремих. - -**Попередження щодо безпеки:** Ніколи не використовуйте як `$values` невалідовані дані. Ознайомтеся з [можливими ризиками |security#Безпечна робота зі стовпцями]. - - -Оновлення даних (UPDATE) ------------------------- - -Для оновлення записів використовується SQL-команда `UPDATE`. - -```php -// Оновлення одного запису -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Кількість зачеплених рядків поверне `$result->getRowCount()`. - -Для UPDATE можна використовувати оператори `+=` та `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // інкрементація login_count -], 1); -``` - -Приклад вставки або оновлення запису, якщо він вже існує. Використаємо техніку `ON DUPLICATE KEY UPDATE`: - -```php -$values = [ - 'name' => $name, - 'year' => $year, -]; -$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?', - $values + ['id' => $id], - $values, -); -// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) -// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 -``` - -Зверніть увагу, що Nette Database розпізнає, в якому контексті SQL-команди вставляється параметр з масивом, і відповідно до цього складає з нього SQL-код. Так, з першого масиву він склав `(id, name, year) VALUES (123, 'Jim', 1978)`, тоді як другий перетворив на вигляд `name = 'Jim', year = 1978`. Детальніше про це йдеться в розділі [#Підказки для побудови SQL]. - - -Видалення даних (DELETE) ------------------------- - -Для видалення записів використовується SQL-команда `DELETE`. Приклад з отриманням кількості видалених рядків: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Підказки для побудови SQL -------------------------- - -Підказка - це спеціальний заповнювач у SQL-запиті, який вказує, як значення параметра має бути перетворено на SQL-вираз: - -| Підказка | Опис | Автоматично використовується -|-----------|-------------------------------------------------|----------------------------- -| `?name` | використовується для вставки назви таблиці або стовпця | - -| `?values` | генерує `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | генерує присвоєння `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | об'єднує умови в масиві оператором `AND` | `WHERE ?`, `HAVING ?` -| `?or` | об'єднує умови в масиві оператором `OR` | - -| `?order` | генерує умову `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Для динамічного вставлення назв таблиць та стовпців у запит служить заповнювач `?name`. Nette Database подбає про правильну обробку ідентифікаторів відповідно до конвенцій даної бази даних (наприклад, взяття у зворотні лапки в MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (у MySQL) -``` - -**Попередження:** символ `?name` використовуйте лише для назв таблиць та стовпців з валідованих вхідних даних, інакше ви наражаєтеся на [ризик безпеки |security#Динамічні ідентифікатори]. - -Інші підказки зазвичай не потрібно вказувати, оскільки Nette використовує розумну автодетекцію при складанні SQL-запиту (див. третій стовпець таблиці). Але ви можете її використати, наприклад, у ситуації, коли хочете об'єднати умови за допомогою `OR` замість `AND`: - -```php -$database->query('SELECT * FROM users WHERE ?or', [ - 'name' => 'John', - 'email' => 'john@example.com', -]); -// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com' -``` - - -Спеціальні значення -------------------- - -Крім звичайних скалярних типів (string, int, bool), ви можете передавати як параметри і спеціальні значення: - -- файли: `fopen('image.gif', 'r')` вставить бінарний вміст файлу -- дата та час: об'єкти `DateTime` перетворяться на формат бази даних -- перелічувані типи: екземпляри `enum` перетворяться на їхнє значення -- SQL літерали: створені за допомогою `Connection::literal('NOW()')` вставляться безпосередньо в запит - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -У базах даних, які не мають нативної підтримки для типу даних `datetime` (як SQLite та Oracle), `DateTime` перетворюється на значення, визначене в [конфігурації бази даних|configuration] елементом `formatDateTime` (значення за замовчуванням - `U` - unix timestamp). - - -SQL літерали ------------- - -У деяких випадках потрібно вказати як значення безпосередньо SQL-код, який, однак, не повинен розглядатися як рядок і екрануватися. Для цього служать об'єкти класу `Nette\Database\SqlLiteral`. Їх створює метод `Connection::literal()`. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Або альтернативно: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL літерали можуть містити параметри: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > ? AND year < ?', $min, $max), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017) -``` - -Завдяки чому можна створювати цікаві комбінації: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('?or', [ - 'active' => true, - 'role' => $role, - ]), -]); -// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin') -``` - - -Отримання даних -=============== - - -Скорочення для SELECT-запитів ------------------------------ - -Для спрощення завантаження даних `Connection` пропонує кілька скорочень, які комбінують виклик `query()` з наступним `fetch*()`. Ці методи приймають ті самі параметри, що й `query()`, тобто SQL-запит та необов'язкові параметри. Повний опис методів `fetch*()` знайдете [нижче |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Виконує запит і повертає перший рядок як об'єкт `Row` -| `fetchAll($sql, ...$params): array` | Виконує запит і повертає всі рядки як масив об'єктів `Row` -| `fetchPairs($sql, ...$params): array` | Виконує запит і повертає асоціативний масив, де перший стовпець представляє ключ, а другий - значення -| `fetchField($sql, ...$params): mixed` | Виконує запит і повертає значення першого поля з першого рядка -| `fetchList($sql, ...$params): ?array` | Виконує запит і повертає перший рядок як індексований масив - -Приклад: - -```php -// fetchField() - повертає значення першої комірки -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - ітерація по рядках ------------------------------- - -Після виконання запиту повертається об'єкт [ResultSet|api:Nette\Database\ResultSet], який дозволяє перебирати результати кількома способами. Найпростіший спосіб виконати запит і отримати рядки - це ітерація в циклі `foreach`. Цей спосіб є найбільш економним з точки зору пам'яті, оскільки повертає дані поступово і не зберігає їх усі в пам'яті одночасно. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` можна ітерувати лише один раз. Якщо вам потрібно ітерувати повторно, ви повинні спочатку завантажити дані в масив, наприклад, за допомогою методу `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Повертає рядок як об'єкт `Row`. Якщо більше немає рядків, повертає `null`. Пересуває внутрішній вказівник на наступний рядок. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // читає перший рядок -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Повертає всі рядки, що залишилися, з `ResultSet` як масив об'єктів `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // читає всі рядки -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Повертає результати як асоціативний масив. Перший аргумент визначає назву стовпця, який буде використаний як ключ у масиві, другий аргумент визначає назву стовпця, який буде використаний як значення: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Якщо вказати лише перший параметр, значенням буде весь рядок, тобто об'єкт `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -У разі дублювання ключів використовується значення з останнього рядка. При використанні `null` як ключа масив буде індексовано нумерично з нуля (тоді колізій не виникає): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Альтернативно, ви можете вказати як параметр callback, який для кожного рядка повертатиме або саме значення, або пару ключ-значення. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback також може повертати масив із парою ключ & значення: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Повертає значення першого поля з поточного рядка. Якщо більше немає рядків, повертає `null`. Пересуває внутрішній вказівник на наступний рядок. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // читає ім'я з першого рядка -``` - - -fetchList(): ?array .[method] ------------------------------ - -Повертає рядок як індексований масив. Якщо більше немає рядків, повертає `null`. Пересуває внутрішній вказівник на наступний рядок. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Повертає кількість зачеплених рядків останнім запитом `UPDATE` або `DELETE`. Для `SELECT` це кількість повернутих рядків, але вона може бути невідомою - у такому випадку метод поверне `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Повертає кількість стовпців у `ResultSet`. - - -Інформація про запити -===================== - -Для цілей налагодження ми можемо отримати інформацію про останній виконаний запит: - -```php -echo $database->getLastQueryString(); // виводить SQL-запит - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // виводить SQL-запит -echo $result->getTime(); // виводить час виконання в секундах -``` - -Для відображення результату у вигляді HTML-таблиці можна використати: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet пропонує інформацію про типи стовпців: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column має тип $type->type"; // напр. 'id має тип int' -} -``` - - -Логування запитів ------------------ - -Ми можемо реалізувати власне логування запитів. Подія `onQuery` - це масив callback'ів, які викликаються після кожного виконаного запиту: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Запит: ' . $result->getQueryString()); - $logger->info('Час: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Великий набір результатів: ' . $result->getRowCount() . ' рядків'); - } -}; -``` diff --git a/database/uk/transactions.texy b/database/uk/transactions.texy deleted file mode 100644 index 57053362e7..0000000000 --- a/database/uk/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Транзакції -********** - -.[perex] -Транзакції гарантують, що або всі операції в рамках транзакції будуть виконані, або жодна з них. Вони корисні для забезпечення узгодженості даних під час складних операцій. - -Найпростіший спосіб використання транзакцій виглядає так: - -```php -$database->beginTransaction(); -try { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); - $database->commit(); -} catch (\Exception $e) { - $database->rollBack(); - throw $e; -} -``` - -Набагато елегантніше те саме можна записати за допомогою методу `transaction()`. Він приймає як параметр callback, який виконується в транзакції. Якщо callback завершується без винятку, транзакція автоматично підтверджується. Якщо виникає виняток, транзакція скасовується (rollback), а виняток поширюється далі. - -```php -$database->transaction(function ($database) use ($id) { - $database->query('DELETE FROM articles WHERE id = ?', $id); - $database->query('INSERT INTO audit_log', [ - 'article_id' => $id, - 'action' => 'delete' - ]); -}); -``` - -Метод `transaction()` також може повертати значення: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // повертає кількість оновлених рядків -}); -``` diff --git a/dependency-injection/bg/@home.texy b/dependency-injection/bg/@home.texy deleted file mode 100644 index 6e1130be6c..0000000000 --- a/dependency-injection/bg/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection е дизайн патърн, който коренно ще промени вашия поглед върху кода и разработката. Ще ви отвори пътя към света на чисто проектирани и устойчиви приложения. - -- [Какво е Dependency Injection? |introduction] -- [Глобално състояние и сингълтони |global-state] -- [Предаване на зависимости |passing-dependencies] -- [Какво е DI контейнер? |container] -- [Често задавани въпроси|faq] - - -Пакетът `nette/di` предоставя изключително усъвършенстван компилиран DI контейнер за PHP. - -- [Nette DI Container |nette-container] -- [Конфигурация |configuration] -- [Дефиниране на сървиси |services] -- [Autowiring |autowiring] -- [Генерирани фабрики |factory] -- [Създаване на разширения за Nette DI|extensions] diff --git a/dependency-injection/bg/@left-menu.texy b/dependency-injection/bg/@left-menu.texy deleted file mode 100644 index 77e92a85f8..0000000000 --- a/dependency-injection/bg/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Какво е DI? |introduction] -- [Глобално състояние и сингълтони |global-state] -- [Предаване на зависимости |passing-dependencies] -- [Какво е DI контейнер? |container] -- [Често задавани въпроси|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Конфигурация |configuration] -- [Дефиниране на сървиси |services] -- [Autowiring |autowiring] -- [Генерирани фабрики |factory] -- [Създаване на разширения за Nette DI|extensions] diff --git a/dependency-injection/bg/@meta.texy b/dependency-injection/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/dependency-injection/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/dependency-injection/bg/autowiring.texy b/dependency-injection/bg/autowiring.texy deleted file mode 100644 index c5ce14c62f..0000000000 --- a/dependency-injection/bg/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring е страхотна функция, която може автоматично да предава необходимите сървиси към конструктора и други методи, така че изобщо не е необходимо да ги пишем. Ще ви спести много време. - -Благодарение на това можем да пропуснем по-голямата част от аргументите при писане на дефиниции на сървиси. Вместо: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Достатъчно е да напишете: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring се ръководи от типовете, така че за да работи, класът `ArticleRepository` трябва да бъде дефиниран приблизително така: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -За да може да се използва autowiring, за всеки тип трябва да има **точно един сървис** в контейнера. Ако има повече, autowiring няма да знае кой от тях да предаде и ще хвърли изключение: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, отговарят и mainDb, и tempDb -``` - -Решението би било или да се заобиколи autowiring и изрично да се посочи името на сървиса (т.е. `articles: Model\ArticleRepository(@mainDb)`). По-удобно обаче е autowiring-ът на един от сървисите да се [изключи |#Изключване на autowiring] или първият сървис да се [предпочете |#Предпочитание за autowiring]. - - -Изключване на autowiring ------------------------- - -Можем да изключим autowiring-а на сървис с помощта на опцията `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # сървисът tempDb е изключен от autowiring - - articles: Model\ArticleRepository # следователно предава mainDb на конструктора -``` - -Сървисът `articles` няма да хвърли изключение, че съществуват два подходящи сървиса от тип `PDO` (т.е. `mainDb` и `tempDb`), които могат да бъдат предадени на конструктора, защото вижда само сървиса `mainDb`. - -.[note] -Конфигурацията на autowiring в Nette работи различно от тази в Symfony, където опцията `autowire: false` указва, че autowiring не трябва да се използва за аргументите на конструктора на дадения сървис. В Nette autowiring се използва винаги, независимо дали за аргументите на конструктора, или за които и да било други методи. Опцията `autowired: false` указва, че инстанцията на дадения сървис не трябва да бъде предавана никъде чрез autowiring. - - -Предпочитание за autowiring ---------------------------- - -Ако имаме няколко сървиса от един и същи тип и за един от тях посочим опцията `autowired`, този сървис става предпочитан: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # става предпочитан - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Сървисът `articles` няма да хвърли изключение, че съществуват два подходящи сървиса от тип `PDO` (т.е. `mainDb` и `tempDb`), а ще използва предпочитания сървис, т.е. `mainDb`. - - -Масив от сървиси ----------------- - -Autowiring може да предава и масиви от сървиси от определен тип. Тъй като в PHP не може нативно да се запише типът на елементите на масива, е необходимо освен типа `array` да се добави и phpDoc коментар с типа на елемента във формата `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -След това DI контейнерът автоматично предава масив от сървиси, съответстващи на дадения тип. Пропуска сървисите, които имат изключен autowiring. - -Типът в коментара може да бъде също във формата `array<int, Class>` или `list<Class>`. Ако не можете да повлияете на формата на phpDoc коментара, можете да предадете масива от сървиси директно в конфигурацията с помощта на [`typed()` |services#Специални функции]. - - -Скаларни аргументи ------------------- - -Autowiring може да инжектира само обекти и масиви от обекти. Скаларните аргументи (напр. низове, числа, булеви стойности) [се записват в конфигурацията |services#Аргументи]. Алтернатива е да се създаде [settings-обект |best-practices:passing-settings-to-presenters], който капсулира скаларната стойност (или няколко стойности) под формата на обект, и той след това може отново да се предава чрез autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly може да се използва от PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Създавате сървис от него, като го добавите към конфигурацията: - -```neon -services: - - MySettings('any value') -``` - -След това всички класове го изискват чрез autowiring. - - -Стесняване на autowiring ------------------------- - -За отделни сървиси autowiring може да бъде стеснен само до определени класове или интерфейси. - -Обикновено autowiring предава сървиса на всеки параметър на метод, чийто тип съответства на сървиса. Стесняването означава, че задаваме условия, на които трябва да отговарят типовете, посочени в параметрите на методите, за да им бъде предаден сървисът. - -Ще го покажем с пример: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Ако ги регистрираме всички като сървиси, autowiring ще се провали: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, отговарят сървисите parent и child - childDep: ChildDependent # autowiring предава сървиса child на конструктора -``` - -Сървисът `parentDep` ще хвърли изключение `Multiple services of type ParentClass found: parent, child`, тъй като и двата сървиса `parent` и `child` отговарят на конструктора му, и autowiring не може да реши кой от тях да избере. - -Затова можем да стесним autowiring-а на сървиса `child` до тип `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # може да се напише и 'autowired: self' - - parentDep: ParentDependent # autowiring предава сървиса parent на конструктора - childDep: ChildDependent # autowiring предава сървиса child на конструктора -``` - -Сега на конструктора на сървиса `parentDep` се предава сървисът `parent`, защото сега той е единственият подходящ обект. Autowiring вече не предава сървиса `child` там. Да, сървисът `child` все още е от тип `ParentClass`, но стесняващото условие, зададено за типа на параметъра, вече не е валидно, т.е. не е вярно, че `ParentClass` *е надтип на* `ChildClass`. - -При сървиса `child` би било възможно `autowired: ChildClass` да се запише и като `autowired: self`, тъй като `self` е заместващо означение за класа на текущия сървис. - -В ключа `autowired` е възможно да се посочат и няколко класа или интерфейса като масив: - -```neon -autowired: [BarClass, FooInterface] -``` - -Нека допълним примера и с интерфейси: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Ако не ограничим сървиса `child` по никакъв начин, той ще пасне на конструкторите на всички класове `FooDependent`, `BarDependent`, `ParentDependent` и `ChildDependent` и autowiring ще го предаде там. - -Но ако стесним неговия autowiring до `ChildClass` с помощта на `autowired: ChildClass` (или `self`), autowiring ще го предаде само на конструктора на `ChildDependent`, тъй като той изисква аргумент от тип `ChildClass` и е вярно, че `ChildClass` *е от тип* `ChildClass`. Никой друг тип, посочен в другите параметри, не е надтип на `ChildClass`, така че сървисът не се предава. - -Ако го ограничим до `ParentClass` с помощта на `autowired: ParentClass`, autowiring отново ще го предаде на конструктора на `ChildDependent` (тъй като изискваният `ChildClass` е надтип на `ParentClass`), а също и на конструктора на `ParentDependent`, тъй като изискваният тип `ParentClass` също е подходящ. - -Ако го ограничим до `FooInterface`, той все още ще бъде автоматично инжектиран в `ParentDependent` (изискваният `ParentClass` е надтип на `FooInterface`) и `ChildDependent`, но освен това и в конструктора на `FooDependent`, но не и в `BarDependent`, тъй като `BarInterface` не е надтип на `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring предава child на конструктора - barDep: BarDependent # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, нито един сървис не отговаря - parentDep: ParentDependent # autowiring предава child на конструктора - childDep: ChildDependent # autowiring предава child на конструктора -``` diff --git a/dependency-injection/bg/configuration.texy b/dependency-injection/bg/configuration.texy deleted file mode 100644 index 3f6bf580ca..0000000000 --- a/dependency-injection/bg/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Конфигурация на DI контейнера -***************************** - -.[perex] -Преглед на опциите за конфигурация на Nette DI контейнера. - - -Конфигурационен файл -==================== - -Nette DI контейнерът се управлява лесно с помощта на конфигурационни файлове. Те обикновено се записват във [формат NEON|neon:format]. За редактиране препоръчваме [редактори с поддръжка |best-practices:editors-and-tools#IDE редактор] на този формат. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Декоратор .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI контейнер .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Разширения]: "Инсталиране на други DI разширения .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Включване на файлове]: "Включване на файлове .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Параметри]: "Параметри .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Автоматично регистриране на сървиси .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Сървиси .[prism-token prism-comment]" -</pre> - -.[note] -Ако искате да напишете низ, съдържащ знака `%`, трябва да го екранирате, като го удвоите на `%%`. - - -Параметри -========= - -В конфигурацията можете да дефинирате параметри, които след това могат да се използват като част от дефинициите на сървисите. Това може да направи конфигурацията по-ясна или да обедини и изолира стойности, които ще се променят. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Към параметъра `dsn` се обръщаме навсякъде в конфигурацията, като напишем `%dsn%`. Параметрите могат да се използват и в низове като `'%wwwDir%/images'`. - -Параметрите не трябва да бъдат само низове или числа, те могат да съдържат и масиви: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Към конкретен ключ се обръщаме като `%mailer.user%`. - -Ако трябва да разберете стойността на който и да е параметър във вашия код, например в клас, предайте го на този клас. Например в конструктора. Няма глобален обект, представляващ конфигурацията, към който класовете да се обръщат за стойности на параметри. Това би било нарушение на принципа на dependency injection. - - -Сървиси -======= - -Вижте [отделна глава|services]. - - -Decorator -========= - -Как да модифицирате масово всички сървиси от определен тип? Например, да извикате определен метод за всички презентери, които наследяват от конкретен общ предшественик? За това служи декораторът. - -```neon -decorator: - # за всички сървиси, които са инстанции на този клас или интерфейс - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # извикайте този метод - - $absoluteUrls = true # и задайте променливата -``` - -Decorator може да се използва и за задаване на [тагове |services#Тагове] или за активиране на режим [inject |services#Режим Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Технически настройки на DI контейнера. - -```neon -di: - # показва ли се DIC в Tracy Bar? - debugger: ... # (bool) по подразбиране е true - - # типове параметри, които никога да не се autowire-ват - excluded: ... # (string[]) - - # разрешава ли се lazy създаване на сървиси? - lazy: ... # (bool) по подразбиране е false - - # клас, от който наследява DI контейнерът - parentClass: ... # (string) по подразбиране е Nette\DI\Container -``` - - -Lazy сървиси .{data-version:3.2.4} ----------------------------------- - -Настройката `lazy: true` активира lazy (отложено) създаване на сървиси. Това означава, че сървисите не се създават реално в момента, в който ги поискаме от DI контейнера, а едва в момента на първото им използване. Това може да ускори стартирането на приложението и да намали изискванията за памет, тъй като се създават само тези сървиси, които са действително необходими в дадена заявка. - -За конкретен сървис lazy създаването може да бъде [променено |services#Lazy сървиси]. - -.[note] -Lazy обектите могат да се използват само за потребителски класове, а не за вътрешни PHP класове. Изисква PHP 8.4 или по-нова версия. - - -Експортиране на метаданни -------------------------- - -Класът на DI контейнера съдържа и много метаданни. Можете да го намалите, като редуцирате експорта на метаданни. - -```neon -di: - export: - # експортиране на параметри? - parameters: false # (bool) по подразбиране е true - - # експортиране на тагове и кои? - tags: # (string[]|bool) по подразбиране са всички - - event.subscriber - - # експортиране на данни за autowiring и кои? - types: # (string[]|bool) по подразбиране са всички - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Ако не използвате масива `$container->getParameters()`, можете да изключите експорта на параметри. Освен това можете да експортирате само тези тагове, чрез които получавате сървиси с метода `$container->findByTag(...)`. Ако изобщо не извиквате метода, можете напълно да изключите експорта на тагове с `false`. - -Можете значително да намалите метаданните за [autowiring |autowiring] , като посочите класовете, които използвате като параметър на метода `$container->getByType()`. И отново, ако изобщо не извиквате метода (или само в [bootstrap|application:bootstrapping], за да получите `Nette\Application\Application`), можете напълно да изключите експорта с `false`. - - -Разширения -========== - -Регистриране на други DI разширения. По този начин добавяме например DI разширението `Dibi\Bridges\Nette\DibiExtension22` под името `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -След това го конфигурираме в секцията `dibi`: - -```neon -dibi: - host: localhost -``` - -Като разширение може да се добави и клас, който има параметри: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Включване на файлове -==================== - -Можем да включим други конфигурационни файлове в секцията `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Името `parameters.php` не е печатна грешка, конфигурацията може да бъде записана и в PHP файл, който я връща като масив: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Ако в конфигурационните файлове се появят елементи с еднакви ключове, те ще бъдат презаписани или, в случай на [масиви, слети |#Сливане]. Файлът, включен по-късно, има по-висок приоритет от предишния. Файлът, в който е посочена секцията `includes`, има по-висок приоритет от файловете, включени в него. - - -Search -====== - -Автоматичното добавяне на сървиси към DI контейнера прави работата изключително приятна. Nette автоматично добавя презентери към контейнера, но можете лесно да добавяте и всякакви други класове. - -Достатъчно е да посочите в кои директории (и поддиректории) да търси класове: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Обикновено обаче не искаме да добавяме абсолютно всички класове и интерфейси, така че можем да ги филтрираме: - -```neon -search: - - in: %appDir%/Forms - - # филтриране по име на файл (string|string[]) - files: - - *Factory.php - - # филтриране по име на клас (string|string[]) - classes: - - *Factory -``` - -Или можем да изберем класове, които наследяват или имплементират поне един от изброените класове: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Могат да се дефинират и изключващи правила, т.е. маски на имена на класове или наследствени предци, които, ако съвпадат, сървисът няма да бъде добавен към DI контейнера: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -На всички сървиси могат да се зададат тагове: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Сливане -======= - -Ако в няколко конфигурационни файла се появят елементи с еднакви ключове, те ще бъдат презаписани или, в случай на масиви, слети. Файлът, включен по-късно, има по-висок приоритет от предишния. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>резултат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -При масивите сливането може да бъде предотвратено чрез добавяне на удивителен знак след името на ключа: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>резултат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Конфигурация на Dependency Injection}} diff --git a/dependency-injection/bg/container.texy b/dependency-injection/bg/container.texy deleted file mode 100644 index dc3ae58a36..0000000000 --- a/dependency-injection/bg/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Какво е DI контейнер? -********************* - -.[perex] -Dependency injection контейнерът (DIC) е клас, който може да инстанцира и конфигурира обекти. - -Може да ви изненада, но в много случаи не се нуждаете от dependency injection контейнер, за да се възползвате от предимствата на dependency injection (накратко DI). В края на краищата, дори в [уводната глава|introduction] показахме DI с конкретни примери и не беше необходим контейнер. - -Въпреки това, ако трябва да управлявате голям брой различни обекти с много зависимости, dependency injection контейнерът ще бъде наистина полезен. Такъв е случаят например с уеб приложения, изградени върху framework. - -В предишната глава представихме класовете `Article` и `UserController`. И двата имат някои зависимости, а именно база данни и фабриката `ArticleFactory`. И сега ще създадем контейнер за тези класове. Разбира се, за толкова прост пример няма смисъл да имаме контейнер. Но ще го създадем, за да покажем как изглежда и работи. - -Ето един прост hardcoded контейнер за дадения пример: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -Използването би изглеждало така: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Просто питаме контейнера за обект и вече не е нужно да знаем нищо за това как да го създадем или какви са неговите зависимости; контейнерът знае всичко това. Зависимостите се инжектират автоматично от контейнера. В това е неговата сила. - -Засега контейнерът има всички данни, записани hardcoded. Така че ще направим следващата стъпка и ще добавим параметри, за да направим контейнера наистина полезен: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Наблюдателните читатели може би са забелязали определен проблем. Всеки път, когато получа обект `UserController`, се създава и нова инстанция на `ArticleFactory` и базата данни. Определено не искаме това. - -Затова ще добавим метод `getService()`, който винаги ще връща едни и същи инстанции: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') ще извика createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -При първото извикване, например `$container->getService('Database')`, той ще накара `createDatabase()` да създаде обект на базата данни, ще го съхрани в масива `$services` и ще го върне директно при следващото извикване. - -Ще модифицираме и останалата част от контейнера, за да използва `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Между другото, терминът сървис се отнася до всеки обект, управляван от контейнера. Оттук и името на метода `getService()`. - -Готово. Имаме напълно функционален DI контейнер! И можем да го използваме: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Както виждате, написването на DIC не е сложно. Струва си да се отбележи, че самите обекти не знаят, че се създават от някакъв контейнер. Следователно е възможно да се създаде по този начин всеки PHP обект, без да се променя неговият изходен код. - -Ръчното създаване и поддръжка на клас контейнер може бързо да се превърне в кошмар. Затова в следващата глава ще говорим за [Nette DI Container|nette-container], който може да се генерира и актуализира почти сам. - - -{{maintitle: Какво е dependency injection контейнер?}} diff --git a/dependency-injection/bg/extensions.texy b/dependency-injection/bg/extensions.texy deleted file mode 100644 index d38bb6e373..0000000000 --- a/dependency-injection/bg/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Създаване на разширения за Nette DI -*********************************** - -.[perex] -Генерирането на DI контейнера, освен от конфигурационните файлове, се влияе и от така наречените *разширения*. Активираме ги в конфигурационния файл в секцията `extensions`. - -По този начин добавяме разширение, представено от класа `BlogExtension`, под името `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Всяко разширение на компилатора наследява от [api:Nette\DI\CompilerExtension] и може да имплементира следните методи, които се извикват последователно по време на изграждането на DI контейнера: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Този метод се извиква пръв. Той дефинира схема за валидиране на конфигурационните параметри. - -Конфигурираме разширението в секция, чието име е същото като това, под което е добавено разширението, т.е. `blog`: - -```neon -# същото име като разширението -blog: - postsPerPage: 10 - allowComments: false -``` - -Създаваме схема, описваща всички опции за конфигурация, включително техните типове, разрешени стойности и евентуално стойности по подразбиране: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -Документацията можете да намерите на страницата [Schema |schema:]. Освен това можете да посочите кои опции могат да бъдат [динамични |application:bootstrapping#Динамични параметри] с помощта на `dynamic()`, напр. `Expect::int()->dynamic()`. - -Достъпваме конфигурацията чрез променливата `$this->config`, която е обект `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Използва се за добавяне на сървиси към контейнера. За това служи [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // или setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Конвенцията е сървисите, добавени от разширение, да се префиксират с неговото име, за да се избегнат конфликти на имена. Това прави методът `prefix()`, така че ако разширението се нарича `blog`, сървисът ще носи името `blog.articles`. - -Ако трябва да преименуваме сървис, можем да създадем псевдоним с оригиналното име, за да запазим обратната съвместимост. Nette прави нещо подобно, например със сървиса `routing.router`, който е достъпен и под предишното име `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Зареждане на сървиси от файл ----------------------------- - -Не е необходимо да създаваме сървиси само с помощта на API на класа ContainerBuilder, но и с познатия синтаксис, използван в конфигурационния файл NEON в секцията services. Префиксът `@extension` представлява текущото разширение. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Зареждаме сървисите: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // зареждане на конфигурационния файл за разширението - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Методът се извиква, когато контейнерът съдържа всички сървиси, добавени от отделните разширения в методите `loadConfiguration`, както и от потребителските конфигурационни файлове. Следователно на този етап от изграждането можем да модифицираме дефинициите на сървисите или да добавим връзки между тях. За търсене на сървиси в контейнера по тагове може да се използва методът `findByTag()`, а по клас или интерфейс - методът `findByType()`. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -На този етап класът на контейнера вече е генериран под формата на обект [ClassType |php-generator:#Класове], съдържа всички методи, които създават сървиси, и е готов за запис в кеша. Все още можем да модифицираме получения код на класа на този етап. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Класът Configurator, след [създаване на контейнера |application:bootstrapping#index.php], извиква инициализационен код, който се създава чрез запис в обекта `$this->initialization` с помощта на [метода addBody() |php-generator:#Тела на методи и функции]. - -Ще покажем пример как да стартирате сесия или да стартирате сървиси, които имат таг `run`, с помощта на инициализационен код: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // автоматично стартиране на сесията - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // сървисите с таг run трябва да бъдат създадени след инстанциране на контейнера - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/bg/factory.texy b/dependency-injection/bg/factory.texy deleted file mode 100644 index eec7b3abf4..0000000000 --- a/dependency-injection/bg/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Генерирани фабрики -****************** - -.[perex] -Nette DI може автоматично да генерира код на фабрики въз основа на интерфейси, което ви спестява писане на код. - -Фабриката е клас, който произвежда и конфигурира обекти. Следователно тя им предава и техните зависимости. Моля, не бъркайте с дизайн патърна *factory method*, който описва специфичен начин за използване на фабрики и не е свързан с тази тема. - -Как изглежда такава фабрика, показахме в [уводната глава |introduction#Фабрика]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI може автоматично да генерира код на фабрики. Всичко, което трябва да направите, е да създадете интерфейс и Nette DI ще генерира имплементацията. Интерфейсът трябва да има точно един метод с име `create` и да декларира тип на връщане: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Така фабриката `ArticleFactory` има метод `create`, който създава обекти `Article`. Класът `Article` може да изглежда например така: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Добавяме фабриката към конфигурационния файл: - -```neon -services: - - ArticleFactory -``` - -Nette DI ще генерира съответната имплементация на фабриката. - -В кода, който използва фабриката, изискваме обект по интерфейс и Nette DI ще използва генерираната имплементация: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // оставяме фабриката да създаде обект - $article = $this->articleFactory->create(); - } -} -``` - - -Параметризирана фабрика -======================= - -Фабричният метод `create` може да приема параметри, които след това предава на конструктора. Нека добавим например ID на автора на статията към класа `Article`: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Добавяме параметъра и към фабриката: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Тъй като параметърът в конструктора и параметърът във фабриката имат едно и също име, Nette DI ги предава напълно автоматично. - - -Разширена дефиниция -=================== - -Дефиницията може да бъде записана и в многоредов вид, като се използва ключът `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -При писане по този по-дълъг начин е възможно да се посочат допълнителни аргументи за конструктора в ключа `arguments` и допълнителна конфигурация с помощта на `setup`, точно както при обикновените сървиси. - -Пример: ако методът `create()` не приемаше параметъра `$authorId`, бихме могли да посочим фиксирана стойност в конфигурацията, която да бъде предадена на конструктора на `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Или обратно, ако `create()` приемаше параметъра `$authorId`, но той не беше част от конструктора и се предаваше чрез метода `Article::setAuthorId()`, щяхме да се обърнем към него в секцията `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Освен фабрики, Nette може да генерира и т.нар. аксесори. Това са обекти с метод `get()`, който връща определен сървис от DI контейнера. Повторното извикване на `get()` винаги връща същата инстанция. - -Аксесорите осигуряват lazy-loading за зависимостите. Да приемем, че имаме клас, който записва грешки в специална база данни. Ако този клас получаваше връзката с базата данни като зависимост чрез конструктора, връзката винаги трябваше да се създава, въпреки че на практика грешка се появява само рядко и следователно връзката в повечето случаи би останала неизползвана. Вместо това класът получава аксесор и едва когато се извика неговият `get()`, се създава обектът на базата данни: - -Как да създадем аксесор? Просто напишете интерфейс и Nette DI ще генерира имплементацията. Интерфейсът трябва да има точно един метод с име `get` и да декларира тип на връщане: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Добавяме аксесора към конфигурационния файл, където е и дефиницията на сървиса, който той ще връща: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Тъй като аксесорът връща сървис от тип `PDO` и в конфигурацията има само един такъв сървис, той ще върне точно него. Ако имаше повече сървиси от този тип, щяхме да посочим връщания сървис по име, напр. `- PDOAccessor(@db1)`. - - -Множествена фабрика/аксесор -=========================== -Досега нашите фабрики и аксесори винаги са можели да произвеждат или връщат само един обект. Въпреки това е много лесно да се създадат и множествени фабрики, комбинирани с аксесори. Интерфейсът на такъв клас ще съдържа произволен брой методи с имена `create<name>()` и `get<name>()`, напр.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Така че, вместо да предаваме няколко генерирани фабрики и аксесори, предаваме една по-сложна фабрика, която може да прави повече неща. - -Алтернативно, вместо няколко метода, може да се използва `get()` с параметър: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Тогава `MultiFactory::getArticle()` прави същото като `MultiFactoryAlt::get('article')`. Въпреки това, алтернативният запис има недостатъка, че не е ясно кои стойности на `$name` се поддържат и логично не е възможно да се разграничат различни върнати стойности за различни `$name` в интерфейса. - - -Дефиниция чрез списък ---------------------- -По този начин може да се дефинира множествена фабрика в конфигурацията: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # дефинира createArticle() - db: PDO(%dsn%, %user%, %password%) # дефинира getDb() - ) -``` - -Или можем да се обърнем към съществуващи сървиси в дефиницията на фабриката чрез референция: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # дефинира createArticle() - db: @\PDO # дефинира getDb() - ) -``` - - -Дефиниция с помощта на тагове ------------------------------ - -Втората възможност е да се използват [тагове |services#Тагове] за дефиницията: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/bg/faq.texy b/dependency-injection/bg/faq.texy deleted file mode 100644 index 9c77b0168d..0000000000 --- a/dependency-injection/bg/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Често задавани въпроси за DI (FAQ) -********************************** - - -DI ли е друго име за IoC? -------------------------- - -*Inversion of Control* (IoC) е принцип, фокусиран върху начина, по който се изпълнява кодът - дали вашият код изпълнява чужд код, или вашият код е интегриран в чужд код, който след това го извиква. IoC е широк термин, обхващащ [събития |nette:glossary#Събития events], така наречения [Холивудски принцип |application:components#Hollywood style] и други аспекти. Част от тази концепция са и фабриките, за които се говори в [Правило № 3: оставете го на фабриката |introduction#Правило 3: Остави го на фабриката], и които представляват инверсия за оператора `new`. - -*Dependency Injection* (DI) се фокусира върху начина, по който един обект научава за друг обект, т.е. за неговите зависимости. Това е дизайн патърн, който изисква изрично предаване на зависимости между обектите. - -Следователно може да се каже, че DI е специфична форма на IoC. Въпреки това, не всички форми на IoC са подходящи от гледна точка на чистотата на кода. Например, анти-патърните включват техники, които работят с [глобално състояние |global-state] или така наречения [Service Locator |#Какво е Service Locator]. - - -Какво е Service Locator? ------------------------- - -Това е алтернатива на Dependency Injection. Работи, като създава централно хранилище, където са регистрирани всички налични сървиси или зависимости. Когато обект се нуждае от зависимост, той я иска от Service Locator. - -В сравнение с Dependency Injection обаче, той губи прозрачност: зависимостите не се предават директно на обектите и не са толкова лесно идентифицируеми, което изисква преглед на кода, за да се разкрият и разберат всички връзки. Тестването също е по-сложно, тъй като не можем просто да предаваме mock обекти на тестваните обекти, а трябва да го правим чрез Service Locator. Освен това, Service Locator нарушава дизайна на кода, тъй като отделните обекти трябва да знаят за неговото съществуване, което е различно от Dependency Injection, където обектите нямат представа за DI контейнера. - - -Кога е по-добре да не се използва DI? -------------------------------------- - -Не са известни трудности, свързани с използването на дизайн патърна Dependency Injection. Напротив, получаването на зависимости от глобално достъпни места води до [цяла поредица от усложнения |global-state], както и използването на Service Locator. Затова е препоръчително винаги да се използва DI. Това не е догматичен подход, а просто не е намерена по-добра алтернатива. - -Въпреки това съществуват определени ситуации, в които не предаваме обекти, а ги получаваме от глобалното пространство. Например, при дебъгване на код, когато трябва да изведете стойността на променлива в определена точка от програмата, да измерите продължителността на определена част от програмата или да запишете съобщение. В такива случаи, когато става въпрос за временни действия, които по-късно ще бъдат премахнати от кода, е легитимно да се използва глобално достъпен dumper, хронометър или logger. Тези инструменти всъщност не принадлежат към дизайна на кода. - - -Има ли използването на DI своите недостатъци? ---------------------------------------------- - -Носи ли използването на Dependency Injection някакви недостатъци, като например повишена сложност при писане на код или влошена производителност? Какво губим, когато започнем да пишем код в съответствие с DI? - -DI не влияе на производителността или изискванията за памет на приложението. Производителността на DI Container-а може да играе известна роля, но в случая на [Nette DI |nette-container], контейнерът се компилира в чист PHP, така че неговата режия по време на изпълнение на приложението е практически нулева. - -При писане на код често е необходимо да се създават конструктори, приемащи зависимости. Преди това можеше да бъде досадно, но благодарение на модерните IDE и [constructor property promotion |https://blog.nette.org/bg/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], сега това е въпрос на няколко секунди. Фабриките могат лесно да се генерират с помощта на Nette DI и плъгин за PhpStorm с едно кликване на мишката. От друга страна, отпада необходимостта от писане на сингълтъни и статични точки за достъп. - -Може да се каже, че правилно проектирано приложение, използващо DI, не е нито по-кратко, нито по-дълго в сравнение с приложение, използващо сингълтъни. Частите от кода, работещи със зависимости, са просто извадени от отделните класове и преместени на нови места, т.е. в DI контейнера и фабриките. - - -Как да пренапишем legacy приложение към DI? -------------------------------------------- - -Преходът от legacy приложение към Dependency Injection може да бъде предизвикателен процес, особено при големи и сложни приложения. Важно е да се подходи към този процес систематично. - -- При преминаване към Dependency Injection е важно всички членове на екипа да разбират принципите и процедурите, които се използват. -- Първо, направете анализ на съществуващото приложение и идентифицирайте ключовите компоненти и техните зависимости. Създайте план кои части ще бъдат рефакторирани и в какъв ред. -- Имплементирайте DI контейнер или още по-добре, използвайте съществуваща библиотека, например Nette DI. -- Постепенно рефакторирайте отделните части на приложението, за да използват Dependency Injection. Това може да включва промени в конструкторите или методите, така че да приемат зависимости като параметри. -- Модифицирайте местата в кода, където се създават обекти със зависимости, така че вместо това зависимостите да се инжектират от контейнера. Това може да включва използването на фабрики. - -Помнете, че преходът към Dependency Injection е инвестиция в качеството на кода и дългосрочната поддръжка на приложението. Въпреки че може да е предизвикателство да се направят тези промени, резултатът трябва да бъде по-чист, по-модулен и лесно тестваем код, който е готов за бъдещи разширения и поддръжка. - - -Защо се предпочита композиция пред наследяването? -------------------------------------------------- -По-подходящо е да се използва [композиция |nette:introduction-to-object-oriented-programming#Композиция] вместо [наследяване |nette:introduction-to-object-oriented-programming#Наследяване], тъй като тя служи за повторно използване на код, без да се налага да се притесняваме за последствията от промените. Следователно тя осигурява по-слаба връзка, при която не трябва да се притесняваме, че промяната на някой код ще доведе до необходимост от промяна на друг зависим код. Типичен пример е ситуацията, наречена [constructor hell |passing-dependencies#Адът на конструктора]. - - -Може ли да се използва Nette DI Container извън Nette? ------------------------------------------------------- - -Определено. Nette DI Container е част от Nette, но е проектиран като самостоятелна библиотека, която може да се използва независимо от другите части на framework-а. Просто го инсталирайте с помощта на Composer, създайте конфигурационен файл с дефиницията на вашите сървиси и след това използвайте няколко реда PHP код, за да създадете DI контейнера. И веднага можете да започнете да се възползвате от предимствата на Dependency Injection във вашите проекти. - -Как изглежда конкретното използване, включително кодове, е описано в главата [Nette DI Container |nette-container]. - - -Защо е конфигурацията в NEON файлове? -------------------------------------- - -NEON е прост и лесен за четене конфигурационен език, разработен в рамките на Nette за настройка на приложения, сървиси и техните зависимости. В сравнение с JSON или YAML, той предлага много по-интуитивни и гъвкави опции за тази цел. В NEON могат естествено да се опишат връзки, които в Symfony & YAMLu би било невъзможно да се запишат изобщо или само чрез сложно описание. - - -Не забавя ли приложението парсването на NEON файлове? ------------------------------------------------------ - -Въпреки че NEON файловете се парсват много бързо, на този аспект изобщо няма значение. Причината е, че парсването на файловете се извършва само веднъж при първото стартиране на приложението. След това се генерира кодът на DI контейнера, записва се на диска и се изпълнява при всяка следваща заявка, без да е необходимо допълнително парсване. - -Така работи в продукционна среда. По време на разработка NEON файловете се парсват всеки път, когато съдържанието им се промени, така че разработчикът винаги да има актуален DI контейнер. Самото парсване е, както беше споменато, въпрос на момент. - - -Как да получа достъп до параметрите в конфигурационния файл от моя клас? ------------------------------------------------------------------------- - -Нека си припомним [Правило № 1: нека ти го предадат |introduction#Правило 1: Нека ви го предадат]. Ако класът изисква информация от конфигурационния файл, не е нужно да мислим как да стигнем до тази информация, вместо това просто я искаме - например чрез конструктора на класа. И осъществяваме предаването в конфигурационния файл. - -В този пример `%myParameter%` е placeholder за стойността на параметъра `myParameter`, която се предава на конструктора на класа `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Ако искате да предавате повече параметри или да използвате autowiring, е препоръчително [да опаковате параметрите в обект |best-practices:passing-settings-to-presenters]. - - -Поддържа ли Nette PSR-11: Container interface? ----------------------------------------------- - -Nette DI Container не поддържа директно PSR-11. Въпреки това, ако се нуждаете от оперативна съвместимост между Nette DI Container-а и библиотеки или framework-ове, които очакват PSR-11 Container Interface, можете да създадете [прост адаптер |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], който ще служи като мост между Nette DI Container-а и PSR-11. diff --git a/dependency-injection/bg/global-state.texy b/dependency-injection/bg/global-state.texy deleted file mode 100644 index b4bf3be4e8..0000000000 --- a/dependency-injection/bg/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Глобално състояние и сингълтъни -******************************* - -.[perex] -Предупреждение: Следните конструкции са признак на лошо проектиран код: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` или `static::$var` - -Срещат ли се някои от тези конструкции във вашия код? Тогава имате възможност да го подобрите. Може би си мислите, че това са обичайни конструкции, които виждате дори в примерни решения на различни библиотеки и framework-ове. Ако е така, тогава дизайнът на техния код не е добър. - -Сега определено не говорим за някаква академична чистота. Всички тези конструкции имат едно общо нещо: те използват глобално състояние. А то има разрушителен ефект върху качеството на кода. Класовете лъжат за своите зависимости. Кодът става непредсказуем. Обърква програмистите и намалява тяхната ефективност. - -В тази глава ще обясним защо е така и как да избегнем глобалното състояние. - - -Глобална свързаност -------------------- - -В идеалния свят обектът трябва да може да комуникира само с обекти, които са му били [директно предадени |passing-dependencies]. Ако създам два обекта `A` и `B` и никога не предам референция между тях, тогава нито `A`, нито `B` могат да достигнат до другия обект или да променят неговото състояние. Това е много желана характеристика на кода. Подобно е на това да имате батерия и крушка; крушката няма да свети, докато не я свържете с батерията с проводник. - -Но това не важи за глобални (статични) променливи или сингълтъни. Обект `A` може *безжично* да достигне до обект `C` и да го модифицира без никакво предаване на референция, като извика `C::changeSomething()`. Ако обект `B` също се възползва от глобалния `C`, тогава `A` и `B` могат да си влияят взаимно чрез `C`. - -Използването на глобални променливи въвежда нова форма на *безжична* свързаност в системата, която не се вижда отвън. Създава димна завеса, усложняваща разбирането и използването на кода. За да разберат наистина зависимостите, разработчиците трябва да прочетат всеки ред от изходния код. Вместо просто да се запознаят с интерфейсите на класовете. Освен това става дума за напълно ненужна свързаност. Глобалното състояние се използва, защото е лесно достъпно отвсякъде и позволява например запис в базата данни чрез глобален (статичен) метод `DB::insert()`. Но както ще покажем, предимството, което носи, е незначително, докато усложненията, които причинява, са фатални. - -.[note] -От гледна точка на поведението няма разлика между глобална и статична променлива. Те са еднакво вредни. - - -Призрачно действие от разстояние --------------------------------- - -"Призрачно действие от разстояние" - така Алберт Айнщайн нарича през 1935 г. явление в квантовата физика, което го кара да настръхне. -Става дума за квантово заплитане, чиято особеност е, че когато измерите информация за една частица, веднага повлиявате на другата частица, дори ако те са на милиони светлинни години една от друга. Което привидно нарушава основния закон на Вселената, че нищо не може да се разпространява по-бързо от светлината. - -В света на софтуера можем да наречем "призрачно действие от разстояние" ситуация, при която стартираме някакъв процес, за който смятаме, че е изолиран (защото не сме му предали никакви референции), но на отдалечени места в системата възникват неочаквани взаимодействия и промени в състоянието, за които не сме подозирали. Това може да се случи само чрез глобално състояние. - -Представете си, че се присъединявате към екип от разработчици на проект, който има голяма, зряла кодова база. Новият ви ръководител ви моли да имплементирате нова функция и вие, като добър разработчик, започвате с писане на тест. Но тъй като сте нов в проекта, правите много проучвателни тестове от типа "какво ще се случи, ако извикам този метод". И опитвате да напишете следния тест: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // номер на вашата карта - $cc->charge(100); -} -``` - -Изпълнявате кода, може би няколко пъти, и след известно време забелязвате известия от банката на мобилния си телефон, че при всяко стартиране са били изтеглени 100 долара от вашата кредитна карта 🤦‍♂️ - -Как, за бога, тестът може да е причинил реално теглене на пари? Работата с кредитна карта не е лесна. Трябва да комуникирате с уеб услуга на трета страна, трябва да знаете URL адреса на тази уеб услуга, трябва да влезете и т.н. Нито една от тази информация не се съдържа в теста. Още по-лошо, дори не знаете къде се намира тази информация и следователно как да mock-нете външните зависимости, така че всяко стартиране да не води до повторно теглене на 100 долара. И как вие, като нов разработчик, трябваше да знаете, че това, което се каните да направите, ще доведе до това да сте с 100 долара по-беден? - -Това е призрачно действие от разстояние! - -Не ви остава нищо друго, освен дълго да ровите в много изходни кодове, да питате по-стари и по-опитни колеги, докато разберете как работят връзките в проекта. Това се дължи на факта, че при разглеждане на интерфейса на класа `CreditCard` не може да се установи глобалното състояние, което трябва да се инициализира. Дори поглед към изходния код на класа няма да ви каже кой инициализационен метод трябва да извикате. В най-добрия случай можете да намерите глобална променлива, до която се осъществява достъп, и от нея да се опитате да отгатнете как да я инициализирате. - -Класовете в такъв проект са патологични лъжци. Кредитната карта се преструва, че е достатъчно да я инстанцирате и да извикате метода `charge()`. Но тайно тя си сътрудничи с друг клас `PaymentGateway`, който представлява платежен портал. Неговият интерфейс също казва, че може да се инициализира самостоятелно, но всъщност извлича идентификационни данни от някакъв конфигурационен файл и т.н. За разработчиците, които са написали този код, е ясно, че `CreditCard` се нуждае от `PaymentGateway`. Те са написали кода по този начин. Но за всеки нов в проекта това е пълна загадка и пречи на ученето. - -Как да поправим ситуацията? Лесно. **Нека API декларира зависимостите.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Забележете как изведнъж взаимовръзките в кода стават очевидни. Тъй като методът `charge()` декларира, че се нуждае от `PaymentGateway`, не е нужно да питате никого как е свързан кодът. Знаете, че трябва да създадете негова инстанция и когато се опитате да го направите, ще откриете, че трябва да предоставите параметри за достъп. Без тях кодът дори не би могъл да се изпълни. - -И най-важното, сега можете да mock-нете платежния портал, така че няма да ви бъдат таксувани 100 долара всеки път, когато стартирате теста. - -Глобалното състояние кара вашите обекти да имат таен достъп до неща, които не са декларирани в техните API, и в резултат на това превръща вашите API в патологични лъжци. - -Може би не сте мислили за това по този начин преди, но всеки път, когато използвате глобално състояние, създавате тайни безжични комуникационни канали. Призрачното действие от разстояние принуждава разработчиците да четат всеки ред код, за да разберат потенциалните взаимодействия, намалява производителността на разработчиците и обърква новите членове на екипа. Ако вие сте този, който е създал кода, познавате истинските зависимости, но всеки, който дойде след вас, е безпомощен. - -Не пишете код, който използва глобално състояние, предпочитайте предаването на зависимости. Тоест dependency injection. - - -Крехкост на глобалното състояние --------------------------------- - -В код, който използва глобално състояние и сингълтъни, никога не е сигурно кога и кой е променил това състояние. Този риск се появява още при инициализацията. Следният код трябва да създаде връзка с база данни и да инициализира платежен портал, но постоянно хвърля изключение и намирането на причината е изключително досадно: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Трябва подробно да прегледате кода, за да установите, че обектът `PaymentGateway` осъществява безжичен достъп до други обекти, някои от които изискват връзка с база данни. Следователно е необходимо да се инициализира базата данни преди `PaymentGateway`. Въпреки това, димната завеса на глобалното състояние скрива това от вас. Колко време бихте спестили, ако API-тата на отделните класове не лъжеха и декларираха своите зависимости? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Подобен проблем възниква и при използване на глобален достъп до връзката с базата данни: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -При извикване на метода `save()` не е сигурно дали вече е създадена връзка с базата данни и кой носи отговорност за нейното създаване. Ако искаме например да променяме връзката с базата данни по време на изпълнение, например за тестове, вероятно ще трябва да създадем допълнителни методи като `DB::reconnect(...)` или `DB::reconnectForTest()`. - -Да разгледаме пример: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Къде имаме сигурност, че при извикване на `$article->save()` наистина се използва тестовата база данни? Ами ако методът `Foo::doSomething()` е променил глобалната връзка с базата данни? За да разберем, ще трябва да проучим изходния код на класа `Foo` и вероятно на много други класове. Този подход обаче би донесъл само краткосрочен отговор, тъй като ситуацията може да се промени в бъдеще. - -Ами ако преместим връзката с базата данни в статична променлива вътре в класа `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -С това нищо не се промени. Проблемът е глобалното състояние и няма никакво значение в кой клас се крие. В този случай, както и в предишния, при извикване на метода `$article->save()` нямаме никаква представа в коя база данни ще се запише. Всеки от другия край на приложението може по всяко време да промени базата данни с помощта на `Article::setDb()`. Под носа ни. - -Глобалното състояние прави нашето приложение **изключително крехко**. - -Съществува обаче прост начин за справяне с този проблем. Достатъчно е да оставим API да декларира зависимостите, което ще гарантира правилната функционалност. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Благодарение на този подход отпада притеснението за скрити и неочаквани промени във връзката с базата данни. Сега имаме сигурност къде се съхранява статията и никакви промени в кода в друг несвързан клас вече не могат да променят ситуацията. Кодът вече не е крехък, а стабилен. - -Не пишете код, който използва глобално състояние, предпочитайте предаването на зависимости. Тоест dependency injection. - - -Singleton ---------- - -Singleton е дизайн патърн, който според "дефиницията":https://en.wikipedia.org/wiki/Singleton_pattern от известната публикация на Gang of Four ограничава класа до една единствена инстанция и предлага глобален достъп до нея. Имплементацията на този патърн обикновено прилича на следния код: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // и други методи, изпълняващи функциите на дадения клас -} -``` - -За съжаление, сингълтънът въвежда глобално състояние в приложението. И както показахме по-горе, глобалното състояние е нежелателно. Затова сингълтънът се счита за антипатърн. - -Не използвайте сингълтъни във вашия код и ги заменете с други механизми. Наистина не се нуждаете от сингълтъни. Въпреки това, ако трябва да гарантирате съществуването на една единствена инстанция на клас за цялото приложение, оставете това на [DI контейнера |container]. По този начин създайте апликационен сингълтън, т.е. сървис. Така класът ще спре да се занимава с осигуряването на собствената си уникалност (т.е. няма да има метод `getInstance()` и статична променлива) и ще изпълнява само своите функции. Така ще спре да нарушава принципа на единствената отговорност. - - -Глобално състояние срещу тестове --------------------------------- - -При писане на тестове предполагаме, че всеки тест е изолирана единица и че в него не влиза никакво външно състояние. И никакво състояние не напуска тестовете. След приключване на теста цялото свързано с теста състояние трябва да бъде автоматично премахнато от garbage collector-а. Благодарение на това тестовете са изолирани. Затова можем да изпълняваме тестовете в произволен ред. - -Ако обаче са налице глобални състояния/сингълтъни, всички тези приятни предположения се разпадат. Състоянието може да влиза и излиза от теста. Изведнъж редът на тестовете може да има значение. - -За да можем изобщо да тестваме сингълтъни, разработчиците често трябва да разхлабят техните свойства, например като позволят инстанцията да бъде заменена с друга. Такива решения в най-добрия случай са хак, който създава трудно поддържаем и разбираем код. Всеки тест или метод `tearDown()`, който повлияе на някакво глобално състояние, трябва да върне тези промени обратно. - -Глобалното състояние е най-голямото главоболие при unit тестването! - -Как да поправим ситуацията? Лесно. Не пишете код, който използва сингълтъни, предпочитайте предаването на зависимости. Тоест dependency injection. - - -Глобални константи ------------------- - -Глобалното състояние не се ограничава само до използването на сингълтъни и статични променливи, но може да се отнася и до глобални константи. - -Константи, чиято стойност не ни носи никаква нова (`M_PI`) или полезна (`PREG_BACKTRACK_LIMIT_ERROR`) информация, са недвусмислено в ред. Напротив, константи, които служат като начин за *безжично* предаване на информация вътре в кода, не са нищо друго освен скрита зависимост. Като например `LOG_FILE` в следващия пример. Използването на константата `FILE_APPEND` е напълно коректно. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -В този случай трябва да декларираме параметър в конструктора на класа `Foo`, за да стане част от API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Сега можем да предадем информация за пътя до лог файла и лесно да го променяме при нужда, което улеснява тестването и поддръжката на кода. - - -Глобални функции и статични методи ----------------------------------- - -Искаме да подчертаем, че самото използване на статични методи и глобални функции не е проблематично. Обяснихме защо използването на `DB::insert()` и подобни методи е неподходящо, но винаги ставаше дума само за глобално състояние, което се съхранява в някаква статична променлива. Методът `DB::insert()` изисква съществуването на статична променлива, тъй като в нея се съхранява връзката с базата данни. Без тази променлива би било невъзможно да се имплементира методът. - -Използването на детерминистични статични методи и функции, като например `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` и много други, е в пълно съответствие с dependency injection. Тези функции винаги връщат едни и същи резултати за едни и същи входни параметри и следователно са предвидими. Те не използват никакво глобално състояние. - -Съществуват обаче и функции в PHP, които не са детерминистични. Към тях принадлежи например функцията `htmlspecialchars()`. Нейният трети параметър `$encoding`, ако не е посочен, по подразбиране има стойността на конфигурационната опция `ini_get('default_charset')`. Затова се препоръчва винаги да се посочва този параметър, за да се избегне евентуално непредсказуемо поведение на функцията. Nette го прави последователно. - -Някои функции, като например `strtolower()`, `strtoupper()` и подобни, в близкото минало се държаха недетерминистично и зависеха от настройката `setlocale()`. Това причиняваше много усложнения, най-често при работа с турски език. Той различава малки и големи букви `I` с точка и без точка. Така че `strtolower('I')` връщаше знака `ı`, а `strtoupper('i')` - знака `İ`, което водеше до това, че приложенията започваха да причиняват редица мистериозни грешки. Този проблем обаче беше отстранен в PHP версия 8.2 и функциите вече не зависят от locale. - -Това е хубав пример как глобалното състояние е измъчвало хиляди разработчици по целия свят. Решението беше да се замени с dependency injection. - - -Кога е възможно да се използва глобално състояние? --------------------------------------------------- - -Съществуват определени специфични ситуации, в които е възможно да се използва глобално състояние. Например, при дебъгване на код, когато трябва да изведете стойността на променлива или да измерите продължителността на определена част от програмата. В такива случаи, които се отнасят до временни актове, които по-късно ще бъдат премахнати от кода, е възможно легитимно да се използва глобално достъпен dumper или хронометър. Тези инструменти всъщност не са част от дизайна на кода. - -Друг пример са функциите за работа с регулярни изрази `preg_*`, които вътрешно съхраняват компилирани регулярни изрази в статичен кеш в паметта. Така че, когато извиквате един и същ регулярен израз многократно на различни места в кода, той се компилира само веднъж. Кешът спестява производителност и в същото време е напълно невидим за потребителя, затова такова използване може да се счита за легитимно. - - -Обобщение ---------- - -Обсъдихме защо има смисъл: - -1) Да премахнете всички статични променливи от кода -2) Да декларирате зависимости -3) И да използвате dependency injection - -Когато обмисляте дизайна на кода, имайте предвид, че всяко `static $foo` представлява проблем. За да бъде вашият код среда, уважаваща DI, е необходимо напълно да изкорените глобалното състояние и да го замените с dependency injection. - -По време на този процес може да откриете, че е необходимо да разделите класа, защото той има повече от една отговорност. Не се страхувайте от това; стремете се към принципа на единствената отговорност. - -*Бих искал да благодаря на Miško Hevery, чиито статии, като [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], са в основата на тази глава.* diff --git a/dependency-injection/bg/introduction.texy b/dependency-injection/bg/introduction.texy deleted file mode 100644 index 902247b1dd..0000000000 --- a/dependency-injection/bg/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Какво е Dependency Injection? -***************************** - -.[perex] -Тази глава ще ви запознае с основните програмни практики, които трябва да следвате при писането на всички приложения. Това са основите, необходими за писане на чист, разбираем и поддържаем код. - -Ако усвоите тези правила и ги спазвате, Nette ще ви помага на всяка стъпка. Той ще се справя с рутинните задачи вместо вас и ще ви осигури максимален комфорт, за да можете да се съсредоточите върху самата логика. - -Принципите, които ще покажем тук, са доста прости. Няма нужда да се притеснявате за нищо. - - -Спомняте ли си първата си програма? ------------------------------------ - -Не знаем на какъв език сте я написали, но ако беше PHP, вероятно щеше да изглежда така: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // извежда 24 -``` - -Няколко тривиални реда код, но в тях се крият толкова много ключови концепции. Че съществуват променливи. Че кодът се разделя на по-малки единици, като например функции. Че им предаваме входни аргументи и те връщат резултати. Липсват само условия и цикли. - -Това, че предаваме входни данни на функция и тя връща резултат, е напълно разбираема концепция, която се използва и в други области, като например математиката. - -Функцията има своя сигнатура, която се състои от нейното име, списък с параметри и техните типове, и накрая тип на връщаната стойност. Като потребители ни интересува сигнатурата, обикновено не е необходимо да знаем нищо за вътрешната имплементация. - -Сега си представете, че сигнатурата на функцията изглеждаше така: - -```php -function soucet(float $x): float -``` - -Сума с един параметър? Това е странно… А какво ще кажете за това? - -```php -function soucet(): float -``` - -Това вече е наистина много странно, нали? Как се използва функцията? - -```php -echo soucet(); // какво ли ще изведе? -``` - -При вида на такъв код бихме били объркани. Не само начинаещ не би го разбрал, такъв код не разбира и опитен програмист. - -Чудите ли се как би изглеждала такава функция отвътре? Откъде ще вземе събираемите? Вероятно би си ги набавила *по някакъв начин* сама, например така: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -В тялото на функцията открихме скрити връзки към други глобални функции или статични методи. За да разберем откъде всъщност идват събираемите, трябва да търсим по-нататък. - - -Не така! --------- - -Дизайнът, който току-що показахме, е есенцията на много негативни черти: - -- сигнатурата на функцията се преструваше, че не се нуждае от събираеми, което ни объркваше -- изобщо не знаем как да накараме функцията да събере други две числа -- трябваше да погледнем в кода, за да разберем откъде взема събираемите -- открихме скрити зависимости -- за пълно разбиране е необходимо да се проучат и тези зависимости - -И изобщо задача ли е на функцията за събиране да си набавя входове? Разбира се, че не е. Нейната отговорност е само самото събиране. - - -С такъв код не искаме да се сблъскваме и определено не искаме да го пишем. Поправката е проста: да се върнем към основите и просто да използваме параметри: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Правило № 1: Нека ви го предадат --------------------------------- - -Най-важното правило е: **всички данни, от които функциите или класовете се нуждаят, трябва да им бъдат предадени**. - -Вместо да измисляте скрити начини, чрез които те биха могли да стигнат до тях сами, просто предайте параметрите. Ще спестите време, необходимо за измисляне на скрити пътища, които определено няма да подобрят вашия код. - -Ако спазвате това правило винаги и навсякъде, сте на път към код без скрити зависимости. Към код, който е разбираем не само за автора, но и за всеки, който ще го чете след него. Където всичко е разбираемо от сигнатурите на функциите и класовете и не е необходимо да се търсят скрити тайни в имплементацията. - -Тази техника се нарича професионално **dependency injection**. А тези данни се наричат **зависимости.** Всъщност това е просто предаване на параметри, нищо повече. - -.[note] -Моля, не бъркайте dependency injection, което е дизайнерски патърн, с „dependency injection container“, което пък е инструмент, т.е. нещо диаметрално различно. Ще се занимаваме с контейнерите по-късно. - - -От функции към класове ----------------------- - -А как това е свързано с класовете? Класът е по-сложна единица от проста функция, но правило № 1 важи изцяло и тук. Само че съществуват [повече начини за предаване на аргументи|passing-dependencies]. Например, доста подобно на случая с функция: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Или чрез други методи, или директно чрез конструктора: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -И двата примера са напълно в съответствие с dependency injection. - - -Реални примери --------------- - -В реалния свят няма да пишете класове за събиране на числа. Нека преминем към примери от практиката. - -Нека имаме клас `Article`, представляващ статия в блог: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // запазваме статията в базата данни - } -} -``` - -и употребата ще бъде следната: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Методът `save()` запазва статията в таблица в базата данни. Имплементирането му с помощта на [Nette Database |database:] ще бъде лесно, ако не беше една спънка: откъде `Article` да вземе връзка към базата данни, т.е. обект от клас `Nette\Database\Connection`? - -Изглежда, че имаме много възможности. Може да я вземе отнякъде от статична променлива. Или да наследи от клас, който осигурява връзка с базата данни. Или да използва т.нар. [singleton |global-state#Singleton]. Или т.нар. фасади, които се използват в Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Страхотно, решихме проблема. - -Или не? - -Да си припомним [#Правило № 1: Нека ви го предадат |#Правило 1: Нека ви го предадат]: всички зависимости, от които класът се нуждае, трябва да му бъдат предадени. Защото ако нарушим правилото, сме поели по пътя към мръсен код, пълен със скрити зависимости, неразбираемост, и резултатът ще бъде приложение, което ще бъде болезнено за поддръжка и разработка. - -Потребителят на класа `Article` не знае къде методът `save()` запазва статията. В таблица в базата данни? В коя, продукционната или тестовата? И как може да се промени това? - -Потребителят трябва да погледне как е имплементиран методът `save()` и намира използването на метода `DB::insert()`. Така че трябва да търси по-нататък как този метод си набавя връзка към базата данни. А скритите зависимости могат да образуват доста дълга верига. - -В чист и добре проектиран код никога не се срещат скрити зависимости, фасади в стил Laravel или статични променливи. В чист и добре проектиран код се предават аргументи: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Още по-практично, както ще видим по-нататък, ще бъде чрез конструктора: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Ако сте опитен програмист, може би си мислите, че `Article` изобщо не трябва да има метод `save()`, трябва да представлява чисто компонент за данни и за запазването трябва да се грижи отделно репозитори. Това има смисъл. Но така бихме се отклонили твърде много от темата, която е dependency injection, и от стремежа да даваме прости примери. - -Ако пишете клас, който изисква за дейността си например база данни, не измисляйте откъде да я вземете, а поискайте да ви бъде предадена. Например като параметър на конструктора или друг метод. Признайте зависимостите. Признайте ги в API на вашия клас. Ще получите разбираем и предвидим код. - -А какво ще кажете за този клас, който логва съобщения за грешки: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Какво мислите, спазихме ли [#Правило № 1: Нека ви го предадат |#Правило 1: Нека ви го предадат]? - -Не спазихме. - -Ключовата информация, т.е. директорията с файла с лога, класът *си набавя сам* от константа. - -Погледнете примера за употреба: - -```php -$logger = new Logger; -$logger->log('Температурата е 23 °C'); -$logger->log('Температурата е 10 °C'); -``` - -Без да познавате имплементацията, бихте ли могли да отговорите на въпроса къде се записват съобщенията? Би ли ви хрумнало, че за функционирането е необходимо съществуването на константата `LOG_DIR`? И бихте ли могли да създадете втора инстанция, която да записва другаде? Със сигурност не. - -Нека поправим класа: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Класът сега е много по-разбираем, конфигурируем и следователно по-полезен. - -```php -$logger = new Logger('/път/към/лог.txt'); -$logger->log('Температурата е 15 °C'); -``` - - -Но това не ме интересува! -------------------------- - -*„Когато създам обект Article и извикам save(), не искам да се занимавам с базата данни, просто искам да се запази в тази, която съм настроил в конфигурацията.“* - -*„Когато използвам Logger, просто искам съобщението да се запише и не искам да се занимавам къде. Нека се използва глобалната настройка.“* - -Това са правилни забележки. - -Като пример ще покажем клас, който разпраща бюлетини и логва как е минало: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Имейлите бяха изпратени'); - - } catch (Exception $e) { - $logger->log('Възникна грешка при изпращането'); - throw $e; - } - } -} -``` - -Подобреният `Logger`, който вече не използва константата `LOG_DIR`, изисква в конструктора да се посочи пътят към файла. Как да решим това? Класът `NewsletterDistributor` изобщо не се интересува къде се записват съобщенията, иска само да ги запише. - -Решението е отново [#Правило № 1: Нека ви го предадат |#Правило 1: Нека ви го предадат]: всички данни, от които класът се нуждае, му предаваме. - -Значи това означава, че ще си предадем пътя към лога чрез конструктора, който след това ще използваме при създаването на обекта `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ ТАКА НЕ! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Така не! Пътят всъщност **не принадлежи** към данните, от които класът `NewsletterDistributor` се нуждае; от тях се нуждае `Logger`. Усещате ли разликата? Класът `NewsletterDistributor` се нуждае от логъра като такъв. Така че ще си го предадем: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Имейлите бяха изпратени'); - - } catch (Exception $e) { - $this->logger->log('Възникна грешка при изпращането'); - throw $e; - } - } -} -``` - -Сега от сигнатурите на класа `NewsletterDistributor` е ясно, че част от неговата функционалност е и логването. А задачата да се смени логърът с друг, например за тестване, е напълно тривиална. Освен това, ако конструкторът на класа `Logger` се промени, това няма да има никакво влияние върху нашия клас. - - -Правило № 2: Вземи това, което е твое -------------------------------------- - -Не се заблуждавайте и не си предавайте зависимостите на вашите зависимости. Предавайте си само вашите собствени зависимости. - -Благодарение на това кодът, използващ други обекти, ще бъде напълно независим от промените в техните конструктори. Неговото API ще бъде по-вярно. И най-важното, ще бъде тривиално тези зависимости да се заменят с други. - - -Нов член на семейството ------------------------ - -В екипа за разработка беше взето решение да се създаде втори логър, който записва в база данни. Затова създаваме клас `DatabaseLogger`. Така имаме два класа, `Logger` и `DatabaseLogger`, единият записва във файл, другият в база данни … не ви ли се струва нещо странно в това именуване? Не би ли било по-добре да преименуваме `Logger` на `FileLogger`? Със сигурност да. - -Но ще го направим умно. Под оригиналното име ще създадем интерфейс: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… който и двата логъра ще имплементират: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -И благодарение на това няма да е необходимо да се променя нищо в останалата част от кода, където се използва логърът. Например конструкторът на класа `NewsletterDistributor` ще продължи да бъде доволен, че като параметър изисква `Logger`. И ще зависи само от нас коя инстанция ще му предадем. - -**Затова никога не даваме на имената на интерфейсите суфикс `Interface` или префикс `I`.** В противен случай не би било възможно кодът да се развива толкова добре. - - -Хюстън, имаме проблем ---------------------- - -Докато в цялото приложение можем да се справим с една единствена инстанция на логъра, било то файлов или базиран на данни, и просто го предаваме навсякъде, където нещо се логва, съвсем различно е положението с класа `Article`. Неговите инстанции създаваме според нуждите, дори многократно. Как да се справим със зависимостта от базата данни в неговия конструктор? - -Като пример може да послужи контролер, който след изпращане на формуляр трябва да запази статия в базата данни: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Възможното решение се натрапва само: ще си предадем обекта на базата данни чрез конструктора в `EditController` и ще използваме `$article = new Article($this->db)`. - -Точно както в предишния случай с `Logger` и пътя към файла, това не е правилният подход. Базата данни не е зависимост на `EditController`, а на `Article`. Предаването на базата данни следователно противоречи на [Правило № 2: Вземи това, което е твое |#Правило 2: Вземи това което е твое]. Когато конструкторът на класа `Article` се промени (добави се нов параметър), ще бъде необходимо да се коригира и кодът на всички места, където се създават инстанции. Уф. - -Хюстън, какво предлагаш? - - -Правило № 3: Остави го на фабриката ------------------------------------ - -Като премахнахме скритите зависимости и предаваме всички зависимости като аргументи, получихме по-конфигурируеми и гъвкави класове. И следователно се нуждаем от още нещо, което да ни създаде и конфигурира тези по-гъвкави класове. Ще го наречем фабрики. - -Правилото гласи: ако класът има зависимости, оставете създаването на техните инстанции на фабрика. - -Фабриките са по-умната замяна на оператора `new` в света на dependency injection. - -.[note] -Моля, не бъркайте с дизайнерския патърн *factory method*, който описва специфичен начин за използване на фабрики и не е свързан с тази тема. - - -Фабрика -------- - -Фабриката е метод или клас, който произвежда и конфигурира обекти. Класът, произвеждащ `Article`, ще наречем `ArticleFactory` и би могъл да изглежда например така: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Нейното използване в контролера ще бъде следното: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // оставяме фабриката да създаде обекта - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Ако в този момент се промени сигнатурата на конструктора на класа `Article`, единствената част от кода, която трябва да реагира на това, е самата фабрика `ArticleFactory`. Целият останал код, който работи с обекти `Article`, като например `EditController`, няма да бъде засегнат по никакъв начин. - -Може би сега си удряте челото, дали изобщо сме си помогнали. Количеството код нарасна и всичко започва да изглежда подозрително сложно. - -Не се притеснявайте, скоро ще стигнем до Nette DI контейнера. А той има редица асове в ръкава, с които изграждането на приложения, използващи dependency injection, се опростява неимоверно. Така например, вместо клас `ArticleFactory`, ще е достатъчно [напишете само интерфейс |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Но това е изпреварване, изчакайте още малко :-) - - -Резюме ------- - -В началото на тази глава обещахме, че ще покажем процедура за проектиране на чист код. Достатъчно е на класовете - -1) [предавайте зависимостите, от които се нуждаят |#Правило 1: Нека ви го предадат] -2) [и обратно, не предавайте това, от което не се нуждаят пряко |#Правило 2: Вземи това което е твое] -3) [и че обектите със зависимости се създават най-добре във фабрики |#Правило 3: Остави го на фабриката] - -Може да не изглежда така на пръв поглед, но тези три правила имат далечни последици. Водят до радикално различен поглед върху дизайна на кода. Струва ли си? Програмистите, които са изоставили старите навици и са започнали последователно да използват dependency injection, смятат тази стъпка за ключов момент в професионалния си живот. Открил се е пред тях свят на прегледни и поддържаеми приложения. - -Ами ако кодът не използва последователно dependency injection? Ами ако е изграден върху статични методи или сингълтони? Носи ли това някакви проблеми? [Носи и то много съществени |global-state]. diff --git a/dependency-injection/bg/nette-container.texy b/dependency-injection/bg/nette-container.texy deleted file mode 100644 index 801555f8d8..0000000000 --- a/dependency-injection/bg/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI контейнер -****************** - -.[perex] -Nette DI е една от най-интересните библиотеки на Nette. Тя може да генерира и автоматично да актуализира компилирани DI контейнери, които са изключително бързи и невероятно лесни за конфигуриране. - -Формата на сървисите, които DI контейнерът трябва да създава, обикновено дефинираме с помощта на конфигурационни файлове във [формат NEON|neon:format]. Контейнерът, който ръчно създадохме в [предишната глава|container], би се записал така: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Записът е наистина кратък. - -Всички зависимости, декларирани в конструкторите на класовете `ArticleFactory` и `UserController`, Nette DI само открива и предава благодарение на т.нар. [autowiring|autowiring], затова в конфигурационния файл не е необходимо да се посочва нищо. Така че дори ако параметрите се променят, не е необходимо да променяте нищо в конфигурацията. Nette контейнерът автоматично ще се прегенерира. Вие можете да се съсредоточите изцяло върху разработката на приложението. - -Ако искаме да предаваме зависимости чрез сетъри, използваме за това секцията [setup |services#Setup]. - -Nette DI генерира директно PHP код на контейнера. Резултатът е файл `.php`, който можете да отворите и изучавате. Благодарение на това виждате точно как работи контейнерът. Можете също да го дебъгвате в IDE и да го проследявате стъпка по стъпка. И най-важното: генерираният PHP е изключително бърз. - -Nette DI може също да генерира код на [фабрики|factory] въз основа на предоставен интерфейс. Затова вместо клас `ArticleFactory` ще ни е достатъчно да създадем в приложението само интерфейс: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Целият пример можете да намерите [в GitHub|https://github.com/nette-examples/di-example-doc]. - - -Самостоятелна употреба ----------------------- - -Внедряването на библиотеката Nette DI в приложение е много лесно. Първо я инсталираме с Composer (защото изтеглянето на zip файлове е тааака остаряло): - -```shell -composer require nette/di -``` - -Следващият код създава инстанция на DI контейнер според конфигурацията, съхранена във файла `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Контейнерът се генерира само веднъж, неговият код се записва в кеша (директория `__DIR__ . '/temp'`) и при следващи заявки се зарежда само оттам. - -За създаване и получаване на сървиси служат методите `getService()` или `getByType()`. Така създаваме обект `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -По време на разработка е полезно да се активира режимът на автоматично опресняване, при който контейнерът автоматично се прегенерира, ако настъпи промяна в някой клас или конфигурационен файл. Достатъчно е в конструктора на `ContainerLoader` да се посочи като втори аргумент `true`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Използване с Nette Framework ----------------------------- - -Както показахме, използването на Nette DI не е ограничено до приложения, написани в Nette Framework, можете да го внедрите навсякъде само с 3 реда код. Ако обаче разработвате приложения в Nette Framework, конфигурацията и създаването на контейнера се управляват от [Bootstrap |application:bootstrapping#Конфигурация на DI контейнера]. diff --git a/dependency-injection/bg/passing-dependencies.texy b/dependency-injection/bg/passing-dependencies.texy deleted file mode 100644 index 0652e8cd2f..0000000000 --- a/dependency-injection/bg/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Предаване на зависимости -************************ - -<div class=perex> - -Аргументите, или в терминологията на DI „зависимости“, могат да се предават на класове по следните основни начини: - -* предаване чрез конструктор -* предаване чрез метод (т.нар. сетър) -* задаване на променлива -* чрез метод, анотация или атрибут *inject* - -</div> - -Сега ще покажем отделните варианти с конкретни примери. - - -Предаване чрез конструктор -========================== - -Зависимостите се предават в момента на създаване на обекта като аргументи на конструктора: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Тази форма е подходяща за задължителни зависимости, от които класът непременно се нуждае за своята функция, тъй като без тях инстанцията няма да може да бъде създадена. - -От PHP 8.0 можем да използваме по-кратка форма на запис ([constructor property promotion |https://blog.nette.org/bg/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), която е функционално еквивалентна: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -От PHP 8.1 променливата може да бъде маркирана с флага `readonly`, който декларира, че съдържанието на променливата няма да се променя повече: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI контейнерът предава зависимостите на конструктора автоматично чрез [autowiring |autowiring]. Аргументите, които не могат да бъдат предадени по този начин (напр. низове, числа, булеви стойности), [записваме в конфигурацията |services#Аргументи]. - - -Адът на конструктора --------------------- - -Терминът *constructor hell* описва ситуация, когато наследник наследява от родителски клас, чийто конструктор изисква зависимости, и същевременно наследникът изисква зависимости. При това трябва да приеме и предаде и родителските: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Проблемът възниква в момента, когато искаме да променим конструктора на класа `BaseClass`, например когато се добави нова зависимост. Тогава е необходимо да се коригират и всички конструктори на наследниците. Което превръща такава корекция в ад. - -Как да предотвратим това? Решението е **да се дава предимство на [композиция пред наследяване |faq#Защо се предпочита композиция пред наследяването]**. - -Тоест, ще проектираме кода по друг начин. Ще избягваме [абстрактни |nette:introduction-to-object-oriented-programming#Абстрактни класове] `Base*` класове. Вместо `MyClass` да получава определена функционалност чрез наследяване от `BaseClass`, тази функционалност ще му бъде предадена като зависимост: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Предаване чрез сетър -==================== - -Зависимостите се предават чрез извикване на метод, който ги съхранява в частна променлива. Обичайната конвенция за именуване на тези методи е формата `set*()`, затова се наричат сетъри, но разбира се, могат да се наричат и по друг начин. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Този начин е подходящ за незадължителни зависимости, които не са необходими за функцията на класа, тъй като не е гарантирано, че обектът действително ще получи зависимостта (т.е. че потребителят ще извика метода). - -Същевременно този начин позволява сетърът да се извиква многократно и така зависимостта да се променя. Ако това не е желателно, добавяме проверка в метода, или от PHP 8.1 маркираме свойството `$cache` с флага `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Зависимостта вече е зададена'); - } - $this->cache = $cache; - } -} -``` - -Извикването на сетъра дефинираме в конфигурацията на DI контейнера в [ключа setup |services#Setup]. И тук се използва автоматично предаване на зависимости чрез autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Чрез задаване на променлива -=========================== - -Зависимостите се предават чрез записване директно в член-променлива: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Този начин се счита за неподходящ, тъй като член-променливата трябва да бъде декларирана като `public`. И следователно нямаме контрол над това, че предадената зависимост ще бъде действително от дадения тип (важеше преди PHP 7.4) и губим възможността да реагираме на новоприсвоената зависимост със собствен код, например да предотвратим последваща промяна. Същевременно променливата става част от публичния интерфейс на класа, което може да не е желателно. - -Задаването на променливата дефинираме в конфигурацията на DI контейнера в [секцията setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Докато предходните три начина важат общо за всички обектно-ориентирани езици, инжектирането чрез метод, анотация или атрибут *inject* е специфично само за презентерите в Nette. За тях се разказва в [отделна глава |best-practices:inject-method-attribute]. - - -Кой метод да изберем? -===================== - -- конструкторът е подходящ за задължителни зависимости, от които класът непременно се нуждае за своята функция -- сетърът, напротив, е подходящ за незадължителни зависимости или зависимости, които може да се наложи да се променят по-нататък -- публичните променливи не са подходящи diff --git a/dependency-injection/bg/services.texy b/dependency-injection/bg/services.texy deleted file mode 100644 index 3328859a8f..0000000000 --- a/dependency-injection/bg/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Дефиниране на сървиси -********************* - -.[perex] -Конфигурацията е мястото, където учим DI контейнера как да изгражда отделните сървиси и как да ги свързва с други зависимости. Nette предоставя много прегледен и елегантен начин да се постигне това. - -Секцията `services` в конфигурационния файл във формат NEON е мястото, където дефинираме собствени сървиси и техните конфигурации. Нека разгледаме прост пример за дефиниция на сървис, наречен `database`, който представлява инстанция на класа `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Посочената конфигурация ще доведе до следния фабричен метод в [DI контейнера|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Имената на сървисите ни позволяват да се позоваваме на тях в други части на конфигурационния файл, във формат `@имеНаСървис`. Ако не е необходимо сървисът да се именува, можем просто да използваме само тире: - -```neon -services: - - PDO('sqlite::memory:') -``` - -За да получим сървис от DI контейнера, можем да използваме метода `getService()` с името на сървиса като параметър, или метода `getByType()` с типа на сървиса: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Създаване на сървис -=================== - -Обикновено създаваме сървис просто като създадем инстанция на определен клас. Например: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Ако е необходимо да разширим конфигурацията с допълнителни ключове, дефиницията може да се разпише на няколко реда: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Ключът `create` има псевдоним `factory`, и двата варианта са често срещани в практиката. Въпреки това препоръчваме да използвате `create`. - -Аргументите на конструктора или създаващия метод могат алтернативно да бъдат записани в ключа `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Сървисите не е задължително да се създават само чрез просто създаване на инстанция на клас, те могат да бъдат и резултат от извикване на статични методи или методи на други сървиси: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Обърнете внимание, че за простота вместо `->` се използва `::`, вижте [#изразителни средства]. Ще се генерират тези фабрични методи: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI контейнерът трябва да знае типа на създадения сървис. Ако създаваме сървис чрез метод, който няма указан тип на връщаната стойност, трябва изрично да посочим този тип в конфигурацията: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Аргументи -========= - -Предаваме аргументи на конструктора и методите по начин, много подобен на самия PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -За по-добра четимост можем да разпишем аргументите на отделни редове. В такъв случай използването на запетаи е по избор: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Можете също да именувате аргументите и тогава не е нужно да се притеснявате за техния ред: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Ако искате да пропуснете някои аргументи и да използвате тяхната стойност по подразбиране или да вмъкнете сървис чрез [autowiring|autowiring], използвайте долна черта: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Като аргументи могат да се предават сървиси, да се използват параметри и много повече, вижте [#изразителни средства]. - - -Setup -===== - -В секцията `setup` дефинираме методите, които трябва да се извикат при създаването на сървиса. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Това в PHP би изглеждало така: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Освен извикване на методи, може също да се предават стойности на свойства. Поддържа се и добавяне на елемент към масив, което трябва да се запише в кавички, за да не колидира със синтаксиса на NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Което в PHP кода би изглеждало по следния начин: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -В setup обаче могат да се извикват и статични методи или методи на други сървиси. Ако е необходимо да предадете като аргумент текущия сървис, посочете го като `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Обърнете внимание, че за простота вместо `->` се използва `::`, вижте [#изразителни средства]. Ще се генерира такъв фабричен метод: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Изразителни средства -==================== - -Nette DI ни дава изключително богати изразителни средства, с помощта на които можем да запишем почти всичко. В конфигурационните файлове така можем да използваме [параметри |configuration#Параметри]: - -```neon -# параметър -%wwwDir% - -# стойност на параметър под ключ -%mailer.user% - -# параметър вътре в низ -'%wwwDir%/images' -``` - -Освен това да създаваме обекти, да извикваме методи и функции: - -```neon -# създаване на обект -DateTime() - -# извикване на статичен метод -Collator::create(%locale%) - -# извикване на PHP функция -::getenv(DB_USER) -``` - -Да се позоваваме на сървиси или по тяхното име, или чрез типа: - -```neon -# сървис по име -@database - -# сървис по тип -@Nette\Database\Connection -``` - -Да използваме first-class callable синтаксис: .{data-version:3.2.0} - -```neon -# създаване на callback, аналог на [@user, logout] -@user::logout(...) -``` - -Да използваме константи: - -```neon -# константа на клас -FilesystemIterator::SKIP_DOTS - -# глобална константа се получава с PHP функцията constant() -::constant(PHP_VERSION) -``` - -Извикванията на методи могат да се верижат точно както в PHP. Само за простота вместо `->` се използва `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Тези изрази можете да използвате навсякъде, при [създаване на сървиси |#Създаване на сървис], в [#аргументи], в секцията [#setup] или [параметри |configuration#Параметри]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Специални функции ------------------ - -В конфигурационните файлове можете да използвате тези специални функции: - -- `not()` отрицание на стойност -- `bool()`, `int()`, `float()`, `string()` преобразуване без загуба към дадения тип -- `typed()` създава масив от всички сървиси от указания тип -- `tagged()` създава масив от всички сървиси с дадения таг - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -В сравнение с класическото преобразуване в PHP, като например `(int)`, преобразуването без загуба ще хвърли изключение за нечислови стойности. - -Функцията `typed()` създава масив от всички сървиси от дадения тип (клас или интерфейс). Пропуска сървисите, които имат изключен autowiring. Могат да се посочат и повече типове, разделени със запетая. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Масив от сървиси от определен тип можете да предавате като аргумент и автоматично чрез [autowiring |autowiring#Масив от сървиси]. - -Функцията `tagged()` пък създава масив от всички сървиси с определен таг. И тук можете да специфицирате повече тагове, разделени със запетая. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Ключът `autowired` позволява да се повлияе на поведението на autowiring за конкретен сървис. За детайли вижте [глава за autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # сървисът foo е изключен от autowiring -``` - - -Lazy сървиси .{data-version:3.2.4} -================================== - -Lazy loading е техника, която отлага създаването на сървис до момента, в който той действително е необходим. В глобалната конфигурация може да се [активиране на lazy създаване |configuration#Lazy сървиси] за всички сървиси наведнъж. За отделни сървиси след това можете да презапишете това поведение: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Когато сървисът е дефиниран като lazy, при неговото изискване от DI контейнера получаваме специален прокси обект. Той изглежда и се държи точно като реалния сървис, но реалната инициализация (извикване на конструктора и setup) се извършва едва при първото извикване на някой от неговите методи или свойства. - -.[note] -Lazy loading може да се използва само за потребителски класове, а не за вътрешни PHP класове. Изисква PHP 8.4 или по-нова версия. - - -Тагове -====== - -Таговете служат за добавяне на допълнителна информация към сървисите. На сървис можете да добавите един или повече тагове: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Таговете могат също да носят стойности: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -За да получите всички сървиси с определени тагове, можете да използвате функцията `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -В DI контейнера можете да получите имената на всички сървиси с определен таг чрез метода `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names е масив, съдържащ името на сървиса и стойността на тага -// напр. ['foo' => 'monolog.logger.event', ...] -``` - - -Режим Inject -============ - -С помощта на флага `inject: true` се активира предаването на зависимости чрез публични променливи с анотация [inject |best-practices:inject-method-attribute#Атрибути Inject] и методи [inject*() |best-practices:inject-method-attribute#Методи inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -По подразбиране `inject` е активирано само за презентери. - - -Модификация на сървиси -====================== - -DI контейнерът съдържа много сървиси, които са били добавени чрез вградено или [потребителско разширение|extensions]. Можете да променяте дефинициите на тези сървиси директно в конфигурацията. Например, можете да промените класа на сървиса `application.application`, който стандартно е `Nette\Application\Application`, на друг: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Флагът `alteration` е информативен и казва, че само модифицираме съществуващ сървис. - -Можем също да допълним setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -При презаписване на сървис можем да искаме да премахнем оригиналните аргументи, елементи от setup или тагове, за което служи `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Ако искате да премахнете сървис, добавен от разширение, можете да го направите така: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/cs/@home.texy b/dependency-injection/cs/@home.texy index 4386fac50d..cda285320d 100644 --- a/dependency-injection/cs/@home.texy +++ b/dependency-injection/cs/@home.texy @@ -18,4 +18,5 @@ Balíček `nette/di` poskytuje nesmírně pokročilý kompilovaný DI kontejner - [Definování služeb |services] - [Autowiring |autowiring] - [Generované továrny |factory] -- [Tvorba rozšíření pro Nette DI|extensions] +- [Tvorba rozšíření pro Nette DI |extensions] +- [Kompilace kontejneru do hloubky |compilation-internals] diff --git a/dependency-injection/cs/@left-menu.texy b/dependency-injection/cs/@left-menu.texy index ed7e4452b3..9f2bef9040 100644 --- a/dependency-injection/cs/@left-menu.texy +++ b/dependency-injection/cs/@left-menu.texy @@ -14,4 +14,17 @@ Nette DI - [Definování služeb |services] - [Autowiring |autowiring] - [Generované továrny |factory] -- [Tvorba rozšíření pro Nette DI|extensions] +- [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/autowiring.texy b/dependency-injection/cs/autowiring.texy index a03edb3d11..04e4691507 100644 --- a/dependency-injection/cs/autowiring.texy +++ b/dependency-injection/cs/autowiring.texy @@ -30,7 +30,9 @@ class ArticleRepository } ``` -Aby bylo možné použit autowiring, musí pro každý typ být v kontejneru **právě jedna služba**. Pokud by jich bylo víc, autowiring by nevěděl, kterou z nich předat a vyhodil by výjimku: +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: @@ -39,13 +41,13 @@ services: articles: Model\ArticleRepository # VYHODÍ VÝJIMKU, vyhovuje mainDb i tempDb ``` -Řešením by bylo buď autowiring obejít a explicitně uvést název služby (tj `articles: Model\ArticleRepository(@mainDb)`). Šikovnější ale je autowirování jedné ze služeb [vypnout |#Vypnutí autowiringu], nebo první službu [upřednostnit |#Preference autowiringu]. +Ř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: no`: +Autowirování služby můžeme vypnout pomocí volby `autowired: false`: ```neon services: @@ -55,13 +57,15 @@ services: create: PDO('sqlite::memory:') autowired: false # služba tempDb je vyřazena z autowiringu - articles: Model\ArticleRepository # tudíž předá do kontruktoru mainDb + 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žba nemá být pomocí autowiringu nikam předávána. +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 @@ -102,7 +106,7 @@ class ShipManager } ``` -DI kontejner pak automaticky předá pole služeb odpovídajících danému typu. Vynechá služby, které mají vypnutý autowiring. +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]. @@ -110,7 +114,7 @@ Typ v komentáři může být také ve tvaru `array<int, Class>` nebo `list<Clas Skalární argumenty ------------------ -Autowiring umí dosazovat pouze objekty a pole objektů. Skalární argumenty (např. řetězce, čísla, booleany) [zapíšeme v konfiguraci |services#Argumenty]. Alternativnou je vytvořit [settings-objekt |best-practices:passing-settings-to-presenters], který skalární hodnotu (nebo více hodnot) zapouzdří do podoby objektu, a ten pak lze opět předávat pomocí autowiringu. +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 @@ -130,7 +134,24 @@ services: - MySettings('any value') ``` -Všechny třídy si jej poté vyžádají pomocí autowiringu. +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 @@ -138,7 +159,7 @@ Zúžení autowiringu Jednotlivým službám lze autowiring zúžit jen na určité třídy nebo rozhraní. -Normálně autowiring službu předá do každého parametru metody, jehož typu služba odpovídá. Zúžení znamená, že stanovíme podmínky, kterým musí typy uvedené u parametrů metod vyhovovat, aby jim byla služba předaná. +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: @@ -172,7 +193,7 @@ services: childDep: ChildDependent # autowiring předá do konstruktoru službu child ``` -Služba `parentDep` vyhodí výjimku `Multiple services of type ParentClass found: parent, child`, protože do jejího kontruktoru pasují obě služby `parent` i `child`, a autowiring nemůže rozhodnout, kterou z nich zvolit. +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`: @@ -187,14 +208,14 @@ services: childDep: ChildDependent # autowiring předá do konstruktoru službu child ``` -Nyní se do kontruktoru služby `parentDep` předá služba `parent`, protože teď je to jediný vyhovující objekt. Službu `child` už tam autowiring nepředá. Ano, služba `child` je stále typu `ParentClass`, ale už neplatí zužující podmínka daná pro typ parametru, tj. neplatí, že `ParentClass` *je nadtyp* `ChildClass`. +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: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` Zkusme příklad doplnit ještě o rozhraní: @@ -239,11 +260,11 @@ class ChildDependent Když službu `child` nijak neomezíme, bude pasovat do konstruktorů všech tříd `FooDependent`, `BarDependent`, `ParentDependent` i `ChildDependent` a autowiring ji tam předá. -Pokud její autowiring ale zúžíme na `ChildClass` pomocí `autowired: ChildClass` (nebo `self`), předá ji autowiring pouze do konstruktoru `ChildDependent`, protože vyžaduje argument typu `ChildClass` a platí, že `ChildClass` *je typu* `ChildClass`. Žádný další typ uvedený u dalších parametrů není nadtypem `ChildClass`, takže se služba nepředá. +Pokud její 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 nadtyp `ParentClass` a nově i do konstruktoru `ParentDependent`, protože vyžadovaný typ `ParentClass` je taktéž vyhovující. +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 nadtyp `FooInterface`) a `ChildDependent`, ale navíc i do konstruktoru `FooDependent`, nikoliv však do `BarDependent`, neboť `BarInterface` není nadtyp `FooInterface`. +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: diff --git a/dependency-injection/cs/compilation-internals.texy b/dependency-injection/cs/compilation-internals.texy new file mode 100644 index 0000000000..0c6958e52f --- /dev/null +++ b/dependency-injection/cs/compilation-internals.texy @@ -0,0 +1,222 @@ +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. diff --git a/dependency-injection/cs/configuration.texy b/dependency-injection/cs/configuration.texy index d89e0ab829..f85094b931 100644 --- a/dependency-injection/cs/configuration.texy +++ b/dependency-injection/cs/configuration.texy @@ -8,7 +8,7 @@ Přehled konfiguračních voleb pro Nette DI kontejner. Konfigurační soubor =================== -Nette DI kontejner se snadno ovládá pomocí konfiguračních souborů. Ty se obvykle zapisují ve [formátu NEON|neon:format]. K editaci doporučujeme [editory s podporou |best-practices:editors-and-tools#IDE editor] tohoto formátu. +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> @@ -27,7 +27,7 @@ Chcete-li zapsat řetězec obsahující znak `%`, musíte jej escapovat zdvojen Parametry ========= -V konfiguraci můžete definovat parametry, které lze pak použít jako součást definic služeb. Čímž můžete zpřehlednit konfiguraci nebo sjednotit a vyčlenit hodnoty, které se budou měnit. +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: @@ -51,7 +51,7 @@ parameters: Na konkrétní klíč se odkážeme jako `%mailer.user%`. -Pokud potřebujete ve vašem kódu, například třídě, zjistit hodnotu jakékoliv parametru, tak jej do této třídy předejte. Například v konstruktoru. Neexistuje žádný globální objekt představující konfiguraci, kterého by se třídy dotazovaly na hodnoty parametrů. To by bylo porušením principu dependency injection. +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 @@ -92,7 +92,7 @@ Technické nastavení DI kontejneru. ```neon di: # zobrazit DIC v Tracy Bar? - debugger: ... # (bool) výchozí je true + debugger: ... # (bool) výchozí je autodetekce (zapne se, když je přítomná Tracy) # typy parametrů, které nikdy neautowirovat excluded: ... # (string[]) @@ -108,7 +108,7 @@ di: 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 requestu skutečně potřeba. +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]. @@ -145,11 +145,11 @@ Výrazně můžete zredukovat metadata pro [autowiring] tím, že uvedete tříd Rozšíření ========= -Registrace dalších DI rozšíření. Tímto způsobem přidáme např. DI rozšíření `Dibi\Bridges\Nette\DibiExtension22` pod názvem `dibi` +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\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` Následně ho tedy konfigurujeme v sekci `dibi`: @@ -163,7 +163,7 @@ Jako rozšíření lze přidat i třídu, která má parametry: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` @@ -208,6 +208,13 @@ search: - 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 @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -Lze definovat i vylučující pravidla, tj. masky názvu třídy nebo dědičné předky, které pokud vyhovují, služba se do DI kontejneru nepřidá: +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: @@ -247,7 +254,7 @@ search: implements: ... ``` -Všem službám lze nastavit tagy: +Všem automaticky registrovaným službám lze nastavit tagy: ```neon search: @@ -255,6 +262,8 @@ search: 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í ========= diff --git a/dependency-injection/cs/container.texy b/dependency-injection/cs/container.texy index 0f861f22c6..b364003571 100644 --- a/dependency-injection/cs/container.texy +++ b/dependency-injection/cs/container.texy @@ -2,15 +2,15 @@ Co je DI kontejner? ******************* .[perex] -Dependency injection kontejner (DIC) je třída, která umí instancovat a konfigurovat objekty. +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 `UserController`. Obě mají nějaké závislosti, a to databázi a továrnu `ArticleFactory`. A pro tyto třídy si nyní vytvoříme kontejner. Samozřejmě pro tak jednoduchý příklad nemá smysl mít kontejner. Ale vytvoříme ho, abychom si ukázali, jak vypadá a funguje. +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ý hardcoded kontejner pro uvedený příklad: +Zde je jednoduchý kontejner s natvrdo zapsanými hodnotami pro uvedený příklad: ```php class Container @@ -25,9 +25,9 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` @@ -36,7 +36,7 @@ Použití by vypadalo následovně: ```php $container = new Container; -$controller = $container->createUserController(); +$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. @@ -70,7 +70,7 @@ $container = new Container([ ]); ``` -Bystří čtenáři si možná všimli jistého problému. Pokaždé, když získám objekt `UserController`, vytvoří se také nová instance `ArticleFactory` a databáze. To rozhodně nechceme. +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: @@ -112,9 +112,9 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` @@ -130,11 +130,11 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$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í, ž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. +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. diff --git a/dependency-injection/cs/extensions.texy b/dependency-injection/cs/extensions.texy index 1263e7deff..ba3dee165b 100644 --- a/dependency-injection/cs/extensions.texy +++ b/dependency-injection/cs/extensions.texy @@ -2,38 +2,66 @@ Tvorba rozšíření pro Nette DI ***************************** .[perex] -Generování DI kontejneru kromě konfiguračních souborů ovlivňují ještě tzv *rozšíření*. Aktivujeme je v konfiguračním souboru v sekci `extensions`. +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. -Takto přidáme rozšíření reprezentované třídou `BlogExtension` pod názvem `blog`: +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 ``` -Každé rozšíření kompileru dědí od [api:Nette\DI\CompilerExtension] a může implementovat následující metody, které jsou postupně volány během sestavování DI kontejneru: +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í. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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]) -getConfigSchema() .[method] -=========================== +.[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. -Tato metoda se volá jako první. Definuje schema pro validaci konfiguračních parametrů. +.[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]. -Rozšíření konfigurujeme v sekci, jejíž název je stejný jako ten, pod kterým bylo rozšíření přidáno, tedy `blog`: + +První rozšíření +=============== + +Tady je malé, ale kompletní rozšíření. Aktivujeme ho a nakonfigurujeme ve stejném souboru: ```neon -# stejné jméno jako má extension +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Vytvoříme schema popisující všechny konfigurační volby včetně jejich typů, povolených hodnot a případně i výchozích hodnot: +A toto je celá třída: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -Dokumentaci najdete na stránce [Schema |schema:]. Navíc lze určit, které volby mohou být [dynamické |application:bootstrapping#Dynamické parametry] pomocí `dynamic()`, např. `Expect::int()->dynamic()`. -Ke konfiguraci se dostaneme přes proměnnou `$this->config`, což je objekt `stdClass`: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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. -loadConfiguration() .[method] -============================= +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í. -Používá se přidání služeb do kontejneru. K tomu slouží [api:Nette\DI\ContainerBuilder]: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -Konvence je prefixovat služby přidané rozšířením jeho názvem, aby nevznikaly jmenné konflikty. To dělá metoda `prefix()`, takže pokud se rozšíření jmenuje `blog`, služba ponese název `blog.articles`. +Zvalidovaná konfigurace je k dispozici v `$this->config` jako objekt `stdClass` (nebo jako pole, pokud ke schématu připojíte `castTo('array')`). -Pokud potřebujeme přejmenovat službu, můžeme kvůli zachování zpětné kompatibility vytvořit alias s původním názvem. Podobně to dělá Nette např. u služby `routing.router`, která je dostupná i pod dřívějším názvem `router`. +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 -$builder->addAlias('router', 'routing.router'); +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í: -Načtení služeb ze souboru -------------------------- +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -Služby nemusíme vytvářet jen pomocí API třídy ContainerBuilder, ale i známým zápisem používaným v konfiguračním souboru NEON v sekci services. Prefix `@extension` představuje aktuální extension. +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: @@ -107,88 +160,284 @@ services: comments: create: MyBlog\CommentsModel(@connection, @extension.articles) +``` - articlesList: - create: MyBlog\Components\ArticlesList(@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'], + ); +} ``` -Služby načteme: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // načtení konfiguračního souboru pro rozšíření - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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) { + // ... +} +``` -beforeCompile() .[method] -========================= -Metoda se volá ve chvíli, kdy kontejner obsahuje všechny služby přidané jednotlivými rozšířeními v metodách `loadConfiguration` a taktéž uživatelskými konfiguračními soubory. V této fázi sestavování tedy můžeme definice služeb upravovat nebo doplnit vazby mezi nimi. Pro vyhledávání služeb v kontejneru podle tagů lze využít metodu `findByTag()`, podle třídy či rozhraní zase metodu `findByType()`. +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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ý. -afterCompile() .[method] -======================== -V této fázi už je třída kontejneru vygenerována v podobě objektu [ClassType |php-generator:#Třídy], obsahuje všechny metody, které vytváří služby, a je připravena na zápis do cache. Výsledný kód třídy můžeme v této chvíli ještě upravit. +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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Typickým využitím je registrace Tracy panelu jen ve vývojářském režimu. + -$initialization .[method] -========================= +Složitější argumenty +-------------------- -Třída Configurator po [vytvoření kontejneru |application:bootstrapping#index.php] volá inicializační kód, který se vytváří zápisem do objektu `$this->initialization` pomocí [metody addBody() |php-generator:#Těla metod a funkcí]. +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í: -Ukážeme si příklad, jak třeba inicializačním kódem nastartovat session nebo spustit služby, které mají tag `run`: +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // automatické startování session - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // tento tag se bude exportovat vždy, i když je export omezený + $this->compiler->addExportedTag('event.subscriber'); - // služby s tagem run musejí být vytvořeny po instancování kontejneru - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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. diff --git a/dependency-injection/cs/factory.texy b/dependency-injection/cs/factory.texy index a90573d587..1fae06c265 100644 --- a/dependency-injection/cs/factory.texy +++ b/dependency-injection/cs/factory.texy @@ -4,9 +4,9 @@ Generované továrny .[perex] Nette DI umí automaticky generovat kód továren na základě rozhraní, což vám ušetří psaní kódu. -Továrna je třída, která vyrábí a konfiguruje objekty. Předává jim tedy i jejich závislosti. 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 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]: +Jak taková továrna vypadá, jsme si ukázali v [úvodní kapitole |introduction#Továrna]: ```php class ArticleFactory @@ -75,7 +75,7 @@ class UserController Parametrizovaná továrna ======================= -Tovární metoda `create` může přijímat parametry, které poté předá do konstrukturu. Doplňme například třídu `Article` o ID autora článku: +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 @@ -111,7 +111,7 @@ services: 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ř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`: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -Nebo naopak pokud by `create()` parametr `$authorId` přijimala, ale nebyl by součástí konstruktoru a předával se metodou `Article::setAuthorId()`, odkázali bychom se na něj v sekci `setup`: +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: @@ -139,9 +139,9 @@ Accessor Nette umí krom továren generovat i tzv. accessory. Jde o objekty s metodou `get()`, která vrací určitou službu z DI kontejneru. Opakované volání `get()` vrací stále tutéž instanci. -Accessor poskytují závislostem lazy-loading. Mějme třídu, která zapisuje chyby do speciální databáze. Když by si tato třída nechávala připojení k databázi předávat jako závislost konstruktorem, muselo by se připojení vždycky vytvořit, ačkoliv v praxi se chyba objeví jen výjimečně a tedy povětšinou by zůstalo spojení nevyužité. Místo toho si tak třída předá accessor a teprve když se zavolá jeho `get()`, dojde k vytvoření objektu databáze: +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 metodu s názvem `get` a deklarovat návratový typ: +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 @@ -163,7 +163,8 @@ Protože accessor vrací službu typu `PDO` a v konfiguraci je jediná taková s 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ř.: + +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 @@ -175,7 +176,7 @@ interface MultiFactory 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 parameterem: +Alternativně lze místo několika metod použít `get()` s parametrem: ```php interface MultiFactoryAlt @@ -184,17 +185,19 @@ interface MultiFactoryAlt } ``` -Pak platí, že `MultiFactory::getArticle()` dělá totéž jako `MultiFactoryAlt::get('article')`. Nicméně alternativní zápis má tu nevýhodu, že není zřejmé, jaké hodnoty `$name` jsou podporované a logicky také nelze v rozhraní odlišit různé návratové hodnoty pro různé `$name`. +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 ----------------- -Tímto způsobem lze definovat vícenásobnou továrnu v konfiguraci: .{data-version:3.2.0} +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() + article: Article() # definuje createArticle() db: PDO(%dsn%, %user%, %password%) # definuje getDb() ) ``` @@ -215,12 +218,16 @@ services: Definice pomocí tagů -------------------- -Druhou možností je využít k definici [tagy |services#Tagy]: +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: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + article: + create: Article + tags: {multi: article} # definuje createArticle() + db: + create: PDO(%dsn%, %user%, %password%) + tags: {multi: db} # definuje getDb() + + - MultiFactory(tagged: multi) ``` diff --git a/dependency-injection/cs/faq.texy b/dependency-injection/cs/faq.texy index e9842c0bba..f2a9006bb8 100644 --- a/dependency-injection/cs/faq.texy +++ b/dependency-injection/cs/faq.texy @@ -17,15 +17,15 @@ 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. +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 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. +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 objeky 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. +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? @@ -35,9 +35,9 @@ Obnáší použití Dependency Injection nějaké nevýhody, jako například zv 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 lze snadno generovat pomocí Nette DI a pluginu pro PhpStorm kliknutím myší. Na druhou stranu odpadá potřeba psát singletony a statické přístupové body. +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. +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? @@ -46,12 +46,12 @@ 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 identifikujete klíčové komponenty a jejich závislosti. Vytvořte plán, které části budou refaktorovány a v jakém pořadí. +- 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žitelnosti 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. +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í? @@ -62,7 +62,7 @@ Je vhodnější používat [kompozici |nette:introduction-to-object-oriented-pro 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ůžte začít využívat výhody Dependency Injection ve svých projektech. +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]. @@ -70,7 +70,7 @@ Jak vypadá konkrétní použití včetně kódů popisuje kapitola [Nette DI Co 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 Symfony & YAMLu nebylo možné zapsat buď vůbec, nebo jen prostřednictvím složitého opisu. +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ů? @@ -78,7 +78,7 @@ 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. +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? @@ -88,7 +88,7 @@ Mějme na paměti [Pravidlo č. 1: nech si to předat |introduction#Pravidlo č. V této ukázce je `%myParameter%` zástupný symbol pro hodnotu parametru `myParameter`, který se předá do konstruktoru třídy `MyClass`: -```php +```neon # config.neon parameters: myParameter: Some value @@ -103,4 +103,21 @@ Chcete-li předávat více parametrů nebo využít autowiring, je vhodné [para Podporuje Nette PSR-11: Container interface? -------------------------------------------- -Nette DI Container nepodporuje PSR-11 přímo. Nicméně, pokud potřebujete interoperabilitu mezi Nette DI Containerem a knihovnami nebo frameworky, které očekávají PSR-11 Container Interface, můžete vytvořit [jednoduchý adaptér |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], který bude sloužit jako most mezi Nette DI Containerem a PSR-11. +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. diff --git a/dependency-injection/cs/global-state.texy b/dependency-injection/cs/global-state.texy index d152d69973..6860252421 100644 --- a/dependency-injection/cs/global-state.texy +++ b/dependency-injection/cs/global-state.texy @@ -19,7 +19,7 @@ V této kapitole si vysvětlíme, proč tomu tak je, a jak se globálnímu stavu 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. +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`. @@ -32,10 +32,10 @@ Z hlediska chování není rozdíl mezi globální a statickou proměnnou. Jsou Strašidelné působení na dálku ----------------------------- -"Strašidelné působení na dálku" - tak slavně nazval roku 1935 Albert Einstein jev v kvantové fyzice, který mu naháněl husí kůži. +"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í 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. +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: @@ -49,13 +49,13 @@ function testCreditCardCharge() 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ěl jako nový vývojář vědět, že to, co se chystáte udělat, povede k tomu, že budete o 100 dolarů chudší? +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 credentials 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í. +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.** @@ -74,7 +74,7 @@ A hlavně nyní můžete platební bránu mockovat, takže se vám při každém 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á akce 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ý. +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. @@ -93,7 +93,7 @@ Musíte podrobně procházet kód, abyste zjistili, že objekt `PaymentGateway` ```php $db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` Podobný problém se objevuje i při použití globálního přístupu k databázovému spojení: @@ -145,7 +145,7 @@ class Article 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ímu stav činní naši aplikaci **nesmírně křehkou**. +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. @@ -169,7 +169,7 @@ 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í. +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. @@ -177,7 +177,7 @@ Nepište kód, který využívá globální stav, dejte přednost předávání 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: +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 @@ -218,7 +218,7 @@ 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 nám nepřináší žádnou novou (`M_PI`) nebo užitečnou (`PREG_BACKTRACK_LIMIT_ERROR`) informaci, 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í. +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 = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -V tomto případě bychom měli deklarovat parametr v konstruktoru třídy `Foo`, aby se stal součástí API: +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 @@ -259,15 +259,15 @@ Nyní můžeme předat informaci o cestě k souboru pro logování a snadno ji m Globální funkce a statické metody --------------------------------- -Chceme zdůranit, ž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. +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 `DateTime::createFromFormat()`, `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. +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 bylo nahradit jej za dependency injection. +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? @@ -283,12 +283,14 @@ Shrnutí Probrali jsme si, proč má smysl: -1) Odstranit veškeré statické proměnné z kódu +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é `static $foo` představuje 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. +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.* diff --git a/dependency-injection/cs/introduction.texy b/dependency-injection/cs/introduction.texy index 3b55474f2b..b31b4c5f53 100644 --- a/dependency-injection/cs/introduction.texy +++ b/dependency-injection/cs/introduction.texy @@ -2,9 +2,9 @@ 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žitelného kódu. +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 tyto 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. +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. @@ -25,7 +25,7 @@ echo soucet(23, 1); // vypíše 24 Pár triviálních řádků kódu, ale přitom se v nich skrývá tolik klíčových konceptů. Že existují proměnné. Že se kód člení do menších jednotek, což jsou kupříkladu funkce. Že jim předáváme vstupní argumenty a ony vracejí výsledky. Chybí tam už jen podmínky a cykly. -To, že funkci předáme vstupní data a ona vrátí výsledek, je perfektně srozumitelný koncept, který se používá i v jiných oborech, jako je třeba v matematice. +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. @@ -77,7 +77,7 @@ Návrh, který jsme si právě ukázali, je esencí mnoha negativních rysů: 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: +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 @@ -93,14 +93,14 @@ 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 sami, jednoduše parametry předejte. Ušetříte čas potřebný na vymýšlení skrytých cest, které rozhodně váš kód nevylepší. +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. +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 @@ -136,7 +136,6 @@ class Soucet { return $this->a + $this->b; } - } $soucet = new Soucet(23, 1); @@ -149,7 +148,7 @@ Obě ukázky jsou zcela v souladu s dependency injection. Reálné příklady --------------- -V reálném světe nebudete psát třídy pro sčítání čísel. Pojďme se přesunout k příkladům z praxe. +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: @@ -203,7 +202,7 @@ 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, nastoupili jsme cestu ke špinavému kódu plného skrytých vazeb, nesrozumitelnosti, a výsledkem bude aplikace, kterou bude bolest udržovat a vyvíjet. +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? @@ -245,7 +244,7 @@ class Article ``` .[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, a snaze uvádět jednoduché příklady. +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. @@ -254,7 +253,7 @@ A co třeba tato třída, která loguje chybové zprávy: ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -276,7 +275,7 @@ $logger->log('Teplota je 23 °C'); $logger->log('Teplota je 10 °C'); ``` -Bez znalosti implementace, dokázali byste zodpovědět otázku, kam se zprávy zapisují? Napadlo by vás, že pro fungování je potřeba existence konstanty `LOG_DIR`? A dokázali byste vytvořit druhou instanci, která bude zapisovat jinam? Určitě ne. +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: @@ -306,9 +305,9 @@ $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ž 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í.“* +*"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. @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -Takhle ne! Cesta totiž **nepatří** mezi data, která třída `NewsletterDistributor` potřebuje; ty totiž potřebuje `Logger`. Vnímáte ten rozdíl? Třída `NewsletterDistributor` potřebuje logger jako takový. Takže ten si předáme: +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 @@ -447,7 +446,7 @@ 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í jejich instancí na továrně. +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. @@ -521,6 +520,6 @@ Na začátku této kapitoly jsme slibovali, že si ukážeme postup, jak navrhov 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 tyhle 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í. +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]. diff --git a/dependency-injection/cs/nette-container.texy b/dependency-injection/cs/nette-container.texy index d9931af978..1842d5a543 100644 --- a/dependency-injection/cs/nette-container.texy +++ b/dependency-injection/cs/nette-container.texy @@ -16,12 +16,12 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` Zápis je opravdu stručný. -Všechny závislosti deklarované v konstruktorech tříd `ArticleFactory` a `UserController` si Nette DI samo zjistí a předá díky tzv. [autowiringu|autowiring], v konfiguračním souboru proto není potřeba nic uvádět. Takže i když dojde ke změně parametrů, nemusíte v konfiguraci nic měnit. Nette kontejner automaticky přegeneruje. Vy se tam můžete soustředit čistě na vývoj aplikace. +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]. @@ -36,7 +36,7 @@ interface ArticleFactory } ``` -Celý příklad najdete [na GitHubu|https://github.com/nette-examples/di-example-doc]. +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í @@ -48,7 +48,7 @@ Nasazení knihovny Nette DI do aplikace je velmi snadné. Nejprve ji nainstaluje composer require nette/di ``` -Následující kód vytvoří instanci DI kontejneru podle konfigurace uložené v souboru `config.neon`: +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'); @@ -60,21 +60,83 @@ $container = new $class; Kontejner se vygeneruje jen jednou, jeho kód se zapíše do cache (adresář `__DIR__ . '/temp'`) a při dalších požadavcích se už jen odsud načítá. -Pro vytvoření a získání služeb slouží metody `getService()` nebo `getByType()`. Takto vytvoříme objekt `UserController`: +`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 -$controller = $container->getByType(UserController::class); +$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 auto-refresh mód, kdy se kontejner automaticky přegeneruje, pokud dojde ke změně jakékoliv třídy nebo konfiguračního souboru. Stačí v konstruktoru `ContainerLoader` uvést jako druhý argument `true`. +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í limitované na aplikace psané v Nette Frameworku, můžete jej pomocí pouhých 3 řádků kódu nasadit kdekoliv. Pokud však vyvíjíte aplikace v Nette Framework, konfiguraci a vytvoření kontejneru má na starosti [Bootstrap |application:bootstrapping#Konfigurace DI kontejneru]. +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]. diff --git a/dependency-injection/cs/passing-dependencies.texy b/dependency-injection/cs/passing-dependencies.texy index f5c20e3084..f182be3f93 100644 --- a/dependency-injection/cs/passing-dependencies.texy +++ b/dependency-injection/cs/passing-dependencies.texy @@ -3,12 +3,12 @@ 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: +Argumenty, nebo v terminologii DI "závislosti", lze do tříd předávat těmito hlavními způsoby: * předávání konstruktorem * předávání metodou (tzv. setterem) * nastavením proměnné -* metodou, anotací či atributem *inject* +* metodou `inject*()` nebo atributem `#[Inject]` </div> @@ -36,7 +36,7 @@ $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ě ekvivaletní: +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 @@ -94,7 +94,7 @@ final class MyClass extends BaseClass } ``` -Problém nastane v okamžiku, kdy budeme chtít změnit kontruktor 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. +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í]**. @@ -191,7 +191,7 @@ $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 konfiraci DI kontejneru v [sekci setup |services#Setup]: +Nastavení proměnné definujeme v konfiguraci DI kontejneru v [sekci setup |services#Setup]: ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -Zatímco předchozí tři způsoby platí obecně ve všech objektově orientovaných jazycích, injektování metodou, anotací či atributem *inject* je specifické čistě pro presentery v Nette. Pojednává o nich [samostatná kapitola |best-practices:inject-method-attribute]. +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, které lze mít možnost dále měnit +- 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 diff --git a/dependency-injection/cs/services.texy b/dependency-injection/cs/services.texy index 00a55fed56..5b6df08cf2 100644 --- a/dependency-injection/cs/services.texy +++ b/dependency-injection/cs/services.texy @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -Služby nemusí být vytvářeny 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: +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: @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -DI kontejner potřebuje znát typ vytvořené služby. Pokud vytváříme službu pomocí metody, která nemá specifikovaný návratový typ, musíme tento typ explicitně uvést v konfiguraci: +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: @@ -239,7 +239,7 @@ Collator::create(%locale%) ::getenv(DB_USER) ``` -Odkazovat se na služby buď jejich jménem nebo pomocí typu: +Odkazovat se na služby buď jejich jménem, nebo pomocí typu: ```neon # služba dle názvu @@ -263,7 +263,17 @@ Používat konstanty: FilesystemIterator::SKIP_DOTS # globální konstantu získáme PHP funkcí constant() -::constant(PHP_VERSION) +::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á `::`: @@ -293,11 +303,11 @@ services: Speciální funkce ---------------- -V konfiguračních souborech můžete používa tyto 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 -- `typed()` vytvoří pole všech služeb specifikovaného typu +- `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 @@ -319,7 +329,7 @@ services: 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 specifikovat více tagů oddělených čárkou. +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: @@ -354,14 +364,18 @@ services: 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 lze použít pouze pro uživatelské třídy, nikoliv pro interní PHP třídy. Vyžaduje PHP 8.4 nebo novější. +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í k službám. Službě můžete přidat jeden nebo více tagů: +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: @@ -392,7 +406,7 @@ V DI kontejneru můžete získat názvy všech služeb s určitým tagem pomocí ```php $names = $container->findByTag('logger'); -// $names je pole obsahující název služby a hodnotu tagu +// $names je pole s názvy služeb jako klíči a hodnotami tagů jako hodnotami // např. ['foo' => 'monolog.logger.event', ...] ``` @@ -400,7 +414,7 @@ $names = $container->findByTag('logger'); Režim Inject ============ -Pomocí příznaku `inject: true` se aktivuje předávání závislostí přes veřejné proměnné s anotací [inject |best-practices:inject-method-attribute#Atributy Inject] a metody [inject*() |best-practices:inject-method-attribute#Metody 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: @@ -424,7 +438,7 @@ services: alteration: true ``` -Příznak `alteration` je informativní a říká, že jen modifikujeme existující službu. +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: @@ -437,6 +451,14 @@ services: - '$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 @@ -445,9 +467,9 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` Pokud chcete odstranit službu přidanou rozšířením, můžete to udělat takto: diff --git a/dependency-injection/cs/upgrading.texy b/dependency-injection/cs/upgrading.texy new file mode 100644 index 0000000000..b4f9c5f614 --- /dev/null +++ b/dependency-injection/cs/upgrading.texy @@ -0,0 +1,49 @@ +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í diff --git a/dependency-injection/de/@home.texy b/dependency-injection/de/@home.texy index 2f23b47fd6..ed9eb1555d 100644 --- a/dependency-injection/de/@home.texy +++ b/dependency-injection/de/@home.texy @@ -2,20 +2,21 @@ 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 gestalteter und wartbarer Anwendungen. +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 und Singletons |global-state] -- [Übergeben von Abhängigkeiten |passing-dependencies] +- [Globaler Zustand & Singletons |global-state] +- [Abhängigkeiten übergeben |passing-dependencies] - [Was ist ein DI-Container? |container] -- [Häufig gestellte Fragen|faq] +- [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] -- [Definieren von Diensten |services] +- [Service-Definitionen |services] - [Autowiring |autowiring] - [Generierte Factories |factory] -- [Erstellen von Erweiterungen für Nette DI|extensions] +- [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 index 40f65f1e59..0e123e618a 100644 --- a/dependency-injection/de/@left-menu.texy +++ b/dependency-injection/de/@left-menu.texy @@ -1,17 +1,28 @@ Dependency Injection ******************** - [Was ist DI? |introduction] -- [Globaler Zustand und Singletons |global-state] +- [Globaler Zustand & Singletons |global-state] - [Abhängigkeiten übergeben |passing-dependencies] - [Was ist ein DI-Container? |container] -- [Häufig gestellte Fragen|faq] +- [Häufig gestellte Fragen |faq] Nette DI -------- - [Nette DI Container |nette-container] - [Konfiguration |configuration] -- [Dienste definieren |services] +- [Service-Definitionen |services] - [Autowiring |autowiring] - [Generierte Factories |factory] -- [Erweiterungen für Nette DI erstellen |extensions] +- [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/autowiring.texy b/dependency-injection/de/autowiring.texy index 354c6e4e51..78bcf520d8 100644 --- a/dependency-injection/de/autowiring.texy +++ b/dependency-injection/de/autowiring.texy @@ -2,23 +2,23 @@ Autowiring ********** .[perex] -Autowiring ist eine großartige Funktion, die automatisch die benötigten Dienste an den Konstruktor und andere Methoden übergeben kann, sodass wir sie überhaupt nicht schreiben müssen. Es spart Ihnen viel Zeit. +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 die meisten Argumente beim Schreiben von Dienstdefinitionen weglassen. Anstelle von: +Dadurch können wir beim Schreiben von Service-Definitionen die allermeisten Argumente weglassen. Statt: ```neon services: articles: Model\ArticleRepository(@database, @cache.storage) ``` -Reicht es aus zu schreiben: +schreiben Sie einfach: ```neon services: articles: Model\ArticleRepository ``` -Autowiring orientiert sich an Typen, daher muss die Klasse `ArticleRepository` ungefähr so definiert sein, damit es funktioniert: +Das Autowiring richtet sich nach Typen, damit es funktioniert, muss die Klasse `ArticleRepository` also ungefähr so definiert sein: ```php namespace Model; @@ -30,22 +30,24 @@ class ArticleRepository } ``` -Um Autowiring verwenden zu können, muss für jeden Typ im Container **genau ein Dienst** vorhanden sein. Gäbe es mehr, wüsste Autowiring nicht, welchen er übergeben soll, und würde eine Ausnahme auslösen: +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 AUSNAHME, sowohl mainDb als auch tempDb passen + articles: Model\ArticleRepository # WIRFT EINE EXCEPTION, sowohl mainDb als auch tempDb passen ``` -Die Lösung wäre entweder, Autowiring zu umgehen und den Dienstnamen explizit anzugeben (d.h. `articles: Model\ArticleRepository(@mainDb)`). Geschickter ist es jedoch, das Autowiring für einen der Dienste [zu deaktivieren |#Deaktivieren des Autowirings] oder den ersten Dienst [zu bevorzugen |#Bevorzugung beim Autowiring]. +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]. -Deaktivieren des Autowirings ----------------------------- +Autowiring abschalten +--------------------- -Wir können das Autowiring eines Dienstes mit der Option `autowired: no` deaktivieren: +Über die Option `autowired: false` können wir das Autowiring für einen Service abschalten: ```neon services: @@ -53,27 +55,29 @@ services: tempDb: create: PDO('sqlite::memory:') - autowired: false # Der Dienst tempDb wird vom Autowiring ausgeschlossen + autowired: false # der Service tempDb ist vom Autowiring ausgenommen - articles: Model\ArticleRepository # übergibt daher mainDb an den Konstruktor + articles: Model\ArticleRepository # dem Konstruktor wird daher mainDb übergeben ``` -Der Dienst `articles` löst keine Ausnahme aus, dass zwei passende Dienste vom Typ `PDO` (d.h. `mainDb` und `tempDb`) existieren, die an den Konstruktor übergeben werden können, da er nur den Dienst `mainDb` sieht. +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 in Nette funktioniert anders als in Symfony, wo die Option `autowire: false` besagt, dass Autowiring nicht für die Konstruktorargumente des betreffenden Dienstes verwendet werden soll. In Nette wird Autowiring immer verwendet, sei es für Konstruktorargumente oder für andere Methoden. Die Option `autowired: false` besagt, dass die Instanz des betreffenden Dienstes nirgendwo per Autowiring übergeben werden soll. +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 --------------------------- -Wenn wir mehrere Dienste desselben Typs haben und bei einem davon die Option `autowired` angeben, wird dieser Dienst bevorzugt: +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 bevorzugt + autowired: PDO # wird zum bevorzugten tempDb: create: PDO('sqlite::memory:') @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -Der Dienst `articles` löst keine Ausnahme aus, dass zwei passende Dienste vom Typ `PDO` (d.h. `mainDb` und `tempDb`) existieren, sondern verwendet den bevorzugten Dienst, also `mainDb`. +Der Service `articles` wirft keine Exception darüber, dass mehrere `PDO`-Services passen (`mainDb` und `tempDb`), sondern verwendet den bevorzugten, also `mainDb`. -Array von Diensten ------------------- +Sammlung von Services +--------------------- -Autowiring kann auch Arrays von Diensten eines bestimmten Typs übergeben. Da in PHP der Typ der Array-Elemente nicht nativ angegeben werden kann, muss zusätzlich zum Typ `array` ein phpDoc-Kommentar mit dem Elementtyp im Format `ClassName[]` hinzugefügt werden: +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; @@ -102,45 +106,62 @@ class ShipManager } ``` -Der DI-Container übergibt dann automatisch ein Array von Diensten, die dem angegebenen Typ entsprechen. Dienste, deren Autowiring deaktiviert ist, werden ausgelassen. +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 im Format `array<int, Class>` oder `list<Class>` vorliegen. Wenn Sie die Form des phpDoc-Kommentars nicht beeinflussen können, können Sie das Array von Diensten direkt in der Konfiguration mithilfe von [`typed()` |services#Spezielle Funktionen] übergeben. +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 kann nur Objekte und Arrays von Objekten einfügen. Skalare Argumente (z. B. Zeichenketten, Zahlen, Booleans) [schreiben wir in der Konfiguration |services#Argumente]. Eine Alternative ist die Erstellung eines [Einstellungsobjekts |best-practices:passing-settings-to-presenters], das den skalaren Wert (oder mehrere Werte) in ein Objekt kapselt, welches dann wieder per Autowiring übergeben werden kann. +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 kann ab PHP 8.1 verwendet werden + // readonly lässt sich seit PHP 8.1 verwenden public readonly bool $value, ) {} } ``` -Sie erstellen daraus einen Dienst, indem Sie ihn zur Konfiguration hinzufügen: +Als Service registrieren Sie es, indem Sie es der Konfiguration hinzufügen: ```neon services: - MySettings('any value') ``` -Alle Klassen fordern ihn dann per Autowiring an. +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. -Einschränken des Autowirings ----------------------------- +Autowiring einschränken +----------------------- -Für einzelne Dienste kann das Autowiring auf bestimmte Klassen oder Schnittstellen eingeschränkt werden. +Für einzelne Services lässt sich das Autowiring auf bestimmte Klassen oder Interfaces einschränken. -Normalerweise übergibt Autowiring einen Dienst an jeden Methodenparameter, dessen Typ dem Dienst entspricht. Die Einschränkung bedeutet, dass wir Bedingungen festlegen, denen die bei den Methodenparametern angegebenen Typen entsprechen müssen, damit der Dienst an sie übergeben wird. +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. -Zeigen wir dies an einem Beispiel: +Nehmen wir ein Beispiel: ```php class ParentClass @@ -162,42 +183,42 @@ class ChildDependent } ``` -Wenn wir sie alle als Dienste registrieren würden, würde Autowiring fehlschlagen: +Würden wir sie alle als Services registrieren, scheiterte das Autowiring: ```neon services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # WIRFT EINE AUSNAHME, sowohl parent als auch child passen - childDep: ChildDependent # Autowiring übergibt den Dienst child an den Konstruktor + parentDep: ParentDependent # WIRFT EINE EXCEPTION, sowohl parent als auch child passen + childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child ``` -Der Dienst `parentDep` löst die Ausnahme `Multiple services of type ParentClass found: parent, child` aus, da beide Dienste `parent` und `child` in seinen Konstruktor passen und Autowiring nicht entscheiden kann, welchen es wählen soll. +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 Dienst `child` können wir daher sein Autowiring auf den Typ `ChildClass` einschränken: +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 # kann auch 'autowired: self' geschrieben werden + autowired: ChildClass # lässt sich auch als 'autowired: self' schreiben - parentDep: ParentDependent # Autowiring übergibt den Dienst parent an den Konstruktor - childDep: ChildDependent # Autowiring übergibt den Dienst child an den Konstruktor + parentDep: ParentDependent # das Autowiring übergibt dem Konstruktor den Service parent + childDep: ChildDependent # das Autowiring übergibt dem Konstruktor den Service child ``` -Nun wird der Dienst `parent` an den Konstruktor von `parentDep` übergeben, da er jetzt das einzige passende Objekt ist. Der Dienst `child` wird dort vom Autowiring nicht mehr übergeben. Ja, der Dienst `child` ist immer noch vom Typ `ParentClass`, aber die einschränkende Bedingung für den Parametertyp gilt nicht mehr, d.h. es gilt nicht, dass `ParentClass` *ein Supertyp* von `ChildClass` ist. +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 Dienst `child` könnte `autowired: ChildClass` auch als `autowired: self` geschrieben werden, da `self` ein Platzhalter für die Klasse des aktuellen Dienstes ist. +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` können auch mehrere Klassen oder Schnittstellen als Array angegeben werden: +Im Schlüssel `autowired` lassen sich auch mehrere Klassen oder Interfaces als Array angeben: ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -Ergänzen wir das Beispiel noch um Schnittstellen: +Versuchen wir, dem Beispiel Interfaces hinzuzufügen: ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -Wenn wir den Dienst `child` nicht einschränken, passt er in die Konstruktoren aller Klassen `FooDependent`, `BarDependent`, `ParentDependent` und `ChildDependent`, und Autowiring übergibt ihn dorthin. +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. -Wenn wir sein Autowiring jedoch auf `ChildClass` mit `autowired: ChildClass` (oder `self`) einschränken, übergibt Autowiring ihn nur an den Konstruktor von `ChildDependent`, da dieser ein Argument vom Typ `ChildClass` erfordert und `ChildClass` *vom Typ* `ChildClass` ist. Kein anderer bei den weiteren Parametern angegebener Typ ist ein Supertyp von `ChildClass`, daher wird der Dienst nicht übergeben. +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. -Wenn wir ihn auf `ParentClass` mit `autowired: ParentClass` beschränken, übergibt Autowiring ihn erneut an den Konstruktor von `ChildDependent` (da das erforderliche `ChildClass` ein Supertyp von `ParentClass` ist) und neu auch an den Konstruktor von `ParentDependent`, da der erforderliche Typ `ParentClass` ebenfalls passend ist. +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. -Wenn wir ihn auf `FooInterface` beschränken, wird er immer noch in `ParentDependent` (erforderliches `ParentClass` ist Supertyp von `FooInterface`) und `ChildDependent` autowired, aber zusätzlich auch in den Konstruktor von `FooDependent`, jedoch nicht in `BarDependent`, da `BarInterface` kein Supertyp von `FooInterface` ist. +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: @@ -251,8 +272,8 @@ services: create: ChildClass autowired: FooInterface - fooDep: FooDependent # Autowiring übergibt child an den Konstruktor - barDep: BarDependent # WIRFT EINE AUSNAHME, kein Dienst passt - parentDep: ParentDependent # Autowiring übergibt child an den Konstruktor - childDep: ChildDependent # Autowiring übergibt child an den Konstruktor + 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 new file mode 100644 index 0000000000..e4f02521ed --- /dev/null +++ b/dependency-injection/de/compilation-internals.texy @@ -0,0 +1,222 @@ +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 index 27e564e3b5..c1a7df1460 100644 --- a/dependency-injection/de/configuration.texy +++ b/dependency-injection/de/configuration.texy @@ -2,32 +2,32 @@ Konfiguration des DI-Containers ******************************* .[perex] -Übersicht über die Konfigurationsoptionen für den Nette DI Container. +Übersicht der Konfigurationsmöglichkeiten für den Nette DI Container. Konfigurationsdatei =================== -Der Nette DI Container lässt sich leicht über Konfigurationsdateien steuern. Diese werden normalerweise im [NEON-Format|neon:format] geschrieben. Zur Bearbeitung empfehlen wir [Editoren mit Unterstützung |best-practices:editors-and-tools#IDE-Editor] für dieses Format. +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]":[#Erweiterungen]: "Installation weiterer DI-Erweiterungen .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Dateien einbinden]: "Einbinden von Dateien .[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]":[#Suche]: "Automatische Registrierung von Diensten .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Dienste .[prism-token prism-comment]" +"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 eine Zeichenkette zu schreiben, die das Zeichen `%` enthält, müssen Sie es durch Verdoppelung auf `%%` escapen. +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 dann als Teil der Dienstdefinitionen verwendet werden können. Dadurch können Sie die Konfiguration übersichtlicher gestalten oder Werte vereinheitlichen und ausgliedern, die sich ändern werden. +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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -Auf den Parameter `dsn` verweisen wir überall in der Konfiguration mit der Schreibweise `%dsn%`. Parameter können auch innerhalb von Zeichenketten wie `'%wwwDir%/images'` verwendet werden. +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 Zeichenketten oder Zahlen sein, sie können auch Arrays enthalten: +Parameter müssen nicht nur Strings oder Zahlen sein, sie können auch Arrays enthalten: ```neon parameters: @@ -49,32 +49,32 @@ parameters: languages: [cs, en, de] ``` -Auf einen bestimmten Schlüssel verweisen wir als `%mailer.user%`. +Auf einen bestimmten Schlüssel verweisen wir mit `%mailer.user%`. -Wenn Sie in Ihrem Code, beispielsweise in einer Klasse, den Wert eines Parameters ermitteln müssen, übergeben Sie ihn an diese Klasse. Zum Beispiel im Konstruktor. Es gibt kein globales Objekt, das die Konfiguration repräsentiert, bei dem Klassen Parameterwerte abfragen könnten. Das würde gegen das Prinzip der Dependency Injection verstoßen. +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. -Dienste -======= +Services +======== -Siehe [separates Kapitel|services]. +Siehe [eigenes Kapitel|services]. Decorator ========= -Wie kann man alle Dienste eines bestimmten Typs massenhaft ändern? Zum Beispiel eine bestimmte Methode bei allen Presentern aufrufen, die von einem bestimmten gemeinsamen Vorfahren erben? Dafür gibt es den 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 Dienste, die Instanzen dieser Klasse oder Schnittstelle sind + # für alle Services, die Instanzen dieser Klasse oder dieses Interfaces sind App\Presentation\BasePresenter: setup: - - setProjectId(10) # rufe diese Methode auf - - $absoluteUrls = true # und setze die Variable + - setProjectId(10) # diese Methode aufrufen + - $absoluteUrls = true # und die Variable setzen ``` -Der Decorator kann auch verwendet werden, um [Tags |services#Tags] zu setzen oder den [inject |services#Inject-Modus]-Modus zu aktivieren. +Der Decorator lässt sich auch verwenden, um [Tags |services#Tags] zu setzen oder den [Inject-Modus |services#Inject-Modus] einzuschalten. ```neon decorator: @@ -91,86 +91,86 @@ Technische Einstellungen des DI-Containers. ```neon di: - # DIC in der Tracy Bar anzeigen? - debugger: ... # (bool) Standard ist true + # den DIC in der Tracy Bar anzeigen? + debugger: ... # (bool) standardmäßig automatische Erkennung (eingeschaltet, wenn Tracy vorhanden ist) - # Parametertypen, die niemals autowired werden sollen + # Typen von Parametern, die Sie nie autowiren excluded: ... # (string[]) - # lazy Erstellung von Diensten erlauben? + # das lazy Erzeugen von Services einschalten? lazy: ... # (bool) Standard ist false - # Klasse, von der der DI-Container erbt - parentClass: ... # (string) Standard ist Nette\DI\Container + # die Klasse, von der der DI-Container erbt + parentClass: ... # (string) standardmäßig Nette\DI\Container ``` -Lazy Dienste .{data-version:3.2.4} ----------------------------------- +Lazy Services .{data-version:3.2.4} +----------------------------------- -Die Einstellung `lazy: true` aktiviert die lazy (verzögerte) Erstellung von Diensten. Das bedeutet, dass Dienste nicht tatsächlich erstellt werden, wenn wir sie vom DI-Container anfordern, sondern erst im Moment ihrer ersten Verwendung. Dies kann den Start der Anwendung beschleunigen und den Speicherbedarf reduzieren, da nur die Dienste erstellt werden, die im jeweiligen Request tatsächlich benötigt werden. +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 Dienst kann die lazy Erstellung [geändert werden |services#Lazy Dienste]. +Für einen bestimmten Service lässt sich das lazy Erzeugen [anpassen |services#Lazy Services]. .[note] -Lazy Objekte können nur für benutzerdefinierte Klassen verwendet werden, nicht für interne PHP-Klassen. Erfordert PHP 8.4 oder neuer. +Lazy Objekte lassen sich nur für eigene Klassen verwenden, nicht für interne PHP-Klassen. Erfordert PHP 8.4 oder neuer. -Export von Metadaten +Export der Metadaten -------------------- -Die DI-Container-Klasse enthält auch viele Metadaten. Sie können sie verkleinern, indem Sie den Export von Metadaten reduzieren. +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) Standard ist true + parameters: false # (bool) standardmäßig true - # Tags exportieren und welche? - tags: # (string[]|bool) Standard sind alle + # Tags exportieren, und welche? + tags: # (string[]|bool) standardmäßig alle - event.subscriber - # Daten für Autowiring exportieren und welche? - types: # (string[]|bool) Standard sind alle + # Daten für das Autowiring exportieren, und welche? + types: # (string[]|bool) standardmäßig alle - Nette\Database\Connection - Symfony\Component\Console\Application ``` -Wenn Sie das Array `$container->getParameters()` nicht verwenden, können Sie den Parameter-Export deaktivieren. Weiterhin können Sie nur die Tags exportieren, über die Sie Dienste mit der Methode `$container->findByTag(...)` abrufen. Wenn Sie die Methode überhaupt nicht aufrufen, können Sie den Tag-Export mit `false` vollständig deaktivieren. +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. -Sie können die Metadaten für [Autowiring|autowiring] erheblich reduzieren, indem Sie die Klassen angeben, die Sie als Parameter der Methode `$container->getByType()` verwenden. Und wiederum, wenn Sie die Methode überhaupt nicht aufrufen (bzw. nur im [Bootstrap|application:bootstrapping], um `Nette\Application\Application` zu erhalten), können Sie den Export mit `false` vollständig deaktivieren. +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. -Erweiterungen -============= +Extensions +========== -Registrierung weiterer DI-Erweiterungen. Auf diese Weise fügen wir z. B. die DI-Erweiterung `Dibi\Bridges\Nette\DibiExtension22` unter dem Namen `dibi` hinzu +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\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -Anschließend konfigurieren wir sie im Abschnitt `dibi`: +Konfiguriert wird sie anschließend im Abschnitt `dibi`: ```neon dibi: host: localhost ``` -Als Erweiterung kann auch eine Klasse hinzugefügt werden, die Parameter hat: +Als Extension lässt sich auch eine Klasse mit Parametern hinzufügen: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` Dateien einbinden ================= -Weitere Konfigurationsdateien können wir im Abschnitt `includes` einfügen: +Weitere Konfigurationsdateien lassen sich im Abschnitt `includes` einbinden: ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -Der Name `parameters.php` ist kein Tippfehler, die Konfiguration kann auch in einer PHP-Datei geschrieben werden, die sie als Array zurückgibt: +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 @@ -192,15 +192,15 @@ return [ ]; ``` -Wenn in Konfigurationsdateien Elemente mit denselben Schlüsseln erscheinen, werden sie überschrieben oder im Falle von [Arrays zusammengeführt |#Zusammenführen]. Eine später eingebundene Datei hat eine höhere Priorität als die vorherige. Die Datei, in der der Abschnitt `includes` aufgeführt ist, hat eine höhere Priorität als die darin eingebundenen Dateien. +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. -Suche -===== +Search +====== -Das automatische Hinzufügen von Diensten zum DI-Container macht die Arbeit äußerst angenehm. Nette fügt Presenter automatisch zum Container hinzu, aber es können auch problemlos beliebige andere Klassen hinzugefügt werden. +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. -Es genügt anzugeben, in welchen Verzeichnissen (und Unterverzeichnissen) nach Klassen gesucht werden soll: +Geben Sie einfach an, in welchen Verzeichnissen (und Unterverzeichnissen) nach den Klassen gesucht werden soll: ```neon search: @@ -208,22 +208,29 @@ search: - in: %appDir%/Model ``` -Normalerweise möchten wir jedoch nicht alle Klassen und Schnittstellen hinzufügen, daher können wir sie filtern: +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 - # Filtern nach Dateiname (string|string[]) + # Filterung nach Dateiname (string|string[]) files: - *Factory.php - # Filtern nach Klassenname (string|string[]) + # Filterung nach Klassenname (string|string[]) classes: - *Factory ``` -Oder wir können Klassen auswählen, die mindestens eine der angegebenen Klassen erben oder implementieren: +Oder wir wählen die Klassen aus, die von mindestens einer der aufgeführten Klassen erben oder eines der Interfaces implementieren: ```neon @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -Es können auch Ausschlussregeln definiert werden, d. h. Masken für Klassennamen oder erbende Vorfahren, bei deren Übereinstimmung der Dienst nicht zum DI-Container hinzugefügt wird: +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: @@ -247,7 +254,7 @@ search: implements: ... ``` -Für alle Dienste können Tags gesetzt werden: +Allen automatisch registrierten Services lassen sich Tags zuweisen: ```neon search: @@ -255,11 +262,13 @@ search: 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 ============== -Wenn in mehreren Konfigurationsdateien Elemente mit denselben Schlüsseln erscheinen, werden sie überschrieben oder im Falle von Arrays zusammengeführt. Eine später eingebundene Datei hat eine höhere Priorität als die vorherige. +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> @@ -292,7 +301,7 @@ items: </tr> </table> -Bei Arrays kann das Zusammenführen durch Angabe eines Ausrufezeichens nach dem Schlüsselnamen verhindert werden: +Bei Arrays lässt sich das Zusammenführen verhindern, indem hinter den Namen des Schlüssels ein Ausrufezeichen gesetzt wird: <table class=table> <tr> diff --git a/dependency-injection/de/container.texy b/dependency-injection/de/container.texy index ac12397478..686eeb2215 100644 --- a/dependency-injection/de/container.texy +++ b/dependency-injection/de/container.texy @@ -1,16 +1,16 @@ -Was ist ein DI Container? +Was ist ein DI-Container? ************************* .[perex] -Ein Dependency Injection Container (DIC) ist eine Klasse, die Objekte instanziieren und konfigurieren kann. +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 benötigen Sie keinen Dependency Injection Container, um die Vorteile der Dependency Injection (kurz DI) zu nutzen. Schon im [Einführungskapitel |introduction] haben wir DI anhand konkreter Beispiele gezeigt, und es wurde kein Container benötigt. +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. -Wenn Sie jedoch eine große Anzahl verschiedener Objekte mit vielen Abhängigkeiten verwalten müssen, wird ein Dependency Injection Container wirklich nützlich sein. Dies ist beispielsweise bei Webanwendungen der Fall, die auf einem Framework basieren. +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 vorherigen Kapitel haben wir die Klassen `Article` und `UserController` vorgestellt. Beide haben einige Abhängigkeiten, nämlich die Datenbank und die Fabrik `ArticleFactory`. Und für diese Klassen werden wir nun einen Container erstellen. Natürlich ist es für ein so einfaches Beispiel nicht sinnvoll, einen Container zu haben. Aber wir werden ihn erstellen, um zu zeigen, wie er aussieht und funktioniert. +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 hardcodierter Container für das gegebene Beispiel: +Hier ist ein einfacher, fest verdrahteter Container für das obige Beispiel: ```php class Container @@ -25,23 +25,23 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` -Die Verwendung würde folgendermaßen aussehen: +Die Verwendung sähe so aus: ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -Wir fragen den Container nur nach dem Objekt und müssen nichts mehr darüber wissen, wie es erstellt wird oder welche Abhängigkeiten es hat; das alles weiß der Container. Die Abhängigkeiten werden vom Container automatisch injiziert. Darin liegt seine Stärke. +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. -Bisher hat der Container alle Daten fest codiert. Wir werden also den nächsten Schritt machen und Parameter hinzufügen, damit der Container wirklich nützlich wird: +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 @@ -70,9 +70,9 @@ $container = new Container([ ]); ``` -Aufmerksame Leser haben vielleicht ein Problem bemerkt. Jedes Mal, wenn ich ein `UserController`-Objekt erhalte, werden auch eine neue Instanz von `ArticleFactory` und der Datenbank erstellt. Das wollen wir definitiv nicht. +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 fügen daher eine Methode `getService()` hinzu, die immer dieselben Instanzen zurückgibt: +Wir ergänzen deshalb eine Methode `getService()`, die immer dieselben Instanzen zurückgibt: ```php class Container @@ -98,9 +98,9 @@ class Container } ``` -Beim ersten Aufruf von z. B. `$container->getService('Database')` lässt sie von `createDatabase()` das Datenbankobjekt erstellen, speichert es im Array `$services` und gibt es beim nächsten Aufruf direkt zurück. +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. -Wir passen auch den Rest des Containers an, um `getService()` zu verwenden: +Auch den Rest des Containers passen wir so an, dass er `getService()` verwendet: ```php class Container @@ -112,14 +112,14 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -Übrigens wird der Begriff Dienst (Service) für jedes Objekt verwendet, das vom Container verwaltet wird. Daher auch der Name der Methode `getService()`. +Ü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: @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -Wie Sie sehen, ist es nicht kompliziert, einen DIC zu schreiben. Es sei daran erinnert, dass die Objekte selbst nicht wissen, dass sie von einem Container erstellt werden. Daher ist es möglich, jedes Objekt in PHP auf diese Weise zu erstellen, ohne seinen Quellcode zu ändern. +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. -Das manuelle Erstellen und Warten einer Containerklasse kann schnell zu einem Albtraum werden. Im nächsten Kapitel werden wir daher über den [Nette DI Container|nette-container] sprechen, der sich fast von selbst generieren und aktualisieren kann. +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?}} +{{maintitle: Was ist ein Dependency-Injection-Container?}} diff --git a/dependency-injection/de/extensions.texy b/dependency-injection/de/extensions.texy index 62184d5d59..5a8fd3a9c7 100644 --- a/dependency-injection/de/extensions.texy +++ b/dependency-injection/de/extensions.texy @@ -1,39 +1,67 @@ -Erstellen von Erweiterungen für Nette DI -**************************************** +Extensions für Nette DI erstellen +********************************* .[perex] -Die Generierung des DI-Containers wird neben den Konfigurationsdateien auch durch sogenannte *Erweiterungen* beeinflusst. Wir aktivieren sie in der Konfigurationsdatei im Abschnitt `extensions`. +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. -So fügen wir eine Erweiterung hinzu, die durch die Klasse `BlogExtension` repräsentiert wird, unter dem Namen `blog`: +Ü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 ``` -Jede Compiler-Erweiterung erbt von [api:Nette\DI\CompilerExtension] und kann die folgenden Methoden implementieren, die während der Erstellung des DI-Containers nacheinander aufgerufen werden: +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. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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]) -getConfigSchema() .[method] -=========================== +.[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. -Diese Methode wird zuerst aufgerufen. Sie definiert das Schema zur Validierung der Konfigurationsparameter. +.[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]. -Wir konfigurieren die Erweiterung im Abschnitt, dessen Name mit dem Namen übereinstimmt, unter dem die Erweiterung hinzugefügt wurde, also `blog`: + +Die erste Extension +=================== + +Hier ist eine kleine, aber vollständige Extension. Aktiviert und konfiguriert wird sie in derselben Datei: ```neon -# gleicher Name wie die Extension +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Wir erstellen ein Schema, das alle Konfigurationsoptionen beschreibt, einschließlich ihrer Typen, erlaubten Werte und gegebenenfalls auch Standardwerte: +Und das ist die ganze Klasse: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -Die Dokumentation finden Sie auf der Seite [Schema |schema:]. Zusätzlich kann festgelegt werden, welche Optionen [dynamisch |application:bootstrapping#Dynamische Parameter] sein können, mittels `dynamic()`, z.B. `Expect::int()->dynamic()`. -Auf die Konfiguration greifen wir über die Variable `$this->config` zu, die ein `stdClass`-Objekt ist: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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. -loadConfiguration() .[method] -============================= +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. -Wird verwendet, um Dienste zum Container hinzuzufügen. Dazu dient [api:Nette\DI\ContainerBuilder]: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // oder setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -Die Konvention ist, Dienste, die durch eine Erweiterung hinzugefügt werden, mit deren Namen zu präfixieren, um Namenskonflikte zu vermeiden. Dies macht die Methode `prefix()`, sodass, wenn die Erweiterung `blog` heißt, der Dienst den Namen `blog.articles` trägt. +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 wir einen Dienst umbenennen müssen, können wir aus Gründen der Abwärtskompatibilität einen Alias mit dem ursprünglichen Namen erstellen. Ähnlich macht es Nette z. B. beim Dienst `routing.router`, der auch unter dem früheren Namen `router` verfügbar ist. +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 -$builder->addAlias('router', 'routing.router'); +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: -Laden von Diensten aus einer Datei ----------------------------------- +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -Dienste müssen nicht nur über die API der ContainerBuilder-Klasse erstellt werden, sondern auch mit der bekannten Schreibweise, die in der NEON-Konfigurationsdatei im Abschnitt `services` verwendet wird. Das Präfix `@extension` repräsentiert die aktuelle Extension. +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: @@ -107,88 +160,284 @@ services: 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: - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Wir laden die Dienste: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // Laden der Konfigurationsdatei für die Erweiterung - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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. -beforeCompile() .[method] -========================= +Sie können auch mit anderen registrierten Extensions zusammenarbeiten, die Sie über `$this->compiler->getExtensions()` bekommen, wahlweise nach Klasse oder Interface gefiltert: -Die Methode wird aufgerufen, wenn der Container alle Dienste enthält, die von den einzelnen Erweiterungen in den `loadConfiguration`-Methoden sowie durch die Benutzer-Konfigurationsdateien hinzugefügt wurden. In dieser Phase der Erstellung können wir also die Dienstdefinitionen ändern oder Abhängigkeiten zwischen ihnen hinzufügen. Zum Suchen von Diensten im Container nach Tags kann die Methode `findByTag()` verwendet werden, nach Klasse oder Schnittstelle die Methode `findByType()`. +```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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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. + -afterCompile() .[method] -======================== +ContainerBuilder +================ -In dieser Phase ist die Containerklasse bereits in Form eines [ClassType |php-generator:#Klassen]-Objekts generiert, enthält alle Methoden, die Dienste erstellen, und ist bereit, in den Cache geschrieben zu werden. Der resultierende Klassencode kann zu diesem Zeitpunkt noch geändert werden. +[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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Ein typischer Anwendungsfall ist, ein Tracy-Panel nur im Entwicklermodus zu registrieren. + -$initialization .[method] -========================= +Komplexe Argumente +------------------ -Die Configurator-Klasse ruft nach der [Erstellung des Containers |application:bootstrapping#index.php] Initialisierungscode auf, der durch Schreiben in das `$this->initialization`-Objekt mittels der [Methode addBody() |php-generator:#Körper von Methoden und Funktionen] erstellt wird. +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: -Wir zeigen ein Beispiel, wie man z. B. mit Initialisierungscode die Session startet oder Dienste startet, die das Tag `run` haben: +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // automatisches Starten der Session - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // dieser Tag wird immer exportiert, auch wenn der Export eingeschränkt ist + $this->compiler->addExportedTag('event.subscriber'); - // Dienste mit dem Tag run müssen nach der Instanziierung des Containers erstellt werden - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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 index 9d7eafb558..f82a10c1b8 100644 --- a/dependency-injection/de/factory.texy +++ b/dependency-injection/de/factory.texy @@ -1,12 +1,12 @@ -Generierte Fabriken -******************* +Generierte Factories +******************** .[perex] -Nette DI kann automatisch Code für Fabriken basierend auf Schnittstellen generieren, was Ihnen das Schreiben von Code erspart. +Nette DI kann den Code von Factories automatisch anhand von Interfaces erzeugen und erspart Ihnen damit das Schreiben von Code. -Eine Fabrik ist eine Klasse, die Objekte herstellt und konfiguriert. Sie übergibt ihnen also auch ihre Abhängigkeiten. Bitte verwechseln Sie dies nicht mit dem Entwurfsmuster *Factory Method*, das eine spezifische Art der Verwendung von Fabriken beschreibt und mit diesem Thema nichts zu tun hat. +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 Fabrik aussieht, haben wir im [Einführungskapitel |introduction#Fabrik] gezeigt: +Wie eine solche Factory aussieht, haben wir im [einführenden Kapitel |introduction#Factory] gezeigt: ```php class ArticleFactory @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DI kann den Code von Fabriken automatisch generieren. Alles, was Sie tun müssen, ist, eine Schnittstelle zu erstellen, und Nette DI generiert die Implementierung. Die Schnittstelle muss genau eine Methode namens `create` haben und einen Rückgabetyp deklarieren: +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 @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -Die Fabrik `ArticleFactory` hat also eine Methode `create`, die `Article`-Objekte erstellt. Die Klasse `Article` könnte beispielsweise so aussehen: +Die Factory `ArticleFactory` hat also eine Methode `create`, die `Article`-Objekte erzeugt. Die Klasse `Article` könnte zum Beispiel so aussehen: ```php class Article @@ -44,16 +44,16 @@ class Article } ``` -Wir fügen die Fabrik zur Konfigurationsdatei hinzu: +Fügen Sie die Factory der Konfigurationsdatei hinzu: ```neon services: - ArticleFactory ``` -Nette DI generiert die entsprechende Implementierung der Fabrik. +Nette DI erzeugt die passende Implementierung der Factory. -Im Code, der die Fabrik verwendet, fordern wir das Objekt über die Schnittstelle an, und Nette DI verwendet die generierte Implementierung: +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 @@ -65,17 +65,17 @@ class UserController public function foo() { - // lassen wir die Fabrik das Objekt erstellen + // die Factory das Objekt erzeugen lassen $article = $this->articleFactory->create(); } } ``` -Parametrisierte Fabrik +Factory mit Parametern ====================== -Die Fabrikmethode `create` kann Parameter annehmen, die sie dann an den Konstruktor weitergibt. Ergänzen wir beispielsweise die Klasse `Article` um die ID des Artikelautors: +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 @@ -88,7 +88,7 @@ class Article } ``` -Wir fügen den Parameter auch zur Fabrik hinzu: +Den Parameter ergänzen wir auch in der Factory: ```php interface ArticleFactory @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -Da der Parameter im Konstruktor und der Parameter in der Fabrik denselben Namen haben, übergibt Nette DI sie vollautomatisch. +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 kann auch in mehrzeiliger Form unter Verwendung des Schlüssels `implement` geschrieben werden: +Die Definition lässt sich über den Schlüssel `implement` auch mehrzeilig schreiben: ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -Bei dieser längeren Schreibweise können zusätzliche Argumente für den Konstruktor im Schlüssel `arguments` und zusätzliche Konfigurationen mittels `setup` angegeben werden, genau wie bei regulären Diensten. +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. -Beispiel: Wenn die Methode `create()` den Parameter `$authorId` nicht akzeptieren würde, könnten wir einen festen Wert in der Konfiguration angeben, der an den Konstruktor von `Article` übergeben würde: +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: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -Oder umgekehrt, wenn `create()` den Parameter `$authorId` akzeptieren würde, aber er nicht Teil des Konstruktors wäre und über die Methode `Article::setAuthorId()` übergeben würde, würden wir im Abschnitt `setup` darauf verweisen: +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: @@ -137,11 +137,11 @@ services: Accessor ======== -Nette kann neben Fabriken auch sogenannte Accessoren generieren. Dies sind Objekte mit einer `get()`-Methode, die einen bestimmten Dienst aus dem DI-Container zurückgibt. Wiederholte Aufrufe von `get()` geben immer dieselbe Instanz zurück. +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. -Accessoren ermöglichen Lazy-Loading für Abhängigkeiten. Nehmen wir an, wir haben eine Klasse, die Fehler in eine spezielle Datenbank schreibt. Wenn diese Klasse die Datenbankverbindung als Abhängigkeit im Konstruktor übergeben bekäme, müsste die Verbindung immer erstellt werden, obwohl in der Praxis ein Fehler nur selten auftritt und die Verbindung daher meist ungenutzt bliebe. Stattdessen übergibt sich die Klasse einen Accessor, und erst wenn dessen `get()` aufgerufen wird, wird das Datenbankobjekt erstellt: +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 erstellt man einen Accessor? Schreiben Sie einfach eine Schnittstelle, und Nette DI generiert die Implementierung. Die Schnittstelle muss genau eine Methode namens `get` haben und einen Rückgabetyp deklarieren: +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 @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -Wir fügen den Accessor zur Konfigurationsdatei hinzu, wo auch die Definition des Dienstes steht, den er zurückgeben wird: +Fügen Sie den Accessor der Konfigurationsdatei hinzu, zusammen mit der Definition des Services, den er zurückgeben soll: ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -Da der Accessor einen Dienst vom Typ `PDO` zurückgibt und in der Konfiguration nur ein solcher Dienst vorhanden ist, wird er genau diesen zurückgeben. Wenn es mehrere Dienste dieses Typs gäbe, würden wir den zurückgegebenen Dienst anhand seines Namens bestimmen, z. B. `- PDOAccessor(@db1)`. +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)`. -Mehrfachfabrik/-accessor -======================== -Unsere Fabriken und Accessoren konnten bisher immer nur ein Objekt herstellen oder zurückgeben. Es ist jedoch sehr einfach, auch Mehrfachfabriken in Kombination mit Accessoren zu erstellen. Die Schnittstelle einer solchen Klasse enthält eine beliebige Anzahl von Methoden mit den Namen `create<name>()` und `get<name>()`, z. B.: +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 @@ -173,9 +174,9 @@ interface MultiFactory } ``` -Anstatt also mehrere generierte Fabriken und Accessoren zu übergeben, übergeben wir eine komplexere Fabrik, die mehr kann. +Statt mehrere einzelne Factories und Accessors zu injizieren, können Sie also eine einzige, umfassendere Komponente injizieren. -Alternativ kann anstelle mehrerer Methoden `get()` mit einem Parameter verwendet werden: +Alternativ lässt sich statt mehrerer Methoden ein `get()` mit einem Parameter verwenden: ```php interface MultiFactoryAlt @@ -184,22 +185,24 @@ interface MultiFactoryAlt } ``` -Dann gilt, dass `MultiFactory::getArticle()` dasselbe tut wie `MultiFactoryAlt::get('article')`. Die alternative Schreibweise hat jedoch den Nachteil, dass nicht ersichtlich ist, welche Werte für `$name` unterstützt werden, und logischerweise können in der Schnittstelle auch keine unterschiedlichen Rückgabewerte für verschiedene `$name` unterschieden werden. +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 durch Liste ----------------------- -Auf diese Weise kann eine Mehrfachfabrik in der Konfiguration definiert werden: .{data-version:3.2.0} + +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() + article: Article() # definiert createArticle() db: PDO(%dsn%, %user%, %password%) # definiert getDb() ) ``` -Oder wir können uns in der Fabrikdefinition mittels Referenz auf bestehende Dienste beziehen: +Alternativ können Sie in der Definition der Multifactory über Referenzen auf bestehende Services verweisen: ```neon services: @@ -212,15 +215,19 @@ services: ``` -Definition mittels Tags ------------------------ +Definition über Tags +-------------------- -Die zweite Möglichkeit ist, zur Definition [Tags |services#Tags] zu verwenden: +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: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index cc2f8fd98d..f71d386bbc 100644 --- a/dependency-injection/de/faq.texy +++ b/dependency-injection/de/faq.texy @@ -5,102 +5,119 @@ Häufig gestellte Fragen zu DI (FAQ) Ist DI ein anderer Name für IoC? -------------------------------- -*Inversion of Control* (IoC) ist ein Prinzip, das sich darauf konzentriert, wie Code ausgeführt wird – ob Ihr Code fremden Code ausführt oder ob Ihr Code in fremden Code integriert ist, der ihn anschließend aufruft. IoC ist ein weit gefasster Begriff, der [Ereignisse |nette:glossary#Events Ereignisse], das sogenannte [Hollywood-Prinzip |application:components#Hollywood Style] und andere Aspekte umfasst. Teil dieses Konzepts sind auch Fabriken, über die [Regel Nr. 3: Überlasse es der Fabrik |introduction#Regel Nr. 3: Überlasse es der Fabrik] spricht und die eine Inversion für den `new`-Operator darstellen. +*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) konzentriert sich darauf, wie ein Objekt von einem anderen Objekt, also von seinen Abhängigkeiten, erfährt. Es handelt sich um ein Entwurfsmuster, das die explizite Übergabe von Abhängigkeiten zwischen Objekten erfordert. +*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. -Man kann also sagen, dass DI eine spezifische Form von IoC ist. Allerdings sind nicht alle Formen von IoC im Hinblick auf die Code-Reinheit geeignet. Zu den Anti-Patterns gehören beispielsweise Techniken, die mit [globalem Zustand |global-state] arbeiten oder der sogenannte [Service Locator |#Was ist ein Service Locator]. +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 handelt sich um eine Alternative zur Dependency Injection. Er funktioniert so, dass ein zentraler Speicher erstellt wird, in dem alle verfügbaren Dienste oder Abhängigkeiten registriert sind. Wenn ein Objekt eine Abhängigkeit benötigt, fordert es diese vom Service Locator an. +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. -Im Vergleich zur Dependency Injection geht jedoch die Transparenz verloren: Abhängigkeiten werden den Objekten nicht direkt übergeben und sind daher nicht leicht zu identifizieren, was eine Untersuchung des Codes erfordert, um alle Verknüpfungen aufzudecken und zu verstehen. Das Testen ist ebenfalls komplizierter, da wir Mock-Objekte nicht einfach an die zu testenden Objekte übergeben können, sondern über den Service Locator gehen müssen. Darüber hinaus stört der Service Locator das Code-Design, da einzelne Objekte von seiner Existenz wissen müssen, was sich von der Dependency Injection unterscheidet, bei der Objekte keine Kenntnis vom DI-Container haben. +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 ist es besser, DI nicht zu verwenden? ------------------------------------------- +Wann verwendet man DI besser nicht? +----------------------------------- -Es sind keine Schwierigkeiten im Zusammenhang mit der Verwendung des Dependency Injection-Entwurfsmusters bekannt. Im Gegenteil, das Abrufen von Abhängigkeiten von global verfügbaren Orten führt zu [einer ganzen Reihe von Komplikationen |global-state], ebenso wie die Verwendung des Service Locators. Daher ist es ratsam, DI immer zu verwenden. Dies ist kein dogmatischer Ansatz, sondern es wurde einfach keine bessere Alternative gefunden. +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. -Dennoch gibt es bestimmte Situationen, in denen wir Objekte nicht übergeben und sie aus dem globalen Raum beziehen. Zum Beispiel beim Debuggen von Code, wenn Sie an einem bestimmten Punkt im Programm den Wert einer Variablen ausgeben, die Dauer eines bestimmten Programmteils messen oder eine Nachricht protokollieren müssen. In solchen Fällen, in denen es sich um temporäre Aufgaben handelt, die später aus dem Code entfernt werden, ist es legitim, einen global verfügbaren Dumper, eine Stoppuhr oder einen Logger zu verwenden. Diese Werkzeuge gehören nämlich nicht zum Code-Design. +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 die Verwendung von DI Nachteile? ------------------------------------- +Hat DI Nachteile? +----------------- -Bringt die Verwendung von Dependency Injection Nachteile mit sich, wie z. B. erhöhten Schreibaufwand oder Leistungseinbußen? Was verlieren wir, wenn wir anfangen, Code gemäß DI zu schreiben? +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 hat keinen Einfluss auf die Leistung oder den Speicherbedarf der Anwendung. Die Leistung des DI-Containers kann eine gewisse Rolle spielen, aber im Falle des [Nette DI |nette-container] wird der Container zu reinem PHP kompiliert, sodass sein Overhead während der Laufzeit der Anwendung praktisch null ist. +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. -Beim Schreiben von Code ist es oft notwendig, Konstruktoren zu erstellen, die Abhängigkeiten akzeptieren. Früher konnte dies mühsam sein, aber dank moderner IDEs und [constructor property promotion |https://blog.nette.org/de/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] ist dies jetzt eine Frage von Sekunden. Fabriken können mit Nette DI und einem Plugin für PhpStorm einfach per Mausklick generiert werden. Andererseits entfällt die Notwendigkeit, Singletons und statische Zugriffspunkte zu schreiben. +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. -Man kann feststellen, dass eine korrekt entworfene Anwendung, die DI verwendet, im Vergleich zu einer Anwendung, die Singletons verwendet, weder kürzer noch länger ist. Teile des Codes, die mit Abhängigkeiten arbeiten, werden lediglich aus den einzelnen Klassen extrahiert und an neue Orte verschoben, d. h. in den DI-Container und die Fabriken. +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 Legacy-Anwendung auf DI um? -------------------------------------------------- +Wie schreibt man eine Altanwendung auf DI um? +--------------------------------------------- -Der Übergang von einer Legacy-Anwendung zur Dependency Injection kann ein anspruchsvoller Prozess sein, insbesondere bei großen und komplexen Anwendungen. Es ist wichtig, diesen Prozess systematisch anzugehen. +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 Übergang zur Dependency Injection ist es wichtig, dass alle Teammitglieder die verwendeten Prinzipien und Verfahren verstehen. -- Führen Sie zunächst eine Analyse der bestehenden Anwendung durch und identifizieren Sie die Schlüsselkomponenten und ihre Abhängigkeiten. Erstellen Sie einen Plan, welche Teile refaktorisiert werden und in welcher Reihenfolge. -- Implementieren Sie einen DI-Container oder verwenden Sie besser eine vorhandene Bibliothek, z. B. Nette DI. -- Refaktorisieren Sie nach und nach einzelne Teile der Anwendung, um Dependency Injection zu verwenden. Dies kann Anpassungen von Konstruktoren oder Methoden beinhalten, sodass sie Abhängigkeiten als Parameter akzeptieren. -- Passen Sie die Stellen im Code an, an denen Objekte mit Abhängigkeiten erstellt werden, sodass stattdessen die Abhängigkeiten vom Container injiziert werden. Dies kann die Verwendung von Fabriken beinhalten. +- 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 Übergang zur Dependency Injection eine Investition in die Codequalität und die langfristige Wartbarkeit der Anwendung ist. Auch wenn es anspruchsvoll sein kann, diese Änderungen durchzuführen, sollte das Ergebnis ein saubererer, modularerer und leicht testbarer Code sein, der für zukünftige Erweiterungen und Wartung bereit ist. +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 wird Komposition der Vererbung vorgezogen? +Warum ist Komposition der Vererbung vorzuziehen? ------------------------------------------------ -Es ist ratsamer, [Komposition |nette:introduction-to-object-oriented-programming#Komposition] anstelle von [Vererbung |nette:introduction-to-object-oriented-programming#Vererbung] zu verwenden, da sie zur Wiederverwendung von Code dient, ohne sich um die Folgen von Änderungen kümmern zu müssen. Sie bietet also eine lockerere Kopplung, bei der wir keine Bedenken haben müssen, dass die Änderung eines Codes die Notwendigkeit zur Änderung eines anderen abhängigen Codes verursacht. Ein typisches Beispiel ist die Situation, die als [constructor hell |passing-dependencies#Constructor Hell] bezeichnet wird. +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. -Kann Nette DI Container außerhalb von Nette verwendet werden? -------------------------------------------------------------- +Lässt sich der Nette DI Container außerhalb von Nette verwenden? +---------------------------------------------------------------- -Auf jeden Fall. Der Nette DI Container ist Teil von Nette, aber er ist als eigenständige Bibliothek konzipiert, die unabhängig von den anderen Teilen des Frameworks verwendet werden kann. Installieren Sie ihn einfach mit Composer, erstellen Sie eine Konfigurationsdatei mit der Definition Ihrer Dienste und erstellen Sie dann mit wenigen Zeilen PHP-Code den DI-Container. Und schon können Sie die Vorteile der Dependency Injection in Ihren Projekten nutzen. +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. -Wie die konkrete Verwendung einschließlich des Codes aussieht, beschreibt das Kapitel [Nette DI Container |nette-container]. +Das Kapitel über den [Nette DI Container |nette-container] beschreibt einen konkreten Anwendungsfall samt Codebeispielen. -Warum ist die Konfiguration in NEON-Dateien? --------------------------------------------- +Warum steht die Konfiguration in NEON-Dateien? +---------------------------------------------- -NEON ist eine einfache und leicht lesbare Konfigurationssprache, die im Rahmen von Nette für die Konfiguration von Anwendungen, Diensten und deren Abhängigkeiten entwickelt wurde. Im Vergleich zu JSON oder YAML bietet sie für diesen Zweck wesentlich intuitivere und flexiblere Möglichkeiten. In NEON lassen sich Verknüpfungen natürlich beschreiben, die in Symfony & YAML entweder gar nicht oder nur durch eine komplizierte Umschreibung möglich wären. +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. -Verlangsamt das Parsen von NEON-Dateien die Anwendung nicht? ------------------------------------------------------------- +Bremst das Parsen von NEON-Dateien die Anwendung aus? +----------------------------------------------------- -Obwohl NEON-Dateien sehr schnell geparst werden, spielt dieser Aspekt überhaupt keine Rolle. Der Grund dafür ist, dass das Parsen der Dateien nur einmal beim ersten Start der Anwendung erfolgt. Danach wird der Code des DI-Containers generiert, auf der Festplatte gespeichert und bei jeder weiteren Anfrage ausgeführt, ohne dass weiteres Parsen erforderlich ist. +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 der Produktionsumgebung. Während der Entwicklung werden NEON-Dateien jedes Mal geparst, wenn sich ihr Inhalt ändert, damit der Entwickler immer über einen aktuellen DI-Container verfügt. Das eigentliche Parsen ist, wie gesagt, eine Frage von Augenblicken. +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 von meiner Klasse auf Parameter in der Konfigurationsdatei zu? ------------------------------------------------------------------------------ +Wie greife ich in meiner Klasse auf die Parameter aus der Konfigurationsdatei zu? +--------------------------------------------------------------------------------- -Denken wir an [Regel Nr. 1: Lass es dir übergeben |introduction#Regel Nr. 1: Lass es dir übergeben]. Wenn eine Klasse Informationen aus der Konfigurationsdatei benötigt, müssen wir nicht darüber nachdenken, wie wir an diese Informationen gelangen, sondern wir fordern sie einfach an – zum Beispiel über den Konstruktor der Klasse. Und die Übergabe erfolgt in der Konfigurationsdatei. +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 an den Konstruktor der Klasse `MyClass` übergeben wird: +In diesem Beispiel ist `%myParameter%` ein Platzhalter für den Wert des Parameters `myParameter`, der dem Konstruktor von `MyClass` übergeben wird: -```php +```neon # config.neon parameters: - myParameter: Some value + myParameter: Irgendein Wert services: - MyClass(%myParameter%) ``` -Um mehrere Parameter zu übergeben oder Autowiring zu nutzen, ist es ratsam, [die Parameter in ein Objekt zu verpacken |best-practices:passing-settings-to-presenters]. +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 PSR-11: Container interface? ----------------------------------------------- +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]: -Nette DI Container unterstützt PSR-11 nicht direkt. Wenn Sie jedoch Interoperabilität zwischen dem Nette DI Container und Bibliotheken oder Frameworks benötigen, die das PSR-11 Container Interface erwarten, können Sie einen [einfachen Adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] erstellen, der als Brücke zwischen dem Nette DI Container und PSR-11 dient. +- **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 index 888cd7de63..e25543a757 100644 --- a/dependency-injection/de/global-state.texy +++ b/dependency-injection/de/global-state.texy @@ -2,42 +2,42 @@ Globaler Zustand und Singletons ******************************* .[perex] -Warnung: Die folgenden Konstrukte sind Anzeichen für schlecht entworfenen Code: +Achtung: Die folgenden Konstrukte sind Symptome schlecht entworfenen Codes: - `Foo::getInstance()` - `DB::insert(...)` - `Article::setDb($db)` - `ClassName::$var` oder `static::$var` -Treten einige dieser Konstrukte in Ihrem Code auf? Dann haben Sie die Möglichkeit, ihn zu verbessern. Vielleicht denken Sie, dass dies übliche Konstrukte sind, die Sie vielleicht sogar in Beispiel-Lösungen verschiedener Bibliotheken und Frameworks sehen. Wenn dies der Fall ist, dann ist das Design ihres Codes nicht gut. +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 sprechen hier definitiv nicht von irgendeiner akademischen Reinheit. Alle diese Konstrukte haben eines gemeinsam: Sie verwenden globalen Zustand. Und dieser hat einen zerstörerischen Einfluss auf die Codequalität. Klassen lügen über ihre Abhängigkeiten. Code wird unvorhersehbar. Er verwirrt Programmierer und reduziert ihre Effizienz. +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 man globalen Zustand vermeidet. +In diesem Kapitel erklären wir, warum das so ist und wie sich globaler Zustand vermeiden lässt. -Globale Kopplung ----------------- +Globale Verflechtung +-------------------- -In einer idealen Welt sollte ein Objekt nur mit Objekten kommunizieren können, die ihm [direkt übergeben |passing-dependencies] wurden. Wenn ich zwei Objekte `A` und `B` erstelle und niemals eine Referenz zwischen ihnen übergebe, dann können weder `A` noch `B` auf das andere Objekt zugreifen oder seinen Zustand ändern. Das ist eine sehr wünschenswerte Eigenschaft von Code. Es ist ähnlich wie bei einer Batterie und einer Glühbirne; die Glühbirne leuchtet nicht, solange Sie sie nicht mit einem Draht mit der Batterie verbinden. +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. -Das gilt jedoch nicht für globale (statische) Variablen oder Singletons. Objekt `A` könnte *drahtlos* auf Objekt `C` zugreifen und es modifizieren, ohne dass eine Referenz übergeben wird, indem es `C::changeSomething()` aufruft. Wenn Objekt `B` ebenfalls auf das globale `C` zugreift, dann können sich `A` und `B` gegenseitig über `C` beeinflussen. +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. -Die Verwendung globaler Variablen führt eine neue Form der *drahtlosen* Kopplung in das System ein, die von außen nicht sichtbar ist. Sie erzeugt eine Nebelwand, die das Verständnis und die Verwendung des Codes erschwert. Um die Abhängigkeiten wirklich zu verstehen, müssen Entwickler jede Zeile des Quellcodes lesen, anstatt sich nur mit der Schnittstelle der Klassen vertraut zu machen. Es handelt sich zudem um eine völlig unnötige Kopplung. Globaler Zustand wird verwendet, weil er von überall leicht zugänglich ist und es beispielsweise ermöglicht, über eine globale (statische) Methode `DB::insert()` in die Datenbank zu schreiben. Aber wie wir zeigen werden, ist der Vorteil, den dies bringt, gering, während die dadurch verursachten Komplikationen fatal sind. +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] -Aus Verhaltenssicht gibt es keinen Unterschied zwischen einer globalen und einer statischen Variablen. Sie sind gleichermaßen schädlich. +Was das Verhalten angeht, gibt es zwischen einer globalen und einer statischen Variablen keinen Unterschied. Sie sind gleichermaßen schädlich. -Spukhafte Fernwirkung ---------------------- +Die spukhafte Fernwirkung +------------------------- -"Spukhafte Fernwirkung" – so nannte Albert Einstein 1935 berühmt ein Phänomen in der Quantenphysik, das ihm Gänsehaut bereitete. -Es handelt sich um die Quantenverschränkung, deren Besonderheit darin besteht, dass, wenn man Informationen über ein Teilchen misst, man sofort das andere Teilchen beeinflusst, auch wenn sie Millionen von Lichtjahren voneinander entfernt sind. Dies scheint das Grundgesetz des Universums zu verletzen, dass sich nichts schneller als Licht ausbreiten kann. +"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 Softwarewelt können wir "spukhafte Fernwirkung" eine Situation nennen, in der wir einen Prozess starten, von dem wir annehmen, dass er isoliert ist (weil wir ihm keine Referenzen übergeben haben), aber an entfernten Stellen im System unerwartete Interaktionen und Zustandsänderungen auftreten, von denen wir keine Ahnung hatten. Dies kann nur durch globalen Zustand geschehen. +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 treten einem Entwicklerteam eines Projekts bei, das über eine umfangreiche, ausgereifte Codebasis verfügt. Ihr neuer Vorgesetzter bittet Sie, eine neue Funktion zu implementieren, und Sie beginnen als guter Entwickler mit dem Schreiben eines Tests. Da Sie jedoch neu im Projekt sind, führen Sie viele explorative Tests durch, wie z. B. "Was passiert, wenn ich diese Methode aufrufe?". Und Sie versuchen, den folgenden Test zu schreiben: +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() @@ -47,17 +47,17 @@ function testCreditCardCharge() } ``` -Sie führen den Code aus, vielleicht mehrmals, und nach einer Weile bemerken Sie Benachrichtigungen von Ihrer Bank auf Ihrem Handy, dass bei jedem Ausführen 100 Dollar von Ihrer Kreditkarte abgebucht wurden 🤦‍♂️ +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 dazu führen, dass tatsächlich Geld abgebucht wird? Die Handhabung einer Kreditkarte ist nicht einfach. Sie müssen mit einem Webdienst eines Drittanbieters kommunizieren, Sie müssen die URL dieses Webdienstes kennen, Sie müssen sich anmelden und so weiter. Keine dieser Informationen ist im Test enthalten. Schlimmer noch, Sie wissen nicht einmal, wo diese Informationen vorhanden sind, und daher auch nicht, wie Sie externe Abhängigkeiten mocken können, damit nicht bei jeder Ausführung erneut 100 Dollar abgebucht werden. Und wie hätten Sie als neuer Entwickler wissen sollen, dass das, was Sie tun wollten, dazu führen würde, dass Sie um 100 Dollar ärmer sind? +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 spukhafte Fernwirkung! +Das ist eine spukhafte Fernwirkung! -Es bleibt Ihnen nichts anderes übrig, als sich lange durch eine Menge Quellcode zu wühlen und ältere und erfahrenere Kollegen zu fragen, bis Sie verstehen, wie die Abhängigkeiten im Projekt funktionieren. Dies liegt daran, dass beim Betrachten der Schnittstelle der Klasse `CreditCard` der globale Zustand, der initialisiert werden muss, nicht erkannt werden kann. Selbst ein Blick in den Quellcode der Klasse verrät Ihnen nicht, welche Initialisierungsmethode Sie aufrufen müssen. Im besten Fall finden Sie eine globale Variable, auf die zugegriffen wird, und können daraus versuchen abzuleiten, wie sie initialisiert wird. +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. -Klassen in einem solchen Projekt sind pathologische Lügner. Die Kreditkarte tut so, als ob es ausreicht, sie zu instanziieren und die Methode `charge()` aufzurufen. Im Verborgenen arbeitet sie jedoch mit einer anderen Klasse `PaymentGateway` zusammen, die das Zahlungsgateway darstellt. Auch deren Schnittstelle besagt, dass sie separat initialisiert werden kann, aber tatsächlich holt sie sich Anmeldeinformationen aus einer Konfigurationsdatei und so weiter. Den Entwicklern, die diesen Code geschrieben haben, ist klar, dass `CreditCard` `PaymentGateway` benötigt. Sie haben den Code auf diese Weise geschrieben. Aber für jeden, der neu im Projekt ist, ist es ein absolutes Rätsel und behindert das Lernen. +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 kann man die Situation beheben? Einfach. **Lassen Sie die API Abhängigkeiten deklarieren.** +Wie lässt sich die Lage in Ordnung bringen? Ganz leicht. **Lassen Sie die API die Abhängigkeiten deklarieren.** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -Beachten Sie, wie die Abhängigkeiten innerhalb des Codes plötzlich offensichtlich sind. Dadurch, dass die Methode `charge()` deklariert, dass sie `PaymentGateway` benötigt, müssen Sie niemanden fragen, wie der Code verknüpft ist. Sie wissen, dass Sie eine Instanz davon erstellen müssen, und wenn Sie dies versuchen, stoßen Sie darauf, dass Sie Zugriffsparameter angeben müssen. Ohne sie ließe sich der Code nicht einmal ausführen. +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 jetzt das Zahlungsgateway mocken, sodass Ihnen nicht bei jedem Testlauf 100 Dollar berechnet werden. +Und vor allem können Sie das Zahlungs-Gateway jetzt mocken, sodass Ihnen nicht bei jedem Testlauf 100 $ belastet werden. -Globaler Zustand führt dazu, dass Ihre Objekte heimlich auf Dinge zugreifen können, die nicht in ihrer API deklariert sind, und macht Ihre APIs dadurch zu pathologischen Lügnern. +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 bisher nicht so darüber nachgedacht, aber wann immer Sie globalen Zustand verwenden, erstellen Sie geheime drahtlose Kommunikationskanäle. Spukhafte Fernwirkung zwingt Entwickler, jede Codezeile zu lesen, um potenzielle Interaktionen zu verstehen, reduziert die Produktivität der Entwickler und verwirrt neue Teammitglieder. Wenn Sie derjenige sind, der den Code erstellt hat, kennen Sie die tatsächlichen Abhängigkeiten, aber jeder, der nach Ihnen kommt, ist ratlos. +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 globalen Zustand verwendet, bevorzugen Sie die Übergabe von Abhängigkeiten. Also Dependency Injection. +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 verwendet, ist nie sicher, wann und wer diesen Zustand geändert hat. Dieses Risiko tritt bereits bei der Initialisierung auf. Der folgende Code soll eine Datenbankverbindung herstellen und das Zahlungsgateway initialisieren, wirft jedoch ständig eine Ausnahme, und die Suche nach der Ursache ist extrem langwierig: +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 detailliert durchgehen, um festzustellen, dass das `PaymentGateway`-Objekt drahtlos auf andere Objekte zugreift, von denen einige eine Datenbankverbindung erfordern. Daher muss die Datenbank vor `PaymentGateway` initialisiert werden. Die Nebelwand des globalen Zustands verbirgt dies jedoch vor Ihnen. Wie viel Zeit hätten Sie gespart, wenn die API der einzelnen Klassen nicht gelogen und ihre Abhängigkeiten deklariert hätte? +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, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -Ein ähnliches Problem tritt auch bei der Verwendung des globalen Zugriffs auf die Datenbankverbindung auf: +Ein ähnliches Problem entsteht beim globalen Zugriff auf eine Datenbankverbindung: ```php use Illuminate\Support\Facades\DB; @@ -110,7 +110,7 @@ class Article } ``` -Beim Aufruf der Methode `save()` ist nicht sicher, ob die Datenbankverbindung bereits hergestellt wurde und wer für ihre Erstellung verantwortlich ist. Wenn wir beispielsweise die Datenbankverbindung zur Laufzeit ändern möchten, etwa für Tests, müssten wir wahrscheinlich weitere Methoden wie `DB::reconnect(...)` oder `DB::reconnectForTest()` erstellen. +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: @@ -122,9 +122,9 @@ Foo::doSomething(); $article->save(); ``` -Woher haben wir die Gewissheit, dass beim Aufruf von `$article->save()` tatsächlich die Testdatenbank verwendet wird? Was wäre, wenn die Methode `Foo::doSomething()` die globale Datenbankverbindung geändert hätte? Um dies herauszufinden, müssten wir den Quellcode der Klasse `Foo` und wahrscheinlich auch vieler anderer Klassen untersuchen. Dieser Ansatz würde jedoch nur eine kurzfristige Antwort liefern, da sich die Situation in Zukunft ändern kann. +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. -Und was wäre, wenn wir die Datenbankverbindung in eine statische Variable innerhalb der Klasse `Article` verschieben? +Was, wenn wir die Datenbankverbindung in eine statische Variable innerhalb der Klasse `Article` verschieben? ```php class Article @@ -143,11 +143,11 @@ class Article } ``` -Dadurch hat sich überhaupt nichts geändert. Das Problem ist der globale Zustand, und es ist völlig egal, in welcher Klasse er sich versteckt. In diesem Fall haben wir, genau wie im vorherigen, beim Aufruf der Methode `$article->save()` keinen Hinweis darauf, in welche Datenbank geschrieben wird. Irgendjemand am anderen Ende der Anwendung könnte die Datenbank jederzeit mit `Article::setDb()` ändern. Unter unseren Händen. +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. -Globaler Zustand macht unsere Anwendung **extrem zerbrechlich**. +Der globale Zustand macht unsere Anwendung **äußerst zerbrechlich**. -Es gibt jedoch eine einfache Möglichkeit, dieses Problem zu lösen. Lassen Sie einfach die API Abhängigkeiten deklarieren, um die korrekte Funktionalität sicherzustellen. +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 @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -Dank dieses Ansatzes entfällt die Sorge über versteckte und unerwartete Änderungen der Datenbankverbindung. Jetzt haben wir die Gewissheit, wohin der Artikel gespeichert wird, und keine Codeänderungen innerhalb einer anderen, nicht zusammenhängenden Klasse können die Situation mehr ändern. Der Code ist nicht mehr zerbrechlich, sondern stabil. +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 globalen Zustand verwendet, bevorzugen Sie die Übergabe von Abhängigkeiten. Also Dependency Injection. +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 laut der "Definition":https://en.wikipedia.org/wiki/Singleton_pattern aus der bekannten Publikation der Gang of Four eine Klasse auf eine einzige Instanz beschränkt und einen globalen Zugriff darauf bietet. Die Implementierung dieses Musters ähnelt normalerweise dem folgenden Code: +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 @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // und weitere Methoden, die die Funktionen der gegebenen Klasse erfüllen + // 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. Daher wird das Singleton als Anti-Pattern betrachtet. +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 keine Singletons in Ihrem Code und ersetzen Sie sie durch andere Mechanismen. Sie benötigen Singletons wirklich nicht. Wenn Sie jedoch sicherstellen müssen, dass nur eine einzige Instanz einer Klasse für die gesamte Anwendung existiert, überlassen Sie dies dem [DI-Container |container]. Erstellen Sie so einen Anwendungs-Singleton, also einen Dienst. Dadurch kümmert sich die Klasse nicht mehr um die Sicherstellung ihrer eigenen Einzigartigkeit (d. h. sie hat keine `getInstance()`-Methode und keine statische Variable) und erfüllt nur noch ihre Funktionen. So hört sie auf, das Prinzip der einzigen Verantwortung zu verletzen. +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 versus Tests ------------------------------ +Globaler Zustand und Tests +-------------------------- -Beim Schreiben von Tests gehen wir davon aus, dass jeder Test eine isolierte Einheit ist und kein externer Zustand in ihn eintritt. Und kein Zustand verlässt die Tests. Nach Abschluss eines Tests sollte der gesamte zugehörige Zustand automatisch vom Garbage Collector entfernt werden. Dadurch sind die Tests isoliert. Daher können wir Tests in beliebiger Reihenfolge ausführen. +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. -Wenn jedoch globale Zustände/Singletons vorhanden sind, zerfallen all diese angenehmen Annahmen. Zustand kann in den Test eintreten und ihn verlassen. Plötzlich kann die Reihenfolge der Tests eine Rolle spielen. +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 Singletons überhaupt testen zu können, müssen Entwickler oft ihre Eigenschaften lockern, etwa indem sie erlauben, die Instanz durch eine andere zu ersetzen. Solche Lösungen sind bestenfalls Hacks, die schwer wartbaren und verständlichen Code erzeugen. Jeder Test oder jede `tearDown()`-Methode, die einen globalen Zustand beeinflusst, muss diese Änderungen rückgängig machen. +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 der größte Kopfschmerz beim Unit-Testing! +Globaler Zustand ist das größte Kopfzerbrechen beim Unit-Testing! -Wie kann man die Situation beheben? Einfach. Schreiben Sie keinen Code, der Singletons verwendet, bevorzugen Sie die Übergabe von Abhängigkeiten. Also Dependency Injection. +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 nur auf die Verwendung von Singletons und statischen Variablen, sondern kann auch globale Konstanten betreffen. +Globaler Zustand beschränkt sich nicht auf Singletons und statische Variablen, er kann auch globale Konstanten betreffen. -Konstanten, deren Wert uns keine neue (`M_PI`) oder nützliche (`PREG_BACKTRACK_LIMIT_ERROR`) Information bringt, sind eindeutig in Ordnung. Im Gegensatz dazu sind Konstanten, die als Mittel dienen, Informationen *drahtlos* in den Code zu übergeben, nichts anderes als versteckte Abhängigkeiten. Wie z. B. `LOG_FILE` im folgenden Beispiel. Die Verwendung der Konstante `FILE_APPEND` ist völlig korrekt. +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 = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -In diesem Fall sollten wir einen Parameter im Konstruktor der Klasse `Foo` deklarieren, damit er Teil der API wird: +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 @@ -253,29 +253,29 @@ class Foo } ``` -Jetzt können wir die Information über den Pfad zur Log-Datei übergeben und sie bei Bedarf leicht ändern, was das Testen und die Wartung des Codes erleichtert. +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 nicht problematisch ist. Wir haben erklärt, warum die Verwendung von `DB::insert()` und ähnlichen Methoden ungeeignet ist, aber es ging immer nur um den globalen Zustand, der in einer statischen Variablen gespeichert ist. Die Methode `DB::insert()` erfordert die Existenz einer statischen Variablen, da darin die Datenbankverbindung gespeichert ist. Ohne diese Variable wäre es unmöglich, die Methode zu implementieren. +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. -Die Verwendung deterministischer statischer Methoden und Funktionen wie `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` und vielen anderen steht in vollem Einklang mit der Dependency Injection. Diese Funktionen geben bei gleichen Eingabeparametern immer die gleichen Ergebnisse zurück und sind daher vorhersagbar. Sie verwenden keinen globalen Zustand. +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. -Es gibt jedoch auch Funktionen in PHP, die nicht deterministisch sind. Dazu gehört beispielsweise die Funktion `htmlspecialchars()`. Ihr dritter Parameter `$encoding`, falls nicht angegeben, hat als Standardwert den Wert der Konfigurationsoption `ini_get('default_charset')`. Daher wird empfohlen, diesen Parameter immer anzugeben, um mögliches unvorhersehbares Verhalten der Funktion zu vermeiden. Nette tut dies konsequent. +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. -Einige Funktionen wie `strtolower()`, `strtoupper()` und ähnliche verhielten sich in der jüngeren Vergangenheit nicht deterministisch und waren von der Einstellung `setlocale()` abhängig. Dies verursachte viele Komplikationen, am häufigsten bei der Arbeit mit der türkischen Sprache. Diese unterscheidet nämlich sowohl Klein- als auch Großbuchstaben `I` mit und ohne Punkt. So gab `strtolower('I')` den Buchstaben `ı` zurück und `strtoupper('i')` den Buchstaben `İ`, was dazu führte, dass Anwendungen eine Reihe rätselhafter Fehler verursachten. Dieses Problem wurde jedoch in PHP Version 8.2 behoben, und die Funktionen sind nicht mehr von der Locale abhä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. -Dies ist ein schönes Beispiel dafür, wie globaler Zustand Tausende von Entwicklern weltweit geplagt hat. Die Lösung bestand darin, ihn durch Dependency Injection zu ersetzen. +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 ist die Verwendung von globalem Zustand möglich? ------------------------------------------------------ +Wann lässt sich globaler Zustand verwenden? +------------------------------------------- -Es gibt bestimmte spezifische Situationen, in denen die Verwendung von globalem Zustand möglich ist. Zum Beispiel beim Debuggen von Code, wenn Sie den Wert einer Variablen ausgeben oder die Dauer eines bestimmten Programmteils messen müssen. In solchen Fällen, die sich auf temporäre Aktionen beziehen, die später aus dem Code entfernt werden, ist es legitim, einen global verfügbaren Dumper oder eine Stoppuhr zu verwenden. Diese Werkzeuge sind nämlich nicht Teil des Code-Designs. +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 Funktionen zur Arbeit mit regulären Ausdrücken `preg_*`, die intern kompilierte reguläre Ausdrücke in einem statischen Cache im Speicher ablegen. Wenn Sie also denselben regulären Ausdruck mehrmals an verschiedenen Stellen im Code aufrufen, wird er nur einmal kompiliert. Der Cache spart Leistung und ist gleichzeitig für den Benutzer völlig unsichtbar, daher kann eine solche Verwendung als legitim angesehen werden. +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 @@ -283,12 +283,12 @@ Zusammenfassung Wir haben besprochen, warum es sinnvoll ist: -1) Alle statischen Variablen aus dem Code zu entfernen -2) Abhängigkeiten zu deklarieren -3) Und Dependency Injection zu verwenden +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 -Wenn Sie über das Code-Design nachdenken, denken Sie daran, dass jedes `static $foo` ein Problem darstellt. Damit Ihr Code eine Umgebung ist, die DI respektiert, ist es unerlässlich, den globalen Zustand vollständig zu beseitigen und ihn durch Dependency Injection zu ersetzen. +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. -Während dieses Prozesses stellen Sie möglicherweise fest, dass eine Klasse aufgeteilt werden muss, da sie mehr als eine Verantwortung hat. Scheuen Sie sich nicht davor; streben Sie das Prinzip der einzigen Verantwortung an. +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 für dieses Kapitel bilden.* +*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 index e1dece6d1b..464080acaa 100644 --- a/dependency-injection/de/introduction.texy +++ b/dependency-injection/de/introduction.texy @@ -2,57 +2,57 @@ Was ist Dependency Injection? ***************************** .[perex] -Dieses Kapitel führt Sie in die grundlegenden Programmierpraktiken ein, die Sie beim Schreiben aller Anwendungen befolgen sollten. Dies sind die Grundlagen für das Schreiben von sauberem, verständlichem und wartbarem Code. +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 sich diese Regeln aneignen und befolgen, wird Nette Sie bei jedem Schritt unterstützen. Es wird Routineaufgaben für Sie erledigen und Ihnen maximalen Komfort bieten, damit Sie sich auf die eigentliche Logik konzentrieren können. +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 vorstellen werden, sind dabei recht einfach. Sie müssen sich keine Sorgen machen. +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 gewesen wäre, hätte es wahrscheinlich so ausgesehen: +Wir wissen nicht, in welcher Sprache Sie es geschrieben haben, aber wenn es PHP war, sah es wahrscheinlich ungefähr so aus: ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // gibt 24 aus +echo addition(23, 1); // gibt 24 aus ``` -Ein paar triviale Codezeilen, aber sie enthalten so viele Schlüsselkonzepte. Dass es Variablen gibt. Dass Code in kleinere Einheiten unterteilt wird, wie zum Beispiel Funktionen. Dass wir ihnen Eingabeargumente übergeben und sie Ergebnisse zurückgeben. Es fehlen nur noch Bedingungen und Schleifen. +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 perfekt verständliches Konzept, das auch in anderen Bereichen verwendet wird, wie zum Beispiel in der Mathematik. +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, einer Übersicht der Parameter und ihrer Typen und schließlich dem Typ des Rückgabewerts besteht. Als Benutzer interessiert uns die Signatur, über die interne Implementierung müssen wir normalerweise nichts wissen. +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 Funktionssignatur sähe so aus: +Stellen Sie sich nun vor, die Signatur der Funktion sähe so aus: ```php -function soucet(float $x): float +function addition(float $x): float ``` -Eine Summe mit einem Parameter? Das ist seltsam… Und was ist hiermit? +Eine Addition mit einem Parameter? Das ist seltsam… Und was ist damit? ```php -function soucet(): float +function addition(): float ``` -Das ist jetzt wirklich sehr seltsam, oder? Wie wird die Funktion wohl verwendet? +Das ist jetzt wirklich seltsam, oder? Wie wird die Funktion überhaupt verwendet? ```php -echo soucet(); // was gibt das wohl aus? +echo addition(); // was gibt sie aus? ``` -Beim Anblick eines solchen Codes wären wir verwirrt. Nicht nur ein Anfänger würde ihn nicht verstehen, auch ein erfahrener Programmierer versteht solchen Code nicht. +Bei solchem Code wären wir ratlos. Nicht nur ein Anfänger würde ihn nicht verstehen, sondern auch ein erfahrener Programmierer nicht. -Überlegen Sie, wie eine solche Funktion intern aussehen würde? Woher nimmt sie die Summanden? Wahrscheinlich würde sie sie sich *irgendwie* selbst beschaffen, vielleicht so: +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 soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -Im Funktionskörper haben wir versteckte Abhängigkeiten zu anderen globalen Funktionen oder statischen Methoden entdeckt. Um herauszufinden, woher die Summanden tatsächlich stammen, müssen wir weiter suchen. +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 Entwurf, den wir gerade vorgestellt haben, ist die Essenz vieler negativer Eigenschaften: +Der eben gezeigte Entwurf ist der Inbegriff vieler negativer Eigenschaften: -- Die Funktionssignatur tat so, als ob sie keine Summanden bräuchte, was uns verwirrte -- Wir wissen überhaupt nicht, wie wir die Funktion dazu bringen können, zwei andere Zahlen zu addieren -- Wir mussten uns den Code ansehen, um herauszufinden, woher sie die Summanden nimmt -- Wir haben versteckte Abhängigkeiten entdeckt -- Für ein vollständiges Verständnis müssen auch diese Abhängigkeiten untersucht werden +- 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 Eingaben zu beschaffen? Natürlich nicht. Ihre Verantwortung liegt ausschließlich in der Addition selbst. +Und ist es überhaupt die Aufgabe einer Additionsfunktion, sich die Eingaben zu beschaffen? Natürlich nicht. Ihre Verantwortung ist nur die Addition selbst. -Solchen Code wollen wir nicht sehen und schon gar nicht schreiben. Die Korrektur ist dabei einfach: Zurück zu den Grundlagen und einfach Parameter verwenden: +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 soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -Regel Nr. 1: Lass es dir übergeben ----------------------------------- +Regel Nr. 1: Lassen Sie es sich übergeben +----------------------------------------- -Die wichtigste Regel lautet: **Alle Daten, die Funktionen oder Klassen benötigen, müssen ihnen übergeben werden**. +Die wichtigste Regel lautet: **Alle Daten, die Funktionen oder Klassen brauchen, müssen ihnen übergeben werden**. -Anstatt versteckte Wege zu erfinden, über die sie irgendwie selbst an die Daten gelangen könnten, übergeben Sie einfach die Parameter. Sie sparen sich die Zeit, die für das Ausdenken versteckter Pfade benötigt wird, die Ihren Code definitiv nicht verbessern 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 nach ihm liest. Wo alles aus den Signaturen von Funktionen und Klassen verständlich ist und man nicht nach versteckten Geheimnissen in der Implementierung suchen muss. +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 wird fachmännisch als **Dependency Injection** bezeichnet. Und diese Daten werden **Abhängigkeiten** genannt. Dabei handelt es sich um die einfache Übergabe von Parametern, nichts weiter. +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“, einem Werkzeug, also etwas völlig anderem. Container werden wir später behandeln. +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 hängt das mit Klassen zusammen? Eine Klasse ist eine komplexere Einheit als eine einfache Funktion, aber Regel Nr. 1 gilt hier uneingeschränkt. Es gibt nur [mehr Möglichkeiten, Argumente zu übergeben |passing-dependencies]. Zum Beispiel ganz ähnlich wie im Fall einer Funktion: +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 Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` -Oder über andere Methoden oder direkt über den Konstruktor: +Oder über andere Methoden, oder direkt über den Konstruktor: ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,24 +132,23 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` -Beide Beispiele stehen vollständig im Einklang mit Dependency Injection. +Beide Beispiele entsprechen vollständig der Dependency Injection. -Reale Beispiele ---------------- +Beispiele aus dem echten Leben +------------------------------ -In der realen Welt werden Sie keine Klassen zum Addieren von Zahlen schreiben. Gehen wir zu Beispielen aus der Praxis über. +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: @@ -162,23 +161,23 @@ class Article public function save(): void { - // wir speichern den Artikel in der Datenbank + // speichert den Artikel in der Datenbank } } ``` -und die Verwendung wird wie folgt sein: +und die Verwendung sieht so aus: ```php $article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; +$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. Die Implementierung mit [Nette Database |database:] wäre ein Kinderspiel, gäbe es nicht einen Haken: Woher nimmt `Article` die Datenbankverbindung, d.h. das Objekt der Klasse `Nette\Database\Connection`? +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, wir haben viele Möglichkeiten. Sie könnte sie irgendwoher aus einer statischen Variablen nehmen. Oder von einer Klasse erben, die die Datenbankverbindung bereitstellt. Oder das sogenannte [Singleton |global-state#Singleton] verwenden. Oder sogenannte Facades, wie sie in Laravel verwendet werden: +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; @@ -199,15 +198,15 @@ class Article } ``` -Großartig, wir haben das Problem gelöst. +Prima, das Problem ist gelöst. -Oder nicht? +Oder etwa nicht? -Erinnern wir uns an [#Regel Nr. 1: Lass es dir übergeben]: Alle Abhängigkeiten, die eine Klasse benötigt, müssen ihr übergeben werden. Denn wenn wir die Regel verletzen, haben wir den Weg zu schmutzigem Code voller versteckter Abhängigkeiten und Unverständlichkeit eingeschlagen, und das Ergebnis wird eine Anwendung sein, deren Wartung und Entwicklung mühsam sein wird. +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 Benutzer der Klasse `Article` weiß nicht, wohin die Methode `save()` den Artikel speichert. In eine Datenbanktabelle? In welche, die Produktiv- oder die Testdatenbank? Und wie kann man das ändern? +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 Benutzer muss sich ansehen, wie die Methode `save()` implementiert ist, und findet die Verwendung der Methode `DB::insert()`. Also muss er weiter suchen, wie diese Methode die Datenbankverbindung beschafft. Und versteckte Abhängigkeiten können eine ziemlich lange Kette bilden. +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: @@ -224,7 +223,7 @@ class Article } ``` -Noch praktischer, wie wir später sehen werden, ist es über den Konstruktor: +Noch praktischer ist, wie wir später sehen werden, die Verwendung des Konstruktors: ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -Wenn Sie ein erfahrener Programmierer sind, denken Sie vielleicht, dass `Article` überhaupt keine `save()`-Methode haben sollte, sondern eine reine Datenkomponente sein sollte und die Speicherung von einem separaten Repository übernommen werden sollte. Das macht Sinn. Aber damit würden wir weit über das Thema Dependency Injection hinausgehen und das Bemühen, einfache Beispiele zu geben, sprengen. +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 ihre Tätigkeit z. B. eine Datenbank benötigt, überlegen Sie nicht, woher Sie sie bekommen, sondern lassen Sie sie sich übergeben. Zum Beispiel als Parameter des Konstruktors oder einer anderen Methode. Geben Sie Abhängigkeiten zu. Geben Sie sie in der API Ihrer Klasse zu. Sie erhalten verständlichen und vorhersagbaren Code. +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: +Und was ist mit dieser Klasse, die Fehlermeldungen protokolliert? ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,23 +261,23 @@ class Logger } ``` -Was meinen Sie, haben wir [#Regel Nr. 1: Lass es dir übergeben] eingehalten? +Was meinen Sie, haben wir uns an [#Regel Nr. 1: Lassen Sie es sich übergeben] gehalten? -Nein, haben wir nicht. +Haben wir nicht. -Die Schlüsselinformation, nämlich das Verzeichnis mit der Logdatei, beschafft sich die Klasse *selbst* aus einer Konstante. +Die entscheidende Information, das Verzeichnis mit der Logdatei, *beschafft sich die Klasse selbst* aus einer Konstanten. -Sehen Sie sich das Anwendungsbeispiel an: +Sehen Sie sich das Beispiel der Verwendung an: ```php $logger = new Logger; -$logger->log('Temperatur ist 23 °C'); -$logger->log('Temperatur ist 10 °C'); +$logger->log('Die Temperatur beträgt 23 °C'); +$logger->log('Die Temperatur beträgt 10 °C'); ``` -Ohne Kenntnis der Implementierung, könnten Sie die Frage beantworten, wohin die Nachrichten geschrieben werden? Wäre Ihnen eingefallen, dass für die Funktion die Existenz der Konstante `LOG_DIR` erforderlich ist? Und könnten Sie eine zweite Instanz erstellen, die woanders hinschreibt? Sicher nicht. +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. -Lassen Sie uns die Klasse korrigieren: +Bringen wir die Klasse in Ordnung: ```php class Logger @@ -295,24 +294,24 @@ class Logger } ``` -Die Klasse ist jetzt viel verständlicher, konfigurierbarer und daher nützlicher. +Die Klasse ist jetzt viel verständlicher, konfigurierbarer und damit nützlicher. ```php -$logger = new Logger('/pfad/zum/log.txt'); -$logger->log('Temperatur ist 15 °C'); +$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 erstelle und save() aufrufe, will ich mich nicht um die Datenbank kümmern, ich will einfach, dass es in die Datenbank gespeichert wird, die ich in der Konfiguration eingestellt habe.“* +*"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 Logger verwende, will ich einfach, dass die Nachricht geschrieben wird, und ich will mich nicht darum kümmern, wohin. Es soll die globale Einstellung verwendet werden.“* +*"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. -Als Beispiel zeigen wir eine Klasse, die Newsletter versendet und protokolliert, wie es gelaufen ist: +Zeigen wir als Beispiel eine Klasse, die Newsletter verteilt und das Ergebnis protokolliert: ```php class NewsletterDistributor @@ -322,27 +321,27 @@ class NewsletterDistributor $logger = new Logger(/* ... */); try { $this->sendEmails(); - $logger->log('E-Mails wurden versendet'); + $logger->log('Die E-Mails wurden versendet'); } catch (Exception $e) { - $logger->log('Fehler beim Versenden aufgetreten'); + $logger->log('Beim Versenden ist ein Fehler aufgetreten'); throw $e; } } } ``` -Der verbesserte `Logger`, der die Konstante `LOG_DIR` nicht mehr verwendet, erfordert im Konstruktor die Angabe des Dateipfads. Wie löst man das? Die Klasse `NewsletterDistributor` interessiert sich überhaupt nicht dafür, wohin die Nachrichten geschrieben werden, sie will sie nur schreiben. +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: Lass es dir übergeben]: Alle Daten, die die Klasse benötigt, übergeben wir ihr. +Die Lösung ist wieder [#Regel Nr. 1: Lassen Sie es sich übergeben]: Wir übergeben alle Daten, die die Klasse braucht. -Bedeutet das also, dass wir uns den Pfad zum Log über den Konstruktor übergeben, den wir dann beim Erstellen des `Logger`-Objekts verwenden? +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, // ⛔ SO NICHT! + private string $file, // ⛔ NICHT SO! ) { } @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -So nicht! Der Pfad gehört nämlich **nicht** zu den Daten, die die Klasse `NewsletterDistributor` benötigt; diese benötigt der `Logger`. Erkennen Sie den Unterschied? Die Klasse `NewsletterDistributor` benötigt den Logger als solchen. Also übergeben wir uns diesen: +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 @@ -365,33 +364,33 @@ class NewsletterDistributor { try { $this->sendEmails(); - $this->logger->log('E-Mails wurden versendet'); + $this->logger->log('Die E-Mails wurden versendet'); } catch (Exception $e) { - $this->logger->log('Fehler beim Versenden aufgetreten'); + $this->logger->log('Beim Versenden ist ein Fehler aufgetreten'); throw $e; } } } ``` -Nun ist aus den Signaturen der Klasse `NewsletterDistributor` klar, dass auch Logging Teil ihrer Funktionalität ist. Und die Aufgabe, den Logger gegen einen anderen auszutauschen, z. B. zum Testen, ist völlig trivial. Außerdem: Wenn sich der Konstruktor der Klasse `Logger` ändern würde, hätte dies keinerlei Auswirkungen auf unsere Klasse. +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: Nimm, was deins ist --------------------------------- +Regel Nr. 2: Nehmen Sie, was Ihnen gehört +----------------------------------------- -Lassen Sie sich nicht täuschen und lassen Sie sich nicht die Abhängigkeiten Ihrer Abhängigkeiten übergeben. Lassen Sie sich nur Ihre eigenen Abhängigkeiten übergeben. +Lassen Sie sich nicht verwirren und übernehmen Sie nicht die Abhängigkeiten Ihrer Abhängigkeiten. Nehmen Sie nur Ihre eigenen. -Dank dessen wird der Code, der andere Objekte verwendet, völlig unabhängig von Änderungen an deren Konstruktoren. Seine API wird wahrheitsgetreuer sein. Und vor allem wird es trivial sein, diese Abhängigkeiten gegen andere auszutauschen. +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 ---------------------- -Im Entwicklungsteam wurde beschlossen, einen zweiten Logger zu erstellen, der in die Datenbank schreibt. Wir erstellen also die Klasse `DatabaseLogger`. Wir haben also zwei Klassen, `Logger` und `DatabaseLogger`, eine schreibt in eine Datei, die andere in die Datenbank … scheint Ihnen an dieser Benennung nicht etwas seltsam? Wäre es nicht besser, `Logger` in `FileLogger` umzubenennen? Sicherlich ja. +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 wir machen es clever. Unter dem ursprünglichen Namen erstellen wir eine Schnittstelle: +Aber machen wir es geschickt. Wir legen ein Interface mit dem ursprünglichen Namen an: ```php interface Logger @@ -400,7 +399,7 @@ interface Logger } ``` -… die beide Logger implementieren werden: +… das beide Logger implementieren werden: ```php class FileLogger implements Logger @@ -410,17 +409,17 @@ class DatabaseLogger implements Logger // ... ``` -Und dank dessen muss im Rest des Codes, wo der Logger verwendet wird, nichts geändert werden. Zum Beispiel wird der Konstruktor der Klasse `NewsletterDistributor` weiterhin damit zufrieden sein, dass er als Parameter `Logger` benötigt. Und es liegt nur an uns, welche Instanz wir ihm übergeben. +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 geben wir Schnittstellennamen niemals das Suffix `Interface` oder das Präfix `I`.** Sonst wäre es nicht möglich, den Code so schön weiterzuentwickeln. +**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 können, sei es datei- oder datenbankbasiert, und ihn einfach überall dorthin übergeben, wo etwas protokolliert wird, ist es bei der Klasse `Article` ganz anders. Ihre Instanzen erstellen wir nach Bedarf, gerne auch mehrmals. Wie gehen wir mit der Abhängigkeit zur Datenbank in ihrem Konstruktor um? +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? -Als Beispiel kann ein Controller dienen, der nach dem Absenden eines Formulars einen Artikel in der Datenbank speichern soll: +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 @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -Eine mögliche Lösung bietet sich direkt an: Wir lassen uns das Datenbankobjekt über den Konstruktor in `EditController` übergeben und verwenden `$article = new Article($this->db)`. +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 vorherigen Fall mit `Logger` und dem Dateipfad ist dies nicht der richtige Ansatz. Die Datenbank ist keine Abhängigkeit von `EditController`, sondern von `Article`. Sich die Datenbank übergeben zu lassen, verstößt also gegen [#Regel Nr. 2: Nimm, was deins ist]. Wenn sich der Konstruktor der Klasse `Article` ändert (ein neuer Parameter kommt hinzu), muss auch der Code an allen Stellen angepasst werden, an denen Instanzen erstellt werden. Uff. +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: Überlasse es der Fabrik ------------------------------------- +Regel Nr. 3: Überlassen Sie es der Factory +------------------------------------------ -Dadurch, dass wir versteckte Abhängigkeiten beseitigt und alle Abhängigkeiten als Argumente übergeben haben, haben wir konfigurierbarere und flexiblere Klassen erhalten. Und daher brauchen wir noch etwas anderes, das uns diese flexibleren Klassen erstellt und konfiguriert. Wir nennen es Fabriken. +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: Wenn eine Klasse Abhängigkeiten hat, überlasse die Erstellung ihrer Instanzen einer Fabrik. +Die Regel lautet: Hat eine Klasse Abhängigkeiten, überlassen Sie das Erzeugen ihrer Instanzen einer Factory. -Fabriken sind der clevere Ersatz für den `new`-Operator in der Welt der Dependency Injection. +Factories sind in der Welt der Dependency Injection die klügere Alternative zum Operator `new`. .[note] -Bitte verwechseln Sie dies nicht mit dem Entwurfsmuster *Factory Method*, das eine spezifische Art der Verwendung von Fabriken beschreibt und mit diesem Thema nichts zu tun hat. +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. -Fabrik ------- +Factory +------- -Eine Fabrik ist eine Methode oder Klasse, die Objekte herstellt und konfiguriert. Die Klasse, die `Article` herstellt, nennen wir `ArticleFactory` und sie könnte beispielsweise so aussehen: +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 @@ -475,7 +474,7 @@ class ArticleFactory } ``` -Ihre Verwendung im Controller wird wie folgt sein: +Ihre Verwendung im Controller sieht so aus: ```php class EditController extends Controller @@ -487,7 +486,7 @@ class EditController extends Controller public function formSubmitted($data) { - // lassen wir die Fabrik das Objekt erstellen + // die Factory das Objekt erzeugen lassen $article = $this->articleFactory->create(); $article->title = $data->title; $article->content = $data->content; @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -Wenn sich zu diesem Zeitpunkt die Signatur des Konstruktors der Klasse `Article` ändert, ist der einzige Teil des Codes, der darauf reagieren muss, die Fabrik `ArticleFactory` selbst. Aller anderer Code, der mit `Article`-Objekten arbeitet, wie zum Beispiel `EditController`, bleibt davon unberührt. +Ä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 klopfen Sie sich jetzt an die Stirn, ob wir uns überhaupt geholfen haben. Die Menge an Code ist gewachsen und das Ganze beginnt verdächtig kompliziert auszusehen. +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 gleich zum Nette DI Container. Und der hat eine Reihe von Assen im Ärmel, die den Bau von Anwendungen, die Dependency Injection verwenden, enorm vereinfachen. So wird beispielsweise anstelle der Klasse `ArticleFactory` nur [das Schreiben einer reinen Schnittstelle |factory] ausreichen: +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 @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -Aber das greifen wir vor, bleiben Sie noch dran :-) +Aber wir greifen vor, bleiben Sie dran :-) Zusammenfassung --------------- -Am Anfang dieses Kapitels haben wir versprochen, Ihnen einen Ansatz zu zeigen, wie man sauberen Code entwirft. Es genügt, den Klassen +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 zu übergeben, die sie benötigen |#Regel Nr. 1: Lass es dir übergeben] -2) [und umgekehrt nicht das zu übergeben, was sie nicht direkt benötigen |#Regel Nr. 2: Nimm was deins ist] -3) [und dass Objekte mit Abhängigkeiten am besten in Fabriken hergestellt werden |#Regel Nr. 3: Überlasse es der Fabrik] +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] -Es mag auf den ersten Blick nicht so erscheinen, aber diese drei Regeln haben weitreichende Konsequenzen. Sie führen zu einer radikal anderen Sichtweise auf das Code-Design. Lohnt es sich? Programmierer, die alte Gewohnheiten aufgegeben haben und konsequent Dependency Injection verwenden, betrachten diesen Schritt als einen entscheidenden Moment in ihrer beruflichen Laufbahn. Es öffnete sich ihnen die Welt übersichtlicher und wartbarer Anwendungen. +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. -Was aber, wenn der Code Dependency Injection nicht konsequent verwendet? Was, wenn er auf statischen Methoden oder Singletons basiert? Bringt das irgendwelche Probleme mit sich? [Ja, und zwar sehr grundlegende |global-state]. +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 index 872434616a..5b0d095ae8 100644 --- a/dependency-injection/de/nette-container.texy +++ b/dependency-injection/de/nette-container.texy @@ -2,9 +2,9 @@ Nette DI Container ****************** .[perex] -Nette DI ist eine der interessantesten Bibliotheken von Nette. Sie kann kompilierte DI-Container generieren und automatisch aktualisieren, die extrem schnell und erstaunlich einfach zu konfigurieren sind. +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. -Die Form der Dienste, die der DI-Container erstellen soll, definieren wir normalerweise mithilfe von Konfigurationsdateien im [NEON-Format|neon:format]. Der Container, den wir im [vorherigen Kapitel|container] manuell erstellt haben, würde so geschrieben werden: +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: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -Die Notation ist sehr prägnant. +Die Syntax ist sehr knapp. -Alle in den Konstruktoren der Klassen `ArticleFactory` und `UserController` deklarierten Abhängigkeiten findet Nette DI selbst heraus und übergibt sie dank des sogenannten [Autowiring|autowiring]. Daher muss in der Konfigurationsdatei nichts angegeben werden. Selbst wenn sich die Parameter ändern, müssen Sie in der Konfiguration nichts ändern. Der Nette-Container wird automatisch neu generiert. Sie können sich dort ganz auf die Entwicklung der Anwendung konzentrieren. +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. -Wenn wir Abhängigkeiten über Setter übergeben möchten, verwenden wir dazu den Abschnitt [setup |services#Setup]. +Wollen wir Abhängigkeiten über Setter übergeben, verwenden wir dafür den Abschnitt [setup |services#Setup]. -Nette DI generiert direkt PHP-Code für den Container. Das Ergebnis ist also eine `.php`-Datei, die Sie öffnen und studieren können. Dadurch sehen Sie genau, wie der Container funktioniert. Sie können ihn auch in der IDE debuggen und schrittweise durchgehen. Und vor allem: Das generierte PHP ist extrem schnell. +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 Code für [Fabriken|factory] basierend auf einer bereitgestellten Schnittstelle generieren. Anstelle der Klasse `ArticleFactory` reicht es uns daher aus, in der Anwendung nur eine Schnittstelle zu erstellen: +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 @@ -42,13 +42,13 @@ Das vollständige Beispiel finden Sie [auf GitHub|https://github.com/nette-examp Eigenständige Verwendung ------------------------ -Der Einsatz der Nette DI-Bibliothek in einer Anwendung ist sehr einfach. Zuerst installieren wir sie mit Composer (denn das Herunterladen von ZIP-Dateien ist sooo veraltet): +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 erstellt eine Instanz des DI-Containers gemäß der Konfiguration, die in der Datei `config.neon` gespeichert ist: +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'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -Der Container wird nur einmal generiert, sein Code wird im Cache (Verzeichnis `__DIR__ . '/temp'`) gespeichert und bei weiteren Anfragen nur noch von dort geladen. +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. -Zum Erstellen und Abrufen von Diensten dienen die Methoden `getService()` oder `getByType()`. So erstellen wir das `UserController`-Objekt: +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 -$controller = $container->getByType(UserController::class); +$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 Auto-Refresh-Modus zu aktivieren, bei dem der Container automatisch neu generiert wird, wenn sich eine Klasse oder Konfigurationsdatei ändert. Geben Sie einfach im Konstruktor von `ContainerLoader` als zweites Argument `true` an. +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 im Nette Framework geschrieben wurden; Sie können es mit nur 3 Zeilen Code überall einsetzen. Wenn Sie jedoch Anwendungen im Nette Framework entwickeln, ist der [Bootstrap |application:bootstrapping#Konfiguration des DI-Containers] für die Konfiguration und Erstellung des Containers verantwortlich. +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 index cc5a8ed7ee..e504b71079 100644 --- a/dependency-injection/de/passing-dependencies.texy +++ b/dependency-injection/de/passing-dependencies.texy @@ -1,24 +1,24 @@ -Übergeben von Abhängigkeiten -**************************** +Abhängigkeiten übergeben +************************ <div class=perex> -Argumente, oder in der DI-Terminologie „Abhängigkeiten“, können auf folgende Hauptarten an Klassen übergeben werden: +Argumente, in der Terminologie der DI "Abhängigkeiten", lassen sich Klassen auf diese Hauptarten übergeben: -* Übergabe per Konstruktor -* Übergabe per Methode (sog. Setter) -* Zuweisung zu einer Variablen -* Methode, Annotation oder Attribut *inject* +* Ü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> -Nun werden wir die einzelnen Varianten anhand konkreter Beispiele erläutern. +Zeigen wir jede Variante an konkreten Beispielen. -Übergabe per Konstruktor -======================== +Übergabe im Konstruktor +======================= -Abhängigkeiten werden im Moment der Objekterstellung als Argumente des Konstruktors übergeben: +Die Abhängigkeiten werden beim Erzeugen des Objekts als Argumente des Konstruktors übergeben: ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -Diese Form eignet sich für obligatorische Abhängigkeiten, die die Klasse unbedingt für ihre Funktion benötigt, da ohne sie keine Instanz erstellt werden kann. +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/de/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), die funktional äquivalent ist: +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 @@ -49,7 +49,7 @@ class MyClass } ``` -Seit PHP 8.1 kann die Variable mit dem Flag `readonly` markiert werden, was deklariert, dass sich der Inhalt der Variablen nicht mehr ändern wird: +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 @@ -62,13 +62,13 @@ class MyClass } ``` -Der DI-Container übergibt Abhängigkeiten automatisch an den Konstruktor mittels [Autowiring |autowiring]. Argumente, die auf diese Weise nicht übergeben werden können (z. B. Zeichenketten, Zahlen, Booleans), [schreiben wir in die Konfiguration |services#Argumente]. +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* bezeichnet eine Situation, in der eine Kindklasse von einer Elternklasse erbt, deren Konstruktor Abhängigkeiten erfordert, und gleichzeitig die Kindklasse Abhängigkeiten erfordert. Dabei muss sie auch die elterlichen übernehmen und übergeben: +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 @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -Das Problem tritt auf, wenn wir den Konstruktor der Klasse `BaseClass` ändern wollen, zum Beispiel wenn eine neue Abhängigkeit hinzukommt. Dann müssen nämlich auch alle Konstruktoren der Kindklassen angepasst werden. Was eine solche Anpassung zur Hölle macht. +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 kann man dem vorbeugen? Die Lösung ist, **[Komposition der Vererbung vorzuziehen |faq#Warum wird Komposition der Vererbung vorgezogen]**. +Wie lässt sich dem vorbeugen? Die Lösung ist, **[Komposition der Vererbung vorzuziehen |faq#Warum ist Komposition der Vererbung vorzuziehen?]**. -Also entwerfen wir den Code anders. Wir werden [abstrakte |nette:introduction-to-object-oriented-programming#Abstrakte Klassen] `Base*` Klassen vermeiden. Anstatt dass `MyClass` bestimmte Funktionalität durch Erben von `BaseClass` erhält, lässt sie sich diese Funktionalität als Abhängigkeit übergeben: +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 @@ -125,10 +125,10 @@ final class MyClass ``` -Übergabe per Setter -=================== +Setter Injection +================ -Abhängigkeiten werden durch Aufruf einer Methode übergeben, die sie in einer privaten Variablen speichert. Die übliche Namenskonvention für diese Methoden ist die Form `set*()`, daher werden sie Setter genannt, aber sie können natürlich auch anders heißen. +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 @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -Diese Methode eignet sich für optionale Abhängigkeiten, die für die Funktion der Klasse nicht notwendig sind, da nicht garantiert ist, dass das Objekt die Abhängigkeit tatsächlich erhält (d. h. dass der Benutzer die Methode aufruft). +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). -Gleichzeitig erlaubt diese Methode, den Setter wiederholt aufzurufen und die Abhängigkeit so zu ändern. Wenn dies nicht erwünscht ist, fügen wir der Methode eine Prüfung hinzu oder markieren ab PHP 8.1 die Eigenschaft `$cache` mit dem Flag `readonly`. +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 @@ -157,14 +157,14 @@ class MyClass public function setCache(Cache $cache): void { if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); + 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] definiert. Auch hier wird die automatische Übergabe von Abhängigkeiten mittels Autowiring genutzt: +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: @@ -174,10 +174,10 @@ services: ``` -Zuweisung zu einer Variablen -============================ +Property Injection +================== -Abhängigkeiten werden durch Schreiben direkt in eine Mitgliedsvariable übergeben: +Die Abhängigkeiten werden übergeben, indem direkt in eine Property geschrieben wird: ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -Diese Methode wird als ungeeignet angesehen, da die Mitgliedsvariable als `public` deklariert werden muss. Dadurch haben wir keine Kontrolle darüber, dass die übergebene Abhängigkeit tatsächlich vom angegebenen Typ ist (galt vor PHP 7.4), und wir verlieren die Möglichkeit, auf die neu zugewiesene Abhängigkeit mit eigenem Code zu reagieren, beispielsweise um eine nachfolgende Änderung zu verhindern. Gleichzeitig wird die Variable Teil der öffentlichen Schnittstelle der Klasse, was möglicherweise nicht erwünscht ist. +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 der Variablen wird in der Konfiguration des DI-Containers im [Abschnitt setup |services#Setup] definiert: +Die Zuweisung an die Property wird in der Konfiguration des DI-Containers im [Abschnitt setup |services#Setup] festgelegt: ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -Während die vorherigen drei Methoden allgemein in allen objektorientierten Sprachen gelten, ist das Injizieren per Methode, Annotation oder Attribut *inject* spezifisch für Presenter in Nette. Ein [separates Kapitel |best-practices:inject-method-attribute] behandelt sie. +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]. -Welche Methode wählen? -====================== +Welchen Weg wählen? +=================== -- Der Konstruktor eignet sich für obligatorische Abhängigkeiten, die die Klasse unbedingt für ihre Funktion benötigt. -- Der Setter eignet sich hingegen für optionale Abhängigkeiten oder Abhängigkeiten, die man weiter ändern können möchte. -- Öffentliche Variablen sind nicht geeignet. +- 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 index 7c80005bfb..540b748d58 100644 --- a/dependency-injection/de/services.texy +++ b/dependency-injection/de/services.texy @@ -1,17 +1,17 @@ -Definition von Diensten -*********************** +Service-Definitionen +******************** .[perex] -Die Konfiguration ist der Ort, an dem wir dem DI-Container beibringen, wie er einzelne Dienste erstellen und sie mit anderen Abhängigkeiten verbinden soll. Nette bietet eine sehr übersichtliche und elegante Möglichkeit, dies zu erreichen. +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 Konfigurationsdatei im NEON-Format ist der Ort, an dem wir eigene Dienste und ihre Konfigurationen definieren. Sehen wir uns ein einfaches Beispiel für die Definition eines Dienstes namens `database` an, der eine Instanz der Klasse `PDO` repräsentiert: +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:') ``` -Die angegebene Konfiguration führt zu folgender Factory-Methode im [DI-Container|container]: +Aus der obigen Konfiguration entsteht im [DI-Container|container] diese Factory-Methode: ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -Dienstnamen ermöglichen es uns, uns in anderen Teilen der Konfigurationsdatei im Format `@dienstName` darauf zu beziehen. Wenn es nicht notwendig ist, den Dienst zu benennen, können wir einfach einen Bindestrich verwenden: +Ü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 Dienst aus dem DI-Container zu erhalten, können wir die Methode `getService()` mit dem Dienstnamen als Parameter oder die Methode `getByType()` mit dem Diensttyp verwenden: +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'); @@ -35,17 +35,17 @@ $database = $container->getByType(PDO::class); ``` -Erstellung eines Dienstes +Erstellung eines Services ========================= -Meistens erstellen wir einen Dienst einfach durch Instanziierung einer bestimmten Klasse. Zum Beispiel: +Ü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, kann die Definition auf mehrere Zeilen aufgeteilt werden: +Wenn wir die Konfiguration um weitere Schlüssel erweitern müssen, lässt sich die Definition auf mehrere Zeilen aufteilen: ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -Der Schlüssel `create` hat den Alias `factory`, beide Varianten sind in der Praxis üblich. Wir empfehlen jedoch die Verwendung von `create`. +Der Schlüssel `create` hat den Alias `factory`; beide Varianten sind gebräuchlich. Wir empfehlen jedoch, `create` zu verwenden. -Die Argumente des Konstruktors oder der Erstellungsmethode können alternativ im Schlüssel `arguments` angegeben werden: +Die Argumente für den Konstruktor oder die Factory-Methode lassen sich alternativ über den Schlüssel `arguments` angeben: ```neon services: @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -Dienste müssen nicht nur durch einfache Instanziierung einer Klasse erstellt werden, sie können auch das Ergebnis des Aufrufs statischer Methoden oder Methoden anderer Dienste sein: +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: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -Beachten Sie, dass zur Vereinfachung anstelle von `->` das Zeichen `::` verwendet wird, siehe [#Ausdrucksmittel]. Es werden diese Factory-Methoden generiert: +Beachten Sie, dass der Einfachheit halber `::` statt `->` verwendet wird, siehe [#Ausdruckssprache]. Es entstehen diese Factory-Methoden: ```php public function createServiceDatabase(): PDO @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -Der DI-Container muss den Typ des erstellten Dienstes kennen. Wenn wir einen Dienst mit einer Methode erstellen, die keinen spezifizierten Rückgabetyp hat, müssen wir diesen Typ explizit in der Konfiguration angeben: +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: @@ -100,14 +100,14 @@ services: Argumente ========= -Wir übergeben Argumente an den Konstruktor und Methoden auf eine Weise, die dem Vorgehen in PHP selbst sehr ähnlich ist: +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) ``` -Zur besseren Lesbarkeit können wir die Argumente auf separate Zeilen aufteilen. In diesem Fall ist die Verwendung von Kommas optional: +Für bessere Lesbarkeit lassen sich die Argumente auf einzelne Zeilen schreiben. In diesem Fall sind die Kommas optional: ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -Sie können Argumente auch benennen und müssen sich dann nicht um ihre Reihenfolge kümmern: +Sie können die Argumente auch benennen und müssen sich dann nicht um ihre Reihenfolge kümmern: ```neon services: @@ -129,20 +129,20 @@ services: ) ``` -Wenn Sie einige Argumente auslassen und ihren Standardwert verwenden oder einen Dienst mittels [Autowiring|autowiring] einsetzen möchten, verwenden Sie einen Unterstrich `_`: +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%) ``` -Als Argumente können Dienste übergeben, Parameter verwendet und vieles mehr getan werden, siehe [#Ausdrucksmittel]. +Argumente können Services, Parameter und vieles mehr enthalten, siehe [#Ausdruckssprache]. Setup ===== -Im Abschnitt `setup` definieren wir Methoden, die beim Erstellen des Dienstes aufgerufen werden sollen. +Im Abschnitt `setup` legen wir die Methoden fest, die beim Erzeugen des Services aufgerufen werden sollen. ```neon services: @@ -152,7 +152,7 @@ services: - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ``` -Das würde in PHP so aussehen: +In PHP sähe das so aus: ```php public function createServiceDatabase(): PDO @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -Neben dem Aufruf von Methoden können auch Werte an Eigenschaften übergeben werden. Das Hinzufügen eines Elements zu einem Array wird ebenfalls unterstützt, was in Anführungszeichen geschrieben werden muss, um nicht mit der NEON-Syntax zu kollidieren: +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: @@ -174,7 +174,7 @@ services: - '$onClick[]' = [@bar, clickHandler] ``` -Was im PHP-Code folgendermaßen aussehen würde: +Im PHP-Code sähe das so aus: ```php public function createServiceFoo(): Foo @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -Im Setup können jedoch auch statische Methoden oder Methoden anderer Dienste aufgerufen werden. Wenn Sie den aktuellen Dienst als Argument übergeben müssen, geben Sie ihn als `@self` an: +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: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -Beachten Sie, dass zur Vereinfachung anstelle von `->` das Zeichen `::` verwendet wird, siehe [#Ausdrucksmittel]. Es wird eine solche Factory-Methode generiert: +Beachten Sie, dass der Einfachheit halber `::` statt `->` verwendet wird, siehe [#Ausdruckssprache]. Es entsteht diese Factory-Methode: ```php public function createServiceFoo(): Foo @@ -210,26 +210,26 @@ public function createServiceFoo(): Foo ``` -Ausdrucksmittel -=============== +Ausdruckssprache +================ -Nette DI bietet uns außergewöhnlich reichhaltige Ausdrucksmittel, mit denen wir fast alles schreiben können. In Konfigurationsdateien können wir daher [Parameter |configuration#Parameter] verwenden: +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 des Parameters unter dem Schlüssel +# Wert eines Parameters unter einem Schlüssel %mailer.user% -# Parameter innerhalb einer Zeichenkette +# Parameter innerhalb eines Strings '%wwwDir%/images' ``` -Weiterhin Objekte erstellen, Methoden und Funktionen aufrufen: +Weiter Objekte erzeugen sowie Methoden und Funktionen aufrufen: ```neon -# Objekt erstellen +# Objekt erzeugen DateTime() # statische Methode aufrufen @@ -239,20 +239,20 @@ Collator::create(%locale%) ::getenv(DB_USER) ``` -Auf Dienste entweder nach ihrem Namen oder nach Typ verweisen: +Auf Services entweder über ihren Namen oder über ihren Typ verweisen: ```neon -# Dienst nach Namen +# Service nach Namen @database -# Dienst nach Typ +# Service nach Typ @Nette\Database\Connection ``` -Verwenden Sie die First-Class-Callable-Syntax: .{data-version:3.2.0} +Die First-Class-Callable-Syntax verwenden: .{data-version:3.2.0} ```neon -# Callback erstellen, Äquivalent zu [@user, logout] +# Callback erzeugen, gleichbedeutend mit [@user, logout] @user::logout(...) ``` @@ -262,11 +262,21 @@ Konstanten verwenden: # Klassenkonstante FilesystemIterator::SKIP_DOTS -# globale Konstante erhalten wir mit der PHP-Funktion constant() -::constant(PHP_VERSION) +# globale Konstante über die PHP-Funktion constant() holen +::constant(\PHP_VERSION) ``` -Methodenaufrufe können wie in PHP verkettet werden. Nur zur Vereinfachung wird anstelle von `->` das Zeichen `::` verwendet: +Ü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') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -Diese Ausdrücke können Sie überall verwenden, beim [Erstellen von Diensten |#Erstellung eines Dienstes], in [Argumenten |#Argumente], im Abschnitt [#setup] oder bei [Parametern |configuration#Parameter]: +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: @@ -293,12 +303,12 @@ services: Spezielle Funktionen -------------------- -In Konfigurationsdateien können Sie diese speziellen Funktionen verwenden: +In den Konfigurationsdateien können Sie die folgenden speziellen Funktionen verwenden: -- `not()` Negation eines Wertes -- `bool()`, `int()`, `float()`, `string()` verlustfreie Typumwandlung in den angegebenen Typ -- `typed()` erstellt ein Array aller Dienste des angegebenen Typs -- `tagged()` erstellt ein Array aller Dienste mit dem angegebenen Tag +- `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: @@ -308,18 +318,18 @@ services: ) ``` -Im Gegensatz zur klassischen Typumwandlung in PHP, wie z. B. `(int)`, wirft die verlustfreie Typumwandlung eine Ausnahme für nicht-numerische Werte. +Anders als die übliche Umwandlung in PHP, etwa `(int)`, wirft die verlustfreie Umwandlung bei nicht numerischen Werten eine Exception. -Die Funktion `typed()` erstellt ein Array aller Dienste des angegebenen Typs (Klasse oder Schnittstelle). Sie lässt Dienste aus, deren Autowiring deaktiviert ist. Es können auch mehrere Typen, durch Komma getrennt, angegeben werden. +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) ) ``` -Arrays von Diensten eines bestimmten Typs können auch automatisch mittels [Autowiring |autowiring#Array von Diensten] als Argument übergeben werden. +Ein Array von Services eines bestimmten Typs lässt sich auch automatisch über [Autowiring |autowiring#Sammlung von Services] als Argument übergeben. -Die Funktion `tagged()` erstellt dann ein Array aller Dienste mit einem bestimmten Tag. Auch hier können Sie mehrere Tags durch Komma getrennt angeben. +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: @@ -330,20 +340,20 @@ services: Autowiring ========== -Der Schlüssel `autowired` ermöglicht es, das Verhalten des Autowirings für einen bestimmten Dienst zu beeinflussen. Für Details siehe [Kapitel über Autowiring|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 Dienst foo wird vom Autowiring ausgeschlossen + autowired: false # der Service foo ist vom Autowiring ausgenommen ``` -Lazy Dienste .{data-version:3.2.4} -================================== +Lazy Services .{data-version:3.2.4} +=================================== -Lazy Loading ist eine Technik, die die Erstellung eines Dienstes bis zu dem Zeitpunkt aufschiebt, an dem er tatsächlich benötigt wird. In der globalen Konfiguration kann die [lazy Erstellung |configuration#Lazy Dienste] für alle Dienste gleichzeitig aktiviert werden. Für einzelne Dienste können Sie dieses Verhalten dann überschreiben: +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: @@ -352,16 +362,20 @@ services: lazy: false ``` -Wenn ein Dienst als lazy definiert ist, erhalten wir bei seiner Anforderung aus dem DI-Container ein spezielles Platzhalterobjekt. Dieses sieht aus und verhält sich genauso wie der tatsächliche Dienst, aber die tatsächliche Initialisierung (Aufruf des Konstruktors und des Setups) erfolgt erst beim ersten Zugriff auf eine seiner Methoden oder Eigenschaften. +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 kann nur für benutzerdefinierte Klassen verwendet werden, nicht für interne PHP-Klassen. Erfordert PHP 8.4 oder neuer. +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, Diensten zusätzliche Informationen hinzuzufügen. Sie können einem Dienst einen oder mehrere Tags hinzufügen: +Tags dienen dazu, Services um ergänzende Informationen zu erweitern. Sie können einem Service einen oder mehrere Tags zuweisen: ```neon services: @@ -381,18 +395,18 @@ services: logger: monolog.logger.event ``` -Um alle Dienste mit bestimmten Tags zu erhalten, können Sie die Funktion `tagged()` verwenden: +Um alle Services mit bestimmten Tags zu holen, können Sie die Funktion `tagged()` verwenden: ```neon services: - LoggersDependent( tagged(logger) ) ``` -Im DI-Container können Sie die Namen aller Dienste mit einem bestimmten Tag mithilfe der Methode `findByTag()` abrufen: +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, das den Dienstnamen und den Tag-Wert enthält +// $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', ...] ``` @@ -400,7 +414,7 @@ $names = $container->findByTag('logger'); Inject-Modus ============ -Mit dem Flag `inject: true` wird die Übergabe von Abhängigkeiten über öffentliche Variablen mit der Annotation [inject |best-practices:inject-method-attribute#Inject -Attribute] und Methoden [inject*() |best-practices:inject-method-attribute#inject -Methoden] aktiviert. +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: @@ -409,13 +423,13 @@ services: inject: true ``` -Standardmäßig ist `inject` nur für Presenter aktiviert. +Standardmäßig ist der Modus `inject` nur für Presenter eingeschaltet. -Modifikation von Diensten -========================= +Änderungen an Services +====================== -Der DI-Container enthält viele Dienste, die über eingebaute oder [benutzerdefinierte Erweiterungen|extensions] hinzugefügt wurden. Sie können die Definitionen dieser Dienste direkt in der Konfiguration ändern. Beispielsweise können Sie die Klasse des Dienstes `application.application`, die standardmäßig `Nette\Application\Application` ist, in eine andere ändern: +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: @@ -424,7 +438,7 @@ services: alteration: true ``` -Das Flag `alteration` ist informativ und besagt, dass wir nur einen bestehenden Dienst modifizieren. +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: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -Beim Überschreiben eines Dienstes möchten wir möglicherweise die ursprünglichen Argumente, Setup-Einträge oder Tags entfernen, wozu `reset` dient: +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: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -Wenn Sie einen durch eine Erweiterung hinzugefügten Dienst entfernen möchten, können Sie dies wie folgt tun: +Wenn Sie einen Service entfernen wollen, den eine Extension hinzugefügt hat, geht das so: ```neon services: diff --git a/dependency-injection/de/upgrading.texy b/dependency-injection/de/upgrading.texy new file mode 100644 index 0000000000..389c86ea75 --- /dev/null +++ b/dependency-injection/de/upgrading.texy @@ -0,0 +1,49 @@ +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/el/@home.texy b/dependency-injection/el/@home.texy deleted file mode 100644 index 8c439b2f8a..0000000000 --- a/dependency-injection/el/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Το Dependency Injection είναι ένα πρότυπο σχεδίασης που θα αλλάξει ριζικά την οπτική σας για τον κώδικα και την ανάπτυξη. Θα σας ανοίξει τον δρόμο στον κόσμο των καθαρά σχεδιασμένων και βιώσιμων εφαρμογών. - -- [Τι είναι το Dependency Injection; |introduction] -- [Καθολική κατάσταση και singletons |global-state] -- [Πέρασμα εξαρτήσεων |passing-dependencies] -- [Τι είναι ο DI container; |container] -- [Συχνές Ερωτήσεις|faq] - - -Το πακέτο `nette/di` παρέχει έναν εξαιρετικά προηγμένο μεταγλωττισμένο DI container για PHP. - -- [Nette DI Container |nette-container] -- [Διαμόρφωση |configuration] -- [Ορισμός υπηρεσιών |services] -- [Autowiring |autowiring] -- [Δημιουργημένα factories |factory] -- [Δημιουργία επεκτάσεων για το Nette DI|extensions] diff --git a/dependency-injection/el/@left-menu.texy b/dependency-injection/el/@left-menu.texy deleted file mode 100644 index 4cfa98f34c..0000000000 --- a/dependency-injection/el/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Τι είναι το DI; |introduction] -- [Καθολική κατάσταση και singletons |global-state] -- [Πέρασμα εξαρτήσεων |passing-dependencies] -- [Τι είναι ο DI container; |container] -- [Συχνές Ερωτήσεις|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Διαμόρφωση |configuration] -- [Ορισμός υπηρεσιών |services] -- [Autowiring |autowiring] -- [Δημιουργημένα factories |factory] -- [Δημιουργία επεκτάσεων για το Nette DI|extensions] diff --git a/dependency-injection/el/@meta.texy b/dependency-injection/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/dependency-injection/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/dependency-injection/el/autowiring.texy b/dependency-injection/el/autowiring.texy deleted file mode 100644 index 024d4079d1..0000000000 --- a/dependency-injection/el/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Το Autowiring είναι ένα εξαιρετικό χαρακτηριστικό που μπορεί να περάσει αυτόματα τις απαιτούμενες υπηρεσίες στον κατασκευαστή και σε άλλες μεθόδους, οπότε δεν χρειάζεται να τις γράψουμε καθόλου. Σας εξοικονομεί πολύ χρόνο. - -Χάρη σε αυτό, μπορούμε να παραλείψουμε τη συντριπτική πλειοψηφία των ορισμάτων κατά τη σύνταξη ορισμών υπηρεσιών. Αντί για: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Αρκεί να γράψουμε: - -```neon -services: - articles: Model\ArticleRepository -``` - -Το Autowiring καθοδηγείται από τους τύπους, οπότε για να λειτουργήσει, η κλάση `ArticleRepository` πρέπει να οριστεί κάπως έτσι: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Για να είναι δυνατή η χρήση του autowiring, πρέπει να υπάρχει **ακριβώς μία υπηρεσία** για κάθε τύπο στο container. Αν υπήρχαν περισσότερες, το autowiring δεν θα ήξερε ποια να περάσει και θα προκαλούσε εξαίρεση: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, ταιριάζουν και η mainDb και η tempDb -``` - -Η λύση θα ήταν είτε να παρακάμψουμε το autowiring και να δηλώσουμε ρητά το όνομα της υπηρεσίας (δηλ. `articles: Model\ArticleRepository(@mainDb)`). Πιο έξυπνο όμως είναι να [απενεργοποιήσουμε |#Απενεργοποίηση του autowiring] το autowiring για μία από τις υπηρεσίες, ή να [δώσουμε προτεραιότητα |#Προτίμηση autowiring] στην πρώτη υπηρεσία. - - -Απενεργοποίηση του autowiring ------------------------------ - -Μπορούμε να απενεργοποιήσουμε το autowiring μιας υπηρεσίας χρησιμοποιώντας την επιλογή `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # η υπηρεσία tempDb εξαιρείται από το autowiring - - articles: Model\ArticleRepository # επομένως περνάει τη mainDb στον κατασκευαστή -``` - -Η υπηρεσία `articles` δεν προκαλεί εξαίρεση ότι υπάρχουν δύο κατάλληλες υπηρεσίες τύπου `PDO` (δηλ. `mainDb` και `tempDb`) που μπορούν να περάσουν στον κατασκευαστή, επειδή βλέπει μόνο την υπηρεσία `mainDb`. - -.[note] -Η διαμόρφωση του autowiring στο Nette λειτουργεί διαφορετικά από ό,τι στο Symfony, όπου η επιλογή `autowire: false` λέει ότι το autowiring δεν πρέπει να χρησιμοποιείται για τα ορίσματα του κατασκευαστή της συγκεκριμένης υπηρεσίας. Στο Nette, το autowiring χρησιμοποιείται πάντα, είτε για τα ορίσματα του κατασκευαστή, είτε για οποιαδήποτε άλλη μέθοδο. Η επιλογή `autowired: false` λέει ότι η παρουσία της συγκεκριμένης υπηρεσίας δεν πρέπει να περνιέται πουθενά μέσω autowiring. - - -Προτίμηση autowiring --------------------- - -Εάν έχουμε πολλές υπηρεσίες του ίδιου τύπου και σε μία από αυτές δηλώσουμε την επιλογή `autowired`, αυτή η υπηρεσία γίνεται η προτιμώμενη: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # γίνεται η προτιμώμενη - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Η υπηρεσία `articles` δεν προκαλεί εξαίρεση ότι υπάρχουν δύο κατάλληλες υπηρεσίες τύπου `PDO` (δηλ. `mainDb` και `tempDb`), αλλά χρησιμοποιεί την προτιμώμενη υπηρεσία, δηλαδή τη `mainDb`. - - -Πίνακας υπηρεσιών ------------------ - -Το Autowiring μπορεί επίσης να περάσει πίνακες υπηρεσιών ενός συγκεκριμένου τύπου. Επειδή στην PHP δεν είναι δυνατό να γραφτεί εγγενώς ο τύπος των στοιχείων του πίνακα, είναι απαραίτητο, εκτός από τον τύπο `array`, να συμπληρωθεί και ένα phpDoc σχόλιο με τον τύπο του στοιχείου στη μορφή `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Το DI container στη συνέχεια περνά αυτόματα έναν πίνακα υπηρεσιών που αντιστοιχούν στον συγκεκριμένο τύπο. Παραλείπει τις υπηρεσίες που έχουν απενεργοποιημένο το autowiring. - -Ο τύπος στο σχόλιο μπορεί επίσης να είναι στη μορφή `array<int, Class>` ή `list<Class>`. Εάν δεν μπορείτε να επηρεάσετε τη μορφή του phpDoc σχολίου, μπορείτε να περάσετε τον πίνακα υπηρεσιών απευθείας στη διαμόρφωση χρησιμοποιώντας το [`typed()` |services#Ειδικές συναρτήσεις]. - - -Σκαλωτά ορίσματα ----------------- - -Το Autowiring μπορεί να αντικαταστήσει μόνο αντικείμενα και πίνακες αντικειμένων. Τα σκαλωτά ορίσματα (π.χ. συμβολοσειρές, αριθμοί, booleans) [τα γράφουμε στη διαμόρφωση |services#Ορίσματα]. Μια εναλλακτική λύση είναι να δημιουργήσετε ένα [settings-object |best-practices:passing-settings-to-presenters], το οποίο ενσωματώνει την σκαλωτή τιμή (ή περισσότερες τιμές) σε μορφή αντικειμένου, το οποίο στη συνέχεια μπορεί να περάσει ξανά μέσω autowiring. - -```php -class MySettings -{ - public function __construct( - // το readonly είναι δυνατό να χρησιμοποιηθεί από την PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Δημιουργείτε μια υπηρεσία από αυτό προσθέτοντάς το στη διαμόρφωση: - -```neon -services: - - MySettings('any value') -``` - -Όλες οι κλάσεις στη συνέχεια το ζητούν μέσω autowiring. - - -Περιορισμός του autowiring --------------------------- - -Για μεμονωμένες υπηρεσίες, το autowiring μπορεί να περιοριστεί μόνο σε συγκεκριμένες κλάσεις ή interfaces. - -Κανονικά, το autowiring περνά την υπηρεσία σε κάθε παράμετρο μεθόδου, ο τύπος της οποίας αντιστοιχεί στην υπηρεσία. Ο περιορισμός σημαίνει ότι θέτουμε συνθήκες που πρέπει να πληρούν οι τύποι που αναφέρονται στις παραμέτρους των μεθόδων, ώστε η υπηρεσία να τους περάσει. - -Ας το δείξουμε με ένα παράδειγμα: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Αν τις καταχωρούσαμε όλες ως υπηρεσίες, το autowiring θα αποτύγχανε: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, ταιριάζουν οι υπηρεσίες parent και child - childDep: ChildDependent # το autowiring περνά την υπηρεσία child στον κατασκευαστή -``` - -Η υπηρεσία `parentDep` προκαλεί εξαίρεση `Multiple services of type ParentClass found: parent, child`, επειδή στον κατασκευαστή της ταιριάζουν και οι δύο υπηρεσίες `parent` και `child`, και το autowiring δεν μπορεί να αποφασίσει ποια να επιλέξει. - -Για την υπηρεσία `child`, μπορούμε επομένως να περιορίσουμε το autowiring της στον τύπο `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # μπορεί να γραφτεί και 'autowired: self' - - parentDep: ParentDependent # το autowiring περνά την υπηρεσία parent στον κατασκευαστή - childDep: ChildDependent # το autowiring περνά την υπηρεσία child στον κατασκευαστή -``` - -Τώρα, στον κατασκευαστή της υπηρεσίας `parentDep` περνιέται η υπηρεσία `parent`, επειδή τώρα είναι το μόνο κατάλληλο αντικείμενο. Το autowiring δεν περνά πλέον την υπηρεσία `child` εκεί. Ναι, η υπηρεσία `child` εξακολουθεί να είναι τύπου `ParentClass`, αλλά η περιοριστική συνθήκη που δόθηκε για τον τύπο της παραμέτρου δεν ισχύει πλέον, δηλ. δεν ισχύει ότι το `ParentClass` *είναι υπερτύπος* του `ChildClass`. - -Για την υπηρεσία `child`, το `autowired: ChildClass` θα μπορούσε επίσης να γραφτεί ως `autowired: self`, καθώς το `self` είναι ένα placeholder για την κλάση της τρέχουσας υπηρεσίας. - -Στο κλειδί `autowired`, είναι δυνατόν να αναφερθούν και πολλές κλάσεις ή interfaces ως πίνακας: - -```neon -autowired: [BarClass, FooInterface] -``` - -Ας δοκιμάσουμε να συμπληρώσουμε το παράδειγμα και με interfaces: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Όταν η υπηρεσία `child` δεν περιορίζεται καθόλου, θα ταιριάζει στους κατασκευαστές όλων των κλάσεων `FooDependent`, `BarDependent`, `ParentDependent` και `ChildDependent` και το autowiring θα την περάσει εκεί. - -Αν όμως περιορίσουμε το autowiring της σε `ChildClass` χρησιμοποιώντας `autowired: ChildClass` (ή `self`), το autowiring θα την περάσει μόνο στον κατασκευαστή του `ChildDependent`, επειδή απαιτεί όρισμα τύπου `ChildClass` και ισχύει ότι το `ChildClass` *είναι τύπου* `ChildClass`. Κανένας άλλος τύπος που αναφέρεται στις άλλες παραμέτρους δεν είναι υπερτύπος του `ChildClass`, οπότε η υπηρεσία δεν περνιέται. - -Αν το περιορίσουμε σε `ParentClass` χρησιμοποιώντας `autowired: ParentClass`, το autowiring θα την περάσει ξανά στον κατασκευαστή του `ChildDependent` (επειδή το απαιτούμενο `ChildClass` είναι υπερτύπος του `ParentClass`) και τώρα και στον κατασκευαστή του `ParentDependent`, επειδή ο απαιτούμενος τύπος `ParentClass` είναι επίσης κατάλληλος. - -Αν το περιορίσουμε σε `FooInterface`, θα εξακολουθεί να γίνεται autowired στο `ParentDependent` (το απαιτούμενο `ParentClass` είναι υπερτύπος του `FooInterface`) και στο `ChildDependent`, αλλά επιπλέον και στον κατασκευαστή του `FooDependent`, όχι όμως στο `BarDependent`, επειδή το `BarInterface` δεν είναι υπερτύπος του `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # το autowiring περνά το child στον κατασκευαστή - barDep: BarDependent # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, καμία υπηρεσία δεν ταιριάζει - parentDep: ParentDependent # το autowiring περνά το child στον κατασκευαστή - childDep: ChildDependent # το autowiring περνά το child στον κατασκευαστή -``` diff --git a/dependency-injection/el/configuration.texy b/dependency-injection/el/configuration.texy deleted file mode 100644 index 03e6ecb64d..0000000000 --- a/dependency-injection/el/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Διαμόρφωση του DI Container -*************************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette DI Container. - - -Αρχείο διαμόρφωσης -================== - -Το Nette DI Container ελέγχεται εύκολα μέσω αρχείων διαμόρφωσης. Αυτά συνήθως γράφονται σε [μορφή NEON |neon:format]. Για την επεξεργασία, συνιστούμε [editors με υποστήριξη |best-practices:editors-and-tools#IDE editor] αυτής της μορφής. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI container .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Επεκτάσεις]: "Εγκατάσταση πρόσθετων επεκτάσεων DI .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Εισαγωγή αρχείων]: "Εισαγωγή αρχείων .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Παράμετροι]: "Παράμετροι .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Αναζήτηση]: "Αυτόματη καταχώρηση υπηρεσιών .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Υπηρεσίες .[prism-token prism-comment]" -</pre> - -.[note] -Για να γράψετε μια συμβολοσειρά που περιέχει τον χαρακτήρα `%`, πρέπει να τον διαφύγετε διπλασιάζοντάς τον σε `%%`. - - -Παράμετροι -========== - -Στη διαμόρφωση, μπορείτε να ορίσετε παραμέτρους που μπορούν στη συνέχεια να χρησιμοποιηθούν ως μέρος των ορισμών υπηρεσιών. Με αυτόν τον τρόπο, μπορείτε να κάνετε τη διαμόρφωση πιο σαφή ή να ενοποιήσετε και να απομονώσετε τιμές που θα αλλάξουν. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Αναφερόμαστε στην παράμετρο `dsn` οπουδήποτε στη διαμόρφωση γράφοντας `%dsn%`. Οι παράμετροι μπορούν να χρησιμοποιηθούν και μέσα σε συμβολοσειρές όπως `'%wwwDir%/images'`. - -Οι παράμετροι δεν χρειάζεται να είναι μόνο συμβολοσειρές ή αριθμοί, μπορούν επίσης να περιέχουν πίνακες: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Αναφερόμαστε σε ένα συγκεκριμένο κλειδί ως `%mailer.user%`. - -Εάν χρειάζεστε στον κώδικά σας, για παράδειγμα σε μια κλάση, να μάθετε την τιμή οποιασδήποτε παραμέτρου, τότε περάστε την σε αυτήν την κλάση. Για παράδειγμα, στον κατασκευαστή. Δεν υπάρχει κανένα καθολικό αντικείμενο που να αντιπροσωπεύει τη διαμόρφωση, το οποίο οι κλάσεις θα ρωτούσαν για τις τιμές των παραμέτρων. Αυτό θα παραβίαζε την αρχή του dependency injection. - - -Υπηρεσίες -========= - -Βλ. [ξεχωριστό κεφάλαιο |services]. - - -Decorator -========= - -Πώς να τροποποιήσετε μαζικά όλες τις υπηρεσίες ενός συγκεκριμένου τύπου; Για παράδειγμα, να καλέσετε μια συγκεκριμένη μέθοδο σε όλους τους presenters που κληρονομούν από έναν συγκεκριμένο κοινό πρόγονο? Γι' αυτό υπάρχει ο decorator. - -```neon -decorator: - # για όλες τις υπηρεσίες που είναι παρουσίες αυτής της κλάσης ή interface - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # καλέστε αυτή τη μέθοδο - - $absoluteUrls = true # και ορίστε τη μεταβλητή -``` - -Ο decorator μπορεί επίσης να χρησιμοποιηθεί για τον ορισμό [tags |services#Tags] ή την ενεργοποίηση της λειτουργίας [inject |services#Λειτουργία Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Τεχνικές ρυθμίσεις του DI container. - -```neon -di: - # εμφάνιση του DIC στο Tracy Bar; - debugger: ... # (bool) η προεπιλογή είναι true - - # τύποι παραμέτρων που δεν γίνονται ποτέ autowired - excluded: ... # (string[]) - - # επιτρέπεται η lazy δημιουργία υπηρεσιών; - lazy: ... # (bool) η προεπιλογή είναι false - - # κλάση από την οποία κληρονομεί το DI container - parentClass: ... # (string) η προεπιλογή είναι Nette\DI\Container -``` - - -Lazy υπηρεσίες .{data-version:3.2.4} ------------------------------------- - -Η ρύθμιση `lazy: true` ενεργοποιεί τη lazy (καθυστερημένη) δημιουργία υπηρεσιών. Αυτό σημαίνει ότι οι υπηρεσίες δεν δημιουργούνται πραγματικά τη στιγμή που τις ζητάμε από το DI container, αλλά τη στιγμή της πρώτης τους χρήσης. Αυτό μπορεί να επιταχύνει την εκκίνηση της εφαρμογής και να μειώσει τις απαιτήσεις μνήμης, καθώς δημιουργούνται μόνο οι υπηρεσίες που είναι πραγματικά απαραίτητες στο συγκεκριμένο request. - -Για μια συγκεκριμένη υπηρεσία, η lazy δημιουργία μπορεί να [αλλάξει |services#Lazy υπηρεσίες]. - -.[note] -Τα lazy αντικείμενα μπορούν να χρησιμοποιηθούν μόνο για κλάσεις χρήστη, όχι για εσωτερικές κλάσεις PHP. Απαιτεί PHP 8.4 ή νεότερη έκδοση. - - -Εξαγωγή μεταδεδομένων ---------------------- - -Η κλάση του DI container περιέχει επίσης πολλά μεταδεδομένα. Μπορείτε να τη μειώσετε περιορίζοντας την εξαγωγή μεταδεδομένων. - -```neon -di: - export: - # εξαγωγή παραμέτρων; - parameters: false # (bool) η προεπιλογή είναι true - - # εξαγωγή tags και ποια; - tags: # (string[]|bool) η προεπιλογή είναι όλα - - event.subscriber - - # εξαγωγή δεδομένων για autowiring και ποια; - types: # (string[]|bool) η προεπιλογή είναι όλα - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Εάν δεν χρησιμοποιείτε τον πίνακα `$container->getParameters()`, μπορείτε να απενεργοποιήσετε την εξαγωγή παραμέτρων. Επιπλέον, μπορείτε να εξάγετε μόνο τα tags μέσω των οποίων λαμβάνετε υπηρεσίες με τη μέθοδο `$container->findByTag(...)`. Εάν δεν καλείτε καθόλου τη μέθοδο, μπορείτε να απενεργοποιήσετε εντελώς την εξαγωγή tags χρησιμοποιώντας `false`. - -Μπορείτε να μειώσετε σημαντικά τα μεταδεδομένα για [autowiring |autowiring] αναφέροντας τις κλάσεις που χρησιμοποιείτε ως παράμετρο της μεθόδου `$container->getByType()`. Και πάλι, εάν δεν καλείτε καθόλου τη μέθοδο (ή μόνο στο [bootstrap |application:bootstrapping] για να λάβετε το `Nette\Application\Application`), μπορείτε να απενεργοποιήσετε εντελώς την εξαγωγή χρησιμοποιώντας `false`. - - -Επεκτάσεις -========== - -Καταχώρηση πρόσθετων επεκτάσεων DI. Με αυτόν τον τρόπο προσθέτουμε, για παράδειγμα, την επέκταση DI `Dibi\Bridges\Nette\DibiExtension22` με το όνομα `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Στη συνέχεια, τη διαμορφώνουμε στην ενότητα `dibi`: - -```neon -dibi: - host: localhost -``` - -Ως επέκταση μπορεί να προστεθεί και μια κλάση που έχει παραμέτρους: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Εισαγωγή αρχείων -================ - -Μπορούμε να εισάγουμε άλλα αρχεία διαμόρφωσης στην ενότητα `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Το όνομα `parameters.php` δεν είναι τυπογραφικό λάθος, η διαμόρφωση μπορεί επίσης να γραφτεί σε ένα αρχείο PHP, το οποίο την επιστρέφει ως πίνακα: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Εάν εμφανιστούν στοιχεία με τα ίδια κλειδιά σε αρχεία διαμόρφωσης, θα αντικατασταθούν ή, στην περίπτωση [πινάκων, θα συγχωνευθούν |#Συγχώνευση]. Το αρχείο που εισάγεται αργότερα έχει υψηλότερη προτεραιότητα από το προηγούμενο. Το αρχείο στο οποίο αναφέρεται η ενότητα `includes` έχει υψηλότερη προτεραιότητα από τα αρχεία που εισάγονται σε αυτό. - - -Αναζήτηση -========= - -Η αυτόματη προσθήκη υπηρεσιών στο DI container διευκολύνει εξαιρετικά την εργασία. Το Nette προσθέτει αυτόματα presenters στο container, αλλά μπορεί εύκολα να προσθέσει και οποιεσδήποτε άλλες κλάσεις. - -Αρκεί να αναφέρετε σε ποιους καταλόγους (και υποκαταλόγους) πρέπει να αναζητήσει κλάσεις: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Συνήθως, όμως, δεν θέλουμε να προσθέσουμε απολύτως όλες τις κλάσεις και τα interfaces, γι' αυτό μπορούμε να τα φιλτράρουμε: - -```neon -search: - - in: %appDir%/Forms - - # φιλτράρισμα με βάση το όνομα αρχείου (string|string[]) - files: - - *Factory.php - - # φιλτράρισμα με βάση το όνομα κλάσης (string|string[]) - classes: - - *Factory -``` - -Ή μπορούμε να επιλέξουμε κλάσεις που κληρονομούν ή υλοποιούν τουλάχιστον μία από τις αναφερόμενες κλάσεις: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Μπορούν επίσης να οριστούν κανόνες εξαίρεσης, δηλ. μάσκες ονόματος κλάσης ή κληρονομικοί πρόγονοι, που εάν ταιριάζουν, η υπηρεσία δεν προστίθεται στο DI container: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Σε όλες τις υπηρεσίες μπορούν να οριστούν tags: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Συγχώνευση -========== - -Εάν εμφανιστούν στοιχεία με τα ίδια κλειδιά σε περισσότερα αρχεία διαμόρφωσης, θα αντικατασταθούν ή, στην περίπτωση πινάκων, θα συγχωνευθούν. Το αρχείο που εισάγεται αργότερα έχει υψηλότερη προτεραιότητα από το προηγούμενο. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>αποτέλεσμα</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Στους πίνακες, η συγχώνευση μπορεί να αποτραπεί αναφέροντας ένα θαυμαστικό μετά το όνομα του κλειδιού: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>αποτέλεσμα</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Διαμόρφωση Dependency Injection}} diff --git a/dependency-injection/el/container.texy b/dependency-injection/el/container.texy deleted file mode 100644 index 9b371ad0da..0000000000 --- a/dependency-injection/el/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Τι είναι το DI Container; -************************* - -.[perex] -Ένα dependency injection container (DIC) είναι μια κλάση που μπορεί να δημιουργήσει και να διαμορφώσει αντικείμενα. - -Μπορεί να σας εκπλήξει, αλλά σε πολλές περιπτώσεις δεν χρειάζεστε ένα dependency injection container για να επωφεληθείτε από το dependency injection (συντομογραφία DI). Άλλωστε, ακόμη και στο [εισαγωγικό κεφάλαιο |introduction] δείξαμε το DI με συγκεκριμένα παραδείγματα και δεν χρειαζόταν κανένα container. - -Ωστόσο, εάν χρειάζεται να διαχειριστείτε μεγάλο αριθμό διαφορετικών αντικειμένων με πολλές εξαρτήσεις, ένα dependency injection container θα είναι πραγματικά χρήσιμο. Αυτό ισχύει, για παράδειγμα, για τις web εφαρμογές που βασίζονται σε ένα framework. - -Στο προηγούμενο κεφάλαιο, παρουσιάσαμε τις κλάσεις `Article` και `UserController`. Και οι δύο έχουν κάποιες εξαρτήσεις, δηλαδή τη βάση δεδομένων και το factory `ArticleFactory`. Και για αυτές τις κλάσεις θα δημιουργήσουμε τώρα ένα container. Φυσικά, για ένα τόσο απλό παράδειγμα, δεν έχει νόημα να έχουμε ένα container. Αλλά θα το δημιουργήσουμε για να δείξουμε πώς μοιάζει και πώς λειτουργεί. - -Εδώ είναι ένα απλό hardcoded container για το αναφερόμενο παράδειγμα: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -Η χρήση θα έμοιαζε ως εξής: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Απλώς ρωτάμε το container για το αντικείμενο και δεν χρειάζεται πλέον να γνωρίζουμε τίποτα για το πώς να το δημιουργήσουμε και ποιες είναι οι εξαρτήσεις του. όλα αυτά τα γνωρίζει το container. Οι εξαρτήσεις εισάγονται αυτόματα από το container. Σε αυτό έγκειται η δύναμή του. - -Το container έχει προς το παρόν όλα τα δεδομένα γραμμένα απευθείας στον κώδικα. Θα κάνουμε λοιπόν το επόμενο βήμα και θα προσθέσουμε παραμέτρους, ώστε το container να είναι πραγματικά χρήσιμο: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Οι προσεκτικοί αναγνώστες μπορεί να έχουν παρατηρήσει ένα συγκεκριμένο πρόβλημα. Κάθε φορά που λαμβάνω ένα αντικείμενο `UserController`, δημιουργείται επίσης μια νέα παρουσία του `ArticleFactory` και της βάσης δεδομένων. Αυτό σίγουρα δεν το θέλουμε. - -Θα προσθέσουμε λοιπόν μια μέθοδο `getService()`, η οποία θα επιστρέφει πάντα τις ίδιες παρουσίες: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // το getService('Database') θα καλέσει το createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Κατά την πρώτη κλήση, π.χ. `$container->getService('Database')`, θα ζητήσει από το `createDatabase()` να δημιουργήσει το αντικείμενο της βάσης δεδομένων, το οποίο θα αποθηκεύσει στον πίνακα `$services` και κατά την επόμενη κλήση θα το επιστρέψει απευθείας. - -Θα τροποποιήσουμε και το υπόλοιπο container, ώστε να χρησιμοποιεί το `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Παρεμπιπτόντως, ο όρος υπηρεσία (service) αναφέρεται σε οποιοδήποτε αντικείμενο διαχειρίζεται το container. Γι' αυτό και το όνομα της μεθόδου `getService()`. - -Έτοιμο. Έχουμε ένα πλήρως λειτουργικό DI container! Και μπορούμε να το χρησιμοποιήσουμε: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Όπως βλέπετε, η σύνταξη ενός DIC δεν είναι κάτι περίπλοκο. Αξίζει να θυμηθούμε ότι τα ίδια τα αντικείμενα δεν γνωρίζουν ότι τα δημιουργεί κάποιο container. Έτσι, είναι δυνατόν να δημιουργηθεί με αυτόν τον τρόπο οποιοδήποτε αντικείμενο στην PHP χωρίς παρέμβαση στον πηγαίο κώδικά του. - -Η χειροκίνητη δημιουργία και συντήρηση της κλάσης του container μπορεί γρήγορα να γίνει εφιάλτης. Στο επόμενο κεφάλαιο, θα μιλήσουμε λοιπόν για το [Nette DI Container |nette-container], το οποίο μπορεί να δημιουργείται και να ενημερώνεται σχεδόν από μόνο του. - - -{{maintitle: Τι είναι το dependency injection container;}} diff --git a/dependency-injection/el/extensions.texy b/dependency-injection/el/extensions.texy deleted file mode 100644 index 3f37bb6398..0000000000 --- a/dependency-injection/el/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Δημιουργία επεκτάσεων για το Nette DI -************************************* - -.[perex] -Η δημιουργία του DI container, εκτός από τα αρχεία διαμόρφωσης, επηρεάζεται και από τις λεγόμενες *επεκτάσεις*. Τις ενεργοποιούμε στο αρχείο διαμόρφωσης στην ενότητα `extensions`. - -Έτσι προσθέτουμε την επέκταση που αντιπροσωπεύεται από την κλάση `BlogExtension` με το όνομα `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Κάθε επέκταση του compiler κληρονομεί από το [api:Nette\DI\CompilerExtension] και μπορεί να υλοποιήσει τις ακόλουθες μεθόδους, οι οποίες καλούνται διαδοχικά κατά τη συναρμολόγηση του DI container: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Αυτή η μέθοδος καλείται πρώτη. Ορίζει το schema για την επικύρωση των παραμέτρων διαμόρφωσης. - -Διαμορφώνουμε την επέκταση στην ενότητα της οποίας το όνομα είναι το ίδιο με αυτό με το οποίο προστέθηκε η επέκταση, δηλαδή `blog`: - -```neon -# ίδιο όνομα με την επέκταση -blog: - postsPerPage: 10 - allowComments: false -``` - -Δημιουργούμε ένα schema που περιγράφει όλες τις επιλογές διαμόρφωσης, συμπεριλαμβανομένων των τύπων τους, των επιτρεπόμενων τιμών και, ενδεχομένως, των προεπιλεγμένων τιμών τους: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -Θα βρείτε την τεκμηρίωση στη σελίδα [Schema |schema:]. Επιπλέον, μπορείτε να καθορίσετε ποιες επιλογές μπορούν να είναι [δυναμικές |application:bootstrapping#Δυναμικές Παράμετροι] χρησιμοποιώντας το `dynamic()`, π.χ. `Expect::int()->dynamic()`. - -Έχουμε πρόσβαση στη διαμόρφωση μέσω της μεταβλητής `$this->config`, η οποία είναι ένα αντικείμενο `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Χρησιμοποιείται για την προσθήκη υπηρεσιών στο container. Γι' αυτό χρησιμοποιείται το [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Η σύμβαση είναι να προτάσσεται στις υπηρεσίες που προστίθενται από την επέκταση το όνομά της, ώστε να μην προκύπτουν συγκρούσεις ονομάτων. Αυτό το κάνει η μέθοδος `prefix()`, οπότε αν η επέκταση ονομάζεται `blog`, η υπηρεσία θα ονομάζεται `blog.articles`. - -Εάν χρειαστεί να μετονομάσουμε μια υπηρεσία, μπορούμε, για λόγους διατήρησης της συμβατότητας προς τα πίσω, να δημιουργήσουμε ένα ψευδώνυμο (alias) με το αρχικό όνομα. Παρόμοια το κάνει το Nette, π.χ. για την υπηρεσία `routing.router`, η οποία είναι διαθέσιμη και με το προηγούμενο όνομα `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Φόρτωση υπηρεσιών από αρχείο ----------------------------- - -Δεν χρειάζεται να δημιουργούμε υπηρεσίες μόνο μέσω του API της κλάσης ContainerBuilder, αλλά και με τη γνωστή σύνταξη που χρησιμοποιείται στο αρχείο διαμόρφωσης NEON στην ενότητα services. Το πρόθεμα `@extension` αντιπροσωπεύει την τρέχουσα επέκταση. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Φορτώνουμε τις υπηρεσίες: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // φόρτωση του αρχείου διαμόρφωσης για την επέκταση - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Η μέθοδος καλείται τη στιγμή που το container περιέχει όλες τις υπηρεσίες που προστέθηκαν από τις μεμονωμένες επεκτάσεις στις μεθόδους `loadConfiguration` καθώς και από τα αρχεία διαμόρφωσης χρήστη. Σε αυτή τη φάση της συναρμολόγησης, μπορούμε λοιπόν να τροποποιήσουμε τους ορισμούς των υπηρεσιών ή να συμπληρώσουμε τις συνδέσεις μεταξύ τους. Για την αναζήτηση υπηρεσιών στο container με βάση τα tags, μπορεί να χρησιμοποιηθεί η μέθοδος `findByTag()`, ενώ με βάση την κλάση ή το interface, η μέθοδος `findByType()`. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -Σε αυτή τη φάση, η κλάση του container έχει ήδη δημιουργηθεί με τη μορφή αντικειμένου [ClassType |php-generator:#Κλάσεις], περιέχει όλες τις μεθόδους που δημιουργούν τις υπηρεσίες και είναι έτοιμη για εγγραφή στην cache. Τον τελικό κώδικα της κλάσης μπορούμε ακόμα να τον τροποποιήσουμε σε αυτή τη στιγμή. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Η κλάση Configurator, μετά τη [δημιουργία του container |application:bootstrapping#index.php], καλεί τον κώδικα αρχικοποίησης, ο οποίος δημιουργείται με εγγραφή στο αντικείμενο `$this->initialization` χρησιμοποιώντας τη [μέθοδο addBody() |php-generator:#Σώματα μεθόδων και συναρτήσεων]. - -Ας δείξουμε ένα παράδειγμα για το πώς, για παράδειγμα, με τον κώδικα αρχικοποίησης να ξεκινήσουμε τη session ή να εκκινήσουμε υπηρεσίες που έχουν το tag `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // αυτόματη εκκίνηση της session - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // οι υπηρεσίες με tag run πρέπει να δημιουργηθούν μετά την παρουσίαση του container - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/el/factory.texy b/dependency-injection/el/factory.texy deleted file mode 100644 index 54f04fde20..0000000000 --- a/dependency-injection/el/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Δημιουργημένα Factories -*********************** - -.[perex] -Το Nette DI μπορεί να δημιουργήσει αυτόματα κώδικα factory βάσει interfaces, εξοικονομώντας σας τη συγγραφή κώδικα. - -Ένα factory είναι μια κλάση που παράγει και διαμορφώνει αντικείμενα. Τους περνάει δηλαδή και τις εξαρτήσεις τους. Μην το συγχέετε με το σχεδιαστικό πρότυπο *factory method*, το οποίο περιγράφει έναν συγκεκριμένο τρόπο χρήσης των factories και δεν σχετίζεται με αυτό το θέμα. - -Πώς μοιάζει ένα τέτοιο factory, το δείξαμε στο [εισαγωγικό κεφάλαιο |introduction#Factory]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Το Nette DI μπορεί να δημιουργήσει αυτόματα τον κώδικα των factories. Το μόνο που έχετε να κάνετε είναι να δημιουργήσετε ένα interface και το Nette DI θα δημιουργήσει την υλοποίηση. Το interface πρέπει να έχει ακριβώς μία μέθοδο με το όνομα `create` και να δηλώνει τον τύπο επιστροφής: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Δηλαδή, το factory `ArticleFactory` έχει μια μέθοδο `create`, η οποία δημιουργεί αντικείμενα `Article`. Η κλάση `Article` μπορεί να μοιάζει κάπως έτσι: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Προσθέτουμε το factory στο αρχείο διαμόρφωσης: - -```neon -services: - - ArticleFactory -``` - -Το Nette DI θα δημιουργήσει την αντίστοιχη υλοποίηση του factory. - -Στον κώδικα που χρησιμοποιεί το factory, ζητάμε λοιπόν το αντικείμενο σύμφωνα με το interface και το Nette DI θα χρησιμοποιήσει τη δημιουργημένη υλοποίηση: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // αφήνουμε το factory να δημιουργήσει το αντικείμενο - $article = $this->articleFactory->create(); - } -} -``` - - -Παραμετροποιημένο factory -========================= - -Η μέθοδος του factory `create` μπορεί να δέχεται παραμέτρους, τις οποίες στη συνέχεια περνά στον κατασκευαστή. Ας συμπληρώσουμε, για παράδειγμα, την κλάση `Article` με το ID του συγγραφέα του άρθρου: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Προσθέτουμε την παράμετρο και στο factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Χάρη στο γεγονός ότι η παράμετρος στον κατασκευαστή και η παράμετρος στο factory ονομάζονται το ίδιο, το Nette DI τις περνά εντελώς αυτόματα. - - -Προηγμένος ορισμός -================== - -Ο ορισμός μπορεί να γραφτεί και σε πολυγραμμική μορφή χρησιμοποιώντας το κλειδί `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Κατά τη σύνταξη με αυτόν τον μακρύτερο τρόπο, είναι δυνατόν να αναφερθούν επιπλέον ορίσματα για τον κατασκευαστή στο κλειδί `arguments` και συμπληρωματική διαμόρφωση μέσω του `setup`, όπως και στις κανονικές υπηρεσίες. - -Παράδειγμα: εάν η μέθοδος `create()` δεν δεχόταν την παράμετρο `$authorId`, θα μπορούσαμε να δηλώσουμε μια σταθερή τιμή στη διαμόρφωση, η οποία θα περνούσε στον κατασκευαστή του `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Ή αντίστροφα, εάν η `create()` δεχόταν την παράμετρο `$authorId`, αλλά δεν ήταν μέρος του κατασκευαστή και περνούσε μέσω της μεθόδου `Article::setAuthorId()`, θα αναφερόμασταν σε αυτήν στην ενότητα `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Το Nette μπορεί, εκτός από factories, να δημιουργεί και τα λεγόμενα accessors. Πρόκειται για αντικείμενα με μια μέθοδο `get()`, η οποία επιστρέφει μια συγκεκριμένη υπηρεσία από το DI container. Η επανειλημμένη κλήση του `get()` επιστρέφει πάντα την ίδια παρουσία. - -Οι accessors παρέχουν lazy-loading στις εξαρτήσεις. Ας υποθέσουμε ότι έχουμε μια κλάση που καταγράφει σφάλματα σε μια ειδική βάση δεδομένων. Εάν αυτή η κλάση λάμβανε τη σύνδεση με τη βάση δεδομένων ως εξάρτηση μέσω του κατασκευαστή, η σύνδεση θα έπρεπε πάντα να δημιουργείται, παρόλο που στην πράξη ένα σφάλμα εμφανίζεται μόνο σπάνια και, επομένως, τις περισσότερες φορές η σύνδεση θα παρέμενε αχρησιμοποίητη. Αντί γι' αυτό, η κλάση περνά έναν accessor και μόνο όταν κληθεί το `get()` του, δημιουργείται το αντικείμενο της βάσης δεδομένων: - -Πώς να δημιουργήσετε έναν accessor; Αρκεί να γράψετε ένα interface και το Nette DI θα δημιουργήσει την υλοποίηση. Το interface πρέπει να έχει ακριβώς μία μέθοδο με το όνομα `get` και να δηλώνει τον τύπο επιστροφής: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Προσθέτουμε τον accessor στο αρχείο διαμόρφωσης, όπου ορίζεται επίσης η υπηρεσία που θα επιστρέφει: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Επειδή ο accessor επιστρέφει μια υπηρεσία τύπου `PDO` και στη διαμόρφωση υπάρχει μόνο μία τέτοια υπηρεσία, θα επιστρέφει ακριβώς αυτήν. Εάν υπήρχαν περισσότερες υπηρεσίες αυτού του τύπου, θα καθορίζαμε την επιστρεφόμενη υπηρεσία χρησιμοποιώντας το όνομα, π.χ. `- PDOAccessor(@db1)`. - - -Πολλαπλό factory/accessor -========================= -Τα factories και οι accessors μας μπορούσαν μέχρι τώρα πάντα να παράγουν ή να επιστρέφουν μόνο ένα αντικείμενο. Ωστόσο, είναι πολύ εύκολο να δημιουργηθούν και πολλαπλά factories συνδυασμένα με accessors. Το interface μιας τέτοιας κλάσης θα περιέχει οποιονδήποτε αριθμό μεθόδων με ονόματα `create<name>()` και `get<name>()`, π.χ.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Έτσι, αντί να περνάμε πολλά δημιουργημένα factories και accessors, περνάμε ένα πιο σύνθετο factory που μπορεί να κάνει περισσότερα. - -Εναλλακτικά, αντί για πολλές μεθόδους, μπορούμε να χρησιμοποιήσουμε το `get()` με παράμετρο: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Τότε ισχύει ότι το `MultiFactory::getArticle()` κάνει το ίδιο πράγμα με το `MultiFactoryAlt::get('article')`. Ωστόσο, η εναλλακτική σύνταξη έχει το μειονέκτημα ότι δεν είναι σαφές ποιες τιμές `$name` υποστηρίζονται και λογικά δεν είναι δυνατό στο interface να διακριθούν διαφορετικές τιμές επιστροφής για διαφορετικά `$name`. - - -Ορισμός με λίστα ----------------- -Με αυτόν τον τρόπο μπορεί να οριστεί ένα πολλαπλό factory στη διαμόρφωση: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # ορίζει το createArticle() - db: PDO(%dsn%, %user%, %password%) # ορίζει το getDb() - ) -``` - -Ή μπορούμε στον ορισμό του factory να αναφερθούμε σε υπάρχουσες υπηρεσίες μέσω αναφοράς: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # ορίζει το createArticle() - db: @\PDO # ορίζει το getDb() - ) -``` - - -Ορισμός με tags ---------------- - -Η δεύτερη επιλογή είναι να χρησιμοποιήσουμε για τον ορισμό [tags |services#Tags]: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/el/faq.texy b/dependency-injection/el/faq.texy deleted file mode 100644 index ba6c075467..0000000000 --- a/dependency-injection/el/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Συχνές ερωτήσεις για το DI (FAQ) -******************************** - - -Είναι το DI άλλο όνομα για το IoC; ----------------------------------- - -Το *Inversion of Control* (IoC) είναι μια αρχή που εστιάζει στον τρόπο εκτέλεσης του κώδικα - εάν ο κώδικάς σας εκτελεί ξένο κώδικα ή εάν ο κώδικάς σας ενσωματώνεται σε ξένο κώδικα, ο οποίος στη συνέχεια τον καλεί. Το IoC είναι ένας ευρύς όρος που περιλαμβάνει [γεγονότα |nette:glossary#Events], το λεγόμενο [Hollywood principle |application:components#Hollywood Style] και άλλες πτυχές. Μέρος αυτής της έννοιας είναι και τα factories, για τα οποία μιλά ο [Κανόνας #3: άφησέ το στο factory |introduction#Κανόνας αρ. 3: άφησέ το στο factory], και τα οποία αντιπροσωπεύουν μια αντιστροφή για τον τελεστή `new`. - -Το *Dependency Injection* (DI) εστιάζει στον τρόπο με τον οποίο ένα αντικείμενο μαθαίνει για ένα άλλο αντικείμενο, δηλαδή για τις εξαρτήσεις του. Πρόκειται για ένα σχεδιαστικό πρότυπο που απαιτεί τη ρητή μεταβίβαση εξαρτήσεων μεταξύ αντικειμένων. - -Μπορούμε λοιπόν να πούμε ότι το DI είναι μια συγκεκριμένη μορφή IoC. Ωστόσο, δεν είναι όλες οι μορφές IoC κατάλληλες από την άποψη της καθαρότητας του κώδικα. Για παράδειγμα, μεταξύ των αντι-προτύπων (antipatterns) περιλαμβάνονται τεχνικές που λειτουργούν με [καθολική κατάσταση |global-state] ή το λεγόμενο [Service Locator |#Τι είναι το Service Locator]. - - -Τι είναι το Service Locator; ----------------------------- - -Πρόκειται για μια εναλλακτική λύση στο Dependency Injection. Λειτουργεί δημιουργώντας ένα κεντρικό αποθετήριο όπου καταχωρούνται όλες οι διαθέσιμες υπηρεσίες ή εξαρτήσεις. Όταν ένα αντικείμενο χρειάζεται μια εξάρτηση, τη ζητά από το Service Locator. - -Σε σύγκριση με το Dependency Injection, ωστόσο, χάνει σε διαφάνεια: οι εξαρτήσεις δεν περνούν απευθείας στα αντικείμενα και δεν είναι τόσο εύκολα αναγνωρίσιμες, πράγμα που απαιτεί την εξέταση του κώδικα για να αποκαλυφθούν και να κατανοηθούν όλες οι συνδέσεις. Ο έλεγχος (testing) είναι επίσης πιο περίπλοκος, επειδή δεν μπορούμε απλώς να περάσουμε mock αντικείμενα στα υπό έλεγχο αντικείμενα, αλλά πρέπει να το κάνουμε μέσω του Service Locator. Επιπλέον, το Service Locator διαταράσσει τον σχεδιασμό του κώδικα, καθώς τα μεμονωμένα αντικείμενα πρέπει να γνωρίζουν την ύπαρξή του, πράγμα που διαφέρει από το Dependency Injection, όπου τα αντικείμενα δεν έχουν επίγνωση του DI container. - - -Πότε είναι καλύτερο να μην χρησιμοποιηθεί το DI; ------------------------------------------------- - -Δεν είναι γνωστές δυσκολίες που να σχετίζονται με τη χρήση του σχεδιαστικού προτύπου Dependency Injection. Αντίθετα, η λήψη εξαρτήσεων από καθολικά διαθέσιμα σημεία οδηγεί σε [μια ολόκληρη σειρά επιπλοκών |global-state], όπως και η χρήση του Service Locator. Επομένως, είναι σκόπιμο να χρησιμοποιείται πάντα το DI. Αυτό δεν είναι μια δογματική προσέγγιση, αλλά απλώς δεν έχει βρεθεί καλύτερη εναλλακτική λύση. - -Παρ' όλα αυτά, υπάρχουν ορισμένες καταστάσεις όπου δεν περνάμε τα αντικείμενα και τα λαμβάνουμε από τον καθολικό χώρο. Για παράδειγμα, κατά τον εντοπισμό σφαλμάτων στον κώδικα, όταν χρειάζεται να εκτυπώσετε την τιμή μιας μεταβλητής σε ένα συγκεκριμένο σημείο του προγράμματος, να μετρήσετε τη διάρκεια ενός συγκεκριμένου τμήματος του προγράμματος ή να καταγράψετε ένα μήνυμα. Σε τέτοιες περιπτώσεις, όπου πρόκειται για προσωρινές ενέργειες που θα αφαιρεθούν αργότερα από τον κώδικα, είναι θεμιτό να χρησιμοποιηθεί ένας καθολικά διαθέσιμος dumper, χρονόμετρο ή logger. Αυτά τα εργαλεία, δηλαδή, δεν ανήκουν στον σχεδιασμό του κώδικα. - - -Έχει η χρήση του DI τα μειονεκτήματά της; ------------------------------------------ - -Συνεπάγεται η χρήση του Dependency Injection κάποια μειονεκτήματα, όπως για παράδειγμα αυξημένη δυσκολία στη συγγραφή κώδικα ή χειρότερη απόδοση; Τι χάνουμε όταν αρχίζουμε να γράφουμε κώδικα σύμφωνα με το DI; - -Το DI δεν επηρεάζει την απόδοση ή τις απαιτήσεις μνήμης της εφαρμογής. Ορισμένο ρόλο μπορεί να παίξει η απόδοση του DI Container, ωστόσο στην περίπτωση του [Nette DI |nette-container], το container μεταγλωττίζεται σε καθαρή PHP, οπότε η επιβάρυνσή του κατά την εκτέλεση της εφαρμογής είναι ουσιαστικά μηδενική. - -Κατά τη συγγραφή κώδικα, συχνά είναι απαραίτητο να δημιουργηθούν κατασκευαστές που δέχονται εξαρτήσεις. Παλαιότερα αυτό μπορούσε να είναι χρονοβόρο, ωστόσο χάρη στα σύγχρονα IDE και το [constructor property promotion |https://blog.nette.org/el/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], είναι πλέον θέμα δευτερολέπτων. Τα factories μπορούν εύκολα να δημιουργηθούν με το Nette DI και το plugin για το PhpStorm με ένα κλικ του ποντικιού. Από την άλλη πλευρά, εξαλείφεται η ανάγκη συγγραφής singletons και στατικών σημείων πρόσβασης. - -Μπορούμε να συμπεράνουμε ότι μια σωστά σχεδιασμένη εφαρμογή που χρησιμοποιεί DI δεν είναι ούτε συντομότερη ούτε μακρύτερη σε σύγκριση με μια εφαρμογή που χρησιμοποιεί singletons. Τα τμήματα του κώδικα που εργάζονται με εξαρτήσεις απλώς αφαιρούνται από τις μεμονωμένες κλάσεις και μεταφέρονται σε νέα σημεία, δηλαδή στο DI container και στα factories. - - -Πώς να ξαναγράψετε μια legacy εφαρμογή σε DI; ---------------------------------------------- - -Η μετάβαση από μια legacy εφαρμογή στο Dependency Injection μπορεί να είναι μια απαιτητική διαδικασία, ειδικά σε μεγάλες και πολύπλοκες εφαρμογές. Είναι σημαντικό να προσεγγίσετε αυτή τη διαδικασία συστηματικά. - -- Κατά τη μετάβαση στο Dependency Injection, είναι σημαντικό όλα τα μέλη της ομάδας να κατανοούν τις αρχές και τις διαδικασίες που χρησιμοποιούνται. -- Πρώτα, πραγματοποιήστε μια ανάλυση της υπάρχουσας εφαρμογής και εντοπίστε τα βασικά στοιχεία και τις εξαρτήσεις τους. Δημιουργήστε ένα σχέδιο για το ποια τμήματα θα αναδιαρθρωθούν και με ποια σειρά. -- Υλοποιήστε ένα DI container ή, ακόμα καλύτερα, χρησιμοποιήστε μια υπάρχουσα βιβλιοθήκη, για παράδειγμα το Nette DI. -- Σταδιακά αναδιαρθρώστε τα μεμονωμένα τμήματα της εφαρμογής ώστε να χρησιμοποιούν το Dependency Injection. Αυτό μπορεί να περιλαμβάνει τροποποιήσεις των κατασκευαστών ή των μεθόδων ώστε να δέχονται εξαρτήσεις ως παραμέτρους. -- Τροποποιήστε τα σημεία στον κώδικα όπου δημιουργούνται αντικείμενα με εξαρτήσεις, ώστε αντί γι' αυτό οι εξαρτήσεις να εισάγονται από το container. Αυτό μπορεί να περιλαμβάνει τη χρήση factories. - -Θυμηθείτε ότι η μετάβαση στο Dependency Injection είναι μια επένδυση στην ποιότητα του κώδικα και τη μακροπρόθεσμη συντηρησιμότητα της εφαρμογής. Αν και μπορεί να είναι δύσκολο να πραγματοποιηθούν αυτές οι αλλαγές, το αποτέλεσμα θα πρέπει να είναι ένας καθαρότερος, πιο αρθρωτός και εύκολα ελεγχόμενος κώδικας, ο οποίος είναι έτοιμος για μελλοντική επέκταση και συντήρηση. - - -Γιατί προτιμάται η σύνθεση (composition) έναντι της κληρονομικότητας; ---------------------------------------------------------------------- -Είναι προτιμότερο να χρησιμοποιείται η [σύνθεση |nette:introduction-to-object-oriented-programming#Σύνθεση] αντί της [κληρονομικότητας |nette:introduction-to-object-oriented-programming#Κληρονομικότητα], επειδή χρησιμεύει στην επαναχρησιμοποίηση του κώδικα, χωρίς να χρειάζεται να ανησυχούμε για τις συνέπειες των αλλαγών. Παρέχει δηλαδή μια πιο χαλαρή σύνδεση, όπου δεν χρειάζεται να φοβόμαστε ότι η αλλαγή κάποιου κώδικα θα προκαλέσει την ανάγκη αλλαγής άλλου εξαρτώμενου κώδικα. Τυπικό παράδειγμα είναι η κατάσταση που ονομάζεται [constructor hell |passing-dependencies#Constructor hell]. - - -Μπορεί να χρησιμοποιηθεί το Nette DI Container εκτός του Nette; ---------------------------------------------------------------- - -Σίγουρα. Το Nette DI Container είναι μέρος του Nette, αλλά έχει σχεδιαστεί ως μια αυτόνομη βιβλιοθήκη που μπορεί να χρησιμοποιηθεί ανεξάρτητα από τα υπόλοιπα μέρη του framework. Αρκεί να την εγκαταστήσετε μέσω του Composer, να δημιουργήσετε ένα αρχείο διαμόρφωσης με τον ορισμό των υπηρεσιών σας και στη συνέχεια, με λίγες γραμμές κώδικα PHP, να δημιουργήσετε το DI container. Και αμέσως μπορείτε να αρχίσετε να επωφελείστε από το Dependency Injection στα έργα σας. - -Πώς μοιάζει η συγκεκριμένη χρήση, συμπεριλαμβανομένων των κωδίκων, περιγράφεται στο κεφάλαιο [Nette DI Container |nette-container]. - - -Γιατί η διαμόρφωση είναι σε αρχεία NEON; ----------------------------------------- - -Το NEON είναι μια απλή και ευανάγνωστη γλώσσα διαμόρφωσης, η οποία αναπτύχθηκε στο πλαίσιο του Nette για τη ρύθμιση εφαρμογών, υπηρεσιών και των εξαρτήσεών τους. Σε σύγκριση με το JSON ή το YAML, προσφέρει για τον σκοπό αυτό πολύ πιο διαισθητικές και ευέλικτες δυνατότητες. Στο NEON μπορούν να περιγραφούν φυσικά συνδέσεις, οι οποίες στο Symfony & YAMLu δεν θα ήταν δυνατόν να γραφτούν είτε καθόλου, είτε μόνο μέσω πολύπλοκης περιγραφής. - - -Δεν επιβραδύνει την εφαρμογή η ανάλυση (parsing) των αρχείων NEON; ------------------------------------------------------------------- - -Παρόλο που τα αρχεία NEON αναλύονται πολύ γρήγορα, αυτή η πτυχή δεν έχει καμία σημασία. Ο λόγος είναι ότι η ανάλυση των αρχείων πραγματοποιείται μόνο μία φορά κατά την πρώτη εκκίνηση της εφαρμογής. Στη συνέχεια, δημιουργείται ο κώδικας του DI container, αποθηκεύεται στον δίσκο και εκτελείται σε κάθε επόμενο αίτημα, χωρίς να είναι απαραίτητη η περαιτέρω ανάλυση. - -Έτσι λειτουργεί στο περιβάλλον παραγωγής. Κατά την ανάπτυξη, τα αρχεία NEON αναλύονται κάθε φορά που αλλάζει το περιεχόμενό τους, ώστε ο προγραμματιστής να έχει πάντα τον τρέχοντα DI container. Η ίδια η ανάλυση είναι, όπως ειπώθηκε, θέμα στιγμής. - - -Πώς μπορώ να αποκτήσω πρόσβαση στις παραμέτρους στο αρχείο διαμόρφωσης από την κλάση μου; ------------------------------------------------------------------------------------------ - -Ας θυμηθούμε τον [Κανόνα #1: άφησέ το να σου περαστεί |introduction#Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]. Εάν η κλάση απαιτεί πληροφορίες από το αρχείο διαμόρφωσης, δεν χρειάζεται να σκεφτούμε πώς να φτάσουμε σε αυτές τις πληροφορίες, αντίθετα απλώς τις ζητάμε - για παράδειγμα, μέσω του κατασκευαστή της κλάσης. Και πραγματοποιούμε τη μεταβίβαση στο αρχείο διαμόρφωσης. - -Σε αυτό το παράδειγμα, το `%myParameter%` είναι ένα placeholder για την τιμή της παραμέτρου `myParameter`, η οποία περνά στον κατασκευαστή της κλάσης `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Για να περάσετε πολλαπλές παραμέτρους ή να χρησιμοποιήσετε autowiring, είναι σκόπιμο να [ενσωματώσετε τις παραμέτρους σε ένα αντικείμενο |best-practices:passing-settings-to-presenters]. - - -Υποστηρίζει το Nette το PSR-11: Container interface; ----------------------------------------------------- - -Το Nette DI Container δεν υποστηρίζει απευθείας το PSR-11. Ωστόσο, εάν χρειάζεστε διαλειτουργικότητα μεταξύ του Nette DI Container και βιβλιοθηκών ή frameworks που αναμένουν το PSR-11 Container Interface, μπορείτε να δημιουργήσετε έναν [απλό προσαρμογέα |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], ο οποίος θα χρησιμεύσει ως γέφυρα μεταξύ του Nette DI Container και του PSR-11. diff --git a/dependency-injection/el/global-state.texy b/dependency-injection/el/global-state.texy deleted file mode 100644 index 5207b38638..0000000000 --- a/dependency-injection/el/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Καθολική κατάσταση και singletons -********************************* - -.[perex] -Προειδοποίηση: Οι ακόλουθες κατασκευές είναι σημάδι κακώς σχεδιασμένου κώδικα: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ή `static::$var` - -Εμφανίζονται κάποιες από αυτές τις κατασκευές στον κώδικά σας; Τότε έχετε την ευκαιρία να τον βελτιώσετε. Ίσως σκέφτεστε ότι πρόκειται για συνήθεις κατασκευές, τις οποίες βλέπετε ίσως και σε παραδείγματα λύσεων διαφόρων βιβλιοθηκών και frameworks. Αν ισχύει αυτό, τότε ο σχεδιασμός του κώδικά τους δεν είναι καλός. - -Τώρα σίγουρα δεν μιλάμε για κάποια ακαδημαϊκή καθαρότητα. Όλες αυτές οι κατασκευές έχουν ένα κοινό: χρησιμοποιούν καθολική κατάσταση. Και αυτή έχει καταστροφική επίδραση στην ποιότητα του κώδικα. Οι κλάσεις λένε ψέματα για τις εξαρτήσεις τους. Ο κώδικας γίνεται απρόβλεπτος. Μπερδεύει τους προγραμματιστές και μειώνει την αποδοτικότητά τους. - -Σε αυτό το κεφάλαιο θα εξηγήσουμε γιατί συμβαίνει αυτό και πώς να αποφύγετε την καθολική κατάσταση. - - -Καθολική σύζευξη ----------------- - -Σε έναν ιδανικό κόσμο, ένα αντικείμενο θα έπρεπε να μπορεί να επικοινωνεί μόνο με αντικείμενα που του έχουν [περαστεί απευθείας |passing-dependencies]. Εάν δημιουργήσω δύο αντικείμενα `A` και `B` και ποτέ δεν περάσω αναφορά μεταξύ τους, τότε ούτε το `A`, ούτε το `B`, μπορούν να φτάσουν στο άλλο αντικείμενο ή να αλλάξουν την κατάστασή του. Αυτό είναι ένα πολύ επιθυμητό χαρακτηριστικό του κώδικα. Είναι παρόμοιο με το να έχετε μια μπαταρία και μια λάμπα. η λάμπα δεν θα ανάψει αν δεν τη συνδέσετε με την μπαταρία με ένα καλώδιο. - -Αυτό όμως δεν ισχύει για τις καθολικές (στατικές) μεταβλητές ή τα singletons. Το αντικείμενο `A` θα μπορούσε *ασύρματα* να φτάσει στο αντικείμενο `C` και να το τροποποιήσει χωρίς καμία μεταβίβαση αναφοράς, καλώντας το `C::changeSomething()`. Εάν το αντικείμενο `B` αρπάξει επίσης το καθολικό `C`, τότε τα `A` και `B` μπορούν να αλληλεπιδράσουν μέσω του `C`. - -Η χρήση καθολικών μεταβλητών εισάγει στο σύστημα μια νέα μορφή *ασύρματης* σύζευξης, η οποία δεν είναι ορατή από έξω. Δημιουργεί ένα παραπέτασμα καπνού που περιπλέκει την κατανόηση και τη χρήση του κώδικα. Για να κατανοήσουν πραγματικά οι προγραμματιστές τις εξαρτήσεις, πρέπει να διαβάσουν κάθε γραμμή του πηγαίου κώδικα. Αντί απλώς να εξοικειωθούν με τα interfaces των κλάσεων. Επιπλέον, πρόκειται για μια εντελώς περιττή σύζευξη. Η καθολική κατάσταση χρησιμοποιείται επειδή είναι εύκολα προσβάσιμη από οπουδήποτε και επιτρέπει, για παράδειγμα, την εγγραφή στη βάση δεδομένων μέσω της καθολικής (στατικής) μεθόδου `DB::insert()`. Αλλά όπως θα δείξουμε, το πλεονέκτημα που προσφέρει είναι ασήμαντο, ενώ αντίθετα οι επιπλοκές που προκαλεί είναι μοιραίες. - -.[note] -Από την άποψη της συμπεριφοράς, δεν υπάρχει διαφορά μεταξύ καθολικής και στατικής μεταβλητής. Είναι εξίσου επιβλαβείς. - - -Απόκοσμη δράση από απόσταση ---------------------------- - -"Απόκοσμη δράση από απόσταση" (Spooky action at a distance) - έτσι ονόμασε περίφημα το 1935 ο Άλμπερτ Αϊνστάιν ένα φαινόμενο στην κβαντική φυσική που του προκαλούσε ανατριχίλα. -Πρόκειται για την κβαντική διεμπλοκή, της οποίας η ιδιαιτερότητα είναι ότι όταν μετράτε πληροφορίες για ένα σωματίδιο, επηρεάζετε αμέσως το άλλο σωματίδιο, ακόμα κι αν απέχουν εκατομμύρια έτη φωτός. Αυτό φαινομενικά παραβιάζει τον θεμελιώδη νόμο του σύμπαντος ότι τίποτα δεν μπορεί να ταξιδέψει γρηγορότερα από το φως. - -Στον κόσμο του λογισμικού, μπορούμε να ονομάσουμε "απόκοσμη δράση από απόσταση" μια κατάσταση όπου εκκινούμε μια διαδικασία, την οποία θεωρούμε απομονωμένη (επειδή δεν της περάσαμε καμία αναφορά), αλλά σε απομακρυσμένα σημεία του συστήματος συμβαίνουν απροσδόκητες αλληλεπιδράσεις και αλλαγές κατάστασης, για τις οποίες δεν είχαμε ιδέα. Αυτό μπορεί να συμβεί μόνο μέσω της καθολικής κατάστασης. - -Φανταστείτε ότι εντάσσεστε σε μια ομάδα προγραμματιστών ενός έργου που έχει μια εκτεταμένη, ώριμη βάση κώδικα. Ο νέος σας προϊστάμενος σας ζητά να υλοποιήσετε μια νέα λειτουργία και εσείς, ως σωστός προγραμματιστής, ξεκινάτε γράφοντας ένα τεστ. Επειδή όμως είστε νέοι στο έργο, κάνετε πολλά διερευνητικά τεστ του τύπου "τι θα συμβεί αν καλέσω αυτή τη μέθοδο". Και δοκιμάζετε να γράψετε το ακόλουθο τεστ: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // ο αριθμός της κάρτας σας - $cc->charge(100); -} -``` - -Εκτελείτε τον κώδικα, ίσως αρκετές φορές, και μετά από λίγο παρατηρείτε ειδοποιήσεις στο κινητό σας από την τράπεζα ότι κάθε φορά που εκτελείται, χρεώνονται 100 δολάρια από την πιστωτική σας κάρτα 🤦‍♂️ - -Πώς στο καλό μπόρεσε το τεστ να προκαλέσει πραγματική χρέωση χρημάτων; Η λειτουργία με πιστωτική κάρτα δεν είναι εύκολη. Πρέπει να επικοινωνήσετε με μια web υπηρεσία τρίτου μέρους, πρέπει να γνωρίζετε τη διεύθυνση URL αυτής της web υπηρεσίας, πρέπει να συνδεθείτε και ούτω καθεξής. Καμία από αυτές τις πληροφορίες δεν περιέχεται στο τεστ. Ακόμα χειρότερα, ούτε καν γνωρίζετε πού βρίσκονται αυτές οι πληροφορίες, και επομένως ούτε πώς να κάνετε mock τις εξωτερικές εξαρτήσεις, ώστε κάθε εκτέλεση να μην οδηγεί ξανά σε χρέωση 100 δολαρίων. Και πώς έπρεπε να γνωρίζετε, ως νέος προγραμματιστής, ότι αυτό που ετοιμαζόσασταν να κάνετε θα οδηγούσε στο να γίνετε 100 δολάρια φτωχότεροι; - -Αυτή είναι η απόκοσμη δράση από απόσταση! - -Δεν σας μένει παρά να ψάξετε για πολλή ώρα σε πολλούς πηγαίους κώδικες, να ρωτήσετε παλαιότερους και πιο έμπειρους συναδέλφους, μέχρι να καταλάβετε πώς λειτουργούν οι συνδέσεις στο έργο. Αυτό οφείλεται στο ότι, κοιτάζοντας το interface της κλάσης `CreditCard`, δεν μπορείτε να προσδιορίσετε την καθολική κατάσταση που πρέπει να αρχικοποιηθεί. Ακόμη και η ματιά στον πηγαίο κώδικα της κλάσης δεν σας αποκαλύπτει ποια μέθοδο αρχικοποίησης πρέπει να καλέσετε. Στην καλύτερη περίπτωση, μπορείτε να βρείτε μια καθολική μεταβλητή στην οποία γίνεται πρόσβαση και από αυτήν να προσπαθήσετε να μαντέψετε πώς να την αρχικοποιήσετε. - -Οι κλάσεις σε ένα τέτοιο έργο είναι παθολογικοί ψεύτες. Η πιστωτική κάρτα προσποιείται ότι αρκεί να την παρουσιάσετε και να καλέσετε τη μέθοδο `charge()`. Κρυφά, όμως, συνεργάζεται με μια άλλη κλάση `PaymentGateway`, η οποία αντιπροσωπεύει την πύλη πληρωμών. Ακόμη και το interface της λέει ότι μπορεί να αρχικοποιηθεί ξεχωριστά, αλλά στην πραγματικότητα αντλεί διαπιστευτήρια από κάποιο αρχείο διαμόρφωσης και ούτω καθεξής. Για τους προγραμματιστές που έγραψαν αυτόν τον κώδικα, είναι σαφές ότι η `CreditCard` χρειάζεται την `PaymentGateway`. Έγραψαν τον κώδικα με αυτόν τον τρόπο. Αλλά για οποιονδήποτε είναι νέος στο έργο, είναι ένα απόλυτο μυστήριο και εμποδίζει τη μάθηση. - -Πώς να διορθώσετε την κατάσταση; Εύκολα. **Αφήστε το API να δηλώσει τις εξαρτήσεις.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Παρατηρήστε πώς οι συνδέσεις μέσα στον κώδικα γίνονται ξαφνικά προφανείς. Με το γεγονός ότι η μέθοδος `charge()` δηλώνει ότι χρειάζεται την `PaymentGateway`, δεν χρειάζεται να ρωτήσετε κανέναν πώς συνδέεται ο κώδικας. Γνωρίζετε ότι πρέπει να δημιουργήσετε την παρουσία της, και όταν προσπαθήσετε να το κάνετε, θα διαπιστώσετε ότι πρέπει να δώσετε παραμέτρους πρόσβασης. Χωρίς αυτές, ο κώδικας δεν θα μπορούσε καν να εκτελεστεί. - -Και κυρίως, τώρα μπορείτε να κάνετε mock την πύλη πληρωμών, ώστε να μην χρεώνεστε 100 δολάρια κάθε φορά που εκτελείτε το τεστ. - -Η καθολική κατάσταση κάνει τα αντικείμενά σας να μπορούν κρυφά να έχουν πρόσβαση σε πράγματα που δεν δηλώνονται στα API τους, και ως αποτέλεσμα, μετατρέπει τα API σας σε παθολογικούς ψεύτες. - -Ίσως να μην το είχατε σκεφτεί έτσι προηγουμένως, αλλά κάθε φορά που χρησιμοποιείτε καθολική κατάσταση, δημιουργείτε μυστικούς ασύρματους διαύλους επικοινωνίας. Η απόκοσμη δράση από απόσταση αναγκάζει τους προγραμματιστές να διαβάζουν κάθε γραμμή κώδικα για να κατανοήσουν τις πιθανές αλληλεπιδράσεις, μειώνει την παραγωγικότητα των προγραμματιστών και μπερδεύει τα νέα μέλη της ομάδας. Εάν είστε εσείς αυτός που δημιούργησε τον κώδικα, γνωρίζετε τις πραγματικές εξαρτήσεις, αλλά οποιοσδήποτε έρθει μετά από εσάς είναι αβοήθητος. - -Μην γράφετε κώδικα που χρησιμοποιεί καθολική κατάσταση, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection. - - -Ευθραυστότητα της καθολικής κατάστασης --------------------------------------- - -Στον κώδικα που χρησιμοποιεί καθολική κατάσταση και singletons, δεν είναι ποτέ σίγουρο πότε και ποιος άλλαξε αυτή την κατάσταση. Αυτός ο κίνδυνος εμφανίζεται ήδη κατά την αρχικοποίηση. Ο ακόλουθος κώδικας υποτίθεται ότι δημιουργεί μια σύνδεση βάσης δεδομένων και αρχικοποιεί την πύλη πληρωμών, ωστόσο προκαλεί συνεχώς εξαίρεση και η εύρεση της αιτίας είναι εξαιρετικά χρονοβόρα: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Πρέπει να εξετάσετε λεπτομερώς τον κώδικα για να διαπιστώσετε ότι το αντικείμενο `PaymentGateway` έχει ασύρματη πρόσβαση σε άλλα αντικείμενα, ορισμένα από τα οποία απαιτούν σύνδεση βάσης δεδομένων. Δηλαδή, είναι απαραίτητο να αρχικοποιήσετε τη βάση δεδομένων πριν από την `PaymentGateway`. Ωστόσο, το παραπέτασμα καπνού της καθολικής κατάστασης το κρύβει αυτό από εσάς. Πόσο χρόνο θα είχατε εξοικονομήσει εάν τα API των μεμονωμένων κλάσεων δεν εξαπατούσαν και δήλωναν τις εξαρτήσεις τους; - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Ένα παρόμοιο πρόβλημα εμφανίζεται και κατά τη χρήση καθολικής πρόσβασης στη σύνδεση της βάσης δεδομένων: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Κατά την κλήση της μεθόδου `save()`, δεν είναι βέβαιο εάν έχει ήδη δημιουργηθεί η σύνδεση με τη βάση δεδομένων και ποιος φέρει την ευθύνη για τη δημιουργία της. Εάν θέλουμε, για παράδειγμα, να αλλάξουμε τη σύνδεση της βάσης δεδομένων κατά την εκτέλεση, ίσως για λόγους δοκιμών, θα έπρεπε πιθανότατα να δημιουργήσουμε επιπλέον μεθόδους όπως `DB::reconnect(...)` ή `DB::reconnectForTest()`. - -Ας εξετάσουμε ένα παράδειγμα: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Πού έχουμε τη βεβαιότητα ότι κατά την κλήση του `$article->save()` χρησιμοποιείται όντως η δοκιμαστική βάση δεδομένων; Τι γίνεται αν η μέθοδος `Foo::doSomething()` άλλαξε την καθολική σύνδεση της βάσης δεδομένων; Για να το διαπιστώσουμε, θα έπρεπε να εξετάσουμε τον πηγαίο κώδικα της κλάσης `Foo` και πιθανώς και πολλών άλλων κλάσεων. Αυτή η προσέγγιση, ωστόσο, θα έδινε μόνο μια βραχυπρόθεσμη απάντηση, καθώς η κατάσταση μπορεί να αλλάξει στο μέλλον. - -Και τι γίνεται αν μεταφέρουμε τη σύνδεση με τη βάση δεδομένων σε μια στατική μεταβλητή μέσα στην κλάση `Article`; - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Αυτό δεν άλλαξε απολύτως τίποτα. Το πρόβλημα είναι η καθολική κατάσταση και είναι εντελώς αδιάφορο σε ποια κλάση κρύβεται. Σε αυτή την περίπτωση, όπως και στην προηγούμενη, δεν έχουμε καμία ένδειξη κατά την κλήση της μεθόδου `$article->save()` για το σε ποια βάση δεδομένων θα γίνει η εγγραφή. Οποιοσδήποτε στο άλλο άκρο της εφαρμογής θα μπορούσε ανά πάσα στιγμή να αλλάξει τη βάση δεδομένων χρησιμοποιώντας το `Article::setDb()`. Κάτω από τα χέρια μας. - -Η καθολική κατάσταση καθιστά την εφαρμογή μας **εξαιρετικά εύθραυστη**. - -Υπάρχει όμως ένας απλός τρόπος για να αντιμετωπίσουμε αυτό το πρόβλημα. Αρκεί να αφήσουμε το API να δηλώσει τις εξαρτήσεις, εξασφαλίζοντας έτσι τη σωστή λειτουργικότητα. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Χάρη σε αυτή την προσέγγιση, εξαλείφεται η ανησυχία για κρυφές και απροσδόκητες αλλαγές στη σύνδεση της βάσης δεδομένων. Τώρα έχουμε τη βεβαιότητα για το πού αποθηκεύεται το άρθρο και καμία τροποποίηση του κώδικα μέσα σε μια άλλη άσχετη κλάση δεν μπορεί πλέον να αλλάξει την κατάσταση. Ο κώδικας δεν είναι πλέον εύθραυστος, αλλά σταθερός. - -Μην γράφετε κώδικα που χρησιμοποιεί καθολική κατάσταση, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection. - - -Singleton ---------- - -Το Singleton είναι ένα σχεδιαστικό πρότυπο που, σύμφωνα με τον "ορισμό":https://en.wikipedia.org/wiki/Singleton_pattern από τη γνωστή δημοσίευση Gang of Four, περιορίζει μια κλάση σε μία μόνο παρουσία και προσφέρει καθολική πρόσβαση σε αυτήν. Η υλοποίηση αυτού του προτύπου συνήθως μοιάζει με τον ακόλουθο κώδικα: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // και άλλες μέθοδοι που εκτελούν τις λειτουργίες της συγκεκριμένης κλάσης -} -``` - -Δυστυχώς, το singleton εισάγει καθολική κατάσταση στην εφαρμογή. Και όπως δείξαμε παραπάνω, η καθολική κατάσταση είναι ανεπιθύμητη. Επομένως, το singleton θεωρείται αντι-πρότυπο (antipattern). - -Μην χρησιμοποιείτε singletons στον κώδικά σας και αντικαταστήστε τα με άλλους μηχανισμούς. Τα singletons πραγματικά δεν τα χρειάζεστε. Εάν, ωστόσο, χρειάζεται να εγγυηθείτε την ύπαρξη μιας μόνο παρουσίας της κλάσης για ολόκληρη την εφαρμογή, αφήστε το στον [DI container |container]. Δημιουργήστε έτσι ένα application singleton, δηλαδή μια υπηρεσία. Με αυτόν τον τρόπο, η κλάση παύει να ασχολείται με τη διασφάλιση της μοναδικότητάς της (δηλ. δεν θα έχει μέθοδο `getInstance()` και στατική μεταβλητή) και θα εκτελεί μόνο τις λειτουργίες της. Έτσι, παύει να παραβιάζει την αρχή της μοναδικής ευθύνης (single responsibility principle). - - -Καθολική κατάσταση έναντι δοκιμών ---------------------------------- - -Κατά τη συγγραφή δοκιμών, υποθέτουμε ότι κάθε δοκιμή είναι μια απομονωμένη μονάδα και ότι καμία εξωτερική κατάσταση δεν εισέρχεται σε αυτήν. Και καμία κατάσταση δεν εξέρχεται από τις δοκιμές. Μετά την ολοκλήρωση της δοκιμής, όλη η σχετική κατάσταση με τη δοκιμή θα πρέπει να αφαιρεθεί αυτόματα από τον garbage collector. Χάρη σε αυτό, οι δοκιμές είναι απομονωμένες. Επομένως, μπορούμε να εκτελέσουμε τις δοκιμές με οποιαδήποτε σειρά. - -Εάν, ωστόσο, υπάρχουν καθολικές καταστάσεις/singletons, όλες αυτές οι ευχάριστες υποθέσεις καταρρέουν. Η κατάσταση μπορεί να εισέλθει και να εξέλθει από τη δοκιμή. Ξαφνικά, η σειρά των δοκιμών μπορεί να έχει σημασία. - -Για να μπορέσουμε καν να δοκιμάσουμε τα singletons, οι προγραμματιστές συχνά πρέπει να χαλαρώσουν τις ιδιότητές τους, για παράδειγμα επιτρέποντας την αντικατάσταση της παρουσίας με μια άλλη. Τέτοιες λύσεις είναι στην καλύτερη περίπτωση ένα hack, που δημιουργεί κώδικα δύσκολο στη συντήρηση και την κατανόηση. Κάθε δοκιμή ή μέθοδος `tearDown()`, που επηρεάζει οποιαδήποτε καθολική κατάσταση, πρέπει να αναιρέσει αυτές τις αλλαγές. - -Η καθολική κατάσταση είναι ο μεγαλύτερος πονοκέφαλος στις δοκιμές μονάδας (unit testing)! - -Πώς να διορθώσετε την κατάσταση; Εύκολα. Μην γράφετε κώδικα που χρησιμοποιεί singletons, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection. - - -Καθολικές σταθερές ------------------- - -Η καθολική κατάσταση δεν περιορίζεται μόνο στη χρήση singletons και στατικών μεταβλητών, αλλά μπορεί να αφορά και τις καθολικές σταθερές. - -Οι σταθερές, η τιμή των οποίων δεν μας προσφέρει καμία νέα (`M_PI`) ή χρήσιμη (`PREG_BACKTRACK_LIMIT_ERROR`) πληροφορία, είναι σαφώς εντάξει. Αντίθετα, οι σταθερές που χρησιμεύουν ως τρόπος για να περάσουμε *ασύρματα* πληροφορίες μέσα στον κώδικα, δεν είναι τίποτα άλλο από κρυφές εξαρτήσεις. Όπως για παράδειγμα το `LOG_FILE` στο ακόλουθο παράδειγμα. Η χρήση της σταθεράς `FILE_APPEND` είναι απολύτως σωστή. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Σε αυτή την περίπτωση, θα έπρεπε να δηλώσουμε μια παράμετρο στον κατασκευαστή της κλάσης `Foo`, ώστε να γίνει μέρος του API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Τώρα μπορούμε να περάσουμε την πληροφορία για τη διαδρομή του αρχείου καταγραφής και να την αλλάξουμε εύκολα ανάλογα με τις ανάγκες, πράγμα που διευκολύνει τις δοκιμές και τη συντήρηση του κώδικα. - - -Καθολικές συναρτήσεις και στατικές μέθοδοι ------------------------------------------- - -Θέλουμε να τονίσουμε ότι η ίδια η χρήση στατικών μεθόδων και καθολικών συναρτήσεων δεν είναι προβληματική. Εξηγήσαμε σε τι συνίσταται η ακαταλληλότητα της χρήσης του `DB::insert()` και παρόμοιων μεθόδων, αλλά πάντα αφορούσε μόνο την καθολική κατάσταση, η οποία είναι αποθηκευμένη σε κάποια στατική μεταβλητή. Η μέθοδος `DB::insert()` απαιτεί την ύπαρξη στατικής μεταβλητής, επειδή σε αυτήν είναι αποθηκευμένη η σύνδεση με τη βάση δεδομένων. Χωρίς αυτή τη μεταβλητή, θα ήταν αδύνατο να υλοποιηθεί η μέθοδος. - -Η χρήση ντετερμινιστικών στατικών μεθόδων και συναρτήσεων, όπως `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` και πολλών άλλων, είναι απολύτως σύμφωνη με το dependency injection. Αυτές οι συναρτήσεις επιστρέφουν πάντα τα ίδια αποτελέσματα για τις ίδιες παραμέτρους εισόδου και είναι επομένως προβλέψιμες. Δεν χρησιμοποιούν καμία καθολική κατάσταση. - -Υπάρχουν, ωστόσο, και συναρτήσεις στην PHP που δεν είναι ντετερμινιστικές. Σε αυτές ανήκει, για παράδειγμα, η συνάρτηση `htmlspecialchars()`. Η τρίτη της παράμετρος `$encoding`, εάν δεν αναφέρεται, έχει ως προεπιλεγμένη τιμή την τιμή της επιλογής διαμόρφωσης `ini_get('default_charset')`. Επομένως, συνιστάται να αναφέρεται πάντα αυτή η παράμετρος και να αποφεύγεται έτσι η πιθανή απρόβλεπτη συμπεριφορά της συνάρτησης. Το Nette το κάνει αυτό με συνέπεια. - -Ορισμένες συναρτήσεις, όπως `strtolower()`, `strtoupper()` και παρόμοιες, στο πρόσφατο παρελθόν συμπεριφέρονταν μη ντετερμινιστικά και εξαρτώνταν από τη ρύθμιση `setlocale()`. Αυτό προκαλούσε πολλές επιπλοκές, συχνότερα κατά την εργασία με την τουρκική γλώσσα. Αυτή, δηλαδή, διακρίνει το πεζό και το κεφαλαίο γράμμα `I` με και χωρίς τελεία. Έτσι, το `strtolower('I')` επέστρεφε τον χαρακτήρα `ı` και το `strtoupper('i')` τον χαρακτήρα `İ`, πράγμα που οδηγούσε στο να αρχίσουν οι εφαρμογές να προκαλούν μια σειρά από μυστηριώδη σφάλματα. Αυτό το πρόβλημα, ωστόσο, διορθώθηκε στην έκδοση PHP 8.2 και οι συναρτήσεις δεν εξαρτώνται πλέον από το locale. - -Πρόκειται για ένα ωραίο παράδειγμα του πώς η καθολική κατάσταση ταλαιπώρησε χιλιάδες προγραμματιστές σε όλο τον κόσμο. Η λύση ήταν η αντικατάστασή της με dependency injection. - - -Πότε είναι δυνατόν να χρησιμοποιηθεί η καθολική κατάσταση? ----------------------------------------------------------- - -Υπάρχουν ορισμένες συγκεκριμένες καταστάσεις όπου είναι δυνατόν να χρησιμοποιηθεί η καθολική κατάσταση. Για παράδειγμα, κατά τον εντοπισμό σφαλμάτων στον κώδικα, όταν χρειάζεται να εκτυπώσετε την τιμή μιας μεταβλητής ή να μετρήσετε τη διάρκεια ενός συγκεκριμένου τμήματος του προγράμματος. Σε τέτοιες περιπτώσεις, που αφορούν προσωρινές ενέργειες οι οποίες θα αφαιρεθούν αργότερα από τον κώδικα, είναι δυνατόν να χρησιμοποιηθεί θεμιτά ένας καθολικά διαθέσιμος dumper ή χρονόμετρο. Αυτά τα εργαλεία, δηλαδή, δεν αποτελούν μέρος του σχεδιασμού του κώδικα. - -Ένα άλλο παράδειγμα είναι οι συναρτήσεις για την εργασία με κανονικές εκφράσεις `preg_*`, οι οποίες εσωτερικά αποθηκεύουν τις μεταγλωττισμένες κανονικές εκφράσεις σε μια στατική cache στη μνήμη. Όταν λοιπόν καλείτε την ίδια κανονική έκφραση πολλές φορές σε διαφορετικά σημεία του κώδικα, μεταγλωττίζεται μόνο μία φορά. Η cache εξοικονομεί απόδοση και ταυτόχρονα είναι για τον χρήστη εντελώς αόρατη, επομένως μια τέτοια χρήση μπορεί να θεωρηθεί θεμιτή. - - -Σύνοψη ------- - -Συζητήσαμε γιατί έχει νόημα: - -1) Να αφαιρέσετε όλες τις στατικές μεταβλητές από τον κώδικα -2) Να δηλώσετε τις εξαρτήσεις -3) Και να χρησιμοποιείτε dependency injection - -Όταν σκέφτεστε τον σχεδιασμό του κώδικα, σκεφτείτε ότι κάθε `static $foo` αποτελεί πρόβλημα. Για να είναι ο κώδικάς σας ένα περιβάλλον που σέβεται το DI, είναι απαραίτητο να εξαλείψετε εντελώς την καθολική κατάσταση και να την αντικαταστήσετε με dependency injection. - -Κατά τη διάρκεια αυτής της διαδικασίας, ίσως διαπιστώσετε ότι είναι απαραίτητο να χωρίσετε την κλάση, επειδή έχει περισσότερες από μία ευθύνες. Μην το φοβάστε. επιδιώξτε την αρχή της μοναδικής ευθύνης. - -*Θα ήθελα να ευχαριστήσω τον Miško Hevery, του οποίου τα άρθρα, όπως το [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], αποτελούν τη βάση αυτού του κεφαλαίου.* diff --git a/dependency-injection/el/introduction.texy b/dependency-injection/el/introduction.texy deleted file mode 100644 index 1711cd3f9b..0000000000 --- a/dependency-injection/el/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Τι είναι το Dependency Injection; -********************************* - -.[perex] -Αυτό το κεφάλαιο θα σας εισαγάγει στις βασικές πρακτικές προγραμματισμού που πρέπει να ακολουθείτε κατά τη συγγραφή όλων των εφαρμογών. Αυτά είναι τα θεμέλια που απαιτούνται για τη συγγραφή καθαρού, κατανοητού και συντηρήσιμου κώδικα. - -Εάν υιοθετήσετε αυτούς τους κανόνες και τους ακολουθήσετε, το Nette θα σας βοηθήσει σε κάθε βήμα. Θα χειριστεί τις εργασίες ρουτίνας για εσάς και θα σας προσφέρει μέγιστη άνεση, ώστε να μπορείτε να επικεντρωθείτε στην ίδια τη λογική. - -Οι αρχές που θα παρουσιάσουμε εδώ είναι αρκετά απλές. Δεν χρειάζεται να ανησυχείτε για τίποτα. - - -Θυμάστε το πρώτο σας πρόγραμμα; -------------------------------- - -Δεν ξέρουμε σε ποια γλώσσα το γράψατε, αλλά αν ήταν PHP, πιθανότατα θα έμοιαζε κάπως έτσι: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // εκτυπώνει 24 -``` - -Λίγες ασήμαντες γραμμές κώδικα, αλλά περιέχουν τόσες πολλές βασικές έννοιες. Ότι υπάρχουν μεταβλητές. Ότι ο κώδικας χωρίζεται σε μικρότερες μονάδες, όπως συναρτήσεις. Ότι τους περνάμε ορίσματα εισόδου και επιστρέφουν αποτελέσματα. Λείπουν μόνο οι συνθήκες και οι βρόχοι. - -Το γεγονός ότι περνάμε δεδομένα εισόδου σε μια συνάρτηση και αυτή επιστρέφει ένα αποτέλεσμα είναι μια απολύτως κατανοητή έννοια που χρησιμοποιείται και σε άλλους τομείς, όπως τα μαθηματικά. - -Μια συνάρτηση έχει την υπογραφή της, η οποία αποτελείται από το όνομά της, μια λίστα παραμέτρων και τους τύπους τους, και τέλος τον τύπο της τιμής επιστροφής. Ως χρήστες, μας ενδιαφέρει η υπογραφή· συνήθως δεν χρειάζεται να γνωρίζουμε τίποτα για την εσωτερική υλοποίηση. - -Τώρα φανταστείτε η υπογραφή της συνάρτησης να έμοιαζε κάπως έτσι: - -```php -function soucet(float $x): float -``` - -Άθροισμα με μία παράμετρο; Αυτό είναι περίεργο… Και τι θα λέγατε για αυτό; - -```php -function soucet(): float -``` - -Αυτό είναι πραγματικά πολύ περίεργο, έτσι δεν είναι; Πώς χρησιμοποιείται η συνάρτηση; - -```php -echo soucet(); // τι θα εκτυπώσει άραγε; -``` - -Κοιτάζοντας έναν τέτοιο κώδικα, θα ήμασταν μπερδεμένοι. Όχι μόνο ένας αρχάριος δεν θα τον καταλάβαινε, αλλά ούτε και ένας έμπειρος προγραμματιστής δεν καταλαβαίνει τέτοιο κώδικα. - -Αναρωτιέστε πώς θα έμοιαζε μια τέτοια συνάρτηση εσωτερικά; Από πού θα έπαιρνε τους προσθετέους; Προφανώς, θα τους έβρισκε *με κάποιο τρόπο* μόνη της, ίσως κάπως έτσι: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -Στο σώμα της συνάρτησης, ανακαλύψαμε κρυφές εξαρτήσεις από άλλες καθολικές συναρτήσεις ή στατικές μεθόδους. Για να μάθουμε από πού προέρχονται πραγματικά οι προσθετέοι, πρέπει να ψάξουμε περαιτέρω. - - -Όχι από εδώ! ------------- - -Ο σχεδιασμός που μόλις δείξαμε είναι η ουσία πολλών αρνητικών χαρακτηριστικών: - -- η υπογραφή της συνάρτησης προσποιούνταν ότι δεν χρειαζόταν προσθετέους, πράγμα που μας μπέρδεψε -- δεν ξέρουμε καθόλου πώς να κάνουμε τη συνάρτηση να προσθέσει δύο άλλους αριθμούς -- έπρεπε να κοιτάξουμε τον κώδικα για να δούμε από πού έπαιρνε τους προσθετέους -- ανακαλύψαμε κρυφές εξαρτήσεις -- για πλήρη κατανόηση, είναι απαραίτητο να εξετάσουμε και αυτές τις εξαρτήσεις - -Και είναι καθόλου έργο της συνάρτησης πρόσθεσης να αποκτά εισόδους; Φυσικά και όχι. Η ευθύνη της είναι μόνο η ίδια η πρόσθεση. - - -Δεν θέλουμε να συναντήσουμε τέτοιο κώδικα, και σίγουρα δεν θέλουμε να τον γράψουμε. Η διόρθωση είναι απλή: επιστροφή στα βασικά και απλή χρήση παραμέτρων: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Κανόνας αρ. 1: αφήστε το να σας παραδοθεί ------------------------------------------ - -Ο πιο σημαντικός κανόνας είναι: **όλα τα δεδομένα που χρειάζονται οι συναρτήσεις ή οι κλάσεις πρέπει να τους παραδίδονται**. - -Αντί να επινοείτε κρυφούς τρόπους με τους οποίους θα μπορούσαν να τα αποκτήσουν μόνοι τους, απλά περάστε τις παραμέτρους. Θα εξοικονομήσετε χρόνο που απαιτείται για την επινόηση κρυφών μονοπατιών, τα οποία σίγουρα δεν θα βελτιώσουν τον κώδικά σας. - -Αν ακολουθείτε πάντα και παντού αυτόν τον κανόνα, βρίσκεστε στο δρόμο για κώδικα χωρίς κρυφές εξαρτήσεις. Για κώδικα που είναι κατανοητός όχι μόνο στον συγγραφέα, αλλά και σε οποιονδήποτε τον διαβάσει μετά από αυτόν. Όπου όλα είναι κατανοητά από τις υπογραφές των συναρτήσεων και των κλάσεων και δεν χρειάζεται να ψάχνετε για κρυμμένα μυστικά στην υλοποίηση. - -Αυτή η τεχνική ονομάζεται τεχνικά **dependency injection**. Και αυτά τα δεδομένα ονομάζονται **εξαρτήσεις (dependencies).** Στην πραγματικότητα, είναι απλή παράδοση παραμέτρων, τίποτα περισσότερο. - -.[note] -Παρακαλώ μην συγχέετε το dependency injection, το οποίο είναι ένα πρότυπο σχεδίασης, με το "dependency injection container", το οποίο είναι ένα εργαλείο, δηλαδή κάτι διαμετρικά αντίθετο. Θα ασχοληθούμε με τα containers αργότερα. - - -Από συναρτήσεις σε κλάσεις --------------------------- - -Και πώς σχετίζονται οι κλάσεις με αυτό; Μια κλάση είναι μια πιο σύνθετη οντότητα από μια απλή συνάρτηση, ωστόσο ο κανόνας αρ. 1 ισχύει πλήρως και εδώ. Απλώς υπάρχουν [περισσότερες επιλογές για την παράδοση ορισμάτων |passing-dependencies]. Για παράδειγμα, αρκετά παρόμοια με την περίπτωση μιας συνάρτησης: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Ή χρησιμοποιώντας άλλες μεθόδους, ή απευθείας τον κατασκευαστή: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Και τα δύο παραδείγματα είναι πλήρως σύμφωνα με το dependency injection. - - -Πραγματικά παραδείγματα ------------------------ - -Στον πραγματικό κόσμο, δεν θα γράφετε κλάσεις για την πρόσθεση αριθμών. Ας προχωρήσουμε σε παραδείγματα από την πράξη. - -Έστω μια κλάση `Article` που αντιπροσωπεύει ένα άρθρο σε ένα blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // αποθηκεύουμε το άρθρο στη βάση δεδομένων - } -} -``` - -και η χρήση θα είναι η εξής: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Η μέθοδος `save()` αποθηκεύει το άρθρο σε έναν πίνακα βάσης δεδομένων. Η υλοποίησή της με τη βοήθεια του [Nette Database |database:] θα ήταν παιχνιδάκι, αν δεν υπήρχε ένα εμπόδιο: πού παίρνει η `Article` τη σύνδεση με τη βάση δεδομένων, δηλαδή το αντικείμενο της κλάσης `Nette\Database\Connection`; - -Φαίνεται ότι έχουμε πολλές επιλογές. Μπορεί να την πάρει από κάπου από μια στατική μεταβλητή. Ή να κληρονομήσει από μια κλάση που εξασφαλίζει τη σύνδεση με τη βάση δεδομένων. Ή να χρησιμοποιήσει το λεγόμενο [singleton |global-state#Singleton]. Ή τις λεγόμενες facades, που χρησιμοποιούνται στο Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Υπέροχα, λύσαμε το πρόβλημα. - -Ή μήπως όχι; - -Ας θυμηθούμε τον [##Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]: όλες οι εξαρτήσεις που χρειάζεται η κλάση πρέπει να της παραδίδονται. Επειδή αν παραβιάσουμε τον κανόνα, έχουμε πάρει τον δρόμο για βρώμικο κώδικα γεμάτο κρυφές εξαρτήσεις, ασάφεια, και το αποτέλεσμα θα είναι μια εφαρμογή που θα είναι επώδυνο να συντηρηθεί και να αναπτυχθεί. - -Ο χρήστης της κλάσης `Article` δεν έχει ιδέα πού αποθηκεύει η μέθοδος `save()` το άρθρο. Σε έναν πίνακα βάσης δεδομένων; Σε ποιον, τον παραγωγικό ή τον δοκιμαστικό; Και πώς μπορεί να αλλάξει αυτό; - -Ο χρήστης πρέπει να δει πώς υλοποιείται η μέθοδος `save()` και βρίσκει τη χρήση της μεθόδου `DB::insert()`. Άρα πρέπει να ψάξει περαιτέρω, πώς αυτή η μέθοδος αποκτά τη σύνδεση με τη βάση δεδομένων. Και οι κρυφές εξαρτήσεις μπορούν να σχηματίσουν μια αρκετά μεγάλη αλυσίδα. - -Σε καθαρό και καλά σχεδιασμένο κώδικα, δεν υπάρχουν ποτέ κρυφές εξαρτήσεις, facades του Laravel ή στατικές μεταβλητές. Σε καθαρό και καλά σχεδιασμένο κώδικα, παραδίδονται ορίσματα: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ακόμα πιο πρακτικό, όπως θα δούμε παρακάτω, θα είναι με τον κατασκευαστή: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Αν είστε έμπειρος προγραμματιστής, ίσως σκέφτεστε ότι η `Article` δεν θα έπρεπε καθόλου να έχει τη μέθοδο `save()`, θα έπρεπε να αντιπροσωπεύει ένα καθαρά δεδομενικό component και η αποθήκευση θα έπρεπε να γίνεται από ένα ξεχωριστό repository. Αυτό έχει νόημα. Αλλά αυτό θα μας πήγαινε πολύ πέρα από το θέμα, το οποίο είναι το dependency injection, και την προσπάθεια να δώσουμε απλά παραδείγματα. - -Αν γράφετε μια κλάση που απαιτεί, για παράδειγμα, μια βάση δεδομένων για τη λειτουργία της, μην επινοείτε από πού να την πάρετε, αλλά αφήστε την να σας παραδοθεί. Ίσως ως παράμετρος του κατασκευαστή ή άλλης μεθόδου. Αναγνωρίστε τις εξαρτήσεις. Αναγνωρίστε τις στο API της κλάσης σας. Θα αποκτήσετε κατανοητό και προβλέψιμο κώδικα. - -Και τι θα λέγατε για αυτήν την κλάση, η οποία καταγράφει μηνύματα σφάλματος: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Τι πιστεύετε, τηρήσαμε τον [##Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]? - -Δεν τον τηρήσαμε. - -Η κλάση *αποκτά μόνη της* την κρίσιμη πληροφορία, δηλαδή τον κατάλογο με το αρχείο καταγραφής, από μια σταθερά. - -Δείτε το παράδειγμα χρήσης: - -```php -$logger = new Logger; -$logger->log('Η θερμοκρασία είναι 23 °C'); -$logger->log('Η θερμοκρασία είναι 10 °C'); -``` - -Χωρίς γνώση της υλοποίησης, θα μπορούσατε να απαντήσετε στην ερώτηση πού γράφονται τα μηνύματα; Θα σκεφτόσασταν ότι για τη λειτουργία απαιτείται η ύπαρξη της σταθεράς `LOG_DIR`; Και θα μπορούσατε να δημιουργήσετε μια δεύτερη παρουσία που θα γράφει αλλού; Σίγουρα όχι. - -Ας διορθώσουμε την κλάση: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Η κλάση είναι τώρα πολύ πιο κατανοητή, διαμορφώσιμη και επομένως πιο χρήσιμη. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Η θερμοκρασία είναι 15 °C'); -``` - - -Αλλά αυτό δεν με ενδιαφέρει! ----------------------------- - -*«Όταν δημιουργώ ένα αντικείμενο Article και καλώ την save(), δεν θέλω να ασχολούμαι με τη βάση δεδομένων, απλά θέλω να αποθηκευτεί σε αυτήν που έχω ορίσει στη διαμόρφωση.»* - -*«Όταν χρησιμοποιώ το Logger, απλά θέλω το μήνυμα να καταγραφεί, και δεν θέλω να ασχολούμαι με το πού. Ας χρησιμοποιηθεί η καθολική ρύθμιση.»* - -Αυτές είναι σωστές παρατηρήσεις. - -Ως παράδειγμα, θα δείξουμε μια κλάση που στέλνει newsletters, η οποία καταγράφει πώς πήγε: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Τα emails στάλθηκαν'); - - } catch (Exception $e) { - $logger->log('Παρουσιάστηκε σφάλμα κατά την αποστολή'); - throw $e; - } - } -} -``` - -Ο βελτιωμένος `Logger`, ο οποίος δεν χρησιμοποιεί πλέον τη σταθερά `LOG_DIR`, απαιτεί τη διαδρομή προς το αρχείο στον κατασκευαστή. Πώς να το λύσουμε αυτό; Η κλάση `NewsletterDistributor` δεν ενδιαφέρεται καθόλου για το πού γράφονται τα μηνύματα, θέλει απλώς να τα γράψει. - -Η λύση είναι και πάλι ο [##Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]: παραδίδουμε όλα τα δεδομένα που χρειάζεται η κλάση. - -Άρα αυτό σημαίνει ότι παραδίδουμε τη διαδρομή προς το αρχείο καταγραφής μέσω του κατασκευαστή, την οποία στη συνέχεια χρησιμοποιούμε κατά τη δημιουργία του αντικειμένου `Logger`; - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ ΟΧΙ ΕΤΣΙ! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Όχι έτσι! Η διαδρομή **δεν ανήκει** στα δεδομένα που χρειάζεται η κλάση `NewsletterDistributor`· αυτά τα χρειάζεται ο `Logger`. Αντιλαμβάνεστε τη διαφορά; Η κλάση `NewsletterDistributor` χρειάζεται τον logger ως τέτοιο. Άρα αυτόν θα παραδώσουμε: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Τα emails στάλθηκαν'); - - } catch (Exception $e) { - $this->logger->log('Παρουσιάστηκε σφάλμα κατά την αποστολή'); - throw $e; - } - } -} -``` - -Τώρα είναι σαφές από τις υπογραφές της κλάσης `NewsletterDistributor` ότι η καταγραφή αποτελεί μέρος της λειτουργικότητάς της. Και η εργασία της αντικατάστασης του logger με έναν άλλο, για παράδειγμα για δοκιμές, είναι εντελώς ασήμαντη. Επιπλέον, αν ο κατασκευαστής της κλάσης `Logger` άλλαζε, αυτό δεν θα είχε καμία επίδραση στην κλάση μας. - - -Κανόνας αρ. 2: πάρε ό,τι είναι δικό σου ---------------------------------------- - -Μην μπερδεύεστε και μην αφήνετε να σας παραδίδουν τις εξαρτήσεις των εξαρτήσεών σας. Αφήστε να σας παραδίδουν μόνο τις δικές σας εξαρτήσεις. - -Χάρη σε αυτό, ο κώδικας που χρησιμοποιεί άλλα αντικείμενα θα είναι εντελώς ανεξάρτητος από τις αλλαγές στους κατασκευαστές τους. Το API του θα είναι πιο αληθινό. Και κυρίως, θα είναι ασήμαντο να αντικαταστήσετε αυτές τις εξαρτήσεις με άλλες. - - -Νέο μέλος της οικογένειας -------------------------- - -Στην ομάδα ανάπτυξης, αποφασίστηκε να δημιουργηθεί ένας δεύτερος logger, ο οποίος γράφει στη βάση δεδομένων. Έτσι, δημιουργούμε την κλάση `DatabaseLogger`. Έχουμε λοιπόν δύο κλάσεις, `Logger` και `DatabaseLogger`, η μία γράφει σε αρχείο, η άλλη στη βάση δεδομένων... δεν σας φαίνεται κάτι περίεργο στην ονομασία; Δεν θα ήταν καλύτερα να μετονομάσουμε τον `Logger` σε `FileLogger`; Σίγουρα ναι. - -Αλλά θα το κάνουμε έξυπνα. Κάτω από το αρχικό όνομα, θα δημιουργήσουμε ένα interface: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… το οποίο θα υλοποιούν και οι δύο loggers: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Και χάρη σε αυτό, δεν θα χρειαστεί να αλλάξουμε τίποτα στον υπόλοιπο κώδικα όπου χρησιμοποιείται ο logger. Για παράδειγμα, ο κατασκευαστής της κλάσης `NewsletterDistributor` θα είναι ακόμα ικανοποιημένος με το ότι απαιτεί `Logger` ως παράμετρο. Και θα εξαρτάται από εμάς ποια παρουσία θα του παραδώσουμε. - -**Γι' αυτό ποτέ δεν δίνουμε στα ονόματα των interfaces την κατάληξη `Interface` ή το πρόθεμα `I`.** Διαφορετικά, δεν θα ήταν δυνατόν να αναπτύξουμε τον κώδικα τόσο όμορφα. - - -Χιούστον, έχουμε πρόβλημα -------------------------- - -Ενώ σε ολόκληρη την εφαρμογή μπορούμε να αρκεστούμε σε μία μόνο παρουσία του logger, είτε αρχείου είτε βάσης δεδομένων, και απλά να τον παραδίδουμε παντού όπου κάτι καταγράφεται, η κατάσταση είναι εντελώς διαφορετική στην περίπτωση της κλάσης `Article`. Οι παρουσίες της δημιουργούνται ανάλογα με τις ανάγκες, ακόμα και πολλές φορές. Πώς να αντιμετωπίσουμε την εξάρτηση από τη βάση δεδομένων στον κατασκευαστή της; - -Ως παράδειγμα μπορεί να χρησιμεύσει ένας controller, ο οποίος μετά την υποβολή μιας φόρμας πρέπει να αποθηκεύσει το άρθρο στη βάση δεδομένων: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Μια πιθανή λύση προσφέρεται άμεσα: αφήνουμε το αντικείμενο της βάσης δεδομένων να παραδοθεί μέσω του κατασκευαστή στον `EditController` και χρησιμοποιούμε `$article = new Article($this->db)`. - -Όπως και στην προηγούμενη περίπτωση με τον `Logger` και τη διαδρομή προς το αρχείο, αυτή δεν είναι η σωστή προσέγγιση. Η βάση δεδομένων δεν είναι εξάρτηση του `EditController`, αλλά του `Article`. Η παράδοση της βάσης δεδομένων λοιπόν αντιβαίνει στον [Κανόνα αρ. 2: πάρε ό,τι είναι δικό σου |#Κανόνας αρ. 2: πάρε ό τι είναι δικό σου]. Όταν αλλάξει ο κατασκευαστής της κλάσης `Article` (προστεθεί μια νέα παράμετρος), θα είναι απαραίτητο να τροποποιηθεί ο κώδικας σε όλα τα σημεία όπου δημιουργούνται παρουσίες. Ουφ. - -Χιούστον, τι προτείνεις; - - -Κανόνας αρ. 3: άφησέ το στο factory ------------------------------------ - -Καταργώντας τις κρυφές εξαρτήσεις και παραδίδοντας όλες τις εξαρτήσεις ως ορίσματα, αποκτήσαμε πιο διαμορφώσιμες και ευέλικτες κλάσεις. Και επομένως χρειαζόμαστε κάτι ακόμα, το οποίο θα δημιουργήσει και θα διαμορφώσει αυτές τις πιο ευέλικτες κλάσεις για εμάς. Θα το ονομάσουμε factories. - -Ο κανόνας λέει: αν μια κλάση έχει εξαρτήσεις, άφησε τη δημιουργία των παρουσιών της σε ένα factory. - -Τα factories είναι μια πιο έξυπνη αντικατάσταση του τελεστή `new` στον κόσμο του dependency injection. - -.[note] -Παρακαλώ μην συγχέετε με το πρότυπο σχεδίασης *factory method*, το οποίο περιγράφει έναν συγκεκριμένο τρόπο χρήσης των factories και δεν σχετίζεται με αυτό το θέμα. - - -Factory -------- - -Ένα factory είναι μια μέθοδος ή μια κλάση που παράγει και διαμορφώνει αντικείμενα. Την κλάση που παράγει `Article` θα την ονομάσουμε `ArticleFactory` και θα μπορούσε να μοιάζει κάπως έτσι: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Η χρήση της στον controller θα είναι η εξής: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // αφήνουμε το factory να δημιουργήσει το αντικείμενο - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Αν αυτή τη στιγμή αλλάξει η υπογραφή του κατασκευαστή της κλάσης `Article`, το μόνο μέρος του κώδικα που πρέπει να αντιδράσει σε αυτό είναι το ίδιο το factory `ArticleFactory`. Όλος ο υπόλοιπος κώδικας που λειτουργεί με αντικείμενα `Article`, όπως για παράδειγμα ο `EditController`, δεν θα επηρεαστεί καθόλου. - -Ίσως τώρα χτυπάτε το κεφάλι σας, αν βοηθήσαμε καθόλου. Η ποσότητα του κώδικα αυξήθηκε και όλο αυτό αρχίζει να φαίνεται ύποπτα περίπλοκο. - -Μην ανησυχείτε, σε λίγο θα φτάσουμε στο Nette DI container. Και αυτός έχει πολλούς άσους στο μανίκι του, οι οποίοι θα απλοποιήσουν εξαιρετικά την κατασκευή εφαρμογών που χρησιμοποιούν dependency injection. Έτσι, για παράδειγμα, αντί για την κλάση `ArticleFactory`, θα αρκεί να [γράψουμε απλώς ένα interface |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Αλλά προτρέχουμε, περιμένετε λίγο ακόμα :-) - - -Σύνοψη ------- - -Στην αρχή αυτού του κεφαλαίου, υποσχεθήκαμε ότι θα δείξουμε μια διαδικασία για τον σχεδιασμό καθαρού κώδικα. Αρκεί στις κλάσεις - -1) [να παραδίδονται οι εξαρτήσεις που χρειάζονται |#Κανόνας αρ. 1: αφήστε το να σας παραδοθεί] -2) [και αντίστροφα, να μην παραδίδονται ό,τι δεν χρειάζονται άμεσα |#Κανόνας αρ. 2: πάρε ό τι είναι δικό σου] -3) [και ότι τα αντικείμενα με εξαρτήσεις κατασκευάζονται καλύτερα σε factories |#Κανόνας αρ. 3: άφησέ το στο factory] - -Μπορεί να μην φαίνεται έτσι με την πρώτη ματιά, αλλά αυτοί οι τρεις κανόνες έχουν εκτεταμένες συνέπειες. Οδηγούν σε μια ριζικά διαφορετική άποψη για τον σχεδιασμό του κώδικα. Αξίζει τον κόπο; Οι προγραμματιστές που εγκατέλειψαν τις παλιές συνήθειες και άρχισαν να χρησιμοποιούν με συνέπεια το dependency injection θεωρούν αυτό το βήμα ως μια κρίσιμη στιγμή στην επαγγελματική τους ζωή. Τους άνοιξε τον κόσμο των σαφών και συντηρήσιμων εφαρμογών. - -Τι γίνεται όμως αν ο κώδικας δεν χρησιμοποιεί με συνέπεια το dependency injection; Τι γίνεται αν βασίζεται σε στατικές μεθόδους ή singletons; Προκαλεί αυτό προβλήματα; [Προκαλεί, και μάλιστα πολύ σοβαρά |global-state]. diff --git a/dependency-injection/el/nette-container.texy b/dependency-injection/el/nette-container.texy deleted file mode 100644 index 7489be893e..0000000000 --- a/dependency-injection/el/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Το Nette DI είναι μία από τις πιο ενδιαφέρουσες βιβλιοθήκες του Nette. Μπορεί να δημιουργεί και να ενημερώνει αυτόματα μεταγλωττισμένα DI containers, τα οποία είναι εξαιρετικά γρήγορα και εκπληκτικά εύκολα στη διαμόρφωση. - -Τη μορφή των υπηρεσιών που πρόκειται να δημιουργήσει το DI container την ορίζουμε συνήθως χρησιμοποιώντας αρχεία διαμόρφωσης σε [μορφή NEON|neon:format]. Το container που δημιουργήσαμε χειροκίνητα στο [προηγούμενο κεφάλαιο|container], θα γραφόταν ως εξής: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Η σύνταξη είναι πραγματικά συνοπτική. - -Όλες οι εξαρτήσεις που δηλώνονται στους κατασκευαστές των κλάσεων `ArticleFactory` και `UserController`, το Nette DI τις βρίσκει και τις παραδίδει αυτόματα χάρη στο λεγόμενο [autowiring|autowiring], επομένως δεν χρειάζεται να δηλωθεί τίποτα στο αρχείο διαμόρφωσης. Έτσι, ακόμα κι αν αλλάξουν οι παράμετροι, δεν χρειάζεται να αλλάξετε τίποτα στη διαμόρφωση. Το Nette container θα αναδημιουργηθεί αυτόματα. Μπορείτε να επικεντρωθείτε αποκλειστικά στην ανάπτυξη της εφαρμογής. - -Αν θέλουμε να παραδώσουμε εξαρτήσεις χρησιμοποιώντας setters, χρησιμοποιούμε την ενότητα [setup |services#Setup] για αυτό. - -Το Nette DI παράγει απευθείας τον κώδικα PHP του container. Το αποτέλεσμα είναι λοιπόν ένα αρχείο `.php`, το οποίο μπορείτε να ανοίξετε και να μελετήσετε. Χάρη σε αυτό, βλέπετε ακριβώς πώς λειτουργεί το container. Μπορείτε επίσης να το κάνετε debug στο IDE και να το εκτελέσετε βήμα-βήμα. Και κυρίως: ο παραγόμενος κώδικας PHP είναι εξαιρετικά γρήγορος. - -Το Nette DI μπορεί επίσης να παράγει κώδικα για [factories|factory] βάσει ενός παρεχόμενου interface. Επομένως, αντί για την κλάση `ArticleFactory`, θα αρκεί να δημιουργήσουμε μόνο ένα interface στην εφαρμογή: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ολόκληρο το παράδειγμα μπορείτε να το βρείτε [στο GitHub|https://github.com/nette-examples/di-example-doc]. - - -Αυτόνομη χρήση --------------- - -Η ενσωμάτωση της βιβλιοθήκης Nette DI σε μια εφαρμογή είναι πολύ εύκολη. Πρώτα την εγκαθιστούμε με το Composer (επειδή η λήψη zip είναι τόοοσο παλιομοδίτικη): - -```shell -composer require nette/di -``` - -Ο παρακάτω κώδικας δημιουργεί μια παρουσία του DI container σύμφωνα με τη διαμόρφωση που είναι αποθηκευμένη στο αρχείο `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Το container δημιουργείται μόνο μία φορά, ο κώδικας του γράφεται στην cache (κατάλογος `__DIR__ . '/temp'`) και στα επόμενα αιτήματα απλώς φορτώνεται από εκεί. - -Για τη δημιουργία και λήψη υπηρεσιών χρησιμοποιούνται οι μέθοδοι `getService()` ή `getByType()`. Έτσι δημιουργούμε το αντικείμενο `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Κατά την ανάπτυξη, είναι χρήσιμο να ενεργοποιήσετε τη λειτουργία auto-refresh, όπου το container αναδημιουργείται αυτόματα εάν αλλάξει οποιαδήποτε κλάση ή αρχείο διαμόρφωσης. Αρκεί να δώσετε `true` ως δεύτερο όρισμα στον κατασκευαστή `ContainerLoader`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Χρήση με το Nette Framework ---------------------------- - -Όπως δείξαμε, η χρήση του Nette DI δεν περιορίζεται σε εφαρμογές γραμμένες στο Nette Framework, μπορείτε να το ενσωματώσετε οπουδήποτε με μόλις 3 γραμμές κώδικα. Ωστόσο, εάν αναπτύσσετε εφαρμογές στο Nette Framework, τη διαμόρφωση και τη δημιουργία του container την αναλαμβάνει το [Bootstrap |application:bootstrapping#Διαμόρφωση του DI Container]. diff --git a/dependency-injection/el/passing-dependencies.texy b/dependency-injection/el/passing-dependencies.texy deleted file mode 100644 index 9d5c8b4701..0000000000 --- a/dependency-injection/el/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Παράδοση εξαρτήσεων -******************* - -<div class=perex> - -Τα ορίσματα, ή στην ορολογία του DI "εξαρτήσεις", μπορούν να παραδοθούν στις κλάσεις με τους ακόλουθους κύριους τρόπους: - -* παράδοση μέσω κατασκευαστή -* παράδοση μέσω μεθόδου (του λεγόμενου setter) -* ρύθμιση μεταβλητής -* με μέθοδο, annotation ή attribute *inject* - -</div> - -Τώρα θα δείξουμε τις διάφορες παραλλαγές με συγκεκριμένα παραδείγματα. - - -Παράδοση μέσω κατασκευαστή -========================== - -Οι εξαρτήσεις παραδίδονται τη στιγμή της δημιουργίας του αντικειμένου ως ορίσματα του κατασκευαστή: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Αυτή η μορφή είναι κατάλληλη για υποχρεωτικές εξαρτήσεις που η κλάση χρειάζεται απαραίτητα για τη λειτουργία της, καθώς χωρίς αυτές δεν θα είναι δυνατή η δημιουργία της παρουσίας. - -Από την PHP 8.0, μπορούμε να χρησιμοποιήσουμε μια συντομότερη μορφή σύνταξης ([constructor property promotion |https://blog.nette.org/el/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), η οποία είναι λειτουργικά ισοδύναμη: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Από την PHP 8.1, η μεταβλητή μπορεί να επισημανθεί με τη σημαία `readonly`, η οποία δηλώνει ότι το περιεχόμενο της μεταβλητής δεν θα αλλάξει πλέον: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Το DI container παραδίδει αυτόματα τις εξαρτήσεις στον κατασκευαστή χρησιμοποιώντας [autowiring |autowiring]. Τα ορίσματα που δεν μπορούν να παραδοθούν με αυτόν τον τρόπο (π.χ. strings, αριθμοί, booleans) τα [γράφουμε στη διαμόρφωση |services#Ορίσματα]. - - -Constructor hell ----------------- - -Ο όρος *constructor hell* περιγράφει την κατάσταση όπου ένας απόγονος κληρονομεί από μια γονική κλάση, της οποίας ο κατασκευαστής απαιτεί εξαρτήσεις, και ταυτόχρονα ο απόγονος απαιτεί εξαρτήσεις. Ταυτόχρονα, πρέπει να αναλάβει και να παραδώσει και τις γονικές: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Το πρόβλημα προκύπτει τη στιγμή που θα θέλαμε να αλλάξουμε τον κατασκευαστή της κλάσης `BaseClass`, για παράδειγμα, όταν προστεθεί μια νέα εξάρτηση. Τότε είναι απαραίτητο να τροποποιηθούν και όλοι οι κατασκευαστές των απογόνων. Κάτι που καθιστά μια τέτοια τροποποίηση κόλαση. - -Πώς να το αποτρέψουμε αυτό; Η λύση είναι **να προτιμάμε τη [σύνθεση έναντι κληρονομικότητας |faq#Γιατί προτιμάται η σύνθεση composition έναντι της κληρονομικότητας]**. - -Δηλαδή, θα σχεδιάσουμε τον κώδικα διαφορετικά. Θα αποφεύγουμε τις [αφηρημένες |nette:introduction-to-object-oriented-programming#Αφηρημένες κλάσεις] `Base*` κλάσεις. Αντί η `MyClass` να αποκτά μια συγκεκριμένη λειτουργικότητα κληρονομώντας από την `BaseClass`, θα της παραδοθεί αυτή η λειτουργικότητα ως εξάρτηση: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Παράδοση μέσω setter -==================== - -Οι εξαρτήσεις παραδίδονται καλώντας μια μέθοδο, η οποία τις αποθηκεύει σε μια ιδιωτική μεταβλητή. Η συνήθης σύμβαση ονομασίας αυτών των μεθόδων είναι η μορφή `set*()`, γι' αυτό ονομάζονται setters, αλλά μπορούν φυσικά να ονομάζονται και οτιδήποτε άλλο. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Αυτός ο τρόπος είναι κατάλληλος για προαιρετικές εξαρτήσεις που δεν είναι απαραίτητες για τη λειτουργία της κλάσης, καθώς δεν εγγυάται ότι το αντικείμενο θα λάβει πραγματικά την εξάρτηση (δηλαδή ότι ο χρήστης θα καλέσει τη μέθοδο). - -Ταυτόχρονα, αυτός ο τρόπος επιτρέπει την επανειλημμένη κλήση του setter και την αλλαγή της εξάρτησης. Εάν αυτό δεν είναι επιθυμητό, προσθέτουμε έναν έλεγχο στη μέθοδο, ή από την PHP 8.1 επισημαίνουμε την property `$cache` με τη σημαία `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -Η κλήση του setter ορίζεται στη διαμόρφωση του DI container στο [κλειδί setup |services#Setup]. Και εδώ χρησιμοποιείται η αυτόματη παράδοση εξαρτήσεων μέσω autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Ρύθμιση μεταβλητής -================== - -Οι εξαρτήσεις παραδίδονται γράφοντας απευθείας στη μεταβλητή μέλους: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Αυτός ο τρόπος θεωρείται ακατάλληλος, επειδή η μεταβλητή μέλους πρέπει να δηλωθεί ως `public`. Και επομένως δεν έχουμε έλεγχο ότι η παραδοθείσα εξάρτηση θα είναι πράγματι του συγκεκριμένου τύπου (ίσχυε πριν την PHP 7.4) και χάνουμε τη δυνατότητα να αντιδράσουμε στη νέα εκχωρημένη εξάρτηση με δικό μας κώδικα, για παράδειγμα, να αποτρέψουμε την επακόλουθη αλλαγή. Ταυτόχρονα, η μεταβλητή γίνεται μέρος του δημόσιου interface της κλάσης, κάτι που μπορεί να μην είναι επιθυμητό. - -Η ρύθμιση της μεταβλητής ορίζεται στη διαμόρφωση του DI container στην [ενότητα setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Ενώ οι τρεις προηγούμενοι τρόποι ισχύουν γενικά σε όλες τις αντικειμενοστραφείς γλώσσες, η έγχυση με μέθοδο, annotation ή attribute *inject* είναι ειδική αποκλειστικά για τους presenters στο Nette. Αυτά συζητούνται σε [ξεχωριστό κεφάλαιο |best-practices:inject-method-attribute]. - - -Ποιον τρόπο να επιλέξω; -======================= - -- ο κατασκευαστής είναι κατάλληλος για υποχρεωτικές εξαρτήσεις που η κλάση χρειάζεται απαραίτητα για τη λειτουργία της -- ο setter είναι αντίθετα κατάλληλος για προαιρετικές εξαρτήσεις, ή εξαρτήσεις που μπορεί να χρειαστεί να αλλάξουν περαιτέρω -- οι δημόσιες μεταβλητές δεν είναι κατάλληλες diff --git a/dependency-injection/el/services.texy b/dependency-injection/el/services.texy deleted file mode 100644 index c611508ddd..0000000000 --- a/dependency-injection/el/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Ορισμός υπηρεσιών -***************** - -.[perex] -Η διαμόρφωση είναι το μέρος όπου διδάσκουμε στο DI container πώς να συναρμολογεί τις επιμέρους υπηρεσίες και πώς να τις συνδέει με άλλες εξαρτήσεις. Το Nette παρέχει έναν πολύ σαφή και κομψό τρόπο για να το πετύχουμε αυτό. - -Η ενότητα `services` στο αρχείο διαμόρφωσης μορφής NEON είναι το μέρος όπου ορίζουμε τις δικές μας υπηρεσίες και τις διαμορφώσεις τους. Ας δούμε ένα απλό παράδειγμα ορισμού μιας υπηρεσίας με όνομα `database`, η οποία αντιπροσωπεύει μια παρουσία της κλάσης `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Η παραπάνω διαμόρφωση θα οδηγήσει στην ακόλουθη μέθοδο factory στο [DI container|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Τα ονόματα των υπηρεσιών μας επιτρέπουν να αναφερόμαστε σε αυτές σε άλλα μέρη του αρχείου διαμόρφωσης, με τη μορφή `@ονομαΥπηρεσιας`. Εάν δεν χρειάζεται να ονομάσουμε την υπηρεσία, μπορούμε απλά να χρησιμοποιήσουμε μόνο μια παύλα: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Για να λάβουμε μια υπηρεσία από το DI container, μπορούμε να χρησιμοποιήσουμε τη μέθοδο `getService()` με το όνομα της υπηρεσίας ως παράμετρο, ή τη μέθοδο `getByType()` με τον τύπο της υπηρεσίας: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Δημιουργία υπηρεσίας -==================== - -Συνήθως δημιουργούμε μια υπηρεσία απλά δημιουργώντας μια παρουσία μιας συγκεκριμένης κλάσης. Για παράδειγμα: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Εάν χρειάζεται να επεκτείνουμε τη διαμόρφωση με επιπλέον κλειδιά, μπορούμε να αναπτύξουμε τον ορισμό σε πολλές γραμμές: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Το κλειδί `create` έχει ένα alias `factory`, και οι δύο παραλλαγές είναι συνηθισμένες στην πράξη. Ωστόσο, συνιστούμε τη χρήση του `create`. - -Τα ορίσματα του κατασκευαστή ή της μεθόδου δημιουργίας μπορούν εναλλακτικά να γραφτούν στο κλειδί `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Οι υπηρεσίες δεν χρειάζεται να δημιουργούνται μόνο με την απλή δημιουργία μιας παρουσίας κλάσης, μπορούν επίσης να είναι το αποτέλεσμα της κλήσης στατικών μεθόδων ή μεθόδων άλλων υπηρεσιών: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Σημειώστε ότι για λόγους απλότητας, αντί για `->` χρησιμοποιείται `::`, δείτε [#Εκφραστικά μέσα]. Θα δημιουργηθούν αυτές οι μέθοδοι factory: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Το DI container πρέπει να γνωρίζει τον τύπο της δημιουργημένης υπηρεσίας. Εάν δημιουργούμε μια υπηρεσία χρησιμοποιώντας μια μέθοδο που δεν έχει καθορισμένο τύπο επιστροφής, πρέπει να δηλώσουμε ρητά αυτόν τον τύπο στη διαμόρφωση: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Ορίσματα -======== - -Στον κατασκευαστή και τις μεθόδους παραδίδουμε ορίσματα με τρόπο πολύ παρόμοιο με την ίδια την PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Για καλύτερη αναγνωσιμότητα, μπορούμε να αναπτύξουμε τα ορίσματα σε ξεχωριστές γραμμές. Σε αυτή την περίπτωση, η χρήση κομμάτων είναι προαιρετική: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Μπορείτε επίσης να ονομάσετε τα ορίσματα και δεν χρειάζεται να ανησυχείτε για τη σειρά τους: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Εάν θέλετε να παραλείψετε ορισμένα ορίσματα και να χρησιμοποιήσετε την προεπιλεγμένη τους τιμή ή να εισαγάγετε μια υπηρεσία χρησιμοποιώντας [autowiring|autowiring], χρησιμοποιήστε την κάτω παύλα: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Ως ορίσματα μπορούν να παραδοθούν υπηρεσίες, να χρησιμοποιηθούν παράμετροι και πολλά άλλα, δείτε [#Εκφραστικά μέσα]. - - -Setup -===== - -Στην ενότητα `setup` ορίζουμε τις μεθόδους που πρέπει να κληθούν κατά τη δημιουργία της υπηρεσίας. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Αυτό θα έμοιαζε έτσι στην PHP: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Εκτός από την κλήση μεθόδων, μπορούν επίσης να παραδοθούν τιμές σε properties. Υποστηρίζεται επίσης η προσθήκη ενός στοιχείου σε έναν πίνακα, το οποίο πρέπει να γραφτεί σε εισαγωγικά για να μην συγκρούεται με τη σύνταξη NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Αυτό θα έμοιαζε ως εξής στον κώδικα PHP: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Στο setup, ωστόσο, μπορούν να κληθούν και στατικές μέθοδοι ή μέθοδοι άλλων υπηρεσιών. Εάν χρειάζεται να παραδώσετε την τρέχουσα υπηρεσία ως όρισμα, δηλώστε την ως `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Σημειώστε ότι για λόγους απλότητας, αντί για `->` χρησιμοποιείται `::`, δείτε [#Εκφραστικά μέσα]. Θα δημιουργηθεί μια τέτοια μέθοδος factory: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Εκφραστικά μέσα -=============== - -Το Nette DI μας δίνει εξαιρετικά πλούσια εκφραστικά μέσα, με τα οποία μπορούμε να γράψουμε σχεδόν οτιδήποτε. Στα αρχεία διαμόρφωσης μπορούμε έτσι να χρησιμοποιούμε [παραμέτρους |configuration#Παράμετροι]: - -```neon -# παράμετρος -%wwwDir% - -# τιμή παραμέτρου κάτω από κλειδί -%mailer.user% - -# παράμετρος μέσα σε string -'%wwwDir%/images' -``` - -Επίσης, να δημιουργούμε αντικείμενα, να καλούμε μεθόδους και συναρτήσεις: - -```neon -# δημιουργία αντικειμένου -DateTime() - -# κλήση στατικής μεθόδου -Collator::create(%locale%) - -# κλήση συνάρτησης PHP -::getenv(DB_USER) -``` - -Να αναφερόμαστε σε υπηρεσίες είτε με το όνομά τους είτε με τον τύπο τους: - -```neon -# υπηρεσία βάσει ονόματος -@database - -# υπηρεσία βάσει τύπου -@Nette\Database\Connection -``` - -Να χρησιμοποιούμε first-class callable syntax: .{data-version:3.2.0} - -```neon -# δημιουργία callback, αντίστοιχο του [@user, logout] -@user::logout(...) -``` - -Να χρησιμοποιούμε σταθερές: - -```neon -# σταθερά κλάσης -FilesystemIterator::SKIP_DOTS - -# καθολική σταθερά λαμβάνεται με τη συνάρτηση PHP constant() -::constant(PHP_VERSION) -``` - -Οι κλήσεις μεθόδων μπορούν να αλυσιδωθούν όπως στην PHP. Απλώς για λόγους απλότητας, αντί για `->` χρησιμοποιείται `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Αυτές τις εκφράσεις μπορείτε να τις χρησιμοποιείτε οπουδήποτε, κατά τη [δημιουργία υπηρεσιών |#Δημιουργία υπηρεσίας], στα [#ορίσματα], στην ενότητα [#setup] ή στις [παραμέτρους |configuration#Παράμετροι]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Ειδικές συναρτήσεις -------------------- - -Στα αρχεία διαμόρφωσης μπορείτε να χρησιμοποιείτε αυτές τις ειδικές συναρτήσεις: - -- `not()` άρνηση της τιμής -- `bool()`, `int()`, `float()`, `string()` μετατροπή τύπου χωρίς απώλειες στον καθορισμένο τύπο -- `typed()` δημιουργεί έναν πίνακα όλων των υπηρεσιών του καθορισμένου τύπου -- `tagged()` δημιουργεί έναν πίνακα όλων των υπηρεσιών με το δεδομένο tag - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Σε αντίθεση με την κλασική μετατροπή τύπου στην PHP, όπως π.χ. `(int)`, η μετατροπή τύπου χωρίς απώλειες θα προκαλέσει εξαίρεση για μη αριθμητικές τιμές. - -Η συνάρτηση `typed()` δημιουργεί έναν πίνακα όλων των υπηρεσιών του δεδομένου τύπου (κλάση ή interface). Παραλείπει τις υπηρεσίες που έχουν απενεργοποιημένο το autowiring. Μπορούν να δηλωθούν και περισσότεροι τύποι διαχωρισμένοι με κόμμα. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Μπορείτε επίσης να παραδώσετε αυτόματα έναν πίνακα υπηρεσιών ενός συγκεκριμένου τύπου ως όρισμα χρησιμοποιώντας [autowiring |autowiring#Πίνακας υπηρεσιών]. - -Η συνάρτηση `tagged()` στη συνέχεια δημιουργεί έναν πίνακα όλων των υπηρεσιών με ένα συγκεκριμένο tag. Και εδώ μπορείτε να καθορίσετε περισσότερα tags διαχωρισμένα με κόμμα. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Το κλειδί `autowired` επιτρέπει την επίδραση στη συμπεριφορά του autowiring για μια συγκεκριμένη υπηρεσία. Για λεπτομέρειες, δείτε το [κεφάλαιο για το autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # η υπηρεσία foo εξαιρείται από το autowiring -``` - - -Lazy υπηρεσίες .{data-version:3.2.4} -==================================== - -Το Lazy loading είναι μια τεχνική που αναβάλλει τη δημιουργία μιας υπηρεσίας μέχρι τη στιγμή που πραγματικά χρειάζεται. Στην καθολική διαμόρφωση, μπορείτε να [ενεργοποιήσετε την τεμπέλικη δημιουργία |configuration#Lazy υπηρεσίες] για όλες τις υπηρεσίες ταυτόχρονα. Για μεμονωμένες υπηρεσίες, μπορείτε στη συνέχεια να παρακάμψετε αυτή τη συμπεριφορά: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Όταν μια υπηρεσία ορίζεται ως lazy, κατά την αίτησή της από το DI container, λαμβάνουμε ένα ειδικό αντικείμενο υποκατάστατο. Αυτό φαίνεται και συμπεριφέρεται το ίδιο με την πραγματική υπηρεσία, αλλά η πραγματική αρχικοποίηση (κλήση του κατασκευαστή και του setup) πραγματοποιείται μόνο κατά την πρώτη κλήση οποιασδήποτε μεθόδου ή property της. - -.[note] -Το Lazy loading μπορεί να χρησιμοποιηθεί μόνο για κλάσεις χρήστη, όχι για εσωτερικές κλάσεις PHP. Απαιτεί PHP 8.4 ή νεότερη έκδοση. - - -Tags -==== - -Τα tags χρησιμοποιούνται για την προσθήκη συμπληρωματικών πληροφοριών στις υπηρεσίες. Μπορείτε να προσθέσετε ένα ή περισσότερα tags σε μια υπηρεσία: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Τα tags μπορούν επίσης να φέρουν τιμές: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Για να λάβετε όλες τις υπηρεσίες με συγκεκριμένα tags, μπορείτε να χρησιμοποιήσετε τη συνάρτηση `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Στο DI container, μπορείτε να λάβετε τα ονόματα όλων των υπηρεσιών με ένα συγκεκριμένο tag χρησιμοποιώντας τη μέθοδο `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// Το $names είναι ένας πίνακας που περιέχει το όνομα της υπηρεσίας και την τιμή του tag -// π.χ. ['foo' => 'monolog.logger.event', ...] -``` - - -Λειτουργία Inject -================= - -Με τη χρήση της σημαίας `inject: true` ενεργοποιείται η παράδοση εξαρτήσεων μέσω δημόσιων μεταβλητών με την annotation [inject |best-practices:inject-method-attribute#Attributes Inject] και μεθόδων [inject*() |best-practices:inject-method-attribute#Μέθοδοι inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Στην προεπιλεγμένη ρύθμιση, το `inject` ενεργοποιείται μόνο για τους presenters. - - -Τροποποίηση υπηρεσιών -===================== - -Το DI container περιέχει πολλές υπηρεσίες που έχουν προστεθεί μέσω ενσωματωμένης ή [επέκτασης χρήστη|extensions]. Μπορείτε να τροποποιήσετε τους ορισμούς αυτών των υπηρεσιών απευθείας στη διαμόρφωση. Για παράδειγμα, μπορείτε να αλλάξετε την κλάση της υπηρεσίας `application.application`, η οποία είναι συνήθως `Nette\Application\Application`, σε άλλη: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Η σημαία `alteration` είναι πληροφοριακή και λέει ότι απλώς τροποποιούμε μια υπάρχουσα υπηρεσία. - -Μπορούμε επίσης να συμπληρώσουμε το setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Κατά την αντικατάσταση μιας υπηρεσίας, μπορεί να θέλουμε να αφαιρέσουμε τα αρχικά ορίσματα, στοιχεία setup ή tags, για τα οποία χρησιμοποιείται το `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Εάν θέλετε να αφαιρέσετε μια υπηρεσία που προστέθηκε από επέκταση, μπορείτε να το κάνετε ως εξής: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/en/@home.texy b/dependency-injection/en/@home.texy index fb7805396b..06fd53c5e5 100644 --- a/dependency-injection/en/@home.texy +++ b/dependency-injection/en/@home.texy @@ -18,4 +18,5 @@ The `nette/di` package provides an extremely advanced compiled DI container for - [Service Definitions |services] - [Autowiring |autowiring] - [Generated Factories |factory] -- [Creating Extensions for Nette DI|extensions] +- [Creating Extensions for Nette DI |extensions] +- [Container Compilation Explained |compilation-internals] diff --git a/dependency-injection/en/@left-menu.texy b/dependency-injection/en/@left-menu.texy index db31b078c0..251ff50487 100644 --- a/dependency-injection/en/@left-menu.texy +++ b/dependency-injection/en/@left-menu.texy @@ -14,4 +14,17 @@ Nette DI - [Service Definitions |services] - [Autowiring |autowiring] - [Generated Factories |factory] -- [Creating Extensions for Nette DI|extensions] +- [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/autowiring.texy b/dependency-injection/en/autowiring.texy index e8a4114cd0..3e9efc9b76 100644 --- a/dependency-injection/en/autowiring.texy +++ b/dependency-injection/en/autowiring.texy @@ -30,6 +30,8 @@ class ArticleRepository } ``` +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 @@ -39,7 +41,7 @@ services: articles: Model\ArticleRepository # THROWS EXCEPTION, both mainDb and tempDb match ``` -The solution is to either 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. +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 @@ -60,6 +62,8 @@ services: 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. @@ -102,7 +106,7 @@ class ShipManager } ``` -The DI container then automatically passes an array of services corresponding to the given type. It omits services that have autowiring disabled. +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]. @@ -133,6 +137,23 @@ services: 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 -------------------- @@ -172,7 +193,7 @@ services: childDep: ChildDependent # autowiring passes the child service to the constructor ``` -The `parentDep` service throws the exception `Multiple services of type ParentClass found: parent, child`, because both the `parent` and `child` services fit into its constructor, and autowiring cannot decide which one to choose. +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`: @@ -194,7 +215,7 @@ For the `child` service, `autowired: ChildClass` could also be written as `autow In the `autowired` key, it is also possible to specify multiple classes or interfaces as an array: ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` Let's try to add interfaces to the example: @@ -239,11 +260,11 @@ class ChildDependent 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`. No other type specified for the other parameters is a supertype of `ChildClass`, so the service is not passed. +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 supertype of `ParentClass`) and now also to the `ParentDependent` constructor, because the required type `ParentClass` is also suitable. +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 supertype of `FooInterface`) and `ChildDependent`, but additionally also into the `FooDependent` constructor, but not into `BarDependent`, because `BarInterface` is not a supertype of `FooInterface`. +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: diff --git a/dependency-injection/en/compilation-internals.texy b/dependency-injection/en/compilation-internals.texy new file mode 100644 index 0000000000..2ea48d8fec --- /dev/null +++ b/dependency-injection/en/compilation-internals.texy @@ -0,0 +1,222 @@ +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. diff --git a/dependency-injection/en/configuration.texy b/dependency-injection/en/configuration.texy index 7f42e4bc97..ece63f23ef 100644 --- a/dependency-injection/en/configuration.texy +++ b/dependency-injection/en/configuration.texy @@ -8,7 +8,7 @@ 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 |best-practices:editors-and-tools#IDE Editor] for this format. +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> @@ -38,7 +38,7 @@ parameters: 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: +Parameters do not have to be just strings or numbers; they can also contain arrays: ```neon parameters: @@ -92,7 +92,7 @@ Technical settings of the DI container. ```neon di: # show DIC in Tracy Bar? - debugger: ... # (bool) defaults to true + debugger: ... # (bool) defaults to autodetection (enabled when Tracy is present) # parameter types that you never autowire excluded: ... # (string[]) @@ -145,11 +145,11 @@ You can significantly reduce metadata for [autowiring|autowiring] by listing onl Extensions ========== -Registration of additional DI extensions. This is how you add, for example, the DI extension `Dibi\Bridges\Nette\DibiExtension22` under the name `dibi`: +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\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` You then configure it in the `dibi` section: @@ -163,7 +163,7 @@ You can also add a class with parameters as an extension: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` @@ -208,6 +208,13 @@ search: - 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 @@ -255,6 +262,8 @@ search: 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 ======= diff --git a/dependency-injection/en/container.texy b/dependency-injection/en/container.texy index fc8dd7bbec..8cd0cc2e5b 100644 --- a/dependency-injection/en/container.texy +++ b/dependency-injection/en/container.texy @@ -1,5 +1,5 @@ -What Is DI Container? -********************* +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). @@ -8,7 +8,7 @@ It might surprise you, but in many cases, you don't need a dependency injection 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 `UserController`. 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. +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: @@ -25,9 +25,9 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` @@ -36,7 +36,7 @@ The usage would look like this: ```php $container = new Container; -$controller = $container->createUserController(); +$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. @@ -70,7 +70,7 @@ $container = new Container([ ]); ``` -Sharp-eyed readers might notice a problem. Every time we retrieve a `UserController` object, new instances of `ArticleFactory` and the database connection are also created. We definitely don't want that. +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: @@ -112,9 +112,9 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` @@ -130,7 +130,7 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` @@ -139,4 +139,4 @@ As you can see, writing a DIC isn't difficult. It's worth noting that the object 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 Dependency Injection Container?}} +{{maintitle: What Is a Dependency Injection Container?}} diff --git a/dependency-injection/en/extensions.texy b/dependency-injection/en/extensions.texy index 6a5ccdd87d..e14637bfe2 100644 --- a/dependency-injection/en/extensions.texy +++ b/dependency-injection/en/extensions.texy @@ -2,38 +2,66 @@ Creating Extensions for Nette DI ******************************** .[perex] -Besides configuration files, the generation of the DI container is also influenced by *extensions*. We activate them in the configuration file in the `extensions` section. +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. -This is how you add an extension, represented by the `BlogExtension` class, under the name `blog`: +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 ``` -Each compiler extension inherits from [api:Nette\DI\CompilerExtension] and can implement the following methods, which are called sequentially during the DI container compilation process: +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. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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]) -getConfigSchema() .[method] -=========================== +.[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. -This method is called first. It defines the schema for validating configuration parameters. +.[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]. -You configure the extension in a section named after the extension, in this case `blog`: + +First Extension +=============== + +Here is a small but complete extension. We activate it and configure it in the same file: ```neon -# same name as the extension +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -We create a schema describing all configuration options, including their types, allowed values, and optional default values: +And this is the whole class: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -Refer to the [Schema |schema:] page for documentation. Additionally, you can specify which options can be [dynamic |application:bootstrapping#Dynamic Parameters] using `dynamic()`, for example `Expect::int()->dynamic()`. -We access the configuration via the `$this->config` variable, which is an `stdClass` object: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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. -loadConfiguration() .[method] -============================= +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. -This method is used to add services to the container. The [api:Nette\DI\ContainerBuilder] is used for this: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -The convention is to prefix services added by the extension with its name to avoid name conflicts. The `prefix()` method does this, so if the extension is named `blog`, the service will be named `blog.articles`. +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 we need to rename a service, we can create an alias with the original name for backward compatibility. Nette does this similarly, e.g., for the `routing.router` service, which is also available under the former name `router`. +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 -$builder->addAlias('router', 'routing.router'); +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: -Loading Services from File --------------------------- +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -Services can be defined not only using the ContainerBuilder API but also using the familiar NEON syntax within the extension's configuration file or a separate NEON file. The prefix `@extension` represents the current extension. +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: @@ -107,88 +160,284 @@ services: 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: - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Load the services using `Compiler::loadDefinitionsFromConfig()`: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // load the configuration file for the extension - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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. -beforeCompile() .[method] -========================= +You can also cooperate with other registered extensions, obtained via `$this->compiler->getExtensions()`, optionally filtered by class or interface: -This method is called once the container builder holds all service definitions loaded from extensions (`loadConfiguration` methods) and user configuration files. At this stage, you can modify existing service definitions or add relationships between them (e.g., using method calls). You can find services using `findByTag()` or `findByType()`. +```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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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. + -afterCompile() .[method] -======================== +ContainerBuilder +================ -At this stage, the container class has been generated as a [ClassType |php-generator:#Classes] object. It includes all the factory methods for creating services and is ready to be written to the cache file. You can still modify the generated class code at this point. +[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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +A typical use is registering a Tracy panel only in developer mode. + -$initialization .[method] -========================= +Complex Arguments +----------------- -The `Configurator` executes initialization code after the [container is created |application:bootstrapping#index.php]. This code is built by adding statements to the `$this->initialization` object using its [addBody() method |php-generator:#Method and Function Bodies]. +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: -Here's an example showing how to start a session or instantiate services tagged with `run` using initialization code: +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // automatic session startup - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // this tag will always be exported, even when the export is restricted + $this->compiler->addExportedTag('event.subscriber'); - // services with tag 'run' must be created after the container is instantiated - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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. diff --git a/dependency-injection/en/factory.texy b/dependency-injection/en/factory.texy index 64ac6435b7..aa91328f90 100644 --- a/dependency-injection/en/factory.texy +++ b/dependency-injection/en/factory.texy @@ -139,9 +139,9 @@ 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: +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` and declare the return type: +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 @@ -163,6 +163,7 @@ Because the accessor returns a `PDO` service, and there's only one such service 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 @@ -186,15 +187,17 @@ interface MultiFactoryAlt 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: .{data-version:3.2.0} +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() + article: Article() # defines createArticle() db: PDO(%dsn%, %user%, %password%) # defines getDb() ) ``` @@ -215,12 +218,16 @@ services: Definition with Tags -------------------- -Another option how to define a multifactory is to use [tags |services#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: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + article: + create: Article + tags: {multi: article} # defines createArticle() + db: + create: PDO(%dsn%, %user%, %password%) + tags: {multi: db} # defines getDb() + + - MultiFactory(tagged: multi) ``` diff --git a/dependency-injection/en/faq.texy b/dependency-injection/en/faq.texy index b7f65321ba..65a27df0a3 100644 --- a/dependency-injection/en/faq.texy +++ b/dependency-injection/en/faq.texy @@ -49,12 +49,12 @@ Migrating from a legacy application to Dependency Injection can be a challenging - 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. This may include the use of factories. +- 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 composition is preferred over inheritance? +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]. @@ -88,7 +88,7 @@ Keep in mind [Rule #1: Let It Be Passed to You |introduction#Rule #1: Let It Be In this example, `%myParameter%` is a placeholder for the value of the `myParameter` parameter, which will be passed to the `MyClass` constructor: -```php +```neon # config.neon parameters: myParameter: Some value @@ -103,4 +103,21 @@ If you want to pass multiple parameters or use autowiring, it is useful to [wrap Does Nette support PSR-11 Container interface? ---------------------------------------------- -Nette DI Container does not support PSR-11 directly. However, if you need interoperability between the Nette DI Container and libraries or frameworks that expect the PSR-11 Container Interface, you can create a [simple adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] to serve as a bridge between the Nette DI Container and PSR-11. +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. diff --git a/dependency-injection/en/global-state.texy b/dependency-injection/en/global-state.texy index f626392831..adae065c9f 100644 --- a/dependency-injection/en/global-state.texy +++ b/dependency-injection/en/global-state.texy @@ -32,8 +32,8 @@ In terms of behavior, there is no difference between a global and a static varia 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 in 1935. -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. +"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. @@ -93,7 +93,7 @@ You must meticulously trace the code to discover that the `PaymentGateway` objec ```php $db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` A similar problem arises when using global access to a database connection: @@ -261,11 +261,11 @@ 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 `DateTimeImmutable::createFromFormat()`, `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. +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. +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. @@ -291,4 +291,6 @@ When designing your code, remember that every mutable `static $foo` is a potenti 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.* diff --git a/dependency-injection/en/introduction.texy b/dependency-injection/en/introduction.texy index ae1db18bac..ea731b1a35 100644 --- a/dependency-injection/en/introduction.texy +++ b/dependency-injection/en/introduction.texy @@ -72,7 +72,7 @@ The design we just showed is the essence of many negative characteristics: - 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 requires examining these dependencies as well. +- 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. @@ -136,7 +136,6 @@ class Sum { return $this->a + $this->b; } - } $sum = new Sum(23, 1); @@ -254,7 +253,7 @@ And what about this class, which logs error messages: ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -303,7 +302,7 @@ $logger->log('Temperature is 15 °C'); ``` -But I Don’t Care! +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."* @@ -375,7 +374,7 @@ class NewsletterDistributor } ``` -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 will have no impact on our class. +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 @@ -410,7 +409,7 @@ 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 requiring `Logger` as a parameter. And it will be up to us which instance we provide to it. +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. @@ -437,7 +436,7 @@ class EditController extends Controller 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. +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? diff --git a/dependency-injection/en/nette-container.texy b/dependency-injection/en/nette-container.texy index f651345142..eb91e58878 100644 --- a/dependency-injection/en/nette-container.texy +++ b/dependency-injection/en/nette-container.texy @@ -16,12 +16,12 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` The syntax is very concise. -All dependencies declared in the constructors of the `ArticleFactory` and `UserController` 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. Nette automatically regenerates the container. You can focus purely on application development. +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. @@ -36,7 +36,7 @@ interface ArticleFactory } ``` -You can find the full example [on GitHub|https://github.com/nette-examples/di-example-doc]. +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 @@ -48,7 +48,7 @@ Integrating the Nette DI library into an application is very easy. First, we ins composer require nette/di ``` -The following code creates an instance of the DI container according to the configuration stored in the `config.neon` file: +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'); @@ -60,19 +60,81 @@ $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. -The `getService()` or `getByType()` methods are used to create and retrieve services. This is how we create the `UserController` object: +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 -$controller = $container->getByType(UserController::class); +$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` constructor. +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 -------------------------- diff --git a/dependency-injection/en/passing-dependencies.texy b/dependency-injection/en/passing-dependencies.texy index a35fda44c5..e5e93c86ba 100644 --- a/dependency-injection/en/passing-dependencies.texy +++ b/dependency-injection/en/passing-dependencies.texy @@ -8,7 +8,7 @@ Arguments, or 'dependencies' in DI terminology, can be passed to classes in the * Constructor injection * Method injection (so-called setter injection) * Property injection -* Using the `inject` method, annotation, or attribute +* Using the `inject*()` method or the `#[Inject]` attribute </div> @@ -94,7 +94,7 @@ final class MyClass extends BaseClass } ``` -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. +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 composition is preferred over inheritance]**. @@ -156,7 +156,7 @@ class MyClass public function setCache(Cache $cache): void { - if ($this->cache) { + if (isset($this->cache)) { throw new RuntimeException('The dependency has already been set'); } $this->cache = $cache; @@ -189,7 +189,7 @@ $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 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. +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]: @@ -204,7 +204,7 @@ services: Inject ====== -While the previous three approaches apply generally in all object-oriented languages, injection via method, annotation, or the `inject` attribute is specific to Nette presenters. They are discussed in a [separate chapter |best-practices:inject-method-attribute]. +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? diff --git a/dependency-injection/en/services.texy b/dependency-injection/en/services.texy index 7b3ad6b364..2a38d75fc1 100644 --- a/dependency-injection/en/services.texy +++ b/dependency-injection/en/services.texy @@ -266,6 +266,16 @@ FilesystemIterator::SKIP_DOTS ::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 @@ -296,7 +306,7 @@ 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 +- `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 @@ -354,8 +364,12 @@ services: 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 can only be used for user-defined classes, not for internal PHP classes. It requires PHP 8.4 or newer. +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 @@ -400,7 +414,7 @@ $names = $container->findByTag('logger'); Inject Mode =========== -Using the `inject: true` flag enables dependency injection via public properties annotated with [inject |best-practices:inject-method-attribute#Inject Attributes] and [inject*() |best-practices:inject-method-attribute#inject Methods] methods. +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: @@ -424,7 +438,7 @@ services: alteration: true ``` -The `alteration` flag is informative, indicating that we are merely modifying an existing service. +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: @@ -437,6 +451,14 @@ services: - '$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 @@ -445,9 +467,9 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` If you want to remove a service added by an extension, you can do so as follows: diff --git a/dependency-injection/en/upgrading.texy b/dependency-injection/en/upgrading.texy new file mode 100644 index 0000000000..c50c1905d2 --- /dev/null +++ b/dependency-injection/en/upgrading.texy @@ -0,0 +1,49 @@ +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 diff --git a/dependency-injection/es/@home.texy b/dependency-injection/es/@home.texy index ced4c8f5ab..4a950bb72e 100644 --- a/dependency-injection/es/@home.texy +++ b/dependency-injection/es/@home.texy @@ -2,13 +2,13 @@ Nette DI ******** .[perex] -La Inyección de Dependencias es un patrón de diseño que cambiará fundamentalmente su perspectiva sobre el código y el desarrollo. Le abrirá las puertas a un mundo de aplicaciones limpiamente diseñadas y mantenibles. +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] +- [¿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] +- [Preguntas frecuentes |faq] El paquete `nette/di` proporciona un contenedor DI compilado extremadamente avanzado para PHP. @@ -17,5 +17,6 @@ El paquete `nette/di` proporciona un contenedor DI compilado extremadamente avan - [Configuración |configuration] - [Definición de servicios |services] - [Autowiring |autowiring] -- [Fábricas generadas |factory] -- [Creación de extensiones para Nette DI|extensions] +- [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 index 5b3b787b97..7b1c9af8f0 100644 --- a/dependency-injection/es/@left-menu.texy +++ b/dependency-injection/es/@left-menu.texy @@ -1,17 +1,28 @@ -Inyección de Dependencias -************************* -- [¿Qué es DI? |introduction] +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] +- [Preguntas frecuentes |faq] Nette DI -------- - [Contenedor DI de Nette |nette-container] -- [Configuración |configuration] +- [Configuración|configuration] - [Definición de servicios |services] -- [Autowiring |autowiring] -- [Fábricas generadas |factory] -- [Creación de extensiones para Nette DI|extensions] +- [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 index 1670b124ad..3798d9cda4 100644 --- a/dependency-injection/es/@meta.texy +++ b/dependency-injection/es/@meta.texy @@ -1 +1 @@ -{{sitename: Nette Documentación}} +{{sitename: Documentación de Nette}} diff --git a/dependency-injection/es/autowiring.texy b/dependency-injection/es/autowiring.texy index fab3b4da8c..83be9bbc3c 100644 --- a/dependency-injection/es/autowiring.texy +++ b/dependency-injection/es/autowiring.texy @@ -2,23 +2,23 @@ Autowiring ********** .[perex] -Autowiring es una gran característica que puede pasar automáticamente los servicios requeridos al constructor y otros métodos, por lo que no tenemos que escribirlos en absoluto. Le ahorrará mucho tiempo. +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 esto, podemos omitir la gran mayoría de los argumentos al escribir definiciones de servicios. En lugar de: +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) ``` -Simplemente escriba: +Basta con escribir: ```neon services: articles: Model\ArticleRepository ``` -Autowiring se guía por tipos, por lo que para que funcione, la clase `ArticleRepository` debe definirse de la siguiente manera: +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; @@ -30,22 +30,24 @@ class ArticleRepository } ``` -Para poder usar autowiring, debe haber **exactamente un servicio** para cada tipo en el contenedor. Si hubiera más, autowiring no sabría cuál pasar y lanzaría una excepción: +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 # LANZARÁ EXCEPCIÓN, coinciden mainDb y tempDb + articles: Model\ArticleRepository # LANZA UNA EXCEPCIÓN, coinciden mainDb y tempDb ``` -La solución sería omitir autowiring y especificar explícitamente el nombre del servicio (es decir, `articles: Model\ArticleRepository(@mainDb)`). Pero es más inteligente [desactivar |#Desactivación de autowiring] el autowiring de uno de los servicios, o [dar preferencia |#Preferencia de autowiring] al primer servicio. +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. -Desactivación de autowiring ---------------------------- +Desactivar el autowiring +------------------------ -Podemos desactivar el autowiring de un servicio usando la opción `autowired: no`: +Podemos desactivar el autowiring de un servicio con la opción `autowired: false`: ```neon services: @@ -53,27 +55,29 @@ services: tempDb: create: PDO('sqlite::memory:') - autowired: false # el servicio tempDb está excluido de autowiring + autowired: false # el servicio tempDb queda excluido del autowiring - articles: Model\ArticleRepository # por lo tanto, pasa mainDb al constructor + articles: Model\ArticleRepository # por eso pasa mainDb al constructor ``` -El servicio `articles` no lanzará una excepción porque existen dos servicios compatibles de tipo `PDO` (es decir, `mainDb` y `tempDb`) que se pueden pasar al constructor, ya que solo ve el servicio `mainDb`. +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 de autowiring en Nette funciona de manera diferente que en Symfony, donde la opción `autowire: false` indica que no se debe usar autowiring para los argumentos del constructor del servicio dado. En Nette, autowiring siempre se usa, ya sea para los argumentos del constructor o cualquier otro método. La opción `autowired: false` indica que la instancia del servicio dado no debe pasarse a ningún lugar mediante autowiring. +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 y especificamos la opción `autowired` para uno de ellos, este servicio se convierte en el preferido: +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 # se convierte en el preferido + autowired: PDO # pasa a ser el preferido tempDb: create: PDO('sqlite::memory:') @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -El servicio `articles` no lanzará una excepción porque existen dos servicios compatibles de tipo `PDO` (es decir, `mainDb` y `tempDb`), sino que utilizará el servicio preferido, es decir, `mainDb`. +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`. -Array de servicios ------------------- +Colección de servicios +---------------------- -Autowiring también puede pasar arrays de servicios de un tipo específico. Dado que en PHP no se puede escribir nativamente el tipo de los elementos del array, es necesario, además del tipo `array`, agregar un comentario phpDoc con el tipo del elemento en el formato `ClassName[]`: +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; @@ -102,15 +106,15 @@ class ShipManager } ``` -El contenedor DI luego pasa automáticamente un array de servicios que coinciden con el tipo dado. Omite los servicios que tienen autowiring desactivado. +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 en el comentario también puede tener el formato `array<int, Class>` o `list<Class>`. Si no puede influir en la forma del comentario phpDoc, puede pasar el array de servicios directamente en la configuración usando [`typed()` |services#Funciones especiales]. +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 -------------------- -Autowiring solo puede inyectar objetos y arrays de objetos. Los argumentos escalares (por ejemplo, cadenas, números, booleanos) [los escribimos en la configuración |services#Argumentos]. Una alternativa es crear un [objeto de configuración |best-practices:passing-settings-to-presenters], que encapsula el valor escalar (o múltiples valores) en forma de objeto, y este luego se puede pasar nuevamente mediante autowiring. +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 @@ -123,24 +127,41 @@ class MySettings } ``` -Lo convierte en un servicio agregándolo a la configuración: +Lo registra como servicio añadiéndolo a la configuración: ```neon services: - MySettings('any value') ``` -Todas las clases lo solicitarán luego mediante autowiring. +Las demás clases pueden pedirlo entonces mediante autowiring. -Restricción de 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 +----------------------- -Para servicios individuales, autowiring se puede restringir solo a ciertas clases o interfaces. +En los servicios concretos, el autowiring se puede estrechar a clases o interfaces determinadas. -Normalmente, autowiring pasa el servicio a cada parámetro del método cuyo tipo coincide con el servicio. La restricción significa que establecemos condiciones que deben cumplir los tipos especificados en los parámetros del método para que se les pase el servicio. +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. -Lo mostraremos con un ejemplo: +Veamos un ejemplo: ```php class ParentClass @@ -162,42 +183,42 @@ class ChildDependent } ``` -Si los registráramos todos como servicios, autowiring fallaría: +Si los registráramos todos como servicios, el autowiring fallaría: ```neon services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # LANZARÁ EXCEPCIÓN, coinciden los servicios parent y child - childDep: ChildDependent # autowiring pasa el servicio child al constructor + 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` lanzará una excepción `Multiple services of type ParentClass found: parent, child`, porque ambos servicios `parent` y `child` encajan en su constructor, y autowiring no puede decidir cuál elegir. +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. -Por lo tanto, para el servicio `child`, podemos restringir su autowiring al tipo `ChildClass`: +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 'autowired: self' + autowired: ChildClass # también se puede escribir como 'autowired: self' - parentDep: ParentDependent # autowiring pasa el servicio parent al constructor - childDep: ChildDependent # autowiring pasa el servicio child al constructor + parentDep: ParentDependent # el autowiring pasa el servicio parent al constructor + childDep: ChildDependent # el autowiring pasa el servicio child al constructor ``` -Ahora, el servicio `parent` se pasa al constructor del servicio `parentDep`, porque ahora es el único objeto compatible. Autowiring ya no pasa el servicio `child` allí. Sí, el servicio `child` sigue siendo de tipo `ParentClass`, pero la condición de restricción dada para el tipo de parámetro ya no se cumple, es decir, no es cierto que `ParentClass` *es un supertipo de* `ChildClass`. +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 podría escribirse como `autowired: self`, ya que `self` es un marcador de posición para la clase del servicio actual. +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 especificar varias clases o interfaces como un array: +En la clave `autowired` también es posible indicar varias clases o interfaces como array: ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -Intentemos complementar el ejemplo con interfaces: +Probemos a añadir interfaces al ejemplo: ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -Si no restringimos el servicio `child` de ninguna manera, encajará en los constructores de todas las clases `FooDependent`, `BarDependent`, `ParentDependent` y `ChildDependent`, y autowiring lo pasará allí. +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. -Pero si restringimos su autowiring a `ChildClass` usando `autowired: ChildClass` (o `self`), autowiring solo lo pasará al constructor de `ChildDependent`, porque requiere un argumento de tipo `ChildClass` y es cierto que `ChildClass` *es de tipo* `ChildClass`. Ningún otro tipo especificado en los otros parámetros es un supertipo de `ChildClass`, por lo que el servicio no se pasa. +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` usando `autowired: ParentClass`, autowiring lo pasará nuevamente al constructor de `ChildDependent` (porque el `ChildClass` requerido es un supertipo de `ParentClass`) y ahora también al constructor de `ParentDependent`, porque el tipo requerido `ParentClass` también es compatible. +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`, seguirá siendo autowired en `ParentDependent` (el `ParentClass` requerido es un supertipo de `FooInterface`) y `ChildDependent`, pero además también en el constructor de `FooDependent`, pero no en `BarDependent`, porque `BarInterface` no es un supertipo de `FooInterface`. +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: @@ -251,8 +272,8 @@ services: create: ChildClass autowired: FooInterface - fooDep: FooDependent # autowiring pasa child al constructor - barDep: BarDependent # LANZARÁ EXCEPCIÓN, ningún servicio coincide - parentDep: ParentDependent # autowiring pasa child al constructor - childDep: ChildDependent # autowiring pasa child al constructor + 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 new file mode 100644 index 0000000000..ad0f71ebad --- /dev/null +++ b/dependency-injection/es/compilation-internals.texy @@ -0,0 +1,222 @@ +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 index 8d3c0151ae..7dfc6fd876 100644 --- a/dependency-injection/es/configuration.texy +++ b/dependency-injection/es/configuration.texy @@ -1,33 +1,33 @@ -Configuración del Contenedor DI +Configuración del contenedor DI ******************************* .[perex] -Resumen de las opciones de configuración para el contenedor Nette DI. +Resumen de las opciones de configuración del contenedor DI de Nette. Archivo de configuración ======================== -El contenedor Nette DI se controla fácilmente mediante archivos de configuración. Normalmente se escriben en [formato NEON |neon:format]. Para la edición, recomendamos [editores con soporte |best-practices:editors-and-tools#Editor IDE] para este formato. +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]: "Decorador .[prism-token prism-comment]"<br> +"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> +"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] -Si desea escribir una cadena que contenga el carácter `%`, debe escaparlo duplicándolo a `%%`. +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 luego se pueden usar como parte de las definiciones de servicios. Esto puede aclarar la configuración o unificar y separar valores que cambiarán. +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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -Nos referimos al parámetro `dsn` en cualquier parte de la configuración escribiendo `%dsn%`. Los parámetros también se pueden usar dentro de cadenas como `'%wwwDir%/images'`. +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 que ser solo cadenas o números, también pueden contener arrays: +Los parámetros no tienen por qué ser solo cadenas o números: también pueden contener arrays: ```neon parameters: @@ -49,32 +49,32 @@ parameters: languages: [cs, en, de] ``` -Nos referimos a una clave específica como `%mailer.user%`. +A una clave concreta nos referimos como `%mailer.user%`. -Si necesita averiguar el valor de cualquier parámetro en su código, por ejemplo, en una clase, páselo a esa clase. Por ejemplo, en el constructor. No existe un objeto global que represente la configuración al que las clases consultarían los valores de los parámetros. Eso violaría el principio de inyección de dependencias. +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 ========= -Ver [capítulo separado |services]. +Véase el [capítulo aparte|services]. Decorator ========= -¿Cómo modificar masivamente todos los servicios de un tipo determinado? Por ejemplo, ¿llamar a un método específico en todos los presenters que heredan de un ancestro común específico? Para eso está el 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 son instancias de esta clase o interfaz + # 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 también se puede usar para configurar [tags |services#Tags] o activar el modo [inject |services#Modo Inject]. +El decorator se puede usar también para poner [etiquetas |services#Etiquetas] o para activar el [modo inject |services#Modo inject]. ```neon decorator: @@ -87,90 +87,90 @@ decorator: DI === -Configuración técnica del contenedor DI. +Ajustes técnicos del contenedor DI. ```neon di: - # ¿mostrar DIC en Tracy Bar? - debugger: ... # (bool) predeterminado es true + # ¿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 autowirear + # tipos de parámetros que nunca se autoconectan excluded: ... # (string[]) - # ¿permitir la creación lazy de servicios? - lazy: ... # (bool) predeterminado es false + # ¿activar la creación lazy de los servicios? + lazy: ... # (bool) el valor predeterminado es false - # clase de la que hereda el contenedor DI - parentClass: ... # (string) predeterminado es Nette\DI\Container + # 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} ------------------------------------ -La configuración `lazy: true` activa la creación lazy (diferida) de servicios. Esto significa que los servicios no se crean realmente en el momento en que los solicitamos del contenedor DI, sino en el momento de su primer uso. Esto puede acelerar el inicio de la aplicación y reducir los requisitos de memoria, ya que solo se crean los servicios que realmente se necesitan en la solicitud dada. +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 específico, la creación lazy se puede [cambiar |services#Servicios Lazy]. +Para un servicio concreto, la creación lazy se puede [ajustar |services#Servicios lazy]. .[note] -Los objetos lazy solo se pueden usar para clases de usuario, no para clases internas de PHP. Requiere PHP 8.4 o posterior. +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 también contiene muchos metadatos. Puede reducir su tamaño reduciendo la 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 parámetros? - parameters: false # (bool) predeterminado es true + # ¿exportar los parámetros? + parameters: false # (bool) el valor predeterminado es true - # ¿exportar tags y cuáles? - tags: # (string[]|bool) predeterminados son todos + # ¿exportar las etiquetas y cuáles? + tags: # (string[]|bool) de forma predeterminada, todas - event.subscriber - # ¿exportar datos para autowiring y cuáles? - types: # (string[]|bool) predeterminados son todos + # ¿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 utiliza el array `$container->getParameters()`, puede desactivar la exportación de parámetros. Además, puede exportar solo los tags a través de los cuales obtiene servicios con el método `$container->findByTag(...)`. Si no llama al método en absoluto, puede desactivar completamente la exportación de tags usando `false`. +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 significativamente los metadatos para [autowiring |autowiring] especificando las clases que usa como parámetro del método `$container->getByType()`. Y nuevamente, si no llama al método en absoluto (o solo en el [bootstrap |application:bootstrapping] para obtener `Nette\Application\Application`), puede desactivar la exportación por completo usando `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. De esta manera, agregamos, por ejemplo, la extensión DI `Dibi\Bridges\Nette\DibiExtension22` bajo el nombre `dibi` +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\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -Posteriormente, la configuramos en la sección `dibi`: +Después la configura en la sección `dibi`: ```neon dibi: host: localhost ``` -También se puede agregar como extensión una clase que tiene parámetros: +También puede añadir como extensión una clase con parámetros: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` Inclusión de archivos ===================== -Podemos incluir otros archivos de configuración en la sección `includes`: +Se pueden incluir archivos de configuración adicionales en la sección `includes`: ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -El nombre `parameters.php` no es un error tipográfico, la configuración también se puede escribir en un archivo PHP, que la devuelve como un array: +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 @@ -192,15 +192,15 @@ return [ ]; ``` -Si aparecen elementos con las mismas claves en los archivos de configuración, se sobrescribirán o, en el caso de [arrays, se fusionarán |#Fusión]. El archivo incluido posteriormente tiene mayor prioridad que el anterior. El archivo en el que se especifica la sección `includes` tiene mayor prioridad que los archivos incluidos en él. +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 ====== -La adición automática de servicios al contenedor DI hace que el trabajo sea extremadamente agradable. Nette agrega automáticamente presenters al contenedor, pero también se pueden agregar fácilmente cualquier otra clase. +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. -Simplemente especifique en qué directorios (y subdirectorios) debe buscar clases: +Basta con indicar en qué directorios (y subdirectorios) deben buscarse las clases: ```neon search: @@ -208,7 +208,14 @@ search: - in: %appDir%/Model ``` -Sin embargo, generalmente no queremos agregar absolutamente todas las clases e interfaces, por lo que podemos filtrarlas: +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: @@ -223,7 +230,7 @@ search: - *Factory ``` -O podemos seleccionar clases que heredan o implementan al menos una de las clases especificadas: +O podemos elegir las clases que heredan o implementan al menos una de las clases indicadas: ```neon @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -También se pueden definir reglas de exclusión, es decir, máscaras de nombre de clase o ancestros hereditarios, que si coinciden, el servicio no se agrega al contenedor DI: +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: @@ -247,7 +254,7 @@ search: implements: ... ``` -Se pueden establecer tags para todos los servicios: +A todos los servicios registrados automáticamente se les pueden asignar etiquetas: ```neon search: @@ -255,11 +262,13 @@ search: 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 aparecen elementos con las mismas claves en varios archivos de configuración, se sobrescribirán o, en el caso de arrays, se fusionarán. El archivo incluido posteriormente tiene mayor prioridad que el anterior. +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> @@ -292,7 +301,7 @@ items: </tr> </table> -Para los arrays, se puede evitar la fusión agregando un signo de exclamación después del nombre de la clave: +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> @@ -323,4 +332,4 @@ items: </tr> </table> -{{maintitle: Configuración de Inyección de Dependencias}} +{{maintitle: Configuración de la inyección de dependencias}} diff --git a/dependency-injection/es/container.texy b/dependency-injection/es/container.texy index 75f9e13142..db8f6c5753 100644 --- a/dependency-injection/es/container.texy +++ b/dependency-injection/es/container.texy @@ -2,15 +2,15 @@ ************************* .[perex] -Un contenedor de inyección de dependencias (DIC) es una clase que puede instanciar y configurar objetos. +Un contenedor de inyección de dependencias (DIC o contenedor DI) es un objeto responsable de crear y configurar otros objetos (llamados servicios). -Puede que le sorprenda, pero en muchos casos no necesita un contenedor de inyección de dependencias para aprovechar los beneficios de la inyección de dependencias (DI para abreviar). Después de todo, incluso en el [capítulo introductorio |introduction], mostramos DI con ejemplos concretos y no se necesitó ningún contenedor. +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, si necesita administrar una gran cantidad de objetos diferentes con muchas dependencias, un contenedor de inyección de dependencias será realmente útil. Este es el caso, por ejemplo, de las aplicaciones web construidas sobre un framework. +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 `UserController`. Ambas tienen algunas dependencias, a saber, la base de datos y la fábrica `ArticleFactory`. Y ahora crearemos un contenedor para estas clases. Por supuesto, para un ejemplo tan simple, no tiene sentido tener un contenedor. Pero lo crearemos para mostrar cómo se ve y funciona. +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. -Aquí hay un contenedor simple codificado para el ejemplo dado: +Este es un contenedor sencillo, escrito a fuego, para el ejemplo anterior: ```php class Container @@ -25,23 +25,23 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` -El uso se vería así: +Su uso sería así: ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -Simplemente le pedimos al contenedor el objeto y ya no necesitamos saber nada sobre cómo crearlo y cuáles son sus dependencias; el contenedor sabe todo eso. Las dependencias son inyectadas automáticamente por el contenedor. Ahí radica su poder. +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. -Hasta ahora, el contenedor tiene todos los datos codificados. Así que daremos el siguiente paso y agregaremos parámetros para que el contenedor sea realmente útil: +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 @@ -70,9 +70,9 @@ $container = new Container([ ]); ``` -Los lectores atentos pueden haber notado un cierto problema. Cada vez que obtengo un objeto `UserController`, también se crea una nueva instancia de `ArticleFactory` y la base de datos. Definitivamente no queremos eso. +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 lo tanto, agregaremos un método `getService()` que devolverá las mismas instancias siempre: +Por eso añadiremos un método `getService()` que devolverá siempre las mismas instancias: ```php class Container @@ -98,9 +98,9 @@ class Container } ``` -En la primera llamada, por ejemplo, `$container->getService('Database')`, hará que `createDatabase()` cree el objeto de la base de datos, lo almacenará en el array `$services` y lo devolverá directamente en la próxima llamada. +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. -También modificaremos el resto del contenedor para usar `getService()`: +Modificamos también el resto del contenedor para que use `getService()`: ```php class Container @@ -112,16 +112,16 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -Por cierto, el término servicio se refiere a cualquier objeto administrado por el contenedor. Por eso el nombre del método `getService()`. +Por cierto, el término servicio se refiere a cualquier objeto gestionado por el contenedor. De ahí el nombre del método `getService()`. -Hecho. ¡Tenemos un contenedor DI completamente funcional! Y podemos usarlo: +Listo. ¡Tenemos un contenedor DI plenamente funcional! Y podemos usarlo: ```php $container = new Container([ @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -Como puede ver, escribir un DIC no es nada complicado. Vale la pena recordar que los propios objetos no saben que algún contenedor los está creando. Por lo tanto, es posible crear cualquier objeto en PHP de esta manera sin interferir con su código fuente. +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 manualmente una clase de contenedor puede convertirse rápidamente en una pesadilla. Por lo tanto, en el próximo capítulo, hablaremos sobre el [Contenedor Nette DI |nette-container], que puede generarse y actualizarse casi por sí mismo. +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 index 17faf12c21..c68399c9df 100644 --- a/dependency-injection/es/extensions.texy +++ b/dependency-injection/es/extensions.texy @@ -2,38 +2,66 @@ Creación de extensiones para Nette DI ************************************* .[perex] -La generación del contenedor DI, además de los archivos de configuración, también está influenciada por las llamadas *extensiones*. Las activamos en el archivo de configuración en la sección `extensions`. +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. -Así es como agregamos una extensión representada por la clase `BlogExtension` bajo el nombre `blog`: +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 ``` -Cada extensión del compilador hereda de [api:Nette\DI\CompilerExtension] y puede implementar los siguientes métodos, que se llaman secuencialmente durante la construcción del contenedor DI: +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. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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]) -getConfigSchema() .[method] -=========================== +.[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é. -Este método se llama primero. Define el esquema para validar los parámetros de configuración. +.[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]. -Configuramos la extensión en la sección cuyo nombre es el mismo que aquel bajo el cual se agregó la extensión, es decir, `blog`: + +Primera extensión +================= + +Aquí tiene una extensión pequeña pero completa. La activamos y la configuramos en el mismo archivo: ```neon -# mismo nombre que la extensión +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Creamos un esquema que describe todas las opciones de configuración, incluidos sus tipos, valores permitidos y, opcionalmente, valores predeterminados: +Y esta es la clase entera: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -Encontrará la documentación en la página [Schema |schema:]. Además, se puede especificar qué opciones pueden ser [dinámicas |application:bootstrapping#Parámetros dinámicos] usando `dynamic()`, por ejemplo, `Expect::int()->dynamic()`. -Accedemos a la configuración a través de la variable `$this->config`, que es un objeto `stdClass`: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postsPerPage; + $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. -loadConfiguration() .[method] +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 ============================= -Se utiliza para agregar servicios al contenedor. Para esto se utiliza [api:Nette\DI\ContainerBuilder]: +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // o setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -La convención es prefijar los servicios agregados por la extensión con su nombre para evitar conflictos de nombres. Esto lo hace el método `prefix()`, por lo que si la extensión se llama `blog`, el servicio se llamará `blog.articles`. +La configuración validada está disponible en `$this->config` como objeto `stdClass` (o como array, si añade `castTo('array')` al esquema). -Si necesitamos renombrar un servicio, podemos crear un alias con el nombre original para mantener la compatibilidad hacia atrás. Nette hace algo similar, por ejemplo, con el servicio `routing.router`, que también está disponible bajo el nombre anterior `router`. +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 -$builder->addAlias('router', 'routing.router'); +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: -Carga de servicios desde un archivo ------------------------------------ +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -No tenemos que crear servicios solo usando la API de la clase ContainerBuilder, sino también con la notación familiar utilizada en el archivo de configuración NEON en la sección de servicios. El prefijo `@extension` representa la extensión actual. +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: @@ -107,88 +160,284 @@ services: 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: - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Cargamos los servicios: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // cargar el archivo de configuración para la extensión - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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. -beforeCompile() .[method] -========================= +También puede colaborar con otras extensiones registradas, obtenidas con `$this->compiler->getExtensions()` y filtradas opcionalmente por clase o interfaz: -El método se llama cuando el contenedor contiene todos los servicios agregados por las extensiones individuales en los métodos `loadConfiguration` y también por los archivos de configuración del usuario. En esta etapa de construcción, podemos modificar las definiciones de servicios o agregar enlaces entre ellos. Para buscar servicios en el contenedor por tags, se puede usar el método `findByTag()`, y por clase o interfaz, el método `findByType()`. +```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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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. + -afterCompile() .[method] -======================== +ContainerBuilder +================ -En esta etapa, la clase del contenedor ya está generada en forma de objeto [ClassType |php-generator:#Clases], contiene todos los métodos que crean servicios y está lista para ser escrita en la caché. Todavía podemos modificar el código de la clase resultante en este momento. +[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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Un uso típico es registrar un panel de Tracy solo en modo de desarrollo. + -$initialization .[method] -========================= +Argumentos complejos +-------------------- -La clase Configurator, después de [crear el contenedor |application:bootstrapping#index.php], llama al código de inicialización, que se crea escribiendo en el objeto `$this->initialization` usando el [método addBody() |php-generator:#Cuerpos de métodos y funciones]. +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: -Mostraremos un ejemplo de cómo iniciar la sesión con código de inicialización o ejecutar servicios que tienen el tag `run`: +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // inicio automático de la sesión - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // esta etiqueta se exportará siempre, aunque la exportación esté restringida + $this->compiler->addExportedTag('event.subscriber'); - // los servicios con el tag run deben crearse después de instanciar el contenedor - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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 index 79d16143b1..271383f43f 100644 --- a/dependency-injection/es/factory.texy +++ b/dependency-injection/es/factory.texy @@ -1,12 +1,12 @@ -Fábricas generadas -****************** +Factories generadas +******************* .[perex] -Nette DI puede generar automáticamente código de fábrica basado en interfaces, lo que le ahorra escribir código. +Nette DI puede generar automáticamente el código de las factories a partir de interfaces, lo que le ahorra escribir código. -Una fábrica es una clase que produce y configura objetos. Por lo tanto, también les pasa sus dependencias. Por favor, no lo confunda con el patrón de diseño *factory method*, que describe una forma específica de usar fábricas y no está relacionado con este tema. +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. -Mostramos cómo se ve una fábrica así en el [capítulo introductorio |introduction#Fábrica]: +Ya hemos mostrado qué aspecto tiene una factory así en el [capítulo introductorio |introduction#Factory]: ```php class ArticleFactory @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DI puede generar automáticamente el código de las fábricas. 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: +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 @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -Es decir, la fábrica `ArticleFactory` tiene un método `create` que crea objetos `Article`. La clase `Article` podría verse así: +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 @@ -44,16 +44,16 @@ class Article } ``` -Agregamos la fábrica al archivo de configuración: +Añada la factory al archivo de configuración: ```neon services: - ArticleFactory ``` -Nette DI generará la implementación correspondiente de la fábrica. +Nette DI generará la implementación correspondiente de la factory. -En el código que usa la fábrica, solicitamos el objeto según la interfaz y Nette DI usará la implementación generada: +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 @@ -65,17 +65,17 @@ class UserController public function foo() { - // dejamos que la fábrica cree el objeto + // dejamos que la factory cree el objeto $article = $this->articleFactory->create(); } } ``` -Fábrica parametrizada +Factory parametrizada ===================== -El método de fábrica `create` puede aceptar parámetros, que luego pasa al constructor. Agreguemos, por ejemplo, a la clase `Article` el ID del autor del artículo: +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 @@ -88,7 +88,7 @@ class Article } ``` -También agregamos el parámetro a la fábrica: +Añadiremos el parámetro también a la factory: ```php interface ArticleFactory @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -Gracias a que el parámetro en el constructor y el parámetro en la fábrica tienen el mismo nombre, Nette DI los pasa de forma completamente automática. +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 multilínea usando la clave `implement`: +La definición también se puede escribir en forma de varias líneas con la clave `implement`: ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -Al escribir de esta manera más larga, es posible especificar argumentos adicionales para el constructor en la clave `arguments` y configuración adicional usando `setup`, al igual que con los servicios normales. +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 especificar un valor fijo en la configuración, que se pasaría al constructor de `Article`: +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: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -O viceversa, si `create()` aceptara el parámetro `$authorId`, pero no fuera parte del constructor y se pasara mediante el método `Article::setAuthorId()`, nos referiríamos a él en la sección `setup`: +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: @@ -137,11 +137,11 @@ services: Accessor ======== -Además de las fábricas, Nette también puede generar los llamados accessors. Son objetos con un método `get()` que devuelve un servicio específico del contenedor DI. Las llamadas repetidas a `get()` devuelven siempre la misma instancia. +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 proporcionan carga diferida (lazy-loading) a las dependencias. Supongamos que tenemos una clase que escribe errores en una base de datos especial. Si esta clase recibiera la conexión a la base de datos como dependencia a través del constructor, la conexión siempre tendría que crearse, aunque en la práctica un error ocurre solo excepcionalmente y, por lo tanto, la conexión permanecería mayormente sin usar. En lugar de eso, la clase recibe un accessor y solo cuando se llama a su `get()`, se crea el objeto de la base de datos: +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 crear un accessor? Simplemente escriba una interfaz y Nette DI generará la implementación. La interfaz debe tener exactamente un método llamado `get` y declarar un tipo de retorno: +¿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 @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -Agregamos el accessor al archivo de configuración, donde también está la definición del servicio que devolverá: +Añada el accessor al archivo de configuración junto con la definición del servicio que debe devolver: ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -Dado que el accessor devuelve un servicio de tipo `PDO` y en la configuración hay un único servicio de este tipo, devolverá precisamente ese. Si hubiera más servicios del tipo dado, especificaríamos el servicio devuelto usando el nombre, por ejemplo, `- PDOAccessor(@db1)`. +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)`. -Fábrica/Accessor múltiple -========================= -Hasta ahora, nuestras fábricas y accessors siempre podían producir o devolver solo un objeto. Pero también es muy fácil crear fábricas múltiples combinadas con accessors. La interfaz de tal clase contendrá cualquier número de métodos con los nombres `create<name>()` y `get<name>()`, por ejemplo: +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 @@ -173,9 +174,9 @@ interface MultiFactory } ``` -Así que en lugar de pasar varias fábricas y accessors generados, pasamos una fábrica más compleja que puede hacer más cosas. +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: +Alternativamente, en lugar de varios métodos se puede usar `get()` con un parámetro: ```php interface MultiFactoryAlt @@ -184,22 +185,24 @@ interface MultiFactoryAlt } ``` -Entonces se cumple que `MultiFactory::getArticle()` hace lo mismo que `MultiFactoryAlt::get('article')`. Sin embargo, la notación alternativa tiene la desventaja de que no está claro qué valores de `$name` son compatibles y, lógicamente, tampoco es posible distinguir diferentes valores de retorno para diferentes `$name` en la interfaz. +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 por lista --------------------- -De esta manera se puede definir una fábrica múltiple en la configuración: .{data-version:3.2.0} + +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() + article: Article() # define createArticle() db: PDO(%dsn%, %user%, %password%) # define getDb() ) ``` -O podemos referirnos a servicios existentes en la definición de la fábrica usando una referencia: +Alternativamente puede referirse a servicios existentes en la definición de la multifactory mediante referencias: ```neon services: @@ -212,15 +215,19 @@ services: ``` -Definición mediante tags +Definición con etiquetas ------------------------ -La segunda opción es usar [tags |services#Tags] para la definición: +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: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index fda627b6c1..c5d3ecb7eb 100644 --- a/dependency-injection/es/faq.texy +++ b/dependency-injection/es/faq.texy @@ -5,90 +5,90 @@ Preguntas frecuentes sobre DI (FAQ) ¿Es DI otro nombre para IoC? ---------------------------- -*Inversion of Control* (IoC) es un principio centrado en la forma en que se ejecuta el código: si su código ejecuta código ajeno o si su código se integra en código ajeno, que luego lo llama. IoC es un término amplio que incluye [eventos |nette:glossary#Eventos], el llamado [Principio de Hollywood |application:components#Estilo Hollywood] y otros aspectos. Parte de este concepto también son las fábricas, de las que habla la [Regla n.º 3: déjalo en manos de la fábrica |introduction#Regla nº 3: déjalo en manos de la fábrica], y que representan una inversión para el operador `new`. +*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 la forma en que un objeto conoce a otro objeto, es decir, sus dependencias. Es un patrón de diseño que requiere el paso explícito de dependencias entre objetos. +*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 lo tanto, se puede decir que DI es una forma específica de IoC. Sin embargo, no todas las formas de IoC son adecuadas desde el punto de vista de la limpieza del código. Por ejemplo, entre los antipatrones se encuentran técnicas que trabajan con [estado global |global-state] o el llamado [Service Locator |#Qué es Service Locator]. +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 Service Locator? ------------------------- +¿Qué es un Service Locator? +--------------------------- -Es una alternativa a la Inyección de Dependencias. Funciona creando un repositorio central donde se registran todos los servicios o dependencias disponibles. Cuando un objeto necesita una dependencia, la solicita al 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. -Sin embargo, en comparación con la Inyección de Dependencias, pierde transparencia: las dependencias no se pasan directamente a los objetos y no son tan fáciles de identificar, lo que requiere examinar el código para revelar y comprender todas las conexiones. Las pruebas también son más complicadas porque no podemos simplemente pasar objetos simulados (mock) a los objetos probados, sino que debemos hacerlo a través del Service Locator. Además, el Service Locator interrumpe el diseño del código, ya que los objetos individuales deben conocer su existencia, lo que difiere de la Inyección de Dependencias, donde los objetos no tienen conocimiento del contenedor DI. +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 dificultades asociadas con el uso del patrón de diseño de Inyección de Dependencias. Por el contrario, obtener dependencias de lugares globalmente disponibles conduce a [toda una serie de complicaciones |global-state], al igual que el uso de Service Locator. Por lo tanto, es aconsejable usar DI siempre. Esto no es un enfoque dogmático, sino simplemente que no se ha encontrado una alternativa mejor. +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. -Sin embargo, existen ciertas situaciones en las que no pasamos objetos y los obtenemos del espacio global. Por ejemplo, al depurar código, cuando necesita imprimir el valor de una variable en un punto específico del programa, medir la duración de una parte específica del programa o registrar un mensaje. En tales casos, cuando se trata de tareas temporales que luego se eliminarán del código, es legítimo utilizar un dumper, cronómetro o logger globalmente disponible. Estas herramientas no pertenecen al diseño del código. +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 el uso de DI sus desventajas? ------------------------------------- +¿Tiene inconvenientes usar DI? +------------------------------ -¿Implica el uso de la Inyección de Dependencias alguna desventaja, como una mayor dificultad para escribir código o un peor rendimiento? ¿Qué perdemos cuando empezamos a escribir código de acuerdo con 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? -DI no tiene impacto en el rendimiento ni en los requisitos de memoria de la aplicación. El rendimiento del Contenedor DI puede jugar un cierto papel, sin embargo, en el caso de [Nette DI |nette-container], el contenedor se compila en PHP puro, por lo que su sobrecarga durante la ejecución de la aplicación es esencialmente nula. +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, suele ser necesario crear constructores que acepten dependencias. Antes esto podía ser tedioso, pero gracias a los IDE modernos y la [promoción de propiedades del constructor |https://blog.nette.org/es/php-8-0-resumen-completo-de-novedades#toc-promocion-de-propiedades-del-constructor], ahora es cuestión de segundos. Las fábricas se pueden generar fácilmente usando Nette DI y el plugin para PhpStorm con un clic del ratón. Por otro lado, desaparece la necesidad de escribir singletons y puntos de acceso estáticos. +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. -Se puede afirmar que una aplicación correctamente diseñada que utiliza DI no es ni más corta ni más larga en comparación con una aplicación que utiliza singletons. Las partes del código que trabajan con dependencias simplemente se extraen de las clases individuales y se mueven a nuevos lugares, es decir, al contenedor DI y a las fábricas. +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 legacy a DI? --------------------------------------------- +¿Cómo reescribir una aplicación heredada para que use DI? +--------------------------------------------------------- -La transición de una aplicación legacy a la Inyección de Dependencias puede ser un proceso desafiante, especialmente para aplicaciones grandes y complejas. Es importante abordar este proceso sistemáticamente. +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 comprendan los principios y procedimientos que se utilizan. -- Primero, realice un análisis de la aplicación existente e identifique 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 gradualmente partes individuales de la aplicación para usar la Inyección de Dependencias. Esto puede incluir modificar constructores o métodos para que acepten dependencias como parámetros. -- Modifique los lugares en el código donde se crean objetos con dependencias para que, en su lugar, las dependencias sean inyectadas por el contenedor. Esto puede incluir el uso de fábricas. +- 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 la transición a la Inyección de Dependencias es una inversión en la calidad del código y la mantenibilidad a largo plazo de la aplicación. Aunque puede ser desafiante realizar estos cambios, el resultado debería ser un código más limpio, modular y fácilmente comprobable, listo para futuras expansiones y mantenimiento. +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 sobre la herencia? ------------------------------------------------------- -Es preferible usar la [composición |nette:introduction-to-object-oriented-programming#Composición] en lugar de la [herencia |nette:introduction-to-object-oriented-programming#Herencia], porque sirve para reutilizar código sin tener que preocuparnos por las consecuencias de los cambios. Proporciona, por tanto, un acoplamiento más flexible, donde no tenemos que temer que un cambio en algún código provoque la necesidad de cambiar otro código dependiente. Un ejemplo típico es la situación conocida como [constructor hell |passing-dependencies#Constructor hell]. +¿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 Nette DI Container fuera de Nette? -------------------------------------------------- +¿Se puede usar el contenedor DI de Nette fuera de Nette? +-------------------------------------------------------- -Definitivamente. Nette DI Container es parte de Nette, pero está diseñado como una biblioteca independiente que se puede usar independientemente de otras partes del framework. Simplemente instálelo usando Composer, cree un archivo de configuración con la definición de sus servicios y luego, usando unas pocas líneas de código PHP, cree el contenedor DI. Y puede comenzar a aprovechar de inmediato las ventajas de la Inyección de Dependencias en sus proyectos. +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. -Cómo se ve el uso concreto, incluidos los códigos, se describe en el capítulo [Nette DI Container |nette-container]. +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 simple y fácil de leer que se desarrolló dentro de Nette para configurar aplicaciones, servicios y sus dependencias. En comparación con JSON o YAML, ofrece opciones mucho más intuitivas y flexibles para este propósito. En NEON, se pueden describir naturalmente las relaciones que en Symfony & YAML serían imposibles de escribir o solo a través de una descripción compleja. +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. -¿No ralentiza la aplicación el análisis de archivos NEON? ---------------------------------------------------------- +¿Ralentiza la aplicación el análisis de los archivos NEON? +---------------------------------------------------------- -Aunque los archivos NEON se analizan muy rápidamente, este aspecto no importa en absoluto. La razón es que el análisis de archivos solo ocurre una vez cuando la aplicación se inicia por primera vez. Luego, se genera el código del contenedor DI, se guarda en el disco y se ejecuta en cada solicitud posterior, sin necesidad de realizar más análisis. +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í es como funciona en un entorno de producción. Durante el desarrollo, los archivos NEON se analizan cada vez que cambia su contenido, para que el desarrollador siempre tenga un contenedor DI actualizado. El análisis en sí, como se dijo, es cuestión de un momento. +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 en el archivo de configuración? ----------------------------------------------------------------------------- +¿Cómo accedo desde mi clase a los parámetros del archivo de configuración? +-------------------------------------------------------------------------- -Tengamos en cuenta la [Regla n.º 1: haz que te lo pasen |introduction#Regla nº 1: deja que te lo pasen]. Si una clase requiere información del archivo de configuración, no tenemos que pensar en cómo acceder a esa información, sino que simplemente la solicitamos, por ejemplo, a través del constructor de la clase. Y realizamos el paso en el 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 de posición para el valor del parámetro `myParameter`, que se pasa al constructor de la clase `MyClass`: +En este ejemplo, `%myParameter%` es un marcador del valor del parámetro `myParameter`, que se pasará al constructor de `MyClass`: -```php +```neon # config.neon parameters: myParameter: Some value @@ -97,10 +97,27 @@ services: - MyClass(%myParameter%) ``` -Si desea pasar múltiples parámetros o usar autowiring, es aconsejable [envolver los parámetros en un objeto |best-practices:passing-settings-to-presenters]. +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.? +------------------------------------------------------------------- -¿Soporta Nette PSR-11: Container interface? -------------------------------------------- +Un breve vocabulario de las palabras que aparecen una y otra vez alrededor de Nette DI, la mayoría al [escribir extensiones |extensions]: -Nette DI Container no soporta PSR-11 directamente. Sin embargo, si necesita interoperabilidad entre Nette DI Container y bibliotecas o frameworks que esperan la Interfaz de Contenedor PSR-11, puede crear un [adaptador simple |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] que sirva como puente entre Nette DI Container y PSR-11. +- **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 index be82e804ba..df0d5d6e03 100644 --- a/dependency-injection/es/global-state.texy +++ b/dependency-injection/es/global-state.texy @@ -2,62 +2,62 @@ Estado global y singletons ************************** .[perex] -Advertencia: Las siguientes construcciones son un signo de código mal diseñado: +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? Entonces tiene la oportunidad de mejorarlo. Quizás piense que son construcciones comunes que ve incluso en soluciones de ejemplo de varias bibliotecas y frameworks. Si es así, entonces el diseño de su código no es bueno. +¿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. -Ahora definitivamente no estamos hablando de algún tipo de pureza académica. Todas estas construcciones tienen una cosa en común: utilizan estado global. Y eso tiene un impacto destructivo en la calidad del código. Las clases mienten sobre sus dependencias. El código se vuelve impredecible. Confunde a los programadores y reduce su eficiencia. +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. -Acoplamiento global -------------------- +Interconexión global +-------------------- -En un mundo ideal, un objeto debería poder comunicarse solo con los objetos que le han sido [pasados directamente |passing-dependencies]. Si creo dos objetos `A` y `B` y nunca paso una referencia entre ellos, entonces ni `A` ni `B` pueden acceder al otro objeto o cambiar su estado. Esta es una propiedad muy deseable del código. Es similar a tener una batería y una bombilla; la bombilla no se encenderá hasta que la conecte a la batería con un cable. +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. -Pero esto no se aplica a las variables globales (estáticas) o singletons. El objeto `A` podría acceder *inalámbricamente* al objeto `C` y modificarlo sin pasar ninguna referencia, llamando a `C::changeSomething()`. Si el objeto `B` también toma el `C` global, entonces `A` y `B` pueden influenciarse mutuamente a través de `C`. +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`. -El uso de variables globales introduce en el sistema una nueva forma de acoplamiento *inalámbrico* que no es visible desde el exterior. Crea una cortina de humo que complica la comprensión y el uso del código. Para que los desarrolladores comprendan realmente las dependencias, deben leer cada línea del código fuente. En lugar de simplemente familiarizarse con la interfaz de las clases. Además, es un acoplamiento completamente innecesario. El estado global se usa porque es fácilmente accesible desde cualquier lugar y permite, por ejemplo, escribir en la base de datos a través del método global (estático) `DB::insert()`. Pero como mostraremos, la ventaja que esto aporta es insignificante, mientras que las complicaciones que causa son fatales. +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] -Desde el punto de vista del comportamiento, no hay diferencia entre una variable global y una estática. Son igualmente dañinas. +En cuanto al comportamiento no hay diferencia entre una variable global y una estática. Son igual de dañinas. -Acción fantasmal a distancia ----------------------------- +La fantasmagórica acción a distancia +------------------------------------ -"Acción fantasmal a distancia" - así llamó famosamente Albert Einstein en 1935 a un fenómeno de la física cuántica que le ponía la piel de gallina. -Se trata del entrelazamiento cuántico, cuya peculiaridad es que cuando mides información sobre una partícula, influyes instantáneamente en la otra partícula, incluso si están separadas por millones de años luz. Lo cual aparentemente viola la ley fundamental del universo de que nada puede propagarse más rápido que la luz. +"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, podemos llamar "acción fantasmal a distancia" a una situación en la que iniciamos un proceso que creemos que está aislado (porque no le pasamos ninguna referencia), pero en lugares remotos del sistema ocurren interacciones y cambios de estado inesperados de los que no teníamos ni idea. Esto solo puede ocurrir a través del estado global. +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 une a un equipo de desarrolladores de un proyecto que tiene una base de código extensa y madura. Su nuevo jefe le pide que implemente una nueva función y usted, como buen desarrollador, comienza escribiendo una prueba. Pero como es nuevo en el proyecto, realiza muchas pruebas exploratorias del tipo "¿qué pasa si llamo a este método?". E intenta escribir la siguiente prueba: +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); // número de su tarjeta + $cc = new CreditCard('1234567890123456', 5, 2028); // el número de su tarjeta $cc->charge(100); } ``` -Ejecuta el código, quizás varias veces, y después de un tiempo nota notificaciones en su móvil del banco de que cada vez que se ejecuta, se cargan 100 dólares a su tarjeta de crédito 🤦‍♂️ +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 diablos pudo la prueba causar un cargo real de dinero? Operar con una tarjeta de crédito no es fácil. Debe comunicarse con un servicio web de terceros, debe conocer la URL de este servicio web, debe iniciar sesión, etc. Ninguna de esta información está contenida en la prueba. Peor aún, ni siquiera sabe dónde está presente esta información y, por lo tanto, tampoco cómo simular (mock) las dependencias externas para que cada ejecución no resulte en que se carguen nuevamente 100 dólares. ¿Y cómo se suponía que usted, como nuevo desarrollador, supiera que lo que estaba a punto de hacer le haría 100 dólares más pobre? +¿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 acción fantasmal a distancia! +¡Eso es una fantasmagórica acción a distancia! -No le queda más remedio que rebuscar durante mucho tiempo en un montón de código fuente, preguntar a colegas más antiguos y experimentados, antes de comprender cómo funcionan las conexiones en el proyecto. Esto se debe a que al mirar la interfaz de la clase `CreditCard`, no se puede determinar el estado global que debe inicializarse. Incluso mirar el código fuente de la clase no le dice qué método de inicialización debe llamar. En el mejor de los casos, puede encontrar una variable global a la que se accede y, a partir de ella, intentar adivinar cómo inicializarla. +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 en tal proyecto son mentirosas patológicas. La tarjeta de crédito finge que basta con instanciarla y llamar al método `charge()`. En secreto, sin embargo, colabora con otra clase `PaymentGateway`, que representa la pasarela de pago. Incluso su interfaz dice que se puede inicializar por separado, pero en realidad extrae credenciales de algún archivo de configuración, etc. Para los desarrolladores que escribieron este código, está claro que `CreditCard` necesita `PaymentGateway`. Escribieron el código de esta manera. Pero para cualquiera que sea nuevo en el proyecto, es un completo misterio y dificulta el aprendizaje. +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ácilmente. **Deje que la API declare las dependencias.** +¿Cómo arreglar la situación? Fácil. **Deje que la API declare las dependencias.** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -Observe cómo de repente las interconexiones dentro del código son obvias. Al declarar el método `charge()` que necesita `PaymentGateway`, no tiene que preguntar a nadie cómo está interconectado el código. Sabe que debe crear su instancia, y cuando intenta hacerlo, se da cuenta de que debe proporcionar los parámetros de acceso. Sin ellos, el código ni siquiera se ejecutaría. +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 (mock) la pasarela de pago, para que no se le cobren 100 dólares cada vez que ejecute la prueba. +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 hace que sus objetos puedan acceder en secreto a cosas que no están declaradas en su API y, como resultado, convierten sus API en mentirosos patológicos. +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. -Quizás no lo había pensado así antes, pero cada vez que usa estado global, está creando canales de comunicación inalámbricos secretos. La acción fantasmal a distancia obliga a los desarrolladores a leer cada línea de código para comprender las interacciones potenciales, reduce la productividad de los desarrolladores 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 de usted está perdido. +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. -No escriba código que utilice estado global, dé preferencia al paso de dependencias. Es decir, inyección de dependencias. +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 el código que utiliza estado global y singletons, nunca se sabe cuándo y quién cambió este estado. Este riesgo aparece ya durante la inicialización. El siguiente código debe crear una conexión a la base de datos e inicializar la pasarela de pago, pero constantemente lanza una excepción y encontrar la causa es extremadamente tedioso: +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'); ``` -Debe examinar detenidamente el código para descubrir que el objeto `PaymentGateway` accede de forma inalámbrica a otros objetos, algunos de los cuales requieren una conexión a la base de datos. Por lo tanto, es necesario inicializar la base de datos antes que `PaymentGateway`. Sin embargo, la cortina de humo del estado global le oculta esto. ¿Cuánto tiempo habría ahorrado si la API de las clases individuales no engañara y declarara sus dependencias? +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, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -Un problema similar surge también al usar acceso global a la conexión de la base de datos: +Un problema parecido surge al usar un acceso global a la conexión de la base de datos: ```php use Illuminate\Support\Facades\DB; @@ -110,9 +110,9 @@ class Article } ``` -Al llamar al método `save()`, no está claro si ya se ha creado la conexión a la base de datos y quién es responsable de su creación. Si quisiéramos, por ejemplo, cambiar la conexión a la base de datos sobre la marcha, por ejemplo, para pruebas, probablemente tendríamos que crear métodos adicionales como `DB::reconnect(...)` o `DB::reconnectForTest()`. +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()`. -Consideremos un ejemplo: +Considere un ejemplo: ```php $article = new Article; @@ -122,7 +122,7 @@ Foo::doSomething(); $article->save(); ``` -¿Dónde tenemos la certeza de que al llamar a `$article->save()` se está utilizando realmente la base de datos de prueba? ¿Qué pasa si el método `Foo::doSomething()` cambió la conexión global a la base de datos? Para averiguarlo, tendríamos que examinar el código fuente de la clase `Foo` y probablemente de muchas otras clases. Sin embargo, este enfoque solo proporcionaría una respuesta a corto plazo, ya que la situación puede cambiar en el futuro. +¿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`? @@ -143,11 +143,11 @@ class Article } ``` -Esto no cambia nada en absoluto. El problema es el estado global y es completamente irrelevante en qué clase se esconde. En este caso, al igual que en el anterior, al llamar al método `$article->save()` no tenemos ninguna pista sobre en qué base de datos se escribirá. Cualquiera en el otro extremo de la aplicación podría haber cambiado la base de datos en cualquier momento usando `Article::setDb()`. Bajo nuestras narices. +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 que nuestra aplicación sea **extremadamente frágil**. +El estado global hace nuestra aplicación **extremadamente frágil**. -Sin embargo, existe una forma sencilla de abordar este problema. Simplemente deje que la API declare las dependencias, lo que garantizará la funcionalidad correcta. +Pero hay una forma sencilla de tratar este problema. Basta con que la API declare las dependencias necesarias para funcionar correctamente. ```php class Article @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -Gracias a este enfoque, desaparece la preocupación por cambios ocultos e inesperados en la conexión a la base de datos. Ahora tenemos la certeza de dónde se guarda el artículo y ninguna modificación del código dentro de otra clase no relacionada puede cambiar la situación. El código ya no es frágil, sino estable. +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. -No escriba código que utilice estado global, dé preferencia al paso de dependencias. Es decir, inyección de dependencias. +Evite escribir código que dependa del estado global; prefiera pasar las dependencias explícitamente. Adopte la inyección de dependencias. Singleton --------- -Singleton es un patrón de diseño que, según la "definición":https://en.wikipedia.org/wiki/Singleton_pattern de la conocida publicación Gang of Four, restringe una clase a una única instancia y ofrece acceso global a ella. La implementación de este patrón generalmente se asemeja al siguiente código: +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 @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // y otros métodos que cumplen las funciones de la clase dada + // y otros métodos que realizan las funciones de la clase } ``` -Desafortunadamente, singleton introduce estado global en la aplicación. Y como hemos mostrado anteriormente, el estado global es indeseable. Por lo tanto, singleton se considera un antipatrón. +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 reemplácelos con otros mecanismos. Realmente no necesita singletons. Sin embargo, si necesita garantizar la existencia de una única instancia de una clase para toda la aplicación, déjelo en manos del [contenedor DI |container]. Cree así un singleton de aplicación, o servicio. De esta manera, la clase dejará de ocuparse de garantizar su propia unicidad (es decir, no tendrá el método `getInstance()` ni la variable estática) y cumplirá solo sus funciones. Así dejará de violar el principio de responsabilidad única. +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. -Estado global versus pruebas ----------------------------- +El estado global frente a los tests +----------------------------------- -Al escribir pruebas, asumimos que cada prueba es una unidad aislada y que ningún estado externo entra en ella. Y ningún estado sale de las pruebas. Después de completar una prueba, todo el estado relacionado con la prueba debería ser eliminado automáticamente por el recolector de basura. Gracias a esto, las pruebas están aisladas. Por lo tanto, podemos ejecutar las pruebas en cualquier orden. +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. -Sin embargo, si hay estados globales/singletons presentes, todas estas agradables suposiciones se desmoronan. El estado puede entrar y salir de la prueba. De repente, el orden de las pruebas puede importar. +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 probar los singletons, los desarrolladores a menudo tienen que relajar sus propiedades, por ejemplo, permitiendo que la instancia sea reemplazada por otra. Tales soluciones son, en el mejor de los casos, un hack que crea código difícil de mantener y comprender. Cada prueba o método `tearDown()` que afecte a cualquier estado global debe revertir estos cambios. +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 dolor de cabeza en las pruebas unitarias! +¡El estado global es el mayor quebradero de cabeza de las pruebas unitarias! -¿Cómo arreglar la situación? Fácilmente. No escriba código que utilice singletons, dé preferencia al paso de dependencias. Es decir, inyección de dependencias. +¿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 solo al uso de singletons y variables estáticas, sino que también puede referirse a 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 cuyo valor no nos aporta ninguna información nueva (`M_PI`) o útil (`PREG_BACKTRACK_LIMIT_ERROR`) están claramente bien. Por el contrario, las constantes que sirven como una forma de pasar información *inalámbricamente* al código no son más que una dependencia oculta. Como `LOG_FILE` en el siguiente ejemplo. El uso de la constante `FILE_APPEND` es completamente correcto. +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 = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -En este caso, deberíamos declarar un parámetro en el constructor de la clase `Foo` para que se convierta en parte de la API: +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 @@ -253,42 +253,42 @@ class Foo } ``` -Ahora podemos pasar la información sobre la ruta al archivo de registro y cambiarla fácilmente según sea necesario, lo que facilita las pruebas y el mantenimiento del código. +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 enfatizar que el uso de métodos estáticos y funciones globales en sí mismo no es problemático. Explicamos por qué el uso de `DB::insert()` y métodos similares es inapropiado, pero siempre se trató solo de una cuestión de estado global, que se almacena en alguna variable estática. El método `DB::insert()` requiere la existencia de una variable estática porque la conexión a la base de datos se almacena en ella. Sin esta variable, sería imposible implementar el método. +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. -El uso de métodos estáticos y funciones deterministas, como `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` y muchas otras, está en perfecta consonancia con la inyección de dependencias. Estas funciones siempre devuelven los mismos resultados para los mismos parámetros de entrada y, por lo tanto, son predecibles. No utilizan ningún estado global. +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. -Sin embargo, también existen funciones en PHP que no son deterministas. Entre ellas se encuentra, por ejemplo, la función `htmlspecialchars()`. Su tercer parámetro `$encoding`, si no se especifica, tiene como valor predeterminado el valor de la opción de configuración `ini_get('default_charset')`. Por lo tanto, se recomienda especificar siempre este parámetro y evitar así un posible comportamiento impredecible de la función. Nette lo hace consistentemente. +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()`, `strtoupper()` y similares, en el pasado reciente se comportaron de forma no determinista y dependían de la configuración de `setlocale()`. Esto causó muchas complicaciones, más comúnmente al trabajar con el idioma turco. Este distingue entre las letras `I` mayúscula y minúscula con y sin punto. Así que `strtolower('I')` devolvía el carácter `ı` y `strtoupper('i')` el carácter `İ`, lo que provocó que las aplicaciones comenzaran a causar una serie de errores misteriosos. Sin embargo, este problema se solucionó en la versión 8.2 de PHP y las funciones ya no dependen de la configuración regional (locale). +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. -Este es un buen ejemplo de cómo el estado global atormentó a miles de desarrolladores en todo el mundo. La solución fue reemplazarlo por inyección de dependencias. +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? -------------------------------------- -Existen ciertas situaciones específicas en las que es posible utilizar el estado global. Por ejemplo, al depurar código, cuando necesita imprimir el valor de una variable o medir la duración de una parte específica del programa. En tales casos, que se refieren a acciones temporales que luego se eliminarán del código, es legítimo utilizar un dumper o cronómetro globalmente disponible. Estas herramientas no forman parte del diseño del código. +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 para trabajar con expresiones regulares `preg_*`, que almacenan internamente las expresiones regulares compiladas en una caché estática en memoria. Por lo tanto, cuando llama a la misma expresión regular varias veces en diferentes lugares del código, solo se compila una vez. La caché ahorra rendimiento y, al mismo tiempo, es completamente invisible para el usuario, por lo que dicho uso puede considerarse legítimo. +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 discutido por qué tiene sentido: +Hemos hablado de por qué tiene sentido: -1) Eliminar todas las variables estáticas del código -2) Declarar dependencias -3) Y usar inyección de dependencias +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 pensar en el diseño del código, tenga en cuenta que cada `static $foo` representa un problema. Para que su código sea un entorno que respete DI, es esencial erradicar por completo el estado global y reemplazarlo mediante 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 este proceso, puede descubrir que es necesario dividir la clase porque tiene más de una responsabilidad. No tenga miedo de eso; esfuércese por el principio de responsabilidad única. +Durante ese proceso quizá descubra la necesidad de dividir clases que tienen varias responsabilidades. No lo dude; aspire al principio de responsabilidad única. -*Me gustaría agradecer 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.* +*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 index 8655556c51..e74833dd90 100644 --- a/dependency-injection/es/introduction.texy +++ b/dependency-injection/es/introduction.texy @@ -1,58 +1,58 @@ -¿Qué es la Inyección de Dependencias? +¿Qué es la inyección de dependencias? ************************************* .[perex] -Este capítulo le introducirá en las prácticas básicas de programación que debe seguir al escribir todas las aplicaciones. Estos son los fundamentos necesarios para escribir código limpio, comprensible y mantenible. +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 estas reglas y las sigue, Nette le ayudará en cada paso del camino. Se encargará de las tareas rutinarias por usted y le proporcionará la máxima comodidad, para que pueda centrarse en la lógica en sí. +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 simples. No tiene que preocuparse por nada. +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 PHP, probablemente se parecía a esto: +No sabemos en qué lenguaje lo escribió, pero si fue en PHP, probablemente tenía este aspecto: ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // imprime 24 +echo addition(23, 1); // imprime 24 ``` -Unas pocas líneas triviales de código, pero contienen muchos conceptos clave. Que existen variables. Que el código se divide en unidades más pequeñas, como funciones. Que les pasamos argumentos de entrada y devuelven resultados. Solo faltan las condiciones y los bucles. +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. -El hecho de que pasemos datos de entrada a una función y esta devuelva un resultado es un concepto perfectamente comprensible que se utiliza en otros campos, como las matemáticas. +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, que consiste en su nombre, una lista de parámetros y sus tipos, y finalmente el tipo de valor de retorno. Como usuarios, nos interesa la firma, normalmente no necesitamos saber nada sobre la implementación interna. +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 fuera así: +Ahora imagine que la firma de la función tuviera este aspecto: ```php -function soucet(float $x): float +function addition(float $x): float ``` -¿Una suma con un solo parámetro? Eso es extraño... ¿Y qué tal así? +¿Una suma con un solo parámetro? Qué raro… ¿Y qué tal esta? ```php -function soucet(): float +function addition(): float ``` -Esto ya es realmente muy extraño, ¿verdad? ¿Cómo se usaría la función? +Ahora sí que es raro, ¿verdad? ¿Cómo se usa esta función? ```php -echo soucet(); // ¿qué imprimirá? +echo addition(); // ¿qué imprimirá? ``` -Al ver tal código, estaríamos confundidos. No solo un principiante no lo entendería, sino que incluso un programador experimentado no comprendería este código. +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 cómo sería realmente tal función por dentro? ¿De dónde obtendría los sumandos? Aparentemente, los obtendría *de alguna manera* por sí misma, tal vez así: +¿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 soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -En el cuerpo de la función, descubrimos dependencias ocultas a otras funciones globales o métodos estáticos. Para averiguar de dónde provienen realmente los sumandos, debemos investigar más a fondo. +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 aquí no! -------------- +¡Por ahí no! +------------ -El diseño que acabamos de mostrar es la esencia de muchas características negativas: +El diseño que acabamos de mostrar es la esencia de muchas propiedades negativas: -- la firma de la función pretendía no necesitar sumandos, lo que nos confundió -- no sabemos en absoluto cómo hacer que la función sume otros dos números -- tuvimos que mirar el código para averiguar de dónde obtenía los sumandos -- descubrimos dependencias ocultas -- para una comprensión completa, también es necesario examinar estas dependencias +- 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 siquiera tarea de la función de suma obtener las entradas? Por supuesto que no. Su responsabilidad es únicamente la suma en sí. +¿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 encontrarnos con tal código, y definitivamente no queremos escribirlo. La solución es simple: volver a lo básico y simplemente usar parámetros: +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 soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -Regla nº 1: deja que te lo pasen --------------------------------- +Regla n.º 1: deje que se lo pasen +--------------------------------- -La regla más importante es: **todos los datos que las funciones o clases necesitan deben serles pasados**. +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 para que puedan obtenerlos por sí mismos, simplemente pase los parámetros. Ahorrará tiempo necesario para inventar caminos ocultos, que definitivamente no mejorarán su código. +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, estará en camino hacia un código sin dependencias ocultas. Hacia un código que sea comprensible no solo para el autor, sino también para cualquiera que lo lea después. Donde todo es comprensible a partir de las firmas de funciones y clases y no es necesario buscar secretos ocultos en la implementación. +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. -Esta técnica se llama técnicamente **inyección de dependencias**. Y esos datos se llaman **dependencias.** Sin embargo, es simplemente pasar parámetros, nada más. +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] -Por favor, no confunda la inyección de dependencias, que es un patrón de diseño, con el "contenedor de inyección de dependencias", que es una herramienta, es decir, algo diametralmente diferente. Hablaremos de los contenedores más adelante. +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 funciones a clases ---------------------- +De las funciones a las clases +----------------------------- -¿Y cómo se relacionan las clases con esto? Una clase es una unidad más compleja que una simple función, sin embargo, la regla nº 1 se aplica aquí sin excepción. Solo que hay [más formas de pasar argumentos |passing-dependencies]. Por ejemplo, de manera bastante similar al caso de una función: +¿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 Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` O mediante otros métodos, o directamente el constructor: ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,26 +132,25 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` -Ambos ejemplos están completamente en línea con la inyección de dependencias. +Ambos ejemplos cumplen plenamente con la inyección de dependencias. -Ejemplos reales ---------------- +Ejemplos de la vida real +------------------------ -En el mundo real, no escribirá clases para sumar números. Pasemos a ejemplos prácticos. +En el mundo real no escribirá clases para sumar números. Pasemos a ejemplos prácticos. -Tengamos una clase `Article` que represente un artículo de blog: +Tengamos una clase `Article` que representa un artículo de un blog: ```php class Article @@ -162,12 +161,12 @@ class Article public function save(): void { - // guardamos el artículo en la base de datos + // guarda el artículo en la base de datos } } ``` -y el uso será el siguiente: +y su uso será el siguiente: ```php $article = new Article; @@ -176,9 +175,9 @@ $article->content = 'Every year millions of people in ...'; $article->save(); ``` -El método `save()` guarda el artículo en una tabla de la base de datos. Implementarlo usando [Nette Database |database:] sería pan comido, si no fuera por un pequeño inconveniente: ¿de dónde obtiene `Article` la conexión a la base de datos, es decir, el objeto de la clase `Nette\Database\Connection`? +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. Puede tomarla de alguna variable estática. O heredar de una clase que proporcione la conexión a la base de datos. O usar el llamado [singleton |global-state#Singleton]. O las llamadas facades, que se usan en Laravel: +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; @@ -203,13 +202,13 @@ Genial, hemos resuelto el problema. ¿O no? -Recordemos la [##Regla nº 1: deja que te 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, incomprensibilidad, y el resultado será una aplicación que será doloroso mantener y desarrollar. +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 idea de dónde guarda el artículo el método `save()`. ¿En una tabla de base de datos? ¿En cuál, la de producción o la de prueba? ¿Y cómo se puede cambiar eso? +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 debe mirar cómo está implementado el método `save()` y encuentra el uso del método `DB::insert()`. Así que debe investigar más a fondo cómo este método obtiene la conexión a la base de datos. Y las dependencias ocultas pueden formar una cadena bastante larga. +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, facades de Laravel o variables estáticas. En un código limpio y bien diseñado, se pasan argumentos: +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 @@ -224,7 +223,7 @@ class Article } ``` -Aún más práctico, como veremos más adelante, será mediante el constructor: +Aún más práctico, como veremos más adelante, es usar el constructor: ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -Si es un programador experimentado, quizás piense que `Article` no debería tener un método `save()` en absoluto, debería representar un componente puramente de datos y un repositorio separado debería encargarse del almacenamiento. Eso tiene sentido. Pero eso nos llevaría mucho más allá del alcance del tema, que es la inyección de dependencias, y el esfuerzo por dar ejemplos simples. +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 requiere, por ejemplo, una base de datos para su funcionamiento, no piense de dónde obtenerla, sino deje que se la pasen. Tal vez como parámetro del constructor u otro método. Admita las dependencias. Admítalas en la API de su clase. Obtendrá un código comprensible y predecible. +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?: +¿Y qué tal esta clase, que registra mensajes de error? ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,23 +261,23 @@ class Logger } ``` -¿Qué piensa, hemos seguido la [##Regla nº 1: deja que te lo pasen]? +¿Qué opina, hemos seguido la [#Regla n.º 1: deje que se lo pasen]? -No lo hemos hecho. +No la hemos seguido. -La información clave, es decir, el directorio con el archivo de registro, la clase la *obtiene por sí misma* de una constante. +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('La temperatura es 23 °C'); -$logger->log('La temperatura es 10 °C'); +$logger->log('Temperature is 23 °C'); +$logger->log('Temperature is 10 °C'); ``` -Sin conocer la implementación, ¿podría responder a la pregunta de dónde se escriben los mensajes? ¿Se le ocurriría que para funcionar es necesaria la existencia de la constante `LOG_DIR`? ¿Y podría crear una segunda instancia que escriba en otro lugar? Seguramente no. +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. -Corrijamos la clase: +Arreglemos la clase: ```php class Logger @@ -295,24 +294,24 @@ class Logger } ``` -La clase ahora es mucho más comprensible, configurable y, por lo tanto, más útil. +La clase es ahora mucho más comprensible, configurable y, por tanto, más útil. ```php -$logger = new Logger('/ruta/al/log.txt'); -$logger->log('La temperatura es 15 °C'); +$logger = new Logger('/path/to/log.txt'); +$logger->log('Temperature is 15 °C'); ``` -¡Pero eso no me interesa! -------------------------- +¡Pero a mí eso me da igual! +--------------------------- -*„Cuando creo un objeto Article y llamo a save(), no quiero ocuparme de la base de datos, simplemente quiero que se guarde en la que tengo configurada.“* +*"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, simplemente quiero que el mensaje se escriba, y no quiero preocuparme por dónde. Que se use la configuración global.“* +*"Cuando uso Logger, solo quiero que se escriba el mensaje y no quiero ocuparme de dónde. Que se use la configuración global."* -Estos son comentarios válidos. +Son observaciones válidas. -Como ejemplo, mostraremos una clase que envía boletines informativos y registra cómo fue: +Como ejemplo, mostremos una clase que distribuye boletines y registra el resultado: ```php class NewsletterDistributor @@ -322,21 +321,21 @@ class NewsletterDistributor $logger = new Logger(/* ... */); try { $this->sendEmails(); - $logger->log('Los correos electrónicos fueron enviados'); + $logger->log('Emails have been sent out'); } catch (Exception $e) { - $logger->log('Ocurrió un error al enviar'); + $logger->log('An error occurred during sending'); throw $e; } } } ``` -El `Logger` mejorado, que ya no usa la constante `LOG_DIR`, requiere que se especifique la ruta del archivo en el constructor. ¿Cómo resolver esto? A la clase `NewsletterDistributor` no le importa en absoluto dónde se escriben los mensajes, solo quiere escribirlos. +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 nuevamente la [##Regla nº 1: deja que te lo pasen]: todos los datos que la clase necesita, se los pasamos. +La solución es de nuevo la [#Regla n.º 1: deje que se lo pasen]: le pasamos todos los datos que la clase necesita. -Entonces, ¿eso significa que pasamos la ruta del registro a través del constructor, que luego usamos al crear el objeto `Logger`? +¿Significa eso que pasamos la ruta del registro por el constructor y la usamos después al crear el objeto `Logger`? ```php class NewsletterDistributor @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -¡Así no! La ruta **no pertenece** a los datos que la clase `NewsletterDistributor` necesita; esos los necesita `Logger`. ¿Percibe la diferencia? La clase `NewsletterDistributor` necesita el logger como tal. Así que eso es lo que pasaremos: +¡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 @@ -365,33 +364,33 @@ class NewsletterDistributor { try { $this->sendEmails(); - $this->logger->log('Los correos electrónicos fueron enviados'); + $this->logger->log('Emails have been sent out'); } catch (Exception $e) { - $this->logger->log('Ocurrió un error al enviar'); + $this->logger->log('An error occurred during sending'); throw $e; } } } ``` -Ahora está claro a partir de las firmas de la clase `NewsletterDistributor` que el registro es parte de su funcionalidad. Y la tarea de cambiar el logger por otro, por ejemplo, para pruebas, es completamente trivial. Además, si el constructor de la clase `Logger` cambiara, no tendría ningún efecto en nuestra clase. +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: toma lo que es tuyo -------------------------------- +Regla n.º 2: tome lo que es suyo +-------------------------------- -No se deje engañar y no deje que le pasen las dependencias de sus dependencias. Deje que le pasen solo sus dependencias. +No se líe y no acepte las dependencias de sus dependencias. Acepte solo las suyas propias. -Gracias a esto, el código que utiliza otros objetos será completamente independiente de los cambios en sus constructores. Su API será más veraz. Y, sobre todo, será trivial reemplazar estas dependencias por otras. +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. -Nuevo miembro de la familia ---------------------------- +Un nuevo miembro de la familia +------------------------------ -En el equipo de desarrollo, se decidió crear un segundo logger que escriba en la base de datos. Por lo tanto, crearemos la clase `DatabaseLogger`. Así que tenemos dos clases, `Logger` y `DatabaseLogger`, una escribe en un archivo, la otra en la base de datos... ¿no le parece algo extraño en el nombre? ¿No sería mejor renombrar `Logger` a `FileLogger`? Definitivamente sí. +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 lo haremos inteligentemente. Crearemos una interfaz con el nombre original: +Pero hagámoslo con cabeza. Creamos una interfaz con el nombre original: ```php interface Logger @@ -400,7 +399,7 @@ interface Logger } ``` -... que ambos loggers implementarán: +… que ambos loggers implementarán: ```php class FileLogger implements Logger @@ -410,17 +409,17 @@ class DatabaseLogger implements Logger // ... ``` -Y gracias a esto, no será necesario cambiar nada en el resto del código donde se utiliza el logger. Por ejemplo, el constructor de la clase `NewsletterDistributor` seguirá estando satisfecho con requerir `Logger` como parámetro. Y dependerá de nosotros qué instancia le pasemos. +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 damos a los nombres de las interfaces el sufijo `Interface` o el prefijo `I`.** De lo contrario, no sería posible desarrollar el código de esta manera tan agradable. +**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 podemos arreglárnoslas con una única instancia de logger, ya sea de archivo o de base de datos, y simplemente pasarla a donde sea que algo se registre, la situación es bastante diferente en el caso de la clase `Article`. Creamos sus instancias según sea necesario, incluso varias veces. ¿Cómo lidiar con la dependencia de la base de datos en su constructor? +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? -Como ejemplo puede servir un controlador que, después de enviar un formulario, debe guardar el artículo en la base de datos: +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 @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -Una posible solución se presenta directamente: dejamos que el objeto de la base de datos se pase mediante el constructor a `EditController` y usamos `$article = new Article($this->db)`. +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)`. -Al igual que en el caso anterior con `Logger` y la ruta al archivo, este no es el procedimiento correcto. La base de datos no es una dependencia de `EditController`, sino de `Article`. Pasar la base de datos va en contra de la [##Regla nº 2: toma lo que es tuyo]. Cuando cambie el constructor de la clase `Article` (se agregue un nuevo parámetro), será necesario modificar también el código en todos los lugares donde se creen instancias. Ufff. +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é sugieres? +Houston, ¿qué propone? -Regla nº 3: déjalo en manos de la fábrica ------------------------------------------ +Regla n.º 3: déjelo en manos de la factory +------------------------------------------ -Al eliminar las dependencias ocultas y pasar todas las dependencias como argumentos, hemos obtenido clases más configurables y flexibles. Y, por lo tanto, necesitamos algo más que cree y configure esas clases más flexibles para nosotros. Lo llamaremos fábricas. +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, deja la creación de sus instancias en manos de una fábrica. +La regla es: si una clase tiene dependencias, delegue la creación de sus instancias en una factory. -Las fábricas son un reemplazo más inteligente del operador `new` en el mundo de la inyección de dependencias. +Las factories son una alternativa más inteligente al operador `new` en el mundo de la inyección de dependencias. .[note] -Por favor, no confunda con el patrón de diseño *factory method*, que describe una forma específica de usar fábricas y no está relacionado con este tema. +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. -Fábrica +Factory ------- -Una fábrica es un método o clase que produce y configura objetos. La clase que produce `Article` la llamaremos `ArticleFactory` y podría verse así, por ejemplo: +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 @@ -487,7 +486,7 @@ class EditController extends Controller public function formSubmitted($data) { - // dejamos que la fábrica cree el objeto + // dejamos que la factory cree el objeto $article = $this->articleFactory->create(); $article->title = $data->title; $article->content = $data->content; @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -Si en este momento cambia la firma del constructor de la clase `Article`, la única parte del código que debe reaccionar es la propia fábrica `ArticleFactory`. Todo el código adicional que trabaja con objetos `Article`, como `EditController`, no se verá afectado de ninguna manera. +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. -Quizás ahora se esté golpeando la frente, preguntándose si realmente hemos mejorado algo. La cantidad de código ha aumentado y todo comienza a parecer sospechosamente complicado. +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. -No se preocupe, pronto llegaremos al contenedor DI de Nette. Y este tiene varios ases bajo la manga que simplificarán enormemente la construcción de aplicaciones que utilizan inyección de dependencias. Por ejemplo, en lugar de la clase `ArticleFactory`, será suficiente [escribir solo una interfaz |factory]: +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 @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -Pero nos estamos adelantando, espere un poco más :-) +Pero nos estamos adelantando, siga atento :-) Resumen ------- -Al comienzo de este capítulo, prometimos mostrar un método para diseñar código limpio. Basta con que las clases +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: deja que te lo pasen] -2) [y, por el contrario, no reciban lo que no necesitan directamente |#Regla nº 2: toma lo que es tuyo] -3) [y que los objetos con dependencias se fabriquen mejor en fábricas |#Regla nº 3: déjalo en manos de la fábrica] +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] -Puede que no lo parezca a primera vista, pero estas tres reglas tienen consecuencias de gran alcance. Conducen a una visión radicalmente diferente del diseño de código. ¿Vale la pena? Los programadores que abandonaron viejos hábitos y comenzaron a usar consistentemente la inyección de dependencias consideran este paso un momento crucial en sus vidas profesionales. Se les abrió un mundo de aplicaciones claras y mantenibles. +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. -Pero, ¿qué pasa si el código no utiliza consistentemente la inyección de dependencias? ¿Qué pasa si se basa en métodos estáticos o singletons? ¿Trae algún problema? [Sí, y muy fundamentales |global-state]. +¿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 index 3839e5515b..51843f43d1 100644 --- a/dependency-injection/es/nette-container.texy +++ b/dependency-injection/es/nette-container.texy @@ -2,9 +2,9 @@ Contenedor DI de Nette ********************** .[perex] -Nette DI es una de las librerías más interesantes de Nette. Puede generar y actualizar automáticamente contenedores DI compilados, que son extremadamente rápidos y sorprendentemente fáciles de configurar. +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 debe crear el contenedor DI se define generalmente mediante archivos de configuración en [formato NEON|neon:format]. El contenedor que creamos manualmente en el [capítulo anterior|container] se escribiría así: +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: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -La notación es realmente concisa. +La sintaxis es muy concisa. -Todas las dependencias declaradas en los constructores de las clases `ArticleFactory` y `UserController` son detectadas y pasadas automáticamente por Nette DI gracias al llamado [autowiring|autowiring], por lo que no es necesario especificar nada en el archivo de configuración. Así que incluso si los parámetros cambian, no necesita cambiar nada en la configuración. Nette regenerará automáticamente el contenedor. Puede concentrarse puramente en el desarrollo de la aplicación. +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 dependencias mediante setters, usamos la sección [setup |services#Setup] para ello. +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 estudiar. Gracias a esto, puede ver exactamente cómo funciona el contenedor. También puede depurarlo en su IDE y recorrerlo paso a paso. Y lo más importante: el PHP generado es extremadamente rápido. +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 código de [fábricas|factory] basándose en la interfaz proporcionada. Por lo tanto, en lugar de la clase `ArticleFactory`, solo necesitaremos crear una interfaz en la aplicación: +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 @@ -36,19 +36,19 @@ interface ArticleFactory } ``` -Puede encontrar el ejemplo completo [en GitHub|https://github.com/nette-examples/di-example-doc]. +Encontrará el ejemplo completo [en GitHub|https://github.com/nette-examples/di-example-doc]. Uso independiente ----------------- -Implementar la librería Nette DI en una aplicación es muy fácil. Primero, la instalamos con Composer (porque descargar zips es taaan anticuado): +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 crea una instancia del contenedor DI según la configuración almacenada en el archivo `config.neon`: +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'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -El contenedor se genera solo una vez, su código se escribe en la caché (directorio `__DIR__ . '/temp'`) y en las siguientes peticiones simplemente se carga desde allí. +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í. -Para crear y obtener servicios, se utilizan los métodos `getService()` o `getByType()`. Así creamos el objeto `UserController`: +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 -$controller = $container->getByType(UserController::class); +$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, es útil activar el modo de auto-refresco, donde el contenedor se regenera automáticamente si se cambia alguna clase o archivo de configuración. Simplemente proporcione `true` como segundo argumento en el constructor de `ContainerLoader`. +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); ``` -Uso con el framework Nette +Trabajar con el contenedor -------------------------- -Como hemos mostrado, el uso de Nette DI no está limitado a aplicaciones escritas en Nette Framework, puede implementarlo en cualquier lugar con solo 3 líneas de código. Sin embargo, si desarrolla aplicaciones en Nette Framework, la configuración y creación del contenedor está a cargo de [Bootstrap |application:bootstrapping#Configuración del contenedor DI]. +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 index 3640c239aa..2b969a2217 100644 --- a/dependency-injection/es/passing-dependencies.texy +++ b/dependency-injection/es/passing-dependencies.texy @@ -3,22 +3,22 @@ Paso de dependencias <div class=perex> -Los argumentos, o en la terminología de DI "dependencias", se pueden pasar a las clases de las siguientes maneras principales: +Los argumentos, o "dependencias" en la terminología de la DI, se pueden pasar a las clases de las siguientes maneras principales: -* paso por constructor -* paso por método (llamado setter) -* asignación a variable -* mediante método, anotación o atributo *inject* +* 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> -Ahora mostraremos las variantes individuales con ejemplos concretos. +Mostremos cada variante con ejemplos concretos. -Paso por constructor -==================== +Inyección por constructor +========================= -Las dependencias se pasan en el momento de la creación del objeto como argumentos del constructor: +Las dependencias se proporcionan como argumentos del constructor en el momento de crear el objeto: ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -Esta forma es adecuada para dependencias obligatorias que la clase necesita indispensablemente para su función, ya que sin ellas no se podrá crear la instancia. +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 forma de escritura más corta ([constructor property promotion |https://blog.nette.org/es/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), que es funcionalmente equivalente: +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 @@ -49,7 +49,7 @@ class MyClass } ``` -Desde PHP 8.1, se puede marcar la variable con el flag `readonly`, que declara que el contenido de la variable ya no cambiará: +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 @@ -62,13 +62,13 @@ class MyClass } ``` -El contenedor DI pasa las dependencias al constructor automáticamente mediante [autowiring |autowiring]. Los argumentos que no se pueden pasar de esta manera (por ejemplo, cadenas, números, booleanos) [se escriben en la configuración |services#Argumentos]. +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]. -Constructor hell ----------------- +Infierno del constructor +------------------------ -El término *constructor hell* se refiere a la situación en la que un descendiente hereda de una clase padre cuyo constructor requiere dependencias, y al mismo tiempo el descendiente requiere dependencias. Además, debe recibir y pasar también las del padre: +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 @@ -85,7 +85,7 @@ final class MyClass extends BaseClass { private Database $db; - // ⛔ CONSTRUCTOR HELL + // ⛔ INFIERNO DEL CONSTRUCTOR public function __construct(Cache $cache, Database $db) { parent::__construct($cache); @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -El problema surge en el momento en que queremos cambiar el constructor de la clase `BaseClass`, por ejemplo, cuando se agrega una nueva dependencia. Entonces es necesario modificar también todos los constructores de los descendientes. Lo que convierte tal modificación en un infierno. +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 prevenir esto? La solución es **dar preferencia a la [composición sobre la herencia |faq#Por qué se prefiere la composición sobre la herencia]**. +¿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?]**. -Es decir, diseñaremos el código de manera diferente. Evitaremos las clases [abstractas |nette:introduction-to-object-oriented-programming#Clases abstractas] `Base*`. En lugar de que `MyClass` obtenga cierta funcionalidad heredando de `BaseClass`, dejará que esta funcionalidad se le pase como dependencia: +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 @@ -125,10 +125,10 @@ final class MyClass ``` -Paso por setter -=============== +Inyección por setter +==================== -Las dependencias se pasan llamando a un método que las almacena en una variable privada. La convención habitual para nombrar estos métodos es la forma `set*()`, por eso se les llama setters, pero por supuesto pueden llamarse de cualquier otra manera. +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 @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -Este método es adecuado para dependencias opcionales que no son necesarias para la función de la clase, ya que no se garantiza que el objeto reciba realmente la dependencia (es decir, que el usuario llame al método). +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 y así cambiar la dependencia. Si esto no es deseable, agregamos una verificación al método, o desde PHP 8.1 marcamos la propiedad `$cache` con el flag `readonly`. +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 @@ -157,14 +157,14 @@ class MyClass public function setCache(Cache $cache): void { if (isset($this->cache)) { - throw new RuntimeException('La dependencia ya ha sido establecida'); + 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]. Aquí también se utiliza el paso automático de dependencias mediante autowiring: +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: @@ -174,10 +174,10 @@ services: ``` -Asignación a variable -===================== +Inyección en propiedades +======================== -Las dependencias se pasan escribiendo directamente en la variable miembro: +Las dependencias se proporcionan escribiendo directamente en una propiedad miembro: ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -Este método se considera inadecuado porque la variable miembro debe declararse como `public`. Y, por lo tanto, no tenemos control sobre si la dependencia pasada será realmente del tipo dado (válido antes de PHP 7.4) y perdemos la posibilidad de reaccionar a la dependencia recién asignada con nuestro propio código, por ejemplo, para evitar cambios posteriores. Al mismo tiempo, la variable se convierte en parte de la interfaz pública de la clase, lo que puede no ser deseable. +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 de la variable se define en la configuración del contenedor DI en la [sección setup |services#Setup]: +La asignación a la propiedad se define en la configuración del contenedor DI en la [sección setup |services#Setup]: ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -Mientras que los tres métodos anteriores son válidos en general en todos los lenguajes orientados a objetos, la inyección mediante método, anotación o atributo *inject* es específica puramente para los presenters en Nette. Se tratan en un [capítulo separado |best-practices:inject-method-attribute]. +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 dependencias obligatorias que la clase necesita indispensablemente para su función -- el setter, por el contrario, es adecuado para dependencias opcionales, o dependencias que se puedan cambiar más adelante -- las variables públicas no son adecuadas +- 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 index efa7c55b79..37760a0faa 100644 --- a/dependency-injection/es/services.texy +++ b/dependency-injection/es/services.texy @@ -2,16 +2,16 @@ Definición de servicios *********************** .[perex] -La configuración es el lugar donde enseñamos al contenedor DI cómo debe construir los servicios individuales y cómo conectarlos con otras dependencias. Nette proporciona una forma muy clara y elegante de lograrlo. +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` en el archivo de configuración de formato NEON es el lugar donde definimos nuestros propios servicios y sus configuraciones. Veamos un ejemplo simple de definición de un servicio llamado `database`, que representa una instancia de la clase `PDO`: +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 resultará en el siguiente método de fábrica en el [contenedor DI|container]: +La configuración anterior da como resultado el siguiente método factory en el [contenedor DI|container]: ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -Los nombres de los servicios nos permiten referirnos a ellos en otras partes del archivo de configuración, en el formato `@nombreDelServicio`. Si no es necesario nombrar el servicio, podemos simplemente usar una viñeta: +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 de servicio: +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'); @@ -35,17 +35,17 @@ $database = $container->getByType(PDO::class); ``` -Creación de servicio -==================== +Creación de servicios +===================== -Normalmente, creamos un servicio simplemente creando una instancia de una clase determinada. Por ejemplo: +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 claves adicionales, la definición se puede desglosar en varias líneas: +Si necesitamos ampliar la configuración con más claves, la definición se puede repartir en varias líneas: ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -La clave `create` tiene el alias `factory`, ambas variantes son comunes en la práctica. Sin embargo, recomendamos usar `create`. +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 de creación pueden escribirse alternativamente en la clave `arguments`: +Los argumentos del constructor o del método factory se pueden indicar alternativamente con la clave `arguments`: ```neon services: @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -Los servicios no tienen que crearse solo mediante la simple creación de una instancia de clase, también pueden ser el resultado de llamar a métodos estáticos o métodos de otros servicios: +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: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -Observe que, por simplicidad, se usa `::` en lugar de `->`, consulte [#expresiones]. Se generarán estos métodos de fábrica: +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 @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -El contenedor DI necesita conocer el tipo del servicio creado. Si creamos un servicio mediante un método que no tiene especificado un tipo de retorno, debemos indicar explícitamente este tipo en la configuración: +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: @@ -100,14 +100,14 @@ services: Argumentos ========== -Pasamos argumentos al constructor y a los métodos de una manera muy similar a como se hace en PHP mismo: +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 una mejor legibilidad, podemos desglosar los argumentos en líneas separadas. En tal caso, el uso de comas es opcional: +Para mejorar la legibilidad podemos poner los argumentos en líneas separadas. En ese caso, las comas son opcionales: ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -También puede nombrar los argumentos y no tener que preocuparse por su orden: +También puede dar nombre a los argumentos, con lo que no tendrá que preocuparse por su orden: ```neon services: @@ -129,20 +129,20 @@ services: ) ``` -Si desea omitir algunos argumentos y usar su valor predeterminado o inyectar un servicio mediante [autowiring|autowiring], use un guion bajo: +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%) ``` -Como argumentos se pueden pasar servicios, usar parámetros y mucho más, consulte [#expresiones]. +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 se deben llamar al crear el servicio. +En la sección `setup` definimos los métodos que deben llamarse al crear el servicio. ```neon services: @@ -152,7 +152,7 @@ services: - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ``` -Esto se vería así en PHP: +En PHP esto tendría este aspecto: ```php public function createServiceDatabase(): PDO @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -Además de llamar a métodos, también se pueden pasar valores a las propiedades. También se admite agregar un elemento a un array, lo cual debe escribirse entre comillas para no entrar en conflicto con la sintaxis de NEON: +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: @@ -174,7 +174,7 @@ services: - '$onClick[]' = [@bar, clickHandler] ``` -Lo que se vería así en el código PHP: +Lo que en código PHP tendría este aspecto: ```php public function createServiceFoo(): Foo @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -En setup, sin embargo, también se pueden llamar métodos estáticos o métodos de otros servicios. Si necesita pasar el servicio actual como argumento, indíquelo como `@self`: +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: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -Observe que, por simplicidad, se usa `::` en lugar de `->`, consulte [#expresiones]. Se generará tal método de fábrica: +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 @@ -210,36 +210,36 @@ public function createServiceFoo(): Foo ``` -Expresiones -=========== +Lenguaje de expresiones +======================= -Nette DI nos proporciona capacidades expresivas extraordinariamente ricas, con las que podemos escribir casi cualquier cosa. En los archivos de configuración, podemos usar [parámetros |configuration#Parámetros]: +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 del parámetro bajo la clave +# valor de un parámetro bajo una clave %mailer.user% # parámetro dentro de una cadena '%wwwDir%/images' ``` -Además, crear objetos, llamar a métodos y funciones: +Además, crear objetos y llamar a métodos y funciones: ```neon -# creación de objeto +# crear un objeto DateTime() -# llamada a método estático +# llamar a un método estático Collator::create(%locale%) -# llamada a función PHP +# llamar a una función de PHP ::getenv(DB_USER) ``` -Referirse a servicios ya sea por su nombre o por tipo: +Referirse a los servicios por su nombre o por su tipo: ```neon # servicio por nombre @@ -249,10 +249,10 @@ Referirse a servicios ya sea por su nombre o por tipo: @Nette\Database\Connection ``` -Usar la sintaxis callable de primera clase: .{data-version:3.2.0} +Usar la sintaxis first-class callable: .{data-version:3.2.0} ```neon -# creación de callback, análogo a [@user, logout] +# crear un callback, equivalente a [@user, logout] @user::logout(...) ``` @@ -262,11 +262,21 @@ Usar constantes: # constante de clase FilesystemIterator::SKIP_DOTS -# constante global se obtiene con la función PHP constant() -::constant(PHP_VERSION) +# obtener una constante global con la función constant() de PHP +::constant(\PHP_VERSION) ``` -Las llamadas a métodos se pueden encadenar igual que en PHP. Solo que, por simplicidad, se usa `::` en lugar de `->`: +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') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -Puede usar estas expresiones en cualquier lugar, al [crear servicios |#Creación de servicio], en [#argumentos], en la sección [#setup] o en [parámetros |configuration#Parámetros]: +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: @@ -293,12 +303,12 @@ services: Funciones especiales -------------------- -En los archivos de configuración puede usar estas funciones especiales: +En los archivos de configuración puede usar las siguientes funciones especiales: -- `not()` negación del valor -- `bool()`, `int()`, `float()`, `string()` conversión sin pérdidas al tipo dado -- `typed()` crea un array de todos los servicios del tipo especificado -- `tagged()` crea un array de todos los servicios con el tag dado +- `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: @@ -308,18 +318,18 @@ services: ) ``` -A diferencia de la conversión de tipos clásica en PHP, como `(int)`, la conversión sin pérdidas lanzará una excepción para valores no numéricos. +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 dado (clase o interfaz). Omite los servicios que tienen el autowiring desactivado. Se pueden especificar múltiples tipos separados por comas. +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) ) ``` -También puede pasar un array de servicios de un tipo determinado como argumento automáticamente mediante [autowiring |autowiring#Array de servicios]. +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 un tag determinado. Aquí también puede especificar múltiples tags separados por comas. +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: @@ -330,20 +340,20 @@ services: Autowiring ========== -La clave `autowired` permite influir en el comportamiento del autowiring para un servicio específico. Para más detalles, consulte el [capítulo sobre autowiring|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 se excluye del autowiring + autowired: false # el servicio foo queda excluido del autowiring ``` -Servicios Lazy .{data-version:3.2.4} +Servicios lazy .{data-version:3.2.4} ==================================== -La carga diferida (lazy loading) es una técnica que pospone la creación de un servicio hasta el momento en que realmente se necesita. En la configuración global, se puede [habilitar la creación lazy |configuration#Servicios lazy] para todos los servicios a la vez. Para servicios individuales, puede sobrescribir este comportamiento: +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: @@ -352,16 +362,20 @@ services: lazy: false ``` -Cuando un servicio se define como lazy, al solicitarlo desde el contenedor DI, obtenemos un objeto proxy especial. Este se ve y se comporta igual que el servicio real, pero la inicialización real (llamada al constructor y setup) ocurre solo en la primera llamada a cualquiera de sus métodos o propiedades. +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 diferida solo se puede usar para clases de usuario, nikoliv pro interní PHP třídy. Vyžaduje PHP 8.4 nebo novější.no para clases internas de PHP. Requiere PHP 8.4 o posterior. +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. -Tags -==== +Etiquetas +========= -Los tags sirven para agregar información adicional a los servicios. Puede agregar uno o más tags a un servicio: +Las etiquetas sirven para añadir información complementaria a los servicios. Puede asignar una o varias etiquetas a un servicio: ```neon services: @@ -371,7 +385,7 @@ services: - cached ``` -Los tags también pueden llevar valores: +Las etiquetas también pueden llevar valores: ```neon services: @@ -381,26 +395,26 @@ services: logger: monolog.logger.event ``` -Para obtener todos los servicios con ciertos tags, puede usar la función `tagged()`: +Para obtener todos los servicios asociados a determinadas etiquetas puede usar la función `tagged()`: ```neon services: - LoggersDependent( tagged(logger) ) ``` -En el contenedor DI, puede obtener los nombres de todos los servicios con un tag determinado usando el método `findByTag()`: +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 que contiene el nombre del servicio y el valor del tag -// por ej. ['foo' => 'monolog.logger.event', ...] +// $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 +Modo inject =========== -Mediante el flag `inject: true` se activa el paso de dependencias a través de variables públicas con la anotación [inject |best-practices:inject-method-attribute#Atributos Inject] y los métodos [inject*() |best-practices:inject-method-attribute#Métodos 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: @@ -409,13 +423,13 @@ services: inject: true ``` -Por defecto, `inject` está activado solo para los presenters. +De forma predeterminada, el modo `inject` está activado solo para los presenters. Modificación de servicios ========================= -El contenedor DI contiene muchos servicios que fueron agregados mediante extensiones incorporadas o [de usuario|extensions]. Puede modificar las definiciones de estos servicios directamente en la configuración. Por ejemplo, puede cambiar la clase del servicio `application.application`, que es estándarmente `Nette\Application\Application`, por otra: +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: @@ -424,9 +438,9 @@ services: alteration: true ``` -El flag `alteration` es informativo e indica que solo estamos modificando un servicio existente. +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 complementar el setup: +También podemos completar el setup: ```neon services: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -Al sobrescribir un servicio, podemos querer eliminar los argumentos originales, elementos de setup o tags, para lo cual se usa `reset`: +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: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -Si desea eliminar un servicio agregado por una extensión, puede hacerlo así: +Si quiere eliminar un servicio añadido por una extensión, puede hacerlo así: ```neon services: diff --git a/dependency-injection/es/upgrading.texy b/dependency-injection/es/upgrading.texy new file mode 100644 index 0000000000..995c047db3 --- /dev/null +++ b/dependency-injection/es/upgrading.texy @@ -0,0 +1,49 @@ +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 index 0d8bf439da..882a530082 100644 --- a/dependency-injection/fr/@home.texy +++ b/dependency-injection/fr/@home.texy @@ -2,20 +2,21 @@ Nette DI ******** .[perex] -L'Injection de Dépendances est un patron de conception qui changera fondamentalement votre façon de voir le code et le développement. Il vous ouvrira la voie vers un monde d'applications conçues proprement et maintenables. +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 et singletons |global-state] +- [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] -- [Foire aux questions|faq] +- [Questions fréquentes |faq] Le paquet `nette/di` fournit un conteneur DI compilé extrêmement avancé pour PHP. -- [Conteneur Nette DI |nette-container] +- [Nette DI Container |nette-container] - [Configuration |configuration] - [Définition des services |services] - [Autowiring |autowiring] - [Factories générées |factory] -- [Création d'extensions pour Nette DI|extensions] +- [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 index 5562cd8ba9..91e718b494 100644 --- a/dependency-injection/fr/@left-menu.texy +++ b/dependency-injection/fr/@left-menu.texy @@ -1,17 +1,28 @@ -Injection de dépendances -************************ -- [Qu'est-ce que DI ? |introduction] -- [État global et singletons |global-state] +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] -- [Foire aux questions|faq] +- [Questions fréquentes |faq] Nette DI -------- -- [Conteneur Nette DI |nette-container] +- [Nette DI Container |nette-container] - [Configuration |configuration] - [Définition des services |services] - [Autowiring |autowiring] - [Factories générées |factory] -- [Création d'extensions pour Nette DI|extensions] +- [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/autowiring.texy b/dependency-injection/fr/autowiring.texy index bdb671496a..f421d02c91 100644 --- a/dependency-injection/fr/autowiring.texy +++ b/dependency-injection/fr/autowiring.texy @@ -2,9 +2,9 @@ Autowiring ********** .[perex] -L'autowiring est une fonctionnalité formidable qui peut automatiquement passer les services requis au constructeur et à d'autres méthodes, de sorte que nous n'avons pas du tout besoin de les écrire. Cela vous fait gagner beaucoup de temps. +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 à cela, nous pouvons omettre la grande majorité des arguments lors de l'écriture des définitions de service. Au lieu de : +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: @@ -18,7 +18,7 @@ services: articles: Model\ArticleRepository ``` -L'autowiring est basé sur les types, donc pour qu'il fonctionne, la classe `ArticleRepository` doit être définie à peu près comme ceci : +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; @@ -30,7 +30,9 @@ class ArticleRepository } ``` -Pour pouvoir utiliser l'autowiring, il doit y avoir **exactement un service** pour chaque type dans le conteneur. S'il y en avait plus, l'autowiring ne saurait pas lequel passer et lèverait une exception : +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: @@ -39,13 +41,13 @@ services: articles: Model\ArticleRepository # LÈVE UNE EXCEPTION, mainDb et tempDb correspondent ``` -La solution serait soit de contourner l'autowiring et de spécifier explicitement le nom du service (c'est-à-dire `articles: Model\ArticleRepository(@mainDb)`). Mais il est plus judicieux de [désactiver |#Désactivation de l autowiring] l'autowiring pour l'un des services, ou de [préférer |#Préférence d autowiring] le premier service. +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ésactivation de l'autowiring ------------------------------ +Désactiver l'autowiring +----------------------- -Nous pouvons désactiver l'autowiring d'un service à l'aide de l'option `autowired: no` : +Nous pouvons désactiver l'autowiring d'un service à l'aide de l'option `autowired: false` : ```neon services: @@ -55,25 +57,27 @@ services: create: PDO('sqlite::memory:') autowired: false # le service tempDb est exclu de l'autowiring - articles: Model\ArticleRepository # donc il passe mainDb au constructeur + articles: Model\ArticleRepository # passe donc mainDb au constructeur ``` -Le service `articles` ne lèvera pas d'exception indiquant qu'il existe deux services correspondants de type `PDO` (c'est-à-dire `mainDb` et `tempDb`) qui peuvent être passés au constructeur, car il ne voit que le service `mainDb`. +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 fonctionne différemment de Symfony, où l'option `autowire: false` indique que l'autowiring ne doit pas être utilisé pour les arguments du constructeur du service donné. Dans Nette, l'autowiring est toujours utilisé, que ce soit pour les arguments du constructeur ou pour toute autre méthode. L'option `autowired: false` indique que l'instance du service donné ne doit être passée nulle part via l'autowiring. +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 spécifions l'option `autowired` pour l'un d'entre eux, ce service devient le préféré : +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 préféré + autowired: PDO # devient le préféré tempDb: create: PDO('sqlite::memory:') @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -Le service `articles` ne lèvera pas d'exception indiquant qu'il existe deux services correspondants de type `PDO` (c'est-à-dire `mainDb` et `tempDb`), mais utilisera le service préféré, c'est-à-dire `mainDb`. +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`. -Tableau de services -------------------- +Collection de services +---------------------- -L'autowiring peut également passer des tableaux de services d'un certain type. Comme il n'est pas possible d'écrire nativement le type des éléments d'un tableau en PHP, il faut, en plus du type `array`, ajouter un commentaire phpDoc avec le type de l'élément sous la forme `ClassName[]` : +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; @@ -102,15 +106,15 @@ class ShipManager } ``` -Le conteneur DI passera alors automatiquement un tableau de services correspondant au type donné. Il omettra les services dont l'autowiring est désactivé. +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 dans le commentaire peut également être sous la forme `array<int, Class>` ou `list<Class>`. Si vous ne pouvez pas influencer la forme du commentaire phpDoc, vous pouvez passer le tableau de services directement dans la configuration à l'aide de [`typed()` |services#Fonctions spéciales]. +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 peut injecter que des objets et des tableaux d'objets. Les arguments scalaires (par exemple, chaînes, nombres, booléens) sont [écrits dans la configuration |services#Arguments]. Une alternative est de créer un [objet de paramètres |best-practices:passing-settings-to-presenters], qui encapsule la valeur scalaire (ou plusieurs valeurs) sous forme d'objet, lequel peut ensuite être à nouveau passé via l'autowiring. +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 @@ -123,24 +127,41 @@ class MySettings } ``` -Vous en faites un service en l'ajoutant à la configuration : +Vous l'enregistrez comme service en l'ajoutant à la configuration : ```neon services: - MySettings('any value') ``` -Toutes les classes le demanderont ensuite via l'autowiring. +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. -Réduction de l'autowiring -------------------------- +Restreindre l'autowiring +------------------------ -Pour les services individuels, l'autowiring peut être limité à certaines classes ou interfaces. +Pour chaque service, l'autowiring peut être restreint à certaines classes ou interfaces. -Normalement, l'autowiring passe le service à chaque paramètre de méthode dont le type correspond au service. La réduction signifie que nous définissons des conditions auxquelles les types spécifiés pour les paramètres de méthode doivent satisfaire pour que le service leur soit passé. +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é. -Illustrons cela par un exemple : +Prenons un exemple : ```php class ParentClass @@ -168,13 +189,13 @@ Si nous les enregistrions tous comme services, l'autowiring échouerait : services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # LÈVE UNE EXCEPTION, les services parent et child correspondent + parentDep: ParentDependent # LÈVE UNE EXCEPTION, parent et child correspondent childDep: ChildDependent # l'autowiring passe le service child au constructeur ``` -Le service `parentDep` lèvera une exception `Multiple services of type ParentClass found: parent, child`, car les deux services `parent` et `child` correspondent à son constructeur, et l'autowiring ne peut pas décider lequel choisir. +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 réduire son autowiring au type `ChildClass` : +Pour le service `child`, nous pouvons donc restreindre son autowiring au type `ChildClass` : ```neon services: @@ -187,17 +208,17 @@ services: childDep: ChildDependent # l'autowiring passe le service child au constructeur ``` -Maintenant, le service `parent` est passé au constructeur du service `parentDep`, car c'est maintenant le seul objet correspondant. L'autowiring ne passera plus le service `child` là-bas. Oui, le service `child` est toujours de type `ParentClass`, mais la condition de réduction donnée pour le type de paramètre n'est plus remplie, c'est-à-dire qu'il n'est pas vrai que `ParentClass` *est un supertype de* `ChildClass`. +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 également être écrit comme `autowired: self`, car `self` est un alias pour la classe du service actuel. +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 de spécifier plusieurs classes ou interfaces sous forme de tableau : +Dans la clé `autowired`, il est également possible d'indiquer plusieurs classes ou interfaces sous forme de tableau : ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -Essayons de compléter l'exemple avec des interfaces : +Essayons d'ajouter des interfaces à l'exemple : ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -Si nous ne limitons pas le service `child` de quelque manière que ce soit, il correspondra aux constructeurs de toutes les classes `FooDependent`, `BarDependent`, `ParentDependent` et `ChildDependent` et l'autowiring l'y passera. +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. -Cependant, si nous limitons son autowiring à `ChildClass` en utilisant `autowired: ChildClass` (ou `self`), l'autowiring ne le passera qu'au constructeur de `ChildDependent`, car il nécessite un argument de type `ChildClass` et il est vrai que `ChildClass` *est de type* `ChildClass`. Aucun autre type spécifié pour les autres paramètres n'est un supertype de `ChildClass`, donc le service ne sera pas passé. +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 limitons à `ParentClass` en utilisant `autowired: ParentClass`, il sera à nouveau passé au constructeur de `ChildDependent` (car le `ChildClass` requis est un supertype de `ParentClass`) et nouvellement aussi au constructeur de `ParentDependent`, car le type `ParentClass` requis est également satisfaisant. +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 limitons à `FooInterface`, il sera toujours autowiré dans `ParentDependent` (le `ParentClass` requis est un supertype de `FooInterface`) et `ChildDependent`, mais en plus aussi dans le constructeur de `FooDependent`, mais pas dans `BarDependent`, car `BarInterface` n'est pas un supertype de `FooInterface`. +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: diff --git a/dependency-injection/fr/compilation-internals.texy b/dependency-injection/fr/compilation-internals.texy new file mode 100644 index 0000000000..a2b595c699 --- /dev/null +++ b/dependency-injection/fr/compilation-internals.texy @@ -0,0 +1,222 @@ +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 index a5167ac052..85dd8902fd 100644 --- a/dependency-injection/fr/configuration.texy +++ b/dependency-injection/fr/configuration.texy @@ -2,19 +2,19 @@ Configuration du conteneur DI ***************************** .[perex] -Aperçu des options de configuration pour le conteneur Nette DI. +Aperçu des options de configuration du conteneur DI de Nette. Fichier de configuration ======================== -Le conteneur Nette DI est facilement contrôlé à l'aide de fichiers de configuration. Ceux-ci sont généralement écrits au [format NEON |neon:format]. Pour l'édition, nous recommandons des [éditeurs avec support |best-practices:editors-and-tools#Éditeur IDE] pour ce format. +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]: "Décorateur .[prism-token prism-comment]"<br> +"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]: "Installation d'extensions DI supplémentaires .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Inclusion de fichiers]: "Inclusion de fichiers .[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]" @@ -27,7 +27,7 @@ Pour écrire une chaîne contenant le caractère `%`, vous devez l'échapper en Paramètres ========== -Dans la configuration, vous pouvez définir des paramètres qui peuvent ensuite être utilisés dans le cadre des définitions de service. Cela peut clarifier la configuration ou unifier et isoler les valeurs qui changeront. +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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -Nous nous référons au paramètre `dsn` n'importe où dans la configuration en écrivant `%dsn%`. Les paramètres peuvent également être utilisés à l'intérieur de chaînes comme `'%wwwDir%/images'`. +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 doivent pas nécessairement être des chaînes ou des nombres, ils peuvent également contenir des tableaux : +Les paramètres ne sont pas obligatoirement des chaînes ou des nombres, ils peuvent aussi contenir des tableaux : ```neon parameters: @@ -49,32 +49,32 @@ parameters: languages: [cs, en, de] ``` -Nous nous référons à une clé spécifique comme `%mailer.user%`. +Nous faisons référence à une clé précise par `%mailer.user%`. -Si vous avez besoin de connaître la valeur d'un paramètre dans votre code, par exemple dans une classe, passez-le à cette classe. Par exemple, dans le constructeur. Il n'existe pas d'objet global représentant la configuration que les classes interrogeraient pour les valeurs des paramètres. Cela violerait le principe d'injection de dépendances. +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 séparé |services]. +Voir le [chapitre distinct|services]. Decorator ========= -Comment modifier en masse tous les services d'un certain type ? Par exemple, appeler une certaine méthode sur tous les presenters qui héritent d'un ancêtre commun spécifique ? C'est là qu'intervient le décorateur. +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) # appelle cette méthode - - $absoluteUrls = true # et définit la variable + - setProjectId(10) # appeler cette méthode + - $absoluteUrls = true # et définir la variable ``` -Le décorateur peut également être utilisé pour définir des [tags |services#Tags] ou activer le mode [inject |services#Mode Inject]. +Le decorator peut aussi servir à poser des [tags |services#Tags] ou à activer le [mode inject |services#Mode inject]. ```neon decorator: @@ -87,90 +87,90 @@ decorator: DI === -Paramètres techniques du conteneur DI. +Réglages techniques du conteneur DI. ```neon di: - # afficher le DIC dans la barre Tracy ? - debugger: ... # (bool) par défaut est true + # 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 à ne jamais autowirer + # types de paramètres que vous n'autowirez jamais excluded: ... # (string[]) - # autoriser la création paresseuse de services ? - lazy: ... # (bool) par défaut est false + # activer la création lazy des services ? + lazy: ... # (bool) false par défaut - # classe dont hérite le conteneur DI - parentClass: ... # (string) par défaut est Nette\DI\Container + # la classe dont le conteneur DI hérite + parentClass: ... # (string) Nette\DI\Container par défaut ``` -Services paresseux .{data-version:3.2.4} ----------------------------------------- +Services lazy .{data-version:3.2.4} +----------------------------------- -Le paramètre `lazy: true` active la création paresseuse (différée) des services. Cela signifie que les services ne sont pas réellement créés au moment où nous les demandons au conteneur DI, mais seulement au moment de leur première utilisation. Cela peut accélérer le démarrage de l'application et réduire l'empreinte mémoire, car seuls les services réellement nécessaires dans la requête donnée sont créés. +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 spécifique, la création paresseuse peut être [modifiée |services#Services Lazy]. +Pour un service précis, la création lazy peut être [ajustée |services#Services lazy]. .[note] -Les objets paresseux ne peuvent être utilisés que pour les classes utilisateur, pas pour les classes PHP internes. Nécessite PHP 8.4 ou plus récent. +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. -Exportation des métadonnées ---------------------------- +Export des métadonnées +---------------------- -La classe du conteneur DI contient également beaucoup de métadonnées. Vous pouvez la réduire en réduisant l'exportation 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) par défaut est true + parameters: false # (bool) true par défaut - # exporter les tags et lesquels ? - tags: # (string[]|bool) par défaut tous + # 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) par défaut toutes + # 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 le tableau `$container->getParameters()`, vous pouvez désactiver l'exportation des paramètres. De plus, vous pouvez exporter uniquement les tags via lesquels vous obtenez des services avec la méthode `$container->findByTag(...)`. Si vous n'appelez pas du tout la méthode, vous pouvez désactiver complètement l'exportation des tags en utilisant `false`. +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 considérablement les métadonnées pour l'[autowiring |autowiring] en listant les classes que vous utilisez comme paramètre de la méthode `$container->getByType()`. Et encore une fois, si vous n'appelez pas du tout la méthode (ou seulement dans le [bootstrap |application:bootstrapping] pour obtenir `Nette\Application\Application`), vous pouvez désactiver complètement l'exportation en utilisant `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. De cette manière, nous ajoutons par exemple l'extension DI `Dibi\Bridges\Nette\DibiExtension22` sous le nom `dibi` +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\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -Ensuite, nous la configurons dans la section `dibi` : +Vous la configurez ensuite dans la section `dibi` : ```neon dibi: host: localhost ``` -Une classe avec des paramètres peut également être ajoutée comme extension : +Vous pouvez aussi ajouter comme extension une classe avec des paramètres : ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` -Inclusion de fichiers -===================== +Inclure des fichiers +==================== -Nous pouvons inclure d'autres fichiers de configuration dans la section `includes` : +D'autres fichiers de configuration peuvent être inclus dans la section `includes` : ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -Le nom `parameters.php` n'est pas une faute de frappe, la configuration peut également être écrite dans un fichier PHP qui la renvoie sous forme de tableau : +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 @@ -192,13 +192,13 @@ return [ ]; ``` -Si des éléments avec les mêmes clés apparaissent dans les fichiers de configuration, ils seront écrasés ou, dans le cas de [tableaux fusionnés |#Fusion]. Le fichier inclus plus tard 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 qu'il inclut. +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'ajout automatique de services au conteneur DI rend le travail extrêmement agréable. Nette ajoute automatiquement les presenters au conteneur, mais il est facile d'ajouter également d'autres classes. +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 : @@ -208,7 +208,14 @@ search: - in: %appDir%/Model ``` -Cependant, nous ne voulons généralement pas ajouter absolument toutes les classes et interfaces, nous pouvons donc les filtrer : +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: @@ -223,7 +230,7 @@ search: - *Factory ``` -Ou nous pouvons sélectionner des classes qui héritent ou implémentent au moins une des classes listées : +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 @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -Il est également possible de définir des règles d'exclusion, c'est-à-dire des masques de nom de classe ou des ancêtres héréditaires, qui, s'ils correspondent, empêchent l'ajout du service au conteneur DI : +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: @@ -247,7 +254,7 @@ search: implements: ... ``` -Des tags peuvent être définis pour tous les services : +Des tags peuvent être attribués à tous les services enregistrés automatiquement : ```neon search: @@ -255,11 +262,13 @@ search: 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 avec les mêmes clés apparaissent dans plusieurs fichiers de configuration, ils seront écrasés ou, dans le cas de tableaux, fusionnés. Le fichier inclus plus tard a une priorité plus élevée que le précédent. +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> diff --git a/dependency-injection/fr/container.texy b/dependency-injection/fr/container.texy index fe447ab679..f8fb2dcd09 100644 --- a/dependency-injection/fr/container.texy +++ b/dependency-injection/fr/container.texy @@ -2,15 +2,15 @@ Qu'est-ce qu'un conteneur DI ? ****************************** .[perex] -Un conteneur d'injection de dépendances (DIC) est une classe qui peut instancier et configurer des objets. +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 peut vous surprendre, mais dans de nombreux 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é la DI avec des exemples concrets, et aucun conteneur n'était nécessaire. +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. -Cependant, si vous devez gérer un grand nombre d'objets différents avec de nombreuses dépendances, un conteneur d'injection de dépendances sera vraiment utile. C'est le cas, par exemple, des applications web construites sur un framework. +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 `UserController`. Les deux ont des dépendances, à savoir la base de données et la factory `ArticleFactory`. Et pour ces classes, nous allons maintenant créer un conteneur. Bien sûr, pour un exemple aussi simple, il n'est pas logique d'avoir un conteneur. Mais nous allons le créer pour montrer à quoi il ressemble et comment il fonctionne. +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 codé en dur pour l'exemple donné : +Voici un conteneur simple, écrit en dur, pour l'exemple ci-dessus : ```php class Container @@ -25,9 +25,9 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` @@ -36,12 +36,12 @@ L'utilisation ressemblerait à ceci : ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -Nous demandons simplement l'objet au conteneur et nous n'avons plus besoin de savoir comment le créer ni quelles sont ses dépendances ; le conteneur sait tout cela. Les dépendances sont injectées automatiquement par le conteneur. C'est là sa force. +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 données codées en dur. Faisons donc un pas de plus et ajoutons des paramètres pour rendre le conteneur vraiment utile : +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 @@ -70,9 +70,9 @@ $container = new Container([ ]); ``` -Les lecteurs attentifs ont peut-être remarqué un certain problème. Chaque fois que j'obtiens un objet `UserController`, une nouvelle instance de `ArticleFactory` et de la base de données est également créée. Ce n'est certainement pas ce que nous voulons. +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. -Ajoutons donc une méthode `getService()` qui renverra toujours les mêmes instances : +Nous allons donc ajouter une méthode `getService()` qui renverra toujours les mêmes instances : ```php class Container @@ -98,9 +98,9 @@ class Container } ``` -Lors du premier appel, par exemple `$container->getService('Database')`, il demandera à `createDatabase()` de créer l'objet de base de données, le stockera dans le tableau `$services` et le renverra directement lors du prochain appel. +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. -Modifions également le reste du conteneur pour utiliser `getService()` : +Nous modifions aussi le reste du conteneur pour qu'il utilise `getService()` : ```php class Container @@ -112,16 +112,16 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -Au fait, le terme service désigne tout objet géré par le conteneur. D'où le nom de la méthode `getService()`. +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()`. -Terminé. Nous avons un conteneur DI entièrement fonctionnel ! Et nous pouvons l'utiliser : +C'est fait. Nous avons un conteneur DI pleinement fonctionnel ! Et nous pouvons l'utiliser : ```php $container = new Container([ @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -Comme vous pouvez le voir, écrire un DIC n'est pas compliqué. Il convient de rappeler que les objets eux-mêmes ne savent pas qu'ils sont créés par un conteneur. Par conséquent, il est possible de créer ainsi n'importe quel objet en PHP sans interférer avec son code source. +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. -La création et la maintenance manuelles de la classe du conteneur peuvent rapidement devenir un cauchemar. Dans le chapitre suivant, nous parlerons donc du [Conteneur Nette DI |nette-container], qui peut se générer et se mettre à jour presque tout seul. +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 index 8feef95ca4..e6549a7b30 100644 --- a/dependency-injection/fr/extensions.texy +++ b/dependency-injection/fr/extensions.texy @@ -1,39 +1,67 @@ -Création d'extensions pour Nette DI -*********************************** +Créer des extensions pour Nette DI +********************************** .[perex] -La génération du conteneur DI, en plus des fichiers de configuration, est également influencée par ce qu'on appelle des *extensions*. Nous les activons dans le fichier de configuration dans la section `extensions`. +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. -De cette manière, nous ajoutons l'extension représentée par la classe `BlogExtension` sous le nom `blog` : +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 ``` -Chaque extension du compilateur hérite de [api:Nette\DI\CompilerExtension] et peut implémenter les méthodes suivantes, qui sont appelées séquentiellement lors de la construction du conteneur DI : +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é. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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]) -getConfigSchema() .[method] -=========================== +.[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. -Cette méthode est appelée en premier. Elle définit le schéma pour la validation des paramètres de configuration. +.[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]. -Nous configurons l'extension dans la section dont le nom est le même que celui sous lequel l'extension a été ajoutée, c'est-à-dire `blog` : + +Première extension +================== + +Voici une extension petite mais complète. Nous l'activons et la configurons dans le même fichier : ```neon -# même nom que l'extension +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Nous créons un schéma décrivant toutes les options de configuration, y compris leurs types, les valeurs autorisées et éventuellement les valeurs par défaut : +Et voici la classe entière : ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -La documentation se trouve sur la page [Schéma |schema:]. De plus, il est possible de spécifier quelles options peuvent être [dynamiques |application:bootstrapping#Paramètres Dynamiques] à l'aide de `dynamic()`, par ex. `Expect::int()->dynamic()`. -Nous accédons à la configuration via la variable `$this->config`, qui est un objet `stdClass` : -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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. -loadConfiguration() .[method] -============================= +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. -Utilisé pour ajouter des services au conteneur. Pour cela, on utilise [api:Nette\DI\ContainerBuilder] : + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // ou setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -La convention est de préfixer les services ajoutés par l'extension avec son nom pour éviter les conflits de noms. C'est ce que fait la méthode `prefix()`, donc si l'extension s'appelle `blog`, le service portera le nom `blog.articles`. +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 nous devons renommer un service, nous pouvons créer un alias avec le nom d'origine pour maintenir la compatibilité ascendante. Nette fait de même, par exemple, pour le service `routing.router`, qui est également disponible sous son ancien nom `router`. +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 -$builder->addAlias('router', 'routing.router'); +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 : -Chargement des services depuis un fichier ------------------------------------------ +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -Nous n'avons pas besoin de créer des services uniquement à l'aide de l'API de la classe ContainerBuilder, mais aussi avec la notation familière utilisée dans le fichier de configuration NEON dans la section services. Le préfixe `@extension` représente l'extension actuelle. +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: @@ -107,88 +160,284 @@ services: 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 : - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Nous chargeons les 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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // chargement du fichier de configuration pour l'extension - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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. -beforeCompile() .[method] -========================= +Vous pouvez aussi coopérer avec les autres extensions enregistrées, obtenues via `$this->compiler->getExtensions()`, éventuellement filtrées par classe ou interface : -La méthode est appelée lorsque le conteneur contient tous les services ajoutés par les extensions individuelles dans les méthodes `loadConfiguration` ainsi que par les fichiers de configuration utilisateur. À ce stade de la construction, nous pouvons donc modifier les définitions de service ou ajouter des liens entre elles. Pour rechercher des services dans le conteneur par tags, on peut utiliser la méthode `findByTag()`, et par classe ou interface, la méthode `findByType()`. +```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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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. + -afterCompile() .[method] -======================== +ContainerBuilder +================ -À ce stade, la classe du conteneur est déjà générée sous forme d'objet [ClassType |php-generator:#Classes], contient toutes les méthodes qui créent les services et est prête à être écrite dans le cache. Nous pouvons encore modifier le code résultant de la classe à ce moment. +[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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Un usage typique est d'enregistrer un panneau Tracy uniquement en mode développement. + -$initialization .[method] -========================= +Arguments complexes +------------------- -Après la [création du conteneur |application:bootstrapping#index.php], la classe Configurator appelle le code d'initialisation, qui est créé en écrivant dans l'objet `$this->initialization` à l'aide de la [méthode addBody() |php-generator:#Corps de méthodes et de fonctions]. +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 : -Montrons un exemple de comment démarrer la session ou lancer des services qui ont le tag `run` avec le code d'initialisation : +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // démarrage automatique de la session - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // ce tag sera toujours exporté, même si l'export est restreint + $this->compiler->addExportedTag('event.subscriber'); - // les services avec le tag run doivent être créés après l'instanciation du conteneur - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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 index b8a0c3f249..236b129092 100644 --- a/dependency-injection/fr/factory.texy +++ b/dependency-injection/fr/factory.texy @@ -2,9 +2,9 @@ Factories générées ****************** .[perex] -Nette DI peut générer automatiquement le code des factories basé sur des interfaces, ce qui vous évite d'écrire du code. +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 qui produit et configure des objets. Elle leur transmet donc également leurs dépendances. Ne confondez pas, s'il vous plaît, avec le patron de conception *factory method*, qui décrit une manière spécifique d'utiliser les factories et n'est pas lié à ce sujet. +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] : @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DI peut générer automatiquement le code des factories. Tout ce que vous avez à faire est de créer une interface et Nette DI générera l'implémentation. L'interface doit avoir exactement une méthode nommée `create` et déclarer un type de retour : +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 @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -Ainsi, la factory `ArticleFactory` a une méthode `create` qui crée des objets `Article`. La classe `Article` peut ressembler à ceci : +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 @@ -44,7 +44,7 @@ class Article } ``` -Nous ajoutons la factory au fichier de configuration : +Ajoutez la factory au fichier de configuration : ```neon services: @@ -53,7 +53,7 @@ services: Nette DI générera l'implémentation correspondante de la factory. -Dans le code qui utilise la factory, nous demandons donc l'objet par l'interface et Nette DI utilisera l'implémentation générée : +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 @@ -65,7 +65,7 @@ class UserController public function foo() { - // laissons la factory créer l'objet + // laissons la factory créer un objet $article = $this->articleFactory->create(); } } @@ -75,7 +75,7 @@ class UserController Factory paramétrée ================== -La méthode de factory `create` peut accepter des paramètres, qu'elle transmet ensuite au constructeur. Ajoutons par exemple à la classe `Article` l'ID de l'auteur de l'article : +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 @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -Grâce au fait que le paramètre dans le constructeur et le paramètre dans la factory portent le même nom, Nette DI les transmet de manière entièrement automatique. +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 également être écrite sous forme multiligne en utilisant la clé `implement` : +La définition peut aussi s'écrire sous forme multiligne à l'aide de la clé `implement` : ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -Lors de l'écriture de cette manière plus longue, il est possible de spécifier des arguments supplémentaires pour le constructeur dans la clé `arguments` et une configuration supplémentaire à l'aide de `setup`, tout comme pour les services normaux. +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 spécifier une valeur fixe dans la configuration, qui serait transmise au constructeur de `Article` : +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: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -Ou inversement, si `create()` acceptait le paramètre `$authorId`, mais qu'il ne faisait pas partie du constructeur et était transmis par la méthode `Article::setAuthorId()`, nous nous y référerions dans la section `setup` : +À 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: @@ -137,11 +137,11 @@ services: Accessor ======== -En plus des factories, Nette peut également générer ce qu'on appelle des accessors. Ce sont des objets avec une méthode `get()` qui renvoie un certain service du conteneur DI. Les appels répétés à `get()` renvoient toujours la même instance. +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 accessors fournissent un chargement paresseux (lazy-loading) pour les dépendances. Supposons une classe qui écrit des erreurs dans une base de données spéciale. Si cette classe se faisait passer la connexion à la base de données comme dépendance par le constructeur, la connexion devrait toujours être créée, même si en pratique une erreur n'apparaît qu'exceptionnellement et donc la plupart du temps la connexion resterait inutilisée. Au lieu de cela, la classe se fait passer un accessor et ce n'est que lorsque son `get()` est appelé que l'objet de base de données est créé : +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 accessor ? Il suffit d'écrire une interface et Nette DI générera l'implémentation. L'interface doit avoir exactement une méthode nommée `get` et déclarer un type de retour : +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 @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -Nous ajoutons l'accessor au fichier de configuration, où se trouve également la définition du service qu'il renverra : +Ajoutez l'accesseur au fichier de configuration, avec la définition du service qu'il doit renvoyer : ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -Comme l'accessor renvoie un service de type `PDO` et qu'il n'y a qu'un seul service de ce type dans la configuration, il renverra précisément celui-ci. S'il y avait plusieurs services de ce type, nous spécifierions le service renvoyé à l'aide de son nom, par ex. `- PDOAccessor(@db1)`. +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)`. -Factory/Accessor multiple -========================= -Nos factories et accessors ne pouvaient jusqu'à présent produire ou renvoyer qu'un seul objet. Mais il est très facile de créer également des factories multiples combinées avec des accessors. L'interface d'une telle classe contiendra un nombre quelconque de méthodes nommées `create<name>()` et `get<name>()`, par ex. : +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 @@ -173,9 +174,9 @@ interface MultiFactory } ``` -Ainsi, au lieu de nous passer plusieurs factories et accessors générés, nous passons une seule factory plus complexe qui en fait plus. +Ainsi, au lieu d'injecter plusieurs factories et accesseurs distincts, vous pouvez injecter un seul composant plus complet. -Alternativement, au lieu de plusieurs méthodes, on peut utiliser `get()` avec un paramètre : +Autre possibilité : au lieu de plusieurs méthodes, utiliser `get()` avec un paramètre : ```php interface MultiFactoryAlt @@ -184,22 +185,24 @@ interface MultiFactoryAlt } ``` -Alors, `MultiFactory::getArticle()` fait la même chose que `MultiFactoryAlt::get('article')`. Cependant, la notation alternative a l'inconvénient qu'il n'est pas clair quelles valeurs de `$name` sont prises en charge et logiquement, il n'est pas non plus possible de distinguer différentes valeurs de retour pour différents `$name` dans l'interface. +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 par liste --------------------- -De cette manière, on peut définir une factory multiple dans la configuration : .{data-version:3.2.0} +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() + article: Article() # définit createArticle() db: PDO(%dsn%, %user%, %password%) # définit getDb() ) ``` -Ou nous pouvons nous référer à des services existants dans la définition de la factory à l'aide d'une référence : +Vous pouvez aussi renvoyer, dans la définition de la multifactory, vers des services existants à l'aide de références : ```neon services: @@ -212,15 +215,19 @@ services: ``` -Définition par tags -------------------- +Définition à l'aide de tags +--------------------------- -La deuxième option consiste à utiliser des [tags |services#Tags] pour la définition : +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: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index c65c685eab..a49533872d 100644 --- a/dependency-injection/fr/faq.texy +++ b/dependency-injection/fr/faq.texy @@ -1,94 +1,94 @@ -Foire aux questions sur la DI (FAQ) -*********************************** +Questions fréquentes sur la DI (FAQ) +************************************ -DI est-il un autre nom pour IoC ? ---------------------------------- +La DI est-elle un autre nom pour l'IoC ? +---------------------------------------- -L'*Inversion de Contrôle* (IoC) est un principe axé sur la manière dont le code est exécuté - si votre code exécute du code étranger ou si votre code est intégré dans du code étranger qui l'appelle ensuite. IoC est un terme large englobant les [événements |nette:glossary#Événements events], ce qu'on appelle le [principe d'Hollywood |application:components#Style Hollywood] et d'autres aspects. Les factories, dont parle la [Règle n°3 : laissez faire la factory |introduction#Règle n 3 : laissez faire la factory], font également partie de ce concept et représentent une inversion pour l'opérateur `new`. +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) se concentre sur la manière dont un objet prend connaissance d'un autre objet, c'est-à-dire de ses dépendances. C'est un patron de conception qui exige le passage explicite des dépendances entre les objets. +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. -On peut donc dire que la DI est une forme spécifique d'IoC. Cependant, toutes les formes d'IoC ne sont pas appropriées du point de vue de la propreté du code. Par exemple, parmi les anti-patrons figurent les techniques qui travaillent avec l'[état global |global-state] ou ce qu'on appelle le [Service Locator |#Qu est-ce que le Service Locator]. +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 que le Service Locator ? ----------------------------------- +Qu'est-ce qu'un Service Locator ? +--------------------------------- -C'est une alternative à l'Injection de Dépendances. Il fonctionne en créant un dépôt central où tous les services ou dépendances disponibles sont enregistrés. Lorsqu'un objet a besoin d'une dépendance, il la demande au 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. -Cependant, par rapport à l'Injection de Dépendances, il perd en transparence : les dépendances ne sont pas passées directement aux objets et ne sont donc pas facilement identifiables, ce qui nécessite d'examiner le code pour découvrir et comprendre toutes les liaisons. Les tests sont également plus complexes, car nous ne pouvons pas simplement passer des objets mock aux objets testés, mais nous devons passer par le Service Locator. De plus, le Service Locator perturbe la conception du code, car les objets individuels doivent connaître son existence, ce qui diffère de l'Injection de Dépendances, où les objets n'ont pas connaissance du conteneur DI. +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 est-il préférable de ne pas utiliser la DI ? --------------------------------------------------- +Quand vaut-il mieux ne pas utiliser la DI ? +------------------------------------------- -Aucune difficulté connue n'est associée à l'utilisation du patron de conception Injection de Dépendances. Au contraire, l'obtention de dépendances à partir d'emplacements globalement disponibles entraîne [toute une série de complications |global-state], tout comme l'utilisation du Service Locator. Il est donc conseillé d'utiliser toujours la DI. Ce n'est pas une approche dogmatique, mais simplement aucune meilleure alternative n'a été trouvée. +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. -Néanmoins, il existe certaines situations où nous ne passons pas d'objets et les obtenons depuis l'espace global. Par exemple, lors du débogage de code, lorsque vous devez afficher la valeur d'une variable à un point spécifique du programme, mesurer la durée d'une certaine partie du programme ou enregistrer un message. Dans de tels cas, lorsqu'il s'agit d'actions temporaires qui seront ultérieurement supprimées du code, il est légitime d'utiliser un dumper, un chronomètre ou un logger globalement disponible. Ces outils ne font en effet pas partie de la conception du code. +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'utilisation de l'Injection de Dépendances entraîne-t-elle des inconvénients, tels qu'une complexité accrue de l'écriture du code ou une dégradation des performances ? Que perdons-nous lorsque nous commençons à écrire du code conformément à la DI ? +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 n'a pas d'impact sur les performances ou l'utilisation de la mémoire de l'application. Les performances du conteneur DI peuvent jouer un certain rôle, mais dans le cas de [Nette DI |nette-container], le conteneur est compilé en PHP pur, de sorte que sa surcharge lors de l'exécution de l'application est pratiquement nulle. +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. -Lors de l'écriture du code, il est parfois nécessaire de créer des constructeurs acceptant des dépendances. Auparavant, cela pouvait être fastidieux, mais grâce aux IDE modernes et à la [promotion des propriétés du constructeur |https://blog.nette.org/fr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], c'est maintenant une question de quelques secondes. Les factories peuvent être facilement générées à l'aide de Nette DI et du plugin pour PhpStorm en un clic de souris. D'un autre côté, il n'est plus nécessaire d'écrire des singletons et des points d'accès statiques. +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. -On peut affirmer qu'une application correctement conçue utilisant la DI n'est ni plus courte ni plus longue qu'une application utilisant des singletons. Les parties du code travaillant avec des dépendances sont simplement extraites des classes individuelles et déplacées vers de nouveaux emplacements, c'est-à-dire dans le conteneur DI et les factories. +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 transition 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. +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 de la transition vers l'Injection de Dépendances, il est important que tous les membres de l'équipe comprennent les principes et les procédures utilisés. -- Commencez par analyser l'application existante et identifier les composants clés et leurs dépendances. Créez 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, telle que Nette DI. -- Refactorisez progressivement les différentes parties de l'application pour utiliser l'Injection de Dépendances. Cela peut inclure la modification des constructeurs ou des méthodes pour accepter les dépendances comme paramètres. -- Modifiez les endroits du code où les objets avec des dépendances sont créés, afin que les dépendances soient injectées par le conteneur à la place. Cela peut inclure l'utilisation de factories. +- 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 la transition vers l'Injection de Dépendances est un investissement dans la qualité du code et la maintenabilité à long terme de l'application. Bien qu'il puisse être difficile d'apporter ces changements, le résultat devrait être un code plus propre, plus modulaire et facilement testable, prêt pour les extensions et la maintenance futures. +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 ? -------------------------------------------------------- -Il est préférable d'utiliser la [composition |nette:introduction-to-object-oriented-programming#Composition] plutôt que l'[héritage |nette:introduction-to-object-oriented-programming#Héritage], car elle sert à réutiliser le code sans avoir à se soucier des conséquences des changements. Elle offre donc un couplage plus lâche, où nous n'avons pas à craindre que la modification d'un code n'entraîne la nécessité de modifier un autre code dépendant. Un exemple typique est la situation appelée [enfer du constructeur |passing-dependencies#Constructor hell]. +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 le conteneur Nette DI en dehors de Nette ? ------------------------------------------------------------ +Peut-on utiliser Nette DI Container en dehors de Nette ? +-------------------------------------------------------- -Absolument. Le conteneur Nette DI fait partie de Nette, mais il est conçu comme une bibliothèque autonome qui peut être utilisée indépendamment des autres parties du framework. Il suffit de l'installer à l'aide de Composer, de créer un fichier de configuration avec la définition de vos services, puis d'utiliser quelques lignes de code PHP pour créer le conteneur DI. Et vous pouvez immédiatement commencer à profiter des avantages de l'Injection de Dépendances dans vos projets. +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. -L'utilisation concrète, y compris les codes, est décrite dans le chapitre [Conteneur Nette DI |nette-container]. +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 facile à lire, développé dans le cadre de Nette pour configurer les applications, les services et leurs dépendances. Par rapport à JSON ou YAML, il offre des possibilités beaucoup plus intuitives et flexibles à cet effet. En NEON, on peut décrire naturellement des liaisons qui seraient impossibles à écrire en Symfony & YAML, ou seulement au moyen d'une description complexe. +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 ? --------------------------------------------------------- -Bien que les fichiers NEON soient analysés très rapidement, cet aspect n'a aucune importance. La raison en est que l'analyse des fichiers n'a lieu qu'une seule fois lors du premier lancement de l'application. Ensuite, le code du conteneur DI est généré, enregistré sur le disque et exécuté à chaque requête ultérieure, sans qu'il soit nécessaire d'effectuer d'autres analyses. +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 fonctionne dans un environnement de production. Pendant le développement, les fichiers NEON sont analysés chaque fois que leur contenu est modifié, afin que le développeur dispose toujours d'un conteneur DI à jour. L'analyse elle-même est, comme mentionné, une question d'instant. +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 aux paramètres du fichier de configuration depuis ma classe ? ------------------------------------------------------------------------------ +Comment accéder dans ma classe aux paramètres du fichier de configuration ? +--------------------------------------------------------------------------- -Gardons à l'esprit la [Règle n°1 : faites-vous le passer |introduction#Règle n 1 : faites-vous passer les choses]. Si une classe nécessite des informations du fichier de configuration, nous n'avons pas besoin de réfléchir à la manière d'accéder à ces informations, nous les demandons simplement - par exemple, via le constructeur de la classe. Et nous effectuons le passage dans le 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 est passée au constructeur de la classe `MyClass` : +Dans cet exemple, `%myParameter%` est un placeholder pour la valeur du paramètre `myParameter`, qui sera passée au constructeur de `MyClass` : -```php +```neon # config.neon parameters: myParameter: Some value @@ -97,10 +97,27 @@ services: - MyClass(%myParameter%) ``` -Si vous souhaitez passer plusieurs paramètres ou utiliser l'autowiring, il est conseillé d'[encapsuler les paramètres dans un objet |best-practices:passing-settings-to-presenters]. +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. ? +----------------------------------------------------------------- -Nette supporte-t-il PSR-11 : Container interface ? --------------------------------------------------- +Un petit lexique des mots qui reviennent sans cesse autour de Nette DI, la plupart lors de l'[écriture d'extensions |extensions] : -Le conteneur Nette DI ne prend pas en charge PSR-11 directement. Cependant, si vous avez besoin d'interopérabilité entre le conteneur Nette DI et des bibliothèques ou des frameworks qui attendent une interface de conteneur PSR-11, vous pouvez créer un [simple adaptateur |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] qui servira de pont entre le conteneur Nette DI et PSR-11. +- **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 index dc60e290a1..454b2f853e 100644 --- a/dependency-injection/fr/global-state.texy +++ b/dependency-injection/fr/global-state.texy @@ -1,43 +1,43 @@ -État global et singletons -************************* +État global & singletons +************************ .[perex] -Avertissement : Les constructions suivantes sont le signe d'un code mal conçu : +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` -Certaines de ces constructions apparaissent-elles dans votre code ? Alors vous avez l'occasion de l'améliorer. Vous pensez peut-être qu'il s'agit de constructions courantes que vous voyez même dans les exemples de solutions de diverses bibliothèques et frameworks. Si c'est le cas, alors la conception de leur code n'est pas bonne. +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. -Nous ne parlons certainement pas ici d'une sorte de pureté académique. Toutes ces constructions ont une chose en commun : elles utilisent l'état global. Et celui-ci a un impact destructeur sur la qualité du code. Les classes mentent sur leurs dépendances. Le code devient imprévisible. Il embrouille les programmeurs et réduit leur efficacité. +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. -Couplage global ---------------- +Interconnexion globale +---------------------- -Dans un monde idéal, un objet ne devrait pouvoir 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 entre eux, alors ni `A` ni `B` ne peuvent accéder à l'autre objet ou modifier son état. C'est une propriété très souhaitable du code. C'est similaire à avoir une batterie et une ampoule ; l'ampoule ne s'allumera pas tant que vous ne la connecterez pas à la batterie avec un fil. +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. -Mais cela ne s'applique pas aux variables globales (statiques) ou aux singletons. L'objet `A` pourrait accéder *sans fil* à l'objet `C` et le modifier sans aucun passage de référence, en appelant `C::changeSomething()`. Si l'objet `B` s'empare également du `C` global, alors `A` et `B` peuvent s'influencer mutuellement via `C`. +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 dans le système une nouvelle forme de couplage *sans fil*, qui n'est pas visible de l'extérieur. Elle crée un écran de fumée compliquant la compréhension et l'utilisation du code. Pour que les développeurs comprennent réellement les dépendances, ils doivent lire chaque ligne du code source. Au lieu de simplement se familiariser avec l'interface des classes. De plus, il s'agit d'un couplage totalement inutile. L'état global est utilisé parce qu'il est facilement accessible de n'importe où et permet, par exemple, d'écrire dans la base de données via la méthode globale (statique) `DB::insert()`. Mais comme nous le montrerons, l'avantage que cela apporte est minime, tandis que les complications qu'il provoque sont fatales. +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 pas de différence entre une variable globale et une variable statique. Elles sont tout aussi nuisibles. +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. -Action fantôme à distance -------------------------- +L'action fantomatique à distance +-------------------------------- -"Action fantôme à distance" - c'est ainsi qu'Albert Einstein a fameusement nommé en 1935 un phénomène de la physique quantique qui lui donnait la chair de poule. -Il s'agit de l'intrication quantique, dont la particularité est que lorsque vous mesurez une information sur une particule, vous influencez instantanément l'autre particule, même si elles sont séparées par des millions d'années-lumière. Ce qui semble violer la loi fondamentale de l'univers selon laquelle rien ne peut se propager plus vite que la lumière. +"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 logiciel, nous pouvons appeler "action fantôme à distance" une situation où nous lançons un processus que nous croyons isolé (parce que nous ne lui avons passé aucune référence), mais où des interactions et des changements d'état inattendus se produisent dans des endroits éloignés du système, dont nous n'avions aucune idée. Cela ne peut se produire que par le biais de l'état global. +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éveloppeurs sur un projet doté d'une base de code vaste et mature. Votre nouveau responsable vous demande d'implémenter une nouvelle fonctionnalité et, en tant que bon développeur, vous commencez par écrire un test. Mais comme vous êtes nouveau dans le projet, vous effectuez de nombreux tests exploratoires du type "que se passe-t-il si j'appelle cette méthode". Et vous essayez d'écrire le test suivant : +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() @@ -47,17 +47,17 @@ function testCreditCardCharge() } ``` -Vous exécutez le code, peut-être plusieurs fois, et après un certain temps, vous remarquez des notifications de votre banque sur votre mobile indiquant qu'à chaque exécution, 100 dollars ont été débités de votre carte de crédit 🤦‍♂️ +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 débit réel d'argent ? Opérer avec une carte de crédit n'est pas facile. Vous devez communiquer avec un service web tiers, vous devez connaître l'URL de ce service web, vous devez vous connecter, etc. Aucune de ces informations n'est contenue dans le test. Pire encore, vous ne savez même pas où ces informations sont présentes, et donc vous ne savez pas non plus comment mocker les dépendances externes pour que chaque exécution n'entraîne pas à nouveau le débit de 100 dollars. Et comment, en tant que nouveau développeur, auriez-vous pu savoir que ce que vous alliez faire vous rendrait 100 dollars plus pauvre ? +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 $ ? -C'est l'action fantôme à distance ! +Voilà une action fantomatique à distance ! -Il ne vous reste plus qu'à fouiller longuement dans de nombreux codes sources, à interroger des collègues plus âgés et plus expérimentés, avant de comprendre comment fonctionnent les liens dans le projet. Cela est dû au fait qu'en regardant l'interface de la classe `CreditCard`, on ne peut pas déterminer l'état global qu'il faut initialiser. Même un coup d'œil au code source de la classe ne vous dira pas quelle méthode d'initialisation appeler. Au mieux, vous pouvez trouver une variable globale à laquelle on accède et essayer d'en déduire comment l'initialiser. +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 menteurs pathologiques. La carte de crédit prétend qu'il suffit de l'instancier et d'appeler la méthode `charge()`. En secret, elle coopère avec une autre classe `PaymentGateway`, qui représente la passerelle de paiement. Son interface dit également qu'elle peut être initialisée séparément, mais en réalité, elle extrait les informations d'identification d'un fichier de configuration, etc. Pour les développeurs qui ont écrit ce code, il est clair que `CreditCard` a besoin de `PaymentGateway`. Ils ont écrit le code de cette manière. Mais pour quiconque est nouveau dans le projet, c'est un mystère complet et cela entrave l'apprentissage. +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. **Laissez l'API déclarer les dépendances.** +Comment corriger la situation ? Facilement. **Que l'API déclare ses dépendances.** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -Remarquez comment les interconnexions à l'intérieur du code deviennent soudainement évidentes. Le fait que la méthode `charge()` déclare avoir besoin de `PaymentGateway` signifie que vous n'avez pas besoin de demander à qui que ce soit comment le code est interconnecté. Vous savez que vous devez créer son instance, et lorsque vous essayez de le faire, vous réalisez que vous devez fournir les paramètres d'accès. Sans eux, le code ne pourrait même pas s'exécuter. +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 maintenant mocker la passerelle de paiement, de sorte que 100 dollars ne vous seront pas facturés à chaque exécution du test. +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 fait que vos objets peuvent accéder secrètement à des choses qui ne sont pas déclarées dans leur API et, par conséquent, font de vos API des menteurs pathologiques. +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 n'y avez peut-être pas pensé de cette façon auparavant, mais chaque fois que vous utilisez l'état global, vous créez des canaux de communication secrets sans fil. L'action fantôme à distance oblige les développeurs à lire chaque ligne de code pour comprendre les interactions potentielles, réduit la productivité des développeurs et embrouille les nouveaux membres de l'équipe. Si c'est vous qui avez créé le code, vous connaissez les dépendances réelles, mais quiconque viendra après vous sera désemparé. +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. -N'écrivez pas de code qui utilise l'état global, préférez le passage de dépendances. C'est-à-dire l'injection de dépendances. +É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 le code qui utilise l'état global et les singletons, on n'est jamais sûr de quand et qui a modifié cet état. Ce risque apparaît dès l'initialisation. Le code suivant est censé créer une connexion à la base de données et initialiser la passerelle de paiement, mais il lève constamment une exception et trouver la cause est extrêmement long : +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 parcourir le code en détail pour découvrir que l'objet `PaymentGateway` accède sans fil à d'autres objets, dont certains nécessitent une connexion à la base de données. Il est donc nécessaire d'initialiser la base de données avant `PaymentGateway`. Cependant, l'écran de fumée de l'état global vous le cache. Combien de temps auriez-vous gagné si les API des classes individuelles ne mentaient pas et déclaraient leurs dépendances ? +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, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -Un problème similaire se pose lors de l'utilisation d'un accès global à la connexion à la base de données : +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; @@ -110,9 +110,9 @@ class Article } ``` -Lors de l'appel de la méthode `save()`, on n'est pas certain que la connexion à la base de données a déjà été créée et qui est responsable de sa création. Si nous voulons, par exemple, modifier la connexion à la base de données à la volée, par exemple pour des tests, nous devrions probablement créer d'autres méthodes comme `DB::reconnect(...)` ou `DB::reconnectForTest()`. +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()`. -Considérons un exemple : +Prenons un exemple : ```php $article = new Article; @@ -122,7 +122,7 @@ Foo::doSomething(); $article->save(); ``` -Où avons-nous la certitude que lors de l'appel de `$article->save()`, la base de données de test est réellement utilisée ? Et si la méthode `Foo::doSomething()` avait modifié la connexion globale à la base de données ? Pour le savoir, nous devrions examiner le code source de la classe `Foo` et probablement de nombreuses autres classes. Cette approche ne fournirait cependant qu'une réponse à court terme, car la situation pourrait changer à l'avenir. +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` ? @@ -143,11 +143,11 @@ class Article } ``` -Cela ne change absolument rien. Le problème est l'état global et peu importe dans quelle classe il se cache. Dans ce cas, comme dans le précédent, nous n'avons aucune idée, lors de l'appel de la méthode `$article->save()`, dans quelle base de données l'écriture aura lieu. N'importe qui à l'autre bout de l'application aurait pu modifier la base de données à tout moment en utilisant `Article::setDb()`. Sous notre nez. +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 cependant un moyen simple de traiter ce problème. Il suffit de laisser l'API déclarer les dépendances, ce qui garantit le bon fonctionnement. +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 @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -Grâce à cette approche, la crainte de modifications cachées et inattendues de la connexion à la base de données disparaît. Nous avons maintenant la certitude de l'endroit où l'article est enregistré et aucune modification du code à l'intérieur d'une autre classe non liée ne peut plus changer la situation. Le code n'est plus fragile, mais stable. +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. -N'écrivez pas de code qui utilise l'état global, préférez le passage de dépendances. C'est-à-dire l'injection de dépendances. +É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://en.wikipedia.org/wiki/Singleton_pattern de la célèbre publication du Gang of Four, limite une classe à une seule instance et offre un accès global à celle-ci. L'implémentation de ce patron ressemble généralement au code suivant : +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 @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // et d'autres méthodes remplissant les fonctions de la classe donnée + // et d'autres méthodes qui assurent les fonctions de la classe } ``` -Malheureusement, le singleton introduit un état global dans l'application. Et comme nous l'avons montré ci-dessus, l'état global est indésirable. C'est pourquoi le singleton est considéré comme un anti-patron. +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. Cependant, si vous devez garantir l'existence d'une seule instance d'une classe pour toute l'application, laissez cela au [conteneur DI |container]. Créez ainsi un singleton d'application, c'est-à-dire un service. De cette façon, la classe cessera de s'occuper d'assurer sa propre unicité (c'est-à-dire qu'elle n'aura pas de méthode `getInstance()` ni de variable statique) et ne remplira que ses fonctions. Elle cessera ainsi de violer le principe de responsabilité unique. +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. -État global versus tests ------------------------- +L'état global face aux tests +---------------------------- -Lors de l'écriture de tests, nous supposons que chaque test est une unité isolée et qu'aucun état externe n'y entre. Et aucun état ne quitte les tests. Une fois le test terminé, tout l'état lié au test devrait être automatiquement supprimé par le garbage collector. Grâce à cela, les tests sont isolés. Nous pouvons donc exécuter les tests dans n'importe quel ordre. +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. -Cependant, s'il y a des états globaux/singletons, toutes ces hypothèses agréables s'effondrent. L'état peut entrer et sortir du test. Soudain, l'ordre des tests peut avoir de l'importance. +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 pouvoir tester les singletons, les développeurs doivent souvent assouplir leurs propriétés, par exemple en permettant de remplacer l'instance par une autre. De telles solutions sont au mieux des hacks qui créent un code difficile à maintenir et à comprendre. Chaque test ou méthode `tearDown()` qui affecte un état global doit annuler ces changements. +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 grand casse-tête des tests unitaires ! +L'état global est le plus gros casse-tête des tests unitaires ! -Comment corriger la situation ? Facilement. N'écrivez pas de code qui utilise des singletons, préférez le passage de dépendances. C'est-à-dire l'injection de dépendances. +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'utilisation de singletons et de variables statiques, mais peut également concerner les 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 la valeur ne nous apporte aucune information nouvelle (`M_PI`) ou utile (`PREG_BACKTRACK_LIMIT_ERROR`) sont clairement acceptables. En revanche, les constantes qui servent de moyen de passer *sans fil* des informations à l'intérieur du code ne sont rien d'autre qu'une dépendance cachée. Comme `LOG_FILE` dans l'exemple suivant. L'utilisation de la constante `FILE_APPEND` est tout à fait correcte. +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 = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -Dans ce cas, nous devrions déclarer un paramètre dans le constructeur de la classe `Foo` pour qu'il fasse partie de l'API : +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 @@ -253,42 +253,42 @@ class Foo } ``` -Maintenant, nous pouvons passer l'information sur le chemin du fichier journal et la modifier facilement selon les besoins, ce qui facilite les tests et la maintenance du code. +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 tenons à souligner que l'utilisation de méthodes statiques et de fonctions globales n'est pas problématique en soi. Nous avons expliqué pourquoi l'utilisation de `DB::insert()` et de méthodes similaires est inappropriée, mais il s'agissait toujours uniquement d'une question d'état global stocké dans une variable statique. La méthode `DB::insert()` nécessite l'existence d'une variable statique car la connexion à la base de données y est stockée. Sans cette variable, il serait impossible d'implémenter la méthode. +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'utilisation de méthodes statiques et de fonctions déterministes, telles que `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` et bien d'autres, est parfaitement conforme à l'injection de dépendances. Ces fonctions renvoient toujours les mêmes résultats pour les mêmes paramètres d'entrée et sont donc prévisibles. Elles n'utilisent aucun état global. +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 des fonctions en PHP qui ne sont pas déterministes. Parmi elles, par exemple, la fonction `htmlspecialchars()`. Son troisième paramètre `$encoding`, s'il n'est pas spécifié, prend par défaut la valeur de l'option de configuration `ini_get('default_charset')`. C'est pourquoi il est recommandé de toujours spécifier ce paramètre et d'éviter ainsi un comportement éventuellement imprévisible de la fonction. Nette le fait systématiquement. +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, telles que `strtolower()`, `strtoupper()` et similaires, se comportaient de manière non déterministe dans un passé récent et dépendaient du paramètre `setlocale()`. Cela causait de nombreuses complications, le plus souvent lors du travail avec la langue turque. Celle-ci distingue en effet les lettres `I` majuscules et minuscules avec et sans point. Ainsi, `strtolower('I')` renvoyait le caractère `ı` et `strtoupper('i')` le caractère `İ`, ce qui entraînait l'apparition de nombreuses erreurs mystérieuses dans les applications. Ce problème a cependant été corrigé dans la version PHP 8.2 et les fonctions ne dépendent plus de la locale. +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 bel exemple de la façon dont l'état global a tourmenté des milliers de développeurs dans le monde entier. La solution a été de le remplacer par l'injection de dépendances. +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 est-il possible d'utiliser l'état global ? ------------------------------------------------- +Quand peut-on utiliser l'état global ? +-------------------------------------- -Il existe certaines situations spécifiques où il est possible d'utiliser l'état global. Par exemple, lors du débogage de code, lorsque vous devez afficher la valeur d'une variable ou mesurer la durée d'une certaine partie du programme. Dans de tels cas, qui concernent des actions temporaires qui seront ultérieurement supprimées du code, il est légitime d'utiliser un dumper ou un chronomètre globalement disponible. Ces outils ne font en effet pas partie de la conception du code. +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. -Un autre exemple sont les fonctions pour travailler avec les expressions régulières `preg_*`, qui stockent en interne les expressions régulières compilées dans un cache statique en mémoire. Ainsi, lorsque vous appelez la même expression régulière plusieurs fois à différents endroits du code, elle n'est compilée qu'une seule fois. Le cache économise les performances et est en même temps totalement invisible pour l'utilisateur, c'est pourquoi une telle utilisation peut être considérée comme légitime. +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 discuté des raisons pour lesquelles il est judicieux de : +Nous avons expliqué pourquoi il est judicieux de : -1) Supprimer toutes les variables statiques du code -2) Déclarer les dépendances -3) Et utiliser l'injection de dépendances +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. -Lorsque vous réfléchissez à la conception du code, gardez à l'esprit que chaque `static $foo` représente un problème. Pour que votre code soit un environnement respectant la DI, il est essentiel d'éradiquer complètement l'état global et de le remplacer par 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 processus, vous découvrirez peut-être qu'il est nécessaire de diviser une classe car elle a plus d'une responsabilité. N'ayez pas peur de cela ; visez le principe de responsabilité unique. +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, tels que [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.* +*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 index 543c963588..c801c7f8ea 100644 --- a/dependency-injection/fr/introduction.texy +++ b/dependency-injection/fr/introduction.texy @@ -1,58 +1,58 @@ -Qu'est-ce que l'Injection de Dépendances ? +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 toutes vos applications. Ce sont les fondations nécessaires pour écrire du code propre, compréhensible et maintenable. +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 ces règles et les suivez, Nette vous soutiendra à chaque étape. Il s'occupera des tâches routinières pour vous et vous offrira un maximum de confort, afin que vous puissiez vous concentrer sur la logique elle-même. +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. Vous n'avez rien à craindre. +Les principes que nous allons montrer ici sont assez simples. Il n'y a rien à craindre. -Vous souvenez-vous de votre premier programme ? +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 aurait probablement ressemblé à quelque chose comme ceci : +Nous ne savons pas dans quel langage vous l'avez écrit, mais si c'était en PHP, il ressemblait probablement à ceci : ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // affiche 24 +echo addition(23, 1); // affiche 24 ``` -Quelques lignes de code triviales, mais elles cachent tellement de concepts clés. Qu'il existe des variables. Que le code est divisé en unités plus petites, telles que des fonctions. Que nous leur passons des arguments d'entrée et qu'elles retournent des résultats. Il ne manque que les conditions et les boucles. +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 qu'une fonction reçoive des données d'entrée et retourne un résultat est un concept parfaitement compréhensible, utilisé également dans d'autres domaines, comme les mathématiques. +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, qui comprend son nom, une liste de paramètres et leurs types, et enfin le type de la valeur de retour. En tant qu'utilisateur, nous nous intéressons à la signature ; nous n'avons généralement pas besoin de connaître l'implémentation interne. +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. -Maintenant, imaginez que la signature de la fonction ressemble à ceci : +Imaginez maintenant que la signature de la fonction ressemble à ceci : ```php -function soucet(float $x): float +function addition(float $x): float ``` -Une somme avec un seul paramètre ? C'est étrange... Et comme ça ? +Une addition à un seul paramètre ? C'est étrange… Et celle-ci ? ```php -function soucet(): float +function addition(): float ``` -C'est vraiment très étrange, n'est-ce pas ? Comment la fonction est-elle utilisée ? +Là, c'est vraiment étrange, non ? Comment utilise-t-on cette fonction ? ```php -echo soucet(); // qu'est-ce que cela va afficher ? +echo addition(); // qu'est-ce que cela va afficher ? ``` -En regardant un tel code, nous serions confus. Non seulement un débutant ne le comprendrait pas, mais même un programmeur expérimenté ne comprendrait pas ce code. +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 demandez-vous à quoi ressemblerait réellement une telle fonction à l'intérieur ? Où obtiendrait-elle les opérandes ? Elle les obtiendrait probablement *d'une manière ou d'une autre* elle-même, peut-être comme ceci : +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 soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -Dans le corps de la fonction, nous avons découvert des liens cachés vers d'autres fonctions globales ou méthodes statiques. Pour savoir d'où proviennent réellement les opérandes, nous devons chercher plus loin. +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 par ici ! -------------- +Pas comme ça ! +-------------- -La conception que nous venons de montrer est l'essence de nombreuses caractéristiques négatives : +La conception que nous venons de montrer est à l'origine de nombreuses caractéristiques négatives : -- La signature de la fonction prétendait qu'elle n'avait pas besoin d'opérandes, ce qui nous a induits en erreur. -- Nous ne savons pas du tout comment faire pour que la fonction additionne deux autres nombres. -- Nous avons dû regarder dans le code pour savoir d'où elle tirait les opérandes. -- Nous avons découvert des liens cachés. -- Pour une compréhension complète, il faut également examiner ces liens. +- 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 vraiment la tâche d'une fonction d'addition d'obtenir des entrées ? Bien sûr que non. Sa responsabilité est uniquement l'addition elle-même. +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 solution est simple : revenir aux bases et simplement utiliser des paramètres : +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 soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -Règle n°1 : faites-vous passer les choses ------------------------------------------ +Règle n° 1 : laissez-vous les passer +------------------------------------ -La règle la plus importante est : **toutes les données dont une fonction ou une classe a besoin doivent lui être passées**. +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 puissent y accéder elles-mêmes, passez simplement les paramètres. Vous économiserez du temps nécessaire à l'invention de chemins cachés, qui n'amélioreront certainement pas votre code. +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 et partout cette règle, vous êtes sur la voie d'un code sans liens cachés. D'un code compréhensible non seulement par l'auteur, mais aussi par quiconque le lira après lui. Où tout est compréhensible à partir des signatures des fonctions et des classes, et il n'est pas nécessaire de chercher des secrets cachés dans l'implémentation. +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 est appelée techniquement **Injection de Dépendances**. Et ces données sont appelées **dépendances.** En fait, c'est simplement du passage de paramètres, rien de plus. +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, c'est-à-dire quelque chose de diamétralement différent. Nous aborderons les conteneurs plus tard. +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 quel est le rapport avec les classes ? Une classe est une entité plus complexe qu'une simple fonction, mais la règle n°1 s'applique également ici sans exception. Il existe simplement [plus d'options pour passer les arguments|passing-dependencies]. Par exemple, de manière assez similaire au cas d'une fonction : +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 Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` -Ou en utilisant d'autres méthodes, ou directement le constructeur : +Ou à l'aide d'autres méthodes, ou directement du constructeur : ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,26 +132,25 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` -Les deux exemples sont entièrement conformes à l'injection de dépendances. +Les deux exemples respectent pleinement l'injection de dépendances. -Exemples réels --------------- +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. -Prenons une classe `Article` représentant un article de blog : +Soit une classe `Article` représentant un article de blog : ```php class Article @@ -167,18 +166,18 @@ class Article } ``` -et l'utilisation sera la suivante : +et son utilisation sera la suivante : ```php $article = new Article; -$article->title = '10 choses que vous devez savoir sur la perte de poids'; -$article->content = 'Chaque année, des millions de personnes dans ...'; +$article->title = '10 choses à savoir pour perdre du poids'; +$article->content = 'Chaque année, des millions de personnes ...'; $article->save(); ``` -La méthode `save()` enregistre l'article dans une table de base de données. L'implémenter avec [Nette Database |database:] serait un jeu d'enfant, s'il n'y avait pas un hic : où `Article` obtient-il la connexion à la base de données, c'est-à-dire l'objet de la classe `Nette\Database\Connection` ? +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 beaucoup d'options. Il peut la prendre quelque part dans une variable statique. Ou hériter d'une classe qui assure la connexion à la base de données. Ou utiliser un [singleton |global-state#Singleton]. Ou les façades, qui sont utilisées dans Laravel : +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; @@ -199,17 +198,17 @@ class Article } ``` -Génial, nous avons résolu le problème. +Parfait, nous avons résolu le problème. -Ou pas ? +Vraiment ? -Rappelons la [##Règle n°1 : faites-vous passer les choses] : 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 sale plein de liens cachés, d'incompréhensibilité, et le résultat sera une application qu'il sera pénible de maintenir et de développer. +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` ne sait pas où la méthode `save()` enregistre l'article. Dans une table de base de données ? Laquelle, la production ou le test ? Et comment peut-on changer cela ? +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 trouve l'utilisation de la méthode `DB::insert()`. Il doit donc chercher plus loin comment cette méthode obtient la connexion à la base de données. Et les liens cachés peuvent former une chaîne assez longue. +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 liens cachés, de façades Laravel ou de variables statiques. Dans un code propre et bien conçu, on passe des arguments : +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 @@ -224,7 +223,7 @@ class Article } ``` -Ce sera encore plus pratique, comme nous le verrons plus loin, avec le constructeur : +Encore plus pratique, comme nous le verrons plus loin, est l'utilisation du constructeur : ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -Si vous êtes un programmeur expérimenté, vous pensez peut-être que `Article` ne devrait pas du tout avoir de méthode `save()`, qu'il devrait représenter purement un composant de données et que l'enregistrement devrait être géré par un dépôt séparé. C'est logique. Mais cela nous éloignerait beaucoup du sujet, qui est l'injection de dépendances, et de l'effort de fournir des exemples simples. +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 nécessite, par exemple, une base de données pour fonctionner, n'inventez pas d'où l'obtenir, mais faites-la vous passer. Par exemple, comme paramètre du constructeur ou d'une autre méthode. Admettez les dépendances. Admettez-les dans l'API de votre classe. Vous obtiendrez un code compréhensible et prévisible. +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 enregistre les messages d'erreur : +Et que dire de cette classe, qui journalise les messages d'erreur : ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,11 +261,11 @@ class Logger } ``` -Que pensez-vous, avons-nous respecté la [##Règle n°1 : faites-vous passer les choses] ? +Qu'en pensez-vous, avons-nous respecté la [#Règle n° 1 : laissez-vous les passer] ? -Nous ne l'avons pas respectée. +Non. -L'information clé, c'est-à-dire le répertoire avec le fichier journal, la classe *l'obtient elle-même* à partir d'une constante. +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 : @@ -276,7 +275,7 @@ $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 répondre à la question de savoir où les messages sont écrits ? Auriez-vous pensé que pour fonctionner, l'existence de la constante `LOG_DIR` est nécessaire ? Et pourriez-vous créer une deuxième instance qui écrirait ailleurs ? Certainement pas. +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 : @@ -295,24 +294,24 @@ class Logger } ``` -La classe est maintenant beaucoup plus compréhensible, configurable et donc plus utile. +La classe est maintenant bien plus compréhensible, configurable et donc plus utile. ```php -$logger = new Logger('/chemin/vers/log.txt'); +$logger = new Logger('/path/to/log.txt'); $logger->log('La température est de 15 °C'); ``` -Mais ça ne m'intéresse pas ! ----------------------------- +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 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 me soucier de l'endroit. Qu'il utilise la configuration globale. »* +*"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 pertinentes. +Ce sont des remarques légitimes. -Comme exemple, montrons une classe qui envoie des newsletters et enregistre le résultat : +Prenons comme exemple une classe qui distribue des newsletters et journalise le résultat : ```php class NewsletterDistributor @@ -325,18 +324,18 @@ class NewsletterDistributor $logger->log('Les e-mails ont été envoyés'); } catch (Exception $e) { - $logger->log('Une erreur est survenue lors de l\'envoi'); + $logger->log('Une erreur est survenue pendant l\'envoi'); throw $e; } } } ``` -Le `Logger` amélioré, qui n'utilise plus la constante `LOG_DIR`, nécessite le chemin du fichier dans le constructeur. Comment résoudre cela ? La classe `NewsletterDistributor` ne se soucie pas du tout de l'endroit où les messages sont écrits, elle veut juste les écrire. +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 à nouveau la [##Règle n°1 : faites-vous passer les choses] : toutes les données dont la classe a besoin, nous les lui passons. +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 signifie donc que nous passons le chemin du journal via le constructeur, que nous utilisons ensuite lors de la création de l'objet `Logger` ? +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 @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -Pas comme ça ! Le chemin **n'appartient pas** aux données dont la classe `NewsletterDistributor` a besoin ; ce sont les besoins de `Logger`. Voyez-vous la différence ? La classe `NewsletterDistributor` a besoin du logger en tant que tel. Donc, nous le passons : +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 @@ -368,30 +367,30 @@ class NewsletterDistributor $this->logger->log('Les e-mails ont été envoyés'); } catch (Exception $e) { - $this->logger->log('Une erreur est survenue lors de l\'envoi'); + $this->logger->log('Une erreur est survenue pendant l\'envoi'); throw $e; } } } ``` -Maintenant, il est clair d'après les signatures de la classe `NewsletterDistributor` que le logging fait partie de sa fonctionnalité. Et la tâche de remplacer le logger par un autre, par exemple pour les tests, est tout à fait triviale. De plus, si le constructeur de la classe `Logger` changeait, cela n'aurait aucune incidence sur notre classe. +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 vous appartient ------------------------------------------ +Règle n° 2 : prenez ce qui est à vous +------------------------------------- -Ne vous laissez pas tromper et ne vous faites pas passer les dépendances de vos dépendances. Faites-vous passer uniquement vos propres dépendances. +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 utilisant d'autres objets sera totalement indépendant des changements dans leurs constructeurs. Son API sera plus véridique. Et surtout, il sera trivial de remplacer ces dépendances par d'autres. +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. -Nouveau membre de la famille ----------------------------- +Un nouveau membre de la famille +------------------------------- -L'équipe de développement a décidé de créer un deuxième logger, qui écrit dans la base de données. Nous créons donc une classe `DatabaseLogger`. Nous avons donc deux classes, `Logger` et `DatabaseLogger`, l'une écrit dans un fichier, l'autre dans la base de données... ne trouvez-vous pas quelque chose d'étrange dans ce nommage ? Ne serait-il pas préférable de renommer `Logger` en `FileLogger` ? Certainement oui. +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 nous allons le faire intelligemment. Sous le nom d'origine, nous créons une interface : +Mais faisons-le intelligemment. Nous créons une interface sous le nom d'origine : ```php interface Logger @@ -410,17 +409,17 @@ class DatabaseLogger implements Logger // ... ``` -Et grâce à cela, il ne sera pas nécessaire de changer quoi que ce soit dans le reste du code où le logger est utilisé. Par exemple, le constructeur de la classe `NewsletterDistributor` sera toujours satisfait d'exiger `Logger` comme paramètre. Et ce sera à nous de décider quelle instance lui passer. +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 ne donnons jamais aux noms d'interfaces le suffixe `Interface` ou le préfixe `I`.** Sinon, il ne serait pas possible de développer le code aussi joliment. +**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 de logger, qu'il soit fichier ou base de données, et simplement la passer partout où quelque chose est loggué, la situation est tout à fait différente dans le cas de la classe `Article`. En effet, nous créons ses instances selon les besoins, parfois plusieurs fois. Comment gérer la dépendance à la base de données dans son constructeur ? +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 ? -Comme exemple, prenons un contrôleur qui, après soumission d'un formulaire, doit enregistrer l'article dans la base de données : +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 @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -Une solution possible s'offre directement : nous nous faisons passer l'objet de la base de données via le constructeur dans `EditController` et utilisons `$article = new Article($this->db)`. +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)`. -Comme dans le cas précédent avec `Logger` et le chemin du fichier, ce n'est pas la bonne approche. La base de données n'est pas une dépendance de `EditController`, mais de `Article`. Passer la base de données va donc à l'encontre de la [##Règle n°2 : prenez ce qui vous appartient]. Si le constructeur de la classe `Article` change (un nouveau paramètre est ajouté), il faudra également modifier le code à tous les endroits où une instance est créée. Ouf. +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, que suggérez-vous ? +Houston, quelle est votre proposition ? -Règle n°3 : laissez faire la factory ------------------------------------- +Règle n° 3 : laissez faire la factory +------------------------------------- -En supprimant les liens cachés et en passant toutes les dépendances comme arguments, nous avons obtenu des classes plus configurables et flexibles. Et par conséquent, nous avons besoin de quelque chose d'autre pour créer et configurer ces classes plus flexibles. Nous appellerons cela des factories. +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, laissez la création de ses instances à une factory. +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 un remplacement plus intelligent de l'opérateur `new` dans le monde de l'injection de dépendances. +Les factories sont une alternative plus intelligente à l'opérateur `new` dans le monde de l'injection de dépendances. .[note] -Ne confondez pas avec le patron de conception *factory method*, qui décrit une manière spécifique d'utiliser les factories et n'est pas lié à ce sujet. +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 produit et configure des objets. La classe produisant `Article` s'appellera `ArticleFactory` et pourrait ressembler à ceci : +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 @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -Si la signature du constructeur de la classe `Article` change à ce moment, la seule partie du code qui doit y réagir est la factory `ArticleFactory` elle-même. Tout autre code qui travaille avec les objets `Article`, comme `EditController`, ne sera en aucun cas affecté. +À 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é. -Peut-être vous tapez-vous sur le front maintenant, vous demandant si nous avons vraiment amélioré les choses. La quantité de code a augmenté et tout cela commence à paraître suspectement compliqué. +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 Nette DI. Et il a plusieurs atouts dans sa manche qui simplifieront énormément 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] : +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 @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -Mais nous anticipons, attendez encore un peu :-) +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 aux classes de +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) [se faire passer les dépendances dont elles ont besoin |#Règle n 1 : faites-vous passer les choses] -2) [et inversement, ne pas se faire passer ce dont elles n'ont pas directement besoin |#Règle n 2 : prenez ce qui vous appartient] -3) [et que les objets avec dépendances sont mieux fabriqués dans des factories |#Règle n 3 : laissez faire la factory] +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 peut ne pas sembler évident au premier abord, mais ces trois règles ont des conséquences considérables. Elles conduisent à une vision radicalement différente de la conception du code. Est-ce que ça en vaut la peine ? Les programmeurs qui ont abandonné leurs anciennes habitudes et ont commencé à utiliser systématiquement l'injection de dépendances considèrent cette étape comme un moment crucial de leur vie professionnelle. Un monde d'applications claires et maintenables s'est ouvert à eux. +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. -Mais que se passe-t-il si le code n'utilise pas systématiquement l'injection de dépendances ? Que se passe-t-il s'il est basé sur des méthodes statiques ou des singletons ? Cela pose-t-il des problèmes ? [Oui, et de très importants |global-state]. +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 index 37afd6f97a..e318c94226 100644 --- a/dependency-injection/fr/nette-container.texy +++ b/dependency-injection/fr/nette-container.texy @@ -1,10 +1,10 @@ -Conteneur Nette DI +Nette DI Container ****************** .[perex] -Nette DI est l'une des bibliothèques les plus intéressantes de Nette. Elle peut générer et mettre à jour automatiquement des conteneurs DI compilés, qui sont extrêmement rapides et incroyablement faciles à configurer. +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 est généralement définie à 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 : +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: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -La notation est vraiment concise. +La syntaxe est très concise. -Toutes les dépendances déclarées dans les constructeurs des classes `ArticleFactory` et `UserController` sont découvertes et passées automatiquement par Nette DI grâce à ce qu'on appelle l'[autowiring|autowiring], il n'est donc pas nécessaire de spécifier quoi que ce soit dans le fichier de configuration. Ainsi, même si les paramètres changent, vous n'avez rien à modifier dans la configuration. Le conteneur Nette se régénère automatiquement. Vous pouvez ainsi vous concentrer uniquement sur le développement de l'application. +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 via des setters, nous utilisons la section [setup |services#Setup]. +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 étudier. Grâce à cela, vous voyez exactement comment fonctionne le conteneur. Vous pouvez également le déboguer dans votre IDE et le parcourir pas à pas. Et surtout : le PHP généré est extrêmement rapide. +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 peut également générer du code pour les [factories|factory] sur la base d'une interface fournie. Par conséquent, au lieu de la classe `ArticleFactory`, il nous suffira de créer uniquement une interface dans l'application : +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 @@ -42,13 +42,13 @@ Vous trouverez l'exemple complet [sur GitHub|https://github.com/nette-examples/d Utilisation autonome -------------------- -Déployer la bibliothèque Nette DI dans une application est très facile. D'abord, nous l'installons avec Composer (car télécharger des zips est tellement dépassé) : +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 crée une instance du conteneur DI selon la configuration stockée dans le fichier `config.neon` : +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'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -Le conteneur n'est généré qu'une seule fois, son code est écrit dans le cache (répertoire `__DIR__ . '/temp'`) et lors des requêtes suivantes, il est simplement chargé à partir de là. +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à. -Pour créer et obtenir des services, on utilise les méthodes `getService()` ou `getByType()`. C'est ainsi que nous créons l'objet `UserController` : +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 -$controller = $container->getByType(UserController::class); +$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 automatiquement si une classe ou un fichier de configuration est modifié. Il suffit d'indiquer `true` comme deuxième argument dans le constructeur de `ContainerLoader`. +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); ``` -Utilisation avec le framework Nette ------------------------------------ +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 limitée aux applications écrites avec Nette Framework, vous pouvez le déployer n'importe où avec seulement 3 lignes de code. Cependant, si vous développez des applications avec Nette Framework, la configuration et la création du conteneur sont gérées par [Bootstrap |application:bootstrapping#Configuration du Conteneur DI]. +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 index fe05ae4015..9276d54c77 100644 --- a/dependency-injection/fr/passing-dependencies.texy +++ b/dependency-injection/fr/passing-dependencies.texy @@ -3,22 +3,22 @@ Passage des dépendances <div class=perex> -Les arguments, ou dans la terminologie DI "dépendances", peuvent être passés aux classes de ces manières principales : +Les arguments, ou 'dépendances' dans la terminologie DI, peuvent être passés aux classes des principales façons suivantes : -* passage par constructeur -* passage par méthode (appelée setter) -* assignation à une variable -* méthode, annotation ou attribut *inject* +* 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> -Nous allons maintenant montrer les différentes variantes avec des exemples concrets. +Illustrons chaque variante par des exemples concrets. -Passage par constructeur -======================== +Injection par le constructeur +============================= -Les dépendances sont passées au moment de la création de l'objet comme arguments du constructeur : +Les dépendances sont fournies comme arguments du constructeur au moment où l'objet est instancié : ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -Cette forme convient aux dépendances obligatoires dont la classe a absolument besoin pour fonctionner, car sans elles, l'instance ne pourra pas être créée. +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 forme d'écriture plus courte ([constructor property promotion |https://blog.nette.org/fr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), qui est fonctionnellement équivalente : +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 @@ -49,7 +49,7 @@ class MyClass } ``` -Depuis PHP 8.1, la variable peut être marquée avec l'indicateur `readonly`, qui déclare que le contenu de la variable ne changera plus : +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 @@ -62,13 +62,13 @@ class MyClass } ``` -Le conteneur DI passe automatiquement les dépendances au constructeur via l'[autowiring |autowiring]. Les arguments qui ne peuvent pas être passés de cette manière (par exemple, les chaînes de caractères, les nombres, les booléens) sont [écrits dans la configuration |services#Arguments]. +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ésigne la situation où un enfant hérite d'une classe parente dont le constructeur requiert des dépendances, et en même temps, l'enfant requiert également des dépendances. Il doit alors reprendre et passer également celles du parent : +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 @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -Le problème survient lorsque nous voulons changer le constructeur de la classe `BaseClass`, par exemple lorsqu'une nouvelle dépendance est ajoutée. Il est alors nécessaire de modifier également tous les constructeurs des enfants. Ce qui transforme une telle modification en enfer. +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 éviter cela ? La solution est de **préférer la [composition plutôt que l'héritage |faq#Pourquoi la composition est-elle préférée à l héritage]**. +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 concevrons donc le code différemment. 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`, elle se fera passer cette fonctionnalité comme une dépendance : +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 @@ -125,10 +125,10 @@ final class MyClass ``` -Passage par setter -================== +Injection par setter +==================== -Les dépendances sont passées en appelant une méthode qui les stocke dans une variable privée. La convention habituelle pour nommer ces méthodes est la forme `set*()`, c'est pourquoi on les appelle setters, mais elles peuvent bien sûr s'appeler autrement. +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 @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -Cette méthode convient aux dépendances facultatives qui ne sont pas nécessaires au fonctionnement de la classe, car il n'est pas garanti que l'objet reçoive réellement la dépendance (c'est-à-dire que l'utilisateur appelle la méthode). +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 de manière répétée et de changer ainsi la dépendance. Si cela n'est pas souhaité, nous ajoutons un contrôle dans la méthode, ou à partir de PHP 8.1, nous marquons la propriété `$cache` avec l'indicateur `readonly`. +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 @@ -157,14 +157,14 @@ class MyClass public function setCache(Cache $cache): void { if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); + throw new RuntimeException('La dépendance a déjà été définie'); } $this->cache = $cache; } } ``` -L'appel du setter est défini dans la configuration du conteneur DI dans la [clé setup |services#Setup]. Ici aussi, le passage automatique des dépendances via l'autowiring est utilisé : +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: @@ -174,10 +174,10 @@ services: ``` -Assignation à une variable -========================== +Injection dans une propriété +============================ -Les dépendances sont passées en écrivant directement dans la variable membre : +Les dépendances sont fournies par écriture directe dans une propriété de l'objet : ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -Cette méthode est considérée comme inappropriée car la variable membre doit être déclarée comme `public`. Par conséquent, nous n'avons aucun contrôle sur le fait que la dépendance passée sera réellement du type donné (valable avant PHP 7.4) et nous perdons la possibilité de réagir à la dépendance nouvellement assignée avec notre propre code, par exemple pour empêcher un changement ultérieur. En même temps, la variable devient partie intégrante de l'interface publique de la classe, ce qui peut ne pas être souhaitable. +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'assignation de la variable est définie dans la configuration du conteneur DI dans la [section setup |services#Setup] : +L'affectation de la propriété se définit dans la configuration du conteneur DI, dans la [section setup |services#Setup] : ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -Alors que les trois méthodes précédentes sont généralement valables dans tous les langages orientés objet, l'injection par méthode, annotation ou attribut *inject* est spécifique uniquement aux presenters dans Nette. Un [chapitre distinct |best-practices:inject-method-attribute] leur est consacré. +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 convient au contraire aux dépendances facultatives, ou aux dépendances qu'il est possible de modifier ultérieurement. -- Les variables publiques ne sont pas appropriées. +- 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 index a85d365709..8275eb3d75 100644 --- a/dependency-injection/fr/services.texy +++ b/dependency-injection/fr/services.texy @@ -2,16 +2,16 @@ Définition des services *********************** .[perex] -La configuration est l'endroit où nous apprenons au conteneur DI comment assembler les différents services et comment les connecter à d'autres dépendances. Nette fournit une manière très claire et élégante d'y parvenir. +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` dans le fichier de configuration au format NEON est l'endroit où nous définissons nos propres services et leurs configurations. Voyons un exemple simple de définition d'un service nommé `database`, qui représente une instance de la classe `PDO` : +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 indiquée aboutira à la méthode factory suivante dans le [conteneur DI|container] : +La configuration ci-dessus produit la méthode fabrique suivante dans le [conteneur DI|container] : ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -Les noms des services nous permettent d'y faire référence dans d'autres parties du fichier de configuration, sous la forme `@nomDuService`. S'il n'est pas nécessaire de nommer le service, nous pouvons simplement utiliser un tiret : +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 obtenir un service du conteneur DI, nous pouvons utiliser la méthode `getService()` avec le nom du service comme paramètre, ou la méthode `getByType()` avec le type du service : +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'); @@ -35,17 +35,17 @@ $database = $container->getByType(PDO::class); ``` -Création de service -=================== +Création de services +==================== -La plupart du temps, nous créons un service simplement en créant une instance d'une certaine classe. Par exemple : +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 devons étendre la configuration avec d'autres clés, la définition peut être répartie sur plusieurs lignes : +Si nous avons besoin d'étoffer la configuration avec d'autres clés, la définition peut s'étaler sur plusieurs lignes : ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -La clé `create` a un alias `factory`, les deux variantes sont courantes en pratique. Cependant, nous recommandons d'utiliser `create`. +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 de création peuvent alternativement être écrits dans la clé `arguments` : +Les arguments du constructeur ou de la méthode fabrique peuvent également être indiqués à l'aide de la clé `arguments` : ```neon services: @@ -65,7 +65,7 @@ services: 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 d'appels de méthodes statiques ou de méthodes d'autres services : +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: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -Notez que pour la simplicité, `::` est utilisé à la place de `->`, voir [##expressions]. Ces méthodes factory seront générées : +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 @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -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 spécifié, nous devons explicitement indiquer ce type dans la configuration : +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: @@ -100,14 +100,14 @@ services: Arguments ========= -Nous passons les arguments au constructeur et aux méthodes d'une manière très similaire à PHP lui-même : +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 répartir les arguments sur des lignes séparées. Dans ce cas, l'utilisation de virgules est facultative : +Pour une meilleure lisibilité, nous pouvons énumérer les arguments sur des lignes distinctes. Dans ce cas, les virgules deviennent facultatives : ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -Vous pouvez également nommer les arguments et ne pas vous soucier de leur ordre : +Vous pouvez aussi nommer les arguments, ce qui vous dispense de vous soucier de leur ordre : ```neon services: @@ -129,14 +129,14 @@ services: ) ``` -Si vous souhaitez omettre certains arguments et utiliser leur valeur par défaut ou injecter un service via l'[autowiring|autowiring], utilisez le trait de soulignement : +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%) ``` -Comme arguments, on peut passer des services, utiliser des paramètres et bien plus encore, voir [##expressions]. +Les arguments peuvent contenir des services, des paramètres et bien plus encore, voir [#Langage d'expressions]. Setup @@ -152,7 +152,7 @@ services: - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ``` -Cela ressemblerait à ceci en PHP : +En PHP, cela donnerait ceci : ```php public function createServiceDatabase(): PDO @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -En plus des appels de méthodes, il est également possible de passer des valeurs aux propriétés. L'ajout d'un élément à un tableau est également pris en charge, ce qui doit être écrit entre guillemets pour ne pas entrer en conflit avec la syntaxe NEON : +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: @@ -174,7 +174,7 @@ services: - '$onClick[]' = [@bar, clickHandler] ``` -Ce qui ressemblerait au code PHP suivant : +Ce qui, en code PHP, donnerait ceci : ```php public function createServiceFoo(): Foo @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -Dans le setup, on peut cependant aussi appeler des méthodes statiques ou des méthodes d'autres services. Si vous avez besoin de passer le service actuel comme argument, indiquez-le comme `@self` : +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: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -Notez que pour la simplicité, `::` est utilisé à la place de `->`, voir [##expressions]. Une telle méthode factory sera générée : +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 @@ -210,49 +210,49 @@ public function createServiceFoo(): Foo ``` -Expressions -=========== +Langage d'expressions +===================== -Nette DI nous offre des moyens d'expression extraordinairement riches, grâce auxquels nous pouvons écrire presque n'importe quoi. Dans les fichiers de configuration, nous pouvons ainsi utiliser des [paramètres |configuration#Paramètres] : +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 du paramètre sous la clé +# valeur d'un paramètre sous une clé %mailer.user% # paramètre à l'intérieur d'une chaîne '%wwwDir%/images' ``` -De plus, créer des objets, appeler des méthodes et des fonctions : +Nous pouvons également créer des objets, appeler des méthodes et des fonctions : ```neon -# création d'objet +# créer un objet DateTime() -# appel de méthode statique +# appeler une méthode statique Collator::create(%locale%) -# appel de fonction PHP +# appeler une fonction PHP ::getenv(DB_USER) ``` -Se référer aux services soit par leur nom, soit par leur type : +Référencer les services par leur nom ou par leur type : ```neon -# service par nom +# service par son nom @database -# service par type +# service par son type @Nette\Database\Connection ``` Utiliser la syntaxe first-class callable : .{data-version:3.2.0} ```neon -# création d'un callback, équivalent à [@user, logout] +# créer un callback, équivalent à [@user, logout] @user::logout(...) ``` @@ -262,11 +262,21 @@ Utiliser des constantes : # constante de classe FilesystemIterator::SKIP_DOTS -# constante globale obtenue avec la fonction PHP constant() -::constant(PHP_VERSION) +# obtenir une constante globale à l'aide de la fonction PHP constant() +::constant(\PHP_VERSION) ``` -Les appels de méthodes peuvent être chaînés comme en PHP. Seulement pour la simplicité, `::` est utilisé à la place de `->` : +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') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -Vous pouvez utiliser ces expressions n'importe où, lors de la [création de services |#Création de service], dans les [#arguments], dans la section [#setup] ou les [paramètres |configuration#Paramètres] : +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: @@ -293,12 +303,12 @@ services: Fonctions spéciales ------------------- -Dans les fichiers de configuration, vous pouvez utiliser ces fonctions spéciales : +Dans les fichiers de configuration, vous pouvez utiliser les fonctions spéciales suivantes : -- `not()` négation de la valeur -- `bool()`, `int()`, `float()`, `string()` conversion sans perte vers le type donné -- `typed()` crée un tableau de tous les services du type spécifié -- `tagged()` crée un tableau de tous les services avec le tag donné +- `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: @@ -308,18 +318,18 @@ services: ) ``` -Contrairement à la conversion de type classique en PHP, comme par exemple `(int)`, la conversion sans perte lèvera une exception pour les valeurs non numériques. +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 donné (classe ou interface). Elle omet les services dont l'autowiring est désactivé. Il est possible d'indiquer plusieurs types séparés par une virgule. +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) ) ``` -Vous pouvez également passer un tableau de services d'un certain type comme argument automatiquement via l'[autowiring |autowiring#Tableau de services]. +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 ensuite un tableau de tous les services avec un certain tag. Ici aussi, vous pouvez spécifier plusieurs tags séparés par une virgule. +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: @@ -330,7 +340,7 @@ services: Autowiring ========== -La clé `autowired` permet d'influencer le comportement de l'autowiring pour un service spécifique. Pour plus de détails, voir le [chapitre sur l'autowiring|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: @@ -340,10 +350,10 @@ services: ``` -Services Lazy .{data-version:3.2.4} +Services lazy .{data-version:3.2.4} =================================== -Le chargement paresseux (lazy loading) est une technique qui reporte la création d'un service jusqu'au moment où il est réellement nécessaire. Dans la configuration globale, il est possible d'[activer la création lazy |configuration#Services paresseux] pour tous les services en même temps. Pour des services individuels, vous pouvez ensuite surcharger ce comportement : +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: @@ -352,16 +362,20 @@ services: lazy: false ``` -Lorsqu'un service est défini comme lazy, lors de sa demande depuis le conteneur DI, nous recevons un objet placeholder spécial. Celui-ci ressemble et se comporte comme le service réel, mais l'initialisation réelle (appel du constructeur et du setup) n'a lieu qu'au premier appel de l'une de ses méthodes ou propriétés. +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 paresseux ne peut être utilisé que pour les classes utilisateur, pas pour les classes internes de PHP. Nécessite PHP 8.4 ou plus récent. +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 supplémentaires aux services. Vous pouvez ajouter un ou plusieurs tags à un service : +Les tags servent à ajouter des informations complémentaires aux services. Vous pouvez attribuer un ou plusieurs tags à un service : ```neon services: @@ -371,7 +385,7 @@ services: - cached ``` -Les tags peuvent également porter des valeurs : +Les tags peuvent aussi porter des valeurs : ```neon services: @@ -381,26 +395,26 @@ services: logger: monolog.logger.event ``` -Pour obtenir tous les services avec certains tags, vous pouvez utiliser la fonction `tagged()` : +Pour récupérer tous les services associés à des tags précis, vous pouvez utiliser la fonction `tagged()` : ```neon services: - LoggersDependent( tagged(logger) ) ``` -Dans le conteneur DI, vous pouvez obtenir les noms de tous les services avec un certain tag en utilisant la méthode `findByTag()` : +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 contenant le nom du service et la valeur du tag +// $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 +Mode inject =========== -À l'aide de l'indicateur `inject: true`, le passage des dépendances via les variables publiques avec l'annotation [inject |best-practices:inject-method-attribute#Attributs Inject] et les méthodes [inject*() |best-practices:inject-method-attribute#Méthodes inject] est activé. +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: @@ -409,13 +423,13 @@ services: inject: true ``` -Par défaut, `inject` est activé uniquement pour les presenters. +Par défaut, le mode `inject` n'est activé que pour les presenters. Modification des services ========================= -Le conteneur DI contient de nombreux services qui ont été ajoutés via une extension intégrée ou [utilisateur|extensions]. Vous pouvez modifier les définitions de ces services directement dans la configuration. Par exemple, vous pouvez changer la classe du service `application.application`, qui est par défaut `Nette\Application\Application`, en une autre : +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: @@ -424,9 +438,9 @@ services: alteration: true ``` -L'indicateur `alteration` est informatif et indique que nous modifions simplement un service existant. +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 également compléter le setup : +Nous pouvons aussi compléter le setup : ```neon services: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -Lors de la réécriture d'un service, nous pouvons vouloir supprimer les arguments d'origine, les éléments de setup ou les tags, ce à quoi sert `reset` : +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: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -Si vous souhaitez supprimer un service ajouté par une extension, vous pouvez le faire comme ceci : +Si vous voulez supprimer un service ajouté par une extension, vous pouvez procéder ainsi : ```neon services: diff --git a/dependency-injection/fr/upgrading.texy b/dependency-injection/fr/upgrading.texy new file mode 100644 index 0000000000..eeeb7574c3 --- /dev/null +++ b/dependency-injection/fr/upgrading.texy @@ -0,0 +1,49 @@ +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/hu/@home.texy b/dependency-injection/hu/@home.texy deleted file mode 100644 index efbfe2814b..0000000000 --- a/dependency-injection/hu/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -A Dependency Injection egy tervezési minta, amely alapvetően megváltoztatja a kódra és a fejlesztésre vonatkozó nézeteit. Megnyitja az utat a tisztán megtervezett és fenntartható alkalmazások világába. - -- [Mi az a Dependency Injection? |introduction] -- [Globális állapot és singletonok |global-state] -- [Függőségek átadása |passing-dependencies] -- [Mi az a DI konténer? |container] -- [Gyakran Ismételt Kérdések|faq] - - -A `nette/di` csomag egy rendkívül fejlett, fordított DI konténert biztosít PHP-hoz. - -- [Nette DI Konténer |nette-container] -- [Konfiguráció |configuration] -- [Szolgáltatások definiálása |services] -- [Autowiring |autowiring] -- [Generált factory-k |factory] -- [Bővítmények készítése Nette DI-hez|extensions] diff --git a/dependency-injection/hu/@left-menu.texy b/dependency-injection/hu/@left-menu.texy deleted file mode 100644 index c82c0c7af5..0000000000 --- a/dependency-injection/hu/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Mi az a DI? |introduction] -- [Globális állapot és singletonok |global-state] -- [Függőségek átadása |passing-dependencies] -- [Mi az a DI konténer? |container] -- [Gyakran Ismételt Kérdések|faq] - - -Nette DI --------- -- [Nette DI Konténer |nette-container] -- [Konfiguráció |configuration] -- [Szolgáltatások definiálása |services] -- [Autowiring |autowiring] -- [Generált factory-k |factory] -- [Bővítmények készítése Nette DI-hez|extensions] diff --git a/dependency-injection/hu/@meta.texy b/dependency-injection/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/dependency-injection/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/dependency-injection/hu/autowiring.texy b/dependency-injection/hu/autowiring.texy deleted file mode 100644 index fff308f812..0000000000 --- a/dependency-injection/hu/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Az Autowiring egy nagyszerű funkció, amely automatikusan átadja a szükséges szolgáltatásokat a konstruktornak és más metódusoknak, így egyáltalán nem kell őket megírnunk. Rengeteg időt takarít meg Önnek. - -Ennek köszönhetően a szolgáltatásdefiníciók írásakor a legtöbb argumentumot elhagyhatjuk. Helyette: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Elég ennyit írni: - -```neon -services: - articles: Model\ArticleRepository -``` - -Az autowiring típusok alapján működik, tehát ahhoz, hogy működjön, az `ArticleRepository` osztályt valahogy így kell definiálni: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Az autowiring használatához minden típushoz **pontosan egy szolgáltatásnak** kell lennie a konténerben. Ha több lenne belőlük, az autowiring nem tudná, melyiket adja át, és kivételt dobna: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # KIVÉTELT DOB, a mainDb és a tempDb is megfelel -``` - -A megoldás az lenne, ha vagy megkerülnénk az autowiringot, és explicit módon megadnánk a szolgáltatás nevét (azaz `articles: Model\ArticleRepository(@mainDb)`). De ügyesebb az egyik szolgáltatás autowiringját [kikapcsolni |#Autowiring kikapcsolása], vagy az első szolgáltatást [előnyben részesíteni |#Autowiring preferencia]. - - -Autowiring kikapcsolása ------------------------ - -Egy szolgáltatás autowiringját kikapcsolhatjuk az `autowired: no` opcióval: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # a tempDb szolgáltatás ki van zárva az autowiringból - - articles: Model\ArticleRepository # tehát a konstruktorba a mainDb-t adja át -``` - -Az `articles` szolgáltatás nem dob kivételt, hogy két megfelelő `PDO` típusú szolgáltatás létezik (azaz `mainDb` és `tempDb`), amelyeket át lehet adni a konstruktorba, mert csak a `mainDb` szolgáltatást látja. - -.[note] -Az autowiring konfigurációja a Nette-ben másképp működik, mint a Symfony-ban, ahol az `autowire: false` opció azt mondja, hogy ne használja az autowiringot az adott szolgáltatás konstruktorának argumentumaihoz. A Nette-ben az autowiring mindig használatos, akár a konstruktor argumentumaihoz, akár bármely más metódushoz. Az `autowired: false` opció azt mondja, hogy az adott szolgáltatás példányát ne adják át sehova autowiring segítségével. - - -Autowiring preferencia ----------------------- - -Ha több azonos típusú szolgáltatásunk van, és az egyiknél megadjuk az `autowired` opciót, ez a szolgáltatás preferálttá válik: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # preferálttá válik - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Az `articles` szolgáltatás nem dob kivételt, hogy két megfelelő `PDO` típusú szolgáltatás létezik (azaz `mainDb` és `tempDb`), hanem a preferált szolgáltatást használja, tehát a `mainDb`-t. - - -Szolgáltatások tömbje ---------------------- - -Az autowiring képes átadni egy adott típusú szolgáltatások tömbjét is. Mivel PHP-ban natívan nem lehet megadni a tömb elemeinek típusát, a `array` típus mellett egy phpDoc kommentet is hozzá kell adni az elem típusával `ClassName[]` formában: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -A DI konténer ezután automatikusan átadja az adott típusnak megfelelő szolgáltatások tömbjét. Kihagyja azokat a szolgáltatásokat, amelyeknek ki van kapcsolva az autowiringja. - -A kommentben szereplő típus lehet `array<int, Class>` vagy `list<Class>` formájú is. Ha nem tudja befolyásolni a phpDoc komment formáját, átadhatja a szolgáltatások tömbjét közvetlenül a konfigurációban a [`typed()` |services#Speciális függvények] segítségével. - - -Skalár argumentumok -------------------- - -Az autowiring csak objektumokat és objektumok tömbjeit tudja beilleszteni. A skalár argumentumokat (pl. stringek, számok, logikai értékek) [a konfigurációban írjuk le |services#Argumentumok]. Alternatíva egy [settings-objektum |best-practices:passing-settings-to-presenters] létrehozása, amely a skalár értéket (vagy több értéket) objektum formájába csomagolja, és ezt aztán újra át lehet adni autowiring segítségével. - -```php -class MySettings -{ - public function __construct( - // a readonly PHP 8.1-től használható - public readonly bool $value, - ) - {} -} -``` - -Szolgáltatást hozhat létre belőle a konfigurációhoz való hozzáadással: - -```neon -services: - - MySettings('any value') -``` - -Ezután minden osztály autowiring segítségével kérheti azt. - - -Autowiring szűkítése --------------------- - -Az egyes szolgáltatások autowiringját le lehet szűkíteni csak bizonyos osztályokra vagy interfészekre. - -Normális esetben az autowiring átadja a szolgáltatást minden olyan metódusparaméternek, amelynek típusa megfelel a szolgáltatásnak. A szűkítés azt jelenti, hogy feltételeket szabunk, amelyeknek a metódusparamétereknél megadott típusoknak meg kell felelniük ahhoz, hogy a szolgáltatást átadják nekik. - -Nézzünk egy példát: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Ha mindegyiket szolgáltatásként regisztrálnánk, az autowiring meghiúsulna: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # KIVÉTELT DOB, a parent és a child szolgáltatás is megfelel - childDep: ChildDependent # az autowiring a child szolgáltatást adja át a konstruktorba -``` - -A `parentDep` szolgáltatás `Multiple services of type ParentClass found: parent, child` kivételt dob, mert a konstruktorába mind a `parent`, mind a `child` szolgáltatás illeszkedik, és az autowiring nem tudja eldönteni, melyiket válassza. - -Ezért a `child` szolgáltatásnál leszűkíthetjük az autowiringját a `ChildClass` típusra: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # 'autowired: self'-et is lehet írni - - parentDep: ParentDependent # az autowiring a parent szolgáltatást adja át a konstruktorba - childDep: ChildDependent # az autowiring a child szolgáltatást adja át a konstruktorba -``` - -Most a `parentDep` szolgáltatás konstruktorába a `parent` szolgáltatás kerül átadásra, mert most ez az egyetlen megfelelő objektum. A `child` szolgáltatást az autowiring már nem adja át oda. Igen, a `child` szolgáltatás továbbra is `ParentClass` típusú, de már nem teljesül a paraméter típusára vonatkozó szűkítő feltétel, azaz nem igaz, hogy a `ParentClass` *felülírja* a `ChildClass`-t. - -A `child` szolgáltatásnál az `autowired: ChildClass`-t `autowired: self`-ként is lehetne írni, mivel a `self` az aktuális szolgáltatás osztályának helyettesítő jelölése. - -Az `autowired` kulcsban több osztályt vagy interfészt is meg lehet adni tömbként: - -```neon -autowired: [BarClass, FooInterface] -``` - -Próbáljuk meg a példát kiegészíteni egy interfésszel: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Ha a `child` szolgáltatást semmilyen módon nem korlátozzuk, akkor illeszkedni fog az összes `FooDependent`, `BarDependent`, `ParentDependent` és `ChildDependent` osztály konstruktorába, és az autowiring oda fogja átadni. - -Ha azonban az autowiringját leszűkítjük a `ChildClass`-ra az `autowired: ChildClass` (vagy `self`) segítségével, az autowiring csak a `ChildDependent` konstruktorába adja át, mert az `ChildClass` típusú argumentumot igényel, és igaz, hogy a `ChildClass` *típusa* `ChildClass`. A többi paraméternél megadott további típusok egyike sem felülírja a `ChildClass`-t, így a szolgáltatás nem kerül átadásra. - -Ha a `ParentClass`-ra korlátozzuk az `autowired: ParentClass` segítségével, az autowiring ismét átadja a `ChildDependent` konstruktorába (mert a szükséges `ChildClass` felülírja a `ParentClass`-t), és újonnan a `ParentDependent` konstruktorába is, mert a szükséges `ParentClass` típus szintén megfelelő. - -Ha a `FooInterface`-re korlátozzuk, akkor továbbra is autowire-olva lesz a `ParentDependent`-be (a szükséges `ParentClass` felülírja a `FooInterface`-t) és a `ChildDependent`-be, de ráadásul a `FooDependent` konstruktorába is, viszont nem a `BarDependent`-be, mert a `BarInterface` nem felülírja a `FooInterface`-t. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # az autowiring a child-ot adja át a konstruktorba - barDep: BarDependent # KIVÉTELT DOB, egyetlen szolgáltatás sem felel meg - parentDep: ParentDependent # az autowiring a child-ot adja át a konstruktorba - childDep: ChildDependent # az autowiring a child-ot adja át a konstruktorba -``` diff --git a/dependency-injection/hu/configuration.texy b/dependency-injection/hu/configuration.texy deleted file mode 100644 index a4f889fa85..0000000000 --- a/dependency-injection/hu/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -DI konténer konfigurációja -************************** - -.[perex] -A Nette DI konténer konfigurációs opcióinak áttekintése. - - -Konfigurációs fájl -================== - -A Nette DI konténer könnyen vezérelhető konfigurációs fájlok segítségével. Ezek általában [NEON formátumban|neon:format] íródnak. A szerkesztéshez [támogatással rendelkező szerkesztőket |best-practices:editors-and-tools#IDE szerkesztő] ajánlunk ehhez a formátumhoz. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Dekorátor .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI konténer .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Kiterjesztések]: "További DI kiterjesztések telepítése .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Fájlok beillesztése]: "Fájlok beillesztése .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Paraméterek]: "Paraméterek .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Szolgáltatások automatikus regisztrálása .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Szolgáltatások .[prism-token prism-comment]" -</pre> - -.[note] -Ha `%` karaktert tartalmazó stringet szeretne írni, duplázással kell escapelni `%%`-ra. - - -Paraméterek -=========== - -A konfigurációban definiálhat paramétereket, amelyeket aztán a szolgáltatásdefiníciók részeként használhat. Ezzel áttekinthetőbbé teheti a konfigurációt, vagy egységesítheti és kiemelheti azokat az értékeket, amelyek változni fognak. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -A `dsn` paraméterre bárhol a konfigurációban `%dsn%` írással hivatkozhatunk. A paramétereket stringeken belül is használhatjuk, mint például `'%wwwDir%/images'`. - -A paraméterek nem csak stringek vagy számok lehetnek, tartalmazhatnak tömböket is: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Egy konkrét kulcsra `%mailer.user%`-ként hivatkozhatunk. - -Ha a kódjában, például egy osztályban, meg kell tudnia bármely paraméter értékét, adja át azt ennek az osztálynak. Például a konstruktorban. Nincs globális objektum, amely a konfigurációt képviselné, és amelytől az osztályok lekérdeznék a paraméterértékeket. Ez megsértené a dependency injection elvét. - - -Szolgáltatások -============== - -Lásd a [külön fejezetben|services]. - - -Decorator -========= - -Hogyan lehet tömegesen módosítani egy adott típusú összes szolgáltatást? Például meghívni egy bizonyos metódust minden olyan presenter esetén, amely egy konkrét közös őstől öröklődik? Erre való a decorator. - -```neon -decorator: - # minden olyan szolgáltatásnál, amely ennek az osztálynak vagy interfésznek a példánya - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # hívd meg ezt a metódust - - $absoluteUrls = true # és állítsd be a változót -``` - -A decorator használható [tagekkel |services#Tagek] beállítására vagy az [inject |services#Inject mód] mód bekapcsolására is. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -A DI konténer technikai beállításai. - -```neon -di: - # megjeleníteni a DIC-t a Tracy Bar-ban? - debugger: ... # (bool) alapértelmezett true - - # soha nem autowire-olandó paramétertípusok - excluded: ... # (string[]) - - # engedélyezni a szolgáltatások lazy létrehozását? - lazy: ... # (bool) alapértelmezett false - - # osztály, amelytől a DI konténer öröklődik - parentClass: ... # (string) alapértelmezett Nette\DI\Container -``` - - -Lazy szolgáltatások .{data-version:3.2.4} ------------------------------------------ - -A `lazy: true` beállítás aktiválja a szolgáltatások lazy (késleltetett) létrehozását. Ez azt jelenti, hogy a szolgáltatások nem jönnek létre ténylegesen abban a pillanatban, amikor lekérjük őket a DI konténerből, hanem csak az első használatuk pillanatában. Ez gyorsíthatja az alkalmazás indítását és csökkentheti a memóriaterhelést, mivel csak azok a szolgáltatások jönnek létre, amelyekre az adott kérésben valóban szükség van. - -Egy konkrét szolgáltatásnál a lazy létrehozást [módosítani |services#Lazy szolgáltatások] lehet. - -.[note] -A lazy objektumok csak felhasználói osztályokhoz használhatók, nem belső PHP osztályokhoz. PHP 8.4 vagy újabb verziót igényel. - - -Metaadatok exportálása ----------------------- - -A DI konténer osztálya sok metaadatot is tartalmaz. Csökkentheti a méretét azáltal, hogy redukálja a metaadatok exportálását. - -```neon -di: - export: - # exportálni a paramétereket? - parameters: false # (bool) alapértelmezett true - - # exportálni a tageket és melyeket? - tags: # (string[]|bool) alapértelmezés szerint mindet - - event.subscriber - - # exportálni az autowiring adatokat és melyeket? - types: # (string[]|bool) alapértelmezés szerint mindet - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Ha nem használja a `$container->getParameters()` tömböt, kikapcsolhatja a paraméterek exportálását. Továbbá exportálhatja csak azokat a tageket, amelyeken keresztül szolgáltatásokat szerez a `$container->findByTag(...)` metódussal. Ha egyáltalán nem hívja meg a metódust, teljesen kikapcsolhatja a tagek exportálását `false`-szal. - -Jelentősen redukálhatja az [autowiring |autowiring] metaadatait azáltal, hogy megadja azokat az osztályokat, amelyeket a `$container->getByType()` metódus paramétereként használ. És ismét, ha egyáltalán nem hívja meg a metódust (illetve csak a [bootstrapban|application:bootstrapping] a `Nette\Application\Application` megszerzéséhez), teljesen kikapcsolhatja az exportálást `false`-szal. - - -Kiterjesztések -============== - -További DI kiterjesztések regisztrálása. Ezzel a módszerrel hozzáadjuk például a `Dibi\Bridges\Nette\DibiExtension22` DI kiterjesztést `dibi` néven. - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Ezután a `dibi` szekcióban konfiguráljuk: - -```neon -dibi: - host: localhost -``` - -Kiterjesztésként hozzá lehet adni egy osztályt is, amelynek paraméterei vannak: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Fájlok beillesztése -=================== - -További konfigurációs fájlokat illeszthetünk be az `includes` szekcióban: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -A `parameters.php` név nem elírás, a konfiguráció PHP fájlban is leírható, amely tömbként adja vissza: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Ha a konfigurációs fájlokban azonos kulcsokkal rendelkező elemek jelennek meg, felülíródnak, vagy [tömbök esetén egyesítve |#Összefésülés] lesznek. A később beillesztett fájl magasabb prioritású, mint az előző. Az a fájl, amelyben az `includes` szekció szerepel, magasabb prioritású, mint a benne beillesztett fájlok. - - -Search -====== - -A szolgáltatások automatikus hozzáadása a DI konténerhez rendkívül megkönnyíti a munkát. A Nette automatikusan hozzáadja a presentereket a konténerhez, de könnyen hozzáadhat bármilyen más osztályt is. - -Csak meg kell adni, mely könyvtárakban (és alkönyvtárakban) keresse az osztályokat: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Általában azonban nem akarjuk hozzáadni az összes osztályt és interfészt, ezért szűrhetjük őket: - -```neon -search: - - in: %appDir%/Forms - - # szűrés fájlnév alapján (string|string[]) - files: - - *Factory.php - - # szűrés osztálynév alapján (string|string[]) - classes: - - *Factory -``` - -Vagy kiválaszthatunk olyan osztályokat, amelyek legalább egyet örökölnek vagy implementálnak a megadott osztályok közül: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Definiálhatunk kizáró szabályokat is, azaz osztálynév maszkokat vagy örökölt ősöket, amelyek ha megfelelnek, a szolgáltatás nem kerül hozzáadásra a DI konténerhez: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Minden szolgáltatáshoz be lehet állítani tageket: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Összefésülés -============ - -Ha több konfigurációs fájlban azonos kulcsokkal rendelkező elemek jelennek meg, felülíródnak, vagy tömbök esetén összefésülődnek. A később beillesztett fájl magasabb prioritású, mint az előző. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>eredmény</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Tömbök esetén megakadályozható az összefésülés egy felkiáltójel hozzáadásával a kulcs neve után: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>eredmény</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Dependency Injection Konfiguráció}} diff --git a/dependency-injection/hu/container.texy b/dependency-injection/hu/container.texy deleted file mode 100644 index d20eb30da4..0000000000 --- a/dependency-injection/hu/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Mi az a DI konténer? -******************** - -.[perex] -A Dependency injection konténer (DIC) egy olyan osztály, amely képes objektumokat példányosítani és konfigurálni. - -Talán meglepő, de sok esetben nincs szüksége dependency injection konténerre ahhoz, hogy kihasználja a dependency injection (röviden DI) előnyeit. Hiszen már a [bevezető fejezetben|introduction] is konkrét példákon keresztül mutattuk be a DI-t, és nem volt szükség semmilyen konténerre. - -Ha azonban nagyszámú, sok függőséggel rendelkező különböző objektumot kell kezelnie, a dependency injection konténer valóban hasznos lesz. Ez például a keretrendszerre épülő webalkalmazások esetében igaz. - -Az előző fejezetben bemutattuk az `Article` és `UserController` osztályokat. Mindkettőnek vannak bizonyos függőségei, nevezetesen az adatbázis és az `ArticleFactory` factory. És ezekhez az osztályokhoz most létrehozunk egy konténert. Természetesen egy ilyen egyszerű példához nincs értelme konténert használni. De létrehozzuk, hogy megmutassuk, hogyan néz ki és működik. - -Itt van egy egyszerű hardcoded konténer a megadott példához: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -A használat így nézne ki: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Csak megkérdezzük a konténert az objektumról, és már nem kell tudnunk semmit arról, hogyan kell létrehozni, és milyen függőségei vannak; mindezt a konténer tudja. A függőségeket a konténer automatikusan injektálja. Ebben rejlik az ereje. - -A konténernek eddig minden adata fixen be van írva. Tegyünk tehát egy újabb lépést, és adjunk hozzá paramétereket, hogy a konténer valóban hasznos legyen: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Az éles szemű olvasók talán észrevettek egy problémát. Minden alkalommal, amikor lekérünk egy `UserController` objektumot, új `ArticleFactory` példány és adatbázis is létrejön. Ezt biztosan nem akarjuk. - -Ezért hozzáadunk egy `getService()` metódust, amely mindig ugyanazokat a példányokat adja vissza: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // a getService('Database') a createDatabase()-t fogja hívni - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Az első híváskor, pl. `$container->getService('Database')`, a `createDatabase()` metódussal létrehozza az adatbázis objektumot, amelyet a `$services` tömbbe ment, és a következő híváskor egyenesen visszaadja. - -Módosítjuk a konténer többi részét is, hogy a `getService()`-t használja: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Mellesleg, a szolgáltatás kifejezés bármely, a konténer által kezelt objektumot jelöl. Ezért is a metódus neve `getService()`. - -Kész. Van egy teljesen működőképes DI konténerünk! És használhatjuk: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Ahogy láthatja, egy DIC megírása nem bonyolult dolog. Érdemes megjegyezni, hogy maguk az objektumok nem tudják, hogy valamilyen konténer hozza őket létre. Így bármilyen PHP objektumot létre lehet hozni anélkül, hogy a forráskódjába bele kellene nyúlni. - -A konténer osztály manuális létrehozása és karbantartása meglehetősen gyorsan rémálommá válhat. Ezért a következő fejezetben a [Nette DI Container-ről|nette-container] beszélünk, amely szinte önmagát tudja generálni és frissíteni. - - -{{maintitle: Mi az a dependency injection konténer?}} diff --git a/dependency-injection/hu/extensions.texy b/dependency-injection/hu/extensions.texy deleted file mode 100644 index b29a120db5..0000000000 --- a/dependency-injection/hu/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Kiterjesztések készítése a Nette DI-hez -*************************************** - -.[perex] -A DI konténer generálását a konfigurációs fájlokon kívül az úgynevezett *kiterjesztések* is befolyásolják. Ezeket a konfigurációs fájl `extensions` szekciójában aktiváljuk. - -Így adjuk hozzá a `BlogExtension` osztály által reprezentált kiterjesztést `blog` néven: - -```neon -extensions: - blog: BlogExtension -``` - -Minden compiler kiterjesztés a [api:Nette\DI\CompilerExtension]-ből öröklődik, és implementálhatja a következő metódusokat, amelyeket a DI konténer összeállítása során sorban hívnak meg: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Ez a metódus hívódik meg először. Definiálja a sémát a konfigurációs paraméterek validálásához. - -A kiterjesztést abban a szekcióban konfiguráljuk, amelynek neve megegyezik azzal, amely alatt a kiterjesztést hozzáadták, tehát `blog`: - -```neon -# ugyanaz a név, mint a kiterjesztésé -blog: - postsPerPage: 10 - allowComments: false -``` - -Létrehozunk egy sémát, amely leírja az összes konfigurációs opciót, beleértve azok típusait, megengedett értékeit és esetleg alapértelmezett értékeit is: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -A dokumentációt a [Schema |schema:] oldalon találja. Ezenkívül meg lehet határozni, mely opciók lehetnek [dinamikusak |application:bootstrapping#Dinamikus paraméterek] a `dynamic()` segítségével, pl. `Expect::int()->dynamic()`. - -A konfigurációhoz a `$this->config` változón keresztül férünk hozzá, amely egy `stdClass` objektum: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Szolgáltatások hozzáadására szolgál a konténerhez. Erre a [api:Nette\DI\ContainerBuilder] szolgál: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // vagy setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -A konvenció az, hogy a kiterjesztés által hozzáadott szolgáltatásokat annak nevével prefixeljük, hogy ne keletkezzenek névütközések. Ezt a `prefix()` metódus teszi, tehát ha a kiterjesztés neve `blog`, a szolgáltatás neve `blog.articles` lesz. - -Ha át kell neveznünk egy szolgáltatást, a visszamenőleges kompatibilitás megőrzése érdekében létrehozhatunk egy aliast az eredeti névvel. Hasonlóan teszi ezt a Nette például a `routing.router` szolgáltatásnál, amely a korábbi `router` néven is elérhető. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Szolgáltatások betöltése fájlból --------------------------------- - -A szolgáltatásokat nem csak a ContainerBuilder osztály API-ján keresztül hozhatjuk létre, hanem a konfigurációs fájlban a services szekcióban használt ismert írásmóddal is. Az `@extension` prefix az aktuális kiterjesztést jelenti. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -A szolgáltatásokat betöltjük: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // a kiterjesztés konfigurációs fájljának betöltése - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -A metódus akkor hívódik meg, amikor a konténer tartalmazza az összes, az egyes kiterjesztések által a `loadConfiguration` metódusokban hozzáadott szolgáltatást, valamint a felhasználói konfigurációs fájlokból származókat is. Az összeállítás ezen szakaszában tehát módosíthatjuk a szolgáltatásdefiníciókat, vagy kiegészíthetjük a köztük lévő kapcsolatokat. A szolgáltatások konténerben való kereséséhez tagek alapján a `findByTag()` metódust, osztály vagy interfész alapján pedig a `findByType()` metódust használhatjuk. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -Ebben a fázisban a konténer osztálya már [ClassType |php-generator:#Osztályok] objektum formájában van generálva, tartalmazza az összes metódust, amely szolgáltatásokat hoz létre, és készen áll a cache-be írásra. Az eredményül kapott osztálykódot ebben a pillanatban még módosíthatjuk. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -A Configurator osztály a [konténer létrehozása |application:bootstrapping#index.php] után meghívja az inicializációs kódot, amely a `$this->initialization` objektumba való írással jön létre a [addBody() metódusával |php-generator:#Metódus és függvény törzsek] segítségével. - -Mutatunk egy példát, hogyan indíthatjuk el például a sessiont inicializációs kóddal, vagy futtathatunk olyan szolgáltatásokat, amelyeknek `run` tagjük van: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // session automatikus indítása - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // a run taggel rendelkező szolgáltatásokat a konténer példányosítása után kell létrehozni - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/hu/factory.texy b/dependency-injection/hu/factory.texy deleted file mode 100644 index 07f077420e..0000000000 --- a/dependency-injection/hu/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Generált factory-k -****************** - -.[perex] -A Nette DI képes automatikusan generálni factory kódot interfészek alapján, ami megkíméli Önt a kódírástól. - -A factory egy olyan osztály, amely objektumokat gyárt és konfigurál. Tehát átadja nekik a függőségeiket is. Kérjük, ne keverje össze a *factory method* tervezési mintával, amely a factory-k specifikus felhasználási módját írja le, és nem kapcsolódik ehhez a témához. - -Hogy néz ki egy ilyen factory, azt a [bevezető fejezetben |introduction#Factory] mutattuk be: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -A Nette DI képes automatikusan generálni a factory kódot. Mindössze annyit kell tennie, hogy létrehoz egy interfészt, és a Nette DI legenerálja az implementációt. Az interfésznek pontosan egy `create` nevű metódussal kell rendelkeznie, és deklarálnia kell a visszatérési típust: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Tehát az `ArticleFactory` factorynak van egy `create` metódusa, amely `Article` objektumokat hoz létre. Az `Article` osztály például így nézhet ki: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -A factoryt hozzáadjuk a konfigurációs fájlhoz: - -```neon -services: - - ArticleFactory -``` - -A Nette DI legenerálja a factory megfelelő implementációját. - -A kódban, amely a factoryt használja, így kérünk egy objektumot az interfész alapján, és a Nette DI a generált implementációt használja: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // hagyjuk, hogy a factory létrehozza az objektumot - $article = $this->articleFactory->create(); - } -} -``` - - -Paraméterezett factory -====================== - -A `create` factory metódus elfogadhat paramétereket, amelyeket aztán átad a konstruktornak. Egészítsük ki például az `Article` osztályt a cikk szerzőjének ID-jával: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -A paramétert hozzáadjuk a factoryhoz is: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Annak köszönhetően, hogy a konstruktorban és a factoryban lévő paraméter neve ugyanaz, a Nette DI teljesen automatikusan átadja őket. - - -Haladó definíció -================ - -A definíciót többsoros formában is le lehet írni az `implement` kulcs használatával: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Ezzel a hosszabb írásmóddal további argumentumokat lehet megadni a konstruktorhoz az `arguments` kulcsban, és kiegészítő konfigurációt a `setup` segítségével, ugyanúgy, mint a [normál szolgáltatásoknál|services]. - -Példa: ha a `create()` metódus nem fogadná el a `$authorId` paramétert, megadhatnánk egy fix értéket a konfigurációban, amelyet átadnánk az `Article` konstruktorának: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Vagy fordítva, ha a `create()` elfogadná a `$authorId` paramétert, de az nem lenne része a konstruktornak, és a `Article::setAuthorId()` metódussal adnánk át, akkor a `setup` szekcióban hivatkoznánk rá: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -A Nette a factory-k mellett ún. accessorokat is tud generálni. Ezek olyan objektumok, amelyeknek van egy `get()` metódusa, amely egy bizonyos szolgáltatást ad vissza a DI konténerből. A `get()` ismételt hívása mindig ugyanazt a példányt adja vissza. - -Az accessorok lazy-loadingot biztosítanak a függőségeknek. Tegyük fel, hogy van egy osztályunk, amely hibákat ír egy speciális adatbázisba. Ha ez az osztály konstruktorfüggőségként kapná meg az adatbázis-kapcsolatot, a kapcsolatot mindig létre kellene hozni, bár a gyakorlatban hiba csak kivételesen fordul elő, és így a kapcsolat legtöbbször kihasználatlan maradna. Ehelyett az osztály átad egy accessort, és csak akkor jön létre az adatbázis objektum, amikor annak `get()` metódusát meghívják: - -Hogyan hozzunk létre accessort? Csak írjunk egy interfészt, és a Nette DI legenerálja az implementációt. Az interfésznek pontosan egy `get` nevű metódussal kell rendelkeznie, és deklarálnia kell a visszatérési típust: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Az accessort hozzáadjuk a konfigurációs fájlhoz, ahol a szolgáltatás definíciója is található, amelyet vissza fog adni: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Mivel az accessor `PDO` típusú szolgáltatást ad vissza, és a konfigurációban csak egy ilyen szolgáltatás van, pontosan azt fogja visszaadni. Ha több ilyen típusú szolgáltatás lenne, a visszaadott szolgáltatást név szerint határoznánk meg, pl. `- PDOAccessor(@db1)`. - - -Többszörös factory/accessor -=========================== -Eddig a factory-ink és accessoraink mindig csak egy objektumot tudtak gyártani vagy visszaadni. De nagyon könnyen létrehozhatunk többszörös factory-kat is accessorokkal kombinálva. Egy ilyen osztály interfésze tetszőleges számú `create<name>()` és `get<name>()` nevű metódust tartalmazhat, pl.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Tehát ahelyett, hogy több generált factoryt és accessort adnánk át, egy komplexebb factoryt adunk át, amely többet tud. - -Alternatívaként több metódus helyett használhatjuk a `get()`-et paraméterrel: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Ekkor igaz, hogy a `MultiFactory::getArticle()` ugyanazt csinálja, mint a `MultiFactoryAlt::get('article')`. Az alternatív írásmódnak azonban az a hátránya, hogy nem egyértelmű, milyen `$name` értékek támogatottak, és logikailag nem lehet megkülönböztetni a különböző visszatérési értékeket a különböző `$name`-ekhez az interfészben. - - -Definíció listával ------------------- -Ezzel a módszerrel definiálhatunk többszörös factoryt a konfigurációban: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # definiálja a createArticle()-t - db: PDO(%dsn%, %user%, %password%) # definiálja a getDb()-t - ) -``` - -Vagy a factory definíciójában hivatkozhatunk létező szolgáltatásokra referenciával: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definiálja a createArticle()-t - db: @\PDO # definiálja a getDb()-t - ) -``` - - -Definíció tagekkel ------------------- - -A második lehetőség a [tageket |services#Tagek] használni a definícióhoz: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/hu/faq.texy b/dependency-injection/hu/faq.texy deleted file mode 100644 index 3ac34906d6..0000000000 --- a/dependency-injection/hu/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Gyakran Ismételt Kérdések a DI-ről (GYIK) -***************************************** - - -A DI egy másik név az IoC-re? ------------------------------ - -Az *Inversion of Control* (IoC) egy elv, amely arra összpontosít, hogyan fut a kód - hogy a kódja futtat-e egy idegen kódot, vagy a kódja integrálva van egy idegen kódba, amely aztán meghívja. Az IoC egy tág fogalom, amely magában foglalja az [eseményeket |nette:glossary#Eventek események], az úgynevezett [Hollywood-elvet |application:components#Hollywood style] és más szempontokat is. Ennek a koncepciónak a része a factory is, amelyről a [3. szabály: hagyd a factory-ra |introduction#3. szabály: Hagyd a factory-ra] szól, és amely az `new` operátor inverzióját jelenti. - -A *Dependency Injection* (DI) arra összpontosít, hogyan tud meg egy objektum egy másik objektumról, azaz annak függőségeiről. Ez egy tervezési minta, amely megköveteli a függőségek explicit átadását az objektumok között. - -Tehát mondhatjuk, hogy a DI az IoC egy specifikus formája. Azonban nem minden IoC forma megfelelő a kód tisztasága szempontjából. Például az antipattern-ek közé tartoznak azok a technikák, amelyek [globális állapottal |global-state] dolgoznak, vagy az úgynevezett [Service Locator |#Mi az a Service Locator]. - - -Mi az a Service Locator? ------------------------- - -Ez egy alternatíva a Dependency Injection-re. Úgy működik, hogy létrehoz egy központi tárolót, ahol minden elérhető szolgáltatás vagy függőség regisztrálva van. Amikor egy objektumnak szüksége van egy függőségre, a Service Locatortól kéri azt. - -A Dependency Injection-nel szemben azonban elveszíti az átláthatóságot: a függőségek nem közvetlenül kerülnek átadásra az objektumoknak, és így nem könnyen azonosíthatók, ami megköveteli a kód átvizsgálását, hogy minden kapcsolatot feltárjunk és megértsünk. A tesztelés is bonyolultabb, mert nem tudunk egyszerűen mock objektumokat átadni a tesztelt objektumoknak, hanem a Service Locatoron keresztül kell ezt megtennünk. Ráadásul a Service Locator megzavarja a kód tervezését, mivel az egyes objektumoknak tudniuk kell a létezéséről, ami eltér a Dependency Injection-től, ahol az objektumoknak nincs tudomásuk a DI konténerről. - - -Mikor jobb nem használni a DI-t? --------------------------------- - -Nincsenek ismert nehézségek a Dependency Injection tervezési minta használatával kapcsolatban. Ellenkezőleg, a függőségek globálisan elérhető helyekről való beszerzése [számos komplikációhoz |global-state] vezet, ahogy a Service Locator használata is. Ezért célszerű mindig DI-t használni. Ez nem dogmatikus megközelítés, egyszerűen nem találtak jobb alternatívát. - -Ennek ellenére léteznek bizonyos helyzetek, amikor nem adunk át objektumokat, és a globális térből szerezzük be őket. Például a kód debuggolásakor, amikor egy adott ponton ki kell íratni egy változó értékét, meg kell mérni egy programrész futási idejét, vagy naplózni kell egy üzenetet. Ilyen esetekben, amikor ideiglenes műveletekről van szó, amelyeket később eltávolítanak a kódból, legitim egy globálisan elérhető dumper, stopperóra vagy logger használata. Ezek az eszközök ugyanis nem tartoznak a kód tervezéséhez. - - -Vannak árnyoldalai a DI használatának? --------------------------------------- - -Jár-e a Dependency Injection használata valamilyen hátránnyal, például megnövekedett kódírási igénybevétellel vagy rosszabb teljesítménnyel? Mit veszítünk, ha elkezdünk DI-kompatibilis kódot írni? - -A DI nincs hatással az alkalmazás teljesítményére vagy memóriaigényére. A DI Container teljesítménye játszhat némi szerepet, azonban a [Nette DI |nette-container] esetében a konténer tiszta PHP-ba van fordítva, így a futásidejű overhead lényegében nulla. - -A kódírás során szükség lehet konstruktorok létrehozására, amelyek függőségeket fogadnak el. Korábban ez időigényes lehetett, de a modern IDE-knek és a [constructor property promotion |https://blog.nette.org/hu/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]-nek köszönhetően ez most másodpercek kérdése. A factory-kat könnyen lehet generálni a Nette DI és a PhpStorm plugin segítségével egy egérkattintással. Másrészt nincs szükség singletonok és statikus hozzáférési pontok írására. - -Megállapítható, hogy egy helyesen megtervezett, DI-t használó alkalmazás sem rövidebb, sem hosszabb nem lesz egy singletonokat használó alkalmazáshoz képest. A függőségekkel dolgozó kódrészek csupán ki vannak emelve az egyes osztályokból, és új helyekre kerülnek, azaz a DI konténerbe és a factory-kba. - - -Hogyan írjunk át egy legacy alkalmazást DI-re? ----------------------------------------------- - -Egy legacy alkalmazás átállítása Dependency Injection-re kihívást jelentő folyamat lehet, különösen nagy és komplex alkalmazások esetén. Fontos, hogy ezt a folyamatot szisztematikusan közelítsük meg. - -- A Dependency Injection-re való áttéréskor fontos, hogy a csapat minden tagja megértse az alkalmazott elveket és eljárásokat. -- Először végezzen elemzést a meglévő alkalmazásról, és azonosítsa a kulcsfontosságú komponenseket és azok függőségeit. Készítsen tervet arról, mely részeket kell refaktorálni és milyen sorrendben. -- Implementáljon egy DI konténert, vagy még jobb, ha egy létező könyvtárat használ, például a Nette DI-t. -- Fokozatosan refaktorálja az alkalmazás egyes részeit, hogy Dependency Injection-t használjanak. Ez magában foglalhatja a konstruktorok vagy metódusok módosítását úgy, hogy paraméterként fogadják el a függőségeket. -- Módosítsa azokat a kódrészeket, ahol függőségekkel rendelkező objektumok jönnek létre, hogy ehelyett a függőségeket a konténer injektálja. Ez magában foglalhatja a factory-k használatát. - -Ne feledje, hogy a Dependency Injection-re való áttérés befektetés a kód minőségébe és az alkalmazás hosszú távú fenntarthatóságába. Bár kihívást jelenthet ezeknek a változtatásoknak a végrehajtása, az eredmény egy tisztább, modulárisabb és könnyen tesztelhető kód kell, hogy legyen, amely készen áll a jövőbeli bővítésre és karbantartásra. - - -Miért részesítjük előnyben a kompozíciót az öröklődéssel szemben? ------------------------------------------------------------------ -Célszerűbb a [kompozíciót |nette:introduction-to-object-oriented-programming#Kompozíció] használni az [öröklődés |nette:introduction-to-object-oriented-programming#Öröklődés] helyett, mert a kód újrafelhasználására szolgál anélkül, hogy aggódnunk kellene a változtatások következményei miatt. Tehát lazább kötést biztosít, ahol nem kell attól tartanunk, hogy egy kód módosítása szükségessé teszi egy másik függő kód módosítását. Tipikus példa erre a [constructor hell |passing-dependencies#Constructor hell] néven ismert helyzet. - - -Használható a Nette DI Container a Nette-n kívül? -------------------------------------------------- - -Határozottan. A Nette DI Container a Nette része, de önálló könyvtárként lett tervezve, amely a keretrendszer többi részétől függetlenül használható. Csak telepíteni kell a Composer segítségével, létre kell hozni egy konfigurációs fájlt a szolgáltatások definíciójával, majd néhány sor PHP kóddal létre kell hozni a DI konténert. És azonnal elkezdheti kihasználni a Dependency Injection előnyeit a projektjeiben. - -A konkrét használatot, beleértve a kódokat is, a [Nette DI Container |nette-container] fejezet írja le. - - -Miért van a konfiguráció NEON fájlokban? ----------------------------------------- - -A NEON egy egyszerű és könnyen olvasható konfigurációs nyelv, amelyet a Nette keretében fejlesztettek ki alkalmazások, szolgáltatások és azok függőségeinek beállítására. A JSON-nal vagy YAML-lel összehasonlítva sokkal intuitívabb és rugalmasabb lehetőségeket kínál erre a célra. A NEON-ban természetesen leírhatók olyan kapcsolatok, amelyeket a Symfony & YAML-ben vagy egyáltalán nem lehetne leírni, vagy csak bonyolult leírással. - - -Nem lassítja le az alkalmazást a NEON fájlok feldolgozása? ----------------------------------------------------------- - -Bár a NEON fájlok nagyon gyorsan feldolgozódnak, ez a szempont egyáltalán nem számít. Az ok az, hogy a fájlok feldolgozása csak egyszer történik meg az alkalmazás első indításakor. Ezután legenerálódik a DI konténer kódja, elmentődik a lemezre, és minden további kérésnél elindul anélkül, hogy további feldolgozásra lenne szükség. - -Ez így működik a produkciós környezetben. A fejlesztés során a NEON fájlok minden alkalommal feldolgozódnak, amikor a tartalmuk megváltozik, hogy a fejlesztő mindig naprakész DI konténerrel rendelkezzen. Maga a feldolgozás, ahogy említettük, pillanatok kérdése. - - -Hogyan férek hozzá az osztályomból a konfigurációs fájl paramétereihez? ------------------------------------------------------------------------ - -Tartsuk szem előtt az [1. szabályt: kérd el, hogy átadják |introduction#1. szabály: Kérd el]. Ha egy osztálynak információra van szüksége a konfigurációs fájlból, nem kell azon gondolkodnunk, hogyan jussunk hozzá ehhez az információhoz, ehelyett egyszerűen kérjük el - például az osztály konstruktorán keresztül. Az átadást pedig a konfigurációs fájlban valósítjuk meg. - -Ebben a példában a `%myParameter%` a `myParameter` paraméter értékének helyettesítője, amelyet átadunk a `MyClass` osztály konstruktorának: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Ha több paramétert szeretne átadni, vagy autowiringot szeretne használni, célszerű [a paramétereket objektumba csomagolni |best-practices:passing-settings-to-presenters]. - - -Támogatja a Nette a PSR-11: Container interface-t? --------------------------------------------------- - -A Nette DI Container nem támogatja közvetlenül a PSR-11-et. Azonban, ha interoperabilitásra van szüksége a Nette DI Container és olyan könyvtárak vagy keretrendszerek között, amelyek PSR-11 Container Interface-t várnak, létrehozhat egy [egyszerű adaptert |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], amely hídként szolgál a Nette DI Container és a PSR-11 között. diff --git a/dependency-injection/hu/global-state.texy b/dependency-injection/hu/global-state.texy deleted file mode 100644 index 1cef481b30..0000000000 --- a/dependency-injection/hu/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Globális állapot és singletonok -******************************* - -.[perex] -Figyelmeztetés: A következő konstrukciók rosszul megtervezett kód jelei: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` vagy `static::$var` - -Előfordulnak ezek a konstrukciók a kódjában? Akkor itt a lehetőség a javításra. Talán azt gondolja, hogy ezek általános konstrukciók, amelyeket akár különböző könyvtárak és keretrendszerek példamegoldásaiban is lát. Ha ez így van, akkor a kódjuk tervezése nem jó. - -Most biztosan nem valamilyen akadémiai tisztaságról beszélünk. Minden ilyen konstrukciónak egy közös vonása van: globális állapotot használnak. És ennek romboló hatása van a kód minőségére. Az osztályok hazudnak a függőségeikről. A kód kiszámíthatatlanná válik. Megzavarja a programozókat és csökkenti hatékonyságukat. - -Ebben a fejezetben elmagyarázzuk, miért van ez így, és hogyan kerüljük el a globális állapotot. - - -Globális összekapcsolás ------------------------ - -Egy ideális világban egy objektumnak csak azokkal az objektumokkal kellene tudnia kommunikálni, amelyeket [közvetlenül átadva |passing-dependencies] kapott. Ha létrehozok két `A` és `B` objektumot, és soha nem adok át referenciát közöttük, akkor sem `A`, sem `B` nem férhet hozzá a másik objektumhoz, vagy nem változtathatja meg annak állapotát. Ez a kód egy nagyon kívánatos tulajdonsága. Hasonló ahhoz, mint amikor van egy elem és egy izzó; az izzó nem fog világítani, amíg nem köti össze az elemmel egy dróttal. - -Ez azonban nem igaz a globális (statikus) változókra vagy singletonokra. Az `A` objektum *vezeték nélkül* hozzáférhetne a `C` objektumhoz, és módosíthatná azt anélkül, hogy bármilyen referenciát átadna, azáltal, hogy meghívja a `C::changeSomething()`-t. Ha a `B` objektum is megragadja a globális `C`-t, akkor `A` és `B` kölcsönösen befolyásolhatják egymást a `C`-n keresztül. - -A globális változók használata a *vezeték nélküli* összekapcsolás új formáját vezeti be a rendszerbe, amely kívülről nem látható. Füstfüggönyt hoz létre, amely bonyolítja a kód megértését és használatát. Ahhoz, hogy a fejlesztők valóban megértsék a függőségeket, el kell olvasniuk a forráskód minden sorát. Ahelyett, hogy egyszerűen megismerkednének az osztályok interfészével. Ráadásul ez egy teljesen felesleges összekapcsolás. A globális állapotot azért használják, mert könnyen hozzáférhető bárhonnan, és lehetővé teszi például az adatbázisba írást a globális (statikus) `DB::insert()` metóduson keresztül. De ahogy megmutatjuk, az ebből származó előny elenyésző, míg a okozott komplikációk végzetesek. - -.[note] -Viselkedés szempontjából nincs különbség a globális és a statikus változó között. Ugyanolyan károsak. - - -Kísérteties távolhatás ----------------------- - -"Kísérteties távolhatás" - így nevezte el híresen 1935-ben Albert Einstein a kvantumfizika egy jelenségét, amelytől libabőrös lett. -Ez egy kvantum-összefonódás, amelynek különlegessége, hogy ha megmérjük az információt az egyik részecskéről, azonnal befolyásoljuk a másik részecskét is, még akkor is, ha millió fényév távolságra vannak egymástól. Ami látszólag megsérti az univerzum alapvető törvényét, hogy semmi sem terjedhet gyorsabban a fénynél. - -A szoftver világában "kísérteties távolhatásnak" nevezhetjük azt a helyzetet, amikor elindítunk egy folyamatot, amelyről azt gondoljuk, hogy izolált (mert nem adtunk át neki semmilyen referenciát), de a rendszer távoli pontjain váratlan interakciók és állapotváltozások következnek be, amelyekről nem volt tudomásunk. Ez csak globális állapoton keresztül történhet meg. - -Képzelje el, hogy csatlakozik egy projekt fejlesztői csapatához, amelynek kiterjedt, kiforrott kódbázisa van. Az új vezetője megkéri Önt egy új funkció implementálására, és Ön, mint jó fejlesztő, a teszt írásával kezdi. Mivel azonban új a projektben, sok feltáró tesztet végez, mint például "mi történik, ha meghívom ezt a metódust". És megpróbálja megírni a következő tesztet: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // az Ön kártyaszáma - $cc->charge(100); -} -``` - -Futtatja a kódot, talán többször is, és egy idő után észreveszi a mobilján a banki értesítéseket, hogy minden futtatáskor 100 dollárt vontak le a bankkártyájáról 🤦‍♂️ - -Hogy a fenébe okozhatta a teszt a valódi pénzlevonást? A bankkártyával való művelet nem egyszerű. Kommunikálnia kell egy harmadik fél webszolgáltatásával, ismernie kell ennek a webszolgáltatásnak az URL-jét, be kell jelentkeznie és így tovább. Ezek közül az információk közül egyik sem szerepel a tesztben. Sőt, még azt sem tudja, hol vannak ezek az információk, és így azt sem, hogyan mockolja az externális függőségeket, hogy minden futtatás ne vezessen újabb 100 dollár levonásához. És honnan kellett volna tudnia új fejlesztőként, hogy amit tenni készül, az 100 dollárral szegényebbé teszi? - -Ez a kísérteties távolhatás! - -Nem marad más hátra, mint hosszan turkálni a rengeteg forráskódban, kérdezgetni az idősebb és tapasztaltabb kollégákat, amíg meg nem érti, hogyan működnek a kapcsolatok a projektben. Ez azért van, mert a `CreditCard` osztály interfészének megtekintésekor nem lehet megállapítani a globális állapotot, amelyet inicializálni kell. Még az osztály forráskódjának megtekintése sem árulja el, melyik inicializációs metódust kell meghívnia. Legjobb esetben találhat egy globális változót, amelyhez hozzáférnek, és abból megpróbálhatja kitalálni, hogyan inicializálja. - -Az ilyen projekt osztályai patologikus hazudozók. A bankkártya úgy tesz, mintha elég lenne példányosítani és meghívni a `charge()` metódust. Titokban azonban együttműködik egy másik `PaymentGateway` osztállyal, amely a fizetési kaput képviseli. Annak interfésze is azt mondja, hogy önállóan inicializálható, de valójában kihúzza a hitelesítő adatokat valamilyen konfigurációs fájlból és így tovább. A fejlesztőknek, akik ezt a kódot írták, világos, hogy a `CreditCard`-nak szüksége van a `PaymentGateway`-re. Így írták a kódot. De bárki számára, aki új a projektben, ez teljes rejtély, és akadályozza a tanulást. - -Hogyan javítsuk a helyzetet? Könnyen. **Hagyja, hogy az API deklarálja a függőségeket.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Figyelje meg, hogyan válnak hirtelen nyilvánvalóvá a kódon belüli kapcsolatok. Azzal, hogy a `charge()` metódus deklarálja, hogy szüksége van a `PaymentGateway`-re, nem kell senkitől megkérdeznie, hogyan van összekapcsolva a kód. Tudja, hogy létre kell hoznia annak példányát, és amikor megpróbálja, rájön, hogy meg kell adnia a hozzáférési paramétereket. Nélkülük a kód el sem indulna. - -És ami a legfontosabb, most már mockolhatja a fizetési kaput, így nem vonnak le 100 dollárt minden tesztfuttatáskor. - -A globális állapot miatt az objektumai titokban hozzáférhetnek olyan dolgokhoz, amelyek nincsenek deklarálva az API-jukban, és ennek következtében az API-jai patologikus hazudozókká válnak. - -Talán korábban nem gondolt rá így, de minden alkalommal, amikor globális állapotot használ, titkos vezeték nélküli kommunikációs csatornákat hoz létre. A kísérteties távolhatás arra kényszeríti a fejlesztőket, hogy minden kódsort elolvassanak a potenciális interakciók megértéséhez, csökkenti a fejlesztők termelékenységét és megzavarja az új csapattagokat. Ha Ön hozta létre a kódot, ismeri a valódi függőségeket, de bárki, aki Ön után jön, tanácstalan. - -Ne írjon olyan kódot, amely globális állapotot használ, részesítse előnyben a függőségek átadását. Tehát a dependency injection-t. - - -Globális állapot törékenysége ------------------------------ - -A globális állapotot és singletonokat használó kódban soha nem biztos, hogy mikor és ki változtatta meg ezt az állapotot. Ez a kockázat már az inicializáláskor megjelenik. A következő kódnak adatbázis-kapcsolatot kellene létrehoznia és inicializálnia a fizetési kaput, azonban folyamatosan kivételt dob, és az ok keresése rendkívül hosszadalmas: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Részletesen át kell néznie a kódot, hogy rájöjjön, a `PaymentGateway` objektum vezeték nélkül hozzáfér más objektumokhoz, amelyek közül néhány adatbázis-kapcsolatot igényel. Tehát az adatbázist korábban kell inicializálni, mint a `PaymentGateway`-t. Azonban a globális állapot füstfüggönye ezt elrejti Ön elől. Mennyi időt takaríthatna meg, ha az egyes osztályok API-ja nem hazudna, és deklarálná a függőségeit? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Hasonló probléma merül fel az adatbázis-kapcsolat globális elérésének használatakor is: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -A `save()` metódus hívásakor nem biztos, hogy már létrejött-e az adatbázis-kapcsolat, és ki felelős annak létrehozásáért. Ha például futás közben szeretnénk megváltoztatni az adatbázis-kapcsolatot, például tesztek miatt, valószínűleg további metódusokat kellene létrehoznunk, mint például `DB::reconnect(...)` vagy `DB::reconnectForTest()`. - -Vegyünk egy példát: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Hol van a biztosíték arra, hogy a `$article->save()` hívásakor valóban a tesztadatbázist használjuk? Mi van, ha a `Foo::doSomething()` metódus megváltoztatta a globális adatbázis-kapcsolatot? Ennek kiderítéséhez meg kellene vizsgálnunk a `Foo` osztály forráskódját, és valószínűleg sok más osztályét is. Ez a megközelítés azonban csak rövid távú választ adna, mivel a helyzet a jövőben megváltozhat. - -És mi van, ha az adatbázis-kapcsolatot egy statikus változóba helyezzük az `Article` osztályon belül? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Ezzel egyáltalán semmi sem változott. A probléma a globális állapot, és teljesen mindegy, melyik osztályban rejtőzik. Ebben az esetben, akárcsak az előzőben, a `$article->save()` metódus hívásakor nincs semmilyen támpontunk arra vonatkozóan, hogy melyik adatbázisba íródik. Bárki az alkalmazás másik végén bármikor megváltoztathatta az adatbázist az `Article::setDb()` segítségével. A kezünk alatt. - -A globális állapot **rendkívül törékennyé** teszi az alkalmazásunkat. - -Van azonban egy egyszerű módja ennek a problémának a kezelésére. Csak hagyni kell, hogy az API deklarálja a függőségeket, ami biztosítja a helyes működést. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Ennek a megközelítésnek köszönhetően megszűnik az aggodalom a rejtett és váratlan adatbázis-kapcsolat változások miatt. Most már biztosak lehetünk benne, hova mentődik a cikk, és semmilyen kódmódosítás egy másik, nem kapcsolódó osztályon belül már nem változtathat a helyzeten. A kód már nem törékeny, hanem stabil. - -Ne írjon olyan kódot, amely globális állapotot használ, részesítse előnyben a függőségek átadását. Tehát a dependency injection-t. - - -Singleton ---------- - -A Singleton egy tervezési minta, amely a híres Gang of Four kiadvány "definíciója":https://en.wikipedia.org/wiki/Singleton_pattern szerint egy osztályt egyetlen példányra korlátoz, és globális hozzáférést kínál hozzá. Ennek a mintának az implementációja általában a következő kódhoz hasonlít: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // és további metódusok, amelyek az adott osztály funkcióit töltik be -} -``` - -Sajnos a singleton globális állapotot vezet be az alkalmazásba. És ahogy fentebb megmutattuk, a globális állapot nemkívánatos. Ezért a singletont antipattern-nek tekintik. - -Ne használjon singletonokat a kódjában, és helyettesítse őket más mechanizmusokkal. Valóban nincs szüksége singletonokra. Ha azonban garantálnia kell egy osztály egyetlen példányának létezését az egész alkalmazás számára, bízza azt a [DI konténerre |container]. Hozzon létre így egy alkalmazás szintű singletont, azaz egy szolgáltatást. Ezzel az osztály megszűnik foglalkozni saját egyediségének biztosításával (azaz nem lesz `getInstance()` metódusa és statikus változója), és csak a funkcióit fogja ellátni. Így megszűnik megsérteni az egyetlen felelősség elvét. - - -Globális állapot versus tesztek -------------------------------- - -Tesztek írásakor feltételezzük, hogy minden teszt egy izolált egység, és hogy semmilyen külső állapot nem lép be. És semmilyen állapot nem hagyja el a teszteket. A teszt befejezése után minden, a teszthez kapcsolódó állapotot automatikusan el kell távolítania a garbage collectornak. Ennek köszönhetően a tesztek izoláltak. Ezért futtathatjuk a teszteket tetszőleges sorrendben. - -Ha azonban globális állapotok/singletonok vannak jelen, mindezek a kellemes feltételezések összeomlanak. Az állapot beléphet a tesztbe és kiléphet belőle. Hirtelen számíthat a tesztek sorrendje. - -Ahhoz, hogy egyáltalán tesztelni tudjuk a singletonokat, a fejlesztők gyakran kénytelenek lazítani a tulajdonságaikat, például azáltal, hogy megengedik a példány cseréjét egy másikkal. Az ilyen megoldások legjobb esetben is hackek, amelyek nehezen karbantartható és érthető kódot hoznak létre. Minden tesztnek vagy `tearDown()` metódusnak, amely bármilyen globális állapotot befolyásol, vissza kell állítania ezeket a változtatásokat. - -A globális állapot a legnagyobb fejfájás az unit tesztelés során! - -Hogyan javítsuk a helyzetet? Könnyen. Ne írjon olyan kódot, amely singletonokat használ, részesítse előnyben a függőségek átadását. Tehát a dependency injection-t. - - -Globális konstansok -------------------- - -A globális állapot nem korlátozódik csak a singletonok és statikus változók használatára, hanem globális konstansokra is vonatkozhat. - -Azok a konstansok, amelyek értéke nem hoz számunkra semmilyen új (`M_PI`) vagy hasznos (`PREG_BACKTRACK_LIMIT_ERROR`) információt, egyértelműen rendben vannak. Ellenben azok a konstansok, amelyek arra szolgálnak, hogy *vezeték nélkül* információt adjanak át a kódba, nem mások, mint rejtett függőségek. Mint például a `LOG_FILE` a következő példában. A `FILE_APPEND` konstans használata teljesen korrekt. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Ebben az esetben deklarálnunk kellene egy paramétert a `Foo` osztály konstruktorában, hogy az API részévé váljon: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Most már átadhatjuk az információt a naplófájl elérési útjáról, és szükség szerint könnyen megváltoztathatjuk, ami megkönnyíti a kód tesztelését és karbantartását. - - -Globális függvények és statikus metódusok ------------------------------------------ - -Szeretnénk hangsúlyozni, hogy maguk a statikus metódusok és globális függvények használata nem problematikus. Elmagyaráztuk, miért nem megfelelő a `DB::insert()` és hasonló metódusok használata, de ez mindig csak a globális állapot kérdése volt, amely valamilyen statikus változóban van tárolva. A `DB::insert()` metódus megköveteli egy statikus változó létezését, mert abban van tárolva az adatbázis-kapcsolat. E változó nélkül lehetetlen lenne a metódust implementálni. - -Determinisztikus statikus metódusok és függvények használata, mint például a `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` és sok más, teljes mértékben összhangban van a dependency injection-nel. Ezek a függvények mindig ugyanazokat az eredményeket adják vissza ugyanazokból a bemeneti paraméterekből, és ezért előrejelezhetők. Nem használnak semmilyen globális állapotot. - -Léteznek azonban olyan függvények is PHP-ban, amelyek nem determinisztikusak. Ezek közé tartozik például a `htmlspecialchars()` függvény. Annak harmadik paramétere, a `$encoding`, ha nincs megadva, alapértelmezett értékként a `ini_get('default_charset')` konfigurációs opció értékét veszi fel. Ezért ajánlott ezt a paramétert mindig megadni, hogy elkerüljük a függvény esetleges kiszámíthatatlan viselkedését. A Nette ezt következetesen megteszi. - -Néhány függvény, mint például a `strtolower()`, `strtoupper()` és hasonlók, a közelmúltban nem determinisztikusan viselkedtek, és a `setlocale()` beállítástól függtek. Ez sok komplikációt okozott, leggyakrabban a török nyelvvel való munka során. Az ugyanis megkülönbözteti a kis- és nagybetűs `I`-t ponttal és pont nélkül is. Így a `strtolower('I')` az `ı` karaktert adta vissza, a `strtoupper('i')` pedig az `İ` karaktert, ami ahhoz vezetett, hogy az alkalmazások számos rejtélyes hibát kezdtek okozni. Ezt a problémát azonban a PHP 8.2-es verziójában orvosolták, és a függvények már nem függnek a locale-tól. - -Ez egy szép példa arra, hogyan okozott fejfájást a globális állapot több ezer fejlesztőnek világszerte. A megoldás az volt, hogy dependency injection-nel helyettesítették. - - -Mikor lehet globális állapotot használni? ------------------------------------------ - -Léteznek bizonyos specifikus helyzetek, amikor lehet globális állapotot használni. Például a kód debuggolásakor, amikor ki kell íratni egy változó értékét, vagy meg kell mérni egy programrész futási idejét. Ilyen esetekben, amelyek ideiglenes műveletekre vonatkoznak, amelyeket később eltávolítanak a kódból, legitim egy globálisan elérhető dumper vagy stopperóra használata. Ezek az eszközök ugyanis nem részei a kód tervezésének. - -Egy másik példa a reguláris kifejezésekkel dolgozó `preg_*` függvények, amelyek belsőleg statikus cache-ben tárolják a lefordított reguláris kifejezéseket a memóriában. Tehát ha ugyanazt a reguláris kifejezést többször hívja meg a kód különböző pontjain, csak egyszer fordítódik le. A cache teljesítményt takarít meg, és ugyanakkor a felhasználó számára teljesen láthatatlan, ezért az ilyen használat legitimnek tekinthető. - - -Összegzés ---------- - -Megbeszéltük, miért van értelme: - -1) Eltávolítani minden statikus változót a kódból -2) Deklarálni a függőségeket -3) És használni a dependency injection-t - -Amikor a kód tervezésén gondolkodik, gondoljon arra, hogy minden `static $foo` problémát jelent. Ahhoz, hogy a kódja DI-t tiszteletben tartó környezet legyen, elengedhetetlen a globális állapot teljes kiirtása és dependency injection-nel való helyettesítése. - -E folyamat során talán rájön, hogy egy osztályt fel kell osztani, mert több felelőssége van. Ne féljen ettől; törekedjen az egyetlen felelősség elvére. - -*Szeretnék köszönetet mondani Miško Hevery-nek, akinek cikkei, mint például a [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], képezik ennek a fejezetnek az alapját.* diff --git a/dependency-injection/hu/introduction.texy b/dependency-injection/hu/introduction.texy deleted file mode 100644 index 8cd8b4fe69..0000000000 --- a/dependency-injection/hu/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Mi az a Dependency Injection? -***************************** - -.[perex] -Ez a fejezet bemutatja azokat az alapvető programozási gyakorlatokat, amelyeket minden alkalmazás írásakor követnie kell. Ezek az alapok szükségesek a tiszta, érthető és karbantartható kód írásához. - -Ha elsajátítja és követi ezeket a szabályokat, a Nette minden lépésben segíteni fog Önnek. Kezelni fogja a rutinfeladatokat, és maximális kényelmet biztosít Önnek, hogy a tényleges logikára koncentrálhasson. - -Az itt bemutatott elvek meglehetősen egyszerűek. Nincs mitől félnie. - - -Emlékszel az első programodra? ------------------------------- - -Nem tudjuk, milyen nyelven írta, de ha PHP lett volna, valószínűleg így nézett volna ki: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // kiírja a 24-et -``` - -Néhány triviális kódsor, de annyi kulcsfontosságú koncepciót rejtenek magukban. Hogy vannak változók. Hogy a kód kisebb egységekre van osztva, mint például a függvények. Hogy bemeneti argumentumokat adunk át nekik, és eredményeket adnak vissza. Már csak a feltételek és a ciklusok hiányoznak. - -Az, hogy bemeneti adatokat adunk át egy függvénynek, és az eredményt ad vissza, egy tökéletesen érthető koncepció, amelyet más területeken is használnak, például a matematikában. - -Egy függvénynek van szignatúrája, amely a nevéből, a paraméterek és típusaik listájából, valamint végül a visszatérési érték típusából áll. Felhasználóként minket a szignatúra érdekel, a belső megvalósításról általában nem kell tudnunk semmit. - -Most képzelje el, hogy a függvény szignatúrája így néz ki: - -```php -function soucet(float $x): float -``` - -Összeadás egy paraméterrel? Ez furcsa… És mi van ezzel? - -```php -function soucet(): float -``` - -Ez már tényleg nagyon furcsa, nem? Hogyan használják a függvényt? - -```php -echo soucet(); // vajon mit ír ki? -``` - -Egy ilyen kódot látva összezavarodnánk. Nemcsak egy kezdő nem értené, de egy tapasztalt programozó sem. - -Gondolkodik azon, hogyan nézne ki egy ilyen függvény belülről? Honnan veszi az összeadandókat? Valószínűleg *valahogy* maga szerezné be őket, például így: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -A függvény törzsében rejtett függőségeket fedeztünk fel más globális függvényekre vagy statikus metódusokra. Ahhoz, hogy megtudjuk, honnan származnak valójában az összeadandók, tovább kell kutatnunk. - - -Nem erre! ---------- - -Az imént bemutatott tervezés számos negatív tulajdonság esszenciája: - -- A függvény szignatúrája úgy tett, mintha nem lenne szüksége összeadandókra, ami félrevezetett minket. -- Fogalmunk sincs, hogyan vegyük rá a függvényt, hogy két másik számot adjon össze. -- Bele kellett néznünk a kódba, hogy megtudjuk, honnan veszi az összeadandókat. -- Rejtett függőségeket fedeztünk fel. -- A teljes megértéshez ezeket a függőségeket is meg kell vizsgálni. - -És egyáltalán az összeadó függvény feladata a bemenetek beszerzése? Természetesen nem. Az ő felelőssége csak maga az összeadás. - - -Ilyen kóddal nem akarunk találkozni, és határozottan nem akarunk ilyet írni. A javítás egyszerű: térjünk vissza az alapokhoz, és egyszerűen használjunk paramétereket: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -1. szabály: Kérd el -------------------- - -A legfontosabb szabály: **minden adatot, amire egy függvénynek vagy osztálynak szüksége van, át kell adni neki**. - -Ahelyett, hogy rejtett módokat találnál ki, amelyekkel maguk is hozzáférhetnének, egyszerűen add át a paramétereket. Időt takarítasz meg a rejtett utak kitalálásával, amelyek biztosan nem javítják a kódodat. - -Ha ezt a szabályt mindig és mindenhol betartod, úton vagy a rejtett függőségek nélküli kód felé. Egy olyan kód felé, amely nemcsak a szerző számára érthető, hanem bárki számára is, aki utána olvassa. Ahol minden érthető a függvények és osztályok szignatúráiból, és nem kell rejtett titkok után kutatni a megvalósításban. - -Ezt a technikát szakmailag **dependency injection**-nek (függőséginjektálás) nevezik. És ezeket az adatokat **függőségeknek** (dependencies). Valójában ez csak egyszerű paraméterátadás, semmi több. - -.[note] -Kérjük, ne keverje össze a dependency injection-t, ami egy tervezési minta, a „dependency injection container”-rel, ami egy eszköz, tehát valami gyökeresen más. A konténerekkel később foglalkozunk. - - -Függvényektől az osztályokig ----------------------------- - -És hogyan kapcsolódik ez az osztályokhoz? Az osztály egy összetettebb egység, mint egy egyszerű függvény, de az 1. szabály itt is maradéktalanul érvényes. Csak [több lehetőség van az argumentumok átadására|passing-dependencies]. Például egészen hasonlóan, mint egy függvénynél: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Vagy más metódusokkal, vagy közvetlenül a konstruktorral: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Mindkét példa teljes mértékben összhangban van a dependency injection elvével. - - -Valós példák ------------- - -A való világban nem fogsz osztályokat írni számok összeadására. Térjünk át a gyakorlati példákra. - -Legyen egy `Article` osztályunk, amely egy blogbejegyzést reprezentál: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // elmentjük a cikket az adatbázisba - } -} -``` - -és a használat a következő lesz: - -```php -$article = new Article; -$article->title = '10 dolog, amit tudnod kell a fogyásról'; -$article->content = 'Minden évben emberek milliói ...'; -$article->save(); -``` - -A `save()` metódus elmenti a cikket egy adatbázis táblába. A [Nette Database |database:] segítségével megvalósítani gyerekjáték lenne, ha nem lenne egy bökkenő: honnan veszi az `Article` az adatbázis-kapcsolatot, azaz a `Nette\Database\Connection` osztály objektumát? - -Úgy tűnik, sok lehetőségünk van. Veheti valahonnan egy statikus változóból. Vagy örökölhet egy olyan osztálytól, amely biztosítja az adatbázis-kapcsolatot. Vagy használhatja az úgynevezett [singleton |global-state#Singleton] mintát. Vagy az úgynevezett facades-okat, amelyeket a Laravelben használnak: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Nagyszerű, megoldottuk a problémát. - -Vagy mégsem? - -Idézzük fel az [##1. szabály: Kérd el]: minden függőséget, amire az osztálynak szüksége van, át kell adni neki. Mert ha megszegjük a szabályt, a piszkos kód útjára léptünk, tele rejtett függőségekkel, érthetetlenséggel, és az eredmény egy olyan alkalmazás lesz, amelyet fájdalmas lesz karbantartani és fejleszteni. - -Az `Article` osztály felhasználója nem tudja, hova menti a `save()` metódus a cikket. Adatbázis táblába? Melyikbe, az élesbe vagy a tesztbe? És hogyan lehet ezt megváltoztatni? - -A felhasználónak meg kell néznie, hogyan van implementálva a `save()` metódus, és megtalálja a `DB::insert()` metódus használatát. Tehát tovább kell kutatnia, hogyan szerzi be ez a metódus az adatbázis-kapcsolatot. És a rejtett függőségek elég hosszú láncot alkothatnak. - -A tiszta és jól megtervezett kódban soha nincsenek rejtett függőségek, Laravel facade-ok vagy statikus változók. A tiszta és jól megtervezett kódban argumentumokat adnak át: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Még praktikusabb lesz, ahogy később látni fogjuk, a konstruktorral: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Ha tapasztalt programozó vagy, talán azt gondolod, hogy az `Article`-nek egyáltalán nem kellene `save()` metódussal rendelkeznie, tisztán adatkomponensnek kellene lennie, és a mentésről egy különálló repositorynak kellene gondoskodnia. Ennek van értelme. De ezzel messze túllépnénk a témán, ami a dependency injection, és az egyszerű példák bemutatására tett erőfeszítésen. - -Ha olyan osztályt írsz, amelynek a működéséhez például adatbázisra van szüksége, ne azon gondolkodj, honnan szerezd be, hanem kérd el. Például a konstruktor vagy egy másik metódus paramétereként. Ismerd el a függőségeket. Ismerd el őket az osztályod API-jában. Érthető és kiszámítható kódot kapsz. - -És mi van ezzel az osztállyal, amely hibaüzeneteket naplóz: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Mit gondolsz, betartottuk az [##1. szabály: Kérd el]? - -Nem tartottuk be. - -A kulcsinformációt, azaz a naplófájlt tartalmazó könyvtárat, az osztály *maga szerzi be* egy konstansból. - -Nézd meg a használati példát: - -```php -$logger = new Logger; -$logger->log('A hőmérséklet 23 °C'); -$logger->log('A hőmérséklet 10 °C'); -``` - -Az implementáció ismerete nélkül tudnál válaszolni arra a kérdésre, hogy hova íródnak az üzenetek? Eszedbe jutna, hogy a működéshez szükség van a `LOG_DIR` konstans létezésére? És tudnál létrehozni egy második példányt, amely máshova ír? Biztosan nem. - -Javítsuk ki az osztályt: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Az osztály most sokkal érthetőbb, konfigurálhatóbb és ezáltal hasznosabb. - -```php -$logger = new Logger('/útvonal/a/naplóhoz.txt'); -$logger->log('A hőmérséklet 15 °C'); -``` - - -De ez engem nem érdekel! ------------------------- - -*"Amikor létrehozok egy Article objektumot és meghívom a save()-t, nem akarok az adatbázissal foglalkozni, egyszerűen azt akarom, hogy abba mentse el, amit a konfigurációban beállítottam."* - -*"Amikor a Logger-t használom, egyszerűen azt akarom, hogy az üzenet íródjon ki, és nem akarom megoldani, hogy hova. Használja a globális beállítást."* - -Ezek helyes észrevételek. - -Példaként egy hírleveleket küldő osztályt mutatunk be, amely naplózza, hogyan sikerült: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Az e-mailek elküldve'); - - } catch (Exception $e) { - $logger->log('Hiba történt a küldés során'); - throw $e; - } - } -} -``` - -A továbbfejlesztett `Logger`, amely már nem használja a `LOG_DIR` konstansot, a konstruktorban megköveteli a fájl elérési útjának megadását. Hogyan oldjuk ezt meg? A `NewsletterDistributor` osztályt egyáltalán nem érdekli, hova íródnak az üzenetek, csak ki akarja írni őket. - -A megoldás ismét az [##1. szabály: Kérd el]: minden adatot, amire az osztálynak szüksége van, átadunk neki. - -Tehát ez azt jelenti, hogy a konstruktoron keresztül átadjuk a napló elérési útját, amelyet aztán a `Logger` objektum létrehozásakor használunk? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NEM ÍGY! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Nem így! Az elérési út ugyanis **nem tartozik** azok közé az adatok közé, amelyekre a `NewsletterDistributor` osztálynak szüksége van; azokra ugyanis a `Logger`-nek van szüksége. Érzed a különbséget? A `NewsletterDistributor` osztálynak magára a loggerre van szüksége. Tehát azt adjuk át: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Az e-mailek elküldve'); - - } catch (Exception $e) { - $this->logger->log('Hiba történt a küldés során'); - throw $e; - } - } -} -``` - -Most már a `NewsletterDistributor` osztály szignatúráiból világos, hogy a funkcionalitásának része a naplózás is. És a logger cseréjének feladata egy másikra, például tesztelés céljából, teljesen triviális. Ráadásul, ha a `Logger` osztály konstruktora megváltozna, az nem lenne hatással az osztályunkra. - - -2. szabály: Vedd el, ami a tiéd -------------------------------- - -Ne hagyd magad megtéveszteni, és ne kérd a függőségeid függőségeinek átadását. Csak a saját függőségeidet kérd el. - -Ennek köszönhetően a más objektumokat használó kód teljesen független lesz a konstruktoraik változásaitól. Az API-ja igazabb lesz. És főleg triviális lesz ezeket a függőségeket másokra cserélni. - - -Új családtag ------------- - -A fejlesztői csapat úgy döntött, hogy létrehoz egy második loggert, amely adatbázisba ír. Tehát létrehozunk egy `DatabaseLogger` osztályt. Így van két osztályunk, a `Logger` és a `DatabaseLogger`, az egyik fájlba ír, a másik adatbázisba… nem tűnik valami furcsának az elnevezés? Nem lenne jobb átnevezni a `Logger`-t `FileLogger`-re? Biztosan igen. - -De okosan csináljuk. Az eredeti név alatt létrehozunk egy interfészt: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… amelyet mindkét logger implementálni fog: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Ennek köszönhetően nem kell semmit sem változtatni a kód többi részében, ahol a loggert használják. Például a `NewsletterDistributor` osztály konstruktora továbbra is elégedett lesz azzal, hogy paraméterként `Logger`-t igényel. És csak rajtunk múlik, melyik példányt adjuk át neki. - -**Ezért soha nem adunk az interfészek nevéhez `Interface` utótagot vagy `I` előtagot.** Különben nem lehetne a kódot ilyen szépen fejleszteni. - - -Houston, van egy problémánk ---------------------------- - -Míg az egész alkalmazásban megelégedhetünk egyetlen logger példánnyal, legyen az fájl- vagy adatbázis-alapú, és egyszerűen átadjuk mindenhol, ahol valami naplózásra kerül, egészen más a helyzet az `Article` osztály esetében. Ennek példányait ugyanis szükség szerint hozzuk létre, akár többször is. Hogyan kezeljük az adatbázis-függőséget a konstruktorában? - -Példaként szolgálhat egy kontroller, amelynek egy űrlap elküldése után el kell mentenie a cikket az adatbázisba: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Egy lehetséges megoldás közvetlenül adódik: átadjuk az adatbázis objektumot a konstruktoron keresztül az `EditController`-nek, és használjuk a `$article = new Article($this->db)` kódot. - -Ahogy az előző esetben a `Logger`-rel és a fájl elérési útjával, ez sem a helyes megközelítés. Az adatbázis nem az `EditController` függősége, hanem az `Article`-é. Az adatbázis átadása tehát ellentétes a [#2. szabály: Vedd el, ami a tiéd] szabállyal. Ha az `Article` osztály konstruktora megváltozik (új paraméter kerül hozzáadásra), akkor a kódot is módosítani kell mindenhol, ahol példányt hoznak létre. Pfff. - -Houston, mit javasolsz? - - -3. szabály: Hagyd a factory-ra ------------------------------- - -Azzal, hogy megszüntettük a rejtett függőségeket, és minden függőséget argumentumként adunk át, konfigurálhatóbb és rugalmasabb osztályokat kaptunk. És ezért szükségünk van még valamire, ami létrehozza és konfigurálja nekünk ezeket a rugalmasabb osztályokat. Ezt factory-nak (gyárnak) fogjuk nevezni. - -A szabály így szól: ha egy osztálynak függőségei vannak, hagyd a példányok létrehozását a factory-ra. - -A factory-k az `new` operátor okosabb helyettesítői a dependency injection világában. - -.[note] -Kérjük, ne keverje össze a *factory method* tervezési mintával, amely a factory-k specifikus felhasználási módját írja le, és nem kapcsolódik ehhez a témához. - - -Factory -------- - -A factory egy metódus vagy osztály, amely objektumokat gyárt és konfigurál. Az `Article`-t gyártó osztályt `ArticleFactory`-nak nevezzük, és például így nézhet ki: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Használata a kontrollerben a következő lesz: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // hagyjuk, hogy a factory hozza létre az objektumot - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Ha ebben a pillanatban megváltozik az `Article` osztály konstruktorának szignatúrája, az egyetlen kódrészlet, amelynek reagálnia kell rá, maga a `ArticleFactory`. Minden más kód, amely `Article` objektumokkal dolgozik, mint például az `EditController`, ettől érintetlen marad. - -Talán most a homlokodra csapsz, hogy egyáltalán segítettünk-e magunkon. A kód mennyisége megnőtt, és az egész kezd gyanúsan bonyolultnak tűnni. - -Ne aggódj, hamarosan eljutunk a Nette DI konténerhez. És annak számos aduásza van a tarsolyában, amelyek rendkívül leegyszerűsítik a dependency injectiont használó alkalmazások építését. Például az `ArticleFactory` osztály helyett elég lesz [csak egy interfészt írni |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -De ezzel előreszaladunk, még tarts ki :-) - - -Összegzés ---------- - -Ennek a fejezetnek az elején azt ígértük, hogy bemutatunk egy módszert a tiszta kód tervezésére. Elég az osztályoknak - -1) [átadni a szükséges függőségeket |#1. szabály: Kérd el] -2) [és fordítva, nem átadni azt, amire közvetlenül nincs szükségük |#2. szabály: Vedd el ami a tiéd] -3) [és hogy a függőségekkel rendelkező objektumokat a legjobban factory-kban lehet létrehozni |#3. szabály: Hagyd a factory-ra] - -Első pillantásra talán nem tűnik úgy, de ennek a három szabálynak messzemenő következményei vannak. Radikálisan más nézőponthoz vezetnek a kódtervezésben. Megéri? Azok a programozók, akik elhagyták régi szokásaikat és következetesen elkezdték használni a dependency injectiont, ezt a lépést szakmai életük kulcsfontosságú pillanatának tartják. Megnyílt előttük az áttekinthető és karbantartható alkalmazások világa. - -De mi van, ha a kód nem használja következetesen a dependency injectiont? Mi van, ha statikus metódusokra vagy singletonokra épül? Ez okoz valamilyen problémát? [Igen, és nagyon alapvetőeket |global-state]. diff --git a/dependency-injection/hu/nette-container.texy b/dependency-injection/hu/nette-container.texy deleted file mode 100644 index ffcccc429a..0000000000 --- a/dependency-injection/hu/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -A Nette DI a Nette egyik legérdekesebb könyvtára. Képes generálni és automatikusan frissíteni a lefordított DI konténereket, amelyek rendkívül gyorsak és elképesztően könnyen konfigurálhatók. - -A DI konténer által létrehozandó szolgáltatások formáját általában konfigurációs fájlokban definiáljuk [NEON formátumban|neon:format]. A konténer, amelyet manuálisan hoztunk létre az [előző fejezetben|container], így íródna le: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -A leírás valóban tömör. - -Az `ArticleFactory` és `UserController` osztályok konstruktoraiban deklarált összes függőséget a Nette DI maga kideríti és átadja az úgynevezett [autowiring|autowiring] segítségével, ezért a konfigurációs fájlban semmit sem kell megadni. Tehát még ha a paraméterek megváltoznak is, a konfigurációban semmit sem kell módosítani. A Nette konténer automatikusan újragenerálódik. Ön így tisztán az alkalmazás fejlesztésére koncentrálhat. - -Ha a függőségeket setterek segítségével szeretnénk átadni, használjuk a [setup |services#Setup] szekciót. - -A Nette DI közvetlenül PHP kódot generál a konténerhez. Az eredmény tehát egy `.php` fájl, amelyet megnyithat és tanulmányozhat. Ennek köszönhetően pontosan láthatja, hogyan működik a konténer. Debuggolhatja is az IDE-ben és lépésenként végigkövetheti. És ami a legfontosabb: a generált PHP rendkívül gyors. - -A Nette DI képes [factory|factory] kódot is generálni egy megadott interfész alapján. Ezért az `ArticleFactory` osztály helyett elég lesz csak egy interfészt létrehozni az alkalmazásban: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -A teljes példát megtalálja [GitHubon|https://github.com/nette-examples/di-example-doc]. - - -Önálló használat ----------------- - -A Nette DI könyvtár bevezetése egy alkalmazásba nagyon egyszerű. Először telepítjük a Composerrel (mert a zip fájlok letöltése annyira elavult): - -```shell -composer require nette/di -``` - -A következő kód létrehoz egy DI konténer példányt a `config.neon` fájlban tárolt konfiguráció alapján: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -A konténer csak egyszer generálódik le, a kódja a cache-be íródik (a `__DIR__ . '/temp'` könyvtárba), és a további kéréseknél már csak innen töltődik be. - -A szolgáltatások létrehozására és lekérésére a `getService()` vagy a `getByType()` metódusok szolgálnak. Így hozunk létre egy `UserController` objektumot: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Fejlesztés közben hasznos aktiválni az auto-refresh módot, amelyben a konténer automatikusan újragenerálódik, ha bármelyik osztály vagy konfigurációs fájl megváltozik. Ehhez elég a `ContainerLoader` konstruktorában második argumentumként `true`-t megadni. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Használat a Nette keretrendszerrel ----------------------------------- - -Ahogy bemutattuk, a Nette DI használata nem korlátozódik a Nette Frameworkben írt alkalmazásokra, mindössze 3 sor kóddal bárhol bevethető. Ha azonban alkalmazásokat fejleszt a Nette Frameworkben, a konténer konfigurálását és létrehozását a [Bootstrap |application:bootstrapping#DI konténer konfigurálása] végzi. diff --git a/dependency-injection/hu/passing-dependencies.texy b/dependency-injection/hu/passing-dependencies.texy deleted file mode 100644 index 6d33c4c5f1..0000000000 --- a/dependency-injection/hu/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Függőségek átadása -****************** - -<div class=perex> - -Az argumentumokat, vagy a DI terminológiájában „függőségeket”, a következő fő módokon lehet átadni az osztályoknak: - -* konstruktoron keresztüli átadás -* metóduson (úgynevezett setteren) keresztüli átadás -* property beállításával -* *inject* metódussal, annotációval vagy attribútummal - -</div> - -Most az egyes változatokat konkrét példákon mutatjuk be. - - -Konstruktoron keresztüli átadás -=============================== - -A függőségek az objektum létrehozásának pillanatában kerülnek átadásra a konstruktor argumentumaiként: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Ez a forma alkalmas a kötelező függőségekre, amelyekre az osztálynak feltétlenül szüksége van a működéséhez, mivel nélkülük nem lehet példányt létrehozni. - -PHP 8.0 óta használhatunk rövidebb írásmódot ([constructor property promotion |https://blog.nette.org/hu/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), amely funkcionálisan ekvivalens: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -PHP 8.1 óta a property-t `readonly` jelzővel lehet ellátni, amely deklarálja, hogy a property tartalma már nem fog megváltozni: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -A DI konténer automatikusan átadja a függőségeket a konstruktornak az [autowiring |autowiring] segítségével. Azokat az argumentumokat, amelyeket így nem lehet átadni (pl. stringek, számok, booleanek), [a konfigurációban írjuk le |services#Argumentumok]. - - -Constructor hell ----------------- - -A *constructor hell* kifejezés azt a helyzetet jelöli, amikor egy leszármazott egy szülő osztálytól örököl, amelynek konstruktora függőségeket igényel, és ugyanakkor a leszármazott is függőségeket igényel. Eközben át kell vennie és át kell adnia a szülő függőségeit is: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -A probléma akkor merül fel, amikor meg akarjuk változtatni a `BaseClass` osztály konstruktorát, például ha új függőség kerül hozzáadásra. Ekkor ugyanis módosítani kell az összes leszármazott konstruktorát is. Ami egy ilyen módosítást pokollá tesz. - -Hogyan előzzük ezt meg? A megoldás az, hogy **előnyben részesítjük a [kompozíciót az öröklődéssel szemben |faq#Miért részesítjük előnyben a kompozíciót az öröklődéssel szemben]**. - -Tehát másképp tervezzük meg a kódot. Kerülni fogjuk az [absztrakt |nette:introduction-to-object-oriented-programming#Absztrakt osztályok] `Base*` osztályokat. Ahelyett, hogy a `MyClass` bizonyos funkcionalitást úgy szerezne meg, hogy a `BaseClass`-tól örököl, ezt a funkcionalitást függőségként kapja meg: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Setteren keresztüli átadás -========================== - -A függőségek egy metódus hívásával kerülnek átadásra, amely egy privát property-be menti őket. Ezeknek a metódusoknak a szokásos elnevezési konvenciója a `set*()` forma, ezért settereknek nevezik őket, de természetesen bármilyen más néven is nevezhetők. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Ez a módszer alkalmas a nem kötelező függőségekre, amelyek nem szükségesek az osztály működéséhez, mivel nincs garantálva, hogy az objektum ténylegesen megkapja a függőséget (azaz hogy a felhasználó meghívja a metódust). - -Ugyanakkor ez a módszer lehetővé teszi a setter ismételt meghívását és a függőség megváltoztatását. Ha ez nem kívánatos, adjunk hozzá egy ellenőrzést a metódushoz, vagy PHP 8.1 óta jelöljük a `$cache` property-t `readonly` jelzővel. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -A setter hívását a DI konténer konfigurációjában a [setup kulcsban |services#Setup] definiáljuk. Itt is automatikus függőségátadás történik az autowiring segítségével: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Property beállításával -====================== - -A függőségek közvetlenül a tagváltozóba (property-be) írással kerülnek átadásra: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Ez a módszer nem megfelelőnek tekinthető, mivel a property-t `public`-ként kell deklarálni. Így nincs ellenőrzésünk afölött, hogy az átadott függőség valóban a megadott típusú-e (ez a PHP 7.4 előtt volt érvényes), és elveszítjük a lehetőséget, hogy saját kóddal reagáljunk az újonnan hozzárendelt függőségre, például megakadályozzuk a későbbi módosítást. Ugyanakkor a property az osztály nyilvános interfészének részévé válik, ami nem feltétlenül kívánatos. - -A property beállítását a DI konténer konfigurációjában a [setup szekcióban |services#Setup] definiáljuk: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Míg az előző három módszer általánosan érvényes minden objektumorientált nyelvben, a metódussal, annotációval vagy *inject* attribútummal történő injektálás kizárólag a Nette presenterjeire jellemző. Ezekről egy [külön fejezet |best-practices:inject-method-attribute] szól. - - -Melyik módszert válasszuk? -========================== - -- A konstruktor alkalmas a kötelező függőségekre, amelyekre az osztálynak feltétlenül szüksége van a működéséhez. -- A setter viszont alkalmas a nem kötelező függőségekre, vagy olyan függőségekre, amelyeket lehetőség szerint tovább lehet módosítani. -- A public property-k nem megfelelőek. diff --git a/dependency-injection/hu/services.texy b/dependency-injection/hu/services.texy deleted file mode 100644 index d1891fed65..0000000000 --- a/dependency-injection/hu/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Szolgáltatások definiálása -************************** - -.[perex] -A konfiguráció az a hely, ahol megtanítjuk a DI konténernek, hogyan állítsa össze az egyes szolgáltatásokat, és hogyan kapcsolja össze őket más függőségekkel. A Nette nagyon áttekinthető és elegáns módot kínál ennek elérésére. - -A `services` szekció a NEON formátumú konfigurációs fájlban az a hely, ahol saját szolgáltatásainkat és azok konfigurációját definiáljuk. Nézzünk egy egyszerű példát egy `database` nevű szolgáltatás definíciójára, amely egy `PDO` osztály példányát reprezentálja: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -A megadott konfiguráció a következő factory metódust eredményezi a [DI konténerben|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -A szolgáltatásnevek lehetővé teszik, hogy a konfigurációs fájl más részeiben hivatkozzunk rájuk, `@szolgaltatasNev` formátumban. Ha nincs szükség a szolgáltatás elnevezésére, egyszerűen használhatunk csak egy kötőjelet: - -```neon -services: - - PDO('sqlite::memory:') -``` - -A szolgáltatás lekéréséhez a DI konténerből használhatjuk a `getService()` metódust a szolgáltatás nevével paraméterként, vagy a `getByType()` metódust a szolgáltatás típusával: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Szolgáltatás létrehozása -======================== - -Legtöbbször egyszerűen úgy hozunk létre egy szolgáltatást, hogy létrehozunk egy példányt egy adott osztályból. Például: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Ha a konfigurációt további kulcsokkal kell bővítenünk, a definíciót több sorba is szétírhatjuk: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -A `create` kulcsnak van egy `factory` aliasa, mindkét változat gyakori a gyakorlatban. Azonban javasoljuk a `create` használatát. - -A konstruktor vagy a létrehozó metódus argumentumai alternatívaként az `arguments` kulcsban is megadhatók: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -A szolgáltatásokat nemcsak egyszerű osztálypéldányosítással lehet létrehozni, hanem statikus metódusok vagy más szolgáltatások metódusainak hívásának eredményeként is: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Vegyük észre, hogy az egyszerűség kedvéért `->` helyett `::` használatos, lásd [#kifejező eszközök]. Ezek a factory metódusok generálódnak: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -A DI konténernek ismernie kell a létrehozott szolgáltatás típusát. Ha egy olyan metódussal hozunk létre szolgáltatást, amelynek nincs megadva visszatérési típusa, akkor ezt a típust explicit módon meg kell adnunk a konfigurációban: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumentumok -============ - -A konstruktoroknak és metódusoknak argumentumokat adunk át, nagyon hasonlóan magához a PHP-hez: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -A jobb olvashatóság érdekében az argumentumokat külön sorokba írhatjuk. Ebben az esetben a vesszők használata opcionális: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Az argumentumokat el is nevezheti, és akkor nem kell törődnie a sorrendjükkel: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Ha ki szeretne hagyni néhány argumentumot, és azok alapértelmezett értékét szeretné használni, vagy egy szolgáltatást szeretne beilleszteni az [autowiring|autowiring] segítségével, használjon aláhúzást: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Argumentumként átadhatók szolgáltatások, használhatók paraméterek és még sok más, lásd [#kifejező eszközök]. - - -Setup -===== - -A `setup` szekcióban definiáljuk azokat a metódusokat, amelyeket a szolgáltatás létrehozásakor kell meghívni. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Ez PHP-ban így nézne ki: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -A metódushívásokon kívül értékeket is átadhatunk a property-knek. Támogatott az elem hozzáadása egy tömbhöz is, amelyet idézőjelek közé kell írni, hogy ne ütközzön a NEON szintaxisával: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Ami a PHP kódban a következőképpen nézne ki: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -A setupban azonban hívhatunk statikus metódusokat vagy más szolgáltatások metódusait is. Ha az aktuális szolgáltatást argumentumként kell átadni, adja meg `@self`-ként: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Vegyük észre, hogy az egyszerűség kedvéért `->` helyett `::` használatos, lásd [#kifejező eszközök]. Ilyen factory metódus generálódik: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Kifejező eszközök -================= - -A Nette DI rendkívül gazdag kifejező eszközöket ad nekünk, amelyekkel szinte bármit leírhatunk. A konfigurációs fájlokban így használhatunk [paramétereket |configuration#Paraméterek]: - -```neon -# paraméter -%wwwDir% - -# paraméter értéke kulcs alatt -%mailer.user% - -# paraméter egy stringen belül -'%wwwDir%/images' -``` - -Továbbá objektumokat hozhatunk létre, metódusokat és függvényeket hívhatunk: - -```neon -# objektum létrehozása -DateTime() - -# statikus metódus hívása -Collator::create(%locale%) - -# PHP függvény hívása -::getenv(DB_USER) -``` - -Hivatkozhatunk szolgáltatásokra akár a nevükkel, akár a típusukkal: - -```neon -# szolgáltatás név szerint -@database - -# szolgáltatás típus szerint -@Nette\Database\Connection -``` - -Használhatunk first-class callable szintaxist: .{data-version:3.2.0} - -```neon -# callback létrehozása, hasonlóan a [@user, logout]-hoz -@user::logout(...) -``` - -Használhatunk konstansokat: - -```neon -# osztály konstans -FilesystemIterator::SKIP_DOTS - -# globális konstansot a constant() PHP függvénnyel kapunk -::constant(PHP_VERSION) -``` - -A metódushívásokat ugyanúgy lehet láncolni, mint PHP-ban. Csak az egyszerűség kedvéért `->` helyett `::` használatos: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Ezeket a kifejezéseket bárhol használhatja, a [szolgáltatások létrehozásakor |#Szolgáltatás létrehozása], az [argumentumokban |#Argumentumok], a [#setup] szekcióban vagy a [paraméterekben |configuration#Paraméterek]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Speciális függvények --------------------- - -A konfigurációs fájlokban használhatja ezeket a speciális függvényeket: - -- `not()` érték negálása -- `bool()`, `int()`, `float()`, `string()` veszteségmentes típuskonverzió a megadott típusra -- `typed()` létrehozza a megadott típusú összes szolgáltatás tömbjét -- `tagged()` létrehozza a megadott taggel rendelkező összes szolgáltatás tömbjét - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -A klasszikus PHP típuskonverzióval ellentétben, mint pl. az `(int)`, a veszteségmentes típuskonverzió kivételt dob nem numerikus értékek esetén. - -A `typed()` függvény létrehozza a megadott típusú (osztály vagy interfész) összes szolgáltatás tömbjét. Kihagyja azokat a szolgáltatásokat, amelyeknek ki van kapcsolva az autowiringja. Több típust is meg lehet adni vesszővel elválasztva. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Egy adott típusú szolgáltatások tömbjét argumentumként is átadhatja automatikusan az [autowiring |autowiring#Szolgáltatások tömbje] segítségével. - -A `tagged()` függvény pedig létrehozza az összes, adott taggel rendelkező szolgáltatás tömbjét. Itt is megadhat több taget vesszővel elválasztva. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Az `autowired` kulcs lehetővé teszi az autowiring viselkedésének befolyásolását egy adott szolgáltatásra. Részletekért lásd az [autowiringról szóló fejezetet|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # a foo szolgáltatás ki van zárva az autowiringból -``` - - -Lazy szolgáltatások .{data-version:3.2.4} -========================================= - -A lazy loading egy technika, amely elhalasztja a szolgáltatás létrehozását egészen addig a pillanatig, amíg valóban szükség van rá. A globális konfigurációban [engedélyezhető a lazy létrehozás |configuration#Lazy szolgáltatások] minden szolgáltatásra egyszerre. Az egyes szolgáltatások esetében ezt a viselkedést felülbírálhatja: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Ha egy szolgáltatás lazy-ként van definiálva, annak a DI konténerből való lekérésekor egy speciális helyettesítő objektumot kapunk. Ez ugyanúgy néz ki és viselkedik, mint a valódi szolgáltatás, de a tényleges inicializálás (konstruktor és setup hívása) csak bármely metódusának vagy property-jének első hívásakor történik meg. - -.[note] -A lazy loading csak felhasználói osztályokra használható, belső PHP osztályokra nem. PHP 8.4 vagy újabb verziót igényel. - - -Tagek -===== - -A tagek további információk hozzáadására szolgálnak a szolgáltatásokhoz. Egy szolgáltatáshoz egy vagy több taget adhat hozzá: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -A tagek értékeket is hordozhatnak: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Ahhoz, hogy megkapja az összes, adott tagekkel rendelkező szolgáltatást, használhatja a `tagged()` függvényt: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -A DI konténerben lekérheti az összes, adott taggel rendelkező szolgáltatás nevét a `findByTag()` metódussal: - -```php -$names = $container->findByTag('logger'); -// $names egy tömb, amely tartalmazza a szolgáltatás nevét és a tag értékét -// pl. ['foo' => 'monolog.logger.event', ...] -``` - - -Inject mód -========== - -Az `inject: true` jelzővel aktiválódik a függőségek átadása a public property-ken keresztül [inject |best-practices:inject-method-attribute#Inject attribútumok] annotációval és az [inject*() |best-practices:inject-method-attribute#inject metódusok] metódusokkal. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Alapértelmezés szerint az `inject` csak a presenterekre van aktiválva. - - -Szolgáltatások módosítása -========================= - -A DI konténer számos szolgáltatást tartalmaz, amelyeket beépített vagy [felhasználói kiterjesztés|extensions] révén adtak hozzá. Módosíthatja ezeknek a szolgáltatásoknak a definícióit közvetlenül a konfigurációban. Például megváltoztathatja az `application.application` szolgáltatás osztályát, amely alapértelmezés szerint `Nette\Application\Application`, egy másikra: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Az `alteration` jelző informatív jellegű, és azt jelzi, hogy csak egy meglévő szolgáltatást módosítunk. - -Kiegészíthetjük a setupot is: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Egy szolgáltatás felülírásakor előfordulhat, hogy el akarjuk távolítani az eredeti argumentumokat, setup elemeket vagy tageket, erre szolgál a `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Ha el szeretne távolítani egy kiterjesztés által hozzáadott szolgáltatást, azt így teheti meg: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/it/@home.texy b/dependency-injection/it/@home.texy index 28ffcf93fb..6c42813c18 100644 --- a/dependency-injection/it/@home.texy +++ b/dependency-injection/it/@home.texy @@ -2,20 +2,21 @@ Nette DI ******** .[perex] -La Dependency Injection è un design pattern che cambierà radicalmente la tua prospettiva sul codice e sullo sviluppo. Ti aprirà le porte a un mondo di applicazioni progettate in modo pulito e sostenibile. +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. -- [Cos'è la Dependency Injection? |introduction] +- [Che cos'è la dependency injection? |introduction] - [Stato globale e singleton |global-state] - [Passaggio delle dipendenze |passing-dependencies] -- [Cos'è un container DI? |container] -- [Domande frequenti|faq] +- [Che cos'è un container DI? |container] +- [Domande frequenti |faq] -Il pacchetto `nette/di` fornisce un container DI compilato estremamente avanzato per PHP. +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] -- [Creazione di estensioni per Nette DI|extensions] +- [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 index 5fb12cd34a..bef29981f4 100644 --- a/dependency-injection/it/@left-menu.texy +++ b/dependency-injection/it/@left-menu.texy @@ -1,10 +1,10 @@ Dependency Injection ******************** -- [Cos'è la DI? |introduction] +- [Che cos'è la DI? |introduction] - [Stato globale e singleton |global-state] - [Passaggio delle dipendenze |passing-dependencies] -- [Cos'è un container DI? |container] -- [Domande frequenti|faq] +- [Che cos'è un container DI? |container] +- [Domande frequenti |faq] Nette DI @@ -14,4 +14,15 @@ Nette DI - [Definizione dei servizi |services] - [Autowiring |autowiring] - [Factory generate |factory] -- [Creazione di estensioni per Nette DI|extensions] +- [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/autowiring.texy b/dependency-injection/it/autowiring.texy index 231e26bbc0..c57398680a 100644 --- a/dependency-injection/it/autowiring.texy +++ b/dependency-injection/it/autowiring.texy @@ -2,9 +2,9 @@ Autowiring ********** .[perex] -L'Autowiring è una funzionalità fantastica che può passare automaticamente i servizi richiesti al costruttore e ad altri metodi, quindi non dobbiamo scriverli affatto. Ti fa risparmiare un sacco di tempo. +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 questo, possiamo omettere la stragrande maggioranza degli argomenti quando scriviamo le definizioni dei servizi. Invece di: +Grazie a esso possiamo omettere la stragrande maggioranza degli argomenti quando scriviamo le definizioni dei servizi. Invece di: ```neon services: @@ -18,7 +18,7 @@ services: articles: Model\ArticleRepository ``` -L'Autowiring si basa sui tipi, quindi affinché funzioni, la classe `ArticleRepository` deve essere definita più o meno così: +L'autowiring si basa sui tipi, quindi perché funzioni la classe `ArticleRepository` deve essere definita più o meno così: ```php namespace Model; @@ -30,22 +30,24 @@ class ArticleRepository } ``` -Per poter utilizzare l'autowiring, deve esserci **esattamente un servizio** per ogni tipo nel container. Se ce ne fossero di più, l'autowiring non saprebbe quale passare e lancerebbe un'eccezione: +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 # LANCIA ECCEZIONE, soddisfano sia mainDb che tempDb + articles: Model\ArticleRepository # SOLLEVA UN'ECCEZIONE, corrispondono sia mainDb sia tempDb ``` -La soluzione sarebbe bypassare l'autowiring e specificare esplicitamente il nome del servizio (cioè `articles: Model\ArticleRepository(@mainDb)`). Ma è più intelligente [disattivare |#Disattivazione dell autowiring] l'autowiring per uno dei servizi, o [dare la preferenza |#Preferenza dell autowiring] al primo servizio. +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. -Disattivazione dell'autowiring ------------------------------- +Disattivare l'autowiring +------------------------ -Possiamo disattivare l'autowiring di un servizio usando l'opzione `autowired: no`: +Possiamo disattivare l'autowiring di un servizio con l'opzione `autowired: false`: ```neon services: @@ -55,25 +57,27 @@ services: create: PDO('sqlite::memory:') autowired: false # il servizio tempDb è escluso dall'autowiring - articles: Model\ArticleRepository # quindi passa mainDb al costruttore + articles: Model\ArticleRepository # al costruttore viene quindi passato mainDb ``` -Il servizio `articles` non lancerà un'eccezione perché esistono due servizi compatibili di tipo `PDO` (cioè `mainDb` e `tempDb`) che possono essere passati al costruttore, perché vede solo il servizio `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 funziona diversamente rispetto a Symfony, dove l'opzione `autowire: false` indica che l'autowiring non deve essere utilizzato per gli argomenti del costruttore del servizio specificato. In Nette, l'autowiring viene sempre utilizzato, sia per gli argomenti del costruttore che per qualsiasi altro metodo. L'opzione `autowired: false` indica che l'istanza del servizio specificato non deve essere passata da nessuna parte tramite autowiring. +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 dell'autowiring +Preferenza nell'autowiring -------------------------- -Se abbiamo più servizi dello stesso tipo e per uno di essi specifichiamo l'opzione `autowired`, questo servizio diventa preferito: +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 preferito + autowired: PDO # diventa quello preferito tempDb: create: PDO('sqlite::memory:') @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -Il servizio `articles` non lancerà un'eccezione perché esistono due servizi compatibili di tipo `PDO` (cioè `mainDb` e `tempDb`), ma utilizzerà il servizio preferito, ovvero `mainDb`. +Il servizio `articles` non solleverà un'eccezione sulla presenza di più servizi `PDO` corrispondenti (`mainDb` e `tempDb`), ma userà quello preferito, cioè `mainDb`. -Array di servizi ----------------- +Collezione di servizi +--------------------- -L'Autowiring può anche passare array di servizi di un certo tipo. Poiché in PHP non è possibile scrivere nativamente il tipo degli elementi dell'array, è necessario aggiungere, oltre al tipo `array`, anche un commento phpDoc con il tipo dell'elemento nella forma `ClassName[]`: +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; @@ -102,45 +106,62 @@ class ShipManager } ``` -Il container DI passerà quindi automaticamente un array di servizi corrispondenti al tipo specificato. Ometterà i servizi che hanno l'autowiring disattivato. +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ò anche essere nella forma `array<int, Class>` o `list<Class>`. Se non puoi influenzare la forma del commento phpDoc, puoi passare l'array di servizi direttamente nella configurazione usando [`typed()` |services#Funzioni speciali]. +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 può fornire solo oggetti e array di oggetti. Gli argomenti scalari (ad es. stringhe, numeri, booleani) [li scriviamo nella configurazione |services#Argomenti]. Un'alternativa è creare un [oggetto-impostazioni |best-practices:passing-settings-to-presenters], che incapsula il valore scalare (o più valori) in un oggetto, che può poi essere nuovamente passato tramite autowiring. +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 può essere usato da PHP 8.1 + // readonly si può usare da PHP 8.1 public readonly bool $value, ) {} } ``` -Lo trasformi in un servizio aggiungendolo alla configurazione: +Lo registrate come servizio aggiungendolo alla configurazione: ```neon services: - MySettings('any value') ``` -Tutte le classi lo richiederanno quindi tramite autowiring. +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. -Restrizione dell'autowiring ---------------------------- +Restringere l'autowiring +------------------------ -Per singoli servizi, l'autowiring può essere ristretto a determinate classi o interfacce. +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. La restrizione significa che stabiliamo condizioni che i tipi specificati nei parametri dei metodi devono soddisfare affinché il servizio venga loro passato. +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. -Lo mostreremo con un esempio: +Prendiamo un esempio: ```php class ParentClass @@ -168,36 +189,36 @@ Se li registrassimo tutti come servizi, l'autowiring fallirebbe: services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # LANCIA ECCEZIONE, soddisfano i servizi parent e child - childDep: ChildDependent # l'autowiring passa il servizio child al costruttore + parentDep: ParentDependent # SOLLEVA UN'ECCEZIONE, corrispondono sia parent sia child + childDep: ChildDependent # l'autowiring passa al costruttore il servizio child ``` -Il servizio `parentDep` lancerà l'eccezione `Multiple services of type ParentClass found: parent, child`, perché entrambi i servizi `parent` e `child` corrispondono al suo costruttore, e l'autowiring non può decidere quale scegliere. +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 restringere il suo autowiring al tipo `ChildClass`: +Per il servizio `child` possiamo quindi restringerne l'autowiring al tipo `ChildClass`: ```neon services: parent: ParentClass child: create: ChildClass - autowired: ChildClass # si può anche scrivere 'autowired: self' + autowired: ChildClass # si può scrivere anche 'autowired: self' - parentDep: ParentDependent # l'autowiring passa il servizio parent al costruttore - childDep: ChildDependent # l'autowiring passa il servizio child al costruttore + 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é ora è l'unico oggetto compatibile. L'autowiring non passerà più il servizio `child` lì. Sì, il servizio `child` è ancora di tipo `ParentClass`, ma la condizione restrittiva data per il tipo del parametro non è più valida, cioè non è vero che `ParentClass` *è un supertipo di* `ChildClass`. +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` potrebbe anche essere scritto come `autowired: self`, poiché `self` è un segnaposto per la classe del servizio corrente. +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 specificare anche più classi o interfacce come array: +Nella chiave `autowired` è possibile indicare anche più classi o interfacce, come array: ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -Proviamo a completare l'esempio con un'interfaccia: +Proviamo ad aggiungere delle interfacce all'esempio: ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -Se non limitiamo in alcun modo il servizio `child`, corrisponderà ai costruttori di tutte le classi `FooDependent`, `BarDependent`, `ParentDependent` e `ChildDependent` e l'autowiring lo passerà lì. +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à. -Ma se restringiamo il suo autowiring a `ChildClass` usando `autowired: ChildClass` (o `self`), l'autowiring lo passerà solo al costruttore di `ChildDependent`, perché richiede un argomento di tipo `ChildClass` ed è vero che `ChildClass` *è di tipo* `ChildClass`. Nessun altro tipo specificato negli altri parametri è un supertipo di `ChildClass`, quindi il servizio non viene passato. +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` usando `autowired: ParentClass`, l'autowiring lo passerà di nuovo al costruttore di `ChildDependent` (perché il `ChildClass` richiesto è un supertipo di `ParentClass`) e ora anche al costruttore di `ParentDependent`, perché anche il tipo `ParentClass` richiesto è compatibile. +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`, sarà ancora autowired in `ParentDependent` (il `ParentClass` richiesto è un supertipo di `FooInterface`) e `ChildDependent`, ma inoltre anche nel costruttore di `FooDependent`, ma non in `BarDependent`, perché `BarInterface` non è un supertipo di `FooInterface`. +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: @@ -251,8 +272,8 @@ services: create: ChildClass autowired: FooInterface - fooDep: FooDependent # l'autowiring passa child al costruttore - barDep: BarDependent # LANCIA ECCEZIONE, nessun servizio corrisponde - parentDep: ParentDependent # l'autowiring passa child al costruttore - childDep: ChildDependent # l'autowiring passa child al costruttore + 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 new file mode 100644 index 0000000000..9c89e43c60 --- /dev/null +++ b/dependency-injection/it/compilation-internals.texy @@ -0,0 +1,222 @@ +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 index 7155ff7ebf..002ee915cc 100644 --- a/dependency-injection/it/configuration.texy +++ b/dependency-injection/it/configuration.texy @@ -2,18 +2,18 @@ Configurazione del container DI ******************************* .[perex] -Panoramica delle opzioni di configurazione per il container Nette DI. +Panoramica delle opzioni di configurazione del container DI di Nette. File di configurazione ====================== -Il container Nette DI è facilmente controllabile tramite file di configurazione. Questi sono solitamente scritti nel [formato NEON|neon:format]. Per la modifica, consigliamo [editor con supporto |best-practices:editors-and-tools#Editor IDE] per questo formato. +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 estensioni DI aggiuntive .[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> @@ -21,13 +21,13 @@ Il container Nette DI è facilmente controllabile tramite file di configurazione </pre> .[note] -Per scrivere una stringa contenente il carattere `%`, è necessario escaparlo raddoppiandolo in `%%`. +Per scrivere una stringa che contiene il carattere `%`, dovete effettuare l'escape raddoppiandolo in `%%`. Parametri ========= -Nella configurazione puoi definire parametri che possono poi essere utilizzati come parte delle definizioni dei servizi. In questo modo puoi rendere la configurazione più chiara o unificare ed estrarre valori che cambieranno. +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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -Ci riferiamo al parametro `dsn` ovunque nella configurazione scrivendo `%dsn%`. I parametri possono essere utilizzati anche all'interno di stringhe come `'%wwwDir%/images'`. +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 solo stringhe o numeri, possono anche contenere array: +I parametri non devono essere per forza solo stringhe o numeri: possono contenere anche array: ```neon parameters: @@ -49,21 +49,21 @@ parameters: languages: [cs, en, de] ``` -Ci riferiamo a una chiave specifica come `%mailer.user%`. +Facciamo riferimento a una chiave specifica come `%mailer.user%`. -Se hai bisogno nel tuo codice, ad esempio in una classe, di conoscere il valore di un qualsiasi parametro, passalo a questa classe. Ad esempio nel costruttore. Non esiste un oggetto globale che rappresenti la configurazione, a cui le classi chiedono i valori dei parametri. Ciò violerebbe il principio di dependency injection. +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 [capitolo separato|services]. +Vedi il [capitolo dedicato|services]. Decorator ========= -Come modificare in blocco tutti i servizi di un certo tipo? Ad esempio, chiamare un certo metodo su tutti i presenter che ereditano da un antenato comune specifico? A questo serve il 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: @@ -74,7 +74,7 @@ decorator: - $absoluteUrls = true # e imposta la variabile ``` -Il decorator può essere utilizzato anche per impostare [tag |services#Tag] o attivare la modalità [inject |services#Modalità Inject]. +I decorator si possono usare anche per impostare i [tag |services#Tag] o per attivare la [modalità inject |services#Modalità inject]. ```neon decorator: @@ -91,86 +91,86 @@ Impostazioni tecniche del container DI. ```neon di: - # visualizzare DIC nella Tracy Bar? - debugger: ... # (bool) predefinito è true + # mostrare il DIC nella Tracy Bar? + debugger: ... # (bool) di norma per rilevamento automatico (attivo quando Tracy è presente) - # tipi di parametri da non autowirare mai + # tipi di parametro a cui non applicare mai l'autowiring excluded: ... # (string[]) - # consentire la creazione lazy dei servizi? - lazy: ... # (bool) predefinito è false + # attivare la creazione pigra dei servizi? + lazy: ... # (bool) di norma false - # classe da cui eredita il container DI - parentClass: ... # (string) predefinito è Nette\DI\Container + # la classe da cui eredita il container DI + parentClass: ... # (string) di norma Nette\DI\Container ``` -Servizi lazy .{data-version:3.2.4} ----------------------------------- +Servizi pigri .{data-version:3.2.4} +----------------------------------- -L'impostazione `lazy: true` attiva la creazione lazy (differita) dei servizi. Ciò significa che i servizi non vengono effettivamente creati nel momento in cui li richiediamo dal container DI, ma solo al momento del loro primo utilizzo. Ciò può accelerare l'avvio dell'applicazione e ridurre l'utilizzo della memoria, poiché vengono creati solo i servizi effettivamente necessari nella richiesta corrente. +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 lazy può essere [modificata |services#Servizi Lazy]. +Per un servizio specifico la creazione pigra si può [regolare |services#Servizi pigri]. .[note] -Gli oggetti lazy possono essere utilizzati solo per classi utente, non per classi PHP interne. Richiede PHP 8.4 o successivo. +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 metadati ---------------------- +Esportazione dei metadati +------------------------- -La classe del container DI contiene anche molti metadati. Puoi ridurne le dimensioni riducendo l'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) predefinito è true + parameters: false # (bool) di norma true # esportare i tag e quali? - tags: # (string[]|bool) predefiniti sono tutti + tags: # (string[]|bool) di norma tutti - event.subscriber # esportare i dati per l'autowiring e quali? - types: # (string[]|bool) predefiniti sono tutti + types: # (string[]|bool) di norma tutti - Nette\Database\Connection - Symfony\Component\Console\Application ``` -Se non utilizzi l'array `$container->getParameters()`, puoi disattivare l'esportazione dei parametri. Inoltre, puoi esportare solo i tag tramite i quali ottieni i servizi con il metodo `$container->findByTag(...)`. Se non chiami affatto il metodo, puoi disattivare completamente l'esportazione dei tag usando `false`. +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`. -Puoi ridurre significativamente i metadati per [l'autowiring |autowiring] specificando le classi che usi come parametro del metodo `$container->getByType()`. E ancora, se non chiami affatto il metodo (o solo nel [bootstrap|application:bootstrapping] per ottenere `Nette\Application\Application`), puoi disattivare completamente l'esportazione usando `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 estensioni DI aggiuntive. In questo modo aggiungiamo ad esempio l'estensione DI `Dibi\Bridges\Nette\DibiExtension22` con il nome `dibi` +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\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -Successivamente, la configuriamo nella sezione `dibi`: +La configurate poi nella sezione `dibi`: ```neon dibi: host: localhost ``` -Come estensione si può aggiungere anche una classe che ha parametri: +Come estensione potete aggiungere anche una classe con parametri: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` Inclusione di file ================== -Possiamo includere altri file di configurazione nella sezione `includes`: +Ulteriori file di configurazione si possono includere nella sezione `includes`: ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -Il nome `parameters.php` non è un errore di battitura, la configurazione può essere scritta anche in un file PHP, che la restituisce come array: +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 @@ -192,15 +192,15 @@ return [ ]; ``` -Se nei file di configurazione compaiono elementi con le stesse chiavi, verranno sovrascritti o, nel caso di [array, uniti |#Unione]. Il file incluso successivamente ha una priorità maggiore rispetto al precedente. Il file in cui è specificata la sezione `includes` ha una priorità maggiore rispetto ai file inclusi al suo interno. +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 ====== -L'aggiunta automatica di servizi al container DI rende il lavoro estremamente piacevole. Nette aggiunge automaticamente i presenter al container, ma è possibile aggiungere facilmente anche qualsiasi altra classe. +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 specificare in quali directory (e sottodirectory) cercare le classi: +Basta indicare in quali directory (e sottodirectory) cercare le classi: ```neon search: @@ -208,22 +208,29 @@ search: - in: %appDir%/Model ``` -Di solito, però, non vogliamo aggiungere assolutamente tutte le classi e le interfacce, quindi possiamo filtrarle: +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 file (string|string[]) + # filtraggio per nome di file (string|string[]) files: - *Factory.php - # filtraggio per nome classe (string|string[]) + # filtraggio per nome di classe (string|string[]) classes: - *Factory ``` -Oppure possiamo selezionare classi che ereditano o implementano almeno una delle classi specificate: +Oppure possiamo selezionare le classi che ereditano o implementano almeno una delle classi elencate: ```neon @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -È possibile definire anche regole di esclusione, cioè maschere di nomi di classi o antenati ereditari, che se soddisfatte, il servizio non viene aggiunto al container DI: +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: @@ -247,7 +254,7 @@ search: implements: ... ``` -A tutti i servizi possono essere assegnati tag: +A tutti i servizi registrati automaticamente si possono assegnare dei tag: ```neon search: @@ -255,11 +262,13 @@ search: 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, verranno sovrascritti o, nel caso di array, uniti. Il file incluso successivamente ha una priorità maggiore rispetto al precedente. +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> @@ -292,7 +301,7 @@ items: </tr> </table> -Per gli array, è possibile impedire l'unione specificando un punto esclamativo dopo il nome della chiave: +Per gli array l'unione si può impedire aggiungendo un punto esclamativo dopo il nome della chiave: <table class=table> <tr> @@ -323,4 +332,4 @@ items: </tr> </table> -{{maintitle: Configurazione Dependency Injection}} +{{maintitle: Configurazione della dependency injection}} diff --git a/dependency-injection/it/container.texy b/dependency-injection/it/container.texy index d5ab3d2efd..2d70848770 100644 --- a/dependency-injection/it/container.texy +++ b/dependency-injection/it/container.texy @@ -1,16 +1,16 @@ -Cos'è un container DI? -********************** +Che cos'è un container DI? +************************** .[perex] -Un container dependency injection (DIC) è una classe che può istanziare e configurare oggetti. +Un container di dependency injection (DIC o container DI) è un oggetto che si occupa di istanziare e configurare altri oggetti (chiamati servizi). -Potrebbe sorprenderti, ma in molti casi non hai bisogno di un container dependency injection per sfruttare i vantaggi della dependency injection (abbreviato DI). Dopotutto, anche nel [capitolo introduttivo|introduction] abbiamo mostrato DI con esempi concreti e non era necessario alcun container. +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. -Tuttavia, se devi gestire un gran numero di oggetti diversi con molte dipendenze, un container dependency injection sarà davvero utile. Questo è il caso, ad esempio, delle applicazioni web costruite su un framework. +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` e `UserController`. Entrambe hanno alcune dipendenze, ovvero il database e la factory `ArticleFactory`. E per queste classi creeremo ora un container. Ovviamente, per un esempio così semplice, non ha senso avere un container. Ma lo creeremo per mostrare come appare e funziona. +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 hardcoded per l'esempio fornito: +Ecco un semplice container scritto a mano per l'esempio qui sopra: ```php class Container @@ -25,23 +25,23 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` -L'utilizzo sarebbe il seguente: +L'uso avrebbe questo aspetto: ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -Chiediamo semplicemente l'oggetto al container e non dobbiamo più sapere nulla su come crearlo e quali dipendenze ha; il container sa tutto questo. Le dipendenze vengono iniettate automaticamente dal container. In questo sta la sua forza. +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. -Per ora, il container ha tutti i dati scritti in modo fisso. Faremo quindi il passo successivo e aggiungeremo parametri per rendere il container veramente utile: +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 @@ -70,7 +70,7 @@ $container = new Container([ ]); ``` -I lettori attenti potrebbero aver notato un certo problema. Ogni volta che ottengo un oggetto `UserController`, viene creata anche una nuova istanza di `ArticleFactory` e del database. Questo decisamente non lo vogliamo. +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: @@ -98,9 +98,9 @@ class Container } ``` -Alla prima chiamata, ad esempio `$container->getService('Database')`, farà creare l'oggetto database da `createDatabase()`, lo salverà nell'array `$services` e alla chiamata successiva lo restituirà direttamente. +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. -Modificheremo anche il resto del container per utilizzare `getService()`: +Modifichiamo anche il resto del container perché usi `getService()`: ```php class Container @@ -112,16 +112,16 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -A proposito, il termine servizio si riferisce a qualsiasi oggetto gestito dal container. Ecco perché anche il nome del metodo `getService()`. +A proposito, il termine servizio indica qualsiasi oggetto gestito dal container. Da qui il nome del metodo `getService()`. -Fatto. Abbiamo un container DI completamente funzionante! E possiamo usarlo: +Fatto. Abbiamo un container DI pienamente funzionante! E possiamo usarlo: ```php $container = new Container([ @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -Come vedi, scrivere un DIC non è complicato. Vale la pena ricordare che gli oggetti stessi non sanno di essere creati da un container. Di conseguenza, è possibile creare in questo modo qualsiasi oggetto in PHP senza intervenire sul suo codice sorgente. +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. -La creazione e la manutenzione manuale della classe del container possono diventare rapidamente un incubo. Nel prossimo capitolo, parleremo quindi del [Container Nette DI|nette-container], che può generarsi e aggiornarsi quasi da solo. +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: Cos'è un container dependency injection?}} +{{maintitle: Che cos'è un container di dependency injection?}} diff --git a/dependency-injection/it/extensions.texy b/dependency-injection/it/extensions.texy index 2c7da1fc47..b9cea84239 100644 --- a/dependency-injection/it/extensions.texy +++ b/dependency-injection/it/extensions.texy @@ -1,39 +1,67 @@ -Creazione di estensioni per Nette DI -************************************ +Creare estensioni per Nette DI +****************************** .[perex] -La generazione del container DI, oltre ai file di configurazione, è influenzata anche dalle cosiddette *estensioni*. Le attiviamo nel file di configurazione nella sezione `extensions`. +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. -In questo modo aggiungiamo l'estensione rappresentata dalla classe `BlogExtension` con il nome `blog`: +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 ``` -Ogni estensione del compilatore eredita da [api:Nette\DI\CompilerExtension] e può implementare i seguenti metodi, che vengono chiamati in sequenza durante la costruzione del container DI: +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. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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: -getConfigSchema() .[method] -=========================== +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]) -Questo metodo viene chiamato per primo. Definisce lo schema per la validazione dei parametri di configurazione. +.[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. -Configuriamo l'estensione nella sezione il cui nome è lo stesso di quello con cui è stata aggiunta l'estensione, cioè `blog`: +.[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 -# stesso nome dell'estensione +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Creiamo uno schema che descrive tutte le opzioni di configurazione, inclusi i loro tipi, valori consentiti ed eventualmente anche valori predefiniti: +E questa è tutta la classe: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` -La documentazione si trova nella pagina [Schema |schema:]. Inoltre, è possibile specificare quali opzioni possono essere [dinamiche |application:bootstrapping#Parametri Dinamici] usando `dynamic()`, ad es. `Expect::int()->dynamic()`. -Accediamo alla configurazione tramite la variabile `$this->config`, che è un oggetto `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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. -loadConfiguration() .[method] -============================= -Utilizzato per aggiungere servizi al container. A questo serve [api:Nette\DI\ContainerBuilder]: +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -La convenzione è di prefissare i servizi aggiunti dall'estensione con il suo nome, per evitare conflitti di nomi. Questo lo fa il metodo `prefix()`, quindi se l'estensione si chiama `blog`, il servizio si chiamerà `blog.articles`. +La configurazione validata è disponibile in `$this->config` come oggetto `stdClass` (oppure come array, se allo schema aggiungete `castTo('array')`). -Se dobbiamo rinominare un servizio, possiamo creare un alias con il nome originale per mantenere la compatibilità all'indietro. Nette fa qualcosa di simile, ad esempio, con il servizio `routing.router`, che è disponibile anche con il nome precedente `router`. +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 -$builder->addAlias('router', 'routing.router'); +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: -Caricamento dei servizi da file -------------------------------- +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -Non dobbiamo creare servizi solo tramite l'API della classe ContainerBuilder, ma anche con la nota sintassi utilizzata nel file di configurazione NEON nella sezione services. Il prefisso `@extension` rappresenta l'estensione corrente. +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: @@ -107,88 +160,284 @@ services: 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: - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Carichiamo i servizi: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // caricamento del file di configurazione per l'estensione - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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) { + // ... +} +``` -beforeCompile() .[method] -========================= -Il metodo viene chiamato nel momento in cui il container contiene tutti i servizi aggiunti dalle singole estensioni nei metodi `loadConfiguration` e anche dai file di configurazione utente. In questa fase di costruzione, possiamo quindi modificare le definizioni dei servizi o aggiungere legami tra di essi. Per cercare servizi nel container in base ai tag, si può utilizzare il metodo `findByTag()`, per classe o interfaccia invece il metodo `findByType()`. +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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. -afterCompile() .[method] -======================== -In questa fase, la classe del container è già generata sotto forma di oggetto [ClassType |php-generator:#Classi], contiene tutti i metodi che creano i servizi ed è pronta per essere scritta nella cache. Possiamo ancora modificare il codice risultante della classe in questo momento. +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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Un uso tipico è registrare un pannello di Tracy solo in modalità di sviluppo. + -$initialization .[method] -========================= +Argomenti complessi +------------------- -La classe Configurator, dopo la [creazione del container |application:bootstrapping#index.php], chiama il codice di inizializzazione, che viene creato scrivendo nell'oggetto `$this->initialization` tramite il [metodo addBody() |php-generator:#Corpi di metodi e funzioni]. +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: -Mostriamo un esempio di come, ad esempio, avviare la sessione con il codice di inizializzazione o avviare servizi che hanno il tag `run`: +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // avvio automatico della sessione - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // questo tag verrà sempre esportato, anche se l'esportazione è limitata + $this->compiler->addExportedTag('event.subscriber'); - // i servizi con il tag run devono essere creati dopo l'istanza del container - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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 index 6ee12f3e24..f7fc9a08d1 100644 --- a/dependency-injection/it/factory.texy +++ b/dependency-injection/it/factory.texy @@ -2,11 +2,11 @@ Factory generate **************** .[perex] -Nette DI può generare automaticamente il codice delle factory basandosi su interfacce, risparmiandoti la scrittura di codice. +Nette DI sa generare automaticamente il codice delle factory a partire dalle interfacce, risparmiandovi di scrivere codice. -Una factory è una classe che produce e configura oggetti. Quindi passa loro anche le loro dipendenze. Si prega di non confondere con il design pattern *factory method*, che descrive un modo specifico di utilizzare le factory e non è correlato a questo argomento. +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. -Come appare una tale factory lo abbiamo mostrato nel [capitolo introduttivo |introduction#Factory]: +Abbiamo mostrato che aspetto ha una factory del genere nel [capitolo introduttivo |introduction#Factory]: ```php class ArticleFactory @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DI può generare automaticamente il codice delle factory. Tutto ciò che devi fare è creare un'interfaccia e Nette DI genererà l'implementazione. L'interfaccia deve avere esattamente un metodo chiamato `create` e dichiarare il tipo di ritorno: +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 @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -Quindi la factory `ArticleFactory` ha un metodo `create`, che crea oggetti `Article`. La classe `Article` può apparire ad esempio così: +La factory `ArticleFactory` ha quindi un metodo `create` che crea oggetti `Article`. La classe `Article` potrebbe avere per esempio questo aspetto: ```php class Article @@ -44,16 +44,16 @@ class Article } ``` -Aggiungiamo la factory al file di configurazione: +Aggiungete la factory al file di configurazione: ```neon services: - ArticleFactory ``` -Nette DI genererà l'implementazione corrispondente della factory. +Nette DI genererà la corrispondente implementazione della factory. -Nel codice che utilizza la factory, richiediamo quindi l'oggetto tramite l'interfaccia e Nette DI utilizzerà l'implementazione generata: +Nel codice che usa la factory, chiedete l'oggetto tramite la sua interfaccia e Nette DI vi fornirà l'implementazione generata: ```php class UserController @@ -65,17 +65,17 @@ class UserController public function foo() { - // facciamo creare l'oggetto alla factory + // lascia che sia la factory a creare l'oggetto $article = $this->articleFactory->create(); } } ``` -Factory parametrizzata -====================== +Factory con parametri +===================== -Il metodo della factory `create` può accettare parametri, che poi passa al costruttore. Aggiungiamo ad esempio alla classe `Article` l'ID dell'autore dell'articolo: +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 @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -Grazie al fatto che il parametro nel costruttore e il parametro nella factory si chiamano allo stesso modo, Nette DI li passa in modo completamente automatico. +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 può essere scritta anche in forma multiriga utilizzando la chiave `implement`: +La definizione si può scrivere anche su più righe, con la chiave `implement`: ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -Scrivendo in questo modo più lungo, è possibile specificare argomenti aggiuntivi per il costruttore nella chiave `arguments` e configurazioni supplementari tramite `setup`, proprio come per i servizi normali. +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 specificare un valore fisso nella configurazione, che verrebbe passato al costruttore di `Article`: +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: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -O al contrario, se `create()` accettasse il parametro `$authorId`, ma non fosse parte del costruttore e venisse passato tramite il metodo `Article::setAuthorId()`, ci riferiremmo ad esso nella sezione `setup`: +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: @@ -137,11 +137,11 @@ services: Accessor ======== -Nette, oltre alle factory, può generare anche i cosiddetti accessor. Si tratta di oggetti con un metodo `get()`, che restituisce un determinato servizio dal container DI. Chiamate ripetute a `get()` restituiscono sempre la stessa istanza. +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 forniscono il lazy-loading alle dipendenze. Supponiamo di avere una classe che scrive errori in un database speciale. Se questa classe ricevesse la connessione al database come dipendenza tramite il costruttore, la connessione dovrebbe sempre essere creata, anche se in pratica un errore si verifica solo eccezionalmente e quindi la maggior parte delle volte la connessione rimarrebbe inutilizzata. Invece, la classe riceve un accessor e solo quando viene chiamato il suo `get()`, viene creato l'oggetto del database: +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 creare un accessor? Basta scrivere un'interfaccia e Nette DI genererà l'implementazione. L'interfaccia deve avere esattamente un metodo chiamato `get` e dichiarare il tipo di ritorno: +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 @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -Aggiungiamo l'accessor al file di configurazione, dove è definita anche la definizione del servizio che restituirà: +Aggiungete l'accessor al file di configurazione, insieme alla definizione del servizio che deve restituire: ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -Poiché l'accessor restituisce un servizio di tipo `PDO` e nella configurazione c'è un solo servizio di questo tipo, restituirà proprio quello. Se ci fossero più servizi di quel tipo, specificheremmo il servizio restituito tramite il nome, ad es. `- PDOAccessor(@db1)`. +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)`. -Factory/accessor multipli -========================= -Le nostre factory e accessor finora sapevano sempre produrre o restituire solo un oggetto. Ma è possibile creare molto facilmente anche factory multiple combinate con accessor. L'interfaccia di una tale classe conterrà un numero qualsiasi di metodi con nomi `create<name>()` e `get<name>()`, ad es.: +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 @@ -173,9 +174,9 @@ interface MultiFactory } ``` -Quindi, invece di passare diverse factory e accessor generati, passiamo una factory più complessa che sa fare di più. +Invece di iniettare più factory e accessor separati, potete quindi iniettare un unico componente più completo. -In alternativa, invece di più metodi, si può usare `get()` con un parametro: +In alternativa ai metodi multipli si può usare `get()` con un parametro: ```php interface MultiFactoryAlt @@ -184,22 +185,24 @@ interface MultiFactoryAlt } ``` -Allora vale che `MultiFactory::getArticle()` fa la stessa cosa di `MultiFactoryAlt::get('article')`. Tuttavia, la scrittura alternativa ha lo svantaggio che non è chiaro quali valori di `$name` siano supportati e logicamente non è possibile distinguere nell'interfaccia diversi valori di ritorno per diversi `$name`. +`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 tramite elenco --------------------------- -In questo modo è possibile definire una factory multipla nella configurazione: .{data-version:3.2.0} +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() + article: Article() # definisce createArticle() db: PDO(%dsn%, %user%, %password%) # definisce getDb() ) ``` -Oppure possiamo riferirci a servizi esistenti nella definizione della factory tramite riferimento: +In alternativa, nella definizione della multifactory potete fare riferimento a servizi esistenti tramite i riferimenti: ```neon services: @@ -212,15 +215,19 @@ services: ``` -Definizione tramite tag ------------------------ +Definizione con i tag +--------------------- -La seconda possibilità è utilizzare per la definizione i [tag |services#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: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index 794c8dfb48..928dca3dea 100644 --- a/dependency-injection/it/faq.texy +++ b/dependency-injection/it/faq.texy @@ -1,94 +1,94 @@ -Domande frequenti su DI (FAQ) -***************************** +Domande frequenti sulla DI (FAQ) +******************************** DI è un altro nome per IoC? --------------------------- -*Inversion of Control* (IoC) è un principio focalizzato sul modo in cui il codice viene eseguito - se il tuo codice esegue codice altrui o se il tuo codice è integrato in codice altrui, che poi lo chiama. IoC è un termine ampio che include [eventi |nette:glossary#Eventi], il cosiddetto [Principio di Hollywood |application:components#Stile Hollywood] e altri aspetti. Parte di questo concetto sono anche le factory, di cui parla la [Regola n. 3: lascialo alla factory |introduction#Regola n. 3: lascia fare alla factory], e che rappresentano un'inversione per l'operatore `new`. +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`. -*Dependency Injection* (DI) si concentra sul modo in cui un oggetto viene a conoscenza di un altro oggetto, cioè delle sue dipendenze. È un design pattern che richiede il passaggio esplicito delle dipendenze tra oggetti. +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. -Si può quindi dire che DI è una forma specifica di IoC. Tuttavia, non tutte le forme di IoC sono adatte dal punto di vista della pulizia del codice. Ad esempio, tra gli antipattern ci sono tecniche che lavorano con lo [stato globale |global-state] o il cosiddetto [Service Locator |#Cos è il Service Locator]. +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'è il Service Locator? +Cos'è un Service Locator? ------------------------- -È un'alternativa alla Dependency Injection. Funziona creando un repository centrale dove sono registrati tutti i servizi o le dipendenze disponibili. Quando un oggetto ha bisogno di una dipendenza, la richiede al 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 Dependency Injection, tuttavia, perde in trasparenza: le dipendenze non vengono passate direttamente agli oggetti e non sono quindi facilmente identificabili, il che richiede l'esame del codice per rivelare e comprendere tutti i legami. Anche il testing è più complesso, perché non possiamo semplicemente passare oggetti mock agli oggetti testati, ma dobbiamo passare attraverso il Service Locator. Inoltre, il Service Locator infrange il design del codice, poiché i singoli oggetti devono essere a conoscenza della sua esistenza, il che differisce dalla Dependency Injection, dove gli oggetti non sono consapevoli del container DI. +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 DI? ------------------------------ +Quando è meglio non usare la DI? +-------------------------------- -Non sono note difficoltà associate all'uso del design pattern Dependency Injection. Al contrario, ottenere dipendenze da luoghi globalmente accessibili porta a [tutta una serie di complicazioni |global-state], così come l'uso del Service Locator. Pertanto, è consigliabile utilizzare sempre DI. Questo non è un approccio dogmatico, ma semplicemente non è stata trovata un'alternativa migliore. +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. -Tuttavia, esistono alcune situazioni in cui non passiamo oggetti e li otteniamo dallo spazio globale. Ad esempio, durante il debugging del codice, quando è necessario stampare il valore di una variabile in un punto specifico del programma, misurare la durata di una certa parte del programma o registrare un messaggio. In tali casi, quando si tratta di operazioni temporanee che verranno successivamente rimosse dal codice, è legittimo utilizzare un dumper, un cronometro o un logger globalmente accessibili. Questi strumenti, infatti, non appartengono al design del codice. +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 di DI ha i suoi lati negativi? ------------------------------------- +L'uso della DI ha degli svantaggi? +---------------------------------- -L'uso della Dependency Injection comporta degli svantaggi, come ad esempio una maggiore complessità nella scrittura del codice o prestazioni peggiori? Cosa perdiamo quando iniziamo a scrivere codice in conformità con DI? +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? -DI non ha alcun impatto sulle prestazioni o sui requisiti di memoria dell'applicazione. Le prestazioni del DI Container possono giocare un certo ruolo, tuttavia, nel caso di [Nette DI |nette-container], il container viene compilato in PHP puro, quindi il suo overhead durante l'esecuzione dell'applicazione è essenzialmente nullo. +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. -Durante la scrittura del codice, è spesso necessario creare costruttori che accettano dipendenze. In passato questo poteva essere noioso, ma grazie agli IDE moderni e alla [constructor property promotion |https://blog.nette.org/it/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], ora è questione di pochi secondi. Le factory possono essere facilmente generate usando Nette DI e il plugin per PhpStorm con un clic del mouse. D'altra parte, scompare la necessità di scrivere singleton e punti di accesso statici. +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. -Si può affermare che un'applicazione progettata correttamente che utilizza DI non è né più corta né più lunga di un'applicazione che utilizza singleton. Le parti di codice che lavorano con le dipendenze vengono semplicemente estratte dalle singole classi e spostate in nuovi luoghi, cioè nel container DI e nelle factory. +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 riscrivere un'applicazione legacy in DI? ---------------------------------------------- +Come si riscrive con la DI un'applicazione datata? +-------------------------------------------------- -La transizione da un'applicazione legacy alla Dependency Injection può essere un processo impegnativo, soprattutto per applicazioni grandi e complesse. È importante approcciare questo processo in modo sistematico. +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. -- Durante la transizione alla Dependency Injection, è importante che tutti i membri del team comprendano i principi e le procedure utilizzate. -- Innanzitutto, esegui un'analisi dell'applicazione esistente e identifica i componenti chiave e le loro dipendenze. Crea un piano su quali parti verranno refattorizzate e in quale ordine. -- Implementa un container DI o, ancora meglio, utilizza una libreria esistente, ad esempio Nette DI. -- Refattorizza gradualmente le singole parti dell'applicazione per utilizzare la Dependency Injection. Ciò può includere la modifica di costruttori o metodi in modo che accettino le dipendenze come parametri. -- Modifica i punti nel codice in cui vengono creati oggetti con dipendenze, in modo che invece le dipendenze vengano iniettate dal container. Ciò può includere l'uso di factory. +- 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. -Ricorda che la transizione alla Dependency Injection è un investimento nella qualità del codice e nella manutenibilità a lungo termine dell'applicazione. Sebbene possa essere impegnativo apportare queste modifiche, il risultato dovrebbe essere un codice più pulito, modulare e facilmente testabile, pronto per future estensioni e manutenzione. +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à? ------------------------------------------------------ -È preferibile utilizzare la [composizione |nette:introduction-to-object-oriented-programming#Composizione] invece dell'[ereditarietà |nette:introduction-to-object-oriented-programming#Ereditarietà], perché serve a riutilizzare il codice senza doversi preoccupare delle conseguenze delle modifiche. Fornisce quindi un legame più lasco, in cui non dobbiamo preoccuparci che la modifica di un codice causi la necessità di modificare un altro codice dipendente. Un esempio tipico è la situazione nota come [constructor hell |passing-dependencies#Constructor hell]. +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]. -È possibile utilizzare Nette DI Container al di fuori di Nette? ---------------------------------------------------------------- +Nette DI Container si può usare fuori da Nette? +----------------------------------------------- -Assolutamente. Nette DI Container fa parte di Nette, ma è progettato come una libreria autonoma che può essere utilizzata indipendentemente dalle altre parti del framework. Basta installarla tramite Composer, creare un file di configurazione con la definizione dei tuoi servizi e quindi, con poche righe di codice PHP, creare il container DI. E puoi iniziare subito a sfruttare i vantaggi della Dependency Injection nei tuoi progetti. +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. -Come appare l'uso concreto, compresi i codici, è descritto nel capitolo [Nette DI Container |nette-container]. +Il capitolo su [Nette DI Container |nette-container] descrive un caso d'uso concreto con esempi di codice. -Perché la configurazione è nei file NEON? ------------------------------------------ +Perché la configurazione è in file NEON? +---------------------------------------- -NEON è un linguaggio di configurazione semplice e facilmente leggibile, sviluppato nell'ambito di Nette per impostare applicazioni, servizi e le loro dipendenze. Rispetto a JSON o YAML, offre opzioni molto più intuitive e flessibili per questo scopo. In NEON è possibile descrivere naturalmente legami che in Symfony & YAML non sarebbe possibile scrivere affatto, o solo tramite una descrizione complessa. +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. -Il parsing dei file NEON non rallenta l'applicazione? ------------------------------------------------------ +L'analisi dei file NEON rallenta l'applicazione? +------------------------------------------------ -Sebbene i file NEON vengano parsati molto rapidamente, questo aspetto non ha alcuna importanza. Il motivo è che il parsing dei file avviene solo una volta al primo avvio dell'applicazione. Successivamente, viene generato il codice del container DI, salvato su disco ed eseguito ad ogni richiesta successiva, senza la necessità di eseguire ulteriori parsing. +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 ambiente di produzione. Durante lo sviluppo, i file NEON vengono parsati ogni volta che il loro contenuto viene modificato, in modo che lo sviluppatore abbia sempre un container DI aggiornato. Il parsing stesso è, come detto, questione di un attimo. +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 ai parametri nel file di configurazione dalla mia classe? +Come accedo nella mia classe ai parametri del file di configurazione? --------------------------------------------------------------------- -Teniamo presente la [Regola n. 1: fattelo passare |introduction#Regola n. 1: fatti passare le dipendenze]. Se una classe richiede informazioni dal file di configurazione, non dobbiamo pensare a come ottenere quelle informazioni, ma semplicemente le chiediamo - ad esempio tramite il costruttore della classe. E il passaggio lo effettuiamo nel 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 viene passato al costruttore della classe `MyClass`: +In questo esempio `%myParameter%` è un segnaposto per il valore del parametro `myParameter`, che verrà passato al costruttore di `MyClass`: -```php +```neon # config.neon parameters: myParameter: Some value @@ -97,10 +97,27 @@ services: - MyClass(%myParameter%) ``` -Se vuoi passare più parametri o utilizzare l'autowiring, è consigliabile [impacchettare i parametri in un oggetto |best-practices:passing-settings-to-presenters]. +Se volete passare più parametri o usare l'autowiring, conviene [racchiudere i parametri in un oggetto |best-practices:passing-settings-to-presenters]. -Nette supporta PSR-11: Container interface? -------------------------------------------- +Nette supporta l'interfaccia Container di PSR-11? +------------------------------------------------- -Nette DI Container non supporta direttamente PSR-11. Tuttavia, se hai bisogno di interoperabilità tra Nette DI Container e librerie o framework che si aspettano PSR-11 Container Interface, puoi creare un [semplice adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], che fungerà da ponte tra Nette DI Container e 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 index 49cda6dd0c..3faf2c3817 100644 --- a/dependency-injection/it/global-state.texy +++ b/dependency-injection/it/global-state.texy @@ -2,62 +2,62 @@ Stato globale e singleton ************************* .[perex] -Avviso: I seguenti costrutti sono un segno di codice mal progettato: +Attenzione: i costrutti seguenti sono sintomi di codice progettato male: - `Foo::getInstance()` - `DB::insert(...)` - `Article::setDb($db)` -- `ClassName::$var` o `static::$var` +- `ClassName::$var` oppure `static::$var` -Alcuni di questi costrutti compaiono nel tuo codice? Allora hai l'opportunità di migliorarlo. Forse pensi che si tratti di costrutti comuni, che vedi anche in soluzioni di esempio di varie librerie e framework. Se è così, allora il design del loro codice non è buono. +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. -Ora non stiamo certo parlando di una sorta di purezza accademica. Tutti questi costrutti hanno una cosa in comune: utilizzano lo stato globale. E questo ha un impatto distruttivo sulla qualità del codice. Le classi mentono sulle loro dipendenze. Il codice diventa imprevedibile. Confonde i programmatori e riduce la loro efficienza. +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. -Accoppiamento globale ---------------------- +Intreccio globale +----------------- -In un mondo ideale, un oggetto dovrebbe essere in grado di comunicare solo con 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 all'altro oggetto o modificarne lo stato. Questa è una proprietà molto desiderabile del codice. È simile a quando hai una batteria e una lampadina; la lampadina non si accenderà finché non la colleghi alla batteria con un filo. +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 i singleton. L'oggetto `A` potrebbe accedere *senza fili* all'oggetto `C` e modificarlo senza alcun passaggio di riferimento, chiamando `C::changeSomething()`. Se anche l'oggetto `B` si appropria del `C` globale, allora `A` e `B` possono influenzarsi a vicenda tramite `C`. +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 di variabili globali introduce nel sistema una nuova forma di accoppiamento *senza fili*, che non è visibile dall'esterno. Crea una cortina fumogena che complica la comprensione e l'uso del codice. Affinché gli sviluppatori comprendano veramente le dipendenze, devono leggere ogni riga del codice sorgente. Invece di familiarizzare semplicemente con le interfacce delle classi. Si tratta inoltre di un accoppiamento del tutto inutile. Lo stato globale viene utilizzato perché è facilmente accessibile da qualsiasi luogo e consente, ad esempio, di scrivere nel database tramite il metodo globale (statico) `DB::insert()`. Ma come mostreremo, il vantaggio che ciò porta è minimo, mentre le complicazioni che causa sono fatali. +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. +Dal punto di vista del comportamento non c'è differenza tra una variabile globale e una statica. Sono ugualmente dannose. -Azione spettrale a distanza ---------------------------- +L'inquietante azione a distanza +------------------------------- -"Azione spettrale a distanza" - così famosamente chiamò nel 1935 Albert Einstein un fenomeno della fisica quantistica che gli faceva venire la pelle d'oca. -Si tratta dell'entanglement quantistico, la cui particolarità è che quando misuri l'informazione su una particella, influenzi istantaneamente l'altra particella, anche se sono distanti milioni di anni luce. Ciò sembra violare la legge fondamentale dell'universo, secondo cui nulla può propagarsi più velocemente della luce. +"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, possiamo chiamare "azione spettrale a distanza" una situazione in cui avviamo un processo che riteniamo isolato (perché non gli abbiamo passato alcun riferimento), ma in luoghi remoti del sistema si verificano interazioni e cambiamenti di stato imprevisti, di cui non avevamo idea. Ciò può accadere solo tramite lo stato globale. +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. -Immagina di unirti a un team di sviluppatori di un progetto che ha una vasta e matura base di codice. Il tuo nuovo capo ti chiede di implementare una nuova funzionalità e tu, da bravo sviluppatore, inizi scrivendo un test. Ma poiché sei nuovo nel progetto, fai molti test esplorativi del tipo "cosa succede se chiamo questo metodo". E provi a scrivere il seguente test: +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 tua carta + $cc = new CreditCard('1234567890123456', 5, 2028); // il numero della vostra carta $cc->charge(100); } ``` -Esegui il codice, magari più volte, e dopo un po' noti notifiche dalla banca sul cellulare, che ad ogni esecuzione sono stati addebitati 100 dollari dalla tua carta di pagamento 🤦‍♂️ +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 fatto il test a causare un addebito reale di denaro? Operare con una carta di pagamento non è facile. Devi comunicare con un servizio web di terze parti, devi conoscere l'URL di questo servizio web, devi autenticarti e così via. Nessuna di queste informazioni è contenuta nel test. Peggio ancora, non sai nemmeno dove queste informazioni siano presenti, e quindi nemmeno come mockare le dipendenze esterne, in modo che ogni esecuzione non porti a un nuovo addebito di 100 dollari. E come avresti dovuto sapere, come nuovo sviluppatore, che quello che stavi per fare ti avrebbe reso più povero di 100 dollari? +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 è l'azione spettrale a distanza! +Questa è un'inquietante azione a distanza! -Non ti resta che scavare a lungo in un sacco di codice sorgente, chiedere ai colleghi più anziani ed esperti, prima di capire come funzionano i legami nel progetto. Ciò è dovuto al fatto che guardando l'interfaccia della classe `CreditCard` non è possibile determinare lo stato globale che deve essere inizializzato. Nemmeno uno sguardo al codice sorgente della classe ti dirà quale metodo di inizializzazione devi chiamare. Nel migliore dei casi, puoi trovare una variabile globale a cui si accede e da essa cercare di indovinare come inizializzarla. +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 in un tale progetto sono bugiarde patologiche. La carta di pagamento finge che basti istanziarla e chiamare il metodo `charge()`. Di nascosto, però, collabora con un'altra classe `PaymentGateway`, che rappresenta il gateway di pagamento. Anche la sua interfaccia dice che può essere inizializzata separatamente, ma in realtà estrae le credenziali da qualche file di configurazione e così via. Agli sviluppatori che hanno scritto questo codice è chiaro che `CreditCard` ha bisogno di `PaymentGateway`. Hanno scritto il codice in questo modo. Ma per chiunque sia nuovo nel progetto, è un mistero assoluto e ostacola l'apprendimento. +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 risolvere la situazione? Facilmente. **Lascia che l'API dichiari le dipendenze.** +Come sistemare la situazione? Facile. **Fate in modo che l'API dichiari le dipendenze.** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -Nota come improvvisamente le interconnessioni all'interno del codice diventano evidenti. Poiché il metodo `charge()` dichiara di aver bisogno di `PaymentGateway`, non devi chiedere a nessuno come è interconnesso il codice. Sai che devi crearne un'istanza e, quando ci provi, ti imbatti nel fatto che devi fornire i parametri di accesso. Senza di essi, il codice non potrebbe nemmeno essere eseguito. +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 puoi mockare il gateway di pagamento, così non ti verranno addebitati 100 dollari ogni volta che esegui il test. +E soprattutto, ora potete simulare il gateway di pagamento, così non vi verranno addebitati 100 dollari a ogni esecuzione del test. -Lo stato globale fa sì che i tuoi oggetti possano accedere segretamente a cose che non sono dichiarate nella loro API e, di conseguenza, rende le tue API bugiarde patologiche. +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 ci avevi pensato in questo modo prima, ma ogni volta che usi lo stato globale, stai creando canali di comunicazione segreti senza fili. L'azione spettrale a distanza costringe gli sviluppatori a leggere ogni riga di codice per comprendere le potenziali interazioni, riduce la produttività degli sviluppatori e confonde i nuovi membri del team. Se sei tu quello che ha creato il codice, conosci le vere dipendenze, ma chiunque venga dopo di te è perso. +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. -Non scrivere codice che utilizza lo stato globale, dai la preferenza al passaggio delle dipendenze. Cioè, dependency injection. +Evitate di scrivere codice che si affida allo stato globale; preferite passare le dipendenze esplicitamente. Abbracciate la dependency injection. -Fragilità dello stato globale ------------------------------ +La fragilità dello stato globale +-------------------------------- -Nel codice che utilizza lo stato globale e i singleton, non è mai certo quando e chi ha modificato questo stato. Questo rischio si presenta già durante l'inizializzazione. Il seguente codice dovrebbe creare una connessione al database e inizializzare il gateway di pagamento, ma lancia costantemente un'eccezione e trovare la causa è estremamente lungo: +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'); ``` -Devi esaminare attentamente il codice per scoprire che l'oggetto `PaymentGateway` accede senza fili ad altri oggetti, alcuni dei quali richiedono una connessione al database. Quindi è necessario inizializzare il database prima di `PaymentGateway`. Tuttavia, la cortina fumogena dello stato globale ti nasconde questo. Quanto tempo avresti risparmiato se le API delle singole classi non avessero mentito e avessero dichiarato le loro dipendenze? +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, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -Un problema simile si presenta anche quando si utilizza l'accesso globale alla connessione del database: +Un problema simile nasce usando l'accesso globale a una connessione al database: ```php use Illuminate\Support\Facades\DB; @@ -110,9 +110,9 @@ class Article } ``` -Quando si chiama il metodo `save()`, non è certo se sia già stata creata una connessione al database e chi sia responsabile della sua creazione. Se volessimo, ad esempio, cambiare la connessione al database durante l'esecuzione, magari per i test, dovremmo probabilmente creare altri metodi come `DB::reconnect(...)` o `DB::reconnectForTest()`. +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()`. -Consideriamo un esempio: +Considerate un esempio: ```php $article = new Article; @@ -122,9 +122,9 @@ Foo::doSomething(); $article->save(); ``` -Dove abbiamo la certezza che quando si chiama `$article->save()` si stia effettivamente utilizzando il database di test? E se il metodo `Foo::doSomething()` avesse cambiato la connessione globale al database? Per scoprirlo, dovremmo esaminare il codice sorgente della classe `Foo` e probabilmente anche di molte altre classi. Questo approccio, tuttavia, fornirebbe solo una risposta a breve termine, poiché la situazione potrebbe cambiare in futuro. +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 all'interno della classe `Article`? +E se spostassimo la connessione al database in una variabile statica dentro la classe `Article`? ```php class Article @@ -143,11 +143,11 @@ class Article } ``` -Questo non cambia assolutamente nulla. Il problema è lo stato globale ed è del tutto indifferente in quale classe si nasconda. In questo caso, come nel precedente, non abbiamo alcun indizio, quando chiamiamo il metodo `$article->save()`, su quale database verrà scritto. Chiunque all'altro capo dell'applicazione avrebbe potuto cambiare il database in qualsiasi momento usando `Article::setDb()`. Sotto il nostro naso. +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 tuttavia un modo semplice per affrontare questo problema. Basta lasciare che l'API dichiari le dipendenze, garantendo così la corretta funzionalità. +Esiste però un modo semplice di affrontare questo problema. Basta far dichiarare all'API le dipendenze necessarie al corretto funzionamento. ```php class Article @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -Grazie a questo approccio, scompare la preoccupazione per modifiche nascoste e impreviste della connessione al database. Ora abbiamo la certezza di dove viene salvato l'articolo e nessuna modifica del codice all'interno di un'altra classe non correlata può più cambiare la situazione. Il codice non è più fragile, ma stabile. +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. -Non scrivere codice che utilizza lo stato globale, dai la preferenza al passaggio delle dipendenze. Cioè, dependency injection. +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, secondo la "definizione":https://en.wikipedia.org/wiki/Singleton_pattern della nota pubblicazione Gang of Four, limita una classe a una singola istanza e offre un accesso globale ad essa. L'implementazione di questo pattern di solito assomiglia al seguente codice: +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 @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // e altri metodi che svolgono le funzioni della classe data + // e altri metodi che svolgono le funzioni della classe } ``` -Purtroppo, il singleton introduce uno stato globale nell'applicazione. E come abbiamo mostrato sopra, lo stato globale è indesiderabile. Pertanto, il singleton è considerato un antipattern. +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 utilizzare singleton nel tuo codice e sostituiscili con altri meccanismi. I singleton non ti servono davvero. Tuttavia, se hai bisogno di garantire l'esistenza di una singola istanza di una classe per l'intera applicazione, lascialo fare al [container DI |container]. Crea così un singleton applicativo, ovvero un servizio. In questo modo, la classe smette di occuparsi di garantire la propria unicità (cioè non avrà il metodo `getInstance()` e una variabile statica) e svolgerà solo le sue funzioni. Così smetterà di violare il principio di singola responsabilità. +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 versus test -------------------------- +Stato globale e test +-------------------- -Quando scriviamo test, presumiamo che ogni test sia un'unità isolata e che nessuno stato esterno vi entri. E nessuno stato lascia i test. Al termine del test, tutto lo stato correlato al test dovrebbe essere rimosso automaticamente dal garbage collector. Grazie a ciò, i test sono isolati. Pertanto, possiamo eseguire i test in qualsiasi ordine. +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. -Tuttavia, se sono presenti stati globali/singleton, tutte queste piacevoli supposizioni crollano. Lo stato può entrare e uscire dal test. Improvvisamente, l'ordine dei test può avere importanza. +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 poter testare affatto i singleton, gli sviluppatori spesso devono allentare le loro proprietà, ad esempio permettendo di sostituire l'istanza con un'altra. Tali soluzioni sono nel migliore dei casi un hack, che crea codice difficile da mantenere e comprendere. Ogni test o metodo `tearDown()`, che influenzi qualsiasi stato globale, deve annullare queste modifiche. +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 mal di testa nel unit testing! +Lo stato globale è il più grande grattacapo dei test unitari! -Come risolvere la situazione? Facilmente. Non scrivere codice che utilizza singleton, dai la preferenza al passaggio delle dipendenze. Cioè, dependency injection. +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 solo all'uso di singleton e variabili statiche, ma può riguardare anche 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 non ci porta alcuna nuova (`M_PI`) o utile (`PREG_BACKTRACK_LIMIT_ERROR`) informazione, sono chiaramente a posto. Al contrario, le costanti che servono come modo per passare informazioni *senza fili* all'interno del codice, non sono altro che una dipendenza nascosta. Come ad esempio `LOG_FILE` nell'esempio seguente. L'uso della costante `FILE_APPEND` è del tutto corretto. +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 = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -In questo caso, dovremmo dichiarare un parametro nel costruttore della classe `Foo`, affinché diventi parte dell'API: +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 @@ -253,42 +253,42 @@ class Foo } ``` -Ora possiamo passare l'informazione sul percorso del file per il logging e modificarla facilmente secondo necessità, il che facilita il testing e la manutenzione del codice. +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 stesso di metodi statici e funzioni globali non è problematico. Abbiamo spiegato perché l'uso di `DB::insert()` e metodi simili è inappropriato, ma si è sempre trattato solo di una questione di stato globale, che è memorizzato in qualche variabile statica. Il metodo `DB::insert()` richiede l'esistenza di una variabile statica, perché in essa è memorizzata la connessione al database. Senza questa variabile, sarebbe impossibile implementare il metodo. +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. -L'uso di metodi statici e funzioni deterministiche, come ad esempio `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` e molte altre, è in perfetta armonia con la dependency injection. Queste funzioni restituiscono sempre gli stessi risultati dagli stessi parametri di input e sono quindi prevedibili. Non utilizzano alcuno stato globale. +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. -Esistono tuttavia anche funzioni in PHP che non sono deterministiche. Tra queste c'è ad esempio la funzione `htmlspecialchars()`. Il suo terzo parametro `$encoding`, se non specificato, ha come valore predefinito il valore dell'opzione di configurazione `ini_get('default_charset')`. Pertanto, si consiglia di specificare sempre questo parametro per evitare un eventuale comportamento imprevedibile della funzione. Nette lo fa costantemente. +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 ad esempio `strtolower()`, `strtoupper()` e simili, in un passato recente si comportavano in modo non deterministico ed erano dipendenti dall'impostazione `setlocale()`. Ciò causava molte complicazioni, più spesso quando si lavorava con la lingua turca. Questa, infatti, distingue sia la lettera minuscola che maiuscola `I` con e senza punto. Quindi `strtolower('I')` restituiva il carattere `ı` e `strtoupper('i')` il carattere `İ`, il che portava le applicazioni a causare una serie di errori misteriosi. Questo problema è stato tuttavia risolto nella versione PHP 8.2 e le funzioni non dipendono più dalla locale. +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 bell'esempio di come lo stato globale abbia tormentato migliaia di sviluppatori in tutto il mondo. La soluzione è stata sostituirlo con la dependency injection. +È 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 utilizzare lo stato globale? ------------------------------------------------ +Quando è possibile usare lo stato globale? +------------------------------------------ -Esistono alcune situazioni specifiche in cui è possibile utilizzare lo stato globale. Ad esempio, durante il debugging del codice, quando è necessario stampare il valore di una variabile o misurare la durata di una certa parte del programma. In tali casi, che riguardano azioni temporanee che verranno successivamente rimosse dal codice, è possibile utilizzare legittimamente un dumper o un cronometro globalmente accessibili. Questi strumenti, infatti, non fanno parte del design del codice. +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 sono le funzioni per lavorare con le espressioni regolari `preg_*`, che internamente memorizzano le espressioni regolari compilate in una cache statica in memoria. Quindi, quando chiami la stessa espressione regolare più volte in punti diversi del codice, viene compilata solo una volta. La cache risparmia prestazioni ed è allo stesso tempo completamente invisibile per l'utente, quindi tale utilizzo può essere considerato legittimo. +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 discusso perché ha senso: +Abbiamo parlato del perché abbia senso: -1) Rimuovere tutte le variabili statiche dal codice -2) Dichiarare le dipendenze -3) E usare la dependency injection +1) eliminare dal vostro codice tutte le proprietà statiche modificabili (lo stato globale) +2) dichiarare esplicitamente le dipendenze +3) e usare la dependency injection -Quando pensi al design del codice, tieni presente che ogni `static $foo` rappresenta un problema. Affinché il tuo codice sia un ambiente che rispetta DI, è indispensabile sradicare completamente lo stato globale e sostituirlo con 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, potresti scoprire che è necessario dividere la classe, perché ha più di una responsabilità. Non averne paura; cerca di rispettare il principio di singola responsabilità. +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 index b66a1e8e13..fa5ea49a89 100644 --- a/dependency-injection/it/introduction.texy +++ b/dependency-injection/it/introduction.texy @@ -1,58 +1,58 @@ -Cos'è la Dependency Injection? -****************************** +Che cos'è la dependency injection? +********************************** .[perex] -Questo capitolo vi introdurrà alle pratiche di programmazione di base che dovreste seguire quando scrivete qualsiasi applicazione. Si tratta delle basi necessarie per scrivere codice pulito, comprensibile e manutenibile. +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 queste regole e le seguirete, Nette vi supporterà in ogni passo. Si occuperà dei compiti di routine per voi e vi fornirà la massima comodità, così potrete concentrarvi sulla logica stessa. +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 vi mostreremo qui sono piuttosto semplici. Non c'è nulla di cui preoccuparsi. +I principi che mostreremo qui sono piuttosto semplici. Non c'è nulla da temere. -Ricordi il tuo primo programma? -------------------------------- +Vi ricordate il vostro primo programma? +--------------------------------------- -Non sappiamo in quale linguaggio lo hai scritto, ma se fosse stato PHP, probabilmente sarebbe stato simile a questo: +Non sappiamo in quale linguaggio l'avete scritto, ma se fosse stato PHP avrebbe probabilmente avuto più o meno questo aspetto: ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // stampa 24 +echo addition(23, 1); // stampa 24 ``` -Poche righe di codice banali, ma contengono così tanti concetti chiave. Che esistono le variabili. Che il codice è diviso in unità più piccole, come le funzioni. Che passiamo loro argomenti di input e restituiscono risultati. Mancano solo le condizioni e i cicli. +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 di input a una funzione e questa restituisca un risultato è un concetto perfettamente comprensibile, utilizzato anche in altri campi, come la matematica. +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 sua firma, che consiste nel suo nome, un elenco di parametri e i loro tipi, e infine il tipo di valore di ritorno. Come utenti, ci interessa la firma; di solito non abbiamo bisogno di sapere nulla dell'implementazione interna. +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. -Ora immagina che la firma della funzione fosse così: +Immaginate ora che la firma della funzione fosse questa: ```php -function soucet(float $x): float +function addition(float $x): float ``` -Una somma con un solo parametro? Strano... E che ne dici di questo? +Una somma con un solo parametro? Strano... E questa? ```php -function soucet(): float +function addition(): float ``` -Questo è davvero molto strano, vero? Come si usa la funzione? +Ora è davvero strano, no? Come si usa la funzione? ```php -echo soucet(); // cosa stamperà? +echo addition(); // cosa stamperà? ``` -Guardando un codice del genere, saremmo confusi. Non solo un principiante non lo capirebbe, ma nemmeno un programmatore esperto capirebbe un codice del genere. +Guardando un codice del genere resteremmo perplessi. Non solo un principiante non lo capirebbe: nemmeno un programmatore esperto capirebbe un codice così. -Ti stai chiedendo come sarebbe effettivamente una funzione del genere all'interno? Dove prenderebbe gli addendi? Probabilmente se li procurerebbe *in qualche modo* da sola, forse 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 soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -Nel corpo della funzione, abbiamo scoperto dipendenze nascoste verso altre funzioni globali o metodi statici. Per scoprire da dove provengono effettivamente gli addendi, dobbiamo indagare ulteriormente. +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 da questa parte! --------------------- +Non così! +--------- -Il design che abbiamo appena mostrato è l'essenza di molte caratteristiche negative: +Il progetto che abbiamo appena mostrato è l'essenza di molte caratteristiche negative: -- la firma della funzione fingeva di non aver bisogno di addendi, il che ci confondeva -- non sappiamo affatto come far sommare alla funzione altri due numeri -- abbiamo dovuto guardare nel codice per scoprire dove prendeva gli addendi -- abbiamo scoperto dipendenze nascoste -- per una comprensione completa, è necessario esaminare anche queste dipendenze +- 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 è compito della funzione di somma procurarsi gli input? Ovviamente no. La sua responsabilità è solo la somma stessa. +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 certamente non vogliamo scriverlo. La correzione è semplice: tornare alle basi e usare semplicemente i parametri: +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 soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -Regola n. 1: fatti passare le dipendenze ----------------------------------------- +Regola n. 1: fatevelo passare +----------------------------- -La regola più importante è: **tutti i dati di cui le funzioni o le classi hanno bisogno devono essere passati loro**. +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 attraverso i quali potrebbero ottenerli da soli, passa semplicemente i parametri. Risparmierai tempo necessario per inventare percorsi nascosti, che sicuramente non miglioreranno il tuo codice. +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 seguirai sempre e ovunque questa regola, sarai sulla strada per un codice senza dipendenze nascoste. Verso un codice comprensibile non solo per l'autore, ma anche per chiunque lo leggerà dopo di lui. Dove tutto è comprensibile dalle firme delle funzioni e delle classi e non c'è bisogno di cercare segreti nascosti nell'implementazione. +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 è tecnicamente chiamata **dependency injection**. E questi dati sono chiamati **dipendenze.** In realtà, si tratta semplicemente di passare parametri, niente di più. +Questa tecnica si chiama tecnicamente **dependency injection**. E i dati si chiamano **dipendenze**. È solo un semplice passaggio di parametri, nulla di più. .[note] -Per favore, non confondete la dependency injection, che è un design pattern, con il "dependency injection container", che è invece uno strumento, cioè qualcosa di diametralmente diverso. Ci occuperemo dei container più avanti. +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 relaziona questo con le classi? Una classe è un'unità più complessa di una semplice funzione, ma la regola n. 1 si applica pienamente anche qui. Ci sono solo [più opzioni per passare gli argomenti |passing-dependencies]. Ad esempio, in modo abbastanza simile al caso di una funzione: +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 Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` -Oppure usando altri metodi, o direttamente il costruttore: +Oppure con altri metodi, o direttamente con il costruttore: ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,26 +132,25 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` Entrambi gli esempi sono pienamente conformi alla dependency injection. -Esempi reali ------------- +Esempi dalla vita reale +----------------------- -Nel mondo reale, non scriverai classi per sommare numeri. Passiamo a esempi pratici. +Nel mondo reale non scriverete classi per sommare numeri. Passiamo a esempi pratici. -Abbiamo una classe `Article` che rappresenta un articolo di blog: +Prendiamo una classe `Article`, che rappresenta un articolo di blog: ```php class Article @@ -162,12 +161,12 @@ class Article public function save(): void { - // salviamo l'articolo nel database + // salva l'articolo nel database } } ``` -e l'utilizzo sarà il seguente: +e l'uso sarà questo: ```php $article = new Article; @@ -176,9 +175,9 @@ $article->content = 'Every year millions of people in ...'; $article->save(); ``` -Il metodo `save()` salva l'articolo in una tabella del database. Implementarlo usando [Nette Database |database:] sarebbe un gioco da ragazzi, se non fosse per un intoppo: dove prende `Article` la connessione al database, cioè l'oggetto della classe `Nette\Database\Connection`? +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 abbiamo molte opzioni. Può prenderla da qualche variabile statica. O ereditare da una classe che fornisce la connessione al database. O utilizzare il cosiddetto [singleton |global-state#Singleton]. O le cosiddette facades, che vengono utilizzate in Laravel: +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; @@ -199,17 +198,17 @@ class Article } ``` -Fantastico, abbiamo risolto il problema. +Ottimo, abbiamo risolto il problema. -O no? +Oppure no? -Ricordiamo la [##Regola n. 1: fatti passare le dipendenze]: tutte le dipendenze di cui la classe ha bisogno devono essere passate ad essa. Perché se violiamo la regola, abbiamo intrapreso la strada verso un codice sporco pieno di dipendenze nascoste, incomprensibilità, e il risultato sarà un'applicazione che sarà doloroso mantenere e sviluppare. +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. -L'utente della classe `Article` non ha idea di dove il metodo `save()` salvi l'articolo. In una tabella del database? In quale, quella di produzione o di test? E come si può cambiare? +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? -L'utente deve guardare come è implementato il metodo `save()` e trova l'uso del metodo `DB::insert()`. Quindi deve indagare ulteriormente su come questo metodo ottiene la connessione al database. E le dipendenze nascoste possono formare una catena piuttosto lunga. +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. -Nel codice pulito e ben progettato, non ci sono mai dipendenze nascoste, facades di Laravel o variabili statiche. Nel codice pulito e ben progettato, si passano argomenti: +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 @@ -224,7 +223,7 @@ class Article } ``` -Ancora più pratico, come vedremo più avanti, sarà tramite il costruttore: +Ancora più pratico, come vedremo più avanti, è usare il costruttore: ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -Se sei un programmatore esperto, potresti pensare che `Article` non dovrebbe affatto avere un metodo `save()`, dovrebbe rappresentare puramente un componente dati e il salvataggio dovrebbe essere gestito da un repository separato. Questo ha senso. Ma ci porterebbe molto lontano dall'argomento, che è la dependency injection, e dallo sforzo di fornire esempi semplici. +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 scrivi una classe che richiede, ad esempio, un database per funzionare, non inventare da dove ottenerlo, ma fattelo passare. Ad esempio, come parametro del costruttore o di un altro metodo. Riconosci le dipendenze. Riconoscile nell'API della tua classe. Otterrai un codice comprensibile e prevedibile. +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 che ne dici di questa classe, che registra i messaggi di errore: +E questa classe, che registra i messaggi di errore? ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,23 +261,23 @@ class Logger } ``` -Cosa ne pensi, abbiamo rispettato la [##Regola n. 1: fatti passare le dipendenze]? +Cosa ne dite, abbiamo seguito la [Regola n. 1: fatevelo passare |#Regola n. 1: fatevelo passare]? -Non l'abbiamo rispettata. +No. -L'informazione chiave, cioè la directory con il file di log, la classe se la *procura da sola* da una costante. +L'informazione chiave, la directory che contiene il file di log, *se la procura la classe stessa* da una costante. -Guarda l'esempio di utilizzo: +Guardate l'esempio d'uso: ```php $logger = new Logger; -$logger->log('La temperatura è 23 °C'); -$logger->log('La temperatura è 10 °C'); +$logger->log('Temperature is 23 °C'); +$logger->log('Temperature is 10 °C'); ``` -Senza conoscere l'implementazione, saresti in grado di rispondere alla domanda su dove vengono scritti i messaggi? Ti verrebbe in mente che per funzionare è necessaria l'esistenza della costante `LOG_DIR`? E saresti in grado di creare una seconda istanza che scriva altrove? Certamente no. +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. -Correggiamo la classe: +Sistemiamo la classe: ```php class Logger @@ -298,21 +297,21 @@ class Logger La classe è ora molto più comprensibile, configurabile e quindi più utile. ```php -$logger = new Logger('/percorso/al/log.txt'); -$logger->log('La temperatura è 15 °C'); +$logger = new Logger('/path/to/log.txt'); +$logger->log('Temperature is 15 °C'); ``` -Ma questo non mi interessa! ---------------------------- +Ma a me non interessa! +---------------------- -*„Quando creo un oggetto Article e chiamo save(), non voglio occuparmi del database, voglio semplicemente che venga salvato in quello che ho impostato nella configurazione.“* +*"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 semplicemente che il messaggio venga scritto, e non voglio preoccuparmi di dove. Che venga utilizzata l'impostazione globale.“* +*"Quando uso Logger, voglio solo che il messaggio venga scritto e non voglio occuparmi di dove. Che vengano usate le impostazioni globali."* -Queste sono osservazioni corrette. +Sono obiezioni legittime. -Come esempio, mostreremo una classe che invia newsletter e registra come è andata: +Come esempio, mostriamo una classe che distribuisce newsletter e registra l'esito: ```php class NewsletterDistributor @@ -322,21 +321,21 @@ class NewsletterDistributor $logger = new Logger(/* ... */); try { $this->sendEmails(); - $logger->log('Le email sono state inviate'); + $logger->log('Emails have been sent out'); } catch (Exception $e) { - $logger->log('Si è verificato un errore durante l\'invio'); + $logger->log('An error occurred during sending'); throw $e; } } } ``` -Il `Logger` migliorato, che non utilizza più la costante `LOG_DIR`, richiede nel costruttore di specificare il percorso del file. Come risolvere questo problema? La classe `NewsletterDistributor` non si preoccupa affatto di dove vengono scritti i messaggi, vuole solo scriverli. +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: fatti passare le dipendenze]: tutti i dati di cui la classe ha bisogno, glieli passiamo. +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. -Quindi significa che passiamo il percorso del log tramite il costruttore, che poi usiamo quando creiamo l'oggetto `Logger`? +Significa quindi che passiamo il percorso del log attraverso il costruttore, per usarlo poi nella creazione dell'oggetto `Logger`? ```php class NewsletterDistributor @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -Non così! Il percorso infatti **non appartiene** ai dati di cui la classe `NewsletterDistributor` ha bisogno; questi li necessita `Logger`. Percepisci la differenza? La classe `NewsletterDistributor` ha bisogno del logger come tale. Quindi glielo passiamo: +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 @@ -365,33 +364,33 @@ class NewsletterDistributor { try { $this->sendEmails(); - $this->logger->log('Le email sono state inviate'); + $this->logger->log('Emails have been sent out'); } catch (Exception $e) { - $this->logger->log('Si è verificato un errore durante l\'invio'); + $this->logger->log('An error occurred during sending'); throw $e; } } } ``` -Ora è chiaro dalle firme della classe `NewsletterDistributor` che parte della sua funzionalità è anche il logging. E il compito di sostituire il logger con un altro, ad esempio per i test, è del tutto banale. Inoltre, se il costruttore della classe `Logger` dovesse cambiare, ciò non avrebbe alcun impatto sulla nostra classe. +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: prendi ciò che è tuo ---------------------------------- +Regola n. 2: prendete ciò che è vostro +-------------------------------------- -Non lasciarti confondere e non farti passare le dipendenze delle tue dipendenze. Fatti passare solo le tue dipendenze. +Non fatevi confondere e non accettate le dipendenze delle vostre dipendenze. Accettate solo le vostre. -Grazie a ciò, il codice che utilizza altri oggetti sarà completamente indipendente dalle modifiche ai loro costruttori. La sua API sarà più veritiera. E soprattutto, sarà banale sostituire queste dipendenze con altre. +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. -Nuovo membro della famiglia ---------------------------- +Un nuovo membro della famiglia +------------------------------ -Nel team di sviluppo è stata presa la decisione di creare un secondo logger, che scrive nel database. Creeremo quindi la classe `DatabaseLogger`. Quindi abbiamo due classi, `Logger` e `DatabaseLogger`, una scrive su file, l'altra nel database... non ti sembra ci sia qualcosa di strano in questa denominazione? Non sarebbe meglio rinominare `Logger` in `FileLogger`? Certamente sì. +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 lo faremo in modo intelligente. Sotto il nome originale, creeremo un'interfaccia: +Ma facciamolo con astuzia. Creiamo un'interfaccia con il nome originale: ```php interface Logger @@ -400,7 +399,7 @@ interface Logger } ``` -... che entrambi i logger implementeranno: +… che entrambi i logger implementeranno: ```php class FileLogger implements Logger @@ -410,17 +409,17 @@ class DatabaseLogger implements Logger // ... ``` -E grazie a ciò, non sarà necessario cambiare nulla nel resto del codice dove viene utilizzato il logger. Ad esempio, il costruttore della classe `NewsletterDistributor` sarà ancora soddisfatto del fatto che come parametro richiede `Logger`. E starà solo a noi decidere quale istanza passargli. +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. -**Per questo motivo non diamo mai ai nomi delle interfacce il suffisso `Interface` o il prefisso `I`.** Altrimenti non sarebbe possibile sviluppare il codice in modo così elegante. +**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 possiamo accontentarci di una singola istanza del logger, sia esso basato su file o database, e semplicemente passarlo ovunque si registri qualcosa, la situazione è completamente diversa nel caso della classe `Article`. Le sue istanze, infatti, le creiamo secondo necessità, anche più volte. Come gestire la dipendenza dal database nel suo costruttore? +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? -Come esempio può servire un controller che, dopo l'invio di un form, deve salvare l'articolo nel database: +Un esempio può essere un controller che deve salvare un articolo nel database dopo l'invio di un form: ```php class EditController extends Controller @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -Una possibile soluzione si offre direttamente: ci facciamo passare l'oggetto database tramite il costruttore in `EditController` e usiamo `$article = new Article($this->db)`. +Una possibile soluzione sembra ovvia: facciamoci passare l'oggetto database nel costruttore di `EditController` e usiamo `$article = new Article($this->db)`. -Proprio come nel caso precedente con `Logger` e il percorso del file, questa non è la procedura corretta. Il database non è una dipendenza di `EditController`, ma di `Article`. Passare il database va quindi contro la [#Regola n. 2: prendi ciò che è tuo]. Se il costruttore della classe `Article` cambia (viene aggiunto un nuovo parametro), sarà necessario modificare anche il codice in tutti i punti in cui viene creata un'istanza. Ufff. +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 suggerisci? +Houston, cosa proponi? -Regola n. 3: lascia fare alla factory -------------------------------------- +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. E quindi abbiamo bisogno di qualcos'altro che crei e configuri per noi queste classi più flessibili. Lo chiameremo 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 dipendenze, lascia la creazione delle sue istanze a una factory. +La regola è: se una classe ha delle dipendenze, delegate la creazione delle sue istanze a una factory. -Le factory sono un sostituto più intelligente dell'operatore `new` nel mondo della dependency injection. +Le factory sono un'alternativa più intelligente all'operatore `new` nel mondo della dependency injection. .[note] -Per favore, non confondete con il design pattern *factory method*, che descrive un modo specifico di utilizzare le factory e non è correlato a questo argomento. +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 produce e configura oggetti. La classe che produce `Article` la chiameremo `ArticleFactory` e potrebbe assomigliare ad esempio a questo: +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 @@ -475,7 +474,7 @@ class ArticleFactory } ``` -Il suo utilizzo nel controller sarà il seguente: +Il suo uso nel controller sarà questo: ```php class EditController extends Controller @@ -487,7 +486,7 @@ class EditController extends Controller public function formSubmitted($data) { - // lasciamo che la factory crei l'oggetto + // lascia che sia la factory a creare l'oggetto $article = $this->articleFactory->create(); $article->title = $data->title; $article->content = $data->content; @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -Se in questo momento la firma del costruttore della classe `Article` cambia, l'unica parte del codice che deve reagire è la factory stessa `ArticleFactory`. Tutto il resto del codice che lavora con gli oggetti `Article`, come ad esempio `EditController`, non ne sarà minimamente influenzato. +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. -Forse ora ti stai chiedendo se ci siamo davvero aiutati. La quantità di codice è aumentata e tutto inizia a sembrare sospettosamente complicato. +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. -Non preoccuparti, tra poco arriveremo al Nette DI container. E quello ha molti assi nella manica che semplificheranno enormemente la costruzione di applicazioni che utilizzano la dependency injection. Ad esempio, invece della classe `ArticleFactory`, basterà [scrivere una semplice interfaccia |factory]: +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 @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -Ma stiamo anticipando, resisti ancora un po' :-) +Ma stiamo correndo troppo, restate sintonizzati :-) Riepilogo --------- -All'inizio di questo capitolo, abbiamo promesso di mostrarvi un metodo per progettare codice pulito. Basta alle classi +All'inizio di questo capitolo avevamo promesso di mostrare un procedimento per progettare codice pulito. Basta assicurarsi che alle classi: -1) [passare le dipendenze di cui hanno bisogno |#Regola n. 1: fatti passare le dipendenze] -2) [e al contrario non passare ciò di cui non hanno direttamente bisogno |#Regola n. 2: prendi ciò che è tuo] -3) [e che gli oggetti con dipendenze si producono meglio nelle factory |#Regola n. 3: lascia fare alla factory] +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] -Potrebbe non sembrare così a prima vista, ma queste tre regole hanno conseguenze di vasta portata. Portano a una visione radicalmente diversa della progettazione del codice. Ne vale la pena? I programmatori che hanno abbandonato le vecchie abitudini e hanno iniziato a usare costantemente la dependency injection considerano questo passo un momento fondamentale nella loro vita professionale. Si è aperto loro il mondo delle applicazioni chiare e manutenibili. +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. -Ma cosa succede se il codice non utilizza costantemente la dependency injection? Cosa succede se è basato su metodi statici o singleton? Porta a qualche problema? [Sì, e molto fondamentali |global-state]. +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 index 36bf88de99..bdc9338714 100644 --- a/dependency-injection/it/nette-container.texy +++ b/dependency-injection/it/nette-container.texy @@ -2,9 +2,9 @@ Nette DI Container ****************** .[perex] -Nette DI è una delle librerie più interessanti di Nette. Può generare e aggiornare automaticamente container DI compilati, che sono estremamente veloci e incredibilmente facili da configurare. +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 viene solitamente definita tramite file di configurazione nel [formato NEON |neon:format]. Il container che abbiamo creato manualmente nel [capitolo precedente |container] sarebbe scritto così: +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: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -La scrittura è davvero concisa. +La sintassi è molto concisa. -Tutte le dipendenze dichiarate nei costruttori delle classi `ArticleFactory` e `UserController` vengono rilevate e passate automaticamente da Nette DI grazie al cosiddetto [autowiring |autowiring], quindi non è necessario specificare nulla nel file di configurazione. Quindi, anche se i parametri cambiano, non è necessario modificare nulla nella configurazione. Il container Nette si rigenera automaticamente. Puoi concentrarti esclusivamente sullo sviluppo dell'applicazione. +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 tramite setter, usiamo la sezione [setup |services#Setup]. +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 puoi aprire e studiare. Grazie a ciò, vedi esattamente come funziona il container. Puoi anche eseguirne il debug nell'IDE e fare lo step-by-step. E soprattutto: il PHP generato è estremamente veloce. +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 può anche generare codice per le [factory |factory] basate sull'interfaccia fornita. Pertanto, invece della classe `ArticleFactory`, basterà creare solo un'interfaccia nell'applicazione: +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 @@ -36,19 +36,19 @@ interface ArticleFactory } ``` -L'esempio completo è disponibile [su GitHub |https://github.com/nette-examples/di-example-doc]. +Trovate l'esempio completo [su GitHub|https://github.com/nette-examples/di-example-doc]. -Utilizzo indipendente ---------------------- +Uso autonomo +------------ -Implementare la libreria Nette DI in un'applicazione è molto semplice. Prima la installiamo con Composer (perché scaricare zip è coooosì obsoleto): +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 seguente codice crea un'istanza del container DI secondo la configurazione salvata nel file `config.neon`: +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'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -Il container viene generato solo una volta, il suo codice viene scritto nella cache (directory `__DIR__ . '/temp'`) e nelle richieste successive viene semplicemente caricato da lì. +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ì. -Per creare e ottenere servizi si usano i metodi `getService()` o `getByType()`. In questo modo creiamo l'oggetto `UserController`: +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 -$controller = $container->getByType(UserController::class); +$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 auto-refresh, in cui il container si rigenera automaticamente se viene modificata una qualsiasi classe o file di configurazione. Basta specificare `true` come secondo argomento nel costruttore di `ContainerLoader`. +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); ``` -Utilizzo con Nette Framework ----------------------------- +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 scritte in Nette Framework, puoi implementarlo ovunque con sole 3 righe di codice. Tuttavia, se sviluppi applicazioni in Nette Framework, la configurazione e la creazione del container sono gestite da [Bootstrap |application:bootstrapping#Configurazione del Container DI]. +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 index 2c026635f3..255cf42efd 100644 --- a/dependency-injection/it/passing-dependencies.texy +++ b/dependency-injection/it/passing-dependencies.texy @@ -3,22 +3,22 @@ Passaggio delle dipendenze <div class=perex> -Gli argomenti, o nella terminologia DI "dipendenze", possono essere passati alle classi nei seguenti modi principali: +Gli argomenti, o "dipendenze" nella terminologia della DI, si possono passare alle classi nei modi principali seguenti: -* passaggio tramite costruttore -* passaggio tramite metodo (cosiddetto setter) -* impostazione di una variabile -* tramite metodo, annotazione o attributo *inject* +* iniezione tramite costruttore +* iniezione tramite metodo (la cosiddetta setter injection) +* iniezione tramite proprietà +* con il metodo `inject*()` o l'attributo `#[Inject]` </div> -Ora mostreremo le singole varianti con esempi concreti. +Mostriamo ogni variante con esempi concreti. -Passaggio tramite costruttore +Iniezione tramite costruttore ============================= -Le dipendenze vengono passate al momento della creazione dell'oggetto come argomenti del costruttore: +Le dipendenze vengono fornite come argomenti del costruttore nel momento in cui l'oggetto viene istanziato: ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -Questa forma è adatta per le dipendenze obbligatorie di cui la classe ha necessariamente bisogno per funzionare, poiché senza di esse non sarà possibile creare l'istanza. +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 forma di scrittura più breve ([constructor property promotion |https://blog.nette.org/it/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), che è funzionalmente equivalente: +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 @@ -49,7 +49,7 @@ class MyClass } ``` -Da PHP 8.1 è possibile contrassegnare la variabile con il flag `readonly`, che dichiara che il contenuto della variabile non cambierà più: +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 @@ -62,13 +62,13 @@ class MyClass } ``` -Il container DI passa automaticamente le dipendenze al costruttore tramite [autowiring |autowiring]. Gli argomenti che non possono essere passati in questo modo (es. stringhe, numeri, booleani) [li scriviamo nella configurazione |services#Argomenti]. +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]. -Constructor hell ----------------- +L'inferno dei costruttori +------------------------- -Il termine *constructor hell* indica la situazione in cui un figlio eredita da una classe genitore il cui costruttore richiede dipendenze, e allo stesso tempo il figlio richiede dipendenze. Deve quindi ricevere e passare anche quelle del genitore: +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 @@ -85,7 +85,7 @@ final class MyClass extends BaseClass { private Database $db; - // ⛔ CONSTRUCTOR HELL + // ⛔ INFERNO DEI COSTRUTTORI public function __construct(Cache $cache, Database $db) { parent::__construct($cache); @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -Il problema sorge nel momento in cui vogliamo modificare il costruttore della classe `BaseClass`, ad esempio quando viene aggiunta una nuova dipendenza. Allora è necessario modificare anche tutti i costruttori dei figli. Il che rende una tale modifica un inferno. +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 prevenirlo? La soluzione è **dare la preferenza alla [composizione rispetto all'ereditarietà |faq#Perché si preferisce la composizione all ereditarietà]**. +Come si può evitare? La soluzione è **preferire la [composizione all'ereditarietà |faq#Perché si preferisce la composizione all'ereditarietà?]**. -Quindi progetteremo il codice diversamente. Eviteremo le classi [astratte |nette:introduction-to-object-oriented-programming#Classi astratte] `Base*`. Invece che `MyClass` ottenga una certa funzionalità ereditando da `BaseClass`, si farà passare questa funzionalità come dipendenza: +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 @@ -125,10 +125,10 @@ final class MyClass ``` -Passaggio tramite setter -======================== +Setter injection +================ -Le dipendenze vengono passate chiamando un metodo che le salva in una variabile privata. La convenzione usuale per nominare questi metodi è la forma `set*()`, per questo vengono chiamati setter, ma possono ovviamente chiamarsi in qualsiasi altro modo. +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 @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -Questo metodo è adatto per le dipendenze opzionali che non sono indispensabili per la funzione della classe, poiché non è garantito che l'oggetto riceva effettivamente la dipendenza (cioè che l'utente chiami il metodo). +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 consente di chiamare il setter ripetutamente e quindi modificare la dipendenza. Se ciò non è desiderato, aggiungiamo un controllo nel metodo, o da PHP 8.1 contrassegniamo la property `$cache` con il flag `readonly`. +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 @@ -164,7 +164,7 @@ class MyClass } ``` -La chiamata del setter viene definita nella configurazione del container DI nella [chiave setup |services#Setup]. Anche qui si utilizza il passaggio automatico delle dipendenze tramite autowiring: +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: @@ -174,10 +174,10 @@ services: ``` -Impostazione di una variabile -============================= +Iniezione tramite proprietà +=========================== -Le dipendenze vengono passate scrivendo direttamente nella variabile membro: +Le dipendenze vengono fornite scrivendo direttamente in una proprietà della classe: ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -Questo metodo è considerato inappropriato, poiché la variabile membro deve essere dichiarata come `public`. E quindi non abbiamo controllo sul fatto che la dipendenza passata sia effettivamente del tipo specificato (valido prima di PHP 7.4) e perdiamo la possibilità di reagire alla dipendenza appena assegnata con codice personalizzato, ad esempio impedendo modifiche successive. Allo stesso tempo, la variabile diventa parte dell'interfaccia pubblica della classe, il che potrebbe non essere desiderabile. +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'impostazione della variabile viene definita nella configurazione del container DI nella [sezione setup |services#Setup]: +L'assegnazione alla proprietà si definisce nella configurazione del container DI, nella [sezione setup |services#Setup]: ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -Mentre i tre metodi precedenti valgono in generale in tutti i linguaggi orientati agli oggetti, l'iniezione tramite metodo, annotazione o attributo *inject* è specifica esclusivamente per i presenter in Nette. Ne parla un [capitolo separato |best-practices:inject-method-attribute]. +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 per le dipendenze obbligatorie di cui la classe ha necessariamente bisogno per funzionare -- il setter è invece adatto per le dipendenze opzionali, o dipendenze che si desidera poter modificare ulteriormente -- le variabili pubbliche non sono adatte +- 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 index a01436837d..f99d9d22e9 100644 --- a/dependency-injection/it/services.texy +++ b/dependency-injection/it/services.texy @@ -2,16 +2,16 @@ Definizione dei servizi *********************** .[perex] -La configurazione è il luogo in cui insegniamo al container DI come costruire i singoli servizi e come collegarli ad altre dipendenze. Nette fornisce un modo molto chiaro ed elegante per raggiungere questo obiettivo. +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` nel file di configurazione in formato NEON è il luogo in cui definiamo i nostri servizi personalizzati e le loro configurazioni. Vediamo un semplice esempio di definizione di un servizio chiamato `database`, che rappresenta un'istanza della classe `PDO`: +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 specificata darà luogo al seguente metodo factory nel [container DI |container]: +La configurazione qui sopra produce il seguente metodo factory nel [container DI|container]: ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -I nomi dei servizi ci consentono di fare riferimento ad essi in altre parti del file di configurazione, nel formato `@nomeServizio`. Se non è necessario nominare il servizio, possiamo semplicemente usare solo un trattino: +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 utilizzare il metodo `getService()` con il nome del servizio come parametro, o il metodo `getByType()` con il tipo del servizio: +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'); @@ -35,17 +35,17 @@ $database = $container->getByType(PDO::class); ``` -Creazione del servizio -====================== +Creazione dei servizi +===================== -Di solito creiamo un servizio semplicemente creando un'istanza di una certa classe. Ad esempio: +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 abbiamo bisogno di estendere la configurazione con ulteriori chiavi, la definizione può essere suddivisa su più righe: +Se dobbiamo ampliare la configurazione con altre chiavi, la definizione si può spezzare su più righe: ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -La chiave `create` ha un alias `factory`, entrambe le varianti sono comuni nella pratica. Tuttavia, raccomandiamo di usare `create`. +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 di creazione possono essere alternativamente scritti nella chiave `arguments`: +Gli argomenti del costruttore o del metodo factory si possono indicare, in alternativa, con la chiave `arguments`: ```neon services: @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -I servizi non devono essere creati solo tramite la semplice creazione di un'istanza di classe, possono anche essere il risultato della chiamata di metodi statici o metodi di altri servizi: +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: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -Notate che per semplicità, invece di `->` si usa `::`, vedi [#Espressioni]. Verranno generati questi metodi factory: +Notate che, per semplicità, si usa `::` invece di `->`, vedi [#Linguaggio delle espressioni]. Verranno generati questi metodi factory: ```php public function createServiceDatabase(): PDO @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -Il container DI ha bisogno di conoscere il tipo del servizio creato. Se creiamo un servizio tramite un metodo che non ha un tipo di ritorno specificato, dobbiamo indicare esplicitamente questo tipo nella configurazione: +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: @@ -100,14 +100,14 @@ services: Argomenti ========= -Passiamo gli argomenti al costruttore e ai metodi in modo molto simile a come avviene in PHP stesso: +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 suddividere gli argomenti su righe separate. In tal caso, l'uso delle virgole è opzionale: +Per una migliore leggibilità possiamo elencare gli argomenti su righe separate. In tal caso le virgole diventano facoltative: ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -Puoi anche nominare gli argomenti e non dovrai preoccuparti del loro ordine: +Potete anche dare un nome agli argomenti, eliminando la necessità di preoccuparvi del loro ordine: ```neon services: @@ -129,20 +129,20 @@ services: ) ``` -Se vuoi omettere alcuni argomenti e usare il loro valore predefinito o inserire un servizio tramite [autowiring |autowiring], usa un trattino basso: +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%) ``` -Come argomenti si possono passare servizi, usare parametri e molto altro, vedi [#Espressioni]. +Gli argomenti possono comprendere servizi, parametri e molto altro, vedi [#Linguaggio delle espressioni]. Setup ===== -Nella sezione `setup` definiamo i metodi che devono essere chiamati durante la creazione del servizio. +Nella sezione `setup` definiamo i metodi da chiamare al momento della creazione del servizio. ```neon services: @@ -152,7 +152,7 @@ services: - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ``` -Questo in PHP sarebbe simile a: +In PHP avrebbe questo aspetto: ```php public function createServiceDatabase(): PDO @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -Oltre alla chiamata di metodi, è possibile anche passare valori alle proprietà. È supportata anche l'aggiunta di un elemento a un array, che deve essere scritto tra virgolette per non entrare in conflitto con la sintassi NEON: +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: @@ -174,7 +174,7 @@ services: - '$onClick[]' = [@bar, clickHandler] ``` -Che nel codice PHP sarebbe simile a: +Che in codice PHP avrebbe questo aspetto: ```php public function createServiceFoo(): Foo @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -Nel setup è tuttavia possibile chiamare anche metodi statici o metodi di altri servizi. Se hai bisogno di passare come argomento il servizio corrente, indicalo come `@self`: +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: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -Notate che per semplicità, invece di `->` si usa `::`, vedi [#Espressioni]. Verrà generato un tale metodo factory: +Notate che, per semplicità, si usa `::` invece di `->`, vedi [#Linguaggio delle espressioni]. Verrà generato questo metodo factory: ```php public function createServiceFoo(): Foo @@ -210,36 +210,36 @@ public function createServiceFoo(): Foo ``` -Espressioni -=========== +Linguaggio delle espressioni +============================ -Nette DI ci offre mezzi espressivi eccezionalmente ricchi, con i quali possiamo scrivere quasi qualsiasi cosa. Nei file di configurazione possiamo quindi utilizzare [parametri |configuration#Parametri]: +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 del parametro sotto la chiave +# valore di un parametro sotto una chiave %mailer.user% -# parametro all'interno di una stringa +# parametro dentro una stringa '%wwwDir%/images' ``` Inoltre creare oggetti, chiamare metodi e funzioni: ```neon -# creazione di un oggetto +# crea un oggetto DateTime() -# chiamata di un metodo statico +# chiama un metodo statico Collator::create(%locale%) -# chiamata di una funzione PHP +# chiama una funzione PHP ::getenv(DB_USER) ``` -Fare riferimento ai servizi tramite il loro nome o tipo: +Fare riferimento ai servizi per nome oppure per tipo: ```neon # servizio per nome @@ -249,24 +249,34 @@ Fare riferimento ai servizi tramite il loro nome o tipo: @Nette\Database\Connection ``` -Utilizzare la sintassi first-class callable: .{data-version:3.2.0} +Usare la first-class callable syntax: .{data-version:3.2.0} ```neon -# creazione di un callback, analogo a [@user, logout] +# crea una callback, equivalente a [@user, logout] @user::logout(...) ``` -Utilizzare le costanti: +Usare le costanti: ```neon # costante di classe FilesystemIterator::SKIP_DOTS -# costante globale ottenuta tramite la funzione PHP constant() -::constant(PHP_VERSION) +# ottiene una costante globale con la funzione PHP constant() +::constant(\PHP_VERSION) ``` -Le chiamate ai metodi possono essere concatenate come in PHP. Solo per semplicità, invece di `->` si usa `::`: +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') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -Queste espressioni possono essere utilizzate ovunque, durante la [creazione dei servizi |#Creazione del servizio], negli [#argomenti], nella sezione [#setup] o nei [parametri |configuration#Parametri]: +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: @@ -293,12 +303,12 @@ services: Funzioni speciali ----------------- -Nei file di configurazione è possibile utilizzare queste funzioni speciali: +Nei file di configurazione potete usare queste funzioni speciali: -- `not()` negazione del valore -- `bool()`, `int()`, `float()`, `string()` conversione senza perdita al tipo specificato -- `typed()` crea un array di tutti i servizi del tipo specificato -- `tagged()` crea un array di tutti i servizi con il tag specificato +- `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: @@ -308,18 +318,18 @@ services: ) ``` -Rispetto al classico type casting in PHP, come ad esempio `(int)`, la conversione senza perdita genera un'eccezione per valori non numerici. +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 specificato (classe o interfaccia). Omette i servizi che hanno l'autowiring disabilitato. È possibile specificare anche più tipi separati da virgola. +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) ) ``` -È possibile passare l'array di servizi di un certo tipo come argomento anche automaticamente tramite [autowiring |autowiring#Array di servizi]. +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 quindi un array di tutti i servizi con un determinato tag. Anche qui è possibile specificare più tag separati da virgola. +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: @@ -330,7 +340,7 @@ services: Autowiring ========== -La chiave `autowired` consente di influenzare il comportamento dell'autowiring per un servizio specifico. Per i dettagli, vedi [capitolo sull'autowiring |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: @@ -340,10 +350,10 @@ services: ``` -Servizi Lazy .{data-version:3.2.4} -================================== +Servizi pigri .{data-version:3.2.4} +=================================== -Il lazy loading è una tecnica che posticipa la creazione di un servizio fino al momento in cui è effettivamente necessario. Nella configurazione globale è possibile [abilitare la creazione lazy |configuration#Servizi lazy] per tutti i servizi contemporaneamente. Per i singoli servizi è poi possibile sovrascrivere questo comportamento: +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: @@ -352,16 +362,20 @@ services: lazy: false ``` -Quando un servizio è definito come lazy, al momento della sua richiesta dal container DI, otteniamo uno speciale oggetto placeholder. Questo sembra e si comporta come il servizio reale, ma l'inizializzazione effettiva (chiamata del costruttore e del setup) avviene solo alla prima chiamata di uno qualsiasi dei suoi metodi o proprietà. +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 lazy loading può essere utilizzato solo per classi utente, non per classi PHP interne. Richiede PHP 8.4 o versioni successive. +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 per aggiungere informazioni supplementari ai servizi. A un servizio è possibile aggiungere uno o più tag: +I tag servono ad aggiungere informazioni supplementari ai servizi. Potete assegnare a un servizio uno o più tag: ```neon services: @@ -381,26 +395,26 @@ services: logger: monolog.logger.event ``` -Per ottenere tutti i servizi con determinati tag, puoi usare la funzione `tagged()`: +Per ottenere tutti i servizi associati a determinati tag potete usare la funzione `tagged()`: ```neon services: - LoggersDependent( tagged(logger) ) ``` -Nel container DI è possibile ottenere i nomi di tutti i servizi con un determinato tag tramite il metodo `findByTag()`: +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 contenente il nome del servizio e il valore del tag -// es. ['foo' => 'monolog.logger.event', ...] +// $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 +Modalità inject =============== -Tramite il flag `inject: true` si attiva il passaggio delle dipendenze tramite variabili pubbliche con l'annotazione [inject |best-practices:inject-method-attribute#Attributi Inject] e i metodi [inject*() |best-practices:inject-method-attribute#Metodi 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: @@ -409,13 +423,13 @@ services: inject: true ``` -Nell'impostazione predefinita, `inject` è attivato solo per i presenter. +Per impostazione predefinita la modalità `inject` è attiva solo per i presenter. -Modifica dei servizi +Modifiche ai servizi ==================== -Il container DI contiene molti servizi che sono stati aggiunti tramite estensioni integrate o [estensioni utente |extensions]. È possibile modificare le definizioni di questi servizi direttamente nella configurazione. Ad esempio, è possibile modificare la classe del servizio `application.application`, che è standard `Nette\Application\Application`, in un'altra: +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: @@ -424,9 +438,9 @@ services: alteration: true ``` -Il flag `alteration` è informativo e indica che stiamo solo modificando un servizio esistente. +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 completare il setup: +Possiamo anche integrare il setup: ```neon services: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -Durante la sovrascrittura di un servizio, potremmo voler rimuovere gli argomenti originali, le voci di setup o i tag, a tale scopo serve `reset`: +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: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -Se si desidera rimuovere un servizio aggiunto da un'estensione, è possibile farlo in questo modo: +Se volete rimuovere un servizio aggiunto da un'estensione, potete farlo così: ```neon services: diff --git a/dependency-injection/it/upgrading.texy b/dependency-injection/it/upgrading.texy new file mode 100644 index 0000000000..fb5e8eb11c --- /dev/null +++ b/dependency-injection/it/upgrading.texy @@ -0,0 +1,49 @@ +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 index 88c0bf78b6..b31effb927 100644 --- a/dependency-injection/ja/@home.texy +++ b/dependency-injection/ja/@home.texy @@ -2,20 +2,21 @@ Nette DI ******** .[perex] -依存関係注入は、コードと開発に対する見方を根本的に変えるデザインパターンです。クリーンに設計され、保守可能なアプリケーションの世界への扉を開きます。 +依存性注入(Dependency Injection)は、コードと開発に対する見方を根本から変えるデザインパターンです。きれいに設計され、長く保てるアプリケーションの世界への道を開きます。 -- [依存関係注入とは? |introduction] -- [グローバルステートとシングルトン |global-state] +- [Dependency Injection とは |introduction] +- [グローバル状態とシングルトン |global-state] - [依存関係の受け渡し |passing-dependencies] -- [DI コンテナとは? |container] -- [よくある質問|faq] +- [DI コンテナとは |container] +- [よくある質問 |faq] -`nette/di` パッケージは、PHP用の非常に高度なコンパイル済みDIコンテナを提供します。 +`nette/di` パッケージは、PHP のためのきわめて高度なコンパイル済み DI コンテナを提供します。 -- [Nette DI コンテナ |nette-container] +- [Nette DI Container |nette-container] - [設定 |configuration] - [サービスの定義 |services] -- [Autowiring |autowiring] -- [生成されたファクトリ |factory] -- [Nette DI 拡張機能の作成|extensions] +- [オートワイヤリング |autowiring] +- [生成されるファクトリ |factory] +- [Nette DI の拡張の作成 |extensions] +- [コンテナのコンパイルの詳細 |compilation-internals] diff --git a/dependency-injection/ja/@left-menu.texy b/dependency-injection/ja/@left-menu.texy index 9b82537edd..e0ca8dae9b 100644 --- a/dependency-injection/ja/@left-menu.texy +++ b/dependency-injection/ja/@left-menu.texy @@ -1,17 +1,28 @@ Dependency Injection ******************** -- [DI とは? |introduction] -- [グローバルステートとシングルトン |global-state] +- [DI とは |introduction] +- [グローバル状態とシングルトン |global-state] - [依存関係の受け渡し |passing-dependencies] -- [DI コンテナとは? |container] -- [よくある質問|faq] +- [DI コンテナとは |container] +- [よくある質問 |faq] Nette DI -------- -- [Nette DI コンテナ |nette-container] +- [Nette DI Container |nette-container] - [設定 |configuration] - [サービスの定義 |services] -- [Autowiring |autowiring] -- [生成されたファクトリ |factory] -- [Nette DI 拡張機能の作成|extensions] +- [オートワイヤリング |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 index d3c41dc3d7..43b85f3cac 100644 --- a/dependency-injection/ja/@meta.texy +++ b/dependency-injection/ja/@meta.texy @@ -1 +1 @@ -{{sitename: Nette ドキュメンテーション}} +{{sitename: Nette ドキュメント}} diff --git a/dependency-injection/ja/autowiring.texy b/dependency-injection/ja/autowiring.texy index 2c16cc73d5..a0a962b972 100644 --- a/dependency-injection/ja/autowiring.texy +++ b/dependency-injection/ja/autowiring.texy @@ -1,24 +1,24 @@ -Autowiring -********** +オートワイヤリング +********* .[perex] -オートワイヤリングは、コンストラクタや他のメソッドに必要なサービスを自動的に渡すことができる素晴らしい機能であり、それらを書く必要がまったくありません。これにより、多くの時間を節約できます。 +オートワイヤリングは、必要なサービスをコンストラクタやほかのメソッドに自動的に渡してくれる素晴らしい機能で、明示的に指定する必要がなくなります。多くの時間を節約できます。 -これにより、サービス定義を書く際にほとんどの引数を省略できます。代わりに: +おかげで、サービスの定義を書くときに大部分の引数を省けます。次のように書く代わりに、 ```neon services: articles: Model\ArticleRepository(@database, @cache.storage) ``` -次のように書くだけで十分です: +こう書くだけで済みます。 ```neon services: articles: Model\ArticleRepository ``` -オートワイヤリングは型に基づいて行われるため、機能するためには `ArticleRepository` クラスが次のように定義されている必要があります: +オートワイヤリングは型に導かれるので、それが働くには `ArticleRepository` クラスがおおよそ次のように定義されている必要があります。 ```php namespace Model; @@ -30,22 +30,24 @@ class ArticleRepository } ``` -オートワイヤリングを使用できるようにするには、コンテナ内に各型に対して**ちょうど1つのサービス**が存在する必要があります。もし複数存在する場合、オートワイヤリングはどれを渡すべきか分からず、例外をスローします: +オートワイヤリングはサービスの名前を決して使いません。PHP の型システムだけに導かれるので、クラスが実装するインターフェースや継承するクラスも満たすと分かります。おかげでサービスの名前は補助的な識別子にすぎず、名前を変えてもアプリケーションの何も壊れません。 + +オートワイヤリングを使うには、コンテナの中に各型の**サービスがちょうどひとつ**なければなりません。複数あると、オートワイヤリングはどれを渡すべきか分からず例外を投げます。 ```neon services: mainDb: PDO(%dsn%, %user%, %password%) tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # 例外をスローします。mainDb と tempDb の両方が適合します + articles: Model\ArticleRepository # 例外を投げます。mainDb と tempDb の両方が当てはまります ``` -解決策としては、オートワイヤリングを回避してサービス名を明示的に指定する(例:`articles: Model\ArticleRepository(@mainDb)`)か、より賢い方法として、サービスの1つのオートワイヤリングを[無効にする |#オートワイヤリングの無効化]か、最初のサービスを[優先する |#オートワイヤリングの優先順位]ことです。 +ひとつの解は、オートワイヤリングを迂回してサービス名を明示的に指定すること(たとえば `articles: Model\ArticleRepository(@mainDb)`)です。とはいえ、いずれかのサービスのオートワイヤリングを[無効にする |#オートワイヤリングの無効化]か、ひとつのサービスをほかより[優先する |#オートワイヤリングの優先]ほうが便利です。 オートワイヤリングの無効化 ------------- -サービスのオートワイヤリングは、`autowired: no` オプションを使用して無効にできます: +`autowired: false` オプションを使うと、サービスのオートワイヤリングを無効にできます。 ```neon services: @@ -53,27 +55,29 @@ services: tempDb: create: PDO('sqlite::memory:') - autowired: false # tempDb サービスはオートワイヤリングから除外されます + autowired: false # tempDb サービスはオートワイヤリングから除かれます - articles: Model\ArticleRepository # したがって、mainDb をコンストラクタに渡します + articles: Model\ArticleRepository # なのでコンストラクタには mainDb が渡されます ``` -`articles` サービスは、コンストラクタに渡すことができる `PDO` 型の適合するサービスが2つ(`mainDb` と `tempDb`)存在するという例外をスローしません。なぜなら、`mainDb` サービスしか見ていないからです。 +`articles` サービスは、コンストラクタに使える `PDO` サービスが 2 つ(`mainDb` と `tempDb`)あるという例外を投げません。`mainDb` サービスだけを考慮するからです。 + +オートワイヤリングは、[`di › excluded` |configuration#DI]設定オプションを使って型ごとにまとめて無効にすることもできます。ここには、決してオートワイヤリングされるべきでない型(とその子孫)を並べます。 .[note] -Netteでのオートワイヤリングの設定はSymfonyとは異なります。Symfonyでは `autowire: false` オプションは、特定のサービスのコンストラクタ引数にオートワイヤリングを使用しないことを意味します。 Netteでは、オートワイヤリングは常に、コンストラクタ引数であろうと他のメソッドであろうと使用されます。`autowired: false` オプションは、特定のサービスのインスタンスがオートワイヤリングによってどこにも渡されるべきではないことを意味します。 +Nette のオートワイヤリングの設定は Symfony とは違います。Symfony では `autowire: false` は、そのサービスのコンストラクタの引数にオートワイヤリングを使わない、という意味です。Nette では、オートワイヤリングはコンストラクタの引数と、コンテナ経由で呼ばれるほかのメソッド(セッターインジェクションなど)に当てはまります。`autowired: false` オプションは、そのサービスのインスタンスがほかのサービスの依存関係として自動的に渡されるのをコンテナに禁じます。 -オートワイヤリングの優先順位 --------------- +オートワイヤリングの優先 +------------ -同じ型のサービスが複数あり、そのうちの1つに `autowired` オプションを指定すると、そのサービスが優先されます: +同じ型のサービスが複数あり、そのひとつに `autowired` オプションを指定すると、そのサービスが優先されるようになります。 ```neon services: mainDb: create: PDO(%dsn%, %user%, %password%) - autowired: PDO # 優先されます + autowired: PDO # 優先されるようになります tempDb: create: PDO('sqlite::memory:') @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -`articles` サービスは、`PDO` 型の適合するサービスが2つ(`mainDb` と `tempDb`)存在するという例外をスローしませんが、優先されるサービス、つまり `mainDb` を使用します。 +`articles` サービスは、当てはまる `PDO` サービスが複数(`mainDb` と `tempDb`)あるという例外を投げず、優先されるほう、つまり `mainDb` を使います。 -サービスの配列 -------- +サービスのコレクション +----------- -オートワイヤリングは、特定の型のサービスの配列も渡すことができます。PHPでは配列の要素の型をネイティブに記述できないため、`array` 型に加えて、`ClassName[]` 形式の要素型を持つphpDocコメントを追加する必要があります: +オートワイヤリングは、特定の型のサービスの配列を渡すこともできます。PHP は型宣言で配列の要素の型を指定できないので、`array` の型宣言に `ClassName[]` のように要素の型を示す phpDoc のコメントを添える必要があります。 ```php namespace Model; @@ -102,45 +106,62 @@ class ShipManager } ``` -DIコンテナは、指定された型に対応するサービスの配列を自動的に渡します。オートワイヤリングが無効になっているサービスは除外されます。 +すると DI コンテナは、その型に対応するサービスの配列を自動的に渡します。[オートワイヤリングが無効にされた |#オートワイヤリングの無効化]サービスは除かれ、今まさに作られているサービス自身がそのコレクションに含まれることもありません。個々のサービスを渡す場合と違い、オートワイヤリングを特定の型に[絞り込んだり |#オートワイヤリングの絞り込み]、サービスを[優先 |#オートワイヤリングの優先]にしたりしても、ここでは影響がありません。配列には常にその型のすべてのサービスが入ります。 -コメント内の型は `array<int, Class>` または `list<Class>` の形式でもかまいません。phpDocコメントの形式を変更できない場合は、[`typed()` |services#特殊関数] を使用して設定で直接サービスの配列を渡すことができます。 +コメントの型は `array<int, Class>` や `list<Class>` の形でもかまいません。phpDoc のコメントの形を自分で決められない場合は、[`typed()` |services#特別な関数]を使って設定でサービスの配列を直接渡せます。 -スカラー引数 ------- +スカラーの引数 +------- -オートワイヤリングは、オブジェクトとオブジェクトの配列のみを注入できます。スカラー引数(例:文字列、数値、ブール値)は[設定で記述します |services#引数]。 代替案は、スカラー値(または複数の値)をオブジェクトの形式にカプセル化する[settings-objekt |best-practices:passing-settings-to-presenters]を作成することです。その後、このオブジェクトを再びオートワイヤリングで渡すことができます。 +オートワイヤリングが働くのはオブジェクトとオブジェクトの配列だけです。スカラーの引数(文字列、数値、真偽値など)は[設定で指定 |services#引数]しなければなりません。別の方法として、そのスカラー値(や複数の値)を包む[設定オブジェクト|best-practices:passing-settings-to-presenters]を作ることもできます。そのオブジェクトはオートワイヤリングで渡せます。 ```php class MySettings { public function __construct( - // readonly は PHP 8.1 以降で使用可能です + // 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 @@ -162,42 +183,42 @@ class ChildDependent } ``` -これらすべてをサービスとして登録すると、オートワイヤリングは失敗します: +これらをすべてサービスとして登録すると、オートワイヤリングは失敗します。 ```neon services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # 例外をスローします。parent と child の両方のサービスが適合します + parentDep: ParentDependent # 例外を投げます。parent と child の両方が当てはまります childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します ``` -`parentDep` サービスは `Multiple services of type ParentClass found: parent, child` という例外をスローします。なぜなら、そのコンストラクタには `parent` と `child` の両方のサービスが適合し、オートワイヤリングはどちらを選択すべきか決定できないからです。 +`parentDep` サービスは `Multiple services of type ParentClass found: child, parent` という例外を投げます。`parent` と `child` の両方がそのコンストラクタに当てはまり、オートワイヤリングがどちらを選ぶか決められないからです。 -したがって、`child` サービスのオートワイヤリングを `ChildClass` 型に絞り込むことができます: +そこで `child` サービスについて、オートワイヤリングを `ChildClass` 型に絞り込めます。 ```neon services: parent: ParentClass child: create: ChildClass - autowired: ChildClass # 'autowired: self' と書くこともできます + autowired: ChildClass # 'autowired: self' とも書けます parentDep: ParentDependent # オートワイヤリングは parent サービスをコンストラクタに渡します childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します ``` -これで、`parentDep` サービスのコンストラクタには `parent` サービスが渡されます。なぜなら、これが現在唯一適合するオブジェクトだからです。`child` サービスはもはやオートワイヤリングによって渡されません。はい、`child` サービスは依然として `ParentClass` 型ですが、パラメータの型に指定された絞り込み条件はもはや満たされません。つまり、`ParentClass` が `ChildClass` の*スーパータイプである*という条件は満たされません。 +これで `parentDep` サービスのコンストラクタには `parent` サービスが渡されます。当てはまるオブジェクトがそれだけになったからです。`child` サービスはもうそこへオートワイヤリングされません。たしかに `child` サービスは今も `ParentClass` 型ですが、絞り込みの条件 `autowired: ChildClass` により、明示的に `ChildClass`(またはその派生型)と型付けされたパラメータにしか渡されません。`ParentDependent` は `ParentClass` を求めるので、`child` サービスはそこでのオートワイヤリングの候補から外れます。 -`child` サービスでは、`autowired: ChildClass` は `autowired: self` と書くこともできます。なぜなら `self` は現在のサービスクラスのプレースホルダーだからです。 +`child` サービスの `autowired: ChildClass` は `autowired: self` とも書けます。`self` は現在のサービスのクラスを表すプレースホルダーだからです。 -`autowired` キーには、複数のクラスやインターフェースを配列として指定することもできます: +`autowired` キーには、複数のクラスやインターフェースを配列として指定することもできます。 ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -例にインターフェースを追加してみましょう: +例にインターフェースを足してみましょう。 ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -`child` サービスを制限しない場合、`FooDependent`、`BarDependent`、`ParentDependent`、`ChildDependent` のすべてのクラスのコンストラクタに適合し、オートワイヤリングはそれを渡します。 +`child` サービスを何も制限しなければ、`FooDependent`、`BarDependent`、`ParentDependent`、`ChildDependent` のすべてのクラスのコンストラクタに当てはまり、オートワイヤリングはそこへ渡します。 -しかし、そのオートワイヤリングを `autowired: ChildClass` (または `self`) を使用して `ChildClass` に絞り込むと、オートワイヤリングはそれを `ChildDependent` のコンストラクタにのみ渡します。なぜなら、要求される引数の型は `ChildClass` であり、`ChildClass` は `ChildClass` の*型である*という条件が満たされるからです。他のパラメータで指定された他の型は `ChildClass` のスーパータイプではないため、サービスは渡されません。 +しかし `autowired: ChildClass`(または `self`)でオートワイヤリングを `ChildClass` に絞り込むと、渡されるのは `ChildDependent` のコンストラクタだけになります。そこは `ChildClass` 型の引数を求めており、`ChildClass` は `ChildClass` *である*からです。ほかのパラメータが求める型はどれも `ChildClass` でもその派生型でもないので、サービスは渡されません。 -`autowired: ParentClass` を使用して `ParentClass` に制限すると、オートワイヤリングはそれを再び `ChildDependent` のコンストラクタに渡します(要求される `ChildClass` は `ParentClass` のスーパータイプであるため)。そして、新たに `ParentDependent` のコンストラクタにも渡します。なぜなら、要求される型 `ParentClass` も適合するからです。 +`autowired: ParentClass` で `ParentClass` に制限すると、オートワイヤリングは再び `ChildDependent` のコンストラクタに渡し(求められる `ChildClass` は `ParentClass` の派生型だからです)、さらに `ParentDependent` のコンストラクタにも渡します。求められる型 `ParentClass` も適合するからです。 -`FooInterface` に制限すると、依然として `ParentDependent`(要求される `ParentClass` は `FooInterface` のスーパータイプ)と `ChildDependent` にオートワイヤリングされますが、さらに `FooDependent` のコンストラクタにも渡されます。しかし、`BarDependent` には渡されません。なぜなら `BarInterface` は `FooInterface` のスーパータイプではないからです。 +`FooInterface` に制限すると、引き続き `ParentDependent`(求められる `ParentClass` は `FooInterface` の派生型です)と `ChildDependent` にオートワイヤリングされ、加えて `FooDependent` のコンストラクタにも渡されますが、`BarDependent` には渡されません。`BarInterface` は `FooInterface` の派生型ではないからです。 ```neon services: @@ -251,8 +272,8 @@ services: create: ChildClass autowired: FooInterface - fooDep: FooDependent # オートワイヤリングは child をコンストラクタに渡します - barDep: BarDependent # 例外をスローします。適合するサービスがありません - parentDep: ParentDependent # オートワイヤリングは child をコンストラクタに渡します - childDep: ChildDependent # オートワイヤリングは child をコンストラクタに渡します + 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 new file mode 100644 index 0000000000..0a6d842e3e --- /dev/null +++ b/dependency-injection/ja/compilation-internals.texy @@ -0,0 +1,222 @@ +コンテナのコンパイルの詳細 +************* + +.[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 index b9510329cf..ad291dd4cb 100644 --- a/dependency-injection/ja/configuration.texy +++ b/dependency-injection/ja/configuration.texy @@ -1,33 +1,33 @@ -DIコンテナの設定 -********* +DI コンテナの設定 +********** .[perex] -Nette DIコンテナの設定オプションの概要。 +Nette DI コンテナの設定オプションの概要です。 設定ファイル ====== -Nette DIコンテナは、設定ファイルを使用して簡単に制御できます。これらは通常、[NEON形式|neon:format]で記述されます。編集には、この形式を[サポートするエディタ |best-practices:editors-and-tools#IDEエディタ]をお勧めします。 +Nette DI コンテナは設定ファイルで簡単に制御できます。設定ファイルはふつう [NEON 形式|neon:format]で書きます。この形式に[対応したエディタ |tools:ide]を使うことをおすすめします。 <pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "デコレータ .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DIコンテナ .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#拡張機能]: "追加のDI拡張機能のインストール .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#ファイルのインクルード]: "ファイルのインクルード .[prism-token prism-comment]"<br> +"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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -パラメータ `dsn` は、設定内のどこでも `%dsn%` と記述して参照できます。パラメータは `'%wwwDir%/images'` のような文字列内でも使用できます。 +`dsn` パラメータは、設定のどこからでも `%dsn%` という書き方で参照します。パラメータは `'%wwwDir%/images'` のように文字列の中でも使えます。 -パラメータは文字列や数値だけでなく、配列を含むこともできます: +パラメータは文字列や数値だけでなく、配列を含むこともできます。 ```neon parameters: @@ -51,30 +51,30 @@ parameters: 特定のキーは `%mailer.user%` のように参照します。 -コード内、例えばクラス内で、任意のパラメータの値を知る必要がある場合は、そのクラスに渡します。例えばコンストラクタで。パラメータの値をクラスが問い合わせるような、設定を表すグローバルオブジェクトは存在しません。それは依存関係注入の原則に反します。 +コード(クラスなど)がパラメータの値を必要とするなら、その値をクラスに渡してください。たとえばコンストラクタで渡します。クラスがパラメータの値を問い合わせられるグローバルな設定オブジェクトはありません。それは依存性注入の原則に反するからです。 サービス ==== -[別の章を参照|services]。 +[別の章|services]をご覧ください。 Decorator ========= -特定の型のすべてのサービスを一括して変更するにはどうすればよいでしょうか?例えば、特定の共通の親クラスを継承するすべてのPresenterで特定のメソッドを呼び出すには?そのためにデコレータがあります。 +ある型の複数のサービスを一度に変更するにはどうすればよいでしょうか。たとえば、ある基底クラスを継承するすべてのプレゼンターで特定のメソッドを呼ぶには? そのための decorator です。 ```neon decorator: - # このクラスまたはインターフェースのインスタンスであるすべてのサービスに対して + # このクラスやインターフェースのインスタンスであるすべてのサービスに対して App\Presentation\BasePresenter: setup: - - setProjectId(10) # このメソッドを呼び出す - - $absoluteUrls = true # そして変数を設定する + - setProjectId(10) # このメソッドを呼びます + - $absoluteUrls = true # そして変数を設定します ``` -デコレータは、[タグ |services#タグ]の設定や[injectモード |services#Injectモード]の有効化にも使用できます。 +decorator は[タグ |services#タグ]の設定や [inject モード |services#Inject モード]の有効化にも使えます。 ```neon decorator: @@ -87,90 +87,90 @@ decorator: DI === -DIコンテナの技術設定。 +DI コンテナの技術的な設定です。 ```neon di: - # Tracy Bar に DIC を表示しますか? - debugger: ... # (bool) デフォルトは true + # DIC を Tracy バーに表示しますか? + debugger: ... # (bool) 既定は自動検出(Tracy があれば有効) # 決してオートワイヤリングしないパラメータの型 excluded: ... # (string[]) - # サービスの遅延生成を許可しますか? - lazy: ... # (bool) デフォルトは false + # サービスの遅延生成を有効にしますか? + lazy: ... # (bool) 既定は false - # DIコンテナが継承するクラス - parentClass: ... # (string) デフォルトは Nette\DI\Container + # DI コンテナが継承するクラス + parentClass: ... # (string) 既定は Nette\DI\Container ``` 遅延サービス .{data-version:3.2.4} ---------------------------- -`lazy: true` 設定は、サービスの遅延(遅延)生成を有効にします。これは、サービスがDIコンテナから要求された瞬間に実際に作成されるのではなく、初回使用時に作成されることを意味します。これにより、特定のリクエストで実際に必要なサービスのみが作成されるため、アプリケーションの起動が高速化され、メモリ要件が削減される可能性があります。 +`lazy: true` を設定すると、サービスの遅延生成が有効になります。つまりサービスは DI コンテナに要求された時点では実際には作られず、最初に使われるときにはじめて作られます。そのリクエストで本当に必要なサービスだけが作られるので、アプリケーションの起動が速くなり、メモリの使用量も減らせます。 -特定のサービスに対して、遅延生成は[変更できます |services#遅延サービス]。 +個々のサービスについて、遅延生成は[調整できます |services#遅延サービス]。 .[note] -遅延オブジェクトは、ユーザー定義クラスにのみ使用でき、PHP内部クラスには使用できません。PHP 8.4以降が必要です。 +遅延オブジェクトはユーザー定義のクラスにしか使えず、PHP の内部クラスには使えません。PHP 8.4 以上が必要です。 -メタデータのエクスポート ------------- +メタデータの書き出し +---------- -DIコンテナクラスには多くのメタデータも含まれています。メタデータのエクスポートを削減することで、そのサイズを小さくすることができます。 +DI コンテナのクラスには多くのメタデータも含まれます。メタデータの書き出しを減らせば、その大きさを抑えられます。 ```neon di: export: - # パラメータをエクスポートしますか? - parameters: false # (bool) デフォルトは true + # パラメータを書き出しますか? + parameters: false # (bool) 既定は true - # タグをエクスポートしますか?どのタグを? - tags: # (string[]|bool) デフォルトはすべて + # タグを書き出しますか、そしてどれを? + tags: # (string[]|bool) 既定はすべて - event.subscriber - # オートワイヤリング用のデータをエクスポートしますか?どのデータを? - types: # (string[]|bool) デフォルトはすべて + # オートワイヤリング用のデータを書き出しますか、そしてどれを? + types: # (string[]|bool) 既定はすべて - Nette\Database\Connection - Symfony\Component\Console\Application ``` -`$container->getParameters()` 配列を使用しない場合は、パラメータのエクスポートを無効にできます。さらに、`$container->findByTag(...)` メソッドでサービスを取得するために使用するタグのみをエクスポートできます。このメソッドをまったく呼び出さない場合は、`false` を使用してタグのエクスポートを完全に無効にできます。 +`$container->getParameters()` を使わないなら、パラメータの書き出しを無効にできます。さらに、`$container->findByTag(...)` でサービスを取得するのに実際に使うタグだけを書き出せます。このメソッドをまったく呼ばないなら、`false` を使ってタグの書き出しを完全に無効にできます。 -`$container->getByType()` メソッドのパラメータとして使用するクラスを指定することで、[オートワイヤリング |autowiring]用のメタデータを大幅に削減できます。そして再び、このメソッドをまったく呼び出さない場合(または[bootstrap|application:bootstrapping]で `Nette\Application\Application` を取得するためだけに使用する場合)、`false` を使用してエクスポートを完全に無効にできます。 +[オートワイヤリング|autowiring]のメタデータも、`$container->getByType()` で実際に要求するクラスだけを並べれば大きく減らせます。このメソッドを呼ばないなら(あるいは `Nette\Application\Application` を得るために [bootstrap|application:bootstrapping]ファイルでしか呼ばないなら)、`false` を使って型の書き出しを完全に無効にできます。 -拡張機能 -==== +Extensions +========== -追加のDI拡張機能を登録します。この方法で、例えば `Dibi\Bridges\Nette\DibiExtension22` DI拡張機能を `dibi` という名前で追加します。 +追加の DI 拡張の登録です。たとえば DI 拡張 `Dibi\Bridges\Nette\DibiExtension3` を `dibi` という名前で追加するにはこうします。 ```neon extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -その後、`dibi` セクションで設定します: +そして `dibi` セクションで設定します。 ```neon dibi: host: localhost ``` -パラメータを持つクラスを拡張機能として追加することもできます: +パラメータ付きのクラスを拡張として追加することもできます。 ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` -ファイルのインクルード -=========== +ファイルの読み込み +========= -`includes` セクションで他の設定ファイルをインクルードできます: +追加の設定ファイルは `includes` セクションで読み込めます。 ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -`parameters.php` という名前はタイプミスではありません。設定はPHPファイルで記述することもでき、そのファイルは配列として設定を返します: +`parameters.php` という名前は誤りではありません。設定は、それを配列として返す PHP ファイルで書くこともできます。 ```php <?php @@ -192,15 +192,15 @@ return [ ]; ``` -設定ファイル内で同じキーを持つ要素が現れた場合、それらは上書きされるか、[配列の場合はマージされます |#マージ]。後からインクルードされるファイルは、前のファイルよりも高い優先度を持ちます。`includes` セクションが記載されているファイルは、その中でインクルードされるファイルよりも高い優先度を持ちます。 +複数の設定ファイルに同じキーの項目が現れた場合、それらは上書きされるか、配列の場合は[統合されます |#統合]。あとで読み込まれたファイルのほうが、前のものより優先度が高くなります。`includes` セクションを持つファイルは、そこで読み込まれるファイルより優先度が高くなります。 Search ====== -DIコンテナへのサービスの自動追加は、作業を非常に快適にします。NetteはPresenterを自動的にコンテナに追加しますが、他のクラスも簡単に追加できます。 +DI コンテナへのサービスの自動登録は、開発を大きく楽にします。Nette はプレゼンターを自動的にコンテナへ追加しますが、ほかのクラスも簡単に追加できます。 -クラスを検索するディレクトリ(およびサブディレクトリ)を指定するだけです: +クラスをどのディレクトリ(とサブディレクトリ)で探すべきかを指定するだけです。 ```neon search: @@ -208,22 +208,29 @@ search: - in: %appDir%/Model ``` -通常、すべてのクラスとインターフェースを追加したいわけではないため、それらをフィルタリングできます: +探索の規則がひとつだけなら、リストを省いてそのキーを `search` の直下に書けます。 + +```neon +search: + in: %appDir% +``` + +とはいえ、ふつうはすべてのクラスとインターフェースを追加したいわけではないので、絞り込めます。 ```neon search: - in: %appDir%/Forms - # ファイル名によるフィルタリング (string|string[]) + # ファイル名による絞り込み (string|string[]) files: - *Factory.php - # クラス名によるフィルタリング (string|string[]) + # クラス名による絞り込み (string|string[]) classes: - *Factory ``` -または、指定されたクラスの少なくとも1つを継承または実装するクラスを選択することもできます: +あるいは、並べたクラスの少なくともひとつを継承または実装するクラスを選べます。 ```neon @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -除外ルール、つまりクラス名のマスクや継承元の親クラスを定義することもできます。これらが一致する場合、サービスはDIコンテナに追加されません: +クラス名のマスクや祖先を使って除外の規則を定めることもできます。クラスが除外の規則に当てはまれば、DI コンテナには追加されません。 ```neon search: @@ -247,7 +254,7 @@ search: implements: ... ``` -すべてのサービスにタグを設定できます: +自動登録されるすべてのサービスにタグを割り当てられます。 ```neon search: @@ -255,11 +262,13 @@ search: tags: ... ``` +クラスのほかに、探索は `create()` または `get()` メソッドをひとつだけ持つインターフェースも[生成されるファクトリやアクセサ |factory]として登録します。同じ型のサービスがすでにコンテナに登録されているクラスは飛ばされるので、重複はできません。 + -マージ +統合 === -複数の設定ファイルで同じキーを持つ要素が現れた場合、それらは上書きされるか、配列の場合はマージされます。後からインクルードされるファイルは、前のファイルよりも高い優先度を持ちます。 +複数の設定ファイルに同じキーの要素が現れた場合、それらは上書きされるか、配列の場合は統合されます。あとで読み込まれたファイルのほうが、前のものより優先度が高くなります。 <table class=table> <tr> @@ -292,7 +301,7 @@ items: </tr> </table> -配列の場合、キー名の後に感嘆符を付けることでマージを防ぐことができます: +配列については、キー名のあとに感嘆符を付けると統合を防げます。 <table class=table> <tr> @@ -323,4 +332,4 @@ items: </tr> </table> -{{maintitle: 依存性注入の設定}} +{{maintitle: Dependency Injection の設定}} diff --git a/dependency-injection/ja/container.texy b/dependency-injection/ja/container.texy index 97a2692df3..691ce75896 100644 --- a/dependency-injection/ja/container.texy +++ b/dependency-injection/ja/container.texy @@ -1,16 +1,16 @@ -DIコンテナとは? -********* +DI コンテナとは? +********** .[perex] -依存性注入コンテナ(DIC)は、オブジェクトのインスタンス化と設定を行うクラスです。 +依存性注入コンテナ(DIC、または DI コンテナ)は、ほかのオブジェクト(サービスと呼ばれます)を生成して設定する役目を持つオブジェクトです。 -驚かれるかもしれませんが、多くの場合、依存性注入(略してDI)の利点を活用するために依存性注入コンテナは必要ありません。[導入章|introduction]でも、DIの具体例を示しましたが、コンテナは必要ありませんでした。 +意外に思うかもしれませんが、多くの場合、依存性注入(略して DI)の恩恵を受けるのに依存性注入コンテナは必要ありません。実際、[はじめの章|introduction]でも DI の具体的な例を示しましたが、コンテナは不要でした。 -しかし、多くの依存関係を持つ大量の異なるオブジェクトを管理する必要がある場合、依存性注入コンテナは本当に便利です。これは、フレームワーク上に構築されたWebアプリケーションの場合などです。 +とはいえ、複雑な依存関係を持つ多数のオブジェクトを管理するようになると、DI コンテナはとても役に立ちます。フレームワークの上に作られたウェブアプリケーションでは、たいていそうなります。 -前の章で、`Article` と `UserController` クラスを紹介しました。どちらもデータベースと `ArticleFactory` ファクトリという依存関係を持っています。そして、これらのクラスのためにコンテナを作成します。もちろん、このような簡単な例ではコンテナを持つ意味はありません。しかし、それがどのように見え、機能するかを示すために作成します。 +前の章で `Article` と `EditController` のクラスを紹介しました。どちらも依存関係、すなわちデータベースとファクトリ `ArticleFactory` を持っています。これらのクラスのために、ここでコンテナを作ってみましょう。もちろん、これほど単純な例にコンテナを作るのはやりすぎです。それでも、どんな形でどう動くかを示すために作ります。 -以下は、上記の例のための簡単なハードコードされたコンテナです: +上の例のための、値を直接書いた単純なコンテナです。 ```php class Container @@ -25,23 +25,23 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` -使用法は次のようになります: +使い方は次のようになります。 ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -コンテナにオブジェクトを問い合わせるだけで、それをどのように作成するか、どのような依存関係を持っているかを知る必要はありません。コンテナがすべてを知っています。依存関係はコンテナによって自動的に注入されます。これがその強みです。 +オブジェクトをコンテナに要求するだけで、その作り方も依存関係も知る必要はありません。すべてコンテナが引き受けます。依存関係はコンテナが自動的に注入します。それがコンテナの強みです。 -コンテナにはまだすべてのデータがハードコードされています。そこで、次のステップとしてパラメータを追加し、コンテナを本当に便利にします: +今のところ、コンテナはすべての情報を直接書き込んでいます。次の段階として、コンテナを本当に使えるものにするためにパラメータを足しましょう。 ```php class Container @@ -70,9 +70,9 @@ $container = new Container([ ]); ``` -鋭い読者は特定の問題に気付いたかもしれません。`UserController` オブジェクトを取得するたびに、新しい `ArticleFactory` インスタンスとデータベースも作成されます。これは絶対に望ましくありません。 +目ざとい読者は問題に気づくかもしれません。`EditController` オブジェクトを取り出すたびに、`ArticleFactory` とデータベース接続の新しいインスタンスも作られてしまいます。これは望ましくありません。 -そこで、常に同じインスタンスを返す `getService()` メソッドを追加します: +そこで、常に同じインスタンスを返す `getService()` メソッドを足します。 ```php class Container @@ -87,7 +87,7 @@ class Container public function getService(string $name): object { if (!isset($this->services[$name])) { - // getService('Database') は createDatabase() を呼び出します + // getService('Database') は createDatabase() を呼びます $method = 'create' . $name; $this->services[$name] = $this->$method(); } @@ -98,9 +98,9 @@ class Container } ``` -例えば `$container->getService('Database')` を初めて呼び出すと、`createDatabase()` にデータベースオブジェクトを作成させ、それを `$services` 配列に保存し、次回の呼び出しではそのまま返します。 +たとえば `$container->getService('Database')` の最初の呼び出しでは、`createDatabase()` を呼んでデータベースのオブジェクトを作り、`$services` 配列に保存して返します。以降の呼び出しでは、保存されたインスタンスをそのまま返します。 -コンテナの残りの部分も `getService()` を使用するように修正します: +コンテナの残りの部分も `getService()` を使うように書き換えます。 ```php class Container @@ -112,16 +112,16 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -ちなみに、サービスという用語は、コンテナによって管理される任意のオブジェクトを指します。そのため、メソッド名も `getService()` です。 +ちなみに、サービスという言葉はコンテナが管理する任意のオブジェクトを指します。メソッド名が `getService()` なのはそのためです。 -完了です。完全に機能するDIコンテナができました!そして、それを使用できます: +これで完成です。きちんと動く DI コンテナができました。さっそく使ってみましょう。 ```php $container = new Container([ @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -ご覧のとおり、DICを書くことは複雑ではありません。オブジェクト自体は、何らかのコンテナによって作成されていることを知らないという点を思い出す価値があります。その結果、PHPの任意のオブジェクトを、そのソースコードに介入することなくこのように作成することが可能です。 +ご覧のとおり、DIC を書くのは難しくありません。注目すべきは、オブジェクト自身はコンテナが自分を作っていることを知らない、という点です。ですから、ソースコードを変えずに任意の PHP オブジェクトをこの方法で作れます。 -コンテナクラスの手動での作成とメンテナンスは、かなり早く悪夢になる可能性があります。したがって、次の章では、ほぼ自動的に生成および更新できる[Nette DIコンテナ|nette-container]について話します。 +とはいえ、コンテナのクラスを手で作って保守するのは、すぐに悪夢になりかねません。そこで次の章では、ほぼ自動で自分自身を生成・更新できる [Nette DI Container|nette-container]を扱います。 {{maintitle: 依存性注入コンテナとは?}} diff --git a/dependency-injection/ja/extensions.texy b/dependency-injection/ja/extensions.texy index c86b993487..6b0233efd5 100644 --- a/dependency-injection/ja/extensions.texy +++ b/dependency-injection/ja/extensions.texy @@ -1,39 +1,67 @@ -Nette DIの拡張機能の作成 -**************** +Nette DI の拡張の作成 +*************** .[perex] -DIコンテナの生成は、設定ファイルに加えて、いわゆる*拡張機能*によっても影響を受けます。これらは、設定ファイルの `extensions` セクションで有効化します。 +拡張は、DI コンテナのコンパイルに割り込むクラスです。サービスをプログラムから登録し、自分の設定セクションを検証し、ほかが定義したサービスを変更し、さらには生成されるコンテナのコードまで変えられます。このページでは、その書き方、何がいつ起こるのか、そして何に気をつけるべきかを学びます。 -このようにして、`BlogExtension` クラスによって表現される拡張機能を `blog` という名前で追加します: +拡張は、パッケージが Nette に本来の形で組み込まれるための手段です。すべての `nette/*` パッケージがそれを使っていますし、あなたのパッケージもそうできます。典型的な拡張は、次のうちひとつ以上を行います。 + +- **ライブラリを統合する** - そのサービスをコンテナに登録し、親しみやすく検証済みの設定セクションを公開します(`mail:` や `database:` のセクションはここから来ます) +- **登録を自動化する** - `services:` に並べるのが面倒になるほど似たサービスを、ループや規則にもとづいてまとめて登録します +- **横断的な変更を加える** - ほかが登録したサービスを見つけて補います。たとえば特定のタグを持つすべてのサービスにロガーを結びつけます + +日々のアプリケーション開発で拡張が必要になることはめったにありません。クラスの登録と結びつけは、設定の [services |services]セクションで足ります。設定だけでは足りなくなったときに拡張へ手を伸ばしてください。 + +拡張は `extensions` セクションで有効にします。`BlogExtension` クラスが表す拡張を `blog` という名前で追加するにはこうします。 ```neon extensions: blog: BlogExtension ``` -各コンパイラ拡張機能は [api:Nette\DI\CompilerExtension] を継承し、DIコンテナのビルド中に順次呼び出される以下のメソッドを実装できます: +コンストラクタが引数を取るなら、その場で渡します。 + +```neon +extensions: + blog: BlogExtension(%debugMode%) +``` + + +コンパイルのしくみ +========= + +拡張を自信を持って書くには、ひとつ重要なことを知っておく必要があります。**あなたのコードがいつ走るのか**です。Nette はリクエストの処理中にサービスを結びつけたりしません。代わりにコンテナをあらかじめ*コンパイル*します。すべての設定ファイルを読み、拡張に仕事をさせ、最適化された PHP のクラスを生成してディスクに保存します。以降のリクエストは、この出来上がったクラスを読み込むだけです。ですから拡張のコードは、コンテナが(再)構築されるときにだけ走り、リクエストごとには走りません。 + +これには重要な帰結があります。コンパイル中はまだサービスが存在しません。存在するのは**定義**です。各サービスがどのクラスになるか、どう作るか、そのあと何を呼ぶかを記したレシピです。定義は [ContainerBuilder |#ContainerBuilder]オブジェクトの中にあります。拡張は本質的に*スクリプトで書ける設定*です。`services:` セクションで宣言できることは何でも、条件つきで、ループで、あるいはほかが登録したものに反応して、PHP でも組み立てられます。 -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +コンパイルは段階を追って進み、拡張はそのそれぞれに入り込めます。 +1) すべての拡張の設定セクションが検証されます(`getConfigSchema()`) +2) 各拡張が自分のサービスを登録します(`loadConfiguration()`)。ユーザーの `services:` セクションは最後に処理されるので、アプリケーションが常に最終決定権を持ちます +3) すべての定義がそろい、サービスの型が解決されると、拡張はそれらを変更できます(`beforeCompile()`) +4) コンテナのクラスが生成されます。拡張はまだそのコードを調整でき(`afterCompile()`)、アプリケーションの起動時に走るコードを出力できます([初期化 |#初期化のコード]) -getConfigSchema() .[method] -=========================== +.[note] +開発モードでは、設定ファイルや拡張のクラス自体を変えるたびにコンテナが自動的に再コンパイルされます。どちらも依存関係として追跡されているからです。ですからキャッシュを消さずに拡張を開発できます。 -このメソッドが最初に呼び出されます。設定パラメータの検証のためのスキーマを定義します。 +.[tip] +各段階で何が起きるのか、パラメータがいつ展開されるのか、`@service` がいつ参照になるのか、そして型でサービスを探しても安全なのは正確にいつなのかを深く知りたい場合は、[コンテナのコンパイルの詳細 |compilation-internals]をご覧ください。 -拡張機能は、拡張機能が追加された名前と同じ名前のセクション、つまり `blog` で設定します: + +最初の拡張 +===== + +小さいながら完全な拡張の例です。同じファイルで有効にし、設定します。 ```neon -# extension と同じ名前 +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -すべての設定オプション、それらの型、許可された値、そして場合によってはデフォルト値を含むスキーマを作成します: +そしてこれがクラスの全体です。 ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -ドキュメントは [スキーマ |schema:] ページにあります。さらに、`dynamic()` を使用してどのオプションが[動的 |application:bootstrapping#動的パラメータ]であるかを指定できます。例:`Expect::int()->dynamic()`。 -設定には、`stdClass` オブジェクトである `$this->config` 変数を通じてアクセスします: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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` を生むので、異なる拡張のサービスが衝突することはありません。 -loadConfiguration() .[method] -============================= +そして最後の数行が、そもそも拡張が存在する理由を示しています。`comments` サービスはコメントが有効なときにだけ登録されます。ただの設定ファイルでは、こうした判断はできません。 + +こうして登録されたサービスは、`services:` に書いた場合とまったく同じように振る舞います。必要になったときに遅延して作られ、`Blog\Articles` と型宣言されたところにはオートワイヤリングが渡します。 + +以降の章では、拡張のライフサイクルを詳しく説明し、次に拡張の中で使う [ContainerBuilder |#ContainerBuilder]の API を、最後に知っておく価値のある[落とし穴 |#ヒントと落とし穴]を扱います。 + + +拡張のライフサイクル +========== + +拡張は [api:Nette\DI\CompilerExtension]を継承し、`getConfigSchema()`、`loadConfiguration()`、`beforeCompile()`、`afterCompile()` の 4 つのメソッドのうちいくつかを上書きします。コンパイラはコンパイル中にこの順序でそれらを呼びます。 -コンテナにサービスを追加するために使用されます。これには [api:Nette\DI\ContainerBuilder] を使用します: + +getConfigSchema(): Nette\Schema\Schema .[method] +------------------------------------------------ + +拡張の設定セクションのスキーマを定義します。おかげで利用者は、検証と分かりやすいエラーメッセージをただで手に入れます。`blog:` セクションの打ち間違いや型の誤りは、あなたがチェックを 1 行も書かずに、理解しやすいメッセージで報告されます。 + +スキーマは [Schema |schema:]ライブラリで記述し、型、既定値、許される値などを表せます。 ```php -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // または setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -慣習として、拡張機能によって追加されたサービスには、名前の衝突が発生しないように、その名前でプレフィックスを付けます。これは `prefix()` メソッドが行うため、拡張機能の名前が `blog` であれば、サービスは `blog.articles` という名前を持ちます。 +検証された設定は `stdClass` オブジェクトとして `$this->config` から使えます(スキーマに `castTo('array')` を足せば配列としても使えます)。 -サービスの名前を変更する必要がある場合、後方互換性を維持するために、元の名前でエイリアスを作成できます。Netteは、例えば `routing.router` サービスで同様のことを行っています。これは以前の名前 `router` でも利用可能です。 +オプションの値がコンパイル時には分からない場合、たとえば環境変数から来る場合は、`dynamic()` で印を付けてください。たとえば `Expect::int()->dynamic()` です。詳しくは[動的パラメータ |application:bootstrapping#動的パラメータ]をご覧ください。 + + +loadConfiguration() .[method] +----------------------------- + +拡張が [ContainerBuilder |#ContainerBuilder]を使って自分のサービスを登録する場所です。 ```php -$builder->addAlias('router', 'routing.router'); +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')); +} +``` -サービスは、ContainerBuilderクラスのAPIを使用して作成するだけでなく、設定ファイルNEONのservicesセクションで使用されるよく知られた記法でも作成できます。プレフィックス `@extension` は現在の拡張機能を表します。 +サービスが多い場合は、見慣れた [services |services]の構文を使って別の NEON ファイルで定義するほうが便利かもしれません。`@extension` の接頭辞は現在の拡張を指します。 ```neon services: @@ -107,88 +160,284 @@ services: comments: create: MyBlog\CommentsModel(@connection, @extension.articles) +``` + +これらの定義は `loadDefinitionsFromConfig()` で読み込みます。名前には自動的に接頭辞が付き、ファイルは依存関係として追跡されるので、変更すれば再コンパイルが起こります。 - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -サービスをロードします: + +beforeCompile() .[method] +------------------------- + +このメソッドが呼ばれる時点で、builder は**すべての**定義を持っています。あなたの定義、ほかの拡張の定義、そしてユーザーの設定ファイルの定義です。サービスの型も解決済みなので、型による検索が確実に働きます。この段階は、最終的なサービスのつながりを調べて補うのにうってつけです。 + +ふつうはタグや型でサービスを探し、見つけた定義を補います。 ```php -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // 拡張機能の設定ファイルをロード - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + foreach ($builder->findByTag('logaware') as $name => $attrs) { + $builder->getDefinition($name)->addSetup('setLogger'); } } ``` +`setLogger()` の呼び出しには明示的な引数がありません。ファクトリの場合と同じく、オートワイヤリングが渡してくれます。 -beforeCompile() .[method] -========================= +`$this->compiler->getExtensions()` で取得したほかの登録済みの拡張と協調することもできます。クラスやインターフェースで絞り込むこともできます。 -このメソッドは、コンテナが `loadConfiguration` メソッドで個々の拡張機能によって追加されたすべてのサービス、およびユーザー設定ファイルによって追加されたすべてのサービスを含む時点で呼び出されます。したがって、ビルドのこの段階で、サービス定義を修正したり、それらの間の関連を補完したりできます。コンテナ内のサービスをタグで検索するには `findByTag()` メソッドを、クラスまたはインターフェースで検索するには `findByType()` メソッドを利用できます。 +```php +foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { + // ... +} +``` + + +afterCompile(Nette\PhpGenerator\ClassType $class) .[method] +----------------------------------------------------------- + +最後の段階で、コンテナのクラスが [ClassType |php-generator:#クラス]オブジェクト([PHP Generator |php-generator:]ライブラリのもの)として生成されます。そこにはサービスごとのファクトリメソッドが入っていて、これからキャッシュに書き込まれます。そのコードはまだ変更できます。 ```php -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +この段階が必要になることはめったにありません。アプリケーションの起動時に走るコードを足したいなら、代わりに初期化を使ってください。 + + +初期化のコード +------- + +ここまでの段階はすべて、コンテナがどう*構築される*かに影響します。それに加えて拡張は、コンテナが作られた直後、つまり*実行時*に走るコードも出力できます。たとえばセッションを始めたり、サービスを起動したりするためです。そのコードは `$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 ヘッダーの送信などに初期化を使っています。そして覚えておいてください。拡張のほかの部分と違い、このコードは**リクエストごとに**走るので、小さく保ちましょう。 + -afterCompile() .[method] -======================== +ContainerBuilder +================ -この段階では、コンテナクラスはすでに [ClassType |php-generator:#クラス] オブジェクトの形式で生成されており、サービスを作成するすべてのメソッドを含み、キャッシュへの書き込み準備ができています。この時点で、結果のクラスコードをさらに修正できます。 +[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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +典型的な用途は、開発モードのときだけ Tracy のパネルを登録することです。 + -$initialization .[method] -========================= +複雑な引数 +----- -Configuratorクラスは、[コンテナ作成後 |application:bootstrapping#index.php] に初期化コードを呼び出します。これは、[メソッド addBody() |php-generator:#メソッドと関数の本体] を使用して `$this->initialization` オブジェクトに書き込むことによって作成されます。 +ファクトリや setup の呼び出しの引数が、素の値、クラス名、`@service` の参照のいずれでもないことがあります。そうした場合のために次のものがあります。 -例えば、初期化コードでセッションを開始したり、`run` タグを持つサービスを開始したりする方法の例を示します: +- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - その場で作られるオブジェクト。引数として使う「無名のサービス」です +- `new Nette\DI\Definitions\Reference('blog.articles')` - サービスへの参照。`@name` という文字列のオブジェクト版です +- `$builder::literal('PHP_SAPI')` - 生成されるコンテナにそのまま挿入される生の PHP コード + +例として、Tracy のパネルを登録してみましょう。 ```php -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // セッションの自動開始 - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // このタグは、書き出しが制限されていても常に書き出されます + $this->compiler->addExportedTag('event.subscriber'); - // run タグを持つサービスはコンテナのインスタンス化後に作成される必要がある - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // この型は常に 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 index f3416962b4..4dec2f51be 100644 --- a/dependency-injection/ja/factory.texy +++ b/dependency-injection/ja/factory.texy @@ -1,12 +1,12 @@ -生成されたファクトリ +生成されるファクトリ ********** .[perex] -Nette DIはインターフェースに基づいてファクトリコードを自動生成でき、コード記述の手間を省きます。 +Nette DI はインターフェースにもとづいてファクトリのコードを自動生成できるので、あなたがコードを書く手間が省けます。 -ファクトリは、オブジェクトを製造し設定するクラスです。したがって、それらの依存関係も渡します。デザインパターンの*ファクトリメソッド*と混同しないでください。これはファクトリの特定の利用方法を説明するものであり、このトピックとは関係ありません。 +ファクトリとは、オブジェクトを作り、その依存関係を渡す役目を持つクラスです。*factory method* デザインパターンと混同しないでください。あちらはファクトリの特定の使い方を述べたもので、この話題とは関係がありません。 -そのようなファクトリがどのように見えるかは、[導入章 |introduction#ファクトリ]で示しました: +そうしたファクトリがどんな形になるかは、[はじめの章 |introduction#ファクトリ]で示しました。 ```php class ArticleFactory @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DIはファクトリコードを自動生成できます。あなたがする必要があるのはインターフェースを作成することだけで、Nette DIが実装を生成します。インターフェースは、`create` という名前のメソッドを正確に1つ持ち、戻り値の型を宣言する必要があります: +Nette DI はファクトリのコードを自動生成できます。あなたはインターフェースを作るだけで、実装は Nette DI が生成します。インターフェースには `create` という名前のメソッドがちょうどひとつあり、戻り値の型が宣言されている必要があります。 ```php interface ArticleFactory @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -つまり、`ArticleFactory` ファクトリには、`Article` オブジェクトを作成する `create` メソッドがあります。`Article` クラスは、例えば次のようになります: +つまりファクトリ `ArticleFactory` には、`Article` オブジェクトを作る `create` メソッドがあります。`Article` クラスはたとえば次のようなものです。 ```php class Article @@ -44,16 +44,16 @@ class Article } ``` -ファクトリを設定ファイルに追加します: +ファクトリを設定ファイルに足します。 ```neon services: - ArticleFactory ``` -Nette DIは対応するファクトリの実装を生成します。 +Nette DI が対応するファクトリの実装を生成します。 -ファクトリを使用するコード内で、インターフェースに基づいてオブジェクトを要求し、Nette DIは生成された実装を使用します: +ファクトリを使うコードでは、インターフェースでそのオブジェクトを要求すれば、Nette DI が生成された実装を渡してくれます。 ```php class UserController @@ -65,17 +65,17 @@ class UserController public function foo() { - // ファクトリにオブジェクトを作成させる + // ファクトリにオブジェクトを作らせます $article = $this->articleFactory->create(); } } ``` -パラメータ化されたファクトリ -============== +パラメータ付きのファクトリ +============= -ファクトリメソッド `create` はパラメータを受け取ることができ、その後それらをコンストラクタに渡します。例えば、`Article` クラスに記事の著者IDを追加しましょう: +ファクトリのメソッド `create` はパラメータを受け取り、それをコンストラクタに渡せます。たとえば `Article` クラスに記事の著者の ID を足してみましょう。 ```php class Article @@ -88,7 +88,7 @@ class Article } ``` -パラメータをファクトリにも追加します: +ファクトリにもパラメータを足します。 ```php interface ArticleFactory @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -コンストラクタのパラメータとファクトリのパラメータが同じ名前であるという理由で、Nette DIはそれらを完全に自動的に渡します。 +コンストラクタのパラメータ名(`$authorId`)とファクトリのメソッドのパラメータ名が一致しているので、Nette DI が自動的に渡します。 高度な定義 ===== -定義は、`implement` キーを使用して複数行形式で記述することもできます: +定義は `implement` キーを使って複数行の形でも書けます。 ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -この長い形式で記述する場合、通常のサービスと同様に、`arguments` キーでコンストラクタ用の追加の引数を指定し、`setup` で追加の設定を行うことが可能です。 +この長い形を使うと、通常のサービスの定義と同じように、`arguments` キーでコンストラクタの追加の引数を指定したり、`setup` でさらに設定したりできます。 -例:`create()` メソッドが `$authorId` パラメータを受け取らない場合、設定内で固定値を指定でき、それが `Article` のコンストラクタに渡されます: +例: `create()` メソッドが `$authorId` パラメータを受け取らない場合、`Article` のコンストラクタに渡す固定の値を設定で与えられます。 ```neon services: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -または逆に、`create()` が `$authorId` パラメータを受け取るが、コンストラクタの一部ではなく、`Article::setAuthorId()` メソッドによって渡される場合、`setup` セクションでそれを参照します: +逆に `create()` が `$authorId` を受け取るものの、それがコンストラクタの一部ではなく `Article::setAuthorId()` のようなメソッドで渡される場合は、`setup` セクションでそのパラメータを参照します。 ```neon services: @@ -137,11 +137,11 @@ services: アクセサ ==== -Netteは、ファクトリに加えて、いわゆるアクセサも生成できます。これらは、DIコンテナから特定のサービスを返す `get()` メソッドを持つオブジェクトです。`get()` を繰り返し呼び出すと、常に同じインスタンスが返されます。 +ファクトリのほかに、Nette はいわゆるアクセサも生成できます。これは `get()` メソッドを持つオブジェクトで、DI コンテナから特定のサービスを返します。`get()` を繰り返し呼んでも常に同じインスタンスが返ります。 -アクセサは依存関係に遅延ロードを提供します。特別なデータベースにエラーを書き込むクラスを考えてみましょう。このクラスがデータベース接続をコンストラクタの依存関係として渡させていた場合、実際にはエラーは例外的にしか発生せず、したがってほとんどの場合、接続は未使用のままになるでしょうが、接続は常に作成される必要があったでしょう。 その代わりに、クラスはアクセサを渡し、その `get()` が呼び出されたときに初めてデータベースオブジェクトが作成されます: +アクセサは依存関係の遅延読み込みを可能にします。エラーを専用のデータベースに記録するクラスを考えてみてください。このクラスがコンストラクタでデータベース接続を注入されるなら、エラーがめったに起きず接続がほとんど使われない場合でも、接続は常に確立されてしまいます。代わりにアクセサを受け取れば、データベースのオブジェクト(接続)はアクセサの `get()` メソッドが最初に呼ばれたときにはじめて作られます。 -アクセサを作成するには?インターフェースを書くだけで、Nette DIが実装を生成します。インターフェースは、`get` という名前のメソッドを正確に1つ持ち、戻り値の型を宣言する必要があります: +アクセサはどう作るのでしょうか。インターフェースを書くだけで、実装は Nette DI が生成します。インターフェースには `get` という名前のメソッドがちょうどひとつあり、パラメータを取らず、戻り値の型が宣言されている必要があります。 ```php interface PDOAccessor @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -アクセサを設定ファイルに追加します。そこには、それが返すサービス定義も含まれます: +アクセサを、それが返すべきサービスの定義とともに設定ファイルに足します。 ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -なぜなら、アクセサは `PDO` 型のサービスを返し、設定にはそのようなサービスが1つしかないため、まさにそれを返します。その型のサービスが複数ある場合、返されるサービスを名前を使用して指定します。例:`- PDOAccessor(@db1)`。 +アクセサは `PDO` サービスを返し、設定にはそのサービスがひとつしか定義されていないので、アクセサはそのサービスを返します。その型のサービスが複数ある場合は、アクセサがどれを返すべきかを名前で指定してください。たとえば `- PDOAccessor(@db1)` です。 -複数ファクトリ/アクセサ -============ -私たちのファクトリとアクセサは、これまでは常に1つのオブジェクトしか製造または返せませんでした。しかし、アクセサと組み合わせた複数ファクトリを非常に簡単に作成できます。そのようなクラスのインターフェースは、`create<name>()` および `get<name>()` という名前の任意の数のメソッドを含むでしょう。例: +マルチファクトリ/アクセサ +============= + +ここまでのファクトリとアクセサは、1 種類のオブジェクトしか作れず、また返せませんでした。しかしファクトリとアクセサの機能を組み合わせたマルチファクトリも簡単に作れます。そうした部品のインターフェースには、`create<Name>()` と `get<Name>()` という名前のメソッドを複数書けます。たとえば次のようにです。 ```php interface MultiFactory @@ -173,9 +174,9 @@ interface MultiFactory } ``` -したがって、いくつかの生成されたファクトリとアクセサを渡す代わりに、より多くのことができる1つのより複雑なファクトリを渡します。 +つまり個々のファクトリやアクセサをいくつも注入する代わりに、ひとつのより包括的な部品を注入できます。 -あるいは、いくつかのメソッドの代わりにパラメータ付きの `get()` を使用できます: +複数のメソッドの代わりに、パラメータ付きの `get()` を使うこともできます。 ```php interface MultiFactoryAlt @@ -184,22 +185,24 @@ interface MultiFactoryAlt } ``` -その場合、`MultiFactory::getArticle()` は `MultiFactoryAlt::get('article')` と同じことをするということが成り立ちます。しかしながら、代替の記法には、どの `$name` の値がサポートされているかが明らかではないという欠点があり、論理的にもインターフェースで異なる `$name` に対して異なる戻り値を区別することはできません。 +このとき `MultiFactory::getDb()` は `MultiFactoryAlt::get('db')` と同じことをします。ただしこの書き方には、`$name` に渡せる値がインターフェースのシグネチャから明らかでないという欠点があります。さらに、`$name` の値ごとに異なる戻り値の型をインターフェースで定義することもできません。 + +`get($name)` の代わりに、インターフェースは `create($name)` を宣言することもでき、こちらは呼ぶたびに新しいインスタンスを返します(`get()` は共有されたものを返します)。インターフェースに書けるパラメータ付きのメソッドはひとつだけです。メソッドの戻り値の型が nullable(たとえば `?PDO`)なら、未知の `$name` に対して例外を投げる代わりに `null` を返します。 リストによる定義 -------- -この方法で、設定内で複数ファクトリを定義できます: .{data-version:3.2.0} +設定では、サービスをその場に書くリストの形でマルチファクトリを定義できます。 .{data-version:3.2.0} ```neon services: - MultiFactory( - article: Article # createArticle() を定義します + article: Article() # createArticle() を定義します db: PDO(%dsn%, %user%, %password%) # getDb() を定義します ) ``` -または、ファクトリの定義内で、参照を使用して既存のサービスを参照できます: +あるいは、マルチファクトリの定義の中で参照を使って既存のサービスを指すこともできます。 ```neon services: @@ -215,12 +218,16 @@ services: タグによる定義 ------- -2番目の選択肢は、定義に[タグ |services#タグ]を利用することです: +マルチファクトリを定義するもうひとつの方法は[タグ |services#タグ]を使うことです。タグの値が対応するメソッドの名前を決めます。 ```neon services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index 2e8beb4533..c67766703e 100644 --- a/dependency-injection/ja/faq.texy +++ b/dependency-injection/ja/faq.texy @@ -1,94 +1,94 @@ -DIに関するよくある質問(FAQ) -***************** +DI のよくある質問(FAQ) +*************** -DIはIoCの別名ですか? -------------- +DI は IoC の別名ですか? +---------------- -*Inversion of Control*(IoC)は、コードがどのように実行されるかに焦点を当てた原則です - あなたのコードが外部のコードを実行するのか、それともあなたのコードが外部のコードに統合され、その後呼び出されるのか。 IoCは、[イベント |nette:glossary#イベント]、いわゆる[ハリウッド原則 |application:components#ハリウッドスタイル]、およびその他の側面を含む広範な概念です。 この概念の一部には、[ルールNo.3:ファクトリに任せる |introduction#ルール3 ファクトリに任せる]で説明されているファクトリも含まれ、これらは`new`演算子の逆転を表します。 +*制御の反転*(Inversion of Control、IoC)は、プログラムの中で制御がどう流れるかを述べる原則です。あなたのコードが外部のコードを呼ぶのか、それとも(フレームワークのような)外部のコードがあなたのコードを呼ぶのか、という話です。IoC は幅広い概念で、[イベント |nette:glossary#イベント]、いわゆる[ハリウッドの原則 |application:components#ハリウッド流]などを含みます。[原則 3: ファクトリに任せる |introduction#原則 3: ファクトリに任せる]で扱うファクトリもこの概念に含まれ、`new` 演算子の反転にあたります。 -*Dependency Injection*(DI)は、あるオブジェクトが別のオブジェクト、つまりその依存関係についてどのように知るかに焦点を当てています。これは、オブジェクト間で依存関係を明示的に渡すことを要求する設計パターンです。 +*依存性注入*(Dependency Injection、DI)は、オブジェクトがどうやって依存関係(つまり一緒に働く必要のあるほかのオブジェクト)を手に入れるかに焦点を当てます。オブジェクト自身に作らせたり探させたりするのではなく、依存関係を明示的に渡すことを勧めるデザインパターンです。 -したがって、DIはIoCの特定の形式であると言えます。ただし、すべての形式のIoCがコードの純粋性の観点から適切であるわけではありません。たとえば、アンチパターンの中には、[グローバル状態 |global-state]を操作する手法や、いわゆる[サービスロケータ |#サービスロケータとは何ですか]があります。 +ですから DI は IoC の特定の形と見なせます。ただし IoC のすべての形がきれいなコードを促すわけではありません。たとえば[グローバル状態|global-state]や [Service Locator |#Service Locator とは何ですか?]パターンに頼る手法は、アンチパターンです。 -サービスロケータとは何ですか? ---------------- +Service Locator とは何ですか? +----------------------- -これはDependency Injectionの代替案です。利用可能なすべてのサービスまたは依存関係が登録される中央リポジトリを作成することで機能します。オブジェクトが依存関係を必要とするとき、Service Locatorにそれを要求します。 +依存性注入に代わる方法です。中心となるオブジェクト(ロケーター)があり、利用できるすべてのサービス(依存関係)がそこに登録されます。オブジェクトが依存関係を必要とすると、Service Locator に要求します。 -ただし、Dependency Injectionと比較して、透明性が失われます:依存関係はオブジェクトに直接渡されず、簡単には識別できないため、すべての関連性を明らかにし理解するにはコードを調査する必要があります。テストもより複雑になります。なぜなら、テスト対象のオブジェクトにモックオブジェクトを単純に渡すのではなく、Service Locatorを介して行う必要があるからです。さらに、Service Locatorはコードの設計を損ないます。個々のオブジェクトはその存在を知る必要があるため、オブジェクトがDIコンテナを認識しないDependency Injectionとは異なります。 +しかし DI と比べると透明性に欠けます。依存関係が API(コンストラクタやメソッド)に現れず、オブジェクトのコードの中(ロケーターの呼び出し)に隠れてしまうので、つながりを知るにはコードを読まなければなりません。テストも複雑になります。オブジェクトを作るときに単にモックの依存関係を渡すことができず、Service Locator 自体を操作する必要がしばしば出てくるからです。さらに Service Locator は不要な依存関係を持ち込みます。DI ではオブジェクトが理想的にはコンテナの存在を知らずに済むのに対し、こちらではオブジェクトがロケーターと結びついてしまいます。 -DIを使用しない方が良い場合はいつですか? ---------------------- +DI を使わないほうがよいのはどんなときですか? +------------------------ -設計パターンDependency Injectionの使用に関連する既知の困難はありません。逆に、グローバルに利用可能な場所から依存関係を取得することは、[多くの合併症 |global-state]につながり、Service Locatorの使用も同様です。 したがって、常にDIを使用することが推奨されます。これは独断的なアプローチではなく、単により良い代替案が見つからなかったためです。 +依存性注入のデザインパターンを正しく使うことに、知られた大きな欠点はありません。むしろ、グローバルにアクセスできる場所(静的プロパティやシングルトンなど)から依存関係を得ることは[数々の面倒|global-state]につながりますし、Service Locator を使うのも同様です。ですから DI を使うことは一般に常におすすめできます。これは教条ではありません。単に、依存関係をきれいに管理するこれより良い代案が広く受け入れられていない、というだけのことです。 -それでも、オブジェクトを渡さずにグローバル空間から取得する特定の状況があります。たとえば、コードのデバッグ中に、プログラムの特定のポイントで変数の値を出力したり、プログラムの特定の部分の期間を測定したり、メッセージを記録したりする必要がある場合です。 このような場合、後でコードから削除される一時的なタスクである場合、グローバルに利用可能なダンパー、ストップウォッチ、またはロガーを利用することは正当です。これらのツールはコードの設計には属しません。 +とはいえ、オブジェクトにグローバルにアクセスしてもよい、限られた特定の場面はあります。たとえばデバッグ中に、変数の値をダンプしたり、実行時間を測ったり、ある地点でメッセージを記録したりする場合です。あとでコードから取り除く一時的な処置であれば、グローバルにアクセスできるダンパー、タイマー、ロガーを使うのは正当です。これらの道具はアプリケーションの設計の中核ではありません。 -DIの使用には欠点がありますか? ----------------- +DI を使うことに欠点はありますか? +------------------ -Dependency Injectionの使用には、たとえばコード記述の複雑さの増加やパフォーマンスの低下などの欠点がありますか? DIに従ってコードを書き始めたときに何を失いますか? +依存性注入を使うと、コードを書く手間が増えたり性能が落ちたりといった不利益はあるのでしょうか。DI に沿ってコードを書き始めると、私たちは何を失うのでしょうか。 -DIはアプリケーションのパフォーマンスやメモリ要件に影響を与えません。DIコンテナのパフォーマンスが役割を果たす可能性がありますが、[Nette DI |nette-container]の場合、コンテナは純粋なPHPにコンパイルされるため、アプリケーション実行時のオーバーヘッドは基本的にゼロです。 +DI 自体が実行時の性能やメモリ使用量に与える影響はごくわずかです。DI コンテナの性能は要因になり得ますが、[Nette DI |nette-container]はコンテナを素の PHP コードにコンパイルするので、アプリケーションの実行中のオーバーヘッドは事実上ゼロです。 -コードを記述する際には、依存関係を受け入れるコンストラクタを作成する必要がある場合があります。以前は時間がかかる可能性がありましたが、最新のIDEと[コンストラクタプロパティプロモーション |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]のおかげで、現在は数秒の問題です。ファクトリは、Nette DIとPhpStorm用プラグインを使用してマウスをクリックするだけで簡単に生成できます。 一方、シングルトンや静的アクセスポイントを記述する必要はなくなります。 +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 コンテナの設定とファクトリに移るだけです。 -レガシーアプリケーションをDIに書き換える方法は? -------------------------- +古いアプリケーションを DI に書き換えるには? +------------------------ -レガシーアプリケーションからDependency Injectionへの移行は、特に大規模で複雑なアプリケーションの場合、困難なプロセスになる可能性があります。このプロセスに体系的にアプローチすることが重要です。 +古いアプリケーションから依存性注入への移行は、とくに大きく複雑なアプリケーションでは骨の折れる作業になり得ます。この過程には体系的に取り組むことが大切です。 -- Dependency Injectionに移行する際には、チームのすべてのメンバーが使用される原則と手順を理解することが重要です。 -- まず、既存のアプリケーションの分析を実行し、主要なコンポーネントとその依存関係を特定します。どの部分をリファクタリングし、どの順序で行うかの計画を作成します。 -- DIコンテナを実装するか、さらに良いのは、Nette DIなどの既存のライブラリを使用することです。 -- Dependency Injectionを使用するように、アプリケーションの個々の部分を徐々にリファクタリングします。これには、依存関係をパラメータとして受け入れるようにコンストラクタまたはメソッドを変更することが含まれる場合があります。 -- 依存関係を持つオブジェクトが作成されるコード内の場所を変更して、代わりに依存関係がコンテナによって注入されるようにします。これにはファクトリの使用が含まれる場合があります。 +- 依存性注入に移るときは、使う原則と作法をチームの全員が理解していることが大切です。 +- まず既存のアプリケーションを分析し、主要な部品とその依存関係を洗い出します。どの部分をどの順序で書き換えるかの計画を立てます。 +- DI コンテナを実装するか、できれば Nette DI のような既存のライブラリを使いましょう。 +- アプリケーションの各部分を、依存性注入を使うよう少しずつ書き換えます。コンストラクタやメソッドを、依存関係をパラメータとして受け取る形に変えることになるでしょう。 +- オブジェクトを生成しているコードを、コンテナから取得するか、コンテナが提供するファクトリを使うよう更新します。 -Dependency Injectionへの移行は、コードの品質とアプリケーションの長期的な保守性への投資であることを忘れないでください。これらの変更を行うのは困難な場合がありますが、結果は、将来の拡張と保守に対応できる、よりクリーンで、よりモジュール化され、テストしやすいコードになるはずです。 +依存性注入への移行は、コードの品質とアプリケーションの長期的な保守性への投資であることを覚えておいてください。これらの変更は大変かもしれませんが、その結果はよりきれいで、モジュール化され、テストしやすく、将来の拡張と保守に備えたコードになるはずです。 -なぜ継承よりもコンポジションが優先されるのですか? -------------------------- -変更の影響を心配することなくコードを再利用するために、[継承 |nette:introduction-to-object-oriented-programming#コンポジション]の代わりに[コンポジション |nette:introduction-to-object-oriented-programming#継承]を使用する方が適切です。したがって、あるコードの変更が他の依存コードの変更を必要とすることを心配する必要がない、より緩やかな結合を提供します。典型的な例は、[コンストラクタ地獄 |passing-dependencies#コンストラクタ地獄]と呼ばれる状況です。 +なぜ継承よりコンポジションが好まれるのですか? +----------------------- +コードの再利用には、[継承 |nette:introduction-to-object-oriented-programming#合成]より[コンポジション |nette:introduction-to-object-oriented-programming#継承]が一般に好まれます。結びつきがより緩やかになるからです。コンポジションなら、基底クラスの変更が依存するサブクラスを壊す問題に出会いにくくなります。典型的な例が、[コンストラクタ地獄 |passing-dependencies#コンストラクタ地獄]と呼ばれる状況です。 -Nette DIコンテナをNette以外で使用できますか? ------------------------------ +Nette DI Container は Nette の外でも使えますか? +------------------------------------- -もちろんです。Nette DIコンテナはNetteの一部ですが、フレームワークの他の部分から独立して使用できるスタンドアロンライブラリとして設計されています。Composerを使用してインストールし、サービスを定義する設定ファイルを作成し、数行のPHPコードを使用してDIコンテナを作成するだけです。 そして、すぐにプロジェクトでDependency Injectionの利点を活用し始めることができます。 +もちろんです。Nette DI Container は Nette の一部ですが、フレームワークのほかの部分と切り離して使える独立したライブラリとして設計されています。Composer でインストールし、サービスを定義する設定ファイルを作り、数行の PHP コードで DI コンテナを作るだけです。すぐにあなたのプロジェクトで依存性注入の恩恵を受け始められます。 -コードを含む具体的な使用方法は、[Nette DIコンテナ |nette-container]の章で説明されています。 +[Nette DI Container |nette-container]の章で、コードの例とともに具体的な使い方を説明しています。 -なぜ設定はNEONファイルにあるのですか? +なぜ設定は NEON ファイルなのですか? --------------------- -NEONは、アプリケーション、サービス、およびそれらの依存関係を設定するためにNette内で開発された、シンプルで読みやすい設定言語です。JSONやYAMLと比較して、この目的のためにはるかに直感的で柔軟なオプションを提供します。NEONでは、Symfony&YAMLではまったく記述できないか、複雑な記述によってのみ記述できる関連性を自然に記述できます。 +NEON は、アプリケーション、サービス、その依存関係を設定するために Nette の中で生まれた、単純で読みやすい設定言語です。JSON や YAML と比べて、この目的にははるかに直感的で柔軟な選択肢を提供します。NEON なら、JSON や YAML では同じくらい明快に表すのが難しい、あるいは不可能なサービスの定義や関係を、自然に書けます。 -NEONファイルの解析はアプリケーションを遅くしませんか? ------------------------------ +NEON ファイルの解析はアプリケーションを遅くしませんか? +------------------------------ -NEONファイルは非常に高速に解析されますが、この点はまったく重要ではありません。理由は、ファイルの解析はアプリケーションの初回実行時に一度だけ行われるためです。その後、DIコンテナのコードが生成され、ディスクに保存され、それ以降のリクエストごとに実行され、追加の解析を行う必要はありません。 +NEON ファイルの解析はとても速いのですが、その速さは本番ではほとんど関係ありません。設定ファイルが解析されるのは、アプリケーションが最初に動くとき(または設定が変わったとき)の一度きりだからです。解析後は DI コンテナのコードが生成されてキャッシュされ(ディスクに保存され)、以降のリクエストではこのコンパイル済みの PHP コードが実行されるので、それ以上の解析は不要です。 -これは本番環境での動作方法です。開発中は、開発者が常に最新のDIコンテナを持つように、内容が変更されるたびにNEONファイルが解析されます。前述のように、解析自体は一瞬の問題です。 +これが本番環境での動きです。開発中は、内容が変わるたびに NEON ファイルが解析されるので、開発者は常に最新の DI コンテナを手にできます。すでに述べたとおり、解析そのものはとても高速です。 -自分のクラスから設定ファイル内のパラメータにアクセスするにはどうすればよいですか? ------------------------------------------ +設定ファイルのパラメータにクラスからアクセスするには? +--------------------------- -[ルールNo.1:渡してもらう |introduction#ルール1 渡してもらう]を覚えておきましょう。クラスが設定ファイルからの情報を必要とする場合、その情報にアクセスする方法を考える必要はありません。代わりに、単純にそれを要求します - たとえば、クラスのコンストラクタを介して。そして、設定ファイルで受け渡しを行います。 +[原則 1: 渡してもらう |introduction#原則 1: 渡してもらう]を思い出してください。クラスが設定ファイルの情報を必要とするなら、そのクラスがどうやってそれを*取ってくる*かを考えてはいけません。代わりに、たとえばクラスのコンストラクタでそれを要求するだけです。そして設定ファイルでその値を与えます。 -この例では、`%myParameter%`はパラメータ`myParameter`の値のプレースホルダであり、クラス`MyClass`のコンストラクタに渡されます: +次の例では、`%myParameter%` が `myParameter` パラメータの値のプレースホルダーで、`MyClass` のコンストラクタに渡されます。 -```php +```neon # config.neon parameters: myParameter: Some value @@ -97,10 +97,27 @@ services: - MyClass(%myParameter%) ``` -複数のパラメータを渡す場合やautowiringを利用したい場合は、[パラメータをオブジェクトにラップする |best-practices:passing-settings-to-presenters]ことをお勧めします。 +複数のパラメータを渡したい場合やオートワイヤリングを使いたい場合は、[パラメータをオブジェクトにまとめる |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]を作れます。 -NetteはPSR-11: Container interfaceをサポートしていますか? +container、compiler、definition などの用語は何を意味しますか? --------------------------------------------- -Nette DIコンテナはPSR-11を直接サポートしていません。ただし、Nette DIコンテナとPSR-11 Container Interfaceを期待するライブラリまたはフレームワークとの間で相互運用性が必要な場合は、Nette DIコンテナとPSR-11の間のブリッジとして機能する[単純なアダプタ |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f]を作成できます。 +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 index a4ff9c52ef..80decd04ad 100644 --- a/dependency-injection/ja/global-state.texy +++ b/dependency-injection/ja/global-state.texy @@ -2,42 +2,42 @@ ************** .[perex] -警告:以下の構造は、設計の悪いコードの兆候です: +注意: 次のような書き方は、設計のよくないコードの症状です。 - `Foo::getInstance()` - `DB::insert(...)` - `Article::setDb($db)` - `ClassName::$var` または `static::$var` -これらの構造のいずれかがあなたのコードに存在しますか? それなら、それを改善する機会があります。これらは、さまざまなライブラリやフレームワークのサンプルソリューションでも見られる一般的な構造だと思うかもしれません。もしそうなら、それらのコードの設計は良くありません。 +あなたのコードにこうした書き方はありますか。もしあるなら、改善の余地があります。よくある書き方で、さまざまなライブラリやフレームワークの解決例でも見かける、と思うかもしれません。もしそうなら、それらのコードの設計には欠陥があります。 -ここでは、学術的な純粋さについて話しているのではありません。これらの構造はすべて、共通点が1つあります:グローバル状態を利用しています。そして、それはコードの品質に破壊的な影響を与えます。クラスはその依存関係について嘘をつきます。コードは予測不可能になります。プログラマーを混乱させ、効率を低下させます。 +ここで学問的な純粋さの話をしているのではありません。これらの書き方にはひとつの共通点があります。グローバル状態を使っていることです。そしてグローバル状態はコードの品質に有害な影響を与えます。クラスは自分の依存関係について嘘をつくようになります。コードは予測できなくなります。開発者を混乱させ、その効率を下げます。 -この章では、なぜそうなるのか、そしてグローバル状態を回避する方法について説明します。 +この章では、なぜそうなるのか、そしてどうやってグローバル状態を避けるのかを説明します。 -グローバル結合 -------- +グローバルな絡み合い +---------- -理想的な世界では、オブジェクトは[直接渡された |passing-dependencies]オブジェクトとのみ通信できるべきです。2つのオブジェクト `A` と `B` を作成し、それらの間で参照を渡さなければ、`A` も `B` も他のオブジェクトにアクセスしたり、その状態を変更したりすることはできません。これはコードの非常に望ましい特性です。バッテリーと電球を持っているようなものです。バッテリーとワイヤーで接続しない限り、電球は点灯しません。 +理想の世界では、オブジェクトは[直接渡された |passing-dependencies]オブジェクトとだけやり取りするはずです。オブジェクト `A` と `B` を作り、両者のあいだで参照を一度も渡さなければ、`A` も `B` も互いの状態にアクセスしたり変えたりできません。これはコードにとって非常に望ましい性質です。電池と電球のようなもので、電球は導線で電池につながないと光りません。 -しかし、これはグローバル(静的)変数やシングルトンには当てはまりません。オブジェクト `A` は、参照を渡さずに `C::changeSomething()` を呼び出すことで、*ワイヤレス*でオブジェクト `C` にアクセスして変更できます。オブジェクト `B` もグローバル `C` をつかむと、`A` と `B` は `C` を介して相互に影響を与えることができます。 +しかしグローバル(静的)変数やシングルトンでは、そうはいきません。オブジェクト `A` は参照を渡されなくても、`C::changeSomething()` を呼ぶことで*無線で*オブジェクト `C` にアクセスして変更できます。オブジェクト `B` もグローバルな `C` に手を伸ばせば、`A` と `B` は `C` を通じて互いに影響し合えてしまいます。 -グローバル変数の使用は、外部からは見えない新しい形式の*ワイヤレス*結合をシステムにもたらします。コードの理解と使用を複雑にする煙幕を作り出します。開発者が依存関係を真に理解するには、ソースコードのすべての行を読む必要があります。クラスのインターフェースに精通するだけではありません。さらに、これは完全に不要な結合です。グローバル状態は、どこからでも簡単にアクセスでき、たとえばグローバル(静的)メソッド `DB::insert()` を介してデータベースに書き込むことができるため使用されます。しかし、これから示すように、それがもたらす利点はごくわずかであり、逆に引き起こす合併症は致命的です。 +グローバル変数を使うと、外からは見えない*無線の*結合という新しい形が持ち込まれます。それは煙幕を作り、コードの理解と利用を難しくします。依存関係を本当に把握するには、開発者はクラスのインターフェースを頼りにするのではなく、ソースコードの一行一行を読まなければなりません。しかもこの結合はまったく不要なものです。グローバル状態が使われるのは、どこからでも簡単にアクセスでき、たとえばグローバル(静的)メソッド `DB::insert()` でデータベースに書き込めるからです。しかしこれから示すとおり、その手軽さは、それがもたらす深刻な面倒に比べればごくわずかなものです。 .[note] -動作の観点からは、グローバル変数と静的変数に違いはありません。どちらも同じように有害です。 +振る舞いという点で、グローバル変数と静的変数に違いはありません。どちらも同じくらい有害です。 -遠隔での不気味な作用 ----------- +遠隔からの不気味な作用 +----------- -「遠隔での不気味な作用」 - 1935年にアルベルト・アインシュタインが、彼に鳥肌を立たせた量子物理学の現象をそう名付けました。 -これは量子もつれであり、その特徴は、一方の粒子に関する情報を測定すると、たとえそれらが何百万光年も離れていても、即座に他方の粒子に影響を与えることです。 これは、光よりも速く何も伝播できないという宇宙の基本法則に明らかに違反しているように見えます。 +「遠隔からの不気味な作用」。アルベルト・アインシュタインが、量子物理学のある現象を指してそう呼んだのは有名です。彼はそれに不気味さを感じていました。 +それは量子もつれのことで、一方の粒子の性質を測ると、たとえ何百万光年離れていても、もつれたもう一方の粒子に瞬時に影響します。光より速く伝わるものはないという宇宙の基本法則に、一見反しているように見えます。 -ソフトウェアの世界では、「遠隔での不気味な作用」とは、分離されていると信じているプロセス(参照を渡さなかったため)を実行するが、システムの遠隔地で予期しない相互作用や状態の変化が発生し、それについて知らなかった状況を指すことができます。これはグローバル状態を介してのみ発生する可能性があります。 +ソフトウェアの世界では、「遠隔からの不気味な作用」は次のような状況を指します。(依存関係を明示的に渡していないので)独立していると思っていた処理を実行したのに、システムの遠い場所で思いがけないやり取りと状態の変化が、こちらの知らないうちに起きるのです。これはグローバル状態を通じてしか起こりません。 -広範で成熟したコードベースを持つプロジェクトの開発チームに参加したと想像してください。新しい上司が新しい機能の実装を依頼し、あなたは適切な開発者としてテストを書くことから始めます。しかし、プロジェクトに慣れていないため、「このメソッドを呼び出すとどうなるか」のような探索的なテストをたくさん行います。そして、次のテストを書いてみます: +大きく成熟したコードベースを持つプロジェクトの開発チームに加わったところを想像してください。新しいリーダーから機能をひとつ実装するよう頼まれ、良い開発者らしく、まずテストを書き始めます。ただしプロジェクトに慣れていないので、「このメソッドを呼んだら何が起こるのか」といった探りのテストをたくさん書きます。そして次のようなテストを書いてみます。 ```php function testCreditCardCharge() @@ -47,17 +47,17 @@ function testCreditCardCharge() } ``` -コードを実行し、おそらく数回実行した後、しばらくして、実行するたびにクレジットカードから100ドルが引き落とされているという銀行からの通知が携帯電話に表示されることに気づきます 🤦‍♂️ +コードを何度か実行し、しばらくすると携帯に銀行の通知が届きます。実行のたびにクレジットカードから 100 ドルが引き落とされていたのです。🤦‍♂️ -一体どうしてテストが実際のお金の引き落としを引き起こしたのでしょうか? クレジットカードの操作は簡単ではありません。サードパーティのWebサービスと通信する必要があり、そのWebサービスのURLを知る必要があり、ログインする必要があり、などなど。 これらの情報はテストには含まれていません。さらに悪いことに、これらの情報がどこにあるのかさえわからないため、実行するたびに再び100ドルが引き落とされることがないように外部依存関係をモックする方法もわかりません。そして、新しい開発者として、これから行うことが100ドル貧しくなることにつながることをどうやって知ることができたのでしょうか? +いったいどうしてテストが実際の請求を引き起こせるのでしょうか。クレジットカードを扱うのは単純ではありません。第三者のウェブサービスとやり取りし、その URL を知り、認証する必要があります。そうした情報はテストのどこにも書かれていません。さらに悪いことに、その情報がどこにあるかも分からないので、外部の依存関係をモックに置き換えて、テストのたびに 100 ドル取られるのを防ぐこともできません。新入りの開発者に、これからやろうとしていることが 100 ドルの損失につながると、どうやって分かれというのでしょうか。 -これが遠隔での不気味な作用です! +これが遠隔からの不気味な作用です。 -プロジェクト内の結合がどのように機能するかを理解するまで、多くのソースコードを長時間掘り下げ、年上で経験豊富な同僚に尋ねるしかありません。 これは、クラス `CreditCard` のインターフェースを見ても、初期化する必要があるグローバル状態を特定できないためです。クラスのソースコードを見ても、どの初期化メソッドを呼び出す必要があるかはわかりません。最良の場合、アクセスされるグローバル変数を見つけて、そこから初期化方法を推測しようとすることができます。 +あなたは大量のソースコードをふるいにかけ、先輩に相談してプロジェクトの絡み合いを理解するはめになります。この困難が生じるのは、`CreditCard` クラスのインターフェースが、必要なグローバル状態の初期化を明かさないからです。クラスのソースコードを見ても、どの初期化メソッドを呼ぶべきか分からないかもしれません。よくても、アクセスされているグローバル変数を見つけて、その初期化の仕方を推し量ろうとするくらいです。 -このようなプロジェクトのクラスは病的な嘘つきです。クレジットカードは、インスタンス化して `charge()` メソッドを呼び出すだけで十分であるかのように装います。しかし、舞台裏では、支払いゲートウェイを表す別のクラス `PaymentGateway` と協力しています。そのインターフェースも、単独で初期化できると言っていますが、実際には、ある設定ファイルなどから資格情報を取得します。 このコードを書いた開発者には、`CreditCard` が `PaymentGateway` を必要とすることは明らかです。彼らはこの方法でコードを書きました。しかし、プロジェクトに新しい人にとっては、それは完全な謎であり、学習を妨げます。 +こうしたプロジェクトのクラスは、病的な嘘つきです。`CreditCard` クラスは、ただインスタンス化して `charge()` メソッドを呼べばよいかのように振る舞います。しかし裏では、決済ゲートウェイを表す別のクラス `PaymentGateway` とやり取りしています。`PaymentGateway` のインターフェースも独立して初期化できるように見えるかもしれませんが、実際には設定ファイルから認証情報を引っ張ってきていたりします。もとの開発者たちは `CreditCard` が `PaymentGateway` を必要とすることを分かっています。そういうコードを書いたのですから。しかし新しく来た人にとっては完全な謎で、学ぶことにも貢献することにも立ちはだかります。 -状況を修正するにはどうすればよいですか? 簡単です。**APIに依存関係を宣言させます。** +この状況をどう直せばよいでしょうか。簡単です。**API に依存関係を宣言させるのです。** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -コード内の結合が突然どのように明らかになるかに注目してください。`charge()` メソッドが `PaymentGateway` を必要とすることを宣言することで、コードがどのように結合されているかを誰かに尋ねる必要はありません。そのインスタンスを作成する必要があることを知っており、そうしようとすると、アクセスパラメータを提供する必要があることに気づきます。それらがなければ、コードを実行することさえできません。 +コードの中の相互依存が一目で分かるようになったことに注目してください。`charge()` メソッドが `PaymentGateway` を必要とすると宣言しているので、この依存関係を推し量ったり人に聞いたりする必要はもうありません。インスタンスを作る必要があると分かり、その過程で必要なアクセス情報にも行き当たります。それがなければコードはそもそも動きません。 -そして最も重要なことは、これで支払いゲートウェイをモックできるため、テストを実行するたびに100ドルが請求されることはありません。 +そして何より、決済ゲートウェイをモックにできるので、テストを走らせるたびに 100 ドル取られることもなくなります。 -グローバル状態により、オブジェクトはAPIで宣言されていないものに密かにアクセスできるようになり、結果としてAPIを病的な嘘つきにしてしまいます。 +グローバル状態は、API に宣言されていない依存関係にオブジェクトがこっそりアクセスすることを許し、あなたの API を病的な嘘つきに変えてしまいます。 -以前はこのように考えていなかったかもしれませんが、グローバル状態を使用するたびに、秘密のワイヤレス通信チャネルを作成しています。遠隔での不気味なアクションは、開発者に潜在的な相互作用を理解するためにコードのすべての行を読むことを強制し、開発者の生産性を低下させ、新しいチームメンバーを混乱させます。 あなたがコードを作成した人なら、実際の依存関係を知っていますが、あなたの後に来る人は誰でも途方に暮れます。 +これまでそう考えたことはなかったかもしれませんが、グローバル状態を使うたびに、あなたは秘密の無線通信路を作っているのです。この遠隔からの不気味な作用は、起こり得るやり取りを理解するために開発者にコードの一行一行を読むことを強い、生産性を下げ、新しいチームメンバーを混乱させます。そのコードを書いたあなたは本当の依存関係を知っていますが、あとから来る人には何も分かりません。 -グローバル状態を利用するコードを書かないでください。依存関係の受け渡しを優先してください。つまり、依存性注入です。 +グローバル状態に頼るコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。 -グローバル状態の脆弱性 +グローバル状態のもろさ ----------- -グローバル状態とシングルトンを使用するコードでは、いつ誰がこの状態を変更したかが決して確実ではありません。このリスクは初期化時にすでに現れます。次のコードはデータベース接続を作成し、支払いゲートウェイを初期化することを目的としていますが、常に例外をスローし、原因を見つけるのは非常に時間がかかります: +グローバル状態やシングルトンを使ったコードでは、その状態がいつ誰によって変えられたのか、決して確信が持てません。この危険は初期化のときにも現れます。次のコードはデータベース接続を作って決済ゲートウェイを初期化しようとしていますが、何度も例外を投げ、その原因を追うのはきわめて骨が折れます。 ```php PaymentGateway::init(); DB::init('mysql:', 'user', 'password'); ``` -`PaymentGateway`オブジェクトが他のオブジェクトにワイヤレスでアクセスし、その一部がデータベース接続を必要とすることを理解するには、コードを詳細に調べる必要があります。したがって、`PaymentGateway`の前にデータベースを初期化する必要があります。しかし、グローバル状態の煙幕はこれをあなたから隠します。個々のクラスのAPIが欺瞞的でなく、依存関係を宣言していたら、どれだけの時間を節約できたでしょうか? +コードを丹念に追ってようやく、`PaymentGateway` オブジェクトが無線でほかのオブジェクトにアクセスしていて、その一部がデータベース接続を必要とすることが分かります。ですからデータベースは `PaymentGateway` より先に初期化しなければなりません。しかしグローバル状態の煙幕がそれを隠します。これらのクラスの API が正直で依存関係を宣言していたら、どれだけの時間が節約できたでしょうか。 ```php $db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -同様の問題は、データベース接続へのグローバルアクセスを使用する場合にも発生します: +データベース接続にグローバルにアクセスする場合も、同じような問題が起こります。 ```php use Illuminate\Support\Facades\DB; @@ -110,9 +110,9 @@ class Article } ``` -`save()`メソッドを呼び出すとき、データベース接続がすでに作成されているかどうか、そして誰がその作成を担当しているかは定かではありません。たとえば、テストのために実行時にデータベース接続を変更したい場合は、おそらく`DB::reconnect(...)`や`DB::reconnectForTest()`などの追加のメソッドを作成する必要があるでしょう。 +`save()` メソッドを呼ぶとき、データベース接続が確立されているのか、誰がそれを確立する責任を持つのかは分かりません。(テストなどのために)データベース接続を動的に変える必要が出てきたら、`DB::reconnect(...)` や `DB::reconnectForTest()` のようなメソッドを足すことになるでしょう。 -例を考えてみましょう: +例を見てみましょう。 ```php $article = new Article; @@ -122,9 +122,9 @@ Foo::doSomething(); $article->save(); ``` -`$article->save()`を呼び出すときに、テストデータベースが実際に使用されているという確信はどこにありますか? `Foo::doSomething()`メソッドがグローバルデータベース接続を変更した場合はどうなりますか? これを確認するには、クラス`Foo`のソースコード、そしておそらく他の多くのクラスを調べる必要があります。しかし、このアプローチは短期的な答えしか提供せず、状況は将来変わる可能性があります。 +`$article->save()` が呼ばれたとき、本当にテスト用のデータベースが使われていると、どうして確信できるでしょうか。`Foo::doSomething()` メソッドがグローバルなデータベース接続を変えていたら? それを確かめるには `Foo` の、そしておそらくほかの多くのクラスのソースコードを調べる必要があります。しかもその調査は一時的な答えしかもたらしません。状況はあとで変わり得るからです。 -そして、データベース接続をクラス`Article`内の静的変数に移動したらどうなりますか? +データベース接続を `Article` クラスの中の静的変数に移したらどうでしょうか。 ```php class Article @@ -143,11 +143,11 @@ class Article } ``` -これは何も変わりません。問題はグローバル状態であり、どのクラスに隠されているかはまったく関係ありません。この場合、前のケースと同様に、`$article->save()`メソッドを呼び出すときに、どのデータベースに書き込まれるかについての手がかりはありません。アプリケーションの反対側の誰かが、いつでも`Article::setDb()`を使用してデータベースを変更できた可能性があります。私たちの手の下で。 +何ひとつ変わりません。問題はグローバル状態そのものであって、それがどのクラスの中に隠れているかは関係ありません。この場合も先ほどと同じく、`$article->save()` が呼ばれたとき、データがどのデータベースに書き込まれるのか確信が持てません。アプリケーションのどこにいる誰でも、`Article::setDb()` を使っていつでもデータベースを変えられたのです。私たちの知らないうちに。 -グローバル状態は、アプリケーションを**非常に脆弱**にします。 +グローバル状態は、アプリケーションを**きわめてもろく**します。 -しかし、この問題に対処する簡単な方法があります。APIに依存関係を宣言させるだけで、正しい機能が保証されます。 +しかしこの問題に対処する簡単な方法があります。正しく機能するために必要なものを、API に依存関係として宣言させればよいのです。 ```php class Article @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -このアプローチのおかげで、データベース接続の隠れた予期しない変更を心配する必要はなくなります。これで、記事がどこに保存されるかが確実になり、他の無関係なクラス内のコードの変更が状況を変えることはできなくなります。コードはもはや脆弱ではなく、安定しています。 +この方法なら、データベース接続が隠れて思いがけず変わる心配がなくなります。記事がどこに保存されるのかを確信でき、無関係なクラスでの変更がそれに影響することもありません。コードはもろさを失い、安定します。 -グローバル状態を利用するコードを書かないでください。依存関係の受け渡しを優先してください。つまり、依存性注入です。 +グローバル状態に頼るコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。 シングルトン ------ -シングルトンは、有名なGang of Fourの出版物からの[定義|https://en.wikipedia.org/wiki/Singleton_pattern] によると、クラスを単一のインスタンスに制限し、それにグローバルアクセスを提供する設計パターンです。このパターンの実装は通常、次のコードに似ています: +シングルトンはデザインパターンのひとつで、有名な Gang of Four の本による[定義 |https://ja.wikipedia.org/wiki/Singleton_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3]によれば、クラスのインスタンスをひとつに制限し、それへのグローバルなアクセスを提供します。このパターンの実装は、ふつう次のようなコードになります。 ```php class Singleton @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // クラスの機能を実行する他のメソッド + // そしてクラスの機能を果たすほかのメソッド } ``` -残念ながら、シングルトンはアプリケーションにグローバル状態を導入します。そして、上で示したように、グローバル状態は望ましくありません。したがって、シングルトンはアンチパターンと見なされます。 +残念ながら、シングルトンはアプリケーションにグローバル状態を持ち込みます。そして上で示したとおり、グローバル状態は望ましくありません。だからこそシングルトンはアンチパターンと見なされます。 -コードでシングルトンを使用せず、他のメカニズムに置き換えてください。シングルトンは本当に必要ありません。ただし、アプリケーション全体でクラスの単一インスタンスの存在を保証する必要がある場合は、[DIコンテナ |container]に任せてください。 これにより、アプリケーションシングルトン、つまりサービスが作成されます。これにより、クラスは自身の独自性を保証すること(つまり、`getInstance()`メソッドと静的変数を持たないこと)をやめ、その機能のみを実行します。したがって、単一責任の原則に違反しなくなります。 +コードでシングルトンを使わず、ほかのしくみで置き換えてください。本当にシングルトンは必要ありません。ただし、アプリケーション全体でクラスのインスタンスがひとつだけであることを保証したいなら、その責任は [DI コンテナ |container]に委ねましょう。こうしてアプリケーションのスコープを持つシングルトン、一般にサービスと呼ばれるものができます。クラス自身は自分の唯一性を管理する務めから解放され(つまり `getInstance()` メソッドも静的なインスタンスのプロパティも持たなくなり)、自分の責務だけに集中できます。こうして単一責任の原則を破らずに済むようになります。 -グローバル状態 対 テスト -------------- +グローバル状態とテスト +----------- -テストを作成するとき、各テストは分離されたユニットであり、外部状態が入力されないことを前提としています。そして、テストから状態は出力されません。テストが完了すると、テストに関連するすべての状態はガベージコレクタによって自動的に削除されるはずです。これにより、テストは分離されます。したがって、テストは任意の順序で実行できます。 +テストを書くとき、私たちは理想的には、各テストが独立した単位であり、外部の状態が入ることも出ることもないと想定します。テストが終われば、それに関わる状態はガベージコレクタが自動的に片づけるはずです。これがテストを独立させます。ですからテストはどんな順序でも実行できます。 -ただし、グローバル状態/シングルトンが存在する場合、これらの快適な前提はすべて崩壊します。状態はテストに入力および出力できます。突然、テストの順序が重要になる可能性があります。 +しかしグローバル状態やシングルトンがあると、この都合のよい前提は崩れます。状態はテストに漏れ込み、また漏れ出します。とたんに、テストの順序が問題になり得ます。 -シングルトンをテストできるようにするために、開発者はしばしば、インスタンスを別のインスタンスに置き換えることを許可するなどして、そのプロパティを緩和する必要があります。このようなソリューションは、せいぜいハックであり、保守や理解が困難なコードを作成します。グローバル状態に影響を与える各テストまたは`tearDown()`メソッドは、これらの変更を元に戻す必要があります。 +シングルトンを含むコードをテストできるようにするためだけに、開発者はしばしばその健全さを妥協せざるを得ません。たとえばシングルトンのインスタンスを差し替えられるようにする、といった具合です。そうした解はよくても場当たりで、保守と理解が難しいコードにつながります。グローバル状態を変えるテスト(やその `tearDown()` メソッド)は、その変更を丹念に戻さなければなりません。 -グローバル状態は、ユニットテストにおける最大の頭痛の種です! +グローバル状態は、ユニットテストにおける最大の頭痛の種です。 -状況を修正するにはどうすればよいですか? 簡単です。シングルトンを利用するコードを書かないでください。依存関係の受け渡しを優先してください。つまり、依存性注入です。 +どう直せばよいでしょうか。簡単です。シングルトンを使うコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。 グローバル定数 ------- -グローバル状態は、シングルトンや静的変数の使用に限定されず、グローバル定数にも関係する可能性があります。 +グローバル状態はシングルトンや静的変数の利用に限りません。グローバル定数にも当てはまります。 -値が新しい(`M_PI`)または有用な(`PREG_BACKTRACK_LIMIT_ERROR`)情報をもたらさない定数は、明らかに問題ありません。 逆に、情報をコード内に*ワイヤレス*で渡す方法として機能する定数は、隠れた依存関係にすぎません。次の例の`LOG_FILE`のように。 定数`FILE_APPEND`の使用は完全に正しいです。 +普遍的な事実を表す値(`M_PI`)や、それ自体で完結した情報を与える定数(`PREG_BACKTRACK_LIMIT_ERROR`)は、一般に問題ありません。逆に、情報をコードに*無線で*注入する手段として使われる定数は、実質的に隠れた依存関係です。次の例の `LOG_FILE` がそうです。`FILE_APPEND` 定数の使い方はまったく正しいものです。 ```php const LOG_FILE = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -この場合、APIの一部となるように、クラス`Foo`のコンストラクタでパラメータを宣言する必要があります: +代わりに、ログファイルのパスを `Foo` クラスのコンストラクタのパラメータとして宣言し、その API の明示的な一部にすべきです。 ```php class Foo @@ -253,42 +253,42 @@ class Foo } ``` -これで、ロギング用のファイルパスに関する情報を渡し、必要に応じて簡単に変更できるため、コードのテストと保守が容易になります。 +これでログファイルへのパスを明示的に渡すようになりました。必要に応じて簡単に変えられ、テストもコードの保守も楽になります。 グローバル関数と静的メソッド -------------- -静的メソッドとグローバル関数自体の使用が問題ではないことを強調したいと思います。`DB::insert()`や同様のメソッドの使用が不適切である理由を説明しましたが、それは常に、ある静的変数に格納されているグローバル状態の問題にすぎませんでした。`DB::insert()`メソッドは、データベース接続が格納されているため、静的変数の存在を必要とします。この変数がなければ、メソッドを実装することは不可能です。 +強調しておきたいのは、静的メソッドやグローバル関数を使うこと自体は本質的に問題ではない、という点です。`DB::insert()` のようなメソッドの問題を説明しましたが、核心はいつも、その背後にあるグローバル状態、たいていは静的変数に保存された状態でした。`DB::insert()` メソッドは、データベース接続を保持する静的変数に頼っています。その変数がなければ、このメソッドは実装できません。 -`DateTime::createFromFormat()`、`Closure::fromCallable`、`strlen()`、その他多くの決定論的な静的メソッドと関数の使用は、依存性注入と完全に一致しています。これらの関数は、同じ入力パラメータから常に同じ結果を返し、したがって予測可能です。グローバル状態は使用しません。 +`Closure::fromCallable()`、`strlen()` など、決定的な静的メソッドや関数を使うことは、依存性注入と完全に両立します。これらの関数は、同じ入力に対して常に同じ結果を返すので予測できます。グローバル状態をまったく使いません。 -ただし、PHPには決定論的でない関数もあります。これらには、たとえば関数`htmlspecialchars()`が含まれます。その3番目のパラメータ`$encoding`が指定されていない場合、デフォルト値は設定オプション`ini_get('default_charset')`の値になります。したがって、このパラメータを常に指定し、関数の予期しない動作の可能性を防ぐことをお勧めします。Netteはこれを一貫して行っています。 +とはいえ PHP には決定的でない関数もあります。たとえば `htmlspecialchars()` 関数です。第 3 パラメータの `$encoding` を省くと、設定オプション `default_charset` の値(`ini_get('default_charset')`)が既定になります。ですから予測できない振る舞いを防ぐため、このパラメータは常に指定することをおすすめします。Nette は一貫してそうしています。 -`strtolower()`、`strtoupper()`などの一部の関数は、最近まで非決定論的に動作し、`setlocale()`の設定に依存していました。これは多くの合併症を引き起こし、最も一般的にはトルコ語を扱うときに発生しました。トルコ語では、ドット付きとドットなしの小文字と大文字の`I`を区別します。したがって、`strtolower('I')`は文字`ı`を返し、`strtoupper('i')`は文字`İ`を返し、これによりアプリケーションが一連の不可解なエラーを引き起こし始めました。 しかし、この問題はPHPバージョン8.2で修正され、関数はもはやロケールに依存しません。 +`strtolower()` や `strtoupper()` のような一部の関数は、少し前まではロケールの設定(`setlocale()`)に応じて決定的でない振る舞いをしていました。これは多くの面倒を生み、とりわけトルコ語を扱うときに問題になりました。トルコ語では小文字にも大文字にも、点のある I と点のない I の区別があるからです。その結果 `strtolower('I')` は `ı`(点のない小文字の i)を返し、`strtoupper('i')` は `İ`(点のある大文字の I)を返して、原因不明のアプリケーションのエラーを数多く引き起こしました。ただしこの問題は PHP 8.2 で修正され、これらの関数はもうロケールに依存しません。 -これは、グローバル状態が世界中の何千人もの開発者をどのように悩ませたかの良い例です。解決策は、それを依存性注入に置き換えることでした。 +これは、グローバル状態(ロケールの設定)が世界中の何千もの開発者を悩ませた良い例です。最終的な解は、関数をロケールに依存しないものにすること、つまり隠れた依存関係を取り除くことでした。 -グローバル状態を使用できる場合はいつですか? +グローバル状態を使ってもよいのはどんなときか ---------------------- -グローバル状態を利用できる特定の状況があります。たとえば、コードのデバッグ中に、変数の値を出力したり、プログラムの特定の部分の期間を測定したりする必要がある場合です。このような場合、後でコードから削除される一時的なアクションに関する場合、グローバルに利用可能なダンパーまたはストップウォッチを正当に利用できます。これらのツールはコードの設計の一部ではありません。 +グローバル状態を使ってもよい、限られた特定の場面はあります。たとえばデバッグ中に、変数の値をダンプしたり、あるコードの実行時間を測ったりする場合です。あとでコードから取り除く一時的な処置であれば、グローバルにアクセスできるダンパーやタイマーを使うのは正当です。これらの道具はアプリケーションの設計の中核ではありません。 -別の例は、正規表現を扱う関数`preg_*`であり、コンパイルされた正規表現をメモリ内の静的キャッシュに内部的に格納します。したがって、コードの異なる場所で同じ正規表現を複数回呼び出すと、一度だけコンパイルされます。キャッシュはパフォーマンスを節約し、同時にユーザーには完全に表示されないため、このような使用は正当と見なすことができます。 +もうひとつの例が PHP の正規表現の関数(`preg_*`)で、コンパイル済みの正規表現を内部で静的メモリにキャッシュしています。コードの中で同じ正規表現の関数を何度も呼んでも、その式がコンパイルされるのは一度きりです。このキャッシュは性能を高め、利用者からはまったく見えないので、こうした内部の静的な状態の使い方は一般に問題ありません。 まとめ --- -私たちは、なぜ意味があるのか​​を議論しました: +なぜ次のことに意味があるのかを見てきました。 -1) コードからすべての静的変数を削除する -2) 依存関係を宣言する -3) そして依存性注入を使用する +1) コードから変更可能な静的プロパティ(グローバル状態)をすべて取り除く +2) 依存関係を明示的に宣言する +3) そして依存性注入を活用する -コードの設計を考えるとき、すべての`static $foo`が問題であることを念頭に置いてください。コードがDIを尊重する環境であるためには、グローバル状態を完全に根絶し、依存性注入に置き換えることが不可欠です。 +コードを設計するときは、変更可能な `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氏に感謝します。* +*[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 index ce73fa0e23..b4e76fb999 100644 --- a/dependency-injection/ja/introduction.texy +++ b/dependency-injection/ja/introduction.texy @@ -1,58 +1,58 @@ -依存性の注入とは? -********* +Dependency Injection とは? +************************ .[perex] -この章では、すべてのアプリケーションを作成する際に従うべき基本的なプログラミング手法を紹介します。これらは、クリーンで理解しやすく、保守可能なコードを書くために必要な基礎です。 +この章では、どんなアプリケーションを書くときにも従うべき基本的なプログラミングの作法を紹介します。きれいで分かりやすく、保守しやすいコードを書くために欠かせない土台です。 -これらのルールを習得し、遵守すれば、Netteはあらゆるステップであなたをサポートします。ルーチンタスクを処理し、最大限の快適さを提供するため、ロジック自体に集中できます。 +これらの規則を身につけて守れば、Nette はあらゆる段階であなたを支えます。決まりきった作業を代わりにこなし、最大限の快適さを提供するので、あなたはロジックそのものに集中できます。 -ここで示す原則は、実際には非常にシンプルです。何も心配する必要はありません。 +ここで示す原則はとても単純です。恐れることは何もありません。 -最初のプログラムを覚えていますか? ------------------ +最初に書いたプログラムを覚えていますか +------------------- -どの言語で書いたかはわかりませんが、もしPHPだったら、おそらくこのようになっていたでしょう: +どの言語で書いたかは分かりませんが、もし PHP だったなら、おそらくこんな形だったでしょう。 ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // 24 を出力します +echo addition(23, 1); // 24 を出力 ``` -いくつかの簡単なコード行ですが、そこには非常に多くの重要な概念が隠されています。変数があること。コードがより小さな単位、例えば関数に分割されること。それらに入力引数を渡し、それらが結果を返すこと。そこには条件とループだけが欠けています。 +ほんの数行のありふれたコードですが、そこには多くの重要な考え方が隠れています。変数が存在すること。コードが関数のような小さな単位に分けられること。そこに入力の引数を渡すと、結果が返ってくること。足りないのは条件とループくらいです。 -関数に入力データを渡し、それが結果を返すというのは、数学のような他の分野でも使用される、完全に理解できる概念です。 +関数にデータを渡すと結果が返ってくるというのは、数学など他の分野でも使われる、まったく理解しやすい考え方です。 -関数には、その名前、パラメータとその型のリスト、そして最後に返り値の型からなるシグネチャがあります。ユーザーとしては、シグネチャに興味があり、通常、内部実装について何も知る必要はありません。 +関数にはシグネチャがあり、名前、パラメータとその型の並び、そして戻り値の型から成ります。利用者として関心があるのはシグネチャで、内部の実装について知る必要はふつうありません。 -さて、関数のシグネチャがこのようになっていると想像してみてください: +では、関数のシグネチャが次のようだったらどうでしょうか。 ```php -function soucet(float $x): float +function addition(float $x): float ``` -1つのパラメータでの合計?それは奇妙です… では、これはどうでしょう? +パラメータがひとつの足し算? 妙ですね……。ではこれは? ```php -function soucet(): float +function addition(): float ``` -これは本当に非常に奇妙ですね?関数はどのように使用されるのでしょうか? +これは本当に妙ですね。この関数はどう使うのでしょうか。 ```php -echo soucet(); // 何が出力されるでしょうか? +echo addition(); // 何を出力する? ``` -このようなコードを見ると、私たちは混乱するでしょう。初心者だけでなく、熟練したプログラマーでさえ、そのようなコードを理解できません。 +こんなコードを見たら困惑するでしょう。初心者どころか、熟練したプログラマーでも理解できません。 -そのような関数が内部でどのように見えるか考えていますか?加算される数はどこから取得するのでしょうか?おそらく、*何らかの方法で*それらを自分で取得するでしょう、例えばこのように: +こうした関数は中身がどうなっているのか気になりますか。足す数はどこから手に入れるのでしょうか。おそらく*どうにかして*自分で調達しているはずです。たとえば次のように。 ```php -function soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -関数の本体で、他のグローバル関数や静的メソッドへの隠れた依存関係を発見しました。加算される数が実際にどこから来るのかを知るためには、さらに調査する必要があります。 +関数の本体で、ほかのグローバル関数や静的メソッドへの隠れた依存関係が見つかりました。数が実際にどこから来るのかを知るには、さらに調べる必要があります。 -これはダメ! ------- +こうしてはいけない +--------- -私たちが示した設計は、多くの否定的な特徴の本質です: +いま見せた設計は、多くの悪い性質の元になっています。 -- 関数のシグネチャは、加算される数を必要としないように見せかけ、私たちを混乱させました -- 他の2つの数を合計するように関数をどうやって強制するのか、まったくわかりません -- 加算される数をどこから取得するかを知るために、コードを見る必要がありました -- 隠れた依存関係を発見しました -- 完全に理解するためには、これらの依存関係も調査する必要があります +- 関数のシグネチャは、足す数が要らないかのように装っていて、私たちを混乱させた。 +- この関数に別の 2 つの数を足させる方法が分からない。 +- 数がどこから来るのかを知るためにコードを覗く必要があった。 +- 隠れた依存関係が見つかった。 +- 完全に理解するには、その依存関係も調べなければならない。 -そして、入力データを取得することは、加算関数のタスクなのでしょうか?もちろん、そうではありません。その責任は、加算自体だけです。 +そもそも入力を手に入れることは、足し算の関数の仕事でしょうか。もちろん違います。その責務は足し算そのものだけです。 -このようなコードには出会いたくないし、絶対に書きたくありません。修正は簡単です:基本に戻り、単にパラメータを使用します: +こんなコードには出会いたくありませんし、まして書きたくもありません。直し方は簡単です。基本に戻って、素直にパラメータを使うのです。 ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -ルール1:渡してもらう ------------ +原則 1: 渡してもらう +------------ -最も重要なルールは次のとおりです:**関数やクラスが必要とするすべてのデータは、それらに渡されなければなりません**。 +最も大切な原則はこれです。**関数やクラスが必要とするデータは、すべてそこへ渡さなければならない**。 -それらが何らかの方法で自分でアクセスできる隠れた方法を考案する代わりに、単にパラメータを渡してください。隠れたパスを考案するのに必要な時間を節約できます。それは間違いなくあなたのコードを改善しません。 +データを手に入れる隠れた道を考え出す代わりに、素直にパラメータで渡しましょう。コードを何ひとつ良くしない隠し通路を考える時間を節約できます。 -このルールを常にどこでも守れば、隠れた依存関係のないコードへの道を歩んでいます。作者だけでなく、後でそれを読むすべての人にとって理解しやすいコードへ。関数のシグネチャとクラスからすべてが理解でき、実装内の隠れた秘密を探す必要がないコードへ。 +この原則をいつでもどこでも守れば、隠れた依存関係のないコードへの道を歩むことになります。作者だけでなく、あとから読む誰にとっても分かりやすいコードへ。すべてが関数やクラスのシグネチャから読み取れ、実装の中に隠れた詳細を探す必要のないコードへ。 -この技術は専門的には**依存性の注入** (dependency injection) と呼ばれます。そして、それらのデータは**依存関係** (dependencies) と呼ばれます。実際には、それは単なるパラメータ渡しであり、それ以上のものではありません。 +この手法は専門的には **Dependency Injection(依存性注入)**と呼ばれます。そしてそのデータを**依存関係**と呼びます。単なるパラメータの受け渡しであって、それ以上のものではありません。 .[note] -デザインパターンである依存性の注入と、「依存性注入コンテナ」、つまり全く異なるツールを混同しないでください。コンテナについては後で説明します。 +デザインパターンである Dependency Injection と、道具である「Dependency Injection コンテナ」を混同しないでください。両者は根本的に異なるものです。コンテナについてはあとで扱います。 関数からクラスへ -------- -そして、クラスはこれとどのように関連していますか?クラスは単純な関数よりも複雑な全体ですが、ルール1はここでも完全に適用されます。ただし、[引数を渡すためのより多くのオプション|passing-dependencies]があります。例えば、関数の場合と非常によく似ています: +これはクラスにはどう当てはまるのでしょうか。クラスは単純な関数より複雑な存在ですが、原則 1 はここでもそのまま当てはまります。ただ[引数の渡し方が増える |passing-dependencies]だけです。たとえば、関数の場合とよく似た形にできます。 ```php -class Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` -または、他のメソッドやコンストラクタを使用して: +あるいはほかのメソッドや、コンストラクタを直接使う形でも。 ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,26 +132,25 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` -両方の例は、依存性の注入と完全に一致しています。 +どちらの例も Dependency Injection に完全に沿っています。 -実際の例 +現実の例 ---- -現実の世界では、数を合計するためのクラスを書くことはありません。実際の例に移りましょう。 +現実の世界では、数を足すクラスを書くことはないでしょう。実践的な例に移りましょう。 -ブログ記事を表すクラス `Article` があるとします: +ブログの記事を表す `Article` クラスがあるとします。 ```php class Article @@ -167,18 +166,18 @@ class Article } ``` -そして、使用法は次のようになります: +使い方は次のようになります。 ```php $article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; +$article->title = 'ダイエットについて知っておくべき 10 のこと'; +$article->content = '毎年、何百万人もの人々が ...'; $article->save(); ``` -`save()` メソッドは記事をデータベーステーブルに保存します。[Nette Database |database:] を使用して実装するのは簡単ですが、1つの問題があります:`Article` はデータベース接続、つまり `Nette\Database\Connection` クラスのオブジェクトをどこから取得するのでしょうか? +`save()` メソッドは記事をデータベースのテーブルに保存します。[Nette Database |database:]で実装するのは簡単なはずですが、ひとつ引っかかることがあります。`Article` はデータベース接続、つまり `Nette\Database\Connection` クラスのオブジェクトをどこから手に入れるのでしょうか。 -多くの選択肢があるようです。静的変数から取得できます。または、データベース接続を提供するクラスから継承することもできます。または、いわゆる [シングルトン |global-state#シングルトン] を使用することもできます。または、Laravelで使用されるいわゆるfacades: +選択肢はたくさんありそうです。静的変数から取ってくることもできます。データベース接続を提供するクラスを継承することも。[シングルトン |global-state#シングルトン]を使うことも。あるいは Laravel で使われているような、いわゆるファサードも。 ```php use Illuminate\Support\Facades\DB; @@ -199,17 +198,17 @@ class Article } ``` -素晴らしい、問題を解決しました。 +素晴らしい、問題が解けました。 -それとも? +本当にそうでしょうか。 -[#ルール1 渡してもらう] を思い出してください:クラスが必要とするすべての依存関係は、それに渡されなければなりません。なぜなら、ルールを破ると、隠れた依存関係、不可解さでいっぱいの汚いコードへの道を歩み始め、その結果、維持および開発が苦痛になるアプリケーションになるからです。 +[#原則 1: 渡してもらう]を思い出しましょう。クラスが必要とするすべての依存関係は、そこへ渡さなければなりません。この原則を破れば、隠れた依存関係だらけで見通しの悪いコードへの道に踏み出すことになり、その結果は保守と発展が困難なアプリケーションです。 -`Article` クラスのユーザーは、`save()` メソッドが記事をどこに保存するかを知りません。データベーステーブルに?どちらに、本番用またはテスト用?そして、それをどのように変更できますか? +`Article` クラスの利用者には、`save()` メソッドが記事をどこに保存するのか分かりません。データベースのテーブル? どのデータベース、本番用かテスト用か? そしてそれはどう変えられるのでしょうか。 -ユーザーは `save()` メソッドがどのように実装されているかを確認し、`DB::insert()` メソッドの使用を見つける必要があります。したがって、このメソッドがデータベース接続をどのように取得するかをさらに調査する必要があります。そして、隠れた依存関係は非常に長い連鎖を形成する可能性があります。 +利用者は `save()` メソッドの実装を見て、`DB::insert()` メソッドが使われていることに気づきます。そこで、このメソッドがデータベース接続をどう手に入れるのかをさらに調べなければなりません。隠れた依存関係はかなり長い連鎖を作り得ます。 -クリーンで適切に設計されたコードでは、隠れた依存関係、Laravelのfacades、または静的変数は決して存在しません。クリーンで適切に設計されたコードでは、引数が渡されます: +きれいでよく設計されたコードには、隠れた依存関係も、Laravel のファサードも、静的変数もありません。きれいでよく設計されたコードでは、引数が渡されます。 ```php class Article @@ -224,7 +223,7 @@ class Article } ``` -さらに実用的には、後で見るように、コンストラクタを使用することです: +あとで見るように、コンストラクタを使うほうがさらに実用的です。 ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -経験豊富なプログラマーであれば、`Article` は `save()` メソッドを持つべきではなく、純粋なデータコンポーネントとして表現し、保存は別のリポジトリが担当すべきだと考えるかもしれません。それは理にかなっています。しかし、それでは依存性の注入というトピックや、簡単な例を示すという試みの範囲を大きく超えてしまいます。 +経験を積んだプログラマーなら、そもそも `Article` に `save()` メソッドがあるべきではなく、純粋にデータ構造を表し、保存は別のリポジトリが担うべきだと考えるかもしれません。それはもっともです。しかしそれは、この記事の主題である Dependency Injection と、単純な例を示すという目的をはるかに超えてしまいます。 -例えばデータベースを必要とするクラスを作成する場合、それをどこから取得するかを考え出すのではなく、渡してもらうようにしてください。例えば、コンストラクタや他のメソッドのパラメータとして。依存関係を認めてください。クラスのAPIでそれらを認めてください。理解しやすく予測可能なコードが得られます。 +たとえばデータベースを必要とするクラスを書くなら、それをどこから手に入れるかを考え出すのではなく、渡してもらってください。コンストラクタやほかのメソッドのパラメータとして渡すのがよいでしょう。依存関係を認めてください。クラスの API でそれを認めてください。分かりやすく予測できるコードが手に入ります。 -そして、エラーメッセージをログに記録するこのクラスはどうでしょうか: +では、エラーメッセージを記録する次のクラスはどうでしょうか。 ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,23 +261,23 @@ class Logger } ``` -[#ルール1 渡してもらう] を守ったと思いますか? +どう思いますか。[#原則 1: 渡してもらう]を守っているでしょうか。 守っていません。 -クラスは、重要な情報、つまりログファイルのあるディレクトリを、定数から*自分で取得*しています。 +肝心の情報、つまりログファイルのあるディレクトリを、*クラス自身が*定数から取ってきています。 -使用例を見てください: +使い方の例を見てください。 ```php $logger = new Logger; -$logger->log('温度は23℃です'); -$logger->log('温度は10℃です'); +$logger->log('気温は 23 °C です'); +$logger->log('気温は 10 °C です'); ``` -実装を知らずに、メッセージがどこに書き込まれるかという質問に答えられますか?機能するためには定数 `LOG_DIR` の存在が必要だと考えましたか?そして、別の場所に書き込む2番目のインスタンスを作成できますか?絶対にできません。 +実装を知らずに、メッセージがどこに書かれるか答えられるでしょうか。動作に `LOG_DIR` 定数の存在が必要だと思い当たるでしょうか。そして別の場所に書き込む 2 つめのインスタンスを作れるでしょうか。まず無理です。 -クラスを修正しましょう: +クラスを直しましょう。 ```php class Logger @@ -295,24 +294,24 @@ class Logger } ``` -クラスは今、はるかに理解しやすく、構成可能で、したがってより便利です。 +これでクラスはずっと分かりやすく、設定しやすく、そして役に立つものになりました。 ```php $logger = new Logger('/path/to/log.txt'); -$logger->log('温度は15℃です'); +$logger->log('気温は 15 °C です'); ``` -でも、それは気にしない! ------------- +でも、そんなの気にしたくない +-------------- -*「Article オブジェクトを作成して save() を呼び出すとき、データベースについて考えたくない。設定で設定したデータベースに保存してほしいだけだ。」* +*「Article オブジェクトを作って save() を呼ぶとき、データベースのことなんて考えたくない。設定したところに保存されてほしいだけだ。」* -*「Logger を使用するとき、メッセージが書き込まれるだけで、どこに書き込まれるかは気にしない。グローバル設定が使用されるようにしてほしい。」* +*「Logger を使うとき、メッセージが書かれてほしいだけで、どこに書かれるかは考えたくない。グローバルな設定を使ってくれればいい。」* -これらは正しいコメントです。 +もっともな言い分です。 -例として、ニュースレターを送信し、結果をログに記録するクラスを示します: +例として、ニュースレターを配信して結果を記録するクラスを見てみましょう。 ```php class NewsletterDistributor @@ -322,7 +321,7 @@ class NewsletterDistributor $logger = new Logger(/* ... */); try { $this->sendEmails(); - $logger->log('メールは送信されました'); + $logger->log('メールを送信しました'); } catch (Exception $e) { $logger->log('送信中にエラーが発生しました'); @@ -332,17 +331,17 @@ class NewsletterDistributor } ``` -改善された `Logger` は、もはや定数 `LOG_DIR` を使用せず、コンストラクタでファイルパスを指定する必要があります。これをどのように解決しますか?`NewsletterDistributor` クラスは、メッセージがどこに書き込まれるかにまったく関心がなく、単にそれらを書き込みたいだけです。 +改良された `Logger` は `LOG_DIR` 定数を使わなくなり、コンストラクタでファイルのパスを求めます。これはどう解けばよいでしょうか。`NewsletterDistributor` クラスはメッセージがどこに書かれるかに関心がなく、ただ記録したいだけです。 -解決策は再び [#ルール1 渡してもらう] です:クラスが必要とするすべてのデータは、それに渡します。 +答えはまたも [#原則 1: 渡してもらう]です。クラスが必要とするデータをすべて渡します。 -したがって、ログファイルへのパスをコンストラクタ経由で渡し、それを `Logger` オブジェクトを作成するときに使用するという意味ですか? +では、ログのパスをコンストラクタで渡して、それを `Logger` オブジェクトを作るときに使う、ということでしょうか。 ```php class NewsletterDistributor { public function __construct( - private string $file, // ⛔ これはダメ! + private string $file, // ⛔ こうではありません! ) { } @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -これはダメ!パスは `NewsletterDistributor` クラスが必要とするデータに**属していません**。それらは `Logger` が必要とするものです。違いを理解していますか?`NewsletterDistributor` クラスはロガー自体を必要としています。したがって、それを渡します: +こうではありません。そのパスは `NewsletterDistributor` クラスが必要とするデータでは**なく**、`Logger` が必要とするデータだからです。違いが分かりますか。`NewsletterDistributor` クラスが必要とするのはロガーそのものです。ですからロガーそのものを渡します。 ```php class NewsletterDistributor @@ -365,7 +364,7 @@ class NewsletterDistributor { try { $this->sendEmails(); - $this->logger->log('メールは送信されました'); + $this->logger->log('メールを送信しました'); } catch (Exception $e) { $this->logger->log('送信中にエラーが発生しました'); @@ -375,23 +374,23 @@ class NewsletterDistributor } ``` -これで、`NewsletterDistributor` クラスのシグネチャから、ロギングがその機能の一部であることが明らかになりました。そして、テストなどのためにロガーを別のものに交換するタスクは完全に簡単です。 さらに、`Logger` クラスのコンストラクタが変更された場合、それは私たちのクラスにまったく影響を与えません。 +これで `NewsletterDistributor` クラスのシグネチャから、ログの記録がその機能の一部だと分かるようになりました。しかもテストなどのためにロガーを別のものに差し替えるのは、まったく簡単です。さらに `Logger` クラスのコンストラクタが変わっても、私たちのクラスには何の影響もありません。 -ルール2:自分のものだけを受け取る +原則 2: 自分のものだけ受け取る ----------------- -混乱しないでください。依存関係の依存関係を渡さないでください。自分の依存関係だけを渡してください。 +惑わされず、自分の依存関係の依存関係を受け取らないでください。受け取るのは自分自身の依存関係だけです。 -これにより、他のオブジェクトを使用するコードは、それらのコンストラクタの変更から完全に独立します。そのAPIはより真実になります。そして何よりも、これらの依存関係を他のものに交換することが簡単になります。 +おかげで、ほかのオブジェクトを使うコードは、そのコンストラクタの変更からまったく独立でいられます。API はより正確になります。そして何より、その依存関係を別のものに差し替えるのが簡単になります。 -新しい家族の一員 +家族に新しい仲間 -------- -開発チームは、データベースに書き込む2番目のロガーを作成することを決定しました。したがって、`DatabaseLogger` クラスを作成します。したがって、`Logger` と `DatabaseLogger` の2つのクラスがあり、1つはファイルに書き込み、もう1つはデータベースに書き込みます… このネーミングに何か奇妙な点はありませんか?`Logger` を `FileLogger` に名前変更する方が良いのではないでしょうか?間違いなくそうです。 +開発チームは、データベースに書き込む 2 つめのロガーを作ることにしました。そこで `DatabaseLogger` クラスを作ります。これで `Logger` と `DatabaseLogger` の 2 つのクラスができ、一方はファイルに、もう一方はデータベースに書きます……名前づけが少し変ではないでしょうか。`Logger` を `FileLogger` に改名したほうがよくないでしょうか。もちろんそうです。 -しかし、賢く行います。元の名前の下にインターフェースを作成します: +ただし賢くやりましょう。もとの名前でインターフェースを作ります。 ```php interface Logger @@ -400,7 +399,7 @@ interface Logger } ``` -…両方のロガーが実装します: +……そして両方のロガーがそれを実装します。 ```php class FileLogger implements Logger @@ -410,17 +409,17 @@ class DatabaseLogger implements Logger // ... ``` -そして、これにより、ロガーが使用される残りのコードで何も変更する必要がなくなります。たとえば、`NewsletterDistributor` クラスのコンストラクタは、パラメータとして `Logger` を必要とすることに依然として満足します。そして、どのインスタンスを渡すかは私たち次第です。 +おかげで、ロガーを使っているコードのほかの部分を変える必要はまったくありません。たとえば `NewsletterDistributor` クラスのコンストラクタは、引き続き `Logger` をパラメータとして求めるだけで済みます。どのインスタンスを渡すかは私たち次第です。 -**したがって、インターフェース名に `Interface` サフィックスや `I` プレフィックスを付けないでください。** そうでなければ、このようにコードをきれいに展開することはできません。 +**だからこそ、インターフェース名に `Interface` の接尾辞や `I` の接頭辞を付けないのです。** さもないと、コードをこれほど優雅に拡張できなくなります。 -ヒューストン、問題が発生しました ----------------- +ヒューストン、問題が発生した +-------------- -アプリケーション全体で、ファイルベースまたはデータベースベースのロガーの単一のインスタンスで十分であり、何かがログに記録される場所に単純に渡すことができますが、`Article` クラスの場合はまったく異なります。そのインスタンスは必要に応じて作成され、複数回作成されることもあります。そのコンストラクタでのデータベースへの依存関係をどのように処理しますか? +ファイル用でもデータベース用でも、アプリケーション全体でロガーのインスタンスはひとつあれば足り、記録が必要なところに渡せばよいのに対し、`Article` クラスの事情はかなり違います。そのインスタンスは必要に応じて、何度でも作られます。コンストラクタのデータベース依存はどう扱えばよいでしょうか。 -例として、フォームが送信された後に記事をデータベースに保存する必要があるコントローラーがあります: +例として、フォームの送信後に記事をデータベースに保存すべきコントローラを考えてみましょう。 ```php class EditController extends Controller @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -可能な解決策は明らかです:データベースオブジェクトをコンストラクタ経由で `EditController` に渡し、`$article = new Article($this->db)` を使用します。 +答えは明らかに見えます。データベースのオブジェクトをコンストラクタ経由で `EditController` に渡して、`$article = new Article($this->db)` とすればよさそうです。 -`Logger` とファイルパスの前のケースと同様に、これは正しいアプローチではありません。データベースは `EditController` の依存関係ではなく、`Article` の依存関係です。したがって、データベースを渡すことは [#ルール2:自分のものだけを受け取る] に反します。`Article` クラスのコンストラクタが変更された場合(新しいパラメータが追加された場合)、インスタンスが作成されるすべての場所でコードを調整する必要もあります。うーん。 +しかし `Logger` とファイルのパスの先ほどの場合と同じく、これは正しいやり方ではありません。データベースは `EditController` の依存関係ではなく、`Article` の依存関係です。ですからデータベースを渡すことは[原則 2: 自分のものだけ受け取る |#原則 2: 自分のものだけ受け取る]に反します。`Article` クラスのコンストラクタが変われば(新しいパラメータが加われば)、インスタンスを作っているすべての場所のコードを直す必要があります。まいりましたね。 -ヒューストン、何を提案しますか? +ヒューストン、どうすればいい? -ルール3:ファクトリに任せる --------------- +原則 3: ファクトリに任せる +--------------- -隠れた依存関係を排除し、すべての依存関係を引数として渡すことで、より構成可能で柔軟なクラスが得られました。したがって、より柔軟なクラスを作成および構成する何か他のものが必要です。それをファクトリと呼びます。 +隠れた依存関係をなくし、すべての依存関係を引数として渡すことで、より設定しやすく柔軟なクラスが手に入りました。ですから、その柔軟なクラスを作って設定してくれる何かがさらに必要になります。それをファクトリと呼びます。 -ルールは次のとおりです:クラスに依存関係がある場合、そのインスタンスの作成をファクトリに任せます。 +原則はこうです。クラスが依存関係を持つなら、そのインスタンスの生成をファクトリに任せる。 -ファクトリは、依存性の注入の世界における `new` 演算子のより賢い代替品です。 +ファクトリは、Dependency Injection の世界における `new` 演算子の賢い代替です。 .[note] -デザインパターンである*ファクトリメソッド*と混同しないでください。これはファクトリの特定の利用方法を説明するものであり、このトピックとは関係ありません。 +*factory method* デザインパターンと混同しないでください。あちらはファクトリの特定の使い方を述べたもので、この話題とは関係がありません。 ファクトリ ----- -ファクトリは、オブジェクトを作成および構成するメソッドまたはクラスです。`Article` を作成するクラスを `ArticleFactory` と呼び、たとえば次のようになります: +ファクトリとは、オブジェクトを作って設定するメソッドやクラスです。`Article` を作るクラスを `ArticleFactory` と呼ぶことにすると、次のような形になります。 ```php class ArticleFactory @@ -475,7 +474,7 @@ class ArticleFactory } ``` -コントローラーでの使用法は次のようになります: +コントローラでの使い方は次のようになります。 ```php class EditController extends Controller @@ -487,7 +486,7 @@ class EditController extends Controller public function formSubmitted($data) { - // ファクトリにオブジェクトを作成させます + // ファクトリにオブジェクトを作らせます $article = $this->articleFactory->create(); $article->title = $data->title; $article->content = $data->content; @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -この時点で `Article` クラスのコンストラクタのシグネチャが変更された場合、それに対応する必要があるコードの部分は `ArticleFactory` ファクトリ自体だけです。`Article` オブジェクトを操作する他のすべてのコード、たとえば `EditController` は、まったく影響を受けません。 +これで `Article` クラスのコンストラクタのシグネチャが変わっても、対応が必要なコードは `ArticleFactory` だけです。`EditController` など `Article` オブジェクトを扱うほかのコードは、まったく影響を受けません。 -今、あなたは私たちが本当に助けになったのか疑問に思って、額を叩いているかもしれません。コードの量が増え、全体が疑わしく複雑に見え始めています。 +本当に事態は良くなったのか、と首をかしげているかもしれません。コードの量は増え、全体がやけに複雑に見え始めました。 -心配しないでください、すぐにNette DIコンテナに到達します。そして、それは依存性の注入を使用するアプリケーションの構築を非常に簡素化する多くのトリックを持っています。たとえば、`ArticleFactory` クラスの代わりに、[単なるインターフェースを書くだけで十分です |factory]: +心配は要りません。もうすぐ Nette の DI コンテナに行き着きます。そこには、Dependency Injection を使ったアプリケーションの構築を大きく単純にしてくれる仕掛けがいくつもあります。たとえば `ArticleFactory` クラスの代わりに、[インターフェースを書くだけ |factory]で済むようになります。 ```php interface ArticleFactory @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -しかし、それは先走りです、まだ待ってください :-) +とはいえ先を急ぎすぎました。どうぞこのまま :-) まとめ --- -この章の冒頭で、クリーンなコードを設計する方法を示すことを約束しました。クラスには単純に +この章のはじめに、きれいなコードを設計する道筋を示すと約束しました。クラスについて次のことを守るだけです。 -1) [必要とする依存性を渡す |#ルール1 渡してもらう] -2) [逆に、直接必要としないものは渡さない |#ルール2 自分のものだけを受け取る] -3) [そして、依存性を持つオブジェクトはファクトリで作成するのが最適であること |#ルール3 ファクトリに任せる] +1) [必要とする依存関係を渡してもらう |#原則 1: 渡してもらう] +2) [逆に、直接必要としないものは渡されない |#原則 2: 自分のものだけ受け取る] +3) [そして依存関係を持つオブジェクトはファクトリで作るのが最善 |#原則 3: ファクトリに任せる] -一見そうは見えないかもしれませんが、これらの3つのルールには広範囲にわたる影響があります。それらはコード設計に対する根本的に異なる見方につながります。それは価値がありますか?古い習慣を捨て、一貫して依存性の注入を使用し始めたプログラマーは、このステップをプロとしての人生における決定的な瞬間と見なしています。明確で保守可能なアプリケーションの世界が彼らに開かれました。 +一見そうは見えないかもしれませんが、この 3 つの原則は遠くまで届く結果をもたらします。コードの設計に対するまったく違う見方につながるのです。その価値はあるでしょうか。古い習慣を捨てて Dependency Injection を一貫して使い始めたプログラマーは、この一歩を職業人生の節目と見なします。彼らには、明快で保守しやすいアプリケーションの世界が開けたのです。 -しかし、コードが一貫して依存性の注入を使用していない場合はどうなりますか?静的メソッドまたはシングルトンに基づいて構築されている場合はどうなりますか?それは何らかの問題を引き起こしますか?[非常に重大な問題を引き起こします |global-state]。 +では、コードが Dependency Injection を一貫して使っていなかったら? 静的メソッドやシングルトンの上に築かれていたら? それは問題につながるでしょうか。[はい、それも非常に大きな問題に |global-state]。 diff --git a/dependency-injection/ja/nette-container.texy b/dependency-injection/ja/nette-container.texy index 6d0d2bd667..8dad1cc7eb 100644 --- a/dependency-injection/ja/nette-container.texy +++ b/dependency-injection/ja/nette-container.texy @@ -1,10 +1,10 @@ -Nette DIコンテナ -************ +Nette DI Container +****************** .[perex] -Nette DIは、Netteの最も興味深いライブラリの1つです。非常に高速で驚くほど簡単に構成できるコンパイル済みDIコンテナを生成および自動更新できます。 +Nette DI は Nette の最も興味深いライブラリのひとつです。きわめて高速で、驚くほど設定しやすいコンパイル済みの DI コンテナを生成し、自動的に更新できます。 -DIコンテナが作成するサービスの形式は、通常、[NEONフォーマット|neon:format]の構成ファイルを使用して定義します。[前の章|container]で手動で作成したコンテナは、次のように記述されます: +DI コンテナが作るべきサービスの形は、ふつう [NEON 形式|neon:format]の設定ファイルで定義します。[前の章|container]で手作りしたコンテナは、次のように書けます。 ```neon parameters: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -記述は本当に簡潔です。 +構文はとても簡潔です。 -`ArticleFactory` および `UserController` クラスのコンストラクタで宣言されたすべての依存関係は、いわゆる[オートワイヤリング|autowiring]のおかげでNette DIによって自動的に検出され、渡されるため、構成ファイルに何も指定する必要はありません。したがって、パラメータが変更された場合でも、構成で何も変更する必要はありません。Netteコンテナは自動的に再生成されます。アプリケーションの開発に集中できます。 +`ArticleFactory` と `EditController` クラスのコンストラクタで宣言されたすべての依存関係は、いわゆる[オートワイヤリング|autowiring]のおかげで Nette DI が見つけて自動的に渡すので、設定ファイルに何も書く必要はありません。ですからパラメータが変わっても、設定を変える必要はありません。開発中は Nette がコンテナを自動的に作り直します。あなたはアプリケーションの開発だけに集中できます。 -セッターを使用して依存関係を渡したい場合は、[setup |services#Setup]セクションを使用します。 +セッターで依存関係を渡したい場合は、そのために [setup |services#Setup]セクションを使います。 -Nette DIは、コンテナのPHPコードを直接生成します。結果は`.php`ファイルであり、開いて調べることができます。これにより、コンテナがどのように機能するかを正確に確認できます。IDEでデバッグしてステップ実行することもできます。そして最も重要なこと:生成されたPHPは非常に高速です。 +Nette DI はコンテナの PHP コードを直接生成します。ですから結果は `.php` ファイルで、開いて中を確かめられます。コンテナがどう機能するかを正確に見られるわけです。IDE でデバッグして、実行を追うこともできます。そして何より、生成された PHP コードはきわめて高速です。 -Nette DIは、提供されたインターフェースに基づいて[ファクトリ|factory]のコードを生成することもできます。したがって、`ArticleFactory` クラスの代わりに、アプリケーションでインターフェースを作成するだけで十分です: +Nette DI は、与えられたインターフェースにもとづいて[ファクトリ|factory]のコードを生成することもできます。ですから `ArticleFactory` クラスの代わりに、アプリケーションではインターフェースを作るだけで済みます。 ```php interface ArticleFactory @@ -36,19 +36,19 @@ interface ArticleFactory } ``` -完全な例は[GitHub上|https://github.com/nette-examples/di-example-doc]にあります。 +完全な例は [GitHub|https://github.com/nette-examples/di-example-doc]にあります。 -スタンドアロンでの使用 ------------ +単体での利用 +------ -Nette DIライブラリをアプリケーションに導入するのは非常に簡単です。まず、Composerでインストールします(zipファイルのダウンロードは時代遅れなので): +Nette DI ライブラリをアプリケーションに組み込むのはとても簡単です。まず Composer でインストールします(zip ファイルをダウンロードするのは、もう時代遅れですから)。 ```shell composer require nette/di ``` -次のコードは、`config.neon`ファイルに保存された構成に従ってDIコンテナのインスタンスを作成します: +次のコードは [Compiler |api:Nette\DI\Compiler]を使い、`config.neon` ファイルに書かれた設定に従って DI コンテナのインスタンスを作ります。 ```php $loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -コンテナは一度だけ生成され、そのコードはキャッシュ(ディレクトリ`__DIR__ . '/temp'`)に書き込まれ、後続のリクエストではそこから読み込まれるだけです。 +コンテナが生成されるのは一度きりで、そのコードはキャッシュ(`__DIR__ . '/temp'` ディレクトリ)に書かれ、以降のリクエストではそこから読み込まれるだけです。 -サービスを作成および取得するには、サービス名をパラメータとして`getService()`メソッドを使用するか、サービスタイプをパラメータとして`getByType()`メソッドを使用します。このようにして`UserController`オブジェクトを作成します: +`Compiler` は単体では、設定の `services` と `parameters` セクションだけを有効にします。`search`、`decorator`、`di`、`inject` などほかのセクションを使うには、まずその拡張を登録してください。そして設定の `extensions` セクションから拡張を登録できるようにするには、`ExtensionsExtension` を足します。 ```php -$controller = $container->getByType(UserController::class); +$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`のコンストラクタで2番目の引数として`true`を指定するだけです。 +開発中は自動リフレッシュのモードを有効にすると便利です。クラスや設定ファイルが変わると、コンテナが自動的に作り直されます。[ContainerLoader |api:Nette\DI\ContainerLoader]のコンストラクタの第 2 引数に `true` を渡すだけです。 ```php $loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); ``` -Netteフレームワークでの使用 ----------------- +コンテナを扱う +------- + +`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コンテナの設定]が担当します。 +ここまで見てきたとおり、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 index 0422ec61a6..ffe6167e6f 100644 --- a/dependency-injection/ja/passing-dependencies.texy +++ b/dependency-injection/ja/passing-dependencies.texy @@ -1,24 +1,24 @@ -依存性の受け渡し -******** +依存関係の受け渡し +********* <div class=perex> -引数、またはDIの用語では「依存関係」は、次の主な方法でクラスに渡すことができます: +引数、DI の用語でいう「依存関係」は、次の主な方法でクラスに渡せます。 -* コンストラクタによる受け渡し -* メソッド(いわゆるセッター)による受け渡し -* 変数の設定による受け渡し -* *inject*メソッド、アノテーション、または属性による受け渡し +* コンストラクタインジェクション +* メソッドインジェクション(いわゆるセッターインジェクション) +* プロパティインジェクション +* `inject*()` メソッドまたは `#[Inject]` アトリビュートの利用 </div> -次に、具体的な例で各バリアントを示します。 +それぞれの形を具体的な例で見ていきましょう。 -コンストラクタによる受け渡し -============== +コンストラクタインジェクション +=============== -依存関係は、オブジェクトの作成時にコンストラクタの引数として渡されます: +依存関係は、オブジェクトが生成されるときにコンストラクタの引数として渡されます。 ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -この形式は、クラスが機能するために不可欠な必須の依存関係に適しています。なぜなら、それらなしではインスタンスを作成できないからです。 +この方法は、クラスが動くのに絶対に必要な依存関係に向いています。それがなければインスタンスを作れないからです。 -PHP 8.0以降では、より短い形式の記述([コンストラクタプロパティプロモーション |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion])を使用できます。これは機能的に同等です: +PHP 8.0 以降は、より短い書き方([コンストラクタのプロパティ昇格 |https://blog.nette.org/en/php-8-0-complete-overview-of-news#toc-constructor-property-promotion])が使え、機能は同じです。 ```php // PHP 8.0 @@ -49,7 +49,7 @@ class MyClass } ``` -PHP 8.1以降では、変数を`readonly`フラグでマークできます。これは、変数の内容が変更されないことを宣言します: +PHP 8.1 以降はプロパティに `readonly` フラグを付けられ、初期化のあとに値が変わらないことを宣言できます。 ```php // PHP 8.1 @@ -62,13 +62,13 @@ class MyClass } ``` -DIコンテナは、[オートワイヤリング |autowiring]を使用してコンストラクタに依存関係を自動的に渡します。このように渡すことができない引数(文字列、数値、ブール値など)は、[設定ファイルに記述します |services#引数]。 +DI コンテナは[オートワイヤリング |autowiring]を使って、依存関係をコンストラクタに自動的に渡します。この方法で渡せない引数(文字列、数値、真偽値など)は[設定で指定します |services#引数]。 コンストラクタ地獄 --------- -*コンストラクタ地獄*という用語は、子が親クラスから継承し、その親クラスのコンストラクタが依存関係を必要とし、同時に子も依存関係を必要とする状況を指します。その際、親の依存関係も引き継いで渡す必要があります: +*コンストラクタ地獄*という言葉は、依存関係を必要とするコンストラクタを持つ親クラスを子クラスが継承し、その子クラスも依存関係を必要とする状況を指します。子クラスは親の依存関係も受け取って渡さなければなりません。 ```php abstract class BaseClass @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -問題は、`BaseClass`クラスのコンストラクタを変更したいときに発生します。たとえば、新しい依存関係が追加された場合です。その場合、すべての子のコンストラクタも変更する必要があります。これにより、そのような変更は地獄になります。 +問題が起こるのは、たとえば新しい依存関係が加わって `BaseClass` のコンストラクタを変えたくなったときです。すると子クラスのコンストラクタもすべて直す必要が出てきます。こうしてその変更は地獄になります。 -これをどのように防ぐか?解決策は、**[継承よりもコンポジションを |faq#なぜ継承よりもコンポジションが優先されるのですか]優先すること**です。 +どうすれば防げるでしょうか。答えは**[継承よりコンポジションを優先する |faq#なぜ継承よりコンポジションが好まれるのですか?]**ことです。 -つまり、コードを異なる方法で設計します。[抽象 |nette:introduction-to-object-oriented-programming#抽象クラス] `Base*` クラスを避けます。`MyClass` が `BaseClass` から継承することによって特定の機能を取得する代わりに、この機能を依存関係として渡してもらいます: +そこでコードの設計を変えます。[抽象 |nette:introduction-to-object-oriented-programming#抽象クラス]の `Base*` クラスを避けるのです。`MyClass` が `BaseClass` を継承して機能を得る代わりに、その機能を依存関係として渡します。 ```php final class SomeFunctionality @@ -125,10 +125,10 @@ final class MyClass ``` -セッターによる受け渡し -=========== +セッターインジェクション +============ -依存関係は、それらをプライベート変数に保存するメソッドを呼び出すことによって渡されます。これらのメソッドの一般的な命名規則は`set*()`形式であるため、セッターと呼ばれますが、もちろん他の名前を付けることもできます。 +依存関係は、それを private なプロパティに保存するメソッドを呼ぶことで渡されます。こうしたメソッドの一般的な命名は `set*()` の形なのでセッターと呼ばれますが、もちろん別の名前でもかまいません。 ```php class MyClass @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -この方法は、クラスの機能に必須ではないオプションの依存関係に適しています。なぜなら、オブジェクトが実際に依存関係を受け取る(つまり、ユーザーがメソッドを呼び出す)ことは保証されていないからです。 +この方法は、クラスの動作に不可欠でない省略可能な依存関係に向いています。オブジェクトが実際にその依存関係を受け取る保証(つまり呼び出し側がそのメソッドを呼ぶ保証)がないからです。 -同時に、この方法はセッターを繰り返し呼び出して依存関係を変更することを可能にします。これが望ましくない場合は、メソッドにチェックを追加するか、PHP 8.1以降ではプロパティ`$cache`を`readonly`フラグでマークします。 +同時にこの方法では、セッターを繰り返し呼んで依存関係を変えられます。それが望ましくないなら、メソッドの中にチェックを足すか、PHP 8.1 以降なら `$cache` プロパティに `readonly` フラグを付けてください。 ```php class MyClass @@ -164,7 +164,7 @@ class MyClass } ``` -セッターの呼び出しは、DIコンテナの構成の[setupキー |services#Setup]で定義します。ここでも、オートワイヤリングによる依存関係の自動受け渡しが利用されます: +セッターの呼び出しは、DI コンテナの設定の [setup キー |services#Setup]で定義します。ここでもオートワイヤリングによる依存関係の自動的な受け渡しが使われます。 ```neon services: @@ -174,10 +174,10 @@ services: ``` -変数の設定による受け渡し -============ +プロパティインジェクション +============= -依存関係は、メンバー変数に直接書き込むことによって渡されます: +依存関係は、メンバーのプロパティに直接書き込むことで渡されます。 ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -この方法は、メンバー変数を`public`として宣言する必要があるため、不適切と見なされます。したがって、渡された依存関係が実際に指定された型であること(PHP 7.4以前に適用)を制御できず、新しく割り当てられた依存関係に独自のコードで応答する可能性(たとえば、後続の変更を防ぐ)を失います。同時に、変数はクラスのパブリックインターフェースの一部となり、これは望ましくない場合があります。 +この方法は不適切と見なされます。メンバーのプロパティを `public` として宣言しなければならないからです。その結果、渡された依存関係が本当に求める型かどうかを保証する制御を失い(これは PHP 7.4 でプロパティの型宣言が入る前にはとくに当てはまりました)、新しく代入された依存関係に対して独自のロジックで反応する、たとえばそのあとの変更を防ぐ、といったこともできなくなります。同時にそのプロパティはクラスの公開 API の一部になってしまい、それは意図しないことかもしれません。 -変数の設定は、DIコンテナの構成の[setupセクション |services#Setup]で定義します: +プロパティへの代入は、DI コンテナの設定の [setup セクション |services#Setup]で定義します。 ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -前の3つの方法はすべてのオブジェクト指向言語で一般的に適用されますが、*inject*メソッド、アノテーション、または属性による注入は、NetteのPresenterに特有のものです。[別の章 |best-practices:inject-method-attribute]で説明されています。 +ここまでの 3 つの方法はあらゆるオブジェクト指向言語に一般に当てはまりますが、`inject*()` メソッドや `#[Inject]` アトリビュートによる注入は、ふつう Nette のプレゼンターで使われ、そこでは既定で有効です。ほかのサービスも [`inject: true` |services#Inject モード]で使えるようにできます。これらは[別の章 |best-practices:inject-method-attribute]で扱います。 -どの方法を選択するか? -=========== +どの方法を選ぶか +======== -- コンストラクタは、クラスが機能するために不可欠な必須の依存関係に適しています -- セッターは、オプションの依存関係、または後で変更できる可能性のある依存関係に適しています -- public変数は適していません +- コンストラクタは、クラスが動くのに絶対に必要な依存関係に向いています。 +- 逆にセッターは、省略可能な依存関係や、あとで変える必要があるかもしれない依存関係に向いています。 +- 公開プロパティは一般におすすめしません。 diff --git a/dependency-injection/ja/services.texy b/dependency-injection/ja/services.texy index b8fc4824f9..dffad9eb64 100644 --- a/dependency-injection/ja/services.texy +++ b/dependency-injection/ja/services.texy @@ -2,16 +2,16 @@ ******* .[perex] -設定は、DIコンテナに個々のサービスをどのように組み立て、他の依存関係とどのように接続するかを教える場所です。Netteは、これを達成するための非常に明確でエレガントな方法を提供します。 +設定は、個々のサービスをどう作り、依存関係とどう結びつけるかを DI コンテナに指示する場所です。Nette はそのための、とても明快で優雅な方法を提供します。 -NEON形式の設定ファイルの`services`セクションは、独自のサービスとその構成を定義する場所です。`PDO`クラスのインスタンスを表す`database`という名前のサービスを定義する簡単な例を見てみましょう: +NEON の設定ファイルの `services` セクションで、独自のサービスとその設定を定義します。`PDO` クラスのインスタンスを表す `database` というサービスを定義する簡単な例を見てみましょう。 ```neon services: database: PDO('sqlite::memory:') ``` -上記の構成は、[DIコンテナ|container]で次のファクトリメソッドになります: +上の設定から、[DI コンテナ|container]に次のファクトリメソッドが作られます。 ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -サービス名を使用すると、設定ファイルの他の部分で`@サービス名`の形式で参照できます。サービスに名前を付ける必要がない場合は、単に箇条書きを使用できます: +サービス名を付けると、設定ファイルのほかの場所から `@serviceName` の形で参照できます。サービスに名前を付ける必要がなければ、単に箇条書きの記号(`-`)を使えます。 ```neon services: - PDO('sqlite::memory:') ``` -DIコンテナからサービスを取得するには、サービス名をパラメータとして`getService()`メソッドを使用するか、サービスタイプをパラメータとして`getByType()`メソッドを使用できます: +DI コンテナからサービスを取り出すには、サービス名をパラメータに取る `getService()` メソッドか、サービスの型を取る `getByType()` メソッドを使います。 ```php $database = $container->getService('database'); @@ -35,17 +35,17 @@ $database = $container->getByType(PDO::class); ``` -サービスの作成 +サービスの生成 ======= -ほとんどの場合、特定のクラスのインスタンスを作成するだけでサービスを作成します。例: +ふつうサービスは、特定のクラスをインスタンス化するだけで作ります。たとえば次のようにです。 ```neon services: database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) ``` -構成を他のキーで拡張する必要がある場合は、定義を複数行に分割できます: +キーを足して設定を広げたい場合は、定義を複数行に分けられます。 ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -`create`キーにはエイリアス`factory`があり、両方のバリアントが実際によく使用されます。ただし、`create`を使用することをお勧めします。 +`create` キーには `factory` という別名があり、どちらの書き方もよく使われます。ただし `create` の利用をおすすめします。 -コンストラクタまたは作成メソッドの引数は、代わりに`arguments`キーに記述することもできます: +コンストラクタやファクトリメソッドの引数は、`arguments` キーで指定することもできます。 ```neon services: @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -サービスは、クラスのインスタンスを単純に作成するだけでなく、静的メソッドや他のサービスのメソッドを呼び出すことによっても作成できます: +サービスは必ずしも単純なクラスのインスタンス化で作る必要はなく、静的メソッドやほかのサービスのメソッドを呼んだ結果でもかまいません。 ```neon services: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -単純化のために`->`の代わりに`::`が使用されていることに注意してください。[#表現手段]を参照してください。これらのファクトリメソッドが生成されます: +分かりやすさのために `->` の代わりに `::` を使っていることに注意してください。[#式の言語]をご覧ください。次のファクトリメソッドが生成されます。 ```php public function createServiceDatabase(): PDO @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -DIコンテナは、作成されたサービスのタイプを知る必要があります。指定された戻り値の型を持たないメソッドを使用してサービスを作成する場合、このタイプを構成で明示的に指定する必要があります: +DI コンテナは、作られるサービスの型を知る必要があります。戻り値の型が指定されていないメソッドでサービスを作る場合は、その型を設定で明示的に宣言しなければなりません。 ```neon services: @@ -98,16 +98,16 @@ services: 引数 -========= +=== -コンストラクタとメソッドに引数を渡す方法は、PHP自体と非常によく似ています: +コンストラクタやメソッドへの引数は、PHP 自体とよく似たやり方で渡します。 ```neon services: database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) ``` -読みやすくするために、引数を個別の行に分割できます。この場合、カンマの使用はオプションです: +読みやすさのために、引数を別々の行に並べることもできます。その場合、カンマは省略できます。 ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -引数に名前を付けることもでき、その順序を気にする必要はありません: +引数に名前を付ければ、順序を気にする必要もなくなります。 ```neon services: @@ -129,20 +129,20 @@ services: ) ``` -一部の引数を省略してデフォルト値を使用するか、[オートワイヤリング|autowiring]を使用してサービスを挿入する場合は、アンダースコアを使用します: +一部の引数を省いて既定値を使いたい場合や、[オートワイヤリング|autowiring]でサービスを注入させたい場合は、アンダースコア(`_`)を使います。 ```neon services: foo: Foo(_, %appDir%) ``` -引数としてサービスを渡したり、パラメータを使用したり、その他多くのことができます。[#表現手段]を参照してください。 +引数にはサービスやパラメータなど、さまざまなものを書けます。[#式の言語]をご覧ください。 Setup ===== -`setup`セクションでは、サービスの作成時に呼び出すメソッドを定義します。 +`setup` セクションでは、サービスの生成時に呼ぶべきメソッドを定義します。 ```neon services: @@ -152,7 +152,7 @@ services: - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ``` -これはPHPでは次のようになります: +PHP では次のようになります。 ```php public function createServiceDatabase(): PDO @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -メソッドの呼び出しに加えて、プロパティに値を渡すこともできます。配列への要素の追加もサポートされており、NEON構文と衝突しないように引用符で囲む必要があります: +メソッドの呼び出しのほかに、プロパティへの値の代入もできます。配列への要素の追加にも対応していますが、NEON の構文との衝突を避けるため、配列アクセスを引用符で囲む必要があります。 ```neon services: @@ -174,7 +174,7 @@ services: - '$onClick[]' = [@bar, clickHandler] ``` -これはPHPコードでは次のようになります: +PHP のコードでは次のようになります。 ```php public function createServiceFoo(): Foo @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -ただし、setupでは静的メソッドや他のサービスのメソッドを呼び出すこともできます。現在のサービスを引数として渡す必要がある場合は、`@self`として指定します: +さらに setup では、静的メソッドやほかのサービスのメソッドも呼べます。現在のサービス自身を引数として渡す必要があるなら、`@self` で参照します。 ```neon services: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -単純化のために`->`の代わりに`::`が使用されていることに注意してください。[#表現手段]を参照してください。このようなファクトリメソッドが生成されます: +分かりやすさのために `->` の代わりに `::` を使っていることに注意してください。[#式の言語]をご覧ください。次のファクトリメソッドが生成されます。 ```php public function createServiceFoo(): Foo @@ -210,63 +210,73 @@ public function createServiceFoo(): Foo ``` -表現手段 +式の言語 ==== -Nette DIは、ほとんど何でも記述できる非常に豊富な表現手段を提供します。したがって、構成ファイルで[パラメータ |configuration#パラメータ]を使用できます: +Nette DI はきわめて豊かな式の言語を備えていて、ほとんど何でも定義できます。設定ファイルでは[パラメータ |configuration#パラメータ]が使えます。 ```neon # パラメータ %wwwDir% -# キーの下のパラメータ値 +# キーの下のパラメータの値 %mailer.user% -# 文字列内のパラメータ +# 文字列の中のパラメータ '%wwwDir%/images' ``` -さらに、オブジェクトを作成し、メソッドと関数を呼び出します: +さらにオブジェクトの生成、メソッドや関数の呼び出しもできます。 ```neon -# オブジェクトの作成 +# オブジェクトの生成 DateTime() # 静的メソッドの呼び出し Collator::create(%locale%) -# PHP関数の呼び出し +# PHP の関数の呼び出し ::getenv(DB_USER) ``` -サービスを名前またはタイプで参照します: +サービスは名前でも型でも参照できます。 ```neon # 名前によるサービス @database -# タイプによるサービス +# 型によるサービス @Nette\Database\Connection ``` -ファーストクラスの呼び出し可能構文を使用します: .{data-version:3.2.0} +ファーストクラス callable 構文も使えます。 .{data-version:3.2.0} ```neon -# コールバックの作成、[@user, logout]に類似 +# コールバックの生成。[@user, logout] と同じです @user::logout(...) ``` -定数を使用します: +定数も使えます。 ```neon # クラス定数 FilesystemIterator::SKIP_DOTS -# グローバル定数はPHP関数constant()で取得します -::constant(PHP_VERSION) +# PHP の関数 constant() でグローバル定数を取得 +::constant(\PHP_VERSION) +``` + +サービスの公開プロパティと定数には `@service::member` でアクセスします。その名前がプロパティを指すのか定数を指すのかは最初の文字で決まります。小文字で始まれば公開プロパティ、大文字で始まれば定数です。 + +```neon +# サービスの公開プロパティ(小文字で始まります) +@settings::apiUrl + +# サービスのクラス定数(大文字で始まります) +@settings::Version ``` -メソッド呼び出しはPHPと同様に連鎖させることができます。ただし、単純化のために`->`の代わりに`::`が使用されます: +メソッドの呼び出しは PHP と同じように連ねられます。分かりやすさのために `->` の代わりに `::` を使います。 ```neon DateTime()::format('Y-m-d') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -これらの式は、[#サービスの作成]、[#引数]、[#setup]セクション、または[パラメータ |configuration#パラメータ]でどこでも使用できます: +これらの式は、[サービスの生成 |#サービスの生成]、[引数 |#引数]、[setup |#Setup]セクション、[パラメータ |configuration#パラメータ]など、どこでも使えます。 ```neon parameters: @@ -290,15 +300,15 @@ services: ``` -特殊関数 ----- +特別な関数 +----- -構成ファイルでは、次の特殊関数を使用できます: +設定ファイルでは次の特別な関数が使えます。 -- `not()` 値の否定 -- `bool()`, `int()`, `float()`, `string()` 指定された型へのロスレス型キャスト -- `typed()` 指定された型のすべてのサービスの配列を作成します -- `tagged()` 指定されたタグを持つすべてのサービスの配列を作成します +- `not()` は値を反転します +- `bool()`、`int()`、`float()`、`string()` は指定した型への損失のないキャストです .{data-version:3.0.5} +- `typed()` は指定した型のすべてのサービスの配列を作ります +- `tagged()` は指定したタグを持つすべてのサービスの配列を作ります ```neon services: @@ -308,18 +318,18 @@ services: ) ``` -PHPの従来の型キャスト(例:`(int)`)とは異なり、ロスレス型キャストは非数値に対して例外をスローします。 +`(int)` のような標準の PHP のキャストと違い、損失のないキャストは数値でない値に対して例外を投げます。 -`typed()`関数は、指定された型(クラスまたはインターフェース)のすべてのサービスの配列を作成します。オートワイヤリングが無効になっているサービスは除外されます。カンマで区切って複数の型を指定することもできます。 +`typed()` 関数は、指定した型(クラスまたはインターフェース)のすべてのサービスの配列を作ります。オートワイヤリングが無効にされたサービスは除かれます。カンマで区切って複数の型を指定することもできます。 ```neon services: - BarsDependent( typed(Bar) ) ``` -特定の型のサービスの配列は、[オートワイヤリング |autowiring#サービスの配列]を使用して自動的に引数として渡すこともできます。 +ある型のサービスの配列は、[オートワイヤリング |autowiring#サービスのコレクション]で自動的に引数として渡すこともできます。 -`tagged()`関数は、特定のタグを持つすべてのサービスの配列を作成します。ここでも、カンマで区切って複数のタグを指定できます。 +`tagged()` 関数は、特定のタグを持つすべてのサービスの配列を作ります。ここでもカンマで区切って複数のタグを指定できます。 ```neon services: @@ -330,20 +340,20 @@ services: オートワイヤリング ========= -`autowired`キーを使用すると、特定のサービスのオートワイヤリングの動作に影響を与えることができます。詳細については、[オートワイヤリングに関する章|autowiring]を参照してください。 +`autowired` キーを使うと、特定のサービスのオートワイヤリングの振る舞いを変えられます。詳しくは[オートワイヤリングの章|autowiring]をご覧ください。 ```neon services: foo: create: Foo - autowired: false # サービスfooはオートワイヤリングから除外されます + autowired: false # foo サービスはオートワイヤリングから除かれます ``` 遅延サービス .{data-version:3.2.4} ============================ -遅延読み込みは、サービスが実際に必要になるまでその作成を延期する技術です。グローバル設定では、すべてのサービスに対して[遅延作成を有効にする |configuration#遅延サービス]ことができます。個々のサービスについては、この動作を上書きできます: +遅延読み込みは、サービスの生成を実際に必要になるまで先延ばしにする手法です。グローバルな設定では、すべてのサービスについて[遅延生成を有効に |configuration#遅延サービス]できます。個々のサービスでは、その振る舞いを上書きできます。 ```neon services: @@ -352,16 +362,20 @@ services: lazy: false ``` -サービスが遅延として定義されている場合、DIコンテナから要求されると、特別なプレースホルダーオブジェクトが返されます。これは実際のサービスと同じように見え、動作しますが、実際の初期化(コンストラクタとセットアップの呼び出し)は、そのメソッドまたはプロパティのいずれかが最初に呼び出されたときにのみ行われます。 +サービスが遅延として定義されていると、DI コンテナからそれを要求したとき、特別なプロキシオブジェクトを受け取ります。このプロキシは実際のサービスと見た目も振る舞いも同じですが、本当の初期化(コンストラクタの呼び出しと setup の実行)は、そのメソッドやプロパティに最初にアクセスしたときにはじめて起こります。 + +サービスが遅れて作られるので、設定の誤りも遅れて現れることを覚えておいてください。たとえばデータベースの認証情報の誤りは、アプリケーションの起動時ではなく、最初のクエリのときにはじめて明らかになります。 + +遅延生成は循環依存、つまりサービス A がサービス B を必要とし、同時に B が A を必要とする状況も和らげます。遅延生成がなければ、コンテナは `Circular reference detected` のエラーを報告します。遅延プロキシがあれば、サービス A は B のプロキシだけを受け取り、それが実際に使われるとき、つまり A がすでに存在する時点で自分を初期化します。とはいえ循環依存は設計の欠陥の兆しなので、取り除くほうがよいでしょう。 .[note] -遅延読み込みは、ユーザー定義クラスにのみ使用でき、内部PHPクラスには使用できません。PHP 8.4以降が必要です。 +遅延読み込みには PHP 8.4 以上が必要で、クラスを直接インスタンス化して作られるサービス(`create: Foo` など)にだけ働き、ファクトリメソッドで作られるサービスには働きません。最終的に PHP の内部クラスを継承するクラスにも使えません。遅延読み込みを適用できない場合、`lazy: true` のフラグは黙って無視されます。 タグ -==== +=== -タグは、サービスに追加情報を提供するために使用されます。サービスに1つ以上のタグを追加できます: +タグはサービスに補足の情報を足すためのものです。サービスにはひとつ以上のタグを割り当てられます。 ```neon services: @@ -371,7 +385,7 @@ services: - cached ``` -タグは値を持つこともできます: +タグは値を持つこともできます。 ```neon services: @@ -381,26 +395,26 @@ services: logger: monolog.logger.event ``` -特定のタグを持つすべてのサービスを取得するには、`tagged()`関数を使用できます: +特定のタグを持つすべてのサービスを取得するには、`tagged()` 関数が使えます。 ```neon services: - LoggersDependent( tagged(logger) ) ``` -DIコンテナでは、`findByTag()`メソッドを使用して特定のタグを持つすべてのサービスのリストを取得できます: +DI コンテナの中では、`findByTag()` メソッドで特定のタグを持つすべてのサービスの名前を取得できます。 ```php $names = $container->findByTag('logger'); -// $names はサービス名とタグ値を含む配列です -// 例:['foo' => 'monolog.logger.event', ...] +// $names はサービス名をキー、タグの値を値とする配列です +// たとえば ['foo' => 'monolog.logger.event', ...] ``` -Injectモード -========= +Inject モード +========== -`inject: true`フラグを使用すると、[inject |best-practices:inject-method-attribute#Inject 属性]アノテーションを持つパブリック変数と[inject*() |best-practices:inject-method-attribute#inject メソッド]メソッドを介した依存関係の受け渡しが有効になります。 +`inject: true` フラグを使うと、[Inject |best-practices:inject-method-attribute#Inject アトリビュート]アトリビュートを付けた公開プロパティと [inject*() |best-practices:inject-method-attribute#inject*() メソッド]メソッドによる依存性注入が有効になります。 ```neon services: @@ -409,13 +423,13 @@ services: inject: true ``` -デフォルトでは、`inject`はPresenterに対してのみ有効化されます。 +既定では、`inject` モードはプレゼンターでのみ有効です。 サービスの変更 ======= -DIコンテナには、組み込みまたは[ユーザー拡張機能|extensions]を介して追加された多くのサービスが含まれています。これらのサービスの定義は、構成で直接変更できます。たとえば、通常は`Nette\Application\Application`であるサービス`application.application`のクラスを別のものに変更できます: +DI コンテナには、組み込みの拡張や[ユーザーの拡張|extensions]によって追加された多くのサービスが入っています。こうした既存のサービスの定義は、設定で直接変更できます。たとえば `application.application` サービスのクラスは既定で `Nette\Application\Application` ですが、別のものに変えられます。 ```neon services: @@ -424,9 +438,9 @@ services: alteration: true ``` -`alteration`フラグは情報提供であり、既存のサービスを変更しているだけであることを示します。 +`alteration` フラグは、既存のサービスを変更しているだけであることを示します。同時に安全装置としても働き、変更しようとしたサービスが存在しなければ、コンパイルは例外で失敗します。 -セットアップを追加することもできます: +setup を足すこともできます。 ```neon services: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -サービスを上書きするときに、元の引数、セットアップ項目、またはタグを削除したい場合は、`reset`を使用します: +サービスは内部の名前で特定しなくてもよく、型で参照することもできます。先ほどの例は次のようにも書けます。 + +```neon +services: + @Nette\Application\Application: + create: MyApplication +``` + +サービスを変更するとき、もとの引数、setup の項目、タグを取り除きたいことがあります。そのときは `reset` キーを使います。 ```neon services: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -拡張機能によって追加されたサービスを削除したい場合は、次のようにします: +拡張が追加したサービスを取り除きたい場合は、次のようにします。 ```neon services: diff --git a/dependency-injection/ja/upgrading.texy b/dependency-injection/ja/upgrading.texy new file mode 100644 index 0000000000..ee16a755a8 --- /dev/null +++ b/dependency-injection/ja/upgrading.texy @@ -0,0 +1,49 @@ +アップグレード +******* + + +バージョン 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 index 534033e8c9..bd9ae96928 100644 --- a/dependency-injection/meta.json +++ b/dependency-injection/meta.json @@ -1,5 +1,6 @@ { "version": "3.x", "repo": "nette/di", - "composer": "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 index bb16bf32da..0a0bfe4739 100644 --- a/dependency-injection/pl/@home.texy +++ b/dependency-injection/pl/@home.texy @@ -2,20 +2,21 @@ Nette DI ******** .[perex] -Dependency Injection to wzorzec projektowy, który zasadniczo zmieni Twój pogląd na kod i rozwój. Otworzy Ci drogę do świata czysto zaprojektowanych i łatwych w utrzymaniu aplikacji. +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. -- [Co to jest Dependency Injection? |introduction] +- [Czym jest wstrzykiwanie zależności? |introduction] - [Stan globalny i singletony |global-state] - [Przekazywanie zależności |passing-dependencies] -- [Co to jest kontener DI? |container] -- [Często zadawane pytania|faq] +- [Czym jest kontener DI? |container] +- [Często zadawane pytania |faq] -Pakiet `nette/di` dostarcza niezwykle zaawansowany kompilowany kontener DI dla PHP. +Pakiet `nette/di` dostarcza wyjątkowo zaawansowany, kompilowany kontener DI dla PHP. -- [Kontener Nette DI |nette-container] +- [Nette DI Container |nette-container] - [Konfiguracja |configuration] - [Definiowanie usług |services] - [Autowiring |autowiring] - [Generowane fabryki |factory] -- [Tworzenie rozszerzeń dla Nette DI|extensions] +- [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 index a4acad0599..4701baf249 100644 --- a/dependency-injection/pl/@left-menu.texy +++ b/dependency-injection/pl/@left-menu.texy @@ -1,17 +1,28 @@ Dependency Injection ******************** -- [Co to jest DI? |introduction] +- [Czym jest DI? |introduction] - [Stan globalny i singletony |global-state] - [Przekazywanie zależności |passing-dependencies] -- [Co to jest kontener DI? |container] -- [Często zadawane pytania|faq] +- [Czym jest kontener DI? |container] +- [Często zadawane pytania |faq] Nette DI -------- -- [Kontener Nette DI |nette-container] +- [Nette DI Container |nette-container] - [Konfiguracja |configuration] - [Definiowanie usług |services] - [Autowiring |autowiring] - [Generowane fabryki |factory] -- [Tworzenie rozszerzeń dla Nette DI|extensions] +- [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/autowiring.texy b/dependency-injection/pl/autowiring.texy index 9b3ba7e86c..1b9cdd29b4 100644 --- a/dependency-injection/pl/autowiring.texy +++ b/dependency-injection/pl/autowiring.texy @@ -2,23 +2,23 @@ Autowiring ********** .[perex] -Autowiring to świetna funkcja, która potrafi automatycznie przekazywać do konstruktora i innych metod wymagane usługi, dzięki czemu nie musimy ich w ogóle pisać. Oszczędza to mnóstwo czasu. +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 możemy pominąć zdecydowaną większość argumentów podczas pisania definicji usług. Zamiast: +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ć: +wystarczy napisać: ```neon services: articles: Model\ArticleRepository ``` -Autowiring kieruje się typami, więc aby działał, klasa `ArticleRepository` musi być zdefiniowana mniej więcej tak: +Autowiring kieruje się typami, więc aby zadziałał, klasa `ArticleRepository` musi być zdefiniowana mniej więcej tak: ```php namespace Model; @@ -30,22 +30,24 @@ class ArticleRepository } ``` -Aby można było użyć autowiringu, dla każdego typu musi istnieć w kontenerze **dokładnie jedna usługa**. Jeśli byłoby ich więcej, autowiring nie wiedziałby, którą z nich przekazać i rzuciłby wyjątek: +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 # RZUCI WYJĄTEK, pasuje zarówno mainDb, jak i tempDb + articles: Model\ArticleRepository # ZGŁASZA WYJĄTEK, pasują zarówno mainDb, jak i tempDb ``` -Rozwiązaniem byłoby albo obejście autowiringu i jawne podanie nazwy usługi (tj. `articles: Model\ArticleRepository(@mainDb)`). Lepszym rozwiązaniem jest jednak [wyłączenie |#Wyłączenie autowiringu] autowiringu dla jednej z usług lub [nadanie priorytetu |#Preferencja autowiringu] pierwszej usłudze. +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 usługi możemy wyłączyć za pomocą opcji `autowired: no`: +Autowiring dla usługi możemy wyłączyć opcją `autowired: false`: ```neon services: @@ -55,25 +57,27 @@ services: create: PDO('sqlite::memory:') autowired: false # usługa tempDb jest wyłączona z autowiringu - articles: Model\ArticleRepository # w związku z tym przekazuje do konstruktora mainDb + articles: Model\ArticleRepository # dlatego do konstruktora trafia mainDb ``` -Usługa `articles` nie rzuci wyjątku, że istnieją dwie pasujące usługi typu `PDO` (tj. `mainDb` i `tempDb`), które można przekazać do konstruktora, ponieważ widzi tylko usługę `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 działa inaczej niż w Symfony, gdzie opcja `autowire: false` mówi, że nie należy używać autowiringu dla argumentów konstruktora danej usługi. W Nette autowiring jest używany zawsze, czy to dla argumentów konstruktora, czy jakiejkolwiek innej metody. Opcja `autowired: false` mówi, że instancja danej usługi nie powinna być nigdzie przekazywana za pomocą autowiringu. +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. -Preferencja autowiringu ------------------------ +Preferowanie w autowiringu +-------------------------- -Jeśli mamy więcej usług tego samego typu i dla jednej z nich podamy opcję `autowired`, staje się ona usługą preferowaną: +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ę preferowaną + autowired: PDO # staje się preferowana tempDb: create: PDO('sqlite::memory:') @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -Usługa `articles` nie rzuci wyjątku, że istnieją dwie pasujące usługi typu `PDO` (tj. `mainDb` i `tempDb`), ale użyje usługi preferowanej, czyli `mainDb`. +Usługa `articles` nie zgłosi wyjątku o kilku pasujących usługach `PDO` (`mainDb` i `tempDb`), lecz użyje preferowanej, czyli `mainDb`. -Tablica usług -------------- +Kolekcja usług +-------------- -Autowiring potrafi przekazywać również tablice usług określonego typu. Ponieważ w PHP nie można natywnie zapisać typu elementów tablicy, oprócz typu `array` należy dodać komentarz phpDoc z typem elementu w formacie `ClassName[]`: +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; @@ -102,15 +106,15 @@ class ShipManager } ``` -Kontener DI następnie automatycznie przekaże tablicę usług odpowiadających danemu typowi. Pominie usługi, które mają wyłączony autowiring. +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 być również w formacie `array<int, Class>` lub `list<Class>`. Jeśli nie możesz wpłynąć na postać komentarza phpDoc, możesz przekazać tablicę usług bezpośrednio w konfiguracji za pomocą [`typed()` |services#Funkcje specjalne]. +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 potrafi podstawiać tylko obiekty i tablice obiektów. Argumenty skalarne (np. ciągi znaków, liczby, wartości logiczne) [zapisujemy w konfiguracji |services#Argumenty]. Alternatywą jest utworzenie [obiektu ustawień |best-practices:passing-settings-to-presenters], który enkapsuluje wartość skalarną (lub więcej wartości) w postaci obiektu, a ten następnie można ponownie przekazywać za pomocą autowiringu. +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 @@ -123,24 +127,41 @@ class MySettings } ``` -Utworzysz z niego usługę, dodając ją do konfiguracji: +Rejestrujesz go jako usługę, dodając do konfiguracji: ```neon services: - MySettings('any value') ``` -Wszystkie klasy następnie zażądają jej za pomocą autowiringu. +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ężenie autowiringu +Zawężanie autowiringu --------------------- -Dla poszczególnych usług można zawęzić autowiring tylko do określonych klas lub interfejsów. +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, które muszą spełniać typy podane przy parametrach metod, aby usługa została im przekazana. +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. -Pokażemy to na przykładzie: +Weźmy przykład: ```php class ParentClass @@ -162,42 +183,42 @@ class ChildDependent } ``` -Gdybyśmy wszystkie zarejestrowali jako usługi, autowiring by zawiódł: +Gdybyśmy zarejestrowali je wszystkie jako usługi, autowiring by zawiódł: ```neon services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # RZUCI WYJĄTEK, pasują usługi parent i child - childDep: ChildDependent # autowiring przekaże do konstruktora usługę child + 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` rzuci wyjątek `Multiple services of type ParentClass found: parent, child`, ponieważ do jej konstruktora pasują obie usługi `parent` i `child`, a autowiring nie może zdecydować, którą z nich wybrać. +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 zatem zawęzić jej autowiring do typu `ChildClass`: +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 napisać również 'autowired: self' + autowired: ChildClass # można też zapisać jako 'autowired: self' - parentDep: ParentDependent # autowiring przekaże do konstruktora usługę parent - childDep: ChildDependent # autowiring przekaże do konstruktora usługę child + parentDep: ParentDependent # autowiring przekazuje do konstruktora usługę parent + childDep: ChildDependent # autowiring przekazuje do konstruktora usługę child ``` -Teraz do konstruktora usługi `parentDep` zostanie przekazana usługa `parent`, ponieważ jest to teraz jedyny pasujący obiekt. Usługi `child` autowiring już tam nie przekaże. Tak, usługa `child` nadal jest typu `ParentClass`, ale nie jest już spełniony warunek zawężający podany dla typu parametru, tj. nie jest prawdą, że `ParentClass` *jest nadtypem* `ChildClass`. +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` można by `autowired: ChildClass` zapisać również jako `autowired: self`, ponieważ `self` jest zastępczym oznaczeniem dla klasy bieżącej usługi. +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 podać również kilka klas lub interfejsów jako tablicę: +W kluczu `autowired` można też podać kilka klas albo interfejsów jako tablicę: ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -Spróbujmy uzupełnić przykład o interfejsy: +Spróbujmy dodać do przykładu interfejsy: ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -Gdy usługi `child` w żaden sposób nie ograniczymy, będzie pasować do konstruktorów wszystkich klas `FooDependent`, `BarDependent`, `ParentDependent` i `ChildDependent`, a autowiring ją tam przekaże. +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 jej autowiring zawęzimy do `ChildClass` za pomocą `autowired: ChildClass` (lub `self`), autowiring przekaże ją tylko do konstruktora `ChildDependent`, ponieważ wymaga on argumentu typu `ChildClass` i jest prawdą, że `ChildClass` *jest typu* `ChildClass`. Żaden inny typ podany przy pozostałych parametrach nie jest nadtypem `ChildClass`, więc usługa nie zostanie przekazana. +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` za pomocą `autowired: ParentClass`, autowiring przekaże ją ponownie do konstruktora `ChildDependent` (ponieważ wymagany `ChildClass` jest nadtypem `ParentClass`), a nowo również do konstruktora `ParentDependent`, ponieważ wymagany typ `ParentClass` jest również pasujący. +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 nadtypem `FooInterface`) i `ChildDependent`, ale dodatkowo również do konstruktora `FooDependent`, jednak nie do `BarDependent`, ponieważ `BarInterface` nie jest nadtypem `FooInterface`. +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: @@ -251,8 +272,8 @@ services: create: ChildClass autowired: FooInterface - fooDep: FooDependent # autowiring przekaże do konstruktora child - barDep: BarDependent # RZUCI WYJĄTEK, żadna usługa nie pasuje - parentDep: ParentDependent # autowiring przekaże do konstruktora child - childDep: ChildDependent # autowiring przekaże do konstruktora child + 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 new file mode 100644 index 0000000000..f89d5478db --- /dev/null +++ b/dependency-injection/pl/compilation-internals.texy @@ -0,0 +1,222 @@ +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 index d56a300da7..dbd47e6fe8 100644 --- a/dependency-injection/pl/configuration.texy +++ b/dependency-injection/pl/configuration.texy @@ -2,32 +2,32 @@ Konfiguracja kontenera DI ************************* .[perex] -Przegląd opcji konfiguracyjnych dla kontenera Nette DI. +Przegląd opcji konfiguracyjnych kontenera Nette DI. Plik konfiguracyjny =================== -Kontener Nette DI łatwo się kontroluje za pomocą plików konfiguracyjnych. Zazwyczaj są one zapisywane w [formacie NEON|neon:format]. Do edycji polecamy [edytory z obsługą |best-practices:editors-and-tools#Edytor IDE] tego formatu. +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]":[#decorator]: "Dekorator .[prism-token prism-comment]"<br> +"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> +"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ć ciąg znaków zawierający znak `%`, musisz go escapować podwajając na `%%`. +Aby zapisać string zawierający znak `%`, musisz go zescapować, podwajając na `%%`. Parametry ========= -W konfiguracji możesz zdefiniować parametry, które można następnie użyć jako część definicji usług. Dzięki temu możesz uporządkować konfigurację lub ujednolicić i wyodrębnić wartości, które będą się zmieniać. +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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -Do parametru `dsn` odwołujemy się w dowolnym miejscu konfiguracji zapisem `%dsn%`. Parametry można używać również wewnątrz ciągów znaków, jak `'%wwwDir%/images'`. +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 ciągami znaków lub liczbami, mogą również zawierać tablice: +Parametry nie muszą być tylko stringami czy liczbami, mogą zawierać również tablice: ```neon parameters: @@ -51,7 +51,7 @@ parameters: Do konkretnego klucza odwołujemy się jako `%mailer.user%`. -Jeśli potrzebujesz w swoim kodzie, na przykład w klasie, poznać wartość dowolnego parametru, przekaż go do tej klasy. Na przykład w konstruktorze. Nie istnieje żaden globalny obiekt reprezentujący konfigurację, którego klasy pytałyby o wartości parametrów. Byłoby to naruszeniem zasady wstrzykiwania zależności. +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 @@ -60,21 +60,21 @@ Usługi Zobacz [osobny rozdział|services]. -Decorator +Dekorator ========= -Jak masowo modyfikować wszystkie usługi określonego typu? Na przykład wywołać określoną metodę u wszystkich prezenterów, które dziedziczą po konkretnym wspólnym przodku? Do tego służy 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, które są instancją tej klasy lub interfejsu + # 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ą ``` -Dekorator można również używać do ustawiania [tagów |services#Tagi] lub włączania trybu [inject |services#Tryb Inject]. +Dekoratora można też używać do ustawiania [tagów |services#Tagi] albo włączania [trybu inject |services#Tryb inject]. ```neon decorator: @@ -87,17 +87,17 @@ decorator: DI === -Techniczne ustawienia kontenera DI. +Ustawienia techniczne kontenera DI. ```neon di: - # wyświetlać DIC w Tracy Bar? - debugger: ... # (bool) domyślnie true + # pokazywać DIC w pasku Tracy? + debugger: ... # (bool) domyślnie autodetekcja (włączone, gdy Tracy jest obecne) - # typy parametrów, których nigdy nie autowirować + # typy parametrów, których nigdy nie autowirujesz excluded: ... # (string[]) - # zezwolić na lazy tworzenie usług? + # włączyć leniwe tworzenie usług? lazy: ... # (bool) domyślnie false # klasa, po której dziedziczy kontener DI @@ -105,21 +105,21 @@ di: ``` -Usługi lazy .{data-version:3.2.4} ---------------------------------- +Usługi leniwe .{data-version:3.2.4} +----------------------------------- -Ustawienie `lazy: true` aktywuje lazy (odroczone) tworzenie usług. Oznacza to, że usługi nie są faktycznie tworzone w momencie, gdy żądamy ich z kontenera DI, ale dopiero w chwili ich pierwszego użycia. Może to przyspieszyć start aplikacji i zmniejszyć zużycie pamięci, ponieważ tworzone są tylko te usługi, które są faktycznie potrzebne w danym żądaniu. +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 można [zmienić |services#Usługi lazy] lazy tworzenie. +Dla konkretnej usługi leniwe tworzenie można [dostosować |services#Usługi leniwe]. .[note] -Obiekty lazy można używać tylko dla klas użytkownika, a nie dla wewnętrznych klas PHP. Wymaga PHP 8.4 lub nowszego. +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ż wiele metadanych. Możesz ją zmniejszyć, redukując eksport metadanych. +Klasa kontenera DI zawiera również sporo metadanych. Możesz zmniejszyć jej rozmiar, ograniczając eksport metadanych. ```neon di: @@ -131,46 +131,46 @@ di: tags: # (string[]|bool) domyślnie wszystkie - event.subscriber - # eksportować dane dla autowiringu i które? + # 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 tablicy `$container->getParameters()`, możesz wyłączyć eksport parametrów. Ponadto możesz eksportować tylko te tagi, za pomocą których uzyskujesz usługi metodą `$container->findByTag(...)`. Jeśli metody nie wywołujesz w ogóle, możesz całkowicie wyłączyć eksport tagów za pomocą `false`. +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 znacznie zredukować metadane dla [autowiringu |autowiring] podając klasy, których używasz jako parametr metody `$container->getByType()`. I ponownie, jeśli metody nie wywołujesz w ogóle (lub tylko w [bootstrapie|application:bootstrapping] do uzyskania `Nette\Application\Application`), możesz eksport całkowicie wyłączyć za pomocą `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. W ten sposób dodajemy np. rozszerzenie DI `Dibi\Bridges\Nette\DibiExtension22` pod nazwą `dibi` +Rejestracja dodatkowych rozszerzeń DI. Tak dodasz na przykład rozszerzenie DI `Dibi\Bridges\Nette\DibiExtension3` pod nazwą `dibi`: ```neon extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -Następnie konfigurujemy je w sekcji `dibi`: +Konfigurujesz je potem w sekcji `dibi`: ```neon dibi: host: localhost ``` -Jako rozszerzenie można dodać również klasę, która ma parametry: +Jako rozszerzenie możesz też dodać klasę z parametrami: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` Dołączanie plików ================= -Kolejne pliki konfiguracyjne możemy dołączyć w sekcji `includes`: +Kolejne pliki konfiguracyjne można dołączyć w sekcji `includes`: ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -Nazwa `parameters.php` nie jest literówką, konfiguracja może być zapisana również w pliku PHP, który zwróci ją jako tablicę: +Nazwa `parameters.php` nie jest literówką; konfigurację można zapisać także w pliku PHP, który zwraca ją jako tablicę: ```php <?php @@ -192,15 +192,15 @@ return [ ]; ``` -Jeśli w plikach konfiguracyjnych pojawią się elementy o tych samych kluczach, zostaną nadpisane, lub w przypadku [tablic połączone |#Łączenie]. Później dołączany plik ma wyższy priorytet niż poprzedni. Plik, w którym znajduje się sekcja `includes`, ma wyższy priorytet niż w nim dołączane pliki. +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 ====== -Automatyczne dodawanie usług do kontenera DI niezwykle ułatwia pracę. Nette automatycznie dodaje do kontenera presentery, ale można łatwo dodawać również dowolne inne klasy. +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) ma szukać klas: +Wystarczy podać, w których katalogach (i podkatalogach) mają być wyszukiwane klasy: ```neon search: @@ -208,22 +208,29 @@ search: - in: %appDir%/Model ``` -Zazwyczaj jednak nie chcemy dodawać absolutnie wszystkich klas i interfejsów, dlatego możemy je filtrować: +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 według nazwy pliku (string|string[]) + # filtrowanie po nazwie pliku (string|string[]) files: - *Factory.php - # filtrowanie według nazwy klasy (string|string[]) + # filtrowanie po nazwie klasy (string|string[]) classes: - *Factory ``` -Lub możemy wybierać klasy, które dziedziczą lub implementują co najmniej jedną z podanych klas: +Albo możemy wybrać klasy, które dziedziczą po co najmniej jednej z wymienionych klas albo ją implementują: ```neon @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -Można zdefiniować również reguły wykluczające, tj. maski nazwy klasy lub przodków dziedziczenia, które jeśli pasują, usługa nie zostanie dodana do kontenera DI: +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: @@ -247,7 +254,7 @@ search: implements: ... ``` -Wszystkim usługom można ustawić tagi: +Wszystkim automatycznie zarejestrowanym usługom można przypisać tagi: ```neon search: @@ -255,11 +262,13 @@ search: 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. + -Łączenie +Scalanie ======== -Jeśli w wielu plikach konfiguracyjnych pojawią się elementy o tych samych kluczach, zostaną nadpisane, lub w przypadku tablic połączone. Później dołączany plik ma wyższy priorytet niż poprzedni. +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> @@ -292,7 +301,7 @@ items: </tr> </table> -W przypadku tablic można zapobiec łączeniu, dodając wykrzyknik za nazwą klucza: +Dla tablic scalaniu można zapobiec, dodając po nazwie klucza wykrzyknik: <table class=table> <tr> @@ -323,4 +332,4 @@ items: </tr> </table> -{{maintitle: Konfiguracja Dependency Injection}} +{{maintitle: Konfiguracja wstrzykiwania zależności}} diff --git a/dependency-injection/pl/container.texy b/dependency-injection/pl/container.texy index 37940a6e1b..0a2b376a75 100644 --- a/dependency-injection/pl/container.texy +++ b/dependency-injection/pl/container.texy @@ -1,16 +1,16 @@ -Co to jest kontener DI? -*********************** +Czym jest kontener DI? +********************** .[perex] -Kontener wstrzykiwania zależności (DIC) to klasa, która potrafi tworzyć instancje i konfigurować obiekty. +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 móc korzystać z zalet wstrzykiwania zależności (krótko DI). Przecież nawet w [rozdziale wstępnym|introduction] pokazaliśmy DI na konkretnych przykładach i żaden kontener nie był potrzebny. +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. -Jeśli jednak potrzebujesz zarządzać dużą liczbą różnych obiektów z wieloma zależnościami, kontener wstrzykiwania zależności będzie naprawdę przydatny. Co ma miejsce na przykład w przypadku aplikacji internetowych zbudowanych na frameworku. +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 `UserController`. Obie mają pewne zależności, a mianowicie bazę danych i fabrykę `ArticleFactory`. A dla tych klas utworzymy teraz kontener. Oczywiście dla tak prostego przykładu nie ma sensu mieć kontenera. Ale utworzymy go, aby pokazać, jak wygląda i działa. +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, hardkodowany kontener dla podanego przykładu: +Oto prosty, zapisany na sztywno kontener dla powyższego przykładu: ```php class Container @@ -25,23 +25,23 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` -Użycie wyglądałoby następująco: +Użycie wyglądałoby tak: ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -Kontenera pytamy tylko o obiekt i nie musimy już wiedzieć nic o tym, jak go utworzyć i jakie ma zależności; to wszystko wie kontener. Zależności są wstrzykiwane przez kontener automatycznie. W tym tkwi jego siła. +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. -Kontener ma na razie wszystkie dane zapisane na stałe. Zrobimy więc kolejny krok i dodamy parametry, aby kontener był rzeczywiście użyteczny: +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 @@ -70,9 +70,9 @@ $container = new Container([ ]); ``` -Bystrzy czytelnicy mogli zauważyć pewien problem. Za każdym razem, gdy pobieram obiekt `UserController`, tworzona jest również nowa instancja `ArticleFactory` i bazy danych. Tego zdecydowanie nie chcemy. +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 będzie zwracać zawsze te same instancje: +Dodamy więc metodę `getService()`, która zawsze zwróci te same instancje: ```php class Container @@ -87,7 +87,7 @@ class Container public function getService(string $name): object { if (!isset($this->services[$name])) { - // getService('Database') będzie wywoływać createDatabase() + // getService('Database') wywoła createDatabase() $method = 'create' . $name; $this->services[$name] = $this->$method(); } @@ -98,9 +98,9 @@ class Container } ``` -Przy pierwszym wywołaniu np. `$container->getService('Database')` zleci `createDatabase()` utworzenie obiektu bazy danych, który zapisze w tablicy `$services`, a przy następnym wywołaniu od razu go zwróci. +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ę. -Zmodyfikujemy również resztę kontenera, aby używał `getService()`: +Modyfikujemy również resztę kontenera, aby używała `getService()`: ```php class Container @@ -112,16 +112,16 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -Nawiasem mówiąc, terminem usługa określa się dowolny obiekt zarządzany przez kontener. Stąd też nazwa metody `getService()`. +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 funkcjonalny kontener DI! I możemy go użyć: +Gotowe. Mamy w pełni działający kontener DI! I możemy go używać: ```php $container = new Container([ @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -Jak widzisz, napisanie DIC nie jest niczym skomplikowanym. Warto przypomnieć, że same obiekty nie wiedzą, że tworzy je jakiś kontener. Dzięki temu można w ten sposób tworzyć dowolny obiekt w PHP bez ingerencji w jego kod źródłowy. +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 dość szybko stać się koszmarem. Dlatego w następnym rozdziale opowiemy o [Kontenerze Nette DI|nette-container], który potrafi generować się i aktualizować niemal sam. +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: Co to jest kontener wstrzykiwania zależności?}} +{{maintitle: Czym jest kontener wstrzykiwania zależności?}} diff --git a/dependency-injection/pl/extensions.texy b/dependency-injection/pl/extensions.texy index 827dd74ec4..48d28d5991 100644 --- a/dependency-injection/pl/extensions.texy +++ b/dependency-injection/pl/extensions.texy @@ -2,38 +2,66 @@ Tworzenie rozszerzeń dla Nette DI ********************************* .[perex] -Na generowanie kontenera DI oprócz plików konfiguracyjnych wpływają również tzw. *rozszerzenia*. Aktywujemy je w pliku konfiguracyjnym w sekcji `extensions`. +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ć. -W ten sposób dodajemy rozszerzenie reprezentowane przez klasę `BlogExtension` pod nazwą `blog`: +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 ``` -Każde rozszerzenie kompilatora dziedziczy po [api:Nette\DI\CompilerExtension] i może implementować następujące metody, które są kolejno wywoływane podczas budowania kontenera DI: +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. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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]) -getConfigSchema() .[method] -=========================== +.[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. -Ta metoda jest wywoływana jako pierwsza. Definiuje schemat do walidacji parametrów konfiguracyjnych. +.[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]. -Rozszerzenie konfigurujemy w sekcji, której nazwa jest taka sama jak ta, pod którą rozszerzenie zostało dodane, czyli `blog`: + +Pierwsze rozszerzenie +===================== + +Oto małe, ale kompletne rozszerzenie. Aktywujemy je i konfigurujemy w tym samym pliku: ```neon -# ta sama nazwa co rozszerzenie +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Tworzymy schemat opisujący wszystkie opcje konfiguracyjne, w tym ich typy, dozwolone wartości i ewentualnie wartości domyślne: +A oto cała klasa: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -Dokumentację znajdziesz na stronie [Schema |schema:]. Dodatkowo można określić, które opcje mogą być [dynamiczne |application:bootstrapping#Parametry dynamiczne] za pomocą `dynamic()`, np. `Expect::int()->dynamic()`. -Do konfiguracji dostajemy się przez zmienną `$this->config`, która jest obiektem `stdClass`: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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ć. -loadConfiguration() .[method] -============================= +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. -Służy do dodawania usług do kontenera. Do tego służy [api:Nette\DI\ContainerBuilder]: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // lub setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -Konwencją jest prefiksowanie usług dodanych przez rozszerzenie jego nazwą, aby nie dochodziło do konfliktów nazw. Robi to metoda `prefix()`, więc jeśli rozszerzenie nazywa się `blog`, usługa będzie nosić nazwę `blog.articles`. +Zwalidowana konfiguracja dostępna jest w `$this->config` jako obiekt `stdClass` (albo jako tablica, jeśli dopiszesz do schematu `castTo('array')`). -Jeśli potrzebujemy zmienić nazwę usługi, możemy ze względu na zachowanie wstecznej kompatybilności utworzyć alias z pierwotną nazwą. Podobnie robi Nette np. w przypadku usługi `routing.router`, która jest dostępna również pod wcześniejszą nazwą `router`. +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 -$builder->addAlias('router', 'routing.router'); +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ć: -Ładowanie usług z pliku ------------------------ +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -Usługi możemy tworzyć nie tylko za pomocą API klasy ContainerBuilder, ale także znanym zapisem używanym w pliku konfiguracyjnym NEON w sekcji services. Prefiks `@extension` reprezentuje bieżące rozszerzenie. +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: @@ -107,88 +160,284 @@ services: 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ę: - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Usługi wczytamy: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // wczytanie pliku konfiguracyjnego dla rozszerzenia - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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. -beforeCompile() .[method] -========================= +Możesz też współpracować z innymi zarejestrowanymi rozszerzeniami, pozyskanymi przez `$this->compiler->getExtensions()`, opcjonalnie przefiltrowanymi po klasie albo interfejsie: -Metoda jest wywoływana w momencie, gdy kontener zawiera wszystkie usługi dodane przez poszczególne rozszerzenia w metodach `loadConfiguration` oraz przez użytkownika w plikach konfiguracyjnych. Na tym etapie budowania możemy więc modyfikować definicje usług lub uzupełniać powiązania między nimi. Do wyszukiwania usług w kontenerze według tagów można użyć metody `findByTag()`, a według klasy lub interfejsu metody `findByType()`. +```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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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. + -afterCompile() .[method] -======================== +ContainerBuilder +================ -Na tym etapie klasa kontenera jest już wygenerowana w postaci obiektu [ClassType |php-generator:#Klasy], zawiera wszystkie metody tworzące usługi i jest gotowa do zapisu do cache. Wynikowy kod klasy możemy na tym etapie jeszcze zmodyfikować. +[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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Typowym zastosowaniem jest rejestrowanie panelu Tracy tylko w trybie deweloperskim. + -$initialization .[method] -========================= +Argumenty złożone +----------------- -Klasa Configurator po [utworzeniu kontenera |application:bootstrapping#index.php] wywołuje kod inicjalizacyjny, który tworzy się zapisem do obiektu `$this->initialization` za pomocą [metody addBody() |php-generator:#Ciała metod i funkcji]. +Czasem argument fabryki albo wywołania w setupie nie jest zwykłą wartością, nazwą klasy ani referencją `@service`. Na takie przypadki są: -Pokażemy przykład, jak na przykład kodem inicjalizacyjnym uruchomić sesję lub uruchomić usługi, które mają tag `run`: +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // automatyczne uruchamianie sesji - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // ten tag będzie eksportowany zawsze, nawet gdy eksport jest ograniczony + $this->compiler->addExportedTag('event.subscriber'); - // usługi z tagiem run muszą być utworzone po instancjonowaniu kontenera - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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 index ae649ef771..7106fa5f68 100644 --- a/dependency-injection/pl/factory.texy +++ b/dependency-injection/pl/factory.texy @@ -2,11 +2,11 @@ Generowane fabryki ****************** .[perex] -Nette DI potrafi automatycznie generować kod fabryk na podstawie interfejsów, co oszczędza Ci pisania kodu. +Nette DI potrafi automatycznie wygenerować kod fabryki na podstawie interfejsów, oszczędzając Ci pisania kodu. -Fabryka to klasa, która tworzy i konfiguruje obiekty. Przekazuje im więc również ich zależności. Proszę nie mylić z wzorcem projektowym *factory method*, który opisuje specyficzny sposób wykorzystania fabryk i nie jest związany z tym tematem. +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 wygląda taka fabryka, pokazaliśmy w [rozdziale wstępnym |introduction#Fabryka]: +Jak taka fabryka wygląda, pokazaliśmy w [rozdziale wprowadzającym |introduction#Fabryka]: ```php class ArticleFactory @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DI potrafi automatycznie generować kod fabryk. Wszystko, co musisz zrobić, to utworzyć interfejs, a Nette DI wygeneruje implementację. Interfejs musi mieć dokładnie jedną metodę o nazwie `create` i deklarować typ zwracany: +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 @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -Czyli fabryka `ArticleFactory` ma metodę `create`, która tworzy obiekty `Article`. Klasa `Article` może wyglądać na przykład następująco: +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 @@ -44,16 +44,16 @@ class Article } ``` -Fabrykę dodajemy do pliku konfiguracyjnego: +Dodaj fabrykę do pliku konfiguracyjnego: ```neon services: - ArticleFactory ``` -Nette DI wygeneruje odpowiednią implementację fabryki. +Nette DI wygeneruje odpowiadającą jej implementację fabryki. -W kodzie, który używa fabryki, żądamy obiektu według interfejsu, a Nette DI użyje wygenerowanej implementacji: +W kodzie używającym fabryki poproś o obiekt przez jego interfejs, a Nette DI dostarczy wygenerowaną implementację: ```php class UserController @@ -65,17 +65,17 @@ class UserController public function foo() { - // zlecamy fabryce utworzenie obiektu + // pozwalamy fabryce utworzyć obiekt $article = $this->articleFactory->create(); } } ``` -Fabryka sparametryzowana -======================== +Fabryka z parametrami +===================== -Metoda fabryczna `create` może przyjmować parametry, które następnie przekaże do konstruktora. Uzupełnijmy na przykład klasę `Article` o ID autora artykułu: +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 @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -Dzięki temu, że parametr w konstruktorze i parametr w fabryce nazywają się tak samo, Nette DI przekaże je całkowicie automatycznie. +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 formie wieloliniowej za pomocą klucza `implement`: +Definicję można zapisać również w postaci wielowierszowej, używając klucza `implement`: ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -Przy zapisie tym dłuższym sposobem można podać dodatkowe argumenty dla konstruktora w kluczu `arguments` oraz dodatkową konfigurację za pomocą `setup`, tak samo jak w przypadku zwykłych usług. +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ć stałą wartość w konfiguracji, która byłaby przekazywana do konstruktora `Article`: +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: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -Lub odwrotnie, gdyby `create()` przyjmowała parametr `$authorId`, ale nie byłby on częścią konstruktora i przekazywany byłby metodą `Article::setAuthorId()`, odwołalibyśmy się do niego w sekcji `setup`: +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: @@ -134,14 +134,14 @@ services: ``` -Accessor -======== +Akcesor +======= -Nette potrafi oprócz fabryk generować również tzw. akcesory. Są to obiekty z metodą `get()`, która zwraca określoną usługę z kontenera DI. Powtarzane wywołanie `get()` zwraca zawsze tę samą instancję. +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ą lazy-loading zależności. Miejmy klasę, która zapisuje błędy do specjalnej bazy danych. Gdyby ta klasa otrzymywała połączenie z bazą danych jako zależność przez konstruktor, połączenie musiałoby być zawsze tworzone, chociaż w praktyce błąd pojawia się tylko wyjątkowo, a więc w większości przypadków połączenie pozostałoby niewykorzystane. Zamiast tego klasa przekaże sobie akcesor i dopiero gdy zostanie wywołana jego metoda `get()`, dojdzie do utworzenia obiektu bazy danych: +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` i deklarować typ zwracany: +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 @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -Akcesor dodajemy do pliku konfiguracyjnego, gdzie znajduje się również definicja usługi, którą będzie zwracał: +Dodaj akcesor do pliku konfiguracyjnego wraz z definicją usługi, którą ma zwracać: ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -Ponieważ akcesor zwraca usługę typu `PDO`, a w konfiguracji jest jedyna taka usługa, będzie zwracał właśnie ją. Gdyby usług danego typu było więcej, określimy zwracaną usługę za pomocą nazwy, np. `- PDOAccessor(@db1)`. +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)`. -Wielokrotna fabryka/akcesor -=========================== -Nasze fabryki i akcesory potrafiły dotychczas zawsze tworzyć lub zwracać tylko jeden obiekt. Można jednak bardzo łatwo utworzyć również wielokrotne fabryki połączone z akcesorami. Interfejs takiej klasy będzie zawierał dowolną liczbę metod o nazwach `create<name>()` i `get<name>()`, np.: +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 @@ -173,9 +174,9 @@ interface MultiFactory } ``` -Więc zamiast przekazywać sobie kilka generowanych fabryk i akcesorów, przekażemy jedną bardziej złożoną fabrykę, która potrafi więcej. +Zamiast wstrzykiwać wiele osobnych fabryk i akcesorów, możesz więc wstrzyknąć jeden, bardziej kompleksowy komponent. -Alternatywnie można zamiast kilku metod użyć `get()` z parametrem: +Alternatywnie zamiast wielu metod można użyć `get()` z parametrem: ```php interface MultiFactoryAlt @@ -184,22 +185,24 @@ interface MultiFactoryAlt } ``` -Wtedy obowiązuje, że `MultiFactory::getArticle()` robi to samo co `MultiFactoryAlt::get('article')`. Jednak alternatywny zapis ma tę wadę, że nie jest oczywiste, jakie wartości `$name` są obsługiwane i logicznie rzecz biorąc, nie można również w interfejsie rozróżnić różnych wartości zwracanych dla różnych `$name`. +`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 listą ---------------- -W ten sposób można zdefiniować wielokrotną fabrykę w konfiguracji: .{data-version:3.2.0} +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() + article: Article() # definiuje createArticle() db: PDO(%dsn%, %user%, %password%) # definiuje getDb() ) ``` -Lub możemy w definicji fabryki odwołać się do istniejących usług za pomocą referencji: +Alternatywnie możesz odwołać się w definicji multifabryki do istniejących usług za pomocą referencji: ```neon services: @@ -212,15 +215,19 @@ services: ``` -Definicja za pomocą tagów -------------------------- +Definicja z tagami +------------------ -Drugą możliwością jest wykorzystanie do definicji [tagów |services#Tagi]: +Innym sposobem zdefiniowania multifabryki jest użycie [tagów |services#Tagi]. Wartość tagu określa nazwę odpowiadającej metody: ```neon services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index dd9bdfd93d..7013fb6240 100644 --- a/dependency-injection/pl/faq.texy +++ b/dependency-injection/pl/faq.texy @@ -2,93 +2,93 @@ Często zadawane pytania o DI (FAQ) ********************************** -Czy DI to inna nazwa dla IoC? ------------------------------ +Czy DI to inna nazwa IoC? +------------------------- -*Inversion of Control* (IoC) to zasada skupiająca się na sposobie, w jaki kod jest uruchamiany - czy Twój kod uruchamia obcy kod, czy Twój kod jest integrowany z obcym kodem, który go następnie wywołuje. IoC to szerokie pojęcie obejmujące [zdarzenia |nette:glossary#Eventy zdarzenia], tak zwaną [zasadę Hollywood |application:components#Styl Hollywood] i inne aspekty. Częścią tej koncepcji są również fabryki, o których mówi [Reguła nr 3: zostaw to fabryce |introduction#Zasada nr 3: zostaw to fabryce], które stanowią inwersję dla operatora `new`. +*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 sposobie, w jaki jeden obiekt dowiaduje się o innym obiekcie, czyli o jego zależnościach. Jest to wzorzec projektowy, który wymaga jawnego przekazywania zależności między obiektami. +*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ć. -Można więc powiedzieć, że DI jest specyficzną formą IoC. Jednak nie wszystkie formy IoC są odpowiednie z punktu widzenia czystości kodu. Na przykład do antywzorców należą techniki, które pracują z [globalnym stanem |global-state] lub tak zwany [Service Locator |#Co to jest Service Locator]. +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?]. -Co to jest Service Locator? ---------------------------- +Czym jest Service Locator? +-------------------------- -Jest to alternatywa dla Dependency Injection. Działa tak, że tworzy centralne repozytorium, w którym rejestrowane są wszystkie dostępne usługi lub zależności. Kiedy obiekt potrzebuje zależności, prosi o nią 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 do Dependency Injection traci jednak na przejrzystości: zależności nie są przekazywane obiektom bezpośrednio i nie są tak łatwo identyfikowalne, co wymaga przeanalizowania kodu, aby wszystkie powiązania zostały odkryte i zrozumiane. Testowanie jest również bardziej skomplikowane, ponieważ nie możemy po prostu przekazywać obiektów mock do testowanych obiektów, ale musimy to robić przez Service Locator. Ponadto Service Locator narusza projekt kodu, ponieważ poszczególne obiekty muszą wiedzieć o jego istnieniu, co różni się od Dependency Injection, gdzie obiekty nie mają świadomości istnienia kontenera DI. +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 żadne trudności związane z użyciem wzorca projektowego Dependency Injection. Wręcz przeciwnie, pobieranie zależności z globalnie dostępnych miejsc prowadzi do [całego szeregu komplikacji |global-state], podobnie jak używanie Service Locatora. Dlatego warto zawsze korzystać z DI. To nie jest podejście dogmatyczne, ale po prostu nie znaleziono lepszej alternatywy. +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. -Mimo to istnieją pewne sytuacje, w których nie przekazujemy sobie obiektów i pobieramy je z przestrzeni globalnej. Na przykład podczas debugowania kodu, gdy potrzebujesz w konkretnym punkcie programu wypisać wartość zmiennej, zmierzyć czas trwania określonej części programu lub zapisać komunikat. W takich przypadkach, gdy chodzi o tymczasowe czynności, które zostaną później usunięte z kodu, uzasadnione jest wykorzystanie globalnie dostępnego dumpera, stopera lub loggera. Te narzędzia bowiem nie należą do projektu kodu. +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 swoje wady? ------------------------------- +Czy używanie DI ma wady? +------------------------ -Czy użycie Dependency Injection wiąże się z jakimiś wadami, takimi jak zwiększona pracochłonność pisania kodu lub pogorszona wydajność? Co tracimy, gdy zaczniemy pisać kod zgodnie z DI? +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? -DI nie ma wpływu na wydajność ani zużycie pamięci aplikacji. Pewną rolę może odgrywać wydajność Kontenera DI, jednak w przypadku [Nette DI |nette-container] kontener jest kompilowany do czystego PHP, więc jego narzut podczas działania aplikacji jest w zasadzie zerowy. +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. -Podczas pisania kodu często konieczne jest tworzenie konstruktorów przyjmujących zależności. Kiedyś mogło to być czasochłonne, jednak dzięki nowoczesnym IDE i [constructor property promotion |https://blog.nette.org/pl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] jest to teraz kwestia kilku sekund. Fabryki można łatwo generować za pomocą Nette DI i wtyczki do PhpStorm jednym kliknięciem myszy. Z drugiej strony odpada potrzeba pisania singletonów i statycznych punktów dostępu. +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. -Można stwierdzić, że poprawnie zaprojektowana aplikacja wykorzystująca DI nie jest ani krótsza, ani dłuższa w porównaniu z aplikacją wykorzystującą singletony. Części kodu pracujące z zależnościami są jedynie wyjęte z poszczególnych klas i przeniesione do nowych miejsc, czyli do kontenera DI i fabryk. +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ć aplikację legacy na DI? -------------------------------------- +Jak przepisać starą aplikację na DI? +------------------------------------ -Przejście z aplikacji legacy na Dependency Injection może być wymagającym procesem, zwłaszcza w przypadku dużych i złożonych aplikacji. Ważne jest, aby podchodzić do tego procesu systematycznie. +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. -- Podczas przechodzenia na Dependency Injection ważne jest, aby wszyscy członkowie zespołu rozumieli zasady i procedury, które są stosowane. -- Najpierw przeprowadź analizę istniejącej aplikacji i zidentyfikuj kluczowe komponenty oraz ich zależności. Stwórz plan, które części będą refaktoryzowane i w jakiej kolejności. -- Zaimplementuj kontener DI lub jeszcze lepiej użyj istniejącej biblioteki, na przykład Nette DI. -- Stopniowo refaktoryzuj poszczególne części aplikacji, aby używały Dependency Injection. Może to obejmować modyfikacje konstruktorów lub metod tak, aby przyjmowały zależności jako parametry. -- Zmodyfikuj miejsca w kodzie, gdzie tworzone są obiekty z zależnościami, aby zamiast tego zależności były wstrzykiwane przez kontener. Może to obejmować użycie fabryk. +- 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 Dependency Injection to inwestycja w jakość kodu i długoterminową utrzymywalność aplikacji. Chociaż przeprowadzenie tych zmian może być trudne, wynikiem powinien być czystszy, bardziej modularny i łatwo testowalny kod, który jest gotowy na przyszłe rozszerzenia i konserwację. +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 preferuje się kompozycję nad dziedziczeniem? ------------------------------------------------------ -Lepiej jest używać [kompozycji |nette:introduction-to-object-oriented-programming#Kompozycja] zamiast [dziedziczenia |nette:introduction-to-object-oriented-programming#Dziedziczenie], ponieważ służy ona do ponownego wykorzystania kodu, nie martwiąc się o konsekwencje zmian. Zapewnia więc luźniejsze powiązanie, dzięki czemu nie musimy się obawiać, że zmiana jakiegoś kodu spowoduje potrzebę zmiany innego zależnego kodu. Typowym przykładem jest sytuacja określana jako [constructor hell |passing-dependencies#Constructor hell]. +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 można użyć Nette DI Container poza Nette? ---------------------------------------------- +Czy Nette DI Container da się używać poza Nette? +------------------------------------------------ -Zdecydowanie. Nette DI Container jest częścią Nette, ale został zaprojektowany jako samodzielna biblioteka, która może być używana niezależnie od pozostałych części frameworka. Wystarczy ją zainstalować za pomocą Composera, utworzyć plik konfiguracyjny z definicją Twoich usług, a następnie za pomocą kilku linii kodu PHP utworzyć kontener DI. I od razu możesz zacząć korzystać z zalet Dependency Injection w swoich projektach. +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. -Jak wygląda konkretne użycie wraz z kodami opisuje rozdział [Nette DI Container |nette-container]. +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 odczytania język konfiguracyjny, który został opracowany w ramach Nette do ustawiania aplikacji, usług i ich zależności. W porównaniu z JSONem lub YAMLem oferuje dla tego celu znacznie bardziej intuicyjne i elastyczne możliwości. W NEONie można naturalnie opisać powiązania, których w Symfony & YAMLu nie dałoby się zapisać albo w ogóle, albo tylko za pomocą skomplikowanego opisu. +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 nie spowalnia aplikacji? ---------------------------------------------------- +Czy parsowanie plików NEON spowalnia aplikację? +----------------------------------------------- -Chociaż pliki NEON parsują się bardzo szybko, ten aspekt w ogóle nie ma znaczenia. Powodem jest to, że parsowanie plików odbywa się tylko raz przy pierwszym uruchomieniu aplikacji. Następnie generowany jest kod kontenera DI, zapisywany na dysku i uruchamiany przy każdym kolejnym żądaniu, bez konieczności przeprowadzania dalszego parsowania. +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 rozwoju pliki NEON są parsowane za każdym razem, gdy dojdzie do zmiany ich zawartości, aby programista miał zawsze aktualny kontener DI. Samo parsowanie jest, jak powiedziano, kwestią chwili. +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 dostać się z mojej klasy do parametrów w pliku konfiguracyjnym? -------------------------------------------------------------------- +Jak sięgnąć w klasie po parametry z pliku konfiguracyjnego? +----------------------------------------------------------- -Pamiętajmy o [Regule nr 1: niech Ci to przekażą |introduction#Zasada nr 1: niech ci to przekażą]. Jeśli klasa wymaga informacji z pliku konfiguracyjnego, nie musimy zastanawiać się, jak się do tych informacji dostać, zamiast tego po prostu o nie prosimy - na przykład za pomocą konstruktora klasy. A przekazanie realizujemy w pliku konfiguracyjnym. +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%` jest symbolem zastępczym dla wartości parametru `myParameter`, który zostanie przekazany do konstruktora klasy `MyClass`: +W tym przykładzie `%myParameter%` to symbol zastępczy wartości parametru `myParameter`, która zostanie przekazana do konstruktora `MyClass`: -```php +```neon # config.neon parameters: myParameter: Some value @@ -97,10 +97,27 @@ services: - MyClass(%myParameter%) ``` -Aby przekazywać więcej parametrów lub wykorzystać autowiring, warto [opakować parametry w obiekt |best-practices:passing-settings-to-presenters]. +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 PSR-11: Container interface? ------------------------------------------------- +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]: -Nette DI Container nie obsługuje bezpośrednio PSR-11. Jednakże, jeśli potrzebujesz interoperacyjności między Nette DI Containerem a bibliotekami lub frameworkami, które oczekują PSR-11 Container Interface, możesz utworzyć [prosty adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], który będzie służył jako most między Nette DI Containerem a PSR-11. +- **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 index 8d9e41920d..0e478ec9d0 100644 --- a/dependency-injection/pl/global-state.texy +++ b/dependency-injection/pl/global-state.texy @@ -2,42 +2,42 @@ Stan globalny i singletony ************************** .[perex] -Ostrzeżenie: Poniższe konstrukcje są oznaką źle zaprojektowanego kodu: +Ostrzeżenie: poniższe konstrukcje są objawami źle zaprojektowanego kodu: - `Foo::getInstance()` - `DB::insert(...)` - `Article::setDb($db)` -- `ClassName::$var` lub `static::$var` +- `ClassName::$var` albo `static::$var` -Czy niektóre z tych konstrukcji występują w Twoim kodzie? W takim razie masz okazję do jego ulepszenia. Być może myślisz, że są to powszechne konstrukcje, które widzisz nawet w przykładowych rozwiązaniach różnych bibliotek i frameworków. Jeśli tak jest, to projekt ich kodu nie jest dobry. +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. -Teraz zdecydowanie nie mówimy o jakiejś akademickiej czystości. Wszystkie te konstrukcje mają jedną wspólną cechę: wykorzystują stan globalny. A ten ma destrukcyjny wpływ na jakość kodu. Klasy kłamią o swoich zależnościach. Kod staje się nieprzewidywalny. Mylą programistów i obniżają ich efektywność. +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ązanie +Globalne powiązania ------------------- -W idealnym świecie obiekt powinien móc komunikować się tylko z obiektami, które zostały mu [bezpośrednio przekazane |passing-dependencies]. Jeśli utworzę dwa obiekty `A` i `B` i nigdy nie przekażę referencji między nimi, to ani `A`, ani `B` nie mogą dostać się do drugiego obiektu ani zmienić jego stanu. To jest bardzo pożądana właściwość kodu. Jest to podobne do sytuacji, gdy masz baterię i żarówkę; żarówka nie zaświeci, dopóki nie połączysz jej z baterią drutem. +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. -Ale to nie dotyczy globalnych (statycznych) zmiennych lub singletonów. Obiekt `A` mógłby *bezprzewodowo* dostać się do obiektu `C` i zmodyfikować go bez jakiegokolwiek przekazania referencji, wywołując `C::changeSomething()`. Jeśli obiekt `B` również chwyci globalne `C`, to `A` i `B` mogą wzajemnie na siebie wpływać za pośrednictwem `C`. +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życie globalnych zmiennych wprowadza do systemu nową formę *bezprzewodowego* powiązania, która nie jest widoczna z zewnątrz. Tworzy zasłonę dymną utrudniającą zrozumienie i używanie kodu. Aby programiści rzeczywiście zrozumieli zależności, muszą przeczytać każdą linię kodu źródłowego. Zamiast jedynie zapoznać się z interfejsem klas. Jest to ponadto powiązanie całkowicie zbędne. Stan globalny jest używany dlatego, że jest łatwo dostępny z dowolnego miejsca i pozwala na przykład zapisać do bazy danych za pomocą globalnej (statycznej) metody `DB::insert()`. Ale jak pokażemy, korzyść, którą to przynosi, jest znikoma, natomiast komplikacje, które powoduje, są fatalne. +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] -Z punktu widzenia zachowania nie ma różnicy między zmienną globalną a statyczną. Są równie szkodliwe. +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 słynnie nazwał w 1935 roku Albert Einstein zjawisko w fizyce kwantowej, które przyprawiało go o gęsią skórkę. -Chodzi o splątanie kwantowe, którego osobliwością jest to, że gdy zmierzysz informację o jednej cząstce, natychmiast wpływasz na drugą cząstkę, nawet jeśli są oddalone od siebie o miliony lat świetlnych. Co pozornie narusza podstawowe prawo wszechświata, że nic nie może poruszać się szybciej niż światł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 możemy nazwać "upiornym działaniem na odległość" sytuację, gdy uruchamiamy jakiś proces, o którym sądzimy, że jest izolowany (ponieważ nie przekazaliśmy mu żadnych referencji), ale w odległych miejscach systemu dochodzi do nieoczekiwanych interakcji i zmian stanu, o których nie mieliśmy pojęcia. Może do tego dojść tylko za pośrednictwem stanu globalnego. +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 programistów projektu, który ma obszerną, dojrzałą bazę kodu. Twój nowy przełożony prosi Cię o zaimplementowanie nowej funkcji, a Ty jako dobry programista zaczynasz od napisania testu. Ale ponieważ jesteś nowy w projekcie, robisz wiele testów eksploracyjnych typu "co się stanie, jeśli wywołam tę metodę". I próbujesz napisać następujący test: +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() @@ -47,17 +47,17 @@ function testCreditCardCharge() } ``` -Uruchamiasz kod, może kilka razy, i po jakimś czasie zauważasz na telefonie powiadomienia z banku, że przy każdym uruchomieniu pobrano 100 dolarów z Twojej karty płatniczej 🤦‍♂️ +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! 🤦‍♂️ -Jak, do diabła, test mógł spowodować rzeczywiste pobranie pieniędzy? Operowanie kartą płatniczą nie jest łatwe. Musisz komunikować się z usługą internetową strony trzeciej, musisz znać adres URL tej usługi, musisz się zalogować i tak dalej. Żadna z tych informacji nie jest zawarta w teście. Co gorsza, nawet nie wiesz, gdzie te informacje się znajdują, a więc ani jak mockować zewnętrzne zależności, aby każde uruchomienie nie prowadziło do ponownego pobrania 100 dolarów. I skąd jako nowy programista miałeś wiedzieć, że to, co zamierzasz zrobić, doprowadzi do tego, że będziesz o 100 dolarów biedniejszy? +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 jest upiorne działanie na odległość! +To właśnie upiorne działanie na odległość! -Nie pozostaje Ci nic innego, jak długo grzebać w mnóstwie kodu źródłowego, pytać starszych i bardziej doświadczonych kolegów, zanim zrozumiesz, jak działają powiązania w projekcie. Jest to spowodowane tym, że patrząc na interfejs klasy `CreditCard`, nie można zidentyfikować stanu globalnego, który należy zainicjować. Nawet spojrzenie na kod źródłowy klasy nie powie Ci, którą metodę inicjalizacyjną masz wywołać. W najlepszym przypadku możesz znaleźć globalną zmienną, do której uzyskuje się dostęp, i na jej podstawie próbować odgadnąć, jak ją zainicjować. +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. Karta płatnicza udaje, że wystarczy ją utworzyć i wywołać metodę `charge()`. W ukryciu jednak współpracuje z inną klasą `PaymentGateway`, która reprezentuje bramkę płatniczą. Jej interfejs również mówi, że można ją zainicjować samodzielnie, ale w rzeczywistości pobiera dane uwierzytelniające z jakiegoś pliku konfiguracyjnego i tak dalej. Programistom, którzy napisali ten kod, jest jasne, że `CreditCard` potrzebuje `PaymentGateway`. Napisali kod w ten sposób. Ale dla każdego, kto jest nowy w projekcie, jest to kompletna zagadka i utrudnia naukę. +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ć sytuację? Łatwo. **Niech API deklaruje zależności.** +Jak naprawić tę sytuację? Łatwo. **Niech API deklaruje zależności.** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -Zauważ, jak nagle powiązania wewnątrz kodu stają się oczywiste. Dzięki temu, że metoda `charge()` deklaruje, że potrzebuje `PaymentGateway`, nie musisz nikogo pytać, jak kod jest powiązany. Wiesz, że musisz utworzyć jej instancję, a gdy spróbujesz to zrobić, natkniesz się na to, że musisz podać parametry dostępu. Bez nich kod nie dałby się nawet uruchomić. +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, teraz możesz mockować bramkę płatniczą, dzięki czemu przy każdym uruchomieniu testu nie zostanie Ci naliczone 100 dolarów. +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 powoduje, że Twoje obiekty mogą potajemnie uzyskiwać dostęp do rzeczy, które nie są zadeklarowane w ich API, a w rezultacie czynią z Twoich API patologicznych kłamcó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. -Być może wcześniej o tym tak nie myślałeś, ale za każdym razem, gdy używasz stanu globalnego, tworzysz tajne bezprzewodowe kanały komunikacyjne. Upiorne działanie na odległość zmusza programistów do czytania każdej linii kodu, aby zrozumieć potencjalne interakcje, obniża produktywność programistów i myli nowych członków zespołu. Jeśli to Ty stworzyłeś kod, znasz rzeczywiste zależności, ale każdy, kto przyjdzie po Tobie, jest bezradny. +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. -Nie pisz kodu, który wykorzystuje stan globalny, preferuj przekazywanie zależności. Czyli dependency injection. +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, który używa stanu globalnego i singletonów, nigdy nie jest pewne, kiedy i kto ten stan zmienił. To ryzyko pojawia się już przy inicjalizacji. Poniższy kod ma utworzyć połączenie z bazą danych i zainicjować bramkę płatniczą, jednak ciągle rzuca wyjątek, a znalezienie przyczyny jest niezwykle czasochłonne: +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 szczegółowo przeglądać kod, aby dowiedzieć się, że obiekt `PaymentGateway` uzyskuje bezprzewodowy dostęp do innych obiektów, z których niektóre wymagają połączenia z bazą danych. Czyli konieczne jest zainicjowanie bazy danych przed `PaymentGateway`. Jednak zasłona dymna stanu globalnego ukrywa to przed Tobą. Ile czasu byś zaoszczędził, gdyby API poszczególnych klas nie kłamało i deklarowało swoje zależności? +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, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -Podobny problem pojawia się również przy użyciu globalnego dostępu do połączenia z bazą danych: +Podobny problem pojawia się przy globalnym dostępie do połączenia z bazą danych: ```php use Illuminate\Support\Facades\DB; @@ -110,9 +110,9 @@ class Article } ``` -Przy wywołaniu metody `save()` nie jest pewne, czy połączenie z bazą danych zostało już utworzone i kto jest odpowiedzialny za jego utworzenie. Jeśli chcemy na przykład zmieniać połączenie z bazą danych w trakcie działania, na przykład w celu testów, musielibyśmy najprawdopodobniej utworzyć dodatkowe metody, takie jak `DB::reconnect(...)` lub `DB::reconnectForTest()`. +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żmy przykład: +Rozważ przykład: ```php $article = new Article; @@ -122,9 +122,9 @@ 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 sprawdzić, musielibyśmy przeanalizować kod źródłowy klasy `Foo` i prawdopodobnie wielu innych klas. To podejście przyniosłoby jednak tylko krótkoterminową odpowiedź, ponieważ sytuacja może się w przyszłości zmienić. +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 co jeśli przeniesiemy połączenie z bazą danych do zmiennej statycznej wewnątrz klasy `Article`? +A gdybyśmy przenieśli połączenie z bazą danych do zmiennej statycznej wewnątrz klasy `Article`? ```php class Article @@ -143,11 +143,11 @@ class Article } ``` -To w ogóle nic nie zmieniło. Problemem jest stan globalny i jest zupełnie obojętne, w której klasie się ukrywa. W tym przypadku, podobnie jak w poprzednim, przy wywołaniu metody `$article->save()` nie mamy żadnej wskazówki co do tego, do jakiej bazy danych zostanie zapisany. Ktokolwiek na drugim końcu aplikacji mógł w dowolnym momencie za pomocą `Article::setDb()` zmienić bazę danych. Nam pod nosem. +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ę **niezwykle kruchą**. +Stan globalny czyni naszą aplikację **wyjątkowo kruchą**. -Istnieje jednak prosty sposób, aby poradzić sobie z tym problemem. Wystarczy pozwolić API deklarować zależności, co zapewni poprawną funkcjonalność. +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 @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -Dzięki temu podejściu znika obawa o ukryte i nieoczekiwane zmiany połączenia z bazą danych. Teraz mamy pewność, gdzie artykuł jest zapisywany, a żadne modyfikacje kodu wewnątrz innej, niepowiązanej klasy nie mogą już zmienić sytuacji. Kod nie jest już kruchy, ale stabilny. +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. -Nie pisz kodu, który wykorzystuje stan globalny, preferuj przekazywanie zależności. Czyli dependency injection. +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 według [definicji|https://en.wikipedia.org/wiki/Singleton_pattern] ze znanej publikacji Gang of Four ogranicza klasę do jednej instancji i oferuje do niej globalny dostęp. Implementacja tego wzorca zwykle przypomina następujący kod: +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 @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // i inne metody pełniące funkcje danej klasy + // 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 jest uważany za antywzorzec. +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 w swoim kodzie singletonów i zastąp je innymi mechanizmami. Singletony naprawdę nie są potrzebne. Jeśli jednak potrzebujesz zagwarantować istnienie jednej instancji klasy dla całej aplikacji, pozostaw to [kontenerowi DI |container]. Stwórz w ten sposób singleton aplikacyjny, czyli usługę. Dzięki temu klasa przestanie zajmować się zapewnieniem swojej własnej unikalności (tj. nie będzie miała metody `getInstance()` i zmiennej statycznej) i będzie pełnić tylko swoje funkcje. W ten sposób przestanie naruszać zasadę pojedynczej odpowiedzialności. +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 a testy ---------------------- +Stan globalny kontra testy +-------------------------- -Podczas pisania testów zakładamy, że każdy test jest izolowaną jednostką i że nie wchodzi do niego żaden zewnętrzny stan. I żaden stan nie opuszcza testów. Po zakończeniu testu cały powiązany z nim stan powinien zostać automatycznie usunięty przez garbage collector. Dzięki temu testy są izolowane. Dlatego możemy uruchamiać testy w dowolnej kolejności. +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. -Jeśli jednak obecne są stany globalne/singletony, wszystkie te przyjemne założenia się rozpadają. Stan może wchodzić do testu i wychodzić z niego. Nagle kolejność testów może mieć znaczenie. +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 móc testować singletony, programiści często muszą rozluźnić ich właściwości, na przykład pozwalając na zastąpienie instancji inną. Takie rozwiązania są w najlepszym przypadku hackiem, który tworzy trudny do utrzymania i zrozumienia kod. Każdy test lub metoda `tearDown()`, która wpływa na jakikolwiek stan globalny, musi te zmiany cofnąć. +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 naprawić sytuację? Łatwo. Nie pisz kodu, który wykorzystuje singletony, preferuj przekazywanie zależności. Czyli dependency injection. +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ę tylko do używania singletonów i zmiennych statycznych, ale może dotyczyć również stałych globalnych. +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ść nie wnosi nam żadnej nowej (`M_PI`) lub użytecznej (`PREG_BACKTRACK_LIMIT_ERROR`) informacji, są jednoznacznie w porządku. Natomiast stałe, które służą jako sposób na *bezprzewodowe* przekazanie informacji do wnętrza kodu, są niczym innym jak ukrytą zależnością. Jak na przykład `LOG_FILE` w poniższym przykładzie. Użycie stałej `FILE_APPEND` jest całkowicie poprawne. +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 = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -W tym przypadku powinniśmy zadeklarować parametr w konstruktorze klasy `Foo`, aby stał się częścią API: +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 @@ -253,29 +253,29 @@ class Foo } ``` -Teraz możemy przekazać informację o ścieżce do pliku logów i łatwo ją zmieniać w zależności od potrzeb, co ułatwia testowanie i konserwację kodu. +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 samo używanie metod statycznych i funkcji globalnych nie jest problematyczne. Wyjaśnialiśmy, na czym polega nieodpowiedniość użycia `DB::insert()` i podobnych metod, ale zawsze chodziło tylko o kwestię stanu globalnego, który jest przechowywany w jakiejś zmiennej statycznej. Metoda `DB::insert()` wymaga istnienia zmiennej statycznej, ponieważ w niej jest przechowywane połączenie z bazą danych. Bez tej zmiennej implementacja metody byłaby niemożliwa. +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 `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` i wielu innych, jest w całkowitej zgodzie z dependency injection. Funkcje te zawsze zwracają te same wyniki dla tych samych parametrów wejściowych i są zatem przewidywalne. Nie używają żadnego stanu globalnego. +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. -Istnieją jednak również funkcje w PHP, które nie są deterministyczne. Należy do nich na przykład funkcja `htmlspecialchars()`. Jej trzeci parametr `$encoding`, jeśli nie jest podany, domyślnie przyjmuje wartość opcji konfiguracyjnej `ini_get('default_charset')`. Dlatego zaleca się zawsze podawać ten parametr, aby zapobiec ewentualnemu nieprzewidywalnemu zachowaniu funkcji. Nette konsekwentnie to robi. +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, takie jak `strtolower()`, `strtoupper()` i podobne, w niedawnej przeszłości zachowywały się niedeterministycznie i były zależne od ustawienia `setlocale()`. Powodowało to wiele komplikacji, najczęściej przy pracy z językiem tureckim. Ten bowiem rozróżnia małą i dużą literę `I` z kropką i bez kropki. Tak więc `strtolower('I')` zwracało znak `ı`, a `strtoupper('i')` znak `İ`, co prowadziło do tego, że aplikacje zaczęły powodować szereg zagadkowych błędów. Ten problem został jednak usunięty w PHP w wersji 8.2 i funkcje nie są już zależne od locale. +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. -Jest to piękny przykład, jak stan globalny napsuł krwi tysiącom programistów na całym świecie. Rozwiązaniem było zastąpienie go przez dependency injection. +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żyć stanu globalnego? ----------------------------------- +Kiedy można używać stanu globalnego? +------------------------------------ -Istnieją pewne specyficzne sytuacje, w których można wykorzystać stan globalny. Na przykład podczas debugowania kodu, gdy potrzebujesz wypisać wartość zmiennej lub zmierzyć czas trwania określonej części programu. W takich przypadkach, które dotyczą tymczasowych działań, które zostaną później usunięte z kodu, uzasadnione jest wykorzystanie globalnie dostępnego dumpera lub stopera. Te narzędzia bowiem nie są częścią projektu kodu. +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 do pracy z wyrażeniami regularnymi `preg_*`, które wewnętrznie przechowują skompilowane wyrażenia regularne w statycznej pamięci podręcznej w pamięci. Kiedy więc wywołujesz to samo wyrażenie regularne wielokrotnie w różnych miejscach kodu, kompiluje się ono tylko raz. Pamięć podręczna oszczędza wydajność, a jednocześnie jest dla użytkownika całkowicie niewidoczna, dlatego takie wykorzystanie można uznać za uzasadnione. +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 @@ -283,12 +283,12 @@ Podsumowanie Omówiliśmy, dlaczego warto: -1) Usunąć wszystkie zmienne statyczne z kodu -2) Deklarować zależności -3) I używać dependency injection +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 -Kiedy zastanawiasz się nad projektem kodu, pamiętaj, że każde `static $foo` stanowi problem. Aby Twój kod był środowiskiem szanującym DI, konieczne jest całkowite wyeliminowanie stanu globalnego i zastąpienie go za pomocą dependency injection. +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 być może odkryjesz, że trzeba podzielić klasę, ponieważ ma więcej niż jedną odpowiedzialność. Nie bój się tego; dąż do zasady pojedynczej odpowiedzialnoś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škovi 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/], są podstawą tego rozdziału.* +*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 index 6e0570520d..37b2e8f95a 100644 --- a/dependency-injection/pl/introduction.texy +++ b/dependency-injection/pl/introduction.texy @@ -1,58 +1,58 @@ -Co to jest Wstrzykiwanie Zależności? -************************************ +Czym jest wstrzykiwanie zależności? +*********************************** .[perex] -Ten rozdział wprowadzi Cię w podstawowe praktyki programistyczne, których powinieneś przestrzegać podczas pisania wszystkich aplikacji. Są to podstawy niezbędne do pisania czystego, zrozumiałego i łatwego w utrzymaniu kodu. +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 zasady i będziesz ich przestrzegać, Nette będzie Cię wspierać na każdym kroku. Zajmie się za Ciebie rutynowymi zadaniami i zapewni maksymalną wygodę, abyś mógł skupić się na samej logice. +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 tutaj przedstawimy, są przy tym całkiem proste. Nie musisz się niczego obawiać. +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, prawdopodobnie wyglądałby jakoś tak: +Nie wiemy, w jakim języku go napisałeś, ale gdyby to było PHP, wyglądał zapewne mniej więcej tak: ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // wypisze 24 +echo addition(23, 1); // wypisze 24 ``` -Kilka trywialnych linii kodu, a jednak kryje się w nich tyle kluczowych koncepcji. Że istnieją zmienne. Że kod dzieli się na mniejsze jednostki, jakimi są na przykład funkcje. Że przekazujemy im argumenty wejściowe, a one zwracają wyniki. Brakuje tam już tylko warunków i pętli. +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 funkcji przekazujemy dane wejściowe, a ona zwraca wynik, jest doskonale zrozumiałym konceptem, stosowanym również w innych dziedzinach, jak na przykład w matematyce. +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ę, którą tworzy jej nazwa, przegląd parametrów i ich typów, a na końcu typ zwracanej wartości. Jako użytkowników interesuje nas sygnatura, o wewnętrznej implementacji zazwyczaj nie potrzebujemy nic wiedzieć. +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. -Teraz wyobraź sobie, że sygnatura funkcji wyglądałaby tak: +Wyobraź sobie teraz, że sygnatura funkcji wyglądałaby tak: ```php -function soucet(float $x): float +function addition(float $x): float ``` -Suma z jednym parametrem? To dziwne… A co na przykład tak? +Dodawanie z jednym parametrem? To dziwne… A co powiesz na to? ```php -function soucet(): float +function addition(): float ``` -To już jest naprawdę bardzo dziwne, prawda? Jak się takiej funkcji używa? +To już naprawdę dziwne, prawda? Jak używa się takiej funkcji? ```php -echo soucet(); // co to może wypisać? +echo addition(); // co wypisze? ``` -Patrząc na taki kod, bylibyśmy zdezorientowani. Nie tylko początkujący by go nie zrozumiał, takiego kodu nie rozumie nawet doświadczony programista. +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 właściwie taka funkcja wyglądałaby w środku? Skąd wzięłaby składniki sumy? Prawdopodobnie *w jakiś sposób* załatwiłaby je sobie sama, na przykład tak: +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 soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -W ciele funkcji odkryliśmy ukryte powiązania z innymi globalnymi funkcjami lub metodami statycznymi. Aby dowiedzieć się, skąd naprawdę biorą się składniki sumy, musimy szukać dalej. +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. -Tędy nie! ---------- +Nie tędy droga! +--------------- -Projekt, który właśnie przedstawiliśmy, jest esencją wielu negatywnych cech: +Konstrukcja, którą właśnie pokazaliśmy, jest źródłem wielu negatywnych cech: -- sygnatura funkcji udawała, że nie potrzebuje składników sumy, co nas myliło -- w ogóle nie wiemy, jak zmusić funkcję do zsumowania dwóch innych liczb -- musieliśmy zajrzeć do kodu, aby dowiedzieć się, skąd bierze składniki sumy -- odkryliśmy ukryte powiązania -- do pełnego zrozumienia trzeba zbadać również te powiązania +- 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 -A czy w ogóle zadaniem funkcji sumującej jest pozyskiwanie danych wejściowych? Oczywiście, że nie. Jej odpowiedzialnością jest tylko samo sumowanie. +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. -Z takim kodem nie chcemy się spotykać i zdecydowanie nie chcemy go pisać. Naprawa jest przy tym prosta: wrócić do podstaw i po prostu użyć parametrów: +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 soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -Zasada nr 1: niech ci to przekażą ---------------------------------- +Zasada nr 1: pozwól, aby Ci to przekazano +----------------------------------------- -Najważniejsza zasada brzmi: **wszystkie dane, których funkcje lub klasy potrzebują, muszą być im przekazane**. +Najważniejsza zasada brzmi: **wszystkie dane, których potrzebują funkcje albo klasy, muszą zostać im dostarczone**. -Zamiast wymyślać ukryte sposoby, za pomocą których mogłyby jakoś same do nich dotrzeć, po prostu przekaż parametry. Zaoszczędzisz czas potrzebny na wymyślanie ukrytych ścieżek, które zdecydowanie nie ulepszą twojego kodu. +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 będziesz przestrzegać tej zasady zawsze i wszędzie, jesteś na drodze do kodu bez ukrytych powiązań. Do kodu, który jest zrozumiały nie tylko dla autora, ale i dla każdego, kto będzie go po nim czytał. Gdzie wszystko jest zrozumiałe z sygnatur funkcji i klas i nie trzeba szukać ukrytych tajemnic w implementacji. +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. -Tej technice fachowo mówi się **wstrzykiwanie zależności** (dependency injection). A tym danym mówi się **zależności.** Przy czym jest to zwykłe przekazywanie parametrów, nic więcej. +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] -Proszę nie mylić wstrzykiwania zależności, które jest wzorcem projektowym, z „kontenerem wstrzykiwania zależności” (dependency injection container), który jest z kolei narzędziem, czyli czymś diametralnie innym. Kontenerami DI zajmiemy się później. +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 to się ma do klas? Klasa jest bardziej złożoną całością niż prosta funkcja, niemniej jednak zasada nr 1 obowiązuje bez wyjątku również tutaj. Istnieje tylko [więcej możliwości przekazania argumentów|passing-dependencies]. Na przykład całkiem podobnie jak w przypadku funkcji: +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 Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` -Lub za pomocą innych metod, czy bezpośrednio konstruktora: +Albo za pomocą innych metod, albo bezpośrednio konstruktora: ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,26 +132,25 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` -Oba przykłady są całkowicie zgodne z wstrzykiwaniem zależności. +Oba przykłady są w pełni zgodne ze wstrzykiwaniem zależności. -Prawdziwe przykłady -------------------- +Przykłady z życia +----------------- -W prawdziwym świecie nie będziesz pisać klas do sumowania liczb. Przejdźmy do przykładów z praktyki. +W prawdziwym świecie nie będziesz pisać klas dodających liczby. Przejdźmy do praktycznych przykładów. -Mamy klasę `Article` reprezentującą artykuł na blogu: +Weźmy klasę `Article` reprezentującą artykuł na blogu: ```php class Article @@ -162,7 +161,7 @@ class Article public function save(): void { - // zapiszemy artykuł do bazy danych + // zapisujemy artykuł do bazy danych } } ``` @@ -171,14 +170,14 @@ a użycie będzie następujące: ```php $article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; +$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 w bazie danych. Zaimplementowanie jej za pomocą [Nette Database |database:] byłoby dziecinnie proste, gdyby nie jeden haczyk: skąd `Article` ma wziąć połączenie z bazą danych, tj. obiekt klasy `Nette\Database\Connection`? +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`? -Wydaje się, że mamy wiele możliwości. Może je wziąć skądś ze zmiennej statycznej. Lub dziedziczyć po klasie, która zapewni połączenie z bazą danych. Lub wykorzystać tzw. [singleton |global-state#Singleton]. Lub tzw. fasady (facades), które są używane w Laravelu: +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; @@ -201,15 +200,15 @@ class Article Świetnie, problem rozwiązany. -Czy na pewno? +Czy aby na pewno? -Przypomnijmy [#zasadę nr 1: niech ci to przekażą |#Zasada nr 1: niech ci to przekażą]: wszystkie zależności, których klasa potrzebuje, muszą być jej przekazane. Ponieważ jeśli naruszymy tę zasadę, wkroczyliśmy na drogę do brudnego kodu pełnego ukrytych powiązań, niezrozumiałości, a wynikiem będzie aplikacja, której utrzymanie i rozwój będą bolesne. +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 w bazie danych? Do której, produkcyjnej czy testowej? I jak to można zmienić? +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ć, jak zaimplementowana jest metoda `save()`, i znajduje użycie metody `DB::insert()`. Musi więc szukać dalej, jak ta metoda pozyskuje połączenie z bazą danych. A ukryte powiązania mogą tworzyć całkiem długi łańcuch. +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 występują ukryte powiązania, fasady Laravela czy zmienne statyczne. W czystym i dobrze zaprojektowanym kodzie przekazuje się argumenty: +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 @@ -224,7 +223,7 @@ class Article } ``` -Jeszcze bardziej praktyczne, jak zobaczymy dalej, będzie to przez konstruktor: +Jeszcze praktyczniejsze, jak zobaczymy później, jest użycie konstruktora: ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -Jeśli jesteś doświadczonym programistą, być może myślisz, że `Article` w ogóle nie powinien mieć metody `save()`, powinien reprezentować czysto komponent danych, a o zapisywaniu powinien dbać oddzielny repozytorium. To ma sens. Ale tym wyszlibyśmy daleko poza zakres tematu, którym jest wstrzykiwanie zależności, i starania o podawanie prostych przykładów. +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 będziesz pisać klasę wymagającą do swojego działania np. bazy danych, nie wymyślaj, skąd ją zdobyć, ale pozwól sobie ją przekazać. Na przykład jako parametr konstruktora lub innej metody. Przyznaj się do zależności. Przyznaj się do nich w API swojej klasy. Zyskasz zrozumiały i przewidywalny kod. +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 na przykład z tą klasą, która loguje komunikaty błędów: +A co z tą klasą, która loguje komunikaty o błędach: ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,11 +261,11 @@ class Logger } ``` -Co myślisz, czy przestrzegaliśmy [#zasady nr 1: niech ci to przekażą |#Zasada nr 1: niech ci to przekażą]? +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 przestrzegaliśmy. +Nie trzymaliśmy. -Kluczową informację, czyli katalog z plikiem logu, klasa *pozyskuje sama* ze stałej. +Kluczową informację, czyli katalog zawierający plik logu, *klasa pozyskuje sama* ze stałej. Spójrz na przykład użycia: @@ -276,9 +275,9 @@ $logger->log('Temperatura wynosi 23 °C'); $logger->log('Temperatura wynosi 10 °C'); ``` -Bez znajomości implementacji, czy potrafiłbyś odpowiedzieć na pytanie, gdzie zapisywane są komunikaty? Czy przyszłoby ci do głowy, że do działania potrzebna jest stała `LOG_DIR`? I czy potrafiłbyś utworzyć drugą instancję, która będzie zapisywać gdzie indziej? Na pewno nie. +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. -Poprawmy klasę: +Naprawmy tę klasę: ```php class Logger @@ -295,24 +294,24 @@ class Logger } ``` -Klasa jest teraz znacznie bardziej zrozumiała, konfigurowalna, a zatem bardziej użyteczna. +Klasa jest teraz o wiele bardziej zrozumiała, konfigurowalna, a przez to użyteczniejsza. ```php -$logger = new Logger('/sciezka/do/logu.txt'); +$logger = new Logger('/path/to/log.txt'); $logger->log('Temperatura wynosi 15 °C'); ``` -Ale to mnie nie obchodzi! +Ale mnie to nie obchodzi! ------------------------- -*„Kiedy tworzę obiekt Article i wywołuję save(), nie chcę zajmować się bazą danych, po prostu chcę, żeby zapisał się do tej, którą mam ustawioną w konfiguracji.”* +*"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ą."* -*„Kiedy używam Loggera, po prostu chcę, żeby komunikat został zapisany, i nie chcę zajmować się tym, gdzie. Niech użyje globalnych ustawień.”* +*"Gdy używam Loggera, chcę tylko, aby komunikat został zapisany, i nie chcę zajmować się gdzie. Niech użyje ustawień globalnych."* -To są słuszne uwagi. +To słuszne uwagi. -Jako przykład pokażemy klasę rozsyłającą newslettery, która zaloguje, jak poszło: +Jako przykład pokażmy klasę, która rozsyła newslettery i loguje wynik: ```php class NewsletterDistributor @@ -322,27 +321,27 @@ class NewsletterDistributor $logger = new Logger(/* ... */); try { $this->sendEmails(); - $logger->log('E-maile zostały rozesłane'); + $logger->log('E-maile zostały wysłane'); } catch (Exception $e) { - $logger->log('Wystąpił błąd podczas rozsyłania'); + $logger->log('Podczas wysyłania wystąpił błąd'); throw $e; } } } ``` -Ulepszony `Logger`, który już nie używa stałej `LOG_DIR`, wymaga w konstruktorze podania ścieżki do pliku. Jak to rozwiązać? Klasę `NewsletterDistributor` w ogóle nie interesuje, gdzie zapisywane są komunikaty, chce je tylko zapisać. +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 ponownie [##zasada nr 1: niech ci to przekażą]: wszystkie dane, których klasa potrzebuje, przekazujemy jej. +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 to oznacza, że przez konstruktor przekażemy ścieżkę do logu, którą następnie użyjemy przy tworzeniu obiektu `Logger`? +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, // ⛔ TAK NIE! + private string $file, // ⛔ NIE TAK! ) { } @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -Tak nie! Ścieżka bowiem **nie należy** do danych, których potrzebuje klasa `NewsletterDistributor`; te bowiem potrzebuje `Logger`. Czy dostrzegasz różnicę? Klasa `NewsletterDistributor` potrzebuje loggera jako takiego. Więc to jego sobie przekażemy: +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 @@ -365,33 +364,33 @@ class NewsletterDistributor { try { $this->sendEmails(); - $this->logger->log('E-maile zostały rozesłane'); + $this->logger->log('E-maile zostały wysłane'); } catch (Exception $e) { - $this->logger->log('Wystąpił błąd podczas rozsyłania'); + $this->logger->log('Podczas wysyłania wystąpił błąd'); throw $e; } } } ``` -Teraz z sygnatur klasy `NewsletterDistributor` jest jasne, że częścią jej funkcjonalności jest również logowanie. A zadanie wymiany loggera na inny, na przykład w celu testowania, jest całkowicie trywialne. Co więcej, jeśli konstruktor klasy `Logger` się zmieni, nie będzie to miało żadnego wpływu na naszą klasę. +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, co twoje ----------------------------- +Zasada nr 2: bierz to, co Twoje +------------------------------- -Nie daj się zwieść i nie pozwól sobie przekazywać zależności swoich zależności. Pozwól sobie przekazywać tylko swoje zależności. +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 wykorzystujący inne obiekty będzie całkowicie niezależny od zmian ich konstruktorów. Jego API będzie bardziej prawdziwe. A przede wszystkim będzie trywialne wymienić te zależności na inne. +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 -------------------- -W zespole deweloperskim podjęto decyzję o stworzeniu drugiego loggera, który zapisuje do bazy danych. Stworzymy więc klasę `DatabaseLogger`. Mamy więc dwie klasy, `Logger` i `DatabaseLogger`, jedna zapisuje do pliku, druga do bazy danych… czy nie wydaje ci się, że w tej nazwie jest coś dziwnego? Czy nie byłoby lepiej zmienić nazwę `Logger` na `FileLogger`? Na pewno tak. +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ą. -Ale zrobimy to sprytnie. Pod pierwotną nazwą stworzymy interfejs: +Zróbmy to jednak sprytnie. Tworzymy interfejs pod pierwotną nazwą: ```php interface Logger @@ -400,7 +399,7 @@ interface Logger } ``` -… który oba loggery będą implementować: +… który zaimplementują oba loggery: ```php class FileLogger implements Logger @@ -410,17 +409,17 @@ class DatabaseLogger implements Logger // ... ``` -A dzięki temu nie będzie trzeba niczego zmieniać w reszcie kodu, gdzie używany jest logger. Na przykład konstruktor klasy `NewsletterDistributor` nadal będzie zadowolony z tego, że jako parametr wymaga `Logger`. A od nas będzie zależeć, którą instancję mu przekażemy. +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 byłoby możliwe tak ładnie rozwijać kodu. +**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 możemy sobie poradzić z jedną instancją loggera, czy to plikowego, czy bazodanowego, i po prostu przekazujemy go wszędzie tam, gdzie coś jest logowane, zupełnie inaczej jest w przypadku klasy `Article`. Jej instancje bowiem tworzymy w miarę potrzeb, nawet wielokrotnie. Jak poradzić sobie z powiązaniem z bazą danych w jej konstruktorze? +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? -Jako przykład może posłużyć kontroler, który po wysłaniu formularza ma zapisać artykuł do bazy danych: +Przykładem może być kontroler, który po wysłaniu formularza ma zapisać artykuł do bazy danych: ```php class EditController extends Controller @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -Możliwe rozwiązanie nasuwa się samo: przekażemy sobie obiekt bazy danych konstruktorem do `EditController` i użyjemy `$article = new Article($this->db)`. +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, to nie jest właściwe postępowanie. Baza danych nie jest zależnością `EditController`, ale `Article`. Przekazywanie sobie bazy danych jest więc sprzeczne z [#zasadą nr 2: bierz, co twoje]. Kiedy zmieni się konstruktor klasy `Article` (pojawi się nowy parametr), konieczne będzie również zmodyfikowanie kodu we wszystkich miejscach, gdzie tworzone są instancje. Ufff. +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, co proponujesz? +Houston, jaka jest Twoja propozycja? -Zasada nr 3: zostaw to fabryce ------------------------------- +Zasada nr 3: niech zajmie się tym fabryka +----------------------------------------- -Dzięki temu, że zlikwidowaliśmy ukryte powiązania i wszystkie zależności przekazujemy jako argumenty, zyskaliśmy bardziej konfigurowalne i elastyczne klasy. A zatem potrzebujemy jeszcze czegoś innego, co nam te bardziej elastyczne klasy utworzy i skonfiguruje. Będziemy to nazywać fabrykami. +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, zostaw tworzenie ich instancji fabryce. +Zasada brzmi: jeśli klasa ma zależności, deleguj tworzenie jej instancji do fabryki. -Fabryki są sprytniejszym zamiennikiem operatora `new` w świecie wstrzykiwania zależności. +Fabryki są sprytniejszą alternatywą dla operatora `new` w świecie wstrzykiwania zależności. .[note] -Proszę nie mylić z wzorcem projektowym *factory method*, który opisuje specyficzny sposób wykorzystania fabryk i nie ma związku z tym tematem. +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 lub klasa, która produkuje i konfiguruje obiekty. Klasę produkującą `Article` nazwiemy `ArticleFactory` i mogłaby wyglądać na przykład tak: +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 @@ -487,7 +486,7 @@ class EditController extends Controller public function formSubmitted($data) { - // pozwolimy fabryce utworzyć obiekt + // pozwalamy fabryce utworzyć obiekt $article = $this->articleFactory->create(); $article->title = $data->title; $article->content = $data->content; @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -Gdy w tym momencie zmieni się sygnatura konstruktora klasy `Article`, jedyną częścią kodu, która musi na to zareagować, jest sama fabryka `ArticleFactory`. Całego pozostałego kodu, który pracuje z obiektami `Article`, jak na przykład `EditController`, to w żaden sposób nie dotknie. +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. -Być może teraz pukasz się w czoło, czy w ogóle sobie pomogliśmy. Ilość kodu wzrosła i całość zaczyna wyglądać podejrzanie skomplikowanie. +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. -Nie martw się, za chwilę dojdziemy do kontenera Nette DI. A ten ma wiele asów w rękawie, którymi budowanie aplikacji wykorzystujących wstrzykiwanie zależności niezmiernie uprości. Na przykład zamiast klasy `ArticleFactory` wystarczy [napisać tylko interfejs |factory]: +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 @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -Ale wyprzedzamy fakty, jeszcze wytrzymaj :-) +Ale wyprzedzamy fakty, bądź czujny :-) Podsumowanie ------------ -Na początku tego rozdziału obiecywaliśmy, że pokażemy sposób projektowania czystego kodu. Wystarczy klasom +Na początku tego rozdziału obiecaliśmy pokazać sposób projektowania czystego kodu. Wystarczy zadbać o to, aby klasom: -1) [przekazywać zależności, których potrzebują |#Zasada nr 1: niech ci to przekażą] -2) [i odwrotnie, nie przekazywać tego, czego bezpośrednio nie potrzebują |#Zasada nr 2: bierz co twoje] -3) [oraz że obiekty z zależnościami najlepiej tworzyć w fabrykach |#Zasada nr 3: zostaw to fabryce] +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 się tak nie wydawać, ale te trzy zasady mają dalekosiężne 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, uważają ten krok za kluczowy moment w życiu zawodowym. Otworzył się przed nimi świat przejrzystych i łatwych w utrzymaniu aplikacji. +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 konsekwentnie nie używa wstrzykiwania zależności? Co jeśli jest zbudowany na metodach statycznych lub singletonach? Czy przynosi to jakieś problemy? [Przynosi i to bardzo zasadnicze |global-state]. +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 index 4ddf9b1d98..b906f70810 100644 --- a/dependency-injection/pl/nette-container.texy +++ b/dependency-injection/pl/nette-container.texy @@ -1,10 +1,10 @@ -Kontener Nette DI -***************** +Nette DI Container +****************** .[perex] -Nette DI jest jedną z najciekawszych bibliotek Nette. Potrafi generować i automatycznie aktualizować skompilowane kontenery DI, które są ekstremalnie szybkie i niezwykle łatwe w konfiguracji. +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 ma tworzyć kontener DI, definiujemy zazwyczaj za pomocą plików konfiguracyjnych w [formacie NEON|neon:format]. Kontener, który ręcznie utworzyliśmy w [poprzednim rozdziale|container], zapisałby się tak: +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: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -Zapis jest naprawdę zwięzły. +Składnia jest bardzo zwięzła. -Wszystkie zależności zadeklarowane w konstruktorach klas `ArticleFactory` i `UserController` Nette DI samo wykryje i przekaże dzięki tzw. [autowiringu|autowiring], dlatego w pliku konfiguracyjnym nie trzeba niczego podawać. Więc nawet jeśli dojdzie do zmiany parametrów, nie musisz niczego zmieniać w konfiguracji. Kontener Nette automatycznie się przgeneruje. Ty możesz skupić się wyłącznie na rozwoju aplikacji. +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 za pomocą setterów, użyjemy do tego sekcji [setup |services#Setup]. +Jeśli chcemy przekazywać zależności przez settery, użyjemy do tego sekcji [setup |services#Setup]. -Nette DI generuje bezpośrednio kod PHP kontenera. Wynikiem jest więc plik `.php`, który możesz otworzyć i studiować. Dzięki temu dokładnie widzisz, jak działa kontener. Możesz go również debugować w IDE i krokowo śledzić. A co najważniejsze: wygenerowany PHP jest ekstremalnie szybki. +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 również generować kod [fabryk|factory] na podstawie dostarczonego interfejsu. Dlatego zamiast klasy `ArticleFactory` wystarczy nam stworzyć w aplikacji tylko interfejs: +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 @@ -36,19 +36,19 @@ interface ArticleFactory } ``` -Cały przykład znajdziesz [na GitHubie|https://github.com/nette-examples/di-example-doc]. +Pełny przykład znajdziesz [na GitHubie|https://github.com/nette-examples/di-example-doc]. -Samodzielne użycie +Użycie samodzielne ------------------ -Wdrożenie biblioteki Nette DI do aplikacji jest bardzo łatwe. Najpierw zainstalujemy ją Composerem (ponieważ pobieranie zipów jest taaak przestarzałe): +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 tworzy instancję kontenera DI zgodnie z konfiguracją zapisaną w pliku `config.neon`: +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'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -Kontener generuje się tylko raz, jego kod zapisuje się do cache (katalog `__DIR__ . '/temp'`) i przy kolejnych żądaniach jest już tylko stamtąd odczytywany. +Kontener generowany jest tylko raz, jego kod zapisywany jest do cache (katalog `__DIR__ . '/temp'`), a przy kolejnych żądaniach jest już tylko stamtąd wczytywany. -Do tworzenia i pobierania usług służą metody `getService()` lub `getByType()`. W ten sposób utworzymy obiekt `UserController`: +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 -$controller = $container->getByType(UserController::class); +$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 rozwoju przydatne jest aktywowanie trybu auto-refresh, w którym kontener automatycznie się przgeneruje, jeśli dojdzie do zmiany jakiejkolwiek klasy lub pliku konfiguracyjnego. Wystarczy w konstruktorze `ContainerLoader` podać jako drugi argument `true`. +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); ``` -Użycie z frameworkiem Nette ---------------------------- +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 jest ograniczone do aplikacji pisanych w Nette Framework, możesz go za pomocą zaledwie 3 linii kodu wdrożyć gdziekolwiek. Jeśli jednak rozwijasz aplikacje w Nette Framework, konfigurację i tworzenie kontenera ma na starcie [Bootstrap |application:bootstrapping#Konfiguracja kontenera DI]. +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 index 7e60c0010c..6f906dee13 100644 --- a/dependency-injection/pl/passing-dependencies.texy +++ b/dependency-injection/pl/passing-dependencies.texy @@ -3,22 +3,22 @@ Przekazywanie zależności <div class=perex> -Argumenty, lub w terminologii DI „zależności”, można przekazywać do klas na następujące główne sposoby: +Argumenty, czyli w terminologii DI "zależności", można przekazywać klasom na następujące główne sposoby: -* przekazywanie przez konstruktor -* przekazywanie przez metodę (tzw. setter) -* ustawienie właściwości (zmiennej członkowskiej) -* metodą, adnotacją lub atrybutem *inject* +* przez konstruktor +* przez metodę (tak zwany setter injection) +* przez właściwość +* za pomocą metody `inject*()` albo atrybutu `#[Inject]` </div> -Teraz pokażemy poszczególne warianty na konkretnych przykładach. +Pokażmy każdy wariant na konkretnych przykładach. Przekazywanie przez konstruktor =============================== -Zależności są przekazywane w momencie tworzenia obiektu jako argumenty konstruktora: +Zależności podawane są jako argumenty konstruktora w chwili tworzenia instancji obiektu: ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -Ta forma jest odpowiednia dla obowiązkowych zależności, których klasa bezwzględnie potrzebuje do swojego działania, ponieważ bez nich nie da się utworzyć instancji. +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ótszej formy zapisu ([constructor property promotion |https://blog.nette.org/pl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), która jest funkcjonalnie równoważna: +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 @@ -49,7 +49,7 @@ class MyClass } ``` -Od PHP 8.1 można właściwość oznaczyć flagą `readonly`, która deklaruje, że zawartość właściwości już się nie zmieni: +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 @@ -62,13 +62,13 @@ class MyClass } ``` -Kontener DI przekaże konstruktorowi zależności automatycznie za pomocą [autowiringu |autowiring]. Argumenty, których w ten sposób przekazać nie można (np. stringi, liczby, booleany) [zapiszemy w konfiguracji |services#Argumenty]. +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]. -Constructor hell ----------------- +Piekło konstruktorów +-------------------- -Termin *constructor hell* (piekło konstruktorów) oznacza sytuację, gdy potomek dziedziczy po klasie rodzicielskiej, której konstruktor wymaga zależności, a jednocześnie potomek wymaga zależności. Przy tym musi przejąć i przekazać również te rodzicielskie: +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 @@ -85,7 +85,7 @@ final class MyClass extends BaseClass { private Database $db; - // ⛔ CONSTRUCTOR HELL + // ⛔ PIEKŁO KONSTRUKTORÓW public function __construct(Cache $cache, Database $db) { parent::__construct($cache); @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -Problem pojawia się w momencie, gdy będziemy chcieli zmienić konstruktor klasy `BaseClass`, na przykład gdy pojawi się nowa zależność. Wtedy konieczne jest również zmodyfikowanie wszystkich konstruktorów potomków. Co z takiej modyfikacji czyni piekło. +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 zapobiegać? Rozwiązaniem jest **preferowanie [kompozycji nad dziedziczeniem |faq#Dlaczego preferuje się kompozycję nad dziedziczeniem]**. +Jak temu zapobiec? Rozwiązaniem jest **preferowanie [kompozycji nad dziedziczeniem |faq#Dlaczego kompozycja jest preferowana nad dziedziczeniem?]**. -Czyli zaprojektujemy kod inaczej. Będziemy unikać [abstrakcyjnych |nette:introduction-to-object-oriented-programming#Klasy abstrakcyjne] klas `Base*`. Zamiast tego, aby `MyClass` uzyskiwała pewną funkcjonalność przez dziedziczenie po `BaseClass`, tę funkcjonalność przekażemy jej jako zależność: +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 @@ -128,7 +128,7 @@ final class MyClass Przekazywanie przez setter ========================== -Zależności są przekazywane przez wywołanie metody, która zapisuje je do prywatnej właściwości. Zwykłą konwencją nazewnictwa tych metod jest forma `set*()`, dlatego nazywa się je setterami, ale mogą oczywiście nazywać się jakkolwiek inaczej. +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 @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -Ten sposób jest odpowiedni dla nieobowiązkowych zależności, które nie są niezbędne do działania klasy, ponieważ nie ma gwarancji, że obiekt faktycznie otrzyma zależność (tj. że użytkownik wywoła metodę). +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 ten sposób pozwala na wielokrotne wywoływanie settera i tym samym zmianę zależności. Jeśli nie jest to pożądane, dodamy do metody kontrolę, lub od PHP 8.1 oznaczymy właściwość `$cache` flagą `readonly`. +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 @@ -164,7 +164,7 @@ class MyClass } ``` -Wywołanie settera definiujemy w konfiguracji kontenera DI w [kluczu setup |services#Setup]. Również tutaj wykorzystuje się automatyczne przekazywanie zależności za pomocą autowiringu: +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: @@ -174,10 +174,10 @@ services: ``` -Ustawienie właściwości -====================== +Przekazywanie przez właściwość +============================== -Zależności są przekazywane przez zapisanie bezpośrednio do publicznej właściwości (zmiennej członkowskiej): +Zależności podawane są przez bezpośredni zapis do właściwości składowej: ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -Ten sposób uważa się za niewłaściwy, ponieważ właściwość musi być zadeklarowana jako `public`. A zatem nie mamy kontroli nad tym, że przekazana zależność będzie faktycznie danego typu (obowiązywało przed PHP 7.4) i tracimy możliwość reagowania na nowo przypisaną zależność własnym kodem, na przykład zapobiegania późniejszej zmianie. Jednocześnie właściwość staje się częścią publicznego interfejsu klasy, co może nie być pożądane. +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. -Ustawienie właściwości definiujemy w konfiguracji kontenera DI w [sekcji setup |services#Setup]: +Przypisanie do właściwości definiuje się w konfiguracji kontenera DI w [sekcji setup |services#Setup]: ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -Podczas gdy poprzednie trzy sposoby obowiązują ogólnie we wszystkich językach zorientowanych obiektowo, wstrzykiwanie metodą, adnotacją lub atrybutem *inject* jest specyficzne wyłącznie dla prezenterów w Nette. Omawiają je [osobny rozdział |best-practices:inject-method-attribute]. +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 jest odpowiedni dla obowiązkowych zależności, których klasa bezwzględnie potrzebuje do swojego działania -- setter jest natomiast odpowiedni dla nieobowiązkowych zależności, lub zależności, które można mieć możliwość dalej zmieniać -- publiczne właściwości nie są odpowiednie +- 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 index f1ecc1112c..660ce57a23 100644 --- a/dependency-injection/pl/services.texy +++ b/dependency-injection/pl/services.texy @@ -2,16 +2,16 @@ Definiowanie usług ****************** .[perex] -Konfiguracja jest miejscem, w którym uczymy kontener DI, jak ma budować poszczególne usługi i jak je łączyć z innymi zależnościami. Nette dostarcza bardzo przejrzysty i elegancki sposób, jak tego dokonać. +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 formatu NEON jest miejscem, gdzie definiujemy własne usługi i ich konfiguracje. Spójrzmy na prosty przykład definicji usługi o nazwie `database`, która reprezentuje instancję klasy `PDO`: +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:') ``` -Podana konfiguracja zaowocuje następującą metodą fabrykującą w [kontenerze DI|container]: +Powyższa konfiguracja daje w [kontenerze DI|container] następującą metodę fabryczną: ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -Nazwy usług pozwalają nam odwoływać się do nich w innych częściach pliku konfiguracyjnego, w formacie `@nazwaUslugi`. Jeśli nie ma potrzeby nazywania usługi, możemy po prostu użyć tylko myślnika: +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 uzyskać usługę z kontenera DI, możemy wykorzystać metodę `getService()` z nazwą usługi jako parametrem, lub metodę `getByType()` z typem usługi: +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'); @@ -38,14 +38,14 @@ $database = $container->getByType(PDO::class); Tworzenie usługi ================ -Zazwyczaj tworzymy usługę po prostu tworząc instancję określonej klasy. Na przykład: +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 rozszerzyć konfigurację o dodatkowe klucze, można definicję rozpisać na więcej linii: +Jeśli potrzebujemy rozbudować konfigurację o kolejne klucze, definicję można rozbić na wiele wierszy: ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -Klucz `create` ma alias `factory`, obie warianty są w praktyce powszechne. Niemniej jednak zalecamy używanie `create`. +Klucz `create` ma alias `factory`; oba warianty są powszechnie używane. Zalecamy jednak `create`. -Argumenty konstruktora lub metody tworzącej mogą być alternatywnie zapisane w kluczu `arguments`: +Argumenty konstruktora albo metody fabrycznej można alternatywnie podać kluczem `arguments`: ```neon services: @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -Usługi nie muszą być tworzone tylko przez proste utworzenie instancji klasy, mogą być również wynikiem wywołania metod statycznych lub metod innych usług: +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: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -Zauważ, że dla uproszczenia zamiast `->` używa się `::`, zobacz [#wyrażenia]. Wygenerują się te metody fabrykujące: +Zwróć uwagę, że dla uproszczenia zamiast `->` używa się `::`, zobacz [#Język wyrażeń]. Wygenerowane zostaną takie metody fabryczne: ```php public function createServiceDatabase(): PDO @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -Kontener DI potrzebuje znać typ utworzonej usługi. Jeśli tworzymy usługę za pomocą metody, która nie ma określonego typu zwracanego, musimy ten typ jawnie podać w konfiguracji: +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: @@ -100,14 +100,14 @@ services: Argumenty ========= -Do konstruktora i metod przekazujemy argumenty w sposób bardzo podobny jak w samym PHP: +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 argumenty rozpisać na osobne linie. W takim przypadku używanie przecinków jest opcjonalne: +Dla lepszej czytelności możemy wypisać argumenty w osobnych wierszach. W takim przypadku używanie przecinków staje się opcjonalne: ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -Argumenty możesz również nazwać i nie musisz się wtedy martwić o ich kolejność: +Argumenty możesz też nazwać, dzięki czemu nie musisz przejmować się ich kolejnością: ```neon services: @@ -129,20 +129,20 @@ services: ) ``` -Jeśli chcesz niektóre argumenty pominąć i użyć ich wartości domyślnej lub podstawić usługę za pomocą [autowiringu|autowiring], użyj podkreślenia: +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%) ``` -Jako argumenty można przekazywać usługi, używać parametrów i wiele więcej, zobacz [#wyrażenia]. +Argumenty mogą zawierać usługi, parametry i wiele więcej, zobacz [#Język wyrażeń]. Setup ===== -W sekcji `setup` definiujemy metody, które mają być wywoływane podczas tworzenia usługi. +W sekcji `setup` definiujemy metody, które mają zostać wywołane przy tworzeniu usługi. ```neon services: @@ -152,7 +152,7 @@ services: - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ``` -To w PHP wyglądałoby tak: +W PHP wyglądałoby to tak: ```php public function createServiceDatabase(): PDO @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -Oprócz wywoływania metod można również przekazywać wartości do właściwości. Obsługiwane jest również dodanie elementu do tablicy, które należy zapisać w cudzysłowach, aby nie kolidowało ze składnią NEON: +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: @@ -174,7 +174,7 @@ services: - '$onClick[]' = [@bar, clickHandler] ``` -Co w kodzie PHP wyglądałoby następująco: +W kodzie PHP wyglądałoby to następująco: ```php public function createServiceFoo(): Foo @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -W setupie można jednak wywoływać również metody statyczne lub metody innych usług. Jeśli potrzebujesz przekazać jako argument aktualną usługę, podaj ją jako `@self`: +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: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -Zauważ, że dla uproszczenia zamiast `->` używa się `::`, zobacz [#wyrażenia]. Wygeneruje się taka metoda fabrykująca: +Zwróć uwagę, że dla uproszczenia zamiast `->` używa się `::`, zobacz [#Język wyrażeń]. Wygenerowana zostanie taka metoda fabryczna: ```php public function createServiceFoo(): Foo @@ -210,10 +210,10 @@ public function createServiceFoo(): Foo ``` -Wyrażenia -========= +Język wyrażeń +============= -Nette DI daje nam niezwykle bogate środki wyrazu, za pomocą których możemy zapisać prawie wszystko. W plikach konfiguracyjnych możemy więc wykorzystywać [parametry |configuration#Parametry]: +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 @@ -226,10 +226,10 @@ Nette DI daje nam niezwykle bogate środki wyrazu, za pomocą których możemy z '%wwwDir%/images' ``` -Dalej tworzyć obiekty, wywoływać metody i funkcje: +Ponadto tworzyć obiekty, wywoływać metody i funkcje: ```neon -# tworzenie obiektu +# utworzenie obiektu DateTime() # wywołanie metody statycznej @@ -239,34 +239,44 @@ Collator::create(%locale%) ::getenv(DB_USER) ``` -Odwoływać się do usług albo ich nazwą, albo za pomocą typu: +Odwoływać się do usług po nazwie albo po typie: ```neon -# usługa według nazwy +# usługa po nazwie @database -# usługa według typu +# usługa po typie @Nette\Database\Connection ``` Używać składni first-class callable: .{data-version:3.2.0} ```neon -# tworzenie callbacku, odpowiednik [@user, logout] +# utworzenie callbacku, odpowiednik [@user, logout] @user::logout(...) ``` Używać stałych: ```neon -# stała klasy +# stała klasowa FilesystemIterator::SKIP_DOTS -# stałą globalną uzyskamy funkcją PHP constant() -::constant(PHP_VERSION) +# pobranie stałej globalnej funkcją PHP constant() +::constant(\PHP_VERSION) ``` -Wywołania metod można łączyć w łańcuchy tak samo jak w PHP. Tylko dla uproszczenia zamiast `->` używa się `::`: +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') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -Te wyrażenia możesz używać wszędzie, przy [tworzeniu usług |#Tworzenie usługi], w [argumentach |#Argumenty], w sekcji [#setup] lub [parametrach |configuration#Parametry]: +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: @@ -293,12 +303,12 @@ services: Funkcje specjalne ----------------- -W plikach konfiguracyjnych możesz używać tych specjalnych funkcji: +W plikach konfiguracyjnych możesz używać następujących funkcji specjalnych: -- `not()` negacja wartości -- `bool()`, `int()`, `float()`, `string()` bezstratne rzutowanie na dany typ -- `typed()` stworzy tablicę wszystkich usług określonego typu -- `tagged()` stworzenie tablicy wszystkich usług z danym tagiem +- `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: @@ -308,18 +318,18 @@ services: ) ``` -W przeciwieństwie do klasycznego rzutowania w PHP, jak np. `(int)`, bezstratne rzutowanie rzuci wyjątek dla wartości nieliczbowych. +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 danego typu (klasa lub interfejs). Pomija usługi, które mają wyłączony autowiring. Można podać również więcej typów oddzielonych przecinkiem. +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żesz przekazywać jako argument również automatycznie za pomocą [autowiringu |autowiring#Tablica usług]. +Tablicę usług określonego typu można też przekazać jako argument automatycznie, za pomocą [autowiringu |autowiring#Kolekcja usług]. -Funkcja `tagged()` tworzy następnie tablicę wszystkich usług z określonym tagiem. Również tutaj możesz specyfikować więcej tagów oddzielonych przecinkiem. +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: @@ -340,10 +350,10 @@ services: ``` -Usługi lazy .{data-version:3.2.4} -================================= +Usługi leniwe .{data-version:3.2.4} +=================================== -Lazy loading (leniwe ładowanie) to technika, która odkłada tworzenie usługi aż do momentu, gdy jest ona faktycznie potrzebna. W globalnej konfiguracji można [włączyć leniwe tworzenie |configuration#Usługi lazy] dla wszystkich usług naraz. Dla poszczególnych usług można następnie to zachowanie nadpisać: +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: @@ -352,16 +362,20 @@ services: lazy: false ``` -Gdy usługa jest zdefiniowana jako lazy, przy jej żądaniu z kontenera DI otrzymujemy specjalny obiekt zastępczy. Wygląda on i zachowuje się tak samo jak rzeczywista usługa, ale rzeczywista inicjalizacja (wywołanie konstruktora i setupu) nastąpi dopiero przy pierwszym wywołaniu jakiejkolwiek jej metody lub właściwości. +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 można stosować tylko dla klas użytkownika, a nie dla wewnętrznych klas PHP. Wymaga PHP 8.4 lub nowszego. +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 dodatkowych informacji do usług. Usłudze możesz dodać jeden lub więcej tagów: +Tagi służą do dodawania usługom dodatkowych informacji. Usłudze możesz przypisać jeden albo więcej tagów: ```neon services: @@ -371,7 +385,7 @@ services: - cached ``` -Tagi mogą również przenosić wartości: +Tagi mogą też przechowywać wartości: ```neon services: @@ -381,26 +395,26 @@ services: logger: monolog.logger.event ``` -Aby uzyskać wszystkie usługi z określonymi tagami, możesz użyć funkcji `tagged()`: +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 możesz uzyskać nazwy wszystkich usług z określonym tagiem za pomocą metody `findByTag()`: +W kontenerze DI nazwy wszystkich usług z określonym tagiem pobierzesz metodą `findByTag()`: ```php $names = $container->findByTag('logger'); -// $names to tablica zawierająca nazwę usługi i wartość tagu +// $names to tablica z nazwami usług jako kluczami i wartościami tagów jako wartościami // np. ['foo' => 'monolog.logger.event', ...] ``` -Tryb Inject +Tryb inject =========== -Za pomocą flagi `inject: true` aktywuje się przekazywanie zależności przez publiczne właściwości z adnotacją [inject |best-practices:inject-method-attribute#Atrybuty Inject] i metody [inject*() |best-practices:inject-method-attribute#Metody 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: @@ -409,13 +423,13 @@ services: inject: true ``` -Domyślnie `inject` jest aktywowany tylko dla prezenterów. +Domyślnie tryb `inject` włączony jest tylko dla presenterów. -Modyfikacja usług -================= +Modyfikowanie usług +=================== -Kontener DI zawiera wiele usług, które zostały dodane za pośrednictwem wbudowanego lub [użytkownika rozszerzenia|extensions]. Możesz modyfikować definicje tych usług bezpośrednio w konfiguracji. Na przykład możesz zmienić klasę usługi `application.application`, która standardowo jest `Nette\Application\Application`, na inną: +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: @@ -424,9 +438,9 @@ services: alteration: true ``` -Flaga `alteration` jest informacyjna i mówi, że tylko modyfikujemy istniejącą usługę. +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 również uzupełnić setup: +Możemy też uzupełnić setup: ```neon services: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -Podczas nadpisywania usługi możemy chcieć usunąć oryginalne argumenty, pozycje setup lub tagi, do czego służy `reset`: +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: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -Jeśli chcesz usunąć usługę dodaną przez rozszerzenie, możesz to zrobić tak: +Jeśli chcesz usunąć usługę dodaną przez rozszerzenie, możesz zrobić to tak: ```neon services: diff --git a/dependency-injection/pl/upgrading.texy b/dependency-injection/pl/upgrading.texy new file mode 100644 index 0000000000..9ae2f60298 --- /dev/null +++ b/dependency-injection/pl/upgrading.texy @@ -0,0 +1,49 @@ +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/pt/@home.texy b/dependency-injection/pt/@home.texy deleted file mode 100644 index 513481f0e3..0000000000 --- a/dependency-injection/pt/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -A Injeção de Dependência é um padrão de projeto que mudará fundamentalmente sua perspectiva sobre código e desenvolvimento. Abrirá o caminho para o mundo de aplicações bem projetadas e sustentáveis. - -- [O que é Injeção de Dependência? |introduction] -- [Estado global e singletons |global-state] -- [Passando dependências |passing-dependencies] -- [O que é um Contêiner DI? |container] -- [Perguntas frequentes|faq] - - -O pacote `nette/di` fornece um contêiner de DI compilado extremamente avançado para PHP. - -- [Contêiner Nette DI |nette-container] -- [Configuração |configuration] -- [Definindo serviços |services] -- [Autowiring |autowiring] -- [Fábricas geradas |factory] -- [Criando extensões para Nette DI|extensions] diff --git a/dependency-injection/pt/@left-menu.texy b/dependency-injection/pt/@left-menu.texy deleted file mode 100644 index 2b9273bd2a..0000000000 --- a/dependency-injection/pt/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Injeção de Dependência -********************** -- [O que é DI? |introduction] -- [Estado global e singletons |global-state] -- [Passando dependências |passing-dependencies] -- [O que é um Contêiner DI? |container] -- [Perguntas frequentes|faq] - - -Nette DI --------- -- [Contêiner Nette DI |nette-container] -- [Configuração |configuration] -- [Definindo serviços |services] -- [Autowiring |autowiring] -- [Fábricas geradas |factory] -- [Criando extensões para Nette DI|extensions] diff --git a/dependency-injection/pt/@meta.texy b/dependency-injection/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/dependency-injection/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/dependency-injection/pt/autowiring.texy b/dependency-injection/pt/autowiring.texy deleted file mode 100644 index bcf85fc932..0000000000 --- a/dependency-injection/pt/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring é um ótimo recurso que pode passar automaticamente os serviços necessários para o construtor e outros métodos, para que não precisemos escrevê-los. Isso economiza muito tempo. - -Graças a isso, podemos omitir a grande maioria dos argumentos ao escrever definições de serviço. Em vez de: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Basta escrever: - -```neon -services: - articles: Model\ArticleRepository -``` - -O Autowiring é orientado por tipos, então para funcionar, a classe `ArticleRepository` deve ser definida aproximadamente assim: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Para poder usar o autowiring, deve haver **exatamente um serviço** para cada tipo no contêiner. Se houver mais, o autowiring não saberá qual passar e lançará uma exceção: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # LANÇARÁ EXCEÇÃO, tanto mainDb quanto tempDb correspondem -``` - -A solução seria contornar o autowiring e especificar explicitamente o nome do serviço (ou seja, `articles: Model\ArticleRepository(@mainDb)`). Mas é mais inteligente [desativar |#Desativação do autowiring] o autowiring para um dos serviços, ou [dar preferência |#Preferência de autowiring] ao primeiro serviço. - - -Desativação do autowiring -------------------------- - -Podemos desativar o autowiring de um serviço usando a opção `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # o serviço tempDb é excluído do autowiring - - articles: Model\ArticleRepository # portanto, passa mainDb para o construtor -``` - -O serviço `articles` não lançará uma exceção dizendo que existem dois serviços do tipo `PDO` correspondentes (ou seja, `mainDb` e `tempDb`) que podem ser passados para o construtor, porque ele vê apenas o serviço `mainDb`. - -.[note] -A configuração do autowiring no Nette funciona de forma diferente do Symfony, onde a opção `autowire: false` diz que o autowiring não deve ser usado para os argumentos do construtor do serviço fornecido. No Nette, o autowiring é sempre usado, seja para argumentos do construtor ou para quaisquer outros métodos. A opção `autowired: false` diz que a instância do serviço fornecido não deve ser passada para lugar nenhum usando autowiring. - - -Preferência de autowiring -------------------------- - -Se tivermos vários serviços do mesmo tipo e especificarmos a opção `autowired` para um deles, esse serviço se torna o preferido: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # torna-se preferido - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -O serviço `articles` não lançará uma exceção dizendo que existem dois serviços do tipo `PDO` correspondentes (ou seja, `mainDb` e `tempDb`), mas usará o serviço preferido, ou seja, `mainDb`. - - -Array de serviços ------------------ - -O Autowiring também pode passar arrays de serviços de um determinado tipo. Como não é possível escrever nativamente o tipo dos itens do array em PHP, é necessário, além do tipo `array`, adicionar um comentário phpDoc com o tipo do item no formato `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -O contêiner DI então passa automaticamente um array de serviços correspondentes ao tipo fornecido. Ele omite serviços que têm o autowiring desativado. - -O tipo no comentário também pode estar no formato `array<int, Class>` ou `list<Class>`. Se você não pode influenciar a forma do comentário phpDoc, pode passar o array de serviços diretamente na configuração usando [`typed()` |services#Funções especiais]. - - -Argumentos escalares --------------------- - -O Autowiring só pode injetar objetos e arrays de objetos. Argumentos escalares (por exemplo, strings, números, booleanos) [são escritos na configuração |services#Argumentos]. Uma alternativa é criar um [objeto de configurações |best-practices:passing-settings-to-presenters], que encapsula o valor escalar (ou múltiplos valores) em um objeto, que pode então ser passado novamente usando autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly pode ser usado a partir do PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Você cria um serviço a partir dele adicionando-o à configuração: - -```neon -services: - - MySettings('any value') -``` - -Todas as classes então o solicitarão usando autowiring. - - -Restringindo o autowiring -------------------------- - -Para serviços individuais, o autowiring pode ser restrito a certas classes ou interfaces. - -Normalmente, o autowiring passa o serviço para cada parâmetro de método cujo tipo o serviço corresponde. Restringir significa que estabelecemos condições que os tipos especificados nos parâmetros do método devem satisfazer para que o serviço seja passado para eles. - -Vamos ilustrar com um exemplo: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Se registrássemos todos eles como serviços, o autowiring falharia: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # LANÇARÁ EXCEÇÃO, os serviços parent e child correspondem - childDep: ChildDependent # autowiring passa o serviço child para o construtor -``` - -O serviço `parentDep` lançará a exceção `Multiple services of type ParentClass found: parent, child`, porque ambos os serviços `parent` e `child` se encaixam em seu construtor, e o autowiring não pode decidir qual escolher. - -Para o serviço `child`, podemos, portanto, restringir seu autowiring ao tipo `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # também pode escrever 'autowired: self' - - parentDep: ParentDependent # autowiring passa o serviço parent para o construtor - childDep: ChildDependent # autowiring passa o serviço child para o construtor -``` - -Agora, o serviço `parent` é passado para o construtor do serviço `parentDep`, porque agora é o único objeto correspondente. O autowiring não passa mais o serviço `child` para lá. Sim, o serviço `child` ainda é do tipo `ParentClass`, mas a condição restritiva dada para o tipo do parâmetro não é mais válida, ou seja, não é verdade que `ParentClass` *é um supertipo de* `ChildClass`. - -Para o serviço `child`, `autowired: ChildClass` também poderia ser escrito como `autowired: self`, já que `self` é um placeholder para a classe do serviço atual. - -Na chave `autowired`, também é possível especificar várias classes ou interfaces como um array: - -```neon -autowired: [BarClass, FooInterface] -``` - -Vamos tentar complementar o exemplo com interfaces: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Se não restringirmos o serviço `child` de forma alguma, ele se encaixará nos construtores de todas as classes `FooDependent`, `BarDependent`, `ParentDependent` e `ChildDependent`, e o autowiring o passará para lá. - -No entanto, se restringirmos seu autowiring a `ChildClass` usando `autowired: ChildClass` (ou `self`), o autowiring o passará apenas para o construtor de `ChildDependent`, porque ele requer um argumento do tipo `ChildClass` e é verdade que `ChildClass` *é do tipo* `ChildClass`. Nenhum outro tipo especificado nos outros parâmetros é um supertipo de `ChildClass`, então o serviço não é passado. - -Se o restringirmos a `ParentClass` usando `autowired: ParentClass`, ele será novamente passado para o construtor de `ChildDependent` (porque o `ChildClass` exigido é um supertipo de `ParentClass`) e, agora também para o construtor de `ParentDependent`, porque o tipo `ParentClass` exigido também é adequado. - -Se o restringirmos a `FooInterface`, ele ainda será autowired para `ParentDependent` (o `ParentClass` exigido é um supertipo de `FooInterface`) e `ChildDependent`, mas adicionalmente também para o construtor de `FooDependent`, mas não para `BarDependent`, porque `BarInterface` não é um supertipo de `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring passa child para o construtor - barDep: BarDependent # LANÇARÁ EXCEÇÃO, nenhum serviço corresponde - parentDep: ParentDependent # autowiring passa child para o construtor - childDep: ChildDependent # autowiring passa child para o construtor -``` diff --git a/dependency-injection/pt/configuration.texy b/dependency-injection/pt/configuration.texy deleted file mode 100644 index e22c943046..0000000000 --- a/dependency-injection/pt/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Configuração do Contêiner DI -**************************** - -.[perex] -Visão geral das opções de configuração para o contêiner Nette DI. - - -Arquivo de Configuração -======================= - -O contêiner Nette DI é facilmente controlado por meio de arquivos de configuração. Eles geralmente são escritos no [formato NEON|neon:format]. Para edição, recomendamos [editores com suporte |best-practices:editors-and-tools#Editor IDE] para este formato. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Decorador .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Contêiner DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensões]: "Instalação de extensões DI adicionais .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Inclusão de arquivos]: "Inclusão de arquivos .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parâmetros]: "Parâmetros .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Registro automático de serviços .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Serviços .[prism-token prism-comment]" -</pre> - -.[note] -Para escrever uma string contendo o caractere `%`, você deve escapá-lo duplicando-o para `%%`. - - -Parâmetros -========== - -Na configuração, você pode definir parâmetros que podem ser usados como parte das definições de serviço. Isso pode tornar a configuração mais clara ou unificar e extrair valores que serão alterados. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Referimo-nos ao parâmetro `dsn` em qualquer lugar na configuração escrevendo `%dsn%`. Os parâmetros também podem ser usados dentro de strings como `'%wwwDir%/images'`. - -Os parâmetros não precisam ser apenas strings ou números, eles também podem conter arrays: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Referimo-nos a uma chave específica como `%mailer.user%`. - -Se você precisar descobrir o valor de qualquer parâmetro em seu código, por exemplo, em uma classe, passe-o para essa classe. Por exemplo, no construtor. Não existe um objeto global representando a configuração que as classes consultariam para obter valores de parâmetros. Isso violaria o princípio da injeção de dependência. - - -Serviços -======== - -Veja [capítulo separado|services]. - - -Decorator -========= - -Como modificar em massa todos os serviços de um determinado tipo? Por exemplo, chamar um determinado método em todos os presenters que herdam de um ancestral comum específico? É para isso que serve o decorator. - -```neon -decorator: - # para todos os serviços que são instâncias desta classe ou interface - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # chame este método - - $absoluteUrls = true # e defina a variável -``` - -O decorator também pode ser usado para definir [tags |services#Tags] ou ativar o modo [inject |services#Modo Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Configurações técnicas do contêiner DI. - -```neon -di: - # exibir DIC na Tracy Bar? - debugger: ... # (bool) padrão é true - - # tipos de parâmetros que nunca devem ser autowired - excluded: ... # (string[]) - - # permitir criação lazy de serviços? - lazy: ... # (bool) padrão é false - - # classe da qual o contêiner DI herda - parentClass: ... # (string) padrão é Nette\DI\Container -``` - - -Serviços Lazy .{data-version:3.2.4} ------------------------------------ - -A configuração `lazy: true` ativa a criação lazy (adiada) de serviços. Isso significa que os serviços não são realmente criados no momento em que os solicitamos do contêiner DI, mas apenas no momento de seu primeiro uso. Isso pode acelerar o início da aplicação e reduzir o consumo de memória, pois apenas os serviços que são realmente necessários na requisição atual são criados. - -Para um serviço específico, a criação lazy pode ser [alterada |services#Serviços Lazy]. - -.[note] -Objetos lazy só podem ser usados para classes de usuário, não para classes internas do PHP. Requer PHP 8.4 ou posterior. - - -Exportação de metadados ------------------------ - -A classe do contêiner DI também contém muitos metadados. Você pode reduzi-la reduzindo a exportação de metadados. - -```neon -di: - export: - # exportar parâmetros? - parameters: false # (bool) padrão é true - - # exportar tags e quais? - tags: # (string[]|bool) padrão são todas - - event.subscriber - - # exportar dados para autowiring e quais? - types: # (string[]|bool) padrão são todas - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Se você não usa o array `$container->getParameters()`, pode desativar a exportação de parâmetros. Além disso, você pode exportar apenas as tags pelas quais obtém serviços usando o método `$container->findByTag(...)`. Se você não chamar o método, pode desativar completamente a exportação de tags usando `false`. - -Você pode reduzir significativamente os metadados para [autowiring] especificando as classes que você usa como parâmetro do método `$container->getByType()`. E novamente, se você não chamar o método (respectivamente, apenas no [bootstrap|application:bootstrapping] para obter `Nette\Application\Application`), pode desativar completamente a exportação usando `false`. - - -Extensões -========= - -Registro de extensões DI adicionais. Desta forma, adicionamos, por exemplo, a extensão DI `Dibi\Bridges\Nette\DibiExtension22` sob o nome `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Posteriormente, a configuramos na seção `dibi`: - -```neon -dibi: - host: localhost -``` - -Também é possível adicionar uma classe que tem parâmetros como extensão: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Inclusão de arquivos -==================== - -Podemos incluir outros arquivos de configuração na seção `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -O nome `parameters.php` não é um erro de digitação, a configuração também pode ser escrita em um arquivo PHP, que a retorna como um array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Se elementos com as mesmas chaves aparecerem em vários arquivos de configuração, eles serão sobrescritos ou, no caso de [arrays, mesclados |#Mesclagem]. O arquivo incluído posteriormente tem prioridade maior que o anterior. O arquivo no qual a seção `includes` está listada tem prioridade maior que os arquivos incluídos nele. - - -Search -====== - -A adição automática de serviços ao contêiner DI torna o trabalho extremamente agradável. Nette adiciona automaticamente presenters ao contêiner, mas também é fácil adicionar quaisquer outras classes. - -Basta especificar em quais diretórios (e subdiretórios) as classes devem ser procuradas: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -No entanto, geralmente não queremos adicionar absolutamente todas as classes e interfaces, por isso podemos filtrá-las: - -```neon -search: - - in: %appDir%/Forms - - # filtragem por nome de arquivo (string|string[]) - files: - - *Factory.php - - # filtragem por nome de classe (string|string[]) - classes: - - *Factory -``` - -Ou podemos selecionar classes que herdam ou implementam pelo menos uma das classes listadas: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Também é possível definir regras de exclusão, ou seja, máscaras de nome de classe ou ancestrais herdados, que, se corresponderem, o serviço não será adicionado ao contêiner DI: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Tags podem ser definidas para todos os serviços: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Mesclagem -========= - -Se elementos com as mesmas chaves aparecerem em vários arquivos de configuração, eles serão sobrescritos ou, no caso de arrays, mesclados. O arquivo incluído posteriormente tem prioridade maior que o anterior. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>resultado</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Para arrays, a mesclagem pode ser evitada adicionando um ponto de exclamação após o nome da chave: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>resultado</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Configuração de Injeção de Dependência}} diff --git a/dependency-injection/pt/container.texy b/dependency-injection/pt/container.texy deleted file mode 100644 index fff5f871d2..0000000000 --- a/dependency-injection/pt/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -O que é um Contêiner DI? -************************ - -.[perex] -Um contêiner de injeção de dependência (DIC) é uma classe que pode instanciar e configurar objetos. - -Pode surpreendê-lo, mas em muitos casos, você não precisa de um contêiner de injeção de dependência para aproveitar os benefícios da injeção de dependência (DI para abreviar). Afinal, mesmo no [capítulo introdutório|introduction], mostramos DI com exemplos concretos e nenhum contêiner foi necessário. - -No entanto, se você precisar gerenciar um grande número de objetos diferentes com muitas dependências, um contêiner de injeção de dependência será realmente útil. É o caso, por exemplo, de aplicações web construídas sobre um framework. - -No capítulo anterior, apresentamos as classes `Article` e `UserController`. Ambas têm algumas dependências, nomeadamente o banco de dados e a fábrica `ArticleFactory`. E para essas classes, agora criaremos um contêiner. Claro, para um exemplo tão simples, não faz sentido ter um contêiner. Mas vamos criá-lo para mostrar como ele se parece e funciona. - -Aqui está um contêiner simples hardcoded para o exemplo dado: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -O uso seria o seguinte: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Apenas pedimos ao contêiner o objeto e não precisamos mais saber nada sobre como criá-lo ou quais são suas dependências; o contêiner sabe tudo isso. As dependências são injetadas automaticamente pelo contêiner. Essa é a sua força. - -Por enquanto, o contêiner tem todos os dados codificados. Daremos o próximo passo e adicionaremos parâmetros para tornar o contêiner realmente útil: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Leitores atentos podem ter notado um certo problema. Toda vez que obtenho um objeto `UserController`, uma nova instância de `ArticleFactory` e do banco de dados também é criada. Definitivamente não queremos isso. - -Portanto, adicionaremos um método `getService()` que sempre retornará as mesmas instâncias: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') chamará createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Na primeira chamada, por exemplo, `$container->getService('Database')`, ele fará com que `createDatabase()` crie o objeto do banco de dados, que ele armazena no array `$services`, e na próxima chamada, ele o retorna diretamente. - -Também modificaremos o restante do contêiner para usar `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -A propósito, o termo serviço refere-se a qualquer objeto gerenciado pelo contêiner. É por isso que o método se chama `getService()`. - -Feito. Temos um contêiner DI totalmente funcional! E podemos usá-lo: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Como você pode ver, escrever um DIC não é complicado. Vale a pena notar que os próprios objetos não sabem que estão sendo criados por algum contêiner. Assim, é possível criar qualquer objeto em PHP dessa forma sem interferir em seu código-fonte. - -Criar e manter manualmente a classe do contêiner pode se tornar rapidamente um pesadelo. Portanto, no próximo capítulo, falaremos sobre o [Nette DI Container|nette-container], que pode se gerar e atualizar quase sozinho. - - -{{maintitle: O que é um contêiner de injeção de dependência?}} diff --git a/dependency-injection/pt/extensions.texy b/dependency-injection/pt/extensions.texy deleted file mode 100644 index a0fa7b614e..0000000000 --- a/dependency-injection/pt/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Criação de extensões para Nette DI -********************************** - -.[perex] -A geração do contêiner DI, além dos arquivos de configuração, também é influenciada pelas chamadas *extensões*. Nós as ativamos no arquivo de configuração na seção `extensions`. - -Desta forma, adicionamos a extensão representada pela classe `BlogExtension` sob o nome `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Cada extensão do compilador herda de [api:Nette\DI\CompilerExtension] e pode implementar os seguintes métodos, que são chamados sequencialmente durante a construção do contêiner DI: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Este método é chamado primeiro. Ele define o schema para validação dos parâmetros de configuração. - -Configuramos a extensão na seção cujo nome é o mesmo sob o qual a extensão foi adicionada, ou seja, `blog`: - -```neon -# mesmo nome da extensão -blog: - postsPerPage: 10 - allowComments: false -``` - -Criamos um schema descrevendo todas as opções de configuração, incluindo seus tipos, valores permitidos e, opcionalmente, valores padrão: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -A documentação pode ser encontrada na página [Schema |schema:]. Além disso, pode-se especificar quais opções podem ser [dinâmicas |application:bootstrapping#Parâmetros dinâmicos] usando `dynamic()`, např. `Expect::int()->dynamic()`. - -Acessamos a configuração através da variável `$this->config`, que é um objeto `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postsPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Usado para adicionar serviços ao contêiner. Para isso, serve a [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // ou setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -A convenção é prefixar os serviços adicionados pela extensão com seu nome, para que não ocorram conflitos de nomes. O método `prefix()` faz isso, então se a extensão se chama `blog`, o serviço será nomeado `blog.articles`. - -Se precisarmos renomear um serviço, podemos, para manter a compatibilidade retroativa, criar um alias com o nome original. A Nette faz algo semelhante, por exemplo, com o serviço `routing.router`, que também está disponível sob o nome anterior `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Carregamento de serviços de um arquivo --------------------------------------- - -Não precisamos criar serviços apenas usando a API da classe ContainerBuilder, mas também com a notação familiar usada no arquivo de configuração NEON na seção services. O prefixo `@extension` representa a extensão atual. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Carregamos os serviços: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // carregamento do arquivo de configuração para a extensão - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -O método é chamado no momento em que o contêiner contém todos os serviços adicionados pelas extensões individuais nos métodos `loadConfiguration` e também pelos arquivos de configuração do usuário. Nesta fase de construção, podemos, portanto, modificar as definições de serviço ou adicionar ligações entre eles. Para pesquisar serviços no contêiner por tags, pode-se usar o método `findByTag()`, por classe ou interface, o método `findByType()`. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -Nesta fase, a classe do contêiner já está gerada na forma de um objeto [ClassType |php-generator:#Classes], contém todos os métodos que criam serviços e está pronta para ser escrita no cache. O código resultante da classe ainda pode ser modificado neste momento. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -A classe Configurator, após [criar o contêiner |application:bootstrapping#index.php], chama o código de inicialização, que é criado escrevendo no objeto `$this->initialization` usando o [método addBody() |php-generator:#Corpos de métodos e funções]. - -Mostraremos um exemplo de como, por exemplo, iniciar a sessão com o código de inicialização ou iniciar serviços que têm a tag `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // início automático da sessão - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // serviços com a tag run devem ser criados após a instanciação do contêiner - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/pt/factory.texy b/dependency-injection/pt/factory.texy deleted file mode 100644 index 7d64f484c7..0000000000 --- a/dependency-injection/pt/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Fábricas Geradas -**************** - -.[perex] -A Nette DI pode gerar automaticamente código de fábricas com base em interfaces, o que economiza a escrita de código. - -Uma fábrica é uma classe que produz e configura objetos. Portanto, ela também passa suas dependências para eles. Por favor, não confunda com o padrão de projeto *factory method*, que descreve uma maneira específica de usar fábricas e não está relacionado a este tópico. - -Mostramos como é uma fábrica no [capítulo introdutório |introduction#Fábrica]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -A Nette DI pode gerar automaticamente o código das fábricas. Tudo o que você precisa fazer é criar uma interface e a Nette DI gerará a implementação. A interface deve ter exatamente um método chamado `create` e declarar o tipo de retorno: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ou seja, a fábrica `ArticleFactory` tem um método `create` que cria objetos `Article`. A classe `Article` pode se parecer com o seguinte: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Adicionamos a fábrica ao arquivo de configuração: - -```neon -services: - - ArticleFactory -``` - -A Nette DI gerará a implementação correspondente da fábrica. - -No código que usa a fábrica, solicitamos o objeto pela interface e a Nette DI usará a implementação gerada: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // deixamos a fábrica criar o objeto - $article = $this->articleFactory->create(); - } -} -``` - - -Fábrica Parametrizada -===================== - -O método da fábrica `create` pode aceitar parâmetros, que são então passados para o construtor. Vamos adicionar, por exemplo, o ID do autor do artigo à classe `Article`: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Também adicionamos o parâmetro à fábrica: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Graças ao fato de que o parâmetro no construtor e o parâmetro na fábrica têm o mesmo nome, a Nette DI os passa de forma totalmente automática. - - -Definição Avançada -================== - -A definição também pode ser escrita em formato de múltiplas linhas usando a chave `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Ao escrever desta forma mais longa, é possível especificar argumentos adicionais para o construtor na chave `arguments` e configuração adicional usando `setup`, assim como nos serviços comuns. - -Exemplo: se o método `create()` não aceitasse o parâmetro `$authorId`, poderíamos especificar um valor fixo na configuração, que seria passado para o construtor de `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Ou, inversamente, se `create()` aceitasse o parâmetro `$authorId`, mas ele não fizesse parte do construtor e fosse passado pelo método `Article::setAuthorId()`, faríamos referência a ele na seção `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Além das fábricas, a Nette também pode gerar os chamados accessors. São objetos com um método `get()`, que retorna um determinado serviço do contêiner DI. Chamadas repetidas de `get()` retornam sempre a mesma instância. - -Os accessors fornecem carregamento preguiçoso (lazy-loading) para dependências. Considere uma classe que registra erros em um banco de dados especial. Se essa classe recebesse a conexão com o banco de dados como dependência via construtor, a conexão sempre teria que ser criada, embora na prática um erro ocorra apenas excepcionalmente e, portanto, na maioria das vezes a conexão permaneceria inutilizada. Em vez disso, a classe recebe um accessor e somente quando seu `get()` é chamado, o objeto do banco de dados é criado: - -Como criar um accessor? Basta escrever uma interface e a Nette DI gerará a implementação. A interface deve ter exatamente um método chamado `get` e declarar o tipo de retorno: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Adicionamos o accessor ao arquivo de configuração, onde também está a definição do serviço que ele retornará: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Como o accessor retorna um serviço do tipo `PDO` e há apenas um serviço desse tipo na configuração, ele retornará exatamente esse. Se houvesse mais serviços do tipo fornecido, especificaríamos o serviço retornado usando o nome, por exemplo, `- PDOAccessor(@db1)`. - - -Fábrica/Accessor Múltiplo -========================= -Nossas fábricas e accessors até agora só podiam produzir ou retornar um objeto. No entanto, é muito fácil criar também fábricas múltiplas combinadas сom accessors. A interface de tal classe conterá qualquer número de métodos com os nomes `create<name>()` e `get<name>()`, por exemplo: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Então, em vez de passar várias fábricas e accessors gerados, passamos uma fábrica mais complexa que pode fazer mais. - -Alternativamente, em vez de vários métodos, pode-se usar `get()` сom um parâmetro: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Então, vale que `MultiFactory::createArticle()` faz o mesmo que `MultiFactoryAlt::get('article')`. No entanto, a notação alternativa tem a desvantagem de não ficar claro quais valores de `$name` são suportados e, logicamente, também não é possível na interface distinguir diferentes valores de retorno para diferentes `$name`. - - -Definição por lista -------------------- -Desta forma, é possível definir uma fábrica múltipla na configuração: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # define createArticle() - db: PDO(%dsn%, %user%, %password%) # define getDb() - ) -``` - -Ou podemos, na definição da fábrica, referir-nos a serviços existentes usando uma referência: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # define createArticle() - db: @\PDO # define getDb() - ) -``` - - -Definição usando tags ---------------------- - -A segunda opção é usar [tags |services#Tags] para a definição: - -```neon -services: - - App\Core\RouterFactory::createRouter # Assumindo que isso é um serviço ou factory - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer # Assumindo que existe um serviço com este nome - ) -``` diff --git a/dependency-injection/pt/faq.texy b/dependency-injection/pt/faq.texy deleted file mode 100644 index d93dd8c94a..0000000000 --- a/dependency-injection/pt/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Perguntas Frequentes sobre DI (FAQ) -*********************************** - - -DI é outro nome para IoC? -------------------------- - -*Inversion of Control* (IoC) é um princípio focado na maneira como o código é executado - se o seu código executa código de terceiros ou se o seu código é integrado a código de terceiros que o chama posteriormente. IoC é um termo amplo que inclui [eventos |nette:glossary#Eventos], o chamado [Princípio de Hollywood |application:components#Estilo Hollywood] e outros aspectos. Parte deste conceito também são as fábricas, sobre as quais fala a [Regra nº 3: deixe para a fábrica |introduction#Regra nº 3: deixe para a fábrica], e que representam uma inversão para o operador `new`. - -*Dependency Injection* (DI) foca na maneira como um objeto aprende sobre outro objeto, ou seja, sobre suas dependências. É um padrão de projeto que exige a passagem explícita de dependências entre objetos. - -Pode-se dizer, portanto, que DI é uma forma específica de IoC. No entanto, nem todas as formas de IoC são adequadas do ponto de vista da pureza do código. Por exemplo, entre os antipadrões estão técnicas que trabalham com [estado global |global-state] ou o chamado [Service Locator |#O que é Service Locator]. - - -O que é Service Locator? ------------------------- - -É uma alternativa à Injeção de Dependência. Funciona criando um repositório central onde todos os serviços ou dependências disponíveis são registrados. Quando um objeto precisa de uma dependência, ele a solicita ao Service Locator. - -No entanto, em comparação com a Injeção de Dependência, perde em transparência: as dependências não são passadas diretamente aos objetos e não são tão facilmente identificáveis, o que exige examinar o código para revelar e entender todas as ligações. O teste também é mais complicado, pois não podemos simplesmente passar objetos mock para os objetos testados, mas temos que passar pelo Service Locator. Além disso, o Service Locator perturba o design do código, pois objetos individuais precisam saber de sua existência, o que difere da Injeção de Dependência, onde os objetos não têm conhecimento do contêiner DI. - - -Quando é melhor não usar DI? ----------------------------- - -Não são conhecidas dificuldades associadas ao uso do padrão de projeto Injeção de Dependência. Pelo contrário, obter dependências de locais globalmente disponíveis leva a [uma série de complicações |global-state], assim como o uso do Service Locator. Portanto, é aconselhável usar DI sempre. Isso não é uma abordagem dogmática, mas simplesmente não foi encontrada uma alternativa melhor. - -No entanto, existem certas situações em que não passamos objetos e os obtemos do espaço global. Por exemplo, ao depurar código, quando você precisa imprimir o valor de uma variável em um ponto específico do programa, medir a duração de uma determinada parte do programa ou registrar uma mensagem. Nesses casos, quando se trata de tarefas temporárias que serão posteriormente removidas do código, é legítimo usar um dumper, cronômetro ou logger globalmente disponível. Essas ferramentas não pertencem ao design do código. - - -O uso de DI tem desvantagens? ------------------------------ - -O uso da Injeção de Dependência traz alguma desvantagem, como aumento da complexidade na escrita do código ou piora no desempenho? O que perdemos quando começamos a escrever código de acordo com DI? - -DI não tem impacto no desempenho ou nos requisitos de memória da aplicação. O desempenho do Contêiner DI pode desempenhar algum papel, mas no caso do [Nette DI |nette-container], o contêiner é compilado em PHP puro, então sua sobrecarga durante a execução da aplicação é essencialmente zero. - -Ao escrever código, geralmente é necessário criar construtores que aceitam dependências. Antigamente, isso podia ser demorado, mas graças aos IDEs modernos e à [promoção de propriedades do construtor |https://blog.nette.org/pt/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], agora é uma questão de segundos. As fábricas podem ser facilmente geradas usando Nette DI e o plugin para PhpStorm com um clique do mouse. Por outro lado, elimina-se a necessidade de escrever singletons e pontos de acesso estáticos. - -Pode-se afirmar que uma aplicação corretamente projetada usando DI não é nem mais curta nem mais longa em comparação com uma aplicação usando singletons. As partes do código que trabalham com dependências são apenas extraídas das classes individuais e movidas para novos locais, ou seja, para o contêiner DI e fábricas. - - -Como reescrever uma aplicação legada para DI? ---------------------------------------------- - -A transição de uma aplicação legada para Injeção de Dependência pode ser um processo desafiador, especialmente para aplicações grandes e complexas. É importante abordar este processo sistematicamente. - -- Ao fazer a transição para Injeção de Dependência, é importante que todos os membros da equipe entendam os princípios e procedimentos que estão sendo usados. -- Primeiro, realize uma análise da aplicação existente e identifique os componentes chave e suas dependências. Crie um plano de quais partes serão refatoradas e em que ordem. -- Implemente um contêiner DI ou, melhor ainda, use uma biblioteca existente, como Nette DI. -- Refatore gradualmente partes individuais da aplicação para usar Injeção de Dependência. Isso pode incluir a modificação de construtores ou métodos para aceitar dependências como parâmetros. -- Modifique os locais no código onde objetos com dependências são criados para que, em vez disso, as dependências sejam injetadas pelo contêiner. Isso pode incluir o uso de fábricas. - -Lembre-se que a transição para Injeção de Dependência é um investimento na qualidade do código e na sustentabilidade a longo prazo da aplicação. Embora possa ser desafiador fazer essas mudanças, o resultado deve ser um código mais limpo, modular e facilmente testável, pronto para futuras extensões e manutenção. - - -Por que a composição é preferida em relação à herança? ------------------------------------------------------- -É preferível usar [composição |nette:introduction-to-object-oriented-programming#Composição] em vez de [herança |nette:introduction-to-object-oriented-programming#Herança], porque ela serve para reutilizar código sem ter que nos preocupar com as consequências das mudanças. Ela fornece, portanto, um acoplamento mais fraco, onde não precisamos nos preocupar que a mudança em algum código cause a necessidade de mudar outro código dependente. Um exemplo típico é a situação conhecida como [inferno de construtores |passing-dependencies#Constructor hell]. - - -É possível usar o Nette DI Container fora do Nette? ---------------------------------------------------- - -Com certeza. O Nette DI Container faz parte do Nette, mas foi projetado como uma biblioteca independente que pode ser usada independentemente de outras partes do framework. Basta instalá-lo usando o Composer, criar um arquivo de configuração com a definição de seus serviços e, em seguida, usar algumas linhas de código PHP para criar o contêiner DI. E você pode começar imediatamente a aproveitar os benefícios da Injeção de Dependência em seus projetos. - -O uso específico, incluindo códigos, é descrito no capítulo [Nette DI Container |nette-container]. - - -Por que a configuração está em arquivos NEON? ---------------------------------------------- - -NEON é uma linguagem de configuração simples e fácil de ler, desenvolvida no Nette para configurar aplicações, serviços e suas dependências. Em comparação com JSON ou YAML, oferece opções muito mais intuitivas e flexíveis para este propósito. Em NEON, é possível descrever naturalmente ligações que em Symfony & YAMLu não seria possível escrever, ou apenas por meio de uma descrição complexa. - - -A análise de arquivos NEON não torna a aplicação mais lenta? ------------------------------------------------------------- - -Embora os arquivos NEON sejam analisados muito rapidamente, este aspecto não importa. A razão é que a análise dos arquivos ocorre apenas uma vez na primeira execução da aplicação. Depois disso, o código do contêiner DI é gerado, salvo em disco e executado em cada requisição subsequente, sem a necessidade de realizar análises adicionais. - -É assim que funciona em um ambiente de produção. Durante o desenvolvimento, os arquivos NEON são analisados toda vez que seu conteúdo é alterado, para que o desenvolvedor sempre tenha um contêiner DI atualizado. A análise em si é, como mencionado, uma questão de momento. - - -Como acesso os parâmetros do arquivo de configuração a partir da minha classe? ------------------------------------------------------------------------------- - -Lembre-se da [Regra nº 1: peça para receber |introduction#Regra nº 1: peça para ser passado]. Se uma classe requer informações do arquivo de configuração, não precisamos pensar em como obter essas informações, em vez disso, simplesmente as solicitamos - por exemplo, através do construtor da classe. E realizamos a passagem no arquivo de configuração. - -Neste exemplo, `%myParameter%` é um placeholder para o valor do parâmetro `myParameter`, que é passado para o construtor da classe `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Se você deseja passar vários parâmetros ou usar autowiring, é aconselhável [envolver os parâmetros em um objeto |best-practices:passing-settings-to-presenters]. - - -Nette suporta a interface PSR-11: Container? --------------------------------------------- - -O Nette DI Container não suporta PSR-11 diretamente. No entanto, se você precisar de interoperabilidade entre o Nette DI Container e bibliotecas ou frameworks que esperam a Interface de Contêiner PSR-11, você pode criar um [adaptador simples |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] que servirá como uma ponte entre o Nette DI Container e o PSR-11. diff --git a/dependency-injection/pt/global-state.texy b/dependency-injection/pt/global-state.texy deleted file mode 100644 index 65375c7661..0000000000 --- a/dependency-injection/pt/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Estado Global e Singletons -************************** - -.[perex] -Aviso: As seguintes construções são um sinal de código mal projetado: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ou `static::$var` - -Alguma dessas construções ocorre em seu código? Então você tem a oportunidade de melhorá-lo. Você pode pensar que são construções comuns que você vê até mesmo em soluções de exemplo de várias bibliotecas e frameworks. Se for esse o caso, então o design do código deles não é bom. - -Agora, definitivamente não estamos falando de alguma pureza acadêmica. Todas essas construções têm uma coisa em comum: elas usam estado global. E isso tem um impacto destrutivo na qualidade do código. As classes mentem sobre suas dependências. O código se torna imprevisível. Confunde os programadores e reduz sua eficiência. - -Neste capítulo, explicaremos por que isso acontece e como evitar o estado global. - - -Acoplamento Global ------------------- - -Em um mundo ideal, um objeto só deveria ser capaz de se comunicar com objetos que lhe foram [passados diretamente |passing-dependencies]. Se eu criar dois objetos `A` e `B` e nunca passar uma referência entre eles, então nem `A` nem `B` podem acessar o outro objeto ou alterar seu estado. Esta é uma propriedade muito desejável do código. É semelhante a ter uma bateria e uma lâmpada; a lâmpada não acenderá até que você a conecte à bateria com um fio. - -Mas isso não se aplica a variáveis globais (estáticas) ou singletons. O objeto `A` poderia acessar *sem fio* o objeto `C` e modificá-lo sem qualquer passagem de referência, chamando `C::changeSomething()`. Se o objeto `B` também pegar o `C` global, então `A` e `B` podem se influenciar mutuamente através de `C`. - -O uso de variáveis globais introduz no sistema uma nova forma de acoplamento *sem fio*, que não é visível de fora. Cria uma cortina de fumaça complicando a compreensão e o uso do código. Para que os desenvolvedores realmente entendam as dependências, eles precisam ler cada linha do código-fonte. Em vez de apenas se familiarizarem com as interfaces das classes. Além disso, é um acoplamento completamente desnecessário. O estado global é usado porque é facilmente acessível de qualquer lugar e permite, por exemplo, escrever no banco de dados através do método global (estático) `DB::insert()`. Mas, como mostraremos, a vantagem que isso traz é insignificante, enquanto as complicações que causa são fatais. - -.[note] -Do ponto de vista do comportamento, não há diferença entre uma variável global e estática. Elas são igualmente prejudiciais. - - -Ação fantasmagórica à distância -------------------------------- - -"Ação fantasmagórica à distância" - foi assim que Albert Einstein famosamente chamou, em 1935, um fenômeno na física quântica que lhe causava arrepios. -Trata-se do emaranhamento quântico, cuja peculiaridade é que, quando você mede a informação sobre uma partícula, influencia instantaneamente a outra partícula, mesmo que estejam a milhões de anos-luz de distância. Isso aparentemente viola a lei fundamental do universo de que nada pode se propagar mais rápido que a luz. - -No mundo do software, podemos chamar de "ação fantasmagórica à distância" a situação em que iniciamos um processo que acreditamos ser isolado (porque não passamos nenhuma referência a ele), mas em locais remotos do sistema ocorrem interações inesperadas e mudanças de estado das quais não tínhamos conhecimento. Isso só pode acontecer através do estado global. - -Imagine que você se junta a uma equipe de desenvolvedores de um projeto que tem uma base de código extensa e madura. Seu novo líder pede que você implemente uma nova funcionalidade e você, como um bom desenvolvedor, começa escrevendo um teste. Mas como você é novo no projeto, faz muitos testes exploratórios do tipo "o que acontece se eu chamar este método". E tenta escrever o seguinte teste: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // número do seu cartão - $cc->charge(100); -} -``` - -Você executa o código, talvez várias vezes, e depois de um tempo percebe notificações do banco no seu celular informando que a cada execução foram debitados 100 dólares do seu cartão de crédito 🤦‍♂️ - -Como diabos o teste pôde causar um débito real de dinheiro? Operar com um cartão de crédito não é fácil. Você precisa se comunicar com um serviço web de terceiros, precisa saber a URL desse serviço web, precisa fazer login e assim por diante. Nenhuma dessas informações está contida no teste. Pior ainda, você nem sabe onde essas informações estão presentes e, portanto, nem como mockar as dependências externas para que cada execução não leve a um novo débito de 100 dólares. E como você, como novo desenvolvedor, deveria saber que o que estava prestes a fazer resultaria em ficar 100 dólares mais pobre? - -Isso é ação fantasmagórica à distância! - -Você não tem escolha a não ser vasculhar longamente um monte de código-fonte, perguntar aos colegas mais velhos e experientes, até entender como as ligações no projeto funcionam. Isso ocorre porque, ao olhar para a interface da classe `CreditCard`, não é possível identificar o estado global que precisa ser inicializado. Mesmo olhar para o código-fonte da classe não revela qual método de inicialização você deve chamar. Na melhor das hipóteses, você pode encontrar uma variável global que está sendo acessada e, a partir dela, tentar adivinhar como inicializá-la. - -As classes em tal projeto são mentirosas patológicas. O cartão de crédito finge que basta instanciá-lo e chamar o método `charge()`. Secretamente, porém, ele colabora com outra classe `PaymentGateway`, que representa o gateway de pagamento. Sua interface também diz que pode ser inicializada separadamente, mas na realidade ela extrai credenciais de algum arquivo de configuração e assim por diante. Para os desenvolvedores que escreveram este código, está claro que `CreditCard` precisa de `PaymentGateway`. Eles escreveram o código desta forma. Mas para qualquer pessoa nova no projeto, é um mistério absoluto e impede o aprendizado. - -Como consertar a situação? Facilmente. **Deixe a API declarar as dependências.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Observe como as interconexões dentro do código se tornam repentinamente óbvias. Como o método `charge()` declara que precisa de `PaymentGateway`, você não precisa perguntar a ninguém como o código está interconectado. Você sabe que precisa criar sua instância e, ao tentar fazê-lo, descobrirá que precisa fornecer parâmetros de acesso. Sem eles, o código nem sequer seria executado. - -E, o mais importante, agora você pode mockar o gateway de pagamento, para não ser cobrado 100 dólares toda vez que executar o teste. - -O estado global faz com que seus objetos possam acessar secretamente coisas que não são declaradas em sua API e, como resultado, tornam suas APIs mentirosas patológicas. - -Talvez você não tenha pensado nisso antes, mas sempre que usa estado global, está criando canais de comunicação secretos sem fio. A ação fantasmagórica à distância força os desenvolvedores a ler cada linha de código para entender as interações potenciais, reduz a produtividade dos desenvolvedores e confunde os novos membros da equipe. Se você foi quem criou o código, conhece as dependências reais, mas qualquer pessoa que vier depois de você ficará perdida. - -Não escreva código que utilize estado global, prefira passar dependências. Ou seja, injeção de dependência. - - -Fragilidade do estado global ----------------------------- - -No código que usa estado global e singletons, nunca é certo quando e quem alterou esse estado. Esse risco surge já na inicialização. O código a seguir deve criar uma conexão com o banco de dados e inicializar o gateway de pagamento, mas lança constantemente uma exceção e encontrar a causa é extremamente demorado: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Você precisa percorrer detalhadamente o código para descobrir que o objeto `PaymentGateway` acessa sem fio outros objetos, alguns dos quais requerem uma conexão com o banco de dados. Portanto, é necessário inicializar o banco de dados antes de `PaymentGateway`. No entanto, a cortina de fumaça do estado global esconde isso de você. Quanto tempo você economizaria se a API das classes individuais não mentisse e declarasse suas dependências? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Um problema semelhante surge também ao usar acesso global à conexão do banco de dados: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Ao chamar o método `save()`, não é certo se a conexão com o banco de dados já foi criada e quem é responsável por sua criação. Se quisermos, por exemplo, alterar a conexão com o banco de dados em tempo de execução, talvez para testes, provavelmente teríamos que criar outros métodos como `DB::reconnect(...)` ou `DB::reconnectForTest()`. - -Considere o exemplo: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Onde temos certeza de que ao chamar `$article->save()` o banco de dados de teste está realmente sendo usado? E se o método `Foo::doSomething()` alterou a conexão global do banco de dados? Para descobrir, teríamos que examinar o código-fonte da classe `Foo` e provavelmente de muitas outras classes. Essa abordagem, no entanto, traria apenas uma resposta de curto prazo, pois a situação pode mudar no futuro. - -E se movermos a conexão com o banco de dados para uma variável estática dentro da classe `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Isso não mudou nada. O problema é o estado global e é completamente irrelevante em qual classe ele está escondido. Neste caso, assim como no anterior, ao chamar o método `$article->save()`, não temos nenhuma pista sobre em qual banco de dados ele será escrito. Qualquer pessoa do outro lado da aplicação poderia ter alterado o banco de dados a qualquer momento usando `Article::setDb()`. Sob nossos narizes. - -O estado global torna nossa aplicação **extremamente frágil**. - -No entanto, existe uma maneira simples de lidar com esse problema. Basta deixar a API declarar as dependências, garantindo assim a funcionalidade correta. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Graças a essa abordagem, elimina-se a preocupação com alterações ocultas e inesperadas na conexão do banco de dados. Agora temos certeza de onde o artigo está sendo salvo e nenhuma modificação no código dentro de outra classe não relacionada pode mais alterar a situação. O código não é mais frágil, mas estável. - -Não escreva código que utilize estado global, prefira passar dependências. Ou seja, injeção de dependência. - - -Singleton ---------- - -Singleton é um padrão de projeto que, de acordo com a "definição":https://en.wikipedia.org/wiki/Singleton_pattern da conhecida publicação Gang of Four, restringe uma classe a uma única instância e oferece acesso global a ela. A implementação desse padrão geralmente se assemelha ao seguinte código: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // e outros métodos que cumprem as funções da classe dada -} -``` - -Infelizmente, o singleton introduz estado global na aplicação. E como mostramos acima, o estado global é indesejável. Portanto, o singleton é considerado um antipadrão. - -Não use singletons em seu código e substitua-os por outros mecanismos. Você realmente não precisa de singletons. No entanto, se precisar garantir a existência de uma única instância de uma classe para toda a aplicação, deixe isso para o [contêiner DI |container]. Crie assim um singleton de aplicação, ou seja, um serviço. Com isso, a classe deixa de se preocupar em garantir sua própria unicidade (ou seja, não terá o método `getInstance()` e a variável estática) e cumprirá apenas suas funções. Assim, deixará de violar o princípio da responsabilidade única. - - -Estado global versus testes ---------------------------- - -Ao escrever testes, assumimos que cada teste é uma unidade isolada e que nenhum estado externo entra nele. E nenhum estado sai dos testes. Após a conclusão do teste, todo o estado relacionado ao teste deve ser removido automaticamente pelo coletor de lixo. Graças a isso, os testes são isolados. Portanto, podemos executar os testes em qualquer ordem. - -No entanto, se houver estados globais/singletons, todas essas suposições agradáveis desmoronam. O estado pode entrar e sair do teste. De repente, a ordem dos testes pode importar. - -Para poder testar singletons, os desenvolvedores muitas vezes precisam afrouxar suas propriedades, talvez permitindo que a instância seja substituída por outra. Tais soluções são, na melhor das hipóteses, um hack que cria código difícil de manter e entender. Cada teste ou método `tearDown()`, que afeta qualquer estado global, deve reverter essas alterações. - -O estado global é a maior dor de cabeça nos testes unitários! - -Como consertar a situação? Facilmente. Não escreva código que utilize singletons, prefira passar dependências. Ou seja, injeção de dependência. - - -Constantes Globais ------------------- - -O estado global não se limita apenas ao uso de singletons e variáveis estáticas, mas também pode se referir a constantes globais. - -Constantes cujo valor não nos traz nenhuma informação nova (`M_PI`) ou útil (`PREG_BACKTRACK_LIMIT_ERROR`) são claramente aceitáveis. Por outro lado, constantes que servem como uma forma de passar informações *sem fio* para dentro do código não são nada mais do que uma dependência oculta. Como `LOG_FILE` no exemplo a seguir. O uso da constante `FILE_APPEND` é totalmente correto. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Neste caso, deveríamos declarar um parâmetro no construtor da classe `Foo`, para que ele se torne parte da API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Agora podemos passar a informação sobre o caminho do arquivo para log e alterá-la facilmente conforme necessário, o que facilita o teste e a manutenção do código. - - -Funções Globais e Métodos Estáticos ------------------------------------ - -Queremos enfatizar que o uso de métodos estáticos e funções globais em si não é problemático. Explicamos por que o uso de `DB::insert()` e métodos semelhantes é inadequado, mas sempre foi apenas uma questão de estado global armazenado em alguma variável estática. O método `DB::insert()` requer a existência de uma variável estática porque a conexão com o banco de dados está armazenada nela. Sem essa variável, seria impossível implementar o método. - -O uso de métodos estáticos e funções determinísticas, como `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` e muitas outras, está em total conformidade com a injeção de dependência. Essas funções sempre retornam os mesmos resultados para os mesmos parâmetros de entrada e são, portanto, previsíveis. Elas não usam nenhum estado global. - -Existem, porém, também funções no PHP que não são determinísticas. Entre elas está, por exemplo, a função `htmlspecialchars()`. Seu terceiro parâmetro `$encoding`, se não for especificado, tem como valor padrão o valor da opção de configuração `ini_get('default_charset')`. Portanto, recomenda-se sempre especificar este parâmetro para evitar possíveis comportamentos imprevisíveis da função. A Nette faz isso consistentemente. - -Algumas funções, como `strtolower()`, `strtoupper()` e semelhantes, comportaram-se de forma não determinística no passado recente e dependiam da configuração `setlocale()`. Isso causou muitas complicações, mais frequentemente ao trabalhar com a língua turca. Isso porque o turco distingue entre letras `I` maiúsculas e minúsculas com e sem ponto. Assim, `strtolower('I')` retornava o caractere `ı` e `strtoupper('i')` o caractere `İ`, o que levou as aplicações a causar uma série de erros misteriosos. No entanto, esse problema foi corrigido na versão 8.2 do PHP e as funções já não dependem do locale. - -Este é um bom exemplo de como o estado global atormentou milhares de desenvolvedores em todo o mundo. A solução foi substituí-lo por injeção de dependência. - - -Quando é possível usar estado global? -------------------------------------- - -Existem certas situações específicas em que é possível utilizar o estado global. Por exemplo, ao depurar código, quando você precisa imprimir o valor de uma variável ou medir a duração de uma determinada parte do programa. Nesses casos, que dizem respeito a ações temporárias que serão posteriormente removidas do código, é legítimo usar um dumper ou cronômetro globalmente disponível. Essas ferramentas não fazem parte do design do código. - -Outro exemplo são as funções para trabalhar com expressões regulares `preg_*`, que internamente armazenam expressões regulares compiladas em um cache estático na memória. Assim, quando você chama a mesma expressão regular várias vezes em diferentes partes do código, ela é compilada apenas uma vez. O cache economiza desempenho e, ao mesmo tempo, é completamente invisível para o usuário, portanto, tal uso pode ser considerado legítimo. - - -Resumo ------- - -Discutimos por que faz sentido: - -1) Remover todas as variáveis estáticas do código -2) Declarar dependências -3) E usar injeção de dependência - -Ao pensar no design do código, lembre-se de que cada `static $foo` representa um problema. Para que seu código seja um ambiente que respeite DI, é essencial erradicar completamente o estado global e substituí-lo por injeção de dependência. - -Durante esse processo, você pode descobrir que é necessário dividir a classe porque ela tem mais de uma responsabilidade. Não tenha medo disso; busque o princípio da responsabilidade única. - -*Gostaria de agradecer a Miško Hevery, cujos artigos, como [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], são a base deste capítulo.* diff --git a/dependency-injection/pt/introduction.texy b/dependency-injection/pt/introduction.texy deleted file mode 100644 index cfa0126a29..0000000000 --- a/dependency-injection/pt/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -O que é Injeção de Dependência? -******************************* - -.[perex] -Este capítulo apresentará os procedimentos básicos de programação que você deve seguir ao escrever todas as aplicações. São os fundamentos necessários para escrever código limpo, compreensível e sustentável. - -Se você dominar e seguir estas regras, o Nette o apoiará em cada passo. Ele cuidará das tarefas rotineiras para você e fornecerá o máximo de conforto, para que você possa se concentrar na lógica em si. - -Os princípios que mostraremos aqui são bastante simples. Você não precisa se preocupar com nada. - - -Lembra do seu primeiro programa? --------------------------------- - -Não sabemos em que linguagem você o escreveu, mas se fosse PHP, provavelmente seria algo assim: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // imprime 24 -``` - -Algumas linhas triviais de código, mas nelas se escondem tantos conceitos-chave. Que existem variáveis. Que o código é dividido em unidades menores, como funções. Que passamos argumentos de entrada para elas e elas retornam resultados. Faltam apenas condições e loops. - -O fato de passarmos dados de entrada para uma função e ela retornar um resultado é um conceito perfeitamente compreensível, usado também em outras áreas, como na matemática. - -Uma função tem sua assinatura, que consiste em seu nome, uma lista de parâmetros e seus tipos, e finalmente o tipo do valor de retorno. Como usuários, estamos interessados na assinatura; geralmente não precisamos saber nada sobre a implementação interna. - -Agora imagine que a assinatura da função fosse assim: - -```php -function soucet(float $x): float -``` - -Soma com um parâmetro? Isso é estranho... E que tal assim? - -```php -function soucet(): float -``` - -Isso já é muito estranho, não é? Como a função seria usada? - -```php -echo soucet(); // o que será que imprime? -``` - -Ao olhar para tal código, ficaríamos confusos. Não apenas um iniciante não entenderia, mas nem mesmo um programador experiente entenderia tal código. - -Você está pensando como essa função seria por dentro? Onde ela obteria os operandos? Provavelmente, ela os obteria *de alguma forma* por conta própria, talvez assim: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -No corpo da função, descobrimos ligações ocultas a outras funções globais ou métodos estáticos. Para descobrir de onde os operandos realmente vêm, precisamos investigar mais. - - -Não por aqui! -------------- - -O design que acabamos de mostrar é a essência de muitas características negativas: - -- a assinatura da função fingia não precisar de operandos, o que nos confundiu -- não sabemos como fazer a função somar outros dois números -- tivemos que olhar o código para descobrir onde ela obtém os operandos -- descobrimos ligações ocultas -- para entender completamente, é necessário examinar também essas ligações - -E é tarefa da função de soma obter as entradas? Claro que não. Sua responsabilidade é apenas a soma em si. - - -Não queremos encontrar tal código, e definitivamente não queremos escrevê-lo. A correção é simples: voltar ao básico e simplesmente usar parâmetros: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Regra nº 1: peça para ser passado ---------------------------------- - -A regra mais importante é: **todos os dados que uma função ou classe precisa devem ser passados para ela**. - -Em vez de inventar maneiras ocultas pelas quais eles poderiam obtê-los sozinhos, simplesmente passe os parâmetros. Você economizará o tempo necessário para inventar caminhos ocultos, que definitivamente não melhorarão seu código. - -Se você seguir esta regra sempre e em toda parte, estará no caminho para um código sem ligações ocultas. Para um código que é compreensível não apenas para o autor, mas também para qualquer pessoa que o leia depois dele. Onde tudo é compreensível a partir das assinaturas das funções e classes e não há necessidade de procurar segredos ocultos na implementação. - -Essa técnica é tecnicamente chamada de **injeção de dependência**. E esses dados são chamados de **dependências.** Na verdade, é apenas a passagem comum de parâmetros, nada mais. - -.[note] -Por favor, não confunda injeção de dependência, que é um padrão de projeto, com "contêiner de injeção de dependência", que é uma ferramenta, ou seja, algo diametralmente diferente. Falaremos sobre contêineres mais tarde. - - -De funções para classes ------------------------ - -E como as classes se relacionam com isso? Uma classe é uma unidade mais complexa do que uma função simples, mas a regra nº 1 se aplica integralmente aqui também. Apenas existem [mais opções para passar argumentos|passing-dependencies]. Por exemplo, de forma bastante semelhante ao caso de uma função: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Ou usando outros métodos, ou diretamente o construtor: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Ambos os exemplos estão totalmente de acordo com a injeção de dependência. - - -Exemplos reais --------------- - -No mundo real, você não escreverá classes para somar números. Vamos passar para exemplos práticos. - -Temos uma classe `Article` representando um artigo de blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // salvamos o artigo no banco de dados - } -} -``` - -e o uso será o seguinte: - -```php -$article = new Article; -$article->title = '10 coisas que você precisa saber sobre perder peso'; -$article->content = 'Todo ano milhões de pessoas em ...'; -$article->save(); -``` - -O método `save()` salva o artigo em uma tabela do banco de dados. Implementá-lo usando [Nette Database |database:] seria moleza, se não fosse por um obstáculo: onde `Article` obtém a conexão com o banco de dados, ou seja, o objeto da classe `Nette\Database\Connection`? - -Parece que temos muitas opções. Pode obtê-lo de algum lugar em uma variável estática. Ou herdar de uma classe que fornece a conexão com o banco de dados. Ou usar o chamado [singleton |global-state#Singleton]. Ou as chamadas facades, que são usadas no Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Ótimo, resolvemos o problema. - -Ou não? - -Lembre-se da [##Regra nº 1: peça para ser passado]: todas as dependências que a classe precisa devem ser passadas para ela. Porque se quebrarmos a regra, entramos no caminho do código sujo cheio de ligações ocultas, incompreensibilidade, e o resultado será uma aplicação que será dolorosa de manter e desenvolver. - -O usuário da classe `Article` não tem ideia de onde o método `save()` salva o artigo. Em uma tabela do banco de dados? Em qual, produção ou teste? E como isso pode ser alterado? - -O usuário precisa olhar como o método `save()` é implementado e encontra o uso do método `DB::insert()`. Então, ele precisa investigar mais, como esse método obtém a conexão com o banco de dados. E as ligações ocultas podem formar uma cadeia bastante longa. - -Em código limpo e bem projetado, nunca existem ligações ocultas, facades do Laravel ou variáveis estáticas. Em código limpo e bem projetado, os argumentos são passados: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ainda mais prático, como veremos mais adiante, será pelo construtor: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Se você é um programador experiente, pode estar pensando que `Article` não deveria ter um método `save()`, deveria representar puramente um componente de dados e o armazenamento deveria ser responsabilidade de um repositório separado. Isso faz sentido. Mas isso nos levaria muito além do escopo do tópico, que é a injeção de dependência, e do esforço para fornecer exemplos simples. - -Se você for escrever uma classe que requer, por exemplo, um banco de dados para sua operação, não invente de onde obtê-lo, mas peça para que seja passado. Talvez como um parâmetro do construtor ou de outro método. Admita as dependências. Admita-as na API da sua classe. Você obterá um código compreensível e previsível. - -E que tal esta classe, que registra mensagens de erro: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -O que você acha, seguimos a [##Regra nº 1: peça para ser passado]? - -Não seguimos. - -A informação chave, ou seja, o diretório com o arquivo de log, a classe *obtém por si mesma* a partir de uma constante. - -Veja o exemplo de uso: - -```php -$logger = new Logger; -$logger->log('A temperatura é 23 °C'); -$logger->log('A temperatura é 10 °C'); -``` - -Sem conhecer a implementação, você conseguiria responder à pergunta de onde as mensagens são escritas? Você pensaria que para funcionar é necessária a existência da constante `LOG_DIR`? E você conseguiria criar uma segunda instância que escreveria em outro lugar? Certamente não. - -Vamos corrigir a classe: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -A classe agora é muito mais compreensível, configurável e, portanto, mais útil. - -```php -$logger = new Logger('/caminho/para/log.txt'); -$logger->log('A temperatura é 15 °C'); -``` - - -Mas isso não me interessa! --------------------------- - -*"Quando crio um objeto Article e chamo save(), não quero lidar com o banco de dados, só quero que ele seja salvo naquele que configurei."* - -*"Quando uso o Logger, só quero que a mensagem seja escrita, e não quero me preocupar onde. Que use a configuração global."* - -Essas são observações válidas. - -Como exemplo, mostraremos uma classe que envia newsletters e registra o resultado: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-mails foram enviados'); - - } catch (Exception $e) { - $logger->log('Ocorreu um erro ao enviar'); - throw $e; - } - } -} -``` - -O `Logger` aprimorado, que não usa mais a constante `LOG_DIR`, requer que o caminho do arquivo seja especificado no construtor. Como resolver isso? A classe `NewsletterDistributor` não se importa onde as mensagens são escritas, ela só quer escrevê-las. - -A solução é novamente a [##Regra nº 1: peça para ser passado]: todos os dados que a classe precisa, nós passamos para ela. - -Então isso significa que passamos o caminho do log através do construtor, que então usamos ao criar o objeto `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ ASSIM NÃO! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Assim não! O caminho, de fato, **não pertence** aos dados que a classe `NewsletterDistributor` precisa; esses são necessários pelo `Logger`. Você percebe a diferença? A classe `NewsletterDistributor` precisa do logger como tal. Então, passamos ele: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-mails foram enviados'); - - } catch (Exception $e) { - $this->logger->log('Ocorreu um erro ao enviar'); - throw $e; - } - } -} -``` - -Agora está claro pelas assinaturas da classe `NewsletterDistributor` que o log faz parte de sua funcionalidade. E a tarefa de trocar o logger por outro, talvez para testes, é completamente trivial. Além disso, se o construtor da classe `Logger` mudar, isso não terá nenhum efeito em nossa classe. - - -Regra nº 2: pegue o que é seu ------------------------------ - -Não se deixe enganar e não peça para passar as dependências de suas dependências. Peça para passar apenas suas dependências. - -Graças a isso, o código que utiliza outros objetos será completamente independente das mudanças em seus construtores. Sua API será mais verdadeira. E, principalmente, será trivial trocar essas dependências por outras. - - -Novo membro da família ----------------------- - -Na equipe de desenvolvimento, foi decidido criar um segundo logger, que escreve no banco de dados. Criaremos então a classe `DatabaseLogger`. Então temos duas classes, `Logger` e `DatabaseLogger`, uma escreve em arquivo, a outra no banco de dados... não parece algo estranho nessa nomenclatura? Não seria melhor renomear `Logger` para `FileLogger`? Certamente sim. - -Mas faremos isso de forma inteligente. Sob o nome original, criaremos uma interface: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -... que ambos os loggers implementarão: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -E graças a isso, não será necessário alterar nada no restante do código onde o logger é utilizado. Por exemplo, o construtor da classe `NewsletterDistributor` continuará satisfeito em exigir `Logger` como parâmetro. E caberá a nós qual instância passar para ele. - -**Por isso, nunca damos aos nomes das interfaces o sufixo `Interface` ou o prefixo `I`.** Caso contrário, não seria possível desenvolver o código de forma tão elegante. - - -Houston, temos um problema --------------------------- - -Enquanto em toda a aplicação podemos nos contentar com uma única instância de logger, seja de arquivo ou de banco de dados, e simplesmente passá-la para todos os lugares onde algo é registrado, a situação é bem diferente no caso da classe `Article`. Suas instâncias são criadas conforme necessário, até mesmo várias vezes. Como lidar com a dependência do banco de dados em seu construtor? - -Como exemplo, pode servir um controller que, após o envio de um formulário, deve salvar o artigo no banco de dados: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Uma solução possível se oferece diretamente: passamos o objeto do banco de dados pelo construtor para `EditController` e usamos `$article = new Article($this->db)`. - -Assim como no caso anterior com `Logger` e o caminho do arquivo, este não é o procedimento correto. O banco de dados não é uma dependência de `EditController`, mas de `Article`. Passar o banco de dados, portanto, vai contra a [#regra nº 2: pegue o que é seu]. Quando o construtor da classe `Article` mudar (um novo parâmetro for adicionado), será necessário modificar também o código em todos os lugares onde instâncias são criadas. Ufa. - -Houston, o que você sugere? - - -Regra nº 3: deixe para a fábrica --------------------------------- - -Ao eliminar as ligações ocultas e passar todas as dependências como argumentos, obtivemos classes mais configuráveis e flexíveis. E, portanto, precisamos de algo mais, que crie e configure essas classes mais flexíveis para nós. Chamaremos isso de fábricas. - -A regra é: se uma classe tem dependências, deixe a criação de suas instâncias para a fábrica. - -As fábricas são substitutos mais inteligentes do operador `new` no mundo da injeção de dependência. - -.[note] -Por favor, não confunda com o padrão de projeto *factory method*, que descreve um uso específico de fábricas e não está relacionado a este tópico. - - -Fábrica -------- - -Uma fábrica é um método ou classe que produz e configura objetos. A classe que produz `Article` chamaremos de `ArticleFactory` e poderia parecer, por exemplo, assim: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Seu uso no controller será o seguinte: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // deixamos a fábrica criar o objeto - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Neste momento, se a assinatura do construtor da classe `Article` mudar, a única parte do código que precisa reagir é a própria fábrica `ArticleFactory`. Todo o restante do código que trabalha com objetos `Article`, como `EditController`, não será afetado de forma alguma. - -Talvez você esteja batendo na testa agora, se realmente nos ajudamos. A quantidade de código aumentou e tudo começa a parecer suspeitosamente complicado. - -Não se preocupe, em breve chegaremos ao Contêiner de DI do Nette. E ele tem vários ases na manga que simplificarão imensamente a construção de aplicações usando injeção de dependência. Por exemplo, em vez da classe `ArticleFactory`, será suficiente [escrever apenas uma interface |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Mas estamos nos adiantando, aguarde mais um pouco :-) - - -Resumo ------- - -No início deste capítulo, prometemos mostrar um procedimento para projetar código limpo. Basta para as classes - -1) [passar as dependências que precisam |#Regra nº 1: peça para ser passado |#pravidlo č. 1: nech si to předat] -2) [e, inversamente, não passar o que não precisam diretamente |#Regra nº 2: pegue o que é seu |#Pravidlo č. 2: ber, co tvé jest] -3) [e que objetos com dependências são melhor criados em fábricas |#Regra nº 3: deixe para a fábrica |#Pravidlo č. 3: nech to na továrně] - -Pode não parecer à primeira vista, mas essas três regras têm consequências de longo alcance. Elas levam a uma visão radicalmente diferente do design de código. Vale a pena? Programadores que abandonaram velhos hábitos e começaram a usar consistentemente a injeção de dependência consideram este passo um momento crucial em suas vidas profissionais. Abriu-se para eles o mundo de aplicações claras e sustentáveis. - -Mas e se o código não usar consistentemente a injeção de dependência? E se for construído sobre métodos estáticos ou singletons? Isso traz algum problema? [Traz e muito fundamentais |global-state]. diff --git a/dependency-injection/pt/nette-container.texy b/dependency-injection/pt/nette-container.texy deleted file mode 100644 index d7373bd24d..0000000000 --- a/dependency-injection/pt/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Contêiner de DI do Nette -************************ - -.[perex] -Nette DI é uma das bibliotecas mais interessantes do Nette. Ela pode gerar e atualizar automaticamente contêineres de DI compilados, que são extremamente rápidos e incrivelmente fáceis de configurar. - -A forma dos serviços que o Contêiner de DI deve criar é geralmente definida usando arquivos de configuração no [formato NEON|neon:format]. O contêiner que criamos manualmente no [capítulo anterior|container] seria escrito assim: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -A notação é realmente concisa. - -Todas as dependências declaradas nos construtores das classes `ArticleFactory` e `UserController` são descobertas e passadas automaticamente pelo Nette DI graças ao chamado [autowiring|autowiring], portanto, não é necessário especificar nada no arquivo de configuração. Assim, mesmo que os parâmetros mudem, você não precisa alterar nada na configuração. O contêiner Nette é regenerado automaticamente. Você pode se concentrar puramente no desenvolvimento da aplicação. - -Se quisermos passar dependências usando setters, usamos a seção [setup |services#Setup] para isso. - -Nette DI gera diretamente o código PHP do contêiner. O resultado é, portanto, um arquivo `.php` que você pode abrir e estudar. Graças a isso, você vê exatamente como o contêiner funciona. Você também pode depurá-lo no IDE e percorrer passo a passo. E o mais importante: o PHP gerado é extremamente rápido. - -Nette DI também pode gerar código para [fábricas|factory] com base na interface fornecida. Portanto, em vez da classe `ArticleFactory`, basta criar apenas uma interface na aplicação: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Você pode encontrar o exemplo completo [no GitHub|https://github.com/nette-examples/di-example-doc]. - - -Uso independente ----------------- - -Implantar a biblioteca Nette DI em uma aplicação é muito fácil. Primeiro, instalamos com o Composer (porque baixar zips é tããão ultrapassado): - -```shell -composer require nette/di -``` - -O código a seguir cria uma instância do Contêiner de DI de acordo com a configuração armazenada no arquivo `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -O contêiner é gerado apenas uma vez, seu código é escrito no cache (diretório `__DIR__ . '/temp'`) e nas requisições subsequentes ele é apenas carregado de lá. - -Para criar e obter serviços, são usados os métodos `getService()` ou `getByType()`. Assim criamos o objeto `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Durante o desenvolvimento, é útil ativar o modo de atualização automática, onde o contêiner é automaticamente regenerado se qualquer classe ou arquivo de configuração for alterado. Basta especificar `true` como segundo argumento no construtor `ContainerLoader`. - -```php -$loader = new ContainerLoader(__DIR__ . '/temp', autoRebuild: true); -``` - - -Uso com o framework Nette -------------------------- - -Como mostramos, o uso do Nette DI não se limita a aplicações escritas no Nette Framework, você pode implantá-lo em qualquer lugar com apenas 3 linhas de código. No entanto, se você desenvolve aplicações no Nette Framework, a configuração e criação do contêiner são de responsabilidade do [Bootstrap |application:bootstrapping#Configuração do contêiner de DI]. diff --git a/dependency-injection/pt/passing-dependencies.texy b/dependency-injection/pt/passing-dependencies.texy deleted file mode 100644 index 303ddb09c0..0000000000 --- a/dependency-injection/pt/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Passando Dependências -********************* - -<div class=perex> - -Argumentos, ou na terminologia de DI "dependências", podem ser passados para classes das seguintes maneiras principais: - -* passagem pelo construtor -* passagem por método (chamado setter) -* configuração de propriedade -* método, anotação ou atributo *inject* - -</div> - -Agora mostraremos cada variante com exemplos concretos. - - -Passagem pelo construtor -======================== - -As dependências são passadas no momento da criação do objeto como argumentos do construtor: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Esta forma é adequada para dependências obrigatórias que a classe necessita essencialmente para sua função, pois sem elas a instância não poderá ser criada. - -A partir do PHP 8.0, podemos usar uma forma mais curta de notação ([constructor property promotion |https://blog.nette.org/pt/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), que é funcionalmente equivalente: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -A partir do PHP 8.1, a propriedade pode ser marcada com o sinalizador `readonly`, que declara que o conteúdo da propriedade não mudará mais: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -O contêiner de DI passa as dependências para o construtor automaticamente usando [autowiring |autowiring]. Argumentos que não podem ser passados dessa forma (por exemplo, strings, números, booleanos) [escrevemos na configuração |services#Argumentos]. - - -Constructor hell ----------------- - -O termo *constructor hell* descreve a situação em que um descendente herda de uma classe pai cujo construtor requer dependências, e ao mesmo tempo o descendente requer dependências. Ele também deve receber e passar as dependências do pai: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -O problema surge no momento em que queremos alterar o construtor da classe `BaseClass`, por exemplo, quando uma nova dependência é adicionada. Então, é necessário modificar também todos os construtores dos descendentes. O que torna tal modificação um inferno. - -Como evitar isso? A solução é **dar preferência à [composição em vez de herança |faq#Por que a composição é preferida em relação à herança]**. - -Ou seja, projetaremos o código de forma diferente. Evitaremos classes [abstratas |nette:introduction-to-object-oriented-programming#Classes Abstratas] `Base*`. Em vez de `MyClass` obter certas funcionalidades herdando de `BaseClass`, essa funcionalidade será passada como dependência: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Passagem por setter -=================== - -As dependências são passadas chamando um método que as armazena em uma propriedade privada. A convenção usual de nomenclatura para esses métodos é a forma `set*()`, por isso são chamados de setters, mas podem, é claro, ter qualquer outro nome. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Este método é adequado para dependências opcionais que não são essenciais para a função da classe, pois não há garantia de que o objeto realmente receberá a dependência (ou seja, que o usuário chamará o método). - -Ao mesmo tempo, este método permite chamar o setter repetidamente e, assim, alterar a dependência. Se isso não for desejado, adicionamos uma verificação ao método ou, a partir do PHP 8.1, marcamos a propriedade `$cache` com o sinalizador `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new \RuntimeException('A dependência já foi definida'); - } - $this->cache = $cache; - } -} -``` - -A chamada do setter é definida na configuração do contêiner de DI na [chave setup |services#Setup]. Aqui também se utiliza a passagem automática de dependências por autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Configuração de propriedade -=========================== - -As dependências são passadas escrevendo diretamente na propriedade de membro: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Este método é considerado inadequado porque a propriedade de membro deve ser declarada como `public`. E, portanto, não temos controle sobre se a dependência passada será realmente do tipo especificado (válido antes do PHP 7.4) e perdemos a capacidade de reagir à dependência recém-atribuída com código próprio, por exemplo, para impedir alterações subsequentes. Ao mesmo tempo, a propriedade se torna parte da interface pública da classe, o que pode não ser desejável. - -A configuração da propriedade é definida na configuração do contêiner de DI na [seção setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Enquanto os três métodos anteriores se aplicam geralmente em todas as linguagens orientadas a objetos, a injeção por método, anotação ou atributo *inject* é específica puramente para presenters no Nette. Eles são discutidos em um [capítulo separado |best-practices:inject-method-attribute]. - - -Qual método escolher? -===================== - -- o construtor é adequado para dependências obrigatórias que a classe necessita essencialmente para sua função -- o setter, por outro lado, é adequado para dependências opcionais, ou dependências que podem ser alteradas posteriormente -- propriedades públicas não são adequadas diff --git a/dependency-injection/pt/services.texy b/dependency-injection/pt/services.texy deleted file mode 100644 index 916ac4050d..0000000000 --- a/dependency-injection/pt/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Definindo Serviços -****************** - -.[perex] -A configuração é o local onde ensinamos ao contêiner de DI como construir serviços individuais e como conectá-los a outras dependências. O Nette fornece uma maneira muito clara e elegante de conseguir isso. - -A seção `services` no arquivo de configuração no formato NEON é onde definimos nossos próprios serviços e suas configurações. Vejamos um exemplo simples de definição de um serviço chamado `database`, que representa uma instância da classe `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -A configuração fornecida resultará no seguinte método de fábrica no [Contêiner de DI|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Os nomes dos serviços nos permitem referenciá-los em outras partes do arquivo de configuração, no formato `@nomeDoServico`. Se não for necessário nomear o serviço, podemos simplesmente usar um marcador: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Para obter um serviço do contêiner de DI, podemos usar o método `getService()` com o nome do serviço como parâmetro, ou o método `getByType()` com o tipo do serviço: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Criação do serviço -================== - -Geralmente, criamos um serviço simplesmente criando uma instância de uma determinada classe. Por exemplo: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Se precisarmos estender a configuração com outras chaves, a definição pode ser dividida em várias linhas: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -A chave `create` tem um alias `factory`, ambas as variantes são comuns na prática. No entanto, recomendamos usar `create`. - -Os argumentos do construtor ou do método de criação podem ser escritos alternativamente na chave `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Os serviços não precisam ser criados apenas pela simples criação de uma instância de classe, eles também podem ser o resultado da chamada de métodos estáticos ou métodos de outros serviços: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Observe que, para simplificar, `::` é usado em vez de `->`, veja [#expressões]. Os seguintes métodos de fábrica serão gerados: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -O contêiner de DI precisa saber o tipo do serviço criado. Se criarmos um serviço usando um método que não tem um tipo de retorno especificado, devemos especificar explicitamente esse tipo na configuração: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumentos -========== - -Passamos argumentos para o construtor e métodos de maneira muito semelhante ao próprio PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Para melhor legibilidade, podemos dividir os argumentos em linhas separadas. Nesse caso, o uso de vírgulas é opcional: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Você também pode nomear os argumentos e não precisa se preocupar com a ordem deles: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Se você quiser omitir alguns argumentos e usar seu valor padrão ou injetar um serviço usando [autowiring|autowiring], use um sublinhado: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Como argumentos, é possível passar serviços, usar parâmetros e muito mais, veja [#expressões]. - - -Setup -===== - -Na seção `setup`, definimos os métodos que devem ser chamados ao criar o serviço. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Isso seria assim em PHP: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Além de chamar métodos, também é possível passar valores para propriedades. A adição de um elemento a um array também é suportada, o que precisa ser escrito entre aspas para não colidir com a sintaxe NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -O que seria assim no código PHP: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -No setup, no entanto, também é possível chamar métodos estáticos ou métodos de outros serviços. Se você precisar passar o serviço atual como argumento, indique-o como `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Observe que, para simplificar, `::` é usado em vez de `->`, veja [#expressões]. O seguinte método de fábrica será gerado: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Expressões .{expressões} -======================== - -Nette DI nos dá recursos de expressão extraordinariamente ricos, com os quais podemos escrever quase qualquer coisa. Nos arquivos de configuração, podemos usar [parâmetros |configuration#Parâmetros]: - -```neon -# parâmetro -%wwwDir% - -# valor do parâmetro sob a chave -%mailer.user% - -# parâmetro dentro de uma string -'%wwwDir%/images' -``` - -Além disso, criar objetos, chamar métodos e funções: - -```neon -# criação de objeto -DateTime() - -# chamada de método estático -Collator::create(%locale%) - -# chamada de função PHP -::getenv(DB_USER) -``` - -Referenciar serviços pelo nome ou pelo tipo: - -```neon -# serviço por nome -@database - -# serviço por tipo -@Nette\Database\Connection -``` - -Usar a sintaxe first-class callable: .{data-version:3.2.0} - -```neon -# criação de callback, análogo a [@user, logout] -@user::logout(...) -``` - -Usar constantes: - -```neon -# constante de classe -FilesystemIterator::SKIP_DOTS - -# constante global obtida pela função PHP constant() -::constant(PHP_VERSION) -``` - -As chamadas de método podem ser encadeadas como em PHP. Apenas para simplificar, `::` é usado em vez de `->`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Você pode usar essas expressões em qualquer lugar, ao [criar serviços |#Criação do serviço], em [#argumentos], na seção [#Setup] ou em [parâmetros |configuration#Parâmetros]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Funções especiais ------------------ - -Nos arquivos de configuração, você pode usar estas funções especiais: - -- `not()` negação do valor -- `bool()`, `int()`, `float()`, `string()` conversão sem perdas para o tipo especificado -- `typed()` cria um array de todos os serviços do tipo especificado -- `tagged()` cria um array de todos os serviços com a tag especificada - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Em comparação com a conversão de tipo clássica em PHP, como `(int)`, a conversão sem perdas lançará uma exceção para valores não numéricos. - -A função `typed()` cria um array de todos os serviços de um determinado tipo (classe ou interface). Ela omite serviços que têm o autowiring desativado. É possível especificar vários tipos separados por vírgula. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Você também pode passar um array de serviços de um determinado tipo como argumento automaticamente usando [autowiring |autowiring#Array de serviços]. - -A função `tagged()` então cria um array de todos os serviços com uma determinada tag. Aqui também você pode especificar várias tags separadas por vírgula. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -A chave `autowired` permite influenciar o comportamento do autowiring para um serviço específico. Para detalhes, veja [o capítulo sobre autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # o serviço foo é excluído do autowiring -``` - - -Serviços Lazy .{data-version:3.2.4} -=================================== - -Lazy loading é uma técnica que adia a criação de um serviço até o momento em que ele é realmente necessário. Na configuração global, é possível [habilitar a criação lazy |configuration#Serviços Lazy] para todos os serviços de uma vez. Para serviços individuais, você pode então substituir esse comportamento: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Quando um serviço é definido como lazy, ao solicitá-lo do contêiner de DI, recebemos um objeto substituto especial. Ele parece e se comporta da mesma forma que o serviço real, mas a inicialização real (chamada do construtor e setup) ocorre apenas na primeira chamada de qualquer um de seus métodos ou propriedades. - -.[note] -O lazy loading pode ser usado apenas para classes de usuário, não para classes internas do PHP. Requer PHP 8.4 ou mais recente. - - -Tags -==== - -As tags servem para adicionar informações complementares aos serviços. Você pode adicionar uma ou mais tags a um serviço: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -As tags também podem carregar valores: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Para obter todos os serviços com certas tags, você pode usar a função `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -No contêiner de DI, você pode obter os nomes de todos os serviços com uma determinada tag usando o método `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names é um array contendo o nome do serviço e o valor da tag -// por exemplo, ['foo' => 'monolog.logger.event', ...] -``` - - -Modo Inject -=========== - -Usando o sinalizador `inject: true`, a passagem de dependências é ativada através de propriedades públicas com a anotação [inject |best-practices:inject-method-attribute#Atributos Inject] e métodos [inject*() |best-practices:inject-method-attribute#Métodos inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Por padrão, `inject` é ativado apenas para presenters. - - -Modificação de serviços -======================= - -O contêiner de DI contém muitos serviços que foram adicionados através de extensões embutidas ou [de usuário|extensions]. Você pode modificar as definições desses serviços diretamente na configuração. Por exemplo, você pode alterar a classe do serviço `application.application`, que por padrão é `Nette\Application\Application`, para outra: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -O sinalizador `alteration` é informativo e indica que estamos apenas modificando um serviço existente. - -Também podemos complementar o setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Ao sobrescrever um serviço, podemos querer remover os argumentos originais, itens de setup ou tags, para o qual usamos `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Se você quiser remover um serviço adicionado por uma extensão, pode fazer assim: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/ro/@home.texy b/dependency-injection/ro/@home.texy deleted file mode 100644 index 4f32db93ab..0000000000 --- a/dependency-injection/ro/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection este un pattern de design care vă va schimba fundamental perspectiva asupra codului și dezvoltării. Vă va deschide calea către lumea aplicațiilor proiectate curat și sustenabile. - -- [Ce este Dependency Injection? |introduction] -- [Stare globală și singleton-uri |global-state] -- [Transmiterea dependențelor |passing-dependencies] -- [Ce este un container DI? |container] -- [Întrebări frecvente|faq] - - -Pachetul `nette/di` oferă un container DI compilat extrem de avansat pentru PHP. - -- [Nette DI Container |nette-container] -- [Configurație |configuration] -- [Definirea serviciilor |services] -- [Autowiring |autowiring] -- [Fabrici generate |factory] -- [Crearea extensiilor pentru Nette DI|extensions] diff --git a/dependency-injection/ro/@left-menu.texy b/dependency-injection/ro/@left-menu.texy deleted file mode 100644 index 5ae7872994..0000000000 --- a/dependency-injection/ro/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Ce este DI? |introduction] -- [Stare globală și singleton-uri |global-state] -- [Transmiterea dependențelor |passing-dependencies] -- [Ce este un container DI? |container] -- [Întrebări frecvente|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Configurație |configuration] -- [Definirea serviciilor |services] -- [Autowiring |autowiring] -- [Fabrici generate |factory] -- [Crearea extensiilor pentru Nette DI|extensions] diff --git a/dependency-injection/ro/@meta.texy b/dependency-injection/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/dependency-injection/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/dependency-injection/ro/autowiring.texy b/dependency-injection/ro/autowiring.texy deleted file mode 100644 index 58ccaf9083..0000000000 --- a/dependency-injection/ro/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring este o caracteristică excelentă care poate transmite automat serviciile necesare către constructor și alte metode, astfel încât nu trebuie să le scriem deloc. Vă economisește mult timp. - -Datorită acestui fapt, putem omite marea majoritate a argumentelor atunci când scriem definiții de servicii. În loc de: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Este suficient să scrieți: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring se ghidează după tipuri, așa că pentru a funcționa, clasa `ArticleRepository` trebuie definită aproximativ astfel: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Pentru a putea utiliza autowiring, trebuie să existe **exact un serviciu** pentru fiecare tip în container. Dacă ar exista mai multe, autowiring nu ar ști pe care să îl transmită și ar arunca o excepție: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ARUNCĂ EXCEPȚIE, se potrivesc atât mainDb cât și tempDb -``` - -Soluția ar fi fie să ocoliți autowiring-ul și să specificați explicit numele serviciului (adică `articles: Model\ArticleRepository(@mainDb)`). Dar este mai convenabil să [dezactivați |#Dezactivarea autowiring-ului] autowiring-ul pentru unul dintre servicii sau să [prioritizați |#Preferința autowiring-ului] primul serviciu. - - -Dezactivarea autowiring-ului ----------------------------- - -Putem dezactiva autowiring-ul unui serviciu folosind opțiunea `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # serviciul tempDb este exclus din autowiring - - articles: Model\ArticleRepository # prin urmare, transmite mainDb către constructor -``` - -Serviciul `articles` nu aruncă o excepție că există două servicii potrivite de tip `PDO` (adică `mainDb` și `tempDb`) care pot fi transmise constructorului, deoarece vede doar serviciul `mainDb`. - -.[note] -Configurarea autowiring-ului în Nette funcționează diferit față de Symfony, unde opțiunea `autowire: false` specifică faptul că autowiring-ul nu trebuie utilizat pentru argumentele constructorului serviciului respectiv. În Nette, autowiring-ul este întotdeauna utilizat, fie pentru argumentele constructorului, fie pentru orice altă metodă. Opțiunea `autowired: false` specifică faptul că instanța serviciului respectiv nu trebuie transmisă nicăieri prin autowiring. - - -Preferința autowiring-ului --------------------------- - -Dacă avem mai multe servicii de același tip și pentru unul dintre ele specificăm opțiunea `autowired`, acest serviciu devine preferat: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # devine preferat - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Serviciul `articles` nu aruncă o excepție că există două servicii potrivite de tip `PDO` (adică `mainDb` și `tempDb`), ci folosește serviciul preferat, adică `mainDb`. - - -Array de servicii ------------------ - -Autowiring poate transmite și array-uri de servicii de un anumit tip. Deoarece în PHP nu se poate scrie nativ tipul elementelor unui array, este necesar, pe lângă tipul `array`, să se adauge și un comentariu phpDoc cu tipul elementului în formatul `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Containerul DI transmite apoi automat un array de servicii corespunzătoare tipului respectiv. Omită serviciile care au autowiring-ul dezactivat. - -Tipul din comentariu poate fi și în formatul `array<int, Class>` sau `list<Class>`. Dacă nu puteți influența forma comentariului phpDoc, puteți transmite array-ul de servicii direct în configurație folosind [`typed()` |services#Funcții speciale]. - - -Argumente scalare ------------------ - -Autowiring poate injecta doar obiecte și array-uri de obiecte. Argumentele scalare (de ex. șiruri, numere, booleeni) [le scriem în configurație |services#Argumente]. O alternativă este crearea unui [obiect de setări |best-practices:passing-settings-to-presenters], care încapsulează valoarea scalară (sau mai multe valori) sub formă de obiect, care apoi poate fi transmis din nou prin autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly poate fi utilizat începând cu PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Creați un serviciu din acesta adăugându-l în configurație: - -```neon -services: - - MySettings('any value') -``` - -Toate clasele îl vor solicita apoi prin autowiring. - - -Restrângerea autowiring-ului ----------------------------- - -Autowiring-ul serviciilor individuale poate fi restrâns la anumite clase sau interfețe. - -În mod normal, autowiring-ul transmite serviciul către fiecare parametru al metodei al cărui tip corespunde serviciului. Restrângerea înseamnă că stabilim condiții pe care tipurile specificate la parametrii metodelor trebuie să le îndeplinească pentru ca serviciul să le fie transmis. - -Să ilustrăm acest lucru cu un exemplu: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Dacă le-am înregistra pe toate ca servicii, autowiring-ul ar eșua: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ARUNCĂ EXCEPȚIE, se potrivesc serviciile parent și child - childDep: ChildDependent # autowiring transmite serviciul child către constructor -``` - -Serviciul `parentDep` aruncă excepția `Multiple services of type ParentClass found: parent, child`, deoarece ambele servicii `parent` și `child` se potrivesc constructorului său, iar autowiring-ul nu poate decide pe care să îl aleagă. - -Prin urmare, pentru serviciul `child`, putem restrânge autowiring-ul său la tipul `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # se poate scrie și 'autowired: self' - - parentDep: ParentDependent # autowiring transmite serviciul parent către constructor - childDep: ChildDependent # autowiring transmite serviciul child către constructor -``` - -Acum, serviciul `parent` este transmis constructorului serviciului `parentDep`, deoarece acum este singurul obiect potrivit. Autowiring-ul nu mai transmite serviciul `child` acolo. Da, serviciul `child` este încă de tip `ParentClass`, dar condiția de restrângere dată pentru tipul parametrului nu mai este valabilă, adică nu este adevărat că `ParentClass` *este un supratip* al `ChildClass`. - -Pentru serviciul `child`, `autowired: ChildClass` ar putea fi scris și ca `autowired: self`, deoarece `self` este un substituent pentru clasa serviciului curent. - -În cheia `autowired` este posibil să se specifice și mai multe clase sau interfețe ca un array: - -```neon -autowired: [BarClass, FooInterface] -``` - -Să încercăm să completăm exemplul cu interfețe: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Dacă nu restricționăm în niciun fel serviciul `child`, acesta se va potrivi constructorilor tuturor claselor `FooDependent`, `BarDependent`, `ParentDependent` și `ChildDependent`, iar autowiring-ul îl va transmite acolo. - -Dar dacă îi restrângem autowiring-ul la `ChildClass` folosind `autowired: ChildClass` (sau `self`), autowiring-ul îl va transmite doar constructorului `ChildDependent`, deoarece necesită un argument de tip `ChildClass` și este adevărat că `ChildClass` *este de tip* `ChildClass`. Niciun alt tip specificat la ceilalți parametri nu este un supratip al `ChildClass`, deci serviciul nu este transmis. - -Dacă îl restricționăm la `ParentClass` folosind `autowired: ParentClass`, autowiring-ul îl va transmite din nou constructorului `ChildDependent` (deoarece `ChildClass` necesar este un supratip al `ParentClass`) și, nou, și constructorului `ParentDependent`, deoarece tipul necesar `ParentClass` este, de asemenea, potrivit. - -Dacă îl restricționăm la `FooInterface`, va fi în continuare autowired în `ParentDependent` (necesarul `ParentClass` este un supratip al `FooInterface`) și `ChildDependent`, dar în plus și în constructorul `FooDependent`, însă nu în `BarDependent`, deoarece `BarInterface` nu este un supratip al `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring transmite child către constructor - barDep: BarDependent # ARUNCĂ EXCEPȚIE, niciun serviciu nu se potrivește - parentDep: ParentDependent # autowiring transmite child către constructor - childDep: ChildDependent # autowiring transmite child către constructor -``` diff --git a/dependency-injection/ro/configuration.texy b/dependency-injection/ro/configuration.texy deleted file mode 100644 index c78aeade7a..0000000000 --- a/dependency-injection/ro/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Configurarea containerului DI -***************************** - -.[perex] -Prezentare generală a opțiunilor de configurare pentru containerul Nette DI. - - -Fișier de configurare -===================== - -Containerul Nette DI este ușor de controlat folosind fișiere de configurare. Acestea sunt de obicei scrise în [formatul NEON |neon:format]. Pentru editare, recomandăm [editoare cu suport |best-practices:editors-and-tools#Editor IDE] pentru acest format. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Container DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensii]: "Instalarea altor extensii DI .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Includerea fișierelor]: "Includerea fișierelor .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parametri]: "Parametri .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Înregistrarea automată a serviciilor .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Servicii .[prism-token prism-comment]" -</pre> - -.[note] -Pentru a scrie un șir care conține caracterul `%`, trebuie să îl escapați dublându-l la `%%`. - - -Parametri -========= - -În configurație puteți defini parametri care pot fi apoi utilizați ca parte a definițiilor serviciilor. Astfel puteți clarifica configurația sau puteți unifica și extrage valorile care se vor modifica. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Ne referim la parametrul `dsn` oriunde în configurație scriind `%dsn%`. Parametrii pot fi utilizați și în interiorul șirurilor precum `'%wwwDir%/images'`. - -Parametrii nu trebuie să fie doar șiruri sau numere, pot conține și array-uri: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Ne referim la cheia specifică ca `%mailer.user%`. - -Dacă aveți nevoie în codul dvs., de exemplu într-o clasă, să aflați valoarea oricărui parametru, transmiteți-l acelei clase. De exemplu, în constructor. Nu există niciun obiect global care să reprezinte configurația, pe care clasele să îl interogheze pentru valorile parametrilor. Acest lucru ar încălca principiul injecției de dependență. - - -Servicii -======== - -Vezi [capitolul separat |services]. - - -Decorator -========= - -Cum să modificați în masă toate serviciile de un anumit tip? De exemplu, să apelați o anumită metodă la toți presenterii care moștenesc de la un anumit strămoș comun? Pentru asta există decoratorul. - -```neon -decorator: - # pentru toate serviciile care sunt instanțe ale acestei clase sau interfețe - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # apelează această metodă - - $absoluteUrls = true # și setează variabila -``` - -Decoratorul poate fi utilizat și pentru setarea [tag-urilor |services#Tag-uri] sau activarea modului [inject |services#Mod Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Setări tehnice ale containerului DI. - -```neon -di: - # afișează DIC în Tracy Bar? - debugger: ... # (bool) implicit este true - - # tipuri de parametri care nu se autowirează niciodată - excluded: ... # (string[]) - - # permite crearea lazy a serviciilor? - lazy: ... # (bool) implicit este false - - # clasa de la care moștenește containerul DI - parentClass: ... # (string) implicit este Nette\DI\Container -``` - - -Servicii lazy .{data-version:3.2.4} ------------------------------------ - -Setarea `lazy: true` activează crearea lazy (amânată) a serviciilor. Acest lucru înseamnă că serviciile nu sunt create efectiv în momentul în care le solicităm din containerul DI, ci abia în momentul primei lor utilizări. Acest lucru poate accelera pornirea aplicației și reduce cerințele de memorie, deoarece se creează doar serviciile care sunt efectiv necesare în request-ul respectiv. - -Pentru un serviciu specific, crearea lazy poate fi [modificată |services#Servicii lazy]. - -.[note] -Obiectele lazy pot fi utilizate doar pentru clasele utilizatorului, nu și pentru clasele interne PHP. Necesită PHP 8.4 sau o versiune mai recentă. - - -Export metadate ---------------- - -Clasa containerului DI conține și multe metadate. Puteți reduce dimensiunea acesteia prin reducerea exportului de metadate. - -```neon -di: - export: - # exportă parametrii? - parameters: false # (bool) implicit este true - - # exportă tag-urile și care anume? - tags: # (string[]|bool) implicit sunt toate - - event.subscriber - - # exportă datele pentru autowiring și care anume? - types: # (string[]|bool) implicit sunt toate - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Dacă nu utilizați array-ul `$container->getParameters()`, puteți dezactiva exportul parametrilor. În plus, puteți exporta doar acele tag-uri prin care obțineți servicii folosind metoda `$container->findByTag(...)`. Dacă nu apelați deloc metoda, puteți dezactiva complet exportul tag-urilor folosind `false`. - -Puteți reduce semnificativ metadatele pentru [autowiring |autowiring] specificând clasele pe care le utilizați ca parametru al metodei `$container->getByType()`. Și din nou, dacă nu apelați deloc metoda (respectiv doar în [bootstrap |application:bootstrapping] pentru a obține `Nette\Application\Application`), puteți dezactiva complet exportul folosind `false`. - - -Extensii -======== - -Înregistrarea altor extensii DI. În acest fel adăugăm, de exemplu, extensia DI `Dibi\Bridges\Nette\DibiExtension22` sub numele `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Ulterior, o configurăm în secțiunea `dibi`: - -```neon -dibi: - host: localhost -``` - -Ca extensie se poate adăuga și o clasă care are parametri: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Includerea fișierelor -===================== - -Putem include alte fișiere de configurare în secțiunea `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Numele `parameters.php` nu este o greșeală de tipar, configurația poate fi scrisă și într-un fișier PHP, care o returnează ca array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Dacă în fișierele de configurare apar elemente cu aceleași chei, acestea vor fi suprascrise sau, în cazul [array-urilor, combinate |#Combinare]. Fișierul inclus ulterior are prioritate mai mare decât cel anterior. Fișierul în care este specificată secțiunea `includes` are prioritate mai mare decât fișierele incluse în el. - - -Search -====== - -Adăugarea automată a serviciilor în containerul DI face munca extrem de plăcută. Nette adaugă automat presenterii în container, dar se pot adăuga ușor și orice alte clase. - -Este suficient să specificați în ce directoare (și subdirectoare) trebuie căutate clasele: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -De obicei, însă, nu dorim să adăugăm absolut toate clasele și interfețele, așa că le putem filtra: - -```neon -search: - - in: %appDir%/Forms - - # filtrare după numele fișierului (string|string[]) - files: - - *Factory.php - - # filtrare după numele clasei (string|string[]) - classes: - - *Factory -``` - -Sau putem selecta clase care moștenesc sau implementează cel puțin una dintre clasele specificate: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Se pot defini și reguli de excludere, adică măști pentru numele clasei sau strămoși ereditari, care, dacă se potrivesc, serviciul nu se adaugă în containerul DI: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Tuturor serviciilor li se pot seta tag-uri: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Combinare -========= - -Dacă în mai multe fișiere de configurare apar elemente cu aceleași chei, acestea vor fi suprascrise sau, în cazul array-urilor, combinate. Fișierul inclus ulterior are prioritate mai mare decât cel anterior. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Pentru array-uri, se poate preveni combinarea specificând un semn de exclamare după numele cheii: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Configurarea Injecției de Dependență}} diff --git a/dependency-injection/ro/container.texy b/dependency-injection/ro/container.texy deleted file mode 100644 index 2a47b7c357..0000000000 --- a/dependency-injection/ro/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Ce este un container DI? -************************ - -.[perex] -Containerul de injecție de dependență (DIC) este o clasă care poate instanția și configura obiecte. - -Poate vă va surprinde, dar în multe cazuri nu aveți nevoie de un container de injecție de dependență pentru a beneficia de avantajele injecției de dependență (pe scurt DI). Până la urmă, chiar și în [capitolul introductiv |introduction] am arătat DI pe exemple concrete și nu a fost nevoie de niciun container. - -Cu toate acestea, dacă trebuie să gestionați un număr mare de obiecte diferite cu multe dependențe, un container de injecție de dependență va fi cu adevărat util. Ceea ce este cazul aplicațiilor web construite pe un framework. - -În capitolul anterior, am prezentat clasele `Article` și `UserController`. Ambele au anumite dependențe, și anume baza de date și factory-ul `ArticleFactory`. Și pentru aceste clase vom crea acum un container. Desigur, pentru un exemplu atât de simplu nu are sens să avem un container. Dar îl vom crea pentru a arăta cum arată și cum funcționează. - -Iată un container simplu hardcodat pentru exemplul dat: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -Utilizarea ar arăta astfel: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Întrebăm doar containerul despre obiect și nu mai trebuie să știm nimic despre cum să îl creăm și ce dependențe are; containerul știe toate acestea. Dependențele sunt injectate automat de container. Aici stă puterea sa. - -Containerul are deocamdată toate datele scrise hardcodat. Vom face deci următorul pas și vom adăuga parametri pentru ca containerul să fie cu adevărat util: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Cititorii atenți ar fi putut observa o anumită problemă. De fiecare dată când obțin obiectul `UserController`, se creează și o nouă instanță `ArticleFactory` și a bazei de date. Cu siguranță nu dorim acest lucru. - -Vom adăuga deci metoda `getService()`, care va returna mereu aceleași instanțe: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') va apela createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -La primul apel, de ex. `$container->getService('Database')`, va lăsa `createDatabase()` să creeze obiectul bazei de date, pe care îl va stoca în array-ul `$services` și la următorul apel îl va returna direct. - -Modificăm și restul containerului pentru a utiliza `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Apropo, termenul serviciu se referă la orice obiect gestionat de container. De aceea și numele metodei `getService()`. - -Gata. Avem un container DI complet funcțional! Și îl putem folosi: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -După cum puteți vedea, scrierea unui DIC nu este nimic complicat. Merită menționat că obiectele în sine nu știu că sunt create de vreun container. Astfel, este posibil să se creeze în acest mod orice obiect în PHP fără a interveni în codul său sursă. - -Crearea și întreținerea manuală a clasei containerului poate deveni destul de repede un coșmar. De aceea, în capitolul următor vom vorbi despre [Containerul Nette DI |nette-container], care se poate genera și actualiza aproape singur. - - -{{maintitle: Ce este un container de injecție de dependență?}} diff --git a/dependency-injection/ro/extensions.texy b/dependency-injection/ro/extensions.texy deleted file mode 100644 index 016ab80c2d..0000000000 --- a/dependency-injection/ro/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Crearea extensiilor pentru Nette DI -*********************************** - -.[perex] -Generarea containerului DI, pe lângă fișierele de configurare, este influențată și de așa-numitele *extensii*. Le activăm în fișierul de configurare în secțiunea `extensions`. - -Astfel adăugăm extensia reprezentată de clasa `BlogExtension` sub numele `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Fiecare extensie a compilatorului moștenește de la [api:Nette\DI\CompilerExtension] și poate implementa următoarele metode, care sunt apelate succesiv în timpul construirii containerului DI: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Această metodă este apelată prima. Definește schema pentru validarea parametrilor de configurare. - -Configurăm extensia în secțiunea al cărei nume este același cu cel sub care a fost adăugată extensia, adică `blog`: - -```neon -# același nume ca extensia -blog: - postsPerPage: 10 - allowComments: false -``` - -Creăm o schemă care descrie toate opțiunile de configurare, inclusiv tipurile lor, valorile permise și, eventual, valorile implicite: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -Documentația o găsiți pe pagina [Schema |schema:]. În plus, se poate specifica ce opțiuni pot fi [dinamice |application:bootstrapping#Parametri dinamici] folosind `dynamic()`, de ex. `Expect::int()->dynamic()`. - -Accesăm configurația prin variabila `$this->config`, care este un obiect `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Se utilizează pentru adăugarea serviciilor în container. Pentru aceasta se folosește [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // sau setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Convenția este de a prefixa serviciile adăugate de extensie cu numele său, pentru a evita conflictele de nume. Acest lucru îl face metoda `prefix()`, deci dacă extensia se numește `blog`, serviciul va purta numele `blog.articles`. - -Dacă trebuie să redenumim un serviciu, putem crea un alias cu numele original pentru a menține compatibilitatea retroactivă. Nette face acest lucru similar, de exemplu, pentru serviciul `routing.router`, care este disponibil și sub numele anterior `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Încărcarea serviciilor din fișier ---------------------------------- - -Serviciile nu trebuie create doar folosind API-ul clasei ContainerBuilder, ci și prin sintaxa cunoscută utilizată în fișierul de configurare NEON în secțiunea services. Prefixul `@extension` reprezintă extensia curentă. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Încărcăm serviciile: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // încărcarea fișierului de configurare pentru extensie - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Metoda este apelată în momentul în care containerul conține toate serviciile adăugate de extensiile individuale în metodele `loadConfiguration` și, de asemenea, de fișierele de configurare ale utilizatorului. În această fază a construirii, putem deci modifica definițiile serviciilor sau completa legăturile dintre ele. Pentru căutarea serviciilor în container după tag-uri se poate utiliza metoda `findByTag()`, iar după clasă sau interfață, metoda `findByType()`. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -În această fază, clasa containerului este deja generată sub forma unui obiect [ClassType |php-generator:#Clase], conține toate metodele care creează servicii și este pregătită pentru scrierea în cache. Putem încă modifica codul rezultat al clasei în acest moment. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Clasa Configurator, după [crearea containerului |application:bootstrapping#index.php], apelează codul de inițializare, care se creează prin scrierea în obiectul `$this->initialization` folosind [metoda addBody() |php-generator:#Corpuri de metode și funcții]. - -Vom arăta un exemplu despre cum, de exemplu, să pornim sesiunea cu codul de inițializare sau să rulăm servicii care au tag-ul `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // pornirea automată a sesiunii - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // serviciile cu tag-ul run trebuie create după instanțierea containerului - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/ro/factory.texy b/dependency-injection/ro/factory.texy deleted file mode 100644 index 540c2320d7..0000000000 --- a/dependency-injection/ro/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Factory-uri generate -******************** - -.[perex] -Nette DI poate genera automat codul factory-urilor pe baza interfețelor, ceea ce vă economisește scrierea codului. - -Un factory este o clasă care produce și configurează obiecte. Le transmite deci și dependențele lor. Vă rugăm să nu confundați cu pattern-ul de design *factory method*, care descrie un mod specific de utilizare a factory-urilor și nu are legătură cu acest subiect. - -Cum arată un astfel de factory am arătat în [capitolul introductiv |introduction#Fabrica]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI poate genera automat codul factory-urilor. Tot ce trebuie să faceți este să creați o interfață și Nette DI va genera implementarea. Interfața trebuie să aibă exact o metodă numită `create` și să declare tipul returnat: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Deci, factory-ul `ArticleFactory` are o metodă `create`, care creează obiecte `Article`. Clasa `Article` poate arăta, de exemplu, astfel: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Adăugăm factory-ul în fișierul de configurare: - -```neon -services: - - ArticleFactory -``` - -Nette DI va genera implementarea corespunzătoare a factory-ului. - -În codul care utilizează factory-ul, solicităm astfel obiectul conform interfeței și Nette DI va utiliza implementarea generată: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // lăsăm factory-ul să creeze obiectul - $article = $this->articleFactory->create(); - } -} -``` - - -Factory parametrizat -==================== - -Metoda factory `create` poate accepta parametri, pe care îi transmite apoi constructorului. Să completăm, de exemplu, clasa `Article` cu ID-ul autorului articolului: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Adăugăm parametrul și în factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Datorită faptului că parametrul din constructor și parametrul din factory se numesc la fel, Nette DI îi transmite complet automat. - - -Definiție avansată -================== - -Definiția poate fi scrisă și într-o formă multi-linie folosind cheia `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -La scrierea în această formă mai lungă, este posibil să se specifice argumente suplimentare pentru constructor în cheia `arguments` și configurație suplimentară folosind `setup`, la fel ca la serviciile obișnuite. - -Exemplu: dacă metoda `create()` nu ar accepta parametrul `$authorId`, am putea specifica o valoare fixă în configurație, care ar fi transmisă constructorului `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Sau invers, dacă `create()` ar accepta parametrul `$authorId`, dar acesta nu ar face parte din constructor și s-ar transmite prin metoda `Article::setAuthorId()`, ne-am referi la el în secțiunea `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette poate genera, pe lângă factory-uri, și așa-numiții accesori. Aceștia sunt obiecte cu o metodă `get()`, care returnează un anumit serviciu din containerul DI. Apelarea repetată a `get()` returnează mereu aceeași instanță. - -Accesorii oferă lazy-loading pentru dependențe. Să presupunem că avem o clasă care scrie erori într-o bază de date specială. Dacă această clasă ar primi conexiunea la baza de date ca dependență prin constructor, conexiunea ar trebui creată întotdeauna, deși în practică eroarea apare doar excepțional și, prin urmare, în majoritatea cazurilor conexiunea ar rămâne neutilizată. În schimb, clasa primește un accesor și abia atunci când se apelează `get()`, se creează obiectul bazei de date: - -Cum se creează un accesor? Este suficient să scrieți o interfață și Nette DI va genera implementarea. Interfața trebuie să aibă exact o metodă numită `get` și să declare tipul returnat: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Adăugăm accesorul în fișierul de configurare, unde este definit și serviciul pe care îl va returna: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Deoarece accesorul returnează un serviciu de tip `PDO` și în configurație există un singur astfel de serviciu, îl va returna tocmai pe acesta. Dacă ar exista mai multe servicii de tipul respectiv, specificăm serviciul returnat folosind numele, de ex. `- PDOAccessor(@db1)`. - - -Factory/Accesor multiplu -======================== -Factory-urile și accesorii noștri au putut până acum să producă sau să returneze doar un singur obiect. Dar se pot crea foarte ușor și factory-uri multiple combinate cu accesori. Interfața unei astfel de clase va conține un număr arbitrar de metode cu numele `create<name>()` și `get<name>()`, de ex.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Deci, în loc să transmitem mai multe factory-uri și accesori generați, transmitem un factory mai complex care poate face mai multe lucruri. - -Alternativ, în loc de mai multe metode, se poate folosi `get()` cu un parametru: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Atunci este valabil că `MultiFactory::getArticle()` face același lucru ca `MultiFactoryAlt::get('article')`. Cu toate acestea, scrierea alternativă are dezavantajul că nu este evident ce valori `$name` sunt suportate și, logic, nici nu se pot distinge în interfață diferite valori returnate pentru diferite `$name`. - - -Definiție prin listă --------------------- -În acest mod se poate defini un factory multiplu în configurație: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # definește createArticle() - db: PDO(%dsn%, %user%, %password%) # definește getDb() - ) -``` - -Sau ne putem referi în definiția factory-ului la servicii existente folosind o referință: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definește createArticle() - db: @\PDO # definește getDb() - ) -``` - - -Definiție prin tag-uri ----------------------- - -A doua opțiune este utilizarea [tag-urilor |services#Tag-uri] pentru definire: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/ro/faq.texy b/dependency-injection/ro/faq.texy deleted file mode 100644 index 241a7f3c64..0000000000 --- a/dependency-injection/ro/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Întrebări frecvente despre DI (FAQ) -*********************************** - - -Este DI un alt nume pentru IoC? -------------------------------- - -*Inversion of Control* (IoC) este un principiu axat pe modul în care este executat codul - dacă codul dvs. rulează cod străin sau dacă codul dvs. este integrat în cod străin, care îl apelează ulterior. IoC este un termen larg care include [evenimente |nette:glossary#Evenimente], așa-numitul [Principiu Hollywood |application:components#Stilul Hollywood] și alte aspecte. Parte a acestui concept sunt și factory-urile, despre care vorbește [Regula nr. 3: lasă pe seama factory-ului |introduction#Regula nr. 3: Lasă pe seama fabricii], și care reprezintă o inversiune pentru operatorul `new`. - -*Dependency Injection* (DI) se concentrează pe modul în care un obiect află despre alt obiect, adică despre dependențele sale. Este un pattern de design care necesită transmiterea explicită a dependențelor între obiecte. - -Se poate deci spune că DI este o formă specifică de IoC. Cu toate acestea, nu toate formele de IoC sunt potrivite din punct de vedere al curățeniei codului. De exemplu, printre anti-pattern-uri se numără tehnicile care lucrează cu [starea globală |global-state] sau așa-numitul [Service Locator |#Ce este Service Locator]. - - -Ce este Service Locator? ------------------------- - -Este o alternativă la Dependency Injection. Funcționează prin crearea unui depozit central unde sunt înregistrate toate serviciile sau dependențele disponibile. Când un obiect are nevoie de o dependență, o solicită de la Service Locator. - -Cu toate acestea, în comparație cu Dependency Injection, pierde din transparență: dependențele nu sunt transmise direct obiectelor și nu sunt la fel de ușor de identificat, ceea ce necesită examinarea codului pentru a descoperi și înțelege toate legăturile. Testarea este, de asemenea, mai complicată, deoarece nu putem transmite pur și simplu obiecte mock obiectelor testate, ci trebuie să trecem prin Service Locator. În plus, Service Locator perturbă designul codului, deoarece obiectele individuale trebuie să știe despre existența sa, ceea ce diferă de Dependency Injection, unde obiectele nu au cunoștință despre containerul DI. - - -Când este mai bine să nu folosim DI? ------------------------------------- - -Nu sunt cunoscute dificultăți asociate cu utilizarea pattern-ului de design Dependency Injection. Dimpotrivă, obținerea dependențelor din locații disponibile global duce la [o întreagă serie de complicații |global-state], la fel ca și utilizarea Service Locator-ului. Prin urmare, este recomandat să se utilizeze DI întotdeauna. Aceasta nu este o abordare dogmatică, ci pur și simplu nu a fost găsită o alternativă mai bună. - -Cu toate acestea, există anumite situații în care nu transmitem obiecte și le obținem din spațiul global. De exemplu, la depanarea codului, când trebuie să afișați valoarea unei variabile într-un anumit punct al programului, să măsurați durata unei anumite părți a programului sau să înregistrați un mesaj. În astfel de cazuri, când este vorba de acțiuni temporare care vor fi ulterior eliminate din cod, este legitim să se utilizeze un dumper, un cronometru sau un logger disponibil global. Aceste instrumente nu fac parte din designul codului. - - -Are utilizarea DI dezavantaje? ------------------------------- - -Implică utilizarea Dependency Injection vreun dezavantaj, cum ar fi o complexitate crescută a scrierii codului sau o performanță redusă? Ce pierdem când începem să scriem cod în conformitate cu DI? - -DI nu are impact asupra performanței sau a cerințelor de memorie ale aplicației. Performanța containerului DI poate juca un anumit rol, însă în cazul [Nette DI |nette-container], containerul este compilat în PHP pur, astfel încât overhead-ul său în timpul rulării aplicației este practic nul. - -La scrierea codului, este necesar să se creeze constructori care acceptă dependențe. În trecut, acest lucru putea fi anevoios, însă datorită IDE-urilor moderne și [promovării proprietăților constructorului |https://blog.nette.org/ro/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], acum este o chestiune de câteva secunde. Factory-urile pot fi generate ușor folosind Nette DI și plugin-ul pentru PhpStorm printr-un clic de mouse. Pe de altă parte, dispare necesitatea de a scrie singleton-uri și puncte de acces statice. - -Se poate constata că o aplicație proiectată corect care utilizează DI nu este nici mai scurtă, nici mai lungă în comparație cu o aplicație care utilizează singleton-uri. Părțile de cod care lucrează cu dependențe sunt doar extrase din clasele individuale și mutate în locații noi, adică în containerul DI și în factory-uri. - - -Cum să rescrii o aplicație legacy la DI? ----------------------------------------- - -Trecerea de la o aplicație legacy la Dependency Injection poate fi un proces solicitant, în special pentru aplicații mari și complexe. Este important să abordați acest proces sistematic. - -- La trecerea la Dependency Injection, este important ca toți membrii echipei să înțeleagă principiile și procedurile utilizate. -- Mai întâi, efectuați o analiză a aplicației existente și identificați componentele cheie și dependențele lor. Creați un plan care să specifice ce părți vor fi refactorizate și în ce ordine. -- Implementați un container DI sau, și mai bine, utilizați o bibliotecă existentă, de exemplu Nette DI. -- Refactorizați treptat părțile individuale ale aplicației pentru a utiliza Dependency Injection. Acest lucru poate include modificarea constructorilor sau metodelor astfel încât să accepte dependențe ca parametri. -- Modificați locurile din cod unde se creează obiecte cu dependențe, astfel încât dependențele să fie injectate de container. Acest lucru poate include utilizarea factory-urilor. - -Rețineți că trecerea la Dependency Injection este o investiție în calitatea codului și în mentenabilitatea pe termen lung a aplicației. Deși poate fi dificil să efectuați aceste modificări, rezultatul ar trebui să fie un cod mai curat, mai modular și mai ușor de testat, pregătit pentru extinderi și întreținere viitoare. - - -De ce se preferă compoziția în locul moștenirii? ------------------------------------------------- -Este mai potrivit să se utilizeze [compoziția |nette:introduction-to-object-oriented-programming#Compoziție] în locul [moștenirii |nette:introduction-to-object-oriented-programming#Moștenire], deoarece servește la reutilizarea codului fără a ne preocupa de consecințele modificărilor. Oferă deci o legătură mai slabă, în care nu trebuie să ne temem că modificarea unui cod va necesita modificarea altui cod dependent. Un exemplu tipic este situația denumită [constructor hell |passing-dependencies#Constructor hell]. - - -Se poate utiliza Nette DI Container în afara Nette? ---------------------------------------------------- - -Categoric. Nette DI Container face parte din Nette, dar este proiectat ca o bibliotecă independentă care poate fi utilizată independent de celelalte părți ale framework-ului. Este suficient să o instalați folosind Composer, să creați un fișier de configurare cu definiția serviciilor dvs. și apoi, folosind câteva linii de cod PHP, să creați containerul DI. Și puteți începe imediat să beneficiați de avantajele Dependency Injection în proiectele dvs. - -Modul concret de utilizare, inclusiv codurile, este descris în capitolul [Containerul Nette DI |nette-container]. - - -De ce este configurația în fișiere NEON? ----------------------------------------- - -NEON este un limbaj de configurare simplu și ușor de citit, care a fost dezvoltat în cadrul Nette pentru setarea aplicațiilor, serviciilor și dependențelor lor. În comparație cu JSON sau YAML, oferă opțiuni mult mai intuitive și flexibile în acest scop. În NEON se pot descrie natural legături care în Symfony & YAMLu nu ar putea fi scrise fie deloc, fie doar printr-o descriere complicată. - - -Nu încetinește aplicația parsarea fișierelor NEON? --------------------------------------------------- - -Deși fișierele NEON se parsează foarte rapid, acest aspect nu contează deloc. Motivul este că parsarea fișierelor are loc doar o singură dată la prima rulare a aplicației. Apoi se generează codul containerului DI, se salvează pe disc și se rulează la fiecare request ulterior, fără a fi necesară o altă parsare. - -Așa funcționează în mediul de producție. În timpul dezvoltării, fișierele NEON se parsează de fiecare dată când conținutul lor se modifică, pentru ca dezvoltatorul să aibă mereu containerul DI actualizat. Parsarea în sine este, așa cum s-a spus, o chestiune de moment. - - -Cum accesez din clasa mea parametrii din fișierul de configurare? ------------------------------------------------------------------ - -Să ne amintim [Regula nr. 1: lasă-l să ți se transmită |introduction#Regula nr. 1: Primește ce ai nevoie]. Dacă o clasă necesită informații din fișierul de configurare, nu trebuie să ne gândim cum să ajungem la acele informații, ci pur și simplu le solicităm - de exemplu, prin constructorul clasei. Și realizăm transmiterea în fișierul de configurare. - -În acest exemplu, `%myParameter%` este un substituent pentru valoarea parametrului `myParameter`, care se transmite constructorului clasei `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Dacă doriți să transmiteți mai mulți parametri sau să utilizați autowiring, este recomandat [să împachetați parametrii într-un obiect |best-practices:passing-settings-to-presenters]. - - -Suportă Nette PSR-11: Container interface? ------------------------------------------- - -Nette DI Container nu suportă PSR-11 direct. Cu toate acestea, dacă aveți nevoie de interoperabilitate între Nette DI Container și biblioteci sau framework-uri care așteaptă PSR-11 Container Interface, puteți crea un [adaptor simplu |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], care va servi ca o punte între Nette DI Container și PSR-11. diff --git a/dependency-injection/ro/global-state.texy b/dependency-injection/ro/global-state.texy deleted file mode 100644 index 7008c14d33..0000000000 --- a/dependency-injection/ro/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Stare globală și singleton-uri -****************************** - -.[perex] -Avertisment: Următoarele construcții sunt un semn al unui cod prost proiectat: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` sau `static::$var` - -Apar unele dintre aceste construcții în codul dvs.? Atunci aveți ocazia să îl îmbunătățiți. Poate vă gândiți că sunt construcții obișnuite, pe care le vedeți poate chiar și în soluții demonstrative ale diverselor biblioteci și framework-uri. Dacă este așa, atunci designul codului lor nu este bun. - -Acum nu vorbim deloc despre vreo puritate academică. Toate aceste construcții au un lucru în comun: utilizează starea globală. Și aceasta are un impact distructiv asupra calității codului. Clasele mint despre dependențele lor. Codul devine imprevizibil. Încurcă programatorii și le reduce eficiența. - -În acest capitol vom explica de ce este așa și cum să evitați starea globală. - - -Cuplare globală ---------------- - -Într-o lume ideală, un obiect ar trebui să poată comunica doar cu obiectele care i-au fost [transmise direct |passing-dependencies]. Dacă creez două obiecte `A` și `B` și nu transmit niciodată o referință între ele, atunci nici `A`, nici `B`, nu pot ajunge la celălalt obiect sau să îi modifice starea. Aceasta este o proprietate foarte dorită a codului. Este similar cu situația în care aveți o baterie și un bec; becul nu va lumina până nu îl conectați la baterie cu un fir. - -Dar acest lucru nu este valabil pentru variabilele globale (statice) sau singleton-uri. Obiectul `A` ar putea ajunge *fără fir* la obiectul `C` și să îl modifice fără nicio transmitere de referință, prin apelarea `C::changeSomething()`. Dacă obiectul `B` se agață și el de `C` global, atunci `A` și `B` se pot influența reciproc prin intermediul `C`. - -Utilizarea variabilelor globale introduce în sistem o nouă formă de cuplare *fără fir*, care nu este vizibilă din exterior. Creează o perdea de fum care complică înțelegerea și utilizarea codului. Pentru ca dezvoltatorii să înțeleagă cu adevărat dependențele, trebuie să citească fiecare linie de cod sursă. În loc să se familiarizeze pur și simplu cu interfața claselor. Mai mult, este o cuplare complet inutilă. Starea globală se folosește deoarece este ușor accesibilă de oriunde și permite, de exemplu, scrierea în baza de date prin metoda globală (statică) `DB::insert()`. Dar, așa cum vom arăta, avantajul pe care îl aduce este nesemnificativ, în timp ce complicațiile pe care le provoacă sunt fatale. - -.[note] -Din punct de vedere comportamental, nu există nicio diferență între o variabilă globală și una statică. Sunt la fel de dăunătoare. - - -Acțiune înfricoșătoare la distanță ----------------------------------- - -"Acțiune înfricoșătoare la distanță" - așa a numit celebrul Albert Einstein în 1935 un fenomen din fizica cuantică care îi dădea fiori. -Este vorba despre inseparabilitatea cuantică, a cărei particularitate este că atunci când măsori informația despre o particulă, influențezi instantaneu cealaltă particulă, chiar dacă sunt la milioane de ani-lumină distanță. Ceea ce pare să încalce legea fundamentală a universului, că nimic nu se poate propaga mai repede decât lumina. - -În lumea software, putem numi "acțiune înfricoșătoare la distanță" situația în care pornim un proces despre care credem că este izolat (deoarece nu i-am transmis nicio referință), dar în locuri îndepărtate ale sistemului apar interacțiuni neașteptate și modificări de stare despre care nu aveam nicio idee. Acest lucru se poate întâmpla doar prin intermediul stării globale. - -Imaginați-vă că vă alăturați unei echipe de dezvoltatori ai unui proiect care are o bază de cod extinsă și matură. Noul dvs. șef vă cere să implementați o nouă funcționalitate și, ca un dezvoltator bun, începeți prin scrierea unui test. Dar, fiind nou în proiect, faceți multe teste exploratorii de tipul "ce se întâmplă dacă apelez această metodă". Și încercați să scrieți următorul test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // numărul cardului dvs. - $cc->charge(100); -} -``` - -Rulați codul, poate de mai multe ori, și după un timp observați pe mobil notificări de la bancă că la fiecare rulare s-au retras 100 de dolari de pe cardul dvs. de plată 🤦‍♂️ - -Cum naiba a putut testul să provoace retragerea reală de bani? Operarea cu un card de plată nu este ușoară. Trebuie să comunicați cu un serviciu web terț, trebuie să cunoașteți URL-ul acestui serviciu web, trebuie să vă autentificați și așa mai departe. Nicio informație de acest gen nu este conținută în test. Mai rău, nici măcar nu știți unde sunt prezente aceste informații și, prin urmare, nici cum să mock-uiți dependențele externe, astfel încât fiecare rulare să nu ducă la retragerea din nou a 100 de dolari. Și cum trebuia să știți, ca dezvoltator nou, că ceea ce urmați să faceți va duce la sărăcirea cu 100 de dolari? - -Aceasta este acțiunea înfricoșătoare la distanță! - -Nu vă rămâne decât să scormoniți îndelung în multe coduri sursă, să întrebați colegii mai vechi și mai experimentați, până când înțelegeți cum funcționează legăturile în proiect. Acest lucru este cauzat de faptul că, privind interfața clasei `CreditCard`, nu se poate identifica starea globală care trebuie inițializată. Nici măcar privirea în codul sursă al clasei nu vă dezvăluie ce metodă de inițializare trebuie să apelați. În cel mai bun caz, puteți găsi o variabilă globală la care se accesează și din ea să încercați să ghiciți cum să o inițializați. - -Clasele dintr-un astfel de proiect sunt mincinoși patologici. Cardul de plată pretinde că este suficient să îl instanțiați și să apelați metoda `charge()`. În secret, însă, colaborează cu o altă clasă `PaymentGateway`, care reprezintă poarta de plată. Și interfața sa spune că poate fi inițializată separat, dar în realitate își extrage credențialele dintr-un fișier de configurare și așa mai departe. Dezvoltatorilor care au scris acest cod le este clar că `CreditCard` are nevoie de `PaymentGateway`. Au scris codul în acest fel. Dar pentru oricine este nou în proiect, este un mister total și împiedică învățarea. - -Cum să reparați situația? Ușor. **Lăsați API-ul să declare dependențele.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Observați cum legăturile din interiorul codului devin brusc evidente. Prin faptul că metoda `charge()` declară că are nevoie de `PaymentGateway`, nu trebuie să întrebați pe nimeni cum este legat codul. Știți că trebuie să creați instanța sa și, când încercați să faceți acest lucru, veți descoperi că trebuie să furnizați parametrii de acces. Fără ei, codul nici măcar nu ar rula. - -Și, cel mai important, acum puteți mock-ui poarta de plată, astfel încât să nu vi se taxeze 100 de dolari la fiecare rulare a testului. - -Starea globală face ca obiectele dvs. să poată accesa în secret lucruri care nu sunt declarate în API-ul lor și, în consecință, transformă API-urile dvs. în mincinoși patologici. - -Poate că nu v-ați gândit la asta înainte în acest fel, dar ori de câte ori utilizați starea globală, creați canale de comunicare secrete fără fir. Acțiunea înfricoșătoare la distanță îi obligă pe dezvoltatori să citească fiecare linie de cod pentru a înțelege interacțiunile potențiale, reduce productivitatea dezvoltatorilor și îi încurcă pe noii membri ai echipei. Dacă sunteți cel care a creat codul, cunoașteți dependențele reale, dar oricine vine după dvs. este neajutorat. - -Nu scrieți cod care utilizează starea globală, preferați transmiterea dependențelor. Adică injecția de dependență. - - -Fragilitatea stării globale ---------------------------- - -În codul care utilizează starea globală și singleton-uri, nu este niciodată sigur când și cine a modificat această stare. Acest risc apare deja la inițializare. Următorul cod ar trebui să creeze o conexiune la baza de date și să inițializeze poarta de plată, însă aruncă constant o excepție și găsirea cauzei este extrem de anevoioasă: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Trebuie să parcurgeți codul în detaliu pentru a descoperi că obiectul `PaymentGateway` accesează fără fir alte obiecte, dintre care unele necesită o conexiune la baza de date. Prin urmare, este necesar să inițializați baza de date înainte de `PaymentGateway`. Cu toate acestea, perdeaua de fum a stării globale ascunde acest lucru de dvs. Cât timp ați economisi dacă API-urile claselor individuale nu ar minți și și-ar declara dependențele? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -O problemă similară apare și la utilizarea accesului global la conexiunea bazei de date: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -La apelarea metodei `save()`, nu este sigur dacă a fost deja creată conexiunea la baza de date și cine poartă responsabilitatea pentru crearea sa. Dacă dorim, de exemplu, să schimbăm conexiunea la baza de date în timpul rulării, de exemplu pentru teste, ar trebui probabil să creăm alte metode precum `DB::reconnect(...)` sau `DB::reconnectForTest()`. - -Să luăm în considerare un exemplu: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Unde avem certitudinea că la apelarea `$article->save()` se utilizează într-adevăr baza de date de test? Ce se întâmplă dacă metoda `Foo::doSomething()` a schimbat conexiunea globală la baza de date? Pentru a afla, ar trebui să examinăm codul sursă al clasei `Foo` și probabil și al multor altor clase. Această abordare ar aduce însă doar un răspuns pe termen scurt, deoarece situația se poate schimba în viitor. - -Și ce se întâmplă dacă mutăm conexiunea la baza de date într-o variabilă statică în interiorul clasei `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Acest lucru nu a schimbat absolut nimic. Problema este starea globală și este complet irelevant în ce clasă se ascunde. În acest caz, la fel ca în cel precedent, nu avem niciun indiciu la apelarea metodei `$article->save()` despre în ce bază de date se va scrie. Oricine de la celălalt capăt al aplicației ar fi putut schimba oricând baza de date folosind `Article::setDb()`. Sub nasul nostru. - -Starea globală face aplicația noastră **extrem de fragilă**. - -Există însă o modalitate simplă de a aborda această problemă. Este suficient să lăsăm API-ul să declare dependențele, asigurându-se astfel funcționalitatea corectă. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Datorită acestei abordări, dispare teama de modificări ascunse și neașteptate ale conexiunii la baza de date. Acum avem certitudinea unde se salvează articolul și nicio modificare a codului în interiorul altei clase nelegate nu mai poate schimba situația. Codul nu mai este fragil, ci stabil. - -Nu scrieți cod care utilizează starea globală, preferați transmiterea dependențelor. Adică injecția de dependență. - - -Singleton ---------- - -Singleton este un pattern de design care, conform "definiției":https://en.wikipedia.org/wiki/Singleton_pattern din celebra publicație Gang of Four, limitează clasa la o singură instanță și oferă acces global la aceasta. Implementarea acestui pattern seamănă de obicei cu următorul cod: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // și alte metode care îndeplinesc funcțiile clasei respective -} -``` - -Din păcate, singleton introduce starea globală în aplicație. Și, așa cum am arătat mai sus, starea globală este nedorită. Prin urmare, singleton este considerat un antipattern. - -Nu utilizați singleton-uri în codul dvs. și înlocuiți-le cu alte mecanisme. Chiar nu aveți nevoie de singleton-uri. Cu toate acestea, dacă trebuie să garantați existența unei singure instanțe a clasei pentru întreaga aplicație, lăsați acest lucru pe seama [containerului DI |container]. Creați astfel un singleton de aplicație, adică un serviciu. Astfel, clasa încetează să se mai ocupe de asigurarea propriei unicități (adică nu va avea metoda `getInstance()` și variabila statică) și va îndeplini doar funcțiile sale. Astfel, nu va mai încălca principiul responsabilității unice. - - -Stare globală versus teste --------------------------- - -La scrierea testelor, presupunem că fiecare test este o unitate izolată și că nicio stare externă nu intră în el. Și nicio stare nu părăsește testele. După finalizarea testului, toată starea asociată cu testul ar trebui eliminată automat de garbage collector. Datorită acestui fapt, testele sunt izolate. Prin urmare, putem rula testele în orice ordine. - -Cu toate acestea, dacă sunt prezente stări globale/singleton-uri, toate aceste presupuneri plăcute se destramă. Starea poate intra și ieși din test. Brusc, ordinea testelor poate conta. - -Pentru a putea testa singleton-urile, dezvoltatorii trebuie adesea să le relaxeze proprietățile, de exemplu, permițând înlocuirea instanței cu alta. Astfel de soluții sunt, în cel mai bun caz, hack-uri care creează cod dificil de întreținut și de înțeles. Fiecare test sau metodă `tearDown()` care afectează orice stare globală trebuie să anuleze aceste modificări. - -Starea globală este cea mai mare durere de cap la testarea unitară! - -Cum să reparați situația? Ușor. Nu scrieți cod care utilizează singleton-uri, preferați transmiterea dependențelor. Adică injecția de dependență. - - -Constante globale ------------------ - -Starea globală nu se limitează doar la utilizarea singleton-urilor și a variabilelor statice, ci se poate referi și la constantele globale. - -Constantele a căror valoare nu ne aduce nicio informație nouă (`M_PI`) sau utilă (`PREG_BACKTRACK_LIMIT_ERROR`) sunt în mod clar în regulă. Dimpotrivă, constantele care servesc ca o modalitate de a transmite *fără fir* informații în interiorul codului nu sunt altceva decât o dependență ascunsă. Cum ar fi `LOG_FILE` în exemplul următor. Utilizarea constantei `FILE_APPEND` este complet corectă. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -În acest caz, ar trebui să declarăm un parametru în constructorul clasei `Foo`, pentru ca acesta să devină parte a API-ului: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Acum putem transmite informația despre calea către fișierul de logare și o putem schimba ușor după nevoie, ceea ce facilitează testarea și întreținerea codului. - - -Funcții globale și metode statice ---------------------------------- - -Dorim să subliniem că utilizarea în sine a metodelor statice și a funcțiilor globale nu este problematică. Am explicat în ce constă inadecvarea utilizării `DB::insert()` și a metodelor similare, dar întotdeauna a fost vorba doar de o chestiune de stare globală, care este stocată într-o variabilă statică. Metoda `DB::insert()` necesită existența unei variabile statice, deoarece în ea este stocată conexiunea la baza de date. Fără această variabilă, ar fi imposibil să se implementeze metoda. - -Utilizarea metodelor statice și a funcțiilor deterministe, precum `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` și multe altele, este în perfectă concordanță cu injecția de dependență. Aceste funcții returnează întotdeauna aceleași rezultate pentru aceiași parametri de intrare și sunt deci previzibile. Nu utilizează nicio stare globală. - -Există însă și funcții în PHP care nu sunt deterministe. Printre acestea se numără, de exemplu, funcția `htmlspecialchars()`. Al treilea său parametru `$encoding`, dacă nu este specificat, are ca valoare implicită valoarea opțiunii de configurare `ini_get('default_charset')`. De aceea se recomandă specificarea întotdeauna a acestui parametru și prevenirea astfel a unui eventual comportament imprevizibil al funcției. Nette face acest lucru în mod consecvent. - -Unele funcții, precum `strtolower()`, `strtoupper()` și altele similare, s-au comportat nedeterminist în trecutul recent și au fost dependente de setarea `setlocale()`. Acest lucru a cauzat multe complicații, cel mai adesea la lucrul cu limba turcă. Aceasta distinge literele mici și mari `I` cu și fără punct. Astfel, `strtolower('I')` returna caracterul `ı` și `strtoupper('i')` caracterul `İ`, ceea ce a dus la faptul că aplicațiile au început să provoace o serie de erori misterioase. Această problemă a fost însă eliminată în PHP versiunea 8.2 și funcțiile nu mai sunt dependente de locale. - -Este un exemplu frumos despre cum starea globală a chinuit mii de dezvoltatori din întreaga lume. Soluția a fost înlocuirea sa cu injecția de dependență. - - -Când este posibil să se utilizeze starea globală? -------------------------------------------------- - -Există anumite situații specifice în care este posibil să se utilizeze starea globală. De exemplu, la depanarea codului, când trebuie să afișați valoarea unei variabile sau să măsurați durata unei anumite părți a programului. În astfel de cazuri, care se referă la acțiuni temporare ce vor fi ulterior eliminate din cod, este posibil să se utilizeze legitim un dumper sau un cronometru disponibil global. Aceste instrumente nu fac parte din designul codului. - -Un alt exemplu sunt funcțiile pentru lucrul cu expresii regulate `preg_*`, care stochează intern expresiile regulate compilate într-un cache static în memorie. Astfel, când apelați aceeași expresie regulată de mai multe ori în diferite locuri ale codului, aceasta se compilează o singură dată. Cache-ul economisește performanța și, în același timp, este complet invizibil pentru utilizator, prin urmare o astfel de utilizare poate fi considerată legitimă. - - -Rezumat -------- - -Am discutat de ce are sens: - -1) Să eliminați toate variabilele statice din cod -2) Să declarați dependențele -3) Și să utilizați injecția de dependență - -Când vă gândiți la designul codului, gândiți-vă că fiecare `static $foo` reprezintă o problemă. Pentru ca codul dvs. să fie un mediu care respectă DI, este necesar să eliminați complet starea globală și să o înlocuiți folosind injecția de dependență. - -În timpul acestui proces, este posibil să descoperiți că este necesar să împărțiți clasa, deoarece are mai mult de o responsabilitate. Nu vă temeți de acest lucru; urmăriți principiul responsabilității unice. - -*Aș dori să îi mulțumesc lui Miško Hevery, ale cărui articole, precum [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], stau la baza acestui capitol.* diff --git a/dependency-injection/ro/introduction.texy b/dependency-injection/ro/introduction.texy deleted file mode 100644 index 1d0be17a88..0000000000 --- a/dependency-injection/ro/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Ce este Dependency Injection? -***************************** - -.[perex] -Acest capitol vă va introduce în practicile de programare de bază pe care ar trebui să le urmați atunci când scrieți toate aplicațiile. Acestea sunt elementele de bază necesare pentru a scrie cod curat, ușor de înțeles și de întreținut. - -Dacă adoptați aceste reguli și le urmați, Nette vă va sprijini la fiecare pas. Se va ocupa de sarcinile de rutină pentru dvs. și vă va oferi confort maxim, astfel încât să vă puteți concentra pe logica în sine. - -Principiile pe care le vom arăta aici sunt destul de simple. Nu trebuie să vă faceți griji pentru nimic. - - -Vă amintiți primul program? ---------------------------- - -Nu știm în ce limbaj l-ați scris, dar dacă ar fi fost PHP, probabil ar fi arătat cam așa: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // afișează 24 -``` - -Câteva rânduri triviale de cod, dar conțin atât de multe concepte cheie. Că există variabile. Că codul este împărțit în unități mai mici, cum ar fi funcțiile. Că le transmitem argumente de intrare și ele returnează rezultate. Lipsesc doar condițiile și buclele. - -Faptul că transmitem date de intrare unei funcții și aceasta returnează un rezultat este un concept perfect de înțeles, care este utilizat și în alte domenii, cum ar fi matematica. - -O funcție are semnătura sa, care constă în numele său, o listă de parametri și tipurile acestora și, în final, tipul valorii returnate. Ca utilizatori, suntem interesați de semnătură, de obicei nu trebuie să știm nimic despre implementarea internă. - -Acum imaginați-vă că semnătura funcției ar arăta astfel: - -```php -function soucet(float $x): float -``` - -O sumă cu un singur parametru? Ciudat... Și ce ziceți de asta? - -```php -function soucet(): float -``` - -Asta e deja foarte ciudat, nu-i așa? Cum se folosește funcția? - -```php -echo soucet(); // ce va afișa oare? -``` - -Privind un astfel de cod, am fi confuzi. Nu numai că un începător nu l-ar înțelege, dar nici un programator experimentat nu înțelege un astfel de cod. - -Vă întrebați cum ar arăta de fapt o astfel de funcție în interior? De unde ar lua termenii? Probabil că i-ar obține *într-un fel* singură, poate așa: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -În corpul funcției am descoperit legături ascunse către alte funcții globale sau metode statice. Pentru a afla de unde provin de fapt termenii, trebuie să investigăm mai departe. - - -Nu pe aici! ------------ - -Designul pe care tocmai l-am arătat este esența multor caracteristici negative: - -- semnătura funcției pretindea că nu are nevoie de termeni, ceea ce ne-a indus în eroare -- nu știm deloc cum să facem funcția să adune alte două numere -- a trebuit să ne uităm în cod pentru a afla de unde ia termenii -- am descoperit dependențe ascunse -- pentru o înțelegere completă, este necesar să examinăm și aceste dependențe - -Și este oare sarcina funcției de adunare să obțină intrări? Desigur că nu. Responsabilitatea sa este doar adunarea în sine. - - -Nu vrem să întâlnim un astfel de cod și cu siguranță nu vrem să-l scriem. Remedierea este simplă: revenirea la elementele de bază și pur și simplu folosirea parametrilor: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Regula nr. 1: Primește ce ai nevoie ------------------------------------ - -Cea mai importantă regulă este: **toate datele de care funcțiile sau clasele au nevoie trebuie să le fie transmise**. - -În loc să inventați modalități ascunse prin care acestea ar putea ajunge cumva singure la ele, pur și simplu transmiteți parametrii. Veți economisi timp necesar pentru a inventa căi ascunse, care cu siguranță nu vă vor îmbunătăți codul. - -Dacă veți respecta această regulă întotdeauna și peste tot, sunteți pe drumul către un cod fără dependențe ascunse. Către un cod care este de înțeles nu numai pentru autor, ci și pentru oricine îl va citi după el. Unde totul este de înțeles din semnăturile funcțiilor și claselor și nu este nevoie să căutați secrete ascunse în implementare. - -Această tehnică se numește tehnic **dependency injection** (injectarea dependențelor). Iar acele date se numesc **dependențe.** De fapt, este vorba de transmiterea obișnuită a parametrilor, nimic mai mult. - -.[note] -Vă rugăm să nu confundați dependency injection, care este un model de design (design pattern), cu „container DI”, care este un instrument, adică ceva diametral opus. Vom discuta despre containere mai târziu. - - -De la funcții la clase ----------------------- - -Și cum se leagă clasele de asta? O clasă este o unitate mai complexă decât o funcție simplă, dar regula nr. 1 se aplică în totalitate și aici. Doar că există [mai multe opțiuni pentru a pasa argumente|passing-dependencies]. De exemplu, destul de similar cu cazul unei funcții: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Sau folosind alte metode, sau direct constructorul: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Ambele exemple sunt pe deplin în concordanță cu dependency injection. - - -Exemple reale -------------- - -În lumea reală, nu veți scrie clase pentru adunarea numerelor. Să trecem la exemple din practică. - -Să avem o clasă `Article` care reprezintă un articol de blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // salvăm articolul în baza de date - } -} -``` - -și utilizarea va fi următoarea: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Metoda `save()` salvează articolul într-un tabel din baza de date. Implementarea acesteia cu ajutorul [Nette Database |database:] ar fi o joacă de copil, dacă n-ar fi o mică problemă: de unde obține `Article` conexiunea la baza de date, adică obiectul clasei `Nette\Database\Connection`? - -Se pare că avem multe opțiuni. Poate să o ia de undeva dintr-o variabilă statică. Sau să moștenească de la o clasă care asigură conexiunea la baza de date. Sau să utilizeze așa-numitul [singleton |global-state#Singleton]. Sau așa-numitele facades, care sunt utilizate în Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Excelent, am rezolvat problema. - -Sau nu? - -Să ne amintim [##Regula nr. 1: Primește ce ai nevoie]: toate dependențele de care clasa are nevoie trebuie să-i fie transmise. Pentru că dacă încălcăm regula, am pornit pe calea către un cod murdar, plin de dependențe ascunse, neinteligibil, iar rezultatul va fi o aplicație pe care va fi dureros să o întreținem și să o dezvoltăm. - -Utilizatorul clasei `Article` nu știe unde metoda `save()` salvează articolul. Într-un tabel din baza de date? În care, cel de producție sau cel de test? Și cum se poate schimba asta? - -Utilizatorul trebuie să se uite cum este implementată metoda `save()` și găsește utilizarea metodei `DB::insert()`. Așa că trebuie să investigheze mai departe cum își obține această metodă conexiunea la baza de date. Iar dependențele ascunse pot forma un lanț destul de lung. - -Într-un cod curat și bine proiectat nu există niciodată dependențe ascunse, facades Laravel sau variabile statice. Într-un cod curat și bine proiectat se transmit argumente: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Și mai practic, așa cum vom vedea mai departe, va fi prin constructor: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Dacă sunteți un programator experimentat, poate vă gândiți că `Article` nu ar trebui să aibă deloc metoda `save()`, ar trebui să reprezinte o componentă pură de date, iar de salvare ar trebui să se ocupe un repository separat. Asta are sens. Dar astfel am depăși cu mult subiectul dependency injection și efortul de a oferi exemple simple. - -Dacă scrieți o clasă care necesită, de exemplu, o bază de date pentru funcționarea sa, nu vă gândiți de unde să o obțineți, ci lăsați să vă fie transmisă. De exemplu, ca parametru al constructorului sau al altei metode. Recunoașteți dependențele. Recunoașteți-le în API-ul clasei dvs. Veți obține un cod inteligibil și previzibil. - -Și ce ziceți de această clasă, care loghează mesajele de eroare: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Ce credeți, am respectat [##Regula nr. 1: Primește ce ai nevoie]? - -Nu am respectat-o. - -Informația cheie, adică directorul cu fișierul de log, clasa *o obține singură* dintr-o constantă. - -Uitați-vă la exemplul de utilizare: - -```php -$logger = new Logger; -$logger->log('Temperatura este 23 °C'); -$logger->log('Temperatura este 10 °C'); -``` - -Fără a cunoaște implementarea, ați putea răspunde la întrebarea unde se scriu mesajele? V-ați fi gândit că pentru funcționare este necesară existența constantei `LOG_DIR`? Și ați putea crea o a doua instanță care să scrie în altă parte? Cu siguranță nu. - -Să corectăm clasa: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Clasa este acum mult mai inteligibilă, configurabilă și, prin urmare, mai utilă. - -```php -$logger = new Logger('/cale/catre/log.txt'); -$logger->log('Temperatura este 15 °C'); -``` - - -Dar nu mă interesează! ----------------------- - -*„Când creez un obiect Article și apelez save(), nu vreau să mă ocup de baza de date, vreau doar să fie salvat în cea pe care o am setată în configurație.”* - -*„Când folosesc Logger, vreau doar ca mesajul să fie scris și nu vreau să mă ocup de unde. Să se folosească setarea globală.”* - -Acestea sunt observații corecte. - -Ca exemplu, vom arăta o clasă care distribuie newslettere și care loghează cum a decurs: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-mailurile au fost trimise'); - - } catch (Exception $e) { - $logger->log('A apărut o eroare la trimitere'); - throw $e; - } - } -} -``` - -`Logger`-ul îmbunătățit, care nu mai folosește constanta `LOG_DIR`, necesită specificarea căii către fișier în constructor. Cum rezolvăm asta? Clasa `NewsletterDistributor` nu este deloc interesată unde se scriu mesajele, vrea doar să le scrie. - -Soluția este din nou [##Regula nr. 1: Primește ce ai nevoie]: toate datele de care clasa are nevoie, i le transmitem. - -Deci asta înseamnă că transmitem calea către log prin constructor, pe care apoi o folosim la crearea obiectului `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NU AȘA! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Nu așa! Calea **nu face parte** din datele de care are nevoie clasa `NewsletterDistributor`; de acestea are nevoie `Logger`. Percepeți diferența? Clasa `NewsletterDistributor` are nevoie de logger ca atare. Așa că îl vom transmite pe acesta: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-mailurile au fost trimise'); - - } catch (Exception $e) { - $this->logger->log('A apărut o eroare la trimitere'); - throw $e; - } - } -} -``` - -Acum, din semnăturile clasei `NewsletterDistributor` este clar că logarea face parte din funcționalitatea sa. Iar sarcina de a înlocui loggerul cu altul, de exemplu pentru testare, este complet trivială. Mai mult, dacă constructorul clasei `Logger` s-ar schimba, acest lucru nu ar avea niciun impact asupra clasei noastre. - - -Regula nr. 2: Ia doar ce este al tău ------------------------------------- - -Nu vă lăsați induși în eroare și nu vă lăsați să vi se transmită dependențele dependențelor voastre. Lăsați să vi se transmită doar dependențele voastre. - -Datorită acestui fapt, codul care utilizează alte obiecte va fi complet independent de modificările constructorilor acestora. API-ul său va fi mai veridic. Și, mai presus de toate, va fi trivial să înlocuiți aceste dependențe cu altele. - - -Un nou membru al familiei -------------------------- - -În echipa de dezvoltare s-a decis crearea unui al doilea logger, care scrie în baza de date. Vom crea deci clasa `DatabaseLogger`. Așadar, avem două clase, `Logger` și `DatabaseLogger`, una scrie într-un fișier, cealaltă în baza de date... nu vi se pare ceva ciudat la această denumire? Nu ar fi mai bine să redenumim `Logger` în `FileLogger`? Cu siguranță da. - -Dar o vom face inteligent. Sub numele original vom crea o interfață: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… pe care ambii loggeri o vor implementa: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Și datorită acestui fapt, nu va fi nevoie să schimbăm nimic în restul codului unde se utilizează loggerul. De exemplu, constructorul clasei `NewsletterDistributor` va fi în continuare mulțumit că necesită `Logger` ca parametru. Și va depinde doar de noi ce instanță îi vom transmite. - -**De aceea nu adăugăm niciodată sufixul `Interface` sau prefixul `I` la numele interfețelor.** Altfel nu ar fi posibil să dezvoltăm codul atât de frumos. - - -Houston, avem o problemă ------------------------- - -În timp ce în întreaga aplicație ne putem descurca cu o singură instanță de logger, fie el de fișier sau de bază de date, și pur și simplu o transmitem oriunde se loghează ceva, situația este destul de diferită în cazul clasei `Article`. Instanțele sale le creăm după nevoie, chiar de mai multe ori. Cum să gestionăm dependența de baza de date în constructorul său? - -Ca exemplu poate servi un controller care, după trimiterea unui formular, trebuie să salveze articolul în baza de date: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -O posibilă soluție se oferă direct: lăsăm obiectul bazei de date să fie transmis prin constructor către `EditController` și folosim `$article = new Article($this->db)`. - -La fel ca în cazul anterior cu `Logger` și calea către fișier, aceasta nu este abordarea corectă. Baza de date nu este o dependență a `EditController`, ci a `Article`. Transmiterea bazei de date contravine deci [Regulii nr. 2: Ia doar ce este al tău |#Regula nr. 2: Ia doar ce este al tău]. Când se schimbă constructorul clasei `Article` (se adaugă un nou parametru), va fi necesar să se modifice și codul în toate locurile unde se creează instanțe. Ufff. - -Houston, ce propui? - - -Regula nr. 3: Lasă pe seama fabricii ------------------------------------- - -Prin eliminarea dependențelor ascunse și transmiterea tuturor dependențelor ca argumente, am obținut clase mai configurabile și mai flexibile. Și, prin urmare, avem nevoie de ceva în plus, care să ne creeze și să ne configureze acele clase mai flexibile. Le vom numi fabrici. - -Regula este: dacă o clasă are dependențe, lăsați crearea instanțelor sale pe seama unei fabrici. - -Fabricile sunt înlocuitori mai inteligenți ai operatorului `new` în lumea dependency injection. - -.[note] -Vă rugăm să nu confundați cu modelul de design (design pattern) *factory method*, care descrie un mod specific de utilizare a fabricilor și nu are legătură cu acest subiect. - - -Fabrica -------- - -O fabrică este o metodă sau o clasă care produce și configurează obiecte. Clasa care produce `Article` o vom numi `ArticleFactory` și ar putea arăta, de exemplu, astfel: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Utilizarea sa în controller va fi următoarea: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // lăsăm fabrica să creeze obiectul - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Dacă în acest moment se schimbă semnătura constructorului clasei `Article`, singura parte a codului care trebuie să reacționeze este însăși fabrica `ArticleFactory`. Tot restul codului care lucrează cu obiecte `Article`, cum ar fi `EditController`, nu va fi afectat în niciun fel. - -Poate vă bateți acum capul dacă ne-am ajutat cu ceva. Cantitatea de cod a crescut și totul începe să pară suspect de complicat. - -Nu vă faceți griji, în curând vom ajunge la containerul Nette DI. Și acesta are o serie de ași în mânecă, care simplifică enorm construirea aplicațiilor care utilizează dependency injection. De exemplu, în loc de clasa `ArticleFactory`, va fi suficient să [scrie doar o interfață |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Dar anticipăm, mai aveți puțină răbdare :-) - - -Rezumat -------- - -La începutul acestui capitol am promis că vom arăta o metodă de a proiecta cod curat. Este suficient ca claselor - -1) [să le transmitem dependențele de care au nevoie |#Regula nr. 1: Primește ce ai nevoie] -2) [și, dimpotrivă, să nu le transmitem ceea ce nu au nevoie direct |#Regula nr. 2: Ia doar ce este al tău] -3) [și că obiectele cu dependențe sunt cel mai bine create în fabrici |#Regula nr. 3: Lasă pe seama fabricii] - -Poate nu pare așa la prima vedere, dar aceste trei reguli au consecințe de anvergură. Conduc la o perspectivă radical diferită asupra designului codului. Merită? Programatorii care au renunțat la vechile obiceiuri și au început să utilizeze consecvent dependency injection consideră acest pas un moment crucial în viața lor profesională. Li s-a deschis lumea aplicațiilor clare și ușor de întreținut. - -Dar ce se întâmplă dacă codul nu utilizează consecvent dependency injection? Ce se întâmplă dacă este construit pe metode statice sau singleton-uri? Aduce asta probleme? [Aduce și foarte fundamentale |global-state]. diff --git a/dependency-injection/ro/nette-container.texy b/dependency-injection/ro/nette-container.texy deleted file mode 100644 index c076686fe6..0000000000 --- a/dependency-injection/ro/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI este una dintre cele mai interesante biblioteci Nette. Poate genera și actualiza automat containere DI compilate, care sunt extrem de rapide și uimitor de ușor de configurat. - -Forma serviciilor pe care containerul DI trebuie să le creeze o definim de obicei folosind fișiere de configurație în [format NEON|neon:format]. Containerul pe care l-am creat manual în [capitolul anterior|container] s-ar scrie astfel: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Notația este într-adevăr concisă. - -Toate dependențele declarate în constructorii claselor `ArticleFactory` și `UserController`, Nette DI le descoperă și le transmite singur datorită așa-numitului [autowiring |autowiring], de aceea nu este nevoie să se specifice nimic în fișierul de configurație. Astfel, chiar dacă parametrii se schimbă, nu trebuie să modificați nimic în configurație. Containerul Nette se regenerează automat. Vă puteți concentra astfel exclusiv pe dezvoltarea aplicației. - -Dacă dorim să transmitem dependențe folosind setteri, folosim secțiunea [setup |services#Setup] pentru aceasta. - -Nette DI generează direct cod PHP pentru container. Rezultatul este deci un fișier `.php`, pe care îl puteți deschide și studia. Datorită acestui fapt, vedeți exact cum funcționează containerul. Îl puteți de asemenea depana în IDE și parcurge pas cu pas. Și cel mai important: PHP-ul generat este extrem de rapid. - -Nette DI poate genera și cod pentru [fabrici|factory] pe baza interfeței furnizate. De aceea, în loc de clasa `ArticleFactory`, ne va fi suficient să creăm în aplicație doar o interfață: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Exemplul complet îl găsiți [pe GitHub|https://github.com/nette-examples/di-example-doc]. - - -Utilizare independentă ----------------------- - -Implementarea bibliotecii Nette DI într-o aplicație este foarte ușoară. Mai întâi o instalăm cu Composer (pentru că descărcarea arhivelor zip este așaaa de învechită): - -```shell -composer require nette/di -``` - -Următorul cod creează o instanță a containerului DI conform configurației stocate în fișierul `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Containerul se generează o singură dată, codul său se scrie în cache (directorul `__DIR__ . '/temp'`) și la cererile ulterioare se încarcă doar de aici. - -Pentru crearea și obținerea serviciilor se folosesc metodele `getService()` sau `getByType()`. Astfel creăm obiectul `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -În timpul dezvoltării este util să activăm modul auto-refresh, în care containerul se regenerează automat dacă se modifică orice clasă sau fișier de configurație. Este suficient să specificăm `true` ca al doilea argument în constructorul `ContainerLoader`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Utilizare cu framework-ul Nette -------------------------------- - -Așa cum am arătat, utilizarea Nette DI nu este limitată la aplicațiile scrise în Nette Framework, îl puteți implementa oriunde cu doar 3 rânduri de cod. Dacă însă dezvoltați aplicații în Nette Framework, configurarea și crearea containerului sunt gestionate de [Bootstrap |application:bootstrapping#Configurarea containerului DI]. diff --git a/dependency-injection/ro/passing-dependencies.texy b/dependency-injection/ro/passing-dependencies.texy deleted file mode 100644 index 3d70d4acba..0000000000 --- a/dependency-injection/ro/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Transmiterea dependențelor -************************** - -<div class=perex> - -Argumentele, sau în terminologia DI „dependențele”, pot fi transmise claselor în următoarele moduri principale: - -* transmitere prin constructor -* transmitere prin metodă (așa-numitul setter) -* setarea proprietății (variabilei membru) -* prin metodă, adnotare sau atribut *inject* - -</div> - -Acum vom arăta fiecare variantă cu exemple concrete. - - -Transmitere prin constructor -============================ - -Dependențele sunt transmise în momentul creării obiectului ca argumente ale constructorului: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Această formă este potrivită pentru dependențele obligatorii, de care clasa are neapărat nevoie pentru funcționarea sa, deoarece fără ele instanța nu va putea fi creată. - -Începând cu PHP 8.0 putem folosi o formă mai scurtă de notație ([constructor property promotion |https://blog.nette.org/ro/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), care este funcțional echivalentă: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Începând cu PHP 8.1, proprietatea poate fi marcată cu flag-ul `readonly`, care declară că conținutul proprietății nu se va mai schimba: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Containerul DI transmite constructorului dependențele automat folosind [autowiring |autowiring]. Argumentele care nu pot fi transmise astfel (de ex. șiruri, numere, booleeni) [le scriem în configurație |services#Argumente]. - - -Constructor hell ----------------- - -Termenul *constructor hell* desemnează situația în care un descendent moștenește de la o clasă părinte al cărei constructor necesită dependențe, și în același timp descendentul necesită dependențe. În acest caz, trebuie să preia și să transmită și pe cele părintești: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Problema apare în momentul în care dorim să schimbăm constructorul clasei `BaseClass`, de exemplu când se adaugă o nouă dependență. Atunci este necesar să modificăm și toți constructorii descendenților. Ceea ce face o astfel de modificare un iad. - -Cum să prevenim asta? Soluția este **să preferăm [compoziția în detrimentul moștenirii |faq#De ce se preferă compoziția în locul moștenirii]**. - -Deci vom proiecta codul altfel. Vom evita clasele [abstracte |nette:introduction-to-object-oriented-programming#Clase abstracte] `Base*`. În loc ca `MyClass` să obțină o anumită funcționalitate prin moștenirea de la `BaseClass`, își va lăsa această funcționalitate să-i fie transmisă ca dependență: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Transmitere prin setter -======================= - -Dependențele sunt transmise prin apelarea unei metode care le stochează într-o proprietate privată. Convenția obișnuită de denumire a acestor metode este forma `set*()`, de aceea li se spune setteri, dar pot fi, desigur, numite oricum altfel. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Acest mod este potrivit pentru dependențele opționale, care nu sunt necesare pentru funcționarea clasei, deoarece nu este garantat că obiectul va primi efectiv dependența (adică că utilizatorul va apela metoda). - -În același timp, acest mod permite apelarea repetată a setterului și astfel modificarea dependenței. Dacă acest lucru nu este dorit, adăugăm o verificare în metodă sau, începând cu PHP 8.1, marcăm proprietatea `$cache` cu flag-ul `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Dependența a fost deja setată.'); - } - $this->cache = $cache; - } -} -``` - -Apelarea setterului o definim în configurația containerului DI în [cheia setup |services#Setup]. Și aici se utilizează transmiterea automată a dependențelor prin autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Setarea proprietății -==================== - -Dependențele sunt transmise prin scrierea directă în proprietatea membru: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Acest mod este considerat nepotrivit, deoarece proprietatea membru trebuie declarată ca `public`. Și, prin urmare, nu avem control asupra faptului că dependența transmisă va fi într-adevăr de tipul dat (valabil înainte de PHP 7.4) și pierdem posibilitatea de a reacționa la dependența nou atribuită cu cod propriu, de exemplu, pentru a preveni modificarea ulterioară. În același timp, proprietatea devine parte a interfeței publice a clasei, ceea ce poate să nu fie de dorit. - -Setarea proprietății o definim în configurația containerului DI în [secțiunea setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -În timp ce cele trei moduri anterioare sunt valabile în general în toate limbajele orientate pe obiecte, injectarea prin metodă, adnotare sau atribut *inject* este specifică exclusiv presenterilor din Nette. Despre acestea se discută într-un [capitol separat |best-practices:inject-method-attribute]. - - -Ce mod să alegem? -================= - -- constructorul este potrivit pentru dependențele obligatorii, de care clasa are neapărat nevoie pentru funcționarea sa -- setterul este, dimpotrivă, potrivit pentru dependențele opționale sau dependențele care pot fi modificate ulterior -- proprietățile publice nu sunt potrivite diff --git a/dependency-injection/ro/services.texy b/dependency-injection/ro/services.texy deleted file mode 100644 index 08b7683a29..0000000000 --- a/dependency-injection/ro/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Definirea serviciilor -********************* - -.[perex] -Configurația este locul unde învățăm containerul DI cum să asambleze serviciile individuale și cum să le conecteze cu alte dependențe. Nette oferă o modalitate foarte clară și elegantă de a realiza acest lucru. - -Secțiunea `services` din fișierul de configurație în format NEON este locul unde definim serviciile proprii și configurațiile lor. Să vedem un exemplu simplu de definire a unui serviciu numit `database`, care reprezintă o instanță a clasei `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Configurația menționată va rezulta în următoarea metodă factory în [containerul DI|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Numele serviciilor ne permit să ne referim la ele în alte părți ale fișierului de configurație, în formatul `@numeServiciu`. Dacă nu este necesar să numim serviciul, putem folosi pur și simplu doar o liniuță: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Pentru a obține un serviciu din containerul DI, putem utiliza metoda `getService()` cu numele serviciului ca parametru, sau metoda `getByType()` cu tipul serviciului: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Crearea serviciului -=================== - -De cele mai multe ori, creăm un serviciu pur și simplu prin crearea unei instanțe a unei anumite clase. De exemplu: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Dacă avem nevoie să extindem configurația cu alte chei, definiția poate fi împărțită pe mai multe rânduri: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Cheia `create` are aliasul `factory`, ambele variante sunt comune în practică. Cu toate acestea, recomandăm utilizarea `create`. - -Argumentele constructorului sau ale metodei de creare pot fi alternativ scrise în cheia `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Serviciile nu trebuie create doar prin simpla instanțiere a unei clase, ele pot fi, de asemenea, rezultatul apelării metodelor statice sau metodelor altor servicii: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Observați că, pentru simplitate, în loc de `->` se folosește `::`, vezi [#Expresii]. Se vor genera aceste metode factory: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Containerul DI trebuie să cunoască tipul serviciului creat. Dacă creăm un serviciu folosind o metodă care nu are specificat tipul returnat, trebuie să specificăm explicit acest tip în configurație: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumente -========= - -Transmitem argumente constructorului și metodelor într-un mod foarte similar cu cel din PHP însuși: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Pentru o mai bună lizibilitate, putem împărți argumentele pe rânduri separate. În acest caz, utilizarea virgulelor este opțională: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Puteți, de asemenea, să numiți argumentele și nu trebuie să vă mai faceți griji cu privire la ordinea lor: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Dacă doriți să omiteți unele argumente și să folosiți valoarea lor implicită sau să injectați un serviciu folosind [autowiring |autowiring], utilizați underscore `_`: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Ca argumente se pot transmite servicii, se pot utiliza parametri și multe altele, vezi [#Expresii]. - - -Setup -===== - -În secțiunea `setup` definim metodele care trebuie apelate la crearea serviciului. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Acest lucru ar arăta astfel în PHP: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Pe lângă apelarea metodelor, se pot transmite și valori către proprietăți. Este suportată și adăugarea unui element într-un array, care trebuie scris între ghilimele pentru a nu intra în conflict cu sintaxa NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Ceea ce în codul PHP ar arăta astfel: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -În setup se pot apela însă și metode statice sau metode ale altor servicii. Dacă aveți nevoie să transmiteți serviciul curent ca argument, specificați-l ca `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Observați că, pentru simplitate, în loc de `->` se folosește `::`, vezi [#Expresii]. Se va genera o astfel de metodă factory: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Expresii -======== - -Nette DI ne oferă expresii extrem de bogate, cu ajutorul cărora putem scrie aproape orice. În fișierele de configurație putem astfel utiliza [parametri |configuration#Parametri]: - -```neon -# parametru -%wwwDir% - -# valoarea parametrului sub cheie -%mailer.user% - -# parametru în interiorul șirului -'%wwwDir%/images' -``` - -Mai departe, putem crea obiecte, apela metode și funcții: - -```neon -# crearea obiectului -DateTime() - -# apelarea metodei statice -Collator::create(%locale%) - -# apelarea funcției PHP -::getenv(DB_USER) -``` - -Ne putem referi la servicii fie după numele lor, fie după tip: - -```neon -# serviciu după nume -@database - -# serviciu după tip -@Nette\Database\Connection -``` - -Putem folosi sintaxa first-class callable: .{data-version:3.2.0} - -```neon -# crearea callback-ului, echivalent cu [@user, logout] -@user::logout(...) -``` - -Putem folosi constante: - -```neon -# constanta clasei -FilesystemIterator::SKIP_DOTS - -# constanta globală o obținem cu funcția PHP constant() -::constant(PHP_VERSION) -``` - -Apelurile metodelor pot fi înlănțuite la fel ca în PHP. Doar pentru simplitate, în loc de `->` se folosește `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Aceste expresii le puteți utiliza oriunde, la [crearea serviciilor |#Crearea serviciului], în [#argumente], în secțiunea [#Setup] sau în [parametri |configuration#Parametri]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Funcții speciale ----------------- - -În fișierele de configurație puteți utiliza aceste funcții speciale: - -- `not()` negația valorii -- `bool()`, `int()`, `float()`, `string()` conversie de tip fără pierderi la tipul specificat -- `typed()` creează un array al tuturor serviciilor de tipul specificat -- `tagged()` creează un array al tuturor serviciilor cu tag-ul dat - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Spre deosebire de conversia de tip clasică în PHP, cum ar fi de ex. `(int)`, conversia de tip fără pierderi va arunca o excepție pentru valorile non-numerice. - -Funcția `typed()` creează un array al tuturor serviciilor de tipul dat (clasă sau interfață). Omite serviciile care au autowiring-ul dezactivat. Se pot specifica și mai multe tipuri separate prin virgulă. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Puteți transmite array-ul de servicii de un anumit tip ca argument și automat folosind [autowiring |autowiring#Array de servicii]. - -Funcția `tagged()` creează apoi un array al tuturor serviciilor cu un anumit tag. Și aici puteți specifica mai multe tag-uri separate prin virgulă. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Cheia `autowired` permite influențarea comportamentului autowiring-ului pentru un serviciu specific. Pentru detalii, vezi [capitolul despre autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # serviciul foo este exclus din autowiring -``` - - -Servicii lazy .{data-version:3.2.4} -=================================== - -Încărcarea leneșă (Lazy loading) este o tehnică care amână crearea unui serviciu până în momentul în care este efectiv necesar. În configurația globală se poate [permite crearea lazy |configuration#Servicii lazy] pentru toate serviciile simultan. Pentru servicii individuale, puteți apoi suprascrie acest comportament: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Când un serviciu este definit ca lazy, la solicitarea sa din containerul DI, primim un obiect substituent special. Acesta arată și se comportă la fel ca serviciul real, dar inițializarea reală (apelarea constructorului și a setup-ului) are loc abia la primul apel al oricărei metode sau proprietăți ale sale. - -.[note] -Încărcarea leneșă poate fi utilizată numai pentru clasele definite de utilizator, nu și pentru clasele interne PHP. Necesită PHP 8.4 sau o versiune mai recentă. - - -Tag-uri -======= - -Tag-urile servesc la adăugarea de informații suplimentare serviciilor. Puteți adăuga unul sau mai multe tag-uri unui serviciu: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tag-urile pot purta și valori: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Pentru a obține toate serviciile cu anumite tag-uri, puteți utiliza funcția `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -În containerul DI puteți obține numele tuturor serviciilor cu un anumit tag folosind metoda `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names este un array care conține numele serviciului și valoarea tag-ului -// de ex. ['foo' => 'monolog.logger.event', ...] -``` - - -Mod Inject -========== - -Folosind flag-ul `inject: true` se activează transmiterea dependențelor prin proprietăți publice cu adnotarea [inject |best-practices:inject-method-attribute#Atribute Inject] și metodele [inject*() |best-practices:inject-method-attribute#Metode inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -În mod implicit, `inject` este activat doar pentru presenteri. - - -Modificarea serviciilor -======================= - -Containerul DI conține multe servicii care au fost adăugate prin extensii încorporate sau [extensii utilizator|extensions]. Puteți modifica definițiile acestor servicii direct în configurație. De exemplu, puteți schimba clasa serviciului `application.application`, care este standard `Nette\Application\Application`, cu alta: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Flag-ul `alteration` este informativ și indică faptul că doar modificăm un serviciu existent. - -Putem, de asemenea, completa setup-ul: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -La suprascrierea unui serviciu, putem dori să eliminăm argumentele originale, elementele setup sau tag-urile, pentru aceasta folosim `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Dacă doriți să eliminați un serviciu adăugat de o extensie, o puteți face astfel: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/ru/@home.texy b/dependency-injection/ru/@home.texy index 495ee1282f..c2e6f046ae 100644 --- a/dependency-injection/ru/@home.texy +++ b/dependency-injection/ru/@home.texy @@ -2,13 +2,13 @@ Nette DI ******** .[perex] -Внедрение зависимостей (Dependency Injection) — это шаблон проектирования, который кардинально изменит ваш взгляд на код и разработку. Он откроет вам путь в мир чисто спроектированных и поддерживаемых приложений. +Dependency Injection - шаблон проектирования, который в корне изменит ваш взгляд на код и разработку. Он открывает дорогу в мир чисто спроектированных и жизнеспособных приложений. -- [Что такое внедрение зависимостей? |introduction] +- [Что такое Dependency Injection? |introduction] - [Глобальное состояние и синглтоны |global-state] - [Передача зависимостей |passing-dependencies] - [Что такое DI-контейнер? |container] -- [Часто задаваемые вопросы|faq] +- [Часто задаваемые вопросы |faq] Пакет `nette/di` предоставляет чрезвычайно продвинутый компилируемый DI-контейнер для PHP. @@ -18,4 +18,5 @@ Nette DI - [Определение сервисов |services] - [Autowiring |autowiring] - [Генерируемые фабрики |factory] -- [Создание расширений для Nette DI|extensions] +- [Создание расширений для Nette DI |extensions] +- [Компиляция в подробностях |compilation-internals] diff --git a/dependency-injection/ru/@left-menu.texy b/dependency-injection/ru/@left-menu.texy index 51c371afdb..31eccac945 100644 --- a/dependency-injection/ru/@left-menu.texy +++ b/dependency-injection/ru/@left-menu.texy @@ -1,10 +1,10 @@ -Внедрение зависимостей -********************** +Dependency Injection +******************** - [Что такое DI? |introduction] - [Глобальное состояние и синглтоны |global-state] - [Передача зависимостей |passing-dependencies] - [Что такое DI-контейнер? |container] -- [Часто задаваемые вопросы|faq] +- [Часто задаваемые вопросы |faq] Nette DI @@ -14,4 +14,15 @@ Nette DI - [Определение сервисов |services] - [Autowiring |autowiring] - [Генерируемые фабрики |factory] -- [Создание расширений для Nette DI|extensions] +- [Создание расширений для 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/autowiring.texy b/dependency-injection/ru/autowiring.texy index 1085b88fe0..23ebf399c8 100644 --- a/dependency-injection/ru/autowiring.texy +++ b/dependency-injection/ru/autowiring.texy @@ -2,9 +2,9 @@ Autowiring ********** .[perex] -Autowiring — это отличная функция, которая умеет автоматически передавать в конструктор и другие методы требуемые сервисы, так что нам вообще не нужно их писать. Это сэкономит вам много времени. +Autowiring - прекрасная возможность, которая автоматически передаёт нужные сервисы в конструктор и другие методы, так что нам не приходится указывать их явно. Это экономит вам массу времени. -Благодаря этому мы можем опустить подавляющее большинство аргументов при написании определений сервисов. Вместо: +Благодаря ему мы можем опускать подавляющее большинство аргументов при написании определений сервисов. Вместо: ```neon services: @@ -18,7 +18,7 @@ services: articles: Model\ArticleRepository ``` -Autowiring руководствуется типами, поэтому для его работы класс `ArticleRepository` должен быть определен примерно так: +Autowiring руководствуется типами, поэтому, чтобы он работал, класс `ArticleRepository` должен быть определён примерно так: ```php namespace Model; @@ -30,7 +30,9 @@ class ArticleRepository } ``` -Чтобы можно было использовать autowiring, для каждого типа в контейнере должен быть **ровно один сервис**. Если их будет больше, autowiring не будет знать, какой из них передать, и выбросит исключение: +Autowiring никогда не использует имена сервисов. Он руководствуется исключительно системой типов PHP, поэтому знает и то, что класс удовлетворяет интерфейсам, которые он реализует, и классам, от которых он наследуется. Благодаря этому имя сервиса - лишь вспомогательный идентификатор, и его переименование ничего в приложении не сломает. + +Чтобы autowiring можно было использовать, в контейнере должен быть **ровно один сервис** каждого типа. Если бы их было больше, autowiring не знал бы, какой передать, и выбросил бы исключение: ```neon services: @@ -39,13 +41,13 @@ services: articles: Model\ArticleRepository # ВЫБРОСИТ ИСКЛЮЧЕНИЕ, подходят и mainDb, и tempDb ``` -Решением было бы либо обойти autowiring и явно указать имя сервиса (т.е. `articles: Model\ArticleRepository(@mainDb)`). Но удобнее autowiring одного из сервисов [отключить |#Отключение autowiring] или первый сервис [сделать предпочтительным |#Предпочтение autowiring]. +Одно из решений - обойти autowiring и явно указать имя сервиса (например, `articles: Model\ArticleRepository(@mainDb)`). Однако удобнее либо [отключить |#Отключение autowiring] autowiring для одного из сервисов, либо [сделать |#Предпочтительный сервис для autowiring] один сервис предпочтительным перед остальными. Отключение autowiring --------------------- -Autowiring сервиса можно отключить с помощью опции `autowired: no`: +Мы можем отключить autowiring для сервиса параметром `autowired: false`: ```neon services: @@ -53,21 +55,23 @@ services: tempDb: create: PDO('sqlite::memory:') - autowired: false # сервис tempDb исключен из autowiring + autowired: false # сервис tempDb исключён из autowiring - articles: Model\ArticleRepository # следовательно, передает mainDb в конструктор + articles: Model\ArticleRepository # поэтому в конструктор передаётся mainDb ``` -Сервис `articles` не выбросит исключение о том, что существуют два подходящих сервиса типа `PDO` (т.е. `mainDb` и `tempDb`), которые можно передать в конструктор, потому что он видит только сервис `mainDb`. +Сервис `articles` не выбросит исключение о том, что для конструктора доступны два подходящих сервиса `PDO` (`mainDb` и `tempDb`), потому что он видит только сервис `mainDb`. + +Autowiring можно отключить и глобально для целых типов параметром конфигурации [`di › excluded` |configuration#DI], где перечисляются типы (и их потомки), которые никогда не должны попадать в autowiring. .[note] -Конфигурация autowiring в Nette работает иначе, чем в Symfony, где опция `autowire: false` говорит, что не следует использовать autowiring для аргументов конструктора данного сервиса. В Nette autowiring используется всегда, будь то для аргументов конструктора или любых других методов. Опция `autowired: false` говорит, что экземпляр данного сервиса не должен передаваться никуда с помощью autowiring. +Настройка autowiring в Nette отличается от Symfony. В Symfony `autowire: false` означает, что autowiring не нужно использовать для аргументов конструктора сервиса. В Nette autowiring относится к аргументам конструктора и любых других методов, вызываемых через контейнер (например, при внедрении через сеттер). Параметр `autowired: false` не даёт контейнеру автоматически передавать экземпляр этого сервиса как зависимость другим сервисам. -Предпочтение autowiring ------------------------ +Предпочтительный сервис для autowiring +-------------------------------------- -Если у нас есть несколько сервисов одного типа и у одного из них указана опция `autowired`, этот сервис становится предпочтительным: +Если у нас несколько сервисов одного типа и для одного из них мы задаём параметр `autowired`, этот сервис становится предпочтительным: ```neon services: @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -Сервис `articles` не выбросит исключение о том, что существуют два подходящих сервиса типа `PDO` (т.е. `mainDb` и `tempDb`), но использует предпочтительный сервис, то есть `mainDb`. +Сервис `articles` не выбросит исключение о нескольких подходящих сервисах `PDO` (`mainDb` и `tempDb`), а использует предпочтительный, то есть `mainDb`. -Массив сервисов ---------------- +Набор сервисов +-------------- -Autowiring умеет передавать и массивы сервисов определенного типа. Поскольку в PHP нельзя нативно записать тип элементов массива, необходимо помимо типа `array` добавить и phpDoc-комментарий с типом элемента в формате `ClassName[]`: +Autowiring умеет передавать и массивы сервисов определённого типа. Поскольку PHP не поддерживает указание типа элементов массива в объявлениях типов, вам нужно дополнить объявление типа `array` комментарием phpDoc с типом элементов, например `ClassName[]`: ```php namespace Model; @@ -102,45 +106,62 @@ class ShipManager } ``` -DI-контейнер затем автоматически передаст массив сервисов, соответствующих данному типу. Он пропустит сервисы, у которых отключен autowiring. +DI-контейнер тогда автоматически передаст массив сервисов соответствующего типа. Он пропускает сервисы, у которых [autowiring отключён |#Отключение autowiring], и никогда не включает создаваемый в данный момент сервис в его собственный набор. В отличие от передачи отдельного сервиса, [сужение |#Сужение autowiring] autowiring до определённого типа или пометка сервиса как [предпочтительного |#Предпочтительный сервис для autowiring] здесь не действуют: в массиве всегда оказываются все сервисы заданного типа. -Тип в комментарии может быть также в формате `array<int, Class>` или `list<Class>`. Если вы не можете повлиять на вид phpDoc-комментария, вы можете передать массив сервисов непосредственно в конфигурации с помощью [`typed()` |services#Специальные функции]. +Тип в комментарии может иметь и вид `array<int, Class>` или `list<Class>`. Если вы не можете управлять видом комментария phpDoc, вы можете передать массив сервисов прямо в конфигурации через [`typed()` |services#Специальные функции]. Скалярные аргументы ------------------- -Autowiring умеет подставлять только объекты и массивы объектов. Скалярные аргументы (например, строки, числа, булевы значения) [запишем в конфигурации |services#Аргументы]. Альтернативой является создание [объекта настроек |best-practices:passing-settings-to-presenters], который инкапсулирует скалярное значение (или несколько значений) в виде объекта, и его затем можно снова передавать с помощью autowiring. +Autowiring работает только для объектов и массивов объектов. Скалярные аргументы (например, строки, числа, логические значения) нужно [указывать в конфигурации |services#Аргументы]. Альтернатива - создать [объект настроек|best-practices:passing-settings-to-presenters], упаковывающий скалярное значение (или несколько значений). Такой объект затем можно передавать через autowiring. ```php class MySettings { public function __construct( - // readonly можно использовать с PHP 8.1 + // readonly можно использовать начиная с PHP 8.1 public readonly bool $value, ) {} } ``` -Вы создадите из него сервис, добавив его в конфигурацию: +Вы регистрируете его как сервис, добавив в конфигурацию: ```neon services: - MySettings('any value') ``` -Все классы затем запросят его с помощью autowiring. +Остальные классы затем могут запросить его через autowiring. + + +Необязательные зависимости +-------------------------- + +Если у параметра конструктора или метода есть значение по умолчанию, а сервиса нужного типа в контейнере нет, autowiring не выбрасывает исключение: он просто пропускает аргумент, и используется значение по умолчанию. Так объявляются необязательные зависимости: + +```php +class Foo +{ + public function __construct( + private ?Logger $logger = null, + ) {} +} +``` + +Напротив, для параметра без значения по умолчанию отсутствие сервиса всегда приводит к исключению. Сужение autowiring ------------------ -Для отдельных сервисов можно сузить autowiring только до определенных классов или интерфейсов. +Для отдельных сервисов autowiring можно сузить до определённых классов или интерфейсов. -Обычно autowiring передает сервис в каждый параметр метода, типу которого сервис соответствует. Сужение означает, что мы устанавливаем условия, которым должны соответствовать типы, указанные у параметров методов, чтобы им был передан сервис. +Обычно autowiring передаёт сервис в каждый параметр метода, типу которого сервис соответствует. Сужение означает, что мы устанавливаем условия, которым должны отвечать типы, указанные у параметров метода, чтобы сервис туда передавался. -Покажем это на примере: +Возьмём пример: ```php class ParentClass @@ -162,42 +183,42 @@ class ChildDependent } ``` -Если бы мы зарегистрировали их все как сервисы, то autowiring завершился бы неудачей: +Если бы мы зарегистрировали их все как сервисы, autowiring не справился бы: ```neon services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # ВЫБРОСИТ ИСКЛЮЧЕНИЕ, подходят сервисы parent и child - childDep: ChildDependent # autowiring передаст сервис child в конструктор + parentDep: ParentDependent # ВЫБРОСИТ ИСКЛЮЧЕНИЕ, подходят и parent, и child + childDep: ChildDependent # autowiring передаёт в конструктор сервис child ``` -Сервис `parentDep` выбросит исключение `Multiple services of type ParentClass found: parent, child`, потому что в его конструктор подходят оба сервиса `parent` и `child`, и autowiring не может решить, какой из них выбрать. +Сервис `parentDep` выбрасывает исключение `Multiple services of type ParentClass found: child, parent`, потому что в его конструктор подходят и сервис `parent`, и сервис `child`, а autowiring не может решить, какой выбрать. -Поэтому для сервиса `child` мы можем сузить его autowiring до типа `ChildClass`: +Поэтому для сервиса `child` мы можем сузить autowiring до типа `ChildClass`: ```neon services: parent: ParentClass child: create: ChildClass - autowired: ChildClass # можно также написать 'autowired: self' + autowired: ChildClass # можно записать и как 'autowired: self' - parentDep: ParentDependent # autowiring передаст сервис parent в конструктор - childDep: ChildDependent # autowiring передаст сервис child в конструктор + parentDep: ParentDependent # autowiring передаёт в конструктор сервис parent + childDep: ChildDependent # autowiring передаёт в конструктор сервис child ``` -Теперь в конструктор сервиса `parentDep` передается сервис `parent`, потому что теперь это единственный подходящий объект. Сервис `child` autowiring туда больше не передаст. Да, сервис `child` по-прежнему имеет тип `ParentClass`, но сужающее условие, заданное для типа параметра, больше не выполняется, т.е. неверно, что `ParentClass` *является супертипом* `ChildClass`. +Теперь в конструктор сервиса `parentDep` передаётся сервис `parent`, потому что он стал единственным подходящим объектом. Сервис `child` autowiring туда больше не передаёт. Да, сервис `child` по-прежнему имеет тип `ParentClass`, но условие сужения `autowired: ChildClass` означает, что он будет передаваться только в параметры, явно объявленные как `ChildClass` (или его подтипы). Поскольку `ParentDependent` требует `ParentClass`, сервис `child` больше не считается там кандидатом на autowiring. -Для сервиса `child` можно было бы записать `autowired: ChildClass` также как `autowired: self`, поскольку `self` является псевдонимом для класса текущего сервиса. +Для сервиса `child` запись `autowired: ChildClass` можно было бы заменить на `autowired: self`, потому что `self` - это подстановка для класса текущего сервиса. -В ключе `autowired` можно указать и несколько классов или интерфейсов в виде массива: +В ключе `autowired` можно указать и несколько классов или интерфейсов массивом: ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -Попробуем дополнить пример еще и интерфейсами: +Попробуем добавить в пример интерфейсы: ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -Если сервис `child` никак не ограничивать, он будет подходить в конструкторы всех классов `FooDependent`, `BarDependent`, `ParentDependent` и `ChildDependent`, и autowiring его туда передаст. +Если мы никак не ограничим сервис `child`, он подойдёт в конструкторы всех классов `FooDependent`, `BarDependent`, `ParentDependent` и `ChildDependent`, и autowiring передаст его туда. -Но если его autowiring сузить до `ChildClass` с помощью `autowired: ChildClass` (или `self`), autowiring передаст его только в конструктор `ChildDependent`, потому что он требует аргумент типа `ChildClass` и верно, что `ChildClass` *имеет тип* `ChildClass`. Никакой другой тип, указанный у других параметров, не является супертипом `ChildClass`, поэтому сервис не передается. +Однако если мы сузим его autowiring до `ChildClass` через `autowired: ChildClass` (или `self`), autowiring передаст его только в конструктор `ChildDependent`, потому что тот требует аргумент типа `ChildClass`, а `ChildClass` *имеет тип* `ChildClass`. Ни у одного другого параметра требуемый тип не является `ChildClass` или его подтипом, поэтому в них сервис не передаётся. -Если его ограничить до `ParentClass` с помощью `autowired: ParentClass`, autowiring снова передаст его в конструктор `ChildDependent` (потому что требуемый `ChildClass` является супертипом `ParentClass`) и теперь также в конструктор `ParentDependent`, потому что требуемый тип `ParentClass` также подходит. +Если мы ограничим его до `ParentClass` через `autowired: ParentClass`, autowiring снова передаст его в конструктор `ChildDependent` (потому что требуемый `ChildClass` - подтип `ParentClass`), а теперь ещё и в конструктор `ParentDependent`, потому что требуемый тип `ParentClass` тоже подходит. -Если его ограничить до `FooInterface`, он по-прежнему будет автовайриться в `ParentDependent` (требуемый `ParentClass` является супертипом `FooInterface`) и `ChildDependent`, но дополнительно и в конструктор `FooDependent`, однако не в `BarDependent`, поскольку `BarInterface` не является супертипом `FooInterface`. +Если мы ограничим его до `FooInterface`, он по-прежнему будет передаваться в `ParentDependent` (требуемый `ParentClass` - подтип `FooInterface`) и `ChildDependent`, а дополнительно и в конструктор `FooDependent`, но не в `BarDependent`, потому что `BarInterface` не является подтипом `FooInterface`. ```neon services: @@ -251,8 +272,8 @@ services: create: ChildClass autowired: FooInterface - fooDep: FooDependent # autowiring передаст child в конструктор + fooDep: FooDependent # autowiring передаёт в конструктор сервис child barDep: BarDependent # ВЫБРОСИТ ИСКЛЮЧЕНИЕ, ни один сервис не подходит - parentDep: ParentDependent # autowiring передаст child в конструктор - childDep: ChildDependent # autowiring передаст child в конструктор + parentDep: ParentDependent # autowiring передаёт в конструктор сервис child + childDep: ChildDependent # autowiring передаёт в конструктор сервис child ``` diff --git a/dependency-injection/ru/compilation-internals.texy b/dependency-injection/ru/compilation-internals.texy new file mode 100644 index 0000000000..6a7c921834 --- /dev/null +++ b/dependency-injection/ru/compilation-internals.texy @@ -0,0 +1,222 @@ +Компиляция в подробностях +************************* + +.[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 index bc18331de9..e8671a28b2 100644 --- a/dependency-injection/ru/configuration.texy +++ b/dependency-injection/ru/configuration.texy @@ -2,32 +2,32 @@ ************************** .[perex] -Обзор опций конфигурации для DI-контейнера Nette. +Обзор параметров конфигурации DI-контейнера Nette. -Файл конфигурации -================= +Конфигурационный файл +===================== -DI-контейнер Nette легко управляется с помощью файлов конфигурации. Они обычно записываются в [формате NEON |neon:format]. Для редактирования рекомендуем [редакторы с поддержкой |best-practices:editors-and-tools#IDE редактор] этого формата. +DI-контейнером Nette легко управлять с помощью конфигурационных файлов. Обычно они пишутся в [формате NEON|neon:format]. Мы рекомендуем использовать [редакторы с поддержкой |tools:ide] этого формата. <pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Декоратор .[prism-token prism-comment]"<br> +"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]":[#Расширения]: "Установка других DI-расширений .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Включение файлов]: "Включение файлов .[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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -На параметр `dsn` можно сослаться где угодно в конфигурации записью `%dsn%`. Параметры можно использовать и внутри строк, например `'%wwwDir%/images'`. +На параметр `dsn` мы ссылаемся где угодно в конфигурации записью `%dsn%`. Параметры можно использовать и внутри строк вроде `'%wwwDir%/images'`. -Параметры не обязательно должны быть только строками или числами, они также могут содержать массивы: +Параметрами могут быть не только строки или числа, они могут содержать и массивы: ```neon parameters: @@ -49,32 +49,32 @@ parameters: languages: [cs, en, de] ``` -На конкретный ключ можно сослаться как `%mailer.user%`. +На конкретный ключ мы ссылаемся как `%mailer.user%`. -Если вам нужно в вашем коде, например, в классе, узнать значение какого-либо параметра, передайте его в этот класс. Например, в конструкторе. Не существует никакого глобального объекта, представляющего конфигурацию, у которого классы запрашивали бы значения параметров. Это было бы нарушением принципа dependency injection. +Если вашему коду (например, классу) нужно значение параметра, передайте его в класс. Например, в конструкторе. Никакого глобального объекта конфигурации, у которого классы могли бы запросить значения параметров, нет. Это было бы нарушением принципа внедрения зависимостей. Сервисы ======= -См. [отдельную главу |services]. +См. [отдельную главу|services]. Decorator ========= -Как массово изменить все сервисы определенного типа? Например, вызвать определенный метод у всех презентеров, которые наследуются от конкретного общего предка? Для этого существует decorator. +Как изменить сразу несколько сервисов определённого типа? Например, как вызвать определённый метод у всех презентеров, наследующих от конкретного базового класса? Для этого и служит decorator. ```neon decorator: - # для всех сервисов, являющихся экземплярами этого класса или интерфейса + # для всех сервисов, которые являются экземплярами этого класса или интерфейса App\Presentation\BasePresenter: setup: - - setProjectId(10) # вызовите этот метод - - $absoluteUrls = true # и установите переменную + - setProjectId(10) # вызвать этот метод + - $absoluteUrls = true # и задать переменную ``` -Decorator также можно использовать для установки [тегов |services#Теги] или включения режима [inject |services#Режим Inject]. +Decorator можно использовать и для установки [тегов |services#Теги] или включения [режима inject |services#Режим inject]. ```neon decorator: @@ -91,13 +91,13 @@ DI ```neon di: - # показать DI-контейнер в Tracy Bar? - debugger: ... # (bool) по умолчанию true + # показывать DIC в панели Tracy? + debugger: ... # (bool) по умолчанию определяется автоматически (включено при наличии Tracy) - # типы параметров, которые никогда не следует автовайрить + # типы параметров, которые никогда не проходят autowiring excluded: ... # (string[]) - # разрешить ленивое создание сервисов? + # включить ленивое создание сервисов? lazy: ... # (bool) по умолчанию false # класс, от которого наследуется DI-контейнер @@ -108,18 +108,18 @@ di: Ленивые сервисы .{data-version:3.2.4} ------------------------------------- -Настройка `lazy: true` активирует ленивое (отложенное) создание сервисов. Это означает, что сервисы не создаются в момент, когда мы запрашиваем их из DI-контейнера, а только в момент их первого использования. Это может ускорить запуск приложения и снизить потребление памяти, поскольку создаются только те сервисы, которые действительно необходимы в данном запросе. +Установка `lazy: true` включает ленивое (отложенное) создание сервисов. Это значит, что сервисы на самом деле создаются не в момент запроса из DI-контейнера, а только при первом использовании. Это может ускорить старт приложения и снизить расход памяти, потому что создаются только те сервисы, которые действительно нужны для данного запроса. -Для конкретного сервиса ленивое создание можно [изменить |services#Ленивые сервисы]. +Для конкретного сервиса ленивое создание можно [настроить отдельно |services#Ленивые сервисы]. .[note] -Ленивые объекты можно использовать только для пользовательских классов, а не для внутренних классов PHP. Требуется PHP 8.4 или новее. +Ленивые объекты можно использовать только для пользовательских классов, но не для внутренних классов PHP. Требуется PHP 8.4 или новее. Экспорт метаданных ------------------ -Класс DI-контейнера также содержит много метаданных. Вы можете уменьшить его размер, сократив экспорт метаданных. +Класс DI-контейнера содержит также много метаданных. Вы можете уменьшить его размер, сократив экспорт метаданных. ```neon di: @@ -137,40 +137,40 @@ di: - Symfony\Component\Console\Application ``` -Если вы не используете массив `$container->getParameters()`, вы можете отключить экспорт параметров. Далее вы можете экспортировать только те теги, по которым вы получаете сервисы методом `$container->findByTag(...)`. Если вы вообще не вызываете этот метод, вы можете полностью отключить экспорт тегов с помощью `false`. +Если вы не используете `$container->getParameters()`, вы можете отключить экспорт параметров. Кроме того, вы можете экспортировать только те теги, которые действительно используете для получения сервисов через `$container->findByTag(...)`. Если вы этот метод вообще не вызываете, экспорт тегов можно полностью отключить значением `false`. -Вы можете значительно сократить метаданные для [autowiring |autowiring], указав классы, которые вы используете в качестве параметра метода `$container->getByType()`. И снова, если вы вообще не вызываете этот метод (или только в [bootstrap |application:bootstrapping] для получения `Nette\Application\Application`), вы можете полностью отключить экспорт с помощью `false`. +Метаданные для [autowiring|autowiring] можно заметно сократить, перечислив только те классы, которые вы действительно запрашиваете через `$container->getByType()`. Снова: если вы этот метод не вызываете (или вызываете только в файле [bootstrap|application:bootstrapping], например чтобы получить `Nette\Application\Application`), экспорт типов можно полностью отключить значением `false`. -Расширения +Extensions ========== -Регистрация дополнительных DI-расширений. Таким образом мы добавим, например, DI-расширение `Dibi\Bridges\Nette\DibiExtension22` под именем `dibi` +Регистрация дополнительных DI-расширений. Вот так вы добавите, например, DI-расширение `Dibi\Bridges\Nette\DibiExtension3` под именем `dibi`: ```neon extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -Затем мы конфигурируем его в секции `dibi`: +Затем вы настраиваете его в секции `dibi`: ```neon dibi: host: localhost ``` -В качестве расширения можно добавить и класс, у которого есть параметры: +Как расширение можно добавить и класс с параметрами: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` -Включение файлов -================ +Подключение файлов +================== -Другие файлы конфигурации можно включить в секции `includes`: +Дополнительные конфигурационные файлы можно подключить в секции `includes`: ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -Имя `parameters.php` — это не опечатка, конфигурация может быть записана и в PHP-файле, который вернет ее как массив: +Имя `parameters.php` - не опечатка: конфигурацию можно записать и в PHP-файле, который возвращает её массивом: ```php <?php @@ -192,15 +192,15 @@ return [ ]; ``` -Если в файлах конфигурации появляются элементы с одинаковыми ключами, они будут перезаписаны или, в случае [массивов, объединены |#Слияние]. Позже включенный файл имеет более высокий приоритет, чем предыдущий. Файл, в котором указана секция `includes`, имеет более высокий приоритет, чем включенные в нем файлы. +Если в нескольких конфигурационных файлах встречаются элементы с одинаковыми ключами, они будут перезаписаны или, в случае массивов, [объединены |#Слияние]. Файл, подключённый позже, имеет более высокий приоритет, чем предыдущий. Файл, в котором указана секция `includes`, имеет более высокий приоритет, чем подключённые в нём файлы. Search ====== -Автоматическое добавление сервисов в DI-контейнер чрезвычайно упрощает работу. Nette автоматически добавляет в контейнер презентеры, но можно легко добавлять и любые другие классы. +Автоматическая регистрация сервисов в DI-контейнере значительно упрощает разработку. Nette автоматически добавляет в контейнер презентеры, но вы легко можете добавить и любые другие классы. -Достаточно указать, в каких каталогах (и подкаталогах) следует искать классы: +Достаточно указать, в каких каталогах (и подкаталогах) искать классы: ```neon search: @@ -208,7 +208,14 @@ search: - in: %appDir%/Model ``` -Обычно, однако, мы не хотим добавлять абсолютно все классы и интерфейсы, поэтому их можно отфильтровать: +Если вам нужно всего одно правило поиска, список можно опустить и записать его ключи прямо под `search`: + +```neon +search: + in: %appDir% +``` + +Однако обычно мы не хотим добавлять совершенно все классы и интерфейсы, поэтому их можно отфильтровать: ```neon search: @@ -223,7 +230,7 @@ search: - *Factory ``` -Или мы можем выбирать классы, которые наследуют или реализуют хотя бы один из указанных классов: +Либо мы можем выбрать классы, которые наследуют или реализуют хотя бы один из перечисленных: ```neon @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -Можно определить и исключающие правила, т.е. маски имени класса или наследственных предков, которым если соответствует, сервис в DI-контейнер не добавляется: +Можно задать и правила исключения по маскам имён классов или по предкам. Если класс подходит под правило исключения, он не будет добавлен в DI-контейнер: ```neon search: @@ -247,7 +254,7 @@ search: implements: ... ``` -Всем сервисам можно установить теги: +Всем автоматически зарегистрированным сервисам можно назначить теги: ```neon search: @@ -255,11 +262,13 @@ search: tags: ... ``` +Помимо классов поиск регистрирует и интерфейсы с единственным методом `create()` или `get()` - как [генерируемые фабрики или аксессоры |factory]. Классы, для которых сервис того же типа уже зарегистрирован в контейнере, пропускаются, поэтому дубликаты не возникают. + Слияние ======= -Если в нескольких файлах конфигурации появляются элементы с одинаковыми ключами, они будут перезаписаны или, в случае массивов, объединены. Позже включенный файл имеет более высокий приоритет, чем предыдущий. +Если в нескольких конфигурационных файлах встречаются элементы с одинаковыми ключами, они будут перезаписаны или, в случае массивов, объединены. Подключённый позже файл имеет более высокий приоритет, чем предыдущий. <table class=table> <tr> @@ -292,7 +301,7 @@ items: </tr> </table> -Для массивов можно предотвратить слияние, указав восклицательный знак после имени ключа: +Для массивов слияние можно предотвратить, добавив после имени ключа восклицательный знак: <table class=table> <tr> diff --git a/dependency-injection/ru/container.texy b/dependency-injection/ru/container.texy index 0b734735ee..5e973b08e8 100644 --- a/dependency-injection/ru/container.texy +++ b/dependency-injection/ru/container.texy @@ -2,15 +2,15 @@ *********************** .[perex] -Dependency Injection контейнер (DIC) — это класс, который умеет инстанцировать и конфигурировать объекты. +Контейнер внедрения зависимостей (DIC или DI-контейнер) - объект, отвечающий за создание и настройку других объектов (называемых сервисами). -Возможно, вас это удивит, но во многих случаях вам не нужен dependency injection контейнер, чтобы использовать преимущества dependency injection (кратко DI). Ведь даже во [вводной главе|introduction] мы на конкретных примерах показали DI, и никакой контейнер не был нужен. +Возможно, вас это удивит, но во многих случаях, чтобы пользоваться преимуществами внедрения зависимостей (сокращённо DI), контейнер не нужен. Ведь даже во [вводной главе|introduction] мы показали конкретные примеры DI, и никакой контейнер там не понадобился. -Однако, если вам нужно управлять большим количеством различных объектов с множеством зависимостей, dependency injection container будет действительно полезен. Что, например, имеет место в веб-приложениях, построенных на фреймворке. +Однако когда приходится управлять большим количеством объектов со сложными зависимостями, DI-контейнер становится очень полезен. Обычно так и бывает у веб-приложений, построенных на фреймворке. -В предыдущей главе мы представили классы `Article` и `UserController`. Обе имеют некоторые зависимости, а именно базу данных и фабрику `ArticleFactory`. И для этих классов мы теперь создадим контейнер. Конечно, для такого простого примера нет смысла иметь контейнер. Но мы создадим его, чтобы показать, как он выглядит и работает. +В предыдущей главе мы познакомились с классами `Article` и `EditController`. У обоих есть зависимости, а именно база данных и фабрика `ArticleFactory`. Для этих классов мы сейчас и создадим контейнер. Разумеется, создавать контейнер для такого простого примера - перебор. Но мы создадим его, чтобы показать, как он выглядит и работает. -Вот простой жестко закодированный контейнер для приведенного примера: +Вот простой жёстко прописанный контейнер для приведённого примера: ```php class Container @@ -25,23 +25,23 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` -Использование выглядело бы следующим образом: +Использование выглядело бы так: ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -Мы просто запрашиваем у контейнера объект и больше не должны ничего знать о том, как его создать и какие у него зависимости; все это знает контейнер. Зависимости внедряются контейнером автоматически. В этом его сила. +Мы просто запрашиваем объект у контейнера, и нам не нужно знать, как его создать и какие у него зависимости; всем этим занимается контейнер. Зависимости контейнер внедряет автоматически. В этом его сила. -Контейнер пока что имеет все данные, записанные жестко. Сделаем следующий шаг и добавим параметры, чтобы контейнер стал действительно полезным: +Пока в контейнере все сведения прописаны жёстко. Поэтому сделаем следующий шаг и добавим параметры, чтобы контейнер стал по-настоящему полезным: ```php class Container @@ -70,9 +70,9 @@ $container = new Container([ ]); ``` -Проницательные читатели, возможно, заметили некоторую проблему. Каждый раз, когда я получаю объект `UserController`, также создается новый экземпляр `ArticleFactory` и базы данных. Этого мы определенно не хотим. +Внимательные читатели могли заметить проблему. Каждый раз, когда мы получаем объект `EditController`, создаются и новые экземпляры `ArticleFactory` и соединения с базой данных. Этого мы точно не хотим. -Поэтому добавим метод `getService()`, который будет возвращать одни и те же экземпляры: +Поэтому мы добавим метод `getService()`, который будет всегда возвращать одни и те же экземпляры: ```php class Container @@ -98,9 +98,9 @@ class Container } ``` -При первом вызове, например, `$container->getService('Database')`, он запросит у `createDatabase()` создание объекта базы данных, который сохранит в массиве `$services`, и при следующем вызове вернет его напрямую. +При первом вызове, например `$container->getService('Database')`, он вызывает `createDatabase()`, чтобы создать объект базы данных, сохраняет его в массив `$services` и возвращает. При последующих вызовах он сразу возвращает уже сохранённый экземпляр. -Изменим и остальную часть контейнера, чтобы он использовал `getService()`: +Изменим и остальную часть контейнера, чтобы она использовала `getService()`: ```php class Container @@ -112,16 +112,16 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -Кстати, термином сервис обозначается любой объект, управляемый контейнером. Поэтому и название метода `getService()`. +Кстати, термином "сервис" называют любой объект, управляемый контейнером. Отсюда и имя метода `getService()`. -Готово. У нас есть полнофункциональный DI-контейнер! И мы можем его использовать: +Готово. У нас полностью работающий DI-контейнер! И мы можем им пользоваться: ```php $container = new Container([ @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -Как видите, написать DIC несложно. Стоит напомнить, что сами объекты не знают, что их создает какой-то контейнер. Таким образом, можно таким образом создавать любой объект в PHP без вмешательства в его исходный код. +Как видите, написать DIC несложно. Стоит отметить, что сами объекты не знают о том, что их создаёт контейнер. Благодаря этому таким способом можно создавать любой объект PHP, не меняя его исходный код. -Ручное создание и поддержка класса контейнера может довольно быстро стать кошмаром. Поэтому в следующей главе мы поговорим о [Nette DI Container|nette-container], который умеет генерироваться и обновляться почти сам. +Создавать и поддерживать класс контейнера вручную может быстро превратиться в кошмар. Поэтому в следующей главе мы поговорим о [Nette DI Container|nette-container], который умеет порождать и обновлять себя практически автоматически. -{{maintitle: Что такое Dependency Injection контейнер?}} +{{maintitle: Что такое контейнер внедрения зависимостей?}} diff --git a/dependency-injection/ru/extensions.texy b/dependency-injection/ru/extensions.texy index 850b6672ca..1c8d894fec 100644 --- a/dependency-injection/ru/extensions.texy +++ b/dependency-injection/ru/extensions.texy @@ -2,38 +2,66 @@ ******************************** .[perex] -На генерацию DI-контейнера, помимо файлов конфигурации, влияют так называемые *расширения*. Мы активируем их в файле конфигурации в секции `extensions`. +Расширение - это класс, который подключается к компиляции DI-контейнера. Он может программно регистрировать сервисы, проверять собственную секцию конфигурации, изменять сервисы, определённые другими, и даже править порождённый код контейнера. Эта страница научит вас писать такое расширение, объяснит, что и когда происходит и на что стоит обратить внимание. -Таким образом мы добавим расширение, представленное классом `BlogExtension`, под именем `blog`: +Расширения - это тот способ, которым пакеты встраиваются в Nette по-родному: их используют все пакеты `nette/*`, и ваш может тоже. Типичное расширение делает одно или несколько из этого: + +- **интегрирует библиотеку** - регистрирует её сервисы в контейнере и предоставляет удобную проверяемую секцию конфигурации (именно оттуда берутся секции `mail:` или `database:`) +- **автоматизирует регистрацию** - регистрирует множество похожих сервисов в цикле или по правилу, когда перечислять их в `services:` было бы утомительно +- **вносит сквозные изменения** - находит сервисы, зарегистрированные другими, и дополняет их, например прикрепляет логгер к каждому сервису с определённым тегом + +В повседневной работе над приложением расширение нужно редко: секция [services |services] конфигурации покрывает регистрацию и связывание ваших классов. Беритесь за расширение тогда, когда одной конфигурации перестаёт хватать. + +Расширение включается в секции `extensions`. Вот так вы добавите расширение, представленное классом `BlogExtension`, под именем `blog`: ```neon extensions: blog: BlogExtension ``` -Каждое расширение компилятора наследуется от [api:Nette\DI\CompilerExtension] и может реализовывать следующие методы, которые последовательно вызываются во время сборки DI-контейнера: +Если его конструктор принимает аргументы, передайте их прямо там: + +```neon +extensions: + blog: BlogExtension(%debugMode%) +``` + + +Как работает компиляция +======================= + +Чтобы уверенно писать расширения, вам нужно знать одну ключевую вещь: **когда выполняется ваш код.** Nette не связывает сервисы во время обработки запросов. Вместо этого он *компилирует* контейнер заранее: читает все конфигурационные файлы, даёт расширениям сделать свою работу и порождает оптимизированный PHP-класс, который сохраняет на диск. Каждый следующий запрос лишь загружает этот готовый класс. Поэтому код вашего расширения выполняется только тогда, когда контейнер (пере)собирается, а не при каждом запросе. + +Отсюда важное следствие: во время компиляции сервисов ещё не существует. Существуют **определения** - рецепты, описывающие, какого класса будет каждый сервис, как его создать и что у него потом вызвать. Определения живут в объекте [ContainerBuilder |#ContainerBuilder]. Расширение - это, по сути, *конфигурация с возможностью писать код*: всё, что можно объявить в секции `services:`, можно собрать и на PHP - по условию, в цикле или в ответ на то, что зарегистрировали другие. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +Компиляция идёт фазами, и расширение может вступить в каждую из них: +1) проверяются секции конфигурации всех расширений (`getConfigSchema()`) +2) каждое расширение регистрирует свои сервисы (`loadConfiguration()`); пользовательская секция `services:` обрабатывается последней, поэтому последнее слово всегда за приложением +3) когда все определения на месте и типы сервисов разрешены, расширения могут их изменить (`beforeCompile()`) +4) порождается класс контейнера; расширения ещё могут поправить его код (`afterCompile()`) и добавить код, который выполнится при старте приложения ([инициализация |#Код инициализации]) -getConfigSchema() .[method] -=========================== +.[note] +В режиме разработки контейнер автоматически перекомпилируется при изменении конфигурационного файла или самого класса расширения: и то, и другое отслеживается как зависимость. Так что вы можете разрабатывать расширения, ни разу не очищая кеш. -Этот метод вызывается первым. Он определяет схему для валидации конфигурационных параметров. +.[tip] +Чтобы глубже разобраться, что происходит в каждой фазе - когда разворачиваются параметры, когда `@service` становится ссылкой и когда именно безопасно искать сервисы по типу, - см. [Компиляцию в подробностях |compilation-internals]. -Расширение конфигурируется в секции, имя которой совпадает с тем, под которым было добавлено расширение, то есть `blog`: + +Первое расширение +================= + +Вот небольшое, но полноценное расширение. Мы включаем и настраиваем его в одном файле: ```neon -# то же имя, что и у расширения +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Создадим схему, описывающую все опции конфигурации, включая их типы, допустимые значения и, возможно, значения по умолчанию: +А вот и весь класс: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -Документацию можно найти на странице [Schema |schema:]. Кроме того, можно указать, какие опции могут быть [динамическими |application:bootstrapping#Динамические параметры] с помощью `dynamic()`, например, `Expect::int()->dynamic()`. -К конфигурации мы получаем доступ через переменную `$this->config`, которая является объектом `stdClass`: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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`, поэтому сервисы разных расширений не могут столкнуться. -loadConfiguration() .[method] -============================= +А последние несколько строк показывают, зачем расширения вообще нужны: сервис `comments` регистрируется только тогда, когда комментарии включены. Обычный конфигурационный файл таких решений принимать не может. + +Зарегистрированные так сервисы ведут себя ровно так же, как если бы были записаны в `services:`: они создаются лениво по требованию, а autowiring передаёт их везде, где объявлен тип `Blog\Articles`. + +Следующие главы подробно описывают жизненный цикл расширения, затем API [ContainerBuilder |#ContainerBuilder], которым вы будете пользоваться внутри расширения, и наконец [подводные камни |#Советы и подводные камни], о которых стоит знать. + + +Жизненный цикл расширения +========================= + +Расширение наследует от [api:Nette\DI\CompilerExtension] и переопределяет некоторые из четырёх методов `getConfigSchema()`, `loadConfiguration()`, `beforeCompile()` и `afterCompile()`, которые компилятор вызывает в этом порядке во время компиляции. -Используется для добавления сервисов в контейнер. Для этого служит [api:Nette\DI\ContainerBuilder]: + +getConfigSchema(): Nette\Schema\Schema .[method] +------------------------------------------------ + +Определяет схему секции конфигурации расширения. Благодаря ей пользователи бесплатно получают проверку и понятные сообщения об ошибках: опечатка или неверный тип в секции `blog:` сопровождается вразумительным сообщением, а вы не пишете ни одной проверки. + +Схема описывается с помощью библиотеки [Schema |schema:] и может выражать типы, значения по умолчанию, допустимые значения и многое другое: ```php -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // или setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -Конвенция заключается в том, чтобы префиксировать сервисы, добавленные расширением, его именем, чтобы избежать конфликтов имен. Это делает метод `prefix()`, так что если расширение называется `blog`, сервис будет носить имя `blog.articles`. +Проверенная конфигурация доступна в `$this->config` как объект `stdClass` (или как массив, если добавить к схеме `castTo('array')`). + +Если значение параметра нельзя знать во время компиляции - например, оно приходит из переменной окружения, - пометьте его через `dynamic()`, например `Expect::int()->dynamic()`. Подробнее в разделе [динамические параметры |application:bootstrapping#Динамические параметры]. + -Если нам нужно переименовать сервис, мы можем для сохранения обратной совместимости создать псевдоним с исходным именем. Аналогично Nette делает, например, для сервиса `routing.router`, который доступен и под прежним именем `router`. +loadConfiguration() .[method] +----------------------------- + +Место, где расширение регистрирует свои сервисы с помощью [ContainerBuilder |#ContainerBuilder]: ```php -$builder->addAlias('router', 'routing.router'); +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')); +} +``` -Сервисы можно создавать не только с помощью API класса ContainerBuilder, но и знакомой записью, используемой в файле конфигурации NEON в секции services. Префикс `@extension` представляет текущее расширение. +Когда сервисов много, удобнее бывает определить их в отдельном файле NEON привычным синтаксисом [services |services]. Префикс `@extension` отсылает к текущему расширению: ```neon services: @@ -107,88 +160,284 @@ services: comments: create: MyBlog\CommentsModel(@connection, @extension.articles) +``` - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +Эти определения мы загружаем через `loadDefinitionsFromConfig()`; имена получают префикс автоматически, а файл отслеживается как зависимость, поэтому его изменение вызывает перекомпиляцию: + +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Сервисы загрузим: + +beforeCompile() .[method] +------------------------- + +Когда вызывается этот метод, в билдере уже есть **все** определения: ваши, других расширений и из пользовательских конфигурационных файлов. Типы сервисов тоже разрешены, поэтому поиск по типу надёжен. Благодаря этому данная фаза идеальна для исследования и дополнения окончательного графа сервисов. + +Обычно вы ищете сервисы по тегу или по типу и дополняете найденные определения: ```php -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // загрузка файла конфигурации для расширения - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + foreach ($builder->findByTag('logaware') as $name => $attrs) { + $builder->getDefinition($name)->addSetup('setLogger'); } } ``` +У вызова `setLogger()` нет явных аргументов: их подставит autowiring, как и в фабриках. -beforeCompile() .[method] -========================= +Вы можете сотрудничать и с другими зарегистрированными расширениями, полученными через `$this->compiler->getExtensions()`, при желании отфильтрованными по классу или интерфейсу: -Метод вызывается в момент, когда контейнер содержит все сервисы, добавленные отдельными расширениями в методах `loadConfiguration`, а также пользовательскими файлами конфигурации. На этом этапе сборки мы можем изменять определения сервисов или дополнять связи между ними. Для поиска сервисов в контейнере по тегам можно использовать метод `findByTag()`, по классу или интерфейсу — метод `findByType()`. +```php +foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) { + // ... +} +``` + + +afterCompile(Nette\PhpGenerator\ClassType $class) .[method] +----------------------------------------------------------- + +В последней фазе класс контейнера порождается как объект [ClassType |php-generator:#Классы] библиотеки [PHP Generator |php-generator:]. Он содержит фабричный метод для каждого сервиса и вот-вот будет записан в кеш. Вы ещё можете изменить его код: ```php -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +Эта фаза понадобится вам лишь изредка. Чтобы добавить код, выполняющийся при старте приложения, используйте вместо этого инициализацию: + + +Код инициализации +----------------- + +Все предыдущие фазы влияют на то, как контейнер *собирается*. Кроме того, расширение может выдать код, который выполняется *во время выполнения*, сразу после создания контейнера, например чтобы стартовать сессию или запустить сервисы. Код записывается в объект `$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()`: -afterCompile() .[method] +```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'); +``` + + +Советы и подводные камни ======================== -На этом этапе класс контейнера уже сгенерирован в виде объекта [ClassType |php-generator:#Классы], содержит все методы, которые создают сервисы, и готов к записи в кеш. Результирующий код класса мы можем на этом этапе еще изменить. + +Время компиляции и время выполнения +----------------------------------- + +Самый частый источник путаницы: код расширения выполняется тогда, когда контейнер **компилируется**, а не когда приложение обрабатывает запросы. На практике это значит: + +- Расширение никогда не работает с экземплярами сервисов: их ещё нет. Не создавайте сервисы через `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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Типичный случай применения - регистрация панели Tracy только в режиме разработки. -$initialization .[method] -========================= -Класс Configurator после [создания контейнера |application:bootstrapping#index.php] вызывает инициализационный код, который создается записью в объект `$this->initialization` с помощью [метода addBody() |php-generator:#Тела методов и функций]. +Сложные аргументы +----------------- + +Иногда аргумент фабрики или вызова setup - не обычное значение, не имя класса и не ссылка `@service`. Для таких случаев есть: + +- `new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args])` - объект, создаваемый на месте, "анонимный сервис", используемый как аргумент +- `new Nette\DI\Definitions\Reference('blog.articles')` - ссылка на сервис, объектный аналог строки `@name` +- `$builder::literal('PHP_SAPI')` - кусок сырого PHP-кода, вставляемый в порождённый контейнер как есть -Покажем пример, как, например, инициализационным кодом запустить сессию или запустить сервисы, имеющие тег `run`: +Пример - регистрация панели Tracy: ```php -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // автоматический запуск сессии - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // этот тег будет экспортирован всегда, даже при ограниченном экспорте + $this->compiler->addExportedTag('event.subscriber'); - // сервисы с тегом run должны быть созданы после инстанцирования контейнера - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // этот тип будет всегда доступен для 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 index 80938ff1d6..dffdf7fc06 100644 --- a/dependency-injection/ru/factory.texy +++ b/dependency-injection/ru/factory.texy @@ -2,9 +2,9 @@ ******************** .[perex] -Nette DI умеет автоматически генерировать код фабрик на основе интерфейсов, что экономит вам написание кода. +Nette DI умеет автоматически порождать код фабрик на основе интерфейсов, избавляя вас от написания кода. -Фабрика — это класс, который производит и конфигурирует объекты. Следовательно, он передает им и их зависимости. Пожалуйста, не путайте с паттерном проектирования *factory method*, который описывает специфический способ использования фабрик и не связан с этой темой. +Фабрика - это класс, отвечающий за создание объектов и передачу их зависимостей. Не путайте это с шаблоном проектирования *factory method*, который описывает конкретный способ использования фабрик и с этой темой не связан. Как выглядит такая фабрика, мы показали во [вводной главе |introduction#Фабрика]: @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DI умеет автоматически генерировать код фабрик. Все, что вам нужно сделать, — это создать интерфейс, и Nette DI сгенерирует реализацию. Интерфейс должен иметь ровно один метод с именем `create` и объявлять возвращаемый тип: +Nette DI умеет порождать код фабрики автоматически. Вам достаточно создать интерфейс, а реализацию сгенерирует Nette DI. У интерфейса должен быть ровно один метод с именем `create` и объявленным типом возвращаемого значения: ```php interface ArticleFactory @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -То есть фабрика `ArticleFactory` имеет метод `create`, который создает объекты `Article`. Класс `Article` может выглядеть, например, следующим образом: +Итак, у фабрики `ArticleFactory` есть метод `create`, создающий объекты `Article`. Класс `Article` может выглядеть, например, так: ```php class Article @@ -44,7 +44,7 @@ class Article } ``` -Фабрику добавим в файл конфигурации: +Добавьте фабрику в конфигурационный файл: ```neon services: @@ -53,7 +53,7 @@ services: Nette DI сгенерирует соответствующую реализацию фабрики. -В коде, который использует фабрику, мы запросим объект по интерфейсу, и Nette DI использует сгенерированную реализацию: +В коде, который использует фабрику, запрашивайте объект по его интерфейсу, и Nette DI предоставит порождённую реализацию: ```php class UserController @@ -65,17 +65,17 @@ class UserController public function foo() { - // позволяем фабрике создать объект + // пусть фабрика создаст объект $article = $this->articleFactory->create(); } } ``` -Параметризованная фабрика -========================= +Фабрика с параметрами +===================== -Фабричный метод `create` может принимать параметры, которые затем передаст в конструктор. Дополним, например, класс `Article` ID автора статьи: +Метод фабрики `create` может принимать параметры, которые он затем передаёт в конструктор. Например, добавим в класс `Article` идентификатор автора статьи: ```php class Article @@ -88,7 +88,7 @@ class Article } ``` -Параметр добавим также в фабрику: +Добавим параметр и в фабрику: ```php interface ArticleFactory @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -Благодаря тому, что параметр в конструкторе и параметр в фабрике называются одинаково, Nette DI их совершенно автоматически передаст. +Поскольку имя параметра в конструкторе (`$authorId`) совпадает с именем параметра в методе фабрики, Nette DI передаёт его автоматически. Расширенное определение ======================= -Определение можно записать и в многострочном виде с использованием ключа `implement`: +Определение можно записать и в многострочной форме с помощью ключа `implement`: ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -При записи этим более длинным способом можно указать дополнительные аргументы для конструктора в ключе `arguments` и дополнительную конфигурацию с помощью `setup`, так же, как у обычных сервисов. +Такая более длинная форма позволяет указать дополнительные аргументы конструктора через ключ `arguments` и дальнейшую настройку через `setup`, как у обычных определений сервисов. -Пример: если бы метод `create()` не принимал параметр `$authorId`, мы могли бы указать фиксированное значение в конфигурации, которое передавалось бы в конструктор `Article`: +Пример: если бы метод `create()` не принимал параметр `$authorId`, мы могли бы задать в конфигурации фиксированное значение, передаваемое в конструктор `Article`: ```neon services: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -Или наоборот, если бы `create()` параметр `$authorId` принимал, но он не был бы частью конструктора и передавался бы методом `Article::setAuthorId()`, мы бы сослались на него в секции `setup`: +И наоборот, если бы `create()` принимал `$authorId`, но тот не входил бы в конструктор, а передавался бы методом вроде `Article::setAuthorId()`, мы сослались бы на параметр в секции `setup`: ```neon services: @@ -134,14 +134,14 @@ services: ``` -Accessor +Аксессор ======== -Nette умеет кроме фабрик генерировать и так называемые accessory. Это объекты с методом `get()`, который возвращает определенный сервис из DI-контейнера. Повторный вызов `get()` возвращает все тот же экземпляр. +Помимо фабрик Nette умеет порождать и так называемые аксессоры. Это объекты с методом `get()`, возвращающим определённый сервис из DI-контейнера. Повторные вызовы `get()` всегда возвращают один и тот же экземпляр. -Accessor предоставляют зависимостям lazy-loading. Представим класс, который записывает ошибки в специальную базу данных. Если бы этот класс получал подключение к базе данных как зависимость через конструктор, подключение всегда должно было бы создаваться, хотя на практике ошибка возникает лишь изредка, и, следовательно, в большинстве случаев соединение оставалось бы неиспользованным. Вместо этого класс передаст себе accessor, и только когда будет вызван его `get()`, произойдет создание объекта базы данных: +Аксессоры дают отложенную загрузку зависимостей. Представьте класс, который записывает ошибки в отдельную базу данных. Если бы этот класс получал соединение с базой через внедрение в конструктор, соединение устанавливалось бы всегда, даже если ошибки случаются редко и соединение почти всё время не используется. Вместо этого класс может получить аксессор. Объект базы данных (соединение) создаётся только тогда, когда метод `get()` аксессора вызывается впервые. -Как создать accessor? Достаточно написать интерфейс, и Nette DI сгенерирует реализацию. Интерфейс должен иметь ровно один метод с именем `get` и объявлять возвращаемый тип: +Как создать аксессор? Достаточно написать интерфейс, а реализацию сгенерирует Nette DI. У интерфейса должен быть ровно один метод с именем `get`, без параметров и с объявленным типом возвращаемого значения: ```php interface PDOAccessor @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -Accessor добавим в файл конфигурации, где также находится определение сервиса, который он будет возвращать: +Добавьте аксессор в конфигурационный файл вместе с определением сервиса, который он должен возвращать: ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -Поскольку accessor возвращает сервис типа `PDO`, а в конфигурации есть единственный такой сервис, он будет возвращать именно его. Если бы сервисов данного типа было больше, мы бы определили возвращаемый сервис с помощью имени, например, `- PDOAccessor(@db1)`. +Поскольку аксессор возвращает сервис `PDO`, а такой сервис в конфигурации определён только один, аксессор вернёт именно его. Если сервисов такого типа несколько, укажите по имени, какой из них должен вернуть аксессор, например `- PDOAccessor(@db1)`. -Множественная фабрика/аксессор +Мультифабрика и мультиаксессор ============================== -Наши фабрики и accessory до сих пор умели всегда производить или возвращать только один объект. Но можно очень легко создать и множественные фабрики, комбинированные с accessory. Интерфейс такого класса будет содержать любое количество методов с именами `create<name>()` и `get<name>()`, например: + +До сих пор наши фабрики и аксессоры умели создавать или возвращать только один тип объектов. Однако вы легко можете создать мультифабрики, сочетающие возможности фабрик и аксессоров. Интерфейс такой составляющей может содержать несколько методов с именами `create<Name>()` и `get<Name>()`, например: ```php interface MultiFactory @@ -173,9 +174,9 @@ interface MultiFactory } ``` -Так что вместо того, чтобы передавать себе несколько сгенерированных фабрик и accessory, мы передадим одну более комплексную фабрику, которая умеет больше. +Так что вместо внедрения нескольких отдельных фабрик и аксессоров вы можете внедрить одну более всеобъемлющую составляющую. -Альтернативно, вместо нескольких методов можно использовать `get()` с параметром: +Как вариант, вместо нескольких методов можно использовать `get()` с параметром: ```php interface MultiFactoryAlt @@ -184,43 +185,49 @@ interface MultiFactoryAlt } ``` -Тогда верно, что `MultiFactory::getArticle()` делает то же самое, что и `MultiFactoryAlt::get('article')`. Однако альтернативная запись имеет тот недостаток, что неясно, какие значения `$name` поддерживаются, и логически также нельзя в интерфейсе различить разные возвращаемые значения для разных `$name`. +Тогда `MultiFactory::getDb()` делает то же самое, что `MultiFactoryAlt::get('db')`. Однако у этой альтернативной записи есть недостаток: из сигнатуры интерфейса не видно явно, какие значения `$name` поддерживаются. Кроме того, в интерфейсе нельзя задать разные типы возвращаемых значений для разных значений `$name`. + +Вместо `get($name)` интерфейс может объявить `create($name)`, который при каждом вызове возвращает новый экземпляр (тогда как `get()` возвращает общий). В интерфейсе может быть только один такой метод с параметром. Если тип возвращаемого значения метода nullable (например, `?PDO`), для неизвестного `$name` он возвращает `null` вместо выбрасывания исключения. Определение списком ------------------- -Таким образом можно определить множественную фабрику в конфигурации: .{data-version:3.2.0} +Мультифабрику можно определить в конфигурации списком, записав сервисы прямо в нём: .{data-version:3.2.0} ```neon services: - MultiFactory( - article: Article # определяет createArticle() - db: PDO(%dsn%, %user%, %password%) # определяет getDb() + article: Article() # задаёт createArticle() + db: PDO(%dsn%, %user%, %password%) # задаёт getDb() ) ``` -Или мы можем в определении фабрики сослаться на существующие сервисы с помощью ссылки: +Как вариант, в определении мультифабрики можно сослаться на существующие сервисы через ссылки: ```neon services: article: Article - PDO(%dsn%, %user%, %password%) - MultiFactory( - article: @article # определяет createArticle() - db: @\PDO # определяет getDb() + article: @article # задаёт createArticle() + db: @\PDO # задаёт getDb() ) ``` -Определение с помощью тегов ---------------------------- +Определение тегами +------------------ -Второй возможностью является использование для определения [тегов |services#Теги]: +Ещё один способ определить мультифабрику - использовать [теги |services#Теги]. Значение тега определяет имя соответствующего метода: ```neon services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index fc535a614f..ce5e8ae1a7 100644 --- a/dependency-injection/ru/faq.texy +++ b/dependency-injection/ru/faq.texy @@ -2,93 +2,93 @@ *********************************** -Является ли DI другим названием для IoC? ----------------------------------------- +DI - это другое название IoC? +----------------------------- -*Inversion of Control* (IoC) — это принцип, ориентированный на способ запуска кода — запускает ли ваш код чужой код, или ваш код интегрирован в чужой, который его затем вызывает. IoC — это широкий термин, включающий [события |nette:glossary#События Events], так называемый [Голливудский принцип |application:components#Стиль Голливуда] и другие аспекты. Частью этой концепции являются и фабрики, о которых говорит [Правило № 3: пусть это сделает фабрика |introduction#Правило 3: оставь это фабрике], и которые представляют собой инверсию для оператора `new`. +*Inversion of Control* (IoC) - принцип, описывающий поток управления в программе: ваш код вызывает внешний код или внешний код (например, фреймворк) вызывает ваш? IoC - широкое понятие, охватывающее [события |nette:glossary#События], так называемый [голливудский принцип |application:components#Голливудский стиль] и другие аспекты. К этому понятию относятся и фабрики, о которых говорится в разделе [Правило № 3: пусть этим займётся фабрика |introduction#Правило № 3: пусть этим займётся фабрика], представляющие собой инверсию оператора `new`. -*Dependency Injection* (DI) фокусируется на способе, которым один объект узнает о другом объекте, то есть о его зависимостях. Это паттерн проектирования, который требует явной передачи зависимостей между объектами. +*Dependency Injection* (DI) сосредоточен на том, как объекты получают свои зависимости (то есть другие объекты, с которыми им нужно работать). Это шаблон проектирования, призывающий передавать зависимости объектам явно, а не заставлять объекты создавать или разыскивать их. -Таким образом, можно сказать, что DI является специфической формой IoC. Однако не все формы IoC подходят с точки зрения чистоты кода. Например, к антипаттернам относятся техники, которые работают с [глобальным состоянием |global-state] или так называемый [Service Locator |#Что такое Service Locator]. +Поэтому DI можно считать частным случаем IoC. Однако не все формы IoC способствуют чистоте кода. Например, антипаттернами являются приёмы, опирающиеся на [глобальное состояние|global-state] или на шаблон [Service Locator |#Что такое Service Locator?]. Что такое Service Locator? -------------------------- -Это альтернатива Dependency Injection. Он работает так, что создает центральное хранилище, где зарегистрированы все доступные сервисы или зависимости. Когда объекту нужна зависимость, он запрашивает ее у Service Locator. +Это альтернативный подход к Dependency Injection. Он состоит в том, что есть центральный объект (локатор), в котором зарегистрированы все доступные сервисы (зависимости). Когда объекту нужна зависимость, он запрашивает её у Service Locator. -Однако по сравнению с Dependency Injection он теряет в прозрачности: зависимости не передаются объектам напрямую и не так легко идентифицируются, что требует изучения кода для выявления и понимания всех связей. Тестирование также сложнее, потому что мы не можем просто передавать mock-объекты тестируемым объектам, а должны делать это через Service Locator. Кроме того, Service Locator нарушает дизайн кода, поскольку отдельные объекты должны знать о его существовании, что отличается от Dependency Injection, где объекты не имеют представления о DI-контейнере. +Однако по сравнению с DI ему не хватает прозрачности. Зависимости спрятаны внутри кода объекта (в вызовах локатора), а не выражены явно в его API (в конструкторе или методах), поэтому, чтобы разобраться в связях, приходится читать код. Тестирование тоже усложняется: вы не можете просто передать mock-зависимости при создании объекта, часто приходится вмешиваться в сам Service Locator. Кроме того, Service Locator вносит лишнюю зависимость: объекты становятся связаны с локатором, тогда как при DI объекты в идеале о контейнере ничего не знают. Когда лучше не использовать DI? ------------------------------- -Неизвестны никакие трудности, связанные с использованием паттерна проектирования Dependency Injection. Напротив, получение зависимостей из глобально доступных мест приводит к [целому ряду осложнений |global-state], так же как и использование Service Locator. Поэтому целесообразно использовать DI всегда. Это не догматический подход, а просто не была найдена лучшая альтернатива. +Известных существенных недостатков правильного использования шаблона Dependency Injection нет. Наоборот, получение зависимостей из глобально доступных мест (таких как статические свойства или синглтоны) приводит к [множеству осложнений|global-state], как и использование Service Locator. Поэтому использовать DI, как правило, всегда уместно. Это не догма; просто ничего лучшего для чистого управления зависимостями пока широко не прижилось. -Тем не менее, существуют определенные ситуации, когда мы не передаем объекты, а получаем их из глобального пространства. Например, при отладке кода, когда нужно в конкретной точке программы вывести значение переменной, измерить продолжительность определенной части программы или записать сообщение. В таких случаях, когда речь идет о временных действиях, которые позже будут удалены из кода, легитимно использовать глобально доступный дампер, секундомер или логгер. Эти инструменты не относятся к дизайну кода. +Однако есть отдельные, ограниченные ситуации, когда глобальное обращение к объектам может быть допустимо. Например, при отладке, когда нужно вывести значение переменной, измерить время выполнения или записать сообщение в определённом месте. В этих случаях, связанных с временными действиями, которые позже уберут из кода, использование глобально доступного дампера, таймера или логгера может быть законным. Эти инструменты не входят в основную архитектуру приложения. -Есть ли у использования DI недостатки? --------------------------------------- +Есть ли у DI недостатки? +------------------------ -Влечет ли использование Dependency Injection какие-либо недостатки, такие как повышенная трудоемкость написания кода или ухудшение производительности? Что мы теряем, когда начинаем писать код в соответствии с DI? +Влечёт ли использование Dependency Injection недостатки вроде увеличения объёма кода или снижения производительности? Что мы теряем, начиная писать код в соответствии с DI? -DI не влияет на производительность или потребление памяти приложения. Определенную роль может играть производительность DI Container, однако в случае [Nette DI |nette-container] контейнер компилируется в чистый PHP, так что его накладные расходы во время выполнения приложения практически нулевые. +Сам DI пренебрежимо влияет на производительность во время выполнения и на расход памяти. Производительность DI-контейнера может иметь значение, но [Nette DI |nette-container] компилирует контейнер в обычный PHP-код, поэтому накладные расходы при выполнении приложения практически нулевые. -При написании кода обычно необходимо создавать конструкторы, принимающие зависимости. Раньше это могло быть утомительно, однако благодаря современным IDE и [constructor property promotion |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] это теперь вопрос нескольких секунд. Фабрики можно легко генерировать с помощью Nette DI и плагина для PhpStorm щелчком мыши. С другой стороны, отпадает необходимость писать синглтоны и статические точки доступа. +Когда вы пишете код по принципам 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 обычно не заметно короче и не заметно длиннее того, что опирается на синглтоны или глобальный доступ. Код, отвечающий за создание и связывание зависимостей, просто переезжает из отдельных классов в специально отведённые места: конфигурацию DI-контейнера и фабрики. -Как переписать устаревшее приложение на DI? -------------------------------------------- +Как переписать старое приложение на DI? +--------------------------------------- -Переход с устаревшего приложения на Dependency Injection может быть сложным процессом, особенно для больших и комплексных приложений. Важно подходить к этому процессу систематически. +Переход старого приложения на Dependency Injection может оказаться непростым, особенно для больших и сложных приложений. Важно подойти к этому процессу систематически. -- При переходе на Dependency Injection важно, чтобы все члены команды понимали принципы и процедуры, которые используются. -- Сначала проведите анализ существующего приложения и определите ключевые компоненты и их зависимости. Создайте план, какие части будут рефакторены и в каком порядке. -- Реализуйте DI-контейнер или, еще лучше, используйте существующую библиотеку, например, Nette DI. -- Постепенно рефакторьте отдельные части приложения, чтобы они использовали Dependency Injection. Это может включать изменения конструкторов или методов так, чтобы они принимали зависимости в качестве параметров. -- Измените места в коде, где создаются объекты с зависимостями, чтобы вместо этого зависимости внедрялись контейнером. Это может включать использование фабрик. +- При переходе на Dependency Injection важно, чтобы все члены команды понимали используемые принципы и практики. +- Сначала проанализируйте существующее приложение, чтобы выявить ключевые составляющие и их зависимости. Составьте план, какие части и в каком порядке будут переработаны. +- Реализуйте DI-контейнер или, что лучше, используйте существующую библиотеку, например Nette DI. +- Постепенно переводите части приложения на Dependency Injection. Это может означать изменение конструкторов или методов так, чтобы они принимали зависимости параметрами. +- Обновите код в местах создания объектов, чтобы получать их из контейнера или использовать фабрики, предоставляемые контейнером. -Помните, что переход на Dependency Injection — это инвестиция в качество кода и долгосрочную поддерживаемость приложения. Хотя может быть сложно внести эти изменения, результатом должен стать более чистый, модульный и легко тестируемый код, готовый к будущему расширению и обслуживанию. +Помните, что переход на Dependency Injection - это вложение в качество кода и долгосрочную поддерживаемость приложения. Внести эти изменения может быть непросто, но результатом должен стать более чистый, модульный и легко тестируемый код, готовый к будущим расширениям и поддержке. Почему композиция предпочтительнее наследования? ------------------------------------------------ -Предпочтительнее использовать [композицию |nette:introduction-to-object-oriented-programming#Композиция] вместо [наследования |nette:introduction-to-object-oriented-programming#Наследование], потому что она служит для повторного использования кода, не заботясь о последствиях изменений. Она обеспечивает более слабую связь, когда нам не нужно беспокоиться, что изменение какого-либо кода вызовет необходимость изменения другого зависимого кода. Типичным примером является ситуация, называемая [constructor hell |passing-dependencies#Ад конструкторов]. +Для переиспользования кода [композиция |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, но спроектирован как самостоятельная библиотека, которую можно использовать независимо от других частей фреймворка. Достаточно установить её через Composer, создать конфигурационный файл с описанием ваших сервисов, а затем несколькими строками PHP-кода создать DI-контейнер. И вы сразу можете начать пользоваться преимуществами Dependency Injection в своих проектах. -Как выглядит конкретное использование, включая код, описывает глава [Nette DI Container |nette-container]. +В главе [Nette DI Container |nette-container] описан конкретный сценарий использования с примерами кода. -Почему конфигурация находится в NEON-файлах? --------------------------------------------- +Почему конфигурация в файлах NEON? +---------------------------------- -NEON — это простой и легко читаемый язык конфигурации, который был разработан в рамках Nette для настройки приложений, сервисов и их зависимостей. По сравнению с JSON или YAML он предлагает для этой цели гораздо более интуитивные и гибкие возможности. В NEON можно естественно описать связи, которые в Symfony & YAML было бы невозможно записать либо вообще, либо только посредством сложного описания. +NEON - простой и легко читаемый язык конфигурации, разработанный внутри Nette для настройки приложений, сервисов и их зависимостей. По сравнению с JSON или YAML он даёт для этой цели куда более интуитивные и гибкие возможности. В NEON можно естественно описать определения сервисов и связи, которые в JSON или YAML выразить так же наглядно было бы трудно или невозможно. -Не замедляет ли приложение парсинг NEON-файлов? ------------------------------------------------ +Не замедляет ли разбор файлов NEON приложение? +---------------------------------------------- -Хотя файлы NEON парсятся очень быстро, этот аспект вообще не имеет значения. Причина в том, что парсинг файлов происходит только один раз при первом запуске приложения. Затем генерируется код DI-контейнера, сохраняется на диск и запускается при каждом следующем запросе, без необходимости выполнять дополнительный парсинг. +Хотя файлы NEON разбираются очень быстро, скорость их разбора в продакшене по большому счёту не важна. Дело в том, что конфигурационные файлы разбираются только один раз, при первом запуске приложения (или при их изменении). После разбора порождается код DI-контейнера, он кешируется (сохраняется на диск), и при каждом последующем запросе выполняется уже этот скомпилированный PHP-код, так что повторный разбор не нужен. -Так это работает в производственной среде. Во время разработки NEON-файлы парсятся каждый раз, когда происходит изменение их содержимого, чтобы разработчик всегда имел актуальный DI-контейнер. Сам парсинг, как было сказано, — вопрос мгновения. +Так это работает в производственной среде. Во время разработки файлы NEON разбираются каждый раз при изменении их содержимого, благодаря чему у разработчика всегда актуальный DI-контейнер. Как уже сказано, сам разбор происходит очень быстро. -Как получить доступ к параметрам в файле конфигурации из моего класса? +Как обратиться в своём классе к параметрам из конфигурационного файла? ---------------------------------------------------------------------- -Будем помнить [Правило № 1: пусть тебе это передадут |introduction#Правило 1: пусть тебе это передадут]. Если класс требует информацию из файла конфигурации, нам не нужно думать, как к этой информации добраться, вместо этого мы просто запросим ее — например, через конструктор класса. А передачу осуществим в файле конфигурации. +Помните [Правило № 1: пусть вам это передадут |introduction#Правило № 1: пусть вам это передадут]. Если классу нужны сведения из конфигурационного файла, не пытайтесь придумать, как класс может их *раздобыть*. Вместо этого просто запросите их, например через конструктор класса. А затем передайте это значение в конфигурационном файле. -В этом примере `%myParameter%` является заполнителем для значения параметра `myParameter`, который передается в конструктор класса `MyClass`: +В этом примере `%myParameter%` - подстановка значения параметра `myParameter`, которое будет передано в конструктор `MyClass`: -```php +```neon # config.neon parameters: myParameter: Some value @@ -97,10 +97,27 @@ services: - MyClass(%myParameter%) ``` -Чтобы передавать несколько параметров или использовать autowiring, целесообразно [параметры упаковать в объект |best-practices:passing-settings-to-presenters]. +Если вы хотите передать несколько параметров или использовать autowiring, полезно [упаковать параметры в объект |best-practices:passing-settings-to-presenters]. -Поддерживает ли Nette PSR-11: Container interface? +Поддерживает ли Nette интерфейс контейнера PSR-11? -------------------------------------------------- -Nette DI Container не поддерживает PSR-11 напрямую. Однако, если вам нужна интероперабельность между Nette DI Container и библиотеками или фреймворками, которые ожидают PSR-11 Container Interface, вы можете создать [простой адаптер |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], который будет служить мостом между Nette DI Container и 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 index d0b8d5733a..ef66900eaa 100644 --- a/dependency-injection/ru/global-state.texy +++ b/dependency-injection/ru/global-state.texy @@ -2,42 +2,42 @@ ******************************** .[perex] -Предупреждение: Следующие конструкции являются признаком плохо спроектированного кода: +Внимание: следующие конструкции - симптомы плохо спроектированного кода: - `Foo::getInstance()` - `DB::insert(...)` - `Article::setDb($db)` - `ClassName::$var` или `static::$var` -Встречаются ли некоторые из этих конструкций в вашем коде? Тогда у вас есть возможность его улучшить. Возможно, вы думаете, что это обычные конструкции, которые вы видите, например, и в примерах решений различных библиотек и фреймворков. Если это так, то дизайн их кода нехорош. +Встречаются ли какие-нибудь из этих конструкций в вашем коде? Если да, у вас есть возможность его улучшить. Вы можете подумать, что это обычные конструкции, которые встречаются в примерах решений из разных библиотек и фреймворков. Если так, то их код спроектирован ошибочно. -Сейчас мы определенно не говорим о какой-то академической чистоте. Все эти конструкции имеют одно общее: они используют глобальное состояние. А оно оказывает разрушительное воздействие на качество кода. Классы лгут о своих зависимостях. Код становится непредсказуемым. Путает программистов и снижает их эффективность. +Речь здесь не о какой-то академической чистоте. У всех этих конструкций есть одна общая черта: они используют глобальное состояние. А глобальное состояние губительно сказывается на качестве кода. Классы начинают вводить в заблуждение относительно своих зависимостей. Код становится непредсказуемым. Он сбивает разработчиков с толку и снижает их эффективность. -В этой главе мы объясним, почему это так, и как избежать глобального состояния. +В этой главе мы объясним, почему так происходит и как избежать глобального состояния. -Глобальная связанность ----------------------- +Глобальные связи +---------------- -В идеальном мире объект должен иметь возможность общаться только с объектами, которые были ему [напрямую переданы |passing-dependencies]. Если я создам два объекта `A` и `B` и никогда не передам ссылку между ними, то ни `A`, ни `B` не смогут получить доступ к другому объекту или изменить его состояние. Это очень желательное свойство кода. Это похоже на то, как если бы у вас были батарейка и лампочка; лампочка не загорится, пока вы не соедините ее с батарейкой проводом. +В идеальном мире объект должен общаться только с объектами, которые ему [передали напрямую |passing-dependencies]. Если я создам два объекта `A` и `B` и никогда не передам ссылку между ними, то ни `A`, ни `B` не смогут обратиться к состоянию другого или изменить его. Это крайне желательное свойство кода. Это похоже на батарейку и лампочку: лампочка не загорится, пока вы не соедините её с батарейкой проводом. -Но это не относится к глобальным (статическим) переменным или синглтонам. Объект `A` мог бы *беспроводным* способом получить доступ к объекту `C` и модифицировать его без какой-либо передачи ссылки, вызвав `C::changeSomething()`. Если объект `B` также захватит глобальный `C`, то `A` и `B` могут взаимно влиять друг на друга через `C`. +Однако для глобальных (статических) переменных или синглтонов это не так. Объект `A` может *по воздуху* обратиться к объекту `C` и изменить его без всякой передачи ссылки, вызвав `C::changeSomething()`. Если объект `B` тоже подключается к глобальному `C`, то `A` и `B` могут влиять друг на друга через `C`. -Использование глобальных переменных вносит в систему новую форму *беспроводной* связанности, которая не видна снаружи. Создает дымовую завесу, усложняющую понимание и использование кода. Чтобы разработчики действительно поняли зависимости, им нужно прочитать каждую строку исходного кода. Вместо простого ознакомления с интерфейсами классов. К тому же это совершенно излишняя связанность. Глобальное состояние используется потому, что оно легко доступно откуда угодно и позволяет, например, записать в базу данных через глобальный (статический) метод `DB::insert()`. Но, как мы покажем, преимущество, которое это дает, незначительно, в то время как осложнения вызывает фатальные. +Использование глобальных переменных вводит новый вид *беспроводной* связанности, невидимой снаружи. Оно создаёт дымовую завесу, из-за которой код становится сложнее понимать и использовать. Чтобы по-настоящему разобраться в зависимостях, разработчикам приходится читать каждую строку исходного кода, а не полагаться на интерфейсы классов. Кроме того, эта связанность совершенно не нужна. Глобальное состояние используют потому, что оно легко доступно откуда угодно и позволяет, например, писать в базу данных через глобальный (статический) метод `DB::insert()`. Однако, как мы покажем, кажущееся удобство ничтожно по сравнению с тяжёлыми осложнениями, которые оно приносит. .[note] -С точки зрения поведения нет разницы между глобальной и статической переменной. Они одинаково вредны. +С точки зрения поведения между глобальной и статической переменной нет разницы. Они одинаково вредны. Жуткое действие на расстоянии ----------------------------- -«Жуткое действие на расстоянии» — так знаменито назвал в 1935 году Альберт Эйнштейн явление в квантовой физике, которое вызывало у него мурашки по коже. -Речь идет о квантовой запутанности, особенностью которой является то, что когда вы измеряете информацию об одной частице, вы немедленно влияете на другую частицу, даже если они находятся на расстоянии миллионов световых лет друг от друга. Что, казалось бы, нарушает основной закон Вселенной, что ничто не может распространяться быстрее света. +"Жуткое действие на расстоянии" - так Альберт Эйнштейн знаменито назвал явление квантовой физики, которое приводило его в содрогание. +Речь о квантовой запутанности, когда измерение свойства одной частицы мгновенно влияет на другую запутанную частицу, независимо от разделяющего их расстояния, даже в миллионы световых лет, что как будто нарушает основополагающий закон Вселенной о том, что ничто не может двигаться быстрее света. -В мире программного обеспечения мы можем назвать «жутким действием на расстоянии» ситуацию, когда мы запускаем какой-то процесс, который, как мы полагаем, изолирован (потому что мы не передали ему никаких ссылок), но в отдаленных местах системы происходят неожиданные взаимодействия и изменения состояния, о которых мы не подозревали. Это может произойти только через глобальное состояние. +В мире программ "жуткое действие на расстоянии" описывает ситуацию, когда мы запускаем процесс, который считаем изолированным (ведь никаких зависимостей явно не передавали), а в отдалённых частях системы неожиданно происходят взаимодействия и изменения состояния, о которых мы не подозреваем. Такое возможно только через глобальное состояние. -Представьте, что вы присоединились к команде разработчиков проекта, у которого обширная и развитая кодовая база. Ваш новый руководитель просит вас реализовать новую функцию, и вы, как правильный разработчик, начинаете с написания теста. Но поскольку вы новичок в проекте, вы проводите много исследовательских тестов типа «что произойдет, если я вызову этот метод». И пробуете написать следующий тест: +Представьте, что вы приходите в команду разработки проекта с большой зрелой кодовой базой. Ваш новый руководитель просит реализовать новую возможность, и вы, как хороший разработчик, начинаете с написания теста. Но поскольку вы в проекте новичок, вы много экспериментируете в духе "что будет, если вызвать этот метод". И вы пробуете написать такой тест: ```php function testCreditCardCharge() @@ -47,17 +47,17 @@ function testCreditCardCharge() } ``` -Вы запускаете код, возможно, несколько раз, и через некоторое время замечаете на мобильном телефоне уведомления от банка, что при каждом запуске с вашей платежной карты списывалось 100 долларов 🤦‍♂️ +Вы запускаете код, возможно несколько раз, и через некоторое время замечаете на телефоне уведомления банка: с вашей кредитной карты при каждом запуске списывали 100 долларов! 🤦‍♂️ -Как, черт возьми, тест мог вызвать реальное списание денег? Оперировать платежной картой непросто. Нужно общаться со сторонним веб-сервисом, нужно знать URL этого веб-сервиса, нужно войти в систему и так далее. Никакой из этой информации в тесте нет. Хуже того, вы даже не знаете, где эта информация находится, и, следовательно, как замокать внешние зависимости, чтобы каждый запуск не приводил к тому, что снова спишется 100 долларов. И как вы, как новый разработчик, должны были знать, что то, что вы собираетесь сделать, приведет к тому, что вы станете на 100 долларов беднее? +Как вообще тест мог вызвать настоящее списание? Работа с кредитной картой - дело непростое. Нужно обратиться к стороннему веб-сервису, знать его URL, пройти аутентификацию и так далее. Ничего этого в тесте нет. Хуже того, вы не знаете, где эти сведения находятся, поэтому не можете подменить внешние зависимости заглушками, чтобы не терять 100 долларов при каждом запуске теста. И как вы, новый разработчик, должны были догадаться, что то, что вы собираетесь сделать, оставит вас на 100 долларов беднее? -Это жуткое действие на расстоянии! +Вот это и есть жуткое действие на расстоянии! -Вам не остается ничего другого, как долго копаться в куче исходных кодов, спрашивать старших и более опытных коллег, пока вы не поймете, как работают связи в проекте. Это вызвано тем, что при взгляде на интерфейс класса `CreditCard` нельзя определить глобальное состояние, которое нужно инициализировать. Даже взгляд в исходный код класса не подскажет вам, какой инициализационный метод нужно вызвать. В лучшем случае вы можете найти глобальную переменную, к которой осуществляется доступ, и из нее попытаться угадать, как ее инициализировать. +Вы вынуждены перелопачивать огромный исходный код и советоваться со старшими коллегами, чтобы понять взаимосвязи в проекте. Трудность возникает потому, что интерфейс класса `CreditCard` не выдаёт необходимой инициализации глобального состояния. Даже изучение исходного кода класса может не подсказать, какой метод инициализации вызвать. В лучшем случае вы найдёте, к какой глобальной переменной он обращается, и попробуете вывести, как её инициализировать. -Классы в таком проекте — патологические лжецы. Платежная карта делает вид, что ее достаточно инстанцировать и вызвать метод `charge()`. Втайне же она сотрудничает с другим классом `PaymentGateway`, который представляет платежный шлюз. И его интерфейс говорит, что его можно инициализировать самостоятельно, но на самом деле он извлекает учетные данные из какого-то конфигурационного файла и так далее. Разработчикам, написавшим этот код, ясно, что `CreditCard` нуждается в `PaymentGateway`. Они написали код таким образом. Но для любого, кто новичок в проекте, это полная загадка и мешает обучению. +Классы в таком проекте - патологические лжецы. Класс `CreditCard` делает вид, что его можно просто создать и вызвать метод `charge()`. Однако втайне он взаимодействует с другим классом `PaymentGateway`, представляющим платёжный шлюз. Даже интерфейс `PaymentGateway` может намекать на самостоятельную инициализацию, а на деле он может вытаскивать учётные данные из конфигурационного файла и так далее. Исходные разработчики понимают, что `CreditCard` требует `PaymentGateway`. Они так написали код. Но для новичков это полная загадка, мешающая им учиться и вносить полезный вклад. -Как исправить ситуацию? Легко. **Пусть API декларирует зависимости.** +Как исправить положение? Легко. **Пусть API объявляет зависимости.** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -Обратите внимание, как сразу становятся очевидными связи внутри кода. Тем, что метод `charge()` декларирует, что ему нужен `PaymentGateway`, вам не нужно никого спрашивать о том, как связан код. Вы знаете, что нужно создать его экземпляр, и когда вы попытаетесь это сделать, вы столкнетесь с тем, что нужно предоставить параметры доступа. Без них код даже не запустится. +Обратите внимание, как взаимозависимости в коде сразу становятся очевидны. Поскольку метод `charge()` объявляет, что ему нужен `PaymentGateway`, вам больше не нужно догадываться об этой зависимости или спрашивать о ней. Вы знаете, что нужно создать экземпляр, и при этом обнаружите нужные параметры доступа. Без них код просто не запустится. -И главное, теперь вы можете замокать платежный шлюз, так что при каждом запуске теста с вас не будет списываться 100 долларов. +И, что важнее всего, теперь вы можете подменить платёжный шлюз заглушкой, чтобы с вас не списывали 100 долларов при каждом запуске теста. -Глобальное состояние приводит к тому, что ваши объекты могут тайно получать доступ к вещам, которые не объявлены в их API, и в результате делают ваши API патологическими лжецами. +Глобальное состояние позволяет объектам тайно обращаться к зависимостям, не объявленным в их API, фактически превращая ваши API в патологических лжецов. -Возможно, вы раньше не думали об этом так, но всякий раз, когда вы используете глобальное состояние, вы создаете секретные беспроводные каналы связи. Жуткое действие на расстоянии заставляет разработчиков читать каждую строку кода, чтобы понять потенциальные взаимодействия, снижает производительность разработчиков и сбивает с толку новых членов команды. Если вы тот, кто создал код, вы знаете реальные зависимости, но любой, кто придет после вас, будет в растерянности. +Возможно, раньше вы об этом так не думали, но всякий раз, используя глобальное состояние, вы создаёте тайные беспроводные каналы связи. Это жуткое действие на расстоянии заставляет разработчиков читать каждую строку кода, чтобы понять возможные взаимодействия, снижает продуктивность и сбивает с толку новых членов команды. Если код создали вы, вы знаете настоящие зависимости, но тот, кто придёт после вас, останется в неведении. -Не пишите код, использующий глобальное состояние, отдавайте предпочтение передаче зависимостей. То есть dependency injection. +Не пишите код, опирающийся на глобальное состояние; предпочитайте явную передачу зависимостей. Возьмите на вооружение внедрение зависимостей. Хрупкость глобального состояния ------------------------------- -В коде, использующем глобальное состояние и синглтоны, никогда не известно, когда и кто это состояние изменил. Этот риск возникает уже при инициализации. Следующий код должен создать подключение к базе данных и инициализировать платежный шлюз, однако постоянно выбрасывает исключение, и поиск причины чрезвычайно утомителен: +В коде, использующем глобальное состояние и синглтоны, вы никогда не можете быть уверены, когда и кем состояние было изменено. Этот риск проявляется даже при инициализации. Следующий код собирается создать соединение с базой данных и инициализировать платёжный шлюз, но раз за разом выбрасывает исключения, а искать причину крайне утомительно: ```php PaymentGateway::init(); DB::init('mysql:', 'user', 'password'); ``` -Вам нужно подробно просмотреть код, чтобы выяснить, что объект `PaymentGateway` беспроводным способом обращается к другим объектам, некоторые из которых требуют подключения к базе данных. То есть необходимо инициализировать базу данных раньше, чем `PaymentGateway`. Однако дымовая завеса глобального состояния это от вас скрывает. Сколько времени вы бы сэкономили, если бы API отдельных классов не лгали и декларировали свои зависимости? +Вам придётся дотошно проследить код, чтобы обнаружить, что объект `PaymentGateway` по воздуху обращается к другим объектам, часть которых требует соединения с базой данных. Поэтому базу нужно инициализировать раньше `PaymentGateway`. Однако дымовая завеса глобального состояния скрывает это от вас. Сколько времени было бы сэкономлено, если бы API этих классов были честны и объявляли свои зависимости? ```php $db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -Подобная проблема возникает и при использовании глобального доступа к подключению к базе данных: +Похожая проблема возникает при глобальном доступе к соединению с базой данных: ```php use Illuminate\Support\Facades\DB; @@ -110,7 +110,7 @@ class Article } ``` -При вызове метода `save()` неясно, было ли уже создано подключение к базе данных и кто несет ответственность за его создание. Если мы хотим, например, изменять подключение к базе данных во время выполнения, например, для тестов, нам, скорее всего, пришлось бы создать дополнительные методы, такие как `DB::reconnect(...)` или `DB::reconnectForTest()`. +При вызове метода `save()` неясно, установлено ли соединение с базой данных и кто отвечает за его установку. Если нам нужно динамически поменять соединение с базой (например, для тестирования), мы можем скатиться к добавлению методов вроде `DB::reconnect(...)` или `DB::reconnectForTest()`. Рассмотрим пример: @@ -122,9 +122,9 @@ Foo::doSomething(); $article->save(); ``` -Где у нас уверенность, что при вызове `$article->save()` действительно используется тестовая база данных? Что, если метод `Foo::doSomething()` изменил глобальное подключение к базе данных? Для выяснения нам пришлось бы изучить исходный код класса `Foo` и, вероятно, многих других классов. Однако этот подход принес бы лишь краткосрочный ответ, поскольку ситуация может измениться в будущем. +Как нам убедиться, что при вызове `$article->save()` действительно используется тестовая база данных? Что, если метод `Foo::doSomething()` изменил глобальное соединение с базой? Чтобы это выяснить, нам пришлось бы изучить исходный код `Foo` и, возможно, множества других классов. И это расследование дало бы лишь временный ответ, потому что позже положение может измениться. -А что, если мы перенесем подключение к базе данных в статическую переменную внутри класса `Article`? +А что, если мы перенесём соединение с базой данных в статическую переменную внутри класса `Article`? ```php class Article @@ -143,11 +143,11 @@ class Article } ``` -Это вообще ничего не изменило. Проблема в глобальном состоянии, и совершенно неважно, в каком классе оно скрывается. В этом случае, как и в предыдущем, у нас нет никаких подсказок при вызове метода `$article->save()` о том, в какую базу данных будет произведена запись. Кто угодно на другом конце приложения мог в любой момент с помощью `Article::setDb()` изменить базу данных. У нас под носом. +Это ничего не меняет. Проблема в самом глобальном состоянии, независимо от того, внутри какого класса оно спрятано. В этом случае, как и в предыдущем, при вызове `$article->save()` у нас нет уверенности, в какую базу данных запишутся данные. Кто угодно и где угодно в приложении мог в любой момент поменять базу через `Article::setDb()`. Без нашего ведома. Глобальное состояние делает наше приложение **чрезвычайно хрупким**. -Однако существует простой способ справиться с этой проблемой. Достаточно позволить API декларировать зависимости, что обеспечит правильную функциональность. +Однако с этой проблемой можно справиться просто. Достаточно, чтобы API объявлял зависимости, нужные для правильной работы. ```php class Article @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -Благодаря этому подходу отпадает беспокойство о скрытых и неожиданных изменениях подключения к базе данных. Теперь у нас есть уверенность, куда сохраняется статья, и никакие изменения кода внутри другого несвязанного класса уже не могут изменить ситуацию. Код больше не хрупкий, а стабильный. +Такой подход снимает опасения по поводу скрытых или неожиданных изменений соединения с базой данных. Теперь мы точно знаем, куда сохраняется статья, и изменения в несвязанных классах на это уже не повлияют. Код больше не хрупкий, а устойчивый. -Не пишите код, использующий глобальное состояние, отдавайте предпочтение передаче зависимостей. То есть dependency injection. +Не пишите код, опирающийся на глобальное состояние; предпочитайте явную передачу зависимостей. Возьмите на вооружение внедрение зависимостей. -Singleton ---------- +Синглтон +-------- -Singleton — это паттерн проектирования, который согласно "определению":https://en.wikipedia.org/wiki/Singleton_pattern из известной публикации Gang of Four ограничивает класс единственным экземпляром и предлагает к нему глобальный доступ. Реализация этого паттерна обычно напоминает следующий код: +Синглтон - шаблон проектирования, который, по [определению |https://ru.wikipedia.org/wiki/Одиночка_(шаблон_проектирования)] из знаменитой книги "банды четырёх", ограничивает класс одним экземпляром и предоставляет к нему глобальный доступ. Реализация этого шаблона обычно похожа на такой код: ```php class Singleton @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // и другие методы, выполняющие функции данного класса + // и другие методы, выполняющие задачи класса } ``` -К сожалению, синглтон вводит в приложение глобальное состояние. И, как мы показали выше, глобальное состояние нежелательно. Поэтому синглтон считается антипаттерном. +К сожалению, синглтон вносит в приложение глобальное состояние. А как мы показали выше, глобальное состояние нежелательно. Поэтому синглтон считается антипаттерном. -Не используйте в своем коде синглтоны и замените их другими механизмами. Синглтоны вам действительно не нужны. Однако, если вам нужно гарантировать существование единственного экземпляра класса для всего приложения, оставьте это [DI-контейнеру |container]. Создайте таким образом прикладной синглтон, то есть сервис. Тем самым класс перестанет заниматься обеспечением своей собственной уникальности (т.е. не будет иметь метода `getInstance()` и статической переменной) и будет выполнять только свои функции. Так он перестанет нарушать принцип единственной ответственности. +Не используйте синглтоны в своём коде и заменяйте их другими механизмами. Синглтоны вам действительно не нужны. Однако если вам нужно обеспечить, чтобы во всём приложении существовал только один экземпляр класса, поручите эту обязанность [DI-контейнеру |container]. Так возникнет синглтон в пределах приложения, который обычно называют сервисом. Сам класс тогда освобождается от заботы о собственной уникальности (то есть у него не будет метода `getInstance()` или статического свойства с экземпляром) и может сосредоточиться исключительно на своих обязанностях. Тем самым он перестанет нарушать принцип единственной ответственности. Глобальное состояние и тесты ---------------------------- -При написании тестов мы предполагаем, что каждый тест является изолированной единицей и что в него не входит никакое внешнее состояние. И никакое состояние тесты не покидают. После завершения теста все связанное с тестом состояние должно быть автоматически удалено сборщиком мусора. Благодаря этому тесты изолированы. Поэтому мы можем запускать тесты в любом порядке. +Когда мы пишем тесты, мы в идеале исходим из того, что каждый тест - изолированная единица, в которую не входит и из которой не выходит никакое внешнее состояние. После завершения теста всё связанное с ним состояние должно автоматически убираться сборщиком мусора. Это делает тесты изолированными. Поэтому мы можем запускать тесты в любом порядке. -Однако, если присутствуют глобальные состояния/синглтоны, все эти приятные предположения рушатся. Состояние может входить в тест и выходить из него. Внезапно порядок тестов может иметь значение. +Однако при наличии глобального состояния или синглтонов эти полезные предположения рушатся. Состояние может утекать в тесты и из них. Внезапно порядок тестов начинает иметь значение. -Чтобы вообще иметь возможность тестировать синглтоны, разработчики часто вынуждены ослаблять их свойства, например, разрешая замену экземпляра другим. Такие решения в лучшем случае являются хаком, который создает трудно поддерживаемый и понятный код. Каждый тест или метод `tearDown()`, который влияет на какое-либо глобальное состояние, должен отменять эти изменения. +Чтобы вообще протестировать код с синглтонами, разработчикам часто приходится поступаться их цельностью, например разрешая подменять экземпляр синглтона. Такие решения в лучшем случае остаются костылями, приводящими к коду, который трудно поддерживать и понимать. Любой тест (или его метод `tearDown()`), меняющий глобальное состояние, обязан дотошно откатывать эти изменения. -Глобальное состояние — самая большая головная боль при юнит-тестировании! +Глобальное состояние - главная головная боль модульного тестирования! -Как исправить ситуацию? Легко. Не пишите код, использующий синглтоны, отдавайте предпочтение передаче зависимостей. То есть dependency injection. +Как это исправить? Просто. Не пишите код, использующий синглтоны; предпочитайте явную передачу зависимостей. Возьмите на вооружение внедрение зависимостей. Глобальные константы -------------------- -Глобальное состояние не ограничивается только использованием синглтонов и статических переменных, но может касаться и глобальных констант. +Глобальное состояние не ограничивается синглтонами и статическими переменными, оно может относиться и к глобальным константам. -Константы, значение которых не несет нам никакой новой (`M_PI`) или полезной (`PREG_BACKTRACK_LIMIT_ERROR`) информации, однозначно в порядке. Напротив, константы, которые служат способом *беспроводной* передачи информации внутрь кода, являются не чем иным, как скрытой зависимостью. Как, например, `LOG_FILE` в следующем примере. Использование константы `FILE_APPEND` совершенно корректно. +Константы, значения которых выражают всеобщие истины (`M_PI`) или несут самодостаточные сведения (`PREG_BACKTRACK_LIMIT_ERROR`), в целом допустимы. И наоборот, константы, используемые как способ *по воздуху* подсунуть сведения в код, фактически являются скрытыми зависимостями. Как `LOG_FILE` в следующем примере. Использование константы `FILE_APPEND` совершенно корректно. ```php const LOG_FILE = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -В этом случае мы должны были бы объявить параметр в конструкторе класса `Foo`, чтобы он стал частью API: +Вместо этого нам следует объявить путь к файлу лога параметром конструктора класса `Foo`, сделав его явной частью его API: ```php class Foo @@ -253,42 +253,42 @@ class Foo } ``` -Теперь мы можем передать информацию о пути к файлу для логирования и легко изменять ее по мере необходимости, что облегчает тестирование и поддержку кода. +Теперь мы передаём путь к файлу лога явно. Мы легко можем изменить его при необходимости, что упрощает тестирование и поддержку кода. Глобальные функции и статические методы --------------------------------------- -Мы хотим подчеркнуть, что само по себе использование статических методов и глобальных функций не является проблематичным. Мы объясняли, в чем заключается нецелесообразность использования `DB::insert()` и подобных методов, но всегда речь шла только о глобальном состоянии, которое хранится в какой-то статической переменной. Метод `DB::insert()` требует существования статической переменной, потому что в ней хранится подключение к базе данных. Без этой переменной было бы невозможно реализовать метод. +Мы хотим подчеркнуть, что использование статических методов и глобальных функций само по себе не проблема. Мы объяснили сложности с методами вроде `DB::insert()`, но корнем проблемы всегда было лежащее в основе глобальное состояние, обычно хранящееся в статической переменной. Метод `DB::insert()` опирается на статическую переменную, хранящую соединение с базой данных. Без этой переменной реализовать метод было бы невозможно. -Использование детерминированных статических методов и функций, таких как `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` и многих других, полностью соответствует dependency injection. Эти функции всегда возвращают одинаковые результаты для одинаковых входных параметров и, следовательно, предсказуемы. Они не используют никакого глобального состояния. +Использование детерминированных статических методов и функций вроде `Closure::fromCallable()`, `strlen()` и множества других полностью совместимо с внедрением зависимостей. Эти функции предсказуемы, потому что при одних и тех же входных параметрах всегда возвращают один и тот же результат. Они не используют никакого глобального состояния. -Однако существуют и функции в PHP, которые не являются детерминированными. К ним относится, например, функция `htmlspecialchars()`. Ее третий параметр `$encoding`, если не указан, по умолчанию имеет значение конфигурационной опции `ini_get('default_charset')`. Поэтому рекомендуется всегда указывать этот параметр и предотвращать тем самым возможное непредсказуемое поведение функции. Nette это последовательно делает. +Однако в PHP есть и недетерминированные функции. К ним относится, например, функция `htmlspecialchars()`. Её третий параметр `$encoding`, если его опустить, по умолчанию берёт значение параметра конфигурации `default_charset` (`ini_get('default_charset')`). Поэтому рекомендуется всегда указывать этот параметр, чтобы избежать возможного непредсказуемого поведения. Nette последовательно так и делает. -Некоторые функции, такие как `strtolower()`, `strtoupper()` и подобные, в недавнем прошлом вели себя недетерминированно и зависели от настройки `setlocale()`. Это вызывало много осложнений, чаще всего при работе с турецким языком. Он различает как строчную, так и прописную букву `I` с точкой и без точки. Так что `strtolower('I')` возвращало символ `ı`, а `strtoupper('i')` — символ `İ`, что приводило к тому, что приложения начинали вызывать ряд загадочных ошибок. Однако эта проблема была устранена в PHP версии 8.2, и функции больше не зависят от локали. +Некоторые функции, такие как `strtolower()` и `strtoupper()`, ещё недавно вели себя недетерминированно в зависимости от настройки локали (`setlocale()`). Это вызывало множество осложнений, чаще всего при работе с турецким языком. Дело в том, что в турецком различаются "I" с точкой и без точки как в нижнем, так и в верхнем регистре. В результате `strtolower('I')` возвращала `ı` (строчную i без точки), а `strtoupper('i')` - `İ` (заглавную I с точкой), что приводило к множеству загадочных ошибок в приложениях. Однако в PHP версии 8.2 эта проблема исправлена, и функции больше не зависят от локали. -Это хороший пример того, как глобальное состояние доставило хлопот тысячам разработчиков по всему миру. Решением было заменить его на dependency injection. +Это хороший пример того, как глобальное состояние (настройка локали) досаждало тысячам разработчиков по всему миру. Решением в итоге стало сделать функции независимыми от локали, фактически убрав скрытую зависимость. -Когда можно использовать глобальное состояние? ----------------------------------------------- +Когда глобальное состояние всё же можно использовать? +----------------------------------------------------- -Существуют определенные специфические ситуации, когда можно использовать глобальное состояние. Например, при отладке кода, когда нужно вывести значение переменной или измерить продолжительность определенной части программы. В таких случаях, которые касаются временных действий, которые позже будут удалены из кода, легитимно использовать глобально доступный дампер или секундомер. Эти инструменты не являются частью дизайна кода. +Есть отдельные, ограниченные ситуации, когда использование глобального состояния может быть допустимо. Например, при отладке, когда нужно вывести значение переменной или измерить время выполнения определённого участка кода. В этих случаях, связанных с временными действиями, которые позже уберут из кода, использование глобально доступного дампера или таймера может быть законным. Эти инструменты не входят в основную архитектуру приложения. -Другим примером являются функции для работы с регулярными выражениями `preg_*`, которые внутренне хранят скомпилированные регулярные выражения в статическом кеше в памяти. Когда вы вызываете одно и то же регулярное выражение несколько раз в разных местах кода, оно компилируется только один раз. Кеш экономит производительность и в то же время для пользователя совершенно невидим, поэтому такое использование можно считать легитимным. +Другой пример - функции регулярных выражений PHP (`preg_*`), которые внутренне кешируют скомпилированные регулярные выражения в статической памяти. Когда вы многократно вызываете функции с одним и тем же регулярным выражением в разных местах кода, выражение компилируется только один раз. Такое кеширование повышает производительность и совершенно невидимо для пользователя, поэтому такое использование внутреннего статического состояния в целом допустимо. -Резюме ------- +Итоги +----- -Мы рассмотрели, почему имеет смысл: +Мы обсудили, почему имеет смысл: -1) Удалить все статические переменные из кода -2) Декларировать зависимости -3) И использовать dependency injection +1) убрать из кода все изменяемые статические свойства (глобальное состояние) +2) явно объявлять зависимости +3) и использовать внедрение зависимостей -Когда вы продумываете дизайн кода, помните, что каждое `static $foo` представляет собой проблему. Чтобы ваш код был средой, уважающей DI, необходимо полностью искоренить глобальное состояние и заменить его с помощью dependency injection. +Проектируя код, помните, что каждое изменяемое `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, чьи статьи, такие как [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 index 95c80acebd..f1c8ca28a3 100644 --- a/dependency-injection/ru/introduction.texy +++ b/dependency-injection/ru/introduction.texy @@ -2,57 +2,57 @@ ******************************* .[perex] -Эта глава познакомит вас с основными методами программирования, которым вы должны следовать при написании всех приложений. Это основы, необходимые для написания чистого, понятного и поддерживаемого кода. +Эта глава знакомит с базовыми приёмами программирования, которых стоит придерживаться при написании любого приложения. Это основы, необходимые для чистого, понятного и поддерживаемого кода. -Если вы освоите эти правила и будете им следовать, Nette будет помогать вам на каждом шагу. Он будет решать за вас рутинные задачи и обеспечит максимальное удобство, чтобы вы могли сосредоточиться на самой логике. +Если вы примете и будете соблюдать эти правила, Nette поддержит вас на каждом шагу. Он возьмёт на себя рутину и обеспечит максимальное удобство, чтобы вы могли сосредоточиться на самой логике. -Принципы, которые мы здесь покажем, довольно просты. Вам не нужно ничего бояться. +Принципы, которые мы здесь покажем, довольно просты. Бояться нечего. Помните свою первую программу? ------------------------------ -Мы не знаем, на каком языке вы ее написали, но если бы это был PHP, она, вероятно, выглядела бы так: +Мы не знаем, на каком языке вы её написали, но если на PHP, она, вероятно, выглядела примерно так: ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // выведет 24 +echo addition(23, 1); // выводит 24 ``` -Несколько тривиальных строк кода, но в них скрыто так много ключевых концепций. Что существуют переменные. Что код делится на меньшие единицы, такие как функции. Что мы передаем им входные аргументы, и они возвращают результаты. Не хватает только условий и циклов. +Несколько тривиальных строк кода, а сколько ключевых понятий в них скрыто. Что существуют переменные. Что код делится на меньшие единицы, например на функции. Что мы передаём в них входные аргументы, а они возвращают результаты. Не хватает только условий и циклов. -То, что мы передаем функции входные данные, и она возвращает результат, — это совершенно понятная концепция, которая используется и в других областях, например, в математике. +То, что мы передаём в функцию входные данные, а она возвращает результат, - совершенно понятное представление, используемое и в других областях, например в математике. -Функция имеет свою сигнатуру, которая состоит из ее имени, списка параметров и их типов, и, наконец, типа возвращаемого значения. Как пользователей, нас интересует сигнатура, о внутренней реализации нам обычно ничего знать не нужно. +У функции есть сигнатура, состоящая из её имени, списка параметров с их типами и, наконец, типа возвращаемого значения. Как пользователей нас интересует сигнатура; о внутренней реализации нам обычно знать не нужно. -Теперь представьте, что сигнатура функции выглядела бы так: +А теперь представьте, что сигнатура функции выглядела бы так: ```php -function soucet(float $x): float +function addition(float $x): float ``` -Сумма с одним параметром? Это странно… А как насчет этого? +Сложение с одним параметром? Странно… А как насчёт такого? ```php -function soucet(): float +function addition(): float ``` -Это уже действительно очень странно, не так ли? Как используется эта функция? +Вот это уже совсем странно, правда? Как эта функция используется? ```php -echo soucet(); // что она выведет? +echo addition(); // что она выведет? ``` -Глядя на такой код, мы были бы сбиты с толку. Его не понял бы не только новичок, но и опытный программист. +Глядя на такой код, мы бы растерялись. Не только новичок его не понял бы, но и опытный программист не разобрался бы в таком коде. -Вы думаете, как бы выглядела такая функция внутри? Откуда она возьмет слагаемые? Очевидно, она бы их *каким-то образом* получила сама, например, так: +Задумались, как такая функция выглядела бы внутри? Откуда бы она брала числа для сложения? Вероятно, она *как-нибудь* раздобыла бы их сама, скажем так: ```php -function soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -В теле функции мы обнаружили скрытые связи с другими глобальными функциями или статическими методами. Чтобы выяснить, откуда на самом деле берутся слагаемые, нам нужно копать дальше. +В теле функции мы обнаружили скрытые зависимости от других глобальных функций или статических методов. Чтобы выяснить, откуда на самом деле берутся числа, нам нужно копать дальше. -Так нельзя! ------------ +Только не так! +-------------- -Дизайн, который мы только что показали, является квинтэссенцией многих негативных черт: +Только что показанное решение - средоточие множества отрицательных свойств: -- сигнатура функции делала вид, что ей не нужны слагаемые, что сбивало нас с толку -- мы совершенно не знаем, как заставить функцию сложить два других числа -- нам пришлось заглянуть в код, чтобы выяснить, откуда она берет слагаемые -- мы обнаружили скрытые связи -- для полного понимания необходимо изучить и эти связи +- Сигнатура функции делала вид, что числа для сложения ей не нужны, и это сбило нас с толку. +- Мы понятия не имеем, как заставить функцию сложить два других числа. +- Нам пришлось лезть в код, чтобы выяснить, откуда она берёт числа. +- Мы обнаружили скрытые зависимости. +- Полное понимание требует изучить и эти зависимости. -И вообще, задача функции сложения — получать входные данные? Конечно, нет. Ее ответственность — только само сложение. +И вообще, дело ли функции сложения - добывать входные данные? Разумеется, нет. Её обязанность - только само сложение. -Мы не хотим сталкиваться с таким кодом, и уж точно не хотим его писать. Исправление при этом простое: вернуться к основам и просто использовать параметры: +Мы не хотим встречать такой код и уж точно не хотим его писать. Исправление простое: вернуться к основам и просто использовать параметры: ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -Правило № 1: пусть тебе это передадут -------------------------------------- +Правило № 1: пусть вам это передадут +------------------------------------ -Самое важное правило гласит: **все данные, которые нужны функции или классу, должны быть им переданы**. +Самое важное правило гласит: **все данные, которые нужны функциям или классам, должны быть им переданы**. -Вместо того чтобы изобретать скрытые способы, с помощью которых они могли бы как-то получить их сами, просто передайте параметры. Вы сэкономите время, необходимое на придумывание скрытых путей, которые определенно не улучшат ваш код. +Вместо того чтобы придумывать скрытые способы, которыми они раздобудут данные, просто передайте параметры. Вы сэкономите время, потраченное на выдумывание скрытых путей, которые точно не улучшат ваш код. -Если вы будете всегда и везде следовать этому правилу, вы на пути к коду без скрытых связей. К коду, который понятен не только автору, но и всем, кто будет его читать после него. Где все понятно из сигнатур функций и классов, и не нужно искать скрытые тайны в реализации. +Если вы будете всегда и везде соблюдать это правило, вы на пути к коду без скрытых зависимостей. К коду, понятному не только автору, но и всякому, кто прочитает его позже. Где всё понятно из сигнатур функций и классов и не нужно разыскивать скрытые подробности в реализации. -Эта техника профессионально называется **dependency injection**. А эти данные называются **зависимостями.** При этом это обычная передача параметров, ничего больше. +Профессионально этот приём называется **Dependency Injection** (внедрение зависимостей). А сами данные называются **зависимостями**. Это просто передача параметров, ничего больше. .[note] -Пожалуйста, не путайте dependency injection, который является паттерном проектирования, с «dependency injection container», который является инструментом, то есть чем-то диаметрально противоположным. Контейнерам мы посвятим внимание позже. +Пожалуйста, не путайте Dependency Injection, который является шаблоном проектирования, с "контейнером внедрения зависимостей", который является инструментом, то есть чем-то принципиально другим. О контейнерах мы поговорим позже. От функций к классам -------------------- -А как с этим связаны классы? Класс — это более сложная единица, чем простая функция, однако правило № 1 действует здесь без исключений. Просто существует [больше возможностей для передачи аргументов|passing-dependencies]. Например, довольно похоже на случай с функцией: +А как это относится к классам? Класс - сущность посложнее простой функции, но правило № 1 полностью применимо и здесь. Просто [способов передать аргументы |passing-dependencies] больше. Например, довольно похоже на случай с функцией: ```php -class Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` -Или с помощью других методов, или непосредственно конструктора: +Или другими методами, или прямо через конструктор: ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,26 +132,25 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` -Оба примера полностью соответствуют dependency injection. +Оба примера полностью соответствуют Dependency Injection. -Реальные примеры +Примеры из жизни ---------------- -В реальном мире вы не будете писать классы для сложения чисел. Давайте перейдем к примерам из практики. +В реальном мире вы не будете писать классы для сложения чисел. Перейдём к практическим примерам. -Пусть у нас есть класс `Article`, представляющий статью в блоге: +Пусть у нас будет класс `Article`, представляющий статью блога: ```php class Article @@ -162,12 +161,12 @@ class Article public function save(): void { - // сохраним статью в базу данных + // сохраняем статью в базу данных } } ``` -и использование будет следующим: +а использование будет таким: ```php $article = new Article; @@ -176,9 +175,9 @@ $article->content = 'Every year millions of people in ...'; $article->save(); ``` -Метод `save()` сохраняет статью в таблицу базы данных. Реализовать его с помощью [Nette Database |database:] было бы легко, если бы не одна загвоздка: где `Article` возьмет подключение к базе данных, т. е. объект класса `Nette\Database\Connection`? +Метод `save()` сохранит статью в таблицу базы данных. Реализовать его с помощью [Nette Database |database:] было бы просто, если бы не одна загвоздка: откуда `Article` возьмёт соединение с базой данных, то есть объект класса `Nette\Database\Connection`? -Кажется, у нас много вариантов. Он может взять его откуда-то из статической переменной. Или унаследовать от класса, который обеспечивает соединение с базой данных. Или использовать так называемый [синглтон |global-state#Singleton]. Или так называемые фасады, которые используются в Laravel: +Кажется, что вариантов много. Он мог бы взять его из статической переменной. Или наследуясь от класса, предоставляющего соединение с базой. Или использовать [синглтон |global-state#Синглтон]. Или так называемые фасады, как в Laravel: ```php use Illuminate\Support\Facades\DB; @@ -199,17 +198,17 @@ class Article } ``` -Отлично, мы решили проблему. +Отлично, задачу мы решили. -Или нет? +Или всё же нет? -Напомним [правилу № 1: пусть тебе это передадут |#Правило 1: пусть тебе это передадут]: все зависимости, которые нужны классу, должны быть ему переданы. Потому что если мы нарушим правило, мы встанем на путь грязного кода, полного скрытых связей, непонятности, и результатом будет приложение, которое будет больно поддерживать и развивать. +Вспомним [#Правило № 1: пусть вам это передадут]: все зависимости, которые нужны классу, должны быть ему переданы. Потому что если мы нарушим правило, мы вступим на путь запутанного кода со скрытыми зависимостями и недостатком ясности, а в итоге получим приложение, которое трудно поддерживать и развивать. -Пользователь класса `Article` не знает, куда метод `save()` сохраняет статью. В таблицу базы данных? В какую, рабочую или тестовую? И как это можно изменить? +Пользователь класса `Article` понятия не имеет, куда метод `save()` сохраняет статью. В таблицу базы данных? В какую именно, в производственную или тестовую? И как это поменять? -Пользователь должен посмотреть, как реализован метод `save()`, и найдет использование метода `DB::insert()`. Значит, он должен искать дальше, как этот метод получает соединение с базой данных. А скрытые связи могут образовывать довольно длинную цепочку. +Пользователю приходится смотреть, как реализован метод `save()`, и он находит использование метода `DB::insert()`. Значит, ему нужно копать дальше, чтобы понять, как этот метод получает соединение с базой данных. А скрытые зависимости могут образовывать довольно длинную цепочку. -В чистом и хорошо спроектированном коде никогда не встречаются скрытые связи, фасады Laravel или статические переменные. В чистом и хорошо спроектированном коде передаются аргументы: +В чистом и хорошо спроектированном коде никогда не бывает скрытых зависимостей, фасадов Laravel или статических переменных. В чистом и хорошо спроектированном коде аргументы передаются: ```php class Article @@ -224,7 +223,7 @@ class Article } ``` -Еще практичнее, как мы увидим далее, будет конструктор: +Ещё практичнее, как мы увидим позже, использовать конструктор: ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -Если вы опытный программист, вы, возможно, подумаете, что `Article` вообще не должен иметь метод `save()`, он должен представлять собой чисто компонент данных, а сохранением должен заниматься отдельный репозиторий. Это имеет смысл. Но так мы бы ушли далеко за рамки темы, которой является dependency injection, и стремления приводить простые примеры. +Если вы опытный программист, вы можете подумать, что у `Article` вообще не должно быть метода `save()`: он должен представлять чистую структуру данных, а сохранением должен заниматься отдельный репозиторий. И это разумно. Но это увело бы нас далеко за пределы темы, которой является Dependency Injection, и цели давать простые примеры. -Если вы пишете класс, требующий для своей работы, например, базу данных, не придумывайте, откуда ее взять, а попросите передать ее вам. Например, как параметр конструктора или другого метода. Признайте зависимости. Признайте их в API вашего класса. Вы получите понятный и предсказуемый код. +Если вы пишете класс, которому для работы нужна, например, база данных, не выдумывайте, откуда её взять, а попросите, чтобы её вам передали. Скажем, параметром конструктора или другого метода. Признайте зависимости. Признайте их в API своего класса. Вы получите понятный и предсказуемый код. -А как насчет этого класса, который логирует сообщения об ошибках: +А как насчёт этого класса, который записывает сообщения об ошибках? ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,23 +261,23 @@ class Logger } ``` -Как вы думаете, мы соблюли [правило № 1: пусть тебе это передадут |#Правило 1: пусть тебе это передадут]? +Как вы думаете, соблюли ли мы [#Правило № 1: пусть вам это передадут]? -Не соблюли. +Нет. -Ключевую информацию, то есть каталог с файлом лога, класс *получает сам* из константы. +Ключевые сведения, каталог с файлом лога, класс *добывает сам* из константы. Посмотрите на пример использования: ```php $logger = new Logger; -$logger->log('Температура 23 °C'); -$logger->log('Температура 10 °C'); +$logger->log('Temperature is 23 °C'); +$logger->log('Temperature is 10 °C'); ``` -Не зная реализации, смогли бы вы ответить на вопрос, куда записываются сообщения? Пришло бы вам в голову, что для работы необходимо существование константы `LOG_DIR`? И смогли бы вы создать второй экземпляр, который будет записывать в другое место? Определенно нет. +Не зная реализации, смогли бы вы ответить, куда пишутся сообщения? Пришло бы вам в голову, что для его работы нужно существование константы `LOG_DIR`? И смогли бы вы создать второй экземпляр, который писал бы в другое место? Уж точно нет. -Давайте исправим класс: +Исправим класс: ```php class Logger @@ -295,24 +294,24 @@ class Logger } ``` -Класс теперь гораздо понятнее, конфигурируемее и, следовательно, полезнее. +Класс теперь куда понятнее, настраиваемее и потому полезнее. ```php -$logger = new Logger('/путь/к/логу.txt'); -$logger->log('Температура 15 °C'); +$logger = new Logger('/path/to/log.txt'); +$logger->log('Temperature is 15 °C'); ``` -Но меня это не интересует! --------------------------- +Но мне всё равно! +----------------- -*«Когда я создаю объект Article и вызываю save(), я не хочу заниматься базой данных, я просто хочу, чтобы он сохранился в ту, которую я настроил в конфигурации.»* +*"Когда я создаю объект Article и вызываю save(), я не хочу возиться с базой данных; я просто хочу, чтобы он сохранился в той, которую я настроил."* -*«Когда я использую Logger, я просто хочу, чтобы сообщение записалось, и не хочу думать, куда. Пусть используется глобальная настройка.»* +*"Когда я использую Logger, я просто хочу, чтобы сообщение записалось, и не хочу разбираться, куда. Пусть используются глобальные настройки."* -Это правильные замечания. +Это справедливые замечания. -В качестве примера покажем класс, рассылающий новостные письма, который залогирует, как все прошло: +Для примера покажем класс, который рассылает новостные письма и записывает результат в лог: ```php class NewsletterDistributor @@ -322,21 +321,21 @@ class NewsletterDistributor $logger = new Logger(/* ... */); try { $this->sendEmails(); - $logger->log('Письма были разосланы'); + $logger->log('Emails have been sent out'); } catch (Exception $e) { - $logger->log('Произошла ошибка при рассылке'); + $logger->log('An error occurred during sending'); throw $e; } } } ``` -Улучшенный `Logger`, который больше не использует константу `LOG_DIR`, требует указать путь к файлу в конструкторе. Как это решить? Класс `NewsletterDistributor` совершенно не интересует, куда записываются сообщения, он хочет их просто записать. +Улучшенный `Logger`, который больше не использует константу `LOG_DIR`, требует путь к файлу в конструкторе. Как это решить? Класс `NewsletterDistributor` не заботит, куда пишутся сообщения, он просто хочет их записывать. -Решение снова [правило № 1: пусть тебе это передадут |#Правило 1: пусть тебе это передадут]: все данные, которые нужны классу, мы ему передаем. +Решение снова [#Правило № 1: пусть вам это передадут]: мы передаём все данные, которые нужны классу. -Значит ли это, что мы передадим путь к логу через конструктор, который затем используем при создании объекта `Logger`? +Значит ли это, что мы передадим путь к логу через конструктор, а затем используем его при создании объекта `Logger`? ```php class NewsletterDistributor @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -Не так! Путь **не относится** к данным, которые нужны классу `NewsletterDistributor`; они нужны `Logger`. Чувствуете разницу? Классу `NewsletterDistributor` нужен логгер как таковой. Значит, его мы и передадим: +Не так! Потому что путь - это **не** данные, которые нужны классу `NewsletterDistributor`; они нужны `Logger`. Улавливаете разницу? Классу `NewsletterDistributor` нужен сам логгер. Поэтому мы передадим сам логгер: ```php class NewsletterDistributor @@ -365,33 +364,33 @@ class NewsletterDistributor { try { $this->sendEmails(); - $this->logger->log('Письма были разосланы'); + $this->logger->log('Emails have been sent out'); } catch (Exception $e) { - $this->logger->log('Произошла ошибка при рассылке'); + $this->logger->log('An error occurred during sending'); throw $e; } } } ``` -Теперь из сигнатур класса `NewsletterDistributor` ясно, что частью его функциональности является логирование. И задача заменить логгер на другой, например, для тестирования, совершенно тривиальна. Кроме того, если конструктор класса `Logger` изменится, это никак не повлияет на наш класс. +Теперь из сигнатуры класса `NewsletterDistributor` ясно, что ведение лога входит в его работу. А задача подменить логгер другим, скажем ради тестирования, становится совершенно тривиальной. Более того, если конструктор класса `Logger` изменится, на наш класс это никак не повлияет. -Правило № 2: бери то, что твое ------------------------------- +Правило № 2: берите своё +------------------------ -Не позволяйте себя обмануть и не позволяйте передавать вам зависимости ваших зависимостей. Пусть вам передают только ваши зависимости. +Не путайтесь и не принимайте зависимости своих зависимостей. Принимайте только собственные зависимости. -Благодаря этому код, использующий другие объекты, будет полностью независим от изменений их конструкторов. Его API будет правдивее. И главное, будет тривиально заменить эти зависимости на другие. +Благодаря этому код, использующий другие объекты, будет полностью независим от изменений их конструкторов. Его API станет точнее. А главное, подменить эти зависимости другими будет проще простого. Новый член семьи ---------------- -В команде разработчиков было принято решение создать второй логгер, который записывает в базу данных. Создадим класс `DatabaseLogger`. Итак, у нас есть два класса, `Logger` и `DatabaseLogger`, один записывает в файл, другой в базу данных… вам не кажется, что в этом названии что-то странное? Не лучше ли было бы переименовать `Logger` в `FileLogger`? Определенно да. +Команда разработки решила создать второй логгер, который пишет в базу данных. Итак, мы создаём класс `DatabaseLogger`. Теперь у нас два класса, `Logger` и `DatabaseLogger`; один пишет в файл, другой в базу данных... не кажутся ли имена странноватыми? Не лучше ли переименовать `Logger` в `FileLogger`? Определённо. -Но мы сделаем это умнее. Под старым названием создадим интерфейс: +Но сделаем это с умом. Мы создадим интерфейс с исходным именем: ```php interface Logger @@ -400,7 +399,7 @@ interface Logger } ``` -… который будут реализовывать оба логгера: +… который реализуют оба логгера: ```php class FileLogger implements Logger @@ -410,17 +409,17 @@ class DatabaseLogger implements Logger // ... ``` -И благодаря этому не нужно будет ничего менять в остальном коде, где используется логгер. Например, конструктор класса `NewsletterDistributor` по-прежнему будет доволен тем, что в качестве параметра требует `Logger`. И только от нас будет зависеть, какой экземпляр мы ему передадим. +И благодаря этому в остальном коде, где используется логгер, ничего менять не придётся. Например, конструктор класса `NewsletterDistributor` по-прежнему будет доволен требованием параметра `Logger`. А уже от нас будет зависеть, какой экземпляр мы ему предоставим. -**Поэтому мы никогда не добавляем к именам интерфейсов суффикс `Interface` или префикс `I`.** Иначе было бы невозможно так красиво развивать код. +**Именно поэтому мы никогда не добавляем к именам интерфейсов суффикс `Interface` или префикс `I`.** Иначе расширить код так изящно было бы невозможно. Хьюстон, у нас проблема ----------------------- -В то время как во всем приложении мы можем обойтись одним экземпляром логгера, будь то файловый или баз данных, и просто передавать его везде, где что-то логируется, совершенно иначе обстоит дело с классом `Article`. Его экземпляры мы создаем по мере необходимости, возможно, несколько раз. Как справиться с зависимостью от базы данных в его конструкторе? +Во всём приложении мы можем обойтись одним экземпляром логгера, файлового или базы данных, и просто передавать его туда, где ведётся лог, но с классом `Article` дело обстоит совсем иначе. Мы создаём его экземпляры по мере надобности, даже многократно. Как быть с зависимостью от базы данных в его конструкторе? -В качестве примера может служить контроллер, который после отправки формы должен сохранить статью в базу данных: +Примером может служить контроллер, который должен сохранить статью в базу данных после отправки формы: ```php class EditController extends Controller @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -Возможное решение напрашивается само собой: передадим объект базы данных конструктором в `EditController` и используем `$article = new Article($this->db)`. +Возможное решение кажется очевидным: передадим объект базы данных через конструктор в `EditController` и используем `$article = new Article($this->db)`. -Как и в предыдущем случае с `Logger` и путем к файлу, это неправильный подход. База данных — это не зависимость `EditController`, а `Article`. Передача базы данных противоречит [правилу № 2: бери то, что твое |#Правило 2: бери то что твое]. Когда изменится конструктор класса `Article` (добавится новый параметр), придется изменять код во всех местах, где создаются экземпляры. Уфф. +Как и в предыдущем случае с `Logger` и путём к файлу, это неправильный подход. База данных - зависимость не `EditController`, а `Article`. Передача базы данных тем самым нарушает [Правило № 2: берите своё |#Правило № 2: берите своё]. Если конструктор класса `Article` изменится (добавится новый параметр), вам придётся править код во всех местах, где создаются экземпляры. Ох. -Хьюстон, что предлагаешь? +Хьюстон, что предлагаете? -Правило № 3: оставь это фабрике -------------------------------- +Правило № 3: пусть этим займётся фабрика +---------------------------------------- -Устранив скрытые связи и передавая все зависимости как аргументы, мы получили более конфигурируемые и гибкие классы. Следовательно, нам нужно что-то еще, что создаст и настроит нам эти более гибкие классы. Будем называть это фабриками. +Убрав скрытые зависимости и передавая все зависимости аргументами, мы получили более настраиваемые и гибкие классы. Поэтому нам нужно что-то ещё, что будет создавать и настраивать эти более гибкие классы за нас. Мы назовём это фабриками. -Правило гласит: если у класса есть зависимости, пусть создание их экземпляров берет на себя фабрика. +Правило гласит: если у класса есть зависимости, поручите создание его экземпляров фабрике. -Фабрики — это более умная замена оператору `new` в мире dependency injection. +Фабрики - более умная альтернатива оператору `new` в мире Dependency Injection. .[note] -Пожалуйста, не путайте с паттерном проектирования *factory method*, который описывает специфический способ использования фабрик и не связан с этой темой. +Пожалуйста, не путайте это с шаблоном проектирования *factory method*, который описывает конкретный способ использования фабрик и с этой темой не связан. Фабрика ------- -Фабрика — это метод или класс, который производит и конфигурирует объекты. Класс, производящий `Article`, назовем `ArticleFactory` и он мог бы выглядеть, например, так: +Фабрика - это метод или класс, который создаёт и настраивает объекты. Класс, производящий `Article`, мы назовём `ArticleFactory`, и он может выглядеть так: ```php class ArticleFactory @@ -475,7 +474,7 @@ class ArticleFactory } ``` -Его использование в контроллере будет следующим: +Его использование в контроллере будет таким: ```php class EditController extends Controller @@ -487,7 +486,7 @@ class EditController extends Controller public function formSubmitted($data) { - // позволим фабрике создать объект + // пусть фабрика создаст объект $article = $this->articleFactory->create(); $article->title = $data->title; $article->content = $data->content; @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -Если в этот момент изменится сигнатура конструктора класса `Article`, единственная часть кода, которая должна на это отреагировать, — это сама фабрика `ArticleFactory`. Весь остальной код, работающий с объектами `Article`, такой как `EditController`, это никак не затронет. +Теперь, если сигнатура конструктора класса `Article` изменится, единственная часть кода, которая должна отреагировать, - сама `ArticleFactory`. Весь остальной код, работающий с объектами `Article`, например `EditController`, останется нетронутым. -Возможно, вы сейчас стучите себя по лбу, думая, помогли ли мы себе вообще. Количество кода выросло, и все это начинает выглядеть подозрительно сложно. +Возможно, вы сейчас чешете затылок, размышляя, действительно ли мы улучшили положение. Объём кода вырос, и всё вместе начинает выглядеть подозрительно сложно. -Не беспокойтесь, скоро мы доберемся до DI-контейнера Nette. А у него есть ряд козырей в рукаве, которые чрезвычайно упростят создание приложений, использующих dependency injection. Так, например, вместо класса `ArticleFactory` достаточно будет [написать всего лишь интерфейс |factory]: +Не переживайте, вскоре мы доберёмся до DI-контейнера Nette. А у него в рукаве припасено несколько приёмов, которые сильно упростят построение приложений с Dependency Injection. Например, вместо класса `ArticleFactory` достаточно будет [написать только интерфейс |factory]: ```php interface ArticleFactory @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -Но мы забегаем вперед, потерпите еще немного :-) +Но мы забегаем вперёд, оставайтесь с нами :-) -Резюме ------- +Итоги +----- -В начале этой главы мы обещали показать вам, как проектировать чистый код. Достаточно классам +В начале этой главы мы пообещали показать порядок проектирования чистого кода. Достаточно позаботиться о том, чтобы классам: -1) [передавать зависимости, которые им нужны |#Правило 1: пусть тебе это передадут] -2) [и наоборот, не передавать то, что им напрямую не нужно |#Правило 2: бери то что твое] -3) [и что объекты с зависимостями лучше всего создавать в фабриках |#Правило 3: оставь это фабрике] +1) [передавались зависимости, которые им нужны |#Правило № 1: пусть вам это передадут] +2) [и наоборот, не передавалось то, что им напрямую не нужно |#Правило № 2: берите своё] +3) [а объекты с зависимостями лучше всего создавать в фабриках |#Правило № 3: пусть этим займётся фабрика] -На первый взгляд это может показаться не так, но эти три правила имеют далеко идущие последствия. Они ведут к радикально иному взгляду на проектирование кода. Стоит ли оно того? Программисты, отбросившие старые привычки и начавшие последовательно использовать dependency injection, считают этот шаг ключевым моментом в своей профессиональной жизни. Им открылся мир понятных и поддерживаемых приложений. +На первый взгляд это может быть неочевидно, но эти три правила имеют далеко идущие последствия. Они приводят к принципиально другому взгляду на проектирование кода. Стоит ли оно того? Программисты, которые оставили старые привычки и стали последовательно использовать Dependency Injection, считают этот шаг поворотным в своей профессиональной карьере. Он открыл им мир ясных и поддерживаемых приложений. -Но что, если код не использует последовательно dependency injection? Что, если он построен на статических методах или синглтонах? Приносит ли это какие-либо проблемы? [Приносит, и очень серьезные |global-state]. +А что, если код не использует Dependency Injection последовательно? Что, если он построен на статических методах или синглтонах? Приводит ли это к проблемам? [Да, и к весьма серьёзным |global-state]. diff --git a/dependency-injection/ru/nette-container.texy b/dependency-injection/ru/nette-container.texy index 781eac7cfa..29d2e44d14 100644 --- a/dependency-injection/ru/nette-container.texy +++ b/dependency-injection/ru/nette-container.texy @@ -1,10 +1,10 @@ -Nette DI Контейнер +Nette DI Container ****************** .[perex] -Nette DI — одна из самых интересных библиотек Nette. Она умеет генерировать и автоматически обновлять скомпилированные DI-контейнеры, которые чрезвычайно быстры и удивительно легко конфигурируются. +Nette DI - одна из самых интересных библиотек Nette. Она умеет порождать и автоматически обновлять скомпилированные DI-контейнеры, которые исключительно быстры и на удивление просты в настройке. -Структуру сервисов, которые должен создавать DI-контейнер, мы обычно определяем с помощью конфигурационных файлов в [формате NEON|neon:format]. Контейнер, который мы вручную создали в [предыдущей главе|container], был бы записан так: +Вид сервисов, которые должен создавать DI-контейнер, обычно задаётся в конфигурационных файлах [формата NEON|neon:format]. Контейнер, который мы вручную создали в [предыдущей главе|container], записывался бы так: ```neon parameters: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -Запись действительно краткая. +Запись очень краткая. -Все зависимости, объявленные в конструкторах классов `ArticleFactory` и `UserController`, Nette DI само обнаружит и передаст благодаря так называемому [autowiring|autowiring], поэтому в конфигурационном файле ничего указывать не нужно. Так что даже если параметры изменятся, вам не придется ничего менять в конфигурации. Контейнер Nette автоматически перегенерируется. Вы можете сосредоточиться исключительно на разработке приложения. +Все зависимости, объявленные в конструкторах классов `ArticleFactory` и `EditController`, Nette DI обнаруживает и передаёт автоматически благодаря так называемому [autowiring|autowiring], поэтому в конфигурационном файле указывать ничего не нужно. Так что даже при изменении параметров менять в конфигурации ничего не придётся. Во время разработки Nette автоматически перегенерирует контейнер. Вы можете сосредоточиться исключительно на разработке приложения. -Если мы хотим передавать зависимости с помощью сеттеров, мы используем для этого секцию [setup |services#Setup]. +Если мы хотим передавать зависимости через сеттеры, для этого служит секция [setup |services#Setup]. -Nette DI генерирует непосредственно PHP-код контейнера. Результатом является файл `.php`, который вы можете открыть и изучить. Благодаря этому вы точно видите, как работает контейнер. Вы также можете отлаживать его в IDE и пошагово выполнять. И главное: сгенерированный PHP чрезвычайно быстр. +Nette DI порождает PHP-код контейнера напрямую. Результатом становится файл `.php`, который вы можете открыть и изучить. Благодаря этому вы видите в точности, как работает контейнер. Вы можете и отлаживать его в своей IDE, проходя по шагам. И, что важнее всего, порождённый PHP-код исключительно быстр. -Nette DI также умеет генерировать код [фабрик|factory] на основе предоставленного интерфейса. Поэтому вместо класса `ArticleFactory` нам достаточно будет создать в приложении только интерфейс: +Nette DI умеет порождать и код [фабрики|factory] на основе заданного интерфейса. Поэтому вместо класса `ArticleFactory` нам достаточно создать в приложении только интерфейс: ```php interface ArticleFactory @@ -36,19 +36,19 @@ interface ArticleFactory } ``` -Полный пример вы найдете [на GitHub|https://github.com/nette-examples/di-example-doc]. +Полный пример вы найдёте [на GitHub|https://github.com/nette-examples/di-example-doc]. Самостоятельное использование ----------------------------- -Внедрение библиотеки Nette DI в приложение очень просто. Сначала установим ее с помощью Composer (потому что скачивание zip-архивов тааак устарело): +Встроить библиотеку Nette DI в приложение очень просто. Сначала установим её через Composer (потому что скачивать zip-файлы давно вышло из моды): ```shell composer require nette/di ``` -Следующий код создает экземпляр DI-контейнера согласно конфигурации, сохраненной в файле `config.neon`: +Следующий код использует [Compiler |api:Nette\DI\Compiler], чтобы создать экземпляр DI-контейнера по конфигурации, хранящейся в файле `config.neon`: ```php $loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -Контейнер генерируется только один раз, его код записывается в кеш (каталог `__DIR__ . '/temp'`) и при последующих запросах просто загружается оттуда. +Контейнер порождается только один раз, его код записывается в кеш (каталог `__DIR__ . '/temp'`), а при последующих запросах только оттуда загружается. -Для создания и получения сервисов служат методы `getService()` или `getByType()`. Так мы создадим объект `UserController`: +Сам по себе `Compiler` включает в конфигурации только секции `services` и `parameters`. Чтобы использовать остальные, такие как `search`, `decorator`, `di` или `inject`, сначала зарегистрируйте их расширения. А чтобы можно было регистрировать расширения из секции `extensions` конфигурации, добавьте `ExtensionsExtension`: ```php -$controller = $container->getByType(UserController::class); +$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(); ``` -Во время разработки полезно активировать режим автообновления, когда контейнер автоматически перегенерируется, если изменяется какой-либо класс или конфигурационный файл. Достаточно указать в конструкторе `ContainerLoader` второй аргумент `true`. +Во время разработки полезно включить режим автообновления, когда контейнер автоматически перегенерируется при изменении любого класса или конфигурационного файла. Достаточно передать `true` вторым аргументом в конструктор [ContainerLoader |api:Nette\DI\ContainerLoader]. ```php $loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); ``` -Использование с фреймворком Nette ---------------------------------- +Работа с контейнером +-------------------- + +Помимо `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, вы можете внедрить его где угодно с помощью всего 3 строк кода. Однако, если вы разрабатываете приложения на Nette Framework, за конфигурацию и создание контейнера отвечает [Bootstrap |application:bootstrapping#Конфигурация DI-контейнера]. +Как мы показали, использование 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 index c798252965..686187b5ed 100644 --- a/dependency-injection/ru/passing-dependencies.texy +++ b/dependency-injection/ru/passing-dependencies.texy @@ -3,22 +3,22 @@ <div class=perex> -Аргументы, или в терминологии DI «зависимости», можно передавать в классы следующими основными способами: +Аргументы, или, в терминологии DI, зависимости, можно передавать классам следующими основными способами: -* передача через конструктор -* передача через метод (так называемый сеттер) -* установка переменной -* методом, аннотацией или атрибутом *inject* +* внедрение через конструктор +* внедрение через метод (так называемое внедрение через сеттер) +* внедрение в свойство +* с помощью метода `inject*()` или атрибута `#[Inject]` </div> -Теперь покажем каждый вариант на конкретных примерах. +Покажем каждый вариант на конкретных примерах. -Передача через конструктор -========================== +Внедрение через конструктор +=========================== -Зависимости передаются в момент создания объекта как аргументы конструктора: +Зависимости передаются как аргументы конструктора в момент создания объекта: ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -Эта форма подходит для обязательных зависимостей, которые класс непременно нуждается для своей работы, так как без них экземпляр создать не получится. +Этот подход подходит для обязательных зависимостей, без которых класс совершенно не может работать, потому что без них экземпляр создать нельзя. -Начиная с PHP 8.0, мы можем использовать более короткую форму записи ([constructor property promotion |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), которая функционально эквивалентна: +Начиная с PHP 8.0 мы можем использовать более краткую запись ([продвижение свойств конструктора |https://blog.nette.org/ru/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), функционально равнозначную: ```php // PHP 8.0 @@ -49,7 +49,7 @@ class MyClass } ``` -Начиная с PHP 8.1, переменную можно пометить флагом `readonly`, который объявляет, что содержимое переменной больше не изменится: +Начиная с PHP 8.1 свойство можно пометить флагом `readonly`, который объявляет, что значение свойства после инициализации меняться не будет: ```php // PHP 8.1 @@ -62,13 +62,13 @@ class MyClass } ``` -DI-контейнер передает зависимости конструктору автоматически с помощью [autowiring |autowiring]. Аргументы, которые таким образом передать нельзя (например, строки, числа, булевы значения), [записываем в конфигурации |services#Аргументы]. +DI-контейнер передаёт зависимости в конструктор автоматически через [autowiring |autowiring]. Аргументы, которые так передать нельзя (например, строки, числа, логические значения), [указываются в конфигурации |services#Аргументы]. Ад конструкторов ---------------- -Термин *constructor hell* (ад конструкторов) обозначает ситуацию, когда потомок наследует от родительского класса, конструктор которого требует зависимости, и в то же время потомок требует зависимости. При этом он должен принять и передать также родительские: +Термином *ад конструкторов* описывают ситуацию, когда дочерний класс наследует от родительского, конструктору которого нужны зависимости, и самому дочернему классу тоже нужны зависимости. Тогда он вынужден принимать и передавать дальше и зависимости родителя: ```php abstract class BaseClass @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -Проблема возникает в момент, когда мы захотим изменить конструктор класса `BaseClass`, например, когда добавится новая зависимость. Тогда необходимо изменить также все конструкторы потомков. Что превращает такое изменение в ад. +Проблема возникает, когда мы хотим изменить конструктор `BaseClass`, например когда добавляется новая зависимость. Тогда приходится менять и все конструкторы дочерних классов. Что превращает такое изменение в ад. -Как этого избежать? Решение — **отдавать предпочтение [композиции перед наследованием |faq#Почему композиция предпочтительнее наследования]**. +Как этого избежать? Решение - **предпочитать [композицию наследованию |faq#Почему композиция предпочтительнее наследования?]**. -То есть спроектируем код иначе. Будем избегать [абстрактным |nette:introduction-to-object-oriented-programming#Абстрактные классы] `Base*` классов. Вместо того чтобы `MyClass` получал определенную функциональность путем наследования от `BaseClass`, он получит эту функциональность как зависимость: +Итак, мы проектируем код иначе. Мы обойдёмся без [абстрактных |nette:introduction-to-object-oriented-programming#Абстрактные классы] классов `Base*`. Вместо того чтобы `MyClass` получал определённую функциональность наследованием от `BaseClass`, эта функциональность будет передана ему как зависимость: ```php final class SomeFunctionality @@ -125,10 +125,10 @@ final class MyClass ``` -Передача сеттером -================= +Внедрение через сеттер +====================== -Зависимости передаются вызовом метода, который сохраняет их в приватную переменную. Обычное соглашение об именовании этих методов — форма `set*()`, поэтому их называют сеттерами, но они, конечно, могут называться как угодно иначе. +Зависимости передаются вызовом метода, который сохраняет их в приватное свойство. Общепринятое соглашение об именовании таких методов - шаблон `set*()`, отсюда и название "сеттеры", но, разумеется, они могут называться иначе. ```php class MyClass @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -Этот способ подходит для необязательных зависимостей, которые не являются необходимыми для работы класса, так как не гарантируется, что объект действительно получит зависимость (т. е. что пользователь вызовет метод). +Этот подход подходит для необязательных зависимостей, без которых класс может работать, потому что нет гарантии, что объект действительно получит зависимость (то есть что вызывающий вызовет метод). -В то же время этот способ позволяет вызывать сеттер повторно и таким образом изменять зависимость. Если это нежелательно, добавим в метод проверку, или с PHP 8.1 пометим свойство `$cache` флагом `readonly`. +При этом такой способ позволяет вызывать сеттер многократно и менять зависимость. Если это нежелательно, добавьте в метод проверку или, начиная с PHP 8.1, пометьте свойство `$cache` флагом `readonly`. ```php class MyClass @@ -164,7 +164,7 @@ class MyClass } ``` -Вызов сеттера определяем в конфигурации DI-контейнера в [ключе setup |services#Setup]. Здесь также используется автоматическая передача зависимостей с помощью autowiring: +Вызов сеттера задаётся в конфигурации DI-контейнера в [ключе setup |services#Setup]. Здесь тоже используется автоматическая передача зависимостей через autowiring: ```neon services: @@ -174,10 +174,10 @@ services: ``` -Установка переменной +Внедрение в свойство ==================== -Зависимости передаются записью непосредственно в переменную-член: +Зависимости передаются записью прямо в свойство объекта: ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -Этот способ считается неподходящим, поскольку переменная-член должна быть объявлена как `public`. Следовательно, у нас нет контроля над тем, что переданная зависимость действительно будет данного типа (действовало до PHP 7.4), и мы теряем возможность реагировать на вновь назначенную зависимость собственным кодом, например, предотвратить последующее изменение. В то же время переменная становится частью публичного интерфейса класса, что может быть нежелательно. +Этот способ считается неудачным, потому что свойство должно быть объявлено как `public`. В результате мы теряем контроль над тем, действительно ли переданная зависимость нужного типа (особенно это было верно до появления объявлений типов свойств в PHP 7.4), и теряем возможность отреагировать на присвоенную зависимость собственной логикой, например запретить последующее изменение. При этом свойство становится частью публичного API класса, чего может и не подразумеваться. -Установку переменной определяем в конфигурации DI-контейнера в [секции setup |services#Setup]: +Присваивание свойства задаётся в конфигурации DI-контейнера в [секции setup |services#Setup]: ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -В то время как предыдущие три способа применимы в целом во всех объектно-ориентированных языках, инъекция методом, аннотацией или атрибутом *inject* специфична исключительно для презентеров в Nette. О них рассказывается в [отдельной главе |best-practices:inject-method-attribute]. +Три предыдущих подхода применимы вообще во всех объектно-ориентированных языках, а внедрение через методы `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 index b6601ef372..ea79c864c7 100644 --- a/dependency-injection/ru/services.texy +++ b/dependency-injection/ru/services.texy @@ -2,16 +2,16 @@ ******************** .[perex] -Конфигурация — это место, где мы учим DI-контейнер, как собирать отдельные сервисы и как связывать их с другими зависимостями. Nette предоставляет очень понятный и элегантный способ достижения этой цели. +В конфигурации мы указываем DI-контейнеру, как создавать отдельные сервисы и как связывать их с зависимостями. Nette предлагает для этого очень наглядный и изящный способ. -Секция `services` в конфигурационном файле формата NEON — это место, где мы определяем собственные сервисы и их конфигурации. Посмотрим на простой пример определения сервиса с именем `database`, который представляет экземпляр класса `PDO`: +Секция `services` в конфигурационном файле NEON - это место, где мы определяем собственные сервисы и их настройку. Посмотрим на простой пример, определяющий сервис с именем `database`, который представляет экземпляр класса `PDO`: ```neon services: database: PDO('sqlite::memory:') ``` -Указанная конфигурация приведет к следующему фабричному методу в [DI-контейнере|container]: +Приведённая конфигурация порождает в [DI-контейнере|container] такой фабричный метод: ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -Имена сервисов позволяют нам ссылаться на них в других частях конфигурационного файла в формате `@имяСервиса`. Если нет необходимости именовать сервис, мы можем просто использовать дефис: +Имена сервисов позволяют ссылаться на них в других частях конфигурационного файла в форме `@имяСервиса`. Если давать сервису имя не нужно, можно просто использовать маркер списка (`-`): ```neon services: - PDO('sqlite::memory:') ``` -Для получения сервиса из DI-контейнера мы можем использовать метод `getService()` с именем сервиса в качестве параметра или метод `getByType()` с типом сервиса: +Чтобы получить сервис из DI-контейнера, можно использовать метод `getService()` с именем сервиса в параметре или метод `getByType()` с типом сервиса: ```php $database = $container->getService('database'); @@ -38,7 +38,7 @@ $database = $container->getByType(PDO::class); Создание сервиса ================ -Обычно мы создаем сервис, просто создавая экземпляр определенного класса. Например: +Обычно мы создаём сервис просто созданием экземпляра конкретного класса. Например: ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -Ключ `create` имеет псевдоним `factory`, оба варианта на практике распространены. Однако мы рекомендуем использовать `create`. +У ключа `create` есть псевдоним `factory`; оба варианта встречаются часто. Однако мы рекомендуем использовать `create`. -Аргументы конструктора или метода создания могут быть альтернативно записаны в ключе `arguments`: +Аргументы конструктора или фабричного метода можно указать и через ключ `arguments`: ```neon services: @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -Сервисы не обязательно должны создаваться только простым созданием экземпляра класса, они также могут быть результатом вызова статических методов или методов других сервисов: +Сервисы не обязательно должны создаваться простым созданием экземпляра класса: они могут быть и результатом вызова статических методов или методов других сервисов: ```neon services: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -Обратите внимание, что для простоты вместо `->` используется `::`, см. [#выразительные средства]. Будут сгенерированы следующие фабричные методы: +Обратите внимание, что ради простоты вместо `->` используется `::`, см. [#Язык выражений]. Будут порождены такие фабричные методы: ```php public function createServiceDatabase(): PDO @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -DI-контейнеру необходимо знать тип созданного сервиса. Если мы создаем сервис с помощью метода, у которого не указан тип возвращаемого значения, мы должны явно указать этот тип в конфигурации: +DI-контейнеру нужно знать тип создаваемого сервиса. Если мы создаём сервис методом, у которого не указан тип возвращаемого значения, мы должны явно объявить этот тип в конфигурации: ```neon services: @@ -100,14 +100,14 @@ services: Аргументы ========= -В конструктор и методы мы передаем аргументы способом, очень похожим на сам PHP: +Аргументы в конструкторы и методы мы передаём очень похоже на то, как это делается в самом PHP: ```neon services: database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) ``` -Для лучшей читаемости мы можем разбить аргументы на отдельные строки. В таком случае использование запятых необязательно: +Ради читаемости аргументы можно перечислить на отдельных строках. В этом случае запятые становятся необязательны: ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -Аргументы также можно именовать, и тогда не нужно беспокоиться об их порядке: +Аргументы можно и именовать, тогда о их порядке заботиться не нужно: ```neon services: @@ -129,20 +129,20 @@ services: ) ``` -Если вы хотите пропустить некоторые аргументы и использовать их значение по умолчанию или подставить сервис с помощью [autowiring|autowiring], используйте подчеркивание: +Если вы хотите опустить какие-то аргументы и использовать их значения по умолчанию либо получить сервис через [autowiring|autowiring], используйте подчёркивание (`_`): ```neon services: foo: Foo(_, %appDir%) ``` -В качестве аргументов можно передавать сервисы, использовать параметры и многое другое, см. [#выразительные средства]. +Аргументами могут быть сервисы, параметры и многое другое, см. [#Язык выражений]. Setup ===== -В секции `setup` мы определяем методы, которые должны вызываться при создании сервиса. +В секции `setup` мы задаём методы, которые нужно вызвать при создании сервиса. ```neon services: @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -Кроме вызова методов, можно также передавать значения в свойства. Поддерживается также добавление элемента в массив, которое необходимо записывать в кавычках, чтобы не конфликтовать с синтаксисом NEON: +Помимо вызова методов можно присваивать значения свойствам. Поддерживается и добавление элементов в массивы, для чего обращение к массиву нужно заключить в кавычки, чтобы не столкнуться с синтаксисом NEON: ```neon services: @@ -174,7 +174,7 @@ services: - '$onClick[]' = [@bar, clickHandler] ``` -Что в PHP-коде выглядело бы следующим образом: +В PHP-коде это выглядело бы так: ```php public function createServiceFoo(): Foo @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -В setup можно также вызывать статические методы или методы других сервисов. Если вам нужно передать в качестве аргумента текущий сервис, укажите его как `@self`: +Впрочем, в setup можно вызывать и статические методы или методы других сервисов. Если вам нужно передать аргументом сам текущий сервис, сошлитесь на него через `@self`: ```neon services: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -Обратите внимание, что для простоты вместо `->` используется `::`, см. [#выразительные средства]. Будет сгенерирован такой фабричный метод: +Обратите внимание, что ради простоты вместо `->` используется `::`, см. [#Язык выражений]. Будет порождён такой фабричный метод: ```php public function createServiceFoo(): Foo @@ -210,23 +210,23 @@ public function createServiceFoo(): Foo ``` -Выразительные средства -====================== +Язык выражений +============== -Nette DI предоставляет нам чрезвычайно богатые выразительные средства, с помощью которых мы можем записать почти все что угодно. В конфигурационных файлах мы можем использовать [параметры |configuration#Параметры]: +Nette DI предлагает исключительно богатый язык выражений, с помощью которого можно определить почти что угодно. В конфигурационных файлах мы можем использовать [параметры |configuration#Параметры]: ```neon # параметр %wwwDir% -# значение параметра под ключом +# значение параметра по ключу %mailer.user% # параметр внутри строки '%wwwDir%/images' ``` -Далее создавать объекты, вызывать методы и функции: +Кроме того, создавать объекты, вызывать методы и функции: ```neon # создание объекта @@ -235,11 +235,11 @@ DateTime() # вызов статического метода Collator::create(%locale%) -# вызов PHP функции +# вызов функции PHP ::getenv(DB_USER) ``` -Ссылаться на сервисы либо по их имени, либо по типу: +Ссылаться на сервисы по имени или по типу: ```neon # сервис по имени @@ -252,7 +252,7 @@ Collator::create(%locale%) Использовать синтаксис first-class callable: .{data-version:3.2.0} ```neon -# создание callback, аналог [@user, logout] +# создание callback, равнозначно [@user, logout] @user::logout(...) ``` @@ -262,11 +262,21 @@ Collator::create(%locale%) # константа класса FilesystemIterator::SKIP_DOTS -# глобальную константу получим PHP функцией constant() -::constant(PHP_VERSION) +# получение глобальной константы функцией PHP constant() +::constant(\PHP_VERSION) ``` -Вызовы методов можно объединять в цепочку так же, как в PHP. Только для простоты вместо `->` используется `::`: +Обращаться к публичным свойствам и константам сервиса через `@service::member`. Является ли имя свойством или константой, решает его первая буква: строчная означает публичное свойство, заглавная - константу: + +```neon +# публичное свойство сервиса (начинается со строчной буквы) +@settings::apiUrl + +# константа класса сервиса (начинается с заглавной буквы) +@settings::Version +``` + +Вызовы методов можно объединять в цепочку, как в PHP. Ради простоты вместо `->` используется `::`: ```neon DateTime()::format('Y-m-d') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -Эти выражения можно использовать где угодно, при [создании сервисов |#Создание сервиса], в [аргументах |#Аргументы], в секции [#setup] или [параметрах |configuration#Параметры]: +Эти выражения можно использовать где угодно: при [создании сервисов |#Создание сервиса], в [аргументах |#Аргументы], в секции [setup |#Setup] или в [параметрах |configuration#Параметры]: ```neon parameters: @@ -293,12 +303,12 @@ services: Специальные функции ------------------- -В конфигурационных файлах вы можете использовать эти специальные функции: +В конфигурационных файлах можно использовать следующие специальные функции: -- `not()` отрицание значения -- `bool()`, `int()`, `float()`, `string()` преобразование типа без потерь -- `typed()` создает массив всех сервисов указанного типа -- `tagged()` создает массив всех сервисов с данным тегом +- `not()` отрицает значение +- `bool()`, `int()`, `float()`, `string()` приведение к указанному типу без потерь .{data-version:3.0.5} +- `typed()` создаёт массив всех сервисов указанного типа +- `tagged()` создаёт массив всех сервисов с заданным тегом ```neon services: @@ -308,18 +318,18 @@ services: ) ``` -В отличие от классического приведения типов в PHP, такого как `(int)`, преобразование без потерь вызовет исключение для нечисловых значений. +В отличие от обычного приведения PHP вроде `(int)`, приведение без потерь выбрасывает исключение для нечисловых значений. -Функция `typed()` создает массив всех сервисов данного типа (класс или интерфейс). Она пропускает сервисы, у которых отключен autowiring. Можно указать несколько типов, разделенных запятой. +Функция `typed()` создаёт массив всех сервисов указанного типа (класса или интерфейса). Она пропускает сервисы, у которых отключён autowiring. Можно указать и несколько типов через запятую. ```neon services: - BarsDependent( typed(Bar) ) ``` -Массив сервисов определенного типа можно передавать как аргумент также автоматически с помощью [autowiring |autowiring#Массив сервисов]. +Массив сервисов определённого типа можно передать аргументом и автоматически через [autowiring |autowiring#Набор сервисов]. -Функция `tagged()` создает массив всех сервисов с определенным тегом. Здесь также можно указать несколько тегов, разделенных запятой. +Функция `tagged()` создаёт массив всех сервисов с определённым тегом. Здесь тоже можно указать несколько тегов через запятую. ```neon services: @@ -330,20 +340,20 @@ services: Autowiring ========== -Ключ `autowired` позволяет влиять на поведение autowiring для конкретного сервиса. Для деталей см. [главу об autowiring|autowiring]. +Ключ `autowired` позволяет повлиять на поведение autowiring для конкретного сервиса. Подробности см. в [главе об autowiring|autowiring]. ```neon services: foo: create: Foo - autowired: false # сервис foo исключен из autowiring + autowired: false # сервис foo исключён из autowiring ``` Ленивые сервисы .{data-version:3.2.4} ===================================== -Ленивая загрузка (Lazy loading) — это техника, которая откладывает создание сервиса до момента, когда он действительно необходим. В глобальной конфигурации можно [включить ленивое создание |configuration#Ленивые сервисы] для всех сервисов сразу. Для отдельных сервисов можно переопределить это поведение: +Ленивая загрузка - приём, откладывающий создание сервиса до момента, когда он действительно понадобится. В глобальной конфигурации вы можете [включить ленивое создание |configuration#Ленивые сервисы] сразу для всех сервисов. Для отдельных сервисов это поведение можно переопределить: ```neon services: @@ -352,16 +362,20 @@ services: lazy: false ``` -Когда сервис определен как ленивый, при его запросе из DI-контейнера мы получаем специальный объект-заместитель. Он выглядит и ведет себя так же, как реальный сервис, но фактическая инициализация (вызов конструктора и setup) происходит только при первом вызове любого его метода или свойства. +Когда сервис определён как ленивый, при запросе из DI-контейнера мы получаем специальный объект-заместитель. Этот заместитель выглядит и ведёт себя точно как настоящий сервис, но настоящая инициализация (вызов конструктора и настроек) происходит только при первом обращении к какому-либо из его методов или свойств. + +Учтите, что раз сервис создаётся позже, то и ошибки в его конфигурации проявляются позже. Например, неверные учётные данные базы данных обнаружатся не при старте приложения, а только при первом запросе. + +Ленивое создание к тому же смягчает круговые зависимости, то есть ситуацию, когда сервис A требует сервис B, а B одновременно требует A. Без него контейнер сообщает об ошибке `Circular reference detected`. С ленивым заместителем сервис A получает лишь заместителя сервиса B, который инициализируется тогда, когда действительно используется, в момент, когда A уже существует. Тем не менее круговая зависимость сигнализирует об ошибочной архитектуре, и от неё лучше избавиться. .[note] -Ленивая загрузка может использоваться только для пользовательских классов, а не для внутренних классов PHP. Требуется PHP 8.4 или новее. +Ленивая загрузка требует PHP 8.4 или новее и работает только для сервисов, создаваемых прямым созданием экземпляра класса (например, `create: Foo`), но не для тех, что создаются фабричным методом. Её также нельзя использовать для классов, которые в конечном счёте наследуют от внутреннего класса PHP. Когда ленивую загрузку применить нельзя, флаг `lazy: true` молча игнорируется. Теги ==== -Теги служат для добавления дополнительной информации к сервисам. Сервису можно добавить один или несколько тегов: +Теги служат для добавления к сервисам дополнительных сведений. Сервису можно назначить один или несколько тегов: ```neon services: @@ -371,7 +385,7 @@ services: - cached ``` -Теги также могут нести значения: +Теги могут нести и значения: ```neon services: @@ -381,26 +395,26 @@ services: logger: monolog.logger.event ``` -Чтобы получить все сервисы с определенными тегами, вы можете использовать функцию `tagged()`: +Чтобы получить все сервисы, связанные с определёнными тегами, можно использовать функцию `tagged()`: ```neon services: - LoggersDependent( tagged(logger) ) ``` -В DI-контейнере вы можете получить имена всех сервисов с определенным тегом с помощью метода `findByTag()`: +Внутри DI-контейнера имена всех сервисов с определённым тегом можно получить методом `findByTag()`: ```php $names = $container->findByTag('logger'); -// $names - это массив, содержащий имя сервиса и значение тега +// $names - массив, где ключи это имена сервисов, а значения это значения тега // например, ['foo' => 'monolog.logger.event', ...] ``` -Режим Inject +Режим inject ============ -С помощью флага `inject: true` активируется передача зависимостей через публичные переменные с аннотацией [inject |best-practices:inject-method-attribute#Атрибуты Inject] и методы [inject*() |best-practices:inject-method-attribute#Методы inject]. +Флаг `inject: true` включает внедрение зависимостей через публичные свойства с атрибутом [Inject |best-practices:inject-method-attribute#Атрибуты Inject] и через методы [inject*() |best-practices:inject-method-attribute#Методы inject*()]. ```neon services: @@ -409,13 +423,13 @@ services: inject: true ``` -По умолчанию `inject` активирован только для презентеров. +По умолчанию режим `inject` включён только для презентеров. -Модификация сервисов -==================== +Изменение сервисов +================== -DI-контейнер содержит множество сервисов, которые были добавлены с помощью встроенного или [пользовательского расширения|extensions]. Вы можете изменять определения этих сервисов прямо в конфигурации. Например, вы можете изменить класс сервиса `application.application`, который по умолчанию является `Nette\Application\Application`, на другой: +DI-контейнер содержит множество сервисов, добавленных встроенными или [пользовательскими расширениями|extensions]. Определения этих существующих сервисов можно изменить прямо в конфигурации. Например, вы можете поменять класс сервиса `application.application`, которым по умолчанию является `Nette\Application\Application`, на другой: ```neon services: @@ -424,9 +438,9 @@ services: alteration: true ``` -Флаг `alteration` является информативным и указывает, что мы только модифицируем существующий сервис. +Флаг `alteration` указывает, что мы лишь изменяем существующий сервис. Он же служит страховкой: если изменяемого сервиса не существует, компиляция завершится исключением. -Мы также можем дополнить setup: +Мы можем и дополнить setup: ```neon services: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -При переопределении сервиса мы можем захотеть удалить исходные аргументы, элементы setup или теги, для чего служит `reset`: +Определять сервис по его внутреннему имени необязательно, вместо этого можно сослаться на него по типу. Предыдущий пример можно записать и так: + +```neon +services: + @Nette\Application\Application: + create: MyApplication +``` + +Изменяя сервис, мы можем захотеть убрать исходные аргументы, элементы setup или теги с помощью ключа `reset`: ```neon services: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -Если вы хотите удалить сервис, добавленный расширением, вы можете сделать это так: +Если вы хотите удалить сервис, добавленный расширением, это делается так: ```neon services: diff --git a/dependency-injection/ru/upgrading.texy b/dependency-injection/ru/upgrading.texy new file mode 100644 index 0000000000..7a5e6806e9 --- /dev/null +++ b/dependency-injection/ru/upgrading.texy @@ -0,0 +1,49 @@ +Обновление +********** + + +Обновление до версии 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/sl/@home.texy b/dependency-injection/sl/@home.texy deleted file mode 100644 index e6abd1050d..0000000000 --- a/dependency-injection/sl/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection je načrtovalski vzorec, ki bo bistveno spremenil vaš pogled na kodo in razvoj. Odprl vam bo pot v svet čisto načrtovanih in vzdržljivih aplikacij. - -- [Kaj je Dependency Injection? |introduction] -- [Globalno stanje in singletoni |global-state] -- [Posredovanje odvisnosti |passing-dependencies] -- [Kaj je DI vsebnik? |container] -- [Pogosto zastavljena vprašanja|faq] - - -Paket `nette/di` ponuja izjemno napreden kompiliran DI vsebnik za PHP. - -- [Nette DI Vsebnik |nette-container] -- [Konfiguracija |configuration] -- [Definiranje storitev |services] -- [Autowiring |autowiring] -- [Generirane tovarne |factory] -- [Ustvarjanje razširitev za Nette DI|extensions] diff --git a/dependency-injection/sl/@left-menu.texy b/dependency-injection/sl/@left-menu.texy deleted file mode 100644 index 70ce6dceb5..0000000000 --- a/dependency-injection/sl/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Kaj je DI? |introduction] -- [Globalno stanje in singletoni |global-state] -- [Posredovanje odvisnosti |passing-dependencies] -- [Kaj je DI vsebnik? |container] -- [Pogosto zastavljena vprašanja|faq] - - -Nette DI --------- -- [Nette DI Vsebnik |nette-container] -- [Konfiguracija |configuration] -- [Definiranje storitev |services] -- [Autowiring |autowiring] -- [Generirane tovarne |factory] -- [Ustvarjanje razširitev za Nette DI|extensions] diff --git a/dependency-injection/sl/@meta.texy b/dependency-injection/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/dependency-injection/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/dependency-injection/sl/autowiring.texy b/dependency-injection/sl/autowiring.texy deleted file mode 100644 index 6f70c0b6e5..0000000000 --- a/dependency-injection/sl/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring je odlična lastnost, ki zna samodejno posredovati v konstruktor in druge metode zahtevane storitve, tako da jih sploh ni treba pisati. Prihrani vam veliko časa. - -Zahvaljujoč temu lahko izpustimo večino argumentov pri pisanju definicij storitev. Namesto: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Zadostuje napisati: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring se ravna po tipih, zato mora biti za delovanje razred `ArticleRepository` definiran približno takole: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Da bi lahko uporabili autowiring, mora za vsak tip v vsebniku obstajati **točno ena storitev**. Če bi jih bilo več, autowiring ne bi vedel, katero naj posreduje, in bi vrgel izjemo: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # VRŽE IZJEMO, ustrezata mainDb in tempDb -``` - -Rešitev bi bila bodisi obiti autowiring in eksplicitno navesti ime storitve (tj. `articles: Model\ArticleRepository(@mainDb)`). Pametneje pa je autowiring ene od storitev [izklopiti |#Izklop autowiringa] ali prvo storitev [dati prednost |#Prednost autowiringa]. - - -Izklop autowiringa ------------------- - -Autowiring storitve lahko izklopimo z uporabo možnosti `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # storitev tempDb je izključena iz autowiringa - - articles: Model\ArticleRepository # zato posreduje v konstruktor mainDb -``` - -Storitev `articles` ne bo vrgla izjeme, da obstajata dve ustrezni storitvi tipa `PDO` (tj. `mainDb` in `tempDb`), ki ju je mogoče posredovati v konstruktor, ker vidi samo storitev `mainDb`. - -.[note] -Konfiguracija autowiringa v Nette deluje drugače kot v Symfonyju, kjer možnost `autowire: false` pove, da se autowiring ne sme uporabljati za argumente konstruktorja dane storitve. V Nette se autowiring uporablja vedno, bodisi za argumente konstruktorja ali katere koli druge metode. Možnost `autowired: false` pove, da instanca dane storitve ne sme biti nikamor posredovana z uporabo autowiringa. - - -Prednost autowiringa --------------------- - -Če imamo več storitev istega tipa in pri eni od njih navedemo možnost `autowired`, postane ta storitev prednostna: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # postane prednostna - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Storitev `articles` ne bo vrgla izjeme, da obstajata dve ustrezni storitvi tipa `PDO` (tj. `mainDb` in `tempDb`), ampak bo uporabila prednostno storitev, torej `mainDb`. - - -Polje storitev --------------- - -Autowiring zna posredovati tudi polja storitev določenega tipa. Ker v PHP ni mogoče nativno zapisati tipa elementov polja, je treba poleg tipa `array` dopolniti tudi phpDoc komentar s tipom elementa v obliki `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI vsebnik nato samodejno posreduje polje storitev, ki ustrezajo danemu tipu. Izpusti storitve, ki imajo izklopljen autowiring. - -Tip v komentarju je lahko tudi v obliki `array<int, Class>` ali `list<Class>`. Če ne morete vplivati na obliko phpDoc komentarja, lahko polje storitev posredujete neposredno v konfiguraciji z uporabo [`typed()` |services#Posebne funkcije]. - - -Skalarni argumenti ------------------- - -Autowiring zna vstavljati samo objekte in polja objektov. Skalarne argumente (npr. nize, števila, booleane) [zapišemo v konfiguraciji |services#Argumenti]. Alternativa je ustvariti [settings-objekt |best-practices:passing-settings-to-presenters], ki skalarno vrednost (ali več vrednosti) zapakira v obliko objekta, ki ga nato lahko spet posredujemo z uporabo autowiringa. - -```php -class MySettings -{ - public function __construct( - // readonly je mogoče uporabiti od PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Iz njega ustvarite storitev z dodajanjem v konfiguracijo: - -```neon -services: - - MySettings('any value') -``` - -Vsi razredi jo nato zahtevajo z uporabo autowiringa. - - -Omejitev autowiringa --------------------- - -Posameznim storitvam lahko autowiring omejimo samo na določene razrede ali vmesnike. - -Običajno autowiring storitev posreduje v vsak parameter metode, katerega tipu storitev ustreza. Omejitev pomeni, da določimo pogoje, ki jim morajo ustrezati tipi, navedeni pri parametrih metod, da jim bo storitev posredovana. - -Poglejmo si to na primeru: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Če bi jih vse registrirali kot storitve, bi autowiring spodletel: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # VRŽE IZJEMO, ustrezata storitvi parent in child - childDep: ChildDependent # autowiring posreduje v konstruktor storitev child -``` - -Storitev `parentDep` vrže izjemo `Multiple services of type ParentClass found: parent, child`, ker v njen konstruktor ustrezata obe storitvi `parent` in `child`, in autowiring ne more odločiti, katero naj izbere. - -Pri storitvi `child` lahko zato omejimo njen autowiring na tip `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # lahko napišemo tudi 'autowired: self' - - parentDep: ParentDependent # autowiring posreduje v konstruktor storitev parent - childDep: ChildDependent # autowiring posreduje v konstruktor storitev child -``` - -Zdaj se v konstruktor storitve `parentDep` posreduje storitev `parent`, ker je zdaj to edini ustrezen objekt. Storitve `child` autowiring tja ne posreduje več. Da, storitev `child` je še vedno tipa `ParentClass`, vendar ne velja več omejitveni pogoj, dan za tip parametra, tj. ne velja, da je `ParentClass` *nadtip* `ChildClass`. - -Pri storitvi `child` bi bilo mogoče `autowired: ChildClass` zapisati tudi kot `autowired: self`, ker je `self` nadomestno ime za razred trenutne storitve. - -V ključu `autowired` je mogoče navesti tudi več razredov ali vmesnikov kot polje: - -```neon -autowired: [BarClass, FooInterface] -``` - -Poskusimo primer dopolniti še z vmesniki: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Če storitve `child` nikakor ne omejimo, bo ustrezala konstruktorjem vseh razredov `FooDependent`, `BarDependent`, `ParentDependent` in `ChildDependent`, in autowiring jo bo tja posredoval. - -Če pa njen autowiring omejimo na `ChildClass` z `autowired: ChildClass` (ali `self`), jo bo autowiring posredoval samo v konstruktor `ChildDependent`, ker zahteva argument tipa `ChildClass` in velja, da je `ChildClass` *tipa* `ChildClass`. Noben drug tip, naveden pri drugih parametrih, ni nadtip `ChildClass`, zato se storitev ne posreduje. - -Če jo omejimo na `ParentClass` z `autowired: ParentClass`, jo bo autowiring spet posredoval v konstruktor `ChildDependent` (ker je zahtevani `ChildClass` nadtip `ParentClass`) in na novo tudi v konstruktor `ParentDependent`, ker je zahtevani tip `ParentClass` prav tako ustrezen. - -Če jo omejimo na `FooInterface`, bo še vedno avtomatsko povezana v `ParentDependent` (zahtevani `ParentClass` je nadtip `FooInterface`) in `ChildDependent`, poleg tega pa tudi v konstruktor `FooDependent`, vendar ne v `BarDependent`, ker `BarInterface` ni nadtip `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring posreduje v konstruktor child - barDep: BarDependent # VRŽE IZJEMO, nobena storitev ne ustreza - parentDep: ParentDependent # autowiring posreduje v konstruktor child - childDep: ChildDependent # autowiring posreduje v konstruktor child -``` diff --git a/dependency-injection/sl/configuration.texy b/dependency-injection/sl/configuration.texy deleted file mode 100644 index d4b773c1a5..0000000000 --- a/dependency-injection/sl/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Konfiguracija DI vsebnika -************************* - -.[perex] -Pregled konfiguracijskih možnosti za Nette DI vsebnik. - - -Konfiguracijska datoteka -======================== - -Nette DI vsebnik se enostavno upravlja s konfiguracijskimi datotekami. Te se običajno zapisujejo v [formatu NEON|neon:format]. Za urejanje priporočamo [urejevalnike s podporo |best-practices:editors-and-tools#IDE urejevalnik] za ta format. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Dekorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI vsebnik .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Razširitve]: "Namestitev dodatnih DI razširitev .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Vključevanje datotek]: "Vključevanje datotek .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parametri]: "Parametri .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Iskanje]: "Samodejna registracija storitev .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Storitve .[prism-token prism-comment]" -</pre> - -.[note] -Če želite zapisati niz, ki vsebuje znak `%`, ga morate ubežati z podvojitvijo na `%%`. - - -Parametri -========= - -V konfiguraciji lahko definirate parametre, ki jih lahko nato uporabite kot del definicij storitev. S tem lahko naredite konfiguracijo preglednejšo ali združite in izločite vrednosti, ki se bodo spreminjale. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Na parameter `dsn` se sklicujemo kjerkoli v konfiguraciji z zapisom `%dsn%`. Parametre lahko uporabljamo tudi znotraj nizov kot `'%wwwDir%/images'`. - -Parametri niso nujno samo nizi ali števila, lahko vsebujejo tudi polja: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Na določen ključ se sklicujemo kot `%mailer.user%`. - -Če potrebujete v vaši kodi, na primer v razredu, ugotoviti vrednost katerega koli parametra, ga posredujte v ta razred. Na primer v konstruktorju. Ne obstaja noben globalni objekt, ki bi predstavljal konfiguracijo, katerega bi razredi spraševali za vrednosti parametrov. To bi bilo kršenje načela dependency injection. - - -Storitve -======== - -Glej [samostojno poglavje|services]. - - -Decorator -========= - -Kako množično urediti vse storitve določenega tipa? Na primer poklicati določeno metodo pri vseh presenterjih, ki dedujejo od določenega skupnega prednika? Za to je tu decorator. - -```neon -decorator: - # pri vseh storitvah, ki so instanca tega razreda ali vmesnika - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # pokliči to metodo - - $absoluteUrls = true # in nastavi spremenljivko -``` - -Decorator se lahko uporablja tudi za nastavitev [oznak |services#Oznake] ali vklop načina [inject |services#Način Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Tehnične nastavitve DI vsebnika. - -```neon -di: - # prikazati DIC v Tracy Bar? - debugger: ... # (bool) privzeto je true - - # tipi parametrov, ki jih nikoli ne avtomatsko povezovati - excluded: ... # (string[]) - - # dovoliti leno ustvarjanje storitev? - lazy: ... # (bool) privzeto je false - - # razred, od katerega deduje DI vsebnik - parentClass: ... # (string) privzeto je Nette\DI\Container -``` - - -Lene storitve .{data-version:3.2.4} ------------------------------------ - -Nastavitev `lazy: true` aktivira leno (odloženo) ustvarjanje storitev. To pomeni, da storitve niso dejansko ustvarjene v trenutku, ko jih zahtevamo iz DI vsebnika, ampak šele v trenutku njihove prve uporabe. To lahko pospeši zagon aplikacije in zmanjša pomnilniške zahteve, saj se ustvarijo samo tiste storitve, ki so v danem zahtevku dejansko potrebne. - -Pri določeni storitvi lahko leno ustvarjanje [spremenimo |services#Lazy storitve]. - -.[note] -Lene objekte je mogoče uporabiti samo za uporabniške razrede, ne pa za interne PHP razrede. Zahteva PHP 8.4 ali novejšo različico. - - -Izvoz metapodatkov ------------------- - -Razred DI vsebnika vsebuje tudi veliko metapodatkov. Lahko ga zmanjšate tako, da zmanjšate izvoz metapodatkov. - -```neon -di: - export: - # izvoziti parametre? - parameters: false # (bool) privzeto je true - - # izvoziti oznake in katere? - tags: # (string[]|bool) privzeto so vse - - event.subscriber - - # izvoziti podatke za autowiring in katere? - types: # (string[]|bool) privzeto so vsi - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Če ne uporabljate polja `$container->getParameters()`, lahko izklopite izvoz parametrov. Nadalje lahko izvozite samo tiste oznake, prek katerih pridobivate storitve z metodo `$container->findByTag(...)`. Če metode sploh ne kličete, lahko popolnoma izklopite izvoz oznak z `false`. - -Znatno lahko zmanjšate metapodatke za [samodejnim povezovanjem |autowiring] tako, da navedete razrede, ki jih uporabljate kot parameter metode `$container->getByType()`. In spet, če metode sploh ne kličete (oz. samo v [bootstrapu|application:bootstrapping] za pridobitev `Nette\Application\Application`), lahko izvoz popolnoma izklopite z `false`. - - -Razširitve -========== - -Registracija dodatnih DI razširitev. Na ta način dodamo npr. DI razširitev `Dibi\Bridges\Nette\DibiExtension22` pod imenom `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Nato jo torej konfiguriramo v sekciji `dibi`: - -```neon -dibi: - host: localhost -``` - -Kot razširitev lahko dodamo tudi razred, ki ima parametre: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Vključevanje datotek -==================== - -Druge konfiguracijske datoteke lahko vključimo v sekciji `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Ime `parameters.php` ni napaka, konfiguracija je lahko zapisana tudi v PHP datoteki, ki jo vrne kot polje: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Če se v konfiguracijskih datotekah pojavijo elementi z enakimi ključi, bodo prepisani ali v primeru [polj združeni |#Združevanje]. Kasneje vključena datoteka ima višjo prioriteto kot prejšnja. Datoteka, v kateri je navedena sekcija `includes`, ima višjo prioriteto kot v njej vključene datoteke. - - -Iskanje -======= - -Samodejno dodajanje storitev v DI vsebnik izjemno olajša delo. Nette samodejno dodaja v vsebnik presenterje, vendar je mogoče enostavno dodajati tudi katere koli druge razrede. - -Zadostuje navesti, v katerih mapah (in podmapah) naj išče razrede: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Običajno pa ne želimo dodati popolnoma vseh razredov in vmesnikov, zato jih lahko filtriramo: - -```neon -search: - - in: %appDir%/Forms - - # filtriranje po imenu datoteke (string|string[]) - files: - - *Factory.php - - # filtriranje po imenu razreda (string|string[]) - classes: - - *Factory -``` - -Ali pa lahko izberemo razrede, ki dedujejo ali implementirajo vsaj enega od navedenih razredov: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Lahko definiramo tudi izključujoča pravila, tj. maske imena razreda ali dedne prednike, ki če ustrezajo, se storitev v DI vsebnik ne doda: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Vsem storitvam lahko nastavimo oznake: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Združevanje -=========== - -Če se v več konfiguracijskih datotekah pojavijo elementi z enakimi ključi, bodo prepisani ali v primeru polj združeni. Kasneje vključena datoteka ima višjo prioriteto kot prejšnja. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Pri poljih lahko preprečimo združevanje z navedbo klicaja za imenom ključa: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Konfiguracija Dependency Injection}} diff --git a/dependency-injection/sl/container.texy b/dependency-injection/sl/container.texy deleted file mode 100644 index 6fd042ac1c..0000000000 --- a/dependency-injection/sl/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Kaj je DI vsebnik? -****************** - -.[perex] -Dependency injection vsebnik (DIC) je razred, ki zna instancirati in konfigurirati objekte. - -Morda vas bo presenetilo, toda v mnogih primerih ne potrebujete dependency injection vsebnika, da bi lahko izkoristili prednosti dependency injection (kratko DI). Saj smo si tudi v [uvodnem poglavju|introduction] na konkretnih primerih DI pokazali in noben vsebnik ni bil potreben. - -Če pa morate upravljati veliko število različnih objektov z mnogimi odvisnostmi, bo dependency injection vsebnik resnično koristen. Kar je na primer primer spletnih aplikacij, zgrajenih na ogrodju. - -V prejšnjem poglavju smo si predstavili razreda `Article` in `UserController`. Oba imata neke odvisnosti, in sicer podatkovno bazo in tovarno `ArticleFactory`. In za te razrede si zdaj ustvarimo vsebnik. Seveda za tako preprost primer nima smisla imeti vsebnika. Ampak ga bomo ustvarili, da si pokažemo, kako izgleda in deluje. - -Tukaj je preprost hardcoded vsebnik za navedeni primer: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -Uporaba bi izgledala takole: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Vsebniku samo vprašamo za objekt in že nam ni treba vedeti ničesar o tem, kako ga ustvariti in kakšne ima odvisnosti; vse to ve vsebnik. Odvisnosti so z vsebnikom injicirane samodejno. V tem je njegova moč. - -Vsebnik ima zaenkrat zapisane vse podatke trdo kodirano. Naredimo torej naslednji korak in dodajmo parametre, da bo vsebnik resnično koristen: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Bistri bralci so morda opazili določeno težavo. Vsakič, ko pridobim objekt `UserController`, se ustvari tudi nova instanca `ArticleFactory` in podatkovne baze. Tega zagotovo nočemo. - -Dodajmo zato metodo `getService()`, ki bo vračala vedno iste instance: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') bo klical createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Pri prvem klicu npr. `$container->getService('Database')` si pusti od `createDatabase()` ustvariti objekt podatkovne baze, ki ga shrani v polje `$services` in pri naslednjem klicu ga takoj vrne. - -Prilagodimo tudi preostanek vsebnika, da bo uporabljal `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Mimogrede, izraz storitev se nanaša na kateri koli objekt, ki ga upravlja vsebnik. Zato tudi ime metode `getService()`. - -Končano. Imamo popolnoma funkcionalen DI vsebnik! In lahko ga uporabimo: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Kot vidite, napisati DIC ni nič zapletenega. Omeniti velja, da sami objekti ne vedo, da jih ustvarja nek vsebnik. S tem je mogoče tako ustvarjati kateri koli objekt v PHP brez posega v njegovo izvorno kodo. - -Ročno ustvarjanje in vzdrževanje razreda vsebnika se lahko precej hitro spremeni v nočno moro. V naslednjem poglavju si zato povemo o [Nette DI Containeru|nette-container], ki se zna generirati in posodabljati skoraj sam. - - -{{maintitle: Kaj je dependency injection vsebnik?}} diff --git a/dependency-injection/sl/extensions.texy b/dependency-injection/sl/extensions.texy deleted file mode 100644 index 56ee1b9806..0000000000 --- a/dependency-injection/sl/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Ustvarjanje razširitev za Nette DI -********************************** - -.[perex] -Generiranje DI vsebnika poleg konfiguracijskih datotek vplivajo še t.i. *razširitve*. Aktiviramo jih v konfiguracijski datoteki v sekciji `extensions`. - -Tako dodamo razširitev, predstavljeno z razredom `BlogExtension`, pod imenom `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Vsaka razširitev kompilerja deduje od [api:Nette\DI\CompilerExtension] in lahko implementira naslednje metode, ki so postopoma klicane med sestavljanjem DI vsebnika: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Ta metoda se kliče prva. Definira shemo za validacijo konfiguracijskih parametrov. - -Razširitev konfiguriramo v sekciji, katere ime je enako tistemu, pod katerim je bila razširitev dodana, torej `blog`: - -```neon -# enako ime kot ima extension -blog: - postsPerPage: 10 - allowComments: false -``` - -Ustvarimo shemo, ki opisuje vse konfiguracijske možnosti, vključno z njihovimi tipi, dovoljenimi vrednostmi in po potrebi tudi privzetimi vrednostmi: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -Dokumentacijo najdete na strani [Shema |schema:]. Poleg tega lahko določimo, katere možnosti so lahko [dinamične |application:bootstrapping#Dinamični parametri] z uporabo `dynamic()`, npr. `Expect::int()->dynamic()`. - -Do konfiguracije dostopamo prek spremenljivke `$this->config`, ki je objekt `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Uporablja se za dodajanje storitev v vsebnik. Za to služi [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Konvencija je, da storitve, dodane z razširitvijo, predponamo z njenim imenom, da ne pride do konflikta imen. To počne metoda `prefix()`, tako da če se razširitev imenuje `blog`, bo storitev nosila ime `blog.articles`. - -Če moramo storitev preimenovati, lahko zaradi ohranjanja povratne združljivosti ustvarimo alias s prvotnim imenom. Podobno to počne Nette npr. pri storitvi `routing.router`, ki je dostopna tudi pod prejšnjim imenom `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Nalaganje storitev iz datoteke ------------------------------- - -Storitve ne ustvarjamo samo z API-jem razreda ContainerBuilder, ampak tudi z znanim zapisom, uporabljenim v konfiguracijski datoteki NEON v sekciji services. Predpona `@extension` predstavlja trenutno razširitev. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Storitve naložimo: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // nalaganje konfiguracijske datoteke za razširitev - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Metoda se kliče v trenutku, ko vsebnik vsebuje vse storitve, dodane z posameznimi razširitvami v metodah `loadConfiguration` in tudi z uporabniškimi konfiguracijskimi datotekami. V tej fazi sestavljanja torej lahko definicije storitev urejamo ali dopolnimo povezave med njimi. Za iskanje storitev v vsebniku po oznakah lahko uporabimo metodo `findByTag()`, po razredu ali vmesniku pa metodo `findByType()`. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -V tej fazi je razred vsebnika že generiran v obliki objekta [ClassType |php-generator:#Razredi], vsebuje vse metode, ki ustvarjajo storitve, in je pripravljen za zapis v predpomnilnik. Rezultatno kodo razreda lahko v tej točki še vedno urejamo. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Razred Configurator po [ustvarjanju vsebnika |application:bootstrapping#index.php] kliče inicializacijsko kodo, ki se ustvarja z zapisom v objekt `$this->initialization` z uporabo [metode addBody() |php-generator:#Telesa metod in funkcij]. - -Pokažimo si primer, kako na primer z inicializacijsko kodo zagnati sejo ali zagnati storitve, ki imajo oznako `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // samodejni zagon seje - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // storitve z oznako run morajo biti ustvarjene po instanciranju vsebnika - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/sl/factory.texy b/dependency-injection/sl/factory.texy deleted file mode 100644 index f4cd79285c..0000000000 --- a/dependency-injection/sl/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Generirane tovarne -****************** - -.[perex] -Nette DI zna samodejno generirati kodo tovarn na podlagi vmesnikov, kar vam prihrani pisanje kode. - -Tovarna je razred, ki izdeluje in konfigurira objekte. Posreduje jim torej tudi njihove odvisnosti. Ne zamenjujte prosim z načrtovalskim vzorcem *factory method*, ki opisuje specifičen način uporabe tovarn in s to temo ni povezan. - -Kako taka tovarna izgleda, smo si pokazali v [uvodnem poglavju |introduction#Tovarna]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI zna kodo tovarn samodejno generirati. Vse, kar morate storiti, je ustvariti vmesnik in Nette DI bo generiral implementacijo. Vmesnik mora imeti točno eno metodo z imenom `create` in deklarirati povratni tip: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Torej tovarna `ArticleFactory` ima metodo `create`, ki ustvarja objekte `Article`. Razred `Article` lahko izgleda na primer takole: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Tovarno dodamo v konfiguracijsko datoteko: - -```neon -services: - - ArticleFactory -``` - -Nette DI bo generiral ustrezno implementacijo tovarne. - -V kodi, ki tovarno uporablja, tako zahtevamo objekt po vmesniku in Nette DI bo uporabil generirano implementacijo: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // pustimo tovarni ustvariti objekt - $article = $this->articleFactory->create(); - } -} -``` - - -Parametrizirana tovarna -======================= - -Tovarniška metoda `create` lahko sprejema parametre, ki jih nato posreduje v konstruktor. Dopolnimo na primer razred `Article` z ID avtorja članka: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Parameter dodamo tudi v tovarno: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Zahvaljujoč temu, da se parameter v konstruktorju in parameter v tovarni imenujeta enako, jih Nette DI popolnoma samodejno posreduje. - - -Napredna definicija -=================== - -Definicijo lahko zapišemo tudi v večvrstični obliki z uporabo ključa `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Pri zapisu na ta daljši način je mogoče navesti dodatne argumente za konstruktor v ključu `arguments` in dopolnilno konfiguracijo z uporabo `setup`, enako kot pri običajnih storitvah. - -Primer: če metoda `create()` ne bi sprejemala parametra `$authorId`, bi lahko navedli fiksno vrednost v konfiguraciji, ki bi se posredovala v konstruktor `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Ali obratno, če bi `create()` parameter `$authorId` sprejemala, vendar ne bi bil del konstruktorja in bi se posredoval z metodo `Article::setAuthorId()`, bi se nanj sklicevali v sekciji `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette zna poleg tovarn generirati tudi t.i. accessorje. Gre za objekte z metodo `get()`, ki vrača določeno storitev iz DI vsebnika. Ponavljajoči klic `get()` vrača vedno isto instanco. - -Accessorji zagotavljajo odvisnostim lazy-loading. Imejmo razred, ki zapisuje napake v posebno podatkovno bazo. Če bi si ta razred pustil povezavo z podatkovno bazo posredovati kot odvisnost prek konstruktorja, bi se morala povezava vedno ustvariti, čeprav se v praksi napaka pojavi le izjemoma in bi torej večinoma povezava ostala neizkoriščena. Namesto tega si razred posreduje accessor in šele ko se pokliče njegov `get()`, pride do ustvarjanja objekta podatkovne baze: - -Kako ustvariti accessor? Zadostuje napisati vmesnik in Nette DI bo generiral implementacijo. Vmesnik mora imeti točno eno metodo z imenom `get` in deklarirati povratni tip: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Accessor dodamo v konfiguracijsko datoteko, kjer je tudi definicija storitve, ki jo bo vračal: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Ker accessor vrača storitev tipa `PDO` in je v konfiguraciji edina taka storitev, bo vračal prav njo. Če bi bilo storitev danega tipa več, določimo vračano storitev z imenom, npr. `- PDOAccessor(@db1)`. - - -Večkratna tovarna/accessor -========================== -Naše tovarne in accessorji so doslej vedno znali izdelovati ali vračati samo en objekt. Lahko pa zelo enostavno ustvarimo tudi večkratne tovarne, kombinirane z accessorji. Vmesnik takega razreda bo vseboval poljubno število metod z imeni `create<name>()` in `get<name>()`, npr.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Torej namesto da bi si posredovali več generiranih tovarn in accessorjev, posredujemo eno kompleksnejšo tovarno, ki zna več. - -Alternativno lahko namesto več metod uporabimo `get()` s parametrom: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Potem velja, da `MultiFactory::getArticle()` počne isto kot `MultiFactoryAlt::get('article')`. Vendar ima alternativni zapis to slabost, da ni očitno, katere vrednosti `$name` so podprte in logično tudi ni mogoče v vmesniku ločiti različnih povratnih vrednosti za različne `$name`. - - -Definicija s seznamom ---------------------- -Na ta način lahko definiramo večkratno tovarno v konfiguraciji: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # definira createArticle() - db: PDO(%dsn%, %user%, %password%) # definira getDb() - ) -``` - -Ali pa se lahko v definiciji tovarne sklicujemo na obstoječe storitve z referenco: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definira createArticle() - db: @\PDO # definira getDb() - ) -``` - - -Definicija z oznakami ---------------------- - -Druga možnost je uporaba [oznak |services#Oznake] za definicijo: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/sl/faq.texy b/dependency-injection/sl/faq.texy deleted file mode 100644 index b8e129a93b..0000000000 --- a/dependency-injection/sl/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Pogosto zastavljena vprašanja o DI (FAQ) -**************************************** - - -Je DI drugo ime za IoC? ------------------------ - -*Inversion of Control* (IoC) je načelo, osredotočeno na način, kako se koda izvaja - ali vaša koda izvaja tujo ali je vaša koda integrirana v tujo, ki jo nato kliče. IoC je širok pojem, ki vključuje [dogodke |nette:glossary#Dogodki eventi], tako imenovani [Hollywoodski princip |application:components#Hollywood style] in druge vidike. Del tega koncepta so tudi tovarne, o katerih govori [Pravilo št. 3: pusti tovarni |introduction#Pravilo št. 3: prepusti tovarni], in ki predstavljajo inverzijo za operator `new`. - -*Dependency Injection* (DI) se osredotoča na način, kako en objekt izve za drug objekt, torej za njegove odvisnosti. Gre za načrtovalski vzorec, ki zahteva eksplicitno posredovanje odvisnosti med objekti. - -Lahko torej rečemo, da je DI specifična oblika IoC. Vendar niso vse oblike IoC primerne z vidika čistosti kode. Na primer, med antivzorci so tehnike, ki delujejo z [globalnim stanjem |global-state] ali tako imenovani [Service Locator |#Kaj je Service Locator]. - - -Kaj je Service Locator? ------------------------ - -Gre za alternativo Dependency Injection. Deluje tako, da ustvari centralno shrambo, kjer so registrirane vse razpoložljive storitve ali odvisnosti. Ko objekt potrebuje odvisnost, zanjo prosi Service Locator. - -V primerjavi z Dependency Injection pa izgublja na transparentnosti: odvisnosti niso objektom posredovane neposredno in niso tako enostavno prepoznavne, kar zahteva pregled kode, da bi bile vse povezave odkrite in razumljene. Testiranje je prav tako bolj zapleteno, ker ne moremo preprosto posredovati mock objektov testiranim objektom, ampak moramo iti prek Service Locatorja. Poleg tega Service Locator krši načrtovanje kode, saj morajo posamezni objekti vedeti za njegov obstoj, kar se razlikuje od Dependency Injection, kjer objekti nimajo vedenja o DI vsebniku. - - -Kdaj je bolje DI ne uporabiti? ------------------------------- - -Niso znane nobene težave, povezane z uporabo načrtovalskega vzorca Dependency Injection. Nasprotno, pridobivanje odvisnosti iz globalno dostopnih mest vodi k [celi vrsti zapletov |global-state], enako velja za uporabo Service Locatorja. Zato je primerno uporabljati DI vedno. To ni dogmatski pristop, ampak preprosto ni bila najdena boljša alternativa. - -Kljub temu obstajajo določene situacije, ko si objektov ne posredujemo in jih pridobimo iz globalnega prostora. Na primer pri razhroščevanju kode, ko morate na določeni točki programa izpisati vrednost spremenljivke, izmeriti trajanje določenega dela programa ali zabeležiti sporočilo. V takih primerih, ko gre za začasna dejanja, ki bodo kasneje odstranjena iz kode, je legitimno uporabiti globalno dostopen dumper, štoparico ali logger. Ti orodji namreč ne spadajo k načrtovanju kode. - - -Ima uporaba DI svoje slabe strani? ----------------------------------- - -Ali uporaba Dependency Injection prinaša kakšne slabosti, kot na primer povečano zahtevnost pisanja kode ali poslabšano zmogljivost? Kaj izgubimo, ko začnemo pisati kodo v skladu z DI? - -DI nima vpliva na zmogljivost ali pomnilniške zahteve aplikacije. Določeno vlogo lahko igra zmogljivost DI Containerja, vendar v primeru [Nette DI |nette-container] je vsebnik preveden v čisti PHP, tako da je njegova režija med izvajanjem aplikacije v bistvu nična. - -Pri pisanju kode je včasih treba ustvarjati konstruktorje, ki sprejemajo odvisnosti. Prej je to lahko bilo dolgotrajno, vendar je zahvaljujoč sodobnim IDE in [constructor property promotion |https://blog.nette.org/sl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] zdaj vprašanje nekaj sekund. Tovarne lahko enostavno generiramo z Nette DI in vtičnikom za PhpStorm s klikom miške. Po drugi strani odpade potreba po pisanju singletonov in statičnih dostopnih točk. - -Lahko ugotovimo, da pravilno načrtovana aplikacija, ki uporablja DI, v primerjavi z aplikacijo, ki uporablja singletone, ni niti krajša niti daljša. Deli kode, ki delajo z odvisnostmi, so le izvzeti iz posameznih razredov in premaknjeni na nova mesta, torej v DI vsebnik in tovarne. - - -Kako prenoviti staro aplikacijo na DI? --------------------------------------- - -Prehod s stare aplikacije na Dependency Injection je lahko zahteven proces, zlasti pri velikih in kompleksnih aplikacijah. Pomembno je, da k temu procesu pristopimo sistematično. - -- Pri prehodu na Dependency Injection je pomembno, da vsi člani ekipe razumejo načela in postopke, ki se uporabljajo. -- Najprej izvedite analizo obstoječe aplikacije in identificirajte ključne komponente ter njihove odvisnosti. Ustvarite načrt, kateri deli bodo refaktorirani in v kakšnem vrstnem redu. -- Implementirajte DI vsebnik ali še bolje uporabite obstoječo knjižnico, na primer Nette DI. -- Postopoma refaktorirajte posamezne dele aplikacije, da bodo uporabljali Dependency Injection. To lahko vključuje prilagoditve konstruktorjev ali metod tako, da sprejemajo odvisnosti kot parametre. -- Prilagodite mesta v kodi, kjer se ustvarjajo objekti z odvisnostmi, da bodo namesto tega odvisnosti injicirane z vsebnikom. To lahko vključuje uporabo tovarn. - -Ne pozabite, da je prehod na Dependency Injection naložba v kakovost kode in dolgoročno vzdržljivost aplikacije. Čeprav je lahko zahtevno izvesti te spremembe, bi moral biti rezultat čistejša, bolj modularna in enostavno testirana koda, ki je pripravljena za prihodnje razširitve in vzdrževanje. - - -Zakaj se daje prednost kompoziciji pred dedovanjem? ---------------------------------------------------- -Primerneje je uporabljati [kompozicijo |nette:introduction-to-object-oriented-programming#Kompozicija] namesto [dedovanja |nette:introduction-to-object-oriented-programming#Dedovanje], ker služi za ponovno uporabo kode, ne da bi se morali ukvarjati s posledicami sprememb. Zagotavlja torej ohlapnejšo povezavo, pri kateri se nam ni treba bati, da bo sprememba neke kode povzročila potrebo po spremembi druge odvisne kode. Tipičen primer je situacija, označena kot [constructor hell |passing-dependencies#Constructor hell]. - - -Ali je mogoče uporabiti Nette DI Container zunaj Nette? -------------------------------------------------------- - -Vsekakor. Nette DI Container je del Nette, vendar je zasnovan kot samostojna knjižnica, ki jo je mogoče uporabiti neodvisno od drugih delov ogrodja. Zadostuje jo namestiti z Composerjem, ustvariti konfiguracijsko datoteko z definicijo vaših storitev in nato z nekaj vrsticami PHP kode ustvariti DI vsebnik. In takoj lahko začnete izkoriščati prednosti Dependency Injection v svojih projektih. - -Kako izgleda konkretna uporaba, vključno s kodami, opisuje poglavje [Nette DI Container |nette-container]. - - -Zakaj je konfiguracija v NEON datotekah? ----------------------------------------- - -NEON je preprost in lahko berljiv konfiguracijski jezik, ki je bil razvit v okviru Nette za nastavitev aplikacij, storitev in njihovih odvisnosti. V primerjavi z JSONom ali YAMLom ponuja za ta namen veliko bolj intuitivne in fleksibilne možnosti. V NEONu je mogoče naravno opisati povezave, ki jih v Symfony & YAMLu ne bi bilo mogoče zapisati bodisi sploh, bodisi le prek zapletenega opisa. - - -Ali razčlenjevanje NEON datotek upočasnjuje aplikacijo? -------------------------------------------------------- - -Čeprav se datoteke NEON razčlenjujejo zelo hitro, ta vidik sploh ni pomemben. Razlog je, da se razčlenjevanje datotek zgodi samo enkrat ob prvem zagonu aplikacije. Nato se generira koda DI vsebnika, shrani se na disk in se zažene ob vsakem naslednjem zahtevku, ne da bi bilo treba izvajati nadaljnje razčlenjevanje. - -Tako to deluje v produkcijskem okolju. Med razvojem se NEON datoteke razčlenjujejo vsakič, ko pride do spremembe njihove vsebine, da ima razvijalec vedno aktualen DI vsebnik. Samo razčlenjevanje je, kot je bilo rečeno, vprašanje trenutka. - - -Kako iz svojega razreda dostopam do parametrov v konfiguracijski datoteki? --------------------------------------------------------------------------- - -Imejmo v mislih [Pravilo št. 1: naj ti posredujejo |introduction#Pravilo št. 1: naj ti bo predano]. Če razred zahteva informacije iz konfiguracijske datoteke, nam ni treba razmišljati, kako do teh informacij priti, namesto tega jih preprosto zahtevamo - na primer prek konstruktorja razreda. In posredovanje izvedemo v konfiguracijski datoteki. - -V tej predstavitvi je `%myParameter%` nadomestni znak za vrednost parametra `myParameter`, ki se posreduje v konstruktor razreda `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Če želite posredovati več parametrov ali izkoristiti autowiring, je primerno [parametre zapakirati v objekt |best-practices:passing-settings-to-presenters]. - - -Ali Nette podpira PSR-11: Container interface? ----------------------------------------------- - -Nette DI Container ne podpira PSR-11 neposredno. Vendar, če potrebujete interoperabilnost med Nette DI Containerjem in knjižnicami ali ogrodji, ki pričakujejo PSR-11 Container Interface, lahko ustvarite [preprost adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], ki bo služil kot most med Nette DI Containerjem in PSR-11. diff --git a/dependency-injection/sl/global-state.texy b/dependency-injection/sl/global-state.texy deleted file mode 100644 index 960d8c9cb9..0000000000 --- a/dependency-injection/sl/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Globalno stanje in singletoni -***************************** - -.[perex] -Opozorilo: Naslednje konstrukcije so znak slabo načrtovane kode: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ali `static::$var` - -Ali se nekatere od teh konstrukcij pojavljajo v vaši kodi? Potem imate priložnost za njeno izboljšanje. Morda si mislite, da gre za običajne konstrukcije, ki jih vidite na primer tudi v vzorčnih rešitvah različnih knjižnic in ogrodij. Če je temu tako, potem načrtovanje njihove kode ni dobro. - -Zdaj zagotovo ne govorimo o neki akademski čistosti. Vse te konstrukcije imajo eno skupno: izkoriščajo globalno stanje. In to ima uničujoč vpliv na kakovost kode. Razredi lažejo o svojih odvisnostih. Koda postane nepredvidljiva. Zmede programerje in zmanjšuje njihovo učinkovitost. - -V tem poglavju si bomo razložili, zakaj je temu tako in kako se globalnemu stanju izogniti. - - -Globalna povezanost -------------------- - -V idealnem svetu bi moral objekt biti sposoben komunicirati samo z objekti, ki so mu bili [neposredno posredovani |passing-dependencies]. Če ustvarim dva objekta `A` in `B` in nikoli ne posredujem reference med njima, potem se niti `A` niti `B` ne moreta dostopati do drugega objekta ali spremeniti njegovega stanja. To je zelo zaželena lastnost kode. Podobno je, kot če imate baterijo in žarnico; žarnica ne bo svetila, dokler je z baterijo ne povežete z žico. - -To pa ne velja pri globalnih (statičnih) spremenljivkah ali singletonih. Objekt `A` bi se lahko *brezžično* dostopal do objekta `C` in ga modificiral brez kakršnega koli posredovanja reference, s klicem `C::changeSomething()`. Če se objekt `B` prav tako oprime globalnega `C`, potem se `A` in `B` lahko medsebojno vplivata prek `C`. - -Uporaba globalnih spremenljivk v sistem vnaša novo obliko *brezžične* povezanosti, ki od zunaj ni vidna. Ustvarja dimno zaveso, ki otežuje razumevanje in uporabo kode. Da bi razvijalci odvisnosti resnično razumeli, morajo prebrati vsako vrstico izvorne kode. Namesto zgolj seznanitve z vmesnikom razredov. Gre poleg tega za popolnoma nepotrebno povezanost. Globalno stanje se uporablja zato, ker je enostavno dostopno od kjerkoli in omogoča na primer zapis v podatkovno bazo prek globalne (statične) metode `DB::insert()`. Ampak kot si bomo pokazali, je prednost, ki jo to prinaša, neznatna, nasprotno pa povzroča usodne zaplete. - -.[note] -Z vidika obnašanja ni razlike med globalno in statično spremenljivko. Sta enako škodljivi. - - -Strašljivo delovanje na daljavo -------------------------------- - -"Strašljivo delovanje na daljavo" - tako je slavno leta 1935 Albert Einstein poimenoval pojav v kvantni fiziki, ki mu je naganjal kurjo polt. -Gre za kvantno prepletenost, katere posebnost je, da ko izmerite informacijo o enem delcu, s tem takoj vplivate na drugi delec, tudi če sta med seboj oddaljena milijone svetlobnih let. Kar navidezno krši osnovni zakon vesolja, da se nič ne more širiti hitreje od svetlobe. - -V svetu programske opreme lahko "strašljivo delovanje na daljavo" poimenujemo situacijo, ko zaženemo nek proces, za katerega menimo, da je izoliran (ker mu nismo posredovali nobenih referenc), vendar na oddaljenih mestih sistema pride do nepričakovanih interakcij in sprememb stanja, o katerih nismo imeli pojma. Do tega lahko pride samo prek globalnega stanja. - -Predstavljajte si, da se pridružite ekipi razvijalcev projekta, ki ima obsežno napredno kodno bazo. Vaš novi vodja vas prosi za implementacijo nove funkcije in vi kot pravi razvijalec začnete s pisanjem testa. Ker pa ste v projektu novi, delate veliko raziskovalnih testov tipa "kaj se zgodi, če pokličem to metodo". In poskusite napisati naslednji test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // številka vaše kartice - $cc->charge(100); -} -``` - -Zaženete kodo, morda večkrat, in po nekem času opazite na mobilnem telefonu obvestila iz banke, da se je ob vsakem zagonu odštelo 100 dolarjev z vaše plačilne kartice 🤦‍♂️ - -Kako za vraga je lahko test povzročil dejansko odtegnitev denarja? Upravljanje s plačilno kartico ni enostavno. Morate komunicirati s spletno storitvijo tretje osebe, morate poznati URL te spletne storitve, morate se prijaviti in tako naprej. Nobena od teh informacij ni vsebovana v testu. Še huje, niti ne veste, kje so te informacije prisotne, in torej niti kako mockati zunanje odvisnosti, da vsak zagon ne bi vodil k temu, da se ponovno odšteje 100 dolarjev. In kako ste kot novi razvijalec morali vedeti, da bo to, kar se pripravljate storiti, vodilo k temu, da boste za 100 dolarjev revnejši? - -To je strašljivo delovanje na daljavo! - -Ne preostane vam drugega, kot da se dolgo prebijate skozi veliko izvorne kode, sprašujete starejše in izkušenejše kolege, preden razumete, kako povezave v projektu delujejo. To je posledica tega, da ob pogledu na vmesnik razreda `CreditCard` ni mogoče ugotoviti globalnega stanja, ki ga je treba inicializirati. Celo pogled v izvorno kodo razreda vam ne bo razkril, katero inicializacijsko metodo morate poklicati. V najboljšem primeru lahko najdete globalno spremenljivko, do katere se dostopa, in iz nje poskusite uganiti, kako jo inicializirati. - -Razredi v takem projektu so patološki lažnivci. Plačilna kartica se pretvarja, da jo zadostuje instancirati in poklicati metodo `charge()`. Skrito pa sodeluje z drugim razredom `PaymentGateway`, ki predstavlja plačilni prehod. Tudi njen vmesnik pravi, da jo je mogoče inicializirati samostojno, vendar v resnici potegne poverilnice iz neke konfiguracijske datoteke in tako naprej. Razvijalcem, ki so to kodo napisali, je jasno, da `CreditCard` potrebuje `PaymentGateway`. Kodo so napisali na ta način. Ampak za vsakogar, ki je v projektu nov, je to popolna uganka in ovira učenje. - -Kako situacijo popraviti? Enostavno. **Pustite API-ju, da deklarira odvisnosti.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Opazite, kako so naenkrat povezave znotraj kode očitne. S tem, ko metoda `charge()` deklarira, da potrebuje `PaymentGateway`, vam ni treba nikogar spraševati, kako je koda povezana. Veste, da morate ustvariti njeno instanco, in ko to poskusite, naletite na to, da morate dodati dostopne parametre. Brez njih kode ne bi bilo mogoče niti zagnati. - -In predvsem zdaj lahko plačilni prehod mockate, tako da se vam ob vsakem zagonu testa ne bo zaračunalo 100 dolarjev. - -Globalno stanje povzroča, da se vaši objekti lahko skrivaj dostopajo do stvari, ki niso deklarirane v njihovem API-ju, in posledično delajo iz vaših API-jev patološke lažnivce. - -Morda o tem prej niste tako razmišljali, ampak kadarkoli uporabljate globalno stanje, ustvarjate skrivne brezžične komunikacijske kanale. Strašljivo delovanje na daljavo sili razvijalce, da berejo vsako vrstico kode, da bi razumeli potencialne interakcije, zmanjšuje produktivnost razvijalcev in zmede nove člane ekipe. Če ste vi tisti, ki ste kodo ustvarili, poznate dejanske odvisnosti, ampak vsakdo, ki pride za vami, je nemočen. - -Ne pišite kode, ki izkorišča globalno stanje, dajte prednost posredovanju odvisnosti. Torej dependency injection. - - -Krhkost globalnega stanja -------------------------- - -V kodi, ki uporablja globalno stanje in singletone, nikoli ni gotovo, kdaj in kdo je to stanje spremenil. To tveganje se pojavlja že pri inicializaciji. Naslednja koda naj bi ustvarila povezavo s podatkovno bazo in inicializirala plačilni prehod, vendar nenehno meče izjemo in iskanje vzroka je izjemno dolgotrajno: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Morate podrobno pregledovati kodo, da ugotovite, da objekt `PaymentGateway` brezžično dostopa do drugih objektov, od katerih nekateri zahtevajo povezavo s podatkovno bazo. Torej je treba inicializirati podatkovno bazo prej kot `PaymentGateway`. Vendar dimna zavesa globalnega stanja to pred vami skriva. Koliko časa bi prihranili, če API posameznih razredov ne bi lagal in bi deklariral svoje odvisnosti? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Podobna težava se pojavlja tudi pri uporabi globalnega dostopa do povezave s podatkovno bazo: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Pri klicu metode `save()` ni gotovo, ali je bila povezava s podatkovno bazo že ustvarjena in kdo nosi odgovornost za njeno ustvarjanje. Če želimo na primer spreminjati povezavo s podatkovno bazo med izvajanjem, na primer zaradi testov, bi morali najverjetneje ustvariti dodatne metode, kot na primer `DB::reconnect(...)` ali `DB::reconnectForTest()`. - -Razmislimo o primeru: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Kje imamo gotovost, da se pri klicu `$article->save()` res uporablja testna podatkovna baza? Kaj če je metoda `Foo::doSomething()` spremenila globalno povezavo s podatkovno bazo? Za ugotovitev bi morali pregledati izvorno kodo razreda `Foo` in verjetno tudi mnogih drugih razredov. Ta pristop bi prinesel le kratkoročen odgovor, saj se situacija lahko v prihodnosti spremeni. - -In kaj če povezavo s podatkovno bazo premaknemo v statično spremenljivko znotraj razreda `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -S tem se sploh nič ni spremenilo. Težava je globalno stanje in popolnoma vseeno je, v katerem razredu se skriva. V tem primeru, enako kot v prejšnjem, nimamo pri klicu metode `$article->save()` nobenega namiga o tem, v katero bazo podatkov se bo zapisalo. Kdorkoli na drugem koncu aplikacije je lahko kadarkoli z `Article::setDb()` bazo podatkov spremenil. Nam pod rokami. - -Globalno stanje naredi našo aplikacijo **izjemno krhko**. - -Obstaja pa preprost način, kako se s to težavo spopasti. Zadostuje, da API deklarira odvisnosti, s čimer se zagotovi pravilna funkcionalnost. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Zahvaljujoč temu pristopu odpade skrb za skrite in nepričakovane spremembe povezave z bazo podatkov. Zdaj imamo gotovost, kam se članek shranjuje in nobene spremembe kode znotraj druge nepovezane razreda že ne morejo situacije spremeniti. Koda ni več krhka, ampak stabilna. - -Ne pišite kode, ki izkorišča globalno stanje, dajte prednost posredovanju odvisnosti. Torej dependency injection. - - -Singleton ---------- - -Singleton je načrtovalski vzorec, ki po "definiciji":https://en.wikipedia.org/wiki/Singleton_pattern iz znane publikacije Gang of Four omejuje razred na eno samo instanco in ponuja globalni dostop do nje. Implementacija tega vzorca se običajno podobna naslednji kodi: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // in druge metode, ki opravljajo funkcije danega razreda -} -``` - -Na žalost singleton v aplikacijo uvaja globalno stanje. In kot smo si pokazali zgoraj, je globalno stanje nezaželeno. Zato je singleton obravnavan kot antipattern. - -Ne uporabljajte v svoji kodi singletonov in jih nadomestite z drugimi mehanizmi. Singletonov resnično ne potrebujete. Če pa morate zagotoviti obstoj ene same instance razreda za celotno aplikacijo, pustite to [DI vsebniku |container]. Ustvarite tako aplikacijski singleton, ali storitev. S tem se razred preneha ukvarjati z zagotavljanjem svoje lastne edinstvenosti (tj. ne bo imel metode `getInstance()` in statične spremenljivke) in bo opravljal samo svoje funkcije. Tako ne bo več kršil načela ene same odgovornosti. - - -Globalno stanje proti testom ----------------------------- - -Pri pisanju testov predpostavljamo, da je vsak test izolirana enota in da vanj ne vstopa nobeno zunanje stanje. In nobeno stanje testov ne zapušča. Po zaključku testa bi moralo biti vse povezano stanje s testom samodejno odstranjeno z garbage collectorjem. Zahvaljujoč temu so testi izolirani. Zato lahko teste izvajamo v poljubnem vrstnem redu. - -Če pa so prisotna globalna stanja/singletoni, se vse te prijetne predpostavke razblinijo. Stanje lahko vstopa v test in izstopa iz njega. Naenkrat lahko postane pomemben vrstni red testov. - -Da bi sploh lahko testirali singletone, morajo razvijalci pogosto sprostiti njihove lastnosti, na primer tako, da dovolijo zamenjavo instance z drugo. Take rešitve so v najboljšem primeru hack, ki ustvarja težko vzdržljivo in razumljivo kodo. Vsak test ali metoda `tearDown()`, ki vpliva na katero koli globalno stanje, mora te spremembe vrniti nazaj. - -Globalno stanje je največja bolečina pri unit testiranju! - -Kako situacijo popraviti? Enostavno. Ne pišite kode, ki izkorišča singletone, dajte prednost posredovanju odvisnosti. Torej dependency injection. - - -Globalne konstante ------------------- - -Globalno stanje se ne omejuje samo na uporabo singletonov in statičnih spremenljivk, ampak se lahko nanaša tudi na globalne konstante. - -Konstante, katerih vrednost nam ne prinaša nobene nove (`M_PI`) ali koristne (`PREG_BACKTRACK_LIMIT_ERROR`) informacije, so nedvomno v redu. Nasprotno pa konstante, ki služijo kot način, kako *brezžično* posredovati informacijo znotraj kode, niso nič drugega kot skrita odvisnost. Kot na primer `LOG_FILE` v naslednjem primeru. Uporaba konstante `FILE_APPEND` je popolnoma pravilna. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -V tem primeru bi morali deklarirati parameter v konstruktorju razreda `Foo`, da postane del API-ja: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Zdaj lahko posredujemo informacijo o poti do datoteke za beleženje in jo enostavno spreminjamo po potrebi, kar olajša testiranje in vzdrževanje kode. - - -Globalne funkcije in statične metode ------------------------------------- - -Želimo poudariti, da sama uporaba statičnih metod in globalnih funkcij ni problematična. Razložili smo, v čem je neprimernost uporabe `DB::insert()` in podobnih metod, vendar je vedno šlo le za zadevo globalnega stanja, ki je shranjeno v neki statični spremenljivki. Metoda `DB::insert()` zahteva obstoj statične spremenljivke, ker je v njej shranjena povezava z bazo podatkov. Brez te spremenljivke bi bilo nemogoče metodo implementirati. - -Uporaba determinističnih statičnih metod in funkcij, kot na primer `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` in mnogih drugih, je v popolnem skladu z dependency injection. Te funkcije vedno vračajo enake rezultate iz enakih vhodnih parametrov in so torej predvidljive. Ne uporabljajo nobenega globalnega stanja. - -Obstajajo pa tudi funkcije v PHP, ki niso deterministične. K njim spada na primer funkcija `htmlspecialchars()`. Njen tretji parameter `$encoding`, če ni naveden, ima kot privzeto vrednost vrednost konfiguracijske možnosti `ini_get('default_charset')`. Zato se priporoča ta parameter vedno navesti in preprečiti morebitno nepredvidljivo obnašanje funkcije. Nette to dosledno počne. - -Nekatere funkcije, kot na primer `strtolower()`, `strtoupper()` in podobne, so se v nedavni preteklosti nedeterministično obnašale in bile odvisne od nastavitve `setlocale()`. To je povzročalo veliko zapletov, najpogosteje pri delu s turškim jezikom. Ta namreč razlikuje malo in veliko črko `I` s piko in brez pike. Tako je `strtolower('I')` vračalo znak `ı` in `strtoupper('i')` znak `İ`, kar je vodilo k temu, da so aplikacije začele povzročati vrsto skrivnostnih napak. Ta težava pa je bila odpravljena v PHP različici 8.2 in funkcije niso več odvisne od locale. - -Gre za lep primer, kako je globalno stanje mučilo na tisoče razvijalcev po vsem svetu. Rešitev je bila zamenjava z dependency injection. - - -Kdaj je mogoče uporabiti globalno stanje? ------------------------------------------ - -Obstajajo določene specifične situacije, ko je mogoče izkoristiti globalno stanje. Na primer pri razhroščevanju kode, ko morate izpisati vrednost spremenljivke ali izmeriti trajanje določenega dela programa. V takih primerih, ki se nanašajo na začasna dejanja, ki bodo kasneje odstranjena iz kode, je mogoče legitimno izkoristiti globalno dostopen dumper ali štoparico. Ti orodji namreč niso del načrtovanja kode. - -Drug primer so funkcije za delo z regularnimi izrazi `preg_*`, ki interno shranjujejo prevedene regularne izraze v statični predpomnilnik v pomnilniku. Ko torej kličete isti regularni izraz večkrat na različnih mestih kode, se prevede samo enkrat. Predpomnilnik varčuje z zmogljivostjo in hkrati je za uporabnika popolnoma neviden, zato lahko tako uporabo štejemo za legitimno. - - -Povzetek --------- - -Pregledali smo, zakaj ima smisel: - -1) Odstraniti vse statične spremenljivke iz kode -2) Deklarirati odvisnosti -3) In uporabljati dependency injection - -Ko razmišljate o načrtovanju kode, mislite na to, da vsak `static $foo` predstavlja težavo. Da bi vaša koda bila okolje, ki spoštuje DI, je nujno popolnoma izkoreniniti globalno stanje in ga nadomestiti z dependency injection. - -Med tem procesom morda ugotovite, da je treba razred razdeliti, ker ima več kot eno odgovornost. Ne bojte se tega; prizadevajte si za načelo ene same odgovornosti. - -*Rad bi se zahvalil Mišku Heveryju, čigar članki, kot je [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], so osnova tega poglavja.* diff --git a/dependency-injection/sl/introduction.texy b/dependency-injection/sl/introduction.texy deleted file mode 100644 index 2a3c0bdcc6..0000000000 --- a/dependency-injection/sl/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Kaj je Vbrizgavanje odvisnosti? -******************************* - -.[perex] -To poglavje vas bo seznanilo z osnovnimi programerskimi postopki, ki jih morate upoštevati pri pisanju vseh aplikacij. Gre za osnove, potrebne za pisanje čiste, razumljive in vzdržljive kode. - -Če boste ta pravila sprejeli in jih upoštevali, vam bo Nette v vsakem koraku pomagal. Za vas bo reševal rutinske naloge in vam zagotovil maksimalno udobje, da se boste lahko osredotočili na samo logiko. - -Principi, ki jih bomo tukaj predstavili, so precej preprosti. Ničesar se vam ni treba bati. - - -Se spomnite svojega prvega programa? ------------------------------------- - -Ne vemo sicer, v katerem jeziku ste ga napisali, a če bi bil to PHP, bi verjetno izgledal nekako takole: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // izpiše 24 -``` - -Nekaj trivialnih vrstic kode, a v njih se skriva toliko ključnih konceptov. Da obstajajo spremenljivke. Da se koda deli na manjše enote, kot so na primer funkcije. Da jim predajamo vhodne argumente in one vračajo rezultate. Manjkajo le še pogoji in zanke. - -To, da funkciji predamo vhodne podatke in ona vrne rezultat, je popolnoma razumljiv koncept, ki se uporablja tudi na drugih področjih, kot na primer v matematiki. - -Funkcija ima svojo signaturo, ki jo sestavljajo njeno ime, seznam parametrov in njihovih tipov ter na koncu tip vrnjene vrednosti. Kot uporabnike nas zanima signatura, o notranji implementaciji običajno ne potrebujemo vedeti ničesar. - -Zdaj si predstavljajte, da bi signatura funkcije izgledala takole: - -```php -function soucet(float $x): float -``` - -Seštevanje z enim parametrom? To je čudno… Kaj pa takole? - -```php -function soucet(): float -``` - -To pa je že res zelo čudno, kajne? Kako se funkcija sploh uporablja? - -```php -echo soucet(); // kaj naj bi izpisalo? -``` - -Ob pogledu na takšno kodo bi bili zmedeni. Ne samo, da je ne bi razumel začetnik, takšne kode ne razume niti izkušen programer. - -Razmišljate, kako bi takšna funkcija sploh izgledala znotraj? Kje bi vzela seštevance? Očitno bi si jih *na nek način* priskrbela sama, na primer takole: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -V telesu funkcije smo odkrili skrite povezave na druge globalne funkcije ali statične metode. Da bi ugotovili, od kod se seštevanci dejansko vzamejo, moramo raziskovati naprej. - - -Tako ne! --------- - -Načrt, ki smo ga pravkar predstavili, je bistvo mnogih negativnih lastnosti: - -- signatura funkcije se je pretvarjala, da ne potrebuje seštevancev, kar nas je zmedlo -- sploh ne vemo, kako funkcijo pripraviti do tega, da sešteje drugi dve števili -- morali smo pogledati v kodo, da bi ugotovili, kje vzame seštevance -- odkrili smo skrite povezave -- za popolno razumevanje je treba preučiti tudi te povezave - -In ali je sploh naloga seštevalne funkcije, da si priskrbi vhode? Seveda ni. Njena odgovornost je le samo seštevanje. - - -S takšno kodo se nočemo srečati in je zagotovo nočemo pisati. Popravek je pri tem preprost: vrniti se k osnovam in preprosto uporabiti parametre: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Pravilo št. 1: naj ti bo predano --------------------------------- - -Najpomembnejše pravilo se glasi: **vsi podatki, ki jih funkcije ali razredi potrebujejo, jim morajo biti predani**. - -Namesto da bi si izmišljali skrite načine, s katerimi bi lahko sami prišli do njih, preprosto predajte parametre. Prihranili boste čas, potreben za izmišljanje skritih poti, ki zagotovo ne bodo izboljšale vaše kode. - -Če boste to pravilo vedno in povsod upoštevali, ste na poti h kodi brez skritih povezav. H kodi, ki je razumljiva ne samo avtorju, ampak tudi vsakomur, ki jo bo bral za njim. Kjer je vse razumljivo iz signatur funkcij in razredov in ni treba iskati skritih skrivnosti v implementaciji. - -Tej tehniki se strokovno reče **dependency injection** (vbrizgavanje odvisnosti). In tem podatkom se reče **odvisnosti.** Pri tem gre za povsem običajno predajanje parametrov, nič več. - -.[note] -Prosimo, ne zamenjujte dependency injection, ki je načrtovalski vzorec, z „dependency injection container“, ki je orodje, torej nekaj diametralno drugačnega. Z vsebniki se bomo ukvarjali kasneje. - - -Od funkcij k razredom ---------------------- - -In kako so s tem povezani razredi? Razred je kompleksnejša celota kot preprosta funkcija, vendar pravilo št. 1 velja brez izjeme tudi tukaj. Obstaja le [več možnosti, kako predati argumente|passing-dependencies]. Na primer precej podobno kot pri funkciji: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Ali z drugimi metodami ali neposredno s konstruktorjem: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Oba primera sta popolnoma v skladu z dependency injection. - - -Realni primeri --------------- - -V resničnem svetu ne boste pisali razredov za seštevanje števil. Premaknimo se k primerom iz prakse. - -Imejmo razred `Article`, ki predstavlja članek na blogu: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // shranimo članek v podatkovno bazo - } -} -``` - -in uporaba bo naslednja: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Metoda `save()` shrani članek v podatkovno tabelo. Implementirati jo s pomočjo [Nette Database |database:] bi bilo enostavno, če ne bi bilo ene ovire: kje naj `Article` vzame povezavo s podatkovno bazo, tj. objekt razreda `Nette\Database\Connection`? - -Zdi se, da imamo veliko možnosti. Lahko jo vzame od nekod iz statične spremenljivke. Ali podeduje od razreda, ki zagotovi povezavo s podatkovno bazo. Ali uporabi t.i. [singleton |global-state#Singleton]. Ali t.i. facades, ki se uporabljajo v Laravelu: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Odlično, problem smo rešili. - -Ali ne? - -Spomnimo se [##pravilo št. 1: naj ti bo predano]: vse odvisnosti, ki jih razred potrebuje, mu morajo biti predane. Ker če pravilo kršimo, smo stopili na pot k umazani kodi, polni skritih povezav, nerazumljivosti, in rezultat bo aplikacija, ki jo bo boleče vzdrževati in razvijati. - -Uporabnik razreda `Article` ne ve, kam metoda `save()` članek shranjuje. V podatkovno tabelo? V katero, produkcijsko ali testno? In kako je to mogoče spremeniti? - -Uporabnik mora pogledati, kako je implementirana metoda `save()`, in najde uporabo metode `DB::insert()`. Torej mora raziskovati naprej, kako si ta metoda priskrbi podatkovno povezavo. In skrite povezave lahko tvorijo precej dolgo verigo. - -V čisti in dobro zasnovani kodi se nikoli ne pojavljajo skrite povezave, Laravelove facades ali statične spremenljivke. V čisti in dobro zasnovani kodi se predajajo argumenti: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Še bolj praktično, kot bomo videli kasneje, bo to s konstruktorjem: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Če ste izkušen programer, morda mislite, da `Article` sploh ne bi smel imeti metode `save()`, moral bi predstavljati zgolj podatkovno komponento in za shranjevanje bi moral skrbeti ločen repozitorij. To ima smisel. Toda s tem bi se oddaljili daleč preko okvira teme, ki je dependency injection, in prizadevanja za navajanje preprostih primerov. - -Če boste pisali razred, ki za svoje delovanje potrebuje npr. podatkovno bazo, ne izmišljajte si, od kod jo dobiti, ampak naj vam jo predajo. Na primer kot parameter konstruktorja ali druge metode. Priznajte odvisnosti. Priznajte jih v API-ju vašega razreda. Dobili boste razumljivo in predvidljivo kodo. - -Kaj pa ta razred, ki beleži sporočila o napakah: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Kaj mislite, smo upoštevali [##pravilo št. 1: naj ti bo predano]? - -Nismo. - -Ključno informacijo, torej imenik z datoteko z logom, si razred *priskrbi sam* iz konstante. - -Poglejte primer uporabe: - -```php -$logger = new Logger; -$logger->log('Temperatura je 23 °C'); -$logger->log('Temperatura je 10 °C'); -``` - -Brez poznavanja implementacije, bi lahko odgovorili na vprašanje, kam se sporočila zapisujejo? Bi pomislili, da je za delovanje potrebna obstoj konstante `LOG_DIR`? In bi lahko ustvarili drugo instanco, ki bo zapisovala drugam? Zagotovo ne. - -Popravimo razred: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Razred je zdaj veliko bolj razumljiv, nastavljiv in torej uporabnejši. - -```php -$logger = new Logger('/pot/do/loga.txt'); -$logger->log('Temperatura je 15 °C'); -``` - - -Ampak to me ne zanima! ----------------------- - -*„Ko ustvarim objekt Article in pokličem save(), potem nočem reševati podatkovne baze, preprosto želim, da se shrani v tisto, ki jo imam nastavljeno v konfiguraciji.“* - -*„Ko uporabim Logger, preprosto želim, da se sporočilo zapiše, in nočem reševati kam. Naj se uporabi globalna nastavitev.“* - -To so pravilne pripombe. - -Kot primer si bomo pokazali razred, ki pošilja novice (newsletterje) in zabeleži, kako se je izšlo: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-pošta je bila poslana'); - - } catch (Exception $e) { - $logger->log('Prišlo je do napake pri pošiljanju'); - throw $e; - } - } -} -``` - -Izboljšan `Logger`, ki ne uporablja več konstante `LOG_DIR`, zahteva v konstruktorju navedbo poti do datoteke. Kako to rešiti? Razreda `NewsletterDistributor` sploh ne zanima, kam se sporočila zapisujejo, želi jih le zapisati. - -Rešitev je spet [##pravilo št. 1: naj ti bo predano]: vse podatke, ki jih razred potrebuje, mu predamo. - -Torej to pomeni, da si preko konstruktorja predamo pot do loga, ki jo nato uporabimo pri ustvarjanju objekta `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ TAKO NE! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Tako ne! Pot namreč **ne spada** med podatke, ki jih razred `NewsletterDistributor` potrebuje; te namreč potrebuje `Logger`. Zaznavate razliko? Razred `NewsletterDistributor` potrebuje logger kot takega. Torej si tega predamo: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-pošta je bila poslana'); - - } catch (Exception $e) { - $this->logger->log('Prišlo je do napake pri pošiljanju'); - throw $e; - } - } -} -``` - -Zdaj je iz signatur razreda `NewsletterDistributor` jasno, da je del njegove funkcionalnosti tudi logiranje. In naloga zamenjati logger za drugega, na primer zaradi testiranja, je popolnoma trivialna. Poleg tega, če bi se konstruktor razreda `Logger` spremenil, to ne bo imelo nobenega vpliva na naš razred. - - -Pravilo št. 2: vzemi, kar je tvoje ----------------------------------- - -Ne pustite se zmesti in ne pustite si predajati odvisnosti svojih odvisnosti. Pustite si predajati le svoje odvisnosti. - -Zahvaljujoč temu bo koda, ki uporablja druge objekte, popolnoma neodvisna od sprememb njihovih konstruktorjev. Njen API bo bolj resničen. In predvsem bo trivialno te odvisnosti zamenjati za druge. - - -Nov član družine ----------------- - -V razvojni ekipi je padla odločitev ustvariti drugi logger, ki zapisuje v podatkovno bazo. Ustvarili bomo torej razred `DatabaseLogger`. Imamo torej dva razreda, `Logger` in `DatabaseLogger`, eden zapisuje v datoteko, drugi v podatkovno bazo … se vam pri tem poimenovanju ne zdi nekaj čudnega? Ali ne bi bilo bolje preimenovati `Logger` v `FileLogger`? Zagotovo da. - -Ampak naredili bomo pametno. Pod prvotnim imenom bomo ustvarili vmesnik: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… ki ga bosta oba loggerja implementirala: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -In zahvaljujoč temu ne bo treba ničesar spreminjati v preostalem delu kode, kjer se logger uporablja. Na primer konstruktor razreda `NewsletterDistributor` bo še vedno zadovoljen s tem, da kot parameter zahteva `Logger`. In samo od nas bo odvisno, katero instanco mu bomo predali. - -**Zato nikoli ne dajemo imenom vmesnikov pripone `Interface` ali predpone `I`.** Sicer ne bi bilo mogoče kode tako lepo razvijati. - - -Houston, imamo problem ----------------------- - -Medtem ko si lahko v celotni aplikaciji zadostujemo z eno samo instanco loggerja, bodisi datotečnega ali podatkovnega, in ga preprosto predajamo povsod tam, kjer se nekaj logira, je povsem drugače v primeru razreda `Article`. Njegove instance namreč ustvarjamo po potrebi, lahko tudi večkrat. Kako se spopasti s povezavo na podatkovno bazo v njegovem konstruktorju? - -Kot primer lahko služi kontroler, ki mora po oddaji obrazca shraniti članek v podatkovno bazo: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Možna rešitev se ponuja kar sama: pustimo si objekt podatkovne baze predati s konstruktorjem v `EditController` in uporabimo `$article = new Article($this->db)`. - -Enako kot v prejšnjem primeru z `Logger` in potjo do datoteke, to ni pravilen postopek. Podatkovna baza ni odvisnost `EditController`, ampak `Article`. Predajanje podatkovne baze torej gre proti [pravilu št. 2: vzemi, kar je tvoje |#Pravilo št. 2: vzemi kar je tvoje]. Ko se spremeni konstruktor razreda `Article` (doda se nov parameter), bo treba prilagoditi tudi kodo na vseh mestih, kjer se ustvarjajo instance. Ufff. - -Houston, kaj predlagaš? - - -Pravilo št. 3: prepusti tovarni -------------------------------- - -S tem, ko smo odpravili skrite povezave in vse odvisnosti predajamo kot argumente, smo dobili bolj nastavljive in prožne razrede. In zato potrebujemo še nekaj drugega, kar nam bo te prožnejše razrede ustvarilo in konfiguriralo. Temu bomo rekli tovarne. - -Pravilo se glasi: če ima razred odvisnosti, prepusti ustvarjanje njihovih instanc tovarni. - -Tovarne so pametnejša zamenjava za operator `new` v svetu dependency injection. - -.[note] -Prosimo, ne zamenjujte z načrtovalskim vzorcem *factory method*, ki opisuje specifičen način uporabe tovarn in s to temo ni povezan. - - -Tovarna -------- - -Tovarna je metoda ali razred, ki izdeluje in konfigurira objekte. Razred, ki izdeluje `Article`, bomo poimenovali `ArticleFactory` in bi lahko izgledal na primer takole: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Njegova uporaba v kontrolerju bo naslednja: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // pustimo tovarni ustvariti objekt - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Če se v tem trenutku spremeni signatura konstruktorja razreda `Article`, je edini del kode, ki se mora na to odzvati, sama tovarna `ArticleFactory`. Vse ostale kode, ki delajo z objekti `Article`, kot na primer `EditController`, se to nikakor ne dotakne. - -Morda si zdaj trkate po čelu, ali smo si sploh pomagali. Količina kode se je povečala in vse skupaj začenja izgledati sumljivo zapleteno. - -Ne skrbite, kmalu bomo prišli do Nette DI vsebnika. In ta ima vrsto asov v rokavu, s katerimi gradnjo aplikacij, ki uporabljajo dependency injection, neizmerno poenostavi. Tako na primer namesto razreda `ArticleFactory` bo zadostovalo [napisati zgolj vmesnik |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ampak to prehitevamo, še počakajte :-) - - -Povzetek --------- - -Na začetku tega poglavja smo obljubili, da si bomo pokazali postopek, kako načrtovati čisto kodo. Zadostuje razredom - -1) [predajati odvisnosti, ki jih potrebujejo |#Pravilo št. 1: naj ti bo predano] -2) [in nasprotno ne predajati, česar neposredno ne potrebujejo |#Pravilo št. 2: vzemi kar je tvoje] -3) [in da se objekti z odvisnostmi najbolje izdelujejo v tovarnah |#Pravilo št. 3: prepusti tovarni] - -Morda se na prvi pogled ne zdi tako, a ta tri pravila imajo daljnosežne posledice. Vodijo k radikalno drugačnemu pogledu na načrtovanje kode. Se splača? Programerji, ki so opustili stare navade in začeli dosledno uporabljati dependency injection, menijo, da je ta korak ključni trenutek v njihovem poklicnem življenju. Odprl se jim je svet preglednih in vzdržljivih aplikacij. - -Kaj pa, če koda dosledno ne uporablja dependency injection? Kaj če je zgrajena na statičnih metodah ali singletonih? Ali to prinaša kakšne težave? [Prinaša in zelo bistvene |global-state]. diff --git a/dependency-injection/sl/nette-container.texy b/dependency-injection/sl/nette-container.texy deleted file mode 100644 index a74d0f73bc..0000000000 --- a/dependency-injection/sl/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Vsebnik -**************** - -.[perex] -Nette DI je ena izmed najbolj zanimivih knjižnic Nette. Zna generirati in samodejno posodabljati prevedene DI vsebnike, ki so izjemno hitri in neverjetno enostavni za konfiguracijo. - -Podobo storitev, ki jih mora ustvarjati DI vsebnik, definiramo običajno s pomočjo konfiguracijskih datotek v [formatu NEON|neon:format]. Vsebnik, ki smo ga ročno ustvarili v [prejšnjem poglavju|container], bi se zapisal takole: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Zapis je resnično kratek. - -Vse odvisnosti, deklarirane v konstruktorjih razredov `ArticleFactory` in `UserController`, si Nette DI sam ugotovi in preda zahvaljujoč t.i. [autowiringu|autowiring], v konfiguracijski datoteki zato ni treba ničesar navajati. Torej tudi če pride do spremembe parametrov, vam ni treba v konfiguraciji ničesar spreminjati. Nette vsebnik samodejno pregenerira. Vi se lahko tam osredotočite izključno na razvoj aplikacije. - -Če želimo odvisnosti predajati s pomočjo setterjev, uporabimo za to sekcijo [setup |services#Setup]. - -Nette DI generira neposredno PHP kodo vsebnika. Rezultat je torej datoteka `.php`, ki jo lahko odprete in preučujete. Zahvaljujoč temu natančno vidite, kako vsebnik deluje. Lahko ga tudi razhroščujete v IDE in korakate skozi. In predvsem: generirana PHP koda je izjemno hitra. - -Nette DI zna tudi generirati kodo [tovarn|factory] na podlagi posredovanega vmesnika. Zato namesto razreda `ArticleFactory` bo zadostovalo ustvariti v aplikaciji le vmesnik: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Celoten primer najdete [na GitHubu|https://github.com/nette-examples/di-example-doc]. - - -Samostojna uporaba ------------------- - -Uvedba knjižnice Nette DI v aplikacijo je zelo enostavna. Najprej jo namestimo s Composerjem (ker je prenašanje zipov taaaako zastarelo): - -```shell -composer require nette/di -``` - -Naslednja koda ustvari instanco DI vsebnika glede na konfiguracijo, shranjeno v datoteki `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Vsebnik se generira le enkrat, njegova koda se zapiše v predpomnilnik (imenik `__DIR__ . '/temp'`) in pri naslednjih zahtevah se le še od tam naloži. - -Za ustvarjanje in pridobivanje storitev služita metodi `getService()` ali `getByType()`. Tako ustvarimo objekt `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Med razvojem je koristno aktivirati način samodejnega osveževanja, ko se vsebnik samodejno pregenerira, če pride do spremembe kateregakoli razreda ali konfiguracijske datoteke. Zadostuje, da v konstruktorju `ContainerLoader` navedete kot drugi argument `true`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Uporaba z ogrodjem Nette ------------------------- - -Kot smo pokazali, uporaba Nette DI ni omejena na aplikacije, napisane v Nette Frameworku, lahko ga s pomočjo le 3 vrstic kode uvedete kjerkoli. Če pa razvijate aplikacije v Nette Frameworku, ima konfiguracijo in ustvarjanje vsebnika na skrbi [Bootstrap |application:bootstrapping#Konfiguracija DI vsebnika]. diff --git a/dependency-injection/sl/passing-dependencies.texy b/dependency-injection/sl/passing-dependencies.texy deleted file mode 100644 index aa75ecf278..0000000000 --- a/dependency-injection/sl/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Predajanje odvisnosti -********************* - -<div class=perex> - -Argumente ali v terminologiji DI „odvisnosti“ lahko v razrede predajamo na naslednje glavne načine: - -* predajanje s konstruktorjem -* predajanje z metodo (t.i. setterjem) -* nastavitev spremenljivke -* z metodo, anotacijo ali atributom *inject* - -</div> - -Zdaj si bomo posamezne variante pokazali na konkretnih primerih. - - -Predajanje s konstruktorjem -=========================== - -Odvisnosti se predajajo v trenutku ustvarjanja objekta kot argumenti konstruktorja: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Ta oblika je primerna za obvezne odvisnosti, ki jih razred nujno potrebuje za svoje delovanje, saj brez njih instance ne bo mogoče ustvariti. - -Od PHP 8.0 lahko uporabimo krajšo obliko zapisa ([constructor property promotion |https://blog.nette.org/sl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), ki je funkcionalno ekvivalentna: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Od PHP 8.1 lahko spremenljivko označimo z zastavico `readonly`, ki deklarira, da se vsebina spremenljivke ne bo več spremenila: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI vsebnik preda konstruktorju odvisnosti samodejno s pomočjo [autowiringa |autowiring]. Argumente, ki jih na ta način ni mogoče predati (npr. nizi, števila, booleani) [zapišemo v konfiguraciji |services#Argumenti]. - - -Constructor hell ----------------- - -Izraz *constructor hell* označuje situacijo, ko potomec deduje od starševskega razreda, katerega konstruktor zahteva odvisnosti, in hkrati potomec zahteva odvisnosti. Pri tem mora prevzeti in predati tudi starševske: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Težava nastane v trenutku, ko bomo želeli spremeniti konstruktor razreda `BaseClass`, na primer ko se doda nova odvisnost. Potem je namreč treba prilagoditi tudi vse konstruktorje potomcev. Kar iz takšne prilagoditve naredi pekel. - -Kako temu preprečiti? Rešitev je **dajati prednost [kompoziciji pred dedovanjem |faq#Zakaj se daje prednost kompoziciji pred dedovanjem]**. - -Torej bomo kodo zasnovali drugače. Izogibali se bomo [abstraktnim |nette:introduction-to-object-oriented-programming#Abstraktni razredi] `Base*` razredom. Namesto da bi `MyClass` pridobival določeno funkcionalnost s tem, da deduje od `BaseClass`, si bo to funkcionalnost pustil predati kot odvisnost: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Predajanje s setterjem -====================== - -Odvisnosti se predajajo s klicem metode, ki jih shrani v zasebno spremenljivko. Običajna konvencija poimenovanja teh metod je oblika `set*()`, zato se jim reče setterji, vendar se lahko seveda imenujejo kakorkoli drugače. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Ta način je primeren za neobvezne odvisnosti, ki niso nujne za delovanje razreda, saj ni zagotovljeno, da bo objekt odvisnost dejansko prejel (tj. da bo uporabnik metodo poklical). - -Hkrati ta način dopušča ponavljajoče klicanje setterja in s tem spreminjanje odvisnosti. Če to ni zaželeno, dodamo v metodo preverjanje ali od PHP 8.1 označimo lastnost `$cache` z zastavico `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Odvisnost je že bila nastavljena'); - } - $this->cache = $cache; - } -} -``` - -Klic setterja definiramo v konfiguraciji DI vsebnika v [ključu setup |services#Setup]. Tudi tukaj se uporablja samodejno predajanje odvisnosti s pomočjo autowiringa: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Nastavitev spremenljivke -======================== - -Odvisnosti se predajajo z zapisom neposredno v člansko spremenljivko: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Ta način se šteje za neprimernega, ker mora biti članska spremenljivka deklarirana kot `public`. In zato nimamo nadzora nad tem, da bo predana odvisnost dejansko danega tipa (veljalo pred PHP 7.4) in izgubimo možnost reagirati na novo dodeljeno odvisnost z lastno kodo, na primer preprečiti nadaljnjo spremembo. Hkrati spremenljivka postane del javnega vmesnika razreda, kar morda ni zaželeno. - -Nastavitev spremenljivke definiramo v konfiguraciji DI vsebnika v [sekciji setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Medtem ko prejšnji trije načini veljajo na splošno v vseh objektno usmerjenih jezikih, je vbrizgavanje z metodo, anotacijo ali atributom *inject* specifično izključno za presenterje v Nette. O njih govori [samostojno poglavje |best-practices:inject-method-attribute]. - - -Kateri način izbrati? -===================== - -- konstruktor je primeren za obvezne odvisnosti, ki jih razred nujno potrebuje za svoje delovanje -- setter je nasprotno primeren za neobvezne odvisnosti ali odvisnosti, ki jih je mogoče še naprej spreminjati -- javne spremenljivke niso primerne diff --git a/dependency-injection/sl/services.texy b/dependency-injection/sl/services.texy deleted file mode 100644 index d3a0e1bc48..0000000000 --- a/dependency-injection/sl/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Definiranje storitev -******************** - -.[perex] -Konfiguracija je mesto, kjer učimo DI vsebnik, kako naj sestavlja posamezne storitve in kako jih povezuje z drugimi odvisnostmi. Nette ponuja zelo pregleden in eleganten način, kako to doseči. - -Sekcija `services` v konfiguracijski datoteki formata NEON je mesto, kjer definiramo lastne storitve in njihove konfiguracije. Poglejmo si preprost primer definicije storitve, imenovane `database`, ki predstavlja instanco razreda `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Navedena konfiguracija bo vodila do naslednje tovarne metode v [DI vsebniku|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Imena storitev nam omogočajo, da se nanje sklicujemo v drugih delih konfiguracijske datoteke, in sicer v formatu `@imeStoritve`. Če storitve ni treba poimenovati, lahko preprosto uporabimo le alinejo: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Za pridobitev storitve iz DI vsebnika lahko uporabimo metodo `getService()` z imenom storitve kot parametrom ali metodo `getByType()` s tipom storitve: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Ustvarjanje storitve -==================== - -Večinoma ustvarimo storitev preprosto tako, da ustvarimo instanco določenega razreda. Na primer: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Če moramo konfiguracijo razširiti z dodatnimi ključi, lahko definicijo razpišemo v več vrstic: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Ključ `create` ima alias `factory`, obe varianti sta v praksi pogosti. Vendar priporočamo uporabo `create`. - -Argumenti konstruktorja ali ustvarjalne metode so lahko alternativno zapisani v ključu `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Storitve ni treba ustvarjati le s preprostim ustvarjanjem instance razreda, lahko so tudi rezultat klica statičnih metod ali metod drugih storitev: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Opazite, da se za enostavnost namesto `->` uporablja `::`, glej [#Izrazna sredstva]. Generirale se bodo te tovarne metode: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI vsebnik mora poznati tip ustvarjene storitve. Če ustvarjamo storitev s pomočjo metode, ki nima specificiranega vrnjenega tipa, moramo ta tip eksplicitno navesti v konfiguraciji: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumenti -========= - -V konstruktor in metode predajamo argumente na način, ki je zelo podoben kot v samem PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Za boljšo berljivost lahko argumente razpišemo v ločene vrstice. V takem primeru je uporaba vejic neobvezna: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Argumente lahko tudi poimenujete in vam ni treba skrbeti za njihov vrstni red: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Če želite nekatere argumente izpustiti in uporabiti njihovo privzeto vrednost ali dodati storitev s pomočjo [autowiringa|autowiring], uporabite podčrtaj: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Kot argumente lahko predajate storitve, uporabljate parametre in še veliko več, glej [#Izrazna sredstva]. - - -Setup -===== - -V sekciji `setup` definiramo metode, ki se morajo poklicati pri ustvarjanju storitve. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -To bi v PHP izgledalo takole: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Poleg klicanja metod lahko tudi predajate vrednosti v lastnosti. Podprto je tudi dodajanje elementa v polje, ki ga je treba zapisati v narekovajih, da ne pride do kolizije s sintakso NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Kar bi v PHP kodi izgledalo takole: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -V setupu lahko pa kličete tudi statične metode ali metode drugih storitev. Če morate kot argument predati trenutno storitev, jo navedite kot `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Opazite, da se za enostavnost namesto `->` uporablja `::`, glej [#Izrazna sredstva]. Generirala se bo takšna tovarna metoda: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Izrazna sredstva -================ - -Nette DI nam daje izjemno bogata izrazna sredstva, s katerimi lahko zapišemo skoraj karkoli. V konfiguracijskih datotekah lahko tako uporabljamo [parametre |configuration#Parametri]: - -```neon -# parameter -%wwwDir% - -# vrednost parametra pod ključem -%mailer.user% - -# parameter znotraj niza -'%wwwDir%/images' -``` - -Nadalje ustvarjati objekte, klicati metode in funkcije: - -```neon -# ustvarjanje objekta -DateTime() - -# klic statične metode -Collator::create(%locale%) - -# klic PHP funkcije -::getenv(DB_USER) -``` - -Sklicujemo se na storitve bodisi po njihovem imenu ali s pomočjo tipa: - -```neon -# storitev po imenu -@database - -# storitev po tipu -@Nette\Database\Connection -``` - -Uporabljati first-class callable sintakso: .{data-version:3.2.0} - -```neon -# ustvarjanje povratnega klica, podobno [@user, logout] -@user::logout(...) -``` - -Uporabljati konstante: - -```neon -# konstanta razreda -FilesystemIterator::SKIP_DOTS - -# globalno konstanto dobimo s PHP funkcijo constant() -::constant(PHP_VERSION) -``` - -Klicanje metod lahko verižimo enako kot v PHP. Le za enostavnost se namesto `->` uporablja `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Te izraze lahko uporabljate kjerkoli, pri [ustvarjanju storitev |#Ustvarjanje storitve], v [argumentih |#Argumenti], v sekciji [#setup] ali [parametrih |configuration#Parametri]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Posebne funkcije ----------------- - -V konfiguracijskih datotekah lahko uporabljate te posebne funkcije: - -- `not()` negacija vrednosti -- `bool()`, `int()`, `float()`, `string()` pretvorba brez izgube v dani tip -- `typed()` ustvari polje vseh storitev specificiranega tipa -- `tagged()` ustvari polje vseh storitev z dano oznako - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -V primerjavi s klasično pretvorbo v PHP, kot je npr. `(int)`, pretvorba brez izgube vrže izjemo za neštevilske vrednosti. - -Funkcija `typed()` ustvari polje vseh storitev danega tipa (razred ali vmesnik). Izpusti storitve, ki imajo izklopljen autowiring. Lahko navedete tudi več tipov, ločenih z vejico. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Polje storitev določenega tipa lahko predajate kot argument tudi samodejno s pomočjo [autowiringa |autowiring#Polje storitev]. - -Funkcija `tagged()` pa ustvarja polje vseh storitev z določeno oznako. Tudi tukaj lahko specificirate več oznak, ločenih z vejico. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Ključ `autowired` omogoča vplivanje na obnašanje autowiringa za specifično storitev. Za podrobnosti glej [poglavje o autowiringu|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # storitev foo je izključena iz autowiringa -``` - - -Lazy storitve .{data-version:3.2.4} -=================================== - -Lazy loading je tehnika, ki odloži ustvarjanje storitve do trenutka, ko je dejansko potrebna. V globalni konfiguraciji lahko [omogočite lazy ustvarjanje |configuration#Lene storitve] za vse storitve hkrati. Za posamezne storitve pa lahko to obnašanje prepišete: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Ko je storitev definirana kot lazy, ob njeni zahtevi iz DI vsebnika dobimo poseben nadomestni objekt. Ta izgleda in se obnaša enako kot dejanska storitev, vendar se dejanska inicializacija (klic konstruktorja in setupa) zgodi šele ob prvem klicu katerekoli njene metode ali lastnosti. - -.[note] -Lazy loading je mogoče uporabiti samo za uporabniške razrede, ne pa za notranje PHP razrede. Zahteva PHP 8.4 ali novejšo različico. - - -Oznake -====== - -Oznake (tags) služijo za dodajanje dopolnilnih informacij k storitvam. Storitvi lahko dodate eno ali več oznak: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Oznake lahko nosijo tudi vrednosti: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Da bi dobili vse storitve z določenimi oznakami, lahko uporabite funkcijo `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -V DI vsebniku lahko dobite imena vseh storitev z določeno oznako s pomočjo metode `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names je polje, ki vsebuje ime storitve in vrednost oznake -// npr. ['foo' => 'monolog.logger.event', ...] -``` - - -Način Inject -============ - -S pomočjo zastavice `inject: true` se aktivira predajanje odvisnosti preko javnih spremenljivk z anotacijo [inject |best-practices:inject-method-attribute#Atributi Inject] in metod [inject*() |best-practices:inject-method-attribute#Metode inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Privzeto je `inject` aktiviran samo za presenterje. - - -Modifikacija storitev -===================== - -DI vsebnik vsebuje veliko storitev, ki so bile dodane preko vgrajene ali [uporabniške razširitve|extensions]. Definicije teh storitev lahko prilagodite neposredno v konfiguraciji. Na primer, lahko spremenite razred storitve `application.application`, ki je standardno `Nette\Application\Application`, na drugega: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Zastavica `alteration` je informativna in pove, da le modificiramo obstoječo storitev. - -Lahko tudi dopolnimo setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Pri prepisovanju storitve lahko želimo odstraniti prvotne argumente, postavke setupa ali oznake, za kar služi `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Če želite odstraniti storitev, dodano z razširitvijo, lahko to storite takole: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/tr/@home.texy b/dependency-injection/tr/@home.texy index 2e2673922e..7767318013 100644 --- a/dependency-injection/tr/@home.texy +++ b/dependency-injection/tr/@home.texy @@ -2,20 +2,21 @@ Nette DI ******** .[perex] -Dependency Injection, koda ve geliştirmeye bakış açınızı temelden değiştirecek bir tasarım desenidir. Size temiz tasarlanmış ve sürdürülebilir uygulamaların dünyasına giden yolu açacaktır. +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. -- [Dependency Injection Nedir? |introduction] -- [Küresel Durum ve Singleton'lar |global-state] -- [Bağımlılıkları Geçirme |passing-dependencies] -- [DI Konteyneri Nedir? |container] -- [Sıkça Sorulan Sorular|faq] +- [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ş, derlenmiş bir DI konteyneri sağlar. +`nette/di` paketi, PHP için son derece gelişmiş, derlenen bir DI container sunar. -- [Nette DI Konteyneri |nette-container] +- [Nette DI Container |nette-container] - [Yapılandırma |configuration] -- [Servisleri Tanımlama |services] -- [Otomatik Kablolama |autowiring] -- [Oluşturulan Fabrikalar |factory] -- [Nette DI için Uzantılar Oluşturma|extensions] +- [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 index 13be13e5fe..4c5192e6e2 100644 --- a/dependency-injection/tr/@left-menu.texy +++ b/dependency-injection/tr/@left-menu.texy @@ -1,17 +1,28 @@ -Dependency Injection -******************** -- [DI Nedir? |introduction] -- [Küresel Durum ve Singleton'lar |global-state] -- [Bağımlılıkları Geçirme |passing-dependencies] -- [DI Konteyneri Nedir? |container] -- [Sıkça Sorulan Sorular|faq] +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 Konteyneri |nette-container] +- [Nette DI Container |nette-container] - [Yapılandırma |configuration] -- [Servisleri Tanımlama |services] -- [Otomatik Kablolama |autowiring] -- [Oluşturulan Fabrikalar |factory] -- [Nette DI için Uzantılar Oluşturma|extensions] +- [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/autowiring.texy b/dependency-injection/tr/autowiring.texy index fa4eb901e5..5409f3bd11 100644 --- a/dependency-injection/tr/autowiring.texy +++ b/dependency-injection/tr/autowiring.texy @@ -1,24 +1,24 @@ -Otomatik Bağlama (Autowiring) -***************************** +Autowiring +********** .[perex] -Otomatik bağlama (Autowiring), kurucuya (constructor) ve diğer metotlara gerekli servisleri otomatik olarak aktarabilen harika bir özelliktir, böylece onları hiç yazmamıza gerek kalmaz. Size çok zaman kazandırır. +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. Yerine: +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) ``` -Sadece şunu yazmak yeterlidir: +Yalnızca şunu yazın: ```neon services: articles: Model\ArticleRepository ``` -Otomatik bağlama tiplere göre yönlendirilir, bu yüzden çalışması için `ArticleRepository` sınıfının yaklaşık olarak şöyle tanımlanması gerekir: +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; @@ -30,22 +30,24 @@ class ArticleRepository } ``` -Otomatik bağlamanın kullanılabilmesi için, her tip için konteynerde **tam olarak bir servis** olmalıdır. Eğer birden fazla olsaydı, otomatik bağlama hangisini aktaracağını bilemez ve bir istisna fırlatırdı: +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 de tempDb uyar + articles: Model\ArticleRepository # İSTİSNA FIRLATIR, hem mainDb hem tempDb uyuyor ``` -Çözüm, ya otomatik bağlamayı atlamak ve servis adını açıkça belirtmek (yani `articles: Model\ArticleRepository(@mainDb)`) ya da daha akıllıca olanı, servislerden birinin otomatik bağlanmasını [kapatmak |#Otomatik Bağlamayı Kapatma] veya ilk servisi [önceliklendirmektir |#Otomatik Bağlama Önceliği]. +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]. -Otomatik Bağlamayı Kapatma --------------------------- +Autowiring'i Kapatma +-------------------- -Bir servisin otomatik bağlanmasını `autowired: no` seçeneğiyle kapatabiliriz: +Bir servis için autowiring'i `autowired: false` seçeneğiyle kapatabiliriz: ```neon services: @@ -53,27 +55,29 @@ services: tempDb: create: PDO('sqlite::memory:') - autowired: false # tempDb servisi otomatik bağlamadan çıkarıldı + autowired: false # tempDb servisi autowiring'den çıkarılır - articles: Model\ArticleRepository # bu yüzden kurucuya mainDb aktarılır + articles: Model\ArticleRepository # bu yüzden yapıcıya mainDb aktarılır ``` -`articles` servisi, kurucuya aktarılabilecek iki uygun `PDO` tipi servis (yani `mainDb` ve `tempDb`) olduğu için istisna fırlatmaz, çünkü yalnızca `mainDb` servisini görü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 otomatik bağlama yapılandırması, Symfony'den farklı çalışır; burada `autowire: false` seçeneği, verilen servisin kurucu argümanları için otomatik bağlamanın kullanılmaması gerektiğini belirtir. Nette'de otomatik bağlama her zaman kullanılır, ister kurucu argümanları için ister başka herhangi bir metot için olsun. `autowired: false` seçeneği, verilen servisin örneğinin otomatik bağlama yoluyla hiçbir yere aktarılmaması gerektiğini belirtir. +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. -Otomatik Bağlama Önceliği -------------------------- +Autowiring Yeğlemesi +-------------------- -Aynı tipten birden fazla servisimiz varsa ve bunlardan birinde `autowired` seçeneğini belirtirsek, bu servis tercih edilen olur: +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 # tercih edilen olur + autowired: PDO # yeğlenen olur tempDb: create: PDO('sqlite::memory:') @@ -81,13 +85,13 @@ services: articles: Model\ArticleRepository ``` -`articles` servisi, iki uygun `PDO` tipi servis (yani `mainDb` ve `tempDb`) olduğu için istisna fırlatmaz, ancak tercih edilen servisi, yani `mainDb`'yi kullanır. +`articles` servisi, birden çok `PDO` servisinin (`mainDb` ve `tempDb`) uyduğuna dair istisna fırlatmaz; yeğlenen servisi, yani `mainDb` servisini kullanır. -Servis Dizileri ---------------- +Servis Koleksiyonu +------------------ -Otomatik bağlama, belirli bir tipteki servis dizilerini de aktarabilir. PHP'de dizi öğelerinin tipini yerel olarak yazılamadığı için, `array` tipine ek olarak `ClassName[]` formatında öğe tipini içeren bir phpDoc yorumu eklemek gerekir: +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; @@ -102,45 +106,62 @@ class ShipManager } ``` -DI konteyneri daha sonra otomatik olarak ilgili tipe karşılık gelen servis dizisini aktarır. Otomatik bağlanması kapalı olan servisleri atlar. +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 tip `array<int, Class>` veya `list<Class>` şeklinde de olabilir. Eğer phpDoc yorumunun şeklini etkileyemiyorsanız, servis dizisini doğrudan yapılandırmada [`typed()` |services#Özel Fonksiyonlar] kullanarak aktarabilirsiniz. +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 ----------------- -Otomatik bağlama yalnızca nesneleri ve nesne dizilerini yerleştirebilir. Skaler argümanları (örn. karakter dizileri, sayılar, booleanlar) [yapılandırmada yazarız |services#Argümanlar]. Alternatif olarak, skaler değeri (veya birden fazla değeri) bir nesne şeklinde kapsülleyen bir [ayarlar nesnesi |best-practices:passing-settings-to-presenters] oluşturmaktır ve bu nesne daha sonra tekrar otomatik bağlama ile aktarılabilir. +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 itibaren kullanılabilir + // readonly, PHP 8.1'den beri kullanılabilir public readonly bool $value, ) {} } ``` -Yapılandırmaya ekleyerek ondan bir servis oluşturursunuz: +Onu yapılandırmaya ekleyerek servis olarak kaydedersiniz: ```neon services: - MySettings('any value') ``` -Tüm sınıflar daha sonra onu otomatik bağlama ile talep eder. +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. -Otomatik Bağlamayı Daraltma ---------------------------- +Autowiring'i Daraltma +--------------------- -Bireysel servisler için otomatik bağlama yalnızca belirli sınıflara veya arayüzlere daraltılabilir. +Tek tek servislerde autowiring, belirli sınıflara ya da arayüzlere daraltılabilir. -Normalde otomatik bağlama, servisi, tipine uyduğu her metot parametresine aktarır. Daraltma, servisin onlara aktarılması için metot parametrelerinde belirtilen tiplerin uyması gereken koşulları belirlediğimiz anlamına gelir. +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. -Bunu bir örnekle gösterelim: +Bir örnek alalım: ```php class ParentClass @@ -162,42 +183,42 @@ class ChildDependent } ``` -Eğer hepsini servis olarak kaydetseydik, otomatik bağlama başarısız olurdu: +Hepsini servis olarak kaydetseydik autowiring başarısız olurdu: ```neon services: parent: ParentClass child: ChildClass - parentDep: ParentDependent # İSTİSNA FIRLATIR, hem parent hem de child servisleri uyar - childDep: ChildDependent # otomatik bağlama kurucuya child servisini aktarır + 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: parent, child` istisnasını fırlatır, çünkü kurucusuna hem `parent` hem de `child` servisleri uyar ve otomatik bağlama hangisini seçeceğine karar veremez. +`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 nedenle, `child` servisi için otomatik bağlanmasını `ChildClass` tipine daraltabiliriz: +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' de yazılabilir + autowired: ChildClass # 'autowired: self' olarak da yazılabilir - parentDep: ParentDependent # otomatik bağlama kurucuya parent servisini aktarır - childDep: ChildDependent # otomatik bağlama kurucuya child servisini aktarır + parentDep: ParentDependent # autowiring yapıcıya parent servisini aktarır + childDep: ChildDependent # autowiring yapıcıya child servisini aktarır ``` -Şimdi `parentDep` servisi kurucusuna `parent` servisi aktarılır, çünkü şimdi tek uygun nesne odur. `child` servisini otomatik bağlama artık oraya aktarmaz. Evet, `child` servisi hala `ParentClass` tipindedir, ancak parametre tipi için verilen daraltma koşulu artık geçerli değildir, yani `ParentClass` *'ın* `ChildClass` *'ın üst tipi olduğu* geçerli değildir. +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 tutucu tanımdır. +`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, bir dizi olarak birkaç sınıf veya arayüz de belirtilebilir: +`autowired` anahtarında dizi olarak birden çok sınıf ya da arayüz belirtmek de olanaklıdır: ```neon -autowired: [BarClass, FooInterface] +autowired: [ParentClass, FooInterface] ``` -Örneği bir arayüzle daha tamamlamayı deneyelim: +Örneğe arayüzler eklemeyi deneyelim: ```php interface FooInterface @@ -237,13 +258,13 @@ class ChildDependent } ``` -Eğer `child` servisini hiçbir şekilde sınırlamazsak, tüm `FooDependent`, `BarDependent`, `ParentDependent` ve `ChildDependent` sınıflarının kurucularına uyar ve otomatik bağlama onu oraya aktarır. +`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 otomatik bağlanmasını `autowired: ChildClass` (veya `self`) kullanarak `ChildClass`'a daraltırsak, otomatik bağlama onu yalnızca `ChildDependent` kurucusuna aktarır, çünkü `ChildClass` tipinde bir argüman gerektirir ve `ChildClass` *'ın* `ChildClass` *tipinde olduğu* geçerlidir. Diğer parametrelerde belirtilen başka hiçbir tip `ChildClass`'ın üst tipi değildir, bu yüzden servis aktarılmaz. +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. -Eğer onu `autowired: ParentClass` kullanarak `ParentClass`'a sınırlarsak, otomatik bağlama onu tekrar `ChildDependent` kurucusuna (çünkü gerekli `ChildClass`, `ParentClass`'ın üst tipidir) ve yeni olarak `ParentDependent` kurucusuna aktarır, çünkü gerekli `ParentClass` tipi de uygundur. +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. -Eğer onu `FooInterface`'e sınırlarsak, hala `ParentDependent` (gerekli `ParentClass`, `FooInterface`'in üst tipidir) ve `ChildDependent`'e otomatik bağlanır, ancak ek olarak `FooDependent` kurucusuna da bağlanır, ancak `BarDependent`'e bağlanmaz, çünkü `BarInterface`, `FooInterface`'in üst tipi değildir. +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: @@ -251,8 +272,8 @@ services: create: ChildClass autowired: FooInterface - fooDep: FooDependent # otomatik bağlama kurucuya child aktarır - barDep: BarDependent # İSTİSNA FIRLATIR, hiçbir servis uymuyor - parentDep: ParentDependent # otomatik bağlama kurucuya child aktarır - childDep: ChildDependent # otomatik bağlama kurucuya child aktarır + 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 new file mode 100644 index 0000000000..cc71e33592 --- /dev/null +++ b/dependency-injection/tr/compilation-internals.texy @@ -0,0 +1,222 @@ +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 index 41b28cddb2..9970d0a514 100644 --- a/dependency-injection/tr/configuration.texy +++ b/dependency-injection/tr/configuration.texy @@ -1,33 +1,33 @@ -DI Konteyner Yapılandırması +DI Container Yapılandırması *************************** .[perex] -Nette DI konteyneri için yapılandırma seçeneklerine genel bakış. +Nette DI container'ının yapılandırma seçeneklerine genel bakış. Yapılandırma Dosyası ==================== -Nette DI konteyneri, yapılandırma dosyaları aracılığıyla kolayca kontrol edilir. Bunlar genellikle [NEON formatı |neon:format] kullanılarak yazılır. Düzenleme için bu formatı [destekleyen düzenleyiciler |best-practices:editors-and-tools#IDE Editörü] öneririz. +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]":[#Dekoratör Decorator]: "Dekoratör .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI konteyner .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Uzantılar]: "Diğer DI uzantılarının kurulumu .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Dosya Dahil Etme]: "Dosya dahil etme .[prism-token prism-comment]"<br> +"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]":[#Arama Search]: "Servislerin otomatik kaydı .[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] -`%` karakterini içeren bir karakter dizisi yazmak istiyorsanız, onu `%%` olarak iki katına çıkararak kaçış yapmanız gerekir. +İç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, daha sonra servis tanımlarının bir parçası olarak kullanılabilecek parametreler tanımlayabilirsiniz. Bu sayede yapılandırmayı daha anlaşılır hale getirebilir veya değişecek değerleri birleştirebilir ve ayırabilirsiniz. +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: @@ -36,9 +36,9 @@ parameters: password: secret ``` -`dsn` parametresine yapılandırmanın herhangi bir yerinde `%dsn%` yazarak başvururuz. Parametreler, `'%wwwDir%/images'` gibi karakter dizileri içinde de kullanılabilir. +`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 karakter dizileri veya sayılar olmak zorunda değildir, diziler de içerebilirler: +Parametreler yalnızca dize ya da sayı olmak zorunda değildir, dizi de içerebilirler: ```neon parameters: @@ -51,30 +51,30 @@ parameters: Belirli bir anahtara `%mailer.user%` olarak başvururuz. -Kodunuzda, örneğin bir sınıfta, herhangi bir parametrenin değerini öğrenmeniz gerekiyorsa, onu bu sınıfa aktarın. Örneğin kurucuda. Parametre değerleri için sınıfların sorgulayacağı, yapılandırmayı temsil eden global bir nesne yoktur. Bu, bağımlılık enjeksiyonu ilkesinin ihlali olurdu. +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]. +Bkz. [ayrı bölüm|services]. -Dekoratör (Decorator) -===================== +Decorator +========= -Belirli bir tipteki tüm servisleri toplu olarak nasıl düzenlersiniz? Örneğin, belirli bir ortak atadan kalıtım alan tüm presenter'larda belirli bir metodu çağırmak? İşte bunun için dekoratör var. +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 veya arayüzün örneği olan tüm servislerde + # 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 ``` -Dekoratör ayrıca [etiketleri |services#Etiketler Tags] ayarlamak veya [inject modunu |services#Inject Modu] açmak için de kullanılabilir. +Decorator'lar [etiket |services#Etiketler] koymak ya da [inject kipini |services#Inject Kipi] açmak için de kullanılabilir. ```neon decorator: @@ -87,90 +87,90 @@ decorator: DI === -DI konteynerinin teknik ayarları. +DI container'ının teknik ayarları. ```neon di: - # DIC'yi Tracy Bar'da göster? - debugger: ... # (bool) varsayılan true'dur + # DIC, Tracy Bar'da gösterilsin mi? + debugger: ... # (bool) varsayılan otomatik saptama (Tracy varsa açık) - # asla otomatik bağlanmayacak parametre tipleri + # asla autowiring'e girmeyecek parametre türleri excluded: ... # (string[]) - # servislerin tembel oluşturulmasına izin ver? - lazy: ... # (bool) varsayılan false'dur + # tembel servis oluşturma açık olsun mu? + lazy: ... # (bool) varsayılan false - # DI konteynerinin kalıtım aldığı sınıf - parentClass: ... # (string) varsayılan Nette\DI\Container'dır + # 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. Bu, servislerin DI konteynerinden talep edildiği anda değil, ilk kullanıldıkları anda gerçekten oluşturulduğu anlamına gelir. Bu, uygulamanın başlangıcını hızlandırabilir ve bellek gereksinimlerini azaltabilir, çünkü yalnızca ilgili istekte gerçekten ihtiyaç duyulan servisler oluşturulur. +`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 [değiştirilebilir |services#Lazy Servisler]. +Belirli bir servis için tembel oluşturma [ayarlanabilir |services#Tembel Servisler]. .[note] -Tembel nesneler yalnızca kullanıcı sınıfları için kullanılabilir, dahili PHP sınıfları için kullanılamaz. PHP 8.4 veya daha yenisini gerektirir. +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 Aktarma ----------------------- +Meta Veri Dışa Aktarımı +----------------------- -DI konteyner sınıfı ayrıca birçok meta veri içerir. Meta veri dışa aktarımını azaltarak onu küçültebilirsiniz. +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: - # parametreleri dışa aktar? - parameters: false # (bool) varsayılan true'dur + # parametreler dışa aktarılsın mı? + parameters: false # (bool) varsayılan true - # etiketleri ve hangilerini dışa aktar? + # etiketler dışa aktarılsın mı, hangileri? tags: # (string[]|bool) varsayılan hepsi - event.subscriber - # otomatik bağlama için verileri ve hangilerini dışa aktar? + # autowiring verileri dışa aktarılsın mı, hangileri? types: # (string[]|bool) varsayılan hepsi - Nette\Database\Connection - Symfony\Component\Console\Application ``` -Eğer `$container->getParameters()` dizisini kullanmıyorsanız, parametre dışa aktarımını kapatabilirsiniz. Ayrıca, yalnızca `$container->findByTag(...)` metoduyla servis aldığınız etiketleri dışa aktarabilirsiniz. Eğer metodu hiç çağırmıyorsanız, etiket dışa aktarımını `false` ile tamamen kapatabilirsiniz. +`$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. -`$container->getByType()` metodunun parametresi olarak kullandığınız sınıfları belirterek [otomatik bağlama |autowiring] için meta veriyi önemli ölçüde azaltabilirsiniz. Ve yine, eğer metodu hiç çağırmıyorsanız (veya yalnızca `Nette\Application\Application` almak için [bootstrap |application:bootstrapping] içinde çağırıyorsanız), dışa aktarımı `false` ile tamamen 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. -Uzantılar -========= +Extension'lar +============= -Diğer DI uzantılarının kaydı. Bu şekilde örneğin `Dibi\Bridges\Nette\DibiExtension22` DI uzantısını `dibi` adı altında ekleriz +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\DibiExtension22 + dibi: Dibi\Bridges\Nette\DibiExtension3 ``` -Daha sonra onu `dibi` bölümünde yapılandırırız: +Sonra onu `dibi` bölümünde yapılandırırsınız: ```neon dibi: host: localhost ``` -Parametreleri olan bir sınıf da uzantı olarak eklenebilir: +Extension olarak parametreli bir sınıf da ekleyebilirsiniz: ```neon extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) + application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, [%appDir%], %tempDir%/cache) ``` -Dosya Dahil Etme -================ +Dosyaları Dahil Etme +==================== -Diğer yapılandırma dosyalarını `includes` bölümüne ekleyebiliriz: +Ek yapılandırma dosyaları `includes` bölümünde dahil edilebilir: ```neon includes: @@ -179,7 +179,7 @@ includes: - presenters.neon ``` -`parameters.php` adı bir yazım hatası değildir, yapılandırma PHP dosyasında da yazılabilir ve bir dizi olarak döndürülebilir: +`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 @@ -192,15 +192,15 @@ return [ ]; ``` -Yapılandırma dosyalarında aynı anahtarlara sahip öğeler görünürse, üzerine yazılır veya [diziler durumunda birleştirilir |#Birleştirme]. Daha sonra dahil edilen dosya, öncekinden daha yüksek önceliğe sahiptir. `includes` bölümünün belirtildiği dosya, içine dahil edilen dosyalardan daha yüksek önceliğe sahiptir. +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. -Arama (Search) -============== +Search +====== -Servislerin DI konteynerine otomatik olarak eklenmesi işi son derece keyifli hale getirir. Nette, presenter'ları otomatik olarak konteynere ekler, ancak diğer herhangi bir sınıfı da kolayca eklemek mümkündür. +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. -Sadece hangi dizinlerde (ve alt dizinlerde) sınıfları araması gerektiğini belirtmek yeterlidir: +Yalnızca sınıfların hangi dizinlerde (ve alt dizinlerinde) aranacağını belirtin: ```neon search: @@ -208,7 +208,14 @@ search: - in: %appDir%/Model ``` -Ancak genellikle tüm sınıfları ve arayüzleri eklemek istemeyiz, bu yüzden onları filtreleyebiliriz: +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: @@ -223,7 +230,7 @@ search: - *Factory ``` -Veya belirtilen sınıflardan en az birini kalıtım alan veya uygulayan sınıfları seçebiliriz: +Ya da listelenen sınıflardan en az birinden türeyen veya onu uygulayan sınıfları seçebiliriz: ```neon @@ -235,7 +242,7 @@ search: - App\*FormInterface ``` -Ayrıca dışlama kuralları da tanımlanabilir, yani sınıf adı maskeleri veya kalıtımsal atalar, eğer uyuyorsa servis DI konteynerine eklenmez: +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: @@ -247,7 +254,7 @@ search: implements: ... ``` -Tüm servislere etiketler atanabilir: +Otomatik kaydedilen tüm servislere etiket atanabilir: ```neon search: @@ -255,11 +262,13 @@ search: 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 fazla yapılandırma dosyasında aynı anahtarlara sahip öğeler görünürse, üzerine yazılır veya diziler durumunda birleştirilir. Daha sonra dahil edilen dosya, öncekinden daha yüksek önceliğe sahiptir. +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> @@ -292,7 +301,7 @@ items: </tr> </table> -Dizilerde, anahtar adından sonra ünlem işareti belirterek birleştirmeyi önleyebilirsiniz: +Dizilerde birleştirme, anahtar adından sonra bir ünlem işareti eklenerek önlenebilir: <table class=table> <tr> diff --git a/dependency-injection/tr/container.texy b/dependency-injection/tr/container.texy index 520e53b491..6931f03e92 100644 --- a/dependency-injection/tr/container.texy +++ b/dependency-injection/tr/container.texy @@ -1,16 +1,16 @@ -DI Konteyner Nedir? +DI Container Nedir? ******************* .[perex] -Bağımlılık enjeksiyonu konteyneri (DIC), nesneleri örnekleyebilen ve yapılandırabilen bir sınıftır. +Bağımlılık enjeksiyonu container'ı (DIC ya da DI container), başka nesneleri (servis denir) örneklemekten ve yapılandırmaktan sorumlu bir nesnedir. -Belki sizi şaşırtacak ama birçok durumda bağımlılık enjeksiyonunun (kısaca DI) avantajlarından yararlanmak için bir bağımlılık enjeksiyonu konteynerine ihtiyacınız yoktur. Sonuçta, [giriş bölümü |introduction] içinde bile DI'yi somut örneklerle gösterdik ve hiçbir konteynere gerek yoktu. +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, birçok bağımlılığa sahip çok sayıda farklı nesneyi yönetmeniz gerekiyorsa, bir bağımlılık enjeksiyonu konteyneri gerçekten faydalı olacaktır. Bu, örneğin bir framework üzerine kurulu web uygulamaları için geçerlidir. +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 `UserController` sınıflarını tanıttık. Her ikisinin de bazı bağımlılıkları var, yani veritabanı ve `ArticleFactory` fabrikası. Ve şimdi bu sınıflar için bir konteyner oluşturacağız. Elbette, böylesine basit bir örnek için bir konteynere sahip olmanın anlamı yok. Ama nasıl göründüğünü ve çalıştığını göstermek için onu oluşturacağız. +Ö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 belirtilen örnek için basit, sabit kodlanmış (hardcoded) bir konteyner: +İşte yukarıdaki örnek için basit, sabit kodlanmış bir container: ```php class Container @@ -25,23 +25,23 @@ class Container return new ArticleFactory($this->createDatabase()); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->createArticleFactory()); + return new EditController($this->createArticleFactory()); } } ``` -Kullanım şöyle görünürdü: +Kullanımı şöyle görünürdü: ```php $container = new Container; -$controller = $container->createUserController(); +$controller = $container->createEditController(); ``` -Konteynere sadece nesneyi sorarız ve artık onu nasıl oluşturacağımızı ve bağımlılıklarının ne olduğunu bilmemize gerek yoktur; tüm bunları konteyner bilir. Bağımlılıklar konteyner tarafından otomatik olarak enjekte edilir. Gücü buradadır. +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. -Konteyner şimdilik tüm verileri sabit (hardcoded) olarak yazmıştır. Bu yüzden bir sonraki adımı atacağız ve konteynerin gerçekten kullanışlı olması için parametreler ekleyeceğiz: +Ş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 @@ -70,9 +70,9 @@ $container = new Container([ ]); ``` -Dikkatli okuyucular belki de belirli bir sorunu fark etmişlerdir. `UserController` nesnesini her aldığımda, yeni bir `ArticleFactory` örneği ve veritabanı da oluşturulur. Bunu kesinlikle istemiyoruz. +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 olan `getService()` metodunu ekleyeceğiz: +Bu yüzden, her zaman aynı örnekleri döndürecek bir `getService()` metodu ekleyeceğiz: ```php class Container @@ -87,7 +87,7 @@ class Container public function getService(string $name): object { if (!isset($this->services[$name])) { - // getService('Database') createDatabase() çağıracak + // getService('Database') çağrısı createDatabase() metodunu çağırır $method = 'create' . $name; $this->services[$name] = $this->$method(); } @@ -98,9 +98,9 @@ class Container } ``` -Örneğin `$container->getService('Database')` ilk çağrıldığında, `createDatabase()`'den veritabanı nesnesini oluşturmasını ister, onu `$services` dizisine kaydeder ve bir sonraki çağrıda doğrudan onu döndürür. +İ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. -Konteynerin geri kalanını da `getService()` kullanacak şekilde düzenleyeceğiz: +Container'ın geri kalanını da `getService()` kullanacak şekilde değiştiriyoruz: ```php class Container @@ -112,16 +112,16 @@ class Container return new ArticleFactory($this->getService('Database')); } - public function createUserController(): UserController + public function createEditController(): EditController { - return new UserController($this->getService('ArticleFactory')); + return new EditController($this->getService('ArticleFactory')); } } ``` -Bu arada, servis terimi konteyner tarafından yönetilen herhangi bir nesneyi ifade eder. Bu yüzden metot adı `getService()`'dir. +Bu arada, servis terimi container'ın yönettiği herhangi bir nesneyi anlatır. Metodun adı da bu yüzden `getService()`. -Bitti. Tamamen işlevsel bir DI konteynerimiz var! Ve onu kullanabiliriz: +Bitti. Tam işlevsel bir DI container'ımız var! Ve onu kullanabiliriz: ```php $container = new Container([ @@ -130,13 +130,13 @@ $container = new Container([ 'db.password' => '***', ]); -$controller = $container->getService('UserController'); +$controller = $container->getService('EditController'); $database = $container->getService('Database'); ``` -Gördüğünüz gibi, bir DIC yazmak karmaşık bir şey değil. Nesnelerin kendilerinin bir konteyner tarafından oluşturulduğunu bilmediklerini hatırlatmakta fayda var. Bu nedenle, kaynak koduna müdahale etmeden PHP'deki herhangi bir nesneyi bu şekilde oluşturmak mümkündür. +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. -Konteyner sınıfını manuel olarak oluşturmak ve bakımını yapmak oldukça hızlı bir şekilde bir kabusa dönüşebilir. Bu yüzden bir sonraki bölümde, neredeyse kendi kendine üretebilen ve güncelleyebilen [Nette DI Konteyner |nette-container] hakkında konuşacağız. +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 konteyneri nedir?}} +{{maintitle: Bağımlılık Enjeksiyonu Container'ı Nedir?}} diff --git a/dependency-injection/tr/extensions.texy b/dependency-injection/tr/extensions.texy index 0822983ae3..fb35e44889 100644 --- a/dependency-injection/tr/extensions.texy +++ b/dependency-injection/tr/extensions.texy @@ -1,39 +1,67 @@ -Nette DI için Uzantı Oluşturma -****************************** +Nette DI İçin Extension Yazma +***************************** .[perex] -DI konteynerinin oluşturulması, yapılandırma dosyalarının yanı sıra *uzantılar* olarak adlandırılanlar tarafından da etkilenir. Bunları yapılandırma dosyasında `extensions` bölümünde etkinleştiririz. +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. -Bu şekilde, `BlogExtension` sınıfı tarafından temsil edilen uzantıyı `blog` adı altında ekleriz: +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 ``` -Her derleyici uzantısı [api:Nette\DI\CompilerExtension]'dan kalıtım alır ve DI konteynerinin oluşturulması sırasında sırayla çağrılan aşağıdaki metotları uygulayabilir: +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. -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() +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]) -getConfigSchema() .[method] -=========================== +.[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. -Bu metot ilk olarak çağrılır. Yapılandırma parametrelerinin doğrulanması için şemayı tanımlar. +.[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]. -Uzantıyı, uzantının eklendiği adla aynı olan bölümde, yani `blog`'da yapılandırırız: + +İlk Extension +============= + +İşte küçük ama eksiksiz bir extension. Onu aynı dosyada etkinleştirip yapılandırıyoruz: ```neon -# uzantı adıyla aynı +extensions: + blog: BlogExtension + blog: - postsPerPage: 10 - allowComments: false + postsPerPage: 5 ``` -Tipleri, izin verilen değerleri ve isteğe bağlı olarak varsayılan değerleri de dahil olmak üzere tüm yapılandırma seçeneklerini açıklayan bir şema oluştururuz: +Ve sınıfın tamamı şu: ```php use Nette\Schema\Expect; @@ -43,62 +71,87 @@ class BlogExtension extends Nette\DI\CompilerExtension public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), + 'postsPerPage' => Expect::int(10), + 'allowComments' => Expect::bool(true), ]); } -} -``` - -Dokümantasyonu [Schema |schema:] sayfasında bulabilirsiniz. Ayrıca, `dynamic()` kullanarak hangi seçeneklerin [dinamik |application:bootstrapping#Dinamik Parametreler] olabileceğini belirleyebilirsiniz, örn. `Expect::int()->dynamic()`. -Yapılandırmaya, bir `stdClass` nesnesi olan `$this->config` değişkeni aracılığıyla erişiriz: -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() + public function loadConfiguration(): void { - $num = $this->config->postPerPage; + $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. -loadConfiguration() .[method] -============================= +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. -Konteynere servis eklemek için kullanılır. Bunun için [api:Nette\DI\ContainerBuilder] kullanılır: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function getConfigSchema(): Nette\Schema\Schema { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // veya setCreator() - ->addSetup('setLogger', ['@logger']); - } + return Expect::structure([ + 'postsPerPage' => Expect::int(10), + 'storage' => Expect::anyOf('files', 'database')->firstIsDefault(), + ]); } ``` -Kural, uzantı tarafından eklenen servisleri adıyla ön eklemektir, böylece isim çakışmaları olmaz. Bunu `prefix()` metodu yapar, yani uzantı adı `blog` ise, servis `blog.articles` adını taşır. +Doğrulanan yapılandırma, `$this->config` içinde bir `stdClass` nesnesi olarak bulunur (şemaya `castTo('array')` eklerseniz dizi olarak). -Bir servisi yeniden adlandırmamız gerekirse, geriye dönük uyumluluğu korumak için orijinal adla bir takma ad (alias) oluşturabiliriz. Nette bunu benzer şekilde yapar, örn. önceki adı `router` altında da mevcut olan `routing.router` servisi için. +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 -$builder->addAlias('router', 'routing.router'); +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: -Dosyadan Servis Yükleme ------------------------ +```php +if ($this->name === 'blog') { + $builder->addAlias('articles', $this->prefix('articles')); +} +``` -Servisleri yalnızca ContainerBuilder sınıfının API'sini kullanarak değil, aynı zamanda yapılandırma dosyasında `services` bölümünde kullanılan bilinen yazımla da oluşturabiliriz. `@extension` ön eki mevcut uzantıyı temsil eder. +Ç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: @@ -107,88 +160,284 @@ services: 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: - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) +```php +public function loadConfiguration(): void +{ + $this->loadDefinitionsFromConfig( + $this->loadFromFile(__DIR__ . '/services.neon')['services'], + ); +} ``` -Servisleri yükleriz: + +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 -class BlogExtension extends Nette\DI\CompilerExtension +public function beforeCompile(): void { - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder(); - // uzantı için yapılandırma dosyasını yükleme - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); + 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. -beforeCompile() .[method] -========================= +`$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: -Metot, konteynerin `loadConfiguration` metotlarında bireysel uzantılar tarafından eklenen tüm servisleri ve ayrıca kullanıcı yapılandırma dosyalarını içerdiği anda çağrılır. Bu derleme aşamasında, servis tanımlarını düzenleyebilir veya aralarındaki bağlantıları tamamlayabiliriz. Konteynerdeki servisleri etiketlere göre aramak için `findByTag()` metodunu, sınıfa veya arayüze göre aramak için ise `findByType()` metodunu kullanabiliriz. +```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 -class BlogExtension extends Nette\DI\CompilerExtension +public function afterCompile(Nette\PhpGenerator\ClassType $class): void { - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); + $method = $class->getMethod('__construct'); + // ... +} +``` - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } +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. + -afterCompile() .[method] -======================== +ContainerBuilder +================ -Bu aşamada, konteyner sınıfı zaten bir [ClassType |php-generator:#Sınıflar] nesnesi şeklinde üretilmiştir, servisleri oluşturan tüm metotları içerir ve önbelleğe yazılmaya hazırdır. Sonuç kodunu bu noktada hala düzenleyebiliriz. +[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 afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } + public function __construct( + private bool $debugMode = false, + ) {} } ``` +Tipik bir kullanım, bir Tracy panelini yalnızca geliştirici kipinde kaydetmektir. + -$initialization .[method] -========================= +Karmaşık Argümanlar +------------------- -Configurator sınıfı, [konteyner oluşturulduktan sonra |application:bootstrapping#index.php] `$this->initialization` nesnesine [addBody() metodu |php-generator:#Metot ve Fonksiyon Gövdeleri] kullanılarak yazılan başlatma kodunu çağırır. +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: -Örneğin, başlatma koduyla oturumu nasıl başlatacağımızı veya `run` etiketine sahip servisleri nasıl çalıştıracağımızı gösteren bir örnek: +- `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 -class BlogExtension extends Nette\DI\CompilerExtension +$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 { - public function loadConfiguration() - { - // oturumun otomatik başlatılması - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } + // bu etiket, dışa aktarım kısıtlansa bile her zaman dışa aktarılır + $this->compiler->addExportedTag('event.subscriber'); - // run etiketli servisler konteyner örneklendikten sonra oluşturulmalıdır - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } + // 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 index 80def9a73c..d02415f06e 100644 --- a/dependency-injection/tr/factory.texy +++ b/dependency-injection/tr/factory.texy @@ -1,12 +1,12 @@ -Üretilmiş Fabrikalar +Üretilen Factory'ler ******************** .[perex] -Nette DI, arayüzlere dayalı olarak fabrika kodunu otomatik olarak üretebilir, bu da size kod yazmaktan tasarruf sağlar. +Nette DI, arayüzlere dayanarak factory kodunu otomatik üretebilir ve sizi kod yazmaktan kurtarır. -Fabrika, nesneleri üreten ve yapılandıran bir sınıftır. Dolayısıyla onlara bağımlılıklarını da aktarır. Lütfen bunu, fabrikaların belirli bir kullanım şeklini açıklayan ve bu konuyla ilgisi olmayan *factory method* tasarım deseniyle karıştırmayın. +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 fabrikanın nasıl göründüğünü [giriş bölümü |introduction#Fabrika] içinde gösterdik: +Böyle bir factory'nin nasıl göründüğünü [giriş bölümünde |introduction#Factory] gösterdik: ```php class ArticleFactory @@ -23,7 +23,7 @@ class ArticleFactory } ``` -Nette DI, fabrika kodunu otomatik olarak üretebilir. Tek yapmanız gereken bir arayüz oluşturmaktır ve Nette DI uygulamayı üretecektir. Arayüzün tam olarak `create` adında bir metodu olmalı ve dönüş değeri tipini bildirmelidir: +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 @@ -32,7 +32,7 @@ interface ArticleFactory } ``` -Yani `ArticleFactory` fabrikasının, `Article` nesneleri oluşturan bir `create` metodu vardır. `Article` sınıfı örneğin şöyle görünebilir: +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 @@ -44,16 +44,16 @@ class Article } ``` -Fabrikayı yapılandırma dosyasına ekleriz: +Factory'yi yapılandırma dosyasına ekleyin: ```neon services: - ArticleFactory ``` -Nette DI, fabrikanın ilgili uygulamasını üretecektir. +Nette DI ilgili factory gerçekleştirimini üretir. -Fabrikayı kullanan kodda, nesneyi arayüze göre talep ederiz ve Nette DI üretilen uygulamayı kullanır: +Factory'yi kullanan kodda nesneyi arayüzü üzerinden isteyin; Nette DI üretilen gerçekleştirimi verir: ```php class UserController @@ -65,17 +65,17 @@ class UserController public function foo() { - // fabrikanın nesneyi oluşturmasına izin veririz + // bırakın nesneyi factory oluştursun $article = $this->articleFactory->create(); } } ``` -Parametreli Fabrika +Parametreli Factory =================== -Fabrika metodu `create`, daha sonra kurucuya aktarılacak parametreleri kabul edebilir. Örneğin, `Article` sınıfına makale yazarının ID'sini ekleyelim: +`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 @@ -88,7 +88,7 @@ class Article } ``` -Parametreyi fabrikaya da ekleriz: +Parametreyi factory'ye de ekleyeceğiz: ```php interface ArticleFactory @@ -97,13 +97,13 @@ interface ArticleFactory } ``` -Kurucudaki parametre ile fabrikadaki parametrenin aynı adı taşıması sayesinde, Nette DI bunları tamamen otomatik olarak aktarır. +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ını kullanarak çok satırlı bir formda da yazılabilir: +Tanım, `implement` anahtarı kullanılarak çok satırlı biçimde de yazılabilir: ```neon services: @@ -111,9 +111,9 @@ services: implement: ArticleFactory ``` -Bu daha uzun yolla yazarken, normal servislerde olduğu gibi `arguments` anahtarında kurucu için ek argümanlar ve `setup` kullanarak ek yapılandırma belirtmek mümkündür. +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: Eğer `create()` metodu `$authorId` parametresini kabul etmeseydi, yapılandırmada `Article` kurucusuna aktarılacak sabit bir değer belirtebilirdik: +Örnek: `create()` metodu `$authorId` parametresini almasaydı, `Article` yapıcısına aktarılacak sabit bir değeri yapılandırmada verebilirdik: ```neon services: @@ -123,7 +123,7 @@ services: authorId: 123 ``` -Veya tam tersi, eğer `create()` `$authorId` parametresini kabul etseydi, ancak kurucunun bir parçası olmasaydı ve `Article::setAuthorId()` metoduyla aktarılsaydı, ona `setup` bölümünde başvururduk: +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: @@ -134,14 +134,14 @@ services: ``` -Erişimci (Accessor) -=================== +Accessor +======== -Nette, fabrikaların yanı sıra erişimciler (accessor) olarak adlandırılanları da üretebilir. Bunlar, DI konteynerinden belirli bir servisi döndüren bir `get()` metoduna sahip nesnelerdir. `get()`'in tekrarlanan çağrıları her zaman aynı örneği döndürür. +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. -Erişimciler, bağımlılıklara tembel yükleme (lazy-loading) sağlar. Hataları özel bir veritabanına yazan bir sınıfımız olduğunu varsayalım. Eğer bu sınıf veritabanı bağlantısını kurucu bağımlılığı olarak alsaydı, bağlantı her zaman oluşturulmak zorunda kalırdı, ancak pratikte hata yalnızca istisnai olarak ortaya çıkar ve bu nedenle çoğu zaman bağlantı kullanılmadan kalırdı. Bunun yerine, sınıf bir erişimci aktarır ve yalnızca onun `get()` metodu çağrıldığında veritabanı nesnesi oluşturulur: +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. -Bir erişimci nasıl oluşturulur? Sadece bir arayüz yazmanız yeterlidir ve Nette DI uygulamayı üretecektir. Arayüzün tam olarak `get` adında bir metodu olmalı ve dönüş değeri tipini bildirmelidir: +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 @@ -150,7 +150,7 @@ interface PDOAccessor } ``` -Erişimciyi, döndüreceği servisin tanımının da bulunduğu yapılandırma dosyasına ekleriz: +Accessor'ı, döndürmesi gereken servisin tanımıyla birlikte yapılandırma dosyasına ekleyin: ```neon services: @@ -158,12 +158,13 @@ services: - PDO(%dsn%, %user%, %password%) ``` -Erişimci `PDO` tipinde bir servis döndürdüğü ve yapılandırmada bu türden tek bir servis olduğu için, tam olarak onu döndürecektir. Eğer ilgili tipten birden fazla servis olsaydı, döndürülen servisi adıyla belirlerdik, örn. `- PDOAccessor(@db1)`. +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 Fabrika/Erişimci +Çoklu Factory/Accessor ====================== -Fabrikalarımız ve erişimcilerimiz şimdiye kadar her zaman yalnızca bir nesne üretebildi veya döndürebildi. Ancak, erişimcilerle birleştirilmiş çoklu fabrikaları da çok kolay bir şekilde oluşturmak mümkündür. Böyle bir sınıfın arayüzü, `create<name>()` ve `get<name>()` adlarına sahip istenilen sayıda metot içerecektir, örn.: + +Ş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 @@ -173,9 +174,9 @@ interface MultiFactory } ``` -Yani, birkaç üretilmiş fabrika ve erişimci aktarmak yerine, daha fazlasını yapabilen daha karmaşık bir fabrika aktarırız. +Yani birden çok ayrı factory ve accessor enjekte etmek yerine tek ve daha kapsamlı bir bileşen enjekte edebilirsiniz. -Alternatif olarak, birkaç metot yerine parametreli `get()` kullanılabilir: +Alternatif olarak, birden çok metot yerine parametreli bir `get()` kullanılabilir: ```php interface MultiFactoryAlt @@ -184,22 +185,24 @@ interface MultiFactoryAlt } ``` -O zaman `MultiFactory::getArticle()`'ın `MultiFactoryAlt::get('article')` ile aynı şeyi yaptığı geçerlidir. Ancak, alternatif yazımın dezavantajı, hangi `$name` değerlerinin desteklendiğinin açık olmaması ve mantıksal olarak arayüzde farklı `$name` için farklı dönüş değerlerini ayırt etmenin mümkün olmamasıdır. +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. -Liste ile Tanım ---------------- -Bu şekilde, yapılandırmada çoklu bir fabrika tanımlanabilir: .{data-version:3.2.0} + +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 + article: Article() # createArticle() tanımlar db: PDO(%dsn%, %user%, %password%) # getDb() tanımlar ) ``` -Veya fabrika tanımında mevcut servislere referansla başvurabiliriz: +Alternatif olarak, çoklu factory tanımında var olan servislere referanslarla başvurabilirsiniz: ```neon services: @@ -215,12 +218,16 @@ services: Etiketlerle Tanım ----------------- -İkinci seçenek, tanım için [etiketleri |services#Etiketler Tags] kullanmaktır: +Ç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: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) + 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 index 298e6277f8..b45ccdd923 100644 --- a/dependency-injection/tr/faq.texy +++ b/dependency-injection/tr/faq.texy @@ -1,106 +1,123 @@ -DI Hakkında Sıkça Sorulan Sorular (SSS) +DI Hakkında Sıkça Sorulan Sorular (FAQ) *************************************** -DI, IoC'nin başka bir adı mıdır? --------------------------------- +DI, IoC'nin başka bir adı mı? +----------------------------- -*Kontrolün Tersine Çevrilmesi* (IoC - Inversion of Control), kodun nasıl çalıştırıldığına odaklanan bir ilkedir - kodunuzun yabancı bir kodu mu çalıştırdığı yoksa kodunuzun onu daha sonra çağıran yabancı bir koda mı entegre edildiği. IoC, [olayları |nette:glossary#Olaylar Events], [Hollywood İlkesi |application:components#Hollywood Tarzı] olarak adlandırılanı ve diğer yönleri kapsayan geniş bir kavramdır. Bu konseptin bir parçası, [Kural No. 3: Fabrikaya bırak |introduction#Kural 3: Fabrikaya Bırakın]'da bahsedilen ve `new` operatörü için bir tersine çevirme temsil eden fabrikalardır. +*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. -*Bağımlılık Enjeksiyonu* (DI - Dependency Injection), bir nesnenin başka bir nesne hakkında, yani bağımlılıkları hakkında nasıl bilgi edindiğine odaklanır. Nesneler arasında bağımlılıkların açıkça aktarılmasını gerektiren bir tasarım desenidir. +*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'nin IoC'nin özel bir formu olduğu söylenebilir. Ancak, tüm IoC formları kod temizliği açısından uygun değildir. Örneğin, anti-desenler arasında [global durum |global-state] ile çalışan teknikler veya [Servis Bulucu |#Servis Bulucu Service Locator Nedir] olarak adlandırılan teknikler bulunur. +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. -Servis Bulucu (Service Locator) Nedir? --------------------------------------- +Service Locator nedir? +---------------------- -Bağımlılık Enjeksiyonu'na bir alternatiftir. Mevcut tüm servislerin veya bağımlılıkların kaydedildiği merkezi bir depo oluşturarak çalışır. Bir nesne bir bağımlılığa ihtiyaç duyduğunda, onu Servis Bulucu'dan ister. +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, Bağımlılık Enjeksiyonu'na kıyasla şeffaflığını kaybeder: bağımlılıklar nesnelere doğrudan aktarılmaz ve bu nedenle kolayca tanımlanamaz, bu da tüm bağlantıları ortaya çıkarmak ve anlamak için kodun incelenmesini gerektirir. Test etme de daha karmaşıktır, çünkü mock nesnelerini test edilen nesnelere basitçe aktaramayız, bunun yerine Servis Bulucu üzerinden gitmemiz gerekir. Ayrıca, Servis Bulucu kod tasarımını bozar, çünkü bireysel nesnelerin onun varlığından haberdar olması gerekir, bu da nesnelerin DI konteynerinden haberdar olmadığı Bağımlılık Enjeksiyonu'ndan farklıdır. +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 Ne Zaman Kullanılmamalıdır? ------------------------------- +DI'yı ne zaman kullanmamak daha iyidir? +--------------------------------------- -Bağımlılık Enjeksiyonu tasarım deseninin kullanımıyla ilişkili bilinen herhangi bir zorluk yoktur. Aksine, bağımlılıkları global olarak erişilebilir yerlerden almak [bir dizi komplikasyona |global-state] yol açar, aynı şekilde Servis Bulucu kullanımı da. Bu nedenle, DI'yi her zaman kullanmak uygundur. Bu dogmatik bir yaklaşım değildir, sadece daha iyi bir alternatif bulunamamıştır. +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, nesneleri aktarmadığımız ve onları global alandan aldığımız belirli durumlar vardır. Örneğin, kod hata ayıklarken, programın belirli bir noktasında bir değişkenin değerini yazdırmanız, programın belirli bir bölümünün süresini ölçmeniz veya bir mesaj kaydetmeniz gerektiğinde. Bu gibi durumlarda, daha sonra koddan kaldırılacak geçici görevler söz konusu olduğunda, global olarak erişilebilir bir dumper, kronometre veya logger kullanmak meşrudur. Bu araçlar çünkü kod tasarımına ait değildir. +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 Dezavantajları Var mı? -------------------------------------- +DI kullanmanın sakıncaları var mı? +---------------------------------- -Bağımlılık Enjeksiyonu kullanmak, örneğin kod yazma zorluğunun artması veya performansın kötüleşmesi gibi herhangi bir dezavantaj içerir mi? DI ile uyumlu kod yazmaya başladığımızda ne kaybederiz? +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'nin uygulamanın performansı veya bellek gereksinimleri üzerinde bir etkisi yoktur. DI Konteynerinin performansı belirli bir rol oynayabilir, ancak [Nette DI |nette-container] durumunda, konteyner saf PHP'ye derlenir, bu nedenle uygulama çalışma zamanındaki ek yükü (overhead) temelde sıfırdır. +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. -Kod yazarken, bağımlılıkları kabul eden kurucuları oluşturmak gerekli olabilir. Eskiden bu uzun sürebilirdi, ancak modern IDE'ler ve [kurucu özellik tanıtımı |https://blog.nette.org/tr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] sayesinde bu artık birkaç saniyelik bir meseledir. Fabrikalar, Nette DI ve PhpStorm için bir eklenti kullanılarak fare tıklamasıyla kolayca üretilebilir. Diğer yandan, singleton'lar ve statik erişim noktaları yazma ihtiyacı ortadan kalkar. +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. -DI kullanan doğru tasarlanmış bir uygulamanın, singleton'ları kullanan bir uygulamayla karşılaştırıldığında ne daha kısa ne de daha uzun olduğu söylenebilir. Bağımlılıklarla çalışan kod bölümleri yalnızca bireysel sınıflardan çıkarılır ve yeni yerlere, yani DI konteynerine ve fabrikalara taşınır. +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'ye Nasıl Yeniden Yazılır? ----------------------------------------------- +Eski bir uygulama DI'ya nasıl dönüştürülür? +------------------------------------------- -Eski bir uygulamadan (legacy application) Bağımlılık Enjeksiyonu'na geçiş, özellikle büyük ve karmaşık uygulamalarda zorlu bir süreç olabilir. Bu sürece sistematik olarak yaklaşmak önemlidir. +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 Enjeksiyonu'na geçerken, tüm takım üyelerinin kullanılan ilkeleri ve prosedürleri anlaması önemlidir. -- İlk olarak, mevcut uygulamanın analizini yapın ve anahtar bileşenleri ve bağımlılıklarını tanımlayın. Hangi bölümlerin yeniden düzenleneceğini (refactored) ve hangi sırayla yapılacağını içeren bir plan oluşturun. -- Bir DI konteyneri uygulayın veya daha da iyisi, örneğin Nette DI gibi mevcut bir kütüphaneyi kullanın. -- Bağımlılık Enjeksiyonu'nu kullanmak için uygulamanın bireysel bölümlerini adım adım yeniden düzenleyin. Bu, bağımlılıkları parametre olarak kabul etmek için kurucuların veya metotların düzenlenmesini içerebilir. -- Kodda bağımlılıkları olan nesnelerin oluşturulduğu yerleri, bunun yerine bağımlılıkların konteyner tarafından enjekte edilmesi için düzenleyin. Bu, fabrikaların kullanımını içerebilir. +- 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 Enjeksiyonu'na geçişin kod kalitesine ve uygulamanın uzun vadeli sürdürülebilirliğine yapılan bir yatırım olduğunu unutmayın. Bu değişiklikleri yapmak zorlu olsa da, sonuç daha temiz, daha modüler ve kolayca test edilebilir, gelecekteki genişletmelere ve bakıma hazır bir kod olmalıdır. +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. -Neden Kalıtım Yerine Kompozisyon Tercih Edilir? ------------------------------------------------ -Değişikliklerin sonuçları hakkında endişelenmeden kodu yeniden kullanmamıza hizmet ettiği için [kalıtım |nette:introduction-to-object-oriented-programming#Kompozisyon] yerine [kompozisyonu |nette:introduction-to-object-oriented-programming#Kalıtım] kullanmak daha uygundur. Dolayısıyla, bir kod değişikliğinin başka bir bağımlı kodun değiştirilmesi ihtiyacına neden olacağından endişelenmemize gerek olmayan daha gevşek bir bağlantı sağlar. Tipik bir örnek, [kurucu cehennemi |passing-dependencies#Constructor Hell] olarak adlandırılan durumdur. +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 Konteyner Nette Dışında Kullanılabilir mi? ---------------------------------------------------- +Nette DI Container, Nette dışında kullanılabilir mi? +---------------------------------------------------- -Kesinlikle. Nette DI Konteyner, Nette'nin bir parçasıdır, ancak framework'ün diğer bölümlerinden bağımsız olarak kullanılabilecek bağımsız bir kütüphane olarak tasarlanmıştır. Sadece Composer kullanarak yüklemeniz, servislerinizin tanımıyla bir yapılandırma dosyası oluşturmanız ve ardından birkaç satır PHP kodu kullanarak bir DI konteyneri oluşturmanız yeterlidir. Ve hemen projelerinizde Bağımlılık Enjeksiyonu'nun avantajlarından yararlanmaya başlayabilirsiniz. +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. -Kodlar dahil olmak üzere somut kullanımın nasıl göründüğünü [Nette DI Konteyner |nette-container] bölümü açıklar. +[Nette DI Container |nette-container] bölümü, kod örnekleriyle somut bir kullanım durumunu anlatır. -Yapılandırma Neden NEON Dosyalarındadır? ----------------------------------------- +Yapılandırma neden NEON dosyalarında? +------------------------------------- -NEON, uygulamaları, servisleri ve bağımlılıklarını ayarlamak için Nette kapsamında geliştirilmiş basit ve kolay okunabilir bir yapılandırma dilidir. JSON veya YAML ile karşılaştırıldığında, bu amaç için çok daha sezgisel ve esnek seçenekler sunar. NEON'da, Symfony & YAMLu'da ya hiç yazılamayacak ya da sadece karmaşık bir tanım aracılığıyla yazılabilecek bağlantıları doğal olarak tanımlamak mümkündür. +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ı Ayrıştırmak Uygulamayı Yavaşlatır mı? ------------------------------------------------------- +NEON dosyalarının ayrıştırılması uygulamayı yavaşlatır mı? +---------------------------------------------------------- -NEON dosyaları çok hızlı ayrıştırılsa da, bu bakış açısı hiç önemli değildir. Nedeni, dosyaların ayrıştırılmasının yalnızca uygulamanın ilk çalıştırılmasında bir kez gerçekleşmesidir. Daha sonra DI konteynerinin kodu üretilir, diske kaydedilir ve daha fazla ayrıştırma yapmaya gerek kalmadan sonraki her istekte çalıştırılır. +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. -Bu, üretim ortamında bu şekilde çalışır. Geliştirme sırasında, geliştiricinin her zaman güncel bir DI konteynerine sahip olması için NEON dosyaları içerikleri her değiştiğinde ayrıştırılır. Ayrıştırmanın kendisi, söylendiği gibi, anlık bir meseledir. +Ü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. -Sınıfımdan Yapılandırma Dosyasındaki Parametrelere Nasıl Erişirim? +Yapılandırma dosyasındaki parametrelere sınıfımdan nasıl erişirim? ------------------------------------------------------------------ -[Kural No. 1: Sana aktarılmasına izin ver |introduction#Kural 1: Size İletilmesini Sağlayın]'i aklımızda bulunduralım. Eğer sınıf yapılandırma dosyasından bilgi gerektiriyorsa, bilgiye nasıl ulaşacağımızı düşünmemize gerek yok, bunun yerine basitçe isteriz - örneğin sınıfın kurucusu aracılığıyla. Ve aktarımı yapılandırma dosyasında gerçekleştiririz. +[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` sınıfının kurucusuna aktarılan `myParameter` parametresinin değeri için bir yer tutucu semboldür: +Bu örnekte `%myParameter%`, `MyClass` yapıcısına aktarılacak `myParameter` parametresinin değerinin yer tutucusudur: -```php +```neon # config.neon parameters: - myParameter: Some value + myParameter: Bir değer services: - MyClass(%myParameter%) ``` -Daha fazla parametre aktarmak veya otomatik bağlama kullanmak istiyorsanız, [parametreleri bir nesneye sarmak |best-practices:passing-settings-to-presenters] uygundur. +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: Konteyner Arayüzünü Destekliyor mu? +Nette, PSR-11 Container arayüzünü destekliyor mu? ------------------------------------------------- -Nette DI Konteyner, PSR-11'i doğrudan desteklemez. Ancak, Nette DI Konteyneri ile PSR-11 Konteyner Arayüzü bekleyen kütüphaneler veya framework'ler arasında birlikte çalışabilirliğe ihtiyacınız varsa, Nette DI Konteyneri ile PSR-11 arasında köprü görevi görecek [basit bir adaptör |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] oluşturabilirsiniz. +[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 index b1cb35c9ca..282cba33f2 100644 --- a/dependency-injection/tr/global-state.texy +++ b/dependency-injection/tr/global-state.texy @@ -1,63 +1,63 @@ -Global Durum ve Singleton'lar -***************************** +Genel Durum ve Singleton'lar +**************************** .[perex] -Uyarı: Aşağıdaki yapılar kötü tasarlanmış kodun işaretidir: +Uyarı: Aşağıdaki yapılar, kötü tasarlanmış kodun belirtileridir: - `Foo::getInstance()` - `DB::insert(...)` - `Article::setDb($db)` -- `ClassName::$var` veya `static::$var` +- `ClassName::$var` ya da `static::$var` -Bu yapılardan bazıları kodunuzda bulunuyor mu? O zaman onu iyileştirme fırsatınız var. Belki de bunların, çeşitli kütüphanelerin ve framework'lerin örnek çözümlerinde bile gördüğünüz yaygın yapılar olduğunu düşünüyorsunuzdur. Eğer durum buysa, o zaman kodlarının tasarımı iyi değildir. +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. -Şimdi kesinlikle bir tür akademik saflıktan bahsetmiyoruz. Tüm bu yapıların ortak bir yanı var: global durumu kullanıyorlar. Ve bunun kod kalitesi üzerinde yıkıcı bir etkisi var. Sınıflar bağımlılıkları hakkında yalan söylüyor. Kod öngörülemez hale geliyor. Programcıları şaşırtıyor ve verimliliklerini düşürüyor. +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, neden böyle olduğunu ve global durumdan nasıl kaçınılacağını açıklayacağız. +Bu bölümde bunun neden böyle olduğunu ve genel durumdan nasıl kaçınılacağını açıklayacağız. -Global Bağlantı ---------------- +Genel Bağlantılar +----------------- -İdeal bir dünyada, bir nesne yalnızca [doğrudan aktarılan |passing-dependencies] nesnelerle iletişim kurabilmelidir. Eğer iki `A` ve `B` nesnesi oluşturursam ve aralarında asla bir referans aktarmazsam, o zaman ne `A` ne de `B`, diğer nesneye erişemez veya durumunu değiştiremez. Bu, kodun çok istenen bir özelliğidir. Bu, bir piliniz ve bir ampulünüz olmasına benzer; ampulü pille bir telle bağlamadığınız sürece yanmaz. +İ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, global (statik) değişkenler veya singleton'lar için geçerli değildir. `A` nesnesi, `C::changeSomething()` çağırarak herhangi bir referans aktarımı olmadan *kablosuz olarak* `C` nesnesine erişebilir ve onu değiştirebilir. Eğer `B` nesnesi de global `C`'yi ele geçirirse, o zaman `A` ve `B` birbirini `C` aracılığıyla etkileyebilir. +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. -Global değişkenlerin kullanımı, sisteme dışarıdan görünmeyen yeni bir *kablosuz* bağlantı formu katar. Kodun anlaşılmasını ve kullanılmasını zorlaştıran bir sis perdesi oluşturur. Geliştiricilerin bağımlılıkları gerçekten anlamaları için, kaynak kodunun her satırını okumaları gerekir. Sadece sınıf arayüzleriyle tanışmak yerine. Üstelik bu tamamen gereksiz bir bağlantıdır. Global durum, her yerden kolayca erişilebilir olduğu ve örneğin `DB::insert()` global (statik) metodu aracılığıyla veritabanına yazmaya izin verdiği için kullanılır. Ama göstereceğimiz gibi, bunun getirdiği avantaj önemsizdir, aksine neden olduğu komplikasyonlar ölümcüldür. +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 global ve statik değişken arasında bir fark yoktur. Eşit derecede zararlıdırlar. +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" - 1935'te Albert Einstein, kuantum fiziğinde tüylerini diken diken eden bir olguyu bu şekilde ünlü bir şekilde adlandırdı. -Bu, kuantum dolaşıklığıdır ve özelliği, bir parçacık hakkındaki bilgiyi ölçtüğünüzde, milyonlarca ışık yılı uzakta olsalar bile diğer parçacığı anında etkilemenizdir. Bu, görünüşte evrenin temel yasasını, yani hiçbir şeyin ışıktan daha hızlı yayılamayacağını ihlal eder. +"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" olarak, izole olduğunu düşündüğümüz (çünkü ona hiçbir referans aktarmadık) bir süreci başlattığımızda, ancak sistemin uzak yerlerinde haberimiz olmayan beklenmedik etkileşimlerin ve durum değişikliklerinin meydana geldiği durumu adlandırabiliriz. Bu, yalnızca global durum aracılığıyla meydana gelebilir. +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. -Geniş, olgun bir kod tabanına sahip bir projenin geliştirici ekibine katıldığınızı hayal edin. Yeni yöneticiniz sizden yeni bir özellik uygulamanızı ister ve siz de doğru bir geliştirici olarak test yazarak başlarsınız. Ama projede yeni olduğunuz için, "bu metodu çağırırsam ne olur" türünde bir sürü keşif testi yaparsınız. Ve aşağıdaki testi yazmayı denersiniz: +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); // kart numaranız + $cc = new CreditCard('1234567890123456', 5, 2028); // sizin kart numaranız $cc->charge(100); } ``` -Kodu çalıştırırsınız, belki birkaç kez, ve bir süre sonra cep telefonunuzda bankadan bildirimler fark edersiniz, her çalıştırmada ödeme kartınızdan 100 dolar çekildiğini 🤦‍♂️ +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ş! 🤦‍♂️ -Tanrı aşkına nasıl test gerçek para çekme işlemine neden olabilir? Ödeme kartıyla işlem yapmak kolay değildir. Üçüncü taraf bir web servisiyle iletişim kurmanız, bu web servisinin URL'sini bilmeniz, giriş yapmanız vb. gerekir. Bu bilgilerin hiçbiri testte yer almaz. Daha da kötüsü, bu bilgilerin nerede bulunduğunu bile bilmiyorsunuz ve dolayısıyla her çalıştırmanın tekrar 100 dolar çekilmesine yol açmaması için dış bağımlılıkları nasıl mocklayacağınızı da bilmiyorsunuz. Ve yeni bir geliştirici olarak, yapmaya hazırlandığınız şeyin sizi 100 dolar daha fakir yapacağını nasıl bilmeliydiniz? +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? -Bu uzaktan ürkütücü etki! +İşte bu, uzaktan ürkütücü etkidir! -Yapmaktan başka çareniz yok, uzun süre bir sürü kaynak kodunu eşelemek, daha yaşlı ve deneyimli meslektaşlara sormak, projedeki bağlantıların nasıl çalıştığını anlayana kadar. Bu, `CreditCard` sınıfının arayüzüne bakıldığında, başlatılması gereken global durumu tespit etmenin mümkün olmamasından kaynaklanmaktadır. Hatta sınıfın kaynak koduna bakmak bile hangi başlatma metodunu çağırmanız gerektiğini açığa çıkarmaz. En iyi durumda, erişilen bir global değişken bulabilir ve ondan nasıl başlatılacağını tahmin etmeye çalışabilirsiniz. +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. Ödeme kartı, sadece örneklenip `charge()` metodunun çağrılmasının yeterliymiş gibi davranır. Ancak gizlice, ödeme ağ geçidini temsil eden başka bir `PaymentGateway` sınıfıyla işbirliği yapar. Onun arayüzü de bağımsız olarak başlatılabileceğini söyler, ancak gerçekte kimlik bilgilerini bir yapılandırma dosyasından çeker vb. Bu kodu yazan geliştiricilere, `CreditCard`'ın `PaymentGateway`'e ihtiyaç duyduğu açıktır. Kodu bu şekilde yazdılar. Ama projede yeni olan herkes için bu tam bir gizemdir ve öğrenmeyi engeller. +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? Kolayca. **API'nin bağımlılıkları bildirmesine izin verin.** +Durum nasıl düzeltilir? Kolay. **Bırakın API bağımlılıkları bildirsin.** ```php function testCreditCardCharge() @@ -68,35 +68,35 @@ function testCreditCardCharge() } ``` -Kod içindeki bağlantıların nasıl birdenbire açık hale geldiğine dikkat edin. `charge()` metodunun `PaymentGateway`'e ihtiyaç duyduğunu bildirmesiyle, kodun nasıl bağlantılı olduğunu kimseye sormanıza gerek kalmaz. Bir örnek oluşturmanız gerektiğini bilirsiniz ve bunu denediğinizde, erişim parametrelerini sağlamanız gerektiğiyle karşılaşırsınız. Onlar olmadan kod çalıştırılamazdı bile. +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, şimdi ödeme ağ geçidini mocklayabilirsiniz, böylece testin her çalıştırılmasında size 100 dolar fatura edilmeyecek. +Ve en önemlisi, artık ödeme geçidini taklit edebilirsiniz; böylece her test çalıştırmasında 100 dolar ödemezsiniz. -Global durum, nesnelerinizin API'lerinde bildirilmemiş şeylere gizlice erişebilmesine neden olur ve sonuç olarak API'lerinizi patolojik yalancılara dönüştürür. +Genel durum, nesnelerin API'lerinde bildirilmeyen bağımlılıklara gizlice erişmesine izin verir ve API'lerinizi fiilen patolojik yalancılara çevirir. -Belki de daha önce bu şekilde düşünmediniz, ancak ne zaman global durumu kullanırsanız, gizli kablosuz iletişim kanalları oluşturursunuz. Uzaktan ürkütücü eylem, geliştiricileri potansiyel etkileşimleri anlamak için kodun her satırını okumaya zorlar, geliştirici üretkenliğini düşürür ve yeni takım üyelerini şaşırtır. Eğer kodu oluşturan sizseniz, gerçek bağımlılıkları bilirsiniz, ama sizden sonra gelen herkes çaresizdir. +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. -Global durumu kullanan kod yazmayın, bağımlılıkların aktarılmasına öncelik verin. Yani bağımlılık enjeksiyonu. +Genel duruma dayanan kod yazmaktan kaçının; bağımlılıkları açıkça aktarmayı yeğleyin. Bağımlılık enjeksiyonunu benimseyin. -Global Durumun Kırılganlığı ---------------------------- +Genel Durumun Kırılganlığı +-------------------------- -Global durumu ve singleton'ları kullanan kodda, bu durumun ne zaman ve kim tarafından değiştirildiği asla emin değildir. Bu risk zaten başlatma sırasında ortaya çıkar. Aşağıdaki kodun veritabanı bağlantısı oluşturması ve ödeme ağ geçidini başlatması gerekiyor, ancak sürekli istisna fırlatıyor ve nedenini aramak son derece uzun sürüyor: +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 kablosuz olarak diğer nesnelere eriştiğini ve bunlardan bazılarının veritabanı bağlantısı gerektirdiğini öğrenmek için kodu ayrıntılı olarak incelemeniz gerekir. Yani veritabanını `PaymentGateway`'den önce başlatmak gereklidir. Ancak global durumun sis perdesi bunu sizden gizler. Eğer bireysel sınıfların API'leri aldatmasaydı ve bağımlılıklarını bildirseydi ne kadar zaman kazanırdınız? +`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, ...); +$gateway = new PaymentGateway($db, /* ... */); ``` -Benzer bir sorun, veritabanı bağlantısına global erişim kullanıldığında da ortaya çıkar: +Bir veritabanı bağlantısına genel erişim kullanıldığında da benzer bir sorun doğar: ```php use Illuminate\Support\Facades\DB; @@ -110,9 +110,9 @@ class Article } ``` -`save()` metodu çağrıldığında, veritabanı bağlantısının zaten oluşturulup oluşturulmadığı ve onun oluşturulmasından kimin sorumlu olduğu emin değildir. Eğer örneğin testler için çalışma zamanında veritabanı bağlantısını değiştirmek istersek, muhtemelen `DB::reconnect(...)` veya `DB::reconnectForTest()` gibi başka metotlar oluşturmamız gerekirdi. +`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üşünelim: +Bir örnek düşünün: ```php $article = new Article; @@ -122,9 +122,9 @@ Foo::doSomething(); $article->save(); ``` -`$article->save()` çağrıldığında test veritabanının gerçekten kullanıldığına nerede emin olabiliriz? Ya `Foo::doSomething()` metodu global veritabanı bağlantısını değiştirdiyse? Öğrenmek için `Foo` sınıfının kaynak kodunu ve muhtemelen birçok başka sınıfın da kodunu incelememiz gerekirdi. Ancak bu yaklaşım yalnızca kısa vadeli bir cevap getirirdi, çünkü durum gelecekte değişebilir. +`$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 bir statik değişkene taşırsak? +Ya veritabanı bağlantısını `Article` sınıfının içindeki statik bir değişkene taşırsak? ```php class Article @@ -143,11 +143,11 @@ class Article } ``` -Bu hiçbir şeyi değiştirmedi. Sorun global durumdur ve hangi sınıfta saklandığı hiç fark etmez. Bu durumda, önceki gibi, `$article->save()` metodunu çağırdığımızda hangi veritabanına yazılacağına dair hiçbir ipucumuz yok. Uygulamanın diğer ucundaki herhangi biri, `Article::setDb()` kullanarak veritabanını herhangi bir zamanda değiştirebilirdi. Elimizin altında. +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. -Global durum uygulamamızı **son derece kırılgan** yapar. +Genel durum, uygulamamızı **son derece kırılgan** kılar. -Ancak bu sorunla başa çıkmanın basit bir yolu var. Sadece API'nin bağımlılıkları bildirmesine izin vermek yeterlidir, bu da doğru işlevselliği sağ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 @@ -169,15 +169,15 @@ Foo::doSomething(); $article->save(); ``` -Bu yaklaşım sayesinde, veritabanı bağlantısındaki gizli ve beklenmedik değişiklikler hakkında endişe ortadan kalkar. Şimdi makalenin nereye kaydedildiğinden eminiz ve başka ilişkisiz bir sınıfın içindeki kod düzenlemeleri artık durumu değiştiremez. Kod artık kırılgan değil, ama kararlı. +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. -Global durumu kullanan kod yazmayın, bağımlılıkların aktarılmasına öncelik verin. Yani bağımlılık enjeksiyonu. +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, bilinen Gang of Four yayınından "tanıma göre":https://en.wikipedia.org/wiki/Singleton_pattern, sınıfı tek bir örneğe sınırlayan ve ona global erişim sunan bir tasarım desenidir. Bu desenin uygulanması genellikle aşağıdaki koda benzer: +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 @@ -190,35 +190,35 @@ class Singleton return self::$instance; } - // ve sınıfın verilen işlevlerini yerine getiren diğer metotlar + // ve sınıfın işlevlerini yerine getiren diğer metotlar } ``` -Maalesef, singleton uygulamaya global durum getirir. Ve yukarıda gösterdiğimiz gibi, global durum istenmez. Bu yüzden singleton bir anti-desen olarak kabul edilir. +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'ları kullanmayın ve onları başka mekanizmalarla değiştirin. Singleton'lara gerçekten ihtiyacınız yok. Ancak tüm uygulama için sınıfın tek bir örneğinin varlığını garanti etmeniz gerekiyorsa, bunu [DI konteynerine |container] bırakın. Böylece bir uygulama singleton'u, yani bir servis oluşturun. Bu sayede sınıf kendi benzersizliğini sağlamaya (yani `getInstance()` metodu ve statik değişkene sahip olmayacak) odaklanmayı bırakır ve yalnızca kendi işlevlerini yerine getirir. Böylece Tek Sorumluluk İlkesi'ni ihlal etmeyi bırakı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. -Global Durum ve Testler ------------------------ +Genel Durum ve Testler +---------------------- -Test yazarken, her testin izole bir birim olduğunu ve ona hiçbir dış durumun girmediğini varsayarız. Ve hiçbir durum testlerden çıkmaz. Test tamamlandıktan sonra, testle ilgili tüm durumun çöp toplayıcı (garbage collector) tarafından otomatik olarak kaldırılması gerekir. Bu sayede testler izole edilmiştir. Bu yüzden testleri istenilen sırada çalıştırabiliriz. +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 global durumlar/singleton'lar mevcutsa, tüm bu hoş varsayımlar parçalanır. Durum teste girebilir ve ondan çıkabilir. Birdenbire testlerin sırası önemli olabilir. +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'ları test edebilmek için bile, geliştiriciler genellikle özelliklerini gevşetmek zorunda kalır, belki de örneği başkasıyla değiştirmeye izin vererek. Böyle çözümler en iyi durumda bir hack'tir ve bakımı zor, anlaşılır olmayan kod oluşturur. Herhangi bir global durumu etkileyen her test veya `tearDown()` metodu, bu değişiklikleri geri almalıdır. +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. -Global durum, birim testi sırasında en büyük baş ağrısıdır! +Genel durum, birim testlerdeki en büyük baş ağrısıdır! -Durum nasıl düzeltilir? Kolayca. Singleton'ları kullanan kod yazmayın, bağımlılıkların aktarılmasına öncelik verin. Yani bağımlılık enjeksiyonu. +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. -Global Sabitler ---------------- +Genel Sabitler +-------------- -Global durum yalnızca singleton'ların ve statik değişkenlerin kullanımıyla sınırlı değildir, aynı zamanda global sabitlerle de ilgili olabilir. +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ğeri bize yeni (`M_PI`) veya faydalı (`PREG_BACKTRACK_LIMIT_ERROR`) bir bilgi getirmeyen sabitler kesinlikle sorunsuzdur. Aksine, bilgiyi kodun içine *kablosuz olarak* aktarmanın bir yolu olarak hizmet eden sabitler, gizli bir bağımlılıktan başka bir şey değildir. Aşağıdaki örnekteki `LOG_FILE` gibi. `FILE_APPEND` sabitinin kullanımı tamamen doğrudur. +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 = '...'; @@ -234,7 +234,7 @@ class Foo } ``` -Bu durumda, API'nin bir parçası olması için `Foo` sınıfının kurucusunda bir parametre bildirmeliyiz: +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 @@ -253,42 +253,42 @@ class Foo } ``` -Şimdi kayıt tutma için dosya yolu bilgisini aktarabilir ve ihtiyaca göre kolayca değiştirebiliriz, bu da kodun test edilmesini ve bakımını kolaylaştırır. +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. -Global Fonksiyonlar ve Statik Metotlar --------------------------------------- +Genel Fonksiyonlar ve Statik Metotlar +------------------------------------- -Statik metotların ve global fonksiyonların kullanımının kendisinin sorunlu olmadığını vurgulamak istiyoruz. `DB::insert()` ve benzeri metotların kullanımının uygunsuzluğunun ne içerdiğini açıkladık, ancak her zaman sadece bir statik değişkende saklanan global durum meselesiydi. `DB::insert()` metodu, içinde veritabanı bağlantısı saklandığı için statik değişkenin varlığını gerektirir. Bu değişken olmadan metodu uygulamak imkansız olurdu. +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. -`DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` ve birçok diğer gibi deterministik statik metotların ve fonksiyonların kullanımı, bağımlılık enjeksiyonu ile tamamen uyumludur. Bu fonksiyonlar her zaman aynı giriş parametrelerinden aynı sonuçları döndürürler ve bu yüzden öngörülebilirler. Hiçbir global durum kullanmazlar. +`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 deterministik olmayan fonksiyonlar da vardır. Bunlara örneğin `htmlspecialchars()` fonksiyonu dahildir. Üçüncü parametresi `$encoding`, eğer belirtilmemişse, varsayılan değer olarak `ini_get('default_charset')` yapılandırma seçeneğinin değerine sahiptir. Bu yüzden bu parametreyi her zaman belirtmek ve böylece fonksiyonun olası öngörülemez davranışını önlemek tavsiye edilir. Nette bunu tutarlı bir şekilde yapar. +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()`, `strtoupper()` ve benzeri bazı fonksiyonlar, yakın geçmişte deterministik olmayan şekilde davrandılar ve `setlocale()` ayarına bağımlıydılar. Bu, en sık Türkçe dili ile çalışırken birçok komplikasyona neden oldu. Çünkü o, noktalı ve noktasız küçük ve büyük `I` harfini ayırt eder. Yani `strtolower('I')` `ı` karakterini ve `strtoupper('i')` `İ` karakterini döndürüyordu, bu da uygulamaların bir dizi gizemli hataya neden olmaya başlamasına yol açtı. Ancak bu sorun PHP sürüm 8.2'de kaldırıldı ve fonksiyonlar artık yerel ayara bağımlı değil. +`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, global durumun tüm dünyada binlerce geliştiriciyi nasıl rahatsız ettiğinin güzel bir örneğidir. Çözüm, onu bağımlılık enjeksiyonu ile değiştirmekti. +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. -Global Durum Ne Zaman Kullanılabilir? -------------------------------------- +Genel Durum Ne Zaman Kullanılabilir? +------------------------------------ -Global durumu kullanmanın mümkün olduğu belirli özel durumlar vardır. Örneğin kod hata ayıklarken, bir değişkenin değerini yazdırmanız veya programın belirli bir kısmının süresini ölçmeniz gerektiğinde. Bu gibi durumlarda, daha sonra koddan kaldırılacak geçici eylemlerle ilgili olanlarda, global olarak erişilebilir bir dumper veya kronometre kullanmak meşru olarak mümkündür. Bu araçlar çünkü kod tasarımının bir parçası değildir. +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. -Başka bir örnek, dahili olarak derlenmiş düzenli ifadeleri bellekteki statik önbelleğe saklayan `preg_*` düzenli ifadelerle çalışmak için fonksiyonlardır. Yani aynı düzenli ifadeyi kodun farklı yerlerinde birden çok kez çağırdığınızda, yalnızca bir kez derlenir. Önbellek performanstan tasarruf sağlar ve aynı zamanda kullanıcı için tamamen görünmezdir, bu yüzden böyle bir kullanım meşru kabul edilebilir. +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 ---- -Neden mantıklı olduğunu ele aldık: +Şunları yapmanın neden anlamlı olduğunu ele aldık: -1) Koddan tüm statik değişkenleri kaldırmak -2) Bağımlılıkları bildirmek -3) Ve bağımlılık enjeksiyonu kullanmak +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 -Kod tasarımını düşündüğünüzde, her `static $foo`'nun bir sorun teşkil ettiğini düşünün. Kodunuzun DI'ye saygı duyan bir ortam olması için, global durumu tamamen ortadan kaldırmak ve onu bağımlılık enjeksiyonu kullanarak değiştirmek gereklidir. +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ç sırasında, birden fazla sorumluluğu olduğu için sınıfı bölmek gerektiğini keşfedebilirsiniz. Bundan korkmayın; Tek Sorumluluk İlkesi'ni hedefleyin. +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. -*Miško Hevery'ye teşekkür etmek isterim, [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/] gibi makaleleri bu bölümün temelini oluşturur.* +*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 index c9db239b87..244ab19e79 100644 --- a/dependency-injection/tr/introduction.texy +++ b/dependency-injection/tr/introduction.texy @@ -1,58 +1,58 @@ -Dependency Injection Nedir? -*************************** +Bağımlılık Enjeksiyonu Nedir? +***************************** .[perex] -Bu bölüm, tüm uygulamaları yazarken uymanız gereken temel programlama uygulamalarını size tanıtacaktır. Bunlar temiz, anlaşılır ve sürdürülebilir kod yazmak için gerekli temellerdir. +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ı benimser ve uygularsanız, Nette her adımda size yardımcı olacaktır. Rutin görevleri sizin için halledecek ve mantığın kendisine odaklanabilmeniz için size maksimum rahatlık sağlayacaktır. +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 prensipler oldukça basittir. Korkacak bir şey yok. +Burada göstereceğimiz ilkeler oldukça basit. Korkulacak bir şey yok. İlk Programınızı Hatırlıyor musunuz? ------------------------------------ -Hangi dilde yazdığınızı bilmiyoruz, ancak PHP olsaydı, muhtemelen şöyle görünürdü: +Onu hangi dilde yazdığınızı bilmiyoruz, ama PHP olsaydı muhtemelen şöyle görünürdü: ```php -function soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } -echo soucet(23, 1); // 24 yazdırır +echo addition(23, 1); // 24 yazdırır ``` -Birkaç önemsiz kod satırı, ancak içlerinde çok sayıda anahtar kavram gizli. Değişkenlerin var olduğu. Kodun, örneğin fonksiyonlar gibi daha küçük birimlere ayrıldığı. Onlara girdi argümanları ilettiğimiz ve sonuçları döndürdükleri. Sadece koşullar ve döngüler eksik. +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. -Fonksiyona girdi verilerini iletmemiz ve bir sonuç döndürmesi, matematikte olduğu gibi diğer alanlarda da kullanılan mükemmel anlaşılır bir kavramdır. +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, adını, parametrelerinin ve türlerinin bir özetini ve son olarak dönüş değerinin türünü içeren bir imzası vardır. Kullanıcılar olarak bizi ilgilendiren imzadır; genellikle iç uygulama hakkında hiçbir şey bilmemize gerek yoktur. +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 göründüğünü hayal edin: +Şimdi fonksiyon imzasının şöyle olduğunu düşünün: ```php -function soucet(float $x): float +function addition(float $x): float ``` -Tek parametreli bir toplam mı? Bu garip… Peki ya şöyle? +Tek parametreli toplama? Bu tuhaf… Peki ya bu? ```php -function soucet(): float +function addition(): float ``` -Bu gerçekten çok garip, değil mi? Fonksiyon nasıl kullanılır? +Bu artık gerçekten tuhaf, değil mi? Fonksiyon nasıl kullanılıyor? ```php -echo soucet(); // ne yazdırır acaba? +echo addition(); // ne yazdıracak? ``` -Böyle bir koda baktığımızda kafamız karışırdı. Sadece bir başlangıç seviyesindeki kişi anlamazdı, yetenekli bir programcı bile böyle bir kodu anlamazdı. +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çinde nasıl görüneceğini merak ediyor musunuz? Toplanacak sayıları nereden alır? Muhtemelen onları *bir şekilde* kendi başına elde ederdi, belki şöyle: +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 soucet(): float +function addition(): float { $a = Input::get('a'); $b = Input::get('b'); @@ -60,71 +60,71 @@ function soucet(): float } ``` -Fonksiyon gövdesinde, diğer global fonksiyonlara veya statik metotlara gizli bağlantılar keşfettik. Toplanacak sayıların gerçekten nereden geldiğini bulmak için daha fazla araştırmamız gerekiyor. +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. -Bu Yol Yanlış! --------------- +Böyle Olmaz! +------------ -Az önce gösterdiğimiz tasarım, birçok olumsuz özelliğin özüdür: +Az önce gösterdiğimiz tasarım, pek çok olumsuz özelliğin özüdür: -- fonksiyon imzası, toplanacak sayılara ihtiyaç duymuyormuş gibi davrandı, bu da kafamızı karıştırdı -- fonksiyonun başka iki sayıyı nasıl toplayacağını hiç bilmiyoruz -- toplanacak sayıları nereden aldığını bulmak için koda bakmak zorunda kaldık -- gizli bağlantılar keşfettik -- tam olarak anlamak için bu bağlantıları da incelemek gerekiyor +- 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. -Ve girdileri elde etmek gerçekten toplama fonksiyonunun görevi mi? Tabii ki değil. Sorumluluğu sadece toplama işleminin kendisidir. +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 istemiyoruz ve kesinlikle yazmak istemiyoruz. Çözüm basit: temellere geri dönün ve sadece parametreleri kullanın: +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 soucet(float $a, float $b): float +function addition(float $a, float $b): float { return $a + $b; } ``` -Kural 1: Size İletilmesini Sağlayın ------------------------------------ +Kural #1: Bırakın Size Aktarılsın +--------------------------------- -En önemli kural şudur: **fonksiyonların veya sınıfların ihtiyaç duyduğu tüm veriler onlara iletilmelidir**. +En önemli kural şudur: **fonksiyonların ya da sınıfların ihtiyaç duyduğu tüm veriler onlara verilmelidir**. -Onlara bir şekilde kendi başlarına ulaşabilecekleri gizli yollar icat etmek yerine, parametreleri basitçe iletin. Kodunuzu kesinlikle iyileştirmeyecek gizli yollar icat etmek için gereken zamandan tasarruf edeceksiniz. +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 kuralı her zaman ve her yerde uygularsanız, gizli bağlantıları olmayan bir koda giden yoldasınız demektir. Sadece yazar tarafından değil, ondan sonra okuyacak herkes tarafından anlaşılır olan bir koda. Fonksiyonların ve sınıfların imzalarından her şeyin anlaşılabildiği ve uygulamada gizli sırları aramaya gerek olmayan bir koda. +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ğe profesyonel olarak **dependency injection** denir. Ve bu verilere **bağımlılıklar** denir. Aslında, bu sadece parametre iletmedir, başka bir şey değil. +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 ile bir araç olan, yani tamamen farklı bir şey olan "dependency injection container"ı karıştırmayın. Konteynerleri daha sonra ele alacağız. +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 bunun sınıflarla ne ilgisi var? Bir sınıf, basit bir fonksiyondan daha karmaşık bir bütündür, ancak Kural 1 burada da tamamen geçerlidir. Sadece [argümanları iletmenin daha fazla yolu|passing-dependencies] vardır. Örneğin, bir fonksiyona oldukça benzer şekilde: +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 Matematika +class Math { - public function soucet(float $a, float $b): float + public function sum(float $a, float $b): float { return $a + $b; } } -$math = new Matematika; -echo $math->soucet(23, 1); // 24 +$math = new Math; +echo $math->sum(23, 1); // 24 ``` -Veya diğer metotlarla ya da doğrudan yapıcı ile: +Ya da başka metotlarla veya doğrudan yapıcıyla: ```php -class Soucet +class Sum { public function __construct( private float $a, @@ -132,24 +132,23 @@ class Soucet ) { } - public function spocti(): float + public function calculate(): float { return $this->a + $this->b; } - } -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 +$sum = new Sum(23, 1); +echo $sum->calculate(); // 24 ``` -Her iki örnek de dependency injection ile tamamen uyumludur. +Her iki örnek de Dependency Injection ile tümüyle uyumludur. -Gerçek Hayat Örnekleri ----------------------- +Gerçek Hayattan Örnekler +------------------------ -Gerçek dünyada, sayıları toplamak için sınıflar yazmayacaksınız. Pratik örneklere geçelim. +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: @@ -162,23 +161,23 @@ class Article public function save(): void { - // makaleyi veritabanına kaydedeceğiz + // makaleyi veritabanına kaydet } } ``` -ve kullanımı şöyle olacaktır: +kullanımı da şöyle olsun: ```php $article = new Article; -$article->title = 'Kilo Verme Hakkında Bilmeniz Gereken 10 Şey'; +$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. [Nette Database |database:] kullanarak uygulamak çocuk oyuncağı olurdu, ancak bir engel var: `Article` veritabanı bağlantısını, yani `Nette\Database\Connection` sınıfının nesnesini nereden alacak? +`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 birçok seçeneğimiz var. Statik bir değişkenden alabilir. Veya veritabanı bağlantısını sağlayan bir sınıftan miras alabilir. Veya [singleton |global-state#Singleton] olarak adlandırılanı kullanabilir. Veya Laravel'de kullanılan facades olarak adlandırılanları kullanabilir: +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; @@ -201,15 +200,15 @@ class Article Harika, sorunu çözdük. -Ya da çözmedik mi? +Öyle mi? -[##Kural 1: Size İletilmesini Sağlayın] hatırlayalım: sınıfın ihtiyaç duyduğu tüm bağımlılıklar ona iletilmelidir. Çünkü kuralı ihlal edersek, gizli bağlantılarla dolu, anlaşılmaz, kirli bir koda giden yola girmiş oluruz ve sonuç, bakımı ve geliştirilmesi acı verici olacak bir uygulama olur. +[#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ı, `save()` metodunun makaleyi nereye kaydettiğini bilmiyor. Bir veritabanı tablosuna mı? Hangisine, canlıya mı yoksa test olanına mı? Ve bu nasıl değiştirilebilir? +`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ı, `save()` metodunun nasıl uygulandığına bakmalı ve `DB::insert()` metodunun kullanımını bulmalıdır. Bu yüzden, bu metodun veritabanı bağlantısını nasıl elde ettiğini daha fazla araştırmalıdır. Ve gizli bağlantılar oldukça uzun bir zincir oluşturabilir. +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 asla gizli bağlantılar, Laravel facades veya statik değişkenler bulunmaz. Temiz ve iyi tasarlanmış kodda argümanlar iletilir: +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 @@ -224,7 +223,7 @@ class Article } ``` -Daha da pratik olanı, ileride göreceğimiz gibi, yapıcı ile olacaktır: +Sonra göreceğimiz gibi, yapıcıyı kullanmak daha da pratiktir: ```php class Article @@ -245,16 +244,16 @@ class Article ``` .[note] -Eğer deneyimli bir programcıysanız, muhtemelen `Article` sınıfının hiç `save()` metoduna sahip olmaması gerektiğini, tamamen bir veri bileşeni olması gerektiğini ve kaydetme işleminin ayrı bir depo tarafından yapılması gerektiğini düşünüyorsunuzdur. Bu mantıklı. Ancak bu bizi dependency injection konusunun çok ötesine ve basit örnekler verme çabasının dışına çıkarırdı. +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ı. -Faaliyeti için örneğin bir veritabanı gerektiren bir sınıf yazıyorsanız, onu nereden alacağınızı düşünmeyin, size iletilmesini sağlayın. Belki yapıcı veya 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 edeceksiniz. +Ç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 ya hata mesajlarını günlüğe kaydeden bu sınıfa ne dersiniz: +Peki hata mesajlarını günlükleyen şu sınıf: ```php class Logger { - public function log(string $message) + public function log(string $message): void { $file = LOG_DIR . '/log.txt'; file_put_contents($file, $message . "\n", FILE_APPEND); @@ -262,11 +261,11 @@ class Logger } ``` -Sizce [##Kural 1: Size İletilmesini Sağlayın] uyduk mu? +Sizce [#Kural #1: Bırakın Size Aktarılsın] kuralına uyduk mu? Uymadık. -Anahtar bilgi, yani günlük dosyasının bulunduğu dizin, sınıf tarafından *kendi başına* bir sabitten elde ediliyor. +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: @@ -276,7 +275,7 @@ $logger->log('Sıcaklık 23 °C'); $logger->log('Sıcaklık 10 °C'); ``` -Uygulamayı bilmeden, mesajların nereye yazıldığı sorusunu cevaplayabilir miydiniz? Çalışması için `LOG_DIR` sabitinin varlığının gerekli olduğunu düşünür müydünüz? Ve başka bir yere yazacak ikinci bir örnek oluşturabilir miydiniz? Kesinlikle hayır. +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: @@ -295,7 +294,7 @@ class Logger } ``` -Sınıf şimdi çok daha anlaşılır, yapılandırılabilir ve dolayısıyla daha kullanışlı. +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'); @@ -303,16 +302,16 @@ $logger->log('Sıcaklık 15 °C'); ``` -Ama Bu Beni İlgilendirmiyor! ----------------------------- +Ama Beni İlgilendirmiyor! +------------------------- -*„Bir Article nesnesi oluşturup save() çağırdığımda, veritabanıyla uğraşmak istemiyorum, sadece yapılandırmada ayarladığım veritabanına kaydedilmesini istiyorum.“* +*"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, sadece mesajın yazılmasını istiyorum ve nereye yazılacağıyla ilgilenmek istemiyorum. Global ayar kullanılsın.“* +*"Logger kullandığımda mesajın yazılmasını istiyorum, nereye yazıldığıyla uğraşmak istemiyorum. Genel ayarlar kullanılsın."* -Bunlar doğru yorumlar. +Bunlar geçerli noktalar. -Örnek olarak, bültenleri dağıtan ve sonucunu günlüğe kaydeden bir sınıf göstereceğiz: +Örnek olarak, bülten dağıtan ve sonucu günlükleyen bir sınıf gösterelim: ```php class NewsletterDistributor @@ -325,24 +324,24 @@ class NewsletterDistributor $logger->log('E-postalar gönderildi'); } catch (Exception $e) { - $logger->log('Gönderim sırasında bir hata oluştu'); + $logger->log('Gönderim sırasında hata oluştu'); throw $e; } } } ``` -Artık `LOG_DIR` sabitini kullanmayan geliştirilmiş `Logger`, yapıcısında dosya yolunun belirtilmesini gerektiriyor. Bunu nasıl çözeceğiz? `NewsletterDistributor` sınıfı mesajların nereye yazıldığıyla hiç ilgilenmiyor, sadece onları yazmak istiyor. +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: Size İletilmesini Sağlayın]: sınıfın ihtiyaç duyduğu tüm verileri ona iletiyoruz. +Çö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. -Yani bu, `Logger` nesnesini oluştururken kullanacağımız günlük yolunu yapıcı aracılığıyla ileteceğimiz anlamına mı geliyor? +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, // ⛔ BU ŞEKİLDE DEĞİL! + private string $file, // ⛔ BÖYLE DEĞİL! ) { } @@ -351,7 +350,7 @@ class NewsletterDistributor $logger = new Logger($this->file); ``` -Bu şekilde değil! Çünkü yol, `NewsletterDistributor` sınıfının ihtiyaç duyduğu veriler arasında **değildir**; bunlara `Logger` ihtiyaç duyar. Farkı anlıyor musunuz? `NewsletterDistributor` sınıfı, logger'ın kendisine ihtiyaç duyar. Bu yüzden onu ileteceğiz: +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 @@ -368,30 +367,30 @@ class NewsletterDistributor $this->logger->log('E-postalar gönderildi'); } catch (Exception $e) { - $this->logger->log('Gönderim sırasında bir hata oluştu'); + $this->logger->log('Gönderim sırasında hata oluştu'); throw $e; } } } ``` -Şimdi `NewsletterDistributor` sınıfının imzalarından, işlevselliğinin bir parçası olarak günlüklemenin de olduğu açıktır. Ve logger'ı başka biriyle değiştirmek, örneğin test için, tamamen önemsizdir. Ayrıca, `Logger` sınıfının yapıcısı değişirse, bunun sınıfımız üzerinde hiçbir etkisi olmayacaktır. +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: Sadece Size Ait Olanı Alın ------------------------------------ +Kural #2: Kendine Ait Olanı Al +------------------------------ -Kafanızın karışmasına izin vermeyin ve bağımlılıklarınızın bağımlılıklarını size iletmeyin. Sadece kendi bağımlılıklarınızı size iletin. +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, diğer nesneleri kullanan kod, yapıcılarındaki değişikliklerden tamamen bağımsız olacaktır. API'si daha doğru olacaktır. Ve en önemlisi, bu bağımlılıkları başkalarıyla değiştirmek önemsiz olacaktır. +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 Üye ---------------- +Aileye Yeni Katılan +------------------- -Geliştirme ekibinde, veritabanına yazan ikinci bir logger oluşturma kararı alındı. Bu yüzden `DatabaseLogger` sınıfını oluşturacağız. Yani iki sınıfımız var, `Logger` ve `DatabaseLogger`, biri dosyaya yazıyor, diğeri veritabanına… Bu isimlendirmede size garip gelen bir şey yok mu? `Logger`ı `FileLogger` olarak yeniden adlandırmak daha iyi olmaz mıydı? Kesinlikle evet. +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 yapacağız. Orijinal ad altında bir arayüz oluşturacağız: +Ama bunu akıllıca yapalım. Özgün adı kullanarak bir arayüz oluşturuyoruz: ```php interface Logger @@ -410,17 +409,17 @@ 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` gerektirmesinden hala memnun olacaktır. Ve hangi örneği ona ileteceğimiz bize kalmış olacak. +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 nedenle arayüz adlarına asla `Interface` sonekini veya `I` önekini vermeyiz.** Aksi takdirde, kodu bu kadar güzel bir şekilde geliştirmek mümkün olmazdı. +**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 Problemimiz Var ----------------------------- +Houston, Bir Sorunumuz Var +-------------------------- -Tüm uygulamada, ister dosya tabanlı ister veritabanı tabanlı olsun, tek bir logger örneğiyle idare edebilir ve onu bir şeylerin günlüğe kaydedildiği her yere basitçe iletebilirken, `Article` sınıfı durumunda durum oldukça farklıdır. Örneklerini ihtiyaca göre, hatta birden çok kez oluştururuz. Yapıcısındaki veritabanı bağımlılığıyla nasıl başa çıkılır? +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 denetleyici (controller) hizmet edebilir: +Örnek olarak, bir form gönderildikten sonra makaleyi veritabanına kaydetmesi gereken bir controller düşünelim: ```php class EditController extends Controller @@ -435,30 +434,30 @@ class EditController extends Controller } ``` -Olası bir çözüm kendini gösteriyor: veritabanı nesnesini yapıcı aracılığıyla `EditController`'a iletelim ve `$article = new Article($this->db)` kullanalım. +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. -Önceki `Logger` ve dosya yolu örneğinde olduğu gibi, bu doğru bir yaklaşım değildir. Veritabanı `EditController`'ın değil, `Article`'ın bir bağımlılığıdır. Bu nedenle veritabanını iletmek [#Kural 2: Sadece Size Ait Olanı Alın] aykırıdır. `Article` sınıfının yapıcısı değiştiğinde (yeni bir parametre eklendiğinde), örneklerin oluşturulduğu tüm yerlerdeki kodu da değiştirmek gerekecektir. Ufff. +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, ne önerirsin? +Houston, öneriniz nedir? -Kural 3: Fabrikaya Bırakın --------------------------- +Kural #3: Bırakın Factory Halletsin +----------------------------------- -Gizli bağlantıları kaldırarak ve tüm bağımlılıkları argüman olarak ileterek, daha yapılandırılabilir ve esnek sınıflar elde ettik. Ve dolayısıyla, bu daha esnek sınıfları bizim için oluşturacak ve yapılandıracak başka bir şeye ihtiyacımız var. Buna fabrikalar diyeceğiz. +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 fabrikaya bırakın. +Kural şudur: Bir sınıfın bağımlılıkları varsa, örneklerinin oluşturulmasını bir factory'ye devredin. -Fabrikalar, dependency injection dünyasında `new` operatörünün daha akıllı bir alternatifidir. +Factory'ler, bağımlılık enjeksiyonu dünyasında `new` operatörünün daha akıllı alternatifidir. .[note] -Lütfen fabrikaların belirli bir kullanım şeklini tanımlayan ve bu konuyla ilgisi olmayan *factory method* tasarım deseniyle karıştırmayın. +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. -Fabrika +Factory ------- -Fabrika, nesneleri üreten ve yapılandıran bir metot veya sınıftır. `Article` üreten sınıfı `ArticleFactory` olarak adlandıracağız ve örneğin şöyle görünebilir: +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 @@ -475,7 +474,7 @@ class ArticleFactory } ``` -Denetleyicideki kullanımı şöyle olacaktır: +Controller'daki kullanımı şöyle olacak: ```php class EditController extends Controller @@ -487,7 +486,7 @@ class EditController extends Controller public function formSubmitted($data) { - // fabrikanın nesneyi oluşturmasına izin veriyoruz + // bırakın nesneyi factory oluştursun $article = $this->articleFactory->create(); $article->title = $data->title; $article->content = $data->content; @@ -496,11 +495,11 @@ class EditController extends Controller } ``` -Bu noktada `Article` sınıfının yapıcısının imzası değişirse, buna tepki vermesi gereken tek kod parçası `ArticleFactory` fabrikasının kendisidir. `Article` nesneleriyle çalışan diğer tüm kodlar, örneğin `EditController`, bundan hiçbir şekilde etkilenmeyecektir. +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. -Belki şimdi kendinize hiç yardımcı olup olmadığımızı merak ederek alnınıza vuruyorsunuzdur. Kod miktarı arttı ve her şey şüpheli bir şekilde karmaşık görünmeye başladı. +Ş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ı. -Endişelenmeyin, birazdan Nette DI konteynerine geleceğiz. Ve dependency injection kullanan uygulamalar oluşturmayı son derece basitleştiren birçok numarası var. Örneğin, `ArticleFactory` sınıfı yerine [sadece bir arayüz yazın |factory] yeterli olacaktır: +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 @@ -509,18 +508,18 @@ interface ArticleFactory } ``` -Ama acele ediyoruz, biraz daha bekleyin :-) +Ama kendimizden ileri gidiyoruz, bizi izlemeye devam edin :-) Özet ---- -Bu bölümün başında, temiz kod tasarlamak için bir prosedür göstereceğimize söz vermiştik. Sınıflara sadece şunları yapmak yeterlidir: +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ı iletin |#Kural 1: Size İletilmesini Sağlayın] -2) [ve doğrudan ihtiyaç duymadıklarını iletmeyin |#Kural 2: Sadece Size Ait Olanı Alın] -3) [ve bağımlılıkları olan nesnelerin en iyi fabrikalarda üretildiği |#Kural 3: Fabrikaya Bırakın] +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 öyle görünmeyebilir, ancak bu üç kuralın geniş kapsamlı sonuçları vardır. Kod tasarımına kökten farklı bir bakış açısına yol açarlar. Buna değer mi? Eski alışkanlıklarını bırakan ve tutarlı bir şekilde dependency injection kullanmaya başlayan programcılar, bu adımı profesyonel yaşamlarında önemli bir an olarak görürler. Onlara açık ve sürdürülebilir uygulamaların dünyasını açtı. +İ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 ya kod tutarlı bir şekilde dependency injection kullanmıyorsa? Statik metotlara veya singleton'lara dayanıyorsa ne olur? Bu herhangi bir sorun yaratır mı? [Getirir ve çok temeldir |global-state]. +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 index 760f3addcc..681739f236 100644 --- a/dependency-injection/tr/nette-container.texy +++ b/dependency-injection/tr/nette-container.texy @@ -1,10 +1,10 @@ -Nette DI Konteyner +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ılabilen derlenmiş DI konteynerlerini üretebilir ve otomatik olarak güncelleyebilir. +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 konteynerinin oluşturması gereken servislerin şeklini genellikle [NEON formatı|neon:format] yapılandırma dosyaları kullanarak tanımlarız. [önceki bölümde|container] manuel olarak oluşturduğumuz konteyner şöyle yazılırdı: +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: @@ -16,18 +16,18 @@ parameters: services: - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - ArticleFactory - - UserController + - EditController ``` -Yazım gerçekten kısa ve özdür. +Söz dizimi çok derli topludur. -`ArticleFactory` ve `UserController` sınıflarının yapıcılarında bildirilen tüm bağımlılıklar, Nette DI tarafından sözde [autowiring|autowiring] sayesinde otomatik olarak bulunur ve iletilir, bu nedenle yapılandırma dosyasında hiçbir şey belirtmeye gerek yoktur. Bu nedenle, parametreler değişse bile yapılandırmada hiçbir şeyi değiştirmeniz gerekmez. Nette konteyneri otomatik olarak yeniden oluşturur. Orada tamamen uygulama geliştirmeye odaklanabilirsiniz. +`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'lar kullanarak iletmek istiyorsak, bunun için [setup |services#Setup] bölümünü kullanırız. +Bağımlılıkları setter'larla aktarmak istersek, bunun için [setup |services#Setup] bölümünü kullanırız. -Nette DI, konteynerin PHP kodunu doğrudan üretir. Sonuç, açıp inceleyebileceğiniz bir `.php` dosyasıdır. Bu sayede konteynerin tam olarak nasıl çalıştığını görebilirsiniz. Ayrıca IDE'de hata ayıklayabilir ve adım adım ilerleyebilirsiniz. Ve en önemlisi: üretilen PHP son derece hızlıdır. +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 ayrıca sağlanan arayüze dayalı olarak [fabrikalar|factory] için kod üretebilir. Bu nedenle, `ArticleFactory` sınıfı yerine uygulamada sadece bir arayüz oluşturmamız yeterli olacaktı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 @@ -36,19 +36,19 @@ interface ArticleFactory } ``` -Örneğin tamamını [GitHub'da|https://github.com/nette-examples/di-example-doc] bulabilirsiniz. +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 dağıtmak çok kolaydır. Önce Composer ile kurarız (çünkü zip indirmek çooook eski moda): +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 konteyneri örneği oluşturur: +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'); @@ -58,23 +58,60 @@ $class = $loader->load(function ($compiler) { $container = new $class; ``` -Konteyner yalnızca bir kez üretilir, kodu önbelleğe (`__DIR__ . '/temp'` dizini) yazılır ve sonraki isteklerde yalnızca buradan yüklenir. +Container yalnızca bir kez üretilir, kodu önbelleğe (`__DIR__ . '/temp'` dizinine) yazılır ve sonraki isteklerde yalnızca oradan yüklenir. -Servisleri oluşturmak ve almak için `getService()` veya `getByType()` metotları kullanılır. Bu şekilde `UserController` nesnesini oluştururuz: +`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 -$controller = $container->getByType(UserController::class); +$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 veya yapılandırma dosyası değiştiğinde konteynerin otomatik olarak yeniden oluşturulduğu otomatik yenileme modunu etkinleştirmek faydalıdır. `ContainerLoader` yapıcısında ikinci argüman olarak `true` belirtmek yeterlidir. +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 yazılmış uygulamalarla sınırlı değildir, sadece 3 satır kodla herhangi bir yere dağıtabilirsiniz. Ancak, Nette Framework'te uygulamalar geliştiriyorsanız, konteynerin yapılandırılması ve oluşturulmasından [Bootstrap |application:bootstrapping#DI Konteyner Yapılandırması] sorumludur. +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 index 515799eede..9d903fd4f4 100644 --- a/dependency-injection/tr/passing-dependencies.texy +++ b/dependency-injection/tr/passing-dependencies.texy @@ -1,24 +1,24 @@ -Bağımlılıkların İletilmesi -************************** +Bağımlılıkların Aktarılması +*************************** <div class=perex> -Argümanlar veya DI terminolojisinde "bağımlılıklar", sınıflara şu ana yollarla iletilebilir: +Argümanlar, ya da DI terminolojisiyle "bağımlılıklar", sınıflara başlıca şu yollarla aktarılabilir: -* yapıcı ile iletme -* metot ile iletme (sözde setter) -* değişken ayarlayarak -* *inject* metodu, anotasyonu veya niteliği ile +* Yapıcı enjeksiyonu (constructor injection) +* Metot enjeksiyonu (setter injection denir) +* Özellik enjeksiyonu (property injection) +* `inject*()` metodu ya da `#[Inject]` attribute'u ile </div> -Şimdi farklı varyantları belirli örneklerle göstereceğiz. +Her seçeneği somut örneklerle gösterelim. -Yapıcı ile İletme -================= +Yapıcı Enjeksiyonu +================== -Bağımlılıklar, nesne oluşturma anında yapıcı argümanları olarak iletilir: +Bağımlılıklar, nesne örneklenirken yapıcı argümanları olarak verilir: ```php class MyClass @@ -34,9 +34,9 @@ class MyClass $obj = new MyClass($cache); ``` -Bu form, sınıfın işlevi için mutlaka ihtiyaç duyduğu zorunlu bağımlılıklar için uygundur, çünkü onlarsız örnek oluşturulamaz. +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 itibaren, 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 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 @@ -49,7 +49,7 @@ class MyClass } ``` -PHP 8.1'den itibaren, değişkenin içeriğinin artık değişmeyeceğini bildiren `readonly` bayrağıyla işaretlenebilir: +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 @@ -62,13 +62,13 @@ class MyClass } ``` -DI konteyneri, yapıcıya bağımlılıkları [autowiring |autowiring] kullanarak otomatik olarak iletir. Bu şekilde iletilemeyen argümanlar (örneğin dizeler, sayılar, boolean'lar) [yapılandırmada belirtiriz |services#Argümanlar]. +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ıklar gerektiren bir üst sınıftan miras aldığı ve aynı zamanda alt sınıfın da bağımlılıklar gerektirdiği durumu ifade eder. Bu durumda, üst sınıfın bağımlılıklarını da alıp iletmesi gerekir: +*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 @@ -94,11 +94,11 @@ final class MyClass extends BaseClass } ``` -Sorun, `BaseClass` sınıfının yapıcısını değiştirmek istediğimizde ortaya çıkar, örneğin yeni bir bağımlılık eklendiğinde. O zaman tüm alt sınıfların yapıcılarını da değiştirmek gerekir. Bu da böyle bir değişikliği cehenneme çevirir. +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. -Bundan nasıl kaçınılır? Çözüm, **[kalıtım yerine kompozisyonu |faq#Neden Kalıtım Yerine Kompozisyon Tercih Edilir] tercih etmektir**. +Bu nasıl önlenir? Çözüm, **[kompozisyonu kalıtıma yeğlemektir |faq#Kompozisyon neden kalıtıma yeğlenir?]**. -Yani, kodu farklı tasarlayacağız. [Soyut |nette:introduction-to-object-oriented-programming#Soyut Sınıflar] `Base*` sınıflarından kaçınacağız. `MyClass`'ın belirli bir işlevselliği `BaseClass`'tan miras alarak elde etmesi yerine, bu işlevselliği bir bağımlılık olarak almasını sağlayacağız: +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 @@ -125,10 +125,10 @@ final class MyClass ``` -Setter ile İletme -================= +Setter Enjeksiyonu +================== -Bağımlılıklar, onları özel bir değişkende saklayan bir metot çağrılarak iletilir. Bu metotları adlandırmak için yaygın bir kural `set*()` şeklindedir, bu yüzden onlara setter denir, ancak elbette başka herhangi bir şekilde adlandırılabilirler. +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 @@ -145,9 +145,9 @@ $obj = new MyClass; $obj->setCache($cache); ``` -Bu yöntem, sınıfın işlevi için gerekli olmayan isteğe bağlı bağımlılıklar için uygundur, çünkü nesnenin bağımlılığı gerçekten alacağı garanti edilmez (yani kullanıcının metodu çağıracağı). +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, setter'ı tekrar tekrar çağırmaya ve böylece bağımlılığı değiştirmeye izin verir. Bu istenmiyorsa, metoda bir kontrol ekleriz veya PHP 8.1'den itibaren `$cache` özelliğini `readonly` bayrağıyla işaretleriz. +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 @@ -156,15 +156,15 @@ class MyClass public function setCache(Cache $cache): void { - if ($this->cache) { - throw new RuntimeException('Bağımlılık zaten ayarlandı'); + if (isset($this->cache)) { + throw new RuntimeException('The dependency has already been set'); } $this->cache = $cache; } } ``` -Setter çağrısını DI konteyneri yapılandırmasında [setup anahtarında |services#Setup] tanımlarız. Burada da autowiring kullanarak otomatik bağımlılık iletimi kullanılır: +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: @@ -174,10 +174,10 @@ services: ``` -Değişken Ayarlayarak -==================== +Özellik Enjeksiyonu +=================== -Bağımlılıklar, doğrudan üye değişkene yazılarak iletilir: +Bağımlılıklar, doğrudan bir üye özelliğe yazılarak verilir: ```php class MyClass @@ -189,9 +189,9 @@ $obj = new MyClass; $obj->cache = $cache; ``` -Bu yöntem uygunsuz kabul edilir, çünkü üye değişken `public` olarak bildirilmelidir. Ve dolayısıyla, iletilen bağımlılığın gerçekten verilen türde olacağını kontrol edemeyiz (PHP 7.4 öncesinde geçerliydi) ve yeni atanan bağımlılığa kendi kodumuzla tepki verme, örneğin sonraki değişikliği engelleme yeteneğini kaybederiz. Aynı zamanda değişken, sınıfın genel arayüzünün bir parçası haline gelir, bu da istenmeyebilir. +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. -Değişken ayarını DI konteyneri yapılandırmasında [setup bölümünde |services#Setup] tanımlarız: +Özellik ataması, DI container yapılandırmasında [setup bölümünde |services#Setup] tanımlanır: ```neon services: @@ -204,12 +204,12 @@ services: Inject ====== -Önceki üç yöntem tüm nesne yönelimli dillerde genel olarak geçerliyken, *inject* metodu, anotasyonu veya niteliği ile enjekte etme, Nette'deki presenter'lara özgüdür. Bunlar [ayrı bir bölüm |best-practices:inject-method-attribute] içinde ele alınmaktadır. +Ö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 işlevi için mutlaka ihtiyaç duyduğu zorunlu bağımlılıklar için uygundur -- setter ise isteğe bağlı bağımlılıklar veya daha sonra değiştirilebilme olasılığı olan bağımlılıklar için uygundur -- public değişkenler uygun değildir +- 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 index d80777fce8..fc97a464f7 100644 --- a/dependency-injection/tr/services.texy +++ b/dependency-injection/tr/services.texy @@ -1,17 +1,17 @@ -Servislerin Tanımlanması -************************ +Servis Tanımları +**************** .[perex] -Yapılandırma, DI konteynerine bireysel servisleri nasıl oluşturacağını ve diğer bağımlılıklarla nasıl bağlayacağını öğrettiğimiz yerdir. Nette, bunu başarmak için çok net ve zarif bir yol sunar. +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 formatındaki 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 servisin basit bir tanım örneğine bakalım: +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 konteynerinde|container] aşağıdaki fabrika metoduna yol açacaktır: +Yukarıdaki yapılandırma, [DI container|container] içinde şu factory metodunu doğurur: ```php public function createServiceDatabase(): PDO @@ -20,14 +20,14 @@ public function createServiceDatabase(): PDO } ``` -Servis adları, yapılandırma dosyasının diğer bölümlerinde `@servisAdi` formatında onlara başvurmamızı sağlar. Bir servisi adlandırmaya gerek yoksa, basitçe bir tire kullanabiliriz: +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 konteynerinden bir servis almak için, parametre olarak servis adıyla `getService()` metodunu veya servis türüyle `getByType()` metodunu kullanabiliriz: +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'); @@ -38,14 +38,14 @@ $database = $container->getByType(PDO::class); Servis Oluşturma ================ -Çoğunlukla, bir servisi basitçe belirli bir sınıfın bir örneğini oluşturarak oluştururuz. Örneğin: +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 ayırabiliriz: +Yapılandırmayı ek anahtarlarla genişletmemiz gerekirse, tanım birden çok satıra bölünebilir: ```neon services: @@ -54,9 +54,9 @@ services: setup: ... ``` -`create` anahtarının `factory` takma adı vardır, her iki varyant da pratikte yaygındır. Ancak, `create` kullanmanızı öneririz. +`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ı veya oluşturma metodunun argümanları alternatif olarak `arguments` anahtarında yazılabilir: +Yapıcının ya da factory metodunun argümanları, alternatif olarak `arguments` anahtarıyla da belirtilebilir: ```neon services: @@ -65,7 +65,7 @@ services: arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] ``` -Servisler sadece bir sınıfın örneğini basitçe oluşturarak oluşturulmak zorunda değildir, aynı zamanda statik metotların veya diğer servislerin metotlarının çağrılmasının sonucu da olabilirler: +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: @@ -73,7 +73,7 @@ services: router: @routerFactory::create() ``` -Basitlik için `->` yerine `::` kullanıldığına dikkat edin, bkz. [##İfade Araçları]. Bu fabrika metotları üretilecektir: +Basitlik için `->` yerine `::` kullanıldığına dikkat edin, bkz. [#İfade Dili]. Şu factory metotları üretilecek: ```php public function createServiceDatabase(): PDO @@ -87,7 +87,7 @@ public function createServiceRouter(): RouteList } ``` -DI konteynerinin oluşturulan servisin türünü bilmesi gerekir. Belirtilen bir dönüş türü olmayan bir metot kullanarak bir servis oluşturuyorsak, bu türü yapılandırmada açıkça belirtmemiz gerekir: +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: @@ -100,14 +100,14 @@ services: Argümanlar ========== -Yapıcıya ve metotlara argümanları, PHP'nin kendisinde olduğu gibi çok benzer bir şekilde iletiyoruz: +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 okunabilirlik için argümanları ayrı satırlara ayırabiliriz. Bu durumda virgül kullanımı isteğe bağlıdır: +Daha iyi okunurluk için argümanları ayrı satırlarda sıralayabiliriz. Bu durumda virgül kullanmak isteğe bağlı olur: ```neon services: @@ -118,7 +118,7 @@ services: ) ``` -Argümanları adlandırabilir ve sıralarıyla ilgilenmek zorunda kalmazsınız: +Argümanları adlandırabilir ve böylece sıralarıyla uğraşmaktan kurtulabilirsiniz: ```neon services: @@ -129,14 +129,14 @@ services: ) ``` -Bazı argümanları atlamak ve varsayılan değerlerini kullanmak veya [autowiring|autowiring] kullanarak bir servis eklemek isterseniz, alt çizgi kullanın: +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üman olarak servisleri iletebilir, parametreleri kullanabilir ve çok daha fazlasını yapabilirsiniz, bkz. [##İfade Araçları]. +Argümanlar servisleri, parametreleri ve daha fazlasını içerebilir, bkz. [#İfade Dili]. Setup @@ -152,7 +152,7 @@ services: - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) ``` -Bu, PHP'de şöyle görünürdü: +Bu PHP'de şöyle görünürdü: ```php public function createServiceDatabase(): PDO @@ -163,7 +163,7 @@ public function createServiceDatabase(): PDO } ``` -Metotları çağırmanın yanı sıra, özelliklere değerler de iletebilirsiniz. Bir diziye öğe eklemek de desteklenir, bu, NEON sözdizimiyle çakışmaması için tırnak içinde yazılmalıdır: +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: @@ -186,7 +186,7 @@ public function createServiceFoo(): Foo } ``` -Ancak setup'ta statik metotları veya diğer servislerin metotlarını da çağırabilirsiniz. Argüman olarak mevcut servisi iletmeniz gerekiyorsa, onu `@self` olarak belirtin: +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: @@ -197,7 +197,7 @@ services: - @anotherService::setFoo(@self) ``` -Basitlik için `->` yerine `::` kullanıldığına dikkat edin, bkz. [##İfade Araçları]. Böyle bir fabrika metodu üretilecektir: +Basitlik için `->` yerine `::` kullanıldığına dikkat edin, bkz. [#İfade Dili]. Şöyle bir factory metodu üretilecek: ```php public function createServiceFoo(): Foo @@ -210,36 +210,36 @@ public function createServiceFoo(): Foo ``` -İfade Araçları -============== +İfade Dili +========== -Nette DI, neredeyse her şeyi yazabileceğimiz son derece zengin ifade araçları sunar. Yapılandırma dosyalarında [parametreler |configuration#Parametreler] kullanabiliriz: +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% -# anahtar altındaki parametre değeri +# bir anahtarın altındaki parametrenin değeri %mailer.user% -# dize içindeki parametre +# dize içinde parametre '%wwwDir%/images' ``` -Ayrıca nesneler oluşturabilir, metotları ve fonksiyonları çağırabiliriz: +Ayrıca nesne oluşturabilir, metot ve fonksiyon çağırabiliriz: ```neon -# nesne oluşturma +# nesne oluştur DateTime() -# statik metot çağırma +# statik metot çağır Collator::create(%locale%) -# PHP fonksiyonu çağırma +# PHP fonksiyonu çağır ::getenv(DB_USER) ``` -Servislere adlarıyla veya türleriyle başvurabiliriz: +Servislere ya adlarıyla ya da türleriyle başvurabiliriz: ```neon # ada göre servis @@ -249,10 +249,10 @@ Servislere adlarıyla veya türleriyle başvurabiliriz: @Nette\Database\Connection ``` -Birinci sınıf çağrılabilir sözdizimini kullanın: .{data-version:3.2.0} +First-class callable söz dizimini kullanın: .{data-version:3.2.0} ```neon -# geri arama oluşturma, [@user, logout] benzeri +# callback oluşturur, [@user, logout] ile eşdeğerdir @user::logout(...) ``` @@ -262,11 +262,21 @@ Sabitleri kullanın: # sınıf sabiti FilesystemIterator::SKIP_DOTS -# global sabiti PHP fonksiyonu constant() ile alırız -::constant(PHP_VERSION) +# 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ı PHP'de olduğu gibi zincirlenebilir. Sadece basitlik için `->` yerine `::` kullanılır: +Metot çağrıları, tıpkı PHP'deki gibi zincirlenebilir. Basitlik için `->` yerine `::` kullanılır: ```neon DateTime()::format('Y-m-d') @@ -276,7 +286,7 @@ DateTime()::format('Y-m-d') # PHP: $this->getService('http.request')->getUrl()->getHost() ``` -Bu ifadeleri her yerde, [#servis oluşturma], [argümanlarda |#Argümanlar], [#setup] bölümünde veya [parametrelerde |configuration#Parametreler] kullanabilirsiniz: +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: @@ -295,10 +305,10 @@ services: Yapılandırma dosyalarında şu özel fonksiyonları kullanabilirsiniz: -- `not()` değerin olumsuzlanması -- `bool()`, `int()`, `float()`, `string()` kayıpsız olarak belirtilen türe dönüştürme -- `typed()` belirtilen türdeki tüm servislerin bir dizisini oluşturur -- `tagged()` belirtilen etikete sahip tüm servislerin bir dizisini oluşturur +- `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: @@ -308,18 +318,18 @@ services: ) ``` -PHP'deki klasik tür dönüştürme, örneğin `(int)` gibi, aksine kayıpsız tür dönüştürme sayısal olmayan değerler için bir istisna fırlatır. +`(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 veya arayüz) tüm servislerin bir dizisini oluşturur. Autowiring'i devre dışı bırakılmış servisleri atlar. Virgülle ayrılmış birden çok tür de belirtebilirsiniz. +`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 servislerin dizisini [autowiring |autowiring#Servis Dizileri] kullanarak otomatik olarak argüman olarak da iletebilirsiniz. +Belirli bir türdeki servislerden oluşan bir dizi, [autowiring |autowiring#Servis Koleksiyonu] ile argüman olarak otomatik de aktarılabilir. -`tagged()` fonksiyonu daha sonra belirli bir etikete sahip tüm servislerin bir dizisini oluşturur. Burada da virgülle ayrılmış birden çok etiket belirtebilirsiniz. +`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: @@ -330,20 +340,20 @@ services: Autowiring ========== -`autowired` anahtarı, belirli bir servis için autowiring davranışını etkilemenizi sağlar. Ayrıntılar için [autowiring bölümü|autowiring] bakın. +`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ıldı + autowired: false # foo servisi autowiring'den çıkarılır ``` -Lazy Servisler .{data-version:3.2.4} -==================================== +Tembel Servisler .{data-version:3.2.4} +====================================== -Lazy loading, bir servisin oluşturulmasını gerçekten ihtiyaç duyulana kadar erteleyen bir tekniktir. Global yapılandırmada, tüm servisler için aynı anda [lazy oluşturmayı etkinleştirin |configuration#Tembel Servisler]. Bireysel servisler için daha sonra bu davranışı geçersiz kılabilirsiniz: +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: @@ -352,16 +362,20 @@ services: lazy: false ``` -Bir servis lazy olarak tanımlandığında, DI konteynerinden istendiğinde özel bir yer tutucu nesne alırız. Bu, gerçek servis gibi görünür ve davranır, ancak gerçek başlatma (yapıcı ve setup çağrısı) yalnızca herhangi bir metodunun veya özelliğinin ilk çağrısında gerçekleşir. +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] -Lazy loading yalnızca kullanıcı sınıfları için kullanılabilir, dahili PHP sınıfları için kullanılamaz. PHP 8.4 veya daha yenisini gerektirir. +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 (Tags) -================ +Etiketler +========= -Etiketler, servislere ek bilgiler eklemek için kullanılır. Bir servise bir veya daha fazla etiket ekleyebilirsiniz: +Etiketler, servislere ek bilgi eklemeye yarar. Bir servise bir ya da daha çok etiket atayabilirsiniz: ```neon services: @@ -371,7 +385,7 @@ services: - cached ``` -Etiketler ayrıca değerler de taşıyabilir: +Etiketler değer de taşıyabilir: ```neon services: @@ -381,26 +395,26 @@ services: logger: monolog.logger.event ``` -Belirli etiketlere sahip tüm servisleri almak için `tagged()` fonksiyonunu kullanabilirsiniz: +Belirli etiketlerle ilişkili tüm servisleri almak için `tagged()` fonksiyonunu kullanabilirsiniz: ```neon services: - LoggersDependent( tagged(logger) ) ``` -DI konteynerinde, belirli bir etikete sahip tüm servislerin adlarını `findByTag()` metodunu kullanarak alabilirsiniz: +DI container içinde, belirli bir etikete sahip tüm servislerin adlarını `findByTag()` metoduyla alabilirsiniz: ```php $names = $container->findByTag('logger'); -// $names, servis adını ve etiket değerini içeren bir dizidir -// örn. ['foo' => 'monolog.logger.event', ...] +// $names, anahtarları servis adları, değerleri etiket değerleri olan bir dizidir +// örneğin ['foo' => 'monolog.logger.event', ...] ``` -Inject Modu +Inject Kipi =========== -`inject: true` bayrağı kullanılarak, [inject |best-practices:inject-method-attribute#Inject Nitelikleri] anotasyonuna sahip public değişkenler ve [inject*() |best-practices:inject-method-attribute#inject Metotları] metotları aracılığıyla bağımlılıkların iletilmesi etkinleştirilir. +`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: @@ -409,13 +423,13 @@ services: inject: true ``` -Varsayılan olarak, `inject` yalnızca presenter'lar için etkinleştirilir. +Varsayılan olarak `inject` kipi yalnızca presenter'larda açıktır. -Servislerin Değiştirilmesi -========================== +Servis Değişiklikleri +===================== -DI konteyneri, yerleşik veya [kullanıcı uzantısı|extensions] aracılığıyla eklenen birçok servis içerir. Bu servislerin tanımlarını doğrudan yapılandırmada değiştirebilirsiniz. Örneğin, `application.application` servisinin sınıfını, standart olarak `Nette\Application\Application` olan, başka bir sınıfla değiştirebilirsiniz: +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: @@ -424,7 +438,7 @@ services: alteration: true ``` -`alteration` bayrağı bilgilendiricidir ve yalnızca mevcut bir servisi değiştirdiğimizi belirtir. +`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: @@ -437,7 +451,15 @@ services: - '$onStartup[]' = [@resource, init] ``` -Bir servisi yeniden yazarken, orijinal argümanları, setup öğelerini veya etiketleri kaldırmak isteyebiliriz, bunun için `reset` kullanılır: +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: @@ -445,12 +467,12 @@ services: create: MyApplication alteration: true reset: - - arguments - - setup - - tags + arguments: true + setup: true + tags: true ``` -Bir uzantı tarafından eklenen bir servisi kaldırmak isterseniz, bunu şu şekilde yapabilirsiniz: +Bir extension tarafından eklenen bir servisi kaldırmak isterseniz bunu şöyle yapabilirsiniz: ```neon services: diff --git a/dependency-injection/tr/upgrading.texy b/dependency-injection/tr/upgrading.texy new file mode 100644 index 0000000000..a1586f6c77 --- /dev/null +++ b/dependency-injection/tr/upgrading.texy @@ -0,0 +1,49 @@ +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/dependency-injection/uk/@home.texy b/dependency-injection/uk/@home.texy deleted file mode 100644 index 7a92429299..0000000000 --- a/dependency-injection/uk/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection — це патерн проектування, який кардинально змінить ваш погляд на код та розробку. Він відкриє вам шлях до світу чисто спроектованих та підтримуваних застосунків. - -- [Що таке Dependency Injection? |introduction] -- [Глобальний стан та синглтони |global-state] -- [Передача залежностей |passing-dependencies] -- [Що таке DI-контейнер? |container] -- [Часті питання|faq] - - -Пакет `nette/di` надає надзвичайно просунутий компільований DI-контейнер для PHP. - -- [Nette DI Container |nette-container] -- [Конфігурація |configuration] -- [Визначення сервісів |services] -- [Autowiring |autowiring] -- [Згенеровані фабрики |factory] -- [Створення розширень для Nette DI|extensions] diff --git a/dependency-injection/uk/@left-menu.texy b/dependency-injection/uk/@left-menu.texy deleted file mode 100644 index 1c592f52fd..0000000000 --- a/dependency-injection/uk/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Що таке DI? |introduction] -- [Глобальний стан та синглтони |global-state] -- [Передача залежностей |passing-dependencies] -- [Що таке DI-контейнер? |container] -- [Часті питання|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Конфігурація |configuration] -- [Визначення сервісів |services] -- [Autowiring |autowiring] -- [Згенеровані фабрики |factory] -- [Створення розширень для Nette DI|extensions] diff --git a/dependency-injection/uk/@meta.texy b/dependency-injection/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/dependency-injection/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/dependency-injection/uk/autowiring.texy b/dependency-injection/uk/autowiring.texy deleted file mode 100644 index 387589ccd9..0000000000 --- a/dependency-injection/uk/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Автоматичне підключення -*********************** - -.[perex] -Автоматичне підключення (Autowiring) — це чудова функція, яка вміє автоматично передавати до конструктора та інших методів необхідні сервіси, тому нам не потрібно їх взагалі писати. Це заощадить вам багато часу. - -Завдяки цьому ми можемо пропустити переважну більшість аргументів при написанні визначень сервісів. Замість: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Достатньо написати: - -```neon -services: - articles: Model\ArticleRepository -``` - -Автоматичне підключення керується типами, тому для його роботи клас `ArticleRepository` має бути визначений приблизно так: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Щоб можна було використовувати автоматичне підключення, для кожного типу в контейнері має бути **рівно один сервіс**. Якщо їх буде більше, автоматичне підключення не знатиме, який з них передати, і викине виняток: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ВИКИНЕ ВИНЯТОК, підходять mainDb і tempDb -``` - -Рішенням було б або обійти автоматичне підключення та явно вказати назву сервісу (тобто `articles: Model\ArticleRepository(@mainDb)`). Але зручніше [вимкнути |#Вимкнення автоматичного підключення] автоматичне підключення одного з сервісів або [надати перевагу |#Перевага автоматичного підключення] першому сервісу. - - -Вимкнення автоматичного підключення ------------------------------------ - -Автоматичне підключення сервісу можна вимкнути за допомогою опції `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # сервіс tempDb виключено з автоматичного підключення - - articles: Model\ArticleRepository # отже, передасть до конструктора mainDb -``` - -Сервіс `articles` не викине виняток, що існують два відповідні сервіси типу `PDO` (тобто `mainDb` та `tempDb`), які можна передати до конструктора, оскільки він бачить лише сервіс `mainDb`. - -.[note] -Конфігурація автоматичного підключення в Nette працює інакше, ніж у Symfony, де опція `autowire: false` вказує, що не слід використовувати автоматичне підключення для аргументів конструктора даного сервісу. У Nette автоматичне підключення використовується завжди, чи то для аргументів конструктора, чи для будь-яких інших методів. Опція `autowired: false` вказує, що екземпляр даного сервісу не повинен передаватися нікуди за допомогою автоматичного підключення. - - -Перевага автоматичного підключення ----------------------------------- - -Якщо у нас є кілька сервісів одного типу і для одного з них ми вказуємо опцію `autowired`, цей сервіс стає пріоритетним: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # стає пріоритетним - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Сервіс `articles` не викине виняток, що існують два відповідні сервіси типу `PDO` (тобто `mainDb` та `tempDb`), але використає пріоритетний сервіс, тобто `mainDb`. - - -Масив сервісів --------------- - -Автоматичне підключення вміє передавати і масиви сервісів певного типу. Оскільки в PHP неможливо нативно записати тип елементів масиву, потрібно крім типу `array` додати phpDoc коментар з типом елемента у форматі `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI-контейнер потім автоматично передасть масив сервісів, що відповідають даному типу. Він пропустить сервіси, у яких вимкнено автоматичне підключення. - -Тип у коментарі може бути також у форматі `array<int, Class>` або `list<Class>`. Якщо ви не можете вплинути на вигляд phpDoc коментаря, ви можете передати масив сервісів безпосередньо в конфігурації за допомогою [`typed()` |services#Спеціальні функції]. - - -Скалярні аргументи ------------------- - -Автоматичне підключення вміє підставляти лише об'єкти та масиви об'єктів. Скалярні аргументи (наприклад, рядки, числа, булеві значення) [запишемо в конфігурації |services#Аргументи]. Альтернативою є створення [об'єкта налаштувань |best-practices:passing-settings-to-presenters], який інкапсулює скалярне значення (або кілька значень) у вигляді об'єкта, і його потім можна знову передавати за допомогою автоматичного підключення. - -```php -class MySettings -{ - public function __construct( - // readonly можна використовувати з PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Ви створите з нього сервіс, додавши до конфігурації: - -```neon -services: - - MySettings('any value') -``` - -Усі класи потім запитають його за допомогою автоматичного підключення. - - -Звуження автоматичного підключення ----------------------------------- - -Для окремих сервісів можна звузити автоматичне підключення лише до певних класів або інтерфейсів. - -Зазвичай автоматичне підключення передає сервіс до кожного параметра методу, типу якого сервіс відповідає. Звуження означає, що ми встановлюємо умови, яким повинні відповідати типи, зазначені у параметрах методів, щоб їм було передано сервіс. - -Покажемо це на прикладі: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Якщо ми зареєструємо їх усі як сервіси, то автоматичне підключення зазнає невдачі: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ВИКИНЕ ВИНЯТОК, підходять сервіси parent і child - childDep: ChildDependent # автоматичне підключення передасть до конструктора сервіс child -``` - -Сервіс `parentDep` викине виняток `Multiple services of type ParentClass found: parent, child`, оскільки до його конструктора підходять обидва сервіси `parent` і `child`, і автоматичне підключення не може вирішити, який з них вибрати. - -Тому для сервісу `child` ми можемо звузити його автоматичне підключення до типу `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # можна написати і 'autowired: self' - - parentDep: ParentDependent # автоматичне підключення передасть до конструктора сервіс parent - childDep: ChildDependent # автоматичне підключення передасть до конструктора сервіс child -``` - -Тепер до конструктора сервісу `parentDep` передається сервіс `parent`, оскільки тепер це єдиний відповідний об'єкт. Сервіс `child` автоматичне підключення туди вже не передасть. Так, сервіс `child` все ще є типу `ParentClass`, але вже не виконується звужуюча умова, задана для типу параметра, тобто не виконується, що `ParentClass` *є надтипом* `ChildClass`. - -Для сервісу `child` можна було б `autowired: ChildClass` записати також як `autowired: self`, оскільки `self` є заповнювачем для класу поточного сервісу. - -У ключі `autowired` можна вказати і кілька класів або інтерфейсів як масив: - -```neon -autowired: [BarClass, FooInterface] -``` - -Спробуємо доповнити приклад ще інтерфейсами: - -```php -interface FooInterface -{} - -interface BarInterface -{} - -class ParentClass implements FooInterface -{} - -class ChildClass extends ParentClass implements BarInterface -{} - -class FooDependent -{ - function __construct(FooInterface $obj) - {} -} - -class BarDependent -{ - function __construct(BarInterface $obj) - {} -} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Якщо ми ніяк не обмежимо сервіс `child`, він підійде до конструкторів усіх класів `FooDependent`, `BarDependent`, `ParentDependent` та `ChildDependent`, і автоматичне підключення його туди передасть. - -Але якщо ми звузимо його автоматичне підключення до `ChildClass` за допомогою `autowired: ChildClass` (або `self`), автоматичне підключення передасть його лише до конструктора `ChildDependent`, оскільки він вимагає аргумент типу `ChildClass` і виконується умова, що `ChildClass` *є типу* `ChildClass`. Жоден інший тип, зазначений у інших параметрах, не є надтипом `ChildClass`, тому сервіс не передається. - -Якщо ми обмежимо його до `ParentClass` за допомогою `autowired: ParentClass`, автоматичне підключення знову передасть його до конструктора `ChildDependent` (оскільки необхідний `ChildClass` є надтипом `ParentClass`) і тепер також до конструктора `ParentDependent`, оскільки необхідний тип `ParentClass` також є відповідним. - -Якщо ми обмежимо його до `FooInterface`, він все одно буде автоматично підключений до `ParentDependent` (необхідний `ParentClass` є надтипом `FooInterface`) та `ChildDependent`, але крім того, і до конструктора `FooDependent`, однак не до `BarDependent`, оскільки `BarInterface` не є надтипом `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # автоматичне підключення передасть до конструктора child - barDep: BarDependent # ВИКИНЕ ВИНЯТОК, жоден сервіс не відповідає - parentDep: ParentDependent # автоматичне підключення передасть до конструктора child - childDep: ChildDependent # автоматичне підключення передасть до конструктора child -``` diff --git a/dependency-injection/uk/configuration.texy b/dependency-injection/uk/configuration.texy deleted file mode 100644 index be8b7c4869..0000000000 --- a/dependency-injection/uk/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Конфігурація DI-контейнера -************************** - -.[perex] -Огляд конфігураційних опцій для Nette DI-контейнера. - - -Конфігураційний файл -==================== - -Nette DI-контейнер легко керується за допомогою конфігураційних файлів. Вони зазвичай записуються у [форматі NEON|neon:format]. Для редагування рекомендуємо [редактори з підтримкою |best-practices:editors-and-tools#IDE редактор] цього формату. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Декоратор .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI-контейнер .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Розширення]: "Встановлення додаткових DI-розширень .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Включення файлів]: "Включення файлів .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Параметри]: "Параметри .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Автоматична реєстрація сервісів .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Сервіси .[prism-token prism-comment]" -</pre> - -.[note] -Щоб записати рядок, що містить символ `%`, потрібно його екранувати, подвоївши до `%%`. - - -Параметри -========= - -У конфігурації можна визначити параметри, які потім можна використовувати як частину визначень сервісів. Це може зробити конфігурацію більш зрозумілою або об'єднати та виділити значення, які будуть змінюватися. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -На параметр `dsn` можна посилатися будь-де в конфігурації записом `%dsn%`. Параметри можна використовувати і всередині рядків, як `'%wwwDir%/images'`. - -Параметри не обов'язково мають бути лише рядками або числами, вони також можуть містити масиви: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -На конкретний ключ можна посилатися як `%mailer.user%`. - -Якщо вам потрібно у вашому коді, наприклад, у класі, дізнатися значення будь-якого параметра, передайте його до цього класу. Наприклад, у конструкторі. Не існує жодного глобального об'єкта, що представляє конфігурацію, до якого класи могли б звертатися за значеннями параметрів. Це було б порушенням принципу dependency injection. - - -Сервіси -======= - -Див. [окремий розділ|services]. - - -Decorator -========= - -Як масово змінити всі сервіси певного типу? Наприклад, викликати певний метод у всіх presenter'ів, які успадковують від конкретного спільного предка? Для цього існує decorator. - -```neon -decorator: - # для всіх сервісів, що є екземплярами цього класу або інтерфейсу - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # виклич цей метод - - $absoluteUrls = true # і встанови змінну -``` - -Decorator можна також використовувати для налаштування [тегів |services#Теги] або ввімкнення режиму [inject |services#Режим Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Технічні налаштування DI-контейнера. - -```neon -di: - # показати DI-контейнер у Tracy Bar? - debugger: ... # (bool) за замовчуванням true - - # типи параметрів, які ніколи не підключати автоматично - excluded: ... # (string[]) - - # дозволити ліниве створення сервісів? - lazy: ... # (bool) за замовчуванням false - - # клас, від якого успадковується DI-контейнер - parentClass: ... # (string) за замовчуванням Nette\DI\Container -``` - - -Lazy-сервіси .{data-version:3.2.4} ----------------------------------- - -Налаштування `lazy: true` активує ліниве (відкладене) створення сервісів. Це означає, що сервіси не створюються насправді в момент, коли ми їх запитуємо з DI-контейнера, а лише в момент їх першого використання. Це може прискорити запуск програми та зменшити споживання пам'яті, оскільки створюються лише ті сервіси, які дійсно потрібні в даному запиті. - -Для конкретного сервісу ліниве створення можна [змінити |services#Lazy-сервіси]. - -.[note] -Ліниві об'єкти можна використовувати лише для користувацьких класів, а не для внутрішніх класів PHP. Потребує PHP 8.4 або новішої версії. - - -Експорт метаданих ------------------ - -Клас DI-контейнера містить також багато метаданих. Ви можете зменшити його розмір, скоротивши експорт метаданих. - -```neon -di: - export: - # експортувати параметри? - parameters: false # (bool) за замовчуванням true - - # експортувати теги і які? - tags: # (string[]|bool) за замовчуванням всі - - event.subscriber - - # експортувати дані для автопідключення і які? - types: # (string[]|bool) за замовчуванням всі - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Якщо ви не використовуєте масив `$container->getParameters()`, ви можете вимкнути експорт параметрів. Далі, ви можете експортувати лише ті теги, через які ви отримуєте сервіси методом `$container->findByTag(...)`. Якщо ви взагалі не викликаєте цей метод, ви можете повністю вимкнути експорт тегів за допомогою `false`. - -Ви можете значно скоротити метадані для [автоматичного підключення|autowiring], вказавши класи, які ви використовуєте як параметр методу `$container->getByType()`. І знову ж таки, якщо ви взагалі не викликаєте цей метод (або лише в [bootstrap|application:bootstrapping] для отримання `Nette\Application\Application`), ви можете повністю вимкнути експорт за допомогою `false`. - - -Розширення -========== - -Реєстрація додаткових DI-розширень. Таким чином додамо, наприклад, DI-розширення `Dibi\Bridges\Nette\DibiExtension22` під назвою `dibi`. - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Потім ми конфігуруємо його в секції `dibi`: - -```neon -dibi: - host: localhost -``` - -Як розширення можна додати і клас, який має параметри: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Включення файлів -================ - -Додаткові конфігураційні файли можна включити в секції `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Назва `parameters.php` не є помилкою, конфігурація може бути записана також у PHP-файлі, який поверне її як масив: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Якщо в конфігураційних файлах з'являться елементи з однаковими ключами, вони будуть перезаписані, або у випадку [масивів об'єднані |#Об єднання]. Файл, що включається пізніше, має вищий пріоритет, ніж попередній. Файл, у якому вказана секція `includes`, має вищий пріоритет, ніж файли, що включаються в ньому. - - -Search -====== - -Автоматичне додавання сервісів до DI-контейнера надзвичайно полегшує роботу. Nette автоматично додає до контейнера presenter'и, але можна легко додавати й будь-які інші класи. - -Достатньо вказати, у яких каталогах (та підкаталогах) слід шукати класи: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Зазвичай, однак, ми не хочемо додавати абсолютно всі класи та інтерфейси, тому їх можна фільтрувати: - -```neon -search: - - in: %appDir%/Forms - - # фільтрація за назвою файлу (string|string[]) - files: - - *Factory.php - - # фільтрація за назвою класу (string|string[]) - classes: - - *Factory -``` - -Або ми можемо вибирати класи, які успадковують або реалізують принаймні один із зазначених класів: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Можна визначити і правила виключення, тобто маски назви класу або предків, які, якщо відповідають, сервіс не додається до DI-контейнера: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Усім сервісам можна встановити теги: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Об'єднання -========== - -Якщо у кількох конфігураційних файлах з'являться елементи з однаковими ключами, вони будуть перезаписані, або у випадку масивів об'єднані. Файл, що включається пізніше, має вищий пріоритет, ніж попередній. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>результат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Для масивів можна запобігти об'єднанню, вказавши знак оклику після назви ключа: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>результат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Конфігурація Dependency Injection}} diff --git a/dependency-injection/uk/container.texy b/dependency-injection/uk/container.texy deleted file mode 100644 index 82010d258f..0000000000 --- a/dependency-injection/uk/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Що таке DI-контейнер? -********************* - -.[perex] -Dependency injection контейнер (DIC) — це клас, який вміє інстанціювати та конфігурувати об'єкти. - -Можливо, вас це здивує, але в багатьох випадках вам не потрібен dependency injection контейнер, щоб скористатися перевагами dependency injection (коротко DI). Адже навіть у [вступному розділі|introduction] ми показали DI на конкретних прикладах, і жоден контейнер не був потрібний. - -Однак, якщо вам потрібно керувати великою кількістю різних об'єктів з багатьма залежностями, dependency injection контейнер буде дійсно корисним. Що, наприклад, стосується веб-додатків, побудованих на фреймворку. - -У попередньому розділі ми представили класи `Article` та `UserController`. Обидва мають певні залежності, а саме базу даних та фабрику `ArticleFactory`. І для цих класів ми тепер створимо контейнер. Звичайно, для такого простого прикладу немає сенсу мати контейнер. Але ми створимо його, щоб показати, як він виглядає і працює. - -Ось простий жорстко закодований контейнер для наведеного прикладу: - -```php -class Container -{ - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection('mysql:', 'root', '***'); - } - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->createDatabase()); - } - - public function createUserController(): UserController - { - return new UserController($this->createArticleFactory()); - } -} -``` - -Використання виглядало б так: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Ми лише запитуємо у контейнера об'єкт і вже не повинні нічого знати про те, як його створити та які у нього залежності; все це знає контейнер. Залежності контейнером вводяться автоматично. У цьому його сила. - -Контейнер поки що має всі дані записані жорстко. Зробимо наступний крок і додамо параметри, щоб контейнер став дійсно корисним: - -```php -class Container -{ - public function __construct( - private array $parameters, - ) { - } - - public function createDatabase(): Nette\Database\Connection - { - return new Nette\Database\Connection( - $this->parameters['db.dsn'], - $this->parameters['db.user'], - $this->parameters['db.password'], - ); - } - - // ... -} - -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); -``` - -Уважні читачі, можливо, помітили певну проблему. Кожного разу, коли я отримую об'єкт `UserController`, також створюється новий екземпляр `ArticleFactory` та бази даних. Цього ми точно не хочемо. - -Тому додамо метод `getService()`, який буде повертати завжди ті самі екземпляри: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') викличе createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -При першому виклику, наприклад, `$container->getService('Database')`, він попросить `createDatabase()` створити об'єкт бази даних, який збереже в масиві `$services`, а при наступному виклику просто поверне його. - -Змінимо і решту контейнера, щоб він використовував `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -До речі, терміном "сервіс" позначається будь-який об'єкт, керований контейнером. Тому й назва методу `getService()`. - -Готово. У нас є повністю функціональний DI-контейнер! І ми можемо його використовувати: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Як бачите, написати DIC не так вже й складно. Варто нагадати, що самі об'єкти не знають, що їх створює якийсь контейнер. Таким чином, можна створювати будь-який об'єкт у PHP без втручання в його вихідний код. - -Ручне створення та підтримка класу контейнера може досить швидко стати кошмаром. Тому в наступному розділі ми поговоримо про [Nette DI Container|nette-container], який вміє генеруватися та оновлюватися майже самостійно. - - -{{maintitle: Що таке dependency injection контейнер?}} diff --git a/dependency-injection/uk/extensions.texy b/dependency-injection/uk/extensions.texy deleted file mode 100644 index c3e3aa8511..0000000000 --- a/dependency-injection/uk/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Створення розширень для Nette DI -******************************** - -.[perex] -На генерацію DI-контейнера, крім конфігураційних файлів, впливають також так звані *розширення*. Ми активуємо їх у конфігураційному файлі в секції `extensions`. - -Так ми додаємо розширення, представлене класом `BlogExtension`, під назвою `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Кожне розширення компілятора успадковує від [api:Nette\DI\CompilerExtension] і може реалізовувати наступні методи, які послідовно викликаються під час складання DI-контейнера: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Цей метод викликається першим. Він визначає схему для валідації конфігураційних параметрів. - -Розширення конфігуруємо в секції, назва якої збігається з тією, під якою було додано розширення, тобто `blog`: - -```neon -# та сама назва, що й у розширення -blog: - postsPerPage: 10 - allowComments: false -``` - -Створимо схему, що описує всі конфігураційні опції, включаючи їхні типи, допустимі значення та, можливо, значення за замовчуванням: - -```php -use Nette\Schema\Expect; - -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function getConfigSchema(): Nette\Schema\Schema - { - return Expect::structure([ - 'postsPerPage' => Expect::int(), - 'allowComments' => Expect::bool()->default(true), - ]); - } -} -``` - -Документацію знайдете на сторінці [Schema |schema:]. Крім того, можна визначити, які опції можуть бути [динамічними |application:bootstrapping#Динамічні параметри] за допомогою `dynamic()`, наприклад `Expect::int()->dynamic()`. - -До конфігурації ми отримуємо доступ через змінну `$this->config`, яка є об'єктом `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Використовується для додавання сервісів до контейнера. Для цього служить [api:Nette\DI\ContainerBuilder]: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // або setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Конвенція полягає в тому, щоб префіксувати сервіси, додані розширенням, його назвою, щоб уникнути конфліктів імен. Це робить метод `prefix()`, тому якщо розширення називається `blog`, сервіс матиме назву `blog.articles`. - -Якщо потрібно перейменувати сервіс, для збереження зворотної сумісності можна створити псевдонім з оригінальною назвою. Подібно Nette робить, наприклад, для сервісу `routing.router`, який доступний і під попередньою назвою `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Завантаження сервісів з файлу ------------------------------ - -Сервіси можна створювати не лише за допомогою API класу ContainerBuilder, але й відомим записом, що використовується в конфігураційному файлі NEON у секції services. Префікс `@extension` представляє поточне розширення. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Завантажимо сервіси: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // завантаження конфігураційного файлу для розширення - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Метод викликається в момент, коли контейнер містить усі сервіси, додані окремими розширеннями в методах `loadConfiguration`, а також користувацькими конфігураційними файлами. На цій стадії складання ми можемо редагувати визначення сервісів або доповнювати зв'язки між ними. Для пошуку сервісів у контейнері за тегами можна використовувати метод `findByTag()`, а за класом чи інтерфейсом - метод `findByType()`. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function beforeCompile() - { - $builder = $this->getContainerBuilder(); - - foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) { - $builder->getDefinition($serviceName)->addSetup('setLogger'); - } - } -} -``` - - -afterCompile() .[method] -======================== - -На цій фазі клас контейнера вже згенеровано у вигляді об'єкта [ClassType |php-generator:#Класи], він містить усі методи, що створюють сервіси, і готовий до запису в кеш. Кінцевий код класу ми можемо на цьому етапі ще змінити. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Клас Configurator після [створення контейнера |application:bootstrapping#index.php] викликає ініціалізаційний код, який створюється записом в об'єкт `$this->initialization` за допомогою [методу addBody() |php-generator:#Тіла методів та функцій]. - -Покажемо приклад, як, наприклад, ініціалізаційним кодом запустити сесію або запустити сервіси, що мають тег `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // автоматичний запуск сесії - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // сервіси з тегом run мають бути створені після інстанціювання контейнера - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/uk/factory.texy b/dependency-injection/uk/factory.texy deleted file mode 100644 index c3f0ffeb36..0000000000 --- a/dependency-injection/uk/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Згенеровані фабрики -******************* - -.[perex] -Nette DI вміє автоматично генерувати код фабрик на основі інтерфейсів, що заощаджує вам написання коду. - -Фабрика — це клас, який виробляє та конфігурує об'єкти. Отже, вона передає їм і їхні залежності. Будь ласка, не плутайте з патерном проектування *factory method*, який описує специфічний спосіб використання фабрик і не пов'язаний з цією темою. - -Як виглядає така фабрика, ми показали у [вступному розділі |introduction#Фабрика]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI вміє автоматично генерувати код фабрик. Все, що вам потрібно зробити, це створити інтерфейс, і Nette DI згенерує реалізацію. Інтерфейс повинен мати рівно один метод з назвою `create` та декларувати тип повернення: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Отже, фабрика `ArticleFactory` має метод `create`, який створює об'єкти `Article`. Клас `Article` може виглядати, наприклад, так: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Фабрику додаємо до конфігураційного файлу: - -```neon -services: - - ArticleFactory -``` - -Nette DI згенерує відповідну реалізацію фабрики. - -У коді, який використовує фабрику, ми запитуємо об'єкт за інтерфейсом, і Nette DI використає згенеровану реалізацію: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // дозволимо фабриці створити об'єкт - $article = $this->articleFactory->create(); - } -} -``` - - -Параметризована фабрика -======================= - -Фабричний метод `create` може приймати параметри, які потім передасть до конструктора. Доповнимо, наприклад, клас `Article` ідентифікатором автора статті: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Параметр додамо також до фабрики: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Завдяки тому, що параметр у конструкторі та параметр у фабриці називаються однаково, Nette DI їх повністю автоматично передасть. - - -Розширена дефініція -=================== - -Визначення можна записати і в багаторядковому вигляді за допомогою ключа `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -При записі цим довшим способом можна вказати додаткові аргументи для конструктора в ключі `arguments` та додаткову конфігурацію за допомогою `setup`, так само, як для звичайних сервісів. - -Приклад: якби метод `create()` не приймав параметр `$authorId`, ми могли б вказати фіксоване значення в конфігурації, яке передавалося б до конструктора `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Або навпаки, якби `create()` приймав параметр `$authorId`, але він не був би частиною конструктора і передавався б методом `Article::setAuthorId()`, ми б посилалися на нього в секції `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette, крім фабрик, вміє генерувати так звані accessor'и. Це об'єкти з методом `get()`, який повертає певний сервіс з DI-контейнера. Повторний виклик `get()` повертає завжди той самий екземпляр. - -Accessor'и забезпечують ліниве завантаження (lazy-loading) залежностей. Уявімо клас, який записує помилки до спеціальної бази даних. Якби цей клас отримував підключення до бази даних як залежність через конструктор, підключення завжди б створювалося, хоча на практиці помилка виникає лише зрідка, і тому здебільшого з'єднання залишалося б невикористаним. Замість цього клас передає accessor, і лише коли викликається його `get()`, відбувається створення об'єкта бази даних: - -Як створити accessor? Достатньо написати інтерфейс, і Nette DI згенерує реалізацію. Інтерфейс повинен мати рівно один метод з назвою `get` та декларувати тип повернення: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Accessor додаємо до конфігураційного файлу, де також є визначення сервісу, який він буде повертати: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Оскільки accessor повертає сервіс типу `PDO`, а в конфігурації є лише один такий сервіс, він повертатиме саме його. Якщо сервісів даного типу було б більше, ми б визначили сервіс, що повертається, за допомогою назви, наприклад, `- PDOAccessor(@db1)`. - - -Багаторазова фабрика/accessor -============================= -Наші фабрики та accessor'и досі вміли завжди виробляти або повертати лише один об'єкт. Але можна дуже легко створити і багаторазові фабрики, комбіновані з accessor'ами. Інтерфейс такого класу міститиме довільну кількість методів з назвами `create<name>()` та `get<name>()`, наприклад: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Отже, замість того, щоб передавати кілька згенерованих фабрик та accessor'ів, ми передамо одну більш комплексну фабрику, яка вміє більше. - -Альтернативно, замість кількох методів можна використовувати `get()` з параметром: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Тоді виконується, що `MultiFactory::getArticle()` робить те саме, що й `MultiFactoryAlt::get('article')`. Однак альтернативний запис має той недолік, що незрозуміло, які значення `$name` підтримуються, і логічно також неможливо в інтерфейсі розрізнити різні значення, що повертаються, для різних `$name`. - - -Визначення списком ------------------- -Таким чином можна визначити багаторазову фабрику в конфігурації: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # визначає createArticle() - db: PDO(%dsn%, %user%, %password%) # визначає getDb() - ) -``` - -Або ми можемо у визначенні фабрики посилатися на існуючі сервіси за допомогою посилання: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # визначає createArticle() - db: @\PDO # визначає getDb() - ) -``` - - -Визначення за допомогою тегів ------------------------------ - -Другою можливістю є використання для визначення [тегів |services#Теги]: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/uk/faq.texy b/dependency-injection/uk/faq.texy deleted file mode 100644 index 449d37a9da..0000000000 --- a/dependency-injection/uk/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Часті питання про DI (FAQ) -************************** - - -Чи є DI іншою назвою для IoC? ------------------------------ - -*Inversion of Control* (IoC) — це принцип, зосереджений на способі виконання коду: чи ваш код запускає чужий, чи ваш код інтегрований у чужий, який його потім викликає. IoC — це широкий термін, що охоплює [події |nette:glossary#Події události], так званий [Голлівудський принцип |application:components#Голлівудський стиль] та інші аспекти. Частиною цієї концепції є також фабрики, про які йдеться у [Правило №3: залиште це фабриці |introduction#Правило 3: доручи це фабриці], і які представляють інверсію для оператора `new`. - -*Dependency Injection* (DI) зосереджується на способі, яким один об'єкт дізнається про інший об'єкт, тобто про його залежності. Це патерн проектування, який вимагає явного передавання залежностей між об'єктами. - -Отже, можна сказати, що DI є специфічною формою IoC. Однак не всі форми IoC є доцільними з точки зору чистоти коду. Наприклад, до антипатернів належать техніки, що працюють з [глобальним станом |global-state] або так званий [Service Locator |#Що таке Service Locator]. - - -Що таке Service Locator? ------------------------- - -Це альтернатива Dependency Injection. Він працює так, що створює центральне сховище, де реєструються всі доступні сервіси або залежності. Коли об'єкту потрібна залежність, він запитує її у Service Locator. - -Однак, порівняно з Dependency Injection, він втрачає прозорість: залежності не передаються об'єктам безпосередньо і їх не так легко ідентифікувати, що вимагає дослідження коду для виявлення та розуміння всіх зв'язків. Тестування також складніше, оскільки ми не можемо просто передавати mock-об'єкти тестованим об'єктам, а повинні робити це через Service Locator. Крім того, Service Locator порушує дизайн коду, оскільки окремі об'єкти повинні знати про його існування, що відрізняється від Dependency Injection, де об'єкти не мають уявлення про DI-контейнер. - - -Коли краще не використовувати DI? ---------------------------------- - -Немає відомих труднощів, пов'язаних з використанням патерну проектування Dependency Injection. Навпаки, отримання залежностей з глобально доступних місць призводить до [цілої низки ускладнень |global-state], так само як і використання Service Locator. Тому доцільно використовувати DI завжди. Це не догматичний підхід, а просто не було знайдено кращої альтернативи. - -Проте існують певні ситуації, коли ми не передаємо об'єкти, а отримуємо їх з глобального простору. Наприклад, при налагодженні коду, коли потрібно в конкретній точці програми вивести значення змінної, виміряти тривалість певної частини програми або записати повідомлення. У таких випадках, коли йдеться про тимчасові дії, які пізніше будуть видалені з коду, легітимно використовувати глобально доступний дампер, секундомір або логер. Ці інструменти не належать до дизайну коду. - - -Чи має використання DI свої тіньові сторони? --------------------------------------------- - -Чи несе використання Dependency Injection якісь недоліки, такі як підвищена складність написання коду або погіршена продуктивність? Що ми втрачаємо, коли починаємо писати код відповідно до DI? - -DI не впливає на продуктивність або споживання пам'яті програми. Певну роль може відігравати продуктивність DI-контейнера, однак у випадку [Nette DI |nette-container] контейнер компілюється в чистий PHP, тому його накладні витрати під час роботи програми практично нульові. - -При написанні коду буває необхідно створювати конструктори, що приймають залежності. Раніше це могло бути трудомістким, однак завдяки сучасним IDE та [constructor property promotion |https://blog.nette.org/uk/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] це тепер питання кількох секунд. Фабрики можна легко генерувати за допомогою Nette DI та плагіна для PhpStorm кліком миші. З іншого боку, відпадає потреба писати singleton'и та статичні точки доступу. - -Можна констатувати, що правильно спроектована програма, що використовує DI, не є ні коротшою, ні довшою порівняно з програмою, що використовує singleton'и. Частини коду, що працюють із залежностями, просто вилучаються з окремих класів і переміщуються на нові місця, тобто до DI-контейнера та фабрик. - - -Як legacy-додаток переписати на DI? ------------------------------------ - -Перехід від legacy-додатка до Dependency Injection може бути складним процесом, особливо для великих і комплексних додатків. Важливо підходити до цього процесу систематично. - -- При переході на Dependency Injection важливо, щоб усі члени команди розуміли принципи та процедури, що використовуються. -- Спочатку проведіть аналіз існуючого додатка та ідентифікуйте ключові компоненти та їхні залежності. Створіть план, які частини будуть рефакторені та в якому порядку. -- Реалізуйте DI-контейнер або, ще краще, використайте існуючу бібліотеку, наприклад, Nette DI. -- Поступово рефакторте окремі частини додатка, щоб вони використовували Dependency Injection. Це може включати зміни конструкторів або методів так, щоб вони приймали залежності як параметри. -- Змініть місця в коді, де створюються об'єкти із залежностями, щоб замість цього залежності вводилися контейнером. Це може включати використання фабрик. - -Пам'ятайте, що перехід на Dependency Injection — це інвестиція в якість коду та довгострокову підтримку додатка. Хоча може бути складно виконати ці зміни, результатом має бути чистіший, модульніший та легко тестований код, готовий до майбутнього розширення та підтримки. - - -Чому композиції надається перевага перед успадкуванням? -------------------------------------------------------- -Доцільніше використовувати [композицію |nette:introduction-to-object-oriented-programming#Композиція] замість [успадкування |nette:introduction-to-object-oriented-programming#Успадкування], оскільки вона служить для повторного використання коду, не турбуючись про наслідки змін. Таким чином, вона забезпечує вільніший зв'язок, коли нам не потрібно турбуватися, що зміна якогось коду спричинить необхідність зміни іншого залежного коду. Типовим прикладом є ситуація, що позначається як [пекло конструкторів |passing-dependencies#Пекло конструкторів]. - - -Чи можна використовувати Nette DI Container поза Nette? -------------------------------------------------------- - -Безумовно. Nette DI Container є частиною Nette, але він розроблений як самостійна бібліотека, яка може бути використана незалежно від інших частин фреймворку. Достатньо встановити її за допомогою Composer, створити конфігураційний файл з визначенням ваших сервісів, а потім за допомогою кількох рядків PHP-коду створити DI-контейнер. І одразу можете почати використовувати переваги Dependency Injection у своїх проектах. - -Як виглядає конкретне використання, включаючи коди, описує розділ [Nette DI Container |nette-container]. - - -Чому конфігурація у файлах NEON? --------------------------------- - -NEON — це проста та легко читабельна конфігураційна мова, яка була розроблена в рамках Nette для налаштування додатків, сервісів та їхніх залежностей. Порівняно з JSON або YAML, вона пропонує для цієї мети набагато інтуїтивніші та гнучкіші можливості. У NEON можна природно описати зв'язки, які в Symfony & YAMLu було б неможливо записати або взагалі, або лише за допомогою складного опису. - - -Чи не сповільнює додаток парсинг файлів NEON? ---------------------------------------------- - -Хоча файли NEON парсяться дуже швидко, цей аспект взагалі не має значення. Причина в тому, що парсинг файлів відбувається лише один раз при першому запуску додатка. Потім генерується код DI-контейнера, зберігається на диску і запускається при кожному наступному запиті, без необхідності виконувати подальший парсинг. - -Так це працює в робочому середовищі. Під час розробки файли NEON парсяться кожного разу, коли відбувається зміна їхнього вмісту, щоб розробник завжди мав актуальний DI-контейнер. Сам парсинг, як було сказано, є питанням миттєвості. - - -Як отримати доступ до параметрів у конфігураційному файлі з мого класу? ------------------------------------------------------------------------ - -Пам'ятаймо [Правило №1: нехай тобі це передадуть |introduction#Правило 1: нехай тобі це передадуть]. Якщо клас вимагає інформацію з конфігураційного файлу, нам не потрібно думати, як отримати цю інформацію, замість цього ми просто просимо її — наприклад, через конструктор класу. А передачу здійснюємо в конфігураційному файлі. - -У цьому прикладі `%myParameter%` є заповнювачем для значення параметра `myParameter`, який передається до конструктора класу `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Якщо ви хочете передавати більше параметрів або використовувати автоматичне підключення, доцільно [упакувати параметри в об'єкт |best-practices:passing-settings-to-presenters]. - - -Чи підтримує Nette PSR-11: Container interface? ------------------------------------------------ - -Nette DI Container не підтримує PSR-11 безпосередньо. Однак, якщо вам потрібна взаємодія між Nette DI Container та бібліотеками або фреймворками, які очікують PSR-11 Container Interface, ви можете створити [простий адаптер |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], який слугуватиме мостом між Nette DI Container та PSR-11. diff --git a/dependency-injection/uk/global-state.texy b/dependency-injection/uk/global-state.texy deleted file mode 100644 index c573434ce1..0000000000 --- a/dependency-injection/uk/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Глобальний стан та singleton'и -****************************** - -.[perex] -Попередження: Наступні конструкції є ознакою погано спроектованого коду: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` або `static::$var` - -Чи зустрічаються деякі з цих конструкцій у вашому коді? Тоді у вас є можливість його покращити. Можливо, ви думаєте, що це звичайні конструкції, які ви бачите, наприклад, у демонстраційних рішеннях різних бібліотек та фреймворків. Якщо це так, то дизайн їхнього коду не є добрим. - -Зараз ми точно не говоримо про якусь академічну чистоту. Всі ці конструкції мають одну спільну рису: вони використовують глобальний стан. А він має руйнівний вплив на якість коду. Класи брешуть про свої залежності. Код стає непередбачуваним. Плутає програмістів та знижує їхню ефективність. - -У цьому розділі ми пояснимо, чому це так, і як уникнути глобального стану. - - -Глобальний зв'язок ------------------- - -В ідеальному світі об'єкт повинен мати можливість спілкуватися лише з об'єктами, які йому були [безпосередньо передані |passing-dependencies]. Якщо я створю два об'єкти `A` та `B` і ніколи не передам посилання між ними, то ні `A`, ні `B` не зможуть отримати доступ до іншого об'єкта або змінити його стан. Це дуже бажана властивість коду. Це схоже на те, якби у вас була батарейка та лампочка; лампочка не світитиме, доки ви не з'єднаєте її з батарейкою дротом. - -Але це не стосується глобальних (статичних) змінних або singleton'ів. Об'єкт `A` міг би *бездротово* отримати доступ до об'єкта `C` та модифікувати його без будь-якої передачі посилання, викликавши `C::changeSomething()`. Якщо об'єкт `B` також звернеться до глобального `C`, то `A` та `B` можуть взаємно впливати один на одного через `C`. - -Використання глобальних змінних вносить у систему нову форму *бездротового* зв'язку, яка невидима ззовні. Створює димову завісу, що ускладнює розуміння та використання коду. Щоб розробники дійсно зрозуміли залежності, вони повинні прочитати кожен рядок вихідного коду. Замість простого ознайомлення з інтерфейсом класів. До того ж, це абсолютно зайвий зв'язок. Глобальний стан використовується тому, що він легко доступний звідусіль і дозволяє, наприклад, записати в базу даних через глобальний (статичний) метод `DB::insert()`. Але, як ми покажемо, перевага, яку це дає, незначна, натомість ускладнення це спричиняє фатальні. - -.[note] -З точки зору поведінки немає різниці між глобальною та статичною змінною. Вони однаково шкідливі. - - -Моторошна дія на відстані -------------------------- - -"Моторошна дія на відстані" - так славетно назвав у 1935 році Альберт Ейнштейн явище в квантовій фізиці, яке викликало у нього мурашки по шкірі. -Йдеться про квантове заплутування, особливістю якого є те, що коли ви вимірюєте інформацію про одну частинку, ви миттєво впливаєте на іншу частинку, навіть якщо вони знаходяться на відстані мільйонів світлових років одна від одної. Що, здавалося б, порушує основний закон Всесвіту, що ніщо не може поширюватися швидше за світло. - -У світі програмного забезпечення ми можемо назвати "моторошною дією на відстані" ситуацію, коли ми запускаємо якийсь процес, про який вважаємо, що він ізольований (оскільки ми не передали йому жодних посилань), але у віддалених місцях системи відбуваються несподівані взаємодії та зміни стану, про які ми не мали уявлення. Це може статися лише через глобальний стан. - -Уявіть, що ви приєдналися до команди розробників проекту, який має велику розвинену кодову базу. Ваш новий керівник просить вас реалізувати нову функцію, і ви, як правильний розробник, починаєте з написання тесту. Але оскільки ви новачок у проекті, ви робите багато дослідницьких тестів типу "що станеться, якщо я викличу цей метод". І спробуєте написати наступний тест: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // номер вашої картки - $cc->charge(100); -} -``` - -Ви запускаєте код, можливо, кілька разів, і через деякий час помічаєте на мобільному сповіщення від банку, що при кожному запуску з вашої платіжної картки списувалося 100 доларів 🤦‍♂️ - -Як, чорт забирай, тест міг спричинити реальне списання грошей? Оперувати платіжною карткою непросто. Ви повинні спілкуватися з веб-сервісом третьої сторони, ви повинні знати URL цього веб-сервісу, ви повинні увійти в систему і так далі. Жодна з цих інформацій не міститься в тесті. Ба більше, ви навіть не знаєте, де ця інформація знаходиться, а отже, і як мокувати зовнішні залежності, щоб кожен запуск не призводив до того, що знову списується 100 доларів. І як ви, як новий розробник, мали знати, що те, що ви збираєтеся зробити, призведе до того, що ви станете на 100 доларів біднішими? - -Це моторошна дія на відстані! - -Вам не залишається нічого іншого, як довго копатися в купі вихідних кодів, питати старших та досвідченіших колег, перш ніж ви зрозумієте, як працюють зв'язки в проекті. Це спричинено тим, що при погляді на інтерфейс класу `CreditCard` неможливо визначити глобальний стан, який потрібно ініціалізувати. Навіть погляд на вихідний код класу вам не підкаже, який ініціалізаційний метод ви маєте викликати. У кращому випадку ви можете знайти глобальну змінну, до якої здійснюється доступ, і з неї спробувати здогадатися, як її ініціалізувати. - -Класи в такому проекті є патологічними брехунами. Платіжна картка вдає, що її достатньо інстанціювати та викликати метод `charge()`. Але приховано вона співпрацює з іншим класом `PaymentGateway`, який представляє платіжний шлюз. Його інтерфейс також говорить, що його можна ініціалізувати окремо, але насправді він витягує облікові дані з якогось конфігураційного файлу і так далі. Розробникам, які написали цей код, зрозуміло, що `CreditCard` потребує `PaymentGateway`. Вони написали код таким чином. Але для кожного, хто є новачком у проекті, це повна загадка і заважає навчанню. - -Як виправити ситуацію? Легко. **Нехай API декларує залежності.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Зверніть увагу, як раптом стають очевидними зв'язки всередині коду. Тим, що метод `charge()` декларує, що потребує `PaymentGateway`, вам не потрібно нікого питати про те, як пов'язаний код. Ви знаєте, що повинні створити його екземпляр, і коли спробуєте це зробити, зіткнетеся з тим, що повинні надати параметри доступу. Без них код навіть не запуститься. - -І головне, тепер ви можете мокувати платіжний шлюз, тож при кожному запуску тесту вам не буде нараховуватися 100 доларів. - -Глобальний стан призводить до того, що ваші об'єкти можуть таємно отримувати доступ до речей, які не задекларовані в їхньому API, і в результаті роблять ваші API патологічними брехунами. - -Можливо, ви раніше не думали про це так, але кожного разу, коли ви використовуєте глобальний стан, ви створюєте таємні бездротові канали зв'язку. Моторошна дія на відстані змушує розробників читати кожен рядок коду, щоб зрозуміти потенційні взаємодії, знижує продуктивність розробників та плутає нових членів команди. Якщо ви той, хто створив код, ви знаєте справжні залежності, але кожен, хто прийде після вас, безпорадний. - -Не пишіть код, який використовує глобальний стан, надавайте перевагу передачі залежностей. Тобто dependency injection. - - -Крихкість глобального стану ---------------------------- - -У коді, який використовує глобальний стан та singleton'и, ніколи не можна бути впевненим, коли і хто цей стан змінив. Цей ризик з'являється вже при ініціалізації. Наступний код має створити підключення до бази даних та ініціалізувати платіжний шлюз, однак постійно викидає виняток, і пошук причини є надзвичайно тривалим: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Ви повинні детально переглядати код, щоб з'ясувати, що об'єкт `PaymentGateway` бездротово звертається до інших об'єктів, деякі з яких вимагають підключення до бази даних. Отже, необхідно ініціалізувати базу даних раніше, ніж `PaymentGateway`. Однак димова завіса глобального стану це від вас приховує. Скільки часу ви б зекономили, якби API окремих класів не обманювало і декларувало свої залежності? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Подібна проблема виникає і при використанні глобального доступу до підключення до бази даних: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -При виклику методу `save()` невідомо, чи було вже створено підключення до бази даних та хто несе відповідальність за його створення. Якщо ми хочемо, наприклад, змінювати підключення до бази даних під час виконання, наприклад, для тестів, нам, ймовірно, довелося б створити додаткові методи, такі як `DB::reconnect(...)` або `DB::reconnectForTest()`. - -Розглянемо приклад: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Де ми маємо впевненість, що при виклику `$article->save()` дійсно використовується тестова база даних? Що, якщо метод `Foo::doSomething()` змінив глобальне підключення до бази даних? Щоб з'ясувати це, нам довелося б дослідити вихідний код класу `Foo` і, ймовірно, багатьох інших класів. Цей підхід, однак, дав би лише короткострокову відповідь, оскільки ситуація може змінитися в майбутньому. - -А що, якщо підключення до бази даних перемістити в статичну змінну всередині класу `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Це абсолютно нічого не змінило. Проблемою є глобальний стан, і абсолютно байдуже, в якому класі він ховається. У цьому випадку, так само як і в попередньому, ми не маємо при виклику методу `$article->save()` жодного натяку на те, до якої бази даних буде здійснено запис. Будь-хто на іншому кінці програми міг будь-коли за допомогою `Article::setDb()` змінити базу даних. Нам під носом. - -Глобальний стан робить нашу програму **надзвичайно крихкою**. - -Однак існує простий спосіб вирішити цю проблему. Достатньо дозволити API декларувати залежності, що забезпечить правильну функціональність. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Завдяки цьому підходу зникає побоювання щодо прихованих та несподіваних змін підключення до бази даних. Тепер ми маємо впевненість, куди зберігається стаття, і жодні зміни коду всередині іншого непов'язаного класу вже не можуть змінити ситуацію. Код вже не крихкий, а стабільний. - -Не пишіть код, який використовує глобальний стан, надавайте перевагу передачі залежностей. Тобто dependency injection. - - -Singleton ---------- - -Singleton — це патерн проектування, який, згідно з "визначенням":https://en.wikipedia.org/wiki/Singleton_pattern з відомої публікації Gang of Four, обмежує клас єдиним екземпляром і пропонує до нього глобальний доступ. Реалізація цього патерну зазвичай схожа на наступний код: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // та інші методи, що виконують функції даного класу -} -``` - -На жаль, singleton вводить у програму глобальний стан. А як ми показали вище, глобальний стан є небажаним. Тому singleton вважається антипатерном. - -Не використовуйте у своєму коді singleton'и та замініть їх іншими механізмами. Singleton'и вам дійсно не потрібні. Однак, якщо вам потрібно гарантувати існування єдиного екземпляра класу для всієї програми, залиште це на [DI-контейнера |container]. Створіть таким чином аплікаційний singleton, тобто сервіс. Тим самим клас перестане займатися забезпеченням власної унікальності (тобто не матиме методу `getInstance()` та статичної змінної) і виконуватиме лише свої функції. Так він перестане порушувати принцип єдиної відповідальності. - - -Глобальний стан проти тестів ----------------------------- - -При написанні тестів ми припускаємо, що кожен тест є ізольованою одиницею і що до нього не входить жоден зовнішній стан. І жоден стан тести не залишає. Після завершення тесту весь пов'язаний з тестом стан повинен бути автоматично видалений збирачем сміття. Завдяки цьому тести ізольовані. Тому ми можемо запускати тести в будь-якому порядку. - -Однак, якщо присутні глобальні стани/singleton'и, всі ці приємні припущення руйнуються. Стан може входити в тест і виходити з нього. Раптом може мати значення порядок тестів. - -Щоб взагалі мати можливість тестувати singleton'и, розробники часто змушені послаблювати їхні властивості, наприклад, дозволяючи замінити екземпляр іншим. Такі рішення в кращому випадку є хаком, який створює код, що важко підтримувати та розуміти. Кожен тест або метод `tearDown()`, який впливає на будь-який глобальний стан, повинен ці зміни скасувати. - -Глобальний стан — це найбільший головний біль при юніт-тестуванні! - -Як виправити ситуацію? Легко. Не пишіть код, який використовує singleton'и, надавайте перевагу передачі залежностей. Тобто dependency injection. - - -Глобальні константи -------------------- - -Глобальний стан не обмежується лише використанням singleton'ів та статичних змінних, але може стосуватися також глобальних констант. - -Константи, значення яких не приносить нам жодної нової (`M_PI`) або корисної (`PREG_BACKTRACK_LIMIT_ERROR`) інформації, є однозначно в порядку. Навпаки, константи, які служать способом *бездротово* передати інформацію всередину коду, є нічим іншим, як прихованою залежністю. Як, наприклад, `LOG_FILE` у наступному прикладі. Використання константи `FILE_APPEND` є цілком коректним. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -У цьому випадку ми повинні задекларувати параметр у конструкторі класу `Foo`, щоб він став частиною API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Тепер ми можемо передати інформацію про шлях до файлу для логування та легко змінювати її за потребою, що полегшує тестування та підтримку коду. - - -Глобальні функції та статичні методи ------------------------------------- - -Хочемо підкреслити, що саме використання статичних методів та глобальних функцій не є проблематичним. Ми пояснювали, в чому полягає недоцільність використання `DB::insert()` та подібних методів, але завжди йшлося лише про глобальний стан, який зберігається в якійсь статичній змінній. Метод `DB::insert()` вимагає існування статичної змінної, оскільки в ній зберігається підключення до бази даних. Без цієї змінної було б неможливо реалізувати метод. - -Використання детермінованих статичних методів та функцій, таких як `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` та багатьох інших, є цілком сумісним з dependency injection. Ці функції завжди повертають однакові результати для однакових вхідних параметрів і тому є передбачуваними. Вони не використовують жодного глобального стану. - -Однак існують і функції в PHP, які не є детермінованими. До них належить, наприклад, функція `htmlspecialchars()`. Її третій параметр `$encoding`, якщо не вказаний, за замовчуванням має значення конфігураційної опції `ini_get('default_charset')`. Тому рекомендується цей параметр завжди вказувати, щоб уникнути можливої непередбачуваної поведінки функції. Nette це послідовно робить. - -Деякі функції, такі як `strtolower()`, `strtoupper()` та подібні, в недавньому минулому поводилися недетерміновано і залежали від налаштування `setlocale()`. Це спричиняло багато ускладнень, найчастіше при роботі з турецькою мовою. Вона розрізняє малу та велику літеру `I` з крапкою та без крапки. Отже, `strtolower('I')` повертало символ `ı`, а `strtoupper('i')` — символ `İ`, що призводило до того, що програми починали спричиняти низку загадкових помилок. Ця проблема, однак, була усунена в PHP версії 8.2, і функції вже не залежать від локалі. - -Це гарний приклад того, як глобальний стан завдав клопоту тисячам розробників у всьому світі. Рішенням було замінити його на dependency injection. - - -Коли можна використовувати глобальний стан? -------------------------------------------- - -Існують певні специфічні ситуації, коли можна використовувати глобальний стан. Наприклад, при налагодженні коду, коли потрібно вивести значення змінної або виміряти тривалість певної частини програми. У таких випадках, що стосуються тимчасових дій, які пізніше будуть видалені з коду, легітимно використовувати глобально доступний дампер або секундомір. Ці інструменти не є частиною дизайну коду. - -Іншим прикладом є функції для роботи з регулярними виразами `preg_*`, які внутрішньо зберігають скомпільовані регулярні вирази в статичному кеші в пам'яті. Коли ви викликаєте той самий регулярний вираз кілька разів у різних місцях коду, він компілюється лише один раз. Кеш економить продуктивність і водночас є для користувача абсолютно невидимим, тому таке використання можна вважати легітимним. - - -Резюме ------- - -Ми розглянули, чому має сенс: - -1) Видалити всі статичні змінні з коду -2) Декларувати залежності -3) І використовувати dependency injection - -Коли ви продумуєте дизайн коду, пам'ятайте, що кожне `static $foo` становить проблему. Щоб ваш код був середовищем, що поважає DI, необхідно повністю викорінити глобальний стан і замінити його за допомогою dependency injection. - -Під час цього процесу ви, можливо, виявите, що потрібно розділити клас, оскільки він має більше однієї відповідальності. Не бійтеся цього; прагніть до принципу єдиної відповідальності. - -*Я хотів би подякувати Мішкові Хевері, чиї статті, такі як [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], є основою цього розділу.* diff --git a/dependency-injection/uk/introduction.texy b/dependency-injection/uk/introduction.texy deleted file mode 100644 index 81cdcb0fa5..0000000000 --- a/dependency-injection/uk/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Що таке Dependency Injection? -***************************** - -.[perex] -Цей розділ познайомить вас з основними практиками програмування, яких слід дотримуватися під час написання будь-яких застосунків. Це основи, необхідні для написання чистого, зрозумілого та підтримуваного коду. - -Якщо ви засвоїте ці правила і будете їх дотримуватися, Nette допомагатиме вам на кожному кроці. Він вирішуватиме за вас рутинні завдання та забезпечить максимальний комфорт, щоб ви могли зосередитися на самій логіці. - -Принципи, які ми тут покажемо, при цьому досить прості. Вам не потрібно нічого боятися. - - -Пам'ятаєте свою першу програму? -------------------------------- - -Ми не знаємо, якою мовою ви її написали, але якби це була PHP, вона, ймовірно, виглядала б приблизно так: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // виведе 24 -``` - -Кілька тривіальних рядків коду, але в них приховано стільки ключових концепцій. Що існують змінні. Що код ділиться на менші одиниці, якими, наприклад, є функції. Що ми передаємо їм вхідні аргументи, а вони повертають результати. Не вистачає лише умов та циклів. - -Те, що ми передаємо дані у функцію, а вона повертає результат, є цілком зрозумілою концепцією, яка використовується і в інших галузях, наприклад, у математиці. - -Функція має свою сигнатуру, яка складається з її назви, переліку параметрів та їхніх типів, і, нарешті, типу значення, що повертається. Як користувачів, нас цікавить сигнатура, про внутрішню реалізацію нам зазвичай нічого знати не потрібно. - -Тепер уявіть, що сигнатура функції виглядала б так: - -```php -function soucet(float $x): float -``` - -Сума з одним параметром? Це дивно… А як щодо цього? - -```php -function soucet(): float -``` - -Це вже справді дуже дивно, чи не так? Як, напевно, використовується функція? - -```php -echo soucet(); // що вона, ймовірно, виведе? -``` - -Дивлячись на такий код, ми були б спантеличені. Його не зрозумів би не тільки початківець, такий код не зрозуміє і досвідчений програміст. - -Ви думаєте, як би така функція виглядала всередині? Звідки вона візьме доданки? Мабуть, вона *якимось чином* отримала б їх сама, наприклад, так: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -У тілі функції ми виявили приховані зв'язки з іншими глобальними функціями чи статичними методами. Щоб з'ясувати, звідки насправді беруться доданки, нам потрібно шукати далі. - - -Не сюди! --------- - -Дизайн, який ми щойно показали, є сутністю багатьох негативних рис: - -- сигнатура функції вдавала, що не потребує доданків, що нас спантеличувало -- ми взагалі не знаємо, як змусити функцію додати два інші числа -- нам довелося заглянути в код, щоб з'ясувати, звідки вона бере доданки -- ми виявили приховані зв'язки -- для повного розуміння необхідно дослідити і ці зв'язки - -А чи взагалі завданням функції додавання є отримання вхідних даних? Звісно, ні. Її відповідальність - лише саме додавання. - - -Ми не хочемо зустрічатися з таким кодом, і точно не хочемо його писати. Виправлення при цьому просте: повернутися до основ і просто використовувати параметри: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Правило № 1: нехай тобі це передадуть -------------------------------------- - -Найважливіше правило звучить так: **усі дані, які потрібні функції або класу, мають бути передані їм**. - -Замість того, щоб вигадувати приховані способи, за допомогою яких вони могли б якось отримати їх самі, просто передайте параметри. Ви заощадите час, необхідний для вигадування прихованих шляхів, які точно не покращать ваш код. - -Якщо ви завжди і скрізь дотримуватиметеся цього правила, ви на шляху до коду без прихованих зв'язків. До коду, який зрозумілий не лише автору, але й кожному, хто читатиме його після нього. Де все зрозуміло з сигнатур функцій та класів і не потрібно шукати прихованих таємниць у реалізації. - -Ця техніка професійно називається **dependency injection**. А ці дані називаються **залежностями.** При цьому це звичайнісінька передача параметрів, нічого більше. - -.[note] -Будь ласка, не плутайте dependency injection, що є патерном проектування, з „dependency injection container“, що є інструментом, тобто чимось діаметрально іншим. Контейнерам ми приділимо увагу пізніше. - - -Від функцій до класів ---------------------- - -А як із цим пов'язані класи? Клас - це складніша одиниця, ніж проста функція, однак правило №1 діє тут без винятку. Просто існує [більше способів передачі аргументів |passing-dependencies]. Наприклад, досить схоже на випадок з функцією: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Або за допомогою інших методів, чи безпосередньо конструктора: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Обидва приклади повністю відповідають dependency injection. - - -Реальні приклади ----------------- - -У реальному світі ви не будете писати класи для додавання чисел. Перейдемо до прикладів із практики. - -Маємо клас `Article`, що представляє статтю в блозі: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // збережемо статтю в базу даних - } -} -``` - -а використання буде таким: - -```php -$article = new Article; -$article->title = '10 Things You Need to Know About Losing Weight'; -$article->content = 'Every year millions of people in ...'; -$article->save(); -``` - -Метод `save()` зберігає статтю в таблицю бази даних. Реалізувати його за допомогою [Nette Database |database:] буде легко, якби не одна заковика: де `Article` має взяти підключення до бази даних, тобто об'єкт класу `Nette\Database\Connection`? - -Здається, у нас багато варіантів. Він може взяти його звідкись зі статичної змінної. Або успадкувати від класу, який забезпечить підключення до бази даних. Або використати так званий [singleton |global-state#Singleton]. Або так звані facades, які використовуються в Laravel: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - DB::insert( - 'INSERT INTO articles (title, content) VALUES (?, ?)', - [$this->title, $this->content], - ); - } -} -``` - -Чудово, ми вирішили проблему. - -Чи ні? - -Нагадаємо [#правило №1: нехай тобі це передадуть |#Правило 1: нехай тобі це передадуть]: усі залежності, які потрібні класу, мають бути передані йому. Тому що якщо ми порушимо правило, ми ступили на шлях до брудного коду, повного прихованих зв'язків, незрозумілості, і результатом буде застосунок, який буде боляче підтримувати та розвивати. - -Користувач класу `Article` не знає, куди метод `save()` зберігає статтю. У таблицю бази даних? В яку, робочу чи тестову? А як це можна змінити? - -Користувач повинен подивитися, як реалізований метод `save()`, і знайде використання методу `DB::insert()`. Тож він повинен шукати далі, як цей метод отримує підключення до бази даних. А приховані зв'язки можуть утворювати досить довгий ланцюжок. - -У чистому та добре спроектованому коді ніколи не зустрічаються приховані зв'язки, фасади Laravel або статичні змінні. У чистому та добре спроектованому коді передаються аргументи: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ще практичніше, як ми побачимо далі, це буде з конструктором: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Якщо ви досвідчений програміст, можливо, ви думаєте, що `Article` взагалі не повинен мати метод `save()`, він повинен представляти чисто компонент даних, а про збереження повинен дбати окремий репозиторій. Це має сенс. Але цим ми б вийшли далеко за рамки теми, якою є dependency injection, та прагнення наводити прості приклади. - -Якщо ви будете писати клас, який потребує для своєї роботи, наприклад, базу даних, не вигадуйте, звідки її отримати, а нехай вам її передадуть. Наприклад, як параметр конструктора або іншого методу. Визнайте залежності. Визнайте їх в API вашого класу. Ви отримаєте зрозумілий та передбачуваний код. - -А як щодо цього класу, який логує повідомлення про помилки: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Як ви думаєте, чи дотрималися ми [#правило №1: нехай тобі це передадуть |#Правило 1: нехай тобі це передадуть]? - -Не дотрималися. - -Ключову інформацію, тобто каталог із файлом логу, клас *отримує сам* із константи. - -Подивіться на приклад використання: - -```php -$logger = new Logger; -$logger->log('Температура 23 °C'); -$logger->log('Температура 10 °C'); -``` - -Без знання реалізації, чи змогли б ви відповісти на питання, куди записуються повідомлення? Чи спало б вам на думку, що для роботи потрібна константа `LOG_DIR`? А чи змогли б ви створити другий екземпляр, який буде записувати в інше місце? Звісно, ні. - -Давайте виправимо клас: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Клас тепер набагато зрозуміліший, конфігурованіший і, отже, корисніший. - -```php -$logger = new Logger('/шлях/до/логу.txt'); -$logger->log('Температура 15 °C'); -``` - - -Але мене це не цікавить! ------------------------- - -*«Коли я створюю об'єкт Article і викликаю save(), я не хочу займатися базою даних, я просто хочу, щоб він зберігся в ту, яку я налаштував у конфігурації».* - -*«Коли я використовую Logger, я просто хочу, щоб повідомлення записалося, і не хочу думати куди. Нехай використовуються глобальні налаштування».* - -Це слушні зауваження. - -Як приклад, покажемо клас, що розсилає інформаційні бюлетені, який залогує результат: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Електронні листи були надіслані'); - - } catch (Exception $e) { - $logger->log('Сталася помилка під час надсилання'); - throw $e; - } - } -} -``` - -Покращений `Logger`, який більше не використовує константу `LOG_DIR`, вимагає в конструкторі вказати шлях до файлу. Як це вирішити? Клас `NewsletterDistributor` зовсім не цікавить, куди записуються повідомлення, він хоче їх просто записати. - -Рішенням знову є [#правило №1: нехай тобі це передадуть |#Правило 1: нехай тобі це передадуть]: усі дані, які потрібні класу, ми йому передаємо. - -Отже, це означає, що ми передамо шлях до логу через конструктор, який потім використаємо при створенні об'єкта `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ НЕ ТАК! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Не так! Тому що шлях **не належить** до даних, які потрібні класу `NewsletterDistributor`; вони потрібні `Logger`. Ви відчуваєте різницю? Клас `NewsletterDistributor` потребує логер як такий. Тож його ми й передамо: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Електронні листи були надіслані'); - - } catch (Exception $e) { - $this->logger->log('Сталася помилка під час надсилання'); - throw $e; - } - } -} -``` - -Тепер із сигнатур класу `NewsletterDistributor` зрозуміло, що частиною його функціональності є логування. А завдання замінити логер на інший, наприклад, для тестування, є абсолютно тривіальним. Крім того, якщо конструктор класу `Logger` зміниться, це ніяк не вплине на наш клас. - - -Правило № 2: бери те, що твоє ------------------------------ - -Не дозволяйте себе заплутати і не дозволяйте передавати залежності ваших залежностей. Нехай вам передають лише ваші залежності. - -Завдяки цьому код, що використовує інші об'єкти, буде повністю незалежним від змін їхніх конструкторів. Його API буде правдивішим. І головне, буде тривіально замінити ці залежності на інші. - - -Новий член родини ------------------ - -У команді розробників було прийнято рішення створити другий логер, який записує в базу даних. Тож ми створимо клас `DatabaseLogger`. Отже, у нас є два класи, `Logger` і `DatabaseLogger`, один записує у файл, інший у базу даних... вам не здається щось дивним у цій назві? Чи не краще було б перейменувати `Logger` на `FileLogger`? Звісно, так. - -Але зробимо це розумно. Під оригінальною назвою створимо інтерфейс: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -... який обидва логери будуть реалізовувати: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -І завдяки цьому не потрібно буде нічого змінювати в решті коду, де використовується логер. Наприклад, конструктор класу `NewsletterDistributor` все ще буде задоволений тим, що вимагає `Logger` як параметр. І тільки від нас залежатиме, який екземпляр ми йому передамо. - -**Тому ми ніколи не додаємо до назв інтерфейсів суфікс `Interface` або префікс `I`.** Інакше неможливо було б так гарно розвивати код. - - -Х'юстоне, у нас проблема ------------------------- - -Хоча в усьому застосунку ми можемо обійтися єдиним екземпляром логера, чи то файлового, чи то базданного, і просто передавати його скрізь, де щось логується, зовсім інакше у випадку класу `Article`. Адже його екземпляри ми створюємо за потребою, навіть кілька разів. Як впоратися зі зв'язком з базою даних у його конструкторі? - -Як приклад може слугувати контролер, який після надсилання форми має зберегти статтю в базу даних: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Можливе рішення напрошується саме собою: передамо об'єкт бази даних конструктором у `EditController` і використаємо `$article = new Article($this->db)`. - -Так само, як у попередньому випадку з `Logger` та шляхом до файлу, це неправильний підхід. База даних не є залежністю `EditController`, а `Article`. Отже, передача бази даних суперечить [правилу №2: бери те, що твоє |#Правило 2: бери те що твоє]. Коли зміниться конструктор класу `Article` (додасться новий параметр), потрібно буде також змінити код у всіх місцях, де створюються екземпляри. Уфф. - -Х'юстоне, що ти пропонуєш? - - -Правило № 3: доручи це фабриці ------------------------------- - -Скасувавши приховані зв'язки та передаючи всі залежності як аргументи, ми отримали більш конфігуровані та гнучкі класи. А отже, нам потрібно ще щось, що створить і налаштує ці гнучкіші класи. Ми будемо називати це фабриками. - -Правило звучить так: якщо клас має залежності, доручи створення їхніх екземплярів фабриці. - -Фабрики - це розумніша заміна оператора `new` у світі dependency injection. - -.[note] -Будь ласка, не плутайте з патерном проектування *factory method*, який описує специфічний спосіб використання фабрик і не пов'язаний з цією темою. - - -Фабрика -------- - -Фабрика - це метод або клас, який виробляє та конфігурує об'єкти. Клас, що виробляє `Article`, назвемо `ArticleFactory`, і він може виглядати, наприклад, так: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Його використання в контролері буде таким: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // доручаємо фабриці створити об'єкт - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Якщо в цей момент зміниться сигнатура конструктора класу `Article`, єдиною частиною коду, яка повинна на це реагувати, є сама фабрика `ArticleFactory`. Весь інший код, який працює з об'єктами `Article`, наприклад `EditController`, це ніяк не зачепить. - -Можливо, ви зараз стукаєте себе по лобі, чи ми взагалі собі допомогли. Кількість коду зросла, і все це починає виглядати підозріло складно. - -Не хвилюйтеся, незабаром ми дійдемо до DI-контейнера Nette. А він має низку козирів у рукаві, які надзвичайно спрощують створення застосунків, що використовують dependency injection. Наприклад, замість класу `ArticleFactory` достатньо буде [написати лише інтерфейс |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Але ми забігаємо наперед, зачекайте ще :-) - - -Підсумок --------- - -На початку цього розділу ми обіцяли показати вам, як проектувати чистий код. Достатньо класам - -1) [передавати залежності, які їм потрібні |#Правило 1: нехай тобі це передадуть] -2) [і навпаки, не передавати те, що їм безпосередньо не потрібно |#Правило 2: бери те що твоє] -3) [і що об'єкти із залежностями найкраще створювати у фабриках |#Правило 3: доручи це фабриці] - -На перший погляд це може здатися не так, але ці три правила мають далекосяжні наслідки. Вони ведуть до радикально іншого погляду на проектування коду. Чи варте воно того? Програмісти, які відкинули старі звички і почали послідовно використовувати dependency injection, вважають цей крок ключовим моментом у професійному житті. Їм відкрився світ зрозумілих та підтримуваних застосунків. - -А що, якщо код послідовно не використовує dependency injection? Що, якщо він побудований на статичних методах або синглтонах? Чи спричиняє це якісь проблеми? [Спричиняє, і дуже серйозні |global-state]. diff --git a/dependency-injection/uk/nette-container.texy b/dependency-injection/uk/nette-container.texy deleted file mode 100644 index 6959fd7129..0000000000 --- a/dependency-injection/uk/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI - одна з найцікавіших бібліотек Nette. Вона вміє генерувати та автоматично оновлювати скомпільовані DI-контейнери, які є надзвичайно швидкими та дивовижно легко конфігуруються. - -Вигляд сервісів, які має створювати DI-контейнер, ми зазвичай визначаємо за допомогою конфігураційних файлів у [форматі NEON |neon:format]. Контейнер, який ми вручну створили в [попередньому розділі |container], записується так: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Запис дійсно короткий. - -Усі залежності, оголошені в конструкторах класів `ArticleFactory` та `UserController`, Nette DI самостійно виявляє та передає завдяки так званому [autowiring |autowiring], тому в конфігураційному файлі нічого вказувати не потрібно. Тож навіть якщо параметри зміняться, вам не потрібно нічого змінювати в конфігурації. Контейнер Nette автоматично перегенерується. Ви можете зосередитися виключно на розробці застосунку. - -Якщо ми хочемо передавати залежності за допомогою сеттерів, ми використовуємо для цього секцію [setup |services#Setup]. - -Nette DI генерує безпосередньо PHP-код контейнера. Результатом є файл `.php`, який ви можете відкрити та вивчити. Завдяки цьому ви точно бачите, як працює контейнер. Ви також можете налагоджувати його в IDE та виконувати покроково. І головне: згенерований PHP надзвичайно швидкий. - -Nette DI також вміє генерувати код [фабрик |factory] на основі наданого інтерфейсу. Тому замість класу `ArticleFactory` нам достатньо буде створити в застосунку лише інтерфейс: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Повний приклад ви знайдете [на GitHub |https://github.com/nette-examples/di-example-doc]. - - -Самостійне використання ------------------------ - -Впровадження бібліотеки Nette DI в застосунок дуже просте. Спочатку встановимо її за допомогою Composer (бо завантаження zip-архівів тааак застаріло): - -```shell -composer require nette/di -``` - -Наступний код створить екземпляр DI-контейнера відповідно до конфігурації, збереженої у файлі `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Контейнер генерується лише один раз, його код записується в кеш (каталог `__DIR__ . '/temp'`), а при наступних запитах він просто завантажується звідти. - -Для створення та отримання сервісів служать методи `getService()` або `getByType()`. Таким чином ми створимо об'єкт `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Під час розробки корисно активувати режим автоматичного оновлення, коли контейнер автоматично перегенерується, якщо зміниться будь-який клас або конфігураційний файл. Достатньо в конструкторі `ContainerLoader` вказати `true` як другий аргумент. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Використання з фреймворком Nette --------------------------------- - -Як ми показали, використання Nette DI не обмежується застосунками, написаними на Nette Framework, ви можете впровадити його де завгодно за допомогою лише 3 рядків коду. Однак, якщо ви розробляєте застосунки на Nette Framework, конфігурацією та створенням контейнера займається [Bootstrap |application:bootstrapping#Конфігурація DI-контейнера]. diff --git a/dependency-injection/uk/passing-dependencies.texy b/dependency-injection/uk/passing-dependencies.texy deleted file mode 100644 index 6601427616..0000000000 --- a/dependency-injection/uk/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Передача залежностей -******************** - -<div class=perex> - -Аргументи, або в термінології DI «залежності», можна передавати в класи такими основними способами: - -* передача конструктором -* передача методом (так званим сеттером) -* встановленням змінної -* методом, анотацією чи атрибутом *inject* - -</div> - -Тепер розглянемо кожен варіант на конкретних прикладах. - - -Передача конструктором -====================== - -Залежності передаються в момент створення об'єкта як аргументи конструктора: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Ця форма підходить для обов'язкових залежностей, які клас неодмінно потребує для своєї роботи, оскільки без них неможливо створити екземпляр. - -Починаючи з PHP 8.0, ми можемо використовувати коротшу форму запису ([constructor property promotion |https://blog.nette.org/uk/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), яка функціонально еквівалентна: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Починаючи з PHP 8.1, змінну можна позначити прапорцем `readonly`, який оголошує, що вміст змінної більше не зміниться: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI-контейнер автоматично передає залежності конструктору за допомогою [autowiring |autowiring]. Аргументи, які неможливо передати таким чином (наприклад, рядки, числа, логічні значення), [ми записуємо в конфігурації |services#Аргументи]. - - -Пекло конструкторів -------------------- - -Термін *constructor hell* (пекло конструкторів) позначає ситуацію, коли нащадок успадковує від батьківського класу, конструктор якого вимагає залежностей, і водночас нащадок також вимагає залежностей. При цьому він повинен прийняти та передати також батьківські: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ ПЕКЛО КОНСТРУКТОРІВ - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Проблема виникає в той момент, коли ми хочемо змінити конструктор класу `BaseClass`, наприклад, коли додається нова залежність. Тоді необхідно також змінити всі конструктори нащадків. Що перетворює таку модифікацію на пекло. - -Як цьому запобігти? Рішенням є **надавати перевагу [композиції над успадкуванням |faq#Чому композиції надається перевага перед успадкуванням]**. - -Отже, ми спроектуємо код інакше. Ми будемо уникати [абстрактних |nette:introduction-to-object-oriented-programming#Абстрактні класи] `Base*` класів. Замість того, щоб `MyClass` отримував певну функціональність шляхом успадкування від `BaseClass`, він отримає цю функціональність як залежність: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Передача сеттером -================= - -Залежності передаються викликом методу, який зберігає їх у приватній змінній. Звичайною конвенцією іменування цих методів є форма `set*()`, тому їх називають сеттерами, але вони, звісно, можуть називатися будь-як інакше. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Цей спосіб підходить для необов'язкових залежностей, які не є необхідними для роботи класу, оскільки не гарантується, що об'єкт дійсно отримає залежність (тобто, що користувач викличе метод). - -Водночас цей спосіб дозволяє викликати сеттер повторно і таким чином змінювати залежність. Якщо це небажано, ми додамо перевірку в метод, або, починаючи з PHP 8.1, позначимо властивість `$cache` прапорцем `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Залежність вже встановлена'); - } - $this->cache = $cache; - } -} -``` - -Виклик сеттера ми визначаємо в конфігурації DI-контейнера в [ключі setup |services#Setup]. Тут також використовується автоматична передача залежностей за допомогою autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Встановленням змінної -===================== - -Залежності передаються записом безпосередньо в змінну-член: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Цей спосіб вважається недоречним, оскільки змінна-член повинна бути оголошена як `public`. А отже, ми не маємо контролю над тим, що передана залежність буде дійсно зазначеного типу (це було актуально до PHP 7.4), і втрачаємо можливість реагувати на новопризначену залежність власним кодом, наприклад, запобігти подальшій зміні. Водночас змінна стає частиною публічного інтерфейсу класу, що може бути небажаним. - -Встановлення змінної ми визначаємо в конфігурації DI-контейнера в [секції setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Хоча попередні три способи застосовуються загалом у всіх об'єктно-орієнтованих мовах, ін'єкція методом, анотацією чи атрибутом *inject* є специфічною виключно для презентерів у Nette. Про них йдеться в [окремому розділі |best-practices:inject-method-attribute]. - - -Який спосіб обрати? -=================== - -- конструктор підходить для обов'язкових залежностей, які клас неодмінно потребує для своєї роботи -- сеттер, навпаки, підходить для необов'язкових залежностей або залежностей, які можна буде змінювати надалі -- публічні змінні не підходять diff --git a/dependency-injection/uk/services.texy b/dependency-injection/uk/services.texy deleted file mode 100644 index 1db05bb9d8..0000000000 --- a/dependency-injection/uk/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Визначення сервісів -******************* - -.[perex] -Конфігурація - це місце, де ми навчаємо DI-контейнер, як створювати окремі сервіси та як пов'язувати їх з іншими залежностями. Nette надає дуже зрозумілий та елегантний спосіб досягти цього. - -Секція `services` у конфігураційному файлі формату NEON - це місце, де ми визначаємо власні сервіси та їхню конфігурацію. Розглянемо простий приклад визначення сервісу під назвою `database`, який представляє екземпляр класу `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Наведена конфігурація призведе до створення наступного фабричного методу в [DI-контейнері |container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Назви сервісів дозволяють нам посилатися на них в інших частинах конфігураційного файлу у форматі `@назваСервісу`. Якщо немає потреби називати сервіс, ми можемо просто використати маркер списку: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Для отримання сервісу з DI-контейнера ми можемо використати метод `getService()` з назвою сервісу як параметром, або метод `getByType()` з типом сервісу: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Створення сервісу -================= - -Зазвичай ми створюємо сервіс просто шляхом створення екземпляра певного класу. Наприклад: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Якщо нам потрібно розширити конфігурацію додатковими ключами, визначення можна розписати на кілька рядків: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Ключ `create` має псевдонім `factory`, обидва варіанти поширені на практиці. Однак ми рекомендуємо використовувати `create`. - -Аргументи конструктора або методу створення можуть бути альтернативно записані в ключі `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Сервіси не обов'язково створюються лише простим створенням екземпляра класу, вони також можуть бути результатом виклику статичних методів або методів інших сервісів: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Зверніть увагу, що для простоти замість `->` використовується `::`, див. [##виразні засоби]. Будуть згенеровані такі фабричні методи: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI-контейнеру потрібно знати тип створеного сервісу. Якщо ми створюємо сервіс за допомогою методу, який не має вказаного типу повернення, ми повинні явно вказати цей тип у конфігурації: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Аргументи -========= - -Ми передаємо аргументи в конструктор та методи способом, дуже схожим на сам PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Для кращої читабельності ми можемо розписати аргументи на окремі рядки. У такому випадку використання ком є необов'язковим: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Ви також можете назвати аргументи і тоді не турбуватися про їхній порядок: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Якщо ви хочете пропустити деякі аргументи та використати їхнє значення за замовчуванням або підставити сервіс за допомогою [autowiring |autowiring], використовуйте підкреслення: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Як аргументи можна передавати сервіси, використовувати параметри та багато іншого, див. [##виразні засоби]. - - -Setup -===== - -У секції `setup` ми визначаємо методи, які мають викликатися при створенні сервісу. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -У PHP це виглядало б так: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Крім виклику методів, можна також передавати значення у властивості. Також підтримується додавання елемента до масиву, що потрібно записувати в лапках, щоб не конфліктувати з синтаксисом NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Що в PHP-коді виглядало б наступним чином: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Однак у setup можна викликати також статичні методи або методи інших сервісів. Якщо вам потрібно передати поточний сервіс як аргумент, вкажіть його як `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Зверніть увагу, що для простоти замість `->` використовується `::`, див. [##виразні засоби]. Буде згенеровано такий фабричний метод: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Виразні засоби -============== - -Nette DI надає нам надзвичайно багаті виразні засоби, за допомогою яких ми можемо записати майже будь-що. Таким чином, у конфігураційних файлах ми можемо використовувати [параметри |configuration#Параметри]: - -```neon -# параметр -%wwwDir% - -# значення параметра за ключем -%mailer.user% - -# параметр всередині рядка -'%wwwDir%/images' -``` - -Далі створювати об'єкти, викликати методи та функції: - -```neon -# створення об'єкта -DateTime() - -# виклик статичного методу -Collator::create(%locale%) - -# виклик функції PHP -::getenv(DB_USER) -``` - -Посилатися на сервіси або за їхньою назвою, або за типом: - -```neon -# сервіс за назвою -@database - -# сервіс за типом -@Nette\Database\Connection -``` - -Використовувати синтаксис first-class callable: .{data-version:3.2.0} - -```neon -# створення callback, аналог [@user, logout] -@user::logout(...) -``` - -Використовувати константи: - -```neon -# константа класу -FilesystemIterator::SKIP_DOTS - -# глобальну константу отримуємо функцією PHP constant() -::constant(PHP_VERSION) -``` - -Виклики методів можна ланцюгувати так само, як у PHP. Лише для простоти замість `->` використовується `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Ці вирази можна використовувати будь-де, при [створенні сервісів |#Створення сервісу], в [аргументах |#Аргументи], у секції [#setup] або [параметрах |configuration#Параметри]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Спеціальні функції ------------------- - -У конфігураційних файлах ви можете використовувати ці спеціальні функції: - -- `not()` заперечення значення -- `bool()`, `int()`, `float()`, `string()` перетворення типу без втрат -- `typed()` створює масив усіх сервісів зазначеного типу -- `tagged()` створює масив усіх сервісів із заданим тегом - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -На відміну від класичного перетворення типів у PHP, такого як `(int)`, перетворення без втрат викине виняток для нечислових значень. - -Функція `typed()` створює масив усіх сервісів даного типу (класу або інтерфейсу). Вона пропускає сервіси, у яких вимкнено autowiring. Можна вказати кілька типів, розділених комою. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Масив сервісів певного типу ви також можете передавати як аргумент автоматично за допомогою [autowiring |autowiring#Масив сервісів]. - -Функція `tagged()` створює масив усіх сервісів з певним тегом. Тут також можна вказати кілька тегів, розділених комою. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Ключ `autowired` дозволяє впливати на поведінку autowiring для конкретного сервісу. Детальніше див. [розділ про autowiring |autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # сервіс foo виключено з autowiring -``` - - -Lazy-сервіси .{data-version:3.2.4} -================================== - -Lazy loading (ліниве завантаження) - це техніка, яка відкладає створення сервісу до моменту, коли він дійсно потрібен. У глобальній конфігурації можна [увімкнути ліниве створення |configuration#Lazy-сервіси] для всіх сервісів одночасно. Для окремих сервісів ви можете змінити цю поведінку: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Коли сервіс визначено як lazy, при його запиті з DI-контейнера ми отримуємо спеціальний об'єкт-заступник. Він виглядає і поводиться так само, як реальний сервіс, але фактична ініціалізація (виклик конструктора та setup) відбувається лише при першому виклику будь-якого його методу або властивості. - -.[note] -Lazy loading можна використовувати лише для користувацьких класів, а не для внутрішніх класів PHP. Потребує PHP 8.4 або новішої версії. - - -Теги -==== - -Теги служать для додавання додаткової інформації до сервісів. До сервісу можна додати один або кілька тегів: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Теги також можуть містити значення: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Щоб отримати всі сервіси з певними тегами, ви можете використати функцію `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -У DI-контейнері ви можете отримати назви всіх сервісів з певним тегом за допомогою методу `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names - це масив, що містить назву сервісу та значення тегу -// напр. ['foo' => 'monolog.logger.event', ...] -``` - - -Режим Inject -============ - -За допомогою прапорця `inject: true` активується передача залежностей через публічні змінні з анотацією [inject |best-practices:inject-method-attribute#Атрибути Inject] та методи [inject*() |best-practices:inject-method-attribute#Методи inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -За замовчуванням `inject` активовано лише для презентерів. - - -Модифікація сервісів -==================== - -DI-контейнер містить багато сервісів, які були додані за допомогою вбудованого або [користувацького розширення |extensions]. Ви можете змінювати визначення цих сервісів безпосередньо в конфігурації. Наприклад, ви можете змінити клас сервісу `application.application`, який за замовчуванням є `Nette\Application\Application`, на інший: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Прапорець `alteration` є інформативним і вказує, що ми лише модифікуємо існуючий сервіс. - -Ми також можемо доповнити setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -При переписуванні сервісу ми можемо захотіти видалити початкові аргументи, елементи setup або теги, для чого служить `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Якщо ви хочете видалити сервіс, доданий розширенням, ви можете зробити це так: - -```neon -services: - cache.journal: false -``` diff --git a/dibi/cs/@home.texy b/dibi/cs/@home.texy new file mode 100644 index 0000000000..1d51220696 --- /dev/null +++ b/dibi/cs/@home.texy @@ -0,0 +1,726 @@ +Dibi: Šikovná Database Abstraction Library pro PHP +************************************************** + +Nejnovější stabilní verzi Dibi instalujte pomocí [Composer|best-practices:composer] příkazem: + +``` +composer require dibi/dibi +``` + +Přehled verzí najdete na stránce [Releases | https://github.com/dibi/dibi/releases]. + +Vyžaduje PHP 8.2 nebo vyšší. + + +Připojení k databázi +==================== + +Databázové spojení je reprezentováno objektem [Dibi\Connection|api:]: + +```php +$database = new Dibi\Connection([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'table', +]); + +$result = $database->query('SELECT * FROM users'); +``` + +Alternativně můžete používat statický registr `dibi`, který udržuje v globálně dostupném úložišti objekt spojení a nad ním volá všechny funkce: + +```php +dibi::connect([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'test', + 'charset' => 'utf8', +]); + +$result = dibi::query('SELECT * FROM users'); +``` + +V případě chyby připojení se vyhodí `Dibi\Exception`. + + +Dotazy +====== + +Databázové dotazy pokládáme metodou `query()`, která vrací [Dibi\Result |api:Dibi\Result]. Řádky jako objekty [Dibi\Row |api:Dibi\Row]. + +Všechny příklady si můžete zkoušet [online na hřišti |https://repl.it/@DavidGrudl/dibi-playground]. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; +} + +// pole všech řádků +$all = $result->fetchAll(); + +// pole všech řádků, klíčem je 'id' +$all = $result->fetchAssoc('id'); + +// asociativní pole id => name +$pairs = $result->fetchPairs('id', 'name'); + +// počet řádků výsledku, pokud je znám, nebo počet ovlivněných řádků +$count = $result->getRowCount(); +``` + +Metoda fetchAssoc() umí vracet i [složitější asociativní pole |#Výsledek jako asociativní pole]. + +Do dotazu lze velmi snadno přidávat i parametry, všimněte si otazníku: + +```php +$result = $database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active); + +// nebo +$result = $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND active = ?', $active); + +$ids = [10, 20, 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (?)', $ids); +``` + +<div class=warning> +**POZOR, nikdy dotazy neskládejte jako řetězce, vznikla by zranitelnost [SQL injection |https://cs.wikipedia.org/wiki/SQL_injection]** +/-- +$database->query('SELECT * FROM users WHERE id = ' . $id); // ŠPATNĚ!!! +\-- +</div> + +Místo otazníku lze používat i tzv. [#modifikátory]. + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +V případě selhání `query()` vyhodí buď `Dibi\Exception`, nebo některého z potomků: + +- [ConstraintViolationException |api:Dibi\ConstraintViolationException] - porušení nějakého omezení pro tabulku +- [ForeignKeyConstraintViolationException |api:Dibi\ForeignKeyConstraintViolationException] - neplatný cizí klíč +- [NotNullConstraintViolationException |api:Dibi\NotNullConstraintViolationException] - porušení podmínky NOT NULL +- [UniqueConstraintViolationException |api:Dibi\UniqueConstraintViolationException] - koliduje unikátní index + +Dotazy lze pokládat také pomocí zkratek: + +```php +// vrátí asociativní pole id => name, zkratka pro query(...)->fetchPairs() +$pairs = $database->fetchPairs('SELECT id, name FROM users'); + +// vrátí pole všech řádků, zkratka pro query(...)->fetchAll() +$rows = $database->fetchAll('SELECT * FROM users'); + +// vrátí řádek, zkratka pro query(...)->fetch() +$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id); + +// vrátí buňku, zkratka pro query(...)->fetchSingle() +$name = $database->fetchSingle('SELECT name FROM users WHERE id = ?', $id); +``` + + +Modifikátory +============ + +Kromě zástupného symbolu `?` můžeme používat i modifikátory: + +| %s | string +| %sN | string, ale '' se přeloží jako NULL +| %bin | binární data +| %b | boolean +| %i | integer +| %iN | integer, ale 0 se přeloží jako NULL +| %f | float +| %d | datum (očekává DateTime, string nebo UNIX timestamp) +| %dt | datum & čas (očekává DateTime, string nebo UNIX timestamp) +| %n | identifikátor, tedy název tabulky či sloupce +| %N | identifikátor, považuje tečku za běžný znak +| %SQL | SQL - přímo vloží do SQL (alternativou je Dibi\Literal) +| %ex | expanduje pole +| %lmt | speciální - doplní do dotazu LIMIT +| %ofs | speciální - doplní do dotazu OFFSET + +Příklad: + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +Pokud je `$name` `null`, vloží se do SQL příkazu `NULL`. + +Pokud je proměnná pole, modifikátor se aplikuje na všechny jeho prvky a ty se vloží do SQL oddělené čárkami: + +```php +$ids = [10, '20', 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (%i)', $ids); +// SELECT * FROM users WHERE id IN (10, 20, 30) +``` + +Modifikátor `%n` využijete v případě, že název tabulky nebo sloupce je proměnnou. (Pozor, nedovolte uživateli manipulovat s obsahem takové proměnné): + +```php +$table = 'blog.users'; +$column = 'name'; +$result = $database->query('SELECT * FROM %n WHERE %n = ?', $table, $column, $value); +// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim' +``` + +Pro operátor LIKE jsou k dispozici čtyři speciální modifikátory: + +| %like~ | výraz začíná řetězcem +| %~like | výraz končí řetězcem +| %~like~ | výraz obsahuje řetězec +| `%like` | výraz je řetězec + +Hledej jména začínající na určitý řetězec: + +```php +$result = $database->query('SELECT * FROM table WHERE name LIKE %like~', $query); +``` + + +Modifikátory polí +================= + +Parametrem vkládaným do SQL dotazu může být i pole. Tyto modifikátory určují, jak z něj sestavit SQL příkaz: + +| %and | | `key1 = value1 AND key2 = value2 AND ...` +| %or | | `key1 = value1 OR key2 = value2 OR ...` +| %a | assoc | `key1 = value1, key2 = value2, ...` +| %l %in | list | `(val1, val2, ...)` +| %v | values | `(key1, key2, ...) VALUES (value1, value2, ...)` +| %m | multi | `(key1, key2, ...) VALUES (value1, value2, ...), (value1, value2, ...), ...` +| %by | řazení | `key1 ASC, key2 DESC ...` +| %n | názvy | `key1, key2 AS alias, ...` + +Příklad: + +```php +$arr = [ + 'a' => 'hello', + 'b' => true, +]; + +$database->query('INSERT INTO table %v', $arr); +// INSERT INTO `table` (`a`, `b`) VALUES ('hello', 1) + +$database->query('UPDATE `table` SET %a', $arr); +// UPDATE `table` SET `a`='hello', `b`=1 +``` + +V klauzuli WHERE lze použít modifikátory `%and` nebo `%or`: + +```php +$result = $database->query('SELECT * FROM users WHERE %and', [ + 'name' => $name, + 'year' => $year, +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978 +``` + +Viz také [#Složitější dotazy]. + +Modifikátor `%by` slouží k řazení, v klíčích uvedeme sloupce a hodnotou bude boolean určující, zda řadit vzestupně: + +```php +$result = $database->query('SELECT id FROM author ORDER BY %by', [ + 'id' => true, // vzestupně + 'name' => false, // sestupně +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + +Insert, Update & Delete +======================= + +Data vkládáme do SQL dotazu jako asociativní pole. Modifikátory ani zástupný znak `?` není nutné v těchto případech uvádět. + +```php +$database->query('INSERT INTO users', [ + 'name' => $name, + 'year' => $year, +]); +// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) + +$id = $database->getInsertId(); // vrátí auto-increment vloženého záznamu + +$id = $database->getInsertId($sequence); // nebo hodnotu sekvence +``` + +Vícenásobný INSERT: + +```php +$database->query( + 'INSERT INTO users', + [ + 'name' => 'Jim', + 'year' => 1978, + ], + [ + 'name' => 'Jack', + 'year' => 1987, + ] +); +// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987) +``` + +Mazání: + +```php +$database->query('DELETE FROM users WHERE id = ?', $id); + +// vrací počet smazaných řádků +$affectedRows = $database->getAffectedRows(); +``` + +Úprava záznamů: + +```php +$database->query('UPDATE users SET', [ + 'name' => $name, + 'year' => $year, +], 'WHERE id = ?', $id); +// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123 + +// vrací počet změněných řádků +$affectedRows = $database->getAffectedRows(); +``` + +Vložení záznamu, nebo úprava, pokud již existuje: + +```php +$database->query('INSERT INTO users', [ + 'id' => $id, + 'name' => $name, + 'year' => $year, +], 'ON DUPLICATE KEY UPDATE %a', [ // tady už modifikátor %a uvést musíme + 'name' => $name, + 'year' => $year, +]); +// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) +// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 +``` + + +Fluent +====== + +Kromě ručního psaní SQL umí Dibi dotazy skládat přes fluent interface, kdy příkaz sestavujete řetězením metod. Začínáte metodami `select()`, `insert()`, `update()`, `delete()` nebo obecnou `command()` nad spojením: + +```php +$rows = $database->select('id, name') + ->from('users') + ->where('age > ?', $age) + ->orderBy('name') + ->fetchAll(); +// SELECT id, name FROM users WHERE age > 18 ORDER BY name +``` + +Názvy tabulek a sloupců se automaticky escapují, zástupný znak `?` i [modifikátory |#modifikátory] fungují stejně jako v běžném `query()`. + +Vložení záznamu a získání jeho ID: + +```php +$id = $database->insert('users', [ + 'name' => $name, + 'year' => $year, +])->execute(Dibi\Fluent::Identifier); +``` + +Úprava a mazání fungují stejně; předáním `Dibi\Fluent::AffectedRows` metodě `execute()` získáte počet ovlivněných řádků: + +```php +$affected = $database->update('users', ['name' => $name]) + ->where('id = ?', $id) + ->execute(Dibi\Fluent::AffectedRows); + +$database->delete('users') + ->where('id = ?', $id) + ->execute(); +``` + +Pomocí `setFlag()` lze přidat SQL příznak, například pro sestavení `INSERT IGNORE`: + +```php +$database->insert('users', $record) + ->setFlag('IGNORE') + ->execute(); +``` + +Řádky se načítají stejnými metodami jako u [Dibi\Result |#dotazy] - `fetch()`, `fetchSingle()`, `fetchAll()`, `fetchPairs()`, `fetchAssoc()` - nebo iterací přes `foreach`. Chcete-li místo vykonání dotazu jen vypsat vygenerované SQL, zavolejte `test()`. + + +DataSource +========== + +[Dibi\DataSource |api:] obalí tabulku nebo SQL dotaz do objektu, který můžete před samotným vykonáním dále filtrovat, řadit a stránkovat - hodí se pro komponenty typu datagrid. Vytvoříte jej metodou `dataSource()` z názvu tabulky nebo z celého dotazu: + +```php +$ds = $database->dataSource('SELECT id, name, age FROM users'); +``` + +Poté data upřesníte a načtete. Metoda `count()` vrací počet řádků odpovídajících aktuálním podmínkám (a limitu, je-li nastaven), zatímco `getTotalCount()` vrací celkový počet bez ohledu na podmínky a limit: + +```php +$ds->where('age > ?', $age) + ->orderBy('name'); + +$count = $ds->count(); // počet řádků odpovídajících filtru +$rows = $ds->applyLimit(10) // jedna stránka výsledků + ->fetchAll(); +``` + + +Transakce +========= + +Pro práci s transakcemi slouží čtveřice metod: + +```php +$database->beginTransaction(); // zahájení transakce + +$database->commit(); // potvrzení + +$database->rollback(); // vrácení zpět + +$database->transaction(function () { + // nejaka akce +}); +``` + + +Testování +========= + +Abyste si mohli trošku s Dibi hrát, je tu připravena metoda `test()`, které předáte parametry stejně jako `query()`, ovšem místo provedení SQL příkazu se tento barevně vypíše na obrazovku. + +Výsledky dotazu je možné vypsat jako tabulku pomocí `$result->dump()`. + +K dispozici jsou dále proměnné: + +```php +dibi::$sql; // poslední SQL příkaz +dibi::$elapsedTime; // jeho doba trvání v sekundách +dibi::$numOfQueries; // celkem SQL příkazů +dibi::$totalTime; // celkový čas v sekundách +``` + + +Složitější dotazy +================= + +Parametrem může být také objekt `DateTime`. + +```php +$result = $database->query('SELECT * FROM users WHERE created < ?', new DateTime); + +$database->query('INSERT INTO users', [ + 'created' => new DateTime, +]); +``` + +Nebo SQL literál: + +```php +$database->query('UPDATE table SET', [ + 'date' => $database->literal('NOW()'), +]); +// UPDATE table SET `date` = NOW() +``` + +Nebo výraz, ve kterém lze používat zástupné znaky `?` nebo modifikátory: + +```php +$database->query('UPDATE `table` SET', [ + 'title' => $database::expression('SHA1(?)', 'tajne'), +]); +// UPDATE `table` SET `title` = SHA1('tajne') +``` + +Při update lze modifikátory uvádět přímo v klíčích: + +```php +$database->query('UPDATE table SET', [ + 'date%SQL' => 'NOW()', // %SQL znamená SQL ;) +]); +// UPDATE table SET `date` = NOW() +``` + +V podmínkách (tj. u modifikátorů `%and` a `%or`) není nutné uvádět klíče: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + 'number > 10', + 'number < 100', +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) +``` + +V položkách lze používat i modifikátory nebo zástupné znaky: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + ['number > ?', 10], // nebo $database::expression('number > ?', 10) + ['number < ?', 100], + ['%or', [ + 'left' => 1, + 'top' => 2, + ]], +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) AND (`left` = 1 OR `top` = 2) +``` + +Modifikátor `%ex` vloží do SQL všechny prvky pole: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %ex', [ + $database::expression('left = ?', 1), + 'AND', + 'top IS NULL', +]); +// SELECT * FROM `table` WHERE left = 1 AND top IS NULL +``` + + +Podmínky v SQL příkazu +====================== + +Podmíněné SQL příkazy se ovládají pomocí tří modifikátorů `%if`, `%else` a `%end`. První z nich `%if` se musí nacházet zcela na konci řetězce představujícího SQL a za ním následuje proměnná: + +```php +// $user = ???; + +$result = $database->query(' + SELECT * + FROM table + %if', isset($user), 'WHERE user=%s', $user, '%end + ORDER BY name +'); +``` + +Podmínku lze doplnit o část `%else`: + +```php +$result = $database->query(' + SELECT * + FROM %if', $cond, 'one_table %else second_table +'); +``` + +Podmínky můžete zanořovat do sebe. + + +Identifikátory a řetězce v SQL +============================== + +Samotné SQL prochází zpracováním, aby vyhovovalo konvencím dané databáze. Identifikátory (jména tabulek a sloupců) lze uvozovat do hranatých závorek nebo zpětných uvozovek, dále řetězce jednoduchými či dvojitými uvozovkami, nicméně na server se pošle vždy to, co databáze žádá. Příklad: + +```php +$database->query("UPDATE `table` SET [status]='I''m fine'"); +// MySQL: UPDATE `table` SET `status`='I\'m fine' +// ODBC: UPDATE [table] SET [status]='I''m fine' +``` + +Uvozovka se uvnitř řetězce v SQL zapisuje zdvojením. + + +Výsledek jako asociativní pole +============================== + +Příklad: vrátí výsledky jako asociativní pole, kde klíčem bude hodnota políčka `id`: + +```php +$assoc = $result->fetchAssoc('id'); +``` + +Největší síla funkce `fetchAssoc()` se projeví u SQL dotazu spojujícího několik tabulek s různými typy vazeb. Databáze z toho udělá plochou tabulku, fetchAssoc jí vrátí tvar. + +Příklad: Mějme tabulku zákazníků a objednávek (vazba N:M) a položíme dotaz: + +```php +$result = $database->query(' + SELECT customer_id, customers.name, order_id, orders.number, ... + FROM customers + INNER JOIN orders USING (customer_id) + WHERE ... +'); +``` + +A rádi bychom získali vnořené asociativní pole podle ID zákazníka a poté podle ID objednávky: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +// budeme jej procházet takto: +foreach ($all as $customerId => $orders) { + foreach ($orders as $orderId => $order) { + // ... + } +} +``` + +Asociativní deskriptor má obdobnou syntax, jako když pole píšete pomocí přiřazení v PHP. Tedy `'customer_id|order_id'` představuje sérii přiřazení `$all[$customerId][$orderId] = $row;`, postupně pro všechny řádky. + +Někdy by se hodilo, aby se asociovalo podle jména zákazníka namísto jeho ID: + +```php +$all = $result->fetchAssoc('name|order_id'); + +// k prvkům pak přistupujeme třeba takto: +$order = $all['Arnold Rimmer'][$orderId]; +``` + +Co když ale existuje více zákazníků se stejným jménem? Tabulka by měla mít spíš tvar: + +```php +$row = $all['Arnold Rimmer'][0][$orderId]; +$row = $all['Arnold Rimmer'][1][$orderId]; +``` + +Rozlišujeme tedy více možných Rimmerů pomocí klasického pole. Asociativní deskriptor má opět formát podobný přiřazování, s tím, že sekvenční pole představuje `[]`: + +```php +$all = $result->fetchAssoc('name[]order_id'); + +// iterujeme všechny Arnoldy ve výsledcích +foreach ($all['Arnold Rimmer'] as $arnoldOrders) { + foreach ($arnoldOrders as $orderId => $order) { + // ... + } +} +``` + +Vrátíme se k příkladu s deskriptorem `'customer_id|order_id'` a zkusíme vypsat objednávky jednotlivých zákazníků: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +foreach ($all as $customerId => $orders) { + echo "Objednávky zákazníka $customerId:"; + + foreach ($orders as $orderId => $order) { + echo "Číslo dokladu: $order->number"; + // jméno zákazníka je v $order->name + } +} +``` + +Bylo by hezké místo ID zákazníka vypsat jeho jméno. Jenže to bychom museli dohledávat v poli `$orders`. Výsledky si proto necháme upravit do takovéhoto tvaru: + +```php +$all[$customerId]->name = 'John Doe'; +$all[$customerId]->order_id[$orderId] = $row; +$all[$customerId]->order_id[$orderId2] = $row2; +``` + +Tedy mezi `$customerId` a `$orderId` vložíme ještě mezičlánek. Tentokrát ne číslované indexy, jaké jsme použili pro odlišení jednotlivých Rimmerů, ale rovnou databázový záznam. Řešení je velmi podobné, jen si stačí zapamatovat, že záznam symbolizuje šipka: + +```php +$all = $result->fetchAssoc('customer_id->order_id'); + +foreach ($all as $customerId => $row) { + echo "Objednávky zákazníka $row->name:"; + + foreach ($row->order_id as $orderId => $order) { + echo "Číslo dokladu: $order->number"; + } +} +``` + + +Prefixy & substituce +==================== + +Názvy tabulek a sloupců mohou obsahovat proměnné části. Ty si nejprve nadefinujeme: + +```php +// vytvoří novou substituci :blog: ==> wp_ +$database->getSubstitutes()->blog = 'wp_'; +``` + +a poté použijeme v SQL. Všimněte si, že v SQL jsou uvozeny dvojtečkami: + +```php +$database->query("UPDATE [:blog:items] SET [text]='Hello World'"); +// UPDATE `wp_items` SET `text`='Hello World' +``` + + +Datové typy buněk +================= + +Dibi automaticky detekuje typy jednotlivých sloupců dotazu a převádí buňky na nativní typy PHP. Typ můžeme určit i manuálně. Možné typy najdete ve třídě [Dibi\Type |api:Dibi\Type]. + +```php +$result->setType('id', Dibi\Type::INTEGER); // id bude integer +$row = $result->fetch(); + +is_int($row->id) // true +``` + + +Logování +======== + +Dibi má v sobě zabudovaný logger, kterým můžete sledovat všechny vykonané SQL příkazy a měřit délku jejich trvání. Aktivace: + +```php +$database->connect([ + 'driver' => 'sqlite', + 'database' => 'sample.sdb', + 'profiler' => [ + 'file' => 'file.log', + ], +]); +``` + +Šikovnější profiler je panel pro Tracy, který se aktivuje při propojení s Nette. + + +Připojení do [Nette |https://nette.org] +======================================= + +V konfiguračním souboru zaregistrujeme DI rozšíření a přidáme sekci `dibi` - tím se vytvoří potřebné objekty a také databázový panel v [Tracy |https://tracy.nette.org] debugger baru. + +```neon +extensions: + dibi: Dibi\Bridges\Nette\DibiExtension3 + +dibi: + host: localhost + username: root + password: *** + database: foo + lazy: true +``` + +Poté objekt spojení [získáme jako službu z DI kontejneru |https://doc.nette.org/di-usage], např.: + +```php +class Model +{ + private $database; + + public function __construct(Dibi\Connection $database) + { + $this->database = $database; + } +} +``` + + +Komunitní rozšíření +=================== + +Nad Dibi staví nejrůznější knihovny, ORM a rozšíření. Celý jejich seznam najdete na "Packagistu":https://packagist.org/packages/dibi/dibi/dependents?order_by=downloads&requires=require. + +{{maintitle: Dibi – Šikovná Database Abstraction Library pro PHP}} +{{leftbar: no}} diff --git a/dibi/cs/@menu.texy b/dibi/cs/@menu.texy new file mode 100644 index 0000000000..9a7a7c3b48 --- /dev/null +++ b/dibi/cs/@menu.texy @@ -0,0 +1,4 @@ +- [Úvod | @home] +- "Blog .[link-external]":https://phpfashion.com/category/dibi +- "API .[link-external]":https://api.nette.org/dibi/ +- "GitHub .[link-external]":https://github.com/dibi/dibi diff --git a/dibi/cs/@meta.texy b/dibi/cs/@meta.texy new file mode 100644 index 0000000000..49d44d0cfa --- /dev/null +++ b/dibi/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Dibi Dokumentace}} diff --git a/dibi/en/@home.texy b/dibi/en/@home.texy new file mode 100644 index 0000000000..892a4e926a --- /dev/null +++ b/dibi/en/@home.texy @@ -0,0 +1,726 @@ +Dibi: Smart Database Abstraction Library for PHP +************************************************ + +To install the latest stable Dibi version, use the [Composer|best-practices:composer] command: + +``` +composer require dibi/dibi +``` + +You can find a version overview on the [Releases | https://github.com/dibi/dibi/releases] page. + +Requires PHP 8.2 or newer. + + +Connecting to Database +====================== + +The database connection is represented by the [Dibi\Connection|api:] object: + +```php +$database = new Dibi\Connection([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'table', +]); + +$result = $database->query('SELECT * FROM users'); +``` + +Alternatively, you can use the `dibi` static registry, which maintains a connection object in globally accessible storage and calls all functions on it: + +```php +dibi::connect([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'test', + 'charset' => 'utf8', +]); + +$result = dibi::query('SELECT * FROM users'); +``` + +In case of a connection error, it throws `Dibi\Exception`. + + +Queries +======= + +We query the database using the `query()` method, which returns [Dibi\Result |api:Dibi\Result]. Rows are returned as [Dibi\Row |api:Dibi\Row] objects. + +You can try all the examples [online at the playground |https://repl.it/@DavidGrudl/dibi-playground]. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; +} + +// array of all rows +$all = $result->fetchAll(); + +// array of all rows, keyed by 'id' +$all = $result->fetchAssoc('id'); + +// associative pairs id => name +$pairs = $result->fetchPairs('id', 'name'); + +// number of result rows, if known, or number of affected rows +$count = $result->getRowCount(); +``` + +The fetchAssoc() method can return [more complex associative arrays |#Result as associative array]. + +You can easily add parameters to the query - note the question mark: + +```php +$result = $database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active); + +// or +$result = $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND active = ?', $active); + +$ids = [10, 20, 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (?)', $ids); +``` + +<div class=warning> +**WARNING: never concatenate parameters into SQL queries, as this would create an [SQL injection |https://en.wikipedia.org/wiki/SQL_injection] vulnerability** +/-- +$database->query('SELECT * FROM users WHERE id = ' . $id); // BAD!!! +\-- +</div> + +Instead of question marks, you can also use so-called [#modifiers]. + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +In case of failure, `query()` throws either `Dibi\Exception` or one of its descendants: + +- [ConstraintViolationException |api:Dibi\ConstraintViolationException] - violation of some table constraint +- [ForeignKeyConstraintViolationException |api:Dibi\ForeignKeyConstraintViolationException] - invalid foreign key +- [NotNullConstraintViolationException |api:Dibi\NotNullConstraintViolationException] - violation of the NOT NULL condition +- [UniqueConstraintViolationException |api:Dibi\UniqueConstraintViolationException] - collision with unique index + +You can also use shortcut methods: + +```php +// returns associative pairs id => name, shortcut for query(...)->fetchPairs() +$pairs = $database->fetchPairs('SELECT id, name FROM users'); + +// returns array of all rows, shortcut for query(...)->fetchAll() +$rows = $database->fetchAll('SELECT * FROM users'); + +// returns row, shortcut for query(...)->fetch() +$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id); + +// returns cell, shortcut for query(...)->fetchSingle() +$name = $database->fetchSingle('SELECT name FROM users WHERE id = ?', $id); +``` + + +Modifiers +========= + +In addition to the `?` placeholder, we can also use modifiers: + +| %s | string +| %sN | string, but '' translates as NULL +| %bin | binary data +| %b | boolean +| %i | integer +| %iN | integer, but 0 translates as NULL +| %f | float +| %d | date (accepts DateTime, string or UNIX timestamp) +| %dt | datetime (accepts DateTime, string or UNIX timestamp) +| %n | identifier, i.e. table or column name +| %N | identifier, treats period as ordinary character +| %SQL | SQL - directly inserts into SQL (alternative is Dibi\Literal) +| %ex | expands array +| %lmt | special - adds LIMIT to the query +| %ofs | special - adds OFFSET to the query + +Example: + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +If `$name` is `null`, `NULL` is inserted into the SQL statement. + +If the variable is an array, the modifier is applied to all of its elements and they are inserted into SQL separated by commas: + +```php +$ids = [10, '20', 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (%i)', $ids); +// SELECT * FROM users WHERE id IN (10, 20, 30) +``` + +The `%n` modifier is used when the table or column name is a variable. (Beware: do not allow the user to manipulate the content of such a variable): + +```php +$table = 'blog.users'; +$column = 'name'; +$result = $database->query('SELECT * FROM %n WHERE %n = ?', $table, $column, $value); +// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim' +``` + +Four special modifiers are available for the LIKE operator: + +| %like~ | expression starts with string +| %~like | expression ends with string +| %~like~ | expression contains string +| `%like` | expression matches string + +Search for names starting with a certain string: + +```php +$result = $database->query('SELECT * FROM table WHERE name LIKE %like~', $query); +``` + + +Array Modifiers +=============== + +The parameter inserted into an SQL query can also be an array. These modifiers determine how to construct the SQL statement from it: + +| %and | | `key1 = value1 AND key2 = value2 AND ...` +| %or | | `key1 = value1 OR key2 = value2 OR ...` +| %a | assoc | `key1 = value1, key2 = value2, ...` +| %l %in | list | `(val1, val2, ...)` +| %v | values | `(key1, key2, ...) VALUES (value1, value2, ...)` +| %m | multi | `(key1, key2, ...) VALUES (value1, value2, ...), (value1, value2, ...), ...` +| %by | ordering | `key1 ASC, key2 DESC ...` +| %n | names | `key1, key2 AS alias, ...` + +Example: + +```php +$arr = [ + 'a' => 'hello', + 'b' => true, +]; + +$database->query('INSERT INTO table %v', $arr); +// INSERT INTO `table` (`a`, `b`) VALUES ('hello', 1) + +$database->query('UPDATE `table` SET %a', $arr); +// UPDATE `table` SET `a`='hello', `b`=1 +``` + +In the WHERE clause, you can use `%and` or `%or` modifiers: + +```php +$result = $database->query('SELECT * FROM users WHERE %and', [ + 'name' => $name, + 'year' => $year, +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978 +``` + +See also [#Complex queries]. + +The `%by` modifier is used for sorting - keys specify the columns, and the boolean value determines whether to sort in ascending order: + +```php +$result = $database->query('SELECT id FROM author ORDER BY %by', [ + 'id' => true, // ascending + 'name' => false, // descending +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + +Insert, Update & Delete +======================= + +We insert data into SQL queries as associative arrays. Modifiers and the `?` placeholder are not necessary in these cases. + +```php +$database->query('INSERT INTO users', [ + 'name' => $name, + 'year' => $year, +]); +// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) + +$id = $database->getInsertId(); // returns the auto-increment of the inserted record + +$id = $database->getInsertId($sequence); // or sequence value +``` + +Multiple INSERT: + +```php +$database->query( + 'INSERT INTO users', + [ + 'name' => 'Jim', + 'year' => 1978, + ], + [ + 'name' => 'Jack', + 'year' => 1987, + ] +); +// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987) +``` + +Deleting: + +```php +$database->query('DELETE FROM users WHERE id = ?', $id); + +// returns number of deleted rows +$affectedRows = $database->getAffectedRows(); +``` + +Updating records: + +```php +$database->query('UPDATE users SET', [ + 'name' => $name, + 'year' => $year, +], 'WHERE id = ?', $id); +// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123 + +// returns the number of updated rows +$affectedRows = $database->getAffectedRows(); +``` + +Substitute any identifier: + +```php +$database->query('INSERT INTO users', [ + 'id' => $id, + 'name' => $name, + 'year' => $year, +], 'ON DUPLICATE KEY UPDATE %a', [ // here the modifier %a must be used + 'name' => $name, + 'year' => $year, +]); +// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) +// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 +``` + + +Fluent +====== + +Besides writing SQL by hand, Dibi can build queries through a fluent interface, where you assemble the statement by chaining methods. You start with the `select()`, `insert()`, `update()`, `delete()`, or the generic `command()` methods on the connection: + +```php +$rows = $database->select('id, name') + ->from('users') + ->where('age > ?', $age) + ->orderBy('name') + ->fetchAll(); +// SELECT id, name FROM users WHERE age > 18 ORDER BY name +``` + +Table and column names are escaped automatically, and the `?` placeholder and [modifiers |#modifiers] work just like in a regular `query()`. + +Inserting a record and getting its ID: + +```php +$id = $database->insert('users', [ + 'name' => $name, + 'year' => $year, +])->execute(Dibi\Fluent::Identifier); +``` + +Updating and deleting work the same way; pass `Dibi\Fluent::AffectedRows` to `execute()` to get the number of affected rows: + +```php +$affected = $database->update('users', ['name' => $name]) + ->where('id = ?', $id) + ->execute(Dibi\Fluent::AffectedRows); + +$database->delete('users') + ->where('id = ?', $id) + ->execute(); +``` + +You can add an SQL flag with `setFlag()`, for example to build `INSERT IGNORE`: + +```php +$database->insert('users', $record) + ->setFlag('IGNORE') + ->execute(); +``` + +The rows are fetched with the same methods as [Dibi\Result |#queries] - `fetch()`, `fetchSingle()`, `fetchAll()`, `fetchPairs()`, `fetchAssoc()` - or by iterating with `foreach`. To only print the generated SQL instead of running the query, call `test()`. + + +DataSource +========== + +[Dibi\DataSource |api:] wraps a table or an SQL query in an object that you can further filter, sort, and paginate before it is actually executed - handy for components such as data grids. You create it with `dataSource()` from a table name or a whole query: + +```php +$ds = $database->dataSource('SELECT id, name, age FROM users'); +``` + +Then you refine the data and read it. The `count()` method returns the number of rows matching the current conditions (and limit, if set), while `getTotalCount()` returns the total number ignoring conditions and limit: + +```php +$ds->where('age > ?', $age) + ->orderBy('name'); + +$count = $ds->count(); // number of rows matching the filter +$rows = $ds->applyLimit(10) // one page of results + ->fetchAll(); +``` + + +Transaction +=========== + +There are four methods for dealing with transactions: + +```php +$database->beginTransaction(); + +$database->commit(); + +$database->rollback(); + +$database->transaction(function () { + // some action +}); +``` + + +Testing +======= + +In order to play with Dibi a little, there is a `test()` method that takes the same parameters as `query()`, but instead of executing the SQL statement, it echoes it on the screen. + +The query results can be echoed as a table using `$result->dump()`. + +These variables are also available: + +```php +dibi::$sql; // the latest SQL query +dibi::$elapsedTime; // its duration in sec +dibi::$numOfQueries; +dibi::$totalTime; +``` + + +Complex Queries +=============== + +The parameter may also be a `DateTime` object. + +```php +$result = $database->query('SELECT * FROM users WHERE created < ?', new DateTime); + +$database->query('INSERT INTO users', [ + 'created' => new DateTime, +]); +``` + +Or an SQL literal: + +```php +$database->query('UPDATE table SET', [ + 'date' => $database->literal('NOW()'), +]); +// UPDATE table SET `date` = NOW() +``` + +Or an expression in which you can use `?` or modifiers: + +```php +$database->query('UPDATE `table` SET', [ + 'title' => $database::expression('SHA1(?)', 'secret'), +]); +// UPDATE `table` SET `title` = SHA1('secret') +``` + +When updating, modifiers can be placed directly in the keys: + +```php +$database->query('UPDATE table SET', [ + 'date%SQL' => 'NOW()', // %SQL means SQL ;) +]); +// UPDATE table SET `date` = NOW() +``` + +In conditions (i.e., for the `%and` and `%or` modifiers), it is not necessary to specify the keys: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + 'number > 10', + 'number < 100', +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) +``` + +Modifiers or placeholders can also be used in expressions: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + ['number > ?', 10], // or $database::expression('number > ?', 10) + ['number < ?', 100], + ['%or', [ + 'left' => 1, + 'top' => 2, + ]], +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) AND (`left` = 1 OR `top` = 2) +``` + +The `%ex` modifier inserts all items of the array into SQL: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %ex', [ + $database::expression('left = ?', 1), + 'AND', + 'top IS NULL', +]); +// SELECT * FROM `table` WHERE left = 1 AND top IS NULL +``` + + +Conditions in SQL Statements +============================ + +Conditional SQL statements are controlled by three modifiers: `%if`, `%else`, and `%end`. The `%if` must be at the end of the string representing SQL and is followed by a variable: + +```php +// $user = ???; + +$result = $database->query(' + SELECT * + FROM table + %if', isset($user), 'WHERE user=%s', $user, '%end + ORDER BY name +'); +``` + +The condition can be supplemented with an `%else` section: + +```php +$result = $database->query(' + SELECT * + FROM %if', $cond, 'one_table %else second_table +'); +``` + +Conditions can be nested within each other. + + +Identifiers and Strings in SQL +============================== + +SQL itself goes through processing to meet the conventions of the given database. Identifiers (table and column names) can be enclosed in square brackets or backticks, and strings in single or double quotes, but the server is always sent what the database requires. Example: + +```php +$database->query("UPDATE `table` SET [status]='I''m fine'"); +// MySQL: UPDATE `table` SET `status`='I\'m fine' +// ODBC: UPDATE [table] SET [status]='I''m fine' +``` + +Quotes inside strings in SQL are written by doubling them. + + +Result as Associative Array +=========================== + +Example: returns results as an associative array where the key will be the value of the `id` field: + +```php +$assoc = $result->fetchAssoc('id'); +``` + +The greatest power of `fetchAssoc()` is demonstrated in SQL queries joining several tables with different types of relationships. The database creates a flat table; fetchAssoc restores the shape. + +Example: Let's have a customer and order table (N:M relationship) and query: + +```php +$result = $database->query(' + SELECT customer_id, customers.name, order_id, orders.number, ... + FROM customers + INNER JOIN orders USING (customer_id) + WHERE ... +'); +``` + +And we'd like to get a nested associative array by Customer ID and then by Order ID: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +// we will iterate like this: +foreach ($all as $customerId => $orders) { + foreach ($orders as $orderId => $order) { + // ... + } +} +``` + +The associative descriptor has a similar syntax to when you write arrays using assignment in PHP. Thus `'customer_id|order_id'` represents the assignment series `$all[$customerId][$orderId] = $row;` sequentially for all rows. + +Sometimes it would be useful to associate by the customer's name instead of their ID: + +```php +$all = $result->fetchAssoc('name|order_id'); + +// elements are then accessed like this: +$order = $all['Arnold Rimmer'][$orderId]; +``` + +But what if there are multiple customers with the same name? The table should have the form: + +```php +$row = $all['Arnold Rimmer'][0][$orderId]; +$row = $all['Arnold Rimmer'][1][$orderId]; +``` + +So we distinguish multiple possible Rimmers using a regular array. The associative descriptor again has a format similar to assignment, with sequential arrays represented by `[]`: + +```php +$all = $result->fetchAssoc('name[]order_id'); + +// we iterate all Arnolds in the results +foreach ($all['Arnold Rimmer'] as $arnoldOrders) { + foreach ($arnoldOrders as $orderId => $order) { + // ... + } +} +``` + +Returning to the example with the `customer_id|order_id` descriptor, let's try to list orders for each customer: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +foreach ($all as $customerId => $orders) { + echo "Orders for customer $customerId:"; + + foreach ($orders as $orderId => $order) { + echo "Document number: $order->number"; + // customer name is in $order->name + } +} +``` + +It would be nice to display the customer name instead of ID. But we would have to look it up in the `$orders` array. So let's modify the results to have this shape: + +```php +$all[$customerId]->name = 'John Doe'; +$all[$customerId]->order_id[$orderId] = $row; +$all[$customerId]->order_id[$orderId2] = $row2; +``` + +So, between `$customerId` and `$orderId`, we insert an intermediate element. This time not the numbered indexes we used to distinguish individual Rimmers, but a database record directly. The solution is very similar - just remember that a record is symbolized by an arrow: + +```php +$all = $result->fetchAssoc('customer_id->order_id'); + +foreach ($all as $customerId => $row) { + echo "Orders for customer $row->name:"; + + foreach ($row->order_id as $orderId => $order) { + echo "Document number: $order->number"; + } +} +``` + + +Prefixes & Substitutions +======================== + +Table and column names can contain variable parts. You will first define them: + +```php +// create new substitution :blog: ==> wp_ +$database->getSubstitutes()->blog = 'wp_'; +``` + +and then use them in SQL. Note that in SQL they are enclosed in colons: + +```php +$database->query("UPDATE [:blog:items] SET [text]='Hello World'"); +// UPDATE `wp_items` SET `text`='Hello World' +``` + + +Field Data Types +================ + +Dibi automatically detects the types of individual query columns and converts cells to native PHP types. We can also specify the type manually. Possible types can be found in the [Dibi\Type |api:Dibi\Type] class. + +```php +$result->setType('id', Dibi\Type::INTEGER); // id will be integer +$row = $result->fetch(); + +is_int($row->id) // true +``` + + +Logging +======= + +Dibi has a built-in logger that lets you track all executed SQL statements and measure the duration of their execution. Activation: + +```php +$database->connect([ + 'driver' => 'sqlite', + 'database' => 'sample.sdb', + 'profiler' => [ + 'file' => 'file.log', + ], +]); +``` + +A more versatile profiler is the Tracy panel, which is activated when connecting to Nette. + + +Connect to [Nette |https://nette.org] +===================================== + +In the configuration file, we register the DI extension and add the `dibi` section - this creates the required objects and also the database panel in the [Tracy |https://tracy.nette.org] debugger bar. + +```neon +extensions: + dibi: Dibi\Bridges\Nette\DibiExtension3 + +dibi: + host: localhost + username: root + password: *** + database: foo + lazy: true +``` + +Then the connection object can be [obtained as a service from the DI container |https://doc.nette.org/di-usage], e.g.: + +```php +class Model +{ + private $database; + + public function __construct(Dibi\Connection $database) + { + $this->database = $database; + } +} +``` + + +Community Extensions +==================== + +Various libraries, ORMs and extensions are built on top of Dibi. You can find a complete list of them on "Packagist":https://packagist.org/packages/dibi/dibi/dependents?order_by=downloads&requires=require. + +{{maintitle: Dibi – Smart Database Abstraction Library for PHP}} +{{leftbar: no}} diff --git a/dibi/en/@menu.texy b/dibi/en/@menu.texy new file mode 100644 index 0000000000..ba2e4587aa --- /dev/null +++ b/dibi/en/@menu.texy @@ -0,0 +1,3 @@ +- [Home | @home] +- "API .[link-external]":https://api.nette.org/dibi/ +- "GitHub .[link-external]":https://github.com/dibi/dibi diff --git a/dibi/en/@meta.texy b/dibi/en/@meta.texy new file mode 100644 index 0000000000..b9ca163d2f --- /dev/null +++ b/dibi/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Dibi Documentation}} diff --git a/dibi/meta.json b/dibi/meta.json new file mode 100644 index 0000000000..73dd6f31ab --- /dev/null +++ b/dibi/meta.json @@ -0,0 +1,6 @@ +{ + "version": "5.x", + "repo": "dibi/dibi", + "composer": "dibi/dibi", + "api": "https://api.nette.org/dibi/" +} diff --git a/dresscode/cs/@home.texy b/dresscode/cs/@home.texy new file mode 100644 index 0000000000..8deec46fb1 --- /dev/null +++ b/dresscode/cs/@home.texy @@ -0,0 +1,38 @@ +DressCode +********* + +{{maintitle: DressCode: kontrola a oprava stylu PHP kódu}} +{{description: DressCode je nástroj pro kontrolu a automatickou opravu stylu PHP kódu. Stojí na bezztrátovém syntaktickém stromu, takže opraví jen to, na co sáhne. Podporuje PER Coding Style 3.1, PSR-12 i Nette Coding Standard.}} + +.[perex] +DressCode hlídá styl vašeho PHP kódu a většinu nálezů rovnou opraví. Pracuje nad bezztrátovým syntaktickým stromem, takže změní jen to, na co sáhne, a zbytek souboru zůstane bajt po bajtu stejný. Vlastní názor na styl nemá, řídí se presetem: [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/], PSR-12 nebo Nette Coding Standard. + +Léta jsem používal PHP CS Fixer i PHP_CodeSniffer a bral je jako hotovou věc. Pak jsem potřeboval jedno nové pravidlo a zjistil, kolik práce dá napsat ho, když nástroj vidí jen ploché pole tokenů a strukturu kódu si musí domýšlet. DressCode vznikl proto, aby se pravidlo dalo napsat za odpoledne a aby se dalo věřit tomu, co opraví. + + +Kudy dál +-------- +- [Začínáme s DressCode |getting-started] instalace, první kontrola a první oprava. +- [Přechod na DressCode |migration] pokud dnes používáte PHP CS Fixer, PHP_CodeSniffer nebo Slevomat. +- [Jak napsat vlastní pravidlo |custom-rule] pokud vám v katalogu něco chybí. + + +Používání +--------- +- [Jak DressCode funguje |how-it-works] strom místo tokenů, pravidla, presety, průchody a slovníček pojmů. +- [Konfigurace |configuration] soubor v NEONu nebo v PHP, presety, pravidla a jejich volby, cesty. +- [Potlačení pravidel a baseline |suppressing] výjimky na řádku, v souboru a v celém projektu. +- [Příkazová řádka |cli] příkazy, přepínače, formáty výstupu, exit kódy. + + +Reference +--------- +- [Přehled pravidel |rules/@home] všechna vestavěná pravidla s příklady a volbami. +- [Presety |presets-reference] co přesně zapíná PER, PSR-12 a Nette. + + +Rozšíření a integrace +--------------------- +- [Rozšiřování DressCode |extending], [pravidlo do detailu |rule-contract], [pravidla pro bílé znaky |whitespace-rules], [testování pravidel |testing-rules]. +- [Editory a IDE |editors], [průběžná integrace |continuous-integration], [Git hooky a agenti |hooks]. +- [PhpSyntax |phpsyntax:] bezztrátový syntaktický strom, na kterém DressCode stojí, jako samostatná knihovna. diff --git a/dresscode/cs/@left-menu.texy b/dresscode/cs/@left-menu.texy new file mode 100644 index 0000000000..ac3a2482a7 --- /dev/null +++ b/dresscode/cs/@left-menu.texy @@ -0,0 +1,37 @@ +- [Úvod |@home] +- [Začínáme |getting-started] +- [Jak DressCode funguje |how-it-works] + +- Použití + - [Konfigurace |configuration] + - [Potlačení a baseline |suppressing] + - [Příkazová řádka |cli] + - [Řešení potíží |troubleshooting] + +- Reference + - [Přehled pravidel |rules/@home] + - [Presety |presets-reference] + +- Rozšíření + - [Rozšiřování DressCode |extending] + - [Vlastní pravidlo |custom-rule] + - [Pravidlo do detailu |rule-contract] + - [Pravidla pro bílé znaky |whitespace-rules] + - [Testování pravidel |testing-rules] + - [Preset a rozšíření |presets-and-extensions] + - [Přepis pravidla odjinud |porting-rules] + - [PHP API |php-api] + +- Integrace + - [Editory a IDE |editors] + - [Průběžná integrace |continuous-integration] + - [Git hooky a agenti |hooks] + +- Migrace + - [Přechod na DressCode |migration] + - [Z PHP CS Fixeru |from-php-cs-fixer] + - [Z PHP_CodeSniffer a Slevomatu |from-phpcs] + - [Z Nette Coding Standardu |from-ncs] + +- Pod kapotou + - [PhpSyntax |phpsyntax:] strom, na kterém DressCode stojí diff --git a/dresscode/cs/@meta.texy b/dresscode/cs/@meta.texy new file mode 100644 index 0000000000..e4568539d8 --- /dev/null +++ b/dresscode/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: DressCode}} diff --git a/dresscode/cs/cli.texy b/dresscode/cs/cli.texy new file mode 100644 index 0000000000..0f67129e8b --- /dev/null +++ b/dresscode/cs/cli.texy @@ -0,0 +1,101 @@ +Příkazová řádka +*************** + +.[perex] +Příkazy `check` a `fix`, výpis pravidel, převod cizí konfigurace, všechny přepínače, formáty výstupu pro terminál i pro CI, exit kódy, cache a paralelní běh. + + +Příkazy +======= + +```shell +dresscode check [cesty...] +dresscode fix [cesty...] +dresscode rules +dresscode import <soubor> +dresscode migrate-suppressions [cesty...] +dresscode lsp +``` + +- `check` ohlásí porušení a nic nezapíše. +- `fix` opraví, co pravidla umějí, a zbytek ohlásí. Soubor se zapíše jen tehdy, když se změnil a výsledek se znovu naparsuje na totéž; soubor se syntaktickou chybou nebo s pravidlem, které selhalo, zůstane nedotčený. +- `rules` vypíše všechna známá pravidla: hvězdičkou označí ta, která v aktuální konfiguraci platí, a u každého uvede fázi, popis a jména pravidel jiných nástrojů, která pokrývá. Hodí se, když hledáte, jak se co jmenuje. +- `import` převede konfiguraci PHP CS Fixeru nebo PHP_CodeSniffer, viz [Přechod na DressCode |migration#2. Přeložte konfiguraci]. +- `migrate-suppressions` přepíše komentáře `phpcs:*` na `dresscode:*`, viz [tamtéž |migration#3. Přepište komentáře]. +- `lsp` spustí jazykový server pro [editory |editors]. + +Cesty jsou soubory nebo adresáře relativně k aktuálnímu adresáři a mají přednost před klíčem `paths` z konfigurace. Bez cest i bez konfiguračního souboru příkaz skončí chybou, protože nechce hádat, co má kontrolovat. + + +Přepínače +========= + +| přepínač | význam | +|---|---| +| `-c`, `--config <soubor>` | konfigurační soubor místo nejbližšího `dresscode.neon` nebo `dresscode.php` | +| `-f`, `--format <název>` | formát výstupu, viz níže | +| `--diff` | u `check` ukáže, co by `fix` změnil; u `fix` to, co změnil | +| `--preset <název>` | přidá preset; lze uvést vícekrát | +| `--rule <název>=on` nebo `=off` | zapne nebo vypne pravidlo pro tenhle běh; lze uvést vícekrát | +| `--stdin <cesta>` | čte kód ze standardního vstupu, jako by to byl soubor na dané cestě; `fix` pak opravený kód vypíše na standardní výstup | +| `--generate-baseline` | zapíše nalezená porušení do [baseline |suppressing#Baseline] místo hlášení; jen u `check` | +| `--no-cache` | zpracuje každý soubor, i ten, o kterém se ví, že je čistý | +| `--jobs <n>` | počet pracovních procesů; `1` znamená běh v jediném procesu | +| `--strict-rules` | pravidlo, které poruší svůj kontrakt, je chyba, ne varování; pro vývoj vlastních pravidel | +| `--no-color` | výstup bez barev | +| `--version`, `--help` | verze, nápověda | + + +Formáty výstupu +=============== + +Přepínač `--format` volí celý tvar výstupu, ne jeho detail; formáty se nekombinují. + +`console` je výchozí volba pro terminál: hlavička s konfigurací, cílovou verzí PHP a rozsahem kontroly, pak jednotlivé soubory s porušeními (řádek, sloupec, zpráva, pravidlo) a nakonec shrnutí. Barvy se vypnou samy, jakmile výstup nejde do terminálu. + +`github` se zvolí sám, když běh probíhá jako krok GitHub Actions. Každé porušení je anotace, která se ukáže přímo v diffu pull requestu. + +`bare` je pro Git hooky a nástroje, které výstup čtou: bez hlavičky a bez shrnutí, jen porušení, která zůstala na uživateli, ve stejném rozvržení jako `console`, a u každého přepsaného souboru řádek `rewritten`. Čistý běh nevypíše vůbec nic. + +/--pre .[terminal] +src/Cart.php rewritten + +src/Order.php + error 12:1 The line is 133 characters long, the limit is 120 line-length +\-- + +`json` je strojově čitelný a jeho tvar se v minoritních verzích nemění: pole `files` s porušeními (pravidlo, zpráva, řádek, sloupec, závažnost, zda je opravitelné, otisk), `summary` s počty a `warnings`. + +`checkstyle` je XML, kterému rozumí Jenkins, nástroj `cs2pr` a další nástroje pro CI. + + +Exit kódy +========= + +| kód | význam | +|---|---| +| `0` | čisto; u `fix` také tehdy, když všechno opravil | +| `1` | zůstala porušení, nebo soubor nejde parsovat | +| `2` | selhání: špatná konfigurace, neznámé pravidlo, pravidlo, které vyhodilo výjimku nebo se s jiným zacyklilo | + +Selhání jednoho souboru běh nezastaví: soubor se ohlásí jako selhavší, nic se do něj nezapíše a ostatní se zpracují dál. + + +Cache a paralelní běh +===================== + +DressCode si pamatuje otisk obsahu každého souboru, který prošel čistě, a spolu s ním otisk konfigurace, cílové verze PHP a verzí nainstalovaných balíčků. Soubor se stejným obsahem a stejnou konfigurací příště přeskočí; jakmile se kterákoli z těch věcí změní, zpracuje ho znovu. Cache leží v systémovém dočasném adresáři, nebo tam, kam ukazuje `cacheDir` v konfiguraci; `--no-cache` ji pro jeden běh obejde. + +Soubory, které cache nepokryje, se rozdělí mezi pracovní procesy. Výchozí počet odpovídá počtu procesorů, nejvýš však jeden proces na čtyři soubory, protože spuštění procesu něco stojí. `--jobs 1` běží bez nich, což se hodí při ladění vlastního pravidla, a `--jobs 8` se vyplatí na stroji s mnoha jádry, kde by výchozí odhad byl zbytečně nízký. + + +Standardní vstup +================ + +`--stdin` je rozhraní pro editory a hooky: obsah přijde na standardním vstupu, cesta říká, která pravidla pro něj platí (podle ní se vyhodnotí výjimky pro cesty), a `fix` vrátí opravený kód na standardní výstup místo toho, aby zapisoval do souboru: + +```shell +git show :src/Cart.php | dresscode check --stdin src/Cart.php +``` + +Cache se u standardního vstupu nepoužívá. diff --git a/dresscode/cs/configuration.texy b/dresscode/cs/configuration.texy new file mode 100644 index 0000000000..781bf7cfe6 --- /dev/null +++ b/dresscode/cs/configuration.texy @@ -0,0 +1,159 @@ +Konfigurace +*********** + +.[perex] +Konfigurace je jeden soubor v kořeni projektu: buď `dresscode.neon`, tedy [NEON |neon:] známý z PHPStanu a se schválně podobnými klíči, nebo `dresscode.php`, pokud dáváte přednost PHP. Řekne se v něm, co se kontroluje, podle jakého presetu a která pravidla mají jinou volbu. + + +Konfigurační soubor +=================== + +Oba formáty umějí totéž: každý klíč NEONu odpovídá jedné metodě třídy `DressCode\Config`. Jediné, co se do NEONu nevejde, jsou anonymní funkce (closures), a ty patří do PHP nebo do rozšíření. Následující dva soubory dělají přesně totéž: + +```neon +presets: + - dresscode/per + +rules: + dresscode/ordered-imports: true + dresscode/line-length: + limit: 100 + +paths: + - src + - tests + +excludePaths: + - tests/fixtures +``` + +```php +<?php declare(strict_types=1); + +use DressCode\Config; + +return Config::create() + ->preset('dresscode/per') + ->enable('dresscode/ordered-imports') + ->enable('dresscode/line-length', ['limit' => 100]) + ->paths(['src', 'tests']) + ->excludePaths(['tests/fixtures']); +``` + +Dál na téhle stránce píšeme NEON, protože se čte lépe; převod do PHP je vždy mechanický. + +Soubor se hledá od aktuálního adresáře směrem nahoru a kořenem projektu se stane adresář, ve kterém se našel. Všechny cesty v konfiguraci jsou relativní k němu. Vedle něj může ležet šablona `dresscode.neon.dist` (nebo `.php.dist`), kterou commitujete; soubor bez `.dist` ji pak celou nahradí a hodí se pro místní odchylku, kterou commitovat nechcete. Mít vedle sebe NEON i PHP je chyba, ne přednost jednoho z nich. Jiný soubor vnutíte přepínačem `--config`. + +Překlep v klíči je chyba při načtení, ne tiše přeskočený řádek. Bez konfiguračního souboru platí preset `dresscode/per`, cesty se zadávají na příkazové řádce a jiný preset se vybere přepínačem `--preset`. + + +Presety a vrstvy +================ + +Preset je pojmenovaná sada pravidel s volbami, dohromady jeden coding standard. Vestavěné jsou tři: `dresscode/psr12`, `dresscode/per` podle [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/] (potomek PSR-12 a výchozí volba) a `dresscode/nette`. Co přesně každý z nich zapíná, ukazuje [přehled presetů |presets-reference]. + +```neon +presets: + - dresscode/nette +``` + +Nastavení se skládá z vrstev a vyšší vrstva přepisuje nižší tam, kde něco nastavila: + +1. výchozí hodnoty, +2. rozšíření (extensions) v pořadí zápisu, +3. presety v pořadí zápisu, přičemž potomek přebírá rodiče, +4. klíče vašeho konfiguračního souboru, +5. příkazová řádka (`--preset`, `--rule`). + +Přepisuje se vždy celá položka, nikdy se neslučuje: když preset nastaví pravidlu `dresscode/braces-position` osm voleb a vy uvedete jednu, platí ta vaše a zbylých sedm se vrátí na výchozí hodnoty pravidla, ne na hodnoty presetu. Je to méně pohodlné než hloubkové slučování, zato nikdy nemusíte hádat, co všechno se vám do konfigurace přimíchalo odjinud. + +**Rozšíření** (extension) je balíček, který se do konfigurace přihlásí, podobně jako u PHPStanu: zaregistruje vlastní pravidla a presety pod jmény a může nastavit výchozí hodnoty, které vaše konfigurace přepíše. Takhle se do DressCode zapojuje třeba Nette Coding Standard: + +```neon +extensions: + - Nette\CodingStandard\Extension +``` + +**Styl** je odsazovací jednotka a konec řádku. Můžete si ho určit sám; jinak platí to, co říká poslední preset, který styl deklaruje, a když žádný, pak tabulátor a ten konec řádku, který v souboru převládá: + +```neon +style: + indent: " " + eol: "\n" +``` + +Hodnota `eol` je `"\n"`, `"\r\n"`, nebo `auto` pro zachování stavu každého souboru. + +**Cílová verze PHP** je vlastnost projektu a čte se z `composer.json`; pravidla pro novější syntaxi se pod ní sama vynechají. Přepisujte ji jen tehdy, když se od `composer.json` liší, a vždy jako řetězec, aby se `8.10` nepřečetlo jako 8.1: + +```neon +phpVersion: '8.2' +``` + + +Pravidla a jejich volby +======================= + +Klíč `rules` je mapa, ve které je jménu pravidla přiřazeno `true`, `false`, nebo mapa voleb. Jména jsou ta z [přehledu pravidel |rules/@home] a z výpisu `dresscode check`; volby každého pravidla najdete i s příklady na jeho stránce. + +```neon +rules: + dresscode/strict-comparison: true + dresscode/no-alternative-syntax: false + dresscode/trailing-comma: + multiLine: [arrays, arguments] +``` + +Dvě věci, na kterých se dá zakopnout: + +- **Seznam ve volbě nahrazuje výchozí seznam, neslučuje se s ním.** `multiLine: [arguments]` zapne koncovou čárku u argumentů a vypne ji u polí, i když u polí byla ve výchozím stavu. Chcete-li přidávat, opište i výchozí hodnoty. +- **Jméno pravidla z jiného nástroje není platný klíč.** `no_unused_imports` ani `SlevomatCodingStandard.Namespaces.UnusedUses` sem nepatří. DressCode je zná a v chybové hlášce vám řekne, které jeho pravidlo jim odpovídá, ale do konfigurace patří jméno jeho. Celý cizí konfigurační soubor převede příkaz [`dresscode import` |migration#2. Přeložte konfiguraci]. + +Místo jména lze všude použít název třídy, což se hodí u [vlastních pravidel |custom-rule], která pak nemusíte nikde registrovat: + +```neon +rules: + App\CodeStyle\ExceptionMessagePeriodRule: true +``` + +Na jeden běh se pravidlo zapíná a vypíná z příkazové řádky: `--rule dresscode/line-length=off`. + + +Cesty +===== + +`paths` říká, co se kontroluje: soubory a adresáře relativně ke kořeni projektu. Cesty zadané na příkazové řádce mají přednost před konfigurací. + +`excludePaths` naopak cesty vynechává a **jen přidává**: k výchozímu seznamu (`vendor`, `node_modules`, `temp`, `tmp`, `log` a všechny adresáře začínající tečkou) přidá vaše, a totéž udělá každé rozšíření. Žádná vrstva nemůže vrátit zpátky to, co jiná vyloučila, takže se nestane, že by preset omylem zapnul kontrolu `vendor`. + +```neon +paths: + - src + - tests + +excludePaths: + - tests/fixtures + - '*.generated.php' +``` + +Vzor s lomítkem je ukotvený ke kořeni, takže `tests/fixtures` je právě ten jeden adresář. Vzor bez lomítka odpovídá jménu souboru nebo adresáře v jakékoli hloubce, takže `fixtures` vynechá každý adresář toho jména. Hvězdička zastupuje cokoli kromě lomítka. + +Jednotlivé pravidlo lze vypnout jen pro některé cesty; zbytek pravidel takový soubor zkontroluje normálně: + +```neon +excludeRulePaths: + dresscode/strict-comparison: [legacy] + dresscode/line-length: [tests] +``` + +`fileExtensions` říká, které přípony se berou jako PHP (výchozí je jen `php`); projekt s testy Nette Testeru přidá `phpt`. Vynechat soubor podle jeho obsahu, třeba generovaný kód podle hlavičky, umí anonymní funkce `skipWhen()`, takže tohle nastavení patří do `dresscode.php` nebo do rozšíření. + + +Další klíče +=========== + +- `baseline`: soubor se soupisem porušení, která se nemají hlásit; jak vzniká a kdy se hodí, popisuje [Potlačení pravidel a baseline |suppressing#Baseline]. +- `cacheDir`: kam si DressCode ukládá, které soubory už prošly čistě; výchozí je systémový dočasný adresář. +- `analyses`: registrace vlastní analýzy pro [vlastní pravidla |rule-contract#Analýzy]. + +Co je nakonec zapnuté, ukáže příkaz `dresscode rules`: vypíše všechna známá pravidla, hvězdičkou označí ta, která v aktuální konfiguraci platí, a u každého uvede jména pravidel jiných nástrojů, která pokrývá. diff --git a/dresscode/cs/continuous-integration.texy b/dresscode/cs/continuous-integration.texy new file mode 100644 index 0000000000..2625fdfa12 --- /dev/null +++ b/dresscode/cs/continuous-integration.texy @@ -0,0 +1,81 @@ +Průběžná integrace: GitHub Actions a GitLab +******************************************* + +.[perex] +Kontrola stylu v CI: formát výstupu, který se zvolí sám, anotace přímo v pull requestu, cache mezi běhy a exit kódy, na které se dá spolehnout. + + +Co v CI spouštět +================ + +`check`, ne `fix`. CI má říct, jestli je kód v pořádku, a exit kód `1` u porušení job shodí; opravu dělá vývojář lokálně, kde ji vidí. Kdo chce, aby CI opravu i nabídlo, pustí `fix` a za ním `git diff --exit-code`. Diff ve výpisu jobu je pak přesně to, co má vývojář commitnout. + +Exit kódy jsou pevně dané: `0` znamená čisto, `1` porušení nebo soubor, který nejde parsovat, a `2` selhání nástroje (špatná konfigurace, výjimka v pravidle). Na `2` má job selhat hlasitěji než na `1`, protože znamená, že se nezkontrolovalo nic. + + +GitHub Actions +============== + +```yaml +name: Code style + +on: [push, pull_request] + +jobs: + dresscode: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + - run: composer create-project dresscode/dresscode temp/dresscode --no-progress + - run: temp/dresscode/bin/dresscode check +``` + +Nic dalšího potřeba není: DressCode pozná, že běží jako krok GitHub Actions, a přepne výstup na formát `github`, ve kterém je každé porušení anotace. V pull requestu se pak ukáže přímo u řádku, kterého se týká, se zprávou i jménem pravidla. Ve výpisu jobu zůstane shrnutí. + +Barvy se vypnou samy, protože výstup nejde do terminálu, takže `--no-color` psát nemusíte. A pozor na verzi PHP v běžci: samotný DressCode potřebuje 8.4 nebo novější, i když kontroluje projekt psaný pro starší verzi. + + +GitLab, Jenkins a ostatní +========================= + +Kde nativní formát není, poslouží `checkstyle`, kterému rozumí Jenkins, nástroj `cs2pr` i většina převodníků do formátu Code Quality v GitLabu, a `json` pro vlastní zpracování: + +```yaml +dresscode: + script: + - composer create-project dresscode/dresscode temp/dresscode --no-progress + - temp/dresscode/bin/dresscode check -f checkstyle > dresscode.xml + artifacts: + when: always + paths: [dresscode.xml] +``` + +Tvar obou formátů se v minoritních verzích nemění. Formáty `console` a `github` jsou naopak pro lidi a měnit se mohou. + + +Cache mezi běhy +=============== + +DressCode si pamatuje otisky souborů, které prošly čistě, v systémovém dočasném adresáři. V CI, kde každý běh začíná načisto, z toho nic nemá. Když ale cache nasměrujete do projektu a ten adresář necháte CI uchovat, zkontroluje druhý běh jen změněné soubory: + +```neon +cacheDir: temp/dresscode-cache +``` + +```yaml + - uses: actions/cache@v4 + with: + path: temp/dresscode-cache + key: dresscode-${{ hashFiles('composer.lock', 'dresscode.neon') }} +``` + +Součástí klíče cache je i otisk konfigurace a verzí nainstalovaných balíčků, takže po změně konfigurace nebo po aktualizaci nástroje se soubory zkontrolují znovu samy od sebe. Hash v klíči jobu je jen proto, aby se stará cache neuchovávala navěky. + + +Paralelní běh a verze +===================== + +Počet pracovních procesů se řídí počtem procesorů běžce, takže na dvoujádrovém běžci není co ladit; na stroji s mnoha jádry pomůže `--jobs 8`. Verzi DressCode si přibijte, ať už přes `composer.lock`, nebo číslem u `create-project`: nové pravidlo v minoritní verzi může začít hlásit něco, co dosud procházelo, a to se má stát při vědomé aktualizaci, ne v cizím pull requestu. diff --git a/dresscode/cs/custom-rule.texy b/dresscode/cs/custom-rule.texy new file mode 100644 index 0000000000..af1635afda --- /dev/null +++ b/dresscode/cs/custom-rule.texy @@ -0,0 +1,150 @@ +Jak napsat vlastní pravidlo +*************************** + +.[perex] +Od fixtury k hotovému pravidlu za odpoledne: napíšete kód před opravou a po ní, řeknete, které uzly vás zajímají, sepíšete podmínky a `RuleTester` pohlídá zbytek. + + +Co budeme psát +============== + +Každý tým má pár pravidel, která žádný standard nepokrývá, protože jsou jenom jeho. Vezměme si tohle: zpráva výjimky je věta a končí tečkou, takže `throw new InvalidArgumentException('The name must not be empty')` chceme vidět jako `'The name must not be empty.'`. Roky jsem to hlídal v code review, protože napsat na to sniff (pravidlo pro PHP_CodeSniffer) dalo víc práce, než kolik to ušetřilo. Tady to bude čtyřicet řádků a napíšeme si je spolu. + + +Nejdřív fixtura +=============== + +Pravidlo se testuje nad fixturami (fixtures), tedy dvojicemi souborů s kódem před opravou a po ní. Jedna fixtura je až trojice souborů: `.code` s kódem před opravou, `.expected` s kódem po ní a `.violations` s řádky a zprávami, které má pravidlo ohlásit. Fixtura je nejlepší zadání, jaké si můžete napsat, tak s ní začneme. Do `tests/fixtures/exception-message-period/basic.code`: + +```php +<?php + +throw new InvalidArgumentException('The name must not be empty'); +throw new \RuntimeException('Cannot connect to the database.'); +throw new App\NotFoundException("Order $id not found"); +throw new LogicException('Unreachable '); +$logger->log('Started'); +``` + +Už tady se rozhoduje, co pravidlo umí a co ne: druhý řádek je v pořádku, třetí obsahuje interpolovaný řetězec a ten necháme být (kdo ví, co je v `$id`), čtvrtý má na konci mezeru navíc, kterou tečka spolkne, a pátý není výjimka. Soubor `basic.expected` je totéž s tečkami na řádcích 3 a 6 a `basic.violations` říká, co se má ohlásit: + +``` +3: The message of an exception must end with a period +6: The message of an exception must end with a period +``` + + +Kostra pravidla +=============== + +Pravidlo je třída odvozená od `DressCode\NodeRule` s atributem `#[RuleInfo]`, který nese jméno, fázi a popis. Jméno má tvar `vendor/slug` a je to to, co uvidíte ve výpisu a co budete psát do `dresscode:ignore`. Fáze (`Stage`) říká, kdy pravidlo běží: `Structure` pro změny kódu, `Formatting` pro bílé znaky a zalomení, `Cleanup` pro závěrečný úklid. Naše pravidlo mění text, takže patří do `Structure`. + +```php +namespace App\CodeStyle; + +use DressCode\NodeRule; +use DressCode\RuleContext; +use DressCode\RuleInfo; +use DressCode\Stage; +use PhpSyntax\Node; +use PhpSyntax\Nodes\Expression\NewNode; +use PhpSyntax\Token; + +#[RuleInfo('app/exception-message-period', Stage::Structure, description: 'Ends the message of an exception with a period')] +final class ExceptionMessagePeriodRule extends NodeRule +{ + public function getVisitedTypes(): array + { + return [NewNode::class]; + } + + + public function enter(Node|Token $node, RuleContext $context): void + { + } +} +``` + +`NodeRule` je jedna ze dvou podob pravidla: navštěvuje uzly stromu a pracuje na nich. Druhá podoba je `GapRule`, které místo návštěv [vyslovuje požadavky na bílé znaky |whitespace-rules]. + +Metoda `getVisitedTypes()` říká, které uzly chcete vidět; jádro pak volá `enter()` jen pro ně. Nás zajímá `new`, tedy `NewNode`. Které uzly existují a co v nich najdete, říká [přehled uzlů |phpsyntax:nodes]: `NewNode` má sloty `newKeyword`, `class` a `args`. + + +Podmínky a oprava +================= + +Teď to podstatné. Do `enter()` napíšeme jako řadu otázek na strom, kdy je co špatně (k importům přibudou `PhpSyntax\Nodes\NameNode` a `PhpSyntax\Nodes\Scalar\StringNode`): + +```php +public function enter(Node|Token $node, RuleContext $context): void +{ + if ( + !$node instanceof NewNode + || !$node->class instanceof NameNode + || !str_ends_with($node->class->text, 'Exception') + ) { + return; + } + + $message = $node->args?->items->getItems()[0]->expr ?? null; + if ( + !$message instanceof StringNode + || $message->quote !== "'" + || $message->value === '' + || str_ends_with(rtrim($message->value), '.') + || !$context->report($message, 'The message of an exception must end with a period') + ) { + return; + } + + $message->setValue(rtrim($message->value) . '.'); +} +``` + +Čtěte to shora: je to `new` se jménem třídy (tedy ne `new $class` ani anonymní třída) a jméno končí na `Exception`. První argument je řetězec bez interpolace, protože interpolovaný řetězec je jiný uzel než `StringNode`, takže třetí řádek fixtury odpadne sám. Ten řetězec je v jednoduchých uvozovkách, není prázdný a nekončí tečkou. A poslední otázka: **nikdo pravidlo v tomhle místě nepotlačil.** Volání `report()` vrátí `false`, pokud na řádku stojí `dresscode:ignore`, a pak se nesmí nic měnit. Proto stojí v podmínce, a ne před ní. + +Teprve za tím vším je oprava: jediné `setValue()`. Nezapisujeme text tokenu, ale hodnotu řetězce, takže o escapování a uvozovky se postará uzel sám. Zbytek souboru se nezmění ani o bajt. + +Všimněte si, co v pravidle není: žádné počítání závorek, žádné hledání, kde končí argument, žádná obrana proti komentáři uprostřed volání. To všechno ví strom. Nad plochým polem tokenů by většina kódu řešila, co všechno může stát mezi `new` a řetězcem; tady taková otázka vůbec nevzniká. + + +Test pravidla +============= + +`RuleTester` dostane třídu pravidla a adresář s fixturami a ověří všechno, na co byste sami zapomněli: že výstup odpovídá souboru `.expected`, že hlášení odpovídají souboru `.violations`, že pravidlo nad vlastním výstupem už nic nemění, že nezahodilo žádný komentář, že poslechne `dresscode:ignore-file` a že nic nezměnilo bez ohlášení. Z Nette Testeru vypadá takový test takhle: + +```php +use App\CodeStyle\ExceptionMessagePeriodRule; +use DressCode\Testing\RuleTester; + +require __DIR__ . '/../vendor/autoload.php'; +Tester\Environment::setup(); + +RuleTester::run(ExceptionMessagePeriodRule::class, __DIR__ . '/fixtures/exception-message-period'); +``` + +Z PHPUnit je to totéž volání uvnitř testovací metody. Selhání je výjimka `DressCode\Testing\TestFailure` s diffem, takže ji každý framework ukáže srozumitelně. + +Když teď test pustíte, projde. Kdyby neprošel, řekne vám diff, na kterém řádku fixtury pravidlo vidí něco jiného než vy, a to bývá chvíle, kdy se o kódu něco nového dozvíte. V tomhle případě to bylo `rtrim()`: v první verzi pravidla nebylo a fixtura s mezerou na konci ho vynutila. + + +Zapnutí v projektu +================== + +Třídu můžete dát kamkoli, kam vede autoload; vlastní pravidla mívám v adresáři `tools/CodeStyle/` s `autoload-dev`, aby se `DressCode\NodeRule` nedostal do produkčního autoloadu. V konfiguraci se pravidlo zapíná názvem třídy a jméno z atributu se zaregistruje samo: + +```neon +rules: + App\CodeStyle\ExceptionMessagePeriodRule: true +``` + +Od téhle chvíle ho `dresscode check` hlásí, `dresscode fix` opravuje, komentář `// dresscode:ignore app/exception-message-period` potlačuje a v editoru se ukazuje jako každé jiné. Když stejné pravidlo potřebuje víc projektů, zabalte ho do [presetu nebo rozšíření |presets-and-extensions]. + + +Kam dál +======= + +- Pravidlo, které má mít volby (třeba seznam přípon výjimek), implementuje rozhraní `ConfigurableRule` se schématem. Jak na to a co všechno musí pravidlo dodržet, je na stránce [Pravidlo do detailu |rule-contract]. +- Naše pravidlo poznává výjimku podle jména. Kdo chce víc, sáhne po analýze `NameResolver`, která jméno přeloží podle importů a jmenného prostoru; analýzy popisuje [PhpSyntax |phpsyntax:analyses]. +- Pravidlo pro mezery a zalomení řádků se píše jinak, požadavkem místo `enter()`: [Pravidla pro bílé znaky |whitespace-rules]. +- Kdo přepisuje existující sniff nebo fixer, má vlastní návod: [Přepis pravidla z jiného nástroje |porting-rules]. diff --git a/dresscode/cs/editors.texy b/dresscode/cs/editors.texy new file mode 100644 index 0000000000..640d9e3c07 --- /dev/null +++ b/dresscode/cs/editors.texy @@ -0,0 +1,65 @@ +Editory a IDE: PhpStorm a VS Code +********************************* + +.[perex] +Porušení podtržená přímo v kódu, oprava jedním klikem a formátování při uložení. Co nainstalovat, co naopak vypnout a co se stane s rozepsaným kódem, který zrovna nejde přeložit. + + +Co integrace umí +================ + +Nástroj, který běží jen v průběžné integraci, je poloviční užitek: o porušení se dozvíte až po pushi a opravujete ho v jiném rozpoložení, než jste kód psali. Integrace do editoru umí čtyři věci a v tomhle pořadí jsou i užitečné: + +1. **Formátování při uložení.** Soubor se po uložení opraví tak, jak by ho opravil `dresscode fix`. Zdaleka nejužitečnější věc a stačí na ni i editor bez jakéhokoli rozšíření, viz níže. +2. **Podtržení porušení** přímo v kódu, se zprávou a jménem pravidla v bublině. +3. **Oprava jednoho porušení** z nabídky rychlých oprav, ne celého souboru najednou. +4. **Potlačení jedním klikem**, které na správný řádek vloží `dresscode:ignore` se jménem pravidla. + +Pod tím vším běží jeden jazykový server, `dresscode lsp`, který je součástí balíku. Díky tomu se editory chovají stejně a totéž dostane každý, kdo mluví protokolem Language Server Protocol: Neovim, Zed, Helix i Sublime Text. + + +Jazykový server +=============== + +Server spouští plugin nebo rozšíření editoru samo; nastavovat ho musíte jen tehdy, když DressCode neleží ve `vendor/bin` projektu. Kdo ho zapojuje do editoru, pro který hotová integrace není, spustí: + +```shell +dresscode lsp +``` + +Server komunikuje přes standardní vstup a výstup, konfiguraci projektu čte stejně jako příkazová řádka a soubory drží v paměti, takže se při každém uložení neplatí start PHP. Neuložený obsah bere z editoru, ne z disku. + + +PhpStorm +======== + +Nainstalujte plugin **DressCode** z marketplace. Po instalaci si `vendor/bin/dresscode` v projektu najde sám; porušení se podtrhnou jako inspekce, `Alt+Enter` nabídne opravu nebo potlačení a v nastavení pluginu zapnete opravu při uložení. + +Zároveň vypněte vestavěné formátování PHP (Settings › Tools › Actions on Save › Reformat code), jinak se oba formátovače přetahují a soubor se při každém uložení změní dvakrát. + +I bez pluginu funguje **File Watcher** (Settings › Tools › File Watchers): typ souboru PHP, program `$ProjectFileDir$/vendor/bin/dresscode`, argumenty `fix $FilePath$ -f bare`, pracovní adresář `$ProjectFileDir$`. Formát `bare` vypíše jen porušení, která zůstala na vás, a u přepsaného souboru řádek `rewritten`, takže u čistého souboru watcher mlčí. + + +Visual Studio Code +================== + +Nainstalujte rozšíření **DressCode** z marketplace. Podtrhávání funguje hned, formátování při uložení zapnete v nastavení: + +```json +{ + "editor.formatOnSave": true, + "[php]": { + "editor.defaultFormatter": "dresscode.dresscode" + } +} +``` + +Jiný formátovač PHP (třeba Intelephense) přitom vypněte, nebo mu nechte jen to, co DressCode neřeší. Bez rozšíření udělá totéž rozšíření **Run on Save** s příkazem `vendor/bin/dresscode fix ${file} -f bare`. + + +Rozepsaný a rozbitý kód +======================= + +Během psaní je soubor půl vteřiny syntakticky rozbitý. Server v takové chvíli nehlásí záplavu chyb: ukáže jedinou, syntaktickou, na místě, kde parser skončil, a podtržení z posledního platného stavu nechá být. Oprava při uložení se nad souborem, který nejde parsovat, neprovede vůbec: radši neopravený soubor než zmrzačený. + +Totéž platí o každé opravě. DressCode zapíše jen výsledek, který se znovu naparsuje a vytiskne na totéž. Je to týž round trip, tedy zaručená shoda vytištěného stromu s původním souborem, na které stojí celý nástroj. V editoru je z něj pojistka, že formátování při uložení nemůže soubor rozbít. diff --git a/dresscode/cs/extending.texy b/dresscode/cs/extending.texy new file mode 100644 index 0000000000..9a4c62a38b --- /dev/null +++ b/dresscode/cs/extending.texy @@ -0,0 +1,42 @@ +Rozšiřování DressCode +********************* + +.[perex] +Vlastní pravidlo, preset, rozšíření, analýza nebo reporter: co je kdy správná volba a na které části DressCode se dá spolehnout jako na veřejné API. + + +Pět míst, kam se dá sáhnout +=========================== + +**Pravidlo** je odpověď na "chci hlídat ještě tohle". Buď `NodeRule`, které řekne, které uzly ho zajímají, položí otázku a případně opraví, nebo `GapRule`, které vysloví požadavek na bílé znaky mezi tokeny. Napíšete ho za odpoledne: [Jak napsat vlastní pravidlo |custom-rule]. + +**Preset** je odpověď na "chci tenhle styl na všech projektech". Pojmenovaná sada pravidel s volbami, která může vycházet z jiného presetu. Viz [Vlastní preset a rozšíření |presets-and-extensions]. + +**Rozšíření** (extension) je odpověď na "chci to rozdávat jako balíček". Třída, která se přihlásí do konfigurace, zaregistruje pravidla a presety pod jmény a nastaví výchozí hodnoty; uživatel ji zapne jediným řádkem. Takhle je postavený Nette Coding Standard. Viz [Vlastní preset a rozšíření |presets-and-extensions]. + +**Analýza** je odpověď na "tuhle informaci o souboru potřebuje víc pravidel". Obyčejná třída postavená nad stromem, kterou si pravidla vyžádají; jádro ji vytvoří jednou a po každé změně stromu zahodí. Vestavěné jsou [NameResolver a Scope |phpsyntax:analyses] a `PhpDoc`. Viz [Pravidlo do detailu |rule-contract#Analýzy]. + +**Reporter** je odpověď na "chci výsledky jinam než do terminálu": rozhraní se třemi metodami, kterému chodí výsledky soubor po souboru. Viz [PHP API |php-api]. + +Co rozšířit nejde, je parser. Ten je samostatná knihovna [PhpSyntax |phpsyntax:], jeho gramatikou je PHP a nic jiného, a strom, který z ní vzejde, je pro všechna pravidla stejný. Právě proto se dají pravidla skládat, aniž by o sobě musela navzájem vědět. + + +Co je veřejné API +================= + +Na tyhle třídy se rozšíření může spolehnout mezi minoritními verzemi: + +- **psaní pravidla**: `Rule` s potomky `NodeRule` a `GapRule`, `ConfigurableRule`, `RuleInfo`, `RuleContext`, `Claim`, `Gap`, `Violation` a výčty `Stage`, `Severity`, `Space`, `Line`; +- **styl a konfigurace**: `Config`, `Config\Loader`, `Preset`, `PresetInfo`, `PresetContext`, vestavěné presety v `Presets\` a vestavěná pravidla v `Rules\`; +- **běh a výstup**: `Runner`, `Reporter` s `FileResult` a `RunResult`, vestavěné reportery v `Reporters\`, `Console\Application`; +- **analýzy a testování**: `Analyses\PhpDoc`, `Analyses\NativeType`, `Testing\RuleTester` s `Testing\TestFailure`; +- **výjimky**: `ConfigurationException`, `RuleException`, `ConvergenceException`, `Console\UsageException`; +- z knihovny [PhpSyntax |phpsyntax:] všechno, co není označené `@internal`: `Parser`, `Lexer`, `Printer`, `Node`, `Token`, `Trivia`, uzly, analýzy, `Style`, `Indentation`, výčty a `ParseException`. + +Co je označené `@internal`, se může změnit kdykoli. Rozšíření k tomu nemá důvod sahat, protože všechno, co potřebuje vestavěné pravidlo, je veřejné. Oba seznamy hlídá test, takže třída, která není ani v jednom, shodí testovací sadu, místo aby se veřejným API stala nedopatřením. A protože s novou syntaxí PHP může v minoritní verzi přibýt nový uzel stromu, potřebuje každý `match` přes třídy uzlů větev `default`. + + +Pojmenování +=========== + +Jméno pravidla má tvar `vendor/slug` v kebab-case a říká stav, který pravidlo vynucuje, ne krok, který dělá oprava: tedy `acme/exception-message-period`, ne `acme/add-period`. Předpona `no-` znamená konstrukci, která se nesmí objevit vůbec, předpona `useless-` konstrukci, která je jinde v pořádku, ale na tomhle místě nic nepřidává. Jméno presetu je `vendor/jméno-standardu`. Je to konvence vestavěného katalogu a rozšíření, které se jí drží, do něj zapadne. diff --git a/dresscode/cs/from-ncs.texy b/dresscode/cs/from-ncs.texy new file mode 100644 index 0000000000..c51f3f1956 --- /dev/null +++ b/dresscode/cs/from-ncs.texy @@ -0,0 +1,86 @@ +Přechod z Nette Coding Standardu +******************************** + +.[perex] +Nette Coding Standard 4 je preset DressCode. Sám standard se jmenuje `dresscode/nette`, balíček k němu přidává tři odvozené presety a filtr souborů. Co udělá `ecs migrate` a proč první oprava přeformátuje víc souborů, než čekáte. + + +Co se změnilo +============= + +Nette Coding Standard byl do verze 3 tenká obálka nad PHP CS Fixerem a PHP_CodeSniffer s balíkem pravidel od Slevomatu: dva nástroje, dvě konfigurace (`ncs.php` pro fixer, `ncs.xml` pro sniffer) a příkaz `ecs`, který to celé spouštěl. + +Od verze 4 je standard **presetem DressCode**. Jmenuje se `dresscode/nette` a je součástí DressCode, takže ho má k dispozici každý projekt bez ohledu na to, jestli s Nette pracuje. Balíček `nette/coding-standard` k němu přidává to, co potřebuje navíc projekt postavený na Nette: + +- presety `nette/clean-code`, `nette/optimize-fn` a `nette/types`, které standard rozšiřují o pravidla nad rámec stylu, o importy globálních funkcí optimalizovaných kompilátorem a o doplňování nativních typů z anotací; +- `PhpVersionFilter`, který vynechá soubory s anotací `@phpVersion` nad běžící verzí PHP, jak to potřebují testy knihoven Nette; +- příkaz `ecs` z verze 3, aby projekt na trojce fungoval dál, než přejde. + + +Instalace a konfigurace +======================= + +```shell +composer require --dev dresscode/dresscode nette/coding-standard +``` + +Do kořene projektu přijde `dresscode.neon`, který přihlásí rozšíření balíčku a vybere presety: + +```neon +extensions: + - Nette\CodingStandard\Extension + +presets: + - dresscode/nette + - nette/types # volitelně také nette/clean-code, nette/optimize-fn + +paths: + - src + - tests +``` + +Rozšíření jen zpřístupní jména presetů z balíčku, vyloučí cesty, které vylučovala verze 3 (`expected`, `tmp`, `fixtures*`), a zapne filtr podle `@phpVersion`. Který styl se použije, říká klíč `presets`, protože to je rozhodnutí projektu, ne balíčku. + +Kontroluje a opravuje se pak samotným DressCode: + +```shell +dresscode check +dresscode fix +``` + + +Převod konfigurace +================== + +Projekt s `ncs.php` nebo `ncs.xml` je převede jedním příkazem: + +```shell +vendor/bin/ecs migrate +``` + +Přečte `ncs.php` a `ncs.xml` v aktuálním adresáři a napíše `dresscode.neon`. Jména pravidel a kódy sniffů z verze 3 projdou překladem na jména pravidel DressCode. Co žádné pravidlo nepokrývá, příkaz vypíše a vynechá, a u zkopírovaných voleb připojí poznámku, že jejich jména je potřeba zkontrolovat, protože volba fixeru a volba pravidla DressCode se mohou jmenovat jinak. Existující `dresscode.neon` příkaz nepřepíše. + +| verze 3 | verze 4 | +|---|---| +| `ncs.php` s `'pravidlo' => false` | `rules: {pravidlo: false}` v `dresscode.neon`; stará jména fungují jako aliasy | +| `ncs.xml` s `<exclude name="…"/>` | `rules: {…: false}` | +| `ncs.xml` s `<exclude-pattern>` | klíč `excludePaths`, nebo komentář `// dresscode:ignore` | +| `--config-file overrides.php` | `dresscode.neon` v kořeni projektu | +| `--preset php81`, `--preset php` | nic, cílovou verzi PHP bere DressCode z vašeho `composer.json` | + +Příkaz `ecs` zůstává a přijímá starou příkazovou řádku (`ecs check`, `ecs fix`, výchozí cesty `src` a `tests`, `--preset clean-code`), takže CI ani editor nemusíte sáhnout hned. Verzní presety se ignorují, protože cílová verze PHP se teď čte z projektu. Je to ale věc přechodová: jakmile je `dresscode.neon` na místě, volejte rovnou `dresscode`. + + +První oprava přeformátuje víc, než čekáte +========================================= + +Verze 4 není řádek po řádku totéž co verze 3, a je to tak schválně. Některé rozdíly jsou opravy toho, co verze 3 nechávala být: velké písmeno na začátku komentáře `// Komentář`, komentář `/* enum */` před `case`, `declare(strict_types=1)` na řádku otevíracího tagu, nezlomitelná mezera zapsaná doslova v řetězci. Napříč balíčky Nette to bylo víc než sto souborů z necelých tří tisíc. + +Udělejte to jako samostatný commit: `dresscode fix`, commit bez jiných změn, hotovo. Procházet to řádek po řádku nemá cenu; cenu má vědět, že v tom commitu není nic jiného. + + +Co zůstalo +========== + +- Komentáře `phpcs:ignore` a spol. fungují dál; přepis na `dresscode:ignore` udělá [dresscode migrate-suppressions |migration#3. Přepište komentáře]. +- Vlastní sniffy a fixery, pokud nějaké byly, se přepisují podle [návodu |porting-rules]; přepsaný sniff bývá několikanásobně kratší. diff --git a/dresscode/cs/from-php-cs-fixer.texy b/dresscode/cs/from-php-cs-fixer.texy new file mode 100644 index 0000000000..e262563d82 --- /dev/null +++ b/dresscode/cs/from-php-cs-fixer.texy @@ -0,0 +1,66 @@ +Přechod z PHP CS Fixeru +*********************** + +.[perex] +Převod `.php-cs-fixer.dist.php` na konfiguraci DressCode, sady `@PSR12` a `@PER-CS` na presety, tabulka příkazů a vysvětlení, proč tu rizikové (risky) pravidlo rizikové obvykle není. + + +Než začnete +=========== + +Projděte si [společný postup |migration]: kód a komentáře můžete nechat, jak jsou, konfiguraci převede `import` a komentáře přepíše `migrate-suppressions`. Tahle stránka doplňuje, co je u PHP CS Fixeru zvláštní. + +Nejdůležitější věc rovnou: **konfigurace PHP CS Fixeru je PHP soubor, který se musí spustit**, a vrací objekt jeho knihovny. Příkaz `dresscode import` ho proto přečte jen v projektu, kde je `friendsofphp/php-cs-fixer` ještě nainstalovaný. Překládejte tedy dřív, než ho odeberete. + + +Převod konfigurace +================== + +```shell +dresscode import .php-cs-fixer.dist.php > dresscode.php +``` + +Sady se překládají na presety: `@PSR1`, `@PSR2` a `@PSR12` na `dresscode/psr12`, `@PER`, `@PER-CS` a jejich číslované varianty na `dresscode/per`, který odpovídá [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/]. Sady bez protějšku, hlavně `@Symfony` a `@PhpCsFixer`, `import` ohlásí; v takovém případě začněte od `dresscode/per` a doplňte pravidla, na kterých vám záleží. Sady s příponou `:risky` protějšek nemají, protože rozdělení na riziková a bezpečná pravidla tu neexistuje (viz níže). + +Jednotlivé fixery se překládají jménem, a kde má volba protějšek, tak i s ní: `no_unused_imports` je `dresscode/unused-imports`, `binary_operator_spaces` je `dresscode/binary-operator-spacing`, `trailing_comma_in_multiline` se svým `elements` je `dresscode/trailing-comma` s volbou `multiLine`. Fixer, který protějšek nemá, se objeví ve výpisu na chybovém výstupu, a z toho máte seznam věcí, o kterých je potřeba rozhodnout. Vlastní fixery z balíčku `kubawerlos/php-cs-fixer-custom-fixers` se překládají také, pokud pro ně DressCode pravidlo má. + +Co `import` nepřenese, protože to v konfiguraci Fixeru nejsou pravidla: + +- **Finder.** Cesty, `exclude()` a `notPath()` přepište do klíčů `paths` a `excludePaths`; vzory popisuje [Konfigurace |configuration#Cesty]. +- **Odsazení a konce řádků** (`setIndent()`, `setLineEnding()`). Nastaví je preset; pokud žádný nepoužíváte, doplňte klíč `style`. +- **`setRiskyAllowed()`** protějšek nemá a není potřeba. + + +Riziková pravidla +================= + +PHP CS Fixer označuje jako rizikové (risky) takové pravidlo, které nad polem tokenů nedokáže odlišit bezpečný případ od nebezpečného, a rozhodnutí nechává na vás. Typický příklad je `ternary_to_elvis_operator`: nepozná, jestli je podmínka ternárního operátoru proměnná, nebo volání funkce s vedlejším účinkem, takže hlídá jen `++` a `--` a zbytek je váš problém. + +V DressCode tohle rozdělení není, protože otázku zodpoví strom: pravidlo `dresscode/short-ternary-operator` zkrátí ternární operátor jen tehdy, když je podmínku bezpečné vyhodnotit dvakrát, a volání funkce nechá být. Totéž platí pro další pravidla, která byla riziková z téhož důvodu. + +Něco jiného jsou pravidla, která mění chování programu ze své podstaty: `strict_comparison` dělá z `==` `===` a to není otázka syntaxe, ale významu. `dresscode/strict-comparison` dělá totéž a je na vás, jestli ho zapnete. Jen se to nedozvíte ze značky risky, ale z popisu pravidla. + + +Příkazy +======= + +| PHP CS Fixer | DressCode | +|---|---| +| `php-cs-fixer fix --dry-run` | `dresscode check` | +| `php-cs-fixer fix --dry-run --diff` | `dresscode check --diff` | +| `php-cs-fixer fix` | `dresscode fix` | +| `php-cs-fixer fix --config=soubor` | `dresscode fix --config soubor` | +| `php-cs-fixer fix --rules=jméno` | `dresscode fix --rule jméno=on` (přidá pravidlo k presetu, nenahradí jím celou sadu) | +| `php-cs-fixer fix --format=checkstyle` | `dresscode check -f checkstyle` | +| `php-cs-fixer fix --allow-risky=yes` | nic, viz výše | +| `.php-cs-fixer.cache` | cache je zapnutá sama; `--no-cache` ji obejde | + +Exit kód PHP CS Fixeru je bitová maska (8 znamená nalezená porušení, 16 chybu konfigurace a tak dále), kdežto DressCode vrací `0` pro čisto, `1` pro porušení a `2` pro selhání nástroje. Skript v CI, který masku vyhodnocoval, potřebuje jednu úpravu. + + +Co v PHP CS Fixeru nebylo +========================= + +- **Potlačení na řádku.** PHP CS Fixer neumí vypnout pravidlo pro jeden řádek ani pro blok, jen pro celý soubor přes Finder. Tady je na to `// dresscode:ignore jméno` a dvojice `dresscode:disable` a `dresscode:enable`, viz [Potlačení pravidel a baseline |suppressing]. +- **Pravidlo jen pro některé cesty** je klíč `excludeRulePaths` v konfiguraci, bez druhého konfiguračního souboru. +- **Vlastní pravidlo** píšete proti stromu, ne proti tokenům. [Návod |custom-rule] je na jedno odpoledne a kdo má vlastní fixer, přepíše ho podle [samostatné stránky |porting-rules]. diff --git a/dresscode/cs/from-phpcs.texy b/dresscode/cs/from-phpcs.texy new file mode 100644 index 0000000000..692ca10ba3 --- /dev/null +++ b/dresscode/cs/from-phpcs.texy @@ -0,0 +1,59 @@ +Přechod z PHP_CodeSniffer a Slevomatu +************************************* + +.[perex] +Převod `phpcs.xml` na konfiguraci DressCode, příkazy `phpcs` a `phpcbf` nahrazené příkazy `check` a `fix`, jiné exit kódy a přehled toho, co odpovídá čemu. + + +Než začnete +=========== + +Projděte si [společný postup |migration]: kód a komentáře můžete nechat, jak jsou, konfiguraci převede `import` a komentáře přepíše `migrate-suppressions`. Tahle stránka doplňuje, co je u PHP_CodeSniffer zvláštní. A jedna dobrá zpráva rovnou: `phpcs.xml` je XML, takže ho `import` přečte i tehdy, když už jsou PHP_CodeSniffer i Slevomat odinstalované. + + +Převod konfigurace +================== + +```shell +dresscode import phpcs.xml > dresscode.php +``` + +Sady `PSR1`, `PSR2` a `PSR12` se překládají na preset `dresscode/psr12`. Jednotlivé sniffy (pravidla PHP_CodeSniffer) se překládají jménem, a kde má vlastnost z `<properties>` protějšek, tak i s ní: `Generic.Files.LineLength` s `lineLimit` je `dresscode/line-length` s volbou `limit`, `SlevomatCodingStandard.Namespaces.UnusedUses` je `dresscode/unused-imports`, `Squiz.WhiteSpace.OperatorSpacing` je `dresscode/binary-operator-spacing`. + +Vyloučení sniffu uvnitř sady (`<exclude name="…"/>`) a umlčení přes `<severity>0</severity>` `import` nepřenáší; takové pravidlo vypněte v klíči `rules` hodnotou `false`. Sniff bez protějšku skončí ve výpisu na chybovém výstupu; typicky jde o sniffy o dokumentačních komentářích z balíku `Squiz.Commenting` a o sniffy, které hlídají totéž co jiný sniff, jen jinými slovy. + +Co v `phpcs.xml` není pravidlo a přepíšete to ručně: + +- `<file>` a `<exclude-pattern>` do klíčů `paths` a `excludePaths`; vzory popisuje [Konfigurace |configuration#Cesty]. +- `<arg name="extensions">` do klíče `fileExtensions`. +- `<config name="php_version">` není potřeba, cílovou verzi si DressCode přečte z `composer.json`. +- `<arg name="tab-width">` a odsazení řeší preset, nebo klíč `style`. + +Slevomat: jeho sniffy, které DressCode pokrývá, se přeloží jménem jako každé jiné. Jeho pomocné třídy ani nastavení `installed_paths` tu nic nepotřebuje. + + +Příkazy +======= + +| PHP_CodeSniffer | DressCode | +|---|---| +| `phpcs src` | `dresscode check src` | +| `phpcbf src` | `dresscode fix src` | +| `phpcs --standard=PSR12 src` | `dresscode check --preset dresscode/psr12 src` | +| `phpcs --report=checkstyle` | `dresscode check -f checkstyle` | +| `phpcs --report=json` | `dresscode check -f json` | +| `phpcs --parallel=8` | `dresscode check --jobs 8` | +| `phpcs --cache` | cache je zapnutá sama; `--no-cache` ji obejde | +| `phpcs -p` | průběh se na terminálu ukazuje sám | +| `.phpcs.xml.dist` | `dresscode.neon.dist` | + +PHP_CodeSniffer rozlišuje exit kódem, jestli jsou nalezená porušení opravitelná, a `phpcbf` má vlastní stupnici. DressCode vrací `0` pro čisto, `1` pro porušení a `2` pro selhání nástroje; kolik porušení šlo opravit, řekne shrnutí a formát `json`. Skript, který exit kód vyhodnocuje, potřebuje jednu úpravu. + + +Co je jinak +=========== + +- **Jeden nástroj na kontrolu i opravu.** `phpcs` a `phpcbf` byly dva příkazy, které se občas neshodly, protože oprava mohla odhalit další porušení. Tady je `fix` totéž co `check` plus zápis, a co `fix` neopraví, ohlásí stejně jako `check`. +- **Priority sniffů neexistují**, pravidla běží opakovaně do ustálení; [proč |how-it-works#Bez priorit: pravidla běží do ustálení]. +- **Komentáře `phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, `phpcs:ignoreFile` a `@phpcsSuppress` fungují dál**, a to i se jmény sniffů. Přepis na `dresscode:*` je jeden příkaz a je dobrovolný. +- **Vlastní sniff** se nepřenáší, ale přepisuje; nad stromem z něj obvykle zbude zlomek délky. Návod je na [samostatné stránce |porting-rules]. diff --git a/dresscode/cs/getting-started.texy b/dresscode/cs/getting-started.texy new file mode 100644 index 0000000000..74723f5398 --- /dev/null +++ b/dresscode/cs/getting-started.texy @@ -0,0 +1,129 @@ +Začínáme s DressCode +******************** + +.[perex] +Za pět minut máte nástroj nainstalovaný, projekt zkontrolovaný a většinu nálezů opravenou. Projdeme instalaci, příkazy `check` a `fix`, volbu stylu podle [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/] nebo Nette a konfigurační soubor, který pak stačí commitnout. + + +Instalace +========= + +DressCode je nástroj, ne knihovna, takže nejlepší je nainstalovat ho **mimo projekt**, který kontroluje. Nejjednodušší je globální instalace: + +```shell +composer global require dresscode/dresscode +``` + +Adresář s globálními binárkami Composeru přidejte do [proměnné PATH |https://getcomposer.org/doc/03-cli.md#global] a příkaz `dresscode` je pak k dispozici odkudkoli. + +Do průběžné integrace, kde chcete verzi nástroje přibít na konkrétní číslo, se hodí instalace jako samostatný projekt: + +```shell +composer create-project dresscode/dresscode temp/dresscode +temp/dresscode/bin/dresscode check src +``` + +A do třetice: DressCode lze přidat i jako vývojovou závislost projektu (`composer require --dev dresscode/dresscode`) a spouštět z `vendor/bin/dresscode`. Funguje to, ale platíte za to tím, že se závislosti nástroje mísí se závislostmi projektu. Sám DressCode vyžaduje PHP 8.4 nebo novější, takže by na 8.4 musel běžet i váš projekt, i kdyby mu jinak stačilo starší PHP. + +To je totiž věc, která se plete nejčastěji: **verze PHP, na které běží nástroj, a verze PHP, pro kterou je psaný váš kód, jsou dvě různá čísla.** Když je DressCode nainstalovaný mimo projekt, může běžet třeba na PHP 8.5 a přitom kontrolovat kód psaný pro PHP 8.1. Cílovou verzi si přečte z `composer.json` vašeho projektu a pravidla se jí řídí, takže vám do kódu nikdy nenapíše syntaxi, kterou by projekt neuměl přeložit. + +Víc DressCode nepotřebuje: parser a strom, na kterých stojí, jsou samostatná knihovna [PhpSyntax |phpsyntax:] bez jediné závislosti, k tomu čtyři malé balíčky z Nette a parser phpDocu od PHPStanu. Žádný framework. + + +Kontrola kódu +============= + +Řekněte DressCode, kam se má podívat: + +```shell +dresscode check src tests +``` + +Bez další konfigurace platí preset `dresscode/per`, tedy [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/], nástupce PSR-12. Jiný styl vyberete přepínačem `--preset`, ať už jednorázově, nebo než si založíte konfigurační soubor: + +```shell +dresscode check src --preset dresscode/nette +``` + +Výstup vypadá takhle: + +/--pre .[terminal] +DRESS|CODE 1.0 +Config none, preset dresscode/per +Target PHP 8.2 from composer.json +Checking 214 files in /var/www/shop + +src/Cart.php + error 8:12 A line break before the opening brace braces-position + error 9:25 An array must be written with the short syntax short-array-syntax + error 10:21 No whitespace after the opening parenthesis parentheses-spacing + error 11:11 At least one space before the == operator binary-operator-spacing + error 11:19 The body of a control structure must be enclosed in braces control-structure-braces + +FOUND 36 violations, 36 of them fixable in 12 files +\-- + +Každý řádek říká, kde problém je (řádek a sloupec), co je špatně (zpráva popisuje, jak má kód vypadat) a které pravidlo to hlásí. Jméno pravidla vpravo je to, s čím se dá dál pracovat: najít ho v [přehledu pravidel |rules/@home], [nastavit nebo vypnout |configuration] anebo [potlačit na jednom místě |suppressing]. + +Hlavička nahoře odpovídá na dvě otázky, které jinak stojí za polovinou nedorozumění: podle čeho se kontroluje (konfigurační soubor a presety) a pro jakou verzi PHP. + +Exit kódy jsou tři a stojí za zapamatování, protože na nich stojí kontrola v [průběžné integraci |continuous-integration]: + +| kód | význam | +|---|---| +| `0` | čisto | +| `1` | nalezená porušení, nebo soubor, který nejde parsovat | +| `2` | selhání nástroje: špatná konfigurace, neznámé pravidlo, chyba za běhu | + + +Automatická oprava +================== + +Většinu nálezů opraví DressCode sám. Před prvním během si soubory commitněte nebo aspoň mějte čistý pracovní strom, ať v diffu vidíte přesně to, co nástroj změnil: + +```shell +dresscode fix src tests +``` + +/--pre .[terminal] +src/Cart.php + fixed 8:12 A line break before the opening brace braces-position + fixed 9:25 An array must be written with the short syntax short-array-syntax + ... + +FIXED 36 violations fixed in 12 files +\-- + +Co opravit nejde (třeba příliš dlouhý řádek), zůstane ve výpisu jako `error` a exit kód bude `1`; jinak `0`. Kdo chce opravy napřed vidět, pustí `dresscode check --diff`: ukáže, co by `fix` změnil, a nic nezapíše. + +Opravu udělejte jako samostatný commit bez jiných změn. Je to jeden z těch commitů, které nikdo nečte řádek po řádku, a přesně tak má vypadat: `git blame` pak vede na něj a ne na váš další commit se skutečnou změnou. Na velkém projektu, kde by byl takový commit neúnosný, je druhá cesta: [baseline |suppressing#Baseline] zapíše dnešní stav a hlásí jen nová porušení. + +Druhé spuštění je rychlejší než první: DressCode si pamatuje obsah souborů, které prošly čistě, a pokud se nezměnil ani obsah, ani konfigurace, nezpracovává je znovu. + + +Konfigurace v NEONu +=================== + +Aby nebylo nutné pokaždé vypisovat cesty a preset, založte v kořeni projektu soubor `dresscode.neon`: + +```neon +presets: + - dresscode/nette + +paths: + - src + - tests +``` + +Je to [NEON |neon:], tedy formát, který znáte z konfigurace PHPStanu, a klíče jsou schválně podobné: `paths`, `excludePaths`, `phpVersion`. Kdo má radši PHP, napíše totéž do `dresscode.php` jako volání nad objektem `Config`; oba zápisy umějí totéž, jen anonymní funkce se do NEONu nevejdou. + +Od téhle chvíle stačí `dresscode check`. Do téhož souboru přijdou i jednotlivá pravidla s volbami a výjimky pro cesty; všechno popisuje stránka [Konfigurace |configuration]. + + +Kam dál +======= + +- [Jak DressCode funguje |how-it-works], pokud chcete rozumět tomu, proč se nástroj chová, jak se chová. +- [Přechod na DressCode |migration], pokud dnes používáte PHP CS Fixer, PHP_CodeSniffer nebo Slevomat. +- [Editory a IDE |editors], aby se porušení ukazovala rovnou při psaní a ne až v terminálu. +- [Průběžná integrace |continuous-integration], aby styl hlídal i server. diff --git a/dresscode/cs/hooks.texy b/dresscode/cs/hooks.texy new file mode 100644 index 0000000000..77ba078d84 --- /dev/null +++ b/dresscode/cs/hooks.texy @@ -0,0 +1,55 @@ +Git hooky a agenti +****************** + +.[perex] +Pre-commit hook, který mlčí, dokud je čisto, a formát `bare` pro nástroje a AI agenty, kteří potřebují vědět, které soubory se pod nimi přepsaly. + + +Pre-commit +========== + +Hook má být rychlý a tichý: zkontrolovat jen soubory, které jdou do commitu, a když je všechno v pořádku, nevypsat nic. Přesně na to je formát `bare`, a přepínač `--stdin` navíc umožňuje kontrolovat obsah z indexu, ne z pracovního stromu, který se od něj může lišit: + +```shell +#!/bin/sh +status=0 +for file in $(git diff --cached --name-only --diff-filter=ACM -- '*.php'); do + git show ":$file" | dresscode check --stdin "$file" -f bare || status=1 +done +exit $status +``` + +Uložte jako `.git/hooks/pre-commit` a nastavte spustitelný bit. Když je vše v pořádku, hook nic nevypíše a commit projde; jinak vypíše soubor a pod ním porušení s řádkem, zprávou a jménem pravidla, a commit zastaví. + +Kdo chce hook, který rovnou opravuje, pustí `fix` nad pracovním stromem a opravené soubory přidá do indexu. Je to pohodlné, ale znamená to, že commit obsahuje změny, které vývojář neviděl. Mně je bližší první varianta a oprava na jedno stisknutí přímo v [editoru |editors]. + +S nástrojem `pre-commit` je to jeden záznam v konfiguraci: + +```yaml +repos: + - repo: local + hooks: + - id: dresscode + name: DressCode + entry: dresscode check -f bare + language: system + types: [php] +``` + + +Agenti +====== + +Kdo nechává kód psát AI agenta, potřebuje dvě věci: aby se soubor po každém zápisu srovnal podle standardu, a aby se agent dozvěděl, že se soubor pod ním změnil. Bez toho nastane známá smyčka: agent zapíše `use`, hook ho jako nepoužitý smaže, agent přidá kód, který ten import potřeboval, a diví se, kde se stala chyba. + +Formát `bare` na to odpovídá jedním řádkem: + +```shell +dresscode fix src/Cart.php -f bare +``` + +/--pre .[terminal] +src/Cart.php rewritten +\-- + +Řádek `rewritten` říká agentovi, že si má soubor přečíst znovu, než na něj zase sáhne. Porušení, která zůstala neopravená, dostane ve stejném výpisu a může je opravit sám. Čistý soubor nevypíše nic, takže hook nezahlcuje kontext. Přesně takhle je zapojený hook v pluginu Nette pro Claude Code a stejný tvar funguje pro jakýkoli nástroj, který umí spustit příkaz po zápisu souboru. diff --git a/dresscode/cs/how-it-works.texy b/dresscode/cs/how-it-works.texy new file mode 100644 index 0000000000..9c8546711c --- /dev/null +++ b/dresscode/cs/how-it-works.texy @@ -0,0 +1,130 @@ +Jak DressCode funguje +********************* + +.[perex] +Kontrola i oprava stylu kódu stojí na jednom nápadu: nástroj nevidí ploché pole tokenů, ale syntaktický strom, ve kterém nic nechybí. Odtud plyne všechno ostatní, od přesnosti oprav přes presety až po to, proč tu nejsou priority pravidel. Na konci najdete slovníček pojmů, které se v dokumentaci opakují. + + +Syntaktický strom místo pole tokenů +=================================== + +Nástroje na styl PHP kódu se dvacet let stavěly nad funkcí `token_get_all()`, tedy nad plochým seznamem: tady je `if`, tady závorka, tady proměnná, tady mezera. Ten seznam neřekne, kde `if` končí, ani jestli `[` otvírá pole, nebo přístup k prvku. Každé pravidlo si strukturu domýšlí samo a velká část jeho kódu je jen obrana proti tomu, aby se nespletlo. + +DressCode místo toho soubor parsuje do **bezztrátového syntaktického stromu** (anglicky *lossless concrete syntax tree*). Každý uzel (node) ví, co je zač: `IfNode` má podmínku a tělo, `TernaryNode` má tři části. A v tom stromu je opravdu **všechno** ze zdrojáku včetně mezer, prázdných řádků a komentářů; ty visí na tokenech jako takzvaná trivia. Když strom vytisknete, dostanete původní soubor bajt po bajtu. Strom je samostatná knihovna [PhpSyntax |phpsyntax:], takže po něm může sáhnout i nástroj, který se stylem kódu nemá nic společného. + +Z toho plyne základní vlastnost celého nástroje: **pravidlo změní jen to, na co sáhne, a zbytek souboru zůstane, jak byl.** Žádné přetištění souboru podle vlastních představ, žádný ztracený komentář, diff přesně tak velký jako oprava. A protože pravidlo pracuje s konkrétním uzlem, jsou přesná i hlášení: když říká, že u ternárního operátoru chybí mezera, myslí tenhle ternární operátor, ne "něco kolem otazníku na řádku 12". + + +Jak vypadá pravidlo +=================== + +Pravidlo je malá třída, která řekne, které uzly ji zajímají, a pro každý z nich položí několik otázek. Takhle vypadá tělo pravidla, které zkracuje `$a ? $a : $b` na `$a ?: $b`: + +```php +public function enter(Node|Token $node, RuleContext $context): void +{ + if ( + $node instanceof TernaryNode + && $node->if !== null + && $node->cond->isRepeatableRead() + && $node->cond->matches($node->if) + && $node->question->getLine() === $node->colon->getLine() + && !$node->question->hasCommentUpTo($node->colon) + && $context->report($node, "A ternary repeating its condition must be written '?:'") + ) { + $node->if = null; + $node->question->setTrailingTrivia([]); + } +} +``` + +Ty podmínky se dají přečíst jako věty: je to ternární operátor, má prostřední část, podmínku lze bezpečně vyhodnotit dvakrát, prostřední část je stejná jako podmínka, obojí je na jednom řádku, není mezi nimi komentář a nikdo pravidlo v tomhle místě nepotlačil. Na otázky typu "dá se tenhle výraz bezpečně přečíst podruhé" odpovídá strom, takže si je pravidlo nemusí odvozovat samo. Proto má většina vestavěných pravidel pod sto řádků a proto [vlastní pravidlo napíšete za odpoledne |custom-rule]. + + +Nejdřív ohlásit, teprve pak opravit +=================================== + +Všimněte si, že `report()` stojí uvnitř podmínky. Není to náhoda, ale pravidlo, které jádro nástroje vynucuje: **kód se smí změnit až poté, co pravidlo porušení ohlásilo a hlášení prošlo.** Jádro spáruje každou změnu stromu s hlášením; pravidlo, které něco změní potichu, kontrakt poruší a dozvíte se to. + +Díky tomu má potlačení skutečnou váhu. Když napíšete `// dresscode:ignore` nebo pravidlo pro danou cestu vypnete, nezmizí jen hláška ve výpisu, ale opravdu se neprovede ani oprava. Nespoléhá se přitom na dobrou vůli autora pravidla: pravidlo, které by opravovalo i potlačené místo, neprojde vlastním testem. + + +Presety: PER, PSR-12 a Nette +============================ + +DressCode nemá vlastní názor na styl kódu. Má katalog pravidel a jejich voleb, a **preset** je pojmenovaná sada pravidel s volbami, která dohromady dává jeden coding standard. Vestavěné jsou tři: + +| preset | co je to | +|---|---| +| `dresscode/psr12` | [PSR-12 |https://www.php-fig.org/psr/psr-12/], oddíl po oddílu | +| `dresscode/per` | [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/] v plném rozsahu, nástupce PSR-12 a výchozí volba | +| `dresscode/nette` | [Nette Coding Standard |contributing:coding-standard], tedy PER s tabulátory a několika odchylkami | + +Kde se pravidlo se specifikací rozchází, dostane volbu, ne výjimku schovanou v presetu. Tytéž volby má tedy k dispozici i váš vlastní preset. + +Presety se skládají jako vrstvy: potomek přebírá rodiče a přepisuje celé položky, vaše konfigurace je poslední vrstva nad nimi. Podrobně to popisuje stránka [Konfigurace |configuration#Presety a vrstvy]. + + +Bez priorit: pravidla běží do ustálení +====================================== + +Když dvě pravidla sahají na totéž místo, záleží na pořadí. PHP CS Fixer to řeší ručně udržovanými prioritami: většina jeho fixerů nese číslo od −100 do 100 a v komentáři prózu o tom, po kom musí běžet. Kdo si píše vlastní fixer, to číslo hádá. + +DressCode priority nemá. Pustí všechna pravidla ve třech fázích (nejdřív strukturní změny, pak formátování, nakonec úklid bílých znaků), a když některé strom změnilo, pustí je znovu, a tak dokud se strom nepřestane měnit. Od pravidla to vyžaduje dvě vlastnosti: musí být **idempotentní**, tedy nad vlastním výstupem už nesmí nic měnit, a nesmí záviset na pořadí průchodu. Obojí se dá otestovat, na rozdíl od čísla priority, které se dá jen hádat. A kdyby se dvě pravidla přetahovala, jádro to pozná podle toho, že se strom vrátil do stavu, ve kterém už jednou byl, a vypíše, která pravidla to byla. + +Jeden důsledek stojí za zapamatování: **co běh ohlásí, závisí na tom, která pravidla máte zapnutá.** Pravidlo, které tvar kódu opraví dřív, sebere hlášení pravidlu, které by na něj narazilo později. + + +Bílé znaky mají jednoho vlastníka +================================= + +Mezery, zalomení řádků a prázdné řádky jsou v každém nástroji na styl zdrojem sporů: jedno pravidlo chce mezeru za čárkou, druhé zarovnává sloupce, třetí láme dlouhý řádek. V DressCode proto **žádné pravidlo bílé znaky nepřepisuje**. Místo toho vysloví požadavek: mezi tímhle a tamtím tokenem má být jedna mezera, nebo zalomení řádku, nebo dva prázdné řádky. Jádro požadavky posbírá, rozhodne, opraví a hlášení vypíše pod jménem pravidla, jehož požadavek vyhrál. + +Pro vás z toho plyne: + +- Hlášení o mezeře nese vždy jméno jednoho konkrétního pravidla, i když se toho místa dotýká víc pravidel. Právě to jméno pak nastavíte nebo vypnete. +- Dvě pravidla, která by chtěla rozhodovat o téže mezeře, jsou chyba konfigurace, na kterou nástroj upozorní při startu. Stát se to může jen s pluginem; vestavěná pravidla se nekříží. +- Odsazení řádků má jediného vlastníka, pravidlo `indentation`, které ho odvozuje ze stromu, a ne z toho, jak byl odsazený řádek nad ním. Jeden špatně odsazený řádek proto nestrhne řádky pod sebou. + +Kdo píše vlastní pravidlo tohohle druhu, najde podrobnosti na stránce [Pravidla pro bílé znaky |whitespace-rules]. + + +Verze PHP je vlastnost projektu +=============================== + +Pravidlo, které zapisuje syntaxi novějšího PHP (třeba `0o755` z PHP 8.1), se nesmí zapnout v projektu, který na takové verzi ještě neběží. DressCode proto cílovou verzi bere z projektu, ne z interpretu, na kterém běží on sám: z klíče `phpVersion` v konfiguraci, jinak z `require.php` v `composer.json`, jinak předpokládá PHP 8.0 jako nejnižší verzi, pro kterou se dá psát. Stejný soubor tak dostane stejný verdikt na jakémkoli počítači. Pravidlo pro novější syntaxi se pod svou verzí samo vynechá, takže to preset ani vy hlídat nemusíte. + + +Co si z toho odnést +=================== + +- Pravidla zapínáte a nastavujete jménem, které vidíte ve výpisu. Nic jiného než jméno a volby k nastavení není. +- Na pořadí pravidel v konfiguraci nezáleží. +- Když nějaké pravidlo vypnete, může se objevit hlášení jiného, které se k tomu místu dosud nedostalo. Je to totéž místo v kódu, jen ho teď hlásí někdo jiný. +- Výjimka pro cestu i pro řádek vypíná i opravu, ne jen hlášku. + + +Slovníček +========= + +Pojmy, které se v dokumentaci opakují a stojí za to je mít v ruce. Anglický název je uvedený proto, že se objevuje ve jménech tříd, v konfiguraci i v hláškách nástroje. + +| pojem | anglicky | co to je | +|---|---|---| +| pravidlo | rule | třída, která hlídá jednu vlastnost kódu, a obvykle ji umí i opravit; má jméno tvaru `dresscode/no-empty-comment` | +| porušení | violation | jeden nález pravidla: soubor, řádek, sloupec a zpráva | +| preset | preset | pojmenovaná sada pravidel s volbami, dohromady jeden coding standard | +| rozšíření | extension | balíček, který se přihlásí do konfigurace a přinese vlastní pravidla a presety | +| potlačení | suppression | vypnutí pravidla na jednom řádku, v bloku nebo v celém souboru komentářem `dresscode:ignore` | +| baseline | baseline | soupis porušení, která v projektu jsou dnes a nemají se hlásit | +| jádro | engine | ta část nástroje, která pouští pravidla, rozhoduje spory a skládá výstup | +| průchod | pass | jedno projití stromu všemi pravidly jedné fáze | +| fáze | stage | `Structure`, `Formatting` a `Cleanup`; průchody jdou v tomto pořadí | +| token | token | nejmenší kus zdrojáku: klíčové slovo, závorka, jméno, operátor | +| uzel | node | prvek stromu, jedna konstrukce jazyka; `IfNode`, `ClassNode`, `ArrayNode` | +| slot | slot | pojmenované místo v uzlu, ve kterém sedí token nebo další uzel; `IfNode` má sloty `cond` a `body` | +| trivia | trivia | bílé znaky a komentáře; nejsou uzly, visí na tokenech | +| mezera mezi tokeny | gap | místo mezi dvěma sousedními tokeny, ve kterém můžou být mezery, konce řádků i komentáře | +| požadavek | claim | co pravidlo pro bílé znaky v takové mezeře žádá, místo aby ji přepsalo samo | +| fixtura | fixture | dvojice souborů s kódem před opravou a po ní, na které se pravidlo testuje | +| bezztrátový strom | lossless CST | strom, ve kterém nechybí ani mezera, takže po vytištění dá původní soubor bajt po bajtu | diff --git a/dresscode/cs/migration.texy b/dresscode/cs/migration.texy new file mode 100644 index 0000000000..b8f1d8524f --- /dev/null +++ b/dresscode/cs/migration.texy @@ -0,0 +1,92 @@ +Přechod na DressCode +******************** + +.[perex] +Stará jména pravidel i potlačovací komentáře fungují dál, konfiguraci převede jeden příkaz a vypíše, co se nepodařilo přenést. Společný postup pro přechod z jakéhokoli nástroje a poctivý seznam toho, co bude jinak. + + +Proč to nebolí +============== + +Vím, jak to vypadá: máte konfiguraci, ve které jsou roky ladění, v kódu stovky komentářů `phpcs:ignore`, možná pár vlastních sniffů a k tomu CI, které to celé spouští. Nástroj se nemění proto, že je nový hezčí. Mění se, když přechod nebolí, a přesně tak je DressCode postavený. + +Klíčová věc: **DressCode zná svá pravidla nejen pod vlastním jménem, ale i pod tím, jak se jmenují v PHP CS Fixeru, PHP_CodeSniffer a Slevomatu.** Když napíšete `dresscode rules`, uvidíte u každého pravidla cizí jména, která pokrývá. Z toho plyne všechno ostatní: komentáře v kódu fungují beze změny a konfigurace se dá převést strojově. + +Postup má tři kroky a každý z nich se dá vrátit. + + +1. Kód nechte, jak je +===================== + +Komentáře `// phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, `phpcs:ignoreFile` i anotace `@phpcsSuppress` dělají dál to, co dělaly. DressCode je přečte, cizí jméno pravidla si přeloží na své, a potlačení platí. Do kódu tedy zatím vůbec nemusíte sahat a první kontrolu můžete pustit hned: + +```shell +dresscode check src tests +``` + +Bez konfigurace platí preset `dresscode/per`, tedy [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/]. Kdo dosud používal `@PER-CS` nebo `PSR12`, uvidí zhruba to, co čekal. Kdo měl doladěno nad rámec standardu, uvidí rozdíly, které vyřeší další krok. + + +2. Přeložte konfiguraci +======================= + +Příkaz `import` přečte cizí konfiguraci a vypíše její ekvivalent pro DressCode: + +```shell +dresscode import phpcs.xml > dresscode.php +dresscode import .php-cs-fixer.dist.php > dresscode.php +``` + +`phpcs.xml` je XML, takže se přečte bez čehokoli dalšího. Naproti tomu `.php-cs-fixer.dist.php` je PHP, které se musí spustit a které vrací objekt cizí knihovny; `import` na něm funguje jen v projektu, kde je ta knihovna ještě nainstalovaná. Proto konfiguraci překládejte dřív, než starý nástroj odeberete. + +Na standardní výstup jde hotová konfigurace, na chybový výstup to, co se přenést nepodařilo: + +/--pre .[terminal] +<?php declare(strict_types=1); + +use DressCode\Config; + +return Config::create() + ->preset('dresscode/psr12') + ->enable('dresscode/line-length', ['limit' => 100]) + ->enable('dresscode/unused-imports', ['searchAnnotations' => false]); + +Read 4 rules, enabled 2 and 1 preset. + No DressCode rule covers Squiz.Commenting.FunctionComment. +\-- + +Druhý seznam je stejně cenný jako první: říká, kde se musíte rozhodnout. Pravidlo bez protějšku buď nepotřebujete, nebo si ho [napíšete |porting-rules]. Sady jako `@PSR12` nebo `PSR12` se překládají na presety; u sad, které protějšek nemají (třeba `@PhpCsFixer`), to `import` ohlásí a doporučí začít od `dresscode/per`. + +Přeložené volby s sebou nesou i hodnoty (`lineLimit` se stane `limit`), ale ne každá cizí volba protějšek má; i to `import` vypíše. Výsledný soubor si projděte, je krátký. + + +3. Přepište komentáře +===================== + +Komentáře v kódu fungují i se starými jmény, nové jsou ale kratší a čitelnější. Přepis udělá jeden příkaz: + +```shell +dresscode migrate-suppressions src tests +``` + +Z `phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses` se stane `dresscode:ignore dresscode/unused-imports`, z `phpcs:disable` a `phpcs:enable` se stanou `dresscode:disable` a `dresscode:enable`, z `phpcs:ignoreFile` pak `dresscode:ignore-file`. Jméno, které žádné pravidlo nepokrývá, zůstane, jak bylo, a příkaz ho vypíše. Z toho výpisu máte seznam potlačení, která už nic nepotlačují. + + +První oprava +============ + +Až konfigurace sedí, pusťte `dresscode fix` a výsledek commitněte samostatně, bez jiných změn. I u pravidla, které je "stejné", může oprava vyjít o mezeru jinak než u starého nástroje, a procházet to řádek po řádku nemá smysl. Smysl má vědět, že v tom commitu není nic jiného. U velkého projektu, kde by byl takový commit neúnosný, použijte [baseline |suppressing#Baseline]: dnešní porušení se zapíšou a hlásit se budou jen nová. + + +Co bude jinak +============= + +Návod, který slibuje bezešvý přechod, ztratí důvěru u prvního rozdílu, tak raději rovnou: + +- **Priority neexistují.** Kdo si výsledek stavěl na pořadí fixerů, dostane u některých souborů jiný tvar kódu. Pravidla tu běží opakovaně, dokud se výsledek neustálí; [proč |how-it-works#Bez priorit: pravidla běží do ustálení]. +- **Pravidla bez protějšku** vypíše `import` i `migrate-suppressions`. Není jich málo, hlavně mezi sniffy o dokumentaci a mezi pravidly, kterými se nástroje navzájem překrývají. +- **Rizikové (risky) pravidlo tu rizikové být nemusí.** Co PHP CS Fixer zapínal jen na výslovnou žádost, protože nad tokeny nerozeznal bezpečný případ od nebezpečného, rozhoduje tady strom, a pravidlo opraví jen ty bezpečné. Naopak pravidlo, které mění chování programu ze své podstaty (`strict-comparison` dělá z `==` `===`), zůstává vaším rozhodnutím, ať ho zapnete kdekoli. +- **Exit kódy jsou jiné**: `0` čisto, `1` porušení, `2` selhání nástroje. PHP_CodeSniffer i PHP CS Fixer mají vlastní stupnice, takže skript, který je vyhodnocuje, potřebuje jednu úpravu. +- **Jeden nástroj místo dvou.** Kdo kombinoval CodeSniffer a Fixer, měl dvě konfigurace a dva kroky v CI. Teď je jedna a jeden. + +Podrobnosti pro konkrétní nástroj: [PHP CS Fixer |from-php-cs-fixer], [PHP_CodeSniffer a Slevomat |from-phpcs], [Nette Coding Standard |from-ncs]. diff --git a/dresscode/cs/php-api.texy b/dresscode/cs/php-api.texy new file mode 100644 index 0000000000..89f098f390 --- /dev/null +++ b/dresscode/cs/php-api.texy @@ -0,0 +1,115 @@ +PHP API +******* + +.[perex] +Jak DressCode spustit z vlastního kódu: načtení konfigurace, `Runner`, výsledky běhu a vlastní reporter, který si výstup zpracuje po svém. + + +Kdy sáhnout po API +================== + +Příkazová řádka pokryje průběžnou integraci, Git hooky i editory. API potřebujete tehdy, když DressCode zapojujete do vlastního nástroje: do generátoru, který má vyrobený kód rovnou naformátovat, do migračního skriptu, do služby, která kontroluje kód z formuláře, nebo když chcete výsledky v podobě, kterou žádný z vestavěných formátů nedává. + + +Běh nad projektem +================= + +Konfigurace se načte stejně jako z příkazové řádky, z ní se postaví `Runner` a ten dostane seznam souborů a reporter: + +```php +use DressCode\Config\Loader; +use DressCode\Config\RunnerFactory; +use DressCode\Reporters\JsonReporter; + +[$config, $root] = new Loader()->load(file: null, directory: getcwd()); +$runner = new RunnerFactory()->createRunner($config, $root); + +$files = $runner->findFiles(['src', 'tests']); +$result = $runner->run($files, fix: false, reporter: new JsonReporter(STDOUT)); + +exit($result->getExitCode()); +``` + +`Loader::load()` najde `dresscode.neon` nebo `dresscode.php` od zadaného adresáře směrem nahoru (nebo vezme soubor, který mu určíte) a vrátí konfiguraci i s vrstvami rozšíření a kořenový adresář projektu; bez konfiguračního souboru platí výchozí preset, nebo konfigurace, kterou předáte třetím argumentem. `Runner::findFiles()` rozvine cesty podle klíčů `paths`, vyloučení a přípon z konfigurace, `run()` je zpracuje a výsledky posílá reporteru soubor po souboru. + +.[caution] +`RunnerFactory` je zatím označená `@internal`, takže se její podoba může v minoritní verzi změnit. Je to jediné hrubé místo tohohle API a zároveň jediná cesta, jak `Runner` postavit. Komu to vadí, ať volá rovnou [celý příkaz |#Vlastní příkaz], jehož rozhraní stabilní je. + +`RunResult` obsahuje `FileResult` pro každý soubor a k tomu souhrnná čísla: `countViolations()`, `countFixable()`, `countChangedFiles()`, `countErrors()` (soubory, které nejdou parsovat), `countFailures()` (soubory, kde pravidlo selhalo) a `getExitCode()` podle stejných pravidel jako na příkazové řádce. + + +Jeden soubor nebo řetězec +========================= + +Pro kód, který neleží na disku, nebo pro jeden soubor bez hledání: + +```php +$result = $runner->processFile('src/Cart.php', $code); + +foreach ($result->violations as $violation) { + echo "$violation->line: $violation->message ($violation->ruleName)\n"; +} + +$fixed = $result->output; +``` + +`processFile()` nikdy nezapisuje. Cesta říká, která pravidla pro kód platí (podle ní se vyhodnotí výjimky pro cesty), `$result->output` je opravený kód a `$result->isChanged()` řekne, jestli se od vstupu liší. Naproti tomu `processPath()` soubor přečte a s `fix: true` i zapíše. Ani jedno nepoužívá cache. + +`FileResult` nese cestu, původní kód, výstup, seznam objektů `Violation` (pravidlo, zpráva, řádek, sloupec, závažnost, jestli bylo opraveno, otisk pro baseline), varování a případnou chybu parsování nebo selhání pravidla. + + +Vlastní reporter +================ + +Reporter je rozhraní se třemi metodami. Výsledky mu chodí v pořadí vstupu, shrnutí na konci: + +```php +use DressCode\FileResult; +use DressCode\Reporter; +use DressCode\RunResult; + +final class CountingReporter implements Reporter +{ + private array $byRule = []; + + + public function start(int $fileCount, bool $fix): void + { + } + + + public function reportFile(FileResult $result): void + { + foreach ($result->violations as $violation) { + $this->byRule[$violation->ruleName] = ($this->byRule[$violation->ruleName] ?? 0) + 1; + } + } + + + public function finish(RunResult $result): void + { + arsort($this->byRule); + foreach ($this->byRule as $rule => $count) { + printf("%5d %s\n", $count, $rule); + } + } +} +``` + +Takový reporter po prvním běhu nad starým projektem řekne, která tři pravidla dělají devadesát procent všech porušení, a to je přesně informace, podle které se rozhoduje, co vypnout a co opravit. Vestavěné reportery (`ConsoleReporter`, `JsonReporter`, `CheckstyleReporter`, `GithubReporter`) jsou dobrý vzor k nahlédnutí. + + +Vlastní příkaz +============== + +Kdo balí DressCode do vlastní binárky (tak vznikl příkaz `ecs` v Nette Coding Standardu), spustí v procesu `Console\Application` a dá jí konfiguraci, která platí, když projekt žádnou vlastní nemá: + +```php +use DressCode\Config; +use DressCode\Console\Application; + +$application = new Application(defaultConfig: Config::create()->extension(Nette\CodingStandard\Extension::class)); +exit($application->run($argv)); +``` + +Všechno ostatní, tedy příkazy, přepínače, formáty i paralelní běh, zůstává na příkazové řádce. diff --git a/dresscode/cs/porting-rules.texy b/dresscode/cs/porting-rules.texy new file mode 100644 index 0000000000..ac21132640 --- /dev/null +++ b/dresscode/cs/porting-rules.texy @@ -0,0 +1,72 @@ +Přepis pravidla z jiného nástroje +********************************* + +.[perex] +Jak přepsat sniff z PHP_CodeSniffer nebo fixer z PHP CS Fixeru: specifikaci vzít z testů a ne ze zdrojáku, přeložit otázky z tokenů na strom a zahodit obranný kód, který nad stromem nemá co dělat. + + +Pro koho to je +============== + +Máte vlastní sniff (pravidlo PHP_CodeSniffer) nebo fixer (pravidlo PHP CS Fixeru), který léta hlídá něco, co žádný standard neumí, a chcete ho mít i tady. Tenhle postup je napsaný tak, aby se podle něj dalo pracovat krok za krokem, a stejně dobře ho zvládne agent, kterému cizí pravidlo předložíte. Většinu vestavěných pravidel DressCode ostatně takhle přepsali agenti. + +Nejdřív si ale ověřte, jestli přepis vůbec potřebujete: `dresscode rules` u každého pravidla vypíše jména z PHP CS Fixeru, PHP_CodeSniffer a Slevomatu, která pokrývá. Přepisujte jen to, co v tom seznamu není. + + +1. Specifikaci vezměte z testů, ne ze zdrojáku +============================================== + +Zdrojový kód cizího pravidla popisuje, jak se ta věc obchází nad plochým polem tokenů: kde se couvá, co se počítá, kdy to pravidlo vzdá. Přesně tahle informace je tady k ničemu. Co cenu má, jsou **testovací případy**: dvojice kódu před opravou a po ní a hraniční případy, které autor za roky nasbíral. Ty přeneste skoro doslova do [fixtur |testing-rules]. Fixtura je vaše zadání a zároveň důkaz, že jste cestou nic neztratili. + + +2. Přeložte otázky z tokenů na strom +==================================== + +Každý cyklus přes tokeny je v originále ve skutečnosti otázka na strukturu kódu. Tenhle překlad se opakuje pořád dokola: + +| v originále | v DressCode | +|---|---| +| `getPrevMeaningfulToken()` v cyklu, dokud se nenajde začátek výrazu | slot uzlu: `$node->cond`, `$node->args`, nebo `$node->parent` | +| ruční počítání závorek, aby se našel konec bloku | sloty `openParen` a `closeParen`, `openBrace` a `closeBrace` | +| `T_STRING` a hádání z kontextu, co to vlastně je | konkrétní třída uzlu: `NameNode`, `IdentifierNode`, `FunctionCallNode` | +| vlastní pomocník na "je to globální funkce" a "v jakém jsme jmenném prostoru" | `NameResolver::isGlobalFunctionCall()`, `getNamespace()`, `resolveClass()` | +| vlastní pomocník na "jsme uvnitř metody" a "je tu `$this`" | `Scope::getFunction()`, `getClass()`, `hasThis()` | +| porovnání dvou úseků tokenů | `Node::matches()` | +| kontrola, že v úseku není `++`, volání a podobně | `Node::isRepeatableRead()` | +| hledání komentáře mezi tokeny | `Token::hasCommentUpTo()`, `Node::hasComment()` | +| `$phpcsFile->addFixableError()` a pak `$fixer->replaceToken()` | `report()` v podmínce a teprve za ní zápis do slotu nebo `replaceWith()` | +| `Tokens::insertAt()` s ručně sestavenými tokeny | `Parser::parseExpression()` nebo `parseStatement()` a `insert()` do seznamu | + +Kde originál pracoval s indexy tokenů, pracujte s uzly; kde četl text, ptejte se stromu. Které uzly a sloty existují, říká [přehled uzlů |phpsyntax:nodes]. + + +3. Zahoďte obranný kód +====================== + +Velká část cizího pravidla existuje jen proto, aby se nespletlo: aby `[` nebylo přístupem k prvku místo pole, aby se nepočítala závorka uvnitř řetězce, aby komentář uprostřed volání nerozbil hledání. Nad stromem taková možnost nevzniká, takže ten kód nemá co přenášet. Pokušení překládat originál řádek po řádku je silné, zvlášť pro agenta, a proti němu stojí jednoduchá zkouška: **každý řádek nového pravidla musí odpovídat na otázku o kódu, ne o tokenech.** Řádek, který řeší, co všechno může stát mezi dvěma tokeny, jde ven. + +Výsledek bývá několikanásobně kratší. Pravidlo, které má po přepisu přes sto řádků, je podezřelé: buď dělá víc věcí najednou a má se rozdělit, nebo v něm zůstal překlad místo přepisu. + + +4. Bezpečnost řešte jinak než originál +====================================== + +Originál často hlídal vedlejší účinky výčtem: `T_INC`, `T_DEC`, možná volání. Tady je na to `isRepeatableRead()`, které je přísnější i přesnější. Pravidlo, které bylo v PHP CS Fixeru označené jako risky (rizikové), po přepisu risky být nemusí, pokud se ptá stromu. Pravidlo, které mění význam programu ze své podstaty, zůstane rozhodnutím uživatele a jeho stránka to má říct. + + +5. Jméno a stará jména +====================== + +Nové pravidlo dostane jméno podle konvence (`vendor/slug`, stav, ne krok). Jméno původního sniffu nebo fixeru si ale uživatelé ponesou v komentářích `phpcs:ignore`. Cizí jména překládá DressCode jen pro vestavěná pravidla, takže u vlastního pravidla je v kódu nahraďte: `migrate-suppressions` vypíše, která jména nezná, a to je přesně ten seznam. + + +6. Ověřte na cizích fixturách i na vlastních +============================================ + +Prošly cizí testovací případy? Pak přidejte to, co v nich chybělo, protože strom vidí víc: komentář uvnitř konstrukce, konstrukci přes několik řádků, alternativní syntaxi, kód prokládaný kusy HTML. Nakonec pusťte `fix` nad větším cizím kódem a podívejte se na diff. + + +Licence +======= + +Přenášíte chování a testovací případy, ne kód. PHP_CodeSniffer je pod licencí BSD-3-Clause, PHP CS Fixer a Slevomat pod MIT; obojí dovoluje odvozenou práci s uvedením autorství. Fixtury převzaté z cizího projektu označte v hlavičce souboru původem a licencí. Pravidlo napsané podle tohoto postupu cizí kód neobsahuje, protože z něj nakonec nezbylo co přenášet. diff --git a/dresscode/cs/presets-and-extensions.texy b/dresscode/cs/presets-and-extensions.texy new file mode 100644 index 0000000000..93e4075cb8 --- /dev/null +++ b/dresscode/cs/presets-and-extensions.texy @@ -0,0 +1,125 @@ +Vlastní preset a rozšíření +************************** + +.[perex] +Jak zabalit vlastní pravidla a styl do presetu, jak z něj udělat rozšíření (extension), rozdávat ho přes Composer a nechat uživatele zapnout ho jediným řádkem. + + +Preset +====== + +Preset je třída s atributem `#[PresetInfo]`, která vrátí sadu pravidel s volbami a může vycházet z jiných presetů: + +```php +namespace Acme\CodeStyle; + +use DressCode\Preset; +use DressCode\PresetContext; +use DressCode\PresetInfo; +use DressCode\Presets\Per; + +#[PresetInfo('acme/house', 'The Acme house style', indent: "\t", eol: "\n")] +final class HousePreset implements Preset +{ + public function getRules(PresetContext $context): array + { + return [ + 'dresscode/ordered-imports' => true, + 'dresscode/line-length' => ['limit' => 100], + 'dresscode/no-alternative-syntax' => false, + ExceptionMessagePeriodRule::class => true, + 'dresscode/octal-notation' => version_compare($context->getPhpVersion(), '8.1', '>='), + ]; + } + + + public function getParents(): array + { + return [Per::class]; + } +} +``` + +- Rodičovské presety se použijí nejdřív. Potomek pak přepisuje celé položky, takže se volby nikdy neslučují. Pořadí pravidel odpovídá pořadí první zmínky. +- Vestavěná pravidla lze uvést jménem, vlastní názvem třídy; jméno vlastního pravidla se zaregistruje při první zmínce. +- Hodnotou je `true`, `false`, mapa voleb, nebo továrna `fn(): Rule` pro pravidlo se závislostmi. +- `indent` a `eol` v atributu `PresetInfo` je styl, se kterým preset počítá; konfigurace projektu ho může přepsat. + +Poslední řádek ukázky je vlastně zbytečný: pravidlo s `minPhpVersion` se pod svou verzí vynechá samo. `PresetContext` se hodí spíš tam, kde má preset pod různými verzemi PHP zapínat různá pravidla nebo jim dávat jiné volby. Verze je řetězec ve tvaru `major.minor` a porovnává se funkcí `version_compare()`. + +Preset se dá zapnout názvem třídy, dokud nemá rozšíření, které mu dá jméno: + +```neon +presets: + - Acme\CodeStyle\HousePreset +``` + + +Rozšíření +========= + +Rozšíření (extension) je to, co balíček dodá místo konfiguračního souboru: třída s metodou `__invoke()`, která dostane prázdný objekt `Config` a nastaví do něj, co uzná za vhodné. Takhle vypadá rozšíření Nette Coding Standardu: + +```php +namespace Nette\CodingStandard; + +use DressCode\Config; + +final class Extension +{ + public function __invoke(Config $config): void + { + $config + ->registerPresets([Presets\CleanCode::class, Presets\OptimizeFn::class, Presets\Types::class]) + ->excludePaths(['expected', 'tmp', 'fixtures*']) + ->skipWhen(PhpVersionFilter::create()); + } +} +``` + +Všimněte si, co tam **není**: žádné volání `preset()`. Rozšíření dělá jména dostupnými a nastavuje výchozí hodnoty, ale který styl se nakonec použije, je rozhodnutí projektu. Uživatel rozšíření zapne jedním řádkem a presety si vybere sám: + +```neon +extensions: + - Nette\CodingStandard\Extension + +presets: + - dresscode/nette + - nette/clean-code +``` + +Rozšíření může nastavit cokoli, co umí `Config`: zaregistrovat pravidla (`registerRules()`) a presety (`registerPresets()`), zapnout preset, přidat vyloučené cesty, přípony souborů, filtr `skipWhen` podle obsahu souboru nebo analýzu s továrnou. Všechno, co nastaví, je vrstva **pod** konfigurací projektu, takže to projekt může přepsat. Výjimkou jsou vyloučené cesty, které se jen sčítají. Na pořadí rozšíření v konfiguraci nezáleží a rozšíření, které zapnulo jiné rozšíření, se použije jen jednou. + +Anonymní funkce patří sem, ne do NEONu: filtr `skipWhen`, továrna pravidla se závislostmi, továrna analýzy. NEON deklaruje, rozšíření implementuje. + + +Balíček +======= + +Balíček s presetem nebo s pravidly je obyčejný Composer balíček, který vyžaduje `dresscode/dresscode` a sdílí s projektem autoloader: + +```json +{ + "name": "acme/code-style", + "require": { + "dresscode/dresscode": "^1.0" + }, + "autoload": { + "psr-4": {"Acme\\CodeStyle\\": "src/"} + } +} +``` + +Uvnitř jsou třídy pravidel, presety, rozšíření a fixtury s testy. Jména pravidel nesou vendor balíčku (`acme/…`), aby se nesrazila s jinými. A kdo chce, aby uživatel po `composer require` nemusel psát ani řádek `extensions`, přidá do `composer.json` balíčku záznam, podle kterého si DressCode rozšíření najde sám: + +```json +{ + "extra": { + "dresscode": { + "extensions": ["Acme\\CodeStyle\\Extension"] + } + } +} +``` + +Automaticky nalezené rozšíření se chová stejně jako ručně zapsané. U každého pravidla pak `dresscode rules` řekne, odkud přišlo. diff --git a/dresscode/cs/presets-reference.texy b/dresscode/cs/presets-reference.texy new file mode 100644 index 0000000000..32263303ea --- /dev/null +++ b/dresscode/cs/presets-reference.texy @@ -0,0 +1,278 @@ +Presety PER, PSR-12 a Nette +*************************** + +.[perex] +Co přesně zapíná každý vestavěný preset: pravidla i s volbami, které nastavuje jinak než výchozí, styl odsazení a konců řádků a preset, ze kterého vychází. Pravidlo bez uvedených voleb běží s výchozími hodnotami ze své stránky. + +Preset se skládá z rodiče a vlastních položek: potomek přebírá rodiče celého a položku, kterou uvede znovu, přepisuje celou. Vaše konfigurace je pak další taková vrstva, viz [Konfigurace |configuration#Presety a vrstvy]. + + +`dresscode/per` +=============== + +PER Coding Style 3.1, vychází z `dresscode/psr12`, odsazení 4 mezerami, konec řádku LF. Pravidel celkem: 58. + +- `attribute-after-phpdoc` +- `attribute-position` +- `attribute-spacing` +- [binary-operator-spacing |rules/binary-operator-spacing] +- [braces-position |rules/braces-position] `allowSingleLineAnonymousFunctions: false, emptyBodies: sameLine` +- `cast-canonical-type` +- `cast-spacing` +- `class-definition-spacing` `spaceBeforeParenthesis: false` +- `comma-spacing` `tabAlignment: false` +- `concat-spacing` +- `constant-casing` +- `construct-spacing` +- `continuation-position` +- `control-structure-braces` +- [declaration-blank-lines |rules/declaration-blank-lines] `betweenFunctions: null, betweenFunctionsInInterface: null, betweenMembers: null, beforeDocumentedMember: null, afterPhpdoc: null` +- `declare-spacing` +- `elseif-keyword` +- `eof-newline` +- `fall-through-comment` +- `full-opening-tag` +- `function-name-spacing` +- `header-blank-lines` +- `heredoc-indentation` +- `indentation` +- `keyword-casing` +- `line-ending` +- `multi-line-array` +- `multi-line-call` +- `multi-line-chain` +- `multi-line-condition` +- `multi-line-signature` `promotedProperties: false` +- `multi-line-ternary` +- `name-casing` `classes: PascalCase, methods: camelCase, constants: UPPER_CASE, enumCases: PascalCase` +- `named-argument-spacing` +- `new-argument-parentheses` `namedClasses: required, anonymousClasses: forbidden` +- `no-byte-order-mark` +- `no-closing-tag` +- `no-leading-backslash-in-import` +- `no-trailing-whitespace` +- `nowdoc-without-interpolation` +- `ordered-imports` `alphabetically: false` +- `ordered-members` `order: [use_trait]` +- `parentheses-spacing` +- `reference-spacing` +- `semicolon-spacing` `after: null` +- `short-array-syntax` +- `single-member-per-declaration` `members: [property, trait]` +- `single-member-per-line` +- `single-statement-per-line` +- `spread-operator-spacing` +- `switch-case-colon` +- `switch-case-spacing` +- `ternary-operator-spacing` +- [trailing-comma |rules/trailing-comma] `multiLine: [arrays, arguments, parameters, match, closureUses]` +- `type-hint-spacing` +- `unary-operator-spacing` +- `useless-attribute-parentheses` +- `visibility-required` + + +`dresscode/psr12` +================= + +PSR-12 Extended Coding Style, odsazení 4 mezerami, konec řádku LF. Pravidel celkem: 44. + +- [binary-operator-spacing |rules/binary-operator-spacing] +- [braces-position |rules/braces-position] `allowSingleLineAnonymousFunctions: false` +- `cast-canonical-type` +- `cast-spacing` +- `class-definition-spacing` +- `comma-spacing` `tabAlignment: false` +- `constant-casing` +- `construct-spacing` +- `continuation-position` +- `control-structure-braces` +- [declaration-blank-lines |rules/declaration-blank-lines] `betweenFunctions: null, betweenFunctionsInInterface: null, betweenMembers: null, beforeDocumentedMember: null, afterPhpdoc: null` +- `declare-spacing` +- `elseif-keyword` +- `eof-newline` +- `fall-through-comment` +- `full-opening-tag` +- `function-name-spacing` +- `header-blank-lines` +- `indentation` +- `keyword-casing` +- `line-ending` +- `multi-line-call` +- `multi-line-condition` +- `multi-line-signature` `promotedProperties: false` +- `name-casing` `classes: PascalCase, methods: camelCase, constants: UPPER_CASE` +- `new-argument-parentheses` `anonymousClasses: null` +- `no-byte-order-mark` +- `no-closing-tag` +- `no-leading-backslash-in-import` +- `no-trailing-whitespace` +- `ordered-imports` `alphabetically: false` +- `ordered-members` `order: [use_trait]` +- `parentheses-spacing` +- `reference-spacing` +- `short-array-syntax` +- `single-member-per-declaration` `members: [property, trait]` +- `single-statement-per-line` +- `spread-operator-spacing` +- `switch-case-colon` +- `switch-case-spacing` +- `ternary-operator-spacing` +- `type-hint-spacing` `catchTypes: single` +- `unary-operator-spacing` +- `visibility-required` + + +`dresscode/nette` +================= + +Nette Coding Standard, vychází z `dresscode/per`, odsazení tabulátorem, konec řádku podle souboru. Pravidel celkem: 148. + +- `annotation-name` +- `array-spacing` +- `arrow-function` +- `attribute-after-phpdoc` +- `attribute-position` +- `attribute-spacing` +- [binary-operator-spacing |rules/binary-operator-spacing] +- `body-blank-lines` +- [braces-position |rules/braces-position] `multiLineParameters: nextLineAfterReturnType` +- `cast-canonical-type` +- `cast-spacing` +- `class-definition-spacing` +- `class-reference-name-casing` +- `combined-assignment-operator` +- `combined-issets` +- `combined-unsets` +- `comma-spacing` +- `comment-spacing` +- `commented-out-function` `functions: [print_r, var_dump, var_export, dump]` +- `complex-string-variable` +- `concat-spacing` +- `constant-casing` +- `construct-spacing` +- `continuation-position` +- `control-structure-braces` +- [declaration-blank-lines |rules/declaration-blank-lines] +- `declare-spacing` +- `double-colon-spacing` +- `elseif-keyword` +- `eof-newline` +- `explicit-assertion` +- `explicit-operator-precedence` +- `fall-through-comment` `comment: break omitted` +- `forbidden-annotations` `annotations: [@access, @author, @copyright, @created, @license, @package, @since, @subpackage, @todo, @version]` +- `forbidden-phpdoc-lines` `patterns: [~^(?:(?!private|protected|static)\S+ )?(?:con|de)structor\.\z~i, ~^Created by \S+\.\z~i, ~^\S+ [gs]etter\.\z~i]` +- `full-opening-tag` +- `function-name-spacing` +- `header-blank-lines` `beforeNamespace: 1, afterOpeningTag: null, afterNamespace: 1, afterImports: 1, betweenImportGroups: 0, beforeDeclaration: 2` +- `heredoc-indentation` +- `import-notation` `functions: combined, constants: combined, groupUse: keep` +- `increment-operator` +- `indentation` +- `keyword-casing` +- `line-ending` +- `magic-constant-casing` +- `modern-class-name-reference` `onObjects: true` +- `multi-line-array` `oneItemPerLine: false` +- `multi-line-call` +- `multi-line-chain` `leadingLinksOnFirstLine: true` +- `multi-line-condition` +- `multi-line-signature` +- `multi-line-ternary` +- `name-casing` `classes: PascalCase, methods: camelCase, functions: camelCase, constants: PascalCase, enumCases: PascalCase, properties: camelCase, variables: camelCase` +- `named-argument-spacing` +- `native-function-casing` +- `new-argument-parentheses` `namedClasses: forbidden, anonymousClasses: forbidden` +- `no-alias-functions` +- `no-alternative-syntax` +- `no-backtick-operator` +- `no-byte-order-mark` +- `no-closing-tag` +- `no-continue-in-switch` +- `no-conversion-functions` +- `no-deprecated-functions` +- `no-dirname-of-file` +- `no-duplicate-assignment` +- `no-duplicate-return-annotation` +- `no-empty-comment` +- `no-empty-phpdoc` +- `no-empty-statement` +- `no-empty-var-annotation` +- `no-global-keyword` +- `no-hash-comment` +- `no-implicit-backslash` +- `no-inner-functions` +- `no-invisible-characters` +- `no-is-null` +- `no-leading-backslash-in-global-namespace` +- `no-leading-backslash-in-import` +- `no-settype` +- `no-short-bool-cast` +- `no-this-in-static-context` +- `no-trailing-whitespace` +- `no-trailing-whitespace-in-string` +- `no-unknown-param-annotation` +- `no-unpacking-in-optimized-call` +- `no-unreachable-catch` +- `no-yoda-comparison` +- `not-equals-operator` +- `nowdoc-without-interpolation` +- `null-coalescing-operator` +- `nullable-type-for-default-null` +- `numeric-literal-separator` `minDigitsBeforeDecimalPoint: 7, minDigitsAfterDecimalPoint: 20` +- `object-operator-spacing` +- [octal-notation |rules/octal-notation] +- `offset-bracket-spacing` +- `ordered-imports` +- `ordered-members` +- `parentheses-spacing` +- `phpdoc-alignment` +- `phpdoc-canonical-types` `arrayNotation: null` +- `phpdoc-null-last` +- `phpdoc-trim` +- `promoted-property-annotation-position` +- `property-phpdoc-required` +- `property-phpdoc-single-line` +- `property-var-annotation` +- `reference-spacing` +- `reference-throwable-only` +- `reference-used-names-only` +- `self-for-current-class` +- `semicolon-spacing` +- `short-array-syntax` +- `short-list-syntax` +- [short-ternary-operator |rules/short-ternary-operator] +- `single-member-per-declaration` `members: [property, trait]` +- `single-member-per-line` +- `single-quoted-strings` +- `single-statement-per-line` +- `spread-operator-spacing` +- `strict-call` +- `strict-types-required` `placement: openingTagLine` +- `switch-case-colon` +- `switch-case-spacing` +- `symbolic-logical-operators` +- `ternary-operator-spacing` `spacing: single` +- [trailing-comma |rules/trailing-comma] `multiLine: [arrays, arguments, parameters]` +- `type-hint-spacing` +- `unary-operator-spacing` +- [unused-imports |rules/unused-imports] +- `use-from-same-namespace` +- `useless-alias` +- `useless-attribute-parentheses` +- `useless-braces` +- `useless-catch-variable` +- `useless-constant-var-annotation` +- `useless-construct-parentheses` +- `useless-function-phpdoc` +- `useless-if-condition-with-return` +- `useless-inheritdoc` +- `useless-modifier` +- `useless-null-property-initialization` +- `useless-parameter-default` +- `useless-parentheses-around-new` +- `useless-return` +- `useless-string-concat` +- `useless-ternary-operator` +- `visibility-required` diff --git a/dresscode/cs/rule-contract.texy b/dresscode/cs/rule-contract.texy new file mode 100644 index 0000000000..68ea7b575f --- /dev/null +++ b/dresscode/cs/rule-contract.texy @@ -0,0 +1,155 @@ +Pravidlo do detailu +******************* + +.[perex] +Co musí každé pravidlo dodržet: opravovat až po ohlášení, být bez stavu a idempotentní, správně zvolit fázi, vyplnit atribut `RuleInfo`, popsat své volby schématem a hlásit i problémy v bílých znacích. + + +Dvě podoby pravidla +=================== + +Základem je třída `DressCode\Rule` a existují dvě podoby, mezi kterými se vybírá podle toho, co pravidlo dělá: + +- **`NodeRule`** navštěvuje uzly a tokeny, které si vyžádá, a pracuje s nimi v metodách `enter()` a `leave()`. Takhle je psaná většina pravidel a je o nich celá tahle stránka. +- **`GapRule`** nenavštěvuje nic. Jen vysloví požadavek na to, co má být v bílých znacích mezi tokeny, a vyhodnotí to za něj jádro nástroje. Píše se jinak a má [vlastní stránku |whitespace-rules]. + +Třída je vždy jen jedno, nebo druhé. Pravidlo, které by potřebovalo obojí, jsou ve skutečnosti dvě pravidla se dvěma jmény, aby šlo každé vypnout zvlášť a aby bylo z hlášení poznat, které z nich mluví. + + +Tvar pravidla +============= + +Pravidlo dědí od `DressCode\NodeRule` a nese atribut `#[RuleInfo]`: + +```php +#[RuleInfo( + 'acme/no-var-dump', + Stage::Structure, + description: 'Reports calls of var_dump()', + minPhpVersion: null, + modifiesComments: false, +)] +final class NoVarDumpRule extends NodeRule +{ + // ... +} +``` + +- **Jméno** `vendor/slug` je identita pravidla: objevuje se ve výpisu, v konfiguraci, v komentářích `dresscode:ignore` i v baseline. Dvě třídy se stejným jménem jsou chyba konfigurace. +- **Fáze** (`Stage`) říká, ve které ze tří fází průchodu pravidlo běží. `Structure` je pro změny kódu (přepis výrazu, odstranění importu), `Formatting` pro bílé znaky a zalomení řádků, `Cleanup` pro závěrečný úklid (mezery na konci řádků, konec souboru, délka řádku). Průchody jdou v tomhle pořadí, takže formátování už vidí kód po strukturních změnách. +- **Popis** je jedna anglická věta v oznamovacím způsobu o tom, co pravidlo dělá; vypisuje ji `dresscode rules`. +- **`minPhpVersion`** uveďte tehdy, když pravidlo zapisuje syntaxi, která existuje až od nějaké verze PHP (třeba `0o755` od PHP 8.1). Pod tou verzí se pravidlo samo vynechá a preset ho nemusí hlídat. PHP 8.0 je nejnižší podporovaná verze, takže na nic, co v 8.0 už bylo, se ptát nemusíte. +- **`modifiesComments`** nastavte na `true` jen tehdy, když pravidlo opravdu mění text komentářů. Jinak `RuleTester` hlídá, že žádný komentář nezmizel ani se nezměnil, což je nejčastější chyba oprav. + + +Které uzly a kdy +================ + +```php +public function getVisitedTypes(): array +{ + return [FunctionCallNode::class]; +} +``` + +Je to seznam tříd uzlů (nebo `Token::class`), pro které jádro zavolá `enter()` a `leave()`. Porovnává se přes `instanceof`, takže `StatementNode::class` zachytí každý příkaz a `Node::class` úplně všechno; čím užší seznam, tím rychlejší běh. Prázdný seznam znamená, že pravidlo pracuje jen v `beforeFile()` a `afterFile()`, což dělají pravidla nad celým souborem, například to o délce řádku. + +`enter()` se volá při vstupu do uzlu, tedy před jeho dětmi, `leave()` až po nich. Když pravidlo uzel v `enter()` nahradí nebo odstraní, jádro do něj už nesestoupí a `leave()` pro něj nezavolá. + + +Nejdřív ohlásit, teprve pak opravit +=================================== + +Tohle je nejdůležitější pravidlo celého kontraktu: **strom se smí změnit až poté, co `report()` vrátil `true`.** + +```php +if ($context->report($node, 'The var_dump() call must not stay in the code')) { + $node->remove(); +} +``` + +`report()` vrátí `false`, pokud je porušení na svém řádku potlačené komentářem, a pak se nesmí nic měnit. Nespoléhá se přitom na dobrou vůli: strom počítá své změny a jádro každou změnu spáruje s hlášením ze stejného volání. Změna bez hlášení, nebo po hlášení, které vrátilo `false`, je porušený kontrakt. Za běhu je z toho varování, s přepínačem `--strict-rules` a v `RuleTester`u chyba. + +Hlášení se váže na uzel nebo na token. Problém, který leží v bílých znacích nebo v komentáři, ohlaste s příslušnou trivia (`report($token, $message, trivia: $trivia)`), aby porušení dostalo řádek té trivia a `dresscode:ignore` na tom řádku ho našel; jinak spadne na řádek tokenu. Závažnost je `Severity::Error`, nebo `Severity::Warning`; varování se vypíše, ale exit kód neovlivní. + +Zpráva popisuje kód, ne čtenáře, a nikdy nerozkazuje. Má jeden ze tří tvarů: požadovaný stav (`A single space after the comma`), norma (`The opening brace must be on its own line`), nebo nález (`Function foo() is deprecated`). Konkrétní jména a hodnoty do zprávy patří, jméno pravidla ne, to doplní výpis. + + +Bez stavu a idempotentní +======================== + +Jedna instance pravidla slouží celému běhu a všem souborům. Stav vztažený k jednomu souboru patří do pole `$context->storage`, které jádro pro každý soubor založí prázdné; vlastnosti třídy jsou jen na volby. + +Pravidla se pouštějí opakovaně, dokud se strom mění, takže pravidlo musí být **idempotentní**: nad vlastním výstupem už nesmí nic ohlásit ani změnit. A nesmí záviset na pořadí průchodu ani na tom, kolikátý průchod zrovna běží; kontext to schválně neprozradí. Dvě pravidla, která se přetahují, jádro pozná a soubor ohlásí jako selhání se jmény obou. Idempotenci ověřuje i `RuleTester`: pustí pravidlo nad jeho vlastním výstupem podruhé a čeká ticho. + + +Co strom dovolí +=============== + +Slot uzlu se zapisuje přiřazením (`$if->cond = $expr`), uzel se nahrazuje metodou `replaceWith()` a odstraňuje metodou `remove()`, text a trivia tokenu mění jeho vlastní metody. O rodiče a o index tokenů se strom stará sám, a kde na to nemá property hook, hlídá zápis viditelnost vlastnosti; strom tedy nerozbijete ani zápisem mimo API. Podrobnosti jsou na stránce [Úpravy |phpsyntax:mutation]; pro pravidla k tomu platí navíc: + +- Sourozence měňte z callbacku jejich vlastníka (`FileNode`, `BlockNode`, `ClassNode`) nebo z `afterFile()`, ne z `enter()` položky, kterou právě procházíte. Jádro procházený seznam nepřepočítává. +- Novou konstrukci nestavějte z tokenů, ale naparsujte ji: `Parser::parseExpression()`, `parseStatement()`, `parseType()`, `parseName()`. +- Komentář nesmí zmizet, dokud pravidlo neřekne `modifiesComments`. Před zásahem se ptejte `Token::hasComment()`, `Token::hasCommentUpTo()` nebo `Node::hasComment()`; odstraňujte komentář jen přes `removeTrivia()`. +- Konec řádku, který uzavírá řádek tokenu, patří do jeho koncových trivia, ne do úvodních trivia dalšího tokenu. Metody `ensureLeadingNewline()` a `setBlankLinesBefore()` to dělají správně, tak na nich stavějte. +- Na otázky "je tenhle výraz stejný jako tamten" a "dá se bezpečně vyhodnotit dvakrát" odpovídají `Node::matches()` a `Node::isRepeatableRead()`. Neimplementujte je znovu. + + +Volby pravidla +============== + +Pravidlo s volbami implementuje rozhraní `ConfigurableRule`: dodá schéma z knihovny [nette/schema |schema:] a metodu `configure()`, která dostane volby už zvalidované: + +```php +final class ForbiddenFunctionsRule extends NodeRule implements ConfigurableRule +{ + /** @var list<string> */ + private array $functions = []; + + + public static function getOptionsSchema(): Schema + { + return Expect::structure([ + 'functions' => Expect::listOf('string')->default(['var_dump', 'print_r']) + ->description('Names of the forbidden functions'), + ]); + } + + + public function configure(array $options): void + { + $this->functions = $options['functions']; + } +} +``` + +Jména voleb se píší v camelCase. Seznam zadaný v konfiguraci nahrazuje výchozí hodnotu celou, nikdy se s ní neslučuje; s tím počítejte v popisu volby. Popis vůbec uvádějte jen tam, kde jméno, typ a výchozí hodnota neříkají všechno. + +Pravidlo, u kterého verze PHP rozhoduje o tom, co smí zapsat (ne o tom, jestli vůbec poběží), se zeptá `$context->getPhpVersion()`. Vrací řetězec ve tvaru `major.minor`, který se porovnává funkcí, ne operátory, protože `'8.10' > '8.9'` jako řetězec neplatí: + +```php +if (version_compare($context->getPhpVersion(), '8.4', '>=')) { + // ... +} +``` + + +Analýzy +======= + +Informace o souboru, kterou potřebuje víc pravidel, patří do analýzy. Je to obyčejná třída, jejíž konstruktor přijme `FileNode` (nebo nic). Pravidlo si ji vyžádá: + +```php +$resolver = $context->getAnalysis(NameResolver::class); +if ($resolver->isGlobalFunctionCall($node, 'var_dump')) { + // ... +} +``` + +Jádro analýzu vytvoří napoprvé a drží ji, dokud se strom nezmění; po každé změně vzniká znovu, takže nikdy nečtete zastaralý stav. Vestavěné jsou `PhpSyntax\Analyses\NameResolver` (jmenný prostor, importy, překlad jmen), `PhpSyntax\Analyses\Scope` (funkce, třída, dostupnost `$this`) a `DressCode\Analyses\PhpDoc` (dokumentační komentáře jako strom z parseru phpDocu). Vlastní analýza s konstruktorem nad `FileNode` se nikde neregistruje; jen ta, která potřebuje továrnu, se zapisuje do konfigurace klíčem `analyses`. + + +Testování +========= + +Každé pravidlo má fixtury a `RuleTester`, který na nich ověří výstup, hlášení a všechno výše popsané: [Testování pravidel |testing-rules]. diff --git a/dresscode/cs/rules/@home.texy b/dresscode/cs/rules/@home.texy new file mode 100644 index 0000000000..27a153fbf0 --- /dev/null +++ b/dresscode/cs/rules/@home.texy @@ -0,0 +1,249 @@ +Přehled pravidel +**************** + +.[perex] +Všechna vestavěná pravidla DressCode podle oblasti, u každého odkaz na stránku s příklady a volbami. Pravidlo, které porušení jen hlásí a neopravuje, je tak označeno, a pravidlo pro syntaxi novějšího PHP nese verzi, od které se zapíná. + +Pravidla se zapínají presetem nebo jednotlivě v [konfiguraci |/configuration#Pravidla a jejich volby]; jméno pravidla je to, co vidíte ve výpisu `dresscode check`. + + +Soubor +====== +Co platí pro celý soubor nebo pro každý jeho řádek: tagy, kódování, konce řádků, hlavička. + +- declare-spacing: Removes whitespace inside a declare statement. +- eof-newline: Ends the file with exactly one line ending. +- full-opening-tag: Requires the <?php opening tag. +- header-blank-lines: Puts a fixed number of blank lines around the blocks of the file header. +- line-ending: Unifies line endings. +- [line-length |line-length] Řádek delší než limit se ohlásí; opravit ho pravidlo neumí. (jen hlásí) +- no-byte-order-mark: Removes the UTF-8 byte order mark. +- no-closing-tag: Removes the closing tag at the end of the file. +- no-invisible-characters: Removes or escapes invisible characters in comments and strings, reports them in names. +- no-trailing-whitespace: Removes whitespace at the end of lines. +- strict-types-required: Requires declare(strict_types=1) as the first statement of a file. + + +Jmenné prostory a importy +========================= +Importy a zápis jmen tříd, funkcí a konstant. + +- class-reference-name-casing: Writes the names of internal classes and interfaces in their declared case. +- global-imports: Imports the global functions and constants a namespaced file uses. +- import-notation: Writes the imports of each kind one per use statement or all in one, and expands group use declarations. +- no-leading-backslash-in-global-namespace: Removes the leading backslash of names referenced in the global namespace. +- no-leading-backslash-in-import: Removes the leading backslash from imported names. +- ordered-imports: Sorts use statements alphabetically, classes before functions before constants. +- reference-used-names-only: Imports fully qualified names instead of referencing them in place. +- [unused-imports |unused-imports] Import, který kód nikde nepoužije, se odstraní. +- use-from-same-namespace: Removes imports of names from the current namespace. +- useless-alias: Removes an import alias equal to the imported name. + + +Třídy +===== +Deklarace třídy a jejích členů. + +- class-definition-spacing: Puts single spaces in the head of a class declaration. +- final-internal-class: Makes classes annotated as internal final. +- modern-class-name-reference: Uses ::class instead of get_class() and __CLASS__. +- name-casing: Reports declared names that do not follow the case convention configured for their kind. (jen hlásí) +- no-kind-in-class-name: Reports a class, interface or trait name repeating its kind. (jen hlásí) +- no-this-in-static-context: Reports $this used where no object is available. (jen hlásí) +- ordered-members: Orders class members by kind and visibility. +- self-for-current-class: Replaces the name of the current class with self. +- single-member-per-declaration: Splits a declaration of several constants, properties or traits into one per member. +- single-member-per-line: Puts every member of a class on its own line. +- useless-modifier: Removes a member modifier the class already implies. +- useless-null-property-initialization: Removes the explicit null initialization of untyped properties. +- visibility-required: Requires visibility on class members and orders their modifiers. + + +Funkce +====== +Deklarace i volání funkcí a metod, parametry a argumenty. + +- arrow-function: Replaces a closure returning a single expression with an arrow function. +- forbidden-functions: Reports calls of the configured functions. (jen hlásí) +- function-name-spacing: Removes whitespace between a function name and its parentheses. +- multi-line-call: Puts every argument of a multi-line call on its own line. +- multi-line-signature: Splits long signatures and constructors with promoted properties into one parameter per line. +- named-argument-spacing: Normalizes whitespace around the colon of a named argument. +- native-function-casing: Calls native functions in lowercase. +- no-alias-functions: Calls a function by its canonical name instead of an alias. +- no-conversion-functions: Uses a cast instead of intval() and friends. +- no-deprecated-functions: Reports calls of deprecated internal functions. (jen hlásí) +- no-direct-invoke-call: Calls an invokable object directly instead of its __invoke() method. +- no-dirname-of-file: Replaces dirname(__FILE__) with __DIR__ and nested dirname() calls with the levels argument. +- no-inner-functions: Reports a function declared inside another function. (jen hlásí) +- no-is-null: Replaces is_null() with a comparison with null. +- no-settype: Assigns a cast instead of calling settype(). +- no-unpacking-in-optimized-call: Reports argument unpacking in a call of a function the compiler optimizes. (jen hlásí) +- static-closure: Declares a closure that does not use $this as static. +- strict-call: Calls in_array(), array_search(), array_keys(), base64_decode() and mb_detect_encoding() with $strict = true. +- useless-parameter-default: Removes a default value that a required parameter makes unreachable. + + +Řízení toku +=========== +Řídicí struktury, příkazy, bloky a výjimky. + +- continuation-position: Puts else, elseif, catch, finally and the while of do on the line of the closing brace, or on the next one. +- control-structure-braces: Encloses the body of every control structure in braces. +- early-exit: Turns a trailing if into a guard that leaves early, and an else that leaves into the first branch. +- elseif-keyword: Replaces else if with elseif. +- fall-through-comment: Requires a comment on an intentional case fall-through. +- multi-line-condition: Splits a long condition of if, elseif, while and do-while into one part per line. +- no-alternative-syntax: Replaces the alternative syntax with braces. +- no-continue-in-switch: Leaves a switch with break, never with continue. +- no-empty-statement: Removes empty statements. +- no-unreachable-catch: Reports a catch block following one that catches Throwable. (jen hlásí) +- reference-throwable-only: Reports references to the general Exception where Throwable belongs. (jen hlásí) +- single-statement-per-line: Puts every statement on its own line. +- switch-case-colon: Ends case and default with a colon. +- switch-case-spacing: Removes whitespace before the colon of a case. +- ternary-for-simple-branch: Uses the ternary operator where an if-else only picks one of two values. +- useless-braces: Removes braces around a bare statement group. +- useless-catch-variable: Removes the variable of a catch clause that is never used. +- useless-construct-parentheses: Removes parentheses around the operand of a language construct. +- useless-else: Removes an else after branches which always leave, and may turn an elseif there into an if. +- useless-if-condition-with-return: Replaces an if returning true or false with a return of the condition. +- useless-return: Removes a bare return at the end of a function body. + + +Výrazy +====== +Operátory, přetypování, ternár, instanciace a přístup ke členům. + +- [binary-operator-spacing |binary-operator-spacing] Kolem binárního operátoru, přiřazení, instanceof, => a = výchozí hodnoty je z každé strany aspoň jedna mezera, nebo přesně jedna. +- cast-canonical-type: Writes a cast with the short type name in lowercase. +- cast-spacing: Puts a single space between a cast and its operand. +- combined-assignment-operator: Uses += and friends where an assignment repeats its target. +- concat-spacing: Puts a single space around the concatenation operator. +- double-colon-spacing: Removes whitespace around the double colon. +- explicit-operator-precedence: Parenthesizes an operand where the precedence of logical or bitwise operators is easy to misread. +- increment-operator: Uses ++ and -- instead of += 1 and -= 1. +- multi-line-chain: Puts every link of a multi-line chain of calls on its own line, the first one included. +- multi-line-ternary: Puts the two operators of a multi-line ternary on lines of their own. +- new-argument-parentheses: Requires or forbids the empty parentheses of an instantiation. +- no-short-bool-cast: Replaces !! with a (bool) cast. +- no-yoda-comparison: Puts the variable side of a comparison on the left. +- not-equals-operator: Writes != instead of <>. +- null-coalescing-operator: Replaces a ternary testing for null with the null coalescing operator. +- object-operator-spacing: Removes whitespace around the object operator. +- offset-bracket-spacing: Removes whitespace around the brackets of an offset access. +- reference-spacing: Removes whitespace between & and its operand. +- [short-ternary-operator |short-ternary-operator] Ternár, který ve své prostřední části opakuje podmínku, se píše zkráceně jako ?:. +- spread-operator-spacing: Removes whitespace between ... and its operand. +- strict-comparison: Replaces loose comparisons with strict ones. +- symbolic-logical-operators: Uses && and || instead of and and or. +- ternary-operator-spacing: Puts whitespace around the ternary operators. +- unary-operator-spacing: Removes whitespace between a unary operator and its operand. +- useless-attribute-parentheses: Removes the empty parentheses after the name of an attribute. +- useless-parentheses-around-new: Removes the parentheses around new when a member of the new object is accessed. (PHP 8.4+) +- useless-ternary-operator: Replaces a ternary operator choosing between true and false with the condition. + + +Literály +======== +Zápis řetězců, čísel, konstant a klíčových slov. + +- complex-string-variable: Replaces the deprecated ${name} interpolation with {$name}. +- constant-casing: Lowercases true, false and null. +- heredoc-indentation: Indents the body and the closing marker of a heredoc relative to its starting line. +- keyword-casing: Writes keywords in lowercase. +- magic-constant-casing: Writes magic constants in uppercase. +- no-backtick-operator: Runs a command through shell_exec() instead of backticks. +- no-implicit-backslash: Writes every backslash of a string escaped. +- no-trailing-whitespace-in-string: Removes trailing whitespace from string lines. +- nowdoc-without-interpolation: Uses nowdoc where a heredoc interpolates nothing. +- numeric-literal-separator: Groups the digits of long numbers with underscores. +- [octal-notation |octal-notation] Osmičkové číslo se píše s prefixem 0o, ne s pouhou nulou na začátku. (PHP 8.1+) +- single-quoted-strings: Uses single quotes where double quotes give nothing. +- useless-string-concat: Joins two string literals concatenated on one line and drops concatenation with an empty string. + + +Pole +==== +Pole a destrukturace jako syntaktická konstrukce. + +- array-spacing: Removes whitespace inside the brackets of an array. +- multi-line-array: Puts every item of a multi-line array on its own line and the opening bracket on the line before. +- short-array-syntax: Writes arrays with the short syntax. +- short-list-syntax: Uses the short syntax for destructuring instead of list(). +- [trailing-comma |trailing-comma] Víceřádkový seznam končí čárkou za poslední položkou, jednořádkový ji nemá. + + +Typy +==== +Typové deklarace v kódu, ne v anotacích. + +- nullable-type-for-default-null: Marks the type of a parameter defaulting to null as nullable. +- type-hint-required: Adds native types from annotations and reports declarations without any type. +- type-hint-spacing: Normalizes whitespace in type declarations. +- union-type-format: Writes a nullable type as ?T, puts null last in a union type and may sort the rest. + + +PhpDoc +====== +Dokumentační komentáře a anotace v nich. + +- annotation-name: Writes known annotations in their canonical case. +- attribute-after-phpdoc: Moves a doc comment written after the attributes above them. +- explicit-assertion: Replaces an inline @var annotation with an assert() of the type. +- forbidden-annotations: Removes the configured annotations from doc comments. +- forbidden-phpdoc-lines: Removes description lines matching the configured patterns from doc comments. +- no-duplicate-return-annotation: Reports more than one @return in a function doc comment. (jen hlásí) +- no-empty-phpdoc: Removes empty doc comments. +- no-empty-var-annotation: Reports a @var or @see without content in a property doc comment. (jen hlásí) +- no-unknown-param-annotation: Reports a @param of a parameter the function does not declare. (jen hlásí) +- phpdoc-alignment: Aligns the stars of a doc comment with its opening. +- phpdoc-canonical-types: Writes the types in doc comments in their canonical form: short lowercase built-ins, one array notation, no repeated type in a union. +- phpdoc-null-last: Moves null to the end of union types in doc comments. +- phpdoc-trim: Removes extra blank lines in a doc comment. +- promoted-property-annotation-position: Moves the @param annotation of a promoted property to a doc comment at the property. +- property-phpdoc-required: Reports a plain comment in place of a property doc comment. (jen hlásí) +- property-phpdoc-single-line: Writes a property doc comment with a single line of content on one line. +- property-var-annotation: Reports a repeated or misplaced @var in a property doc comment. (jen hlásí) +- useless-constant-var-annotation: Removes a useless @var from a class constant. +- useless-function-phpdoc: Removes a function doc comment that only repeats the native types. +- useless-inheritdoc: Removes a doc comment consisting of @inheritDoc only. + + +Komentáře +========= +Obyčejné komentáře. + +- comment-spacing: Puts a space after the marker of a comment and before a comment following code. +- commented-out-function: Comments out statements calling the configured debugging functions. +- no-empty-comment: Removes empty comments. +- no-hash-comment: Writes single-line comments with //, not #. + + +Proměnné +======== +Proměnné, jejich rozsah a životnost. + +- combined-issets: Asks about several variables in one isset() instead of a chain joined by &&. +- combined-unsets: Drops several variables in one unset instead of consecutive statements. +- no-duplicate-assignment: Reports an assignment repeated to the same variable in one expression. (jen hlásí) +- no-global-keyword: Forbids the global statement. (jen hlásí) +- no-unset-on-property: Assigns null to a property instead of unsetting it. + + +Bílé znaky +========== +Bílé znaky, které nepatří žádné jedné konstrukci: odsazení, závorky, čárky, prázdné řádky. + +- attribute-position: Puts the attributes of a declaration on lines of their own above it. +- attribute-spacing: Removes whitespace inside the brackets of an attribute group. +- body-blank-lines: Removes the blank line after the opening brace of a block. +- [braces-position |braces-position] Otevírací složená závorka stojí u tříd a funkcí na vlastním řádku a u řídicích struktur, closures a anonymních tříd na řádku hlavičky; tělo začíná na novém řádku a zavírací závorka má řádek pro sebe. +- comma-spacing: Puts a single space after a comma and none before it. +- construct-spacing: Puts a single space around language constructs. +- [declaration-blank-lines |declaration-blank-lines] Mezi metodami jsou dva prázdné řádky, mezi vlastnostmi a konstantami nejvýš jeden, za otevírací závorkou třídy a před zavírací žádný a dokumentační komentář se drží své deklarace. +- indentation: Indents every line by the construct it continues, one level per nesting. +- parentheses-spacing: Removes whitespace inside parentheses. +- semicolon-spacing: Removes whitespace before a semicolon and puts a single space after it. +- single-level-indentation: Reports indentation that deepens by more than a level or returns to a level nothing opened. (jen hlásí) +- statement-blank-lines: Puts blank lines before and after statements of the configured kinds. diff --git a/dresscode/cs/rules/@meta.texy b/dresscode/cs/rules/@meta.texy new file mode 100644 index 0000000000..ddb4f78731 --- /dev/null +++ b/dresscode/cs/rules/@meta.texy @@ -0,0 +1,2 @@ +{{sitename: DressCode}} +{{leftbar: /@left-menu}} diff --git a/dresscode/cs/rules/binary-operator-spacing.texy b/dresscode/cs/rules/binary-operator-spacing.texy new file mode 100644 index 0000000000..73ca1f0599 --- /dev/null +++ b/dresscode/cs/rules/binary-operator-spacing.texy @@ -0,0 +1,102 @@ +binary-operator-spacing +*********************** + +.[perex] +Kolem binárního operátoru, přiřazení, `instanceof`, `=>` a `=` výchozí hodnoty je z každé strany aspoň jedna mezera, nebo přesně jedna. + +Opravuje · v presetech `dresscode/per`, `dresscode/psr12`, `dresscode/nette` · pokrývá `binary_operator_spaces`, `Squiz.WhiteSpace.LogicalOperatorSpacing`, `Squiz.WhiteSpace.OperatorSpacing` .[rule-info] + + +Co pravidlo hlídá +================= + +`$a+1` se čte hůř než `$a + 1` a `$a=1` vypadá jako překlep. Pravidlo se stará o mezery kolem aritmetických, porovnávacích, logických a bitových operátorů, kolem `??`, kolem všech přiřazení včetně `+=` a `??=`, kolem `instanceof`, kolem `=>` v poli, `match` i arrow funkci a kolem `=` u výchozí hodnoty parametru, vlastnosti nebo konstanty. Tečka pro spojování řetězců má vlastní pravidlo `concat-spacing`, protože o ní se styly rozcházejí; `=` v `declare(strict_types=1)` patří pravidlu `declare-spacing`. + +Ve výchozím nastavení stačí aspoň jedna mezera: kdo si zarovnává přiřazení nebo hodnoty v poli pod sebe, o zarovnání nepřijde. Volba `spacing` umí vyžadovat přesně jednu mezeru a `tabAlignment` povolit zarovnání tabulátorem. Operátor na začátku nebo na konci řádku se nehlídá; o rozlomení výrazu přes řádky se starají pravidla `multi-line-*`. + + +Příklad +======= + +```php .[before] +$total=$price*$count; // At least one space before the = operator // At least one space after the = operator // At least one space before the * operator // At least one space after the * operator +$label = $count>1 ? 'items' : 'item'; // At least one space before the > operator // At least one space after the > operator +$map = ['a'=>1, 'b' => 2]; // At least one space before the double arrow // At least one space after the double arrow +$aligned = 1; +$alsoFine = 2; +``` + +```php .[after] +$total = $price * $count; +$label = $count > 1 ? 'items' : 'item'; +$map = ['a' => 1, 'b' => 2]; +$aligned = 1; +$alsoFine = 2; +``` + +Poslední dva řádky zůstaly, jak byly: víc mezer než jedna výchozí nastavení dovoluje. + + +Volby +===== + + +spacing .[option] +----------------- + +`atLeastSingle` nebo `single`, výchozí `atLeastSingle`. S `atLeastSingle` zůstanou mezery navíc, které zarovnávají přiřazení nebo položky pole pod sebe; `single` je stáhne na jednu. + +```neon +rules: + dresscode/binary-operator-spacing: + spacing: single +``` + +```php .[before] +$aligned = 1; // A single space before the = operator +$alsoFine = 2; // A single space before the = operator +``` + +```php .[after] +$aligned = 1; +$alsoFine = 2; +``` + + +tabAlignment .[option] +---------------------- + +`bool`, výchozí `false`. S `true` zůstane i mezera tvořená tabulátorem, která zarovnává sloupce, a to i s `spacing: single`. + +```neon +rules: + dresscode/binary-operator-spacing: + spacing: single + tabAlignment: true +``` + +```php .[before] +$short = 1; +$longerName = 2; +$spaced = 3; // A single space before the = operator +``` + +```php .[after] +$short = 1; +$longerName = 2; +$spaced = 3; +``` + + +Související pravidla +==================== + +- `concat-spacing` mezery kolem tečky +- `unary-operator-spacing` žádná mezera mezi unárním operátorem a operandem +- `ternary-operator-spacing` mezery kolem `?` a `:` + + +Zdroj +===== + +Třída "BinaryOperatorSpacingRule":https://github.com/dg/dresscode/blob/master/src/Rules/Expressions/BinaryOperatorSpacingRule.php, fixtury "binary-operator-spacing":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/binary-operator-spacing. diff --git a/dresscode/cs/rules/braces-position.texy b/dresscode/cs/rules/braces-position.texy new file mode 100644 index 0000000000..53ae0a2693 --- /dev/null +++ b/dresscode/cs/rules/braces-position.texy @@ -0,0 +1,291 @@ +braces-position +*************** + +.[perex] +Otevírací složená závorka stojí u tříd a funkcí na vlastním řádku a u řídicích struktur, closures a anonymních tříd na řádku hlavičky; tělo začíná na novém řádku a zavírací závorka má řádek pro sebe. + +Opravuje · v presetech `dresscode/per`, `dresscode/psr12`, `dresscode/nette` · pokrývá `braces_position`, `PSR2.Classes.ClassDeclaration`, `Squiz.Functions.MultiLineFunctionDeclaration` .[rule-info] + + +Co pravidlo hlídá +================= + +Kam patří `{`, je nejviditelnější rozhodnutí každého stylu a zároveň to, na kterém se styly nejčastěji rozcházejí. Pravidlo má pro každý druh konstrukce volbu: třídy, rozhraní, traity a výčty (`classes`), anonymní třídy, anonymní funkce a řídicí struktury. Výchozí hodnoty jsou ty z PSR-12 a z [PER Coding Style 3.1 |https://www.php-fig.org/per/coding-style/]: deklarace mají závorku na dalším řádku, všechno ostatní na témže. U funkce s parametry na několika řádcích rozhoduje volba `multiLineParameters`, protože tam říká PER něco jiného než některé domácí styly. + +Vedle otevírací závorky pravidlo hlídá, že za ní tělo začíná na novém řádku a že zavírací závorka stojí na řádku sama. Výjimky jsou tři a každá má vlastní volbu: closure (anonymní funkce) napsaná celá na jednom řádku (`allowSingleLineAnonymousFunctions`), prázdná anonymní třída zapsaná jako `{}` (`emptyAnonymousClasses`) a prázdné tělo třídy nebo funkce jako `{}` (`emptyBodies`). Property hooky mají závorku na řádku vlastnosti a zkrácený zápis `{ get; set; }` zůstává, kdežto rozepsané hooky dostanou každý svůj řádek. + +Pravidlo přesouvá jen závorky. Kde pak řádek stojí, tedy jeho odsazení, je věc pravidla `indentation`. + + +Příklad +======= + +```php .[before] +class Cart { // A line break before the opening brace + public function add(Item $item): void { // A line break before the opening brace + if ($item->isFree()) + { // No line break before the opening brace + return; + } + $this->items[] = $item; + } +} +``` + +```php .[after] +class Cart +{ + public function add(Item $item): void + { + if ($item->isFree()) { + return; + } + $this->items[] = $item; + } +} +``` + + +Volby +===== + + +multiLineParameters .[option] +----------------------------- + +`sameLine`, `nextLine` nebo `nextLineAfterReturnType`, výchozí `sameLine`. Kam jde závorka funkce, jejíž parametry zabírají několik řádků: hned za zavírací kulatou závorku (PER), na další řádek, nebo na další řádek jen tehdy, když má funkce návratový typ. Poslední hodnotu používá preset `dresscode/nette`: bez návratového typu by závorka na vlastním řádku vypadala jako `) {` bez důvodu, s ním by `): void {` schovala typ na konec řádku. + +```neon +rules: + dresscode/braces-position: + multiLineParameters: nextLineAfterReturnType +``` + +```php .[before] +function send( + string $to, + string $subject, +): void { // A line break before the opening brace + mail($to, $subject); +} + +function log( + string $message, +) +{ // No line break before the opening brace + echo $message; +} +``` + +```php .[after] +function send( + string $to, + string $subject, +): void +{ + mail($to, $subject); +} + +function log( + string $message, +) { + echo $message; +} +``` + + +classes .[option] +----------------- + +`sameLine` nebo `nextLine`, výchozí `nextLine`. Třídy, rozhraní, traity a výčty. + +```neon +rules: + dresscode/braces-position: + classes: sameLine +``` + +```php .[before] +class Cart +{ // No line break before the opening brace + private array $items = []; +} +``` + +```php .[after] +class Cart { + private array $items = []; +} +``` + + +anonymousClasses .[option] +-------------------------- + +`sameLine` nebo `nextLine`, výchozí `sameLine`. + +```neon +rules: + dresscode/braces-position: + anonymousClasses: nextLine +``` + +```php .[before] +$logger = new class implements Logger { // A line break before the opening brace + public function log(string $message): void + { + echo $message; + } +}; +``` + +```php .[after] +$logger = new class implements Logger +{ + public function log(string $message): void + { + echo $message; + } +}; +``` + + +anonymousFunctions .[option] +---------------------------- + +`sameLine` nebo `nextLine`, výchozí `sameLine`. + +```neon +rules: + dresscode/braces-position: + anonymousFunctions: nextLine +``` + +```php .[before] +$double = function (int $x) { // A line break before the opening brace + return $x * 2; +}; +``` + +```php .[after] +$double = function (int $x) +{ + return $x * 2; +}; +``` + + +controlStructures .[option] +--------------------------- + +`sameLine` nebo `nextLine`, výchozí `sameLine`. Podmínky, cykly, `switch`, `match`, `try` a jejich pokračování. + +```neon +rules: + dresscode/braces-position: + controlStructures: nextLine +``` + +```php .[before] +if ($ready) { // A line break before the opening brace + start(); +} +``` + +```php .[after] +if ($ready) +{ + start(); +} +``` + + +allowSingleLineAnonymousFunctions .[option] +------------------------------------------- + +`bool`, výchozí `true`. Closure napsaná celá na jednom řádku smí tak zůstat. Presety `dresscode/psr12` a `dresscode/per` to zakazují, protože PSR-12 chce tělo každé closure na vlastních řádcích. + +```neon +rules: + dresscode/braces-position: + allowSingleLineAnonymousFunctions: false +``` + +```php .[before] +$ids = array_map(function ($row) { return $row->id; }, $rows); // A line break after the opening brace // A line break before the closing brace +``` + +```php .[after] +$ids = array_map(function ($row) { + return $row->id; +}, $rows); +``` + + +emptyAnonymousClasses .[option] +------------------------------- + +`sameLine` nebo `ownLine`, výchozí `sameLine`. Prázdná anonymní třída se zapíše jako `{}` na řádku `new`, ať `emptyBodies` říká cokoli; `ownLine` dá zavírací závorku na vlastní řádek. + +```neon +rules: + dresscode/braces-position: + emptyAnonymousClasses: ownLine +``` + +```php .[before] +$marker = new class {}; // A line break after the opening brace +``` + +```php .[after] +$marker = new class { +}; +``` + + +emptyBodies .[option] +--------------------- + +`sameLine` nebo `ownLine`, výchozí `ownLine`. Prázdné tělo třídy, funkce, metody nebo closure: buď `{}` na řádku hlavičky, nebo otevírací a zavírací závorka každá na svém řádku. Komentář uvnitř dělá z těla neprázdné. Preset `dresscode/per` nastavuje `sameLine`, protože PER prázdná těla zkracuje. + +```neon +rules: + dresscode/braces-position: + emptyBodies: sameLine +``` + +```php .[before] +class NotFound extends Exception +{ // No line break before the opening brace +} // No line break before the closing brace + +class Point +{ + public function __construct(private int $x, private int $y) + { // No line break before the opening brace + } // No line break before the closing brace +} +``` + +```php .[after] +class NotFound extends Exception {} + +class Point +{ + public function __construct(private int $x, private int $y) {} +} +``` + + +Související pravidla +==================== + +- `indentation` odsadí řádky, které tohle pravidlo otevřelo +- `control-structure-braces` doplní závorky kolem těla řídicí struktury, které je nemá +- `continuation-position` umístí `else`, `catch` a `finally` k zavírací závorce + + +Zdroj +===== + +Třída "BracesPositionRule":https://github.com/dg/dresscode/blob/master/src/Rules/Whitespace/BracesPositionRule.php, fixtury "braces-position":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/braces-position. diff --git a/dresscode/cs/rules/declaration-blank-lines.texy b/dresscode/cs/rules/declaration-blank-lines.texy new file mode 100644 index 0000000000..fd5832cdc8 --- /dev/null +++ b/dresscode/cs/rules/declaration-blank-lines.texy @@ -0,0 +1,413 @@ +declaration-blank-lines +*********************** + +.[perex] +Mezi metodami jsou dva prázdné řádky, mezi vlastnostmi a konstantami nejvýš jeden, za otevírací závorkou třídy a před zavírací žádný a dokumentační komentář se drží své deklarace. + +Opravuje · v presetech `dresscode/per`, `dresscode/psr12`, `dresscode/nette` · pokrývá `no_blank_lines_after_class_opening`, `no_blank_lines_after_phpdoc`, `SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing`, `SlevomatCodingStandard.Classes.ConstantSpacing`, `SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces`, `SlevomatCodingStandard.Classes.PropertySpacing`, `SlevomatCodingStandard.Classes.TraitUseSpacing`, `Squiz.WhiteSpace.FunctionSpacing` .[rule-info] + + +Co pravidlo hlídá +================= + +Prázdné řádky mezi deklaracemi jsou to, co dělá třídu čitelnou na první pohled: metody od sebe oddělené výrazně, vlastnosti a konstanty pohromadě, komentář přilepený k tomu, co popisuje. Pravidlo má pro každé takové místo volbu: mezi funkcemi a metodami, v rozhraní zvlášť, před první a za poslední metodou třídy, za otevírací a před zavírací závorkou, kolem `use` traitů, mezi vlastnostmi, konstantami a případy výčtu, před dokumentovaným členem a mezi dokumentačním komentářem či atributem a deklarací. + +Každá volba je buď počet prázdných řádků, nebo rozsah `[min, max]` s otevřeným koncem zapsaným jako `null`, nebo `null`, které dané místo nechá na pokoji. Rozsah se hodí tam, kde chcete připustit dvojí zápis: `betweenMembers: [0, 1]` dovolí psát vlastnosti těsně pod sebe i oddělené jedním řádkem, ale dva už ne. Počet, který do rozsahu nespadá, pravidlo posune k nejbližší povolené hodnotě. + +Výchozí hodnoty odpovídají stylu se dvěma prázdnými řádky mezi metodami. Preset `dresscode/psr12` nastavuje `betweenFunctions`, `betweenFunctionsInInterface`, `betweenMembers`, `beforeDocumentedMember` a `afterPhpdoc` na `null`, protože PSR-12 o počtu řádků mezi metodami nic neříká. + + +Příklad +======= + +```php .[before] +class Cart +{ + + private array $items = []; // Expected 0 blank lines before the property, 1 found + public function add(Item $item): void // Expected 2 blank lines before the method, 0 found + { + $this->items[] = $item; + } + + public function total(): int // Expected 2 blank lines before the method, 1 found + { + return array_sum($this->items); + } +} +``` + +```php .[after] +class Cart +{ + private array $items = []; + + + public function add(Item $item): void + { + $this->items[] = $item; + } + + + public function total(): int + { + return array_sum($this->items); + } +} +``` + + +Volby +===== + + +betweenFunctions .[option] +-------------------------- + +Počet, rozsah nebo `null`, výchozí `2`. Před funkcí nebo metodou a za ní; první a poslední metoda třídy se řídí volbami `beforeFirst` a `afterLast`. Platí i pro funkce deklarované mimo třídu. + +```neon +rules: + dresscode/declaration-blank-lines: + betweenFunctions: 1 +``` + +```php .[before] +class Cart +{ + public function add(Item $item): void + { + } + + + public function total(): int // Expected 1 blank line before the method, 2 found + { + } +} +``` + +```php .[after] +class Cart +{ + public function add(Item $item): void + { + } + + public function total(): int + { + } +} +``` + + +betweenFunctionsInInterface .[option] +------------------------------------- + +Počet, rozsah nebo `null`, výchozí `1`. Mezi metodami rozhraní, které nemají tělo, a stačí jim proto menší odstup. + +```neon +rules: + dresscode/declaration-blank-lines: + betweenFunctionsInInterface: 0 +``` + +```php .[before] +interface Storage +{ + public function read(string $key): mixed; + + public function write(string $key, mixed $value): void; // Expected 0 blank lines before the method, 1 found +} +``` + +```php .[after] +interface Storage +{ + public function read(string $key): mixed; + public function write(string $key, mixed $value): void; +} +``` + + +beforeFirst .[option] +--------------------- + +Počet, rozsah nebo `null`, výchozí `0`. Před metodou, která je prvním členem třídy. + +```neon +rules: + dresscode/declaration-blank-lines: + beforeFirst: 1 +``` + +```php .[before] +class Cart +{ + public function add(Item $item): void // Expected 1 blank line before the method, 0 found + { + } +} +``` + +```php .[after] +class Cart +{ + + public function add(Item $item): void + { + } +} +``` + + +afterLast .[option] +------------------- + +Počet, rozsah nebo `null`, výchozí `0`. Za metodou, která je posledním členem třídy. + +```neon +rules: + dresscode/declaration-blank-lines: + afterLast: 1 +``` + +```php .[before] +class Cart +{ + public function add(Item $item): void + { + } +} // Expected 1 blank line after the method, 0 found +``` + +```php .[after] +class Cart +{ + public function add(Item $item): void + { + } + +} +``` + + +afterOpeningBrace .[option] +--------------------------- + +Počet, rozsah nebo `null`, výchozí `0`. Před prvním členem třídy, pokud to není metoda; pak platí `beforeFirst`. + +```neon +rules: + dresscode/declaration-blank-lines: + afterOpeningBrace: 1 +``` + +```php .[before] +class Cart +{ + private array $items = []; // Expected 1 blank line before the property, 0 found +} +``` + +```php .[after] +class Cart +{ + + private array $items = []; +} +``` + + +beforeClosingBrace .[option] +---------------------------- + +Počet, rozsah nebo `null`, výchozí `0`. Za posledním členem třídy, pokud to není metoda; pak platí `afterLast`. + +```neon +rules: + dresscode/declaration-blank-lines: + beforeClosingBrace: 1 +``` + +```php .[before] +class Cart +{ + private array $items = []; +} // Expected 1 blank line before the closing brace, 0 found +``` + +```php .[after] +class Cart +{ + private array $items = []; + +} +``` + + +betweenTraitUses .[option] +-------------------------- + +Počet, rozsah nebo `null`, výchozí `0`. Mezi `use` traitů na začátku třídy. + +```php .[before] +class Cart +{ + use Countable; + + use Serializable; // Expected 0 blank lines before the trait use, 1 found +} +``` + +```php .[after] +class Cart +{ + use Countable; + use Serializable; +} +``` + + +afterTraitUses .[option] +------------------------ + +Počet, rozsah nebo `null`, výchozí `1`. Před členem, který následuje za `use` traitů, pokud to není metoda; pak platí `betweenFunctions`. + +```php .[before] +class Cart +{ + use Countable; + private array $items = []; // Expected 1 blank line before the property, 0 found +} +``` + +```php .[after] +class Cart +{ + use Countable; + + private array $items = []; +} +``` + + +betweenMembers .[option] +------------------------ + +Počet, rozsah nebo `null`, výchozí `[0, 1]`. Mezi vlastnostmi, konstantami a případy výčtu bez dokumentačního komentáře a atributu. Výchozí rozsah dovolí členy psát těsně pod sebe i oddělené jedním řádkem. + +```php .[before] +class Cart +{ + private array $items = []; + private int $count = 0; + + + private ?Customer $customer = null; // Expected at most 1 blank line before the property, 2 found +} +``` + +```php .[after] +class Cart +{ + private array $items = []; + private int $count = 0; + + private ?Customer $customer = null; +} +``` + +S pevným počtem musí být odstup všude stejný: + +```neon +rules: + dresscode/declaration-blank-lines: + betweenMembers: 1 +``` + +```php .[before] +class Cart +{ + private array $items = []; + private int $count = 0; // Expected 1 blank line before the property, 0 found +} +``` + +```php .[after] +class Cart +{ + private array $items = []; + + private int $count = 0; +} +``` + + +beforeDocumentedMember .[option] +-------------------------------- + +Počet, rozsah nebo `null`, výchozí `1`. Před vlastností, konstantou nebo případem výčtu, který má dokumentační komentář nebo atribut; komentář potřebuje odstup od předchozího člena, aby bylo vidět, ke kterému patří. + +```php .[before] +class Cart +{ + private array $items = []; + /** @var int<0, max> */ + private int $count = 0; // Expected 1 blank line before the property, 0 found +} +``` + +```php .[after] +class Cart +{ + private array $items = []; + + /** @var int<0, max> */ + private int $count = 0; +} +``` + + +afterPhpdoc .[option] +--------------------- + +Počet, rozsah nebo `null`, výchozí `0`. Mezi dokumentačním komentářem nebo atributem a deklarací, ke které patří. + +```php .[before] +class Cart +{ + /** + * Adds an item. + */ + + public function add(Item $item): void // Expected 0 blank lines after the doc comment, 1 found + { + } +} +``` + +```php .[after] +class Cart +{ + /** + * Adds an item. + */ + public function add(Item $item): void + { + } +} +``` + + +Související pravidla +==================== + +- `header-blank-lines` prázdné řádky v hlavičce souboru kolem `declare`, `namespace` a importů +- `body-blank-lines` prázdný řádek za otevírací závorkou těla funkce nebo řídicí struktury +- `statement-blank-lines` prázdné řádky kolem příkazů jako `return` + + +Zdroj +===== + +Třída "DeclarationBlankLinesRule":https://github.com/dg/dresscode/blob/master/src/Rules/Whitespace/DeclarationBlankLinesRule.php, fixtury "declaration-blank-lines":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/declaration-blank-lines. diff --git a/dresscode/cs/rules/line-length.texy b/dresscode/cs/rules/line-length.texy new file mode 100644 index 0000000000..bfba8a3baa --- /dev/null +++ b/dresscode/cs/rules/line-length.texy @@ -0,0 +1,97 @@ +line-length +*********** + +.[perex] +Řádek delší než limit se ohlásí; opravit ho pravidlo neumí. + +Jen hlásí · v žádném presetu · pokrývá `Generic.Files.LineLength`, `SlevomatCodingStandard.Files.LineLength` .[rule-info] + + +Co pravidlo hlídá +================= + +Pravidlo změří každý řádek souboru a ohlásí ty, které přesahují limit. Šířka se počítá tak, jak řádek vidíte: tabulátor se počítá na svou šířku podle stylu, ne za jeden znak. Neměří se řádky, které jsou obsahem a zalomením by změnily význam: vnitřek heredocu, víceřádkový řetězec a HTML mimo PHP tagy. Dlouhý řádek uvnitř víceřádkového komentáře se hlásí na řádku, kde ten komentář začíná. + +Pravidlo běží jako poslední, až po pravidlech, která umějí dlouhé řádky zalomit (`multi-line-call`, `multi-line-condition`, `multi-line-array` a další). Co ohlásí, je tedy to, co nic zalomit nedokázalo, a zbývá to na vás: rozdělit výraz, pojmenovat mezivýsledek, zkrátit řetězec. + + +Příklad +======= + +Limit je tu snížený, aby se příklad vešel na stránku; výchozí je 120 znaků. + +```neon +rules: + dresscode/line-length: + limit: 60 +``` + +```php .[before] +use Some\Very\Long\Vendor\Namespace\WithAnEvenLonger\ClassName; + +$message = sprintf('%s has %d unread messages', $user->name, $count); // The line is 69 characters long, the limit is 60 +$greeting = 'Hello'; +``` + +Import je delší než limit, ale nehlásí se: nejde rozdělit na víc řádků a volba `ignoreImports` ho vynechává. + + +Volby +===== + + +limit .[option] +--------------- + +`int`, výchozí `120`. Největší povolená šířka řádku ve znacích. + +```neon +rules: + dresscode/line-length: + limit: 80 +``` + +```php .[before] +$title = $translator->translate('order.confirmation.subject', ['number' => $order->id]); // The line is 88 characters long, the limit is 80 +``` + + +ignoreImports .[option] +----------------------- + +`bool`, výchozí `true`. Řádek s `use` se nehlásí, protože import nejde zalomit. + +```neon +rules: + dresscode/line-length: + limit: 60 + ignoreImports: false +``` + +```php .[before] +use Some\Very\Long\Vendor\Namespace\WithAnEvenLonger\ClassName; // The line is 63 characters long, the limit is 60 +``` + + +ignorePatterns .[option] +------------------------ + +Seznam řetězců, výchozí `[]`. Regulární výrazy; řádek, kterému některý odpovídá, se nehlásí. Hodí se pro řádky s URL nebo pro tabulky dat, které by zalomením ztratily čitelnost. + +```neon +rules: + dresscode/line-length: + limit: 60 + ignorePatterns: ['~https?://~'] +``` + +```php .[before] +$docs = 'https://example.com/documentation/very/long/path/to/the/page'; +$data = ['alpha' => 1, 'beta' => 2, 'gamma' => 3, 'delta' => 4]; // The line is 64 characters long, the limit is 60 +``` + + +Zdroj +===== + +Třída "LineLengthRule":https://github.com/dg/dresscode/blob/master/src/Rules/Files/LineLengthRule.php, fixtury "line-length":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/line-length. diff --git a/dresscode/cs/rules/octal-notation.texy b/dresscode/cs/rules/octal-notation.texy new file mode 100644 index 0000000000..a16b1962fd --- /dev/null +++ b/dresscode/cs/rules/octal-notation.texy @@ -0,0 +1,37 @@ +octal-notation +************** + +.[perex] +Osmičkové číslo se píše s prefixem `0o`, ne s pouhou nulou na začátku. + +Opravuje · PHP 8.1+ · v presetech `dresscode/nette` · pokrývá `octal_notation` .[rule-info] + + +Co pravidlo hlídá +================= + +Zápis `0755` je osmičkové číslo, ale na první pohled vypadá jako desítkové s nulou navíc. PHP 8.1 přineslo prefix `0o`, který osmičkovou soustavu říká stejně zřetelně jako `0x` šestnáctkovou a `0b` dvojkovou. Pravidlo přepíše každé celé číslo začínající nulou a pokračující osmičkovými číslicemi na tvar s `0o`; samotná `0`, čísla `0x`, `0b` a čísla s desetinnou tečkou se ho netýkají. + +Protože `0o755` starší PHP neumí přečíst, zapne se pravidlo jen v projektu, jehož [cílová verze PHP |/configuration#Presety a vrstvy] je 8.1 nebo vyšší. V presetu tak nemusíte nic hlídat: pod verzí 8.1 se pravidlo samo vynechá. + + +Příklad +======= + +```php .[before] +$mode = 0755; // An octal number must be written with the '0o' prefix +$zero = 0; +$flags = 0x1F; +``` + +```php .[after] +$mode = 0o755; +$zero = 0; +$flags = 0x1F; +``` + + +Zdroj +===== + +Třída "OctalNotationRule":https://github.com/dg/dresscode/blob/master/src/Rules/Literals/OctalNotationRule.php, fixtury "octal-notation":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/octal-notation. diff --git a/dresscode/cs/rules/short-ternary-operator.texy b/dresscode/cs/rules/short-ternary-operator.texy new file mode 100644 index 0000000000..bec0b26785 --- /dev/null +++ b/dresscode/cs/rules/short-ternary-operator.texy @@ -0,0 +1,50 @@ +short-ternary-operator +********************** + +.[perex] +Ternár, který ve své prostřední části opakuje podmínku, se píše zkráceně jako `?:`. + +Opravuje · v presetech `dresscode/nette` · pokrývá `ternary_to_elvis_operator`, `SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator` .[rule-info] + + +Co pravidlo hlídá +================= + +Zápis `$a ? $a : $b` říká dvakrát totéž: když `$a` platí, vrať `$a`. PHP na to má zkrácený ternár `$a ?: $b`, který dá stejný výsledek a podmínku vyhodnotí jen jednou. Pravidlo najde ternár, jehož prostřední část je doslova stejná jako podmínka, a prostřední část vypustí. + +Zkrácení mění počet vyhodnocení podmínky a to nemusí být neškodné: `array_shift($queue) ? array_shift($queue) : null` odebere z fronty dva prvky, kdežto zkrácená podoba jen jeden. Pravidlo proto zkracuje jen výrazy, jejichž opakované čtení nemá vedlejší účinek: proměnné, prvky polí, vlastnosti a konstanty. Volání funkce nebo metody nechá být, i kdyby se opakovalo slovo od slova. Právě tady je vidět, co dává strom: PHP CS Fixer označuje svůj protějšek `ternary_to_elvis_operator` za rizikový (risky), protože nad polem tokenů nerozezná volání od proměnné a tu úvahu nechává na vás. Tady ji za vás udělá pravidlo. + +Ternár rozepsaný přes několik řádků a ternár s komentářem mezi otazníkem a dvojtečkou pravidlo nechává, protože by při zkrácení muselo komentář zahodit. + + +Příklad +======= + +```php .[before] +$name = $input ? $input : 'anonymous'; // A ternary repeating its condition must be written '?:' +$size = $options['size'] ? $options['size'] : 10; // A ternary repeating its condition must be written '?:' +$title = $this->title ? $this->title : $default; // A ternary repeating its condition must be written '?:' +$next = array_shift($queue) ? array_shift($queue) : null; +``` + +```php .[after] +$name = $input ?: 'anonymous'; +$size = $options['size'] ?: 10; +$title = $this->title ?: $default; +$next = array_shift($queue) ? array_shift($queue) : null; +``` + +Poslední řádek zůstal: `array_shift()` má vedlejší účinek, takže by zkrácení změnilo chování programu. + + +Související pravidla +==================== + +- `useless-ternary-operator` odstraní ternár, který vrací jen `true` a `false` +- `null-coalescing-operator` nahradí `isset($a) ? $a : $b` operátorem `??` + + +Zdroj +===== + +Třída "ShortTernaryOperatorRule":https://github.com/dg/dresscode/blob/master/src/Rules/Expressions/ShortTernaryOperatorRule.php, fixtury "short-ternary-operator":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/short-ternary-operator. diff --git a/dresscode/cs/rules/trailing-comma.texy b/dresscode/cs/rules/trailing-comma.texy new file mode 100644 index 0000000000..ca63b4d3eb --- /dev/null +++ b/dresscode/cs/rules/trailing-comma.texy @@ -0,0 +1,118 @@ +trailing-comma +************** + +.[perex] +Víceřádkový seznam končí čárkou za poslední položkou, jednořádkový ji nemá. + +Opravuje · v presetech `dresscode/per`, `dresscode/nette` · pokrývá `no_trailing_comma_in_singleline`, `trailing_comma_in_multiline`, `SlevomatCodingStandard.Arrays.TrailingArrayComma`, `SlevomatCodingStandard.Functions.RequireTrailingCommaInCall`, `SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration` .[rule-info] + + +Co pravidlo hlídá +================= + +Čárka za poslední položkou víceřádkového pole má praktický důvod: přidání další položky změní v diffu jeden řádek, ne dva. PHP ji od verze 7.3 snese i v argumentech volání a od verze 8.0 v parametrech a v seznamu `use` u closure, takže tentýž zvyk platí všude, kde se seznam rozepisuje na řádky. Naopak v seznamu na jednom řádku čárka na konci nic neusnadňuje a jen vypadá jako překlep. + +O všem rozhoduje zavírací závorka. Když stojí na vlastním řádku, seznam je víceřádkový a čárka tam patří; když stojí na řádku poslední položky, seznam se považuje za jednořádkový, i kdyby některá položka sama zabírala víc řádků, a čárka tam být nesmí. Volba `multiLine` říká, kterých druhů seznamů se čárka týká; `singleLine` řídí odstranění čárky z jednořádkových polí, argumentů a `list()`. + + +Příklad +======= + +```php .[before] +$colors = [ + 'red', + 'green' +]; // A multi-line array must end with a trailing comma + +$sizes = ['S', 'M', 'L',]; // No trailing comma in a one-line list + +$matrix = [ + [1, 2], + [3, 4],]; // No trailing comma before a closing bracket on the line of the last item of a multi-line array +``` + +```php .[after] +$colors = [ + 'red', + 'green', +]; + +$sizes = ['S', 'M', 'L']; + +$matrix = [ + [1, 2], + [3, 4]]; +``` + +Poslední případ ukazuje, kdo rozhoduje: závorka na řádku poslední položky dělá ze seznamu jednořádkový, takže tam čárka nepatří. Přesunout závorku na vlastní řádek je práce pravidla `multi-line-array`; teprve pak sem čárka přijde. + + +Volby +===== + + +multiLine .[option] +------------------- + +Seznam hodnot `arrays`, `arguments`, `parameters`, `match`, `closureUses`, výchozí `[arrays]`. Druhy seznamů, které při zavírací závorce na vlastním řádku končí čárkou. Preset `dresscode/per` zapíná všech pět, `dresscode/nette` pole, argumenty a parametry. + +Zadaný seznam nahrazuje výchozí, neslučuje se s ním: `multiLine: [arguments]` zapne čárku u argumentů a vypne ji u polí. + +```neon +rules: + dresscode/trailing-comma: + multiLine: [arrays, arguments, parameters] +``` + +```php .[before] +function send( + string $to, + string $subject +): void { // A multi-line parameter list must end with a trailing comma + mail( + $to, + $subject + ); // A multi-line argument list must end with a trailing comma +} +``` + +```php .[after] +function send( + string $to, + string $subject, +): void { + mail( + $to, + $subject, + ); +} +``` + + +singleLine .[option] +-------------------- + +`bool`, výchozí `true`. Z pole, seznamu argumentů nebo `list()` zapsaného na jednom řádku se koncová čárka odstraní, ať `multiLine` říká cokoli. S hodnotou `false` pravidlo jednořádkové seznamy nechá být. + +```neon +rules: + dresscode/trailing-comma: + singleLine: false +``` + +```php .[before] +$sizes = ['S', 'M', 'L',]; +``` + + +Související pravidla +==================== + +- `multi-line-array` rozepíše položky víceřádkového pole na samostatné řádky a zavírací závorku na vlastní řádek +- `multi-line-call` a `multi-line-signature` dělají totéž s argumenty a parametry + + +Zdroj +===== + +Třída "TrailingCommaRule":https://github.com/dg/dresscode/blob/master/src/Rules/Arrays/TrailingCommaRule.php, fixtury "trailing-comma":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/trailing-comma. diff --git a/dresscode/cs/rules/unused-imports.texy b/dresscode/cs/rules/unused-imports.texy new file mode 100644 index 0000000000..77c86269fb --- /dev/null +++ b/dresscode/cs/rules/unused-imports.texy @@ -0,0 +1,135 @@ +unused-imports +************** + +.[perex] +Import, který kód nikde nepoužije, se odstraní. + +Opravuje · v presetech `dresscode/nette` · pokrývá `no_unused_imports`, `SlevomatCodingStandard.Namespaces.UnusedUses` .[rule-info] + + +Co pravidlo hlídá +================= + +Každý `use` na začátku souboru je slib, že se to jméno v kódu objeví. Když slib neplatí, import jen mate: čtenář hledá, kde se třída používá, a našeptávání v editoru ho vede na cestu, která nikam nevede. Pravidlo projde všechny importy tříd, funkcí i konstant a ty, které nic nepoužívá, smaže. + +Použití je jakýkoli výskyt importovaného jména nebo jeho aliasu v kódu: v typu parametru či vlastnosti, za `new`, `extends`, `implements`, `instanceof`, v `catch`, v atributu, v `::class`, i jako první část částečně kvalifikovaného jména (`Prefix\Deep` použije import `Prefix`). Jméno zapsané v řetězci se nepočítá, protože pro PHP to je jen text. Jména tříd a funkcí se srovnávají bez ohledu na velikost písmen, jména konstant přesně, tak jak to dělá PHP. + +Import zmíněný jen v dokumentačním komentáři (`@var Order`, `@param User[]`) se počítá jako použitý, protože ho čte PHPStan i IDE; volba `searchAnnotations` to umí vypnout. Z importu s několika jmény (`use A, B;`) a ze skupinového importu (`use App\{A, B};`) pravidlo odstraní jen nepoužitou položku. + + +Příklad +======= + +```php .[before] +namespace App; + +use App\Model\User; +use App\Model\Order; // The import of 'Order' is unused +use App\Model\Address; +use function App\format; +use function App\slugify; // The import of 'slugify' is unused + +class Profile +{ + /** @var Address[] */ + private array $addresses = []; + + public function __construct( + private User $user, + ) { + } + + public function render(): string + { + return format($this->user); + } +} +``` + +```php .[after] +namespace App; + +use App\Model\User; +use App\Model\Address; +use function App\format; + +class Profile +{ + /** @var Address[] */ + private array $addresses = []; + + public function __construct( + private User $user, + ) { + } + + public function render(): string + { + return format($this->user); + } +} +``` + +`Address` zůstal, protože se vyskytuje v anotaci `@var`; kdyby pravidlo import smazalo, PHPStan by přestal typu rozumět. + + +Volby +===== + + +searchAnnotations .[option] +--------------------------- + +`bool`, výchozí `true`. Jméno třídy zmíněné v dokumentačním komentáři se počítá jako použití importu. + +Vypnout to dává smysl jen v kódu, kde dokumentační komentáře nečte žádný nástroj. Jinak import, který drží jen anotace, po smazání chybí PHPStanu i našeptávání v editoru. + +```neon +rules: + dresscode/unused-imports: + searchAnnotations: false +``` + +```php .[before] +namespace App; + +use App\Model\Address; // The import of 'Address' is unused +use App\Model\User; + +/** + * @param Address[] $addresses + */ +function first(array $addresses, User $owner): mixed +{ + return $addresses[0] ?? null; +} +``` + +```php .[after] +namespace App; + +use App\Model\User; + +/** + * @param Address[] $addresses + */ +function first(array $addresses, User $owner): mixed +{ + return $addresses[0] ?? null; +} +``` + + +Související pravidla +==================== + +- `ordered-imports` řadí importy abecedně +- `useless-alias` odstraní alias, který jméno nemění +- `use-from-same-namespace` odstraní import z vlastního jmenného prostoru +- `reference-used-names-only` nahradí plně kvalifikované jméno v kódu importem + + +Zdroj +===== + +Třída "UnusedImportsRule":https://github.com/dg/dresscode/blob/master/src/Rules/Namespaces/UnusedImportsRule.php, fixtury "unused-imports":https://github.com/dg/dresscode/tree/master/tests/DressCode/Rules/fixtures/unused-imports. diff --git a/dresscode/cs/suppressing.texy b/dresscode/cs/suppressing.texy new file mode 100644 index 0000000000..94988fe943 --- /dev/null +++ b/dresscode/cs/suppressing.texy @@ -0,0 +1,120 @@ +Potlačení pravidel a baseline +***************************** + +.[perex] +Jak vypnout pravidlo na jednom řádku, v bloku nebo v celém souboru, proč potlačení zastaví i opravu, a jak nasadit DressCode na velký projekt bez obřího commitu díky baseline. + + +Čtyři úrovně +============ + +Výjimky z pravidel osobně nemám rád a do kódu je nepíšu; když už, tak v konfiguraci pro celou cestu. Jsou ale místa, kde pravidlo prostě nemá pravdu, a tam se hodí přesný nástroj a ne kladivo. Potlačení (anglicky *suppression*) má čtyři úrovně, od nejužší po nejširší: + +| úroveň | jak | kde | +|---|---|---| +| jeden řádek nebo příkaz | `// dresscode:ignore` | v kódu | +| blok | `dresscode:disable` a `dresscode:enable` | v kódu | +| celý soubor | `dresscode:ignore-file` | v kódu | +| cesta | `excludeRulePaths`, `excludePaths` | v konfiguraci | + +Vedle nich stojí **baseline**, která není výjimka z pravidla, ale z času: zapíše porušení, která v projektu jsou dnes, a hlásí jen ta nová. + +Ať zvolíte kteroukoli, platí jedna věc: **potlačené porušení se neopraví.** Pravidlo smí kód změnit jen poté, co porušení ohlásilo a hlášení prošlo, a hlídá to jádro nástroje, ne autor pravidla. Potlačení tedy není jen ticho ve výpisu, ale skutečné vypnutí. + + +Potlačení na řádku +================== + +Komentář `dresscode:ignore` na konci řádku potlačí porušení na tomto řádku. Bez jména potlačí všechna pravidla, se jménem jen to jedno; víc jmen se odděluje čárkou: + +```php +$isEmpty = $value == null; // dresscode:ignore dresscode/strict-comparison +``` + +Komentář na vlastním řádku platí pro příkaz, který začíná na řádku pod ním, i když se ten příkaz táhne přes několik řádků: + +```php +// dresscode:ignore dresscode/multi-line-array +$matrix = [[1, 0, 0], + [0, 1, 0], + [0, 0, 1]]; +``` + +Funguje `//`, `#` i `/* */`. Jméno pravidla je to z výpisu; místo něj DressCode přijme i jméno pravidla PHP CS Fixeru nebo PHP_CodeSniffer, které jeho pravidlo pokrývá. Stejně tak rozumí komentářům `phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, `phpcs:ignoreFile` a anotaci `@phpcsSuppress`, takže kdo přechází z jiného nástroje, nemusí do kódu vůbec sáhnout. Přepis na nová jména pak udělá [dresscode migrate-suppressions |migration#3. Přepište komentáře]. + + +Potlačení v bloku +================= + +```php +// dresscode:disable dresscode/line-length +$data = ['alpha' => 1, 'beta' => 2, 'gamma' => 3, 'delta' => 4, 'epsilon' => 5, 'zeta' => 6, 'eta' => 7]; +$more = ['theta' => 8, 'iota' => 9, 'kappa' => 10, 'lambda' => 11, 'mu' => 12, 'nu' => 13, 'xi' => 14]; +// dresscode:enable +``` + +`disable` bez jména vypne všechna pravidla až po `enable`; bez `enable` platí do konce souboru. + + +Potlačení v celém souboru +========================= + +```php +<?php // dresscode:ignore-file +``` + +Tenhle komentář kdekoli v souboru vypne pro celý soubor všechna pravidla. Hodí se pro generovaný kód, který leží mezi ručně psaným. Když je takových souborů víc, je čistší vyloučit je cestou nebo podle obsahu, viz `skipWhen` v [konfiguraci |configuration#Cesty]. + + +Vypnutí pro cestu +================= + +Výjimka, která platí pro celý adresář, patří do konfigurace, ne do stovky souborů: + +```neon +excludeRulePaths: + dresscode/strict-comparison: [legacy] + dresscode/line-length: [tests/fixtures] +``` + +Zbytek pravidel takový soubor zkontroluje normálně. Celé cesty vynechá `excludePaths`; obojí popisuje stránka [Konfigurace |configuration#Cesty]. + + +Baseline +======== + +Na projektu s tisíci porušeními by první `fix` znamenal jeden obří commit. Někdy je to přesně to, co chcete udělat a mít za sebou. Někdy ne: kód se právě reviduje na jiné větvi, tým na to nemá týden, nebo chcete nové pravidlo zapnout jen pro nově psaný kód. Pro tyhle případy je baseline, tedy soupis porušení, která se dnes nemají hlásit. + +```shell +dresscode check --generate-baseline +``` + +/--pre .[terminal] +Baseline with 1408 violations written to dresscode-baseline.neon. +Name it in the configuration to make it apply. +\-- + +Soubor vznikne vedle konfigurace a ve stejném formátu (`.neon` vedle `dresscode.neon`, `.php` vedle `dresscode.php`). Platit začne ve chvíli, kdy ho konfigurace pojmenuje: + +```neon +baseline: dresscode-baseline.neon +``` + +Uvnitř je pro každý soubor seznam porušení s pravidlem, zprávou a otiskem: + +```neon +files: + src/Cart.php: + - + rule: dresscode/strict-comparison + message: 'The == comparison must be written ''===''' + fingerprint: a91a46b053d6d827 +``` + +Otisk se počítá z pravidla, zprávy a obsahu řádku, ne z jeho čísla, takže baseline přežije úpravy jinde v souboru. Porušení, která jsou v baseline, se nehlásí ani neopravují, a shrnutí běhu je přizná, aby nikdo nežil v domnění, že je uklizeno: + +/--pre .[terminal] +OK 1408 violations in the baseline in 214 files +\-- + +Když nějaké porušení z baseline zmizí, protože ho někdo opravil, běh upozorní, že položka už ničemu neodpovídá; stačí baseline vygenerovat znovu. Baseline se má zmenšovat, a jakmile je prázdná, smažte řádek z konfigurace a soubor s ním. diff --git a/dresscode/cs/testing-rules.texy b/dresscode/cs/testing-rules.texy new file mode 100644 index 0000000000..6ac3245753 --- /dev/null +++ b/dresscode/cs/testing-rules.texy @@ -0,0 +1,66 @@ +Testování pravidel +****************** + +.[perex] +`RuleTester` a formát fixtur: soubory `.code`, `.expected` a `.violations`, volby a cílová verze PHP v hlavičce fixtury, a co všechno tester ohlídá za vás. + + +Fixtury +======= + +Pravidlo se testuje nad adresářem fixtur, jedním na pravidlo. Fixtura (fixture) je až trojice souborů se stejným jménem: + +- `basic.code` je kód před opravou; +- `basic.expected` je kód po opravě; když soubor chybí, pravidlo nesmí kód změnit; +- `basic.violations` jsou očekávaná hlášení, každé na svém řádku ve tvaru řádek a zpráva: + +``` +3: The message of an exception must end with a period +6: The message of an exception must end with a period +``` + +Fixtura může na některém z prvních tří řádků nést volby pravidla jako JSON a verzi PHP, pro kterou je psaná: + +```php +<?php +// {"functions": ["dd", "dump"]} +// php 8.4 +``` + +Bez uvedené verze se pravidlo testuje na verzi ze své `minPhpVersion`, jinak na PHP 8.0. Nikdy ne na verzi interpretu, který testy spouští, protože verdikt pravidla má být na počítači nezávislý. + +Fixtura není ukázka do dokumentace: má být ošklivá a plná hraničních případů. Komentář uprostřed konstrukce, konstrukce na jednom řádku i přes tři, prázdné tělo, interpolovaný řetězec, alternativní syntaxe. Právě na těchhle místech se pravidla lámou. + + +RuleTester +========== + +```php +use DressCode\Testing\RuleTester; + +RuleTester::run(ExceptionMessagePeriodRule::class, __DIR__ . '/fixtures/exception-message-period'); +``` + +Metoda `run()` projde všechny soubory `*.code` v adresáři a vrátí jejich počet. Selhání je výjimka `DressCode\Testing\TestFailure` se jménem fixtury a s diffem, takže ji srozumitelně ukáže každý testovací framework. Z Nette Testeru je tohle celý test, z PHPUnit je to jedno volání v testovací metodě. + +U každé fixtury tester ověří: + +- **výstup** se rovná souboru `.expected` (nebo vstupu, když `.expected` není); +- **hlášení** se rovnají souboru `.violations`, řádek po řádku a ve stejném pořadí; +- **idempotenci**: pravidlo nad vlastním výstupem už nic nezmění ani neohlásí opravitelné porušení; +- **komentáře**: ve výstupu jsou všechny komentáře ze vstupu, pokud pravidlo nemá `modifiesComments`; +- **potlačení**: s komentářem `dresscode:ignore-file` v hlavičce pravidlo nic neohlásí ani nezmění; +- **kontrakt oprav**: žádná změna stromu bez hlášení, které prošlo; +- **strom**: každý uzel má správného rodiče, což se rozbije při chybném vkládání. + +Pravidlo, které volby dostává jinak než z konfigurace (třeba se závislostí v konstruktoru), předáte místo třídy jako továrnu `fn(array $options): Rule`. + +Pro zvláštní případy jsou tu menší nástroje: `RuleTester::runFixture()` spustí jedinou fixturu, `RuleTester::check()` ověří pravidlo nad řetězcem bez souborů (hodí se na rychlou reprodukci) a `RuleTester::collectViolations()` vrátí hlášení nad fixturou přímo ve tvaru souboru `.violations`, takže si ho můžete nechat zapsat a jen zkontrolovat diff, místo abyste hlášení opisovali ručně. + + +Zkouška v reálném provozu +========================= + +Test pravidla ověří pravidlo samotné. Jak se chová ve společnosti ostatních, ukáže až běh nad skutečným kódem. Přepínač `dresscode check --strict-rules` udělá z každého porušeného kontraktu chybu místo varování a `--jobs 1` nechá všechno běžet v jediném procesu, kde se pohodlně ladí. + +Než pravidlo zveřejníte, pusťte `fix` nad větším cizím kódem, třeba nad adresářem `vendor/`, a projděte si diff. Idempotenci a komentáře ohlídá tester, vkus ne. diff --git a/dresscode/cs/troubleshooting.texy b/dresscode/cs/troubleshooting.texy new file mode 100644 index 0000000000..1db4b36ed8 --- /dev/null +++ b/dresscode/cs/troubleshooting.texy @@ -0,0 +1,52 @@ +Řešení potíží +************* + +.[perex] +Proč je najednou všechno čisté, co znamenají hlášky o zacyklení pravidel nebo o porušeném kontraktu a co dělat, když se DressCode přetahuje s formátovačem v editoru. + + +Proč je čisto, i když nemá být +============================== + +Běh skončil s `OK` a vy víte, že by neměl. Odpověď najdete v hlavičce a ve shrnutí: + +- **`OK 36 violations in the baseline`**: porušení tam jsou, ale [baseline |suppressing#Baseline] je zná. Vygenerujte ji znovu, nebo ji z konfigurace odeberte. +- **Soubor není v rozsahu kontroly.** Hlavička říká, kolik souborů se kontroluje. Ve výchozím stavu jsou vyloučené `vendor`, `node_modules`, `temp`, `tmp`, `log` a všechny adresáře začínající tečkou, k tomu `excludePaths` z každé vrstvy. Pozor na to, že se vyloučené cesty jen sčítají a žádná vrstva je nemůže zrušit. +- **Pravidlo není zapnuté.** `dresscode rules` označí hvězdičkou ta, která platí. Pravidlo, které není v presetu, se zapíná v klíči `rules`. +- **Pravidlo se vynechalo kvůli verzi PHP.** Pravidlo pro novější syntaxi se pod cílovou verzí projektu samo vypne, a pokud jste ho zapnuli jménem, běh o tom napíše varování. Cílovou verzi ukazuje hlavička jako `Target PHP 8.2 from composer.json`. +- **Cache.** Soubor se stejným obsahem a stejnou konfigurací se přeskočí. Konfigurace je součástí klíče, takže její změna cache zneplatní sama; pro jistotu pomůže `--no-cache`. +- **Potlačení v kódu.** `dresscode:ignore` i `phpcs:ignore` platí i pro opravu, a `dresscode:disable` bez `enable` platí až do konce souboru. + + +Hlášky, které stojí za vysvětlení +================================= + +**`No paths given and none configured.`** Bez konfiguračního souboru musíte cesty zadat na příkazové řádce; se souborem stačí klíč `paths`. + +**`Unknown rule 'no_unused_imports'. It is covered by dresscode/unused-imports; run dresscode import to translate a configuration of another tool.`** Do konfigurace patří jména pravidel DressCode. Cizí jméno hláška rovnou přeloží, celý cizí konfigurační soubor převede příkaz [import |migration#2. Přeložte konfiguraci]. + +**`Unknown rule 'acme/foo'.`** Buď překlep, nebo pravidlo z rozšíření, které není uvedené v klíči `extensions` ani zapsané názvem třídy. + +**`Rules acme/one, acme/two do not converge in src/Cart.php.`** Dvě pravidla se přetahují: jedno kód změní, druhé ho vrátí zpátky, a strom se tak dostal do stavu, ve kterém už jednou byl. Jádro to pozná, soubor ohlásí jako selhání (exit kód `2`) a nic do něj nezapíše. Vestavěná pravidla se nekříží, takže hláška ukazuje na vlastní pravidlo nebo na rozšíření a jmenuje obě strany sporu. Než to vyřešíte, jedno z nich vypněte. + +**Varování o pravidle, které změnilo kód bez ohlášení** (nebo po potlačeném hlášení) znamená, že pravidlo porušilo kontrakt: každá změna stromu musí následovat až po volání `report()`, které vrátilo `true`. U vestavěných pravidel to hlídá `RuleTester`, u vlastního pravidla je to chyba v něm. Přepínač `--strict-rules` z takového varování udělá chybu, aby neproklouzlo testy. + +**Chyba konfigurace o dvou pravidlech, která chtějí rozhodovat o téže mezeře.** Dvě [pravidla pro bílé znaky |whitespace-rules] si nárokují totéž místo; jádro to odmítne hned při startu, protože jinak by výsledek závisel na pořadí. Stát se to může jen s rozšířením. + +**Soubor se syntaktickou chybou** se vypíše s číslem řádku a exit kód je `1`, stejně jako u porušení. DressCode ho neopravuje, ale ani mlčky nepřeskočí: na kódu, který PHP nepřijme, se styl měřit nedá. + +**`Warning: 3 entries of the baseline no longer match a violation; regenerate it`** znamená, že někdo opravil porušení, které baseline zná. Nic se neděje, jen baseline přestala odpovídat skutečnosti; vygenerujte ji znovu. + + +Editor a formátování +==================== + +Když se soubor po každém uložení změní dvakrát, přetahují se v něm dva formátovače. Vypněte vestavěné formátování PHP v PhpStormu nebo výchozí formátovač pro PHP ve VS Code, jak popisuje stránka [Editory a IDE |editors]. + +Když DressCode v editoru zdánlivě nic nedělá, zkontrolujte, jestli běží nad kořenem projektu s konfigurací. Hledá ji od adresáře souboru směrem nahoru, takže otevřený podadresář bez konfigurace znamená, že platí výchozí preset. + + +Konce řádků +=========== + +Hlášení `Wrong line ending` znamená, že konfigurace nebo preset předepisuje pevný konec řádku (`eol: "\n"`) a soubor má jiný. Projekt, který chce konce řádků zachovat tak, jak jsou, nastaví `eol: auto`. Projekt na Windows, kde konce řádků převádí Git, má převod nechat na Gitu a v DressCode nastavit `"\n"`. diff --git a/dresscode/cs/whitespace-rules.texy b/dresscode/cs/whitespace-rules.texy new file mode 100644 index 0000000000..29abf74731 --- /dev/null +++ b/dresscode/cs/whitespace-rules.texy @@ -0,0 +1,126 @@ +Pravidla pro bílé znaky +*********************** + +.[perex] +Pravidla, která hlídají mezery, zalomení řádků a prázdné řádky, se píšou jinak než ostatní: do kódu vůbec nesahají. Jen řeknou, co má být mezi dvěma tokeny, a o zbytek se postará jádro nástroje. + + +Co je mezera mezi tokeny +======================== + +Nejdřív pojem, na kterém celá stránka stojí. **Mezerou mezi tokeny** se tu myslí všechno, co ve zdrojáku stojí mezi dvěma sousedními tokeny: buď nic, nebo jedna či víc mezer, nebo zalomení řádku, prázdné řádky a komentáře. V API se takové místo jmenuje `Gap`. + +```php +$sum = $a + $b; +``` + +Tenhle řádek má pět mezer v tomhle smyslu: mezi `$sum` a `=`, mezi `=` a `$a`, mezi `$a` a `+`, mezi `+` a `$b` a nakonec mezi `$b` a `;`. Ta poslední je taky mezera, jen v ní nic nestojí, a i o ní se dá něco prohlásit: že tam nemá být nic. + +Pravidlo pro bílé znaky (třída `GapRule`) o takovém místě řekne, jak má vypadat. Nepíše do něj, nemaže z něj a nic v něm nehledá. + + +Proč pravidla bílé znaky nepřepisují +==================================== + +Kdyby si pravidlo o mezeře za čárkou psalo mezeru samo a pravidlo o zalomení dlouhého seznamu si samo psalo konec řádku, potkala by se na jednom místě dvě pravidla a vyhrálo by to, které náhodou běželo později. Podruhé by to mohlo dopadnout obráceně. + +Proto pravidla pro bílé znaky do kódu nesahají. Každé jen vysloví **požadavek** (v API `Claim`), co má na daném místě být, a jádro nástroje, které vidí všechny požadavky najednou, rozhodne, opraví a hlášení vypíše pod jménem toho pravidla, jehož požadavek vyhrál. + +Z toho plyne to, co vidí uživatel: každá mezera má právě jednoho vlastníka a dvě pravidla, která by si nárokovala totéž místo, jsou chyba konfigurace, na kterou nástroj upozorní hned při startu. + + +Požadavek +========= + +Požadavek `Claim` má čtyři složky a pravidlo vyplní jen ty, na kterých mu záleží: + +| složka | co říká | hodnoty | +|---|---|---| +| `space` | vodorovná mezera, když jsou oba tokeny na jednom řádku | `Space::None`, `Single`, `AtLeastSingle` a varianty `SingleOrTabs`, `AtLeastSingleOrTabs`, které nechají projít tabulátor zarovnávající sloupce | +| `line` | jestli druhý token stojí na řádku prvního, nebo na dalším | `Line::Same`, `Line::Next` | +| `blank` | kolik prázdných řádků, když je tam zalomení | počet, nebo rozsah `[min, max]` s `null` jako otevřeným koncem | +| `blankBelowComment` | prázdné řádky mezi komentářem v mezeře a tokenem | totéž | + +Nejčastější požadavky mají továrny, které vracejí sdílené instance: `Claim::none()`, `single()`, `atLeastSingle()`, `sameLine()`, `nextLine()` a `blank(1)`. Požadavek na víc složek najednou nebo požadavek s odůvodněním se staví konstruktorem: `new Claim(Space::None, line: Line::Same)` nebo `new Claim(line: Line::Next, because: 'the line is 135 characters long')`. Odůvodnění jádro připojí za čárku k hlášce. + +Prázdné řádky se počítají **nad** komentářem, který stojí na vlastních řádcích nad tokenem, protože komentář patří ke kódu pod sebou. Výjimkou je komentář před zavírací závorkou a před koncem souboru, který patří k tomu, co je nad ním. Řádky mezi posledním komentářem a tokenem tvoří vlastní složku `blankBelowComment`; tu si nárokuje třeba pravidlo o mezeře mezi dokumentačním komentářem a deklarací. + + +Kde požadavek platí +=================== + +Pravidlo dědí od `GapRule` a v metodě `getClaims()` řekne, u kterých slotů kterých uzlů má požadavek, a to zvlášť před hodnotou slotu a za ní: + +```php +final class BlankLineBeforeReturnRule extends GapRule +{ + public function getClaims(): array + { + return [ + '*' => [ + 'stmts:item' => [ + fn(Gap $gap) => $gap->value instanceof ReturnNode && $gap->index > 0 ? Claim::blank(1) : null, + null, + ], + ], + ]; + } +} +``` + +Klíč první úrovně je třída uzlu, nebo `*` pro každý uzel, který takový slot má. Klíč druhé úrovně je jméno slotu (`'body'`), případně `slot:item` pro každou položku seznamu (`'stmts:item'`, `'items:item'`) nebo `slot:separator` pro jeho oddělovače (`'items:separator'`). Hodnotou je dvojice požadavků, před a za: buď `Claim`, nebo `null`, když pravidlo nic nechce, nebo anonymní funkce, která dostane `Gap` a vrátí požadavek či `null`. Požadavek u slotu, ve kterém sedí uzel, platí pro jeho první token (před) nebo poslední token (za). + +Jméno slotu je řetězec, takže ho refaktoring nepřejmenuje spolu se slotem. Přejmenovaný slot udělá z požadavku tichý omyl, který odhalí až fixtura pravidla; jména slotů najdete v [přehledu uzlů |phpsyntax:nodes]. + +Ukázka výše říká: před každým příkazem seznamu, který je `return` a není první v pořadí, má být jeden prázdný řádek. Pravidlo nenavštěvuje žádné uzly, protože `GapRule` návštěvy vůbec nemá; všechnu práci odvede jádro z požadavků. Hlášení zní `Expected 1 blank line before the return, 0 found` a skládá ho jádro z toho, u čeho požadavek stojí. Stejně vznikají hlášky `A single space after the comma` nebo `A line break before the opening brace`. Autor pravidla texty hlášek nepíše. + +Anonymní funkce dostane v objektu `Gap` token na okraji mezery, hodnotu slotu nebo položku, které se požadavek týká, její index v seznamu a styl souboru. Podle toho se rozhodne, například jinak pro tečku než pro ostatní operátory: + +```php +public function getClaims(): array +{ + $operator = fn(Gap $gap): ?Claim => $gap->token->text === '.' ? null : $this->claim; + return [ + BinaryNode::class => ['operator' => [$operator, $operator]], + AssignNode::class => ['operator' => [$this->claim, $this->claim]], + ]; +} +``` + +Pravidlo s volbou si požadavek postaví jednou v `configure()` a v anonymní funkci ho jen vrací; nestaví ho u každé mezery znovu. + + +Rozhodnutí, které musí být pokaždé stejné +========================================= + +Požadavek, který závisí na tvaru kódu (seznam už je rozlomený na řádky, řádek je moc dlouhý), musí dát každé mezeře téže konstrukce stejnou odpověď, ať už jádro s předchozími mezerami mezitím udělalo cokoli. Jinak by první čárka seznam rozlomila a druhá ho zase slepila. Takové rozhodnutí se proto dělá jen jednou: + +```php +fn(Gap $gap) => $gap->once($list, fn() => $this->isBroken($list)) ? Claim::nextLine() : null +``` + +Metoda `once()` vyhodnotí funkci u první mezery daného uzlu a stejnou odpověď pak vrací u všech dalších až do konce průchodu. Seznam se přitom počítá za rozlomený, jakmile některá položka nebo zavírací závorka začíná řádek, aby rozhodnutí rozlomit platilo pro celý zbytek. + + +Kdo vyhraje +=========== + +Jádro skládá požadavky obou stran mezery složku po složce, a to podle pevných pravidel, ne podle pořadí v konfiguraci: + +- požadavek konkrétní třídy má přednost před požadavkem `*`, +- požadavek vnitřního slotu před požadavkem předka, +- u vodorovné mezery přísnější před volnějším, takže u `return;` vyhraje `No whitespace` nad `At least one space`, +- u zalomení řádku má `Line::Next` přednost před `Line::Same`, +- u prázdných řádků platí průnik toho, co obě strany dovolí, a když se rozsahy vylučují, ten užší z nich. + +Dva pevné požadavky na tutéž složku téže strany téhož slotu jsou `ConfigurationException` už při skládání pravidel. Dvě anonymní funkce si slot rozdělit smějí, protože každá z nich může tam, kde rozhoduje ta druhá, vrátit `null`. Vestavěná pravidla se nekříží; rozšíření, které chce jednu mezeru řešit jinak než vestavěné pravidlo, ho pro to místo dnes musí vypnout a požadavek převzít, nebo požádat o novou volbu. + +Vynucené zalomení řádku jádro udělá hned a prázdné řádky doladí až v dalším průchodu. Zakázané zalomení naopak odstraní jen tehdy, když v mezeře není nic než bílé znaky. Odsazení řádku, který zalomením vznikl, není věcí požadavku: jádro mu dá obvyklé odsazení a pravidlo `indentation` ho pak umístí přesně. + + +Testování +========= + +Pravidlo pro bílé znaky se testuje stejně jako každé jiné, `RuleTester`em nad [fixturami |testing-rules]; vypsané hlášky jsou ty, které složilo jádro. + +Co `GapRule` neumí, je sáhnout na tokeny: skládá se z požadavků a nic jiného nedělá. Když je u jedné konstrukce potřeba obojí, jsou z toho dvě pravidla se dvěma jmény, jedno `GapRule` a jedno [`NodeRule` |rule-contract]. Vestavěná dvojice `attribute-spacing` a `useless-attribute-parentheses` je přesně tenhle případ: první si nárokuje bílé znaky uvnitř `#[...]`, druhé odstraňuje prázdné závorky za jménem atributu. Vypadá to jako zbytečná komplikace, dokud někdo nechce vypnout jen jednu z těch dvou věcí. diff --git a/dresscode/meta.json b/dresscode/meta.json new file mode 100644 index 0000000000..c5978ae0ec --- /dev/null +++ b/dresscode/meta.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "repo": "dg/dresscode", + "composer": "dresscode/dresscode" +} diff --git a/forms/bg/@home.texy b/forms/bg/@home.texy deleted file mode 100644 index ca9dfebeda..0000000000 --- a/forms/bg/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms донесоха революция в създаването на уеб форми. Изведнъж стана достатъчно да напишете няколко разбираеми реда код и имахте готова форма, включително рендиране, JavaScript и сървърна валидация, и освен това отлично защитена. Ще ви покажем как: - -- да създавате удобни за потребителя форми -- да валидирате изпратените данни -- да рендирате елементи точно според нуждите - -</div> - - -Използвайки Nette Forms, ще избегнете редица рутинни задачи, като например писане на валидация (при това двойна, на страната на сървъра и клиента), ще минимизирате вероятността от възникване на грешки и пропуски в сигурността. - -Формите можете да използвате или като част от Nette Приложение (т.е. в презентери), или напълно самостоятелно. Тъй като в двата случая използването се различава малко, подготвихме за вас два урока: - -<div class="wiki-buttons"> -<div> "Форми в презентери .[wiki-button]":in-presenter </div> -<div> "Форми самостоятелно .[wiki-button]":standalone </div> -</div> - - -Инсталация ----------- - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/bg/@left-menu.texy b/forms/bg/@left-menu.texy deleted file mode 100644 index 489560e8be..0000000000 --- a/forms/bg/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Въведение |@home] -- [Форми в презентери|in-presenter] -- [Форми самостоятелно|standalone] -- [Формулярни елементи |controls] -- [Валидация |validation] -- [Рендиране |rendering] -- [Конфигурация |configuration] - - -Допълнително четене -******************* -- [Ръководства и процедури |best-practices:] diff --git a/forms/bg/@meta.texy b/forms/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/forms/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/forms/bg/configuration.texy b/forms/bg/configuration.texy deleted file mode 100644 index a0e73d82cc..0000000000 --- a/forms/bg/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Конфигурация на формуляри -************************* - -.[perex] -В конфигурацията могат да се променят съобщенията за грешки във формуляри по подразбиране [съобщения за грешки във формуляри|validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Ето превода на български език: - -```neon -forms: - messages: - Equal: 'Моля, въведете %s.' - NotEqual: 'Тази стойност не трябва да бъде %s.' - Filled: 'Това поле е задължително.' - Blank: 'Това поле трябва да бъде празно.' - MinLength: 'Моля, въведете поне %d знака.' - MaxLength: 'Моля, въведете не повече от %d знака.' - Length: 'Моля, въведете стойност с дължина между %d и %d знака.' - Email: 'Моля, въведете валиден имейл адрес.' - URL: 'Моля, въведете валиден URL адрес.' - Integer: 'Моля, въведете валидно цяло число.' - Float: 'Моля, въведете валидно число.' - Min: 'Моля, въведете стойност, по-голяма или равна на %d.' - Max: 'Моля, въведете стойност, по-малка или равна на %d.' - Range: 'Моля, въведете стойност между %d и %d.' - MaxFileSize: 'Размерът на качения файл може да бъде до %d байта.' - MaxPostSize: 'Качените данни надвишават ограничението от %d байта.' - MimeType: 'Каченият файл не е в очаквания формат.' - Image: 'Каченият файл трябва да бъде изображение във формат JPEG, GIF, PNG или WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Моля, изберете валидна опция.' - Nette\Forms\Controls\UploadControl::Valid: 'Възникна грешка при качване на файла.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Вашата сесия изтече. Моля, върнете се на началната страница и опитайте отново.' -``` - -Ако не използвате целия framework и следователно не използвате конфигурационни файлове, можете да промените съобщенията за грешки по подразбиране директно в масива `Nette\Forms\Validator::$messages`. diff --git a/forms/bg/controls.texy b/forms/bg/controls.texy deleted file mode 100644 index cf98691e43..0000000000 --- a/forms/bg/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Елементи на формуляр -******************** - -.[perex] -Преглед на стандартните елементи на формуляр. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Добавя едноредово текстово поле (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Ако потребителят не попълни полето, връща празен низ `''`, или чрез `setNullable()` може да се укаже да връща `null`. - -```php -$form->addText('name', 'Име:') - ->setRequired() - ->setNullable(); -``` - -Автоматично валидира UTF-8, премахва водещите и крайните интервали и премахва знаците за нов ред, които атакуващ би могъл да изпрати. - -Максималната дължина може да се ограничи чрез `setMaxLength()`. Промяна на въведената от потребителя стойност позволява [addFilter() |validation#Модификация на входа]. - -Чрез `setHtmlType()` може да се промени визуалният характер на текстовото поле на типове като `search`, `tel` или `url` вижте [спецификация|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Помнете, че промяната на типа е само визуална и не замества функцията за валидация. За тип `url` е препоръчително да се добави специфично [правило URL |validation#Текстови полета]. - -.[note] -За други типове входове, като `number`, `range`, `email`, `date`, `datetime-local`, `time` и `color`, използвайте специализирани методи като [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] и [#addColor], които осигуряват сървърна валидация. Типовете `month` и `week` засега не се поддържат напълно във всички браузъри. - -На елемента може да се зададе т.нар. empty-value, което е нещо като стойност по подразбиране, но ако потребителят не я промени, елементът връща празен низ или `null`. - -```php -$form->addText('phone', 'Телефон:') - ->setHtmlType('tel') - ->setEmptyValue('+359'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Добавя поле за въвеждане на многоредов текст (клас [TextArea |api:Nette\Forms\Controls\TextArea]). Ако потребителят не попълни полето, връща празен низ `''`, или чрез `setNullable()` може да се укаже да връща `null`. - -```php -$form->addTextArea('note', 'Бележка:') - ->addRule($form::MaxLength, 'Бележката е твърде дълга', 10000); -``` - -Автоматично валидира UTF-8 и нормализира разделителите на редове на `\n`. За разлика от едноредовото входно поле, не се извършва премахване на интервали. - -Максималната дължина може да се ограничи чрез `setMaxLength()`. Промяна на въведената от потребителя стойност позволява [addFilter() |validation#Модификация на входа]. Може да се зададе т.нар. empty-value чрез `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Добавя поле за въвеждане на цяло число (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Връща или integer, или `null`, ако потребителят не въведе нищо. - -```php -$form->addInteger('year', 'Година:') - ->addRule($form::Range, 'Годината трябва да бъде в диапазона от %d до %d.', [1900, 2023]); -``` - -Елементът се рендира като `<input type="number">`. Чрез използване на метода `setHtmlType()` може да се промени типът на `range` за показване под формата на плъзгач, или на `text`, ако предпочитате стандартно текстово поле без специалното поведение на тип `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Добавя поле за въвеждане на десетично число (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Връща или float, или `null`, ако потребителят не въведе нищо. - -```php -$form->addFloat('level', 'Ниво:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Нивото трябва да бъде в диапазона от %d до %d.', [0, 100]); -``` - -Елементът се рендира като `<input type="number">`. Чрез използване на метода `setHtmlType()` може да се промени типът на `range` за показване под формата на плъзгач, или на `text`, ако предпочитате стандартно текстово поле без специалното поведение на тип `number`. - -Nette и браузърът Chrome приемат като разделител на десетичните места както запетая, така и точка. За да бъде тази функционалност достъпна и във Firefox, е препоръчително да се зададе атрибутът `lang` или за дадения елемент, или за цялата страница, например `<html lang="bg">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Добавя поле за въвеждане на имейл адрес (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Ако потребителят не попълни полето, връща празен низ `''`, или чрез `setNullable()` може да се укаже да връща `null`. - -```php -$form->addEmail('email', 'Имейл:'); -``` - -Оверява дали стойността е валиден имейл адрес. Не се проверява дали домейнът действително съществува, проверява се само синтаксисът. Автоматично валидира UTF-8, премахва водещите и крайните интервали. - -Максималната дължина може да се ограничи чрез `setMaxLength()`. Промяна на въведената от потребителя стойност позволява [addFilter() |validation#Модификация на входа]. Може да се зададе т.нар. empty-value чрез `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Добавя поле за въвеждане на парола (клас [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Парола:') - ->setRequired() - ->addRule($form::MinLength, 'Паролата трябва да съдържа поне %d знака', 8) - ->addRule($form::Pattern, 'Трябва да съдържа цифра', '.*[0-9].*'); -``` - -При повторно показване на формуляра полето ще бъде празно. Автоматично валидира UTF-8, премахва водещите и крайните интервали и премахва знаците за нов ред, които атакуващ би могъл да изпрати. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Добавя чекбокс (клас [Checkbox |api:Nette\Forms\Controls\Checkbox]). Връща стойност `true` или `false`, в зависимост от това дали е отметнат. - -```php -$form->addCheckbox('agree', 'Съгласен съм с условията') - ->setRequired('Необходимо е да се съгласите с условията'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Добавя чекбоксове за избор на няколко елемента (клас [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Връща масив от ключовете на избраните елементи. Методът `getSelectedItems()` връща стойностите вместо ключовете. - -```php -$form->addCheckboxList('colors', 'Цветове:', [ - 'r' => 'червен', - 'g' => 'зелен', - 'b' => 'син', -]); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. - -Чрез `setDisabled(['r', 'g'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраните елементи са действително едни от предлаганите и не са били деактивирани. Чрез метода `getRawValue()` могат да се получат изпратените елементи без тази важна проверка. - -При задаване на избраните по подразбиране елементи също проверява дали те са едни от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - -Ако изпращате формуляра с метод `GET`, можете да изберете по-компактен начин за пренос на данни, който спестява размер на query string-а. Активира се чрез задаване на HTML атрибут на формуляра: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Добавя радио бутони (клас [RadioList |api:Nette\Forms\Controls\RadioList]). Връща ключа на избрания елемент, или `null`, ако потребителят не е избрал нищо. Методът `getSelectedItem()` връща стойността вместо ключа. - -```php -$sex = [ - 'm' => 'мъж', - 'f' => 'жена', -]; -$form->addRadioList('gender', 'Пол:', $sex); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. - -Чрез `setDisabled(['m', 'f'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраният елемент е действително един от предлаганите и не е бил деактивиран. Чрез метода `getRawValue()` може да се получи изпратеният елемент без тази важна проверка. - -При задаване на избрания по подразбиране елемент също проверява дали той е един от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Добавя селект бокс (клас [SelectBox |api:Nette\Forms\Controls\SelectBox]). Връща ключа на избрания елемент, или `null`, ако потребителят не е избрал нищо. Методът `getSelectedItem()` връща стойността вместо ключа. - -```php -$countries = [ - 'BG' => 'България', - 'CZ' => 'Чешка република', - 'SK' => 'Словакия', -]; - -$form->addSelect('country', 'Държава:', $countries) - ->setDefaultValue('BG'); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. Елементите могат да бъдат и двумерен масив: - -```php -$countries = [ - 'Европа' => [ - 'BG' => 'България', - 'CZ' => 'Чешка република', - 'SK' => 'Словакия', - ], - 'CA' => 'Канада', - 'US' => 'САЩ', - '?' => 'друга', -]; -``` - -При селект боксовете често първият елемент има специално значение, служи като призив за действие. За добавяне на такъв елемент служи методът `setPrompt()`. - -```php -$form->addSelect('country', 'Държава:', $countries) - ->setPrompt('Изберете държава'); -``` - -Чрез `setDisabled(['CZ', 'SK'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраният елемент е действително един от предлаганите и не е бил деактивиран. Чрез метода `getRawValue()` може да се получи изпратеният елемент без тази важна проверка. - -При задаване на избрания по подразбиране елемент също проверява дали той е един от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Добавя селект бокс за избор на няколко елемента (клас [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Връща масив от ключовете на избраните елементи. Методът `getSelectedItems()` връща стойностите вместо ключовете. - -```php -$form->addMultiSelect('countries', 'Държави:', $countries); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. Елементите могат да бъдат и двумерен масив. - -Чрез `setDisabled(['CZ', 'SK'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраните елементи са действително едни от предлаганите и не са били деактивирани. Чрез метода `getRawValue()` могат да се получат изпратените елементи без тази важна проверка. - -При задаване на избраните по подразбиране елементи също проверява дали те са едни от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Добавя поле за качване на файл (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Връща обект [FileUpload |http:request#FileUpload] и то дори в случай, че потребителят не е изпратил никакъв файл, което може да се установи чрез метода `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Аватар:') - ->addRule($form::Image, 'Аватарът трябва да е JPEG, PNG, GIF, WebP или AVIF.') - ->addRule($form::MaxFileSize, 'Максималният размер е 1 MB.', 1024 * 1024); -``` - -Ако файлът не успее да се качи коректно, формулярът не е успешно изпратен и се показва грешка. Т.е. при успешно изпращане не е необходимо да се проверява методът `FileUpload::isOk()`. - -Никога не вярвайте на оригиналното име на файла, върнато от метода `FileUpload::getName()`, клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - -Правилата `MimeType` и `Image` откриват изисквания тип въз основа на сигнатурата на файла и не проверяват неговата цялост. Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Добавя поле за качване на няколко файла едновременно (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Връща масив от обекти [FileUpload |http:request#FileUpload]. Методът `FileUpload::hasFile()` при всеки от тях ще връща `true`. - -```php -$form->addMultiUpload('files', 'Файлове:') - ->addRule($form::MaxLength, 'Могат да бъдат качени максимум %d файла', 10); -``` - -Ако някой файл не успее да се качи коректно, формулярът не е успешно изпратен и се показва грешка. Т.е. при успешно изпращане не е необходимо да се проверява методът `FileUpload::isOk()`. - -Никога не вярвайте на оригиналните имена на файловете, върнати от метода `FileUpload::getName()`, клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - -Правилата `MimeType` и `Image` откриват изисквания тип въз основа на сигнатурата на файла и не проверяват неговата цялост. Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Добавя поле, което позволява на потребителя лесно да въведе дата, състояща се от година, месец и ден (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Като стойност по подразбиране приема или обекти, имплементиращи интерфейса `DateTimeInterface`, низ с време, или число, представляващо UNIX timestamp. Същото важи и за аргументите на правилата `Min`, `Max` или `Range`, които дефинират минималната и максималната разрешена дата. - -```php -$form->addDate('date', 'Дата:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Датата трябва да е поне преди един месец.', new DateTime('-1 month')); -``` - -Стандартно връща обект `DateTimeImmutable`, чрез метода `setFormat()` можете да специфицирате [текстов формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] или timestamp: - -```php -$form->addDate('date', 'Дата:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Добавя поле, което позволява на потребителя лесно да въведе час, състоящ се от часове, минути и по избор и секунди (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Като стойност по подразбиране приема или обекти, имплементиращи интерфейса `DateTimeInterface`, низ с време, или число, представляващо UNIX timestamp. От тези входове се използва само информацията за времето, датата се игнорира. Същото важи и за аргументите на правилата `Min`, `Max` или `Range`, които дефинират минималния и максималния разрешен час. Ако зададената минимална стойност е по-висока от максималната, се създава времеви диапазон, преминаващ през полунощ. - -```php -$form->addTime('time', 'Час:', withSeconds: true) - ->addRule($form::Range, 'Часът трябва да бъде в диапазона от %s до %s.', ['12:30', '13:30']); -``` - -Стандартно връща обект `DateTimeImmutable` (с дата 1 януари година 1), чрез метода `setFormat()` можете да специфицирате [текстов формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Час:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Добавя поле, което позволява на потребителя лесно да въведе дата и час, състоящи се от година, месец, ден, часове, минути и по избор и секунди (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Като стойност по подразбиране приема или обекти, имплементиращи интерфейса `DateTimeInterface`, низ с време, или число, представляващо UNIX timestamp. Същото важи и за аргументите на правилата `Min`, `Max` или `Range`, които дефинират минималната и максималната разрешена дата. - -```php -$form->addDateTime('datetime', 'Дата и час:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Датата трябва да е поне преди един месец.', new DateTime('-1 month')); -``` - -Стандартно връща обект `DateTimeImmutable`, чрез метода `setFormat()` можете да специфицирате [текстов формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] или timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Добавя поле за избор на цвят (клас [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Цветът е низ във формата `#rrggbb`. Ако потребителят не направи избор, се връща черен цвят `#000000`. - -```php -$form->addColor('color', 'Цвят:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Добавя скрито поле (клас [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Чрез `setNullable()` може да се настрои да връща `null` вместо празен низ. Промяна на изпратената стойност позволява [addFilter() |validation#Модификация на входа]. - -Въпреки че елементът е скрит, е **важно да се осъзнае**, че стойността все още може да бъде модифицирана или подправена от атакуващ. Винаги щателно проверявайте и валидирайте всички получени стойности на сървърна страна, за да се предотвратят рискове за сигурността, свързани с манипулиране на данни. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Добавя бутон за изпращане (клас [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Изпрати'); -``` - -Във формуляра е възможно да има и няколко бутона за изпращане: - -```php -$form->addSubmit('register', 'Регистрирай се'); -$form->addSubmit('cancel', 'Отказ'); -``` - -За да установите кой от тях е бил кликнат, използвайте: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Ако не искате да валидирате целия формуляр при натискане на бутона (например при бутони *Отказ* или *Преглед*), използвайте [setValidationScope() |validation#Изключване на валидацията]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Добавя бутон (клас [Button |api:Nette\Forms\Controls\Button]), който няма функция за изпращане. Може следователно да се използва за някаква друга функция, напр. извикване на JavaScript функция при кликване. - -```php -$form->addButton('raise', 'Увеличи заплатата') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Добавя бутон за изпращане под формата на изображение (клас [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -При използване на няколко бутона за изпращане може да се установи кой е бил кликнат, чрез `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Добавя подформуляр (клас [Container|api:Nette\Forms\Container]), или контейнер, в който могат да се добавят други елементи по същия начин, както ги добавяме към формуляра. Работят и методите `setDefaults()` или `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Вашето име:'); -$sub1->addEmail('email', 'Имейл:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Вашето име:'); -$sub2->addEmail('email', 'Имейл:'); -``` - -Изпратените данни след това връща като многомерна структура: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Преглед на настройките -====================== - -При всички елементи можем да извикваме следните методи (пълен преглед в [API документация|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | задава стойност по подразбиране -| `getValue()` | получава текущата стойност -| `setOmitted()` | [#пропускане на стойност] -| `setDisabled()` | [#деактивиране на елементи] - -Рендиране: -.[table-form-methods language-php] -| `setCaption($caption)` | променя етикета на елемента -| `setTranslator($translator)` | задава [преводач |rendering#Превод] -| `setHtmlAttribute($name, $value)` | задава [HTML атрибут |rendering#HTML атрибути] на елемента -| `setHtmlId($id)` | задава HTML атрибут `id` -| `setHtmlType($type)` | задава HTML атрибут `type` -| `setHtmlName($name)` | задава HTML атрибут `name` -| `setOption($key, $value)` | [настройка за рендиране |rendering#Options] - -Валидация: -.[table-form-methods language-php] -| `setRequired()` | [задължителен елемент |validation] -| `addRule()` | задава [правило за валидация |validation#Правила] -| `addCondition()`, `addConditionOn()` | задава [условие за валидация |validation#Условия] -| `addError($message)` | [предаване на съобщение за грешка |validation#Грешки при обработка] - -При елементите `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` могат да се извикват следните методи: - -.[table-form-methods language-php] -| `setNullable()` | задава дали getValue() да връща `null` вместо празен низ -| `setEmptyValue($value)` | задава специална стойност, която се счита за празен низ -| `setMaxLength($length)` | задава максималния брой разрешени знаци -| `addFilter($filter)` | [редактиране на въведеното |validation#Модификация на входа] - - -Пропускане на стойност -====================== - -Ако попълнената от потребителя стойност не ни интересува, можем чрез `setOmitted()` да я пропуснем от резултата на метода `$form->getValues()` или от данните, предавани на хендлърите. Това е полезно за различни пароли за проверка, антиспам елементи и т.н. - -```php -$form->addPassword('passwordVerify', 'Парола за проверка:') - ->setRequired('Моля, въведете паролата отново за проверка') - ->addRule($form::Equal, 'Паролите не съвпадат', $form['password']) - ->setOmitted(); -``` - - -Деактивиране на елементи -======================== - -Елементите могат да се деактивират чрез `setDisabled()`. Такъв елемент потребителят не може да редактира. - -```php -$form->addText('username', 'Потребителско име:') - ->setDisabled(); -``` - -Деактивираните елементи браузърът изобщо не изпраща на сървъра, т.е. няма да ги намерите и в данните, върнати от функцията `$form->getValues()`. Ако обаче зададете `setOmitted(false)`, Nette ще включи в тези данни тяхната стойност по подразбиране. - -При извикване на `setDisabled()` от съображения за сигурност **се изтрива стойността на елемента**. Ако задавате стойност по подразбиране, е необходимо да го направите след неговото деактивиране: - -```php -$form->addText('username', 'Потребителско име:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Алтернатива на деактивираните елементи са елементите с HTML атрибут `readonly`, които браузърът изпраща на сървъра. Въпреки че елементът е само за четене, е **важно да се осъзнае**, че неговата стойност все още може да бъде модифицирана или подправена от атакуващ. - - -Персонализирани елементи -======================== - -Освен широката гама от вградени елементи на формуляр, можете да добавяте към формуляра собствени елементи по следния начин: - -```php -$form->addComponent(new DateInput('Дата:'), 'date'); -// алтернативен синтаксис: $form['date'] = new DateInput('Дата:'); -``` - -.[note] -Формулярът е наследник на класа [Container |component-model:#Container], а отделните елементи са наследници на [Component |component-model:#Component]. - -Съществува начин да се дефинират нови методи на формуляра, служещи за добавяне на собствени елементи (напр. `$form->addZip()`). Това са т.нар. extension methods. Недостатъкът е, че за тях няма да работи подсказването в редакторите. - -```php -use Nette\Forms\Container; - -// добавяме метод addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Поне 5 цифри', '[0-9]{5}'); -}); - -// използване -$form->addZip('zip', 'Пощенски код:'); -``` - - -Елементи на ниско ниво -====================== - -Могат да се използват и елементи, които записваме само в шаблона и не ги добавяме към формуляра с някой от методите `$form->addXyz()`. Когато например изписваме записи от база данни и предварително не знаем колко ще бъдат и какви ще бъдат техните ID, и искаме при всеки ред да покажем чекбокс или радио бутон, е достатъчно да го кодираме в шаблона: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -А след изпращане стойността установяваме: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -където първият параметър е типът на елемента (`DataFile` за `type=file`, `DataLine` за едноредови входове като `text`, `password`, `email` и др. и `DataText` за всички останали), а вторият параметър `sel[]` съответства на HTML атрибута name. Типът на елемента можем да комбинираме със стойността `DataKeys`, която запазва ключовете на елементите. Това е полезно особено за `select`, `radioList` и `checkboxList`. - -Същественото е, че `getHttpData()` връща санирана стойност, в този случай това винаги ще бъде масив от валидни UTF-8 низове, независимо какво би се опитал да подхвърли атакуващ на сървъра. Това е аналог на директната работа с `$_POST` или `$_GET`, но със съществената разлика, че винаги връща чисти данни, така както сте свикнали при стандартните елементи на Nette формулярите. diff --git a/forms/bg/in-presenter.texy b/forms/bg/in-presenter.texy deleted file mode 100644 index 1778ed84f5..0000000000 --- a/forms/bg/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Форми в презентерите -******************** - -.[perex] -Nette Forms значително улесняват създаването и обработката на уеб форми. В тази глава ще се запознаете с използването на форми в презентерите. - -Ако се интересувате как да ги използвате напълно самостоятелно без останалата част от framework-а, ръководството за [самостоятелна употреба |standalone] е за вас. - - -Първа форма -=========== - -Нека опитаме да напишем проста форма за регистрация. Кодът ѝ ще бъде следният: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Име:'); -$form->addPassword('password', 'Парола:'); -$form->addSubmit('send', 'Регистрирай се'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -и ще се покаже в браузъра по следния начин: - -[* form-cs.webp *] - -Формата в презентера е обект от класа `Nette\Application\UI\Form`, неговият предшественик `Nette\Forms\Form` е предназначен за самостоятелна употреба. Добавихме към нея така наречените елементи име, парола и бутон за изпращане. И накрая, редът с `$form->onSuccess` казва, че след изпращане и успешна валидация трябва да се извика методът `$this->formSucceeded()`. - -От гледна точка на презентера, формата е обикновен компонент. Затова се третира като компонент и се включва в презентера чрез [фабричен метод |application:components#Фабрични методи]. Ще изглежда така: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Име:'); - $form->addPassword('password', 'Парола:'); - $form->addSubmit('send', 'Регистрирай се'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // тук обработваме данните, изпратени от формата - // $data->name съдържа името - // $data->password съдържа паролата - $this->flashMessage('Бяхте успешно регистриран.'); - $this->redirect('Home:'); - } -} -``` - -И в шаблона рендираме формата с тага `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Регистрация</h1> - -{control registrationForm} -``` - -И това всъщност е всичко :-) Имаме функционална и перфектно [защитена |#Защита от уязвимости] форма. - -И сега вероятно си мислите, че това беше твърде прибързано, чудите се как е възможно да се извика методът `formSucceeded()` и какви са параметрите, които получава. Разбира се, прави сте, това заслужава обяснение. - -Nette всъщност идва със свеж механизъм, който наричаме [Холивудски стил |application:components#Hollywood style]. Вместо вие като разработчик постоянно да питате дали нещо се е случило („формата изпратена ли е?“, „изпратена ли е валидно?“, „фалшифицирана ли е?“), казвате на framework-а „когато формата е валидно попълнена, извикай този метод“ и оставяте останалата работа на него. Ако програмирате на JavaScript, този стил на програмиране ви е добре познат. Пишете функции, които се извикват, когато настъпи определено [събитие |nette:glossary#Събития events]. И езикът им предава съответните аргументи. - -Точно така е изграден и горният код на презентера. Масивът `$form->onSuccess` представлява списък от PHP callback-ове, които Nette извиква в момента, когато формата е изпратена и правилно попълнена (т.е. е валидна). В рамките на [жизнения цикъл на презентера |application:presenters#Жизнен цикъл на презентера] това е така нареченият сигнал, така че те се извикват след метода `action*` и преди метода `render*`. И на всеки callback предава като първи параметър самата форма, а като втори - изпратените данни под формата на обект [ArrayHash |utils:arrays#ArrayHash]. Можете да пропуснете първия параметър, ако не се нуждаете от обекта на формата. А вторият параметър може да бъде по-хитър, но за това [по-късно |#Мапване към класове]. - -Обектът `$data` съдържа ключовете `name` и `password` с данните, които потребителят е попълнил. Обикновено данните се изпращат директно за по-нататъшна обработка, което може да бъде например вмъкване в база данни. По време на обработката обаче може да възникне грешка, например потребителското име вече е заето. В такъв случай предаваме грешката обратно към формата чрез `addError()` и я оставяме да се рендира отново, заедно със съобщението за грешка. - -```php -$form->addError('Извиняваме се, потребителското име вече се използва.'); -``` - -Освен `onSuccess` съществува и `onSubmit`: callback-овете се извикват винаги след изпращане на формата, дори ако тя не е попълнена правилно. И също `onError`: callback-овете се извикват само ако изпращането не е валидно. Те се извикват дори ако във `onSuccess` или `onSubmit` направим формата невалидна чрез `addError()`. - -След обработка на формата пренасочваме към следващата страница. Това предотвратява нежелано повторно изпращане на формата чрез бутона *обнови*, *назад* или чрез движение в историята на браузъра. - -Опитайте да добавите и други [елементи на формата |controls]. - - -Достъп до елементите -==================== - -Формата е компонент на презентера, в нашия случай наречена `registrationForm` (според името на фабричния метод `createComponentRegistrationForm`), така че навсякъде в презентера можете да получите достъп до формата чрез: - -```php -$form = $this->getComponent('registrationForm'); -// алтернативен синтаксис: $form = $this['registrationForm']; -``` - -Отделните елементи на формата също са компоненти, така че можете да получите достъп до тях по същия начин: - -```php -$input = $form->getComponent('name'); // или $input = $form['name']; -$button = $form->getComponent('send'); // или $button = $form['send']; -``` - -Елементите се премахват с помощта на unset: - -```php -unset($form['name']); -``` - - -Правила за валидация -==================== - -Споменахме думата *валидна*, но формата все още няма правила за валидация. Нека поправим това. - -Името ще бъде задължително, затова го маркираме с метода `setRequired()`, чийто аргумент е текстът на съобщението за грешка, което ще се покаже, ако потребителят не попълни името. Ако не посочим аргумент, ще се използва съобщението за грешка по подразбиране. - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, въведете име'); -``` - -Опитайте да изпратите формата без попълнено име и ще видите, че ще се покаже съобщение за грешка и браузърът или сървърът ще я отхвърлят, докато не попълните полето. - -В същото време няма да измамите системата, като напишете само интервали в полето. Няма начин. Nette автоматично премахва водещите и крайните интервали. Опитайте. Това е нещо, което винаги трябва да правите с всеки едноредов вход, но често се забравя. Nette го прави автоматично. (Можете да опитате да измамите формата и да изпратите многоредов низ като име. Дори тук Nette няма да се обърка и ще преобразува новите редове в интервали.) - -Формата винаги се валидира от страна на сървъра, но също така се генерира JavaScript валидация, която се извършва мигновено и потребителят научава за грешката веднага, без да е необходимо да изпраща формата до сървъра. За това отговаря скриптът `netteForms.js`. Вмъкнете го в шаблона на лейаута: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ако погледнете изходния код на страницата с формата, може да забележите, че Nette вмъква задължителните елементи в елементи с CSS клас `required`. Опитайте да добавите следния стил в шаблона и етикетът „Име“ ще стане червен. Така елегантно ще маркираме задължителните елементи за потребителите: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Добавяме допълнителни правила за валидация с метода `addRule()`. Първият параметър е правилото, вторият отново е текстът на съобщението за грешка, а може да последва и аргумент на правилото за валидация. Какво означава това? - -Ще разширим формата с ново незадължително поле „възраст“, което трябва да бъде цяло число (`addInteger()`) и освен това в допустим диапазон (`$form::Range`). И тук ще използваме третия параметър на метода `addRule()`, с който ще предадем на валидатора необходимия диапазон като двойка `[от, до]`: - -```php -$form->addInteger('age', 'Възраст:') - ->addRule($form::Range, 'Възрастта трябва да е между 18 и 120', [18, 120]); -``` - -.[tip] -Ако потребителят не попълни полето, правилата за валидация няма да бъдат проверени, тъй като елементът е незадължителен. - -Тук възниква възможност за малък рефакторинг. В съобщението за грешка и в третия параметър числата са посочени дублирано, което не е идеално. Ако създавахме [многоезични форми |rendering#Превод] и съобщението, съдържащо числа, беше преведено на няколко езика, евентуалната промяна на стойностите би била затруднена. Поради тази причина е възможно да се използват плейсхолдъри `%d` и Nette ще допълни стойностите: - -```php - ->addRule($form::Range, 'Възрастта трябва да бъде от %d до %d години', [18, 120]); -``` - -Да се върнем към елемента `password`, който също ще направим задължителен и ще проверим минималната дължина на паролата (`$form::MinLength`), отново с използване на плейсхолдър: - -```php -$form->addPassword('password', 'Парола:') - ->setRequired('Изберете парола') - ->addRule($form::MinLength, 'Паролата трябва да съдържа поне %d знака', 8); -``` - -Ще добавим към формата и поле `passwordVerify`, където потребителят ще въведе паролата още веднъж, за проверка. С помощта на правилата за валидация ще проверим дали двете пароли са еднакви (`$form::Equal`). И като параметър ще дадем препратка към първата парола с помощта на [квадратни скоби |#Достъп до елементите]: - -```php -$form->addPassword('passwordVerify', 'Парола за проверка:') - ->setRequired('Моля, въведете паролата отново за проверка') - ->addRule($form::Equal, 'Паролите не съвпадат', $form['password']) - ->setOmitted(); -``` - -С помощта на `setOmitted()` маркирахме елемент, чиято стойност всъщност не ни интересува и който съществува само с цел валидация. Стойността не се предава на `$data`. - -С това имаме напълно функционална форма с валидация както в PHP, така и в JavaScript. Възможностите за валидация на Nette са много по-широки, могат да се създават условия, според тях да се показват и скриват части от страницата и т.н. Всичко ще научите в главата за [валидация на форми |validation]. - - -Стойности по подразбиране -========================= - -Обикновено задаваме стойности по подразбиране на елементите на формата: - -```php -$form->addEmail('email', 'Имейл') - ->setDefaultValue($lastUsedEmail); -``` - -Често е полезно да се зададат стойности по подразбиране на всички елементи едновременно. Например, когато формата се използва за редактиране на записи. Прочитаме записа от базата данни и задаваме стойностите по подразбиране: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Извиквайте `setDefaults()` след дефинирането на елементите. - - -Рендиране на формата -==================== - -По подразбиране формата се рендира като таблица. Отделните елементи отговарят на основното правило за достъпност - всички етикети са написани като `<label>` и са свързани със съответния елемент на формата. При кликване върху етикета курсорът автоматично се появява в полето на формата. - -На всеки елемент можем да задаваме произволни HTML атрибути. Например, да добавим placeholder: - -```php -$form->addInteger('age', 'Възраст:') - ->setHtmlAttribute('placeholder', 'Моля, попълнете възрастта'); -``` - -Има наистина много начини за рендиране на форма, така че на това е посветена [отделна глава за рендиране |rendering]. - - -Мапване към класове -=================== - -Да се върнем към метода `formSucceeded()`, който във втория параметър `$data` получава изпратените данни като обект `ArrayHash`. Тъй като това е генеричен клас, нещо като `stdClass`, при работа с него ще ни липсва известно удобство, като например подсказване на свойствата в редакторите или статичен анализ на кода. Това може да се реши, като за всяка форма имаме конкретен клас, чиито свойства представляват отделните елементи. Напр.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Алтернативно можете да използвате конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Свойствата на класа с данни могат да бъдат и enum-и и те ще бъдат автоматично мапнати. .{data-version:3.2.4} - -Как да кажем на Nette да ни връща данните като обекти от този клас? По-лесно, отколкото си мислите. Достатъчно е само да посочите класа като тип на параметъра `$data` в обработващия метод: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data е инстанция на RegistrationFormData - $name = $data->name; - // ... -} -``` - -Като тип може да се посочи и `array` и тогава данните ще бъдат предадени като масив. - -По подобен начин може да се използва и функцията `getValues()`, на която предаваме името на класа или обекта за хидратиране като параметър: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Ако формите образуват многостепенна структура, съставена от контейнери, създайте отделен клас за всеки: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапването след това разпознава от типа на свойството `$person`, че трябва да мапне контейнера към класа `PersonFormData`. Ако свойството съдържа масив от контейнери, посочете типа `array` и предайте класа за мапване директно на контейнера: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Можете да генерирате дизайна на класа с данни за формата с помощта на метода `Nette\Forms\Blueprint::dataClass($form)`, който ще го изведе на страницата на браузъра. След това е достатъчно да маркирате кода с кликване и да го копирате в проекта. .{data-version:3.1.15} - - -Множество бутони -================ - -Ако формата има повече от един бутон, обикновено трябва да разграничим кой от тях е бил натиснат. Можем да създадем собствена обработваща функция за всеки бутон. Ще я зададем като хендлър за [събитието |nette:glossary#Събития events] `onClick`: - -```php -$form->addSubmit('save', 'Запази') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Изтрий') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Тези хендлъри се извикват само в случай на валидно попълнена форма, точно както при събитието `onSuccess`. Разликата е, че като първи параметър вместо формата може да се предаде изпращащият бутон, в зависимост от типа, който посочите: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Когато формата се изпрати с бутона <kbd>Enter</kbd>, се счита, че е изпратена с първия бутон. - - -Събитие onAnchor -================ - -Когато изграждаме формата във фабричния метод (като например `createComponentRegistrationForm`), тя все още не знае дали е била изпратена, нито с какви данни. Но има случаи, когато трябва да знаем изпратените стойности, например по-нататъшният вид на формата зависи от тях, или ни трябват за зависими selectbox-ове и т.н. - -Затова можете да оставите частта от кода, която изгражда формата, да бъде извикана едва в момента, когато тя е т. нар. „закотвена“, т.е. вече е свързана с презентера и знае своите изпратени данни. Предаваме такъв код в масива `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Държава:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Град:'); - -$form->onAnchor[] = function () use ($country, $city) { - // тази функция ще се извика, когато формата знае дали е била изпратена и с какви данни - // следователно може да се използва методът getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Защита от уязвимости -==================== - -Nette Framework поставя голям акцент върху сигурността и затова стриктно се грижи за добрата защита на формите. Прави го напълно прозрачно и не изисква ръчна настройка. - -Освен че защитава формите от атаки [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], той извършва много малки защити, за които вече не е нужно да мислите. - -Например, филтрира всички контролни знаци от входовете и проверява валидността на UTF-8 кодирането, така че данните от формата винаги ще бъдат чисти. При select box-ове и radio list-ове проверява дали избраните елементи са наистина от предлаганите и не е имало фалшификация. Вече споменахме, че при едноредови текстови входове премахва знаците за край на ред, които нападателят може да е изпратил. При многоредови входове пък нормализира знаците за край на ред. И така нататък. - -Nette решава вместо вас рисковете за сигурността, за които много програмисти дори не подозират, че съществуват. - -Споменатата CSRF атака се състои в това, че нападателят примамва жертвата към страница, която незабелязано в браузъра на жертвата изпълнява заявка към сървъра, на който жертвата е влязла, и сървърът смята, че заявката е изпълнена от жертвата по нейна воля. Затова Nette предотвратява изпращането на POST форма от друг домейн. Ако по някаква причина искате да изключите защитата и да позволите изпращането на формата от друг домейн, използвайте: - -```php -$form->allowCrossOrigin(); // ВНИМАНИЕ! Изключва защитата! -``` - -Тази защита използва SameSite cookie, наречена `_nss`. Защитата чрез SameSite cookie може да не е 100% надеждна, затова е препоръчително да включите и защита чрез токен: - -```php -$form->addProtection(); -``` - -Препоръчваме да защитавате по този начин формите в административната част на сайта, които променят чувствителни данни в приложението. Framework-ът се защитава срещу CSRF атака чрез генериране и проверка на оторизационен токен, който се съхранява в сесията. Затова е необходимо да имате отворена сесия преди показването на формата. В административната част на сайта обикновено сесията вече е стартирана поради влизането на потребителя. В противен случай стартирайте сесията с метода `Nette\Http\Session::start()`. - - -Същата форма в множество презентери -=================================== - -Ако трябва да използвате една и съща форма в множество презентери, препоръчваме да създадете фабрика за нея, която след това да предадете на презентера. Подходящо място за такъв клас е например директорията `app/Forms`. - -Фабричният клас може да изглежда например така: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Име:'); - $form->addSubmit('send', 'Вход'); - return $form; - } -} -``` - -Искаме от класа да произведе формата във фабричния метод за компоненти в презентера: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // можем да променим формата, тук например променяме етикета на бутона - $form['send']->setCaption('Продължи'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // и добавяме хендлър - return $form; -} -``` - -Хендлърът за обработка на формата може да бъде предоставен и от фабриката: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Име:'); - $form->addSubmit('send', 'Вход'); - $form->onSuccess[] = function (Form $form, $data): void { - // тук извършваме обработката на формата - }; - return $form; - } -} -``` - -И така, направихме бързо въведение във формите в Nette. Опитайте да разгледате и директорията [examples |https://github.com/nette/forms/tree/master/examples] в дистрибуцията, където ще намерите още вдъхновение. diff --git a/forms/bg/rendering.texy b/forms/bg/rendering.texy deleted file mode 100644 index 953d4618f1..0000000000 --- a/forms/bg/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Рендиране на форми -****************** - -Външният вид на формите може да бъде много разнообразен. На практика можем да срещнем две крайности. От една страна, стои нуждата в приложението да се рендират редица форми, които визуално си приличат като две капки вода, и ще оценим лесното рендиране без шаблон с помощта на `$form->render()`. Обикновено това е случаят с административните интерфейси. - -От друга страна, има разнообразни форми, за които важи: всяка е оригинал. Техният вид най-добре се описва с HTML език в шаблона на формата. И разбира се, освен двете споменати крайности, ще срещнем много форми, които се намират някъде по средата. - - -Рендиране с помощта на Latte -============================ - -[Шаблонната система Latte|latte:] значително улеснява рендирането на форми и техните елементи. Първо ще покажем как да рендираме формите ръчно, елемент по елемент, и така да получим пълен контрол над кода. По-късно ще покажем как такова рендиране може да бъде [автоматизирано |#Автоматично рендиране]. - -Можете да генерирате дизайна на Latte шаблона за формата с помощта на метода `Nette\Forms\Blueprint::latte($form)`, който го извежда на страницата на браузъра. След това просто маркирайте кода с кликване и го копирайте в проекта си. .{data-version:3.1.15} - - -`{control}` ------------ - -Най-лесният начин да рендирате форма е да напишете в шаблона: - -```latte -{control signInForm} -``` - -Можете да повлияете на външния вид на така рендираната форма чрез конфигуриране на [#Renderer] и [отделните елементи |#HTML атрибути]. - - -`n:name` --------- - -Дефиницията на формата в PHP кода може да бъде изключително лесно свързана с HTML кода. Достатъчно е само да добавите атрибутите `n:name`. Толкова е лесно! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Потребителско име: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Парола: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Имате пълен контрол над вида на получения HTML код. Ако използвате атрибута `n:name` при елементите `<select>`, `<button>` или `<textarea>`, тяхното вътрешно съдържание ще се попълни автоматично. Тагът `<form n:name>` освен това създава локална променлива `$form` с обекта на рендираната форма, а затварящият `</form>` рендира всички нерендирани скрити елементи (същото важи и за `{form} ... {/form}`). - -Не трябва обаче да забравяме да рендираме възможните съобщения за грешки. Както тези, които са добавени към отделните елементи с метода `addError()` (с помощта на `{inputError}`), така и тези, добавени директно към формата (връщат се от `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Потребителско име: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Парола: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -По-сложни елементи на формата, като RadioList или CheckboxList, могат да се рендират по този начин, поотделно: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Не искате да мислите за всеки елемент какъв HTML елемент да използвате за него в шаблона, дали `<input>`, `<textarea>` и т.н.? Решението е универсалният таг `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Потребителско име: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Парола: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Ако формата използва преводач, текстът вътре в таговете `{label}` ще бъде преведен. - -И в този случай по-сложни елементи на формата, като RadioList или CheckboxList, могат да се рендират поотделно: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -За да рендирате само `<input>` в елемента Checkbox, използвайте `{input myCheckbox:}`. В този случай винаги разделяйте HTML атрибутите със запетая `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Извежда съобщение за грешка за елемента на формата, ако има такова. Обикновено обвиваме съобщението в HTML елемент за стилизиране. Можете елегантно да предотвратите рендирането на празен елемент, ако няма съобщение, с помощта на `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Можем да проверим наличието на грешка с метода `hasErrors()` и съответно да зададем клас на родителския елемент: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Таговете `{form signInForm}...{/form}` са алтернатива на `<form n:name="signInForm">...</form>`. - - -Автоматично рендиране ---------------------- - -Благодарение на таговете `{input}` и `{label}` можем лесно да създадем общ шаблон за всяка форма. Той ще итерира последователно и ще рендира всички нейни елементи, с изключение на скритите елементи, които ще се рендират автоматично при затваряне на формата с тага `</form>`. Името на рендираната форма ще се очаква в променливата `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Използваните самозатварящи се двойни тагове `{label .../}` показват етикети, идващи от дефиницията на формата в PHP кода. - -Запазете този общ шаблон например във файл `basic-form.latte` и за да рендирате формата, е достатъчно да го включите и да предадете името (или инстанцията) на формата в параметъра `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Ако при рендирането на една конкретна форма искате да се намесите във вида й и например да рендирате един елемент по различен начин, тогава най-лесният начин е да подготвите предварително блокове в шаблона, които след това ще могат да бъдат презаписани. Блоковете могат да имат и [динамични имена |latte:template-inheritance#Динамични имена на блокове], така че в тях може да се вмъкне и името на рендирания елемент. Например: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -За елемент, напр. `username`, така се създава блок `input-username`, който може лесно да бъде презаписан с помощта на тага [{embed} |latte:template-inheritance#Единично наследяване]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Алтернативно, цялото съдържание на шаблона `basic-form.latte` може да бъде [дефинирано |latte:template-inheritance#Дефиниции] като блок, включително параметъра `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Благодарение на това извикването му ще бъде малко по-лесно: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -При това е достатъчно блокът да се импортира само на едно място, и то в началото на шаблона на лейаута: - -```latte -{import basic-form.latte} -``` - - -Специални случаи ----------------- - -Ако трябва да рендирате само вътрешната част на формата без HTML таговете `<form>`, например при изпращане на снипети, скрийте ги с помощта на атрибута `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Потребителско име: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -С рендирането на елементи вътре във формулярния контейнер ще помогне тагът `{formContainer}`. - -```latte -<p>Кои новини желаете да получавате:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Рендиране без Latte -=================== - -Най-лесният начин да рендирате форма е да извикате: - -```php -$form->render(); -``` - -Можете да повлияете на външния вид на така рендираната форма чрез конфигуриране на [#Renderer] и [отделните елементи |#HTML атрибути]. - - -Ръчно рендиране ---------------- - -Всеки елемент на формата разполага с методи, които генерират HTML код за полето на формата и етикетите. Те могат да го връщат или като низ, или като обект [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` връща HTML кода на елемента -- `getLabel($caption = null): Html|string|null` връща HTML кода на етикета, ако съществува - -Така формата може да се рендира елемент по елемент: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Докато при някои елементи `getControl()` връща единствен HTML елемент (напр. `<input>`, `<select>` и т.н.), при други връща цял фрагмент HTML код (CheckboxList, RadioList). В такъв случай можете да използвате методи, които генерират отделни input-и и етикети, за всеки елемент поотделно: - -- `getControlPart($key = null): ?Html` връща HTML кода на един елемент -- `getLabelPart($key = null): ?Html` връща HTML кода на етикета на един елемент - -.[note] -Тези методи имат префикс `get` по исторически причини, но `generate` би бил по-добър, тъй като при всяко извикване създава и връща нов елемент `Html`. - - -Renderer -======== - -Това е обект, осигуряващ рендирането на формата. Той може да бъде зададен с метода `$form->setRenderer`. Контролът му се предава при извикване на метода `$form->render()`. - -Ако не зададем собствен renderer, ще бъде използван renderer-ът по подразбиране [api:Nette\Forms\Rendering\DefaultFormRenderer]. Той рендира елементите на формата под формата на HTML таблица. Изходът изглежда така: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Име:</label></th> - - <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> -</tr> - -<tr class="required"> - <th><label class="required" for="frm-age">Възраст:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Пол:</label></th> - ... -``` - -Дали да се използва или не таблица за скелета на формата е спорно и много уеб дизайнери предпочитат друг markup. Например дефиниционен списък. Затова ще преконфигурираме `DefaultFormRenderer` така, че да рендира формата под формата на списък. Конфигурацията се извършва чрез редактиране на масива [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Първият индекс винаги представлява областта, а вторият - нейния атрибут. Отделните области са показани на изображението: - -[* defaultformrenderer.webp *] - -Стандартно групата елементи `controls` е обвита в таблица `<table>`, всеки `pair` представлява ред на таблицата `<tr>`, а двойката `label` и `control` са клетки `<th>` и `<td>`. Сега ще променим обвиващите елементи. Ще вмъкнем областта `controls` в контейнер `<dl>`, ще оставим областта `pair` без контейнер, ще вмъкнем `label` в `<dt>` и накрая ще обвием `control` с тагове `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Резултатът е следният HTML код: - -```latte -<dl> - <dt><label class="required" for="frm-name">Име:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Възраст:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Пол:</label></dt> - ... -</dl> -``` - -В масива wrappers може да се повлияе на редица други атрибути: - -- добавяне на CSS класове към отделните типове елементи на формата -- разграничаване на четни и нечетни редове с CSS клас -- визуално разграничаване на задължителни и незадължителни елементи -- определяне дали съобщенията за грешки да се показват директно при елементите или над формата - - -Options -------- - -Поведението на Renderer-а може да се контролира и чрез задаване на *options* на отделните елементи на формата. По този начин може да се зададе описание, което ще се изведе до входното поле: - -```php -$form->addText('phone', 'Номер:') - ->setOption('description', 'Този номер ще остане скрит'); -``` - -Ако искаме да поставим HTML съдържание в него, ще използваме класа [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Номер:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Условия за съхранение на Вашия номер</a>') - ); -``` - -.[tip] -Html елементът може да се използва и вместо етикет: `$form->addCheckbox('conditions', $label)`. - - -Групиране на елементи ---------------------- - -Renderer-ът позволява групиране на елементи във визуални групи (fieldset-и): - -```php -$form->addGroup('Лични данни'); -``` - -След създаване на нова група, тя става активна и всеки новодобавен елемент се добавя и към нея. Така че формата може да се изгражда по този начин: - -```php -$form = new Form; -$form->addGroup('Лични данни'); -$form->addText('name', 'Вашето име:'); -$form->addInteger('age', 'Вашата възраст:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Адрес за доставка'); -$form->addCheckbox('send', 'Изпрати на адрес'); -$form->addText('street', 'Улица:'); -$form->addText('city', 'Град:'); -$form->addSelect('country', 'Държава:', $countries); -``` - -Renderer-ът първо рендира групите и едва след това елементите, които не принадлежат към никоя група. - - -Поддръжка за Bootstrap ----------------------- - -[В примерите |https://github.com/nette/forms/tree/master/examples] ще намерите примери как да конфигурирате Renderer за [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] и [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -HTML атрибути -============= - -За задаване на произволни HTML атрибути на елементите на формата използваме метода `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Номер:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Сортиране по:', ['цена', 'име']) - ->setHtmlAttribute('onchange', 'submit()'); // изпрати при промяна - - -// За задаване на атрибути на самия <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Спецификация на типа на елемента: - -```php -$form->addText('tel', 'Вашият телефон:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'въведете телефон'); -``` - -.[warning] -Задаването на типа и други атрибути служи само за визуални цели. Проверката на коректността на входовете трябва да се извършва на сървъра, което се осигурява чрез избор на подходящ [елемент на формата|controls] и посочване на [правила за валидация|validation]. - -На отделните елементи в radio или checkbox списъци можем да зададем HTML атрибут с различни стойности за всеки от тях. Обърнете внимание на двоеточието след `style:`, което осигурява избор на стойност според ключа: - -```php -$colors = ['r' => 'червен', 'g' => 'зелен', 'b' => 'син']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Цветове:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Извежда: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">червен</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">зелен</label> -<label><input type="checkbox" name="colors[]" value="b">син</label> -``` - -За задаване на логически атрибути, като `readonly`, можем да използваме запис с въпросителен знак: - -```php -$form->addCheckboxList('colors', 'Цветове:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // за повече ключове използвайте масив, напр. ['r', 'g'] -``` - -Извежда: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">червен</label> -<label><input type="checkbox" name="colors[]" value="g">зелен</label> -<label><input type="checkbox" name="colors[]" value="b">син</label> -``` - -В случай на selectbox-ове методът `setHtmlAttribute()` задава атрибути на елемента `<select>`. Ако искаме да зададем атрибути на отделните `<option>`, използваме метода `setOptionAttribute()`. Записите с двоеточие и въпросителен знак, посочени по-горе, също работят: - -```php -$form->addSelect('colors', 'Цветове:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Извежда: - -```latte -<select name="colors"> - <option value="r" style="background:red">червен</option> - <option value="g" style="background:green">зелен</option> - <option value="b">син</option> -</select> -``` - - -Прототипи ---------- - -Алтернативен начин за задаване на HTML атрибути е чрез модифициране на шаблона, от който се генерира HTML елементът. Шаблонът е обект `Html` и се връща от метода `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Номер:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -По този начин може да се модифицира и шаблонът на етикета, който се връща от `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -При елементите Checkbox, CheckboxList и RadioList можете да повлияете на шаблона на елемента, който обвива целия елемент. Той се връща от `getContainerPrototype()`. В състояние по подразбиране това е „празен“ елемент, така че нищо не се рендира, но като му зададем име, той ще се рендира: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -В случай на CheckboxList и RadioList може да се повлияе и на шаблона на разделителя на отделните елементи, който се връща от метода `getSeparatorPrototype()`. В състояние по подразбиране това е елементът `<br>`. Ако го промените на двоен елемент, той ще обвива отделните елементи, вместо да ги разделя. Освен това може да се повлияе на шаблона на HTML елемента на етикета при отделните елементи, който се връща от `getItemLabelPrototype()`. - - -Превод -====== - -Ако програмирате многоезично приложение, вероятно ще трябва да рендирате формата в различни езикови версии. За тази цел Nette Framework дефинира интерфейс за превод [api:Nette\Localization\Translator]. В Nette няма имплементация по подразбиране, можете да избирате според нуждите си от няколко готови решения, които ще намерите на [Componette |https://componette.org/search/localization]. В тяхната документация ще научите как да конфигурирате преводача. - -Формите поддържат извеждане на текстове чрез преводач. Предаваме им го с помощта на метода `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -От този момент нататък не само всички етикети, но и всички съобщения за грешки или елементи на select box-ове ще бъдат преведени на друг език. - -При отделните елементи на формата е възможно да се зададе друг преводач или преводът да се изключи напълно със стойност `null`: - -```php -$form->addSelect('carModel', 'Модел:', $cars) - ->setTranslator(null); -``` - -При [правилата за валидация|validation] на преводача се предават и специфични параметри, например при правилото: - -```php -$form->addPassword('password', 'Парола:') - ->addRule($form::MinLength, 'Паролата трябва да съдържа поне %d знака', 8); -``` - -се извиква преводачът с тези параметри: - -```php -$translator->translate('Паролата трябва да съдържа поне %d знака', 8); -``` - -и следователно може да избере правилната форма за множествено число на думата `знака` според броя. - - -Събитие onRender -================ - -Точно преди формата да се рендира, можем да извикаме наш код. Той може например да добави HTML класове към елементите на формата за правилно показване. Добавяме кода към масива `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/bg/standalone.texy b/forms/bg/standalone.texy deleted file mode 100644 index 6a4e89a8a9..0000000000 --- a/forms/bg/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Форми, използвани самостоятелно -******************************* - -.[perex] -Nette Forms значително улесняват създаването и обработката на уеб форми. Можете да ги използвате във вашите приложения напълно самостоятелно, без останалата част от framework-а, което ще покажем в тази глава. - -Но ако използвате Nette Application и презентери, за вас е предназначено ръководството за [използване в презентери|in-presenter]. - - -Първа форма -=========== - -Нека опитаме да напишем проста форма за регистрация. Кодът й ще бъде следният ("целия код":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Име:'); -$form->addPassword('password', 'Парола:'); -$form->addSubmit('send', 'Регистриране'); -``` - -Много лесно можем да я рендираме: - -```php -$form->render(); -``` - -и в браузъра ще се покаже така: - -[* form-cs.webp *] - -Формата е обект от класа `Nette\Forms\Form` (класът `Nette\Application\UI\Form` се използва в презентери). Добавихме към нея т.нар. елементи име, парола и бутон за изпращане. - -А сега да оживим формата. С проверка на `$form->isSuccess()` ще разберем дали формата е била изпратена и дали е била попълнена валидно. Ако да, ще изведем данните. Следователно, след дефиницията на формата добавяме: - -```php -if ($form->isSuccess()) { - echo 'Формата беше правилно попълнена и изпратена'; - $data = $form->getValues(); - // $data->name съдържа името - // $data->password съдържа паролата - var_dump($data); -} -``` - -Методът `getValues()` връща изпратените данни под формата на обект [ArrayHash |utils:arrays#ArrayHash]. Как да променим това, ще покажем [по-късно |#Мапиране към класове]. Обектът `$data` съдържа ключове `name` и `password` с данните, които е попълнил потребителят. - -Обикновено данните веднага се изпращат за по-нататъшна обработка, което може да бъде например вмъкване в база данни. По време на обработката обаче може да възникне грешка, например потребителското име вече е заето. В такъв случай предаваме грешката обратно към формата с помощта на `addError()` и я оставяме да се рендира отново, заедно със съобщението за грешка. - -```php -$form->addError('Извиняваме се, това потребителско име вече се използва.'); -``` - -След обработката на формата пренасочваме към следващата страница. Това предотвратява нежеланото повторно изпращане на формата с бутона *обнови*, *назад* или чрез движение в историята на браузъра. - -Формата стандартно се изпраща с метод POST и то към същата страница. И двете могат да се променят: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -И това всъщност е всичко :-) Имаме функционална и перфектно [защитена |#Защита от уязвимости] форма. - -Опитайте да добавите и други [елементи на формата|controls]. - - -Достъп до елементи -================== - -Формата и нейните отделни елементи наричаме компоненти. Те образуват дърво от компоненти, където коренът е именно формата. До отделните елементи на формата можем да достигнем по следния начин: - -```php -$input = $form->getComponent('name'); -// алтернативен синтаксис: $input = $form['name']; - -$button = $form->getComponent('send'); -// алтернативен синтаксис: $button = $form['send']; -``` - -Елементите се премахват с помощта на unset: - -```php -unset($form['name']); -``` - - -Правила за валидация -==================== - -Споменахме думата *валидна*, но формата засега няма никакви правила за валидация. Нека поправим това. - -Името ще бъде задължително, затова го маркираме с метода `setRequired()`, чийто аргумент е текстът на съобщението за грешка, което ще се покаже, ако потребителят не попълни името. Ако не посочим аргумент, ще се използва съобщението за грешка по подразбиране. - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, въведете име'); -``` - -Опитайте да изпратите формата без попълнено име и ще видите, че ще се покаже съобщение за грешка и браузърът или сървърът ще я отхвърлят, докато не попълните полето. - -Същевременно системата няма да ви измами, като напишете в полето например само интервали. Не. Nette автоматично премахва левите и десните интервали. Опитайте. Това е нещо, което винаги трябва да правите с всеки едноредов input, но често се забравя. Nette го прави автоматично. (Можете да опитате да измамите формата и да изпратите многоредов низ като име. И тук Nette няма да се обърка и ще промени новите редове на интервали.) - -Формата винаги се валидира от страна на сървъра, но също така се генерира JavaScript валидация, която протича мигновено и потребителят научава за грешката веднага, без да е необходимо да изпраща формата на сървъра. За това се грижи скриптът `netteForms.js`. Вмъкнете го в страницата: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ако погледнете в изходния код на страницата с формата, можете да забележите, че Nette вмъква задължителните елементи в елементи с CSS клас `required`. Опитайте да добавите следния стил в шаблона и надписът „Име“ ще бъде червен. Така елегантно маркираме задължителните елементи за потребителите: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Други правила за валидация добавяме с метода `addRule()`. Първият параметър е правилото, вторият е отново текстът на съобщението за грешка и може да последва още аргумент на правилото за валидация. Какво се има предвид? - -Ще разширим формата с ново незадължително поле „възраст“, което трябва да бъде цяло число (`addInteger()`) и освен това в разрешен диапазон (`$form::Range`). И тук именно ще използваме третия параметър на метода `addRule()`, с който ще предадем на валидатора изисквания диапазон като двойка `[от, до]`: - -```php -$form->addInteger('age', 'Възраст:') - ->addRule($form::Range, 'Възрастта трябва да е между 18 и 120', [18, 120]); -``` - -.[tip] -Ако потребителят не попълни полето, правилата за валидация няма да се проверяват, тъй като елементът е незадължителен. - -Тук възниква възможност за дребен рефакторинг. В съобщението за грешка и в третия параметър числата са посочени дублирано, което не е идеално. Ако създавахме [многоезични форми |rendering#Превод] и съобщението, съдържащо числа, беше преведено на няколко езика, евентуалната промяна на стойностите би се затруднила. Поради тази причина е възможно да се използват заместващи знаци `%d` и Nette ще допълни стойностите: - -```php - ->addRule($form::Range, 'Възрастта трябва да е между %d и %d години', [18, 120]); -``` - -Да се върнем към елемента `password`, който също ще направим задължителен и ще проверим минималната дължина на паролата (`$form::MinLength`), отново с използване на заместващ знак: - -```php -$form->addPassword('password', 'Парола:') - ->setRequired('Изберете парола') - ->addRule($form::MinLength, 'Паролата трябва да има поне %d знака', 8); -``` - -Ще добавим към формата още поле `passwordVerify`, където потребителят ще въведе паролата още веднъж, за проверка. С помощта на правилата за валидация ще проверим дали двете пароли са еднакви (`$form::Equal`). И като параметър ще дадем препратка към първата парола с помощта на [квадратни скоби |#Достъп до елементи]: - -```php -$form->addPassword('passwordVerify', 'Парола за проверка:') - ->setRequired('Моля, въведете паролата отново за проверка') - ->addRule($form::Equal, 'Паролите не съвпадат', $form['password']) - ->setOmitted(); -``` - -С помощта на `setOmitted()` маркирахме елемент, чиято стойност всъщност не ни интересува и който съществува само поради валидация. Стойността не се предава в `$data`. - -С това имаме готова напълно функционална форма с валидация в PHP и JavaScript. Валидационните способности на Nette са далеч по-широки, могат да се създават условия, според тях да се показват и скриват части от страницата и т.н. Всичко ще научите в главата за [валидация на форми|validation]. - - -Стойности по подразбиране -========================= - -На елементите на формата обикновено задаваме стойности по подразбиране: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Често е полезно да се зададат стойности по подразбиране на всички елементи едновременно. Например, когато формата служи за редактиране на записи. Прочитаме записа от базата данни и задаваме стойностите по подразбиране: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Извиквайте `setDefaults()` след дефинирането на елементите. - - -Рендиране на формата -==================== - -Стандартно формата се рендира като таблица. Отделните елементи отговарят на основното правило за достъпност - всички надписи са записани като `<label>` и са свързани със съответния елемент на формата. При кликване върху надписа курсорът автоматично се появява в полето на формата. - -На всеки елемент можем да задаваме произволни HTML атрибути. Например да добавим placeholder: - -```php -$form->addInteger('age', 'Възраст:') - ->setHtmlAttribute('placeholder', 'Моля, попълнете възрастта'); -``` - -Начините за рендиране на форма са наистина много, затова на това е посветена [самостоятелна глава за рендиране|rendering]. - - -Мапиране към класове -==================== - -Да се върнем към обработката на данните от формата. Методът `getValues()` ни връщаше изпратените данни като обект `ArrayHash`. Тъй като това е генеричен клас, нещо като `stdClass`, при работа с него ще ни липсва определен комфорт, като например подсказване на свойствата в редакторите или статичен анализ на кода. Това би могло да се реши, като за всяка форма имаме конкретен клас, чиито свойства представляват отделните елементи. Напр.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Алтернативно можете да използвате конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Свойствата на класа с данни могат да бъдат и enum-и и те ще бъдат автоматично мапирани. .{data-version:3.2.4} - -Как да кажем на Nette да ни връща данните като обекти от този клас? По-лесно, отколкото си мислите. Достатъчно е само името на класа или обектът за хидратиране да се посочи като параметър: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Като параметър може да се посочи също `'array'` и тогава данните ще се върнат като масив. - -Ако формите образуват многостепенна структура, съставена от контейнери, създайте за всеки отделен клас: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапирането след това от типа на свойството `$person` ще разбере, че трябва да мапира контейнера към класа `PersonFormData`. Ако свойството съдържа масив от контейнери, посочете тип `array` и предайте класа за мапиране директно на контейнера: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Можете да генерирате дизайна на класа с данни на формата с помощта на метода `Nette\Forms\Blueprint::dataClass($form)`, който го извежда на страницата на браузъра. След това е достатъчно да маркирате кода с кликване и да го копирате в проекта. .{data-version:3.1.15} - - -Повече бутони -============= - -Ако формата има повече от един бутон, обикновено трябва да разграничим кой от тях е бил натиснат. Тази информация ни връща методът `isSubmittedBy()` на бутона: - -```php -$form->addSubmit('save', 'Запазване'); -$form->addSubmit('delete', 'Изтриване'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Не пропускайте проверката `$form->isSuccess()`, с нея ще проверите валидността на данните. - -Когато формата се изпрати с бутона <kbd>Enter</kbd>, се счита, че е изпратена с първия бутон. - - -Защита от уязвимости -==================== - -Nette Framework поставя голям акцент върху сигурността и затова стриктно се грижи за доброто обезопасяване на формите. - -Освен че формите защитават от атаки [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], той прави много дребни защити, за които вие вече не трябва да мислите. - -Например, филтрира от входовете всички контролни знаци и проверява валидността на UTF-8 кодирането, така че данните от формата винаги ще бъдат чисти. При select кутиите и radio списъците проверява дали избраните елементи са били действително от предлаганите и дали не е имало подправяне. Вече споменахме, че при едноредовите текстови входове премахва знаците за край на ред, които нападателят е могъл да изпрати. При многоредовите входове пък нормализира знаците за край на ред. И така нататък. - -Nette решава вместо вас рисковете за сигурността, за които много програмисти дори не подозират, че съществуват. - -Споменатата CSRF атака се състои в това, че нападателят примамва жертвата на страница, която незабележимо в браузъра на жертвата изпълнява заявка към сървъра, на който жертвата е влязла, и сървърът смята, че заявката е била изпълнена от жертвата по нейна воля. Затова Nette предотвратява изпращането на POST форма от друг домейн. Ако по някаква причина искате да изключите защитата и да позволите изпращането на формата от друг домейн, използвайте: - -```php -$form->allowCrossOrigin(); // ВНИМАНИЕ! Изключва защитата! -``` - -Тази защита използва SameSite бисквитка с име `_nss`. Затова създавайте обекта на формата преди изпращането на първия изход, за да може бисквитката да бъде изпратена. - -Защитата с помощта на SameSite бисквитка може да не е 100% надеждна, затова е препоръчително да включите и защита с помощта на токен: - -```php -$form->addProtection(); -``` - -Препоръчваме да защитавате по този начин формите в административната част на сайта, които променят чувствителни данни в приложението. Framework-ът се защитава срещу CSRF атака чрез генериране и проверка на оторизационен токен, който се съхранява в сесията. Затова е необходимо преди показването на формата да има отворена сесия. В административната част на сайта обикновено сесията вече е стартирана поради влизането на потребителя. В противен случай стартирайте сесията с метода `Nette\Http\Session::start()`. - -Така, преминахме през бързо въведение във формите в Nette. Опитайте да разгледате още директорията [examples|https://github.com/nette/forms/tree/master/examples] в дистрибуцията, където ще намерите повече вдъхновение. diff --git a/forms/bg/validation.texy b/forms/bg/validation.texy deleted file mode 100644 index 2342c274de..0000000000 --- a/forms/bg/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Валидация на форми -****************** - - -Задължителни елементи -===================== - -Задължителните елементи маркираме с метода `setRequired()`, чийто аргумент е текстът на [#Съобщения за грешки], който ще се покаже, ако потребителят не попълни елемента. Ако не посочим аргумент, ще се използва съобщението за грешка по подразбиране. - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, въведете име'); -``` - - -Правила -======= - -Правилата за валидация добавяме към елементите с метода `addRule()`. Първият параметър е правилото, вторият е текстът на [#Съобщения за грешки] и третият е аргументът на правилото за валидация. - -```php -$form->addPassword('password', 'Парола:') - ->addRule($form::MinLength, 'Паролата трябва да има поне %d знака', 8); -``` - -**Правилата за валидация се проверяват само в случай, че потребителят е попълнил елемента.** - -Nette идва с цяла редица предварително дефинирани правила, чиито имена са константи на класа `Nette\Forms\Form`. При всички елементи можем да използваме тези правила: - -| константа | описание | тип аргумент -|------- -| `Required` | задължителен елемент, псевдоним за `setRequired()` | - -| `Filled` | задължителен елемент, псевдоним за `setRequired()` | - -| `Blank` | елементът не трябва да бъде попълнен | - -| `Equal` | стойността е равна на параметъра | `mixed` -| `NotEqual` | стойността не е равна на параметъра | `mixed` -| `IsIn` | стойността е равна на някой елемент в масива | `array` -| `IsNotIn` | стойността не е равна на никой елемент в масива | `array` -| `Valid` | елементът попълнен ли е правилно? (за [#Условия]) | - - - -Текстови полета ---------------- - -При елементите `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` могат да се използват и някои от следните правила: - -| `MinLength` | минимална дължина на текста | `int` -| `MaxLength` | максимална дължина на текста | `int` -| `Length` | дължина в диапазон или точна дължина | двойка `[int, int]` или `int` -| `Email` | валиден имейл адрес | - -| `URL` | абсолютен URL | - -| `Pattern` | съответства на регулярен израз | `string` -| `PatternInsensitive` | като `Pattern`, но независимо от големината на буквите | `string` -| `Integer` | целочислена стойност | - -| `Numeric` | псевдоним за `Integer` | - -| `Float` | число | - -| `Min` | минимална стойност на числов елемент | `int\|float` -| `Max` | максимална стойност на числов елемент | `int\|float` -| `Range` | стойност в диапазон | двойка `[int\|float, int\|float]` - -Правилата за валидация `Integer`, `Numeric` и `Float` веднага преобразуват стойността в integer съответно float. Освен това правилото `URL` приема и адрес без схема (напр. `nette.org`) и допълва схемата (`https://nette.org`). Изразът в `Pattern` и `PatternIcase` трябва да важи за цялата стойност, т.е. сякаш е обгърнат със знаците `^` и `$`. - - -Брой елементи -------------- - -При елементите `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` могат да се използват и следните правила за ограничаване на броя на избраните елементи съответно качените файлове: - -| `MinLength` | минимален брой | `int` -| `MaxLength` | максимален брой | `int` -| `Length` | брой в диапазон или точен брой | двойка `[int, int]` или `int` - - -Качване на файлове ------------------- - -При елементите `addUpload()`, `addMultiUpload()` могат да се използват и следните правила: - -| `MaxFileSize` | максимален размер на файла в байтове | `int` -| `MimeType` | MIME тип, разрешени са заместващи знаци (`'video/*'`) | `string\|string[]` -| `Image` | изображение JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | името на файла съответства на регулярен израз | `string` -| `PatternInsensitive` | като `Pattern`, но независимо от големината на буквите | `string` - -`MimeType` и `Image` изискват PHP разширението `fileinfo`. Дали файлът или изображението е от изисквания тип се открива въз основа на неговата сигнатура и **не се проверява целостта на целия файл.** Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |http:request#toImage]. - - -Съобщения за грешки -=================== - -Всички предварително дефинирани правила с изключение на `Pattern` и `PatternInsensitive` имат съобщение за грешка по подразбиране, така че то може да бъде пропуснато. Въпреки това, като посочите и формулирате всички съобщения по мярка, ще направите формата по-удобна за потребителя. - -Можете да промените съобщенията по подразбиране в [конфигурацията|forms:configuration], като редактирате текстовете в масива `Nette\Forms\Validator::$messages` или като използвате [преводач |rendering#Превод]. - -В текста на съобщенията за грешки могат да се използват следните заместващи низове: - -| `%d` | заменя последователно с аргументите на правилото -| `%n$d` | заменя с n-тия аргумент на правилото -| `%label` | заменя с надписа на елемента (без двоеточие) -| `%name` | заменя с името на елемента (напр. `name`) -| `%value` | заменя с въведената от потребителя стойност - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, попълнете %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'поне %d и най-много %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'най-много %2$d и поне %1$d', [5, 10]); -``` - - -Условия -======= - -Освен правила могат да се добавят и условия. Те се записват подобно на правилата, само че вместо `addRule()` използваме метода `addCondition()` и разбира се не посочваме никакво съобщение за грешка (условието само пита): - -```php -$form->addPassword('password', 'Парола:') - // ако паролата не е по-дълга от 8 знака - ->addCondition($form::MaxLength, 8) - // тогава трябва да съдържа цифра - ->addRule($form::Pattern, 'Трябва да съдържа цифра', '.*[0-9].*'); -``` - -Условието може да бъде обвързано и с друг елемент освен текущия с помощта на `addConditionOn()`. Като първи параметър посочваме референция към елемента. В този пример имейлът ще бъде задължителен само тогава, когато се отметне чекбоксът (неговата стойност ще бъде true): - -```php -$form->addCheckbox('newsletters', 'изпращайте ми бюлетини'); - -$form->addEmail('email', 'Имейл:') - // ако чекбоксът е отметнат - ->addConditionOn($form['newsletters'], $form::Equal, true) - // тогава изисквай имейл - ->setRequired('Въведете имейл адрес'); -``` - -От условията могат да се създават комплексни структури с помощта на `elseCondition()` и `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // ако е изпълнено първото условие - ->addConditionOn(/* ... */) // и второто условие на друг елемент - ->addRule(/* ... */) // изисквай това правило - ->elseCondition() // ако второто условие не е изпълнено - ->addRule(/* ... */) // изисквай тези правила - ->addRule(/* ... */) - ->endCondition() // връщаме се към първото условие - ->addRule(/* ... */); -``` - -В Nette може много лесно да се реагира на изпълнението или неизпълнението на условие и от страна на JavaScript с помощта на метода `toggle()`, виж [#Динамичен JavaScript]. - - -Референция към друг елемент -=========================== - -Като аргумент на правило или условие може да се предаде и друг елемент на формата. Правилото тогава ще използва стойността, въведена по-късно от потребителя в браузъра. Така може например динамично да се валидира, че елементът `password` съдържа същия низ като елемента `password_confirm`: - -```php -$form->addPassword('password', 'Парола'); -$form->addPassword('password_confirm', 'Потвърдете паролата') - ->addRule($form::Equal, 'Въведените пароли не съвпадат', $form['password']); -``` - - -Персонализирани правила и условия -================================= - -Понякога се озоваваме в ситуация, когато вградените правила за валидация в Nette не са достатъчни и трябва да валидираме данните от потребителя по свой начин. В Nette това е много лесно! - -На методите `addRule()` или `addCondition()` може да се предаде като първи параметър произволен callback. Той приема като първи параметър самия елемент и връща булева стойност, определяща дали валидацията е преминала успешно. При добавяне на правило с `addRule()` е възможно да се зададат и други аргументи, те след това се предават като втори параметър. - -Така можем да създадем собствен набор от валидатори като клас със статични методи: - -```php -class MyValidators -{ - // проверява дали стойността се дели на аргумента - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // други валидатори - } -} -``` - -Използването след това е много лесно: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Стойността трябва да е кратна на %d', - 8, - ); -``` - -Персонализирани правила за валидация могат да се добавят и в JavaScript. Условието е правилото да бъде статичен метод. Неговото име за JavaScript валидатора се образува чрез свързване на името на класа без обратни наклонени черти `\`, долна черта `_` и името на метода. Напр. `App\MyValidators::validateDivisibility` записваме като `AppMyValidators_validateDivisibility` и добавяме към обекта `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Събитие onValidate -================== - -След изпращане на формата се извършва валидация, при която се проверяват отделните правила, добавени с `addRule()`, и след това се извиква [събитие |nette:glossary#Събития events] `onValidate`. Неговият handler може да се използва за допълнителна валидация, типично проверка на правилната комбинация от стойности в няколко елемента на формата. - -Ако се открие грешка, я предаваме на формата с метода `addError()`. Той може да се извика или на конкретен елемент, или директно на формата. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Тази комбинация не е възможна.'); - } -} -``` - - -Грешки при обработка -==================== - -В много случаи научаваме за грешката едва когато обработваме валидната форма, например записваме нов елемент в базата данни и се натъкваме на дублиране на ключове. В такъв случай отново предаваме грешката на формата с метода `addError()`. Той може да се извика или на конкретен елемент, или директно на формата: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Невалидна парола.'); - } -} -``` - -Ако е възможно, препоръчваме да прикачите грешката директно към елемента на формата, тъй като тя ще се покаже до него при използване на рендеръра по подразбиране. - -```php -$form['date']->addError('Извиняваме се, но тази дата вече е заета.'); -``` - -Можете да извиквате `addError()` многократно и така да предавате на формата или елемента повече съобщения за грешки. Получавате ги с `getErrors()`. - -Внимание, `$form->getErrors()` връща резюме на всички съобщения за грешки, включително тези, които са били предадени директно на отделни елементи, не само директно на формата. Съобщенията за грешки, предадени само на формата, получавате чрез `$form->getOwnErrors()`. - - -Модификация на входа -==================== - -С помощта на метода `addFilter()` можем да променим въведената от потребителя стойност. В този пример ще толерираме и премахваме интервали в пощенския код: - -```php -$form->addText('zip', 'Пощенски код:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // премахваме интервалите от пощенския код - }) - ->addRule($form::Pattern, 'Пощенският код не е във формат от пет цифри', '\d{5}'); -``` - -Филтърът се интегрира между правилата за валидация и условията и следователно редът на методите има значение, т.е. филтърът и правилото се извикват в такъв ред, какъвто е редът на методите `addFilter()` и `addRule()`. - - -JavaScript валидация -==================== - -Езикът за формулиране на условия и правила е много мощен. Всички конструкции при това работят както от страна на сървъра, така и от страна на JavaScript. Пренасят се в HTML атрибути `data-nette-rules` като JSON. Самата валидация след това се извършва от скрипт, който прихваща събитието `submit` на формата, преминава през отделните елементи и извършва съответната валидация. - -Този скрипт е `netteForms.js` и е достъпен от няколко възможни източника: - -Можете да вмъкнете скрипта директно в HTML страницата от CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Или да го копирате локално в публичната папка на проекта (напр. от `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Или да го инсталирате чрез [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -И след това да го заредите и стартирате: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Алтернативно можете да го заредите директно от папката `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Динамичен JavaScript -==================== - -Искате да покажете полетата за въвеждане на адрес само ако потребителят избере стоката да бъде изпратена по пощата? Няма проблем. Ключът е двойката методи `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Този код казва, че когато условието е изпълнено, т.е. когато чекбоксът е отметнат, ще бъде видим HTML елементът `#address-container`. И обратното. Елементите на формата с адреса на получателя така поставяме в контейнер с това ID и при кликване върху чекбокса те се скриват или показват. Това осигурява скриптът `netteForms.js`. - -Като аргумент на метода `toggle()` може да се предаде произволен селектор. По исторически причини буквено-цифров низ без други специални знаци се разбира като ID на елемент, т.е. същото, като че ли му предхожда знакът `#`. Вторият незадължителен параметър позволява да се обърне поведението, т.е. ако използваме `toggle('#address-container', false)`, елементът ще се покаже обратно само тогава, ако чекбоксът не е отметнат. - -Имплементацията по подразбиране в JavaScript променя свойството `hidden` на елементите. Поведението обаче можем лесно да променим, например да добавим анимация. Достатъчно е в JavaScript да презапишем метода `Nette.toggle` със собствено решение: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // скриваме или показваме 'el' според стойността на 'visible' - }); -}; -``` - - -Изключване на валидацията -========================= - -Понякога може да се наложи да изключите валидацията. Ако натискането на бутона за изпращане не трябва да извършва валидация (подходящо за бутони *Cancel* или *Preview*), я изключваме с метода `$submit->setValidationScope([])`. Ако трябва да извършва само частична валидация, можем да определим кои полета или контейнери на формата трябва да се валидират. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Валидира цялата форма -$form->addSubmit('send2') - ->setValidationScope([]); // Не валидира изобщо -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Валидира само елемента name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Валидира само елемента age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Валидира контейнера details -``` - -`setValidationScope` не влияе на [#Събитие onValidate] на формата, която ще бъде извикана винаги. Събитието `onValidate` на контейнера ще бъде извикано само ако този контейнер е маркиран за частична валидация. diff --git a/forms/cs/@home.texy b/forms/cs/@home.texy index 496012fa21..8fe918e6ff 100644 --- a/forms/cs/@home.texy +++ b/forms/cs/@home.texy @@ -25,7 +25,7 @@ Formuláře můžete používat buď jako součást Nette Aplikace (tedy v prese Instalace --------- -Knihovnu stáhěte a nainstalujete pomocí nástroje [Composer|best-practices:composer]: +Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:composer]: ```shell composer require nette/forms diff --git a/forms/cs/@left-menu.texy b/forms/cs/@left-menu.texy index dbf61e5dbe..9c52ad98b6 100644 --- a/forms/cs/@left-menu.texy +++ b/forms/cs/@left-menu.texy @@ -6,7 +6,9 @@ Nette Forms - [Formulářové prvky |controls] - [Validace |validation] - [Vykreslování |rendering] +- [Vlastní prvky |custom-controls] - [Konfigurace |configuration] +- [Upgrade |upgrading] Další četba diff --git a/forms/cs/configuration.texy b/forms/cs/configuration.texy index 313f3faa12..cdd7fd2de6 100644 --- a/forms/cs/configuration.texy +++ b/forms/cs/configuration.texy @@ -17,6 +17,7 @@ forms: 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.' @@ -41,21 +42,22 @@ forms: Blank: 'Toto pole by mělo být prázdné.' MinLength: 'Zadejte prosím alespoň %d znaků.' MaxLength: 'Zadejte prosím maximálně %d znaků.' - Length: 'Zadejte prosím hodnotu %d až %d znaků dlouho.' + 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 bytů.' - MaxPostSize: 'Nahraná data překračují limit %d bytů.' + 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 obraz ve formátu JPEG, GIF, PNG, WebP nebo AVIF.' + 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žívate celý framework a tedy ani konfigurační soubory, můžete změnit výchozí chybové hlášky přímo v poli `Nette\Forms\Validator::$messages`. +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`. diff --git a/forms/cs/controls.texy b/forms/cs/controls.texy index 8b3396fa72..776ecf1c17 100644 --- a/forms/cs/controls.texy +++ b/forms/cs/controls.texy @@ -5,8 +5,8 @@ Formulářové prvky Přehled standardních formulářových prvků. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +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`. @@ -20,10 +20,10 @@ Automaticky validuje UTF-8, ořezává levo- a pravostranné mezery a odstraňuj 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]. +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. +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`. @@ -34,8 +34,8 @@ $form->addText('phone', 'Telefon:') ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +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`. @@ -49,10 +49,10 @@ Automaticky validuje UTF-8 a normalizuje oddělovače řádků na `\n`. Na rozd Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Přidá políčko pro zadání celočíselného čísla (třída [TextInput |api:Nette\Forms\Controls\TextInput]). Vrací buď integer, nebo `null`, pokud uživatel nic nezadá. +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:') @@ -62,8 +62,8 @@ $form->addInteger('year', 'Rok:') Prvek se vykresluje jako `<input type="number">`. Použitím metody `setHtmlType()` lze změnit typ na `range` pro zobrazení v podobě posuvníku, nebo na `text`, pokud preferujete standardní textové pole bez speciálního chování typu `number`. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +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á. @@ -75,11 +75,11 @@ $form->addFloat('level', 'Úroveň:') Prvek se vykresluje jako `<input type="number">`. Použitím metody `setHtmlType()` lze změnit typ na `range` pro zobrazení v podobě posuvníku, nebo na `text`, pokud preferujete standardní textové pole bez speciálního chování typu `number`. -Nette a prohlížeč Chrome akceptují jako oddělovač desetinných míst jak čárku, tak tečku. Aby byla tato funkcionalita dostupná i ve Firefoxu, je doporučeno nastavit atribut `lang` buď pro daný prvek nebo pro celou stránku, například `<html lang="cs">`. +Nette a prohlížeč Chrome akceptují jako oddělovač desetinných míst jak čárku, tak tečku. Aby byla tato funkcionalita dostupná i ve Firefoxu, je doporučeno nastavit atribut `lang` buď pro daný prvek, nebo pro celou stránku, například `<html lang="cs">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +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`. @@ -92,8 +92,8 @@ Ověří, zda je hodnota platná e-mailová adresa. Neověřuje se, zda doména Maximální délku lze omezit pomocí `setMaxLength()`. Pozměnit uživatelem vloženou hodnotu umožňuje [addFilter() |validation#Úprava vstupu]. Lze nastavit tzv. empty-value pomocí `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +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]). @@ -107,10 +107,10 @@ $form->addPassword('password', 'Heslo:') 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|int $name, $caption=null): Checkbox .[method] -================================================================ +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é. +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') @@ -118,10 +118,10 @@ $form->addCheckbox('agree', 'Souhlasím s podmínkami') ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +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í hodnoty místo klíčů. +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:', [ @@ -131,11 +131,11 @@ $form->addCheckboxList('colors', 'Barvy:', [ ]); ``` -Pole nabízených položek předáme jako třetí parametr nebo metodou `setItems()`. +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. +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)`. @@ -146,8 +146,8 @@ $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +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. @@ -155,27 +155,28 @@ Přidá přepínací tlačítka (třída [RadioList |api:Nette\Forms\Controls\Ra $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', 'f'])` lze deaktivovat jednotlivé položky. +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 jednou z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. +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|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +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', + 'CZ' => 'Česká republika', 'SK' => 'Slovensko', 'GB' => 'Velká Británie', ]; @@ -189,7 +190,7 @@ Pole nabízených položek předáme jako třetí parametr nebo metodou `setItem ```php $countries = [ 'Europe' => [ - 'CZ' => 'Česká Republika', + 'CZ' => 'Česká republika', 'SK' => 'Slovensko', 'GB' => 'Velká Británie', ], @@ -210,13 +211,13 @@ 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 jednou z nabízených, jinak vyhodí výjimku. Tuto kontrolu lze vypnout pomocí `checkDefaultValue(false)`. +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|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +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í hodnoty místo klíčů. +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); @@ -226,19 +227,19 @@ Pole nabízených položek předáme jako třetí parametr nebo metodou `setItem 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. +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|int $name, $label=null): UploadControl .[method] -================================================================= +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()`. +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 or AVIF.') + ->addRule($form::Image, 'Avatar musí být JPEG, PNG, GIF, WebP nebo AVIF.') ->addRule($form::MaxFileSize, 'Maximální velikost je 1 MB.', 1024 * 1024); ``` @@ -246,13 +247,13 @@ Pokud se soubor nepodaří korektně nahrát, formulář není úspěšně odesl 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]. +Pravidla `MimeType` a `Image` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený, lze zjistit například pokusem o jeho [načtení |http:request#toImage]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Přidá políčko pro upload více souboru najednou (třída [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrací pole objektů [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` u každého z nich bude vracet `true`. +Přidá políčko pro upload více 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:') @@ -263,15 +264,15 @@ Pokud se některý soubor nepodaří korektně nahrát, formulář není úspě 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]. +Pravidla `MimeType` a `Image` detekují požadovaný typ na základě signatury souboru a neověřují jeho integritu. Zda není obrázek poškozený, lze zjistit například pokusem o jeho [načtení |http:request#toImage]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +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. +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:') @@ -287,8 +288,8 @@ $form->addDate('date', 'Datum:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +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]). @@ -307,12 +308,12 @@ $form->addTime('time', 'Čas:') ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +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. +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:') @@ -328,8 +329,8 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +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`. @@ -339,8 +340,8 @@ $form->addColor('color', 'Barva:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== Přidá skryté pole (třída [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -353,8 +354,8 @@ Pomocí `setNullable()` lze nastavit, aby vracel `null` místo prázdného řet Ačkoli je prvek skrytý, je **důležité si uvědomit**, že hodnota může být stále modifikována nebo podvržena útočníkem. Vždy důkladně ověřujte a validujte všechny přijaté hodnoty na serverové straně, aby se předešlo bezpečnostním rizikům spojeným s manipulací dat. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== Přidá odesílací tlačítko (třída [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). @@ -362,6 +363,15 @@ Přidá odesílací tlačítko (třída [SubmitButton |api:Nette\Forms\Controls\ $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 @@ -380,8 +390,8 @@ 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|int $name, $caption): Button .[method] -======================================================= +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í. @@ -391,22 +401,22 @@ $form->addButton('raise', 'Zvýšit plat') ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +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'); +$form->addImageButton('submit', '/path/to/image.png', 'Odeslat'); ``` -Při použití více odeslacích tlačítek lze zjistit, na které bylo kliknuto, pomocí `$form['submit']->isSubmittedBy()`. +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]), nebo-li 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()`. +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'); @@ -437,7 +447,7 @@ Odeslaná data pak vrací jako vícerozměrnou strukturu: Přehled nastavení ================= -U všech prvků můžeme volat následující metody (kompletní přehled v [API dokumetaci|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): +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 @@ -451,8 +461,6 @@ Vykreslování: | `setTranslator($translator)` | nastaví [překladač |rendering#Překládání] | `setHtmlAttribute($name, $value)` | nastaví [HTML atribut |rendering#HTML atributy] elementu | `setHtmlId($id)` | nastaví HTML atribut `id` -| `setHtmlType($type)` | nastaví HTML atribut `type` -| `setHtmlName($name)` | nastaví HTML atribut `name` | `setOption($key, $value)` | [nastavení pro vykreslování |rendering#Options] Validace: @@ -462,7 +470,7 @@ Validace: | `addCondition()`, `addConditionOn()` | nastaví [validační podmínku |validation#Podmínky] | `addError($message)` | [předání chybové zprávy |validation#Chyby při zpracování] -U prvků `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` lze volat následující metody: +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 @@ -510,30 +518,14 @@ Alternativou disablovaných prvků jsou prvky s HTML atributem `readonly`, kter Vlastní prvky ============= -Vedle široké škály vestavěných formulářových prvků můžete do formuláře přidávat vlastní prvky tímto způsobem: +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:'); ``` -.[note] -Formulář je potomkem třídy [Container |component-model:#Container] a jednotlivé prvky jsou potomky [Component |component-model:#Component]. - -Existuje způsob, jak definovat nové metody formuláře sloužící k přidávání vlastních prvků (např. `$form->addZip()`). Jde o tzv. extension methods. Nevýhoda je, že pro ně nebude fungovat napovídání v editorech. - -```php -use Nette\Forms\Container; - -// přidáme metodu addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Alespoň 5 čísel', '[0-9]{5}'); -}); - -// použití -$form->addZip('zip', 'ZIP code:'); -``` +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 @@ -556,4 +548,4 @@ $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ářů. +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ářů. diff --git a/forms/cs/custom-controls.texy b/forms/cs/custom-controls.texy new file mode 100644 index 0000000000..1075017627 --- /dev/null +++ b/forms/cs/custom-controls.texy @@ -0,0 +1,268 @@ +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 `<select>` (zanořená pole se vykreslí jako `<optgroup>`) a vrátí jej jako `Html` - hodí se třeba pro políčko měsíce našeho data. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` vygeneruje seznam prvků `<input>` obalených v `<label>` (radio buttony nebo checkboxy) a vrátí jej jako řetězec. + +První políčko našeho data tedy vznikne takto: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select pro měsíc a input pro rok ... */); +} +``` + +Popisku vykresluje `getLabel()` a její výchozí implementace obvykle vyhovuje. Jen pozor: u složeného prvku ukazuje atributem `for` na `getHtmlId()`, dejte tedy toto id prvnímu inputu - přesně jako v ukázce. + +Aby šlo složený prvek v šabloně vykreslovat po částech (např. `{input birthdate:day}`), přepište metody `getControlPart($key)` a `getLabelPart($key)`, které vrací `Html` element dané části - stejně, jako to dělají `CheckboxList` a `RadioList`. + +.[note] +Pokud přepisujete `getControl()`, mějte na paměti, že `BaseControl::getControl()` zároveň označuje prvek jako vykreslený pomocí `setOption('rendered', true)`. Zavolejte to také (nebo zavolejte `parent::getControl()`), když kombinujete ruční a automatické vykreslování téhož formuláře, aby se prvek nevykreslil dvakrát. (Ukázka `DateInput` výše to pro stručnost nedělá.) + + +Kompletní příklad: DateInput +============================ + +Všechny popsané kousky pohromadě, doplněné o select box pro výběr měsíce, najdete v hotovém prvku `DateInput` mezi [příklady přímo v repozitáři |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Za pozornost stojí, že prvek si v konstruktoru sám přidává validační pravidlo kontrolující smysluplnost data. Nesmyslný vstup, třeba 31. února, se tak projeví jako běžná validační chyba formuláře: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'Datum není platné.'); +} +``` + +A použití? Přesně jako u vestavěných prvků: + +```php +$form['birthdate'] = (new DateInput('Datum narození:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('Kdy jste se narodil?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +V Latte šabloně ho vykreslíte běžnou značkou `{input birthdate}` nebo `{label birthdate /}`, stejně jako kterýkoliv jiný prvek. + + +Validace +======== + +Vestavěná validační pravidla fungují s vlastním prvkem rovnou - pracují s hodnotou z `getValue()`. Náš `DateInput` tak může používat třeba `Form::Min` pro nejstarší povolené datum. Jak psát pravidla vlastní, včetně JavaScriptového protějšku, popisuje kapitola [Vlastní pravidla a podmínky |validation#Vlastní pravidla a podmínky]. + + +Vlastní přidávací metoda +======================== + +Vestavěné prvky přidáváme pohodlnými metodami `$form->addText()` a spol. Vlastní prvek žádnou takovou metodu nemá, přidáte ho proto prostým přiřazením - funguje stejně ve formuláři i v kontejneru a editory i statická analýza tomu rozumí: + +```php +$form['birthdate'] = new DateInput('Datum narození:'); +``` + +Pokud chcete přidávání zkrátit a zároveň zachovat našeptávání, nabízí se statická tovární metoda přímo na prvku. Ta funguje i ve vnořených kontejnerech, což by metoda na potomkovi třídy `Form` neuměla - vnořené kontejnery ji totiž neznají: + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// funguje ve formuláři i v libovolném kontejneru: +DateInput::addTo($form, 'birthdate', 'Datum narození:'); +``` + +Stejný postup se hodí i jako pojmenovaná zkratka pro opakovanou konfiguraci vestavěného prvku: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'PSČ musí mít přesně 5 číslic', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'PSČ:'); +``` diff --git a/forms/cs/in-presenter.texy b/forms/cs/in-presenter.texy index 33ac7fa95d..937003bf7b 100644 --- a/forms/cs/in-presenter.texy +++ b/forms/cs/in-presenter.texy @@ -19,16 +19,16 @@ $form = new Form; $form->addText('name', 'Jméno:'); $form->addPassword('password', 'Heslo:'); $form->addSubmit('send', 'Registrovat'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` a v prohlížeči se zobrazí takto: [* form-cs.webp *] -Formulář v presenteru je objekt třídy `Nette\Application\UI\Form`, její předchůdce `Nette\Forms\Form` je určen pro samostatné užití. Přidali jsem do něj tzv. prvky jméno, heslo a odesílací tlačítko. A nakonec řádek s `$form->onSuccess` říká, že po odeslání a úspěšné validaci se má zavolat metoda `$this->formSucceeded()`. +Formulář v presenteru je objekt třídy `Nette\Application\UI\Form`, její předek `Nette\Forms\Form` je určen pro samostatné užití. Přidali jsme do něj prvky pojmenované name, password a odesílací tlačítko. A nakonec řádek s `$form->onSuccess` říká, že po odeslání a úspěšné validaci se má zavolat metoda `$this->formSucceeded()`. -Z pohledu presenteru je formulář běžná komponenta. Proto se s ním jako s komponentou zachází a začleníme ji do presenteru pomocí [tovární metody |application:components#Tovární metody]. Bude to vypadat takto: +Z pohledu presenteru je formulář běžná komponenta. Proto se s ním jako s komponentou zachází a začleníme jej do presenteru pomocí [tovární metody |application:components#Tovární metody]. Bude to vypadat takto: ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -42,11 +42,11 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Jméno:'); $form->addPassword('password', 'Heslo:'); $form->addSubmit('send', 'Registrovat'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { // tady zpracujeme data odeslaná formulářem // $data->name obsahuje jméno @@ -69,9 +69,9 @@ A to je vlastně vše :-) Máme funkční a perfektně [zabezpečený |#Ochrana A teď si nejspíš říkáte, že to bylo moc hrr, přemýšlíte, jak je možné, že se zavolá metoda `formSucceeded()` a co jsou parametry, které dostává. Jistě, máte pravdu, tohle si zaslouží vysvětlení. -Nette totiž přichází se svěžím mechanismem, kterému říkáme [Hollywood style |application:components#Hollywood style]. Místo toho, abyste se jako vývojář musel neustále vyptávat, jestli se něco událo („byl formulář odeslaný?“, „byl odeslaný validně?“ a „nedošlo k jeho podvržení?“), řeknete frameworku „až bude formulář validně vyplněný, zavolej tuhle metodu“ a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce, které se volají, až nastane určitá [událost |nette:glossary#události]. A jazyk jim předává příslušné argumenty. +Nette totiž přichází se svěžím mechanismem, kterému říkáme [Hollywood style |application:components#Hollywood style]. Místo toho, abyste se jako vývojář musel neustále vyptávat, jestli se něco událo ("byl formulář odeslaný?", "byl odeslaný validně?" a "nedošlo k jeho podvržení?"), řeknete frameworku "až bude formulář validně vyplněný, zavolej tuhle metodu" a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce, které se volají, až nastane určitá [událost |nette:glossary#události]. A jazyk jim předává příslušné argumenty. -Právě takhle je postaven i výše uvedený kód presenteru. Pole `$form->onSuccess` představuje seznam PHP callbacků, které Nette zavolá v okamžiku, kdy je formulář odeslán a správně vyplněn (tj. je validní). V rámci [životního cyklu presenteru |application:presenters#Životní cyklus presenteru] jde o tzv. signál, volají se tedy po `action*` metodě a před `render*` metodou. A každému callbacku předá jako první parametr samotný formulář a jako druhý odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash]. První parametr můžete vynechat, pokud objekt formuláře nepotřebujete. A druhý parametr umí být mazanější, ale o tom až [později |#Mapování na třídy]. +Právě takhle je postaven i výše uvedený kód presenteru. Pole `$form->onSuccess` představuje seznam PHP callbacků, které Nette zavolá v okamžiku, kdy je formulář odeslán a správně vyplněn (tj. je validní). V rámci [životního cyklu presenteru |application:presenters#Životní cyklus presenteru] jde o tzv. signál, volají se tedy po `action*` metodě a před `render*` metodou. A každému callbacku předá jako první parametr samotný formulář a jako druhý odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash] (nebo stdClass, případně vlastní třídy). První parametr můžete vynechat, pokud objekt formuláře nepotřebujete. A druhý parametr umí být mazanější, ale o tom až [později |#Mapování na třídy]. Objekt `$data` obsahuje klíče `name` a `password` s údaji, které vyplnil uživatel. Obvykle data rovnou posíláme k dalšímu zpracování, což může být například vložení do databáze. Během zpracování se ale může objevit chyba, například uživatelské jméno už je obsazené. V takovém případě chybu předáme zpět do formuláře pomocí `addError()` a necháme jej vykreslit znovu, i s chybovou hláškou. @@ -79,10 +79,12 @@ Objekt `$data` obsahuje klíče `name` a `password` s údaji, které vyplnil už $form->addError('Omlouváme se, uživatelské jméno už někdo používá.'); ``` -Kromě `onSuccess` existuje ještě `onSubmit`: callbacky se volají vždy po odeslání formuláře, i tehdy, pokud není správně vyplněn. A dále `onError`: callbacky se volají jen pokud odeslání validní není. Zavolají se dokonce i tehdy, pokud v `onSuccess` nebo `onSubmit` znevalidníme formulář pomocí `addError()`. +Kromě `onSuccess` existuje ještě `onSubmit`: callbacky se volají vždy po odeslání formuláře, i tehdy, pokud není správně vyplněn. A dále `onError`: callbacky se volají jen tehdy, pokud odeslání validní není. Zavolají se dokonce i tehdy, pokud v `onSuccess` znevalidníme formulář pomocí `addError()`. Po zpracování formuláře přesměrujeme na další stránku. Zabrání se tak nechtěnému opětovnému odeslání formuláře tlačítkem *obnovit*, *zpět* nebo pohybem v historii prohlížeče. +Pokud se formulář odesílá přes AJAX, místo přesměrování obvykle překreslíte [snippet |application:ajax] se znovu vykresleným formulářem. + Zkuste si přidat i další [formulářové prvky|controls]. @@ -132,7 +134,7 @@ Formulář se vždy validuje na straně serveru, ale také se generuje JavaScrip <script src="https://unpkg.com/nette-forms@3"></script> ``` -Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si všimnout, že Nette povinné prvky vkládá do elementů s CSS třídou `required`. Zkuste přidat do šablony následující stylopis a popiska „Jméno“ bude červená. Elegantně tak uživatelům vyznačíme povinné prvky: +Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si všimnout, že Nette povinné prvky vkládá do elementů s CSS třídou `required`. Zkuste přidat do šablony následující stylopis a popiska "Jméno" bude červená. Elegantně tak uživatelům vyznačíme povinné prvky: ```latte <style> @@ -142,7 +144,7 @@ Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si v Další validační pravidla přidáme metodou `addRule()`. První parametr je pravidlo, druhý je opět text chybové hlášky a může ještě následovat argument validačního pravidla. Co se tím myslí? -Formulář rozšíříme o nové nepovinné políčko „věk“, které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::Range`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: +Formulář rozšíříme o nové nepovinné políčko "věk", které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::Range`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: ```php $form->addInteger('age', 'Věk:') @@ -166,7 +168,7 @@ $form->addPassword('password', 'Heslo:') ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8); ``` -Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::Equal`). A jako parametr dáme odvolávku na první heslo pomocí [hranatých závorek |#Přístup k prvkům]: +Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::Equal`). A jako parametr dáme odkaz na první heslo pomocí [hranatých závorek |#Přístup k prvkům]: ```php $form->addPassword('passwordVerify', 'Heslo pro kontrolu:') @@ -175,7 +177,7 @@ $form->addPassword('passwordVerify', 'Heslo pro kontrolu:') ->setOmitted(); ``` -Pomocí `setOmitted()` jsme označili prvek, na jehož hodnotě nám vlastně nezáleží a která existuje jen z důvodu validace. Hodnota se nepředá do `$data`. +Pomocí `setOmitted()` jsme označili prvek, na jehož hodnotě nám vlastně nezáleží a který existuje jen z důvodu validace. Hodnota se nepředá do `$data`. Tímto máme hotový plně funkční formulář s validací v PHP i JavaScriptu. Validační schopnosti Nette jsou daleko širší, dají se vytvářet podmínky, nechávat podle nich zobrazovat a skrývat části stránky atd. Vše se dozvíte v kapitole o [validaci formulářů|validation]. @@ -183,7 +185,7 @@ Tímto máme hotový plně funkční formulář s validací v PHP i JavaScriptu. Výchozí hodnoty =============== -Prvkům formuláře běžne nastavujeme výchozí hodnoty: +Prvkům formuláře běžně nastavujeme výchozí hodnoty: ```php $form->addEmail('email', 'E-mail') @@ -193,12 +195,14 @@ $form->addEmail('email', 'E-mail') Často se hodí nastavit výchozí hodnoty všem prvkům současně. Třeba když formulář slouží k editaci záznamů. Přečteme záznam z databáze a nastavíme výchozí hodnoty: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Volejte `setDefaults()` až po definici prvků. +Na již odeslaném formuláři `setDefaults()` nic neudělá - nepřepíše, co uživatel vyplnil, takže ho můžete v továrně na formulář bez obav volat vždy. Pokud potřebujete hodnoty vynutit i po odeslání, použijte `setValues()`. + Vykreslení formuláře ==================== @@ -218,7 +222,7 @@ Způsobů, jak vykreslit formulář, je opravdu velké množství, takže je tom Mapování na třídy ================= -Vraťme se k metodě `formSucceeded()`, která ve druhém parametru `$data` dostává odeslaná data jako objekt `ArrayHash`. Protože jde o generickou třídu, něco jako `stdClass`, bude nám při práci s ní chybět určitý komfort, jako je třeba našeptávání properties v editorech nebo statická analýza kódu. To by se dalo vyrešit tím, že bychom pro každý formulář měli konkrétní třídu, jejíž properties reprezentují jednotlivé prvky. Např.: +Vraťme se k metodě `formSucceeded()`, která ve druhém parametru `$data` dostává odeslaná data jako objekt `ArrayHash` (nebo `stdClass`). Protože jde o generickou třídu, něco jako `stdClass`, bude nám při práci s ní chybět určitý komfort, jako je třeba našeptávání properties v editorech nebo statická analýza kódu. To by se dalo vyřešit tím, že bychom pro každý formulář měli konkrétní třídu, jejíž properties reprezentují jednotlivé prvky. Např.: ```php class RegistrationFormData @@ -245,18 +249,18 @@ class RegistrationFormData Property datové třídy mohou být také enumy a dojde k jejich automatickému namapování. .{data-version:3.2.4} -Jak říci Nette, aby nám data vracel jako objekty této třídy? Snadněji než si myslíte. Stačí pouze třídu uvést jako typ parametru `$data` v obslužné metodě: +Jak říci Nette, aby nám data vracelo jako objekty této třídy? Snadněji, než si myslíte. Stačí pouze třídu uvést jako typ parametru `$data` v obslužné metodě: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void { - // $name je instance RegistrationFormData + // $data je instance RegistrationFormData $name = $data->name; // ... } ``` -Jako typ lze uvést také `array` a pak data předá jako pole. +Jako typ lze uvést také `array` a pak Nette data předá jako pole. Obdobným způsobem lze používat i funkci `getValues()`, které název třídy nebo objekt k hydrataci předáme jako parametr: @@ -265,6 +269,8 @@ $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` +Pokud potřebujete hodnoty přečíst ještě před validací formuláře, typicky uvnitř handleru `onValidate`, použijte místo toho metodu `getUntrustedValues()`. Přijímá stejné parametry jako `getValues()`, ale vrací odeslané hodnoty bez záruky, že prošly validací. + Pokud formuláře tvoří víceúrovňovou strukturu složenou z kontejnerů, vytvořte pro každý samostatnou třídu: ```php @@ -303,23 +309,26 @@ Pokud má formulář více než jedno tlačítko, potřebujeme zpravidla rozliš ```php $form->addSubmit('save', 'Uložit') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Smazat') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -Tyto handlery se volají pouze v případě validně vyplněného formuláře, stejně jako v případě události `onSuccess`. Rozdíl je v tom, že jako první parametr se místo formulář může předat odesílací tlačítko, záleží na typu, který uvedete: +.{data-version:3.3.0} +Handler lze tlačítku předat také rovnou jako třetí argument metody `addSubmit()`. + +Tyto handlery se volají pouze v případě validně vyplněného formuláře (pokud není u tlačítka vypnutá validace), stejně jako v případě události `onSuccess`. Rozdíl je v tom, že jako první parametr se místo formuláře může předat odesílací tlačítko, záleží na typu, který uvedete: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Když se formulář odešle tlačítkem <kbd>Enter</kbd>, bere se to jako kdyby byl odeslán prvním tlačítkem. +Když se formulář odešle tlačítkem <kbd>Enter</kbd>, bere se to, jako kdyby byl odeslán prvním tlačítkem. Událost onAnchor @@ -334,7 +343,7 @@ $country = $form->addSelect('country', 'Stát:', $this->model->getCountries()); $city = $form->addSelect('city', 'Město:'); $form->onAnchor[] = function () use ($country, $city) { - // tato funkce se zavolá až bude formulář vědět, zda byl odeslán a s jakými daty + // tato funkce se zavolá, až bude formulář vědět, zda byl odeslán a s jakými daty // lze tedy používat metodu getValue() $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); @@ -345,27 +354,26 @@ $form->onAnchor[] = function () use ($country, $city) { Ochrana před zranitelnostmi =========================== -Nette Framework klade velký důraz na bezpečnost a proto úzkostlivě dbá na dobré zabezpečení formulářů. Dělá to zcela transparentně a nevyžaduje manuálně nic nastavovat. +Nette Framework klade velký důraz na bezpečnost, a proto úzkostlivě dbá na dobré zabezpečení formulářů. Dělá to zcela transparentně a nevyžaduje manuálně nic nastavovat. Kromě toho, že formuláře ochrání před útokem [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], dělá spoustu drobných zabezpečení, na které vy už nemusíte myslet. -Tak třeba odfiltruje ze vstupů všechny kontrolní znaky a prověří validitu UTF-8 kódování, takže data z formuláře budou vždycky čistá. U select boxů a radio listů ověřuje, že vybrané položky byly skutečně z nabízených a nedošlo k podvrhu. Už jsme zmiňovali, že u jednořádkových textových vstupů ostraňuje znaky konce řádků, které tam mohl poslat útočník. U víceřádkových vstupů zase normalizuje znaky pro konce řádků. A tak dále. +Tak třeba odfiltruje ze vstupů všechny kontrolní znaky a prověří validitu UTF-8 kódování, takže data z formuláře budou vždycky čistá. U select boxů a radio listů ověřuje, že vybrané položky byly skutečně z nabízených a nedošlo k podvrhu. Už jsme zmiňovali, že u jednořádkových textových vstupů nahrazuje znaky konce řádků, které tam mohl poslat útočník, mezerami. U víceřádkových vstupů zase normalizuje znaky pro konce řádků. A tak dále. Nette za vás řeší bezpečnostní rizika, o kterých spousta programátorů ani netuší, že existují. -Zmíněný CSRF útok spočívá v tom, že útočník naláká oběť na stránku, která nenápadně v prohlížeči oběti vykoná požadavek na server, na kterém je oběť přihlášena, a server se domnívá, že požadavek vykonala oběť o své vůli. Proto Nette zabraňuje odeslání POST formuláře z jiné domény. Pokud z nějakého důvodu chcete ochranu vypnout a dovolit odesílat formulář z jiné domény, použijte: +Zmíněný CSRF útok spočívá v tom, že útočník naláká oběť na stránku, která nenápadně v prohlížeči oběti vykoná požadavek na server, na kterém je oběť přihlášena, a server se domnívá, že požadavek vykonala oběť o své vůli. Proto Nette odmítá POST formuláře odeslané z cizího původu (origin); za cizí se přitom považuje i jiná subdoména téhož webu. Pokud potřebujete odesílání z jiného původu povolit, ochranu vypnete metodou: ```php -$form->allowCrossOrigin(); // POZOR! Vypne ochranu! +$form->allowCrossOrigin(); // POZOR! Vypne ochranu úplně! ``` -Tato ochrana využívá SameSite cookie pojmenovanou `_nss`. Ochrana pomocí SameSite cookie nemusí být 100% spolehlivá, proto je vhodné zapnout ještě ochranu pomocí tokenu: +Tím se ale ochrana vypne pro libovolný původ. Chcete-li povolit jen některé konkrétní původy, ochranu vypněte a hlavičku `Origin` si ověřte sami proti vlastnímu seznamu. -```php -$form->addProtection(); -``` +Ochrana se opírá o hlavičku `Sec-Fetch-Site` (Fetch Metadata), kterou prohlížeč posílá automaticky a kterou nelze podvrhnout ani při zranitelnosti XSS. U starších prohlížečů bez jejich podpory se uplatní záložní ochrana přes SameSite cookie, kterou Nette aplikace nastavuje automaticky. Podrobně to popisuje článek [CSRF konečně řeší prohlížeč |https://blog.nette.org/cs/csrf-konecne-resi-prohlizec]. -Doporučujeme takto chránit formuláře v administrační části webu, které mění citlivá data v aplikaci. Framework se proti útoku CSRF brání vygenerováním a ověřováním autorizačního tokenu, který se ukládá do session. Proto je nutné před zobrazením formuláře mít otevřenou session. V administrační části webu obvykle už session nastartovaná je kvůli přihlášení uživatele. Jinak session nastartujte metodou `Nette\Http\Session::start()`. +.[note] +Dřívější ochrana pomocí autorizačního tokenu ukládaného do session, kterou aktivovala metoda `$form->addProtection()`, už není potřeba a od verze 3.3 je zavržená. Stejný formulář ve více presenterech @@ -403,7 +411,7 @@ protected function createComponentSignInForm(): Form $form = $this->formFactory->create(); // můžeme formulář pozměnit, zde například měníme popisku na tlačítku $form['send']->setCaption('Pokračovat'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // a přidáme handler + $form->onSuccess[] = $this->signInFormSucceeded(...); // a přidáme handler return $form; } ``` @@ -428,4 +436,4 @@ class SignInFormFactory } ``` -Tak, máme za sebou rychlý úvod do formulářů v Nette. Zkuste se ještě podívat do adresáře [examples|https://github.com/nette/forms/tree/master/examples] v distrubuci, kde najdete další inspiraci. +Tak, máme za sebou rychlý úvod do formulářů v Nette. Zkuste se ještě podívat do adresáře [examples|https://github.com/nette/forms/tree/master/examples] v distribuci, kde najdete další inspiraci. diff --git a/forms/cs/rendering.texy b/forms/cs/rendering.texy index e1231ad302..f41c771425 100644 --- a/forms/cs/rendering.texy +++ b/forms/cs/rendering.texy @@ -9,7 +9,7 @@ Na druhé straně tu jsou rozmanité formuláře, kde platí: co kus, to origin Vykreslení pomocí Latte ======================= -[Šablonovací sytém Latte|latte:] zásadně usnadňuje vykreslení formulářů a jejich prvků. Nejprve si ukážeme, jak formuláře vykreslovat ručně po jednotlivých prvcích a tím získat plnou kontrolu nad kódem. Později si ukážeme, jak lze takové vykreslování [zautomatizovat |#Automatické vykreslování]. +[Šablonovací systém Latte|latte:] zásadně usnadňuje vykreslení formulářů a jejich prvků. Nejprve si ukážeme, jak formuláře vykreslovat ručně po jednotlivých prvcích a tím získat plnou kontrolu nad kódem. Později si ukážeme, jak lze takové vykreslování [zautomatizovat |#Automatické vykreslování]. Návrh Latte šablony formuláře si můžete nechat vygenerovat pomocí metody `Nette\Forms\Blueprint::latte($form)`, která jej vypíše do stránky prohlížeče. Kód pak stačí kliknutím označit a zkopírovat do projektu. .{data-version:3.1.15} @@ -56,7 +56,7 @@ protected function createComponentSignInForm(): Form </form> ``` -Podobu výsledného HTML kódu máte plně ve svých rukou. Pokud atribut `n:name` použijete u elementů `<select>`, `<button>` nebo `<textarea>`, jejich vnitřní obsah se automaticky doplní. Značka `<form n:name>` navíc vytvoří lokální proměnnou `$form` s objektem kresleného formuláře a uzavírací `</form>` vykreslí všechny nevykreslené hidden prvky (totéž platí i pro `{form} ... {/form}`). +Podobu výsledného HTML kódu máte plně ve svých rukou. Pokud atribut `n:name` použijete u elementů `<select>`, `<button>` nebo `<textarea>`, jejich vnitřní obsah se automaticky doplní. Značka `<form n:name>` navíc vytvoří lokální proměnnou `$form` s objektem vykreslovaného formuláře a uzavírací `</form>` vykreslí všechny nevykreslené hidden prvky (totéž platí i pro `{form} ... {/form}`). Nesmíme ovšem zapomenout na vykreslení možných chybových zpráv. A to jak těch, které se metodou `addError()` přidaly k jednotlivým prvkům (pomocí `{inputError}`), tak i těch přidaných přímo k formuláři (vrací je `$form->getOwnErrors()`): @@ -92,7 +92,7 @@ Složitější formulářové prvky, jako je RadioList nebo CheckboxList, lze ta `{label}` `{input}` ------------------- -Nechcete u každého prvku přemýšlet, jaký HTML element pro něj v šabloně použít, zda `<input>`, `<textarea>` atd? Řešením je univerzální značka `{input}`: +Nechcete u každého prvku přemýšlet, jaký HTML element pro něj v šabloně použít, zda `<input>`, `<textarea>` atd.? Řešením je univerzální značka `{input}`: ```latte <form n:name=signInForm class=form> @@ -114,7 +114,7 @@ Nechcete u každého prvku přemýšlet, jaký HTML element pro něj v šabloně </form> ``` -Pokud formulář používá translator, bude text uvnitř značek `{label}` překládán. +Pokud formulář používá translator, překládají se popisky pocházející z definice formuláře (např. `{label username /}`). Text zapsaný přímo mezi značky `{label}` a `{/label}` se nepřekládá. I v tomto případě lze složitější formulářové prvky, jako je RadioList nebo CheckboxList, vykreslovat po jednotlivých položkách: @@ -149,7 +149,25 @@ Přítomnost chyby můžeme zjistit metodou `hasErrors()` a podle toho nastavit `{form}` -------- -Značky `{form signInForm}...{/form}` jsou alternativou k `<form n:name="signInForm">...</form>`. +Značky `{form signInForm}...{/form}` jsou alternativou k `<form n:name="signInForm">...</form>`. Případné argumenty oddělte od názvu čárkou: `{form signInForm, class: foo}`. + +.{data-version:3.3.0} +Klíčové slovo `scope` před názvem způsobí, že se formulář pouze vloží na zásobník (aby na něj navazovaly `{input}`, `{label}` atd.), ale značka `<form>` se nevykreslí. Hodí se k vykreslení části formuláře, např. ve snippetu. Pokud je už nějaký formulář aktivní, název se hledá relativně v něm, takže `{form scope}` zároveň zastupuje `{formContainer}`: + +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +Klíčové slovo `detached` vykreslí prázdné `<form></form>` a každý prvek na něj naváže přes HTML atribut `form`. Díky tomu lze mít formulář vnořený uvnitř jiného formuláře, což HTML jinak zakazuje. Odpojený formulář musí mít HTML `id`, které vznikne automaticky, když mu předáte název (jako `outerForm` níže): + +```latte +{form detached outerForm} + ... +{/form} +``` Automatické vykreslování @@ -240,7 +258,7 @@ Pokud potřebujete vykreslit jen vnitřní část formuláře bez HTML značek ` </form> ``` -S vykreslením prvků uvnitř formulářového kontejneru pomůže tag `{formContainer}`. +S vykreslením prvků uvnitř formulářového kontejneru pomůže tag `{formContainer}` nebo novější [`{form scope}` |#form]. ```latte <p>Which news you wish to receive:</p> @@ -269,7 +287,7 @@ Ovlivnit podobu takto vykresleného formuláře lze konfigurací [Rendereru |#Re Manuální vykreslení ------------------- -Každý formulářový prvek disponuje metodami, které generují HTML kód formulářového políčka a popisky. Mohou jej vracet buď jako řetězec nebo objekt [Nette\Utils\Html|utils:html-elements]: +Každý formulářový prvek disponuje metodami, které generují HTML kód formulářového políčka a popisky. Mohou jej vracet buď jako řetězec, nebo jako objekt [Nette\Utils\Html|utils:html-elements]: - `getControl(): Html|string` vrací HTML kód prvku - `getLabel($caption = null): Html|string|null` vrací HTML kód popisky, pokud existuje @@ -278,18 +296,18 @@ Formulář tak lze vykreslovat po jednotlivých elementech: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,10 +315,10 @@ Formulář tak lze vykreslovat po jednotlivých elementech: <?php $form->render('end') ?> ``` -Zatímco u některých prvků vrací `getControl()` jediný HTML element (např. `<input>`, `<select>` apod.), u jiných celý kus HTML kódu (CheckboxList, RadioList). V takovém případě můžete využít metod, které generují jednotlivé inputy a popisky, pro každou položku zvlášt: +Zatímco u některých prvků vrací `getControl()` jediný HTML element (např. `<input>`, `<select>` apod.), u jiných celý kus HTML kódu (CheckboxList, RadioList). V takovém případě můžete využít metod, které generují jednotlivé inputy a popisky, pro každou položku zvlášť: -- `getControlPart($key = null): ?Html` vrací HTML kód jedné položky -- `getLabelPart($key = null): ?Html` vrací HTML kód popisky jedené položky +- `getControlPart($key = null): Html` vrací HTML kód jedné položky +- `getLabelPart($key = null): Html` vrací HTML kód popisky jedné položky .[note] Tyto metody mají z historických důvodů prefix `get`, ale lepší by byl `generate`, protože při každém volání vytvoří a vrátí nový element `Html`. @@ -332,7 +350,7 @@ Pokud nenastavíme vlastní renderer, bude použit výchozí vykreslovač [api:N ... ``` -Zda použít nebo nepoužít pro kostru formuláře tabulku je sporné a řada webdesignerů preferuje jiný markup. Například definiční seznam. Překonfigurujeme proto `DefaultFormRenderer` tak, aby formulář v podobě seznamu vykreslil. Konfigurace se provádí editací pole [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. První index vždy představuje oblast a druhý její atribut. Jednotlivé oblasti znázorňuje obrázek: +Zda pro kostru formuláře použít tabulku, je sporné a řada webdesignerů preferuje jiný markup. Například definiční seznam. Překonfigurujeme proto `DefaultFormRenderer` tak, aby formulář v podobě seznamu vykreslil. Konfigurace se provádí editací pole [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. První index vždy představuje oblast a druhý její atribut. Jednotlivé oblasti znázorňuje obrázek: [* defaultformrenderer.webp *] @@ -372,7 +390,7 @@ V poli wrappers lze ovlivnit celou řadu dalších atributů: - přidávat CSS třídy jednotlivým typům formulářových prvků - rozlišovat CSS třídou liché a sudé řádky - vizuálně odlišit povinné a volitelné položky -- určovat, zda se chybové zprávy zobrazí přímo u prvků nebo nad formulářem +- určovat, zda se chybové zprávy zobrazí přímo u prvků, nebo nad formulářem Options @@ -385,7 +403,7 @@ $form->addText('phone', 'Číslo:') ->setOption('description', 'Toto číslo zůstane skryté'); ``` -Pokud do něj chceme umístit HTML obsah, využijeme třídy [Html |utils:html-elements] +Pokud do něj chceme umístit HTML obsah, využijeme třídu [Html |utils:html-elements]: ```php use Nette\Utils\Html; @@ -431,7 +449,7 @@ Renderer nejprve vykresluje skupiny a teprve poté prvky, které do žádné sku Podpora pro Bootstrap --------------------- -[V příkladech |https://github.com/nette/forms/tree/master/examples] najdete ukázky, jak nakonfigurovat Renderer pro [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] a [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] +[V příkladech |https://github.com/nette/forms/tree/master/examples] najdete ukázky, jak nakonfigurovat Renderer pro [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] a [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. HTML atributy @@ -460,7 +478,7 @@ $form->addText('tel', 'Váš telefon:') ``` .[warning] -Nastavení typu a dalších atributů slouží jen pro vizuální účely. Ověření správnosti vstupů musí probíhat na serveru, což zajístíte volbou vhodného [formulářového prvku|controls] a uvedením [validačních pravidel|validation]. +Nastavení typu a dalších atributů slouží jen pro vizuální účely. Ověření správnosti vstupů musí probíhat na serveru, což zajistíte volbou vhodného [formulářového prvku|controls] a uvedením [validačních pravidel|validation]. Jednotlivým položkám v radio nebo checkbox listech můžeme nastavit HTML atribut s rozdílnými hodnotami pro každou z nich. Povšimněte si dvojtečky za `style:`, která zajistí volbu hodnoty podle klíče: @@ -483,7 +501,7 @@ Pro nastavení logických atributů, jako je `readonly`, můžeme použít zápi ```php $form->addCheckboxList('colors', 'Barvy:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // pro více klíču použijte pole, např. ['r', 'g'] + ->setHtmlAttribute('readonly?', 'r'); // pro více klíčů použijte pole, např. ['r', 'g'] ``` Vypíše: @@ -530,7 +548,7 @@ $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -U prvků Checkbox, CheckboxList a RadioList můžete ovlivnit předlohu elementu, který celý prvek obaluje. Vrací jej `getContainerPrototype()`. Ve výchozím stavu jde o „prázdný“ element, takže se nic nevykresluje, ale tím, že mu nastavíme název, se vykreslovat bude: +U prvků Checkbox, CheckboxList a RadioList můžete ovlivnit předlohu elementu, který celý prvek obaluje. Vrací jej `getContainerPrototype()`. Ve výchozím stavu jde o "prázdný" element, takže se nic nevykresluje, ale tím, že mu nastavíme název, se vykreslovat bude: ```php $input = $form->addCheckbox('send'); @@ -541,13 +559,13 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -V připadě CheckboxList a RadioList lze ovlivnit i předlohu oddělovače jednotlivých položek, který vrací metoda `getSeparatorPrototype()`. Ve výchozím stavu je to element `<br>`. Pokud jej změníte na párový element, bude jednotlivé položky obalovat místo oddělovat. A dále lze ovlivnit předlohu HTML elementu popisky u jednotlivých položek, který vrací `getItemLabelPrototype()`. +V případě CheckboxList a RadioList lze ovlivnit i předlohu oddělovače jednotlivých položek, který vrací metoda `getSeparatorPrototype()`. Ve výchozím stavu je to element `<br>`. Pokud jej změníte na párový element, bude jednotlivé položky obalovat místo oddělovat. A dále lze ovlivnit předlohu HTML elementu popisky u jednotlivých položek, který vrací `getItemLabelPrototype()`. Překládání ========== -Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat formulář vykreslit v různých jazykových mutacích. Nette Framework k tomuto účelu definuje rozhraní pro překlad [api:Nette\Localization\Translator]. V Nette není žádná výchozí implementace, můžete si vybrat podle svých potřeb z několika hotových řešeních, které najdete na [Componette |https://componette.org/search/localization]. V jejich dokumentaci se dozvíte, jak translator konfigurovat. +Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat formulář vykreslit v různých jazykových mutacích. Nette Framework k tomuto účelu definuje rozhraní pro překlad [api:Nette\Localization\Translator]. V Nette není žádná výchozí implementace, můžete si vybrat podle svých potřeb z několika hotových řešení, která najdete na [Componette |https://componette.org/search/localization]. V jejich dokumentaci se dozvíte, jak translator konfigurovat. Formuláře podporují vypisování textů přes translator. Předáme jim ho pomocí metody `setTranslator()`: @@ -555,7 +573,7 @@ Formuláře podporují vypisování textů přes translator. Předáme jim ho po $form->setTranslator($translator); ``` -Od této chvíle se nejen všechny popisky, ale i všechny chybové hlášky nebo položky select boxů přeloží do jiného jazyka. +Od této chvíle se nejen všechny popisky, ale i všechny chybové hlášky, položky select boxů a placeholdery inputů přeloží do jiného jazyka. U jednotlivých formulářových prvků je přitom možné nastavit jiný překladač nebo překládání úplně vypnout hodnotou `null`: @@ -583,7 +601,7 @@ a tedy může zvolit správný tvar plurálu u slova `znaky` podle počtu. Událost onRender ================ -Těsně před tím, než se formulář vykreslí, můžeme nechat zavolat náš kód. Ten může například doplnit formulářovým prvkům HTML třídy pro správné zobrazení. Kód přidáme do pole `onRender`: +Těsně předtím, než se formulář vykreslí, můžeme nechat zavolat náš kód. Ten může například doplnit formulářovým prvkům HTML třídy pro správné zobrazení. Kód přidáme do pole `onRender`: ```php $form->onRender[] = function ($form) { diff --git a/forms/cs/standalone.texy b/forms/cs/standalone.texy index 1b980ceeda..d38efd762d 100644 --- a/forms/cs/standalone.texy +++ b/forms/cs/standalone.texy @@ -10,6 +10,12 @@ Pokud ale používáte Nette Application a presentery, je pro vás určen návod První formulář ============== +Než začnete, nainstalujte balíček pomocí [Composeru |best-practices:composer]: + +```shell +composer require nette/forms +``` + Zkusíme si napsat jednoduchý registrační formulář. Jeho kód bude následující ("celý kód":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): ```php @@ -31,7 +37,7 @@ a v prohlížeči se zobrazí takto: [* form-cs.webp *] -Formulář je objekt třídy `Nette\Forms\Form` (třída `Nette\Application\UI\Form` se používá v presenterech). Přidali jsem do něj tzv. prvky jméno, heslo a odesílací tlačítko. +Formulář je objekt třídy `Nette\Forms\Form` (třída `Nette\Application\UI\Form` se používá v presenterech). Přidali jsme do něj prvky pojmenované 'name', 'password' a odesílací tlačítko. A teď formulář oživíme. Dotazem na `$form->isSuccess()` zjistíme, zda byl formulář odeslán a zda byl vyplněn validně. Pokud ano, data vypíšeme. Za definici formuláře tedy doplníme: @@ -45,7 +51,7 @@ if ($form->isSuccess()) { } ``` -Metoda `getValues()` vrací odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash]. Jak to změnit si ukážeme [později |#Mapování na třídy]. Objekt `$data` obsahuje klíče `name` a `password` s údaji, které vyplnil uživatel. +Metoda `getValues()` vrací odeslaná data v podobě objektu [ArrayHash |utils:arrays#ArrayHash]. Jak to změnit, si ukážeme [později |#Mapování na třídy]. Objekt `$data` obsahuje klíče `name` a `password` s údaji, které vyplnil uživatel. Obvykle data rovnou posíláme k dalšímu zpracování, což může být například vložení do databáze. Během zpracování se ale může objevit chyba, například uživatelské jméno už je obsazené. V takovém případě chybu předáme zpět do formuláře pomocí `addError()` a necháme jej vykreslit znovu, i s chybovou hláškou. @@ -80,7 +86,7 @@ $button = $form->getComponent('send'); // alternativní syntax: $button = $form['send']; ``` -Prvky se odstraní pomocí unset: +Prvky se odstraní pomocí `unset`: ```php unset($form['name']); @@ -109,7 +115,7 @@ Formulář se vždy validuje na straně serveru, ale také se generuje JavaScrip <script src="https://unpkg.com/nette-forms@3"></script> ``` -Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si všimnout, že Nette povinné prvky vkládá do elementů s CSS třídou `required`. Zkuste přidat do šablony následující stylopis a popiska „Jméno“ bude červená. Elegantně tak uživatelům vyznačíme povinné prvky: +Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si všimnout, že Nette povinné prvky vkládá do elementů s CSS třídou `required`. Zkuste přidat do šablony následující stylopis a popiska "Jméno" bude červená. Elegantně tak uživatelům vyznačíme povinné prvky: ```latte <style> @@ -119,7 +125,7 @@ Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si v Další validační pravidla přidáme metodou `addRule()`. První parametr je pravidlo, druhý je opět text chybové hlášky a může ještě následovat argument validačního pravidla. Co se tím myslí? -Formulář rozšíříme o nové nepovinné políčko „věk“, které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::Range`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: +Formulář rozšíříme o nové nepovinné políčko "věk", které musí být celé číslo (`addInteger()`) a navíc v povoleném rozsahu (`$form::Range`). A zde právě využijeme třetí parametr metody `addRule()`, kterým předáme validátoru požadovaný rozsah jako dvojici `[od, do]`: ```php $form->addInteger('age', 'Věk:') @@ -143,7 +149,7 @@ $form->addPassword('password', 'Heslo:') ->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8); ``` -Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::Equal`). A jako parametr dáme odvolávku na první heslo pomocí [hranatých závorek |#Přístup k prvkům]: +Přidáme do formuláře ještě políčko `passwordVerify`, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (`$form::Equal`). A jako parametr dáme odkaz na první heslo pomocí [hranatých závorek |#Přístup k prvkům]: ```php $form->addPassword('passwordVerify', 'Heslo pro kontrolu:') @@ -152,7 +158,7 @@ $form->addPassword('passwordVerify', 'Heslo pro kontrolu:') ->setOmitted(); ``` -Pomocí `setOmitted()` jsme označili prvek, na jehož hodnotě nám vlastně nezáleží a která existuje jen z důvodu validace. Hodnota se nepředá do `$data`. +Pomocí `setOmitted()` jsme označili prvek, na jehož hodnotě nám vlastně nezáleží a který existuje jen z důvodu validace. Hodnota se nepředá do `$data`. Tímto máme hotový plně funkční formulář s validací v PHP i JavaScriptu. Validační schopnosti Nette jsou daleko širší, dají se vytvářet podmínky, nechávat podle nich zobrazovat a skrývat části stránky atd. Vše se dozvíte v kapitole o [validaci formulářů|validation]. @@ -160,7 +166,7 @@ Tímto máme hotový plně funkční formulář s validací v PHP i JavaScriptu. Výchozí hodnoty =============== -Prvkům formuláře běžne nastavujeme výchozí hodnoty: +Prvkům formuláře běžně nastavujeme výchozí hodnoty: ```php $form->addEmail('email', 'E-mail') @@ -170,12 +176,14 @@ $form->addEmail('email', 'E-mail') Často se hodí nastavit výchozí hodnoty všem prvkům současně. Třeba když formulář slouží k editaci záznamů. Přečteme záznam z databáze a nastavíme výchozí hodnoty: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Volejte `setDefaults()` až po definici prvků. +Na již odeslaném formuláři `setDefaults()` nic neudělá - nepřepíše, co uživatel vyplnil, takže ho můžete v továrně na formulář bez obav volat vždy. Pokud potřebujete hodnoty vynutit i po odeslání, použijte `setValues()`. + Vykreslení formuláře ==================== @@ -192,10 +200,25 @@ $form->addInteger('age', 'Věk:') Způsobů, jak vykreslit formulář, je opravdu velké množství, takže je tomu věnována [samostatná kapitola o vykreslování|rendering]. +Vykreslení pomocí Latte +----------------------- + +Pokud máte po ruce šablonovací systém [Latte |latte:], můžete jím formulář vykreslit a získat plnou kontrolu nad výsledným HTML. Vytvoříme engine, zaregistrujeme rozšíření pro formuláře a formulář předáme do šablony jako proměnnou: + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); +``` + +V šabloně pak s formulářem pracujete přes proměnnou `$form` a značky jako `{input}`, `{label}` nebo `n:name`. Kompletní příklad včetně šablony najdete v adresáři [examples |https://github.com/nette/forms/tree/master/examples] (soubory `latte.php` a `latte/`). Jednotlivé značky popisuje kapitola o [vykreslování |rendering]. + + Mapování na třídy ================= -Vraťme se ke zpracování formulářových dat. Metoda `getValues()` nám vracela odeslaná data jako objekt `ArrayHash`. Protože jde o generickou třídu, něco jako `stdClass`, bude nám při práci s ní chybět určitý komfort, jako je třeba našeptávání properties v editorech nebo statická analýza kódu. To by se dalo vyrešit tím, že bychom pro každý formulář měli konkrétní třídu, jejíž properties reprezentují jednotlivé prvky. Např.: +Vraťme se ke zpracování formulářových dat. Metoda `getValues()` nám vracela odeslaná data jako objekt `ArrayHash`. Protože jde o generickou třídu, něco jako `stdClass`, bude nám při práci s ní chybět určitý komfort, jako je třeba našeptávání properties v editorech nebo statická analýza kódu. To by se dalo vyřešit tím, že bychom pro každý formulář měli konkrétní třídu, jejíž properties reprezentují jednotlivé prvky. Např.: ```php class RegistrationFormData @@ -222,14 +245,14 @@ class RegistrationFormData Property datové třídy mohou být také enumy a dojde k jejich automatickému namapování. .{data-version:3.2.4} -Jak říci Nette, aby nám data vracel jako objekty této třídy? Snadněji než si myslíte. Stačí pouze název třídy nebo objekt k hydrataci uvést jako parametr: +Jak říci Nette, aby nám data vracelo jako objekty této třídy? Snadněji, než si myslíte. Stačí pouze název třídy nebo objekt k hydrataci uvést jako parametr: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Jako parametr lze uvést také `'array'` a pak data vrátí jako pole. +Jako parametr lze uvést také `'array'` a pak Nette data vrátí jako pole. Pokud formuláře tvoří víceúrovňovou strukturu složenou z kontejnerů, vytvořte pro každý samostatnou třídu: @@ -284,34 +307,31 @@ if ($form->isSuccess()) { Dotaz na `$form->isSuccess()` nevynechejte, ověříte tím validitu dat. -Když se formulář odešle tlačítkem <kbd>Enter</kbd>, bere se to jako kdyby byl odeslán prvním tlačítkem. +Když se formulář odešle klávesou <kbd>Enter</kbd>, bere se to, jako kdyby byl odeslán prvním tlačítkem. Ochrana před zranitelnostmi =========================== -Nette Framework klade velký důraz na bezpečnost a proto úzkostlivě dbá na dobré zabezpečení formulářů. +Nette Framework klade velký důraz na bezpečnost, a proto úzkostlivě dbá na dobré zabezpečení formulářů. Kromě toho, že formuláře ochrání před útokem [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], dělá spoustu drobných zabezpečení, na které vy už nemusíte myslet. -Tak třeba odfiltruje ze vstupů všechny kontrolní znaky a prověří validitu UTF-8 kódování, takže data z formuláře budou vždycky čistá. U select boxů a radio listů ověřuje, že vybrané položky byly skutečně z nabízených a nedošlo k podvrhu. Už jsme zmiňovali, že u jednořádkových textových vstupů ostraňuje znaky konce řádků, které tam mohl poslat útočník. U víceřádkových vstupů zase normalizuje znaky pro konce řádků. A tak dále. +Tak třeba odfiltruje ze vstupů všechny kontrolní znaky a prověří validitu UTF-8 kódování, takže data z formuláře budou vždycky čistá. U select boxů a radio listů ověřuje, že vybrané položky byly skutečně z nabízených a nedošlo k podvrhu. Už jsme zmiňovali, že u jednořádkových textových vstupů nahrazuje znaky konce řádků, které tam mohl poslat útočník, mezerami. U víceřádkových vstupů zase normalizuje znaky pro konce řádků. A tak dále. Nette za vás řeší bezpečnostní rizika, o kterých spousta programátorů ani netuší, že existují. -Zmíněný CSRF útok spočívá v tom, že útočník naláká oběť na stránku, která nenápadně v prohlížeči oběti vykoná požadavek na server, na kterém je oběť přihlášena, a server se domnívá, že požadavek vykonala oběť o své vůli. Proto Nette zabraňuje odeslání POST formuláře z jiné domény. Pokud z nějakého důvodu chcete ochranu vypnout a dovolit odesílat formulář z jiné domény, použijte: +Zmíněný CSRF útok spočívá v tom, že útočník naláká oběť na stránku, která nenápadně v prohlížeči oběti vykoná požadavek na server, na kterém je oběť přihlášena, a server se domnívá, že požadavek vykonala oběť o své vůli. Proto Nette odmítá POST formuláře odeslané z cizího původu (origin); za cizí se přitom považuje i jiná subdoména téhož webu. Pokud potřebujete odesílání z jiného původu povolit, ochranu vypnete metodou: ```php -$form->allowCrossOrigin(); // POZOR! Vypne ochranu! +$form->allowCrossOrigin(); // POZOR! Vypne ochranu úplně! ``` -Tato ochrana využívá SameSite cookie pojmenovanou `_nss`. Vytvářejte proto objekt formuláře ještě před odesláním prvního výstupu, aby bylo možné cookie odeslat. +Tím se ale ochrana vypne pro libovolný původ. Chcete-li povolit jen některé konkrétní původy, ochranu vypněte a hlavičku `Origin` si ověřte sami proti vlastnímu seznamu. -Ochrana pomocí SameSite cookie nemusí být 100% spolehlivá, proto je vhodné zapnout ještě ochranu pomocí tokenu: - -```php -$form->addProtection(); -``` +Ochrana se opírá o hlavičku `Sec-Fetch-Site` (Fetch Metadata), kterou prohlížeč posílá automaticky a kterou nelze podvrhnout ani při zranitelnosti XSS. Starší prohlížeče, které tyto hlavičky neposílají, kontrolou neprojdou. Podrobně to popisuje článek [CSRF konečně řeší prohlížeč |https://blog.nette.org/cs/csrf-konecne-resi-prohlizec]. -Doporučujeme takto chránit formuláře v administrační části webu, které mění citlivá data v aplikaci. Framework se proti útoku CSRF brání vygenerováním a ověřováním autorizačního tokenu, který se ukládá do session. Proto je nutné před zobrazením formuláře mít otevřenou session. V administrační části webu obvykle už session nastartovaná je kvůli přihlášení uživatele. Jinak session nastartujte metodou `Nette\Http\Session::start()`. +.[note] +Dřívější ochrana pomocí autorizačního tokenu ukládaného do session, kterou aktivovala metoda `$form->addProtection()`, už není potřeba a od verze 3.3 je zavržená. -Tak, máme za sebou rychlý úvod do formulářů v Nette. Zkuste se ještě podívat do adresáře [examples|https://github.com/nette/forms/tree/master/examples] v distrubuci, kde najdete další inspiraci. +Tak, máme za sebou rychlý úvod do formulářů v Nette. Zkuste se ještě podívat do adresáře [examples|https://github.com/nette/forms/tree/master/examples] v distribuci, kde najdete další inspiraci. diff --git a/forms/cs/upgrading.texy b/forms/cs/upgrading.texy new file mode 100644 index 0000000000..229f15a5a9 --- /dev/null +++ b/forms/cs/upgrading.texy @@ -0,0 +1,54 @@ +Upgrade +******* + + +Upgrade na verzi 3.3 +==================== + +- automatická ochrana proti CSRF přešla z `isSameSite()` na `isFrom(FetchSite::SameOrigin)` a je přísnější: požadavky přicházející ze subdomén už neprojdou +- díky tomu už není potřeba `addProtection()`, protože automatická ochrana kryje totéž; u nových formulářů ji vynechte a ze stávajících ji můžete smazat + +Proč už tokeny v session nejsou potřeba, vysvětluje článek [Čtvrt století s CSRF |https://blog.nette.org/cs/csrf-konecne-resi-prohlizec]. + + +Upgrade na verzi 3.1 +==================== + +- `getValues()` vrací jen validované prvky; pokud potřebujete hodnoty všech prvků nezávisle na validaci, použijte novou metodu `getUntrustedValues()` +- pole `$values` předávané handlerům `onSuccess` a `onClick` rovněž obsahuje jen validované prvky +- samostatně používané formuláře jsou automaticky chráněny proti CSRF pomocí cookie s příznakem SameSite; odeslání z jiné domény povolíte metodou `allowCrossOrigin()` +- pravidlo `Form::URL` nyní doplňuje chybějící protokol `https` místo `http` +- `Form::addImage()` bylo přejmenováno na `addImageButton()` +- `Checkbox::getSeparatorPrototype()` bylo přejmenováno na `getContainerPrototype()` +- formuláře již nevytvářejí v šabloně proměnnou `$_form` + +Více o těchto změnách v článku [Novinky v Nette Forms 3.1 |https://blog.nette.org/cs/novinky-v-nette-forms-3-1]. + + +Upgrade na verzi 3.0 +==================== + +- všechny formulářové prvky jsou nyní ve výchozím nastavení volitelné (tato změna byla zavedena v Nette 2.4), takže můžete odstranit `setRequired(false)` +- nezapomeňte aktualizovat soubor `netteForms.js` na verzi 3 (`npm install nette-forms`) +- proměnné `ChoiceControl::$checkAllowedValues` a `MultiChoiceControl::$checkAllowedValues` byly nahrazeny metodou `checkDefaultValue()` + + +Upgrade na verzi 2.4 +==================== + +- pokud má prvek nastaveno pravidlo přes `addRule()` (tedy je efektivně povinný), musíte jej také označit jako povinný pomocí `setRequired()`; dále `setRequired(false)` nyní udělá prvek volitelný, čímž lze nahradit větve `addCondition($form::FILLED)` +- validátory `Form::EMAIL`, `URL` a `INTEGER` automaticky mění HTML atribut `type` na `email`, `url`, resp. `number` +- negativní validační pravidla jsou zastaralá; alternativou `~Form::FILLED` je `Form::BLANK` a `~Form::EQUAL` lze nahradit za `Form::NOT_EQUAL` +- interní parametr `do` se nyní u formulářů posílaných přes POST jmenuje `_do`, aby nedocházelo ke kolizi +- interní podtržítkové proměnné jako `$_form` jsou zastaralé +- nezapomeňte aktualizovat `netteForms.js` + + +Upgrade na verzi 2.3 +==================== + +- interní filtrovací metody jako `Nette\Forms\Controls\TextBase::filterFloat` byly odstraněny +- interní validační metody jako `TextBase::validateFloat` byly přesunuty do `Nette\Forms\Validator`, stejně jako `Rules::$defaultMessages` +- tlačítka a skrytá pole se generují bez HTML ID; pokud ID chcete, nastavte ho přes `setHtmlId()` +- položky RadioListu se rovněž generují bez ID; můžete ho zapnout přes `$radioList->generateId = true` +- filtry přidané přes `TextBase::addFilter()` se zpracovávají během validace a filtry nyní můžete přidávat i k podmínkám: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/cs/validation.texy b/forms/cs/validation.texy index e313be57b9..f460ae94d3 100644 --- a/forms/cs/validation.texy +++ b/forms/cs/validation.texy @@ -25,7 +25,7 @@ $form->addPassword('password', 'Heslo:') **Validační pravidla se ověřují pouze v případě, že uživatel prvek vyplnil.** -Nette přichází s celou řadu předdefinovaných pravidel, jejichž názvy jsou konstanty třídy `Nette\Forms\Form`. U všech prvků můžeme použít tyto pravidla: +Nette přichází s celou řadou předdefinovaných pravidel, jejichž názvy jsou konstanty třídy `Nette\Forms\Form`. U všech prvků můžeme použít tato pravidla: | konstanta | popis | typ argumentu |------- @@ -36,7 +36,7 @@ Nette přichází s celou řadu předdefinovaných pravidel, jejichž názvy jso | `NotEqual` | hodnota se nerovná parametru | `mixed` | `IsIn` | hodnota se rovná některé položce v poli | `array` | `IsNotIn` | hodnota se nerovná žádné položce v poli | `array` -| `Valid` | je prvek vyplněn správně? (pro [#podmínky]) | - +| `Valid` | je prvek vyplněn správně? (pouze v [addConditionOn() |#podmínky]) | - Textové vstupy @@ -52,19 +52,19 @@ U prvků `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addIntege | `Pattern` | vyhovuje regulárnímu výrazu | `string` | `PatternInsensitive` | jako `Pattern`, ale nezávislé na velikosti písmen | `string` | `Integer` | celočíselná hodnota | - -| `Numeric` | alias pro `Integer` | - +| `Numeric` | nezáporné celé číslo (pouze číslice) | - | `Float` | číslo | - | `Min` | minimální hodnota číselného prvku | `int\|float` | `Max` | maximální hodnota číselného prvku | `int\|float` | `Range` | hodnota v rozsahu | dvojice `[int\|float, int\|float]` -Validační pravidla `Integer`, `Numeric` a `Float` rovnou převádí hodnotu na integer resp. float. A dále pravidlo `URL` akceptuje i adresu bez schématu (např. `nette.org`) a schéma doplní (`https://nette.org`). Výraz v `Pattern` a `PatternIcase` musí platit pro celou hodnotu, tj. jako by byl obalen znaky `^` a `$`. +Validační pravidla `Integer` a `Float` rovnou převádí hodnotu na integer, resp. float. A dále pravidlo `URL` akceptuje i adresu bez schématu (např. `nette.org`) a schéma doplní (`https://nette.org`). Výraz v `Pattern` a `PatternInsensitive` musí platit pro celou hodnotu, tj. jako by byl obalen znaky `^` a `$`. Počet položek ------------- -U prvků `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` lze použít i následující pravidla pro omezení počtu vybraných položek resp. uploadovaných souborů: +U prvků `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` lze použít i následující pravidla pro omezení počtu vybraných položek, resp. uploadovaných souborů: | `MinLength` | minimální počet | `int` | `MaxLength` | maximální počet | `int` @@ -82,23 +82,23 @@ U prvků `addUpload()`, `addMultiUpload()` lze použít i následující pravidl | `Pattern` | jméno souboru vyhovuje regulárnímu výrazu | `string` | `PatternInsensitive` | jako `Pattern`, ale nezávislé na velikosti písmen | `string` -`MimeType` a `Image` vyžadují PHP rozšíření `fileinfo`. Že je soubor či obrázek požadovaného typu detekují na základě jeho signatury a **neověřují integritu celého souboru.** Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení |http:request#toImage]. +`MimeType` a `Image` vyžadují PHP rozšíření `fileinfo`. To, že je soubor či obrázek požadovaného typu, detekují na základě jeho signatury a **neověřují integritu celého souboru.** Zda není obrázek poškozený, lze zjistit například pokusem o jeho [načtení |http:request#toImage]. Chybové hlášky ============== -Všechny předdefinované pravidla s výjimkou `Pattern` a `PatternInsensitive` mají výchozí chybovou hlášku, takže ji lze vynechat. Nicméně uvedením a formulací všech hlášek na míru uděláte formulář uživatelsky přívětivější. +Všechna předdefinovaná pravidla s výjimkou `Pattern` a `PatternInsensitive` mají výchozí chybovou hlášku, takže ji lze vynechat. Nicméně uvedením a formulací všech hlášek na míru uděláte formulář uživatelsky přívětivější. Změnit výchozí hlášky můžete v [konfiguraci|forms:configuration], úpravou textů v poli `Nette\Forms\Validator::$messages` nebo použitím [translatoru |rendering#Překládání]. V textu chybových hlášek lze používat tyto zástupné řetězce: -| `%d` | nahradí postupně za argumenty pravidla -| `%n$d` | nahradí za n-tý argument pravidla -| `%label` | nahradí za popisku prvku (bez dvojtečky) -| `%name` | nahradí za jméno prvku (např. `name`) -| `%value` | nahradí za uživatelem vloženou hodnotu +| `%d` | nahradí se postupně argumenty pravidla +| `%n$d` | nahradí se n-tým argumentem pravidla +| `%label` | nahradí se popiskem prvku (bez dvojtečky) +| `%name` | nahradí se jménem prvku (např. `name`) +| `%value` | nahradí se hodnotou vloženou uživatelem ```php $form->addText('name', 'Jméno:') @@ -151,6 +151,14 @@ $form->addText(/* ... */) ->addRule(/* ... */); ``` +Prvním argumentem metody `addCondition()` může být i logická hodnota. To se hodí, když je rozhodnutí známé už při sestavování formuláře, například když chceme pravidlo použít jen za určitých okolností: + +```php +$form->addText('nickname') + ->addCondition($isRequired) // hodnota známá při sestavování formuláře + ->setRequired(); +``` + V Nette lze velmi snadno reagovat na splnění či nesplnění podmínky i na straně JavaScriptu pomocí metody `toggle()`, viz [#dynamický JavaScript]. @@ -202,7 +210,7 @@ $form->addInteger('num') ); ``` -Vlastní validační pravidla lze přidávat i do JavaScriptu. Podmínkou je, aby pravidlo byla statická metoda. Její název pro JavaScriptový validátor vznikne spojením názvu třídy bez zpětných lomítek `\`, podtržítka `_` a názvu metody. Např. `App\MyValidators::validateDivisibility` zapíšeme jako `AppMyValidators_validateDivisibility` a přidáme do objektu `Nette.validators`: +Vlastní validační pravidla lze přidávat i do JavaScriptu. Podmínkou je, aby pravidlem byla statická metoda. Její název pro JavaScriptový validátor vznikne spojením názvu třídy bez zpětných lomítek `\`, podtržítka `_` a názvu metody. Např. `App\MyValidators::validateDivisibility` zapíšeme jako `AppMyValidators_validateDivisibility` a přidáme do objektu `Nette.validators`: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -223,11 +231,11 @@ protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('Tato kombinace není možná.'); @@ -260,7 +268,7 @@ Pokud je to možné, doporučujeme připojit chybu přímo k prvku formuláře, $form['date']->addError('Omlouváme se, ale toto datum již je zabrané.'); ``` -Můžete `addError()` volat opakovaně a tak předat formuláři nebo prvku více chybových zpráv. Získáte je pomocí `getErrors()`. +Můžete `addError()` volat opakovaně, a předat tak formuláři nebo prvku více chybových zpráv. Získáte je pomocí `getErrors()`. Pozor, `$form->getErrors()` vrací sumář všech chybových zpráv, i těch, co byly předány přímo jednotlivým prvkům, nejen přímo formuláři. Chybové zprávy předané pouze formuláři získáte přes `$form->getOwnErrors()`. @@ -278,7 +286,7 @@ $form->addText('zip', 'PSČ:') ->addRule($form::Pattern, 'PSČ není ve tvaru pěti číslic', '\d{5}'); ``` -Filtr se začlení mezi validační pravidla a podmínky a tedy záleží na pořadí metod, tj. filtr a pravidlo se volají v takovém pořadí, jako je pořadí metod `addFilter()` a `addRule()`. +Filtr se začlení mezi validační pravidla a podmínky, a záleží tedy na pořadí metod, tj. filtr a pravidlo se volají v takovém pořadí, jako je pořadí metod `addFilter()` a `addRule()`. JavaScriptová validace @@ -320,11 +328,17 @@ import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Klientskou validaci lze úplně vypnout přidáním atributu `novalidate` k formuláři. Skript `netteForms.js` pak formulář při odeslání nevaliduje a validace probíhá jen na serveru: + +```php +$form->setHtmlAttribute('novalidate'); +``` + Dynamický JavaScript ==================== -Chcete zobrazit políčka pro zadání adresy pouze pokud uživatel zvolí zboží zaslat poštou? Žádný problém. Klíčem je dvojice metod `addCondition()` & `toggle()`: +Chcete zobrazit políčka pro zadání adresy pouze tehdy, pokud uživatel zvolí zboží zaslat poštou? Žádný problém. Klíčem je dvojice metod `addCondition()` a `toggle()`: ```php $form->addCheckbox('send_it') @@ -334,7 +348,7 @@ $form->addCheckbox('send_it') Tento kód říká, že když je podmínka splněná, tedy když je zaškrtnutý checkbox, bude viditelný HTML element `#address-container`. A obráceně. Prvky formuláře s adresou příjemce tak umístíme do kontejneru s tímto ID a při kliknutí na checkbox se skryjí nebo zobrazí. To zajišťuje skript `netteForms.js`. -Jako argument metody `toggle()` je možné předat libovolný selektor. Z historických důvodů se alfanumerický řetězec bez dalších speciálních znaků chápe jako ID prvku, tedy stejně, jako by mu předcházel znak `#`. Druhý nepovinný parametr umožňuje obrátit chování, tj. pokud bychom použili `toggle('#address-container', false)`, element by se naopak zobrazil pouze tehdy, pokud by checkbox zaškrtnutý nebyl. +Jako argument metody `toggle()` je možné předat libovolný selektor. Z historických důvodů se řetězec, který začíná písmenem, číslicí nebo podtržítkem a obsahuje jen písmena, číslice, podtržítka, pomlčky, tečky a dvojtečky, chápe jako ID prvku, tedy stejně, jako by mu předcházel znak `#`. Druhý nepovinný parametr umožňuje obrátit chování, tj. pokud bychom použili `toggle('#address-container', false)`, element by se naopak zobrazil pouze tehdy, pokud by checkbox zaškrtnutý nebyl. Výchozí implementace v JavaScriptu mění elementům property `hidden`. Chování však můžeme snadno změnit, například přidat animaci. Stačí v JavaScriptu přepsat metodu `Nette.toggle` vlastním řešením: @@ -350,7 +364,7 @@ Nette.toggle = (selector, visible, srcElement, event) => { Vypnutí validace ================ -Někdy se může hodit validaci vypnout. Pokud stisknutí odesílacího tlačítka nemá provádět validaci (vhodné pro tlačítka *Cancel* nebo *Preview*), vypneme ji metodou `$submit->setValidationScope([])`. Pokud má provádět validaci jen částečnou, můžeme určit které pole nebo formulářové kontejnery se mají validovat. +Někdy se může hodit validaci vypnout. Pokud stisknutí odesílacího tlačítka nemá provádět validaci (vhodné pro tlačítka *Cancel* nebo *Preview*), vypneme ji metodou `$submit->setValidationScope([])`. Pokud má provádět validaci jen částečnou, můžeme určit, která pole nebo formulářové kontejnery se mají validovat. ```php $form->addText('name') @@ -373,4 +387,6 @@ $form->addSubmit('send5') ->setValidationScope([$form['details']]); // Validuje kontejner details ``` -`setValidationScope` neovlivní [#událost onValidate] u formuláře, která bude zavolána vždy. Událost `onValidate` u kontejneru bude vyvolána pouze pokud je tento kontejner označen pro částečnou validaci. +`setValidationScope` neovlivní [#událost onValidate] u formuláře, která bude zavolána vždy. Událost `onValidate` u kontejneru bude vyvolána pouze tehdy, pokud je tento kontejner označen pro částečnou validaci. + +Částečná validace ovlivňuje i hodnoty vrácené metodou `getValues()`: výsledek obsahuje pouze hodnoty prvků, které spadají do rozsahu validace. Hodnoty prvků mimo tento rozsah jsou z výsledku vynechány. diff --git a/forms/de/@home.texy b/forms/de/@home.texy index bdc4d7fb9a..b655294d9b 100644 --- a/forms/de/@home.texy +++ b/forms/de/@home.texy @@ -3,29 +3,29 @@ Nette Forms <div class=perex> -Nette Forms haben die Erstellung von Webformularen revolutioniert. Plötzlich genügten ein paar verständliche Codezeilen, und Sie hatten ein fertiges Formular inklusive Rendering, JavaScript- und serverseitiger Validierung und dazu noch erstklassig gesichert. Wir zeigen Ihnen, wie Sie +Nette Forms haben das Erstellen von Webformularen revolutioniert. Plötzlich genügten ein paar übersichtliche Zeilen Code für ein vollständiges Formular samt Rendering, Validierung in JavaScript und auf dem Server sowie erstklassiger Sicherheit. Wir zeigen Ihnen, wie Sie: - benutzerfreundliche Formulare erstellen -- gesendete Daten validieren -- Elemente genau nach Bedarf rendern +- die gesendeten Daten validieren +- die Elemente genau so rendern, wie Sie es brauchen </div> -Durch die Verwendung von Nette Forms vermeiden Sie eine ganze Reihe von Routineaufgaben, wie das Schreiben von Validierungen (noch dazu doppelt, auf Server- und Clientseite), minimieren die Wahrscheinlichkeit von Fehlern und Sicherheitslücken. +Mit Nette Forms sparen Sie sich viele Routinearbeiten, etwa das Schreiben der Validierungslogik (sowohl auf der Serverseite als auch auf der Clientseite), und senken die Wahrscheinlichkeit von Fehlern und Sicherheitslücken. -Formulare können entweder als Teil einer Nette-Anwendung (also in Presentern) oder völlig eigenständig verwendet werden. Da sich die Verwendung in beiden Fällen leicht unterscheidet, haben wir zwei Anleitungen für Sie vorbereitet: +Formulare können Sie entweder als Teil einer Nette Application (also in Presentern) oder völlig eigenständig verwenden. Weil sich die Verwendung in beiden Fällen leicht unterscheidet, haben wir für Sie getrennte Anleitungen vorbereitet: <div class="wiki-buttons"> <div> "Formulare in Presentern .[wiki-button]":in-presenter </div> -<div> "Eigenständige Formulare .[wiki-button]":standalone </div> +<div> "Formulare eigenständig .[wiki-button]":standalone </div> </div> Installation ------------ -Sie können die Bibliothek mit dem Werkzeug [Composer|best-practices:composer] herunterladen und installieren: +Laden Sie das Paket mit [Composer|best-practices:composer] herunter und installieren Sie es: ```shell composer require nette/forms diff --git a/forms/de/@left-menu.texy b/forms/de/@left-menu.texy index c988dac7e9..c7be17c259 100644 --- a/forms/de/@left-menu.texy +++ b/forms/de/@left-menu.texy @@ -1,14 +1,16 @@ Nette Forms *********** -- [Einführung |@home] +- [Übersicht |@home] - [Formulare in Presentern|in-presenter] - [Formulare eigenständig|standalone] - [Formularelemente |controls] - [Validierung |validation] - [Rendering |rendering] -- [Konfiguration |configuration] +- [Eigene Formularelemente |custom-controls] +- [Konfiguration|configuration] +- [Upgrade|upgrading] -Weitere Lektüre -*************** -- [Anleitungen und Verfahren |best-practices:] +Weiterführende Lektüre +********************** +- [Best Practices |best-practices:] diff --git a/forms/de/configuration.texy b/forms/de/configuration.texy index 19b8d55c2e..c24574979e 100644 --- a/forms/de/configuration.texy +++ b/forms/de/configuration.texy @@ -2,7 +2,7 @@ Konfiguration von Formularen **************************** .[perex] -In der Konfiguration können die standardmäßigen [Fehlermeldungen von Formularen|validation] geändert werden. +In der Konfiguration lassen sich die standardmäßigen [Fehlermeldungen der Formulare|validation] ändern. ```neon forms: @@ -17,6 +17,7 @@ forms: 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.' @@ -45,6 +46,7 @@ forms: Email: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.' URL: 'Bitte geben Sie eine gültige URL ein.' Integer: 'Bitte geben Sie eine gültige ganze Zahl ein.' + Numeric: 'Bitte geben Sie eine nicht negative ganze Zahl ein.' Float: 'Bitte geben Sie eine gültige Zahl ein.' Min: 'Bitte geben Sie einen Wert größer oder gleich %d ein.' Max: 'Bitte geben Sie einen Wert kleiner oder gleich %d ein.' @@ -52,10 +54,10 @@ forms: MaxFileSize: 'Die Größe der hochgeladenen Datei darf maximal %d Bytes betragen.' MaxPostSize: 'Die hochgeladenen Daten überschreiten das Limit von %d Bytes.' MimeType: 'Die hochgeladene Datei hat nicht das erwartete Format.' - Image: 'Die hochgeladene Datei muss ein Bild im Format JPEG, GIF, PNG, WebP oder AVIF sein.' + Image: 'Die hochgeladene Datei muss ein Bild im Format JPEG, GIF, PNG oder WebP sein.' Nette\Forms\Controls\SelectBox::Valid: 'Bitte wählen Sie eine gültige Option aus.' Nette\Forms\Controls\UploadControl::Valid: 'Beim Hochladen der Datei ist ein Fehler aufgetreten.' Nette\Forms\Controls\CsrfProtection::Protection: 'Ihre Sitzung ist abgelaufen. Bitte kehren Sie zur Startseite zurück und versuchen Sie es erneut.' ``` -Wenn Sie nicht das gesamte Framework und somit auch keine Konfigurationsdateien verwenden, können Sie die standardmäßigen Fehlermeldungen direkt im Array `Nette\Forms\Validator::$messages` ändern. +Wenn Sie nicht das ganze Framework und damit auch keine Konfigurationsdateien verwenden, können Sie die standardmäßigen Fehlermeldungen direkt im Array `Nette\Forms\Validator::$messages` ändern. diff --git a/forms/de/controls.texy b/forms/de/controls.texy index d961df40fd..2213022b1b 100644 --- a/forms/de/controls.texy +++ b/forms/de/controls.texy @@ -2,13 +2,13 @@ Formularelemente **************** .[perex] -Übersicht über die standardmäßigen Formularelemente. +Übersicht der Standard-Formularelemente. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -Fügt ein einzeiliges Texteingabefeld hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Wenn der Benutzer das Feld nicht ausfüllt, gibt es eine leere Zeichenkette `''` zurück, oder mit `setNullable()` kann festgelegt werden, dass `null` zurückgegeben wird. +Fügt ein einzeiliges Textfeld hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Füllt der Benutzer das Feld nicht aus, gibt es einen leeren String `''` zurück; mit `setNullable()` sorgen Sie dafür, dass es stattdessen `null` zurückgibt. ```php $form->addText('name', 'Name:') @@ -16,84 +16,84 @@ $form->addText('name', 'Name:') ->setNullable(); ``` -Validiert automatisch UTF-8, schneidet führende und nachfolgende Leerzeichen ab und entfernt Zeilenumbrüche, die ein Angreifer senden könnte. +Es validiert automatisch UTF-8, schneidet Leerraum am Anfang und Ende ab und entfernt Zeilenumbrüche, die ein Angreifer senden könnte. -Die maximale Länge kann mit `setMaxLength()` begrenzt werden. Der vom Benutzer eingegebene Wert kann mit [addFilter() |validation#Anpassung der Eingabe] geändert werden. +Die maximale Länge lässt sich mit `setMaxLength()` begrenzen. Die Methode [addFilter() |validation#Eingaben verändern] erlaubt es, den vom Benutzer eingegebenen Wert zu verändern. -Mit `setHtmlType()` kann der visuelle Charakter des Textfeldes auf Typen wie `search`, `tel` oder `url` geändert werden, siehe [Spezifikation|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Beachten Sie, dass die Typänderung nur visuell ist und keine Validierungsfunktion ersetzt. Für den Typ `url` ist es ratsam, eine spezifische Validierungs[regel URL |validation#Texteingaben] hinzuzufügen. +Mit `setHtmlType()` können Sie das optische Erscheinungsbild des Textfelds auf Typen wie `search`, `tel` oder `url` ändern, wie sie die [Spezifikation|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types] festlegt. Denken Sie daran, dass die Änderung des Typs rein optisch ist und die Validierung nicht ersetzt. Beim Typ `url` empfiehlt es sich, eine passende [Validierungsregel für URLs |validation#Texteingaben] zu ergänzen. .[note] -Für andere Eingabetypen wie `number`, `range`, `email`, `date`, `datetime-local`, `time` und `color` verwenden Sie spezialisierte Methoden wie [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] und [#addColor], die die serverseitige Validierung sicherstellen. Die Typen `month` und `week` werden derzeit noch nicht in allen Browsern vollständig unterstützt. +Für andere Typen von Eingabefeldern wie `number`, `range`, `email`, `date`, `datetime-local`, `time` und `color` verwenden Sie die spezialisierten Methoden [#addInteger()], [#addFloat()], [#addEmail()], [#addDate()], [#addTime()], [#addDateTime()] und [#addColor()], die eine Validierung auf der Serverseite mitbringen. Die Typen `month` und `week` unterstützen noch nicht alle Browser vollständig. -Dem Element kann ein sogenannter Empty-Value zugewiesen werden, was so etwas wie ein Standardwert ist, aber wenn der Benutzer ihn nicht ändert, gibt das Element eine leere Zeichenkette oder `null` zurück. +Für das Element lässt sich ein "leerer Wert" setzen. Er verhält sich ein wenig wie ein Standardwert, aber wenn der Benutzer ihn nicht ändert, gibt das Element einen leeren String oder `null` zurück. ```php $form->addText('phone', 'Telefon:') ->setHtmlType('tel') - ->setEmptyValue('+49'); // Beispiel für DE + ->setEmptyValue('+420'); ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -Fügt ein Feld zur Eingabe von mehrzeiligem Text hinzu (Klasse [TextArea |api:Nette\Forms\Controls\TextArea]). Wenn der Benutzer das Feld nicht ausfüllt, gibt es eine leere Zeichenkette `''` zurück, oder mit `setNullable()` kann festgelegt werden, dass `null` zurückgegeben wird. +Fügt ein mehrzeiliges Textfeld hinzu (Klasse [TextArea |api:Nette\Forms\Controls\TextArea]). Füllt der Benutzer das Feld nicht aus, gibt es einen leeren String `''` zurück; mit `setNullable()` sorgen Sie dafür, dass es stattdessen `null` zurückgibt. ```php -$form->addTextArea('note', 'Anmerkung:') - ->addRule($form::MaxLength, 'Anmerkung ist zu lang', 10000); +$form->addTextArea('note', 'Notiz:') + ->addRule($form::MaxLength, 'Ihre Notiz ist viel zu lang', 10000); ``` -Validiert automatisch UTF-8 und normalisiert Zeilentrenner auf `\n`. Im Gegensatz zum einzeiligen Eingabefeld erfolgt kein Abschneiden von Leerzeichen. +Es validiert automatisch UTF-8 und vereinheitlicht die Zeilenenden zu `\n`. Anders als beim einzeiligen Feld wird kein Leerraum abgeschnitten. -Die maximale Länge kann mit `setMaxLength()` begrenzt werden. Der vom Benutzer eingegebene Wert kann mit [addFilter() |validation#Anpassung der Eingabe] geändert werden. Mit `setEmptyValue()` kann ein sogenannter Empty-Value festgelegt werden. +Die maximale Länge lässt sich mit `setMaxLength()` begrenzen. Die Methode [addFilter() |validation#Eingaben verändern] erlaubt es, den vom Benutzer eingegebenen Wert zu verändern. Einen leeren Wert setzen Sie über `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Fügt ein Feld zur Eingabe einer ganzen Zahl hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Gibt entweder einen Integer oder `null` zurück, wenn der Benutzer nichts eingibt. +Fügt ein Feld zur Eingabe einer ganzen Zahl hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Gibt entweder eine ganze Zahl zurück oder `null`, wenn der Benutzer nichts eingibt. ```php $form->addInteger('year', 'Jahr:') - ->addRule($form::Range, 'Das Jahr muss im Bereich von %d bis %d liegen.', [1900, 2023]); + ->addRule($form::Range, 'Das Jahr muss zwischen %d und %d liegen.', [1900, 2023]); ``` -Das Element wird als `<input type="number">` gerendert. Mit der Methode `setHtmlType()` kann der Typ auf `range` geändert werden, um eine Darstellung als Schieberegler zu erhalten, oder auf `text`, wenn Sie ein Standard-Textfeld ohne das spezielle Verhalten des Typs `number` bevorzugen. +Das Element wird als `<input type="number">` gerendert. Über die Methode `setHtmlType()` können Sie den Typ auf `range` ändern, um es als Schieberegler darzustellen, oder auf `text`, wenn Sie ein gewöhnliches Textfeld ohne das besondere Verhalten des Typs `number` vorziehen. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -Fügt ein Feld zur Eingabe einer Dezimalzahl hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Gibt entweder einen Float oder `null` zurück, wenn der Benutzer nichts eingibt. +Fügt ein Feld zur Eingabe einer Fließkommazahl hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Gibt entweder eine Fließkommazahl zurück oder `null`, wenn der Benutzer nichts eingibt. ```php -$form->addFloat('level', 'Level:') +$form->addFloat('level', 'Stufe:') ->setDefaultValue(0) - ->addRule($form::Range, 'Das Level muss im Bereich von %d bis %d liegen.', [0, 100]); + ->addRule($form::Range, 'Die Stufe muss zwischen %d und %d liegen.', [0, 100]); ``` -Das Element wird als `<input type="number">` gerendert. Mit der Methode `setHtmlType()` kann der Typ auf `range` geändert werden, um eine Darstellung als Schieberegler zu erhalten, oder auf `text`, wenn Sie ein Standard-Textfeld ohne das spezielle Verhalten des Typs `number` bevorzugen. +Das Element wird als `<input type="number">` gerendert. Über die Methode `setHtmlType()` können Sie den Typ auf `range` ändern, um es als Schieberegler darzustellen, oder auf `text`, wenn Sie ein gewöhnliches Textfeld ohne das besondere Verhalten des Typs `number` vorziehen. -Nette und der Chrome-Browser akzeptieren sowohl Komma als auch Punkt als Dezimaltrennzeichen. Damit diese Funktionalität auch in Firefox verfügbar ist, wird empfohlen, das Attribut `lang` entweder für das betreffende Element oder für die gesamte Seite zu setzen, beispielsweise `<html lang="de">`. +Nette und der Browser Chrome akzeptieren als Dezimaltrennzeichen sowohl das Komma als auch den Punkt. Damit das auch in Firefox funktioniert, empfiehlt es sich, das Attribut `lang` entweder für das jeweilige Element oder für die ganze Seite zu setzen, zum Beispiel `<html lang="de">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -Fügt ein Feld zur Eingabe einer E-Mail-Adresse hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Wenn der Benutzer das Feld nicht ausfüllt, gibt es eine leere Zeichenkette `''` zurück, oder mit `setNullable()` kann festgelegt werden, dass `null` zurückgegeben wird. +Fügt ein Feld zur Eingabe einer E-Mail-Adresse hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). Füllt der Benutzer das Feld nicht aus, gibt es einen leeren String `''` zurück; mit `setNullable()` sorgen Sie dafür, dass es stattdessen `null` zurückgibt. ```php $form->addEmail('email', 'E-Mail:'); ``` -Überprüft, ob der Wert eine gültige E-Mail-Adresse ist. Es wird nicht überprüft, ob die Domain tatsächlich existiert, es wird nur die Syntax überprüft. Validiert automatisch UTF-8, schneidet führende und nachfolgende Leerzeichen ab. +Es prüft, ob der Wert eine gültige E-Mail-Adresse ist. Ob die Domain tatsächlich existiert, wird nicht geprüft, sondern nur die Syntax. Es validiert automatisch UTF-8 und schneidet Leerraum am Anfang und Ende ab. -Die maximale Länge kann mit `setMaxLength()` begrenzt werden. Der vom Benutzer eingegebene Wert kann mit [addFilter() |validation#Anpassung der Eingabe] geändert werden. Mit `setEmptyValue()` kann ein sogenannter Empty-Value festgelegt werden. +Die maximale Länge lässt sich mit `setMaxLength()` begrenzen. Die Methode [addFilter() |validation#Eingaben verändern] erlaubt es, den vom Benutzer eingegebenen Wert zu verändern. Einen leeren Wert setzen Sie über `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== Fügt ein Feld zur Eingabe eines Passworts hinzu (Klasse [TextInput |api:Nette\Forms\Controls\TextInput]). @@ -101,27 +101,27 @@ Fügt ein Feld zur Eingabe eines Passworts hinzu (Klasse [TextInput |api:Nette\F $form->addPassword('password', 'Passwort:') ->setRequired() ->addRule($form::MinLength, 'Das Passwort muss mindestens %d Zeichen lang sein', 8) - ->addRule($form::Pattern, 'Muss eine Ziffer enthalten', '.*[0-9].*'); + ->addRule($form::Pattern, 'Das Passwort muss eine Ziffer enthalten', '.*[0-9].*'); ``` -Beim erneuten Anzeigen des Formulars ist das Feld leer. Validiert automatisch UTF-8, schneidet führende und nachfolgende Leerzeichen ab und entfernt Zeilenumbrüche, die ein Angreifer senden könnte. +Wird das Formular erneut angezeigt, ist das Feld leer. Es validiert automatisch UTF-8, schneidet Leerraum am Anfang und Ende ab und entfernt Zeilenumbrüche, die ein Angreifer senden könnte. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -Fügt ein Kontrollkästchen hinzu (Klasse [Checkbox |api:Nette\Forms\Controls\Checkbox]). Gibt entweder `true` oder `false` zurück, je nachdem, ob es aktiviert ist. +Fügt eine Checkbox hinzu (Klasse [Checkbox |api:Nette\Forms\Controls\Checkbox]). Gibt `true` oder `false` zurück, je nachdem, ob sie angehakt ist. ```php $form->addCheckbox('agree', 'Ich stimme den Bedingungen zu') - ->setRequired('Sie müssen den Bedingungen zustimmen'); + ->setRequired('Sie müssen unseren Bedingungen zustimmen'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Fügt Kontrollkästchen zur Auswahl mehrerer Elemente hinzu (Klasse [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Gibt ein Array der Schlüssel der ausgewählten Elemente zurück. Die Methode `getSelectedItems()` gibt die Werte anstelle der Schlüssel zurück. +Fügt eine Liste von Checkboxen zur Auswahl mehrerer Elemente hinzu (Klasse [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Gibt ein Array der Schlüssel der ausgewählten Elemente zurück. Die Methode `getSelectedItems()` gibt die ausgewählten Elemente als Schlüssel-Wert-Paare zurück. ```php $form->addCheckboxList('colors', 'Farben:', [ @@ -131,147 +131,148 @@ $form->addCheckboxList('colors', 'Farben:', [ ]); ``` -Das Array der angebotenen Elemente übergeben wir als dritten Parameter oder mit der Methode `setItems()`. +Das Array der angebotenen Elemente übergeben Sie als dritten Parameter oder über die Methode `setItems()`. Übergeben Sie `setItems()` als zweites Argument `false`, werden die Werte zugleich als Schlüssel verwendet. -Mit `setDisabled(['r', 'g'])` können einzelne Elemente deaktiviert werden. +Mit `setDisabled(['r', 'g'])` deaktivieren Sie einzelne Elemente. -Das Element überprüft automatisch, dass keine Manipulation stattgefunden hat und dass die ausgewählten Elemente tatsächlich zu den angebotenen gehören und nicht deaktiviert wurden. Mit der Methode `getRawValue()` können die gesendeten Elemente ohne diese wichtige Überprüfung abgerufen werden. +Das Element prüft automatisch, dass keine Fälschung stattgefunden hat und dass die ausgewählten Elemente tatsächlich zu den angebotenen gehören und nicht deaktiviert waren. Über die Methode `getRawValue()` lassen sich die gesendeten Elemente ohne diese wichtige Prüfung holen. -Bei der Einstellung der standardmäßig ausgewählten Elemente wird ebenfalls überprüft, ob es sich um angebotene Elemente handelt, andernfalls wird eine Ausnahme ausgelöst. Diese Prüfung kann mit `checkDefaultValue(false)` deaktiviert werden. +Beim Setzen der standardmäßig ausgewählten Elemente wird ebenfalls geprüft, dass sie zu den angebotenen gehören, sonst wirft es eine Exception. Diese Prüfung lässt sich mit `checkDefaultValue(false)` abschalten. -Wenn Sie das Formular mit der Methode `GET` senden, können Sie eine kompaktere Datenübertragungsmethode wählen, die die Größe des Query-Strings spart. Sie wird durch Setzen des HTML-Attributs des Formulars aktiviert: +Wenn Sie das Formular mit der Methode `GET` absenden, können Sie eine kompaktere Art der Datenübertragung wählen, die Platz im Query-String spart. Sie aktivieren sie, indem Sie am Formular ein HTML-Attribut setzen: ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ -Fügt Optionsschaltflächen hinzu (Klasse [RadioList |api:Nette\Forms\Controls\RadioList]). Gibt den Schlüssel des ausgewählten Elements zurück oder `null`, wenn der Benutzer nichts ausgewählt hat. Die Methode `getSelectedItem()` gibt den Wert anstelle des Schlüssels zurück. +Fügt Radiobuttons hinzu (Klasse [RadioList |api:Nette\Forms\Controls\RadioList]). Gibt den Schlüssel des ausgewählten Elements zurück oder `null`, wenn der Benutzer nichts ausgewählt hat. Die Methode `getSelectedItem()` gibt statt des Schlüssels den Wert zurück. ```php $sex = [ 'm' => 'männlich', 'f' => 'weiblich', + 'o' => 'anderes', ]; $form->addRadioList('gender', 'Geschlecht:', $sex); ``` -Das Array der angebotenen Elemente übergeben wir als dritten Parameter oder mit der Methode `setItems()`. +Das Array der angebotenen Elemente übergeben Sie als dritten Parameter oder über die Methode `setItems()`. -Mit `setDisabled(['m', 'f'])` können einzelne Elemente deaktiviert werden. +Mit `setDisabled(['m'])` deaktivieren Sie einzelne Elemente. -Das Element überprüft automatisch, dass keine Manipulation stattgefunden hat und dass das ausgewählte Element tatsächlich zu den angebotenen gehört und nicht deaktiviert wurde. Mit der Methode `getRawValue()` kann das gesendete Element ohne diese wichtige Überprüfung abgerufen werden. +Das Element prüft automatisch, dass keine Fälschung stattgefunden hat und dass das ausgewählte Element tatsächlich zu den angebotenen gehört und nicht deaktiviert war. Über die Methode `getRawValue()` lässt sich das gesendete Element ohne diese wichtige Prüfung holen. -Bei der Einstellung des standardmäßig ausgewählten Elements wird ebenfalls überprüft, ob es sich um ein angebotenes Element handelt, andernfalls wird eine Ausnahme ausgelöst. Diese Prüfung kann mit `checkDefaultValue(false)` deaktiviert werden. +Beim Setzen des standardmäßig ausgewählten Elements wird ebenfalls geprüft, dass es zu den angebotenen gehört, sonst wirft es eine Exception. Diese Prüfung lässt sich mit `checkDefaultValue(false)` abschalten. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -Fügt eine Select-Box hinzu (Klasse [SelectBox |api:Nette\Forms\Controls\SelectBox]). Gibt den Schlüssel des ausgewählten Elements zurück oder `null`, wenn der Benutzer nichts ausgewählt hat. Die Methode `getSelectedItem()` gibt den Wert anstelle des Schlüssels zurück. +Fügt eine Select-Box hinzu (Klasse [SelectBox |api:Nette\Forms\Controls\SelectBox]). Gibt den Schlüssel des ausgewählten Elements zurück oder `null`, wenn der Benutzer nichts ausgewählt hat. Die Methode `getSelectedItem()` gibt statt des Schlüssels den Wert zurück. ```php $countries = [ - 'DE' => 'Deutschland', - 'AT' => 'Österreich', - 'CH' => 'Schweiz', + 'CZ' => 'Tschechien', + 'SK' => 'Slowakei', + 'GB' => 'Großbritannien', ]; $form->addSelect('country', 'Land:', $countries) - ->setDefaultValue('AT'); + ->setDefaultValue('SK'); ``` -Das Array der angebotenen Elemente übergeben wir als dritten Parameter oder mit der Methode `setItems()`. Die Elemente können auch ein zweidimensionales Array sein (für `<optgroup>`): +Das Array der angebotenen Elemente übergeben Sie als dritten Parameter oder über die Methode `setItems()`. Die Elemente können auch ein zweidimensionales Array sein (das Optgroups darstellt): ```php $countries = [ - 'Europa' => [ - 'DE' => 'Deutschland', - 'AT' => 'Österreich', - 'CH' => 'Schweiz', + 'Europe' => [ + 'CZ' => 'Tschechien', + 'SK' => 'Slowakei', + 'GB' => 'Großbritannien', ], 'CA' => 'Kanada', 'US' => 'USA', - '?' => 'andere', + '?' => 'anderes', ]; ``` -Bei Select-Boxen hat das erste Element oft eine besondere Bedeutung, es dient als Aufforderung zur Aktion. Zum Hinzufügen eines solchen Elements dient die Methode `setPrompt()`. +In Select-Boxen hat das erste Element oft eine besondere Bedeutung und dient als Aufforderung zum Handeln. Über die Methode `setPrompt()` fügen Sie ein solches Element hinzu. ```php $form->addSelect('country', 'Land:', $countries) ->setPrompt('Wählen Sie ein Land'); ``` -Mit `setDisabled(['DE', 'AT'])` können einzelne Elemente deaktiviert werden. +Mit `setDisabled(['CZ', 'SK'])` deaktivieren Sie einzelne Elemente. -Das Element überprüft automatisch, dass keine Manipulation stattgefunden hat und dass das ausgewählte Element tatsächlich zu den angebotenen gehört und nicht deaktiviert wurde. Mit der Methode `getRawValue()` kann das gesendete Element ohne diese wichtige Überprüfung abgerufen werden. +Das Element prüft automatisch, dass keine Fälschung stattgefunden hat und dass das ausgewählte Element tatsächlich zu den angebotenen gehört und nicht deaktiviert war. Über die Methode `getRawValue()` lässt sich das gesendete Element ohne diese wichtige Prüfung holen. -Bei der Einstellung des standardmäßig ausgewählten Elements wird ebenfalls überprüft, ob es sich um ein angebotenes Element handelt, andernfalls wird eine Ausnahme ausgelöst. Diese Prüfung kann mit `checkDefaultValue(false)` deaktiviert werden. +Beim Setzen des standardmäßig ausgewählten Elements wird ebenfalls geprüft, dass es zu den angebotenen gehört, sonst wirft es eine Exception. Diese Prüfung lässt sich mit `checkDefaultValue(false)` abschalten. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Fügt eine Select-Box zur Auswahl mehrerer Elemente hinzu (Klasse [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Gibt ein Array der Schlüssel der ausgewählten Elemente zurück. Die Methode `getSelectedItems()` gibt die Werte anstelle der Schlüssel zurück. +Fügt eine Select-Box zur Auswahl mehrerer Elemente hinzu (Klasse [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Gibt ein Array der Schlüssel der ausgewählten Elemente zurück. Die Methode `getSelectedItems()` gibt die ausgewählten Elemente als Schlüssel-Wert-Paare zurück. ```php $form->addMultiSelect('countries', 'Länder:', $countries); ``` -Das Array der angebotenen Elemente übergeben wir als dritten Parameter oder mit der Methode `setItems()`. Die Elemente können auch ein zweidimensionales Array sein (für `<optgroup>`). +Das Array der angebotenen Elemente übergeben Sie als dritten Parameter oder über die Methode `setItems()`. Die Elemente können auch ein zweidimensionales Array sein. -Mit `setDisabled(['DE', 'AT'])` können einzelne Elemente deaktiviert werden. +Mit `setDisabled(['CZ', 'SK'])` deaktivieren Sie einzelne Elemente. -Das Element überprüft automatisch, dass keine Manipulation stattgefunden hat und dass die ausgewählten Elemente tatsächlich zu den angebotenen gehören und nicht deaktiviert wurden. Mit der Methode `getRawValue()` können die gesendeten Elemente ohne diese wichtige Überprüfung abgerufen werden. +Das Element prüft automatisch, dass keine Fälschung stattgefunden hat und dass die ausgewählten Elemente tatsächlich zu den angebotenen gehören und nicht deaktiviert waren. Über die Methode `getRawValue()` lassen sich die gesendeten Elemente ohne diese wichtige Prüfung holen. -Bei der Einstellung der standardmäßig ausgewählten Elemente wird ebenfalls überprüft, ob es sich um angebotene Elemente handelt, andernfalls wird eine Ausnahme ausgelöst. Diese Prüfung kann mit `checkDefaultValue(false)` deaktiviert werden. +Beim Setzen der standardmäßig ausgewählten Elemente wird ebenfalls geprüft, dass sie zu den angebotenen gehören, sonst wirft es eine Exception. Diese Prüfung lässt sich mit `checkDefaultValue(false)` abschalten. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Fügt ein Feld zum Hochladen einer Datei hinzu (Klasse [UploadControl |api:Nette\Forms\Controls\UploadControl]). Gibt ein [FileUpload |http:request#FileUpload]-Objekt zurück, auch wenn der Benutzer keine Datei gesendet hat, was mit der Methode `FileUpload::hasFile()` überprüft werden kann. +Fügt ein Feld zum Hochladen einer Datei hinzu (Klasse [UploadControl |api:Nette\Forms\Controls\UploadControl]). Gibt ein Objekt [FileUpload |http:request#FileUpload] zurück, auch wenn der Benutzer keine Datei hochgeladen hat, was sich über die Methode `FileUpload::hasFile()` prüfen lässt. Mit `setNullable()` sorgen Sie dafür, dass das Element `null` statt eines `FileUpload`-Objekts zurückgibt, wenn keine Datei hochgeladen wurde. ```php $form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Avatar muss JPEG, PNG, GIF, WebP oder AVIF sein.') - ->addRule($form::MaxFileSize, 'Maximale Größe ist 1 MB.', 1024 * 1024); // 1 MB in Bytes + ->addRule($form::Image, 'Der Avatar muss JPEG, PNG, GIF, WebP oder AVIF sein.') + ->addRule($form::MaxFileSize, 'Die maximale Größe beträgt 1 MB.', 1024 * 1024); ``` -Wenn die Datei nicht korrekt hochgeladen werden kann, wird das Formular nicht erfolgreich gesendet und ein Fehler angezeigt. D.h. bei erfolgreichem Senden muss die Methode `FileUpload::isOk()` nicht überprüft werden. +Lässt sich die Datei nicht korrekt hochladen, wird das Formular nicht erfolgreich abgesendet und ein Fehler angezeigt. Nach erfolgreichem Absenden muss die Methode `FileUpload::isOk()` also nicht geprüft werden. -Vertrauen Sie niemals dem ursprünglichen Dateinamen, der von der Methode `FileUpload::getName()` zurückgegeben wird, der Client könnte einen schädlichen Dateinamen gesendet haben, um Ihre Anwendung zu beschädigen oder zu hacken. +Vertrauen Sie niemals dem ursprünglichen Dateinamen, den die Methode `FileUpload::getName()` zurückgibt; der Client könnte einen bösartigen Dateinamen gesendet haben, um Ihre Anwendung zu beschädigen oder zu kapern. -Die Regeln `MimeType` und `Image` erkennen den erforderlichen Typ anhand der Dateisignatur und überprüfen nicht die Integrität der Datei. Ob ein Bild beschädigt ist, kann beispielsweise durch den Versuch, es [zu laden |http:request#toImage], festgestellt werden. +Die Regeln `MimeType` und `Image` erkennen den verlangten Typ anhand der Signatur der Datei und prüfen ihre Unversehrtheit nicht. Ob ein Bild beschädigt ist, lässt sich zum Beispiel durch den Versuch feststellen, es zu [laden |http:request#toImage()]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Fügt ein Feld zum gleichzeitigen Hochladen mehrerer Dateien hinzu (Klasse [UploadControl |api:Nette\Forms\Controls\UploadControl]). Gibt ein Array von [FileUpload |http:request#FileUpload]-Objekten zurück. Die Methode `FileUpload::hasFile()` gibt bei jedem von ihnen `true` zurück, wenn eine Datei hochgeladen wurde. +Fügt ein Feld zum Hochladen mehrerer Dateien auf einmal hinzu (Klasse [UploadControl |api:Nette\Forms\Controls\UploadControl]). Gibt ein Array von [FileUpload |http:request#FileUpload]-Objekten zurück. Die Methode `FileUpload::hasFile()` gibt für jedes von ihnen `true` zurück. ```php $form->addMultiUpload('files', 'Dateien:') - ->addRule($form::MaxLength, 'Maximal können %d Dateien hochgeladen werden', 10); + ->addRule($form::MaxLength, 'Es lassen sich höchstens %d Dateien hochladen.', 10); ``` -Wenn eine der Dateien nicht korrekt hochgeladen werden kann, wird das Formular nicht erfolgreich gesendet und ein Fehler angezeigt. D.h. bei erfolgreichem Senden muss die Methode `FileUpload::isOk()` nicht für jede Datei überprüft werden, da das Formular als Ganzes ungültig wäre. +Lässt sich eine der Dateien nicht korrekt hochladen, wird das Formular nicht erfolgreich abgesendet und ein Fehler angezeigt. Nach erfolgreichem Absenden muss die Methode `FileUpload::isOk()` also nicht für jede Datei geprüft werden. -Vertrauen Sie niemals den ursprünglichen Dateinamen, die von der Methode `FileUpload::getName()` zurückgegeben werden, der Client könnte schädliche Dateinamen gesendet haben, um Ihre Anwendung zu beschädigen oder zu hacken. +Vertrauen Sie niemals den ursprünglichen Dateinamen, die die Methode `FileUpload::getName()` zurückgibt; der Client könnte bösartige Dateinamen gesendet haben, um Ihre Anwendung zu beschädigen oder zu kapern. -Die Regeln `MimeType` und `Image` erkennen den erforderlichen Typ anhand der Dateisignatur und überprüfen nicht die Integrität der Datei. Ob ein Bild beschädigt ist, kann beispielsweise durch den Versuch, es [zu laden |http:request#toImage], festgestellt werden. +Die Regeln `MimeType` und `Image` erkennen den verlangten Typ anhand der Signatur der Datei und prüfen ihre Unversehrtheit nicht. Ob ein Bild beschädigt ist, lässt sich zum Beispiel durch den Versuch feststellen, es zu [laden |http:request#toImage()]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== -Fügt ein Feld hinzu, das es dem Benutzer ermöglicht, einfach ein Datum bestehend aus Jahr, Monat und Tag einzugeben (Klasse [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Fügt ein Feld hinzu, in dem der Benutzer bequem ein Datum aus Jahr, Monat und Tag eingeben kann (Klasse [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Als Standardwert akzeptiert es entweder Objekte, die die Schnittstelle `DateTimeInterface` implementieren, eine Zeichenkette mit der Zeit oder eine Zahl, die einen UNIX-Zeitstempel darstellt. Dasselbe gilt für die Argumente der Regeln `Min`, `Max` oder `Range`, die das minimal und maximal zulässige Datum definieren. +Als Standardwert akzeptiert es Objekte, die `DateTimeInterface` implementieren, einen String mit einer Zeitangabe oder eine Zahl, die einen UNIX-Timestamp darstellt. Dasselbe gilt für die Argumente der Regeln `Min`, `Max` und `Range`, die das früheste und späteste erlaubte Datum festlegen. ```php $form->addDate('date', 'Datum:') @@ -279,7 +280,7 @@ $form->addDate('date', 'Datum:') ->addRule($form::Min, 'Das Datum muss mindestens einen Monat alt sein.', new DateTime('-1 month')); ``` -Standardmäßig gibt es ein `DateTimeImmutable`-Objekt zurück, mit der Methode `setFormat()` können Sie das [Textformat|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] oder den Zeitstempel angeben: +Standardmäßig gibt es ein `DateTimeImmutable`-Objekt zurück. Über die Methode `setFormat()` können Sie ein [Textformat|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] oder einen Timestamp angeben: ```php $form->addDate('date', 'Datum:') @@ -287,19 +288,19 @@ $form->addDate('date', 'Datum:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -Fügt ein Feld hinzu, das es dem Benutzer ermöglicht, einfach eine Zeit bestehend aus Stunden, Minuten und optional auch Sekunden einzugeben (Klasse [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Fügt ein Feld hinzu, in dem der Benutzer bequem eine Zeit aus Stunden, Minuten und wahlweise Sekunden eingeben kann (Klasse [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Als Standardwert akzeptiert es entweder Objekte, die die Schnittstelle `DateTimeInterface` implementieren, eine Zeichenkette mit der Zeit oder eine Zahl, die einen UNIX-Zeitstempel darstellt. Aus diesen Eingaben wird nur die Zeitinformation verwendet, das Datum wird ignoriert. Dasselbe gilt für die Argumente der Regeln `Min`, `Max` oder `Range`, die die minimal und maximal zulässige Zeit definieren. Wenn der minimale Wert höher als der maximale ist, wird ein Zeitbereich erstellt, der Mitternacht überschreitet. +Als Standardwert akzeptiert es Objekte, die `DateTimeInterface` implementieren, einen String mit einer Zeitangabe oder eine Zahl, die einen UNIX-Timestamp darstellt. Aus diesen Eingaben wird nur die Zeitangabe verwendet, das Datum wird ignoriert. Dasselbe gilt für die Argumente der Regeln `Min`, `Max` und `Range`, die die früheste und späteste erlaubte Zeit festlegen. Ist der gesetzte Mindestwert höher als der Höchstwert, entsteht ein Zeitbereich über Mitternacht hinweg. ```php $form->addTime('time', 'Zeit:', withSeconds: true) - ->addRule($form::Range, 'Die Zeit muss im Bereich von %d bis %d liegen.', ['12:30', '13:30']); + ->addRule($form::Range, 'Die Zeit muss zwischen %d und %d liegen.', ['12:30', '13:30']); ``` -Standardmäßig gibt es ein `DateTimeImmutable`-Objekt zurück (mit dem Datum 1. Januar des Jahres 1), mit der Methode `setFormat()` können Sie das [Textformat|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] angeben: +Standardmäßig gibt es ein `DateTimeImmutable`-Objekt zurück (mit dem Datum 1. Januar des Jahres 1). Über die Methode `setFormat()` können Sie ein [Textformat|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] angeben: ```php $form->addTime('time', 'Zeit:') @@ -307,20 +308,20 @@ $form->addTime('time', 'Zeit:') ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -Fügt ein Feld hinzu, das es dem Benutzer ermöglicht, einfach Datum und Uhrzeit bestehend aus Jahr, Monat, Tag, Stunden, Minuten und optional auch Sekunden einzugeben (Klasse [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Fügt ein Feld hinzu, in dem der Benutzer bequem Datum und Zeit zugleich eingeben kann, also Jahr, Monat, Tag, Stunden, Minuten und wahlweise Sekunden (Klasse [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Als Standardwert akzeptiert es entweder Objekte, die die Schnittstelle `DateTimeInterface` implementieren, eine Zeichenkette mit der Zeit oder eine Zahl, die einen UNIX-Zeitstempel darstellt. Dasselbe gilt für die Argumente der Regeln `Min`, `Max` oder `Range`, die das minimal und maximal zulässige Datum und die Uhrzeit definieren. +Als Standardwert akzeptiert es Objekte, die `DateTimeInterface` implementieren, einen String mit einer Zeitangabe oder eine Zahl, die einen UNIX-Timestamp darstellt. Dasselbe gilt für die Argumente der Regeln `Min`, `Max` und `Range`, die das früheste und späteste erlaubte Datum samt Zeit festlegen. ```php -$form->addDateTime('datetime', 'Datum und Uhrzeit:') +$form->addDateTime('datetime', 'Datum und Zeit:') ->setDefaultValue(new DateTime) ->addRule($form::Min, 'Das Datum muss mindestens einen Monat alt sein.', new DateTime('-1 month')); ``` -Standardmäßig gibt es ein `DateTimeImmutable`-Objekt zurück, mit der Methode `setFormat()` können Sie das [Textformat|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] oder den Zeitstempel angeben: +Standardmäßig gibt es ein `DateTimeImmutable`-Objekt zurück. Über die Methode `setFormat()` können Sie ein [Textformat|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] oder einen Timestamp angeben: ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -Fügt ein Feld zur Farbauswahl hinzu (Klasse [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Die Farbe ist eine Zeichenkette im Format `#rrggbb`. Wenn der Benutzer keine Auswahl trifft, wird die schwarze Farbe `#000000` zurückgegeben. +Fügt ein Feld zur Auswahl einer Farbe hinzu (Klasse [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Die Farbe wird als String im Format `#rrggbb` zurückgegeben. Trifft der Benutzer keine Auswahl, gibt es Schwarz `#000000` zurück. ```php $form->addColor('color', 'Farbe:') @@ -339,8 +340,8 @@ $form->addColor('color', 'Farbe:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== Fügt ein verstecktes Feld hinzu (Klasse [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -348,28 +349,37 @@ Fügt ein verstecktes Feld hinzu (Klasse [HiddenField |api:Nette\Forms\Controls\ $form->addHidden('userid'); ``` -Mit `setNullable()` kann festgelegt werden, dass `null` anstelle einer leeren Zeichenkette zurückgegeben wird. Der gesendete Wert kann mit [addFilter() |validation#Anpassung der Eingabe] geändert werden. +Mit `setNullable()` sorgen Sie dafür, dass es `null` statt eines leeren Strings zurückgibt. Die Methode [addFilter() |validation#Eingaben verändern] erlaubt es, den gesendeten Wert zu verändern. -Obwohl das Element versteckt ist, ist es **wichtig zu beachten**, dass der Wert immer noch von einem Angreifer geändert oder gefälscht werden kann. Überprüfen und validieren Sie immer gründlich alle empfangenen Werte auf der Serverseite, um Sicherheitsrisiken im Zusammenhang mit Datenmanipulation zu vermeiden. +Auch wenn das Element versteckt ist, **muss Ihnen klar sein**, dass sich sein Wert von einem Angreifer trotzdem verändern oder fälschen lässt. Prüfen und validieren Sie alle empfangenen Werte auf der Serverseite immer gründlich, um Sicherheitsrisiken durch Manipulation der Daten zu vermeiden. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== -Fügt eine Senden-Schaltfläche hinzu (Klasse [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). +Fügt einen Absende-Button hinzu (Klasse [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). ```php -$form->addSubmit('submit', 'Senden'); +$form->addSubmit('submit', 'Absenden'); ``` -Es ist möglich, mehrere Senden-Schaltflächen in einem Formular zu haben: +.{data-version:3.3.0} +Den Handler können Sie dem Button als dritten Parameter `$onSubmit` direkt übergeben, statt ihn an das Event `onClick` zu hängen: + +```php +$form->addSubmit('submit', 'Absenden', function (SubmitButton $button, $data): void { + // ... +}); +``` + +Ein Formular kann mehr als einen Absende-Button haben: ```php $form->addSubmit('register', 'Registrieren'); $form->addSubmit('cancel', 'Abbrechen'); ``` -Um herauszufinden, welche davon geklickt wurde, verwenden Sie: +Um festzustellen, welcher davon gedrückt wurde, verwenden Sie: ```php if ($form['register']->isSubmittedBy()) { @@ -377,13 +387,13 @@ if ($form['register']->isSubmittedBy()) { } ``` -Wenn Sie das gesamte Formular beim Klicken auf eine Schaltfläche nicht validieren möchten (z. B. bei Schaltflächen *Abbrechen* oder *Vorschau*), verwenden Sie [setValidationScope() |validation#Validierung deaktivieren]. +Wenn Sie beim Drücken eines Buttons nicht das gesamte Formular validieren wollen (etwa bei den Buttons *Abbrechen* oder *Vorschau*), verwenden Sie [setValidationScope() |validation#Validierung abschalten]. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -Fügt eine Schaltfläche hinzu (Klasse [Button |api:Nette\Forms\Controls\Button]), die keine Sende-Funktion hat. Sie kann also für eine andere Funktion verwendet werden, z. B. zum Aufrufen einer JavaScript-Funktion beim Klicken. +Fügt einen Button hinzu (Klasse [Button |api:Nette\Forms\Controls\Button]), der keine Absendefunktion hat. Er lässt sich daher für andere Aufgaben nutzen, etwa um beim Klick eine JavaScript-Funktion aufzurufen. ```php $form->addButton('raise', 'Gehalt erhöhen') @@ -391,22 +401,22 @@ $form->addButton('raise', 'Gehalt erhöhen') ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= -Fügt eine Senden-Schaltfläche in Form eines Bildes hinzu (Klasse [ImageButton |api:Nette\Forms\Controls\ImageButton]). +Fügt einen Absende-Button in Form eines Bildes hinzu (Klasse [ImageButton |api:Nette\Forms\Controls\ImageButton]). ```php -$form->addImageButton('submit', '/pfad/zum/bild'); +$form->addImageButton('submit', '/path/to/image.png', 'Absenden'); ``` -Bei Verwendung mehrerer Senden-Schaltflächen kann mit `$form['submit']->isSubmittedBy()` ermittelt werden, welche geklickt wurde. +Verwenden Sie mehrere Absende-Buttons, stellen Sie über `$form['submit']->isSubmittedBy()` fest, welcher davon gedrückt wurde. addContainer(string|int $name): Container .[method] =================================================== -Fügt ein Unterformular (Klasse [Container|api:Nette\Forms\Container]), also einen Container, hinzu, dem weitere Elemente auf die gleiche Weise hinzugefügt werden können, wie wir sie dem Formular hinzufügen. Auch die Methoden `setDefaults()` oder `getValues()` funktionieren. +Fügt ein Unterformular hinzu (Klasse [Container|api:Nette\Forms\Container]), also einen Container, dem sich weitere Elemente auf dieselbe Weise hinzufügen lassen wie dem Formular. Methoden wie `setDefaults()` oder `getValues()` funktionieren ebenfalls. ```php $sub1 = $form->addContainer('first'); @@ -437,48 +447,46 @@ Die gesendeten Daten werden dann als mehrdimensionale Struktur zurückgegeben: Übersicht der Einstellungen =========================== -Für alle Elemente können wir die folgenden Methoden aufrufen (vollständige Übersicht in der [API-Dokumentation|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): +Für alle Elemente können wir die folgenden Methoden aufrufen (eine vollständige Übersicht bietet die [API-Dokumentation|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): .[table-form-methods language-php] -| `setDefaultValue($value)` | Setzt den Standardwert -| `getValue()` | Ruft den aktuellen Wert ab -| `setOmitted()` | [#Wert auslassen] +| `setDefaultValue($value)` | setzt den Standardwert +| `getValue()` | holt den aktuellen Wert +| `setOmitted()` | [#Ausgelassene Werte] | `setDisabled()` | [#Elemente deaktivieren] Rendering: .[table-form-methods language-php] -| `setCaption($caption)` | Ändert die Beschriftung des Elements -| `setTranslator($translator)` | Setzt den [Übersetzer |rendering#Übersetzung] -| `setHtmlAttribute($name, $value)` | Setzt ein [HTML-Attribut |rendering#HTML-Attribute] des Elements -| `setHtmlId($id)` | Setzt das HTML-Attribut `id` -| `setHtmlName($name)` | Setzt das HTML-Attribut `name` -| `setHtmlType($type)` | Setzt das HTML-Attribut `type` -| `setOption($key, $value)` | [Einstellungen für das Rendering |rendering#Options] +| `setCaption($caption)` | ändert das Label des Elements +| `setTranslator($translator)` | setzt den [Übersetzer |rendering#Übersetzen] +| `setHtmlAttribute($name, $value)` | setzt ein [HTML-Attribut |rendering#HTML-Attribute] des Elements +| `setHtmlId($id)` | setzt das HTML-Attribut `id` +| `setOption($key, $value)` | [setzt die Optionen für das Rendering |rendering#Options] Validierung: .[table-form-methods language-php] -| `setRequired()` | [Pflichtfeld |validation] -| `addRule()` | Setzt eine [Validierungsregel |validation#Regeln] -| `addCondition()`, `addConditionOn()` | Setzt eine [Validierungsbedingung |validation#Bedingungen] -| `addError($message)` | [Fehlermeldung übergeben |validation#Fehler bei der Verarbeitung] +| `setRequired()` | macht das Element zum [Pflichtfeld |validation] +| `addRule()` | fügt eine [Validierungsregel |validation#Regeln] hinzu +| `addCondition()`, `addConditionOn()` | setzt eine [Validierungsbedingung |validation#Bedingungen] +| `addError($message)` | [fügt eine Fehlermeldung hinzu |validation#Fehler verarbeiten] -Für die Elemente `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` können die folgenden Methoden aufgerufen werden: +Für die Elemente `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` lassen sich die folgenden Methoden aufrufen: .[table-form-methods language-php] -| `setNullable()` | Legt fest, ob getValue() `null` anstelle einer leeren Zeichenkette zurückgibt -| `setEmptyValue($value)` | Setzt einen speziellen Wert, der als leere Zeichenkette betrachtet wird -| `setMaxLength($length)` | Setzt die maximale Anzahl erlaubter Zeichen -| `addFilter($filter)` | [Eingabe anpassen |validation#Anpassung der Eingabe] +| `setNullable()` | legt fest, ob getValue() statt eines leeren Strings `null` zurückgibt +| `setEmptyValue($value)` | setzt einen besonderen Wert, der als leerer String gilt +| `setMaxLength($length)` | setzt die maximal erlaubte Anzahl von Zeichen +| `addFilter($filter)` | [verändert die Eingabe |validation#Eingaben verändern] -Wert auslassen -============== +Ausgelassene Werte +================== -Wenn uns der vom Benutzer ausgefüllte Wert nicht interessiert, können wir ihn mit `setOmitted()` aus dem Ergebnis der Methode `$form->getValues()` oder aus den an die Handler übergebenen Daten auslassen. Dies ist nützlich für verschiedene Kontrollpasswörter, Anti-Spam-Elemente usw. +Wenn uns der vom Benutzer eingegebene Wert nicht interessiert, können wir ihn mit `setOmitted()` aus dem Ergebnis der Methode `$form->getValues()` und aus den Daten für die Handler ausschließen. Das ist bei Feldern zur Bestätigung eines Passworts, bei Anti-Spam-Elementen und Ähnlichem nützlich. ```php -$form->addPassword('passwordVerify', 'Passwort zur Kontrolle:') - ->setRequired('Bitte geben Sie das Passwort zur Kontrolle noch einmal ein') +$form->addPassword('passwordVerify', 'Passwort erneut:') + ->setRequired('Geben Sie das Passwort zur Kontrolle erneut ein') ->addRule($form::Equal, 'Die Passwörter stimmen nicht überein', $form['password']) ->setOmitted(); ``` @@ -487,16 +495,16 @@ $form->addPassword('passwordVerify', 'Passwort zur Kontrolle:') Elemente deaktivieren ===================== -Elemente können mit `setDisabled()` deaktiviert werden. Ein solches Element kann der Benutzer nicht bearbeiten. +Elemente lassen sich mit `setDisabled()` deaktivieren. Ein deaktiviertes Element kann der Benutzer nicht bearbeiten. ```php $form->addText('username', 'Benutzername:') ->setDisabled(); ``` -Deaktivierte Elemente sendet der Browser überhaupt nicht an den Server, daher finden Sie sie auch nicht in den von der Funktion `$form->getValues()` zurückgegebenen Daten. Wenn Sie jedoch `setOmitted(false)` einstellen, schließt Nette ihren Standardwert in diese Daten ein. +Deaktivierte Elemente sendet der Browser gar nicht erst an den Server, Sie finden sie also nicht in den Daten, die die Funktion `$form->getValues()` zurückgibt. Setzen Sie jedoch `setOmitted(false)`, nimmt Nette ihren Standardwert in diese Daten auf. -Beim Aufruf von `setDisabled()` wird aus Sicherheitsgründen **der Wert des Elements gelöscht**. Wenn Sie einen Standardwert festlegen, muss dies nach der Deaktivierung erfolgen: +Beim Aufruf von `setDisabled()` wird der **Wert des Elements aus Sicherheitsgründen gelöscht**. Wenn Sie einen Standardwert setzen, müssen Sie das also nach dem Deaktivieren tun: ```php $form->addText('username', 'Benutzername:') @@ -504,42 +512,26 @@ $form->addText('username', 'Benutzername:') ->setDefaultValue($userName); ``` -Eine Alternative zu deaktivierten Elementen sind Elemente mit dem HTML-Attribut `readonly`, die der Browser an den Server sendet. Obwohl das Element nur lesbar ist, ist es **wichtig zu beachten**, dass sein Wert immer noch von einem Angreifer geändert oder gefälscht werden kann. +Eine Alternative zu deaktivierten Elementen sind Elemente mit dem HTML-Attribut `readonly`, die der Browser an den Server sendet. Auch wenn das Element nur lesbar ist, **muss Ihnen klar sein**, dass sich sein Wert von einem Angreifer trotzdem verändern oder fälschen lässt. Eigene Elemente =============== -Neben der breiten Palette an integrierten Formularelementen können Sie dem Formular auf diese Weise eigene Elemente hinzufügen: +Neben der breiten Palette eingebauter Formularelemente können Sie dem Formular eigene Elemente hinzufügen: ```php $form->addComponent(new DateInput('Datum:'), 'date'); -// alternative Syntax: $form['date'] = new DateInput('Datum:'); +// alternative Schreibweise: $form['date'] = new DateInput('Datum:'); ``` -.[note] -Das Formular ist ein Nachkomme der Klasse [Container |component-model:#Container] und die einzelnen Elemente sind Nachkommen von [Component |component-model:#Component]. - -Es gibt eine Möglichkeit, neue Methoden des Formulars zu definieren, die zum Hinzufügen eigener Elemente dienen (z. B. `$form->addZip()`). Dies sind sogenannte Extension Methods. Der Nachteil ist, dass die Code-Vervollständigung in Editoren für sie nicht funktioniert. - -```php -use Nette\Forms\Container; - -// wir fügen die Methode addZip(string $name, ?string $label = null) hinzu -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Mindestens 5 Ziffern', '[0-9]{5}'); -}); - -// Verwendung -$form->addZip('zip', 'PLZ:'); -``` +Wie sich ein solches Element schreiben lässt, samt Einlesen der gesendeten Daten, Validierung und Rendering, beschreibt ein [eigenes Kapitel |custom-controls]. Dort erfahren Sie auch etwas über Extension-Methoden, mit denen Sie sich eine eigene Methode zum Hinzufügen wie `$form->addZip()` schreiben können. Low-Level-Elemente ================== -Es können auch Elemente verwendet werden, die wir nur im Template schreiben und nicht mit einer der `$form->addXyz()`-Methoden zum Formular hinzufügen. Wenn wir beispielsweise Datensätze aus der Datenbank ausgeben und im Voraus nicht wissen, wie viele es sein werden und welche IDs sie haben werden, und wir bei jeder Zeile eine Checkbox oder einen Radiobutton anzeigen möchten, reicht es aus, dies im Template zu codieren: +Sie können auch Elemente verwenden, die nur im Template stehen und dem Formular über keine der Methoden `$form->addXyz()` hinzugefügt wurden. Wenn wir zum Beispiel Datensätze aus einer Datenbank auflisten und vorher nicht wissen, wie viele es sein werden und welche IDs sie haben, und für jede Zeile eine Checkbox oder einen Radiobutton anzeigen wollen, schreiben wir das einfach ins Template: ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ Es können auch Elemente verwendet werden, die wir nur im Template schreiben und {/foreach} ``` -Und nach dem Absenden ermitteln wir den Wert: +Und nach dem Absenden holen wir den Wert: ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -wobei der erste Parameter der Elementtyp ist (`DataFile` für `type=file`, `DataLine` für einzeilige Eingaben wie `text`, `password`, `email` usw. und `DataText` für alle anderen wie `checkbox`, `radio`, `textarea`) und der zweite Parameter `sel[]` dem HTML-Attribut `name` entspricht. Den Elementtyp können wir mit dem Wert `DataKeys` kombinieren, der die Schlüssel der Elemente beibehält. Dies ist besonders nützlich für `select`, `radioList` und `checkboxList`. +Der erste Parameter ist dabei der Typ des Elements (`DataFile` für `type=file`, `DataLine` für einzeilige Eingaben wie `text`, `password`, `email` und so weiter, und `DataText` für alle übrigen), der zweite Parameter `sel[]` entspricht dem HTML-Attribut name. Den Typ des Elements können wir mit dem Wert `DataKeys` kombinieren, der die Schlüssel der Elemente erhält. Besonders nützlich ist das bei `select`, `radioList` und `checkboxList`. -Wichtig ist, dass `getHttpData()` einen bereinigten Wert zurückgibt, in diesem Fall wird es immer ein Array gültiger UTF-8-Zeichenketten sein, egal was ein Angreifer versuchen würde, dem Server unterzuschieben. Dies ist analog zur direkten Arbeit mit `$_POST` oder `$_GET`, jedoch mit dem wesentlichen Unterschied, dass immer saubere Daten zurückgegeben werden, so wie Sie es von den Standardelementen der Nette-Formulare gewohnt sind. +Entscheidend ist, dass `getHttpData()` einen bereinigten Wert zurückgibt. In diesem Fall wird es immer ein Array gültiger UTF-8-Strings sein, ganz gleich, was ein Angreifer an den Server zu senden versucht. Das ist analog zur direkten Arbeit mit `$_POST` oder `$_GET`, mit dem wesentlichen Unterschied, dass es immer saubere Daten zurückgibt, so wie Sie es von den Standard-Formularelementen von Nette gewohnt sind. diff --git a/forms/de/custom-controls.texy b/forms/de/custom-controls.texy new file mode 100644 index 0000000000..224fc46123 --- /dev/null +++ b/forms/de/custom-controls.texy @@ -0,0 +1,268 @@ +Eigene Formularelemente +*********************** + +.[perex] +Nette bietet eine breite Palette [eingebauter Formularelemente |controls]. Wenn Sie aber auf eine Anforderung stoßen, die nicht dabei ist, müssen Sie nichts umständlich umgehen oder zusammenkleben: Sie schreiben ein eigenes Element. Es kann alles, was die eingebauten können - validieren, sich übersetzen, sich rendern -, und wird genauso verwendet. + +Wir zeigen es an einem praktischen Beispiel: einem Element zur Eingabe eines Datums über drei Felder, Tag, Monat und Jahr. Dabei erfahren Sie alles, was Sie zum Schreiben von Elementen wissen müssen. + + +Wann ein eigenes Element sinnvoll ist und wann nicht +==================================================== + +Ein eigenes Element ist das mächtigste Werkzeug, das Formulare bieten. Und wie jedes mächtige Werkzeug sollte es die letzte Wahl sein, nicht die erste. Viele Situationen lassen sich mit einfacheren Mitteln lösen: + +- **Einen Wert verändern** erledigt [addFilter() |validation#Eingaben verändern]. Sie wollen Leerzeichen in einer Postleitzahl oder Kleinbuchstaben in einem Code dulden? Ein Filter ist ein paar Zeilen lang. +- **Wiederkehrende Konfiguration** verpackt eine eigene Methode zum Hinzufügen. Sie fügen an zehn Stellen ein Feld für die Postleitzahl mit derselben Validierung hinzu? Legen Sie sich dafür eine benannte Abkürzung an, [wir zeigen es am Ende |#Eigene Methode zum Hinzufügen]. +- **Eine Gruppe zusammengehöriger Felder** bedient ein [Container |controls#addContainer()]. Eine Adresse aus Straße, Stadt und Postleitzahl braucht kein eigenes Element, ein Container mit drei Textfeldern genügt. +- **Ein anderes Aussehen** erreichen Sie über [setHtmlType() |controls#addText()] und HTML-Attribute oder über [Prototypen |rendering#Prototypen]. + +Ein eigenes Element ergibt in dem Moment Sinn, in dem Sie einen **eigenen Wert** brauchen: ein Element, das nach außen wie ein einzelnes Feld mit einem einzigen Wert wirkt, innen aber aus mehreren Eingaben besteht oder den Wert anders speichert, als es ihn anzeigt. Ein Datum aus drei Feldern. Koordinaten, die per Klick auf eine Karte gewählt werden. Eine Eingabe von Tags mit Autovervollständigung. + + +Anatomie eines Elements +======================= + +Jedes eigene Element erbt von der abstrakten Klasse [api:Nette\Forms\Controls\BaseControl]. Von ihr erbt es eine Menge fertiger Funktionalität: das Speichern des Werts, Validierungsregeln und -bedingungen, Fehlermeldungen, Übersetzungen, HTML-Attribute, das Label und die Anbindung an das Rendering. Sie schreiben nur, was Ihr Element unterscheidet. + +Ein minimales funktionierendes Element ist überraschend kurz: + +```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, + ]); + } +} +``` + +Zwei Methoden: Die eine sagt, wie der Wert aus den gesendeten Daten zu holen ist, die andere, wie das Element zu rendern ist. Beide sehen wir uns gleich genauer an. Alles andere - `setRequired()`, `addRule()`, `setDefaultValue()`, Übersetzungen - funktioniert bereits von selbst. + +Dem Formular fügen Sie das Element über die Methode `addComponent()` hinzu oder knapper über eckige Klammern: + +```php +$form['nickname'] = new SimpleInput('Spitzname:'); +``` + + +Lebenszyklus eines Elements +=========================== + +Bevor wir zu einem interessanteren Element kommen, ist es gut zu wissen, was mit einem Element wann geschieht. Das Formular und seine Elemente sind [Komponenten |component-model:], die einen Baum bilden. Das hat eine angenehme Folge: Das Element muss nichts selbst herausfinden, das Framework kümmert sich zum richtigen Zeitpunkt um alles Wichtige: + +1) In dem Moment, in dem Sie das Element an ein abgesendetes Formular hängen, ruft das Formular selbst `loadHttpData()` darauf auf. Darin liest das Element seinen gesendeten Wert, wie wir gleich zeigen. Es arbeitet nie direkt mit `$_POST` und muss sich überhaupt nicht darum kümmern, ob es in Containern verschachtelt ist. + +2) Wird das Formular abgesendet, läuft die Validierung: Die über `addRule()` hinzugefügten Regeln werden ausgewertet und arbeiten mit dem Wert aus `getValue()`. + +3) Wer danach `$form->getValues()` oder `getValue()` auf dem Element aufruft, bekommt einen sauberen, typisierten Wert - etwa ein `DateTimeImmutable`-Objekt, nicht drei Strings aus dem Formular. + +Und beim Rendern wird `getControl()` aufgerufen, bzw. `getLabel()` für das Label. + + +Den gesendeten Wert lesen +========================= + +In der Methode `loadHttpData()` fragt das Element über die Methode `getHttpData()` nach seinem gesendeten Wert. Ihr Parameter ist ein Typ, der bestimmt, wie der Wert bereinigt werden soll: + +| Typ | Bedeutung +|------- +| `Form::DataLine` | einzeiliger Text: ersetzt Zeilenumbrüche durch Leerzeichen, schneidet Leerzeichen ab +| `Form::DataText` | mehrzeiliger Text: vereinheitlicht die Zeilenenden zu `\n` +| `Form::DataFile` | Upload, eine Instanz von `Nette\Http\FileUpload` + +Ganz gleich, wie sehr sich ein Angreifer bemüht, das Ergebnis ist immer ein gültiger UTF-8-String ohne Steuerzeichen (oder ein Upload-Objekt oder `null`). Genau deshalb lesen wir den Wert nie direkt aus `$_POST` - wir verlören all diese Garantien. + +Ein Element, das aus mehreren Eingaben besteht, wie unser Datum, übergibt als zweiten Parameter einen Teil des HTML-Namens und liest so seine einzelnen Teilwerte. Es legt sie in seinen eigenen Properties `$day`, `$month` und `$year` vom Typ string ab: + +```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]') ?? ''; +} +``` + +Endet der HTML-Name mit `[]`, wird ein Array von Werten zurückgegeben. Durch die Kombination mit dem Typ `Form::DataKeys` (also `Form::DataLine | Form::DataKeys`) bleiben auch dessen Schlüssel erhalten: + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +Ein fehlender Wert ist `null` (bei Arrays ein leeres Array). Der Request muss die Daten des Elements gar nicht enthalten, und nichts hindert einen Angreifer daran, zu senden, was ihm beliebt - deshalb ergänzen wir im Beispiel `?? ''` und deshalb sollten Sie mit dieser Variante immer rechnen. + + +Der Wert des Elements +===================== + +Das Element hält seinen Wert und legt ihn über drei Methoden offen, an deren Vertrag man sich halten sollte. + +Die Methode `setValue()` nimmt einen Wert vom Programmierer entgegen - diesen Weg gehen auch `setDefaultValue()` und `$form->setDefaults()`. Sie sollte alles annehmen, was Sinn ergibt, den Wert in seine interne Form umwandeln und bei unsinniger Eingabe eine Exception werfen, damit der Fehler sofort auffällt und nicht über rätselhaftes Verhalten des Formulars. Unser Datum akzeptiert ein `DateTimeInterface`, einen String, einen Timestamp oder `null` und zerlegt sie in die drei Felder: + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // Unsinn wirft eine Exception + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +Die Methode `getValue()` setzt dagegen einen sauberen, typisierten Wert zusammen - das Einzige, was der Nutzer Ihres Elements zu sehen bekommt. Ist der Wert nicht gültig, gibt sie `null` zurück. Die statische Methode `validateDate()` prüft schlicht, dass die drei Felder ein existierendes Datum ergeben: + +```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; +} +``` + +Und die Methode `isFilled()` sagt, ob der Benutzer das Element ausgefüllt hat - sie nutzt die Regel `setRequired()`. Die Standardimplementierung (ein nicht leerer Wert) genügt oft, bei einem zusammengesetzten Element überschreiben Sie sie aber nach dessen Logik: + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Rendering +========= + +Die Methode `getControl()` gibt die HTML-Form des Elements zurück, üblicherweise als [Html |utils:html-elements]-Objekt, aber ein schlichter String ist ebenso in Ordnung - das spielt keine Rolle. Zum Html-Objekt greifen wir vor allem beim Zusammensetzen des Codes, weil sich das entstehende Markup damit sicher und mit einer angenehmen API bauen lässt. Ihnen stehen mehrere Helfer zur Verfügung: + +- `getHtmlName()` gibt das HTML-Attribut `name` zurück, samt möglicher Verschachtelung in Containern (etwa `invoice[date]`). Bei einem zusammengesetzten Element hängen Sie die Namensteile der einzelnen Eingaben daran: `$name . '[day]'`. +- `getHtmlId()` gibt das Attribut `id` zurück, das mit dem Label verknüpft ist. +- `Helpers::exportRules($this->getRules())` exportiert die Validierungsregeln für das Attribut `data-nette-rules`, dank dessen die [Validierung in JavaScript |validation#Validierung in JavaScript] auch für Ihr Element funktioniert. Das Attribut gehört an die erste Eingabe des Elements. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` setzt aus einem Array von Elementen ein `<select>`-Element zusammen (verschachtelte Arrays werden als `<optgroup>` gerendert) und gibt es als `Html` zurück - praktisch für das Feld für den Monat in unserem Datum. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` erzeugt eine Liste von `<input>`-Elementen, die in `<label>` gepackt sind (Radiobuttons oder Checkboxen), und gibt sie als String zurück. + +Das erste Feld unseres Datums entsteht also so: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select für den Monat und input für das Jahr ... */); +} +``` + +Das Label rendert `getLabel()`, und seine Standardimplementierung passt üblicherweise. Nur Vorsicht: Bei einem zusammengesetzten Element zeigt dessen Attribut `for` auf `getHtmlId()`, geben Sie diese id also der ersten Eingabe - genau wie im Beispiel. + +Damit sich das zusammengesetzte Element im Template Teil für Teil rendern lässt (etwa `{input birthdate:day}`), überschreiben Sie die Methoden `getControlPart($key)` und `getLabelPart($key)`, die das `Html`-Element für den jeweiligen Teil zurückgeben - genauso, wie es `CheckboxList` und `RadioList` tun. + +.[note] +Wenn Sie `getControl()` überschreiben, denken Sie daran, dass `BaseControl::getControl()` das Element über `setOption('rendered', true)` auch als gerendert markiert. Rufen Sie sie ebenfalls auf (oder rufen Sie `parent::getControl()`), wenn Sie manuelles und automatisches Rendering desselben Formulars kombinieren, damit das Element nicht zweimal gerendert wird. (Das obige Beispiel `DateInput` lässt das der Kürze halber weg.) + + +Vollständiges Beispiel: DateInput +================================= + +Alle beschriebenen Teile zusammen, ergänzt um eine Select-Box zur Auswahl des Monats, finden Sie im fertigen Element `DateInput` unter den [Beispielen direkt im Repository |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Beachten Sie, dass sich das Element im Konstruktor selbst eine Validierungsregel hinzufügt, die prüft, dass das Datum Sinn ergibt. Eine unsinnige Eingabe wie der 31. Februar zeigt sich damit als gewöhnlicher Validierungsfehler des Formulars: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'Das Datum ist ungültig.'); +} +``` + +Und die Verwendung? Genau wie bei den eingebauten Elementen: + +```php +$form['birthdate'] = (new DateInput('Geburtsdatum:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('Wann wurden Sie geboren?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +In einem Latte-Template rendern Sie es mit dem gewohnten Tag `{input birthdate}` oder `{label birthdate /}`, wie jedes andere Element. + + +Validierung +=========== + +Die eingebauten Validierungsregeln funktionieren mit einem eigenen Element sofort - sie arbeiten mit dem Wert aus `getValue()`. Unser `DateInput` kann also zum Beispiel `Form::Min` für das älteste erlaubte Datum verwenden. Wie sich eigene Regeln samt ihrem Gegenstück in JavaScript schreiben lassen, beschreibt das Kapitel [Eigene Regeln und Bedingungen |validation#Eigene Regeln und Bedingungen]. + + +Eigene Methode zum Hinzufügen +============================= + +Eingebaute Elemente fügen wir über die bequemen Methoden `$form->addText()` und Konsorten hinzu. Ein eigenes Element hat keine solche Methode, Sie fügen es also über eine schlichte Zuweisung hinzu - das funktioniert in einem Formular wie in einem Container gleich, und Editoren und statische Analyse verstehen es: + +```php +$form['birthdate'] = new DateInput('Geburtsdatum:'); +``` + +Wenn Sie das Hinzufügen abkürzen und dabei die Autovervollständigung behalten wollen, kommt eine statische Factory-Methode direkt am Element gelegen. Sie funktioniert auch in verschachtelten Containern, was eine Methode an einem Nachfahren der Klasse `Form` nicht könnte - verschachtelte Container wissen nichts von ihr: + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// funktioniert in einem Formular und in jedem Container: +DateInput::addTo($form, 'birthdate', 'Geburtsdatum:'); +``` + +Derselbe Ansatz funktioniert auch als benannte Abkürzung für die wiederkehrende Konfiguration eines eingebauten Elements: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'Die Postleitzahl muss genau 5 Ziffern haben', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'Postleitzahl:'); +``` diff --git a/forms/de/in-presenter.texy b/forms/de/in-presenter.texy index 9e76d4bae8..c681e6ee14 100644 --- a/forms/de/in-presenter.texy +++ b/forms/de/in-presenter.texy @@ -2,15 +2,15 @@ Formulare in Presentern *********************** .[perex] -Nette Forms erleichtern die Erstellung und Verarbeitung von Webformularen erheblich. In diesem Kapitel lernen Sie die Verwendung von Formularen innerhalb von Presentern kennen. +Nette Forms vereinfachen das Erstellen und Verarbeiten von Webformularen erheblich. In diesem Kapitel erfahren Sie, wie Sie Formulare innerhalb von Presentern verwenden. -Wenn Sie daran interessiert sind, wie man sie völlig eigenständig ohne den Rest des Frameworks verwendet, ist die Anleitung zur [eigenständigen Verwendung|standalone] für Sie bestimmt. +Wenn Sie sie völlig eigenständig ohne den Rest des Frameworks nutzen möchten, gibt es eine Anleitung zur [eigenständigen Verwendung|standalone]. -Erstes Formular -=============== +Das erste Formular +================== -Versuchen wir, ein einfaches Registrierungsformular zu schreiben. Sein Code wird wie folgt aussehen: +Versuchen wir, ein einfaches Registrierungsformular zu schreiben. Sein Code sieht so aus: ```php use Nette\Application\UI\Form; @@ -19,16 +19,16 @@ $form = new Form; $form->addText('name', 'Name:'); $form->addPassword('password', 'Passwort:'); $form->addSubmit('send', 'Registrieren'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` -und im Browser wird es so angezeigt: +und im Browser wird es so dargestellt: -[* form-cs.webp *] +[* form-en.webp *] -Ein Formular im Presenter ist ein Objekt der Klasse `Nette\Application\UI\Form`, sein Vorgänger `Nette\Forms\Form` ist für die eigenständige Verwendung bestimmt. Wir haben ihm sogenannte Elemente Name, Passwort und eine Senden-Schaltfläche hinzugefügt. Und schließlich besagt die Zeile mit `$form->onSuccess`, dass nach dem Senden und erfolgreicher Validierung die Methode `$this->formSucceeded()` aufgerufen werden soll. +Ein Formular im Presenter ist ein Objekt der Klasse `Nette\Application\UI\Form`; ihr Vorgänger `Nette\Forms\Form` ist für die eigenständige Verwendung gedacht. Wir haben Elemente namens name und password sowie einen Absende-Button hinzugefügt. Die Zeile `$form->onSuccess` sagt schließlich, dass nach dem Absenden und erfolgreicher Validierung die Methode `$this->formSucceeded()` aufgerufen werden soll. -Aus Sicht des Presenters ist das Formular eine gewöhnliche Komponente. Daher wird es wie eine Komponente behandelt und wir integrieren es in den Presenter mithilfe einer [Factory-Methode |application:components#Factory-Methoden]. Das wird so aussehen: +Aus Sicht des Presenters ist das Formular eine gewöhnliche Komponente. Es wird deshalb wie eine Komponente behandelt und über eine [Factory-Methode |application:components#Factory-Methoden] in den Presenter eingebunden. Das sieht so aus: ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -42,22 +42,22 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Name:'); $form->addPassword('password', 'Passwort:'); $form->addSubmit('send', 'Registrieren'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { // hier verarbeiten wir die vom Formular gesendeten Daten // $data->name enthält den Namen // $data->password enthält das Passwort - $this->flashMessage('Sie wurden erfolgreich registriert.'); + $this->flashMessage('Sie haben sich erfolgreich registriert.'); $this->redirect('Home:'); } } ``` -Und im Template rendern wir das Formular mit dem Tag `{control}`: +Und im Template wird das Formular über den Tag `{control}` gerendert: ```latte .{file:app/Presentation/Home/default.latte} <h1>Registrierung</h1> @@ -65,45 +65,47 @@ Und im Template rendern wir das Formular mit dem Tag `{control}`: {control registrationForm} ``` -Und das ist eigentlich alles :-) Wir haben ein funktionsfähiges und perfekt [gesichertes |#Schutz vor Schwachstellen] Formular. +Und das ist im Grunde alles :-) Wir haben ein funktionierendes und bestens [abgesichertes |#Schutz vor Sicherheitslücken] Formular. -Und jetzt denken Sie wahrscheinlich, dass das zu schnell ging, und fragen sich, wie es möglich ist, dass die Methode `formSucceeded()` aufgerufen wird und was die Parameter sind, die sie erhält. Sicher, Sie haben Recht, das verdient eine Erklärung. +Jetzt denken Sie vermutlich, das ging zu schnell, und fragen sich, wie es möglich ist, dass die Methode `formSucceeded()` aufgerufen wird und welche Parameter sie bekommt. Ja, Sie haben recht, das verdient eine Erklärung. -Nette verwendet nämlich einen frischen Mechanismus, den wir [Hollywood style |application:components#Hollywood Style] nennen. Anstatt dass Sie als Entwickler ständig fragen müssen, ob etwas passiert ist („wurde das Formular gesendet?“, „wurde es gültig gesendet?“ und „wurde es nicht gefälscht?“), sagen Sie dem Framework „wenn das Formular gültig ausgefüllt ist, rufe diese Methode auf“ und überlassen ihm die weitere Arbeit. Wenn Sie in JavaScript programmieren, kennen Sie diesen Programmierstil genau. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes [Ereignis |nette:glossary#Events Ereignisse] eintritt. Und die Sprache übergibt ihnen die entsprechenden Argumente. +Nette bringt einen erfrischenden Mechanismus mit, den [Hollywood-Stil |application:components#Hollywood Style]. Statt dass Sie als Entwickler ständig fragen müssen, ob etwas passiert ist ("wurde das Formular abgesendet?", "wurde es gültig abgesendet?", "wurde es nicht gefälscht?"), sagen Sie dem Framework "wenn das Formular gültig ausgefüllt ist, 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 [Event |nette:glossary#Events] eintritt. Und die Sprache übergibt ihnen die passenden Argumente. -Genau so ist auch der oben genannte Presenter-Code aufgebaut. Das Array `$form->onSuccess` stellt eine Liste von PHP-Callbacks dar, die Nette aufruft, wenn das Formular gesendet und korrekt ausgefüllt wurde (d. h. es ist gültig). Im Rahmen des [Lebenszyklus des Presenters |application:presenters#Lebenszyklus des Presenters] handelt es sich um ein sogenanntes Signal, sie werden also nach der `action*`-Methode und vor der `render*`-Methode aufgerufen. Und jedem Callback übergibt es als ersten Parameter das Formular selbst und als zweiten die gesendeten Daten in Form eines [ArrayHash |utils:arrays#ArrayHash]-Objekts (oder einer benutzerdefinierten Klasse, siehe unten). Den ersten Parameter können Sie weglassen, wenn Sie das Formularobjekt nicht benötigen. Und der zweite Parameter kann cleverer sein, aber dazu [später mehr |#Mapping auf Klassen]. +Genau so ist der obige Code des Presenters aufgebaut. Das Array `$form->onSuccess` stellt eine Liste von PHP-Callbacks dar, die Nette in dem Moment aufruft, in dem das Formular abgesendet und richtig ausgefüllt ist (also gültig). Innerhalb des [Lebenszyklus des Presenters |application:presenters#Lebenszyklus des Presenters] handelt es sich um ein sogenanntes Signal, sie werden also nach der Methode `action*` und vor der Methode `render*` aufgerufen. Und jedem Callback übergibt es als ersten Parameter das Formular selbst und als zweiten die gesendeten Daten als Objekt [ArrayHash |utils:arrays#ArrayHash] (oder stdClass oder eine eigene Klasse). Den ersten Parameter können Sie weglassen, wenn Sie das Objekt des Formulars nicht brauchen. Der zweite Parameter kann klüger sein, dazu aber [später |#Mapping auf Klassen] mehr. -Das Objekt `$data` enthält die Schlüssel `name` und `password` mit den Daten, die der Benutzer eingegeben hat. Normalerweise senden wir die Daten direkt zur weiteren Verarbeitung, was beispielsweise das Einfügen in die Datenbank sein kann. Während der Verarbeitung kann jedoch ein Fehler auftreten, z. B. wenn der Benutzername bereits vergeben ist. In diesem Fall übergeben wir den Fehler mit `addError()` zurück an das Formular und lassen es erneut rendern, auch mit der Fehlermeldung. +Das Objekt `$data` enthält die Properties `name` und `password` mit den Daten, die der Benutzer eingegeben hat. Üblicherweise geben wir die Daten direkt zur weiteren Verarbeitung weiter, etwa zum Einfügen in eine Datenbank. Bei der Verarbeitung kann jedoch ein Fehler auftreten, zum Beispiel ist der Benutzername schon vergeben. In einem solchen Fall geben wir den Fehler über `addError()` an das Formular zurück und lassen es samt Fehlermeldung erneut rendern. ```php -$form->addError('Entschuldigung, der Benutzername wird bereits verwendet.'); +$form->addError('Der Benutzername ist leider bereits vergeben.'); ``` -Neben `onSuccess` gibt es noch `onSubmit`: Callbacks werden immer nach dem Senden des Formulars aufgerufen, auch wenn es nicht korrekt ausgefüllt ist. Und weiter `onError`: Callbacks werden nur aufgerufen, wenn das Senden nicht gültig ist. Sie werden sogar dann aufgerufen, wenn wir in `onSuccess` oder `onSubmit` das Formular mit `addError()` ungültig machen. +Neben `onSuccess` gibt es auch `onSubmit`: Die Callbacks werden immer aufgerufen, wenn das Formular abgesendet wird, auch wenn es nicht richtig ausgefüllt ist. Und außerdem `onError`: Die Callbacks werden nur dann aufgerufen, wenn das Absenden nicht gültig ist. Sie werden sogar dann aufgerufen, wenn wir das Formular in `onSuccess` über `addError()` für ungültig erklären. + +Nach dem Verarbeiten des Formulars leiten wir auf eine andere Seite weiter. Das verhindert, dass das Formular ungewollt erneut abgesendet wird, wenn der Benutzer *Aktualisieren* oder *Zurück* drückt oder in der Browser-Historie navigiert. -Nach der Verarbeitung des Formulars leiten wir auf eine andere Seite weiter. Dies verhindert das unbeabsichtigte erneute Senden des Formulars durch die Schaltflächen *Aktualisieren*, *Zurück* oder durch die Bewegung im Browserverlauf. +Wird das Formular über AJAX abgesendet, zeichnen Sie statt einer Weiterleitung üblicherweise ein [Snippet |application:ajax] mit dem neu gerenderten Formular neu. -Versuchen Sie, auch weitere [Formularelemente|controls] hinzuzufügen. +Versuchen Sie, weitere [Formularelemente|controls] hinzuzufügen. -Zugriff auf Elemente -==================== +Zugriff auf die Elemente +======================== -Das Formular ist eine Komponente des Presenters, in unserem Fall namens `registrationForm` (nach dem Namen der Factory-Methode `createComponentRegistrationForm`), sodass Sie überall im Presenter mit Folgendem auf das Formular zugreifen können: +Das Formular ist eine Komponente des Presenters, in unserem Fall namens `registrationForm` (nach dem Namen der Factory-Methode `createComponentRegistrationForm`), Sie können also überall im Presenter so auf das Formular zugreifen: ```php $form = $this->getComponent('registrationForm'); -// alternative Syntax: $form = $this['registrationForm']; +// alternative Schreibweise: $form = $this['registrationForm']; ``` -Auch die einzelnen Formularelemente sind Komponenten, daher können Sie auf die gleiche Weise darauf zugreifen: +Auch die einzelnen Formularelemente sind Komponenten, Sie greifen also genauso auf sie zu: ```php $input = $form->getComponent('name'); // oder $input = $form['name']; $button = $form->getComponent('send'); // oder $button = $form['send']; ``` -Elemente werden mit `unset` entfernt: +Entfernt werden Elemente über `unset`: ```php unset($form['name']); @@ -113,26 +115,26 @@ unset($form['name']); Validierungsregeln ================== -Das Wort *gültig* fiel, aber das Formular hat bisher keine Validierungsregeln. Lassen Sie uns das beheben. +Das Wort *gültig* ist gefallen, aber das Formular hat noch keine Validierungsregeln. Bringen wir das in Ordnung. -Der Name wird obligatorisch sein, daher markieren wir ihn mit der Methode `setRequired()`, deren Argument der Text der Fehlermeldung ist, die angezeigt wird, wenn der Benutzer den Namen nicht ausfüllt. Wenn kein Argument angegeben wird, wird die Standardfehlermeldung verwendet. +Der Name wird zum Pflichtfeld, wir kennzeichnen ihn also über die Methode `setRequired()`. Ihr Argument ist der Text der Fehlermeldung, die angezeigt wird, wenn der Benutzer den Namen nicht ausfüllt. Wird das Argument weggelassen, wird die Standard-Fehlermeldung verwendet. ```php $form->addText('name', 'Name:') - ->setRequired('Bitte geben Sie einen Namen ein'); + ->setRequired('Bitte geben Sie Ihren Namen ein.'); ``` -Versuchen Sie, das Formular ohne ausgefüllten Namen abzusenden, und Sie werden sehen, dass eine Fehlermeldung angezeigt wird und der Browser oder Server es ablehnt, bis Sie das Feld ausfüllen. +Versuchen Sie, das Formular ohne ausgefüllten Namen abzusenden, und Sie sehen die Fehlermeldung; der Browser oder der Server weist es ab, bis Sie das Feld ausfüllen. -Gleichzeitig können Sie das System nicht austricksen, indem Sie beispielsweise nur Leerzeichen in das Feld eingeben. Nein. Nette entfernt automatisch führende und nachfolgende Leerzeichen. Probieren Sie es aus. Das ist etwas, das Sie bei jedem einzeiligen Eingabefeld immer tun sollten, aber oft vergessen wird. Nette tut dies automatisch. (Sie können versuchen, das Formular auszutricksen und als Namen eine mehrzeilige Zeichenkette zu senden. Auch hier lässt sich Nette nicht täuschen und ändert Zeilenumbrüche in Leerzeichen.) +Zugleich können Sie das System nicht austricksen, indem Sie zum Beispiel nur Leerzeichen ins Feld schreiben. Auf keinen Fall. Nette schneidet Leerraum am Anfang und Ende automatisch ab. Probieren Sie es aus. Das sollten Sie bei jeder einzeiligen Eingabe immer tun, es wird aber oft vergessen. Nette macht es automatisch. (Sie können versuchen, das Formular hereinzulegen und als Namen einen mehrzeiligen String zu senden. Auch hier lässt sich Nette nicht täuschen, die Zeilenumbrüche werden in Leerzeichen umgewandelt.) -Das Formular wird immer serverseitig validiert, aber es wird auch eine JavaScript-Validierung generiert, die blitzschnell abläuft, und der Benutzer erfährt sofort von dem Fehler, ohne das Formular an den Server senden zu müssen. Dafür ist das Skript `netteForms.js` verantwortlich. Fügen Sie es in das Layout-Template ein: +Das Formular wird immer auf der Serverseite validiert, es wird aber auch eine JavaScript-Validierung erzeugt, die sofort läuft und den Benutzer unmittelbar über den Fehler informiert, ohne dass das Formular an den Server gesendet werden muss. Dafür sorgt das Skript `netteForms.js`. Binden Sie es in Ihr Layout-Template ein: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, können Sie feststellen, dass Nette Pflichtfelder in Elemente mit der CSS-Klasse `required` einfügt. Versuchen Sie, das folgende Stylesheet zum Template hinzuzufügen, und die Beschriftung „Name“ wird rot. So markieren wir elegant Pflichtfelder für die Benutzer: +Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, fällt Ihnen vielleicht auf, dass Nette die Pflichtelemente in Elemente mit der CSS-Klasse `required` packt. Fügen Sie Ihrem Template das folgende Stylesheet hinzu, und das Label "Name" wird rot. So heben Sie Pflichtfelder für die Benutzer elegant hervor: ```latte <style> @@ -140,85 +142,87 @@ Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, können Sie fest </style> ``` -Weitere Validierungsregeln fügen wir mit der Methode `addRule()` hinzu. Der erste Parameter ist die Regel, der zweite ist wieder der Text der Fehlermeldung, und es kann noch ein Argument der Validierungsregel folgen. Was ist damit gemeint? +Weitere Validierungsregeln fügen wir über die Methode `addRule()` hinzu. Der erste Parameter ist die Regel, der zweite wiederum der Text der Fehlermeldung, und danach kann ein Argument für die Validierungsregel folgen. Was bedeutet das? -Wir erweitern das Formular um ein neues optionales Feld „Alter“, das eine ganze Zahl sein muss (`addInteger()`) und außerdem in einem erlaubten Bereich (`$form::Range`) liegen muss. Und hier verwenden wir genau den dritten Parameter der Methode `addRule()`, mit dem wir dem Validator den erforderlichen Bereich als Paar `[von, bis]` übergeben: +Erweitern wir das Formular um ein neues optionales Feld "Alter", das eine ganze Zahl sein muss (`addInteger()`) und außerdem in einem erlaubten Bereich liegen muss (`$form::Range`). Hier verwenden wir den dritten Parameter der Methode `addRule()`, um dem Validator den verlangten Bereich als Paar `[min, max]` zu übergeben: ```php $form->addInteger('age', 'Alter:') - ->addRule($form::Range, 'Das Alter muss zwischen 18 und 120 liegen', [18, 120]); + ->addRule($form::Range, 'Das Alter muss zwischen 18 und 120 liegen.', [18, 120]); ``` .[tip] -Wenn der Benutzer das Feld nicht ausfüllt, werden die Validierungsregeln nicht überprüft, da das Element optional ist. +Füllt der Benutzer das Feld nicht aus, werden die Validierungsregeln nicht geprüft, denn das Element ist optional. -Hier entsteht Raum für ein kleines Refactoring. In der Fehlermeldung und im dritten Parameter sind die Zahlen doppelt aufgeführt, was nicht ideal ist. Wenn wir [mehrsprachige Formulare |rendering#Übersetzung] erstellen würden und die Meldung mit Zahlen in mehrere Sprachen übersetzt würde, würde eine spätere Änderung der Werte erschwert. Aus diesem Grund können Platzhalter `%d` verwendet werden, und Nette füllt die Werte ein: +Damit entsteht Raum für ein kleines Refactoring. In der Fehlermeldung und im dritten Parameter stehen die Zahlen doppelt, was nicht ideal ist. Würden wir [mehrsprachige Formulare |rendering#Übersetzen] bauen und die Meldung mit den Zahlen in mehrere Sprachen übersetzen, wäre das Ändern der Werte mühsam. Aus diesem Grund lassen sich die Platzhalter `%d` verwenden, und Nette setzt die Werte ein: ```php - ->addRule($form::Range, 'Das Alter muss zwischen %d und %d Jahren liegen', [18, 120]); + ->addRule($form::Range, 'Das Alter muss zwischen %d und %d Jahren liegen.', [18, 120]); ``` -Kehren wir zum Element `password` zurück, das wir ebenfalls obligatorisch machen und noch die minimale Passwortlänge überprüfen (`$form::MinLength`), wieder unter Verwendung des Platzhalters: +Kehren wir zum Element `password` zurück, machen es ebenfalls zum Pflichtfeld und prüfen außerdem die Mindestlänge des Passworts (`$form::MinLength`), wieder mit einem Platzhalter in der Meldung: ```php $form->addPassword('password', 'Passwort:') ->setRequired('Wählen Sie ein Passwort') - ->addRule($form::MinLength, 'Das Passwort muss mindestens %d Zeichen lang sein', 8); + ->addRule($form::MinLength, 'Ihr Passwort muss mindestens %d Zeichen lang sein.', 8); ``` -Wir fügen dem Formular noch ein Feld `passwordVerify` hinzu, in das der Benutzer das Passwort zur Kontrolle noch einmal eingibt. Mit Validierungsregeln überprüfen wir, ob beide Passwörter übereinstimmen (`$form::Equal`). Und als Parameter geben wir einen Verweis auf das erste Passwort mithilfe von [eckigen Klammern |#Zugriff auf Elemente] an: +Fügen wir dem Formular noch ein Feld `passwordVerify` hinzu, in dem der Benutzer das Passwort zur Bestätigung erneut eingibt. Über Validierungsregeln prüfen wir, ob beide Passwörter gleich sind (`$form::Equal`). Als Argument geben wir über [eckige Klammern |#Zugriff auf die Elemente] einen Verweis auf das erste Passwort an: ```php -$form->addPassword('passwordVerify', 'Passwort zur Kontrolle:') - ->setRequired('Bitte geben Sie das Passwort zur Kontrolle noch einmal ein') - ->addRule($form::Equal, 'Die Passwörter stimmen nicht überein', $form['password']) +$form->addPassword('passwordVerify', 'Passwort erneut:') + ->setRequired('Geben Sie das Passwort zur Kontrolle erneut ein') + ->addRule($form::Equal, 'Die Passwörter stimmen nicht überein.', $form['password']) ->setOmitted(); ``` -Mit `setOmitted()` haben wir das Element markiert, dessen Wert uns eigentlich egal ist und das nur aus Validierungsgründen existiert. Der Wert wird nicht an `$data` übergeben. +Mit `setOmitted()` haben wir ein Element gekennzeichnet, dessen Wert uns eigentlich nicht interessiert und das nur zur Validierung da ist. Sein Wert wird nicht an `$data` übergeben. -Damit haben wir ein voll funktionsfähiges Formular mit Validierung in PHP und JavaScript fertiggestellt. Die Validierungsfähigkeiten von Nette sind weitaus umfangreicher, es können Bedingungen erstellt, Teile der Seite entsprechend ein- und ausgeblendet werden usw. Alles erfahren Sie im Kapitel über [Formularvalidierung|validation]. +Damit haben wir ein voll funktionsfähiges Formular mit Validierung in PHP und in JavaScript. Die Fähigkeiten von Nette zur Validierung reichen viel weiter; es lassen sich Bedingungen erstellen, anhand derer sich Teile der Seite ein- und ausblenden lassen, und so weiter. Alles erfahren Sie im Kapitel über die [Validierung von Formularen|validation]. Standardwerte ============= -Formularelementen weisen wir üblicherweise Standardwerte zu: +Für Formularelemente setzen wir üblicherweise Standardwerte: ```php $form->addEmail('email', 'E-Mail') ->setDefaultValue($lastUsedEmail); ``` -Oft ist es nützlich, Standardwerte für alle Elemente gleichzeitig festzulegen. Zum Beispiel, wenn das Formular zur Bearbeitung von Datensätzen dient. Wir lesen den Datensatz aus der Datenbank und setzen die Standardwerte: +Oft ist es nützlich, die Standardwerte für alle Elemente auf einmal zu setzen. Zum Beispiel, wenn das Formular zum Bearbeiten von Datensätzen dient. Wir lesen den Datensatz aus der Datenbank und setzen die Standardwerte: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Rufen Sie `setDefaults()` erst nach der Definition der Elemente auf. +Rufen Sie `setDefaults()` nach dem Definieren der Elemente auf. + +Bei einem bereits abgesendeten Formular hat `setDefaults()` keine Wirkung - es überschreibt nicht, was der Benutzer ausgefüllt hat, es ist also gefahrlos, es in der Factory des Formulars ohne Bedingung aufzurufen. Wenn Sie die Werte auch nach dem Absenden erzwingen müssen, verwenden Sie stattdessen `setValues()`. Rendering des Formulars ======================= -Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Elemente erfüllen die grundlegende Zugänglichkeitsregel – alle Beschriftungen sind als `<label>` geschrieben und mit dem entsprechenden Formularelement verknüpft. Beim Klicken auf die Beschriftung erscheint der Cursor automatisch im Formularfeld. +Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Elemente halten die grundlegenden Regeln der Barrierefreiheit ein - alle Labels sind als `<label>`-Elemente geschrieben und dem jeweiligen Formularelement zugeordnet. Ein Klick auf das Label setzt den Cursor automatisch in das Formularfeld. -Jedem Element können wir beliebige HTML-Attribute zuweisen. Zum Beispiel einen Platzhalter hinzufügen: +Für jedes Element können wir beliebige HTML-Attribute setzen. Fügen wir zum Beispiel einen Platzhalter hinzu: ```php $form->addInteger('age', 'Alter:') - ->setHtmlAttribute('placeholder', 'Bitte geben Sie Ihr Alter an'); + ->setHtmlAttribute('placeholder', 'Bitte geben Sie das Alter an'); ``` -Es gibt wirklich viele Möglichkeiten, ein Formular zu rendern, daher ist dem ein [eigenes Kapitel über Rendering|rendering] gewidmet. +Es gibt wirklich viele Wege, ein Formular zu rendern, deshalb ist ihm ein [eigenes Kapitel über das Rendering|rendering] gewidmet. Mapping auf Klassen =================== -Kehren wir zur Methode `formSucceeded()` zurück, die im zweiten Parameter `$data` die gesendeten Daten als `ArrayHash`-Objekt (oder `stdClass`) erhält. Da es sich um eine generische Klasse handelt, fehlt uns bei der Arbeit damit ein gewisser Komfort, wie z. B. die Autovervollständigung von Eigenschaften in Editoren oder die statische Codeanalyse. Dies könnte gelöst werden, indem wir für jedes Formular eine spezifische Klasse hätten, deren Eigenschaften die einzelnen Elemente repräsentieren. Z. B.: +Kehren wir zur Methode `formSucceeded()` zurück, die die gesendeten Daten im zweiten Parameter `$data` als Objekt `ArrayHash` (oder `stdClass`) bekommt. Weil es eine generische Klasse ist, ähnlich `stdClass`, fehlt uns bei der Arbeit damit einiger Komfort, etwa die Vervollständigung der Properties im Editor oder die statische Analyse des Codes. Lösen ließe sich das, indem es für jedes Formular eine eigene Klasse gibt, deren Properties die einzelnen Elemente darstellen. Zum Beispiel: ```php class RegistrationFormData @@ -229,7 +233,7 @@ class RegistrationFormData } ``` -Alternativ können Sie den Konstruktor verwenden (seit PHP 8.0 mit Property Promotion): +Alternativ können Sie einen Konstruktor verwenden: ```php class RegistrationFormData @@ -243,29 +247,31 @@ class RegistrationFormData } ``` -Die Eigenschaften der Datenklasse können auch Enums sein und werden automatisch zugeordnet. .{data-version:3.2.4} +Die Properties der Datenklasse können auch Enums sein, sie werden automatisch gemappt. .{data-version:3.2.4} -Wie sagen wir Nette, dass es uns Daten als Objekte dieser Klasse zurückgeben soll? Einfacher als Sie denken. Es genügt, die Klasse als Typ des Parameters `$data` in der Handler-Methode anzugeben: +Wie sagen wir Nette, dass es die Daten als Objekte dieser Klasse zurückgeben soll? Einfacher, als Sie denken. Geben Sie die Klasse einfach als Typ des Parameters `$data` in der Methode des Handlers an: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void { - // $name ist eine Instanz von RegistrationFormData + // $data ist eine Instanz von RegistrationFormData $name = $data->name; // ... } ``` -Als Typ kann auch `array` angegeben werden, dann werden die Daten als assoziatives Array übergeben. +Als Typ lässt sich auch `array` angeben, dann werden die Daten als Array übergeben. -Auf ähnliche Weise kann auch die Methode `getValues()` verwendet werden, der wir den Klassennamen oder ein Objekt zur Hydratisierung als Parameter übergeben: +Ebenso lässt sich die Methode `getValues()` verwenden, der Sie den Namen der Klasse oder ein zu füllendes Objekt als Parameter übergeben: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Wenn Formulare eine mehrstufige Struktur aus Containern bilden, erstellen Sie für jeden eine separate Klasse: +Wenn Sie die Werte lesen müssen, bevor das Formular validiert ist - typischerweise in einem `onValidate`-Handler -, verwenden Sie stattdessen die Methode `getUntrustedValues()`. Sie nimmt dieselben Parameter entgegen wie `getValues()`, gibt die gesendeten Werte aber ohne die Garantie zurück, dass sie die Validierung bestanden haben. + +Haben die Formulare eine mehrstufige Struktur aus Containern, legen Sie für jeden eine eigene Klasse an: ```php $form = new Form; @@ -287,93 +293,95 @@ class RegistrationFormData } ``` -Das Mapping erkennt dann am Typ der Eigenschaft `$person`, dass der Container auf die Klasse `PersonFormData` abgebildet werden soll. Wenn die Eigenschaft ein Array von Containern enthalten würde, geben Sie den Typ `array` an und übergeben Sie die Mapping-Klasse direkt an den Container: +Das Mapping leitet dann aus dem Typ der Property `$person` ab, dass es den Container auf die Klasse `PersonFormData` mappen soll. Würde die Property ein Array von Containern enthalten, geben Sie den Typ `array` an und übergeben die zu mappende Klasse direkt dem Container: ```php $person->setMappedType(PersonFormData::class); ``` -Den Entwurf der Datenklasse des Formulars können Sie sich mit der Methode `Nette\Forms\Blueprint::dataClass($form)` generieren lassen, die ihn auf der Browserseite ausgibt. Den Code können Sie dann einfach per Klick markieren und in Ihr Projekt kopieren. .{data-version:3.1.15} +Einen Vorschlag für die Datenklasse des Formulars können Sie sich über die Methode `Nette\Forms\Blueprint::dataClass($form)` erzeugen lassen, die ihn auf der Seite im Browser ausgibt. Dann markieren Sie den Code einfach mit einem Klick und kopieren ihn in Ihr Projekt. .{data-version:3.1.15} -Mehrere Schaltflächen -===================== +Mehrere Absende-Buttons +======================= -Wenn ein Formular mehr als eine Schaltfläche hat, müssen wir in der Regel unterscheiden, welche davon gedrückt wurde. Wir können für jede Schaltfläche eine eigene Handler-Funktion erstellen. Wir setzen sie als Handler für das [Ereignis |nette:glossary#Events Ereignisse] `onClick`: +Hat ein Formular mehr als einen Button, müssen wir üblicherweise unterscheiden, welcher davon gedrückt wurde. Für jeden Button können wir eine eigene Handler-Funktion schreiben. Setzen Sie sie als Handler für das [Event |nette:glossary#Events] `onClick`: ```php $form->addSubmit('save', 'Speichern') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Löschen') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -Diese Handler werden nur im Falle eines gültig ausgefüllten Formulars aufgerufen, genau wie beim `onSuccess`-Ereignis. Der Unterschied besteht darin, dass als erster Parameter anstelle des Formulars die sendende Schaltfläche übergeben werden kann, abhängig vom Typ, den Sie angeben: +.{data-version:3.3.0} +Ein Handler lässt sich dem Button auch direkt als drittes Argument der Methode `addSubmit()` übergeben. + +Diese Handler werden ebenso wie das Event `onSuccess` nur dann aufgerufen, wenn das Formular gültig ausgefüllt ist (sofern die Validierung für den Button nicht abgeschaltet ist). Der Unterschied ist, dass als erster Parameter statt des Formulars das Objekt des Absende-Buttons übergeben werden kann, je nachdem, welche Typdeklaration Sie angeben: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Wenn das Formular mit der <kbd>Enter</kbd>-Taste gesendet wird, wird dies so behandelt, als ob es mit der ersten Schaltfläche gesendet wurde. +Wird das Formular durch Drücken der Taste <kbd>Enter</kbd> abgesendet, wird das behandelt, als wäre es über den ersten Absende-Button abgesendet worden. -Ereignis onAnchor -================= +Event onAnchor +============== -Wenn wir in der Factory-Methode (wie z. B. `createComponentRegistrationForm`) das Formular zusammenstellen, weiß es noch nicht, ob es gesendet wurde oder mit welchen Daten. Es gibt jedoch Fälle, in denen wir die gesendeten Werte kennen müssen, z. B. wenn sich die weitere Form des Formulars danach richtet oder wir sie für abhängige Select-Boxen benötigen usw. +Wenn Sie ein Formular in einer Factory-Methode bauen (etwa `createComponentRegistrationForm`), weiß es noch nicht, ob es abgesendet wurde und mit welchen Daten. Es gibt aber Fälle, in denen wir die gesendeten Werte kennen müssen, weil vielleicht das Aussehen des Formulars von ihnen abhängt oder sie für voneinander abhängige Select-Boxen gebraucht werden. -Den Teil des Codes, der das Formular zusammenstellt, können Sie daher erst aufrufen lassen, wenn es sogenannte verankert ist, d. h. bereits mit dem Presenter verbunden ist und seine gesendeten Daten kennt. Einen solchen Code übergeben wir an das Array `$onAnchor`: +Sie können den Code, der das Formular baut, deshalb erst dann aufrufen lassen, wenn es "verankert" ist, also bereits mit dem Presenter verbunden ist und seine gesendeten Daten kennt. Legen Sie solchen Code in das Array `$onAnchor`: ```php -$country = $form->addSelect('country', 'Staat:', $this->model->getCountries()); +$country = $form->addSelect('country', 'Land:', $this->model->getCountries()); $city = $form->addSelect('city', 'Stadt:'); $form->onAnchor[] = function () use ($country, $city) { - // diese Funktion wird erst aufgerufen, wenn das Formular weiß, ob es gesendet wurde und mit welchen Daten - // es kann also die Methode getValue() verwendet werden + // diese Funktion wird aufgerufen, wenn das Formular die Daten kennt, mit denen es abgesendet wurde + // Sie können also die Methode getValue() verwenden $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; ``` -Schutz vor Schwachstellen -========================= +Schutz vor Sicherheitslücken +============================ -Das Nette Framework legt großen Wert auf Sicherheit und achtet daher sorgfältig auf die gute Absicherung von Formularen. Dies geschieht völlig transparent und erfordert keine manuelle Konfiguration. +Das Nette Framework legt großen Wert auf Sicherheit und achtet deshalb gewissenhaft auf die Sicherheit von Formularen. Es tut das vollkommen transparent und verlangt keine manuelle Einrichtung. -Neben dem Schutz von Formularen vor [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS]- und [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF]-Angriffen bietet es viele kleine Sicherungen, an die Sie nicht mehr denken müssen. +Neben dem Schutz von Formularen vor Angriffen wie [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] und [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)] trifft es viele kleine Sicherheitsmaßnahmen, an die Sie nicht mehr denken müssen. -So filtert es beispielsweise alle Steuerzeichen aus den Eingaben und überprüft die Gültigkeit der UTF-8-Kodierung, sodass die Daten aus dem Formular immer sauber sind. Bei Select-Boxen und Radio-Listen wird überprüft, ob die ausgewählten Elemente tatsächlich zu den angebotenen gehörten und keine Manipulation stattgefunden hat. Wir haben bereits erwähnt, dass bei einzeiligen Texteingaben Zeilenendezeichen entfernt werden, die ein Angreifer senden könnte. Bei mehrzeiligen Eingaben werden wiederum die Zeilenendezeichen normalisiert. Und so weiter. +So filtert es zum Beispiel alle Steuerzeichen aus den Eingaben und prüft die Gültigkeit der UTF-8-Kodierung, sodass die Daten aus dem Formular immer sauber sind. Bei Select-Boxen und Radio-Listen prüft es, dass die ausgewählten Elemente tatsächlich zu den angebotenen gehörten und keine Fälschung stattgefunden hat. Wir haben bereits erwähnt, dass es bei einzeiligen Textfeldern die Zeilenumbruchzeichen, die ein Angreifer senden könnte, durch Leerzeichen ersetzt. Bei mehrzeiligen Eingaben vereinheitlicht es die Zeilenumbruchzeichen. Und so weiter. -Nette löst für Sie Sicherheitsprobleme, von denen viele Programmierer nicht einmal wissen, dass sie existieren. +Nette kümmert sich für Sie um Sicherheitsrisiken, von deren Existenz viele Programmierer nicht einmal wissen. -Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer das Opfer auf eine Seite lockt, die unbemerkt im Browser des Opfers eine Anfrage an den Server stellt, bei dem das Opfer angemeldet ist, und der Server annimmt, dass die Anfrage vom Opfer selbst ausgeführt wurde. Daher verhindert Nette standardmäßig das Senden von POST-Formularen von einer anderen Domain. Wenn Sie aus irgendeinem Grund den Schutz deaktivieren und das Senden von Formularen von einer anderen Domain erlauben möchten, verwenden Sie: +Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer ein Opfer auf eine Seite lockt, die im Browser des Opfers unbemerkt einen Request an den Server ausführt, auf dem das Opfer angemeldet ist. Der Server glaubt dann, das Opfer habe den Request freiwillig ausgelöst. Nette weist deshalb POST-Formulare ab, die von einem fremden Origin abgesendet wurden; auch eine andere Subdomain derselben Website gilt als fremd. Wenn Sie das Absenden von einem anderen Origin erlauben müssen, schalten Sie den Schutz ab mit: ```php -$form->allowCrossOrigin(); // ACHTUNG! Deaktiviert den Schutz! +$form->allowCrossOrigin(); // ACHTUNG! Schaltet den Schutz vollständig ab! ``` -Dieser Schutz verwendet ein SameSite-Cookie namens `_nss`. Der Schutz durch SameSite-Cookies ist möglicherweise nicht 100 % zuverlässig, daher ist es ratsam, auch den Schutz durch ein Token zu aktivieren: +Damit ist der Schutz allerdings für jeden Origin abgeschaltet. Um nur bestimmte Origins zu erlauben, schalten Sie den Schutz ab und prüfen den Header `Origin` selbst gegen eine eigene Allowlist. -```php -$form->addProtection(); -``` +Der Schutz stützt sich auf den Header `Sec-Fetch-Site` des Browsers (Fetch Metadata), den der Browser automatisch sendet und der sich selbst mit einer XSS-Lücke nicht fälschen lässt. Für ältere Browser ohne Unterstützung dafür greift ersatzweise ein SameSite-Cookie, das eine Nette-Anwendung automatisch setzt. Der Artikel [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] beschreibt das ausführlich. -Wir empfehlen, Formulare im Administrationsbereich der Website, die sensible Daten in der Anwendung ändern, auf diese Weise zu schützen. Das Framework wehrt sich gegen CSRF-Angriffe, indem es ein Autorisierungs-Token generiert und überprüft, das in der Session gespeichert wird. Daher muss vor dem Anzeigen des Formulars eine Session geöffnet sein. Im Administrationsbereich der Website ist die Session normalerweise bereits aufgrund der Benutzeranmeldung gestartet. Andernfalls starten Sie die Session mit der Methode `Nette\Http\Session::start()`. +.[note] +Der frühere Schutz über ein Autorisierungstoken in der Session, der über `$form->addProtection()` aktiviert wurde, ist nicht mehr nötig und seit Version 3.3 veraltet. -Gleiches Formular in mehreren Presentern -======================================== +Ein Formular in mehreren Presentern verwenden +============================================= -Wenn Sie ein Formular in mehreren Presentern verwenden müssen, empfehlen wir, dafür eine Factory zu erstellen, die Sie dann an den Presenter übergeben. Ein geeigneter Speicherort für eine solche Klasse ist z. B. das Verzeichnis `app/Forms`. +Wenn Sie dasselbe Formular in mehreren Presentern brauchen, empfehlen wir, dafür eine Factory zu schreiben, die Sie dann in die Presenter injizieren. Ein passender Ort für eine solche Klasse ist zum Beispiel das Verzeichnis `app/Forms`. -Die Factory-Klasse könnte beispielsweise so aussehen: +Die Klasse der Factory könnte so aussehen: ```php use Nette\Application\UI\Form; @@ -390,7 +398,7 @@ class SignInFormFactory } ``` -Wir bitten die Klasse, das Formular in der Factory-Methode für Komponenten im Presenter zu erstellen: +In der Factory-Methode der Komponente im Presenter lassen wir uns von der Klasse das Formular erzeugen: ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // wir können das Formular ändern, hier ändern wir beispielsweise die Beschriftung auf der Schaltfläche + // wir können das Formular ändern, hier ändern wir zum Beispiel das Label des Buttons $form['send']->setCaption('Weiter'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // und fügen einen Handler hinzu + $form->onSuccess[] = $this->signInFormSuceeded(...); // und einen Handler ergänzen return $form; } ``` -Der Handler zur Verarbeitung des Formulars kann auch bereits von der Factory geliefert werden: +Den Handler zum Verarbeiten des Formulars kann auch die Factory selbst bereitstellen: ```php use Nette\Application\UI\Form; @@ -421,11 +429,11 @@ class SignInFormFactory $form->addText('name', 'Name:'); $form->addSubmit('send', 'Anmelden'); $form->onSuccess[] = function (Form $form, $data): void { - // hier führen wir die Verarbeitung des Formulars durch + // hier verarbeiten wir unser abgesendetes Formular }; return $form; } } ``` -So, wir haben eine schnelle Einführung in Formulare in Nette hinter uns. Versuchen Sie, sich noch im Verzeichnis [examples|https://github.com/nette/forms/tree/master/examples] in der Distribution umzusehen, wo Sie weitere Inspiration finden. +Damit haben wir eine kurze Einführung in Formulare in Nette hinter uns. Schauen Sie sich für weitere Anregungen das Verzeichnis [examples |https://github.com/nette/forms/tree/master/examples] in der Distribution an. diff --git a/forms/de/rendering.texy b/forms/de/rendering.texy index 00528b657d..95d32fcacf 100644 --- a/forms/de/rendering.texy +++ b/forms/de/rendering.texy @@ -1,35 +1,35 @@ -Rendern von Formularen -********************** +Rendering von Formularen +************************ -Das Aussehen von Formularen kann sehr vielfältig sein. In der Praxis können wir auf zwei Extreme stoßen. Auf der einen Seite steht die Notwendigkeit, in der Anwendung eine Reihe von Formularen zu rendern, die sich visuell wie ein Ei dem anderen gleichen, und wir schätzen das einfache Rendern ohne Vorlage mit `$form->render()`. Dies ist normalerweise bei Verwaltungsoberflächen der Fall. +Das Aussehen von Formularen kann sehr unterschiedlich sein. In der Praxis begegnen uns zwei Extreme. Auf der einen Seite steht die Notwendigkeit, in einer Anwendung zahlreiche Formulare zu rendern, die optisch identisch sind, und wir wissen das mühelose Rendern ohne Template über `$form->render()` zu schätzen. Typisch ist das bei Verwaltungsoberflächen. -Auf der anderen Seite gibt es vielfältige Formulare, bei denen gilt: jedes ein Unikat ist. Ihre Form beschreiben wir am besten mit der HTML-Sprache in der Formularvorlage. Und natürlich stoßen wir neben den beiden genannten Extremen auf viele Formulare, die sich irgendwo dazwischen bewegen. +Auf der anderen Seite stehen vielfältige Formulare, von denen jedes einzigartig ist. Ihr Aussehen beschreibt man am besten mit HTML im Template des Formulars. Und natürlich begegnen uns neben diesen beiden Extremen viele Formulare, die irgendwo dazwischen liegen. -Rendern mit Latte -================= +Rendering mit Latte +=================== -Das [Latte Template-System |latte:] erleichtert das Rendern von Formularen und ihren Elementen erheblich. Zuerst zeigen wir, wie man Formulare manuell Element für Element rendert und so die volle Kontrolle über den Code erhält. Später zeigen wir, wie man ein solches Rendering [automatisieren |#Automatisches Rendering] kann. +Das [Templating-System Latte |latte:] vereinfacht das Rendern von Formularen und ihren Elementen erheblich. Zuerst zeigen wir, wie sich ein Formular von Hand rendern lässt, Element für Element, um die volle Kontrolle über den Code zu haben. Später zeigen wir, wie sich ein solches Rendern [automatisieren |#Automatisches Rendering] lässt. -Den Entwurf der Latte-Vorlage für ein Formular können Sie sich mit der Methode `Nette\Forms\Blueprint::latte($form)` generieren lassen, die ihn auf der Browserseite ausgibt. Den Code können Sie dann einfach per Klick markieren und in Ihr Projekt kopieren. .{data-version:3.1.15} +Das Latte-Template für ein Formular können Sie sich über die Methode `Nette\Forms\Blueprint::latte($form)` erzeugen lassen, die es auf der Seite im Browser ausgibt. Dann markieren Sie den Code einfach mit einem Klick und kopieren ihn in Ihr Projekt. .{data-version:3.1.15} `{control}` ----------- -Der einfachste Weg, ein Formular zu rendern, ist, im Template zu schreiben: +Am einfachsten rendern Sie ein Formular, indem Sie ins Template schreiben: ```latte {control signInForm} ``` -Das Aussehen des so gerenderten Formulars kann durch Konfiguration des [Renderers |#Renderer] und der [einzelnen Elemente |#HTML-Attribute] beeinflusst werden. +Das Aussehen des gerenderten Formulars lässt sich über die Konfiguration des [#Renderer] und der [einzelnen Elemente |#HTML-Attribute] beeinflussen. `n:name` -------- -Die Definition des Formulars im PHP-Code lässt sich extrem einfach mit dem HTML-Code verknüpfen. Es genügt, die Attribute `n:name` hinzuzufügen. So einfach ist das! +Die Definition des Formulars im PHP-Code mit dem HTML-Code zu verbinden ist außerordentlich einfach. Ergänzen Sie einfach die Attribute `n:name`. So einfach ist das! ```php protected function createComponentSignInForm(): Form @@ -48,7 +48,7 @@ protected function createComponentSignInForm(): Form <label n:name=username>Username: <input n:name=username size=20 autofocus></label> </div> <div> - <label n:name=password>Passwort: <input n:name=password></label> + <label n:name=password>Password: <input n:name=password></label> </div> <div> <input n:name=send class="btn btn-default"> @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Die Form des resultierenden HTML-Codes liegt vollständig in Ihren Händen. Wenn Sie das Attribut `n:name` bei den Elementen `<select>`, `<button>` oder `<textarea>` verwenden, wird ihr innerer Inhalt automatisch ergänzt. Das Tag `<form n:name>` erstellt außerdem eine lokale Variable `$form` mit dem Objekt des gerenderten Formulars, und das schließende `</form>` rendert alle nicht gerenderten versteckten Elemente (dasselbe gilt auch für `{form} ... {/form}`). +Sie haben die volle Kontrolle über das Aussehen des entstehenden HTML-Codes. Verwenden Sie das Attribut `n:name` bei den Elementen `<select>`, `<button>` oder `<textarea>`, wird deren innerer Inhalt automatisch gefüllt. Außerdem erzeugt der Tag `<form n:name>` die lokale Variable `$form` mit dem Objekt des gerenderten Formulars, und der schließende Tag `</form>` rendert alle noch nicht gerenderten versteckten Elemente (dasselbe gilt für `{form} ... {/form}`). -Wir dürfen jedoch nicht vergessen, mögliche Fehlermeldungen zu rendern. Sowohl diejenigen, die mit der Methode `addError()` zu einzelnen Elementen hinzugefügt wurden (mithilfe von `{inputError}`), als auch diejenigen, die direkt zum Formular hinzugefügt wurden (von `$form->getOwnErrors()` zurückgegeben): +Wir dürfen jedoch nicht vergessen, mögliche Fehlermeldungen auszugeben. Das betrifft sowohl Fehler, die den einzelnen Elementen über die Methode `addError()` hinzugefügt wurden (gerendert über `{inputError}`), als auch Fehler, die direkt dem Formular hinzugefügt wurden (die `$form->getOwnErrors()` zurückgibt): ```latte <form n:name=signInForm class=form> @@ -71,7 +71,7 @@ Wir dürfen jedoch nicht vergessen, mögliche Fehlermeldungen zu rendern. Sowohl <span class=error n:ifcontent>{inputError username}</span> </div> <div> - <label n:name=password>Passwort: <input n:name=password></label> + <label n:name=password>Password: <input n:name=password></label> <span class=error n:ifcontent>{inputError password}</span> </div> <div> @@ -80,7 +80,7 @@ Wir dürfen jedoch nicht vergessen, mögliche Fehlermeldungen zu rendern. Sowohl </form> ``` -Komplexere Formularelemente wie RadioList oder CheckboxList können so Element für Element gerendert werden: +Komplexere Formularelemente wie RadioList oder CheckboxList lassen sich Element für Element so rendern: ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ Komplexere Formularelemente wie RadioList oder CheckboxList können so Element f `{label}` `{input}` ------------------- -Möchten Sie nicht bei jedem Element überlegen, welches HTML-Element Sie im Template verwenden sollen, ob `<input>`, `<textarea>` usw.? Die Lösung ist das universelle Tag `{input}`: +Sie möchten lieber nicht darüber nachdenken, welches HTML-Element Sie im Template für welches Formularelement verwenden, ob `<input>`, `<textarea>` und so weiter? Die Lösung ist der universelle Tag `{input}`: ```latte <form n:name=signInForm class=form> @@ -105,7 +105,7 @@ Möchten Sie nicht bei jedem Element überlegen, welches HTML-Element Sie im Tem {inputError username} </div> <div> - {label password}Passwort: {input password}{/label} + {label password}Password: {input password}{/label} {inputError password} </div> <div> @@ -114,9 +114,9 @@ Möchten Sie nicht bei jedem Element überlegen, welches HTML-Element Sie im Tem </form> ``` -Wenn das Formular einen Translator verwendet, wird der Text innerhalb der `{label}`-Tags übersetzt. +Verwendet das Formular einen Übersetzer, werden die Labels aus der Definition des Formulars (etwa `{label username /}`) übersetzt. Text, der direkt zwischen den Tags `{label}` und `{/label}` steht, wird es nicht. -Auch in diesem Fall können komplexere Formularelemente wie RadioList oder CheckboxList Element für Element gerendert werden: +Auch hier lassen sich komplexere Formularelemente wie RadioList oder CheckboxList Element für Element rendern: ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ Auch in diesem Fall können komplexere Formularelemente wie RadioList oder Check {/foreach} ``` -Zum Rendern des reinen `<input>` in einem Checkbox-Element verwenden Sie `{input myCheckbox:}`. HTML-Attribute trennen Sie in diesem Fall immer mit einem Komma `{input myCheckbox:, class: required}`. +Um bei einem Checkbox-Element nur das `<input>` zu rendern, verwenden Sie `{input myCheckbox:}`. Trennen Sie die HTML-Attribute in diesem Fall immer mit einem Komma: `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Gibt die Fehlermeldung für ein Formularelement aus, falls eine vorhanden ist. Die Meldung wird normalerweise zur Gestaltung in ein HTML-Element verpackt. Das Rendern eines leeren Elements, wenn keine Meldung vorhanden ist, lässt sich elegant mit `n:ifcontent` verhindern: +Gibt die Fehlermeldung eines Formularelements aus, sofern es eine gibt. Die Meldung wird üblicherweise in ein HTML-Element gepackt, damit sie sich stylen lässt. Dass bei fehlender Meldung kein leeres Element gerendert wird, erreichen Sie elegant mit `n:ifcontent`: ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -Das Vorhandensein eines Fehlers können wir mit der Methode `hasErrors()` feststellen und entsprechend die Klasse des übergeordneten Elements setzen: +Ob ein Fehler vorliegt, prüfen wir über die Methode `hasErrors()` und setzen danach die Klasse des übergeordneten Elements: ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ Das Vorhandensein eines Fehlers können wir mit der Methode `hasErrors()` festst `{form}` -------- -Die Tags `{form signInForm}...{/form}` sind eine Alternative zu `<form n:name="signInForm">...</form>`. +Die Tags `{form signInForm}...{/form}` sind eine Alternative zu `<form n:name="signInForm">...</form>`. Trennen Sie etwaige Argumente vom Namen mit einem Komma: `{form signInForm, class: foo}`. + +.{data-version:3.3.0} +Das Schlüsselwort `scope` vor dem Namen legt das Formular nur auf den Stapel (sodass sich `{input}`, `{label}` und so weiter daran binden), rendert aber den Tag `<form>` nicht. Praktisch ist das, um einen Teil eines Formulars zu rendern, etwa in einem Snippet. Ist bereits ein Formular aktiv, wird der Name relativ dazu aufgelöst, `{form scope}` ersetzt also auch `{formContainer}`: + +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +Das Schlüsselwort `detached` rendert ein leeres `<form></form>` und verbindet jedes Element über das HTML-Attribut `form` damit. So können Sie ein Formular in ein anderes Formular setzen, was HTML sonst verbietet. Das losgelöste Formular muss ein HTML-`id` haben, das automatisch entsteht, wenn Sie ihm einen Namen geben (wie `outerForm` unten): + +```latte +{form detached outerForm} + ... +{/form} +``` Automatisches Rendering ----------------------- -Dank der Tags `{input}` und `{label}` können wir leicht eine allgemeine Vorlage für jedes beliebige Formular erstellen. Es wird nacheinander alle seine Elemente iterieren und rendern, außer den versteckten Elementen, die beim Schließen des Formulars mit dem Tag `</form>` automatisch gerendert werden. Der Name des zu rendernden Formulars wird in der Variablen `$form` erwartet. +Dank der Tags `{input}` und `{label}` können wir leicht ein allgemeines Template für ein beliebiges Formular schreiben. Es durchläuft alle seine Elemente und rendert sie, ausgenommen die versteckten Elemente, die automatisch gerendert werden, wenn das Formular mit dem Tag `</form>` geschlossen wird. Es erwartet den Namen des zu rendernden Formulars in der Variablen `$form`. ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ Dank der Tags `{input}` und `{label}` können wir leicht eine allgemeine Vorlage </form> ``` -Die verwendeten selbstschließenden paarigen Tags `{label .../}` zeigen Beschriftungen an, die aus der Formulardefinition im PHP-Code stammen. +Die hier verwendeten selbstschließenden Paar-Tags `{label .../}` geben die Labels aus, die aus der Definition des Formulars im PHP-Code stammen. -Speichern Sie diese allgemeine Vorlage beispielsweise in der Datei `basic-form.latte` und zum Rendern des Formulars genügt es, sie zu inkludieren und den Namen (oder die Instanz) des Formulars an den Parameter `$form` zu übergeben: +Speichern Sie dieses allgemeine Template zum Beispiel in der Datei `basic-form.latte`. Um ein Formular zu rendern, binden Sie sie einfach ein und übergeben den Namen des Formulars (oder die Instanz) im Parameter `$form`: ```latte {include basic-form.latte, form: signInForm} ``` -Wenn Sie beim Rendern eines bestimmten Formulars in dessen Form eingreifen und beispielsweise ein Element anders rendern möchten, ist der einfachste Weg, im Template Blöcke vorzubereiten, die anschließend überschrieben werden können. Blöcke können auch [dynamische Namen |latte:template-inheritance#Dynamische Blocknamen] haben, sodass man auch den Namen des zu rendernden Elements einfügen kann. Zum Beispiel: +Wenn Sie das Aussehen eines bestimmten Formulars beim Rendern anpassen wollen, etwa ein Element anders rendern, bereiten Sie am einfachsten im Template Blöcke vor, die sich anschließend überschreiben lassen. Blöcke können auch [dynamische Namen |latte:template-inheritance#Dynamische Blocknamen] haben, sodass sich der Name des gerenderten Elements einsetzen lässt. Zum Beispiel: ```latte ... @@ -189,7 +207,7 @@ Wenn Sie beim Rendern eines bestimmten Formulars in dessen Form eingreifen und b ... ``` -Für ein Element z. B. `username` entsteht so der Block `input-username`, der leicht mit dem Tag [{embed} |latte:template-inheritance#Einheiten-Vererbung] überschrieben werden kann: +Für ein Element namens etwa `username` entsteht so der Block `input-username`, der sich mit dem Tag [{embed} |latte:template-inheritance#Einheiten-Vererbung] leicht überschreiben lässt: ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ Für ein Element z. B. `username` entsteht so der Block `input-username`, der le {/embed} ``` -Alternativ kann der gesamte Inhalt der Vorlage `basic-form.latte` als Block [definiert |latte:template-inheritance#Definition] werden, einschließlich des Parameters `$form`: +Alternativ lässt sich der gesamte Inhalt des Templates `basic-form.latte` als Block [definieren |latte:template-inheritance#Definitionen], samt dem Parameter `$form`: ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ Alternativ kann der gesamte Inhalt der Vorlage `basic-form.latte` als Block [def {/define} ``` -Dadurch wird sein Aufruf etwas einfacher: +Dadurch wird der Aufruf etwas einfacher: ```latte {embed basic-form, signInForm} @@ -219,7 +237,7 @@ Dadurch wird sein Aufruf etwas einfacher: {/embed} ``` -Den Block genügt es dabei an einer einzigen Stelle zu importieren, und zwar am Anfang der Layout-Vorlage: +Den Block müssen Sie nur an einer Stelle importieren, am Anfang des Layout-Templates: ```latte {import basic-form.latte} @@ -229,7 +247,7 @@ Den Block genügt es dabei an einer einzigen Stelle zu importieren, und zwar am Spezialfälle ------------ -Wenn Sie nur den inneren Teil des Formulars ohne die HTML-Tags `<form>` rendern müssen, beispielsweise beim Senden von Snippets, verbergen Sie sie mit dem Attribut `n:tag-if`: +Wenn Sie nur den inneren Teil des Formulars ohne die HTML-Tags `<form>` rendern müssen, etwa beim Senden von Snippets, verstecken Sie sie mit dem Attribut `n:tag-if`: ```latte <form n:name=signInForm n:tag-if=false> @@ -240,10 +258,10 @@ Wenn Sie nur den inneren Teil des Formulars ohne die HTML-Tags `<form>` rendern </form> ``` -Beim Rendern von Elementen innerhalb eines Formularcontainers hilft das Tag `{formContainer}`. +Beim Rendern der Elemente innerhalb eines Formular-Containers hilft der Tag `{formContainer}` oder das neuere [`{form scope}` |#{form}]. ```latte -<p>Welche Nachrichten möchten Sie erhalten:</p> +<p>Which news you wish to receive:</p> {formContainer emailNews} <ul> @@ -254,42 +272,42 @@ Beim Rendern von Elementen innerhalb eines Formularcontainers hilft das Tag `{fo ``` -Rendern ohne Latte -================== +Rendering ohne Latte +==================== -Der einfachste Weg, ein Formular zu rendern, ist der Aufruf: +Am einfachsten rendern Sie ein Formular mit dem Aufruf: ```php $form->render(); ``` -Das Aussehen des so gerenderten Formulars kann durch Konfiguration des [Renderers |#Renderer] und der [einzelnen Elemente |#HTML-Attribute] beeinflusst werden. +Das Aussehen des gerenderten Formulars lässt sich über die Konfiguration des [#Renderer] und der [einzelnen Elemente |#HTML-Attribute] beeinflussen. -Manuelles Rendern ------------------ +Manuelles Rendering +------------------- -Jedes Formularelement verfügt über Methoden, die den HTML-Code des Formularfeldes und der Beschriftung generieren. Sie können ihn entweder als Zeichenkette oder als [Nette\Utils\Html |utils:html-elements]-Objekt zurückgeben: +Jedes Formularelement hat Methoden, die den HTML-Code des Formularfelds und seines Labels erzeugen. Sie können ihn entweder als String oder als Objekt [Nette\Utils\Html |utils:html-elements] zurückgeben: - `getControl(): Html|string` gibt den HTML-Code des Elements zurück -- `getLabel($caption = null): Html|string|null` gibt den HTML-Code der Beschriftung zurück, falls vorhanden +- `getLabel($caption = null): Html|string|null` gibt den HTML-Code des Labels zurück, sofern es eines gibt -Das Formular kann so Element für Element gerendert werden: +So lässt sich das Formular Element für Element rendern: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,21 +315,21 @@ Das Formular kann so Element für Element gerendert werden: <?php $form->render('end') ?> ``` -Während bei einigen Elementen `getControl()` ein einzelnes HTML-Element zurückgibt (z. B. `<input>`, `<select>` usw.), gibt es bei anderen ein ganzes Stück HTML-Code zurück (CheckboxList, RadioList). In diesem Fall können Sie Methoden verwenden, die einzelne Eingaben und Beschriftungen für jedes Element separat generieren: +Während `getControl()` bei manchen Elementen ein einzelnes HTML-Element zurückgibt (etwa `<input>`, `<select>` und so weiter), liefert es bei anderen ein vollständiges Stück HTML-Code (CheckboxList, RadioList). In solchen Fällen können Sie Methoden verwenden, die die einzelnen Inputs und Labels für jedes Element getrennt erzeugen: -- `getControlPart($key = null): ?Html` gibt den HTML-Code eines einzelnen Elements zurück -- `getLabelPart($key = null): ?Html` gibt den HTML-Code der Beschriftung eines einzelnen Elements zurück +- `getControlPart($key = null): Html` gibt den HTML-Code eines einzelnen Elements zurück +- `getLabelPart($key = null): Html` gibt den HTML-Code des Labels eines einzelnen Elements zurück .[note] -Diese Methoden haben aus historischen Gründen das Präfix `get`, aber `generate` wäre besser, da bei jedem Aufruf ein neues `Html`-Element erstellt und zurückgegeben wird. +Diese Methoden tragen aus historischen Gründen das Präfix `get`, passender wäre `generate`, denn sie erzeugen bei jedem Aufruf ein neues `Html`-Element und geben es zurück. Renderer ======== -Dies ist ein Objekt, das das Rendern des Formulars sicherstellt. Es kann mit der Methode `$form->setRenderer` festgelegt werden. Ihm wird die Kontrolle übergeben, wenn die Methode `$form->render()` aufgerufen wird. +Das ist ein Objekt, das für das Rendern des Formulars zuständig ist. Setzen lässt es sich über die Methode `$form->setRenderer()`. Die Kontrolle wird ihm beim Aufruf der Methode `$form->render()` übergeben. -Wenn wir keinen eigenen Renderer festlegen, wird der Standard-Renderer [api:Nette\Forms\Rendering\DefaultFormRenderer] verwendet. Dieser rendert die Formularelemente in Form einer HTML-Tabelle. Die Ausgabe sieht so aus: +Setzen wir keinen eigenen Renderer, wird der Standard-Renderer [api:Nette\Forms\Rendering\DefaultFormRenderer] verwendet. Er rendert die Formularelemente in eine HTML-Tabelle. Die Ausgabe sieht so aus: ```latte <table> @@ -332,11 +350,11 @@ Wenn wir keinen eigenen Renderer festlegen, wird der Standard-Renderer [api:Nett ... ``` -Ob man für das Formulargerüst eine Tabelle verwenden soll oder nicht, ist umstritten, und viele Webdesigner bevorzugen ein anderes Markup. Zum Beispiel eine Definitionsliste. Wir konfigurieren daher den `DefaultFormRenderer` so um, dass er das Formular als Liste rendert. Die Konfiguration erfolgt durch Bearbeitung des Feldes [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Der erste Index stellt immer den Bereich dar und der zweite sein Attribut. Die einzelnen Bereiche zeigt das Bild: +Ob man für die Struktur eines Formulars eine Tabelle verwendet, ist umstritten, und viele Webdesigner bevorzugen ein anderes Markup, etwa eine Definitionsliste. Wir konfigurieren den `DefaultFormRenderer` deshalb so um, dass er das Formular als Liste rendert. Konfiguriert wird über das Bearbeiten des Arrays [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Der erste Index steht immer für einen Bereich, der zweite für dessen Attribut. Die einzelnen Bereiche zeigt das Bild: -[* defaultformrenderer.webp *] +[* form-areas-en.webp *] -Standardmäßig ist die Gruppe der Elemente `controls` von einer Tabelle `<table>` umschlossen, jedes `pair` stellt eine Tabellenzeile `<tr>` dar, und die Paare `label` und `control` sind Zellen `<th>` und `<td>`. Nun ändern wir die umschließenden Elemente. Den Bereich `controls` legen wir in einen Container `<dl>`, den Bereich `pair` lassen wir ohne Container, `label` legen wir in `<dt>` und schließlich `control` umschließen wir mit `<dd>`-Tags: +Standardmäßig ist die Gruppe `controls` von `<table>` umschlossen, jedes `pair` stellt eine Tabellenzeile `<tr>` dar, und das Paar `label` und `control` sind die Zellen `<th>` und `<td>`. Jetzt ändern wir die umschließenden Elemente. Den Bereich `controls` setzen wir in einen Container `<dl>`, den Bereich `pair` lassen wir ohne Container, das `label` setzen wir in `<dt>` und das `control` umschließen wir schließlich mit den Tags `<dd>`: ```php $renderer = $form->getRenderer(); @@ -348,7 +366,7 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -Das Ergebnis ist dieser HTML-Code: +Daraus entsteht der folgende HTML-Code: ```latte <dl> @@ -367,102 +385,102 @@ Das Ergebnis ist dieser HTML-Code: </dl> ``` -Im Wrappers-Array können viele weitere Attribute beeinflusst werden: +Über das Array wrappers lassen sich viele weitere Eigenschaften beeinflussen: - CSS-Klassen zu einzelnen Typen von Formularelementen hinzufügen -- ungerade und gerade Zeilen durch CSS-Klassen unterscheiden -- Pflichtfelder und optionale Felder visuell unterscheiden -- bestimmen, ob Fehlermeldungen direkt bei den Elementen oder über dem Formular angezeigt werden +- ungerade und gerade Zeilen über CSS-Klassen unterscheiden +- Pflicht- und optionale Elemente optisch unterscheiden +- festlegen, ob die Fehlermeldungen direkt neben den Elementen oder über dem Formular ausgegeben werden Options ------- -Das Verhalten des Renderers kann auch durch das Setzen von *options* auf einzelnen Formularelementen gesteuert werden. So kann eine Beschreibung festgelegt werden, die neben dem Eingabefeld ausgegeben wird: +Das Verhalten des Renderers lässt sich auch über *options* an den einzelnen Formularelementen steuern. So setzen Sie eine Beschreibung, die neben dem Eingabefeld erscheint: ```php $form->addText('phone', 'Nummer:') ->setOption('description', 'Diese Nummer bleibt verborgen'); ``` -Wenn wir darin HTML-Inhalt platzieren möchten, verwenden wir die Klasse [Html |utils:html-elements] +Wollen wir HTML-Inhalt hineinsetzen, verwenden wir die Klasse [Html |utils:html-elements]: ```php use Nette\Utils\Html; -$form->addText('phone', 'Nummer:') +$form->addText('phone', 'Telefon:') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Bedingungen zur Speicherung Ihrer Nummer</a>') + ->setHtml('<a href="...">Nutzungsbedingungen.</a>') ); ``` .[tip] -Ein Html-Element kann auch anstelle eines Labels verwendet werden: `$form->addCheckbox('conditions', $label)`. +Ein Html-Element lässt sich auch statt eines Labels verwenden: `$form->addCheckbox('conditions', $label)`. Gruppierung von Elementen ------------------------- -Der Renderer ermöglicht das Gruppieren von Elementen in visuelle Gruppen (Fieldsets): +Der Renderer erlaubt es, Elemente zu optischen Gruppen (Fieldsets) zusammenzufassen: ```php -$form->addGroup('Persönliche Daten'); +$form->addGroup('Personal data'); ``` -Nachdem eine neue Gruppe erstellt wurde, wird diese aktiv, und jedes neu hinzugefügte Element wird gleichzeitig auch zu ihr hinzugefügt. Das Formular kann also auf diese Weise aufgebaut werden: +Nach dem Anlegen einer neuen Gruppe wird diese aktiv, und jedes neu hinzugefügte Element wird auch ihr hinzugefügt. Das Formular lässt sich also so aufbauen: ```php $form = new Form; -$form->addGroup('Persönliche Daten'); -$form->addText('name', 'Ihr Name:'); -$form->addInteger('age', 'Ihr Alter:'); -$form->addEmail('email', 'E-Mail:'); +$form->addGroup('Personal data'); +$form->addText('name', 'Your name:'); +$form->addInteger('age', 'Your age:'); +$form->addEmail('email', 'Email:'); -$form->addGroup('Lieferadresse'); -$form->addCheckbox('send', 'An Adresse liefern'); -$form->addText('street', 'Straße:'); -$form->addText('city', 'Stadt:'); -$form->addSelect('country', 'Land:', $countries); +$form->addGroup('Shipping address'); +$form->addCheckbox('send', 'Ship to address'); +$form->addText('street', 'Street:'); +$form->addText('city', 'City:'); +$form->addSelect('country', 'Country:', $countries); ``` -Der Renderer rendert zuerst die Gruppen und erst danach die Elemente, die zu keiner Gruppe gehören. +Der Renderer zeichnet zuerst die Gruppen und danach die Elemente, die zu keiner Gruppe gehören. Unterstützung für Bootstrap --------------------------- -[In den Beispielen |https://github.com/nette/forms/tree/master/examples] finden Sie Beispiele, wie Sie den Renderer für [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] und [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] konfigurieren. +Im [Verzeichnis der Beispiele |https://github.com/nette/forms/tree/master/examples] finden Sie Beispiele, wie sich der Renderer für [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] und [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] konfigurieren lässt. HTML-Attribute ============== -Um beliebige HTML-Attribute für Formularelemente festzulegen, verwenden wir die Methode `setHtmlAttribute(string $name, $value = true)`: +Um beliebige HTML-Attribute von Formularelementen zu setzen, verwenden Sie die Methode `setHtmlAttribute(string $name, $value = true)`: ```php $form->addInteger('number', 'Nummer:') ->setHtmlAttribute('class', 'big-number'); $form->addSelect('rank', 'Sortieren nach:', ['Preis', 'Name']) - ->setHtmlAttribute('onchange', 'submit()'); // bei Änderung senden + ->setHtmlAttribute('onchange', 'submit()'); // bei Änderung das Formular absenden -// Um Attribute des <form>-Elements selbst festzulegen +// Um die Attribute des <form>-Elements selbst zu setzen $form->setHtmlAttribute('id', 'myForm'); ``` -Spezifikation des Elementtyps: +Angabe des Typs eines Elements: ```php $form->addText('tel', 'Ihr Telefon:') ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'Telefonnummer eingeben'); + ->setHtmlAttribute('placeholder', 'Bitte geben Sie Ihr Telefon an'); ``` .[warning] -Die Einstellung des Typs und anderer Attribute dient nur zu visuellen Zwecken. Die Überprüfung der Richtigkeit der Eingaben muss serverseitig erfolgen, was durch die Wahl des geeigneten [Formularelements |controls] und die Angabe von [Validierungsregeln |validation] sichergestellt wird. +Das Setzen des Typs und weiterer Attribute dient nur der Optik. Die Prüfung der Richtigkeit der Eingaben muss auf der Serverseite geschehen, wofür Sie mit der Wahl eines passenden [Formularelements |controls] und der Angabe von [Validierungsregeln |validation] sorgen. -Einzelnen Elementen in Radio- oder Checkbox-Listen können wir HTML-Attribute mit unterschiedlichen Werten für jedes von ihnen zuweisen. Beachten Sie den Doppelpunkt nach `style:`, der die Auswahl des Wertes nach Schlüssel sicherstellt: +Bei den einzelnen Elementen von Radio- oder Checkbox-Listen können wir ein HTML-Attribut mit für jedes unterschiedlichen Werten setzen. Beachten Sie den Doppelpunkt hinter `style:`, der dafür sorgt, dass der Wert anhand des Schlüssels ausgewählt wird: ```php $colors = ['r' => 'rot', 'g' => 'grün', 'b' => 'blau']; @@ -479,11 +497,11 @@ Gibt aus: <label><input type="checkbox" name="colors[]" value="b">blau</label> ``` -Um logische Attribute wie `readonly` festzulegen, können wir eine Schreibweise mit Fragezeichen verwenden: +Für das Setzen boolescher Attribute wie `readonly` können wir die Schreibweise mit einem Fragezeichen verwenden: ```php $form->addCheckboxList('colors', 'Farben:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // für mehrere Schlüssel verwenden Sie ein Array, z. B. ['r', 'g'] + ->setHtmlAttribute('readonly?', 'r'); // für mehrere Schlüssel ein Array verwenden, etwa ['r', 'g'] ``` Gibt aus: @@ -494,7 +512,7 @@ Gibt aus: <label><input type="checkbox" name="colors[]" value="b">blau</label> ``` -Bei Selectboxen setzt die Methode `setHtmlAttribute()` Attribute des `<select>`-Elements. Wenn wir Attribute für einzelne `<option>` setzen möchten, verwenden wir die Methode `setOptionAttribute()`. Auch Schreibweisen mit Doppelpunkt und Fragezeichen, wie oben erwähnt, funktionieren: +Bei Select-Boxen setzt die Methode `setHtmlAttribute()` die Attribute des Elements `<select>`. Wollen wir die Attribute der einzelnen `<option>`-Elemente setzen, verwenden wir die Methode `setOptionAttribute()`. Die oben erwähnten Schreibweisen mit Doppelpunkt und Fragezeichen funktionieren auch hier: ```php $form->addSelect('colors', 'Farben:', $colors) @@ -515,7 +533,7 @@ Gibt aus: Prototypen ---------- -Eine alternative Methode zum Festlegen von HTML-Attributen besteht darin, die Vorlage zu ändern, aus der das HTML-Element generiert wird. Die Vorlage ist ein `Html`-Objekt und wird von der Methode `getControlPrototype()` zurückgegeben: +Ein alternativer Weg, HTML-Attribute zu setzen, ist, die Vorlage zu verändern, aus der das HTML-Element entsteht. Die Vorlage ist ein `Html`-Objekt und wird von der Methode `getControlPrototype()` zurückgegeben: ```php $input = $form->addInteger('number', 'Nummer:'); @@ -523,14 +541,14 @@ $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -Auf diese Weise kann auch die Vorlage der Beschriftung geändert werden, die von `getLabelPrototype()` zurückgegeben wird: +Ebenso lässt sich die Vorlage des Labels verändern, die `getLabelPrototype()` zurückgibt: ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -Bei den Elementen Checkbox, CheckboxList und RadioList können Sie die Vorlage des Elements beeinflussen, das das gesamte Element umschließt. Sie wird von `getContainerPrototype()` zurückgegeben. Standardmäßig ist dies ein „leeres“ Element, sodass nichts gerendert wird, aber indem wir ihm einen Namen geben, wird es gerendert: +Bei den Elementen Checkbox, CheckboxList und RadioList können Sie die Vorlage des Elements beeinflussen, das das gesamte Element umschließt. Sie gibt `getContainerPrototype()` zurück. Standardmäßig ist es ein "leeres" Element, sodass nichts gerendert wird; geben Sie ihm aber einen Namen, wird es gerendert: ```php $input = $form->addCheckbox('send'); @@ -541,49 +559,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -Im Fall von CheckboxList und RadioList kann auch die Vorlage des Trennzeichens der einzelnen Elemente beeinflusst werden, das von der Methode `getSeparatorPrototype()` zurückgegeben wird. Standardmäßig ist dies das Element `<br>`. Wenn Sie es in ein paarweises Element ändern, wird es die einzelnen Elemente umschließen anstatt sie zu trennen. Weiterhin kann die Vorlage des HTML-Elements der Beschriftung bei den einzelnen Elementen beeinflusst werden, das von `getItemLabelPrototype()` zurückgegeben wird. +Bei CheckboxList und RadioList können Sie außerdem die Vorlage des Trennzeichens der einzelnen Elemente beeinflussen, die die Methode `getSeparatorPrototype()` zurückgibt. Standardmäßig ist es das Element `<br>`. Ändern Sie es in ein Paar-Element, umschließt es die einzelnen Elemente, statt sie zu trennen. Ebenso können Sie die Vorlage des HTML-Elements für die Labels der einzelnen Elemente beeinflussen, die `getItemLabelPrototype()` zurückgibt. -Übersetzung -=========== +Übersetzen +========== -Wenn Sie eine mehrsprachige Anwendung programmieren, müssen Sie das Formular wahrscheinlich in verschiedenen Sprachversionen rendern. Das Nette Framework definiert zu diesem Zweck eine Schnittstelle für die Übersetzung [api:Nette\Localization\Translator]. In Nette gibt es keine Standardimplementierung, Sie können je nach Bedarf aus mehreren fertigen Lösungen wählen, die Sie auf [Componette |https://componette.org/search/localization] finden. In deren Dokumentation erfahren Sie, wie Sie den Translator konfigurieren. +Wenn Sie eine mehrsprachige Anwendung entwickeln, müssen Sie das Formular vermutlich in verschiedenen Sprachversionen rendern. Das Nette Framework definiert dafür ein Interface für die Übersetzung: [api:Nette\Localization\Translator]. Nette hat keine Standardimplementierung; Sie können je nach Bedarf aus mehreren fertigen Lösungen wählen, die Sie auf [Componette |https://componette.org/search/localization] finden. In deren Dokumentation steht, wie Sie den Übersetzer einrichten. -Formulare unterstützen die Ausgabe von Texten über den Translator. Wir übergeben ihn ihnen mit der Methode `setTranslator()`: +Formulare unterstützen die Ausgabe von Texten über den Übersetzer. Wir übergeben ihn über die Methode `setTranslator()`: ```php $form->setTranslator($translator); ``` -Ab diesem Zeitpunkt werden nicht nur alle Beschriftungen, sondern auch alle Fehlermeldungen oder Elemente von Select-Boxen in eine andere Sprache übersetzt. +Von diesem Moment an werden nicht nur alle Labels, sondern auch alle Fehlermeldungen, die Elemente von Select-Boxen und die Platzhalter der Eingabefelder in die Zielsprache übersetzt. -Bei einzelnen Formularelementen ist es dabei möglich, einen anderen Übersetzer einzustellen oder die Übersetzung mit dem Wert `null` vollständig zu deaktivieren: +Für einzelne Formularelemente lässt sich ein anderer Übersetzer setzen oder die Übersetzung vollständig abschalten, indem der Wert auf `null` gesetzt wird: ```php $form->addSelect('carModel', 'Modell:', $cars) ->setTranslator(null); ``` -Bei [Validierungsregeln |validation] werden dem Translator auch spezifische Parameter übergeben, beispielsweise bei der Regel: +Bei den [Validierungsregeln |validation] werden dem Übersetzer zusätzlich spezifische Parameter übergeben, zum Beispiel bei der Regel: ```php $form->addPassword('password', 'Passwort:') ->addRule($form::MinLength, 'Das Passwort muss mindestens %d Zeichen lang sein', 8); ``` -wird der Translator mit diesen Parametern aufgerufen: +wird der Übersetzer mit diesen Parametern aufgerufen: ```php $translator->translate('Das Passwort muss mindestens %d Zeichen lang sein', 8); ``` -und kann somit die richtige Pluralform des Wortes `Zeichen` entsprechend der Anzahl wählen. +und kann so anhand der Anzahl die richtige Pluralform für das Wort `Zeichen` wählen. -Ereignis onRender -================= +Event onRender +============== -Kurz bevor das Formular gerendert wird, können wir unseren Code aufrufen lassen. Dieser kann beispielsweise den Formularelementen HTML-Klassen für die korrekte Anzeige hinzufügen. Den Code fügen wir zum Array `onRender` hinzu: +Kurz bevor das Formular gerendert wird, können wir eigenen Code aufrufen lassen. Dieser Code kann zum Beispiel den Formularelementen HTML-Klassen für die richtige Darstellung hinzufügen. Wir tragen den Code in das Array `onRender` ein: ```php $form->onRender[] = function ($form) { diff --git a/forms/de/standalone.texy b/forms/de/standalone.texy index e3679d704d..873caddf44 100644 --- a/forms/de/standalone.texy +++ b/forms/de/standalone.texy @@ -1,16 +1,22 @@ -Formulare separat verwenden -*************************** +Formulare eigenständig verwenden +******************************** .[perex] -Nette Forms erleichtert die Erstellung und Verarbeitung von Webformularen erheblich. Sie können sie in Ihren Anwendungen völlig unabhängig vom Rest des Frameworks verwenden, was wir in diesem Kapitel zeigen werden. +Nette Forms vereinfachen das Erstellen und Verarbeiten von Webformularen dramatisch. Sie können sie in Ihren Anwendungen völlig eigenständig ohne den Rest des Frameworks verwenden, wie dieses Kapitel zeigt. -Wenn Sie jedoch Nette Application und Presenter verwenden, ist die Anleitung zur [Verwendung in Presentern |in-presenter] für Sie bestimmt. +Wenn Sie jedoch Nette Application und Presenter verwenden, gibt es für Sie eine eigene Anleitung: [Formulare in Presentern |in-presenter]. -Erstes Formular -=============== +Das erste Formular +================== + +Bevor Sie loslegen, installieren Sie das Paket über [Composer |best-practices:composer]: -Versuchen wir, ein einfaches Registrierungsformular zu schreiben. Sein Code wird wie folgt aussehen ("Gesamter Code":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): +```shell +composer require nette/forms +``` + +Versuchen wir, ein einfaches Registrierungsformular zu schreiben. Sein Code sieht so aus ("vollständiger Code":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php use Nette\Forms\Form; @@ -21,23 +27,23 @@ $form->addPassword('password', 'Passwort:'); $form->addSubmit('send', 'Registrieren'); ``` -Wir können es sehr einfach rendern: +Und rendern wir es ganz einfach: ```php $form->render(); ``` -und im Browser wird es so angezeigt: +Das Ergebnis sollte im Browser so aussehen: -[* form-cs.webp *] +[* form-en.webp *] -Das Formular ist ein Objekt der Klasse `Nette\Forms\Form` (die Klasse `Nette\Application\UI\Form` wird in Presentern verwendet). Wir haben sogenannte Elemente hinzugefügt: Name, Passwort und einen Senden-Button. +Das Formular ist ein Objekt der Klasse `Nette\Forms\Form` (in Presentern wird die Klasse `Nette\Application\UI\Form` verwendet). Wir haben ihm Elemente namens "name" und "password" sowie einen Absende-Button hinzugefügt. -Und jetzt beleben wir das Formular. Durch Abfrage von `$form->isSuccess()` stellen wir fest, ob das Formular gesendet wurde und ob es gültig ausgefüllt wurde. Wenn ja, geben wir die Daten aus. Hinter die Formulardefinition fügen wir also hinzu: +Jetzt hauchen wir dem Formular Leben ein. Über die Abfrage `$form->isSuccess()` stellen wir fest, ob das Formular abgesendet und gültig ausgefüllt wurde. Wenn ja, geben wir die Daten aus. Ergänzen Sie hinter der Definition des Formulars: ```php if ($form->isSuccess()) { - echo 'Formular wurde korrekt ausgefüllt und gesendet'; + echo 'Das Formular wurde ausgefüllt und erfolgreich abgesendet'; $data = $form->getValues(); // $data->name enthält den Namen // $data->password enthält das Passwort @@ -45,42 +51,42 @@ if ($form->isSuccess()) { } ``` -Die Methode `getValues()` gibt die gesendeten Daten in Form eines [ArrayHash |utils:arrays#ArrayHash] Objekts zurück. Wie man das ändert, zeigen wir [später |#Mapping auf Klassen]. Das Objekt `$data` enthält die Schlüssel `name` und `password` mit den vom Benutzer eingegebenen Daten. +Die Methode `getValues()` gibt die gesendeten Daten als Objekt [ArrayHash |utils:arrays#ArrayHash] zurück. Wie sich das ändern lässt, zeigen wir [später |#Mapping auf Klassen]. Das Objekt `$data` enthält die Schlüssel `name` und `password` mit den Daten, die der Benutzer eingegeben hat. -Normalerweise senden wir die Daten direkt zur weiteren Verarbeitung, was zum Beispiel das Einfügen in eine Datenbank sein kann. Während der Verarbeitung kann jedoch ein Fehler auftreten, z. B. ist der Benutzername bereits vergeben. In diesem Fall geben wir den Fehler mit `addError()` an das Formular zurück und lassen es erneut rendern, zusammen mit der Fehlermeldung. +Üblicherweise geben wir die Daten direkt zur weiteren Verarbeitung weiter, etwa zum Einfügen in eine Datenbank. Bei der Verarbeitung kann jedoch ein Fehler auftreten, zum Beispiel wenn der Benutzername schon vergeben ist. In diesem Fall geben wir den Fehler über `addError()` an das Formular zurück und lassen es samt Fehlermeldung erneut rendern. ```php -$form->addError('Entschuldigung, dieser Benutzername wird bereits verwendet.'); +$form->addError('Dieser Benutzername ist leider bereits vergeben.'); ``` -Nach der Verarbeitung des Formulars leiten wir auf die nächste Seite weiter. Dies verhindert das unbeabsichtigte erneute Senden des Formulars durch die Schaltflächen *Aktualisieren*, *Zurück* oder durch die Navigation im Browserverlauf. +Nach dem Verarbeiten des Formulars leiten wir auf die nächste Seite weiter. Das verhindert, dass das Formular ungewollt erneut abgesendet wird, wenn der Benutzer *Aktualisieren* oder *Zurück* drückt oder in der Browser-Historie navigiert. -Das Formular wird standardmäßig per POST-Methode an dieselbe Seite gesendet. Beides kann geändert werden: +Standardmäßig wird das Formular mit der Methode POST an dieselbe Seite gesendet. Beides lässt sich ändern: ```php $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -Und das ist eigentlich alles :-) Wir haben ein funktionierendes und perfekt [gesichertes |#Schutz vor Schwachstellen] Formular. +Und das ist im Grunde alles :-) Wir haben ein funktionierendes und bestens [abgesichertes |#Schutz vor Sicherheitslücken] Formular. Versuchen Sie, auch weitere [Formularelemente |controls] hinzuzufügen. -Zugriff auf Elemente -==================== +Zugriff auf die Elemente +======================== -Das Formular und seine einzelnen Elemente nennen wir Komponenten. Sie bilden einen Komponentenbaum, dessen Wurzel das Formular ist. Auf die einzelnen Elemente des Formulars greifen wir folgendermaßen zu: +Das Formular und seine einzelnen Elemente heißen Komponenten. Sie bilden einen Komponentenbaum, dessen Wurzel das Formular ist. Auf die einzelnen Formularelemente greifen Sie so zu: ```php $input = $form->getComponent('name'); -// alternative Syntax: $input = $form['name']; +// alternative Schreibweise: $input = $form['name']; $button = $form->getComponent('send'); -// alternative Syntax: $button = $form['send']; +// alternative Schreibweise: $button = $form['send']; ``` -Elemente werden mit unset entfernt: +Entfernt werden Elemente über `unset`: ```php unset($form['name']); @@ -90,26 +96,26 @@ unset($form['name']); Validierungsregeln ================== -Das Wort *gültig* wurde erwähnt, aber das Formular hat noch keine Validierungsregeln. Ändern wir das. +Das Wort *gültig* ist gefallen, aber das Formular hat noch keine Validierungsregeln. Bringen wir das in Ordnung. -Der Name ist ein Pflichtfeld, daher markieren wir ihn mit der Methode `setRequired()`, deren Argument der Text der Fehlermeldung ist, die angezeigt wird, wenn der Benutzer den Namen nicht eingibt. Wenn kein Argument angegeben wird, wird die Standardfehlermeldung verwendet. +Der Name wird zum Pflichtfeld, wir kennzeichnen ihn also mit der Methode `setRequired()`. Ihr Argument ist der Text der Fehlermeldung, die angezeigt wird, wenn der Benutzer den Namen nicht ausfüllt. Wird kein Argument angegeben, wird die Standard-Fehlermeldung verwendet. ```php $form->addText('name', 'Name:') - ->setRequired('Bitte geben Sie einen Namen ein'); + ->setRequired('Bitte geben Sie einen Namen ein.'); ``` -Versuchen Sie, das Formular ohne ausgefüllten Namen zu senden, und Sie werden sehen, dass eine Fehlermeldung angezeigt wird und der Browser oder Server es ablehnt, bis Sie das Feld ausfüllen. +Versuchen Sie, das Formular ohne ausgefüllten Namen abzusenden, und Sie sehen eine Fehlermeldung erscheinen. Der Browser oder der Server weist es ab, bis Sie das Feld ausfüllen. -Gleichzeitig können Sie das System nicht austricksen, indem Sie beispielsweise nur Leerzeichen in das Feld eingeben. Nein. Nette entfernt automatisch führende und nachfolgende Leerzeichen. Probieren Sie es aus. Das ist etwas, was Sie immer bei jedem einzeiligen Input tun sollten, aber oft vergessen wird. Nette erledigt das automatisch. (Sie können versuchen, das Formular zu täuschen und einen mehrzeiligen String als Namen zu senden. Auch hier lässt sich Nette nicht täuschen und wandelt Zeilenumbrüche in Leerzeichen um.) +Zugleich können Sie das System nicht austricksen, indem Sie nur Leerzeichen in das Feld schreiben. Auf keinen Fall. Nette schneidet Leerraum am Anfang und Ende automatisch ab. Probieren Sie es aus. Das sollten Sie bei jeder einzeiligen Eingabe immer tun, es wird aber oft vergessen. Nette macht es automatisch. (Sie können versuchen, das Formular hereinzulegen und als Namen einen mehrzeiligen String zu senden. Auch hier lässt sich Nette nicht täuschen, die Zeilenumbrüche werden in Leerzeichen umgewandelt.) -Das Formular wird immer serverseitig validiert, aber es wird auch eine JavaScript-Validierung generiert, die blitzschnell abläuft und der Benutzer sofort über den Fehler informiert wird, ohne das Formular an den Server senden zu müssen. Dafür sorgt das Skript `netteForms.js`. Fügen Sie es in die Seite ein: +Das Formular wird immer auf der Serverseite validiert, es wird aber auch eine JavaScript-Validierung erzeugt. Sie läuft sofort, und der Benutzer erfährt unmittelbar von Fehlern, ohne dass das Formular an den Server gesendet werden muss. Dafür sorgt das Skript `netteForms.js`. Binden Sie es in die Seite ein: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, können Sie feststellen, dass Nette Pflichtelemente in Elemente mit der CSS-Klasse `required` einfügt. Versuchen Sie, das folgende Stylesheet in die Vorlage einzufügen, und die Beschriftung „Name“ wird rot. So kennzeichnen wir elegant die Pflichtelemente für die Benutzer: +Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, fällt Ihnen vielleicht auf, dass Nette die Pflichtelemente in Elemente mit der CSS-Klasse `required` setzt. Fügen Sie dem Template das folgende Stylesheet hinzu, und das Label "Name" wird rot. So heben Sie Pflichtelemente für die Benutzer elegant hervor: ```latte <style> @@ -117,25 +123,25 @@ Wenn Sie sich den Quellcode der Seite mit dem Formular ansehen, können Sie fest </style> ``` -Weitere Validierungsregeln fügen wir mit der Methode `addRule()` hinzu. Der erste Parameter ist die Regel, der zweite ist wieder der Text der Fehlermeldung, und es kann noch ein Argument für die Validierungsregel folgen. Was ist damit gemeint? +Weitere Validierungsregeln fügen wir über die Methode `addRule()` hinzu. Der erste Parameter ist die Regel, der zweite wiederum der Text der Fehlermeldung, und danach kann ein optionales Argument der Validierungsregel folgen. Was bedeutet das? -Wir erweitern das Formular um ein neues optionales Feld „Alter“, das eine ganze Zahl sein muss (`addInteger()`) und zusätzlich in einem erlaubten Bereich liegen muss (`$form::Range`). Und hier verwenden wir den dritten Parameter der Methode `addRule()`, mit dem wir dem Validator den gewünschten Bereich als Paar `[von, bis]` übergeben: +Erweitern wir das Formular um ein neues optionales Feld "Alter", das eine ganze Zahl sein muss (`addInteger()`) und in einem erlaubten Bereich liegen muss (`$form::Range`). Hier verwenden wir den dritten Parameter der Methode `addRule()`, um dem Validator den verlangten Bereich als Paar `[min, max]` zu übergeben: ```php $form->addInteger('age', 'Alter:') - ->addRule($form::Range, 'Das Alter muss zwischen 18 und 120 liegen', [18, 120]); + ->addRule($form::Range, 'Das Alter muss zwischen 18 und 120 liegen.', [18, 120]); ``` .[tip] -Wenn der Benutzer das Feld nicht ausfüllt, werden die Validierungsregeln nicht überprüft, da das Element optional ist. +Füllt der Benutzer das Feld nicht aus, werden die Validierungsregeln nicht geprüft, denn das Element ist optional. -Hier entsteht Raum für ein kleines Refactoring. In der Fehlermeldung und im dritten Parameter sind die Zahlen doppelt aufgeführt, was nicht ideal ist. Wenn wir [mehrsprachige Formulare |rendering#Übersetzung] erstellen würden und die Meldung, die Zahlen enthält, in mehrere Sprachen übersetzt würde, würde eine spätere Änderung der Werte erschwert. Aus diesem Grund können Platzhalter `%d` verwendet werden, und Nette füllt die Werte ein: +Damit entsteht Raum für ein kleines Refactoring. In der Fehlermeldung und im dritten Parameter stehen die Zahlen doppelt, was nicht ideal ist. Würden wir [mehrsprachige Formulare |rendering#Übersetzen] bauen und die Meldung mit den Zahlen in mehrere Sprachen übersetzen, wäre das Ändern der Werte mühsam. Aus diesem Grund lassen sich die Platzhalter `%d` verwenden, und Nette setzt die Werte ein: ```php - ->addRule($form::Range, 'Das Alter muss zwischen %d und %d Jahren liegen', [18, 120]); + ->addRule($form::Range, 'Das Alter muss zwischen %d und %d Jahren liegen.', [18, 120]); ``` -Kehren wir zum Element `password` zurück, das wir ebenfalls als Pflichtfeld definieren und zusätzlich die Mindestlänge des Passworts überprüfen (`$form::MinLength`), wieder unter Verwendung des Platzhalters: +Kehren wir zum Element `password` zurück, machen es ebenfalls zum Pflichtfeld und prüfen außerdem die Mindestlänge des Passworts (`$form::MinLength`), wieder mit einem Platzhalter in der Meldung: ```php $form->addPassword('password', 'Passwort:') @@ -143,59 +149,76 @@ $form->addPassword('password', 'Passwort:') ->addRule($form::MinLength, 'Das Passwort muss mindestens %d Zeichen lang sein', 8); ``` -Wir fügen dem Formular noch das Feld `passwordVerify` hinzu, in das der Benutzer das Passwort zur Kontrolle erneut eingibt. Mithilfe von Validierungsregeln überprüfen wir, ob beide Passwörter übereinstimmen (`$form::Equal`). Und als Parameter geben wir einen Verweis auf das erste Passwort mithilfe von [eckigen Klammern |#Zugriff auf Elemente]: +Fügen wir dem Formular noch ein Feld `passwordVerify` hinzu, in dem der Benutzer das Passwort zur Kontrolle erneut eingibt. Über Validierungsregeln prüfen wir, ob beide Passwörter gleich sind (`$form::Equal`). Als Parameter geben wir über [eckige Klammern |#Zugriff auf die Elemente] einen Verweis auf das erste Passwort an: ```php -$form->addPassword('passwordVerify', 'Passwort zur Kontrolle:') - ->setRequired('Bitte geben Sie das Passwort zur Kontrolle erneut ein') +$form->addPassword('passwordVerify', 'Passwort erneut:') + ->setRequired('Geben Sie das Passwort zur Kontrolle erneut ein') ->addRule($form::Equal, 'Die Passwörter stimmen nicht überein', $form['password']) ->setOmitted(); ``` -Mit `setOmitted()` haben wir ein Element markiert, dessen Wert uns eigentlich egal ist und das nur aus Validierungsgründen existiert. Der Wert wird nicht an `$data` übergeben. +Mit `setOmitted()` haben wir ein Element gekennzeichnet, dessen Wert uns eigentlich nicht interessiert und das nur zur Validierung da ist. Sein Wert wird nicht an `$data` übergeben. -Damit haben wir ein voll funktionsfähiges Formular mit Validierung in PHP und JavaScript. Die Validierungsfähigkeiten von Nette sind weitaus umfangreicher, es können Bedingungen erstellt, Teile der Seite darauf basierend ein- und ausgeblendet werden usw. Alles erfahren Sie im Kapitel über [Formularvalidierung |validation]. +Damit haben wir ein voll funktionsfähiges Formular mit Validierung in PHP und in JavaScript. Die Fähigkeiten von Nette zur Validierung reichen viel weiter; Sie können Bedingungen erstellen, anhand derer sich Teile der Seite ein- und ausblenden lassen, und so weiter. Alles erfahren Sie im Kapitel über die [Validierung von Formularen |validation]. Standardwerte ============= -Elementen des Formulars weisen wir üblicherweise Standardwerte zu: +Für Formularelemente setzen wir oft Standardwerte: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'E-Mail') ->setDefaultValue($lastUsedEmail); ``` -Oft ist es nützlich, allen Elementen gleichzeitig Standardwerte zuzuweisen. Zum Beispiel, wenn das Formular zur Bearbeitung von Datensätzen dient. Wir lesen den Datensatz aus der Datenbank und setzen die Standardwerte: +Oft ist es nützlich, die Standardwerte für alle Elemente auf einmal zu setzen, zum Beispiel wenn das Formular zum Bearbeiten von Datensätzen dient. Wir lesen den Datensatz aus der Datenbank und setzen seine Werte als Standardwerte: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Rufen Sie `setDefaults()` erst nach der Definition der Elemente auf. +Rufen Sie `setDefaults()` nach dem Definieren der Elemente auf. + +Bei einem bereits abgesendeten Formular hat `setDefaults()` keine Wirkung - es überschreibt nicht, was der Benutzer ausgefüllt hat, es ist also gefahrlos, es in der Factory des Formulars ohne Bedingung aufzurufen. Wenn Sie die Werte auch nach dem Absenden erzwingen müssen, verwenden Sie stattdessen `setValues()`. Rendering des Formulars ======================= -Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Elemente erfüllen die grundlegende Zugänglichkeitsregel – alle Beschriftungen sind als `<label>` geschrieben und mit dem entsprechenden Formularelement verknüpft. Beim Klicken auf die Beschriftung erscheint der Cursor automatisch im Formularfeld. +Standardmäßig wird das Formular als Tabelle gerendert. Die einzelnen Elemente halten die grundlegenden Richtlinien der Barrierefreiheit ein - alle Labels werden als `<label>`-Elemente erzeugt und dem jeweiligen Formularelement zugeordnet. Ein Klick auf ein Label setzt den Cursor automatisch in das Formularfeld. -Jedem Element können wir beliebige HTML-Attribute zuweisen. Zum Beispiel einen Platzhalter hinzufügen: +Für jedes Element können wir beliebige HTML-Attribute setzen. Fügen wir zum Beispiel einen Platzhalter hinzu: ```php $form->addInteger('age', 'Alter:') - ->setHtmlAttribute('placeholder', 'Bitte geben Sie das Alter ein'); + ->setHtmlAttribute('placeholder', 'Bitte geben Sie das Alter an'); ``` -Es gibt wirklich viele Möglichkeiten, ein Formular zu rendern, daher gibt es ein [eigenes Kapitel zum Rendering |rendering]. +Es gibt viele Wege, ein Formular zu rendern, deshalb ist dem [Rendering ein eigenes Kapitel |rendering] gewidmet. + + +Rendering mit Latte +------------------- + +Wenn Sie die Templating-Engine [Latte |latte:] zur Hand haben, können Sie sie das Formular rendern lassen und die volle Kontrolle über das entstehende HTML gewinnen. Sie erzeugen die Engine, registrieren die Extension für Formulare und übergeben das Formular als Variable an das Template: + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); +``` + +Im Template arbeiten Sie dann über die Variable `$form` und Tags wie `{input}`, `{label}` oder `n:name` mit dem Formular. Ein vollständiges Beispiel samt Template finden Sie im Verzeichnis [examples |https://github.com/nette/forms/tree/master/examples] (die Dateien `latte.php` und `latte/`). Die einzelnen Tags beschreibt das Kapitel über das [Rendering |rendering]. Mapping auf Klassen =================== -Kehren wir zur Verarbeitung der Formulardaten zurück. Die Methode `getValues()` gab uns die gesendeten Daten als `ArrayHash`-Objekt zurück. Da es sich um eine generische Klasse handelt, ähnlich wie `stdClass`, fehlt uns bei der Arbeit damit ein gewisser Komfort, wie z.B. die Code-Vervollständigung für Properties in Editoren oder die statische Codeanalyse. Dies könnte gelöst werden, indem wir für jedes Formular eine spezifische Klasse hätten, deren Properties die einzelnen Elemente repräsentieren. Z.B.: +Kehren wir zur Verarbeitung der Daten des Formulars zurück. Die Methode `getValues()` gab die gesendeten Daten als Objekt `ArrayHash` zurück. Weil es eine generische Klasse ist wie `stdClass`, fehlt uns bei der Arbeit damit einiger Komfort, etwa die Vervollständigung der Properties im Editor oder die statische Analyse des Codes. Lösen ließe sich das, indem es für jedes Formular eine eigene Klasse gibt, deren Properties die einzelnen Elemente darstellen. Zum Beispiel: ```php class RegistrationFormData @@ -206,32 +229,32 @@ class RegistrationFormData } ``` -Alternativ können Sie einen Konstruktor verwenden: +Alternativ können Sie den Konstruktor verwenden: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Die Properties der Datenklasse können auch Enums sein und werden automatisch zugeordnet. .{data-version:3.2.4} +Die Properties der Datenklasse können auch Enums sein, sie werden automatisch gemappt. .{data-version:3.2.4} -Wie sagen wir Nette, dass es die Daten als Objekte dieser Klasse zurückgeben soll? Einfacher als Sie denken. Es reicht aus, den Klassennamen oder das zu hydratisierende Objekt als Parameter anzugeben: +Wie sagen wir Nette, dass es die Daten als Objekte dieser Klasse zurückgeben soll? Einfacher, als Sie denken. Sie müssen nur den Namen der Klasse oder das zu füllende Objekt als Parameter angeben: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Als Parameter kann auch `'array'` angegeben werden, dann werden die Daten als Array zurückgegeben. +Als Parameter lässt sich auch `'array'` angeben, dann werden die Daten als Array zurückgegeben. -Wenn Formulare eine mehrstufige Struktur aus Containern bilden, erstellen Sie für jeden eine separate Klasse: +Bestehen die Formulare aus einer mehrstufigen Struktur von Containern, legen Sie für jeden eine eigene Klasse an: ```php $form = new Form; @@ -253,19 +276,19 @@ class RegistrationFormData } ``` -Das Mapping erkennt dann am Typ der Property `$person`, dass der Container der Klasse `PersonFormData` zugeordnet werden soll. Wenn die Property ein Array von Containern enthält, geben Sie den Typ `array` an und übergeben Sie die zuzuordnende Klasse direkt an den Container: +Das Mapping weiß dann aus dem Typ der Property `$person`, dass es den Container auf die Klasse `PersonFormData` mappen soll. Würde die Property ein Array von Containern enthalten, geben Sie den Typ `array` an und übergeben die zu mappende Klasse direkt dem Container: ```php $person->setMappedType(PersonFormData::class); ``` -Den Entwurf der Datenklasse des Formulars können Sie sich mit der Methode `Nette\Forms\Blueprint::dataClass($form)` generieren lassen, die ihn auf der Browserseite ausgibt. Den Code können Sie dann einfach per Klick markieren und in Ihr Projekt kopieren. .{data-version:3.1.15} +Einen Vorschlag für die Datenklasse des Formulars können Sie sich über die Methode `Nette\Forms\Blueprint::dataClass($form)` erzeugen lassen, die ihn auf der Seite im Browser ausgibt. Dann markieren Sie den Code einfach mit einem Klick und kopieren ihn in Ihr Projekt. .{data-version:3.1.15} -Mehrere Buttons -=============== +Mehrere Absende-Buttons +======================= -Wenn ein Formular mehr als einen Button hat, müssen wir in der Regel unterscheiden, welcher davon gedrückt wurde. Diese Information liefert uns die Methode `isSubmittedBy()` des Buttons: +Hat ein Formular mehr als einen Button, müssen wir üblicherweise unterscheiden, welcher davon gedrückt wurde. Diese Information gibt die Methode `isSubmittedBy()` des Buttons zurück: ```php $form->addSubmit('save', 'Speichern'); @@ -282,36 +305,33 @@ if ($form->isSuccess()) { } ``` -Lassen Sie die Abfrage `$form->isSuccess()` nicht aus, sie überprüft die Gültigkeit der Daten. +Lassen Sie die Prüfung `$form->isSuccess()` nicht weg; sie stellt die Gültigkeit der Daten sicher. -Wenn das Formular durch Drücken von <kbd>Enter</kbd> gesendet wird, wird dies so behandelt, als ob der erste Button gesendet wurde. +Wird ein Formular durch Drücken der Taste <kbd>Enter</kbd> abgesendet, wird das behandelt, als wäre es über den ersten Button abgesendet worden. -Schutz vor Schwachstellen -========================= +Schutz vor Sicherheitslücken +============================ -Das Nette Framework legt großen Wert auf Sicherheit und achtet daher sorgfältig auf die gute Absicherung von Formularen. +Das Nette Framework legt großen Wert auf Sicherheit und achtet deshalb gewissenhaft auf die richtige Absicherung von Formularen. -Neben dem Schutz der Formulare vor [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] und [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF] Angriffen, führt es viele kleine Sicherheitsmaßnahmen durch, an die Sie nicht mehr denken müssen. +Neben dem Schutz von Formularen vor bekannten Sicherheitslücken wie [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] und [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)] trifft es viele kleine Sicherheitsmaßnahmen, an die Sie nicht mehr denken müssen. -So filtert es beispielsweise alle Steuerzeichen aus den Eingaben und überprüft die Gültigkeit der UTF-8-Kodierung, sodass die Daten aus dem Formular immer sauber sind. Bei Select-Boxen und Radio-Listen wird überprüft, ob die ausgewählten Elemente tatsächlich aus den angebotenen stammten und keine Fälschung stattgefunden hat. Wir haben bereits erwähnt, dass bei einzeiligen Texteingaben Zeilenumbruchzeichen entfernt werden, die ein Angreifer dort hätte senden können. Bei mehrzeiligen Eingaben werden die Zeilenumbruchzeichen normalisiert. Und so weiter. +So filtert es zum Beispiel alle Steuerzeichen aus den Eingaben und prüft die Gültigkeit der UTF-8-Kodierung, sodass die Daten aus dem Formular immer sauber sind. Bei Select-Boxen und Radio-Listen prüft es, dass die ausgewählten Elemente tatsächlich zu den angebotenen gehörten und keine Fälschung stattgefunden hat. Wir haben bereits erwähnt, dass es bei einzeiligen Textfeldern die Zeilenumbruchzeichen, die ein Angreifer senden könnte, durch Leerzeichen ersetzt. Bei mehrzeiligen Eingaben vereinheitlicht es die Zeilenumbruchzeichen. Und so weiter. -Nette löst für Sie Sicherheitsrisiken, von denen viele Programmierer nicht einmal wissen, dass sie existieren. +Nette kümmert sich für Sie um Sicherheitsrisiken, von deren Existenz viele Programmierer nicht einmal wissen. -Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer das Opfer auf eine Seite lockt, die unauffällig im Browser des Opfers eine Anfrage an den Server stellt, bei dem das Opfer angemeldet ist, und der Server annimmt, dass die Anfrage vom Opfer aus freiem Willen ausgeführt wurde. Daher verhindert Nette das Senden von POST-Formularen von einer anderen Domain. Wenn Sie aus irgendeinem Grund den Schutz deaktivieren und das Senden von Formularen von einer anderen Domain erlauben möchten, verwenden Sie: +Der erwähnte CSRF-Angriff besteht darin, dass ein Angreifer ein Opfer auf eine Seite lockt, die im Browser des Opfers unbemerkt einen Request an den Server ausführt, auf dem das Opfer gerade angemeldet ist. Der Server glaubt, das Opfer habe den Request freiwillig ausgelöst. Nette weist deshalb POST-Formulare ab, die von einem fremden Origin abgesendet wurden; auch eine andere Subdomain derselben Website gilt als fremd. Wenn Sie das Absenden von einem anderen Origin erlauben müssen, schalten Sie den Schutz ab mit: ```php -$form->allowCrossOrigin(); // VORSICHT! Deaktiviert den Schutz! +$form->allowCrossOrigin(); // ACHTUNG! Schaltet den Schutz vollständig ab! ``` -Dieser Schutz verwendet ein SameSite-Cookie namens `_nss`. Erstellen Sie daher das Formularobjekt, bevor die erste Ausgabe gesendet wird, damit das Cookie gesendet werden kann. - -Der Schutz durch das SameSite-Cookie ist möglicherweise nicht 100% zuverlässig, daher ist es ratsam, zusätzlich den Schutz durch ein Token zu aktivieren: +Damit ist der Schutz allerdings für jeden Origin abgeschaltet. Um nur bestimmte Origins zu erlauben, schalten Sie den Schutz ab und prüfen den Header `Origin` selbst gegen eine eigene Allowlist. -```php -$form->addProtection(); -``` +Der Schutz stützt sich auf den Header `Sec-Fetch-Site` des Browsers (Fetch Metadata), den der Browser automatisch sendet und der sich selbst mit einer XSS-Lücke nicht fälschen lässt. Ältere Browser, die diese Header nicht senden, bestehen die Prüfung nicht. Der Artikel [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] beschreibt das ausführlich. -Wir empfehlen, Formulare im Administrationsbereich der Website, die sensible Daten in der Anwendung ändern, auf diese Weise zu schützen. Das Framework wehrt sich gegen CSRF-Angriffe, indem es ein Autorisierungstoken generiert und überprüft, das in der Session gespeichert wird. Daher muss die Session vor dem Anzeigen des Formulars geöffnet sein. Im Administrationsbereich der Website ist die Session normalerweise bereits aufgrund der Benutzeranmeldung gestartet. Andernfalls starten Sie die Session mit der Methode `Nette\Http\Session::start()`. +.[note] +Der frühere Schutz über ein Autorisierungstoken in der Session, der über `$form->addProtection()` aktiviert wurde, ist nicht mehr nötig und seit Version 3.3 veraltet. -So, das war eine schnelle Einführung in Formulare in Nette. Werfen Sie noch einen Blick in das Verzeichnis [examples |https://github.com/nette/forms/tree/master/examples] in der Distribution, dort finden Sie weitere Inspiration. +Damit haben wir eine kurze Einführung in Formulare in Nette hinter uns. Schauen Sie sich für weitere Anregungen das Verzeichnis [examples |https://github.com/nette/forms/tree/master/examples] in der Distribution an. diff --git a/forms/de/upgrading.texy b/forms/de/upgrading.texy new file mode 100644 index 0000000000..7adaee47b2 --- /dev/null +++ b/forms/de/upgrading.texy @@ -0,0 +1,54 @@ +Upgrade +******* + + +Upgrade auf Version 3.3 +======================= + +- der automatische Schutz vor CSRF ist von `isSameSite()` auf `isFrom(FetchSite::SameOrigin)` umgestiegen und strenger geworden: Requests von Subdomains kommen nicht mehr durch +- dadurch ist `addProtection()` nicht mehr nötig, denn der automatische Schutz deckt dieselben Fälle ab; lassen Sie es in neuen Formularen weg und entfernen Sie es ruhig aus den bestehenden + +Warum Tokens in der Session nicht mehr nötig sind, erklärt der Artikel [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Upgrade auf Version 3.1 +======================= + +- `getValues()` gibt nur die validierten Elemente zurück; wenn Sie die Werte aller Elemente unabhängig von der Validierung brauchen, verwenden Sie die neue Methode `getUntrustedValues()` +- die `$values`, die den Handlern `onSuccess` und `onClick` übergeben werden, enthalten ebenfalls nur die validierten Elemente +- eigenständige Formulare sind über ein Cookie mit dem Flag SameSite automatisch vor CSRF geschützt; das Absenden von einem anderen Origin erlauben Sie über `allowCrossOrigin()` +- die Regel `Form::URL` ergänzt ein fehlendes Protokoll jetzt mit `https` statt mit `http` +- `Form::addImage()` wurde in `addImageButton()` umbenannt +- `Checkbox::getSeparatorPrototype()` wurde in `getContainerPrototype()` umbenannt +- Formulare erzeugen in Templates nicht mehr die Variable `$_form` + +Mehr zu diesen Änderungen im Artikel [News in Nette Forms 3.1 |https://blog.nette.org/en/news-in-nette-forms-3-1]. + + +Upgrade auf Version 3.0 +======================= + +- alle Formularelemente sind jetzt standardmäßig optional (diese Änderung kam in Nette 2.4), Sie können `setRequired(false)` also entfernen +- aktualisieren Sie unbedingt `netteForms.js` auf Version 3 (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` und `MultiChoiceControl::$checkAllowedValues` wurden durch die Methode `checkDefaultValue()` ersetzt + + +Upgrade auf Version 2.4 +======================= + +- hat ein Element eine Regel über `addRule()` (ist es also faktisch ein Pflichtfeld), müssen Sie es auch über `setRequired()` als Pflichtfeld kennzeichnen; außerdem macht `setRequired(false)` das Element jetzt optional, was die Zweige `addCondition($form::FILLED)` ersetzt +- die Validatoren `Form::EMAIL`, `URL` und `INTEGER` ändern das HTML-Attribut `type` automatisch auf `email`, `url` bzw. `number` +- negative Validierungsregeln sind veraltet; die Alternative zu `~Form::FILLED` ist `Form::BLANK`, und `~Form::EQUAL` lässt sich durch `Form::NOT_EQUAL` ersetzen +- der interne Parameter `do` wird jetzt per POST als `_do` gesendet, um eine Kollision zu vermeiden +- die internen Variablen mit Unterstrich wie `$_form` sind veraltet +- denken Sie daran, `netteForms.js` zu aktualisieren + + +Upgrade auf Version 2.3 +======================= + +- die internen Methoden zum Filtern wie `Nette\Forms\Controls\TextBase::filterFloat` wurden entfernt +- die internen Methoden zur Validierung wie `TextBase::validateFloat` sind nach `Nette\Forms\Validator` umgezogen, ebenso `Rules::$defaultMessages` +- Buttons und versteckte Felder werden ohne HTML-ID erzeugt; wenn Sie eine ID wollen, setzen Sie sie über `setHtmlId()` +- auch die Elemente von RadioList werden ohne ID erzeugt; einschalten lässt sie sich über `$radioList->generateId = true` +- Filter, die über `TextBase::addFilter()` hinzugefügt werden, werden während der Validierung verarbeitet, und Sie können Filter jetzt zu Bedingungen hinzufügen: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/de/validation.texy b/forms/de/validation.texy index ba5253e59a..11df4431da 100644 --- a/forms/de/validation.texy +++ b/forms/de/validation.texy @@ -5,100 +5,100 @@ Formularvalidierung Pflichtelemente =============== -Pflichtelemente markieren wir mit der Methode `setRequired()`, deren Argument der Text der [#Fehlermeldungen] ist, die angezeigt wird, wenn der Benutzer das Element nicht ausfüllt. Wenn kein Argument angegeben wird, wird die Standardfehlermeldung verwendet. +Elemente werden über die Methode `setRequired()` als Pflichtfelder gekennzeichnet. Ihr Argument ist der Text der [Fehlermeldung |#Fehlermeldungen], die angezeigt wird, wenn der Benutzer das Element nicht ausfüllt. Wird kein Argument angegeben, wird die Standard-Fehlermeldung verwendet. ```php $form->addText('name', 'Name:') - ->setRequired('Bitte geben Sie einen Namen ein'); + ->setRequired('Bitte füllen Sie Ihren Namen aus.'); ``` Regeln ====== -Validierungsregeln fügen wir Elementen mit der Methode `addRule()` hinzu. Der erste Parameter ist die Regel, der zweite ist der Text der [#Fehlermeldungen] und der dritte ist das Argument der Validierungsregel. +Validierungsregeln fügen wir den Elementen über die Methode `addRule()` hinzu. Der erste Parameter ist die Regel, der zweite die [Fehlermeldung |#Fehlermeldungen] und der dritte das Argument der Validierungsregel. ```php $form->addPassword('password', 'Passwort:') ->addRule($form::MinLength, 'Das Passwort muss mindestens %d Zeichen lang sein', 8); ``` -**Validierungsregeln werden nur überprüft, wenn der Benutzer das Element ausgefüllt hat.** +**Die Validierungsregeln werden nur geprüft, wenn der Benutzer das Element ausgefüllt hat.** -Nette bringt eine ganze Reihe vordefinierter Regeln mit, deren Namen Konstanten der Klasse `Nette\Forms\Form` sind. Für alle Elemente können wir diese Regeln verwenden: +Nette bringt eine Reihe vordefinierter Regeln mit, deren Namen Konstanten der Klasse `Nette\Forms\Form` sind. Diese Regeln können wir auf alle Elemente anwenden: -| Konstante | Beschreibung | Argumenttyp -|------------|----------------------------------------|--------------- +| Konstante | Beschreibung | Typ des Arguments +|------- | `Required` | Pflichtelement, Alias für `setRequired()` | - -| `Filled` | Pflichtelement, Alias für `setRequired()` | - -| `Blank` | Element darf nicht ausgefüllt sein | - -| `Equal` | Wert ist gleich dem Parameter | `mixed` -| `NotEqual` | Wert ist nicht gleich dem Parameter | `mixed` -| `IsIn` | Wert ist gleich einem Element im Array | `array` -| `IsNotIn` | Wert ist keinem Element im Array gleich | `array` -| `Valid` | Ist das Element korrekt ausgefüllt? (für [#Bedingungen]) | - +| `Filled` | Pflichtelement, Alias für `setRequired()` | - +| `Blank` | das Element darf nicht ausgefüllt sein | - +| `Equal` | der Wert muss dem Parameter entsprechen | `mixed` +| `NotEqual` | der Wert darf dem Parameter nicht entsprechen | `mixed` +| `IsIn` | der Wert muss eines der Elemente des Arrays sein | `array` +| `IsNotIn` | der Wert darf keines der Elemente des Arrays sein | `array` +| `Valid` | ist das Element korrekt ausgefüllt? (nur in [addConditionOn() |#Bedingungen]) | - Texteingaben ------------ -Für die Elemente `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` können auch einige der folgenden Regeln verwendet werden: +Für die Elemente `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` lassen sich außerdem einige der folgenden Regeln anwenden: -| `MinLength` | Minimale Textlänge | `int` -| `MaxLength` | Maximale Textlänge | `int` -| `Length` | Länge im Bereich oder genaue Länge | Paar `[int, int]` oder `int` -| `Email` | Gültige E-Mail-Adresse | - -| `URL` | Absolute URL | - -| `Pattern` | Entspricht dem regulären Ausdruck | `string` -| `PatternInsensitive` | Wie `Pattern`, aber Groß-/Kleinschreibung egal | `string` -| `Integer` | Ganzzahliger Wert | - -| `Numeric` | Alias für `Integer` | - -| `Float` | Zahl | - -| `Min` | Minimalwert des numerischen Elements | `int\|float` -| `Max` | Maximalwert des numerischen Elements | `int\|float` -| `Range` | Wert im Bereich | Paar `[int\|float, int\|float]` +| `MinLength` | Mindestlänge des Textes | `int` +| `MaxLength` | maximale Länge des Textes | `int` +| `Length` | Länge im Bereich oder genaue Länge | Paar `[int, int]` oder `int` +| `Email` | gültige E-Mail-Adresse | - +| `URL` | absolute URL | - +| `Pattern` | passt auf einen regulären Ausdruck | `string` +| `PatternInsensitive` | wie `Pattern`, aber ohne Rücksicht auf Groß-/Kleinschreibung | `string` +| `Integer` | ganzzahliger Wert | - +| `Numeric` | nicht negative ganze Zahl (nur Ziffern) | - +| `Float` | Zahl | - +| `Min` | Mindestwert eines numerischen Elements | `int\|float` +| `Max` | Höchstwert eines numerischen Elements | `int\|float` +| `Range` | Wert im Bereich | Paar `[int\|float, int\|float]` -Die Validierungsregeln `Integer`, `Numeric` und `Float` konvertieren den Wert direkt in Integer bzw. Float. Des Weiteren akzeptiert die Regel `URL` auch Adressen ohne Schema (z. B. `nette.org`) und ergänzt das Schema (`https://nette.org`). Der Ausdruck in `Pattern` und `PatternIcase` muss für den gesamten Wert gelten, d.h. als ob er von den Zeichen `^` und `$` umschlossen wäre. +Die Validierungsregeln `Integer` und `Float` wandeln den Wert automatisch in eine ganze bzw. eine Fließkommazahl um. Die Regel `URL` akzeptiert außerdem auch eine Adresse ohne Schema (etwa `nette.org`) und ergänzt das Schema (`https://nette.org`). Der Ausdruck in `Pattern` und `PatternInsensitive` muss für den gesamten Wert gelten, also so, als wäre er von den Zeichen `^` und `$` umschlossen. Anzahl der Elemente ------------------- -Für die Elemente `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` können auch die folgenden Regeln zur Begrenzung der Anzahl ausgewählter Elemente bzw. hochgeladener Dateien verwendet werden: +Für die Elemente `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` lassen sich außerdem die folgenden Regeln verwenden, um die Anzahl der ausgewählten Elemente oder der hochgeladenen Dateien zu begrenzen: -| `MinLength` | Minimale Anzahl | `int` -| `MaxLength` | Maximale Anzahl | `int` -| `Length` | Anzahl im Bereich oder genaue Anzahl | Paar `[int, int]` oder `int` +| `MinLength` | Mindestanzahl | `int` +| `MaxLength` | Höchstanzahl | `int` +| `Length` | Anzahl im Bereich oder genaue Anzahl | Paar `[int, int]` oder `int` -Datei-Uploads -------------- +Hochladen von Dateien +--------------------- -Für die Elemente `addUpload()`, `addMultiUpload()` können auch die folgenden Regeln verwendet werden: +Für die Elemente `addUpload()`, `addMultiUpload()` lassen sich außerdem die folgenden Regeln verwenden: -| `MaxFileSize` | Maximale Dateigröße in Bytes | `int` -| `MimeType` | MIME-Typ, Platzhalter erlaubt (`'video/*'`) | `string\|string[]` -| `Image` | Bild JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | Dateiname entspricht dem regulären Ausdruck | `string` -| `PatternInsensitive` | Wie `Pattern`, aber Groß-/Kleinschreibung egal | `string` +| `MaxFileSize` | maximale Größe der Datei in Bytes | `int` +| `MimeType` | MIME-Typ, Wildcards erlaubt (`'video/*'`) | `string\|string[]` +| `Image` | Bild im Format JPEG, PNG, GIF, WebP, AVIF | - +| `Pattern` | der Dateiname passt auf einen regulären Ausdruck | `string` +| `PatternInsensitive` | wie `Pattern`, aber ohne Rücksicht auf Groß-/Kleinschreibung | `string` -`MimeType` und `Image` erfordern die PHP-Erweiterung `fileinfo`. Ob es sich um eine Datei oder ein Bild des gewünschten Typs handelt, wird anhand seiner Signatur erkannt und **die Integrität der gesamten Datei wird nicht überprüft.** Ob ein Bild beschädigt ist, kann beispielsweise durch den Versuch, es zu [Laden |http:request#toImage], festgestellt werden. +`MimeType` und `Image` erfordern die PHP-Erweiterung `fileinfo`. Ob eine Datei oder ein Bild vom verlangten Typ ist, wird anhand ihrer Signatur erkannt, und **die Unversehrtheit der gesamten Datei wird nicht geprüft.** Ob ein Bild beschädigt ist, lässt sich zum Beispiel durch den Versuch feststellen, es zu [laden |http:request#toImage()]. Fehlermeldungen =============== -Alle vordefinierten Regeln außer `Pattern` und `PatternInsensitive` haben eine Standardfehlermeldung, sodass sie weggelassen werden kann. Durch die Angabe und Formulierung aller Meldungen nach Maß machen Sie das Formular jedoch benutzerfreundlicher. +Alle vordefinierten Regeln außer `Pattern` und `PatternInsensitive` haben eine Standard-Fehlermeldung, sie lassen sich also weglassen. Wenn Sie jedoch alle Meldungen selbst angeben und auf Ihre Bedürfnisse zuschneiden, machen Sie das Formular benutzerfreundlicher. -Die Standardmeldungen können Sie in der [Konfiguration |forms:configuration] ändern, indem Sie die Texte im Array `Nette\Forms\Validator::$messages` bearbeiten oder einen [Übersetzer |rendering#Übersetzung] verwenden. +Die Standardmeldungen ändern Sie in der [Konfiguration |forms:configuration], indem Sie die Texte im Array `Nette\Forms\Validator::$messages` anpassen, oder über einen [Übersetzer |rendering#Übersetzen]. -Im Text der Fehlermeldungen können diese Platzhalter verwendet werden: +Im Text der Fehlermeldungen lassen sich die folgenden Platzhalter verwenden: -| `%d` | Ersetzt nacheinander durch die Argumente der Regel -| `%n$d` | Ersetzt durch das n-te Argument der Regel -| `%label` | Ersetzt durch die Beschriftung des Elements (ohne Doppelpunkt) -| `%name` | Ersetzt durch den Namen des Elements (z.B. `name`) -| `%value` | Ersetzt durch den vom Benutzer eingegebenen Wert +| `%d` | wird der Reihe nach durch die Argumente der Regel ersetzt +| `%n$d` | wird durch das n-te Argument der Regel ersetzt +| `%label` | wird durch das Label des Elements ersetzt (ohne Doppelpunkt) +| `%name` | wird durch den Namen des Elements ersetzt (etwa `name`) +| `%value` | wird durch den vom Benutzer eingegebenen Wert ersetzt ```php $form->addText('name', 'Name:') @@ -115,70 +115,78 @@ $form->addInteger('id', 'ID:') Bedingungen =========== -Neben Regeln können auch Bedingungen hinzugefügt werden. Diese werden ähnlich wie Regeln geschrieben, nur verwenden wir statt `addRule()` die Methode `addCondition()` und geben natürlich keine Fehlermeldung an (die Bedingung fragt nur): +Neben Regeln lassen sich auch Bedingungen hinzufügen. Sie werden ähnlich geschrieben wie Regeln, statt `addRule()` verwenden wir aber die Methode `addCondition()`, und natürlich geben wir keine Fehlermeldung an (die Bedingung fragt nur): ```php $form->addPassword('password', 'Passwort:') - // wenn das Passwort nicht länger als 8 Zeichen ist + // wenn die Länge des Passworts nicht größer als 8 ist ->addCondition($form::MaxLength, 8) // dann muss es eine Ziffer enthalten ->addRule($form::Pattern, 'Muss eine Ziffer enthalten', '.*[0-9].*'); ``` -Eine Bedingung kann auch an ein anderes Element als das aktuelle gebunden werden, indem `addConditionOn()` verwendet wird. Als ersten Parameter geben wir eine Referenz auf das Element an. In diesem Beispiel wird die E-Mail nur dann erforderlich sein, wenn die Checkbox angekreuzt ist (ihr Wert wird true sein): +Über `addConditionOn()` lässt sich die Bedingung an ein anderes als das aktuelle Element knüpfen. Der erste Parameter ist ein Verweis auf das Element. In diesem Beispiel wird die E-Mail nur dann zum Pflichtfeld, wenn die Checkbox angehakt ist (ihr Wert also true ist): ```php -$form->addCheckbox('newsletters', 'Senden Sie mir Newsletter'); +$form->addCheckbox('newsletters', 'Newsletter zusenden'); $form->addEmail('email', 'E-Mail:') - // wenn die Checkbox angekreuzt ist + // wenn die Checkbox angehakt ist ->addConditionOn($form['newsletters'], $form::Equal, true) - // dann fordere E-Mail an - ->setRequired('Geben Sie eine E-Mail-Adresse ein'); + // dann die E-Mail verlangen + ->setRequired('Geben Sie Ihre E-Mail-Adresse ein'); ``` -Aus Bedingungen können komplexe Strukturen mithilfe von `elseCondition()` und `endCondition()` erstellt werden: +Über `elseCondition()` und `endCondition()` lassen sich Bedingungen zu komplexen Strukturen zusammensetzen: ```php $form->addText(/* ... */) ->addCondition(/* ... */) // wenn die erste Bedingung erfüllt ist - ->addConditionOn(/* ... */) // und die zweite Bedingung an einem anderen Element - ->addRule(/* ... */) // fordere diese Regel an + ->addConditionOn(/* ... */) // und auch die zweite Bedingung an einem anderen Element + ->addRule(/* ... */) // verlange diese Regel ->elseCondition() // wenn die zweite Bedingung nicht erfüllt ist - ->addRule(/* ... */) // fordere diese Regeln an + ->addRule(/* ... */) // verlange diese Regeln ->addRule(/* ... */) ->endCondition() // wir kehren zur ersten Bedingung zurück ->addRule(/* ... */); ``` -In Nette ist es sehr einfach, auf die Erfüllung oder Nichterfüllung einer Bedingung auch auf der JavaScript-Seite mit der Methode `toggle()` zu reagieren, siehe [#Dynamisches JavaScript]. +Das erste Argument von `addCondition()` kann auch ein boolescher Wert sein. Das ist nützlich, wenn die Entscheidung beim Bauen des Formulars bereits feststeht, etwa um eine Regel nur unter bestimmten Umständen anzuwenden: +```php +$form->addText('nickname') + ->addCondition($isRequired) // ein Wert, der beim Bauen des Formulars bekannt ist + ->setRequired(); +``` + +In Nette lässt sich sehr leicht über die Methode `toggle()` auf der JavaScript-Seite darauf reagieren, ob eine Bedingung erfüllt ist oder nicht, siehe [#Dynamisches JavaScript]. -Referenz auf ein anderes Element -================================ -Als Argument einer Regel oder Bedingung kann auch ein anderes Formularelement übergeben werden. Die Regel verwendet dann den Wert, der später vom Benutzer im Browser eingegeben wird. So kann z. B. dynamisch validiert werden, dass das Element `password` denselben String enthält wie das Element `password_confirm`: +Verweis auf ein anderes Element +=============================== + +Als Argument einer Regel oder Bedingung können Sie auch ein anderes Formularelement übergeben. Die Regel verwendet dann den Wert, den der Benutzer später im Browser eingibt. So lässt sich zum Beispiel dynamisch prüfen, dass das Element `password` denselben String enthält wie das Element `password_confirm`: ```php $form->addPassword('password', 'Passwort'); $form->addPassword('password_confirm', 'Passwort bestätigen') - ->addRule($form::Equal, 'Die eingegebenen Passwörter stimmen nicht überein', $form['password']); + ->addRule($form::Equal, 'Die Passwörter stimmen nicht überein', $form['password']); ``` -Benutzerdefinierte Regeln und Bedingungen -========================================= +Eigene Regeln und Bedingungen +============================= -Manchmal geraten wir in eine Situation, in der die eingebauten Validierungsregeln in Nette nicht ausreichen und wir die Benutzerdaten auf unsere eigene Weise validieren müssen. In Nette ist das sehr einfach! +Manchmal geraten wir in Situationen, in denen die eingebauten Validierungsregeln von Nette nicht ausreichen und wir die Daten des Benutzers auf eigene Weise prüfen müssen. In Nette ist das sehr einfach! -Den Methoden `addRule()` oder `addCondition()` kann als erster Parameter ein beliebiger Callback übergeben werden. Dieser erhält als ersten Parameter das Element selbst und gibt einen booleschen Wert zurück, der angibt, ob die Validierung erfolgreich war. Beim Hinzufügen einer Regel mit `addRule()` können auch weitere Argumente angegeben werden, die dann als zweiter Parameter übergeben werden. +Den Methoden `addRule()` und `addCondition()` können Sie als ersten Parameter ein beliebiges Callback übergeben. Das Callback nimmt als ersten Parameter das Element selbst entgegen und gibt einen booleschen Wert zurück, der angibt, ob die Validierung erfolgreich war. Beim Hinzufügen einer Regel über `addRule()` lassen sich weitere Argumente angeben, die dann als zweiter Parameter übergeben werden. -Einen eigenen Satz von Validatoren können wir als Klasse mit statischen Methoden erstellen: +Eine eigene Sammlung von Validatoren lässt sich also als Klasse mit statischen Methoden schreiben: ```php class MyValidators { - // testet, ob der Wert durch das Argument teilbar ist + // prüft, ob der Wert durch das Argument teilbar ist public static function validateDivisibility(BaseControl $input, $arg): bool { return $input->getValue() % $arg === 0; @@ -191,7 +199,7 @@ class MyValidators } ``` -Die Verwendung ist dann sehr einfach: +Die Verwendung ist dann sehr unkompliziert: ```php $form->addInteger('num') @@ -202,7 +210,7 @@ $form->addInteger('num') ); ``` -Benutzerdefinierte Validierungsregeln können auch zu JavaScript hinzugefügt werden. Voraussetzung ist, dass die Regel eine statische Methode ist. Ihr Name für den JavaScript-Validator wird durch Verkettung des Klassennamens ohne Backslashes `\`, eines Unterstrichs `_` und des Methodennamens gebildet. Z. B. schreiben wir `App\MyValidators::validateDivisibility` als `AppMyValidators_validateDivisibility` und fügen es zum Objekt `Nette.validators` hinzu: +Eigene Validierungsregeln lassen sich auch zu JavaScript hinzufügen. Die Bedingung ist, dass die Regel eine statische Methode sein muss. Ihr Name für den JavaScript-Validator entsteht, indem der Klassenname ohne Backslashes `\`, ein Unterstrich `_` und der Name der Methode aneinandergehängt werden. `App\MyValidators::validateDivisibility` wird zum Beispiel als `AppMyValidators_validateDivisibility` geschrieben und dem Objekt `Nette.validators` hinzugefügt: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -211,23 +219,23 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => ``` -Ereignis onValidate -=================== +Event onValidate +================ -Nach dem Absenden des Formulars wird die Validierung durchgeführt, bei der die einzelnen mit `addRule()` hinzugefügten Regeln überprüft werden und anschließend das [Ereignis |nette:glossary#Events Ereignisse] `onValidate` ausgelöst wird. Sein Handler kann für zusätzliche Validierungen verwendet werden, typischerweise zur Überprüfung der korrekten Kombination von Werten in mehreren Formularelementen. +Nachdem das Formular abgesendet wurde, läuft die Validierung, die die einzelnen über `addRule()` hinzugefügten Regeln prüft, und anschließend wird das [Event |nette:glossary#Events] `onValidate` ausgelöst. Sein Handler lässt sich für eine zusätzliche Validierung nutzen, typischerweise um die richtige Kombination von Werten in mehreren Formularelementen zu prüfen. -Wenn ein Fehler entdeckt wird, übergeben wir ihn mit der Methode `addError()` an das Formular. Diese kann entweder auf einem bestimmten Element oder direkt auf dem Formular aufgerufen werden. +Wird ein Fehler festgestellt, geben wir ihn über die Methode `addError()` an das Formular weiter. Sie lässt sich entweder auf einem bestimmten Element oder direkt auf dem Formular aufrufen. ```php protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('Diese Kombination ist nicht möglich.'); @@ -236,10 +244,10 @@ public function validateSignInForm(Form $form, \stdClass $data): void ``` -Fehler bei der Verarbeitung -=========================== +Fehler verarbeiten +================== -In vielen Fällen erfahren wir erst von einem Fehler, wenn wir das gültige Formular verarbeiten, z. B. wenn wir einen neuen Eintrag in die Datenbank schreiben und auf einen doppelten Schlüssel stoßen. In diesem Fall übergeben wir den Fehler erneut mit der Methode `addError()` an das Formular. Diese kann entweder auf einem bestimmten Element oder direkt auf dem Formular aufgerufen werden: +In vielen Fällen entdecken wir einen Fehler erst beim Verarbeiten eines gültigen Formulars, etwa wenn wir einen neuen Eintrag in die Datenbank schreiben und auf einen doppelten Schlüssel stoßen. In einem solchen Fall geben wir den Fehler wieder über die Methode `addError()` an das Formular zurück. Sie lässt sich entweder auf einem bestimmten Element oder direkt auf dem Formular aufrufen: ```php try { @@ -254,77 +262,83 @@ try { } ``` -Wenn möglich, empfehlen wir, den Fehler direkt an das Formularelement anzuhängen, da er bei Verwendung des Standard-Renderers daneben angezeigt wird. +Wenn es möglich ist, empfehlen wir, den Fehler direkt dem Formularelement hinzuzufügen, denn beim Standard-Renderer wird er dann neben ihm angezeigt. ```php -$form['date']->addError('Entschuldigung, aber dieses Datum ist bereits vergeben.'); +$form['date']->addError('Dieses Datum ist leider bereits vergeben.'); ``` -Sie können `addError()` wiederholt aufrufen und so dem Formular oder Element mehrere Fehlermeldungen übergeben. Sie erhalten sie mit `getErrors()`. +Sie können `addError()` wiederholt aufrufen, um einem Formular oder Element mehrere Fehlermeldungen zu übergeben. Holen lassen sie sich über `getErrors()`. -Achtung, `$form->getErrors()` gibt eine Zusammenfassung aller Fehlermeldungen zurück, auch derjenigen, die direkt an einzelne Elemente übergeben wurden, nicht nur direkt an das Formular. Fehlermeldungen, die nur an das Formular übergeben wurden, erhalten Sie über `$form->getOwnErrors()`. +Beachten Sie, dass `$form->getErrors()` eine Zusammenfassung aller Fehlermeldungen zurückgibt, auch der Meldungen, die direkt an einzelne Elemente übergeben wurden, nicht nur der direkt an das Formular übergebenen. Die Meldungen, die nur dem Formular übergeben wurden, holen Sie über `$form->getOwnErrors()`. -Anpassung der Eingabe -===================== +Eingaben verändern +================== -Mit der Methode `addFilter()` können wir den vom Benutzer eingegebenen Wert ändern. In diesem Beispiel tolerieren und entfernen wir Leerzeichen in der Postleitzahl: +Über die Methode `addFilter()` können wir den vom Benutzer eingegebenen Wert verändern. In diesem Beispiel dulden und entfernen wir Leerzeichen in der Postleitzahl: ```php -$form->addText('zip', 'PLZ:') +$form->addText('zip', 'Postleitzahl:') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // entfernen Leerzeichen aus der PLZ + return str_replace(' ', '', $value); // die Leerzeichen aus der Postleitzahl entfernen }) - ->addRule($form::Pattern, 'PLZ ist nicht im Format von fünf Ziffern', '\d{5}'); + ->addRule($form::Pattern, 'Die Postleitzahl besteht nicht aus fünf Ziffern', '\d{5}'); ``` -Der Filter wird zwischen Validierungsregeln und Bedingungen eingefügt, daher hängt es von der Reihenfolge der Methoden ab, d. h. Filter und Regel werden in der Reihenfolge aufgerufen, in der die Methoden `addFilter()` und `addRule()` stehen. +Der Filter reiht sich zwischen die Validierungsregeln und Bedingungen ein, die Reihenfolge der Methoden ist also wichtig: Filter und Regel werden in derselben Reihenfolge aufgerufen, in der die Methoden `addFilter()` und `addRule()` aufgeführt sind. -JavaScript-Validierung -====================== +Validierung in JavaScript +========================= -Die Sprache zur Formulierung von Bedingungen und Regeln ist sehr mächtig. Alle Konstrukte funktionieren dabei sowohl serverseitig als auch clientseitig in JavaScript. Sie werden in HTML-Attributen `data-nette-rules` als JSON übertragen. Die eigentliche Validierung führt dann ein Skript durch, das das `submit`-Ereignis des Formulars abfängt, die einzelnen Elemente durchläuft und die entsprechende Validierung durchführt. +Die Sprache zum Formulieren von Bedingungen und Regeln ist sehr mächtig. Alle Konstrukte funktionieren sowohl auf der Serverseite als auch auf der Clientseite in JavaScript. Übertragen werden sie in den HTML-Attributen `data-nette-rules` als JSON. Die Validierung selbst übernimmt ein Skript, das das Event `submit` des Formulars abfängt, die einzelnen Elemente durchläuft und die jeweilige Validierung ausführt. -Dieses Skript ist `netteForms.js` und ist aus mehreren möglichen Quellen verfügbar: +Dieses Skript heißt `netteForms.js` und steht aus mehreren möglichen Quellen zur Verfügung: -Sie können das Skript direkt von einem CDN in die HTML-Seite einbinden: +Sie können das Skript direkt aus einem CDN in die HTML-Seite einbinden: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Oder lokal in den öffentlichen Ordner des Projekts kopieren (z. B. aus `vendor/nette/forms/src/assets/netteForms.min.js`): +Oder es lokal in das öffentliche Verzeichnis Ihres Projekts kopieren (etwa aus `vendor/nette/forms/src/assets/netteForms.min.js`): ```latte <script src="/path/to/netteForms.min.js"></script> ``` -Oder über [npm |https://www.npmjs.com/package/nette-forms] installieren: +Oder es über [npm |https://www.npmjs.com/package/nette-forms] installieren: ```shell npm install nette-forms ``` -Und anschließend laden und starten: +Und es dann laden und starten: ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -Alternativ können Sie es direkt aus dem `vendor`-Ordner laden: +Alternativ lässt es sich direkt aus dem Verzeichnis `vendor` laden: ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Die Validierung auf der Clientseite lässt sich vollständig abschalten, indem Sie dem Formular das Attribut `novalidate` geben. Das Skript `netteForms.js` überspringt dann die Validierung beim Absenden, sodass sie nur auf dem Server stattfindet: + +```php +$form->setHtmlAttribute('novalidate'); +``` + Dynamisches JavaScript ====================== -Möchten Sie die Felder zur Eingabe der Adresse nur anzeigen, wenn der Benutzer den Versand per Post wählt? Kein Problem. Der Schlüssel ist das Methodenpaar `addCondition()` & `toggle()`: +Sie möchten die Felder für die Adresse nur dann anzeigen, wenn der Benutzer wählt, dass die Ware per Post zugestellt werden soll? Kein Problem. Der Schlüssel ist das Methodenpaar `addCondition()` & `toggle()`: ```php $form->addCheckbox('send_it') @@ -332,25 +346,25 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -Dieser Code besagt, dass, wenn die Bedingung erfüllt ist, d. h. wenn die Checkbox angekreuzt ist, das HTML-Element `#address-container` sichtbar sein wird. Und umgekehrt. Die Formularelemente mit der Empfängeradresse platzieren wir also in einem Container mit dieser ID, und beim Klicken auf die Checkbox werden sie ausgeblendet oder angezeigt. Dafür sorgt das Skript `netteForms.js`. +Dieser Code besagt, dass bei erfüllter Bedingung (also wenn die Checkbox angehakt ist) das HTML-Element `#address-container` sichtbar ist und umgekehrt. Wir setzen die Formularelemente mit der Adresse des Empfängers also in einen Container mit dieser ID, und sie verstecken oder zeigen sich beim Klick auf die Checkbox. Dafür sorgt das Skript `netteForms.js`. -Als Argument der Methode `toggle()` kann ein beliebiger Selektor übergeben werden. Aus historischen Gründen wird ein alphanumerischer String ohne weitere Sonderzeichen als ID des Elements verstanden, also genauso, als ob ihm das Zeichen `#` vorangestellt wäre. Der zweite optionale Parameter ermöglicht es, das Verhalten umzukehren, d. h. wenn wir `toggle('#address-container', false)` verwenden würden, würde das Element umgekehrt nur dann angezeigt, wenn die Checkbox nicht angekreuzt wäre. +Der Methode `toggle()` lässt sich als Argument ein beliebiger Selektor übergeben. Aus historischen Gründen gilt ein String, der mit einem Buchstaben, einer Ziffer oder einem Unterstrich beginnt und nur Buchstaben, Ziffern, Unterstriche, Bindestriche, Punkte und Doppelpunkte enthält, als ID eines Elements, so als stünde davor das Zeichen `#`. Der zweite, optionale Parameter erlaubt es, das Verhalten umzukehren; verwendeten wir zum Beispiel `toggle('#address-container', false)`, würde das Element nur dann angezeigt, wenn die Checkbox *nicht* angehakt ist. -Die Standardimplementierung in JavaScript ändert die `hidden`-Eigenschaft der Elemente. Das Verhalten können wir jedoch leicht ändern, z. B. eine Animation hinzufügen. Es genügt, die Methode `Nette.toggle` in JavaScript durch eine eigene Lösung zu überschreiben: +Die Standardimplementierung in JavaScript ändert die Property `hidden` der Elemente. Wir können das Verhalten aber leicht ändern, etwa um eine Animation zu ergänzen. Überschreiben Sie dazu einfach die Methode `Nette.toggle` in JavaScript durch eine eigene Lösung: ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // Blenden Sie 'el' entsprechend dem Wert von 'visible' ein oder aus + // 'el' je nach dem Wert von 'visible' verstecken oder anzeigen }); }; ``` -Validierung deaktivieren -======================== +Validierung abschalten +====================== -Manchmal kann es nützlich sein, die Validierung zu deaktivieren. Wenn das Drücken eines Sende-Buttons keine Validierung durchführen soll (geeignet für *Abbrechen*- oder *Vorschau*-Buttons), deaktivieren wir sie mit der Methode `$submit->setValidationScope([])`. Wenn nur eine teilweise Validierung durchgeführt werden soll, können wir festlegen, welche Felder oder Formularcontainer validiert werden sollen. +Manchmal kann es nützlich sein, die Validierung abzuschalten. Wenn das Drücken eines Absende-Buttons keine Validierung auslösen soll (was sich für die Buttons *Abbrechen* oder *Vorschau* anbietet), schalten wir sie über die Methode `$submit->setValidationScope([])` ab. Soll sie nur teilweise validieren, können wir angeben, welche Felder oder Formular-Container validiert werden sollen. ```php $form->addText('name') @@ -362,15 +376,17 @@ $details->addInteger('age') $details->addInteger('age2') ->setRequired('age2'); -$form->addSubmit('send1'); // Validiert das gesamte Formular +$form->addSubmit('send1'); // validiert das gesamte Formular $form->addSubmit('send2') - ->setValidationScope([]); // Validiert überhaupt nicht + ->setValidationScope([]); // validiert nichts $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Validiert nur das Element name + ->setValidationScope([$form['name']]); // validiert nur das Element 'name' $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Validiert nur das Element age + ->setValidationScope([$form['details']['age']]); // validiert nur das Element 'age' $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Validiert den Container details + ->setValidationScope([$form['details']]); // validiert den Container 'details' ``` -`setValidationScope` beeinflusst nicht das [#Ereignis onValidate] des Formulars, das immer aufgerufen wird. Das `onValidate`-Ereignis eines Containers wird nur ausgelöst, wenn dieser Container für die Teilvalidierung markiert ist. +`setValidationScope` hat keinen Einfluss auf das [#Event onValidate] des Formulars, das immer aufgerufen wird. Das Event `onValidate` eines Containers wird nur dann ausgelöst, wenn dieser Container für die teilweise Validierung markiert ist. + +Die teilweise Validierung wirkt sich auch auf die Werte aus, die `getValues()` zurückgibt: Das Ergebnis enthält nur die Werte der Elemente, die im Gültigkeitsbereich der Validierung liegen. Die Werte der Elemente außerhalb dieses Bereichs entfallen. diff --git a/forms/el/@home.texy b/forms/el/@home.texy deleted file mode 100644 index 1250a801a3..0000000000 --- a/forms/el/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Το Nette Forms έφερε επανάσταση στη δημιουργία φορμών web. Ξαφνικά, αρκούσε να γράψετε μερικές κατανοητές γραμμές κώδικα και είχατε έτοιμη μια φόρμα, συμπεριλαμβανομένης της απόδοσης, της επικύρωσης JavaScript και server-side, και επιπλέον εξαιρετικά ασφαλισμένη. Θα δείξουμε πώς - -- να δημιουργείτε φιλικές φόρμες -- να επικυρώνετε τα υποβληθέντα δεδομένα -- να αποδίδετε τα στοιχεία ακριβώς όπως χρειάζεται - -</div> - - -Χρησιμοποιώντας το Nette Forms αποφεύγετε μια ολόκληρη σειρά από ρουτίνες εργασίες, όπως η συγγραφή επικύρωσης (επιπλέον διπλής, από την πλευρά του server και του client), ελαχιστοποιείτε την πιθανότητα εμφάνισης σφαλμάτων και κενών ασφαλείας. - -Μπορείτε να χρησιμοποιήσετε τις φόρμες είτε ως μέρος της Εφαρμογής Nette (δηλαδή σε presenters), είτε εντελώς αυτόνομα. Επειδή και στις δύο περιπτώσεις η χρήση διαφέρει λίγο, ετοιμάσαμε για εσάς δύο οδηγούς: - -<div class="wiki-buttons"> -<div> "Φόρμες σε presenters .[wiki-button]":in-presenter </div> -<div> "Φόρμες αυτόνομα .[wiki-button]":standalone </div> -</div> - - -Εγκατάσταση ------------ - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/el/@left-menu.texy b/forms/el/@left-menu.texy deleted file mode 100644 index bbf4c1020e..0000000000 --- a/forms/el/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Εισαγωγή |@home] -- [Φόρμες σε presenters|in-presenter] -- [Φόρμες αυτόνομα|standalone] -- [Στοιχεία φόρμας |controls] -- [Επικύρωση |validation] -- [Απόδοση |rendering] -- [Διαμόρφωση |configuration] - - -Περαιτέρω ανάγνωση -****************** -- [Οδηγοί και διαδικασίες |best-practices:] diff --git a/forms/el/@meta.texy b/forms/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/forms/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/forms/el/configuration.texy b/forms/el/configuration.texy deleted file mode 100644 index 44fcbe1719..0000000000 --- a/forms/el/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Διαμόρφωση φορμών -***************** - -.[perex] -Στη διαμόρφωση, μπορείτε να αλλάξετε τα προεπιλεγμένα [μηνύματα σφάλματος φόρμας|validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Εδώ είναι η ελληνική μετάφραση: - -```neon -forms: - messages: - Equal: 'Παρακαλώ εισάγετε %s.' - NotEqual: 'Αυτή η τιμή δεν πρέπει να είναι %s.' - Filled: 'Αυτό το πεδίο είναι υποχρεωτικό.' - Blank: 'Αυτό το πεδίο πρέπει να είναι κενό.' - MinLength: 'Παρακαλώ εισάγετε τουλάχιστον %d χαρακτήρες.' - MaxLength: 'Παρακαλώ εισάγετε το πολύ %d χαρακτήρες.' - Length: 'Παρακαλώ εισάγετε μια τιμή μήκους μεταξύ %d και %d χαρακτήρων.' - Email: 'Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email.' - URL: 'Παρακαλώ εισάγετε ένα έγκυρο URL.' - Integer: 'Παρακαλώ εισάγετε έναν έγκυρο ακέραιο αριθμό.' - Float: 'Παρακαλώ εισάγετε έναν έγκυρο αριθμό.' - Min: 'Παρακαλώ εισάγετε μια τιμή μεγαλύτερη ή ίση με %d.' - Max: 'Παρακαλώ εισάγετε μια τιμή μικρότερη ή ίση με %d.' - Range: 'Παρακαλώ εισάγετε μια τιμή μεταξύ %d και %d.' - MaxFileSize: 'Το μέγεθος του ανεβασμένου αρχείου μπορεί να είναι έως %d bytes.' - MaxPostSize: 'Τα ανεβασμένα δεδομένα υπερβαίνουν το όριο των %d bytes.' - MimeType: 'Το ανεβασμένο αρχείο δεν είναι στην αναμενόμενη μορφή.' - Image: 'Το ανεβασμένο αρχείο πρέπει να είναι εικόνα σε μορφή JPEG, GIF, PNG, WebP ή AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Παρακαλώ επιλέξτε μια έγκυρη επιλογή.' - Nette\Forms\Controls\UploadControl::Valid: 'Παρουσιάστηκε σφάλμα κατά τη μεταφόρτωση του αρχείου.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Η συνεδρία σας έχει λήξει. Παρακαλώ επιστρέψτε στην αρχική σελίδα και προσπαθήστε ξανά.' -``` - -Εάν δεν χρησιμοποιείτε ολόκληρο το framework και επομένως ούτε τα αρχεία διαμόρφωσης, μπορείτε να αλλάξετε τα προεπιλεγμένα μηνύματα σφάλματος απευθείας στον πίνακα `Nette\Forms\Validator::$messages`. diff --git a/forms/el/controls.texy b/forms/el/controls.texy deleted file mode 100644 index faa1fdb363..0000000000 --- a/forms/el/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Στοιχεία φόρμας -*************** - -.[perex] -Επισκόπηση των τυπικών στοιχείων φόρμας. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Προσθέτει ένα πεδίο κειμένου μίας γραμμής (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Εάν ο χρήστης δεν συμπληρώσει το πεδίο, επιστρέφει ένα κενό string `''`, ή με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null`. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired() - ->setNullable(); -``` - -Επικυρώνει αυτόματα το UTF-8, αφαιρεί τα κενά στην αρχή και στο τέλος και αφαιρεί τις αλλαγές γραμμής που θα μπορούσε να στείλει ένας εισβολέας. - -Το μέγιστο μήκος μπορεί να περιοριστεί με τη χρήση του `setMaxLength()`. Η τροποποίηση της τιμής που εισήγαγε ο χρήστης είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. - -Με τη χρήση του `setHtmlType()` μπορεί να αλλάξει ο οπτικός χαρακτήρας του πεδίου κειμένου σε τύπους όπως `search`, `tel` ή `url` δείτε τις [προδιαγραφές|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Να θυμάστε ότι η αλλαγή τύπου είναι μόνο οπτική και δεν αντικαθιστά τη λειτουργία επικύρωσης. Για τον τύπο `url` είναι σκόπιμο να προστεθεί ένας συγκεκριμένος [κανόνας επικύρωσης URL |validation#Είσοδοι Κειμένου]. - -.[note] -Για άλλους τύπους εισόδου, όπως `number`, `range`, `email`, `date`, `datetime-local`, `time` και `color`, χρησιμοποιήστε τις εξειδικευμένες μεθόδους όπως [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] και [#addColor], οι οποίες εξασφαλίζουν την επικύρωση από την πλευρά του διακομιστή. Οι τύποι `month` και `week` δεν υποστηρίζονται ακόμη πλήρως σε όλα τα προγράμματα περιήγησης. - -Στο στοιχείο μπορεί να οριστεί η λεγόμενη empty-value, η οποία είναι κάτι σαν προεπιλεγμένη τιμή, αλλά αν ο χρήστης δεν την αλλάξει, το στοιχείο επιστρέφει κενό string ή `null`. - -```php -$form->addText('phone', 'Τηλέφωνο:') - ->setHtmlType('tel') - ->setEmptyValue('+30'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Προσθέτει ένα πεδίο για την εισαγωγή κειμένου πολλαπλών γραμμών (κλάση [TextArea |api:Nette\Forms\Controls\TextArea]). Εάν ο χρήστης δεν συμπληρώσει το πεδίο, επιστρέφει ένα κενό string `''`, ή με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null`. - -```php -$form->addTextArea('note', 'Σημείωση:') - ->addRule($form::MaxLength, 'Η σημείωση είναι πολύ μεγάλη', 10000); -``` - -Επικυρώνει αυτόματα το UTF-8 και κανονικοποιεί τους διαχωριστές γραμμών σε `\n`. Σε αντίθεση με το πεδίο εισόδου μίας γραμμής, δεν γίνεται καμία αφαίρεση κενών. - -Το μέγιστο μήκος μπορεί να περιοριστεί με τη χρήση του `setMaxLength()`. Η τροποποίηση της τιμής που εισήγαγε ο χρήστης είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. Μπορεί να οριστεί η λεγόμενη empty-value με τη χρήση του `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Προσθέτει ένα πεδίο για την εισαγωγή ακέραιου αριθμού (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Επιστρέφει είτε integer, είτε `null`, εάν ο χρήστης δεν εισάγει τίποτα. - -```php -$form->addInteger('year', 'Έτος:') - ->addRule($form::Range, 'Το έτος πρέπει να είναι στο εύρος από %d έως %d.', [1900, 2023]); -``` - -Το στοιχείο αποδίδεται ως `<input type="number">`. Με τη χρήση της μεθόδου `setHtmlType()` μπορεί να αλλάξει ο τύπος σε `range` για εμφάνιση με τη μορφή ολισθητήρα, ή σε `text`, εάν προτιμάτε ένα τυπικό πεδίο κειμένου χωρίς την ειδική συμπεριφορά του τύπου `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Προσθέτει ένα πεδίο για την εισαγωγή δεκαδικού αριθμού (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Επιστρέφει είτε float, είτε `null`, εάν ο χρήστης δεν εισάγει τίποτα. - -```php -$form->addFloat('level', 'Επίπεδο:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Το επίπεδο πρέπει να είναι στο εύρος από %d έως %d.', [0, 100]); -``` - -Το στοιχείο αποδίδεται ως `<input type="number">`. Με τη χρήση της μεθόδου `setHtmlType()` μπορεί να αλλάξει ο τύπος σε `range` για εμφάνιση με τη μορφή ολισθητήρα, ή σε `text`, εάν προτιμάτε ένα τυπικό πεδίο κειμένου χωρίς την ειδική συμπεριφορά του τύπου `number`. - -Το Nette και ο περιηγητής Chrome αποδέχονται τόσο το κόμμα όσο και την τελεία ως διαχωριστικό δεκαδικών ψηφίων. Για να είναι διαθέσιμη αυτή η λειτουργικότητα και στον Firefox, συνιστάται να ορίσετε το χαρακτηριστικό `lang` είτε για το συγκεκριμένο στοιχείο είτε για ολόκληρη τη σελίδα, για παράδειγμα `<html lang="el">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Προσθέτει ένα πεδίο για την εισαγωγή διεύθυνσης email (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Εάν ο χρήστης δεν συμπληρώσει το πεδίο, επιστρέφει ένα κενό string `''`, ή με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Επαληθεύει εάν η τιμή είναι έγκυρη διεύθυνση email. Δεν επαληθεύεται εάν ο τομέας υπάρχει πραγματικά, επαληθεύεται μόνο η σύνταξη. Επικυρώνει αυτόματα το UTF-8, αφαιρεί τα κενά στην αρχή και στο τέλος. - -Το μέγιστο μήκος μπορεί να περιοριστεί με τη χρήση του `setMaxLength()`. Η τροποποίηση της τιμής που εισήγαγε ο χρήστης είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. Μπορεί να οριστεί η λεγόμενη empty-value με τη χρήση του `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Προσθέτει ένα πεδίο για την εισαγωγή κωδικού πρόσβασης (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->setRequired() - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8) - ->addRule($form::Pattern, 'Πρέπει να περιέχει αριθμό', '.*[0-9].*'); -``` - -Κατά την εκ νέου εμφάνιση της φόρμας, το πεδίο θα είναι κενό. Επικυρώνει αυτόματα το UTF-8, αφαιρεί τα κενά στην αρχή και στο τέλος και αφαιρεί τις αλλαγές γραμμής που θα μπορούσε να στείλει ένας εισβολέας. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Προσθέτει ένα πλαίσιο ελέγχου (κλάση [Checkbox |api:Nette\Forms\Controls\Checkbox]). Επιστρέφει την τιμή είτε `true` είτε `false`, ανάλογα με το αν είναι επιλεγμένο. - -```php -$form->addCheckbox('agree', 'Συμφωνώ με τους όρους') - ->setRequired('Είναι απαραίτητο να συμφωνήσετε με τους όρους'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Προσθέτει πλαίσια ελέγχου για την επιλογή πολλαπλών στοιχείων (κλάση [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Επιστρέφει έναν πίνακα με τα κλειδιά των επιλεγμένων στοιχείων. Η μέθοδος `getSelectedItems()` επιστρέφει τις τιμές αντί για τα κλειδιά. - -```php -$form->addCheckboxList('colors', 'Χρώματα:', [ - 'r' => 'κόκκινο', - 'g' => 'πράσινο', - 'b' => 'μπλε', -]); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. - -Με τη χρήση του `setDisabled(['r', 'g'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι τα επιλεγμένα στοιχεία είναι πράγματι ένα από τα προσφερόμενα και δεν έχουν απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορούν να ληφθούν τα υποβληθέντα στοιχεία χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό των προεπιλεγμένων επιλεγμένων στοιχείων, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - -Εάν υποβάλλετε τη φόρμα με τη μέθοδο `GET`, μπορείτε να επιλέξετε έναν πιο συμπαγή τρόπο μετάδοσης δεδομένων, ο οποίος εξοικονομεί μέγεθος στο query string. Ενεργοποιείται ορίζοντας το HTML attribute της φόρμας: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Προσθέτει κουμπιά επιλογής (κλάση [RadioList |api:Nette\Forms\Controls\RadioList]). Επιστρέφει το κλειδί του επιλεγμένου στοιχείου, ή `null`, εάν ο χρήστης δεν επέλεξε τίποτα. Η μέθοδος `getSelectedItem()` επιστρέφει την τιμή αντί για το κλειδί. - -```php -$sex = [ - 'm' => 'άνδρας', - 'f' => 'γυναίκα', -]; -$form->addRadioList('gender', 'Φύλο:', $sex); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. - -Με τη χρήση του `setDisabled(['m', 'f'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι το επιλεγμένο στοιχείο είναι πράγματι ένα από τα προσφερόμενα και δεν έχει απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορεί να ληφθεί το υποβληθέν στοιχείο χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό του προεπιλεγμένου επιλεγμένου στοιχείου, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Προσθέτει ένα select box (κλάση [SelectBox |api:Nette\Forms\Controls\SelectBox]). Επιστρέφει το κλειδί του επιλεγμένου στοιχείου, ή `null`, εάν ο χρήστης δεν επέλεξε τίποτα. Η μέθοδος `getSelectedItem()` επιστρέφει την τιμή αντί για το κλειδί. - -```php -$countries = [ - 'CZ' => 'Τσεχία', - 'SK' => 'Σλοβακία', - 'GR' => 'Ελλάδα', -]; - -$form->addSelect('country', 'Χώρα:', $countries) - ->setDefaultValue('GR'); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. Τα στοιχεία μπορούν να είναι και δισδιάστατος πίνακας: - -```php -$countries = [ - 'Europe' => [ - 'CZ' => 'Τσεχία', - 'SK' => 'Σλοβακία', - 'GR' => 'Ελλάδα', - ], - 'CA' => 'Καναδάς', - 'US' => 'ΗΠΑ', - '?' => 'άλλη', -]; -``` - -Στα select boxes, συχνά το πρώτο στοιχείο έχει ειδική σημασία, χρησιμεύει ως προτροπή για δράση. Για την προσθήκη ενός τέτοιου στοιχείου χρησιμοποιείται η μέθοδος `setPrompt()`. - -```php -$form->addSelect('country', 'Χώρα:', $countries) - ->setPrompt('Επιλέξτε χώρα'); -``` - -Με τη χρήση του `setDisabled(['CZ', 'SK'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι το επιλεγμένο στοιχείο είναι πράγματι ένα από τα προσφερόμενα και δεν έχει απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορεί να ληφθεί το υποβληθέν στοιχείο χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό του προεπιλεγμένου επιλεγμένου στοιχείου, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Προσθέτει ένα select box για την επιλογή πολλαπλών στοιχείων (κλάση [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Επιστρέφει έναν πίνακα με τα κλειδιά των επιλεγμένων στοιχείων. Η μέθοδος `getSelectedItems()` επιστρέφει τις τιμές αντί για τα κλειδιά. - -```php -$form->addMultiSelect('countries', 'Χώρες:', $countries); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. Τα στοιχεία μπορούν να είναι και δισδιάστατος πίνακας. - -Με τη χρήση του `setDisabled(['CZ', 'SK'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι τα επιλεγμένα στοιχεία είναι πράγματι ένα από τα προσφερόμενα και δεν έχουν απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορούν να ληφθούν τα υποβληθέντα στοιχεία χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό των προεπιλεγμένων επιλεγμένων στοιχείων, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Προσθέτει ένα πεδίο για τη μεταφόρτωση αρχείου (κλάση [UploadControl |api:Nette\Forms\Controls\UploadControl]). Επιστρέφει ένα αντικείμενο [FileUpload |http:request#FileUpload] ακόμα και στην περίπτωση που ο χρήστης δεν υπέβαλε κανένα αρχείο, κάτι που μπορεί να διαπιστωθεί με τη μέθοδο `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Το Avatar πρέπει να είναι JPEG, PNG, GIF, WebP ή AVIF.') - ->addRule($form::MaxFileSize, 'Το μέγιστο μέγεθος είναι 1 MB.', 1024 * 1024); -``` - -Εάν το αρχείο δεν μεταφορτωθεί σωστά, η φόρμα δεν υποβάλλεται επιτυχώς και εμφανίζεται σφάλμα. Δηλαδή, κατά την επιτυχή υποβολή δεν χρειάζεται να επαληθεύσετε τη μέθοδο `FileUpload::isOk()`. - -Ποτέ μην εμπιστεύεστε το αρχικό όνομα του αρχείου που επιστρέφεται από τη μέθοδο `FileUpload::getName()`, ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να χακάρει την εφαρμογή σας. - -Οι κανόνες `MimeType` και `Image` ανιχνεύουν τον απαιτούμενο τύπο βάσει της υπογραφής του αρχείου και δεν επαληθεύουν την ακεραιότητά του. Το αν μια εικόνα είναι κατεστραμμένη μπορεί να διαπιστωθεί, για παράδειγμα, προσπαθώντας να τη [φορτώσετε |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Προσθέτει ένα πεδίο για τη μεταφόρτωση πολλαπλών αρχείων ταυτόχρονα (κλάση [UploadControl |api:Nette\Forms\Controls\UploadControl]). Επιστρέφει έναν πίνακα αντικειμένων [FileUpload |http:request#FileUpload]. Η μέθοδος `FileUpload::hasFile()` σε καθένα από αυτά θα επιστρέφει `true`. - -```php -$form->addMultiUpload('files', 'Αρχεία:') - ->addRule($form::MaxLength, 'Μπορούν να μεταφορτωθούν το πολύ %d αρχεία', 10); -``` - -Εάν κάποιο αρχείο δεν μεταφορτωθεί σωστά, η φόρμα δεν υποβάλλεται επιτυχώς και εμφανίζεται σφάλμα. Δηλαδή, κατά την επιτυχή υποβολή δεν χρειάζεται να επαληθεύσετε τη μέθοδο `FileUpload::isOk()`. - -Ποτέ μην εμπιστεύεστε τα αρχικά ονόματα των αρχείων που επιστρέφονται από τη μέθοδο `FileUpload::getName()`, ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να χακάρει την εφαρμογή σας. - -Οι κανόνες `MimeType` και `Image` ανιχνεύουν τον απαιτούμενο τύπο βάσει της υπογραφής του αρχείου και δεν επαληθεύουν την ακεραιότητά του. Το αν μια εικόνα είναι κατεστραμμένη μπορεί να διαπιστωθεί, για παράδειγμα, προσπαθώντας να τη [φορτώσετε |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Προσθέτει ένα πεδίο που επιτρέπει στον χρήστη να εισάγει εύκολα μια ημερομηνία που αποτελείται από έτος, μήνα και ημέρα (κλάση [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ως προεπιλεγμένη τιμή δέχεται είτε αντικείμενα που υλοποιούν το interface `DateTimeInterface`, ένα string με χρόνο, είτε έναν αριθμό που αντιπροσωπεύει UNIX timestamp. Το ίδιο ισχύει για τα ορίσματα των κανόνων `Min`, `Max` ή `Range`, τα οποία ορίζουν την ελάχιστη και μέγιστη επιτρεπόμενη ημερομηνία. - -```php -$form->addDate('date', 'Ημερομηνία:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Η ημερομηνία πρέπει να είναι τουλάχιστον ενός μηνός παλιά.', new DateTime('-1 month')); -``` - -Συνήθως επιστρέφει ένα αντικείμενο `DateTimeImmutable`, με τη μέθοδο `setFormat()` μπορείτε να καθορίσετε τη [μορφή κειμένου|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ή timestamp: - -```php -$form->addDate('date', 'Ημερομηνία:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Προσθέτει ένα πεδίο που επιτρέπει στον χρήστη να εισάγει εύκολα έναν χρόνο που αποτελείται από ώρες, λεπτά και προαιρετικά δευτερόλεπτα (κλάση [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ως προεπιλεγμένη τιμή δέχεται είτε αντικείμενα που υλοποιούν το interface `DateTimeInterface`, ένα string με χρόνο, είτε έναν αριθμό που αντιπροσωπεύει UNIX timestamp. Από αυτές τις εισόδους χρησιμοποιείται μόνο η πληροφορία του χρόνου, η ημερομηνία αγνοείται. Το ίδιο ισχύει για τα ορίσματα των κανόνων `Min`, `Max` ή `Range`, τα οποία ορίζουν τον ελάχιστο και μέγιστο επιτρεπόμενο χρόνο. Εάν η καθορισμένη ελάχιστη τιμή είναι υψηλότερη από τη μέγιστη, δημιουργείται ένα χρονικό εύρος που υπερβαίνει τα μεσάνυχτα. - -```php -$form->addTime('time', 'Ώρα:', withSeconds: true) - ->addRule($form::Range, 'Η ώρα πρέπει να είναι στο εύρος από %s έως %s.', ['12:30', '13:30']); -``` - -Συνήθως επιστρέφει ένα αντικείμενο `DateTimeImmutable` (με ημερομηνία 1 Ιανουαρίου του έτους 1), με τη μέθοδο `setFormat()` μπορείτε να καθορίσετε τη [μορφή κειμένου|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Ώρα:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Προσθέτει ένα πεδίο που επιτρέπει στον χρήστη να εισάγει εύκολα ημερομηνία και ώρα που αποτελείται από έτος, μήνα, ημέρα, ώρες, λεπτά και προαιρετικά δευτερόλεπτα (κλάση [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ως προεπιλεγμένη τιμή δέχεται είτε αντικείμενα που υλοποιούν το interface `DateTimeInterface`, ένα string με χρόνο, είτε έναν αριθμό που αντιπροσωπεύει UNIX timestamp. Το ίδιο ισχύει για τα ορίσματα των κανόνων `Min`, `Max` ή `Range`, τα οποία ορίζουν την ελάχιστη και μέγιστη επιτρεπόμενη ημερομηνία. - -```php -$form->addDateTime('datetime', 'Ημερομηνία και ώρα:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Η ημερομηνία πρέπει να είναι τουλάχιστον ενός μηνός παλιά.', new DateTime('-1 month')); -``` - -Συνήθως επιστρέφει ένα αντικείμενο `DateTimeImmutable`, με τη μέθοδο `setFormat()` μπορείτε να καθορίσετε τη [μορφή κειμένου|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ή timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Προσθέτει ένα πεδίο για την επιλογή χρώματος (κλάση [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Το χρώμα είναι ένα string στη μορφή `#rrggbb`. Εάν ο χρήστης δεν κάνει επιλογή, επιστρέφεται το μαύρο χρώμα `#000000`. - -```php -$form->addColor('color', 'Χρώμα:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Προσθέτει ένα κρυφό πεδίο (κλάση [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null` αντί για κενό string. Η τροποποίηση της υποβληθείσας τιμής είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. - -Παρόλο που το στοιχείο είναι κρυφό, είναι **σημαντικό να συνειδητοποιήσετε** ότι η τιμή μπορεί ακόμα να τροποποιηθεί ή να πλαστογραφηθεί από έναν εισβολέα. Πάντα επαληθεύετε και επικυρώνετε διεξοδικά όλες τις λαμβανόμενες τιμές στην πλευρά του διακομιστή για να αποφύγετε κινδύνους ασφαλείας που σχετίζονται με τη χειραγώγηση δεδομένων. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Προσθέτει ένα κουμπί υποβολής (κλάση [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Υποβολή'); -``` - -Στη φόρμα είναι δυνατόν να υπάρχουν και περισσότερα κουμπιά υποβολής: - -```php -$form->addSubmit('register', 'Εγγραφή'); -$form->addSubmit('cancel', 'Ακύρωση'); -``` - -Για να διαπιστώσετε σε ποιο από αυτά έγινε κλικ, χρησιμοποιήστε: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Εάν δεν θέλετε να επικυρώσετε ολόκληρη τη φόρμα κατά το πάτημα του κουμπιού (για παράδειγμα, στα κουμπιά *Ακύρωση* ή *Προεπισκόπηση*), χρησιμοποιήστε το [setValidationScope() |validation#Απενεργοποίηση Επικύρωσης]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Προσθέτει ένα κουμπί (κλάση [Button |api:Nette\Forms\Controls\Button]), το οποίο δεν έχει λειτουργία υποβολής. Μπορεί επομένως να χρησιμοποιηθεί για κάποια άλλη λειτουργία, π.χ. κλήση μιας συνάρτησης JavaScript κατά το κλικ. - -```php -$form->addButton('raise', 'Αύξηση μισθού') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Προσθέτει ένα κουμπί υποβολής με τη μορφή εικόνας (κλάση [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -Κατά τη χρήση πολλαπλών κουμπιών υποβολής, μπορείτε να διαπιστώσετε σε ποιο έγινε κλικ, χρησιμοποιώντας το `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Προσθέτει μια υποφόρμα (κλάση [Container|api:Nette\Forms\Container]), ή αλλιώς container, στο οποίο μπορούν να προστεθούν άλλα στοιχεία με τον ίδιο τρόπο που τα προσθέτουμε στη φόρμα. Λειτουργούν επίσης οι μέθοδοι `setDefaults()` ή `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Το όνομά σας:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Το όνομά σας:'); -$sub2->addEmail('email', 'Email:'); -``` - -Τα υποβληθέντα δεδομένα επιστρέφονται στη συνέχεια ως πολυδιάστατη δομή: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Επισκόπηση ρυθμίσεων -==================== - -Σε όλα τα στοιχεία μπορούμε να καλέσουμε τις ακόλουθες μεθόδους (πλήρης επισκόπηση στην [τεκμηρίωση API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | ορίζει την προεπιλεγμένη τιμή -| `getValue()` | λαμβάνει την τρέχουσα τιμή -| `setOmitted()` | [#Παράλειψη τιμής] -| `setDisabled()` | [#Απενεργοποίηση στοιχείων] - -Απόδοση: -.[table-form-methods language-php] -| `setCaption($caption)` | αλλάζει την ετικέτα του στοιχείου -| `setTranslator($translator)` | ορίζει τον [μεταφραστή |rendering#Μετάφραση] -| `setHtmlAttribute($name, $value)` | ορίζει το [HTML attribute |rendering#Χαρακτηριστικά HTML] του στοιχείου -| `setHtmlId($id)` | ορίζει το HTML attribute `id` -| `setHtmlType($type)` | ορίζει το HTML attribute `type` -| `setHtmlName($name)` | ορίζει το HTML attribute `name` -| `setOption($key, $value)` | [ρυθμίσεις για απόδοση |rendering#Options] - -Επικύρωση: -.[table-form-methods language-php] -| `setRequired()` | [υποχρεωτικό στοιχείο |validation] -| `addRule()` | ορίζει τον [κανόνα επικύρωσης |validation#Κανόνες] -| `addCondition()`, `addConditionOn()` | ορίζει τη [συνθήκη επικύρωσης |validation#Συνθήκες] -| `addError($message)` | [παράδοση μηνύματος σφάλματος |validation#Σφάλματα κατά την Επεξεργασία] - -Στα στοιχεία `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` μπορούν να κληθούν οι ακόλουθες μέθοδοι: - -.[table-form-methods language-php] -| `setNullable()` | ορίζει αν το getValue() θα επιστρέψει `null` αντί για κενό string -| `setEmptyValue($value)` | ορίζει μια ειδική τιμή που θεωρείται κενό string -| `setMaxLength($length)` | ορίζει τον μέγιστο αριθμό επιτρεπόμενων χαρακτήρων -| `addFilter($filter)` | [επεξεργασία εισόδου |validation#Τροποποίηση Εισόδου] - - -Παράλειψη τιμής -=============== - -Εάν η τιμή που συμπλήρωσε ο χρήστης δεν μας ενδιαφέρει, μπορούμε να την παραλείψουμε από το αποτέλεσμα της μεθόδου `$form->getValues()` ή από τα δεδομένα που παραδίδονται στους handlers με τη χρήση του `setOmitted()`. Αυτό είναι χρήσιμο για διάφορους κωδικούς ελέγχου, στοιχεία antispam κ.λπ. - -```php -$form->addPassword('passwordVerify', 'Κωδικός για έλεγχο:') - ->setRequired('Παρακαλώ εισάγετε τον κωδικό ξανά για έλεγχο') - ->addRule($form::Equal, 'Οι κωδικοί δεν ταιριάζουν', $form['password']) - ->setOmitted(); -``` - - -Απενεργοποίηση στοιχείων -======================== - -Τα στοιχεία μπορούν να απενεργοποιηθούν με τη χρήση του `setDisabled()`. Ένα τέτοιο στοιχείο δεν μπορεί να επεξεργαστεί ο χρήστης. - -```php -$form->addText('username', 'Όνομα χρήστη:') - ->setDisabled(); -``` - -Τα απενεργοποιημένα στοιχεία ο περιηγητής δεν τα στέλνει καθόλου στον διακομιστή, επομένως δεν θα τα βρείτε ούτε στα δεδομένα που επιστρέφει η συνάρτηση `$form->getValues()`. Ωστόσο, εάν ορίσετε `setOmitted(false)`, το Nette θα συμπεριλάβει την προεπιλεγμένη τους τιμή σε αυτά τα δεδομένα. - -Κατά την κλήση του `setDisabled()`, για λόγους ασφαλείας **διαγράφεται η τιμή του στοιχείου**. Εάν ορίζετε μια προεπιλεγμένη τιμή, είναι απαραίτητο να το κάνετε μετά την απενεργοποίησή του: - -```php -$form->addText('username', 'Όνομα χρήστη:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Μια εναλλακτική λύση στα απενεργοποιημένα στοιχεία είναι τα στοιχεία με το HTML attribute `readonly`, τα οποία ο περιηγητής στέλνει στον διακομιστή. Παρόλο που το στοιχείο είναι μόνο για ανάγνωση, είναι **σημαντικό να συνειδητοποιήσετε** ότι η τιμή του μπορεί ακόμα να τροποποιηθεί ή να πλαστογραφηθεί από έναν εισβολέα. - - -Προσαρμοσμένα στοιχεία -====================== - -Εκτός από την ευρεία γκάμα ενσωματωμένων στοιχείων φόρμας, μπορείτε να προσθέσετε προσαρμοσμένα στοιχεία στη φόρμα με αυτόν τον τρόπο: - -```php -$form->addComponent(new DateInput('Ημερομηνία:'), 'date'); -// εναλλακτική σύνταξη: $form['date'] = new DateInput('Ημερομηνία:'); -``` - -.[note] -Η φόρμα είναι απόγονος της κλάσης [Container |component-model:#Container] και τα επιμέρους στοιχεία είναι απόγονοι του [Component |component-model:#Component]. - -Υπάρχει ένας τρόπος να ορίσετε νέες μεθόδους φόρμας που χρησιμεύουν για την προσθήκη προσαρμοσμένων στοιχείων (π.χ. `$form->addZip()`). Πρόκειται για τις λεγόμενες extension methods. Το μειονέκτημα είναι ότι η αυτόματη συμπλήρωση στους επεξεργαστές δεν θα λειτουργεί για αυτές. - -```php -use Nette\Forms\Container; - -// προσθέτουμε τη μέθοδο addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Τουλάχιστον 5 αριθμοί', '[0-9]{5}'); -}); - -// χρήση -$form->addZip('zip', 'ZIP code:'); -``` - - -Στοιχεία χαμηλού επιπέδου -========================= - -Μπορούν να χρησιμοποιηθούν και στοιχεία που γράφουμε μόνο στο template και δεν τα προσθέτουμε στη φόρμα με κάποια από τις μεθόδους `$form->addXyz()`. Για παράδειγμα, όταν εμφανίζουμε εγγραφές από τη βάση δεδομένων και δεν ξέρουμε εκ των προτέρων πόσες θα είναι και ποια θα είναι τα ID τους, και θέλουμε σε κάθε γραμμή να εμφανίσουμε ένα checkbox ή ένα radio button, αρκεί να το κωδικοποιήσουμε στο template: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -Και μετά την υποβολή, βρίσκουμε την τιμή: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -όπου η πρώτη παράμετρος είναι ο τύπος του στοιχείου (`DataFile` για `type=file`, `DataLine` για εισόδους μίας γραμμής όπως `text`, `password`, `email` κ.λπ. και `DataText` για όλα τα υπόλοιπα) και η δεύτερη παράμετρος `sel[]` αντιστοιχεί στο HTML attribute name. Μπορούμε να συνδυάσουμε τον τύπο του στοιχείου με την τιμή `DataKeys`, η οποία διατηρεί τα κλειδιά των στοιχείων. Αυτό είναι ιδιαίτερα χρήσιμο για `select`, `radioList` και `checkboxList`. - -Το ουσιαστικό είναι ότι το `getHttpData()` επιστρέφει μια απολυμασμένη τιμή, σε αυτή την περίπτωση θα είναι πάντα ένας πίνακας έγκυρων UTF-8 strings, ανεξάρτητα από το τι θα προσπαθούσε να υποβάλει ένας εισβολέας στον διακομιστή. Πρόκειται για μια αναλογία της άμεσης εργασίας με το `$_POST` ή το `$_GET`, αλλά με τη σημαντική διαφορά ότι επιστρέφει πάντα καθαρά δεδομένα, όπως είστε συνηθισμένοι με τα τυπικά στοιχεία των φορμών Nette. diff --git a/forms/el/in-presenter.texy b/forms/el/in-presenter.texy deleted file mode 100644 index 7cc037f5d3..0000000000 --- a/forms/el/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Φόρμες στους presenters -*********************** - -.[perex] -Οι Nette Forms διευκολύνουν κατά πολύ τη δημιουργία και την επεξεργασία φορμών ιστού. Σε αυτό το κεφάλαιο, θα μάθετε πώς να χρησιμοποιείτε φόρμες μέσα στους presenters. - -Αν ενδιαφέρεστε για το πώς να τις χρησιμοποιήσετε εντελώς αυτόνομα χωρίς το υπόλοιπο framework, ο οδηγός για [αυτόνομη χρήση |standalone] είναι για εσάς. - - -Η πρώτη φόρμα -============= - -Ας δοκιμάσουμε να γράψουμε μια απλή φόρμα εγγραφής. Ο κώδικάς της θα είναι ο εξής: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Όνομα:'); -$form->addPassword('password', 'Κωδικός πρόσβασης:'); -$form->addSubmit('send', 'Εγγραφή'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -και στον περιηγητή θα εμφανιστεί έτσι: - -[* form-cs.webp *] - -Μια φόρμα σε έναν presenter είναι ένα αντικείμενο της κλάσης `Nette\Application\UI\Form`, ο προκάτοχός της `Nette\Forms\Form` προορίζεται για αυτόνομη χρήση. Προσθέσαμε σε αυτήν τα λεγόμενα στοιχεία όνομα, κωδικό πρόσβασης και ένα κουμπί υποβολής. Και τέλος, η γραμμή με το `$form->onSuccess` λέει ότι μετά την υποβολή και την επιτυχή επικύρωση, πρέπει να κληθεί η μέθοδος `$this->formSucceeded()`. - -Από την οπτική γωνία του presenter, η φόρμα είναι ένα συνηθισμένο component. Επομένως, αντιμετωπίζεται ως component και την ενσωματώνουμε στον presenter χρησιμοποιώντας [factory methods |application:components#Μέθοδοι Εργοστασίου]. Θα μοιάζει κάπως έτσι: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:'); - $form->addPassword('password', 'Κωδικός πρόσβασης:'); - $form->addSubmit('send', 'Εγγραφή'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // εδώ επεξεργαζόμαστε τα δεδομένα που υποβλήθηκαν από τη φόρμα - // το $data->name περιέχει το όνομα - // το $data->password περιέχει τον κωδικό πρόσβασης - $this->flashMessage('Εγγραφήκατε με επιτυχία.'); - $this->redirect('Home:'); - } -} -``` - -Και στο template, αποδίδουμε τη φόρμα με την ετικέτα `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Εγγραφή</h1> - -{control registrationForm} -``` - -Και αυτό είναι όλο :-) Έχουμε μια λειτουργική και τέλεια [ασφαλή |#Προστασία από ευπάθειες] φόρμα. - -Και τώρα πιθανότατα σκέφτεστε ότι αυτό ήταν πολύ γρήγορο, αναρωτιέστε πώς είναι δυνατόν να κληθεί η μέθοδος `formSucceeded()` και ποιες είναι οι παράμετροι που λαμβάνει. Σίγουρα, έχετε δίκιο, αυτό αξίζει εξήγηση. - -Η Nette εισάγει έναν φρέσκο μηχανισμό, τον οποίο ονομάζουμε [Hollywood style |application:components#Hollywood Style]. Αντί εσείς, ως προγραμματιστής, να πρέπει συνεχώς να ρωτάτε αν συνέβη κάτι («υποβλήθηκε η φόρμα;», «υποβλήθηκε έγκυρα;» και «δεν παραποιήθηκε;»), λέτε στο framework «όταν η φόρμα συμπληρωθεί έγκυρα, κάλεσε αυτή τη μέθοδο» και αφήνετε την υπόλοιπη δουλειά σε αυτό. Αν προγραμματίζετε σε JavaScript, αυτό το στυλ προγραμματισμού σας είναι πολύ οικείο. Γράφετε συναρτήσεις που καλούνται όταν συμβεί ένα συγκεκριμένο [γεγονός |nette:glossary#Events]. Και η γλώσσα τους περνά τα κατάλληλα ορίσματα. - -Ακριβώς έτσι είναι δομημένος και ο παραπάνω κώδικας του presenter. Ο πίνακας `$form->onSuccess` αντιπροσωπεύει μια λίστα από PHP callbacks που η Nette καλεί τη στιγμή που η φόρμα υποβάλλεται και συμπληρώνεται σωστά (δηλαδή είναι έγκυρη). Στο πλαίσιο του [κύκλου ζωής του presenter |application:presenters#Κύκλος ζωής του presenter] πρόκειται για ένα λεγόμενο σήμα, οπότε καλούνται μετά τη μέθοδο `action*` και πριν από τη μέθοδο `render*`. Και σε κάθε callback, περνά ως πρώτη παράμετρο την ίδια τη φόρμα και ως δεύτερη τα υποβληθέντα δεδομένα με τη μορφή ενός αντικειμένου [ArrayHash |utils:arrays#ArrayHash]. Μπορείτε να παραλείψετε την πρώτη παράμετρο αν δεν χρειάζεστε το αντικείμενο της φόρμας. Και η δεύτερη παράμετρος μπορεί να είναι πιο έξυπνη, αλλά γι' αυτό θα μιλήσουμε [αργότερα |#Αντιστοίχιση σε κλάσεις]. - -Το αντικείμενο `$data` περιέχει τα κλειδιά `name` και `password` με τα δεδομένα που συμπλήρωσε ο χρήστης. Συνήθως, στέλνουμε αμέσως τα δεδομένα για περαιτέρω επεξεργασία, η οποία μπορεί να είναι, για παράδειγμα, η εισαγωγή στη βάση δεδομένων. Ωστόσο, κατά την επεξεργασία μπορεί να προκύψει σφάλμα, για παράδειγμα, το όνομα χρήστη είναι ήδη κατειλημμένο. Σε αυτή την περίπτωση, επιστρέφουμε το σφάλμα στη φόρμα χρησιμοποιώντας την `addError()` και την αφήνουμε να αποδοθεί ξανά, μαζί με το μήνυμα σφάλματος. - -```php -$form->addError('Λυπούμαστε, το όνομα χρήστη χρησιμοποιείται ήδη.'); -``` - -Εκτός από το `onSuccess`, υπάρχει επίσης το `onSubmit`: τα callbacks καλούνται πάντα μετά την υποβολή της φόρμας, ακόμη και αν δεν έχει συμπληρωθεί σωστά. Και επίσης το `onError`: τα callbacks καλούνται μόνο αν η υποβολή δεν είναι έγκυρη. Καλούνται ακόμη και αν στο `onSuccess` ή στο `onSubmit` ακυρώσουμε την εγκυρότητα της φόρμας χρησιμοποιώντας την `addError()`. - -Μετά την επεξεργασία της φόρμας, ανακατευθύνουμε σε άλλη σελίδα. Αυτό αποτρέπει την ακούσια επανυποβολή της φόρμας με το κουμπί *ανανέωση*, *πίσω* ή με την κίνηση στο ιστορικό του περιηγητή. - -Δοκιμάστε να προσθέσετε και άλλα [στοιχεία φόρμας|controls]. - - -Πρόσβαση στα στοιχεία -===================== - -Η φόρμα είναι ένα component του presenter, στην περίπτωσή μας ονομάζεται `registrationForm` (από το όνομα της factory method `createComponentRegistrationForm`), οπότε οπουδήποτε στον presenter μπορείτε να αποκτήσετε πρόσβαση στη φόρμα χρησιμοποιώντας: - -```php -$form = $this->getComponent('registrationForm'); -// εναλλακτική σύνταξη: $form = $this['registrationForm']; -``` - -Τα μεμονωμένα στοιχεία της φόρμας είναι επίσης components, επομένως μπορείτε να αποκτήσετε πρόσβαση σε αυτά με τον ίδιο τρόπο: - -```php -$input = $form->getComponent('name'); // ή $input = $form['name']; -$button = $form->getComponent('send'); // ή $button = $form['send']; -``` - -Τα στοιχεία αφαιρούνται χρησιμοποιώντας το unset: - -```php -unset($form['name']); -``` - - -Κανόνες επικύρωσης -================== - -Αναφέρθηκε η λέξη *έγκυρη*, αλλά η φόρμα δεν έχει ακόμη κανόνες επικύρωσης. Ας το διορθώσουμε αυτό. - -Το όνομα θα είναι υποχρεωτικό, γι' αυτό το επισημαίνουμε με τη μέθοδο `setRequired()`, το όρισμα της οποίας είναι το κείμενο του μηνύματος σφάλματος που θα εμφανιστεί εάν ο χρήστης δεν συμπληρώσει το όνομα. Εάν δεν παρέχουμε όρισμα, θα χρησιμοποιηθεί το προεπιλεγμένο μήνυμα σφάλματος. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ εισάγετε ένα όνομα'); -``` - -Δοκιμάστε να υποβάλετε τη φόρμα χωρίς να συμπληρώσετε το όνομα και θα δείτε ότι θα εμφανιστεί ένα μήνυμα σφάλματος και ο περιηγητής ή ο διακομιστής θα την απορρίπτει μέχρι να συμπληρώσετε το πεδίο. - -Ταυτόχρονα, δεν μπορείτε να ξεγελάσετε το σύστημα γράφοντας, για παράδειγμα, μόνο κενά στο πεδίο. Όχι. Η Nette αφαιρεί αυτόματα τα κενά στην αρχή και στο τέλος. Δοκιμάστε το. Είναι κάτι που πρέπει πάντα να κάνετε με κάθε input μίας γραμμής, αλλά συχνά ξεχνιέται. Η Nette το κάνει αυτόματα. (Μπορείτε να δοκιμάσετε να ξεγελάσετε τη φόρμα και να στείλετε μια συμβολοσειρά πολλών γραμμών ως όνομα. Ούτε εδώ η Nette δεν θα μπερδευτεί και θα αλλάξει τις αλλαγές γραμμής σε κενά.) - -Η φόρμα επικυρώνεται πάντα από την πλευρά του διακομιστή, αλλά δημιουργείται επίσης επικύρωση JavaScript, η οποία εκτελείται αστραπιαία και ο χρήστης ενημερώνεται για το σφάλμα αμέσως, χωρίς να χρειάζεται να υποβάλει τη φόρμα στον διακομιστή. Αυτό το χειρίζεται το script `netteForms.js`. Εισαγάγετέ το στο template του layout: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Αν κοιτάξετε τον πηγαίο κώδικα της σελίδας με τη φόρμα, μπορείτε να παρατηρήσετε ότι η Nette εισάγει τα υποχρεωτικά στοιχεία σε στοιχεία με την κλάση CSS `required`. Δοκιμάστε να προσθέσετε το ακόλουθο φύλλο στυλ στο template και η ετικέτα "Όνομα" θα γίνει κόκκινη. Με αυτόν τον κομψό τρόπο, επισημαίνουμε τα υποχρεωτικά στοιχεία στους χρήστες: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Προσθέτουμε περαιτέρω κανόνες επικύρωσης με τη μέθοδο `addRule()`. Η πρώτη παράμετρος είναι ο κανόνας, η δεύτερη είναι πάλι το κείμενο του μηνύματος σφάλματος και μπορεί να ακολουθήσει ένα όρισμα του κανόνα επικύρωσης. Τι σημαίνει αυτό; - -Θα επεκτείνουμε τη φόρμα με ένα νέο προαιρετικό πεδίο "ηλικία", το οποίο πρέπει να είναι ακέραιος αριθμός (`addInteger()`) και επιπλέον εντός επιτρεπτού εύρους (`$form::Range`). Και εδώ ακριβώς θα χρησιμοποιήσουμε την τρίτη παράμετρο της μεθόδου `addRule()`, με την οποία περνάμε στον επικυρωτή το απαιτούμενο εύρος ως ζεύγος `[από, έως]`: - -```php -$form->addInteger('age', 'Ηλικία:') - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι από 18 έως 120', [18, 120]); -``` - -.[tip] -Εάν ο χρήστης δεν συμπληρώσει το πεδίο, οι κανόνες επικύρωσης δεν θα ελεγχθούν, καθώς το στοιχείο είναι προαιρετικό. - -Εδώ υπάρχει χώρος για μια μικρή αναδιάρθρωση (refactoring). Στο μήνυμα σφάλματος και στην τρίτη παράμετρο, οι αριθμοί αναφέρονται διπλά, πράγμα που δεν είναι ιδανικό. Αν δημιουργούσαμε [πολύγλωσσες φόρμες |rendering#Μετάφραση] και το μήνυμα που περιέχει αριθμούς μεταφραζόταν σε πολλές γλώσσες, θα δυσκόλευε μια πιθανή αλλαγή των τιμών. Για το λόγο αυτό, είναι δυνατόν να χρησιμοποιηθούν οι χαρακτήρες υποκατάστασης `%d` και η Nette θα συμπληρώσει τις τιμές: - -```php - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι από %d έως %d ετών', [18, 120]); -``` - -Ας επιστρέψουμε στο στοιχείο `password`, το οποίο θα καταστήσουμε επίσης υποχρεωτικό και θα ελέγξουμε επιπλέον το ελάχιστο μήκος του κωδικού πρόσβασης (`$form::MinLength`), χρησιμοποιώντας πάλι τον χαρακτήρα υποκατάστασης: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->setRequired('Επιλέξτε έναν κωδικό πρόσβασης') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8); -``` - -Θα προσθέσουμε στη φόρμα ένα ακόμη πεδίο `passwordVerify`, όπου ο χρήστης θα εισάγει τον κωδικό πρόσβασης ξανά, για έλεγχο. Χρησιμοποιώντας κανόνες επικύρωσης, θα ελέγξουμε αν οι δύο κωδικοί πρόσβασης είναι ίδιοι (`$form::Equal`). Και ως παράμετρο θα δώσουμε μια αναφορά στον πρώτο κωδικό πρόσβασης χρησιμοποιώντας [αγκύλες |#Πρόσβαση στα στοιχεία]: - -```php -$form->addPassword('passwordVerify', 'Κωδικός πρόσβασης για έλεγχο:') - ->setRequired('Παρακαλώ εισάγετε τον κωδικό πρόσβασης ξανά για έλεγχο') - ->addRule($form::Equal, 'Οι κωδικοί πρόσβασης δεν ταιριάζουν', $form['password']) - ->setOmitted(); -``` - -Με τη χρήση της `setOmitted()`, επισημάναμε το στοιχείο του οποίου η τιμή στην πραγματικότητα δεν μας ενδιαφέρει και το οποίο υπάρχει μόνο για λόγους επικύρωσης. Η τιμή δεν θα περάσει στο `$data`. - -Με αυτό, έχουμε μια πλήρως λειτουργική φόρμα με επικύρωση σε PHP και JavaScript. Οι δυνατότητες επικύρωσης της Nette είναι πολύ ευρύτερες, μπορούν να δημιουργηθούν συνθήκες, να εμφανίζονται και να αποκρύπτονται τμήματα της σελίδας βάσει αυτών, κ.λπ. Όλα θα τα μάθετε στο κεφάλαιο για την [επικύρωση φορμών|validation]. - - -Προεπιλεγμένες τιμές -==================== - -Συνήθως ορίζουμε προεπιλεγμένες τιμές για τα στοιχεία της φόρμας: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Συχνά είναι χρήσιμο να ορίσουμε προεπιλεγμένες τιμές για όλα τα στοιχεία ταυτόχρονα. Για παράδειγμα, όταν η φόρμα χρησιμοποιείται για την επεξεργασία εγγραφών. Διαβάζουμε την εγγραφή από τη βάση δεδομένων και ορίζουμε τις προεπιλεγμένες τιμές: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Καλέστε την `setDefaults()` μετά τον ορισμό των στοιχείων. - - -Απόδοση φόρμας -============== - -Από προεπιλογή, η φόρμα αποδίδεται ως πίνακας. Τα μεμονωμένα στοιχεία πληρούν τον βασικό κανόνα προσβασιμότητας - όλες οι ετικέτες γράφονται ως `<label>` και συνδέονται με το αντίστοιχο στοιχείο της φόρμας. Όταν κάνετε κλικ στην ετικέτα, ο δρομέας εμφανίζεται αυτόματα στο πεδίο της φόρμας. - -Μπορούμε να ορίσουμε οποιαδήποτε HTML attributes για κάθε στοιχείο. Για παράδειγμα, να προσθέσουμε ένα placeholder: - -```php -$form->addInteger('age', 'Ηλικία:') - ->setHtmlAttribute('placeholder', 'Παρακαλώ συμπληρώστε την ηλικία'); -``` - -Υπάρχουν πραγματικά πολλοί τρόποι για να αποδοθεί μια φόρμα, γι' αυτό υπάρχει ένα [ξεχωριστό κεφάλαιο για την απόδοση|rendering]. - - -Αντιστοίχιση σε κλάσεις -======================= - -Ας επιστρέψουμε στη μέθοδο `formSucceeded()`, η οποία στη δεύτερη παράμετρο `$data` λαμβάνει τα υποβληθέντα δεδομένα ως αντικείμενο `ArrayHash`. Επειδή πρόκειται για μια γενική κλάση, κάτι σαν `stdClass`, θα μας λείψει κάποια άνεση κατά την εργασία μαζί της, όπως η πρόταση properties στους επεξεργαστές ή η στατική ανάλυση κώδικα. Αυτό θα μπορούσε να λυθεί έχοντας μια συγκεκριμένη κλάση για κάθε φόρμα, της οποίας οι properties αντιπροσωπεύουν τα μεμονωμένα στοιχεία. Π.χ.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Εναλλακτικά, μπορείτε να χρησιμοποιήσετε τον κατασκευαστή: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Οι ιδιότητες της κλάσης δεδομένων μπορούν επίσης να είναι enum και θα αντιστοιχιστούν αυτόματα. .{data-version:3.2.4} - -Πώς να πούμε στη Nette να μας επιστρέφει τα δεδομένα ως αντικείμενα αυτής της κλάσης; Πιο εύκολα από ό,τι νομίζετε. Αρκεί απλώς να δηλώσετε την κλάση ως τύπο της παραμέτρου `$data` στη μέθοδο χειρισμού: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // το $data είναι μια παρουσία του RegistrationFormData - $name = $data->name; - // ... -} -``` - -Ως τύπος μπορεί επίσης να δηλωθεί το `array` και τότε τα δεδομένα θα περάσουν ως πίνακας. - -Με παρόμοιο τρόπο μπορεί να χρησιμοποιηθεί και η συνάρτηση `getValues()`, στην οποία περνάμε το όνομα της κλάσης ή το αντικείμενο προς ενυδάτωση ως παράμετρο: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Εάν οι φόρμες σχηματίζουν μια πολυεπίπεδη δομή αποτελούμενη από containers, δημιουργήστε μια ξεχωριστή κλάση για καθένα: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Η αντιστοίχιση τότε από τον τύπο της property `$person` καταλαβαίνει ότι πρέπει να αντιστοιχίσει το container στην κλάση `PersonFormData`. Εάν η property περιείχε έναν πίνακα από containers, δηλώστε τον τύπο `array` και περάστε την κλάση για αντιστοίχιση απευθείας στο container: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Μπορείτε να ζητήσετε τη δημιουργία του σχεδίου της κλάσης δεδομένων της φόρμας χρησιμοποιώντας τη μέθοδο `Nette\Forms\Blueprint::dataClass($form)`, η οποία θα το εκτυπώσει στη σελίδα του περιηγητή. Στη συνέχεια, αρκεί να επιλέξετε τον κώδικα με κλικ και να τον αντιγράψετε στο έργο σας. .{data-version:3.1.15} - - -Πολλαπλά κουμπιά -================ - -Εάν η φόρμα έχει περισσότερα από ένα κουμπιά, συνήθως χρειαζόμαστε να διακρίνουμε ποιο από αυτά πατήθηκε. Μπορούμε να δημιουργήσουμε τη δική μας συνάρτηση χειρισμού για κάθε κουμπί. Θα την ορίσουμε ως handler για το [γεγονός |nette:glossary#Events] `onClick`: - -```php -$form->addSubmit('save', 'Αποθήκευση') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Διαγραφή') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Αυτοί οι handlers καλούνται μόνο στην περίπτωση έγκυρα συμπληρωμένης φόρμας, όπως και στην περίπτωση του συμβάντος `onSuccess`. Η διαφορά είναι ότι ως πρώτη παράμετρος, αντί για τη φόρμα, μπορεί να περάσει το κουμπί υποβολής, ανάλογα με τον τύπο που θα δηλώσετε: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Όταν η φόρμα υποβάλλεται με το πλήκτρο <kbd>Enter</kbd>, θεωρείται σαν να υποβλήθηκε με το πρώτο κουμπί. - - -Το συμβάν onAnchor -================== - -Όταν στη factory method (όπως π.χ. η `createComponentRegistrationForm`) κατασκευάζουμε τη φόρμα, αυτή δεν γνωρίζει ακόμη αν υποβλήθηκε, ούτε με ποια δεδομένα. Υπάρχουν όμως περιπτώσεις όπου χρειαζόμαστε να γνωρίζουμε τις υποβληθείσες τιμές, για παράδειγμα, η περαιτέρω μορφή της φόρμας εξαρτάται από αυτές, ή τις χρειαζόμαστε για εξαρτώμενα selectboxes κ.λπ. - -Μπορείτε λοιπόν να αφήσετε ένα μέρος του κώδικα που κατασκευάζει τη φόρμα να κληθεί μόνο τη στιγμή που είναι, όπως λέγεται, αγκυρωμένη, δηλαδή είναι ήδη συνδεδεμένη με τον presenter και γνωρίζει τα υποβληθέντα δεδομένα της. Τέτοιο κώδικα τον περνάμε στον πίνακα `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Χώρα:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Πόλη:'); - -$form->onAnchor[] = function () use ($country, $city) { - // αυτή η συνάρτηση καλείται όταν η φόρμα γνωρίζει αν έχει υποβληθεί και με ποια δεδομένα - // επομένως, η μέθοδος getValue() μπορεί να χρησιμοποιηθεί - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Προστασία από ευπάθειες -======================= - -Το Nette Framework δίνει μεγάλη έμφαση στην ασφάλεια και γι' αυτό φροντίζει σχολαστικά για την καλή ασφάλεια των φορμών. Το κάνει εντελώς διαφανώς και δεν απαιτεί χειροκίνητη ρύθμιση. - -Εκτός από την προστασία των φορμών από επιθέσεις [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] και [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], κάνει πολλές μικρές ασφαλιστικές δικλείδες για τις οποίες εσείς δεν χρειάζεται πλέον να σκέφτεστε. - -Για παράδειγμα, φιλτράρει από τις εισόδους όλους τους χαρακτήρες ελέγχου και ελέγχει την εγκυρότητα της κωδικοποίησης UTF-8, έτσι ώστε τα δεδομένα από τη φόρμα να είναι πάντα καθαρά. Στα select boxes και radio lists, ελέγχει ότι τα επιλεγμένα στοιχεία ήταν πραγματικά από τα προσφερόμενα και ότι δεν υπήρξε παραποίηση. Έχουμε ήδη αναφέρει ότι στα text inputs μίας γραμμής αφαιρεί τους χαρακτήρες τέλους γραμμής που θα μπορούσε να στείλει ένας εισβολέας. Στα inputs πολλών γραμμών, κανονικοποιεί τους χαρακτήρες τέλους γραμμής. Και ούτω καθεξής. - -Η Nette λύνει για εσάς κινδύνους ασφαλείας που πολλοί προγραμματιστές ούτε καν υποψιάζονται ότι υπάρχουν. - -Η αναφερόμενη επίθεση CSRF συνίσταται στο ότι ο εισβολέας προσελκύει το θύμα σε μια σελίδα που εκτελεί διακριτικά στον περιηγητή του θύματος ένα αίτημα προς τον διακομιστή στον οποίο το θύμα είναι συνδεδεμένο, και ο διακομιστής πιστεύει ότι το αίτημα εκτελέστηκε από το θύμα με τη θέλησή του. Γι' αυτό η Nette αποτρέπει την υποβολή φορμών POST από άλλο domain. Εάν για κάποιο λόγο θέλετε να απενεργοποιήσετε την προστασία και να επιτρέψετε την υποβολή της φόρμας από άλλο domain, χρησιμοποιήστε: - -```php -$form->allowCrossOrigin(); // ΠΡΟΣΟΧΗ! Απενεργοποιεί την προστασία! -``` - -Αυτή η προστασία χρησιμοποιεί ένα SameSite cookie με το όνομα `_nss`. Η προστασία μέσω SameSite cookie μπορεί να μην είναι 100% αξιόπιστη, γι' αυτό είναι σκόπιμο να ενεργοποιήσετε επιπλέον την προστασία μέσω token: - -```php -$form->addProtection(); -``` - -Συνιστούμε να προστατεύετε με αυτόν τον τρόπο τις φόρμες στο διαχειριστικό τμήμα του ιστότοπου που αλλάζουν ευαίσθητα δεδομένα στην εφαρμογή. Το framework αμύνεται έναντι της επίθεσης CSRF δημιουργώντας και επαληθεύοντας ένα token εξουσιοδότησης, το οποίο αποθηκεύεται στο session. Επομένως, είναι απαραίτητο να έχετε ανοιχτό το session πριν από την εμφάνιση της φόρμας. Στο διαχειριστικό τμήμα του ιστότοπου, το session είναι συνήθως ήδη ενεργοποιημένο λόγω της σύνδεσης του χρήστη. Διαφορετικά, ξεκινήστε το session με τη μέθοδο `Nette\Http\Session::start()`. - - -Η ίδια φόρμα σε πολλούς presenters -================================== - -Εάν χρειάζεστε να χρησιμοποιήσετε την ίδια φόρμα σε πολλούς presenters, συνιστούμε να δημιουργήσετε ένα factory για αυτήν, το οποίο στη συνέχεια θα περάσετε στον presenter. Μια κατάλληλη τοποθεσία για μια τέτοια κλάση είναι, για παράδειγμα, ο κατάλογος `app/Forms`. - -Η κλάση factory μπορεί να μοιάζει κάπως έτσι: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:'); - $form->addSubmit('send', 'Σύνδεση'); - return $form; - } -} -``` - -Ζητάμε από την κλάση να κατασκευάσει τη φόρμα στη factory method για components στον presenter: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // μπορούμε να τροποποιήσουμε τη φόρμα, εδώ για παράδειγμα αλλάζουμε την ετικέτα στο κουμπί - $form['send']->setCaption('Συνέχεια'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // και προσθέτουμε τον handler - return $form; -} -``` - -Ο handler για την επεξεργασία της φόρμας μπορεί επίσης να παρασχεθεί ήδη από το factory: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:'); - $form->addSubmit('send', 'Σύνδεση'); - $form->onSuccess[] = function (Form $form, $data): void { - // εδώ εκτελούμε την επεξεργασία της φόρμας - }; - return $form; - } -} -``` - -Λοιπόν, έχουμε πίσω μας μια γρήγορη εισαγωγή στις φόρμες στη Nette. Δοκιμάστε να ρίξετε μια ματιά στον κατάλογο [examples |https://github.com/nette/forms/tree/master/examples] στη διανομή, όπου θα βρείτε περαιτέρω έμπνευση. diff --git a/forms/el/rendering.texy b/forms/el/rendering.texy deleted file mode 100644 index 56c525c592..0000000000 --- a/forms/el/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Απόδοση φορμών -************** - -Η εμφάνιση των φορμών μπορεί να είναι πολύ διαφορετική. Στην πράξη, μπορούμε να συναντήσουμε δύο άκρα. Από τη μία πλευρά, υπάρχει η ανάγκη να αποδοθούν στην εφαρμογή πολλές φόρμες που είναι οπτικά παρόμοιες σαν δύο σταγόνες νερό, και εκτιμούμε την εύκολη απόδοση χωρίς πρότυπο χρησιμοποιώντας την `$form->render()`. Αυτή είναι συνήθως η περίπτωση των διαχειριστικών διεπαφών. - -Από την άλλη πλευρά, υπάρχουν ποικίλες φόρμες όπου ισχύει: κάθε κομμάτι, ένα πρωτότυπο. Η μορφή τους περιγράφεται καλύτερα με τη γλώσσα HTML στο πρότυπο της φόρμας. Και φυσικά, εκτός από τα δύο αναφερόμενα άκρα, θα συναντήσουμε πολλές φόρμες που κινούνται κάπου στη μέση. - - -Απόδοση με χρήση Latte -====================== - -Το [σύστημα προτύπων Latte |latte:] διευκολύνει σημαντικά την απόδοση των φορμών και των στοιχείων τους. Πρώτα θα δείξουμε πώς να αποδίδετε τις φόρμες χειροκίνητα, στοιχείο προς στοιχείο, αποκτώντας έτσι πλήρη έλεγχο του κώδικα. Αργότερα θα δείξουμε πώς μπορεί αυτή η απόδοση να [αυτοματοποιηθεί |#Αυτόματη απόδοση]. - -Μπορείτε να ζητήσετε τη δημιουργία του σχεδίου του προτύπου Latte της φόρμας χρησιμοποιώντας τη μέθοδο `Nette\Forms\Blueprint::latte($form)`, η οποία θα το εκτυπώσει στη σελίδα του προγράμματος περιήγησης. Στη συνέχεια, αρκεί να επιλέξετε τον κώδικα με κλικ και να τον αντιγράψετε στο έργο σας. .{data-version:3.1.15} - - -`{control}` ------------ - -Ο απλούστερος τρόπος για να αποδοθεί μια φόρμα είναι να γράψετε στο πρότυπο: - -```latte -{control signInForm} -``` - -Μπορείτε να επηρεάσετε την εμφάνιση της φόρμας που αποδίδεται με αυτόν τον τρόπο διαμορφώνοντας τον [#Renderer] και τα [μεμονωμένα στοιχεία ελέγχου |#Χαρακτηριστικά HTML]. - - -`n:name` --------- - -Ο ορισμός της φόρμας στον κώδικα PHP μπορεί να συνδεθεί εξαιρετικά εύκολα με τον κώδικα HTML. Αρκεί απλώς να προσθέσετε τα χαρακτηριστικά `n:name`. Είναι τόσο εύκολο! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Όνομα χρήστη: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Κωδικός πρόσβασης: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Έχετε τον πλήρη έλεγχο της μορφής του τελικού κώδικα HTML. Εάν χρησιμοποιήσετε το χαρακτηριστικό `n:name` στα στοιχεία `<select>`, `<button>` ή `<textarea>`, το εσωτερικό τους περιεχόμενο θα συμπληρωθεί αυτόματα. Επιπλέον, η ετικέτα `<form n:name>` δημιουργεί μια τοπική μεταβλητή `$form` με το αντικείμενο της αποδιδόμενης φόρμας και η κλείνουσα `</form>` αποδίδει όλα τα μη αποδοθέντα κρυφά στοιχεία (το ίδιο ισχύει και για `{form} ... {/form}`). - -Ωστόσο, δεν πρέπει να ξεχάσουμε να αποδώσουμε τα πιθανά μηνύματα σφάλματος. Και αυτά που προστέθηκαν με τη μέθοδο `addError()` στα μεμονωμένα στοιχεία (χρησιμοποιώντας `{inputError}`), και αυτά που προστέθηκαν απευθείας στη φόρμα (τα επιστρέφει η `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Όνομα χρήστη: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Κωδικός πρόσβασης: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Πιο σύνθετα στοιχεία φόρμας, όπως το RadioList ή το CheckboxList, μπορούν να αποδοθούν με αυτόν τον τρόπο ανά μεμονωμένο στοιχείο: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Δεν θέλετε να σκέφτεστε για κάθε στοιχείο ποιο στοιχείο HTML να χρησιμοποιήσετε στο πρότυπο, αν `<input>`, `<textarea>` κ.λπ.; Η λύση είναι η καθολική ετικέτα `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Όνομα χρήστη: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Κωδικός πρόσβασης: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Εάν η φόρμα χρησιμοποιεί μεταφραστή, το κείμενο μέσα στις ετικέτες `{label}` θα μεταφραστεί. - -Ακόμη και σε αυτή την περίπτωση, πιο σύνθετα στοιχεία φόρμας, όπως το RadioList ή το CheckboxList, μπορούν να αποδοθούν ανά μεμονωμένο στοιχείο: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Για να αποδώσετε μόνο το `<input>` στο στοιχείο Checkbox, χρησιμοποιήστε `{input myCheckbox:}`. Σε αυτή την περίπτωση, διαχωρίζετε πάντα τα χαρακτηριστικά HTML με κόμμα `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Εκτυπώνει το μήνυμα σφάλματος για ένα στοιχείο φόρμας, αν υπάρχει. Συνήθως τυλίγουμε το μήνυμα σε ένα στοιχείο HTML για στυλ. Μπορείτε να αποτρέψετε την απόδοση ενός κενού στοιχείου εάν δεν υπάρχει μήνυμα, κομψά με τη χρήση του `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Μπορούμε να ελέγξουμε την παρουσία σφάλματος με τη μέθοδο `hasErrors()` και ανάλογα να ορίσουμε την κλάση στο γονικό στοιχείο: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Οι ετικέτες `{form signInForm}...{/form}` είναι μια εναλλακτική λύση για το `<form n:name="signInForm">...</form>`. - - -Αυτόματη απόδοση ----------------- - -Χάρη στις ετικέτες `{input}` και `{label}`, μπορούμε εύκολα να δημιουργήσουμε ένα γενικό πρότυπο για οποιαδήποτε φόρμα. Θα επαναλαμβάνει και θα αποδίδει διαδοχικά όλα τα στοιχεία της, εκτός από τα κρυφά στοιχεία, τα οποία αποδίδονται αυτόματα κατά το κλείσιμο της φόρμας με την ετικέτα `</form>`. Το όνομα της αποδιδόμενης φόρμας θα αναμένεται στη μεταβλητή `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Οι χρησιμοποιούμενες αυτοκλειόμενες ζευγαρωτές ετικέτες `{label .../}` εμφανίζουν τις ετικέτες που προέρχονται από τον ορισμό της φόρμας στον κώδικα PHP. - -Αποθηκεύστε αυτό το γενικό πρότυπο, για παράδειγμα, στο αρχείο `basic-form.latte` και για να αποδώσετε τη φόρμα, αρκεί να το συμπεριλάβετε και να περάσετε το όνομα (ή την παρουσία) της φόρμας στην παράμετρο `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Αν θέλετε να παρέμβετε στη μορφή μιας συγκεκριμένης φόρμας κατά την απόδοσή της και, για παράδειγμα, να αποδώσετε ένα στοιχείο διαφορετικά, ο ευκολότερος τρόπος είναι να προετοιμάσετε μπλοκ στο πρότυπο που θα μπορούν στη συνέχεια να αντικατασταθούν. Τα μπλοκ μπορούν επίσης να έχουν [δυναμικά ονόματα μπλοκ |latte:template-inheritance#Δυναμικά ονόματα μπλοκ], οπότε μπορείτε να εισαγάγετε σε αυτά και το όνομα του αποδιδόμενου στοιχείου. Για παράδειγμα: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Για το στοιχείο, π.χ., `username`, δημιουργείται έτσι το μπλοκ `input-username`, το οποίο μπορεί εύκολα να αντικατασταθεί χρησιμοποιώντας την [ετικέτα {embed} |latte:template-inheritance#Κληρονομικότητα μονάδας embed]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Εναλλακτικά, μπορείτε να [define |latte:template-inheritance#Ορισμοί define] ολόκληρο το περιεχόμενο του template `basic-form.latte` ως μπλοκ, συμπεριλαμβανομένης της παραμέτρου `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Χάρη σε αυτό, η κλήση του θα είναι ελαφρώς απλούστερη: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -Το μπλοκ αρκεί να εισαχθεί σε ένα μόνο σημείο, στην αρχή του προτύπου της διάταξης: - -```latte -{import basic-form.latte} -``` - - -Ειδικές περιπτώσεις -------------------- - -Εάν χρειάζεστε να αποδώσετε μόνο το εσωτερικό μέρος της φόρμας χωρίς τις ετικέτες HTML `<form>`, για παράδειγμα κατά την αποστολή αποσπασμάτων, αποκρύψτε τις χρησιμοποιώντας το χαρακτηριστικό `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Όνομα χρήστη: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Η ετικέτα `{formContainer}` βοηθά στην απόδοση των στοιχείων μέσα σε ένα κοντέινερ φόρμας. - -```latte -<p>Ποιες ειδήσεις θέλετε να λαμβάνετε:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Απόδοση χωρίς Latte -=================== - -Ο απλούστερος τρόπος για να αποδοθεί μια φόρμα είναι να καλέσετε: - -```php -$form->render(); -``` - -Μπορείτε να επηρεάσετε την εμφάνιση της φόρμας που αποδίδεται με αυτόν τον τρόπο διαμορφώνοντας τον [#Renderer] και τα [μεμονωμένα στοιχεία ελέγχου |#Χαρακτηριστικά HTML]. - - -Χειροκίνητη απόδοση -------------------- - -Κάθε στοιχείο φόρμας διαθέτει μεθόδους που παράγουν τον κώδικα HTML του πεδίου της φόρμας και της ετικέτας. Μπορούν να τον επιστρέψουν είτε ως συμβολοσειρά είτε ως αντικείμενο [Nette\Utils\Html |utils:html-elements]: - -- `getControl(): Html|string` επιστρέφει τον κώδικα HTML του στοιχείου -- `getLabel($caption = null): Html|string|null` επιστρέφει τον κώδικα HTML της ετικέτας, αν υπάρχει - -Έτσι, η φόρμα μπορεί να αποδοθεί ανά μεμονωμένο στοιχείο: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Ενώ σε ορισμένα στοιχεία η `getControl()` επιστρέφει ένα μοναδικό στοιχείο HTML (π.χ. `<input>`, `<select>` κ.λπ.), σε άλλα επιστρέφει ένα ολόκληρο κομμάτι κώδικα HTML (CheckboxList, RadioList). Σε αυτή την περίπτωση, μπορείτε να χρησιμοποιήσετε μεθόδους που παράγουν μεμονωμένες εισόδους και ετικέτες, για κάθε στοιχείο ξεχωριστά: - -- `getControlPart($key = null): ?Html` επιστρέφει τον κώδικα HTML ενός μεμονωμένου στοιχείου -- `getLabelPart($key = null): ?Html` επιστρέφει τον κώδικα HTML της ετικέτας ενός μεμονωμένου στοιχείου - -.[note] -Αυτές οι μέθοδοι έχουν το πρόθεμα `get` για ιστορικούς λόγους, αλλά το `generate` θα ήταν καλύτερο, επειδή σε κάθε κλήση δημιουργούν και επιστρέφουν ένα νέο στοιχείο `Html`. - - -Renderer -======== - -Πρόκειται για ένα αντικείμενο που εξασφαλίζει την απόδοση της φόρμας. Μπορεί να οριστεί με τη μέθοδο `$form->setRenderer`. Ο έλεγχος του περνά όταν καλείται η μέθοδος `$form->render()`. - -Εάν δεν ορίσουμε δικό μας renderer, θα χρησιμοποιηθεί ο προεπιλεγμένος renderer [api:Nette\Forms\Rendering\DefaultFormRenderer]. Αυτός αποδίδει τα στοιχεία της φόρμας με τη μορφή πίνακα HTML. Η έξοδος μοιάζει κάπως έτσι: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Όνομα:</label></th> - - <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> -</tr> - -<tr class="required"> - <th><label class="required" for="frm-age">Ηλικία:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Φύλο:</label></th> - ... -``` - -Το αν θα χρησιμοποιηθεί ή όχι πίνακας για τη δομή της φόρμας είναι αμφιλεγόμενο και πολλοί σχεδιαστές ιστοσελίδων προτιμούν άλλη σήμανση. Για παράδειγμα, μια λίστα ορισμών. Θα αναδιαμορφώσουμε λοιπόν τον `DefaultFormRenderer` έτσι ώστε να αποδώσει τη φόρμα με τη μορφή λίστας. Η διαμόρφωση γίνεται επεξεργαζόμενοι τον πίνακα [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Ο πρώτος δείκτης αντιπροσωπεύει πάντα την περιοχή και ο δεύτερος το χαρακτηριστικό της. Οι μεμονωμένες περιοχές απεικονίζονται στην εικόνα: - -[* defaultformrenderer.webp *] - -Από προεπιλογή, η ομάδα στοιχείων `controls` περιβάλλεται από έναν πίνακα `<table>`, κάθε `pair` αντιπροσωπεύει μια γραμμή του πίνακα `<tr>` και το ζεύγος `label` και `control` είναι κελιά `<th>` και `<td>`. Τώρα θα αλλάξουμε τα περιβάλλοντα στοιχεία. Θα εισαγάγουμε την περιοχή `controls` σε ένα container `<dl>`, την περιοχή `pair` θα την αφήσουμε χωρίς container, το `label` θα το εισαγάγουμε σε `<dt>` και τέλος το `control` θα το περιβάλλουμε με ετικέτες `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Το αποτέλεσμα είναι ο ακόλουθος κώδικας HTML: - -```latte -<dl> - <dt><label class="required" for="frm-name">Όνομα:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Ηλικία:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Φύλο:</label></dt> - ... -</dl> -``` - -Στον πίνακα wrappers μπορείτε να επηρεάσετε πολλά άλλα χαρακτηριστικά: - -- προσθήκη κλάσεων CSS σε μεμονωμένους τύπους στοιχείων φόρμας -- διάκριση μονών και ζυγών γραμμών με κλάση CSS -- οπτική διάκριση υποχρεωτικών και προαιρετικών στοιχείων -- καθορισμός αν τα μηνύματα σφάλματος θα εμφανίζονται απευθείας στα στοιχεία ή πάνω από τη φόρμα - - -Options -------- - -Η συμπεριφορά του Renderer μπορεί επίσης να ελεγχθεί ορίζοντας *options* στα μεμονωμένα στοιχεία της φόρμας. Με αυτόν τον τρόπο μπορείτε να ορίσετε την ετικέτα που θα εκτυπωθεί δίπλα στο πεδίο εισαγωγής: - -```php -$form->addText('phone', 'Αριθμός:') - ->setOption('description', 'Αυτός ο αριθμός θα παραμείνει κρυφός'); -``` - -Εάν θέλουμε να τοποθετήσουμε περιεχόμενο HTML σε αυτό, θα χρησιμοποιήσουμε την [κλάση Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Αριθμός:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Όροι διατήρησης του αριθμού σας</a>') - ); -``` - -.[tip] -Το στοιχείο Html μπορεί επίσης να χρησιμοποιηθεί αντί για ετικέτα: `$form->addCheckbox('conditions', $label)`. - - -Ομαδοποίηση στοιχείων ---------------------- - -Ο Renderer επιτρέπει την ομαδοποίηση στοιχείων σε οπτικές ομάδες (fieldsets): - -```php -$form->addGroup('Προσωπικά δεδομένα'); -``` - -Μετά τη δημιουργία μιας νέας ομάδας, αυτή γίνεται ενεργή και κάθε νεοεισερχόμενο στοιχείο προστίθεται ταυτόχρονα και σε αυτήν. Έτσι, η φόρμα μπορεί να κατασκευαστεί με αυτόν τον τρόπο: - -```php -$form = new Form; -$form->addGroup('Προσωπικά δεδομένα'); -$form->addText('name', 'Το όνομά σας:'); -$form->addInteger('age', 'Η ηλικία σας:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Διεύθυνση αποστολής'); -$form->addCheckbox('send', 'Αποστολή στη διεύθυνση'); -$form->addText('street', 'Οδός:'); -$form->addText('city', 'Πόλη:'); -$form->addSelect('country', 'Χώρα:', $countries); -``` - -Ο Renderer αποδίδει πρώτα τις ομάδες και μετά τα στοιχεία που δεν ανήκουν σε καμία ομάδα. - - -Υποστήριξη για Bootstrap ------------------------- - -[Στα παραδείγματα |https://github.com/nette/forms/tree/master/examples] θα βρείτε παραδείγματα για το πώς να διαμορφώσετε τον Renderer για [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] και [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -Χαρακτηριστικά HTML -=================== - -Για να ορίσετε οποιαδήποτε χαρακτηριστικά HTML για τα στοιχεία της φόρμας, χρησιμοποιούμε τη μέθοδο `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Αριθμός:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Ταξινόμηση κατά:', ['τιμής', 'ονόματος']) - ->setHtmlAttribute('onchange', 'submit()'); // υποβολή κατά την αλλαγή - - -// Για να ορίσετε χαρακτηριστικά για το ίδιο το <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Προδιαγραφή του τύπου του στοιχείου: - -```php -$form->addText('tel', 'Το τηλέφωνό σας:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'γράψτε το τηλέφωνο'); -``` - -.[warning] -Η ρύθμιση του τύπου και άλλων χαρακτηριστικών χρησιμεύει μόνο για οπτικούς σκοπούς. Η επαλήθευση της ορθότητας των εισόδων πρέπει να γίνεται στον διακομιστή, πράγμα που εξασφαλίζετε επιλέγοντας το κατάλληλο [στοιχείο φόρμας |controls] και δηλώνοντας [κανόνες επικύρωσης |validation]. - -Μπορούμε να ορίσουμε χαρακτηριστικά HTML για μεμονωμένα στοιχεία σε λίστες radio ή checkbox με διαφορετικές τιμές για καθένα από αυτά. Παρατηρήστε την άνω και κάτω τελεία μετά το `style:`, η οποία εξασφαλίζει την επιλογή της τιμής βάσει κλειδιού: - -```php -$colors = ['r' => 'κόκκινο', 'g' => 'πράσινο', 'b' => 'μπλε']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Χρώματα:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Εκτυπώνει: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">κόκκινο</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">πράσινο</label> -<label><input type="checkbox" name="colors[]" value="b">μπλε</label> -``` - -Για να ορίσετε λογικά χαρακτηριστικά, όπως το `readonly`, μπορούμε να χρησιμοποιήσουμε τη σύνταξη με ερωτηματικό: - -```php -$form->addCheckboxList('colors', 'Χρώματα:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // για πολλαπλά κλειδιά χρησιμοποιήστε έναν πίνακα, π.χ. ['r', 'g'] -``` - -Εκτυπώνει: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">κόκκινο</label> -<label><input type="checkbox" name="colors[]" value="g">πράσινο</label> -<label><input type="checkbox" name="colors[]" value="b">μπλε</label> -``` - -Στην περίπτωση των selectbox, η μέθοδος `setHtmlAttribute()` ορίζει τα χαρακτηριστικά του στοιχείου `<select>`. Εάν θέλουμε να ορίσουμε χαρακτηριστικά για μεμονωμένα `<option>`, χρησιμοποιούμε τη μέθοδο `setOptionAttribute()`. Λειτουργούν επίσης οι συντάξεις με άνω και κάτω τελεία και ερωτηματικό που αναφέρθηκαν παραπάνω: - -```php -$form->addSelect('colors', 'Χρώματα:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Εκτυπώνει: - -```latte -<select name="colors"> - <option value="r" style="background:red">κόκκινο</option> - <option value="g" style="background:green">πράσινο</option> - <option value="b">μπλε</option> -</select> -``` - - -Πρωτότυπα ---------- - -Ένας εναλλακτικός τρόπος ορισμού των χαρακτηριστικών HTML συνίσταται στην τροποποίηση του προτύπου από το οποίο παράγεται το στοιχείο HTML. Το πρότυπο είναι ένα αντικείμενο `Html` και το επιστρέφει η μέθοδος `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Αριθμός:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Με αυτόν τον τρόπο μπορείτε να τροποποιήσετε και το πρότυπο της ετικέτας, το οποίο επιστρέφει η `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Στα στοιχεία Checkbox, CheckboxList και RadioList μπορείτε να επηρεάσετε το πρότυπο του στοιχείου που περιβάλλει ολόκληρο το στοιχείο. Το επιστρέφει η `getContainerPrototype()`. Στην προεπιλεγμένη κατάσταση, πρόκειται για ένα «κενό» στοιχείο, οπότε δεν αποδίδεται τίποτα, αλλά ορίζοντας του ένα όνομα, θα αποδίδεται: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -Στην περίπτωση των CheckboxList και RadioList μπορείτε να επηρεάσετε και το πρότυπο του διαχωριστή των μεμονωμένων στοιχείων, το οποίο επιστρέφει η μέθοδος `getSeparatorPrototype()`. Στην προεπιλεγμένη κατάσταση, είναι το στοιχείο `<br>`. Εάν το αλλάξετε σε ζευγαρωτό στοιχείο, θα περιβάλλει τα μεμονωμένα στοιχεία αντί να τα διαχωρίζει. Και επιπλέον, μπορείτε να επηρεάσετε το πρότυπο του στοιχείου HTML της ετικέτας στα μεμονωμένα στοιχεία, το οποίο επιστρέφει η `getItemLabelPrototype()`. - - -Μετάφραση -========= - -Εάν προγραμματίζετε μια πολύγλωσση εφαρμογή, πιθανότατα θα χρειαστείτε να αποδώσετε τη φόρμα σε διάφορες γλωσσικές εκδόσεις. Το Nette Framework ορίζει για αυτόν τον σκοπό μια διεπαφή για μετάφραση [api:Nette\Localization\Translator]. Στη Nette δεν υπάρχει προεπιλεγμένη υλοποίηση, μπορείτε να επιλέξετε ανάλογα με τις ανάγκες σας από διάφορες έτοιμες λύσεις που θα βρείτε στο [Componette |https://componette.org/search/localization]. Στην τεκμηρίωσή τους θα μάθετε πώς να διαμορφώσετε τον μεταφραστή. - -Οι φόρμες υποστηρίζουν την εκτύπωση κειμένων μέσω του μεταφραστή. Τον περνάμε σε αυτές χρησιμοποιώντας τη μέθοδο `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -Από αυτή τη στιγμή, όχι μόνο όλες οι ετικέτες, αλλά και όλα τα μηνύματα σφάλματος ή τα στοιχεία των πλαισίων επιλογής μεταφράζονται σε άλλη γλώσσα. - -Στα μεμονωμένα στοιχεία της φόρμας, είναι δυνατόν να οριστεί διαφορετικός μεταφραστής ή να απενεργοποιηθεί εντελώς η μετάφραση με την τιμή `null`: - -```php -$form->addSelect('carModel', 'Μοντέλο:', $cars) - ->setTranslator(null); -``` - -Στους [κανόνες επικύρωσης|validation], περνούν στον μεταφραστή και συγκεκριμένες παράμετροι, για παράδειγμα στον κανόνα: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8); -``` - -καλείται ο μεταφραστής με αυτές τις παραμέτρους: - -```php -$translator->translate('Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8); -``` - -και επομένως μπορεί να επιλέξει τη σωστή μορφή πληθυντικού για τη λέξη `χαρακτήρες` ανάλογα με τον αριθμό. - - -Το συμβάν onRender -================== - -Λίγο πριν αποδοθεί η φόρμα, μπορούμε να αφήσουμε να κληθεί ο κώδικάς μας. Αυτός μπορεί, για παράδειγμα, να συμπληρώσει τα στοιχεία της φόρμας με κλάσεις HTML για σωστή εμφάνιση. Προσθέτουμε τον κώδικα στον πίνακα `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/el/standalone.texy b/forms/el/standalone.texy deleted file mode 100644 index b660fa0178..0000000000 --- a/forms/el/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Αυτόνομες Φόρμες -**************** - -.[perex] -Οι φόρμες Nette διευκολύνουν κατά τάξεις μεγέθους τη δημιουργία και την επεξεργασία φορμών ιστού. Μπορείτε να τις χρησιμοποιήσετε στις εφαρμογές σας εντελώς ανεξάρτητα από το υπόλοιπο framework, όπως θα δείξουμε σε αυτό το κεφάλαιο. - -Ωστόσο, εάν χρησιμοποιείτε το Nette Application και presenters, ο οδηγός για [χρήση σε presenters|in-presenter] είναι για εσάς. - - -Πρώτη Φόρμα -=========== - -Ας προσπαθήσουμε να γράψουμε μια απλή φόρμα εγγραφής. Ο κώδικάς της θα είναι ο ακόλουθος ("πλήρης κώδικας":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Όνομα:'); -$form->addPassword('password', 'Κωδικός πρόσβασης:'); -$form->addSubmit('send', 'Εγγραφή'); -``` - -Μπορούμε να την αποδώσουμε πολύ εύκολα: - -```php -$form->render(); -``` - -και θα εμφανιστεί στον περιηγητή ως εξής: - -[* form-cs.webp *] - -Η φόρμα είναι ένα αντικείμενο της κλάσης `Nette\Forms\Form` (η κλάση `Nette\Application\UI\Form` χρησιμοποιείται σε presenters). Προσθέσαμε σε αυτή τα λεγόμενα στοιχεία: όνομα, κωδικό πρόσβασης και ένα κουμπί υποβολής. - -Τώρα, ας ζωντανέψουμε τη φόρμα. Ρωτώντας `$form->isSuccess()`, θα μάθουμε αν η φόρμα υποβλήθηκε και αν συμπληρώθηκε έγκυρα. Αν ναι, θα εμφανίσουμε τα δεδομένα. Έτσι, μετά τον ορισμό της φόρμας, προσθέτουμε: - -```php -if ($form->isSuccess()) { - echo 'Η φόρμα υποβλήθηκε και επικυρώθηκε με επιτυχία'; - $data = $form->getValues(); - // το $data->name περιέχει το όνομα - // το $data->password περιέχει τον κωδικό πρόσβασης - var_dump($data); -} -``` - -Η μέθοδος `getValues()` επιστρέφει τα υποβληθέντα δεδομένα με τη μορφή ενός αντικειμένου [ArrayHash |utils:arrays#ArrayHash]. Θα δείξουμε πώς να το αλλάξουμε [αργότερα |#Αντιστοίχιση σε Κλάσεις]. Το αντικείμενο `$data` περιέχει τα κλειδιά `name` και `password` με τα δεδομένα που συμπλήρωσε ο χρήστης. - -Συνήθως, στέλνουμε τα δεδομένα απευθείας για περαιτέρω επεξεργασία, η οποία μπορεί να είναι, για παράδειγμα, η εισαγωγή σε μια βάση δεδομένων. Ωστόσο, κατά την επεξεργασία, μπορεί να προκύψει σφάλμα, όπως ένα όνομα χρήστη που είναι ήδη κατειλημμένο. Σε αυτή την περίπτωση, επιστρέφουμε το σφάλμα στη φόρμα χρησιμοποιώντας το `addError()` και την αφήνουμε να αποδοθεί ξανά, μαζί με το μήνυμα σφάλματος. - -```php -$form->addError('Συγγνώμη, αυτό το όνομα χρήστη χρησιμοποιείται ήδη.'); -``` - -Μετά την επεξεργασία της φόρμας, ανακατευθύνουμε σε άλλη σελίδα. Αυτό αποτρέπει την ακούσια επανυποβολή της φόρμας με το κουμπί *ανανέωση*, *πίσω* ή με την κίνηση στο ιστορικό του προγράμματος περιήγησης. - -Η φόρμα υποβάλλεται από προεπιλογή χρησιμοποιώντας τη μέθοδο POST στην ίδια σελίδα. Και τα δύο μπορούν να αλλάξουν: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -Και αυτό είναι όλο :-) Έχουμε μια λειτουργική και τέλεια [ασφαλή |#Προστασία από Ευπάθειες] φόρμα. - -Προσπαθήστε να προσθέσετε και άλλα [στοιχεία φόρμας|controls]. - - -Πρόσβαση στα Στοιχεία -===================== - -Ονομάζουμε τη φόρμα και τα μεμονωμένα στοιχεία της components. Σχηματίζουν ένα δέντρο components, όπου η ρίζα είναι η φόρμα. Μπορούμε να αποκτήσουμε πρόσβαση στα μεμονωμένα στοιχεία της φόρμας ως εξής: - -```php -$input = $form->getComponent('name'); -// εναλλακτική σύνταξη: $input = $form['name']; - -$button = $form->getComponent('send'); -// εναλλακτική σύνταξη: $button = $form['send']; -``` - -Τα στοιχεία αφαιρούνται χρησιμοποιώντας το unset: - -```php -unset($form['name']); -``` - - -Κανόνες Επικύρωσης -================== - -Αναφέραμε τη λέξη *έγκυρη*, αλλά η φόρμα δεν έχει ακόμη κανόνες επικύρωσης. Ας το διορθώσουμε αυτό. - -Το όνομα θα είναι υποχρεωτικό, οπότε θα το επισημάνουμε με τη μέθοδο `setRequired()`. Το όρισμά της είναι το κείμενο του μηνύματος σφάλματος που θα εμφανιστεί εάν ο χρήστης δεν συμπληρώσει το όνομα. Εάν δεν παρέχουμε όρισμα, θα χρησιμοποιηθεί το προεπιλεγμένο μήνυμα σφάλματος. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ εισάγετε το όνομά σας.'); -``` - -Προσπαθήστε να υποβάλετε τη φόρμα χωρίς να συμπληρώσετε το όνομα και θα δείτε ότι εμφανίζεται ένα μήνυμα σφάλματος, και ο περιηγητής ή ο διακομιστής θα αρνηθεί να την αποδεχτεί μέχρι να συμπληρώσετε το πεδίο. - -Ταυτόχρονα, δεν μπορείτε να εξαπατήσετε το σύστημα πληκτρολογώντας μόνο κενά στο πεδίο. Όχι. Το Nette αφαιρεί αυτόματα τα αρχικά και τα τελικά κενά. Δοκιμάστε το. Είναι κάτι που πρέπει πάντα να κάνετε με κάθε input μίας γραμμής, αλλά συχνά ξεχνιέται. Το Nette το κάνει αυτόματα. (Μπορείτε να προσπαθήσετε να εξαπατήσετε τη φόρμα στέλνοντας μια συμβολοσειρά πολλαπλών γραμμών ως όνομα. Ακόμα και εδώ, το Nette δεν θα ξεγελαστεί και θα αλλάξει τις αλλαγές γραμμής σε κενά.) - -Η φόρμα επικυρώνεται πάντα από την πλευρά του διακομιστή, αλλά δημιουργείται επίσης επικύρωση JavaScript, η οποία εκτελείται αμέσως και ο χρήστης ενημερώνεται αμέσως για το σφάλμα, χωρίς να χρειάζεται να υποβάλει τη φόρμα στον διακομιστή. Αυτό γίνεται από το σενάριο `netteForms.js`. Εισάγετέ το στη σελίδα: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Αν κοιτάξετε τον πηγαίο κώδικα της σελίδας με τη φόρμα, μπορείτε να παρατηρήσετε ότι το Nette εισάγει τα υποχρεωτικά στοιχεία σε στοιχεία με την κλάση CSS `required`. Προσπαθήστε να προσθέσετε το ακόλουθο φύλλο στυλ στο πρότυπο και η ετικέτα «Όνομα» θα είναι κόκκινη. Με αυτόν τον τρόπο, επισημαίνουμε κομψά τα υποχρεωτικά στοιχεία για τους χρήστες: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Προσθέτουμε περαιτέρω κανόνες επικύρωσης χρησιμοποιώντας τη μέθοδο `addRule()`. Η πρώτη παράμετρος είναι ο κανόνας, η δεύτερη είναι ξανά το κείμενο του μηνύματος σφάλματος, και μπορεί να ακολουθήσει ένα όρισμα κανόνα επικύρωσης. Τι σημαίνει αυτό; - -Θα επεκτείνουμε τη φόρμα με ένα νέο προαιρετικό πεδίο «ηλικία», το οποίο πρέπει να είναι ακέραιος αριθμός (`addInteger()`) και επιπλέον εντός επιτρεπόμενου εύρους (`$form::Range`). Και εδώ θα χρησιμοποιήσουμε την τρίτη παράμετρο της μεθόδου `addRule()`, με την οποία περνάμε το απαιτούμενο εύρος στον επικυρωτή ως ζεύγος `[από, έως]`: - -```php -$form->addInteger('age', 'Ηλικία:') - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι μεταξύ 18 και 120.', [18, 120]); -``` - -.[tip] -Εάν ο χρήστης δεν συμπληρώσει το πεδίο, οι κανόνες επικύρωσης δεν θα ελεγχθούν, καθώς το στοιχείο είναι προαιρετικό. - -Εδώ υπάρχει περιθώριο για μια μικρή αναδιάρθρωση. Στο μήνυμα σφάλματος και στην τρίτη παράμετρο, οι αριθμοί αναφέρονται διπλά, κάτι που δεν είναι ιδανικό. Εάν δημιουργούσαμε [πολύγλωσσες φόρμες |rendering#Μετάφραση] και το μήνυμα που περιέχει αριθμούς μεταφραζόταν σε πολλές γλώσσες, θα ήταν δύσκολο να αλλάξουμε τις τιμές αργότερα. Για το λόγο αυτό, είναι δυνατό να χρησιμοποιηθούν σύμβολα κράτησης θέσης `%d`, και το Nette θα συμπληρώσει τις τιμές: - -```php - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι μεταξύ %d και %d.', [18, 120]); -``` - -Ας επιστρέψουμε στο στοιχείο `password`, το οποίο θα κάνουμε επίσης υποχρεωτικό και θα ελέγξουμε επίσης το ελάχιστο μήκος του κωδικού πρόσβασης (`$form::MinLength`), χρησιμοποιώντας ξανά ένα σύμβολο κράτησης θέσης: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->setRequired('Επιλέξτε έναν κωδικό πρόσβασης.') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει μήκος τουλάχιστον %d χαρακτήρων.', 8); -``` - -Θα προσθέσουμε ένα πεδίο `passwordVerify` στη φόρμα, όπου ο χρήστης εισάγει ξανά τον κωδικό πρόσβασης για επαλήθευση. Χρησιμοποιώντας κανόνες επικύρωσης, θα ελέγξουμε αν οι δύο κωδικοί πρόσβασης είναι ίδιοι (`$form::Equal`). Και ως παράμετρο, θα δώσουμε μια αναφορά στον πρώτο κωδικό πρόσβασης χρησιμοποιώντας [τετράγωνες αγκύλες |#Πρόσβαση στα Στοιχεία]: - -```php -$form->addPassword('passwordVerify', 'Κωδικός πρόσβασης για έλεγχο:') - ->setRequired('Παρακαλώ εισάγετε ξανά τον κωδικό πρόσβασης για έλεγχο.') - ->addRule($form::Equal, 'Οι κωδικοί πρόσβασης δεν ταιριάζουν.', $form['password']) - ->setOmitted(); -``` - -Χρησιμοποιώντας το `setOmitted()`, επισημάναμε ένα στοιχείο του οποίου η τιμή δεν μας ενδιαφέρει πραγματικά και το οποίο υπάρχει μόνο για λόγους επικύρωσης. Η τιμή δεν περνά στο `$data`. - -Με αυτό, έχουμε μια πλήρως λειτουργική φόρμα με επικύρωση τόσο σε PHP όσο και σε JavaScript. Οι δυνατότητες επικύρωσης του Nette είναι πολύ ευρύτερες. Μπορείτε να δημιουργήσετε συνθήκες, να εμφανίσετε και να αποκρύψετε τμήματα της σελίδας με βάση αυτές, κ.λπ. Θα μάθετε τα πάντα στο κεφάλαιο για την [επικύρωση φορμών|validation]. - - -Προεπιλεγμένες Τιμές -==================== - -Συνήθως ορίζουμε προεπιλεγμένες τιμές για τα στοιχεία της φόρμας: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Συχνά είναι χρήσιμο να ορίζουμε προεπιλεγμένες τιμές για όλα τα στοιχεία ταυτόχρονα. Για παράδειγμα, όταν η φόρμα χρησιμοποιείται για την επεξεργασία εγγραφών. Διαβάζουμε την εγγραφή από τη βάση δεδομένων και ορίζουμε τις προεπιλεγμένες τιμές: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Καλέστε το `setDefaults()` μετά τον ορισμό των στοιχείων. - - -Απόδοση Φόρμας -============== - -Από προεπιλογή, η φόρμα αποδίδεται ως πίνακας. Τα μεμονωμένα στοιχεία πληρούν τον βασικό κανόνα προσβασιμότητας - όλες οι ετικέτες γράφονται ως `<label>` και συνδέονται με το αντίστοιχο στοιχείο της φόρμας. Όταν κάνετε κλικ στην ετικέτα, ο κέρσορας εμφανίζεται αυτόματα στο πεδίο της φόρμας. - -Μπορούμε να ορίσουμε οποιαδήποτε χαρακτηριστικά HTML για κάθε στοιχείο. Για παράδειγμα, προσθέστε ένα placeholder: - -```php -$form->addInteger('age', 'Ηλικία:') - ->setHtmlAttribute('placeholder', 'Παρακαλώ συμπληρώστε την ηλικία σας'); -``` - -Υπάρχουν πραγματικά πολλοί τρόποι για την απόδοση μιας φόρμας, οπότε υπάρχει ένα [ξεχωριστό κεφάλαιο για την απόδοση|rendering] αφιερωμένο σε αυτό. - - -Αντιστοίχιση σε Κλάσεις -======================= - -Ας επιστρέψουμε στην επεξεργασία των δεδομένων της φόρμας. Η μέθοδος `getValues()` μας επέστρεψε τα υποβληθέντα δεδομένα ως αντικείμενο `ArrayHash`. Επειδή πρόκειται για μια γενική κλάση, κάτι σαν `stdClass`, θα μας λείψει κάποια άνεση όταν εργαζόμαστε με αυτήν, όπως η αυτόματη συμπλήρωση ιδιοτήτων στους επεξεργαστές ή η στατική ανάλυση κώδικα. Αυτό θα μπορούσε να λυθεί έχοντας μια συγκεκριμένη κλάση για κάθε φόρμα, της οποίας οι ιδιότητες αντιπροσωπεύουν τα μεμονωμένα στοιχεία. Π.χ.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Εναλλακτικά, μπορείτε να χρησιμοποιήσετε έναν κατασκευαστή: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Οι ιδιότητες της κλάσης δεδομένων μπορούν επίσης να είναι enum και θα αντιστοιχιστούν αυτόματα. .{data-version:3.2.4} - -Πώς να πούμε στο Nette να επιστρέψει τα δεδομένα ως αντικείμενα αυτής της κλάσης; Πιο εύκολα από ό,τι νομίζετε. Απλά δώστε το όνομα της κλάσης ή το αντικείμενο για ενυδάτωση ως παράμετρο: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Η παράμετρος μπορεί επίσης να είναι `'array'`, και τότε τα δεδομένα θα επιστραφούν ως πίνακας. - -Εάν οι φόρμες σχηματίζουν μια πολυεπίπεδη δομή που αποτελείται από containers, δημιουργήστε μια ξεχωριστή κλάση για κάθε μία: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Η αντιστοίχιση θα αναγνωρίσει τότε από τον τύπο της ιδιότητας `$person` ότι πρέπει να αντιστοιχίσει το container στην κλάση `PersonFormData`. Εάν η ιδιότητα περιείχε έναν πίνακα από containers, καθορίστε τον τύπο `array` και περάστε την κλάση για αντιστοίχιση απευθείας στο container: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Μπορείτε να δημιουργήσετε το σχέδιο της κλάσης δεδομένων της φόρμας χρησιμοποιώντας τη μέθοδο `Nette\Forms\Blueprint::dataClass($form)`, η οποία θα το εκτυπώσει στη σελίδα του προγράμματος περιήγησης. Στη συνέχεια, απλά επιλέξτε τον κώδικα με κλικ και αντιγράψτε τον στο έργο σας. .{data-version:3.1.15} - - -Πολλαπλά Κουμπιά -================ - -Εάν η φόρμα έχει περισσότερα από ένα κουμπιά, συνήθως πρέπει να διακρίνουμε ποιο από αυτά πατήθηκε. Η μέθοδος `isSubmittedBy()` του κουμπιού θα μας επιστρέψει αυτή την πληροφορία: - -```php -$form->addSubmit('save', 'Αποθήκευση'); -$form->addSubmit('delete', 'Διαγραφή'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // αποθήκευση δεδομένων - } - - if ($form['delete']->isSubmittedBy()) { - // διαγραφή δεδομένων - } -} -``` - -Μην παραλείψετε την ερώτηση `$form->isSuccess()`, καθώς επαληθεύει την εγκυρότητα των δεδομένων. - -Όταν μια φόρμα υποβάλλεται πατώντας το πλήκτρο <kbd>Enter</kbd>, θεωρείται ότι υποβλήθηκε από το πρώτο κουμπί. - - -Προστασία από Ευπάθειες -======================= - -Το Nette Framework δίνει μεγάλη έμφαση στην ασφάλεια και, ως εκ τούτου, φροντίζει σχολαστικά για την καλή ασφάλεια των φορμών. - -Εκτός από την προστασία των φορμών από επιθέσεις [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] και [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], εφαρμόζει πολλές μικρές προστασίες για τις οποίες δεν χρειάζεται πλέον να ανησυχείτε. - -Για παράδειγμα, φιλτράρει όλους τους χαρακτήρες ελέγχου από την είσοδο και επαληθεύει την εγκυρότητα της κωδικοποίησης UTF-8, έτσι ώστε τα δεδομένα από τη φόρμα να είναι πάντα καθαρά. Για τα πλαίσια επιλογής και τις λίστες radio, επαληθεύει ότι τα επιλεγμένα στοιχεία ήταν πράγματι από τα προσφερόμενα και ότι δεν υπήρξε πλαστογράφηση. Έχουμε ήδη αναφέρει ότι αφαιρεί τους χαρακτήρες τέλους γραμμής από τις εισόδους κειμένου μίας γραμμής, τους οποίους θα μπορούσε να στείλει ένας εισβολέας. Για τις εισόδους πολλαπλών γραμμών, κανονικοποιεί τους χαρακτήρες τέλους γραμμής. Και ούτω καθεξής. - -Το Nette αντιμετωπίζει για εσάς κινδύνους ασφαλείας που πολλοί προγραμματιστές δεν γνωρίζουν καν ότι υπάρχουν. - -Η προαναφερθείσα επίθεση CSRF συνίσταται στο ότι ο εισβολέας δελεάζει το θύμα σε μια σελίδα που εκτελεί διακριτικά ένα αίτημα στον διακομιστή στον οποίο είναι συνδεδεμένο το θύμα, στο πρόγραμμα περιήγησης του θύματος, και ο διακομιστής πιστεύει ότι το αίτημα εκτελέστηκε από το θύμα με δική του βούληση. Επομένως, το Nette αποτρέπει την υποβολή μιας φόρμας POST από άλλο domain. Εάν, για κάποιο λόγο, θέλετε να απενεργοποιήσετε την προστασία και να επιτρέψετε την υποβολή της φόρμας από άλλο domain, χρησιμοποιήστε: - -```php -$form->allowCrossOrigin(); // ΠΡΟΣΟΧΗ! Απενεργοποιεί την προστασία! -``` - -Αυτή η προστασία χρησιμοποιεί ένα SameSite cookie με όνομα `_nss`. Επομένως, δημιουργήστε το αντικείμενο της φόρμας πριν στείλετε την πρώτη έξοδο, ώστε το cookie να μπορεί να σταλεί. - -Η προστασία με SameSite cookie μπορεί να μην είναι 100% αξιόπιστη, οπότε συνιστάται να ενεργοποιήσετε επίσης την προστασία με token: - -```php -$form->addProtection(); -``` - -Συνιστούμε την προστασία των φορμών στο διαχειριστικό τμήμα του ιστότοπου που τροποποιούν ευαίσθητα δεδομένα στην εφαρμογή με αυτόν τον τρόπο. Το framework αμύνεται έναντι επιθέσεων CSRF δημιουργώντας και επαληθεύοντας ένα token εξουσιοδότησης που αποθηκεύεται στο session. Επομένως, είναι απαραίτητο να έχετε ανοιχτό το session πριν εμφανίσετε τη φόρμα. Στο διαχειριστικό τμήμα του ιστότοπου, το session συνήθως έχει ήδη ξεκινήσει λόγω της σύνδεσης του χρήστη. Διαφορετικά, ξεκινήστε το session με τη μέθοδο `Nette\Http\Session::start()`. - -Λοιπόν, αυτή ήταν μια γρήγορη εισαγωγή στις φόρμες στο Nette. Προσπαθήστε να ρίξετε μια ματιά στον κατάλογο [examples|https://github.com/nette/forms/tree/master/examples] στη διανομή, όπου θα βρείτε περισσότερη έμπνευση. diff --git a/forms/el/validation.texy b/forms/el/validation.texy deleted file mode 100644 index b17739b2b7..0000000000 --- a/forms/el/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Επικύρωση Φορμών -**************** - - -Υποχρεωτικά Στοιχεία -==================== - -Επισημαίνουμε τα υποχρεωτικά στοιχεία με τη μέθοδο `setRequired()`. Το όρισμά της είναι το κείμενο του [μηνύματος σφάλματος |#Μηνύματα Σφάλματος] που θα εμφανιστεί εάν ο χρήστης δεν συμπληρώσει το στοιχείο. Εάν δεν παρέχουμε όρισμα, θα χρησιμοποιηθεί το προεπιλεγμένο μήνυμα σφάλματος. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ εισάγετε το όνομά σας.'); -``` - - -Κανόνες -======= - -Προσθέτουμε κανόνες επικύρωσης στα στοιχεία χρησιμοποιώντας τη μέθοδο `addRule()`. Η πρώτη παράμετρος είναι ο κανόνας, η δεύτερη είναι το κείμενο του [μηνύματος σφάλματος |#Μηνύματα Σφάλματος] και η τρίτη είναι το όρισμα του κανόνα επικύρωσης. - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει μήκος τουλάχιστον %d χαρακτήρων.', 8); -``` - -**Οι κανόνες επικύρωσης ελέγχονται μόνο εάν ο χρήστης συμπληρώσει το στοιχείο.** - -Το Nette έρχεται με μια σειρά προκαθορισμένων κανόνων, των οποίων τα ονόματα είναι σταθερές της κλάσης `Nette\Forms\Form`. Μπορούμε να χρησιμοποιήσουμε αυτούς τους κανόνες για όλα τα στοιχεία: - -| σταθερά | περιγραφή | τύπος ορίσματος -|------- -| `Required` | υποχρεωτικό στοιχείο, ψευδώνυμο για `setRequired()` | - -| `Filled` | υποχρεωτικό στοιχείο, ψευδώνυμο για `setRequired()` | - -| `Blank` | το στοιχείο δεν πρέπει να συμπληρωθεί | - -| `Equal` | η τιμή είναι ίση με την παράμετρο | `mixed` -| `NotEqual` | η τιμή δεν είναι ίση με την παράμετρο | `mixed` -| `IsIn` | η τιμή είναι ίση με ένα από τα στοιχεία του πίνακα | `array` -| `IsNotIn` | η τιμή δεν είναι ίση με κανένα στοιχείο του πίνακα | `array` -| `Valid` | είναι το στοιχείο συμπληρωμένο σωστά; (για [#συνθήκες]) | - - - -Είσοδοι Κειμένου ----------------- - -Για τα στοιχεία `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, μπορούν επίσης να χρησιμοποιηθούν ορισμένοι από τους ακόλουθους κανόνες: - -| `MinLength` | ελάχιστο μήκος κειμένου | `int` -| `MaxLength` | μέγιστο μήκος κειμένου | `int` -| `Length` | μήκος εντός εύρους ή ακριβές μήκος | ζεύγος `[int, int]` ή `int` -| `Email` | έγκυρη διεύθυνση email | - -| `URL` | απόλυτο URL | - -| `Pattern` | ταιριάζει με την κανονική έκφραση | `string` -| `PatternInsensitive` | όπως το `Pattern`, αλλά χωρίς διάκριση πεζών-κεφαλαίων | `string` -| `Integer` | ακέραια τιμή | - -| `Numeric` | ψευδώνυμο για `Integer` | - -| `Float` | αριθμός | - -| `Min` | ελάχιστη τιμή αριθμητικού στοιχείου | `int\|float` -| `Max` | μέγιστη τιμή αριθμητικού στοιχείου | `int\|float` -| `Range` | τιμή εντός εύρους | ζεύγος `[int\|float, int\|float]` - -Οι κανόνες επικύρωσης `Integer`, `Numeric` και `Float` μετατρέπουν αμέσως την τιμή σε ακέραιο ή δεκαδικό, αντίστοιχα. Επιπλέον, ο κανόνας `URL` δέχεται επίσης μια διεύθυνση χωρίς σχήμα (π.χ. `nette.org`) και προσθέτει το σχήμα (`https://nette.org`). Η έκφραση στο `Pattern` και το `PatternIcase` πρέπει να ισχύει για ολόκληρη την τιμή, δηλαδή σαν να ήταν περικλεισμένη από τους χαρακτήρες `^` και `$`. - - -Αριθμός Στοιχείων ------------------ - -Για τα στοιχεία `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, μπορούν επίσης να χρησιμοποιηθούν οι ακόλουθοι κανόνες για τον περιορισμό του αριθμού των επιλεγμένων στοιχείων ή των ανεβασμένων αρχείων, αντίστοιχα: - -| `MinLength` | ελάχιστος αριθμός | `int` -| `MaxLength` | μέγιστος αριθμός | `int` -| `Length` | αριθμός εντός εύρους ή ακριβής αριθμός | ζεύγος `[int, int]` ή `int` - - -Ανέβασμα Αρχείων ----------------- - -Για τα στοιχεία `addUpload()`, `addMultiUpload()`, μπορούν επίσης να χρησιμοποιηθούν οι ακόλουθοι κανόνες: - -| `MaxFileSize` | μέγιστο μέγεθος αρχείου σε bytes | `int` -| `MimeType` | Τύπος MIME, επιτρέπονται χαρακτήρες μπαλαντέρ (`'video/*'`) | `string\|string[]` -| `Image` | εικόνα JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | το όνομα αρχείου ταιριάζει με την κανονική έκφραση | `string` -| `PatternInsensitive` | όπως το `Pattern`, αλλά χωρίς διάκριση πεζών-κεφαλαίων | `string` - -Τα `MimeType` και `Image` απαιτούν την επέκταση PHP `fileinfo`. Το αν ένα αρχείο ή μια εικόνα είναι του απαιτούμενου τύπου ανιχνεύεται με βάση την υπογραφή του και **δεν επαληθεύει την ακεραιότητα ολόκληρου του αρχείου.** Το αν μια εικόνα είναι κατεστραμμένη μπορεί να προσδιοριστεί, για παράδειγμα, προσπαθώντας να την [φορτώσετε |http:request#toImage]. - - -Μηνύματα Σφάλματος -================== - -Όλοι οι προκαθορισμένοι κανόνες, εκτός από τα `Pattern` και `PatternInsensitive`, έχουν ένα προεπιλεγμένο μήνυμα σφάλματος, οπότε μπορεί να παραλειφθεί. Ωστόσο, καθορίζοντας και διατυπώνοντας όλα τα μηνύματα κατά παραγγελία, θα κάνετε τη φόρμα πιο φιλική προς τον χρήστη. - -Μπορείτε να αλλάξετε τα προεπιλεγμένα μηνύματα στην [διαμόρφωση|forms:configuration], επεξεργαζόμενοι τα κείμενα στον πίνακα `Nette\Forms\Validator::$messages`, ή χρησιμοποιώντας έναν [μεταφραστή |rendering#Μετάφραση]. - -Οι ακόλουθες συμβολοσειρές κράτησης θέσης μπορούν να χρησιμοποιηθούν στο κείμενο των μηνυμάτων σφάλματος: - -| `%d` | αντικαθίσταται διαδοχικά από τα ορίσματα του κανόνα -| `%n$d` | αντικαθίσταται από το n-οστό όρισμα του κανόνα -| `%label` | αντικαθίσταται από την ετικέτα του στοιχείου (χωρίς την άνω και κάτω τελεία) -| `%name` | αντικαθίσταται από το όνομα του στοιχείου (π.χ. `name`) -| `%value` | αντικαθίσταται από την τιμή που εισήγαγε ο χρήστης - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ συμπληρώστε το %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'τουλάχιστον %d και το πολύ %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'το πολύ %2$d και τουλάχιστον %1$d', [5, 10]); -``` - - -Συνθήκες -======== - -Εκτός από τους κανόνες, μπορούν επίσης να προστεθούν συνθήκες. Γράφονται παρόμοια με τους κανόνες, αλλά αντί για `addRule()`, χρησιμοποιούμε τη μέθοδο `addCondition()`, και φυσικά, δεν παρέχουμε κανένα μήνυμα σφάλματος (η συνθήκη απλώς ρωτά): - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - // εάν ο κωδικός πρόσβασης δεν είναι μεγαλύτερος από 8 χαρακτήρες - ->addCondition($form::MaxLength, 8) - // τότε πρέπει να περιέχει ένα ψηφίο - ->addRule($form::Pattern, 'Πρέπει να περιέχει ένα ψηφίο.', '.*[0-9].*'); -``` - -Η συνθήκη μπορεί επίσης να συνδεθεί με ένα στοιχείο διαφορετικό από το τρέχον χρησιμοποιώντας το `addConditionOn()`. Ως πρώτη παράμετρο, παρέχουμε μια αναφορά στο στοιχείο. Σε αυτό το παράδειγμα, το email θα είναι υποχρεωτικό μόνο εάν το πλαίσιο ελέγχου είναι επιλεγμένο (η τιμή του θα είναι true): - -```php -$form->addCheckbox('newsletters', 'στείλτε μου ενημερωτικά δελτία'); - -$form->addEmail('email', 'E-mail:') - // εάν το πλαίσιο ελέγχου είναι επιλεγμένο - ->addConditionOn($form['newsletters'], $form::Equal, true) - // τότε απαιτήστε το email - ->setRequired('Παρακαλώ εισάγετε τη διεύθυνση email σας.'); -``` - -Μπορείτε να δημιουργήσετε σύνθετες δομές από συνθήκες χρησιμοποιώντας τα `elseCondition()` και `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // εάν η πρώτη συνθήκη πληρούται - ->addConditionOn(/* ... */) // και η δεύτερη συνθήκη σε άλλο στοιχείο - ->addRule(/* ... */) // απαιτήστε αυτόν τον κανόνα - ->elseCondition() // εάν η δεύτερη συνθήκη δεν πληρούται - ->addRule(/* ... */) // απαιτήστε αυτούς τους κανόνες - ->addRule(/* ... */) - ->endCondition() // επιστρέφουμε στην πρώτη συνθήκη - ->addRule(/* ... */); -``` - -Στο Nette, είναι πολύ εύκολο να αντιδράσετε στην εκπλήρωση ή μη εκπλήρωση μιας συνθήκης επίσης στην πλευρά του JavaScript χρησιμοποιώντας τη μέθοδο `toggle()`, δείτε [#δυναμικό-javascript]. - - -Αναφορά σε Άλλο Στοιχείο -======================== - -Ένα άλλο στοιχείο φόρμας μπορεί επίσης να περάσει ως όρισμα σε έναν κανόνα ή μια συνθήκη. Ο κανόνας θα χρησιμοποιήσει τότε την τιμή που εισήγαγε αργότερα ο χρήστης στο πρόγραμμα περιήγησης. Με αυτόν τον τρόπο, μπορείτε, για παράδειγμα, να επικυρώσετε δυναμικά ότι το στοιχείο `password` περιέχει την ίδια συμβολοσειρά με το στοιχείο `password_confirm`: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης'); -$form->addPassword('password_confirm', 'Επιβεβαιώστε τον κωδικό πρόσβασης') - ->addRule($form::Equal, 'Οι κωδικοί πρόσβασης δεν ταιριάζουν.', $form['password']); -``` - - -Προσαρμοσμένοι Κανόνες και Συνθήκες -=================================== - -Μερικές φορές βρισκόμαστε σε μια κατάσταση όπου οι ενσωματωμένοι κανόνες επικύρωσης στο Nette δεν είναι αρκετοί και πρέπει να επικυρώσουμε τα δεδομένα του χρήστη με τον δικό μας τρόπο. Στο Nette, αυτό είναι πολύ εύκολο! - -Μπορείτε να περάσετε οποιαδήποτε επανάκληση ως πρώτη παράμετρο στις μεθόδους `addRule()` ή `addCondition()`. Η επανάκληση δέχεται το ίδιο το στοιχείο ως πρώτη παράμετρο και επιστρέφει μια boolean τιμή που υποδεικνύει εάν η επικύρωση ήταν επιτυχής. Κατά την προσθήκη ενός κανόνα χρησιμοποιώντας το `addRule()`, είναι επίσης δυνατό να καθοριστούν πρόσθετα ορίσματα, τα οποία στη συνέχεια περνούν ως δεύτερη παράμετρος. - -Μπορούμε να δημιουργήσουμε το δικό μας σύνολο επικυρωτών ως κλάση με στατικές μεθόδους: - -```php -class MyValidators -{ - // ελέγχει εάν η τιμή είναι διαιρετή από το όρισμα - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // άλλοι επικυρωτές - } -} -``` - -Η χρήση είναι τότε πολύ απλή: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Η τιμή πρέπει να είναι πολλαπλάσιο του %d.', - 8, - ); -``` - -Προσαρμοσμένοι κανόνες επικύρωσης μπορούν επίσης να προστεθούν στο JavaScript. Η προϋπόθεση είναι ότι ο κανόνας είναι μια στατική μέθοδος. Το όνομά του για τον επικυρωτή JavaScript δημιουργείται συνδυάζοντας το όνομα της κλάσης χωρίς ανάστροφες καθέτους `\`, μια κάτω παύλα `_` και το όνομα της μεθόδου. Για παράδειγμα, το `App\MyValidators::validateDivisibility` γράφεται ως `AppMyValidators_validateDivisibility` και προστίθεται στο αντικείμενο `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Συμβάν onValidate -================= - -Μετά την υποβολή της φόρμας, πραγματοποιείται επικύρωση, όπου ελέγχονται οι μεμονωμένοι κανόνες που προστέθηκαν χρησιμοποιώντας το `addRule()`, και στη συνέχεια ενεργοποιείται το [συμβάν |nette:glossary#Events] `onValidate`. Ο χειριστής του μπορεί να χρησιμοποιηθεί για συμπληρωματική επικύρωση, συνήθως για την επαλήθευση του σωστού συνδυασμού τιμών σε πολλαπλά στοιχεία της φόρμας. - -Εάν εντοπιστεί σφάλμα, το περνάμε στη φόρμα χρησιμοποιώντας τη μέθοδο `addError()`. Αυτή μπορεί να κληθεί είτε σε ένα συγκεκριμένο στοιχείο είτε απευθείας στη φόρμα. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Αυτός ο συνδυασμός δεν είναι δυνατός.'); - } -} -``` - - -Σφάλματα κατά την Επεξεργασία -============================= - -Σε πολλές περιπτώσεις, μαθαίνουμε για ένα σφάλμα μόνο όταν επεξεργαζόμαστε μια έγκυρη φόρμα, για παράδειγμα, όταν γράφουμε ένα νέο στοιχείο στη βάση δεδομένων και συναντάμε διπλότυπα κλειδιά. Σε αυτή την περίπτωση, περνάμε ξανά το σφάλμα στη φόρμα χρησιμοποιώντας τη μέθοδο `addError()`. Αυτή μπορεί να κληθεί είτε σε ένα συγκεκριμένο στοιχείο είτε απευθείας στη φόρμα: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Μη έγκυρος κωδικός πρόσβασης.'); - } -} -``` - -Εάν είναι δυνατόν, συνιστούμε να επισυνάψετε το σφάλμα απευθείας στο στοιχείο της φόρμας, καθώς θα εμφανιστεί δίπλα του όταν χρησιμοποιείτε τον προεπιλεγμένο renderer. - -```php -$form['date']->addError('Συγγνώμη, αλλά αυτή η ημερομηνία είναι ήδη κατειλημμένη.'); -``` - -Μπορείτε να καλέσετε το `addError()` επανειλημμένα για να περάσετε πολλαπλά μηνύματα σφάλματος στη φόρμα ή στο στοιχείο. Μπορείτε να τα λάβετε χρησιμοποιώντας το `getErrors()`. - -Προσοχή, το `$form->getErrors()` επιστρέφει μια σύνοψη όλων των μηνυμάτων σφάλματος, συμπεριλαμβανομένων εκείνων που παραδόθηκαν απευθείας σε μεμονωμένα στοιχεία, όχι μόνο απευθείας στη φόρμα. Μπορείτε να λάβετε τα μηνύματα σφάλματος που παραδόθηκαν μόνο στη φόρμα μέσω του `$form->getOwnErrors()`. - - -Τροποποίηση Εισόδου -=================== - -Χρησιμοποιώντας τη μέθοδο `addFilter()`, μπορούμε να τροποποιήσουμε την τιμή που εισήγαγε ο χρήστης. Σε αυτό το παράδειγμα, θα ανεχτούμε και θα αφαιρέσουμε κενά στον ταχυδρομικό κώδικα: - -```php -$form->addText('zip', 'Τ.Κ.:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // αφαιρούμε τα κενά από τον Τ.Κ. - }) - ->addRule($form::Pattern, 'Ο Τ.Κ. δεν είναι στη μορφή πέντε ψηφίων.', '\d{5}'); -``` - -Το φίλτρο ενσωματώνεται μεταξύ των κανόνων επικύρωσης και των συνθηκών, οπότε η σειρά των μεθόδων έχει σημασία, δηλαδή το φίλτρο και ο κανόνας καλούνται με την ίδια σειρά όπως οι μέθοδοι `addFilter()` και `addRule()`. - - -Επικύρωση JavaScript -==================== - -Η γλώσσα για τη διατύπωση συνθηκών και κανόνων είναι πολύ ισχυρή. Όλες οι κατασκευές λειτουργούν τόσο στην πλευρά του διακομιστή όσο και στην πλευρά του JavaScript. Μεταφέρονται σε χαρακτηριστικά HTML `data-nette-rules` ως JSON. Η ίδια η επικύρωση εκτελείται από ένα σενάριο που παρακολουθεί το συμβάν `submit` της φόρμας, διατρέχει τα μεμονωμένα στοιχεία και εκτελεί την αντίστοιχη επικύρωση. - -Αυτό το σενάριο είναι το `netteForms.js` και είναι διαθέσιμο από πολλές πιθανές πηγές: - -Μπορείτε να εισαγάγετε το σενάριο απευθείας στη σελίδα HTML από ένα CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ή να το αντιγράψετε τοπικά στον δημόσιο φάκελο του έργου (π.χ. από το `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Ή να το εγκαταστήσετε μέσω [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -Και στη συνέχεια να το φορτώσετε και να το εκτελέσετε: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Εναλλακτικά, μπορείτε να το φορτώσετε απευθείας από τον φάκελο `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Δυναμικό JavaScript -=================== - -Θέλετε να εμφανίσετε τα πεδία για την εισαγωγή της διεύθυνσης μόνο εάν ο χρήστης επιλέξει την αποστολή των αγαθών ταχυδρομικώς; Κανένα πρόβλημα. Το κλειδί είναι το ζεύγος μεθόδων `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Αυτός ο κώδικας λέει ότι όταν η συνθήκη πληρούται, δηλαδή όταν το πλαίσιο ελέγχου είναι επιλεγμένο, το στοιχείο HTML `#address-container` θα είναι ορατό. Και αντίστροφα. Έτσι, τοποθετούμε τα στοιχεία της φόρμας με τη διεύθυνση του παραλήπτη σε ένα container με αυτό το ID, και όταν κάνουμε κλικ στο πλαίσιο ελέγχου, θα αποκρυφθούν ή θα εμφανιστούν. Αυτό διασφαλίζεται από το σενάριο `netteForms.js`. - -Οποιοσδήποτε επιλογέας μπορεί να περάσει ως όρισμα στη μέθοδο `toggle()`. Για ιστορικούς λόγους, μια αλφαριθμητική συμβολοσειρά χωρίς άλλους ειδικούς χαρακτήρες νοείται ως το ID του στοιχείου, δηλαδή σαν να προηγείται ο χαρακτήρας `#`. Η δεύτερη προαιρετική παράμετρος επιτρέπει την αντιστροφή της συμπεριφοράς, δηλαδή αν χρησιμοποιούσαμε `toggle('#address-container', false)`, το στοιχείο θα εμφανιζόταν μόνο εάν το πλαίσιο ελέγχου δεν ήταν επιλεγμένο. - -Η προεπιλεγμένη υλοποίηση στο JavaScript αλλάζει την ιδιότητα `hidden` των στοιχείων. Ωστόσο, μπορούμε εύκολα να αλλάξουμε τη συμπεριφορά, για παράδειγμα, προσθέτοντας μια κίνηση. Απλά αντικαταστήστε τη μέθοδο `Nette.toggle` στο JavaScript με τη δική σας λύση: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // απόκρυψη ή εμφάνιση του 'el' βάσει της τιμής 'visible' - }); -}; -``` - - -Απενεργοποίηση Επικύρωσης -========================= - -Μερικές φορές μπορεί να είναι χρήσιμο να απενεργοποιήσετε την επικύρωση. Εάν το πάτημα ενός κουμπιού υποβολής δεν πρέπει να εκτελεί επικύρωση (κατάλληλο για κουμπιά *Ακύρωση* ή *Προεπισκόπηση*), μπορούμε να την απενεργοποιήσουμε χρησιμοποιώντας τη μέθοδο `$submit->setValidationScope([])`. Εάν πρέπει να εκτελεί μόνο μερική επικύρωση, μπορούμε να καθορίσουμε ποια πεδία ή containers φόρμας πρέπει να επικυρωθούν. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Επικυρώνει ολόκληρη τη φόρμα -$form->addSubmit('send2') - ->setValidationScope([]); // Δεν επικυρώνει καθόλου -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Επικυρώνει μόνο το στοιχείο name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Επικυρώνει μόνο το στοιχείο age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Επικυρώνει το container details -``` - -Το `setValidationScope` δεν επηρεάζει το [#συμβάν onValidate] στη φόρμα, το οποίο θα καλείται πάντα. Το συμβάν `onValidate` σε ένα container θα ενεργοποιείται μόνο εάν αυτό το container έχει επισημανθεί για μερική επικύρωση. diff --git a/forms/en/@left-menu.texy b/forms/en/@left-menu.texy index f23cd43594..676b8c155e 100644 --- a/forms/en/@left-menu.texy +++ b/forms/en/@left-menu.texy @@ -6,7 +6,9 @@ Nette Forms - [Form Controls |controls] - [Validation |validation] - [Rendering |rendering] +- [Custom Controls |custom-controls] - [Configuration] +- [Upgrading] Further Reading diff --git a/forms/en/configuration.texy b/forms/en/configuration.texy index ea3c55f991..76d09bcf03 100644 --- a/forms/en/configuration.texy +++ b/forms/en/configuration.texy @@ -17,6 +17,7 @@ forms: 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.' @@ -24,7 +25,7 @@ forms: MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG, WebP or AVIF.' + 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.' @@ -54,8 +55,9 @@ forms: + \-- -If you are not using the whole framework and therefore not even the configuration files, you can change the default error messages directly in the `Nette\Forms\Validator::$messages` field. +If you are not using the whole framework and therefore not even the configuration files, you can change the default error messages directly in the `Nette\Forms\Validator::$messages` array. diff --git a/forms/en/controls.texy b/forms/en/controls.texy index 0850b9ab5e..23e428724f 100644 --- a/forms/en/controls.texy +++ b/forms/en/controls.texy @@ -5,8 +5,8 @@ Form Controls Overview of standard form controls. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== Adds a single-line text input field (class [TextInput |api:Nette\Forms\Controls\TextInput]). If the user does not fill in the field, it returns an empty string `''`, or use `setNullable()` to make it return `null` instead. @@ -34,8 +34,8 @@ $form->addText('phone', 'Phone:') ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== Adds a multi-line text input field (class [TextArea |api:Nette\Forms\Controls\TextArea]). If the user doesn't fill in the field, it returns an empty string `''`, or use `setNullable()` to make it return `null` instead. @@ -49,8 +49,8 @@ Automatically validates UTF-8 and normalizes line endings to `\n`. Unlike the si The maximum length can be limited using `setMaxLength()`. The [addFilter() |validation#Modifying Input Values] method allows modifying the user-entered value. An empty value can be set using `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== Adds an input field for entering an integer (class [TextInput |api:Nette\Forms\Controls\TextInput]). Returns either an integer or `null` if the user enters nothing. @@ -62,15 +62,15 @@ $form->addInteger('year', 'Year:') The control renders as `<input type="number">`. Using the `setHtmlType()` method, you can change the type to `range` for display as a slider, or to `text` if you prefer a standard text field without the special behavior of the `number` type. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= Adds an input field for entering a floating-point number (class [TextInput |api:Nette\Forms\Controls\TextInput]). Returns either a float or `null` if the user enters nothing. ```php $form->addFloat('level', 'Level:') - ->setDefaultValue(0.0) // Explicitly set float default - ->addRule($form::Range, 'The level must be between %f and %f.', [0.0, 100.0]); // Use float range and %f + ->setDefaultValue(0) + ->addRule($form::Range, 'The level must be between %d and %d.', [0, 100]); ``` The control renders as `<input type="number">`. Using the `setHtmlType()` method, you can change the type to `range` for display as a slider, or to `text` if you prefer a standard text field without the special behavior of the `number` type. @@ -78,8 +78,8 @@ The control renders as `<input type="number">`. Using the `setHtmlType()` method Nette and the Chrome browser accept both a comma and a dot as decimal separators. To enable this functionality in Firefox as well, it is recommended to set the `lang` attribute either for the specific control or for the entire page, for example, `<html lang="en">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ Adds an input field for entering an email address (class [TextInput |api:Nette\Forms\Controls\TextInput]). If the user doesn't fill in the field, it returns an empty string `''`, or use `setNullable()` to make it return `null` instead. @@ -92,8 +92,8 @@ Validates that the value is a valid email address. It does not check if the doma The maximum length can be limited using `setMaxLength()`. The [addFilter() |validation#Modifying Input Values] method allows modifying the user-entered value. An empty value can be set using `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== Adds a password input field (class [TextInput |api:Nette\Forms\Controls\TextInput]). @@ -107,8 +107,8 @@ $form->addPassword('password', 'Password:') When the form is redisplayed, the field will be empty. Automatically validates UTF-8, trims leading and trailing whitespace, and removes line breaks that could be sent by an attacker. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ Adds a checkbox (class [Checkbox |api:Nette\Forms\Controls\Checkbox]). Returns `true` or `false`, depending on whether it is checked. @@ -118,10 +118,10 @@ $form->addCheckbox('agree', 'I agree with terms') ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Adds a list of checkboxes for selecting multiple items (class [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Returns an array of the keys of the selected items. The `getSelectedItems()` method returns the values instead of keys. +Adds a list of checkboxes for selecting multiple items (class [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Returns an array of the keys of the selected items. The `getSelectedItems()` method returns the selected items as key-value pairs. ```php $form->addCheckboxList('colors', 'Colors:', [ @@ -131,7 +131,7 @@ $form->addCheckboxList('colors', 'Colors:', [ ]); ``` -Pass the array of offered items as the third parameter or using the `setItems()` method. +Pass the array of offered items as the third parameter or using the `setItems()` method. By passing `false` as the second argument to `setItems()`, the values are used as keys as well. Use `setDisabled(['r', 'g'])` to disable individual items. @@ -146,8 +146,8 @@ $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ Adds radio buttons (class [RadioList |api:Nette\Forms\Controls\RadioList]). Returns the key of the selected item, or `null` if the user selected nothing. The `getSelectedItem()` method returns the value instead of the key. @@ -155,6 +155,7 @@ Adds radio buttons (class [RadioList |api:Nette\Forms\Controls\RadioList]). Retu $sex = [ 'm' => 'male', 'f' => 'female', + 'o' => 'other', ]; $form->addRadioList('gender', 'Gender:', $sex); ``` @@ -168,8 +169,8 @@ The control automatically checks that no forgery occurred and that the selected When setting the default selected item, it also checks that it is one of the offered ones, otherwise it throws an exception. This check can be disabled using `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== Adds a select box (class [SelectBox |api:Nette\Forms\Controls\SelectBox]). Returns the key of the selected item, or `null` if the user selected nothing. The `getSelectedItem()` method returns the value instead of the key. @@ -213,10 +214,10 @@ The control automatically checks that no forgery occurred and that the selected When setting the default selected item, it also checks that it is one of the offered ones, otherwise it throws an exception. This check can be disabled using `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Adds a select box for selecting multiple items (class [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Returns an array of the keys of the selected items. The `getSelectedItems()` method returns the values instead of keys. +Adds a select box for selecting multiple items (class [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Returns an array of the keys of the selected items. The `getSelectedItems()` method returns the selected items as key-value pairs. ```php $form->addMultiSelect('countries', 'Countries:', $countries); @@ -231,10 +232,10 @@ The control automatically checks that no forgery occurred and that the selected When setting default selected items, it also checks that they are among the offered ones, otherwise it throws an exception. This check can be disabled using `checkDefaultValue(false)`. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Adds a file upload field (class [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returns a [FileUpload |http:request#FileUpload] object, even if the user did not upload any file, which can be checked using the `FileUpload::hasFile()` method. +Adds a file upload field (class [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returns a [FileUpload |http:request#FileUpload] object, even if the user did not upload any file, which can be checked using the `FileUpload::hasFile()` method. Using `setNullable()`, you can make the control return `null` instead of a `FileUpload` object when no file is uploaded. ```php $form->addUpload('avatar', 'Avatar:') @@ -249,14 +250,14 @@ Never trust the original file name returned by the `FileUpload::getName()` metho The `MimeType` and `Image` rules detect the required type based on the file's signature and do not verify its integrity. Whether an image is corrupted can be determined, for example, by attempting to [load it |http:request#toImage]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== Adds a field for uploading multiple files at once (class [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returns an array of [FileUpload |http:request#FileUpload] objects. The `FileUpload::hasFile()` method for each of them will return `true`. ```php $form->addMultiUpload('files', 'Files:') - ->addRule($form::MaxLength, 'Maximum of %d files can be uploaded.', 10); // Added period + ->addRule($form::MaxLength, 'Maximum of %d files can be uploaded.', 10); ``` If any file fails to upload correctly, the form is not successfully submitted, and an error is displayed. That is, upon successful submission, it is not necessary to check the `FileUpload::isOk()` method for each file. @@ -266,8 +267,8 @@ Never trust the original file names returned by the `FileUpload::getName()` meth The `MimeType` and `Image` rules detect the required type based on the file's signature and do not verify its integrity. Whether an image is corrupted can be determined, for example, by attempting to [load it |http:request#toImage]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== Adds a field that allows the user to easily enter a date consisting of year, month, and day (class [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). @@ -287,8 +288,8 @@ $form->addDate('date', 'Date:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== Adds a field that allows the user to easily enter a time consisting of hours, minutes, and optionally seconds (class [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). @@ -296,7 +297,7 @@ As a default value, it accepts objects implementing the `DateTimeInterface`, a s ```php $form->addTime('time', 'Time:', withSeconds: true) - ->addRule($form::Range, 'Time must be between %s and %s.', ['12:30', '13:30']); // Use %s for time strings + ->addRule($form::Range, 'Time must be between %d and %d.', ['12:30', '13:30']); ``` By default, it returns a `DateTimeImmutable` object (with the date set to January 1, year 1). Using the `setFormat()` method, you can specify a [text format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: @@ -307,8 +308,8 @@ $form->addTime('time', 'Time:') ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== Adds a field that allows the user to easily enter both date and time, consisting of year, month, day, hours, minutes, and optionally seconds (class [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). @@ -328,8 +329,8 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== Adds a color picker field (class [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). The color is returned as a string in the format `#rrggbb`. If the user does not make a selection, it returns black `#000000`. @@ -339,8 +340,8 @@ $form->addColor('color', 'Color:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== Adds a hidden field (class [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -353,8 +354,8 @@ Use `setNullable()` to make it return `null` instead of an empty string. The [ad Although the control is hidden, **it is important to realize** that its value can still be modified or spoofed by an attacker. Always thoroughly verify and validate all received values on the server side to prevent security risks associated with data manipulation. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== Adds a submit button (class [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). @@ -362,6 +363,15 @@ Adds a submit button (class [SubmitButton |api:Nette\Forms\Controls\SubmitButton $form->addSubmit('submit', 'Submit'); ``` +.{data-version:3.3.0} +The handler can be passed directly to the button as the third parameter `$onSubmit` instead of attaching it to the `onClick` event: + +```php +$form->addSubmit('submit', 'Submit', function (SubmitButton $button, $data): void { + // ... +}); +``` + It is possible to have more than one submit button in the form: ```php @@ -380,8 +390,8 @@ if ($form['register']->isSubmittedBy()) { If you do not want to validate the entire form when a button is pressed (for example, for *Cancel* or *Preview* buttons), use [setValidationScope() |validation#Disabling Validation]. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== Adds a button (class [Button |api:Nette\Forms\Controls\Button]) that does not have a submit function. It can therefore be used for other functions, e.g., calling a JavaScript function on click. @@ -391,8 +401,8 @@ $form->addButton('raise', 'Raise salary') ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= Adds a submit button in the form of an image (class [ImageButton |api:Nette\Forms\Controls\ImageButton]). @@ -441,7 +451,7 @@ For all controls, we can call the following methods (see the [API documentation| .[table-form-methods language-php] | `setDefaultValue($value)` | sets the default value -| `getValue()` | get the current value +| `getValue()` | gets the current value | `setOmitted()` | [#Omitted Values] | `setDisabled()` | [#Disabling Inputs] @@ -451,8 +461,6 @@ Rendering: | `setTranslator($translator)` | sets the [translator |rendering#Translating] | `setHtmlAttribute($name, $value)` | sets an [HTML attribute |rendering#HTML Attributes] for the element | `setHtmlId($id)` | sets the HTML attribute `id` -| `setHtmlType($type)` | sets the HTML attribute `type` -| `setHtmlName($name)` | sets the HTML attribute `name` | `setOption($key, $value)` | [sets rendering options |rendering#Options] Validation: @@ -462,7 +470,7 @@ Validation: | `addCondition()`, `addConditionOn()` | sets a [validation condition |validation#Conditions] | `addError($message)` | [adds an error message |validation#Processing Errors] -For controls `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, the following methods can be called: +For controls `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, the following methods can be called: .[table-form-methods language-php] | `setNullable()` | sets whether getValue() returns `null` instead of an empty string @@ -510,30 +518,14 @@ An alternative to disabled controls are controls with the HTML `readonly` attrib Custom Controls =============== -Besides the wide range of built-in form controls, you can add custom controls to the form in this way: +Besides the wide range of built-in form controls, you can add custom controls to the form: ```php $form->addComponent(new DateInput('Date:'), 'date'); // alternative syntax: $form['date'] = new DateInput('Date:'); ``` -.[note] -The form is a descendant of the [Container |component-model:#Container] class, and individual controls are descendants of the [Component |component-model:#Component] class. - -There is a way to define new form methods for adding custom controls (e.g., `$form->addZip()`). These are called extension methods. The disadvantage is that code completion in editors will not work for them. - -```php -use Nette\Forms\Container; - -// adds method addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'At least 5 numbers', '[0-9]{5}'); -}); - -// usage -$form->addZip('zip', 'ZIP code:'); -``` +How to write such a control, including reading submitted data, validation, and rendering, is described in a [separate chapter |custom-controls]. You'll also learn about extension methods there, which let you create your own adding method like `$form->addZip()`. Low-Level Fields diff --git a/forms/en/custom-controls.texy b/forms/en/custom-controls.texy new file mode 100644 index 0000000000..52fb56c5d1 --- /dev/null +++ b/forms/en/custom-controls.texy @@ -0,0 +1,268 @@ +Custom Form Controls +******************** + +.[perex] +Nette offers a wide palette of [built-in form controls |controls]. But when you run into a requirement that isn't among them, you don't have to work around anything or glue things together: you write your own control. It will be able to do everything the built-in ones can - validate, translate itself, render - and it will be used exactly the same way. + +We'll show it on a practical example: a control for entering a date using three fields, day, month, and year. Along the way, you'll learn everything you need to know about writing controls. + + +When to Write a Custom Control and When Not To +============================================== + +A custom control is the most powerful tool that forms offer. And like every powerful tool, it should be the last choice, not the first. Many situations can be solved by simpler means: + +- **Modifying a value** is handled by [addFilter() |validation#Modifying Input Values]. Want to tolerate spaces in a ZIP code or lowercase letters in a code? A filter is a few lines. +- **Repeated configuration** is wrapped by a custom adding method. Adding a ZIP code field with the same validation in ten places? Create a named shortcut for them, [we'll show it at the end |#Custom Adding Method]. +- **A group of related fields** is served by a [container |controls#addContainer]. An address composed of street, city, and ZIP code doesn't need a custom control, a container with three text fields is enough. +- **A different look** is achieved with [setHtmlType() |controls#addText] and HTML attributes, or [prototypes |rendering#Prototypes]. + +A custom control makes sense the moment you need a **custom value**: a control that acts as a single field with a single value on the outside, but internally consists of several inputs or stores the value differently than it displays it. A date from three fields. Coordinates picked by clicking on a map. A tag input with autocomplete. + + +Anatomy of a Control +==================== + +Every custom control inherits from the abstract class [api:Nette\Forms\Controls\BaseControl]. From it, it inherits a huge amount of ready-made functionality: value storage, validation rules and conditions, error messages, translations, HTML attributes, the label, and the connection to rendering. You write only what makes your control different. + +A minimal working control is surprisingly short: + +```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, + ]); + } +} +``` + +Two methods: one says how to obtain the value from the submitted data, the other how to render the control. We'll take a close look at both in a moment. Everything else - `setRequired()`, `addRule()`, `setDefaultValue()`, translations - already works by itself. + +You add the control to the form using the `addComponent()` method, or more concisely via square brackets: + +```php +$form['nickname'] = new SimpleInput('Nickname:'); +``` + + +Life Cycle of a Control +======================= + +Before we get to a more interesting control, it's good to know what happens to a control and when. The form and its controls are [components |component-model:] forming a tree. This has one pleasant consequence: the control doesn't have to find out anything on its own, the framework takes care of everything important at the right moment: + +1) The moment you attach the control to a submitted form, the form itself calls `loadHttpData()` on it. In it, the control reads its submitted value, as we'll show in a moment. It never works directly with `$_POST` and doesn't have to care at all whether it's nested in containers. + +2) When the form is submitted, validation takes place: the rules added via `addRule()` are evaluated, working with the value from `getValue()`. + +3) Whoever then calls `$form->getValues()` or `getValue()` on the control gets a clean, typed value - such as a `DateTimeImmutable` object, not a triple of strings from the form. + +And during rendering, `getControl()` is called, or `getLabel()` for the label. + + +Reading the Submitted Value +=========================== + +In the `loadHttpData()` method, the control asks for its submitted value using the `getHttpData()` method. Its parameter is a type that determines how the value should be cleaned: + +| type | meaning +|------- +| `Form::DataLine` | single-line text: replaces line breaks with spaces, trims spaces +| `Form::DataText` | multi-line text: normalizes line endings to `\n` +| `Form::DataFile` | upload, an instance of `Nette\Http\FileUpload` + +No matter how hard an attacker tries, the result is always a valid UTF-8 string without control characters (or an upload object or `null`). This is exactly why we never read the value directly from `$_POST` - we'd lose all these guarantees. + +A control consisting of several inputs, like our date, passes a part of the HTML name as the second parameter and reads its individual sub-values this way. It stores them in its own properties `$day`, `$month`, and `$year` of type 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]') ?? ''; +} +``` + +If the HTML name ends with `[]`, an array of values is returned. By combining with the `Form::DataKeys` type (i.e. `Form::DataLine | Form::DataKeys`), you also preserve its keys: + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +A missing value is `null` (an empty array for arrays). The request doesn't have to contain the control's data at all, nothing prevents an attacker from sending whatever they like - that's why we add `?? ''` in the example and why you should always account for this variant. + + +Value of the Control +==================== + +The control keeps its value and exposes it through a trio of methods whose contract is worth following. + +The `setValue()` method accepts a value from the programmer - this is also the path taken by `setDefaultValue()` and `$form->setDefaults()`. It should accept everything that makes sense, convert the value to its internal form, and throw an exception on nonsensical input, so that the error shows up immediately and not through mysterious form behavior. Our date accepts a `DateTimeInterface`, a string, a timestamp, or `null`, and splits them into the three fields: + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // nonsense throws an exception + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +The `getValue()` method, on the other hand, composes a clean, typed value - the only thing the user of your control will see. If the value is not valid, it returns `null`. The static method `validateDate()` simply checks that the three fields add up to an existing date: + +```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; +} +``` + +And the `isFilled()` method says whether the user has filled in the control - it's used by the `setRequired()` rule. The default implementation (a non-empty value) often suffices, but for a composite control, override it according to its logic: + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Rendering +========= + +The `getControl()` method returns the HTML form of the control, usually as an [Html |utils:html-elements] object, but a plain string is fine too - it doesn't matter. We reach for the Html object mainly when assembling the code, because it lets us build the resulting markup safely and with a pleasant API. You have several helpers at your disposal: + +- `getHtmlName()` returns the HTML `name` attribute, including possible nesting in containers (e.g. `invoice[date]`). For a composite control, you append the name parts of the individual inputs to it: `$name . '[day]'`. +- `getHtmlId()` returns the `id` attribute linked with the label. +- `Helpers::exportRules($this->getRules())` exports the validation rules for the `data-nette-rules` attribute, thanks to which [JavaScript validation |validation#JavaScript Validation] will work for your control too. The attribute belongs on the first input of the control. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` assembles a `<select>` element from an array of items (nested arrays are rendered as `<optgroup>`) and returns it as `Html` - handy for the month field of our date. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` generates a list of `<input>` elements wrapped in `<label>`s (radio buttons or checkboxes) and returns it as a string. + +The first field of our date is therefore created like this: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select for the month and input for the year ... */); +} +``` + +The label is rendered by `getLabel()` and its default implementation usually suits. Just beware: for a composite control, its `for` attribute points to `getHtmlId()`, so give this id to the first input - exactly as in the example. + +To let the composite control be rendered part by part in a template (e.g. `{input birthdate:day}`), override the `getControlPart($key)` and `getLabelPart($key)` methods, which return the `Html` element for the given part - the same way `CheckboxList` and `RadioList` do. + +.[note] +If you override `getControl()`, keep in mind that `BaseControl::getControl()` also marks the control as rendered via `setOption('rendered', true)`. Call it as well (or call `parent::getControl()`) when you combine manual and automatic rendering of the same form, so the control is not rendered twice. (The `DateInput` example above omits this for brevity.) + + +Complete Example: DateInput +=========================== + +All the described pieces together, complemented by a select box for choosing the month, can be found in the finished `DateInput` control among the [examples right in the repository |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Note that in the constructor, the control adds a validation rule to itself that checks the date makes sense. Nonsensical input, such as February 31, thus shows up as an ordinary form validation error: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'The date is invalid.'); +} +``` + +And the usage? Exactly like with the built-in controls: + +```php +$form['birthdate'] = (new DateInput('Date of birth:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('When were you born?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +In a Latte template, you render it with the usual `{input birthdate}` or `{label birthdate /}` tag, just like any other control. + + +Validation +========== + +The built-in validation rules work with a custom control right away - they operate on the value from `getValue()`. Our `DateInput` can thus use, for example, `Form::Min` for the oldest allowed date. How to write your own rules, including their JavaScript counterpart, is described in the chapter [Custom Rules and Conditions |validation#Custom Rules and Conditions]. + + +Custom Adding Method +==================== + +We add built-in controls with the convenient methods `$form->addText()` and friends. A custom control has no such method, so you add it by plain assignment - it works the same in a form and in a container, and editors and static analysis understand it: + +```php +$form['birthdate'] = new DateInput('Date of birth:'); +``` + +If you want to shorten the adding while keeping autocompletion, a static factory method right on the control comes in handy. It works even in nested containers, which a method on a descendant of the `Form` class couldn't do - nested containers don't know about it: + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// works in a form and in any container: +DateInput::addTo($form, 'birthdate', 'Date of birth:'); +``` + +The same approach also works as a named shortcut for repeated configuration of a built-in control: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'The ZIP code must be exactly 5 digits', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'ZIP code:'); +``` diff --git a/forms/en/in-presenter.texy b/forms/en/in-presenter.texy index a1543bcaa1..79d29f6c38 100644 --- a/forms/en/in-presenter.texy +++ b/forms/en/in-presenter.texy @@ -19,18 +19,18 @@ $form = new Form; $form->addText('name', 'Name:'); $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Sign up'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` and in the browser, it will be displayed like this: [* form-en.webp *] -A form in a presenter is an object of the `Nette\Application\UI\Form` class; its predecessor `Nette\Forms\Form` is intended for standalone use. We added controls named name, password, and a submit button. Finally, the line `$form->onSuccess[] = [$this, 'formSucceeded'];` states that after submission and successful validation, the method `$this->formSucceeded()` should be called. +A form in a presenter is an object of the `Nette\Application\UI\Form` class; its predecessor `Nette\Forms\Form` is intended for standalone use. We added controls named name, password, and a submit button. Finally, the line `$form->onSuccess` states that after submission and successful validation, the method `$this->formSucceeded()` should be called. From the presenter's perspective, the form is a regular component. Therefore, it is treated as a component and integrated into the presenter using a [factory method |application:components#Factory Methods]. It will look like this: -```php .{file:app/Presentation/HomePresenter.php} +```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; use Nette\Application\UI\Form; @@ -42,11 +42,11 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Name:'); $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Sign up'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { // here we will process the data sent by the form // $data->name contains name @@ -79,10 +79,12 @@ The `$data` object contains the `name` and `password` properties with the data e $form->addError('Sorry, username is already in use.'); ``` -Besides `onSuccess`, there is also `onSubmit`: callbacks are called whenever the form is submitted, even if it is not filled correctly. And also `onError`: callbacks are called only if the submission is not valid. They are even called if we invalidate the form in `onSuccess` or `onSubmit` using `addError()`. +Besides `onSuccess`, there is also `onSubmit`: callbacks are called whenever the form is submitted, even if it is not filled correctly. And also `onError`: callbacks are called only if the submission is not valid. They are even called if we invalidate the form in `onSuccess` using `addError()`. After processing the form, we redirect to another page. This prevents the unwanted resubmission of the form by using the *refresh*, *back* button, or navigating through browser history. +If the form is submitted over AJAX, you usually redraw a [snippet |application:ajax] with the re-rendered form instead of redirecting. + Try adding other [form controls|controls]. @@ -142,7 +144,7 @@ If you look at the source code of the page with the form, you might notice that We add further validation rules using the `addRule()` method. The first parameter is the rule, the second is again the text of the error message, and an argument for the validation rule may follow. What does that mean? -Let's extend the form with a new optional field 'age', which must be an integer (`addInteger()`) and also within an allowed range (`Form::Range`). Here we will use the third parameter of the `addRule()` method to pass the required range to the validator as a pair `[min, max]`: +Let's extend the form with a new optional field 'age', which must be an integer (`addInteger()`) and also within an allowed range (`$form::Range`). Here we will use the third parameter of the `addRule()` method to pass the required range to the validator as a pair `[min, max]`: ```php $form->addInteger('age', 'Age:') @@ -158,7 +160,7 @@ This creates room for a small refactoring. In the error message and the third pa ->addRule($form::Range, 'Age must be between %d and %d years.', [18, 120]); ``` -Let's return to the `password` control, make it required as well, and also verify the minimum password length (`Form::MinLength`), again using a placeholder in the message: +Let's return to the `password` control, make it required as well, and also verify the minimum password length (`$form::MinLength`), again using a placeholder in the message: ```php $form->addPassword('password', 'Password:') @@ -166,7 +168,7 @@ $form->addPassword('password', 'Password:') ->addRule($form::MinLength, 'Your password must be at least %d characters long.', 8); ``` -Let's add another field `passwordVerify` to the form, where the user enters the password again for confirmation. Using validation rules, we check if both passwords are the same (`Form::Equal`). As the argument, we provide a reference to the first password using [square brackets |#Access to Controls]: +Let's add another field `passwordVerify` to the form, where the user enters the password again for confirmation. Using validation rules, we check if both passwords are the same (`$form::Equal`). As the argument, we provide a reference to the first password using [square brackets |#Access to Controls]: ```php $form->addPassword('passwordVerify', 'Password again:') @@ -193,12 +195,14 @@ $form->addEmail('email', 'Email') It's often useful to set default values for all controls simultaneously. For example, when the form is used for editing records. We read the record from the database and set the default values: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Call `setDefaults()` after defining the controls. +On an already submitted form, `setDefaults()` has no effect - it won't overwrite what the user filled in, so it is safe to call it unconditionally in the form factory. If you need to force the values even after submission, use `setValues()` instead. + Rendering the Form ================== @@ -250,7 +254,7 @@ How do we tell Nette to return data as objects of this class? Easier than you mi ```php public function formSucceeded(Form $form, RegistrationFormData $data): void { - // $name is instance of RegistrationFormData + // $data is an instance of RegistrationFormData $name = $data->name; // ... } @@ -265,6 +269,8 @@ $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` +If you need to read the values before the form is validated - typically inside an `onValidate` handler - use the `getUntrustedValues()` method instead. It accepts the same parameters as `getValues()`, but returns the submitted values without guaranteeing they have passed validation. + If the forms have a multi-level structure composed of containers, create a separate class for each: ```php @@ -303,16 +309,19 @@ If the form has more than one button, we usually need to distinguish which one w ```php $form->addSubmit('save', 'Save') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Delete') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` +.{data-version:3.3.0} +A handler can also be passed to the button directly as the third argument of the `addSubmit()` method. + These handlers are called only if the form is validly filled (unless validation is disabled for the button), just like the `onSuccess` event. The difference is that the first parameter passed can be the submit button object instead of the form, depending on the type hint you specify: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... @@ -349,23 +358,22 @@ Nette Framework places great emphasis on security and therefore meticulously ens Besides protecting forms against attacks like [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] and [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], it performs many small security measures that you no longer need to think about. -For example, it filters all control characters from inputs and checks the validity of UTF-8 encoding, ensuring the data from the form is always clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered ones and that no forgery occurred. We've already mentioned that for single-line text inputs, it removes end-of-line characters that an attacker might send. For multi-line inputs, it normalizes end-of-line characters. And so on. +For example, it filters all control characters from inputs and checks the validity of UTF-8 encoding, ensuring the data from the form is always clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered ones and that no forgery occurred. We've already mentioned that for single-line text inputs, it replaces end-of-line characters that an attacker might send with spaces. For multi-line inputs, it normalizes end-of-line characters. And so on. Nette handles security risks for you that many programmers aren't even aware exist. -The mentioned CSRF attack involves an attacker luring a victim to a page that silently executes a request in the victim's browser to the server where the victim is logged in. The server then believes the request was made by the victim willingly. Therefore, Nette prevents POST forms from being submitted from a different domain. If, for some reason, you want to disable this protection and allow form submission from another domain, use: +The mentioned CSRF attack involves an attacker luring a victim to a page that silently executes a request in the victim's browser to the server where the victim is logged in. The server then believes the request was made by the victim willingly. Therefore, Nette rejects POST forms submitted from a foreign origin; even a different subdomain of the same site counts as foreign. If you need to allow submissions from another origin, disable the protection with: ```php -$form->allowCrossOrigin(); // WARNING! Disables protection! +$form->allowCrossOrigin(); // WARNING! Disables protection completely! ``` -This protection uses a SameSite cookie named `_nss`. SameSite cookie protection might not be 100% reliable, so it's advisable to also enable token protection: +This disables the protection for any origin, though. To allow only specific origins, disable the protection and verify the `Origin` header yourself against your own allowlist. -```php -$form->addProtection(); -``` +The protection relies on the browser's `Sec-Fetch-Site` header (Fetch Metadata), which the browser sends automatically and which cannot be spoofed even with an XSS vulnerability. For older browsers without support for it, a fallback SameSite cookie applies, which a Nette application sets automatically. The article [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] describes it in detail. -It is strongly recommended to apply this protection to forms in the administrative parts of your website that modify sensitive data. The framework defends against CSRF attacks by generating and verifying an authorization token stored in the session. Therefore, it is necessary to have a session started before displaying the form. In the administrative part of a website, the session is usually already started due to user login. Otherwise, start the session using `Nette\Http\Session::start()`. +.[note] +The earlier protection using an authorization token stored in the session, activated by `$form->addProtection()`, is no longer needed and is deprecated since version 3.3. Using One Form in Multiple Presenters @@ -402,8 +410,8 @@ protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); // we can change the form, here for example we change the label on the button - $form['login']->setCaption('Continue'); - $form->onSuccess[] = [$this, 'signInFormSubmitted']; // and add handler + $form['send']->setCaption('Continue'); + $form->onSuccess[] = $this->signInFormSuceeded(...); // and add handler return $form; } ``` diff --git a/forms/en/rendering.texy b/forms/en/rendering.texy index ccb19528ba..90cc780e37 100644 --- a/forms/en/rendering.texy +++ b/forms/en/rendering.texy @@ -114,7 +114,7 @@ Prefer not to think about which HTML element to use for each control in the temp </form> ``` -If the form uses a translator, the text inside the `{label}` tags will be translated. +If the form uses a translator, labels rendered from the form definition (e.g. `{label username /}`) are translated. Text written directly between the `{label}` and `{/label}` tags is not. Again, more complex form controls, such as RadioList or CheckboxList, can be rendered item by item: @@ -149,7 +149,25 @@ We can check for the presence of an error using the `hasErrors()` method and set `{form}` -------- -The tags `{form signInForm}...{/form}` are an alternative to `<form n:name="signInForm">...</form>`. +The tags `{form signInForm}...{/form}` are an alternative to `<form n:name="signInForm">...</form>`. Separate any arguments from the name with a comma: `{form signInForm, class: foo}`. + +.{data-version:3.3.0} +The `scope` keyword placed before the name only pushes the form onto the stack (so that `{input}`, `{label}`, etc. bind to it) but does not render the `<form>` tag. It is handy for rendering a part of a form, e.g. in a snippet. If a form is already active, the name is resolved relative to it, so `{form scope}` also replaces `{formContainer}`: + +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +The `detached` keyword renders an empty `<form></form>` and links every control to it via the HTML `form` attribute. This lets you place a form inside another form, which HTML otherwise forbids. The detached form must have an HTML `id`, which is generated automatically when you give it a name (like `outerForm` below): + +```latte +{form detached outerForm} + ... +{/form} +``` Automatic Rendering @@ -240,7 +258,7 @@ If you need to render only the inner part of the form without the `<form>` HTML </form> ``` -The `{formContainer}` tag helps with rendering controls inside a form container. +The `{formContainer}` tag, or the newer [`{form scope}` |#form], helps with rendering controls inside a form container. ```latte <p>Which news you wish to receive:</p> @@ -278,18 +296,18 @@ This allows the form to be rendered element by element: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -299,8 +317,8 @@ This allows the form to be rendered element by element: While for some controls `getControl()` returns a single HTML element (e.g., `<input>`, `<select>`, etc.), for others it returns a complete piece of HTML code (CheckboxList, RadioList). In such cases, you can use methods that generate individual inputs and labels for each item separately: -- `getControlPart($key = null): ?Html` returns the HTML code of a single item -- `getLabelPart($key = null): ?Html` returns the HTML code for the label of a single item +- `getControlPart($key = null): Html` returns the HTML code of a single item +- `getLabelPart($key = null): Html` returns the HTML code for the label of a single item .[note] These methods have the prefix `get` for historical reasons, but `generate` would be more appropriate, as they create and return a new `Html` element upon each call. @@ -555,7 +573,7 @@ Forms support outputting texts via the translator. We pass it using the `setTran $form->setTranslator($translator); ``` -From this point on, not only all labels but also all error messages or items in select boxes will be translated into the target language. +From this point on, not only all labels but also all error messages, items in select boxes, and input placeholders will be translated into the target language. It is possible to set a different translator for individual form controls or disable translation completely by setting the value to `null`: diff --git a/forms/en/standalone.texy b/forms/en/standalone.texy index 0260668a0e..57f0efc234 100644 --- a/forms/en/standalone.texy +++ b/forms/en/standalone.texy @@ -10,6 +10,12 @@ However, if you use Nette Application and presenters, there is a dedicated guide First Form ========== +Before you start, install the package using [Composer |best-practices:composer]: + +```shell +composer require nette/forms +``` + Let's try writing a simple registration form. Its code will be as follows ("full code":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php @@ -170,12 +176,14 @@ $form->addEmail('email', 'Email') It's often useful to set default values for all controls simultaneously, for example, when the form is used for editing records. We read the record from the database and set its values as defaults: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Call `setDefaults()` after defining the controls. +On an already submitted form, `setDefaults()` has no effect - it won't overwrite what the user filled in, so it is safe to call it unconditionally in the form factory. If you need to force the values even after submission, use `setValues()` instead. + Rendering the Form ================== @@ -192,6 +200,21 @@ $form->addInteger('age', 'Age:') There are many ways to render a form, so a [separate chapter is dedicated to rendering |rendering]. +Rendering with Latte +-------------------- + +If you have the [Latte |latte:] templating engine at hand, you can let it render the form and gain full control over the resulting HTML. You create the engine, register the forms extension, and pass the form into the template as a variable: + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); +``` + +In the template, you then work with the form through the `$form` variable and tags like `{input}`, `{label}`, or `n:name`. A complete example including the template can be found in the [examples |https://github.com/nette/forms/tree/master/examples] directory (the files `latte.php` and `latte/`). The individual tags are described in the chapter on [rendering |rendering]. + + Mapping to Classes ================== @@ -213,7 +236,7 @@ class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } @@ -294,24 +317,21 @@ Nette Framework places a strong emphasis on security and therefore meticulously In addition to protecting forms against well-known vulnerabilities like [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] and [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], it performs many small security measures that you no longer need to think about. -For example, it filters all control characters from inputs and checks the validity of UTF-8 encoding, ensuring that the data from the form will always be clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered options and that no forgery occurred. We've already mentioned that for single-line text inputs, it removes end-of-line characters that an attacker might send. For multi-line inputs, it normalizes end-of-line characters. And so on. +For example, it filters all control characters from inputs and checks the validity of UTF-8 encoding, ensuring that the data from the form will always be clean. For select boxes and radio lists, it verifies that the selected items were actually among the offered options and that no forgery occurred. We've already mentioned that for single-line text inputs, it replaces end-of-line characters that an attacker might send with spaces. For multi-line inputs, it normalizes end-of-line characters. And so on. Nette handles security risks for you that many programmers are unaware even exist. -The mentioned CSRF attack involves an attacker luring a victim to a page that silently executes a request in the victim's browser to the server where the victim is currently logged in. The server believes that the request was made by the victim voluntarily. Therefore, Nette prevents the form from being submitted via POST from a different domain. If, for some reason, you want to disable protection and allow the form to be submitted from another domain, use: +The mentioned CSRF attack involves an attacker luring a victim to a page that silently executes a request in the victim's browser to the server where the victim is currently logged in. The server believes that the request was made by the victim voluntarily. Therefore, Nette rejects POST forms submitted from a foreign origin; even a different subdomain of the same site counts as foreign. If you need to allow submissions from another origin, disable the protection with: ```php -$form->allowCrossOrigin(); // WARNING! Disables protection! +$form->allowCrossOrigin(); // WARNING! Disables protection completely! ``` -This protection uses a SameSite cookie named `_nss`. Therefore, create the form object before sending the first output so that the cookie can be sent. +This disables the protection for any origin, though. To allow only specific origins, disable the protection and verify the `Origin` header yourself against your own allowlist. -SameSite cookie protection may not be 100% reliable, so it's advisable to also enable token protection: - -```php -$form->addProtection(); -``` +The protection relies on the browser's `Sec-Fetch-Site` header (Fetch Metadata), which the browser sends automatically and which cannot be spoofed even with an XSS vulnerability. Older browsers that don't send these headers won't pass the check. The article [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] describes it in detail. -We strongly recommend applying this protection to forms in the administrative part of your application that modify sensitive data. The framework defends against CSRF attacks by generating and validating an authorization token stored in the session. Therefore, it is necessary to have a session started before displaying the form. In the administrative part of the website, the session is usually already started due to user login. Otherwise, start the session using the `Nette\Http\Session::start()` method. +.[note] +The earlier protection using an authorization token stored in the session, activated by `$form->addProtection()`, is no longer needed and is deprecated since version 3.3. So, we've had a quick introduction to forms in Nette. Try looking in the [examples |https://github.com/nette/forms/tree/master/examples] directory in the distribution for more inspiration. diff --git a/forms/en/upgrading.texy b/forms/en/upgrading.texy new file mode 100644 index 0000000000..160581e327 --- /dev/null +++ b/forms/en/upgrading.texy @@ -0,0 +1,54 @@ +Upgrading +********* + + +Upgrading to Version 3.3 +======================== + +- the automatic CSRF protection switched from `isSameSite()` to `isFrom(FetchSite::SameOrigin)` and became stricter: requests coming from subdomains no longer pass +- thanks to that, `addProtection()` is no longer needed, because the automatic protection covers the same cases; omit it in new forms and feel free to remove it from the existing ones + +Why tokens in the session are no longer necessary is explained in the article [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Upgrading to Version 3.1 +======================== + +- `getValues()` returns only the validated controls; if you need the values of all controls regardless of validation, use the new method `getUntrustedValues()` +- the `$values` passed to the `onSuccess` and `onClick` handlers likewise contain only the validated controls +- standalone forms are automatically protected against CSRF by a cookie with the SameSite flag; you can allow submission from another origin using `allowCrossOrigin()` +- the `Form::URL` rule now completes a missing protocol with `https` instead of `http` +- `Form::addImage()` was renamed to `addImageButton()` +- `Checkbox::getSeparatorPrototype()` was renamed to `getContainerPrototype()` +- forms no longer create the `$_form` variable in templates + +More about these changes in the article [News in Nette Forms 3.1 |https://blog.nette.org/en/news-in-nette-forms-3-1]. + + +Upgrading to Version 3.0 +======================== + +- all form controls are optional by default now (this change was introduced in Nette 2.4), so you can remove `setRequired(false)` +- be sure to update `netteForms.js` to version 3 (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` and `MultiChoiceControl::$checkAllowedValues` have been replaced by the method `checkDefaultValue()` + + +Upgrading to Version 2.4 +======================== + +- if a control has a rule via `addRule()` (i.e. it is effectively mandatory), you must also mark it as mandatory via `setRequired()`; also, `setRequired(false)` now makes the control optional, which replaces `addCondition($form::FILLED)` branches +- the validators `Form::EMAIL`, `URL` and `INTEGER` automatically change the HTML `type` attribute to `email`, `url`, and `number` respectively +- negative validation rules are deprecated; the alternative for `~Form::FILLED` is `Form::BLANK`, and `~Form::EQUAL` can be replaced with `Form::NOT_EQUAL` +- the internal parameter `do` is now sent via POST as `_do` to avoid a collision +- the internal underscored variables such as `$_form` are deprecated +- remember to update `netteForms.js` + + +Upgrading to Version 2.3 +======================== + +- internal filtering methods such as `Nette\Forms\Controls\TextBase::filterFloat` were removed +- internal validation methods such as `TextBase::validateFloat` were moved to `Nette\Forms\Validator`, as was `Rules::$defaultMessages` +- Buttons and Hidden fields are generated without an HTML ID; if you want an ID, set it via `setHtmlId()` +- RadioList items are generated without an ID too; you can enable it via `$radioList->generateId = true` +- filters added via `TextBase::addFilter()` are processed during validation, and you can now add filters to conditions: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/en/validation.texy b/forms/en/validation.texy index 0c32b3bb85..98de0cf807 100644 --- a/forms/en/validation.texy +++ b/forms/en/validation.texy @@ -36,7 +36,7 @@ Nette comes with several predefined rules whose names are constants of the `Nett | `NotEqual` | value must not be equal to the parameter | `mixed` | `IsIn` | value must be one of the items in the array | `array` | `IsNotIn` | value must not be any of the items in the array | `array` -| `Valid` | is the control filled correctly? (for [#Conditions]) | - +| `Valid` | is the control filled correctly? (only in [addConditionOn() |#Conditions]) | - Text inputs @@ -52,13 +52,13 @@ For controls `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addIn | `Pattern` | matches regular expression | `string` | `PatternInsensitive` | like `Pattern`, but case-insensitive | `string` | `Integer` | integer value | - -| `Numeric` | alias for `Integer` | - +| `Numeric` | non-negative integer (digits only) | - | `Float` | number | - | `Min` | minimum value of a numeric control | `int\|float` | `Max` | maximum value of a numeric control | `int\|float` | `Range` | value in range | pair `[int\|float, int\|float]` -The validation rules `Integer`, `Numeric`, and `Float` automatically convert the value to an integer or float, respectively. Furthermore, the `URL` rule also accepts an address without a scheme (e.g., `nette.org`) and completes the scheme (`https://nette.org`). The expression in `Pattern` and `PatternInsensitive` must be valid for the entire value, i.e., as if it were wrapped in `^` and `$` characters. +The validation rules `Integer` and `Float` automatically convert the value to an integer or float, respectively. Furthermore, the `URL` rule also accepts an address without a scheme (e.g., `nette.org`) and completes the scheme (`https://nette.org`). The expression in `Pattern` and `PatternInsensitive` must be valid for the entire value, i.e., as if it were wrapped in `^` and `$` characters. Number of Items @@ -151,6 +151,14 @@ $form->addText(/* ... */) ->addRule(/* ... */); ``` +The first argument of `addCondition()` can also be a boolean value. This is useful when the decision is already known while the form is being built - for example, to apply a rule only under certain circumstances: + +```php +$form->addText('nickname') + ->addCondition($isRequired) // a value known when building the form + ->setRequired(); +``` + In Nette, it is very easy to react to the fulfillment or non-fulfillment of a condition on the JavaScript side using the `toggle()` method, see [#Dynamic JavaScript]. @@ -223,11 +231,11 @@ protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('This combination is not possible.'); @@ -320,6 +328,12 @@ import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +You can turn client-side validation off entirely by adding the `novalidate` attribute to the form. The `netteForms.js` script then skips validating it on submit, so validation happens only on the server: + +```php +$form->setHtmlAttribute('novalidate'); +``` + Dynamic JavaScript ================== @@ -334,7 +348,7 @@ $form->addCheckbox('send_it') This code states that when the condition is met (i.e., when the checkbox is checked), the HTML element `#address-container` will be visible, and vice versa. So, we place the form controls with the recipient's address in a container with this ID, and they will hide or show when the checkbox is clicked. This is handled by the `netteForms.js` script. -Any selector can be passed as an argument to the `toggle()` method. For historical reasons, an alphanumeric string without other special characters is treated as an element ID, just as if it were preceded by the `#` character. The second optional parameter allows reversing the behavior; for instance, if we used `toggle('#address-container', false)`, the element would be displayed only if the checkbox was *not* checked. +Any selector can be passed as an argument to the `toggle()` method. For historical reasons, a string that begins with a letter, digit, or underscore and contains only letters, digits, underscores, hyphens, dots, and colons is treated as an element ID, just as if it were preceded by the `#` character. The second optional parameter allows reversing the behavior; for instance, if we used `toggle('#address-container', false)`, the element would be displayed only if the checkbox was *not* checked. The default JavaScript implementation changes the `hidden` property of the elements. However, we can easily change the behavior, for example, by adding an animation. Just override the `Nette.toggle` method in JavaScript with a custom solution: @@ -374,3 +388,5 @@ $form->addSubmit('send5') ``` `setValidationScope` does not affect the [#Event onValidate] on the form, which will always be called. The `onValidate` event on a container will only be triggered if that container is marked for partial validation. + +Partial validation also affects the values returned by `getValues()`: the result contains only the values of controls that fall within the validation scope. Values of controls outside this scope are omitted. diff --git a/forms/es/@home.texy b/forms/es/@home.texy index 1d23b3b784..19dae724db 100644 --- a/forms/es/@home.texy +++ b/forms/es/@home.texy @@ -3,29 +3,29 @@ Nette Forms <div class=perex> -Nette Forms revolucionó la creación de formularios web. De repente, bastaba con escribir unas pocas líneas de código comprensibles para tener un formulario completo, incluyendo la representación, validación JavaScript y del lado del servidor, y además con una seguridad de primer nivel. Mostraremos cómo +Nette Forms revolucionó la creación de formularios web. De pronto bastaba con escribir unas pocas líneas de código claras para tener un formulario completo, incluidos el renderizado, la validación en JavaScript y en el servidor y, además, una seguridad de primer nivel. Le mostraremos cómo: -- crear formularios amigables +- crear formularios cómodos para el usuario - validar los datos enviados -- representar elementos exactamente según sea necesario +- renderizar los elementos exactamente como haga falta </div> -Usando Nette Forms, evitará una serie de tareas rutinarias, como escribir validaciones (además, dobles, en el lado del servidor y del cliente), minimizará la probabilidad de errores y agujeros de seguridad. +Con Nette Forms se ahorrará muchas tareas rutinarias, como escribir la lógica de validación (tanto en el servidor como en el cliente), y minimizará la probabilidad de errores y de agujeros de seguridad. -Puede usar formularios como parte de Nette Application (es decir, en presenters), o completamente de forma independiente. Dado que el uso difiere ligeramente en ambos casos, hemos preparado dos tutoriales para usted: +Puede usar los formularios como parte de Nette Application (es decir, en los presenters) o de forma completamente independiente. Como el uso difiere ligeramente en ambos casos, le hemos preparado dos guías separadas: <div class="wiki-buttons"> <div> "Formularios en presenters .[wiki-button]":in-presenter </div> -<div> "Formularios de forma independiente .[wiki-button]":standalone </div> +<div> "Formularios independientes .[wiki-button]":standalone </div> </div> Instalación ----------- -Puede descargar e instalar la librería usando [Composer|best-practices:composer]: +Descargue e instale el paquete con [Composer|best-practices:composer]: ```shell composer require nette/forms diff --git a/forms/es/@left-menu.texy b/forms/es/@left-menu.texy index 697c17770f..b66019efa1 100644 --- a/forms/es/@left-menu.texy +++ b/forms/es/@left-menu.texy @@ -2,13 +2,15 @@ Nette Forms *********** - [Introducción |@home] - [Formularios en presenters|in-presenter] -- [Formularios de forma independiente|standalone] +- [Formularios independientes|standalone] - [Elementos de formulario |controls] - [Validación |validation] -- [Representación |rendering] -- [Configuración |configuration] +- [Renderizado |rendering] +- [Elementos personalizados |custom-controls] +- [Configuración|configuration] +- [Actualización|upgrading] -Lectura adicional -***************** -- [Tutoriales y procedimientos |best-practices:] +Lecturas adicionales +******************** +- [Buenas prácticas |best-practices:] diff --git a/forms/es/@meta.texy b/forms/es/@meta.texy index 1670b124ad..3798d9cda4 100644 --- a/forms/es/@meta.texy +++ b/forms/es/@meta.texy @@ -1 +1 @@ -{{sitename: Nette Documentación}} +{{sitename: Documentación de Nette}} diff --git a/forms/es/configuration.texy b/forms/es/configuration.texy index 01d4c852b7..943c0572d7 100644 --- a/forms/es/configuration.texy +++ b/forms/es/configuration.texy @@ -2,7 +2,7 @@ Configuración de formularios **************************** .[perex] -En la configuración se pueden cambiar los [mensajes de error predeterminados de los formularios|validation]. +En la configuración puede cambiar los [mensajes de error predeterminados de los formularios|validation]. ```neon forms: @@ -17,6 +17,7 @@ forms: 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.' @@ -30,32 +31,33 @@ forms: Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' ``` -Aquí está la traducción al español: +Aquí tiene la traducción al español: ```neon forms: messages: - Equal: 'Por favor, introduzca %s.' - NotEqual: 'Este valor no debe ser %s.' + Equal: 'Introduzca %s.' + NotEqual: 'Este valor no debería ser %s.' Filled: 'Este campo es obligatorio.' - Blank: 'Este campo debe estar vacío.' - MinLength: 'Por favor, introduzca al menos %d caracteres.' - MaxLength: 'Por favor, introduzca no más de %d caracteres.' - Length: 'Por favor, introduzca un valor de entre %d y %d caracteres.' - Email: 'Por favor, introduzca una dirección de correo electrónico válida.' - URL: 'Por favor, introduzca una URL válida.' - Integer: 'Por favor, introduzca un número entero válido.' - Float: 'Por favor, introduzca un número válido.' - Min: 'Por favor, introduzca un valor mayor o igual a %d.' - Max: 'Por favor, introduzca un valor menor o igual a %d.' - Range: 'Por favor, introduzca un valor entre %d y %d.' + Blank: 'Este campo debería estar vacío.' + MinLength: 'Introduzca al menos %d caracteres.' + MaxLength: 'Introduzca como máximo %d caracteres.' + Length: 'Introduzca un valor de entre %d y %d caracteres.' + Email: 'Introduzca una dirección de correo válida.' + URL: 'Introduzca una URL válida.' + Integer: 'Introduzca un número entero válido.' + Numeric: 'Introduzca un número entero no negativo.' + Float: 'Introduzca un número válido.' + Min: 'Introduzca un valor mayor o igual que %d.' + Max: 'Introduzca un valor menor o igual que %d.' + Range: 'Introduzca un valor entre %d y %d.' MaxFileSize: 'El tamaño del archivo subido puede ser como máximo de %d bytes.' - MaxPostSize: 'Los datos subidos exceden el límite de %d bytes.' - MimeType: 'El archivo subido no está en el formato esperado.' - Image: 'El archivo subido debe ser una imagen en formato JPEG, GIF, PNG, WebP o AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Por favor, seleccione una opción válida.' - Nette\Forms\Controls\UploadControl::Valid: 'Ocurrió un error durante la subida del archivo.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Su sesión ha expirado. Por favor, regrese a la página de inicio e inténtelo de nuevo.' + MaxPostSize: 'Los datos subidos superan el límite de %d bytes.' + MimeType: 'El archivo subido no tiene el formato esperado.' + Image: 'El archivo subido debe ser una imagen en formato JPEG, GIF, PNG o WebP.' + Nette\Forms\Controls\SelectBox::Valid: 'Seleccione una opción válida.' + Nette\Forms\Controls\UploadControl::Valid: 'Se produjo un error al subir el archivo.' + Nette\Forms\Controls\CsrfProtection::Protection: 'Su sesión ha caducado. Vuelva a la página de inicio e inténtelo de nuevo.' ``` -Si no utiliza todo el framework y, por lo tanto, tampoco los archivos de configuración, puede cambiar los mensajes de error predeterminados directamente en el array `Nette\Forms\Validator::$messages`. +Si no usa todo el framework y por tanto tampoco los archivos de configuración, puede cambiar los mensajes de error predeterminados directamente en el array `Nette\Forms\Validator::$messages`. diff --git a/forms/es/controls.texy b/forms/es/controls.texy index 1f7aa3bac5..74da68b105 100644 --- a/forms/es/controls.texy +++ b/forms/es/controls.texy @@ -2,325 +2,326 @@ Elementos de formulario *********************** .[perex] -Resumen de los elementos de formulario estándar. +Resumen de los elementos estándar de los formularios. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -Agrega un campo de texto de una línea (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Si el usuario no rellena el campo, devuelve una cadena vacía `''`, o mediante `setNullable()` se puede especificar que devuelva `null`. +Añade un campo de texto de una sola línea (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Si el usuario no rellena el campo, devuelve una cadena vacía `''`, o use `setNullable()` para que devuelva `null` en su lugar. ```php -$form->addText('name', 'Nombre:') +$form->addText('name', 'Name:') ->setRequired() ->setNullable(); ``` -Valida automáticamente UTF-8, recorta los espacios iniciales y finales y elimina los saltos de línea que un atacante podría enviar. +Valida automáticamente UTF-8, recorta los espacios en blanco iniciales y finales, y elimina los saltos de línea que podría enviar un atacante. -La longitud máxima se puede limitar mediante `setMaxLength()`. Modificar el valor introducido por el usuario permite [addFilter() |validation#Modificación de la entrada]. +La longitud máxima se puede limitar con `setMaxLength()`. El método [addFilter() |validation#Modificar los valores de entrada] permite modificar el valor introducido por el usuario. -Mediante `setHtmlType()` se puede cambiar el carácter visual del campo de texto a tipos como `search`, `tel` o `url` ver [especificación|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Recuerde que el cambio de tipo es solo visual y no reemplaza la función de validación. Para el tipo `url` es conveniente agregar una regla de validación específica [URL |validation#Entradas de texto]. +Con `setHtmlType()` puede cambiar el aspecto visual del campo de texto a tipos como `search`, `tel` o `url`, tal como los define la [especificación|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Recuerde que cambiar el tipo es puramente visual y no sustituye a la funcionalidad de validación. Para el tipo `url` conviene añadir una [regla de validación de URL |validation#Campos de texto] concreta. .[note] -Para otros tipos de entradas, como `number`, `range`, `email`, `date`, `datetime-local`, `time` y `color`, utilice métodos especializados como [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] y [#addColor], que aseguran la validación del lado del servidor. Los tipos `month` y `week` aún no son totalmente compatibles con todos los navegadores. +Para otros tipos de entrada como `number`, `range`, `email`, `date`, `datetime-local`, `time` y `color`, use los métodos especializados [#addInteger()], [#addFloat()], [#addEmail()], [#addDate()], [#addTime()], [#addDateTime()] y [#addColor()], que proporcionan validación en el lado del servidor. Los tipos `month` y `week` todavía no están plenamente soportados por todos los navegadores. -Al elemento se le puede establecer el llamado empty-value, que es algo así como un valor predeterminado, pero si el usuario no lo cambia, el elemento devuelve una cadena vacía o `null`. +Al elemento se le puede asignar un "valor vacío". Funciona algo parecido a un valor predeterminado, pero, si el usuario no lo cambia, el elemento devuelve una cadena vacía o `null`. ```php -$form->addText('phone', 'Teléfono:') +$form->addText('phone', 'Phone:') ->setHtmlType('tel') - ->setEmptyValue('+34'); + ->setEmptyValue('+420'); ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -Agrega un campo para introducir texto multilínea (clase [TextArea |api:Nette\Forms\Controls\TextArea]). Si el usuario no rellena el campo, devuelve una cadena vacía `''`, o mediante `setNullable()` se puede especificar que devuelva `null`. +Añade un campo de texto de varias líneas (clase [TextArea |api:Nette\Forms\Controls\TextArea]). Si el usuario no rellena el campo, devuelve una cadena vacía `''`, o use `setNullable()` para que devuelva `null` en su lugar. ```php -$form->addTextArea('note', 'Nota:') - ->addRule($form::MaxLength, 'La nota es demasiado larga', 10000); +$form->addTextArea('note', 'Note:') + ->addRule($form::MaxLength, 'Your note is way too long', 10000); ``` -Valida automáticamente UTF-8 y normaliza los separadores de línea a `\n`. A diferencia del campo de entrada de una sola línea, no se realiza ningún recorte de espacios. +Valida automáticamente UTF-8 y normaliza los finales de línea a `\n`. A diferencia del campo de una sola línea, aquí no se recortan los espacios en blanco. -La longitud máxima se puede limitar mediante `setMaxLength()`. Modificar el valor introducido por el usuario permite [addFilter() |validation#Modificación de la entrada]. Se puede establecer el llamado empty-value mediante `setEmptyValue()`. +La longitud máxima se puede limitar con `setMaxLength()`. El método [addFilter() |validation#Modificar los valores de entrada] permite modificar el valor introducido por el usuario. Se puede establecer un valor vacío con `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Agrega un campo para introducir un número entero (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Devuelve un integer, o `null` si el usuario no introduce nada. +Añade un campo para introducir un número entero (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Devuelve un entero, o `null` si el usuario no introduce nada. ```php -$form->addInteger('year', 'Año:') - ->addRule($form::Range, 'El año debe estar en el rango de %d a %d.', [1900, 2023]); +$form->addInteger('year', 'Year:') + ->addRule($form::Range, 'The year must be between %d and %d.', [1900, 2023]); ``` -El elemento se renderiza como `<input type="number">`. Usando el método `setHtmlType()` se puede cambiar el tipo a `range` para mostrarlo como un control deslizante, o a `text`, si prefiere un campo de texto estándar sin el comportamiento especial del tipo `number`. +El elemento se renderiza como `<input type="number">`. Con el método `setHtmlType()` puede cambiar el tipo a `range` para mostrarlo como un deslizador, o a `text` si prefiere un campo de texto normal sin el comportamiento especial del tipo `number`. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -Agrega un campo para introducir un número decimal (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Devuelve un float, o `null` si el usuario no introduce nada. +Añade un campo para introducir un número decimal (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Devuelve un float, o `null` si el usuario no introduce nada. ```php -$form->addFloat('level', 'Nivel:') +$form->addFloat('level', 'Level:') ->setDefaultValue(0) - ->addRule($form::Range, 'El nivel debe estar en el rango de %d a %d.', [0, 100]); + ->addRule($form::Range, 'The level must be between %d and %d.', [0, 100]); ``` -El elemento se renderiza como `<input type="number">`. Usando el método `setHtmlType()` se puede cambiar el tipo a `range` para mostrarlo como un control deslizante, o a `text`, si prefiere un campo de texto estándar sin el comportamiento especial del tipo `number`. +El elemento se renderiza como `<input type="number">`. Con el método `setHtmlType()` puede cambiar el tipo a `range` para mostrarlo como un deslizador, o a `text` si prefiere un campo de texto normal sin el comportamiento especial del tipo `number`. -Nette y el navegador Chrome aceptan tanto la coma como el punto como separador decimal. Para que esta funcionalidad esté disponible también en Firefox, se recomienda establecer el atributo `lang` ya sea para el elemento dado o para toda la página, por ejemplo `<html lang="es">`. +Nette y el navegador Chrome aceptan como separador decimal tanto la coma como el punto. Para habilitar esta funcionalidad también en Firefox, se recomienda establecer el atributo `lang`, ya sea en el elemento concreto o en toda la página, por ejemplo `<html lang="es">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -Agrega un campo para introducir una dirección de correo electrónico (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Si el usuario no rellena el campo, devuelve una cadena vacía `''`, o mediante `setNullable()` se puede especificar que devuelva `null`. +Añade un campo para introducir una dirección de correo electrónico (clase [TextInput |api:Nette\Forms\Controls\TextInput]). Si el usuario no rellena el campo, devuelve una cadena vacía `''`, o use `setNullable()` para que devuelva `null` en su lugar. ```php $form->addEmail('email', 'E-mail:'); ``` -Verifica si el valor es una dirección de correo electrónico válida. No se verifica si el dominio realmente existe, solo se verifica la sintaxis. Valida automáticamente UTF-8, recorta los espacios iniciales y finales. +Valida que el valor sea una dirección de correo válida. No comprueba si el dominio existe realmente, solo verifica la sintaxis. Valida automáticamente UTF-8 y recorta los espacios en blanco iniciales y finales. -La longitud máxima se puede limitar mediante `setMaxLength()`. Modificar el valor introducido por el usuario permite [addFilter() |validation#Modificación de la entrada]. Se puede establecer el llamado empty-value mediante `setEmptyValue()`. +La longitud máxima se puede limitar con `setMaxLength()`. El método [addFilter() |validation#Modificar los valores de entrada] permite modificar el valor introducido por el usuario. Se puede establecer un valor vacío con `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== -Agrega un campo para introducir una contraseña (clase [TextInput |api:Nette\Forms\Controls\TextInput]). +Añade un campo para introducir una contraseña (clase [TextInput |api:Nette\Forms\Controls\TextInput]). ```php -$form->addPassword('password', 'Contraseña:') +$form->addPassword('password', 'Password:') ->setRequired() - ->addRule($form::MinLength, 'La contraseña debe tener al menos %d caracteres', 8) - ->addRule($form::Pattern, 'Debe contener un dígito', '.*[0-9].*'); + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8) + ->addRule($form::Pattern, 'Password must contain a number', '.*[0-9].*'); ``` -Al volver a mostrar el formulario, el campo estará vacío. Valida automáticamente UTF-8, recorta los espacios iniciales y finales y elimina los saltos de línea que un atacante podría enviar. +Al volver a mostrar el formulario, el campo estará vacío. Valida automáticamente UTF-8, recorta los espacios en blanco iniciales y finales, y elimina los saltos de línea que podría enviar un atacante. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -Agrega una casilla de verificación (clase [Checkbox |api:Nette\Forms\Controls\Checkbox]). Devuelve el valor `true` o `false`, dependiendo de si está marcada. +Añade una casilla de verificación (clase [Checkbox |api:Nette\Forms\Controls\Checkbox]). Devuelve `true` o `false`, según esté marcada o no. ```php -$form->addCheckbox('agree', 'Acepto los términos y condiciones') - ->setRequired('Es necesario aceptar los términos y condiciones'); +$form->addCheckbox('agree', 'I agree with terms') + ->setRequired('You must agree with our terms'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Agrega casillas de verificación para seleccionar múltiples elementos (clase [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Devuelve un array de claves de los elementos seleccionados. El método `getSelectedItems()` devuelve los valores en lugar de las claves. +Añade una lista de casillas de verificación para seleccionar varios elementos (clase [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Devuelve un array con las claves de los elementos seleccionados. El método `getSelectedItems()` devuelve los elementos seleccionados como pares clave-valor. ```php -$form->addCheckboxList('colors', 'Colores:', [ - 'r' => 'rojo', - 'g' => 'verde', - 'b' => 'azul', +$form->addCheckboxList('colors', 'Colors:', [ + 'r' => 'red', + 'g' => 'green', + 'b' => 'blue', ]); ``` -El array de elementos ofrecidos se pasa como tercer parámetro o mediante el método `setItems()`. +El array de elementos ofrecidos se pasa como tercer parámetro o mediante el método `setItems()`. Si pasa `false` como segundo argumento de `setItems()`, los valores se usan también como claves. -Mediante `setDisabled(['r', 'g'])` se pueden desactivar elementos individuales. +Use `setDisabled(['r', 'g'])` para deshabilitar elementos concretos. -El elemento comprueba automáticamente que no haya habido falsificación y que los elementos seleccionados sean realmente uno de los ofrecidos y no hayan sido desactivados. Mediante el método `getRawValue()` se pueden obtener los elementos enviados sin esta importante comprobación. +El elemento comprueba automáticamente que no se haya producido ninguna falsificación y que los elementos seleccionados estén realmente entre los ofrecidos y no estuvieran deshabilitados. El método `getRawValue()` permite obtener los elementos enviados sin esta importante comprobación. -Al establecer los elementos seleccionados por defecto, también comprueba que sean uno de los ofrecidos, de lo contrario lanza una excepción. Esta comprobación se puede desactivar mediante `checkDefaultValue(false)`. +Al establecer los elementos seleccionados de forma predeterminada también comprueba que estén entre los ofrecidos; de lo contrario lanza una excepción. Esta comprobación se puede desactivar con `checkDefaultValue(false)`. -Si envía el formulario mediante el método `GET`, puede elegir un método de transmisión de datos más compacto que ahorra tamaño de la cadena de consulta. Se activa estableciendo el atributo HTML del formulario: +Si envía el formulario con el método `GET`, puede elegir una forma más compacta de transferir los datos que ahorra tamaño en la cadena de consulta. Se activa estableciendo un atributo HTML en el formulario: ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ -Agrega botones de opción (clase [RadioList |api:Nette\Forms\Controls\RadioList]). Devuelve la clave del elemento seleccionado, o `null` si el usuario no seleccionó nada. El método `getSelectedItem()` devuelve el valor en lugar de la clave. +Añade botones de opción (clase [RadioList |api:Nette\Forms\Controls\RadioList]). Devuelve la clave del elemento seleccionado, o `null` si el usuario no seleccionó nada. El método `getSelectedItem()` devuelve el valor en lugar de la clave. ```php $sex = [ - 'm' => 'hombre', - 'f' => 'mujer', + 'm' => 'male', + 'f' => 'female', + 'o' => 'other', ]; -$form->addRadioList('gender', 'Género:', $sex); +$form->addRadioList('gender', 'Gender:', $sex); ``` El array de elementos ofrecidos se pasa como tercer parámetro o mediante el método `setItems()`. -Mediante `setDisabled(['m', 'f'])` se pueden desactivar elementos individuales. +Use `setDisabled(['m'])` para deshabilitar elementos concretos. -El elemento comprueba automáticamente que no haya habido falsificación y que el elemento seleccionado sea realmente uno de los ofrecidos y no haya sido desactivado. Mediante el método `getRawValue()` se puede obtener el elemento enviado sin esta importante comprobación. +El elemento comprueba automáticamente que no se haya producido ninguna falsificación y que el elemento seleccionado esté realmente entre los ofrecidos y no estuviera deshabilitado. El método `getRawValue()` permite obtener el elemento enviado sin esta importante comprobación. -Al establecer el elemento seleccionado por defecto, también comprueba que sea uno de los ofrecidos, de lo contrario lanza una excepción. Esta comprobación se puede desactivar mediante `checkDefaultValue(false)`. +Al establecer el elemento seleccionado de forma predeterminada también comprueba que esté entre los ofrecidos; de lo contrario lanza una excepción. Esta comprobación se puede desactivar con `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -Agrega un cuadro de selección (clase [SelectBox |api:Nette\Forms\Controls\SelectBox]). Devuelve la clave del elemento seleccionado, o `null` si el usuario no seleccionó nada. El método `getSelectedItem()` devuelve el valor en lugar de la clave. +Añade una lista desplegable (clase [SelectBox |api:Nette\Forms\Controls\SelectBox]). Devuelve la clave del elemento seleccionado, o `null` si el usuario no seleccionó nada. El método `getSelectedItem()` devuelve el valor en lugar de la clave. ```php $countries = [ - 'ES' => 'España', - 'MX' => 'México', - 'AR' => 'Argentina', + 'CZ' => 'Czech Republic', + 'SK' => 'Slovakia', + 'GB' => 'United Kingdom', ]; -$form->addSelect('country', 'País:', $countries) - ->setDefaultValue('MX'); +$form->addSelect('country', 'Country:', $countries) + ->setDefaultValue('SK'); ``` -El array de elementos ofrecidos se pasa como tercer parámetro o mediante el método `setItems()`. Los elementos también pueden ser un array bidimensional: +El array de elementos ofrecidos se pasa como tercer parámetro o mediante el método `setItems()`. Los elementos también pueden ser un array bidimensional (que representa optgroups): ```php $countries = [ - 'Europa' => [ - 'ES' => 'España', - 'FR' => 'Francia', - 'DE' => 'Alemania', + 'Europe' => [ + 'CZ' => 'Czech Republic', + 'SK' => 'Slovakia', + 'GB' => 'United Kingdom', ], - 'CA' => 'Canadá', - 'MX' => 'México', - '?' => 'otro', + 'CA' => 'Canada', + 'US' => 'USA', + '?' => 'other', ]; ``` -En los cuadros de selección, a menudo el primer elemento tiene un significado especial, sirve como llamada a la acción. Para agregar tal elemento, se utiliza el método `setPrompt()`. +En las listas desplegables, el primer elemento suele tener un significado especial y sirve de invitación a la acción. Use el método `setPrompt()` para añadir un elemento así. ```php -$form->addSelect('country', 'País:', $countries) - ->setPrompt('Seleccione un país'); +$form->addSelect('country', 'Country:', $countries) + ->setPrompt('Choose a country'); ``` -Mediante `setDisabled(['ES', 'FR'])` se pueden desactivar elementos individuales. +Use `setDisabled(['CZ', 'SK'])` para deshabilitar elementos concretos. -El elemento comprueba automáticamente que no haya habido falsificación y que el elemento seleccionado sea realmente uno de los ofrecidos y no haya sido desactivado. Mediante el método `getRawValue()` se puede obtener el elemento enviado sin esta importante comprobación. +El elemento comprueba automáticamente que no se haya producido ninguna falsificación y que el elemento seleccionado esté realmente entre los ofrecidos y no estuviera deshabilitado. El método `getRawValue()` permite obtener el elemento enviado sin esta importante comprobación. -Al establecer el elemento seleccionado por defecto, también comprueba que sea uno de los ofrecidos, de lo contrario lanza una excepción. Esta comprobación se puede desactivar mediante `checkDefaultValue(false)`. +Al establecer el elemento seleccionado de forma predeterminada también comprueba que esté entre los ofrecidos; de lo contrario lanza una excepción. Esta comprobación se puede desactivar con `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Agrega un cuadro de selección para elegir múltiples elementos (clase [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Devuelve un array de claves de los elementos seleccionados. El método `getSelectedItems()` devuelve los valores en lugar de las claves. +Añade una lista desplegable para seleccionar varios elementos (clase [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Devuelve un array con las claves de los elementos seleccionados. El método `getSelectedItems()` devuelve los elementos seleccionados como pares clave-valor. ```php -$form->addMultiSelect('countries', 'Países:', $countries); +$form->addMultiSelect('countries', 'Countries:', $countries); ``` El array de elementos ofrecidos se pasa como tercer parámetro o mediante el método `setItems()`. Los elementos también pueden ser un array bidimensional. -Mediante `setDisabled(['ES', 'FR'])` se pueden desactivar elementos individuales. +Use `setDisabled(['CZ', 'SK'])` para deshabilitar elementos concretos. -El elemento comprueba automáticamente que no haya habido falsificación y que los elementos seleccionados sean realmente uno de los ofrecidos y no hayan sido desactivados. Mediante el método `getRawValue()` se pueden obtener los elementos enviados sin esta importante comprobación. +El elemento comprueba automáticamente que no se haya producido ninguna falsificación y que los elementos seleccionados estén realmente entre los ofrecidos y no estuvieran deshabilitados. El método `getRawValue()` permite obtener los elementos enviados sin esta importante comprobación. -Al establecer los elementos seleccionados por defecto, también comprueba que sean uno de los ofrecidos, de lo contrario lanza una excepción. Esta comprobación se puede desactivar mediante `checkDefaultValue(false)`. +Al establecer los elementos seleccionados de forma predeterminada también comprueba que estén entre los ofrecidos; de lo contrario lanza una excepción. Esta comprobación se puede desactivar con `checkDefaultValue(false)`. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Agrega un campo para subir un archivo (clase [UploadControl |api:Nette\Forms\Controls\UploadControl]). Devuelve un objeto [FileUpload |http:request#FileUpload] incluso si el usuario no envió ningún archivo, lo cual se puede verificar con el método `FileUpload::hasFile()`. +Añade un campo para subir un archivo (clase [UploadControl |api:Nette\Forms\Controls\UploadControl]). Devuelve un objeto [FileUpload |http:request#FileUpload] incluso si el usuario no subió ningún archivo, lo que se puede comprobar con el método `FileUpload::hasFile()`. Con `setNullable()` puede hacer que el elemento devuelva `null` en lugar de un objeto `FileUpload` cuando no se sube ningún archivo. ```php $form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'El avatar debe ser JPEG, PNG, GIF, WebP o AVIF.') - ->addRule($form::MaxFileSize, 'El tamaño máximo es 1 MB.', 1024 * 1024); + ->addRule($form::Image, 'Avatar must be JPEG, PNG, GIF, WebP or AVIF.') + ->addRule($form::MaxFileSize, 'Maximum size is 1 MB.', 1024 * 1024); ``` -Si el archivo no se sube correctamente, el formulario no se envía con éxito y se muestra un error. Es decir, en caso de envío exitoso, no es necesario verificar el método `FileUpload::isOk()`. +Si el archivo no se sube correctamente, el formulario no se envía con éxito y se muestra un error. Es decir, tras un envío correcto no hace falta comprobar el método `FileUpload::isOk()`. -Nunca confíe en el nombre original del archivo devuelto por el método `FileUpload::getName()`, el cliente podría haber enviado un nombre de archivo malicioso con la intención de dañar o hackear su aplicación. +Nunca confíe en el nombre original del archivo que devuelve el método `FileUpload::getName()`; el cliente pudo haber enviado un nombre de archivo malicioso con la intención de dañar o hackear su aplicación. -Las reglas `MimeType` e `Image` detectan el tipo requerido basándose en la firma del archivo y no verifican su integridad. Si la imagen está dañada se puede verificar, por ejemplo, intentando [cargarla |http:request#toImage]. +Las reglas `MimeType` e `Image` detectan el tipo requerido a partir de la firma del archivo y no verifican su integridad. Que una imagen esté dañada se puede averiguar, por ejemplo, intentando [cargarla |http:request#toImage()]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Agrega un campo para subir múltiples archivos a la vez (clase [UploadControl |api:Nette\Forms\Controls\UploadControl]). Devuelve un array de objetos [FileUpload |http:request#FileUpload]. El método `FileUpload::hasFile()` en cada uno de ellos devolverá `true`. +Añade un campo para subir varios archivos a la vez (clase [UploadControl |api:Nette\Forms\Controls\UploadControl]). Devuelve un array de objetos [FileUpload |http:request#FileUpload]. El método `FileUpload::hasFile()` devolverá `true` para cada uno de ellos. ```php -$form->addMultiUpload('files', 'Archivos:') - ->addRule($form::MaxLength, 'Se pueden subir como máximo %d archivos', 10); +$form->addMultiUpload('files', 'Files:') + ->addRule($form::MaxLength, 'Maximum of %d files can be uploaded.', 10); ``` -Si alguno de los archivos no se sube correctamente, el formulario no se envía con éxito y se muestra un error. Es decir, en caso de envío exitoso, no es necesario verificar el método `FileUpload::isOk()`. +Si alguno de los archivos no se sube correctamente, el formulario no se envía con éxito y se muestra un error. Es decir, tras un envío correcto no hace falta comprobar el método `FileUpload::isOk()` para cada archivo. -Nunca confíe en los nombres originales de los archivos devueltos por el método `FileUpload::getName()`, el cliente podría haber enviado un nombre de archivo malicioso con la intención de dañar o hackear su aplicación. +Nunca confíe en los nombres originales de los archivos que devuelve el método `FileUpload::getName()`; el cliente pudo haber enviado nombres de archivo maliciosos con la intención de dañar o hackear su aplicación. -Las reglas `MimeType` e `Image` detectan el tipo requerido basándose en la firma del archivo y no verifican su integridad. Si la imagen está dañada se puede verificar, por ejemplo, intentando [cargarla |http:request#toImage]. +Las reglas `MimeType` e `Image` detectan el tipo requerido a partir de la firma del archivo y no verifican su integridad. Que una imagen esté dañada se puede averiguar, por ejemplo, intentando [cargarla |http:request#toImage()]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== -Agrega un campo que permite al usuario introducir fácilmente una fecha compuesta por año, mes y día (clase [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Añade un campo que permite al usuario introducir cómodamente una fecha compuesta por año, mes y día (clase [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Como valor predeterminado, acepta objetos que implementan la interfaz `DateTimeInterface`, una cadena con la hora o un número que representa un timestamp UNIX. Lo mismo se aplica a los argumentos de las reglas `Min`, `Max` o `Range`, que definen la fecha mínima y máxima permitida. +Como valor predeterminado acepta objetos que implementan `DateTimeInterface`, una cadena con la hora o un número que representa una marca de tiempo UNIX. Lo mismo vale para los argumentos de las reglas `Min`, `Max` o `Range`, que definen la fecha mínima y máxima permitidas. ```php -$form->addDate('date', 'Fecha:') +$form->addDate('date', 'Date:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'La fecha debe tener al menos un mes de antigüedad.', new DateTime('-1 month')); + ->addRule($form::Min, 'The date must be at least one month old.', new DateTime('-1 month')); ``` -Por defecto, devuelve un objeto `DateTimeImmutable`, con el método `setFormat()` puede especificar el [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] o timestamp: +De forma predeterminada devuelve un objeto `DateTimeImmutable`. Con el método `setFormat()` puede indicar un [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] o una marca de tiempo: ```php -$form->addDate('date', 'Fecha:') +$form->addDate('date', 'Date:') ->setFormat('Y-m-d'); ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -Agrega un campo que permite al usuario introducir fácilmente una hora compuesta por horas, minutos y opcionalmente segundos (clase [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Añade un campo que permite al usuario introducir cómodamente una hora compuesta por horas, minutos y, opcionalmente, segundos (clase [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Como valor predeterminado, acepta objetos que implementan la interfaz `DateTimeInterface`, una cadena con la hora o un número que representa un timestamp UNIX. De estas entradas, solo se utiliza la información de la hora, la fecha se ignora. Lo mismo se aplica a los argumentos de las reglas `Min`, `Max` o `Range`, que definen la hora mínima y máxima permitida. Si el valor mínimo establecido es mayor que el máximo, se crea un rango de tiempo que cruza la medianoche. +Como valor predeterminado acepta objetos que implementan `DateTimeInterface`, una cadena con la hora o un número que representa una marca de tiempo UNIX. De estas entradas solo se usa la información de la hora; la fecha se ignora. Lo mismo vale para los argumentos de las reglas `Min`, `Max` o `Range`, que definen la hora mínima y máxima permitidas. Si el valor mínimo establecido es mayor que el máximo, se crea un rango horario que cruza la medianoche. ```php -$form->addTime('time', 'Hora:', withSeconds: true) - ->addRule($form::Range, 'La hora debe estar en el rango de %d a %d.', ['12:30', '13:30']); +$form->addTime('time', 'Time:', withSeconds: true) + ->addRule($form::Range, 'Time must be between %d and %d.', ['12:30', '13:30']); ``` -Por defecto, devuelve un objeto `DateTimeImmutable` (con la fecha 1 de enero del año 1), con el método `setFormat()` puede especificar el [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: +De forma predeterminada devuelve un objeto `DateTimeImmutable` (con la fecha fijada al 1 de enero del año 1). Con el método `setFormat()` puede indicar un [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: ```php -$form->addTime('time', 'Hora:') +$form->addTime('time', 'Time:') ->setFormat('H:i'); ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -Agrega un campo que permite al usuario introducir fácilmente una fecha y hora compuesta por año, mes, día, horas, minutos y opcionalmente segundos (clase [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Añade un campo que permite al usuario introducir cómodamente la fecha y la hora a la vez, compuestas por año, mes, día, horas, minutos y, opcionalmente, segundos (clase [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Como valor predeterminado, acepta objetos que implementan la interfaz `DateTimeInterface`, una cadena con la hora o un número que representa un timestamp UNIX. Lo mismo se aplica a los argumentos de las reglas `Min`, `Max` o `Range`, que definen la fecha mínima y máxima permitida. +Como valor predeterminado acepta objetos que implementan `DateTimeInterface`, una cadena con la hora o un número que representa una marca de tiempo UNIX. Lo mismo vale para los argumentos de las reglas `Min`, `Max` o `Range`, que definen la fecha y la hora mínimas y máximas permitidas. ```php -$form->addDateTime('datetime', 'Fecha y hora:') +$form->addDateTime('datetime', 'Date and Time:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'La fecha debe tener al menos un mes de antigüedad.', new DateTime('-1 month')); + ->addRule($form::Min, 'The date must be at least one month old.', new DateTime('-1 month')); ``` -Por defecto, devuelve un objeto `DateTimeImmutable`, con el método `setFormat()` puede especificar el [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] o timestamp: +De forma predeterminada devuelve un objeto `DateTimeImmutable`. Con el método `setFormat()` puede indicar un [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] o una marca de tiempo: ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -Agrega un campo para seleccionar un color (clase [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). El color es una cadena en formato `#rrggbb`. Si el usuario no realiza la selección, se devuelve el color negro `#000000`. +Añade un campo para elegir un color (clase [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). El color se devuelve como una cadena con el formato `#rrggbb`. Si el usuario no elige nada, devuelve el negro `#000000`. ```php $form->addColor('color', 'Color:') @@ -339,37 +340,46 @@ $form->addColor('color', 'Color:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== -Agrega un campo oculto (clase [HiddenField |api:Nette\Forms\Controls\HiddenField]). +Añade un campo oculto (clase [HiddenField |api:Nette\Forms\Controls\HiddenField]). ```php $form->addHidden('userid'); ``` -Mediante `setNullable()` se puede establecer que devuelva `null` en lugar de una cadena vacía. Modificar el valor enviado permite [addFilter() |validation#Modificación de la entrada]. +Use `setNullable()` para que devuelva `null` en lugar de una cadena vacía. El método [addFilter() |validation#Modificar los valores de entrada] permite modificar el valor enviado. -Aunque el elemento está oculto, es **importante tener en cuenta** que el valor aún puede ser modificado o falsificado por un atacante. Siempre verifique y valide minuciosamente todos los valores recibidos en el lado del servidor para prevenir riesgos de seguridad asociados con la manipulación de datos. +Aunque el elemento esté oculto, **es importante darse cuenta** de que un atacante puede modificar o falsificar su valor. Verifique y valide siempre a fondo todos los valores recibidos en el lado del servidor para evitar los riesgos de seguridad asociados a la manipulación de datos. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== -Agrega un botón de envío (clase [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). +Añade un botón de envío (clase [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). ```php -$form->addSubmit('submit', 'Enviar'); +$form->addSubmit('submit', 'Submit'); ``` -En el formulario es posible tener múltiples botones de envío: +.{data-version:3.3.0} +El manejador se puede pasar directamente al botón como tercer parámetro `$onSubmit` en lugar de engancharlo al evento `onClick`: ```php -$form->addSubmit('register', 'Registrar'); -$form->addSubmit('cancel', 'Cancelar'); +$form->addSubmit('submit', 'Submit', function (SubmitButton $button, $data): void { + // ... +}); ``` -Para determinar en cuál de ellos se hizo clic, use: +En un formulario puede haber más de un botón de envío: + +```php +$form->addSubmit('register', 'Register'); +$form->addSubmit('cancel', 'Cancel'); +``` + +Para averiguar cuál se pulsó, use: ```php if ($form['register']->isSubmittedBy()) { @@ -377,48 +387,48 @@ if ($form['register']->isSubmittedBy()) { } ``` -Si no desea validar todo el formulario al presionar un botón (por ejemplo, en los botones *Cancelar* o *Vista previa*), use [setValidationScope() |validation#Desactivación de la validación]. +Si no quiere validar todo el formulario al pulsar un botón (por ejemplo, en los botones *Cancelar* o *Vista previa*), use [setValidationScope() |validation#Desactivar la validación]. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -Agrega un botón (clase [Button |api:Nette\Forms\Controls\Button]) que no tiene función de envío. Por lo tanto, se puede usar para alguna otra función, por ejemplo, llamar a una función JavaScript al hacer clic. +Añade un botón (clase [Button |api:Nette\Forms\Controls\Button]) que no tiene función de envío. Por tanto se puede usar para otras funciones, p. ej. para llamar a una función de JavaScript al pulsarlo. ```php -$form->addButton('raise', 'Aumentar salario') +$form->addButton('raise', 'Raise salary') ->setHtmlAttribute('onclick', 'raiseSalary()'); ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= -Agrega un botón de envío en forma de imagen (clase [ImageButton |api:Nette\Forms\Controls\ImageButton]). +Añade un botón de envío en forma de imagen (clase [ImageButton |api:Nette\Forms\Controls\ImageButton]). ```php -$form->addImageButton('submit', '/ruta/a/imagen.png'); +$form->addImageButton('submit', '/path/to/image.png', 'Submit'); ``` -Al usar múltiples botones de envío, se puede determinar en cuál se hizo clic mediante `$form['submit']->isSubmittedBy()`. +Cuando use varios botones de envío, puede averiguar cuál se pulsó con `$form['submit']->isSubmittedBy()`. addContainer(string|int $name): Container .[method] =================================================== -Agrega un subformulario (clase [Container|api:Nette\Forms\Container]), o contenedor, al que se pueden agregar otros elementos de la misma manera que los agregamos al formulario. También funcionan los métodos `setDefaults()` o `getValues()`. +Añade un subformulario (clase [Container|api:Nette\Forms\Container]), o contenedor, al que se pueden añadir otros elementos igual que se añaden al formulario. También funcionan métodos como `setDefaults()` o `getValues()`. ```php $sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Su nombre:'); +$sub1->addText('name', 'Your name:'); $sub1->addEmail('email', 'Email:'); $sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Su nombre:'); +$sub2->addText('name', 'Your name:'); $sub2->addEmail('email', 'Email:'); ``` -Los datos enviados se devuelven como una estructura multidimensional: +Los datos enviados se devuelven después como una estructura multidimensional: ```php [ @@ -434,112 +444,94 @@ Los datos enviados se devuelven como una estructura multidimensional: ``` -Resumen de configuración -======================== +Resumen de la configuración +=========================== -En todos los elementos podemos llamar a los siguientes métodos (resumen completo en la [documentación de la API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): +En todos los elementos podemos llamar a los siguientes métodos (vea la [documentación de la API|https://api.nette.org/forms/master/Nette/Forms/Controls.html] para un resumen completo): .[table-form-methods language-php] -| `setDefaultValue($value)` | establece el valor predeterminado -| `getValue()` | obtener el valor actual -| `setOmitted()` | [#Omisión de valor] -| `setDisabled()` | [#Desactivación de elementos] +| `setDefaultValue($value)` | establece el valor predeterminado +| `getValue()` | obtiene el valor actual +| `setOmitted()` | [#Valores omitidos] +| `setDisabled()` | [#Deshabilitar elementos] Renderizado: .[table-form-methods language-php] | `setCaption($caption)` | cambia la etiqueta del elemento | `setTranslator($translator)` | establece el [traductor |rendering#Traducción] -| `setHtmlAttribute($name, $value)` | establece el [atributo HTML |rendering#Atributos HTML] del elemento +| `setHtmlAttribute($name, $value)` | establece un [atributo HTML |rendering#Atributos HTML] del elemento | `setHtmlId($id)` | establece el atributo HTML `id` -| `setHtmlType($type)` | establece el atributo HTML `type` -| `setHtmlName($name)` | establece el atributo HTML `name` -| `setOption($key, $value)` | [configuración para renderizado |rendering#Opciones] +| `setOption($key, $value)` | [establece opciones de renderizado |rendering#Opciones] Validación: .[table-form-methods language-php] -| `setRequired()` | [elemento obligatorio |validation] -| `addRule()` | establece la [regla de validación |validation#Reglas] -| `addCondition()`, `addConditionOn()` | establece la [condición de validación |validation#Condiciones] -| `addError($message)` | [entrega de mensaje de error |validation#Errores durante el procesamiento] +| `setRequired()` | marca el elemento como [obligatorio |validation] +| `addRule()` | añade una [regla de validación |validation#Reglas] +| `addCondition()`, `addConditionOn()` | establece una [condición de validación |validation#Condiciones] +| `addError($message)` | [añade un mensaje de error |validation#Errores de procesamiento] -En los elementos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` se pueden llamar los siguientes métodos: +En los elementos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` se pueden llamar los siguientes métodos: .[table-form-methods language-php] -| `setNullable()` | establece si getValue() devolverá `null` en lugar de una cadena vacía +| `setNullable()` | establece si getValue() devuelve `null` en lugar de una cadena vacía | `setEmptyValue($value)` | establece un valor especial que se considera una cadena vacía -| `setMaxLength($length)` | establece el número máximo de caracteres permitidos -| `addFilter($filter)` | [modificación de entrada |validation#Modificación de la entrada] +| `setMaxLength($length)` | establece el número máximo de caracteres permitido +| `addFilter($filter)` | [modifica la entrada |validation#Modificar los valores de entrada] -Omisión de valor +Valores omitidos ================ -Si el valor rellenado por el usuario no nos interesa, podemos omitirlo del resultado del método `$form->getValues()` o de los datos pasados a los handlers mediante `setOmitted()`. Esto es útil para diversas contraseñas de verificación, elementos antispam, etc. +Si el valor que rellena el usuario no nos interesa, podemos usar `setOmitted()` para excluirlo del resultado del método `$form->getValues()` o de los datos que se pasan a los manejadores. Es útil para los distintos campos de confirmación de contraseña, elementos antispam, etc. ```php -$form->addPassword('passwordVerify', 'Contraseña para verificación:') - ->setRequired('Por favor, introduzca la contraseña de nuevo para verificar') - ->addRule($form::Equal, 'Las contraseñas no coinciden', $form['password']) +$form->addPassword('passwordVerify', 'Password again:') + ->setRequired('Fill your password again to check for typo') + ->addRule($form::Equal, 'Passwords do not match', $form['password']) ->setOmitted(); ``` -Desactivación de elementos -========================== +Deshabilitar elementos +====================== -Los elementos se pueden desactivar mediante `setDisabled()`. Tal elemento no puede ser editado por el usuario. +Los elementos se pueden deshabilitar con `setDisabled()`. Un elemento deshabilitado no puede ser editado por el usuario. ```php -$form->addText('username', 'Nombre de usuario:') +$form->addText('username', 'User name:') ->setDisabled(); ``` -Los elementos deshabilitados no son enviados por el navegador al servidor, por lo tanto, tampoco los encontrará en los datos devueltos por la función `$form->getValues()`. Sin embargo, si establece `setOmitted(false)`, Nette incluirá su valor predeterminado en estos datos. +Los elementos deshabilitados no los envía el navegador al servidor en absoluto, así que no los encontrará en los datos que devuelve la función `$form->getValues()`. Pero, si establece `setOmitted(false)`, Nette incluirá su valor predeterminado en esos datos. -Al llamar a `setDisabled()`, por razones de seguridad, **se borra el valor del elemento**. Si establece un valor predeterminado, es necesario hacerlo después de desactivarlo: +Al llamar a `setDisabled()`, **el valor del elemento se borra** por motivos de seguridad. Si está estableciendo un valor predeterminado, hay que hacerlo después de deshabilitarlo: ```php -$form->addText('username', 'Nombre de usuario:') +$form->addText('username', 'User name:') ->setDisabled() ->setDefaultValue($userName); ``` -Una alternativa a los elementos deshabilitados son los elementos con el atributo HTML `readonly`, que el navegador sí envía al servidor. Aunque el elemento es solo de lectura, es **importante tener en cuenta** que su valor aún puede ser modificado o falsificado por un atacante. +Una alternativa a los elementos deshabilitados son los elementos con el atributo HTML `readonly`, que el navegador sí envía al servidor. Aunque el elemento sea de solo lectura, **es importante darse cuenta** de que un atacante puede modificar o falsificar su valor. Elementos personalizados ======================== -Además de la amplia gama de elementos de formulario incorporados, puede agregar elementos personalizados al formulario de esta manera: +Además de la amplia oferta de elementos de formulario integrados, puede añadir al formulario elementos propios: ```php -$form->addComponent(new DateInput('Fecha:'), 'date'); -// sintaxis alternativa: $form['date'] = new DateInput('Fecha:'); +$form->addComponent(new DateInput('Date:'), 'date'); +// sintaxis alternativa: $form['date'] = new DateInput('Date:'); ``` -.[note] -El formulario es descendiente de la clase [Container |component-model:#Container] y los elementos individuales son descendientes de [Component |component-model:#Component]. - -Existe una forma de definir nuevos métodos de formulario que sirven para agregar elementos personalizados (por ejemplo, `$form->addZip()`). Se trata de los llamados extension methods. La desventaja es que el autocompletado en los editores no funcionará para ellos. - -```php -use Nette\Forms\Container; - -// agregamos el método addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Al menos 5 números', '[0-9]{5}'); -}); - -// uso -$form->addZip('zip', 'Código postal:'); -``` +Cómo escribir un elemento así, incluyendo la lectura de los datos enviados, la validación y el renderizado, se describe en un [capítulo aparte |custom-controls]. Allí conocerá también los métodos de extensión, que le permiten crear su propio método de adición como `$form->addZip()`. Elementos de bajo nivel ======================= -También se pueden usar elementos que escribimos solo en la plantilla y no los agregamos al formulario con alguno de los métodos `$form->addXyz()`. Por ejemplo, si mostramos registros de la base de datos y no sabemos de antemano cuántos habrá ni qué ID tendrán, y queremos mostrar una casilla de verificación o un botón de opción en cada fila, basta con codificarlo en la plantilla: +También es posible usar elementos que solo se escriben en la plantilla y no se añaden al formulario con ninguno de los métodos `$form->addXyz()`. Por ejemplo, cuando listamos registros de la base de datos y no sabemos de antemano cuántos habrá ni cuáles serán sus ID, y queremos mostrar una casilla de verificación o un botón de opción por cada fila, basta con escribirlo en la plantilla: ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ También se pueden usar elementos que escribimos solo en la plantilla y no los a {/foreach} ``` -Y después de enviar, obtenemos el valor: +Y tras el envío obtenemos el valor: ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -donde el primer parámetro es el tipo de elemento (`DataFile` para `type=file`, `DataLine` para entradas de una línea como `text`, `password`, `email`, etc. y `DataText` para todos los demás) y el segundo parámetro `sel[]` corresponde al atributo HTML name. Podemos combinar el tipo de elemento con el valor `DataKeys`, que conserva las claves de los elementos. Esto es especialmente útil para `select`, `radioList` y `checkboxList`. +donde el primer parámetro es el tipo de elemento (`DataFile` para `type=file`, `DataLine` para las entradas de una sola línea como `text`, `password`, `email`, etc., y `DataText` para todas las demás) y el segundo parámetro `sel[]` corresponde al atributo HTML name. Podemos combinar el tipo de elemento con el valor `DataKeys`, que conserva las claves de los elementos. Eso resulta especialmente útil para `select`, `radioList` y `checkboxList`. -Lo importante es que `getHttpData()` devuelve un valor sanitizado, en este caso siempre será un array de cadenas UTF-8 válidas, sin importar lo que un atacante intente pasar al servidor. Es análogo al trabajo directo con `$_POST` o `$_GET`, pero con la diferencia esencial de que siempre devuelve datos limpios, tal como está acostumbrado con los elementos estándar de los formularios Nette. +Lo esencial es que `getHttpData()` devuelve un valor saneado. En este caso siempre será un array de cadenas UTF-8 válidas, independientemente de lo que un atacante intente enviar al servidor. Es análogo a trabajar directamente con `$_POST` o `$_GET`, pero con la diferencia sustancial de que siempre devuelve datos limpios, tal como está acostumbrado con los elementos de formulario estándar de Nette. diff --git a/forms/es/custom-controls.texy b/forms/es/custom-controls.texy new file mode 100644 index 0000000000..5f2e8bdee7 --- /dev/null +++ b/forms/es/custom-controls.texy @@ -0,0 +1,268 @@ +Elementos de formulario personalizados +************************************** + +.[perex] +Nette ofrece una amplia paleta de [elementos de formulario integrados |controls]. Pero cuando se topa con un requisito que no está entre ellos, no tiene que dar rodeos ni pegar cosas: escribe su propio elemento. Podrá hacer todo lo que hacen los integrados (validarse, traducirse, renderizarse) y se usará exactamente igual. + +Lo mostraremos con un ejemplo práctico: un elemento para introducir una fecha con tres campos, día, mes y año. Por el camino aprenderá todo lo que necesita saber para escribir elementos. + + +Cuándo escribir un elemento propio y cuándo no +============================================== + +Un elemento propio es la herramienta más potente que ofrecen los formularios. Y, como toda herramienta potente, debería ser la última opción, no la primera. Muchas situaciones se resuelven por medios más simples: + +- **Modificar un valor** lo resuelve [addFilter() |validation#Modificar los valores de entrada]. ¿Quiere tolerar espacios en un código postal o minúsculas en un código? Un filtro son unas pocas líneas. +- **La configuración repetida** se envuelve en un método propio para añadir el elemento. ¿Añade en diez sitios un campo de código postal con la misma validación? Créeles un atajo con nombre, [lo mostramos al final |#Método propio para añadir el elemento]. +- **Un grupo de campos relacionados** lo cubre un [contenedor |controls#addContainer()]. Una dirección compuesta de calle, ciudad y código postal no necesita un elemento propio; basta con un contenedor con tres campos de texto. +- **Un aspecto distinto** se consigue con [setHtmlType() |controls#addText()] y atributos HTML, o con los [prototipos |rendering#Prototipos]. + +Un elemento propio tiene sentido en el momento en que necesita un **valor propio**: un elemento que por fuera actúa como un único campo con un único valor, pero que internamente consta de varios inputs o guarda el valor de forma distinta a como lo muestra. Una fecha a partir de tres campos. Unas coordenadas elegidas pulsando en un mapa. Un campo de etiquetas con autocompletado. + + +Anatomía de un elemento +======================= + +Todo elemento propio hereda de la clase abstracta [api:Nette\Forms\Controls\BaseControl]. De ella hereda una cantidad enorme de funcionalidad ya hecha: el almacenamiento del valor, las reglas y condiciones de validación, los mensajes de error, las traducciones, los atributos HTML, la etiqueta y la conexión con el renderizado. Usted solo escribe aquello en lo que su elemento se diferencia. + +Un elemento mínimo que funcione es sorprendentemente corto: + +```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, + ]); + } +} +``` + +Dos métodos: uno dice cómo obtener el valor de los datos enviados, el otro cómo renderizar el elemento. Enseguida veremos ambos de cerca. Todo lo demás (`setRequired()`, `addRule()`, `setDefaultValue()`, las traducciones) ya funciona solo. + +El elemento se añade al formulario con el método `addComponent()`, o de forma más concisa con corchetes: + +```php +$form['nickname'] = new SimpleInput('Nickname:'); +``` + + +Ciclo de vida de un elemento +============================ + +Antes de pasar a un elemento más interesante conviene saber qué le ocurre a un elemento y cuándo. El formulario y sus elementos son [componentes |component-model:] que forman un árbol. Eso tiene una consecuencia agradable: el elemento no tiene que averiguar nada por su cuenta, el framework se ocupa de todo lo importante en el momento adecuado: + +1) En el momento en que adjunta el elemento a un formulario enviado, el propio formulario llama en él a `loadHttpData()`. Ahí el elemento lee su valor enviado, como mostraremos enseguida. Nunca trabaja directamente con `$_POST` y no tiene que preocuparse en absoluto de si está anidado en contenedores. + +2) Al enviar el formulario tiene lugar la validación: se evalúan las reglas añadidas con `addRule()`, que trabajan con el valor de `getValue()`. + +3) Quien después llame a `$form->getValues()` o a `getValue()` en el elemento obtiene un valor limpio y tipado, como un objeto `DateTimeImmutable`, no un trío de cadenas del formulario. + +Y durante el renderizado se llama a `getControl()`, o a `getLabel()` para la etiqueta. + + +Leer el valor enviado +===================== + +En el método `loadHttpData()`, el elemento pide su valor enviado con el método `getHttpData()`. Su parámetro es un tipo que determina cómo debe limpiarse el valor: + +| tipo | significado +|------- +| `Form::DataLine` | texto de una línea: sustituye los saltos de línea por espacios y recorta los espacios +| `Form::DataText` | texto de varias líneas: normaliza los finales de línea a `\n` +| `Form::DataFile` | archivo subido, una instancia de `Nette\Http\FileUpload` + +Por mucho que se esfuerce un atacante, el resultado es siempre una cadena UTF-8 válida sin caracteres de control (o un objeto de archivo subido o `null`). Justamente por eso nunca leemos el valor directamente de `$_POST`: perderíamos todas esas garantías. + +Un elemento formado por varios inputs, como nuestra fecha, pasa una parte del nombre HTML como segundo parámetro y lee así sus distintos subvalores. Los guarda en sus propias propiedades `$day`, `$month` y `$year` de tipo 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]') ?? ''; +} +``` + +Si el nombre HTML termina en `[]`, se devuelve un array de valores. Combinándolo con el tipo `Form::DataKeys` (es decir, `Form::DataLine | Form::DataKeys`) conserva además sus claves: + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +Un valor que falta es `null` (un array vacío en el caso de los arrays). La petición no tiene por qué contener los datos del elemento; nada impide a un atacante enviar lo que le apetezca, y por eso en el ejemplo añadimos `?? ''` y por eso debería contar siempre con esa posibilidad. + + +Valor del elemento +================== + +El elemento guarda su valor y lo expone mediante un trío de métodos cuyo contrato conviene respetar. + +El método `setValue()` acepta un valor del programador; por ahí pasan también `setDefaultValue()` y `$form->setDefaults()`. Debería aceptar todo lo que tenga sentido, convertir el valor a su forma interna y lanzar una excepción ante una entrada absurda, para que el error se vea de inmediato y no a través de un comportamiento misterioso del formulario. Nuestra fecha acepta un `DateTimeInterface`, una cadena, un timestamp o `null`, y los reparte en los tres campos: + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // un disparate lanza una excepción + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +El método `getValue()`, en cambio, compone un valor limpio y tipado, lo único que verá quien use su elemento. Si el valor no es válido, devuelve `null`. El método estático `validateDate()` simplemente comprueba que los tres campos forman una fecha existente: + +```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; +} +``` + +Y el método `isFilled()` dice si el usuario ha rellenado el elemento; lo usa la regla `setRequired()`. La implementación predeterminada (un valor no vacío) suele bastar, pero en un elemento compuesto sobrescríbala según su lógica: + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Renderizado +=========== + +El método `getControl()` devuelve la forma HTML del elemento, normalmente como objeto [Html |utils:html-elements], aunque una simple cadena también vale: da igual. Recurrimos al objeto Html sobre todo al montar el código, porque nos permite construir el marcado resultante de forma segura y con una API agradable. Tiene a su disposición varios ayudantes: + +- `getHtmlName()` devuelve el atributo HTML `name`, incluido el posible anidamiento en contenedores (p. ej. `invoice[date]`). En un elemento compuesto le añade las partes del nombre de cada input: `$name . '[day]'`. +- `getHtmlId()` devuelve el atributo `id` enlazado con la etiqueta. +- `Helpers::exportRules($this->getRules())` exporta las reglas de validación para el atributo `data-nette-rules`, gracias al cual la [validación en JavaScript |validation#Validación en JavaScript] funcionará también en su elemento. El atributo va en el primer input del elemento. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` monta un elemento `<select>` a partir de un array de elementos (los arrays anidados se renderizan como `<optgroup>`) y lo devuelve como `Html`; práctico para el campo del mes de nuestra fecha. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` genera una lista de elementos `<input>` envueltos en `<label>` (radio buttons o checkboxes) y la devuelve como cadena. + +El primer campo de nuestra fecha se crea, por tanto, así: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select para el mes e input para el año ... */); +} +``` + +La etiqueta la renderiza `getLabel()` y su implementación predeterminada suele valer. Solo un aviso: en un elemento compuesto, su atributo `for` apunta a `getHtmlId()`, así que dele ese id al primer input, exactamente como en el ejemplo. + +Para que el elemento compuesto se pueda renderizar por partes en una plantilla (p. ej. `{input birthdate:day}`), sobrescriba los métodos `getControlPart($key)` y `getLabelPart($key)`, que devuelven el elemento `Html` de esa parte, igual que hacen `CheckboxList` y `RadioList`. + +.[note] +Si sobrescribe `getControl()`, tenga presente que `BaseControl::getControl()` marca además el elemento como renderizado con `setOption('rendered', true)`. Llámelo también (o llame a `parent::getControl()`) cuando combine el renderizado manual y el automático del mismo formulario, para que el elemento no se renderice dos veces. (El ejemplo `DateInput` de arriba lo omite por brevedad.) + + +Ejemplo completo: DateInput +=========================== + +Todas las piezas descritas juntas, completadas con un select para elegir el mes, las encontrará en el elemento `DateInput` terminado, entre los [ejemplos del propio repositorio |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Fíjese en que, en el constructor, el elemento se añade a sí mismo una regla de validación que comprueba que la fecha tiene sentido. Una entrada absurda, como el 31 de febrero, aparece así como un error de validación corriente del formulario: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'The date is invalid.'); +} +``` + +¿Y el uso? Exactamente igual que con los elementos integrados: + +```php +$form['birthdate'] = (new DateInput('Date of birth:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('When were you born?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +En una plantilla Latte lo renderiza con la etiqueta habitual `{input birthdate}` o `{label birthdate /}`, igual que cualquier otro elemento. + + +Validación +========== + +Las reglas de validación integradas funcionan con un elemento propio desde el primer momento: trabajan con el valor de `getValue()`. Nuestro `DateInput` puede usar así, por ejemplo, `Form::Min` para la fecha más antigua permitida. Cómo escribir sus propias reglas, incluida su contrapartida en JavaScript, se describe en el capítulo [Reglas y condiciones propias |validation#Reglas y condiciones propias]. + + +Método propio para añadir el elemento +===================================== + +Los elementos integrados los añadimos con los cómodos métodos `$form->addText()` y compañía. Un elemento propio no tiene un método así, de modo que lo añade con una simple asignación: funciona igual en un formulario y en un contenedor, y los editores y el análisis estático lo entienden: + +```php +$form['birthdate'] = new DateInput('Date of birth:'); +``` + +Si quiere acortar la adición sin perder el autocompletado, viene bien un método fábrica estático en el propio elemento. Funciona incluso en contenedores anidados, cosa que un método en un descendiente de la clase `Form` no podría hacer: los contenedores anidados no saben de él: + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// funciona en un formulario y en cualquier contenedor: +DateInput::addTo($form, 'birthdate', 'Date of birth:'); +``` + +El mismo enfoque vale también como atajo con nombre para la configuración repetida de un elemento integrado: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'The ZIP code must be exactly 5 digits', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'ZIP code:'); +``` diff --git a/forms/es/in-presenter.texy b/forms/es/in-presenter.texy index 024789f9bc..f405d61564 100644 --- a/forms/es/in-presenter.texy +++ b/forms/es/in-presenter.texy @@ -2,33 +2,33 @@ Formularios en presenters ************************* .[perex] -Nette Forms facilita enormemente la creación y el procesamiento de formularios web. En este capítulo, aprenderá a usar formularios dentro de los presenters. +Nette Forms simplifica notablemente la creación y el procesamiento de formularios web. En este capítulo aprenderá a usar los formularios dentro de los presenters. -Si le interesa cómo usarlos de forma completamente independiente sin el resto del framework, la guía para [uso independiente|standalone] es para usted. +Si le interesa usarlos de forma completamente independiente, sin el resto del framework, tiene una guía sobre el [uso independiente|standalone]. Primer formulario ================= -Intentemos escribir un formulario de registro simple. Su código será el siguiente: +Probemos a escribir un formulario de registro sencillo. Su código será el siguiente: ```php use Nette\Application\UI\Form; $form = new Form; -$form->addText('name', 'Nombre:'); -$form->addPassword('password', 'Contraseña:'); -$form->addSubmit('send', 'Registrar'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->addText('name', 'Name:'); +$form->addPassword('password', 'Password:'); +$form->addSubmit('send', 'Sign up'); +$form->onSuccess[] = $this->formSucceeded(...); ``` y en el navegador se mostrará así: -[* form-cs.webp *] +[* form-en.webp *] -El formulario en el presenter es un objeto de la clase `Nette\Application\UI\Form`, su predecesor `Nette\Forms\Form` está destinado a un uso independiente. Le hemos agregado los llamados elementos nombre, contraseña y botón de envío. Y finalmente, la línea con `$form->onSuccess` dice que después del envío y la validación exitosa, se debe llamar al método `$this->formSucceeded()`. +Un formulario en un presenter es un objeto de la clase `Nette\Application\UI\Form`; su antecesora `Nette\Forms\Form` está pensada para el uso independiente. Le hemos añadido elementos llamados name y password y un botón de envío. Por último, la línea `$form->onSuccess` dice que, tras el envío y una validación correcta, se debe llamar al método `$this->formSucceeded()`. -Desde el punto de vista del presenter, el formulario es un componente común. Por lo tanto, se trata como un componente y lo incorporamos al presenter mediante un [método de fábrica |application:components#Métodos de fábrica]. Se verá así: +Desde la perspectiva del presenter, el formulario es un componente corriente. Por eso se trata como un componente y se integra en el presenter con un [método fábrica |application:components#Métodos factory]. Quedará así: ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -39,71 +39,73 @@ class HomePresenter extends Nette\Application\UI\Presenter protected function createComponentRegistrationForm(): Form { $form = new Form; - $form->addText('name', 'Nombre:'); - $form->addPassword('password', 'Contraseña:'); - $form->addSubmit('send', 'Registrar'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->addText('name', 'Name:'); + $form->addPassword('password', 'Password:'); + $form->addSubmit('send', 'Sign up'); + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { - // aquí procesamos los datos enviados por el formulario + // aquí procesaremos los datos enviados por el formulario // $data->name contiene el nombre // $data->password contiene la contraseña - $this->flashMessage('Ha sido registrado exitosamente.'); + $this->flashMessage('You have successfully signed up.'); $this->redirect('Home:'); } } ``` -Y en la plantilla renderizamos el formulario con la etiqueta `{control}`: +Y en la plantilla, el formulario se renderiza con la etiqueta `{control}`: ```latte .{file:app/Presentation/Home/default.latte} -<h1>Registro</h1> +<h1>Registration</h1> {control registrationForm} ``` -Y eso es todo :-) Tenemos un formulario funcional y perfectamente [seguro |#Protección contra vulnerabilidades]. +Y eso es básicamente todo :-) Tenemos un formulario funcional y perfectamente [protegido |#Protección frente a vulnerabilidades]. -Y ahora probablemente esté pensando que fue demasiado rápido, se pregunta cómo es posible que se llame al método `formSucceeded()` y cuáles son los parámetros que recibe. Ciertamente, tiene razón, esto merece una explicación. +Ahora estará pensando que ha ido demasiado rápido y se preguntará cómo es posible que se llame al método `formSucceeded()` y qué parámetros recibe. Sí, tiene razón, esto merece una explicación. -Nette presenta un mecanismo fresco que llamamos [Hollywood style |application:components#Estilo Hollywood]. En lugar de que usted, como desarrollador, tenga que preguntar constantemente si algo sucedió ("¿se envió el formulario?", "¿se envió válidamente?" y "¿no fue falsificado?"), le dice al framework "cuando el formulario esté válidamente completado, llama a este método" y deja el trabajo adicional en sus manos. Si programa en JavaScript, este estilo de programación le resultará familiar. Escribe funciones que se llaman cuando ocurre un cierto [evento |nette:glossary#Eventos]. Y el lenguaje les pasa los argumentos apropiados. +Nette introduce un mecanismo refrescante llamado [estilo Hollywood |application:components#Estilo Hollywood]. En lugar de que usted, como desarrollador, tenga que preguntar constantemente si ha pasado algo ("¿se ha enviado el formulario?", "¿se ha enviado de forma válida?" y "¿no ha sido falsificado?"), le dice al framework "cuando el formulario esté válidamente rellenado, 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 llaman cuando ocurre un determinado [evento |nette:glossary#Eventos]. Y el lenguaje les pasa los argumentos adecuados. -Así es exactamente como está construido el código del presenter anterior. El array `$form->onSuccess` representa una lista de callbacks de PHP que Nette llama en el momento en que el formulario se envía y se completa correctamente (es decir, es válido). Dentro del [ciclo de vida del presenter |application:presenters#Ciclo de vida del presenter], esto es una llamada señal, por lo que se llaman después del método `action*` y antes del método `render*`. Y a cada callback le pasa como primer parámetro el propio formulario y como segundo los datos enviados en forma de objeto [ArrayHash |utils:arrays#ArrayHash]. Puede omitir el primer parámetro si no necesita el objeto del formulario. Y el segundo parámetro puede ser más inteligente, pero hablaremos de eso [más adelante |#Mapeo a clases]. +Justamente así está construido el código del presenter de arriba. El array `$form->onSuccess` representa una lista de callbacks de PHP que Nette llama en el momento en que el formulario se envía y está correctamente rellenado (es decir, es válido). Dentro del [ciclo de vida del presenter |application:presenters#Ciclo de vida del presenter] se trata de una señal, así que se llaman después del método `action*` y antes del método `render*`. Y a cada callback le pasa el propio formulario como primer parámetro y los datos enviados como objeto [ArrayHash |utils:arrays#ArrayHash] (o stdClass, o una clase propia) como segundo. Puede omitir el primer parámetro si no necesita el objeto del formulario. El segundo parámetro puede ser más inteligente, pero de eso hablaremos [más adelante |#Mapeo a clases]. -El objeto `$data` contiene las claves `name` y `password` con los datos que el usuario completó. Normalmente, enviamos los datos directamente para su posterior procesamiento, que puede ser, por ejemplo, la inserción en una base de datos. Sin embargo, durante el procesamiento puede ocurrir un error, por ejemplo, el nombre de usuario ya está ocupado. En tal caso, devolvemos el error al formulario usando `addError()` y dejamos que se renderice de nuevo, con el mensaje de error. +El objeto `$data` contiene las propiedades `name` y `password` con los datos introducidos por el usuario. Normalmente enviamos los datos directamente a su procesamiento posterior, que puede ser, por ejemplo, la inserción en una base de datos. Durante el procesamiento puede producirse un error, sin embargo, por ejemplo que el nombre de usuario ya esté ocupado. En ese caso devolvemos el error al formulario con `addError()` y dejamos que se renderice otra vez, junto con el mensaje de error. ```php -$form->addError('Lo sentimos, el nombre de usuario ya está en uso.'); +$form->addError('Sorry, username is already in use.'); ``` -Además de `onSuccess`, también existe `onSubmit`: los callbacks se llaman siempre después de enviar el formulario, incluso si no está correctamente completado. Y además `onError`: los callbacks se llaman solo si el envío no es válido. Se llaman incluso si invalidamos el formulario en `onSuccess` o `onSubmit` usando `addError()`. +Además de `onSuccess` existe también `onSubmit`: los callbacks se llaman siempre que se envía el formulario, aunque no esté correctamente rellenado. Y también `onError`: los callbacks se llaman solo si el envío no es válido. Se llaman incluso si invalidamos el formulario en `onSuccess` con `addError()`. -Después de procesar el formulario, redirigimos a la siguiente página. Esto evita el reenvío no deseado del formulario con el botón *actualizar*, *atrás* o moviéndose en el historial del navegador. +Tras procesar el formulario redirigimos a otra página. Eso evita el reenvío no deseado del formulario mediante el botón *actualizar* o *atrás* o navegando por el historial del navegador. -Intente agregar también otros [elementos de formulario|controls]. +Si el formulario se envía por AJAX, en lugar de redirigir se suele redibujar un [snippet |application:ajax] con el formulario renderizado de nuevo. + +Pruebe a añadir otros [elementos de formulario|controls]. Acceso a los elementos ====================== -El formulario es un componente del presenter, en nuestro caso llamado `registrationForm` (según el nombre del método de fábrica `createComponentRegistrationForm`), por lo que en cualquier lugar del presenter puede acceder al formulario mediante: +El formulario es un componente del presenter, en nuestro caso llamado `registrationForm` (por el nombre del método fábrica `createComponentRegistrationForm`), así que en cualquier lugar del presenter puede acceder al formulario con: ```php $form = $this->getComponent('registrationForm'); // sintaxis alternativa: $form = $this['registrationForm']; ``` -Los elementos individuales del formulario también son componentes, por lo que puede acceder a ellos de la misma manera: +Los distintos elementos del formulario también son componentes, así que puede acceder a ellos de la misma manera: ```php $input = $form->getComponent('name'); // o $input = $form['name']; $button = $form->getComponent('send'); // o $button = $form['send']; ``` -Los elementos se eliminan usando unset: +Los elementos se eliminan con `unset`: ```php unset($form['name']); @@ -113,26 +115,26 @@ unset($form['name']); Reglas de validación ==================== -Se mencionó la palabra *válido*, pero el formulario aún no tiene reglas de validación. Vamos a solucionarlo. +Hemos mencionado la palabra *válido*, pero el formulario todavía no tiene ninguna regla de validación. Arreglémoslo. -El nombre será obligatorio, por lo que lo marcamos con el método `setRequired()`, cuyo argumento es el texto del mensaje de error que se mostrará si el usuario no completa el nombre. Si no se proporciona el argumento, se utilizará el mensaje de error predeterminado. +El nombre será obligatorio, así que lo marcamos con el método `setRequired()`. Su argumento es el texto del mensaje de error que se muestra si el usuario no rellena el nombre. Si se omite el argumento, se usa el mensaje de error predeterminado. ```php -$form->addText('name', 'Nombre:') - ->setRequired('Por favor, introduzca el nombre'); +$form->addText('name', 'Name:') + ->setRequired('Please enter your name.'); ``` -Intente enviar el formulario sin completar el nombre y verá que se muestra un mensaje de error y el navegador o servidor lo rechazará hasta que complete el campo. +Pruebe a enviar el formulario sin rellenar el nombre y verá que se muestra un mensaje de error, y el navegador o el servidor lo rechazarán hasta que rellene el campo. -Al mismo tiempo, el sistema no se deja engañar si escribe, por ejemplo, solo espacios en el campo. De ninguna manera. Nette elimina automáticamente los espacios iniciales y finales. Pruébelo. Es algo que siempre debería hacer con cada input de una sola línea, pero a menudo se olvida. Nette lo hace automáticamente. (Puede intentar engañar al formulario y enviar una cadena multilínea como nombre. Incluso aquí, Nette no se dejará engañar y cambiará los saltos de línea por espacios.) +Al mismo tiempo, no puede engañar al sistema escribiendo en el campo, por ejemplo, solo espacios. De ninguna manera. Nette recorta automáticamente los espacios del principio y del final. Pruébelo. Es algo que debería hacer siempre en todos los campos de una línea, pero que a menudo se olvida. Nette lo hace automáticamente. (Puede intentar engañar al formulario y enviar como nombre una cadena de varias líneas. Tampoco así se dejará engañar Nette, y los saltos de línea se convertirán en espacios.) -El formulario siempre se valida en el lado del servidor, pero también se genera una validación JavaScript, que se ejecuta instantáneamente y el usuario se entera del error de inmediato, sin necesidad de enviar el formulario al servidor. Esto lo maneja el script `netteForms.js`. Insértelo en la plantilla de layout: +El formulario se valida siempre en el lado del servidor, pero además se genera la validación en JavaScript, que se ejecuta al instante y el usuario se entera del error de inmediato, sin necesidad de enviar el formulario al servidor. De eso se ocupa el script `netteForms.js`. Inclúyalo en su plantilla de layout: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Si mira el código fuente de la página con el formulario, puede notar que Nette inserta los elementos obligatorios en elementos con la clase CSS `required`. Intente agregar la siguiente hoja de estilos a la plantilla y la etiqueta "Nombre" será roja. De esta manera, marcamos elegantemente los elementos obligatorios para los usuarios: +Si mira el código fuente de la página con el formulario, quizá se dé cuenta de que Nette envuelve los elementos obligatorios en elementos con la clase CSS `required`. Pruebe a añadir la siguiente hoja de estilos a su plantilla y la etiqueta 'Name' se pondrá roja. Así se resaltan con elegancia los campos obligatorios para los usuarios: ```latte <style> @@ -140,85 +142,87 @@ Si mira el código fuente de la página con el formulario, puede notar que Nette </style> ``` -Agregamos otras reglas de validación con el método `addRule()`. El primer parámetro es la regla, el segundo es nuevamente el texto del mensaje de error y puede seguir un argumento de la regla de validación. ¿Qué significa esto? +Las demás reglas de validación las añadimos con el método `addRule()`. El primer parámetro es la regla, el segundo es de nuevo el texto del mensaje de error y puede seguirle un argumento de la regla de validación. ¿Qué significa eso? -Ampliaremos el formulario con un nuevo campo opcional "edad", que debe ser un número entero (`addInteger()`) y además estar en un rango permitido (`$form::Range`). Y aquí es donde usaremos el tercer parámetro del método `addRule()`, con el que pasaremos al validador el rango requerido como un par `[desde, hasta]`: +Ampliemos el formulario con un nuevo campo opcional 'age', que debe ser un número entero (`addInteger()`) y además estar dentro de un rango permitido (`$form::Range`). Aquí usaremos el tercer parámetro del método `addRule()` para pasarle al validador el rango requerido como par `[min, max]`: ```php -$form->addInteger('age', 'Edad:') - ->addRule($form::Range, 'La edad debe estar entre 18 y 120', [18, 120]); +$form->addInteger('age', 'Age:') + ->addRule($form::Range, 'Age must be between 18 and 120.', [18, 120]); ``` .[tip] -Si el usuario no completa el campo, las reglas de validación no se verificarán, ya que el elemento es opcional. +Si el usuario no rellena el campo, las reglas de validación no se comprobarán, porque el elemento es opcional. -Aquí surge espacio para una pequeña refactorización. En el mensaje de error y en el tercer parámetro, los números se indican de forma duplicada, lo cual no es ideal. Si estuviéramos creando [formularios multilingües |rendering#Traducción] y el mensaje que contiene números se tradujera a varios idiomas, dificultaría un posible cambio de valores. Por esta razón, es posible usar los placeholders `%d` y Nette completará los valores: +Esto abre espacio para una pequeña refactorización. En el mensaje de error y en el tercer parámetro, los números están duplicados, lo que no es ideal. Si estuviéramos creando [formularios multilingües |rendering#Traducción] y el mensaje con los números se tradujera a varios idiomas, cambiar los valores sería complicado. Por eso se pueden usar los marcadores `%d` y Nette sustituirá los valores: ```php - ->addRule($form::Range, 'La edad debe estar entre %d y %d años', [18, 120]); + ->addRule($form::Range, 'Age must be between %d and %d years.', [18, 120]); ``` -Volvamos al elemento `password`, que también haremos obligatorio y además verificaremos la longitud mínima de la contraseña (`$form::MinLength`), nuevamente usando el placeholder: +Volvamos al elemento `password`, hagámoslo también obligatorio y verifiquemos además la longitud mínima de la contraseña (`$form::MinLength`), usando de nuevo un marcador en el mensaje: ```php -$form->addPassword('password', 'Contraseña:') - ->setRequired('Elija una contraseña') - ->addRule($form::MinLength, 'La contraseña debe tener al menos %d caracteres', 8); +$form->addPassword('password', 'Password:') + ->setRequired('Pick a password') + ->addRule($form::MinLength, 'Your password must be at least %d characters long.', 8); ``` -Agregaremos al formulario otro campo `passwordVerify`, donde el usuario ingresará la contraseña nuevamente, para verificar. Usando reglas de validación, verificaremos si ambas contraseñas son iguales (`$form::Equal`). Y como parámetro, daremos una referencia a la primera contraseña usando [corchetes |#Acceso a los elementos]: +Añadamos al formulario otro campo `passwordVerify`, donde el usuario introduce la contraseña otra vez para confirmarla. Con las reglas de validación comprobamos que ambas contraseñas sean iguales (`$form::Equal`). Como argumento indicamos una referencia a la primera contraseña usando [corchetes |#Acceso a los elementos]: ```php -$form->addPassword('passwordVerify', 'Contraseña para verificar:') - ->setRequired('Por favor, introduzca la contraseña de nuevo para verificar') - ->addRule($form::Equal, 'Las contraseñas no coinciden', $form['password']) +$form->addPassword('passwordVerify', 'Password again:') + ->setRequired('Fill your password again to check for typo') + ->addRule($form::Equal, 'Passwords do not match.', $form['password']) ->setOmitted(); ``` -Con `setOmitted()`, hemos marcado un elemento cuyo valor en realidad no nos importa y que existe solo con fines de validación. El valor no se pasará a `$data`. +Con `setOmitted()` hemos marcado un elemento cuyo valor no nos interesa realmente y que existe solo a efectos de validación. Su valor no se pasa a `$data`. -Con esto, tenemos un formulario completamente funcional con validación en PHP y JavaScript. Las capacidades de validación de Nette son mucho más amplias, se pueden crear condiciones, hacer que partes de la página se muestren y oculten según ellas, etc. Todo lo aprenderá en el capítulo sobre [validación de formularios|validation]. +Con esto tenemos un formulario plenamente funcional con validación tanto en PHP como en JavaScript. Las capacidades de validación de Nette son mucho más amplias; se pueden crear condiciones, mostrar u ocultar partes de la página en función de ellas, etc. Lo aprenderá todo en el capítulo sobre la [validación de formularios|validation]. Valores predeterminados ======================= -Normalmente establecemos valores predeterminados para los elementos del formulario: +Habitualmente establecemos valores predeterminados en los elementos del formulario: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -A menudo es útil establecer valores predeterminados para todos los elementos a la vez. Por ejemplo, cuando el formulario se usa para editar registros. Leemos el registro de la base de datos y establecemos los valores predeterminados: +A menudo resulta útil establecer los valores predeterminados de todos los elementos a la vez. Por ejemplo, cuando el formulario sirve para editar registros. Leemos el registro de la base de datos y establecemos los valores predeterminados: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Llame a `setDefaults()` después de definir los elementos. +En un formulario ya enviado, `setDefaults()` no tiene efecto: no sobrescribirá lo que el usuario rellenó, así que es seguro llamarlo sin condiciones en la fábrica del formulario. Si necesita forzar los valores incluso después del envío, use `setValues()` en su lugar. + -Renderizado del formulario -========================== +Renderizar el formulario +======================== -Por defecto, el formulario se renderiza como una tabla. Los elementos individuales cumplen la regla básica de accesibilidad: todas las etiquetas se escriben como `<label>` y están vinculadas al elemento de formulario correspondiente. Al hacer clic en la etiqueta, el cursor aparece automáticamente en el campo del formulario. +De forma predeterminada, el formulario se renderiza como una tabla. Los distintos elementos respetan las reglas básicas de accesibilidad web: todas las etiquetas se escriben como elementos `<label>` y se asocian con sus respectivos elementos de formulario. Al pulsar la etiqueta, el cursor se sitúa automáticamente en el campo del formulario. -Podemos establecer cualquier atributo HTML para cada elemento. Por ejemplo, agregar un placeholder: +A cada elemento le podemos poner atributos HTML arbitrarios. Por ejemplo, añadir un placeholder: ```php -$form->addInteger('age', 'Edad:') - ->setHtmlAttribute('placeholder', 'Por favor, complete la edad'); +$form->addInteger('age', 'Age:') + ->setHtmlAttribute('placeholder', 'Please fill in the age'); ``` -Hay realmente muchas formas de renderizar un formulario, por lo que hay un [capítulo separado dedicado al renderizado|rendering]. +Hay de verdad muchas maneras de renderizar un formulario, así que se le dedica un [capítulo aparte sobre el renderizado|rendering]. Mapeo a clases ============== -Volvamos al método `formSucceeded()`, que en el segundo parámetro `$data` recibe los datos enviados como un objeto `ArrayHash`. Dado que es una clase genérica, algo así como `stdClass`, nos faltará cierta comodidad al trabajar con ella, como el autocompletado de propiedades en los editores o el análisis estático de código. Esto podría resolverse teniendo una clase específica para cada formulario, cuyas propiedades representen los elementos individuales. Por ejemplo: +Volvamos al método `formSucceeded()`, que recibe los datos enviados en el segundo parámetro `$data` como objeto `ArrayHash` (o `stdClass`). Como es una clase genérica, parecida a `stdClass`, al trabajar con ella nos faltan ciertas comodidades, como el autocompletado de las propiedades en los editores o el análisis estático del código. Eso se podría resolver teniendo una clase concreta para cada formulario, cuyas propiedades representen los distintos elementos. P. ej.: ```php class RegistrationFormData @@ -229,23 +233,23 @@ class RegistrationFormData } ``` -Alternativamente, puede usar el constructor: +Alternativamente puede usar un constructor: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Las propiedades de la clase de datos también pueden ser enums y se mapearán automáticamente. .{data-version:3.2.4} +Las propiedades de la clase de datos también pueden ser enums, y se mapearán automáticamente. .{data-version:3.2.4} -¿Cómo decirle a Nette que nos devuelva los datos como objetos de esta clase? Más fácil de lo que piensa. Simplemente especifique la clase como el tipo del parámetro `$data` en el método manejador: +¿Cómo le decimos a Nette que devuelva los datos como objetos de esa clase? Es más fácil de lo que parece. Basta con indicar la clase como tipo del parámetro `$data` en el método manejador: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void @@ -256,16 +260,18 @@ public function formSucceeded(Form $form, RegistrationFormData $data): void } ``` -Como tipo también se puede especificar `array` y luego los datos se pasarán como un array. +Como tipo también puede indicar `array`, y entonces los datos se pasarán como array. -De manera similar, también se puede usar la función `getValues()`, a la que pasamos el nombre de la clase o el objeto a hidratar como parámetro: +De forma parecida puede usar el método `getValues()`, pasándole como parámetro el nombre de la clase o un objeto que hidratar: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Si los formularios forman una estructura multinivel compuesta por contenedores, cree una clase separada para cada uno: +Si necesita leer los valores antes de que el formulario se valide, normalmente dentro de un manejador `onValidate`, use en su lugar el método `getUntrustedValues()`. Acepta los mismos parámetros que `getValues()`, pero devuelve los valores enviados sin garantizar que hayan pasado la validación. + +Si los formularios tienen una estructura de varios niveles compuesta de contenedores, cree una clase separada para cada uno: ```php $form = new Form; @@ -287,93 +293,95 @@ class RegistrationFormData } ``` -El mapeo entonces, a partir del tipo de la propiedad `$person`, sabe que debe mapear el contenedor a la clase `PersonFormData`. Si la propiedad contuviera un array de contenedores, especifique el tipo `array` y pase la clase para el mapeo directamente al contenedor: +El mapeo deduce entonces, por el tipo de la propiedad `$person`, que debe mapear el contenedor a la clase `PersonFormData`. Si la propiedad tuviera que contener un array de contenedores, indique el tipo `array` y pase la clase que hay que mapear directamente al contenedor: ```php $person->setMappedType(PersonFormData::class); ``` -Puede hacer que el diseño de la clase de datos del formulario se genere usando el método `Nette\Forms\Blueprint::dataClass($form)`, que lo imprimirá en la página del navegador. Luego, simplemente haga clic para seleccionar el código y cópielo en su proyecto. .{data-version:3.1.15} +Puede generar una propuesta de la clase de datos del formulario con el método `Nette\Forms\Blueprint::dataClass($form)`, que la imprime en la página del navegador. Después basta con seleccionar y copiar el código a su proyecto. .{data-version:3.1.15} -Múltiples botones -================= +Varios botones de envío +======================= -Si el formulario tiene más de un botón, generalmente necesitamos distinguir cuál de ellos fue presionado. Podemos crear nuestra propia función de manejo para cada botón. La establecemos como handler para el [evento |nette:glossary#Eventos] `onClick`: +Si el formulario tiene más de un botón, normalmente necesitamos distinguir cuál se pulsó. Podemos crear una función manejadora separada para cada botón. Establézcala como manejador del [evento |nette:glossary#Eventos] `onClick`: ```php -$form->addSubmit('save', 'Guardar') - ->onClick[] = [$this, 'saveButtonPressed']; +$form->addSubmit('save', 'Save') + ->onClick[] = $this->saveButtonPressed(...); -$form->addSubmit('delete', 'Eliminar') - ->onClick[] = [$this, 'deleteButtonPressed']; +$form->addSubmit('delete', 'Delete') + ->onClick[] = $this->deleteButtonPressed(...); ``` -Estos handlers se llaman solo en caso de un formulario válidamente completado, al igual que en el caso del evento `onSuccess`. La diferencia es que como primer parámetro, en lugar del formulario, se puede pasar el botón de envío, dependiendo del tipo que especifique: +.{data-version:3.3.0} +El manejador también se le puede pasar al botón directamente como tercer argumento del método `addSubmit()`. + +Estos manejadores se llaman solo si el formulario está válidamente rellenado (a no ser que la validación esté desactivada para el botón), igual que el evento `onSuccess`. La diferencia está en que el primer parámetro que se pasa puede ser el objeto del botón de envío en lugar del formulario, según el type hint que indique: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Cuando el formulario se envía con la tecla <kbd>Enter</kbd>, se considera como si se hubiera enviado con el primer botón. +Cuando el formulario se envía pulsando la tecla <kbd>Enter</kbd>, se trata como si se hubiera enviado con el primer botón de envío. Evento onAnchor =============== -Cuando construimos el formulario en el método de fábrica (como `createComponentRegistrationForm`), este aún no sabe si fue enviado, ni con qué datos. Pero hay casos en los que necesitamos conocer los valores enviados, por ejemplo, si la forma posterior del formulario depende de ellos, o si los necesitamos para select boxes dependientes, etc. +Cuando construye un formulario en un método fábrica (como `createComponentRegistrationForm`), este todavía no sabe si se ha enviado ni con qué datos. Hay casos, sin embargo, en los que necesitamos conocer los valores enviados: quizá el aspecto del formulario dependa de ellos, o hagan falta para select boxes dependientes, etc. -Por lo tanto, puede dejar que parte del código que construye el formulario se llame solo en el momento en que está, por así decirlo, anclado, es decir, ya está conectado al presenter y conoce sus datos enviados. Pasamos dicho código al array `$onAnchor`: +Por eso puede hacer que el código que construye el formulario se llame solo cuando este esté "anclado", es decir, cuando ya esté conectado al presenter y conozca sus datos enviados. Coloque ese código en el array `$onAnchor`: ```php -$country = $form->addSelect('country', 'Estado:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Ciudad:'); +$country = $form->addSelect('country', 'Country:', $this->model->getCountries()); +$city = $form->addSelect('city', 'City:'); $form->onAnchor[] = function () use ($country, $city) { - // esta función se llamará solo cuando el formulario sepa si fue enviado y con qué datos - // por lo tanto, se puede usar el método getValue() + // esta función se llamará cuando el formulario conozca los datos con los que se envió + // así que puede usar el método getValue() $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; ``` -Protección contra vulnerabilidades -================================== +Protección frente a vulnerabilidades +==================================== -Nette Framework pone gran énfasis en la seguridad y, por lo tanto, se preocupa meticulosamente por la buena seguridad de los formularios. Lo hace de forma totalmente transparente y no requiere configurar nada manualmente. +Nette Framework pone un gran énfasis en la seguridad y por eso vela meticulosamente por la seguridad de los formularios. Lo hace de forma completamente transparente y no requiere ninguna configuración manual. -Además de proteger los formularios contra ataques [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] y [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], realiza muchas pequeñas protecciones en las que ya no tiene que pensar. +Además de proteger los formularios frente a ataques como el [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] y el [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], realiza muchas pequeñas medidas de seguridad en las que ya no tiene que pensar. -Por ejemplo, filtra todos los caracteres de control de las entradas y verifica la validez de la codificación UTF-8, por lo que los datos del formulario siempre estarán limpios. En los select boxes y radio lists, verifica que los elementos seleccionados fueran realmente de los ofrecidos y que no hubo falsificación. Ya mencionamos que en las entradas de texto de una sola línea, elimina los caracteres de fin de línea que un atacante podría haber enviado. En las entradas multilínea, normaliza los caracteres de fin de línea. Y así sucesivamente. +Por ejemplo, filtra de las entradas todos los caracteres de control y comprueba la validez de la codificación UTF-8, lo que garantiza que los datos del formulario estén siempre limpios. En los select boxes y las listas de radio verifica que los elementos seleccionados estaban realmente entre los ofrecidos y que no hubo ninguna falsificación. Ya hemos mencionado que en los campos de texto de una línea sustituye por espacios los caracteres de fin de línea que un atacante pudiera enviar. En los campos de varias líneas normaliza los caracteres de fin de línea. Etcétera. -Nette resuelve por usted los riesgos de seguridad que muchos programadores ni siquiera saben que existen. +Nette se ocupa por usted de riesgos de seguridad cuya existencia muchos programadores ni siquiera conocen. -El ataque CSRF mencionado consiste en que un atacante atrae a la víctima a una página que ejecuta discretamente una petición en el navegador de la víctima al servidor en el que la víctima ha iniciado sesión, y el servidor cree que la petición fue realizada por la víctima por su propia voluntad. Por lo tanto, Nette evita el envío de formularios POST desde otro dominio. Si por alguna razón desea desactivar la protección y permitir el envío de formularios desde otro dominio, use: +El mencionado ataque CSRF consiste en que un atacante atrae a la víctima a una página que, en silencio, ejecuta en el navegador de la víctima una petición al servidor en el que esta tiene la sesión iniciada. El servidor cree entonces que la petición la hizo la víctima de buen grado. Por eso Nette rechaza los formularios POST enviados desde un origen ajeno; incluso otro subdominio del mismo sitio cuenta como ajeno. Si necesita permitir el envío desde otro origen, desactive la protección con: ```php -$form->allowCrossOrigin(); // ¡ATENCIÓN! ¡Desactiva la protección! +$form->allowCrossOrigin(); // ¡ATENCIÓN! ¡Desactiva la protección por completo! ``` -Esta protección utiliza una cookie SameSite llamada `_nss`. La protección mediante cookie SameSite puede no ser 100% confiable, por lo que es recomendable activar también la protección mediante token: +Eso desactiva la protección para cualquier origen. Para permitir solo orígenes concretos, desactive la protección y verifique usted mismo la cabecera `Origin` contra su propia lista blanca. -```php -$form->addProtection(); -``` +La protección se apoya en la cabecera `Sec-Fetch-Site` del navegador (Fetch Metadata), que el navegador envía automáticamente y que no se puede falsificar ni siquiera con una vulnerabilidad XSS. Para los navegadores antiguos que no las soportan se aplica una cookie SameSite de reserva, que una aplicación Nette establece automáticamente. El artículo [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] lo describe en detalle. -Recomendamos proteger de esta manera los formularios en la parte administrativa del sitio web que modifican datos sensibles en la aplicación. El framework se defiende contra el ataque CSRF generando y verificando un token de autorización que se almacena en la sesión. Por lo tanto, es necesario tener una sesión abierta antes de mostrar el formulario. En la parte administrativa del sitio web, la sesión generalmente ya está iniciada debido al inicio de sesión del usuario. De lo contrario, inicie la sesión con el método `Nette\Http\Session::start()`. +.[note] +La protección anterior, con un token de autorización guardado en la sesión y activada con `$form->addProtection()`, ya no hace falta y está obsoleta desde la versión 3.3. -Mismo formulario en múltiples presenters -======================================== +Usar un mismo formulario en varios presenters +============================================= -Si necesita usar un formulario en múltiples presenters, recomendamos crear una fábrica para él, que luego pasará al presenter. Una ubicación adecuada para tal clase es, por ejemplo, el directorio `app/Forms`. +Si necesita usar el mismo formulario en varios presenters, recomendamos crearle una fábrica que después inyecta en los presenters. Un lugar adecuado para esa clase es, por ejemplo, el directorio `app/Forms`. -La clase de fábrica podría verse así: +La clase fábrica podría tener este aspecto: ```php use Nette\Application\UI\Form; @@ -383,14 +391,14 @@ class SignInFormFactory public function create(): Form { $form = new Form; - $form->addText('name', 'Nombre:'); - $form->addSubmit('send', 'Iniciar sesión'); + $form->addText('name', 'Name:'); + $form->addSubmit('send', 'Log in'); return $form; } } ``` -Solicitamos a la clase que fabrique el formulario en el método de fábrica de componentes en el presenter: +Pedimos la clase que produce el formulario en el método fábrica del componente dentro del presenter: ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // podemos modificar el formulario, aquí por ejemplo cambiamos la etiqueta del botón - $form['send']->setCaption('Continuar'); - $form->onSuccess[] = [$this, 'signInFormSucceeded']; // y agregamos el handler + // podemos cambiar el formulario; aquí, por ejemplo, cambiamos el texto del botón + $form['send']->setCaption('Continue'); + $form->onSuccess[] = $this->signInFormSuceeded(...); // y añadimos el manejador return $form; } ``` -El handler para procesar el formulario también puede ser proporcionado desde la fábrica: +El manejador del procesamiento del formulario lo puede proporcionar también la propia fábrica: ```php use Nette\Application\UI\Form; @@ -418,14 +426,14 @@ class SignInFormFactory public function create(): Form { $form = new Form; - $form->addText('name', 'Nombre:'); - $form->addSubmit('send', 'Iniciar sesión'); + $form->addText('name', 'Name:'); + $form->addSubmit('send', 'Log in'); $form->onSuccess[] = function (Form $form, $data): void { - // aquí realizamos el procesamiento del formulario + // aquí procesamos nuestro formulario enviado }; return $form; } } ``` -Bien, hemos tenido una rápida introducción a los formularios en Nette. Intente mirar también en el directorio [examples|https://github.com/nette/forms/tree/master/examples] en la distribución, donde encontrará más inspiración. +Con esto hemos cubierto una introducción rápida a los formularios en Nette. Pruebe a mirar en el directorio de [ejemplos |https://github.com/nette/forms/tree/master/examples] de la distribución para inspirarse más. diff --git a/forms/es/rendering.texy b/forms/es/rendering.texy index 68d2466002..09541ac008 100644 --- a/forms/es/rendering.texy +++ b/forms/es/rendering.texy @@ -1,17 +1,17 @@ Renderizado de formularios ************************** -La apariencia de los formularios puede ser muy diversa. En la práctica, podemos encontrar dos extremos. Por un lado, está la necesidad de renderizar en la aplicación una serie de formularios que son visualmente similares como dos gotas de agua, y apreciaremos el fácil renderizado sin plantilla usando `$form->render()`. Este suele ser el caso de las interfaces de administración. +El aspecto de los formularios puede ser muy variado. En la práctica podemos encontrarnos con dos extremos. Por un lado está la necesidad de renderizar en una aplicación un montón de formularios visualmente idénticos, y agradecemos el renderizado sencillo sin plantilla con `$form->render()`. Es el caso típico de las interfaces de administración. -Por otro lado, están los formularios diversos donde aplica: cada pieza es original. Su forma se describe mejor usando el lenguaje HTML en la plantilla del formulario. Y, por supuesto, además de ambos extremos mencionados, encontraremos muchos formularios que se mueven en algún punto intermedio. +Por otro lado están los formularios variados, en los que cada uno es único. Su aspecto se describe mejor con HTML en la plantilla del formulario. Y, por supuesto, además de estos dos extremos nos encontramos con muchos formularios que quedan en algún punto intermedio. -Renderizado mediante Latte -========================== +Renderizado con Latte +===================== -El [sistema de plantillas Latte|latte:] facilita fundamentalmente el renderizado de formularios y sus elementos. Primero mostraremos cómo renderizar formularios manualmente, elemento por elemento, y así obtener control total sobre el código. Más adelante mostraremos cómo se puede [automatizar |#Renderizado automático] dicho renderizado. +El [sistema de plantillas Latte |latte:] simplifica notablemente el renderizado de los formularios y de sus elementos. Primero mostraremos cómo renderizar un formulario a mano, elemento por elemento, para tener pleno control sobre el código. Más adelante veremos cómo se puede [automatizar |#Renderizado automático] ese renderizado. -Puede hacer que el diseño de la plantilla Latte del formulario se genere usando el método `Nette\Forms\Blueprint::latte($form)`, que lo imprimirá en la página del navegador. Luego, simplemente haga clic para seleccionar el código y cópielo en su proyecto. .{data-version:3.1.15} +La plantilla Latte del formulario se puede generar con el método `Nette\Forms\Blueprint::latte($form)`, que la imprime en la página del navegador. Después basta con seleccionar el código con un clic y copiarlo a su proyecto. .{data-version:3.1.15} `{control}` @@ -23,13 +23,13 @@ La forma más sencilla de renderizar un formulario es escribir en la plantilla: {control signInForm} ``` -Se puede influir en la apariencia del formulario renderizado de esta manera configurando el [#Renderer] y los [elementos individuales |#Atributos HTML]. +El aspecto del formulario renderizado se puede influir configurando el [#Renderer] y los [distintos elementos |#Atributos HTML]. `n:name` -------- -La definición del formulario en el código PHP se puede vincular muy fácilmente con el código HTML. Simplemente agregue los atributos `n:name`. ¡Así de fácil! +Enlazar la definición del formulario en el código PHP con el código HTML es extremadamente fácil. Basta con añadir los atributos `n:name`. ¡Así de sencillo! ```php protected function createComponentSignInForm(): Form @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Tiene control total sobre la forma del código HTML resultante. Si usa el atributo `n:name` en los elementos `<select>`, `<button>` o `<textarea>`, su contenido interno se completará automáticamente. La etiqueta `<form n:name>` además crea una variable local `$form` con el objeto del formulario que se está renderizando y el cierre `</form>` renderiza todos los elementos ocultos no renderizados (lo mismo aplica a `{form} ... {/form}`). +Tiene pleno control sobre el aspecto del código HTML resultante. Si usa el atributo `n:name` con los elementos `<select>`, `<button>` o `<textarea>`, su contenido interno se rellena automáticamente. Además, la etiqueta `<form n:name>` crea una variable local `$form` con el objeto del formulario renderizado, y la etiqueta de cierre `</form>` renderiza todos los elementos ocultos que no se hayan renderizado (lo mismo vale para `{form} ... {/form}`). -Sin embargo, no debemos olvidar renderizar los posibles mensajes de error. Tanto los que se agregaron a elementos individuales con el método `addError()` (usando `{inputError}`), como los agregados directamente al formulario (devueltos por `$form->getOwnErrors()`): +Pero no debemos olvidarnos de renderizar los posibles mensajes de error. Tanto los añadidos a los distintos elementos con el método `addError()` (que se renderizan con `{inputError}`) como los añadidos directamente al formulario (que devuelve `$form->getOwnErrors()`): ```latte <form n:name=signInForm class=form> @@ -80,7 +80,7 @@ Sin embargo, no debemos olvidar renderizar los posibles mensajes de error. Tanto </form> ``` -Los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar así por elementos individuales: +Los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar elemento a elemento así: ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ Los elementos de formulario más complejos, como RadioList o CheckboxList, se pu `{label}` `{input}` ------------------- -¿No quiere pensar para cada elemento qué elemento HTML usar en la plantilla, si `<input>`, `<textarea>`, etc.? La solución es la etiqueta universal `{input}`: +¿Prefiere no pensar en la plantilla qué elemento HTML usar para cada elemento del formulario, si `<input>`, `<textarea>`, etc.? La solución es la etiqueta universal `{input}`: ```latte <form n:name=signInForm class=form> @@ -114,9 +114,9 @@ Los elementos de formulario más complejos, como RadioList o CheckboxList, se pu </form> ``` -Si el formulario utiliza un traductor, el texto dentro de las etiquetas `{label}` será traducido. +Si el formulario usa un traductor, las etiquetas renderizadas a partir de la definición del formulario (p. ej. `{label username /}`) se traducen. El texto escrito directamente entre las etiquetas `{label}` y `{/label}` no. -Incluso en este caso, los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar por elementos individuales: +También aquí, los elementos de formulario más complejos, como RadioList o CheckboxList, se pueden renderizar elemento a elemento: ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ Incluso en este caso, los elementos de formulario más complejos, como RadioList {/foreach} ``` -Para renderizar solo el `<input>` en el elemento Checkbox, use `{input myCheckbox:}`. Los atributos HTML en este caso siempre se separan con coma `{input myCheckbox:, class: required}`. +Para renderizar solo el `<input>` de un elemento Checkbox, use `{input myCheckbox:}`. En ese caso, separe siempre los atributos HTML con una coma: `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Muestra el mensaje de error para un elemento de formulario, si tiene alguno. El mensaje generalmente se envuelve en un elemento HTML para estilizarlo. Evitar renderizar un elemento vacío si no hay mensaje se puede hacer elegantemente usando `n:ifcontent`: +Muestra el mensaje de error de un elemento del formulario, si existe. El mensaje se suele envolver en un elemento HTML para darle estilo. Evitar que se renderice un elemento vacío cuando no hay mensaje se consigue elegantemente con `n:ifcontent`: ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -Podemos verificar la presencia de un error con el método `hasErrors()` y establecer la clase del elemento padre en consecuencia: +La presencia de un error se puede comprobar con el método `hasErrors()` y establecer en consecuencia la clase del elemento padre: ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ Podemos verificar la presencia de un error con el método `hasErrors()` y establ `{form}` -------- -Las etiquetas `{form signInForm}...{/form}` son una alternativa a `<form n:name="signInForm">...</form>`. +Las etiquetas `{form signInForm}...{/form}` son una alternativa a `<form n:name="signInForm">...</form>`. Los posibles argumentos se separan del nombre con una coma: `{form signInForm, class: foo}`. + +.{data-version:3.3.0} +La palabra clave `scope` colocada delante del nombre solo mete el formulario en la pila (para que `{input}`, `{label}`, etc. se enlacen con él), pero no renderiza la etiqueta `<form>`. Resulta práctica para renderizar una parte del formulario, p. ej. en un snippet. Si ya hay un formulario activo, el nombre se resuelve de forma relativa a él, así que `{form scope}` sustituye también a `{formContainer}`: + +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +La palabra clave `detached` renderiza un `<form></form>` vacío y enlaza con él todos los elementos mediante el atributo HTML `form`. Eso le permite colocar un formulario dentro de otro formulario, cosa que HTML normalmente prohíbe. El formulario detached debe tener un `id` HTML, que se genera automáticamente cuando le da un nombre (como `outerForm` más abajo): + +```latte +{form detached outerForm} + ... +{/form} +``` Renderizado automático ---------------------- -Gracias a las etiquetas `{input}` y `{label}`, podemos crear fácilmente una plantilla genérica para cualquier formulario. Iterará y renderizará gradualmente todos sus elementos, excepto los elementos ocultos, que se renderizarán automáticamente al cerrar el formulario con la etiqueta `</form>`. Esperará el nombre del formulario a renderizar en la variable `$form`. +Gracias a las etiquetas `{input}` y `{label}` podemos crear fácilmente una plantilla genérica para cualquier formulario. Recorrerá y renderizará todos sus elementos, salvo los ocultos, que se renderizan automáticamente al cerrar el formulario con la etiqueta `</form>`. Espera el nombre del formulario a renderizar en la variable `$form`. ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ Gracias a las etiquetas `{input}` y `{label}`, podemos crear fácilmente una pla </form> ``` -Las etiquetas pares autocerradas `{label .../}` utilizadas muestran las etiquetas provenientes de la definición del formulario en el código PHP. +Las etiquetas pareadas autocerradas `{label .../}` que se usan aquí muestran las etiquetas que provienen de la definición del formulario en el código PHP. -Guarde esta plantilla genérica, por ejemplo, en el archivo `basic-form.latte` y para renderizar el formulario, simplemente inclúyala y pase el nombre (o instancia) del formulario al parámetro `$form`: +Guarde esta plantilla genérica, por ejemplo, en el archivo `basic-form.latte`. Para renderizar el formulario basta con incluirla y pasar el nombre del formulario (o su instancia) al parámetro `$form`: ```latte {include basic-form.latte, form: signInForm} ``` -Si al renderizar un formulario específico quisiera intervenir en su forma y, por ejemplo, renderizar un elemento de manera diferente, entonces la forma más sencilla es preparar bloques en la plantilla que luego se puedan sobrescribir. Los bloques también pueden tener [nombres dinámicos |latte:template-inheritance#Nombres de bloque dinámicos], por lo que también se puede insertar el nombre del elemento que se está renderizando. Por ejemplo: +Si quiere modificar el aspecto de un formulario concreto al renderizarlo, y quizá renderizar un elemento de otra manera, lo más fácil es preparar en la plantilla bloques que se puedan sobrescribir después. Los bloques también pueden tener [nombres dinámicos |latte:template-inheritance#Nombres de bloque dinámicos], lo que le permite insertar el nombre del elemento renderizado. Por ejemplo: ```latte ... @@ -189,7 +207,7 @@ Si al renderizar un formulario específico quisiera intervenir en su forma y, po ... ``` -Para un elemento, por ejemplo, `username`, se creará el bloque `input-username`, que se puede sobrescribir fácilmente usando la etiqueta [{embed} |latte:template-inheritance#Herencia de unidades embed]: +Para un elemento llamado, p. ej., `username`, se crea así el bloque `input-username`, que se puede sobrescribir fácilmente con la etiqueta [{embed} |latte:template-inheritance#Unit Inheritance]: ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ Para un elemento, por ejemplo, `username`, se creará el bloque `input-username` {/embed} ``` -Alternativamente, todo el contenido de la plantilla `basic-form.latte` se puede [definir |latte:template-inheritance#Definiciones define] como un bloque, incluido el parámetro `$form`: +Alternativamente, todo el contenido de la plantilla `basic-form.latte` se puede [definir |latte:template-inheritance#Definitions] como un bloque, incluido el parámetro `$form`: ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ Alternativamente, todo el contenido de la plantilla `basic-form.latte` se puede {/define} ``` -Gracias a esto, su llamada será ligeramente más simple: +Eso hace su llamada un poco más simple: ```latte {embed basic-form, signInForm} @@ -219,7 +237,7 @@ Gracias a esto, su llamada será ligeramente más simple: {/embed} ``` -El bloque solo necesita importarse en un solo lugar, al principio de la plantilla de layout: +El bloque solo hace falta importarlo en un sitio, al principio de la plantilla del layout: ```latte {import basic-form.latte} @@ -229,7 +247,7 @@ El bloque solo necesita importarse en un solo lugar, al principio de la plantill Casos especiales ---------------- -Si necesita renderizar solo la parte interna del formulario sin las etiquetas HTML `<form>`, por ejemplo, al enviar snippets, ocúltelas usando el atributo `n:tag-if`: +Si necesita renderizar solo la parte interna del formulario sin las etiquetas HTML `<form>`, por ejemplo al enviar snippets, ocúltelas con el atributo `n:tag-if`: ```latte <form n:name=signInForm n:tag-if=false> @@ -240,10 +258,10 @@ Si necesita renderizar solo la parte interna del formulario sin las etiquetas HT </form> ``` -La etiqueta `{formContainer}` ayuda a renderizar elementos dentro de un contenedor de formulario. +Con el renderizado de los elementos que hay dentro de un contenedor del formulario ayuda la etiqueta `{formContainer}`, o la más reciente [`{form scope}` |#{form}]. ```latte -<p>Qué noticias desea recibir:</p> +<p>Which news you wish to receive:</p> {formContainer emailNews} <ul> @@ -257,39 +275,39 @@ La etiqueta `{formContainer}` ayuda a renderizar elementos dentro de un contened Renderizado sin Latte ===================== -La forma más sencilla de renderizar un formulario es llamar a: +La forma más fácil de renderizar un formulario es llamar a: ```php $form->render(); ``` -Se puede influir en la apariencia del formulario renderizado de esta manera configurando el [#Renderer] y los [elementos individuales |#Atributos HTML]. +El aspecto del formulario renderizado se puede influir configurando el [#Renderer] y los [distintos elementos |#Atributos HTML]. Renderizado manual ------------------ -Cada elemento de formulario dispone de métodos que generan el código HTML del campo de formulario y la etiqueta. Pueden devolverlo como una cadena o como un objeto [Nette\Utils\Html|utils:html-elements]: +Cada elemento del formulario tiene métodos que generan el código HTML del campo y de su etiqueta. Pueden devolverlo como cadena o como objeto [Nette\Utils\Html |utils:html-elements]: - `getControl(): Html|string` devuelve el código HTML del elemento - `getLabel($caption = null): Html|string|null` devuelve el código HTML de la etiqueta, si existe -Así, el formulario se puede renderizar elemento por elemento: +Eso permite renderizar el formulario elemento por elemento: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,46 +315,46 @@ Así, el formulario se puede renderizar elemento por elemento: <?php $form->render('end') ?> ``` -Mientras que para algunos elementos `getControl()` devuelve un único elemento HTML (por ejemplo, `<input>`, `<select>`, etc.), para otros devuelve una pieza completa de código HTML (CheckboxList, RadioList). En tal caso, puede utilizar métodos que generan inputs y etiquetas individuales, para cada elemento por separado: +Mientras que en algunos elementos `getControl()` devuelve un único elemento HTML (p. ej. `<input>`, `<select>`, etc.), en otros devuelve un fragmento completo de código HTML (CheckboxList, RadioList). En esos casos puede usar los métodos que generan por separado los distintos inputs y etiquetas de cada ítem: -- `getControlPart($key = null): ?Html` devuelve el código HTML de un elemento -- `getLabelPart($key = null): ?Html` devuelve el código HTML de la etiqueta de un elemento +- `getControlPart($key = null): Html` devuelve el código HTML de un solo ítem +- `getLabelPart($key = null): Html` devuelve el código HTML de la etiqueta de un solo ítem .[note] -Estos métodos tienen el prefijo `get` por razones históricas, pero `generate` sería mejor, porque en cada llamada crean y devuelven un nuevo elemento `Html`. +Estos métodos llevan el prefijo `get` por motivos históricos, pero `generate` sería más adecuado, ya que crean y devuelven un elemento `Html` nuevo en cada llamada. Renderer ======== -Es un objeto que se encarga de renderizar el formulario. Se puede establecer mediante el método `$form->setRenderer`. Se le pasa el control cuando se llama al método `$form->render()`. +Es un objeto que se encarga de renderizar el formulario. Se establece con el método `$form->setRenderer()`. Se le pasa el control cuando se llama al método `$form->render()`. -Si no establecemos nuestro propio renderer, se utilizará el renderer predeterminado [api:Nette\Forms\Rendering\DefaultFormRenderer]. Este renderiza los elementos del formulario en forma de tabla HTML. La salida se ve así: +Si no establecemos un renderer propio, se usará el renderer predeterminado [api:Nette\Forms\Rendering\DefaultFormRenderer]. Este renderiza los elementos del formulario en una tabla HTML. La salida tiene este aspecto: ```latte <table> <tr class="required"> - <th><label class="required" for="frm-name">Nombre:</label></th> + <th><label class="required" for="frm-name">Name:</label></th> <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> </tr> <tr class="required"> - <th><label class="required" for="frm-age">Edad:</label></th> + <th><label class="required" for="frm-age">Age:</label></th> <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> </tr> <tr> - <th><label>Género:</label></th> + <th><label>Gender:</label></th> ... ``` -Si usar o no una tabla para la estructura del formulario es discutible y muchos diseñadores web prefieren otro marcado. Por ejemplo, una lista de definición. Por lo tanto, reconfiguraremos `DefaultFormRenderer` para que renderice el formulario en forma de lista. La configuración se realiza editando el array [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. El primer índice siempre representa el área y el segundo su atributo. Las áreas individuales se muestran en la imagen: +Si usar o no una tabla para la estructura del formulario es discutible, y muchos diseñadores web prefieren otro marcado, por ejemplo una lista de definiciones. Por eso reconfiguraremos `DefaultFormRenderer` para que renderice el formulario como una lista. La configuración se hace editando el array [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. El primer índice representa siempre un área y el segundo su atributo. Las distintas áreas se ven en la imagen: -[* defaultformrenderer-es.webp *] *** Áreas del formulario al usar DefaultFormRenderer *** +[* form-areas-en.webp *] -Por defecto, el grupo de elementos `controls` está envuelto en una tabla `<table>`, cada `pair` representa una fila de la tabla `<tr>` y el par `label` y `control` son celdas `<th>` y `<td>`. Ahora cambiaremos los elementos envolventes. El área `controls` la insertaremos en un contenedor `<dl>`, el área `pair` la dejaremos sin contenedor, `label` la insertaremos en `<dt>` y finalmente `control` la envolveremos con etiquetas `<dd>`: +De forma predeterminada, el grupo `controls` está envuelto en `<table>`, cada `pair` representa una fila de la tabla `<tr>` y el par `label` y `control` son las celdas `<th>` y `<td>`. Ahora cambiaremos los elementos envolventes. Colocaremos el área `controls` en un contenedor `<dl>`, dejaremos el área `pair` sin contenedor, pondremos la `label` en `<dt>` y, por último, envolveremos el `control` con las etiquetas `<dd>`: ```php $renderer = $form->getRenderer(); @@ -348,166 +366,166 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -El resultado es este código HTML: +El resultado es el siguiente código HTML: ```latte <dl> - <dt><label class="required" for="frm-name">Nombre:</label></dt> + <dt><label class="required" for="frm-name">Name:</label></dt> <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - <dt><label class="required" for="frm-age">Edad:</label></dt> + <dt><label class="required" for="frm-age">Age:</label></dt> <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - <dt><label>Género:</label></dt> + <dt><label>Gender:</label></dt> ... </dl> ``` -En el array wrappers se pueden influir en muchos otros atributos: +El array wrappers permite influir en muchos otros atributos: -- agregar clases CSS a tipos individuales de elementos de formulario -- distinguir con clase CSS las filas pares e impares -- distinguir visualmente los elementos obligatorios y opcionales -- determinar si los mensajes de error se muestran directamente junto a los elementos o sobre el formulario +- añadir clases CSS a los distintos tipos de elementos del formulario +- distinguir con clases CSS las filas pares e impares +- distinguir visualmente los ítems obligatorios de los opcionales +- determinar si los mensajes de error se muestran directamente junto a los elementos o encima del formulario Opciones -------- -El comportamiento del Renderer también se puede controlar estableciendo *options* en los elementos de formulario individuales. Así se puede establecer la descripción que se mostrará junto al campo de entrada: +El comportamiento del Renderer también se puede controlar estableciendo *opciones* en los distintos elementos del formulario. Así puede establecer una descripción que aparece junto al campo de entrada: ```php -$form->addText('phone', 'Número:') - ->setOption('description', 'Este número permanecerá oculto'); +$form->addText('phone', 'Number:') + ->setOption('description', 'This number will remain hidden'); ``` -Si queremos colocar contenido HTML en él, utilizaremos la clase [Html |utils:html-elements]: +Si queremos poner ahí contenido HTML, usamos la clase [Html |utils:html-elements]: ```php use Nette\Utils\Html; -$form->addText('phone', 'Número:') +$form->addText('phone', 'Phone:') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Condiciones de almacenamiento de su número</a>') + ->setHtml('<a href="...">Terms of service.</a>') ); ``` .[tip] -El elemento Html también se puede usar en lugar de la etiqueta: `$form->addCheckbox('conditions', $label)`. +Un elemento Html se puede usar también en lugar de la etiqueta: `$form->addCheckbox('conditions', $label)`. -Agrupación de elementos ------------------------ +Agrupar elementos +----------------- -El Renderer permite agrupar elementos en grupos visuales (fieldsets): +El Renderer permite agrupar los elementos en grupos visuales (fieldsets): ```php -$form->addGroup('Datos personales'); +$form->addGroup('Personal data'); ``` -Después de crear un nuevo grupo, este se vuelve activo y cada elemento recién agregado también se agrega a él. Por lo tanto, el formulario se puede construir de esta manera: +Tras crear un grupo nuevo, este pasa a estar activo, y cada elemento recién añadido se añade también a él. Así que el formulario se puede construir de esta manera: ```php $form = new Form; -$form->addGroup('Datos personales'); -$form->addText('name', 'Su nombre:'); -$form->addInteger('age', 'Su edad:'); +$form->addGroup('Personal data'); +$form->addText('name', 'Your name:'); +$form->addInteger('age', 'Your age:'); $form->addEmail('email', 'Email:'); -$form->addGroup('Dirección de envío'); -$form->addCheckbox('send', 'Enviar a dirección'); -$form->addText('street', 'Calle:'); -$form->addText('city', 'Ciudad:'); -$form->addSelect('country', 'País:', $countries); +$form->addGroup('Shipping address'); +$form->addCheckbox('send', 'Ship to address'); +$form->addText('street', 'Street:'); +$form->addText('city', 'City:'); +$form->addSelect('country', 'Country:', $countries); ``` -El Renderer primero renderiza los grupos y solo después los elementos que no pertenecen a ningún grupo. +El renderer dibuja primero los grupos y después los elementos que no pertenecen a ningún grupo. -Soporte para Bootstrap ----------------------- +Soporte de Bootstrap +-------------------- -[En los ejemplos |https://github.com/nette/forms/tree/master/examples] encontrará ejemplos de cómo configurar el Renderer para [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] y [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. +En el [directorio de ejemplos |https://github.com/nette/forms/tree/master/examples] encontrará ejemplos de cómo configurar el Renderer para [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] y [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. Atributos HTML ============== -Para establecer cualquier atributo HTML de los elementos de formulario, usamos el método `setHtmlAttribute(string $name, $value = true)`: +Para establecer cualquier atributo HTML de los elementos del formulario, use el método `setHtmlAttribute(string $name, $value = true)`: ```php -$form->addInteger('number', 'Número:') +$form->addInteger('number', 'Number:') ->setHtmlAttribute('class', 'big-number'); -$form->addSelect('rank', 'Ordenar por:', ['precio', 'nombre']) - ->setHtmlAttribute('onchange', 'submit()'); // enviar al cambiar +$form->addSelect('rank', 'Order by:', ['price', 'name']) + ->setHtmlAttribute('onchange', 'submit()'); // envía el formulario al cambiar -// Para establecer atributos del propio <form> +// Para establecer los atributos del propio elemento <form> $form->setHtmlAttribute('id', 'myForm'); ``` -Especificación del tipo de elemento: +Especificar el tipo del elemento: ```php -$form->addText('tel', 'Su teléfono:') +$form->addText('tel', 'Your telephone:') ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'escriba el teléfono'); + ->setHtmlAttribute('placeholder', 'Please, fill in your telephone'); ``` .[warning] -Establecer el tipo y otros atributos sirve solo para fines visuales. La verificación de la corrección de las entradas debe realizarse en el servidor, lo que se asegura eligiendo el [elemento de formulario|controls] adecuado e indicando las [reglas de validación|validation]. +Establecer el tipo y otros atributos solo tiene fines visuales. La verificación de que la entrada es correcta debe ocurrir en el lado del servidor, algo que consigue eligiendo un [elemento de formulario |controls] adecuado e indicando [reglas de validación |validation]. -Podemos establecer atributos HTML con valores diferentes para cada uno de los elementos individuales en listas de radio o checkbox. Observe los dos puntos después de `style:`, que aseguran la elección del valor según la clave: +En los distintos ítems de las listas de radio o de checkbox podemos establecer un atributo HTML con valores diferentes para cada uno. Fíjese en los dos puntos tras `style:`, que hacen que el valor se seleccione según la clave: ```php -$colors = ['r' => 'rojo', 'g' => 'verde', 'b' => 'azul']; +$colors = ['r' => 'red', 'g' => 'green', 'b' => 'blue']; $styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Colores:', $colors) +$form->addCheckboxList('colors', 'Colors:', $colors) ->setHtmlAttribute('style:', $styles); ``` -Imprime: +Renderiza: ```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">rojo</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">azul</label> +<label><input type="checkbox" name="colors[]" style="background:red" value="r">red</label> +<label><input type="checkbox" name="colors[]" style="background:green" value="g">green</label> +<label><input type="checkbox" name="colors[]" value="b">blue</label> ``` -Para establecer atributos booleanos, como `readonly`, podemos usar la notación con un signo de interrogación: +Para establecer atributos booleanos, como `readonly`, podemos usar la notación con signo de interrogación: ```php -$form->addCheckboxList('colors', 'Colores:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // para múltiples claves use un array, ej. ['r', 'g'] +$form->addCheckboxList('colors', 'Colors:', $colors) + ->setHtmlAttribute('readonly?', 'r'); // para varias claves use un array, p. ej. ['r', 'g'] ``` -Imprime: +Renderiza: ```latte -<label><input type="checkbox" name="colors[]" readonly value="r">rojo</label> -<label><input type="checkbox" name="colors[]" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">azul</label> +<label><input type="checkbox" name="colors[]" readonly value="r">red</label> +<label><input type="checkbox" name="colors[]" value="g">green</label> +<label><input type="checkbox" name="colors[]" value="b">blue</label> ``` -En el caso de los selectbox, el método `setHtmlAttribute()` establece los atributos del elemento `<select>`. Si queremos establecer atributos para los `<option>` individuales, usamos el método `setOptionAttribute()`. También funcionan las notaciones con dos puntos y signo de interrogación mencionadas anteriormente: +En las listas desplegables, el método `setHtmlAttribute()` establece los atributos del elemento `<select>`. Si queremos establecer los atributos de los distintos elementos `<option>`, usamos el método `setOptionAttribute()`. Las notaciones con dos puntos y con signo de interrogación mencionadas arriba también funcionan: ```php -$form->addSelect('colors', 'Colores:', $colors) +$form->addSelect('colors', 'Colors:', $colors) ->setOptionAttribute('style:', $styles); ``` -Imprime: +Renderiza: ```latte <select name="colors"> - <option value="r" style="background:red">rojo</option> - <option value="g" style="background:green">verde</option> - <option value="b">azul</option> + <option value="r" style="background:red">red</option> + <option value="g" style="background:green">green</option> + <option value="b">blue</option> </select> ``` @@ -515,22 +533,22 @@ Imprime: Prototipos ---------- -Una forma alternativa de establecer atributos HTML consiste en modificar la plantilla a partir de la cual se genera el elemento HTML. La plantilla es un objeto `Html` y la devuelve el método `getControlPrototype()`: +Otra forma de establecer atributos HTML es modificar la plantilla a partir de la cual se genera el elemento HTML. La plantilla es un objeto `Html` y la devuelve el método `getControlPrototype()`: ```php -$input = $form->addInteger('number', 'Número:'); +$input = $form->addInteger('number', 'Number:'); $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -De esta manera, también se puede modificar la plantilla de la etiqueta, que devuelve `getLabelPrototype()`: +De esta manera se puede modificar también la plantilla de la etiqueta, que devuelve `getLabelPrototype()`: ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -En los elementos Checkbox, CheckboxList y RadioList, puede influir en la plantilla del elemento que envuelve todo el elemento. La devuelve `getContainerPrototype()`. En el estado predeterminado, es un elemento "vacío", por lo que no se renderiza nada, pero al establecerle un nombre, se renderizará: +En los elementos Checkbox, CheckboxList y RadioList puede influir en la plantilla del elemento que envuelve todo el control. La devuelve `getContainerPrototype()`. De forma predeterminada es un elemento "vacío", así que no se renderiza nada, pero si le da un nombre se renderizará: ```php $input = $form->addCheckbox('send'); @@ -541,49 +559,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -En el caso de CheckboxList y RadioList, también se puede influir en la plantilla del separador de elementos individuales, que devuelve el método `getSeparatorPrototype()`. En el estado predeterminado, es el elemento `<br>`. Si lo cambia a un elemento par, envolverá los elementos individuales en lugar de separarlos. Y además, se puede influir en la plantilla del elemento HTML de la etiqueta en los elementos individuales, que devuelve `getItemLabelPrototype()`. +En el caso de CheckboxList y RadioList puede influir también en la plantilla del separador de los distintos ítems, que devuelve el método `getSeparatorPrototype()`. De forma predeterminada es el elemento `<br>`. Si lo cambia por un elemento pareado, envolverá los distintos ítems en lugar de separarlos. Además, puede influir en la plantilla del elemento HTML de las etiquetas de los distintos ítems, que devuelve `getItemLabelPrototype()`. Traducción ========== -Si programa una aplicación multilingüe, probablemente necesitará renderizar el formulario en diferentes versiones lingüísticas. Nette Framework define para este propósito una interfaz para la traducción [api:Nette\Localization\Translator]. En Nette no hay una implementación predeterminada, puede elegir según sus necesidades entre varias soluciones listas que encontrará en [Componette |https://componette.org/search/localization]. En su documentación aprenderá cómo configurar el traductor. +Si desarrolla una aplicación multilingüe, probablemente necesitará renderizar el formulario en distintas versiones de idioma. Nette Framework define para ello una interfaz de traducción: [api:Nette\Localization\Translator]. Nette no trae ninguna implementación predeterminada; puede elegir entre varias soluciones ya hechas que encontrará en [Componette |https://componette.org/search/localization] según sus necesidades. En su documentación aprenderá cómo configurar el traductor. -Los formularios admiten la impresión de textos a través del traductor. Se lo pasamos usando el método `setTranslator()`: +Los formularios soportan la salida de textos a través del traductor. Se lo pasamos con el método `setTranslator()`: ```php $form->setTranslator($translator); ``` -A partir de este momento, no solo todas las etiquetas, sino también todos los mensajes de error o elementos de select boxes se traducirán a otro idioma. +A partir de ese momento se traducirán al idioma de destino no solo todas las etiquetas, sino también todos los mensajes de error, los ítems de las listas desplegables y los placeholders de los campos. -Para elementos de formulario individuales, es posible establecer un traductor diferente o desactivar completamente la traducción con el valor `null`: +Es posible establecer un traductor distinto para elementos concretos del formulario, o desactivar la traducción por completo poniendo el valor `null`: ```php -$form->addSelect('carModel', 'Modelo:', $cars) +$form->addSelect('carModel', 'Model:', $cars) ->setTranslator(null); ``` -Para las [reglas de validación|validation], también se pasan parámetros específicos al traductor, por ejemplo, para la regla: +En las [reglas de validación |validation] se le pasan al traductor además parámetros concretos. Por ejemplo, para la regla: ```php -$form->addPassword('password', 'Contraseña:') - ->addRule($form::MinLength, 'La contraseña debe tener al menos %d caracteres', 8); +$form->addPassword('password', 'Password:') + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8); ``` -se llama al traductor con estos parámetros: +el traductor se llama con estos parámetros: ```php -$translator->translate('La contraseña debe tener al menos %d caracteres', 8); +$translator->translate('Password must be at least %d characters long', 8); ``` -y, por lo tanto, puede elegir la forma plural correcta de la palabra `caracteres` según el número. +y así puede elegir la forma plural correcta de la palabra `characters` según el número. Evento onRender =============== -Justo antes de que se renderice el formulario, podemos hacer que se llame nuestro código. Este puede, por ejemplo, complementar los elementos del formulario con clases HTML para una correcta visualización. Agregamos el código al array `onRender`: +Justo antes de renderizar el formulario podemos hacer que se ejecute nuestro código. Ese código puede, por ejemplo, añadir clases HTML a los elementos del formulario para que se muestren correctamente. Añadimos el código al array `onRender`: ```php $form->onRender[] = function ($form) { diff --git a/forms/es/standalone.texy b/forms/es/standalone.texy index 15159afe91..2457426aca 100644 --- a/forms/es/standalone.texy +++ b/forms/es/standalone.texy @@ -1,43 +1,49 @@ -Formularios utilizados de forma independiente -********************************************* +Formularios independientes +************************** .[perex] -Nette Forms facilita enormemente la creación y el procesamiento de formularios web. Puede usarlos en sus aplicaciones de forma completamente independiente del resto del framework, como mostraremos en este capítulo. +Nette Forms simplifica enormemente la creación y el procesamiento de formularios web. Puede usarlos en sus aplicaciones de forma completamente independiente, sin el resto del framework, como se muestra en este capítulo. -Pero si utiliza Nette Application y presenters, la guía para [uso en presenters|in-presenter] es para usted. +Si usa Nette Application y presenters, en cambio, tiene una guía dedicada: [formularios en presenters |in-presenter]. Primer formulario ================= -Intentemos escribir un formulario de registro simple. Su código será el siguiente ("código completo":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): +Antes de empezar, instale el paquete con [Composer |best-practices:composer]: + +```shell +composer require nette/forms +``` + +Probemos a escribir un formulario de registro sencillo. Su código será el siguiente ("código completo":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php use Nette\Forms\Form; $form = new Form; -$form->addText('name', 'Nombre:'); -$form->addPassword('password', 'Contraseña:'); -$form->addSubmit('send', 'Registrarse'); +$form->addText('name', 'Name:'); +$form->addPassword('password', 'Password:'); +$form->addSubmit('send', 'Sign up'); ``` -Lo renderizamos muy fácilmente: +Y rendericémoslo de forma muy sencilla: ```php $form->render(); ``` -y en el navegador se mostrará así: +El resultado en el navegador debería tener este aspecto: -[* form-es.webp *] *** Formulario de registro simple *** +[* form-en.webp *] -El formulario es un objeto de la clase `Nette\Forms\Form` (la clase `Nette\Application\UI\Form` se usa en presenters). Le hemos añadido los llamados elementos nombre, contraseña y un botón de envío. +El formulario es un objeto de la clase `Nette\Forms\Form` (en los presenters se usa la clase `Nette\Application\UI\Form`). Le hemos añadido elementos llamados 'name' y 'password' y un botón de envío. -Y ahora vamos a darle vida al formulario. Preguntando a `$form->isSuccess()` averiguamos si el formulario fue enviado y si se rellenó válidamente. Si es así, mostramos los datos. Detrás de la definición del formulario, añadimos: +Ahora demos vida al formulario. Consultando `$form->isSuccess()` averiguamos si el formulario se envió y si se rellenó de forma válida. Si es así, mostraremos los datos. Después de la definición del formulario añada: ```php if ($form->isSuccess()) { - echo 'El formulario se rellenó correctamente y se envió'; + echo 'Form was filled and submitted successfully'; $data = $form->getValues(); // $data->name contiene el nombre // $data->password contiene la contraseña @@ -45,32 +51,32 @@ if ($form->isSuccess()) { } ``` -El método `getValues()` devuelve los datos enviados en forma de objeto [ArrayHash |utils:arrays#ArrayHash]. Mostraremos cómo cambiar esto [más adelante |#Mapeo a clases]. El objeto `$data` contiene las claves `name` y `password` con los datos que el usuario rellenó. +El método `getValues()` devuelve los datos enviados como objeto [ArrayHash |utils:arrays#ArrayHash]. Mostraremos cómo cambiarlo [más adelante |#Mapeo a clases]. El objeto `$data` contiene las claves `name` y `password` con los datos introducidos por el usuario. -Normalmente, enviamos los datos directamente para su posterior procesamiento, que puede ser, por ejemplo, insertarlos en la base de datos. Sin embargo, durante el procesamiento puede ocurrir un error, por ejemplo, que el nombre de usuario ya esté ocupado. En tal caso, devolvemos el error al formulario usando `addError()` y dejamos que se renderice de nuevo, junto con el mensaje de error. +Normalmente enviamos los datos directamente a su procesamiento posterior, por ejemplo a insertarlos en la base de datos. Durante el procesamiento puede producirse un error, sin embargo, por ejemplo si el nombre de usuario ya está ocupado. En ese caso devolvemos el error al formulario con `addError()` y dejamos que se renderice otra vez, junto con el mensaje de error. ```php -$form->addError('Lo sentimos, este nombre de usuario ya está en uso.'); +$form->addError('Sorry, this username is already taken.'); ``` -Después de procesar el formulario, redirigimos a la página siguiente. Esto evita el reenvío no deseado del formulario con el botón *actualizar*, *atrás* o moviéndose en el historial del navegador. +Tras procesar el formulario redirigimos a la página siguiente. Eso evita que el formulario se reenvíe involuntariamente al pulsar los botones *actualizar* o *atrás*, o al navegar por el historial del navegador. -El formulario se envía por defecto mediante el método POST y a la misma página. Ambos se pueden cambiar: +De forma predeterminada, el formulario se envía por el método POST a la misma página. Ambas cosas se pueden cambiar: ```php $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -Y eso es todo :-) Tenemos un formulario funcional y perfectamente [seguro |#Protección contra vulnerabilidades]. +Y eso es básicamente todo :-) Tenemos un formulario funcional y perfectamente [protegido |#Protección frente a vulnerabilidades]. -Intente añadir también otros [elementos de formulario|controls]. +Pruebe a añadir también otros [elementos de formulario |controls]. Acceso a los elementos ====================== -Llamamos componentes tanto al formulario como a sus elementos individuales. Forman un árbol de componentes, donde la raíz es precisamente el formulario. Podemos acceder a los elementos individuales del formulario de esta manera: +El formulario y sus distintos elementos se llaman componentes. Forman un árbol de componentes cuya raíz es el formulario. A los distintos elementos del formulario se accede así: ```php $input = $form->getComponent('name'); @@ -80,7 +86,7 @@ $button = $form->getComponent('send'); // sintaxis alternativa: $button = $form['send']; ``` -Los elementos se eliminan usando `unset`: +Los elementos se eliminan con `unset`: ```php unset($form['name']); @@ -90,26 +96,26 @@ unset($form['name']); Reglas de validación ==================== -Se mencionó la palabra *válido,* pero el formulario aún no tiene reglas de validación. Vamos a corregirlo. +Hemos mencionado la palabra *válido*, pero el formulario todavía no tiene ninguna regla de validación. Arreglémoslo. -El nombre será obligatorio, por lo que lo marcamos con el método `setRequired()`, cuyo argumento es el texto del mensaje de error que se mostrará si el usuario no rellena el nombre. Si no se proporciona el argumento, se utilizará el mensaje de error predeterminado. +El nombre será obligatorio, así que lo marcamos con el método `setRequired()`. Su argumento es el texto del mensaje de error que se muestra si el usuario no rellena el nombre. Si no se indica ningún argumento, se usa el mensaje de error predeterminado. ```php -$form->addText('name', 'Nombre:') - ->setRequired('Por favor, introduzca su nombre'); +$form->addText('name', 'Name:') + ->setRequired('Please enter a name.'); ``` -Intente enviar el formulario sin rellenar el nombre y verá que se muestra un mensaje de error y el navegador o el servidor lo rechazarán hasta que rellene el campo. +Pruebe a enviar el formulario sin rellenar el nombre y verá aparecer un mensaje de error. El navegador o el servidor lo rechazarán hasta que rellene el campo. -Al mismo tiempo, no puede engañar al sistema escribiendo, por ejemplo, solo espacios en el campo. De ninguna manera. Nette elimina automáticamente los espacios iniciales y finales. Pruébelo usted mismo. Es algo que siempre debería hacer con cada input de una sola línea, pero a menudo se olvida. Nette lo hace automáticamente. (Puede intentar engañar al formulario y enviar una cadena de varias líneas como nombre. Incluso aquí, Nette no se deja engañar y convierte los saltos de línea en espacios). +Al mismo tiempo, no puede engañar al sistema escribiendo solo espacios en el campo. De ninguna manera. Nette recorta automáticamente los espacios del principio y del final. Pruébelo. Es algo que debería hacer siempre en todos los campos de una línea, pero que a menudo se olvida. Nette lo hace automáticamente. (Puede intentar engañar al formulario enviando como nombre una cadena de varias líneas. Tampoco así se dejará engañar Nette, y los saltos de línea se convertirán en espacios.) -El formulario siempre se valida en el lado del servidor, pero también se genera una validación JavaScript, que se ejecuta instantáneamente y el usuario se entera del error de inmediato, sin necesidad de enviar el formulario al servidor. Esto lo gestiona el script `netteForms.js`. Insértelo en la página: +El formulario se valida siempre en el lado del servidor, pero además se genera la validación en JavaScript. Esta se ejecuta al instante y el usuario se entera de los errores de inmediato, sin necesidad de enviar el formulario al servidor. De eso se ocupa el script `netteForms.js`. Insértelo en la página: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Si mira el código fuente de la página con el formulario, puede notar que Nette inserta los elementos obligatorios en elementos con la clase CSS `required`. Intente añadir la siguiente hoja de estilos a la plantilla y la etiqueta "Nombre" será roja. De esta manera, marcamos elegantemente los elementos obligatorios para los usuarios: +Si mira el código fuente de la página con el formulario, quizá se dé cuenta de que Nette inserta los elementos obligatorios en elementos con la clase CSS `required`. Pruebe a añadir la siguiente hoja de estilos a la plantilla y la etiqueta "Name" se pondrá roja. Así se resaltan con elegancia los elementos obligatorios para los usuarios: ```latte <style> @@ -117,85 +123,102 @@ Si mira el código fuente de la página con el formulario, puede notar que Nette </style> ``` -Añadimos otras reglas de validación con el método `addRule()`. El primer parámetro es la regla, el segundo es nuevamente el texto del mensaje de error y puede seguir un argumento de la regla de validación. ¿Qué significa esto? +Las demás reglas de validación las añadimos con el método `addRule()`. El primer parámetro es la regla, el segundo es de nuevo el texto del mensaje de error y puede seguirle un argumento opcional de la regla de validación. ¿Qué significa eso? -Ampliaremos el formulario con un nuevo campo opcional "edad", que debe ser un número entero (`addInteger()`) y además estar en un rango permitido (`$form::Range`). Y aquí es donde usaremos el tercer parámetro del método `addRule()`, con el que pasamos al validador el rango requerido como un par `[desde, hasta]`: +Ampliemos el formulario con un nuevo campo opcional "age", que debe ser un número entero (`addInteger()`) y estar dentro de un rango permitido (`$form::Range`). Aquí usaremos el tercer parámetro del método `addRule()` para pasarle al validador el rango requerido como par `[min, max]`: ```php -$form->addInteger('age', 'Edad:') - ->addRule($form::Range, 'La edad debe estar entre 18 y 120', [18, 120]); +$form->addInteger('age', 'Age:') + ->addRule($form::Range, 'Age must be between 18 and 120.', [18, 120]); ``` .[tip] -Si el usuario no rellena el campo, las reglas de validación no se verificarán, ya que el elemento es opcional. +Si el usuario no rellena el campo, las reglas de validación no se comprobarán, porque el elemento es opcional. -Aquí surge espacio para una pequeña refactorización. En el mensaje de error y en el tercer parámetro, los números se indican de forma duplicada, lo cual no es ideal. Si estuviéramos creando [formularios multilingües |rendering#Traducción] y el mensaje que contiene números se tradujera a varios idiomas, dificultaría un posible cambio de valores. Por esta razón, es posible usar los marcadores de posición `%d` y Nette completará los valores: +Esto abre espacio para una pequeña refactorización. Los números están duplicados en el mensaje de error y en el tercer parámetro, lo que no es ideal. Si estuviéramos creando [formularios multilingües |rendering#Traducción] y el mensaje con los números se tradujera a varios idiomas, cambiar los valores sería complicado. Por eso se pueden usar los marcadores `%d` y Nette rellenará los valores: ```php - ->addRule($form::Range, 'La edad debe estar entre %d y %d años', [18, 120]); + ->addRule($form::Range, 'Age must be between %d and %d years.', [18, 120]); ``` -Volvamos al elemento `password`, que también haremos obligatorio y además verificaremos la longitud mínima de la contraseña (`$form::MinLength`), nuevamente usando un marcador de posición: +Volvamos al elemento `password`, hagámoslo también obligatorio y verifiquemos además la longitud mínima de la contraseña (`$form::MinLength`), usando de nuevo un marcador en el mensaje: ```php -$form->addPassword('password', 'Contraseña:') - ->setRequired('Elija una contraseña') - ->addRule($form::MinLength, 'La contraseña debe tener al menos %d caracteres', 8); +$form->addPassword('password', 'Password:') + ->setRequired('Choose a password') + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8); ``` -Añadimos al formulario otro campo `passwordVerify`, donde el usuario introduce la contraseña de nuevo, para verificar. Usando reglas de validación, comprobamos si ambas contraseñas son iguales (`$form::Equal`). Y como parámetro, damos una referencia a la primera contraseña usando [corchetes |#Acceso a los elementos]: +Añadamos al formulario otro campo `passwordVerify`, donde el usuario introduce la contraseña otra vez para verificarla. Con las reglas de validación comprobamos que ambas contraseñas sean iguales (`$form::Equal`). Como parámetro indicamos una referencia a la primera contraseña usando [corchetes |#Acceso a los elementos]: ```php -$form->addPassword('passwordVerify', 'Contraseña para verificar:') - ->setRequired('Por favor, introduzca la contraseña de nuevo para verificarla') - ->addRule($form::Equal, 'Las contraseñas no coinciden', $form['password']) +$form->addPassword('passwordVerify', 'Password again:') + ->setRequired('Please enter the password again for verification') + ->addRule($form::Equal, 'Passwords do not match', $form['password']) ->setOmitted(); ``` -Con `setOmitted()` hemos marcado un elemento cuyo valor en realidad no nos importa y que existe solo por motivos de validación. El valor no se pasará a `$data`. +Con `setOmitted()` hemos marcado un elemento cuyo valor no nos interesa realmente y que existe solo a efectos de validación. Su valor no se pasa a `$data`. -Con esto, tenemos un formulario completamente funcional con validación en PHP y JavaScript. Las capacidades de validación de Nette son mucho más amplias, se pueden crear condiciones, mostrar y ocultar partes de la página según ellas, etc. Todo lo aprenderá en el capítulo sobre [validación de formularios|validation]. +Con esto tenemos un formulario plenamente funcional con validación tanto en PHP como en JavaScript. Las capacidades de validación de Nette son mucho más amplias; puede crear condiciones, mostrar y ocultar partes de la página en función de ellas, etc. Lo aprenderá todo en el capítulo sobre la [validación de formularios |validation]. -Valores por defecto -=================== +Valores predeterminados +======================= -Normalmente establecemos valores por defecto para los elementos del formulario: +A menudo establecemos valores predeterminados en los elementos del formulario: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -A menudo es útil establecer valores por defecto para todos los elementos a la vez. Por ejemplo, cuando el formulario se utiliza para editar registros. Leemos el registro de la base de datos y establecemos los valores por defecto: +A menudo resulta útil establecer los valores predeterminados de todos los elementos a la vez, por ejemplo cuando el formulario sirve para editar registros. Leemos el registro de la base de datos y establecemos sus valores como predeterminados: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Llame a `setDefaults()` después de definir los elementos. +En un formulario ya enviado, `setDefaults()` no tiene efecto: no sobrescribirá lo que el usuario rellenó, así que es seguro llamarlo sin condiciones en la fábrica del formulario. Si necesita forzar los valores incluso después del envío, use `setValues()` en su lugar. + + +Renderizar el formulario +======================== + +De forma predeterminada, el formulario se renderiza como una tabla. Los distintos elementos respetan las pautas básicas de accesibilidad: todas las etiquetas se generan como elementos `<label>` y se asocian con sus respectivos elementos de formulario. Al pulsar una etiqueta, el cursor se coloca automáticamente en el campo del formulario. + +A cada elemento le podemos poner atributos HTML arbitrarios. Por ejemplo, añadir un placeholder: + +```php +$form->addInteger('age', 'Age:') + ->setHtmlAttribute('placeholder', 'Please fill in the age'); +``` -Renderizado del formulario -========================== +Hay muchas maneras de renderizar un formulario, así que al renderizado se le dedica un [capítulo aparte |rendering]. -Por defecto, el formulario se renderiza como una tabla. Los elementos individuales cumplen la regla básica de accesibilidad: todas las etiquetas se escriben como `<label>` y están vinculadas al elemento de formulario correspondiente. Al hacer clic en la etiqueta, el cursor aparece automáticamente en el campo del formulario. -Podemos establecer atributos HTML arbitrarios para cada elemento. Por ejemplo, añadir un placeholder: +Renderizado con Latte +--------------------- + +Si tiene a mano el sistema de plantillas [Latte |latte:], puede dejar que renderice el formulario y ganar control total sobre el HTML resultante. Crea el motor, registra la extensión de formularios y pasa el formulario a la plantilla como variable: ```php -$form->addInteger('age', 'Edad:') - ->setHtmlAttribute('placeholder', 'Por favor, introduzca la edad'); +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); ``` -Hay realmente muchas formas de renderizar un formulario, por lo que se dedica a ello un [capítulo separado sobre renderizado|rendering]. +En la plantilla trabaja después con el formulario mediante la variable `$form` y etiquetas como `{input}`, `{label}` o `n:name`. Un ejemplo completo, incluida la plantilla, lo encontrará en el directorio de [ejemplos |https://github.com/nette/forms/tree/master/examples] (los archivos `latte.php` y `latte/`). Las distintas etiquetas se describen en el capítulo sobre el [renderizado |rendering]. Mapeo a clases ============== -Volvamos al procesamiento de los datos del formulario. El método `getValues()` nos devolvía los datos enviados como un objeto `ArrayHash`. Dado que es una clase genérica, algo así como `stdClass`, nos faltará cierta comodidad al trabajar con ella, como el autocompletado de propiedades en los editores o el análisis estático de código. Esto podría resolverse teniendo una clase específica para cada formulario, cuyas propiedades representen los elementos individuales. Por ejemplo: +Volvamos al procesamiento de los datos del formulario. El método `getValues()` devolvía los datos enviados como objeto `ArrayHash`. Como es una clase genérica, parecida a `stdClass`, al trabajar con ella nos faltan ciertas comodidades, como el autocompletado de las propiedades en los editores o el análisis estático del código. Eso se podría resolver teniendo una clase concreta para cada formulario, cuyas propiedades representen los distintos elementos. P. ej.: ```php class RegistrationFormData @@ -206,32 +229,32 @@ class RegistrationFormData } ``` -Alternativamente, puede utilizar un constructor: +Alternativamente puede usar el constructor: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Las propiedades de la clase de datos también pueden ser enums y se mapearán automáticamente. .{data-version:3.2.4} +Las propiedades de la clase de datos también pueden ser enums, y se mapearán automáticamente. .{data-version:3.2.4} -¿Cómo decirle a Nette que nos devuelva los datos como objetos de esta clase? Más fácil de lo que piensa. Simplemente indique el nombre de la clase o el objeto a hidratar como parámetro: +¿Cómo le decimos a Nette que devuelva los datos como objetos de esa clase? Es más fácil de lo que parece. Basta con indicar como parámetro el nombre de la clase o el objeto que hay que hidratar: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -También se puede indicar `'array'` como parámetro y entonces los datos se devolverán como un array. +Como parámetro también puede indicar `'array'` y los datos se devolverán como array. -Si los formularios forman una estructura multinivel compuesta por contenedores, cree una clase separada para cada uno: +Si los formularios constan de una estructura de varios niveles compuesta de contenedores, cree una clase separada para cada uno: ```php $form = new Form; @@ -253,65 +276,62 @@ class RegistrationFormData } ``` -El mapeo entonces, a partir del tipo de la propiedad `$person`, sabe que debe mapear el contenedor a la clase `PersonFormData`. Si la propiedad contuviera un array de contenedores, indique el tipo `array` y pase la clase para el mapeo directamente al contenedor: +El mapeo sabe entonces, por el tipo de la propiedad `$person`, que debe mapear el contenedor a la clase `PersonFormData`. Si la propiedad tuviera que contener un array de contenedores, indique el tipo `array` y pase la clase que hay que mapear directamente al contenedor: ```php $person->setMappedType(PersonFormData::class); ``` -Puede generar el diseño de la clase de datos del formulario usando el método `Nette\Forms\Blueprint::dataClass($form)`, que lo imprimirá en la página del navegador. Luego, simplemente seleccione el código haciendo clic y cópielo en su proyecto. .{data-version:3.1.15} +Puede hacer que se le genere una propuesta de la clase de datos del formulario con el método `Nette\Forms\Blueprint::dataClass($form)`, que la imprimirá en la página del navegador. Después basta con seleccionar y copiar el código a su proyecto. .{data-version:3.1.15} -Múltiples botones -================= +Varios botones de envío +======================= -Si el formulario tiene más de un botón, generalmente necesitamos distinguir cuál de ellos fue presionado. Esta información nos la devuelve el método `isSubmittedBy()` del botón: +Si el formulario tiene más de un botón, normalmente necesitamos distinguir cuál se pulsó. Esa información la devuelve el método `isSubmittedBy()` del botón: ```php -$form->addSubmit('save', 'Guardar'); -$form->addSubmit('delete', 'Eliminar'); +$form->addSubmit('save', 'Save'); +$form->addSubmit('delete', 'Delete'); if ($form->isSuccess()) { if ($form['save']->isSubmittedBy()) { - // procesar guardar + // ... } if ($form['delete']->isSubmittedBy()) { - // procesar eliminar + // ... } } ``` -No omita la consulta `$form->isSuccess()`, verifica la validez de los datos. +No omita la comprobación `$form->isSuccess()`; verifica la validez de los datos. -Cuando el formulario se envía con la tecla <kbd>Enter</kbd>, se considera como si se hubiera enviado con el primer botón. +Cuando un formulario se envía pulsando la tecla <kbd>Enter</kbd>, se trata como si se hubiera enviado con el primer botón. -Protección contra vulnerabilidades -================================== +Protección frente a vulnerabilidades +==================================== -Nette Framework pone gran énfasis en la seguridad y, por lo tanto, se preocupa escrupulosamente por la buena seguridad de los formularios. +Nette Framework pone un fuerte énfasis en la seguridad y por eso vela meticulosamente por la seguridad correcta de los formularios. -Además de proteger los formularios contra ataques [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] y [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], realiza muchas pequeñas medidas de seguridad en las que ya no tiene que pensar. +Además de proteger los formularios frente a vulnerabilidades conocidas como el [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] y el [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], realiza muchas pequeñas medidas de seguridad en las que ya no tiene que pensar. -Por ejemplo, filtra todos los caracteres de control de las entradas y verifica la validez de la codificación UTF-8, por lo que los datos del formulario siempre estarán limpios. En los select boxes y radio lists, verifica que los elementos seleccionados fueran realmente de los ofrecidos y que no hubo suplantación. Ya mencionamos que en las entradas de texto de una sola línea elimina los caracteres de fin de línea que un atacante podría haber enviado. En las entradas de varias líneas, normaliza los caracteres de fin de línea. Y así sucesivamente. +Por ejemplo, filtra de las entradas todos los caracteres de control y comprueba la validez de la codificación UTF-8, lo que garantiza que los datos del formulario estarán siempre limpios. En los select boxes y las listas de radio verifica que los elementos seleccionados estaban realmente entre las opciones ofrecidas y que no hubo ninguna falsificación. Ya hemos mencionado que en los campos de texto de una línea sustituye por espacios los caracteres de fin de línea que un atacante pudiera enviar. En los campos de varias líneas normaliza los caracteres de fin de línea. Etcétera. -Nette resuelve por usted los riesgos de seguridad que muchos programadores ni siquiera saben que existen. +Nette se ocupa por usted de riesgos de seguridad cuya existencia muchos programadores ni siquiera conocen. -El ataque CSRF mencionado consiste en que un atacante atrae a la víctima a una página que ejecuta discretamente una solicitud en el navegador de la víctima al servidor en el que la víctima está conectada, y el servidor cree que la solicitud fue realizada por la víctima por su propia voluntad. Por lo tanto, Nette evita el envío de formularios POST desde otro dominio. Si por alguna razón desea desactivar la protección y permitir el envío de formularios desde otro dominio, use: +El mencionado ataque CSRF consiste en que un atacante atrae a la víctima a una página que, en silencio, ejecuta en el navegador de la víctima una petición al servidor en el que esta tiene la sesión iniciada. El servidor cree que la petición la hizo la víctima voluntariamente. Por eso Nette rechaza los formularios POST enviados desde un origen ajeno; incluso otro subdominio del mismo sitio cuenta como ajeno. Si necesita permitir el envío desde otro origen, desactive la protección con: ```php -$form->allowCrossOrigin(); // ¡CUIDADO! Desactiva la protección! +$form->allowCrossOrigin(); // ¡ATENCIÓN! ¡Desactiva la protección por completo! ``` -Esta protección utiliza una cookie SameSite llamada `_nss`. Por lo tanto, cree el objeto de formulario antes de enviar la primera salida, para que la cookie pueda ser enviada. - -La protección mediante la cookie SameSite puede no ser 100% fiable, por lo que es recomendable activar también la protección mediante token: +Eso desactiva la protección para cualquier origen. Para permitir solo orígenes concretos, desactive la protección y verifique usted mismo la cabecera `Origin` contra su propia lista blanca. -```php -$form->addProtection(); -``` +La protección se apoya en la cabecera `Sec-Fetch-Site` del navegador (Fetch Metadata), que el navegador envía automáticamente y que no se puede falsificar ni siquiera con una vulnerabilidad XSS. Los navegadores antiguos que no envían estas cabeceras no pasarán la comprobación. El artículo [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] lo describe en detalle. -Recomendamos proteger de esta manera los formularios en la parte de administración del sitio web que modifican datos sensibles en la aplicación. El framework se defiende contra el ataque CSRF generando y verificando un token de autorización que se almacena en la sesión. Por lo tanto, es necesario tener la sesión abierta antes de mostrar el formulario. En la parte de administración del sitio web, la sesión generalmente ya está iniciada debido al inicio de sesión del usuario. De lo contrario, inicie la sesión con el método `Nette\Http\Session::start()`. +.[note] +La protección anterior, con un token de autorización guardado en la sesión y activada con `$form->addProtection()`, ya no hace falta y está obsoleta desde la versión 3.3. -Bien, hemos cubierto una introducción rápida a los formularios en Nette. Intente también mirar el directorio [examples|https://github.com/nette/forms/tree/master/examples] en la distribución, donde encontrará más inspiración. +Con esto hemos hecho una introducción rápida a los formularios en Nette. Pruebe a mirar en el directorio de [ejemplos |https://github.com/nette/forms/tree/master/examples] de la distribución para inspirarse más. diff --git a/forms/es/upgrading.texy b/forms/es/upgrading.texy new file mode 100644 index 0000000000..084f7072a6 --- /dev/null +++ b/forms/es/upgrading.texy @@ -0,0 +1,54 @@ +Actualización +************* + + +Actualización a la versión 3.3 +============================== + +- la protección automática contra CSRF pasó de `isSameSite()` a `isFrom(FetchSite::SameOrigin)` y se volvió más estricta: las peticiones que llegan desde subdominios ya no pasan +- gracias a eso, `addProtection()` ya no hace falta, porque la protección automática cubre los mismos casos; omítalo en los formularios nuevos y elimínelo sin problema de los existentes + +Por qué ya no hacen falta los tokens en la sesión se explica en el artículo [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Actualización a la versión 3.1 +============================== + +- `getValues()` devuelve solo los elementos validados; si necesita los valores de todos los elementos independientemente de la validación, use el nuevo método `getUntrustedValues()` +- los `$values` que se pasan a los manejadores `onSuccess` y `onClick` contienen igualmente solo los elementos validados +- los formularios independientes están protegidos automáticamente contra CSRF mediante una cookie con la bandera SameSite; puede permitir el envío desde otro origen con `allowCrossOrigin()` +- la regla `Form::URL` completa ahora el protocolo que falta con `https` en lugar de `http` +- `Form::addImage()` se ha renombrado a `addImageButton()` +- `Checkbox::getSeparatorPrototype()` se ha renombrado a `getContainerPrototype()` +- los formularios ya no crean la variable `$_form` en las plantillas + +Más sobre estos cambios en el artículo [News in Nette Forms 3.1 |https://blog.nette.org/en/news-in-nette-forms-3-1]. + + +Actualización a la versión 3.0 +============================== + +- todos los elementos de formulario son ahora opcionales de forma predeterminada (este cambio se introdujo en Nette 2.4), así que puede eliminar `setRequired(false)` +- no olvide actualizar `netteForms.js` a la versión 3 (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` y `MultiChoiceControl::$checkAllowedValues` se han sustituido por el método `checkDefaultValue()` + + +Actualización a la versión 2.4 +============================== + +- si un elemento tiene una regla mediante `addRule()` (es decir, es en la práctica obligatorio), tiene que marcarlo además como obligatorio con `setRequired()`; además, `setRequired(false)` hace ahora que el elemento sea opcional, lo que sustituye a las ramas `addCondition($form::FILLED)` +- los validadores `Form::EMAIL`, `URL` e `INTEGER` cambian automáticamente el atributo HTML `type` a `email`, `url` y `number`, respectivamente +- las reglas de validación negativas están obsoletas; la alternativa a `~Form::FILLED` es `Form::BLANK`, y `~Form::EQUAL` se puede sustituir por `Form::NOT_EQUAL` +- el parámetro interno `do` se envía ahora por POST como `_do` para evitar una colisión +- las variables internas con guion bajo, como `$_form`, están obsoletas +- recuerde actualizar `netteForms.js` + + +Actualización a la versión 2.3 +============================== + +- se han eliminado los métodos internos de filtrado, como `Nette\Forms\Controls\TextBase::filterFloat` +- los métodos internos de validación, como `TextBase::validateFloat`, se han movido a `Nette\Forms\Validator`, igual que `Rules::$defaultMessages` +- los botones y los campos Hidden se generan sin ID de HTML; si quiere un ID, establézcalo con `setHtmlId()` +- los elementos de RadioList también se generan sin ID; puede activarlo con `$radioList->generateId = true` +- los filtros añadidos con `TextBase::addFilter()` se procesan durante la validación, y ahora puede añadir filtros a las condiciones: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/es/validation.texy b/forms/es/validation.texy index f3e6d61efd..4babf6c974 100644 --- a/forms/es/validation.texy +++ b/forms/es/validation.texy @@ -5,204 +5,212 @@ Validación de formularios Elementos obligatorios ====================== -Marcamos los elementos obligatorios con el método `setRequired()`, cuyo argumento es el texto del [mensaje de error |#Mensajes de error] que se mostrará si el usuario no rellena el elemento. Si no se proporciona el argumento, se utilizará el mensaje de error predeterminado. +Los elementos se marcan como obligatorios con el método `setRequired()`. Su argumento es el texto del [mensaje de error |#Mensajes de error] que se mostrará si el usuario no rellena el elemento. Si no se indica ningún argumento, se usa el mensaje de error predeterminado. ```php -$form->addText('name', 'Nombre:') - ->setRequired('Por favor, introduzca su nombre'); +$form->addText('name', 'Name:') + ->setRequired('Please fill in your name.'); ``` Reglas ====== -Añadimos reglas de validación a los elementos con el método `addRule()`. El primer parámetro es la regla, el segundo es el texto del [mensaje de error |#Mensajes de error] y el tercero es el argumento de la regla de validación. +Las reglas de validación se añaden a los elementos con el método `addRule()`. El primer parámetro es la regla, el segundo el [mensaje de error |#Mensajes de error] y el tercero el argumento de la regla de validación. ```php -$form->addPassword('password', 'Contraseña:') - ->addRule($form::MinLength, 'La contraseña debe tener al menos %d caracteres', 8); +$form->addPassword('password', 'Password:') + ->addRule($form::MinLength, 'Password must be at least %d characters long', 8); ``` -**Las reglas de validación solo se verifican si el usuario ha rellenado el elemento.** +**Las reglas de validación solo se comprueban si el usuario ha rellenado el elemento.** -Nette viene con una serie de reglas predefinidas, cuyos nombres son constantes de la clase `Nette\Forms\Form`. Podemos usar estas reglas para todos los elementos: +Nette trae varias reglas predefinidas cuyos nombres son constantes de la clase `Nette\Forms\Form`. Estas reglas se pueden aplicar a todos los elementos: -| constante | descripción | tipo de argumento +| constante | descripción | tipo del argumento |------- -| `Required` | elemento obligatorio, alias para `setRequired()` | - -| `Filled` | elemento obligatorio, alias para `setRequired()` | - -| `Blank` | el elemento no debe ser rellenado | - -| `Equal` | el valor es igual al parámetro | `mixed` -| `NotEqual` | el valor no es igual al parámetro | `mixed` -| `IsIn` | el valor es igual a uno de los elementos del array | `array` -| `IsNotIn` | el valor no es igual a ninguno de los elementos del array | `array` -| `Valid` | ¿está el elemento rellenado correctamente? (para [#condiciones]) | - +| `Required` | elemento obligatorio, alias de `setRequired()` | - +| `Filled` | elemento obligatorio, alias de `setRequired()` | - +| `Blank` | el elemento no debe estar relleno | - +| `Equal` | el valor debe ser igual al parámetro | `mixed` +| `NotEqual` | el valor no debe ser igual al parámetro | `mixed` +| `IsIn` | el valor debe ser uno de los elementos del array | `array` +| `IsNotIn` | el valor no debe ser ninguno de los elementos del array | `array` +| `Valid` | ¿está el elemento relleno correctamente? (solo en [addConditionOn() |#Condiciones]) | - -Entradas de texto ------------------ +Campos de texto +--------------- -Para los elementos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, también se pueden usar algunas de las siguientes reglas: +En los elementos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` y `addFloat()` se pueden aplicar además algunas de las siguientes reglas: | `MinLength` | longitud mínima del texto | `int` | `MaxLength` | longitud máxima del texto | `int` -| `Length` | longitud en un rango o longitud exacta | par `[int, int]` o `int` -| `Email` | dirección de correo electrónico válida | - +| `Length` | longitud dentro de un rango o longitud exacta | par `[int, int]` o `int` +| `Email` | dirección de correo válida | - | `URL` | URL absoluta | - -| `Pattern` | coincide con la expresión regular | `string` -| `PatternInsensitive` | como `Pattern`, pero insensible a mayúsculas/minúsculas | `string` +| `Pattern` | encaja con la expresión regular | `string` +| `PatternInsensitive` | como `Pattern`, pero sin distinguir mayúsculas | `string` | `Integer` | valor entero | - -| `Numeric` | alias para `Integer` | - +| `Numeric` | entero no negativo (solo dígitos) | - | `Float` | número | - -| `Min` | valor mínimo del elemento numérico | `int\|float` -| `Max` | valor máximo del elemento numérico | `int\|float` -| `Range` | valor en el rango | par `[int\|float, int\|float]` +| `Min` | valor mínimo de un elemento numérico | `int\|float` +| `Max` | valor máximo de un elemento numérico | `int\|float` +| `Range` | valor dentro de un rango | par `[int\|float, int\|float]` -Las reglas de validación `Integer`, `Numeric` y `Float` convierten directamente el valor a entero o flotante, respectivamente. Además, la regla `URL` también acepta una dirección sin esquema (p. ej., `nette.org`) y añade el esquema (`https://nette.org`). La expresión en `Pattern` y `PatternIcase` debe aplicarse a todo el valor, es decir, como si estuviera envuelta en los caracteres `^` y `$`. +Las reglas de validación `Integer` y `Float` convierten automáticamente el valor a entero o a float, respectivamente. Además, la regla `URL` acepta también una dirección sin esquema (p. ej. `nette.org`) y completa el esquema (`https://nette.org`). La expresión de `Pattern` y `PatternInsensitive` debe ser válida para el valor entero, es decir, como si estuviera envuelta entre los caracteres `^` y `$`. Número de elementos ------------------- -Para los elementos `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, también se pueden usar las siguientes reglas para limitar el número de elementos seleccionados o archivos cargados: +En los elementos `addMultiUpload()`, `addCheckboxList()` y `addMultiSelect()` puede usar además las siguientes reglas para limitar el número de elementos seleccionados o de archivos subidos: | `MinLength` | número mínimo | `int` | `MaxLength` | número máximo | `int` -| `Length` | número en un rango o número exacto | par `[int, int]` o `int` +| `Length` | número dentro de un rango o número exacto | par `[int, int]` o `int` -Carga de archivos ------------------ +Subida de archivos +------------------ -Para los elementos `addUpload()`, `addMultiUpload()`, también se pueden usar las siguientes reglas: +En los elementos `addUpload()` y `addMultiUpload()` se pueden usar además las siguientes reglas: | `MaxFileSize` | tamaño máximo del archivo en bytes | `int` -| `MimeType` | Tipo MIME, se permiten comodines (`'video/*'`) | `string\|string[]` -| `Image` | imagen JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | el nombre del archivo coincide con la expresión regular | `string` -| `PatternInsensitive` | como `Pattern`, pero insensible a mayúsculas/minúsculas | `string` +| `MimeType` | tipo MIME, se permiten comodines (`'video/*'`) | `string\|string[]` +| `Image` | imagen JPEG, PNG, GIF, WebP o AVIF | - +| `Pattern` | el nombre del archivo encaja con la expresión regular | `string` +| `PatternInsensitive` | como `Pattern`, pero sin distinguir mayúsculas | `string` -`MimeType` e `Image` requieren la extensión PHP `fileinfo`. Detectan si el archivo o imagen es del tipo requerido basándose en su firma y **no verifican la integridad de todo el archivo.** Si una imagen está dañada se puede detectar, por ejemplo, intentando [cargarla |http:request#toImage]. +`MimeType` e `Image` requieren la extensión de PHP `fileinfo`. Que un archivo o una imagen sea del tipo requerido se detecta a partir de su firma, y **no se comprueba la integridad del archivo entero**. Si una imagen está dañada se puede averiguar, por ejemplo, intentando [cargarla |http:request#toImage()]. Mensajes de error ================= -Todas las reglas predefinidas, excepto `Pattern` y `PatternInsensitive`, tienen un mensaje de error predeterminado, por lo que se puede omitir. Sin embargo, al proporcionar y formular todos los mensajes a medida, hará que el formulario sea más fácil de usar. +Todas las reglas predefinidas salvo `Pattern` y `PatternInsensitive` tienen un mensaje de error predeterminado, así que se pueden omitir. Pero, si indica y formula todos los mensajes propios a la medida de sus necesidades, hará el formulario más cómodo para el usuario. -Puede cambiar los mensajes predeterminados en la [configuración|forms:configuration], editando los textos en el array `Nette\Forms\Validator::$messages` o usando un [traductor |rendering#Traducción]. +Puede cambiar los mensajes predeterminados en la [configuración |forms:configuration], modificando los textos del array `Nette\Forms\Validator::$messages`, o usando un [traductor |rendering#Traducción]. -En el texto de los mensajes de error se pueden usar estas cadenas de marcador de posición: +En el texto de los mensajes de error se pueden usar los siguientes marcadores: -| `%d` | reemplaza secuencialmente con los argumentos de la regla -| `%n$d` | reemplaza con el n-ésimo argumento de la regla -| `%label` | reemplaza con la etiqueta del elemento (sin dos puntos) -| `%name` | reemplaza con el nombre del elemento (p. ej. `name`) -| `%value` | reemplaza con el valor introducido por el usuario +| `%d` | se sustituye sucesivamente por los argumentos de la regla +| `%n$d` | se sustituye por el n-ésimo argumento de la regla +| `%label` | se sustituye por la etiqueta del elemento (sin los dos puntos) +| `%name` | se sustituye por el nombre del elemento (p. ej. `name`) +| `%value` | se sustituye por el valor introducido por el usuario ```php -$form->addText('name', 'Nombre:') - ->setRequired('Por favor, rellene %label'); +$form->addText('name', 'Name:') + ->setRequired('Please fill in %label'); $form->addInteger('id', 'ID:') - ->addRule($form::Range, 'al menos %d y como máximo %d', [5, 10]); + ->addRule($form::Range, 'at least %d and at most %d', [5, 10]); $form->addInteger('id', 'ID:') - ->addRule($form::Range, 'como máximo %2$d y al menos %1$d', [5, 10]); + ->addRule($form::Range, 'at most %2$d and at least %1$d', [5, 10]); ``` Condiciones =========== -Además de las reglas, también se pueden añadir condiciones. Se escriben de forma similar a las reglas, solo que en lugar de `addRule()` usamos el método `addCondition()` y, por supuesto, no proporcionamos ningún mensaje de error (la condición solo pregunta): +Además de reglas se pueden añadir condiciones. Se escriben de forma parecida a las reglas, pero en lugar de `addRule()` usamos el método `addCondition()` y, naturalmente, no indicamos ningún mensaje de error (la condición solo pregunta): ```php -$form->addPassword('password', 'Contraseña:') - // si la contraseña no tiene más de 8 caracteres +$form->addPassword('password', 'Password:') + // si la longitud de la contraseña no es mayor que 8 ->addCondition($form::MaxLength, 8) // entonces debe contener un dígito - ->addRule($form::Pattern, 'Debe contener un dígito', '.*[0-9].*'); + ->addRule($form::Pattern, 'Must contain a digit', '.*[0-9].*'); ``` -La condición también se puede vincular a un elemento diferente al actual usando `addConditionOn()`. Como primer parámetro, proporcionamos una referencia al elemento. En este ejemplo, el correo electrónico será obligatorio solo si se marca la casilla de verificación (su valor será `true`): +La condición se puede enlazar con un elemento distinto del actual mediante `addConditionOn()`. El primer parámetro es una referencia al elemento. En este ejemplo, el correo será obligatorio solo si el checkbox está marcado (es decir, su valor es true): ```php -$form->addCheckbox('newsletters', 'Enviarme boletines'); +$form->addCheckbox('newsletters', 'Send me newsletters'); -$form->addEmail('email', 'E-mail:') - // si la casilla de verificación está marcada +$form->addEmail('email', 'Email:') + // si el checkbox está marcado ->addConditionOn($form['newsletters'], $form::Equal, true) - // entonces requiere el correo electrónico - ->setRequired('Introduzca una dirección de correo electrónico'); + // entonces exige el correo + ->setRequired('Enter your email address'); ``` -Se pueden crear estructuras complejas a partir de condiciones usando `elseCondition()` y `endCondition()`: +Las condiciones se pueden componer en estructuras complejas con `elseCondition()` y `endCondition()`: ```php $form->addText(/* ... */) ->addCondition(/* ... */) // si se cumple la primera condición - ->addConditionOn(/* ... */) // y la segunda condición en otro elemento - ->addRule(/* ... */) // requiere esta regla - ->elseCondition() // si la segunda condición no se cumple - ->addRule(/* ... */) // requiere estas reglas + ->addConditionOn(/* ... */) // y se cumple también la segunda condición sobre otro elemento + ->addRule(/* ... */) // exige esta regla + ->elseCondition() // si no se cumple la segunda condición + ->addRule(/* ... */) // exige estas reglas ->addRule(/* ... */) ->endCondition() // volvemos a la primera condición ->addRule(/* ... */); ``` -En Nette, es muy fácil reaccionar al cumplimiento o incumplimiento de una condición también en el lado de JavaScript usando el método `toggle()`, consulte [#JavaScript dinámico]. +El primer argumento de `addCondition()` también puede ser un valor booleano. Es útil cuando la decisión ya se conoce mientras se construye el formulario, por ejemplo para aplicar una regla solo en determinadas circunstancias: + +```php +$form->addText('nickname') + ->addCondition($isRequired) // un valor conocido al construir el formulario + ->setRequired(); +``` + +En Nette es muy fácil reaccionar en el lado de JavaScript a que se cumpla o no una condición, con el método `toggle()`, véase [#JavaScript dinámico]. Referencia a otro elemento ========================== -También se puede pasar otro elemento del formulario como argumento de una regla o condición. La regla entonces usará el valor introducido posteriormente por el usuario en el navegador. De esta manera, se puede validar dinámicamente, por ejemplo, que el elemento `password` contenga la misma cadena que el elemento `password_confirm`: +Como argumento de una regla o una condición también puede pasar otro elemento del formulario. La regla usará entonces el valor que el usuario introduzca más tarde en el navegador. Eso se puede usar, por ejemplo, para validar dinámicamente que el elemento `password` contiene la misma cadena que el elemento `password_confirm`: ```php -$form->addPassword('password', 'Contraseña'); -$form->addPassword('password_confirm', 'Confirmar contraseña') - ->addRule($form::Equal, 'Las contraseñas introducidas no coinciden', $form['password']); +$form->addPassword('password', 'Password'); +$form->addPassword('password_confirm', 'Confirm Password') + ->addRule($form::Equal, 'The passwords do not match', $form['password']); ``` -Reglas y condiciones personalizadas -=================================== +Reglas y condiciones propias +============================ -A veces nos encontramos en una situación en la que las reglas de validación incorporadas en Nette no son suficientes y necesitamos validar los datos del usuario a nuestra manera. ¡En Nette esto es muy simple! +A veces nos encontramos con situaciones en las que las reglas de validación integradas de Nette no bastan y necesitamos validar los datos del usuario a nuestra manera. ¡En Nette eso es muy sencillo! -Se puede pasar cualquier callback como primer parámetro a los métodos `addRule()` o `addCondition()`. Este recibe el propio elemento como primer parámetro y devuelve un valor booleano que indica si la validación se realizó correctamente. Al añadir una regla con `addRule()`, también es posible especificar argumentos adicionales, que luego se pasan como segundo parámetro. +Como primer parámetro de los métodos `addRule()` o `addCondition()` puede pasar cualquier callback. El callback acepta el propio elemento como primer parámetro y devuelve un valor booleano que indica si la validación tuvo éxito. Al añadir una regla con `addRule()` se pueden indicar argumentos adicionales, que se pasan después como segundo parámetro. -Así podemos crear nuestro propio conjunto de validadores como una clase con métodos estáticos: +Un conjunto propio de validadores se puede crear, por tanto, como una clase con métodos estáticos: ```php class MyValidators { // comprueba si el valor es divisible por el argumento - public static function validateDivisibility(Nette\Forms\Control $input, $arg): bool + public static function validateDivisibility(BaseControl $input, $arg): bool { return $input->getValue() % $arg === 0; } - public static function validateEmailDomain(Nette\Forms\Control $input, $domain) + public static function validateEmailDomain(BaseControl $input, $domain) { // otros validadores } } ``` -El uso es entonces muy simple: +El uso es después muy directo: ```php $form->addInteger('num') ->addRule( [MyValidators::class, 'validateDivisibility'], - 'El valor debe ser un múltiplo de %d', + 'The value must be a multiple of %d', 8, ); ``` -Las reglas de validación personalizadas también se pueden añadir a JavaScript. La condición es que la regla sea un método estático. Su nombre para el validador JavaScript se crea concatenando el nombre de la clase sin barras invertidas `\`, un guion bajo `_` y el nombre del método. Por ejemplo, `App\MyValidators::validateDivisibility` se escribe como `AppMyValidators_validateDivisibility` y se añade al objeto `Nette.validators`: +Las reglas de validación propias también se pueden añadir a JavaScript. La condición es que la regla sea un método estático. Su nombre para el validador de JavaScript se forma concatenando el nombre de la clase sin las barras invertidas `\`, un guion bajo `_` y el nombre del método. Por ejemplo, `App\MyValidators::validateDivisibility` se escribe como `AppMyValidators_validateDivisibility` y se añade al objeto `Nette.validators`: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -214,32 +222,32 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => Evento onValidate ================= -Después de enviar el formulario, se realiza la validación, donde se comprueban las reglas individuales añadidas mediante `addRule()` y luego se dispara el [evento |nette:glossary#Eventos] `onValidate`. Su handler se puede usar para validación adicional, típicamente para verificar la combinación correcta de valores en múltiples elementos del formulario. +Tras enviar el formulario se realiza la validación, que comprueba las distintas reglas añadidas con `addRule()`, y a continuación se dispara el [evento |nette:glossary#Eventos] `onValidate`. Su manejador se puede usar para validaciones adicionales, normalmente para verificar la combinación correcta de valores en varios elementos del formulario. -Si se detecta un error, lo pasamos al formulario con el método `addError()`. Se puede llamar en un elemento específico o directamente en el formulario. +Si se detecta un error, se le pasa al formulario con el método `addError()`. Se puede llamar sobre un elemento concreto o directamente sobre el formulario. ```php protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Esta combinación no es posible.'); + $form->addError('This combination is not possible.'); } } ``` -Errores durante el procesamiento -================================ +Errores de procesamiento +======================== -En muchos casos, nos enteramos del error solo cuando procesamos un formulario válido, por ejemplo, al escribir un nuevo elemento en la base de datos y encontrar una duplicidad de claves. En tal caso, nuevamente pasamos el error al formulario con el método `addError()`. Se puede llamar en un elemento específico o directamente en el formulario: +En muchos casos descubrimos un error solo al procesar un formulario válido, por ejemplo al escribir una entrada nueva en la base de datos y toparnos con una clave duplicada. En ese caso devolvemos de nuevo el error al formulario con el método `addError()`. Se puede llamar sobre un elemento concreto o directamente sobre el formulario: ```php try { @@ -249,82 +257,88 @@ try { } catch (Nette\Security\AuthenticationException $e) { if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Contraseña inválida.'); + $form->addError('Invalid password.'); } } ``` -Si es posible, recomendamos adjuntar el error directamente al elemento del formulario, ya que se mostrará junto a él al usar el renderizador predeterminado. +Si es posible, recomendamos añadir el error directamente al elemento del formulario, porque con el renderizador predeterminado se mostrará junto a él. ```php -$form['date']->addError('Lo sentimos, pero esta fecha ya está ocupada.'); +$form['date']->addError('Sorry, this date is already taken.'); ``` -Puede llamar a `addError()` repetidamente para pasar múltiples mensajes de error al formulario o elemento. Los obtiene usando `getErrors()`. +Puede llamar a `addError()` repetidamente para pasar varios mensajes de error a un formulario o a un elemento. Puede obtenerlos con `getErrors()`. -Tenga en cuenta que `$form->getErrors()` devuelve un resumen de todos los mensajes de error, incluidos los que se pasaron directamente a elementos individuales, no solo directamente al formulario. Los mensajes de error pasados solo al formulario se obtienen a través de `$form->getOwnErrors()`. +Tenga en cuenta que `$form->getErrors()` devuelve un resumen de todos los mensajes de error, incluidos los que se pasaron directamente a los distintos elementos, no solo los pasados directamente al formulario. Los mensajes de error pasados solo al formulario se pueden obtener con `$form->getOwnErrors()`. -Modificación de la entrada -========================== +Modificar los valores de entrada +================================ -Usando el método `addFilter()`, podemos modificar el valor introducido por el usuario. En este ejemplo, toleraremos y eliminaremos los espacios en el código postal: +Con el método `addFilter()` podemos modificar el valor introducido por el usuario. En este ejemplo toleraremos y eliminaremos los espacios del código postal: ```php -$form->addText('zip', 'Código Postal:') +$form->addText('zip', 'Postal Code:') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // eliminamos los espacios del código postal + return str_replace(' ', '', $value); // elimina los espacios del código postal }) - ->addRule($form::Pattern, 'El código postal no tiene el formato de cinco dígitos', '\d{5}'); + ->addRule($form::Pattern, 'Postal code is not five digits', '\d{5}'); ``` -El filtro se integra entre las reglas y condiciones de validación, por lo que el orden de los métodos importa, es decir, el filtro y la regla se llaman en el mismo orden que los métodos `addFilter()` y `addRule()`. +El filtro se integra entre las reglas de validación y las condiciones, así que el orden de los métodos importa: el filtro y la regla se llaman en el mismo orden en el que están escritos los métodos `addFilter()` y `addRule()`. -Validación JavaScript -===================== +Validación en JavaScript +======================== -El lenguaje para formular condiciones y reglas es muy potente. Todas las construcciones funcionan tanto en el lado del servidor como en el lado de JavaScript. Se transmiten en atributos HTML `data-nette-rules` como JSON. La validación en sí la realiza un script que captura el evento `submit` del formulario, recorre los elementos individuales y realiza la validación correspondiente. +El lenguaje para formular condiciones y reglas es muy potente. Todas las construcciones funcionan tanto en el lado del servidor como en el lado del cliente en JavaScript. Se transfieren en los atributos HTML `data-nette-rules` como JSON. De la validación en sí se ocupa un script que intercepta el evento `submit` del formulario, recorre los distintos elementos y realiza la validación correspondiente. -Ese script es `netteForms.js` y está disponible desde múltiples fuentes posibles: +Ese script es `netteForms.js` y está disponible desde varias fuentes posibles: -Puede insertar el script directamente en la página HTML desde un CDN: +Puede incrustar el script directamente en la página HTML desde una CDN: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -O copiarlo localmente a la carpeta pública del proyecto (p. ej., desde `vendor/nette/forms/src/assets/netteForms.min.js`): +O copiarlo localmente a la carpeta pública de su proyecto (p. ej. desde `vendor/nette/forms/src/assets/netteForms.min.js`): ```latte <script src="/path/to/netteForms.min.js"></script> ``` -O instalarlo a través de [npm|https://www.npmjs.com/package/nette-forms]: +O instalarlo con [npm |https://www.npmjs.com/package/nette-forms]: ```shell npm install nette-forms ``` -Y luego cargarlo y ejecutarlo: +Y después cargarlo y ejecutarlo: ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -Alternativamente, puede cargarlo directamente desde la carpeta `vendor`: +Alternativamente puede cargarlo directamente de la carpeta `vendor`: ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Puede desactivar por completo la validación en el cliente añadiendo al formulario el atributo `novalidate`. El script `netteForms.js` se salta entonces su validación al enviarlo, así que la validación ocurre solo en el servidor: + +```php +$form->setHtmlAttribute('novalidate'); +``` + JavaScript dinámico =================== -¿Quiere mostrar los campos para introducir la dirección solo si el usuario elige enviar la mercancía por correo? No hay problema. La clave es el par de métodos `addCondition()` & `toggle()`: +¿Quiere mostrar los campos de la dirección solo si el usuario elige que le envíen la mercancía por correo? Ningún problema. La clave está en la pareja de métodos `addCondition()` y `toggle()`: ```php $form->addCheckbox('send_it') @@ -332,25 +346,25 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -Este código dice que cuando se cumple la condición, es decir, cuando la casilla de verificación está marcada, el elemento HTML `#address-container` será visible. Y viceversa. Así, colocamos los elementos del formulario con la dirección del destinatario en un contenedor con este ID y al hacer clic en la casilla de verificación se ocultarán o mostrarán. Esto lo asegura el script `netteForms.js`. +Este código dice que, cuando se cumpla la condición (es decir, cuando el checkbox esté marcado), el elemento HTML `#address-container` será visible, y al revés. Así que colocamos los elementos del formulario con la dirección del destinatario en un contenedor con ese ID y se ocultarán o mostrarán al pulsar el checkbox. De eso se ocupa el script `netteForms.js`. -Como argumento del método `toggle()`, se puede pasar cualquier selector. Por razones históricas, una cadena alfanumérica sin otros caracteres especiales se entiende como el ID del elemento, es decir, igual que si le precediera el carácter `#`. El segundo parámetro opcional permite invertir el comportamiento, es decir, si usáramos `toggle('#address-container', false)`, el elemento se mostraría solo si la casilla de verificación no estuviera marcada. +Como argumento del método `toggle()` se puede pasar cualquier selector. Por motivos históricos, una cadena que empiece por una letra, un dígito o un guion bajo y contenga solo letras, dígitos, guiones bajos, guiones, puntos y dos puntos se trata como el ID de un elemento, igual que si fuera precedida del carácter `#`. El segundo parámetro, opcional, permite invertir el comportamiento; por ejemplo, si usáramos `toggle('#address-container', false)`, el elemento se mostraría solo si el checkbox *no* estuviera marcado. -La implementación predeterminada en JavaScript cambia la propiedad `hidden` de los elementos. Sin embargo, podemos cambiar fácilmente el comportamiento, por ejemplo, añadir una animación. Simplemente sobrescriba el método `Nette.toggle` en JavaScript con su propia solución: +La implementación predeterminada de JavaScript cambia la propiedad `hidden` de los elementos. Pero podemos cambiar el comportamiento con facilidad, por ejemplo añadiendo una animación. Basta con sobrescribir en JavaScript el método `Nette.toggle` con una solución propia: ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // ocultamos o mostramos 'el' según el valor de 'visible' + // oculta o muestra 'el' según el valor de 'visible' }); }; ``` -Desactivación de la validación -============================== +Desactivar la validación +======================== -A veces puede ser útil desactivar la validación. Si al presionar un botón de envío no se debe realizar la validación (adecuado para botones *Cancelar* o *Vista previa*), la desactivamos con el método `$submit->setValidationScope([])`. Si debe realizar solo una validación parcial, podemos especificar qué campos o contenedores de formulario se deben validar. +A veces puede resultar útil desactivar la validación. Si pulsar un botón de envío no debe realizar la validación (adecuado para los botones *Cancelar* o *Vista previa*), la desactivamos con el método `$submit->setValidationScope([])`. Si debe realizar solo una validación parcial, podemos indicar qué campos o contenedores del formulario hay que validar. ```php $form->addText('name') @@ -366,11 +380,13 @@ $form->addSubmit('send1'); // Valida todo el formulario $form->addSubmit('send2') ->setValidationScope([]); // No valida nada $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Valida solo el elemento name + ->setValidationScope([$form['name']]); // Valida solo el elemento 'name' $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Valida solo el elemento age + ->setValidationScope([$form['details']['age']]); // Valida solo el elemento 'age' $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Valida el contenedor details + ->setValidationScope([$form['details']]); // Valida el contenedor 'details' ``` -`setValidationScope` no afecta al [#evento onValidate] del formulario, que siempre será llamado. El evento `onValidate` de un contenedor solo se activará si ese contenedor está marcado para validación parcial. +`setValidationScope` no afecta al [#Evento onValidate] del formulario, que se llamará siempre. El evento `onValidate` de un contenedor solo se disparará si ese contenedor está marcado para la validación parcial. + +La validación parcial afecta además a los valores que devuelve `getValues()`: el resultado contiene solo los valores de los elementos que caen dentro del alcance de la validación. Los valores de los elementos que quedan fuera de ese alcance se omiten. diff --git a/forms/fr/@home.texy b/forms/fr/@home.texy index d5fdcc6546..d400e78c2d 100644 --- a/forms/fr/@home.texy +++ b/forms/fr/@home.texy @@ -3,18 +3,18 @@ Nette Forms <div class=perex> -Nette Forms a révolutionné la création de formulaires web. Soudain, il suffisait d'écrire quelques lignes de code compréhensibles pour avoir un formulaire complet, y compris le rendu, la validation JavaScript et côté serveur, et en plus, extrêmement sécurisé. Nous allons vous montrer comment +Nette Forms a révolutionné la création de formulaires web. Soudain, il suffisait d'écrire quelques lignes de code claires pour obtenir un formulaire complet, avec le rendu, la validation JavaScript et côté serveur, et par-dessus le marché une sécurité de premier ordre. Nous allons vous montrer comment : - créer des formulaires conviviaux - valider les données soumises -- rendre les éléments exactement selon les besoins +- rendre les éléments exactement comme vous le souhaitez </div> -En utilisant Nette Forms, vous éviterez de nombreuses tâches routinières, comme l'écriture de la validation (de plus, double, côté serveur et client), vous minimiserez la probabilité d'erreurs et de failles de sécurité. +Grâce à Nette Forms, vous éviterez de nombreuses tâches routinières, comme l'écriture de la logique de validation (côté serveur comme côté client), et vous réduirez au minimum le risque d'erreurs et de failles de sécurité. -Vous pouvez utiliser les formulaires soit comme partie intégrante de l'Application Nette (c'est-à-dire dans les presenters), soit de manière totalement autonome. Comme l'utilisation diffère légèrement dans les deux cas, nous avons préparé deux tutoriels pour vous : +Vous pouvez utiliser les formulaires soit comme partie d'une application Nette (c'est-à-dire dans les presenters), soit de manière totalement autonome. Comme l'utilisation diffère légèrement dans les deux cas, nous avons préparé pour vous deux guides distincts : <div class="wiki-buttons"> <div> "Formulaires dans les presenters .[wiki-button]":in-presenter </div> @@ -25,7 +25,7 @@ Vous pouvez utiliser les formulaires soit comme partie intégrante de l'Applicat Installation ------------ -Téléchargez et installez la bibliothèque à l'aide de l'outil [Composer|best-practices:composer] : +Téléchargez et installez le paquet à l'aide de [Composer|best-practices:composer] : ```shell composer require nette/forms diff --git a/forms/fr/@left-menu.texy b/forms/fr/@left-menu.texy index cc724a0d4c..0f3437880f 100644 --- a/forms/fr/@left-menu.texy +++ b/forms/fr/@left-menu.texy @@ -1,14 +1,16 @@ Nette Forms *********** - [Introduction |@home] -- [Formulaires dans les presenters |in-presenter] -- [Formulaires autonomes |standalone] -- [Éléments de formulaire |controls] +- [Formulaires dans les presenters|in-presenter] +- [Formulaires autonomes|standalone] +- [Champs de formulaire |controls] - [Validation |validation] - [Rendu |rendering] -- [Configuration |configuration] +- [Champs personnalisés |custom-controls] +- [Configuration|configuration] +- [Mise à niveau|upgrading] -Lectures complémentaires -************************ -- [Tutoriels et bonnes pratiques |best-practices:] +Pour aller plus loin +******************** +- [Bonnes pratiques |best-practices:] diff --git a/forms/fr/configuration.texy b/forms/fr/configuration.texy index b649796647..354677edd9 100644 --- a/forms/fr/configuration.texy +++ b/forms/fr/configuration.texy @@ -2,7 +2,7 @@ Configuration des formulaires ***************************** .[perex] -Dans la configuration, il est possible de modifier les [messages d'erreur des formulaires|validation] par défaut. +Dans la configuration, vous pouvez modifier les [messages d'erreur des formulaires|validation] par défaut. ```neon forms: @@ -17,6 +17,7 @@ forms: 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.' @@ -35,27 +36,28 @@ Voici la traduction française : ```neon forms: messages: - Equal: 'Veuillez entrer %s.' + Equal: 'Veuillez saisir %s.' NotEqual: 'Cette valeur ne devrait pas être %s.' - Filled: 'Ce champ est requis.' + Filled: 'Ce champ est obligatoire.' Blank: 'Ce champ devrait être vide.' - MinLength: 'Veuillez entrer au moins %d caractères.' - MaxLength: 'Veuillez ne pas entrer plus de %d caractères.' - Length: 'Veuillez entrer une valeur de %d à %d caractères.' - Email: 'Veuillez entrer une adresse e-mail valide.' - URL: 'Veuillez entrer une URL valide.' - Integer: 'Veuillez entrer un entier valide.' - Float: 'Veuillez entrer un nombre valide.' - Min: 'Veuillez entrer une valeur supérieure ou égale à %d.' - Max: 'Veuillez entrer une valeur inférieure ou égale à %d.' - Range: 'Veuillez entrer une valeur entre %d et %d.' - MaxFileSize: 'La taille du fichier téléchargé peut être au maximum de %d octets.' - MaxPostSize: 'Les données téléchargées dépassent la limite de %d octets.' - MimeType: 'Le fichier téléchargé n\'est pas dans le format attendu.' - Image: 'Le fichier téléchargé doit être une image au format JPEG, GIF, PNG, WebP ou AVIF.' + MinLength: 'Veuillez saisir au moins %d caractères.' + MaxLength: 'Veuillez saisir au plus %d caractères.' + Length: 'Veuillez saisir une valeur de %d à %d caractères.' + Email: 'Veuillez saisir une adresse e-mail valide.' + URL: 'Veuillez saisir une URL valide.' + Integer: 'Veuillez saisir un nombre entier valide.' + Numeric: 'Veuillez saisir un nombre entier non négatif.' + Float: 'Veuillez saisir un nombre valide.' + Min: 'Veuillez saisir une valeur supérieure ou égale à %d.' + Max: 'Veuillez saisir une valeur inférieure ou égale à %d.' + Range: 'Veuillez saisir une valeur comprise entre %d et %d.' + MaxFileSize: 'La taille du fichier envoyé peut être au maximum de %d octets.' + MaxPostSize: 'Les données envoyées dépassent la limite de %d octets.' + MimeType: 'Le fichier envoyé n''est pas au format attendu.' + Image: 'Le fichier envoyé doit être une image au format JPEG, GIF, PNG ou WebP.' Nette\Forms\Controls\SelectBox::Valid: 'Veuillez sélectionner une option valide.' - Nette\Forms\Controls\UploadControl::Valid: 'Une erreur est survenue lors du téléchargement du fichier.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Votre session a expiré. Veuillez retourner à la page d\'accueil et réessayer.' + Nette\Forms\Controls\UploadControl::Valid: 'Une erreur est survenue lors de l''envoi du fichier.' + Nette\Forms\Controls\CsrfProtection::Protection: 'Votre session a expiré. Retournez à la page d''accueil et réessayez.' ``` -Si vous n'utilisez pas l'ensemble du framework et donc pas les fichiers de configuration, vous pouvez modifier les messages d'erreur par défaut directement dans le tableau `Nette\Forms\Validator::$messages`. +Si vous n'utilisez pas l'ensemble du framework et donc pas non plus les fichiers de configuration, vous pouvez modifier les messages d'erreur par défaut directement dans le tableau `Nette\Forms\Validator::$messages`. diff --git a/forms/fr/controls.texy b/forms/fr/controls.texy index 851deaf09a..484a520052 100644 --- a/forms/fr/controls.texy +++ b/forms/fr/controls.texy @@ -1,14 +1,14 @@ -Éléments de formulaire -********************** +Champs de formulaire +******************** .[perex] -Aperçu des éléments de formulaire standard. +Aperçu des champs de formulaire standards. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -Ajoute un champ de texte sur une seule ligne (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Si l'utilisateur ne remplit pas le champ, il renvoie une chaîne vide `''`, ou `setNullable()` peut être utilisé pour spécifier qu'il renvoie `null`. +Ajoute un champ texte sur une ligne (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Si l'utilisateur ne remplit pas le champ, il renvoie une chaîne vide `''` ; utilisez `setNullable()` pour qu'il renvoie `null` à la place. ```php $form->addText('name', 'Nom :') @@ -16,56 +16,56 @@ $form->addText('name', 'Nom :') ->setNullable(); ``` -Valide automatiquement l'UTF-8, supprime les espaces de début et de fin et supprime les sauts de ligne qu'un attaquant pourrait envoyer. +Valide automatiquement l'UTF-8, supprime les espaces au début et à la fin, et enlève les sauts de ligne qu'un attaquant pourrait envoyer. -La longueur maximale peut être limitée à l'aide de `setMaxLength()`. La modification de la valeur saisie par l'utilisateur est possible avec [addFilter() |validation#Modification de l entrée]. +La longueur maximale peut être limitée avec `setMaxLength()`. La méthode [addFilter() |validation#Modifier les valeurs saisies] permet de modifier la valeur saisie par l'utilisateur. -Avec `setHtmlType()`, le caractère visuel du champ de texte peut être modifié en types tels que `search`, `tel` ou `url`, voir la [spécification|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. N'oubliez pas que le changement de type est purement visuel et ne remplace pas la fonction de validation. Pour le type `url`, il est conseillé d'ajouter une [règle de validation URL |validation#Entrées textuelles] spécifique. +Avec `setHtmlType()`, vous pouvez changer l'apparence visuelle du champ texte en types comme `search`, `tel` ou `url`, tels que définis par la [spécification|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Rappelez-vous que le changement de type est purement visuel et ne remplace pas la validation. Pour le type `url`, il est conseillé d'ajouter une [règle de validation d'URL |validation#Champs texte]. .[note] -Pour d'autres types d'entrées, tels que `number`, `range`, `email`, `date`, `datetime-local`, `time` et `color`, utilisez des méthodes spécialisées comme [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] et [#addColor], qui assurent la validation côté serveur. Les types `month` et `week` ne sont pas encore entièrement pris en charge par tous les navigateurs. +Pour les autres types d'input comme `number`, `range`, `email`, `date`, `datetime-local`, `time` et `color`, utilisez les méthodes spécialisées [#addInteger()], [#addFloat()], [#addEmail()], [#addDate()], [#addTime()], [#addDateTime()] et [#addColor()], qui assurent la validation côté serveur. Les types `month` et `week` ne sont pas encore pleinement pris en charge par tous les navigateurs. -Une valeur vide (empty-value) peut être définie pour l'élément, ce qui est similaire à une valeur par défaut, mais si l'utilisateur ne la modifie pas, l'élément renvoie une chaîne vide ou `null`. +Il est possible de définir une "valeur vide" pour le champ. Elle se comporte un peu comme une valeur par défaut, mais si l'utilisateur ne la change pas, le champ renvoie une chaîne vide ou `null`. ```php $form->addText('phone', 'Téléphone :') ->setHtmlType('tel') - ->setEmptyValue('+33'); + ->setEmptyValue('+420'); ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -Ajoute un champ pour la saisie de texte multiligne (classe [TextArea |api:Nette\Forms\Controls\TextArea]). Si l'utilisateur ne remplit pas le champ, il renvoie une chaîne vide `''`, ou `setNullable()` peut être utilisé pour spécifier qu'il renvoie `null`. +Ajoute un champ texte multiligne (classe [TextArea |api:Nette\Forms\Controls\TextArea]). Si l'utilisateur ne remplit pas le champ, il renvoie une chaîne vide `''` ; utilisez `setNullable()` pour qu'il renvoie `null` à la place. ```php $form->addTextArea('note', 'Note :') - ->addRule($form::MaxLength, 'La note est trop longue', 10000); + ->addRule($form::MaxLength, 'Votre note est bien trop longue', 10000); ``` -Valide automatiquement l'UTF-8 et normalise les séparateurs de ligne en `\n`. Contrairement au champ de saisie sur une seule ligne, aucun découpage des espaces n'est effectué. +Valide automatiquement l'UTF-8 et normalise les fins de ligne en `\n`. Contrairement au champ sur une ligne, aucun espace n'est supprimé aux extrémités. -La longueur maximale peut être limitée à l'aide de `setMaxLength()`. La modification de la valeur saisie par l'utilisateur est possible avec [addFilter() |validation#Modification de l entrée]. Une valeur vide peut être définie à l'aide de `setEmptyValue()`. +La longueur maximale peut être limitée avec `setMaxLength()`. La méthode [addFilter() |validation#Modifier les valeurs saisies] permet de modifier la valeur saisie par l'utilisateur. Une valeur vide peut être définie avec `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Ajoute un champ pour la saisie d'un nombre entier (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Renvoie soit un entier, soit `null` si l'utilisateur ne saisit rien. +Ajoute un champ de saisie d'un nombre entier (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Renvoie soit un entier, soit `null` si l'utilisateur ne saisit rien. ```php $form->addInteger('year', 'Année :') ->addRule($form::Range, 'L\'année doit être comprise entre %d et %d.', [1900, 2023]); ``` -L'élément est rendu comme `<input type="number">`. En utilisant la méthode `setHtmlType()`, le type peut être changé en `range` pour un affichage sous forme de curseur, ou en `text` si vous préférez un champ de texte standard sans le comportement spécial du type `number`. +Le champ est rendu sous forme de `<input type="number">`. À l'aide de la méthode `setHtmlType()`, vous pouvez changer le type en `range` pour l'afficher comme un curseur, ou en `text` si vous préférez un champ texte classique sans le comportement particulier du type `number`. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -Ajoute un champ pour la saisie d'un nombre décimal (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Renvoie soit un float, soit `null` si l'utilisateur ne saisit rien. +Ajoute un champ de saisie d'un nombre à virgule flottante (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Renvoie soit un nombre décimal, soit `null` si l'utilisateur ne saisit rien. ```php $form->addFloat('level', 'Niveau :') @@ -73,55 +73,55 @@ $form->addFloat('level', 'Niveau :') ->addRule($form::Range, 'Le niveau doit être compris entre %d et %d.', [0, 100]); ``` -L'élément est rendu comme `<input type="number">`. En utilisant la méthode `setHtmlType()`, le type peut être changé en `range` pour un affichage sous forme de curseur, ou en `text` si vous préférez un champ de texte standard sans le comportement spécial du type `number`. +Le champ est rendu sous forme de `<input type="number">`. À l'aide de la méthode `setHtmlType()`, vous pouvez changer le type en `range` pour l'afficher comme un curseur, ou en `text` si vous préférez un champ texte classique sans le comportement particulier du type `number`. -Nette et le navigateur Chrome acceptent à la fois la virgule et le point comme séparateurs décimaux. Pour que cette fonctionnalité soit également disponible dans Firefox, il est recommandé de définir l'attribut `lang` soit pour l'élément concerné, soit pour la page entière, par exemple `<html lang="fr">`. +Nette et le navigateur Chrome acceptent aussi bien la virgule que le point comme séparateur décimal. Pour que cela fonctionne aussi dans Firefox, il est recommandé de définir l'attribut `lang`, soit sur le champ concerné, soit sur la page entière, par exemple `<html lang="fr">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -Ajoute un champ pour la saisie d'une adresse e-mail (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Si l'utilisateur ne remplit pas le champ, il renvoie une chaîne vide `''`, ou `setNullable()` peut être utilisé pour spécifier qu'il renvoie `null`. +Ajoute un champ de saisie d'une adresse e-mail (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Si l'utilisateur ne remplit pas le champ, il renvoie une chaîne vide `''` ; utilisez `setNullable()` pour qu'il renvoie `null` à la place. ```php $form->addEmail('email', 'E-mail :'); ``` -Vérifie si la valeur est une adresse e-mail valide. Ne vérifie pas si le domaine existe réellement, vérifie uniquement la syntaxe. Valide automatiquement l'UTF-8, supprime les espaces de début et de fin. +Vérifie que la valeur est une adresse e-mail valide. Il ne contrôle pas que le domaine existe réellement, seule la syntaxe est vérifiée. Valide automatiquement l'UTF-8 et supprime les espaces au début et à la fin. -La longueur maximale peut être limitée à l'aide de `setMaxLength()`. La modification de la valeur saisie par l'utilisateur est possible avec [addFilter() |validation#Modification de l entrée]. Une valeur vide peut être définie à l'aide de `setEmptyValue()`. +La longueur maximale peut être limitée avec `setMaxLength()`. La méthode [addFilter() |validation#Modifier les valeurs saisies] permet de modifier la valeur saisie par l'utilisateur. Une valeur vide peut être définie avec `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== -Ajoute un champ pour la saisie d'un mot de passe (classe [TextInput |api:Nette\Forms\Controls\TextInput]). +Ajoute un champ de saisie de mot de passe (classe [TextInput |api:Nette\Forms\Controls\TextInput]). ```php $form->addPassword('password', 'Mot de passe :') ->setRequired() ->addRule($form::MinLength, 'Le mot de passe doit comporter au moins %d caractères', 8) - ->addRule($form::Pattern, 'Doit contenir un chiffre', '.*[0-9].*'); + ->addRule($form::Pattern, 'Le mot de passe doit contenir un chiffre', '.*[0-9].*'); ``` -Lors du réaffichage du formulaire, le champ sera vide. Valide automatiquement l'UTF-8, supprime les espaces de début et de fin et supprime les sauts de ligne qu'un attaquant pourrait envoyer. +Lors du réaffichage du formulaire, le champ sera vide. Valide automatiquement l'UTF-8, supprime les espaces au début et à la fin, et enlève les sauts de ligne qu'un attaquant pourrait envoyer. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -Ajoute une case à cocher (classe [Checkbox |api:Nette\Forms\Controls\Checkbox]). Renvoie la valeur `true` ou `false`, selon qu'elle est cochée ou non. +Ajoute une case à cocher (classe [Checkbox |api:Nette\Forms\Controls\Checkbox]). Renvoie `true` ou `false`, selon qu'elle est cochée ou non. ```php $form->addCheckbox('agree', 'J\'accepte les conditions') - ->setRequired('Il est nécessaire d\'accepter les conditions'); + ->setRequired('Vous devez accepter nos conditions'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Ajoute des cases à cocher pour sélectionner plusieurs éléments (classe [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Renvoie un tableau des clés des éléments sélectionnés. La méthode `getSelectedItems()` renvoie les valeurs au lieu des clés. +Ajoute une liste de cases à cocher permettant de sélectionner plusieurs éléments (classe [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Renvoie un tableau des clés des éléments sélectionnés. La méthode `getSelectedItems()` renvoie les éléments sélectionnés sous forme de paires clé-valeur. ```php $form->addCheckboxList('colors', 'Couleurs :', [ @@ -131,23 +131,23 @@ $form->addCheckboxList('colors', 'Couleurs :', [ ]); ``` -Le tableau des éléments proposés est passé comme troisième paramètre ou via la méthode `setItems()`. +Passez le tableau des éléments proposés en troisième paramètre ou à l'aide de la méthode `setItems()`. En passant `false` comme deuxième argument de `setItems()`, les valeurs servent aussi de clés. -Avec `setDisabled(['r', 'g'])`, il est possible de désactiver des éléments individuels. +Utilisez `setDisabled(['r', 'g'])` pour désactiver certains éléments. -L'élément vérifie automatiquement qu'il n'y a pas eu de falsification et que les éléments sélectionnés font bien partie de ceux proposés et n'ont pas été désactivés. La méthode `getRawValue()` permet d'obtenir les éléments envoyés sans ce contrôle important. +Le champ vérifie automatiquement qu'aucune falsification n'a eu lieu et que les éléments sélectionnés font bien partie de ceux proposés et n'ont pas été désactivés. La méthode `getRawValue()` permet d'obtenir les éléments soumis sans cette vérification importante. -Lors de la définition des éléments sélectionnés par défaut, il vérifie également qu'il s'agit bien de l'un des éléments proposés, sinon il lève une exception. Ce contrôle peut être désactivé avec `checkDefaultValue(false)`. +Lors de la définition des éléments sélectionnés par défaut, il vérifie aussi qu'ils font partie de ceux proposés, sinon il lève une exception. Cette vérification peut être désactivée avec `checkDefaultValue(false)`. -Si vous soumettez le formulaire avec la méthode `GET`, vous pouvez choisir un mode de transmission des données plus compact, ce qui économise la taille de la chaîne de requête. Il est activé en définissant l'attribut HTML du formulaire : +Si vous envoyez le formulaire par la méthode `GET`, vous pouvez choisir une transmission des données plus compacte, qui économise la taille de la query string. Activez-la en définissant un attribut HTML sur le formulaire : ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ Ajoute des boutons radio (classe [RadioList |api:Nette\Forms\Controls\RadioList]). Renvoie la clé de l'élément sélectionné, ou `null` si l'utilisateur n'a rien sélectionné. La méthode `getSelectedItem()` renvoie la valeur au lieu de la clé. @@ -155,86 +155,87 @@ Ajoute des boutons radio (classe [RadioList |api:Nette\Forms\Controls\RadioList] $sex = [ 'm' => 'homme', 'f' => 'femme', + 'o' => 'autre', ]; -$form->addRadioList('gender', 'Sexe :', $sex); +$form->addRadioList('gender', 'Genre :', $sex); ``` -Le tableau des éléments proposés est passé comme troisième paramètre ou via la méthode `setItems()`. +Passez le tableau des éléments proposés en troisième paramètre ou à l'aide de la méthode `setItems()`. -Avec `setDisabled(['m', 'f'])`, il est possible de désactiver des éléments individuels. +Utilisez `setDisabled(['m'])` pour désactiver certains éléments. -L'élément vérifie automatiquement qu'il n'y a pas eu de falsification et que l'élément sélectionné fait bien partie de ceux proposés et n'a pas été désactivé. La méthode `getRawValue()` permet d'obtenir l'élément envoyé sans ce contrôle important. +Le champ vérifie automatiquement qu'aucune falsification n'a eu lieu et que l'élément sélectionné fait bien partie de ceux proposés et n'a pas été désactivé. La méthode `getRawValue()` permet d'obtenir l'élément soumis sans cette vérification importante. -Lors de la définition de l'élément sélectionné par défaut, il vérifie également qu'il s'agit bien de l'un des éléments proposés, sinon il lève une exception. Ce contrôle peut être désactivé avec `checkDefaultValue(false)`. +Lors de la définition de l'élément sélectionné par défaut, il vérifie aussi qu'il fait partie de ceux proposés, sinon il lève une exception. Cette vérification peut être désactivée avec `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -Ajoute une boîte de sélection (classe [SelectBox |api:Nette\Forms\Controls\SelectBox]). Renvoie la clé de l'élément sélectionné, ou `null` si l'utilisateur n'a rien sélectionné. La méthode `getSelectedItem()` renvoie la valeur au lieu de la clé. +Ajoute une liste déroulante (classe [SelectBox |api:Nette\Forms\Controls\SelectBox]). Renvoie la clé de l'élément sélectionné, ou `null` si l'utilisateur n'a rien sélectionné. La méthode `getSelectedItem()` renvoie la valeur au lieu de la clé. ```php $countries = [ - 'FR' => 'France', - 'BE' => 'Belgique', - 'CH' => 'Suisse', + 'CZ' => 'République tchèque', + 'SK' => 'Slovaquie', + 'GB' => 'Royaume-Uni', ]; $form->addSelect('country', 'Pays :', $countries) - ->setDefaultValue('BE'); + ->setDefaultValue('SK'); ``` -Le tableau des éléments proposés est passé comme troisième paramètre ou via la méthode `setItems()`. Les éléments peuvent également être un tableau à deux dimensions : +Passez le tableau des éléments proposés en troisième paramètre ou à l'aide de la méthode `setItems()`. Les éléments peuvent aussi former un tableau à deux dimensions (représentant des optgroups) : ```php $countries = [ 'Europe' => [ - 'FR' => 'France', - 'BE' => 'Belgique', - 'CH' => 'Suisse', + 'CZ' => 'République tchèque', + 'SK' => 'Slovaquie', + 'GB' => 'Royaume-Uni', ], 'CA' => 'Canada', - 'US' => 'USA', + 'US' => 'États-Unis', '?' => 'autre', ]; ``` -Pour les boîtes de sélection, le premier élément a souvent une signification spéciale, servant d'invite à l'action. La méthode `setPrompt()` est utilisée pour ajouter un tel élément. +Dans les listes déroulantes, le premier élément a souvent une signification particulière et sert d'invitation à agir. Utilisez la méthode `setPrompt()` pour ajouter un tel élément. ```php $form->addSelect('country', 'Pays :', $countries) ->setPrompt('Choisissez un pays'); ``` -Avec `setDisabled(['FR', 'BE'])`, il est possible de désactiver des éléments individuels. +Utilisez `setDisabled(['CZ', 'SK'])` pour désactiver certains éléments. -L'élément vérifie automatiquement qu'il n'y a pas eu de falsification et que l'élément sélectionné fait bien partie de ceux proposés et n'a pas été désactivé. La méthode `getRawValue()` permet d'obtenir l'élément envoyé sans ce contrôle important. +Le champ vérifie automatiquement qu'aucune falsification n'a eu lieu et que l'élément sélectionné fait bien partie de ceux proposés et n'a pas été désactivé. La méthode `getRawValue()` permet d'obtenir l'élément soumis sans cette vérification importante. -Lors de la définition de l'élément sélectionné par défaut, il vérifie également qu'il s'agit bien de l'un des éléments proposés, sinon il lève une exception. Ce contrôle peut être désactivé avec `checkDefaultValue(false)`. +Lors de la définition de l'élément sélectionné par défaut, il vérifie aussi qu'il fait partie de ceux proposés, sinon il lève une exception. Cette vérification peut être désactivée avec `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Ajoute une boîte de sélection pour choisir plusieurs éléments (classe [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Renvoie un tableau des clés des éléments sélectionnés. La méthode `getSelectedItems()` renvoie les valeurs au lieu des clés. +Ajoute une liste déroulante permettant de sélectionner plusieurs éléments (classe [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Renvoie un tableau des clés des éléments sélectionnés. La méthode `getSelectedItems()` renvoie les éléments sélectionnés sous forme de paires clé-valeur. ```php $form->addMultiSelect('countries', 'Pays :', $countries); ``` -Le tableau des éléments proposés est passé comme troisième paramètre ou via la méthode `setItems()`. Les éléments peuvent également être un tableau à deux dimensions. +Passez le tableau des éléments proposés en troisième paramètre ou à l'aide de la méthode `setItems()`. Les éléments peuvent aussi former un tableau à deux dimensions. -Avec `setDisabled(['FR', 'BE'])`, il est possible de désactiver des éléments individuels. +Utilisez `setDisabled(['CZ', 'SK'])` pour désactiver certains éléments. -L'élément vérifie automatiquement qu'il n'y a pas eu de falsification et que les éléments sélectionnés font bien partie de ceux proposés et n'ont pas été désactivés. La méthode `getRawValue()` permet d'obtenir les éléments envoyés sans ce contrôle important. +Le champ vérifie automatiquement qu'aucune falsification n'a eu lieu et que les éléments sélectionnés font bien partie de ceux proposés et n'ont pas été désactivés. La méthode `getRawValue()` permet d'obtenir les éléments soumis sans cette vérification importante. -Lors de la définition des éléments sélectionnés par défaut, il vérifie également qu'il s'agit bien de l'un des éléments proposés, sinon il lève une exception. Ce contrôle peut être désactivé avec `checkDefaultValue(false)`. +Lors de la définition des éléments sélectionnés par défaut, il vérifie aussi qu'ils font partie de ceux proposés, sinon il lève une exception. Cette vérification peut être désactivée avec `checkDefaultValue(false)`. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Ajoute un champ pour le téléchargement de fichier (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Renvoie un objet [FileUpload |http:request#FileUpload], même si l'utilisateur n'a envoyé aucun fichier, ce qui peut être vérifié avec la méthode `FileUpload::hasFile()`. +Ajoute un champ d'upload de fichier (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Renvoie un objet [FileUpload |http:request#FileUpload], même si l'utilisateur n'a envoyé aucun fichier, ce que l'on peut vérifier avec la méthode `FileUpload::hasFile()`. Avec `setNullable()`, vous pouvez faire en sorte que le champ renvoie `null` au lieu d'un objet `FileUpload` lorsque aucun fichier n'est envoyé. ```php $form->addUpload('avatar', 'Avatar :') @@ -242,44 +243,44 @@ $form->addUpload('avatar', 'Avatar :') ->addRule($form::MaxFileSize, 'La taille maximale est de 1 Mo.', 1024 * 1024); ``` -Si le fichier ne parvient pas à être téléchargé correctement, le formulaire n'est pas soumis avec succès et une erreur s'affiche. C'est-à-dire qu'en cas de soumission réussie, il n'est pas nécessaire de vérifier la méthode `FileUpload::isOk()`. +Si le fichier n'a pas pu être envoyé correctement, le formulaire n'est pas soumis avec succès et une erreur est affichée. Autrement dit, en cas de soumission réussie, il n'est pas nécessaire de vérifier la méthode `FileUpload::isOk()`. -Ne faites jamais confiance au nom de fichier original renvoyé par la méthode `FileUpload::getName()`, le client pourrait avoir envoyé un nom de fichier malveillant dans le but d'endommager ou de pirater votre application. +Ne faites jamais confiance au nom de fichier d'origine renvoyé par la méthode `FileUpload::getName()` ; le client a pu envoyer un nom de fichier malveillant dans l'intention d'endommager ou de pirater votre application. -Les règles `MimeType` et `Image` détectent le type requis en fonction de la signature du fichier et ne vérifient pas son intégrité. Pour savoir si une image n'est pas endommagée, on peut par exemple essayer de la [charger |http:request#toImage]. +Les règles `MimeType` et `Image` détectent le type requis d'après la signature du fichier et ne vérifient pas son intégrité. Vous pouvez déterminer si une image est endommagée, par exemple, en essayant de la [charger |http:request#toImage()]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Ajoute un champ pour le téléchargement de plusieurs fichiers à la fois (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Renvoie un tableau d'objets [FileUpload |http:request#FileUpload]. La méthode `FileUpload::hasFile()` pour chacun d'eux renverra `true`. +Ajoute un champ permettant d'envoyer plusieurs fichiers à la fois (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Renvoie un tableau d'objets [FileUpload |http:request#FileUpload]. La méthode `FileUpload::hasFile()` renverra `true` pour chacun d'eux. ```php $form->addMultiUpload('files', 'Fichiers :') - ->addRule($form::MaxLength, 'Vous pouvez télécharger au maximum %d fichiers', 10); + ->addRule($form::MaxLength, 'Vous ne pouvez envoyer que %d fichiers au maximum.', 10); ``` -Si l'un des fichiers ne parvient pas à être téléchargé correctement, le formulaire n'est pas soumis avec succès et une erreur s'affiche. C'est-à-dire qu'en cas de soumission réussie, il n'est pas nécessaire de vérifier la méthode `FileUpload::isOk()`. +Si l'un des fichiers n'a pas pu être envoyé correctement, le formulaire n'est pas soumis avec succès et une erreur est affichée. Autrement dit, en cas de soumission réussie, il n'est pas nécessaire de vérifier la méthode `FileUpload::isOk()` pour chaque fichier. -Ne faites jamais confiance aux noms de fichiers originaux renvoyés par la méthode `FileUpload::getName()`, le client pourrait avoir envoyé un nom de fichier malveillant dans le but d'endommager ou de pirater votre application. +Ne faites jamais confiance aux noms de fichiers d'origine renvoyés par la méthode `FileUpload::getName()` ; le client a pu envoyer des noms de fichiers malveillants dans l'intention d'endommager ou de pirater votre application. -Les règles `MimeType` et `Image` détectent le type requis en fonction de la signature du fichier et ne vérifient pas son intégrité. Pour savoir si une image n'est pas endommagée, on peut par exemple essayer de la [charger |http:request#toImage]. +Les règles `MimeType` et `Image` détectent le type requis d'après la signature du fichier et ne vérifient pas son intégrité. Vous pouvez déterminer si une image est endommagée, par exemple, en essayant de la [charger |http:request#toImage()]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== Ajoute un champ qui permet à l'utilisateur de saisir facilement une date composée de l'année, du mois et du jour (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Comme valeur par défaut, il accepte soit des objets implémentant l'interface `DateTimeInterface`, une chaîne de caractères avec l'heure, ou un nombre représentant un timestamp UNIX. Il en va de même pour les arguments des règles `Min`, `Max` ou `Range`, qui définissent la date minimale et maximale autorisée. +Comme valeur par défaut, il accepte des objets implémentant `DateTimeInterface`, une chaîne contenant une heure, ou un nombre représentant un timestamp UNIX. Il en va de même pour les arguments des règles `Min`, `Max` ou `Range`, qui définissent les dates minimale et maximale autorisées. ```php $form->addDate('date', 'Date :') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'La date doit dater d\'au moins un mois.', new DateTime('-1 month')); + ->addRule($form::Min, 'La date doit être vieille d\'au moins un mois.', new DateTime('-1 month')); ``` -Par défaut, il renvoie un objet `DateTimeImmutable`, avec la méthode `setFormat()`, vous pouvez spécifier un [format texte|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou un timestamp : +Par défaut, il renvoie un objet `DateTimeImmutable`. À l'aide de la méthode `setFormat()`, vous pouvez indiquer un [format texte|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou un timestamp : ```php $form->addDate('date', 'Date :') @@ -287,19 +288,19 @@ $form->addDate('date', 'Date :') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -Ajoute un champ qui permet à l'utilisateur de saisir facilement une heure composée des heures, des minutes et éventuellement des secondes (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Ajoute un champ qui permet à l'utilisateur de saisir facilement une heure composée des heures, des minutes et, éventuellement, des secondes (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Comme valeur par défaut, il accepte soit des objets implémentant l'interface `DateTimeInterface`, une chaîne de caractères avec l'heure, ou un nombre représentant un timestamp UNIX. De ces entrées, seule l'information temporelle est utilisée, la date est ignorée. Il en va de même pour les arguments des règles `Min`, `Max` ou `Range`, qui définissent l'heure minimale et maximale autorisée. Si la valeur minimale définie est supérieure à la valeur maximale, une plage horaire dépassant minuit est créée. +Comme valeur par défaut, il accepte des objets implémentant `DateTimeInterface`, une chaîne contenant une heure, ou un nombre représentant un timestamp UNIX. Seule l'information horaire de ces entrées est utilisée, la date est ignorée. Il en va de même pour les arguments des règles `Min`, `Max` ou `Range`, qui définissent les heures minimale et maximale autorisées. Si la valeur minimale définie est supérieure à la maximale, une plage horaire à cheval sur minuit est créée. ```php $form->addTime('time', 'Heure :', withSeconds: true) ->addRule($form::Range, 'L\'heure doit être comprise entre %d et %d.', ['12:30', '13:30']); ``` -Par défaut, il renvoie un objet `DateTimeImmutable` (avec la date du 1er janvier de l'an 1), avec la méthode `setFormat()`, vous pouvez spécifier un [format texte|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] : +Par défaut, il renvoie un objet `DateTimeImmutable` (avec la date fixée au 1er janvier de l'an 1). À l'aide de la méthode `setFormat()`, vous pouvez indiquer un [format texte|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] : ```php $form->addTime('time', 'Heure :') @@ -307,20 +308,20 @@ $form->addTime('time', 'Heure :') ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -Ajoute un champ qui permet à l'utilisateur de saisir facilement une date et une heure composées de l'année, du mois, du jour, des heures, des minutes et éventuellement des secondes (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Ajoute un champ qui permet à l'utilisateur de saisir facilement à la fois la date et l'heure, composées de l'année, du mois, du jour, des heures, des minutes et, éventuellement, des secondes (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Comme valeur par défaut, il accepte soit des objets implémentant l'interface `DateTimeInterface`, une chaîne de caractères avec l'heure, ou un nombre représentant un timestamp UNIX. Il en va de même pour les arguments des règles `Min`, `Max` ou `Range`, qui définissent la date minimale et maximale autorisée. +Comme valeur par défaut, il accepte des objets implémentant `DateTimeInterface`, une chaîne contenant une heure, ou un nombre représentant un timestamp UNIX. Il en va de même pour les arguments des règles `Min`, `Max` ou `Range`, qui définissent la date et l'heure minimales et maximales autorisées. ```php $form->addDateTime('datetime', 'Date et heure :') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'La date doit dater d\'au moins un mois.', new DateTime('-1 month')); + ->addRule($form::Min, 'La date doit être vieille d\'au moins un mois.', new DateTime('-1 month')); ``` -Par défaut, il renvoie un objet `DateTimeImmutable`, avec la méthode `setFormat()`, vous pouvez spécifier un [format texte|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou un timestamp : +Par défaut, il renvoie un objet `DateTimeImmutable`. À l'aide de la méthode `setFormat()`, vous pouvez indiquer un [format texte|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou un timestamp : ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -Ajoute un champ pour choisir une couleur (classe [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). La couleur est une chaîne de caractères au format `#rrggbb`. Si l'utilisateur ne fait pas de choix, la couleur noire `#000000` est renvoyée. +Ajoute un champ de sélection de couleur (classe [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). La couleur est renvoyée sous forme de chaîne au format `#rrggbb`. Si l'utilisateur ne fait aucun choix, il renvoie le noir `#000000`. ```php $form->addColor('color', 'Couleur :') @@ -339,8 +340,8 @@ $form->addColor('color', 'Couleur :') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== Ajoute un champ caché (classe [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -348,28 +349,37 @@ Ajoute un champ caché (classe [HiddenField |api:Nette\Forms\Controls\HiddenFiel $form->addHidden('userid'); ``` -Avec `setNullable()`, il est possible de définir qu'il renvoie `null` au lieu d'une chaîne vide. La modification de la valeur envoyée est possible avec [addFilter() |validation#Modification de l entrée]. +Utilisez `setNullable()` pour qu'il renvoie `null` au lieu d'une chaîne vide. La méthode [addFilter() |validation#Modifier les valeurs saisies] permet de modifier la valeur soumise. -Bien que l'élément soit caché, il est **important de noter** que la valeur peut toujours être modifiée ou falsifiée par un attaquant. Vérifiez et validez toujours soigneusement toutes les valeurs reçues côté serveur pour prévenir les risques de sécurité liés à la manipulation des données. +Bien que le champ soit caché, **il est important de comprendre** que sa valeur peut malgré tout être modifiée ou falsifiée par un attaquant. Vérifiez et validez toujours soigneusement toutes les valeurs reçues côté serveur afin d'éviter les risques de sécurité liés à la manipulation des données. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== -Ajoute un bouton de soumission (classe [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). +Ajoute un bouton d'envoi (classe [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). ```php $form->addSubmit('submit', 'Envoyer'); ``` -Il est possible d'avoir plusieurs boutons de soumission dans un formulaire : +.{data-version:3.3.0} +Le gestionnaire peut être passé directement au bouton comme troisième paramètre `$onSubmit`, au lieu de l'accrocher à l'événement `onClick` : + +```php +$form->addSubmit('submit', 'Envoyer', function (SubmitButton $button, $data): void { + // ... +}); +``` + +Il est possible d'avoir plus d'un bouton d'envoi dans le formulaire : ```php $form->addSubmit('register', 'S\'inscrire'); $form->addSubmit('cancel', 'Annuler'); ``` -Pour savoir sur lequel on a cliqué, utilisez : +Pour savoir lequel a été cliqué, utilisez : ```php if ($form['register']->isSubmittedBy()) { @@ -377,13 +387,13 @@ if ($form['register']->isSubmittedBy()) { } ``` -Si vous ne souhaitez pas valider l'ensemble du formulaire lors de l'appui sur un bouton (par exemple, pour les boutons *Annuler* ou *Aperçu*), utilisez [setValidationScope() |validation#Désactivation de la validation]. +Si vous ne voulez pas valider tout le formulaire lors de l'appui sur un bouton (par exemple pour les boutons *Annuler* ou *Aperçu*), utilisez [setValidationScope() |validation#Désactiver la validation]. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -Ajoute un bouton (classe [Button |api:Nette\Forms\Controls\Button]) qui n'a pas de fonction de soumission. Il peut donc être utilisé pour une autre fonction, par exemple appeler une fonction JavaScript lors d'un clic. +Ajoute un bouton (classe [Button |api:Nette\Forms\Controls\Button]) qui n'a pas de fonction d'envoi. Il peut donc servir à d'autres fonctions, par exemple appeler une fonction JavaScript au clic. ```php $form->addButton('raise', 'Augmenter le salaire') @@ -391,34 +401,34 @@ $form->addButton('raise', 'Augmenter le salaire') ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= -Ajoute un bouton de soumission sous forme d'image (classe [ImageButton |api:Nette\Forms\Controls\ImageButton]). +Ajoute un bouton d'envoi sous forme d'image (classe [ImageButton |api:Nette\Forms\Controls\ImageButton]). ```php -$form->addImageButton('submit', '/chemin/vers/image'); +$form->addImageButton('submit', '/path/to/image.png', 'Envoyer'); ``` -En utilisant plusieurs boutons de soumission, on peut savoir sur lequel on a cliqué à l'aide de `$form['submit']->isSubmittedBy()`. +Avec plusieurs boutons d'envoi, vous pouvez savoir lequel a été cliqué à l'aide de `$form['submit']->isSubmittedBy()`. addContainer(string|int $name): Container .[method] =================================================== -Ajoute un sous-formulaire (classe [Container|api:Nette\Forms\Container]), ou conteneur, auquel on peut ajouter d'autres éléments de la même manière qu'on les ajoute au formulaire. Les méthodes `setDefaults()` ou `getValues()` fonctionnent également. +Ajoute un sous-formulaire (classe [Container|api:Nette\Forms\Container]), autrement dit un conteneur, auquel d'autres champs peuvent être ajoutés de la même façon qu'au formulaire. Les méthodes comme `setDefaults()` ou `getValues()` fonctionnent également. ```php $sub1 = $form->addContainer('first'); $sub1->addText('name', 'Votre nom :'); -$sub1->addEmail('email', 'Email :'); +$sub1->addEmail('email', 'E-mail :'); $sub2 = $form->addContainer('second'); $sub2->addText('name', 'Votre nom :'); -$sub2->addEmail('email', 'Email :'); +$sub2->addEmail('email', 'E-mail :'); ``` -Les données envoyées sont alors renvoyées sous forme de structure multidimensionnelle : +Les données soumises sont alors renvoyées sous forme de structure multidimensionnelle : ```php [ @@ -434,69 +444,67 @@ Les données envoyées sont alors renvoyées sous forme de structure multidimens ``` -Aperçu des paramètres -===================== +Aperçu des réglages +=================== -Pour tous les éléments, nous pouvons appeler les méthodes suivantes (aperçu complet dans la [documentation API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]) : +Pour tous les champs, nous pouvons appeler les méthodes suivantes (voir la [documentation de l'API|https://api.nette.org/forms/master/Nette/Forms/Controls.html] pour un aperçu complet) : .[table-form-methods language-php] -| `setDefaultValue($value)` | définit la valeur par défaut +| `setDefaultValue($value)` | définit la valeur par défaut | `getValue()` | obtient la valeur actuelle -| `setOmitted()` | [#omission de valeur] -| `setDisabled()` | [#désactivation des éléments] +| `setOmitted()` | [#Valeurs omises] +| `setDisabled()` | [#Désactiver des champs] Rendu : .[table-form-methods language-php] -| `setCaption($caption)` | modifie l'étiquette de l'élément +| `setCaption($caption)` | change le label du champ | `setTranslator($translator)` | définit le [traducteur |rendering#Traduction] -| `setHtmlAttribute($name, $value)` | définit l'[attribut HTML |rendering#Attributs HTML] de l'élément +| `setHtmlAttribute($name, $value)` | définit un [attribut HTML |rendering#Attributs HTML] de l'élément | `setHtmlId($id)` | définit l'attribut HTML `id` -| `setHtmlType($type)` | définit l'attribut HTML `type` -| `setHtmlName($name)` | définit l'attribut HTML `name` -| `setOption($key, $value)` | [options de rendu |rendering#Options] +| `setOption($key, $value)` | [définit les options de rendu |rendering#Options] Validation : .[table-form-methods language-php] -| `setRequired()` | [élément requis |validation] -| `addRule()` | définit une [règle de validation |validation#Règles] +| `setRequired()` | rend le champ [obligatoire |validation] +| `addRule()` | ajoute une [règle de validation |validation#Règles] | `addCondition()`, `addConditionOn()` | définit une [condition de validation |validation#Conditions] -| `addError($message)` | [transmission du message d'erreur |validation#Erreurs lors du traitement] +| `addError($message)` | [ajoute un message d'erreur |validation#Erreurs lors du traitement] -Pour les éléments `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, les méthodes suivantes peuvent être appelées : +Pour les champs `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, les méthodes suivantes peuvent être appelées : .[table-form-methods language-php] | `setNullable()` | définit si getValue() renvoie `null` au lieu d'une chaîne vide | `setEmptyValue($value)` | définit une valeur spéciale considérée comme une chaîne vide -| `setMaxLength($length)` | définit le nombre maximal de caractères autorisés -| `addFilter($filter)` | [modification de l'entrée |validation#Modification de l entrée] +| `setMaxLength($length)` | définit le nombre maximal de caractères autorisé +| `addFilter($filter)` | [modifie la saisie |validation#Modifier les valeurs saisies] -Omission de valeur -================== +Valeurs omises +============== -Si la valeur remplie par l'utilisateur ne nous intéresse pas, nous pouvons l'omettre du résultat de la méthode `$form->getValues()` ou des données transmises aux handlers à l'aide de `setOmitted()`. Ceci est utile pour divers mots de passe de contrôle, éléments anti-spam, etc. +Si la valeur saisie par l'utilisateur ne nous intéresse pas, nous pouvons l'exclure du résultat de la méthode `$form->getValues()` ou des données passées aux gestionnaires à l'aide de `setOmitted()`. C'est utile pour les différents champs de confirmation de mot de passe, les champs anti-spam, etc. ```php -$form->addPassword('passwordVerify', 'Mot de passe pour vérification :') - ->setRequired('Veuillez saisir à nouveau le mot de passe pour vérification') +$form->addPassword('passwordVerify', 'Mot de passe à nouveau :') + ->setRequired('Saisissez à nouveau votre mot de passe pour détecter une faute de frappe') ->addRule($form::Equal, 'Les mots de passe ne correspondent pas', $form['password']) ->setOmitted(); ``` -Désactivation des éléments -========================== +Désactiver des champs +===================== -Les éléments peuvent être désactivés à l'aide de `setDisabled()`. Un tel élément ne peut pas être modifié par l'utilisateur. +Les champs peuvent être désactivés à l'aide de `setDisabled()`. Un champ désactivé ne peut pas être modifié par l'utilisateur. ```php $form->addText('username', 'Nom d\'utilisateur :') ->setDisabled(); ``` -Les éléments désactivés ne sont pas du tout envoyés par le navigateur au serveur, vous ne les trouverez donc pas dans les données renvoyées par la fonction `$form->getValues()`. Cependant, si vous définissez `setOmitted(false)`, Nette inclura leur valeur par défaut dans ces données. +Les champs désactivés ne sont pas du tout envoyés par le navigateur au serveur, vous ne les trouverez donc pas dans les données renvoyées par la fonction `$form->getValues()`. Si vous définissez cependant `setOmitted(false)`, Nette inclura leur valeur par défaut dans ces données. -Lors de l'appel de `setDisabled()`, la **valeur de l'élément est effacée** pour des raisons de sécurité. Si vous définissez une valeur par défaut, il est donc nécessaire de le faire après sa désactivation : +Lors de l'appel de `setDisabled()`, la **valeur du champ est effacée** pour des raisons de sécurité. Si vous définissez une valeur par défaut, il faut donc le faire après l'avoir désactivé : ```php $form->addText('username', 'Nom d\'utilisateur :') @@ -504,42 +512,26 @@ $form->addText('username', 'Nom d\'utilisateur :') ->setDefaultValue($userName); ``` -Une alternative aux éléments désactivés sont les éléments avec l'attribut HTML `readonly`, que le navigateur envoie au serveur. Bien que l'élément soit en lecture seule, il est **important de noter** que sa valeur peut toujours être modifiée ou falsifiée par un attaquant. +Une alternative aux champs désactivés est celle des champs portant l'attribut HTML `readonly`, que le navigateur envoie bien au serveur. Bien que le champ soit en lecture seule, **il est important de comprendre** que sa valeur peut malgré tout être modifiée ou falsifiée par un attaquant. -Éléments personnalisés -====================== +Champs personnalisés +==================== -En plus de la large gamme d'éléments de formulaire intégrés, vous pouvez ajouter vos propres éléments personnalisés au formulaire de cette manière : +Outre la large palette de champs de formulaire intégrés, vous pouvez ajouter au formulaire des champs personnalisés : ```php $form->addComponent(new DateInput('Date :'), 'date'); // syntaxe alternative : $form['date'] = new DateInput('Date :'); ``` -.[note] -Le formulaire est un enfant de la classe [Container |component-model:#Container] et les éléments individuels sont des enfants de [Component |component-model:#Component]. - -Il existe une manière de définir de nouvelles méthodes de formulaire servant à ajouter des éléments personnalisés (par ex. `$form->addZip()`). Il s'agit de ce qu'on appelle les méthodes d'extension. L'inconvénient est que l'autocomplétion dans les éditeurs ne fonctionnera pas pour elles. - -```php -use Nette\Forms\Container; - -// ajoutons la méthode addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Au moins 5 chiffres', '[0-9]{5}'); -}); - -// utilisation -$form->addZip('zip', 'Code postal :'); -``` +La façon d'écrire un tel champ, y compris la lecture des données soumises, la validation et le rendu, est décrite dans un [chapitre distinct |custom-controls]. Vous y découvrirez aussi les méthodes d'extension, qui vous permettent de créer votre propre méthode d'ajout comme `$form->addZip()`. -Éléments de bas niveau -====================== +Champs de bas niveau +==================== -Il est également possible d'utiliser des éléments que nous écrivons uniquement dans le template et que nous n'ajoutons pas au formulaire avec l'une des méthodes `$form->addXyz()`. Par exemple, lorsque nous affichons des enregistrements d'une base de données et que nous ne savons pas à l'avance combien il y en aura ni quels seront leurs ID, et que nous voulons afficher une case à cocher ou un bouton radio pour chaque ligne, il suffit de le coder dans le template : +Il est aussi possible d'utiliser des champs qui ne sont écrits que dans le template et ne sont ajoutés au formulaire par aucune des méthodes `$form->addXyz()`. Par exemple, quand nous listons des enregistrements d'une base de données dont nous ne connaissons à l'avance ni le nombre ni les identifiants, et que nous voulons afficher une case à cocher ou un bouton radio pour chaque ligne, il suffit de le coder dans le template : ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ Il est également possible d'utiliser des éléments que nous écrivons uniqueme {/foreach} ``` -Et après soumission, nous obtenons la valeur : +Et après la soumission, nous récupérons la valeur : ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -où le premier paramètre est le type d'élément (`DataFile` pour `type=file`, `DataLine` pour les entrées sur une seule ligne comme `text`, `password`, `email`, etc. et `DataText` pour tous les autres) et le deuxième paramètre `sel[]` correspond à l'attribut HTML name. Nous pouvons combiner le type d'élément avec la valeur `DataKeys`, qui préserve les clés des éléments. Ceci est particulièrement utile pour `select`, `radioList` et `checkboxList`. +où le premier paramètre est le type de l'élément (`DataFile` pour `type=file`, `DataLine` pour les champs sur une ligne comme `text`, `password`, `email`, etc., et `DataText` pour tous les autres) et le deuxième paramètre `sel[]` correspond à l'attribut HTML name. Nous pouvons combiner le type de l'élément avec la valeur `DataKeys`, qui conserve les clés des éléments. C'est particulièrement utile pour `select`, `radioList` et `checkboxList`. -L'essentiel est que `getHttpData()` renvoie une valeur assainie, dans ce cas, ce sera toujours un tableau de chaînes UTF-8 valides, quoi que l'attaquant ait tenté de soumettre au serveur. C'est analogue au travail direct avec `$_POST` ou `$_GET`, mais avec la différence essentielle qu'il renvoie toujours des données propres, comme vous en avez l'habitude avec les éléments standard des formulaires Nette. +Point essentiel : `getHttpData()` renvoie une valeur assainie. Dans ce cas, ce sera toujours un tableau de chaînes UTF-8 valides, quoi qu'un attaquant essaie d'envoyer au serveur. C'est analogue au travail direct avec `$_POST` ou `$_GET`, à la différence majeure qu'il renvoie toujours des données propres, comme vous en avez l'habitude avec les champs de formulaire standards de Nette. diff --git a/forms/fr/custom-controls.texy b/forms/fr/custom-controls.texy new file mode 100644 index 0000000000..f312e3582a --- /dev/null +++ b/forms/fr/custom-controls.texy @@ -0,0 +1,268 @@ +Champs de formulaire personnalisés +********************************** + +.[perex] +Nette propose une large palette de [champs de formulaire intégrés |controls]. Mais lorsque vous vous heurtez à un besoin qui n'y figure pas, vous n'avez rien à contourner ni à bricoler : vous écrivez votre propre champ. Il saura faire tout ce que font les champs intégrés - se valider, se traduire, se rendre - et il s'utilisera exactement de la même façon. + +Nous le montrerons sur un exemple concret : un champ de saisie de date à l'aide de trois cases, jour, mois et année. Chemin faisant, vous apprendrez tout ce qu'il faut savoir pour écrire un champ. + + +Quand écrire un champ personnalisé et quand s'en abstenir +========================================================= + +Un champ personnalisé est l'outil le plus puissant qu'offrent les formulaires. Et comme tout outil puissant, il devrait être le dernier choix, pas le premier. Beaucoup de situations se règlent par des moyens plus simples : + +- **Modifier une valeur** relève d'[addFilter() |validation#Modifier les valeurs saisies]. Vous voulez tolérer les espaces dans un code postal ou les minuscules dans un code ? Un filtre tient en quelques lignes. +- **Une configuration répétée** s'emballe dans une méthode d'ajout personnalisée. Vous ajoutez à dix endroits un champ de code postal avec la même validation ? Créez-leur un raccourci nommé, [nous le montrerons à la fin |#Méthode d'ajout personnalisée]. +- **Un groupe de champs liés** est servi par un [conteneur |controls#addContainer()]. Une adresse composée de la rue, de la ville et du code postal n'a pas besoin d'un champ personnalisé, un conteneur avec trois champs texte suffit. +- **Une apparence différente** s'obtient avec [setHtmlType() |controls#addText()] et les attributs HTML, ou avec les [prototypes |rendering#Prototypes]. + +Un champ personnalisé prend tout son sens dès que vous avez besoin d'une **valeur propre** : un champ qui, vu de l'extérieur, se comporte comme un seul champ portant une seule valeur, mais qui se compose en interne de plusieurs inputs ou stocke la valeur autrement qu'il ne l'affiche. Une date à partir de trois cases. Des coordonnées choisies en cliquant sur une carte. Une saisie de tags avec autocomplétion. + + +Anatomie d'un champ +=================== + +Chaque champ personnalisé hérite de la classe abstraite [api:Nette\Forms\Controls\BaseControl]. Il en hérite une énorme quantité de fonctionnalités toutes prêtes : le stockage de la valeur, les règles et conditions de validation, les messages d'erreur, les traductions, les attributs HTML, le label et le lien avec le rendu. Vous n'écrivez que ce qui distingue votre champ. + +Un champ fonctionnel minimal est étonnamment court : + +```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, + ]); + } +} +``` + +Deux méthodes : l'une dit comment obtenir la valeur à partir des données soumises, l'autre comment rendre le champ. Nous allons les examiner de près dans un instant. Tout le reste - `setRequired()`, `addRule()`, `setDefaultValue()`, les traductions - fonctionne déjà tout seul. + +Vous ajoutez le champ au formulaire avec la méthode `addComponent()`, ou plus brièvement à l'aide des crochets : + +```php +$form['nickname'] = new SimpleInput('Pseudo :'); +``` + + +Cycle de vie d'un champ +======================= + +Avant d'en venir à un champ plus intéressant, il est bon de savoir ce qui arrive à un champ, et quand. Le formulaire et ses champs sont des [composants |component-model:] formant un arbre. Cela a une conséquence agréable : le champ n'a rien à découvrir tout seul, le framework s'occupe de tout ce qui compte au bon moment : + +1) Dès que vous rattachez le champ à un formulaire soumis, le formulaire lui-même appelle `loadHttpData()` dessus. Le champ y lit la valeur qui lui a été soumise, comme nous allons le montrer. Il ne travaille jamais directement avec `$_POST` et n'a pas du tout à se soucier de savoir s'il est imbriqué dans des conteneurs. + +2) Lors de la soumission du formulaire, la validation a lieu : les règles ajoutées par `addRule()` sont évaluées et travaillent avec la valeur de `getValue()`. + +3) Celui qui appelle ensuite `$form->getValues()` ou `getValue()` sur le champ obtient une valeur propre et typée - par exemple un objet `DateTimeImmutable`, et non un trio de chaînes venu du formulaire. + +Et lors du rendu, c'est `getControl()` qui est appelée, ou `getLabel()` pour le label. + + +Lire la valeur soumise +====================== + +Dans la méthode `loadHttpData()`, le champ demande la valeur qui lui a été soumise à l'aide de la méthode `getHttpData()`. Son paramètre est un type qui détermine la façon dont la valeur doit être nettoyée : + +| type | signification +|------- +| `Form::DataLine` | texte sur une ligne : remplace les sauts de ligne par des espaces, supprime les espaces aux extrémités +| `Form::DataText` | texte multiligne : normalise les fins de ligne en `\n` +| `Form::DataFile` | upload, une instance de `Nette\Http\FileUpload` + +Quels que soient les efforts d'un attaquant, le résultat est toujours une chaîne UTF-8 valide sans caractères de contrôle (ou un objet d'upload, ou `null`). C'est exactement pour cela que nous ne lisons jamais la valeur directement dans `$_POST` : nous perdrions toutes ces garanties. + +Un champ composé de plusieurs inputs, comme notre date, passe en second paramètre une partie du nom HTML et lit ainsi ses différentes sous-valeurs. Il les stocke dans ses propres propriétés `$day`, `$month` et `$year` de type 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]') ?? ''; +} +``` + +Si le nom HTML se termine par `[]`, un tableau de valeurs est renvoyé. En le combinant avec le type `Form::DataKeys` (c'est-à-dire `Form::DataLine | Form::DataKeys`), vous en conservez aussi les clés : + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +Une valeur manquante vaut `null` (un tableau vide pour les tableaux). La requête n'est pas obligée de contenir les données du champ, rien n'empêche un attaquant d'envoyer ce qu'il veut - c'est pourquoi nous ajoutons `?? ''` dans l'exemple et pourquoi vous devriez toujours prévoir cette éventualité. + + +La valeur du champ +================== + +Le champ conserve sa valeur et l'expose par un trio de méthodes dont il vaut mieux respecter le contrat. + +La méthode `setValue()` accepte une valeur venant du programmeur - c'est aussi le chemin qu'empruntent `setDefaultValue()` et `$form->setDefaults()`. Elle devrait accepter tout ce qui a du sens, convertir la valeur dans sa forme interne et lever une exception sur une entrée absurde, pour que l'erreur apparaisse tout de suite et non à travers un comportement mystérieux du formulaire. Notre date accepte un `DateTimeInterface`, une chaîne, un timestamp ou `null`, et les répartit dans les trois cases : + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // une absurdité lève une exception + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +La méthode `getValue()`, à l'inverse, compose une valeur propre et typée - la seule que verra l'utilisateur de votre champ. Si la valeur n'est pas valide, elle renvoie `null`. La méthode statique `validateDate()` vérifie simplement que les trois cases forment une date existante : + +```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; +} +``` + +Et la méthode `isFilled()` dit si l'utilisateur a rempli le champ - c'est la règle `setRequired()` qui l'utilise. L'implémentation par défaut (une valeur non vide) suffit souvent, mais pour un champ composite, redéfinissez-la selon sa logique : + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Rendu +===== + +La méthode `getControl()` renvoie la forme HTML du champ, généralement comme objet [Html |utils:html-elements], mais une simple chaîne convient tout aussi bien - cela n'a pas d'importance. Nous recourons à l'objet Html surtout pour assembler le code, car il nous permet de construire le balisage résultant en toute sécurité et avec une API agréable. Vous disposez de plusieurs aides : + +- `getHtmlName()` renvoie l'attribut HTML `name`, imbrication éventuelle dans des conteneurs comprise (par exemple `invoice[date]`). Pour un champ composite, vous y ajoutez les parties de nom des différents inputs : `$name . '[day]'`. +- `getHtmlId()` renvoie l'attribut `id` relié au label. +- `Helpers::exportRules($this->getRules())` exporte les règles de validation pour l'attribut `data-nette-rules`, grâce auquel la [validation JavaScript |validation#Validation JavaScript] fonctionnera aussi pour votre champ. Cet attribut a sa place sur le premier input du champ. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` assemble un élément `<select>` à partir d'un tableau d'éléments (les tableaux imbriqués sont rendus en `<optgroup>`) et le renvoie sous forme d'`Html` - pratique pour la case du mois de notre date. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` génère une liste d'éléments `<input>` enveloppés dans des `<label>` (boutons radio ou cases à cocher) et la renvoie sous forme de chaîne. + +La première case de notre date se crée donc ainsi : + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select pour le mois et input pour l'année ... */); +} +``` + +Le label est rendu par `getLabel()` et son implémentation par défaut convient généralement. Attention seulement : pour un champ composite, son attribut `for` pointe vers `getHtmlId()`, donnez donc cet id au premier input - exactement comme dans l'exemple. + +Pour que le champ composite puisse être rendu partie par partie dans un template (par exemple `{input birthdate:day}`), redéfinissez les méthodes `getControlPart($key)` et `getLabelPart($key)`, qui renvoient l'élément `Html` de la partie donnée - de la même façon que le font `CheckboxList` et `RadioList`. + +.[note] +Si vous redéfinissez `getControl()`, gardez à l'esprit que `BaseControl::getControl()` marque aussi le champ comme rendu via `setOption('rendered', true)`. Appelez-la également (ou appelez `parent::getControl()`) lorsque vous combinez le rendu manuel et automatique d'un même formulaire, afin que le champ ne soit pas rendu deux fois. (L'exemple `DateInput` ci-dessus l'omet par souci de concision.) + + +Exemple complet : DateInput +=========================== + +Toutes les pièces décrites réunies, complétées par une liste déroulante pour choisir le mois, se trouvent dans le champ `DateInput` terminé, parmi les [exemples présents dans le dépôt |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Remarquez que, dans le constructeur, le champ s'ajoute à lui-même une règle de validation qui vérifie que la date a un sens. Une entrée absurde, comme le 31 février, se manifeste ainsi par une simple erreur de validation du formulaire : + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'La date est invalide.'); +} +``` + +Et l'utilisation ? Exactement comme avec les champs intégrés : + +```php +$form['birthdate'] = (new DateInput('Date de naissance :')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('Quand êtes-vous né ?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +Dans un template Latte, vous le rendez avec la balise habituelle `{input birthdate}` ou `{label birthdate /}`, comme n'importe quel autre champ. + + +Validation +========== + +Les règles de validation intégrées fonctionnent immédiatement avec un champ personnalisé - elles travaillent sur la valeur de `getValue()`. Notre `DateInput` peut ainsi utiliser, par exemple, `Form::Min` pour la date la plus ancienne autorisée. La façon d'écrire vos propres règles, y compris leur pendant JavaScript, est décrite dans le chapitre [Règles et conditions personnalisées |validation#Règles et conditions personnalisées]. + + +Méthode d'ajout personnalisée +============================= + +Nous ajoutons les champs intégrés avec les méthodes commodes `$form->addText()` et consorts. Un champ personnalisé n'a pas de telle méthode, vous l'ajoutez donc par simple affectation - cela fonctionne pareillement dans un formulaire et dans un conteneur, et les éditeurs comme l'analyse statique le comprennent : + +```php +$form['birthdate'] = new DateInput('Date de naissance :'); +``` + +Si vous voulez raccourcir l'ajout tout en conservant l'autocomplétion, une méthode fabrique statique posée directement sur le champ est bien pratique. Elle fonctionne même dans des conteneurs imbriqués, ce qu'une méthode sur un descendant de la classe `Form` ne saurait faire - les conteneurs imbriqués ne la connaissent pas : + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// fonctionne dans un formulaire et dans n'importe quel conteneur : +DateInput::addTo($form, 'birthdate', 'Date de naissance :'); +``` + +La même approche fonctionne aussi comme raccourci nommé pour une configuration répétée d'un champ intégré : + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'Le code postal doit comporter exactement 5 chiffres', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'Code postal :'); +``` diff --git a/forms/fr/in-presenter.texy b/forms/fr/in-presenter.texy index 442ef5b88a..61e1f9c013 100644 --- a/forms/fr/in-presenter.texy +++ b/forms/fr/in-presenter.texy @@ -2,15 +2,15 @@ Formulaires dans les presenters ******************************* .[perex] -Nette Forms facilite grandement la création et le traitement des formulaires web. Dans ce chapitre, vous apprendrez à utiliser les formulaires à l'intérieur des presenters. +Nette Forms simplifie considérablement la création et le traitement des formulaires web. Dans ce chapitre, vous apprendrez à utiliser les formulaires à l'intérieur des presenters. -Si vous vous demandez comment les utiliser de manière totalement autonome sans le reste du framework, le guide pour une [utilisation autonome|standalone] est fait pour vous. +Si vous êtes intéressé par leur utilisation totalement autonome, sans le reste du framework, un guide est consacré à l'[utilisation autonome|standalone]. Premier formulaire ================== -Essayons d'écrire un formulaire d'inscription simple. Son code sera le suivant : +Essayons d'écrire un simple formulaire d'inscription. Son code sera le suivant : ```php use Nette\Application\UI\Form; @@ -19,16 +19,16 @@ $form = new Form; $form->addText('name', 'Nom :'); $form->addPassword('password', 'Mot de passe :'); $form->addSubmit('send', 'S\'inscrire'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` -et dans le navigateur, il s'affichera comme ceci : +et dans le navigateur, il s'affichera ainsi : -[* form-cs.webp *] +[* form-en.webp *] -Le formulaire dans le presenter est un objet de la classe `Nette\Application\UI\Form`, son prédécesseur `Nette\Forms\Form` est destiné à une utilisation autonome. Nous y avons ajouté des éléments appelés nom, mot de passe et un bouton d'envoi. Et enfin, la ligne avec `$form->onSuccess` indique qu'après l'envoi et la validation réussie, la méthode `$this->formSucceeded()` doit être appelée. +Un formulaire dans un presenter est un objet de la classe `Nette\Application\UI\Form` ; son prédécesseur `Nette\Forms\Form` est destiné à une utilisation autonome. Nous y avons ajouté des champs nommés name et password, ainsi qu'un bouton d'envoi. Enfin, la ligne `$form->onSuccess` indique qu'après la soumission et une validation réussie, la méthode `$this->formSucceeded()` doit être appelée. -Du point de vue du presenter, le formulaire est un composant courant. Par conséquent, il est traité comme un composant et intégré au presenter à l'aide d'une [méthode factory |application:components#Méthodes Factory]. Cela ressemblera à ceci : +Du point de vue du presenter, le formulaire est un composant ordinaire. Il est donc traité comme un composant et intégré au presenter à l'aide d'une [méthode fabrique |application:components#Méthodes Factory]. Cela ressemblera à ceci : ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -42,22 +42,22 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Nom :'); $form->addPassword('password', 'Mot de passe :'); $form->addSubmit('send', 'S\'inscrire'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { - // ici nous traitons les données envoyées par le formulaire + // nous traiterons ici les données envoyées par le formulaire // $data->name contient le nom // $data->password contient le mot de passe - $this->flashMessage('Vous avez été enregistré avec succès.'); + $this->flashMessage('Vous vous êtes inscrit avec succès.'); $this->redirect('Home:'); } } ``` -Et dans le template, nous affichons le formulaire avec la balise `{control}` : +Et dans le template, le formulaire se rend à l'aide de la balise `{control}` : ```latte .{file:app/Presentation/Home/default.latte} <h1>Inscription</h1> @@ -65,45 +65,47 @@ Et dans le template, nous affichons le formulaire avec la balise `{control}` : {control registrationForm} ``` -Et c'est tout :-) Nous avons un formulaire fonctionnel et parfaitement [sécurisé |#Protection contre les vulnérabilités]. +Et c'est à peu près tout :-) Nous avons un formulaire fonctionnel et parfaitement [sécurisé |#Protection contre les vulnérabilités]. -Et maintenant, vous vous dites probablement que c'était trop rapide, vous vous demandez comment il est possible que la méthode `formSucceeded()` soit appelée et quels sont les paramètres qu'elle reçoit. Bien sûr, vous avez raison, cela mérite une explication. +Vous vous dites sans doute que cela est allé trop vite et vous vous demandez comment il se fait que la méthode `formSucceeded()` soit appelée et quels paramètres elle reçoit. Oui, vous avez raison, cela mérite une explication. -Nette propose en effet un mécanisme rafraîchissant que nous appelons le [style Hollywood |application:components#Style Hollywood]. Au lieu que vous, en tant que développeur, deviez constamment demander si quelque chose s'est passé ("le formulaire a-t-il été envoyé ?", "a-t-il été envoyé valablement ?" et "n'a-t-il pas été falsifié ?"), vous dites au framework "une fois que le formulaire sera valablement rempli, 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 |nette:glossary#Événements events] se produit. Et le langage leur transmet les arguments appropriés. +Nette introduit un mécanisme rafraîchissant appelé [style hollywoodien |application:components#Style Hollywood]. Au lieu que vous, en tant que développeur, ayez sans cesse à demander si quelque chose s'est produit ('le formulaire a-t-il été envoyé ?', 'a-t-il été envoyé valablement ?' et 'n'a-t-il pas été falsifié ?'), vous dites au framework 'quand le formulaire sera valablement rempli, appelle cette méthode' et vous lui laissez le reste du travail. Si vous programmez en JavaScript, vous connaissez intimement ce style de programmation. Vous écrivez des fonctions qui sont appelées quand un certain [événement |nette:glossary#Événements] survient. Et le langage leur passe les arguments appropriés. -C'est précisément ainsi qu'est construit le code du presenter ci-dessus. Le tableau `$form->onSuccess` représente une liste de callbacks PHP que Nette appellera au moment où le formulaire est envoyé et correctement rempli (c'est-à-dire qu'il est valide). Dans le cadre du [cycle de vie du presenter |application:presenters#Cycle de vie du presenter], il s'agit d'un signal, ils sont donc appelés après la méthode `action*` et avant la méthode `render*`. Et à chaque callback, il passe comme premier paramètre le formulaire lui-même et comme second les données envoyées sous forme d'objet [ArrayHash |utils:arrays#ArrayHash]. Vous pouvez omettre le premier paramètre si vous n'avez pas besoin de l'objet formulaire. Et le second paramètre peut être plus malin, mais nous en reparlerons [plus tard |#Mappage sur les classes]. +C'est exactement ainsi qu'est construit le code du presenter ci-dessus. Le tableau `$form->onSuccess` représente une liste de callbacks PHP que Nette appelle au moment où le formulaire est envoyé et correctement rempli (autrement dit valide). Dans le [cycle de vie du presenter |application:presenters#Cycle de vie du presenter], il s'agit de ce qu'on appelle un signal ; ils sont donc appelés après la méthode `action*` et avant la méthode `render*`. Et à chaque callback, il passe le formulaire lui-même en premier paramètre et les données soumises en deuxième, sous forme d'objet [ArrayHash |utils:arrays#ArrayHash] (ou stdClass, ou une classe personnalisée). Vous pouvez omettre le premier paramètre si vous n'avez pas besoin de l'objet formulaire. Le deuxième paramètre peut être plus malin, mais nous y reviendrons [plus loin |#Mapping vers des classes]. -L'objet `$data` contient les clés `name` et `password` avec les informations que l'utilisateur a remplies. Habituellement, nous envoyons directement les données pour un traitement ultérieur, ce qui peut être par exemple une insertion dans la base de données. Cependant, une erreur peut survenir pendant le traitement, par exemple le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire à l'aide de `addError()` et le laissons se réafficher, avec le message d'erreur. +L'objet `$data` contient les propriétés `name` et `password` avec les données saisies par l'utilisateur. Habituellement, nous envoyons les données directement au traitement suivant, qui peut être par exemple leur insertion dans une base de données. Une erreur peut cependant survenir pendant ce traitement, par exemple si le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire à l'aide d'`addError()` et le laissons se rendre à nouveau, avec le message d'erreur. ```php -$form->addError('Désolé, ce nom d\'utilisateur est déjà pris.'); +$form->addError('Désolé, ce nom d\'utilisateur est déjà utilisé.'); ``` -En plus de `onSuccess`, il existe également `onSubmit` : les callbacks sont toujours appelés après l'envoi du formulaire, même s'il n'est pas correctement rempli. Et ensuite `onError` : les callbacks ne sont appelés que si l'envoi n'est pas valide. Ils sont même appelés si, dans `onSuccess` ou `onSubmit`, nous invalidons le formulaire avec `addError()`. +Outre `onSuccess`, il existe aussi `onSubmit` : les callbacks sont appelés chaque fois que le formulaire est envoyé, même s'il n'est pas rempli correctement. Et aussi `onError` : les callbacks ne sont appelés que si la soumission n'est pas valide. Ils sont même appelés si nous invalidons le formulaire dans `onSuccess` à l'aide d'`addError()`. -Après le traitement du formulaire, nous redirigeons vers la page suivante. Cela évite la ré-soumission involontaire du formulaire par le bouton *actualiser*, *retour* ou par le déplacement dans l'historique du navigateur. +Après le traitement du formulaire, nous redirigeons vers une autre page. Cela évite le renvoi involontaire du formulaire par le bouton *actualiser*, *retour* ou en naviguant dans l'historique du navigateur. -Essayez d'ajouter d'autres [éléments de formulaire|controls]. +Si le formulaire est envoyé en AJAX, vous redessinez généralement un [snippet |application:ajax] contenant le formulaire re-rendu au lieu de rediriger. +Essayez d'ajouter d'autres [champs de formulaire|controls]. -Accès aux éléments -================== -Le formulaire est un composant du presenter, dans notre cas nommé `registrationForm` (d'après le nom de la méthode factory `createComponentRegistrationForm`), donc n'importe où dans le presenter, vous pouvez accéder au formulaire via : +Accès aux champs +================ + +Le formulaire est un composant du presenter, dans notre cas nommé `registrationForm` (d'après le nom de la méthode fabrique `createComponentRegistrationForm`), vous pouvez donc accéder au formulaire de n'importe où dans le presenter à l'aide de : ```php $form = $this->getComponent('registrationForm'); // syntaxe alternative : $form = $this['registrationForm']; ``` -Les éléments individuels du formulaire sont également des composants, vous pouvez donc y accéder de la même manière : +Les différents champs du formulaire sont eux aussi des composants, vous y accédez donc de la même façon : ```php $input = $form->getComponent('name'); // ou $input = $form['name']; $button = $form->getComponent('send'); // ou $button = $form['send']; ``` -Les éléments sont supprimés avec unset : +Les champs se suppriment à l'aide d'`unset` : ```php unset($form['name']); @@ -113,26 +115,26 @@ unset($form['name']); Règles de validation ==================== -Le mot *valide* a été mentionné, mais le formulaire n'a pas encore de règles de validation. Corrigeons cela. +Le mot *valide* a été prononcé, mais le formulaire n'a encore aucune règle de validation. Corrigeons cela. -Le nom sera obligatoire, nous le marquerons donc avec la méthode `setRequired()`, dont l'argument est le texte du message d'erreur qui s'affichera si l'utilisateur ne remplit pas le nom. Si l'argument n'est pas fourni, le message d'erreur par défaut sera utilisé. +Le nom sera obligatoire, nous le marquons donc avec la méthode `setRequired()`. Son argument est le texte du message d'erreur affiché si l'utilisateur ne remplit pas le nom. Si l'argument est omis, un message d'erreur par défaut est utilisé. ```php $form->addText('name', 'Nom :') - ->setRequired('Veuillez entrer un nom'); + ->setRequired('Veuillez saisir votre nom.'); ``` -Essayez d'envoyer le formulaire sans remplir le nom et vous verrez que le message d'erreur s'affichera et que le navigateur ou le serveur le refusera jusqu'à ce que vous remplissiez le champ. +Essayez d'envoyer le formulaire sans remplir le nom et vous verrez s'afficher un message d'erreur ; le navigateur ou le serveur le refusera tant que vous n'aurez pas rempli le champ. -En même temps, vous ne tromperez pas le système en tapant, par exemple, uniquement des espaces dans le champ. Non. Nette supprime automatiquement les espaces de début et de fin. Essayez. C'est quelque chose que vous devriez toujours faire avec chaque input sur une seule ligne, mais on l'oublie souvent. Nette le fait automatiquement. (Vous pouvez essayer de tromper le formulaire et envoyer une chaîne multiligne comme nom. Même ici, Nette ne se laissera pas berner et changera les sauts de ligne en espaces.) +En même temps, vous ne pourrez pas tricher en saisissant, par exemple, uniquement des espaces dans le champ. Impossible. Nette supprime automatiquement les espaces au début et à la fin. Essayez. C'est une chose que vous devriez toujours faire avec chaque champ sur une ligne, et que l'on oublie pourtant souvent. Nette le fait automatiquement. (Vous pouvez essayer de piéger le formulaire en envoyant comme nom une chaîne sur plusieurs lignes. Là non plus Nette ne se laisse pas avoir : les sauts de ligne seront convertis en espaces.) -Le formulaire est toujours validé côté serveur, mais une validation JavaScript est également générée, qui s'exécute instantanément et l'utilisateur est informé de l'erreur immédiatement, sans avoir besoin d'envoyer le formulaire au serveur. C'est le script `netteForms.js` qui s'en charge. Insérez-le dans le template de layout : +Le formulaire est toujours validé côté serveur, mais une validation JavaScript est également générée ; elle s'exécute immédiatement et l'utilisateur apprend l'erreur tout de suite, sans avoir à envoyer le formulaire au serveur. C'est le script `netteForms.js` qui s'en charge. Incluez-le dans votre template de layout : ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Si vous regardez le code source de la page avec le formulaire, vous remarquerez peut-être que Nette insère les éléments requis dans des éléments avec la classe CSS `required`. Essayez d'ajouter la feuille de style suivante au template et l'étiquette "Nom" sera rouge. Nous marquons ainsi élégamment les éléments requis pour les utilisateurs : +Si vous regardez le code source de la page contenant le formulaire, vous remarquerez peut-être que Nette enveloppe les champs obligatoires dans des éléments portant la classe CSS `required`. Essayez d'ajouter la feuille de style suivante à votre template et le label 'Nom' deviendra rouge. Cela met élégamment en évidence les champs obligatoires pour les utilisateurs : ```latte <style> @@ -140,85 +142,87 @@ Si vous regardez le code source de la page avec le formulaire, vous remarquerez </style> ``` -Nous ajoutons d'autres règles de validation avec la méthode `addRule()`. Le premier paramètre est la règle, le second est à nouveau le texte du message d'erreur et il peut y avoir un argument de règle de validation. Qu'est-ce que cela signifie ? +Nous ajoutons d'autres règles de validation avec la méthode `addRule()`. Le premier paramètre est la règle, le deuxième là encore le texte du message d'erreur, et un argument de la règle de validation peut suivre. Qu'est-ce que cela veut dire ? -Nous étendons le formulaire avec un nouveau champ facultatif "âge", qui doit être un entier (`addInteger()`) et de plus dans une plage autorisée (`$form::Range`). Et c'est ici que nous utilisons le troisième paramètre de la méthode `addRule()`, avec lequel nous passons la plage requise au validateur sous forme de paire `[de, à]` : +Étoffons le formulaire d'un nouveau champ facultatif 'age', qui doit être un nombre entier (`addInteger()`) et se situer dans une plage autorisée (`$form::Range`). Nous utiliserons ici le troisième paramètre de la méthode `addRule()` pour passer au validateur la plage requise sous forme de paire `[min, max]` : ```php $form->addInteger('age', 'Âge :') - ->addRule($form::Range, 'L\'âge doit être compris entre 18 et 120', [18, 120]); + ->addRule($form::Range, 'L\'âge doit être compris entre 18 et 120 ans.', [18, 120]); ``` .[tip] Si l'utilisateur ne remplit pas le champ, les règles de validation ne seront pas vérifiées, car l'élément est facultatif. -Il y a ici place pour un petit refactoring. Dans le message d'erreur et dans le troisième paramètre, les nombres sont indiqués en double, ce qui n'est pas idéal. Si nous créions des [formulaires multilingues |rendering#Traduction] et que le message contenant des nombres était traduit dans plusieurs langues, un éventuel changement de valeurs serait compliqué. Pour cette raison, il est possible d'utiliser les placeholders `%d` et Nette complétera les valeurs : +Cela laisse place à un petit refactoring. Dans le message d'erreur et dans le troisième paramètre, les nombres sont dupliqués, ce qui n'est pas idéal. Si nous créions des [formulaires multilingues |rendering#Traduction] et que le message contenant les nombres était traduit en plusieurs langues, changer les valeurs deviendrait difficile. C'est pourquoi les placeholders `%d` peuvent être utilisés, et Nette y insérera les valeurs : ```php - ->addRule($form::Range, 'L\'âge doit être compris entre %d et %d ans', [18, 120]); + ->addRule($form::Range, 'L\'âge doit être compris entre %d et %d ans.', [18, 120]); ``` -Revenons à l'élément `password`, que nous rendrons également obligatoire et vérifierons la longueur minimale du mot de passe (`$form::MinLength`), en utilisant à nouveau le placeholder : +Revenons au champ `password`, rendons-le lui aussi obligatoire et vérifions également la longueur minimale du mot de passe (`$form::MinLength`), là encore à l'aide d'un placeholder dans le message : ```php $form->addPassword('password', 'Mot de passe :') ->setRequired('Choisissez un mot de passe') - ->addRule($form::MinLength, 'Le mot de passe doit comporter au moins %d caractères', 8); + ->addRule($form::MinLength, 'Votre mot de passe doit comporter au moins %d caractères.', 8); ``` -Ajoutons au formulaire un champ `passwordVerify`, où l'utilisateur saisira à nouveau le mot de passe, pour vérification. À l'aide des règles de validation, nous vérifions si les deux mots de passe sont identiques (`$form::Equal`). Et comme paramètre, nous donnons une référence au premier mot de passe en utilisant des [crochets |#Accès aux éléments] : +Ajoutons au formulaire un autre champ `passwordVerify`, où l'utilisateur saisit le mot de passe une seconde fois pour confirmation. À l'aide des règles de validation, nous contrôlons que les deux mots de passe sont identiques (`$form::Equal`). Comme argument, nous fournissons une référence au premier mot de passe à l'aide des [crochets |#Accès aux champs] : ```php -$form->addPassword('passwordVerify', 'Mot de passe pour vérification :') - ->setRequired('Veuillez saisir à nouveau le mot de passe pour vérification') - ->addRule($form::Equal, 'Les mots de passe ne correspondent pas', $form['password']) +$form->addPassword('passwordVerify', 'Mot de passe à nouveau :') + ->setRequired('Saisissez à nouveau votre mot de passe pour détecter une faute de frappe') + ->addRule($form::Equal, 'Les mots de passe ne correspondent pas.', $form['password']) ->setOmitted(); ``` -Avec `setOmitted()`, nous avons marqué l'élément dont la valeur ne nous importe pas vraiment et qui n'existe que pour la validation. La valeur n'est pas transmise à `$data`. +Avec `setOmitted()`, nous avons marqué un champ dont la valeur ne nous intéresse pas vraiment et qui n'existe qu'à des fins de validation. Sa valeur n'est pas transmise dans `$data`. -Nous avons ainsi un formulaire entièrement fonctionnel avec validation en PHP et JavaScript. Les capacités de validation de Nette sont beaucoup plus larges, on peut créer des conditions, afficher et masquer des parties de la page en fonction d'elles, etc. Vous apprendrez tout cela dans le chapitre sur la [validation des formulaires|validation]. +Nous avons ainsi un formulaire pleinement fonctionnel, avec validation en PHP comme en JavaScript. Les possibilités de validation de Nette sont bien plus larges : on peut créer des conditions, afficher ou masquer des parties de la page en fonction de celles-ci, etc. Vous apprendrez tout cela dans le chapitre sur la [validation des formulaires|validation]. Valeurs par défaut ================== -Nous définissons couramment des valeurs par défaut pour les éléments de formulaire : +Nous définissons couramment des valeurs par défaut pour les champs du formulaire : ```php $form->addEmail('email', 'E-mail') ->setDefaultValue($lastUsedEmail); ``` -Il est souvent utile de définir les valeurs par défaut de tous les éléments en même temps. Par exemple, lorsque le formulaire sert à modifier des enregistrements. Nous lisons l'enregistrement de la base de données et définissons les valeurs par défaut : +Il est souvent utile de définir les valeurs par défaut de tous les champs d'un coup. Par exemple lorsque le formulaire sert à modifier des enregistrements. Nous lisons l'enregistrement dans la base de données et définissons les valeurs par défaut : ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Appelez `setDefaults()` après avoir défini les éléments. +Appelez `setDefaults()` après avoir défini les champs. + +Sur un formulaire déjà soumis, `setDefaults()` n'a aucun effet - il n'écrasera pas ce que l'utilisateur a rempli, on peut donc l'appeler sans condition dans la factory du formulaire. Si vous avez besoin d'imposer les valeurs même après la soumission, utilisez plutôt `setValues()`. Rendu du formulaire =================== -Par défaut, le formulaire est rendu sous forme de tableau. Les éléments individuels respectent la règle d'accessibilité de base - toutes les étiquettes sont écrites comme `<label>` et liées à l'élément de formulaire correspondant. En cliquant sur l'étiquette, le curseur apparaît automatiquement dans le champ de formulaire. +Par défaut, le formulaire est rendu sous forme de tableau. Les différents champs respectent les règles de base d'accessibilité web : tous les labels sont écrits comme éléments `<label>` et associés au champ correspondant. Un clic sur le label place automatiquement le curseur dans le champ du formulaire. -Nous pouvons définir des attributs HTML arbitraires pour chaque élément. Par exemple, ajouter un placeholder : +Nous pouvons définir n'importe quels attributs HTML pour chaque champ. Ajoutons par exemple un placeholder : ```php $form->addInteger('age', 'Âge :') - ->setHtmlAttribute('placeholder', 'Veuillez remplir l\'âge'); + ->setHtmlAttribute('placeholder', 'Veuillez indiquer votre âge'); ``` -Il existe vraiment de nombreuses façons de rendre un formulaire, c'est pourquoi un [chapitre distinct sur le rendu|rendering] y est consacré. +Il existe vraiment beaucoup de façons de rendre un formulaire, c'est pourquoi un [chapitre distinct sur le rendu|rendering] y est consacré. -Mappage sur les classes -======================= +Mapping vers des classes +======================== -Revenons à la méthode `formSucceeded()`, qui reçoit dans le deuxième paramètre `$data` les données envoyées sous forme d'objet `ArrayHash`. Comme il s'agit d'une classe générique, quelque chose comme `stdClass`, il nous manquera un certain confort lors de son utilisation, comme la suggestion des propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait être résolu en ayant une classe spécifique pour chaque formulaire, dont les propriétés représentent les éléments individuels. Par exemple : +Revenons à la méthode `formSucceeded()`, qui reçoit dans son deuxième paramètre `$data` les données soumises sous forme d'objet `ArrayHash` (ou `stdClass`). Comme il s'agit d'une classe générique, semblable à `stdClass`, il nous manque certains conforts lors du travail avec elle, comme l'autocomplétion des propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait se résoudre en ayant pour chaque formulaire une classe dédiée dont les propriétés représentent les différents champs. Par exemple : ```php class RegistrationFormData @@ -229,7 +233,7 @@ class RegistrationFormData } ``` -Alternativement, vous pouvez utiliser un constructeur : +Vous pouvez aussi utiliser un constructeur : ```php class RegistrationFormData @@ -243,29 +247,31 @@ class RegistrationFormData } ``` -Les propriétés de la classe de données peuvent également être des enums et elles seront automatiquement mappées. .{data-version:3.2.4} +Les propriétés de la classe de données peuvent aussi être des enums, elles seront mappées automatiquement. .{data-version:3.2.4} -Comment dire à Nette de nous retourner les données sous forme d'objets de cette classe ? Plus facilement que vous ne le pensez. Il suffit d'indiquer la classe comme type du paramètre `$data` dans la méthode de gestion : +Comment dire à Nette de renvoyer les données comme objets de cette classe ? Plus simplement que vous ne le pensez. Il suffit d'indiquer la classe comme type du paramètre `$data` dans la méthode gestionnaire : ```php public function formSucceeded(Form $form, RegistrationFormData $data): void { - // $name est une instance de RegistrationFormData + // $data est une instance de RegistrationFormData $name = $data->name; // ... } ``` -Comme type, on peut également indiquer `array` et alors les données seront passées sous forme de tableau. +Vous pouvez aussi indiquer `array` comme type, et les données seront alors passées sous forme de tableau. -De la même manière, on peut utiliser la méthode `getValues()`, à laquelle on passe le nom de la classe ou l'objet à hydrater comme paramètre : +De la même façon, vous pouvez utiliser la méthode `getValues()` en lui passant en paramètre le nom de la classe ou un objet à hydrater : ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Si les formulaires forment une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun : +Si vous avez besoin de lire les valeurs avant que le formulaire ne soit validé - typiquement dans un gestionnaire `onValidate` - utilisez plutôt la méthode `getUntrustedValues()`. Elle accepte les mêmes paramètres que `getValues()`, mais renvoie les valeurs soumises sans garantir qu'elles ont passé la validation. + +Si les formulaires ont une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun : ```php $form = new Form; @@ -287,55 +293,58 @@ class RegistrationFormData } ``` -Le mappage reconnaîtra alors à partir du type de la propriété `$person` qu'il doit mapper le conteneur sur la classe `PersonFormData`. Si la propriété contenait un tableau de conteneurs, indiquez le type `array` et passez la classe pour le mappage directement au conteneur : +Le mapping déduit alors du type de la propriété `$person` qu'il doit mapper le conteneur vers la classe `PersonFormData`. Si la propriété devait contenir un tableau de conteneurs, indiquez le type `array` et passez la classe à mapper directement au conteneur : ```php $person->setMappedType(PersonFormData::class); ``` -Vous pouvez faire générer la conception de la classe de données du formulaire à l'aide de la méthode `Nette\Forms\Blueprint::dataClass($form)`, qui l'affichera dans la page du navigateur. Il suffit ensuite de cliquer pour sélectionner le code et de le copier dans le projet. .{data-version:3.1.15} +Vous pouvez faire générer une proposition de classe de données du formulaire avec la méthode `Nette\Forms\Blueprint::dataClass($form)`, qui l'affiche dans la page du navigateur. Il vous suffit ensuite de sélectionner le code d'un clic et de le copier dans votre projet. .{data-version:3.1.15} -Plusieurs boutons -================= +Plusieurs boutons d'envoi +========================= -Si le formulaire a plus d'un bouton, nous devons généralement distinguer lequel a été pressé. Nous pouvons créer notre propre fonction de gestion pour chaque bouton. Nous la définissons comme handler pour l'[événement |nette:glossary#Événements events] `onClick` : +Si le formulaire comporte plus d'un bouton, nous avons généralement besoin de distinguer lequel a été pressé. Nous pouvons créer une fonction gestionnaire distincte pour chaque bouton. Définissez-la comme gestionnaire de l'[événement |nette:glossary#Événements] `onClick` : ```php $form->addSubmit('save', 'Enregistrer') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Supprimer') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -Ces handlers ne sont appelés que dans le cas d'un formulaire valablement rempli, tout comme dans le cas de l'événement `onSuccess`. La différence est que comme premier paramètre, au lieu du formulaire, le bouton d'envoi peut être passé, cela dépend du type que vous indiquez : +.{data-version:3.3.0} +Un gestionnaire peut aussi être passé directement au bouton, comme troisième argument de la méthode `addSubmit()`. + +Ces gestionnaires ne sont appelés que si le formulaire est valablement rempli (sauf si la validation est désactivée pour le bouton), tout comme l'événement `onSuccess`. La différence est que le premier paramètre passé peut être l'objet du bouton d'envoi au lieu du formulaire, selon la déclaration de type que vous indiquez : ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Lorsque le formulaire est soumis avec la touche <kbd>Entrée</kbd>, il est considéré comme s'il avait été soumis avec le premier bouton. +Lorsque le formulaire est envoyé en appuyant sur la touche <kbd>Entrée</kbd>, il est traité comme s'il avait été envoyé par le premier bouton d'envoi. Événement onAnchor ================== -Lorsque nous assemblons le formulaire dans la méthode factory (comme par exemple `createComponentRegistrationForm`), celui-ci ne sait pas encore s'il a été soumis, ni avec quelles données. Mais il y a des cas où nous avons besoin de connaître les valeurs soumises, par exemple si la forme ultérieure du formulaire en dépend, ou si nous en avons besoin pour des selectbox dépendants, etc. +Lorsque vous construisez un formulaire dans une méthode fabrique (comme `createComponentRegistrationForm`), il ne sait pas encore s'il a été envoyé ni avec quelles données. Il y a pourtant des cas où nous avons besoin de connaître les valeurs soumises, par exemple parce que l'apparence du formulaire en dépend, ou parce qu'elles sont nécessaires à des listes déroulantes dépendantes, etc. -Une partie du code assemblant le formulaire peut donc être appelée uniquement au moment où il est dit ancré, c'est-à-dire qu'il est déjà connecté au presenter et connaît ses données soumises. Nous passons un tel code dans le tableau `$onAnchor` : +Vous pouvez donc faire en sorte que le code qui construit le formulaire ne soit appelé qu'au moment où celui-ci est 'ancré', c'est-à-dire déjà relié au presenter et au courant de ses données soumises. Placez un tel code dans le tableau `$onAnchor` : ```php -$country = $form->addSelect('country', 'État :', $this->model->getCountries()); +$country = $form->addSelect('country', 'Pays :', $this->model->getCountries()); $city = $form->addSelect('city', 'Ville :'); $form->onAnchor[] = function () use ($country, $city) { - // cette fonction sera appelée seulement lorsque le formulaire saura s'il a été soumis et avec quelles données - // on peut donc utiliser la méthode getValue() + // cette fonction sera appelée quand le formulaire connaîtra les données avec lesquelles il a été envoyé + // vous pouvez donc utiliser la méthode getValue() $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; @@ -345,35 +354,34 @@ $form->onAnchor[] = function () use ($country, $city) { Protection contre les vulnérabilités ==================================== -Nette Framework accorde une grande importance à la sécurité et veille donc scrupuleusement à la bonne sécurisation des formulaires. Il le fait de manière totalement transparente et ne nécessite aucune configuration manuelle. +Nette Framework accorde une grande importance à la sécurité et veille donc scrupuleusement à la sécurisation des formulaires. Il le fait de façon totalement transparente et ne demande aucun réglage manuel. -En plus de protéger les formulaires contre les attaques [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] et [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], il effectue de nombreuses petites sécurisations auxquelles vous n'avez plus à penser. +Outre la protection des formulaires contre des attaques comme le [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] et le [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], il applique quantité de petites mesures de sécurité auxquelles vous n'avez plus à penser. -Par exemple, il filtre tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, de sorte que les données du formulaire seront toujours propres. Pour les select box et les radio lists, il vérifie que les éléments sélectionnés étaient bien parmi ceux proposés et qu'il n'y a pas eu de falsification. Nous avons déjà mentionné que pour les entrées de texte sur une seule ligne, il supprime les caractères de fin de ligne qu'un attaquant aurait pu envoyer. Pour les entrées multilignes, il normalise les caractères de fin de ligne. Et ainsi de suite. +Il filtre par exemple tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, si bien que les données issues du formulaire sont toujours propres. Pour les listes déroulantes et les listes de boutons radio, il vérifie que les éléments choisis figuraient bien parmi ceux proposés et qu'aucune falsification n'a eu lieu. Nous avons déjà dit que, pour les champs texte sur une ligne, il remplace par des espaces les caractères de fin de ligne qu'un attaquant pourrait envoyer. Pour les champs multilignes, il normalise les fins de ligne. Et ainsi de suite. -Nette résout pour vous les risques de sécurité dont de nombreux programmeurs ignorent même l'existence. +Nette règle pour vous des risques de sécurité dont beaucoup de programmeurs ignorent jusqu'à l'existence. -L'attaque CSRF mentionnée consiste en ce qu'un attaquant attire la victime sur une page qui exécute discrètement dans le navigateur de la victime une requête vers le serveur sur lequel la victime est connectée, et le serveur croit que la requête a été exécutée par la victime de sa propre volonté. C'est pourquoi Nette empêche l'envoi d'un formulaire POST depuis un autre domaine. Si pour une raison quelconque vous souhaitez désactiver la protection et autoriser l'envoi du formulaire depuis un autre domaine, utilisez : +L'attaque CSRF évoquée consiste, pour un attaquant, à attirer la victime sur une page qui exécute discrètement, depuis le navigateur de la victime, une requête vers le serveur sur lequel elle est connectée. Le serveur croit alors que la requête a été faite volontairement par la victime. C'est pourquoi Nette refuse les formulaires POST envoyés depuis une origine étrangère ; même un autre sous-domaine du même site compte comme étranger. Si vous avez besoin d'autoriser l'envoi depuis une autre origine, désactivez la protection avec : ```php -$form->allowCrossOrigin(); // ATTENTION ! Désactive la protection ! +$form->allowCrossOrigin(); // ATTENTION ! Désactive complètement la protection ! ``` -Cette protection utilise un cookie SameSite nommé `_nss`. La protection via le cookie SameSite peut ne pas être fiable à 100%, il est donc conseillé d'activer également la protection par jeton : +Cela désactive cependant la protection pour toutes les origines. Pour n'autoriser que certaines origines précises, désactivez la protection et vérifiez vous-même l'en-tête `Origin` contre votre propre liste d'autorisations. -```php -$form->addProtection(); -``` +La protection repose sur l'en-tête `Sec-Fetch-Site` du navigateur (Fetch Metadata), que celui-ci envoie automatiquement et qu'il est impossible de falsifier, même avec une faille XSS. Pour les navigateurs plus anciens qui ne les prennent pas en charge, un cookie SameSite de repli s'applique, qu'une application Nette met en place automatiquement. L'article [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] le décrit en détail. -Nous recommandons de protéger ainsi les formulaires dans la partie administrative du site web, qui modifient des données sensibles dans l'application. Le framework se défend contre l'attaque CSRF en générant et en vérifiant un jeton d'autorisation qui est stocké dans la session. Par conséquent, il est nécessaire d'avoir une session ouverte avant d'afficher le formulaire. Dans la partie administrative du site web, la session est généralement déjà démarrée en raison de la connexion de l'utilisateur. Sinon, démarrez la session avec la méthode `Nette\Http\Session::start()`. +.[note] +L'ancienne protection par un token d'autorisation stocké en session, activée par `$form->addProtection()`, n'est plus nécessaire et est obsolète depuis la version 3.3. -Même formulaire dans plusieurs presenters -========================================= +Utiliser un même formulaire dans plusieurs presenters +===================================================== -Si vous avez besoin d'utiliser un même formulaire dans plusieurs presenters, nous vous recommandons de créer une factory pour celui-ci, que vous passerez ensuite au presenter via l'injection de dépendances. Un emplacement approprié pour une telle classe est par exemple le répertoire `app/Forms`. +Si vous avez besoin d'utiliser le même formulaire dans plusieurs presenters, nous vous recommandons de créer pour lui une factory, que vous injecterez ensuite dans les presenters. Un emplacement approprié pour une telle classe est par exemple le répertoire `app/Forms`. -La classe factory peut ressembler à ceci : +La classe factory pourrait ressembler à ceci : ```php use Nette\Application\UI\Form; @@ -390,7 +398,7 @@ class SignInFormFactory } ``` -Nous demandons à la classe de fabriquer le formulaire dans la méthode factory pour les composants dans le presenter : +Nous demandons à la classe de produire le formulaire dans la méthode fabrique du composant, au sein du presenter : ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // nous pouvons modifier le formulaire, ici par exemple nous changeons l'étiquette sur le bouton + // nous pouvons modifier le formulaire, ici par exemple nous changeons le libellé du bouton $form['send']->setCaption('Continuer'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // et ajoutons un handler + $form->onSuccess[] = $this->signInFormSuceeded(...); // et ajoutons un gestionnaire return $form; } ``` -Le handler pour le traitement du formulaire peut également être fourni par la factory : +Le gestionnaire de traitement du formulaire peut aussi être fourni par la factory elle-même : ```php use Nette\Application\UI\Form; @@ -421,11 +429,11 @@ class SignInFormFactory $form->addText('name', 'Nom :'); $form->addSubmit('send', 'Se connecter'); $form->onSuccess[] = function (Form $form, $data): void { - // ici nous effectuons le traitement du formulaire + // nous traitons ici notre formulaire envoyé }; return $form; } } ``` -Voilà, nous avons eu une introduction rapide aux formulaires dans Nette. Essayez de regarder également dans le répertoire [exemples|https://github.com/nette/forms/tree/master/examples] de la distribution, où vous trouverez plus d'inspiration. +Voilà, nous avons fait un tour d'horizon rapide des formulaires dans Nette. Pour plus d'inspiration, essayez de regarder dans le répertoire des [exemples |https://github.com/nette/forms/tree/master/examples] de la distribution. diff --git a/forms/fr/rendering.texy b/forms/fr/rendering.texy index 75299a5126..25688b9513 100644 --- a/forms/fr/rendering.texy +++ b/forms/fr/rendering.texy @@ -1,35 +1,35 @@ Rendu des formulaires ********************* -L'apparence des formulaires peut être très variée. En pratique, nous pouvons rencontrer deux extrêmes. D'un côté, il y a le besoin de rendre dans l'application de nombreux formulaires qui se ressemblent visuellement comme deux gouttes d'eau, et nous apprécierons un rendu facile sans template à l'aide de `$form->render()`. C'est généralement le cas des interfaces d'administration. +L'apparence des formulaires peut être très variée. En pratique, nous pouvons rencontrer deux extrêmes. D'un côté, il y a le besoin de rendre dans une application quantité de formulaires visuellement identiques, et nous apprécions alors le rendu facile, sans template, à l'aide de `$form->render()`. C'est typiquement le cas des interfaces d'administration. -De l'autre côté, il y a des formulaires variés où la règle est : chaque pièce est originale. Leur forme est mieux décrite par le langage HTML dans le template du formulaire. Et bien sûr, en plus des deux extrêmes mentionnés, nous rencontrerons de nombreux formulaires qui se situent quelque part entre les deux. +De l'autre côté, il y a des formulaires variés dont chacun est unique. Leur apparence se décrit le mieux en HTML, dans le template du formulaire. Et bien sûr, entre ces deux extrêmes, nous rencontrons de nombreux formulaires qui se situent quelque part au milieu. Rendu avec Latte ================ -Le [Système de templates Latte|latte:] facilite grandement le rendu des formulaires et de leurs éléments. Nous allons d'abord montrer comment rendre les formulaires manuellement, élément par élément, et ainsi obtenir un contrôle total sur le code. Plus tard, nous montrerons comment ce rendu peut être [automatisé |#Rendu automatique]. +Le système de templates [Latte |latte:] simplifie considérablement le rendu des formulaires et de leurs éléments. Nous montrerons d'abord comment rendre les formulaires manuellement, élément par élément, pour garder le contrôle total sur le code. Nous montrerons ensuite comment un tel rendu peut être [automatisé |#Rendu automatique]. -Vous pouvez faire générer le design du template Latte du formulaire à l'aide de la méthode `Nette\Forms\Blueprint::latte($form)`, qui l'affichera dans la page du navigateur. Il suffit ensuite de cliquer pour sélectionner le code et de le copier dans le projet. .{data-version:3.1.15} +Vous pouvez faire générer le template Latte du formulaire à l'aide de la méthode `Nette\Forms\Blueprint::latte($form)`, qui l'affiche dans la page du navigateur. Il vous suffit ensuite de sélectionner le code d'un clic et de le copier dans votre projet. .{data-version:3.1.15} `{control}` ----------- -La manière la plus simple de rendre un formulaire est d'écrire dans le template : +La façon la plus simple de rendre un formulaire est d'écrire dans le template : ```latte {control signInForm} ``` -L'apparence du formulaire ainsi rendu peut être influencée par la configuration du [#Renderer] et des [éléments individuels |#Attributs HTML]. +L'apparence du formulaire ainsi rendu peut être influencée par la configuration du [#Renderer] et des [différents champs |#Attributs HTML]. `n:name` -------- -La définition du formulaire dans le code PHP peut être très facilement liée au code HTML. Il suffit d'ajouter des attributs `n:name`. C'est aussi simple que ça ! +Relier la définition du formulaire en PHP au code HTML est extrêmement simple. Il suffit d'ajouter les attributs `n:name`. C'est aussi simple que ça ! ```php protected function createComponentSignInForm(): Form @@ -45,10 +45,10 @@ protected function createComponentSignInForm(): Form ```latte <form n:name=signInForm class=form> <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> + <label n:name=username>Nom d'utilisateur : <input n:name=username size=20 autofocus></label> </div> <div> - <label n:name=password>Password: <input n:name=password></label> + <label n:name=password>Mot de passe : <input n:name=password></label> </div> <div> <input n:name=send class="btn btn-default"> @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Vous avez un contrôle total sur la forme du code HTML résultant. Si vous utilisez l'attribut `n:name` sur les éléments `<select>`, `<button>` ou `<textarea>`, leur contenu interne sera automatiquement complété. La balise `<form n:name>` crée en outre une variable locale `$form` avec l'objet du formulaire dessiné et la fermeture `</form>` rend tous les éléments cachés non rendus (il en va de même pour `{form} ... {/form}`). +Vous avez le contrôle total sur l'apparence du code HTML obtenu. Si vous utilisez l'attribut `n:name` avec les éléments `<select>`, `<button>` ou `<textarea>`, leur contenu interne est rempli automatiquement. De plus, la balise `<form n:name>` crée une variable locale `$form` contenant l'objet du formulaire rendu, et la balise fermante `</form>` rend les champs cachés qui n'ont pas encore été rendus (il en va de même pour `{form} ... {/form}`). -Nous ne devons cependant pas oublier de rendre les éventuels messages d'erreur. Aussi bien ceux qui ont été ajoutés aux éléments individuels avec la méthode `addError()` (à l'aide de `{inputError}`), que ceux ajoutés directement au formulaire (renvoyés par `$form->getOwnErrors()`) : +Nous ne devons cependant pas oublier de rendre les éventuels messages d'erreur. Cela concerne aussi bien les erreurs ajoutées aux différents champs par la méthode `addError()` (rendues via `{inputError}`) que celles ajoutées directement au formulaire (renvoyées par `$form->getOwnErrors()`) : ```latte <form n:name=signInForm class=form> @@ -67,11 +67,11 @@ Nous ne devons cependant pas oublier de rendre les éventuels messages d'erreur. </ul> <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> + <label n:name=username>Nom d'utilisateur : <input n:name=username size=20 autofocus></label> <span class=error n:ifcontent>{inputError username}</span> </div> <div> - <label n:name=password>Password: <input n:name=password></label> + <label n:name=password>Mot de passe : <input n:name=password></label> <span class=error n:ifcontent>{inputError password}</span> </div> <div> @@ -80,7 +80,7 @@ Nous ne devons cependant pas oublier de rendre les éventuels messages d'erreur. </form> ``` -Les éléments de formulaire plus complexes, tels que RadioList ou CheckboxList, peuvent être rendus ainsi par éléments individuels : +Les champs de formulaire plus complexes, comme RadioList ou CheckboxList, peuvent être rendus élément par élément de cette façon : ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ Les éléments de formulaire plus complexes, tels que RadioList ou CheckboxList, `{label}` `{input}` ------------------- -Vous ne voulez pas réfléchir pour chaque élément à quel élément HTML utiliser dans le template, que ce soit `<input>`, `<textarea>`, etc. ? La solution est la balise universelle `{input}` : +Vous préférez ne pas avoir à réfléchir, dans le template, à l'élément HTML à utiliser pour chaque champ, `<input>`, `<textarea>`, etc. ? La solution est la balise universelle `{input}` : ```latte <form n:name=signInForm class=form> @@ -101,11 +101,11 @@ Vous ne voulez pas réfléchir pour chaque élément à quel élément HTML util </ul> <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} + {label username}Nom d'utilisateur : {input username, size: 20, autofocus: true}{/label} {inputError username} </div> <div> - {label password}Password: {input password}{/label} + {label password}Mot de passe : {input password}{/label} {inputError password} </div> <div> @@ -114,9 +114,9 @@ Vous ne voulez pas réfléchir pour chaque élément à quel élément HTML util </form> ``` -Si le formulaire utilise un traducteur, le texte à l'intérieur des balises `{label}` sera traduit. +Si le formulaire utilise un traducteur, les labels rendus à partir de la définition du formulaire (par exemple `{label username /}`) sont traduits. Le texte écrit directement entre les balises `{label}` et `{/label}` ne l'est pas. -Même dans ce cas, les éléments de formulaire plus complexes, tels que RadioList ou CheckboxList, peuvent être rendus par éléments individuels : +Là encore, les champs de formulaire plus complexes, comme RadioList ou CheckboxList, peuvent être rendus élément par élément : ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ Même dans ce cas, les éléments de formulaire plus complexes, tels que RadioLi {/foreach} ``` -Pour rendre uniquement le `<input>` dans l'élément Checkbox, utilisez `{input myCheckbox:}`. Les attributs HTML dans ce cas doivent toujours être séparés par une virgule `{input myCheckbox:, class: required}`. +Pour ne rendre que l'`<input>` d'un champ Checkbox, utilisez `{input myCheckbox:}`. Dans ce cas, séparez toujours les attributs HTML par une virgule : `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Affiche le message d'erreur pour l'élément de formulaire, s'il en a un. Le message est généralement enveloppé dans un élément HTML pour le style. Empêcher le rendu d'un élément vide si le message n'existe pas peut être fait élégamment avec `n:ifcontent` : +Affiche le message d'erreur d'un champ de formulaire, s'il en existe un. Le message est généralement enveloppé dans un élément HTML pour la mise en forme. On peut élégamment éviter le rendu d'un élément vide en l'absence de message à l'aide de `n:ifcontent` : ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -La présence d'une erreur peut être vérifiée avec la méthode `hasErrors()` et en fonction de cela, définir une classe sur l'élément parent : +Nous pouvons détecter la présence d'une erreur avec la méthode `hasErrors()` et définir en conséquence la classe de l'élément parent : ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ La présence d'une erreur peut être vérifiée avec la méthode `hasErrors()` e `{form}` -------- -Les balises `{form signInForm}...{/form}` sont une alternative à `<form n:name="signInForm">...</form>`. +Les balises `{form signInForm}...{/form}` sont une alternative à `<form n:name="signInForm">...</form>`. Séparez les éventuels arguments du nom par une virgule : `{form signInForm, class: foo}`. + +.{data-version:3.3.0} +Le mot-clé `scope` placé avant le nom se contente de pousser le formulaire sur la pile (pour que `{input}`, `{label}`, etc. s'y rattachent), mais ne rend pas la balise `<form>`. C'est pratique pour rendre une partie d'un formulaire, par exemple dans un snippet. Si un formulaire est déjà actif, le nom est résolu relativement à lui, si bien que `{form scope}` remplace aussi `{formContainer}` : + +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +Le mot-clé `detached` rend un `<form></form>` vide et relie chaque champ à lui via l'attribut HTML `form`. Cela vous permet de placer un formulaire à l'intérieur d'un autre formulaire, ce que le HTML interdit par ailleurs. Le formulaire détaché doit avoir un `id` HTML, qui est généré automatiquement lorsque vous lui donnez un nom (comme `outerForm` ci-dessous) : + +```latte +{form detached outerForm} + ... +{/form} +``` Rendu automatique ----------------- -Grâce aux balises `{input}` et `{label}`, nous pouvons facilement créer un template générique pour n'importe quel formulaire. Il itérera et rendra progressivement tous ses éléments, à l'exception des éléments cachés, qui seront rendus automatiquement à la fermeture du formulaire par la balise `</form>`. Le nom du formulaire à rendre sera attendu dans la variable `$form`. +Grâce aux balises `{input}` et `{label}`, nous pouvons facilement créer un template générique pour n'importe quel formulaire. Il parcourra et rendra tous ses champs, à l'exception des champs cachés, qui sont rendus automatiquement à la fermeture du formulaire par la balise `</form>`. Il attend le nom du formulaire à rendre dans la variable `$form`. ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ Grâce aux balises `{input}` et `{label}`, nous pouvons facilement créer un tem </form> ``` -Les balises paires auto-fermantes `{label .../}` utilisées affichent les étiquettes provenant de la définition du formulaire dans le code PHP. +Les balises paires auto-fermantes `{label .../}` employées ici affichent les labels provenant de la définition du formulaire dans le code PHP. -Enregistrez ce template générique par exemple dans le fichier `basic-form.latte` et pour rendre le formulaire, il suffit de l'inclure et de passer le nom (ou l'instance) du formulaire au paramètre `$form` : +Enregistrez ce template générique, par exemple, dans le fichier `basic-form.latte`. Pour rendre le formulaire, il suffit de l'inclure et de passer le nom du formulaire (ou son instance) au paramètre `$form` : ```latte {include basic-form.latte, form: signInForm} ``` -Si vous souhaitez intervenir dans l'apparence d'un formulaire particulier lors de son rendu et, par exemple, rendre un élément différemment, le moyen le plus simple est de préparer des blocs dans le template qui pourront être ensuite surchargés. Les blocs peuvent également avoir des [noms dynamiques |latte:template-inheritance#Noms de blocs dynamiques], on peut donc y insérer le nom de l'élément rendu. Par exemple : +Si vous voulez modifier l'apparence d'un formulaire précis lors du rendu, par exemple rendre un champ différemment, le plus simple est de préparer dans le template des blocs que l'on pourra ensuite redéfinir. Les blocs peuvent aussi avoir des [noms dynamiques |latte:template-inheritance#Noms de blocs dynamiques], ce qui vous permet d'y insérer le nom du champ rendu. Par exemple : ```latte ... @@ -189,7 +207,7 @@ Si vous souhaitez intervenir dans l'apparence d'un formulaire particulier lors d ... ``` -Pour l'élément, par exemple `username`, un bloc `input-username` sera créé, qui peut être facilement surchargé en utilisant la balise [{embed} |latte:template-inheritance#Héritage unitaire] : +Pour un champ nommé par exemple `username`, cela crée le bloc `input-username`, qui peut être facilement redéfini à l'aide de la balise [{embed} |latte:template-inheritance#Héritage unitaire] : ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ Pour l'élément, par exemple `username`, un bloc `input-username` sera créé, {/embed} ``` -Alternativement, tout le contenu du template `basic-form.latte` peut être [défini |latte:template-inheritance#Définitions] comme un bloc, y compris le paramètre `$form` : +Le contenu entier du template `basic-form.latte` peut aussi être [défini |latte:template-inheritance#Définitions] comme un bloc, paramètre `$form` compris : ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ Alternativement, tout le contenu du template `basic-form.latte` peut être [déf {/define} ``` -Grâce à cela, son appel sera légèrement plus simple : +Cela rend son appel légèrement plus simple : ```latte {embed basic-form, signInForm} @@ -219,31 +237,31 @@ Grâce à cela, son appel sera légèrement plus simple : {/embed} ``` -Il suffit d'importer le bloc à un seul endroit, au début du template de layout : +Le bloc n'a besoin d'être importé qu'à un seul endroit, au début du template de layout : ```latte {import basic-form.latte} ``` -Cas spéciaux ------------- +Cas particuliers +---------------- -Si vous avez besoin de rendre uniquement la partie interne du formulaire sans les balises HTML `<form>`, par exemple lors de l'envoi de snippets, masquez-les à l'aide de l'attribut `n:tag-if` : +Si vous avez besoin de ne rendre que la partie interne du formulaire, sans les balises HTML `<form>`, par exemple lors de l'envoi de snippets, masquez-les à l'aide de l'attribut `n:tag-if` : ```latte <form n:name=signInForm n:tag-if=false> <div> - <label n:name=username>Username: <input n:name=username></label> + <label n:name=username>Nom d'utilisateur : <input n:name=username></label> {inputError username} </div> </form> ``` -La balise `{formContainer}` aide au rendu des éléments à l'intérieur d'un conteneur de formulaire. +La balise `{formContainer}`, ou la plus récente [`{form scope}` |#{form}], aide à rendre les champs situés dans un conteneur du formulaire. ```latte -<p>Quelles nouvelles souhaitez-vous recevoir :</p> +<p>Quelles actualités souhaitez-vous recevoir :</p> {formContainer emailNews} <ul> @@ -257,39 +275,39 @@ La balise `{formContainer}` aide au rendu des éléments à l'intérieur d'un co Rendu sans Latte ================ -La manière la plus simple de rendre un formulaire est d'appeler : +La façon la plus simple de rendre un formulaire est d'appeler : ```php $form->render(); ``` -L'apparence du formulaire ainsi rendu peut être influencée par la configuration du [#Renderer] et des [éléments individuels |#Attributs HTML]. +L'apparence du formulaire ainsi rendu peut être influencée par la configuration du [#Renderer] et des [différents champs |#Attributs HTML]. Rendu manuel ------------ -Chaque élément de formulaire dispose de méthodes qui génèrent le code HTML du champ de formulaire et de l'étiquette. Elles peuvent le retourner soit sous forme de chaîne de caractères, soit sous forme d'objet [Nette\Utils\Html|utils:html-elements] : +Chaque champ de formulaire possède des méthodes qui génèrent le code HTML du champ et de son label. Elles peuvent le renvoyer soit sous forme de chaîne, soit sous forme d'objet [Nette\Utils\Html |utils:html-elements] : -- `getControl(): Html|string` retourne le code HTML de l'élément -- `getLabel($caption = null): Html|string|null` retourne le code HTML de l'étiquette, si elle existe +- `getControl(): Html|string` renvoie le code HTML du champ +- `getLabel($caption = null): Html|string|null` renvoie le code HTML du label, s'il existe -Le formulaire peut ainsi être rendu élément par élément : +Cela permet de rendre le formulaire élément par élément : ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,21 +315,21 @@ Le formulaire peut ainsi être rendu élément par élément : <?php $form->render('end') ?> ``` -Alors que pour certains éléments, `getControl()` retourne un seul élément HTML (par ex. `<input>`, `<select>`, etc.), pour d'autres, il retourne un morceau entier de code HTML (CheckboxList, RadioList). Dans ce cas, vous pouvez utiliser des méthodes qui génèrent des inputs et des étiquettes individuels, pour chaque élément séparément : +Tandis que, pour certains champs, `getControl()` renvoie un unique élément HTML (par exemple `<input>`, `<select>`, etc.), pour d'autres il renvoie un morceau de code HTML complet (CheckboxList, RadioList). Dans ce cas, vous pouvez utiliser les méthodes qui génèrent séparément les inputs et les labels de chaque élément : -- `getControlPart($key = null): ?Html` retourne le code HTML d'un élément individuel -- `getLabelPart($key = null): ?Html` retourne le code HTML de l'étiquette d'un élément individuel +- `getControlPart($key = null): Html` renvoie le code HTML d'un seul élément +- `getLabelPart($key = null): Html` renvoie le code HTML du label d'un seul élément .[note] -Ces méthodes ont pour des raisons historiques le préfixe `get`, mais `generate` serait meilleur, car à chaque appel, elles créent et retournent un nouvel élément `Html`. +Ces méthodes portent le préfixe `get` pour des raisons historiques, mais `generate` serait plus approprié, car elles créent et renvoient un nouvel élément `Html` à chaque appel. Renderer ======== -C'est un objet assurant le rendu du formulaire. Il peut être défini avec la méthode `$form->setRenderer`. Le contrôle lui est transmis lors de l'appel de la méthode `$form->render()`. +C'est un objet chargé du rendu du formulaire. Il se définit à l'aide de la méthode `$form->setRenderer()`. Le contrôle lui est passé lors de l'appel de la méthode `$form->render()`. -Si nous ne définissons pas notre propre renderer, le renderer par défaut [api:Nette\Forms\Rendering\DefaultFormRenderer] sera utilisé. Celui-ci rend les éléments du formulaire sous forme de tableau HTML. La sortie ressemble à ceci : +Si nous ne définissons pas de renderer personnalisé, le renderer par défaut [api:Nette\Forms\Rendering\DefaultFormRenderer] sera utilisé. Il rend les champs du formulaire dans un tableau HTML. Le résultat ressemble à ceci : ```latte <table> @@ -328,15 +346,15 @@ Si nous ne définissons pas notre propre renderer, le renderer par défaut [api: </tr> <tr> - <th><label>Sexe :</label></th> + <th><label>Genre :</label></th> ... ``` -L'utilisation ou non d'un tableau pour la structure du formulaire est discutable et de nombreux webdesigners préfèrent un autre balisage. Par exemple, une liste de définitions. Nous allons donc reconfigurer `DefaultFormRenderer` pour qu'il rende le formulaire sous forme de liste. La configuration se fait en modifiant le tableau [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Le premier index représente toujours la zone et le second son attribut. Les différentes zones sont illustrées par l'image : +L'usage d'un tableau pour la structure du formulaire est discutable, et beaucoup de web designers préfèrent un balisage différent, par exemple une liste de définitions. Nous allons donc reconfigurer `DefaultFormRenderer` pour qu'il rende le formulaire sous forme de liste. La configuration se fait en modifiant le tableau [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Le premier index représente toujours une zone, et le second son attribut. Les différentes zones sont représentées sur l'image : -[* defaultformrenderer.webp *] +[* form-areas-en.webp *] -Par défaut, le groupe d'éléments `controls` est enveloppé dans un tableau `<table>`, chaque `pair` représente une ligne de tableau `<tr>` et la paire `label` et `control` sont les cellules `<th>` et `<td>`. Nous allons maintenant changer les éléments enveloppants. Nous insérons la zone `controls` dans un conteneur `<dl>`, laissons la zone `pair` sans conteneur, insérons `label` dans `<dt>` et enfin enveloppons `control` avec les balises `<dd>` : +Par défaut, le groupe `controls` est enveloppé dans `<table>`, chaque `pair` représente une ligne de tableau `<tr>`, et le couple `label` et `control` correspond aux cellules `<th>` et `<td>`. Nous allons maintenant changer les éléments d'enveloppe. Nous placerons la zone `controls` dans un conteneur `<dl>`, laisserons la zone `pair` sans conteneur, mettrons le `label` dans `<dt>` et envelopperons enfin le `control` par des balises `<dd>` : ```php $renderer = $form->getRenderer(); @@ -348,7 +366,7 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -Le résultat est ce code HTML : +Il en résulte le code HTML suivant : ```latte <dl> @@ -362,107 +380,107 @@ Le résultat est ce code HTML : <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - <dt><label>Sexe :</label></dt> + <dt><label>Genre :</label></dt> ... </dl> ``` -Dans le tableau wrappers, on peut influencer de nombreux autres attributs : +Le tableau wrappers permet d'influencer bien d'autres attributs : -- ajouter des classes CSS aux types individuels d'éléments de formulaire -- distinguer par une classe CSS les lignes paires et impaires +- ajouter des classes CSS aux différents types de champs de formulaire +- distinguer les lignes paires et impaires par des classes CSS - distinguer visuellement les éléments obligatoires et facultatifs -- déterminer si les messages d'erreur s'affichent directement à côté des éléments ou au-dessus du formulaire +- déterminer si les messages d'erreur s'affichent directement à côté des champs ou au-dessus du formulaire Options ------- -Le comportement du Renderer peut également être contrôlé en définissant des *options* sur les éléments de formulaire individuels. On peut ainsi définir une description qui sera affichée à côté du champ de saisie : +Le comportement du Renderer peut aussi être piloté en définissant des *options* sur les différents champs du formulaire. Vous pouvez ainsi définir une description qui apparaît à côté du champ de saisie : ```php $form->addText('phone', 'Numéro :') - ->setOption('description', 'Ce numéro restera caché'); + ->setOption('description', 'Ce numéro restera masqué'); ``` -Si nous voulons y placer du contenu HTML, nous utilisons la classe [Html |utils:html-elements] +Si nous voulons y placer du contenu HTML, nous utilisons la classe [Html |utils:html-elements] : ```php use Nette\Utils\Html; -$form->addText('phone', 'Numéro :') +$form->addText('phone', 'Téléphone :') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Conditions de conservation de votre numéro</a>') + ->setHtml('<a href="...">Conditions d\'utilisation.</a>') ); ``` .[tip] -L'élément Html peut également être utilisé à la place de l'étiquette : `$form->addCheckbox('conditions', $label)`. +Un élément Html peut aussi être utilisé à la place d'un label : `$form->addCheckbox('conditions', $label)`. -Regroupement d'éléments ------------------------ +Regrouper les champs +-------------------- -Le Renderer permet de regrouper les éléments en groupes visuels (fieldsets) : +Le Renderer permet de regrouper les champs en groupes visuels (fieldsets) : ```php $form->addGroup('Données personnelles'); ``` -Après la création d'un nouveau groupe, celui-ci devient actif et chaque nouvel élément ajouté est également ajouté à ce groupe. Le formulaire peut donc être construit de cette manière : +Après la création d'un nouveau groupe, celui-ci devient actif et chaque champ nouvellement ajouté y est également ajouté. Le formulaire peut donc être construit ainsi : ```php $form = new Form; $form->addGroup('Données personnelles'); $form->addText('name', 'Votre nom :'); $form->addInteger('age', 'Votre âge :'); -$form->addEmail('email', 'Email :'); +$form->addEmail('email', 'E-mail :'); $form->addGroup('Adresse de livraison'); -$form->addCheckbox('send', 'Expédier à l\'adresse'); +$form->addCheckbox('send', 'Livrer à l\'adresse'); $form->addText('street', 'Rue :'); $form->addText('city', 'Ville :'); $form->addSelect('country', 'Pays :', $countries); ``` -Le Renderer rend d'abord les groupes, puis les éléments qui n'appartiennent à aucun groupe. +Le renderer dessine d'abord les groupes, puis les champs qui n'appartiennent à aucun groupe. -Support pour Bootstrap ----------------------- +Prise en charge de Bootstrap +---------------------------- -[Dans les exemples |https://github.com/nette/forms/tree/master/examples], vous trouverez des exemples sur la façon de configurer le Renderer pour [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] et [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] +Vous trouverez dans le [répertoire des exemples |https://github.com/nette/forms/tree/master/examples] des exemples montrant comment configurer le Renderer pour [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] et [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. Attributs HTML ============== -Pour définir des attributs HTML arbitraires pour les éléments de formulaire, utilisez la méthode `setHtmlAttribute(string $name, $value = true)` : +Pour définir n'importe quels attributs HTML sur les champs de formulaire, utilisez la méthode `setHtmlAttribute(string $name, $value = true)` : ```php -$form->addInteger('number', 'Numéro :') +$form->addInteger('number', 'Nombre :') ->setHtmlAttribute('class', 'big-number'); $form->addSelect('rank', 'Trier par :', ['prix', 'nom']) - ->setHtmlAttribute('onchange', 'submit()'); // envoyer lors du changement + ->setHtmlAttribute('onchange', 'submit()'); // envoie le formulaire au changement -// Pour définir les attributs de <form> lui-même +// Pour définir les attributs de l'élément <form> lui-même $form->setHtmlAttribute('id', 'myForm'); ``` -Spécification du type d'élément : +Indiquer le type du champ : ```php $form->addText('tel', 'Votre téléphone :') ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'écrivez le téléphone'); + ->setHtmlAttribute('placeholder', 'Veuillez indiquer votre téléphone'); ``` .[warning] -La définition du type et d'autres attributs sert uniquement à des fins visuelles. La vérification de l'exactitude des entrées doit avoir lieu côté serveur, ce que vous assurez en choisissant un [élément de formulaire|controls] approprié et en indiquant des [règles de validation|validation]. +Définir le type et les autres attributs n'a qu'un but visuel. La vérification de la validité des données doit avoir lieu côté serveur, ce que vous assurez en choisissant un [champ de formulaire |controls] approprié et en indiquant des [règles de validation |validation]. -Nous pouvons définir des attributs HTML avec des valeurs différentes pour chacun des éléments individuels dans les listes radio ou checkbox. Notez les deux-points après `style:`, qui assurent le choix de la valeur selon la clé : +Pour les différents éléments d'une liste de boutons radio ou de cases à cocher, nous pouvons définir un attribut HTML avec des valeurs différentes pour chacun. Remarquez les deux-points après `style:`, qui font que la valeur est choisie d'après la clé : ```php $colors = ['r' => 'rouge', 'g' => 'vert', 'b' => 'bleu']; @@ -471,7 +489,7 @@ $form->addCheckboxList('colors', 'Couleurs :', $colors) ->setHtmlAttribute('style:', $styles); ``` -Affiche : +Rend : ```latte <label><input type="checkbox" name="colors[]" style="background:red" value="r">rouge</label> @@ -479,14 +497,14 @@ Affiche : <label><input type="checkbox" name="colors[]" value="b">bleu</label> ``` -Pour définir des attributs logiques, comme `readonly`, nous pouvons utiliser la notation avec un point d'interrogation : +Pour définir des attributs booléens, comme `readonly`, nous pouvons utiliser la notation avec un point d'interrogation : ```php $form->addCheckboxList('colors', 'Couleurs :', $colors) - ->setHtmlAttribute('readonly?', 'r'); // pour plusieurs clés, utilisez un tableau, par ex. ['r', 'g'] + ->setHtmlAttribute('readonly?', 'r'); // utilisez un tableau pour plusieurs clés, par ex. ['r', 'g'] ``` -Affiche : +Rend : ```latte <label><input type="checkbox" name="colors[]" readonly value="r">rouge</label> @@ -494,14 +512,14 @@ Affiche : <label><input type="checkbox" name="colors[]" value="b">bleu</label> ``` -Dans le cas des selectbox, la méthode `setHtmlAttribute()` définit les attributs de l'élément `<select>`. Si nous voulons définir les attributs des `<option>` individuels, nous utilisons la méthode `setOptionAttribute()`. Les notations avec deux-points et point d'interrogation mentionnées ci-dessus fonctionnent également : +Pour les listes déroulantes, la méthode `setHtmlAttribute()` définit les attributs de l'élément `<select>`. Si nous voulons définir les attributs des différents éléments `<option>`, nous utilisons la méthode `setOptionAttribute()`. Les notations avec les deux-points et le point d'interrogation évoquées plus haut fonctionnent également : ```php $form->addSelect('colors', 'Couleurs :', $colors) ->setOptionAttribute('style:', $styles); ``` -Affiche : +Rend : ```latte <select name="colors"> @@ -515,22 +533,22 @@ Affiche : Prototypes ---------- -Une manière alternative de définir les attributs HTML consiste à modifier le modèle à partir duquel l'élément HTML est généré. Le modèle est un objet `Html` et est retourné par la méthode `getControlPrototype()` : +Une autre façon de définir les attributs HTML consiste à modifier le modèle à partir duquel l'élément HTML est généré. Ce modèle est un objet `Html` et il est renvoyé par la méthode `getControlPrototype()` : ```php -$input = $form->addInteger('number', 'Numéro :'); +$input = $form->addInteger('number', 'Nombre :'); $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -De cette manière, on peut également modifier le modèle de l'étiquette, retourné par `getLabelPrototype()` : +Le modèle du label, renvoyé par `getLabelPrototype()`, peut lui aussi être modifié de cette façon : ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -Pour les éléments Checkbox, CheckboxList et RadioList, vous pouvez influencer le modèle de l'élément qui enveloppe l'élément entier. Il est retourné par `getContainerPrototype()`. Par défaut, il s'agit d'un élément "vide", donc rien n'est rendu, mais en lui définissant un nom, il sera rendu : +Pour les champs Checkbox, CheckboxList et RadioList, vous pouvez influencer le modèle de l'élément qui enveloppe le champ entier. Il est renvoyé par `getContainerPrototype()`. Par défaut, c'est un élément "vide", donc rien n'est rendu, mais si vous lui donnez un nom, il sera rendu : ```php $input = $form->addCheckbox('send'); @@ -541,30 +559,30 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -Dans le cas de CheckboxList et RadioList, on peut également influencer le modèle du séparateur des éléments individuels, retourné par la méthode `getSeparatorPrototype()`. Par défaut, c'est l'élément `<br>`. Si vous le changez en un élément pair, il enveloppera les éléments individuels au lieu de les séparer. Et de plus, on peut influencer le modèle de l'élément HTML de l'étiquette pour les éléments individuels, retourné par `getItemLabelPrototype()`. +Dans le cas de CheckboxList et RadioList, vous pouvez aussi influencer le modèle du séparateur des différents éléments, renvoyé par la méthode `getSeparatorPrototype()`. Par défaut, c'est l'élément `<br>`. Si vous le changez en élément pair, il enveloppera les différents éléments au lieu de les séparer. Vous pouvez en outre influencer le modèle de l'élément HTML des labels des différents éléments, renvoyé par `getItemLabelPrototype()`. Traduction ========== -Si vous programmez une application multilingue, vous aurez probablement besoin de rendre le formulaire dans différentes versions linguistiques. Nette Framework définit à cet effet une interface pour la traduction [api:Nette\Localization\Translator]. 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. +Si vous développez une application multilingue, vous aurez sans doute besoin de rendre le formulaire dans différentes versions linguistiques. Nette Framework définit à cet effet une interface de traduction : [api:Nette\Localization\Translator]. Nette n'a pas d'implémentation par défaut ; vous pouvez choisir, selon vos besoins, parmi plusieurs solutions toutes prêtes disponibles sur [Componette |https://componette.org/search/localization]. Leur documentation explique comment configurer le traducteur. -Les formulaires prennent en charge l'affichage de textes via un traducteur. Nous le leur passons à l'aide de la méthode `setTranslator()` : +Les formulaires prennent en charge l'affichage des textes via le traducteur. Nous le leur passons à l'aide de la méthode `setTranslator()` : ```php $form->setTranslator($translator); ``` -À partir de ce moment, non seulement toutes les étiquettes, mais aussi tous les messages d'erreur ou les éléments des select box seront traduits dans une autre langue. +À partir de ce moment, non seulement tous les labels, mais aussi tous les messages d'erreur, les éléments des listes déroulantes et les placeholders des champs seront traduits dans la langue cible. -Pour les éléments de formulaire individuels, il est possible de définir un traducteur différent ou de désactiver complètement la traduction avec la valeur `null` : +Il est possible de définir un traducteur différent pour chaque champ du formulaire, ou de désactiver complètement la traduction en définissant la valeur `null` : ```php $form->addSelect('carModel', 'Modèle :', $cars) ->setTranslator(null); ``` -Pour les [règles de validation|validation], des paramètres spécifiques sont également transmis au traducteur, par exemple pour la règle : +Pour les [règles de validation |validation], des paramètres spécifiques sont également passés au traducteur. Par exemple, pour la règle : ```php $form->addPassword('password', 'Mot de passe :') @@ -577,13 +595,13 @@ le traducteur est appelé avec ces paramètres : $translator->translate('Le mot de passe doit comporter au moins %d caractères', 8); ``` -et peut donc choisir la forme correcte du pluriel pour le mot `caractères` en fonction du nombre. +et il peut donc choisir la forme plurielle correcte du mot `caractères` en fonction du nombre. Événement onRender ================== -Juste avant que le formulaire ne soit rendu, nous pouvons faire exécuter notre code. Celui-ci peut par exemple ajouter des classes HTML aux éléments de formulaire pour un affichage correct. Nous ajoutons le code au tableau `onRender` : +Juste avant le rendu du formulaire, nous pouvons faire appeler notre propre code. Celui-ci peut par exemple ajouter des classes HTML aux champs du formulaire pour un affichage correct. Nous ajoutons ce code au tableau `onRender` : ```php $form->onRender[] = function ($form) { diff --git a/forms/fr/standalone.texy b/forms/fr/standalone.texy index 1e70ddaf39..4f3f038563 100644 --- a/forms/fr/standalone.texy +++ b/forms/fr/standalone.texy @@ -1,43 +1,49 @@ -Formulaires utilisés indépendamment -*********************************** +Formulaires utilisés de manière autonome +**************************************** .[perex] -Nette Forms facilite grandement la création et le traitement des formulaires web. Vous pouvez les utiliser dans vos applications de manière totalement indépendante du reste du framework, ce que nous allons montrer dans ce chapitre. +Nette Forms simplifie radicalement la création et le traitement des formulaires web. Vous pouvez les utiliser dans vos applications de façon totalement autonome, sans le reste du framework, comme le montre ce chapitre. -Cependant, si vous utilisez Nette Application et les presenters, le guide pour l'[utilisation dans les presenters |in-presenter] est fait pour vous. +Si vous utilisez Nette Application et les presenters, un guide dédié vous attend en revanche : [les formulaires dans les presenters |in-presenter]. Premier formulaire ================== -Essayons d'écrire un formulaire d'inscription simple. Son code sera le suivant ("code complet":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f) : +Avant de commencer, installez le paquet à l'aide de [Composer |best-practices:composer] : + +```shell +composer require nette/forms +``` + +Essayons d'écrire un simple formulaire d'inscription. Son code sera le suivant ("code complet":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851) : ```php use Nette\Forms\Form; $form = new Form; -$form->addText('name', 'Nom:'); -$form->addPassword('password', 'Mot de passe:'); +$form->addText('name', 'Nom :'); +$form->addPassword('password', 'Mot de passe :'); $form->addSubmit('send', 'S\'inscrire'); ``` -Nous pouvons le rendre très facilement : +Et rendons-le très simplement : ```php $form->render(); ``` -et dans le navigateur, il s'affichera comme ceci : +Le résultat dans le navigateur devrait ressembler à ceci : [* form-en.webp *] -Le formulaire est un objet de la classe `Nette\Forms\Form` (la classe `Nette\Application\UI\Form` est utilisée dans les presenters). Nous y avons ajouté ce qu'on appelle les éléments nom, mot de passe et un bouton d'envoi. +Le formulaire est un objet de la classe `Nette\Forms\Form` (dans les presenters, on utilise la classe `Nette\Application\UI\Form`). Nous y avons ajouté des champs nommés 'name' et 'password', ainsi qu'un bouton d'envoi. -Maintenant, animons le formulaire. En interrogeant `$form->isSuccess()`, nous vérifions si le formulaire a été soumis et s'il a été rempli de manière valide. Si oui, nous affichons les données. Ajoutons donc après la définition du formulaire : +Donnons maintenant vie au formulaire. En interrogeant `$form->isSuccess()`, nous apprenons si le formulaire a été soumis et s'il a été rempli valablement. Si c'est le cas, nous afficherons les données. Après la définition du formulaire, ajoutez : ```php if ($form->isSuccess()) { - echo 'Le formulaire a été correctement rempli et soumis'; + echo 'Le formulaire a été rempli et envoyé avec succès'; $data = $form->getValues(); // $data->name contient le nom // $data->password contient le mot de passe @@ -45,17 +51,17 @@ if ($form->isSuccess()) { } ``` -La méthode `getValues()` retourne les données soumises sous forme d'objet [ArrayHash |utils:arrays#ArrayHash]. Nous verrons [plus tard |#Mappage sur des classes] comment changer cela. L'objet `$data` contient les clés `name` et `password` avec les informations saisies par l'utilisateur. +La méthode `getValues()` renvoie les données soumises sous forme d'objet [ArrayHash |utils:arrays#ArrayHash]. Nous montrerons [plus loin |#Mapping vers des classes] comment changer cela. L'objet `$data` contient les clés `name` et `password` avec les données saisies par l'utilisateur. -Habituellement, nous envoyons directement les données pour un traitement ultérieur, qui peut être par exemple une insertion dans la base de données. Cependant, une erreur peut survenir pendant le traitement, par exemple, le nom d'utilisateur est déjà pris. Dans ce cas, nous transmettons l'erreur au formulaire à l'aide de `addError()` et le laissons se rendre à nouveau, avec le message d'erreur. +Habituellement, nous envoyons les données directement au traitement suivant, par exemple leur insertion dans une base de données. Une erreur peut cependant survenir pendant ce traitement, par exemple si le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire à l'aide d'`addError()` et le laissons se rendre à nouveau, avec le message d'erreur. ```php -$form->addError('Désolé, ce nom d\'utilisateur est déjà utilisé.'); +$form->addError('Désolé, ce nom d\'utilisateur est déjà pris.'); ``` -Après le traitement du formulaire, nous redirigeons vers la page suivante. Cela évite la soumission répétée involontaire du formulaire par le bouton *actualiser*, *retour* ou par le déplacement dans l'historique du navigateur. +Après le traitement du formulaire, nous redirigeons vers la page suivante. Cela évite que le formulaire soit renvoyé involontairement en cliquant sur les boutons *actualiser* ou *retour*, ou en naviguant dans l'historique du navigateur. -Le formulaire est envoyé par défaut par la méthode POST et vers la même page. Les deux peuvent être modifiés : +Par défaut, le formulaire est envoyé par la méthode POST vers la même page. Les deux peuvent être changés : ```php $form->setAction('/submit.php'); @@ -64,13 +70,13 @@ $form->setMethod('GET'); Et c'est à peu près tout :-) Nous avons un formulaire fonctionnel et parfaitement [sécurisé |#Protection contre les vulnérabilités]. -Essayez d'ajouter d'autres [éléments de formulaire |controls]. +Essayez d'ajouter aussi d'autres [champs de formulaire |controls]. -Accès aux éléments -================== +Accès aux champs +================ -Nous appelons le formulaire et ses éléments individuels des composants. Ils forment un arbre de composants, dont la racine est précisément le formulaire. Nous pouvons accéder aux éléments individuels du formulaire de cette manière : +Le formulaire et ses différents champs sont appelés composants. Ils forment un arbre de composants dont le formulaire est la racine. Vous pouvez accéder aux différents champs du formulaire de cette façon : ```php $input = $form->getComponent('name'); @@ -80,7 +86,7 @@ $button = $form->getComponent('send'); // syntaxe alternative : $button = $form['send']; ``` -Les éléments sont supprimés à l'aide de unset : +Les champs se suppriment à l'aide d'`unset` : ```php unset($form['name']); @@ -90,26 +96,26 @@ unset($form['name']); Règles de validation ==================== -Nous avons mentionné le mot *valide,* mais le formulaire n'a pour l'instant aucune règle de validation. Corrigeons cela. +Le mot *valablement* a été prononcé, mais le formulaire n'a encore aucune règle de validation. Corrigeons cela. -Le nom sera obligatoire, nous le marquerons donc avec la méthode `setRequired()`, dont l'argument est le texte du message d'erreur qui s'affichera si l'utilisateur ne remplit pas le nom. Si nous n'indiquons pas d'argument, le message d'erreur par défaut sera utilisé. +Le nom sera obligatoire, nous le marquons donc avec la méthode `setRequired()`. Son argument est le texte du message d'erreur affiché si l'utilisateur ne remplit pas le nom. Si aucun argument n'est fourni, le message d'erreur par défaut est utilisé. ```php -$form->addText('name', 'Nom:') - ->setRequired('Veuillez saisir un nom'); +$form->addText('name', 'Nom :') + ->setRequired('Veuillez saisir un nom.'); ``` -Essayez de soumettre le formulaire sans remplir le nom et vous verrez que le message d'erreur s'affichera et que le navigateur ou le serveur le refusera jusqu'à ce que vous remplissiez le champ. +Essayez d'envoyer le formulaire sans remplir le nom et vous verrez apparaître un message d'erreur. Le navigateur ou le serveur le refusera tant que vous n'aurez pas rempli le champ. -En même temps, vous ne tromperez pas le système en tapant par exemple uniquement des espaces dans le champ. Non. Nette supprime automatiquement les espaces de début et de fin. Essayez-le. C'est quelque chose que vous devriez toujours faire avec chaque input sur une seule ligne, mais on l'oublie souvent. Nette le fait automatiquement. (Vous pouvez essayer de tromper le formulaire et envoyer une chaîne de caractères multiligne comme nom. Même ici, Nette ne se laisse pas berner et transforme les retours à la ligne en espaces.) +En même temps, vous ne pourrez pas tricher en tapant uniquement des espaces dans le champ. Impossible. Nette supprime automatiquement les espaces au début et à la fin. Essayez. C'est une chose que vous devriez toujours faire avec chaque champ sur une ligne, et que l'on oublie pourtant souvent. Nette le fait automatiquement. (Vous pouvez essayer de piéger le formulaire en envoyant comme nom une chaîne sur plusieurs lignes. Là non plus Nette ne se laisse pas avoir : les sauts de ligne seront convertis en espaces.) -Le formulaire est toujours validé côté serveur, mais une validation JavaScript est également générée, qui s'exécute instantanément et l'utilisateur est informé de l'erreur immédiatement, sans avoir besoin d'envoyer le formulaire au serveur. C'est le script `netteForms.js` qui s'en charge. Insérez-le dans la page : +Le formulaire est toujours validé côté serveur, mais une validation JavaScript est également générée. Elle s'exécute immédiatement et l'utilisateur apprend l'erreur tout de suite, sans avoir à envoyer le formulaire au serveur. C'est le script `netteForms.js` qui s'en charge. Insérez-le dans la page : ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Si vous regardez le code source de la page avec le formulaire, vous remarquerez peut-être que Nette insère les éléments obligatoires dans des éléments avec la classe CSS `required`. Essayez d'ajouter la feuille de style suivante au template et le libellé "Nom" sera rouge. Nous marquons ainsi élégamment les éléments obligatoires pour les utilisateurs : +Si vous regardez le code source de la page contenant le formulaire, vous remarquerez peut-être que Nette place les champs obligatoires dans des éléments portant la classe CSS `required`. Essayez d'ajouter la feuille de style suivante au template et le label "Nom" deviendra rouge. Cela met élégamment en évidence les champs obligatoires pour les utilisateurs : ```latte <style> @@ -117,85 +123,102 @@ Si vous regardez le code source de la page avec le formulaire, vous remarquerez </style> ``` -Nous ajoutons d'autres règles de validation avec la méthode `addRule()`. Le premier paramètre est la règle, le deuxième est à nouveau le texte du message d'erreur et il peut encore suivre un argument de la règle de validation. Qu'est-ce que cela signifie ? +Nous ajoutons d'autres règles de validation avec la méthode `addRule()`. Le premier paramètre est la règle, le deuxième est là encore le texte du message d'erreur, et un argument facultatif de la règle de validation peut suivre. Qu'est-ce que cela veut dire ? -Nous étendons le formulaire avec un nouveau champ facultatif "âge", qui doit être un entier (`addInteger()`) et en plus dans une plage autorisée (`$form::Range`). Et c'est ici que nous utilisons le troisième paramètre de la méthode `addRule()`, par lequel nous passons au validateur la plage requise sous forme de paire `[de, à]` : +Étoffons le formulaire d'un nouveau champ facultatif "âge", qui doit être un nombre entier (`addInteger()`) et se situer dans une plage autorisée (`$form::Range`). Nous utiliserons ici le troisième paramètre de la méthode `addRule()` pour passer au validateur la plage requise sous forme de paire `[min, max]` : ```php -$form->addInteger('age', 'Âge:') - ->addRule($form::Range, 'L\'âge doit être compris entre 18 et 120 ans', [18, 120]); +$form->addInteger('age', 'Âge :') + ->addRule($form::Range, 'L\'âge doit être compris entre 18 et 120 ans.', [18, 120]); ``` .[tip] -Si l'utilisateur ne remplit pas le champ, les règles de validation ne seront pas vérifiées, car l'élément est facultatif. +Si l'utilisateur ne remplit pas le champ, les règles de validation ne seront pas vérifiées, car le champ est facultatif. -Ici, il y a de la place pour un petit refactoring. Dans le message d'erreur et dans le troisième paramètre, les chiffres sont indiqués en double, ce qui n'est pas idéal. Si nous créions des [formulaires multilingues |rendering#Traduction] et que le message contenant des chiffres était traduit dans plusieurs langues, un éventuel changement de valeurs serait compliqué. Pour cette raison, il est possible d'utiliser les placeholders `%d` et Nette complétera les valeurs : +Cela laisse place à un petit refactoring. Les nombres sont dupliqués dans le message d'erreur et dans le troisième paramètre, ce qui n'est pas idéal. Si nous créions des [formulaires multilingues |rendering#Traduction] et que le message contenant les nombres était traduit en plusieurs langues, changer les valeurs deviendrait difficile. C'est pourquoi les placeholders `%d` peuvent être utilisés, et Nette y insérera les valeurs : ```php - ->addRule($form::Range, 'L\'âge doit être compris entre %d et %d ans', [18, 120]); + ->addRule($form::Range, 'L\'âge doit être compris entre %d et %d ans.', [18, 120]); ``` -Revenons à l'élément `password`, que nous rendrons également obligatoire et vérifierons en plus la longueur minimale du mot de passe (`$form::MinLength`), à nouveau en utilisant le placeholder : +Revenons au champ `password`, rendons-le lui aussi obligatoire et vérifions également la longueur minimale du mot de passe (`$form::MinLength`), là encore à l'aide d'un placeholder dans le message : ```php -$form->addPassword('password', 'Mot de passe:') +$form->addPassword('password', 'Mot de passe :') ->setRequired('Choisissez un mot de passe') ->addRule($form::MinLength, 'Le mot de passe doit comporter au moins %d caractères', 8); ``` -Ajoutons au formulaire un champ `passwordVerify`, où l'utilisateur saisira à nouveau le mot de passe, pour vérification. À l'aide des règles de validation, nous vérifions si les deux mots de passe sont identiques (`$form::Equal`). Et comme paramètre, nous donnons une référence au premier mot de passe en utilisant des [crochets |#Accès aux éléments] : +Ajoutons au formulaire un autre champ `passwordVerify`, où l'utilisateur saisit le mot de passe une seconde fois pour vérification. À l'aide des règles de validation, nous contrôlons que les deux mots de passe sont identiques (`$form::Equal`). Comme paramètre, nous fournissons une référence au premier mot de passe à l'aide des [crochets |#Accès aux champs] : ```php -$form->addPassword('passwordVerify', 'Mot de passe pour vérification:') +$form->addPassword('passwordVerify', 'Mot de passe à nouveau :') ->setRequired('Veuillez saisir à nouveau le mot de passe pour vérification') ->addRule($form::Equal, 'Les mots de passe ne correspondent pas', $form['password']) ->setOmitted(); ``` -Avec `setOmitted()`, nous avons marqué l'élément dont la valeur ne nous importe pas vraiment et qui n'existe que pour des raisons de validation. La valeur ne sera pas transmise à `$data`. +Avec `setOmitted()`, nous avons marqué un champ dont la valeur ne nous intéresse pas vraiment et qui n'existe qu'à des fins de validation. Sa valeur n'est pas transmise dans `$data`. -Nous avons ainsi un formulaire entièrement fonctionnel avec validation en PHP et JavaScript. Les capacités de validation de Nette sont beaucoup plus larges, il est possible de créer des conditions, de faire afficher et masquer des parties de la page en fonction d'elles, etc. Vous apprendrez tout cela dans le chapitre sur la [validation des formulaires |validation]. +Nous avons ainsi un formulaire pleinement fonctionnel, avec validation en PHP comme en JavaScript. Les possibilités de validation de Nette sont bien plus larges : vous pouvez créer des conditions, afficher et masquer des parties de la page en fonction de celles-ci, etc. Vous apprendrez tout cela dans le chapitre sur la [validation des formulaires |validation]. Valeurs par défaut ================== -Nous définissons couramment des valeurs par défaut pour les éléments du formulaire : +Nous définissons souvent des valeurs par défaut pour les champs du formulaire : ```php $form->addEmail('email', 'E-mail') ->setDefaultValue($lastUsedEmail); ``` -Il est souvent utile de définir les valeurs par défaut pour tous les éléments en même temps. Par exemple, lorsque le formulaire sert à modifier des enregistrements. Nous lisons l'enregistrement de la base de données et définissons les valeurs par défaut : +Il est souvent utile de définir les valeurs par défaut de tous les champs d'un coup, par exemple lorsque le formulaire sert à modifier un enregistrement. Nous lisons l'enregistrement dans la base de données et définissons ses valeurs comme valeurs par défaut : ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Appelez `setDefaults()` après la définition des éléments. +Appelez `setDefaults()` après avoir défini les champs. + +Sur un formulaire déjà soumis, `setDefaults()` n'a aucun effet - il n'écrasera pas ce que l'utilisateur a rempli, on peut donc l'appeler sans condition dans la factory du formulaire. Si vous avez besoin d'imposer les valeurs même après la soumission, utilisez plutôt `setValues()`. Rendu du formulaire =================== -Par défaut, le formulaire est rendu sous forme de tableau. Les éléments individuels respectent la règle d'accessibilité de base - tous les libellés sont écrits en tant que `<label>` et liés à l'élément de formulaire correspondant. En cliquant sur le libellé, le curseur apparaît automatiquement dans le champ du formulaire. +Par défaut, le formulaire est rendu sous forme de tableau. Les différents champs respectent les règles de base d'accessibilité : tous les labels sont générés comme éléments `<label>` et associés au champ correspondant. Un clic sur le label place automatiquement le curseur dans le champ du formulaire. + +Nous pouvons définir n'importe quels attributs HTML pour chaque champ. Ajoutons par exemple un placeholder : + +```php +$form->addInteger('age', 'Âge :') + ->setHtmlAttribute('placeholder', 'Veuillez indiquer votre âge'); +``` + +Il existe beaucoup de façons de rendre un formulaire, c'est pourquoi un [chapitre distinct est consacré au rendu |rendering]. + + +Rendu avec Latte +---------------- -Nous pouvons définir des attributs HTML arbitraires pour chaque élément. Par exemple, ajouter un placeholder : +Si vous avez sous la main le moteur de templates [Latte |latte:], vous pouvez lui confier le rendu du formulaire et garder le contrôle total sur le HTML obtenu. Vous créez le moteur, enregistrez l'extension des formulaires et passez le formulaire au template comme variable : ```php -$form->addInteger('age', 'Âge:') - ->setHtmlAttribute('placeholder', 'Veuillez remplir l\'âge'); +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); ``` -Il existe vraiment un grand nombre de façons de rendre un formulaire, c'est pourquoi un [chapitre séparé sur le rendu |rendering] y est consacré. +Dans le template, vous travaillez ensuite avec le formulaire via la variable `$form` et des balises comme `{input}`, `{label}` ou `n:name`. Un exemple complet, template compris, se trouve dans le répertoire des [exemples |https://github.com/nette/forms/tree/master/examples] (les fichiers `latte.php` et `latte/`). Les différentes balises sont décrites dans le chapitre sur le [rendu |rendering]. -Mappage sur des classes -======================= +Mapping vers des classes +======================== -Revenons au traitement des données du formulaire. La méthode `getValues()` nous retournait les données soumises sous forme d'objet `ArrayHash`. Comme il s'agit d'une classe générique, quelque chose comme `stdClass`, il nous manquera un certain confort lors de son utilisation, comme l'autocomplétion des propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait être résolu en ayant une classe spécifique pour chaque formulaire, dont les propriétés représentent les éléments individuels. Par exemple : +Revenons au traitement des données du formulaire. La méthode `getValues()` renvoyait les données soumises sous forme d'objet `ArrayHash`. Comme il s'agit d'une classe générique, semblable à `stdClass`, il nous manque certains conforts lors du travail avec elle, comme l'autocomplétion des propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait se résoudre en ayant pour chaque formulaire une classe dédiée dont les propriétés représentent les différents champs. Par exemple : ```php class RegistrationFormData @@ -206,32 +229,32 @@ class RegistrationFormData } ``` -Alternativement, vous pouvez utiliser un constructeur : +Vous pouvez aussi utiliser le constructeur : ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Les propriétés de la classe de données peuvent également être des enums et leur mappage se fera automatiquement. .{data-version:3.2.4} +Les propriétés de la classe de données peuvent aussi être des enums, elles seront mappées automatiquement. .{data-version:3.2.4} -Comment dire à Nette de nous retourner les données sous forme d'objets de cette classe ? Plus facilement que vous ne le pensez. Il suffit d'indiquer le nom de la classe ou l'objet à hydrater comme paramètre : +Comment dire à Nette de renvoyer les données comme objets de cette classe ? Plus simplement que vous ne le pensez. Il suffit d'indiquer en paramètre le nom de la classe ou l'objet à hydrater : ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Il est également possible d'indiquer `'array'` comme paramètre, et les données seront alors retournées sous forme de tableau. +Vous pouvez aussi indiquer `'array'` comme paramètre, et les données seront renvoyées sous forme de tableau. -Si les formulaires forment une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun : +Si les formulaires forment une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun d'eux : ```php $form = new Form; @@ -253,19 +276,19 @@ class RegistrationFormData } ``` -Le mappage reconnaîtra alors à partir du type de la propriété `$person` qu'il doit mapper le conteneur sur la classe `PersonFormData`. Si la propriété contenait un tableau de conteneurs, indiquez le type `array` et passez la classe pour le mappage directement au conteneur : +Le mapping sait alors, d'après le type de la propriété `$person`, qu'il doit mapper le conteneur vers la classe `PersonFormData`. Si la propriété devait contenir un tableau de conteneurs, indiquez le type `array` et passez la classe à mapper directement au conteneur : ```php $person->setMappedType(PersonFormData::class); ``` -Vous pouvez faire générer la conception de la classe de données du formulaire à l'aide de la méthode `Nette\Forms\Blueprint::dataClass($form)`, qui l'affichera dans la page du navigateur. Il suffit ensuite de cliquer pour sélectionner le code et de le copier dans votre projet. .{data-version:3.1.15} +Vous pouvez faire générer une proposition de classe de données du formulaire avec la méthode `Nette\Forms\Blueprint::dataClass($form)`, qui l'affichera dans la page du navigateur. Il vous suffit ensuite de sélectionner le code d'un clic et de le copier dans votre projet. .{data-version:3.1.15} -Plusieurs boutons -================= +Plusieurs boutons d'envoi +========================= -Si le formulaire a plus d'un bouton, nous avons généralement besoin de distinguer lequel d'entre eux a été pressé. Cette information nous est retournée par la méthode `isSubmittedBy()` du bouton : +Si le formulaire comporte plus d'un bouton, nous avons généralement besoin de distinguer lequel a été pressé. La méthode `isSubmittedBy()` du bouton nous donne cette information : ```php $form->addSubmit('save', 'Enregistrer'); @@ -282,9 +305,9 @@ if ($form->isSuccess()) { } ``` -Ne sautez pas la requête `$form->isSuccess()`, elle vérifie la validité des données. +N'omettez pas la vérification `$form->isSuccess()` ; c'est elle qui contrôle la validité des données. -Lorsque le formulaire est soumis avec la touche <kbd>Entrée</kbd>, cela est considéré comme s'il avait été soumis par le premier bouton. +Lorsqu'un formulaire est envoyé en appuyant sur la touche <kbd>Entrée</kbd>, il est traité comme s'il avait été envoyé par le premier bouton. Protection contre les vulnérabilités @@ -292,26 +315,23 @@ Protection contre les vulnérabilités Nette Framework accorde une grande importance à la sécurité et veille donc scrupuleusement à la bonne sécurisation des formulaires. -En plus de protéger les formulaires contre les attaques [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] et [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], il effectue de nombreuses petites sécurisations auxquelles vous n'avez plus besoin de penser. +Outre la protection des formulaires contre les vulnérabilités bien connues comme le [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] et le [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], il applique quantité de petites mesures de sécurité auxquelles vous n'avez plus à penser. -Par exemple, il filtre tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, de sorte que les données du formulaire seront toujours propres. Pour les select box et les radio lists, il vérifie que les éléments sélectionnés provenaient bien des options proposées et qu'il n'y a pas eu de falsification. Nous avons déjà mentionné que pour les entrées de texte sur une seule ligne, il supprime les caractères de fin de ligne qu'un attaquant aurait pu y envoyer. Pour les entrées multilignes, il normalise les caractères de fin de ligne. Et ainsi de suite. +Il filtre par exemple tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, si bien que les données issues du formulaire seront toujours propres. Pour les listes déroulantes et les listes de boutons radio, il vérifie que les éléments choisis figuraient bien parmi les options proposées et qu'aucune falsification n'a eu lieu. Nous avons déjà dit que, pour les champs texte sur une ligne, il remplace par des espaces les caractères de fin de ligne qu'un attaquant pourrait envoyer. Pour les champs multilignes, il normalise les fins de ligne. Et ainsi de suite. -Nette résout pour vous les risques de sécurité dont beaucoup de programmeurs ne soupçonnent même pas l'existence. +Nette règle pour vous des risques de sécurité dont beaucoup de programmeurs ignorent jusqu'à l'existence. -L'attaque CSRF mentionnée consiste en ce qu'un attaquant attire la victime sur une page qui exécute discrètement dans le navigateur de la victime une requête vers le serveur sur lequel la victime est connectée, et le serveur croit que la requête a été exécutée par la victime de sa propre volonté. C'est pourquoi Nette empêche la soumission d'un formulaire POST depuis un autre domaine. Si pour une raison quelconque vous souhaitez désactiver la protection et autoriser la soumission du formulaire depuis un autre domaine, utilisez : +L'attaque CSRF évoquée consiste, pour un attaquant, à attirer la victime sur une page qui exécute discrètement, depuis le navigateur de la victime, une requête vers le serveur sur lequel elle est actuellement connectée. Le serveur croit alors que la requête a été faite volontairement par la victime. C'est pourquoi Nette refuse les formulaires POST envoyés depuis une origine étrangère ; même un autre sous-domaine du même site compte comme étranger. Si vous avez besoin d'autoriser l'envoi depuis une autre origine, désactivez la protection avec : ```php -$form->allowCrossOrigin(); // ATTENTION ! Désactive la protection ! +$form->allowCrossOrigin(); // ATTENTION ! Désactive complètement la protection ! ``` -Cette protection utilise un cookie SameSite nommé `_nss`. Créez donc l'objet formulaire avant d'envoyer la première sortie, afin que le cookie puisse être envoyé. - -La protection par cookie SameSite peut ne pas être fiable à 100%, il est donc conseillé d'activer également la protection par jeton : +Cela désactive cependant la protection pour toutes les origines. Pour n'autoriser que certaines origines précises, désactivez la protection et vérifiez vous-même l'en-tête `Origin` contre votre propre liste d'autorisations. -```php -$form->addProtection(); -``` +La protection repose sur l'en-tête `Sec-Fetch-Site` du navigateur (Fetch Metadata), que celui-ci envoie automatiquement et qu'il est impossible de falsifier, même avec une faille XSS. Les navigateurs plus anciens, qui n'envoient pas ces en-têtes, ne passeront pas le contrôle. L'article [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] le décrit en détail. -Nous recommandons de protéger ainsi les formulaires dans la partie administration du site, qui modifient des données sensibles dans l'application. Le framework se défend contre l'attaque CSRF en générant et en vérifiant un jeton d'autorisation qui est stocké dans la session. Il est donc nécessaire d'avoir une session ouverte avant d'afficher le formulaire. Dans la partie administration du site, la session est généralement déjà démarrée en raison de la connexion de l'utilisateur. Sinon, démarrez la session avec la méthode `Nette\Http\Session::start()`. +.[note] +L'ancienne protection par un token d'autorisation stocké en session, activée par `$form->addProtection()`, n'est plus nécessaire et est obsolète depuis la version 3.3. -Voilà, nous avons fait une rapide introduction aux formulaires dans Nette. Essayez de jeter un œil au répertoire [examples|https://github.com/nette/forms/tree/master/examples] dans la distribution, où vous trouverez plus d'inspiration. +Voilà, nous avons fait un tour d'horizon rapide des formulaires dans Nette. Pour plus d'inspiration, essayez de regarder dans le répertoire des [exemples |https://github.com/nette/forms/tree/master/examples] de la distribution. diff --git a/forms/fr/upgrading.texy b/forms/fr/upgrading.texy new file mode 100644 index 0000000000..eaf91f1450 --- /dev/null +++ b/forms/fr/upgrading.texy @@ -0,0 +1,54 @@ +Mise à niveau +************* + + +Mise à niveau vers la version 3.3 +================================= + +- la protection automatique contre le CSRF est passée de `isSameSite()` à `isFrom(FetchSite::SameOrigin)` et est devenue plus stricte : les requêtes provenant de sous-domaines ne passent plus +- de ce fait, `addProtection()` n'est plus nécessaire, car la protection automatique couvre les mêmes cas ; ne l'utilisez pas dans les nouveaux formulaires et n'hésitez pas à le retirer des formulaires existants + +L'article [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf] explique pourquoi les tokens en session ne sont plus nécessaires. + + +Mise à niveau vers la version 3.1 +================================= + +- `getValues()` ne renvoie que les champs validés ; si vous avez besoin des valeurs de tous les champs indépendamment de la validation, utilisez la nouvelle méthode `getUntrustedValues()` +- les `$values` passées aux gestionnaires `onSuccess` et `onClick` ne contiennent elles aussi que les champs validés +- les formulaires autonomes sont automatiquement protégés contre le CSRF par un cookie portant le drapeau SameSite ; vous pouvez autoriser la soumission depuis une autre origine avec `allowCrossOrigin()` +- la règle `Form::URL` complète désormais un protocole manquant par `https` au lieu de `http` +- `Form::addImage()` a été renommée en `addImageButton()` +- `Checkbox::getSeparatorPrototype()` a été renommée en `getContainerPrototype()` +- les formulaires ne créent plus la variable `$_form` dans les templates + +Plus d'informations sur ces changements dans l'article [News in Nette Forms 3.1 |https://blog.nette.org/en/news-in-nette-forms-3-1]. + + +Mise à niveau vers la version 3.0 +================================= + +- tous les champs de formulaire sont désormais facultatifs par défaut (ce changement a été introduit dans Nette 2.4), vous pouvez donc supprimer `setRequired(false)` +- pensez à mettre à jour `netteForms.js` vers la version 3 (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` et `MultiChoiceControl::$checkAllowedValues` ont été remplacés par la méthode `checkDefaultValue()` + + +Mise à niveau vers la version 2.4 +================================= + +- si un champ possède une règle via `addRule()` (autrement dit s'il est de fait obligatoire), vous devez aussi le marquer comme obligatoire via `setRequired()` ; par ailleurs, `setRequired(false)` rend désormais le champ facultatif, ce qui remplace les branches `addCondition($form::FILLED)` +- les validateurs `Form::EMAIL`, `URL` et `INTEGER` changent automatiquement l'attribut HTML `type` en `email`, `url` et `number` respectivement +- les règles de validation négatives sont obsolètes ; l'équivalent de `~Form::FILLED` est `Form::BLANK`, et `~Form::EQUAL` peut être remplacé par `Form::NOT_EQUAL` +- le paramètre interne `do` est désormais envoyé en POST sous le nom `_do` afin d'éviter une collision +- les variables internes commençant par un tiret bas, comme `$_form`, sont obsolètes +- pensez à mettre à jour `netteForms.js` + + +Mise à niveau vers la version 2.3 +================================= + +- les méthodes internes de filtrage comme `Nette\Forms\Controls\TextBase::filterFloat` ont été supprimées +- les méthodes internes de validation comme `TextBase::validateFloat` ont été déplacées vers `Nette\Forms\Validator`, tout comme `Rules::$defaultMessages` +- les boutons et les champs Hidden sont générés sans ID HTML ; si vous voulez un ID, définissez-le via `setHtmlId()` +- les éléments RadioList sont eux aussi générés sans ID ; vous pouvez l'activer via `$radioList->generateId = true` +- les filtres ajoutés via `TextBase::addFilter()` sont traités pendant la validation, et vous pouvez désormais ajouter des filtres aux conditions : `$input->addCondition(...)->addFilter(...)` diff --git a/forms/fr/validation.texy b/forms/fr/validation.texy index 0303e327d9..68f04a3926 100644 --- a/forms/fr/validation.texy +++ b/forms/fr/validation.texy @@ -2,47 +2,47 @@ Validation des formulaires ************************** -Éléments obligatoires -===================== +Champs obligatoires +=================== -Nous marquons les éléments obligatoires avec la méthode `setRequired()`, dont l'argument est le texte du [message d'erreur |#Messages d erreur] qui s'affiche si l'utilisateur ne remplit pas l'élément. Si nous n'indiquons pas d'argument, le message d'erreur par défaut est utilisé. +Les champs sont marqués comme obligatoires à l'aide de la méthode `setRequired()`. Son argument est le texte du [message d'erreur |#Messages d'erreur] qui sera affiché si l'utilisateur ne remplit pas le champ. Si aucun argument n'est fourni, le message d'erreur par défaut est utilisé. ```php -$form->addText('name', 'Nom:') - ->setRequired('Veuillez saisir un nom'); +$form->addText('name', 'Nom :') + ->setRequired('Veuillez saisir votre nom.'); ``` Règles ====== -Nous ajoutons des règles de validation aux éléments avec la méthode `addRule()`. Le premier paramètre est la règle, le deuxième est le texte du [message d'erreur |#Messages d erreur] et le troisième est l'argument de la règle de validation. +Nous ajoutons des règles de validation aux champs à l'aide de la méthode `addRule()`. Le premier paramètre est la règle, le deuxième le [message d'erreur |#Messages d'erreur] et le troisième l'argument de la règle de validation. ```php -$form->addPassword('password', 'Mot de passe:') +$form->addPassword('password', 'Mot de passe :') ->addRule($form::MinLength, 'Le mot de passe doit comporter au moins %d caractères', 8); ``` -**Les règles de validation ne sont vérifiées que si l'utilisateur a rempli l'élément.** +**Les règles de validation ne sont vérifiées que si l'utilisateur a rempli le champ.** -Nette est livré avec un certain nombre de règles prédéfinies, dont les noms sont des constantes de la classe `Nette\Forms\Form`. Nous pouvons utiliser ces règles pour tous les éléments : +Nette est livré avec plusieurs règles prédéfinies dont les noms sont des constantes de la classe `Nette\Forms\Form`. Nous pouvons appliquer ces règles à tous les champs : -| constante | description | type d'argument +| constante | description | type de l'argument |------- -| `Required` | élément obligatoire, alias pour `setRequired()` | - -| `Filled` | élément obligatoire, alias pour `setRequired()` | - -| `Blank` | l'élément ne doit pas être rempli | - -| `Equal` | la valeur est égale au paramètre | `mixed` -| `NotEqual` | la valeur n'est pas égale au paramètre | `mixed` -| `IsIn` | la valeur est égale à l'un des éléments du tableau | `array` -| `IsNotIn` | la valeur n'est égale à aucun des éléments du tableau | `array` -| `Valid` | l'élément est-il rempli correctement ? (pour [#conditions]) | - +| `Required` | champ obligatoire, alias de `setRequired()` | - +| `Filled` | champ obligatoire, alias de `setRequired()` | - +| `Blank` | le champ ne doit pas être rempli | - +| `Equal` | la valeur doit être égale au paramètre | `mixed` +| `NotEqual` | la valeur ne doit pas être égale au paramètre | `mixed` +| `IsIn` | la valeur doit être l'un des éléments du tableau | `array` +| `IsNotIn` | la valeur ne doit être aucun des éléments du tableau | `array` +| `Valid` | le champ est-il rempli correctement ? (uniquement dans [addConditionOn() |#Conditions]) | - -Entrées textuelles ------------------- +Champs texte +------------ -Pour les éléments `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, certaines des règles suivantes peuvent également être utilisées : +Pour les champs `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, certaines des règles suivantes peuvent aussi être appliquées : | `MinLength` | longueur minimale du texte | `int` | `MaxLength` | longueur maximale du texte | `int` @@ -52,29 +52,29 @@ Pour les éléments `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, | `Pattern` | correspond à l'expression régulière | `string` | `PatternInsensitive` | comme `Pattern`, mais insensible à la casse | `string` | `Integer` | valeur entière | - -| `Numeric` | alias pour `Integer` | - +| `Numeric` | entier non négatif (chiffres seulement) | - | `Float` | nombre | - -| `Min` | valeur minimale de l'élément numérique | `int\|float` -| `Max` | valeur maximale de l'élément numérique | `int\|float` +| `Min` | valeur minimale d'un champ numérique | `int\|float` +| `Max` | valeur maximale d'un champ numérique | `int\|float` | `Range` | valeur dans une plage | paire `[int\|float, int\|float]` -Les règles de validation `Integer`, `Numeric` et `Float` convertissent directement la valeur en entier resp. flottant. De plus, la règle `URL` accepte également une adresse sans schéma (par ex. `nette.org`) et complète le schéma (`https://nette.org`). L'expression dans `Pattern` et `PatternIcase` doit s'appliquer à toute la valeur, c'est-à-dire comme si elle était entourée des caractères `^` et `$`. +Les règles de validation `Integer` et `Float` convertissent automatiquement la valeur en entier ou en nombre à virgule flottante. De plus, la règle `URL` accepte aussi une adresse sans schéma (par exemple `nette.org`) et complète le schéma (`https://nette.org`). L'expression de `Pattern` et `PatternInsensitive` doit être valable pour la valeur entière, c'est-à-dire comme si elle était encadrée par les caractères `^` et `$`. Nombre d'éléments ----------------- -Pour les éléments `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, les règles suivantes peuvent également être utilisées pour limiter le nombre d'éléments sélectionnés resp. de fichiers uploadés : +Pour les champs `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, vous pouvez aussi utiliser les règles suivantes pour limiter le nombre d'éléments sélectionnés ou de fichiers envoyés : -| `MinLength` | nombre minimum | `int` -| `MaxLength` | nombre maximum | `int` +| `MinLength` | nombre minimal | `int` +| `MaxLength` | nombre maximal | `int` | `Length` | nombre dans une plage ou nombre exact | paire `[int, int]` ou `int` Upload de fichiers ------------------ -Pour les éléments `addUpload()`, `addMultiUpload()`, les règles suivantes peuvent également être utilisées : +Pour les champs `addUpload()`, `addMultiUpload()`, les règles suivantes peuvent aussi être utilisées : | `MaxFileSize` | taille maximale du fichier en octets | `int` | `MimeType` | type MIME, caractères génériques autorisés (`'video/*'`) | `string\|string[]` @@ -82,32 +82,32 @@ Pour les éléments `addUpload()`, `addMultiUpload()`, les règles suivantes peu | `Pattern` | le nom du fichier correspond à l'expression régulière | `string` | `PatternInsensitive` | comme `Pattern`, mais insensible à la casse | `string` -`MimeType` et `Image` nécessitent l'extension PHP `fileinfo`. Le fait qu'un fichier ou une image soit du type requis est détecté sur la base de sa signature et **ne vérifie pas l'intégrité de l'ensemble du fichier.** On peut vérifier si une image n'est pas endommagée, par exemple, en essayant de la [charger |http:request#toImage]. +`MimeType` et `Image` nécessitent l'extension PHP `fileinfo`. Le fait qu'un fichier ou une image soit du type requis est détecté d'après sa signature, et **l'intégrité du fichier entier n'est pas vérifiée.** Vous pouvez déterminer si une image est endommagée, par exemple, en essayant de la [charger |http:request#toImage()]. Messages d'erreur ================= -Toutes les règles prédéfinies à l'exception de `Pattern` et `PatternInsensitive` ont un message d'erreur par défaut, il est donc possible de l'omettre. Cependant, en indiquant et en formulant tous les messages sur mesure, vous rendrez le formulaire plus convivial. +Toutes les règles prédéfinies, à l'exception de `Pattern` et `PatternInsensitive`, ont un message d'erreur par défaut, elles peuvent donc l'omettre. Cependant, en fournissant et en formulant tous les messages personnalisés adaptés à vos besoins, vous rendrez le formulaire plus convivial. -Vous pouvez modifier les messages par défaut dans la [configuration|forms:configuration], en modifiant les textes dans le tableau `Nette\Forms\Validator::$messages` ou en utilisant un [traducteur |rendering#Traduction]. +Vous pouvez changer les messages par défaut dans la [configuration |forms:configuration], en modifiant les textes du tableau `Nette\Forms\Validator::$messages`, ou à l'aide d'un [traducteur |rendering#Traduction]. -Dans le texte des messages d'erreur, les chaînes de remplacement suivantes peuvent être utilisées : +Les chaînes de substitution suivantes peuvent être utilisées dans le texte des messages d'erreur : -| `%d` | remplace successivement par les arguments de la règle -| `%n$d` | remplace par le n-ième argument de la règle -| `%label` | remplace par le libellé de l'élément (sans les deux points) -| `%name` | remplace par le nom de l'élément (par ex. `name`) -| `%value` | remplace par la valeur saisie par l'utilisateur +| `%d` | remplacé successivement par les arguments de la règle +| `%n$d` | remplacé par le n-ième argument de la règle +| `%label` | remplacé par le label du champ (sans les deux-points) +| `%name` | remplacé par le nom du champ (par exemple `name`) +| `%value` | remplacé par la valeur saisie par l'utilisateur ```php -$form->addText('name', 'Nom:') +$form->addText('name', 'Nom :') ->setRequired('Veuillez remplir %label'); -$form->addInteger('id', 'ID:') +$form->addInteger('id', 'ID :') ->addRule($form::Range, 'au moins %d et au plus %d', [5, 10]); -$form->addInteger('id', 'ID:') +$form->addInteger('id', 'ID :') ->addRule($form::Range, 'au plus %2$d et au moins %1$d', [5, 10]); ``` @@ -115,34 +115,34 @@ $form->addInteger('id', 'ID:') Conditions ========== -En plus des règles, il est également possible d'ajouter des conditions. Elles s'écrivent de la même manière que les règles, sauf qu'au lieu de `addRule()`, nous utilisons la méthode `addCondition()` et, bien sûr, nous n'indiquons aucun message d'erreur (la condition ne fait que demander) : +Outre les règles, il est aussi possible d'ajouter des conditions. Elles s'écrivent de façon semblable aux règles, mais au lieu d'`addRule()` nous utilisons la méthode `addCondition()` et, naturellement, nous ne fournissons pas de message d'erreur (la condition ne fait que poser une question) : ```php -$form->addPassword('password', 'Mot de passe:') - // si le mot de passe n'est pas plus long que 8 caractères +$form->addPassword('password', 'Mot de passe :') + // si la longueur du mot de passe ne dépasse pas 8 ->addCondition($form::MaxLength, 8) // alors il doit contenir un chiffre ->addRule($form::Pattern, 'Doit contenir un chiffre', '.*[0-9].*'); ``` -La condition peut également être liée à un autre élément que l'élément actuel à l'aide de `addConditionOn()`. Comme premier paramètre, nous indiquons une référence à l'élément. Dans cet exemple, l'e-mail ne sera obligatoire que si la case à cocher est cochée (sa valeur sera true) : +La condition peut être liée à un autre champ que le champ courant à l'aide d'`addConditionOn()`. Le premier paramètre est une référence au champ. Dans cet exemple, l'e-mail ne sera obligatoire que si la case est cochée (c'est-à-dire si sa valeur est true) : ```php -$form->addCheckbox('newsletters', 'envoyez-moi les newsletters'); +$form->addCheckbox('newsletters', 'Envoyez-moi les newsletters'); -$form->addEmail('email', 'E-mail:') - // si la case à cocher est cochée +$form->addEmail('email', 'E-mail :') + // si la case est cochée ->addConditionOn($form['newsletters'], $form::Equal, true) // alors exiger l'e-mail - ->setRequired('Saisissez une adresse e-mail'); + ->setRequired('Saisissez votre adresse e-mail'); ``` -Il est possible de créer des structures complexes à partir de conditions à l'aide de `elseCondition()` et `endCondition()` : +Les conditions peuvent former des structures complexes à l'aide d'`elseCondition()` et `endCondition()` : ```php $form->addText(/* ... */) ->addCondition(/* ... */) // si la première condition est remplie - ->addConditionOn(/* ... */) // et la deuxième condition sur un autre élément + ->addConditionOn(/* ... */) // et que la deuxième condition sur un autre champ l'est aussi ->addRule(/* ... */) // exiger cette règle ->elseCondition() // si la deuxième condition n'est pas remplie ->addRule(/* ... */) // exiger ces règles @@ -151,47 +151,55 @@ $form->addText(/* ... */) ->addRule(/* ... */); ``` -Dans Nette, il est très facile de réagir à la satisfaction ou non d'une condition également côté JavaScript à l'aide de la méthode `toggle()`, voir [#javascript dynamique]. +Le premier argument d'`addCondition()` peut aussi être une valeur booléenne. C'est utile lorsque la décision est déjà connue au moment de la construction du formulaire, par exemple pour n'appliquer une règle que dans certaines circonstances : + +```php +$form->addText('nickname') + ->addCondition($isRequired) // une valeur connue lors de la construction du formulaire + ->setRequired(); +``` + +Dans Nette, il est très facile de réagir côté JavaScript au fait qu'une condition soit remplie ou non, grâce à la méthode `toggle()`, voir [#JavaScript dynamique]. -Référence à un autre élément -============================ +Référence à un autre champ +========================== -Comme argument de règle ou de condition, il est également possible de passer un autre élément du formulaire. La règle utilisera alors la valeur saisie ultérieurement par l'utilisateur dans le navigateur. Ainsi, il est possible, par exemple, de valider dynamiquement que l'élément `password` contient la même chaîne que l'élément `password_confirm` : +Vous pouvez aussi passer comme argument d'une règle ou d'une condition un autre champ du formulaire. La règle utilisera alors la valeur saisie plus tard par l'utilisateur dans le navigateur. Cela permet par exemple de valider dynamiquement que le champ `password` contient la même chaîne que le champ `password_confirm` : ```php $form->addPassword('password', 'Mot de passe'); $form->addPassword('password_confirm', 'Confirmez le mot de passe') - ->addRule($form::Equal, 'Les mots de passe saisis ne correspondent pas', $form['password']); + ->addRule($form::Equal, 'Les mots de passe ne correspondent pas', $form['password']); ``` Règles et conditions personnalisées =================================== -Parfois, nous nous trouvons dans une situation où les règles de validation intégrées dans Nette ne suffisent pas et nous devons valider les données de l'utilisateur à notre manière. Dans Nette, c'est très simple ! +Il arrive que les règles de validation intégrées à Nette ne suffisent pas et que nous ayons besoin de valider les données de l'utilisateur à notre façon. Dans Nette, c'est très simple ! -Aux méthodes `addRule()` ou `addCondition()`, il est possible de passer n'importe quel callback comme premier paramètre. Celui-ci reçoit comme premier paramètre l'élément lui-même et retourne une valeur booléenne indiquant si la validation s'est déroulée correctement. Lors de l'ajout d'une règle à l'aide de `addRule()`, il est possible de spécifier d'autres arguments, qui sont ensuite passés comme deuxième paramètre. +Vous pouvez passer n'importe quel callback comme premier paramètre aux méthodes `addRule()` ou `addCondition()`. Le callback reçoit le champ lui-même comme premier paramètre et renvoie une valeur booléenne indiquant si la validation a réussi. Lors de l'ajout d'une règle avec `addRule()`, des arguments supplémentaires peuvent être fournis, qui lui sont ensuite passés comme deuxième paramètre. -Nous pouvons ainsi créer notre propre ensemble de validateurs sous forme de classe avec des méthodes statiques : +Un jeu de validateurs personnalisés peut ainsi être créé sous forme de classe avec des méthodes statiques : ```php class MyValidators { // teste si la valeur est divisible par l'argument - public static function validateDivisibility(Nette\Forms\Control $input, $arg): bool + public static function validateDivisibility(BaseControl $input, $arg): bool { return $input->getValue() % $arg === 0; } - public static function validateEmailDomain(Nette\Forms\Control $input, $domain) + public static function validateEmailDomain(BaseControl $input, $domain) { // autres validateurs } } ``` -L'utilisation est alors très simple : +L'utilisation est ensuite très simple : ```php $form->addInteger('num') @@ -202,7 +210,7 @@ $form->addInteger('num') ); ``` -Il est également possible d'ajouter des règles de validation personnalisées à JavaScript. La condition est que la règle soit une méthode statique. Son nom pour le validateur JavaScript est formé en joignant le nom de la classe sans les barres obliques inverses `\`, le trait de soulignement `_` et le nom de la méthode. Par exemple, `App\MyValidators::validateDivisibility` s'écrira `AppMyValidators_validateDivisibility` et sera ajouté à l'objet `Nette.validators`: +Les règles de validation personnalisées peuvent aussi être ajoutées en JavaScript. La condition est que la règle soit une méthode statique. Son nom pour le validateur JavaScript se forme en concaténant le nom de la classe sans les antislashs `\`, un tiret bas `_` et le nom de la méthode. Par exemple, `App\MyValidators::validateDivisibility` s'écrit `AppMyValidators_validateDivisibility` et s'ajoute à l'objet `Nette.validators` : ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -214,20 +222,20 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => Événement onValidate ==================== -Après la soumission du formulaire, une validation est effectuée, au cours de laquelle les règles individuelles ajoutées à l'aide de `addRule()` sont vérifiées, puis l'[événement |nette:glossary#Événements events] `onValidate` est déclenché. Son gestionnaire peut être utilisé pour une validation supplémentaire, typiquement pour vérifier la combinaison correcte de valeurs dans plusieurs éléments du formulaire. +Après la soumission du formulaire, la validation est effectuée : les différentes règles ajoutées par `addRule()` sont vérifiées, puis l'[événement |nette:glossary#Événements] `onValidate` est déclenché. Son gestionnaire peut servir à une validation supplémentaire, typiquement pour vérifier la bonne combinaison de valeurs de plusieurs champs du formulaire. -Si une erreur est détectée, nous la transmettons au formulaire à l'aide de la méthode `addError()`. Celle-ci peut être appelée soit sur un élément spécifique, soit directement sur le formulaire. +Si une erreur est détectée, elle est transmise au formulaire à l'aide de la méthode `addError()`. Celle-ci peut être appelée soit sur un champ précis, soit directement sur le formulaire. ```php protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('Cette combinaison n\'est pas possible.'); @@ -239,7 +247,7 @@ public function validateSignInForm(Form $form, \stdClass $data): void Erreurs lors du traitement ========================== -Dans de nombreux cas, nous ne découvrons une erreur qu'au moment où nous traitons un formulaire valide, par exemple lors de l'écriture d'un nouvel élément dans la base de données et que nous rencontrons une duplication de clés. Dans ce cas, nous transmettons à nouveau l'erreur au formulaire à l'aide de la méthode `addError()`. Celle-ci peut être appelée soit sur un élément spécifique, soit directement sur le formulaire : +Dans bien des cas, nous ne découvrons une erreur qu'au moment du traitement d'un formulaire valide, par exemple en écrivant un nouvel enregistrement dans la base de données et en tombant sur une clé dupliquée. Dans ce cas, nous renvoyons là encore l'erreur au formulaire à l'aide de la méthode `addError()`. Celle-ci peut être appelée soit sur un champ précis, soit directement sur le formulaire : ```php try { @@ -254,77 +262,83 @@ try { } ``` -Si possible, nous recommandons d'attacher l'erreur directement à l'élément du formulaire, car elle s'affichera à côté de lui lors de l'utilisation du moteur de rendu par défaut. +Si possible, nous recommandons d'ajouter l'erreur directement au champ du formulaire, car elle sera alors affichée à côté de lui avec le renderer par défaut. ```php -$form['date']->addError('Désolé, mais cette date est déjà prise.'); +$form['date']->addError('Désolé, cette date est déjà prise.'); ``` -Vous pouvez appeler `addError()` à plusieurs reprises et ainsi transmettre plusieurs messages d'erreur au formulaire ou à l'élément. Vous les obtenez à l'aide de `getErrors()`. +Vous pouvez appeler `addError()` à plusieurs reprises pour transmettre plusieurs messages d'erreur à un formulaire ou à un champ. Vous les récupérez avec `getErrors()`. -Attention, `$form->getErrors()` retourne un résumé de tous les messages d'erreur, y compris ceux qui ont été transmis directement aux éléments individuels, et pas seulement directement au formulaire. Les messages d'erreur transmis uniquement au formulaire sont obtenus via `$form->getOwnErrors()`. +Notez que `$form->getErrors()` renvoie un récapitulatif de tous les messages d'erreur, y compris ceux transmis directement aux différents champs, et pas seulement ceux transmis directement au formulaire. Les messages d'erreur transmis uniquement au formulaire s'obtiennent via `$form->getOwnErrors()`. -Modification de l'entrée -======================== +Modifier les valeurs saisies +============================ À l'aide de la méthode `addFilter()`, nous pouvons modifier la valeur saisie par l'utilisateur. Dans cet exemple, nous tolérerons et supprimerons les espaces dans le code postal : ```php -$form->addText('zip', 'Code postal:') +$form->addText('zip', 'Code postal :') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // nous supprimons les espaces du code postal + return str_replace(' ', '', $value); // supprime les espaces du code postal }) - ->addRule($form::Pattern, 'Le code postal n\'est pas sous la forme de cinq chiffres', '\d{5}'); + ->addRule($form::Pattern, 'Le code postal ne comporte pas cinq chiffres', '\d{5}'); ``` -Le filtre s'insère entre les règles de validation et les conditions, et l'ordre des méthodes est donc important, c'est-à-dire que le filtre et la règle sont appelés dans l'ordre où les méthodes `addFilter()` et `addRule()` sont placées. +Le filtre s'intègre parmi les règles de validation et les conditions, l'ordre des méthodes a donc de l'importance : le filtre et la règle sont appelés dans le même ordre que celui où les méthodes `addFilter()` et `addRule()` sont écrites. Validation JavaScript ===================== -Le langage pour formuler les conditions et les règles est très puissant. Toutes les constructions fonctionnent à la fois côté serveur et côté JavaScript. Elles sont transmises dans les attributs HTML `data-nette-rules` sous forme de JSON. La validation elle-même est ensuite effectuée par un script qui intercepte l'événement `submit` du formulaire, parcourt les éléments individuels et effectue la validation appropriée. +Le langage de formulation des conditions et des règles est très puissant. Toutes les constructions fonctionnent aussi bien côté serveur que côté client en JavaScript. Elles sont transmises dans les attributs HTML `data-nette-rules` sous forme de JSON. La validation elle-même est assurée par un script qui intercepte l'événement `submit` du formulaire, parcourt les différents champs et effectue la validation correspondante. -Ce script est `netteForms.js` et est disponible à partir de plusieurs sources possibles : +Ce script est `netteForms.js` et il est disponible depuis plusieurs sources possibles : -Vous pouvez insérer le script directement dans la page HTML depuis un CDN : +Vous pouvez intégrer le script directement dans la page HTML depuis un CDN : ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Ou le copier localement dans le dossier public du projet (par ex. depuis `vendor/nette/forms/src/assets/netteForms.min.js`) : +Ou le copier localement dans le dossier public de votre projet (par exemple depuis `vendor/nette/forms/src/assets/netteForms.min.js`) : ```latte <script src="/path/to/netteForms.min.js"></script> ``` -Ou l'installer via [npm|https://www.npmjs.com/package/nette-forms] : +Ou l'installer via [npm |https://www.npmjs.com/package/nette-forms] : ```shell npm install nette-forms ``` -Et ensuite le charger et l'exécuter : +Puis le charger et l'exécuter : ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -Alternativement, vous pouvez le charger directement depuis le dossier `vendor` : +Vous pouvez aussi le charger directement depuis le dossier `vendor` : ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Vous pouvez désactiver entièrement la validation côté client en ajoutant l'attribut `novalidate` au formulaire. Le script `netteForms.js` ne le valide alors pas à la soumission, et la validation n'a donc lieu que sur le serveur : + +```php +$form->setHtmlAttribute('novalidate'); +``` + JavaScript dynamique ==================== -Voulez-vous afficher les champs pour saisir l'adresse uniquement si l'utilisateur choisit d'envoyer la marchandise par la poste ? Aucun problème. La clé est la paire de méthodes `addCondition()` & `toggle()` : +Vous voulez n'afficher les champs d'adresse que si l'utilisateur choisit de se faire envoyer la marchandise par la poste ? Aucun problème. La clé est le couple de méthodes `addCondition()` & `toggle()` : ```php $form->addCheckbox('send_it') @@ -332,25 +346,25 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -Ce code dit que lorsque la condition est remplie, c'est-à-dire lorsque la case à cocher est cochée, l'élément HTML `#address-container` sera visible. Et inversement. Nous plaçons donc les éléments du formulaire avec l'adresse du destinataire dans un conteneur avec cet ID, et lors du clic sur la case à cocher, ils se masquent ou s'affichent. C'est le script `netteForms.js` qui s'en charge. +Ce code dit que, lorsque la condition est remplie (c'est-à-dire lorsque la case est cochée), l'élément HTML `#address-container` sera visible, et inversement. Nous plaçons donc les champs de formulaire contenant l'adresse du destinataire dans un conteneur portant cet ID, et ils se masqueront ou s'afficheront au clic sur la case. C'est le script `netteForms.js` qui s'en charge. -Comme argument de la méthode `toggle()`, il est possible de passer n'importe quel sélecteur. Pour des raisons historiques, une chaîne alphanumérique sans autres caractères spéciaux est comprise comme l'ID de l'élément, c'est-à-dire comme si elle était précédée du caractère `#`. Le deuxième paramètre facultatif permet d'inverser le comportement, c'est-à-dire que si nous utilisions `toggle('#address-container', false)`, l'élément ne s'afficherait au contraire que si la case à cocher n'était pas cochée. +N'importe quel sélecteur peut être passé comme argument à la méthode `toggle()`. Pour des raisons historiques, une chaîne qui commence par une lettre, un chiffre ou un tiret bas et qui ne contient que des lettres, des chiffres, des tirets bas, des traits d'union, des points et des deux-points est traitée comme un ID d'élément, comme si elle était précédée du caractère `#`. Le deuxième paramètre facultatif permet d'inverser le comportement ; ainsi, si nous écrivions `toggle('#address-container', false)`, l'élément ne serait affiché que si la case n'était *pas* cochée. -L'implémentation par défaut en JavaScript modifie la propriété `hidden` des éléments. Cependant, nous pouvons facilement changer le comportement, par exemple en ajoutant une animation. Il suffit de remplacer la méthode `Nette.toggle` en JavaScript par notre propre solution : +L'implémentation JavaScript par défaut modifie la propriété `hidden` des éléments. Nous pouvons cependant changer facilement ce comportement, par exemple en ajoutant une animation. Il suffit de redéfinir la méthode `Nette.toggle` en JavaScript par une solution personnalisée : ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // nous masquons ou affichons 'el' selon la valeur de 'visible' + // masquer ou afficher 'el' selon la valeur de 'visible' }); }; ``` -Désactivation de la validation -============================== +Désactiver la validation +======================== -Parfois, il peut être utile de désactiver la validation. Si l'appui sur le bouton d'envoi ne doit pas effectuer de validation (approprié pour les boutons *Annuler* ou *Aperçu*), nous la désactivons avec la méthode `$submit->setValidationScope([])`. S'il doit effectuer une validation partielle, nous pouvons spécifier quels champs ou conteneurs de formulaire doivent être validés. +Il peut parfois être utile de désactiver la validation. Si l'appui sur un bouton d'envoi ne doit pas déclencher la validation (utile pour les boutons *Annuler* ou *Aperçu*), nous la désactivons avec la méthode `$submit->setValidationScope([])`. S'il ne doit déclencher qu'une validation partielle, nous pouvons indiquer quels champs ou conteneurs du formulaire doivent être validés. ```php $form->addText('name') @@ -364,13 +378,15 @@ $details->addInteger('age2') $form->addSubmit('send1'); // Valide tout le formulaire $form->addSubmit('send2') - ->setValidationScope([]); // Ne valide rien du tout + ->setValidationScope([]); // Ne valide rien $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Valide uniquement l'élément name + ->setValidationScope([$form['name']]); // Ne valide que le champ 'name' $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Valide uniquement l'élément age + ->setValidationScope([$form['details']['age']]); // Ne valide que le champ 'age' $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Valide le conteneur details + ->setValidationScope([$form['details']]); // Valide le conteneur 'details' ``` -`setValidationScope` n'affecte pas l'[#événement onValidate] du formulaire, qui sera toujours appelée. L'événement `onValidate` du conteneur ne sera déclenché que si ce conteneur est marqué pour une validation partielle. +`setValidationScope` n'a aucune incidence sur l'[#Événement onValidate] du formulaire, qui sera toujours appelé. L'événement `onValidate` d'un conteneur ne sera déclenché que si ce conteneur est marqué pour la validation partielle. + +La validation partielle influence aussi les valeurs renvoyées par `getValues()` : le résultat ne contient que les valeurs des champs qui entrent dans le périmètre de validation. Les valeurs des champs hors de ce périmètre sont omises. diff --git a/forms/hu/@home.texy b/forms/hu/@home.texy deleted file mode 100644 index 2a5dfbfe40..0000000000 --- a/forms/hu/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -A Nette Forms forradalmasította a webes űrlapok létrehozását. Hirtelen elég volt néhány érthető sor kódot írni, és kész volt az űrlap, beleértve a renderelést, a JavaScript és szerveroldali validációt, ráadásul csúcsminőségű biztonsággal. Megmutatjuk, hogyan - -- hozzunk létre felhasználóbarát űrlapokat -- validáljuk az elküldött adatokat -- rendereljük az elemeket pontosan az igények szerint - -</div> - - -A Nette Forms használatával elkerülheti a rutin feladatok egész sorát, mint például a validáció írását (ráadásul kétszer, szerver- és kliensoldalon), minimalizálhatja a hibák és biztonsági rések kialakulásának valószínűségét. - -Az űrlapokat használhatja a Nette Alkalmazás részeként (azaz presenterekben), vagy teljesen önállóan. Mivel mindkét esetben a használat kissé eltérő, két útmutatót készítettünk Önnek: - -<div class="wiki-buttons"> -<div> "Űrlapok presenterekben .[wiki-button]":in-presenter </div> -<div> "Űrlapok önállóan .[wiki-button]":standalone </div> -</div> - - -Telepítés ---------- - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/forms -``` diff --git a/forms/hu/@left-menu.texy b/forms/hu/@left-menu.texy deleted file mode 100644 index 6fda5dc323..0000000000 --- a/forms/hu/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Bevezetés |@home] -- [Űrlapok presenterekben|in-presenter] -- [Űrlapok önállóan|standalone] -- [Űrlap elemek |controls] -- [Validáció |validation] -- [Renderelés |rendering] -- [Konfiguráció |configuration] - - -További olvasmányok -******************* -- [Útmutatók és eljárások |best-practices:] diff --git a/forms/hu/@meta.texy b/forms/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/forms/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/forms/hu/configuration.texy b/forms/hu/configuration.texy deleted file mode 100644 index d4b7a767c0..0000000000 --- a/forms/hu/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Űrlapok konfigurálása -********************* - -.[perex] -A konfigurációban megváltoztathatók az alapértelmezett [űrlap hibaüzenetek|validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Itt a magyar fordítás: - -```neon -forms: - messages: - Equal: 'Kérjük, adja meg a %s értéket.' - NotEqual: 'Ez az érték nem lehet %s.' - Filled: 'Ez a mező kötelező.' - Blank: 'Ennek a mezőnek üresnek kell lennie.' - MinLength: 'Kérjük, adjon meg legalább %d karaktert.' - MaxLength: 'Kérjük, legfeljebb %d karaktert adjon meg.' - Length: 'Kérjük, adjon meg egy %d és %d karakter közötti értéket.' - Email: 'Kérjük, adjon meg egy érvényes e-mail címet.' - URL: 'Kérjük, adjon meg egy érvényes URL-t.' - Integer: 'Kérjük, adjon meg egy érvényes egész számot.' - Float: 'Kérjük, adjon meg egy érvényes számot.' - Min: 'Kérjük, adjon meg egy %d vagy annál nagyobb értéket.' - Max: 'Kérjük, adjon meg egy %d vagy annál kisebb értéket.' - Range: 'Kérjük, adjon meg egy %d és %d közötti értéket.' - MaxFileSize: 'A feltöltött fájl mérete legfeljebb %d bájt lehet.' - MaxPostSize: 'A feltöltött adatok meghaladják a %d bájtos korlátot.' - MimeType: 'A feltöltött fájl nem a várt formátumban van.' - Image: 'A feltöltött fájlnak JPEG, GIF, PNG, WebP vagy AVIF formátumú képnek kell lennie.' - Nette\Forms\Controls\SelectBox::Valid: 'Kérjük, válasszon érvényes opciót.' - Nette\Forms\Controls\UploadControl::Valid: 'Hiba történt a fájl feltöltése során.' - Nette\Forms\Controls\CsrfProtection::Protection: 'A munkamenete lejárt. Kérjük, térjen vissza a kezdőlapra, és próbálja újra.' -``` - -Ha nem használja a teljes keretrendszert, és így a konfigurációs fájlokat sem, megváltoztathatja az alapértelmezett hibaüzeneteket közvetlenül a `Nette\Forms\Validator::$messages` tömbben. diff --git a/forms/hu/controls.texy b/forms/hu/controls.texy deleted file mode 100644 index 46c606fbef..0000000000 --- a/forms/hu/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Űrlap elemek -************ - -.[perex] -A standard űrlap elemek áttekintése. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Hozzáad egy egysoros szöveges mezőt (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Ha a felhasználó nem tölti ki a mezőt, üres stringet `''` ad vissza, vagy a `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza. - -```php -$form->addText('name', 'Név:') - ->setRequired() - ->setNullable(); -``` - -Automatikusan validálja az UTF-8 kódolást, levágja a bal- és jobboldali szóközöket, és eltávolítja azokat az újsor karaktereket, amelyeket egy támadó küldhetett. - -A maximális hosszúságot a `setMaxLength()` segítségével lehet korlátozni. A felhasználó által bevitt érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. - -A `setHtmlType()` segítségével megváltoztatható a szöveges mező vizuális jellege olyan típusokra, mint a `search`, `tel` vagy `url`, lásd a [specifikációt|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Ne feledje, hogy a típusváltoztatás csak vizuális, és nem helyettesíti a validálási funkciót. Az `url` típushoz célszerű hozzáadni egy specifikus [URL validálási szabályt |validation#Szöveges bevitelek]. - -.[note] -Más beviteli típusokhoz, mint például a `number`, `range`, `email`, `date`, `datetime-local`, `time` és `color`, használjon specializált metódusokat, mint a [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] és [#addColor], amelyek biztosítják a szerveroldali validációt. A `month` és `week` típusok még nem támogatottak teljes mértékben minden böngészőben. - -Az elemhez beállítható ún. empty-value, ami valami olyasmi, mint az alapértelmezett érték, de ha a felhasználó nem változtatja meg, az elem üres stringet vagy `null`-t ad vissza. - -```php -$form->addText('phone', 'Telefon:') - ->setHtmlType('tel') - ->setEmptyValue('+36'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Hozzáad egy mezőt többsoros szöveg bevitelére (osztály: [TextArea |api:Nette\Forms\Controls\TextArea]). Ha a felhasználó nem tölti ki a mezőt, üres stringet `''` ad vissza, vagy a `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza. - -```php -$form->addTextArea('note', 'Megjegyzés:') - ->addRule($form::MaxLength, 'A megjegyzés túl hosszú', 10000); -``` - -Automatikusan validálja az UTF-8 kódolást és normalizálja a sorelválasztókat `\n`-re. Ellentétben az egysoros beviteli mezővel, itt nem történik szóközök levágása. - -A maximális hosszúságot a `setMaxLength()` segítségével lehet korlátozni. A felhasználó által bevitt érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. Beállítható ún. empty-value a `setEmptyValue()` segítségével. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Hozzáad egy mezőt egész számok bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Vagy integert ad vissza, vagy `null`-t, ha a felhasználó nem ad meg semmit. - -```php -$form->addInteger('year', 'Év:') - ->addRule($form::Range, 'Az évnek %d és %d között kell lennie.', [1900, 2023]); -``` - -Az elem `<input type="number">`-ként jelenik meg. A `setHtmlType()` metódus használatával a típust `range`-re lehet változtatni a csúszka formájában történő megjelenítéshez, vagy `text`-re, ha a standard szöveges mezőt részesíti előnyben a `number` típus speciális viselkedése nélkül. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Hozzáad egy mezőt tizedes számok bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Vagy floatot ad vissza, vagy `null`-t, ha a felhasználó nem ad meg semmit. - -```php -$form->addFloat('level', 'Szint:') - ->setDefaultValue(0) - ->addRule($form::Range, 'A szintnek %d és %d között kell lennie.', [0, 100]); -``` - -Az elem `<input type="number">`-ként jelenik meg. A `setHtmlType()` metódus használatával a típust `range`-re lehet változtatni a csúszka formájában történő megjelenítéshez, vagy `text`-re, ha a standard szöveges mezőt részesíti előnyben a `number` típus speciális viselkedése nélkül. - -A Nette és a böngésző tizedes elválasztóként elfogadja mind a vesszőt, mind a pontot. Annak érdekében, hogy ez a funkcionalitás a Firefoxban is elérhető legyen, ajánlott beállítani a `lang` attribútumot vagy az adott elemre, vagy az egész oldalra, például `<html lang="hu">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Hozzáad egy mezőt e-mail cím bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Ha a felhasználó nem tölti ki a mezőt, üres stringet `''` ad vissza, vagy a `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Ellenőrzi, hogy az érték érvényes e-mail cím-e. Nem ellenőrzi, hogy a domain valóban létezik-e, csak a szintaxist ellenőrzi. Automatikusan validálja az UTF-8 kódolást, levágja a bal- és jobboldali szóközöket. - -A maximális hosszúságot a `setMaxLength()` segítségével lehet korlátozni. A felhasználó által bevitt érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. Beállítható ún. empty-value a `setEmptyValue()` segítségével. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Hozzáad egy mezőt jelszó bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Jelszó:') - ->setRequired() - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8) - ->addRule($form::Pattern, 'Tartalmaznia kell számjegyet', '.*[0-9].*'); -``` - -Az űrlap újramegjelenítésekor a mező üres lesz. Automatikusan validálja az UTF-8 kódolást, levágja a bal- és jobboldali szóközöket, és eltávolítja azokat az újsor karaktereket, amelyeket egy támadó küldhetett. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Hozzáad egy jelölőnégyzetet (osztály: [Checkbox |api:Nette\Forms\Controls\Checkbox]). Vagy `true` vagy `false` értéket ad vissza, attól függően, hogy be van-e jelölve. - -```php -$form->addCheckbox('agree', 'Elfogadom a feltételeket') - ->setRequired('El kell fogadni a feltételeket'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Hozzáad jelölőnégyzeteket több elem kiválasztásához (osztály: [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). A kiválasztott elemek kulcsainak tömbjét adja vissza. A `getSelectedItems()` metódus az értékeket adja vissza a kulcsok helyett. - -```php -$form->addCheckboxList('colors', 'Színek:', [ - 'r' => 'piros', - 'g' => 'zöld', - 'b' => 'kék', -]); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. - -A `setDisabled(['r', 'g'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elemek valóban a kínáltak közül valók-e, és nem voltak-e letiltva. A `getRawValue()` metódussal lekérhetők az elküldött elemek e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elemek beállításakor is ellenőrzi, hogy azok a kínáltak közül valók-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - -Ha az űrlapot `GET` metódussal küldi el, választhat egy kompaktabb adatátviteli módot, amely csökkenti a query string méretét. Ez az űrlap HTML attribútumának beállításával aktiválható: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Hozzáad rádiógombokat (osztály: [RadioList |api:Nette\Forms\Controls\RadioList]). A kiválasztott elem kulcsát adja vissza, vagy `null`-t, ha a felhasználó nem választott semmit. A `getSelectedItem()` metódus az értéket adja vissza a kulcs helyett. - -```php -$sex = [ - 'm' => 'férfi', - 'f' => 'nő', -]; -$form->addRadioList('gender', 'Nem:', $sex); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. - -A `setDisabled(['m', 'f'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elem valóban a kínáltak közül való-e, és nem volt-e letiltva. A `getRawValue()` metódussal lekérhető az elküldött elem e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elem beállításakor is ellenőrzi, hogy az a kínáltak közül való-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Hozzáad egy select boxot (osztály: [SelectBox |api:Nette\Forms\Controls\SelectBox]). A kiválasztott elem kulcsát adja vissza, vagy `null`-t, ha a felhasználó nem választott semmit. A `getSelectedItem()` metódus az értéket adja vissza a kulcs helyett. - -```php -$countries = [ - 'CZ' => 'Cseh Köztársaság', - 'SK' => 'Szlovákia', - 'GB' => 'Nagy-Britannia', -]; - -$form->addSelect('country', 'Ország:', $countries) - ->setDefaultValue('SK'); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. Az elemek lehetnek kétdimenziós tömbök is: - -```php -$countries = [ - 'Európa' => [ - 'CZ' => 'Cseh Köztársaság', - 'SK' => 'Szlovákia', - 'GB' => 'Nagy-Britannia', - ], - 'CA' => 'Kanada', - 'US' => 'USA', - '?' => 'más', -]; -``` - -A select boxoknál gyakran az első elemnek speciális jelentése van, felhívásként szolgál. Ilyen elem hozzáadására a `setPrompt()` metódus szolgál. - -```php -$form->addSelect('country', 'Ország:', $countries) - ->setPrompt('Válasszon országot'); -``` - -A `setDisabled(['CZ', 'SK'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elem valóban a kínáltak közül való-e, és nem volt-e letiltva. A `getRawValue()` metódussal lekérhető az elküldött elem e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elem beállításakor is ellenőrzi, hogy az a kínáltak közül való-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Hozzáad egy select boxot több elem kiválasztásához (osztály: [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). A kiválasztott elemek kulcsainak tömbjét adja vissza. A `getSelectedItems()` metódus az értékeket adja vissza a kulcsok helyett. - -```php -$form->addMultiSelect('countries', 'Országok:', $countries); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. Az elemek lehetnek kétdimenziós tömbök is. - -A `setDisabled(['CZ', 'SK'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elemek valóban a kínáltak közül valók-e, és nem voltak-e letiltva. A `getRawValue()` metódussal lekérhetők az elküldött elemek e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elemek beállításakor is ellenőrzi, hogy azok a kínáltak közül valók-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Hozzáad egy mezőt fájl feltöltéséhez (osztály: [UploadControl |api:Nette\Forms\Controls\UploadControl]). Egy [FileUpload |http:request#FileUpload] objektumot ad vissza, még akkor is, ha a felhasználó nem küldött fájlt, amit a `FileUpload::hasFile()` metódussal lehet ellenőrizni. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Az avatarnak JPEG, PNG, GIF, WebP vagy AVIF formátumúnak kell lennie.') - ->addRule($form::MaxFileSize, 'A maximális méret 1 MB.', 1024 * 1024); -``` - -Ha a fájl feltöltése nem sikerül megfelelően, az űrlap nem kerül sikeresen elküldésre, és hiba jelenik meg. Vagyis sikeres elküldés esetén nem szükséges ellenőrizni a `FileUpload::isOk()` metódust. - -Soha ne bízzon a `FileUpload::getName()` metódus által visszaadott eredeti fájlnévben, a kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy kárt okozzon vagy feltörje az alkalmazását. - -A `MimeType` és `Image` szabályok a fájl aláírása alapján észlelik a kívánt típust, és nem ellenőrzik annak integritását. Azt, hogy a kép nem sérült-e, például a [betöltésének |http:request#toImage] megkísérlésével lehet megállapítani. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Hozzáad egy mezőt több fájl egyidejű feltöltéséhez (osztály: [UploadControl |api:Nette\Forms\Controls\UploadControl]). [FileUpload |http:request#FileUpload] objektumok tömbjét adja vissza. A `FileUpload::hasFile()` metódus mindegyiknél `true`-t fog visszaadni. - -```php -$form->addMultiUpload('files', 'Fájlok:') - ->addRule($form::MaxLength, 'Legfeljebb %d fájlt lehet feltölteni', 10); -``` - -Ha valamelyik fájl feltöltése nem sikerül megfelelően, az űrlap nem kerül sikeresen elküldésre, és hiba jelenik meg. Vagyis sikeres elküldés esetén nem szükséges ellenőrizni a `FileUpload::isOk()` metódust. - -Soha ne bízzon a `FileUpload::getName()` metódus által visszaadott eredeti fájlnevekben, a kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy kárt okozzon vagy feltörje az alkalmazását. - -A `MimeType` és `Image` szabályok a fájl aláírása alapján észlelik a kívánt típust, és nem ellenőrzik annak integritását. Azt, hogy a kép nem sérült-e, például a [betöltésének |http:request#toImage] megkísérlésével lehet megállapítani. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Hozzáad egy mezőt, amely lehetővé teszi a felhasználó számára, hogy könnyen megadjon egy dátumot, amely évből, hónapból és napból áll (osztály: [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Alapértelmezett értékként elfogadja vagy a `DateTimeInterface` interfészt implementáló objektumokat, egy időt tartalmazó stringet, vagy egy UNIX timestamp-et képviselő számot. Ugyanez vonatkozik a `Min`, `Max` vagy `Range` szabályok argumentumaira is, amelyek meghatározzák a minimális és maximális megengedett dátumot. - -```php -$form->addDate('date', 'Dátum:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'A dátumnak legalább egy hónaposnak kell lennie.', new DateTime('-1 month')); -``` - -Alapértelmezés szerint `DateTimeImmutable` objektumot ad vissza, a `setFormat()` metódussal megadhatja a [szöveges formátumot|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] vagy a timestamp-et: - -```php -$form->addDate('date', 'Dátum:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Hozzáad egy mezőt, amely lehetővé teszi a felhasználó számára, hogy könnyen megadjon egy időt, amely órákból, percekből és opcionálisan másodpercekből áll (osztály: [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Alapértelmezett értékként elfogadja vagy a `DateTimeInterface` interfészt implementáló objektumokat, egy időt tartalmazó stringet, vagy egy UNIX timestamp-et képviselő számot. Ezekből a bemenetekből csak az időinformáció kerül felhasználásra, a dátum figyelmen kívül marad. Ugyanez vonatkozik a `Min`, `Max` vagy `Range` szabályok argumentumaira is, amelyek meghatározzák a minimális és maximális megengedett időt. Ha a beállított minimális érték magasabb, mint a maximális, akkor egy éjfélen átnyúló időtartomány jön létre. - -```php -$form->addTime('time', 'Idő:', withSeconds: true) - ->addRule($form::Range, 'Az időnek %d és %d között kell lennie.', ['12:30', '13:30']); -``` - -Alapértelmezés szerint `DateTimeImmutable` objektumot ad vissza (az 1. év január 1-jei dátummal), a `setFormat()` metódussal megadhatja a [szöveges formátumot|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Idő:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Hozzáad egy mezőt, amely lehetővé teszi a felhasználó számára, hogy könnyen megadjon egy dátumot és időt, amely évből, hónapból, napból, órákból, percekből és opcionálisan másodpercekből áll (osztály: [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Alapértelmezett értékként elfogadja vagy a `DateTimeInterface` interfészt implementáló objektumokat, egy időt tartalmazó stringet, vagy egy UNIX timestamp-et képviselő számot. Ugyanez vonatkozik a `Min`, `Max` vagy `Range` szabályok argumentumaira is, amelyek meghatározzák a minimális és maximális megengedett dátumot. - -```php -$form->addDateTime('datetime', 'Dátum és idő:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'A dátumnak legalább egy hónaposnak kell lennie.', new DateTime('-1 month')); -``` - -Alapértelmezés szerint `DateTimeImmutable` objektumot ad vissza, a `setFormat()` metódussal megadhatja a [szöveges formátumot|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] vagy a timestamp-et: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Hozzáad egy mezőt színválasztáshoz (osztály: [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). A szín egy `#rrggbb` formátumú string. Ha a felhasználó nem választ, a fekete szín `#000000` kerül visszaadásra. - -```php -$form->addColor('color', 'Szín:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Hozzáad egy rejtett mezőt (osztály: [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -A `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza üres string helyett. Az elküldött érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. - -Bár az elem rejtett, **fontos tudatosítani**, hogy az értékét egy támadó továbbra is módosíthatja vagy hamisíthatja. Mindig alaposan ellenőrizze és validálja az összes fogadott értéket a szerveroldalon, hogy megelőzze az adatmanipulációval kapcsolatos biztonsági kockázatokat. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Hozzáad egy küldés gombot (osztály: [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Küldés'); -``` - -Az űrlapon több küldés gomb is lehet: - -```php -$form->addSubmit('register', 'Regisztráció'); -$form->addSubmit('cancel', 'Mégse'); -``` - -Annak megállapításához, hogy melyikre kattintottak, használja a következőt: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Ha nem szeretné az egész űrlapot validálni a gomb megnyomásakor (például a *Mégse* vagy *Előnézet* gomboknál), használja a [setValidationScope() |validation#Validáció kikapcsolása] metódust. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Hozzáad egy gombot (osztály: [Button |api:Nette\Forms\Controls\Button]), amelynek nincs küldési funkciója. Használható tehát valamilyen más funkcióra, pl. JavaScript függvény meghívására kattintáskor. - -```php -$form->addButton('raise', 'Fizetésemelés') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Hozzáad egy kép formájú küldés gombot (osztály: [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/útvonal/a/képhez.png'); -``` - -Több küldés gomb használatakor a `$form['submit']->isSubmittedBy()` segítségével megállapítható, hogy melyikre kattintottak. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Hozzáad egy alűrlapot (osztály: [Container|api:Nette\Forms\Container]), vagyis egy konténert, amelybe ugyanúgy lehet további elemeket hozzáadni, mint ahogy az űrlaphoz adjuk őket. Működnek a `setDefaults()` vagy `getValues()` metódusok is. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Neved:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Neved:'); -$sub2->addEmail('email', 'Email:'); -``` - -Az elküldött adatok ezután többdimenziós struktúraként kerülnek visszaadásra: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Beállítások áttekintése -======================= - -Minden elemnél meghívhatjuk a következő metódusokat (teljes áttekintés az [API dokumentációban|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | beállítja az alapértelmezett értéket -| `getValue()` | lekéri az aktuális értéket -| `setOmitted()` | [#Érték kihagyása] -| `setDisabled()` | [#Elemek letiltása] - -Megjelenítés: -.[table-form-methods language-php] -| `setCaption($caption)` | megváltoztatja az elem címkéjét -| `setTranslator($translator)` | beállítja a [fordítót |rendering#Fordítás] -| `setHtmlAttribute($name, $value)` | beállítja az elem [HTML attribútumát |rendering#HTML attribútumok] -| `setHtmlId($id)` | beállítja a HTML `id` attribútumot -| `setHtmlType($type)` | beállítja a HTML `type` attribútumot -| `setHtmlName($name)` | beállítja a HTML `name` attribútumot -| `setOption($key, $value)` | [megjelenítési beállítások |rendering#Options] - -Validáció: -.[table-form-methods language-php] -| `setRequired()` | [kötelező elem |validation] -| `addRule()` | beállítja az [érvényesítési szabályt |validation#Szabályok] -| `addCondition()`, `addConditionOn()` | beállítja az [érvényesítési feltételt |validation#Feltételek] -| `addError($message)` | [hibaüzenet átadása |validation#Hibák a feldolgozás során] - -Az `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` elemeknél a következő metódusokat lehet meghívni: - -.[table-form-methods language-php] -| `setNullable()` | beállítja, hogy a getValue() `null`-t adjon-e vissza üres string helyett -| `setEmptyValue($value)` | beállít egy speciális értéket, amely üres stringnek minősül -| `setMaxLength($length)` | beállítja a megengedett karakterek maximális számát -| `addFilter($filter)` | [bemenet módosítása |validation#Bemenet módosítása] - - -Érték kihagyása -=============== - -Ha nem érdekel minket a felhasználó által kitöltött érték, a `setOmitted()` segítségével kihagyhatjuk a `$form->getValues()` metódus eredményéből vagy a handlereknek átadott adatokból. Ez hasznos lehet különböző ellenőrző jelszavaknál, antispam elemeknél stb. - -```php -$form->addPassword('passwordVerify', 'Jelszó újra:') - ->setRequired('Kérjük, adja meg a jelszót újra az ellenőrzéshez') - ->addRule($form::Equal, 'A jelszavak nem egyeznek', $form['password']) - ->setOmitted(); -``` - - -Elemek letiltása -================ - -Az elemeket a `setDisabled()` segítségével lehet letiltani. Egy ilyen elemet a felhasználó nem tud szerkeszteni. - -```php -$form->addText('username', 'Felhasználónév:') - ->setDisabled(); -``` - -A letiltott elemeket a böngésző egyáltalán nem küldi el a szerverre, tehát nem is találja meg őket a `$form->getValues()` függvény által visszaadott adatokban. Ha azonban beállítja a `setOmitted(false)` értéket, a Nette ezekbe az adatokba belefoglalja az alapértelmezett értéküket. - -A `setDisabled()` hívásakor biztonsági okokból **törlődik az elem értéke**. Ha alapértelmezett értéket állít be, azt a letiltás után kell megtenni: - -```php -$form->addText('username', 'Felhasználónév:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -A letiltott elemek alternatívája a `readonly` HTML attribútummal rendelkező elemek, amelyeket a böngésző elküld a szerverre. Bár az elem csak olvasható, **fontos tudatosítani**, hogy az értékét egy támadó továbbra is módosíthatja vagy hamisíthatja. - - -Egyéni elemek -============= - -A beépített űrlap elemek széles skálája mellett egyéni elemeket is hozzáadhat az űrlaphoz a következő módon: - -```php -$form->addComponent(new DateInput('Dátum:'), 'date'); -// alternatív szintaxis: $form['date'] = new DateInput('Dátum:'); -``` - -.[note] -Az űrlap a [Container |component-model:#Container] osztály leszármazottja, az egyes elemek pedig a [Component |component-model:#Component] leszármazottai. - -Létezik egy módszer új űrlap metódusok definiálására, amelyek egyéni elemek hozzáadására szolgálnak (pl. `$form->addZip()`). Ezek az ún. extension methods. Hátrányuk, hogy a szerkesztőkben nem fog működni rájuk a kódkiegészítés. - -```php -use Nette\Forms\Container; - -// hozzáadjuk az addZip(string $name, ?string $label = null) metódust -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Legalább 5 számjegy', '[0-9]{5}'); -}); - -// használat -$form->addZip('zip', 'Irányítószám:'); -``` - - -Alacsony szintű elemek -====================== - -Használhatunk olyan elemeket is, amelyeket csak a sablonban írunk le, és nem adjuk hozzá az űrlaphoz valamelyik `$form->addXyz()` metódussal. Ha például adatbázisból listázunk rekordokat, és előre nem tudjuk, hány lesz belőlük és milyen ID-jük lesz, és minden sornál szeretnénk egy checkboxot vagy radio buttont megjeleníteni, elég azt a sablonban kódolni: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -És elküldés után lekérjük az értéket: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -ahol az első paraméter az elem típusa (`DataFile` a `type=file`-hoz, `DataLine` az egysoros bemenetekhez, mint `text`, `password`, `email` stb., és `DataText` az összes többihez), a második paraméter `sel[]` pedig a HTML `name` attribútumnak felel meg. Az elem típusát kombinálhatjuk a `DataKeys` értékkel, amely megőrzi az elemek kulcsait. Ez különösen hasznos a `select`, `radioList` és `checkboxList` esetén. - -Lényeges, hogy a `getHttpData()` szanitizált értéket ad vissza, ebben az esetben ez mindig érvényes UTF-8 stringek tömbje lesz, függetlenül attól, hogy a támadó mit próbált a szervernek becsempészni. Ez hasonló a közvetlen `$_POST` vagy `$_GET` kezeléséhez, azzal a lényeges különbséggel, hogy mindig tiszta adatokat ad vissza, ahogy azt a standard Nette űrlap elemeknél megszokhattuk. diff --git a/forms/hu/in-presenter.texy b/forms/hu/in-presenter.texy deleted file mode 100644 index 5fe623b39d..0000000000 --- a/forms/hu/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Űrlapok presenterekben -********************** - -.[perex] -A Nette Forms rendkívül megkönnyíti a webes űrlapok létrehozását és feldolgozását. Ebben a fejezetben megismerkedhet az űrlapok használatával a presentereken belül. - -Ha érdekli, hogyan használhatja őket teljesen önállóan a keretrendszer többi része nélkül, akkor az [önálló használat|standalone] útmutatója Önnek szól. - - -Első űrlap -========== - -Próbáljunk meg írni egy egyszerű regisztrációs űrlapot. A kódja a következő lesz: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Név:'); -$form->addPassword('password', 'Jelszó:'); -$form->addSubmit('send', 'Regisztráció'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -és a böngészőben így jelenik meg: - -[* form-cs.webp *] - -Az űrlap a presenterben egy `Nette\Application\UI\Form` osztály objektuma, elődje, a `Nette\Forms\Form` önálló használatra készült. Hozzáadtunk ún. név, jelszó elemeket és egy küldés gombot. Végül a `$form->onSuccess` sor azt mondja, hogy elküldés és sikeres validálás után meg kell hívni a `$this->formSucceeded()` metódust. - -A presenter szempontjából az űrlap egy szokásos komponens. Ezért komponensként kezeljük, és a presenterbe egy [factory metódus |application:components#Factory metódusok] segítségével illesztjük be. Ez így fog kinézni: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Név:'); - $form->addPassword('password', 'Jelszó:'); - $form->addSubmit('send', 'Regisztráció'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // itt dolgozzuk fel az űrlappal küldött adatokat - // $data->name tartalmazza a nevet - // $data->password tartalmazza a jelszót - $this->flashMessage('Sikeresen regisztrált.'); - $this->redirect('Home:'); - } -} -``` - -És a sablonban az űrlapot a `{control}` taggel jelenítjük meg: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Regisztráció</h1> - -{control registrationForm} -``` - -És ez tulajdonképpen minden :-) Van egy működő és tökéletesen [biztonságos |#Védelem a sebezhetőségekkel szemben] űrlapunk. - -És most valószínűleg azt gondolja, hogy ez túl gyors volt, azon tűnődik, hogyan lehetséges, hogy meghívódik a `formSucceeded()` metódus, és mik azok a paraméterek, amelyeket kap. Persze, igaza van, ez magyarázatot érdemel. - -A Nette ugyanis egy friss mechanizmussal érkezik, amelyet [Hollywood style |application:components#Hollywood style]-nak nevezünk. Ahelyett, hogy fejlesztőként állandóan kérdezgetnie kellene, hogy történt-e valami („el lett küldve az űrlap?”, „érvényesen lett elküldve?” és „nem hamisították-e meg?”), azt mondja a keretrendszernek: „amikor az űrlap érvényesen ki van töltve, hívd meg ezt a metódust”, és a további munkát ráhagyja. Ha JavaScriptben programozik, ezt a programozási stílust jól ismeri. Függvényeket ír, amelyek akkor hívódnak meg, amikor egy bizonyos [esemény |nette:glossary#Eventek események] bekövetkezik. És a nyelv átadja nekik a megfelelő argumentumokat. - -Pontosan így épül fel a fenti presenter kód is. A `$form->onSuccess` tömb PHP callbackek listáját képviseli, amelyeket a Nette akkor hív meg, amikor az űrlap elküldésre kerül és helyesen van kitöltve (azaz érvényes). A [presenter életciklusa |application:presenters#Presenter életciklusa] keretében ez egy ún. signal, tehát az `action*` metódus után és a `render*` metódus előtt hívódnak meg. És minden callbacknek átadja első paraméterként magát az űrlapot, második paraméterként pedig az elküldött adatokat egy [ArrayHash |utils:arrays#ArrayHash] objektum formájában. Az első paramétert kihagyhatja, ha nincs szüksége az űrlap objektumra. A második paraméter pedig lehet okosabb, de erről majd [később |#Leképezés osztályokra]. - -A `$data` objektum tartalmazza a `name` és `password` kulcsokat azokkal az adatokkal, amelyeket a felhasználó kitöltött. Általában az adatokat azonnal továbbítjuk további feldolgozásra, ami lehet például adatbázisba való beszúrás. A feldolgozás során azonban hiba léphet fel, például a felhasználónév már foglalt. Ebben az esetben a hibát visszaküldjük az űrlapnak az `addError()` segítségével, és hagyjuk újra megjeleníteni, a hibaüzenettel együtt. - -```php -$form->addError('Sajnáljuk, a felhasználónév már foglalt.'); -``` - -Az `onSuccess` mellett létezik még az `onSubmit`: a callbackek mindig az űrlap elküldése után hívódnak meg, akkor is, ha nincs helyesen kitöltve. Továbbá az `onError`: a callbackek csak akkor hívódnak meg, ha az elküldés nem érvényes. Akkor is meghívódnak, ha az `onSuccess` vagy `onSubmit` során érvénytelenítjük az űrlapot az `addError()` segítségével. - -Az űrlap feldolgozása után átirányítunk a következő oldalra. Ez megakadályozza az űrlap nem kívánt újraküldését a *frissítés*, *vissza* gombbal vagy a böngésző előzményeiben való mozgással. - -Próbáljon meg hozzáadni további [űrlap elemeket|controls] is. - - -Elemekhez való hozzáférés -========================= - -Az űrlap a presenter komponense, esetünkben `registrationForm` néven (a `createComponentRegistrationForm` factory metódus neve alapján), így bárhol a presenterben hozzáférhet az űrlaphoz a következőképpen: - -```php -$form = $this->getComponent('registrationForm'); -// alternatív szintaxis: $form = $this['registrationForm']; -``` - -Az egyes űrlap elemek is komponensek, ezért ugyanúgy hozzáférhet hozzájuk: - -```php -$input = $form->getComponent('name'); // vagy $input = $form['name']; -$button = $form->getComponent('send'); // vagy $button = $form['send']; -``` - -Az elemeket az `unset` segítségével távolíthatja el: - -```php -unset($form['name']); -``` - - -Validálási szabályok -==================== - -Elhangzott a *valid* szó, de az űrlapnak még nincsenek validálási szabályai. Javítsuk ezt ki. - -A név kötelező lesz, ezért megjelöljük a `setRequired()` metódussal, amelynek argumentuma a hibaüzenet szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki a nevet. Ha nem adunk meg argumentumot, az alapértelmezett hibaüzenet kerül felhasználásra. - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, adja meg a nevét'); -``` - -Próbálja meg elküldeni az űrlapot kitöltetlen névvel, és látni fogja, hogy megjelenik a hibaüzenet, és a böngésző vagy a szerver elutasítja, amíg ki nem tölti a mezőt. - -Ugyanakkor a rendszert nem csaphatja be azzal, hogy a mezőbe például csak szóközöket ír. Dehogy. A Nette automatikusan eltávolítja a bal- és jobboldali szóközöket. Próbálja ki. Ez egy olyan dolog, amit minden egysoros inputtal mindig meg kellene tennie, de gyakran elfelejtik. A Nette ezt automatikusan megteszi. (Megpróbálhatja becsapni az űrlapot, és névként többsoros stringet küldeni. A Nette itt sem hagyja magát megtéveszteni, és az újsorokat szóközökre cseréli.) - -Az űrlap mindig a szerveroldalon validálódik, de JavaScript validáció is generálódik, amely villámgyorsan lefut, és a felhasználó azonnal értesül a hibáról, anélkül, hogy az űrlapot el kellene küldenie a szerverre. Ezt a `netteForms.js` szkript végzi. Illessze be a layout sablonba: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ha megnézi az űrlapot tartalmazó oldal forráskódját, észreveheti, hogy a Nette a kötelező elemeket `required` CSS osztállyal rendelkező elemekbe helyezi. Próbálja meg hozzáadni a következő stíluslapot a sablonhoz, és a „Név” címke piros lesz. Elegánsan jelöljük így a felhasználóknak a kötelező elemeket: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -További validálási szabályokat az `addRule()` metódussal adunk hozzá. Az első paraméter a szabály, a második ismét a hibaüzenet szövege, és még következhet a validálási szabály argumentuma. Mit jelent ez? - -Az űrlapot kibővítjük egy új, nem kötelező „életkor” mezővel, amelynek egész számnak kell lennie (`addInteger()`), és ráadásul a megengedett tartományban (`$form::Range`). És itt pontosan kihasználjuk az `addRule()` metódus harmadik paraméterét, amellyel átadjuk a validátornak a kívánt tartományt egy `[tól, ig]` párként: - -```php -$form->addInteger('age', 'Életkor:') - ->addRule($form::Range, 'Az életkornak 18 és 120 között kell lennie', [18, 120]); -``` - -.[tip] -Ha a felhasználó nem tölti ki a mezőt, a validálási szabályok nem kerülnek ellenőrzésre, mivel az elem nem kötelező. - -Itt van lehetőség egy kis refaktorálásra. A hibaüzenetben és a harmadik paraméterben a számok duplikáltan szerepelnek, ami nem ideális. Ha [többnyelvű űrlapokat |rendering#Fordítás] hoznánk létre, és a számokat tartalmazó üzenet több nyelvre lenne lefordítva, megnehezítené az értékek esetleges megváltoztatását. Ebből az okból kifolyólag használhatók a `%d` helyettesítő karakterek, és a Nette kiegészíti az értékeket: - -```php - ->addRule($form::Range, 'Az életkornak %d és %d év között kell lennie', [18, 120]); -``` - -Térjünk vissza a `password` elemhez, amelyet szintén kötelezővé teszünk, és még ellenőrizzük a jelszó minimális hosszát (`$form::MinLength`), ismét a helyettesítő karakter használatával: - -```php -$form->addPassword('password', 'Jelszó:') - ->setRequired('Válasszon jelszót') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -Hozzáadunk az űrlaphoz még egy `passwordVerify` mezőt, ahol a felhasználó még egyszer megadja a jelszót, ellenőrzés céljából. Validálási szabályokkal ellenőrizzük, hogy mindkét jelszó azonos-e (`$form::Equal`). És paraméterként hivatkozást adunk az első jelszóra a [szögletes zárójelek |#Elemekhez való hozzáférés] segítségével: - -```php -$form->addPassword('passwordVerify', 'Jelszó újra:') - ->setRequired('Kérjük, adja meg a jelszót újra az ellenőrzéshez') - ->addRule($form::Equal, 'A jelszavak nem egyeznek', $form['password']) - ->setOmitted(); -``` - -A `setOmitted()` segítségével megjelöltük azt az elemet, amelynek az értékére valójában nem vagyunk kíváncsiak, és amely csak a validáció miatt létezik. Az érték nem kerül átadásra a `$data`-ba. - -Ezzel kész is van egy teljesen működőképes űrlapunk validációval PHP-ban és JavaScriptben is. A Nette validálási képességei sokkal szélesebbek, lehet feltételeket létrehozni, azok alapján megjeleníteni és elrejteni az oldal részeit stb. Mindent megtudhat az [űrlap validációról|validation] szóló fejezetben. - - -Alapértelmezett értékek -======================= - -Az űrlap elemeinek általában beállítunk alapértelmezett értékeket: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Gyakran hasznos az összes elem alapértelmezett értékét egyszerre beállítani. Például, ha az űrlap rekordok szerkesztésére szolgál. Kiolvassuk a rekordot az adatbázisból, és beállítjuk az alapértelmezett értékeket: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Hívja meg a `setDefaults()`-t az elemek definiálása után. - - -Űrlap megjelenítése -=================== - -Alapértelmezés szerint az űrlap táblázatként jelenik meg. Az egyes elemek megfelelnek az alapvető hozzáférhetőségi szabálynak - minden címke `<label>`-ként van megírva és összekapcsolva a megfelelő űrlap elemmel. A címkére kattintva a kurzor automatikusan az űrlap mezőbe kerül. - -Minden elemhez beállíthatunk tetszőleges HTML attribútumokat. Például hozzáadhatunk egy placeholdert: - -```php -$form->addInteger('age', 'Életkor:') - ->setHtmlAttribute('placeholder', 'Kérjük, töltse ki az életkort'); -``` - -Az űrlap megjelenítésének módjai valóban nagyon sokfélék, ezért ennek egy [külön fejezetet szentelünk a megjelenítésről|rendering]. - - -Leképezés osztályokra -===================== - -Térjünk vissza a `formSucceeded()` metódushoz, amely a második paraméterben, a `$data`-ban kapja meg az elküldött adatokat `ArrayHash` objektumként. Mivel ez egy generikus osztály, valami olyasmi, mint a `stdClass`, hiányozni fog belőle bizonyos kényelem a munkavégzés során, mint például a property-k kódkiegészítése a szerkesztőkben vagy a statikus kódelemzés. Ezt meg lehetne oldani azzal, hogy minden űrlaphoz lenne egy konkrét osztályunk, amelynek property-jei az egyes elemeket reprezentálják. Pl.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternatívaként használhatja a konstruktort: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Az adatosztály property-jei lehetnek enumok is, és automatikusan leképeződnek. .{data-version:3.2.4} - -Hogyan mondjuk meg a Nette-nek, hogy az adatokat ennek az osztálynak az objektumaiként adja vissza? Könnyebben, mint gondolná. Elég csak az osztályt megadni a `$data` paraméter típusaként a kezelő metódusban: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data a RegistrationFormData példánya - $name = $data->name; - // ... -} -``` - -Típusként megadható az `array` is, és akkor az adatokat tömbként adja át. - -Hasonló módon használható a `getValues()` függvény is, amelynek az osztály nevét vagy a hidratálandó objektumot paraméterként adjuk át: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Ha az űrlapok többszintű struktúrát alkotnak konténerekből, hozzon létre mindegyikhez külön osztályt: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -A leképezés ezután a `$person` property típusából felismeri, hogy a konténert a `PersonFormData` osztályra kell leképezni. Ha a property konténerek tömbjét tartalmazná, adja meg az `array` típust, és a leképezendő osztályt adja át közvetlenül a konténernek: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Az űrlap adatosztályának tervét legeneráltathatja a `Nette\Forms\Blueprint::dataClass($form)` metódussal, amely kiírja azt a böngésző oldalára. A kódot ezután elég kattintással kijelölni és bemásolni a projektbe. .{data-version:3.1.15} - - -Több gomb -========= - -Ha az űrlapnak több mint egy gombja van, általában meg kell különböztetnünk, hogy melyiket nyomták meg. Minden gombhoz létrehozhatunk saját kezelő függvényt. Ezt beállítjuk a [esemény |nette:glossary#Eventek események] `onClick` kezelőjeként: - -```php -$form->addSubmit('save', 'Mentés') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Törlés') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Ezek a handlerek csak érvényesen kitöltött űrlap esetén hívódnak meg, ugyanúgy, mint az `onSuccess` esemény esetén. A különbség az, hogy első paraméterként az űrlap helyett átadható a küldő gomb, attól függően, hogy milyen típust ad meg: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Amikor az űrlapot az <kbd>Enter</kbd> gombbal küldik el, az úgy tekintendő, mintha az első gombbal küldték volna el. - - -onAnchor esemény -================ - -Amikor a factory metódusban (mint pl. a `createComponentRegistrationForm`) összeállítjuk az űrlapot, az még nem tudja, hogy el lett-e küldve, sem azt, hogy milyen adatokkal. Vannak azonban esetek, amikor szükségünk van az elküldött értékek ismeretére, például ezek alapján alakul az űrlap további formája, vagy szükségünk van rájuk a függő selectboxokhoz stb. - -Az űrlapot összeállító kódrészletet ezért hagyhatjuk meghívni csak abban a pillanatban, amikor az ún. lehorgonyzott, tehát már kapcsolódik a presenterhez és ismeri az elküldött adatait. Ilyen kódot adunk át az `$onAnchor` tömbbe: - -```php -$country = $form->addSelect('country', 'Ország:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Város:'); - -$form->onAnchor[] = function () use ($country, $city) { - // ez a függvény csak akkor hívódik meg, amikor az űrlap már tudja, hogy el lett-e küldve és milyen adatokkal - // tehát használható a getValue() metódus - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Védelem a sebezhetőségekkel szemben -=================================== - -A Nette Framework nagy hangsúlyt fektet a biztonságra, ezért gondosan ügyel az űrlapok jó védelmére. Ezt teljesen átláthatóan teszi, és nem igényel manuális beállítást. - -Amellett, hogy az űrlapokat megvédi a [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] és a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF] támadásoktól, számos apró biztonsági intézkedést tesz, amelyekre Önnek már nem kell gondolnia. - -Például kiszűri a bemenetekből az összes vezérlőkaraktert és ellenőrzi az UTF-8 kódolás érvényességét, így az űrlap adatai mindig tiszták lesznek. A select boxoknál és radio listáknál ellenőrzi, hogy a kiválasztott elemek valóban a kínáltak közül valók voltak-e, és nem történt-e hamisítás. Már említettük, hogy az egysoros szöveges bemeneteknél eltávolítja a sorvégi karaktereket, amelyeket egy támadó küldhetett. A többsoros bemeneteknél pedig normalizálja a sorvégi karaktereket. És így tovább. - -A Nette megoldja Ön helyett azokat a biztonsági kockázatokat, amelyekről sok programozó nem is tudja, hogy léteznek. - -Az említett CSRF támadás lényege, hogy a támadó ráveszi az áldozatot egy olyan oldalra, amely észrevétlenül végrehajt egy kérést az áldozat böngészőjében arra a szerverre, amelyen az áldozat be van jelentkezve, és a szerver azt hiszi, hogy a kérést az áldozat saját akaratából hajtotta végre. Ezért a Nette megakadályozza a POST űrlap elküldését más domainről. Ha valamilyen okból ki szeretné kapcsolni a védelmet, és engedélyezni szeretné az űrlap elküldését más domainről, használja a következőt: - -```php -$form->allowCrossOrigin(); // FIGYELEM! Kikapcsolja a védelmet! -``` - -Ez a védelem a `_nss` nevű SameSite cookie-t használja. A SameSite cookie segítségével történő védelem nem feltétlenül 100%-ban megbízható, ezért célszerű bekapcsolni a token alapú védelmet is: - -```php -$form->addProtection(); -``` - -Javasoljuk, hogy így védje az adminisztrációs felületen lévő űrlapokat, amelyek érzékeny adatokat módosítanak az alkalmazásban. A keretrendszer a CSRF támadás ellen egy engedélyezési token generálásával és ellenőrzésével védekezik, amely a sessionben tárolódik. Ezért szükséges, hogy az űrlap megjelenítése előtt nyitva legyen a session. Az adminisztrációs felületen általában már el van indítva a session a felhasználó bejelentkezése miatt. Ellenkező esetben indítsa el a sessiont a `Nette\Http\Session::start()` metódussal. - - -Ugyanaz az űrlap több presenterben -================================== - -Ha ugyanazt az űrlapot több presenterben is használni szeretné, javasoljuk, hogy hozzon létre hozzá egy factory-t, amelyet aztán átad a presenternek. Egy ilyen osztály megfelelő helye például az `app/Forms` könyvtár. - -A factory osztály például így nézhet ki: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Név:'); - $form->addSubmit('send', 'Bejelentkezés'); - return $form; - } -} -``` - -Az osztályt megkérjük az űrlap legyártására a presenter komponens factory metódusában: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // módosíthatjuk az űrlapot, itt például megváltoztatjuk a gomb címkéjét - $form['send']->setCaption('Folytatás'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // és hozzáadunk egy handlert - return $form; -} -``` - -Az űrlap feldolgozására szolgáló handler is származhat már a factory-ból: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Név:'); - $form->addSubmit('send', 'Bejelentkezés'); - $form->onSuccess[] = function (Form $form, $data): void { - // itt végezzük el az űrlap feldolgozását - }; - return $form; - } -} -``` - -Nos, túl vagyunk a Nette űrlapok gyors bevezetésén. Próbáljon meg még belenézni a disztribúció [examples|https://github.com/nette/forms/tree/master/examples] könyvtárába, ahol további inspirációt találhat. diff --git a/forms/hu/rendering.texy b/forms/hu/rendering.texy deleted file mode 100644 index 2362b0a634..0000000000 --- a/forms/hu/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Űrlapok megjelenítése -********************* - -Az űrlapok megjelenése nagyon változatos lehet. A gyakorlatban két szélsőséggel találkozhatunk. Az egyik oldalon az az igény áll, hogy az alkalmazásban számos olyan űrlapot jelenítsünk meg, amelyek vizuálisan hasonlítanak egymásra, mint két tojás, és értékelnénk az egyszerű megjelenítést sablon nélkül a `$form->render()` segítségével. Ez általában az adminisztrációs felületek esete. - -A másik oldalon pedig ott vannak a változatos űrlapok, amelyekre igaz: minden darab egyedi. Formájukat leginkább HTML nyelven írhatjuk le az űrlap sablonjában. És természetesen a két említett szélsőségen kívül számos olyan űrlappal találkozunk, amelyek valahol a kettő között helyezkednek el. - - -Megjelenítés Latte segítségével -=============================== - -A [Latte sablonrendszer|latte:] alapvetően megkönnyíti az űrlapok és elemeik megjelenítését. Először megmutatjuk, hogyan lehet az űrlapokat manuálisan, elemenként megjeleníteni, és ezzel teljes kontrollt szerezni a kód felett. Később megmutatjuk, hogyan lehet ezt a megjelenítést [automatizálni |#Automatikus megjelenítés]. - -Az űrlap Latte sablonjának tervét legeneráltathatja a `Nette\Forms\Blueprint::latte($form)` metódussal, amely kiírja azt a böngésző oldalára. A kódot ezután elég egy kattintással kijelölni és bemásolni a projektbe. .{data-version:3.1.15} - - -`{control}` ------------ - -Az űrlap megjelenítésének legegyszerűbb módja, ha a sablonba beírjuk: - -```latte -{control signInForm} -``` - -Az így megjelenített űrlap kinézetét a [#Renderer] és az [egyes elemek |#HTML attribútumok] konfigurálásával lehet befolyásolni. - - -`n:name` --------- - -Az űrlap definícióját a PHP kódban rendkívül egyszerűen össze lehet kapcsolni a HTML kóddal. Csak hozzá kell adni a `n:name` attribútumokat. Ennyire egyszerű! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Az eredményül kapott HTML kód formáját teljes mértékben Ön irányítja. Ha az `n:name` attribútumot a `<select>`, `<button>` vagy `<textarea>` elemeknél használja, azok belső tartalma automatikusan kiegészül. A `<form n:name>` tag ezenkívül létrehoz egy lokális `$form` változót a rajzolt űrlap objektumával, és a záró `</form>` megjeleníti az összes meg nem jelenített rejtett elemet (ugyanez érvényes a `{form} ... {/form}`-ra is). - -Nem szabad azonban elfelejtenünk a lehetséges hibaüzenetek megjelenítését. Mindazokat, amelyeket az `addError()` metódussal adtak hozzá az egyes elemekhez (a `{inputError}` segítségével), mindazokat, amelyeket közvetlenül az űrlaphoz adtak hozzá (ezeket a `$form->getOwnErrors()` adja vissza): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -A bonyolultabb űrlap elemeket, mint a RadioList vagy a CheckboxList, így lehet elemenként megjeleníteni: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Nem akar minden elemnél azon gondolkodni, hogy milyen HTML elemet használjon hozzá a sablonban, legyen az `<input>`, `<textarea>` stb? A megoldás az univerzális `{input}` tag: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Ha az űrlap fordítót használ, a `{label}` tageken belüli szöveg lefordításra kerül. - -Ebben az esetben is a bonyolultabb űrlap elemeket, mint a RadioList vagy a CheckboxList, elemenként lehet megjeleníteni: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Magának az `<input>` elemnek a megjelenítéséhez a Checkbox elemben használja a `{input myCheckbox:}`-t. A HTML attribútumokat ebben az esetben mindig vesszővel válassza el `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Kiírja a hibaüzenetet az űrlap elemhez, ha van ilyen. Az üzenetet általában HTML elembe csomagoljuk a stílusozás miatt. Az üres elem megjelenítésének elkerülését, ha nincs üzenet, elegánsan meg lehet oldani az `n:ifcontent` segítségével: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -A hiba jelenlétét a `hasErrors()` metódussal ellenőrizhetjük, és ennek megfelelően beállíthatjuk a szülő elem osztályát: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -A `{form signInForm}...{/form}` tagek alternatívái a `<form n:name="signInForm">...</form>`-nak. - - -Automatikus megjelenítés ------------------------- - -Az `{input}` és `{label}` tageknek köszönhetően könnyen létrehozhatunk egy általános sablont bármilyen űrlaphoz. Fokozatosan iterál és megjeleníti az összes elemét, kivéve a rejtett elemeket, amelyek automatikusan megjelennek az űrlap lezárásakor a `</form>` taggel. A megjelenítendő űrlap nevét a `$form` változóban fogja várni. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -A használt önlezáró páros `{label .../}` tagek a PHP kódban lévő űrlap definícióból származó címkéket jelenítik meg. - -Ezt az általános sablont mentse el például a `basic-form.latte` fájlba, és az űrlap megjelenítéséhez elég csak beilleszteni és átadni az űrlap nevét (vagy példányát) a `$form` paraméterbe: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Ha egy adott űrlap megjelenítésekor bele szeretne szólni a formájába, és például egy elemet másképp szeretne megjeleníteni, akkor a legegyszerűbb út, ha a sablonban előre elkészít blokkokat, amelyeket később felül lehet írni. A blokkoknak lehetnek [dinamikus nevek |latte:template-inheritance#Dinamikus blokknevek] is, így beléjük lehet illeszteni a megjelenítendő elem nevét is. Például: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Egy `username` nevű elemhez így létrejön az `input-username` blokk, amelyet könnyen felül lehet írni a [{embed} |latte:template-inheritance#Egység öröklődés embed] tag használatával: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternatívaként a `basic-form.latte` sablon teljes tartalmát [definiálni |latte:template-inheritance#Definíciók define] lehet blokként, beleértve a `$form` paramétert is: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Ennek köszönhetően kissé egyszerűbb lesz a hívása: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -A blokkot pedig elég egyetlen helyen importálni, a layout sablon elején: - -```latte -{import basic-form.latte} -``` - - -Speciális esetek ----------------- - -Ha csak az űrlap belső részét kell megjeleníteni a `<form>` HTML tagek nélkül, például snippek küldésekor, rejtse el őket az `n:tag-if` attribútummal: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Az elemek megjelenítésében az űrlap konténeren belül segít a `{formContainer}` tag. - -```latte -<p>Melyik híreket szeretné megkapni:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Megjelenítés Latte nélkül -========================= - -Az űrlap megjelenítésének legegyszerűbb módja a következő hívás: - -```php -$form->render(); -``` - -Az így megjelenített űrlap kinézetét a [#Renderer] és az [egyes elemek |#HTML attribútumok] konfigurálásával lehet befolyásolni. - - -Manuális megjelenítés ---------------------- - -Minden űrlap elem rendelkezik metódusokkal, amelyek generálják az űrlap mező és a címke HTML kódját. Ezt visszaadhatják vagy stringként, vagy [Nette\Utils\Html|utils:html-elements] objektumként: - -- `getControl(): Html|string` visszaadja az elem HTML kódját -- `getLabel($caption = null): Html|string|null` visszaadja a címke HTML kódját, ha létezik - -Az űrlapot így elemenként lehet megjeleníteni: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Míg egyes elemeknél a `getControl()` egyetlen HTML elemet ad vissza (pl. `<input>`, `<select>` stb.), másoknál egy egész HTML kódrészletet (CheckboxList, RadioList). Ebben az esetben használhatja azokat a metódusokat, amelyek az egyes inputokat és címkéket generálják, minden elemhez külön: - -- `getControlPart($key = null): ?Html` visszaadja egy elem HTML kódját -- `getLabelPart($key = null): ?Html` visszaadja egy elem címkéjének HTML kódját - -.[note] -Ezeknek a metódusoknak történelmi okokból `get` prefixük van, de jobb lenne a `generate`, mert minden híváskor új `Html` elemet hoznak létre és adnak vissza. - - -Renderer -======== - -Ez egy objektum, amely biztosítja az űrlap megjelenítését. Ezt a `$form->setRenderer` metódussal lehet beállítani. A vezérlés átadódik neki a `$form->render()` metódus hívásakor. - -Ha nem állítunk be saját renderert, az alapértelmezett megjelenítő [api:Nette\Forms\Rendering\DefaultFormRenderer] kerül felhasználásra. Ez az űrlap elemeit HTML táblázat formájában jeleníti meg. A kimenet így néz ki: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Név:</label></th> - - <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> -</tr> - -<tr class="required"> - <th><label class="required" for="frm-age">Életkor:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Nem:</label></th> - ... -``` - -Az, hogy használjunk-e táblázatot az űrlap vázához, vitatható, és sok webdesigner más jelölést részesít előnyben. Például a definíciós listát. Ezért újrakonfiguráljuk a `DefaultFormRenderer`-t úgy, hogy az űrlapot lista formájában jelenítse meg. A konfiguráció a [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers] tömb szerkesztésével történik. Az első index mindig a területet, a második pedig annak attribútumát jelenti. Az egyes területeket az ábra mutatja: - -[* defaultformrenderer.webp *] - -Alapértelmezés szerint az `controls` elemcsoport egy `<table>`-ba van csomagolva, minden `pair` egy táblázat sort (`<tr>`) képvisel, a `label` és `control` páros pedig cellák (`<th>` és `<td>`). Most megváltoztatjuk a csomagoló elemeket. A `controls` területet egy `<dl>` konténerbe helyezzük, a `pair` területet konténer nélkül hagyjuk, a `label`-t `<dt>`-be, végül a `control`-t `<dd>` tagekkel csomagoljuk: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Az eredmény ez a HTML kód: - -```latte -<dl> - <dt><label class="required" for="frm-name">Név:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Életkor:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Nem:</label></dt> - ... -</dl> -``` - -A wrappers tömbben számos további attribútumot lehet befolyásolni: - -- CSS osztályok hozzáadása az egyes űrlap elem típusokhoz -- CSS osztállyal megkülönböztetni a páros és páratlan sorokat -- vizuálisan megkülönböztetni a kötelező és választható elemeket -- meghatározni, hogy a hibaüzenetek közvetlenül az elemeknél vagy az űrlap felett jelenjenek-e meg - - -Options -------- - -A Renderer viselkedését az egyes űrlap elemeken beállított *options* segítségével is lehet irányítani. Így lehet beállítani a leírást, amely a beviteli mező mellett jelenik meg: - -```php -$form->addText('phone', 'Szám:') - ->setOption('description', 'Ez a szám rejtve marad'); -``` - -Ha HTML tartalmat szeretnénk elhelyezni benne, használjuk a [Html |utils:html-elements] osztályt: - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Szám:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">A szám megőrzésének feltételei</a>') - ); -``` - -.[tip] -A Html elemet a címke helyett is lehet használni: `$form->addCheckbox('conditions', $label)`. - - -Elemek csoportosítása ---------------------- - -A Renderer lehetővé teszi az elemek vizuális csoportokba (fieldsetekbe) való csoportosítását: - -```php -$form->addGroup('Személyes adatok'); -``` - -Új csoport létrehozása után ez válik aktívvá, és minden újonnan hozzáadott elem egyúttal hozzáadódik ehhez a csoporthoz is. Tehát az űrlapot így lehet építeni: - -```php -$form = new Form; -$form->addGroup('Személyes adatok'); -$form->addText('name', 'Neved:'); -$form->addInteger('age', 'Korod:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Szállítási cím'); -$form->addCheckbox('send', 'Szállítás címre'); -$form->addText('street', 'Utca:'); -$form->addText('city', 'Város:'); -$form->addSelect('country', 'Ország:', $countries); -``` - -A Renderer először a csoportokat jeleníti meg, és csak utána azokat az elemeket, amelyek egyik csoportba sem tartoznak. - - -Támogatás a Bootstraphez ------------------------- - -[A példákban |https://github.com/nette/forms/tree/master/examples] találhatók minták arra, hogyan konfigurálja a Renderert a [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] és [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] számára. - - -HTML attribútumok -================= - -Tetszőleges HTML attribútumok beállításához az űrlap elemekhez használja a `setHtmlAttribute(string $name, $value = true)` metódust: - -```php -$form->addInteger('number', 'Szám:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Rendezés:', ['ár', 'név']) - ->setHtmlAttribute('onchange', 'submit()'); // változáskor küldés - - -// A <form> elem attribútumainak beállításához -$form->setHtmlAttribute('id', 'myForm'); -``` - -Az elem típusának specifikációja: - -```php -$form->addText('tel', 'Telefonod:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'írja be a telefonszámot'); -``` - -.[warning] -A típus és más attribútumok beállítása csak vizuális célokat szolgál. A bemenetek helyességének ellenőrzése a szerveroldalon kell, hogy történjen, amit a megfelelő [űrlap elem|controls] kiválasztásával és [érvényesítési szabályok|validation] megadásával biztosíthat. - -A rádió- vagy checkbox listák egyes elemeihez beállíthatunk HTML attribútumot különböző értékekkel mindegyikhez. Figyelje meg a kettőspontot a `style:` után, amely biztosítja az érték kiválasztását kulcs szerint: - -```php -$colors = ['r' => 'piros', 'g' => 'zöld', 'b' => 'kék']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Színek:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Kiírja: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">piros</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">zöld</label> -<label><input type="checkbox" name="colors[]" value="b">kék</label> -``` - -Logikai attribútumok, mint a `readonly`, beállításához használhatunk kérdőjeles írásmódot: - -```php -$form->addCheckboxList('colors', 'Színek:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // több kulcshoz használjon tömböt, pl. ['r', 'g'] -``` - -Kiírja: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">piros</label> -<label><input type="checkbox" name="colors[]" value="g">zöld</label> -<label><input type="checkbox" name="colors[]" value="b">kék</label> -``` - -Select boxok esetén a `setHtmlAttribute()` metódus a `<select>` elem attribútumait állítja be. Ha az egyes `<option>` elemek attribútumait szeretnénk beállítani, használjuk a `setOptionAttribute()` metódust. Működnek a fentebb említett kettőspontos és kérdőjeles írásmódok is: - -```php -$form->addSelect('colors', 'Színek:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Kiírja: - -```latte -<select name="colors"> - <option value="r" style="background:red">piros</option> - <option value="g" style="background:green">zöld</option> - <option value="b">kék</option> -</select> -``` - - -Prototípusok ------------- - -A HTML attribútumok beállításának alternatív módja a minta módosítása, amelyből a HTML elem generálódik. A minta egy `Html` objektum, és a `getControlPrototype()` metódus adja vissza: - -```php -$input = $form->addInteger('number', 'Szám:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Ezzel a módszerrel módosítható a címke mintája is, amelyet a `getLabelPrototype()` ad vissza: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -A Checkbox, CheckboxList és RadioList elemeknél befolyásolhatja annak az elemnek a mintáját, amely az egész elemet csomagolja. Ezt a `getContainerPrototype()` adja vissza. Alapértelmezett állapotban ez egy „üres” elem, tehát semmi sem jelenik meg, de azzal, hogy nevet adunk neki, megjelenítésre kerül: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -CheckboxList és RadioList esetén befolyásolható az egyes elemek elválasztójának mintája is, amelyet a `getSeparatorPrototype()` metódus ad vissza. Alapértelmezett állapotban ez a `<br>` elem. Ha páros elemre változtatja, akkor az egyes elemeket csomagolni fogja elválasztás helyett. Továbbá befolyásolható az egyes elemek címkéjének HTML elem mintája is, amelyet a `getItemLabelPrototype()` ad vissza. - - -Fordítás -======== - -Ha többnyelvű alkalmazást programoz, valószínűleg szüksége lesz az űrlap különböző nyelvi változatokban történő megjelenítésére. A Nette Framework ehhez definiál egy fordítási interfészt [api:Nette\Localization\Translator]. A Nette-ben nincs alapértelmezett implementáció, választhat igényei szerint több kész megoldás közül, amelyeket a [Componette |https://componette.org/search/localization] oldalon talál. Dokumentációjukban megtudhatja, hogyan konfigurálja a fordítót. - -Az űrlapok támogatják a szövegek fordítón keresztüli kiírását. Ezt a `setTranslator()` metódussal adjuk át nekik: - -```php -$form->setTranslator($translator); -``` - -Ettől a pillanattól kezdve nemcsak az összes címke, hanem az összes hibaüzenet vagy select box elem is lefordításra kerül egy másik nyelvre. - -Az egyes űrlap elemeknél lehetőség van más fordító beállítására vagy a fordítás teljes kikapcsolására `null` értékkel: - -```php -$form->addSelect('carModel', 'Modell:', $cars) - ->setTranslator(null); -``` - -Az [érvényesítési szabályoknál|validation] a fordítónak specifikus paraméterek is átadásra kerülnek, például a szabálynál: - -```php -$form->addPassword('password', 'Jelszó:') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -a fordító ezekkel a paraméterekkel hívódik meg: - -```php -$translator->translate('A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -és így kiválaszthatja a `karakter` szó helyes többes számú alakját a szám alapján. - - -onRender esemény -================ - -Közvetlenül azelőtt, hogy az űrlap megjelenne, meghívathatjuk a kódunkat. Ez például kiegészítheti az űrlap elemeket HTML osztályokkal a helyes megjelenítés érdekében. A kódot az `onRender` tömbhöz adjuk hozzá: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/hu/standalone.texy b/forms/hu/standalone.texy deleted file mode 100644 index 8d53892ddf..0000000000 --- a/forms/hu/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Önállóan használt űrlapok -************************* - -.[perex] -A Nette Forms nagyságrendekkel megkönnyíti a webes űrlapok létrehozását és feldolgozását. Alkalmazásaiban teljesen önállóan is használhatja őket a keretrendszer többi része nélkül, amit ebben a fejezetben bemutatunk. - -Ha azonban a Nette Applicationt és a presentereket használja, akkor a [használat presenterekben|in-presenter] útmutató Önnek szól. - - -Első űrlap -========== - -Próbáljunk meg írni egy egyszerű regisztrációs űrlapot. A kódja a következő lesz ("teljes kód":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Név:'); -$form->addPassword('password', 'Jelszó:'); -$form->addSubmit('send', 'Regisztráció'); -``` - -Nagyon könnyen megjeleníthetjük: - -```php -$form->render(); -``` - -és a böngészőben így jelenik meg: - -[* form-cs.webp *] - -Az űrlap a `Nette\Forms\Form` osztály objektuma (a `Nette\Application\UI\Form` osztályt a presenterekben használják). Hozzáadtuk az úgynevezett név, jelszó elemeket és egy küldés gombot. - -Most pedig keltsük életre az űrlapot. A `$form->isSuccess()` lekérdezésével megtudjuk, hogy az űrlapot elküldték-e és érvényesen töltötték-e ki. Ha igen, kiírjuk az adatokat. Az űrlapdefiníció után tehát hozzáadjuk: - -```php -if ($form->isSuccess()) { - echo 'Az űrlap helyesen lett kitöltve és elküldve'; - $data = $form->getValues(); - // $data->name tartalmazza a nevet - // $data->password tartalmazza a jelszót - var_dump($data); -} -``` - -A `getValues()` metódus az elküldött adatokat [ArrayHash |utils:arrays#ArrayHash] objektum formájában adja vissza. Hogy ezt hogyan lehet megváltoztatni, azt [később |#Osztályokra való leképezés] mutatjuk be. A `$data` objektum tartalmazza a `name` és `password` kulcsokat a felhasználó által megadott adatokkal. - -Általában az adatokat azonnal további feldolgozásra küldjük, ami lehet például adatbázisba való beszúrás. A feldolgozás során azonban hiba léphet fel, például a felhasználónév már foglalt. Ebben az esetben a hibát az `addError()` segítségével visszaküldjük az űrlapnak, és újra megjelenítjük, a hibaüzenettel együtt. - -```php -$form->addError('Elnézést, ezt a felhasználónevet már használja valaki.'); -``` - -Az űrlap feldolgozása után átirányítunk a következő oldalra. Ez megakadályozza az űrlap nem kívánt újraküldését a *frissítés*, *vissza* gombbal vagy a böngésző előzményeiben való mozgással. - -Az űrlap alapértelmezés szerint POST metódussal és ugyanarra az oldalra küldődik. Mindkettő megváltoztatható: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -És ez tulajdonképpen minden :-) Van egy működő és tökéletesen [biztonságos |#Védelem a sebezhetőségek ellen] űrlapunk. - -Próbáljon meg hozzáadni más [űrlap elemeket|controls] is. - - -Elemekhez való hozzáférés -========================= - -Az űrlapot és annak egyes elemeit komponenseknek nevezzük. Komponensfát alkotnak, ahol a gyökér maga az űrlap. Az űrlap egyes elemeihez a következő módon férhetünk hozzá: - -```php -$input = $form->getComponent('name'); -// alternatív szintaxis: $input = $form['name']; - -$button = $form->getComponent('send'); -// alternatív szintaxis: $button = $form['send']; -``` - -Az elemeket az unset segítségével távolítjuk el: - -```php -unset($form['name']); -``` - - -Validációs szabályok -==================== - -Elhangzott az *érvényes* szó, de az űrlapnak még nincsenek validációs szabályai. Javítsuk ki ezt. - -A név kötelező lesz, ezért a `setRequired()` metódussal jelöljük meg, amelynek argumentuma a hibaüzenet szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki a nevet. Ha nem adunk meg argumentumot, az alapértelmezett hibaüzenet kerül felhasználásra. - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, adja meg a nevét'); -``` - -Próbálja meg elküldeni az űrlapot a név kitöltése nélkül, és látni fogja, hogy hibaüzenet jelenik meg, és a böngésző vagy a szerver addig elutasítja, amíg ki nem tölti a mezőt. - -Ugyanakkor a rendszert nem lehet becsapni azzal, hogy például csak szóközöket ír a mezőbe. Nem. A Nette automatikusan eltávolítja a bal és jobb oldali szóközöket. Próbálja ki. Ezt minden egysoros beviteli mezővel meg kellene tenni, de gyakran elfelejtik. A Nette ezt automatikusan megteszi. (Megpróbálhatja becsapni az űrlapot, és névként többsoros stringet küldeni. A Nette itt sem hagyja magát becsapni, és a sortöréseket szóközökre cseréli.) - -Az űrlap mindig a szerveroldalon validálódik, de JavaScript validáció is generálódik, amely villámgyorsan lefut, és a felhasználó azonnal értesül a hibáról, anélkül, hogy az űrlapot el kellene küldenie a szerverre. Ezt a `netteForms.js` szkript végzi. Illessze be az oldalba: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ha megnézi az űrlapot tartalmazó oldal forráskódját, észreveheti, hogy a Nette a kötelező elemeket `required` CSS osztállyal rendelkező elemekbe helyezi. Próbálja meg hozzáadni a következő stíluslapot a sablonhoz, és a „Név” címke piros lesz. Így elegánsan jelölhetjük meg a felhasználók számára a kötelező elemeket: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -További validációs szabályokat az `addRule()` metódussal adunk hozzá. Az első paraméter a szabály, a második ismét a hibaüzenet szövege, és még következhet a validációs szabály argumentuma. Mit jelent ez? - -Az űrlapot kibővítjük egy új, nem kötelező „kor” mezővel, amelynek egész számnak kell lennie (`addInteger()`), és ezen felül egy megengedett tartományban kell lennie (`$form::Range`). És itt pontosan az `addRule()` metódus harmadik paraméterét használjuk, amellyel átadjuk a validátornak a kívánt tartományt `[tól, ig]` párként: - -```php -$form->addInteger('age', 'Életkor:') - ->addRule($form::Range, 'Az életkornak 18 és 120 között kell lennie', [18, 120]); -``` - -.[tip] -Ha a felhasználó nem tölti ki a mezőt, a validációs szabályok nem kerülnek ellenőrzésre, mivel az elem nem kötelező. - -Itt van lehetőség egy kis refaktorálásra. A hibaüzenetben és a harmadik paraméterben a számok duplikáltan szerepelnek, ami nem ideális. Ha [többnyelvű űrlapokat |rendering#Fordítás] hoznánk létre, és a számokat tartalmazó üzenet több nyelvre lenne lefordítva, megnehezítené az értékek esetleges megváltoztatását. Emiatt lehetséges a `%d` helyettesítő karakterek használata, és a Nette kiegészíti az értékeket: - -```php - ->addRule($form::Range, 'Az életkornak %d és %d év között kell lennie', [18, 120]); -``` - -Térjünk vissza a `password` elemhez, amelyet szintén kötelezővé teszünk, és még ellenőrizzük a jelszó minimális hosszát (`$form::MinLength`), ismét a helyettesítő karakter használatával: - -```php -$form->addPassword('password', 'Jelszó:') - ->setRequired('Válasszon jelszót') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -Adunk hozzá az űrlaphoz még egy `passwordVerify` mezőt, ahol a felhasználó még egyszer megadja a jelszót, ellenőrzés céljából. A validációs szabályok segítségével ellenőrizzük, hogy a két jelszó megegyezik-e (`$form::Equal`). És paraméterként hivatkozást adunk az első jelszóra a [szögletes zárójelek |#Elemekhez való hozzáférés] segítségével: - -```php -$form->addPassword('passwordVerify', 'Jelszó ellenőrzéshez:') - ->setRequired('Kérjük, adja meg a jelszót még egyszer ellenőrzés céljából') - ->addRule($form::Equal, 'A jelszavak nem egyeznek', $form['password']) - ->setOmitted(); -``` - -A `setOmitted()` segítségével megjelöltük azt az elemet, amelynek az értéke valójában nem számít, és amely csak a validáció miatt létezik. Az érték nem kerül átadásra a `$data`-ba. - -Ezzel van egy teljesen működőképes űrlapunk validációval PHP-ban és JavaScriptben is. A Nette validációs képességei sokkal szélesebbek, lehet feltételeket létrehozni, azok alapján megjeleníteni és elrejteni az oldal részeit stb. Mindent megtudhat az [űrlapok validációjáról|validation] szóló fejezetben. - - -Alapértelmezett értékek -======================= - -Az űrlap elemeinek általában alapértelmezett értékeket állítunk be: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Gyakran hasznos az összes elem alapértelmezett értékét egyszerre beállítani. Például, ha az űrlap rekordok szerkesztésére szolgál. Beolvassuk a rekordot az adatbázisból, és beállítjuk az alapértelmezett értékeket: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Hívja a `setDefaults()` metódust az elemek definiálása után. - - -Űrlap megjelenítése -=================== - -Alapértelmezés szerint az űrlap táblázatként jelenik meg. Az egyes elemek megfelelnek az alapvető hozzáférhetőségi szabálynak - minden címke `<label>`-ként van megírva, és a megfelelő űrlap elemhez van kapcsolva. A címkére kattintva a kurzor automatikusan az űrlap mezőjébe kerül. - -Minden elemhez beállíthatunk tetszőleges HTML attribútumokat. Például hozzáadhatunk egy placeholdert: - -```php -$form->addInteger('age', 'Életkor:') - ->setHtmlAttribute('placeholder', 'Kérjük, töltse ki az életkort'); -``` - -Az űrlap megjelenítésének módjai valóban nagyon sokfélék, ezért ennek [külön fejezetet szentelünk a megjelenítésről|rendering]. - - -Osztályokra való leképezés -========================== - -Térjünk vissza az űrlapadatok feldolgozásához. A `getValues()` metódus az elküldött adatokat `ArrayHash` objektumként adta vissza. Mivel ez egy generikus osztály, valami olyasmi, mint a `stdClass`, hiányozni fog belőle bizonyos kényelem a vele való munka során, mint például a propertyk súgása a szerkesztőkben vagy a statikus kódelemzés. Ezt úgy lehetne megoldani, hogy minden űrlaphoz lenne egy konkrét osztályunk, amelynek propertyjei az egyes elemeket reprezentálják. Pl.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternatívaként használhatja a konstruktort: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Az adatosztály propertyjei lehetnek enumok is, és automatikusan leképezésre kerülnek. .{data-version:3.2.4} - -Hogyan mondjuk meg a Nette-nek, hogy az adatokat ennek az osztálynak az objektumaiként adja vissza? Könnyebben, mint gondolná. Csak az osztály nevét vagy a hidratálandó objektumot kell paraméterként megadni: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Paraméterként megadható az `'array'` is, és akkor az adatokat tömbként adja vissza. - -Ha az űrlapok többszintű struktúrát alkotnak konténerekből, hozzon létre mindegyikhez külön osztályt: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -A leképezés ezután a `$person` property típusából tudja, hogy a konténert a `PersonFormData` osztályra kell leképeznie. Ha a property konténerek tömbjét tartalmazná, adja meg az `array` típust, és adja át a leképezendő osztályt közvetlenül a konténernek: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Az űrlap adatosztályának tervét legeneráltathatja a `Nette\Forms\Blueprint::dataClass($form)` metódussal, amely kiírja azt a böngésző oldalára. A kódot ezután elég egy kattintással kijelölni és a projektbe másolni. .{data-version:3.1.15} - - -Több gomb -========= - -Ha az űrlapnak több mint egy gombja van, általában meg kell különböztetnünk, melyiket nyomták meg. Ezt az információt a gomb `isSubmittedBy()` metódusa adja vissza: - -```php -$form->addSubmit('save', 'Mentés'); -$form->addSubmit('delete', 'Törlés'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Ne hagyja ki a `$form->isSuccess()` lekérdezést, ezzel ellenőrzi az adatok érvényességét. - -Amikor az űrlapot az <kbd>Enter</kbd> gombbal küldik el, úgy veszi, mintha az első gombbal küldték volna el. - - -Védelem a sebezhetőségek ellen -============================== - -A Nette Framework nagy hangsúlyt fektet a biztonságra, ezért gondosan ügyel az űrlapok megfelelő védelmére. - -Amellett, hogy az űrlapokat megvédi a [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] és a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF] támadásoktól, számos apró biztonsági intézkedést tesz, amelyekre Önnek már nem kell gondolnia. - -Például kiszűri az összes vezérlőkaraktert a bemenetekből, és ellenőrzi az UTF-8 kódolás érvényességét, így az űrlap adatai mindig tiszták lesznek. A select boxoknál és radio listáknál ellenőrzi, hogy a kiválasztott elemek valóban a felajánlottak közül valók-e, és nem történt-e hamisítás. Már említettük, hogy az egysoros szöveges beviteleknél eltávolítja a sorvégi karaktereket, amelyeket a támadó küldhetett volna. A többsoros beviteleknél pedig normalizálja a sorvégi karaktereket. És így tovább. - -A Nette megoldja Ön helyett azokat a biztonsági kockázatokat, amelyekről sok programozó nem is tudja, hogy léteznek. - -Az említett CSRF támadás lényege, hogy a támadó egy olyan oldalra csalja az áldozatot, amely észrevétlenül végrehajt egy kérést az áldozat böngészőjében ahhoz a szerverhez, amelyen az áldozat be van jelentkezve, és a szerver azt hiszi, hogy a kérést az áldozat saját akaratából hajtotta végre. Ezért a Nette megakadályozza a POST űrlapok küldését más domainről. Ha valamilyen okból ki akarja kapcsolni a védelmet, és engedélyezni szeretné az űrlap küldését más domainről, használja a következőt: - -```php -$form->allowCrossOrigin(); // FIGYELEM! Kikapcsolja a védelmet! -``` - -Ez a védelem a `_nss` nevű SameSite cookie-t használja. Ezért hozza létre az űrlap objektumot még az első kimenet elküldése előtt, hogy a cookie elküldhető legyen. - -A SameSite cookie-val történő védelem nem feltétlenül 100%-ban megbízható, ezért célszerű bekapcsolni a token alapú védelmet is: - -```php -$form->addProtection(); -``` - -Javasoljuk, hogy így védje azokat az űrlapokat a webhely adminisztrációs részében, amelyek érzékeny adatokat módosítanak az alkalmazásban. A keretrendszer a CSRF támadás ellen egy engedélyezési token generálásával és ellenőrzésével védekezik, amelyet a sessionben tárol. Ezért az űrlap megjelenítése előtt nyitott sessionre van szükség. A webhely adminisztrációs részében általában már elindult a session a felhasználó bejelentkezése miatt. Ellenkező esetben indítsa el a sessiont a `Nette\Http\Session::start()` metódussal. - -Nos, ezzel végeztünk a Nette űrlapjainak gyors bemutatásával. Próbáljon meg még belenézni a disztribúció [examples|https://github.com/nette/forms/tree/master/examples] könyvtárába, ahol további inspirációt találhat. diff --git a/forms/hu/validation.texy b/forms/hu/validation.texy deleted file mode 100644 index 78249dc48a..0000000000 --- a/forms/hu/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Űrlap validáció -*************** - - -Kötelező elemek -=============== - -A kötelező elemeket a `setRequired()` metódussal jelöljük meg, amelynek argumentuma a [#hibaüzenetek] hibaüzenet szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki az elemet. Ha nem adunk meg argumentumot, az alapértelmezett hibaüzenet kerül felhasználásra. - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, adja meg a nevét'); -``` - - -Szabályok -========= - -Validációs szabályokat az elemekhez az `addRule()` metódussal adunk hozzá. Az első paraméter a szabály, a második a [#hibaüzenetek] hibaüzenet szövege, a harmadik pedig a validációs szabály argumentuma. - -```php -$form->addPassword('password', 'Jelszó:') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -**A validációs szabályok csak akkor kerülnek ellenőrzésre, ha a felhasználó kitöltötte az elemet.** - -A Nette számos előre definiált szabállyal rendelkezik, amelyek nevei a `Nette\Forms\Form` osztály konstansai. Minden elemhez használhatjuk ezeket a szabályokat: - -| konstans | leírás | argumentum típusa -|------- -| `Required` | kötelező elem, alias a `setRequired()` számára | - -| `Filled` | kötelező elem, alias a `setRequired()` számára | - -| `Blank` | az elem nem lehet kitöltve | - -| `Equal` | az érték megegyezik a paraméterrel | `mixed` -| `NotEqual` | az érték nem egyezik meg a paraméterrel | `mixed` -| `IsIn` | az érték megegyezik a tömb valamelyik elemével | `array` -| `IsNotIn` | az érték nem egyezik meg a tömb egyik elemével sem | `array` -| `Valid` | az elem helyesen van kitöltve? ([#feltételek] számára) | - - - -Szöveges bevitelek ------------------- - -Az `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` elemekhez használhatók a következő szabályok is: - -| `MinLength` | minimális szöveghossz | `int` -| `MaxLength` | maximális szöveghossz | `int` -| `Length` | hossz tartományban vagy pontos hossz | pár `[int, int]` vagy `int` -| `Email` | érvényes e-mail cím | - -| `URL` | abszolút URL | - -| `Pattern` | megfelel a reguláris kifejezésnek | `string` -| `PatternInsensitive` | mint a `Pattern`, de kis- és nagybetű érzéketlen | `string` -| `Integer` | egész szám érték | - -| `Numeric` | alias az `Integer` számára | - -| `Float` | szám | - -| `Min` | numerikus elem minimális értéke | `int\|float` -| `Max` | numerikus elem maximális értéke | `int\|float` -| `Range` | érték tartományban | pár `[int\|float, int\|float]` - -Az `Integer`, `Numeric` és `Float` validációs szabályok azonnal átalakítják az értéket integerre, illetve floatra. Továbbá az `URL` szabály elfogadja a séma nélküli címet is (pl. `nette.org`), és kiegészíti a sémát (`https://nette.org`). A `Pattern` és `PatternIcase` kifejezésnek az egész értékre kell érvényesnek lennie, azaz mintha `^` és `$` karakterekkel lenne körbevéve. - - -Elemek száma ------------- - -Az `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` elemekhez használhatók a következő szabályok is a kiválasztott elemek, illetve feltöltött fájlok számának korlátozására: - -| `MinLength` | minimális szám | `int` -| `MaxLength` | maximális szám | `int` -| `Length` | szám tartományban vagy pontos szám | pár `[int, int]` vagy `int` - - -Fájlfeltöltések ---------------- - -Az `addUpload()`, `addMultiUpload()` elemekhez használhatók a következő szabályok is: - -| `MaxFileSize` | maximális fájlméret bájtban | `int` -| `MimeType` | MIME típus, helyettesítő karakterek engedélyezettek (`'video/*'`) | `string\|string[]` -| `Image` | JPEG, PNG, GIF, WebP, AVIF kép | - -| `Pattern` | a fájlnév megfelel a reguláris kifejezésnek | `string` -| `PatternInsensitive` | mint a `Pattern`, de kis- és nagybetű érzéketlen | `string` - -A `MimeType` és `Image` szabályokhoz szükség van a `fileinfo` PHP kiterjesztésre. Azt, hogy a fájl vagy kép a kívánt típusú-e, az aláírása alapján észlelik, és **nem ellenőrzik az egész fájl integritását.** Azt, hogy a kép nem sérült-e, például a [betöltésével |http:request#toImage] lehet megállapítani. - - -Hibaüzenetek -============ - -Minden előre definiált szabálynak, kivéve a `Pattern` és `PatternInsensitive` szabályokat, van alapértelmezett hibaüzenete, így azt el lehet hagyni. Azonban az összes üzenet testreszabott megadásával és megfogalmazásával felhasználóbarátabbá teheti az űrlapot. - -Az alapértelmezett üzeneteket megváltoztathatja a [konfigurációban|forms:configuration], a `Nette\Forms\Validator::$messages` tömb szövegeinek módosításával, vagy a [fordító |rendering#Fordítás] használatával. - -A hibaüzenetek szövegében a következő helyettesítő stringek használhatók: - -| `%d` | sorban helyettesíti a szabály argumentumaival -| `%n$d` | helyettesíti a szabály n-edik argumentumával -| `%label` | helyettesíti az elem címkéjével (kettőspont nélkül) -| `%name` | helyettesíti az elem nevével (pl. `name`) -| `%value` | helyettesíti a felhasználó által beírt értékkel - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, töltse ki a %label mezőt'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'legalább %d és legfeljebb %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'legfeljebb %2$d és legalább %1$d', [5, 10]); -``` - - -Feltételek -========== - -A szabályokon kívül feltételeket is hozzáadhatunk. Ezeket hasonlóan írjuk, mint a szabályokat, csak az `addRule()` helyett az `addCondition()` metódust használjuk, és természetesen nem adunk meg hibaüzenetet (a feltétel csak kérdez): - -```php -$form->addPassword('password', 'Jelszó:') - // ha a jelszó nem hosszabb 8 karakternél - ->addCondition($form::MaxLength, 8) - // akkor számjegyet kell tartalmaznia - ->addRule($form::Pattern, 'Számjegyet kell tartalmaznia', '.*[0-9].*'); -``` - -A feltételt az aktuális elemen kívül más elemhez is köthetjük az `addConditionOn()` segítségével. Első paraméterként az elemre való hivatkozást adjuk meg. Ebben a példában az e-mail csak akkor lesz kötelező, ha a checkbox be van jelölve (az értéke true lesz): - -```php -$form->addCheckbox('newsletters', 'küldjenek nekem hírleveleket'); - -$form->addEmail('email', 'E-mail:') - // ha a checkbox be van jelölve - ->addConditionOn($form['newsletters'], $form::Equal, true) - // akkor követelje meg az e-mailt - ->setRequired('Adja meg az e-mail címét'); -``` - -A feltételekből komplex struktúrákat hozhatunk létre az `elseCondition()` és `endCondition()` segítségével: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // ha az első feltétel teljesül - ->addConditionOn(/* ... */) // és a második feltétel egy másik elemen - ->addRule(/* ... */) // követelje meg ezt a szabályt - ->elseCondition() // ha a második feltétel nem teljesül - ->addRule(/* ... */) // követelje meg ezeket a szabályokat - ->addRule(/* ... */) - ->endCondition() // visszatérünk az első feltételhez - ->addRule(/* ... */); -``` - -A Nette-ben nagyon könnyen reagálhatunk a feltétel teljesülésére vagy nem teljesülésére JavaScript oldalon is a `toggle()` metódus segítségével, lásd [#dinamikus JavaScript]. - - -Hivatkozás más elemre -===================== - -Szabály vagy feltétel argumentumaként más űrlap elemet is átadhatunk. A szabály ezután a felhasználó által később a böngészőben beírt értéket használja. Így például dinamikusan validálhatjuk, hogy a `password` elem ugyanazt a stringet tartalmazza-e, mint a `password_confirm` elem: - -```php -$form->addPassword('password', 'Jelszó'); -$form->addPassword('password_confirm', 'Jelszó megerősítése') - ->addRule($form::Equal, 'A megadott jelszavak nem egyeznek', $form['password']); -``` - - -Egyéni szabályok és feltételek -============================== - -Néha olyan helyzetbe kerülünk, amikor a Nette beépített validációs szabályai nem elegendőek, és a felhasználótól származó adatokat a saját módunkon kell validálnunk. A Nette-ben ez nagyon egyszerű! - -Az `addRule()` vagy `addCondition()` metódusoknak első paraméterként tetszőleges callbacket adhatunk át. Ez első paraméterként magát az elemet kapja, és boolean értéket ad vissza, amely meghatározza, hogy a validáció rendben lezajlott-e. Az `addRule()` segítségével történő szabály hozzáadásakor további argumentumokat is megadhatunk, ezeket aztán második paraméterként adjuk át. - -Így létrehozhatunk egy saját validátor készletet osztályként statikus metódusokkal: - -```php -class MyValidators -{ - // teszteli, hogy az érték osztható-e az argumentummal - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // további validátorok - } -} -``` - -A használat ezután nagyon egyszerű: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Az értéknek a %d szám többszörösének kell lennie', - 8, - ); -``` - -Egyéni validációs szabályokat JavaScripthez is hozzáadhatunk. A feltétel az, hogy a szabály statikus metódus legyen. A neve a JavaScript validátor számára az osztály nevének a visszaperjelek `\` nélküli, aláhúzásjel `_` és a metódus nevének összekapcsolásával jön létre. Pl. az `App\MyValidators::validateDivisibility`-t `AppMyValidators_validateDivisibility`-ként írjuk, és hozzáadjuk a `Nette.validators` objektumhoz: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -onValidate esemény -================== - -Az űrlap elküldése után validáció történik, amely során ellenőrzésre kerülnek az `addRule()` segítségével hozzáadott egyes szabályok, majd kiváltódik az [esemény |nette:glossary#Eventek események] `onValidate`. Ennek a kezelőjét (handler) kiegészítő validációra használhatjuk, tipikusan az értékek helyes kombinációjának ellenőrzésére több űrlap elemben. - -Ha hibát észlelünk, azt az `addError()` metódussal adjuk át az űrlapnak. Ezt vagy egy konkrét elemen, vagy közvetlenül az űrlapon hívhatjuk meg. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Ez a kombináció nem lehetséges.'); - } -} -``` - - -Hibák a feldolgozás során -========================= - -Sok esetben csak akkor értesülünk a hibáról, amikor az érvényes űrlapot dolgozzuk fel, például új elemet írunk az adatbázisba, és kulcsduplikációba ütközünk. Ebben az esetben a hibát ismét az `addError()` metódussal adjuk át az űrlapnak. Ezt vagy egy konkrét elemen, vagy közvetlenül az űrlapon hívhatjuk meg: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Érvénytelen jelszó.'); - } -} -``` - -Ha lehetséges, javasoljuk, hogy a hibát közvetlenül az űrlap eleméhez csatolja, mert az alapértelmezett renderer használatakor mellette jelenik meg. - -```php -$form['date']->addError('Elnézést, de ez a dátum már foglalt.'); -``` - -Az `addError()` metódust ismételten meghívhatja, és így több hibaüzenetet adhat át az űrlapnak vagy elemnek. Ezeket a `getErrors()` segítségével szerezheti meg. - -Figyelem, a `$form->getErrors()` az összes hibaüzenet összegzését adja vissza, beleértve azokat is, amelyeket közvetlenül az egyes elemekhez adtak át, nem csak közvetlenül az űrlaphoz. A csak az űrlaphoz átadott hibaüzeneteket a `$form->getOwnErrors()` segítségével szerezheti meg. - - -Bemenet módosítása -================== - -Az `addFilter()` metódus segítségével módosíthatjuk a felhasználó által beírt értéket. Ebben a példában toleráljuk és eltávolítjuk a szóközöket az irányítószámban: - -```php -$form->addText('zip', 'Irányítószám:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // eltávolítjuk a szóközöket az irányítószámból - }) - ->addRule($form::Pattern, 'Az irányítószám nem öt számjegyű', '\d{5}'); -``` - -A szűrő beépül a validációs szabályok és feltételek közé, tehát a metódusok sorrendje számít, azaz a szűrő és a szabály abban a sorrendben hívódik meg, ahogy az `addFilter()` és `addRule()` metódusok sorrendje van. - - -JavaScript validáció -==================== - -A feltételek és szabályok megfogalmazásának nyelve nagyon erős. Minden konstrukció működik mind a szerveroldalon, mind a JavaScript oldalon. HTML attribútumokban `data-nette-rules` JSON formátumban kerülnek átadásra. Magát a validációt pedig egy szkript végzi, amely elfogja az űrlap `submit` eseményét, végigmegy az egyes elemeken, és végrehajtja a megfelelő validációt. - -Ez a szkript a `netteForms.js`, és több lehetséges forrásból érhető el: - -A szkriptet közvetlenül beillesztheti a HTML oldalba egy CDN-ről: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Vagy másolja helyileg a projekt nyilvános mappájába (pl. a `vendor/nette/forms/src/assets/netteForms.min.js` fájlból): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Vagy telepítse [npm|https://www.npmjs.com/package/nette-forms] segítségével: - -```shell -npm install nette-forms -``` - -Majd töltse be és futtassa: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternatívaként betöltheti közvetlenül a `vendor` mappából: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Dinamikus JavaScript -==================== - -Szeretné megjeleníteni a cím megadására szolgáló mezőket csak akkor, ha a felhasználó postai kézbesítést választ? Semmi probléma. A kulcs az `addCondition()` & `toggle()` metóduspár: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Ez a kód azt mondja, hogy amikor a feltétel teljesül, tehát amikor a checkbox be van jelölve, a `#address-container` HTML elem látható lesz. És fordítva. A címzett címét tartalmazó űrlap elemeket tehát egy ilyen ID-jű konténerbe helyezzük, és a checkboxra kattintva elrejtődnek vagy megjelennek. Ezt a `netteForms.js` szkript biztosítja. - -A `toggle()` metódus argumentumaként tetszőleges selectort adhatunk át. Történelmi okokból az alfanumerikus string további speciális karakterek nélkül elem ID-ként értelmeződik, tehát ugyanúgy, mintha `#` karakter előzné meg. A második, nem kötelező paraméter lehetővé teszi a viselkedés megfordítását, azaz ha a `toggle('#address-container', false)`-t használnánk, az elem éppen ellenkezőleg, csak akkor jelenne meg, ha a checkbox nem lenne bejelölve. - -Az alapértelmezett implementáció JavaScriptben az elemek `hidden` propertyjét változtatja meg. A viselkedést azonban könnyen megváltoztathatjuk, például animációt adhatunk hozzá. Elég JavaScriptben felülírni a `Nette.toggle` metódust saját megoldással: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // elrejtjük vagy megjelenítjük az 'el'-t a 'visible' értékétől függően - }); -}; -``` - - -Validáció kikapcsolása -====================== - -Néha hasznos lehet a validáció kikapcsolása. Ha a küldés gomb megnyomása nem kell, hogy validációt végezzen (alkalmas *Cancel* vagy *Preview* gombokhoz), kikapcsoljuk a `$submit->setValidationScope([])` metódussal. Ha csak részleges validációt kell végeznie, megadhatjuk, mely mezők vagy űrlap konténerek validálódjanak. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Az egész űrlapot validálja -$form->addSubmit('send2') - ->setValidationScope([]); // Egyáltalán nem validál -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Csak a name elemet validálja -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Csak az age elemet validálja -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // A details konténert validálja -``` - -A `setValidationScope` nem befolyásolja a [#onValidate esemény] eseményt az űrlapon, amely mindig meghívásra kerül. A konténer `onValidate` eseménye csak akkor kerül kiváltásra, ha ez a konténer részleges validációra van megjelölve. diff --git a/forms/it/@home.texy b/forms/it/@home.texy index d6b50b2441..18219336f3 100644 --- a/forms/it/@home.texy +++ b/forms/it/@home.texy @@ -3,18 +3,18 @@ Nette Forms <div class=perex> -Nette Forms ha rivoluzionato la creazione di form web. Improvvisamente, bastava scrivere poche righe di codice comprensibili per avere un form completo, inclusa la resa, la validazione JavaScript e lato server, e inoltre estremamente sicuro. Vediamo come: +Nette Forms ha rivoluzionato la creazione dei form web. All'improvviso bastava scrivere poche righe di codice chiaro per ottenere un form completo, comprensivo di rendering, validazione JavaScript e lato server, oltre a una sicurezza di prim'ordine. Vi mostreremo come: -- creare form user-friendly +- creare form facili da usare - validare i dati inviati -- rendere gli elementi esattamente secondo necessità +- disegnare gli elementi esattamente come serve </div> -Utilizzando Nette Forms, eviterai una serie di compiti ripetitivi, come scrivere la validazione (inoltre doppia, lato server e client), minimizzerai la probabilità di errori e falle di sicurezza. +Con Nette Forms potete evitare molti compiti di routine, come scrivere la logica di validazione (sia lato server sia lato client), e ridurre al minimo la probabilità di errori e di vulnerabilità di sicurezza. -Puoi utilizzare i form sia come parte dell'applicazione Nette (cioè nei presenter), sia completamente autonomamente. Poiché nei due casi l'uso differisce leggermente, abbiamo preparato per te due guide: +Potete usare i form come parte di una Nette Application (cioè nei presenter) oppure in modo completamente autonomo. Poiché l'uso differisce leggermente nei due casi, abbiamo preparato per voi guide separate: <div class="wiki-buttons"> <div> "Form nei presenter .[wiki-button]":in-presenter </div> @@ -25,7 +25,7 @@ Puoi utilizzare i form sia come parte dell'applicazione Nette (cioè nei present Installazione ------------- -È possibile scaricare e installare la libreria utilizzando lo strumento [Composer|best-practices:composer]: +Scaricate e installate il pacchetto con [Composer|best-practices:composer]: ```shell composer require nette/forms diff --git a/forms/it/@left-menu.texy b/forms/it/@left-menu.texy index fc13168966..aa58108f9b 100644 --- a/forms/it/@left-menu.texy +++ b/forms/it/@left-menu.texy @@ -1,14 +1,16 @@ Nette Forms *********** -- [Introduzione |@home] +- [Panoramica |@home] - [Form nei presenter|in-presenter] - [Form autonomi|standalone] -- [Elementi del form |controls] +- [Controlli del form |controls] - [Validazione |validation] - [Rendering |rendering] -- [Configurazione |configuration] +- [Controlli personalizzati |custom-controls] +- [Configurazione|configuration] +- [Aggiornamento|upgrading] -Ulteriori letture -***************** -- [Guide e procedure |best-practices:] +Letture consigliate +******************* +- [Best practice |best-practices:] diff --git a/forms/it/configuration.texy b/forms/it/configuration.texy index 8c67992399..9c4db1b24f 100644 --- a/forms/it/configuration.texy +++ b/forms/it/configuration.texy @@ -2,7 +2,7 @@ Configurazione dei form *********************** .[perex] -Nella configurazione è possibile modificare i [messaggi di errore predefiniti dei form |validation]. +Nella configurazione potete cambiare i [messaggi di errore predefiniti dei form|validation]. ```neon forms: @@ -17,6 +17,7 @@ forms: 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.' @@ -35,27 +36,28 @@ Ecco la traduzione italiana: ```neon forms: messages: - Equal: 'Inserisci %s.' + Equal: 'Inserite %s.' NotEqual: 'Questo valore non dovrebbe essere %s.' Filled: 'Questo campo è obbligatorio.' Blank: 'Questo campo dovrebbe essere vuoto.' - MinLength: 'Inserisci almeno %d caratteri.' - MaxLength: 'Inserisci non più di %d caratteri.' - Length: 'Inserisci un valore lungo tra %d e %d caratteri.' - Email: 'Inserisci un indirizzo email valido.' - URL: 'Inserisci un URL valido.' - Integer: 'Inserisci un numero intero valido.' - Float: 'Inserisci un numero valido.' - Min: 'Inserisci un valore maggiore o uguale a %d.' - Max: 'Inserisci un valore minore o uguale a %d.' - Range: 'Inserisci un valore compreso tra %d e %d.' + MinLength: 'Inserite almeno %d caratteri.' + MaxLength: 'Inserite al massimo %d caratteri.' + Length: 'Inserite un valore lungo da %d a %d caratteri.' + Email: 'Inserite un indirizzo e-mail valido.' + URL: 'Inserite un URL valido.' + Integer: 'Inserite un numero intero valido.' + Numeric: 'Inserite un numero intero non negativo.' + Float: 'Inserite un numero valido.' + Min: 'Inserite un valore maggiore o uguale a %d.' + Max: 'Inserite un valore minore o uguale a %d.' + Range: 'Inserite un valore compreso tra %d e %d.' MaxFileSize: 'La dimensione del file caricato può essere al massimo di %d byte.' MaxPostSize: 'I dati caricati superano il limite di %d byte.' - MimeType: 'Il file caricato non è nel formato previsto.' - Image: 'Il file caricato deve essere un\'immagine in formato JPEG, GIF, PNG, WebP o AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Seleziona un\'opzione valida.' + MimeType: 'Il file caricato non è nel formato atteso.' + Image: 'Il file caricato deve essere un''immagine in formato JPEG, GIF, PNG o WebP.' + Nette\Forms\Controls\SelectBox::Valid: 'Selezionate un''opzione valida.' Nette\Forms\Controls\UploadControl::Valid: 'Si è verificato un errore durante il caricamento del file.' - Nette\Forms\Controls\CsrfProtection::Protection: 'La tua sessione è scaduta. Torna alla home page e riprova.' + Nette\Forms\Controls\CsrfProtection::Protection: 'La vostra sessione è scaduta. Tornate alla home page e riprovate.' ``` -Se non utilizzi l'intero framework e quindi nemmeno i file di configurazione, puoi modificare i messaggi di errore predefiniti direttamente nell'array `Nette\Forms\Validator::$messages`. +Se non usate l'intero framework e quindi nemmeno i file di configurazione, potete cambiare i messaggi di errore predefiniti direttamente nell'array `Nette\Forms\Validator::$messages`. diff --git a/forms/it/controls.texy b/forms/it/controls.texy index 2e80b5e1f6..6089a6041b 100644 --- a/forms/it/controls.texy +++ b/forms/it/controls.texy @@ -1,14 +1,14 @@ -Elementi del Form -***************** +Controlli dei form +****************** .[perex] -Panoramica degli elementi standard del form. +Panoramica dei controlli standard dei form. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -Aggiunge un campo di testo a riga singola (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se l'utente non compila il campo, restituisce una stringa vuota `''`, oppure tramite `setNullable()` è possibile specificare che restituisca `null`. +Aggiunge un campo di testo a riga singola (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se l'utente non compila il campo, restituisce una stringa vuota `''`; usate `setNullable()` per fargli restituire invece `null`. ```php $form->addText('name', 'Nome:') @@ -16,56 +16,56 @@ $form->addText('name', 'Nome:') ->setNullable(); ``` -Valida automaticamente UTF-8, rimuove gli spazi iniziali e finali e rimuove gli a capo che un utente malintenzionato potrebbe inviare. +Valida automaticamente l'UTF-8, elimina gli spazi iniziali e finali e rimuove gli a capo che un aggressore potrebbe inviare. -La lunghezza massima può essere limitata tramite `setMaxLength()`. Modificare il valore inserito dall'utente è possibile tramite [addFilter() |validation#Modifica dell Input]. +La lunghezza massima si può limitare con `setMaxLength()`. Il metodo [addFilter() |validation#Modificare i valori inseriti] permette di modificare il valore inserito dall'utente. -Tramite `setHtmlType()` è possibile modificare l'aspetto visivo del campo di testo in tipi come `search`, `tel` o `url`, vedi [specifiche |https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Ricorda che la modifica del tipo è solo visiva e non sostituisce la funzione di validazione. Per il tipo `url` è opportuno aggiungere una specifica [regola URL |validation#Input di Testo]. +Con `setHtmlType()` potete cambiare l'aspetto visivo del campo di testo in tipi come `search`, `tel` o `url`, definiti nella [specifica|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Ricordate che cambiare il tipo è puramente visivo e non sostituisce la funzione di validazione. Per il tipo `url` conviene aggiungere un'apposita [regola di validazione dell'URL |validation#Campi di testo]. .[note] -Per altri tipi di input, come `number`, `range`, `email`, `date`, `datetime-local`, `time` e `color`, utilizzare metodi specializzati come [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] e [#addColor], che garantiscono la validazione lato server. I tipi `month` e `week` non sono ancora pienamente supportati in tutti i browser. +Per gli altri tipi di input, come `number`, `range`, `email`, `date`, `datetime-local`, `time` e `color`, usate i metodi specializzati come [#addInteger()], [#addFloat()], [#addEmail()], [#addDate()], [#addTime()], [#addDateTime()] e [#addColor()], che offrono la validazione lato server. I tipi `month` e `week` non sono ancora pienamente supportati da tutti i browser. -All'elemento può essere impostato il cosiddetto empty-value, che è qualcosa come un valore predefinito, ma se l'utente non lo modifica, l'elemento restituisce una stringa vuota o `null`. +Al controllo si può impostare un "valore vuoto". Funziona un po' come un valore predefinito, ma se l'utente non lo cambia, il controllo restituisce una stringa vuota oppure `null`. ```php $form->addText('phone', 'Telefono:') ->setHtmlType('tel') - ->setEmptyValue('+39'); + ->setEmptyValue('+420'); ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -Aggiunge un campo per l'inserimento di testo multilinea (classe [TextArea |api:Nette\Forms\Controls\TextArea]). Se l'utente non compila il campo, restituisce una stringa vuota `''`, oppure tramite `setNullable()` è possibile specificare che restituisca `null`. +Aggiunge un campo di testo su più righe (classe [TextArea |api:Nette\Forms\Controls\TextArea]). Se l'utente non compila il campo, restituisce una stringa vuota `''`; usate `setNullable()` per fargli restituire invece `null`. ```php $form->addTextArea('note', 'Nota:') - ->addRule($form::MaxLength, 'La nota è troppo lunga', 10000); + ->addRule($form::MaxLength, 'La vostra nota è troppo lunga', 10000); ``` -Valida automaticamente UTF-8 e normalizza i separatori di riga in `\n`. A differenza del campo di input a riga singola, non viene eseguita alcuna rimozione degli spazi. +Valida automaticamente l'UTF-8 e normalizza i fine riga in `\n`. A differenza del campo a riga singola, non avviene alcuna eliminazione degli spazi. -La lunghezza massima può essere limitata tramite `setMaxLength()`. Modificare il valore inserito dall'utente è possibile tramite [addFilter() |validation#Modifica dell Input]. È possibile impostare il cosiddetto empty-value tramite `setEmptyValue()`. +La lunghezza massima si può limitare con `setMaxLength()`. Il metodo [addFilter() |validation#Modificare i valori inseriti] permette di modificare il valore inserito dall'utente. Un valore vuoto si può impostare con `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Aggiunge un campo per l'inserimento di un numero intero (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Restituisce un intero o `null` se l'utente non inserisce nulla. +Aggiunge un campo per inserire un numero intero (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Restituisce un intero oppure `null` se l'utente non inserisce nulla. ```php $form->addInteger('year', 'Anno:') ->addRule($form::Range, 'L\'anno deve essere compreso tra %d e %d.', [1900, 2023]); ``` -L'elemento viene renderizzato come `<input type="number">`. Utilizzando il metodo `setHtmlType()` è possibile cambiare il tipo in `range` per la visualizzazione come slider, o in `text` se si preferisce un campo di testo standard senza il comportamento speciale del tipo `number`. +Il controllo viene disegnato come `<input type="number">`. Con il metodo `setHtmlType()` potete cambiare il tipo in `range`, per mostrarlo come cursore, oppure in `text`, se preferite un normale campo di testo senza il comportamento particolare del tipo `number`. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -Aggiunge un campo per l'inserimento di un numero decimale (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Restituisce un float o `null` se l'utente non inserisce nulla. +Aggiunge un campo per inserire un numero in virgola mobile (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Restituisce un float oppure `null` se l'utente non inserisce nulla. ```php $form->addFloat('level', 'Livello:') @@ -73,55 +73,55 @@ $form->addFloat('level', 'Livello:') ->addRule($form::Range, 'Il livello deve essere compreso tra %d e %d.', [0, 100]); ``` -L'elemento viene renderizzato come `<input type="number">`. Utilizzando il metodo `setHtmlType()` è possibile cambiare il tipo in `range` per la visualizzazione come slider, o in `text` se si preferisce un campo di testo standard senza il comportamento speciale del tipo `number`. +Il controllo viene disegnato come `<input type="number">`. Con il metodo `setHtmlType()` potete cambiare il tipo in `range`, per mostrarlo come cursore, oppure in `text`, se preferite un normale campo di testo senza il comportamento particolare del tipo `number`. -Nette e il browser Chrome accettano sia la virgola che il punto come separatore decimale. Affinché questa funzionalità sia disponibile anche in Firefox, si consiglia di impostare l'attributo `lang` per l'elemento specifico o per l'intera pagina, ad esempio `<html lang="it">`. +Nette e il browser Chrome accettano come separatore decimale sia la virgola sia il punto. Per abilitare questa funzionalità anche in Firefox, conviene impostare l'attributo `lang`, sul singolo controllo oppure sull'intera pagina, per esempio `<html lang="en">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -Aggiunge un campo per l'inserimento di un indirizzo email (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se l'utente non compila il campo, restituisce una stringa vuota `''`, oppure tramite `setNullable()` è possibile specificare che restituisca `null`. +Aggiunge un campo per inserire un indirizzo e-mail (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se l'utente non compila il campo, restituisce una stringa vuota `''`; usate `setNullable()` per fargli restituire invece `null`. ```php $form->addEmail('email', 'E-mail:'); ``` -Verifica se il valore è un indirizzo email valido. Non verifica se il dominio esiste realmente, verifica solo la sintassi. Valida automaticamente UTF-8, rimuove gli spazi iniziali e finali. +Valida che il valore sia un indirizzo e-mail valido. Non controlla se il dominio esista davvero, verifica solo la sintassi. Valida automaticamente l'UTF-8 ed elimina gli spazi iniziali e finali. -La lunghezza massima può essere limitata tramite `setMaxLength()`. Modificare il valore inserito dall'utente è possibile tramite [addFilter() |validation#Modifica dell Input]. È possibile impostare il cosiddetto empty-value tramite `setEmptyValue()`. +La lunghezza massima si può limitare con `setMaxLength()`. Il metodo [addFilter() |validation#Modificare i valori inseriti] permette di modificare il valore inserito dall'utente. Un valore vuoto si può impostare con `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== -Aggiunge un campo per l'inserimento della password (classe [TextInput |api:Nette\Forms\Controls\TextInput]). +Aggiunge un campo per la password (classe [TextInput |api:Nette\Forms\Controls\TextInput]). ```php $form->addPassword('password', 'Password:') ->setRequired() - ->addRule($form::MinLength, 'La password deve avere almeno %d caratteri', 8) - ->addRule($form::Pattern, 'Deve contenere un numero', '.*[0-9].*'); + ->addRule($form::MinLength, 'La password deve essere lunga almeno %d caratteri', 8) + ->addRule($form::Pattern, 'La password deve contenere un numero', '.*[0-9].*'); ``` -Alla successiva visualizzazione del form, il campo sarà vuoto. Valida automaticamente UTF-8, rimuove gli spazi iniziali e finali e rimuove gli a capo che un utente malintenzionato potrebbe inviare. +Quando il form viene mostrato di nuovo, il campo sarà vuoto. Valida automaticamente l'UTF-8, elimina gli spazi iniziali e finali e rimuove gli a capo che un aggressore potrebbe inviare. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -Aggiunge una casella di controllo (classe [Checkbox |api:Nette\Forms\Controls\Checkbox]). Restituisce il valore `true` o `false`, a seconda che sia selezionata o meno. +Aggiunge una checkbox (classe [Checkbox |api:Nette\Forms\Controls\Checkbox]). Restituisce `true` o `false`, a seconda che sia selezionata. ```php -$form->addCheckbox('agree', 'Accetto i termini e le condizioni') - ->setRequired('È necessario accettare i termini e le condizioni'); +$form->addCheckbox('agree', 'Accetto le condizioni') + ->setRequired('Dovete accettare le nostre condizioni'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Aggiunge caselle di controllo per la selezione di più elementi (classe [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Restituisce un array delle chiavi degli elementi selezionati. Il metodo `getSelectedItems()` restituisce i valori invece delle chiavi. +Aggiunge un elenco di checkbox per selezionare più elementi (classe [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Restituisce un array delle chiavi degli elementi selezionati. Il metodo `getSelectedItems()` restituisce gli elementi selezionati come coppie chiave-valore. ```php $form->addCheckboxList('colors', 'Colori:', [ @@ -131,66 +131,67 @@ $form->addCheckboxList('colors', 'Colori:', [ ]); ``` -L'array degli elementi offerti viene passato come terzo parametro o tramite il metodo `setItems()`. +Passate l'array degli elementi offerti come terzo parametro oppure con il metodo `setItems()`. Passando `false` come secondo argomento di `setItems()`, i valori vengono usati anche come chiavi. -Tramite `setDisabled(['r', 'g'])` è possibile disattivare singoli elementi. +Usate `setDisabled(['r', 'g'])` per disattivare singoli elementi. -L'elemento controlla automaticamente che non ci sia stato un tentativo di manomissione e che gli elementi selezionati siano effettivamente tra quelli offerti e non siano stati disattivati. Tramite il metodo `getRawValue()` è possibile ottenere gli elementi inviati senza questo importante controllo. +Il controllo verifica automaticamente che non ci siano state falsificazioni e che gli elementi selezionati fossero davvero tra quelli offerti e non disattivati. Con il metodo `getRawValue()` si possono ottenere gli elementi inviati senza questo importante controllo. -Durante l'impostazione degli elementi selezionati predefiniti, controlla anche che siano tra quelli offerti, altrimenti genera un'eccezione. Questo controllo può essere disattivato tramite `checkDefaultValue(false)`. +Impostando gli elementi selezionati per impostazione predefinita, controlla anche che siano tra quelli offerti, altrimenti solleva un'eccezione. Questo controllo si può disattivare con `checkDefaultValue(false)`. -Se invii il form con il metodo `GET`, puoi scegliere un modo più compatto per trasferire i dati, che risparmia la dimensione della query string. Si attiva impostando l'attributo HTML del form: +Se inviate il form con il metodo `GET`, potete scegliere un modo di trasferimento dei dati più compatto, che riduce la dimensione della query string. Lo attivate impostando un attributo HTML sul form: ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ -Aggiunge pulsanti di opzione (classe [RadioList |api:Nette\Forms\Controls\RadioList]). Restituisce la chiave dell'elemento selezionato, o `null` se l'utente non ha selezionato nulla. Il metodo `getSelectedItem()` restituisce il valore invece della chiave. +Aggiunge dei radio button (classe [RadioList |api:Nette\Forms\Controls\RadioList]). Restituisce la chiave dell'elemento selezionato, oppure `null` se l'utente non ha selezionato nulla. Il metodo `getSelectedItem()` restituisce il valore invece della chiave. ```php $sex = [ - 'm' => 'uomo', - 'f' => 'donna', + 'm' => 'maschio', + 'f' => 'femmina', + 'o' => 'altro', ]; $form->addRadioList('gender', 'Sesso:', $sex); ``` -L'array degli elementi offerti viene passato come terzo parametro o tramite il metodo `setItems()`. +Passate l'array degli elementi offerti come terzo parametro oppure con il metodo `setItems()`. -Tramite `setDisabled(['m', 'f'])` è possibile disattivare singoli elementi. +Usate `setDisabled(['m'])` per disattivare singoli elementi. -L'elemento controlla automaticamente che non ci sia stato un tentativo di manomissione e che l'elemento selezionato sia effettivamente uno di quelli offerti e non sia stato disattivato. Tramite il metodo `getRawValue()` è possibile ottenere l'elemento inviato senza questo importante controllo. +Il controllo verifica automaticamente che non ci siano state falsificazioni e che l'elemento selezionato fosse davvero tra quelli offerti e non disattivato. Con il metodo `getRawValue()` si può ottenere l'elemento inviato senza questo importante controllo. -Durante l'impostazione dell'elemento selezionato predefinito, controlla anche che sia uno di quelli offerti, altrimenti genera un'eccezione. Questo controllo può essere disattivato tramite `checkDefaultValue(false)`. +Impostando l'elemento selezionato per impostazione predefinita, controlla anche che sia tra quelli offerti, altrimenti solleva un'eccezione. Questo controllo si può disattivare con `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -Aggiunge un select box (classe [SelectBox |api:Nette\Forms\Controls\SelectBox]). Restituisce la chiave dell'elemento selezionato, o `null` se l'utente non ha selezionato nulla. Il metodo `getSelectedItem()` restituisce il valore invece della chiave. +Aggiunge un select box (classe [SelectBox |api:Nette\Forms\Controls\SelectBox]). Restituisce la chiave dell'elemento selezionato, oppure `null` se l'utente non ha selezionato nulla. Il metodo `getSelectedItem()` restituisce il valore invece della chiave. ```php $countries = [ - 'IT' => 'Italia', - 'DE' => 'Germania', + 'CZ' => 'Repubblica Ceca', + 'SK' => 'Slovacchia', 'GB' => 'Regno Unito', ]; $form->addSelect('country', 'Paese:', $countries) - ->setDefaultValue('IT'); + ->setDefaultValue('SK'); ``` -L'array degli elementi offerti viene passato come terzo parametro o tramite il metodo `setItems()`. Gli elementi possono essere anche un array bidimensionale: +Passate l'array degli elementi offerti come terzo parametro oppure con il metodo `setItems()`. Gli elementi possono essere anche un array bidimensionale (che rappresenta gli optgroup): ```php $countries = [ 'Europa' => [ - 'IT' => 'Italia', - 'DE' => 'Germania', + 'CZ' => 'Repubblica Ceca', + 'SK' => 'Slovacchia', 'GB' => 'Regno Unito', ], 'CA' => 'Canada', @@ -199,42 +200,42 @@ $countries = [ ]; ``` -Nei select box, spesso il primo elemento ha un significato speciale, serve come invito all'azione. Per aggiungere un tale elemento serve il metodo `setPrompt()`. +Nei select box il primo elemento ha spesso un significato particolare, perché invita all'azione. Usate il metodo `setPrompt()` per aggiungere un elemento del genere. ```php $form->addSelect('country', 'Paese:', $countries) - ->setPrompt('Scegli un paese'); + ->setPrompt('Scegliete un paese'); ``` -Tramite `setDisabled(['IT', 'DE'])` è possibile disattivare singoli elementi. +Usate `setDisabled(['CZ', 'SK'])` per disattivare singoli elementi. -L'elemento controlla automaticamente che non ci sia stato un tentativo di manomissione e che l'elemento selezionato sia effettivamente uno di quelli offerti e non sia stato disattivato. Tramite il metodo `getRawValue()` è possibile ottenere l'elemento inviato senza questo importante controllo. +Il controllo verifica automaticamente che non ci siano state falsificazioni e che l'elemento selezionato fosse davvero tra quelli offerti e non disattivato. Con il metodo `getRawValue()` si può ottenere l'elemento inviato senza questo importante controllo. -Durante l'impostazione dell'elemento selezionato predefinito, controlla anche che sia uno di quelli offerti, altrimenti genera un'eccezione. Questo controllo può essere disattivato tramite `checkDefaultValue(false)`. +Impostando l'elemento selezionato per impostazione predefinita, controlla anche che sia tra quelli offerti, altrimenti solleva un'eccezione. Questo controllo si può disattivare con `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Aggiunge un select box per la selezione di più elementi (classe [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Restituisce un array delle chiavi degli elementi selezionati. Il metodo `getSelectedItems()` restituisce i valori invece delle chiavi. +Aggiunge un select box per selezionare più elementi (classe [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Restituisce un array delle chiavi degli elementi selezionati. Il metodo `getSelectedItems()` restituisce gli elementi selezionati come coppie chiave-valore. ```php $form->addMultiSelect('countries', 'Paesi:', $countries); ``` -L'array degli elementi offerti viene passato come terzo parametro o tramite il metodo `setItems()`. Gli elementi possono essere anche un array bidimensionale. +Passate l'array degli elementi offerti come terzo parametro oppure con il metodo `setItems()`. Gli elementi possono essere anche un array bidimensionale. -Tramite `setDisabled(['IT', 'DE'])` è possibile disattivare singoli elementi. +Usate `setDisabled(['CZ', 'SK'])` per disattivare singoli elementi. -L'elemento controlla automaticamente che non ci sia stato un tentativo di manomissione e che gli elementi selezionati siano effettivamente tra quelli offerti e non siano stati disattivati. Tramite il metodo `getRawValue()` è possibile ottenere gli elementi inviati senza questo importante controllo. +Il controllo verifica automaticamente che non ci siano state falsificazioni e che gli elementi selezionati fossero davvero tra quelli offerti e non disattivati. Con il metodo `getRawValue()` si possono ottenere gli elementi inviati senza questo importante controllo. -Durante l'impostazione degli elementi selezionati predefiniti, controlla anche che siano tra quelli offerti, altrimenti genera un'eccezione. Questo controllo può essere disattivato tramite `checkDefaultValue(false)`. +Impostando gli elementi selezionati per impostazione predefinita, controlla anche che siano tra quelli offerti, altrimenti solleva un'eccezione. Questo controllo si può disattivare con `checkDefaultValue(false)`. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Aggiunge un campo per l'upload di un file (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Restituisce un oggetto [FileUpload |http:request#FileUpload] anche nel caso in cui l'utente non abbia inviato alcun file, il che può essere verificato con il metodo `FileUpload::hasFile()`. +Aggiunge un campo per caricare un file (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Restituisce un oggetto [FileUpload |http:request#FileUpload], anche se l'utente non ha caricato alcun file, cosa che si può verificare con il metodo `FileUpload::hasFile()`. Con `setNullable()` potete far restituire al controllo `null` invece di un oggetto `FileUpload` quando non viene caricato alcun file. ```php $form->addUpload('avatar', 'Avatar:') @@ -242,44 +243,44 @@ $form->addUpload('avatar', 'Avatar:') ->addRule($form::MaxFileSize, 'La dimensione massima è 1 MB.', 1024 * 1024); ``` -Se il file non viene caricato correttamente, il form non viene inviato con successo e viene visualizzato un errore. Cioè, in caso di invio riuscito, non è necessario verificare il metodo `FileUpload::isOk()`. +Se il file non viene caricato correttamente, il form non viene inviato con successo e viene mostrato un errore. Dopo un invio riuscito, quindi, non è necessario controllare il metodo `FileUpload::isOk()`. -Non fidarti mai del nome originale del file restituito dal metodo `FileUpload::getName()`, il client potrebbe aver inviato un nome di file dannoso con l'intenzione di danneggiare o hackerare la tua applicazione. +Non fidatevi mai del nome originale del file restituito dal metodo `FileUpload::getName()`: il client potrebbe aver inviato un nome di file malevolo con l'intento di danneggiare o violare la vostra applicazione. -Le regole `MimeType` e `Image` rilevano il tipo richiesto in base alla firma del file e non ne verificano l'integrità. Se l'immagine non è danneggiata può essere verificato, ad esempio, tentando di [caricarla |http:request#toImage]. +Le regole `MimeType` e `Image` rilevano il tipo richiesto in base alla firma del file e non ne verificano l'integrità. Se un'immagine sia danneggiata si può stabilire, per esempio, provando a [caricarla |http:request#toImage()]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Aggiunge un campo per l'upload di più file contemporaneamente (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Restituisce un array di oggetti [FileUpload |http:request#FileUpload]. Il metodo `FileUpload::hasFile()` per ciascuno di essi restituirà `true`. +Aggiunge un campo per caricare più file in una volta (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Restituisce un array di oggetti [FileUpload |http:request#FileUpload]. Il metodo `FileUpload::hasFile()` restituirà `true` per ciascuno di essi. ```php $form->addMultiUpload('files', 'File:') - ->addRule($form::MaxLength, 'È possibile caricare al massimo %d file', 10); + ->addRule($form::MaxLength, 'Si possono caricare al massimo %d file.', 10); ``` -Se uno qualsiasi dei file non viene caricato correttamente, il form non viene inviato con successo e viene visualizzato un errore. Cioè, in caso di invio riuscito, non è necessario verificare il metodo `FileUpload::isOk()`. +Se qualche file non viene caricato correttamente, il form non viene inviato con successo e viene mostrato un errore. Dopo un invio riuscito, quindi, non è necessario controllare il metodo `FileUpload::isOk()` per ogni file. -Non fidarti mai dei nomi originali dei file restituiti dal metodo `FileUpload::getName()`, il client potrebbe aver inviato un nome di file dannoso con l'intenzione di danneggiare o hackerare la tua applicazione. +Non fidatevi mai dei nomi originali dei file restituiti dal metodo `FileUpload::getName()`: il client potrebbe aver inviato nomi di file malevoli con l'intento di danneggiare o violare la vostra applicazione. -Le regole `MimeType` e `Image` rilevano il tipo richiesto in base alla firma del file e non ne verificano l'integrità. Se l'immagine non è danneggiata può essere verificato, ad esempio, tentando di [caricarla |http:request#toImage]. +Le regole `MimeType` e `Image` rilevano il tipo richiesto in base alla firma del file e non ne verificano l'integrità. Se un'immagine sia danneggiata si può stabilire, per esempio, provando a [caricarla |http:request#toImage()]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== -Aggiunge un campo che consente all'utente di inserire facilmente una data composta da anno, mese e giorno (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Aggiunge un campo che permette all'utente di inserire facilmente una data composta da anno, mese e giorno (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Come valore predefinito accetta oggetti che implementano l'interfaccia `DateTimeInterface`, una stringa con l'ora o un numero che rappresenta il timestamp UNIX. Lo stesso vale per gli argomenti delle regole `Min`, `Max` o `Range`, che definiscono la data minima e massima consentita. +Come valore predefinito accetta oggetti che implementano `DateTimeInterface`, una stringa che contiene un orario oppure un numero che rappresenta un timestamp UNIX. Lo stesso vale per gli argomenti delle regole `Min`, `Max` o `Range`, che definiscono la data minima e massima ammesse. ```php $form->addDate('date', 'Data:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'La data deve essere almeno un mese fa.', new DateTime('-1 month')); + ->addRule($form::Min, 'La data deve avere almeno un mese.', new DateTime('-1 month')); ``` -Standard restituisce un oggetto `DateTimeImmutable`, con il metodo `setFormat()` puoi specificare il [formato testuale |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] o timestamp: +Per impostazione predefinita restituisce un oggetto `DateTimeImmutable`. Con il metodo `setFormat()` potete indicare un [formato testuale|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] oppure un timestamp: ```php $form->addDate('date', 'Data:') @@ -287,19 +288,19 @@ $form->addDate('date', 'Data:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -Aggiunge un campo che consente all'utente di inserire facilmente un'ora composta da ore, minuti e facoltativamente secondi (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Aggiunge un campo che permette all'utente di inserire facilmente un orario composto da ore, minuti ed eventualmente secondi (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Come valore predefinito accetta oggetti che implementano l'interfaccia `DateTimeInterface`, una stringa con l'ora o un numero che rappresenta il timestamp UNIX. Da questi input viene utilizzata solo l'informazione sull'ora, la data viene ignorata. Lo stesso vale per gli argomenti delle regole `Min`, `Max` o `Range`, che definiscono l'ora minima e massima consentita. Se il valore minimo impostato è superiore al massimo, viene creato un intervallo di tempo che supera la mezzanotte. +Come valore predefinito accetta oggetti che implementano `DateTimeInterface`, una stringa che contiene un orario oppure un numero che rappresenta un timestamp UNIX. Di questi input viene usata solo l'informazione oraria; la data viene ignorata. Lo stesso vale per gli argomenti delle regole `Min`, `Max` o `Range`, che definiscono gli orari minimo e massimo ammessi. Se il valore minimo impostato è maggiore del massimo, si crea un intervallo orario che attraversa la mezzanotte. ```php $form->addTime('time', 'Ora:', withSeconds: true) ->addRule($form::Range, 'L\'ora deve essere compresa tra %d e %d.', ['12:30', '13:30']); ``` -Standard restituisce un oggetto `DateTimeImmutable` (con data 1 gennaio anno 1), con il metodo `setFormat()` puoi specificare il [formato testuale |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: +Per impostazione predefinita restituisce un oggetto `DateTimeImmutable` (con la data impostata al 1° gennaio dell'anno 1). Con il metodo `setFormat()` potete indicare un [formato testuale|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: ```php $form->addTime('time', 'Ora:') @@ -307,20 +308,20 @@ $form->addTime('time', 'Ora:') ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -Aggiunge un campo che consente all'utente di inserire facilmente data e ora composte da anno, mese, giorno, ore, minuti e facoltativamente secondi (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Aggiunge un campo che permette all'utente di inserire facilmente data e ora insieme, composte da anno, mese, giorno, ore, minuti ed eventualmente secondi (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Come valore predefinito accetta oggetti che implementano l'interfaccia `DateTimeInterface`, una stringa con l'ora o un numero che rappresenta il timestamp UNIX. Lo stesso vale per gli argomenti delle regole `Min`, `Max` o `Range`, che definiscono la data minima e massima consentita. +Come valore predefinito accetta oggetti che implementano `DateTimeInterface`, una stringa che contiene un orario oppure un numero che rappresenta un timestamp UNIX. Lo stesso vale per gli argomenti delle regole `Min`, `Max` o `Range`, che definiscono la data e l'ora minime e massime ammesse. ```php $form->addDateTime('datetime', 'Data e ora:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'La data deve essere almeno un mese fa.', new DateTime('-1 month')); + ->addRule($form::Min, 'La data deve avere almeno un mese.', new DateTime('-1 month')); ``` -Standard restituisce un oggetto `DateTimeImmutable`, con il metodo `setFormat()` puoi specificare il [formato testuale |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] o timestamp: +Per impostazione predefinita restituisce un oggetto `DateTimeImmutable`. Con il metodo `setFormat()` potete indicare un [formato testuale|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] oppure un timestamp: ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -Aggiunge un campo per la selezione del colore (classe [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Il colore è una stringa nel formato `#rrggbb`. Se l'utente non effettua la scelta, viene restituito il colore nero `#000000`. +Aggiunge un campo per scegliere un colore (classe [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Il colore viene restituito come stringa nel formato `#rrggbb`. Se l'utente non effettua una scelta, restituisce il nero `#000000`. ```php $form->addColor('color', 'Colore:') @@ -339,8 +340,8 @@ $form->addColor('color', 'Colore:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== Aggiunge un campo nascosto (classe [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -348,13 +349,13 @@ Aggiunge un campo nascosto (classe [HiddenField |api:Nette\Forms\Controls\Hidden $form->addHidden('userid'); ``` -Tramite `setNullable()` è possibile impostare che restituisca `null` invece di una stringa vuota. Modificare il valore inviato è possibile tramite [addFilter() |validation#Modifica dell Input]. +Usate `setNullable()` per fargli restituire `null` invece di una stringa vuota. Il metodo [addFilter() |validation#Modificare i valori inseriti] permette di modificare il valore inviato. -Sebbene l'elemento sia nascosto, è **importante rendersi conto** che il valore può ancora essere modificato o falsificato da un utente malintenzionato. Verifica e convalida sempre attentamente tutti i valori ricevuti lato server per prevenire rischi di sicurezza associati alla manipolazione dei dati. +Benché il controllo sia nascosto, **è importante rendersi conto** che il suo valore può comunque essere modificato o falsificato da un aggressore. Verificate e validate sempre a fondo tutti i valori ricevuti sul lato server, per prevenire i rischi di sicurezza legati alla manipolazione dei dati. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== Aggiunge un pulsante di invio (classe [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). @@ -362,14 +363,23 @@ Aggiunge un pulsante di invio (classe [SubmitButton |api:Nette\Forms\Controls\Su $form->addSubmit('submit', 'Invia'); ``` -Nel form è possibile avere anche più pulsanti di invio: +.{data-version:3.3.0} +Il gestore si può passare direttamente al pulsante come terzo parametro `$onSubmit`, invece di agganciarlo all'evento `onClick`: + +```php +$form->addSubmit('submit', 'Invia', function (SubmitButton $button, $data): void { + // ... +}); +``` + +Nel form è possibile avere più di un pulsante di invio: ```php $form->addSubmit('register', 'Registrati'); $form->addSubmit('cancel', 'Annulla'); ``` -Per scoprire su quale di essi è stato cliccato, usa: +Per stabilire quale sia stato cliccato, usate: ```php if ($form['register']->isSubmittedBy()) { @@ -377,48 +387,48 @@ if ($form['register']->isSubmittedBy()) { } ``` -Se non vuoi validare l'intero form alla pressione del pulsante (ad esempio per i pulsanti *Annulla* o *Anteprima*), usa [setValidationScope() |validation#Disabilitazione della Validazione]. +Se non volete validare l'intero form quando viene premuto un pulsante (per esempio per i pulsanti *Annulla* o *Anteprima*), usate [setValidationScope() |validation#Disattivare la validazione]. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -Aggiunge un pulsante (classe [Button |api:Nette\Forms\Controls\Button]), che non ha funzione di invio. Può quindi essere utilizzato per qualche altra funzione, ad esempio chiamare una funzione JavaScript al clic. +Aggiunge un pulsante (classe [Button |api:Nette\Forms\Controls\Button]) che non ha la funzione di invio. Si può quindi usare per altre funzioni, per esempio per chiamare una funzione JavaScript al clic. ```php -$form->addButton('raise', 'Aumenta stipendio') +$form->addButton('raise', 'Aumenta lo stipendio') ->setHtmlAttribute('onclick', 'raiseSalary()'); ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= Aggiunge un pulsante di invio sotto forma di immagine (classe [ImageButton |api:Nette\Forms\Controls\ImageButton]). ```php -$form->addImageButton('submit', '/path/to/image'); +$form->addImageButton('submit', '/path/to/image.png', 'Invia'); ``` -Utilizzando più pulsanti di invio, è possibile scoprire su quale è stato cliccato tramite `$form['submit']->isSubmittedBy()`. +Usando più pulsanti di invio, potete stabilire quale sia stato cliccato con `$form['submit']->isSubmittedBy()`. addContainer(string|int $name): Container .[method] =================================================== -Aggiunge un sottoform (classe [Container |api:Nette\Forms\Container]), ovvero un contenitore, al quale è possibile aggiungere altri elementi nello stesso modo in cui li aggiungiamo al form. Funzionano anche i metodi `setDefaults()` o `getValues()`. +Aggiunge un sotto-form (classe [Container|api:Nette\Forms\Container]), cioè un container, nel quale si possono aggiungere altri controlli allo stesso modo in cui si aggiungono al form. Funzionano anche metodi come `setDefaults()` o `getValues()`. ```php $sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Il tuo nome:'); +$sub1->addText('name', 'Il vostro nome:'); $sub1->addEmail('email', 'Email:'); $sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Il tuo nome:'); +$sub2->addText('name', 'Il vostro nome:'); $sub2->addEmail('email', 'Email:'); ``` -I dati inviati vengono quindi restituiti come una struttura multidimensionale: +I dati inviati vengono poi restituiti come struttura multidimensionale: ```php [ @@ -437,66 +447,64 @@ I dati inviati vengono quindi restituiti come una struttura multidimensionale: Panoramica delle impostazioni ============================= -Su tutti gli elementi possiamo chiamare i seguenti metodi (panoramica completa nella [documentazione API |https://api.nette.org/forms/master/Nette/Forms/Controls.html]): +Su tutti i controlli possiamo chiamare i metodi seguenti (per una panoramica completa vedi la [documentazione dell'API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): .[table-form-methods language-php] -| `setDefaultValue($value)` | imposta il valore predefinito -| `getValue()` | ottiene il valore attuale -| `setOmitted()` | [#Omissione del valore] -| `setDisabled()` | [#Disattivazione degli elementi] +| `setDefaultValue($value)` | imposta il valore predefinito +| `getValue()` | ottiene il valore corrente +| `setOmitted()` | [#Valori omessi] +| `setDisabled()` | [#Disattivare i controlli] -Renderizzazione: +Rendering: .[table-form-methods language-php] -| `setCaption($caption)` | modifica l'etichetta dell'elemento +| `setCaption($caption)` | cambia l'etichetta del controllo | `setTranslator($translator)` | imposta il [traduttore |rendering#Traduzione] -| `setHtmlAttribute($name, $value)` | imposta l'[attributo HTML |rendering#Attributi HTML] dell'elemento +| `setHtmlAttribute($name, $value)` | imposta un [attributo HTML |rendering#Attributi HTML] dell'elemento | `setHtmlId($id)` | imposta l'attributo HTML `id` -| `setHtmlType($type)` | imposta l'attributo HTML `type` -| `setHtmlName($name)` | imposta l'attributo HTML `name` -| `setOption($key, $value)` | [impostazione per la renderizzazione |rendering#Options] +| `setOption($key, $value)` | [imposta le opzioni di rendering |rendering#Opzioni] Validazione: .[table-form-methods language-php] -| `setRequired()` | [elemento obbligatorio |validation] -| `addRule()` | imposta la [regola di validazione |validation#Regole] -| `addCondition()`, `addConditionOn()` | imposta la [condizione di validazione |validation#Condizioni] -| `addError($message)` | [passaggio del messaggio di errore |validation#Errori durante l Elaborazione] +| `setRequired()` | rende il controllo [obbligatorio |validation] +| `addRule()` | aggiunge una [regola di validazione |validation#Regole] +| `addCondition()`, `addConditionOn()` | imposta una [condizione di validazione |validation#Condizioni] +| `addError($message)` | [aggiunge un messaggio di errore |validation#Elaborare gli errori] -Sugli elementi `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` è possibile chiamare i seguenti metodi: +Sui controlli `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` si possono chiamare i metodi seguenti: .[table-form-methods language-php] -| `setNullable()` | imposta se getValue() restituirà `null` invece di una stringa vuota -| `setEmptyValue($value)` | imposta un valore speciale che viene considerato come stringa vuota -| `setMaxLength($length)` | imposta il numero massimo di caratteri consentiti -| `addFilter($filter)` | [modifica dell'input |validation#Modifica dell Input] +| `setNullable()` | imposta se getValue() restituisce `null` invece di una stringa vuota +| `setEmptyValue($value)` | imposta un valore speciale considerato come stringa vuota +| `setMaxLength($length)` | imposta il numero massimo di caratteri ammessi +| `addFilter($filter)` | [modifica l'input |validation#Modificare i valori inseriti] -Omissione del valore -==================== +Valori omessi +============= -Se il valore compilato dall'utente non ci interessa, possiamo ometterlo dal risultato del metodo `$form->getValues()` o dai dati passati agli handler tramite `setOmitted()`. Questo è utile per varie password di controllo, elementi antispam, ecc. +Se il valore compilato dall'utente non ci interessa, possiamo usare `setOmitted()` per escluderlo dal risultato del metodo `$form->getValues()` o dai dati passati ai gestori. È utile per i vari campi di conferma della password, per i controlli antispam e così via. ```php -$form->addPassword('passwordVerify', 'Password di controllo:') - ->setRequired('Inserisci nuovamente la password per controllo') - ->addRule($form::Equal, 'Le password non corrispondono', $form['password']) +$form->addPassword('passwordVerify', 'Password di nuovo:') + ->setRequired('Inserite di nuovo la password per controllare eventuali errori di battitura') + ->addRule($form::Equal, 'Le password non coincidono', $form['password']) ->setOmitted(); ``` -Disattivazione degli elementi -============================= +Disattivare i controlli +======================= -Gli elementi possono essere disattivati tramite `setDisabled()`. Un tale elemento non può essere modificato dall'utente. +I controlli si possono disattivare con `setDisabled()`. Un controllo disattivato non può essere modificato dall'utente. ```php $form->addText('username', 'Nome utente:') ->setDisabled(); ``` -Gli elementi disabilitati non vengono inviati dal browser al server, quindi non li troverete nei dati restituiti dalla funzione `$form->getValues()`. Tuttavia, se impostate `setOmitted(false)`, Nette includerà in questi dati il loro valore predefinito. +I controlli disattivati non vengono inviati affatto dal browser al server, quindi non li troverete nei dati restituiti dalla funzione `$form->getValues()`. Se però impostate `setOmitted(false)`, Nette includerà in questi dati il loro valore predefinito. -Quando si chiama `setDisabled()`, per motivi di sicurezza **il valore dell'elemento viene cancellato**. Se si imposta un valore predefinito, è necessario farlo dopo la sua disattivazione: +Quando viene chiamato `setDisabled()`, **il valore del controllo viene azzerato** per motivi di sicurezza. Se impostate un valore predefinito, dovete farlo dopo averlo disattivato: ```php $form->addText('username', 'Nome utente:') @@ -504,42 +512,26 @@ $form->addText('username', 'Nome utente:') ->setDefaultValue($userName); ``` -Un'alternativa agli elementi disabilitati sono gli elementi con l'attributo HTML `readonly`, che il browser invia al server. Sebbene l'elemento sia solo di lettura, è **importante rendersi conto** che il suo valore può ancora essere modificato o falsificato da un utente malintenzionato. +Un'alternativa ai controlli disattivati sono i controlli con l'attributo HTML `readonly`, che il browser invia al server. Benché il controllo sia di sola lettura, **è importante rendersi conto** che il suo valore può comunque essere modificato o falsificato da un aggressore. -Elementi personalizzati -======================= +Controlli personalizzati +======================== -Oltre alla vasta gamma di elementi di form integrati, è possibile aggiungere elementi personalizzati al form in questo modo: +Oltre all'ampia gamma di controlli integrati, potete aggiungere al form controlli personalizzati: ```php $form->addComponent(new DateInput('Data:'), 'date'); // sintassi alternativa: $form['date'] = new DateInput('Data:'); ``` -.[note] -Il form è un discendente della classe [Container |component-model:#Container] e i singoli elementi sono discendenti di [Component |component-model:#Component]. - -Esiste un modo per definire nuovi metodi del form che servono ad aggiungere elementi personalizzati (es. `$form->addZip()`). Si tratta delle cosiddette extension methods. Lo svantaggio è che per esse non funzionerà il suggerimento negli editor. - -```php -use Nette\Forms\Container; - -// aggiungiamo il metodo addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Almeno 5 numeri', '[0-9]{5}'); -}); - -// utilizzo -$form->addZip('zip', 'Codice postale:'); -``` +Come scrivere un controllo del genere, compresa la lettura dei dati inviati, la validazione e il rendering, è descritto in un [capitolo a parte |custom-controls]. Lì scoprirete anche i metodi di estensione, che vi permettono di creare un vostro metodo di aggiunta come `$form->addZip()`. -Elementi di basso livello -========================= +Campi di basso livello +====================== -È possibile utilizzare anche elementi che scriviamo solo nel template e non aggiungiamo al form con uno dei metodi `$form->addXyz()`. Ad esempio, quando elenchiamo record da un database e non sappiamo in anticipo quanti ce ne saranno e quali ID avranno, e vogliamo visualizzare una checkbox o un radio button per ogni riga, basta codificarlo nel template: +È possibile usare anche controlli scritti solo nel template e non aggiunti al form con nessuno dei metodi `$form->addXyz()`. Per esempio, elencando record da un database di cui non sappiamo in anticipo quanti saranno né quali saranno i loro ID, e volendo mostrare per ogni riga una checkbox o un radio button, possiamo semplicemente scriverlo nel template: ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ Elementi di basso livello {/foreach} ``` -E dopo l'invio scopriamo il valore: +E dopo l'invio otteniamo il valore: ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -dove il primo parametro è il tipo di elemento (`DataFile` per `type=file`, `DataLine` per input a riga singola come `text`, `password`, `email` ecc. e `DataText` per tutti gli altri) e il secondo parametro `sel[]` corrisponde all'attributo HTML name. Il tipo di elemento può essere combinato con il valore `DataKeys`, che conserva le chiavi degli elementi. Questo è particolarmente utile per `select`, `radioList` e `checkboxList`. +dove il primo parametro è il tipo di elemento (`DataFile` per `type=file`, `DataLine` per i campi a riga singola come `text`, `password`, `email` ecc., e `DataText` per tutti gli altri) e il secondo parametro `sel[]` corrisponde all'attributo HTML name. Possiamo combinare il tipo di elemento con il valore `DataKeys`, che conserva le chiavi degli elementi. È particolarmente utile per `select`, `radioList` e `checkboxList`. -È essenziale che `getHttpData()` restituisca un valore sanificato, in questo caso sarà sempre un array di stringhe UTF-8 valide, indipendentemente da ciò che un utente malintenzionato potrebbe tentare di inviare al server. È analogo al lavoro diretto con `$_POST` o `$_GET`, ma con la differenza sostanziale che restituisce sempre dati puliti, come siete abituati con gli elementi standard dei form Nette. +Cosa essenziale, `getHttpData()` restituisce un valore ripulito. In questo caso sarà sempre un array di stringhe UTF-8 valide, indipendentemente da ciò che un aggressore possa provare a inviare al server. È l'analogo del lavorare direttamente con `$_POST` o `$_GET`, ma con la differenza sostanziale che restituisce sempre dati puliti, come siete abituati con i controlli standard dei form di Nette. diff --git a/forms/it/custom-controls.texy b/forms/it/custom-controls.texy new file mode 100644 index 0000000000..8304480f1e --- /dev/null +++ b/forms/it/custom-controls.texy @@ -0,0 +1,268 @@ +Controlli personalizzati dei form +********************************* + +.[perex] +Nette offre un'ampia gamma di [controlli integrati |controls]. Ma quando vi imbattete in un requisito che non è tra questi, non dovete aggirare nulla né incollare pezzi insieme: scrivete un vostro controllo. Saprà fare tutto quello che sanno fare quelli integrati (validare, tradursi, disegnarsi) e si userà esattamente allo stesso modo. + +Lo mostreremo con un esempio pratico: un controllo per inserire una data con tre campi, giorno, mese e anno. Strada facendo imparerete tutto ciò che serve sapere per scrivere controlli. + + +Quando scrivere un controllo personalizzato e quando no +======================================================= + +Un controllo personalizzato è lo strumento più potente offerto dai form. E come ogni strumento potente, dovrebbe essere l'ultima scelta, non la prima. Molte situazioni si risolvono con mezzi più semplici: + +- **Modificare un valore** è compito di [addFilter() |validation#Modificare i valori inseriti]. Volete tollerare gli spazi in un CAP o le lettere minuscole in un codice? Un filtro sono poche righe. +- **La configurazione ripetuta** si racchiude in un metodo di aggiunta personalizzato. Aggiungete in dieci punti un campo CAP con la stessa validazione? Createne una scorciatoia con un nome, [lo mostriamo alla fine |#Metodo di aggiunta personalizzato]. +- **Un gruppo di campi collegati** è servito da un [container |controls#addContainer()]. Un indirizzo composto da via, città e CAP non ha bisogno di un controllo personalizzato: basta un container con tre campi di testo. +- **Un aspetto diverso** si ottiene con [setHtmlType() |controls#addText()] e gli attributi HTML, oppure con i [prototipi |rendering#Prototipi]. + +Un controllo personalizzato ha senso nel momento in cui vi serve un **valore personalizzato**: un controllo che all'esterno si comporta come un unico campo con un unico valore, ma che internamente è composto da più input o conserva il valore in modo diverso da come lo mostra. Una data da tre campi. Delle coordinate scelte cliccando su una mappa. Un campo per i tag con completamento automatico. + + +Anatomia di un controllo +======================== + +Ogni controllo personalizzato eredita dalla classe astratta [api:Nette\Forms\Controls\BaseControl]. Da essa eredita un'enorme quantità di funzionalità già pronte: la conservazione del valore, le regole e le condizioni di validazione, i messaggi di errore, le traduzioni, gli attributi HTML, l'etichetta e il collegamento al rendering. Voi scrivete solo ciò che rende diverso il vostro controllo. + +Un controllo minimo funzionante è sorprendentemente breve: + +```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, + ]); + } +} +``` + +Due metodi: uno dice come ottenere il valore dai dati inviati, l'altro come disegnare il controllo. Li esamineremo entrambi da vicino tra poco. Tutto il resto (`setRequired()`, `addRule()`, `setDefaultValue()`, le traduzioni) funziona già da sé. + +Il controllo si aggiunge al form con il metodo `addComponent()` oppure, più concisamente, con le parentesi quadre: + +```php +$form['nickname'] = new SimpleInput('Nickname:'); +``` + + +Ciclo di vita di un controllo +============================= + +Prima di passare a un controllo più interessante, è bene sapere cosa succede a un controllo e quando. Il form e i suoi controlli sono [componenti |component-model:] che formano un albero. Questo ha una gradevole conseguenza: il controllo non deve scoprire nulla da solo, il framework si occupa di tutto ciò che conta al momento giusto: + +1) Nel momento in cui agganciate il controllo a un form inviato, il form stesso vi chiama sopra `loadHttpData()`. Lì il controllo legge il proprio valore inviato, come mostreremo tra poco. Non lavora mai direttamente con `$_POST` e non deve preoccuparsi affatto di essere annidato in dei container. + +2) Quando il form viene inviato, avviene la validazione: vengono valutate le regole aggiunte con `addRule()`, che lavorano con il valore restituito da `getValue()`. + +3) Chi poi chiama `$form->getValues()` oppure `getValue()` sul controllo ottiene un valore pulito e tipizzato, per esempio un oggetto `DateTimeImmutable`, non una terna di stringhe provenienti dal form. + +E durante il rendering viene chiamato `getControl()`, oppure `getLabel()` per l'etichetta. + + +Leggere il valore inviato +========================= + +Nel metodo `loadHttpData()` il controllo chiede il proprio valore inviato con il metodo `getHttpData()`. Il suo parametro è un tipo che determina come il valore va ripulito: + +| tipo | significato +|------- +| `Form::DataLine` | testo su una riga: sostituisce gli a capo con spazi, elimina gli spazi ai bordi +| `Form::DataText` | testo su più righe: normalizza i fine riga in `\n` +| `Form::DataFile` | upload, un'istanza di `Nette\Http\FileUpload` + +Per quanto ci provi un aggressore, il risultato è sempre una stringa UTF-8 valida senza caratteri di controllo (oppure un oggetto di upload o `null`). È esattamente per questo che non leggiamo mai il valore direttamente da `$_POST`: perderemmo tutte queste garanzie. + +Un controllo composto da più input, come la nostra data, passa come secondo parametro una parte del nome HTML e legge così i propri sotto-valori. Li conserva nelle proprie proprietà `$day`, `$month` e `$year` di tipo 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]') ?? ''; +} +``` + +Se il nome HTML termina con `[]`, viene restituito un array di valori. Combinandolo con il tipo `Form::DataKeys` (cioè `Form::DataLine | Form::DataKeys`) ne conservate anche le chiavi: + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +Un valore mancante è `null` (un array vuoto per gli array). La richiesta può non contenere affatto i dati del controllo, e nulla impedisce a un aggressore di inviare quello che vuole: ecco perché nell'esempio aggiungiamo `?? ''` e perché dovreste sempre tenere conto di questa possibilità. + + +Valore del controllo +==================== + +Il controllo conserva il proprio valore e lo espone attraverso tre metodi, il cui contratto vale la pena rispettare. + +Il metodo `setValue()` accetta un valore dal programmatore: è anche la strada percorsa da `setDefaultValue()` e da `$form->setDefaults()`. Dovrebbe accettare tutto ciò che ha senso, convertire il valore nella propria forma interna e sollevare un'eccezione per input assurdi, così che l'errore compaia subito e non attraverso comportamenti misteriosi del form. La nostra data accetta un `DateTimeInterface`, una stringa, un timestamp oppure `null`, e li divide nei tre campi: + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // un valore assurdo solleva un'eccezione + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +Il metodo `getValue()`, al contrario, compone un valore pulito e tipizzato, l'unica cosa che vedrà chi usa il vostro controllo. Se il valore non è valido, restituisce `null`. Il metodo statico `validateDate()` si limita a controllare che i tre campi formino una data esistente: + +```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; +} +``` + +E il metodo `isFilled()` dice se l'utente ha compilato il controllo: lo usa la regola `setRequired()`. L'implementazione predefinita (un valore non vuoto) spesso basta, ma per un controllo composito sovrascrivetela secondo la sua logica: + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Rendering +========= + +Il metodo `getControl()` restituisce la forma HTML del controllo, di norma come oggetto [Html |utils:html-elements], ma va bene anche una semplice stringa: non fa differenza. Ricorriamo all'oggetto Html soprattutto per comporre il codice, perché ci permette di costruire il markup risultante in sicurezza e con un'API gradevole. Avete a disposizione diversi aiuti: + +- `getHtmlName()` restituisce l'attributo HTML `name`, compreso l'eventuale annidamento nei container (per esempio `invoice[date]`). Per un controllo composito vi aggiungete le parti del nome dei singoli input: `$name . '[day]'`. +- `getHtmlId()` restituisce l'attributo `id` collegato all'etichetta. +- `Helpers::exportRules($this->getRules())` esporta le regole di validazione per l'attributo `data-nette-rules`, grazie al quale la [validazione JavaScript |validation#Validazione JavaScript] funzionerà anche per il vostro controllo. L'attributo va sul primo input del controllo. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` compone un elemento `<select>` a partire da un array di elementi (gli array annidati vengono disegnati come `<optgroup>`) e lo restituisce come `Html`: comodo per il campo del mese della nostra data. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` genera un elenco di elementi `<input>` racchiusi in `<label>` (radio button o checkbox) e lo restituisce come stringa. + +Il primo campo della nostra data si crea quindi così: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select per il mese e input per l'anno ... */); +} +``` + +L'etichetta viene disegnata da `getLabel()` e la sua implementazione predefinita di norma va bene. Attenzione però: per un controllo composito il suo attributo `for` punta a `getHtmlId()`, quindi date questo id al primo input, esattamente come nell'esempio. + +Per far disegnare il controllo composito pezzo per pezzo in un template (per esempio `{input birthdate:day}`), sovrascrivete i metodi `getControlPart($key)` e `getLabelPart($key)`, che restituiscono l'elemento `Html` della parte indicata, allo stesso modo di `CheckboxList` e `RadioList`. + +.[note] +Se sovrascrivete `getControl()`, tenete presente che `BaseControl::getControl()` contrassegna anche il controllo come disegnato, con `setOption('rendered', true)`. Chiamatelo anche voi (oppure chiamate `parent::getControl()`) quando combinate il rendering manuale e quello automatico dello stesso form, così che il controllo non venga disegnato due volte. (L'esempio `DateInput` qui sopra lo omette per brevità.) + + +Esempio completo: DateInput +=========================== + +Tutti i pezzi descritti messi insieme, integrati da un select box per scegliere il mese, si trovano nel controllo `DateInput` già pronto, tra gli [esempi presenti nel repository |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Notate che nel costruttore il controllo si aggiunge una regola di validazione che controlla che la data abbia senso. Un input assurdo, come il 31 febbraio, compare così come un normale errore di validazione del form: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'The date is invalid.'); +} +``` + +E l'uso? Esattamente come per i controlli integrati: + +```php +$form['birthdate'] = (new DateInput('Data di nascita:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('Quando siete nati?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +In un template Latte lo disegnate con il consueto tag `{input birthdate}` o `{label birthdate /}`, come qualsiasi altro controllo. + + +Validazione +=========== + +Le regole di validazione integrate funzionano subito con un controllo personalizzato: lavorano sul valore restituito da `getValue()`. Il nostro `DateInput` può quindi usare, per esempio, `Form::Min` per la data più antica ammessa. Come scrivere regole proprie, compresa la loro controparte JavaScript, è descritto nel capitolo [Regole e condizioni personalizzate |validation#Regole e condizioni personalizzate]. + + +Metodo di aggiunta personalizzato +================================= + +I controlli integrati si aggiungono con i comodi metodi `$form->addText()` e simili. Un controllo personalizzato non ha un metodo del genere, quindi lo aggiungete con una semplice assegnazione: funziona allo stesso modo in un form e in un container, e gli editor e l'analisi statica lo capiscono: + +```php +$form['birthdate'] = new DateInput('Data di nascita:'); +``` + +Se volete accorciare l'aggiunta mantenendo il completamento automatico, torna comodo un metodo factory statico sul controllo stesso. Funziona anche nei container annidati, cosa che un metodo su un discendente della classe `Form` non potrebbe fare: i container annidati non ne sanno nulla. + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// funziona in un form e in qualsiasi container: +DateInput::addTo($form, 'birthdate', 'Data di nascita:'); +``` + +Lo stesso approccio funziona anche come scorciatoia con un nome per la configurazione ripetuta di un controllo integrato: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'Il CAP deve essere di esattamente 5 cifre', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'CAP:'); +``` diff --git a/forms/it/in-presenter.texy b/forms/it/in-presenter.texy index 4743cf2fc6..1033bdfd36 100644 --- a/forms/it/in-presenter.texy +++ b/forms/it/in-presenter.texy @@ -2,15 +2,15 @@ Form nei presenter ****************** .[perex] -Nette Forms facilita enormemente la creazione e l'elaborazione dei form web. In questo capitolo imparerete come utilizzare i form all'interno dei presenter. +Nette Forms semplifica notevolmente la creazione e l'elaborazione dei form web. In questo capitolo imparerete a usare i form dentro i presenter. -Se siete interessati a come usarli completamente da soli senza il resto del framework, è per voi la guida per l'[uso indipendente |standalone]. +Se vi interessa usarli in modo completamente autonomo, senza il resto del framework, c'è una guida sull'[uso autonomo|standalone]. -Primo form -========== +Il primo form +============= -Proviamo a scrivere un semplice form di registrazione. Il suo codice sarà il seguente: +Proviamo a scrivere un semplice form di registrazione. Il suo codice sarà questo: ```php use Nette\Application\UI\Form; @@ -19,16 +19,16 @@ $form = new Form; $form->addText('name', 'Nome:'); $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Registrati'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` -e nel browser verrà visualizzato così: +e nel browser verrà mostrato così: -[* form-cs.webp *] +[* form-en.webp *] -Il form nel presenter è un oggetto della classe `Nette\Application\UI\Form`, il suo predecessore `Nette\Forms\Form` è destinato all'uso indipendente. Vi abbiamo aggiunto i cosiddetti elementi nome, password e pulsante di invio. E infine, la riga con `$form->onSuccess` dice che dopo l'invio e la validazione riuscita, deve essere chiamato il metodo `$this->formSucceeded()`. +Un form in un presenter è un oggetto della classe `Nette\Application\UI\Form`; il suo predecessore `Nette\Forms\Form` è destinato all'uso autonomo. Vi abbiamo aggiunto i controlli name, password e un pulsante di invio. Infine la riga `$form->onSuccess` dice che dopo l'invio e la validazione riuscita deve essere chiamato il metodo `$this->formSucceeded()`. -Dal punto di vista del presenter, il form è un componente comune. Pertanto, viene trattato come un componente e lo integriamo nel presenter tramite un [metodo factory |application:components#Metodi Factory]. Sarà simile a questo: +Dal punto di vista del presenter, il form è un normale componente. Viene quindi trattato come un componente e integrato nel presenter con un [metodo factory |application:components#Metodi factory]. Avrà questo aspetto: ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -42,22 +42,22 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Nome:'); $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Registrati'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { - // qui elaboriamo i dati inviati dal form + // qui elaboreremo i dati inviati dal form // $data->name contiene il nome // $data->password contiene la password - $this->flashMessage('Sei stato registrato con successo.'); + $this->flashMessage('Vi siete registrati con successo.'); $this->redirect('Home:'); } } ``` -E nel template renderizziamo il form con il tag `{control}`: +E nel template il form si disegna con il tag `{control}`: ```latte .{file:app/Presentation/Home/default.latte} <h1>Registrazione</h1> @@ -65,45 +65,47 @@ E nel template renderizziamo il form con il tag `{control}`: {control registrationForm} ``` -E questo è praticamente tutto :-) Abbiamo un form funzionante e perfettamente [protetto |#Protezione dalle vulnerabilità]. +E in sostanza è tutto :-) Abbiamo un form funzionante e perfettamente [protetto |#Protezione dalle vulnerabilità]. -E ora probabilmente state pensando che sia stato troppo veloce, vi state chiedendo come sia possibile che venga chiamato il metodo `formSucceeded()` e quali siano i parametri che riceve. Certo, avete ragione, questo merita una spiegazione. +Ora starete pensando che è andata troppo in fretta e vi chiederete come sia possibile che il metodo `formSucceeded()` venga chiamato e quali parametri riceva. Sì, avete ragione, la cosa merita una spiegazione. -Nette infatti introduce un meccanismo fresco, che chiamiamo [Hollywood style |application:components#Stile Hollywood]. Invece di dovervi chiedere costantemente come sviluppatori se è successo qualcosa ("il form è stato inviato?", "è stato inviato validamente?" e "non è stato manomesso?"), dite al framework "quando il form sarà compilato validamente, chiama questo metodo" e lasciate il resto del lavoro a lui. Se programmate in JavaScript, questo stile di programmazione vi è familiare. Scrivete funzioni che vengono chiamate quando si verifica un certo [evento |nette:glossary#Eventi]. E il linguaggio passa loro gli argomenti appropriati. +Nette introduce un meccanismo rinfrescante, chiamato [stile hollywoodiano |application:components#Stile hollywoodiano]. Invece che voi, come sviluppatori, dobbiate chiedere di continuo se è successo qualcosa ("il form è stato inviato?", "è stato inviato in modo valido?" e "non è stato falsificato?"), dite al framework "quando il form è compilato validamente, chiama questo metodo" e gli lasciate il lavoro successivo. Se programmate in JavaScript conoscete benissimo questo stile di programmazione: scrivete funzioni che vengono chiamate quando si verifica un certo [evento |nette:glossary#Eventi]. E il linguaggio passa loro gli argomenti appropriati. -È proprio così che è costruito anche il codice del presenter sopra riportato. L'array `$form->onSuccess` rappresenta un elenco di callback PHP che Nette chiama nel momento in cui il form viene inviato e compilato correttamente (cioè è valido). Nell'ambito del [ciclo di vita del presenter |application:presenters#Ciclo di vita del presenter] si tratta del cosiddetto segnale, vengono quindi chiamati dopo il metodo `action*` e prima del metodo `render*`. E ad ogni callback passa come primo parametro il form stesso e come secondo i dati inviati sotto forma di oggetto [ArrayHash |utils:arrays#ArrayHash]. Il primo parametro può essere omesso se non si necessita dell'oggetto form. E il secondo parametro può essere più intelligente, ma di questo parleremo [più avanti |#Mappatura su classi]. +È esattamente così che è costruito il codice del presenter qui sopra. L'array `$form->onSuccess` rappresenta un elenco di callback PHP che Nette chiama nel momento in cui il form viene inviato e compilato correttamente (cioè è valido). Nel [ciclo di vita del presenter |application:presenters#Ciclo di vita del presenter] si tratta di un cosiddetto segnale, quindi vengono chiamate dopo il metodo `action*` e prima del metodo `render*`. E a ogni callback passa come primo parametro il form stesso e come secondo i dati inviati, come oggetto [ArrayHash |utils:arrays#ArrayHash] (oppure stdClass, oppure una classe personalizzata). Potete omettere il primo parametro se non vi serve l'oggetto form. Il secondo parametro può essere più intelligente, ma ne parliamo [più avanti |#Mappatura sulle classi]. -L'oggetto `$data` contiene le chiavi `name` e `password` con i dati compilati dall'utente. Di solito inviamo i dati direttamente per un'ulteriore elaborazione, che può essere ad esempio l'inserimento nel database. Durante l'elaborazione, però, può verificarsi un errore, ad esempio il nome utente è già occupato. In tal caso, restituiamo l'errore al form tramite `addError()` e lo facciamo renderizzare di nuovo, anche con il messaggio di errore. +L'oggetto `$data` contiene le proprietà `name` e `password` con i dati inseriti dall'utente. Di solito inviamo i dati direttamente a un'ulteriore elaborazione, che può essere per esempio l'inserimento in un database. Durante l'elaborazione può però verificarsi un errore, per esempio se il nome utente è già occupato. In tal caso passiamo l'errore al form con `addError()` e lo facciamo disegnare di nuovo, insieme al messaggio di errore. ```php -$form->addError('Ci dispiace, il nome utente è già in uso.'); +$form->addError('Spiacenti, questo nome utente è già in uso.'); ``` -Oltre a `onSuccess` esiste anche `onSubmit`: i callback vengono chiamati sempre dopo l'invio del form, anche se non è compilato correttamente. E inoltre `onError`: i callback vengono chiamati solo se l'invio non è valido. Vengono chiamati anche se in `onSuccess` o `onSubmit` invalidiamo il form tramite `addError()`. +Oltre a `onSuccess` esiste anche `onSubmit`: le callback vengono chiamate ogni volta che il form viene inviato, anche se non è compilato correttamente. E anche `onError`: le callback vengono chiamate solo se l'invio non è valido. Vengono chiamate anche se invalidiamo il form dentro `onSuccess` con `addError()`. -Dopo l'elaborazione del form, reindirizziamo alla pagina successiva. Ciò impedisce l'invio involontario ripetuto del form tramite il pulsante *aggiorna*, *indietro* o muovendosi nella cronologia del browser. +Dopo aver elaborato il form reindirizziamo a un'altra pagina. Questo impedisce il reinvio indesiderato del form usando il pulsante *aggiorna*, il pulsante *indietro* oppure navigando nella cronologia del browser. -Provate ad aggiungere anche altri [elementi del form |controls]. +Se il form viene inviato via AJAX, di norma invece di reindirizzare ridisegnate uno [snippet |application:ajax] con il form disegnato di nuovo. +Provate ad aggiungere anche altri [controlli|controls]. -Accesso agli elementi -===================== -Il form è un componente del presenter, nel nostro caso chiamato `registrationForm` (dal nome del metodo factory `createComponentRegistrationForm`), quindi ovunque nel presenter potete accedere al form tramite: +Accesso ai controlli +==================== + +Il form è un componente del presenter, nel nostro caso chiamato `registrationForm` (dal nome del metodo factory `createComponentRegistrationForm`), quindi in qualsiasi punto del presenter potete accedere al form così: ```php $form = $this->getComponent('registrationForm'); // sintassi alternativa: $form = $this['registrationForm']; ``` -Anche i singoli elementi del form sono componenti, quindi potete accedervi allo stesso modo: +Anche i singoli controlli del form sono componenti, quindi potete accedervi allo stesso modo: ```php -$input = $form->getComponent('name'); // o $input = $form['name']; -$button = $form->getComponent('send'); // o $button = $form['send']; +$input = $form->getComponent('name'); // oppure $input = $form['name']; +$button = $form->getComponent('send'); // oppure $button = $form['send']; ``` -Gli elementi vengono rimossi tramite unset: +I controlli si rimuovono con `unset`: ```php unset($form['name']); @@ -113,26 +115,26 @@ unset($form['name']); Regole di validazione ===================== -Abbiamo menzionato la parola *valido,* ma il form per ora non ha regole di validazione. Rimediamo. +Abbiamo usato la parola *valido*, ma il form non ha ancora alcuna regola di validazione. Rimediamo. -Il nome sarà obbligatorio, quindi lo contrassegniamo con il metodo `setRequired()`, il cui argomento è il testo del messaggio di errore che verrà visualizzato se l'utente non compila il nome. Se non specifichiamo l'argomento, verrà utilizzato il messaggio di errore predefinito. +Il nome sarà obbligatorio, quindi lo contrassegniamo con il metodo `setRequired()`. Il suo argomento è il testo del messaggio di errore mostrato se l'utente non compila il nome. Se l'argomento viene omesso, viene usato il messaggio di errore predefinito. ```php $form->addText('name', 'Nome:') - ->setRequired('Inserisci il nome per favore'); + ->setRequired('Inserite il vostro nome.'); ``` -Provate a inviare il form senza compilare il nome e vedrete che verrà visualizzato un messaggio di errore e il browser o il server lo rifiuteranno finché non compilerete il campo. +Provate a inviare il form senza compilare il nome e vedrete comparire un messaggio di errore; il browser o il server lo rifiuteranno finché non compilate il campo. -Allo stesso tempo, non potete ingannare il sistema scrivendo nel campo, ad esempio, solo spazi. Niente da fare. Nette rimuove automaticamente gli spazi iniziali e finali. Provate. È una cosa che dovreste fare sempre con ogni input a riga singola, ma spesso viene dimenticata. Nette lo fa automaticamente. (Potete provare a ingannare il form e inviare una stringa multilinea come nome. Nemmeno qui Nette si lascia ingannare e trasforma gli a capo in spazi.) +Allo stesso tempo non potete imbrogliare il sistema inserendo nel campo, per esempio, solo degli spazi. Niente da fare. Nette elimina automaticamente gli spazi iniziali e finali. Provate. È una cosa che dovreste sempre fare con ogni campo a riga singola, ma che spesso si dimentica. Nette la fa automaticamente. (Potete provare a ingannare il form inviando come nome una stringa su più righe. Anche qui Nette non si farà ingannare e gli a capo verranno convertiti in spazi.) -Il form viene sempre validato lato server, ma viene generata anche una validazione JavaScript, che avviene istantaneamente e l'utente viene informato dell'errore immediatamente, senza dover inviare il form al server. Questo è gestito dallo script `netteForms.js`. Inseritelo nel template del layout: +Il form viene sempre validato sul lato server, ma viene generata anche la validazione JavaScript, che gira all'istante e permette all'utente di conoscere l'errore subito, senza dover inviare il form al server. Se ne occupa lo script `netteForms.js`. Includetelo nel vostro template di layout: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Se guardate il codice sorgente della pagina con il form, potete notare che Nette inserisce gli elementi obbligatori in elementi con la classe CSS `required`. Provate ad aggiungere al template il seguente foglio di stile e l'etichetta "Nome" diventerà rossa. In questo modo elegante segnaliamo agli utenti gli elementi obbligatori: +Se guardate il codice sorgente della pagina con il form, noterete forse che Nette racchiude i controlli obbligatori in elementi con la classe CSS `required`. Provate ad aggiungere al template il foglio di stile seguente e l'etichetta "Nome" sarà rossa. È un modo elegante di evidenziare i campi obbligatori per gli utenti: ```latte <style> @@ -140,85 +142,87 @@ Se guardate il codice sorgente della pagina con il form, potete notare che Nette </style> ``` -Aggiungiamo ulteriori regole di validazione con il metodo `addRule()`. Il primo parametro è la regola, il secondo è di nuovo il testo del messaggio di errore e può ancora seguire un argomento della regola di validazione. Cosa si intende con questo? +Aggiungiamo altre regole di validazione con il metodo `addRule()`. Il primo parametro è la regola, il secondo è di nuovo il testo del messaggio di errore, e può seguire un argomento della regola di validazione. Cosa significa? -Estendiamo il form con un nuovo campo opzionale "età", che deve essere un numero intero (`addInteger()`) e inoltre in un intervallo consentito (`$form::Range`). E qui useremo proprio il terzo parametro del metodo `addRule()`, con cui passiamo al validatore l'intervallo richiesto come coppia `[da, a]`: +Estendiamo il form con un nuovo campo facoltativo "età", che deve essere un numero intero (`addInteger()`) e rientrare anche in un intervallo consentito (`$form::Range`). Qui useremo il terzo parametro del metodo `addRule()` per passare al validatore l'intervallo richiesto come coppia `[min, max]`: ```php $form->addInteger('age', 'Età:') - ->addRule($form::Range, 'L\'età deve essere compresa tra 18 e 120', [18, 120]); + ->addRule($form::Range, 'L\'età deve essere compresa tra 18 e 120.', [18, 120]); ``` .[tip] -Se l'utente non compila il campo, le regole di validazione non verranno verificate, poiché l'elemento è opzionale. +Se l'utente non compila il campo, le regole di validazione non verranno controllate, perché l'elemento è facoltativo. -Qui si crea spazio per un piccolo refactoring. Nel messaggio di errore e nel terzo parametro, i numeri sono indicati in modo duplicato, il che non è ideale. Se stessimo creando [form multilingue |rendering#Traduzione] e il messaggio contenente numeri fosse tradotto in più lingue, un'eventuale modifica dei valori diventerebbe più difficile. Per questo motivo, è possibile utilizzare i segnaposto `%d` e Nette completerà i valori: +Questo apre spazio a un piccolo refactoring. Nel messaggio di errore e nel terzo parametro i numeri sono duplicati, il che non è ideale. Se creassimo [form multilingue |rendering#Traduzione] e il messaggio contenente i numeri venisse tradotto in più lingue, cambiare i valori diventerebbe difficile. Per questo si possono usare i segnaposto `%d`, che Nette sostituirà con i valori: ```php - ->addRule($form::Range, 'L\'età deve essere compresa tra %d e %d anni', [18, 120]); + ->addRule($form::Range, 'L\'età deve essere compresa tra %d e %d anni.', [18, 120]); ``` -Torniamo all'elemento `password`, che renderemo anch'esso obbligatorio e verificheremo inoltre la lunghezza minima della password (`$form::MinLength`), sempre utilizzando il segnaposto: +Torniamo al controllo `password`, rendiamolo obbligatorio e verifichiamo anche la lunghezza minima della password (`$form::MinLength`), usando di nuovo un segnaposto nel messaggio: ```php $form->addPassword('password', 'Password:') - ->setRequired('Scegli una password') - ->addRule($form::MinLength, 'La password deve avere almeno %d caratteri', 8); + ->setRequired('Scegliete una password') + ->addRule($form::MinLength, 'La vostra password deve essere lunga almeno %d caratteri.', 8); ``` -Aggiungiamo al form anche il campo `passwordVerify`, dove l'utente inserirà nuovamente la password, per controllo. Tramite le regole di validazione verifichiamo se entrambe le password sono uguali (`$form::Equal`). E come parametro diamo un riferimento alla prima password usando le [parentesi quadre |#Accesso agli elementi]: +Aggiungiamo al form un altro campo `passwordVerify`, in cui l'utente inserisce di nuovo la password per conferma. Con le regole di validazione controlliamo che le due password siano uguali (`$form::Equal`). Come argomento indichiamo un riferimento alla prima password usando le [parentesi quadre |#Accesso ai controlli]: ```php -$form->addPassword('passwordVerify', 'Password di controllo:') - ->setRequired('Inserisci nuovamente la password per controllo') - ->addRule($form::Equal, 'Le password non corrispondono', $form['password']) +$form->addPassword('passwordVerify', 'Password di nuovo:') + ->setRequired('Inserite di nuovo la password per controllare eventuali errori di battitura') + ->addRule($form::Equal, 'Le password non coincidono.', $form['password']) ->setOmitted(); ``` -Tramite `setOmitted()` abbiamo contrassegnato l'elemento il cui valore in realtà non ci interessa e che esiste solo per motivi di validazione. Il valore non viene passato a `$data`. +Con `setOmitted()` abbiamo contrassegnato un controllo il cui valore non ci interessa davvero e che esiste solo a scopo di validazione. Il suo valore non viene passato a `$data`. -Con questo abbiamo un form completamente funzionante con validazione sia in PHP che in JavaScript. Le capacità di validazione di Nette sono molto più ampie, si possono creare condizioni, far visualizzare e nascondere parti della pagina in base ad esse, ecc. Tutto questo lo imparerete nel capitolo sulla [validazione dei form |validation]. +Con questo abbiamo un form pienamente funzionante, con validazione sia in PHP sia in JavaScript. Le capacità di validazione di Nette sono molto più ampie: si possono creare condizioni, mostrare o nascondere parti della pagina in base a esse e altro ancora. Imparerete tutto nel capitolo sulla [validazione dei form|validation]. Valori predefiniti ================== -Agli elementi del form impostiamo comunemente valori predefiniti: +Impostiamo comunemente valori predefiniti per i controlli del form: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -Spesso è utile impostare i valori predefiniti per tutti gli elementi contemporaneamente. Ad esempio, quando il form serve per modificare record. Leggiamo il record dal database e impostiamo i valori predefiniti: +Spesso è utile impostare i valori predefiniti di tutti i controlli in una volta. Per esempio quando il form serve a modificare dei record. Leggiamo il record dal database e ne impostiamo i valori predefiniti: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Chiamate `setDefaults()` dopo aver definito gli elementi. +Chiamate `setDefaults()` dopo aver definito i controlli. +Su un form già inviato `setDefaults()` non ha effetto: non sovrascrive ciò che l'utente ha compilato, quindi è sicuro chiamarlo incondizionatamente nella factory del form. Se dovete forzare i valori anche dopo l'invio, usate invece `setValues()`. -Renderizzazione del form -======================== -Standardmente il form viene renderizzato come una tabella. I singoli elementi soddisfano la regola base di accessibilità - tutte le etichette sono scritte come `<label>` e collegate al rispettivo elemento del form. Cliccando sull'etichetta, il cursore appare automaticamente nel campo del form. +Disegnare il form +================= -A ogni elemento possiamo impostare attributi HTML arbitrari. Ad esempio, aggiungere un placeholder: +Per impostazione predefinita il form viene disegnato come una tabella. I singoli controlli rispettano le regole di base dell'accessibilità web: tutte le etichette sono scritte come elementi `<label>` e associate ai rispettivi controlli. Cliccando sull'etichetta il cursore si posiziona automaticamente nel campo del form. + +A ogni controllo possiamo impostare attributi HTML qualsiasi. Aggiungiamo per esempio un placeholder: ```php $form->addInteger('age', 'Età:') - ->setHtmlAttribute('placeholder', 'Inserisci l\'età per favore'); + ->setHtmlAttribute('placeholder', 'Inserite l\'età'); ``` -I modi per renderizzare un form sono davvero tanti, quindi c'è un [capitolo separato sulla renderizzazione |rendering]. +I modi di disegnare un form sono davvero tanti, quindi al [rendering è dedicato un capitolo a parte|rendering]. -Mappatura su classi -=================== +Mappatura sulle classi +====================== -Torniamo al metodo `formSucceeded()`, che nel secondo parametro `$data` riceve i dati inviati come oggetto `ArrayHash`. Poiché si tratta di una classe generica, qualcosa come `stdClass`, ci mancherà una certa comodità nel lavorare con essa, come il suggerimento delle proprietà negli editor o l'analisi statica del codice. Questo potrebbe essere risolto avendo una classe specifica per ogni form, le cui proprietà rappresentano i singoli elementi. Ad esempio: +Torniamo al metodo `formSucceeded()`, che riceve nel secondo parametro `$data` i dati inviati, come oggetto `ArrayHash` (o `stdClass`). Poiché si tratta di una classe generica, simile a `stdClass`, lavorandoci ci mancano certe comodità, come il completamento automatico delle proprietà negli editor o l'analisi statica del codice. Lo si potrebbe risolvere avendo una classe specifica per ogni form, le cui proprietà rappresentino i singoli controlli. Per esempio: ```php class RegistrationFormData @@ -229,23 +233,23 @@ class RegistrationFormData } ``` -In alternativa, puoi utilizzare il costruttore: +In alternativa potete usare un costruttore: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Le proprietà della classe dati possono anche essere enum e verranno mappate automaticamente. .{data-version:3.2.4} +Le proprietà della classe dei dati possono essere anche enum, e verranno mappate automaticamente. .{data-version:3.2.4} -Come dire a Nette di restituirci i dati come oggetti di questa classe? Più facile di quanto pensiate. Basta semplicemente indicare la classe come tipo del parametro `$data` nel metodo handler: +Come diciamo a Nette di restituire i dati come oggetti di questa classe? Più facile di quanto pensiate. Basta indicare la classe come tipo del parametro `$data` nel metodo gestore: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void @@ -256,16 +260,18 @@ public function formSucceeded(Form $form, RegistrationFormData $data): void } ``` -Come tipo si può indicare anche `array` e allora i dati verranno passati come array. +Come tipo potete indicare anche `array`, e allora i dati verranno passati come array. -In modo analogo si può usare anche la funzione `getValues()`, alla quale passiamo il nome della classe o l'oggetto da idratare come parametro: +Allo stesso modo potete usare il metodo `getValues()`, passando come parametro il nome della classe o un oggetto da riempire: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Se i form formano una struttura multilivello composta da container, create una classe separata per ciascuno: +Se dovete leggere i valori prima che il form venga validato, di norma dentro un gestore `onValidate`, usate invece il metodo `getUntrustedValues()`. Accetta gli stessi parametri di `getValues()`, ma restituisce i valori inviati senza garantire che abbiano superato la validazione. + +Se i form hanno una struttura a più livelli composta da container, create una classe separata per ognuno: ```php $form = new Form; @@ -287,55 +293,58 @@ class RegistrationFormData } ``` -La mappatura riconoscerà quindi dal tipo della proprietà `$person` che deve mappare il container sulla classe `PersonFormData`. Se la proprietà contenesse un array di container, specificate il tipo `array` e passate la classe per la mappatura direttamente al container: +La mappatura deduce allora, dal tipo della proprietà `$person`, che deve mappare il container sulla classe `PersonFormData`. Se la proprietà dovesse contenere un array di container, indicate il tipo `array` e passate la classe da mappare direttamente al container: ```php $person->setMappedType(PersonFormData::class); ``` -Puoi farti generare il design della classe dati del form tramite il metodo `Nette\Forms\Blueprint::dataClass($form)`, che la stamperà nella pagina del browser. Basta quindi cliccare per selezionare il codice e copiarlo nel progetto. .{data-version:3.1.15} +Potete generare una proposta della classe dei dati del form con il metodo `Nette\Forms\Blueprint::dataClass($form)`, che la stampa nella pagina del browser. Poi vi basta selezionare con un clic e copiare il codice nel vostro progetto. .{data-version:3.1.15} -Più pulsanti -============ +Più pulsanti di invio +===================== -Se il form ha più di un pulsante, di solito abbiamo bisogno di distinguere quale di essi è stato premuto. Possiamo creare una funzione handler separata per ogni pulsante. La impostiamo come handler per l'[evento |nette:glossary#Eventi] `onClick`: +Se il form ha più di un pulsante, di norma dobbiamo distinguere quale sia stato premuto. Possiamo creare una funzione gestore separata per ogni pulsante. La impostiamo come gestore dell'[evento |nette:glossary#Eventi] `onClick`: ```php $form->addSubmit('save', 'Salva') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Elimina') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -Questi handler vengono chiamati solo nel caso di un form compilato validamente, proprio come nel caso dell'evento `onSuccess`. La differenza è che come primo parametro, invece del form, può essere passato il pulsante di invio, dipende dal tipo che specificate: +.{data-version:3.3.0} +Un gestore si può passare al pulsante anche direttamente, come terzo argomento del metodo `addSubmit()`. + +Questi gestori vengono chiamati solo se il form è compilato validamente (a meno che per il pulsante la validazione non sia disattivata), come per l'evento `onSuccess`. La differenza è che come primo parametro può essere passato l'oggetto del pulsante di invio invece del form, a seconda del tipo che dichiarate: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Quando il form viene inviato con il tasto <kbd>Invio</kbd>, viene considerato come se fosse stato inviato con il primo pulsante. +Quando il form viene inviato premendo il tasto <kbd>Invio</kbd>, viene trattato come se fosse stato inviato dal primo pulsante di invio. Evento onAnchor =============== -Quando nel metodo factory (come ad esempio `createComponentRegistrationForm`) costruiamo il form, questo non sa ancora se è stato inviato, né con quali dati. Ci sono però casi in cui abbiamo bisogno di conoscere i valori inviati, ad esempio se da essi dipende l'ulteriore aspetto del form, o se ne abbiamo bisogno per selectbox dipendenti, ecc. +Quando costruite un form in un metodo factory (come `createComponentRegistrationForm`), esso non sa ancora se sia stato inviato né con quali dati. Ci sono però casi in cui abbiamo bisogno di conoscere i valori inviati: magari l'aspetto del form dipende da essi, oppure servono per select box dipendenti e così via. -La parte del codice che costruisce il form può quindi essere fatta chiamare solo nel momento in cui è cosiddetto ancorato, cioè è già collegato al presenter e conosce i suoi dati inviati. Tale codice lo passiamo all'array `$onAnchor`: +Potete quindi far chiamare il codice che costruisce il form solo quando esso è "ancorato", cioè è già collegato al presenter e conosce i propri dati inviati. Collocate questo codice nell'array `$onAnchor`: ```php -$country = $form->addSelect('country', 'Stato:', $this->model->getCountries()); +$country = $form->addSelect('country', 'Paese:', $this->model->getCountries()); $city = $form->addSelect('city', 'Città:'); $form->onAnchor[] = function () use ($country, $city) { - // questa funzione viene chiamata solo quando il form sa se è stato inviato e con quali dati - // si può quindi usare il metodo getValue() + // questa funzione verrà chiamata quando il form conoscerà i dati con cui è stato inviato + // così potete usare il metodo getValue() $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; @@ -345,35 +354,34 @@ $form->onAnchor[] = function () use ($country, $city) { Protezione dalle vulnerabilità ============================== -Nette Framework pone grande enfasi sulla sicurezza e quindi si preoccupa meticolosamente della buona protezione dei form. Lo fa in modo completamente trasparente e non richiede alcuna impostazione manuale. +Nette Framework dà grande importanza alla sicurezza e si preoccupa quindi scrupolosamente della sicurezza dei form. Lo fa in modo completamente trasparente e senza richiedere alcuna configurazione manuale. -Oltre a proteggere i form dagli attacchi [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] e [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], implementa molte piccole misure di sicurezza a cui non dovete più pensare. +Oltre a proteggere i form da attacchi come il [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] e il [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], adotta molte piccole misure di sicurezza a cui non dovete più pensare. -Ad esempio, filtra tutti i caratteri di controllo dagli input e verifica la validità della codifica UTF-8, quindi i dati dal form saranno sempre puliti. Per i select box e i radio list, verifica che gli elementi selezionati fossero effettivamente tra quelli offerti e che non ci sia stata manomissione. Abbiamo già menzionato che per gli input di testo a riga singola rimuove i caratteri di fine riga che un utente malintenzionato potrebbe aver inviato. Per gli input multilinea, invece, normalizza i caratteri di fine riga. E così via. +Per esempio filtra dagli input tutti i caratteri di controllo e controlla la validità della codifica UTF-8, garantendo che i dati del form siano sempre puliti. Per i select box e le liste di radio button verifica che gli elementi selezionati fossero davvero tra quelli offerti e che non ci siano state falsificazioni. Abbiamo già detto che, per i campi di testo a riga singola, sostituisce con spazi i caratteri di fine riga che un aggressore potrebbe inviare. Per i campi su più righe normalizza i caratteri di fine riga. E così via. -Nette risolve per voi i rischi di sicurezza di cui molti programmatori non sospettano nemmeno l'esistenza. +Nette si occupa al posto vostro di rischi di sicurezza di cui molti programmatori non sospettano nemmeno l'esistenza. -L'attacco CSRF menzionato consiste nel fatto che un utente malintenzionato attira la vittima su una pagina che esegue discretamente nel browser della vittima una richiesta al server su cui la vittima è loggata, e il server crede che la richiesta sia stata eseguita dalla vittima di sua volontà. Pertanto, Nette impedisce l'invio di form POST da un dominio diverso. Se per qualche motivo volete disattivare la protezione e consentire l'invio del form da un dominio diverso, usate: +L'attacco CSRF menzionato consiste nell'attirare la vittima su una pagina che, in silenzio, esegue nel browser della vittima una richiesta al server su cui la vittima è connessa. Il server crede allora che la richiesta sia stata fatta volontariamente dalla vittima. Nette rifiuta quindi i form POST inviati da un'origine estranea; anche un sottodominio diverso dello stesso sito conta come estraneo. Se dovete permettere l'invio da un'altra origine, disattivate la protezione con: ```php -$form->allowCrossOrigin(); // ATTENZIONE! Disattiva la protezione! +$form->allowCrossOrigin(); // ATTENZIONE! Disattiva completamente la protezione! ``` -Questa protezione utilizza un cookie SameSite chiamato `_nss`. La protezione tramite cookie SameSite potrebbe non essere affidabile al 100%, quindi è consigliabile attivare anche la protezione tramite token: +Questo però disattiva la protezione per qualsiasi origine. Per permetterne solo alcune, disattivate la protezione e verificate voi stessi l'header `Origin` rispetto a un vostro elenco di origini consentite. -```php -$form->addProtection(); -``` +La protezione si basa sull'header `Sec-Fetch-Site` del browser (Fetch Metadata), che il browser invia automaticamente e che non si può falsificare nemmeno con una vulnerabilità XSS. Per i browser più vecchi, che non li supportano, vale un cookie SameSite di ripiego, che un'applicazione Nette imposta automaticamente. L'articolo [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] lo descrive in dettaglio. -Raccomandiamo di proteggere in questo modo i form nella parte amministrativa del sito, che modificano dati sensibili nell'applicazione. Il framework si difende dall'attacco CSRF generando e verificando un token di autorizzazione, che viene salvato nella sessione. Pertanto, è necessario avere una sessione aperta prima di visualizzare il form. Nella parte amministrativa del sito, di solito la sessione è già avviata a causa del login dell'utente. Altrimenti, avviate la sessione con il metodo `Nette\Http\Session::start()`. +.[note] +La protezione precedente, basata su un token di autorizzazione salvato nella sessione e attivata con `$form->addProtection()`, non serve più ed è deprecata dalla versione 3.3. -Stesso form in più presenter -============================ +Usare uno stesso form in più presenter +====================================== -Se avete bisogno di utilizzare lo stesso form in più presenter, vi consigliamo di creare una factory per esso, che poi passerete al presenter. Una posizione adatta per una tale classe è ad esempio la directory `app/Forms`. +Se dovete usare lo stesso form in più presenter, consigliamo di creare una factory, che poi iniettate nei presenter. Un posto adatto per una classe del genere è, per esempio, la directory `app/Forms`. -La classe factory può assomigliare a questo: +La classe factory potrebbe avere questo aspetto: ```php use Nette\Application\UI\Form; @@ -390,7 +398,7 @@ class SignInFormFactory } ``` -Chiediamo alla classe di produrre il form nel metodo factory per i componenti nel presenter: +Chiediamo alla classe di produrre il form nel metodo factory del componente, dentro il presenter: ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // possiamo modificare il form, qui ad esempio cambiamo l'etichetta sul pulsante + // possiamo modificare il form, qui per esempio cambiamo l'etichetta del pulsante $form['send']->setCaption('Continua'); - $form->onSuccess[] = [$this, 'signInFormSucceeded']; // e aggiungiamo l'handler + $form->onSuccess[] = $this->signInFormSuceeded(...); // e aggiungiamo il gestore return $form; } ``` -L'handler per l'elaborazione del form può anche essere fornito già dalla factory: +Il gestore dell'elaborazione del form può essere fornito anche dalla factory stessa: ```php use Nette\Application\UI\Form; @@ -421,11 +429,11 @@ class SignInFormFactory $form->addText('name', 'Nome:'); $form->addSubmit('send', 'Accedi'); $form->onSuccess[] = function (Form $form, $data): void { - // qui eseguiamo l'elaborazione del form + // qui elaboriamo il nostro form inviato }; return $form; } } ``` -Bene, abbiamo completato una rapida introduzione ai form in Nette. Provate a dare un'occhiata anche alla directory [examples |https://github.com/nette/forms/tree/master/examples] nella distribuzione, dove troverete ulteriore ispirazione. +Abbiamo così visto una rapida introduzione ai form in Nette. Provate a guardare nella directory degli [esempi |https://github.com/nette/forms/tree/master/examples] della distribuzione per altre idee. diff --git a/forms/it/rendering.texy b/forms/it/rendering.texy index df2923bfad..719c41c46f 100644 --- a/forms/it/rendering.texy +++ b/forms/it/rendering.texy @@ -1,35 +1,35 @@ -Renderizzazione dei Form -************************ +Rendering dei form +****************** -L'aspetto dei form può essere molto vario. In pratica, possiamo incontrare due estremi. Da un lato, c'è la necessità di renderizzare nell'applicazione una serie di form che sono visivamente simili come due gocce d'acqua, e apprezzeremo la facile renderizzazione senza template tramite `$form->render()`. Questo è solitamente il caso delle interfacce di amministrazione. +L'aspetto dei form può essere molto vario. Nella pratica possiamo incontrare due estremi. Da un lato c'è la necessità di disegnare in un'applicazione numerosi form visivamente identici, e apprezziamo il rendering semplice senza template con `$form->render()`. È il caso tipico delle interfacce di amministrazione. -Dall'altro lato, ci sono form diversi, dove vale la regola: ogni pezzo è un originale. La loro forma è meglio descritta dal linguaggio HTML nel template del form. E naturalmente, oltre ai due estremi menzionati, incontreremo molti form che si trovano da qualche parte nel mezzo. +Dall'altro lato ci sono i form più diversi, ognuno dei quali è unico. Il loro aspetto si descrive al meglio con l'HTML nel template del form. E naturalmente, oltre a questi due estremi, incontriamo molti form che stanno da qualche parte nel mezzo. -Renderizzazione tramite Latte -============================= +Rendering con Latte +=================== -Il [sistema di templating Latte |latte:] facilita enormemente la renderizzazione dei form e dei loro elementi. Prima mostreremo come renderizzare i form manualmente, elemento per elemento, ottenendo così il pieno controllo sul codice. Successivamente mostreremo come tale renderizzazione possa essere [automatizzata |#Renderizzazione automatica]. +Il sistema di template [Latte |latte:] semplifica notevolmente il rendering dei form e dei loro elementi. Mostreremo prima come disegnare i form manualmente, elemento per elemento, per avere il pieno controllo del codice. Più avanti mostreremo come questo rendering si possa [automatizzare |#Rendering automatico]. -Puoi farti generare il design del template Latte del form tramite il metodo `Nette\Forms\Blueprint::latte($form)`, che lo stamperà nella pagina del browser. Basta quindi cliccare per selezionare il codice e copiarlo nel progetto. .{data-version:3.1.15} +Potete generare il template Latte del form con il metodo `Nette\Forms\Blueprint::latte($form)`, che lo stampa nella pagina del browser. Poi vi basta selezionare il codice con un clic e copiarlo nel vostro progetto. .{data-version:3.1.15} `{control}` ----------- -Il modo più semplice per renderizzare un form è scrivere nel template: +Il modo più semplice di disegnare un form è scrivere nel template: ```latte {control signInForm} ``` -È possibile influenzare l'aspetto di un form così renderizzato configurando il [#Renderer] e i [singoli elementi |#Attributi HTML]. +Sull'aspetto del form disegnato si può influire configurando il [#Renderer] e i [singoli controlli |#Attributi HTML]. `n:name` -------- -La definizione del form nel codice PHP può essere collegata molto facilmente al codice HTML. Basta aggiungere gli attributi `n:name`. È così facile! +Collegare la definizione del form nel codice PHP con il codice HTML è estremamente semplice. Basta aggiungere gli attributi `n:name`. Tutto qui! ```php protected function createComponentSignInForm(): Form @@ -45,7 +45,7 @@ protected function createComponentSignInForm(): Form ```latte <form n:name=signInForm class=form> <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> + <label n:name=username>Nome utente: <input n:name=username size=20 autofocus></label> </div> <div> <label n:name=password>Password: <input n:name=password></label> @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Avete il pieno controllo sull'aspetto del codice HTML risultante. Se utilizzate l'attributo `n:name` sugli elementi `<select>`, `<button>` o `<textarea>`, il loro contenuto interno verrà completato automaticamente. Il tag `<form n:name>` crea inoltre una variabile locale `$form` con l'oggetto del form disegnato e il tag di chiusura `</form>` renderizza tutti gli elementi nascosti non renderizzati (lo stesso vale anche per `{form} ... {/form}`). +Avete il pieno controllo sull'aspetto del codice HTML risultante. Se usate l'attributo `n:name` con gli elementi `<select>`, `<button>` o `<textarea>`, il loro contenuto interno viene riempito automaticamente. Inoltre il tag `<form n:name>` crea una variabile locale `$form` che contiene l'oggetto del form disegnato, e il tag di chiusura `</form>` disegna gli eventuali controlli nascosti non ancora disegnati (lo stesso vale per `{form} ... {/form}`). -Non dobbiamo però dimenticare di renderizzare eventuali messaggi di errore. Sia quelli aggiunti ai singoli elementi con il metodo `addError()` (tramite `{inputError}`), sia quelli aggiunti direttamente al form (restituiti da `$form->getOwnErrors()`): +Non dobbiamo però dimenticare di disegnare gli eventuali messaggi di errore. Si tratta degli errori aggiunti ai singoli controlli con il metodo `addError()` (disegnati con `{inputError}`) e degli errori aggiunti direttamente al form (restituiti da `$form->getOwnErrors()`): ```latte <form n:name=signInForm class=form> @@ -67,7 +67,7 @@ Non dobbiamo però dimenticare di renderizzare eventuali messaggi di errore. Sia </ul> <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> + <label n:name=username>Nome utente: <input n:name=username size=20 autofocus></label> <span class=error n:ifcontent>{inputError username}</span> </div> <div> @@ -80,7 +80,7 @@ Non dobbiamo però dimenticare di renderizzare eventuali messaggi di errore. Sia </form> ``` -Elementi del form più complessi, come RadioList o CheckboxList, possono essere renderizzati in questo modo per singoli elementi: +I controlli più complessi, come RadioList o CheckboxList, si possono disegnare elemento per elemento così: ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ Elementi del form più complessi, come RadioList o CheckboxList, possono essere `{label}` `{input}` ------------------- -Non vuoi pensare per ogni elemento quale elemento HTML usare nel template, se `<input>`, `<textarea>`, ecc.? La soluzione è il tag universale `{input}`: +Preferite non pensare a quale elemento HTML usare per ogni controllo nel template, se `<input>`, `<textarea>` o altro? La soluzione è il tag universale `{input}`: ```latte <form n:name=signInForm class=form> @@ -101,7 +101,7 @@ Non vuoi pensare per ogni elemento quale elemento HTML usare nel template, se `< </ul> <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} + {label username}Nome utente: {input username, size: 20, autofocus: true}{/label} {inputError username} </div> <div> @@ -114,9 +114,9 @@ Non vuoi pensare per ogni elemento quale elemento HTML usare nel template, se `< </form> ``` -Se il form utilizza un traduttore, il testo all'interno dei tag `{label}` verrà tradotto. +Se il form usa un traduttore, le etichette disegnate a partire dalla definizione del form (per esempio `{label username /}`) vengono tradotte. Il testo scritto direttamente tra i tag `{label}` e `{/label}` no. -Anche in questo caso, elementi del form più complessi, come RadioList o CheckboxList, possono essere renderizzati per singoli elementi: +Anche qui i controlli più complessi, come RadioList o CheckboxList, si possono disegnare elemento per elemento: ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ Anche in questo caso, elementi del form più complessi, come RadioList o Checkbo {/foreach} ``` -Per renderizzare solo l'`<input>` nell'elemento Checkbox, usa `{input myCheckbox:}`. Gli attributi HTML in questo caso vanno sempre separati da virgola `{input myCheckbox:, class: required}`. +Per disegnare solo l'`<input>` di un controllo Checkbox, usate `{input myCheckbox:}`. In tal caso separate sempre gli attributi HTML con una virgola: `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Stampa il messaggio di errore per l'elemento del form, se ne ha uno. Il messaggio viene solitamente racchiuso in un elemento HTML per lo styling. Evitare la renderizzazione di un elemento vuoto, se il messaggio non c'è, è possibile elegantemente tramite `n:ifcontent`: +Mostra il messaggio di errore di un controllo del form, se esiste. Il messaggio viene di norma racchiuso in un elemento HTML per lo stile. Si può impedire elegantemente il rendering di un elemento vuoto, quando non c'è alcun messaggio, con `n:ifcontent`: ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -Possiamo verificare la presenza di un errore con il metodo `hasErrors()` e in base a ciò impostare una classe sull'elemento genitore: +Possiamo verificare la presenza di un errore con il metodo `hasErrors()` e impostare di conseguenza la classe dell'elemento genitore: ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ Possiamo verificare la presenza di un errore con il metodo `hasErrors()` e in ba `{form}` -------- -I tag `{form signInForm}...{/form}` sono un'alternativa a `<form n:name="signInForm">...</form>`. +I tag `{form signInForm}...{/form}` sono un'alternativa a `<form n:name="signInForm">...</form>`. Separate gli eventuali argomenti dal nome con una virgola: `{form signInForm, class: foo}`. +.{data-version:3.3.0} +La parola chiave `scope` posta prima del nome mette il form solo sulla pila (così che `{input}`, `{label}` ecc. si leghino a esso), ma non disegna il tag `<form>`. È comoda per disegnare una parte di un form, per esempio in uno snippet. Se un form è già attivo, il nome viene risolto relativamente a esso, quindi `{form scope}` sostituisce anche `{formContainer}`: -Renderizzazione automatica --------------------------- +```latte +{form scope signInForm} + {input username} +{/form} +``` -Grazie ai tag `{input}` e `{label}` possiamo facilmente creare un template generico per qualsiasi form. Itererà e renderizzerà gradualmente tutti i suoi elementi, ad eccezione degli elementi nascosti, che verranno renderizzati automaticamente alla chiusura del form con il tag `</form>`. Il nome del form da renderizzare sarà atteso nella variabile `$form`. +.{data-version:3.3.0} +La parola chiave `detached` disegna un `<form></form>` vuoto e collega a esso ogni controllo tramite l'attributo HTML `form`. Questo vi permette di collocare un form dentro un altro form, cosa che l'HTML altrimenti vieta. Il form staccato deve avere un `id` HTML, che viene generato automaticamente quando gli date un nome (come `outerForm` qui sotto): + +```latte +{form detached outerForm} + ... +{/form} +``` + + +Rendering automatico +-------------------- + +Grazie ai tag `{input}` e `{label}` possiamo creare facilmente un template generico per qualsiasi form. Scorrerà e disegnerà tutti i suoi controlli, tranne quelli nascosti, che vengono disegnati automaticamente alla chiusura del form con il tag `</form>`. Si aspetta nella variabile `$form` il nome del form da disegnare. ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ Grazie ai tag `{input}` e `{label}` possiamo facilmente creare un template gener </form> ``` -I tag di coppia auto-chiudenti `{label .../}` utilizzati visualizzano le etichette provenienti dalla definizione del form nel codice PHP. +I tag di tipo pari autochiudenti `{label .../}` usati qui mostrano le etichette provenienti dalla definizione del form nel codice PHP. -Salvate questo template generico ad esempio nel file `basic-form.latte` e per renderizzare il form basta includerlo e passare il nome (o l'istanza) del form al parametro `$form`: +Salvate questo template generico, per esempio, nel file `basic-form.latte`. Per disegnare il form basta includerlo e passare il nome del form (o l'istanza) al parametro `$form`: ```latte {include basic-form.latte, form: signInForm} ``` -Se durante la renderizzazione di un particolare form voleste intervenire sulla sua forma e ad esempio renderizzare un elemento diversamente, allora il modo più semplice è preparare nel template dei blocchi che potranno essere successivamente sovrascritti. I blocchi possono avere anche [nomi dinamici |latte:template-inheritance#Nomi dinamici dei blocchi], quindi è possibile inserirvi anche il nome dell'elemento renderizzato. Ad esempio: +Se volete modificare l'aspetto di un determinato form durante il rendering, magari disegnando un controllo in modo diverso, il modo più semplice è preparare nel template dei blocchi che si possano poi sovrascrivere. I blocchi possono avere anche [nomi dinamici |latte:template-inheritance#Nomi dinamici dei blocchi], il che vi permette di inserirvi il nome del controllo disegnato. Per esempio: ```latte ... @@ -189,7 +207,7 @@ Se durante la renderizzazione di un particolare form voleste intervenire sulla s ... ``` -Per l'elemento ad esempio `username` verrà così creato il blocco `input-username`, che può essere facilmente sovrascritto utilizzando il tag [{embed} |latte:template-inheritance#Ereditarietà unitaria embed]: +Per un controllo chiamato per esempio `username`, questo crea il blocco `input-username`, che si può facilmente sovrascrivere con il tag [{embed} |latte:template-inheritance#Ereditarietà delle unità]: ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ Per l'elemento ad esempio `username` verrà così creato il blocco `input-userna {/embed} ``` -In alternativa, l'intero contenuto del template `basic-form.latte` può essere [definito |latte:template-inheritance#Definizioni define] come blocco, incluso il parametro `$form`: +In alternativa, l'intero contenuto del template `basic-form.latte` si può [definire |latte:template-inheritance#Definizioni] come blocco, parametro `$form` compreso: ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ In alternativa, l'intero contenuto del template `basic-form.latte` può essere [ {/define} ``` -Grazie a ciò, la sua chiamata sarà leggermente più semplice: +Questo ne rende leggermente più semplice la chiamata: ```latte {embed basic-form, signInForm} @@ -219,31 +237,31 @@ Grazie a ciò, la sua chiamata sarà leggermente più semplice: {/embed} ``` -Il blocco basta importarlo in un unico punto, all'inizio del template di layout: +Il blocco va importato in un solo punto, all'inizio del template di layout: ```latte {import basic-form.latte} ``` -Casi speciali -------------- +Casi particolari +---------------- -Se hai bisogno di renderizzare solo la parte interna del form senza i tag HTML `<form>`, ad esempio quando invii snippet, nascondili usando l'attributo `n:tag-if`: +Se dovete disegnare solo la parte interna del form, senza i tag HTML `<form>`, per esempio inviando degli snippet, nascondeteli con l'attributo `n:tag-if`: ```latte <form n:name=signInForm n:tag-if=false> <div> - <label n:name=username>Username: <input n:name=username></label> + <label n:name=username>Nome utente: <input n:name=username></label> {inputError username} </div> </form> ``` -Con la renderizzazione degli elementi all'interno del contenitore del form aiuta il tag `{formContainer}`. +Il tag `{formContainer}`, oppure il più recente [`{form scope}` |#{form}], aiuta a disegnare i controlli dentro un container del form. ```latte -<p>Quali notizie desideri ricevere:</p> +<p>Quali notizie volete ricevere:</p> {formContainer emailNews} <ul> @@ -254,42 +272,42 @@ Con la renderizzazione degli elementi all'interno del contenitore del form aiuta ``` -Renderizzazione senza Latte -=========================== +Rendering senza Latte +===================== -Il modo più semplice per renderizzare un form è chiamare: +Il modo più semplice di disegnare un form è chiamare: ```php $form->render(); ``` -È possibile influenzare l'aspetto di un form così renderizzato configurando il [#Renderer] e i [singoli elementi |#Attributi HTML]. +Sull'aspetto del form disegnato si può influire configurando il [#Renderer] e i [singoli controlli |#Attributi HTML]. -Renderizzazione manuale ------------------------ +Rendering manuale +----------------- -Ogni elemento del form dispone di metodi che generano il codice HTML del campo del form e dell'etichetta. Possono restituirlo sia come stringa che come oggetto [Nette\Utils\Html |utils:html-elements]: +Ogni controllo del form ha metodi che generano il codice HTML del campo e della sua etichetta. Possono restituirlo come stringa oppure come oggetto [Nette\Utils\Html |utils:html-elements]: -- `getControl(): Html|string` restituisce il codice HTML dell'elemento +- `getControl(): Html|string` restituisce il codice HTML del controllo - `getLabel($caption = null): Html|string|null` restituisce il codice HTML dell'etichetta, se esiste -Il form può quindi essere renderizzato per singoli elementi: +Questo permette di disegnare il form elemento per elemento: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,21 +315,21 @@ Il form può quindi essere renderizzato per singoli elementi: <?php $form->render('end') ?> ``` -Mentre per alcuni elementi `getControl()` restituisce un singolo elemento HTML (es. `<input>`, `<select>`, ecc.), per altri restituisce un intero pezzo di codice HTML (CheckboxList, RadioList). In tal caso, potete utilizzare metodi che generano singoli input ed etichette, per ogni elemento separatamente: +Mentre per alcuni controlli `getControl()` restituisce un unico elemento HTML (per esempio `<input>`, `<select>` ecc.), per altri restituisce un intero pezzo di codice HTML (CheckboxList, RadioList). In questi casi potete usare i metodi che generano separatamente i singoli input e le singole etichette: -- `getControlPart($key = null): ?Html` restituisce il codice HTML di un singolo elemento -- `getLabelPart($key = null): ?Html` restituisce il codice HTML dell'etichetta di un singolo elemento +- `getControlPart($key = null): Html` restituisce il codice HTML di un singolo elemento +- `getLabelPart($key = null): Html` restituisce il codice HTML dell'etichetta di un singolo elemento .[note] -Questi metodi hanno per motivi storici il prefisso `get`, ma sarebbe stato meglio `generate`, perché ad ogni chiamata creano e restituiscono un nuovo elemento `Html`. +Questi metodi hanno il prefisso `get` per motivi storici, ma `generate` sarebbe più appropriato, perché a ogni chiamata creano e restituiscono un nuovo elemento `Html`. Renderer ======== -È un oggetto che si occupa della renderizzazione del form. Può essere impostato con il metodo `$form->setRenderer`. Gli viene passato il controllo quando viene chiamato il metodo `$form->render()`. +È l'oggetto che si occupa di disegnare il form. Si imposta con il metodo `$form->setRenderer()`. Il controllo gli viene passato quando viene chiamato il metodo `$form->render()`. -Se non impostiamo un renderer personalizzato, verrà utilizzato il renderer predefinito [api:Nette\Forms\Rendering\DefaultFormRenderer]. Questo renderizza gli elementi del form sotto forma di tabella HTML. L'output è simile a questo: +Se non impostiamo un renderer personalizzato, verrà usato il renderer predefinito [api:Nette\Forms\Rendering\DefaultFormRenderer]. Esso disegna i controlli del form in una tabella HTML. Il risultato ha questo aspetto: ```latte <table> @@ -332,11 +350,11 @@ Se non impostiamo un renderer personalizzato, verrà utilizzato il renderer pred ... ``` -Se utilizzare o meno una tabella per la struttura del form è discutibile e molti web designer preferiscono un markup diverso. Ad esempio, una lista di definizioni. Riconfigureremo quindi `DefaultFormRenderer` in modo che renderizzi il form sotto forma di lista. La configurazione viene eseguita modificando l'array [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Il primo indice rappresenta sempre l'area e il secondo il suo attributo. Le singole aree sono illustrate nell'immagine: +Se usare una tabella per la struttura del form è discutibile, e molti web designer preferiscono un markup diverso, per esempio una lista di definizioni. Riconfigureremo quindi `DefaultFormRenderer` perché disegni il form come elenco. La configurazione avviene modificando l'array [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Il primo indice rappresenta sempre un'area, il secondo un suo attributo. Le singole aree sono mostrate nell'immagine: -[* defaultformrenderer.webp *] +[* form-areas-en.webp *] -Standardmente il gruppo di elementi `controls` è avvolto da una tabella `<table>`, ogni `pair` rappresenta una riga della tabella `<tr>` e la coppia `label` e `control` sono le celle `<th>` e `<td>`. Ora cambieremo gli elementi contenitori. L'area `controls` la inseriremo nel contenitore `<dl>`, l'area `pair` la lasceremo senza contenitore, `label` la inseriremo in `<dt>` e infine `control` lo avvolgeremo con i tag `<dd>`: +Per impostazione predefinita il gruppo `controls` è racchiuso in `<table>`, ogni `pair` rappresenta una riga della tabella `<tr>` e la coppia `label` e `control` sono le celle `<th>` e `<td>`. Cambieremo ora gli elementi che racchiudono. Collocheremo l'area `controls` in un container `<dl>`, lasceremo l'area `pair` senza container, metteremo `label` in `<dt>` e infine racchiuderemo `control` nei tag `<dd>`: ```php $renderer = $form->getRenderer(); @@ -348,7 +366,7 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -Il risultato è questo codice HTML: +Ne risulta il codice HTML seguente: ```latte <dl> @@ -367,55 +385,55 @@ Il risultato è questo codice HTML: </dl> ``` -Nell'array wrappers è possibile influenzare tutta una serie di altri attributi: +L'array wrappers permette di influire su molti altri attributi: -- aggiungere classi CSS a singoli tipi di elementi del form -- distinguere con classi CSS le righe pari e dispari -- distinguere visivamente gli elementi obbligatori e facoltativi -- determinare se i messaggi di errore verranno visualizzati direttamente accanto agli elementi o sopra il form +- aggiungere classi CSS ai singoli tipi di controllo +- distinguere le righe pari e dispari con classi CSS +- distinguere visivamente gli elementi obbligatori da quelli facoltativi +- stabilire se i messaggi di errore vengano mostrati direttamente accanto ai controlli oppure sopra il form -Options +Opzioni ------- -Il comportamento del Renderer può essere controllato anche impostando *options* sui singoli elementi del form. In questo modo è possibile impostare una descrizione che verrà visualizzata accanto al campo di input: +Il comportamento del Renderer si può governare anche impostando delle *opzioni* sui singoli controlli. Così potete impostare una descrizione che compare accanto al campo: ```php $form->addText('phone', 'Numero:') - ->setOption('description', 'Questo numero rimarrà nascosto'); + ->setOption('description', 'Questo numero resterà nascosto'); ``` -Se vogliamo inserirvi contenuto HTML, utilizziamo la classe [Html |utils:html-elements] +Se vogliamo inserirvi contenuto HTML, usiamo la classe [Html |utils:html-elements]: ```php use Nette\Utils\Html; -$form->addText('phone', 'Numero:') +$form->addText('phone', 'Telefono:') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Condizioni di conservazione del tuo numero</a>') + ->setHtml('<a href="...">Condizioni del servizio.</a>') ); ``` .[tip] -L'elemento Html può essere utilizzato anche al posto dell'etichetta: `$form->addCheckbox('conditions', $label)`. +Un elemento Html si può usare anche al posto di un'etichetta: `$form->addCheckbox('conditions', $label)`. -Raggruppamento degli elementi ------------------------------ +Raggruppare i controlli +----------------------- -Il Renderer consente di raggruppare gli elementi in gruppi visivi (fieldset): +Il Renderer permette di raggruppare i controlli in gruppi visivi (fieldset): ```php $form->addGroup('Dati personali'); ``` -Dopo aver creato un nuovo gruppo, questo diventa attivo e ogni elemento appena aggiunto viene aggiunto anche ad esso. Quindi il form può essere costruito in questo modo: +Dopo aver creato un nuovo gruppo, esso diventa attivo e ogni controllo appena aggiunto vi viene aggiunto. Il form si può quindi costruire così: ```php $form = new Form; $form->addGroup('Dati personali'); -$form->addText('name', 'Il tuo nome:'); -$form->addInteger('age', 'La tua età:'); +$form->addText('name', 'Il vostro nome:'); +$form->addInteger('age', 'La vostra età:'); $form->addEmail('email', 'Email:'); $form->addGroup('Indirizzo di spedizione'); @@ -425,44 +443,44 @@ $form->addText('city', 'Città:'); $form->addSelect('country', 'Paese:', $countries); ``` -Il Renderer renderizza prima i gruppi e solo dopo gli elementi che non appartengono a nessun gruppo. +Il renderer disegna prima i gruppi e poi i controlli che non appartengono ad alcun gruppo. -Supporto per Bootstrap ----------------------- +Supporto di Bootstrap +--------------------- -[Negli esempi |https://github.com/nette/forms/tree/master/examples] troverete esempi su come configurare il Renderer per [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] e [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] +Nella [directory degli esempi |https://github.com/nette/forms/tree/master/examples] trovate esempi che mostrano come configurare il Renderer per [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] e [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. Attributi HTML ============== -Per impostare attributi HTML arbitrari degli elementi del form, utilizziamo il metodo `setHtmlAttribute(string $name, $value = true)`: +Per impostare attributi HTML qualsiasi sui controlli del form, usate il metodo `setHtmlAttribute(string $name, $value = true)`: ```php $form->addInteger('number', 'Numero:') ->setHtmlAttribute('class', 'big-number'); $form->addSelect('rank', 'Ordina per:', ['prezzo', 'nome']) - ->setHtmlAttribute('onchange', 'submit()'); // invia alla modifica + ->setHtmlAttribute('onchange', 'submit()'); // invia il form al cambiamento -// Per impostare gli attributi del <form> stesso +// per impostare gli attributi dell'elemento <form> stesso $form->setHtmlAttribute('id', 'myForm'); ``` -Specifica del tipo di elemento: +Indicare il tipo del controllo: ```php -$form->addText('tel', 'Il tuo telefono:') +$form->addText('tel', 'Il vostro telefono:') ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'scrivi il telefono'); + ->setHtmlAttribute('placeholder', 'Inserite il vostro telefono'); ``` .[warning] -L'impostazione del tipo e di altri attributi serve solo a scopi visivi. La verifica della correttezza degli input deve avvenire lato server, cosa che si garantisce scegliendo l'[elemento del form |controls] appropriato e indicando le [regole di validazione |validation]. +Impostare il tipo e gli altri attributi ha solo scopo visivo. La verifica della correttezza dell'input deve avvenire sul lato server, cosa che garantite scegliendo un [controllo |controls] appropriato e indicando le [regole di validazione |validation]. -Alle singole voci nelle liste radio o checkbox possiamo impostare un attributo HTML con valori diversi per ciascuna di esse. Notate i due punti dopo `style:`, che assicurano la scelta del valore in base alla chiave: +Per i singoli elementi delle liste di radio button o di checkbox possiamo impostare un attributo HTML con valori diversi per ciascuno. Notate i due punti dopo `style:`, che fanno sì che il valore venga scelto in base alla chiave: ```php $colors = ['r' => 'rosso', 'g' => 'verde', 'b' => 'blu']; @@ -471,7 +489,7 @@ $form->addCheckboxList('colors', 'Colori:', $colors) ->setHtmlAttribute('style:', $styles); ``` -Stampa: +Disegna: ```latte <label><input type="checkbox" name="colors[]" style="background:red" value="r">rosso</label> @@ -479,14 +497,14 @@ Stampa: <label><input type="checkbox" name="colors[]" value="b">blu</label> ``` -Per impostare attributi logici, come `readonly`, possiamo usare la scrittura con il punto interrogativo: +Per impostare attributi booleani, come `readonly`, possiamo usare la notazione con il punto interrogativo: ```php $form->addCheckboxList('colors', 'Colori:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // per più chiavi usa un array, es. ['r', 'g'] + ->setHtmlAttribute('readonly?', 'r'); // per più chiavi usate un array, per esempio ['r', 'g'] ``` -Stampa: +Disegna: ```latte <label><input type="checkbox" name="colors[]" readonly value="r">rosso</label> @@ -494,14 +512,14 @@ Stampa: <label><input type="checkbox" name="colors[]" value="b">blu</label> ``` -Nel caso dei selectbox, il metodo `setHtmlAttribute()` imposta gli attributi dell'elemento `<select>`. Se vogliamo impostare attributi ai singoli `<option>`, usiamo il metodo `setOptionAttribute()`. Funzionano anche le scritture con i due punti e il punto interrogativo indicate sopra: +Per i select box il metodo `setHtmlAttribute()` imposta gli attributi dell'elemento `<select>`. Se vogliamo impostare gli attributi dei singoli elementi `<option>`, usiamo il metodo `setOptionAttribute()`. Anche qui funzionano le notazioni con i due punti e con il punto interrogativo appena viste: ```php $form->addSelect('colors', 'Colori:', $colors) ->setOptionAttribute('style:', $styles); ``` -Stampa: +Disegna: ```latte <select name="colors"> @@ -515,7 +533,7 @@ Stampa: Prototipi --------- -Un modo alternativo per impostare gli attributi HTML consiste nel modificare il prototipo da cui viene generato l'elemento HTML. Il prototipo è un oggetto `Html` e viene restituito dal metodo `getControlPrototype()`: +Un modo alternativo di impostare gli attributi HTML è modificare il modello da cui viene generato l'elemento HTML. Il modello è un oggetto `Html` ed è restituito dal metodo `getControlPrototype()`: ```php $input = $form->addInteger('number', 'Numero:'); @@ -523,14 +541,14 @@ $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -In questo modo è possibile modificare anche il prototipo dell'etichetta, restituito da `getLabelPrototype()`: +Anche il modello dell'etichetta, restituito da `getLabelPrototype()`, si può modificare in questo modo: ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -Per gli elementi Checkbox, CheckboxList e RadioList è possibile influenzare il prototipo dell'elemento che avvolge l'intero elemento. Viene restituito da `getContainerPrototype()`. Nello stato predefinito è un elemento "vuoto", quindi non viene renderizzato nulla, ma impostandogli un nome, verrà renderizzato: +Per i controlli Checkbox, CheckboxList e RadioList potete influire sul modello dell'elemento che racchiude l'intero controllo. Lo restituisce `getContainerPrototype()`. Per impostazione predefinita è un elemento "vuoto", quindi non viene disegnato nulla, ma dandogli un nome verrà disegnato: ```php $input = $form->addCheckbox('send'); @@ -541,49 +559,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -Nel caso di CheckboxList e RadioList è possibile influenzare anche il prototipo del separatore delle singole voci, restituito dal metodo `getSeparatorPrototype()`. Nello stato predefinito è l'elemento `<br>`. Se lo si modifica in un elemento di coppia, avvolgerà le singole voci invece di separarle. Inoltre, è possibile influenzare il prototipo dell'elemento HTML dell'etichetta delle singole voci, restituito da `getItemLabelPrototype()`. +Nel caso di CheckboxList e RadioList potete influire anche sul modello del separatore tra i singoli elementi, restituito dal metodo `getSeparatorPrototype()`. Per impostazione predefinita è l'elemento `<br>`. Se lo cambiate in un elemento di tipo pari, racchiuderà i singoli elementi invece di separarli. Potete inoltre influire sul modello dell'elemento HTML delle etichette dei singoli elementi, restituito da `getItemLabelPrototype()`. Traduzione ========== -Se programmi un'applicazione multilingue, probabilmente avrai bisogno di renderizzare il form in diverse versioni linguistiche. Nette Framework definisce a tale scopo un'interfaccia per la traduzione [api:Nette\Localization\Translator]. In Nette non c'è un'implementazione predefinita, puoi scegliere in base alle tue esigenze tra diverse soluzioni pronte, che trovi su [Componette |https://componette.org/search/localization]. Nella loro documentazione scoprirai come configurare il traduttore. +Se sviluppate un'applicazione multilingue, probabilmente avrete bisogno di disegnare il form in versioni linguistiche diverse. Nette Framework definisce a questo scopo un'interfaccia di traduzione: [api:Nette\Localization\Translator]. 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. -I form supportano la stampa di testi tramite il traduttore. Glielo passiamo tramite il metodo `setTranslator()`: +I form supportano la stampa dei testi tramite il traduttore. Lo passiamo con il metodo `setTranslator()`: ```php $form->setTranslator($translator); ``` -Da questo momento, non solo tutte le etichette, ma anche tutti i messaggi di errore o le voci dei select box verranno tradotti in un'altra lingua. +Da questo momento verranno tradotte nella lingua di destinazione non solo tutte le etichette, ma anche tutti i messaggi di errore, gli elementi dei select box e i placeholder dei campi. -Per i singoli elementi del form è possibile impostare un traduttore diverso o disattivare completamente la traduzione con il valore `null`: +È possibile impostare un traduttore diverso per i singoli controlli del form oppure disattivare completamente la traduzione impostando il valore a `null`: ```php $form->addSelect('carModel', 'Modello:', $cars) ->setTranslator(null); ``` -Alle [regole di validazione |validation] vengono passati al traduttore anche parametri specifici, ad esempio per la regola: +Per le [regole di validazione |validation] al traduttore vengono passati anche i parametri specifici. Per esempio, per la regola: ```php $form->addPassword('password', 'Password:') - ->addRule($form::MinLength, 'La password deve avere almeno %d caratteri', 8); + ->addRule($form::MinLength, 'La password deve essere lunga almeno %d caratteri', 8); ``` -viene chiamato il traduttore con questi parametri: +il traduttore viene chiamato con questi parametri: ```php -$translator->translate('La password deve avere almeno %d caratteri', 8); +$translator->translate('La password deve essere lunga almeno %d caratteri', 8); ``` -e quindi può scegliere la forma plurale corretta della parola `caratteri` in base al numero. +e può quindi scegliere la forma plurale corretta della parola `caratteri` in base al numero. Evento onRender =============== -Poco prima che il form venga renderizzato, possiamo far chiamare il nostro codice. Questo può, ad esempio, aggiungere classi HTML agli elementi del form per una corretta visualizzazione. Aggiungiamo il codice all'array `onRender`: +Poco prima che il form venga disegnato, possiamo far eseguire il nostro codice. Questo codice può, per esempio, aggiungere classi HTML ai controlli del form per una corretta visualizzazione. Aggiungiamo il codice all'array `onRender`: ```php $form->onRender[] = function ($form) { diff --git a/forms/it/standalone.texy b/forms/it/standalone.texy index 945581ef15..96f11b5b7a 100644 --- a/forms/it/standalone.texy +++ b/forms/it/standalone.texy @@ -1,16 +1,22 @@ -Form Usati Autonomamente -************************ +Form autonomi +************* .[perex] -Nette Forms semplifica enormemente la creazione e l'elaborazione dei form web. Potete usarli nelle vostre applicazioni in modo completamente autonomo, senza il resto del framework, come mostreremo in questo capitolo. +Nette Forms semplifica enormemente la creazione e l'elaborazione dei form web. Potete usarli nelle vostre applicazioni in modo completamente autonomo, senza il resto del framework, come mostrato in questo capitolo. -Tuttavia, se utilizzate Nette Application e i presenter, la guida per l'[utilizzo nei presenter|in-presenter] è quella che fa per voi. +Se però usate Nette Application e i presenter, c'è una guida dedicata a voi: [form nei presenter |in-presenter]. -Primo Form -========== +Il primo form +============= -Proviamo a scrivere un semplice form di registrazione. Il suo codice sarà il seguente ("codice completo":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): +Prima di iniziare, installate il pacchetto con [Composer |best-practices:composer]: + +```shell +composer require nette/forms +``` + +Proviamo a scrivere un semplice form di registrazione. Il suo codice sarà questo ("codice completo":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php use Nette\Forms\Form; @@ -21,23 +27,23 @@ $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Registrati'); ``` -Possiamo renderizzarlo molto facilmente: +E disegniamolo in modo semplicissimo: ```php $form->render(); ``` -e nel browser apparirà così: +Il risultato nel browser dovrebbe avere questo aspetto: -[* form-cs.webp *] +[* form-en.webp *] -Il form è un oggetto della classe `Nette\Forms\Form` (la classe `Nette\Application\UI\Form` viene utilizzata nei presenter). Abbiamo aggiunto ad esso gli elementi chiamati nome, password e il pulsante di invio. +Il form è un oggetto della classe `Nette\Forms\Form` (nei presenter si usa la classe `Nette\Application\UI\Form`). Vi abbiamo aggiunto i controlli chiamati 'name', 'password' e un pulsante di invio. -Ora diamo vita al form. Interrogando `$form->isSuccess()` scopriremo se il form è stato inviato e se è stato compilato validamente. Se sì, stamperemo i dati. Quindi, dopo la definizione del form, aggiungiamo: +Diamo ora vita al form. Interrogando `$form->isSuccess()` scopriamo se il form è stato inviato e se è stato compilato in modo valido. In tal caso stamperemo i dati. Dopo la definizione del form aggiungete: ```php if ($form->isSuccess()) { - echo 'Il form è stato compilato correttamente e inviato'; + echo 'Il form è stato compilato e inviato con successo'; $data = $form->getValues(); // $data->name contiene il nome // $data->password contiene la password @@ -45,32 +51,32 @@ if ($form->isSuccess()) { } ``` -Il metodo `getValues()` restituisce i dati inviati sotto forma di oggetto [ArrayHash |utils:arrays#ArrayHash]. Vedremo [più tardi |#Mapping su Classi] come cambiare questo comportamento. L'oggetto `$data` contiene le chiavi `name` e `password` con i dati inseriti dall'utente. +Il metodo `getValues()` restituisce i dati inviati come oggetto [ArrayHash |utils:arrays#ArrayHash]. Mostreremo [più avanti |#Mappatura sulle classi] come cambiarlo. L'oggetto `$data` contiene le chiavi `name` e `password` con i dati inseriti dall'utente. -Di solito, inviamo i dati direttamente per un'ulteriore elaborazione, che potrebbe essere, ad esempio, l'inserimento nel database. Tuttavia, durante l'elaborazione può verificarsi un errore, ad esempio il nome utente è già occupato. In tal caso, restituiamo l'errore al form utilizzando `addError()` e lo facciamo renderizzare di nuovo, insieme al messaggio di errore. +Di solito inviamo i dati direttamente a un'ulteriore elaborazione, per esempio per inserirli in un database. Durante l'elaborazione può però verificarsi un errore, per esempio se il nome utente è già occupato. In tal caso passiamo l'errore al form con `addError()` e lo facciamo disegnare di nuovo, insieme al messaggio di errore. ```php -$form->addError('Siamo spiacenti, questo nome utente è già in uso.'); +$form->addError('Spiacenti, questo nome utente è già occupato.'); ``` -Dopo aver elaborato il form, reindirizziamo a un'altra pagina. Ciò impedisce l'invio involontario ripetuto del form tramite il pulsante *aggiorna*, *indietro* o spostandosi nella cronologia del browser. +Dopo aver elaborato il form reindirizziamo alla pagina successiva. Questo impedisce che il form venga reinviato involontariamente cliccando i pulsanti *aggiorna* o *indietro*, oppure navigando nella cronologia del browser. -Il form viene inviato per default con il metodo POST alla stessa pagina. Entrambi possono essere modificati: +Per impostazione predefinita il form viene inviato con il metodo POST alla stessa pagina. Entrambe le cose si possono cambiare: ```php $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -E questo è tutto :-) Abbiamo un form funzionante e perfettamente [sicuro |#Protezione dalle Vulnerabilità]. +E in sostanza è tutto :-) Abbiamo un form funzionante e perfettamente [protetto |#Protezione dalle vulnerabilità]. -Provate ad aggiungere anche altri [elementi del form|controls]. +Provate ad aggiungere anche altri [controlli |controls]. -Accesso agli Elementi -===================== +Accesso ai controlli +==================== -Chiamiamo componenti sia il form che i suoi singoli elementi. Formano un albero di componenti, dove la radice è proprio il form. Possiamo accedere ai singoli elementi del form in questo modo: +Il form e i suoi singoli controlli si chiamano componenti. Formano un albero di componenti, la cui radice è il form. Ai singoli controlli del form potete accedere così: ```php $input = $form->getComponent('name'); @@ -80,36 +86,36 @@ $button = $form->getComponent('send'); // sintassi alternativa: $button = $form['send']; ``` -Gli elementi vengono rimossi usando unset: +I controlli si rimuovono con `unset`: ```php unset($form['name']); ``` -Regole di Validazione +Regole di validazione ===================== -Abbiamo menzionato la parola *valido,* ma il form non ha ancora regole di validazione. Rimediamo. +Abbiamo usato la parola *valido*, ma il form non ha ancora alcuna regola di validazione. Rimediamo. -Il nome sarà obbligatorio, quindi lo marchiamo con il metodo `setRequired()`, il cui argomento è il testo del messaggio di errore che verrà visualizzato se l'utente non compila il nome. Se non specifichiamo l'argomento, verrà utilizzato il messaggio di errore predefinito. +Il nome sarà obbligatorio, quindi lo contrassegniamo con il metodo `setRequired()`. Il suo argomento è il testo del messaggio di errore mostrato se l'utente non compila il nome. Se non viene indicato alcun argomento, viene usato il messaggio di errore predefinito. ```php $form->addText('name', 'Nome:') - ->setRequired('Inserisci un nome'); + ->setRequired('Inserite un nome.'); ``` -Provate a inviare il form senza compilare il nome e vedrete che verrà visualizzato un messaggio di errore e il browser o il server lo rifiuteranno finché non compilerete il campo. +Provate a inviare il form senza compilare il nome e vedrete comparire un messaggio di errore. Il browser o il server lo rifiuteranno finché non compilate il campo. -Allo stesso tempo, non ingannerete il sistema scrivendo, ad esempio, solo spazi nel campo. Niente affatto. Nette rimuove automaticamente gli spazi iniziali e finali. Provate. È una cosa che dovreste sempre fare con ogni input a riga singola, ma spesso viene dimenticata. Nette lo fa automaticamente. (Potete provare a ingannare il form e inviare una stringa multilinea come nome. Anche qui, Nette non si lascerà ingannare e convertirà gli a capo in spazi.) +Allo stesso tempo non potete imbrogliare il sistema scrivendo nel campo solo degli spazi. Niente da fare. Nette elimina automaticamente gli spazi iniziali e finali. Provate. È una cosa che dovreste sempre fare con ogni campo a riga singola, ma che spesso si dimentica. Nette la fa automaticamente. (Potete provare a ingannare il form inviando come nome una stringa su più righe. Anche qui Nette non si farà ingannare e gli a capo verranno convertiti in spazi.) -Il form viene sempre validato lato server, ma viene generata anche una validazione JavaScript, che avviene istantaneamente e l'utente viene informato dell'errore immediatamente, senza dover inviare il form al server. Questo è gestito dallo script `netteForms.js`. Inseritelo nella pagina: +Il form viene sempre validato sul lato server, ma viene generata anche la validazione JavaScript. Questa gira all'istante e l'utente viene a conoscenza degli errori subito, senza dover inviare il form al server. Se ne occupa lo script `netteForms.js`. Inseritelo nella pagina: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Se guardate il codice sorgente della pagina con il form, potete notare che Nette inserisce gli elementi obbligatori in elementi con la classe CSS `required`. Provate ad aggiungere il seguente foglio di stile al template e l'etichetta "Nome" diventerà rossa. In questo modo, indichiamo elegantemente agli utenti gli elementi obbligatori: +Se guardate il codice sorgente della pagina con il form, noterete forse che Nette inserisce i controlli obbligatori in elementi con la classe CSS `required`. Provate ad aggiungere al template il foglio di stile seguente e l'etichetta "Nome" diventerà rossa. È un modo elegante di evidenziare i controlli obbligatori per gli utenti: ```latte <style> @@ -117,85 +123,102 @@ Se guardate il codice sorgente della pagina con il form, potete notare che Nette </style> ``` -Aggiungiamo ulteriori regole di validazione con il metodo `addRule()`. Il primo parametro è la regola, il secondo è di nuovo il testo del messaggio di errore e può seguire un argomento della regola di validazione. Cosa significa? +Aggiungiamo altre regole di validazione con il metodo `addRule()`. Il primo parametro è la regola, il secondo è di nuovo il testo del messaggio di errore, e può seguire un argomento facoltativo della regola di validazione. Cosa significa? -Estendiamo il form con un nuovo campo opzionale "età", che deve essere un numero intero (`addInteger()`) e inoltre in un intervallo consentito (`$form::Range`). E qui utilizzeremo proprio il terzo parametro del metodo `addRule()`, con cui passiamo al validatore l'intervallo richiesto come coppia `[da, a]`: +Estendiamo il form con un nuovo campo facoltativo "età", che deve essere un numero intero (`addInteger()`) e rientrare in un intervallo consentito (`$form::Range`). Qui useremo il terzo parametro del metodo `addRule()` per passare al validatore l'intervallo richiesto come coppia `[min, max]`: ```php $form->addInteger('age', 'Età:') - ->addRule($form::Range, 'L\'età deve essere compresa tra 18 e 120', [18, 120]); + ->addRule($form::Range, 'L\'età deve essere compresa tra 18 e 120.', [18, 120]); ``` .[tip] -Se l'utente non compila il campo, le regole di validazione non verranno verificate, poiché l'elemento è opzionale. +Se l'utente non compila il campo, le regole di validazione non verranno controllate, perché il controllo è facoltativo. -Qui c'è spazio per un piccolo refactoring. Nel messaggio di errore e nel terzo parametro, i numeri sono indicati in modo duplicato, il che non è ideale. Se stessimo creando [form multilingue |rendering#Traduzione] e il messaggio contenente numeri fosse tradotto in più lingue, un'eventuale modifica dei valori sarebbe più difficile. Per questo motivo, è possibile utilizzare i segnaposto `%d` e Nette completerà i valori: +Questo apre spazio a un piccolo refactoring. I numeri sono duplicati nel messaggio di errore e nel terzo parametro, il che non è ideale. Se creassimo [form multilingue |rendering#Traduzione] e il messaggio contenente i numeri venisse tradotto in più lingue, cambiare i valori diventerebbe difficile. Per questo si possono usare i segnaposto `%d`, che Nette riempirà con i valori: ```php - ->addRule($form::Range, 'L\'età deve essere compresa tra %d e %d anni', [18, 120]); + ->addRule($form::Range, 'L\'età deve essere compresa tra %d e %d anni.', [18, 120]); ``` -Torniamo all'elemento `password`, che renderemo anch'esso obbligatorio e verificheremo anche la lunghezza minima della password (`$form::MinLength`), sempre utilizzando il segnaposto: +Torniamo al controllo `password`, rendiamolo obbligatorio e verifichiamo anche la lunghezza minima della password (`$form::MinLength`), usando di nuovo un segnaposto nel messaggio: ```php $form->addPassword('password', 'Password:') - ->setRequired('Scegli una password') - ->addRule($form::MinLength, 'La password deve contenere almeno %d caratteri', 8); + ->setRequired('Scegliete una password') + ->addRule($form::MinLength, 'La password deve essere lunga almeno %d caratteri', 8); ``` -Aggiungiamo al form anche il campo `passwordVerify`, dove l'utente inserirà nuovamente la password, per verifica. Utilizzando le regole di validazione, verificheremo se entrambe le password sono uguali (`$form::Equal`). E come parametro, daremo un riferimento alla prima password usando le [parentesi quadre |#Accesso agli Elementi]: +Aggiungiamo al form un altro campo `passwordVerify`, in cui l'utente inserisce di nuovo la password per verifica. Con le regole di validazione controlliamo che le due password siano uguali (`$form::Equal`). Come parametro indichiamo un riferimento alla prima password usando le [parentesi quadre |#Accesso ai controlli]: ```php -$form->addPassword('passwordVerify', 'Password per verifica:') - ->setRequired('Inserisci nuovamente la password per verifica') - ->addRule($form::Equal, 'Le password non corrispondono', $form['password']) +$form->addPassword('passwordVerify', 'Password di nuovo:') + ->setRequired('Inserite di nuovo la password per verifica') + ->addRule($form::Equal, 'Le password non coincidono', $form['password']) ->setOmitted(); ``` -Con `setOmitted()` abbiamo contrassegnato l'elemento il cui valore in realtà non ci interessa e che esiste solo ai fini della validazione. Il valore non verrà passato a `$data`. +Con `setOmitted()` abbiamo contrassegnato un controllo il cui valore non ci interessa davvero e che esiste solo a scopo di validazione. Il suo valore non viene passato a `$data`. -Con questo, abbiamo un form completamente funzionante con validazione in PHP e JavaScript. Le capacità di validazione di Nette sono molto più ampie, è possibile creare condizioni, far visualizzare e nascondere parti della pagina in base ad esse, ecc. Imparerete tutto nel capitolo sulla [validazione dei form|validation]. +Con questo abbiamo un form pienamente funzionante, con validazione sia in PHP sia in JavaScript. Le capacità di validazione di Nette sono molto più ampie: potete creare condizioni, mostrare e nascondere parti della pagina in base a esse e altro ancora. Imparerete tutto nel capitolo sulla [validazione dei form |validation]. -Valori Predefiniti +Valori predefiniti ================== -Di solito impostiamo valori predefiniti per gli elementi del form: +Spesso impostiamo valori predefiniti per i controlli del form: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -Spesso è utile impostare valori predefiniti per tutti gli elementi contemporaneamente. Ad esempio, quando il form serve per modificare record. Leggiamo il record dal database e impostiamo i valori predefiniti: +Spesso è utile impostare i valori predefiniti di tutti i controlli in una volta, per esempio quando il form serve a modificare dei record. Leggiamo il record dal database e ne impostiamo i valori come predefiniti: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Chiamate `setDefaults()` dopo aver definito gli elementi. +Chiamate `setDefaults()` dopo aver definito i controlli. +Su un form già inviato `setDefaults()` non ha effetto: non sovrascrive ciò che l'utente ha compilato, quindi è sicuro chiamarlo incondizionatamente nella factory del form. Se dovete forzare i valori anche dopo l'invio, usate invece `setValues()`. -Rendering del Form -================== -Per default, il form viene renderizzato come una tabella. I singoli elementi soddisfano la regola di base dell'accessibilità: tutte le etichette sono scritte come `<label>` e collegate all'elemento del form corrispondente. Facendo clic sull'etichetta, il cursore appare automaticamente nel campo del form. +Disegnare il form +================= + +Per impostazione predefinita il form viene disegnato come una tabella. I singoli controlli rispettano le linee guida di base sull'accessibilità: tutte le etichette sono generate come elementi `<label>` e associate ai rispettivi controlli. Cliccando su un'etichetta il cursore si posiziona automaticamente nel campo del form. -Possiamo impostare attributi HTML arbitrari per ogni elemento. Ad esempio, aggiungere un placeholder: +A ogni controllo possiamo impostare attributi HTML qualsiasi. Aggiungiamo per esempio un placeholder: ```php $form->addInteger('age', 'Età:') - ->setHtmlAttribute('placeholder', 'Inserisci l\'età'); + ->setHtmlAttribute('placeholder', 'Inserite l\'età'); ``` -Ci sono davvero molti modi per renderizzare un form, quindi c'è un [capitolo separato sul rendering|rendering] dedicato a questo. +Ci sono molti modi di disegnare un form, quindi al [rendering è dedicato un capitolo a parte |rendering]. -Mapping su Classi -================= +Rendering con Latte +------------------- + +Se avete a portata di mano il motore di template [Latte |latte:], potete lasciargli disegnare il form e ottenere il pieno controllo sull'HTML risultante. Create il motore, registrate l'estensione dei form e passate il form al template come variabile: + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); +``` + +Nel template lavorate poi con il form tramite la variabile `$form` e tag come `{input}`, `{label}` oppure `n:name`. Un esempio completo, template compreso, si trova nella directory degli [esempi |https://github.com/nette/forms/tree/master/examples] (i file `latte.php` e `latte/`). I singoli tag sono descritti nel capitolo sul [rendering |rendering]. + -Torniamo all'elaborazione dei dati del form. Il metodo `getValues()` ci restituiva i dati inviati come oggetto `ArrayHash`. Poiché si tratta di una classe generica, qualcosa come `stdClass`, ci mancherà una certa comodità nel lavorarci, come il suggerimento delle proprietà negli editor o l'analisi statica del codice. Questo potrebbe essere risolto avendo una classe specifica per ogni form, le cui proprietà rappresentano i singoli elementi. Ad esempio: +Mappatura sulle classi +====================== + +Torniamo all'elaborazione dei dati del form. Il metodo `getValues()` restituiva i dati inviati come oggetto `ArrayHash`. Poiché si tratta di una classe generica, come `stdClass`, lavorandoci ci mancano certe comodità, come il completamento automatico delle proprietà negli editor o l'analisi statica del codice. Lo si potrebbe risolvere avendo una classe specifica per ogni form, le cui proprietà rappresentino i singoli controlli. Per esempio: ```php class RegistrationFormData @@ -206,7 +229,7 @@ class RegistrationFormData } ``` -In alternativa, potete usare il costruttore: +In alternativa potete usare il costruttore: ```php class RegistrationFormData @@ -220,18 +243,18 @@ class RegistrationFormData } ``` -Le proprietà della classe dati possono anche essere enum e verranno mappate automaticamente. .{data-version:3.2.4} +Le proprietà della classe dei dati possono essere anche enum, e verranno mappate automaticamente. .{data-version:3.2.4} -Come dire a Nette di restituirci i dati come oggetti di questa classe? Più facile di quanto pensiate. Basta specificare il nome della classe o l'oggetto da idratare come parametro: +Come diciamo a Nette di restituire i dati come oggetti di questa classe? Più facile di quanto pensiate. Basta indicare come parametro il nome della classe o l'oggetto da riempire: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Come parametro si può specificare anche `'array'` e allora i dati verranno restituiti come array. +Come parametro potete indicare anche `'array'` e i dati verranno restituiti come array. -Se i form formano una struttura multilivello composta da container, create una classe separata per ognuno: +Se i form hanno una struttura a più livelli composta da container, create una classe separata per ognuno: ```php $form = new Form; @@ -253,19 +276,19 @@ class RegistrationFormData } ``` -Il mapping riconoscerà quindi dal tipo della proprietà `$person` che deve mappare il container sulla classe `PersonFormData`. Se la proprietà contenesse un array di container, specificate il tipo `array` e passate la classe per il mapping direttamente al container: +La mappatura capisce allora, dal tipo della proprietà `$person`, che deve mappare il container sulla classe `PersonFormData`. Se la proprietà dovesse contenere un array di container, indicate il tipo `array` e passate la classe da mappare direttamente al container: ```php $person->setMappedType(PersonFormData::class); ``` -Potete far generare il design della classe dati del form usando il metodo `Nette\Forms\Blueprint::dataClass($form)`, che lo stamperà sulla pagina del browser. Quindi basta selezionare il codice con un clic e copiarlo nel progetto. .{data-version:3.1.15} +Potete farvi generare una proposta della classe dei dati del form con il metodo `Nette\Forms\Blueprint::dataClass($form)`, che la stamperà nella pagina del browser. Poi vi basta selezionare con un clic e copiare il codice nel vostro progetto. .{data-version:3.1.15} -Più Pulsanti -============ +Più pulsanti di invio +===================== -Se il form ha più di un pulsante, di solito dobbiamo distinguere quale è stato premuto. Questa informazione ci viene restituita dal metodo `isSubmittedBy()` del pulsante: +Se il form ha più di un pulsante, di norma dobbiamo distinguere quale sia stato premuto. Il metodo `isSubmittedBy()` del pulsante restituisce questa informazione: ```php $form->addSubmit('save', 'Salva'); @@ -282,36 +305,33 @@ if ($form->isSuccess()) { } ``` -Non omettete la domanda `$form->isSuccess()`, verificherete così la validità dei dati. +Non omettete il controllo `$form->isSuccess()`: verifica la validità dei dati. -Quando il form viene inviato premendo il tasto <kbd>Invio</kbd>, viene considerato come se fosse stato inviato dal primo pulsante. +Quando un form viene inviato premendo il tasto <kbd>Invio</kbd>, viene trattato come se fosse stato inviato dal primo pulsante. -Protezione dalle Vulnerabilità +Protezione dalle vulnerabilità ============================== -Nette Framework pone grande enfasi sulla sicurezza e quindi si preoccupa meticolosamente della buona protezione dei form. +Nette Framework dà grande importanza alla sicurezza e si preoccupa quindi scrupolosamente della corretta protezione dei form. -Oltre a proteggere i form dagli attacchi [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] e [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], implementa molte piccole misure di sicurezza a cui non dovete più pensare. +Oltre a proteggere i form dalle vulnerabilità più note, come il [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] e il [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], adotta molte piccole misure di sicurezza a cui non dovete più pensare. -Ad esempio, filtra tutti i caratteri di controllo dagli input e verifica la validità della codifica UTF-8, quindi i dati del form saranno sempre puliti. Per le select box e le radio list, verifica che gli elementi selezionati fossero effettivamente tra quelli offerti e che non ci sia stata alcuna falsificazione. Abbiamo già menzionato che per gli input di testo a riga singola, rimuove i caratteri di fine riga che un attaccante potrebbe aver inviato. Per gli input multilinea, invece, normalizza i caratteri di fine riga. E così via. +Per esempio filtra dagli input tutti i caratteri di controllo e controlla la validità della codifica UTF-8, garantendo che i dati del form siano sempre puliti. Per i select box e le liste di radio button verifica che gli elementi selezionati fossero davvero tra quelli offerti e che non ci siano state falsificazioni. Abbiamo già detto che, per i campi di testo a riga singola, sostituisce con spazi i caratteri di fine riga che un aggressore potrebbe inviare. Per i campi su più righe normalizza i caratteri di fine riga. E così via. -Nette risolve per voi i rischi di sicurezza di cui molti programmatori non sospettano nemmeno l'esistenza. +Nette si occupa al posto vostro di rischi di sicurezza di cui molti programmatori non sospettano nemmeno l'esistenza. -L'attacco CSRF menzionato consiste nel fatto che un attaccante attira la vittima su una pagina che esegue discretamente una richiesta nel browser della vittima al server su cui la vittima è loggata, e il server crede che la richiesta sia stata eseguita dalla vittima di sua spontanea volontà. Pertanto, Nette impedisce l'invio di form POST da un altro dominio. Se per qualche motivo volete disabilitare la protezione e consentire l'invio del form da un altro dominio, usate: +L'attacco CSRF menzionato consiste nell'attirare la vittima su una pagina che, in silenzio, esegue nel browser della vittima una richiesta al server su cui la vittima è attualmente connessa. Il server crede che la richiesta sia stata fatta volontariamente dalla vittima. Nette rifiuta quindi i form POST inviati da un'origine estranea; anche un sottodominio diverso dello stesso sito conta come estraneo. Se dovete permettere l'invio da un'altra origine, disattivate la protezione con: ```php -$form->allowCrossOrigin(); // ATTENZIONE! Disabilita la protezione! +$form->allowCrossOrigin(); // ATTENZIONE! Disattiva completamente la protezione! ``` -Questa protezione utilizza un cookie SameSite chiamato `_nss`. Pertanto, create l'oggetto form prima di inviare il primo output, in modo che il cookie possa essere inviato. - -La protezione tramite cookie SameSite potrebbe non essere affidabile al 100%, quindi è consigliabile abilitare anche la protezione tramite token: +Questo però disattiva la protezione per qualsiasi origine. Per permetterne solo alcune, disattivate la protezione e verificate voi stessi l'header `Origin` rispetto a un vostro elenco di origini consentite. -```php -$form->addProtection(); -``` +La protezione si basa sull'header `Sec-Fetch-Site` del browser (Fetch Metadata), che il browser invia automaticamente e che non si può falsificare nemmeno con una vulnerabilità XSS. I browser più vecchi, che non inviano questi header, non supereranno il controllo. L'articolo [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] lo descrive in dettaglio. -Si consiglia di proteggere in questo modo i form nella parte amministrativa del sito che modificano dati sensibili nell'applicazione. Il framework si difende dall'attacco CSRF generando e verificando un token di autorizzazione, che viene memorizzato nella sessione. Pertanto, è necessario avere una sessione aperta prima di visualizzare il form. Nella parte amministrativa del sito, di solito la sessione è già avviata a causa del login dell'utente. Altrimenti, avviate la sessione con il metodo `Nette\Http\Session::start()`. +.[note] +La protezione precedente, basata su un token di autorizzazione salvato nella sessione e attivata con `$form->addProtection()`, non serve più ed è deprecata dalla versione 3.3. -Bene, abbiamo completato una rapida introduzione ai form in Nette. Provate a dare un'occhiata alla directory [examples|https://github.com/nette/forms/tree/master/examples] nella distribuzione, dove troverete ulteriore ispirazione. +Abbiamo avuto così una rapida introduzione ai form in Nette. Provate a guardare nella directory degli [esempi |https://github.com/nette/forms/tree/master/examples] della distribuzione per altre idee. diff --git a/forms/it/upgrading.texy b/forms/it/upgrading.texy new file mode 100644 index 0000000000..3ba92fc968 --- /dev/null +++ b/forms/it/upgrading.texy @@ -0,0 +1,54 @@ +Aggiornamento +************* + + +Aggiornamento alla versione 3.3 +=============================== + +- la protezione CSRF automatica è passata da `isSameSite()` a `isFrom(FetchSite::SameOrigin)` ed è diventata più rigorosa: le richieste provenienti dai sottodomini non passano più +- grazie a questo `addProtection()` non serve più, perché la protezione automatica copre gli stessi casi; omettetelo nei nuovi form e rimuovetelo pure da quelli esistenti + +Perché i token nella sessione non siano più necessari è spiegato nell'articolo [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Aggiornamento alla versione 3.1 +=============================== + +- `getValues()` restituisce solo i controlli validati; se vi servono i valori di tutti i controlli indipendentemente dalla validazione, usate il nuovo metodo `getUntrustedValues()` +- anche i `$values` passati ai gestori `onSuccess` e `onClick` contengono solo i controlli validati +- i form autonomi sono protetti automaticamente dal CSRF con un cookie con il flag SameSite; potete permettere l'invio da un'altra origine con `allowCrossOrigin()` +- la regola `Form::URL` completa ora un protocollo mancante con `https` invece che con `http` +- `Form::addImage()` è stato rinominato in `addImageButton()` +- `Checkbox::getSeparatorPrototype()` è stato rinominato in `getContainerPrototype()` +- i form non creano più la variabile `$_form` nei template + +Maggiori informazioni su queste modifiche nell'articolo [News in Nette Forms 3.1 |https://blog.nette.org/en/news-in-nette-forms-3-1]. + + +Aggiornamento alla versione 3.0 +=============================== + +- tutti i controlli dei form sono ora facoltativi per impostazione predefinita (questa modifica è stata introdotta in Nette 2.4), quindi potete rimuovere `setRequired(false)` +- assicuratevi di aggiornare `netteForms.js` alla versione 3 (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` e `MultiChoiceControl::$checkAllowedValues` sono stati sostituiti dal metodo `checkDefaultValue()` + + +Aggiornamento alla versione 2.4 +=============================== + +- se un controllo ha una regola aggiunta con `addRule()` (cioè è di fatto obbligatorio), dovete contrassegnarlo come obbligatorio anche con `setRequired()`; inoltre `setRequired(false)` rende ora il controllo facoltativo, il che sostituisce i rami `addCondition($form::FILLED)` +- i validatori `Form::EMAIL`, `URL` e `INTEGER` cambiano automaticamente l'attributo HTML `type` rispettivamente in `email`, `url` e `number` +- le regole di validazione negative sono deprecate; l'alternativa a `~Form::FILLED` è `Form::BLANK`, e `~Form::EQUAL` si può sostituire con `Form::NOT_EQUAL` +- il parametro interno `do` viene ora inviato via POST come `_do`, per evitare collisioni +- le variabili interne con il trattino basso, come `$_form`, sono deprecate +- ricordatevi di aggiornare `netteForms.js` + + +Aggiornamento alla versione 2.3 +=============================== + +- i metodi interni di filtraggio, come `Nette\Forms\Controls\TextBase::filterFloat`, sono stati rimossi +- i metodi interni di validazione, come `TextBase::validateFloat`, sono stati spostati in `Nette\Forms\Validator`, così come `Rules::$defaultMessages` +- i pulsanti e i campi Hidden vengono generati senza un ID HTML; se volete un ID, impostatelo con `setHtmlId()` +- anche gli elementi di RadioList vengono generati senza ID; potete attivarlo con `$radioList->generateId = true` +- i filtri aggiunti con `TextBase::addFilter()` vengono elaborati durante la validazione, e ora potete aggiungere filtri alle condizioni: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/it/validation.texy b/forms/it/validation.texy index 8de77d926d..6a9e400ccd 100644 --- a/forms/it/validation.texy +++ b/forms/it/validation.texy @@ -1,108 +1,108 @@ -Validazione dei Form +Validazione dei form ******************** -Elementi Obbligatori -==================== +Controlli obbligatori +===================== -Gli elementi obbligatori vengono contrassegnati con il metodo `setRequired()`, il cui argomento è il testo del [#Messaggi di errore] che verrà visualizzato se l'utente non compila l'elemento. Se l'argomento non viene fornito, verrà utilizzato il messaggio di errore predefinito. +I controlli si contrassegnano come obbligatori con il metodo `setRequired()`. Il suo argomento è il testo del [messaggio di errore |#Messaggi di errore] che verrà mostrato se l'utente non compila il controllo. Se non viene indicato alcun argomento, viene usato il messaggio di errore predefinito. ```php $form->addText('name', 'Nome:') - ->setRequired('Inserisci un nome'); + ->setRequired('Inserite il vostro nome.'); ``` Regole ====== -Aggiungiamo regole di validazione agli elementi usando il metodo `addRule()`. Il primo parametro è la regola, il secondo è il testo del [#Messaggi di errore] e il terzo è l'argomento della regola di validazione. +Aggiungiamo regole di validazione ai controlli con il metodo `addRule()`. Il primo parametro è la regola, il secondo è il [messaggio di errore |#Messaggi di errore] e il terzo è l'argomento della regola di validazione. ```php $form->addPassword('password', 'Password:') - ->addRule($form::MinLength, 'La password deve contenere almeno %d caratteri', 8); + ->addRule($form::MinLength, 'La password deve essere lunga almeno %d caratteri', 8); ``` -**Le regole di validazione vengono verificate solo se l'utente ha compilato l'elemento.** +**Le regole di validazione vengono controllate solo se l'utente ha compilato il controllo.** -Nette include una serie di regole predefinite, i cui nomi sono costanti della classe `Nette\Forms\Form`. Possiamo usare queste regole per tutti gli elementi: +Nette porta con sé diverse regole predefinite, i cui nomi sono costanti della classe `Nette\Forms\Form`. Possiamo applicare queste regole a tutti i controlli: -| costante | descrizione | tipo argomento +| costante | descrizione | tipo dell'argomento |------- -| `Required` | elemento obbligatorio, alias per `setRequired()` | - -| `Filled` | elemento obbligatorio, alias per `setRequired()` | - -| `Blank` | l'elemento non deve essere compilato | - -| `Equal` | il valore è uguale al parametro | `mixed` -| `NotEqual` | il valore non è uguale al parametro | `mixed` -| `IsIn` | il valore è uguale a uno degli elementi nell'array | `array` -| `IsNotIn` | il valore non è uguale a nessuno degli elementi nell'array | `array` -| `Valid` | l'elemento è compilato correttamente? (per [#Condizioni]) | - +| `Required` | controllo obbligatorio, alias di `setRequired()` | - +| `Filled` | controllo obbligatorio, alias di `setRequired()` | - +| `Blank` | il controllo non deve essere compilato | - +| `Equal` | il valore deve essere uguale al parametro | `mixed` +| `NotEqual` | il valore non deve essere uguale al parametro | `mixed` +| `IsIn` | il valore deve essere uno degli elementi dell'array | `array` +| `IsNotIn` | il valore non deve essere nessuno degli elementi dell'array | `array` +| `Valid` | il controllo è compilato correttamente? (solo in [addConditionOn() |#Condizioni]) | - -Input di Testo +Campi di testo -------------- -Per gli elementi `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` è possibile utilizzare anche alcune delle seguenti regole: +Ai controlli `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` si possono applicare anche alcune delle regole seguenti: | `MinLength` | lunghezza minima del testo | `int` | `MaxLength` | lunghezza massima del testo | `int` -| `Length` | lunghezza nell'intervallo o lunghezza esatta | coppia `[int, int]` o `int` +| `Length` | lunghezza in un intervallo o lunghezza esatta | coppia `[int, int]` oppure `int` | `Email` | indirizzo e-mail valido | - | `URL` | URL assoluto | - -| `Pattern` | corrisponde all'espressione regolare | `string` -| `PatternInsensitive` | come `Pattern`, ma case-insensitive | `string` +| `Pattern` | corrisponde a un'espressione regolare | `string` +| `PatternInsensitive` | come `Pattern`, ma senza distinguere maiuscole e minuscole | `string` | `Integer` | valore intero | - -| `Numeric` | alias per `Integer` | - +| `Numeric` | intero non negativo (solo cifre) | - | `Float` | numero | - -| `Min` | valore minimo dell'elemento numerico | `int\|float` -| `Max` | valore massimo dell'elemento numerico | `int\|float` -| `Range` | valore nell'intervallo | coppia `[int\|float, int\|float]` +| `Min` | valore minimo di un controllo numerico | `int\|float` +| `Max` | valore massimo di un controllo numerico | `int\|float` +| `Range` | valore in un intervallo | coppia `[int\|float, int\|float]` -Le regole di validazione `Integer`, `Numeric` e `Float` convertono direttamente il valore in integer o float rispettivamente. Inoltre, la regola `URL` accetta anche un indirizzo senza schema (es. `nette.org`) e aggiunge lo schema (`https://nette.org`). L'espressione in `Pattern` e `PatternIcase` deve corrispondere all'intero valore, cioè come se fosse racchiusa tra i caratteri `^` e `$`. +Le regole di validazione `Integer` e `Float` convertono automaticamente il valore rispettivamente in intero o in float. La regola `URL` accetta inoltre anche un indirizzo senza schema (per esempio `nette.org`) e ne completa lo schema (`https://nette.org`). L'espressione in `Pattern` e `PatternInsensitive` deve valere per l'intero valore, cioè come se fosse racchiusa tra i caratteri `^` e `$`. -Numero di Elementi +Numero di elementi ------------------ -Per gli elementi `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` è possibile utilizzare anche le seguenti regole per limitare il numero di elementi selezionati o file caricati: +Ai controlli `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` potete applicare anche le regole seguenti, per limitare il numero di elementi selezionati o di file caricati: | `MinLength` | numero minimo | `int` | `MaxLength` | numero massimo | `int` -| `Length` | numero nell'intervallo o numero esatto | coppia `[int, int]` o `int` +| `Length` | numero in un intervallo o numero esatto | coppia `[int, int]` oppure `int` -Upload di File +Upload di file -------------- -Per gli elementi `addUpload()`, `addMultiUpload()` è possibile utilizzare anche le seguenti regole: +Ai controlli `addUpload()`, `addMultiUpload()` si possono applicare anche le regole seguenti: | `MaxFileSize` | dimensione massima del file in byte | `int` -| `MimeType` | tipo MIME, consentiti caratteri jolly (`'video/*'`) | `string\|string[]` +| `MimeType` | tipo MIME, sono ammessi i caratteri jolly (`'video/*'`) | `string\|string[]` | `Image` | immagine JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | il nome del file corrisponde all'espressione regolare | `string` -| `PatternInsensitive` | come `Pattern`, ma case-insensitive | `string` +| `Pattern` | il nome del file corrisponde a un'espressione regolare | `string` +| `PatternInsensitive` | come `Pattern`, ma senza distinguere maiuscole e minuscole | `string` -`MimeType` e `Image` richiedono l'estensione PHP `fileinfo`. Rilevano se un file o un'immagine è del tipo richiesto in base alla sua firma e **non verificano l'integrità dell'intero file.** È possibile verificare se un'immagine è danneggiata, ad esempio, provando a [caricarla |http:request#toImage]. +`MimeType` e `Image` richiedono l'estensione PHP `fileinfo`. Se un file o un'immagine siano del tipo richiesto viene rilevato in base alla loro firma, e **l'integrità dell'intero file non viene controllata**. Potete stabilire se un'immagine è danneggiata, per esempio, provando a [caricarla |http:request#toImage()]. -Messaggi di Errore +Messaggi di errore ================== -Tutte le regole predefinite, ad eccezione di `Pattern` e `PatternInsensitive`, hanno un messaggio di errore predefinito, quindi può essere omesso. Tuttavia, specificando e formulando tutti i messaggi su misura, renderete il form più user-friendly. +Tutte le regole predefinite tranne `Pattern` e `PatternInsensitive` hanno un messaggio di errore predefinito, quindi si può omettere. Indicando e formulando tutti i messaggi personalizzati secondo le vostre esigenze, però, renderete il form più amichevole. -Potete modificare i messaggi predefiniti nella [configurazione|forms:configuration], modificando i testi nell'array `Nette\Forms\Validator::$messages` o utilizzando un [traduttore |rendering#Traduzione]. +Potete cambiare i messaggi predefiniti nella [configurazione |forms:configuration], modificando i testi nell'array `Nette\Forms\Validator::$messages`, oppure usando un [traduttore |rendering#Traduzione]. -Nel testo dei messaggi di errore è possibile utilizzare le seguenti stringhe segnaposto: +Nel testo dei messaggi di errore si possono usare i segnaposto seguenti: -| `%d` | sostituisce progressivamente con gli argomenti della regola -| `%n$d` | sostituisce con l'n-esimo argomento della regola -| `%label` | sostituisce con l'etichetta dell'elemento (senza i due punti) -| `%name` | sostituisce con il nome dell'elemento (es. `name`) -| `%value` | sostituisce con il valore inserito dall'utente +| `%d` | sostituito in sequenza dagli argomenti della regola +| `%n$d` | sostituito dall'n-esimo argomento della regola +| `%label` | sostituito dall'etichetta del controllo (senza i due punti) +| `%name` | sostituito dal nome del controllo (per esempio `name`) +| `%value` | sostituito dal valore inserito dall'utente ```php $form->addText('name', 'Nome:') - ->setRequired('Compila %label'); + ->setRequired('Compilate %label'); $form->addInteger('id', 'ID:') ->addRule($form::Range, 'almeno %d e al massimo %d', [5, 10]); @@ -115,34 +115,34 @@ $form->addInteger('id', 'ID:') Condizioni ========== -Oltre alle regole, è possibile aggiungere anche condizioni. Queste si scrivono in modo simile alle regole, ma invece di `addRule()` usiamo il metodo `addCondition()` e ovviamente non specifichiamo alcun messaggio di errore (la condizione si limita a chiedere): +Oltre alle regole si possono aggiungere anche delle condizioni. Si scrivono come le regole, ma al posto di `addRule()` usiamo il metodo `addCondition()` e naturalmente non indichiamo un messaggio di errore (la condizione si limita a chiedere): ```php $form->addPassword('password', 'Password:') - // se la password non è più lunga di 8 caratteri + // se la lunghezza della password non supera 8 ->addCondition($form::MaxLength, 8) - // allora deve contenere un numero - ->addRule($form::Pattern, 'Deve contenere un numero', '.*[0-9].*'); + // allora deve contenere una cifra + ->addRule($form::Pattern, 'Deve contenere una cifra', '.*[0-9].*'); ``` -La condizione può essere legata anche a un elemento diverso da quello corrente usando `addConditionOn()`. Come primo parametro, specifichiamo un riferimento all'elemento. In questo esempio, l'e-mail sarà obbligatoria solo se la checkbox è selezionata (il suo valore sarà true): +La condizione si può legare a un controllo diverso da quello corrente con `addConditionOn()`. Il primo parametro è un riferimento al controllo. In questo esempio l'e-mail sarà obbligatoria solo se la checkbox è selezionata (cioè se il suo valore è true): ```php -$form->addCheckbox('newsletters', 'inviami le newsletter'); +$form->addCheckbox('newsletters', 'Inviatemi le newsletter'); -$form->addEmail('email', 'E-mail:') +$form->addEmail('email', 'Email:') // se la checkbox è selezionata ->addConditionOn($form['newsletters'], $form::Equal, true) // allora richiedi l'e-mail - ->setRequired('Inserisci l\'indirizzo e-mail'); + ->setRequired('Inserite il vostro indirizzo e-mail'); ``` -È possibile creare strutture complesse di condizioni usando `elseCondition()` e `endCondition()`: +Le condizioni si possono comporre in strutture complesse con `elseCondition()` ed `endCondition()`: ```php $form->addText(/* ... */) ->addCondition(/* ... */) // se la prima condizione è soddisfatta - ->addConditionOn(/* ... */) // e la seconda condizione su un altro elemento + ->addConditionOn(/* ... */) // ed è soddisfatta anche la seconda condizione su un altro controllo ->addRule(/* ... */) // richiedi questa regola ->elseCondition() // se la seconda condizione non è soddisfatta ->addRule(/* ... */) // richiedi queste regole @@ -151,29 +151,37 @@ $form->addText(/* ... */) ->addRule(/* ... */); ``` -In Nette è molto facile reagire al soddisfacimento o meno di una condizione anche lato JavaScript usando il metodo `toggle()`, vedi [#JavaScript dinamico]. +Il primo argomento di `addCondition()` può essere anche un valore booleano. È utile quando la decisione è già nota mentre il form viene costruito, per esempio per applicare una regola solo in certe circostanze: + +```php +$form->addText('nickname') + ->addCondition($isRequired) // un valore noto al momento della costruzione del form + ->setRequired(); +``` + +In Nette è molto semplice reagire dal lato JavaScript al fatto che una condizione sia soddisfatta o meno, con il metodo `toggle()`, vedi [#JavaScript dinamico]. -Riferimento a un Altro Elemento -=============================== +Riferimento a un altro controllo +================================ -Come argomento di una regola o condizione, è possibile passare anche un altro elemento del form. La regola utilizzerà quindi il valore inserito successivamente dall'utente nel browser. In questo modo è possibile, ad esempio, validare dinamicamente che l'elemento `password` contenga la stessa stringa dell'elemento `password_confirm`: +Come argomento di una regola o di una condizione potete passare anche un altro controllo del form. La regola userà allora il valore che l'utente inserirà in seguito nel browser. Lo si può usare, per esempio, per validare dinamicamente che il controllo `password` contenga la stessa stringa del controllo `password_confirm`: ```php $form->addPassword('password', 'Password'); -$form->addPassword('password_confirm', 'Conferma password') - ->addRule($form::Equal, 'Le password inserite non corrispondono', $form['password']); +$form->addPassword('password_confirm', 'Conferma la password') + ->addRule($form::Equal, 'Le password non coincidono', $form['password']); ``` -Regole e Condizioni Personalizzate +Regole e condizioni personalizzate ================================== -A volte ci troviamo in una situazione in cui le regole di validazione integrate in Nette non sono sufficienti e abbiamo bisogno di validare i dati dell'utente a modo nostro. In Nette è molto semplice! +A volte incontriamo situazioni in cui le regole di validazione integrate in Nette non bastano e dobbiamo validare i dati dell'utente a modo nostro. In Nette è semplicissimo! -Ai metodi `addRule()` o `addCondition()` è possibile passare qualsiasi callback come primo parametro. Questo riceve l'elemento stesso come primo parametro e restituisce un valore booleano che indica se la validazione è andata a buon fine. Quando si aggiunge una regola usando `addRule()`, è possibile specificare anche altri argomenti, che vengono poi passati come secondo parametro. +Come primo parametro dei metodi `addRule()` o `addCondition()` potete passare qualsiasi callback. La callback accetta come primo parametro il controllo stesso e restituisce un valore booleano che indica se la validazione è riuscita. Aggiungendo una regola con `addRule()` si possono indicare argomenti aggiuntivi, che vengono poi passati come secondo parametro. -Possiamo quindi creare il nostro set di validatori come una classe con metodi statici: +Un insieme personalizzato di validatori si può quindi creare come classe con metodi statici: ```php class MyValidators @@ -191,7 +199,7 @@ class MyValidators } ``` -L'uso è quindi molto semplice: +L'uso è poi molto immediato: ```php $form->addInteger('num') @@ -202,7 +210,7 @@ $form->addInteger('num') ); ``` -Le regole di validazione personalizzate possono essere aggiunte anche a JavaScript. La condizione è che la regola sia un metodo statico. Il suo nome per il validatore JavaScript viene creato unendo il nome della classe senza backslash `\`, un underscore `_` e il nome del metodo. Ad esempio, `App\MyValidators::validateDivisibility` lo scriviamo come `AppMyValidators_validateDivisibility` e lo aggiungiamo all'oggetto `Nette.validators`: +Le regole di validazione personalizzate si possono aggiungere anche a JavaScript. La condizione è che la regola sia un metodo statico. Il suo nome per il validatore JavaScript si forma concatenando il nome della classe senza le barre rovesciate `\`, un trattino basso `_` e il nome del metodo. Per esempio `App\MyValidators::validateDivisibility` si scrive come `AppMyValidators_validateDivisibility` e si aggiunge all'oggetto `Nette.validators`: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -214,20 +222,20 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => Evento onValidate ================= -Dopo l'invio del form, viene eseguita la validazione, durante la quale vengono controllate le singole regole aggiunte tramite `addRule()` e successivamente viene attivato l'[evento |nette:glossary#Eventi] `onValidate`. Il suo handler può essere utilizzato per una validazione aggiuntiva, tipicamente per verificare la corretta combinazione di valori in più elementi del form. +Dopo l'invio del form avviene la validazione, che controlla le singole regole aggiunte con `addRule()`, e viene poi scatenato l'[evento |nette:glossary#Eventi] `onValidate`. Il suo gestore si può usare per una validazione aggiuntiva, di norma per verificare la corretta combinazione di valori in più controlli del form. -Se viene rilevato un errore, lo passiamo al form usando il metodo `addError()`. Questo può essere chiamato su un elemento specifico o direttamente sul form. +Se viene rilevato un errore, lo si passa al form con il metodo `addError()`. Lo si può chiamare su un controllo specifico oppure direttamente sul form. ```php protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('Questa combinazione non è possibile.'); @@ -236,10 +244,10 @@ public function validateSignInForm(Form $form, \stdClass $data): void ``` -Errori durante l'Elaborazione -============================= +Elaborare gli errori +==================== -In molti casi, veniamo a conoscenza di un errore solo nel momento in cui elaboriamo un form valido, ad esempio quando inseriamo un nuovo elemento nel database e incontriamo una duplicazione di chiavi. In tal caso, passiamo nuovamente l'errore al form usando il metodo `addError()`. Questo può essere chiamato su un elemento specifico o direttamente sul form: +In molti casi scopriamo un errore solo elaborando un form valido, per esempio scrivendo un nuovo record nel database e incontrando una chiave duplicata. In tal caso passiamo di nuovo l'errore al form con il metodo `addError()`. Lo si può chiamare su un controllo specifico oppure direttamente sul form: ```php try { @@ -254,37 +262,37 @@ try { } ``` -Se possibile, si consiglia di allegare l'errore direttamente all'elemento del form, poiché verrà visualizzato accanto ad esso quando si utilizza il renderer predefinito. +Se possibile, consigliamo di aggiungere l'errore direttamente al controllo del form, perché con il renderer predefinito verrà mostrato accanto a esso. ```php -$form['date']->addError('Siamo spiacenti, ma questa data è già occupata.'); +$form['date']->addError('Spiacenti, questa data è già occupata.'); ``` -Potete chiamare `addError()` ripetutamente per passare più messaggi di errore al form o all'elemento. Li ottenete usando `getErrors()`. +Potete chiamare `addError()` più volte per passare più messaggi di errore a un form o a un controllo. Potete ottenerli con `getErrors()`. -Attenzione, `$form->getErrors()` restituisce un riepilogo di tutti i messaggi di errore, anche quelli passati direttamente ai singoli elementi, non solo direttamente al form. I messaggi di errore passati solo al form si ottengono tramite `$form->getOwnErrors()`. +Attenzione: `$form->getErrors()` restituisce il riepilogo di tutti i messaggi di errore, compresi quelli passati direttamente ai singoli controlli, non solo quelli passati direttamente al form. I messaggi di errore passati solo al form si ottengono con `$form->getOwnErrors()`. -Modifica dell'Input -=================== +Modificare i valori inseriti +============================ -Usando il metodo `addFilter()` possiamo modificare il valore inserito dall'utente. In questo esempio, tollereremo e rimuoveremo gli spazi nel CAP (Codice di Avviamento Postale): +Con il metodo `addFilter()` possiamo modificare il valore inserito dall'utente. In questo esempio tolleriamo e rimuoviamo gli spazi nel codice postale: ```php -$form->addText('zip', 'CAP:') +$form->addText('zip', 'Codice postale:') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // rimuoviamo gli spazi dal CAP + return str_replace(' ', '', $value); // rimuove gli spazi dal codice postale }) - ->addRule($form::Pattern, 'Il CAP non è nel formato di cinque cifre', '\d{5}'); + ->addRule($form::Pattern, 'Il codice postale non è di cinque cifre', '\d{5}'); ``` -Il filtro viene inserito tra le regole di validazione e le condizioni, quindi l'ordine dei metodi è importante, cioè il filtro e la regola vengono chiamati nello stesso ordine in cui sono presenti i metodi `addFilter()` e `addRule()`. +Il filtro si integra tra le regole di validazione e le condizioni, quindi l'ordine dei metodi conta: il filtro e la regola vengono chiamati nello stesso ordine in cui sono elencati i metodi `addFilter()` e `addRule()`. Validazione JavaScript ====================== -Il linguaggio per formulare condizioni e regole è molto potente. Tutte le costruzioni funzionano sia lato server che lato JavaScript. Vengono trasmesse negli attributi HTML `data-nette-rules` come JSON. La validazione stessa viene quindi eseguita da uno script che intercetta l'evento `submit` del form, scorre i singoli elementi ed esegue la validazione appropriata. +Il linguaggio per formulare condizioni e regole è molto potente. Tutti i costrutti funzionano sia sul lato server sia sul lato client, in JavaScript. Vengono trasferiti negli attributi HTML `data-nette-rules` come JSON. Della validazione vera e propria si occupa uno script che intercetta l'evento `submit` del form, scorre i singoli controlli ed esegue la validazione corrispondente. Questo script è `netteForms.js` ed è disponibile da diverse fonti possibili: @@ -294,37 +302,43 @@ Potete inserire lo script direttamente nella pagina HTML da una CDN: <script src="https://unpkg.com/nette-forms@3"></script> ``` -Oppure copiarlo localmente nella cartella pubblica del progetto (ad es. da `vendor/nette/forms/src/assets/netteForms.min.js`): +Oppure copiarlo localmente nella cartella pubblica del vostro progetto (per esempio da `vendor/nette/forms/src/assets/netteForms.min.js`): ```latte <script src="/path/to/netteForms.min.js"></script> ``` -Oppure installarlo tramite [npm|https://www.npmjs.com/package/nette-forms]: +Oppure installarlo con [npm |https://www.npmjs.com/package/nette-forms]: ```shell npm install nette-forms ``` -E successivamente caricarlo ed eseguirlo: +E poi caricarlo ed eseguirlo: ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -In alternativa, potete caricarlo direttamente dalla cartella `vendor`: +In alternativa potete caricarlo direttamente dalla cartella `vendor`: ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Potete disattivare del tutto la validazione lato client aggiungendo al form l'attributo `novalidate`. Lo script `netteForms.js` salta allora la validazione all'invio, quindi la validazione avviene solo sul server: + +```php +$form->setHtmlAttribute('novalidate'); +``` + -JavaScript Dinamico +JavaScript dinamico =================== -Volete visualizzare i campi per l'inserimento dell'indirizzo solo se l'utente sceglie di spedire la merce per posta? Nessun problema. La chiave è la coppia di metodi `addCondition()` & `toggle()`: +Volete mostrare i campi dell'indirizzo solo se l'utente sceglie di farsi spedire la merce per posta? Nessun problema. La chiave è la coppia di metodi `addCondition()` e `toggle()`: ```php $form->addCheckbox('send_it') @@ -332,25 +346,25 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -Questo codice dice che quando la condizione è soddisfatta, cioè quando la checkbox è selezionata, l'elemento HTML `#address-container` sarà visibile. E viceversa. Quindi, posizioniamo gli elementi del form con l'indirizzo del destinatario in un container con questo ID e, facendo clic sulla checkbox, verranno nascosti o visualizzati. Questo è gestito dallo script `netteForms.js`. +Questo codice dice che, quando la condizione è soddisfatta (cioè quando la checkbox è selezionata), l'elemento HTML `#address-container` sarà visibile, e viceversa. Collochiamo quindi i controlli con l'indirizzo del destinatario in un container con questo ID, e si nasconderanno o si mostreranno quando la checkbox viene cliccata. Se ne occupa lo script `netteForms.js`. -Come argomento del metodo `toggle()` è possibile passare qualsiasi selettore. Per motivi storici, una stringa alfanumerica senza altri caratteri speciali viene intesa come ID dell'elemento, cioè come se fosse preceduta dal carattere `#`. Il secondo parametro opzionale consente di invertire il comportamento, cioè se usassimo `toggle('#address-container', false)`, l'elemento verrebbe visualizzato solo se la checkbox non fosse selezionata. +Come argomento del metodo `toggle()` si può passare qualsiasi selettore. Per motivi storici, una stringa che inizia con una lettera, una cifra o un trattino basso e contiene solo lettere, cifre, trattini bassi, trattini, punti e due punti viene trattata come un ID di elemento, come se fosse preceduta dal carattere `#`. Il secondo parametro facoltativo permette di invertire il comportamento; se per esempio usassimo `toggle('#address-container', false)`, l'elemento verrebbe mostrato solo se la checkbox *non* fosse selezionata. -L'implementazione predefinita in JavaScript modifica la proprietà `hidden` degli elementi. Tuttavia, possiamo facilmente modificare il comportamento, ad esempio aggiungendo un'animazione. Basta sovrascrivere il metodo `Nette.toggle` in JavaScript con la propria soluzione: +L'implementazione JavaScript predefinita cambia la proprietà `hidden` degli elementi. Possiamo però cambiare facilmente il comportamento, per esempio aggiungendo un'animazione. Basta sovrascrivere in JavaScript il metodo `Nette.toggle` con una soluzione personalizzata: ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // nascondiamo o mostriamo 'el' in base al valore 'visible' + // nasconde o mostra 'el' in base al valore di 'visible' }); }; ``` -Disabilitazione della Validazione -================================= +Disattivare la validazione +========================== -A volte può essere utile disabilitare la validazione. Se la pressione del pulsante di invio non deve eseguire la validazione (adatto per i pulsanti *Annulla* o *Anteprima*), la disabilitiamo con il metodo `$submit->setValidationScope([])`. Se deve eseguire solo una validazione parziale, possiamo specificare quali campi o container del form devono essere validati. +A volte può essere utile disattivare la validazione. Se premere un pulsante di invio non deve eseguire la validazione (adatto ai pulsanti *Annulla* o *Anteprima*), la disattiviamo con il metodo `$submit->setValidationScope([])`. Se deve eseguire solo una validazione parziale, possiamo indicare quali campi o container del form vadano validati. ```php $form->addText('name') @@ -358,19 +372,21 @@ $form->addText('name') $details = $form->addContainer('details'); $details->addInteger('age') - ->setRequired('età'); + ->setRequired('age'); $details->addInteger('age2') - ->setRequired('età2'); + ->setRequired('age2'); -$form->addSubmit('send1'); // Valida l'intero form +$form->addSubmit('send1'); // valida l'intero form $form->addSubmit('send2') - ->setValidationScope([]); // Non valida affatto + ->setValidationScope([]); // non valida nulla $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Valida solo l'elemento name + ->setValidationScope([$form['name']]); // valida solo il controllo 'name' $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Valida solo l'elemento age + ->setValidationScope([$form['details']['age']]); // valida solo il controllo 'age' $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Valida il container details + ->setValidationScope([$form['details']]); // valida il container 'details' ``` -`setValidationScope` non influisce sull'[#evento onValidate] del form, che verrà chiamato sempre. L'evento `onValidate` del container verrà attivato solo se questo container è contrassegnato per la validazione parziale. +`setValidationScope` non influisce sull'[evento onValidate |#Evento onValidate] del form, che verrà sempre chiamato. L'evento `onValidate` di un container verrà scatenato solo se quel container è contrassegnato per la validazione parziale. + +La validazione parziale influisce anche sui valori restituiti da `getValues()`: il risultato contiene solo i valori dei controlli che rientrano nell'ambito della validazione. I valori dei controlli fuori da questo ambito vengono omessi. diff --git a/forms/ja/@home.texy b/forms/ja/@home.texy index a29a5b780e..6293eec010 100644 --- a/forms/ja/@home.texy +++ b/forms/ja/@home.texy @@ -3,29 +3,29 @@ Nette Forms <div class=perex> -Nette Forms は Web フォームの作成に革命をもたらしました。突然、数行のわかりやすいコードを書くだけで、レンダリング、JavaScriptおよびサーバーサイドの検証を含む完成したフォームが得られ、さらに最高レベルのセキュリティが確保されました。以下を示します: +Nette Forms はウェブのフォーム作りを一変させました。分かりやすい数行のコードを書くだけで、描画も JavaScript とサーバー側の検証も、そして一流の安全対策も備えた完全なフォームが手に入るようになったのです。ここでは次のことをお見せします。 -- 使いやすいフォームの作成 -- 送信されたデータの検証 -- 必要に応じて要素を正確にレンダリング +- 使いやすいフォームを作る +- 送られたデータを検証する +- 要素を思いどおりに描く </div> -Nette Forms を使用することで、検証の記述(さらに、サーバーサイドとクライアントサイドの2つの検証)などの多くの日常的なタスクを回避し、エラーやセキュリティホールの発生確率を最小限に抑えることができます。 +Nette Forms を使えば、(サーバー側でもクライアント側でも)検証の論理を書くといった決まりきった仕事の多くを避けられ、間違いやセキュリティ上の弱点が生まれる見込みを小さくできます。 -フォームは、Nette アプリケーションの一部として(つまり Presenter 内で)、または完全に独立して使用できます。両方の場合で使い方が少し異なるため、2つのガイドを用意しました: +フォームは Nette Application の一部として(つまりプレゼンターの中で)使うことも、まったく単独で使うこともできます。この 2 つでは使い方が少し違うので、それぞれ別の案内を用意しました。 <div class="wiki-buttons"> -<div> "Presenter 内のフォーム .[wiki-button]":in-presenter </div> -<div> "独立したフォーム .[wiki-button]":standalone </div> +<div> "プレゼンターでのフォーム .[wiki-button]":in-presenter </div> +<div> "フォームの単体利用 .[wiki-button]":standalone </div> </div> インストール ------ -[Composer|best-practices:composer]を使用してライブラリをダウンロードし、インストールします: +パッケージは [Composer|best-practices:composer]でダウンロードしてインストールします。 ```shell composer require nette/forms diff --git a/forms/ja/@left-menu.texy b/forms/ja/@left-menu.texy index 466166e097..d026fe604c 100644 --- a/forms/ja/@left-menu.texy +++ b/forms/ja/@left-menu.texy @@ -1,14 +1,16 @@ Nette Forms *********** -- [はじめに |@home] -- [Presenter 内のフォーム|in-presenter] -- [独立したフォーム|standalone] -- [フォームコントロール |controls] +- [概要 |@home] +- [プレゼンターでのフォーム|in-presenter] +- [フォームの単体利用|standalone] +- [フォーム要素 |controls] - [検証 |validation] - [レンダリング |rendering] -- [設定 |configuration] +- [カスタムフォーム要素 |custom-controls] +- [設定|configuration] +- [アップグレード|upgrading] -参考文献 +関連情報 **** -- [ガイドとベストプラクティス |best-practices:] +- [ベストプラクティス |best-practices:] diff --git a/forms/ja/@meta.texy b/forms/ja/@meta.texy index d3c41dc3d7..43b85f3cac 100644 --- a/forms/ja/@meta.texy +++ b/forms/ja/@meta.texy @@ -1 +1 @@ -{{sitename: Nette ドキュメンテーション}} +{{sitename: Nette ドキュメント}} diff --git a/forms/ja/configuration.texy b/forms/ja/configuration.texy index c70c6e8417..84f101cdfa 100644 --- a/forms/ja/configuration.texy +++ b/forms/ja/configuration.texy @@ -2,7 +2,7 @@ ******* .[perex] -設定で、デフォルトの[フォームのエラーメッセージ|validation]を変更できます。 +設定では、既定の[フォームのエラーメッセージ|validation]を変えられます。 ```neon forms: @@ -17,6 +17,7 @@ forms: 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.' @@ -30,32 +31,33 @@ forms: Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' ``` -こちらは日本語訳です: +日本語訳は次のとおりです。 ```neon forms: messages: - Equal: '%sを入力してください。' - NotEqual: 'この値は%sであってはなりません。' - Filled: 'このフィールドは必須です。' - Blank: 'このフィールドは空である必要があります。' - MinLength: '少なくとも%d文字入力してください。' - MaxLength: '%d文字以内で入力してください。' - Length: '%d文字から%d文字の間で入力してください。' - Email: '有効なメールアドレスを入力してください。' - URL: '有効なURLを入力してください。' - Integer: '有効な整数を入力してください。' - Float: '有効な数値を入力してください。' - Min: '%d以上の値を入力してください。' - Max: '%d以下の値を入力してください。' - Range: '%dから%dの間の値を入力してください。' - MaxFileSize: 'アップロードされたファイルのサイズは最大%dバイトまでです。' - MaxPostSize: 'アップロードされたデータは%dバイトの制限を超えています。' - MimeType: 'アップロードされたファイルは期待される形式ではありません。' - Image: 'アップロードされたファイルはJPEG、GIF、PNG、WebP、またはAVIF形式の画像である必要があります。' - Nette\Forms\Controls\SelectBox::Valid: '有効なオプションを選択してください。' - Nette\Forms\Controls\UploadControl::Valid: 'ファイルアップロード中にエラーが発生しました。' - Nette\Forms\Controls\CsrfProtection::Protection: 'セッションの有効期限が切れました。ホームページに戻って再度お試しください。' + Equal: '%s を入力してください。' + NotEqual: 'この値は %s であってはいけません。' + Filled: 'この項目は必須です。' + Blank: 'この項目は空でなければなりません。' + MinLength: '%d 文字以上で入力してください。' + MaxLength: '%d 文字以内で入力してください。' + Length: '%d 文字以上 %d 文字以下で入力してください。' + Email: '正しいメールアドレスを入力してください。' + URL: '正しい URL を入力してください。' + Integer: '正しい整数を入力してください。' + Numeric: '0 以上の整数を入力してください。' + Float: '正しい数値を入力してください。' + Min: '%d 以上の値を入力してください。' + Max: '%d 以下の値を入力してください。' + Range: '%d 以上 %d 以下の値を入力してください。' + MaxFileSize: 'アップロードできるファイルの大きさは %d バイトまでです。' + MaxPostSize: 'アップロードされたデータが %d バイトの上限を超えています。' + MimeType: 'アップロードされたファイルは想定された形式ではありません。' + Image: 'アップロードされたファイルは JPEG、GIF、PNG、WebP のいずれかの形式の画像でなければなりません。' + Nette\Forms\Controls\SelectBox::Valid: '正しい選択肢を選んでください。' + Nette\Forms\Controls\UploadControl::Valid: 'ファイルのアップロード中にエラーが起きました。' + Nette\Forms\Controls\CsrfProtection::Protection: 'セッションの有効期限が切れました。トップページに戻ってやり直してください。' ``` -フレームワーク全体を使用せず、したがって設定ファイルも使用しない場合は、`Nette\Forms\Validator::$messages` 配列で直接デフォルトのエラーメッセージを変更できます。 +フレームワーク全体を使っておらず、したがって設定ファイルも使っていないなら、既定のエラーメッセージは `Nette\Forms\Validator::$messages` 配列で直接変えられます。 diff --git a/forms/ja/controls.texy b/forms/ja/controls.texy index bc828fdb30..8d3ee06d4e 100644 --- a/forms/ja/controls.texy +++ b/forms/ja/controls.texy @@ -1,14 +1,14 @@ -フォーム要素 -****** +フォームの要素 +******* .[perex] -標準的なフォーム要素の概要。 +標準のフォームの要素の一覧です。 -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -一行テキストフィールド(クラス[TextInput |api:Nette\Forms\Controls\TextInput])を追加します。ユーザーがフィールドを空にした場合、空の文字列 `''` を返します。または、`setNullable()` を使用して `null` を返すように指定できます。 +1 行のテキストの入力欄を足します(クラス [TextInput |api:Nette\Forms\Controls\TextInput])。利用者がその項目を埋めなければ空の文字列 `''` を返します。`setNullable()` を使えば代わりに `null` を返させられます。 ```php $form->addText('name', '名前:') @@ -16,112 +16,112 @@ $form->addText('name', '名前:') ->setNullable(); ``` -自動的にUTF-8を検証し、左右の空白をトリミングし、攻撃者が送信する可能性のある改行を削除します。 +UTF-8 を自動的に検証し、前後の空白を取り除き、攻撃者が送るかもしれない改行を取り除きます。 -最大長は `setMaxLength()` で制限できます。ユーザーが入力した値を変更するには、[addFilter() |validation#入力の変更] を使用します。 +長さの上限は `setMaxLength()` で決められます。[addFilter() |validation#入力された値を変える]メソッドで、利用者が入力した値を変えられます。 -`setHtmlType()` を使用して、テキストフィールドの視覚的な特性を `search`、`tel`、`url` などのタイプに変更できます([仕様|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]を参照)。タイプの変更は視覚的なものであり、検証機能を代替するものではないことに注意してください。`url` タイプの場合、特定の検証[URLルール |validation#テキスト入力]を追加することをお勧めします。 +`setHtmlType()` を使うと、テキストの項目の見た目を [仕様|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]で定められた `search`、`tel`、`url` などの種類に変えられます。種類を変えるのは純粋に見た目の話で、検証の働きの代わりにはならないことを覚えておいてください。`url` の種類には、専用の [URL の検証の規則 |validation#テキストの入力]を足すとよいでしょう。 .[note] -`number`、`range`、`email`、`date`、`datetime-local`、`time`、`color` などの他の入力タイプについては、[#addInteger]、[#addFloat]、[#addEmail]、[#addDate]、[#addTime]、[#addDateTime]、[#addColor] などの専用メソッドを使用してください。これらはサーバーサイドの検証を保証します。`month` および `week` タイプは、まだすべてのブラウザで完全にサポートされているわけではありません。 +`number`、`range`、`email`、`date`、`datetime-local`、`time`、`color` といったほかの入力の種類には、サーバー側の検証も備えた [#addInteger()]、[#addFloat()]、[#addEmail()]、[#addDate()]、[#addTime()]、[#addDateTime()]、[#addColor()]といった専用のメソッドを使ってください。`month` と `week` の種類は、まだすべてのブラウザが十分に対応していません。 -要素には、いわゆる空の値(empty-value)を設定できます。これはデフォルト値のようなものですが、ユーザーが変更しない場合、要素は空の文字列または `null` を返します。 +要素には「空の値」を設定できます。これは既定値のように振る舞いますが、利用者がそれを変えなければ、要素は空の文字列か `null` を返します。 ```php -$form->addText('phone', '電話番号:') +$form->addText('phone', '電話:') ->setHtmlType('tel') - ->setEmptyValue('+81'); + ->setEmptyValue('+420'); ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -複数行テキストを入力するためのフィールド(クラス[TextArea |api:Nette\Forms\Controls\TextArea])を追加します。ユーザーがフィールドを空にした場合、空の文字列 `''` を返します。または、`setNullable()` を使用して `null` を返すように指定できます。 +複数行のテキストの入力欄を足します(クラス [TextArea |api:Nette\Forms\Controls\TextArea])。利用者がその項目を埋めなければ空の文字列 `''` を返します。`setNullable()` を使えば代わりに `null` を返させられます。 ```php -$form->addTextArea('note', '備考:') - ->addRule($form::MaxLength, '備考が長すぎます', 10000); +$form->addTextArea('note', 'メモ:') + ->addRule($form::MaxLength, 'メモが長すぎます', 10000); ``` -自動的にUTF-8を検証し、改行区切り文字を `\n` に正規化します。一行入力フィールドとは異なり、空白のトリミングは行われません。 +UTF-8 を自動的に検証し、改行を `\n` にそろえます。1 行の入力欄と違って、空白の切り詰めは起きません。 -最大長は `setMaxLength()` で制限できます。ユーザーが入力した値を変更するには、[addFilter() |validation#入力の変更] を使用します。`setEmptyValue()` を使用して、いわゆる空の値を設定できます。 +長さの上限は `setMaxLength()` で決められます。[addFilter() |validation#入力された値を変える]メソッドで、利用者が入力した値を変えられます。空の値は `setEmptyValue()` で設定できます。 -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -整数を入力するためのフィールド(クラス[TextInput |api:Nette\Forms\Controls\TextInput])を追加します。ユーザーが何も入力しない場合は、整数または `null` を返します。 +整数を入力する欄を足します(クラス [TextInput |api:Nette\Forms\Controls\TextInput])。整数を返すか、利用者が何も入力しなければ `null` を返します。 ```php $form->addInteger('year', '年:') - ->addRule($form::Range, '年は %d から %d の範囲である必要があります。', [1900, 2023]); + ->addRule($form::Range, '年は %d から %d のあいだでなければなりません。', [1900, 2023]); ``` -要素は `<input type="number">` としてレンダリングされます。`setHtmlType()` メソッドを使用して、タイプを `range` に変更してスライダーとして表示したり、`number` タイプの特別な動作なしで標準のテキストフィールドを好む場合は `text` に変更したりできます。 +この要素は `<input type="number">` として描かれます。`setHtmlType()` メソッドで、種類を `range` にしてスライダーとして表示させたり、`number` の種類の特別な振る舞いのないふつうのテキストの項目がよければ `text` にしたりできます。 -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -浮動小数点数を入力するためのフィールド(クラス[TextInput |api:Nette\Forms\Controls\TextInput])を追加します。ユーザーが何も入力しない場合は、浮動小数点数または `null` を返します。 +浮動小数点数を入力する欄を足します(クラス [TextInput |api:Nette\Forms\Controls\TextInput])。float を返すか、利用者が何も入力しなければ `null` を返します。 ```php $form->addFloat('level', 'レベル:') ->setDefaultValue(0) - ->addRule($form::Range, 'レベルは %d から %d の範囲である必要があります。', [0, 100]); + ->addRule($form::Range, 'レベルは %d から %d のあいだでなければなりません。', [0, 100]); ``` -要素は `<input type="number">` としてレンダリングされます。`setHtmlType()` メソッドを使用して、タイプを `range` に変更してスライダーとして表示したり、`number` タイプの特別な動作なしで標準のテキストフィールドを好む場合は `text` に変更したりできます。 +この要素は `<input type="number">` として描かれます。`setHtmlType()` メソッドで、種類を `range` にしてスライダーとして表示させたり、`number` の種類の特別な振る舞いのないふつうのテキストの項目がよければ `text` にしたりできます。 -NetteとChromeブラウザは、小数点区切り文字としてカンマとピリオドの両方を受け入れます。Firefoxでもこの機能を利用できるようにするには、特定の要素またはページ全体に `lang` 属性を設定することをお勧めします。例:`<html lang="ja">`。 +Nette と Chrome のブラウザは、小数点としてコンマもドットも受け付けます。Firefox でもこれを働かせるには、その要素かページ全体に `lang` 属性を設定するとよいでしょう。たとえば `<html lang="en">` です。 -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -メールアドレスを入力するためのフィールド(クラス[TextInput |api:Nette\Forms\Controls\TextInput])を追加します。ユーザーがフィールドを空にした場合、空の文字列 `''` を返します。または、`setNullable()` を使用して `null` を返すように指定できます。 +メールアドレスを入力する欄を足します(クラス [TextInput |api:Nette\Forms\Controls\TextInput])。利用者がその項目を埋めなければ空の文字列 `''` を返します。`setNullable()` を使えば代わりに `null` を返させられます。 ```php -$form->addEmail('email', 'メールアドレス:'); +$form->addEmail('email', 'メール:'); ``` -値が有効なメールアドレスであるかどうかを検証します。ドメインが実際に存在するかどうかは検証されず、構文のみが検証されます。自動的にUTF-8を検証し、左右の空白をトリミングします。 +値が正しいメールアドレスかを検証します。そのドメインが実在するかは調べず、書式だけを確かめます。UTF-8 を自動的に検証し、前後の空白を取り除きます。 -最大長は `setMaxLength()` で制限できます。ユーザーが入力した値を変更するには、[addFilter() |validation#入力の変更] を使用します。`setEmptyValue()` を使用して、いわゆる空の値を設定できます。 +長さの上限は `setMaxLength()` で決められます。[addFilter() |validation#入力された値を変える]メソッドで、利用者が入力した値を変えられます。空の値は `setEmptyValue()` で設定できます。 -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== -パスワードを入力するためのフィールド(クラス[TextInput |api:Nette\Forms\Controls\TextInput])を追加します。 +パスワードの入力欄を足します(クラス [TextInput |api:Nette\Forms\Controls\TextInput])。 ```php $form->addPassword('password', 'パスワード:') ->setRequired() - ->addRule($form::MinLength, 'パスワードは少なくとも %d 文字必要です', 8) - ->addRule($form::Pattern, '数字を含める必要があります', '.*[0-9].*'); + ->addRule($form::MinLength, 'パスワードは %d 文字以上でなければなりません', 8) + ->addRule($form::Pattern, 'パスワードには数字を含めてください', '.*[0-9].*'); ``` -フォームを再表示すると、フィールドは空になります。自動的にUTF-8を検証し、左右の空白をトリミングし、攻撃者が送信する可能性のある改行を削除します。 +フォームがもう一度表示されるとき、この項目は空になります。UTF-8 を自動的に検証し、前後の空白を取り除き、攻撃者が送るかもしれない改行を取り除きます。 -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -チェックボックス(クラス[Checkbox |api:Nette\Forms\Controls\Checkbox])を追加します。チェックされているかどうかに応じて、`true` または `false` の値を返します。 +チェックボックスを足します(クラス [Checkbox |api:Nette\Forms\Controls\Checkbox])。入っているかどうかに応じて `true` か `false` を返します。 ```php -$form->addCheckbox('agree', '利用規約に同意します') - ->setRequired('利用規約に同意する必要があります'); +$form->addCheckbox('agree', '利用条件に同意します') + ->setRequired('利用条件に同意していただく必要があります'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -複数の項目を選択するためのチェックボックス(クラス[CheckboxList |api:Nette\Forms\Controls\CheckboxList])を追加します。選択された項目のキーの配列を返します。`getSelectedItems()` メソッドはキーの代わりに値を返します。 +複数の項目を選ぶためのチェックボックスの一覧を足します(クラス [CheckboxList |api:Nette\Forms\Controls\CheckboxList])。選ばれた項目のキーの配列を返します。`getSelectedItems()` メソッドは、選ばれた項目をキーと値の組として返します。 ```php $form->addCheckboxList('colors', '色:', [ @@ -131,47 +131,48 @@ $form->addCheckboxList('colors', '色:', [ ]); ``` -提供される項目の配列は、3番目のパラメータまたは `setItems()` メソッドで渡します。 +提示する項目の配列は第 3 パラメータで渡すか、`setItems()` メソッドで渡します。`setItems()` の第 2 引数に `false` を渡すと、値がキーとしても使われます。 -`setDisabled(['r', 'g'])` を使用して、個々の項目を無効にできます。 +個々の項目を無効にするには `setDisabled(['r', 'g'])` を使います。 -要素は、改ざんが発生していないこと、選択された項目が実際に提供されたものの1つであり、無効にされていないことを自動的にチェックします。`getRawValue()` メソッドを使用すると、この重要なチェックなしで送信された項目を取得できます。 +この要素は、偽造がなかったこと、そして選ばれた項目が本当に提示されたものの中にあって無効にされていないことを自動的に確かめます。この大事な検査なしに送信された項目を取り出したいなら `getRawValue()` メソッドを使えます。 -デフォルトで選択された項目を設定する場合も、それらが提供されたものの1つであることを確認します。そうでない場合は例外をスローします。このチェックは `checkDefaultValue(false)` で無効にできます。 +既定で選ばれる項目を設定するときも、それが提示されたものの中にあるかを確かめ、なければ例外を投げます。この検査は `checkDefaultValue(false)` で切れます。 -`GET` メソッドでフォームを送信する場合、クエリ文字列のサイズを節約する、よりコンパクトなデータ転送方法を選択できます。これは、フォームのHTML属性を設定することで有効になります: +フォームを `GET` メソッドで送信しているなら、クエリ文字列の大きさを節約する、もっとこぢんまりしたデータの送り方を選べます。フォームに HTML の属性を設定して有効にします。 ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ -ラジオボタン(クラス[RadioList |api:Nette\Forms\Controls\RadioList])を追加します。選択された項目のキーを返します。ユーザーが何も選択しない場合は `null` を返します。`getSelectedItem()` メソッドはキーの代わりに値を返します。 +ラジオボタンを足します(クラス [RadioList |api:Nette\Forms\Controls\RadioList])。選ばれた項目のキーを返し、利用者が何も選ばなければ `null` を返します。`getSelectedItem()` メソッドはキーではなく値を返します。 ```php $sex = [ 'm' => '男性', 'f' => '女性', + 'o' => 'その他', ]; $form->addRadioList('gender', '性別:', $sex); ``` -提供される項目の配列は、3番目のパラメータまたは `setItems()` メソッドで渡します。 +提示する項目の配列は第 3 パラメータで渡すか、`setItems()` メソッドで渡します。 -`setDisabled(['m', 'f'])` を使用して、個々の項目を無効にできます。 +個々の項目を無効にするには `setDisabled(['m'])` を使います。 -要素は、改ざんが発生していないこと、選択された項目が実際に提供されたものの1つであり、無効にされていないことを自動的にチェックします。`getRawValue()` メソッドを使用すると、この重要なチェックなしで送信された項目を取得できます。 +この要素は、偽造がなかったこと、そして選ばれた項目が本当に提示されたものの中にあって無効にされていないことを自動的に確かめます。この大事な検査なしに送信された項目を取り出したいなら `getRawValue()` メソッドを使えます。 -デフォルトで選択された項目を設定する場合も、それが提供されたものの1つであることを確認します。そうでない場合は例外をスローします。このチェックは `checkDefaultValue(false)` で無効にできます。 +既定で選ばれる項目を設定するときも、それが提示されたものの中にあるかを確かめ、なければ例外を投げます。この検査は `checkDefaultValue(false)` で切れます。 -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -セレクトボックス(クラス[SelectBox |api:Nette\Forms\Controls\SelectBox])を追加します。選択された項目のキーを返します。ユーザーが何も選択しない場合は `null` を返します。`getSelectedItem()` メソッドはキーの代わりに値を返します。 +セレクトボックスを足します(クラス [SelectBox |api:Nette\Forms\Controls\SelectBox])。選ばれた項目のキーを返し、利用者が何も選ばなければ `null` を返します。`getSelectedItem()` メソッドはキーではなく値を返します。 ```php $countries = [ @@ -184,102 +185,102 @@ $form->addSelect('country', '国:', $countries) ->setDefaultValue('SK'); ``` -提供される項目の配列は、3番目のパラメータまたは `setItems()` メソッドで渡します。項目は2次元配列にすることもできます: +提示する項目の配列は第 3 パラメータで渡すか、`setItems()` メソッドで渡します。項目は 2 次元の配列にもできます(optgroup を表します)。 ```php $countries = [ - 'ヨーロッパ' => [ // 日本語のグループ名 + 'ヨーロッパ' => [ 'CZ' => 'チェコ共和国', 'SK' => 'スロバキア', 'GB' => 'イギリス', ], 'CA' => 'カナダ', - 'US' => 'アメリカ合衆国', + 'US' => 'アメリカ', '?' => 'その他', ]; ``` -セレクトボックスでは、最初の項目が特別な意味を持つことがよくあります。アクションを促すために使用されます。このような項目を追加するには `setPrompt()` メソッドを使用します。 +セレクトボックスでは、最初の項目が特別な意味を持ち、操作を促す役目を果たすことがよくあります。そうした項目を足すには `setPrompt()` メソッドを使います。 ```php $form->addSelect('country', '国:', $countries) - ->setPrompt('国を選択してください'); + ->setPrompt('国を選んでください'); ``` -`setDisabled(['CZ', 'SK'])` を使用して、個々の項目を無効にできます。 +個々の項目を無効にするには `setDisabled(['CZ', 'SK'])` を使います。 -要素は、改ざんが発生していないこと、選択された項目が実際に提供されたものの1つであり、無効にされていないことを自動的にチェックします。`getRawValue()` メソッドを使用すると、この重要なチェックなしで送信された項目を取得できます。 +この要素は、偽造がなかったこと、そして選ばれた項目が本当に提示されたものの中にあって無効にされていないことを自動的に確かめます。この大事な検査なしに送信された項目を取り出したいなら `getRawValue()` メソッドを使えます。 -デフォルトで選択された項目を設定する場合も、それが提供されたものの1つであることを確認します。そうでない場合は例外をスローします。このチェックは `checkDefaultValue(false)` で無効にできます。 +既定で選ばれる項目を設定するときも、それが提示されたものの中にあるかを確かめ、なければ例外を投げます。この検査は `checkDefaultValue(false)` で切れます。 -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -複数の項目を選択するためのセレクトボックス(クラス[MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox])を追加します。選択された項目のキーの配列を返します。`getSelectedItems()` メソッドはキーの代わりに値を返します。 +複数の項目を選ぶためのセレクトボックスを足します(クラス [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox])。選ばれた項目のキーの配列を返します。`getSelectedItems()` メソッドは、選ばれた項目をキーと値の組として返します。 ```php $form->addMultiSelect('countries', '国:', $countries); ``` -提供される項目の配列は、3番目のパラメータまたは `setItems()` メソッドで渡します。項目は2次元配列にすることもできます。 +提示する項目の配列は第 3 パラメータで渡すか、`setItems()` メソッドで渡します。項目は 2 次元の配列にもできます。 -`setDisabled(['CZ', 'SK'])` を使用して、個々の項目を無効にできます。 +個々の項目を無効にするには `setDisabled(['CZ', 'SK'])` を使います。 -要素は、改ざんが発生していないこと、選択された項目が実際に提供されたものの1つであり、無効にされていないことを自動的にチェックします。`getRawValue()` メソッドを使用すると、この重要なチェックなしで送信された項目を取得できます。 +この要素は、偽造がなかったこと、そして選ばれた項目が本当に提示されたものの中にあって無効にされていないことを自動的に確かめます。この大事な検査なしに送信された項目を取り出したいなら `getRawValue()` メソッドを使えます。 -デフォルトで選択された項目を設定する場合も、それらが提供されたものの1つであることを確認します。そうでない場合は例外をスローします。このチェックは `checkDefaultValue(false)` で無効にできます。 +既定で選ばれる項目を設定するときも、それが提示されたものの中にあるかを確かめ、なければ例外を投げます。この検査は `checkDefaultValue(false)` で切れます。 -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -ファイルアップロード用のフィールド(クラス[UploadControl |api:Nette\Forms\Controls\UploadControl])を追加します。ユーザーがファイルを送信しなかった場合でも、[FileUpload |http:request#FileUpload] オブジェクトを返します。これは `FileUpload::hasFile()` メソッドで確認できます。 +ファイルのアップロードの項目を足します(クラス [UploadControl |api:Nette\Forms\Controls\UploadControl])。利用者がファイルをアップロードしなかった場合も [FileUpload |http:request#FileUpload]オブジェクトを返します。それは `FileUpload::hasFile()` メソッドで確かめられます。`setNullable()` を使うと、ファイルがアップロードされなかったときに `FileUpload` オブジェクトではなく `null` を返させられます。 ```php $form->addUpload('avatar', 'アバター:') - ->addRule($form::Image, 'アバターはJPEG、PNG、GIF、WebP、またはAVIFである必要があります。') - ->addRule($form::MaxFileSize, '最大サイズは1MBです。', 1024 * 1024); + ->addRule($form::Image, 'アバターは JPEG、PNG、GIF、WebP、AVIF でなければなりません。') + ->addRule($form::MaxFileSize, '大きさの上限は 1 MB です。', 1024 * 1024); ``` -ファイルが正しくアップロードされなかった場合、フォームは正常に送信されず、エラーが表示されます。つまり、正常に送信された場合、`FileUpload::isOk()` メソッドを確認する必要はありません。 +ファイルが正しくアップロードされなければ、フォームの送信は成功せず、エラーが表示されます。つまり送信が成功したなら、`FileUpload::isOk()` メソッドを確かめる必要はありません。 -`FileUpload::getName()` メソッドによって返される元のファイル名を決して信用しないでください。クライアントは、アプリケーションを破損またはハッキングする意図で悪意のあるファイル名を送信した可能性があります。 +`FileUpload::getName()` メソッドが返すもとのファイル名は決して信じないでください。クライアントは、あなたのアプリケーションを壊したり乗っ取ったりするつもりで、悪意のあるファイル名を送っているかもしれません。 -`MimeType` および `Image` ルールは、ファイルのシグネチャに基づいて要求されたタイプを検出し、その整合性を検証しません。画像が破損していないかどうかは、たとえば[読み込み |http:request#toImage]を試みることで確認できます。 +`MimeType` と `Image` の規則は、求める種類をファイルの署名から見分けるもので、その健全さは確かめません。画像が壊れているかどうかは、たとえば[読み込んでみる |http:request#toImage()]ことで判断できます。 -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -複数のファイルを一度にアップロードするためのフィールド(クラス[UploadControl |api:Nette\Forms\Controls\UploadControl])を追加します。[FileUpload |http:request#FileUpload] オブジェクトの配列を返します。それぞれの `FileUpload::hasFile()` メソッドは `true` を返します。 +複数のファイルを一度にアップロードする項目を足します(クラス [UploadControl |api:Nette\Forms\Controls\UploadControl])。[FileUpload |http:request#FileUpload]オブジェクトの配列を返します。そのそれぞれで `FileUpload::hasFile()` メソッドは `true` を返します。 ```php $form->addMultiUpload('files', 'ファイル:') - ->addRule($form::MaxLength, '最大 %d ファイルまでアップロードできます', 10); + ->addRule($form::MaxLength, 'アップロードできるファイルは %d 個までです。', 10); ``` -いずれかのファイルが正しくアップロードされなかった場合、フォームは正常に送信されず、エラーが表示されます。つまり、正常に送信された場合、`FileUpload::isOk()` メソッドを確認する必要はありません。 +どれかのファイルが正しくアップロードされなければ、フォームの送信は成功せず、エラーが表示されます。つまり送信が成功したなら、ファイルごとに `FileUpload::isOk()` メソッドを確かめる必要はありません。 -`FileUpload::getName()` メソッドによって返される元のファイル名を決して信用しないでください。クライアントは、アプリケーションを破損またはハッキングする意図で悪意のあるファイル名を送信した可能性があります。 +`FileUpload::getName()` メソッドが返すもとのファイル名は決して信じないでください。クライアントは、あなたのアプリケーションを壊したり乗っ取ったりするつもりで、悪意のあるファイル名を送っているかもしれません。 -`MimeType` および `Image` ルールは、ファイルのシグネチャに基づいて要求されたタイプを検出し、その整合性を検証しません。画像が破損していないかどうかは、たとえば[読み込み |http:request#toImage]を試みることで確認できます。 +`MimeType` と `Image` の規則は、求める種類をファイルの署名から見分けるもので、その健全さは確かめません。画像が壊れているかどうかは、たとえば[読み込んでみる |http:request#toImage()]ことで判断できます。 -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== -ユーザーが年、月、日で構成される日付を簡単に入力できるフィールド(クラス[DateTimeControl |api:Nette\Forms\Controls\DateTimeControl])を追加します。 +年、月、日から成る日付を簡単に入力できる項目を足します(クラス [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl])。 -デフォルト値として、`DateTimeInterface` インターフェースを実装するオブジェクト、時間を含む文字列、またはUNIXタイムスタンプを表す数値を受け入れます。最小および最大許容日付を定義する `Min`、`Max`、または `Range` ルールの引数についても同様です。 +既定値としては、`DateTimeInterface` を実装するオブジェクト、時刻を含む文字列、UNIX タイムスタンプを表す数を受け取ります。許される最小と最大の日付を定める `Min`、`Max`、`Range` の規則の引数も同じです。 ```php $form->addDate('date', '日付:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, '日付は少なくとも1か月前である必要があります。', new DateTime('-1 month')); + ->addRule($form::Min, '日付は少なくとも 1 か月前でなければなりません。', new DateTime('-1 month')); ``` -デフォルトでは `DateTimeImmutable` オブジェクトを返します。`setFormat()` メソッドを使用して、[テキスト形式|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]またはタイムスタンプを指定できます: +既定では `DateTimeImmutable` オブジェクトを返します。`setFormat()` メソッドで、[テキストの書式|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]やタイムスタンプを指定できます。 ```php $form->addDate('date', '日付:') @@ -287,40 +288,40 @@ $form->addDate('date', '日付:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -ユーザーが時、分、およびオプションで秒で構成される時間を簡単に入力できるフィールド(クラス[DateTimeControl |api:Nette\Forms\Controls\DateTimeControl])を追加します。 +時、分、そして必要なら秒から成る時刻を簡単に入力できる項目を足します(クラス [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl])。 -デフォルト値として、`DateTimeInterface` インターフェースを実装するオブジェクト、時間を含む文字列、またはUNIXタイムスタンプを表す数値を受け入れます。これらの入力からは時間情報のみが使用され、日付は無視されます。最小および最大許容時間を定義する `Min`、`Max`、または `Range` ルールの引数についても同様です。最小値が最大値より大きい場合、深夜を超える時間範囲が作成されます。 +既定値としては、`DateTimeInterface` を実装するオブジェクト、時刻を含む文字列、UNIX タイムスタンプを表す数を受け取ります。そこから使われるのは時刻の情報だけで、日付は無視されます。許される最小と最大の時刻を定める `Min`、`Max`、`Range` の規則の引数も同じです。設定した最小値が最大値より大きい場合は、真夜中をまたぐ時刻の範囲になります。 ```php -$form->addTime('time', '時間:', withSeconds: true) - ->addRule($form::Range, '時間は %d から %d の範囲である必要があります。', ['12:30', '13:30']); +$form->addTime('time', '時刻:', withSeconds: true) + ->addRule($form::Range, '時刻は %d から %d のあいだでなければなりません。', ['12:30', '13:30']); ``` -デフォルトでは `DateTimeImmutable` オブジェクト(日付は1月1日)を返します。`setFormat()` メソッドを使用して、[テキスト形式|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]を指定できます: +既定では `DateTimeImmutable` オブジェクトを返します(日付は 1 年 1 月 1 日になります)。`setFormat()` メソッドで[テキストの書式|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]を指定できます。 ```php -$form->addTime('time', '時間:') +$form->addTime('time', '時刻:') ->setFormat('H:i'); ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -ユーザーが年、月、日、時、分、およびオプションで秒で構成される日付と時間を簡単に入力できるフィールド(クラス[DateTimeControl |api:Nette\Forms\Controls\DateTimeControl])を追加します。 +年、月、日、時、分、そして必要なら秒から成る日付と時刻の両方を簡単に入力できる項目を足します(クラス [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl])。 -デフォルト値として、`DateTimeInterface` インターフェースを実装するオブジェクト、時間を含む文字列、またはUNIXタイムスタンプを表す数値を受け入れます。最小および最大許容日付を定義する `Min`、`Max`、または `Range` ルールの引数についても同様です。 +既定値としては、`DateTimeInterface` を実装するオブジェクト、時刻を含む文字列、UNIX タイムスタンプを表す数を受け取ります。許される最小と最大の日付と時刻を定める `Min`、`Max`、`Range` の規則の引数も同じです。 ```php -$form->addDateTime('datetime', '日時:') +$form->addDateTime('datetime', '日付と時刻:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, '日付は少なくとも1か月前である必要があります。', new DateTime('-1 month')); + ->addRule($form::Min, '日付は少なくとも 1 か月前でなければなりません。', new DateTime('-1 month')); ``` -デフォルトでは `DateTimeImmutable` オブジェクトを返します。`setFormat()` メソッドを使用して、[テキスト形式|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]またはタイムスタンプを指定できます: +既定では `DateTimeImmutable` オブジェクトを返します。`setFormat()` メソッドで、[テキストの書式|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]やタイムスタンプを指定できます。 ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -色を選択するためのフィールド(クラス[ColorPicker |api:Nette\Forms\Controls\ColorPicker])を追加します。色は `#rrggbb` 形式の文字列です。ユーザーが選択しない場合、黒色 `#000000` が返されます。 +色を選ぶ項目を足します(クラス [ColorPicker |api:Nette\Forms\Controls\ColorPicker])。色は `#rrggbb` の書式の文字列として返されます。利用者が何も選ばなければ、黒 `#000000` を返します。 ```php $form->addColor('color', '色:') @@ -339,37 +340,46 @@ $form->addColor('color', '色:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== -隠しフィールド(クラス[HiddenField |api:Nette\Forms\Controls\HiddenField])を追加します。 +隠しの項目を足します(クラス [HiddenField |api:Nette\Forms\Controls\HiddenField])。 ```php $form->addHidden('userid'); ``` -`setNullable()` を使用して、空の文字列の代わりに `null` を返すように設定できます。送信された値を変更するには、[addFilter() |validation#入力の変更] を使用します。 +`setNullable()` を使うと、空の文字列ではなく `null` を返させられます。[addFilter() |validation#入力された値を変える]メソッドで、送信された値を変えられます。 -要素は隠されていますが、値は依然として攻撃者によって変更または偽造される可能性があることを**認識することが重要です**。データの操作に関連するセキュリティリスクを回避するために、サーバー側で受信したすべての値を常に徹底的に検証および検証してください。 +この要素は隠れていますが、**その値は攻撃者に書き換えられたり偽られたりしうる**ことを忘れないでください。データの改ざんにまつわるセキュリティリスクを防ぐために、受け取ったすべての値をサーバー側でいつも入念に確かめ、検証してください。 -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== -送信ボタン(クラス[SubmitButton |api:Nette\Forms\Controls\SubmitButton])を追加します。 +送信ボタンを足します(クラス [SubmitButton |api:Nette\Forms\Controls\SubmitButton])。 ```php $form->addSubmit('submit', '送信'); ``` -フォームには複数の送信ボタンを含めることができます: +.{data-version:3.3.0} +ハンドラは `onClick` イベントに結び付ける代わりに、第 3 パラメータ `$onSubmit` としてボタンに直接渡せます。 + +```php +$form->addSubmit('submit', '送信', function (SubmitButton $button, $data): void { + // ... +}); +``` + +フォームには送信ボタンを 2 つ以上置けます。 ```php $form->addSubmit('register', '登録'); $form->addSubmit('cancel', 'キャンセル'); ``` -どちらがクリックされたかを確認するには、次を使用します: +どれが押されたかを判断するには次のようにします。 ```php if ($form['register']->isSubmittedBy()) { @@ -377,48 +387,48 @@ if ($form['register']->isSubmittedBy()) { } ``` -ボタンを押したときにフォーム全体を検証したくない場合(たとえば、*キャンセル*または*プレビュー*ボタンの場合)、[setValidationScope() |validation#検証の無効化] を使用します。 +ボタンを押したときにフォーム全体を検証したくないなら(たとえば *キャンセル* や *プレビュー* のボタン)、[setValidationScope() |validation#検証を切る]を使ってください。 -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -送信機能を持たないボタン(クラス[Button |api:Nette\Forms\Controls\Button])を追加します。したがって、クリック時にJavaScript関数を呼び出すなど、他の機能に使用できます。 +送信の働きを持たないボタンを足します(クラス [Button |api:Nette\Forms\Controls\Button])。ですからほかの用途に、たとえばクリックしたときに JavaScript の関数を呼ぶのに使えます。 ```php -$form->addButton('raise', '給与を上げる') +$form->addButton('raise', '給料を上げる') ->setHtmlAttribute('onclick', 'raiseSalary()'); ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= -画像形式の送信ボタン(クラス[ImageButton |api:Nette\Forms\Controls\ImageButton])を追加します。 +画像の形の送信ボタンを足します(クラス [ImageButton |api:Nette\Forms\Controls\ImageButton])。 ```php -$form->addImageButton('submit', '/path/to/image'); +$form->addImageButton('submit', '/path/to/image.png', '送信'); ``` -複数の送信ボタンを使用する場合、`$form['submit']->isSubmittedBy()` を使用してどちらがクリックされたかを確認できます。 +送信ボタンを複数使うときは、`$form['submit']->isSubmittedBy()` でどれが押されたかを判断できます。 addContainer(string|int $name): Container .[method] =================================================== -サブフォーム(クラス[Container|api:Nette\Forms\Container])、つまりコンテナを追加します。これには、フォームに追加するのと同じ方法で他の要素を追加できます。`setDefaults()` または `getValues()` メソッドも機能します。 +下位のフォーム(クラス [Container|api:Nette\Forms\Container])、つまりコンテナを足します。そこにはフォームと同じやり方でほかの要素を足せます。`setDefaults()` や `getValues()` のようなメソッドも働きます。 ```php $sub1 = $form->addContainer('first'); -$sub1->addText('name', 'あなたの名前:'); -$sub1->addEmail('email', 'メールアドレス:'); +$sub1->addText('name', 'お名前:'); +$sub1->addEmail('email', 'メール:'); $sub2 = $form->addContainer('second'); -$sub2->addText('name', 'あなたの名前:'); -$sub2->addEmail('email', 'メールアドレス:'); +$sub2->addText('name', 'お名前:'); +$sub2->addEmail('email', 'メール:'); ``` -送信されたデータは、多次元構造として返されます: +送信されたデータは多次元の構造として返されます。 ```php [ @@ -434,69 +444,67 @@ $sub2->addEmail('email', 'メールアドレス:'); ``` -設定の概要 +設定の一覧 ===== -すべての要素で、次のメソッドを呼び出すことができます(完全な概要は[APIドキュメント|https://api.nette.org/forms/master/Nette/Forms/Controls.html]を参照): +すべての要素で次のメソッドを呼べます(完全な一覧は [API のドキュメント|https://api.nette.org/forms/master/Nette/Forms/Controls.html]をご覧ください)。 .[table-form-methods language-php] -| `setDefaultValue($value)` | デフォルト値を設定します -| `getValue()` | 現在の値を取得します -| `setOmitted()` | [#値の省略] -| `setDisabled()` | [#要素の無効化] +| `setDefaultValue($value)` | 既定値を設定します +| `getValue()` | 今の値を取り出します +| `setOmitted()` | [#外される値] +| `setDisabled()` | [#入力を無効にする] -レンダリング: +描画: .[table-form-methods language-php] -| `setCaption($caption)` | 要素のキャプションを変更します -| `setTranslator($translator)` | [トランスレータ |rendering#翻訳]を設定します -| `setHtmlAttribute($name, $value)` | 要素の[HTML属性 |rendering#HTML属性]を設定します -| `setHtmlId($id)` | HTML属性 `id` を設定します -| `setHtmlType($type)` | HTML属性 `type` を設定します -| `setHtmlName($name)` | HTML属性 `name` を設定します -| `setOption($key, $value)` | [レンダリング設定 |rendering#Options] - -検証: +| `setCaption($caption)` | 要素のラベルを変えます +| `setTranslator($translator)` | [翻訳器 |rendering#翻訳]を設定します +| `setHtmlAttribute($name, $value)` | 要素に [HTML の属性 |rendering#HTML の属性]を設定します +| `setHtmlId($id)` | HTML の `id` 属性を設定します +| `setOption($key, $value)` | [描画のオプションを設定します |rendering#オプション] + +検証: .[table-form-methods language-php] -| `setRequired()` | [必須要素 |validation] -| `addRule()` | [検証ルール |validation#ルール]を設定します -| `addCondition()`, `addConditionOn()` | [検証条件 |validation#条件]を設定します -| `addError($message)` | [エラーメッセージの受け渡し |validation#処理中のエラー] +| `setRequired()` | 要素を[必須 |validation]にします +| `addRule()` | [検証の規則 |validation#規則]を足します +| `addCondition()`, `addConditionOn()` | [検証の条件 |validation#条件]を設定します +| `addError($message)` | [エラーのメッセージを足します |validation#エラーの処理] -`addText()`、`addPassword()`、`addTextArea()`、`addEmail()`、`addInteger()` 要素では、次のメソッドを呼び出すことができます: +`addText()`、`addPassword()`、`addTextArea()`、`addEmail()`、`addInteger()`、`addFloat()` の要素では、次のメソッドを呼べます。 .[table-form-methods language-php] -| `setNullable()` | getValue() が空の文字列の代わりに `null` を返すかどうかを設定します -| `setEmptyValue($value)` | 空の文字列と見なされる特別な値を設定します -| `setMaxLength($length)` | 許可される最大文字数を設定します -| `addFilter($filter)` | [入力の変更 |validation#入力の変更] +| `setNullable()` | getValue() が空の文字列ではなく `null` を返すかを設定します +| `setEmptyValue($value)` | 空の文字列と見なす特別な値を設定します +| `setMaxLength($length)` | 許される文字数の上限を設定します +| `addFilter($filter)` | [入力を変えます |validation#入力された値を変える] -値の省略 -==== +外される値 +===== -ユーザーが入力した値に関心がない場合は、`setOmitted()` を使用して `$form->getValues()` メソッドの結果またはハンドラに渡されるデータから省略できます。これは、さまざまな確認用パスワード、アンチスパム要素などに役立ちます。 +利用者が埋めた値に関心がないなら、`setOmitted()` を使って `$form->getValues()` メソッドの結果やハンドラに渡されるデータからそれを外せます。パスワードの確認の項目やスパム対策の要素などに便利です。 ```php -$form->addPassword('passwordVerify', '確認用パスワード:') - ->setRequired('確認のため、もう一度パスワードを入力してください') +$form->addPassword('passwordVerify', 'パスワード(確認):') + ->setRequired('打ち間違いを確かめるために、もう一度パスワードを入力してください') ->addRule($form::Equal, 'パスワードが一致しません', $form['password']) ->setOmitted(); ``` -要素の無効化 -====== +入力を無効にする +======== -要素は `setDisabled()` で無効にできます。このような要素はユーザーが編集できません。 +要素は `setDisabled()` で無効にできます。無効な要素は利用者が編集できません。 ```php $form->addText('username', 'ユーザー名:') ->setDisabled(); ``` -無効化された要素はブラウザによってサーバーに送信されないため、`$form->getValues()` 関数によって返されるデータには含まれません。ただし、`setOmitted(false)` を設定すると、Netteはこれらのデータにデフォルト値を含めます。 +無効な要素はブラウザからサーバーへまったく送られないので、`$form->getValues()` 関数が返すデータの中にも現れません。とはいえ `setOmitted(false)` を設定すれば、Nette はその既定値をそのデータに含めます。 -`setDisabled()` を呼び出すと、セキュリティ上の理由から**要素の値が削除されます**。デフォルト値を設定する場合は、無効化した後に行う必要があります: +`setDisabled()` を呼ぶと、安全のために**その要素の値は消されます**。既定値を設定するなら、無効にしたあとで行う必要があります。 ```php $form->addText('username', 'ユーザー名:') @@ -504,42 +512,26 @@ $form->addText('username', 'ユーザー名:') ->setDefaultValue($userName); ``` -無効化された要素の代替として、HTML属性 `readonly` を持つ要素があります。これはブラウザによってサーバーに送信されます。要素は読み取り専用ですが、その値は依然として攻撃者によって変更または偽造される可能性があることを**認識することが重要です**。 +無効な要素の代わりになるのが、HTML の `readonly` 属性の付いた要素です。こちらはブラウザがサーバーへ送ります。読み取り専用の要素ではありますが、**その値は攻撃者に書き換えられたり偽られたりしうる**ことを忘れないでください。 -カスタム要素 -====== +独自の要素 +===== -組み込みの幅広いフォーム要素に加えて、次の方法でフォームにカスタム要素を追加できます: +幅広い組み込みのフォームの要素のほかに、フォームには独自の要素を足せます。 ```php $form->addComponent(new DateInput('日付:'), 'date'); -// 代替構文: $form['date'] = new DateInput('日付:'); +// 別の書き方: $form['date'] = new DateInput('日付:'); ``` -.[note] -フォームは[Container |component-model:#Container]クラスの子であり、個々の要素は[Component |component-model:#Component]の子です。 - -カスタム要素を追加するための新しいフォームメソッド(例:`$form->addZip()`)を定義する方法があります。これは拡張メソッドと呼ばれます。欠点は、エディタでの補完が機能しないことです。 - -```php -use Nette\Forms\Container; - -// メソッド addZip(string $name, ?string $label = null) を追加します -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, '少なくとも5桁の数字', '[0-9]{5}'); -}); - -// 使用法 -$form->addZip('zip', '郵便番号:'); -``` +そうした要素を、送信されたデータの読み出しや検証、描画も含めてどう書くかは、[独立した章 |custom-controls]で説明しています。そこでは `$form->addZip()` のような独自の追加のメソッドを作れる拡張のメソッドについても学べます。 -低レベル要素 +低水準の項目 ====== -テンプレートにのみ記述し、`$form->addXyz()` メソッドのいずれかでフォームに追加しない要素を使用することもできます。たとえば、データベースからレコードを出力し、事前にいくつあり、どのようなIDを持つかわからず、各行にチェックボックスまたはラジオボタンを表示したい場合、テンプレートでコーディングするだけで十分です: +テンプレートにだけ書かれ、`$form->addXyz()` のメソッドではフォームに足されていない要素も使えます。たとえばデータベースのレコードを並べるとき、その数も ID もあらかじめ分からず、行ごとにチェックボックスやラジオボタンを表示したい場合、テンプレートにそのまま書けます。 ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ $form->addZip('zip', '郵便番号:'); {/foreach} ``` -そして、送信後に値を確認します: +そして送信後に値を取り出します。 ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -ここで、最初のパラメータは要素のタイプ(`type=file` の場合は `DataFile`、`text`、`password`、`email` などの一行入力の場合は `DataLine`、その他すべての場合は `DataText`)であり、2番目のパラメータ `sel[]` はHTML属性 `name` に対応します。要素のタイプは、要素のキーを保持する `DataKeys` 値と組み合わせることができます。これは、特に `select`、`radioList`、`checkboxList` に役立ちます。 +第 1 パラメータは要素の種類(`type=file` なら `DataFile`、`text`、`password`、`email` などの 1 行の入力なら `DataLine`、そのほかはすべて `DataText`)で、第 2 パラメータの `sel[]` は HTML の name 属性に対応します。要素の種類は `DataKeys` の値と組み合わせられ、そうすると要素のキーが保たれます。これは `select`、`radioList`、`checkboxList` でとりわけ便利です。 -重要なのは、`getHttpData()` がサニタイズされた値を返すことです。この場合、攻撃者がサーバーに何を送信しようとしても、常に有効なUTF-8文字列の配列になります。これは、`$_POST` または `$_GET` を直接操作するのと似ていますが、重要な違いは、常にクリーンなデータを返すことです。これは、標準のNetteフォーム要素で慣れているとおりです。 +大事なのは、`getHttpData()` が清められた値を返すことです。この場合、攻撃者がサーバーへ何を送ろうとしても、結果はいつも正しい UTF-8 の文字列の配列になります。これは `$_POST` や `$_GET` を直接扱うのに似ていますが、Nette の標準のフォームの要素で慣れているのと同じく、いつもきれいなデータが返るという大きな違いがあります。 diff --git a/forms/ja/custom-controls.texy b/forms/ja/custom-controls.texy new file mode 100644 index 0000000000..1920785e58 --- /dev/null +++ b/forms/ja/custom-controls.texy @@ -0,0 +1,268 @@ +カスタムフォーム要素 +********** + +.[perex] +Nette は幅広い[組み込みのフォームの要素 |controls]をそろえています。しかしその中にない要求に出くわしたときも、何かを迂回したり継ぎはぎしたりする必要はありません。自分の要素を書けばよいのです。それは組み込みのものにできることをすべてこなせます。検証も、自分の翻訳も、描画もです。そして使い方もまったく同じです。 + +実用的な例でお見せしましょう。日、月、年の 3 つの項目で日付を入力する要素です。その道すがら、要素を書くのに必要なことをすべて学べます。 + + +独自の要素を書くべきときと、書かなくてよいとき +======================= + +独自の要素はフォームが差し出すもっとも強力な道具です。そして強力な道具の常として、それは最初ではなく最後の選択肢であるべきです。多くの場面はもっと簡単な手段で片付きます。 + +- **値を変える**のは [addFilter() |validation#入力された値を変える]の仕事です。郵便番号の空白やコードの小文字を大目に見たいですか。フィルタなら数行です。 +- **繰り返す設定**は独自の追加のメソッドで包みます。同じ検証の付いた郵便番号の項目を 10 か所で足していますか。名前の付いた近道を作りましょう。[最後にお見せします |#独自の追加のメソッド]。 +- **関連する項目のまとまり**には[コンテナ |controls#addContainer()]があります。通り、市、郵便番号から成る住所に独自の要素は要りません。テキストの項目 3 つのコンテナで十分です。 +- **見た目を変える**には [setHtmlType() |controls#addText()]と HTML の属性、あるいは[プロトタイプ |rendering#プロトタイプ]を使います。 + +独自の要素が意味を持つのは、**独自の値**が必要になった瞬間です。外から見るとひとつの値を持つひとつの項目として振る舞うのに、内側では複数の入力から成っていたり、表示とは違う形で値を持っていたりする要素です。3 つの項目から成る日付。地図をクリックして選ぶ座標。補完の付くタグの入力。 + + +要素の解剖 +===== + +どの独自の要素も、抽象クラス [api:Nette\Forms\Controls\BaseControl]を継承します。そこから、できあいの機能を山ほど受け継ぎます。値の保持、検証の規則と条件、エラーのメッセージ、翻訳、HTML の属性、ラベル、そして描画とのつながりです。あなたが書くのは、その要素を違うものにしている部分だけです。 + +動く最小限の要素は驚くほど短いものです。 + +```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, + ]); + } +} +``` + +メソッドは 2 つ。ひとつは送信されたデータから値をどう取り出すかを、もうひとつは要素をどう描くかを伝えます。どちらもこのあと詳しく見ます。そのほかのすべて、`setRequired()`、`addRule()`、`setDefaultValue()`、翻訳は、もう自分で働きます。 + +要素は `addComponent()` メソッドで、あるいはもっと短く角かっこでフォームに足します。 + +```php +$form['nickname'] = new SimpleInput('ニックネーム:'); +``` + + +要素のライフサイクル +========== + +もっと面白い要素に進む前に、要素に何がいつ起きるかを知っておくとよいでしょう。フォームとその要素は木を作る[コンポーネント |component-model:]です。これにはうれしい結果がひとつあります。要素は自分で何かを調べる必要がなく、大事なことはすべてフレームワークが適切な瞬間に片付けてくれるのです。 + +1) 要素を送信されたフォームに取り付けた瞬間、フォーム自身がその要素の `loadHttpData()` を呼びます。その中で要素は、このあとお見せするように、送信された自分の値を読みます。`$_POST` を直接扱うことは決してなく、自分がコンテナに入れ子になっているかどうかもまったく気にする必要がありません。 + +2) フォームが送信されると検証が行われます。`addRule()` で足した規則が評価され、`getValue()` の値が使われます。 + +3) そのあと `$form->getValues()` や要素の `getValue()` を呼ぶ人は、きれいで型の付いた値を受け取ります。フォームからの 3 つの文字列ではなく、たとえば `DateTimeImmutable` オブジェクトです。 + +そして描画のときには `getControl()` が、ラベルには `getLabel()` が呼ばれます。 + + +送信された値を読む +========= + +`loadHttpData()` メソッドの中で、要素は `getHttpData()` メソッドを使って送信された自分の値を求めます。そのパラメータは、値をどう清めるかを決める種類です。 + +| 種類 | 意味 +|------- +| `Form::DataLine` | 1 行のテキスト: 改行を空白に置き換え、前後の空白を取り除きます +| `Form::DataText` | 複数行のテキスト: 改行を `\n` にそろえます +| `Form::DataFile` | アップロード。`Nette\Http\FileUpload` のインスタンス + +攻撃者がどれだけ頑張っても、結果はいつも制御文字のない正しい UTF-8 の文字列(あるいはアップロードのオブジェクトか `null`)です。値を `$_POST` から直接読まないのは、まさにこのためです。読んでしまえばこれらの保証をすべて失います。 + +私たちの日付のように複数の入力から成る要素は、HTML の名前の一部を第 2 パラメータとして渡し、こうして個々の部分の値を読みます。それらは string 型の自分のプロパティ `$day`、`$month`、`$year` に持ちます。 + +```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]') ?? ''; +} +``` + +HTML の名前が `[]` で終わる場合は、値の配列が返されます。`Form::DataKeys` の種類と組み合わせると(つまり `Form::DataLine | Form::DataKeys`)、そのキーも保てます。 + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +値がなければ `null` です(配列なら空の配列)。リクエストにその要素のデータがまったく含まれていないこともありますし、攻撃者が好きなものを送るのを妨げるものもありません。例で `?? ''` を足しているのはそのためで、あなたもいつもこの場合を考えに入れるべきです。 + + +要素の値 +==== + +要素は自分の値を持ち、3 つのメソッドを通してそれを見せます。その約束事は守る値のあるものです。 + +`setValue()` メソッドはプログラマーから値を受け取ります。`setDefaultValue()` や `$form->setDefaults()` もこの道を通ります。このメソッドは筋の通るものはすべて受け取り、値を内部の形に変え、意味をなさない入力には例外を投げるべきです。そうすればエラーはすぐに現れ、フォームの不可解な振る舞いとして現れることがありません。私たちの日付は `DateTimeInterface`、文字列、タイムスタンプ、`null` を受け取り、3 つの項目に分けます。 + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // 意味をなさないものは例外を投げます + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +一方 `getValue()` メソッドは、きれいで型の付いた値を組み立てます。あなたの要素を使う人が目にするのはこれだけです。値が妥当でなければ `null` を返します。静的メソッド `validateDate()` は、3 つの項目が実在する日付になるかを確かめるだけです。 + +```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; +} +``` + +そして `isFilled()` メソッドは、利用者がその要素を埋めたかどうかを伝えます。これは `setRequired()` の規則が使います。既定の実装(空でない値)で足りることが多いのですが、組み合わせの要素ではその論理に合わせて上書きしてください。 + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +描画 +=== + +`getControl()` メソッドは要素の HTML の形を返します。ふつうは [Html |utils:html-elements]オブジェクトとしてですが、ただの文字列でも構いません。そこは問いません。Html オブジェクトに手を伸ばすのは主にコードを組み立てるときで、できあがるマークアップを安全に、しかも心地よい API で作れるからです。いくつかの助っ人が使えます。 + +- `getHtmlName()` は、コンテナへの入れ子も含めた HTML の `name` 属性を返します(たとえば `invoice[date]`)。組み合わせの要素では、そこに個々の入力の名前の部分を足します。`$name . '[day]'` のようにです。 +- `getHtmlId()` はラベルと結び付けられた `id` 属性を返します。 +- `Helpers::exportRules($this->getRules())` は `data-nette-rules` 属性のために検証の規則を書き出します。おかげであなたの要素でも [JavaScript の検証 |validation#JavaScript での検証]が働きます。この属性は要素の最初の入力に付けます。 +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` は項目の配列から `<select>` 要素を組み立てて(入れ子の配列は `<optgroup>` として描かれます)`Html` として返します。私たちの日付の月の項目に便利です。 +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` は `<label>` で包まれた `<input>` 要素の一覧(ラジオボタンやチェックボックス)を生成して文字列として返します。 + +ですから私たちの日付の最初の項目は、次のように作ります。 + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... 月の select と年の input ... */); +} +``` + +ラベルは `getLabel()` が描き、その既定の実装でたいてい足ります。ただし気をつけてください。組み合わせの要素では、その `for` 属性が `getHtmlId()` を指すので、この id は最初の入力に与えてください。例のとおりです。 + +組み合わせの要素をテンプレートで部分ごとに描けるようにするには(たとえば `{input birthdate:day}`)、`getControlPart($key)` と `getLabelPart($key)` メソッドを上書きします。これらはその部分の `Html` 要素を返します。`CheckboxList` や `RadioList` と同じやり方です。 + +.[note] +`getControl()` を上書きするときは、`BaseControl::getControl()` が `setOption('rendered', true)` で要素を描画済みと印を付けることも忘れないでください。同じフォームで手動の描画と自動の描画を混ぜるときは、これも呼ぶ(あるいは `parent::getControl()` を呼ぶ)ようにしてください。そうすれば要素が二度描かれません。(上の `DateInput` の例では短さのために省いています。) + + +完全な例: DateInput +=============== + +ここまで説明した部品をすべて合わせ、月を選ぶセレクトボックスを添えたものが、[リポジトリの例 |https://github.com/nette/forms/blob/master/examples/custom-control.php]の中のできあがった `DateInput` 要素です。 + +コンストラクタの中で、この要素が日付として筋が通るかを確かめる検証の規則を自分に足していることに注目してください。2 月 31 日のような意味をなさない入力は、こうしてふつうのフォームの検証のエラーとして現れます。 + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), '日付が正しくありません。'); +} +``` + +そして使い方は。組み込みの要素とまったく同じです。 + +```php +$form['birthdate'] = (new DateInput('生年月日:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('生まれた日を入力してください'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +Latte のテンプレートでは、ほかの要素と同じく、いつもの `{input birthdate}` や `{label birthdate /}` のタグで描きます。 + + +検証 +=== + +組み込みの検証の規則は独自の要素でもそのまま働きます。`getValue()` の値を扱うからです。ですから私たちの `DateInput` では、たとえば許される最も古い日付に `Form::Min` を使えます。JavaScript 側も含めて自分の規則を書く方法は、[独自の規則と条件 |validation#独自の規則と条件]の章で説明しています。 + + +独自の追加のメソッド +========== + +組み込みの要素は `$form->addText()` などの便利なメソッドで足します。独自の要素にはそうしたメソッドがないので、ただの代入で足します。フォームでもコンテナでも同じように働き、エディタも静的な解析もそれを理解します。 + +```php +$form['birthdate'] = new DateInput('生年月日:'); +``` + +補完を保ったまま追加を短くしたいなら、要素そのものに静的なファクトリメソッドを置くと便利です。これは入れ子のコンテナでも働きます。`Form` クラスの子孫に置いたメソッドではそうはいきません。入れ子のコンテナはそれを知らないからです。 + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// フォームでも、どのコンテナでも働きます: +DateInput::addTo($form, 'birthdate', '生年月日:'); +``` + +同じやり方は、組み込みの要素の繰り返す設定に名前の付いた近道を与えるのにも使えます。 + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, '郵便番号はちょうど 5 桁でなければなりません', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', '郵便番号:'); +``` diff --git a/forms/ja/in-presenter.texy b/forms/ja/in-presenter.texy index 77d943bd88..a1faebb0b2 100644 --- a/forms/ja/in-presenter.texy +++ b/forms/ja/in-presenter.texy @@ -1,16 +1,16 @@ -Presenter内のフォーム -*************** +プレゼンターでのフォーム +************ .[perex] -Nette Formsは、Webフォームの作成と処理を大幅に簡素化します。この章では、Presenter内でフォームを使用する方法を学びます。 +Nette Forms はウェブのフォームの作成と処理を大いに簡単にします。この章では、プレゼンターの中でフォームを使う方法を学びます。 -フレームワークの残りの部分なしで完全にスタンドアロンで使用する方法に興味がある場合は、[スタンドアロンでの使用|standalone]のガイドが用意されています。 +フレームワークのほかの部分なしで、まったく単独で使うことに関心があるなら、[単独での利用|standalone]の案内があります。 -最初のフォーム -======= +はじめてのフォーム +========= -簡単な登録フォームを作成してみましょう。そのコードは次のようになります: +単純な登録のフォームを書いてみましょう。そのコードは次のようになります。 ```php use Nette\Application\UI\Form; @@ -19,16 +19,16 @@ $form = new Form; $form->addText('name', '名前:'); $form->addPassword('password', 'パスワード:'); $form->addSubmit('send', '登録'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` -そして、ブラウザでは次のように表示されます: +そしてブラウザには次のように表示されます。 -[* form-cs.webp *] +[* form-en.webp *] -Presenter内のフォームは `Nette\Application\UI\Form` クラスのオブジェクトであり、その前身である `Nette\Forms\Form` はスタンドアロンでの使用を目的としています。名前、パスワード、送信ボタンといういわゆる要素を追加しました。そして最後に、`$form->onSuccess` の行は、送信され、正常に検証された後、`$this->formSucceeded()` メソッドを呼び出す必要があることを示しています。 +プレゼンターの中のフォームは `Nette\Application\UI\Form` クラスのオブジェクトです。その前身の `Nette\Forms\Form` は単独で使うためのものです。私たちは name と password という名前の要素と、送信のボタンを足しました。最後の `$form->onSuccess` の行は、送信されて検証を通ったあとに `$this->formSucceeded()` メソッドを呼ぶべきだと伝えています。 -Presenterの観点から見ると、フォームは通常のコンポーネントです。したがって、コンポーネントとして扱われ、[ファクトリメソッド |application:components#ファクトリメソッド]を使用してPresenterに組み込まれます。次のようになります: +プレゼンターから見れば、フォームはふつうのコンポーネントです。ですからコンポーネントとして扱われ、[ファクトリメソッド |application:components#ファクトリメソッド]でプレゼンターに組み込まれます。次のようになります。 ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -42,22 +42,22 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', '名前:'); $form->addPassword('password', 'パスワード:'); $form->addSubmit('send', '登録'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { - // ここでフォームから送信されたデータを処理します - // $data->name には名前が含まれます - // $data->password にはパスワードが含まれます - $this->flashMessage('正常に登録されました。'); + // ここでフォームから送られたデータを処理します + // $data->name に名前が入っています + // $data->password にパスワードが入っています + $this->flashMessage('登録が完了しました。'); $this->redirect('Home:'); } } ``` -そして、テンプレートでは `{control}` タグを使用してフォームをレンダリングします: +そしてテンプレートでは、フォームは `{control}` タグで描かれます。 ```latte .{file:app/Presentation/Home/default.latte} <h1>登録</h1> @@ -65,74 +65,76 @@ class HomePresenter extends Nette\Application\UI\Presenter {control registrationForm} ``` -そして、それがすべてです :-) 機能的で完全に[保護された |#脆弱性からの保護]フォームがあります。 +これで基本はおしまいです :-) 動いて、しかも完璧に[守られた |#弱点からの保護]フォームができました。 -そして今、あなたはおそらくそれが速すぎたと思って、`formSucceeded()` メソッドがどのように呼び出されるのか、そしてそれが受け取るパラメータは何なのか疑問に思っているでしょう。確かに、あなたは正しいです、これは説明に値します。 +きっと今、話が速すぎる、`formSucceeded()` メソッドが呼ばれるのはどうしてで、どんなパラメータを受け取るのかと思っていることでしょう。そのとおりで、これは説明に値します。 -Netteは、[ハリウッドスタイル |application:components#ハリウッドスタイル]と呼ばれる新鮮なメカニズムを導入しています。開発者として常に何かが起こったかどうか(「フォームは送信されましたか?」、「有効に送信されましたか?」、「改ざんされませんでしたか?」)を尋ねる代わりに、フレームワークに「フォームが有効に記入されたら、このメソッドを呼び出して」と言い、残りの作業を任せます。JavaScriptでプログラミングしている場合、このプログラミングスタイルには精通しています。特定の[イベント |nette:glossary#イベント]が発生したときに呼び出される関数を記述します。そして、言語はそれらに適切な引数を渡します。 +Nette は [ハリウッド流 |application:components#ハリウッド流]と呼ばれる新鮮なしくみを持ち込みます。開発者であるあなたが絶えず「フォームは送信されたか」「正しく送信されたか」「偽造されていないか」と問い続ける代わりに、「フォームが正しく埋められたら、このメソッドを呼んで」とフレームワークに伝えて、あとの仕事を任せます。JavaScript でプログラムしているなら、この書き方はよくご存じでしょう。ある[イベント |nette:glossary#イベント]が起きたときに呼ばれる関数を書き、言語が適切な引数をそこに渡してくれます。 -上記Presenterコードもまさにこのように構築されています。`$form->onSuccess` 配列は、フォームが送信され、正しく記入された(つまり、有効である)瞬間にNetteが呼び出すPHPコールバックのリストを表します。[presenterのライフサイクル |application:presenters#Presenterのライフサイクル]のコンテキストでは、これはいわゆるシグナルであり、`action*` メソッドの後、`render*` メソッドの前に呼び出されます。そして、各コールバックに最初のパラメータとしてフォーム自体を渡し、2番目のパラメータとして送信されたデータを[ArrayHash |utils:arrays#ArrayHash]オブジェクト(または指定されたクラス)の形式で渡します。フォームオブジェクトが必要ない場合は、最初のパラメータを省略できます。そして、2番目のパラメータはより賢くなることができますが、それについては[後で |#クラスへのマッピング]説明します。 +上のプレゼンターのコードは、まさにこのように組み立てられています。`$form->onSuccess` 配列は、フォームが送信されて正しく埋められた(つまり妥当な)瞬間に Nette が呼ぶ PHP のコールバックの一覧です。[プレゼンターのライフサイクル |application:presenters#プレゼンターのライフサイクル]の中ではこれはいわゆるシグナルなので、`action*` メソッドのあと、`render*` メソッドの前に呼ばれます。そしてそれぞれのコールバックには、第 1 パラメータとしてフォームそのものを、第 2 パラメータとして送信されたデータを [ArrayHash |utils:arrays#ArrayHash]オブジェクト(あるいは stdClass や独自のクラス)として渡します。フォームのオブジェクトが要らなければ第 1 パラメータは省けます。第 2 パラメータはもっと賢くできますが、それは[のちほど |#クラスへの対応づけ]。 -`$data` オブジェクトには、ユーザーが入力したデータを含む `name` および `password` プロパティが含まれています。通常、データはさらに処理するために直接送信されます。たとえば、データベースへの挿入などです。ただし、処理中にエラーが発生する可能性があります。たとえば、ユーザー名がすでに使用されている場合などです。その場合、`addError()` を使用してエラーをフォームに戻し、エラーメッセージとともに再度レンダリングさせます。 +`$data` オブジェクトには、利用者が入力したデータの入った `name` と `password` のプロパティがあります。ふつうはそのデータをそのまま次の処理へ、たとえばデータベースへの挿入へ送ります。しかし処理の途中でエラーが起きることもあります。たとえばそのユーザー名がすでに使われている場合です。そんなときは `addError()` でエラーをフォームに返し、エラーのメッセージとともにもう一度描かせます。 ```php -$form->addError('申し訳ありませんが、そのユーザー名は既に使用されています。'); +$form->addError('申し訳ありません、そのユーザー名はすでに使われています。'); ``` -`onSuccess` に加えて、`onSubmit` もあります:コールバックは、フォームが送信されたときに常に呼び出されます。正しく記入されていない場合でも。さらに `onError`:コールバックは、送信が有効でない場合にのみ呼び出されます。`onSuccess` または `onSubmit` で `addError()` を使用してフォームを無効にした場合でも呼び出されます。 +`onSuccess` のほかに `onSubmit` もあります。こちらのコールバックは、正しく埋められていなくてもフォームが送信されればいつでも呼ばれます。さらに `onError` もあり、こちらは送信が妥当でない場合にだけ呼ばれます。`onSuccess` の中で `addError()` を使ってフォームを妥当でなくした場合にも呼ばれます。 + +フォームを処理したあとは、別のページへリダイレクトします。これで *更新* や *戻る* のボタン、ブラウザの履歴をたどることによる、望まないフォームの再送信を防げます。 -フォームを処理した後、次のページにリダイレクトします。これにより、*更新*ボタン、*戻る*ボタン、またはブラウザ履歴の移動によってフォームが意図せず再送信されるのを防ぎます。 +フォームが AJAX で送信された場合は、ふつうリダイレクトの代わりに、描き直したフォームを含む[スニペット |application:ajax]を再描画します。 -他の[フォーム要素|controls]も追加してみてください。 +ほかの[フォームの要素|controls]も足してみてください。 要素へのアクセス ======== -フォームはPresenterのコンポーネントであり、この場合は `registrationForm` という名前です(ファクトリメソッド `createComponentRegistrationForm` の名前に基づく)。したがって、Presenter内のどこからでもフォームにアクセスできます: +フォームはプレゼンターのコンポーネントで、この例ではファクトリメソッドの名前 `createComponentRegistrationForm` から `registrationForm` という名前です。ですからプレゼンターのどこからでも、次のようにフォームにアクセスできます。 ```php $form = $this->getComponent('registrationForm'); -// 代替構文: $form = $this['registrationForm']; +// 別の書き方: $form = $this['registrationForm']; ``` -フォームの個々の要素もコンポーネントであるため、同じ方法でアクセスできます: +個々のフォームの要素もコンポーネントなので、同じようにアクセスできます。 ```php $input = $form->getComponent('name'); // または $input = $form['name']; $button = $form->getComponent('send'); // または $button = $form['send']; ``` -要素はunsetを使用して削除されます: +要素は `unset` で取り除きます。 ```php unset($form['name']); ``` -検証ルール +検証の規則 ===== -*有効*という言葉が出ましたが、フォームにはまだ検証ルールがありません。それを修正しましょう。 +*妥当* という語が出てきましたが、フォームにはまだ検証の規則がひとつもありません。それを直しましょう。 -名前は必須なので、`setRequired()` メソッドでマークします。その引数は、ユーザーが名前を入力しなかった場合に表示されるエラーメッセージのテキストです。引数を指定しない場合は、デフォルトのエラーメッセージが使用されます。 +名前は必須にするので、`setRequired()` メソッドで印を付けます。その引数は、利用者が名前を埋めなかったときに表示されるエラーのメッセージの文です。引数を省くと、既定のエラーのメッセージが使われます。 ```php $form->addText('name', '名前:') - ->setRequired('名前を入力してください'); + ->setRequired('名前を入力してください。'); ``` -名前を入力せずにフォームを送信してみてください。エラーメッセージが表示され、フィールドに入力するまでブラウザまたはサーバーがそれを拒否することがわかります。 +名前を埋めずにフォームを送ってみてください。エラーのメッセージが表示され、その項目を埋めるまではブラウザかサーバーが受け付けないのが分かります。 -同時に、フィールドにスペースだけを入力してもシステムをだますことはできません。いいえ。Netteは左右の空白を自動的に削除します。試してみてください。これは、すべての一行入力で常に行うべきことですが、忘れられがちです。Netteはそれを自動的に行います。(フォームをだまして、名前として複数行の文字列を送信してみてください。ここでもNetteはだまされず、改行をスペースに変更します。) +同時に、たとえば項目に空白だけを入れて系をだますこともできません。無理です。Nette は前後の空白を自動的に取り除きます。試してみてください。1 行の入力ではいつもそうすべきことですが、よく忘れられます。Nette は自動的にやってくれます。(名前として複数行の文字列を送ってフォームをだまそうとしてみてください。ここでも Nette はだまされず、改行は空白に変えられます。) -フォームは常にサーバー側で検証されますが、JavaScript検証も生成されます。これは瞬時に実行され、ユーザーはフォームをサーバーに送信することなく、すぐにエラーを知ることができます。これは `netteForms.js` スクリプトが担当します。 レイアウトテンプレートに挿入します: +フォームはいつもサーバー側で検証されますが、JavaScript の検証も生成されます。これはすぐに走るので、利用者はフォームをサーバーへ送らなくてもエラーにすぐ気づけます。これは `netteForms.js` のスクリプトが受け持ちます。レイアウトのテンプレートに読み込んでください。 ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -フォームのあるページのソースコードを見ると、Netteが必須要素をHTML要素の `required` 属性としてマークしていることに気付くかもしれません(またはCSSクラス `required` を持つ要素に挿入)。テンプレートに次のスタイルシートを追加してみてください。「名前」ラベルが赤くなります。これにより、必須要素をユーザーにエレガントに示すことができます: +フォームのあるページのソースコードを見ると、Nette が必須の要素を `required` という CSS クラスの要素で包んでいるのに気づくかもしれません。次のスタイルシートをテンプレートに足してみてください。「名前」のラベルが赤くなります。これで必須の項目を利用者に優雅に示せます。 ```latte <style> @@ -140,85 +142,87 @@ $form->addText('name', '名前:') </style> ``` -他の検証ルールは `addRule()` メソッドで追加します。最初のパラメータはルール、2番目は再びエラーメッセージのテキストであり、検証ルールの引数が続く場合があります。これはどういう意味ですか? +さらに検証の規則を `addRule()` メソッドで足します。第 1 パラメータは規則、第 2 パラメータはやはりエラーのメッセージの文で、そのあとに検証の規則への引数が続くことがあります。それはどういうことでしょうか。 -フォームに新しいオプションのフィールド「年齢」を追加します。これは整数(`addInteger()`)であり、さらに許容範囲(`$form::Range`)内である必要があります。そして、ここで `addRule()` メソッドの3番目のパラメータを使用します。これにより、必要な範囲をペア `[from, to]` としてバリデータに渡します: +フォームに新しい、省略できる項目「年齢」を足しましょう。これは整数でなければならず(`addInteger()`)、しかも許される範囲に収まっていなければなりません(`$form::Range`)。ここでは `addRule()` メソッドの第 3 パラメータを使って、必要な範囲を `[最小, 最大]` の組として検証器に渡します。 ```php $form->addInteger('age', '年齢:') - ->addRule($form::Range, '年齢は18歳から120歳の間である必要があります', [18, 120]); + ->addRule($form::Range, '年齢は 18 歳から 120 歳のあいだでなければなりません。', [18, 120]); ``` .[tip] -ユーザーがフィールドを入力しない場合、要素はオプションであるため、検証ルールはチェックされません。 +利用者がその項目を埋めなければ、その要素は省略できるので検証の規則は確かめられません。 -ここでは、小さなリファクタリングの余地があります。エラーメッセージと3番目のパラメータで数値が重複して記載されており、これは理想的ではありません。[多言語フォーム |rendering#翻訳]を作成し、数値を含むメッセージが複数の言語に翻訳された場合、値の変更が困難になります。このため、プレースホルダー `%d` を使用でき、Netteが値を補完します: +ここでちょっとした整理の余地が生まれます。エラーのメッセージと第 3 パラメータで数が重複していて、あまり気持ちのよいものではありません。[多言語のフォーム |rendering#翻訳]を作っていて、数を含むメッセージが複数の言語に訳されていたら、値を変えるのが大変になります。ですから `%d` のプレースホルダを使えて、Nette が値を差し込んでくれます。 ```php - ->addRule($form::Range, '年齢は %d 歳から %d 歳の間である必要があります', [18, 120]); + ->addRule($form::Range, '年齢は %d 歳から %d 歳のあいだでなければなりません。', [18, 120]); ``` -`password` 要素に戻りましょう。これも必須にし、パスワードの最小長(`Form::MinLength`)も検証します。ここでもプレースホルダーを使用します: +`password` の要素に戻って、これも必須にし、あわせてパスワードの最小の長さも確かめましょう(`$form::MinLength`)。ここでもメッセージにプレースホルダを使います。 ```php $form->addPassword('password', 'パスワード:') - ->setRequired('パスワードを選択してください') - ->addRule($form::MinLength, 'パスワードは少なくとも %d 文字必要です', 8); + ->setRequired('パスワードを決めてください') + ->addRule($form::MinLength, 'パスワードは %d 文字以上でなければなりません。', 8); ``` -フォームにフィールド `passwordVerify` を追加します。ここでユーザーは確認のためにパスワードをもう一度入力します。検証ルールを使用して、両方のパスワードが同じかどうかを確認します(`Form::Equal`)。そして、パラメータとして、[角括弧 |#要素へのアクセス]を使用して最初のパスワードへの参照を与えます: +フォームにもうひとつ `passwordVerify` という項目を足しましょう。利用者は確認のためにもう一度パスワードを入力します。検証の規則を使って、2 つのパスワードが同じかを確かめます(`$form::Equal`)。引数としては、[角かっこ |#要素へのアクセス]で最初のパスワードへの参照を渡します。 ```php -$form->addPassword('passwordVerify', '確認用パスワード:') - ->setRequired('確認のため、もう一度パスワードを入力してください') - ->addRule($form::Equal, 'パスワードが一致しません', $form['password']) +$form->addPassword('passwordVerify', 'パスワード(確認):') + ->setRequired('打ち間違いを確かめるために、もう一度パスワードを入力してください') + ->addRule($form::Equal, 'パスワードが一致しません。', $form['password']) ->setOmitted(); ``` -`setOmitted()` を使用して、実際には値に関心がなく、検証目的でのみ存在する要素をマークしました。値は `$data` に渡されません。 +`setOmitted()` で、値そのものには関心がなく、検証のためだけに存在する要素だと印を付けました。その値は `$data` には渡されません。 -これで、PHPとJavaScriptの両方で検証を備えた完全に機能するフォームが完成しました。Netteの検証機能ははるかに広範であり、条件を作成したり、それらに基づいてページのパーツを表示および非表示にしたりできます。すべては[フォームの検証|validation]に関する章で学びます。 +これで、PHP と JavaScript の両方で検証が働く、しっかり動くフォームができました。Nette の検証の力はもっと広く、条件を作ったり、それに応じてページの一部を見せたり隠したりできます。すべては[フォームの検証|validation]の章で学べます。 -デフォルト値 -====== +既定値 +=== -フォーム要素には通常、デフォルト値を設定します: +フォームの要素には既定値をよく設定します。 ```php -$form->addEmail('email', 'メールアドレス') +$form->addEmail('email', 'メール') ->setDefaultValue($lastUsedEmail); ``` -すべての要素に同時にデフォルト値を設定すると便利なことがよくあります。たとえば、フォームがレコードの編集に使用される場合などです。データベースからレコードを読み取り、デフォルト値を設定します: +すべての要素に既定値を一度に設定できると便利なことがよくあります。たとえばフォームをレコードの編集に使う場合です。データベースからレコードを読んで既定値を設定します。 ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -要素を定義した後に `setDefaults()` を呼び出します。 +`setDefaults()` は要素を定義したあとに呼んでください。 + +すでに送信されたフォームでは `setDefaults()` は何もしません。利用者が入力したものを上書きしないので、フォームのファクトリの中で条件なしに呼んでも安全です。送信後にも値を強いる必要があるなら、代わりに `setValues()` を使ってください。 -フォームのレンダリング -=========== +フォームの描画 +======= -デフォルトでは、フォームはテーブルとしてレンダリングされます。個々の要素は基本的なアクセシビリティルールを満たしています - すべてのラベルは `<label>` として記述され、対応するフォーム要素に関連付けられています。ラベルをクリックすると、カーソルが自動的にフォームフィールドに表示されます。 +既定では、フォームは表として描かれます。個々の要素はウェブのアクセシビリティの基本の決まりに従っていて、すべてのラベルは `<label>` 要素として書かれ、それぞれのフォームの要素と結び付けられています。ラベルをクリックすると、自動的にフォームの項目にカーソルが移ります。 -各要素に任意のHTML属性を設定できます。たとえば、プレースホルダーを追加します: +要素ごとに好きな HTML の属性を設定できます。たとえばプレースホルダを足します。 ```php $form->addInteger('age', '年齢:') ->setHtmlAttribute('placeholder', '年齢を入力してください'); ``` -フォームをレンダリングする方法は本当にたくさんあるので、[レンダリングに関する別の章|rendering]があります。 +フォームを描く方法は本当にたくさんあるので、[描画についての独立した章|rendering]を用意しています。 -クラスへのマッピング -========== +クラスへの対応づけ +========= -2番目のパラメータ `$data` で送信されたデータを `ArrayHash` オブジェクトとして受け取る `formSucceeded()` メソッドに戻りましょう。これは `stdClass` のようなジェネリッククラスであるため、エディタでのプロパティの補完や静的コード分析など、特定の快適さが欠けています。これは、各フォームに特定のクラスを持たせることで解決できます。そのプロパティは個々の要素を表します。例: +`formSucceeded()` メソッドに戻りましょう。これは送信されたデータを第 2 パラメータ `$data` に `ArrayHash` オブジェクト(あるいは `stdClass`)として受け取ります。これは `stdClass` と同じような汎用のクラスなので、エディタのプロパティの補完や静的な解析といった便利さが得られません。これは、フォームごとに専用のクラスを用意し、そのプロパティが個々の要素を表すようにすれば解決できます。たとえば次のようにです。 ```php class RegistrationFormData @@ -229,7 +233,7 @@ class RegistrationFormData } ``` -または、コンストラクタを使用することもできます: +あるいはコンストラクタを使えます。 ```php class RegistrationFormData @@ -243,9 +247,9 @@ class RegistrationFormData } ``` -データクラスのプロパティはenumにすることもでき、自動的にマッピングされます。 .{data-version:3.2.4} +データのクラスのプロパティは enum にもでき、自動的に対応づけられます。 .{data-version:3.2.4} -Netteにこのクラスのオブジェクトとしてデータを返すように指示するにはどうすればよいですか?思ったより簡単です。ハンドラメソッドの `$data` パラメータの型としてクラスを指定するだけです: +このクラスのオブジェクトとしてデータを返すよう Nette に伝えるにはどうすればよいでしょうか。思うより簡単です。ハンドラのメソッドの `$data` パラメータの型として、そのクラスを指定するだけです。 ```php public function formSucceeded(Form $form, RegistrationFormData $data): void @@ -256,16 +260,18 @@ public function formSucceeded(Form $form, RegistrationFormData $data): void } ``` -型として `array` を指定することもでき、その場合、データは配列として渡されます。 +型として `array` を指定することもでき、その場合データは配列として渡されます。 -同様に、`getValues()` 関数を使用することもできます。これには、クラス名またはハイドレートするオブジェクトをパラメータとして渡します: +同じように `getValues()` メソッドも使えます。パラメータとしてクラス名か、値を入れる対象のオブジェクトを渡します。 ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -フォームがコンテナで構成される多層構造を形成する場合、それぞれに個別のクラスを作成します: +フォームが検証される前に値を読む必要があるなら(ふつうは `onValidate` のハンドラの中です)、代わりに `getUntrustedValues()` メソッドを使ってください。`getValues()` と同じパラメータを受け取りますが、検証を通ったことを保証せずに、送信された値を返します。 + +フォームがコンテナから成る多階層の構造なら、それぞれに別のクラスを作ります。 ```php $form = new Form; @@ -287,93 +293,95 @@ class RegistrationFormData } ``` -マッピングは、プロパティ `$person` の型から、コンテナを `PersonFormData` クラスにマッピングする必要があることを認識します。プロパティにコンテナの配列が含まれている場合は、型 `array` を指定し、マッピングするクラスをコンテナに直接渡します: +対応づけはそのあと、`$person` プロパティの型から、そのコンテナを `PersonFormData` クラスに対応づけるべきだと判断します。プロパティがコンテナの配列を持つ場合は、型を `array` にして、対応づけるクラスをコンテナに直接渡します。 ```php $person->setMappedType(PersonFormData::class); ``` -フォームのデータクラスの設計は、`Nette\Forms\Blueprint::dataClass($form)` メソッドを使用して生成できます。これはブラウザページに出力されます。コードをクリックして選択し、プロジェクトにコピーするだけです。 .{data-version:3.1.15} +フォームのデータのクラスの案は `Nette\Forms\Blueprint::dataClass($form)` メソッドで生成でき、ブラウザのページに出力されます。あとはクリックして選び、そのコードをプロジェクトにコピーするだけです。 .{data-version:3.1.15} -複数のボタン -====== +複数の送信ボタン +======== -フォームに複数のボタンがある場合、通常、どちらが押されたかを区別する必要があります。各ボタンに独自のハンドラ関数を作成できます。[イベント |nette:glossary#イベント] `onClick` のハンドラとして設定します: +フォームにボタンが 2 つ以上あるなら、ふつうはどれが押されたかを見分ける必要があります。ボタンごとに別のハンドラの関数を作れます。それを `onClick` の[イベント |nette:glossary#イベント]のハンドラとして設定します。 ```php $form->addSubmit('save', '保存') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', '削除') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -これらのハンドラは、`onSuccess` イベントの場合と同様に、有効に記入されたフォームの場合にのみ呼び出されます。違いは、最初のパラメータとしてフォームの代わりに送信ボタンを渡すことができる点です。指定した型によって異なります: +.{data-version:3.3.0} +ハンドラは `addSubmit()` メソッドの第 3 引数として、ボタンに直接渡すこともできます。 + +これらのハンドラは、`onSuccess` イベントと同じく、フォームが正しく埋められた場合にだけ呼ばれます(そのボタンで検証が切られていない限り)。違うのは、指定した型宣言に応じて、第 1 パラメータにフォームではなく送信ボタンのオブジェクトが渡せる点です。 ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -フォームが<kbd>Enter</kbd>キーで送信された場合、最初のボタンで送信されたかのように扱われます。 +<kbd>Enter</kbd> キーでフォームが送信された場合は、最初の送信ボタンで送信されたものとして扱われます。 -onAnchorイベント -============ +onAnchor イベント +============= -ファクトリメソッド(例:`createComponentRegistrationForm`)でフォームを組み立てるとき、フォームはまだ送信されたかどうか、またはどのデータで送信されたかを知りません。しかし、送信された値を知る必要がある場合があります。たとえば、フォームのさらなる形状がそれらに依存する場合や、依存セレクトボックスなどに必要な場合などです。 +ファクトリメソッド(`createComponentRegistrationForm` など)でフォームを組み立てるとき、そのフォームはまだ、自分が送信されたかどうかも、どんなデータで送信されたかも知りません。とはいえ、送信された値を知る必要がある場面もあります。フォームの見た目がそれに左右されたり、連動する選択肢に必要だったりする場合です。 -したがって、フォームを組み立てるコードの一部は、いわゆるアンカーされたとき、つまりPresenterに接続され、送信されたデータを知っているときにのみ呼び出すことができます。そのようなコードを `$onAnchor` 配列に渡します: +そこで、フォームを組み立てるコードを、フォームが「錨を下ろした」とき、つまりすでにプレゼンターにつながって送信されたデータを知っているときにだけ呼ばせられます。そうしたコードは `$onAnchor` 配列に置きます。 ```php $country = $form->addSelect('country', '国:', $this->model->getCountries()); $city = $form->addSelect('city', '市:'); $form->onAnchor[] = function () use ($country, $city) { - // この関数は、フォームが送信されたかどうか、およびどのデータで送信されたかを知っているときにのみ呼び出されます - // したがって、getValue() メソッドを使用できます + // この関数は、フォームがどんなデータで送信されたかを知ったときに呼ばれます + // ですから getValue() メソッドを使えます $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; ``` -脆弱性からの保護 -======== +弱点からの保護 +======= -Nette Frameworkはセキュリティを非常に重視しており、したがってフォームの適切な保護に細心の注意を払っています。これは完全に透過的に行われ、手動で何も設定する必要はありません。 +Nette Framework は安全をとても大切にしているので、フォームの安全にも細やかに気を配ります。それはまったく見えないところで行われ、手で設定する必要はありません。 -フォームを[クロスサイトスクリプティング (XSS) |nette:glossary#Cross-Site Scripting XSS]および[クロスサイトリクエストフォージェリ (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF]攻撃から保護することに加えて、多くの小さなセキュリティ対策を実行するため、もはや考える必要はありません。 +[クロスサイトスクリプティング(XSS) |nette:glossary#Cross-Site Scripting (XSS)]や[クロスサイトリクエストフォージェリ(CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)]といった攻撃からフォームを守るほかにも、あなたがもう考えなくてよい小さな安全のための手立てをたくさん行っています。 -たとえば、入力からすべての制御文字を除去し、UTF-8エンコーディングの有効性を検証するため、フォームからのデータは常にクリーンになります。セレクトボックスとラジオリストでは、選択された項目が実際に提供されたものであり、改ざんされていないことを検証します。一行テキスト入力では、攻撃者がそこに送信した可能性のある改行文字を削除することをすでに述べました。複数行入力では、改行文字を正規化します。などなど。 +たとえば入力からすべての制御文字を取り除き、UTF-8 の文字コードとして正しいかを確かめるので、フォームから来るデータはいつもきれいです。選択肢やラジオの一覧では、選ばれた項目が本当に提示されたものの中にあり、偽造がなかったことを確かめます。1 行のテキストの入力では、攻撃者が送るかもしれない改行の文字を空白に置き換えることはすでに触れました。複数行の入力では改行の文字をそろえます。ほかにもいろいろあります。 -Netteは、多くのプログラマーが存在することさえ知らないセキュリティリスクを処理します。 +多くのプログラマーが存在すら知らないセキュリティリスクを、Nette があなたの代わりに片付けています。 -言及されたCSRF攻撃は、攻撃者が被害者をページに誘い込み、被害者のブラウザで被害者がログインしているサーバーへのリクエストを密かに実行し、サーバーがリクエストが被害者自身の意志で実行されたと信じ込ませることにあります。したがって、Netteは異なるドメインからのPOSTフォームの送信を防ぎます。何らかの理由で保護を無効にし、異なるドメインからのフォームの送信を許可したい場合は、次を使用します: +先ほどの CSRF の攻撃では、攻撃者が被害者をあるページへ誘い込み、そのページが被害者のブラウザの中で、被害者がログインしているサーバーへのリクエストを黙って実行します。するとサーバーは、そのリクエストが被害者の意思で行われたと信じてしまいます。ですから Nette は、よそのオリジンから送信された POST のフォームを拒みます。同じサイトの違うサブドメインでも「よそ」と見なされます。別のオリジンからの送信を許す必要があるなら、次のようにして保護を切ります。 ```php -$form->allowCrossOrigin(); // 注意!保護を無効にします! +$form->allowCrossOrigin(); // 注意。保護が完全に切れます。 ``` -この保護は、`_nss` という名前のSameSite Cookieを使用します。SameSite Cookieによる保護は100%信頼できるとは限らないため、トークンによる保護も有効にすることをお勧めします: +ただしこれはどのオリジンに対しても保護を切ります。特定のオリジンだけを許したいなら、保護を切ったうえで `Origin` ヘッダーを自分の許可の一覧と自分で照らし合わせてください。 -```php -$form->addProtection(); -``` +この保護はブラウザの `Sec-Fetch-Site` ヘッダー(Fetch Metadata)に頼っています。これはブラウザが自動的に送るもので、XSS の弱点があっても偽れません。これに対応していない古いブラウザには、Nette のアプリケーションが自動的に設定する SameSite のクッキーが代わりに働きます。記事 [ブラウザがついに CSRF を解決する |https://blog.nette.org/en/quarter-century-of-csrf]で詳しく説明しています。 -アプリケーション内の機密データを変更するWebサイトの管理部分のフォームをこのように保護することをお勧めします。フレームワークは、セッションに保存される認証トークンを生成および検証することによってCSRF攻撃から防御します。したがって、フォームを表示する前にセッションを開いておく必要があります。Webサイトの管理部分では、通常、ユーザーログインのためにセッションはすでに開始されています。 それ以外の場合は、`Nette\Http\Session::start()` メソッドでセッションを開始します。 +.[note] +セッションに保存した認可のトークンを使う以前の保護(`$form->addProtection()` で有効にするもの)はもう要らず、バージョン 3.3 から非推奨です。 -複数のPresenterで同じフォームを使用する -======================== +ひとつのフォームを複数のプレゼンターで使う +===================== -複数のPresenterで1つのフォームを使用する必要がある場合は、そのためのファクトリを作成し、それをPresenterに渡すことをお勧めします。このようなクラスの適切な場所は、たとえば `app/Forms` ディレクトリです。 +同じフォームを複数のプレゼンターで使う必要があるなら、そのファクトリを作ってプレゼンターに注入することをおすすめします。そうしたクラスの置き場所としては、たとえば `app/Forms` ディレクトリが適しています。 -ファクトリクラスは次のようになります: +ファクトリのクラスは次のようになります。 ```php use Nette\Application\UI\Form; @@ -390,7 +398,7 @@ class SignInFormFactory } ``` -Presenterのコンポーネントファクトリメソッドで、クラスにフォームの作成を依頼します: +プレゼンターのコンポーネントのファクトリメソッドの中で、フォームを作ってもらうクラスを求めます。 ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // フォームを変更できます。ここでは、たとえばボタンのキャプションを変更します - $form['send']->setCaption('続行'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // そしてハンドラを追加します + // フォームを変えられます。ここではボタンのラベルを変えています + $form['send']->setCaption('続ける'); + $form->onSuccess[] = $this->signInFormSuceeded(...); // そしてハンドラを足します return $form; } ``` -フォーム処理ハンドラは、ファクトリから提供することもできます: +フォームを処理するハンドラも、ファクトリ自身が用意できます。 ```php use Nette\Application\UI\Form; @@ -421,11 +429,11 @@ class SignInFormFactory $form->addText('name', '名前:'); $form->addSubmit('send', 'ログイン'); $form->onSuccess[] = function (Form $form, $data): void { - // ここでフォーム処理を実行します + // ここで送信されたフォームを処理します }; return $form; } } ``` -これで、Netteのフォームの簡単な紹介が終わりました。[examples|https://github.com/nette/forms/tree/master/examples]ディレクトリを調べて、さらなるインスピレーションを見つけてみてください。 +以上で、Nette のフォームの手早い入門を見てきました。もっと着想が欲しければ、配布物の [examples |https://github.com/nette/forms/tree/master/examples]ディレクトリをのぞいてみてください。 diff --git a/forms/ja/rendering.texy b/forms/ja/rendering.texy index fee220e501..2943ad9fa0 100644 --- a/forms/ja/rendering.texy +++ b/forms/ja/rendering.texy @@ -1,35 +1,35 @@ -フォームのレンダリング -*********** +フォームの描画 +******* -フォームの外観は非常に多様です。実際には、2つの極端なケースに遭遇する可能性があります。一方では、アプリケーションで視覚的に互いに似ている多くのフォームをレンダリングする必要があり、`$form->render()` を使ってテンプレートなしで簡単にレンダリングできる点が重宝されます。これは通常、管理インターフェースの場合です。 +フォームの見た目はとても多様です。実務では 2 つの極端に出くわします。ひとつは、見た目のそろったたくさんのフォームをアプリケーションで描く必要がある場合で、そこでは `$form->render()` によるテンプレートなしの簡単な描画がありがたく思えます。管理画面がまさにその例です。 -一方、それぞれがオリジナルである多様なフォームがあります。それらの外観は、フォームテンプレート内でHTML言語を使って記述するのが最適です。そしてもちろん、両方の極端なケースに加えて、その中間のどこかに位置する多くのフォームに遭遇します。 +もうひとつは、ひとつひとつが違う多様なフォームです。その見た目は、フォームのテンプレートの HTML で書くのがいちばんです。そしてもちろん、この 2 つの極端のあいだのどこかに落ちるフォームにも数多く出会います。 -Latteによるレンダリング -============== +Latte での描画 +========== -[テンプレートシステムLatte|latte:]は、フォームとそのコントロールのレンダリングを大幅に簡素化します。まず、個々のコントロールを手動でレンダリングしてコードを完全に制御する方法を示します。後で、そのようなレンダリングを[自動化する |#自動レンダリング]方法を示します。 +[Latte のテンプレートシステム |latte:]は、フォームとその要素の描画を大いに簡単にします。まず、コードを完全に思いどおりにするために、フォームを要素ごとに手で描く方法をお見せします。そのあとで、そうした描画を[自動化する |#自動的な描画]方法を見ます。 -フォームのLatteテンプレートの設計案は、`Nette\Forms\Blueprint::latte($form)` メソッドを使って生成させることができ、これはブラウザページに出力されます。コードをクリックして選択し、プロジェクトにコピーするだけです。 .{data-version:3.1.15} +フォームの Latte のテンプレートは `Nette\Forms\Blueprint::latte($form)` メソッドで生成でき、ブラウザのページに出力されます。あとはクリックしてコードを選び、プロジェクトにコピーするだけです。 .{data-version:3.1.15} `{control}` ----------- -フォームをレンダリングする最も簡単な方法は、テンプレートに次のように記述することです: +フォームを描くいちばん単純な方法は、テンプレートに次のように書くことです。 ```latte {control signInForm} ``` -このようにレンダリングされたフォームの外観は、[#Renderer]と[個々のコントロール |#HTML属性]の設定によって影響を受ける可能性があります。 +描かれるフォームの見た目は、[#Renderer]の設定と[個々の要素 |#HTML の属性]で変えられます。 `n:name` -------- -PHPコードでのフォーム定義は、HTMLコードと非常に簡単に連携できます。`n:name` 属性を追加するだけです。とても簡単です! +PHP のコードのフォームの定義と HTML のコードを結び付けるのはきわめて簡単です。`n:name` の属性を足すだけです。それだけです。 ```php protected function createComponentSignInForm(): Form @@ -45,10 +45,10 @@ protected function createComponentSignInForm(): Form ```latte <form n:name=signInForm class=form> <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> + <label n:name=username>ユーザー名: <input n:name=username size=20 autofocus></label> </div> <div> - <label n:name=password>Password: <input n:name=password></label> + <label n:name=password>パスワード: <input n:name=password></label> </div> <div> <input n:name=send class="btn btn-default"> @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -結果となるHTMLコードの形は、完全にあなたのコントロール下にあります。`n:name` 属性を `<select>`、`<button>`、または `<textarea>` 要素で使用すると、それらの内部コンテンツが自動的に補完されます。`<form n:name>` タグは、レンダリングされるフォームのオブジェクトを持つローカル変数 `$form` も作成し、閉じタグ `</form>` はレンダリングされていないすべての隠しコントロールをレンダリングします(`{form} ... {/form}` にも同じことが当てはまります)。 +できあがる HTML のコードの見た目を完全に思いどおりにできます。`n:name` 属性を `<select>`、`<button>`、`<textarea>` の要素で使うと、その中身は自動的に埋められます。さらに `<form n:name>` のタグは、描かれるフォームのオブジェクトが入った局所変数 `$form` を作り、閉じる `</form>` のタグは描かれていない隠しの要素を描きます(`{form} ... {/form}` でも同じです)。 -ただし、発生しうるエラーメッセージのレンダリングを忘れてはいけません。`addError()` メソッドによって個々のコントロールに追加されたもの(`{inputError}` を使用)と、フォームに直接追加されたもの(`$form->getOwnErrors()` によって返される)の両方です: +とはいえ、エラーのメッセージを描くのを忘れてはいけません。これには `addError()` メソッドで個々の要素に足されたエラー(`{inputError}` で描かれます)と、フォームそのものに足されたエラー(`$form->getOwnErrors()` が返します)が含まれます。 ```latte <form n:name=signInForm class=form> @@ -67,11 +67,11 @@ protected function createComponentSignInForm(): Form </ul> <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> + <label n:name=username>ユーザー名: <input n:name=username size=20 autofocus></label> <span class=error n:ifcontent>{inputError username}</span> </div> <div> - <label n:name=password>Password: <input n:name=password></label> + <label n:name=password>パスワード: <input n:name=password></label> <span class=error n:ifcontent>{inputError password}</span> </div> <div> @@ -80,7 +80,7 @@ protected function createComponentSignInForm(): Form </form> ``` -RadioListやCheckboxListなどのより複雑なフォームコントロールは、個々の項目ごとにこのようにレンダリングできます: +RadioList や CheckboxList のような、もっと込み入ったフォームの要素は、次のように項目ごとに描けます。 ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ RadioListやCheckboxListなどのより複雑なフォームコントロール `{label}` `{input}` ------------------- -各コントロールについて、テンプレートでどのHTML要素、`<input>`、`<textarea>` など、を使用するか考えたくないですか?解決策は、汎用タグ `{input}` です: +要素ごとにテンプレートでどの HTML 要素を使うか、`<input>` なのか `<textarea>` なのかを考えたくないですか。その解が万能の `{input}` タグです。 ```latte <form n:name=signInForm class=form> @@ -101,11 +101,11 @@ RadioListやCheckboxListなどのより複雑なフォームコントロール </ul> <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} + {label username}ユーザー名: {input username, size: 20, autofocus: true}{/label} {inputError username} </div> <div> - {label password}Password: {input password}{/label} + {label password}パスワード: {input password}{/label} {inputError password} </div> <div> @@ -114,9 +114,9 @@ RadioListやCheckboxListなどのより複雑なフォームコントロール </form> ``` -フォームがトランスレータを使用する場合、`{label}` タグ内のテキストは翻訳されます。 +フォームが翻訳器を使っているなら、フォームの定義から描かれるラベル(たとえば `{label username /}`)は翻訳されます。`{label}` と `{/label}` のタグのあいだに直接書かれた文は翻訳されません。 -この場合でも、RadioListやCheckboxListなどのより複雑なフォームコントロールは、個々の項目ごとにレンダリングできます: +ここでも、RadioList や CheckboxList のような、もっと込み入ったフォームの要素は項目ごとに描けます。 ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ RadioListやCheckboxListなどのより複雑なフォームコントロール {/foreach} ``` -Checkboxコントロールの `<input>` 自体をレンダリングするには、`{input myCheckbox:}` を使用します。この場合、HTML属性は常にカンマで区切ります `{input myCheckbox:, class: required}`。 +Checkbox の要素の `<input>` だけを描くには `{input myCheckbox:}` を使います。この場合、HTML の属性は必ずコンマで区切ってください。`{input myCheckbox:, class: required}` のようにです。 `{inputError}` -------------- -フォームコントロールにエラーメッセージがある場合、それを表示します。メッセージは通常、スタイリングのためにHTML要素でラップされます。 メッセージがない場合に空の要素のレンダリングを防ぐには、`n:ifcontent` をエレガントに使用できます: +フォームの要素のエラーのメッセージがあれば表示します。このメッセージは、体裁を整えるためにふつう HTML の要素で包みます。メッセージがないときに空の要素が描かれるのを防ぐには、`n:ifcontent` を使うと優雅です。 ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -エラーの存在は `hasErrors()` メソッドで確認でき、それに応じて親要素にクラスを設定できます: +エラーがあるかどうかは `hasErrors()` メソッドで確かめられ、それに応じて親の要素のクラスを設定できます。 ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ Checkboxコントロールの `<input>` 自体をレンダリングするには `{form}` -------- -`{form signInForm}...{/form}` タグは `<form n:name="signInForm">...</form>` の代替です。 +`{form signInForm}...{/form}` のタグは `<form n:name="signInForm">...</form>` の代わりになります。引数があれば名前とコンマで区切ってください。`{form signInForm, class: foo}` のようにです。 +.{data-version:3.3.0} +名前の前に置く `scope` の語は、フォームをスタックに積むだけで(つまり `{input}` や `{label}` などがそれに結び付きます)、`<form>` のタグは描きません。フォームの一部を、たとえばスニペットの中で描くのに便利です。すでにフォームが有効なら、名前はそれを基準に解決されるので、`{form scope}` は `{formContainer}` の代わりにもなります。 -自動レンダリング --------- +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +`detached` の語は空の `<form></form>` を描き、すべての要素を HTML の `form` 属性でそれに結び付けます。おかげで、HTML がふつうは禁じているフォームの中のフォームを置けます。切り離されたフォームには HTML の `id` が要りますが、名前を与えれば自動的に生成されます(下の `outerForm` のようにです)。 + +```latte +{form detached outerForm} + ... +{/form} +``` -`{input}` および `{label}` タグのおかげで、任意のフォームの汎用テンプレートを簡単に作成できます。それはすべてのコントロールを反復処理してレンダリングしますが、`</form>` タグでフォームが終了するときに自動的にレンダリングされる隠しコントロールは除きます。レンダリングされるフォームの名前は、変数 `$form` で期待されます。 + +自動的な描画 +------ + +`{input}` と `{label}` のタグのおかげで、どんなフォームにも使える汎用のテンプレートを簡単に作れます。それはすべての要素を順に回って描きます。ただし隠しの要素は除きます。それは `</form>` のタグでフォームを閉じたときに自動的に描かれるからです。このテンプレートは、描くフォームの名前が `$form` 変数に入っていることを前提にします。 ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ Checkboxコントロールの `<input>` 自体をレンダリングするには </form> ``` -使用される自己終了ペアタグ `{label .../}` は、PHPコードのフォーム定義から来るラベルを表示します。 +ここで使っている自分で閉じる対のタグ `{label .../}` は、PHP のコードのフォームの定義から来るラベルを表示します。 -この汎用テンプレートを、たとえば `basic-form.latte` ファイルに保存し、フォームをレンダリングするには、それをインクルードしてフォームの名前(またはインスタンス)を `$form` パラメータに渡すだけです: +この汎用のテンプレートを、たとえば `basic-form.latte` というファイルに保存します。フォームを描くには、それを取り込んで `$form` のパラメータにフォームの名前(かインスタンス)を渡すだけです。 ```latte {include basic-form.latte, form: signInForm} ``` -特定のフォームをレンダリングする際にその外観に手を加え、例えば一つのコントロールを異なる方法でレンダリングしたい場合は、最も簡単な方法は、後で上書きできるブロックをテンプレートに事前に準備することです。 ブロックは[動的な名前 |latte:template-inheritance#動的ブロック名]を持つこともできるため、レンダリングされるコントロールの名前を挿入することもできます。たとえば: +特定のフォームの描画の見た目だけを変えたい、たとえばある要素を違うふうに描きたいなら、いちばん簡単なのはテンプレートにあとから上書きできるブロックを用意することです。ブロックには[動的な名前 |latte:template-inheritance#動的なブロック名]も付けられるので、描く要素の名前を差し込めます。たとえば次のようにです。 ```latte ... @@ -189,7 +207,7 @@ Checkboxコントロールの `<input>` 自体をレンダリングするには ... ``` -例えば `username` コントロールの場合、ブロック `input-username` が作成され、[{embed} |latte:template-inheritance#Unit inheritance]タグを使用して簡単に上書きできます: +たとえば `username` という名前の要素なら `input-username` というブロックができ、[{embed} |latte:template-inheritance#Unit Inheritance]のタグで簡単に上書きできます。 ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ Checkboxコントロールの `<input>` 自体をレンダリングするには {/embed} ``` -または、`basic-form.latte` テンプレートの全内容を、`$form` パラメータを含めてブロックとして[定義する |latte:template-inheritance#Definitions]こともできます: +あるいは `basic-form.latte` テンプレートの中身全体を、`$form` のパラメータも含めてブロックとして[定義 |latte:template-inheritance#Definitions]できます。 ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ Checkboxコントロールの `<input>` 自体をレンダリングするには {/define} ``` -これにより、その呼び出しがわずかに簡単になります: +これで呼び出しが少し簡単になります。 ```latte {embed basic-form, signInForm} @@ -219,31 +237,31 @@ Checkboxコントロールの `<input>` 自体をレンダリングするには {/embed} ``` -ブロックは、レイアウトテンプレートの冒頭の1か所でインポートするだけで十分です: +このブロックは 1 か所、レイアウトのテンプレートの先頭で取り込むだけで済みます。 ```latte {import basic-form.latte} ``` -特殊なケース ------- +特別な場合 +----- -HTMLタグ `<form>` なしでフォームの内部部分のみをレンダリングする必要がある場合、たとえばスニペットを送信する場合、`n:tag-if` 属性を使用してそれらを非表示にします: +たとえばスニペットを送るときなど、`<form>` の HTML のタグなしでフォームの内側だけを描く必要があるなら、`n:tag-if` 属性でそれらを隠します。 ```latte <form n:name=signInForm n:tag-if=false> <div> - <label n:name=username>Username: <input n:name=username></label> + <label n:name=username>ユーザー名: <input n:name=username></label> {inputError username} </div> </form> ``` -フォームコンテナ内のコントロールのレンダリングは、`{formContainer}` タグが役立ちます。 +フォームのコンテナの中の要素を描くには、`{formContainer}` のタグ、あるいは新しい [`{form scope}` |#{form}]が役に立ちます。 ```latte -<p>どのニュースを受け取りたいですか:</p> +<p>どのニュースを受け取りますか:</p> {formContainer emailNews} <ul> @@ -254,42 +272,42 @@ HTMLタグ `<form>` なしでフォームの内部部分のみをレンダリン ``` -Latteなしでのレンダリング -=============== +Latte なしの描画 +=========== -フォームをレンダリングする最も簡単な方法は、次を呼び出すことです: +フォームを描くいちばん簡単な方法は、次を呼ぶことです。 ```php $form->render(); ``` -このようにレンダリングされたフォームの外観は、[#Renderer]と[個々のコントロール |#HTML属性]の設定によって影響を受ける可能性があります。 +描かれるフォームの見た目は、[#Renderer]の設定と[個々の要素 |#HTML の属性]で変えられます。 -手動レンダリング --------- +手での描画 +----- -各フォームコントロールには、フォームフィールドとラベルのHTMLコードを生成するメソッドがあります。それらは文字列または[Nette\Utils\Html|utils:html-elements]オブジェクトとして返すことができます: +フォームのそれぞれの要素には、フォームの項目とそのラベルの HTML のコードを生成するメソッドがあります。それらは文字列としても [Nette\Utils\Html |utils:html-elements]オブジェクトとしても返せます。 -- `getControl(): Html|string` コントロールのHTMLコードを返します -- `getLabel($caption = null): Html|string|null` ラベルが存在する場合、そのHTMLコードを返します +- `getControl(): Html|string` は要素の HTML のコードを返します +- `getLabel($caption = null): Html|string|null` は、あればラベルの HTML のコードを返します -したがって、フォームは個々のコントロールごとにレンダリングできます: +おかげでフォームを要素ごとに描けます。 ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,21 +315,21 @@ $form->render(); <?php $form->render('end') ?> ``` -一部のコントロールでは `getControl()` が単一のHTML要素(例:`<input>`、`<select>` など)を返しますが、他のコントロール(CheckboxList、RadioList)ではHTMLコード全体を返します。 その場合、各項目について個々の入力とラベルを生成するメソッドを使用できます: +要素によっては `getControl()` がひとつの HTML 要素(たとえば `<input>` や `<select>` など)を返しますが、HTML のコードのひとまとまり(CheckboxList、RadioList)を返すものもあります。その場合は、項目ごとに個々の入力とラベルを生成するメソッドを使えます。 -- `getControlPart($key = null): ?Html` 1つの項目のHTMLコードを返します -- `getLabelPart($key = null): ?Html` 1つの項目のラベルのHTMLコードを返します +- `getControlPart($key = null): Html` はひとつの項目の HTML のコードを返します +- `getLabelPart($key = null): Html` はひとつの項目のラベルの HTML のコードを返します .[note] -これらのメソッドは歴史的な理由から `get` プレフィックスを持っていますが、呼び出すたびに新しい `Html` 要素を作成して返すため、`generate` の方が適切です。 +これらのメソッドに `get` の接頭辞が付いているのは歴史的な事情によるもので、呼ばれるたびに新しい `Html` 要素を作って返すので、`generate` のほうがふさわしいでしょう。 Renderer ======== -これはフォームのレンダリングを担当するオブジェクトです。`$form->setRenderer` メソッドで設定できます。`$form->render()` メソッドが呼び出されると、制御が渡されます。 +これはフォームを描くことを受け持つオブジェクトです。`$form->setRenderer()` メソッドで設定できます。`$form->render()` メソッドが呼ばれると、そこへ制御が渡ります。 -カスタムレンダラーを設定しない場合、デフォルトのレンダラー [api:Nette\Forms\Rendering\DefaultFormRenderer] が使用されます。これはフォームコントロールをHTMLテーブルとしてレンダリングします。出力は次のようになります: +独自の描画器を設定しなければ、既定の描画器 [api:Nette\Forms\Rendering\DefaultFormRenderer]が使われます。これはフォームの要素を HTML の表に描きます。出力は次のようになります。 ```latte <table> @@ -332,11 +350,11 @@ Renderer ... ``` -フォームの骨格にテーブルを使用するかどうかは議論の余地があり、多くのWebデザイナーは異なるマークアップを好みます。たとえば、定義リストです。したがって、`DefaultFormRenderer` を再構成して、フォームをリストとしてレンダリングします。設定は [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers] 配列を編集することによって行われます。最初のインデックスは常に領域を表し、2番目はその属性を表します。個々の領域は画像で示されています: +フォームの構造に表を使うべきかは議論の分かれるところで、多くのウェブデザイナーは定義リストのような別のマークアップを好みます。ですから `DefaultFormRenderer` を設定し直して、フォームをリストとして描かせましょう。設定は [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]配列を書き換えて行います。最初の添字はいつも領域を、2 つめはその属性を表します。個々の領域は図のとおりです。 -[* defaultformrenderer.webp *] +[* form-areas-en.webp *] -デフォルトでは、コントロールグループ `controls` はテーブル `<table>` でラップされ、各 `pair` はテーブル行 `<tr>` を表し、`label` と `control` のペアはセル `<th>` と `<td>` です。次に、ラッピング要素を変更します。`controls` 領域を `<dl>` コンテナに挿入し、`pair` 領域をコンテナなしのままにし、`label` を `<dt>` に挿入し、最後に `control` を `<dd>` タグでラップします: +既定では `controls` のまとまりは `<table>` で包まれ、それぞれの `pair` は表の行 `<tr>` を、`label` と `control` の組はセル `<th>` と `<td>` を表します。では包む要素を変えましょう。`controls` の領域を `<dl>` のコンテナに入れ、`pair` の領域はコンテナなしにし、`label` は `<dt>` に入れ、最後に `control` を `<dd>` のタグで包みます。 ```php $renderer = $form->getRenderer(); @@ -348,7 +366,7 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -結果は次のHTMLコードです: +その結果、次の HTML のコードになります。 ```latte <dl> @@ -367,102 +385,102 @@ $form->render(); </dl> ``` -wrappers配列では、他の多くの属性に影響を与えることができます: +wrappers の配列では、ほかにも多くの属性に手を入れられます。 -- 個々のフォームコントロールタイプにCSSクラスを追加する -- 奇数行と偶数行をCSSクラスで区別する -- 必須項目とオプション項目を視覚的に区別する -- エラーメッセージをコントロールのすぐ隣に表示するか、フォームの上に表示するかを決定する +- フォームの要素の種類ごとに CSS のクラスを足す +- 奇数と偶数の行を CSS のクラスで区別する +- 必須の項目と省略できる項目を見た目で区別する +- エラーのメッセージを要素のすぐ隣に表示するか、フォームの上に表示するかを決める -Options -------- +オプション +----- -Rendererの動作は、個々のフォームコントロールに *options* を設定することによっても制御できます。これにより、入力フィールドの隣に表示される説明を設定できます: +Renderer の振る舞いは、個々のフォームの要素に *オプション* を設定しても操れます。こうして入力欄の隣に現れる説明を設定できます。 ```php $form->addText('phone', '番号:') - ->setOption('description', 'この番号は非公開のままになります'); + ->setOption('description', 'この番号は公開されません'); ``` -HTMLコンテンツを含めたい場合は、[Html |utils:html-elements] クラスを使用します: +そこに HTML の内容を置きたいなら、[Html |utils:html-elements]クラスを使います。 ```php use Nette\Utils\Html; -$form->addText('phone', '番号:') +$form->addText('phone', '電話:') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">お客様の番号の保管条件</a>') + ->setHtml('<a href="...">利用条件。</a>') ); ``` .[tip] -Htmlコントロールはラベルの代わりに使用することもできます:`$form->addCheckbox('conditions', $label)`。 +Html の要素はラベルの代わりにも使えます。`$form->addCheckbox('conditions', $label)` のようにです。 -コントロールのグループ化 ------------- +入力をまとめる +------- -Rendererを使用すると、コントロールを視覚的なグループ(fieldset)にグループ化できます: +Renderer では、要素を見た目のまとまり(fieldset)にまとめられます。 ```php -$form->addGroup('個人データ'); +$form->addGroup('個人情報'); ``` -新しいグループを作成すると、それがアクティブになり、新しく追加された各コントロールもそれに追加されます。したがって、フォームはこのように構築できます: +新しいまとまりを作るとそれが有効になり、新しく足された要素はそこにも足されます。ですからフォームは次のように組み立てられます。 ```php $form = new Form; -$form->addGroup('個人データ'); -$form->addText('name', 'あなたの名前:'); -$form->addInteger('age', 'あなたの年齢:'); -$form->addEmail('email', 'メールアドレス:'); +$form->addGroup('個人情報'); +$form->addText('name', 'お名前:'); +$form->addInteger('age', '年齢:'); +$form->addEmail('email', 'メール:'); -$form->addGroup('配送先住所'); -$form->addCheckbox('send', '住所に配送する'); +$form->addGroup('お届け先'); +$form->addCheckbox('send', '住所へ届ける'); $form->addText('street', '通り:'); $form->addText('city', '市:'); $form->addSelect('country', '国:', $countries); ``` -Rendererは最初にグループをレンダリングし、次にどのグループにも属さないコントロールをレンダリングします。 +描画器はまずまとまりを描き、そのあとどのまとまりにも属さない要素を描きます。 -Bootstrapのサポート +Bootstrap への対応 -------------- -[例 |https://github.com/nette/forms/tree/master/examples]では、[Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58]、[Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58]、[Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] 用にレンダラーを設定する方法の例を見つけることができます。 +[examples のディレクトリ |https://github.com/nette/forms/tree/master/examples]には、[Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58]、[Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58]、[Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]向けに Renderer を設定する例があります。 -HTML属性 -====== +HTML の属性 +======== -フォームコントロールの任意のHTML属性を設定するには、`setHtmlAttribute(string $name, $value = true)` メソッドを使用します: +フォームの要素に好きな HTML の属性を設定するには、`setHtmlAttribute(string $name, $value = true)` メソッドを使います。 ```php $form->addInteger('number', '番号:') ->setHtmlAttribute('class', 'big-number'); -$form->addSelect('rank', '並び替え:', ['価格', '名前']) - ->setHtmlAttribute('onchange', 'submit()'); // 変更時に送信 +$form->addSelect('rank', '並び順:', ['価格', '名前']) + ->setHtmlAttribute('onchange', 'submit()'); // 変わったらフォームを送信します -// <form> 自体の属性を設定する場合 +// <form> 要素そのものの属性を設定するには $form->setHtmlAttribute('id', 'myForm'); ``` -コントロールのタイプを指定します: +要素の種類を指定します。 ```php -$form->addText('tel', 'あなたの電話番号:') +$form->addText('tel', '電話番号:') ->setHtmlType('tel') ->setHtmlAttribute('placeholder', '電話番号を入力してください'); ``` .[warning] -タイプやその他の属性の設定は、視覚的な目的のみです。入力の正確性の検証はサーバー側で行う必要があり、適切な[フォームコントロール|controls]を選択し、[検証ルール|validation]を指定することで保証されます。 +種類やそのほかの属性の設定は見た目のためだけのものです。入力が正しいかの確認はサーバー側で行わなければならず、それはふさわしい[フォームの要素 |controls]を選び、[検証の規則 |validation]を指定することで果たします。 -ラジオまたはチェックボックスリストの個々の項目に、それぞれ異なる値を持つHTML属性を設定できます。 キーに基づいて値を選択することを保証する `style:` の後のコロンに注意してください: +ラジオやチェックボックスの一覧の個々の項目には、項目ごとに違う値の HTML の属性を設定できます。`style:` のうしろのコロンに注目してください。これでキーに応じて値が選ばれます。 ```php $colors = ['r' => '赤', 'g' => '緑', 'b' => '青']; @@ -471,7 +489,7 @@ $form->addCheckboxList('colors', '色:', $colors) ->setHtmlAttribute('style:', $styles); ``` -出力: +描かれるのは次のとおりです。 ```latte <label><input type="checkbox" name="colors[]" style="background:red" value="r">赤</label> @@ -479,14 +497,14 @@ $form->addCheckboxList('colors', '色:', $colors) <label><input type="checkbox" name="colors[]" value="b">青</label> ``` -`readonly` などの論理属性を設定するには、疑問符付きの表記を使用できます: +`readonly` のような真偽の属性を設定するには、疑問符を使う書き方が使えます。 ```php $form->addCheckboxList('colors', '色:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // 複数のキーには配列を使用します。例:['r', 'g'] + ->setHtmlAttribute('readonly?', 'r'); // 複数のキーには配列を使います。たとえば ['r', 'g'] ``` -出力: +描かれるのは次のとおりです。 ```latte <label><input type="checkbox" name="colors[]" readonly value="r">赤</label> @@ -494,14 +512,14 @@ $form->addCheckboxList('colors', '色:', $colors) <label><input type="checkbox" name="colors[]" value="b">青</label> ``` -セレクトボックスの場合、`setHtmlAttribute()` メソッドは `<select>` 要素の属性を設定します。個々の`<option>` の属性を設定したい場合は、`setOptionAttribute()` メソッドを使用します。上記で述べたコロンと疑問符付きの表記も機能します: +セレクトボックスでは、`setHtmlAttribute()` メソッドは `<select>` 要素の属性を設定します。個々の `<option>` 要素に属性を設定したいなら、`setOptionAttribute()` メソッドを使います。先ほどのコロンと疑問符の書き方もここで働きます。 ```php $form->addSelect('colors', '色:', $colors) ->setOptionAttribute('style:', $styles); ``` -出力: +描かれるのは次のとおりです。 ```latte <select name="colors"> @@ -515,7 +533,7 @@ $form->addSelect('colors', '色:', $colors) プロトタイプ ------ -HTML属性を設定する代替方法は、HTML要素が生成されるテンプレートを変更することです。テンプレートは `Html` オブジェクトであり、`getControlPrototype()` メソッドによって返されます: +HTML の属性を設定するもうひとつの方法は、HTML 要素が生成されるもとになるひな型を変えることです。このひな型は `Html` オブジェクトで、`getControlPrototype()` メソッドが返します。 ```php $input = $form->addInteger('number', '番号:'); @@ -523,14 +541,14 @@ $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -この方法で、`getLabelPrototype()` によって返されるラベルのテンプレートも変更できます: +`getLabelPrototype()` が返すラベルのひな型も同じやり方で変えられます。 ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -Checkbox、CheckboxList、RadioListコントロールの場合、コントロール全体をラップする要素のテンプレートに影響を与えることができます。これは `getContainerPrototype()` によって返されます。デフォルトの状態では「空の」要素であるため、何もレンダリングされませんが、名前を設定することでレンダリングされます: +Checkbox、CheckboxList、RadioList の要素では、要素全体を包む要素のひな型に手を入れられます。これは `getContainerPrototype()` が返します。既定では「空の」要素なので何も描かれませんが、名前を与えれば描かれるようになります。 ```php $input = $form->addCheckbox('send'); @@ -541,49 +559,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -CheckboxListとRadioListの場合、個々の項目を区切るセパレータのテンプレートにも影響を与えることができます。これは `getSeparatorPrototype()` メソッドによって返されます。デフォルトの状態では `<br>` 要素です。ペア要素に変更すると、区切る代わりに個々の項目をラップします。 さらに、個々の項目のラベルのHTML要素のテンプレートに影響を与えることができます。これは `getItemLabelPrototype()` によって返されます。 +CheckboxList と RadioList では、`getSeparatorPrototype()` メソッドが返す、個々の項目の区切りのひな型にも手を入れられます。既定では `<br>` の要素です。これを対の要素に変えると、項目を区切る代わりに個々の項目を包みます。さらに `getItemLabelPrototype()` が返す、個々の項目のラベルの HTML 要素のひな型にも手を入れられます。 翻訳 -========== +=== -多言語アプリケーションをプログラミングしている場合、おそらくフォームを異なる言語バージョンでレンダリングする必要があります。Nette Frameworkはこの目的のために翻訳用のインターフェース [api:Nette\Localization\Translator] を定義しています。Netteにはデフォルトの実装はありません。[Componette |https://componette.org/search/localization]で見つけることができるいくつかの既製のソリューションから、ニーズに応じて選択できます。それらのドキュメントで、トランスレータの設定方法を学ぶことができます。 +多言語のアプリケーションを作っているなら、フォームを言語ごとに描く必要が出てくるでしょう。Nette Framework はそのために翻訳のインターフェース [api:Nette\Localization\Translator]を定めています。Nette には既定の実装がないので、[Componette |https://componette.org/search/localization]にあるできあいの解のいくつかから、必要に応じて選べます。翻訳器の設定の仕方はそれぞれのドキュメントに書かれています。 -フォームは、トランスレータを介したテキストの出力をサポートしています。`setTranslator()` メソッドを使用して渡します: +フォームは翻訳器を通した文の出力に対応しています。`setTranslator()` メソッドで渡します。 ```php $form->setTranslator($translator); ``` -この時点から、すべてのラベルだけでなく、すべてのエラーメッセージやセレクトボックスの項目も別の言語に翻訳されます。 +この時点から、すべてのラベルだけでなく、すべてのエラーのメッセージ、セレクトボックスの項目、入力のプレースホルダも対象の言語に翻訳されます。 -個々のフォームコントロールについて、異なるトランスレータを設定したり、`null` 値で翻訳を完全に無効にしたりすることが可能です: +フォームの要素ごとに違う翻訳器を設定することも、値を `null` にして翻訳を完全に切ることもできます。 ```php -$form->addSelect('carModel', 'モデル:', $cars) +$form->addSelect('carModel', '車種:', $cars) ->setTranslator(null); ``` -[検証ルール|validation]の場合、特定のパラメータもトランスレータに渡されます。たとえば、ルールの場合は: +[検証の規則 |validation]では、翻訳器に特定のパラメータも渡されます。たとえば次の規則では、 ```php $form->addPassword('password', 'パスワード:') - ->addRule($form::MinLength, 'パスワードは少なくとも %d 文字必要です', 8); + ->addRule($form::MinLength, 'パスワードは %d 文字以上でなければなりません', 8); ``` -トランスレータは次のパラメータで呼び出されます: +翻訳器は次のパラメータで呼ばれます。 ```php -$translator->translate('パスワードは少なくとも %d 文字必要です', 8); +$translator->translate('パスワードは %d 文字以上でなければなりません', 8); ``` -したがって、数に応じて単語 `文字` の正しい複数形を選択できます。 +ですから数に応じて、`characters` という語の正しい複数形を選べます。 -onRenderイベント -============ +onRender イベント +============= -フォームがレンダリングされる直前に、コードを実行させることができます。たとえば、フォームコントロールに正しい表示のためのHTMLクラスを追加できます。コードを `onRender` 配列に追加します: +フォームが描かれる直前に、自分のコードを呼ばせられます。このコードはたとえば、正しく表示させるためにフォームの要素に HTML のクラスを足せます。コードは `onRender` 配列に足します。 ```php $form->onRender[] = function ($form) { diff --git a/forms/ja/standalone.texy b/forms/ja/standalone.texy index 3be4150cfb..45aa5a3d99 100644 --- a/forms/ja/standalone.texy +++ b/forms/ja/standalone.texy @@ -1,16 +1,22 @@ -スタンドアロンで使用されるフォーム -***************** +フォームの単体利用 +********* .[perex] -Nette Forms は、Web フォームの作成と処理を大幅に簡素化します。この章で示すように、フレームワークの残りの部分なしで、アプリケーションで完全に独立して使用できます。 +Nette Forms はウェブのフォームの作成と処理を劇的に簡単にします。この章で見るように、フレームワークのほかの部分なしで、まったく単独でアプリケーションに使えます。 -ただし、Nette Application と Presenter を使用している場合は、[Presenter での使用 |in-presenter] ガイドが用意されています。 +とはいえ Nette Application とプレゼンターを使っているなら、あなた向けの案内があります。[プレゼンターでのフォーム |in-presenter]です。 -最初のフォーム -======= +はじめてのフォーム +========= + +始める前に、[Composer |best-practices:composer]でパッケージを入れてください。 + +```shell +composer require nette/forms +``` -簡単な登録フォームを作成してみましょう。そのコードは次のようになります ("全コード":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): +単純な登録のフォームを書いてみましょう。そのコードは次のようになります("完全なコード":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851 をご覧ください)。 ```php use Nette\Forms\Form; @@ -21,95 +27,95 @@ $form->addPassword('password', 'パスワード:'); $form->addSubmit('send', '登録'); ``` -非常に簡単にレンダリングできます: +そしてごく簡単に描きます。 ```php $form->render(); ``` -そしてブラウザではこのように表示されます: +ブラウザでの結果は次のようになるはずです。 -[* form-cs.webp *] +[* form-en.webp *] -フォームは `Nette\Forms\Form` クラスのオブジェクトです(`Nette\Application\UI\Form` クラスは Presenter で使用されます)。名前、パスワード、送信ボタンというコントロールを追加しました。 +フォームは `Nette\Forms\Form` クラスのオブジェクトです(プレゼンターでは `Nette\Application\UI\Form` クラスを使います)。そこに 'name'、'password' という名前の要素と、送信のボタンを足しました。 -では、フォームを動作させてみましょう。`$form->isSuccess()` を問い合わせることで、フォームが送信され、有効に記入されたかどうかを確認します。もしそうなら、データを出力します。フォーム定義の後ろに以下を追加します: +ではフォームに命を吹き込みましょう。`$form->isSuccess()` に尋ねると、フォームが送信されたか、そして妥当に埋められたかが分かります。そうであればデータを出力します。フォームの定義のあとに次を足します。 ```php if ($form->isSuccess()) { - echo 'フォームは正しく記入され、送信されました'; + echo 'フォームは正しく埋められて送信されました'; $data = $form->getValues(); - // $data->name には名前が含まれます - // $data->password にはパスワードが含まれます + // $data->name に名前が入っています + // $data->password にパスワードが入っています var_dump($data); } ``` -`getValues()` メソッドは、送信されたデータを [ArrayHash |utils:arrays#ArrayHash] オブジェクトの形式で返します。これを変更する方法は[後で |#クラスへのマッピング]示します。オブジェクト `$data` には、ユーザーが入力したデータを含む `name` と `password` キーが含まれています。 +`getValues()` メソッドは送信されたデータを [ArrayHash |utils:arrays#ArrayHash]オブジェクトとして返します。これを変える方法は[のちほど |#クラスへの対応づけ]お見せします。`$data` オブジェクトには、利用者が入力したデータの入った `name` と `password` のキーがあります。 -通常、データはすぐにさらなる処理に送られます。これは、例えばデータベースへの挿入などです。しかし、処理中にエラーが発生する可能性があります。例えば、ユーザー名が既に使用されている場合などです。その場合、`addError()` を使用してエラーをフォームに戻し、エラーメッセージとともに再度レンダリングさせます。 +ふつうはそのデータをそのまま次の処理へ、たとえばデータベースへの挿入へ送ります。しかし処理の途中でエラーが起きることもあります。たとえばそのユーザー名がすでに使われている場合です。そんなときは `addError()` でエラーをフォームに返し、エラーのメッセージとともにもう一度描かせます。 ```php -$form->addError('申し訳ありませんが、このユーザー名は既に使用されています。'); +$form->addError('申し訳ありません、そのユーザー名はすでに使われています。'); ``` -フォームを処理した後、次のページにリダイレクトします。これにより、*更新*ボタン、*戻る*ボタン、またはブラウザの履歴の移動によってフォームが意図せず再送信されるのを防ぎます。 +フォームを処理したあとは、次のページへリダイレクトします。これで *更新* や *戻る* のボタンを押したり、ブラウザの履歴をたどったりすることによる、意図しないフォームの再送信を防げます。 -フォームはデフォルトで POST メソッドを使用して同じページに送信されます。両方とも変更できます: +既定では、フォームは POST メソッドで同じページへ送られます。どちらも変えられます。 ```php $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -そして、これで終わりです :-) 機能的で完全に[安全な |#脆弱性からの保護]フォームができました。 +これで基本はおしまいです :-) 動いて、しかも完璧に[守られた |#弱点からの保護]フォームができました。 -他の[フォームコントロール|controls]も追加してみてください。 +ほかの[フォームの要素 |controls]も足してみてください。 -コントロールへのアクセス -============ +要素へのアクセス +======== -フォームとその個々のコントロールをコンポーネントと呼びます。これらはコンポーネントツリーを形成し、フォームがルートになります。フォームの個々のコントロールには、次の方法でアクセスできます: +フォームとその個々の要素はコンポーネントと呼ばれます。それらはコンポーネントの木を作り、フォームがその根になります。個々のフォームの要素には次のようにアクセスできます。 ```php $input = $form->getComponent('name'); -// 代替構文: $input = $form['name']; +// 別の書き方: $input = $form['name']; $button = $form->getComponent('send'); -// 代替構文: $button = $form['send']; +// 別の書き方: $button = $form['send']; ``` -コントロールは unset を使用して削除されます: +要素は `unset` で取り除きます。 ```php unset($form['name']); ``` -検証ルール +検証の規則 ===== -*有効*という言葉が出てきましたが、フォームにはまだ検証ルールがありません。それを修正しましょう。 +*妥当* という語が出てきましたが、フォームにはまだ検証の規則がひとつもありません。それを直しましょう。 -名前は必須なので、`setRequired()` メソッドでマークします。その引数は、ユーザーが名前を入力しなかった場合に表示されるエラーメッセージのテキストです。引数を指定しない場合は、デフォルトのエラーメッセージが使用されます。 +名前は必須にするので、`setRequired()` メソッドで印を付けます。その引数は、利用者が名前を埋めなかったときに表示されるエラーのメッセージの文です。引数を渡さなければ、既定のエラーのメッセージが使われます。 ```php $form->addText('name', '名前:') - ->setRequired('名前を入力してください'); + ->setRequired('名前を入力してください。'); ``` -フォームに名前を入力せずに送信してみてください。エラーメッセージが表示され、フィールドに入力するまでブラウザまたはサーバーが拒否することがわかります。 +名前を埋めずにフォームを送ってみてください。エラーのメッセージが出るのが分かります。その項目を埋めるまでは、ブラウザかサーバーが受け付けません。 -同時に、フィールドにスペースだけを入力してもシステムをだますことはできません。いいえ。Nette は左側と右側のスペースを自動的に削除します。試してみてください。これは、すべての単一行入力で常に行うべきことですが、忘れられがちです。Nette は自動的に行います。(フォームをだまして、名前として複数行の文字列を送信してみてください。ここでも Nette はだまされず、改行をスペースに変更します。) +同時に、入力に空白だけを打ち込んで系をだますこともできません。無理です。Nette は前後の空白を自動的に取り除きます。試してみてください。1 行の入力ではいつもそうすべきことですが、よく忘れられます。Nette は自動的にやってくれます。(名前として複数行の文字列を送ってフォームをだまそうとしてみてください。ここでも Nette はだまされず、改行は空白に変えられます。) -フォームは常にサーバー側で検証されますが、JavaScript 検証も生成されます。これは瞬時に実行され、ユーザーはフォームをサーバーに送信することなく、すぐにエラーを知ることができます。これはスクリプト `netteForms.js` が担当します。 ページに挿入してください: +フォームはいつもサーバー側で検証されますが、JavaScript の検証も生成されます。これはすぐに走るので、利用者はフォームをサーバーへ送らなくてもエラーにすぐ気づけます。これは `netteForms.js` のスクリプトが受け持ちます。ページに読み込んでください。 ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -フォームのあるページのソースコードを見ると、Nette が必須コントロールを CSS クラス `required` を持つ要素に挿入していることに気づくかもしれません。テンプレートに次のスタイルシートを追加してみてください。「名前」ラベルが赤くなります。このようにして、必須コントロールをユーザーにエレガントに示します: +フォームのあるページのソースコードを見ると、Nette が必須の要素を `required` という CSS クラスの要素に入れているのに気づくかもしれません。次のスタイルシートをテンプレートに足してみてください。「名前」のラベルが赤くなります。これで必須の要素を利用者に優雅に示せます。 ```latte <style> @@ -117,85 +123,102 @@ $form->addText('name', '名前:') </style> ``` -`addRule()` メソッドを使用して、さらに検証ルールを追加します。最初のパラメータはルール、2番目は再びエラーメッセージのテキストで、検証ルールの引数が続く場合があります。これはどういう意味ですか? +さらに検証の規則を `addRule()` メソッドで足します。第 1 パラメータは規則、第 2 パラメータはやはりエラーのメッセージの文で、そのあとに省略できる検証の規則への引数が続くことがあります。それはどういうことでしょうか。 -フォームに新しいオプションのフィールド「年齢」を追加します。これは整数(`addInteger()`)であり、さらに許可された範囲内(`$form::Range`)である必要があります。そして、ここでまさに `addRule()` メソッドの3番目のパラメータを使用します。これにより、バリデーターに必要な範囲をペア `[from, to]` として渡します: +フォームに新しい、省略できる項目「年齢」を足しましょう。これは整数でなければならず(`addInteger()`)、許される範囲に収まっていなければなりません(`$form::Range`)。ここでは `addRule()` メソッドの第 3 パラメータを使って、必要な範囲を `[最小, 最大]` の組として検証器に渡します。 ```php $form->addInteger('age', '年齢:') - ->addRule($form::Range, '年齢は18歳から120歳の間でなければなりません', [18, 120]); + ->addRule($form::Range, '年齢は 18 歳から 120 歳のあいだでなければなりません。', [18, 120]); ``` .[tip] -ユーザーがフィールドに入力しない場合、コントロールはオプションであるため、検証ルールはチェックされません。 +利用者がその項目を埋めなければ、その要素は省略できるので検証の規則は確かめられません。 -ここで、小さなリファクタリングの余地があります。エラーメッセージと3番目のパラメータでは、数値が重複して記載されており、これは理想的ではありません。[多言語フォーム |rendering#翻訳]を作成し、数値を含むメッセージが複数の言語に翻訳された場合、値の変更が困難になります。このため、プレースホルダー `%d` を使用することが可能で、Nette が値を補完します: +ここでちょっとした整理の余地が生まれます。エラーのメッセージと第 3 パラメータで数が重複していて、あまり気持ちのよいものではありません。[多言語のフォーム |rendering#翻訳]を作っていて、数を含むメッセージが複数の言語に訳されていたら、値を変えるのが大変になります。ですから `%d` のプレースホルダを使えて、Nette が値を埋めてくれます。 ```php - ->addRule($form::Range, '年齢は %d 歳から %d 歳の間でなければなりません', [18, 120]); + ->addRule($form::Range, '年齢は %d 歳から %d 歳のあいだでなければなりません。', [18, 120]); ``` -`password` コントロールに戻りましょう。これも必須とし、さらにパスワードの最小長(`$form::MinLength`)を検証します。再びプレースホルダーを使用します: +`password` の要素に戻って、これも必須にし、あわせてパスワードの最小の長さも確かめましょう(`$form::MinLength`)。ここでもメッセージにプレースホルダを使います。 ```php $form->addPassword('password', 'パスワード:') - ->setRequired('パスワードを選択してください') - ->addRule($form::MinLength, 'パスワードは少なくとも %d 文字必要です', 8); + ->setRequired('パスワードを決めてください') + ->addRule($form::MinLength, 'パスワードは %d 文字以上でなければなりません', 8); ``` -フォームにフィールド `passwordVerify` を追加します。ここでユーザーは確認のためにパスワードを再度入力します。検証ルールを使用して、両方のパスワードが同じかどうかを確認します(`$form::Equal`)。そして、パラメータとして、[角括弧 |#コントロールへのアクセス]を使用して最初のパスワードへの参照を与えます: +フォームにもうひとつ `passwordVerify` という項目を足しましょう。利用者は確認のためにもう一度パスワードを入力します。検証の規則を使って、2 つのパスワードが同じかを確かめます(`$form::Equal`)。パラメータとしては、[角かっこ |#要素へのアクセス]で最初のパスワードへの参照を渡します。 ```php -$form->addPassword('passwordVerify', '確認用パスワード:') - ->setRequired('確認のため、パスワードをもう一度入力してください') +$form->addPassword('passwordVerify', 'パスワード(確認):') + ->setRequired('確認のためにもう一度パスワードを入力してください') ->addRule($form::Equal, 'パスワードが一致しません', $form['password']) ->setOmitted(); ``` -`setOmitted()` を使用して、値が実際には重要ではなく、検証目的でのみ存在するコントロールをマークしました。値は `$data` に渡されません。 +`setOmitted()` で、値そのものには関心がなく、検証のためだけに存在する要素だと印を付けました。その値は `$data` には渡されません。 -これで、PHP と JavaScript の両方で検証を備えた完全に機能するフォームが完成しました。Nette の検証機能ははるかに広範で、条件を作成したり、それに応じてページの一部を表示したり非表示にしたりすることができます。すべては[フォーム検証|validation]に関する章で学びます。 +これで、PHP と JavaScript の両方で検証が働く、しっかり動くフォームができました。Nette の検証の力はもっと広く、条件を作ったり、それに応じてページの一部を見せたり隠したりできます。すべては[フォームの検証 |validation]の章で学べます。 -デフォルト値 -====== +既定値 +=== -フォームコントロールには通常、デフォルト値を設定します: +フォームの要素には既定値をよく設定します。 ```php -$form->addEmail('email', 'Eメール') +$form->addEmail('email', 'メール') ->setDefaultValue($lastUsedEmail); ``` -すべてのコントロールに同時にデフォルト値を設定すると便利なことがよくあります。例えば、フォームがレコードの編集に使用される場合などです。データベースからレコードを読み取り、デフォルト値を設定します: +すべての要素に既定値を一度に設定できると便利なことがよくあります。たとえばフォームをレコードの編集に使う場合です。データベースからレコードを読んで、その値を既定値として設定します。 ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -コントロールを定義した後に `setDefaults()` を呼び出します。 +`setDefaults()` は要素を定義したあとに呼んでください。 +すでに送信されたフォームでは `setDefaults()` は何もしません。利用者が入力したものを上書きしないので、フォームのファクトリの中で条件なしに呼んでも安全です。送信後にも値を強いる必要があるなら、代わりに `setValues()` を使ってください。 -フォームのレンダリング -=========== -デフォルトでは、フォームはテーブルとしてレンダリングされます。個々のコントロールは基本的なアクセシビリティルールを満たしています - すべてのラベルは `<label>` として記述され、対応するフォームコントロールに関連付けられています。ラベルをクリックすると、カーソルは自動的にフォームフィールドに表示されます。 +フォームの描画 +======= -各コントロールに任意の HTML 属性を設定できます。例えば、プレースホルダーを追加します: +既定では、フォームは表として描かれます。個々の要素はアクセシビリティの基本の指針に従っていて、すべてのラベルは `<label>` 要素として生成され、それぞれのフォームの要素と結び付けられています。ラベルをクリックすると、自動的にフォームの項目にカーソルが移ります。 + +要素ごとに好きな HTML の属性を設定できます。たとえばプレースホルダを足します。 ```php $form->addInteger('age', '年齢:') ->setHtmlAttribute('placeholder', '年齢を入力してください'); ``` -フォームをレンダリングする方法は本当にたくさんあるので、[レンダリングに関する別の章|rendering]があります。 +フォームを描く方法はたくさんあるので、[描画には独立した章 |rendering]を用意しています。 -クラスへのマッピング -========== +Latte での描画 +---------- -フォームデータの処理に戻りましょう。`getValues()` メソッドは、送信されたデータを `ArrayHash` オブジェクトとして返しました。これは `stdClass` のような汎用クラスであるため、エディタでのプロパティの補完やコードの静的解析など、特定の快適さが欠けています。これは、各フォームに特定のクラスを用意し、そのプロパティが個々のコントロールを表すようにすることで解決できます。例: +[Latte |latte:]テンプレートエンジンが手元にあるなら、フォームの描画を任せて、できあがる HTML を完全に思いどおりにできます。エンジンを作り、フォームの拡張を登録して、フォームを変数としてテンプレートに渡します。 + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); +``` + +テンプレートでは `$form` 変数と `{input}`、`{label}`、`n:name` といったタグを通してフォームを扱います。テンプレートを含む完全な例は [examples |https://github.com/nette/forms/tree/master/examples]ディレクトリ(`latte.php` と `latte/`)にあります。個々のタグは[描画 |rendering]の章で説明しています。 + + +クラスへの対応づけ +========= + +フォームのデータの処理に戻りましょう。`getValues()` メソッドは送信されたデータを `ArrayHash` オブジェクトとして返しました。これは `stdClass` と同じような汎用のクラスなので、エディタのプロパティの補完や静的な解析といった便利さが得られません。これは、フォームごとに専用のクラスを用意し、そのプロパティが個々の要素を表すようにすれば解決できます。たとえば次のようにです。 ```php class RegistrationFormData @@ -206,32 +229,32 @@ class RegistrationFormData } ``` -あるいは、コンストラクタを利用することもできます: +あるいはコンストラクタを使えます。 ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -データクラスのプロパティは Enum にすることもでき、自動的にマッピングされます。 .{data-version:3.2.4} +データのクラスのプロパティは enum にもでき、自動的に対応づけられます。 .{data-version:3.2.4} -Nette にこのクラスのオブジェクトとしてデータを返すように指示するにはどうすればよいでしょうか?思ったよりも簡単です。クラス名またはハイドレートするオブジェクトをパラメータとして指定するだけです: +このクラスのオブジェクトとしてデータを返すよう Nette に伝えるにはどうすればよいでしょうか。思うより簡単です。パラメータとしてクラス名か、値を入れる対象のオブジェクトを渡すだけです。 ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -パラメータとして `'array'` を指定することもでき、その場合はデータを配列として返します。 +パラメータとして `'array'` も指定でき、その場合データは配列として返されます。 -フォームがコンテナからなる多層構造を形成する場合、それぞれに個別のクラスを作成します: +フォームがコンテナから成る多階層の構造なら、それぞれに別のクラスを作ります。 ```php $form = new Form; @@ -253,19 +276,19 @@ class RegistrationFormData } ``` -マッピングは、プロパティ `$person` の型から、コンテナを `PersonFormData` クラスにマッピングする必要があることを認識します。プロパティにコンテナの配列が含まれている場合は、型 `array` を指定し、マッピングするクラスをコンテナに直接渡します: +対応づけはそのあと、`$person` プロパティの型から、そのコンテナを `PersonFormData` クラスに対応づけるべきだと分かります。プロパティがコンテナの配列を持つ場合は、`array` の型にして、対応づけるクラスをコンテナに直接渡します。 ```php $person->setMappedType(PersonFormData::class); ``` -フォームのデータクラスの設計は、`Nette\Forms\Blueprint::dataClass($form)` メソッドを使用して生成させることができます。これはブラウザページに出力されます。コードをクリックして選択し、プロジェクトにコピーするだけです。 .{data-version:3.1.15} +フォームのデータのクラスの案は `Nette\Forms\Blueprint::dataClass($form)` メソッドで生成でき、ブラウザのページに出力されます。あとはクリックして選び、そのコードをプロジェクトにコピーするだけです。 .{data-version:3.1.15} -複数のボタン -====== +複数の送信ボタン +======== -フォームに複数のボタンがある場合、通常、どのボタンが押されたかを区別する必要があります。この情報は、ボタンの `isSubmittedBy()` メソッドによって返されます: +フォームにボタンが 2 つ以上あるなら、ふつうはどれが押されたかを見分ける必要があります。ボタンの `isSubmittedBy()` メソッドがそれを教えてくれます。 ```php $form->addSubmit('save', '保存'); @@ -282,36 +305,33 @@ if ($form->isSuccess()) { } ``` -`$form->isSuccess()` の問い合わせを省略しないでください。データの有効性を検証します。 +`$form->isSuccess()` の確認は省かないでください。これがデータの妥当さを確かめています。 -フォームが<kbd>Enter</kbd>キーで送信されると、最初のボタンで送信されたものとして扱われます。 +<kbd>Enter</kbd> キーでフォームが送信された場合は、最初のボタンで送信されたものとして扱われます。 -脆弱性からの保護 -======== +弱点からの保護 +======= -Nette Framework はセキュリティを非常に重視しており、そのためフォームの適切な保護に細心の注意を払っています。 +Nette Framework は安全をとても大切にしているので、フォームがきちんと守られるよう細やかに気を配ります。 -フォームを[クロスサイトスクリプティング(XSS) |nette:glossary#Cross-Site Scripting XSS]攻撃や[クロスサイトリクエストフォージェリ(CSRF) |nette:glossary#Cross-Site Request Forgery CSRF]攻撃から保護することに加えて、あなたが考える必要のない多くの小さなセキュリティ対策を行っています。 +[クロスサイトスクリプティング(XSS) |nette:glossary#Cross-Site Scripting (XSS)]や[クロスサイトリクエストフォージェリ(CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)]といったよく知られた弱点からフォームを守るほかにも、あなたがもう考えなくてよい小さな安全のための手立てをたくさん行っています。 -例えば、入力からすべての制御文字をフィルタリングし、UTF-8 エンコーディングの有効性を検証するため、フォームからのデータは常にクリーンになります。セレクトボックスやラジオリストでは、選択された項目が実際に提供されたものの中から選ばれたものであり、偽装されていないことを検証します。単一行のテキスト入力では、攻撃者が送信した可能性のある改行文字を削除することについては既に述べました。複数行の入力では、改行文字を正規化します。などなど。 +たとえば入力からすべての制御文字を取り除き、UTF-8 の文字コードとして正しいかを確かめるので、フォームから来るデータはいつもきれいです。選択肢やラジオの一覧では、選ばれた項目が本当に提示されたものの中にあり、偽造がなかったことを確かめます。1 行のテキストの入力では、攻撃者が送るかもしれない改行の文字を空白に置き換えることはすでに触れました。複数行の入力では改行の文字をそろえます。ほかにもいろいろあります。 -Nette は、多くのプログラマーが存在することすら知らないセキュリティリスクをあなたに代わって解決します。 +多くのプログラマーが存在すら知らないセキュリティリスクを、Nette があなたの代わりに片付けています。 -前述の CSRF 攻撃は、攻撃者が被害者を、被害者がログインしているサーバーに対して、被害者のブラウザで密かにリクエストを実行するページに誘導し、サーバーがそのリクエストが被害者自身の意志で行われたと誤解することに基づいています。そのため、Nette は他のドメインからの POST フォームの送信を防ぎます。何らかの理由で保護を無効にし、他のドメインからフォームを送信できるようにしたい場合は、次を使用します: +先ほどの CSRF の攻撃では、攻撃者が被害者をあるページへ誘い込み、そのページが被害者のブラウザの中で、被害者が今ログインしているサーバーへのリクエストを黙って実行します。するとサーバーは、そのリクエストが被害者の意思で行われたと信じてしまいます。ですから Nette は、よそのオリジンから送信された POST のフォームを拒みます。同じサイトの違うサブドメインでも「よそ」と見なされます。別のオリジンからの送信を許す必要があるなら、次のようにして保護を切ります。 ```php -$form->allowCrossOrigin(); // 注意!保護を無効にします! +$form->allowCrossOrigin(); // 注意。保護が完全に切れます。 ``` -この保護は `_nss` という名前の SameSite クッキーを利用します。そのため、クッキーを送信できるように、最初の出力が送信される前にフォームオブジェクトを作成してください。 - -SameSite クッキーによる保護は 100% 信頼できるとは限らないため、トークンによる保護も有効にすることをお勧めします: +ただしこれはどのオリジンに対しても保護を切ります。特定のオリジンだけを許したいなら、保護を切ったうえで `Origin` ヘッダーを自分の許可の一覧と自分で照らし合わせてください。 -```php -$form->addProtection(); -``` +この保護はブラウザの `Sec-Fetch-Site` ヘッダー(Fetch Metadata)に頼っています。これはブラウザが自動的に送るもので、XSS の弱点があっても偽れません。これらのヘッダーを送らない古いブラウザは、この検査を通りません。記事 [ブラウザがついに CSRF を解決する |https://blog.nette.org/en/quarter-century-of-csrf]で詳しく説明しています。 -アプリケーション内の機密データを変更するウェブサイトの管理部分のフォームをこのように保護することをお勧めします。フレームワークは、セッションに保存される認証トークンを生成および検証することによって CSRF 攻撃から防御します。そのため、フォームを表示する前にセッションを開いておく必要があります。ウェブサイトの管理部分では、通常、ユーザーのログインのためにセッションは既に開始されています。 それ以外の場合は、`Nette\Http\Session::start()` メソッドでセッションを開始します。 +.[note] +セッションに保存した認可のトークンを使う以前の保護(`$form->addProtection()` で有効にするもの)はもう要らず、バージョン 3.3 から非推奨です。 -これで、Nette のフォームの簡単な紹介は終わりです。配布物の [examples|https://github.com/nette/forms/tree/master/examples] ディレクトリも見て、さらなるインスピレーションを得てください。 +以上で、Nette のフォームの手早い入門を見てきました。もっと着想が欲しければ、配布物の [examples |https://github.com/nette/forms/tree/master/examples]ディレクトリをのぞいてみてください。 diff --git a/forms/ja/upgrading.texy b/forms/ja/upgrading.texy new file mode 100644 index 0000000000..205cdac2b1 --- /dev/null +++ b/forms/ja/upgrading.texy @@ -0,0 +1,54 @@ +アップグレード +******* + + +バージョン 3.3 へのアップグレード +=================== + +- 自動の CSRF 対策は `isSameSite()` から `isFrom(FetchSite::SameOrigin)` に切り替わり、より厳しくなりました。サブドメインから来るリクエストはもう通りません +- おかげで `addProtection()` はもう要りません。自動の対策が同じ場面を覆うからです。新しいフォームでは書かず、既存のフォームからは遠慮なく取り除いてください + +セッションのトークンがもう要らない理由は、記事 [CSRF の四半世紀 |https://blog.nette.org/en/quarter-century-of-csrf]で説明されています。 + + +バージョン 3.1 へのアップグレード +=================== + +- `getValues()` は検証を通った要素だけを返します。検証に関わらずすべての要素の値が必要なら、新しいメソッド `getUntrustedValues()` を使ってください +- `onSuccess` と `onClick` のハンドラに渡される `$values` も同じく、検証を通った要素だけを持ちます +- 単独で使うフォームは、SameSite フラグの付いたクッキーで CSRF から自動的に守られます。別のオリジンからの送信を許すには `allowCrossOrigin()` を使います +- `Form::URL` の規則は、足りないプロトコルを `http` ではなく `https` で補うようになりました +- `Form::addImage()` は `addImageButton()` に改名されました +- `Checkbox::getSeparatorPrototype()` は `getContainerPrototype()` に改名されました +- フォームはテンプレートに `$_form` 変数を作らなくなりました + +これらの変更について詳しくは、記事 [Nette Forms 3.1 の新機能 |https://blog.nette.org/en/news-in-nette-forms-3-1]をご覧ください。 + + +バージョン 3.0 へのアップグレード +=================== + +- すべてのフォームの要素は既定で省略できるようになったので(この変更は Nette 2.4 で入りました)、`setRequired(false)` は取り除けます +- `netteForms.js` は必ずバージョン 3 に更新してください(`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` と `MultiChoiceControl::$checkAllowedValues` は `checkDefaultValue()` メソッドに置き換わりました + + +バージョン 2.4 へのアップグレード +=================== + +- 要素に `addRule()` で規則が付いている(つまり実質的に必須である)なら、`setRequired()` でも必須と印を付けなければなりません。また `setRequired(false)` はその要素を省略できるようにするので、`addCondition($form::FILLED)` の分岐の代わりになります +- `Form::EMAIL`、`URL`、`INTEGER` の検証器は、HTML の `type` 属性をそれぞれ `email`、`url`、`number` に自動的に変えます +- 否定の検証の規則は非推奨です。`~Form::FILLED` の代わりは `Form::BLANK` で、`~Form::EQUAL` は `Form::NOT_EQUAL` に置き換えられます +- 内部のパラメータ `do` は、衝突を避けるために POST で `_do` として送られるようになりました +- `$_form` のようなアンダースコアで始まる内部の変数は非推奨です +- `netteForms.js` の更新をお忘れなく + + +バージョン 2.3 へのアップグレード +=================== + +- `Nette\Forms\Controls\TextBase::filterFloat` のような内部のフィルタのメソッドは取り除かれました +- `TextBase::validateFloat` のような内部の検証のメソッドは `Nette\Forms\Validator` へ移りました。`Rules::$defaultMessages` も同じです +- ボタンと Hidden の項目は HTML の ID なしで生成されます。ID が欲しいなら `setHtmlId()` で設定してください +- RadioList の項目も ID なしで生成されます。`$radioList->generateId = true` で有効にできます +- `TextBase::addFilter()` で足したフィルタは検証のときに処理され、条件にもフィルタを足せるようになりました。`$input->addCondition(...)->addFilter(...)` のようにです diff --git a/forms/ja/validation.texy b/forms/ja/validation.texy index d28667dfc5..d50294bd1d 100644 --- a/forms/ja/validation.texy +++ b/forms/ja/validation.texy @@ -1,184 +1,192 @@ -フォーム検証 -****** +フォームの検証 +******* -必須コントロール -======== +必須の要素 +===== -必須コントロールは `setRequired()` メソッドでマークします。その引数は、ユーザーがコントロールに入力しなかった場合に表示される[#エラーメッセージ]のテキストです。引数を指定しない場合は、デフォルトのエラーメッセージが使用されます。 +要素は `setRequired()` メソッドで必須と印を付けます。その引数は、利用者がその要素を埋めなかったときに表示される[エラーのメッセージ |#エラーのメッセージ]の文です。引数を渡さなければ、既定のエラーのメッセージが使われます。 ```php $form->addText('name', '名前:') - ->setRequired('名前を入力してください'); + ->setRequired('名前を入力してください。'); ``` -ルール +規則 === -検証ルールは `addRule()` メソッドを使用してコントロールに追加します。最初のパラメータはルール、2番目は[#エラーメッセージ]のテキスト、3番目は検証ルールの引数です。 +要素には `addRule()` メソッドで検証の規則を足します。第 1 パラメータは規則、第 2 パラメータは[エラーのメッセージ |#エラーのメッセージ]、第 3 パラメータは検証の規則への引数です。 ```php $form->addPassword('password', 'パスワード:') - ->addRule($form::MinLength, 'パスワードは少なくとも %d 文字必要です', 8); + ->addRule($form::MinLength, 'パスワードは %d 文字以上でなければなりません', 8); ``` -**検証ルールは、ユーザーがコントロールに入力した場合にのみ検証されます。** +**検証の規則は、利用者がその要素を埋めた場合にだけ確かめられます。** -Nette には、`Nette\Forms\Form` クラスの定数である名前を持つ、多数の事前定義されたルールが付属しています。すべてのコントロールでこれらのルールを使用できます: +Nette にはあらかじめ用意された規則がいくつもあり、その名前は `Nette\Forms\Form` クラスの定数です。これらの規則はすべての要素に使えます。 | 定数 | 説明 | 引数の型 |------- -| `Required` | 必須コントロール、`setRequired()` のエイリアス | - -| `Filled` | 必須コントロール、`setRequired()` のエイリアス | - -| `Blank` | コントロールは入力してはいけません | - -| `Equal` | 値がパラメータと等しい | `mixed` -| `NotEqual` | 値がパラメータと等しくない | `mixed` -| `IsIn` | 値が配列内のいずれかの項目と等しい | `array` -| `IsNotIn` | 値が配列内のどの項目とも等しくない | `array` -| `Valid` | コントロールは正しく入力されていますか? ([#条件]用) | - - - -テキスト入力 ------- - -`addText()`、`addPassword()`、`addTextArea()`、`addEmail()`、`addInteger()`、`addFloat()` コントロールでは、次のルールのいくつかを使用することもできます: - -| `MinLength` | テキストの最小長 | `int` -| `MaxLength` | テキストの最大長 | `int` -| `Length` | 範囲内の長さまたは正確な長さ | ペア `[int, int]` または `int` -| `Email` | 有効なメールアドレス | - -| `URL` | 絶対URL | - -| `Pattern` | 正規表現に一致する | `string` -| `PatternInsensitive` | `Pattern` と同様ですが、大文字と小文字を区別しません | `string` -| `Integer` | 整数値 | - -| `Numeric` | `Integer` のエイリアス | - +| `Required` | 必須の要素。`setRequired()` の別名 | - +| `Filled` | 必須の要素。`setRequired()` の別名 | - +| `Blank` | 要素は埋められていてはいけません | - +| `Equal` | 値はパラメータと等しくなければなりません | `mixed` +| `NotEqual` | 値はパラメータと等しくてはいけません | `mixed` +| `IsIn` | 値は配列の要素のどれかでなければなりません | `array` +| `IsNotIn` | 値は配列のどの要素でもあってはいけません | `array` +| `Valid` | 要素は正しく埋められていますか([addConditionOn() |#条件]の中でのみ) | - + + +テキストの入力 +------- + +`addText()`、`addPassword()`、`addTextArea()`、`addEmail()`、`addInteger()`、`addFloat()` の要素には、次の規則も使えます。 + +| `MinLength` | 文字列の最小の長さ | `int` +| `MaxLength` | 文字列の最大の長さ | `int` +| `Length` | 長さが範囲内、または長さがちょうどその値 | 組 `[int, int]` または `int` +| `Email` | 正しいメールアドレス | - +| `URL` | 絶対 URL | - +| `Pattern` | 正規表現に一致 | `string` +| `PatternInsensitive` | `Pattern` と同じですが大文字小文字を区別しません | `string` +| `Integer` | 整数の値 | - +| `Numeric` | 0 以上の整数(数字だけ) | - | `Float` | 数値 | - -| `Min` | 数値コントロールの最小値 | `int\|float` -| `Max` | 数値コントロールの最大値 | `int\|float` -| `Range` | 範囲内の値 | ペア `[int\|float, int\|float]` +| `Min` | 数値の要素の最小値 | `int\|float` +| `Max` | 数値の要素の最大値 | `int\|float` +| `Range` | 値が範囲内 | 組 `[int\|float, int\|float]` -検証ルール `Integer`、`Numeric`、`Float` は、値をそれぞれ整数または浮動小数点数に直接変換します。さらに、ルール `URL` はスキーマのないアドレス(例:`nette.org`)も受け入れ、スキーマを追加します(`https://nette.org`)。`Pattern` と `PatternIcase` の式は、値全体に対して有効でなければなりません。つまり、`^` と `$` 文字で囲まれているかのように扱われます。 +検証の規則 `Integer` と `Float` は、値をそれぞれ整数と浮動小数点数に自動的に変換します。さらに `URL` の規則は、スキームのないアドレス(たとえば `nette.org`)も受け付け、スキームを補います(`https://nette.org`)。`Pattern` と `PatternInsensitive` の式は値の全体に対して当てはまらなければなりません。つまり `^` と `$` の文字で囲まれているかのように扱われます。 -項目数 ---- +項目の数 +---- -`addMultiUpload()`、`addCheckboxList()`、`addMultiSelect()` コントロールでは、選択された項目またはアップロードされたファイルの数を制限するために、次のルールを使用することもできます: +`addMultiUpload()`、`addCheckboxList()`、`addMultiSelect()` の要素では、次の規則で選ばれた項目やアップロードされたファイルの数を制限できます。 -| `MinLength` | 最小数 | `int` -| `MaxLength` | 最大数 | `int` -| `Length` | 範囲内の数または正確な数 | ペア `[int, int]` または `int` +| `MinLength` | 最小の数 | `int` +| `MaxLength` | 最大の数 | `int` +| `Length` | 数が範囲内、または数がちょうどその値 | 組 `[int, int]` または `int` -ファイルアップロード ----------- +ファイルのアップロード +----------- -`addUpload()`、`addMultiUpload()` コントロールでは、次のルールを使用することもできます: +`addUpload()`、`addMultiUpload()` の要素では、次の規則も使えます。 -| `MaxFileSize` | ファイルの最大サイズ(バイト単位) | `int` -| `MimeType` | MIME タイプ、ワイルドカード許可 (`'video/*'`) | `string\|string[]` -| `Image` | JPEG、PNG、GIF、WebP、AVIF 画像 | - -| `Pattern` | ファイル名が正規表現に一致する | `string` -| `PatternInsensitive` | `Pattern` と同様ですが、大文字と小文字を区別しません | `string` +| `MaxFileSize` | ファイルの大きさの上限(バイト) | `int` +| `MimeType` | MIME タイプ。ワイルドカードを使えます(`'video/*'`) | `string\|string[]` +| `Image` | JPEG、PNG、GIF、WebP、AVIF の画像 | - +| `Pattern` | ファイル名が正規表現に一致 | `string` +| `PatternInsensitive` | `Pattern` と同じですが大文字小文字を区別しません | `string` -`MimeType` と `Image` は PHP 拡張機能 `fileinfo` を必要とします。ファイルまたは画像が必要なタイプであるかどうかは、その署名に基づいて検出され、**ファイル全体の整合性は検証されません。** 画像が破損していないかどうかは、例えば[読み込み |http:request#toImage]を試みることで確認できます。 +`MimeType` と `Image` には PHP の `fileinfo` 拡張が要ります。ファイルや画像が求める種類かどうかはその署名から見分けられ、**ファイル全体の健全さは確かめられません。** 画像が壊れているかどうかは、たとえば[読み込んでみる |http:request#toImage()]ことで判断できます。 -エラーメッセージ -======== +エラーのメッセージ +========= -`Pattern` と `PatternInsensitive` を除くすべての事前定義されたルールにはデフォルトのエラーメッセージがあるため、省略できます。ただし、すべてのメッセージをカスタマイズして記述することで、フォームはよりユーザーフレンドリーになります。 +`Pattern` と `PatternInsensitive` を除くあらかじめ用意された規則には既定のエラーのメッセージがあるので、省略できます。とはいえ、あなたの用途に合わせて独自のメッセージをすべて用意して言葉を練れば、フォームはもっと使いやすくなります。 -デフォルトのメッセージは、[設定|forms:configuration]で、`Nette\Forms\Validator::$messages` 配列内のテキストを編集するか、[トランスレータ |rendering#翻訳]を使用することで変更できます。 +既定のメッセージは[設定 |forms:configuration]で変えられますし、`Nette\Forms\Validator::$messages` 配列の文を書き換えても、[翻訳器 |rendering#翻訳]を使っても変えられます。 -エラーメッセージのテキストでは、次のプレースホルダー文字列を使用できます: +エラーのメッセージの文では、次のプレースホルダの文字列を使えます。 -| `%d` | ルールの引数で順次置き換えられます -| `%n$d` | ルールの n 番目の引数で置き換えられます -| `%label` | コントロールのラベルで置き換えられます(コロンなし) -| `%name` | コントロールの名前で置き換えられます(例:`name`) -| `%value` | ユーザーが入力した値で置き換えられます +| `%d` | 規則の引数で順に置き換えられます +| `%n$d` | 規則の n 番目の引数で置き換えられます +| `%label` | 要素のラベルで置き換えられます(コロンなし) +| `%name` | 要素の名前で置き換えられます(たとえば `name`) +| `%value` | 利用者が入力した値で置き換えられます ```php $form->addText('name', '名前:') ->setRequired('%label を入力してください'); $form->addInteger('id', 'ID:') - ->addRule($form::Range, '最小 %d、最大 %d', [5, 10]); + ->addRule($form::Range, '%d 以上 %d 以下', [5, 10]); $form->addInteger('id', 'ID:') - ->addRule($form::Range, '最大 %2$d、最小 %1$d', [5, 10]); + ->addRule($form::Range, '%2$d 以下 %1$d 以上', [5, 10]); ``` 条件 -======== +=== -ルールに加えて、条件を追加することもできます。これらはルールと同様に記述されますが、`addRule()` の代わりに `addCondition()` メソッドを使用し、もちろんエラーメッセージは指定しません(条件は単に問い合わせるだけです): +規則のほかに条件も足せます。書き方は規則と似ていますが、`addRule()` の代わりに `addCondition()` メソッドを使い、当然ながらエラーのメッセージは渡しません(条件は尋ねるだけだからです)。 ```php $form->addPassword('password', 'パスワード:') - // パスワードが8文字より長くない場合 + // パスワードの長さが 8 を超えないなら ->addCondition($form::MaxLength, 8) - // 数字を含まなければならない - ->addRule($form::Pattern, '数字を含める必要があります', '.*[0-9].*'); + // 数字を含まなければなりません + ->addRule($form::Pattern, '数字を含めてください', '.*[0-9].*'); ``` -条件は、`addConditionOn()` を使用して現在のコントロール以外のコントロールにバインドすることもできます。最初のパラメータとして、コントロールへの参照を指定します。この例では、チェックボックスがチェックされた場合(その値が true になる場合)にのみ、メールが必須になります: +条件は `addConditionOn()` を使って、今の要素とは別の要素に結び付けられます。第 1 パラメータはその要素への参照です。この例では、チェックボックスが入っている(つまりその値が true の)場合にだけメールが必須になります。 ```php -$form->addCheckbox('newsletters', 'ニュースレターを購読する'); +$form->addCheckbox('newsletters', 'ニュースレターを受け取る'); -$form->addEmail('email', 'Eメール:') - // チェックボックスがチェックされている場合 +$form->addEmail('email', 'メール:') + // チェックボックスが入っているなら ->addConditionOn($form['newsletters'], $form::Equal, true) - // メールを要求する + // メールを必須にします ->setRequired('メールアドレスを入力してください'); ``` -`elseCondition()` と `endCondition()` を使用して、条件から複雑な構造を作成できます: +条件は `elseCondition()` と `endCondition()` を使って込み入った構造に組み立てられます。 ```php $form->addText(/* ... */) - ->addCondition(/* ... */) // 最初の条件が満たされた場合 - ->addConditionOn(/* ... */) // そして別のコントロールの2番目の条件 - ->addRule(/* ... */) // このルールを要求する - ->elseCondition() // 2番目の条件が満たされない場合 - ->addRule(/* ... */) // これらのルールを要求する + ->addCondition(/* ... */) // 最初の条件が満たされ + ->addConditionOn(/* ... */) // 別の要素についての 2 つめの条件も満たされるなら + ->addRule(/* ... */) // この規則を求めます + ->elseCondition() // 2 つめの条件が満たされないなら + ->addRule(/* ... */) // これらの規則を求めます ->addRule(/* ... */) - ->endCondition() // 最初の条件に戻る + ->endCondition() // 最初の条件に戻ります ->addRule(/* ... */); ``` -Nette では、`toggle()` メソッドを使用して、JavaScript 側で条件の充足または不充足に非常に簡単に対応できます。詳細は[#ダイナミック JavaScript]を参照してください。 +`addCondition()` の第 1 引数には真偽値も渡せます。フォームを組み立てている時点ですでに判断がついている場合、たとえば特定の状況でだけ規則を当てたい場合に便利です。 + +```php +$form->addText('nickname') + ->addCondition($isRequired) // フォームを組み立てる時点で分かっている値 + ->setRequired(); +``` + +Nette では、条件が満たされたかどうかに JavaScript の側で反応するのが `toggle()` メソッドでとても簡単です。[#動的な JavaScript]をご覧ください。 -他のコントロールへの参照 -============ +別の要素への参照 +======== -ルールまたは条件の引数として、フォームの別のコントロールを渡すことができます。ルールは、後でユーザーがブラウザで入力した値を使用します。このようにして、例えば、`password` コントロールが `password_confirm` コントロールと同じ文字列を含むことを動的に検証できます: +規則や条件の引数として、フォームの別の要素を渡すこともできます。そうすると規則は、あとで利用者がブラウザで入力した値を使います。これはたとえば、`password` の要素が `password_confirm` の要素と同じ文字列を含むかを動的に確かめるのに使えます。 ```php $form->addPassword('password', 'パスワード'); -$form->addPassword('password_confirm', 'パスワードを確認') - ->addRule($form::Equal, '入力されたパスワードが一致しません', $form['password']); +$form->addPassword('password_confirm', 'パスワードの確認') + ->addRule($form::Equal, 'パスワードが一致しません', $form['password']); ``` -カスタムルールと条件 -========== +独自の規則と条件 +======== -Nette の組み込み検証ルールでは不十分で、ユーザーからのデータを独自の方法で検証する必要がある状況に陥ることがあります。Nette ではこれは非常に簡単です! +Nette に組み込まれた検証の規則では足りず、利用者のデータを自分のやり方で検証したい場面に出くわすことがあります。Nette ではこれがとても簡単です。 -`addRule()` または `addCondition()` メソッドに、最初のパラメータとして任意のコールバックを渡すことができます。コールバックは、最初のパラメータとしてコントロール自体を受け取り、検証が正常に行われたかどうかを示すブール値を返します。`addRule()` を使用してルールを追加する場合、追加の引数を指定することもでき、それらは2番目のパラメータとして渡されます。 +`addRule()` や `addCondition()` メソッドの第 1 パラメータには、どんなコールバックでも渡せます。コールバックは第 1 パラメータとしてその要素自身を受け取り、検証が通ったかどうかを表す真偽値を返します。`addRule()` で規則を足すときは、追加の引数を渡せて、それが第 2 パラメータとして渡されます。 -したがって、静的メソッドを持つクラスとして独自のバリデーターセットを作成できます: +独自の検証器の一式は、静的メソッドを持つクラスとして作れます。 ```php class MyValidators { - // 値が引数で割り切れるかどうかをテストします + // 値が引数で割り切れるかを調べます public static function validateDivisibility(BaseControl $input, $arg): bool { return $input->getValue() % $arg === 0; @@ -186,12 +194,12 @@ class MyValidators public static function validateEmailDomain(BaseControl $input, $domain) { - // 他のバリデーター + // ほかの検証器 } } ``` -使用方法は非常に簡単です: +使い方はごく分かりやすいものです。 ```php $form->addInteger('num') @@ -202,7 +210,7 @@ $form->addInteger('num') ); ``` -カスタム検証ルールは JavaScript にも追加できます。条件は、ルールが静的メソッドであることです。JavaScript バリデーター用の名前は、バックスラッシュ `\` を含まないクラス名、アンダースコア `_`、およびメソッド名を連結して作成されます。例えば、`App\MyValidators::validateDivisibility` は `AppMyValidators_validateDivisibility` として記述され、`Nette.validators` オブジェクトに追加されます: +独自の検証の規則は JavaScript にも足せます。条件はその規則が静的メソッドであることです。JavaScript の検証器での名前は、バックスラッシュ `\` を取り除いたクラス名、アンダースコア `_`、メソッド名をつないで作られます。たとえば `App\MyValidators::validateDivisibility` は `AppMyValidators_validateDivisibility` と書かれ、`Nette.validators` オブジェクトに足されます。 ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -214,32 +222,32 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => onValidate イベント =============== -フォームが送信されると、検証が実行され、`addRule()` を使用して追加された個々のルールがチェックされ、その後 [イベント |nette:glossary#イベント] `onValidate` がトリガーされます。そのハンドラは、追加の検証、通常は複数のフォームコントロールの値の正しい組み合わせの検証に使用できます。 +フォームが送信されると検証が行われ、`addRule()` で足した個々の規則が確かめられ、続いて `onValidate` [イベント |nette:glossary#イベント]が発火します。そのハンドラは追加の検証に使えます。ふつうは複数のフォームの要素の値の組み合わせが正しいかを確かめます。 -エラーが検出された場合、`addError()` メソッドを使用してフォームに渡します。これは、特定のコントロールまたはフォーム自体で呼び出すことができます。 +エラーが見つかったら、`addError()` メソッドでフォームに伝えます。これは特定の要素に対しても、フォームそのものに対しても呼べます。 ```php protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { - $form->addError('この組み合わせは不可能です。'); + $form->addError('この組み合わせは指定できません。'); } } ``` -処理中のエラー -======= +エラーの処理 +====== -多くの場合、有効なフォームを処理しているときにのみエラーが判明します。例えば、新しい項目をデータベースに書き込んでいるときにキーの重複に遭遇した場合などです。この場合、`addError()` メソッドを使用してエラーをフォームに再度渡します。これは、特定のコントロールまたはフォーム自体で呼び出すことができます: +妥当なフォームを処理している最中にはじめてエラーが分かる場面も多くあります。たとえばデータベースに新しい項目を書き込もうとして、キーが重複していた場合です。そんなときも `addError()` メソッドでエラーをフォームに返します。これは特定の要素に対しても、フォームそのものに対しても呼べます。 ```php try { @@ -249,82 +257,88 @@ try { } catch (Nette\Security\AuthenticationException $e) { if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('無効なパスワードです。'); + $form->addError('パスワードが正しくありません。'); } } ``` -可能であれば、エラーをフォームコントロールに直接添付することをお勧めします。デフォルトのレンダラーを使用すると、その隣に表示されるためです。 +できるならエラーはフォームの要素に直接足すことをおすすめします。既定の描画器ではその要素の隣に表示されるからです。 ```php -$form['date']->addError('申し訳ありませんが、この日付は既に予約されています。'); +$form['date']->addError('申し訳ありません、その日付はすでに埋まっています。'); ``` -`addError()` を繰り返し呼び出して、フォームまたはコントロールに複数のエラーメッセージを渡すことができます。`getErrors()` を使用して取得します。 +`addError()` は繰り返し呼べるので、フォームや要素に複数のエラーのメッセージを渡せます。取り出すには `getErrors()` を使います。 -注意:`$form->getErrors()` は、個々のコントロールに直接渡されたものも含め、すべてのエラーメッセージの要約を返します。フォームにのみ渡されたエラーメッセージは `$form->getOwnErrors()` で取得できます。 +`$form->getErrors()` は、フォームに直接渡されたものだけでなく、個々の要素に渡されたものも含めたすべてのエラーのメッセージをまとめて返すことに注意してください。フォームにだけ渡されたエラーのメッセージは `$form->getOwnErrors()` で取り出せます。 -入力の変更 -===== +入力された値を変える +========== -`addFilter()` メソッドを使用して、ユーザーが入力した値を変更できます。この例では、郵便番号のスペースを許容し、削除します: +`addFilter()` メソッドで、利用者が入力した値を変えられます。この例では、郵便番号の空白を大目に見て取り除きます。 ```php $form->addText('zip', '郵便番号:') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // 郵便番号からスペースを削除します + return str_replace(' ', '', $value); // 郵便番号から空白を取り除きます }) - ->addRule($form::Pattern, '郵便番号は5桁の数字ではありません', '\d{5}'); + ->addRule($form::Pattern, '郵便番号が 5 桁ではありません', '\d{5}'); ``` -フィルタは検証ルールと条件の間に組み込まれるため、メソッドの順序が重要です。つまり、フィルタとルールは `addFilter()` と `addRule()` メソッドの順序で呼び出されます。 +フィルタは検証の規則や条件の中に組み込まれるので、メソッドの順序が意味を持ちます。つまりフィルタと規則は、`addFilter()` と `addRule()` メソッドを並べた順に呼ばれます。 -JavaScript 検証 -============= +JavaScript での検証 +=============== -条件とルールを定式化するための言語は非常に強力です。すべての構造はサーバー側と JavaScript 側の両方で機能します。 それらは HTML 属性 `data-nette-rules` に JSON として転送されます。実際の検証は、フォームの `submit` イベントをキャッチし、個々のコントロールを反復処理して適切な検証を実行するスクリプトによって行われます。 +条件と規則を書くための言語はとても力強いものです。すべての書き方はサーバー側でも、クライアント側の JavaScript でも働きます。それらは HTML の `data-nette-rules` 属性に JSON として運ばれます。検証そのものは、フォームの `submit` イベントを捕まえて、個々の要素を順に回り、それぞれの検証を行うスクリプトが受け持ちます。 -そのスクリプトは `netteForms.js` であり、複数の可能なソースから利用できます: +そのスクリプトが `netteForms.js` で、いくつかの入手先があります。 -スクリプトは CDN から直接 HTML ページに埋め込むことができます: +CDN から HTML のページに直接読み込めます。 ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -または、プロジェクトの公開フォルダにローカルにコピーします(例:`vendor/nette/forms/src/assets/netteForms.min.js` から): +あるいはプロジェクトの公開フォルダにコピーします(たとえば `vendor/nette/forms/src/assets/netteForms.min.js` から)。 ```latte <script src="/path/to/netteForms.min.js"></script> ``` -または、[npm|https://www.npmjs.com/package/nette-forms] を介してインストールします: +あるいは [npm |https://www.npmjs.com/package/nette-forms]で入れます。 ```shell npm install nette-forms ``` -そして、ロードして実行します: +そして読み込んで動かします。 ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -あるいは、`vendor` フォルダから直接ロードすることもできます: +あるいは `vendor` のフォルダから直接読み込めます。 ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +フォームに `novalidate` 属性を足せば、クライアント側の検証をまるごと切れます。すると `netteForms.js` のスクリプトは送信時にそのフォームの検証を飛ばすので、検証はサーバーでだけ行われます。 -ダイナミック JavaScript -================= +```php +$form->setHtmlAttribute('novalidate'); +``` -ユーザーが商品を郵送で送ることを選択した場合にのみ、住所入力フィールドを表示したいですか?問題ありません。キーは `addCondition()` と `toggle()` のペアです: + +動的な JavaScript +============== + +利用者が商品を郵送で受け取ることを選んだときにだけ、住所の項目を表示したいですか。お安いご用です。鍵になるのは `addCondition()` と `toggle()` のメソッドの組み合わせです。 ```php $form->addCheckbox('send_it') @@ -332,25 +346,25 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -このコードは、条件が満たされたとき、つまりチェックボックスがチェックされたときに、HTML 要素 `#address-container` が表示されることを示しています。そしてその逆も同様です。受信者の住所を持つフォームコントロールをこの ID を持つコンテナに配置すると、チェックボックスをクリックすると非表示または表示されます。これはスクリプト `netteForms.js` が保証します。 +このコードは、条件が満たされたとき(つまりチェックボックスが入ったとき)に HTML 要素 `#address-container` が見えるようになり、その逆も起きると伝えています。ですから受取人の住所のフォームの要素をこの ID のコンテナに入れておけば、チェックボックスをクリックしたときに隠れたり現れたりします。これは `netteForms.js` のスクリプトが受け持ちます。 -`toggle()` メソッドの引数として、任意のセレクタを渡すことができます。歴史的な理由から、他の特殊文字を含まない英数字文字列は要素の ID として解釈されます。つまり、`#` 文字が前に付いているのと同じです。2番目のオプションのパラメータを使用すると、動作を反転させることができます。つまり、`toggle('#address-container', false)` を使用した場合、要素はチェックボックスがチェックされていない場合にのみ表示されます。 +`toggle()` メソッドの引数にはどんなセレクタでも渡せます。歴史的な事情から、文字、数字、アンダースコアで始まり、文字、数字、アンダースコア、ハイフン、ドット、コロンだけを含む文字列は要素の ID として扱われ、`#` の文字が前に付いているのと同じになります。第 2 の省略できるパラメータで振る舞いを逆にできます。たとえば `toggle('#address-container', false)` とすれば、その要素はチェックボックスが入って**いない**ときにだけ表示されます。 -JavaScript のデフォルトの実装は、要素の `hidden` プロパティを変更します。ただし、例えばアニメーションを追加するなど、動作を簡単に変更できます。JavaScript で `Nette.toggle` メソッドを独自のソリューションで上書きするだけです: +既定の JavaScript の実装は要素の `hidden` プロパティを変えます。とはいえ、たとえばアニメーションを足すなど、振る舞いは簡単に変えられます。JavaScript で `Nette.toggle` メソッドを独自の解で上書きするだけです。 ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // 'visible' の値に応じて 'el' を非表示または表示します + // 'visible' の値に応じて 'el' を隠したり見せたりします }); }; ``` -検証の無効化 -====== +検証を切る +===== -検証を無効にすると便利な場合があります。送信ボタンの押下が検証を実行しないようにする場合(*Cancel*または*Preview*ボタンに適しています)、`$submit->setValidationScope([])` メソッドで無効にします。部分的な検証のみを実行する場合は、検証するフィールドまたはフォームコンテナを指定できます。 +検証を切ると便利な場面もあります。送信ボタンを押しても検証を行うべきでないなら(*キャンセル* や *プレビュー* のボタンに向いています)、`$submit->setValidationScope([])` メソッドで切ります。一部だけ検証すべきなら、どの項目やフォームのコンテナを検証するかを指定できます。 ```php $form->addText('name') @@ -364,13 +378,15 @@ $details->addInteger('age2') $form->addSubmit('send1'); // フォーム全体を検証します $form->addSubmit('send2') - ->setValidationScope([]); // まったく検証しません + ->setValidationScope([]); // 何も検証しません $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // name コントロールのみを検証します + ->setValidationScope([$form['name']]); // 'name' の要素だけを検証します $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // age コントロールのみを検証します + ->setValidationScope([$form['details']['age']]); // 'age' の要素だけを検証します $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // details コンテナを検証します + ->setValidationScope([$form['details']]); // 'details' のコンテナを検証します ``` -`setValidationScope` は、フォームの[#onValidate イベント]には影響しません。これは常に呼び出されます。コンテナの `onValidate` イベントは、このコンテナが部分検証用にマークされている場合にのみトリガーされます。 +`setValidationScope` はフォームの [#onValidate イベント]には影響せず、それはいつでも呼ばれます。コンテナの `onValidate` イベントは、そのコンテナが一部の検証の対象に印を付けられている場合にだけ発火します。 + +一部だけの検証は `getValues()` が返す値にも影響します。結果には検証の対象に入る要素の値だけが含まれます。その外にある要素の値は外されます。 diff --git a/forms/meta.json b/forms/meta.json index 504d99a95a..c50e0061b2 100644 --- a/forms/meta.json +++ b/forms/meta.json @@ -1,5 +1,6 @@ { - "version": "4.0", + "version": "4.x", "repo": "nette/forms", - "composer": "nette/forms" + "composer": "nette/forms", + "api": "https://api.nette.org/forms/" } diff --git a/forms/pl/@home.texy b/forms/pl/@home.texy index bf3015cfd6..5def102d42 100644 --- a/forms/pl/@home.texy +++ b/forms/pl/@home.texy @@ -3,18 +3,18 @@ Nette Forms <div class=perex> -Nette Forms zrewolucjonizowały tworzenie formularzy internetowych. Nagle wystarczyło napisać kilka zrozumiałych linii kodu, aby mieć gotowy formularz wraz z renderowaniem, walidacją JavaScriptową i serwerową, a dodatkowo doskonale zabezpieczony. Pokażemy, jak: +Nette Forms zrewolucjonizowały tworzenie formularzy webowych. Nagle wystarczyło napisać kilka przejrzystych linii kodu, żeby uzyskać kompletny formularz wraz z renderowaniem, walidacją po stronie JavaScriptu i serwera, w dodatku doskonale zabezpieczony. Pokażemy Ci, jak: -- tworzyć przyjazne formularze -- walidować przesłane dane -- renderować elementy dokładnie według potrzeb +- tworzyć przyjazne dla użytkownika formularze +- walidować wysłane dane +- renderować elementy dokładnie tak, jak potrzebujesz </div> -Używając Nette Forms, unikniesz wielu rutynowych zadań, takich jak pisanie walidacji (dodatkowo podwójnej, po stronie serwera i klienta), zminimalizujesz prawdopodobieństwo wystąpienia błędów i luk bezpieczeństwa. +Dzięki Nette Forms unikniesz wielu rutynowych czynności, na przykład pisania logiki walidacyjnej (po stronie serwera i klienta), i zminimalizujesz prawdopodobieństwo powstania błędów oraz luk bezpieczeństwa. -Formularze można używać albo jako część Aplikacji Nette (czyli w presenterach), albo całkowicie samodzielnie. Ponieważ w obu przypadkach użycie nieco się różni, przygotowaliśmy dla Ciebie dwa poradniki: +Formularzy możesz używać albo jako części Nette Application (czyli w presenterach), albo całkowicie samodzielnie. Ponieważ użycie w obu przypadkach nieco się różni, przygotowaliśmy dla Ciebie osobne poradniki: <div class="wiki-buttons"> <div> "Formularze w presenterach .[wiki-button]":in-presenter </div> @@ -25,7 +25,7 @@ Formularze można używać albo jako część Aplikacji Nette (czyli w presenter Instalacja ---------- -Bibliotekę pobierzesz i zainstalujesz za pomocą narzędzia [Composer|best-practices:composer]: +Pobierz i zainstaluj pakiet za pomocą [Composera|best-practices:composer]: ```shell composer require nette/forms diff --git a/forms/pl/@left-menu.texy b/forms/pl/@left-menu.texy index cbc806cc5c..0f57fedd87 100644 --- a/forms/pl/@left-menu.texy +++ b/forms/pl/@left-menu.texy @@ -1,14 +1,16 @@ Nette Forms *********** -- [Wprowadzenie |@home] +- [Przegląd |@home] - [Formularze w presenterach|in-presenter] - [Formularze samodzielnie|standalone] - [Elementy formularza |controls] - [Walidacja |validation] - [Renderowanie |rendering] -- [Konfiguracja |configuration] +- [Własne elementy |custom-controls] +- [Konfiguracja|configuration] +- [Aktualizacja|upgrading] Dalsza lektura ************** -- [Przewodniki i dobre praktyki |best-practices:] +- [Dobre praktyki |best-practices:] diff --git a/forms/pl/configuration.texy b/forms/pl/configuration.texy index fcea0e4cf4..25e1abd39c 100644 --- a/forms/pl/configuration.texy +++ b/forms/pl/configuration.texy @@ -2,7 +2,7 @@ Konfiguracja formularzy *********************** .[perex] -W konfiguracji można zmienić domyślne [komunikaty błędów formularzy|validation]. +W konfiguracji możesz zmienić domyślne [komunikaty o błędach formularza|validation]. ```neon forms: @@ -17,6 +17,7 @@ forms: 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.' @@ -30,32 +31,33 @@ forms: Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' ``` -Oto polskie tłumaczenie: +/--comment -```neon -forms: - messages: - Equal: 'Proszę podać %s.' - NotEqual: 'Ta wartość nie powinna być %s.' - Filled: 'To pole jest wymagane.' - Blank: 'To pole powinno być puste.' - MinLength: 'Proszę podać co najmniej %d znaków.' - MaxLength: 'Proszę podać maksymalnie %d znaków.' - Length: 'Proszę podać wartość o długości od %d do %d znaków.' - Email: 'Proszę podać prawidłowy adres e-mail.' - URL: 'Proszę podać prawidłowy adres URL.' - Integer: 'Proszę podać prawidłową liczbę całkowitą.' - Float: 'Proszę podać prawidłową liczbę.' - Min: 'Proszę podać wartość większą lub równą %d.' - Max: 'Proszę podać wartość mniejszą lub równą %d.' - Range: 'Proszę podać wartość między %d a %d.' - MaxFileSize: 'Rozmiar przesłanego pliku może wynosić maksymalnie %d bajtów.' - MaxPostSize: 'Przesłane dane przekraczają limit %d bajtów.' - MimeType: 'Przesłany plik nie jest w oczekiwanym formacie.' - Image: 'Przesłany plik musi być obrazem w formacie JPEG, GIF, PNG, WebP lub AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Proszę wybrać prawidłową opcję.' - Nette\Forms\Controls\UploadControl::Valid: 'Wystąpił błąd podczas przesyłania pliku.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Twoja sesja wygasła. Proszę wrócić na stronę główną i spróbować ponownie.' -``` -Jeśli nie używasz całego frameworka, a więc i plików konfiguracyjnych, możesz zmienić domyślne komunikaty błędów bezpośrednio w tablicy `Nette\Forms\Validator::$messages`. + + + + + + + + + + + + + + + + + + + + + + + + +\-- + +Jeśli nie używasz całego frameworku, a więc i plików konfiguracyjnych, możesz zmienić domyślne komunikaty o błędach bezpośrednio w tablicy `Nette\Forms\Validator::$messages`. diff --git a/forms/pl/controls.texy b/forms/pl/controls.texy index 95b89b9ac8..af41c6a47f 100644 --- a/forms/pl/controls.texy +++ b/forms/pl/controls.texy @@ -5,10 +5,10 @@ Elementy formularza Przegląd standardowych elementów formularza. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -Dodaje jednoliniowe pole tekstowe (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Jeśli użytkownik nie wypełni pola, zwraca pusty string `''`, lub za pomocą `setNullable()` można określić, aby zwracał `null`. +Dodaje jednoliniowe pole tekstowe (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Jeśli użytkownik pola nie wypełni, zwraca pusty ciąg `''`, albo użyj `setNullable()`, żeby zwracał zamiast tego `null`. ```php $form->addText('name', 'Imię:') @@ -16,16 +16,16 @@ $form->addText('name', 'Imię:') ->setNullable(); ``` -Automatycznie waliduje UTF-8, przycina lewo- i prawostronne spacje oraz usuwa znaki nowej linii, które mógłby wysłać atakujący. +Automatycznie waliduje UTF-8, przycina białe znaki z lewej i prawej strony i usuwa złamania linii, które mógłby wysłać atakujący. -Maksymalną długość można ograniczyć za pomocą `setMaxLength()`. Zmianę wartości wprowadzonej przez użytkownika umożliwia [addFilter() |validation#Modyfikacja danych wejściowych]. +Maksymalną długość można ograniczyć metodą `setMaxLength()`. Metoda [addFilter() |validation#Modyfikacja wpisanych wartości] pozwala zmodyfikować wartość wpisaną przez użytkownika. -Za pomocą `setHtmlType()` można zmienić wizualny charakter pola tekstowego na typy takie jak `search`, `tel` lub `url` zobacz [specyfikację|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Pamiętaj, że zmiana typu jest tylko wizualna i nie zastępuje funkcji walidacji. Dla typu `url` wskazane jest dodanie specyficznej reguły walidacji [URL |validation#Wejścia tekstowe]. +Metodą `setHtmlType()` możesz zmienić wizualny charakter pola tekstowego na typy takie jak `search`, `tel` czy `url` zgodnie ze [specyfikacją|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Pamiętaj, że zmiana typu jest czysto wizualna i nie zastępuje funkcji walidacyjnej. Dla typu `url` warto dodać konkretną [regułę walidacyjną URL |validation#Inputy tekstowe]. .[note] -Dla innych typów wejść, takich jak `number`, `range`, `email`, `date`, `datetime-local`, `time` i `color`, użyj specjalizowanych metod jak [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] i [#addColor], które zapewniają walidację po stronie serwera. Typy `month` i `week` na razie nie są w pełni obsługiwane we wszystkich przeglądarkach. +Dla innych typów inputów, jak `number`, `range`, `email`, `date`, `datetime-local`, `time` czy `color`, użyj wyspecjalizowanych metod [#addInteger()], [#addFloat()], [#addEmail()], [#addDate()], [#addTime()], [#addDateTime()] i [#addColor()], które zapewniają walidację po stronie serwera. Typy `month` i `week` nie są jeszcze w pełni obsługiwane przez wszystkie przeglądarki. -Elementowi można ustawić tzw. pustą wartość (empty-value), co jest czymś w rodzaju wartości domyślnej, ale jeśli użytkownik jej nie zmieni, element zwróci pusty string lub `null`. +Elementowi można ustawić "pustą wartość". Działa ona trochę jak wartość domyślna, ale jeśli użytkownik jej nie zmieni, element zwraca pusty ciąg albo `null`. ```php $form->addText('phone', 'Telefon:') @@ -34,94 +34,94 @@ $form->addText('phone', 'Telefon:') ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -Dodaje pole do wprowadzania tekstu wieloliniowego (klasa [TextArea |api:Nette\Forms\Controls\TextArea]). Jeśli użytkownik nie wypełni pola, zwraca pusty string `''`, lub za pomocą `setNullable()` można określić, aby zwracał `null`. +Dodaje wieloliniowe pole tekstowe (klasa [TextArea |api:Nette\Forms\Controls\TextArea]). Jeśli użytkownik pola nie wypełni, zwraca pusty ciąg `''`, albo użyj `setNullable()`, żeby zwracał zamiast tego `null`. ```php $form->addTextArea('note', 'Notatka:') - ->addRule($form::MaxLength, 'Notatka jest zbyt długa', 10000); + ->addRule($form::MaxLength, 'Twoja notatka jest zdecydowanie za długa', 10000); ``` -Automatycznie waliduje UTF-8 i normalizuje separatory linii do `\n`. W przeciwieństwie do jednoliniowego pola wejściowego nie dochodzi do przycinania spacji. +Automatycznie waliduje UTF-8 i normalizuje końce linii do `\n`. W przeciwieństwie do jednoliniowego pola tekstowego nie odbywa się tu przycinanie białych znaków. -Maksymalną długość można ograniczyć za pomocą `setMaxLength()`. Zmianę wartości wprowadzonej przez użytkownika umożliwia [addFilter() |validation#Modyfikacja danych wejściowych]. Można ustawić tzw. pustą wartość za pomocą `setEmptyValue()`. +Maksymalną długość można ograniczyć metodą `setMaxLength()`. Metoda [addFilter() |validation#Modyfikacja wpisanych wartości] pozwala zmodyfikować wartość wpisaną przez użytkownika. Pustą wartość można ustawić metodą `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Dodaje pole do wprowadzania liczby całkowitej (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Zwraca albo integer, albo `null`, jeśli użytkownik nic nie wpisze. +Dodaje pole do wpisywania liczby całkowitej (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Zwraca albo liczbę całkowitą, albo `null`, jeśli użytkownik nic nie wpisze. ```php $form->addInteger('year', 'Rok:') - ->addRule($form::Range, 'Rok musi być w zakresie od %d do %d.', [1900, 2023]); + ->addRule($form::Range, 'Rok musi mieścić się między %d a %d.', [1900, 2023]); ``` -Element renderuje się jako `<input type="number">`. Użyciem metody `setHtmlType()` można zmienić typ na `range` do wyświetlania w postaci suwaka, lub na `text`, jeśli preferujesz standardowe pole tekstowe bez specjalnego zachowania typu `number`. +Element renderuje się jako `<input type="number">`. Metodą `setHtmlType()` możesz zmienić typ na `range` dla wyświetlenia w postaci suwaka albo na `text`, jeśli wolisz standardowe pole tekstowe bez specjalnego zachowania typu `number`. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -Dodaje pole do wprowadzania liczby dziesiętnej (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Zwraca albo float, albo `null`, jeśli użytkownik nic nie wpisze. +Dodaje pole do wpisywania liczby zmiennoprzecinkowej (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Zwraca albo liczbę zmiennoprzecinkową, albo `null`, jeśli użytkownik nic nie wpisze. ```php $form->addFloat('level', 'Poziom:') ->setDefaultValue(0) - ->addRule($form::Range, 'Poziom musi być w zakresie od %d do %d.', [0, 100]); + ->addRule($form::Range, 'Poziom musi mieścić się między %d a %d.', [0, 100]); ``` -Element renderuje się jako `<input type="number">`. Użyciem metody `setHtmlType()` można zmienić typ na `range` do wyświetlania w postaci suwaka, lub na `text`, jeśli preferujesz standardowe pole tekstowe bez specjalnego zachowania typu `number`. +Element renderuje się jako `<input type="number">`. Metodą `setHtmlType()` możesz zmienić typ na `range` dla wyświetlenia w postaci suwaka albo na `text`, jeśli wolisz standardowe pole tekstowe bez specjalnego zachowania typu `number`. -Nette i przeglądarka Chrome akceptują jako separator miejsc dziesiętnych zarówno przecinek, jak i kropkę. Aby ta funkcjonalność była dostępna również w Firefoksie, zaleca się ustawienie atrybutu `lang` albo dla danego elementu, albo dla całej strony, na przykład `<html lang="pl">`. +Nette i przeglądarka Chrome akceptują jako separator dziesiętny zarówno przecinek, jak i kropkę. Żeby ta funkcjonalność działała także w Firefoksie, zaleca się ustawienie atrybutu `lang` albo dla konkretnego elementu, albo dla całej strony, na przykład `<html lang="pl">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -Dodaje pole do wprowadzania adresu e-mail (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Jeśli użytkownik nie wypełni pola, zwraca pusty string `''`, lub za pomocą `setNullable()` można określić, aby zwracał `null`. +Dodaje pole do wpisywania adresu e-mail (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). Jeśli użytkownik pola nie wypełni, zwraca pusty ciąg `''`, albo użyj `setNullable()`, żeby zwracał zamiast tego `null`. ```php $form->addEmail('email', 'E-mail:'); ``` -Sprawdza, czy wartość jest prawidłowym adresem e-mail. Nie sprawdza się, czy domena faktycznie istnieje, sprawdza się tylko składnię. Automatycznie waliduje UTF-8, przycina lewo- i prawostronne spacje. +Waliduje, czy wartość jest poprawnym adresem e-mail. Nie sprawdza, czy domena faktycznie istnieje, weryfikowana jest tylko składnia. Automatycznie waliduje UTF-8 i przycina białe znaki z lewej i prawej strony. -Maksymalną długość można ograniczyć za pomocą `setMaxLength()`. Zmianę wartości wprowadzonej przez użytkownika umożliwia [addFilter() |validation#Modyfikacja danych wejściowych]. Można ustawić tzw. pustą wartość za pomocą `setEmptyValue()`. +Maksymalną długość można ograniczyć metodą `setMaxLength()`. Metoda [addFilter() |validation#Modyfikacja wpisanych wartości] pozwala zmodyfikować wartość wpisaną przez użytkownika. Pustą wartość można ustawić metodą `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== -Dodaje pole do wprowadzania hasła (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). +Dodaje pole do wpisywania hasła (klasa [TextInput |api:Nette\Forms\Controls\TextInput]). ```php $form->addPassword('password', 'Hasło:') ->setRequired() ->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8) - ->addRule($form::Pattern, 'Musi zawierać cyfrę', '.*[0-9].*'); + ->addRule($form::Pattern, 'Hasło musi zawierać cyfrę', '.*[0-9].*'); ``` -Przy ponownym wyświetleniu formularza pole będzie puste. Automatycznie waliduje UTF-8, przycina lewo- i prawostronne spacje oraz usuwa znaki nowej linii, które mógłby wysłać atakujący. +Przy ponownym wyświetleniu formularza pole będzie puste. Automatycznie waliduje UTF-8, przycina białe znaki z lewej i prawej strony i usuwa złamania linii, które mógłby wysłać atakujący. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -Dodaje pole wyboru (klasa [Checkbox |api:Nette\Forms\Controls\Checkbox]). Zwraca wartość albo `true`, albo `false`, w zależności od tego, czy jest zaznaczone. +Dodaje checkbox (klasa [Checkbox |api:Nette\Forms\Controls\Checkbox]). Zwraca `true` albo `false`, zależnie od tego, czy jest zaznaczony. ```php -$form->addCheckbox('agree', 'Zgadzam się z warunkami') - ->setRequired('Konieczna jest zgoda na warunki'); +$form->addCheckbox('agree', 'Zgadzam się z regulaminem') + ->setRequired('Musisz zaakceptować nasz regulamin'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Dodaje pola wyboru do wyboru wielu pozycji (klasa [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Zwraca tablicę kluczy wybranych pozycji. Metoda `getSelectedItems()` zwraca wartości zamiast kluczy. +Dodaje listę checkboxów do wyboru wielu pozycji (klasa [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Zwraca tablicę kluczy wybranych pozycji. Metoda `getSelectedItems()` zwraca wybrane pozycje jako pary klucz-wartość. ```php $form->addCheckboxList('colors', 'Kolory:', [ @@ -131,155 +131,156 @@ $form->addCheckboxList('colors', 'Kolory:', [ ]); ``` -Tablicę oferowanych pozycji przekazujemy jako trzeci parametr lub metodą `setItems()`. +Tablicę oferowanych pozycji przekaż jako trzeci parametr albo metodą `setItems()`. Przekazując `false` jako drugi argument `setItems()`, wartości zostaną użyte także jako klucze. -Za pomocą `setDisabled(['r', 'g'])` można dezaktywować poszczególne pozycje. +Metodą `setDisabled(['r', 'g'])` wyłączysz poszczególne pozycje. -Element automatycznie kontroluje, czy nie doszło do fałszerstwa i czy wybrane pozycje są rzeczywiście jednymi z oferowanych i nie zostały dezaktywowane. Metodą `getRawValue()` można uzyskać wysłane pozycje bez tej ważnej kontroli. +Element automatycznie sprawdza, czy nie doszło do podrobienia i czy wybrane pozycje rzeczywiście są wśród oferowanych i nie były wyłączone. Metodą `getRawValue()` można pobrać wysłane pozycje bez tej ważnej kontroli. -Przy ustawianiu domyślnych wybranych pozycji również kontroluje, czy są to jedne z oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć za pomocą `checkDefaultValue(false)`. +Przy ustawianiu domyślnie wybranych pozycji również sprawdza, czy są wśród oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć metodą `checkDefaultValue(false)`. -Jeśli wysyłasz formularz metodą `GET`, możesz wybrać bardziej kompaktowy sposób przesyłania danych, który oszczędza rozmiar query stringu. Aktywuje się go ustawieniem atrybutu HTML formularza: +Jeśli wysyłasz formularz metodą `GET`, możesz wybrać bardziej zwięzły sposób przesyłania danych, który oszczędza rozmiar query stringu. Aktywujesz go, ustawiając formularzowi atrybut HTML: ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ -Dodaje przyciski opcji (klasa [RadioList |api:Nette\Forms\Controls\RadioList]). Zwraca klucz wybranej pozycji, lub `null`, jeśli użytkownik nic nie wybrał. Metoda `getSelectedItem()` zwraca wartość zamiast klucza. +Dodaje radio buttony (klasa [RadioList |api:Nette\Forms\Controls\RadioList]). Zwraca klucz wybranej pozycji albo `null`, jeśli użytkownik nic nie wybrał. Metoda `getSelectedItem()` zwraca zamiast klucza wartość. ```php $sex = [ 'm' => 'mężczyzna', 'f' => 'kobieta', + 'o' => 'inna', ]; $form->addRadioList('gender', 'Płeć:', $sex); ``` -Tablicę oferowanych pozycji przekazujemy jako trzeci parametr lub metodą `setItems()`. +Tablicę oferowanych pozycji przekaż jako trzeci parametr albo metodą `setItems()`. -Za pomocą `setDisabled(['m', 'f'])` można dezaktywować poszczególne pozycje. +Metodą `setDisabled(['m'])` wyłączysz poszczególne pozycje. -Element automatycznie kontroluje, czy nie doszło do fałszerstwa i czy wybrana pozycja jest rzeczywiście jedną z oferowanych i nie została dezaktywowana. Metodą `getRawValue()` można uzyskać wysłaną pozycję bez tej ważnej kontroli. +Element automatycznie sprawdza, czy nie doszło do podrobienia i czy wybrana pozycja rzeczywiście jest jedną z oferowanych i nie była wyłączona. Metodą `getRawValue()` można pobrać wysłaną pozycję bez tej ważnej kontroli. -Przy ustawianiu domyślnej wybranej pozycji również kontroluje, czy jest to jedna z oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć za pomocą `checkDefaultValue(false)`. +Przy ustawianiu domyślnie wybranej pozycji również sprawdza, czy jest jedną z oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć metodą `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -Dodaje pole wyboru (klasa [SelectBox |api:Nette\Forms\Controls\SelectBox]). Zwraca klucz wybranej pozycji, lub `null`, jeśli użytkownik nic nie wybrał. Metoda `getSelectedItem()` zwraca wartość zamiast klucza. +Dodaje selectbox (klasa [SelectBox |api:Nette\Forms\Controls\SelectBox]). Zwraca klucz wybranej pozycji albo `null`, jeśli użytkownik nic nie wybrał. Metoda `getSelectedItem()` zwraca zamiast klucza wartość. ```php $countries = [ - 'CZ' => 'Republika Czeska', - 'PL' => 'Polska', + 'CZ' => 'Czechy', + 'SK' => 'Słowacja', 'GB' => 'Wielka Brytania', ]; $form->addSelect('country', 'Kraj:', $countries) - ->setDefaultValue('PL'); + ->setDefaultValue('SK'); ``` -Tablicę oferowanych pozycji przekazujemy jako trzeci parametr lub metodą `setItems()`. Pozycje mogą być również tablicą dwuwymiarową: +Tablicę oferowanych pozycji przekaż jako trzeci parametr albo metodą `setItems()`. Pozycje mogą być też tablicą dwuwymiarową (reprezentującą optgroupy): ```php $countries = [ - 'Europa' => [ - 'CZ' => 'Republika Czeska', - 'PL' => 'Polska', + 'Europe' => [ + 'CZ' => 'Czechy', + 'SK' => 'Słowacja', 'GB' => 'Wielka Brytania', ], 'CA' => 'Kanada', 'US' => 'USA', - '?' => 'inna', + '?' => 'inny', ]; ``` -W polach wyboru często pierwsza pozycja ma specjalne znaczenie, służy jako wezwanie do działania (prompt). Do dodania takiej pozycji służy metoda `setPrompt()`. +W selectboxach pierwsza pozycja często ma specjalne znaczenie, służy jako zachęta do działania. Do dodania takiej pozycji służy metoda `setPrompt()`. ```php $form->addSelect('country', 'Kraj:', $countries) ->setPrompt('Wybierz kraj'); ``` -Za pomocą `setDisabled(['CZ', 'SK'])` można dezaktywować poszczególne pozycje. +Metodą `setDisabled(['CZ', 'SK'])` wyłączysz poszczególne pozycje. -Element automatycznie kontroluje, czy nie doszło do fałszerstwa i czy wybrana pozycja jest rzeczywiście jedną z oferowanych i nie została dezaktywowana. Metodą `getRawValue()` można uzyskać wysłaną pozycję bez tej ważnej kontroli. +Element automatycznie sprawdza, czy nie doszło do podrobienia i czy wybrana pozycja rzeczywiście jest jedną z oferowanych i nie była wyłączona. Metodą `getRawValue()` można pobrać wysłaną pozycję bez tej ważnej kontroli. -Przy ustawianiu domyślnej wybranej pozycji również kontroluje, czy jest to jedna z oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć za pomocą `checkDefaultValue(false)`. +Przy ustawianiu domyślnie wybranej pozycji również sprawdza, czy jest jedną z oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć metodą `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Dodaje pole wyboru do wyboru wielu pozycji (klasa [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Zwraca tablicę kluczy wybranych pozycji. Metoda `getSelectedItems()` zwraca wartości zamiast kluczy. +Dodaje selectbox do wyboru wielu pozycji (klasa [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Zwraca tablicę kluczy wybranych pozycji. Metoda `getSelectedItems()` zwraca wybrane pozycje jako pary klucz-wartość. ```php $form->addMultiSelect('countries', 'Kraje:', $countries); ``` -Tablicę oferowanych pozycji przekazujemy jako trzeci parametr lub metodą `setItems()`. Pozycje mogą być również tablicą dwuwymiarową. +Tablicę oferowanych pozycji przekaż jako trzeci parametr albo metodą `setItems()`. Pozycje mogą być też tablicą dwuwymiarową. -Za pomocą `setDisabled(['CZ', 'SK'])` można dezaktywować poszczególne pozycje. +Metodą `setDisabled(['CZ', 'SK'])` wyłączysz poszczególne pozycje. -Element automatycznie kontroluje, czy nie doszło do fałszerstwa i czy wybrane pozycje są rzeczywiście jednymi z oferowanych i nie zostały dezaktywowane. Metodą `getRawValue()` można uzyskać wysłane pozycje bez tej ważnej kontroli. +Element automatycznie sprawdza, czy nie doszło do podrobienia i czy wybrane pozycje rzeczywiście są wśród oferowanych i nie były wyłączone. Metodą `getRawValue()` można pobrać wysłane pozycje bez tej ważnej kontroli. -Przy ustawianiu domyślnych wybranych pozycji również kontroluje, czy są to jedne z oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć za pomocą `checkDefaultValue(false)`. +Przy ustawianiu domyślnie wybranych pozycji również sprawdza, czy są wśród oferowanych, w przeciwnym razie rzuca wyjątek. Tę kontrolę można wyłączyć metodą `checkDefaultValue(false)`. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Dodaje pole do przesyłania pliku (klasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Zwraca obiekt [FileUpload |http:request#FileUpload] i to nawet w przypadku, gdy użytkownik nie wysłał żadnego pliku, co można sprawdzić metodą `FileUpload::hasFile()`. +Dodaje pole do wysyłania pliku (klasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Zwraca obiekt [FileUpload |http:request#FileUpload], nawet jeśli użytkownik żadnego pliku nie wysłał, co można sprawdzić metodą `FileUpload::hasFile()`. Metodą `setNullable()` możesz sprawić, żeby element zwracał zamiast obiektu `FileUpload` wartość `null`, gdy żaden plik nie został wysłany. ```php $form->addUpload('avatar', 'Awatar:') - ->addRule($form::Image, 'Awatar musi być JPEG, PNG, GIF, WebP lub AVIF.') + ->addRule($form::Image, 'Awatar musi być w formacie JPEG, PNG, GIF, WebP albo AVIF.') ->addRule($form::MaxFileSize, 'Maksymalny rozmiar to 1 MB.', 1024 * 1024); ``` -Jeśli plik nie zostanie poprawnie przesłany, formularz nie jest pomyślnie wysłany i wyświetli się błąd. Tj. przy pomyślnym wysłaniu nie ma potrzeby weryfikować metody `FileUpload::isOk()`. +Jeśli plik nie wyśle się poprawnie, formularz nie zostanie pomyślnie wysłany i wyświetli się błąd. Czyli po udanym wysłaniu nie trzeba sprawdzać metody `FileUpload::isOk()`. -Nigdy nie ufaj oryginalnej nazwie pliku zwróconej przez metodę `FileUpload::getName()`, klient mógł wysłać szkodliwą nazwę pliku z zamiarem uszkodzenia lub zhakowania Twojej aplikacji. +Nigdy nie ufaj oryginalnej nazwie pliku zwracanej metodą `FileUpload::getName()`; klient mógł wysłać złośliwą nazwę pliku z zamiarem uszkodzenia albo zhakowania Twojej aplikacji. -Reguły `MimeType` i `Image` wykrywają wymagany typ na podstawie sygnatury pliku i nie weryfikują jego integralności. Czy obrazek nie jest uszkodzony można sprawdzić na przykład próbą jego [wczytania |http:request#toImage]. +Reguły `MimeType` i `Image` wykrywają wymagany typ na podstawie sygnatury pliku i nie weryfikują jego integralności. To, czy obrazek nie jest uszkodzony, można ustalić na przykład, próbując go [wczytać |http:request#toImage()]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Dodaje pole do przesyłania wielu plików naraz (klasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Zwraca tablicę obiektów [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` u każdego z nich będzie zwracać `true`. +Dodaje pole do wysyłania wielu plików naraz (klasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Zwraca tablicę obiektów [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` dla każdego z nich zwróci `true`. ```php $form->addMultiUpload('files', 'Pliki:') - ->addRule($form::MaxLength, 'Maksymalnie można przesłać %d plików', 10); + ->addRule($form::MaxLength, 'Można wysłać maksymalnie %d plików.', 10); ``` -Jeśli któryś plik nie zostanie poprawnie przesłany, formularz nie jest pomyślnie wysłany i wyświetli się błąd. Tj. przy pomyślnym wysłaniu nie ma potrzeby weryfikować metody `FileUpload::isOk()`. +Jeśli któryś plik nie wyśle się poprawnie, formularz nie zostanie pomyślnie wysłany i wyświetli się błąd. Czyli po udanym wysłaniu nie trzeba sprawdzać metody `FileUpload::isOk()` dla każdego pliku. -Nigdy nie ufaj oryginalnym nazwom plików zwróconym przez metodę `FileUpload::getName()`, klient mógł wysłać szkodliwą nazwę pliku z zamiarem uszkodzenia lub zhakowania Twojej aplikacji. +Nigdy nie ufaj oryginalnym nazwom plików zwracanym metodą `FileUpload::getName()`; klient mógł wysłać złośliwe nazwy plików z zamiarem uszkodzenia albo zhakowania Twojej aplikacji. -Reguły `MimeType` i `Image` wykrywają wymagany typ na podstawie sygnatury pliku i nie weryfikują jego integralności. Czy obrazek nie jest uszkodzony można sprawdzić na przykład próbą jego [wczytania |http:request#toImage]. +Reguły `MimeType` i `Image` wykrywają wymagany typ na podstawie sygnatury pliku i nie weryfikują jego integralności. To, czy obrazek nie jest uszkodzony, można ustalić na przykład, próbując go [wczytać |http:request#toImage()]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== -Dodaje pole, które umożliwia użytkownikowi łatwe wprowadzenie daty składającej się z roku, miesiąca i dnia (klasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Dodaje pole pozwalające użytkownikowi wygodnie wpisać datę złożoną z roku, miesiąca i dnia (klasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Jako wartość domyślną akceptuje albo obiekty implementujące interfejs `DateTimeInterface`, string z czasem, albo liczbę reprezentującą timestamp UNIX. To samo dotyczy argumentów reguł `Min`, `Max` lub `Range`, które definiują minimalną i maksymalną dozwoloną datę. +Jako wartość domyślną przyjmuje obiekty implementujące `DateTimeInterface`, ciąg zawierający czas albo liczbę reprezentującą uniksowy timestamp. To samo dotyczy argumentów reguł `Min`, `Max` czy `Range`, które definiują minimalną i maksymalną dozwoloną datę. ```php $form->addDate('date', 'Data:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Data musi być co najmniej miesiąc stara.', new DateTime('-1 month')); + ->addRule($form::Min, 'Data musi mieć co najmniej miesiąc.', new DateTime('-1 month')); ``` -Standardowo zwraca obiekt `DateTimeImmutable`, metodą `setFormat()` możesz specyfikować [format tekstowy|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] lub timestamp: +Domyślnie zwraca obiekt `DateTimeImmutable`. Metodą `setFormat()` możesz podać [format tekstowy|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] albo timestamp: ```php $form->addDate('date', 'Data:') @@ -287,19 +288,19 @@ $form->addDate('date', 'Data:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -Dodaje pole, które umożliwia użytkownikowi łatwe wprowadzenie czasu składającego się z godzin, minut i opcjonalnie sekund (klasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Dodaje pole pozwalające użytkownikowi wygodnie wpisać czas złożony z godzin, minut i opcjonalnie sekund (klasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Jako wartość domyślną akceptuje albo obiekty implementujące interfejs `DateTimeInterface`, string z czasem, albo liczbę reprezentującą timestamp UNIX. Z tych wejść wykorzystywana jest tylko informacja o czasie, data jest ignorowana. To samo dotyczy argumentów reguł `Min`, `Max` lub `Range`, które definiują minimalny i maksymalny dozwolony czas. Jeśli ustawiona minimalna wartość jest wyższa niż maksymalna, tworzy się zakres czasowy przekraczający północ. +Jako wartość domyślną przyjmuje obiekty implementujące `DateTimeInterface`, ciąg zawierający czas albo liczbę reprezentującą uniksowy timestamp. Z tych wejść wykorzystywana jest tylko informacja o czasie, data jest ignorowana. To samo dotyczy argumentów reguł `Min`, `Max` czy `Range`, które definiują minimalny i maksymalny dozwolony czas. Jeśli ustawiona wartość minimalna jest wyższa niż maksymalna, powstaje przedział czasowy przechodzący przez północ. ```php $form->addTime('time', 'Czas:', withSeconds: true) - ->addRule($form::Range, 'Czas musi być w zakresie od %d do %d.', ['12:30', '13:30']); + ->addRule($form::Range, 'Czas musi mieścić się między %d a %d.', ['12:30', '13:30']); ``` -Standardowo zwraca obiekt `DateTimeImmutable` (z datą 1. stycznia roku 1), metodą `setFormat()` możesz specyfikować [format tekstowy|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: +Domyślnie zwraca obiekt `DateTimeImmutable` (z datą ustawioną na 1 stycznia roku 1). Metodą `setFormat()` możesz podać [format tekstowy|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: ```php $form->addTime('time', 'Czas:') @@ -307,20 +308,20 @@ $form->addTime('time', 'Czas:') ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -Dodaje pole, które umożliwia użytkownikowi łatwe wprowadzenie daty i czasu składających się z roku, miesiąca, dnia, godzin, minut i opcjonalnie sekund (klasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Dodaje pole pozwalające użytkownikowi wygodnie wpisać datę i czas złożone z roku, miesiąca, dnia, godzin, minut i opcjonalnie sekund (klasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -Jako wartość domyślną akceptuje albo obiekty implementujące interfejs `DateTimeInterface`, string z czasem, albo liczbę reprezentującą timestamp UNIX. To samo dotyczy argumentów reguł `Min`, `Max` lub `Range`, które definiują minimalną i maksymalną dozwoloną datę. +Jako wartość domyślną przyjmuje obiekty implementujące `DateTimeInterface`, ciąg zawierający czas albo liczbę reprezentującą uniksowy timestamp. To samo dotyczy argumentów reguł `Min`, `Max` czy `Range`, które definiują minimalną i maksymalną dozwoloną datę i czas. ```php $form->addDateTime('datetime', 'Data i czas:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Data musi być co najmniej miesiąc stara.', new DateTime('-1 month')); + ->addRule($form::Min, 'Data musi mieć co najmniej miesiąc.', new DateTime('-1 month')); ``` -Standardowo zwraca obiekt `DateTimeImmutable`, metodą `setFormat()` możesz specyfikować [format tekstowy|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] lub timestamp: +Domyślnie zwraca obiekt `DateTimeImmutable`. Metodą `setFormat()` możesz podać [format tekstowy|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] albo timestamp: ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -Dodaje pole do wyboru koloru (klasa [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Kolor jest stringiem w formacie `#rrggbb`. Jeśli użytkownik nie dokona wyboru, zwrócony zostanie czarny kolor `#000000`. +Dodaje pole wyboru koloru (klasa [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Kolor zwracany jest jako ciąg w formacie `#rrggbb`. Jeśli użytkownik nic nie wybierze, zwraca czarny `#000000`. ```php $form->addColor('color', 'Kolor:') @@ -339,8 +340,8 @@ $form->addColor('color', 'Kolor:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== Dodaje ukryte pole (klasa [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -348,28 +349,37 @@ Dodaje ukryte pole (klasa [HiddenField |api:Nette\Forms\Controls\HiddenField]). $form->addHidden('userid'); ``` -Za pomocą `setNullable()` można ustawić, aby zwracał `null` zamiast pustego stringa. Zmianę wysłanej wartości umożliwia [addFilter() |validation#Modyfikacja danych wejściowych]. +Metodą `setNullable()` sprawisz, że zamiast pustego ciągu zwróci `null`. Metoda [addFilter() |validation#Modyfikacja wpisanych wartości] pozwala zmodyfikować wysłaną wartość. -Chociaż element jest ukryty, **ważne jest, aby pamiętać**, że wartość może być nadal modyfikowana lub sfałszowana przez atakującego. Zawsze dokładnie sprawdzaj i waliduj wszystkie otrzymane wartości po stronie serwera, aby zapobiec ryzykom bezpieczeństwa związanym z manipulacją danymi. +Chociaż element jest ukryty, **ważne jest, żeby zdać sobie sprawę**, że jego wartość i tak może zostać zmodyfikowana albo podrobiona przez atakującego. Zawsze dokładnie weryfikuj i waliduj wszystkie otrzymane wartości po stronie serwera, żeby zapobiec zagrożeniom bezpieczeństwa związanym z manipulacją danymi. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== -Dodaje przycisk wysyłania (klasa [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). +Dodaje przycisk wysyłający (klasa [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). ```php $form->addSubmit('submit', 'Wyślij'); ``` -W formularzu można mieć również więcej przycisków wysyłania: +.{data-version:3.3.0} +Handler można przekazać przyciskowi bezpośrednio jako trzeci parametr `$onSubmit` zamiast podpinać go do zdarzenia `onClick`: + +```php +$form->addSubmit('submit', 'Wyślij', function (SubmitButton $button, $data): void { + // ... +}); +``` + +W formularzu może być więcej niż jeden przycisk wysyłający: ```php $form->addSubmit('register', 'Zarejestruj'); $form->addSubmit('cancel', 'Anuluj'); ``` -Aby dowiedzieć się, który z nich został kliknięty, użyj: +Żeby ustalić, który z nich został kliknięty, użyj: ```php if ($form['register']->isSubmittedBy()) { @@ -377,13 +387,13 @@ if ($form['register']->isSubmittedBy()) { } ``` -Jeśli nie chcesz walidować całego formularza po naciśnięciu przycisku (na przykład przy przyciskach *Anuluj* lub *Podgląd*), użyj [setValidationScope() |validation#Wyłączenie walidacji]. +Jeśli po naciśnięciu przycisku nie chcesz walidować całego formularza (na przykład dla przycisków *Anuluj* albo *Podgląd*), użyj [setValidationScope() |validation#Wyłączenie walidacji]. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -Dodaje przycisk (klasa [Button |api:Nette\Forms\Controls\Button]), który nie ma funkcji wysyłania. Można go więc wykorzystać do jakiejś innej funkcji, np. wywołania funkcji JavaScript po kliknięciu. +Dodaje przycisk (klasa [Button |api:Nette\Forms\Controls\Button]), który nie ma funkcji wysyłającej. Można go więc wykorzystać do innych funkcji, np. wywołania funkcji JavaScriptowej po kliknięciu. ```php $form->addButton('raise', 'Podnieś pensję') @@ -391,22 +401,22 @@ $form->addButton('raise', 'Podnieś pensję') ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= -Dodaje przycisk wysyłania w postaci obrazka (klasa [ImageButton |api:Nette\Forms\Controls\ImageButton]). +Dodaje przycisk wysyłający w postaci obrazka (klasa [ImageButton |api:Nette\Forms\Controls\ImageButton]). ```php -$form->addImageButton('submit', '/path/to/image'); +$form->addImageButton('submit', '/path/to/image.png', 'Wyślij'); ``` -Przy użyciu wielu przycisków wysyłania można dowiedzieć się, który został kliknięty, za pomocą `$form['submit']->isSubmittedBy()`. +Przy użyciu wielu przycisków wysyłających możesz ustalić, który został kliknięty, za pomocą `$form['submit']->isSubmittedBy()`. addContainer(string|int $name): Container .[method] =================================================== -Dodaje podformularz (klasa [Container|api:Nette\Forms\Container]), czyli kontener, do którego można dodawać kolejne elementy w ten sam sposób, jak dodajemy je do formularza. Działają również metody `setDefaults()` lub `getValues()`. +Dodaje podformularz (klasa [Container|api:Nette\Forms\Container]), czyli kontener, do którego można dodawać kolejne elementy w ten sam sposób, w jaki dodaje się je do formularza. Działają też metody takie jak `setDefaults()` czy `getValues()`. ```php $sub1 = $form->addContainer('first'); @@ -418,7 +428,7 @@ $sub2->addText('name', 'Twoje imię:'); $sub2->addEmail('email', 'Email:'); ``` -Wysłane dane zwraca następnie jako strukturę wielowymiarową: +Wysłane dane zwracane są potem jako struktura wielowymiarowa: ```php [ @@ -437,66 +447,64 @@ Wysłane dane zwraca następnie jako strukturę wielowymiarową: Przegląd ustawień ================= -U wszystkich elementów możemy wywoływać następujące metody (kompletny przegląd w [dokumentacji API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): +Dla wszystkich elementów możemy wywołać poniższe metody (kompletny przegląd znajdziesz w [dokumentacji API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): .[table-form-methods language-php] -| `setDefaultValue($value)` | ustawia wartość domyślną -| `getValue()` | pobiera aktualną wartość -| `setOmitted()` | [#pominięcie-wartości] -| `setDisabled()` | [#dezaktywacja-elementów] +| `setDefaultValue($value)` | ustawia wartość domyślną +| `getValue()` | pobiera bieżącą wartość +| `setOmitted()` | [#Pomijane wartości] +| `setDisabled()` | [#Wyłączanie elementów] Renderowanie: .[table-form-methods language-php] | `setCaption($caption)` | zmienia etykietę elementu -| `setTranslator($translator)` | ustawia [tłumacza |rendering#Tłumaczenie] +| `setTranslator($translator)` | ustawia [translator |rendering#Tłumaczenie] | `setHtmlAttribute($name, $value)` | ustawia [atrybut HTML |rendering#Atrybuty HTML] elementu | `setHtmlId($id)` | ustawia atrybut HTML `id` -| `setHtmlType($type)` | ustawia atrybut HTML `type` -| `setHtmlName($name)` | ustawia atrybut HTML `name` -| `setOption($key, $value)` | [ustawienia dla renderowania |rendering#Opcje] +| `setOption($key, $value)` | [ustawia opcje renderowania |rendering#Opcje] Walidacja: .[table-form-methods language-php] -| `setRequired()` | [element wymagany |validation] -| `addRule()` | ustawienie [reguły walidacyjnej |validation#Reguły] +| `setRequired()` | czyni element [obowiązkowym |validation] +| `addRule()` | dodaje [regułę walidacyjną |validation#Reguły] | `addCondition()`, `addConditionOn()` | ustawia [warunek walidacyjny |validation#Warunki] -| `addError($message)` | [przekazanie komunikatu błędu |validation#Błędy podczas przetwarzania] +| `addError($message)` | [dodaje komunikat o błędzie |validation#Błędy przy przetwarzaniu] -U elementów `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` można wywoływać następujące metody: +Dla elementów `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` można wywołać poniższe metody: .[table-form-methods language-php] -| `setNullable()` | ustawia, czy getValue() zwróci `null` zamiast pustego stringa -| `setEmptyValue($value)` | ustawia specjalną wartość, która jest uważana za pusty string -| `setMaxLength($length)` | ustawia maksymalną liczbę dozwolonych znaków -| `addFilter($filter)` | [modyfikacja wejścia |validation#Modyfikacja danych wejściowych] +| `setNullable()` | ustawia, czy getValue() zwraca `null` zamiast pustego ciągu +| `setEmptyValue($value)` | ustawia specjalną wartość, która jest traktowana jak pusty ciąg +| `setMaxLength($length)` | ustawia maksymalną dozwoloną liczbę znaków +| `addFilter($filter)` | [modyfikuje wejście |validation#Modyfikacja wpisanych wartości] -Pominięcie wartości -=================== +Pomijane wartości +================= -Jeśli wartość wprowadzona przez użytkownika nas nie interesuje, możemy ją za pomocą `setOmitted()` pominąć w wyniku metody `$form->getValues()` lub w danych przekazywanych do handlerów. Jest to przydatne dla różnych haseł kontrolnych, elementów antyspamowych itp. +Jeśli nie interesuje nas wartość wpisana przez użytkownika, możemy metodą `setOmitted()` wykluczyć ją z wyniku metody `$form->getValues()` albo z danych przekazywanych handlerom. Przydaje się to przy różnych polach do potwierdzania hasła, elementach antyspamowych itd. ```php -$form->addPassword('passwordVerify', 'Hasło do kontroli:') - ->setRequired('Proszę podać hasło jeszcze raz do kontroli') - ->addRule($form::Equal, 'Hasła się nie zgadzają', $form['password']) +$form->addPassword('passwordVerify', 'Hasło ponownie:') + ->setRequired('Wpisz hasło jeszcze raz dla kontroli') + ->addRule($form::Equal, 'Hasła nie są zgodne', $form['password']) ->setOmitted(); ``` -Dezaktywacja elementów -====================== +Wyłączanie elementów +==================== -Elementy można dezaktywować za pomocą `setDisabled()`. Takiego elementu użytkownik nie może edytować. +Elementy można wyłączyć metodą `setDisabled()`. Wyłączonego elementu użytkownik nie może edytować. ```php $form->addText('username', 'Nazwa użytkownika:') ->setDisabled(); ``` -Wyłączone elementy przeglądarka w ogóle nie wysyła na serwer, więc nie znajdziesz ich w danych zwróconych przez funkcję `$form->getValues()`. Jeśli jednak ustawisz `setOmitted(false)`, Nette do tych danych dołączy ich wartość domyślną. +Wyłączone elementy nie są w ogóle wysyłane przez przeglądarkę na serwer, więc nie znajdziesz ich w danych zwracanych przez funkcję `$form->getValues()`. Jeśli jednak ustawisz `setOmitted(false)`, Nette umieści w tych danych ich wartość domyślną. -Przy wywołaniu `setDisabled()` ze względów bezpieczeństwa **usuwana jest wartość elementu**. Jeśli ustawiasz wartość domyślną, należy to zrobić dopiero po jego dezaktywacji: +Przy wywołaniu `setDisabled()` ze względów bezpieczeństwa **wartość elementu jest czyszczona**. Jeśli ustawiasz wartość domyślną, trzeba zrobić to po wyłączeniu: ```php $form->addText('username', 'Nazwa użytkownika:') @@ -504,42 +512,26 @@ $form->addText('username', 'Nazwa użytkownika:') ->setDefaultValue($userName); ``` -Alternatywą dla wyłączonych elementów są elementy z atrybutem HTML `readonly`, które przeglądarka wysyła na serwer. Chociaż element jest tylko do odczytu, **ważne jest, aby pamiętać**, że jego wartość może być nadal modyfikowana lub sfałszowana przez atakującego. +Alternatywą dla elementów wyłączonych są elementy z atrybutem HTML `readonly`, które przeglądarka na serwer wysyła. Chociaż element jest tylko do odczytu, **ważne jest, żeby zdać sobie sprawę**, że jego wartość i tak może zostać zmodyfikowana albo podrobiona przez atakującego. Własne elementy =============== -Oprócz szerokiej gamy wbudowanych elementów formularza możesz dodawać do formularza własne elementy w ten sposób: +Oprócz szerokiej palety wbudowanych elementów formularza możesz dodawać do formularza własne elementy: ```php $form->addComponent(new DateInput('Data:'), 'date'); // alternatywna składnia: $form['date'] = new DateInput('Data:'); ``` -.[note] -Formularz jest potomkiem klasy [Container |component-model:#Container], a poszczególne elementy są potomkami [Component |component-model:#Component]. - -Istnieje sposób, jak zdefiniować nowe metody formularza służące do dodawania własnych elementów (np. `$form->addZip()`). Są to tzw. metody rozszerzające (extension methods). Wadą jest to, że dla nich nie będzie działać podpowiadanie w edytorach. - -```php -use Nette\Forms\Container; - -// dodajemy metodę addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Co najmniej 5 cyfr', '[0-9]{5}'); -}); - -// użycie -$form->addZip('zip', 'Kod pocztowy:'); -``` +Jak napisać taki element wraz z odczytem wysłanych danych, walidacją i renderowaniem, opisuje [osobny rozdział |custom-controls]. Dowiesz się tam także o metodach rozszerzających, które pozwalają utworzyć własną metodę dodającą, jak `$form->addZip()`. Elementy niskopoziomowe ======================= -Można również używać elementów, które zapiszemy tylko w szablonie i nie dodamy ich do formularza za pomocą którejś z metod `$form->addXyz()`. Kiedy na przykład wypisujemy rekordy z bazy danych i z góry nie wiemy, ile ich będzie i jakie będą miały ID, a chcemy przy każdym wierszu wyświetlić checkbox lub radio button, wystarczy zakodować go w szablonie: +Można używać także elementów, które są zapisane tylko w szablonie i nie zostały dodane do formularza żadną z metod `$form->addXyz()`. Na przykład przy wypisywaniu rekordów z bazy danych, gdy z góry nie wiemy, ile ich będzie ani jakie będą ich ID, i chcemy dla każdego wiersza wyświetlić checkbox albo radio button, wystarczy zakodować to w szablonie: ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ Można również używać elementów, które zapiszemy tylko w szablonie i nie d {/foreach} ``` -A po wysłaniu wartość odczytamy: +A po wysłaniu odczytamy wartość: ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -gdzie pierwszy parametr to typ elementu (`DataFile` dla `type=file`, `DataLine` dla jednoliniowych wejść jak `text`, `password`, `email` itp. i `DataText` dla wszystkich pozostałych), a drugi parametr `sel[]` odpowiada atrybutowi HTML name. Typ elementu możemy łączyć z wartością `DataKeys`, która zachowa klucze elementów. Jest to szczególnie przydatne dla `select`, `radioList` i `checkboxList`. +gdzie pierwszy parametr to typ elementu (`DataFile` dla `type=file`, `DataLine` dla inputów jednoliniowych, jak `text`, `password`, `email` itd., a `DataText` dla pozostałych), a drugi parametr `sel[]` odpowiada atrybutowi HTML name. Typ elementu możemy połączyć z wartością `DataKeys`, która zachowuje klucze elementów. Przydaje się to zwłaszcza przy `select`, `radioList` i `checkboxList`. -Istotne jest, że `getHttpData()` zwraca oczyszczoną wartość, w tym przypadku będzie to zawsze tablica prawidłowych stringów UTF-8, niezależnie od tego, co próbowałby podsunąć serwerowi atakujący. Jest to odpowiednik bezpośredniej pracy z `$_POST` lub `$_GET`, ale z tą istotną różnicą, że zawsze zwraca czyste dane, tak jak jesteś przyzwyczajony w standardowych elementach formularzy Nette. +Co istotne, `getHttpData()` zwraca oczyszczoną wartość. W tym przypadku będzie to zawsze tablica poprawnych ciągów UTF-8, niezależnie od tego, co atakujący spróbowałby wysłać na serwer. Jest to analogiczne do bezpośredniej pracy z `$_POST` albo `$_GET`, z tą istotną różnicą, że zawsze zwraca czyste dane, tak jak jesteś przyzwyczajony przy standardowych elementach formularzy Nette. diff --git a/forms/pl/custom-controls.texy b/forms/pl/custom-controls.texy new file mode 100644 index 0000000000..0a437f6cad --- /dev/null +++ b/forms/pl/custom-controls.texy @@ -0,0 +1,268 @@ +Własne elementy formularza +************************** + +.[perex] +Nette oferuje szeroką paletę [wbudowanych elementów formularza |controls]. Ale gdy natrafisz na wymaganie, którego wśród nich nie ma, nie musisz niczego obchodzić ani sklejać: napiszesz własny element. Będzie potrafił wszystko to co wbudowane, czyli walidować się, tłumaczyć, renderować, i używa się go dokładnie tak samo. + +Pokażemy to na praktycznym przykładzie: elemencie do wpisywania daty za pomocą trzech pól, dnia, miesiąca i roku. Po drodze dowiesz się wszystkiego, co trzeba wiedzieć o pisaniu elementów. + + +Kiedy pisać własny element, a kiedy nie +======================================= + +Własny element to najpotężniejsze narzędzie, jakie oferują formularze. I jak każde potężne narzędzie powinien być ostatnim, a nie pierwszym wyborem. Wiele sytuacji da się rozwiązać prostszymi środkami: + +- **Modyfikację wartości** załatwia [addFilter() |validation#Modyfikacja wpisanych wartości]. Chcesz tolerować spacje w kodzie pocztowym albo małe litery w kodzie? Filtr to kilka linii. +- **Powtarzającą się konfigurację** opakujesz własną metodą dodającą. Dodajesz pole na kod pocztowy z tą samą walidacją w dziesięciu miejscach? Utwórz dla nich nazwany skrót, [pokażemy to na końcu |#Własna metoda dodająca]. +- **Grupę powiązanych pól** obsłuży [kontener |controls#addContainer()]. Adres złożony z ulicy, miasta i kodu pocztowego nie potrzebuje własnego elementu, wystarczy kontener z trzema polami tekstowymi. +- **Inny wygląd** osiągniesz przez [setHtmlType() |controls#addText()] i atrybuty HTML albo [prototypy |rendering#Prototypy]. + +Własny element ma sens w momencie, gdy potrzebujesz **własnej wartości**: elementu, który na zewnątrz zachowuje się jak jedno pole z jedną wartością, ale wewnętrznie składa się z kilku inputów albo przechowuje wartość inaczej, niż ją wyświetla. Data z trzech pól. Współrzędne wybrane kliknięciem na mapie. Pole tagów z autouzupełnianiem. + + +Anatomia elementu +================= + +Każdy własny element dziedziczy po abstrakcyjnej klasie [api:Nette\Forms\Controls\BaseControl]. Po niej dziedziczy ogromną ilość gotowej funkcjonalności: przechowywanie wartości, reguły i warunki walidacyjne, komunikaty o błędach, tłumaczenia, atrybuty HTML, etykietę i powiązanie z renderowaniem. Piszesz tylko to, co Twój element odróżnia. + +Minimalny działający element jest zaskakująco krótki: + +```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, + ]); + } +} +``` + +Dwie metody: jedna mówi, jak uzyskać wartość z wysłanych danych, druga jak element wyrenderować. Obie za chwilę dokładnie omówimy. Cała reszta, czyli `setRequired()`, `addRule()`, `setDefaultValue()` czy tłumaczenia, działa już sama. + +Element dodajesz do formularza metodą `addComponent()` albo zwięźlej przez nawiasy kwadratowe: + +```php +$form['nickname'] = new SimpleInput('Pseudonim:'); +``` + + +Cykl życia elementu +=================== + +Zanim przejdziemy do ciekawszego elementu, dobrze jest wiedzieć, co i kiedy się z elementem dzieje. Formularz i jego elementy to [komponenty |component-model:] tworzące drzewo. Ma to jedną przyjemną konsekwencję: element nie musi niczego sam ustalać, framework zadba o wszystko, co ważne, we właściwym momencie: + +1) W momencie, gdy podepniesz element do wysłanego formularza, formularz sam wywoła na nim `loadHttpData()`. Element odczyta w niej swoją wysłaną wartość, co za chwilę pokażemy. Nigdy nie pracuje bezpośrednio z `$_POST` i w ogóle nie musi się przejmować, czy jest zagnieżdżony w kontenerach. + +2) Po wysłaniu formularza odbywa się walidacja: ewaluowane są reguły dodane przez `addRule()`, pracujące z wartością z `getValue()`. + +3) Kto potem wywoła `$form->getValues()` albo `getValue()` na elemencie, dostanie czystą, otypowaną wartość, na przykład obiekt `DateTimeImmutable`, a nie trójkę ciągów z formularza. + +A przy renderowaniu wywoływane jest `getControl()`, względnie `getLabel()` dla etykiety. + + +Odczyt wysłanej wartości +======================== + +W metodzie `loadHttpData()` element prosi o swoją wysłaną wartość metodą `getHttpData()`. Jej parametrem jest typ określający, jak ma zostać wartość oczyszczona: + +| typ | znaczenie +|------- +| `Form::DataLine` | tekst jednoliniowy: zamienia złamania linii na spacje, przycina spacje +| `Form::DataText` | tekst wieloliniowy: normalizuje końce linii do `\n` +| `Form::DataFile` | upload, instancja `Nette\Http\FileUpload` + +Choćby atakujący nie wiem jak się starał, wynikiem jest zawsze poprawny ciąg UTF-8 bez znaków sterujących (albo obiekt uploadu, albo `null`). Właśnie dlatego nigdy nie odczytujemy wartości bezpośrednio z `$_POST`: stracilibyśmy wszystkie te gwarancje. + +Element złożony z kilku inputów, jak nasza data, przekazuje jako drugi parametr część nazwy HTML i w ten sposób odczytuje swoje poszczególne podwartości. Przechowuje je we własnych właściwościach `$day`, `$month` i `$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]') ?? ''; +} +``` + +Jeśli nazwa HTML kończy się na `[]`, zwracana jest tablica wartości. Łącząc z typem `Form::DataKeys` (czyli `Form::DataLine | Form::DataKeys`), zachowasz również jej klucze: + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +Brakująca wartość to `null` (pusta tablica dla tablic). Żądanie w ogóle nie musi zawierać danych elementu, nic nie powstrzyma atakującego przed wysłaniem czegokolwiek: dlatego w przykładzie dopisujemy `?? ''` i dlatego zawsze powinieneś ten wariant uwzględniać. + + +Wartość elementu +================ + +Element przechowuje swoją wartość i udostępnia ją przez trójkę metod, których kontraktu warto się trzymać. + +Metoda `setValue()` przyjmuje wartość od programisty; tą drogą idzie także `setDefaultValue()` i `$form->setDefaults()`. Powinna przyjąć wszystko, co ma sens, przekonwertować wartość na postać wewnętrzną, a przy bezsensownym wejściu rzucić wyjątek, żeby błąd ujawnił się od razu, a nie przez tajemnicze zachowanie formularza. Nasza data przyjmuje `DateTimeInterface`, ciąg, timestamp albo `null` i rozkłada je na trzy pola: + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // bzdura rzuci wyjątek + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +Metoda `getValue()` z kolei składa czystą, otypowaną wartość, czyli jedyne, co zobaczy użytkownik Twojego elementu. Jeśli wartość nie jest poprawna, zwraca `null`. Statyczna metoda `validateDate()` po prostu sprawdza, czy trzy pola składają się na istniejącą datę: + +```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()` mówi, czy użytkownik element wypełnił; wykorzystuje ją reguła `setRequired()`. Domyślna implementacja (niepusta wartość) często wystarcza, ale dla elementu złożonego nadpisz ją zgodnie z jego logiką: + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Renderowanie +============ + +Metoda `getControl()` zwraca postać HTML elementu, zwykle jako obiekt [Html |utils:html-elements], ale zwykły ciąg też jest w porządku, to bez znaczenia. Po obiekt Html sięgamy głównie przy składaniu kodu, bo pozwala budować wynikowy markup bezpiecznie i z przyjemnym API. Do dyspozycji masz kilka pomocników: + +- `getHtmlName()` zwraca atrybut HTML `name` wraz z ewentualnym zagnieżdżeniem w kontenerach (np. `invoice[date]`). Dla elementu złożonego doklejasz do niego części nazw poszczególnych inputów: `$name . '[day]'`. +- `getHtmlId()` zwraca atrybut `id` powiązany z etykietą. +- `Helpers::exportRules($this->getRules())` eksportuje reguły walidacyjne dla atrybutu `data-nette-rules`, dzięki czemu dla Twojego elementu zadziała także [walidacja w JavaScripcie |validation#Walidacja w JavaScripcie]. Atrybut należy do pierwszego inputu elementu. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` składa element `<select>` z tablicy pozycji (zagnieżdżone tablice renderowane są jako `<optgroup>`) i zwraca go jako `Html`; przydaje się do pola miesiąca w naszej dacie. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` generuje listę elementów `<input>` opakowanych w `<label>` (radio buttony albo checkboxy) i zwraca ją jako ciąg. + +Pierwsze pole naszej daty tworzymy więc tak: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select dla miesiąca i input dla roku ... */); +} +``` + +Etykietę renderuje `getLabel()` i jego domyślna implementacja zwykle wystarcza. Uwaga tylko: dla elementu złożonego jego atrybut `for` wskazuje na `getHtmlId()`, więc to id nadaj pierwszemu inputowi, dokładnie jak w przykładzie. + +Żeby element złożony dało się renderować częściami w szablonie (np. `{input birthdate:day}`), nadpisz metody `getControlPart($key)` i `getLabelPart($key)`, które zwracają element `Html` dla danej części, tak samo jak robią to `CheckboxList` i `RadioList`. + +.[note] +Jeśli nadpisujesz `getControl()`, pamiętaj, że `BaseControl::getControl()` oznacza również element jako wyrenderowany przez `setOption('rendered', true)`. Wywołaj je również (albo wywołaj `parent::getControl()`), gdy w tym samym formularzu łączysz renderowanie ręczne i automatyczne, żeby element nie wyrenderował się dwa razy. (Powyższy przykład `DateInput` pomija to dla zwięzłości.) + + +Kompletny przykład: DateInput +============================= + +Wszystkie opisane elementy razem, uzupełnione o selectbox do wyboru miesiąca, znajdziesz w gotowym elemencie `DateInput` wśród [przykładów bezpośrednio w repozytorium |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Zauważ, że w konstruktorze element dodaje sam sobie regułę walidacyjną sprawdzającą, czy data ma sens. Bezsensowne wejście, jak 31 lutego, ujawnia się więc jako zwykły błąd walidacji formularza: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'Data jest nieprawidłowa.'); +} +``` + +A użycie? Dokładnie jak przy elementach wbudowanych: + +```php +$form['birthdate'] = (new DateInput('Data urodzenia:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('Kiedy się urodziłeś?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +W szablonie Latte wyrenderujesz go zwykłym tagiem `{input birthdate}` albo `{label birthdate /}`, tak jak każdy inny element. + + +Walidacja +========= + +Wbudowane reguły walidacyjne działają z własnym elementem od razu: operują na wartości z `getValue()`. Nasz `DateInput` może więc użyć na przykład `Form::Min` dla najstarszej dozwolonej daty. Jak pisać własne reguły wraz z ich odpowiednikiem w JavaScripcie, opisuje rozdział [Własne reguły i warunki |validation#Własne reguły i warunki]. + + +Własna metoda dodająca +====================== + +Wbudowane elementy dodajemy wygodnymi metodami `$form->addText()` i podobnymi. Własny element takiej metody nie ma, więc dodajesz go zwykłym przypisaniem: działa tak samo w formularzu i w kontenerze, a edytory i statyczna analiza to rozumieją: + +```php +$form['birthdate'] = new DateInput('Data urodzenia:'); +``` + +Jeśli chcesz skrócić dodawanie i zachować podpowiadanie, przyda się statyczna metoda fabrykująca bezpośrednio na elemencie. Działa nawet w zagnieżdżonych kontenerach, czego metoda na potomku klasy `Form` nie potrafiłaby: zagnieżdżone kontenery o niej nie wiedzą: + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// działa w formularzu i w dowolnym kontenerze: +DateInput::addTo($form, 'birthdate', 'Data urodzenia:'); +``` + +To samo podejście działa również jako nazwany skrót dla powtarzającej się konfiguracji elementu wbudowanego: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'Kod pocztowy musi mieć dokładnie 5 cyfr', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'Kod pocztowy:'); +``` diff --git a/forms/pl/in-presenter.texy b/forms/pl/in-presenter.texy index 512ebedc83..0e594855d4 100644 --- a/forms/pl/in-presenter.texy +++ b/forms/pl/in-presenter.texy @@ -1,16 +1,16 @@ -Formularze w prezenterach +Formularze w presenterach ************************* .[perex] -Nette Forms znacznie ułatwiają tworzenie i przetwarzanie formularzy internetowych. W tym rozdziale zapoznasz się z używaniem formularzy wewnątrz prezenterów. +Nette Forms znacząco upraszczają tworzenie i przetwarzanie formularzy webowych. W tym rozdziale dowiesz się, jak używać formularzy wewnątrz presenterów. -Jeśli interesuje Cię, jak używać ich całkowicie samodzielnie bez reszty frameworka, przeznaczony jest dla Ciebie przewodnik do [samodzielnego użycia|standalone]. +Jeśli interesuje Cię użycie całkowicie samodzielne, bez reszty frameworku, jest dla Ciebie przewodnik po [użyciu samodzielnym|standalone]. Pierwszy formularz ================== -Spróbujemy napisać prosty formularz rejestracyjny. Jego kod będzie następujący: +Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie wyglądać tak: ```php use Nette\Application\UI\Form; @@ -18,17 +18,17 @@ use Nette\Application\UI\Form; $form = new Form; $form->addText('name', 'Imię:'); $form->addPassword('password', 'Hasło:'); -$form->addSubmit('send', 'Zarejestruj'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->addSubmit('send', 'Zarejestruj się'); +$form->onSuccess[] = $this->formSucceeded(...); ``` a w przeglądarce wyświetli się tak: -[* form-cs.webp *] +[* form-en.webp *] -Formularz w prezenterze to obiekt klasy `Nette\Application\UI\Form`, jej poprzednik `Nette\Forms\Form` jest przeznaczony do samodzielnego użytku. Dodaliśmy do niego tzw. elementy imię, hasło i przycisk wysyłania. A na końcu linia z `$form->onSuccess` mówi, że po wysłaniu i pomyślnej walidacji ma zostać wywołana metoda `$this->formSucceeded()`. +Formularz w presenterze to obiekt klasy `Nette\Application\UI\Form`, jego poprzednik `Nette\Forms\Form` jest przeznaczony do użytku samodzielnego. Dodaliśmy elementy o nazwach name i password oraz przycisk wysyłający. Na koniec linia `$form->onSuccess` mówi, że po wysłaniu i udanej walidacji ma zostać wywołana metoda `$this->formSucceeded()`. -Z punktu widzenia prezentera formularz jest zwykłym komponentem. Dlatego traktuje się go jak komponent i włączamy go do prezentera za pomocą [metody fabrykującej |application:components#Metody fabrykujące]. Będzie to wyglądać tak: +Z perspektywy presentera formularz jest zwykłym komponentem. Dlatego traktuje się go jak komponent i włącza do presentera za pomocą [metody fabrykującej |application:components#Metody fabryczne]. Będzie to wyglądać tak: ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -41,23 +41,23 @@ class HomePresenter extends Nette\Application\UI\Presenter $form = new Form; $form->addText('name', 'Imię:'); $form->addPassword('password', 'Hasło:'); - $form->addSubmit('send', 'Zarejestruj'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->addSubmit('send', 'Zarejestruj się'); + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { - // tutaj przetwarzamy dane wysłane formularzem + // tutaj przetworzymy dane wysłane formularzem // $data->name zawiera imię // $data->password zawiera hasło - $this->flashMessage('Zostałeś pomyślnie zarejestrowany.'); + $this->flashMessage('Rejestracja przebiegła pomyślnie.'); $this->redirect('Home:'); } } ``` -A w szablonie formularz renderujemy znacznikiem `{control}`: +A w szablonie formularz renderujemy tagiem `{control}`: ```latte .{file:app/Presentation/Home/default.latte} <h1>Rejestracja</h1> @@ -65,42 +65,44 @@ A w szablonie formularz renderujemy znacznikiem `{control}`: {control registrationForm} ``` -I to właściwie wszystko :-) Mamy działający i doskonale [zabezpieczony |#Ochrona przed podatnościami] formularz. +I to w zasadzie wszystko :-) Mamy działający i doskonale [zabezpieczony |#Ochrona przed podatnościami] formularz. -A teraz pewnie myślisz, że to było za szybko, zastanawiasz się, jak to możliwe, że wywołuje się metoda `formSucceeded()` i jakie są parametry, które otrzymuje. Oczywiście, masz rację, to zasługuje na wyjaśnienie. +Teraz pewnie myślisz, że poszło to zbyt szybko, i zastanawiasz się, jak to możliwe, że wywołuje się metoda `formSucceeded()` i jakie parametry dostaje. Tak, masz rację, to zasługuje na wyjaśnienie. -Nette bowiem wprowadza świeży mechanizm, który nazywamy [Hollywood style |application:components#Styl Hollywood]. Zamiast tego, abyś jako programista musiał ciągle pytać, czy coś się wydarzyło („czy formularz został wysłany?”, „czy został wysłany poprawnie?” i „czy nie doszło do jego sfałszowania?”), mówisz frameworkowi „kiedy formularz będzie poprawnie wypełniony, wywołaj tę metodę” i zostawiasz dalszą pracę jemu. Jeśli programujesz w JavaScripcie, ten styl programowania znasz doskonale. Piszesz funkcje, które są wywoływane, gdy nastąpi określone [zdarzenie |nette:glossary#Eventy zdarzenia]. A język przekazuje im odpowiednie argumenty. +Nette wprowadza odświeżający mechanizm, który nazywamy [stylem hollywoodzkim |application:components#Styl hollywoodzki]. Zamiast tego, żebyś jako programista musiał ciągle pytać, czy coś się stało ("czy formularz został wysłany?", "czy został wysłany poprawnie?", "czy nie został podrobiony?"), mówisz frameworkowi "kiedy formularz będzie poprawnie wypełniony, wywołaj tę metodę" i dalszą pracę zostawiasz jemu. Jeśli programujesz w JavaScripcie, ten styl programowania znasz doskonale. Piszesz funkcje, które są wywoływane, gdy nastąpi określone [zdarzenie |nette:glossary#Zdarzenia]. A język przekazuje im odpowiednie argumenty. -Właśnie tak zbudowany jest również powyższy kod prezentera. Tablica `$form->onSuccess` reprezentuje listę callbacków PHP, które Nette wywoła w momencie, gdy formularz zostanie wysłany i poprawnie wypełniony (tj. jest ważny). W ramach [cyklu życia prezentera |application:presenters#Cykl życia presentera] jest to tzw. sygnał, wywoływane są więc po metodzie `action*` i przed metodą `render*`. A każdemu callbackowi przekazuje jako pierwszy parametr sam formularz, a jako drugi wysłane dane w postaci obiektu [ArrayHash |utils:arrays#ArrayHash]. Pierwszy parametr możesz pominąć, jeśli obiekt formularza nie jest potrzebny. A drugi parametr potrafi być sprytniejszy, ale o tym [później |#Mapowanie na klasy]. +Dokładnie tak zbudowany jest powyższy kod presentera. Tablica `$form->onSuccess` reprezentuje listę callbacków PHP, które Nette wywoła w momencie, gdy formularz zostanie wysłany i poprawnie wypełniony (czyli będzie valid). W ramach [cyklu życia presentera |application:presenters#Cykl życia presentera] jest to tak zwany sygnał, więc wywołują się po metodzie `action*`, a przed metodą `render*`. I każdemu callbackowi przekazuje jako pierwszy parametr sam formularz, a jako drugi wysłane dane w postaci obiektu [ArrayHash |utils:arrays#ArrayHash] (albo stdClass, albo własnej klasy). Pierwszy parametr możesz pominąć, jeśli obiekt formularza nie jest Ci potrzebny. Drugi parametr potrafi być sprytniejszy, ale o tym [później |#Mapowanie na klasy]. -Obiekt `$data` zawiera właściwości `name` i `password` z danymi, które wypełnił użytkownik. Zazwyczaj dane od razu wysyłamy do dalszego przetwarzania, co może być na przykład wstawienie do bazy danych. Podczas przetwarzania może jednak pojawić się błąd, na przykład nazwa użytkownika jest już zajęta. W takim przypadku błąd przekazujemy z powrotem do formularza za pomocą `addError()` i pozwalamy mu wyrenderować się ponownie, wraz z komunikatem błędu. +Obiekt `$data` zawiera właściwości `name` i `password` z danymi wpisanymi przez użytkownika. Zwykle wysyłamy dane bezpośrednio do dalszego przetwarzania, którym może być na przykład zapis do bazy danych. Podczas przetwarzania może jednak dojść do błędu, na przykład nazwa użytkownika jest już zajęta. W takim przypadku przekazujemy błąd z powrotem do formularza za pomocą `addError()` i pozwalamy go wyrenderować ponownie, wraz z komunikatem o błędzie. ```php -$form->addError('Przepraszamy, nazwa użytkownika jest już zajęta.'); +$form->addError('Przepraszamy, ta nazwa użytkownika jest już zajęta.'); ``` -Oprócz `onSuccess` istnieje jeszcze `onSubmit`: callbacki są wywoływane zawsze po wysłaniu formularza, nawet jeśli nie jest on poprawnie wypełniony. A dalej `onError`: callbacki są wywoływane tylko jeśli wysłanie nie jest ważne. Wywołają się nawet wtedy, jeśli w `onSuccess` lub `onSubmit` unieważnimy formularz za pomocą `addError()`. +Oprócz `onSuccess` istnieje jeszcze `onSubmit`: callbacki wywoływane są zawsze po wysłaniu formularza, nawet jeśli nie jest poprawnie wypełniony. Oraz `onError`: callbacki wywoływane są tylko wtedy, gdy wysłanie nie jest poprawne. Wywołają się nawet wtedy, gdy unieważnimy formularz w `onSuccess` za pomocą `addError()`. -Po przetworzeniu formularza przekierowujemy na następną stronę. Zapobiega to niechcianemu ponownemu wysłaniu formularza przyciskiem *odśwież*, *wstecz* lub poruszaniem się w historii przeglądarki. +Po przetworzeniu formularza przekierowujemy na kolejną stronę. Zapobiega to niepożądanemu ponownemu wysłaniu formularza przyciskiem *odśwież*, *wstecz* albo przez przemieszczanie się po historii przeglądarki. -Spróbuj dodać również inne [elementy formularza|controls]. +Jeśli formularz jest wysyłany przez AJAX, zwykle zamiast przekierowania przerysowujesz [snippet |application:ajax] z ponownie wyrenderowanym formularzem. + +Spróbuj dodać kolejne [elementy formularza|controls]. Dostęp do elementów =================== -Formularz jest komponentem prezentera, w naszym przypadku nazwanym `registrationForm` (według nazwy metody fabrykującej `createComponentRegistrationForm`), więc gdziekolwiek w prezenterze dostaniesz się do formularza za pomocą: +Formularz jest komponentem presentera, w naszym przypadku nazwanym `registrationForm` (po nazwie metody fabrykującej `createComponentRegistrationForm`), więc gdziekolwiek w presenterze dostaniesz się do formularza za pomocą: ```php $form = $this->getComponent('registrationForm'); // alternatywna składnia: $form = $this['registrationForm']; ``` -Komponentami są również poszczególne elementy formularza, dlatego dostaniesz się do nich w ten sam sposób: +Poszczególne elementy formularza również są komponentami, więc dostaniesz się do nich w ten sam sposób: ```php -$input = $form->getComponent('name'); // lub $input = $form['name']; -$button = $form->getComponent('send'); // lub $button = $form['send']; +$input = $form->getComponent('name'); // albo $input = $form['name']; +$button = $form->getComponent('send'); // albo $button = $form['send']; ``` Elementy usuwa się za pomocą `unset`: @@ -113,26 +115,26 @@ unset($form['name']); Reguły walidacyjne ================== -Padło tu słowo *ważny,* ale formularz na razie nie ma żadnych reguł walidacyjnych. Naprawmy to. +Padło słowo *valid*, ale formularz nie ma jeszcze żadnych reguł walidacyjnych. Naprawmy to. -Imię będzie obowiązkowe, dlatego oznaczymy je metodą `setRequired()`, której argumentem jest tekst komunikatu błędu, który wyświetli się, jeśli użytkownik nie wypełni imienia. Jeśli nie podamy argumentu, użyty zostanie domyślny komunikat błędu. +Imię będzie obowiązkowe, więc oznaczymy je metodą `setRequired()`. Jej argumentem jest tekst komunikatu o błędzie, który wyświetli się, jeśli użytkownik imienia nie wypełni. Jeśli argument pominiemy, użyty zostanie domyślny komunikat o błędzie. ```php $form->addText('name', 'Imię:') - ->setRequired('Proszę podać imię'); + ->setRequired('Podaj swoje imię.'); ``` -Spróbuj wysłać formularz bez wypełnionego imienia, a zobaczysz, że wyświetli się komunikat błędu, a przeglądarka lub serwer będzie go odrzucać, dopóki nie wypełnisz pola. +Spróbuj wysłać formularz bez wypełnionego imienia, a zobaczysz, że wyświetli się komunikat o błędzie, a przeglądarka albo serwer odrzuci go, dopóki pola nie wypełnisz. -Jednocześnie systemu nie oszukasz, wpisując w pole na przykład same spacje. Nic z tego. Nette lewo- i prawostronne spacje automatycznie usuwa. Wypróbuj to. To rzecz, którą powinieneś zawsze robić z każdym jednoliniowym inputem, ale często się o tym zapomina. Nette robi to automatycznie. (Możesz spróbować oszukać formularz i jako imię wysłać wieloliniowy string. Nawet tutaj Nette nie da się zmylić i znaki nowej linii zamieni na spacje.) +Jednocześnie systemu nie oszukasz, wpisując do pola na przykład same spacje. Nie ma szans. Nette automatycznie przycina białe znaki z lewej i prawej strony. Wypróbuj to. To coś, co powinieneś zawsze robić z każdym jednoliniowym inputem, ale o czym często się zapomina. Nette robi to automatycznie. (Możesz spróbować oszukać formularz i wysłać jako imię ciąg wieloliniowy. Nawet tutaj Nette nie da się nabrać, a złamania linii zostaną zamienione na spacje.) -Formularz zawsze waliduje się po stronie serwera, ale generuje się również walidacja JavaScriptowa, która przebiega błyskawicznie, a użytkownik dowiaduje się o błędzie natychmiast, bez konieczności wysyłania formularza na serwer. Za to odpowiada skrypt `netteForms.js`. Wstaw go do szablonu layoutu: +Formularz jest zawsze walidowany po stronie serwera, ale generowana jest też walidacja w JavaScripcie, która działa błyskawicznie i użytkownik dowiaduje się o błędzie natychmiast, bez potrzeby wysyłania formularza na serwer. Zajmuje się tym skrypt `netteForms.js`. Wstaw go do szablonu layoutu: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Jeśli spojrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, że Nette obowiązkowe elementy wstawia do elementów z klasą CSS `required`. Spróbuj dodać do szablonu następujący arkusz stylów, a etykieta „Imię” będzie czerwona. Elegancko w ten sposób oznaczamy użytkownikom obowiązkowe elementy: +Jeśli zajrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, że Nette otacza obowiązkowe elementy elementami z klasą CSS `required`. Spróbuj dodać do szablonu poniższy arkusz stylów, a etykieta "Imię" stanie się czerwona. Elegancko oznaczysz w ten sposób użytkownikom pola obowiązkowe: ```latte <style> @@ -140,44 +142,44 @@ Jeśli spojrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, </style> ``` -Kolejne reguły walidacyjne dodajemy metodą `addRule()`. Pierwszy parametr to reguła, drugi to ponownie tekst komunikatu błędu, a może jeszcze nastąpić argument reguły walidacyjnej. Co to oznacza? +Kolejne reguły walidacyjne dodajemy metodą `addRule()`. Pierwszym parametrem jest reguła, drugim znów tekst komunikatu o błędzie, a dalej może następować argument reguły walidacyjnej. Co to znaczy? -Formularz rozszerzymy o nowe nieobowiązkowe pole „wiek”, które musi być liczbą całkowitą (`addInteger()`) i dodatkowo w dozwolonym zakresie (`$form::Range`). I tutaj właśnie wykorzystamy trzeci parametr metody `addRule()`, którym przekażemy walidatorowi wymagany zakres jako parę `[od, do]`: +Rozszerzmy formularz o nowe, opcjonalne pole "wiek", które musi być liczbą całkowitą (`addInteger()`) i w dodatku z dozwolonego przedziału (`$form::Range`). I tutaj wykorzystamy trzeci parametr metody `addRule()`, którym przekażemy walidatorowi wymagany przedział jako parę `[min, max]`: ```php $form->addInteger('age', 'Wiek:') - ->addRule($form::Range, 'Wiek musi być od 18 do 120', [18, 120]); + ->addRule($form::Range, 'Wiek musi mieścić się między 18 a 120.', [18, 120]); ``` .[tip] -Jeśli użytkownik nie wypełni pola, reguły walidacyjne nie będą sprawdzane, ponieważ element jest nieobowiązkowy. +Jeśli użytkownik pola nie wypełni, reguły walidacyjne nie będą sprawdzane, bo element jest opcjonalny. -Tutaj powstaje przestrzeń na drobny refactoring. W komunikacie błędu i w trzecim parametrze liczby są podane podwójnie, co nie jest idealne. Gdybyśmy tworzyli [formularze wielojęzyczne |rendering#Tłumaczenie], a komunikat zawierający liczby byłby przetłumaczony na wiele języków, utrudniłoby to ewentualną zmianę wartości. Z tego powodu możliwe jest użycie symboli zastępczych `%d`, a Nette uzupełni wartości: +Powstaje tu miejsce na drobny refaktoring. W komunikacie o błędzie i w trzecim parametrze liczby są zduplikowane, co nie jest idealne. Gdybyśmy tworzyli [formularze wielojęzyczne |rendering#Tłumaczenie] i komunikat zawierający liczby byłby przetłumaczony na kilka języków, zmiana wartości stałaby się trudna. Z tego powodu można użyć zastępników `%d`, a Nette wartości uzupełni: ```php - ->addRule($form::Range, 'Wiek musi być od %d do %d lat', [18, 120]); + ->addRule($form::Range, 'Wiek musi mieścić się między %d a %d lat.', [18, 120]); ``` -Wróćmy do elementu `password`, który również uczynimy obowiązkowym i jeszcze zweryfikujemy minimalną długość hasła (`$form::MinLength`), ponownie z wykorzystaniem symbolu zastępczego: +Wróćmy do elementu `password`, uczyńmy go również obowiązkowym i sprawdźmy jeszcze minimalną długość hasła (`$form::MinLength`), znów z użyciem zastępnika w komunikacie: ```php $form->addPassword('password', 'Hasło:') ->setRequired('Wybierz hasło') - ->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8); + ->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków.', 8); ``` -Dodamy do formularza jeszcze pole `passwordVerify`, gdzie użytkownik poda hasło jeszcze raz, do kontroli. Za pomocą reguł walidacyjnych sprawdzimy, czy oba hasła są takie same (`$form::Equal`). A jako parametr podamy odwołanie do pierwszego hasła za pomocą [nawiasów kwadratowych |#Dostęp do elementów]: +Dodajmy do formularza jeszcze pole `passwordVerify`, w którym użytkownik wpisze hasło ponownie dla kontroli. Za pomocą reguł walidacyjnych sprawdzimy, czy oba hasła są takie same (`$form::Equal`). Jako argument podamy odwołanie do pierwszego hasła za pomocą [nawiasów kwadratowych |#Dostęp do elementów]: ```php -$form->addPassword('passwordVerify', 'Hasło do kontroli:') - ->setRequired('Proszę podać hasło jeszcze raz do kontroli') - ->addRule($form::Equal, 'Hasła się nie zgadzają', $form['password']) +$form->addPassword('passwordVerify', 'Hasło ponownie:') + ->setRequired('Wpisz hasło jeszcze raz dla kontroli literówki') + ->addRule($form::Equal, 'Hasła nie są zgodne.', $form['password']) ->setOmitted(); ``` -Za pomocą `setOmitted()` oznaczyliśmy element, na którego wartości właściwie nam nie zależy i który istnieje tylko w celu walidacji. Wartość nie zostanie przekazana do `$data`. +Za pomocą `setOmitted()` oznaczyliśmy element, którego wartość właściwie nas nie interesuje i który istnieje tylko na potrzeby walidacji. Jego wartość nie jest przekazywana do `$data`. -Tym samym mamy gotowy w pełni funkcjonalny formularz z walidacją w PHP i JavaScript. Możliwości walidacyjne Nette są znacznie szersze, można tworzyć warunki, pozwalać według nich wyświetlać i ukrywać części strony itp. Wszystkiego dowiesz się w rozdziale o [walidacji formularzy|validation]. +Tym samym mamy w pełni działający formularz z walidacją w PHP i JavaScripcie. Możliwości walidacyjne Nette są znacznie szersze, można tworzyć warunki, na ich podstawie pokazywać i ukrywać części strony itd. Wszystkiego dowiesz się w rozdziale o [walidacji formularzy|validation]. Wartości domyślne @@ -186,39 +188,41 @@ Wartości domyślne Elementom formularza często ustawiamy wartości domyślne: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -Często przydaje się ustawienie wartości domyślnych wszystkim elementom jednocześnie. Na przykład, gdy formularz służy do edycji rekordów. Odczytujemy rekord z bazy danych i ustawiamy wartości domyślne: +Często przydaje się ustawienie wartości domyślnych wszystkim elementom naraz. Na przykład wtedy, gdy formularz służy do edycji rekordów. Wczytamy rekord z bazy danych i ustawimy wartości domyślne: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Wywołuj `setDefaults()` dopiero po zdefiniowaniu elementów. +Wywołaj `setDefaults()` po zdefiniowaniu elementów. + +Na już wysłanym formularzu `setDefaults()` nie ma efektu: nie nadpisze tego, co użytkownik wypełnił, więc bezpiecznie możesz je wywoływać bezwarunkowo w fabryce formularza. Jeśli potrzebujesz wymusić wartości także po wysłaniu, użyj zamiast tego `setValues()`. Renderowanie formularza ======================= -Standardowo formularz renderuje się jako tabela. Poszczególne elementy spełniają podstawową zasadę dostępności - wszystkie etykiety są zapisane jako `<label>` i powiązane z odpowiednim elementem formularza. Po kliknięciu na etykietę kursor automatycznie pojawia się w polu formularza. +Domyślnie formularz renderowany jest jako tabela. Poszczególne elementy spełniają podstawowe zasady dostępności stron: wszystkie etykiety zapisane są jako elementy `<label>` i powiązane z odpowiednimi elementami formularza. Kliknięcie w etykietę automatycznie ustawia kursor w polu formularza. -Każdemu elementowi możemy ustawiać dowolne atrybuty HTML. Na przykład dodać placeholder: +Każdemu elementowi możemy ustawić dowolne atrybuty HTML. Dodajmy na przykład placeholder: ```php $form->addInteger('age', 'Wiek:') - ->setHtmlAttribute('placeholder', 'Proszę wypełnić wiek'); + ->setHtmlAttribute('placeholder', 'Podaj wiek'); ``` -Sposobów renderowania formularza jest naprawdę wiele, dlatego poświęcono temu [osobny rozdział o renderowaniu|rendering]. +Sposobów renderowania formularza jest naprawdę mnóstwo, dlatego poświęcony jest temu [osobny rozdział o renderowaniu|rendering]. Mapowanie na klasy ================== -Wróćmy do metody `formSucceeded()`, która w drugim parametrze `$data` otrzymuje wysłane dane jako obiekt `ArrayHash`. Ponieważ jest to klasa generyczna, coś jak `stdClass`, podczas pracy z nią będzie nam brakować pewnego komfortu, jak na przykład podpowiadania właściwości w edytorach czy statycznej analizy kodu. Można by to rozwiązać, tworząc dla każdego formularza konkretną klasę, której właściwości reprezentują poszczególne elementy. Np.: +Wróćmy do metody `formSucceeded()`, która w drugim parametrze `$data` otrzymuje wysłane dane jako obiekt `ArrayHash` (albo `stdClass`). Ponieważ jest to klasa generyczna, podobna do `stdClass`, brakuje nam przy pracy z nią pewnych wygód, na przykład podpowiadania właściwości w edytorach czy statycznej analizy kodu. Dałoby się to rozwiązać, mając dla każdego formularza konkretną klasę, której właściwości reprezentują poszczególne elementy. Np.: ```php class RegistrationFormData @@ -229,14 +233,14 @@ class RegistrationFormData } ``` -Alternatywnie możesz wykorzystać konstruktor: +Alternatywnie możesz użyć konstruktora: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } @@ -245,7 +249,7 @@ class RegistrationFormData Właściwości klasy danych mogą być również enumami i zostaną automatycznie zmapowane. .{data-version:3.2.4} -Jak powiedzieć Nette, aby zwracał nam dane jako obiekty tej klasy? Łatwiej niż myślisz. Wystarczy tylko podać klasę jako typ parametru `$data` w metodzie obsługującej: +Jak powiedzieć Nette, żeby zwracało dane jako obiekty tej klasy? Prościej, niż myślisz. Wystarczy podać klasę jako typ parametru `$data` w metodzie obsługującej: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void @@ -256,16 +260,18 @@ public function formSucceeded(Form $form, RegistrationFormData $data): void } ``` -Jako typ można również podać `array` a wtedy dane przekaże jako tablicę. +Jako typ możesz podać także `array`, a wtedy dane zostaną przekazane jako tablica. -Podobnym sposobem można używać również funkcji `getValues()`, której nazwę klasy lub obiekt do hydratacji przekażemy jako parametr: +Podobnie możesz użyć metody `getValues()`, przekazując jej jako parametr nazwę klasy albo obiekt do zhydratowania: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Jeśli formularze tworzą wielopoziomową strukturę złożoną z kontenerów, utwórz dla każdego osobną klasę: +Jeśli potrzebujesz odczytać wartości przed walidacją formularza, typowo wewnątrz handlera `onValidate`, użyj zamiast tego metody `getUntrustedValues()`. Przyjmuje te same parametry co `getValues()`, ale zwraca wysłane wartości bez gwarancji, że przeszły walidację. + +Jeśli formularze mają wielopoziomową strukturę złożoną z kontenerów, utwórz dla każdego osobną klasę: ```php $form = new Form; @@ -287,55 +293,58 @@ class RegistrationFormData } ``` -Mapowanie następnie z typu właściwości `$person` rozpozna, że ma mapować kontener na klasę `PersonFormData`. Jeśli właściwość zawierałaby tablicę kontenerów, podaj typ `array` a klasę do mapowania przekaż bezpośrednio kontenerowi: +Mapowanie wywnioskuje wtedy z typu właściwości `$person`, że ma zmapować kontener na klasę `PersonFormData`. Gdyby właściwość miała zawierać tablicę kontenerów, podaj typ `array` i przekaż klasę do zmapowania bezpośrednio kontenerowi: ```php $person->setMappedType(PersonFormData::class); ``` -Projekt klasy danych formularza możesz sobie wygenerować za pomocą metody `Nette\Forms\Blueprint::dataClass($form)`, która wypisze go na stronie przeglądarki. Kod następnie wystarczy kliknięciem zaznaczyć i skopiować do projektu. .{data-version:3.1.15} +Propozycję klasy danych formularza możesz wygenerować metodą `Nette\Forms\Blueprint::dataClass($form)`, która wypisze ją na stronie w przeglądarce. Następnie wystarczy kliknięciem zaznaczyć kod i skopiować go do projektu. .{data-version:3.1.15} -Wiele przycisków -================ +Wiele przycisków wysyłających +============================= -Jeśli formularz ma więcej niż jeden przycisk, zazwyczaj potrzebujemy rozróżnić, który z nich został naciśnięty. Możemy dla każdego przycisku utworzyć własną funkcję obsługującą. Ustawimy ją jako handler dla [zdarzenia |nette:glossary#Eventy zdarzenia] `onClick`: +Jeśli formularz ma więcej niż jeden przycisk, zwykle musimy rozróżnić, który z nich został naciśnięty. Dla każdego przycisku możemy utworzyć osobną funkcję obsługującą. Ustawimy ją jako handler [zdarzenia |nette:glossary#Zdarzenia] `onClick`: ```php $form->addSubmit('save', 'Zapisz') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Usuń') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -Te handlery są wywoływane tylko w przypadku poprawnie wypełnionego formularza, tak samo jak w przypadku zdarzenia `onSuccess`. Różnica polega na tym, że jako pierwszy parametr zamiast formularza może zostać przekazany przycisk wysyłania, zależy to od typu, który podasz: +.{data-version:3.3.0} +Handler można też przekazać przyciskowi bezpośrednio jako trzeci argument metody `addSubmit()`. + +Handlery te wywoływane są tylko wtedy, gdy formularz jest poprawnie wypełniony (chyba że dla przycisku wyłączono walidację), tak samo jak zdarzenie `onSuccess`. Różnica polega na tym, że jako pierwszy parametr może być przekazany zamiast formularza obiekt przycisku wysyłającego, zależnie od tego, jaki typ podasz: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Kiedy formularz zostanie wysłany przyciskiem <kbd>Enter</kbd>, traktuje się to tak, jakby został wysłany pierwszym przyciskiem. +Gdy formularz zostanie wysłany naciśnięciem klawisza <kbd>Enter</kbd>, traktowany jest tak, jakby został wysłany pierwszym przyciskiem wysyłającym. Zdarzenie onAnchor ================== -Kiedy w metodzie fabrykującej (jak np. `createComponentRegistrationForm`) budujemy formularz, ten jeszcze nie wie, czy został wysłany, ani z jakimi danymi. Są jednak przypadki, gdy potrzebujemy znać wysłane wartości, na przykład od nich zależy dalsza postać formularza, lub potrzebujemy ich do zależnych pól wyboru itp. +Gdy budujesz formularz w metodzie fabrykującej (jak `createComponentRegistrationForm`), nie wie on jeszcze, czy został wysłany ani z jakimi danymi. Są jednak przypadki, gdy potrzebujemy znać wysłane wartości, na przykład gdy od nich zależy wygląd formularza albo gdy są potrzebne dla zależnych selectboxów itd. -Część kodu budującego formularz możesz więc pozwolić wywołać dopiero w momencie, gdy jest tzw. zakotwiczony, czyli jest już połączony z prezenterem i zna swoje wysłane dane. Taki kod przekażemy do tablicy `$onAnchor`: +Możesz więc sprawić, żeby kod budujący formularz był wywoływany dopiero wtedy, gdy formularz jest "zakotwiczony", czyli już połączony z presenterem i znający swoje wysłane dane. Taki kod umieść w tablicy `$onAnchor`: ```php -$country = $form->addSelect('country', 'Państwo:', $this->model->getCountries()); +$country = $form->addSelect('country', 'Kraj:', $this->model->getCountries()); $city = $form->addSelect('city', 'Miasto:'); $form->onAnchor[] = function () use ($country, $city) { - // ta funkcja zostanie wywołana dopiero, gdy formularz będzie wiedział, czy został wysłany i z jakimi danymi - // można więc używać metody getValue() + // ta funkcja zostanie wywołana, gdy formularz będzie znał dane, z którymi został wysłany + // możesz więc użyć metody getValue() $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; @@ -345,35 +354,34 @@ $form->onAnchor[] = function () use ($country, $city) { Ochrona przed podatnościami =========================== -Nette Framework kładzie duży nacisk na bezpieczeństwo i dlatego skrupulatnie dba o dobre zabezpieczenie formularzy. Robi to całkowicie transparentnie i nie wymaga ręcznego ustawiania czegokolwiek. +Nette Framework kładzie ogromny nacisk na bezpieczeństwo i dlatego skrupulatnie dba o bezpieczeństwo formularzy. Robi to całkowicie transparentnie i nie wymaga żadnego ręcznego ustawiania. -Oprócz tego, że formularze chronią przed atakiem [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] i [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], wykonuje wiele drobnych zabezpieczeń, o których Ty już nie musisz myśleć. +Oprócz ochrony formularzy przed atakami takimi jak [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] i [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)] wykonuje mnóstwo drobnych zabezpieczeń, o których już nie musisz myśleć. -Na przykład odfiltrowuje ze wejść wszystkie znaki kontrolne i sprawdza poprawność kodowania UTF-8, dzięki czemu dane z formularza zawsze będą czyste. W polach wyboru i listach radio sprawdza, czy wybrane pozycje były rzeczywiście z oferowanych i czy nie doszło do fałszerstwa. Już wspominaliśmy, że w jednoliniowych wejściach tekstowych usuwa znaki końca linii, które mógł tam wysłać atakujący. W wieloliniowych wejściach z kolei normalizuje znaki końca linii. I tak dalej. +Na przykład odfiltrowuje z inputów wszystkie znaki sterujące i sprawdza poprawność kodowania UTF-8, dzięki czemu dane z formularza są zawsze czyste. Przy selectboxach i radiolistach weryfikuje, czy wybrane pozycje rzeczywiście były wśród oferowanych i czy nie doszło do podrobienia. Wspominaliśmy już, że przy jednoliniowych inputach tekstowych zamienia na spacje znaki końca linii, które mógłby wysłać atakujący. Przy inputach wieloliniowych normalizuje znaki końca linii. I tak dalej. -Nette rozwiązuje za Ciebie ryzyka bezpieczeństwa, o których wielu programistów nawet nie wie, że istnieją. +Nette rozwiązuje za Ciebie zagrożenia bezpieczeństwa, o których wielu programistów nawet nie wie, że istnieją. -Wspomniany atak CSRF polega na tym, że atakujący zwabia ofiarę na stronę, która niepozornie w przeglądarce ofiary wykonuje żądanie do serwera, na którym ofiara jest zalogowana, a serwer uważa, że żądanie wykonała ofiara z własnej woli. Dlatego Nette zapobiega wysyłaniu formularza POST z innej domeny. Jeśli z jakiegoś powodu chcesz wyłączyć ochronę i pozwolić na wysyłanie formularza z innej domeny, użyj: +Wspomniany atak CSRF polega na tym, że atakujący zwabi ofiarę na stronę, która po cichu wykona w przeglądarce ofiary żądanie do serwera, na którym ofiara jest zalogowana. Serwer uzna wtedy, że żądanie zostało wykonane przez ofiarę dobrowolnie. Dlatego Nette odrzuca formularze POST wysłane z obcego origin; za obcą uznaje się nawet inną subdomenę tej samej witryny. Jeśli potrzebujesz zezwolić na wysyłanie z innego origin, wyłącz ochronę: ```php -$form->allowCrossOrigin(); // UWAGA! Wyłącza ochronę! +$form->allowCrossOrigin(); // UWAGA! Wyłącza ochronę całkowicie! ``` -Ta ochrona wykorzystuje ciasteczko SameSite o nazwie `_nss`. Ochrona za pomocą ciasteczka SameSite może nie być w 100% niezawodna, dlatego warto włączyć jeszcze ochronę za pomocą tokenu: +To jednak wyłącza ochronę dla dowolnego origin. Żeby zezwolić tylko na konkretne, wyłącz ochronę i samodzielnie zweryfikuj nagłówek `Origin` względem własnej listy dozwolonych. -```php -$form->addProtection(); -``` +Ochrona opiera się na nagłówku przeglądarki `Sec-Fetch-Site` (Fetch Metadata), który przeglądarka wysyła automatycznie i którego nie da się podrobić nawet przy podatności XSS. Dla starszych przeglądarek bez ich wsparcia stosowany jest zapasowy cookie SameSite, które aplikacja Nette ustawia automatycznie. Szczegółowo opisuje to artykuł [Przeglądarka wreszcie rozwiązuje CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. -Zalecamy w ten sposób chronić formularze w administracyjnej części witryny, które zmieniają wrażliwe dane w aplikacji. Framework broni się przed atakiem CSRF poprzez wygenerowanie i weryfikację tokenu autoryzacyjnego, który jest przechowywany w sesji. Dlatego konieczne jest, aby przed wyświetleniem formularza sesja była otwarta. W administracyjnej części witryny zazwyczaj sesja jest już uruchomiona z powodu logowania użytkownika. W przeciwnym razie uruchom sesję metodą `Nette\Http\Session::start()`. +.[note] +Wcześniejsza ochrona za pomocą tokenu autoryzacyjnego przechowywanego w sesji, aktywowana przez `$form->addProtection()`, nie jest już potrzebna i od wersji 3.3 jest przestarzała. -Ten sam formularz w wielu prezenterach -====================================== +Użycie jednego formularza w wielu presenterach +============================================== -Jeśli potrzebujesz użyć jednego formularza w wielu prezenterach, zalecamy stworzenie dla niego fabryki, którą następnie przekażesz do prezentera. Odpowiednim miejscem dla takiej klasy jest np. katalog `app/Forms`. +Jeśli potrzebujesz użyć tego samego formularza w wielu presenterach, zalecamy utworzenie dla niego fabryki, którą następnie wstrzykniesz do presenterów. Odpowiednim miejscem dla takiej klasy jest na przykład katalog `app/Forms`. -Klasa fabryki może wyglądać na przykład tak: +Klasa fabryki może wyglądać tak: ```php use Nette\Application\UI\Form; @@ -390,7 +398,7 @@ class SignInFormFactory } ``` -Klasę poprosimy o wyprodukowanie formularza w metodzie fabrykującej na komponenty w prezenterze: +O klasę produkującą formularz poprosimy w metodzie fabrykującej komponent w presenterze: ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // możemy formularz zmodyfikować, tutaj na przykład zmieniamy etykietę na przycisku + // możemy formularz zmienić, tutaj na przykład zmieniamy etykietę na przycisku $form['send']->setCaption('Kontynuuj'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // i dodajemy handler + $form->onSuccess[] = $this->signInFormSuceeded(...); // i dodajemy handler return $form; } ``` -Handler do przetwarzania formularza może być również dostarczony już z fabryki: +Handler przetwarzający formularz może dostarczyć również sama fabryka: ```php use Nette\Application\UI\Form; @@ -421,11 +429,11 @@ class SignInFormFactory $form->addText('name', 'Imię:'); $form->addSubmit('send', 'Zaloguj się'); $form->onSuccess[] = function (Form $form, $data): void { - // tutaj wykonamy przetwarzanie formularza + // tutaj przetwarzamy wysłany formularz }; return $form; } } ``` -Tak, mamy za sobą szybkie wprowadzenie do formularzy w Nette. Spróbuj jeszcze zajrzeć do katalogu [examples|https://github.com/nette/forms/tree/master/examples] w dystrybucji, gdzie znajdziesz dalszą inspirację. +Tak oto mamy za sobą szybkie wprowadzenie do formularzy w Nette. Po więcej inspiracji zajrzyj do katalogu [examples |https://github.com/nette/forms/tree/master/examples] w dystrybucji. diff --git a/forms/pl/rendering.texy b/forms/pl/rendering.texy index 7d24fb440a..b32f5e77f4 100644 --- a/forms/pl/rendering.texy +++ b/forms/pl/rendering.texy @@ -1,35 +1,35 @@ Renderowanie formularzy *********************** -Wygląd formularzy może być bardzo różnorodny. W praktyce możemy napotkać dwa ekstrema. Z jednej strony stoi potrzeba renderowania w aplikacji wielu formularzy, które są wizualnie podobne jak dwie krople wody, i docenimy łatwe renderowanie bez szablonu za pomocą `$form->render()`. Jest to zazwyczaj przypadek interfejsów administracyjnych. +Wygląd formularzy może być bardzo różnorodny. W praktyce możemy napotkać dwie skrajności. Z jednej strony jest potrzeba wyrenderowania w aplikacji wielu formularzy, które wyglądają identycznie, i doceniamy łatwe renderowanie bez szablonu za pomocą `$form->render()`. Zwykle jest tak w interfejsach administracyjnych. -Z drugiej strony mamy różnorodne formularze, gdzie obowiązuje zasada: co sztuka, to oryginał. Ich postać najlepiej opiszemy językiem HTML w szablonie formularza. I oczywiście oprócz obu wspomnianych ekstremów napotkamy wiele formularzy, które znajdują się gdzieś pomiędzy. +Z drugiej strony są różnorodne formularze, z których każdy jest wyjątkowy. Ich wygląd najlepiej opisać HTML-em w szablonie formularza. I oczywiście oprócz tych dwóch skrajności napotkamy mnóstwo formularzy leżących gdzieś pośrodku. -Renderowanie za pomocą Latte -============================ +Renderowanie z Latte +==================== -[System szablonów Latte|latte:] znacznie ułatwia renderowanie formularzy i ich elementów. Najpierw pokażemy, jak renderować formularze ręcznie po poszczególnych elementach i tym samym uzyskać pełną kontrolę nad kodem. Później pokażemy, jak można takie renderowanie [zautomatyzować |#Automatyczne renderowanie]. +System szablonów [Latte |latte:] zasadniczo upraszcza renderowanie formularzy i ich elementów. Najpierw pokażemy, jak renderować formularz ręcznie, element po elemencie, i uzyskać pełną kontrolę nad kodem. Później pokażemy, jak takie renderowanie [zautomatyzować |#Renderowanie automatyczne]. -Projekt szablonu Latte formularza możesz sobie wygenerować za pomocą metody `Nette\Forms\Blueprint::latte($form)`, która wypisze go na stronie przeglądarki. Kod następnie wystarczy kliknięciem zaznaczyć i skopiować do projektu. .{data-version:3.1.15} +Propozycję szablonu Latte dla formularza możesz wygenerować metodą `Nette\Forms\Blueprint::latte($form)`, która wypisze go na stronie w przeglądarce. Następnie wystarczy kliknięciem zaznaczyć kod i skopiować go do projektu. .{data-version:3.1.15} `{control}` ----------- -Najprostszym sposobem renderowania formularza jest napisanie w szablonie: +Najprostszym sposobem wyrenderowania formularza jest napisanie w szablonie: ```latte {control signInForm} ``` -Można wpłynąć na wygląd tak renderowanego formularza konfigurując [#Renderer] i [poszczególne elementy |#Atrybuty HTML]. +Na wygląd wyrenderowanego formularza można wpłynąć konfiguracją [#Renderer] i [poszczególnych elementów |#Atrybuty HTML]. `n:name` -------- -Definicję formularza w kodzie PHP można niezwykle łatwo powiązać z kodem HTML. Wystarczy tylko uzupełnić atrybuty `n:name`. Takie to proste! +Powiązanie definicji formularza w kodzie PHP z kodem HTML jest niezwykle łatwe. Wystarczy dodać atrybuty `n:name`. Tak prosto to działa! ```php protected function createComponentSignInForm(): Form @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Postać wynikowego kodu HTML masz w pełni w swoich rękach. Jeśli atrybut `n:name` użyjesz w elementach `<select>`, `<button>` lub `<textarea>`, ich wewnętrzna zawartość zostanie automatycznie uzupełniona. Znacznik `<form n:name>` dodatkowo tworzy lokalną zmienną `$form` z obiektem renderowanego formularza, a zamykający `</form>` renderuje wszystkie niewyrenderowane elementy ukryte (to samo dotyczy również `{form} ... {/form}`). +Masz pełną kontrolę nad wyglądem wynikowego kodu HTML. Jeśli użyjesz atrybutu `n:name` przy elementach `<select>`, `<button>` albo `<textarea>`, ich wewnętrzna zawartość zostanie uzupełniona automatycznie. Poza tym tag `<form n:name>` tworzy lokalną zmienną `$form` z obiektem renderowanego formularza, a zamykający tag `</form>` renderuje wszystkie niewyrenderowane elementy ukryte (to samo dotyczy `{form} ... {/form}`). -Nie możemy jednak zapomnieć o renderowaniu możliwych komunikatów błędów. Zarówno tych, które metodą `addError()` zostały dodane do poszczególnych elementów (za pomocą `{inputError}`), jak i tych dodanych bezpośrednio do formularza (zwraca je `$form->getOwnErrors()`): +Nie możemy jednak zapomnieć o wyrenderowaniu ewentualnych komunikatów o błędach. Chodzi zarówno o te dodane do poszczególnych elementów metodą `addError()` (renderowane przez `{inputError}`), jak i o te dodane bezpośrednio do formularza (zwracane przez `$form->getOwnErrors()`): ```latte <form n:name=signInForm class=form> @@ -80,7 +80,7 @@ Nie możemy jednak zapomnieć o renderowaniu możliwych komunikatów błędów. </form> ``` -Bardziej złożone elementy formularza, takie jak RadioList lub CheckboxList, można w ten sposób renderować po poszczególnych pozycjach: +Bardziej złożone elementy formularza, jak RadioList albo CheckboxList, można renderować pozycja po pozycji tak: ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ Bardziej złożone elementy formularza, takie jak RadioList lub CheckboxList, mo `{label}` `{input}` ------------------- -Nie chcesz przy każdym elemencie zastanawiać się, jaki element HTML dla niego użyć w szablonie, czy `<input>`, `<textarea>` itp? Rozwiązaniem jest uniwersalny znacznik `{input}`: +Wolisz nie zastanawiać się w szablonie, jakiego elementu HTML użyć dla danego elementu formularza, czy `<input>`, czy `<textarea>` itd.? Rozwiązaniem jest uniwersalny tag `{input}`: ```latte <form n:name=signInForm class=form> @@ -114,9 +114,9 @@ Nie chcesz przy każdym elemencie zastanawiać się, jaki element HTML dla niego </form> ``` -Jeśli formularz używa translatora, tekst wewnątrz znaczników `{label}` będzie tłumaczony. +Jeśli formularz używa translatora, etykiety renderowane z definicji formularza (np. `{label username /}`) są tłumaczone. Tekst zapisany bezpośrednio między tagami `{label}` i `{/label}` już nie. -Również w tym przypadku bardziej złożone elementy formularza, takie jak RadioList lub CheckboxList, można renderować po poszczególnych pozycjach: +Znów bardziej złożone elementy formularza, jak RadioList albo CheckboxList, można renderować pozycja po pozycji: ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ Również w tym przypadku bardziej złożone elementy formularza, takie jak Radi {/foreach} ``` -Do renderowania samego `<input>` w elemencie Checkbox użyj `{input myCheckbox:}`. Atrybuty HTML w tym przypadku zawsze oddzielaj przecinkiem `{input myCheckbox:, class: required}`. +Żeby wyrenderować sam `<input>` dla elementu Checkbox, użyj `{input myCheckbox:}`. W takim przypadku zawsze oddzielaj atrybuty HTML przecinkiem: `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Wypisuje komunikat błędu dla elementu formularza, jeśli jakiś ma. Komunikat zazwyczaj opakowujemy w element HTML w celu stylizacji. Zapobiec renderowaniu pustego elementu, jeśli komunikatu nie ma, można elegancko za pomocą `n:ifcontent`: +Wypisuje komunikat o błędzie elementu formularza, jeśli taki istnieje. Komunikat zwykle opakowujemy w element HTML do ostylowania. Zapobiec renderowaniu pustego elementu, gdy komunikatu nie ma, można elegancko za pomocą `n:ifcontent`: ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -Obecność błędu możemy sprawdzić metodą `hasErrors()` i według tego ustawić klasę nadrzędnemu elementowi: +Obecność błędu możemy sprawdzić metodą `hasErrors()` i odpowiednio ustawić klasę elementu nadrzędnego: ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ Obecność błędu możemy sprawdzić metodą `hasErrors()` i według tego ustaw `{form}` -------- -Znaczniki `{form signInForm}...{/form}` są alternatywą dla `<form n:name="signInForm">...</form>`. +Tagi `{form signInForm}...{/form}` są alternatywą dla `<form n:name="signInForm">...</form>`. Ewentualne argumenty oddziel od nazwy przecinkiem: `{form signInForm, class: foo}`. +.{data-version:3.3.0} +Słowo kluczowe `scope` umieszczone przed nazwą tylko odkłada formularz na stos (żeby `{input}`, `{label}` itd. się z nim wiązały), ale nie renderuje tagu `<form>`. Przydaje się do renderowania części formularza, np. w snippecie. Jeśli jakiś formularz jest już aktywny, nazwa rozwiązywana jest względem niego, więc `{form scope}` zastępuje też `{formContainer}`: -Automatyczne renderowanie +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +Słowo kluczowe `detached` renderuje pusty `<form></form>` i wiąże z nim każdy element przez atrybut HTML `form`. Pozwala to umieścić formularz wewnątrz innego formularza, czego HTML normalnie zabrania. Odłączony formularz musi mieć HTML-owe `id`, które generowane jest automatycznie, gdy nadasz mu nazwę (jak `outerForm` poniżej): + +```latte +{form detached outerForm} + ... +{/form} +``` + + +Renderowanie automatyczne ------------------------- -Dzięki znacznikom `{input}` i `{label}` możemy łatwo stworzyć ogólny szablon dla dowolnego formularza. Będzie on stopniowo iterował i renderował wszystkie jego elementy, oprócz elementów ukrytych, które renderują się automatycznie przy zakończeniu formularza znacznikiem `</form>`. Nazwę renderowanego formularza będzie oczekiwał w zmiennej `$form`. +Dzięki tagom `{input}` i `{label}` możemy łatwo utworzyć ogólny szablon dla dowolnego formularza. Będzie przechodzić przez wszystkie jego elementy i je renderować, z wyjątkiem elementów ukrytych, które renderowane są automatycznie przy zamknięciu formularza tagiem `</form>`. Oczekuje nazwy renderowanego formularza w zmiennej `$form`. ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ Dzięki znacznikom `{input}` i `{label}` możemy łatwo stworzyć ogólny szablo </form> ``` -Użyte samozamykające się znaczniki parzyste `{label .../}` wyświetlają etykiety pochodzące z definicji formularza w kodzie PHP. +Użyte tu samozamykające się tagi parzyste `{label .../}` wypisują etykiety pochodzące z definicji formularza w kodzie PHP. -Ten ogólny szablon zapisz sobie na przykład do pliku `basic-form.latte`, a do renderowania formularza wystarczy go dołączyć i przekazać nazwę (lub instancję) formularza do parametru `$form`: +Ten ogólny szablon zapisz na przykład w pliku `basic-form.latte`. Żeby wyrenderować formularz, wystarczy go dołączyć i przekazać nazwę formularza (albo instancję) do parametru `$form`: ```latte {include basic-form.latte, form: signInForm} ``` -Gdybyś przy renderowaniu jednego określonego formularza chciał wpłynąć na jego postać i na przykład jeden element wyrenderować inaczej, najprostszą drogą jest przygotowanie sobie w szablonie bloków, które będzie można następnie nadpisać. Bloki mogą mieć również [nazwy dynamiczne |latte:template-inheritance#Dynamiczne nazwy bloków], można w nie wstawić również nazwę renderowanego elementu. Na przykład: +Jeśli chcesz przy renderowaniu zmodyfikować wygląd konkretnego formularza, na przykład wyrenderować jeden element inaczej, najprościej jest przygotować w szablonie bloki, które można następnie nadpisać. Bloki mogą mieć też [dynamiczne nazwy |latte:template-inheritance#Dynamiczne nazwy bloków], dzięki czemu możesz wstawić do nich nazwę renderowanego elementu. Na przykład: ```latte ... @@ -189,7 +207,7 @@ Gdybyś przy renderowaniu jednego określonego formularza chciał wpłynąć na ... ``` -Dla elementu np. `username` powstanie blok `input-username`, który można łatwo nadpisać użyciem znacznika [{embed} |latte:template-inheritance#Dziedziczenie jednostkowe]: +Dla elementu o nazwie np. `username` powstanie blok `input-username`, który można łatwo nadpisać tagiem [{embed} |latte:template-inheritance#Dziedziczenie jednostkowe]: ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ Dla elementu np. `username` powstanie blok `input-username`, który można łatw {/embed} ``` -Alternatywnie można całą zawartość szablonu `basic-form.latte` [zdefiniować |latte:template-inheritance#Definicje define] jako blok, włącznie z parametrem `$form`: +Alternatywnie całą zawartość szablonu `basic-form.latte` można [zdefiniować |latte:template-inheritance#Definicje] jako blok, włącznie z parametrem `$form`: ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ Alternatywnie można całą zawartość szablonu `basic-form.latte` [zdefiniowa {/define} ``` -Dzięki temu jego wywołanie będzie nieco prostsze: +Dzięki temu jego wywołanie będzie odrobinę prostsze: ```latte {embed basic-form, signInForm} @@ -219,17 +237,17 @@ Dzięki temu jego wywołanie będzie nieco prostsze: {/embed} ``` -Blok przy tym wystarczy zaimportować w jednym miejscu, na początku szablonu layoutu: +Blok wystarczy zaimportować w jednym miejscu, na początku szablonu layoutu: ```latte {import basic-form.latte} ``` -Przypadki specjalne -------------------- +Przypadki szczególne +-------------------- -Jeśli potrzebujesz wyrenderować tylko wewnętrzną część formularza bez znaczników HTML `<form>`, na przykład przy wysyłaniu snippetów, ukryj je za pomocą atrybutu `n:tag-if`: +Jeśli potrzebujesz wyrenderować tylko wewnętrzną część formularza bez tagów HTML `<form>`, na przykład przy wysyłaniu snippetów, ukryj je atrybutem `n:tag-if`: ```latte <form n:name=signInForm n:tag-if=false> @@ -240,10 +258,10 @@ Jeśli potrzebujesz wyrenderować tylko wewnętrzną część formularza bez zna </form> ``` -Z renderowaniem elementów wewnątrz kontenera formularza pomoże tag `{formContainer}`. +Z renderowaniem elementów wewnątrz kontenera formularza pomaga tag `{formContainer}`, względnie nowszy [`{form scope}` |#{form}]. ```latte -<p>Które wiadomości chcesz otrzymywać:</p> +<p>Jakie wiadomości chcesz otrzymywać:</p> {formContainer emailNews} <ul> @@ -257,39 +275,39 @@ Z renderowaniem elementów wewnątrz kontenera formularza pomoże tag `{formCont Renderowanie bez Latte ====================== -Najprostszym sposobem renderowania formularza jest wywołanie: +Najprostszym sposobem wyrenderowania formularza jest wywołanie: ```php $form->render(); ``` -Można wpłynąć na wygląd tak renderowanego formularza konfigurując [#Renderer] i [poszczególne elementy |#Atrybuty HTML]. +Na wygląd wyrenderowanego formularza można wpłynąć konfiguracją [#Renderer] i [poszczególnych elementów |#Atrybuty HTML]. -Ręczne renderowanie +Renderowanie ręczne ------------------- -Każdy element formularza dysponuje metodami, które generują kod HTML pola formularza i etykiety. Mogą go zwracać albo jako string, albo obiekt [Nette\Utils\Html|utils:html-elements]: +Każdy element formularza ma metody generujące kod HTML pola formularza i jego etykiety. Mogą go zwracać albo jako ciąg, albo jako obiekt [Nette\Utils\Html |utils:html-elements]: - `getControl(): Html|string` zwraca kod HTML elementu - `getLabel($caption = null): Html|string|null` zwraca kod HTML etykiety, jeśli istnieje -Formularz można więc renderować po poszczególnych elementach: +Pozwala to renderować formularz element po elemencie: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,21 +315,21 @@ Formularz można więc renderować po poszczególnych elementach: <?php $form->render('end') ?> ``` -Podczas gdy u niektórych elementów `getControl()` zwraca pojedynczy element HTML (np. `<input>`, `<select>` itp.), u innych cały fragment kodu HTML (CheckboxList, RadioList). W takim przypadku możesz wykorzystać metody, które generują poszczególne inputy i etykiety, dla każdej pozycji osobno: +Podczas gdy dla niektórych elementów `getControl()` zwraca pojedynczy element HTML (np. `<input>`, `<select>` itd.), dla innych zwraca cały kawałek kodu HTML (CheckboxList, RadioList). W takich przypadkach możesz użyć metod generujących osobno poszczególne inputy i etykiety dla każdej pozycji: -- `getControlPart($key = null): ?Html` zwraca kod HTML jednej pozycji -- `getLabelPart($key = null): ?Html` zwraca kod HTML etykiety jednej pozycji +- `getControlPart($key = null): Html` zwraca kod HTML jednej pozycji +- `getLabelPart($key = null): Html` zwraca kod HTML etykiety jednej pozycji .[note] -Te metody mają z historycznych powodów prefiks `get`, ale lepszy byłby `generate`, ponieważ przy każdym wywołaniu tworzą i zwracają nowy element `Html`. +Metody te mają ze względów historycznych przedrostek `get`, ale bardziej odpowiedni byłby `generate`, bo przy każdym wywołaniu tworzą i zwracają nowy element `Html`. Renderer ======== -Jest to obiekt zapewniający renderowanie formularza. Można go ustawić metodą `$form->setRenderer`. Przekazuje mu się sterowanie przy wywołaniu metody `$form->render()`. +To obiekt zapewniający wyrenderowanie formularza. Można go ustawić metodą `$form->setRenderer()`. Kontrola przekazywana jest mu w momencie wywołania metody `$form->render()`. -Jeśli nie ustawimy własnego renderera, zostanie użyty domyślny renderer [api:Nette\Forms\Rendering\DefaultFormRenderer]. Ten renderuje elementy formularza w postaci tabeli HTML. Wyjście wygląda tak: +Jeśli nie ustawimy własnego renderera, użyty zostanie domyślny renderer [api:Nette\Forms\Rendering\DefaultFormRenderer]. Ten renderuje elementy formularza do tabeli HTML. Wynik wygląda tak: ```latte <table> @@ -332,11 +350,11 @@ Jeśli nie ustawimy własnego renderera, zostanie użyty domyślny renderer [api ... ``` -Czy używać, czy nie używać tabeli dla szkieletu formularza, jest kwestią sporną, a wielu webdesignerów preferuje inne znaczniki. Na przykład listę definicji. Przekonfigurujemy więc `DefaultFormRenderer` tak, aby formularz wyrenderował w postaci listy. Konfiguracja odbywa się przez edycję tablicy [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Pierwszy indeks zawsze reprezentuje obszar, a drugi jego atrybut. Poszczególne obszary ilustruje obrazek: +To, czy używać tabeli do struktury formularza, jest dyskusyjne, a wielu webdesignerów woli inny markup, na przykład listę definicyjną. Przekonfigurujemy więc `DefaultFormRenderer` tak, żeby renderował formularz jako listę. Konfiguracja odbywa się przez edycję tablicy [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Pierwszy indeks zawsze reprezentuje obszar, a drugi jego atrybut. Poszczególne obszary pokazuje rysunek: -[* defaultformrenderer.webp *] +[* form-areas-en.webp *] -Standardowo grupa elementów `controls` jest opakowana tabelą `<table>`, każdy `pair` reprezentuje wiersz tabeli `<tr>`, a para `label` i `control` są komórkami `<th>` i `<td>`. Teraz zmienimy elementy opakowujące. Obszar `controls` włożymy do kontenera `<dl>`, obszar `pair` zostawimy bez kontenera, `label` włożymy do `<dt>`, a na końcu `control` opakujemy znacznikami `<dd>`: +Domyślnie grupa `controls` opakowana jest w `<table>`, każdy `pair` reprezentuje wiersz tabeli `<tr>`, a para `label` i `control` to komórki `<th>` i `<td>`. Teraz zmienimy elementy opakowujące. Obszar `controls` umieścimy w kontenerze `<dl>`, obszar `pair` zostawimy bez kontenera, `label` wstawimy do `<dt>`, a na koniec `control` opakujemy tagami `<dd>`: ```php $renderer = $form->getRenderer(); @@ -348,7 +366,7 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -Wynikiem jest ten kod HTML: +Powstanie w ten sposób poniższy kod HTML: ```latte <dl> @@ -367,49 +385,49 @@ Wynikiem jest ten kod HTML: </dl> ``` -W tablicy wrappers można wpłynąć na wiele innych atrybutów: +Tablica wrappers pozwala wpływać na wiele innych atrybutów: -- dodawać klasy CSS poszczególnym typom elementów formularza -- rozróżniać klasą CSS wiersze parzyste i nieparzyste -- wizualnie odróżniać pozycje obowiązkowe i opcjonalne -- określać, czy komunikaty błędów wyświetlą się bezpośrednio przy elementach, czy nad formularzem +- dodawanie klas CSS poszczególnym typom elementów formularza +- rozróżnianie klasami CSS wierszy nieparzystych i parzystych +- wizualne rozróżnianie pozycji obowiązkowych i opcjonalnych +- określanie, czy komunikaty o błędach wyświetlają się bezpośrednio przy elementach, czy nad formularzem Opcje ----- -Zachowanie Renderera można kontrolować również ustawiając *opcje* na poszczególnych elementach formularza. W ten sposób można ustawić opis, który wypisze się obok pola wejściowego: +Zachowanie Renderera można sterować także ustawianiem *opcji* poszczególnym elementom formularza. W ten sposób ustawisz opis, który wyświetli się obok pola: ```php -$form->addText('phone', 'Numer telefonu:') +$form->addText('phone', 'Numer:') ->setOption('description', 'Ten numer pozostanie ukryty'); ``` -Jeśli chcemy w nim umieścić zawartość HTML, wykorzystamy klasę [Html |utils:html-elements] +Jeśli chcemy umieścić w nim treść HTML, użyjemy klasy [Html |utils:html-elements]: ```php use Nette\Utils\Html; -$form->addText('phone', 'Numer telefonu:') +$form->addText('phone', 'Telefon:') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Warunki przechowywania Twojego numeru</a>') + ->setHtml('<a href="...">Regulamin usługi.</a>') ); ``` .[tip] -Element Html można wykorzystać również zamiast etykiety: `$form->addCheckbox('conditions', $label)`. +Elementu Html można użyć także zamiast etykiety: `$form->addCheckbox('conditions', $label)`. Grupowanie elementów -------------------- -Renderer umożliwia grupowanie elementów w wizualne grupy (fieldsety): +Renderer pozwala grupować elementy w wizualne grupy (fieldsety): ```php $form->addGroup('Dane osobowe'); ``` -Po utworzeniu nowej grupy staje się ona aktywna i każdy nowo dodany element jest jednocześnie dodawany również do niej. Więc formularz można budować w ten sposób: +Po utworzeniu nowej grupy staje się ona aktywna i każdy nowo dodany element jest dodawany także do niej. Formularz można więc budować w ten sposób: ```php $form = new Form; @@ -418,51 +436,51 @@ $form->addText('name', 'Twoje imię:'); $form->addInteger('age', 'Twój wiek:'); $form->addEmail('email', 'Email:'); -$form->addGroup('Adres wysyłki'); +$form->addGroup('Adres dostawy'); $form->addCheckbox('send', 'Wyślij na adres'); $form->addText('street', 'Ulica:'); $form->addText('city', 'Miasto:'); $form->addSelect('country', 'Kraj:', $countries); ``` -Renderer najpierw renderuje grupy, a dopiero potem elementy, które do żadnej grupy nie należą. +Renderer rysuje najpierw grupy, a dopiero potem elementy, które nie należą do żadnej grupy. -Wsparcie dla Bootstrap ----------------------- +Wsparcie dla Bootstrapa +----------------------- -[W przykładach |https://github.com/nette/forms/tree/master/examples] znajdziesz przykłady, jak skonfigurować Renderer dla [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] i [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] +W [katalogu examples |https://github.com/nette/forms/tree/master/examples] znajdziesz przykłady pokazujące, jak skonfigurować Renderer dla [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] i [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. Atrybuty HTML ============= -Do ustawienia dowolnych atrybutów HTML elementów formularza użyjemy metody `setHtmlAttribute(string $name, $value = true)`: +Do ustawiania dowolnych atrybutów HTML elementom formularza służy metoda `setHtmlAttribute(string $name, $value = true)`: ```php -$form->addInteger('number', 'Numer:') +$form->addInteger('number', 'Liczba:') ->setHtmlAttribute('class', 'big-number'); -$form->addSelect('rank', 'Sortuj wg:', ['ceny', 'nazwy']) - ->setHtmlAttribute('onchange', 'submit()'); // wysłać przy zmianie +$form->addSelect('rank', 'Sortuj według:', ['cena', 'nazwa']) + ->setHtmlAttribute('onchange', 'submit()'); // wysyła formularz przy zmianie -// Do ustawienia atrybutów samego <form> +// żeby ustawić atrybuty samego elementu <form> $form->setHtmlAttribute('id', 'myForm'); ``` -Specyfikacja typu elementu: +Podanie typu elementu: ```php $form->addText('tel', 'Twój telefon:') ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'wpisz numer telefonu'); + ->setHtmlAttribute('placeholder', 'Podaj swój telefon'); ``` .[warning] -Ustawienie typu i innych atrybutów służy tylko do celów wizualnych. Weryfikacja poprawności wejść musi odbywać się na serwerze, co zapewnisz wyborem odpowiedniego [elementu formularza|controls] i podaniem [reguł walidacyjnych|validation]. +Ustawianie typu i innych atrybutów służy tylko celom wizualnym. Weryfikacja poprawności wejścia musi odbywać się po stronie serwera, co zapewnisz wyborem odpowiedniego [elementu formularza |controls] i podaniem [reguł walidacyjnych |validation]. -Poszczególnym pozycjom w listach radio lub checkbox możemy ustawić atrybut HTML z różnymi wartościami dla każdej z nich. Zwróć uwagę na dwukropek za `style:`, który zapewnia wybór wartości według klucza: +Poszczególnym pozycjom w listach radio i checkbox możemy ustawić atrybut HTML o różnych wartościach dla każdej z nich. Zwróć uwagę na dwukropek po `style:`, który zapewnia wybór wartości według klucza: ```php $colors = ['r' => 'czerwony', 'g' => 'zielony', 'b' => 'niebieski']; @@ -471,7 +489,7 @@ $form->addCheckboxList('colors', 'Kolory:', $colors) ->setHtmlAttribute('style:', $styles); ``` -Wypisze: +Wyrenderuje: ```latte <label><input type="checkbox" name="colors[]" style="background:red" value="r">czerwony</label> @@ -479,14 +497,14 @@ Wypisze: <label><input type="checkbox" name="colors[]" value="b">niebieski</label> ``` -Do ustawienia atrybutów logicznych, takich jak `readonly`, możemy użyć zapisu ze znakiem zapytania: +Do ustawiania atrybutów logicznych, jak `readonly`, możemy użyć zapisu ze znakiem zapytania: ```php $form->addCheckboxList('colors', 'Kolory:', $colors) ->setHtmlAttribute('readonly?', 'r'); // dla wielu kluczy użyj tablicy, np. ['r', 'g'] ``` -Wypisze: +Wyrenderuje: ```latte <label><input type="checkbox" name="colors[]" readonly value="r">czerwony</label> @@ -494,14 +512,14 @@ Wypisze: <label><input type="checkbox" name="colors[]" value="b">niebieski</label> ``` -W przypadku pól wyboru metoda `setHtmlAttribute()` ustawia atrybuty elementu `<select>`. Jeśli chcemy ustawić atrybuty poszczególnym `<option>`, użyjemy metody `setOptionAttribute()`. Działają również zapisy z dwukropkiem i znakiem zapytania podane wyżej: +Przy selectboxach metoda `setHtmlAttribute()` ustawia atrybuty elementu `<select>`. Jeśli chcemy ustawić atrybuty poszczególnym elementom `<option>`, użyjemy metody `setOptionAttribute()`. Wspomniane wyżej zapisy z dwukropkiem i znakiem zapytania również działają: ```php $form->addSelect('colors', 'Kolory:', $colors) ->setOptionAttribute('style:', $styles); ``` -Wypisze: +Wyrenderuje: ```latte <select name="colors"> @@ -515,22 +533,22 @@ Wypisze: Prototypy --------- -Alternatywny sposób ustawiania atrybutów HTML polega na modyfikacji wzorca, z którego generowany jest element HTML. Wzorcem jest obiekt `Html` i zwraca go metoda `getControlPrototype()`: +Alternatywnym sposobem ustawiania atrybutów HTML jest modyfikacja szablonu, z którego generowany jest element HTML. Szablon jest obiektem `Html` i zwraca go metoda `getControlPrototype()`: ```php -$input = $form->addInteger('number', 'Numer:'); +$input = $form->addInteger('number', 'Liczba:'); $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -W ten sposób można modyfikować również wzorzec etykiety, który zwraca `getLabelPrototype()`: +W ten sposób można modyfikować także szablon etykiety zwracany przez `getLabelPrototype()`: ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -U elementów Checkbox, CheckboxList i RadioList możesz wpłynąć na wzorzec elementu, który cały element opakowuje. Zwraca go `getContainerPrototype()`. W stanie domyślnym jest to „pusty” element, więc nic się nie renderuje, ale przez ustawienie mu nazwy, będzie się renderować: +Przy elementach Checkbox, CheckboxList i RadioList możesz wpłynąć na szablon elementu, który opakowuje cały element formularza. Zwraca go `getContainerPrototype()`. Domyślnie jest to "pusty" element, więc nic się nie renderuje, ale gdy nadasz mu nazwę, zostanie wyrenderowany: ```php $input = $form->addCheckbox('send'); @@ -541,49 +559,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -W przypadku CheckboxList i RadioList można wpłynąć również na wzorzec separatora poszczególnych pozycji, który zwraca metoda `getSeparatorPrototype()`. W stanie domyślnym jest to element `<br>`. Jeśli zmienisz go na element parzysty, będzie poszczególne pozycje opakowywał zamiast oddzielać. A dalej można wpłynąć na wzorzec elementu HTML etykiety u poszczególnych pozycji, który zwraca `getItemLabelPrototype()`. +W przypadku CheckboxList i RadioList możesz wpłynąć także na szablon separatora poszczególnych pozycji zwracany metodą `getSeparatorPrototype()`. Domyślnie jest to element `<br>`. Jeśli zmienisz go na element parzysty, będzie opakowywał poszczególne pozycje zamiast je oddzielać. Poza tym możesz wpłynąć na szablon elementu HTML dla etykiet poszczególnych pozycji, zwracany przez `getItemLabelPrototype()`. Tłumaczenie =========== -Jeśli programujesz aplikację wielojęzyczną, prawdopodobnie będziesz potrzebować wyrenderować formularz w różnych wersjach językowych. Nette Framework w tym celu definiuje interfejs do tłumaczenia [api:Nette\Localization\Translator]. W Nette nie ma żadnej domyślnej implementacji, możesz wybrać według swoich potrzeb z kilku gotowych rozwiązań, które znajdziesz na [Componette |https://componette.org/search/localization]. W ich dokumentacji dowiesz się, jak konfigurować translator. +Jeśli tworzysz aplikację wielojęzyczną, prawdopodobnie będziesz potrzebować wyrenderować formularz w różnych wersjach językowych. Nette Framework definiuje w tym celu interfejs tłumaczący [api:Nette\Localization\Translator]. Nette nie ma domyślnej implementacji, możesz wybrać zgodnie ze swoimi potrzebami spośród kilku gotowych rozwiązań, które znajdziesz na [Componette |https://componette.org/search/localization]. Ich dokumentacja wyjaśnia, jak skonfigurować translator. -Formularze obsługują wypisywanie tekstów przez translator. Przekażemy im go za pomocą metody `setTranslator()`: +Formularze wspierają wypisywanie tekstów przez translator. Przekazujemy go metodą `setTranslator()`: ```php $form->setTranslator($translator); ``` -Od tej chwili nie tylko wszystkie etykiety, ale i wszystkie komunikaty błędów lub pozycje pól wyboru zostaną przetłumaczone na inny język. +Od tego momentu na język docelowy tłumaczone będą nie tylko wszystkie etykiety, ale też wszystkie komunikaty o błędach, pozycje w selectboxach czy placeholdery. -U poszczególnych elementów formularza można przy tym ustawić inny translator lub tłumaczenie całkowicie wyłączyć wartością `null`: +Poszczególnym elementom formularza można ustawić inny translator albo tłumaczenie całkowicie wyłączyć, ustawiając wartość `null`: ```php $form->addSelect('carModel', 'Model:', $cars) ->setTranslator(null); ``` -U [reguł walidacyjnych|validation] translatorowi przekazywane są również specyficzne parametry, na przykład u reguły: +Przy [regułach walidacyjnych |validation] translatorowi przekazywane są także konkretne parametry. Na przykład dla reguły: ```php $form->addPassword('password', 'Hasło:') ->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8); ``` -wywoływany jest translator z tymi parametrami: +translator wywoływany jest z tymi parametrami: ```php $translator->translate('Hasło musi mieć co najmniej %d znaków', 8); ``` -a więc może wybrać poprawną formę liczby mnogiej u słowa `znaków` według liczby. +i może więc wybrać poprawną formę liczby mnogiej słowa `znaków` zależnie od liczby. Zdarzenie onRender ================== -Tuż przed tym, jak formularz zostanie wyrenderowany, możemy pozwolić wywołać nasz kod. Ten może na przykład uzupełnić elementom formularza klasy HTML dla poprawnego wyświetlenia. Kod dodamy do tablicy `onRender`: +Tuż przed wyrenderowaniem formularza możemy wywołać własny kod. Ten może na przykład dodać elementom formularza klasy HTML dla poprawnego wyświetlenia. Kod dodajemy do tablicy `onRender`: ```php $form->onRender[] = function ($form) { diff --git a/forms/pl/standalone.texy b/forms/pl/standalone.texy index 3943cbbd2c..0bab9ed77c 100644 --- a/forms/pl/standalone.texy +++ b/forms/pl/standalone.texy @@ -2,15 +2,21 @@ Formularze używane samodzielnie ******************************* .[perex] -Nette Forms znacznie ułatwiają tworzenie i przetwarzanie formularzy internetowych. Możesz ich używać w swoich aplikacjach całkowicie samodzielnie, bez reszty frameworka, co pokażemy w tym rozdziale. +Nette Forms dramatycznie upraszczają tworzenie i przetwarzanie formularzy webowych. Możesz ich używać w swoich aplikacjach całkowicie samodzielnie, bez reszty frameworku, co pokazujemy w tym rozdziale. -Jeśli jednak używasz Nette Application i prezenterów, przeznaczony jest dla Ciebie przewodnik dotyczący [użycia w prezenterach|in-presenter]. +Jeśli jednak używasz Nette Application i presenterów, jest dla Ciebie osobny przewodnik: [formularze w presenterach |in-presenter]. Pierwszy formularz ================== -Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie następujący ("cały kod":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): +Zanim zaczniesz, zainstaluj pakiet za pomocą [Composera |best-practices:composer]: + +```shell +composer require nette/forms +``` + +Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie wyglądać tak ("pełny kod":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php use Nette\Forms\Form; @@ -18,22 +24,22 @@ use Nette\Forms\Form; $form = new Form; $form->addText('name', 'Imię:'); $form->addPassword('password', 'Hasło:'); -$form->addSubmit('send', 'Zarejestruj'); +$form->addSubmit('send', 'Zarejestruj się'); ``` -Bardzo łatwo go wyrenderujemy: +I wyrenderujmy go bardzo prosto: ```php $form->render(); ``` -a w przeglądarce wyświetli się tak: +Wynik w przeglądarce powinien wyglądać tak: -[* form-cs.webp *] +[* form-en.webp *] -Formularz jest obiektem klasy `Nette\Forms\Form` (klasa `Nette\Application\UI\Form` jest używana w prezenterach). Dodaliśmy do niego tzw. elementy: imię, hasło i przycisk wysyłający. +Formularz jest obiektem klasy `Nette\Forms\Form` (klasa `Nette\Application\UI\Form` używana jest w presenterach). Dodaliśmy do niego elementy o nazwach "name", "password" oraz przycisk wysyłający. -A teraz ożywimy formularz. Pytając `$form->isSuccess()` dowiemy się, czy formularz został wysłany i czy został wypełniony poprawnie. Jeśli tak, wypiszemy dane. Za definicją formularza dopiszemy więc: +Teraz ożywmy formularz. Zapytaniem `$form->isSuccess()` sprawdzimy, czy formularz został wysłany i czy był poprawnie wypełniony. Jeśli tak, wypiszemy dane. Za definicją formularza dopiszemy: ```php if ($form->isSuccess()) { @@ -45,32 +51,32 @@ if ($form->isSuccess()) { } ``` -Metoda `getValues()` zwraca przesłane dane w postaci obiektu [ArrayHash |utils:arrays#ArrayHash]. Jak to zmienić, pokażemy [później |#Mapowanie na klasy]. Obiekt `$data` zawiera klucze `name` i `password` z danymi, które wypełnił użytkownik. +Metoda `getValues()` zwraca wysłane dane jako obiekt [ArrayHash |utils:arrays#ArrayHash]. Jak to zmienić, pokażemy [później |#Mapowanie na klasy]. Obiekt `$data` zawiera klucze `name` i `password` z danymi wpisanymi przez użytkownika. -Zwykle dane od razu wysyłamy do dalszego przetwarzania, co może być na przykład wstawienie do bazy danych. Podczas przetwarzania może jednak pojawić się błąd, na przykład nazwa użytkownika jest już zajęta. W takim przypadku błąd przekazujemy z powrotem do formularza za pomocą `addError()` i pozwalamy mu wyrenderować się ponownie, wraz z komunikatem o błędzie. +Zwykle wysyłamy dane bezpośrednio do dalszego przetwarzania, którym może być na przykład zapis do bazy danych. Podczas przetwarzania może jednak dojść do błędu, na przykład nazwa użytkownika jest już zajęta. W takim przypadku przekazujemy błąd z powrotem do formularza za pomocą `addError()` i pozwalamy go wyrenderować ponownie, wraz z komunikatem o błędzie. ```php $form->addError('Przepraszamy, ta nazwa użytkownika jest już zajęta.'); ``` -Po przetworzeniu formularza przekierowujemy na następną stronę. Zapobiega to niechcianemu ponownemu wysłaniu formularza przyciskiem *odśwież*, *wstecz* lub poruszaniem się w historii przeglądarki. +Po przetworzeniu formularza przekierowujemy na kolejną stronę. Zapobiega to niezamierzonemu ponownemu wysłaniu formularza przyciskiem *odśwież* albo *wstecz* czy przez przemieszczanie się po historii przeglądarki. -Formularz standardowo wysyłany jest metodą POST i to na tę samą stronę. Oba te ustawienia można zmienić: +Domyślnie formularz wysyłany jest metodą POST na tę samą stronę. Jedno i drugie da się zmienić: ```php $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -I to właściwie wszystko :-) Mamy działający i doskonale [zabezpieczony |#Ochrona przed lukami w zabezpieczeniach] formularz. +I to w zasadzie wszystko :-) Mamy działający i doskonale [zabezpieczony |#Ochrona przed podatnościami] formularz. -Spróbuj dodać także inne [elementy formularza|controls]. +Spróbuj dodać też inne [elementy formularza |controls]. Dostęp do elementów =================== -Formularz i jego poszczególne elementy nazywamy komponentami. Tworzą one drzewo komponentów, gdzie korzeniem jest właśnie formularz. Do poszczególnych elementów formularza dostaniemy się w ten sposób: +Formularz i jego poszczególne elementy nazywamy komponentami. Tworzą one drzewo komponentów, którego korzeniem jest formularz. Do poszczególnych elementów formularza dostaniesz się tak: ```php $input = $form->getComponent('name'); @@ -80,36 +86,36 @@ $button = $form->getComponent('send'); // alternatywna składnia: $button = $form['send']; ``` -Elementy usuwa się za pomocą unset: +Elementy usuwa się za pomocą `unset`: ```php unset($form['name']); ``` -Reguły walidacji -================ +Reguły walidacyjne +================== -Padło tu słowo *poprawny,* ale formularz na razie nie ma żadnych reguł walidacji. Naprawmy to. +Padło słowo *valid*, ale formularz nie ma jeszcze żadnych reguł walidacyjnych. Naprawmy to. -Imię będzie obowiązkowe, dlatego oznaczymy je metodą `setRequired()`, której argumentem jest tekst komunikatu o błędzie, który wyświetli się, jeśli użytkownik nie wypełni imienia. Jeśli nie podamy argumentu, użyty zostanie domyślny komunikat o błędzie. +Imię będzie obowiązkowe, więc oznaczymy je metodą `setRequired()`. Jej argumentem jest tekst komunikatu o błędzie, który wyświetli się, jeśli użytkownik imienia nie wypełni. Jeśli argumentu nie podamy, użyty zostanie domyślny komunikat o błędzie. ```php $form->addText('name', 'Imię:') - ->setRequired('Proszę podać imię'); + ->setRequired('Podaj imię.'); ``` -Spróbuj wysłać formularz bez wypełnionego imienia, a zobaczysz, że wyświetli się komunikat o błędzie, a przeglądarka lub serwer będzie go odrzucać, dopóki nie wypełnisz pola. +Spróbuj wysłać formularz bez wypełnionego imienia, a zobaczysz, że pojawi się komunikat o błędzie. Przeglądarka albo serwer odrzuci go, dopóki pola nie wypełnisz. -Jednocześnie systemu nie oszukasz, wpisując w pole na przykład same spacje. Nic z tego. Nette automatycznie usuwa spacje z lewej i prawej strony. Wypróbuj to. To jest rzecz, którą powinieneś zawsze robić z każdym jednoliniowym inputem, ale często się o tym zapomina. Nette robi to automatycznie. (Możesz spróbować oszukać formularz i jako imię wysłać wieloliniowy ciąg znaków. Nawet tutaj Nette nie da się zwieść i zamieni znaki nowej linii na spacje.) +Jednocześnie systemu nie oszukasz, wpisując do inputu same spacje. Nie ma szans. Nette automatycznie przycina białe znaki z lewej i prawej strony. Wypróbuj to. To coś, co powinieneś zawsze robić z każdym jednoliniowym inputem, ale o czym często się zapomina. Nette robi to automatycznie. (Możesz spróbować oszukać formularz i wysłać jako imię ciąg wieloliniowy. Nawet tutaj Nette nie da się nabrać, a złamania linii zostaną zamienione na spacje.) -Formularz zawsze jest walidowany po stronie serwera, ale generowana jest również walidacja JavaScriptowa, która przebiega błyskawicznie, a użytkownik dowiaduje się o błędzie natychmiast, bez konieczności wysyłania formularza na serwer. Za to odpowiada skrypt `netteForms.js`. Wstaw go na stronę: +Formularz jest zawsze walidowany po stronie serwera, ale generowana jest też walidacja w JavaScripcie. Ta działa błyskawicznie i użytkownik dowiaduje się o błędzie natychmiast, bez potrzeby wysyłania formularza na serwer. Zajmuje się tym skrypt `netteForms.js`. Wstaw go na stronę: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Jeśli spojrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, że Nette obowiązkowe elementy wstawia do elementów z klasą CSS `required`. Spróbuj dodać do szablonu następujący arkusz stylów, a etykieta „Imię” będzie czerwona. W ten sposób elegancko zaznaczymy użytkownikom obowiązkowe elementy: +Jeśli zajrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, że Nette wstawia obowiązkowe elementy do elementów z klasą CSS `required`. Spróbuj dodać do szablonu poniższy arkusz stylów, a etykieta "Imię" stanie się czerwona. Elegancko oznaczysz w ten sposób użytkownikom elementy obowiązkowe: ```latte <style> @@ -117,25 +123,25 @@ Jeśli spojrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, </style> ``` -Kolejne reguły walidacji dodamy metodą `addRule()`. Pierwszy parametr to reguła, drugi to ponownie tekst komunikatu o błędzie, a może jeszcze nastąpić argument reguły walidacji. Co to oznacza? +Kolejne reguły walidacyjne dodajemy metodą `addRule()`. Pierwszym parametrem jest reguła, drugim znów tekst komunikatu o błędzie, a dalej może następować opcjonalny argument reguły walidacyjnej. Co to znaczy? -Formularz rozszerzymy o nowe, nieobowiązkowe pole „wiek”, które musi być liczbą całkowitą (`addInteger()`) i dodatkowo w dozwolonym zakresie (`$form::Range`). I tutaj właśnie wykorzystamy trzeci parametr metody `addRule()`, którym przekażemy walidatorowi wymagany zakres jako parę `[od, do]`: +Rozszerzmy formularz o nowe, opcjonalne pole "wiek", które musi być liczbą całkowitą (`addInteger()`) i mieścić się w dozwolonym przedziale (`$form::Range`). I tutaj wykorzystamy trzeci parametr metody `addRule()`, którym przekażemy walidatorowi wymagany przedział jako parę `[min, max]`: ```php $form->addInteger('age', 'Wiek:') - ->addRule($form::Range, 'Wiek musi być od 18 do 120', [18, 120]); + ->addRule($form::Range, 'Wiek musi mieścić się między 18 a 120.', [18, 120]); ``` .[tip] -Jeśli użytkownik nie wypełni pola, reguły walidacji nie będą sprawdzane, ponieważ element jest nieobowiązkowy. +Jeśli użytkownik pola nie wypełni, reguły walidacyjne nie będą sprawdzane, bo element jest opcjonalny. -Tutaj pojawia się miejsce na mały refactoring. W komunikacie o błędzie i w trzecim parametrze liczby są podane podwójnie, co nie jest idealne. Gdybyśmy tworzyli [formularze wielojęzyczne |rendering#Tłumaczenie] a komunikat zawierający liczby byłby przetłumaczony na wiele języków, utrudniłoby to ewentualną zmianę wartości. Z tego powodu można użyć symboli zastępczych `%d`, a Nette uzupełni wartości: +Powstaje tu miejsce na drobny refaktoring. W komunikacie o błędzie i w trzecim parametrze liczby są zduplikowane, co nie jest idealne. Gdybyśmy tworzyli [formularze wielojęzyczne |rendering#Tłumaczenie] i komunikat zawierający liczby byłby przetłumaczony na kilka języków, zmiana wartości stałaby się trudna. Z tego powodu można użyć zastępników `%d`, a Nette wartości uzupełni: ```php - ->addRule($form::Range, 'Wiek musi wynosić od %d do %d lat', [18, 120]); + ->addRule($form::Range, 'Wiek musi mieścić się między %d a %d lat.', [18, 120]); ``` -Wróćmy do elementu `password`, który również uczynimy obowiązkowym i jeszcze sprawdzimy minimalną długość hasła (`$form::MinLength`), ponownie wykorzystując symbol zastępczy: +Wróćmy do elementu `password`, uczyńmy go również obowiązkowym i sprawdźmy jeszcze minimalną długość hasła (`$form::MinLength`), znów z użyciem zastępnika w komunikacie: ```php $form->addPassword('password', 'Hasło:') @@ -143,18 +149,18 @@ $form->addPassword('password', 'Hasło:') ->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8); ``` -Dodamy do formularza jeszcze pole `passwordVerify`, gdzie użytkownik wprowadzi hasło jeszcze raz, dla kontroli. Za pomocą reguł walidacji sprawdzimy, czy oba hasła są takie same (`$form::Equal`). A jako parametr podamy odwołanie do pierwszego hasła za pomocą [nawiasów kwadratowych |#Dostęp do elementów]: +Dodajmy do formularza jeszcze pole `passwordVerify`, w którym użytkownik wpisze hasło ponownie dla kontroli. Za pomocą reguł walidacyjnych sprawdzimy, czy oba hasła są takie same (`$form::Equal`). Jako parametr podamy odwołanie do pierwszego hasła za pomocą [nawiasów kwadratowych |#Dostęp do elementów]: ```php -$form->addPassword('passwordVerify', 'Hasło do weryfikacji:') - ->setRequired('Proszę wprowadzić hasło ponownie w celu weryfikacji') - ->addRule($form::Equal, 'Hasła nie pasują', $form['password']) +$form->addPassword('passwordVerify', 'Hasło ponownie:') + ->setRequired('Wpisz hasło jeszcze raz dla kontroli') + ->addRule($form::Equal, 'Hasła nie są zgodne', $form['password']) ->setOmitted(); ``` -Za pomocą `setOmitted()` oznaczyliśmy element, którego wartość nas właściwie nie obchodzi i który istnieje tylko ze względu na walidację. Wartość nie zostanie przekazana do `$data`. +Za pomocą `setOmitted()` oznaczyliśmy element, którego wartość właściwie nas nie interesuje i który istnieje tylko na potrzeby walidacji. Jego wartość nie jest przekazywana do `$data`. -Tym samym mamy gotowy, w pełni funkcjonalny formularz z walidacją w PHP i JavaScript. Możliwości walidacyjne Nette są znacznie szersze, można tworzyć warunki, pozwalać na ich podstawie wyświetlać i ukrywać części strony itp. Wszystkiego dowiesz się w rozdziale o [walidacji formularzy|validation]. +Tym samym mamy w pełni działający formularz z walidacją w PHP i JavaScripcie. Możliwości walidacyjne Nette są znacznie szersze, można tworzyć warunki, na ich podstawie pokazywać i ukrywać części strony itd. Wszystkiego dowiesz się w rozdziale o [walidacji formularzy |validation]. Wartości domyślne @@ -163,39 +169,56 @@ Wartości domyślne Elementom formularza często ustawiamy wartości domyślne: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -Często przydaje się ustawienie wartości domyślnych dla wszystkich elementów jednocześnie. Na przykład, gdy formularz służy do edycji rekordów. Odczytujemy rekord z bazy danych i ustawiamy wartości domyślne: +Często przydaje się ustawienie wartości domyślnych wszystkim elementom naraz, na przykład wtedy, gdy formularz służy do edycji rekordów. Wczytamy rekord z bazy danych i ustawimy jego wartości jako domyślne: ```php // $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Wywołuj `setDefaults()` dopiero po zdefiniowaniu elementów. +Wywołaj `setDefaults()` po zdefiniowaniu elementów. + +Na już wysłanym formularzu `setDefaults()` nie ma efektu: nie nadpisze tego, co użytkownik wypełnił, więc bezpiecznie możesz je wywoływać bezwarunkowo w fabryce formularza. Jeśli potrzebujesz wymusić wartości także po wysłaniu, użyj zamiast tego `setValues()`. Renderowanie formularza ======================= -Standardowo formularz renderuje się jako tabela. Poszczególne elementy spełniają podstawową zasadę dostępności - wszystkie etykiety są zapisane jako `<label>` i powiązane z odpowiednim elementem formularza. Po kliknięciu na etykietę kursor automatycznie pojawia się w polu formularza. +Domyślnie formularz renderowany jest jako tabela. Poszczególne elementy spełniają podstawowe zasady dostępności: wszystkie etykiety generowane są jako elementy `<label>` i powiązane z odpowiednimi elementami formularza. Kliknięcie w etykietę automatycznie ustawia kursor w polu formularza. -Każdemu elementowi możemy ustawiać dowolne atrybuty HTML. Na przykład dodać placeholder: +Każdemu elementowi możemy ustawić dowolne atrybuty HTML. Dodajmy na przykład placeholder: ```php $form->addInteger('age', 'Wiek:') - ->setHtmlAttribute('placeholder', 'Proszę podać wiek'); + ->setHtmlAttribute('placeholder', 'Podaj wiek'); +``` + +Sposobów renderowania formularza jest mnóstwo, dlatego renderowaniu poświęcony jest [osobny rozdział |rendering]. + + +Renderowanie z Latte +-------------------- + +Jeśli masz pod ręką system szablonów [Latte |latte:], możesz pozwolić mu wyrenderować formularz i zyskać pełną kontrolę nad wynikowym HTML-em. Tworzysz silnik, rejestrujesz rozszerzenie formularzy i przekazujesz formularz do szablonu jako zmienną: + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); ``` -Sposobów na wyrenderowanie formularza jest naprawdę wiele, dlatego poświęcono temu [osobny rozdział o renderowaniu|rendering]. +W szablonie pracujesz potem z formularzem przez zmienną `$form` i tagi takie jak `{input}`, `{label}` czy `n:name`. Kompletny przykład wraz z szablonem znajdziesz w katalogu [examples |https://github.com/nette/forms/tree/master/examples] (pliki `latte.php` i `latte/`). Poszczególne tagi opisuje rozdział o [renderowaniu |rendering]. Mapowanie na klasy ================== -Wróćmy do przetwarzania danych formularza. Metoda `getValues()` zwracała nam przesłane dane jako obiekt `ArrayHash`. Ponieważ jest to klasa generyczna, coś w rodzaju `stdClass`, podczas pracy z nią zabraknie nam pewnego komfortu, jak na przykład podpowiadania właściwości w edytorach czy statycznej analizy kodu. Można by to rozwiązać, tworząc dla każdego formularza konkretną klasę, której właściwości reprezentują poszczególne elementy. Np.: +Wróćmy do przetwarzania danych formularza. Metoda `getValues()` zwróciła wysłane dane jako obiekt `ArrayHash`. Ponieważ jest to klasa generyczna, podobna do `stdClass`, brakuje nam przy pracy z nią pewnych wygód, na przykład podpowiadania właściwości w edytorach czy statycznej analizy kodu. Dałoby się to rozwiązać, mając dla każdego formularza konkretną klasę, której właściwości reprezentują poszczególne elementy. Np.: ```php class RegistrationFormData @@ -206,32 +229,32 @@ class RegistrationFormData } ``` -Alternatywnie możesz wykorzystać konstruktor: +Alternatywnie możesz użyć konstruktora: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Właściwości klasy danych mogą być również typu enum i zostaną automatycznie zmapowane. .{data-version:3.2.4} +Właściwości klasy danych mogą być również enumami i zostaną automatycznie zmapowane. .{data-version:3.2.4} -Jak powiedzieć Nette, aby zwracał nam dane jako obiekty tej klasy? Łatwiej niż myślisz. Wystarczy tylko nazwę klasy lub obiekt do hydratacji podać jako parametr: +Jak powiedzieć Nette, żeby zwracało dane jako obiekty tej klasy? Prościej, niż myślisz. Wystarczy podać jako parametr nazwę klasy albo obiekt do zhydratowania: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Jako parametr można podać również `'array'` i wtedy dane zwróci jako tablicę. +Jako parametr możesz podać także `'array'`, a dane zostaną zwrócone jako tablica. -Jeśli formularze tworzą wielopoziomową strukturę złożoną z kontenerów, utwórz dla każdego osobną klasę: +Jeśli formularze składają się z wielopoziomowej struktury złożonej z kontenerów, utwórz dla każdego osobną klasę: ```php $form = new Form; @@ -253,19 +276,19 @@ class RegistrationFormData } ``` -Mapowanie następnie z typu właściwości `$person` rozpozna, że ma kontener mapować na klasę `PersonFormData`. Jeśli właściwość zawierałaby tablicę kontenerów, podaj typ `array` i klasę do mapowania przekaż bezpośrednio kontenerowi: +Mapowanie wie wtedy z typu właściwości `$person`, że ma zmapować kontener na klasę `PersonFormData`. Gdyby właściwość miała zawierać tablicę kontenerów, podaj typ `array` i przekaż klasę do zmapowania bezpośrednio kontenerowi: ```php $person->setMappedType(PersonFormData::class); ``` -Projekt klasy danych formularza możesz wygenerować za pomocą metody `Nette\Forms\Blueprint::dataClass($form)`, która wypisze go na stronie przeglądarki. Kod wystarczy następnie kliknięciem zaznaczyć i skopiować do projektu. .{data-version:3.1.15} +Propozycję klasy danych formularza możesz wygenerować metodą `Nette\Forms\Blueprint::dataClass($form)`, która wypisze ją na stronie w przeglądarce. Następnie wystarczy kliknięciem zaznaczyć kod i skopiować go do projektu. .{data-version:3.1.15} -Wiele przycisków -================ +Wiele przycisków wysyłających +============================= -Jeśli formularz ma więcej niż jeden przycisk, zazwyczaj potrzebujemy rozróżnić, który z nich został naciśnięty. Tę informację zwróci nam metoda `isSubmittedBy()` przycisku: +Jeśli formularz ma więcej niż jeden przycisk, zwykle musimy rozróżnić, który z nich został naciśnięty. Tę informację zwraca metoda przycisku `isSubmittedBy()`: ```php $form->addSubmit('save', 'Zapisz'); @@ -282,36 +305,33 @@ if ($form->isSuccess()) { } ``` -Nie pomijaj zapytania `$form->isSuccess()`, sprawdzisz w ten sposób poprawność danych. +Nie pomijaj sprawdzenia `$form->isSuccess()`, weryfikuje ono poprawność danych. -Gdy formularz zostanie wysłany przyciskiem <kbd>Enter</kbd>, traktuje się to tak, jakby został wysłany pierwszym przyciskiem. +Gdy formularz zostanie wysłany naciśnięciem klawisza <kbd>Enter</kbd>, traktowany jest tak, jakby został wysłany pierwszym przyciskiem. -Ochrona przed lukami w zabezpieczeniach -======================================= +Ochrona przed podatnościami +=========================== -Nette Framework kładzie duży nacisk na bezpieczeństwo i dlatego skrupulatnie dba o dobre zabezpieczenie formularzy. +Nette Framework kładzie ogromny nacisk na bezpieczeństwo i dlatego skrupulatnie dba o właściwe zabezpieczenie formularzy. -Oprócz tego, że formularze chronią przed atakiem [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] i [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], wykonuje wiele drobnych zabezpieczeń, o których Ty już nie musisz myśleć. +Oprócz ochrony formularzy przed dobrze znanymi podatnościami, takimi jak [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] i [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], wykonuje mnóstwo drobnych zabezpieczeń, o których już nie musisz myśleć. -Na przykład odfiltrowuje ze wejść wszystkie znaki kontrolne i sprawdza poprawność kodowania UTF-8, dzięki czemu dane z formularza zawsze będą czyste. W przypadku select boxów i list radio sprawdza, czy wybrane pozycje rzeczywiście pochodziły z oferowanych i czy nie doszło do fałszerstwa. Już wspominaliśmy, że w przypadku jednoliniowych wejść tekstowych usuwa znaki końca linii, które mógł tam wysłać atakujący. W przypadku wejść wieloliniowych z kolei normalizuje znaki końca linii. I tak dalej. +Na przykład odfiltrowuje z inputów wszystkie znaki sterujące i sprawdza poprawność kodowania UTF-8, dzięki czemu dane z formularza są zawsze czyste. Przy selectboxach i radiolistach weryfikuje, czy wybrane pozycje rzeczywiście były wśród oferowanych i czy nie doszło do podrobienia. Wspominaliśmy już, że przy jednoliniowych inputach tekstowych zamienia na spacje znaki końca linii, które mógłby wysłać atakujący. Przy inputach wieloliniowych normalizuje znaki końca linii. I tak dalej. -Nette rozwiązuje za Ciebie ryzyka bezpieczeństwa, o których wielu programistów nawet nie wie, że istnieją. +Nette rozwiązuje za Ciebie zagrożenia bezpieczeństwa, o których wielu programistów nawet nie wie, że istnieją. -Wspomniany atak CSRF polega na tym, że atakujący zwabia ofiarę na stronę, która niepostrzeżenie w przeglądarce ofiary wykonuje żądanie do serwera, na którym ofiara jest zalogowana, a serwer sądzi, że żądanie wykonała ofiara z własnej woli. Dlatego Nette zapobiega wysyłaniu formularza POST z innej domeny. Jeśli z jakiegoś powodu chcesz wyłączyć ochronę i pozwolić na wysyłanie formularza z innej domeny, użyj: +Wspomniany atak CSRF polega na tym, że atakujący zwabi ofiarę na stronę, która po cichu wykona w przeglądarce ofiary żądanie do serwera, na którym ofiara jest właśnie zalogowana. Serwer uzna, że żądanie zostało wykonane przez ofiarę dobrowolnie. Dlatego Nette odrzuca formularze POST wysłane z obcego origin; za obcą uznaje się nawet inną subdomenę tej samej witryny. Jeśli potrzebujesz zezwolić na wysyłanie z innego origin, wyłącz ochronę: ```php -$form->allowCrossOrigin(); // UWAGA! Wyłącza ochronę! +$form->allowCrossOrigin(); // UWAGA! Wyłącza ochronę całkowicie! ``` -Ta ochrona wykorzystuje ciasteczko SameSite o nazwie `_nss`. Twórz zatem obiekt formularza jeszcze przed wysłaniem pierwszego wyjścia, aby można było wysłać ciasteczko. - -Ochrona za pomocą ciasteczka SameSite może nie być w 100% niezawodna, dlatego warto włączyć jeszcze ochronę za pomocą tokenu: +To jednak wyłącza ochronę dla dowolnego origin. Żeby zezwolić tylko na konkretne, wyłącz ochronę i samodzielnie zweryfikuj nagłówek `Origin` względem własnej listy dozwolonych. -```php -$form->addProtection(); -``` +Ochrona opiera się na nagłówku przeglądarki `Sec-Fetch-Site` (Fetch Metadata), który przeglądarka wysyła automatycznie i którego nie da się podrobić nawet przy podatności XSS. Starsze przeglądarki, które tych nagłówków nie wysyłają, kontroli nie przejdą. Szczegółowo opisuje to artykuł [Przeglądarka wreszcie rozwiązuje CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. -Zalecamy w ten sposób chronić formularze w części administracyjnej strony, które zmieniają wrażliwe dane w aplikacji. Framework broni się przed atakiem CSRF, generując i weryfikując token autoryzacyjny, który jest przechowywany w sesji. Dlatego konieczne jest, aby przed wyświetleniem formularza sesja była otwarta. W części administracyjnej strony zazwyczaj sesja jest już uruchomiona ze względu na logowanie użytkownika. W przeciwnym razie uruchom sesję metodą `Nette\Http\Session::start()`. +.[note] +Wcześniejsza ochrona za pomocą tokenu autoryzacyjnego przechowywanego w sesji, aktywowana przez `$form->addProtection()`, nie jest już potrzebna i od wersji 3.3 jest przestarzała. -Tak, mamy za sobą szybkie wprowadzenie do formularzy w Nette. Spróbuj jeszcze zajrzeć do katalogu [examples|https://github.com/nette/forms/tree/master/examples] w dystrybucji, gdzie znajdziesz więcej inspiracji. +Tak oto mamy za sobą szybkie wprowadzenie do formularzy w Nette. Po więcej inspiracji zajrzyj do katalogu [examples |https://github.com/nette/forms/tree/master/examples] w dystrybucji. diff --git a/forms/pl/upgrading.texy b/forms/pl/upgrading.texy new file mode 100644 index 0000000000..5c1f99ab5d --- /dev/null +++ b/forms/pl/upgrading.texy @@ -0,0 +1,54 @@ +Aktualizacja +************ + + +Aktualizacja do wersji 3.3 +========================== + +- automatyczna ochrona przed CSRF przeszła z `isSameSite()` na `isFrom(FetchSite::SameOrigin)` i stała się surowsza: żądania przychodzące z subdomen już nie przechodzą +- dzięki temu `addProtection()` nie jest już potrzebne, bo automatyczna ochrona pokrywa te same przypadki; w nowych formularzach je pomiń, a z istniejących śmiało usuń + +Dlaczego tokeny w sesji nie są już potrzebne, wyjaśnia artykuł [Ćwierć wieku CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Aktualizacja do wersji 3.1 +========================== + +- `getValues()` zwraca tylko zwalidowane elementy; jeśli potrzebujesz wartości wszystkich elementów niezależnie od walidacji, użyj nowej metody `getUntrustedValues()` +- `$values` przekazywane do handlerów `onSuccess` i `onClick` zawierają tak samo tylko zwalidowane elementy +- samodzielne formularze są automatycznie chronione przed CSRF za pomocą cookie z flagą SameSite; wysyłanie z innego origin możesz dopuścić metodą `allowCrossOrigin()` +- reguła `Form::URL` uzupełnia teraz brakujący protokół jako `https` zamiast `http` +- `Form::addImage()` zostało przemianowane na `addImageButton()` +- `Checkbox::getSeparatorPrototype()` zostało przemianowane na `getContainerPrototype()` +- formularze nie tworzą już w szablonach zmiennej `$_form` + +Więcej o tych zmianach w artykule [Nowości w Nette Forms 3.1 |https://blog.nette.org/en/news-in-nette-forms-3-1]. + + +Aktualizacja do wersji 3.0 +========================== + +- wszystkie elementy formularza są teraz domyślnie opcjonalne (ta zmiana pojawiła się w Nette 2.4), więc `setRequired(false)` możesz usunąć +- koniecznie zaktualizuj `netteForms.js` do wersji 3 (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` i `MultiChoiceControl::$checkAllowedValues` zostały zastąpione metodą `checkDefaultValue()` + + +Aktualizacja do wersji 2.4 +========================== + +- jeśli element ma regułę dodaną przez `addRule()` (czyli jest faktycznie obowiązkowy), musisz oznaczyć go jako obowiązkowy także przez `setRequired()`; poza tym `setRequired(false)` czyni teraz element opcjonalnym, co zastępuje gałęzie `addCondition($form::FILLED)` +- walidatory `Form::EMAIL`, `URL` i `INTEGER` automatycznie zmieniają atrybut HTML `type` odpowiednio na `email`, `url` i `number` +- negatywne reguły walidacyjne są przestarzałe; alternatywą dla `~Form::FILLED` jest `Form::BLANK`, a `~Form::EQUAL` możesz zastąpić przez `Form::NOT_EQUAL` +- wewnętrzny parametr `do` jest teraz wysyłany metodą POST jako `_do`, żeby uniknąć kolizji +- wewnętrzne zmienne z podkreśleniem, takie jak `$_form`, są przestarzałe +- pamiętaj o aktualizacji `netteForms.js` + + +Aktualizacja do wersji 2.3 +========================== + +- wewnętrzne metody filtrujące, takie jak `Nette\Forms\Controls\TextBase::filterFloat`, zostały usunięte +- wewnętrzne metody walidacyjne, takie jak `TextBase::validateFloat`, zostały przeniesione do `Nette\Forms\Validator`, podobnie jak `Rules::$defaultMessages` +- Buttony i pola Hidden są generowane bez HTML-owego ID; jeśli chcesz ID, ustaw je metodą `setHtmlId()` +- pozycje RadioList są również generowane bez ID; możesz je włączyć przez `$radioList->generateId = true` +- filtry dodane przez `TextBase::addFilter()` są przetwarzane podczas walidacji, a filtry możesz teraz dodawać do warunków: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/pl/validation.texy b/forms/pl/validation.texy index 3e58147ce0..c0be218214 100644 --- a/forms/pl/validation.texy +++ b/forms/pl/validation.texy @@ -5,136 +5,136 @@ Walidacja formularzy Elementy obowiązkowe ==================== -Elementy obowiązkowe oznaczamy metodą `setRequired()`, której argumentem jest tekst [komunikatu o błędzie |#Komunikaty o błędach], który wyświetli się, jeśli użytkownik nie wypełni elementu. Jeśli nie podamy argumentu, użyty zostanie domyślny komunikat o błędzie. +Elementy oznaczamy jako obowiązkowe metodą `setRequired()`. Jej argumentem jest tekst [komunikatu o błędzie |#Komunikaty o błędach], który wyświetli się, jeśli użytkownik elementu nie wypełni. Jeśli argumentu nie podamy, użyty zostanie domyślny komunikat o błędzie. ```php $form->addText('name', 'Imię:') - ->setRequired('Proszę podać imię'); + ->setRequired('Wypełnij swoje imię.'); ``` Reguły ====== -Reguły walidacji dodajemy do elementów metodą `addRule()`. Pierwszy parametr to reguła, drugi to tekst [komunikatu o błędzie |#Komunikaty o błędach], a trzeci to argument reguły walidacji. +Reguły walidacyjne dodajemy elementom metodą `addRule()`. Pierwszym parametrem jest reguła, drugim [komunikat o błędzie |#Komunikaty o błędach], a trzecim argument reguły walidacyjnej. ```php $form->addPassword('password', 'Hasło:') ->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8); ``` -**Reguły walidacji są sprawdzane tylko wtedy, gdy użytkownik wypełnił element.** +**Reguły walidacyjne sprawdzane są tylko wtedy, gdy użytkownik element wypełnił.** -Nette dostarcza całą gamę predefiniowanych reguł, których nazwy są stałymi klasy `Nette\Forms\Form`. Dla wszystkich elementów możemy użyć tych reguł: +Nette zawiera szereg predefiniowanych reguł, których nazwy są stałymi klasy `Nette\Forms\Form`. Te reguły możemy zastosować do wszystkich elementów: | stała | opis | typ argumentu |------- | `Required` | element obowiązkowy, alias dla `setRequired()` | - | `Filled` | element obowiązkowy, alias dla `setRequired()` | - | `Blank` | element nie może być wypełniony | - -| `Equal` | wartość jest równa parametrowi | `mixed` -| `NotEqual` | wartość nie jest równa parametrowi | `mixed` -| `IsIn` | wartość jest równa jednemu z elementów w tablicy | `array` -| `IsNotIn` | wartość nie jest równa żadnemu z elementów w tablicy | `array` -| `Valid` | czy element jest wypełniony poprawnie? (dla [#Warunki]) | - +| `Equal` | wartość musi być równa parametrowi | `mixed` +| `NotEqual` | wartość nie może być równa parametrowi | `mixed` +| `IsIn` | wartość musi być jedną z pozycji tablicy | `array` +| `IsNotIn` | wartość nie może być żadną z pozycji tablicy | `array` +| `Valid` | czy element jest poprawnie wypełniony? (tylko w [addConditionOn() |#Warunki]) | - -Wejścia tekstowe ----------------- +Inputy tekstowe +--------------- -Dla elementów `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` można użyć również niektórych z następujących reguł: +Dla elementów `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` można zastosować także niektóre z poniższych reguł: | `MinLength` | minimalna długość tekstu | `int` | `MaxLength` | maksymalna długość tekstu | `int` -| `Length` | długość w zakresie lub dokładna długość | para `[int, int]` lub `int` -| `Email` | prawidłowy adres e-mail | - +| `Length` | długość w przedziale albo dokładna długość | para `[int, int]` albo `int` +| `Email` | poprawny adres e-mail | - | `URL` | absolutny URL | - | `Pattern` | pasuje do wyrażenia regularnego | `string` -| `PatternInsensitive` | jak `Pattern`, ale niezależne od wielkości liter | `string` +| `PatternInsensitive` | jak `Pattern`, ale bez rozróżniania wielkości liter | `string` | `Integer` | wartość całkowita | - -| `Numeric` | alias dla `Integer` | - +| `Numeric` | nieujemna liczba całkowita (same cyfry) | - | `Float` | liczba | - -| `Min` | minimalna wartość elementu numerycznego | `int\|float` -| `Max` | maksymalna wartość elementu numerycznego | `int\|float` -| `Range` | wartość w zakresie | para `[int\|float, int\|float]` +| `Min` | minimalna wartość elementu liczbowego | `int\|float` +| `Max` | maksymalna wartość elementu liczbowego | `int\|float` +| `Range` | wartość w przedziale | para `[int\|float, int\|float]` -Reguły walidacji `Integer`, `Numeric` i `Float` od razu konwertują wartość na integer lub float. Ponadto reguła `URL` akceptuje również adres bez schematu (np. `nette.org`) i dodaje schemat (`https://nette.org`). Wyrażenie w `Pattern` i `PatternIcase` musi pasować do całej wartości, tzn. tak jakby było otoczone znakami `^` i `$`. +Reguły walidacyjne `Integer` i `Float` automatycznie konwertują wartość odpowiednio na liczbę całkowitą albo zmiennoprzecinkową. Ponadto reguła `URL` akceptuje także adres bez schematu (np. `nette.org`) i schemat uzupełnia (`https://nette.org`). Wyrażenie w `Pattern` i `PatternInsensitive` musi być poprawne dla całej wartości, czyli tak, jakby było otoczone znakami `^` i `$`. -Liczba elementów ----------------- +Liczba pozycji +-------------- -Dla elementów `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` można użyć również następujących reguł do ograniczenia liczby wybranych elementów lub przesłanych plików: +Dla elementów `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` możesz użyć także poniższych reguł ograniczających liczbę wybranych pozycji albo wysłanych plików: | `MinLength` | minimalna liczba | `int` | `MaxLength` | maksymalna liczba | `int` -| `Length` | liczba w zakresie lub dokładna liczba | para `[int, int]` lub `int` +| `Length` | liczba w przedziale albo dokładna liczba | para `[int, int]` albo `int` -Przesyłanie plików ------------------- +Wysyłanie plików +---------------- -Dla elementów `addUpload()`, `addMultiUpload()` można użyć również następujących reguł: +Dla elementów `addUpload()`, `addMultiUpload()` można użyć także poniższych reguł: | `MaxFileSize` | maksymalny rozmiar pliku w bajtach | `int` -| `MimeType` | Typ MIME, dozwolone symbole wieloznaczne (`'video/*'`) | `string\|string[]` -| `Image` | obraz JPEG, PNG, GIF, WebP, AVIF | - +| `MimeType` | typ MIME, dozwolone wildcardy (`'video/*'`) | `string\|string[]` +| `Image` | obrazek JPEG, PNG, GIF, WebP, AVIF | - | `Pattern` | nazwa pliku pasuje do wyrażenia regularnego | `string` -| `PatternInsensitive` | jak `Pattern`, ale niezależne od wielkości liter | `string` +| `PatternInsensitive` | jak `Pattern`, ale bez rozróżniania wielkości liter | `string` -`MimeType` i `Image` wymagają rozszerzenia PHP `fileinfo`. To, czy plik lub obraz jest wymaganego typu, wykrywają na podstawie jego sygnatury i **nie weryfikują integralności całego pliku.** Czy obraz nie jest uszkodzony, można sprawdzić na przykład próbując go [załadować |http:request#toImage]. +`MimeType` i `Image` wymagają rozszerzenia PHP `fileinfo`. To, czy plik albo obrazek jest wymaganego typu, wykrywane jest na podstawie jego sygnatury, a **integralność całego pliku nie jest sprawdzana.** To, czy obrazek nie jest uszkodzony, możesz ustalić na przykład, próbując go [wczytać |http:request#toImage()]. Komunikaty o błędach ==================== -Wszystkie predefiniowane reguły z wyjątkiem `Pattern` i `PatternInsensitive` mają domyślny komunikat o błędzie, więc można go pominąć. Jednak podanie i sformułowanie wszystkich komunikatów na miarę sprawi, że formularz będzie bardziej przyjazny dla użytkownika. +Wszystkie predefiniowane reguły oprócz `Pattern` i `PatternInsensitive` mają domyślny komunikat o błędzie, więc można go pominąć. Podając jednak i formułując wszystkie własne komunikaty dopasowane do swoich potrzeb, uczynisz formularz przyjaźniejszym dla użytkownika. -Zmienić domyślne komunikaty można w [konfiguracji|forms:configuration], edytując teksty w tablicy `Nette\Forms\Validator::$messages` lub używając [translatora |rendering#Tłumaczenie]. +Domyślne komunikaty możesz zmienić w [konfiguracji |forms:configuration], modyfikując teksty w tablicy `Nette\Forms\Validator::$messages`, albo za pomocą [translatora |rendering#Tłumaczenie]. -W tekście komunikatów o błędach można używać następujących symboli zastępczych: +W tekście komunikatów o błędach można używać poniższych zastępników: -| `%d` | zastępuje kolejno argumentami reguły -| `%n$d` | zastępuje n-tym argumentem reguły -| `%label` | zastępuje etykietą elementu (bez dwukropka) -| `%name` | zastępuje nazwą elementu (np. `name`) -| `%value` | zastępuje wartością wprowadzoną przez użytkownika +| `%d` | zastępowany kolejno argumentami reguły +| `%n$d` | zastępowany n-tym argumentem reguły +| `%label` | zastępowany etykietą elementu (bez dwukropka) +| `%name` | zastępowany nazwą elementu (np. `name`) +| `%value` | zastępowany wartością wpisaną przez użytkownika ```php $form->addText('name', 'Imię:') - ->setRequired('Proszę wypełnić %label'); + ->setRequired('Wypełnij %label'); $form->addInteger('id', 'ID:') - ->addRule($form::Range, 'co najmniej %d i co najwyżej %d', [5, 10]); + ->addRule($form::Range, 'co najmniej %d i najwyżej %d', [5, 10]); $form->addInteger('id', 'ID:') - ->addRule($form::Range, 'co najwyżej %2$d i co najmniej %1$d', [5, 10]); + ->addRule($form::Range, 'najwyżej %2$d i co najmniej %1$d', [5, 10]); ``` Warunki ======= -Oprócz reguł można dodawać również warunki. Zapisuje się je podobnie jak reguły, tylko zamiast `addRule()` używamy metody `addCondition()` i oczywiście nie podajemy żadnego komunikatu o błędzie (warunek tylko pyta): +Oprócz reguł można dodawać także warunki. Zapisuje się je podobnie jak reguły, ale zamiast `addRule()` używamy metody `addCondition()` i naturalnie nie podajemy komunikatu o błędzie (warunek tylko pyta): ```php $form->addPassword('password', 'Hasło:') - // jeśli hasło nie jest dłuższe niż 8 znaków + // jeśli długość hasła nie jest większa niż 8 ->addCondition($form::MaxLength, 8) // to musi zawierać cyfrę ->addRule($form::Pattern, 'Musi zawierać cyfrę', '.*[0-9].*'); ``` -Warunek można powiązać również z innym elementem niż aktualny za pomocą `addConditionOn()`. Jako pierwszy parametr podajemy referencję do elementu. W tym przykładzie e-mail będzie obowiązkowy tylko wtedy, gdy zaznaczy się checkbox (jego wartość będzie true): +Warunek można powiązać z innym elementem niż bieżący za pomocą `addConditionOn()`. Pierwszym parametrem jest odwołanie do elementu. W tym przykładzie e-mail będzie obowiązkowy tylko wtedy, gdy checkbox będzie zaznaczony (czyli jego wartość będzie true): ```php -$form->addCheckbox('newsletters', 'wysyłaj mi newslettery'); +$form->addCheckbox('newsletters', 'Wysyłaj mi newslettery'); -$form->addEmail('email', 'E-mail:') +$form->addEmail('email', 'Email:') // jeśli checkbox jest zaznaczony ->addConditionOn($form['newsletters'], $form::Equal, true) // to wymagaj e-maila - ->setRequired('Podaj adres e-mail'); + ->setRequired('Podaj swój adres e-mail'); ``` Z warunków można tworzyć złożone struktury za pomocą `elseCondition()` i `endCondition()`: @@ -142,7 +142,7 @@ Z warunków można tworzyć złożone struktury za pomocą `elseCondition()` i ` ```php $form->addText(/* ... */) ->addCondition(/* ... */) // jeśli pierwszy warunek jest spełniony - ->addConditionOn(/* ... */) // i drugi warunek na innym elemencie + ->addConditionOn(/* ... */) // i spełniony jest też drugi warunek na innym elemencie ->addRule(/* ... */) // wymagaj tej reguły ->elseCondition() // jeśli drugi warunek nie jest spełniony ->addRule(/* ... */) // wymagaj tych reguł @@ -151,34 +151,42 @@ $form->addText(/* ... */) ->addRule(/* ... */); ``` -W Nette można bardzo łatwo reagować na spełnienie lub niespełnienie warunku również po stronie JavaScriptu za pomocą metody `toggle()`, zobacz [#Dynamiczny JavaScript]. +Pierwszym argumentem `addCondition()` może być też wartość logiczna. Przydaje się to, gdy decyzja jest znana już w trakcie budowania formularza, na przykład żeby zastosować regułę tylko w określonych okolicznościach: + +```php +$form->addText('nickname') + ->addCondition($isRequired) // wartość znana przy budowaniu formularza + ->setRequired(); +``` + +W Nette bardzo łatwo zareagujesz na spełnienie albo niespełnienie warunku po stronie JavaScriptu metodą `toggle()`, patrz [#Dynamiczny JavaScript]. Odwołanie do innego elementu ============================ -Jako argument reguły lub warunku można przekazać również inny element formularza. Reguła następnie użyje wartości wprowadzonej później przez użytkownika w przeglądarce. W ten sposób można np. dynamicznie walidować, że element `password` zawiera ten sam ciąg znaków co element `password_confirm`: +Jako argument reguły albo warunku możesz przekazać także inny element formularza. Reguła użyje wtedy wartości wpisanej później przez użytkownika w przeglądarce. Można to wykorzystać na przykład do dynamicznego sprawdzenia, czy element `password` zawiera ten sam ciąg co element `password_confirm`: ```php $form->addPassword('password', 'Hasło'); $form->addPassword('password_confirm', 'Potwierdź hasło') - ->addRule($form::Equal, 'Podane hasła nie pasują', $form['password']); + ->addRule($form::Equal, 'Hasła nie są zgodne', $form['password']); ``` Własne reguły i warunki ======================= -Czasami dochodzimy do sytuacji, gdy wbudowane reguły walidacji w Nette nam nie wystarczają i potrzebujemy zweryfikować dane od użytkownika po swojemu. W Nette jest to bardzo proste! +Czasem napotykamy sytuacje, w których wbudowane reguły walidacyjne Nette nie wystarczają i potrzebujemy zwalidować dane użytkownika po swojemu. W Nette jest to bardzo proste! -Metodom `addRule()` lub `addCondition()` można jako pierwszy parametr przekazać dowolny callback. Przyjmuje on jako pierwszy parametr sam element i zwraca wartość boolean określającą, czy walidacja przebiegła pomyślnie. Przy dodawaniu reguły za pomocą `addRule()` można podać również dodatkowe argumenty, które są następnie przekazywane jako drugi parametr. +Metodom `addRule()` albo `addCondition()` możesz przekazać jako pierwszy parametr dowolny callback. Callback przyjmuje jako pierwszy parametr sam element i zwraca wartość logiczną mówiącą, czy walidacja się powiodła. Przy dodawaniu reguły metodą `addRule()` można podać kolejne argumenty, które są potem przekazywane jako drugi parametr. -Własny zestaw walidatorów możemy więc utworzyć jako klasę z metodami statycznymi: +Własny zestaw walidatorów można więc utworzyć jako klasę ze statycznymi metodami: ```php class MyValidators { - // testuje, czy wartość jest podzielna przez argument + // sprawdza, czy wartość jest podzielna przez argument public static function validateDivisibility(BaseControl $input, $arg): bool { return $input->getValue() % $arg === 0; @@ -186,23 +194,23 @@ class MyValidators public static function validateEmailDomain(BaseControl $input, $domain) { - // inne walidatory + // kolejne walidatory } } ``` -Użycie jest następnie bardzo proste: +Użycie jest potem bardzo proste: ```php $form->addInteger('num') ->addRule( [MyValidators::class, 'validateDivisibility'], - 'Wartość musi być wielokrotnością liczby %d', + 'Wartość musi być wielokrotnością %d', 8, ); ``` -Własne reguły walidacji można dodawać również do JavaScriptu. Warunkiem jest, aby reguła była metodą statyczną. Jej nazwa dla walidatora JavaScriptowego powstaje przez połączenie nazwy klasy bez ukośników wstecznych `\`, podkreślenia `_` i nazwy metody. Np. `App\MyValidators::validateDivisibility` zapiszemy jako `AppMyValidators_validateDivisibility` i dodamy do obiektu `Nette.validators`: +Własne reguły walidacyjne można dodać także do JavaScriptu. Warunkiem jest, żeby reguła była metodą statyczną. Jej nazwa dla walidatora JavaScriptowego powstaje przez połączenie nazwy klasy bez odwrotnych ukośników `\`, podkreślenia `_` i nazwy metody. Na przykład `App\MyValidators::validateDivisibility` zapisujemy jako `AppMyValidators_validateDivisibility` i dodajemy do obiektu `Nette.validators`: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -214,7 +222,7 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => Zdarzenie onValidate ==================== -Po wysłaniu formularza przeprowadzana jest walidacja, podczas której sprawdzane są poszczególne reguły dodane za pomocą `addRule()`, a następnie wywoływane jest [zdarzenie |nette:glossary#Eventy zdarzenia] `onValidate`. Jego handler można wykorzystać do dodatkowej walidacji, typowo sprawdzenia poprawnej kombinacji wartości w wielu elementach formularza. +Po wysłaniu formularza przeprowadzana jest walidacja sprawdzająca poszczególne reguły dodane metodą `addRule()`, a następnie wywoływane jest [zdarzenie |nette:glossary#Zdarzenia] `onValidate`. Jego handler można wykorzystać do dodatkowej walidacji, typowo do sprawdzenia poprawnej kombinacji wartości w kilku elementach formularza. Jeśli zostanie wykryty błąd, przekazujemy go do formularza metodą `addError()`. Można ją wywołać albo na konkretnym elemencie, albo bezpośrednio na formularzu. @@ -223,11 +231,11 @@ protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('Ta kombinacja nie jest możliwa.'); @@ -236,10 +244,10 @@ public function validateSignInForm(Form $form, \stdClass $data): void ``` -Błędy podczas przetwarzania -=========================== +Błędy przy przetwarzaniu +======================== -W wielu przypadkach o błędzie dowiadujemy się dopiero w momencie, gdy przetwarzamy poprawny formularz, na przykład zapisujemy nową pozycję do bazy danych i napotykamy na duplikat kluczy. W takim przypadku błąd ponownie przekazujemy do formularza metodą `addError()`. Można ją wywołać albo na konkretnym elemencie, albo bezpośrednio na formularzu: +W wielu przypadkach o błędzie dowiadujemy się dopiero przy przetwarzaniu poprawnego formularza, na przykład gdy zapisujemy nowy wpis do bazy danych i natrafiamy na zduplikowany klucz. W takim przypadku znów przekazujemy błąd z powrotem do formularza metodą `addError()`. Można ją wywołać albo na konkretnym elemencie, albo bezpośrednio na formularzu: ```php try { @@ -254,77 +262,83 @@ try { } ``` -Jeśli to możliwe, zalecamy dołączenie błędu bezpośrednio do elementu formularza, ponieważ zostanie on wyświetlony obok niego przy użyciu domyślnego renderera. +Jeśli to możliwe, zalecamy dodanie błędu bezpośrednio do elementu formularza, bo przy użyciu domyślnego renderera wyświetli się on obok niego. ```php -$form['date']->addError('Przepraszamy, ale ta data jest już zajęta.'); +$form['date']->addError('Przepraszamy, ten termin jest już zajęty.'); ``` -Możesz wywoływać `addError()` wielokrotnie i w ten sposób przekazać formularzowi lub elementowi więcej komunikatów o błędach. Uzyskasz je za pomocą `getErrors()`. +`addError()` możesz wywołać wielokrotnie, żeby przekazać formularzowi albo elementowi kilka komunikatów o błędach. Odczytasz je metodą `getErrors()`. -Uwaga, `$form->getErrors()` zwraca podsumowanie wszystkich komunikatów o błędach, również tych, które zostały przekazane bezpośrednio poszczególnym elementom, a nie tylko bezpośrednio formularzowi. Komunikaty o błędach przekazane tylko formularzowi uzyskasz przez `$form->getOwnErrors()`. +Uwaga: `$form->getErrors()` zwraca podsumowanie wszystkich komunikatów o błędach, także tych przekazanych bezpośrednio poszczególnym elementom, nie tylko tych przekazanych bezpośrednio formularzowi. Komunikaty o błędach przekazane tylko formularzowi odczytasz przez `$form->getOwnErrors()`. -Modyfikacja danych wejściowych +Modyfikacja wpisanych wartości ============================== -Za pomocą metody `addFilter()` możemy zmodyfikować wartość wprowadzoną przez użytkownika. W tym przykładzie będziemy tolerować i usuwać spacje w kodzie pocztowym: +Metodą `addFilter()` możemy zmodyfikować wartość wpisaną przez użytkownika. W tym przykładzie będziemy tolerować i usuwać spacje w kodzie pocztowym: ```php $form->addText('zip', 'Kod pocztowy:') ->addFilter(function ($value) { return str_replace(' ', '', $value); // usuwamy spacje z kodu pocztowego }) - ->addRule($form::Pattern, 'Kod pocztowy nie ma formatu pięciu cyfr', '\d{5}'); + ->addRule($form::Pattern, 'Kod pocztowy nie ma pięciu cyfr', '\d{5}'); ``` -Filtr jest włączany między reguły walidacji i warunki, a zatem zależy od kolejności metod, tzn. filtr i reguła są wywoływane w takiej kolejności, jak kolejność metod `addFilter()` i `addRule()`. +Filtr włącza się między reguły walidacyjne i warunki, a więc kolejność metod ma znaczenie, czyli filtr i reguła wywoływane są w tej samej kolejności, w jakiej podane są metody `addFilter()` i `addRule()`. -Walidacja JavaScript -==================== +Walidacja w JavaScripcie +======================== -Język do formułowania warunków i reguł jest bardzo potężny. Wszystkie konstrukcje działają zarówno po stronie serwera, jak i po stronie JavaScriptu. Są one przekazywane w atrybutach HTML `data-nette-rules` jako JSON. Samą walidację przeprowadza następnie skrypt, który przechwytuje zdarzenie formularza `submit`, przechodzi przez poszczególne elementy i wykonuje odpowiednią walidację. +Język formułowania warunków i reguł jest bardzo mocny. Wszystkie konstrukcje działają zarówno po stronie serwera, jak i po stronie klienta w JavaScripcie. Przenoszone są w atrybutach HTML `data-nette-rules` jako JSON. Samą walidacją zajmuje się skrypt, który przechwytuje zdarzenie `submit` formularza, przechodzi przez poszczególne elementy i przeprowadza odpowiednią walidację. -Tym skryptem jest `netteForms.js` i jest dostępny z wielu możliwych źródeł: +Tym skryptem jest `netteForms.js` i jest dostępny z kilku możliwych źródeł: -Skrypt można wstawić bezpośrednio na stronę HTML z CDN: +Skrypt możesz wstawić bezpośrednio na stronę HTML z CDN: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Lub skopiować lokalnie do publicznego folderu projektu (np. z `vendor/nette/forms/src/assets/netteForms.min.js`): +Albo skopiować lokalnie do publicznego folderu projektu (np. z `vendor/nette/forms/src/assets/netteForms.min.js`): ```latte <script src="/path/to/netteForms.min.js"></script> ``` -Lub zainstalować przez [npm|https://www.npmjs.com/package/nette-forms]: +Albo zainstalować przez [npm |https://www.npmjs.com/package/nette-forms]: ```shell npm install nette-forms ``` -A następnie załadować i uruchomić: +A następnie wczytać i uruchomić: ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -Alternatywnie można go załadować bezpośrednio z folderu `vendor`: +Alternatywnie możesz wczytać go bezpośrednio z folderu `vendor`: ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Walidację po stronie klienta możesz całkowicie wyłączyć, dodając formularzowi atrybut `novalidate`. Skrypt `netteForms.js` pominie wtedy jego walidację przy wysyłaniu, więc walidacja odbędzie się tylko na serwerze: + +```php +$form->setHtmlAttribute('novalidate'); +``` + Dynamiczny JavaScript ===================== -Chcesz wyświetlić pola do wprowadzenia adresu tylko wtedy, gdy użytkownik wybierze wysyłkę towaru pocztą? Żaden problem. Kluczem jest para metod `addCondition()` & `toggle()`: +Chcesz wyświetlić pola adresu tylko wtedy, gdy użytkownik wybierze wysyłkę towaru pocztą? Nie ma problemu. Kluczem jest para metod `addCondition()` i `toggle()`: ```php $form->addCheckbox('send_it') @@ -332,16 +346,16 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -Ten kod mówi, że gdy warunek jest spełniony, czyli gdy checkbox jest zaznaczony, widoczny będzie element HTML `#address-container`. I odwrotnie. Elementy formularza z adresem odbiorcy umieścimy więc w kontenerze o tym ID, a po kliknięciu na checkbox ukryją się lub pokażą. Zapewnia to skrypt `netteForms.js`. +Ten kod mówi, że gdy warunek zostanie spełniony (czyli gdy checkbox będzie zaznaczony), element HTML `#address-container` będzie widoczny, i odwrotnie. Elementy formularza z adresem odbiorcy umieścimy więc w kontenerze o tym ID, a będą się ukrywać i pokazywać po kliknięciu w checkbox. Zajmuje się tym skrypt `netteForms.js`. -Jako argument metody `toggle()` można przekazać dowolny selektor. Z historycznych powodów alfanumeryczny ciąg znaków bez dodatkowych znaków specjalnych jest traktowany jako ID elementu, czyli tak samo, jakby poprzedzał go znak `#`. Drugi, opcjonalny parametr pozwala odwrócić zachowanie, tzn. gdybyśmy użyli `toggle('#address-container', false)`, element zostałby wyświetlony tylko wtedy, gdy checkbox nie byłby zaznaczony. +Jako argument metody `toggle()` można przekazać dowolny selektor. Ze względów historycznych ciąg, który zaczyna się literą, cyfrą albo podkreśleniem i zawiera tylko litery, cyfry, podkreślenia, myślniki, kropki i dwukropki, traktowany jest jako ID elementu, tak jakby poprzedzał go znak `#`. Drugi, opcjonalny parametr pozwala odwrócić zachowanie; gdybyśmy na przykład użyli `toggle('#address-container', false)`, element wyświetlałby się tylko wtedy, gdyby checkbox *nie* był zaznaczony. -Domyślna implementacja w JavaScript zmienia właściwość `hidden` elementów. Zachowanie można jednak łatwo zmienić, na przykład dodać animację. Wystarczy w JavaScript nadpisać metodę `Nette.toggle` własnym rozwiązaniem: +Domyślna implementacja JavaScriptowa zmienia właściwość `hidden` elementów. Zachowanie możemy jednak łatwo zmienić, na przykład dodając animację. Wystarczy nadpisać w JavaScripcie metodę `Nette.toggle` własnym rozwiązaniem: ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // ukrywamy lub pokazujemy 'el' zgodnie z wartością 'visible' + // ukryj albo pokaż 'el' zależnie od wartości 'visible' }); }; ``` @@ -350,7 +364,7 @@ Nette.toggle = (selector, visible, srcElement, event) => { Wyłączenie walidacji ==================== -Czasami może się przydać wyłączenie walidacji. Jeśli naciśnięcie przycisku wysyłającego nie ma przeprowadzać walidacji (odpowiednie dla przycisków *Anuluj* lub *Podgląd*), wyłączymy ją metodą `$submit->setValidationScope([])`. Jeśli ma przeprowadzać tylko częściową walidację, możemy określić, które pola lub kontenery formularza mają być walidowane. +Czasem może się przydać wyłączenie walidacji. Jeśli naciśnięcie przycisku wysyłającego nie ma przeprowadzać walidacji (odpowiednie dla przycisków *Anuluj* albo *Podgląd*), wyłączymy ją metodą `$submit->setValidationScope([])`. Jeśli ma przeprowadzać walidację tylko częściową, możemy określić, które pola albo kontenery formularza mają być walidowane. ```php $form->addText('name') @@ -358,19 +372,21 @@ $form->addText('name') $details = $form->addContainer('details'); $details->addInteger('age') - ->setRequired('wiek'); + ->setRequired('age'); $details->addInteger('age2') - ->setRequired('wiek2'); + ->setRequired('age2'); -$form->addSubmit('send1'); // Waliduje cały formularz +$form->addSubmit('send1'); // waliduje cały formularz $form->addSubmit('send2') - ->setValidationScope([]); // Nie waliduje wcale + ->setValidationScope([]); // nie waliduje nic $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Waliduje tylko element name + ->setValidationScope([$form['name']]); // waliduje tylko element 'name' $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Waliduje tylko element age + ->setValidationScope([$form['details']['age']]); // waliduje tylko element 'age' $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Waliduje kontener details + ->setValidationScope([$form['details']]); // waliduje kontener 'details' ``` -`setValidationScope` nie wpływa na [#zdarzenie onValidate] formularza, które zostanie wywołane zawsze. Zdarzenie `onValidate` kontenera zostanie wywołane tylko wtedy, gdy ten kontener jest oznaczony do częściowej walidacji. +`setValidationScope` nie wpływa na [#Zdarzenie onValidate] na formularzu, które będzie wywoływane zawsze. Zdarzenie `onValidate` na kontenerze zostanie wywołane tylko wtedy, gdy ten kontener jest oznaczony do walidacji częściowej. + +Walidacja częściowa wpływa też na wartości zwracane przez `getValues()`: wynik zawiera tylko wartości elementów mieszczących się w zakresie walidacji. Wartości elementów spoza tego zakresu są pomijane. diff --git a/forms/pt/@home.texy b/forms/pt/@home.texy deleted file mode 100644 index 6a9f3c2857..0000000000 --- a/forms/pt/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms revolucionou a criação de formulários web. De repente, bastava escrever algumas linhas de código compreensíveis e você tinha um formulário completo, incluindo renderização, validação JavaScript e do lado do servidor, e além disso, extremamente seguro. Mostraremos como - -- criar formulários amigáveis -- validar os dados enviados -- renderizar elementos exatamente conforme necessário - -</div> - - -Ao usar Nette Forms, você evitará uma série de tarefas rotineiras, como escrever validação (além disso, dupla, no lado do servidor e do cliente), minimizará a probabilidade de erros e falhas de segurança. - -Você pode usar formulários como parte da Aplicação Nette (ou seja, em presenters) ou de forma completamente independente. Como o uso difere um pouco em ambos os casos, preparamos dois guias para você: - -<div class="wiki-buttons"> -<div> "Formulários em presenters .[wiki-button]":in-presenter </div> -<div> "Formulários independentes .[wiki-button]":standalone </div> -</div> - - -Instalação ----------- - -Faça o download e instale a biblioteca usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/pt/@left-menu.texy b/forms/pt/@left-menu.texy deleted file mode 100644 index 9cf4dd5b9b..0000000000 --- a/forms/pt/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Introdução |@home] -- [Formulários em presenters|in-presenter] -- [Formulários independentes|standalone] -- [Elementos de formulário |controls] -- [Validação |validation] -- [Renderização |rendering] -- [Configuração |configuration] - - -Leitura adicional -***************** -- [Guias e melhores práticas |best-practices:] diff --git a/forms/pt/@meta.texy b/forms/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/forms/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/forms/pt/configuration.texy b/forms/pt/configuration.texy deleted file mode 100644 index e3538aa4f6..0000000000 --- a/forms/pt/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Configuração de Formulários -*************************** - -.[perex] -Na configuração, é possível alterar as [mensagens de erro padrão dos formulários|validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Aqui está a tradução para o português: - -```neon -forms: - messages: - Equal: 'Por favor, insira %s.' - NotEqual: 'Este valor não deve ser %s.' - Filled: 'Este campo é obrigatório.' - Blank: 'Este campo deve estar em branco.' - MinLength: 'Por favor, insira pelo menos %d caracteres.' - MaxLength: 'Por favor, insira no máximo %d caracteres.' - Length: 'Por favor, insira um valor entre %d e %d caracteres.' - Email: 'Por favor, insira um endereço de e-mail válido.' - URL: 'Por favor, insira uma URL válida.' - Integer: 'Por favor, insira um número inteiro válido.' - Float: 'Por favor, insira um número válido.' - Min: 'Por favor, insira um valor maior ou igual a %d.' - Max: 'Por favor, insira um valor menor ou igual a %d.' - Range: 'Por favor, insira um valor entre %d e %d.' - MaxFileSize: 'O tamanho do arquivo enviado pode ser de no máximo %d bytes.' - MaxPostSize: 'Os dados enviados excedem o limite de %d bytes.' - MimeType: 'O arquivo enviado não está no formato esperado.' - Image: 'O arquivo enviado deve ser uma imagem no formato JPEG, GIF, PNG, WebP ou AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Por favor, selecione uma opção válida.' - Nette\Forms\Controls\UploadControl::Valid: 'Ocorreu um erro durante o upload do arquivo.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Sua sessão expirou. Por favor, retorne à página inicial e tente novamente.' -``` - -Se você não usa o framework completo e, portanto, nem os arquivos de configuração, pode alterar as mensagens de erro padrão diretamente no array `Nette\Forms\Validator::$messages`. diff --git a/forms/pt/controls.texy b/forms/pt/controls.texy deleted file mode 100644 index 4b7b77ebbf..0000000000 --- a/forms/pt/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Elementos de Formulário -*********************** - -.[perex] -Visão geral dos elementos de formulário padrão. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Adiciona um campo de texto de linha única (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se o usuário não preencher o campo, retorna uma string vazia `''`, ou usando `setNullable()` pode-se especificar que retorne `null`. - -```php -$form->addText('name', 'Nome:') - ->setRequired() - ->setNullable(); -``` - -Valida automaticamente UTF-8, remove espaços à esquerda e à direita e remove quebras de linha que um invasor poderia enviar. - -O comprimento máximo pode ser limitado usando `setMaxLength()`. Modificar o valor inserido pelo usuário é possível com [addFilter() |validation#Modificação da entrada]. - -Usando `setHtmlType()`, é possível alterar o caractere visual do campo de texto para tipos como `search`, `tel` ou `url`, veja a [especificação|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Lembre-se que a alteração do tipo é apenas visual e não substitui a função de validação. Para o tipo `url`, é apropriado adicionar uma [regra URL |validation#Entradas de texto] de validação específica. - -.[note] -Para outros tipos de entrada, como `number`, `range`, `email`, `date`, `datetime-local`, `time` e `color`, use métodos especializados como [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] e [#addColor], que garantem a validação do lado do servidor. Os tipos `month` e `week` ainda não são totalmente suportados em todos os navegadores. - -Ao elemento pode ser definido o chamado empty-value, que é algo como um valor padrão, mas se o usuário não o alterar, o elemento retorna uma string vazia ou `null`. - -```php -$form->addText('phone', 'Telefone:') - ->setHtmlType('tel') - ->setEmptyValue('+55'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Adiciona um campo para inserir texto multilinha (classe [TextArea |api:Nette\Forms\Controls\TextArea]). Se o usuário não preencher o campo, retorna uma string vazia `''`, ou usando `setNullable()` pode-se especificar que retorne `null`. - -```php -$form->addTextArea('note', 'Nota:') - ->addRule($form::MaxLength, 'A nota é muito longa', 10000); -``` - -Valida automaticamente UTF-8 e normaliza os separadores de linha para `\n`. Ao contrário do campo de entrada de linha única, não ocorre remoção de espaços. - -O comprimento máximo pode ser limitado usando `setMaxLength()`. Modificar o valor inserido pelo usuário é possível com [addFilter() |validation#Modificação da entrada]. É possível definir o chamado empty-value usando `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Adiciona um campo para inserir um número inteiro (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Retorna um inteiro ou `null` se o usuário não inserir nada. - -```php -$form->addInteger('year', 'Ano:') - ->addRule($form::Range, 'O ano deve estar no intervalo de %d a %d.', [1900, 2023]); -``` - -O elemento é renderizado como `<input type="number">`. Usando o método `setHtmlType()`, é possível alterar o tipo para `range` para exibição na forma de um controle deslizante, ou para `text`, se preferir um campo de texto padrão sem o comportamento especial do tipo `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Adiciona um campo para inserir um número decimal (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Retorna um float ou `null` se o usuário não inserir nada. - -```php -$form->addFloat('level', 'Nível:') - ->setDefaultValue(0) - ->addRule($form::Range, 'O nível deve estar no intervalo de %d a %d.', [0, 100]); -``` - -O elemento é renderizado como `<input type="number">`. Usando o método `setHtmlType()`, é possível alterar o tipo para `range` para exibição na forma de um controle deslizante, ou para `text`, se preferir um campo de texto padrão sem o comportamento especial do tipo `number`. - -Nette e o navegador Chrome aceitam tanto vírgula quanto ponto como separador decimal. Para que essa funcionalidade esteja disponível também no Firefox, é recomendado definir o atributo `lang` para o elemento específico ou para a página inteira, por exemplo, `<html lang="pt-BR">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Adiciona um campo para inserir um endereço de e-mail (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se o usuário não preencher o campo, retorna uma string vazia `''`, ou usando `setNullable()` pode-se especificar que retorne `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Verifica se o valor é um endereço de e-mail válido. Não verifica se o domínio realmente existe, apenas a sintaxe é verificada. Valida automaticamente UTF-8, remove espaços à esquerda e à direita. - -O comprimento máximo pode ser limitado usando `setMaxLength()`. Modificar o valor inserido pelo usuário é possível com [addFilter() |validation#Modificação da entrada]. É possível definir o chamado empty-value usando `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Adiciona um campo para inserir uma senha (classe [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Senha:') - ->setRequired() - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8) - ->addRule($form::Pattern, 'Deve conter um dígito', '.*[0-9].*'); -``` - -Ao reexibir o formulário, o campo estará vazio. Valida automaticamente UTF-8, remove espaços à esquerda e à direita e remove quebras de linha que um invasor poderia enviar. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Adiciona uma caixa de seleção (classe [Checkbox |api:Nette\Forms\Controls\Checkbox]). Retorna o valor `true` ou `false`, dependendo se está marcada. - -```php -$form->addCheckbox('agree', 'Concordo com os termos') - ->setRequired('É necessário concordar com os termos'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Adiciona caixas de seleção para escolher vários itens (classe [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Retorna um array das chaves dos itens selecionados. O método `getSelectedItems()` retorna os valores em vez das chaves. - -```php -$form->addCheckboxList('colors', 'Cores:', [ - 'r' => 'vermelho', - 'g' => 'verde', - 'b' => 'azul', -]); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. - -Usando `setDisabled(['r', 'g'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se os itens selecionados estão realmente entre os oferecidos e não foram desativados. O método `getRawValue()` permite obter os itens enviados sem essa importante verificação. - -Ao definir os itens selecionados padrão, também verifica se eles estão entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - -Se você enviar o formulário pelo método `GET`, pode escolher um método de transmissão de dados mais compacto, que economiza o tamanho da query string. Ele é ativado definindo o atributo HTML do formulário: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Adiciona botões de opção (classe [RadioList |api:Nette\Forms\Controls\RadioList]). Retorna a chave do item selecionado, ou `null` se o usuário não selecionou nada. O método `getSelectedItem()` retorna o valor em vez da chave. - -```php -$sex = [ - 'm' => 'masculino', - 'f' => 'feminino', -]; -$form->addRadioList('gender', 'Sexo:', $sex); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. - -Usando `setDisabled(['m', 'f'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se o item selecionado está realmente entre os oferecidos e não foi desativado. O método `getRawValue()` permite obter o item enviado sem essa importante verificação. - -Ao definir o item selecionado padrão, também verifica se ele está entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Adiciona uma caixa de seleção (classe [SelectBox |api:Nette\Forms\Controls\SelectBox]). Retorna a chave do item selecionado, ou `null` se o usuário não selecionou nada. O método `getSelectedItem()` retorna o valor em vez da chave. - -```php -$countries = [ - 'BR' => 'Brasil', - 'PT' => 'Portugal', - 'GB' => 'Reino Unido', -]; - -$form->addSelect('country', 'País:', $countries) - ->setDefaultValue('BR'); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. Os itens também podem ser um array bidimensional: - -```php -$countries = [ - 'Europa' => [ - 'CZ' => 'República Tcheca', - 'SK' => 'Eslováquia', - 'GB' => 'Reino Unido', - ], - 'CA' => 'Canadá', - 'US' => 'EUA', - '?' => 'outro', -]; -``` - -Nas caixas de seleção, o primeiro item geralmente tem um significado especial, servindo como um prompt para ação. Para adicionar tal item, use o método `setPrompt()`. - -```php -$form->addSelect('country', 'País:', $countries) - ->setPrompt('Escolha um país'); -``` - -Usando `setDisabled(['CZ', 'SK'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se o item selecionado está realmente entre os oferecidos e não foi desativado. O método `getRawValue()` permite obter o item enviado sem essa importante verificação. - -Ao definir o item selecionado padrão, também verifica se ele está entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Adiciona uma caixa de seleção para escolher vários itens (classe [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Retorna um array das chaves dos itens selecionados. O método `getSelectedItems()` retorna os valores em vez das chaves. - -```php -$form->addMultiSelect('countries', 'Países:', $countries); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. Os itens também podem ser um array bidimensional. - -Usando `setDisabled(['CZ', 'SK'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se os itens selecionados estão realmente entre os oferecidos e não foram desativados. O método `getRawValue()` permite obter os itens enviados sem essa importante verificação. - -Ao definir os itens selecionados padrão, também verifica se eles estão entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Adiciona um campo para upload de arquivo (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Retorna um objeto [FileUpload |http:request#FileUpload], mesmo que o usuário não tenha enviado nenhum arquivo, o que pode ser verificado pelo método `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'O avatar deve ser JPEG, PNG, GIF, WebP ou AVIF.') - ->addRule($form::MaxFileSize, 'O tamanho máximo é 1 MB.', 1024 * 1024 /* 1 MB em bytes */); -``` - -Se o arquivo não for carregado corretamente, o formulário não é enviado com sucesso e um erro é exibido. Ou seja, em caso de envio bem-sucedido, não é necessário verificar o método `FileUpload::isOk()`. - -Nunca confie no nome original do arquivo retornado pelo método `FileUpload::getName()`, o cliente pode ter enviado um nome de arquivo malicioso com a intenção de danificar ou hackear sua aplicação. - -As regras `MimeType` e `Image` detectam o tipo necessário com base na assinatura do arquivo e não verificam sua integridade. Se a imagem está danificada pode ser verificado, por exemplo, tentando [carregá-la |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Adiciona um campo para upload de vários arquivos de uma vez (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Retorna um array de objetos [FileUpload |http:request#FileUpload]. O método `FileUpload::hasFile()` em cada um deles retornará `true`. - -```php -$form->addMultiUpload('files', 'Arquivos:') - ->addRule($form::MaxLength, 'No máximo %d arquivos podem ser enviados', 10); -``` - -Se algum arquivo não for carregado corretamente, o formulário não é enviado com sucesso e um erro é exibido. Ou seja, em caso de envio bem-sucedido, não é necessário verificar o método `FileUpload::isOk()`. - -Nunca confie nos nomes originais dos arquivos retornados pelo método `FileUpload::getName()`, o cliente pode ter enviado um nome de arquivo malicioso com a intenção de danificar ou hackear sua aplicação. - -As regras `MimeType` e `Image` detectam o tipo necessário com base na assinatura do arquivo e não verificam sua integridade. Se a imagem está danificada pode ser verificado, por exemplo, tentando [carregá-la |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Adiciona um campo que permite ao usuário inserir facilmente uma data composta por ano, mês e dia (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Como valor padrão, aceita objetos que implementam a interface `DateTimeInterface`, uma string com a hora, ou um número representando um timestamp UNIX. O mesmo se aplica aos argumentos das regras `Min`, `Max` ou `Range`, que definem a data mínima e máxima permitida. - -```php -$form->addDate('date', 'Data:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'A data deve ter pelo menos um mês.', new DateTime('-1 month')); -``` - -Por padrão, retorna um objeto `DateTimeImmutable`, com o método `setFormat()` você pode especificar o [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou timestamp: - -```php -$form->addDate('date', 'Data:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Adiciona um campo que permite ao usuário inserir facilmente uma hora composta por horas, minutos e opcionalmente segundos (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Como valor padrão, aceita objetos que implementam a interface `DateTimeInterface`, uma string com a hora, ou um número representando um timestamp UNIX. Desses inputs, apenas a informação de tempo é utilizada, a data é ignorada. O mesmo se aplica aos argumentos das regras `Min`, `Max` ou `Range`, que definem a hora mínima e máxima permitida. Se o valor mínimo definido for maior que o máximo, cria-se um intervalo de tempo que ultrapassa a meia-noite. - -```php -$form->addTime('time', 'Hora:', withSeconds: true) - ->addRule($form::Range, 'A hora deve estar no intervalo de %d a %d.', ['12:30', '13:30']); -``` - -Por padrão, retorna um objeto `DateTimeImmutable` (com a data de 1º de janeiro do ano 1), com o método `setFormat()` você pode especificar o [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Hora:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Adiciona um campo que permite ao usuário inserir facilmente data e hora compostas por ano, mês, dia, horas, minutos e opcionalmente segundos (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Como valor padrão, aceita objetos que implementam a interface `DateTimeInterface`, uma string com a hora, ou um número representando um timestamp UNIX. O mesmo se aplica aos argumentos das regras `Min`, `Max` ou `Range`, que definem a data mínima e máxima permitida. - -```php -$form->addDateTime('datetime', 'Data e hora:') - ->setDefaultValue(new \DateTime) - ->addRule($form::Min, 'A data deve ter pelo menos um mês.', new \DateTime('-1 month')); -``` - -Por padrão, retorna um objeto `DateTimeImmutable`, com o método `setFormat()` você pode especificar o [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Adiciona um campo para seleção de cor (classe [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). A cor é uma string no formato `#rrggbb`. Se o usuário não fizer a escolha, retorna a cor preta `#000000`. - -```php -$form->addColor('color', 'Cor:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Adiciona um campo oculto (classe [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Usando `setNullable()`, pode-se definir que retorne `null` em vez de uma string vazia. Modificar o valor enviado é possível com [addFilter() |validation#Modificação da entrada]. - -Embora o elemento esteja oculto, é **importante notar** que o valor ainda pode ser modificado ou falsificado por um invasor. Sempre verifique e valide cuidadosamente todos os valores recebidos no lado do servidor para evitar riscos de segurança associados à manipulação de dados. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Adiciona um botão de envio (classe [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Enviar'); -``` - -No formulário, é possível ter vários botões de envio: - -```php -$form->addSubmit('register', 'Registrar'); -$form->addSubmit('cancel', 'Cancelar'); -``` - -Para descobrir qual deles foi clicado, use: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Se você não quiser validar o formulário inteiro ao pressionar o botão (por exemplo, para botões *Cancelar* ou *Visualizar*), use [setValidationScope() |validation#Desativação da validação]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Adiciona um botão (classe [Button |api:Nette\Forms\Controls\Button]) que não tem função de envio. Pode ser usado para alguma outra função, por exemplo, chamar uma função JavaScript ao clicar. - -```php -$form->addButton('raise', 'Aumentar salário') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Adiciona um botão de envio na forma de uma imagem (classe [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -Ao usar vários botões de envio, é possível descobrir qual foi clicado usando `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Adiciona um subformulário (classe [Container|api:Nette\Forms\Container]), ou seja, um contêiner, ao qual é possível adicionar outros elementos da mesma forma que os adicionamos ao formulário. Os métodos `setDefaults()` ou `getValues()` também funcionam. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Seu nome:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Seu nome:'); -$sub2->addEmail('email', 'Email:'); -``` - -Os dados enviados retornam como uma estrutura multidimensional: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Visão geral das configurações -============================= - -Para todos os elementos, podemos chamar os seguintes métodos (visão geral completa na [documentação da API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | define o valor padrão -| `getValue()` | obtém o valor atual -| `setOmitted()` | [#Omissão de valor] -| `setDisabled()` | [#Desativação de elementos] - -Renderização: -.[table-form-methods language-php] -| `setCaption($caption)` | altera o rótulo do elemento -| `setTranslator($translator)` | define o [tradutor |rendering#Tradução] -| `setHtmlAttribute($name, $value)` | define o [atributo HTML |rendering#Atributos HTML] do elemento -| `setHtmlId($id)` | define o atributo HTML `id` -| `setHtmlType($type)` | define o atributo HTML `type` -| `setHtmlName($name)` | define o atributo HTML `name` -| `setOption($key, $value)` | [configurações para renderização |rendering#Options] - -Validação: -.[table-form-methods language-php] -| `setRequired()` | [elemento obrigatório |validation] -| `addRule()` | define a [regra de validação |validation#Regras] -| `addCondition()`, `addConditionOn()` | define a [condição de validação |validation#Condições] -| `addError($message)` | [passagem de mensagem de erro |validation#Erros durante o processamento] - -Para os elementos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, podem ser chamados os seguintes métodos: - -.[table-form-methods language-php] -| `setNullable()` | define se getValue() retorna `null` em vez de string vazia -| `setEmptyValue($value)` | define um valor especial que é considerado uma string vazia -| `setMaxLength($length)` | define o número máximo de caracteres permitidos -| `addFilter($filter)` | [modificação da entrada |validation#Modificação da entrada] - - -Omissão de valor -================ - -Se o valor preenchido pelo usuário não nos interessa, podemos omiti-lo do resultado do método `$form->getValues()` ou dos dados passados para os handlers usando `setOmitted()`. Isso é útil para várias senhas de verificação, elementos antispam, etc. - -```php -$form->addPassword('passwordVerify', 'Senha para verificação:') - ->setRequired('Por favor, digite a senha novamente para verificação') - ->addRule($form::Equal, 'As senhas não coincidem', $form['password']) - ->setOmitted(); -``` - - -Desativação de elementos -======================== - -Elementos podem ser desativados usando `setDisabled()`. Tal elemento não pode ser editado pelo usuário. - -```php -$form->addText('username', 'Nome de usuário:') - ->setDisabled(); -``` - -Elementos desativados não são enviados pelo navegador para o servidor, portanto, você não os encontrará nos dados retornados pela função `$form->getValues()`. No entanto, se você definir `setOmitted(false)`, o Nette incluirá seu valor padrão nesses dados. - -Ao chamar `setDisabled()`, por razões de segurança, **o valor do elemento é apagado**. Se você estiver definindo um valor padrão, é necessário fazê-lo após desativá-lo: - -```php -$form->addText('username', 'Nome de usuário:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Uma alternativa aos elementos desativados são elementos com o atributo HTML `readonly`, que o navegador envia para o servidor. Embora o elemento seja apenas para leitura, é **importante notar** que seu valor ainda pode ser modificado ou falsificado por um invasor. - - -Elementos personalizados -======================== - -Além da ampla gama de elementos de formulário embutidos, você pode adicionar elementos personalizados ao formulário desta forma: - -```php -$form->addComponent(new DateInput('Data:'), 'date'); -// sintaxe alternativa: $form['date'] = new DateInput('Data:'); -``` - -.[note] -O formulário é um descendente da classe [Container |component-model:#Container] e os elementos individuais são descendentes de [Component |component-model:#Component]. - -Existe uma maneira de definir novos métodos de formulário para adicionar elementos personalizados (por exemplo, `$form->addZip()`). São os chamados métodos de extensão. A desvantagem é que a sugestão nos editores não funcionará para eles. - -```php -use Nette\Forms\Container; - -// adicionamos o método addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Pelo menos 5 números', '[0-9]{5}'); -}); - -// uso -$form->addZip('zip', 'CEP:'); -``` - - -Elementos de baixo nível -======================== - -Também é possível usar elementos que escrevemos apenas no template e não os adicionamos ao formulário com algum dos métodos `$form->addXyz()`. Por exemplo, ao listar registros do banco de dados sem saber antecipadamente quantos serão e quais serão seus IDs, e queremos exibir uma caixa de seleção ou botão de opção para cada linha, basta codificá-lo no template: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -E após o envio, obtemos o valor: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -onde o primeiro parâmetro é o tipo do elemento (`DataFile` para `type=file`, `DataLine` para entradas de linha única como `text`, `password`, `email`, etc. e `DataText` para todos os outros) e o segundo parâmetro `sel[]` corresponde ao atributo HTML name. O tipo do elemento pode ser combinado com o valor `DataKeys`, que preserva as chaves dos elementos. Isso é especialmente útil para `select`, `radioList` e `checkboxList`. - -O essencial é que `getHttpData()` retorna um valor sanitizado, neste caso, será sempre um array de strings UTF-8 válidas, independentemente do que um invasor tente enviar ao servidor. É análogo ao trabalho direto com `$_POST` ou `$_GET`, mas com a diferença essencial de que sempre retorna dados limpos, como você está acostumado com os elementos padrão dos formulários Nette. diff --git a/forms/pt/in-presenter.texy b/forms/pt/in-presenter.texy deleted file mode 100644 index 955d4dc3f3..0000000000 --- a/forms/pt/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Formulários em Presenters -************************* - -.[perex] -Nette Forms facilitam enormemente a criação e processamento de formulários web. Neste capítulo, você aprenderá a usar formulários dentro de presenters. - -Se você está interessado em como usá-los de forma totalmente independente do resto do framework, o guia para [uso independente|standalone] é para você. - - -Primeiro formulário -=================== - -Vamos tentar escrever um formulário de registro simples. Seu código será o seguinte: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Nome:'); -$form->addPassword('password', 'Senha:'); -$form->addSubmit('send', 'Registrar'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -e no navegador será exibido assim: - -[* form-cs.webp *] - -O formulário no presenter é um objeto da classe `Nette\Application\UI\Form`, seu predecessor `Nette\Forms\Form` é destinado ao uso independente. Adicionamos a ele os chamados elementos nome, senha e botão de envio. E, finalmente, a linha com `$form->onSuccess` diz que após o envio e validação bem-sucedida, o método `$this->formSucceeded()` deve ser chamado. - -Do ponto de vista do presenter, o formulário é um componente comum. Portanto, ele é tratado como um componente e incorporado ao presenter usando [métodos de fábrica |application:components#Métodos de fábrica]. Ficará assim: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Nome:'); - $form->addPassword('password', 'Senha:'); - $form->addSubmit('send', 'Registrar'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // aqui processamos os dados enviados pelo formulário - // $data->name contém o nome - // $data->password contém a senha - $this->flashMessage('Você foi registrado com sucesso.'); - $this->redirect('Home:'); - } -} -``` - -E no template, renderizamos o formulário com a tag `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Registro</h1> - -{control registrationForm} -``` - -E isso é basicamente tudo :-) Temos um formulário funcional e perfeitamente [seguro |#Proteção contra vulnerabilidades]. - -E agora você provavelmente está pensando que foi muito rápido, imaginando como é possível que o método `formSucceeded()` seja chamado e quais são os parâmetros que ele recebe. Certamente, você está certo, isso merece uma explicação. - -Nette introduz um mecanismo inovador que chamamos de [estilo Hollywood |application:components#Estilo Hollywood]. Em vez de você, como desenvolvedor, ter que perguntar constantemente se algo aconteceu ("o formulário foi enviado?", "foi enviado validamente?" e "não foi falsificado?"), você diz ao framework "quando o formulário estiver validamente preenchido, chame este método" e deixa o trabalho restante para ele. Se você programa em JavaScript, conhece bem este estilo de programação. Você escreve funções que são chamadas quando um determinado [evento |nette:glossary#Eventos] ocorre. E a linguagem passa os argumentos apropriados para elas. - -É exatamente assim que o código do presenter acima é construído. O array `$form->onSuccess` representa uma lista de callbacks PHP que o Nette chama no momento em que o formulário é enviado e preenchido corretamente (ou seja, é válido). Dentro do [ciclo de vida do presenter |application:presenters#Ciclo de vida do presenter], isso é chamado de sinal, eles são chamados após o método `action*` e antes do método `render*`. E para cada callback, ele passa como primeiro parâmetro o próprio formulário e como segundo os dados enviados na forma de um objeto [ArrayHash |utils:arrays#ArrayHash] por padrão (ou uma classe/array mapeado). Você pode omitir o primeiro parâmetro se não precisar do objeto do formulário. E o segundo parâmetro pode ser mais inteligente, mas falaremos sobre isso [mais tarde |#Mapeamento para classes]. - -O objeto `$data` contém as chaves `name` e `password` com os dados que o usuário preencheu. Geralmente, enviamos os dados diretamente para processamento adicional, que pode ser, por exemplo, inserção no banco de dados. Durante o processamento, no entanto, pode ocorrer um erro, por exemplo, o nome de usuário já está em uso. Nesse caso, passamos o erro de volta para o formulário usando `addError()` e o deixamos renderizar novamente, com a mensagem de erro. - -```php -$form->addError('Desculpe, o nome de usuário já está em uso.'); -``` - -Além de `onSuccess`, existe também `onSubmit`: os callbacks são chamados sempre após o envio do formulário, mesmo que não esteja preenchido corretamente. E também `onError`: os callbacks são chamados apenas se o envio não for válido. Eles são chamados mesmo se invalidarmos o formulário em `onSuccess` ou `onSubmit` usando `addError()`. - -Após processar o formulário, redirecionamos para a próxima página. Isso evita o reenvio indesejado do formulário pelo botão *atualizar*, *voltar* ou movimento no histórico do navegador. - -Tente adicionar outros [elementos de formulário|controls]. - - -Acesso aos elementos -==================== - -O formulário é um componente do presenter, em nosso caso chamado `registrationForm` (pelo nome do método de fábrica `createComponentRegistrationForm`), então em qualquer lugar no presenter você pode acessar o formulário usando: - -```php -$form = $this->getComponent('registrationForm'); -// sintaxe alternativa: $form = $this['registrationForm']; -``` - -Os elementos individuais do formulário também são componentes, então você pode acessá-los da mesma maneira: - -```php -$input = $form->getComponent('name'); // ou $input = $form['name']; -$button = $form->getComponent('send'); // ou $button = $form['send']; -``` - -Os elementos são removidos usando unset: - -```php -unset($form['name']); -``` - - -Regras de validação -=================== - -A palavra *válido* foi mencionada, mas o formulário ainda não tem regras de validação. Vamos corrigir isso. - -O nome será obrigatório, então o marcamos com o método `setRequired()`, cujo argumento é o texto da mensagem de erro que será exibida se o usuário não preencher o nome. Se o argumento não for fornecido, a mensagem de erro padrão será usada. - -```php -$form->addText('name', 'Nome:') - ->setRequired('Por favor, insira o nome'); -``` - -Tente enviar o formulário sem preencher o nome e você verá que uma mensagem de erro será exibida e o navegador ou servidor o rejeitará até que você preencha o campo. - -Ao mesmo tempo, você não pode enganar o sistema escrevendo apenas espaços no campo. De jeito nenhum. O Nette remove automaticamente os espaços à esquerda e à direita. Experimente. É algo que você deve fazer com cada entrada de linha única, mas muitas vezes é esquecido. O Nette faz isso automaticamente. (Você pode tentar enganar o formulário e enviar uma string multilinha como nome. Mesmo aqui, o Nette não se deixa enganar e transforma as quebras de linha em espaços.) - -O formulário é sempre validado no lado do servidor, mas também é gerada uma validação JavaScript, que ocorre instantaneamente e o usuário é informado sobre o erro imediatamente, sem a necessidade de enviar o formulário ao servidor. Isso é feito pelo script `netteForms.js`. Insira-o no template de layout: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Se você olhar o código-fonte da página com o formulário, poderá notar que o Nette insere os elementos obrigatórios em elementos com a classe CSS `required`. Tente adicionar a seguinte folha de estilo ao template e o rótulo "Nome" ficará vermelho. Elegantemente, marcamos os elementos obrigatórios para os usuários: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Outras regras de validação são adicionadas com o método `addRule()`. O primeiro parâmetro é a regra, o segundo é novamente o texto da mensagem de erro e pode ainda seguir um argumento da regra de validação. O que isso significa? - -Vamos estender o formulário com um novo campo opcional "idade", que deve ser um número inteiro (`addInteger()`) e, além disso, dentro de um intervalo permitido (`Form::Range`). E aqui usaremos o terceiro parâmetro do método `addRule()`, pelo qual passamos o intervalo necessário ao validador como um par `[de, até]`: - -```php -$form->addInteger('age', 'Idade:') - ->addRule($form::Range, 'A idade deve ser entre 18 e 120', [18, 120]); -``` - -.[tip] -Se o usuário não preencher o campo, as regras de validação não serão verificadas, pois o elemento é opcional. - -Aqui surge espaço para uma pequena refatoração. Na mensagem de erro e no terceiro parâmetro, os números são mencionados duplicadamente, o que não é ideal. Se estivéssemos criando [formulários multilíngues |rendering#Tradução] e a mensagem contendo números fosse traduzida para vários idiomas, dificultaria uma possível alteração dos valores. Por esse motivo, é possível usar os marcadores `%d` e o Nette completará os valores: - -```php - ->addRule($form::Range, 'A idade deve ser entre %d e %d anos', [18, 120]); -``` - -Voltemos ao elemento `password`, que também tornaremos obrigatório e verificaremos o comprimento mínimo da senha (`$form::MinLength`), novamente usando o marcador: - -```php -$form->addPassword('password', 'Senha:') - ->setRequired('Escolha uma senha') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -Adicionaremos ao formulário ainda o campo `passwordVerify`, onde o usuário digita a senha novamente, para verificação. Usando regras de validação, verificamos se ambas as senhas são iguais (`$form::Equal`). E como parâmetro, damos uma referência à primeira senha usando [colchetes |#Acesso aos elementos]: - -```php -$form->addPassword('passwordVerify', 'Senha para verificação:') - ->setRequired('Por favor, digite a senha novamente para verificação') - ->addRule($form::Equal, 'As senhas não coincidem', $form['password']) - ->setOmitted(); -``` - -Usando `setOmitted()`, marcamos o elemento cujo valor realmente não nos importa e que existe apenas para fins de validação. O valor não é passado para `$data`. - -Com isso, temos um formulário totalmente funcional com validação em PHP e JavaScript. As capacidades de validação do Nette são muito mais amplas, é possível criar condições, deixar partes da página serem exibidas e ocultadas com base nelas, etc. Tudo será explicado no capítulo sobre [validação de formulários|validation]. - - -Valores padrão -============== - -Normalmente, definimos valores padrão para os elementos do formulário: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Muitas vezes, é útil definir valores padrão para todos os elementos de uma vez. Por exemplo, quando o formulário serve para editar registros. Lemos o registro do banco de dados e definimos os valores padrão: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Chame `setDefaults()` após definir os elementos. - - -Renderização do formulário -========================== - -Por padrão, o formulário é renderizado como uma tabela. Os elementos individuais cumprem a regra básica de acessibilidade - todos os rótulos são escritos como `<label>` e vinculados ao elemento de formulário correspondente. Ao clicar no rótulo, o cursor aparece automaticamente no campo do formulário. - -Podemos definir quaisquer atributos HTML para cada elemento. Por exemplo, adicionar um placeholder: - -```php -$form->addInteger('age', 'Idade:') - ->setHtmlAttribute('placeholder', 'Por favor, preencha a idade'); -``` - -Existem realmente muitas maneiras de renderizar um formulário, então há um [capítulo separado sobre renderização|rendering] dedicado a isso. - - -Mapeamento para classes .{mapeamento-para-classes} -================================================== - -Voltemos ao método `formSucceeded()`, que no segundo parâmetro `$data` recebe os dados enviados como um objeto `ArrayHash`. Como é uma classe genérica, algo como `stdClass`, sentiremos falta de certo conforto ao trabalhar com ela, como sugestão de propriedades nos editores ou análise estática de código. Isso poderia ser resolvido tendo uma classe específica para cada formulário, cujas propriedades representam os elementos individuais. Por exemplo: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativamente, você pode usar o construtor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public ?int $age, - public string $password, - ) { - } -} -``` - -As propriedades da classe de dados também podem ser enumerações e serão mapeadas automaticamente. .{data-version:3.2.4} - -Como dizer ao Nette para nos retornar os dados como objetos desta classe? Mais fácil do que você pensa. Basta apenas indicar a classe como o tipo do parâmetro `$data` no método manipulador: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $name é uma instância de RegistrationFormData - $name = $data->name; - // ... -} -``` - -Como tipo, também pode ser indicado `array` e então os dados são passados como um array. - -Da mesma forma, pode-se usar o método `getValues()`, ao qual o nome da classe ou o objeto a ser hidratado é passado como parâmetro: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Se os formulários formarem uma estrutura multinível composta por contêineres, crie uma classe separada para cada um: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -O mapeamento então, a partir do tipo da propriedade `$person`, reconhece que deve mapear o contêiner para a classe `PersonFormData`. Se a propriedade contivesse um array de contêineres, indique o tipo `array` e passe a classe para mapeamento diretamente para o contêiner: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Você pode ter o design da classe de dados do formulário gerado usando o método `Nette\Forms\Blueprint::dataClass($form)`, que o imprime na página do navegador. O código então só precisa ser clicado, marcado e copiado para o projeto. .{data-version:3.1.15} - - -Vários botões -============= - -Se o formulário tiver mais de um botão, geralmente precisamos distinguir qual deles foi pressionado. Podemos criar nossa própria função manipuladora para cada botão. Definimo-la como um handler para o [evento |nette:glossary#Eventos] `onClick`: - -```php -$form->addSubmit('save', 'Salvar') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Excluir') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Esses handlers são chamados apenas no caso de um formulário validamente preenchido, assim como no caso do evento `onSuccess`. A diferença é que, como primeiro parâmetro, em vez do formulário, pode ser passado o botão de envio, dependendo do tipo que você indicar: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Quando o formulário é enviado com a tecla <kbd>Enter</kbd>, considera-se como se tivesse sido enviado pelo primeiro botão. - - -Evento onAnchor -=============== - -Quando, no método de fábrica (como `createComponentRegistrationForm`), montamos o formulário, ele ainda não sabe se foi enviado, nem com quais dados. Mas há casos em que precisamos conhecer os valores enviados, talvez a forma adicional do formulário dependa deles, ou precisemos deles para selectboxes dependentes, etc. - -Portanto, a parte do código que monta o formulário pode ser deixada para ser chamada apenas no momento em que ele está, por assim dizer, ancorado, ou seja, já está conectado ao presenter e conhece seus dados enviados. Tal código é passado para o array `$onAnchor`: - -```php -$country = $form->addSelect('country', 'País:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Cidade:'); - -$form->onAnchor[] = function () use ($country, $city) { - // esta função será chamada quando o formulário souber se foi enviado e com quais dados - // portanto, é possível usar o método getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Proteção contra vulnerabilidades .{proteção-contra-vulnerabilidades} -==================================================================== - -O Nette Framework dá grande ênfase à segurança e, portanto, cuida meticulosamente da boa segurança dos formulários. Faz isso de forma totalmente transparente e não requer nenhuma configuração manual. - -Além de proteger os formulários contra ataques de [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] e [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], ele realiza muitas pequenas proteções nas quais você não precisa mais pensar. - -Por exemplo, ele filtra todos os caracteres de controle das entradas e verifica a validade da codificação UTF-8, para que os dados do formulário estejam sempre limpos. Para caixas de seleção e listas de rádio, ele verifica se os itens selecionados estavam realmente entre os oferecidos e não foram falsificados. Já mencionamos que, para entradas de texto de linha única, ele remove os caracteres de fim de linha que um invasor poderia ter enviado. Para entradas multilinha, ele normaliza os caracteres de fim de linha. E assim por diante. - -O Nette resolve para você riscos de segurança que muitos programadores nem sabem que existem. - -O ataque CSRF mencionado consiste em um invasor atrair a vítima para uma página que, discretamente no navegador da vítima, executa uma requisição ao servidor no qual a vítima está logada, e o servidor acredita que a requisição foi feita pela vítima por vontade própria. Portanto, o Nette impede o envio de formulários POST de outro domínio. Se, por algum motivo, você quiser desativar a proteção e permitir o envio do formulário de outro domínio, use: - -```php -$form->allowCrossOrigin(); // ATENÇÃO! Desativa a proteção! -``` - -Esta proteção utiliza um cookie SameSite chamado `_nss`. A proteção usando o cookie SameSite pode não ser 100% confiável, por isso é aconselhável ativar também a proteção por token: - -```php -$form->addProtection(); -``` - -Recomendamos proteger assim os formulários na parte administrativa do site, que alteram dados sensíveis na aplicação. O framework se defende contra o ataque CSRF gerando e verificando um token de autorização, que é armazenado na sessão. Portanto, é necessário ter a sessão aberta antes de exibir o formulário. Na parte administrativa do site, a sessão geralmente já está iniciada devido ao login do usuário. Caso contrário, inicie a sessão com o método `Nette\Http\Session::start()`. - - -Mesmo formulário em vários presenters -===================================== - -Se você precisar usar o mesmo formulário em vários presenters, recomendamos criar uma fábrica para ele, que você então passará para o presenter. Um local adequado para tal classe é, por exemplo, o diretório `app/Forms`. - -A classe de fábrica pode parecer assim: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nome:'); - $form->addSubmit('send', 'Entrar'); - return $form; - } -} -``` - -Pedimos à classe para produzir o formulário no método de fábrica para componentes no presenter: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // podemos modificar o formulário, aqui por exemplo mudamos o rótulo no botão - $form['send']->setCaption('Continuar'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // e adicionamos o handler - return $form; -} -``` - -O handler para processamento do formulário também pode ser fornecido pela fábrica: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nome:'); - $form->addSubmit('send', 'Entrar'); - $form->onSuccess[] = function (Form $form, $data): void { - // aqui realizamos o processamento do formulário - }; - return $form; - } -} -``` - -Então, tivemos uma introdução rápida aos formulários no Nette. Tente dar uma olhada no diretório [examples|https://github.com/nette/forms/tree/master/examples] na distribuição, onde encontrará mais inspiração. diff --git a/forms/pt/rendering.texy b/forms/pt/rendering.texy deleted file mode 100644 index 520d0fb787..0000000000 --- a/forms/pt/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Renderização de Formulários -*************************** - -A aparência dos formulários pode variar muito. Na prática, podemos encontrar dois extremos. Por um lado, há a necessidade de renderizar vários formulários na aplicação que são visualmente tão semelhantes quanto dois ovos, e apreciamos a renderização fácil sem um template usando `$form->render()`. Este é geralmente o caso das interfaces administrativas. - -Por outro lado, existem formulários diversos onde a regra é: cada peça é um original. A sua forma é melhor descrita usando a linguagem HTML no template do formulário. E, claro, além dos dois extremos mencionados, encontraremos muitos formulários que se situam algures entre eles. - - -Renderização usando Latte -========================= - -O [Sistema de templates Latte|latte:] facilita fundamentalmente a renderização de formulários e dos seus controlos. Primeiro, mostraremos como renderizar formulários manualmente, controlo por controlo, obtendo assim controlo total sobre o código. Mais tarde, mostraremos como essa renderização pode ser [automatizada |#Renderização automática]. - -Pode gerar o design do template Latte do formulário usando o método `Nette\Forms\Blueprint::latte($form)`, que o imprime na página do navegador. Depois, basta clicar para selecionar o código e copiá-lo para o seu projeto. .{data-version:3.1.15} - - -`{control}` ------------ - -A maneira mais simples de renderizar um formulário é escrever no template: - -```latte -{control signInForm} -``` - -A aparência do formulário renderizado desta forma pode ser influenciada configurando o [#Renderer] e os [controlos individuais |#Atributos HTML]. - - -`n:name` --------- - -A definição do formulário no código PHP pode ser ligada de forma extremamente fácil ao código HTML. Basta adicionar atributos `n:name`. É tão fácil! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Tem controlo total sobre a forma do código HTML resultante. Se usar o atributo `n:name` nos elementos `<select>`, `<button>` ou `<textarea>`, o seu conteúdo interno será preenchido automaticamente. Além disso, a tag `<form n:name>` cria uma variável local `$form` com o objeto do formulário a ser desenhado, e a tag de fecho `</form>` renderiza todos os controlos ocultos não renderizados (o mesmo se aplica a `{form} ... {/form}`). - -No entanto, não devemos esquecer de renderizar possíveis mensagens de erro. Tanto aquelas que foram adicionadas aos controlos individuais pelo método `addError()` (usando `{inputError}`), como aquelas adicionadas diretamente ao formulário (retornadas por `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Controlos de formulário mais complexos, como RadioList ou CheckboxList, podem ser renderizados item por item desta forma: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Não quer pensar em que elemento HTML usar no template para cada controlo, seja `<input>`, `<textarea>`, etc.? A solução é a tag universal `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Se o formulário usar um tradutor, o texto dentro das tags `{label}` será traduzido. - -Mesmo neste caso, controlos de formulário mais complexos, como RadioList ou CheckboxList, podem ser renderizados item por item: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Para renderizar apenas o `<input>` no controlo Checkbox, use `{input myCheckbox:}`. Neste caso, separe sempre os atributos HTML com uma vírgula `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Exibe a mensagem de erro para um controlo de formulário, se houver alguma. A mensagem geralmente é envolvida num elemento HTML para estilização. Evitar a renderização de um elemento vazio, se não houver mensagem, pode ser feito elegantemente usando `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -A presença de um erro pode ser verificada com o método `hasErrors()` e, com base nisso, definir a classe do elemento pai: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -As tags `{form signInForm}...{/form}` são uma alternativa a `<form n:name="signInForm">...</form>`. - - -Renderização automática ------------------------ - -Graças às tags `{input}` e `{label}`, podemos facilmente criar um template genérico para qualquer formulário. Ele iterará e renderizará sequencialmente todos os seus controlos, exceto os controlos ocultos, que são renderizados automaticamente ao fechar o formulário com a tag `</form>`. O nome do formulário a ser renderizado será esperado na variável `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -As tags de par auto-fechadas `{label .../}` usadas exibem os rótulos provenientes da definição do formulário no código PHP. - -Guarde este template genérico, por exemplo, no ficheiro `basic-form.latte` e para renderizar o formulário, basta incluí-lo e passar o nome (ou instância) do formulário para o parâmetro `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Se quiser intervir na forma de um formulário específico durante a renderização e, por exemplo, renderizar um controlo de forma diferente, o caminho mais simples é preparar blocos no template que possam ser sobrescritos posteriormente. Os blocos também podem ter [nomes dinâmicos |latte:template-inheritance#Nomes de Blocos Dinâmicos], pelo que pode inserir o nome do controlo a ser renderizado neles. Por exemplo: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Para o controlo, por exemplo, `username`, será criado o bloco `input-username`, que pode ser facilmente sobrescrito usando a tag [{embed} |latte:template-inheritance#Herança de Unidade]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternativamente, todo o conteúdo do template `basic-form.latte` pode ser [definido |latte:template-inheritance#Definições] como um bloco, incluindo o parâmetro `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Graças a isso, a sua chamada será ligeiramente mais simples: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -O bloco só precisa ser importado num único local, no início do template de layout: - -```latte -{import basic-form.latte} -``` - - -Casos especiais ---------------- - -Se precisar de renderizar apenas a parte interna do formulário sem as tags HTML `<form>`, por exemplo, ao enviar snippets, oculte-as usando o atributo `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -A tag `{formContainer}` ajuda na renderização de controlos dentro de um contêiner de formulário. - -```latte -<p>Quais notícias deseja receber:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Renderização sem Latte -====================== - -A maneira mais simples de renderizar um formulário é chamar: - -```php -$form->render(); -``` - -A aparência do formulário renderizado desta forma pode ser influenciada configurando o [#Renderer] e os [controlos individuais |#Atributos HTML]. - - -Renderização manual -------------------- - -Cada controlo de formulário possui métodos que geram o código HTML do campo de formulário e do rótulo. Podem retorná-lo como uma string ou como um objeto [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` retorna o código HTML do controlo -- `getLabel($caption = null): Html|string|null` retorna o código HTML do rótulo, se existir - -O formulário pode, assim, ser renderizado controlo por controlo: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Enquanto para alguns controlos `getControl()` retorna um único elemento HTML (por exemplo, `<input>`, `<select>`, etc.), para outros retorna um pedaço inteiro de código HTML (CheckboxList, RadioList). Nesse caso, pode usar métodos que geram inputs e rótulos individuais, para cada item separadamente: - -- `getControlPart($key = null): ?Html` retorna o código HTML de um item -- `getLabelPart($key = null): ?Html` retorna o código HTML do rótulo de um item - -.[note] -Estes métodos têm o prefixo `get` por razões históricas, mas `generate` seria melhor, pois a cada chamada criam e retornam um novo elemento `Html`. - - -Renderer -======== - -É um objeto que garante a renderização do formulário. Pode ser definido pelo método `$form->setRenderer`. O controlo é passado para ele quando o método `$form->render()` é chamado. - -Se não definirmos o nosso próprio renderizador, será usado o renderizador padrão [api:Nette\Forms\Rendering\DefaultFormRenderer]. Ele renderiza os controlos do formulário na forma de uma tabela HTML. A saída parece-se com isto: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Nome:</label></th> - - <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> -</tr> - -<tr class="required"> - <th><label class="required" for="frm-age">Idade:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Sexo:</label></th> - ... -``` - -Se usar ou não uma tabela para a estrutura do formulário é discutível, e muitos web designers preferem outra marcação. Por exemplo, uma lista de definição. Reconfiguraremos, portanto, o `DefaultFormRenderer` para que ele renderize o formulário na forma de uma lista. A configuração é feita editando o array [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. O primeiro índice representa sempre a área e o segundo o seu atributo. As áreas individuais são mostradas na imagem: - -[* defaultformrenderer.webp *] - -Por padrão, o grupo de controlos `controls` é envolvido por uma tabela `<table>`, cada `pair` representa uma linha da tabela `<tr>` e o par `label` e `control` são células `<th>` e `<td>`. Agora mudaremos os elementos envolventes. Inseriremos a área `controls` num contêiner `<dl>`, deixaremos a área `pair` sem contêiner, inseriremos `label` em `<dt>` e, finalmente, envolveremos `control` com as tags `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -O resultado é este código HTML: - -```latte -<dl> - <dt><label class="required" for="frm-name">Nome:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Idade:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Sexo:</label></dt> - ... -</dl> -``` - -No array wrappers, é possível influenciar toda uma gama de outros atributos: - -- adicionar classes CSS a tipos individuais de controlos de formulário -- distinguir linhas pares e ímpares com classes CSS -- distinguir visualmente itens obrigatórios e opcionais -- determinar se as mensagens de erro serão exibidas diretamente nos controlos ou acima do formulário - - -Options -------- - -O comportamento do Renderer também pode ser controlado definindo *options* nos controlos de formulário individuais. Assim, pode-se definir um rótulo que será exibido ao lado do campo de entrada: - -```php -$form->addText('phone', 'Número:') - ->setOption('description', 'Este número permanecerá oculto'); -``` - -Se quisermos colocar conteúdo HTML nele, usamos a classe [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Número:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Termos de armazenamento do seu número</a>') - ); -``` - -.[tip] -O elemento Html também pode ser usado em vez de um rótulo: `$form->addCheckbox('conditions', $label)`. - - -Agrupamento de controlos ------------------------- - -O Renderer permite agrupar controlos em grupos visuais (fieldsets): - -```php -$form->addGroup('Dados Pessoais'); -``` - -Após criar um novo grupo, ele torna-se ativo e cada controlo recém-adicionado também é adicionado a ele. Assim, o formulário pode ser construído desta forma: - -```php -$form = new Form; -$form->addGroup('Dados Pessoais'); -$form->addText('name', 'Seu nome:'); -$form->addInteger('age', 'Sua idade:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Endereço de entrega'); -$form->addCheckbox('send', 'Enviar para o endereço'); -$form->addText('street', 'Rua:'); -$form->addText('city', 'Cidade:'); -$form->addSelect('country', 'País:', $countries); -``` - -O Renderer primeiro renderiza os grupos e só depois os controlos que não pertencem a nenhum grupo. - - -Suporte para Bootstrap ----------------------- - -[Nos exemplos |https://github.com/nette/forms/tree/master/examples] encontrará exemplos de como configurar o Renderer para [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] e [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -Atributos HTML -============== - -Para definir quaisquer atributos HTML para controlos de formulário, usamos o método `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Número:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Ordenar por:', ['preço', 'nome']) - ->setHtmlAttribute('onchange', 'submit()'); // enviar ao alterar - - -// Para definir atributos do próprio <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Especificação do tipo de controlo: - -```php -$form->addText('tel', 'Seu telefone:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'escreva o telefone'); -``` - -.[warning] -A definição do tipo e outros atributos servem apenas para fins visuais. A verificação da correção das entradas deve ocorrer no servidor, o que é garantido pela escolha do [controlo de formulário|controls] apropriado e pela indicação das [regras de validação|validation]. - -Para itens individuais em listas de rádio ou checkbox, podemos definir um atributo HTML com valores diferentes para cada um deles. Observe os dois pontos após `style:`, que garantem a seleção do valor pela chave: - -```php -$colors = ['r' => 'vermelho', 'g' => 'verde', 'b' => 'azul']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Cores:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Exibe: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">vermelho</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">azul</label> -``` - -Para definir atributos lógicos, como `readonly`, podemos usar a notação com ponto de interrogação: - -```php -$form->addCheckboxList('colors', 'Cores:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // para mais chaves use um array, ex. ['r', 'g'] -``` - -Exibe: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">vermelho</label> -<label><input type="checkbox" name="colors[]" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">azul</label> -``` - -No caso de selectboxes, o método `setHtmlAttribute()` define os atributos do elemento `<select>`. Se quisermos definir atributos para os `<option>` individuais, usamos o método `setOptionAttribute()`. As notações com dois pontos e ponto de interrogação mencionadas acima também funcionam: - -```php -$form->addSelect('colors', 'Cores:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Exibe: - -```latte -<select name="colors"> - <option value="r" style="background:red">vermelho</option> - <option value="g" style="background:green">verde</option> - <option value="b">azul</option> -</select> -``` - - -Protótipos ----------- - -Uma maneira alternativa de definir atributos HTML consiste em modificar o modelo a partir do qual o elemento HTML é gerado. O modelo é um objeto `Html` e é retornado pelo método `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Número:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Desta forma, também é possível modificar o modelo do rótulo, que é retornado por `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Para os controlos Checkbox, CheckboxList e RadioList, pode influenciar o modelo do elemento que envolve todo o controlo. Ele é retornado por `getContainerPrototype()`. No estado padrão, é um elemento "vazio", então nada é renderizado, mas ao definir um nome para ele, ele será renderizado: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -No caso de CheckboxList e RadioList, também é possível influenciar o modelo do separador dos itens individuais, que é retornado pelo método `getSeparatorPrototype()`. No estado padrão, é o elemento `<br>`. Se o alterar para um elemento de par, ele envolverá os itens individuais em vez de separá-los. E, além disso, é possível influenciar o modelo do elemento HTML do rótulo nos itens individuais, que é retornado por `getItemLabelPrototype()`. - - -Tradução -======== - -Se está a programar uma aplicação multilíngue, provavelmente precisará renderizar o formulário em diferentes versões de idioma. O Nette Framework define uma interface para tradução para este propósito [api:Nette\Localization\Translator]. No Nette, não há implementação padrão, pode escolher de acordo com as suas necessidades entre várias soluções prontas que encontra no [Componette |https://componette.org/search/localization]. Na sua documentação, aprenderá como configurar o tradutor. - -Os formulários suportam a exibição de textos através do tradutor. Passamos para eles usando o método `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -A partir deste momento, não apenas todos os rótulos, mas também todas as mensagens de erro ou itens de caixas de seleção serão traduzidos para outro idioma. - -Para controlos de formulário individuais, é possível definir um tradutor diferente ou desativar completamente a tradução com o valor `null`: - -```php -$form->addSelect('carModel', 'Modelo:', $cars) - ->setTranslator(null); -``` - -Para [regras de validação|validation], parâmetros específicos também são passados ao tradutor, por exemplo, para a regra: - -```php -$form->addPassword('password', 'Senha:') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -o tradutor é chamado com estes parâmetros: - -```php -$translator->translate('A senha deve ter pelo menos %d caracteres', 8); -``` - -e, portanto, pode escolher a forma plural correta da palavra `caracteres` de acordo com o número. - - -Evento onRender -=============== - -Pouco antes de o formulário ser renderizado, podemos deixar o nosso código ser chamado. Ele pode, por exemplo, adicionar classes HTML aos controlos do formulário para exibição correta. Adicionamos o código ao array `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/pt/standalone.texy b/forms/pt/standalone.texy deleted file mode 100644 index d11c0202c9..0000000000 --- a/forms/pt/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Formulários Usados Sozinhos -*************************** - -.[perex] -Os Nette Forms facilitam muito a criação e o processamento de formulários web. Pode usá-los nas suas aplicações de forma totalmente independente do restante do framework, como mostraremos neste capítulo. - -No entanto, se usa Nette Application e presenters, o guia para [uso em presenters|in-presenter] é para si. - - -Primeiro formulário -=================== - -Vamos tentar escrever um formulário de registo simples. O código será o seguinte ("código completo":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Nome:'); -$form->addPassword('password', 'Senha:'); -$form->addSubmit('send', 'Registar'); -``` - -Podemos renderizá-lo facilmente: - -```php -$form->render(); -``` - -e no navegador ele será exibido assim: - -[* form-cs.webp *] - -O formulário é um objeto da classe `Nette\Forms\Form` (a classe `Nette\Application\UI\Form` é usada em presenters). Adicionamos a ele os chamados controlos nome, senha e um botão de envio. - -E agora vamos dar vida ao formulário. Perguntando `$form->isSuccess()`, descobrimos se o formulário foi enviado e se foi preenchido de forma válida. Se sim, exibimos os dados. Após a definição do formulário, adicionamos: - -```php -if ($form->isSuccess()) { - echo 'Formulário foi preenchido corretamente e enviado'; - $data = $form->getValues(); - // $data->name contém o nome - // $data->password contém a senha - var_dump($data); -} -``` - -O método `getValues()` retorna os dados enviados na forma de um objeto [ArrayHash |utils:arrays#ArrayHash]. Mostraremos como alterar isso [mais tarde |#Mapeamento para classes]. O objeto `$data` contém as chaves `name` e `password` com os dados que o utilizador preencheu. - -Normalmente, enviamos os dados diretamente para processamento posterior, que pode ser, por exemplo, inserção no banco de dados. No entanto, durante o processamento, pode ocorrer um erro, por exemplo, o nome de utilizador já está em uso. Nesse caso, passamos o erro de volta para o formulário usando `addError()` e o deixamos renderizar novamente, junto com a mensagem de erro. - -```php -$form->addError('Desculpe, este nome de utilizador já está em uso.'); -``` - -Após processar o formulário, redirecionamos para a próxima página. Isso evita o reenvio indesejado do formulário pelo botão *atualizar*, *voltar* ou movendo-se no histórico do navegador. - -O formulário é enviado por padrão pelo método POST para a mesma página. Ambos podem ser alterados: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -E isso é basicamente tudo :-) Temos um formulário funcional e perfeitamente [seguro |#Proteção contra vulnerabilidades]. - -Tente adicionar também outros [controlos de formulário|controls]. - - -Acesso aos controlos -==================== - -Chamamos o formulário e os seus controlos individuais de componentes. Eles formam uma árvore de componentes, onde a raiz é o formulário. Podemos aceder aos controlos individuais do formulário desta forma: - -```php -$input = $form->getComponent('name'); -// sintaxe alternativa: $input = $form['name']; - -$button = $form->getComponent('send'); -// sintaxe alternativa: $button = $form['send']; -``` - -Os controlos são removidos usando unset: - -```php -unset($form['name']); -``` - - -Regras de validação -=================== - -A palavra *válido* foi mencionada, mas o formulário ainda não possui regras de validação. Vamos corrigir isso. - -O nome será obrigatório, então marcamo-lo com o método `setRequired()`, cujo argumento é o texto da mensagem de erro que será exibida se o utilizador não preencher o nome. Se o argumento não for fornecido, a mensagem de erro padrão será usada. - -```php -$form->addText('name', 'Nome:') - ->setRequired('Por favor, insira o nome'); -``` - -Tente enviar o formulário sem preencher o nome e verá que uma mensagem de erro será exibida e o navegador ou servidor o rejeitará até que preencha o campo. - -Ao mesmo tempo, não enganará o sistema digitando, por exemplo, apenas espaços no campo. De jeito nenhum. Nette remove automaticamente os espaços à esquerda e à direita. Experimente. É algo que deveria sempre fazer com cada input de linha única, mas muitas vezes é esquecido. Nette faz isso automaticamente. (Pode tentar enganar o formulário e enviar uma string de várias linhas como nome. Mesmo aqui, Nette não se deixa enganar e transforma as quebras de linha em espaços.) - -O formulário é sempre validado no lado do servidor, mas também é gerada uma validação JavaScript, que ocorre instantaneamente e o utilizador é informado sobre o erro imediatamente, sem a necessidade de enviar o formulário ao servidor. Isso é feito pelo script `netteForms.js`. Insira-o na página: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Se olhar o código-fonte da página com o formulário, poderá notar que Nette insere os controlos obrigatórios em elementos com a classe CSS `required`. Tente adicionar a seguinte folha de estilo ao template e o rótulo "Nome" ficará vermelho. Desta forma, marcamos elegantemente os controlos obrigatórios para os utilizadores: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Adicionamos outras regras de validação com o método `addRule()`. O primeiro parâmetro é a regra, o segundo é novamente o texto da mensagem de erro e pode haver ainda um argumento da regra de validação. O que isso significa? - -Vamos estender o formulário com um novo campo opcional "idade", que deve ser um número inteiro (`addInteger()`) e, além disso, dentro de um intervalo permitido (`$form::Range`). E aqui usaremos o terceiro parâmetro do método `addRule()`, pelo qual passamos o intervalo desejado ao validador como um par `[de, até]`: - -```php -$form->addInteger('age', 'Idade:') - ->addRule($form::Range, 'A idade deve ser entre 18 e 120', [18, 120]); -``` - -.[tip] -Se o utilizador não preencher o campo, as regras de validação não serão verificadas, pois o controlo é opcional. - -Aqui surge espaço para uma pequena refatoração. Na mensagem de erro e no terceiro parâmetro, os números são listados em duplicidade, o que não é ideal. Se estivéssemos a criar [formulários multilíngues |rendering#Tradução] e a mensagem contendo números fosse traduzida para vários idiomas, uma eventual alteração dos valores seria dificultada. Por esse motivo, é possível usar os placeholders `%d` e Nette preencherá os valores: - -```php - ->addRule($form::Range, 'A idade deve ser entre %d e %d anos', [18, 120]); -``` - -Voltemos ao controlo `password`, que também tornaremos obrigatório e ainda verificaremos o comprimento mínimo da senha (`$form::MinLength`), novamente usando o placeholder: - -```php -$form->addPassword('password', 'Senha:') - ->setRequired('Escolha uma senha') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -Adicionamos ao formulário também o campo `passwordVerify`, onde o utilizador digita a senha novamente, para verificação. Usando regras de validação, verificamos se ambas as senhas são iguais (`$form::Equal`). E como parâmetro, damos uma referência à primeira senha usando [colchetes |#Acesso aos controlos]: - -```php -$form->addPassword('passwordVerify', 'Senha para verificação:') - ->setRequired('Por favor, digite a senha novamente para verificação') - ->addRule($form::Equal, 'As senhas não coincidem', $form['password']) - ->setOmitted(); -``` - -Usando `setOmitted()`, marcamos o controlo cujo valor realmente não nos importa e que existe apenas para fins de validação. O valor não é passado para `$data`. - -Com isso, temos um formulário totalmente funcional com validação em PHP e JavaScript. As capacidades de validação de Nette são muito mais amplas, é possível criar condições, exibir e ocultar partes da página com base nelas, etc. Aprenderá tudo no capítulo sobre [validação de formulários|validation]. - - -Valores padrão -============== - -Normalmente, definimos valores padrão para os controlos do formulário: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Muitas vezes, é útil definir valores padrão para todos os controlos simultaneamente. Por exemplo, quando o formulário é usado para editar registos. Lemos o registo do banco de dados e definimos os valores padrão: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Chame `setDefaults()` após a definição dos controlos. - - -Renderização do formulário -========================== - -Por padrão, o formulário é renderizado como uma tabela. Os controlos individuais cumprem a regra básica de acessibilidade - todos os rótulos são escritos como `<label>` e vinculados ao controlo de formulário correspondente. Ao clicar no rótulo, o cursor aparece automaticamente no campo do formulário. - -Podemos definir atributos HTML arbitrários para cada controlo. Por exemplo, adicionar um placeholder: - -```php -$form->addInteger('age', 'Idade:') - ->setHtmlAttribute('placeholder', 'Por favor, preencha a idade'); -``` - -Existem realmente muitas maneiras de renderizar um formulário, então há um [capítulo separado sobre renderização|rendering] dedicado a isso. - - -Mapeamento para classes -======================= - -Voltemos ao processamento dos dados do formulário. O método `getValues()` retornou-nos os dados enviados como um objeto `ArrayHash`. Como é uma classe genérica, algo como `stdClass`, sentiremos falta de certo conforto ao trabalhar com ela, como sugestões de propriedades em editores ou análise estática de código. Isso poderia ser resolvido tendo uma classe específica para cada formulário, cujas propriedades representam os controlos individuais. Por exemplo: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativamente, pode usar o construtor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -As propriedades da classe de dados também podem ser enums e serão mapeadas automaticamente. .{data-version:3.2.4} - -Como dizer ao Nette para nos retornar os dados como objetos desta classe? Mais fácil do que pensa. Basta fornecer o nome da classe ou o objeto a ser hidratado como parâmetro: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Também é possível fornecer `'array'` como parâmetro e, em seguida, os dados serão retornados como um array. - -Se os formulários formarem uma estrutura multinível composta por contêineres, crie uma classe separada para cada um: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -O mapeamento então reconhece pelo tipo da propriedade `$person` que deve mapear o contêiner para a classe `PersonFormData`. Se a propriedade contiver um array de contêineres, especifique o tipo `array` e passe a classe para mapeamento diretamente para o contêiner: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Pode gerar o design da classe de dados do formulário usando o método `Nette\Forms\Blueprint::dataClass($form)`, que o exibirá na página do navegador. Em seguida, basta clicar para selecionar o código e copiá-lo para o projeto. .{data-version:3.1.15} - - -Múltiplos botões -================ - -Se o formulário tiver mais de um botão, geralmente precisamos distinguir qual deles foi pressionado. Essa informação é retornada pelo método `isSubmittedBy()` do botão: - -```php -$form->addSubmit('save', 'Salvar'); -$form->addSubmit('delete', 'Excluir'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Não pule a verificação `$form->isSuccess()`, ela verifica a validade dos dados. - -Quando o formulário é enviado pressionando a tecla <kbd>Enter</kbd>, é considerado como se tivesse sido enviado pelo primeiro botão. - - -Proteção contra vulnerabilidades -================================ - -O Nette Framework dá grande ênfase à segurança e, portanto, cuida meticulosamente da boa segurança dos formulários. - -Além de proteger os formulários contra ataques [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] e [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], ele realiza muitas pequenas medidas de segurança com as quais já não precisa de se preocupar. - -Por exemplo, ele filtra todos os caracteres de controlo das entradas e verifica a validade da codificação UTF-8, para que os dados do formulário estejam sempre limpos. Para caixas de seleção e listas de rádio, ele verifica se os itens selecionados eram realmente das opções oferecidas e se não houve falsificação. Já mencionamos que, para entradas de texto de linha única, ele remove os caracteres de fim de linha que um invasor poderia ter enviado. Para entradas de várias linhas, ele normaliza os caracteres de fim de linha. E assim por diante. - -Nette resolve para si riscos de segurança que muitos programadores nem sabem que existem. - -O ataque CSRF mencionado consiste no facto de que o invasor atrai a vítima para uma página que, discretamente no navegador da vítima, executa uma requisição ao servidor no qual a vítima está logada, e o servidor acredita que a requisição foi executada pela vítima por sua própria vontade. Portanto, Nette impede o envio de formulários POST de outro domínio. Se, por algum motivo, quiser desativar a proteção e permitir o envio de formulários de outro domínio, use: - -```php -$form->allowCrossOrigin(); // ATENÇÃO! Desativa a proteção! -``` - -Esta proteção usa um cookie SameSite chamado `_nss`. Portanto, crie o objeto do formulário antes de enviar a primeira saída, para que o cookie possa ser enviado. - -A proteção usando o cookie SameSite pode não ser 100% confiável, por isso é aconselhável ativar também a proteção por token: - -```php -$form->addProtection(); -``` - -Recomendamos proteger desta forma os formulários na parte administrativa do site, que alteram dados sensíveis na aplicação. O framework defende-se contra o ataque CSRF gerando e verificando um token de autorização, que é armazenado na sessão. Portanto, é necessário ter a sessão aberta antes de exibir o formulário. Na parte administrativa do site, a sessão geralmente já está iniciada devido ao login do utilizador. Caso contrário, inicie a sessão com o método `Nette\Http\Session::start()`. - -Então, passamos por uma rápida introdução aos formulários em Nette. Tente dar uma olhada no diretório [examples|https://github.com/nette/forms/tree/master/examples] na distribuição, onde encontrará mais inspiração. diff --git a/forms/pt/validation.texy b/forms/pt/validation.texy deleted file mode 100644 index 3e4f378d64..0000000000 --- a/forms/pt/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Validação de formulários -************************ - - -Controlos obrigatórios -====================== - -Marcamos os controlos obrigatórios com o método `setRequired()`, cujo argumento é o texto da [mensagem de erro |#Mensagens de erro], que será exibida se o utilizador não preencher o controlo. Se o argumento não for fornecido, a mensagem de erro padrão será usada. - -```php -$form->addText('name', 'Nome:') - ->setRequired('Por favor, insira o nome'); -``` - - -Regras -====== - -Adicionamos regras de validação aos controlos usando o método `addRule()`. O primeiro parâmetro é a regra, o segundo é o texto da [mensagem de erro |#Mensagens de erro] e o terceiro é o argumento da regra de validação. - -```php -$form->addPassword('password', 'Senha:') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -**As regras de validação são verificadas apenas se o utilizador preencher o controlo.** - -Nette vem com uma série de regras predefinidas, cujos nomes são constantes da classe `Nette\Forms\Form`. Podemos usar estas regras para todos os controlos: - -| constante | descrição | tipo de argumento -|------- -| `Required` | controlo obrigatório, alias para `setRequired()` | - -| `Filled` | controlo obrigatório, alias para `setRequired()` | - -| `Blank` | o controlo não deve ser preenchido | - -| `Equal` | o valor é igual ao parâmetro | `mixed` -| `NotEqual` | o valor não é igual ao parâmetro | `mixed` -| `IsIn` | o valor é igual a um dos itens no array | `array` -| `IsNotIn` | o valor não é igual a nenhum item no array | `array` -| `Valid` | o controlo está preenchido corretamente? (para [#Condições]) | - - - -Entradas de texto ------------------ - -Para os controlos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, algumas das seguintes regras também podem ser usadas: - -| `MinLength` | comprimento mínimo do texto | `int` -| `MaxLength` | comprimento máximo do texto | `int` -| `Length` | comprimento no intervalo ou comprimento exato | par `[int, int]` ou `int` -| `Email` | endereço de e-mail válido | - -| `URL` | URL absoluta | - -| `Pattern` | corresponde à expressão regular | `string` -| `PatternInsensitive` | como `Pattern`, mas insensível a maiúsculas/minúsculas | `string` -| `Integer` | valor inteiro | - -| `Numeric` | alias para `Integer` | - -| `Float` | número | - -| `Min` | valor mínimo do controlo numérico | `int\|float` -| `Max` | valor máximo do controlo numérico | `int\|float` -| `Range` | valor no intervalo | par `[int\|float, int\|float]` - -As regras de validação `Integer`, `Numeric` e `Float` convertem diretamente o valor para inteiro ou float, respetivamente. Além disso, a regra `URL` também aceita um endereço sem esquema (por exemplo, `nette.org`) e adiciona o esquema (`https://nette.org`). A expressão em `Pattern` e `PatternIcase` deve corresponder a todo o valor, ou seja, como se estivesse envolvida pelos caracteres `^` e `$`. - - -Número de itens ---------------- - -Para os controlos `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, as seguintes regras também podem ser usadas para limitar o número de itens selecionados ou ficheiros enviados: - -| `MinLength` | número mínimo | `int` -| `MaxLength` | número máximo | `int` -| `Length` | número no intervalo ou número exato | par `[int, int]` ou `int` - - -Upload de ficheiros -------------------- - -Para os controlos `addUpload()`, `addMultiUpload()`, as seguintes regras também podem ser usadas: - -| `MaxFileSize` | tamanho máximo do ficheiro em bytes | `int` -| `MimeType` | tipo MIME, curingas permitidos (`'video/*'`) | `string\|string[]` -| `Image` | imagem JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | nome do ficheiro corresponde à expressão regular | `string` -| `PatternInsensitive` | como `Pattern`, mas insensível a maiúsculas/minúsculas | `string` - -`MimeType` e `Image` exigem a extensão PHP `fileinfo`. Elas detetam se um ficheiro ou imagem é do tipo desejado com base na sua assinatura e **não verificam a integridade de todo o ficheiro.** Se uma imagem não está danificada pode ser verificado, por exemplo, tentando [carregá-la |http:request#toImage]. - - -Mensagens de erro -================= - -Todas as regras predefinidas, exceto `Pattern` e `PatternInsensitive`, têm uma mensagem de erro padrão, então ela pode ser omitida. No entanto, fornecer e formular todas as mensagens sob medida tornará o formulário mais amigável ao utilizador. - -Pode alterar as mensagens padrão na [configuração|forms:configuration], editando os textos no array `Nette\Forms\Validator::$messages` ou usando um [tradutor |rendering#Tradução]. - -No texto das mensagens de erro, podem ser usadas as seguintes strings de placeholder: - -| `%d` | substitui sequencialmente pelos argumentos da regra -| `%n$d` | substitui pelo n-ésimo argumento da regra -| `%label` | substitui pelo rótulo do controlo (sem dois pontos) -| `%name` | substitui pelo nome do controlo (por exemplo, `name`) -| `%value` | substitui pelo valor inserido pelo utilizador - -```php -$form->addText('name', 'Nome:') - ->setRequired('Preencha por favor %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'pelo menos %d e no máximo %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'no máximo %2$d e pelo menos %1$d', [5, 10]); -``` - - -Condições -========= - -Além das regras, também é possível adicionar condições. Elas são escritas de forma semelhante às regras, mas em vez de `addRule()`, usamos o método `addCondition()` e, obviamente, não fornecemos nenhuma mensagem de erro (a condição apenas pergunta): - -```php -$form->addPassword('password', 'Senha:') - // se a senha não tiver mais de 8 caracteres - ->addCondition($form::MaxLength, 8) - // então deve conter um dígito - ->addRule($form::Pattern, 'Deve conter um dígito', '.*[0-9].*'); -``` - -A condição também pode ser vinculada a outro controlo que não o atual, usando `addConditionOn()`. Como primeiro parâmetro, fornecemos uma referência ao controlo. Neste exemplo, o e-mail será obrigatório apenas se a caixa de seleção for marcada (o seu valor será true): - -```php -$form->addCheckbox('newsletters', 'enviar-me newsletters'); - -$form->addEmail('email', 'E-mail:') - // se a caixa de seleção estiver marcada - ->addConditionOn($form['newsletters'], $form::Equal, true) - // então exija o e-mail - ->setRequired('Insira o endereço de e-mail'); -``` - -É possível criar estruturas complexas a partir de condições usando `elseCondition()` e `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // se a primeira condição for atendida - ->addConditionOn(/* ... */) // e a segunda condição em outro controlo - ->addRule(/* ... */) // exija esta regra - ->elseCondition() // se a segunda condição não for atendida - ->addRule(/* ... */) // exija estas regras - ->addRule(/* ... */) - ->endCondition() // voltamos à primeira condição - ->addRule(/* ... */); -``` - -Em Nette, é muito fácil reagir ao cumprimento ou não cumprimento de uma condição também no lado do JavaScript usando o método `toggle()`, veja [#JavaScript dinâmico]. - - -Referência a outro controlo -=========================== - -Como argumento de uma regra ou condição, também é possível passar outro controlo do formulário. A regra então usará o valor inserido posteriormente pelo utilizador no navegador. Desta forma, é possível, por exemplo, validar dinamicamente que o controlo `password` contém a mesma string que o controlo `password_confirm`: - -```php -$form->addPassword('password', 'Senha'); -$form->addPassword('password_confirm', 'Confirme a senha') - ->addRule($form::Equal, 'As senhas inseridas não coincidem', $form['password']); -``` - - -Regras e condições personalizadas -================================= - -Ocasionalmente, chegamos a uma situação em que as regras de validação incorporadas em Nette não são suficientes e precisamos validar os dados do utilizador à nossa maneira. Em Nette, isso é muito simples! - -Aos métodos `addRule()` ou `addCondition()`, é possível passar qualquer callback como primeiro parâmetro. Ele recebe o próprio controlo como primeiro parâmetro e retorna um valor booleano indicando se a validação foi bem-sucedida. Ao adicionar uma regra usando `addRule()`, é possível fornecer argumentos adicionais, que são então passados como segundo parâmetro. - -Podemos criar o nosso próprio conjunto de validadores como uma classe com métodos estáticos: - -```php -class MyValidators -{ - // testa se o valor é divisível pelo argumento - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // outros validadores - } -} -``` - -O uso é então muito simples: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'O valor deve ser um múltiplo de %d', - 8, - ); -``` - -Regras de validação personalizadas também podem ser adicionadas ao JavaScript. A condição é que a regra seja um método estático. O seu nome para o validador JavaScript é formado pela junção do nome da classe sem barras invertidas `\`, um sublinhado `_` e o nome do método. Por exemplo, `App\MyValidators::validateDivisibility` é escrito como `AppMyValidators_validateDivisibility` e adicionado ao objeto `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Evento onValidate -================= - -Após o envio do formulário, a validação é realizada, onde as regras individuais adicionadas via `addRule()` são verificadas e, em seguida, o [evento |nette:glossary#Eventos] `onValidate` é disparado. O seu handler pode ser usado para validação adicional, tipicamente para verificar a combinação correta de valores em múltiplos controlos do formulário. - -Se um erro for detetado, passamos para o formulário usando o método `addError()`. Ele pode ser chamado num controlo específico ou diretamente no formulário. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Esta combinação não é possível.'); - } -} -``` - - -Erros durante o processamento -============================= - -Em muitos casos, descobrimos um erro apenas no momento em que estamos a processar um formulário válido, por exemplo, ao inserir um novo item no banco de dados e encontrar uma duplicidade de chaves. Nesse caso, passamos novamente o erro para o formulário usando o método `addError()`. Ele pode ser chamado num controlo específico ou diretamente no formulário: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Senha inválida.'); - } -} -``` - -Se possível, recomendamos anexar o erro diretamente ao controlo do formulário, pois ele será exibido ao lado dele ao usar o renderizador padrão. - -```php -$form['date']->addError('Desculpe, mas esta data já está ocupada.'); -``` - -Pode chamar `addError()` repetidamente para passar várias mensagens de erro ao formulário ou controlo. Pode obtê-las usando `getErrors()`. - -Atenção, `$form->getErrors()` retorna um resumo de todas as mensagens de erro, incluindo aquelas que foram passadas diretamente para controlos individuais, não apenas diretamente para o formulário. Mensagens de erro passadas apenas para o formulário podem ser obtidas via `$form->getOwnErrors()`. - - -Modificação da entrada -====================== - -Usando o método `addFilter()`, podemos modificar o valor inserido pelo utilizador. Neste exemplo, toleraremos e removeremos espaços no código postal: - -```php -$form->addText('zip', 'Código Postal:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // removemos espaços do código postal - }) - ->addRule($form::Pattern, 'Código Postal não está no formato de cinco dígitos', '\d{5}'); -``` - -O filtro é integrado entre as regras de validação e condições, portanto, a ordem dos métodos importa, ou seja, o filtro e a regra são chamados na mesma ordem que os métodos `addFilter()` e `addRule()`. - - -Validação JavaScript -==================== - -A linguagem para formular condições e regras é muito poderosa. Todas as construções funcionam tanto no lado do servidor quanto no lado do JavaScript. Elas são transferidas em atributos HTML `data-nette-rules` como JSON. A validação em si é então realizada por um script que captura o evento `submit` do formulário, percorre os controlos individuais e executa a validação apropriada. - -Esse script é `netteForms.js` e está disponível em várias fontes possíveis: - -Pode inserir o script diretamente na página HTML a partir de um CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ou copiá-lo localmente para a pasta pública do projeto (por exemplo, de `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Ou instalar via [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -E, em seguida, carregar e executar: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternativamente, pode carregá-lo diretamente da pasta `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -JavaScript dinâmico -=================== - -Quer exibir os campos para inserir o endereço apenas se o utilizador escolher enviar o produto pelo correio? Sem problemas. A chave é o par de métodos `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Este código diz que quando a condição é atendida, ou seja, quando a caixa de seleção está marcada, o elemento HTML `#address-container` será visível. E vice-versa. Assim, colocamos os controlos do formulário com o endereço do destinatário num contêiner com este ID e, ao clicar na caixa de seleção, eles serão ocultados ou exibidos. Isso é garantido pelo script `netteForms.js`. - -Como argumento do método `toggle()`, é possível passar qualquer seletor. Por razões históricas, uma string alfanumérica sem outros caracteres especiais é entendida como o ID do elemento, ou seja, da mesma forma que se fosse precedida pelo caractere `#`. O segundo parâmetro opcional permite inverter o comportamento, ou seja, se usássemos `toggle('#address-container', false)`, o elemento seria exibido apenas se a caixa de seleção não estivesse marcada. - -A implementação padrão em JavaScript altera a propriedade `hidden` dos elementos. No entanto, podemos facilmente alterar o comportamento, por exemplo, adicionando uma animação. Basta sobrescrever o método `Nette.toggle` em JavaScript com a sua própria solução: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // ocultamos ou exibimos 'el' de acordo com o valor 'visible' - }); -}; -``` - - -Desativação da validação -======================== - -Às vezes, pode ser útil desativar a validação. Se o pressionamento de um botão de envio não deve realizar a validação (adequado para botões *Cancelar* ou *Visualizar*), desativamo-la com o método `$submit->setValidationScope([])`. Se deve realizar apenas validação parcial, podemos especificar quais campos ou contêineres de formulário devem ser validados. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Valida o formulário inteiro -$form->addSubmit('send2') - ->setValidationScope([]); // Não valida nada -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Valida apenas o controlo name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Valida apenas o controlo age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Valida o contêiner details -``` - -`setValidationScope` não afeta o [#evento onValidate] no formulário, que será chamado sempre. O evento `onValidate` num contêiner será disparado apenas se este contêiner estiver marcado para validação parcial. diff --git a/forms/ro/@home.texy b/forms/ro/@home.texy deleted file mode 100644 index 8890cf4096..0000000000 --- a/forms/ro/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms a revoluționat crearea formularelor web. Dintr-o dată, a fost suficient să scrieți câteva rânduri de cod clare și aveați un formular complet, inclusiv randare, validare JavaScript și pe server, și, în plus, extrem de securizat. Vom arăta cum: - -- să creați formulare prietenoase -- să validați datele trimise -- să randati elementele exact după nevoie - -</div> - - -Utilizând Nette Forms, veți evita o serie întreagă de sarcini de rutină, cum ar fi scrierea validării (în plus, dublă, pe partea de server și client), veți minimiza probabilitatea apariției erorilor și a găurilor de securitate. - -Formularele pot fi utilizate fie ca parte a Nette Application (adică în presenteri), fie complet independent. Deoarece în ambele cazuri utilizarea diferă puțin, am pregătit pentru dvs. două tutoriale: - -<div class="wiki-buttons"> -<div> "Formulare în presenteri .[wiki-button]":in-presenter </div> -<div> "Formulare independent .[wiki-button]":standalone </div> -</div> - - -Instalare ---------- - -Descărcați și instalați biblioteca folosind [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/ro/@left-menu.texy b/forms/ro/@left-menu.texy deleted file mode 100644 index a361d6d5de..0000000000 --- a/forms/ro/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Introducere |@home] -- [Formulare în presenteri|in-presenter] -- [Formulare independent|standalone] -- [Elemente de formular |controls] -- [Validare |validation] -- [Randare |rendering] -- [Configurație |configuration] - - -Lectură suplimentară -******************** -- [Tutoriale și proceduri |best-practices:] diff --git a/forms/ro/@meta.texy b/forms/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/forms/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/forms/ro/configuration.texy b/forms/ro/configuration.texy deleted file mode 100644 index bdbed9ef7a..0000000000 --- a/forms/ro/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Configurarea formularelor -************************* - -.[perex] -În configurație se pot modifica [mesajele de eroare implicite ale formularelor|validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Aici este traducerea în română: - -```neon -forms: - messages: - Equal: 'Introduceți %s.' - NotEqual: 'Această valoare nu ar trebui să fie %s.' - Filled: 'Acest câmp este obligatoriu.' - Blank: 'Acest câmp ar trebui să fie gol.' - MinLength: 'Introduceți cel puțin %d caractere.' - MaxLength: 'Introduceți maximum %d caractere.' - Length: 'Introduceți o valoare între %d și %d caractere.' - Email: 'Introduceți o adresă de e-mail validă.' - URL: 'Introduceți un URL valid.' - Integer: 'Introduceți un număr întreg valid.' - Float: 'Introduceți un număr valid.' - Min: 'Introduceți o valoare mai mare sau egală cu %d.' - Max: 'Introduceți o valoare mai mică sau egală cu %d.' - Range: 'Introduceți o valoare între %d și %d.' - MaxFileSize: 'Dimensiunea fișierului încărcat poate fi de maximum %d octeți.' - MaxPostSize: 'Datele încărcate depășesc limita de %d octeți.' - MimeType: 'Fișierul încărcat nu este în formatul așteptat.' - Image: 'Fișierul încărcat trebuie să fie o imagine în format JPEG, GIF, PNG, WebP sau AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Selectați o opțiune validă.' - Nette\Forms\Controls\UploadControl::Valid: 'A apărut o eroare la încărcarea fișierului.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Sesiunea dvs. a expirat. Vă rugăm să reveniți la pagina principală și să încercați din nou.' -``` - -Dacă nu utilizați întregul framework și deci nici fișierele de configurare, puteți modifica mesajele de eroare implicite direct în array-ul `Nette\Forms\Validator::$messages`. diff --git a/forms/ro/controls.texy b/forms/ro/controls.texy deleted file mode 100644 index 4305128641..0000000000 --- a/forms/ro/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Elemente de formular -******************** - -.[perex] -Prezentare generală a elementelor de formular standard. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Adaugă un câmp text pe o singură linie (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Dacă utilizatorul nu completează câmpul, returnează un șir gol `''`, sau folosind `setNullable()` se poate specifica să returneze `null`. - -```php -$form->addText('name', 'Nume:') - ->setRequired() - ->setNullable(); -``` - -Validează automat UTF-8, elimină spațiile de la început și sfârșit și elimină sfârșiturile de linie pe care un atacator le-ar putea trimite. Parametrul `$cols` este depreciat și nu este utilizat. - -Lungimea maximă poate fi limitată folosind `setMaxLength()`. Modificarea valorii introduse de utilizator este posibilă prin [addFilter() |validation#Modificarea intrării]. - -Folosind `setHtmlType()` se poate schimba caracterul vizual al câmpului text la tipuri precum `search`, `tel` sau `url` vezi [specificație|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Rețineți că schimbarea tipului este doar vizuală și nu înlocuiește funcția de validare. Pentru tipul `url` este recomandat să se adauge o [regulă de validare specifică URL |validation#Intrări de text]. - -.[note] -Pentru alte tipuri de intrări, cum ar fi `number`, `range`, `email`, `date`, `datetime-local`, `time` și `color`, utilizați metode specializate precum [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] și [#addColor], care asigură validarea pe server. Tipurile `month` și `week` nu sunt încă pe deplin suportate în toate browserele. - -Elementului i se poate seta așa-numita empty-value, care este ceva asemănător valorii implicite, dar dacă utilizatorul nu o schimbă, elementul returnează un șir gol sau `null`. - -```php -$form->addText('phone', 'Telefon:') - ->setHtmlType('tel') - ->setEmptyValue('+40'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Adaugă un câmp pentru introducerea textului multilinie (clasa [TextArea |api:Nette\Forms\Controls\TextArea]). Dacă utilizatorul nu completează câmpul, returnează un șir gol `''`, sau folosind `setNullable()` se poate specifica să returneze `null`. - -```php -$form->addTextArea('note', 'Notă:') - ->addRule($form::MaxLength, 'Nota este prea lungă', 10000); -``` - -Validează automat UTF-8 și normalizează separatorii de linie la `\n`. Spre deosebire de câmpul de intrare pe o singură linie, nu are loc nicio eliminare a spațiilor. - -Lungimea maximă poate fi limitată folosind `setMaxLength()`. Modificarea valorii introduse de utilizator este posibilă prin [addFilter() |validation#Modificarea intrării]. Se poate seta așa-numita empty-value folosind `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Adaugă un câmp pentru introducerea unui număr întreg (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Returnează fie un integer, fie `null`, dacă utilizatorul nu introduce nimic. - -```php -$form->addInteger('year', 'An:') - ->addRule($form::Range, 'Anul trebuie să fie în intervalul de la %d la %d.', [1900, 2023]); -``` - -Elementul se randează ca `<input type="number">`. Folosind metoda `setHtmlType()` se poate schimba tipul la `range` pentru afișare sub formă de glisor, sau la `text`, dacă preferați un câmp text standard fără comportamentul special al tipului `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Adaugă un câmp pentru introducerea unui număr zecimal (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Returnează fie un float, fie `null`, dacă utilizatorul nu introduce nimic. - -```php -$form->addFloat('level', 'Nivel:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Nivelul trebuie să fie în intervalul de la %d la %d.', [0, 100]); -``` - -Elementul se randează ca `<input type="number">`. Folosind metoda `setHtmlType()` se poate schimba tipul la `range` pentru afișare sub formă de glisor, sau la `text`, dacă preferați un câmp text standard fără comportamentul special al tipului `number`. - -Nette și browserul Chrome acceptă atât virgula, cât și punctul ca separator zecimal. Pentru ca această funcționalitate să fie disponibilă și în Firefox, se recomandă setarea atributului `lang` fie pentru elementul respectiv, fie pentru întreaga pagină, de exemplu `<html lang="ro">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Adaugă un câmp pentru introducerea unei adrese de e-mail (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Dacă utilizatorul nu completează câmpul, returnează un șir gol `''`, sau folosind `setNullable()` se poate specifica să returneze `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Verifică dacă valoarea este o adresă de e-mail validă. Nu se verifică dacă domeniul există efectiv, se verifică doar sintaxa. Validează automat UTF-8, elimină spațiile de la început și sfârșit. - -Lungimea maximă poate fi limitată folosind `setMaxLength()`. Modificarea valorii introduse de utilizator este posibilă prin [addFilter() |validation#Modificarea intrării]. Se poate seta așa-numita empty-value folosind `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Adaugă un câmp pentru introducerea parolei (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Parametrul `$cols` este depreciat și nu este utilizat. - -```php -$form->addPassword('password', 'Parolă:') - ->setRequired() - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8) - ->addRule($form::Pattern, 'Trebuie să conțină o cifră', '.*[0-9].*'); -``` - -La reafișarea formularului, câmpul va fi gol. Validează automat UTF-8, elimină spațiile de la început și sfârșit și elimină sfârșiturile de linie pe care un atacator le-ar putea trimite. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Adaugă o căsuță de bifat (clasa [Checkbox |api:Nette\Forms\Controls\Checkbox]). Returnează valoarea `true` sau `false`, în funcție de dacă este bifată. - -```php -$form->addCheckbox('agree', 'Sunt de acord cu termenii') - ->setRequired('Este necesar să fiți de acord cu termenii'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Adaugă căsuțe de bifat pentru selectarea mai multor elemente (clasa [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Returnează un array al cheilor elementelor selectate. Metoda `getSelectedItems()` returnează valorile în loc de chei. - -```php -$form->addCheckboxList('colors', 'Culori:', [ - 'r' => 'roșu', - 'g' => 'verde', - 'b' => 'albastru', -]); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. - -Folosind `setDisabled(['r', 'g'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementele selectate sunt într-adevăr unele dintre cele oferite și nu au fost dezactivate. Prin metoda `getRawValue()` se pot obține elementele trimise fără această verificare importantă. - -La setarea elementelor selectate implicit, verifică de asemenea că acestea sunt unele dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - -Dacă trimiteți formularul prin metoda `GET`, puteți alege un mod mai compact de transmitere a datelor, care economisește dimensiunea query string-ului. Se activează prin setarea atributului HTML al formularului: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Adaugă butoane radio (clasa [RadioList |api:Nette\Forms\Controls\RadioList]). Returnează cheia elementului selectat, sau `null`, dacă utilizatorul nu a selectat nimic. Metoda `getSelectedItem()` returnează valoarea în loc de cheie. - -```php -$sex = [ - 'm' => 'bărbat', - 'f' => 'femeie', -]; -$form->addRadioList('gender', 'Sex:', $sex); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. - -Folosind `setDisabled(['m', 'f'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementul selectat este într-adevăr unul dintre cele oferite și nu a fost dezactivat. Prin metoda `getRawValue()` se poate obține elementul trimis fără această verificare importantă. - -La setarea elementului selectat implicit, verifică de asemenea că acesta este unul dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Adaugă un select box (clasa [SelectBox |api:Nette\Forms\Controls\SelectBox]). Returnează cheia elementului selectat, sau `null`, dacă utilizatorul nu a selectat nimic. Metoda `getSelectedItem()` returnează valoarea în loc de cheie. - -```php -$countries = [ - 'RO' => 'România', - 'MD' => 'Moldova', - 'GB' => 'Marea Britanie', -]; - -$form->addSelect('country', 'Țara:', $countries) - ->setDefaultValue('RO'); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. Elementele pot fi și un array bidimensional: - -```php -$countries = [ - 'Europe' => [ - 'RO' => 'România', - 'MD' => 'Moldova', - 'GB' => 'Marea Britanie', - ], - 'CA' => 'Canada', - 'US' => 'SUA', - '?' => 'altă', -]; -``` - -La select box-uri, adesea primul element are o semnificație specială, servește ca îndemn la acțiune. Pentru adăugarea unui astfel de element servește metoda `setPrompt()`. - -```php -$form->addSelect('country', 'Țara:', $countries) - ->setPrompt('Alegeți țara'); -``` - -Folosind `setDisabled(['RO', 'MD'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementul selectat este într-adevăr unul dintre cele oferite și nu a fost dezactivat. Prin metoda `getRawValue()` se poate obține elementul trimis fără această verificare importantă. - -La setarea elementului selectat implicit, verifică de asemenea că acesta este unul dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Adaugă un select box pentru selectarea mai multor elemente (clasa [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Returnează un array al cheilor elementelor selectate. Metoda `getSelectedItems()` returnează valorile în loc de chei. - -```php -$form->addMultiSelect('countries', 'Țări:', $countries); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. Elementele pot fi și un array bidimensional. - -Folosind `setDisabled(['RO', 'MD'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementele selectate sunt într-adevăr unele dintre cele oferite și nu au fost dezactivate. Prin metoda `getRawValue()` se pot obține elementele trimise fără această verificare importantă. - -La setarea elementelor selectate implicit, verifică de asemenea că acestea sunt unele dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Adaugă un câmp pentru încărcarea fișierului (clasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returnează un obiect [FileUpload |http:request#FileUpload], chiar și în cazul în care utilizatorul nu a trimis niciun fișier, ceea ce se poate verifica prin metoda `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Avatarul trebuie să fie JPEG, PNG, GIF, WebP sau AVIF.') - ->addRule($form::MaxFileSize, 'Dimensiunea maximă este 1 MB.', 1024 * 1024); -``` - -Dacă fișierul nu reușește să se încarce corect, formularul nu este trimis cu succes și se afișează o eroare. Adică, la trimiterea cu succes nu este nevoie să se verifice metoda `FileUpload::isOk()`. - -Nu aveți niciodată încredere în numele original al fișierului returnat de metoda `FileUpload::getName()`, clientul ar fi putut trimite un nume de fișier malițios cu intenția de a deteriora sau hackui aplicația dvs. - -Regulile `MimeType` și `Image` detectează tipul solicitat pe baza semnăturii fișierului și nu verifică integritatea acestuia. Dacă imaginea nu este deteriorată se poate verifica, de exemplu, prin încercarea de a o [încărca |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Adaugă un câmp pentru încărcarea mai multor fișiere simultan (clasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returnează un array de obiecte [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` pentru fiecare dintre ele va returna `true`. - -```php -$form->addMultiUpload('files', 'Fișiere:') - ->addRule($form::MaxLength, 'Maxim se pot încărca %d fișiere', 10); -``` - -Dacă vreun fișier nu reușește să se încarce corect, formularul nu este trimis cu succes și se afișează o eroare. Adică, la trimiterea cu succes nu este nevoie să se verifice metoda `FileUpload::isOk()`. - -Nu aveți niciodată încredere în numele originale ale fișiierelor returnate de metoda `FileUpload::getName()`, clientul ar fi putut trimite un nume de fișier malițios cu intenția de a deteriora sau hackui aplicația dvs. - -Regulile `MimeType` și `Image` detectează tipul solicitat pe baza semnăturii fișierului și nu verifică integritatea acestuia. Dacă imaginea nu este deteriorată se poate verifica, de exemplu, prin încercarea de a o [încărca |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Adaugă un câmp care permite utilizatorului să introducă ușor o dată formată din an, lună și zi (clasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ca valoare implicită acceptă fie obiecte care implementează interfața `DateTimeInterface`, un șir cu timpul, fie un număr reprezentând timestamp UNIX. Același lucru este valabil și pentru argumentele regulilor `Min`, `Max` sau `Range`, care definesc data minimă și maximă permisă. - -```php -$form->addDate('date', 'Data:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Data trebuie să fie cu cel puțin o lună în urmă.', new DateTime('-1 month')); -``` - -Standard returnează un obiect `DateTimeImmutable`, prin metoda `setFormat()` puteți specifica [formatul text|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] sau timestamp: - -```php -$form->addDate('date', 'Data:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Adaugă un câmp care permite utilizatorului să introducă ușor un timp format din ore, minute și opțional secunde (clasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ca valoare implicită acceptă fie obiecte care implementează interfața `DateTimeInterface`, un șir cu timpul, fie un număr reprezentând timestamp UNIX. Din aceste intrări este utilizată doar informația de timp, data este ignorată. Același lucru este valabil și pentru argumentele regulilor `Min`, `Max` sau `Range`, care definesc timpul minim și maxim permis. Dacă valoarea minimă setată este mai mare decât cea maximă, se creează un interval de timp care depășește miezul nopții. - -```php -$form->addTime('time', 'Ora:', withSeconds: true) - ->addRule($form::Range, 'Ora trebuie să fie în intervalul de la %d la %d.', ['12:30', '13:30']); -``` - -Standard returnează un obiect `DateTimeImmutable` (cu data 1 ianuarie anul 1), prin metoda `setFormat()` puteți specifica [formatul text|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Ora:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Adaugă un câmp care permite utilizatorului să introducă ușor data și ora formate din an, lună, zi, ore, minute și opțional secunde (clasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ca valoare implicită acceptă fie obiecte care implementează interfața `DateTimeInterface`, un șir cu timpul, fie un număr reprezentând timestamp UNIX. Același lucru este valabil și pentru argumentele regulilor `Min`, `Max` sau `Range`, care definesc data minimă și maximă permisă. - -```php -$form->addDateTime('datetime', 'Data și ora:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Data trebuie să fie cu cel puțin o lună în urmă.', new DateTime('-1 month')); -``` - -Standard returnează un obiect `DateTimeImmutable`, prin metoda `setFormat()` puteți specifica [formatul text|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] sau timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Adaugă un câmp pentru selectarea culorii (clasa [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Culoarea este un șir în formatul `#rrggbb`. Dacă utilizatorul nu face nicio selecție, se returnează culoarea neagră `#000000`. - -```php -$form->addColor('color', 'Culoare:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Adaugă un câmp ascuns (clasa [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Folosind `setNullable()` se poate seta să returneze `null` în loc de șir gol. Modificarea valorii trimise este posibilă prin [addFilter() |validation#Modificarea intrării]. - -Deși elementul este ascuns, este **important să rețineți** că valoarea poate fi totuși modificată sau falsificată de un atacator. Verificați și validați întotdeauna cu atenție toate valorile primite pe partea serverului pentru a preveni riscurile de securitate asociate cu manipularea datelor. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Adaugă un buton de trimitere (clasa [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Trimite'); -``` - -În formular este posibil să aveți și mai multe butoane de trimitere: - -```php -$form->addSubmit('register', 'Înregistrează-te'); -$form->addSubmit('cancel', 'Anulează'); -``` - -Pentru a afla pe care dintre ele s-a făcut clic, utilizați: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Dacă nu doriți să validați întregul formular la apăsarea butonului (de exemplu, la butoanele *Anulează* sau *Previzualizare*), utilizați [setValidationScope() |validation#Dezactivarea validării]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Adaugă un buton (clasa [Button |api:Nette\Forms\Controls\Button]), care nu are funcție de trimitere. Poate fi deci utilizat pentru o altă funcție, de ex. apelarea unei funcții JavaScript la clic. - -```php -$form->addButton('raise', 'Mărește salariul') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Adaugă un buton de trimitere sub formă de imagine (clasa [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -La utilizarea mai multor butoane de trimitere se poate afla pe care s-a făcut clic folosind `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Adaugă un subformular (clasa [Container|api:Nette\Forms\Container]), adică un container, în care se pot adăuga alte elemente în același mod în care le adăugăm în formular. Funcționează și metodele `setDefaults()` sau `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Numele dvs.:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Numele dvs.:'); -$sub2->addEmail('email', 'Email:'); -``` - -Datele trimise le returnează apoi ca o structură multidimensională: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Prezentare generală a setărilor -=============================== - -La toate elementele putem apela următoarele metode (prezentare completă în [documentația API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | setează valoarea implicită -| `getValue()` | obține valoarea curentă -| `setOmitted()` | [#Omiterea valorii] -| `setDisabled()` | [#Dezactivarea elementelor] - -Randare: -.[table-form-methods language-php] -| `setCaption($caption)` | schimbă eticheta elementului -| `setTranslator($translator)` | setează [traducătorul |rendering#Traducere] -| `setHtmlAttribute($name, $value)` | setează [atributul HTML |rendering#Atribute HTML] al elementului -| `setHtmlId($id)` | setează atributul HTML `id` -| `setHtmlType($type)` | setează atributul HTML `type` -| `setHtmlName($name)` | setează atributul HTML `name` -| `setOption($key, $value)` | [opțiuni de randare |rendering#Opțiuni] - -Validare: -.[table-form-methods language-php] -| `setRequired()` | [element obligatoriu |validation] -| `addRule()` | setarea [regulii de validare |validation#Reguli] -| `addCondition()`, `addConditionOn()` | setează [condiția de validare |validation#Condiții] -| `addError($message)` | [transmiterea mesajului de eroare |validation#Erori în timpul procesării] - -La elementele `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` se pot apela următoarele metode: - -.[table-form-methods language-php] -| `setNullable()` | setează dacă getValue() va returna `null` în loc de șir gol -| `setEmptyValue($value)` | setează o valoare specială care este considerată șir gol -| `setMaxLength($length)` | setează numărul maxim de caractere permise -| `addFilter($filter)` | [modificarea intrării |validation#Modificarea intrării] - - -Omiterea valorii -================ - -Dacă valoarea completată de utilizator nu ne interesează, o putem omite din rezultatul metodei `$form->getValues()` sau din datele transmise handlerilor folosind `setOmitted()`. Acest lucru este util pentru diverse parole de verificare, elemente antispam etc. - -```php -$form->addPassword('passwordVerify', 'Parola pentru verificare:') - ->setRequired('Vă rugăm introduceți parola încă o dată pentru verificare') - ->addRule($form::Equal, 'Parolele nu se potrivesc', $form['password']) - ->setOmitted(); -``` - - -Dezactivarea elementelor -======================== - -Elementele pot fi dezactivate folosind `setDisabled()`. Un astfel de element nu poate fi editat de utilizator. - -```php -$form->addText('username', 'Nume utilizator:') - ->setDisabled(); -``` - -Elementele dezactivate nu sunt trimise deloc de browser către server, deci nu le veți găsi nici în datele returnate de funcția `$form->getValues()`. Dacă însă setați `setOmitted(false)`, Nette va include în aceste date valoarea lor implicită. - -La apelarea `setDisabled()`, din motive de securitate **se șterge valoarea elementului**. Dacă setați o valoare implicită, este necesar să o faceți după dezactivarea acestuia: - -```php -$form->addText('username', 'Nume utilizator:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -O alternativă la elementele dezactivate sunt elementele cu atributul HTML `readonly`, pe care browserul le trimite la server. Deși elementul este doar pentru citire, este **important să rețineți** că valoarea sa poate fi totuși modificată sau falsificată de un atacator. - - -Elemente personalizate -====================== - -Pe lângă gama largă de elemente de formular încorporate, puteți adăuga elemente personalizate în formular în acest mod: - -```php -$form->addComponent(new DateInput('Data:'), 'date'); -// sintaxă alternativă: $form['date'] = new DateInput('Data:'); -``` - -.[note] -Formularul este un descendent al clasei [Container |component-model:#Container], iar elementele individuale sunt descendenți ai [Component |component-model:#Component]. - -Există o modalitate de a defini noi metode ale formularului care servesc la adăugarea elementelor personalizate (de ex. `$form->addZip()`). Este vorba de așa-numitele extension methods. Dezavantajul este că sugestiile din editori nu vor funcționa pentru ele. - -```php -use Nette\Forms\Container; - -// adăugăm metoda addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Cel puțin 5 cifre', '[0-9]{5}'); -}); - -// utilizare -$form->addZip('zip', 'Cod poștal:'); -``` - - -Elemente de nivel scăzut -======================== - -Se pot utiliza și elemente pe care le scriem doar în șablon și nu le adăugăm în formular prin una dintre metodele `$form->addXyz()`. De exemplu, când afișăm înregistrări din baza de date și nu știm dinainte câte vor fi și ce ID-uri vor avea, și dorim să afișăm la fiecare rând un checkbox sau un radio button, este suficient să îl codăm în șablon: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -Și după trimitere aflăm valoarea: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -unde primul parametru este tipul elementului (`DataFile` pentru `type=file`, `DataLine` pentru intrări pe o singură linie precum `text`, `password`, `email` etc. și `DataText` pentru toate celelalte) iar al doilea parametru `sel[]` corespunde atributului HTML name. Tipul elementului îl putem combina cu valoarea `DataKeys`, care păstrează cheile elementelor. Acest lucru este util în special pentru `select`, `radioList` și `checkboxList`. - -Esențial este că `getHttpData()` returnează o valoare sanitarizată, în acest caz va fi întotdeauna un array de șiruri UTF-8 valide, indiferent ce ar încerca un atacator să strecoare serverului. Este o analogie a lucrului direct cu `$_POST` sau `$_GET`, dar cu diferența esențială că returnează întotdeauna date curate, așa cum sunteți obișnuiți la elementele standard ale formularelor Nette. diff --git a/forms/ro/in-presenter.texy b/forms/ro/in-presenter.texy deleted file mode 100644 index 98aed25f44..0000000000 --- a/forms/ro/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Formulare în presentere -*********************** - -.[perex] -Nette Forms facilitează enorm crearea și procesarea formularelor web. În acest capitol, veți învăța cum să utilizați formularele în interiorul presenterelor. - -Dacă sunteți interesat de cum să le utilizați complet independent, fără restul framework-ului, ghidul pentru [utilizare independentă|standalone] este pentru dumneavoastră. - - -Primul formular -=============== - -Să încercăm să scriem un formular simplu de înregistrare. Codul său va fi următorul: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Nume:'); -$form->addPassword('password', 'Parolă:'); -$form->addSubmit('send', 'Înregistrează-te'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -și în browser se va afișa astfel: - -[* form-cs.webp *] - -Formularul în presenter este un obiect al clasei `Nette\Application\UI\Form`, predecesorul său `Nette\Forms\Form` este destinat utilizării independente. Am adăugat în el elementele numite nume, parolă și un buton de trimitere. Și în final, linia cu `$form->onSuccess` spune că după trimitere și validare reușită, trebuie apelată metoda `$this->formSucceeded()`. - -Din perspectiva presenterului, formularul este o componentă obișnuită. Prin urmare, este tratat ca o componentă și îl vom integra în presenter folosind [metode factory |application:components#Metode factory]. Va arăta astfel: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Nume:'); - $form->addPassword('password', 'Parolă:'); - $form->addSubmit('send', 'Înregistrează-te'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // aici procesăm datele trimise de formular - // $data->name conține numele - // $data->password conține parola - $this->flashMessage('Ați fost înregistrat cu succes.'); - $this->redirect('Home:'); - } -} -``` - -Și în șablon, randăm formularul cu tag-ul `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Înregistrare</h1> - -{control registrationForm} -``` - -Și asta e tot :-) Avem un formular funcțional și perfect [securizat |#Protecția împotriva vulnerabilităților]. - -Și acum probabil vă gândiți că a fost prea rapid, vă întrebați cum este posibil să fie apelată metoda `formSucceeded()` și care sunt parametrii pe care îi primește. Sigur, aveți dreptate, acest lucru merită o explicație. - -Nette vine cu un mecanism proaspăt, pe care îl numim [Stil Hollywood |application:components#Stilul Hollywood]. În loc ca dumneavoastră, ca dezvoltator, să trebuiască să întrebați constant dacă s-a întâmplat ceva („a fost trimis formularul?”, „a fost trimis valid?” și „nu a fost falsificat?”), spuneți framework-ului „când formularul este completat valid, apelează această metodă” și lăsați restul muncii pe seama lui. Dacă programați în JavaScript, acest stil de programare vă este familiar. Scrieți funcții care sunt apelate atunci când are loc un anumit [eveniment |nette:glossary#Evenimente]. Și limbajul le transmite argumentele corespunzătoare. - -Exact așa este construit și codul presenterului de mai sus. Array-ul `$form->onSuccess` reprezintă o listă de callback-uri PHP, pe care Nette le apelează în momentul în care formularul este trimis și completat corect (adică este valid). În cadrul [ciclului de viață al presenterului |application:presenters#Ciclul de viață al presenterului], este vorba despre un așa-numit semnal, deci sunt apelate după metoda `action*` și înainte de metoda `render*`. Și fiecărui callback îi transmite ca prim parametru formularul însuși și ca al doilea, datele trimise sub forma unui obiect [ArrayHash |utils:arrays#ArrayHash]. Primul parametru poate fi omis dacă nu aveți nevoie de obiectul formularului. Iar al doilea parametru poate fi mai inteligent, dar despre asta [mai târziu |#Maparea la clase]. - -Obiectul `$data` conține proprietățile `name` și `password` cu datele completate de utilizator. De obicei, trimitem datele direct pentru procesare ulterioară, ceea ce poate fi, de exemplu, inserarea în baza de date. În timpul procesării, însă, poate apărea o eroare, de exemplu, numele de utilizator este deja ocupat. În acest caz, transmitem eroarea înapoi în formular folosind `addError()` și îl lăsăm să fie randat din nou, inclusiv cu mesajul de eroare. - -```php -$form->addError('Ne pare rău, numele de utilizator este deja folosit de altcineva.'); -``` - -Pe lângă `onSuccess`, mai există și `onSubmit`: callback-urile sunt apelate întotdeauna după trimiterea formularului, chiar și atunci când nu este completat corect. Și, de asemenea, `onError`: callback-urile sunt apelate doar dacă trimiterea nu este validă. Sunt apelate chiar și atunci când în `onSuccess` sau `onSubmit` invalidăm formularul folosind `addError()`. - -După procesarea formularului, redirecționăm către pagina următoare. Acest lucru previne retrimiterea nedorită a formularului prin butonul *reîmprospătare*, *înapoi* sau prin navigarea în istoricul browserului. - -Încercați să adăugați și alte [elemente de formular|controls]. - - -Accesul la elemente -=================== - -Formularul este o componentă a presenterului, în cazul nostru numită `registrationForm` (după numele metodei factory `createComponentRegistrationForm`), așa că oriunde în presenter puteți accesa formularul folosind: - -```php -$form = $this->getComponent('registrationForm'); -// sintaxă alternativă: $form = $this['registrationForm']; -``` - -Componentele sunt și elementele individuale ale formularului, de aceea le puteți accesa în același mod: - -```php -$input = $form->getComponent('name'); // sau $input = $form['name']; -$button = $form->getComponent('send'); // sau $button = $form['send']; -``` - -Elementele se elimină folosind `unset`: - -```php -unset($form['name']); -``` - - -Reguli de validare -================== - -S-a menționat cuvântul *valid*, dar formularul nu are încă nicio regulă de validare. Să remediem acest lucru. - -Numele va fi obligatoriu, de aceea îl marcăm cu metoda `setRequired()`, al cărei argument este textul mesajului de eroare care se afișează dacă utilizatorul nu completează numele. Dacă nu specificăm argumentul, se va folosi mesajul de eroare implicit. - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să introduceți numele'); -``` - -Încercați să trimiteți formularul fără a completa numele și veți vedea că se afișează un mesaj de eroare, iar browserul sau serverul îl va respinge până când completați câmpul. - -În același timp, nu puteți păcăli sistemul scriind, de exemplu, doar spații în câmp. Nici vorbă. Nette elimină automat spațiile de la începutul și sfârșitul șirului. Încercați. Este un lucru pe care ar trebui să-l faceți întotdeauna cu fiecare input de o singură linie, dar adesea se uită. Nette o face automat. (Puteți încerca să păcăliți formularul și să trimiteți un șir multilinie ca nume. Nici aici Nette nu se lasă păcălit și transformă sfârșiturile de rând în spații.) - -Formularul este întotdeauna validat pe partea de server, dar se generează și o validare JavaScript, care se execută instantaneu, iar utilizatorul află despre eroare imediat, fără a fi nevoie să trimită formularul la server. Acest lucru este gestionat de scriptul `netteForms.js`. Inserați-l în șablonul de layout: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Dacă vă uitați la codul sursă al paginii cu formularul, puteți observa că Nette inserează elementele obligatorii în elemente cu clasa CSS `required`. Încercați să adăugați următorul stil în șablon și eticheta „Nume” va fi roșie. Astfel, marcăm elegant elementele obligatorii pentru utilizatori: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Alte reguli de validare le adăugăm cu metoda `addRule()`. Primul parametru este regula, al doilea este din nou textul mesajului de eroare și poate urma un argument al regulii de validare. Ce înseamnă asta? - -Vom extinde formularul cu un nou câmp opțional „vârstă”, care trebuie să fie un număr întreg (`addInteger()`) și, în plus, într-un interval permis (`$form::Range`). Și aici vom folosi al treilea parametru al metodei `addRule()`, prin care transmitem validatorului intervalul dorit ca o pereche `[de la, până la]`: - -```php -$form->addInteger('age', 'Vârsta:') - ->addRule($form::Range, 'Vârsta trebuie să fie între 18 și 120', [18, 120]); -``` - -.[tip] -Dacă utilizatorul nu completează câmpul, regulile de validare nu vor fi verificate, deoarece elementul este opțional. - -Aici apare spațiu pentru o mică refactorizare. În mesajul de eroare și în al treilea parametru, numerele sunt menționate duplicat, ceea ce nu este ideal. Dacă am crea [formulare multilingve |rendering#Traducere] și mesajul care conține numere ar fi tradus în mai multe limbi, o eventuală modificare a valorilor ar fi dificilă. Din acest motiv, este posibil să folosim substituenți `%d` și Nette va completa valorile: - -```php - ->addRule($form::Range, 'Vârsta trebuie să fie între %d și %d ani', [18, 120]); -``` - -Să ne întoarcem la elementul `password`, pe care îl vom face, de asemenea, obligatoriu și vom verifica lungimea minimă a parolei (`$form::MinLength`), din nou folosind substituentul: - -```php -$form->addPassword('password', 'Parolă:') - ->setRequired('Alegeți o parolă') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -Adăugăm în formular și câmpul `passwordVerify`, unde utilizatorul introduce parola încă o dată, pentru verificare. Folosind regulile de validare, verificăm dacă ambele parole sunt identice (`$form::Equal`). Și ca parametru, dăm o referință la prima parolă folosind [paranteze drepte |#Accesul la elemente]: - -```php -$form->addPassword('passwordVerify', 'Parola pentru verificare:') - ->setRequired('Vă rugăm introduceți parola încă o dată pentru verificare') - ->addRule($form::Equal, 'Parolele nu se potrivesc', $form['password']) - ->setOmitted(); -``` - -Cu `setOmitted()`, am marcat elementul a cărui valoare nu ne interesează de fapt și care există doar în scopul validării. Valoarea nu se transmite în `$data`. - -Astfel, avem un formular complet funcțional cu validare în PHP și JavaScript. Capacitățile de validare ale Nette sunt mult mai largi, se pot crea condiții, se pot afișa și ascunde părți ale paginii în funcție de acestea etc. Veți afla totul în capitolul despre [validarea formularelor|validation]. - - -Valori implicite -================ - -Elementelor formularului le setăm în mod obișnuit valori implicite: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Adesea este util să setăm valori implicite pentru toate elementele simultan. De exemplu, când formularul servește pentru editarea înregistrărilor. Citim înregistrarea din baza de date și setăm valorile implicite: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Apelați `setDefaults()` după definirea elementelor. - - -Randarea formularului -===================== - -În mod standard, formularul se randează ca un tabel. Elementele individuale respectă regula de bază a accesibilității - toate etichetele sunt scrise ca `<label>` și legate de elementul de formular corespunzător. La clic pe etichetă, cursorul apare automat în câmpul formularului. - -Fiecărui element îi putem seta atribute HTML arbitrare. De exemplu, adăugăm un placeholder: - -```php -$form->addInteger('age', 'Vârsta:') - ->setHtmlAttribute('placeholder', 'Vă rugăm să completați vârsta'); -``` - -Există într-adevăr o mare varietate de moduri de a randa un formular, așa că există un [capitol separat despre randare|rendering] dedicat acestui subiect. - - -Maparea la clase -================ - -Să ne întoarcem la metoda `formSucceeded()`, care în al doilea parametru `$data` primește datele trimise ca obiect `ArrayHash`. Deoarece este o clasă generică, ceva de genul `stdClass`, ne va lipsi un anumit confort în lucrul cu ea, cum ar fi sugerarea proprietăților în editori sau analiza statică a codului. Acest lucru ar putea fi rezolvat având o clasă specifică pentru fiecare formular, ale cărei proprietăți reprezintă elementele individuale. De ex.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativ, puteți utiliza constructorul: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Proprietățile clasei de date pot fi, de asemenea, enum-uri și vor fi mapate automat. .{data-version:3.2.4} - -Cum să spunem Nette să ne returneze datele ca obiecte ale acestei clase? Mai ușor decât credeți. Este suficient doar să specificați clasa ca tip al parametrului `$data` în metoda handler: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data este o instanță a RegistrationFormData - $name = $data->name; - // ... -} -``` - -Ca tip se poate specifica și `array` și atunci datele vor fi transmise ca array. - -În mod similar, se poate utiliza și metoda `getValues()`, căreia îi transmitem numele clasei sau obiectul de hidratat ca parametru: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Dacă formularele formează o structură multinivel compusă din containere, creați o clasă separată pentru fiecare: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Maparea va recunoaște apoi din tipul proprietății `$person` că trebuie să mapeze containerul la clasa `PersonFormData`. Dacă proprietatea ar conține un array de containere, specificați tipul `array` și transmiteți clasa pentru mapare direct containerului: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Puteți genera designul clasei de date a formularului folosind metoda `Nette\Forms\Blueprint::dataClass($form)`, care o va afișa în pagina browserului. Apoi, este suficient să selectați codul cu un clic și să-l copiați în proiect. .{data-version:3.1.15} - - -Mai multe butoane -================= - -Dacă formularul are mai mult de un buton, de obicei trebuie să distingem care dintre ele a fost apăsat. Putem crea o funcție handler proprie pentru fiecare buton. O setăm ca handler pentru [evenimentul |nette:glossary#Evenimente] `onClick`: - -```php -$form->addSubmit('save', 'Salvează') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Șterge') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Acești handleri sunt apelați doar în cazul unui formular completat valid, la fel ca în cazul evenimentului `onSuccess`. Diferența este că, în loc de formular, ca prim parametru se poate transmite butonul de trimitere, depinde de tipul pe care îl specificați: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Când formularul este trimis cu tasta <kbd>Enter</kbd>, se consideră ca și cum ar fi fost trimis cu primul buton. - - -Evenimentul onAnchor -==================== - -Când construim formularul în metoda factory (cum ar fi `createComponentRegistrationForm`), acesta nu știe încă dacă a fost trimis, nici cu ce date. Dar există cazuri în care avem nevoie să cunoaștem valorile trimise, de exemplu, forma ulterioară a formularului depinde de ele, sau avem nevoie de ele pentru selectbox-uri dependente etc. - -O parte a codului care construiește formularul poate fi, prin urmare, lăsată să fie apelată doar în momentul în care este așa-numit ancorat, adică este deja conectat la presenter și cunoaște datele sale trimise. Un astfel de cod îl transmitem în array-ul `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Țara:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Oraș:'); - -$form->onAnchor[] = function () use ($country, $city) { - // această funcție va fi apelată doar când formularul știe dacă a fost trimis și cu ce date - // deci se poate folosi metoda getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Protecția împotriva vulnerabilităților -====================================== - -Nette Framework pune un accent deosebit pe securitate și, prin urmare, acordă o atenție deosebită securizării formularelor. Face acest lucru complet transparent și nu necesită setări manuale. - -Pe lângă faptul că protejează formularele împotriva atacurilor [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] și [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], realizează o mulțime de mici măsuri de securitate la care nu mai trebuie să vă gândiți. - -De exemplu, filtrează toate caracterele de control din intrări și verifică validitatea codificării UTF-8, astfel încât datele din formular vor fi întotdeauna curate. La select box-uri și liste radio, verifică dacă elementele selectate au fost într-adevăr dintre cele oferite și nu a avut loc o falsificare. Am menționat deja că la intrările text de o singură linie elimină caracterele de sfârșit de rând, pe care un atacator le-ar fi putut trimite. La intrările multilinie, normalizează caracterele de sfârșit de rând. Și așa mai departe. - -Nette rezolvă pentru dumneavoastră riscurile de securitate despre care mulți programatori nici nu bănuiesc că există. - -Atacul CSRF menționat constă în faptul că atacatorul atrage victima pe o pagină care execută discret în browserul victimei o cerere către serverul pe care victima este autentificată, iar serverul crede că cererea a fost executată de victimă din proprie voință. De aceea, Nette împiedică trimiterea formularului POST de pe un alt domeniu. Dacă, din anumite motive, doriți să dezactivați protecția și să permiteți trimiterea formularului de pe un alt domeniu, utilizați: - -```php -$form->allowCrossOrigin(); // ATENȚIE! Dezactivează protecția! -``` - -Această protecție utilizează un cookie SameSite numit `_nss`. Protecția prin cookie SameSite poate să nu fie 100% fiabilă, de aceea este recomandat să activați și protecția prin token: - -```php -$form->addProtection(); -``` - -Recomandăm protejarea în acest mod a formularelor din partea de administrare a site-ului, care modifică date sensibile în aplicație. Framework-ul se apără împotriva atacului CSRF prin generarea și verificarea unui token de autorizare, care se stochează în sesiune (session). De aceea, este necesar ca sesiunea să fie deschisă înainte de afișarea formularului. În partea de administrare a site-ului, de obicei, sesiunea este deja pornită datorită autentificării utilizatorului. Altfel, porniți sesiunea cu metoda `Nette\Http\Session::start()`. - - -Același formular în mai multe presentere -======================================== - -Dacă aveți nevoie să utilizați același formular în mai multe presentere, vă recomandăm să creați o fabrică pentru acesta, pe care apoi să o transmiteți presenterului. O locație potrivită pentru o astfel de clasă este, de exemplu, directorul `app/Forms`. - -Clasa fabrică poate arăta, de exemplu, astfel: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nume:'); - $form->addSubmit('send', 'Conectează-te'); - return $form; - } -} -``` - -Solicităm clasei să producă formularul în metoda factory pentru componente din presenter: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // putem modifica formularul, aici de exemplu schimbăm eticheta butonului - $form['send']->setCaption('Continuă'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // și adăugăm handler - return $form; -} -``` - -Handlerul pentru procesarea formularului poate fi, de asemenea, furnizat deja din fabrică: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nume:'); - $form->addSubmit('send', 'Conectează-te'); - $form->onSuccess[] = function (Form $form, $data): void { - // aici efectuăm procesarea formularului - }; - return $form; - } -} -``` - -Așadar, am parcurs o introducere rapidă în formularele din Nette. Încercați să vă uitați și în directorul [examples|https://github.com/nette/forms/tree/master/examples] din distribuție, unde veți găsi mai multă inspirație. diff --git a/forms/ro/rendering.texy b/forms/ro/rendering.texy deleted file mode 100644 index 21ed2448b7..0000000000 --- a/forms/ro/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Randarea formularelor -********************* - -Aspectul formularelor poate fi foarte divers. În practică, putem întâlni două extreme. Pe de o parte, există nevoia de a randa în aplicație o serie de formulare care sunt vizual asemănătoare ca două picături de apă și apreciem randarea ușoară fără șablon folosind `$form->render()`. Acesta este de obicei cazul interfețelor de administrare. - -Pe de altă parte, există formulare diverse, unde regula este: fiecare piesă este originală. Forma lor este cel mai bine descrisă folosind limbajul HTML în șablonul formularului. Și, desigur, pe lângă cele două extreme menționate, vom întâlni multe formulare care se situează undeva între. - - -Randarea cu Latte -================= - -[Sistemul de șabloane Latte|latte:] facilitează fundamental randarea formularelor și a elementelor acestora. Mai întâi, vom arăta cum să randăm formularele manual, element cu element, obținând astfel control deplin asupra codului. Mai târziu, vom arăta cum se poate [automatiza |#Randare automată] o astfel de randare. - -Puteți genera designul șablonului Latte al formularului folosind metoda `Nette\Forms\Blueprint::latte($form)`, care îl va afișa în pagina browserului. Apoi, este suficient să selectați codul cu un clic și să-l copiați în proiect. .{data-version:3.1.15} - - -`{control}` ------------ - -Cel mai simplu mod de a randa un formular este să scrieți în șablon: - -```latte -{control signInForm} -``` - -Puteți influența aspectul formularului randat astfel prin configurarea [Rendererului |#Renderer] și a [elementelor individuale |#Atribute HTML]. - - -`n:name` --------- - -Definirea formularului în codul PHP poate fi extrem de ușor legată de codul HTML. Este suficient doar să adăugați atributele `n:name`. Atât de simplu este! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Aveți control deplin asupra aspectului codului HTML rezultat. Dacă utilizați atributul `n:name` la elementele `<select>`, `<button>` sau `<textarea>`, conținutul lor intern se va completa automat. Tag-ul `<form n:name>` creează, în plus, o variabilă locală `$form` cu obiectul formularului randat, iar tag-ul de închidere `</form>` randează toate elementele hidden nerandate (același lucru este valabil și pentru `{form} ... {/form}`). - -Nu trebuie însă să uităm de randarea posibilelor mesaje de eroare. Atât cele care au fost adăugate la elementele individuale prin metoda `addError()` (folosind `{inputError}`), cât și cele adăugate direct la formular (returnate de `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Elementele de formular mai complexe, cum ar fi RadioList sau CheckboxList, pot fi randate astfel, element cu element: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Nu doriți să vă gândiți la fiecare element ce element HTML să folosiți pentru el în șablon, dacă `<input>`, `<textarea>` etc.? Soluția este tag-ul universal `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Dacă formularul utilizează un translator, textul din interiorul tag-urilor `{label}` va fi tradus. - -Chiar și în acest caz, elementele de formular mai complexe, cum ar fi RadioList sau CheckboxList, pot fi randate element cu element: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Pentru a randa doar `<input>` în elementul Checkbox, utilizați `{input myCheckbox:}`. Atributele HTML în acest caz separați-le întotdeauna cu virgulă `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Afișează mesajul de eroare pentru elementul formularului, dacă are unul. Mesajul este de obicei încapsulat într-un element HTML pentru stilizare. Puteți preveni randarea elementului gol, dacă nu există mesaj, elegant folosind `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Putem verifica prezența unei erori cu metoda `hasErrors()` și, în funcție de aceasta, seta clasa elementului părinte: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Tag-urile `{form signInForm}...{/form}` sunt o alternativă la `<form n:name="signInForm">...</form>`. - - -Randare automată ----------------- - -Datorită tag-urilor `{input}` și `{label}`, putem crea cu ușurință un șablon generic pentru orice formular. Acesta va itera și va randa succesiv toate elementele sale, cu excepția elementelor hidden, care se randează automat la închiderea formularului cu tag-ul `</form>`. Numele formularului randat va fi așteptat în variabila `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Tag-urile pereche auto-închise `{label .../}` utilizate afișează etichetele provenite din definirea formularului în codul PHP. - -Salvați acest șablon generic, de exemplu, în fișierul `basic-form.latte` și pentru a randa formularul, este suficient să-l includeți și să transmiteți numele (sau instanța) formularului în parametrul `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Dacă doriți să interveniți în aspectul unui anumit formular în timpul randării și, de exemplu, să randați un element diferit, cea mai simplă cale este să pregătiți blocuri în șablon, care vor putea fi ulterior suprascrise. Blocurile pot avea și [nume dinamice |latte:template-inheritance#Denumiri dinamice de blocuri], astfel încât se poate insera în ele și numele elementului randat. De exemplu: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Pentru elementul, de exemplu, `username`, se va crea astfel blocul `input-username`, care poate fi ușor suprascris folosind tag-ul [{embed} |latte:template-inheritance#Moștenirea unitară embed]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternativ, întregul conținut al șablonului `basic-form.latte` poate fi [definit |latte:template-inheritance#Definiții define] ca un bloc, inclusiv parametrul `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Datorită acestui fapt, apelarea sa va fi puțin mai simplă: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -Blocul trebuie importat într-un singur loc, și anume la începutul șablonului de layout: - -```latte -{import basic-form.latte} -``` - - -Cazuri speciale ---------------- - -Dacă aveți nevoie să randați doar partea interioară a formularului fără tag-urile HTML `<form>`, de exemplu la trimiterea snippet-urilor, ascundeți-le folosind atributul `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Tag-ul `{formContainer}` ajută la randarea elementelor din interiorul containerului formularului. - -```latte -<p>Ce știri doriți să primiți:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Randare fără Latte -================== - -Cel mai simplu mod de a randa un formular este să apelați: - -```php -$form->render(); -``` - -Puteți influența aspectul formularului randat astfel prin configurarea [Rendererului |#Renderer] și a [elementelor individuale |#Atribute HTML]. - - -Randare manuală ---------------- - -Fiecare element de formular dispune de metode care generează codul HTML al câmpului formularului și al etichetei. Îl pot returna fie ca șir, fie ca obiect [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` returnează codul HTML al elementului -- `getLabel($caption = null): Html|string|null` returnează codul HTML al etichetei, dacă există - -Formularul poate fi astfel randat element cu element: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -În timp ce la unele elemente `getControl()` returnează un singur element HTML (de ex. `<input>`, `<select>` etc.), la altele returnează o întreagă bucată de cod HTML (CheckboxList, RadioList). Într-un astfel de caz, puteți utiliza metode care generează input-uri și etichete individuale, pentru fiecare element în parte: - -- `getControlPart($key = null): ?Html` returnează codul HTML al unui element -- `getLabelPart($key = null): ?Html` returnează codul HTML al etichetei unui element - -.[note] -Aceste metode au din motive istorice prefixul `get`, dar mai bun ar fi `generate`, deoarece la fiecare apelare creează și returnează un nou element `Html`. - - -Renderer -======== - -Este un obiect care asigură randarea formularului. Acesta poate fi setat cu metoda `$form->setRenderer`. I se transmite controlul la apelarea metodei `$form->render()`. - -Dacă nu setăm propriul renderer, va fi utilizat rendererul implicit [api:Nette\Forms\Rendering\DefaultFormRenderer]. Acesta randează elementele formularului sub formă de tabel HTML. Ieșirea arată astfel: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Nume:</label></th> - - <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> -</tr> - -<tr class="required"> - <th><label class="required" for="frm-age">Vârstă:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Gen:</label></th> - ... -``` - -Dacă să folosim sau nu un tabel pentru structura formularului este discutabil, iar mulți webdesigneri preferă alt markup. De exemplu, o listă de definiții. Vom reconfigura, prin urmare, `DefaultFormRenderer` astfel încât să randeze formularul sub formă de listă. Configurarea se realizează prin editarea array-ului [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Primul index reprezintă întotdeauna zona, iar al doilea atributul său. Zonele individuale sunt ilustrate în imagine: - -[* defaultformrenderer.webp *] - -În mod standard, grupul de elemente `controls` este încapsulat într-un tabel `<table>`, fiecare `pair` reprezintă un rând de tabel `<tr>`, iar perechea `label` și `control` sunt celule `<th>` și `<td>`. Acum vom schimba elementele încapsulatoare. Vom insera zona `controls` într-un container `<dl>`, vom lăsa zona `pair` fără container, vom insera `label` în `<dt>` și, în final, vom încapsula `control` cu tag-urile `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Rezultatul este acest cod HTML: - -```latte -<dl> - <dt><label class="required" for="frm-name">Nume:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Vârstă:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Gen:</label></dt> - ... -</dl> -``` - -În array-ul wrappers se pot influența multe alte atribute: - -- adăugarea claselor CSS la tipurile individuale de elemente de formular -- distingerea rândurilor pare și impare prin clasa CSS -- diferențierea vizuală a elementelor obligatorii și opționale -- determinarea dacă mesajele de eroare se afișează direct lângă elemente sau deasupra formularului - - -Opțiuni -------- - -Comportamentul Rendererului poate fi controlat și prin setarea *opțiunilor* pe elementele individuale ale formularului. Astfel, se poate seta o descriere care se va afișa lângă câmpul de intrare: - -```php -$form->addText('phone', 'Număr:') - ->setOption('description', 'Acest număr va rămâne ascuns'); -``` - -Dacă dorim să plasăm conținut HTML în el, utilizăm clasa [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Număr:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Condițiile de păstrare a numărului dumneavoastră</a>') - ); -``` - -.[tip] -Elementul Html poate fi utilizat și în locul etichetei: `$form->addCheckbox('conditions', $label)`. - - -Gruparea elementelor --------------------- - -Rendererul permite gruparea elementelor în grupuri vizuale (fieldset-uri): - -```php -$form->addGroup('Date personale'); -``` - -După crearea unui nou grup, acesta devine activ și fiecare element nou adăugat este, de asemenea, adăugat în el. Deci, formularul poate fi construit în acest mod: - -```php -$form = new Form; -$form->addGroup('Date personale'); -$form->addText('name', 'Numele dumneavoastră:'); -$form->addInteger('age', 'Vârsta dumneavoastră:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Adresa de livrare'); -$form->addCheckbox('send', 'Livrează la adresă'); -$form->addText('street', 'Stradă:'); -$form->addText('city', 'Oraș:'); -$form->addSelect('country', 'Țara:', $countries); -``` - -Rendererul randează mai întâi grupurile și abia apoi elementele care nu aparțin niciunui grup. - - -Suport pentru Bootstrap ------------------------ - -[În exemple |https://github.com/nette/forms/tree/master/examples] veți găsi exemple despre cum să configurați Rendererul pentru [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] și [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -Atribute HTML -============= - -Pentru a seta atribute HTML arbitrare ale elementelor formularului, folosim metoda `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Număr:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Sortare după:', ['preț', 'nume']) - ->setHtmlAttribute('onchange', 'submit()'); // trimite la modificare - - -// Pentru a seta atributele formularului <form> în sine -$form->setHtmlAttribute('id', 'myForm'); -``` - -Specificarea tipului elementului: - -```php -$form->addText('tel', 'Telefonul dumneavoastră:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'scrieți telefonul'); -``` - -.[warning] -Setarea tipului și a altor atribute servește doar în scopuri vizuale. Verificarea corectitudinii intrărilor trebuie să aibă loc pe server, ceea ce asigurați prin alegerea [elementului de formular|controls] adecvat și specificarea [regulilor de validare|validation]. - -Elementelor individuale din listele radio sau checkbox le putem seta un atribut HTML cu valori diferite pentru fiecare dintre ele. Observați două puncte după `style:`, care asigură alegerea valorii după cheie: - -```php -$colors = ['r' => 'roșu', 'g' => 'verde', 'b' => 'albastru']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Culori:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Afișează: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">roșu</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">albastru</label> -``` - -Pentru a seta atribute logice, cum ar fi `readonly`, putem folosi notația cu semn de întrebare: - -```php -$form->addCheckboxList('colors', 'Culori:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // pentru mai multe chei utilizați un array, de ex. ['r', 'g'] -``` - -Afișează: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">roșu</label> -<label><input type="checkbox" name="colors[]" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">albastru</label> -``` - -În cazul selectbox-urilor, metoda `setHtmlAttribute()` setează atributele elementului `<select>`. Dacă dorim să setăm atributele elementelor individuale `<option>`, folosim metoda `setOptionAttribute()`. Funcționează și notațiile cu două puncte și semn de întrebare menționate mai sus: - -```php -$form->addSelect('colors', 'Culori:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Afișează: - -```latte -<select name="colors"> - <option value="r" style="background:red">roșu</option> - <option value="g" style="background:green">verde</option> - <option value="b">albastru</option> -</select> -``` - - -Prototipuri ------------ - -O modalitate alternativă de setare a atributelor HTML constă în modificarea șablonului din care se generează elementul HTML. Șablonul este un obiect `Html` și este returnat de metoda `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Număr:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -În acest mod se poate modifica și șablonul etichetei, returnat de `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -La elementele Checkbox, CheckboxList și RadioList puteți influența șablonul elementului care încapsulează întregul element. Acesta este returnat de `getContainerPrototype()`. În starea implicită, este un element „gol”, deci nu se randează nimic, dar prin setarea numelui său, va începe să se randeze: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -În cazul CheckboxList și RadioList, se poate influența și șablonul separatorului elementelor individuale, returnat de metoda `getSeparatorPrototype()`. În starea implicită, este elementul `<br>`. Dacă îl schimbați într-un element pereche, va încapsula elementele individuale în loc să le separe. Și, în plus, se poate influența șablonul elementului HTML al etichetei la elementele individuale, returnat de `getItemLabelPrototype()`. - - -Traducere -========= - -Dacă programați o aplicație multilingvă, probabil veți avea nevoie să randați formularul în diferite versiuni lingvistice. Nette Framework definește în acest scop o interfață pentru traducere [api:Nette\Localization\Translator]. În Nette nu există o implementare implicită, puteți alege în funcție de nevoile dumneavoastră din mai multe soluții gata făcute, pe care le găsiți pe [Componette |https://componette.org/search/localization]. În documentația lor veți afla cum să configurați translatorul. - -Formularele suportă afișarea textelor prin translator. Îl transmitem folosind metoda `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -De acum înainte, nu numai toate etichetele, ci și toate mesajele de eroare sau elementele select box-urilor vor fi traduse într-o altă limbă. - -La elementele individuale ale formularului este posibil să setați un alt traducător sau să dezactivați complet traducerea cu valoarea `null`: - -```php -$form->addSelect('carModel', 'Model:', $cars) - ->setTranslator(null); -``` - -La [regulile de validare|validation], translatorului i se transmit și parametri specifici, de exemplu la regula: - -```php -$form->addPassword('password', 'Parolă:') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -se apelează translatorul cu acești parametri: - -```php -$translator->translate('Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -și, prin urmare, poate alege forma corectă de plural a cuvântului `caractere` în funcție de număr. - - -Evenimentul onRender -==================== - -Chiar înainte ca formularul să fie randat, putem lăsa să fie apelat codul nostru. Acesta poate, de exemplu, să completeze elementele formularului cu clase HTML pentru afișarea corectă. Adăugăm codul în array-ul `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/ro/standalone.texy b/forms/ro/standalone.texy deleted file mode 100644 index 58562a54a5..0000000000 --- a/forms/ro/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Formulare utilizate independent -******************************* - -.[perex] -Nette Forms facilitează enorm crearea și procesarea formularelor web. Le puteți utiliza în aplicațiile dvs. complet independent de restul framework-ului, așa cum vom demonstra în acest capitol. - -Dacă însă utilizați Nette Application și presentere, ghidul pentru [utilizarea în presentere|in-presenter] este destinat dvs. - - -Primul formular -=============== - -Vom încerca să scriem un formular simplu de înregistrare. Codul său va fi următorul ("codul complet":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Nume:'); -$form->addPassword('password', 'Parolă:'); -$form->addSubmit('send', 'Înregistrare'); -``` - -Îl putem randa foarte ușor: - -```php -$form->render(); -``` - -și în browser se va afișa astfel: - -[* form-cs.webp *] - -Formularul este un obiect al clasei `Nette\Forms\Form` (clasa `Nette\Application\UI\Form` se utilizează în presentere). Am adăugat în el așa-numitele elemente nume, parolă și buton de trimitere. - -Acum vom anima formularul. Prin interogarea `$form->isSuccess()` vom afla dacă formularul a fost trimis și dacă a fost completat valid. Dacă da, vom afișa datele. După definiția formularului, vom adăuga: - -```php -if ($form->isSuccess()) { - echo 'Formularul a fost completat corect și trimis'; - $data = $form->getValues(); - // $data->name conține numele - // $data->password conține parola - var_dump($data); -} -``` - -Metoda `getValues()` returnează datele trimise sub forma unui obiect [ArrayHash |utils:arrays#ArrayHash]. Cum să schimbăm acest lucru vom arăta [mai târziu |#Maparea pe clase]. Obiectul `$data` conține cheile `name` și `password` cu datele completate de utilizator. - -De obicei, trimitem datele direct pentru procesare ulterioară, cum ar fi inserarea într-o bază de date. Însă, în timpul procesării poate apărea o eroare, de exemplu, numele de utilizator este deja ocupat. În acest caz, transmitem eroarea înapoi formularului folosind `addError()` și îl lăsăm să se randeze din nou, inclusiv cu mesajul de eroare. - -```php -$form->addError('Ne pare rău, acest nume de utilizator este deja folosit.'); -``` - -După procesarea formularului, redirecționăm către pagina următoare. Acest lucru previne retrimiterea nedorită a formularului prin butonul *reîmprospătare*, *înapoi* sau prin navigarea în istoricul browserului. - -Formularul se trimite standard prin metoda POST și către aceeași pagină. Ambele pot fi modificate: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -Și cam asta e tot :-) Avem un formular funcțional și perfect [securizat |#Protecția împotriva vulnerabilităților]. - -Încercați să adăugați și alte [elemente de formular|controls]. - - -Accesul la elemente -=================== - -Formularul și elementele sale individuale le numim componente. Ele formează un arbore de componente, unde rădăcina este chiar formularul. Putem accesa elementele individuale ale formularului în acest mod: - -```php -$input = $form->getComponent('name'); -// sintaxă alternativă: $input = $form['name']; - -$button = $form->getComponent('send'); -// sintaxă alternativă: $button = $form['send']; -``` - -Elementele se elimină folosind unset: - -```php -unset($form['name']); -``` - - -Reguli de validare -================== - -Am menționat cuvântul *valid*, dar formularul nu are încă nicio regulă de validare. Să remediem acest lucru. - -Numele va fi obligatoriu, așa că îl vom marca cu metoda `setRequired()`, al cărei argument este textul mesajului de eroare care se va afișa dacă utilizatorul nu completează numele. Dacă nu specificăm argumentul, se va utiliza mesajul de eroare implicit. - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să introduceți numele'); -``` - -Încercați să trimiteți formularul fără a completa numele și veți vedea că se afișează un mesaj de eroare, iar browserul sau serverul îl va respinge până când nu completați câmpul. - -În același timp, nu puteți păcăli sistemul scriind, de exemplu, doar spații în câmp. Nici vorbă. Nette elimină automat spațiile de la începutul și sfârșitul șirului. Încercați. Este un lucru pe care ar trebui să-l faceți întotdeauna cu fiecare input de o singură linie, dar adesea se uită. Nette o face automat. (Puteți încerca să păcăliți formularul și să trimiteți un șir multilinie ca nume. Nici aici Nette nu se lasă păcălit și transformă sfârșiturile de linie în spații.) - -Formularul se validează întotdeauna pe partea de server, dar se generează și o validare JavaScript, care se execută instantaneu, iar utilizatorul află despre eroare imediat, fără a fi nevoie să trimită formularul la server. Acest lucru este gestionat de scriptul `netteForms.js`. Inserați-l în pagină: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Dacă vă uitați la codul sursă al paginii cu formularul, puteți observa că Nette inserează elementele obligatorii în elemente cu clasa CSS `required`. Încercați să adăugați următorul stil în șablon și eticheta „Nume” va fi roșie. Astfel, marcăm elegant elementele obligatorii pentru utilizatori: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Alte reguli de validare le adăugăm cu metoda `addRule()`. Primul parametru este regula, al doilea este din nou textul mesajului de eroare și poate urma un argument al regulii de validare. Ce înseamnă asta? - -Vom extinde formularul cu un nou câmp opțional „vârstă”, care trebuie să fie un număr întreg (`addInteger()`) și, în plus, într-un interval permis (`$form::Range`). Și aici vom folosi al treilea parametru al metodei `addRule()`, prin care transmitem validatorului intervalul dorit ca pereche `[de la, până la]`: - -```php -$form->addInteger('age', 'Vârstă:') - ->addRule($form::Range, 'Vârsta trebuie să fie între 18 și 120', [18, 120]); -``` - -.[tip] -Dacă utilizatorul nu completează câmpul, regulile de validare nu vor fi verificate, deoarece elementul este opțional. - -Aici apare spațiu pentru o mică refactorizare. În mesajul de eroare și în al treilea parametru, numerele sunt menționate duplicat, ceea ce nu este ideal. Dacă am crea [formulare multilingve |rendering#Traducere] și mesajul care conține numere ar fi tradus în mai multe limbi, o eventuală modificare a valorilor ar fi dificilă. Din acest motiv, este posibil să folosim substituenții `%d`, iar Nette va completa valorile: - -```php - ->addRule($form::Range, 'Vârsta trebuie să fie între %d și %d ani', [18, 120]); -``` - -Să revenim la elementul `password`, pe care îl vom face, de asemenea, obligatoriu și vom verifica lungimea minimă a parolei (`$form::MinLength`), folosind din nou substituentul: - -```php -$form->addPassword('password', 'Parolă:') - ->setRequired('Alegeți o parolă') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -Vom adăuga în formular și câmpul `passwordVerify`, unde utilizatorul introduce parola încă o dată, pentru verificare. Folosind regulile de validare, vom verifica dacă ambele parole sunt identice (`$form::Equal`). Și ca parametru vom da o referință la prima parolă folosind [paranteze drepte |#Accesul la elemente]: - -```php -$form->addPassword('passwordVerify', 'Parola pentru verificare:') - ->setRequired('Vă rugăm să introduceți parola din nou pentru verificare') - ->addRule($form::Equal, 'Parolele nu se potrivesc', $form['password']) - ->setOmitted(); -``` - -Folosind `setOmitted()`, am marcat elementul a cărui valoare nu ne interesează de fapt și care există doar în scopul validării. Valoarea nu se va transmite în `$data`. - -Astfel, avem un formular complet funcțional cu validare în PHP și JavaScript. Capacitățile de validare ale Nette sunt mult mai extinse, se pot crea condiții, se pot afișa și ascunde părți ale paginii în funcție de acestea etc. Veți afla totul în capitolul despre [validarea formularelor|validation]. - - -Valori implicite -================ - -Elementelor formularului le setăm în mod obișnuit valori implicite: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Adesea este util să setăm valori implicite pentru toate elementele simultan. De exemplu, când formularul servește la editarea înregistrărilor. Citim înregistrarea din baza de date și setăm valorile implicite: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Apelați `setDefaults()` după definirea elementelor. - - -Randarea formularului -===================== - -Standard, formularul se randează ca un tabel. Elementele individuale respectă regula de bază a accesibilității - toate etichetele sunt scrise ca `<label>` și legate de elementul de formular corespunzător. La clic pe etichetă, cursorul apare automat în câmpul formularului. - -Fiecărui element îi putem seta atribute HTML arbitrare. De exemplu, adăugăm un placeholder: - -```php -$form->addInteger('age', 'Vârstă:') - ->setHtmlAttribute('placeholder', 'Vă rugăm să completați vârsta'); -``` - -Există într-adevăr o mare varietate de moduri de a randa un formular, așa că acestui subiect i se dedică un [capitol separat despre randare|rendering]. - - -Maparea pe clase -================ - -Să revenim la procesarea datelor formularului. Metoda `getValues()` ne returna datele trimise ca obiect `ArrayHash`. Deoarece este o clasă generică, ceva de genul `stdClass`, ne va lipsi un anumit confort în lucrul cu ea, cum ar fi sugestiile de proprietăți în editori sau analiza statică a codului. Acest lucru ar putea fi rezolvat având o clasă specifică pentru fiecare formular, ale cărei proprietăți reprezintă elementele individuale. De ex.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativ, puteți utiliza constructorul: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Proprietățile clasei de date pot fi, de asemenea, enumuri și vor fi mapate automat. .{data-version:3.2.4} - -Cum să spunem Nette să ne returneze datele ca obiecte ale acestei clase? Mai ușor decât credeți. Este suficient să specificați numele clasei sau obiectul de hidratat ca parametru: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Ca parametru se poate specifica și `'array'`, iar datele vor fi returnate ca array. - -Dacă formularele formează o structură multinivel compusă din containere, creați o clasă separată pentru fiecare: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Maparea va recunoaște apoi din tipul proprietății `$person` că trebuie să mapeze containerul la clasa `PersonFormData`. Dacă proprietatea ar conține un array de containere, specificați tipul `array` și transmiteți clasa pentru mapare direct containerului: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Puteți genera schița clasei de date a formularului folosind metoda `Nette\Forms\Blueprint::dataClass($form)`, care o va afișa în pagina browserului. Apoi, este suficient să selectați codul cu un clic și să îl copiați în proiect. .{data-version:3.1.15} - - -Mai multe butoane -================= - -Dacă formularul are mai mult de un buton, de obicei trebuie să distingem care dintre ele a fost apăsat. Această informație ne este returnată de metoda `isSubmittedBy()` a butonului: - -```php -$form->addSubmit('save', 'Salvare'); -$form->addSubmit('delete', 'Ștergere'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Nu omiteți interogarea `$form->isSuccess()`, aceasta verifică validitatea datelor. - -Când formularul este trimis cu tasta <kbd>Enter</kbd>, se consideră ca și cum ar fi fost trimis cu primul buton. - - -Protecția împotriva vulnerabilităților -====================================== - -Nette Framework acordă o mare importanță securității și, prin urmare, are grijă deosebită de securizarea formularelor. - -Pe lângă protejarea formularelor împotriva atacurilor [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] și [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], realizează o mulțime de mici măsuri de securitate la care nu mai trebuie să vă gândiți. - -De exemplu, filtrează toate caracterele de control din intrări și verifică validitatea codificării UTF-8, astfel încât datele din formular vor fi întotdeauna curate. Pentru casetele de selecție și listele radio, verifică dacă elementele selectate au fost într-adevăr dintre cele oferite și nu a avut loc o falsificare. Am menționat deja că pentru intrările de text de o singură linie elimină caracterele de sfârșit de linie pe care un atacator le-ar fi putut trimite. Pentru intrările multilinie, normalizează caracterele de sfârșit de linie. Și așa mai departe. - -Nette rezolvă pentru dvs. riscurile de securitate despre care mulți programatori nici nu știu că există. - -Atacul CSRF menționat constă în faptul că atacatorul atrage victima pe o pagină care execută discret în browserul victimei o cerere către serverul pe care victima este autentificată, iar serverul crede că cererea a fost executată de victimă din proprie inițiativă. De aceea, Nette previne trimiterea formularelor POST de pe un alt domeniu. Dacă, din anumite motive, doriți să dezactivați protecția și să permiteți trimiterea formularului de pe un alt domeniu, utilizați: - -```php -$form->allowCrossOrigin(); // ATENȚIE! Dezactivează protecția! -``` - -Această protecție utilizează un cookie SameSite numit `_nss`. Prin urmare, creați obiectul formularului înainte de a trimite prima ieșire, pentru a putea trimite cookie-ul. - -Protecția prin cookie SameSite poate să nu fie 100% fiabilă, de aceea este recomandat să activați și protecția prin token: - -```php -$form->addProtection(); -``` - -Recomandăm protejarea în acest mod a formularelor din partea de administrare a site-ului, care modifică date sensibile în aplicație. Framework-ul se apără împotriva atacului CSRF prin generarea și verificarea unui token de autorizare, care este stocat în sesiune. Prin urmare, este necesar să aveți sesiunea deschisă înainte de afișarea formularului. În partea de administrare a site-ului, sesiunea este de obicei deja pornită datorită autentificării utilizatorului. Altfel, porniți sesiunea cu metoda `Nette\Http\Session::start()`. - -Așadar, am parcurs o introducere rapidă în formularele Nette. Încercați să consultați și directorul [examples|https://github.com/nette/forms/tree/master/examples] din distribuție, unde veți găsi mai multă inspirație. diff --git a/forms/ro/validation.texy b/forms/ro/validation.texy deleted file mode 100644 index 8bad54f2d4..0000000000 --- a/forms/ro/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Validarea formularelor -********************** - - -Elemente obligatorii -==================== - -Elementele obligatorii le marcăm cu metoda `setRequired()`, al cărei argument este textul [mesajului de eroare |#Mesaje de eroare], care se afișează dacă utilizatorul nu completează elementul. Dacă nu specificăm argumentul, se va utiliza mesajul de eroare implicit. - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să introduceți numele'); -``` - - -Reguli -====== - -Regulile de validare le adăugăm elementelor cu metoda `addRule()`. Primul parametru este regula, al doilea este textul [mesajului de eroare |#Mesaje de eroare] și al treilea este argumentul regulii de validare. - -```php -$form->addPassword('password', 'Parolă:') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -**Regulile de validare se verifică doar în cazul în care utilizatorul a completat elementul.** - -Nette vine cu o serie întreagă de reguli predefinite, ale căror nume sunt constante ale clasei `Nette\Forms\Form`. Pentru toate elementele putem folosi aceste reguli: - -| constantă | descriere | tip argument -|------- -| `Required` | element obligatoriu, alias pentru `setRequired()` | - -| `Filled` | element obligatoriu, alias pentru `setRequired()` | - -| `Blank` | elementul nu trebuie completat | - -| `Equal` | valoarea este egală cu parametrul | `mixed` -| `NotEqual` | valoarea nu este egală cu parametrul | `mixed` -| `IsIn` | valoarea este egală cu unul dintre elementele din array | `array` -| `IsNotIn` | valoarea nu este egală cu niciun element din array | `array` -| `Valid` | elementul este completat corect? (pentru [#condiții]) | - - - -Intrări de text ---------------- - -Pentru elementele `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` se pot utiliza și unele dintre următoarele reguli: - -| `MinLength` | lungimea minimă a textului | `int` -| `MaxLength` | lungimea maximă a textului | `int` -| `Length` | lungimea în interval sau lungimea exactă | pereche `[int, int]` sau `int` -| `Email` | adresă de e-mail validă | - -| `URL` | URL absolut | - -| `Pattern` | se potrivește expresiei regulate | `string` -| `PatternInsensitive` | ca `Pattern`, dar independent de majuscule/minuscule | `string` -| `Integer` | valoare întreagă | - -| `Numeric` | alias pentru `Integer` | - -| `Float` | număr | - -| `Min` | valoarea minimă a elementului numeric | `int\|float` -| `Max` | valoarea maximă a elementului numeric | `int\|float` -| `Range` | valoarea în interval | pereche `[int\|float, int\|float]` - -Regulile de validare `Integer`, `Numeric` și `Float` convertesc direct valoarea la integer, respectiv float. Mai mult, regula `URL` acceptă și o adresă fără schemă (de ex. `nette.org`) și completează schema (`https://nette.org`). Expresia din `Pattern` și `PatternIcase` trebuie să fie valabilă pentru întreaga valoare, adică ca și cum ar fi încadrată de caracterele `^` și `$`. - - -Numărul de elemente -------------------- - -Pentru elementele `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` se pot utiliza și următoarele reguli pentru a limita numărul de elemente selectate, respectiv fișiere încărcate: - -| `MinLength` | număr minim | `int` -| `MaxLength` | număr maxim | `int` -| `Length` | numărul în interval sau numărul exact | pereche `[int, int]` sau `int` - - -Încărcarea fișierelor ---------------------- - -Pentru elementele `addUpload()`, `addMultiUpload()` se pot utiliza și următoarele reguli: - -| `MaxFileSize` | dimensiunea maximă a fișierului în octeți | `int` -| `MimeType` | tip MIME, permise caractere wildcard (`'video/*'`) | `string\|string[]` -| `Image` | imagine JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | numele fișierului se potrivește expresiei regulate | `string` -| `PatternInsensitive` | ca `Pattern`, dar independent de majuscule/minuscule | `string` - -`MimeType` și `Image` necesită extensia PHP `fileinfo`. Faptul că fișierul sau imaginea este de tipul dorit este detectat pe baza semnăturii sale și **nu verifică integritatea întregului fișier.** Dacă imaginea nu este deteriorată, se poate afla, de exemplu, încercând să o [încărcați |http:request#toImage]. - - -Mesaje de eroare -================ - -Toate regulile predefinite, cu excepția `Pattern` și `PatternInsensitive`, au un mesaj de eroare implicit, deci acesta poate fi omis. Cu toate acestea, specificarea și formularea tuturor mesajelor personalizate va face formularul mai prietenos pentru utilizator. - -Puteți modifica mesajele implicite în [configurație|forms:configuration], editând textele din array-ul `Nette\Forms\Validator::$messages` sau folosind un [translator |rendering#Traducere]. - -În textul mesajelor de eroare se pot utiliza următorii substituenți: - -| `%d` | înlocuiește succesiv cu argumentele regulii -| `%n$d` | înlocuiește cu al n-lea argument al regulii -| `%label` | înlocuiește cu eticheta elementului (fără două puncte) -| `%name` | înlocuiește cu numele elementului (de ex. `name`) -| `%value` | înlocuiește cu valoarea introdusă de utilizator - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să completați %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'cel puțin %d și cel mult %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'cel mult %2$d și cel puțin %1$d', [5, 10]); -``` - - -Condiții -======== - -Pe lângă reguli, se pot adăuga și condiții. Acestea se scriu similar cu regulile, doar că în loc de `addRule()` folosim metoda `addCondition()` și, desigur, nu specificăm niciun mesaj de eroare (condiția doar întreabă): - -```php -$form->addPassword('password', 'Parolă:') - // dacă parola nu este mai lungă de 8 caractere - ->addCondition($form::MaxLength, 8) - // atunci trebuie să conțină o cifră - ->addRule($form::Pattern, 'Trebuie să conțină o cifră', '.*[0-9].*'); -``` - -Condiția poate fi legată și de alt element decât cel curent folosind `addConditionOn()`. Ca prim parametru, specificăm referința la element. În acest exemplu, e-mailul va fi obligatoriu doar dacă se bifează checkbox-ul (valoarea sa va fi true): - -```php -$form->addCheckbox('newsletters', 'trimiteți-mi newslettere'); - -$form->addEmail('email', 'E-mail:') - // dacă checkbox-ul este bifat - ->addConditionOn($form['newsletters'], $form::Equal, true) - // atunci solicită e-mail - ->setRequired('Introduceți adresa de e-mail'); -``` - -Din condiții se pot crea structuri complexe folosind `elseCondition()` și `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // dacă prima condiție este îndeplinită - ->addConditionOn(/* ... */) // și a doua condiție pe un alt element - ->addRule(/* ... */) // solicită această regulă - ->elseCondition() // dacă a doua condiție nu este îndeplinită - ->addRule(/* ... */) // solicită aceste reguli - ->addRule(/* ... */) - ->endCondition() // ne întoarcem la prima condiție - ->addRule(/* ... */); -``` - -În Nette se poate reacționa foarte ușor la îndeplinirea sau neîndeplinirea condiției și pe partea de JavaScript folosind metoda `toggle()`, vezi [#javascript-dinamic]. - - -Referință la alt element -======================== - -Ca argument al regulii sau condiției se poate transmite și alt element al formularului. Regula va folosi atunci valoarea introdusă ulterior de utilizator în browser. Astfel se poate valida dinamic, de exemplu, că elementul `password` conține același șir ca elementul `password_confirm`: - -```php -$form->addPassword('password', 'Parolă'); -$form->addPassword('password_confirm', 'Confirmați parola') - ->addRule($form::Equal, 'Parolele introduse nu se potrivesc', $form['password']); -``` - - -Reguli și condiții personalizate -================================ - -Uneori ajungem în situația în care regulile de validare încorporate în Nette nu sunt suficiente și trebuie să validăm datele de la utilizator în felul nostru. În Nette este foarte simplu! - -Metodelor `addRule()` sau `addCondition()` li se poate transmite ca prim parametru orice callback. Acesta primește ca prim parametru elementul însuși și returnează o valoare booleană care indică dacă validarea a avut loc cu succes. La adăugarea unei reguli folosind `addRule()`, se pot specifica și alte argumente, acestea fiind apoi transmise ca al doilea parametru. - -Putem crea astfel propriul set de validatori ca o clasă cu metode statice: - -```php -class MyValidators -{ - // testează dacă valoarea este divizibilă cu argumentul - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // alți validatori - } -} -``` - -Utilizarea este apoi foarte simplă: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Valoarea trebuie să fie un multiplu al numărului %d', - 8, - ); -``` - -Regulile de validare personalizate pot fi adăugate și în JavaScript. Condiția este ca regula să fie o metodă statică. Numele său pentru validatorul JavaScript se formează prin concatenarea numelui clasei fără backslash-uri `\`, a unui underscore `_` și a numelui metodei. De ex. `App\MyValidators::validateDivisibility` se scrie ca `AppMyValidators_validateDivisibility` și se adaugă la obiectul `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Evenimentul onValidate -====================== - -După trimiterea formularului, se efectuează validarea, în timpul căreia se verifică regulile individuale adăugate folosind `addRule()` și apoi se declanșează [evenimentul |nette:glossary#Evenimente] `onValidate`. Handler-ul său poate fi utilizat pentru validare suplimentară, de obicei verificarea combinației corecte de valori în mai multe elemente ale formularului. - -Dacă se descoperă o eroare, o transmitem formularului prin metoda `addError()`. Aceasta poate fi apelată fie pe un element specific, fie direct pe formular. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Această combinație nu este posibilă.'); - } -} -``` - - -Erori în timpul procesării -========================== - -În multe cazuri, aflăm despre eroare abia în momentul în care procesăm formularul valid, de exemplu, scriem un nou element în baza de date și întâlnim o duplicitate a cheilor. În acest caz, transmitem din nou eroarea formularului prin metoda `addError()`. Aceasta poate fi apelată fie pe un element specific, fie direct pe formular: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Parolă invalidă.'); - } -} -``` - -Dacă este posibil, recomandăm atașarea erorii direct la elementul formularului, deoarece aceasta va fi afișată lângă el la utilizarea renderer-ului implicit. - -```php -$form['date']->addError('Ne pare rău, dar această dată este deja ocupată.'); -``` - -Puteți apela `addError()` în mod repetat pentru a transmite formularului sau elementului mai multe mesaje de eroare. Le puteți obține folosind `getErrors()`. - -Atenție, `$form->getErrors()` returnează un sumar al tuturor mesajelor de eroare, inclusiv cele transmise direct elementelor individuale, nu doar direct formularului. Mesajele de eroare transmise doar formularului le puteți obține prin `$form->getOwnErrors()`. - - -Modificarea intrării -==================== - -Folosind metoda `addFilter()`, putem modifica valoarea introdusă de utilizator. În acest exemplu, vom tolera și elimina spațiile din codul poștal: - -```php -$form->addText('zip', 'Cod poștal:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // eliminăm spațiile din codul poștal - }) - ->addRule($form::Pattern, 'Codul poștal nu este în format de cinci cifre', '\d{5}'); -``` - -Filtrul se integrează între regulile și condițiile de validare, deci ordinea metodelor contează, adică filtrul și regula se apelează în ordinea în care sunt metodele `addFilter()` și `addRule()`. - - -Validare JavaScript -=================== - -Limbajul pentru formularea condițiilor și regulilor este foarte puternic. Toate construcțiile funcționează atât pe partea de server, cât și pe partea de JavaScript. Acestea sunt transmise în atributele HTML `data-nette-rules` ca JSON. Validarea propriu-zisă este apoi efectuată de un script care interceptează evenimentul `submit` al formularului, parcurge elementele individuale și efectuează validarea corespunzătoare. - -Acest script este `netteForms.js` și este disponibil din mai multe surse posibile: - -Puteți include scriptul direct în pagina HTML de pe CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Sau copiați-l local în folderul public al proiectului (de ex. din `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Sau instalați-l prin [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -Și apoi încărcați-l și rulați-l: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternativ, îl puteți încărca direct din folderul `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -JavaScript dinamic -================== - -Doriți să afișați câmpurile pentru introducerea adresei doar dacă utilizatorul alege să primească produsul prin poștă? Nicio problemă. Cheia este perechea de metode `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Acest cod spune că atunci când condiția este îndeplinită, adică atunci când checkbox-ul este bifat, elementul HTML `#address-container` va fi vizibil. Și invers. Elementele formularului cu adresa destinatarului le vom plasa astfel într-un container cu acest ID, iar la clic pe checkbox, acestea se vor ascunde sau afișa. Acest lucru este asigurat de scriptul `netteForms.js`. - -Ca argument al metodei `toggle()` se poate transmite orice selector. Din motive istorice, un șir alfanumeric fără alte caractere speciale este înțeles ca ID-ul elementului, adică la fel ca și cum ar fi precedat de caracterul `#`. Al doilea parametru opțional permite inversarea comportamentului, adică dacă am folosi `toggle('#address-container', false)`, elementul s-ar afișa doar dacă checkbox-ul nu ar fi bifat. - -Implementarea implicită în JavaScript modifică proprietatea `hidden` a elementelor. Putem însă schimba ușor comportamentul, de exemplu, adăugând o animație. Este suficient să suprascriem în JavaScript metoda `Nette.toggle` cu propria soluție: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // ascundem sau afișăm 'el' în funcție de valoarea 'visible' - }); -}; -``` - - -Dezactivarea validării -====================== - -Uneori poate fi util să dezactivăm validarea. Dacă apăsarea butonului de trimitere nu trebuie să efectueze validarea (potrivit pentru butoanele *Cancel* sau *Preview*), o dezactivăm cu metoda `$submit->setValidationScope([])`. Dacă trebuie să efectueze doar o validare parțială, putem specifica ce câmpuri sau containere de formular trebuie validate. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Validează întregul formular -$form->addSubmit('send2') - ->setValidationScope([]); // Nu validează deloc -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Validează doar elementul name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Validează doar elementul age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Validează containerul details -``` - -`setValidationScope` nu afectează [#evenimentul onValidate] al formularului, care va fi apelat întotdeauna. Evenimentul `onValidate` al containerului va fi declanșat doar dacă acest container este marcat pentru validare parțială. diff --git a/forms/ru/@home.texy b/forms/ru/@home.texy index 1351009e4c..d72c5a13d7 100644 --- a/forms/ru/@home.texy +++ b/forms/ru/@home.texy @@ -3,18 +3,18 @@ Nette Forms <div class=perex> -Nette Forms произвели революцию в создании веб-форм. Внезапно стало достаточно написать несколько понятных строк кода, и у вас была готовая форма, включая рендеринг, валидацию на стороне JavaScript и сервера, и к тому же превосходно защищенная. Мы покажем вам, как +Nette Forms перевернули создание веб-форм. Вдруг оказалось, что достаточно написать несколько понятных строк кода, чтобы получить готовую форму вместе с отрисовкой, JavaScript- и серверной проверкой и первоклассной безопасностью. Мы покажем, как: -- создавать удобные формы -- валидировать отправленные данные -- рендерить элементы точно по необходимости +- создавать удобные для пользователя формы +- проверять отправленные данные +- отрисовывать элементы ровно так, как нужно </div> -Используя Nette Forms, вы избежите целого ряда рутинных задач, таких как написание валидации (причем двойной, на стороне сервера и клиента), минимизируете вероятность возникновения ошибок и уязвимостей безопасности. +С помощью Nette Forms вы избавитесь от множества рутинных задач, например от написания логики проверки (и на сервере, и на стороне клиента), и сведёте к минимуму вероятность ошибок и брешей в безопасности. -Формы можно использовать либо как часть Nette Application (то есть в презентерах), либо совершенно самостоятельно. Поскольку в обоих случаях использование немного отличается, мы подготовили для вас два руководства: +Формы можно использовать как часть Nette Application (то есть в презентерах) или совершенно самостоятельно. Поскольку в этих двух случаях использование немного различается, мы подготовили для вас отдельные руководства: <div class="wiki-buttons"> <div> "Формы в презентерах .[wiki-button]":in-presenter </div> @@ -25,7 +25,7 @@ Nette Forms произвели революцию в создании веб-ф Установка --------- -Скачать и установить библиотеку можно с помощью [Composer|best-practices:composer]: +Скачайте и установите пакет с помощью [Composer|best-practices:composer]: ```shell composer require nette/forms diff --git a/forms/ru/@left-menu.texy b/forms/ru/@left-menu.texy index a50f2a3f1c..f3b9eb6d28 100644 --- a/forms/ru/@left-menu.texy +++ b/forms/ru/@left-menu.texy @@ -1,14 +1,16 @@ Nette Forms *********** -- [Введение |@home] +- [Обзор |@home] - [Формы в презентерах|in-presenter] - [Формы отдельно|standalone] -- [Элементы управления формы |controls] +- [Элементы формы |controls] - [Валидация |validation] -- [Рендеринг |rendering] -- [Конфигурация |configuration] +- [Отрисовка |rendering] +- [Пользовательские элементы |custom-controls] +- [Конфигурация|configuration] +- [Обновление|upgrading] -Дополнительное чтение -********************* -- [Руководства и лучшие практики |best-practices:] +Дополнительные материалы +************************ +- [Лучшие практики |best-practices:] diff --git a/forms/ru/configuration.texy b/forms/ru/configuration.texy index e275b176e6..739653c53b 100644 --- a/forms/ru/configuration.texy +++ b/forms/ru/configuration.texy @@ -17,6 +17,7 @@ forms: 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.' @@ -30,32 +31,33 @@ forms: Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' ``` -Вот русский перевод: +/--comment -```neon -forms: - messages: - Equal: 'Пожалуйста, введите %s.' - NotEqual: 'Это значение не должно быть %s.' - Filled: 'Это поле обязательно для заполнения.' - Blank: 'Это поле должно быть пустым.' - MinLength: 'Пожалуйста, введите не менее %d символов.' - MaxLength: 'Пожалуйста, введите не более %d символов.' - Length: 'Пожалуйста, введите значение длиной от %d до %d символов.' - Email: 'Пожалуйста, введите действительный адрес электронной почты.' - URL: 'Пожалуйста, введите действительный URL.' - Integer: 'Пожалуйста, введите действительное целое число.' - Float: 'Пожалуйста, введите действительное число.' - Min: 'Пожалуйста, введите значение больше или равное %d.' - Max: 'Пожалуйста, введите значение меньше или равное %d.' - Range: 'Пожалуйста, введите значение между %d и %d.' - MaxFileSize: 'Размер загружаемого файла может быть не более %d байт.' - MaxPostSize: 'Загруженные данные превышают лимит в %d байт.' - MimeType: 'Загруженный файл не соответствует ожидаемому формату.' - Image: 'Загруженный файл должен быть изображением в формате JPEG, GIF, PNG, WebP или AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Пожалуйста, выберите действительный вариант.' - Nette\Forms\Controls\UploadControl::Valid: 'Произошла ошибка при загрузке файла.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Ваша сессия истекла. Пожалуйста, вернитесь на главную страницу и попробуйте снова.' -``` -Если вы не используете весь фреймворк и, следовательно, конфигурационные файлы, вы можете изменить стандартные сообщения об ошибках непосредственно в массиве `Nette\Forms\Validator::$messages`. + + + + + + + + + + + + + + + + + + + + + + + + +\-- + +Если вы не используете весь фреймворк, а значит и конфигурационные файлы, изменить стандартные сообщения об ошибках можно прямо в массиве `Nette\Forms\Validator::$messages`. diff --git a/forms/ru/controls.texy b/forms/ru/controls.texy index 155cd41359..f45354e878 100644 --- a/forms/ru/controls.texy +++ b/forms/ru/controls.texy @@ -1,14 +1,14 @@ -Элементы формы -************** +Элементы форм +************* .[perex] -Обзор стандартных элементов формы. +Обзор стандартных элементов форм. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -Добавляет однострочное текстовое поле (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Если пользователь не заполняет поле, возвращает пустую строку `''`, или с помощью `setNullable()` можно указать, чтобы возвращал `null`. +Добавляет однострочное текстовое поле (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Если пользователь поле не заполнит, оно вернёт пустую строку `''`, либо через `setNullable()` можно добиться, чтобы вместо неё возвращался `null`. ```php $form->addText('name', 'Имя:') @@ -16,270 +16,271 @@ $form->addText('name', 'Имя:') ->setNullable(); ``` -Автоматически проверяет UTF-8, обрезает пробелы слева и справа и удаляет переводы строк, которые мог бы отправить злоумышленник. +Автоматически проверяет UTF-8, обрезает пробелы слева и справа и удаляет переводы строк, которые мог отправить злоумышленник. -Максимальную длину можно ограничить с помощью `setMaxLength()`. Изменить введенное пользователем значение позволяет [addFilter() |validation#Изменение ввода]. +Максимальную длину можно ограничить методом `setMaxLength()`. Метод [addFilter() |validation#Изменение введённых значений] позволяет изменить значение, введённое пользователем. -С помощью `setHtmlType()` можно изменить визуальный характер текстового поля на типы, такие как `search`, `tel` или `url`, см. [спецификацию|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Помните, что изменение типа является только визуальным и не заменяет функцию валидации. Для типа `url` рекомендуется добавить специфическое правило валидации [URL |validation#Текстовые поля ввода]. +С помощью `setHtmlType()` можно сменить внешний вид текстового поля на типы вроде `search`, `tel` или `url`, определённые в [спецификации|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Помните, что смена типа - чисто визуальная и не заменяет функцию проверки. Для типа `url` уместно добавить конкретное [правило проверки URL |validation#Текстовые поля]. .[note] -Для других типов ввода, таких как `number`, `range`, `email`, `date`, `datetime-local`, `time` и `color`, используйте специализированные методы, такие как [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] и [#addColor], которые обеспечивают серверную валидацию. Типы `month` и `week` пока не полностью поддерживаются во всех браузерах. +Для других типов полей, таких как `number`, `range`, `email`, `date`, `datetime-local`, `time` и `color`, используйте специализированные методы [#addInteger()], [#addFloat()], [#addEmail()], [#addDate()], [#addTime()], [#addDateTime()] и [#addColor()], которые обеспечивают проверку на стороне сервера. Типы `month` и `week` пока поддерживаются не всеми браузерами полностью. -Элементу можно установить так называемое empty-value, что-то вроде значения по умолчанию, но если пользователь его не изменит, элемент вернет пустую строку или `null`. +Элементу можно задать "пустое значение". Оно ведёт себя примерно как значение по умолчанию, но если пользователь его не изменит, элемент вернёт пустую строку или `null`. ```php $form->addText('phone', 'Телефон:') ->setHtmlType('tel') - ->setEmptyValue('+7'); + ->setEmptyValue('+420'); ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -Добавляет поле для ввода многострочного текста (класс [TextArea |api:Nette\Forms\Controls\TextArea]). Если пользователь не заполняет поле, возвращает пустую строку `''`, или с помощью `setNullable()` можно указать, чтобы возвращал `null`. +Добавляет многострочное текстовое поле (класс [TextArea |api:Nette\Forms\Controls\TextArea]). Если пользователь поле не заполнит, оно вернёт пустую строку `''`, либо через `setNullable()` можно добиться, чтобы вместо неё возвращался `null`. ```php -$form->addTextArea('note', 'Примечание:') - ->addRule($form::MaxLength, 'Примечание слишком длинное', 10000); +$form->addTextArea('note', 'Заметка:') + ->addRule($form::MaxLength, 'Ваша заметка слишком длинная', 10000); ``` -Автоматически проверяет UTF-8 и нормализует разделители строк на `\n`. В отличие от однострочного поля ввода, обрезка пробелов не происходит. +Автоматически проверяет UTF-8 и приводит окончания строк к `\n`. В отличие от однострочного поля, обрезки пробелов не происходит. -Максимальную длину можно ограничить с помощью `setMaxLength()`. Изменить введенное пользователем значение позволяет [addFilter() |validation#Изменение ввода]. Можно установить так называемое empty-value с помощью `setEmptyValue()`. +Максимальную длину можно ограничить методом `setMaxLength()`. Метод [addFilter() |validation#Изменение введённых значений] позволяет изменить введённое пользователем значение. Пустое значение задаётся через `setEmptyValue()`. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Добавляет поле для ввода целого числа (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Возвращает либо integer, либо `null`, если пользователь ничего не ввел. +Добавляет поле для ввода целого числа (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Возвращает либо целое число, либо `null`, если пользователь ничего не введёт. ```php $form->addInteger('year', 'Год:') - ->addRule($form::Range, 'Год должен быть в диапазоне от %d до %d.', [1900, 2023]); + ->addRule($form::Range, 'Год должен быть между %d и %d.', [1900, 2023]); ``` -Элемент отображается как `<input type="number">`. С помощью метода `setHtmlType()` можно изменить тип на `range` для отображения в виде ползунка, или на `text`, если вы предпочитаете стандартное текстовое поле без специального поведения типа `number`. +Элемент отрисовывается как `<input type="number">`. Методом `setHtmlType()` можно сменить тип на `range` для отображения ползунком либо на `text`, если вы предпочитаете обычное текстовое поле без особого поведения типа `number`. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -Добавляет поле для ввода десятичного числа (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Возвращает либо float, либо `null`, если пользователь ничего не ввел. +Добавляет поле для ввода дробного числа (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Возвращает либо число с плавающей точкой, либо `null`, если пользователь ничего не введёт. ```php $form->addFloat('level', 'Уровень:') ->setDefaultValue(0) - ->addRule($form::Range, 'Уровень должен быть в диапазоне от %d до %d.', [0, 100]); + ->addRule($form::Range, 'Уровень должен быть между %d и %d.', [0, 100]); ``` -Элемент отображается как `<input type="number">`. С помощью метода `setHtmlType()` можно изменить тип на `range` для отображения в виде ползунка, или на `text`, если вы предпочитаете стандартное текстовое поле без специального поведения типа `number`. +Элемент отрисовывается как `<input type="number">`. Методом `setHtmlType()` можно сменить тип на `range` для отображения ползунком либо на `text`, если вы предпочитаете обычное текстовое поле без особого поведения типа `number`. -Nette и браузер Chrome принимают в качестве разделителя десятичных знаков как запятую, так и точку. Чтобы эта функциональность была доступна и в Firefox, рекомендуется установить атрибут `lang` либо для данного элемента, либо для всей страницы, например `<html lang="ru">`. +Nette и браузер Chrome принимают в качестве десятичного разделителя и запятую, и точку. Чтобы эта возможность работала и в Firefox, рекомендуется задать атрибут `lang` либо конкретному элементу, либо всей странице, например `<html lang="en">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -Добавляет поле для ввода адреса электронной почты (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Если пользователь не заполняет поле, возвращает пустую строку `''`, или с помощью `setNullable()` можно указать, чтобы возвращал `null`. +Добавляет поле для ввода адреса электронной почты (класс [TextInput |api:Nette\Forms\Controls\TextInput]). Если пользователь поле не заполнит, оно вернёт пустую строку `''`, либо через `setNullable()` можно добиться, чтобы вместо неё возвращался `null`. ```php $form->addEmail('email', 'E-mail:'); ``` -Проверяет, является ли значение действительным адресом электронной почты. Не проверяется, существует ли домен на самом деле, проверяется только синтаксис. Автоматически проверяет UTF-8, обрезает пробелы слева и справа. +Проверяет, что значение - корректный адрес электронной почты. Существование домена не проверяется, проверяется только синтаксис. Автоматически проверяет UTF-8 и обрезает пробелы слева и справа. -Максимальную длину можно ограничить с помощью `setMaxLength()`. Изменить введенное пользователем значение позволяет [addFilter() |validation#Изменение ввода]. Можно установить так называемое empty-value с помощью `setEmptyValue()`. +Максимальную длину можно ограничить методом `setMaxLength()`. Метод [addFilter() |validation#Изменение введённых значений] позволяет изменить введённое пользователем значение. Пустое значение задаётся через `setEmptyValue()`. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== Добавляет поле для ввода пароля (класс [TextInput |api:Nette\Forms\Controls\TextInput]). ```php $form->addPassword('password', 'Пароль:') ->setRequired() - ->addRule($form::MinLength, 'Пароль должен содержать не менее %d символов', 8) - ->addRule($form::Pattern, 'Должен содержать цифру', '.*[0-9].*'); + ->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов', 8) + ->addRule($form::Pattern, 'Пароль должен содержать цифру', '.*[0-9].*'); ``` -При повторном отображении формы поле будет пустым. Автоматически проверяет UTF-8, обрезает пробелы слева и справа и удаляет переводы строк, которые мог бы отправить злоумышленник. +При повторном отображении формы поле будет пустым. Автоматически проверяет UTF-8, обрезает пробелы слева и справа и удаляет переводы строк, которые мог отправить злоумышленник. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -Добавляет флажок (чекбокс) (класс [Checkbox |api:Nette\Forms\Controls\Checkbox]). Возвращает значение `true` или `false`, в зависимости от того, установлен ли флажок. +Добавляет флажок (класс [Checkbox |api:Nette\Forms\Controls\Checkbox]). Возвращает `true` или `false` в зависимости от того, отмечен ли он. ```php $form->addCheckbox('agree', 'Я согласен с условиями') - ->setRequired('Необходимо согласиться с условиями'); + ->setRequired('С условиями нужно согласиться'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Добавляет флажки для выбора нескольких элементов (класс [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Возвращает массив ключей выбранных элементов. Метод `getSelectedItems()` возвращает значения вместо ключей. +Добавляет список флажков для выбора нескольких пунктов (класс [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Возвращает массив ключей выбранных пунктов. Метод `getSelectedItems()` возвращает выбранные пункты парами ключ-значение. ```php $form->addCheckboxList('colors', 'Цвета:', [ 'r' => 'красный', - 'g' => 'зеленый', + 'g' => 'зелёный', 'b' => 'синий', ]); ``` -Массив предлагаемых элементов передаем как третий параметр или методом `setItems()`. +Массив предлагаемых пунктов передайте третьим параметром или методом `setItems()`. Если вторым аргументом `setItems()` передать `false`, значения будут использованы и как ключи. -С помощью `setDisabled(['r', 'g'])` можно деактивировать отдельные элементы. +Отдельные пункты отключаются через `setDisabled(['r', 'g'])`. -Элемент автоматически проверяет, не произошла ли подделка и что выбранные элементы действительно являются одними из предлагаемых и не были деактивированы. Методом `getRawValue()` можно получить отправленные элементы без этой важной проверки. +Элемент автоматически проверяет, что подделки не произошло, что выбранные пункты действительно были среди предложенных и не были отключены. Методом `getRawValue()` можно получить отправленные пункты без этой важной проверки. -При установке выбранных по умолчанию элементов также проверяет, что они являются одними из предлагаемых, иначе выбрасывает исключение. Эту проверку можно отключить с помощью `checkDefaultValue(false)`. +При задании выбранных по умолчанию пунктов тоже проверяется, что они есть среди предложенных, иначе выбрасывается исключение. Эту проверку можно отключить через `checkDefaultValue(false)`. -Если вы отправляете форму методом `GET`, вы можете выбрать более компактный способ передачи данных, который экономит размер строки запроса. Он активируется установкой HTML-атрибута формы: +Если вы отправляете форму методом `GET`, можно выбрать более компактный способ передачи данных, экономящий размер строки запроса. Он включается заданием HTML-атрибута формы: ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ -Добавляет переключатели (радиокнопки) (класс [RadioList |api:Nette\Forms\Controls\RadioList]). Возвращает ключ выбранного элемента или `null`, если пользователь ничего не выбрал. Метод `getSelectedItem()` возвращает значение вместо ключа. +Добавляет радиокнопки (класс [RadioList |api:Nette\Forms\Controls\RadioList]). Возвращает ключ выбранного пункта либо `null`, если пользователь ничего не выбрал. Метод `getSelectedItem()` возвращает не ключ, а значение. ```php $sex = [ - 'm' => 'мужчина', - 'f' => 'женщина', + 'm' => 'мужской', + 'f' => 'женский', + 'o' => 'другой', ]; $form->addRadioList('gender', 'Пол:', $sex); ``` -Массив предлагаемых элементов передаем как третий параметр или методом `setItems()`. +Массив предлагаемых пунктов передайте третьим параметром или методом `setItems()`. -С помощью `setDisabled(['m', 'f'])` можно деактивировать отдельные элементы. +Отдельные пункты отключаются через `setDisabled(['m'])`. -Элемент автоматически проверяет, не произошла ли подделка и что выбранный элемент действительно является одним из предлагаемых и не был деактивирован. Методом `getRawValue()` можно получить отправленный элемент без этой важной проверки. +Элемент автоматически проверяет, что подделки не произошло, что выбранный пункт действительно был среди предложенных и не был отключён. Методом `getRawValue()` можно получить отправленный пункт без этой важной проверки. -При установке выбранного по умолчанию элемента также проверяет, что он является одним из предлагаемых, иначе выбрасывает исключение. Эту проверку можно отключить с помощью `checkDefaultValue(false)`. +При задании выбранного по умолчанию пункта тоже проверяется, что он есть среди предложенных, иначе выбрасывается исключение. Эту проверку можно отключить через `checkDefaultValue(false)`. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -Добавляет выпадающий список (select box) (класс [SelectBox |api:Nette\Forms\Controls\SelectBox]). Возвращает ключ выбранного элемента или `null`, если пользователь ничего не выбрал. Метод `getSelectedItem()` возвращает значение вместо ключа. +Добавляет выпадающий список (класс [SelectBox |api:Nette\Forms\Controls\SelectBox]). Возвращает ключ выбранного пункта либо `null`, если пользователь ничего не выбрал. Метод `getSelectedItem()` возвращает не ключ, а значение. ```php $countries = [ - 'CZ' => 'Чешская Республика', + 'CZ' => 'Чехия', 'SK' => 'Словакия', - 'RU' => 'Россия', + 'GB' => 'Великобритания', ]; $form->addSelect('country', 'Страна:', $countries) - ->setDefaultValue('RU'); + ->setDefaultValue('SK'); ``` -Массив предлагаемых элементов передаем как третий параметр или методом `setItems()`. Элементы могут быть и двумерным массивом: +Массив предлагаемых пунктов передайте третьим параметром или методом `setItems()`. Пункты могут быть и двумерным массивом (он представляет optgroup): ```php $countries = [ 'Европа' => [ - 'CZ' => 'Чешская Республика', + 'CZ' => 'Чехия', 'SK' => 'Словакия', 'GB' => 'Великобритания', ], - 'RU' => 'Россия', + 'CA' => 'Канада', 'US' => 'США', '?' => 'другая', ]; ``` -У выпадающих списков часто первый элемент имеет особое значение, служит призывом к действию. Для добавления такого элемента служит метод `setPrompt()`. +У выпадающих списков первый пункт часто имеет особое значение и служит призывом к действию. Для добавления такого пункта служит метод `setPrompt()`. ```php $form->addSelect('country', 'Страна:', $countries) ->setPrompt('Выберите страну'); ``` -С помощью `setDisabled(['CZ', 'SK'])` можно деактивировать отдельные элементы. +Отдельные пункты отключаются через `setDisabled(['CZ', 'SK'])`. -Элемент автоматически проверяет, не произошла ли подделка и что выбранный элемент действительно является одним из предлагаемых и не был деактивирован. Методом `getRawValue()` можно получить отправленный элемент без этой важной проверки. +Элемент автоматически проверяет, что подделки не произошло, что выбранный пункт действительно был среди предложенных и не был отключён. Методом `getRawValue()` можно получить отправленный пункт без этой важной проверки. -При установке выбранного по умолчанию элемента также проверяет, что он является одним из предлагаемых, иначе выбрасывает исключение. Эту проверку можно отключить с помощью `checkDefaultValue(false)`. +При задании выбранного по умолчанию пункта тоже проверяется, что он есть среди предложенных, иначе выбрасывается исключение. Эту проверку можно отключить через `checkDefaultValue(false)`. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Добавляет выпадающий список для выбора нескольких элементов (класс [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Возвращает массив ключей выбранных элементов. Метод `getSelectedItems()` возвращает значения вместо ключей. +Добавляет выпадающий список для выбора нескольких пунктов (класс [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Возвращает массив ключей выбранных пунктов. Метод `getSelectedItems()` возвращает выбранные пункты парами ключ-значение. ```php $form->addMultiSelect('countries', 'Страны:', $countries); ``` -Массив предлагаемых элементов передаем как третий параметр или методом `setItems()`. Элементы могут быть и двумерным массивом. +Массив предлагаемых пунктов передайте третьим параметром или методом `setItems()`. Пункты могут быть и двумерным массивом. -С помощью `setDisabled(['CZ', 'SK'])` можно деактивировать отдельные элементы. +Отдельные пункты отключаются через `setDisabled(['CZ', 'SK'])`. -Элемент автоматически проверяет, не произошла ли подделка и что выбранные элементы действительно являются одними из предлагаемых и не были деактивированы. Методом `getRawValue()` можно получить отправленные элементы без этой важной проверки. +Элемент автоматически проверяет, что подделки не произошло, что выбранные пункты действительно были среди предложенных и не были отключены. Методом `getRawValue()` можно получить отправленные пункты без этой важной проверки. -При установке выбранных по умолчанию элементов также проверяет, что они являются одними из предлагаемых, иначе выбрасывает исключение. Эту проверку можно отключить с помощью `checkDefaultValue(false)`. +При задании выбранных по умолчанию пунктов тоже проверяется, что они есть среди предложенных, иначе выбрасывается исключение. Эту проверку можно отключить через `checkDefaultValue(false)`. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Добавляет поле для загрузки файла (класс [UploadControl |api:Nette\Forms\Controls\UploadControl]). Возвращает объект [FileUpload |http:request#FileUpload], даже если пользователь не отправил ни одного файла, что можно проверить методом `FileUpload::hasFile()`. +Добавляет поле для загрузки файла (класс [UploadControl |api:Nette\Forms\Controls\UploadControl]). Возвращает объект [FileUpload |http:request#FileUpload], даже если пользователь никакого файла не загрузил, что можно выяснить методом `FileUpload::hasFile()`. С помощью `setNullable()` можно добиться, чтобы при отсутствии загруженного файла элемент возвращал `null` вместо объекта `FileUpload`. ```php $form->addUpload('avatar', 'Аватар:') - ->addRule($form::Image, 'Аватар должен быть в формате JPEG, PNG, GIF, WebP или AVIF.') - ->addRule($form::MaxFileSize, 'Максимальный размер 1 МБ.', 1024 * 1024); + ->addRule($form::Image, 'Аватар должен быть JPEG, PNG, GIF, WebP или AVIF.') + ->addRule($form::MaxFileSize, 'Максимальный размер - 1 МБ.', 1024 * 1024); ``` -Если файл не удается корректно загрузить, форма не отправляется успешно и отображается ошибка. То есть при успешной отправке нет необходимости проверять метод `FileUpload::isOk()`. +Если файл не удалось загрузить корректно, форма не считается успешно отправленной и отображается ошибка. То есть при успешной отправке проверять метод `FileUpload::isOk()` не нужно. -Никогда не доверяйте оригинальному имени файла, возвращаемому методом `FileUpload::getName()`, клиент мог отправить вредоносное имя файла с целью повредить или взломать ваше приложение. +Никогда не доверяйте исходному имени файла, которое возвращает метод `FileUpload::getName()`: клиент мог отправить вредоносное имя файла с намерением повредить или взломать ваше приложение. -Правила `MimeType` и `Image` определяют требуемый тип на основе сигнатуры файла и не проверяют его целостность. Не повреждено ли изображение, можно выяснить, например, попытавшись его [загрузить |http:request#toImage]. +Правила `MimeType` и `Image` определяют нужный тип по сигнатуре файла и не проверяют его целостность. Выяснить, не повреждено ли изображение, можно, например, попыткой [его загрузить |http:request#toImage()]. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Добавляет поле для загрузки нескольких файлов одновременно (класс [UploadControl |api:Nette\Forms\Controls\UploadControl]). Возвращает массив объектов [FileUpload |http:request#FileUpload]. Метод `FileUpload::hasFile()` у каждого из них будет возвращать `true`. +Добавляет поле для загрузки нескольких файлов сразу (класс [UploadControl |api:Nette\Forms\Controls\UploadControl]). Возвращает массив объектов [FileUpload |http:request#FileUpload]. Метод `FileUpload::hasFile()` у каждого из них вернёт `true`. ```php $form->addMultiUpload('files', 'Файлы:') - ->addRule($form::MaxLength, 'Максимально можно загрузить %d файлов', 10); + ->addRule($form::MaxLength, 'Можно загрузить не более %d файлов.', 10); ``` -Если какой-либо файл не удается корректно загрузить, форма не отправляется успешно и отображается ошибка. То есть при успешной отправке нет необходимости проверять метод `FileUpload::isOk()`. +Если какой-нибудь файл не удалось загрузить корректно, форма не считается успешно отправленной и отображается ошибка. То есть при успешной отправке проверять метод `FileUpload::isOk()` у каждого файла не нужно. -Никогда не доверяйте оригинальным именам файлов, возвращаемым методом `FileUpload::getName()`, клиент мог отправить вредоносное имя файла с целью повредить или взломать ваше приложение. +Никогда не доверяйте исходным именам файлов, которые возвращает метод `FileUpload::getName()`: клиент мог отправить вредоносные имена файлов с намерением повредить или взломать ваше приложение. -Правила `MimeType` и `Image` определяют требуемый тип на основе сигнатуры файла и не проверяют его целостность. Не повреждено ли изображение, можно выяснить, например, попытавшись его [загрузить |http:request#toImage]. +Правила `MimeType` и `Image` определяют нужный тип по сигнатуре файла и не проверяют его целостность. Выяснить, не повреждено ли изображение, можно, например, попыткой [его загрузить |http:request#toImage()]. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== -Добавляет поле, которое позволяет пользователю легко ввести дату, состоящую из года, месяца и дня (класс [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Добавляет поле, которое позволяет пользователю легко ввести дату из года, месяца и дня (класс [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -В качестве значения по умолчанию принимает либо объекты, реализующие интерфейс `DateTimeInterface`, строку с временем, либо число, представляющее UNIX timestamp. То же самое относится к аргументам правил `Min`, `Max` или `Range`, которые определяют минимальную и максимальную допустимую дату. +В качестве значения по умолчанию оно принимает объекты, реализующие `DateTimeInterface`, строку со временем или число, обозначающее временную метку UNIX. То же относится к аргументам правил `Min`, `Max` или `Range`, задающих минимальную и максимальную допустимую дату. ```php $form->addDate('date', 'Дата:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Дата должна быть не ранее месяца назад.', new DateTime('-1 month')); + ->addRule($form::Min, 'Дата должна быть не менее чем месячной давности.', new DateTime('-1 month')); ``` -По умолчанию возвращает объект `DateTimeImmutable`, методом `setFormat()` вы можете указать [текстовый формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] или timestamp: +По умолчанию возвращает объект `DateTimeImmutable`. Методом `setFormat()` можно задать [текстовый формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] или временную метку: ```php $form->addDate('date', 'Дата:') @@ -287,19 +288,19 @@ $form->addDate('date', 'Дата:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -Добавляет поле, которое позволяет пользователю легко ввести время, состоящее из часов, минут и, опционально, секунд (класс [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Добавляет поле, которое позволяет пользователю легко ввести время из часов, минут и необязательно секунд (класс [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -В качестве значения по умолчанию принимает либо объекты, реализующие интерфейс `DateTimeInterface`, строку с временем, либо число, представляющее UNIX timestamp. Из этих входных данных используется только информация о времени, дата игнорируется. То же самое относится к аргументам правил `Min`, `Max` или `Range`, которые определяют минимальное и максимальное допустимое время. Если установленное минимальное значение выше максимального, создается временной диапазон, пересекающий полночь. +В качестве значения по умолчанию оно принимает объекты, реализующие `DateTimeInterface`, строку со временем или число, обозначающее временную метку UNIX. Из этих входных данных используются только сведения о времени, дата игнорируется. То же относится к аргументам правил `Min`, `Max` или `Range`, задающих минимальное и максимальное допустимое время. Если заданное минимальное значение больше максимального, возникает диапазон времени, проходящий через полночь. ```php $form->addTime('time', 'Время:', withSeconds: true) - ->addRule($form::Range, 'Время должно быть в диапазоне от %s до %s.', ['12:30', '13:30']); + ->addRule($form::Range, 'Время должно быть между %d и %d.', ['12:30', '13:30']); ``` -По умолчанию возвращает объект `DateTimeImmutable` (с датой 1 января 1 года), методом `setFormat()` вы можете указать [текстовый формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: +По умолчанию возвращает объект `DateTimeImmutable` (с датой, заданной как 1 января 1 года). Методом `setFormat()` можно задать [текстовый формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: ```php $form->addTime('time', 'Время:') @@ -307,20 +308,20 @@ $form->addTime('time', 'Время:') ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -Добавляет поле, которое позволяет пользователю легко ввести дату и время, состоящие из года, месяца, дня, часов, минут и, опционально, секунд (класс [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Добавляет поле, которое позволяет пользователю легко ввести и дату, и время из года, месяца, дня, часов, минут и необязательно секунд (класс [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). -В качестве значения по умолчанию принимает либо объекты, реализующие интерфейс `DateTimeInterface`, строку с временем, либо число, представляющее UNIX timestamp. То же самое относится к аргументам правил `Min`, `Max` или `Range`, которые определяют минимальную и максимальную допустимую дату. +В качестве значения по умолчанию оно принимает объекты, реализующие `DateTimeInterface`, строку со временем или число, обозначающее временную метку UNIX. То же относится к аргументам правил `Min`, `Max` или `Range`, задающих минимальные и максимальные допустимые дату и время. ```php $form->addDateTime('datetime', 'Дата и время:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Дата должна быть не ранее месяца назад.', new DateTime('-1 month')); + ->addRule($form::Min, 'Дата должна быть не менее чем месячной давности.', 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: +По умолчанию возвращает объект `DateTimeImmutable`. Методом `setFormat()` можно задать [текстовый формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] или временную метку: ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -Добавляет поле для выбора цвета (класс [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Цвет — это строка в формате `#rrggbb`. Если пользователь не сделал выбор, возвращается черный цвет `#000000`. +Добавляет поле выбора цвета (класс [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Цвет возвращается строкой в формате `#rrggbb`. Если пользователь ничего не выберет, возвращается чёрный `#000000`. ```php $form->addColor('color', 'Цвет:') @@ -339,8 +340,8 @@ $form->addColor('color', 'Цвет:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== Добавляет скрытое поле (класс [HiddenField |api:Nette\Forms\Controls\HiddenField]). @@ -348,13 +349,13 @@ addHidden(string|int $name, ?string $default=null): HiddenField .[method] $form->addHidden('userid'); ``` -С помощью `setNullable()` можно настроить, чтобы возвращался `null` вместо пустой строки. Изменить отправленное значение позволяет [addFilter() |validation#Изменение ввода]. +С помощью `setNullable()` можно добиться, чтобы вместо пустой строки возвращался `null`. Метод [addFilter() |validation#Изменение введённых значений] позволяет изменить отправленное значение. -Хотя элемент скрыт, **важно помнить**, что значение все еще может быть изменено или подделано злоумышленником. Всегда тщательно проверяйте и валидируйте все полученные значения на стороне сервера, чтобы предотвратить риски безопасности, связанные с манипуляцией данными. +Хотя элемент скрыт, **важно осознавать**, что его значение всё равно может быть изменено или подделано злоумышленником. Всегда тщательно проверяйте все полученные значения на стороне сервера, чтобы предотвратить риски безопасности, связанные с манипуляцией данными. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== Добавляет кнопку отправки (класс [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). @@ -362,14 +363,23 @@ addSubmit(string|int $name, $caption=null): SubmitButton .[method] $form->addSubmit('submit', 'Отправить'); ``` -В форме может быть несколько кнопок отправки: +.{data-version:3.3.0} +Обработчик можно передать кнопке напрямую третьим параметром `$onSubmit` вместо того, чтобы привязывать его к событию `onClick`: + +```php +$form->addSubmit('submit', 'Отправить', function (SubmitButton $button, $data): void { + // ... +}); +``` + +В форме может быть и больше одной кнопки отправки: ```php $form->addSubmit('register', 'Зарегистрироваться'); -$form->addSubmit('cancel', 'Отмена'); +$form->addSubmit('cancel', 'Отменить'); ``` -Чтобы определить, какая из них была нажата, используйте: +Чтобы определить, по какой из них щёлкнули, используйте: ```php if ($form['register']->isSubmittedBy()) { @@ -377,13 +387,13 @@ if ($form['register']->isSubmittedBy()) { } ``` -Если вы не хотите валидировать всю форму при нажатии кнопки (например, для кнопок *Отмена* или *Предпросмотр*), используйте [setValidationScope() |validation#Отключение валидации]. +Если вы не хотите проверять всю форму при нажатии кнопки (например, для кнопок *Отмена* или *Предпросмотр*), используйте [setValidationScope() |validation#Отключение проверки]. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -Добавляет кнопку (класс [Button |api:Nette\Forms\Controls\Button]), которая не имеет функции отправки. Ее можно использовать для какой-либо другой функции, например, вызова JavaScript-функции при нажатии. +Добавляет кнопку (класс [Button |api:Nette\Forms\Controls\Button]), у которой нет функции отправки. Её можно использовать для других задач, например для вызова JavaScript-функции по щелчку. ```php $form->addButton('raise', 'Повысить зарплату') @@ -391,22 +401,22 @@ $form->addButton('raise', 'Повысить зарплату') ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= -Добавляет кнопку отправки в виде изображения (класс [ImageButton |api:Nette\Forms\Controls\ImageButton]). +Добавляет кнопку отправки в виде картинки (класс [ImageButton |api:Nette\Forms\Controls\ImageButton]). ```php -$form->addImageButton('submit', '/path/to/image'); +$form->addImageButton('submit', '/path/to/image.png', 'Отправить'); ``` -При использовании нескольких кнопок отправки можно определить, какая была нажата, с помощью `$form['submit']->isSubmittedBy()`. +При использовании нескольких кнопок отправки определить, по какой из них щёлкнули, можно через `$form['submit']->isSubmittedBy()`. addContainer(string|int $name): Container .[method] =================================================== -Добавляет подформу (класс [Container|api:Nette\Forms\Container]), или контейнер, в который можно добавлять другие элементы так же, как мы добавляем их в форму. Работают также методы `setDefaults()` или `getValues()`. +Добавляет подформу (класс [Container|api:Nette\Forms\Container]), то есть контейнер, в который можно добавлять другие элементы так же, как их добавляют в форму. Работают и методы вроде `setDefaults()` или `getValues()`. ```php $sub1 = $form->addContainer('first'); @@ -437,66 +447,64 @@ $sub2->addEmail('email', 'Email:'); Обзор настроек ============== -Для всех элементов мы можем вызывать следующие методы (полный обзор в [документации API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): +У всех элементов мы можем вызвать следующие методы (полный обзор смотрите в [документации API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): .[table-form-methods language-php] -| `setDefaultValue($value)` | устанавливает значение по умолчанию -| `getValue()` | получить текущее значение -| `setOmitted()` | [#Исключение значения] -| `setDisabled()` | [#Деактивация элементов] +| `setDefaultValue($value)` | задаёт значение по умолчанию +| `getValue()` | получает текущее значение +| `setOmitted()` | [#Опущенные значения] +| `setDisabled()` | [#Отключение элементов] -Отображение: +Отрисовка: .[table-form-methods language-php] -| `setCaption($caption)` | изменяет метку элемента -| `setTranslator($translator)` | устанавливает [переводчик |rendering#Перевод] -| `setHtmlAttribute($name, $value)` | устанавливает [HTML атрибут |rendering#HTML-атрибуты] элемента -| `setHtmlId($id)` | устанавливает HTML атрибут `id` -| `setHtmlType($type)` | устанавливает HTML атрибут `type` -| `setHtmlName($name)` | устанавливает HTML атрибут `name` -| `setOption($key, $value)` | [настройки для отображения |rendering#Options] - -Валидация: +| `setCaption($caption)` | меняет метку элемента +| `setTranslator($translator)` | задаёт [переводчик |rendering#Перевод] +| `setHtmlAttribute($name, $value)` | задаёт [HTML-атрибут |rendering#HTML-атрибуты] элемента +| `setHtmlId($id)` | задаёт HTML-атрибут `id` +| `setOption($key, $value)` | [задаёт параметры отрисовки |rendering#Параметры] + +Проверка: .[table-form-methods language-php] -| `setRequired()` | [обязательный элемент |validation] -| `addRule()` | установка [правила валидации |validation#Правила] -| `addCondition()`, `addConditionOn()` | установка [условия валидации |validation#Условия] -| `addError($message)` | [передача сообщения об ошибке |validation#Ошибки при обработке] +| `setRequired()` | делает элемент [обязательным |validation] +| `addRule()` | добавляет [правило проверки |validation#Правила] +| `addCondition()`, `addConditionOn()` | задаёт [условие проверки |validation#Условия] +| `addError($message)` | [добавляет сообщение об ошибке |validation#Обработка ошибок] -Для элементов `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` можно вызывать следующие методы: +У элементов `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` можно вызвать следующие методы: .[table-form-methods language-php] -| `setNullable()` | устанавливает, вернет ли getValue() `null` вместо пустой строки -| `setEmptyValue($value)` | устанавливает специальное значение, которое считается пустой строкой -| `setMaxLength($length)` | устанавливает максимальное количество разрешенных символов -| `addFilter($filter)` | [изменение ввода |validation#Изменение ввода] +| `setNullable()` | задаёт, будет ли getValue() возвращать `null` вместо пустой строки +| `setEmptyValue($value)` | задаёт особое значение, которое считается пустой строкой +| `setMaxLength($length)` | задаёт максимально допустимое количество символов +| `addFilter($filter)` | [изменяет ввод |validation#Изменение введённых значений] -Исключение значения -=================== +Опущенные значения +================== -Если нас не интересует значение, введенное пользователем, мы можем с помощью `setOmitted()` исключить его из результата метода `$form->getValues()` или из данных, передаваемых в обработчики. Это удобно для различных паролей для проверки, антиспам-элементов и т. д. +Если значение, заполненное пользователем, нас не интересует, мы можем через `setOmitted()` исключить его из результата метода `$form->getValues()` или из данных, передаваемых обработчикам. Это удобно для разных полей подтверждения пароля, антиспам-элементов и т. п. ```php -$form->addPassword('passwordVerify', 'Пароль для проверки:') - ->setRequired('Пожалуйста, введите пароль еще раз для проверки') +$form->addPassword('passwordVerify', 'Пароль ещё раз:') + ->setRequired('Введите пароль ещё раз для проверки опечатки') ->addRule($form::Equal, 'Пароли не совпадают', $form['password']) ->setOmitted(); ``` -Деактивация элементов -===================== +Отключение элементов +==================== -Элементы можно деактивировать с помощью `setDisabled()`. Такой элемент пользователь не может редактировать. +Элементы можно отключить через `setDisabled()`. Отключённый элемент пользователь не может редактировать. ```php $form->addText('username', 'Имя пользователя:') ->setDisabled(); ``` -Отключенные элементы браузер вообще не отправляет на сервер, поэтому их не найти в данных, возвращаемых функцией `$form->getValues()`. Однако, если вы установите `setOmitted(false)`, Nette включит в эти данные их значение по умолчанию. +Отключённые элементы браузер вообще не отправляет на сервер, так что в данных, которые возвращает функция `$form->getValues()`, вы их не найдёте. Однако если задать `setOmitted(false)`, Nette включит в эти данные их значение по умолчанию. -При вызове `setDisabled()` из соображений безопасности **значение элемента удаляется**. Если вы устанавливаете значение по умолчанию, это необходимо делать после его деактивации: +При вызове `setDisabled()` **значение элемента очищается** из соображений безопасности. Если вы задаёте значение по умолчанию, делать это нужно после отключения: ```php $form->addText('username', 'Имя пользователя:') @@ -504,42 +512,26 @@ $form->addText('username', 'Имя пользователя:') ->setDefaultValue($userName); ``` -Альтернативой отключенным элементам являются элементы с HTML-атрибутом `readonly`, которые браузер отправляет на сервер. Хотя элемент предназначен только для чтения, **важно помнить**, что его значение все еще может быть изменено или подделано злоумышленником. +Альтернатива отключённым элементам - элементы с HTML-атрибутом `readonly`, которые браузер на сервер отправляет. Хотя элемент доступен только для чтения, **важно осознавать**, что его значение всё равно может быть изменено или подделано злоумышленником. -Пользовательские элементы -========================= +Собственные элементы +==================== -Помимо широкого спектра встроенных элементов формы, вы можете добавлять в форму пользовательские элементы следующим образом: +Кроме широкого набора встроенных элементов формы, в форму можно добавлять собственные: ```php $form->addComponent(new DateInput('Дата:'), 'date'); -// альтернативный синтаксис: $form['date'] = new DateInput('Дата:'); +// альтернативная запись: $form['date'] = new DateInput('Дата:'); ``` -.[note] -Форма является потомком класса [Container |component-model:#Container], а отдельные элементы — потомками [Component |component-model:#Component]. +Как написать такой элемент, включая чтение отправленных данных, проверку и отрисовку, описано в [отдельной главе |custom-controls]. Там же вы узнаете о методах-расширениях, которые позволяют создать собственный метод добавления вроде `$form->addZip()`. -Существует способ определить новые методы формы, служащие для добавления пользовательских элементов (например, `$form->addZip()`). Это так называемые extension methods. Недостаток в том, что для них не будет работать автодополнение в редакторах. -```php -use Nette\Forms\Container; - -// добавим метод addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Не менее 5 цифр', '[0-9]{5}'); -}); - -// использование -$form->addZip('zip', 'Почтовый индекс:'); -``` - - -Низкоуровневые элементы -======================= +Низкоуровневые поля +=================== -Можно использовать и элементы, которые мы записываем только в шаблоне и не добавляем в форму каким-либо из методов `$form->addXyz()`. Например, когда мы выводим записи из базы данных и заранее не знаем, сколько их будет и какие у них будут ID, и хотим у каждой строки отобразить чекбокс или радиокнопку, достаточно закодировать это в шаблоне: +Можно использовать и элементы, которые записаны только в шаблоне и не добавлены в форму ни одним из методов `$form->addXyz()`. Например, когда мы выводим записи из базы данных и заранее не знаем, сколько их будет и какие у них будут ID, а хотим для каждой строки показать флажок или радиокнопку, мы можем просто написать это в шаблоне: ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ $form->addZip('zip', 'Почтовый индекс:'); {/foreach} ``` -А после отправки узнать значение: +А после отправки получим значение: ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -где первый параметр — это тип элемента (`DataFile` для `type=file`, `DataLine` для однострочных вводов, таких как `text`, `password`, `email` и т. д., и `DataText` для всех остальных), а второй параметр `sel[]` соответствует HTML-атрибуту name. Тип элемента можно комбинировать со значением `DataKeys`, которое сохраняет ключи элементов. Это особенно полезно для `select`, `radioList` и `checkboxList`. +где первый параметр - тип элемента (`DataFile` для `type=file`, `DataLine` для однострочных полей вроде `text`, `password`, `email` и т. п. и `DataText` для всех остальных), а второй параметр `sel[]` соответствует HTML-атрибуту name. Тип элемента можно сочетать со значением `DataKeys`, которое сохраняет ключи элементов. Особенно это полезно для `select`, `radioList` и `checkboxList`. -Существенно то, что `getHttpData()` возвращает санитайзенное значение, в данном случае это всегда будет массив валидных UTF-8 строк, независимо от того, что попытался бы подсунуть серверу злоумышленник. Это аналог прямой работы с `$_POST` или `$_GET`, но с тем существенным отличием, что всегда возвращаются чистые данные, так, как вы привыкли у стандартных элементов форм Nette. +Существенно, что `getHttpData()` возвращает очищенное значение. В данном случае это всегда будет массив корректных UTF-8-строк независимо от того, что попытается отправить на сервер злоумышленник. Это аналогично прямой работе с `$_POST` или `$_GET`, но с той существенной разницей, что вы всегда получаете чистые данные, как вы привыкли у стандартных элементов форм Nette. diff --git a/forms/ru/custom-controls.texy b/forms/ru/custom-controls.texy new file mode 100644 index 0000000000..e0a0b9fdc8 --- /dev/null +++ b/forms/ru/custom-controls.texy @@ -0,0 +1,268 @@ +Собственные элементы форм +************************* + +.[perex] +Nette предлагает широкую палитру [встроенных элементов форм |controls]. Но когда вы столкнётесь с требованием, которого среди них нет, ничего обходить и склеивать не придётся: вы напишете собственный элемент. Он будет уметь всё то же, что и встроенные - проверять, переводиться, отрисовываться - и использоваться будет точно так же. + +Покажем это на практическом примере: элемент для ввода даты тремя полями - день, месяц и год. Попутно вы узнаете всё, что нужно знать о написании элементов. + + +Когда писать собственный элемент, а когда нет +============================================= + +Собственный элемент - самый мощный инструмент, который предлагают формы. И, как всякий мощный инструмент, он должен быть последним выбором, а не первым. Многие ситуации решаются более простыми средствами: + +- **Изменение значения** решает [addFilter() |validation#Изменение введённых значений]. Хотите допускать пробелы в почтовом индексе или строчные буквы в коде? Фильтр - это несколько строк. +- **Повторяющуюся настройку** оборачивают в собственный метод добавления. Добавляете поле почтового индекса с одинаковой проверкой в десяти местах? Сделайте для них именованное сокращение, [покажем это в конце |#Собственный метод добавления]. +- **Группе связанных полей** служит [контейнер |controls#addContainer()]. Адресу из улицы, города и индекса собственный элемент не нужен, хватит контейнера с тремя текстовыми полями. +- **Другой внешний вид** достигается через [setHtmlType() |controls#addText()] и HTML-атрибуты либо через [прототипы |rendering#Прототипы]. + +Собственный элемент имеет смысл в тот момент, когда вам нужно **собственное значение**: элемент, который снаружи ведёт себя как одно поле с одним значением, но внутри состоит из нескольких инпутов или хранит значение иначе, чем показывает. Дата из трёх полей. Координаты, выбранные щелчком по карте. Ввод тегов с автодополнением. + + +Анатомия элемента +================= + +Каждый собственный элемент наследуется от абстрактного класса [api:Nette\Forms\Controls\BaseControl]. От него он получает огромное количество готовой функциональности: хранение значения, правила и условия проверки, сообщения об ошибках, переводы, HTML-атрибуты, метку и связь с отрисовкой. Вы пишете только то, чем ваш элемент отличается. + +Минимальный работающий элемент на удивление короток: + +```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, + ]); + } +} +``` + +Два метода: один говорит, как получить значение из отправленных данных, второй - как элемент отрисовать. На оба мы через минуту посмотрим подробно. Всё остальное - `setRequired()`, `addRule()`, `setDefaultValue()`, переводы - работает уже само. + +В форму элемент добавляют методом `addComponent()` или короче, через квадратные скобки: + +```php +$form['nickname'] = new SimpleInput('Ник:'); +``` + + +Жизненный цикл элемента +======================= + +Прежде чем перейти к более интересному элементу, полезно знать, что и когда с элементом происходит. Форма и её элементы - это [компоненты |component-model:], образующие дерево. У этого есть одно приятное следствие: элементу не нужно ничего выяснять самому, обо всём важном в нужный момент позаботится фреймворк: + +1) В момент, когда вы присоедините элемент к отправленной форме, сама форма вызовет у него `loadHttpData()`. В нём элемент считывает своё отправленное значение, как мы сейчас покажем. Он никогда не работает напрямую с `$_POST` и ему совершенно неважно, вложен ли он в контейнеры. + +2) При отправке формы происходит проверка: вычисляются правила, добавленные через `addRule()`, и работают они со значением из `getValue()`. + +3) Тот, кто затем вызовет `$form->getValues()` или `getValue()` у элемента, получит чистое типизированное значение - например, объект `DateTimeImmutable`, а не тройку строк из формы. + +А при отрисовке вызывается `getControl()`, а для метки - `getLabel()`. + + +Чтение отправленного значения +============================= + +В методе `loadHttpData()` элемент запрашивает своё отправленное значение методом `getHttpData()`. Его параметр - тип, определяющий, как значение нужно очистить: + +| тип | значение +|------- +| `Form::DataLine` | однострочный текст: заменяет переводы строк пробелами, обрезает пробелы +| `Form::DataText` | многострочный текст: приводит окончания строк к `\n` +| `Form::DataFile` | загрузка файла, экземпляр `Nette\Http\FileUpload` + +Как бы злоумышленник ни старался, результатом всегда будет корректная UTF-8-строка без управляющих символов (либо объект загрузки, либо `null`). Именно поэтому мы никогда не читаем значение прямо из `$_POST`: мы потеряли бы все эти гарантии. + +Элемент, состоящий из нескольких инпутов, как наша дата, передаёт часть HTML-имени вторым параметром и так считывает свои отдельные подзначения. Он хранит их в собственных свойствах `$day`, `$month` и `$year` типа 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]') ?? ''; +} +``` + +Если HTML-имя заканчивается на `[]`, возвращается массив значений. В сочетании с типом `Form::DataKeys` (то есть `Form::DataLine | Form::DataKeys`) вы сохраните и его ключи: + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +Отсутствующее значение - это `null` (для массивов пустой массив). Запрос вообще может не содержать данных элемента, ничто не мешает злоумышленнику отправить что угодно - именно поэтому в примере мы дописываем `?? ''`, и именно поэтому такой вариант нужно учитывать всегда. + + +Значение элемента +================= + +Элемент хранит своё значение и предоставляет его через тройку методов, договорённостей которых стоит придерживаться. + +Метод `setValue()` принимает значение от программиста - этим же путём идут `setDefaultValue()` и `$form->setDefaults()`. Он должен принимать всё, что имеет смысл, преобразовывать значение во внутреннюю форму и выбрасывать исключение на бессмысленном вводе, чтобы ошибка проявилась сразу, а не через загадочное поведение формы. Наша дата принимает `DateTimeInterface`, строку, метку времени или `null` и раскладывает их на три поля: + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // бессмыслица выбросит исключение + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +Метод `getValue()`, наоборот, собирает чистое типизированное значение - единственное, что увидит пользователь вашего элемента. Если значение некорректно, он возвращает `null`. Статический метод `validateDate()` просто проверяет, что три поля складываются в существующую дату: + +```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; +} +``` + +А метод `isFilled()` говорит, заполнил ли пользователь элемент; его использует правило `setRequired()`. Реализации по умолчанию (непустое значение) часто хватает, но для составного элемента переопределите его согласно его логике: + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Отрисовка +========= + +Метод `getControl()` возвращает HTML-вид элемента, обычно как объект [Html |utils:html-elements], но подойдёт и обычная строка - это неважно. К объекту Html мы обращаемся в основном при сборке кода, потому что он позволяет строить итоговую разметку безопасно и с приятным API. В вашем распоряжении несколько помощников: + +- `getHtmlName()` возвращает HTML-атрибут `name`, включая возможную вложенность в контейнеры (например, `invoice[date]`). Для составного элемента вы дописываете к нему части имён отдельных инпутов: `$name . '[day]'`. +- `getHtmlId()` возвращает атрибут `id`, связанный с меткой. +- `Helpers::exportRules($this->getRules())` экспортирует правила проверки для атрибута `data-nette-rules`, благодаря чему для вашего элемента заработает и [проверка на JavaScript |validation#Проверка на JavaScript]. Атрибут ставится на первый инпут элемента. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)` собирает элемент `<select>` из массива пунктов (вложенные массивы отрисовываются как `<optgroup>`) и возвращает его как `Html` - удобно для поля месяца в нашей дате. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)` порождает список элементов `<input>`, обёрнутых в `<label>` (радиокнопки или флажки), и возвращает его строкой. + +Первое поле нашей даты создаётся, стало быть, так: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... select для месяца и input для года ... */); +} +``` + +Метку отрисовывает `getLabel()`, и её реализация по умолчанию обычно подходит. Только осторожно: у составного элемента её атрибут `for` указывает на `getHtmlId()`, поэтому этот id дайте первому инпуту - ровно как в примере. + +Чтобы составной элемент можно было отрисовывать в шаблоне по частям (например, `{input birthdate:day}`), переопределите методы `getControlPart($key)` и `getLabelPart($key)`, возвращающие элемент `Html` для данной части - точно так же, как это делают `CheckboxList` и `RadioList`. + +.[note] +Если вы переопределяете `getControl()`, помните, что `BaseControl::getControl()` ещё и помечает элемент как отрисованный через `setOption('rendered', true)`. Вызовите его тоже (или вызовите `parent::getControl()`), когда в одной форме сочетаете ручную и автоматическую отрисовку, чтобы элемент не отрисовался дважды. (Пример `DateInput` выше опускает это для краткости.) + + +Полный пример: DateInput +======================== + +Все описанные части вместе, дополненные выпадающим списком для выбора месяца, вы найдёте в готовом элементе `DateInput` среди [примеров прямо в репозитории |https://github.com/nette/forms/blob/master/examples/custom-control.php]. + +Обратите внимание, что в конструкторе элемент добавляет себе правило проверки, которое следит за осмысленностью даты. Бессмысленный ввод, например 31 февраля, тем самым проявляется как обычная ошибка проверки формы: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'Дата некорректна.'); +} +``` + +А использование? Точно как у встроенных элементов: + +```php +$form['birthdate'] = (new DateInput('Дата рождения:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('Когда вы родились?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +В шаблоне Latte вы отрисуете его привычным тегом `{input birthdate}` или `{label birthdate /}`, как и любой другой элемент. + + +Проверка +======== + +Встроенные правила проверки работают с собственным элементом сразу - они действуют со значением из `getValue()`. Наш `DateInput` может, например, использовать `Form::Min` для самой ранней допустимой даты. О том, как написать собственные правила вместе с их JavaScript-двойником, рассказано в главе [Собственные правила и условия |validation#Собственные правила и условия]. + + +Собственный метод добавления +============================ + +Встроенные элементы мы добавляем удобными методами `$form->addText()` и им подобными. У собственного элемента такого метода нет, поэтому вы добавляете его обычным присваиванием - это одинаково работает в форме и в контейнере, и это понимают редакторы и статический анализ: + +```php +$form['birthdate'] = new DateInput('Дата рождения:'); +``` + +Если вы хотите сократить добавление, сохранив автодополнение, пригодится статический фабричный метод прямо на элементе. Он работает и во вложенных контейнерах, чего метод на потомке класса `Form` не смог бы: вложенные контейнеры о нём не знают: + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// работает в форме и в любом контейнере: +DateInput::addTo($form, 'birthdate', 'Дата рождения:'); +``` + +Тот же подход работает и как именованное сокращение для повторяющейся настройки встроенного элемента: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'Почтовый индекс должен состоять ровно из 5 цифр', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'Почтовый индекс:'); +``` diff --git a/forms/ru/in-presenter.texy b/forms/ru/in-presenter.texy index 8aec5e9975..fb63ac431d 100644 --- a/forms/ru/in-presenter.texy +++ b/forms/ru/in-presenter.texy @@ -2,15 +2,15 @@ ******************* .[perex] -Nette Forms значительно упрощают создание и обработку веб-форм. В этой главе вы узнаете, как использовать формы внутри презентеров. +Nette Forms существенно упрощают создание и обработку веб-форм. В этой главе вы узнаете, как использовать формы внутри презентеров. -Если вас интересует, как использовать их полностью самостоятельно, без остальной части фреймворка, для вас предназначено руководство по [самостоятельному использованию|standalone]. +Если вас интересует их совершенно самостоятельное использование без остального фреймворка, есть руководство по [самостоятельному использованию|standalone]. Первая форма ============ -Попробуем написать простую регистрационную форму. Ее код будет следующим: +Попробуем написать простую регистрационную форму. Её код будет таким: ```php use Nette\Application\UI\Form; @@ -19,16 +19,16 @@ $form = new Form; $form->addText('name', 'Имя:'); $form->addPassword('password', 'Пароль:'); $form->addSubmit('send', 'Зарегистрироваться'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` а в браузере она отобразится так: -[* form-cs.webp *] +[* form-en.webp *] -Форма в презентере — это объект класса `Nette\Application\UI\Form`, ее предшественник `Nette\Forms\Form` предназначен для самостоятельного использования. Мы добавили в нее так называемые элементы: имя, пароль и кнопку отправки. И, наконец, строка с `$form->onSuccess` говорит, что после отправки и успешной валидации должен быть вызван метод `$this->formSucceeded()`. +Форма в презентере - это объект класса `Nette\Application\UI\Form`; его предок `Nette\Forms\Form` предназначен для самостоятельного использования. Мы добавили элементы с именами name, password и кнопку отправки. Наконец, строка `$form->onSuccess` говорит, что после отправки и успешной проверки должен быть вызван метод `$this->formSucceeded()`. -С точки зрения презентера форма является обычным компонентом. Поэтому с ней обращаются как с компонентом и включают ее в презентер с помощью [фабричных методов |application:components#Фабричные методы]. Это будет выглядеть так: +С точки зрения презентера форма - обычный компонент. Поэтому с ней и обращаются как с компонентом и встраивают в презентер через [фабричный метод |application:components#Фабричные методы]. Выглядеть это будет так: ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -42,22 +42,22 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Имя:'); $form->addPassword('password', 'Пароль:'); $form->addSubmit('send', 'Зарегистрироваться'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { - // здесь мы обрабатываем данные, отправленные формой + // здесь мы обработаем данные, отправленные формой // $data->name содержит имя // $data->password содержит пароль - $this->flashMessage('Вы были успешно зарегистрированы.'); + $this->flashMessage('Вы успешно зарегистрировались.'); $this->redirect('Home:'); } } ``` -А в шаблоне форму отрисовываем тегом `{control}`: +А в шаблоне форма отрисовывается тегом `{control}`: ```latte .{file:app/Presentation/Home/default.latte} <h1>Регистрация</h1> @@ -65,23 +65,25 @@ class HomePresenter extends Nette\Application\UI\Presenter {control registrationForm} ``` -И это, собственно, все :-) У нас есть рабочая и идеально [защищенная |#Защита от уязвимостей] форма. +И это, по сути, всё :-) У нас есть работающая и превосходно [защищённая |#Защита от уязвимостей] форма. -А теперь вы, вероятно, думаете, что это было слишком быстро, и размышляете, как возможно, что вызывается метод `formSucceeded()` и какие параметры он получает. Конечно, вы правы, это заслуживает объяснения. +Сейчас вы, наверное, думаете, что это было слишком быстро, и гадаете, как так вышло, что метод `formSucceeded()` вызывается и какие параметры получает. Да, вы правы, это заслуживает объяснения. -Nette предлагает свежий механизм, который мы называем [Hollywood style |application:components#Стиль Голливуда]. Вместо того чтобы вам, как разработчику, постоянно спрашивать, произошло ли что-то («была ли форма отправлена?», «была ли она отправлена валидно?» и «не была ли она подделана?»), вы говорите фреймворку: «когда форма будет валидно заполнена, вызови этот метод» и оставляете дальнейшую работу ему. Если вы программируете на JavaScript, этот стиль программирования вам хорошо знаком. Вы пишете функции, которые вызываются, когда наступает определенное [событие |nette:glossary#События Events]. И язык передает им соответствующие аргументы. +Nette привносит освежающий механизм под названием [голливудский стиль |application:components#Голливудский стиль]. Вместо того чтобы вам как разработчику постоянно спрашивать, не случилось ли чего ("была ли форма отправлена?", "была ли она отправлена корректно?" и "не была ли она подделана?"), вы говорите фреймворку: "когда форма будет корректно заполнена, вызови этот метод", а дальнейшую работу оставляете ему. Если вы программируете на JavaScript, этот стиль программирования вам близко знаком. Вы пишете функции, которые вызываются, когда происходит определённое [событие |nette:glossary#События]. И язык передаёт им подходящие аргументы. -Именно так построен и вышеприведенный код презентера. Массив `$form->onSuccess` представляет собой список PHP callback-ов, которые Nette вызовет в момент, когда форма отправлена и правильно заполнена (т. е. валидна). В рамках [жизненного цикла презентера |application:presenters#Жизненный цикл презентера] это так называемый сигнал, то есть они вызываются после метода `action*` и перед методом `render*`. И каждому callback-у он передает в качестве первого параметра саму форму, а в качестве второго — отправленные данные в виде объекта [ArrayHash |utils:arrays#ArrayHash]. Первый параметр можно опустить, если объект формы вам не нужен. А второй параметр может быть хитрее, но об этом [позже |#Маппинг на классы]. +Именно так построен код презентера выше. Массив `$form->onSuccess` представляет собой список PHP-callback'ов, которые Nette вызывает в момент, когда форма отправлена и правильно заполнена (то есть корректна). В рамках [жизненного цикла презентера |application:presenters#Жизненный цикл презентера] это так называемый сигнал, поэтому вызываются они после метода `action*` и перед методом `render*`. И каждому callback'у она передаёт первым параметром саму форму, а вторым - отправленные данные в виде объекта [ArrayHash |utils:arrays#ArrayHash] (либо stdClass, либо собственного класса). Первый параметр можно опустить, если объект формы вам не нужен. Второй параметр может быть умнее, но об этом [позже |#Отображение в классы]. -Объект `$data` содержит ключи `name` и `password` с данными, которые заполнил пользователь. Обычно данные сразу отправляются на дальнейшую обработку, что может быть, например, вставка в базу данных. Однако во время обработки может возникнуть ошибка, например, имя пользователя уже занято. В таком случае мы передаем ошибку обратно в форму с помощью `addError()` и позволяем ей отрисоваться снова, уже с сообщением об ошибке. +Объект `$data` содержит свойства `name` и `password` с данными, которые ввёл пользователь. Обычно мы отправляем данные прямо на дальнейшую обработку, которой может быть, например, вставка в базу данных. Однако при обработке может возникнуть ошибка, например имя пользователя уже занято. В таком случае мы передаём ошибку обратно в форму методом `addError()` и даём отрисовать её снова вместе с сообщением об ошибке. ```php -$form->addError('Извините, это имя пользователя уже используется.'); +$form->addError('Извините, это имя пользователя уже занято.'); ``` -Кроме `onSuccess` существует еще `onSubmit`: callback-и вызываются всегда после отправки формы, даже если она заполнена неправильно. А также `onError`: callback-и вызываются только если отправка невалидна. Они вызываются даже тогда, когда в `onSuccess` или `onSubmit` мы делаем форму невалидной с помощью `addError()`. +Кроме `onSuccess` есть ещё `onSubmit`: callback'и вызываются всегда, когда форма отправлена, даже если она заполнена неправильно. А ещё `onError`: callback'и вызываются, только если отправка некорректна. Они вызываются даже тогда, когда мы объявляем форму некорректной в `onSuccess` методом `addError()`. -После обработки формы мы перенаправляем на следующую страницу. Это предотвратит нежелательную повторную отправку формы кнопкой *обновить*, *назад* или перемещением в истории браузера. +После обработки формы мы перенаправляем на другую страницу. Это предотвращает нежелательную повторную отправку формы кнопками *обновить*, *назад* или переходом по истории браузера. + +Если форма отправляется по AJAX, вы обычно вместо перенаправления перерисовываете [сниппет |application:ajax] с заново отрисованной формой. Попробуйте добавить и другие [элементы формы|controls]. @@ -89,50 +91,50 @@ $form->addError('Извините, это имя пользователя уже Доступ к элементам ================== -Форма является компонентом презентера, в нашем случае названным `registrationForm` (по имени фабричного метода `createComponentRegistrationForm`), поэтому где угодно в презентере к форме можно получить доступ с помощью: +Форма - компонент презентера, в нашем случае с именем `registrationForm` (по имени фабричного метода `createComponentRegistrationForm`), так что где угодно в презентере вы можете получить форму так: ```php $form = $this->getComponent('registrationForm'); -// альтернативный синтаксис: $form = $this['registrationForm']; +// альтернативная запись: $form = $this['registrationForm']; ``` -Отдельные элементы формы также являются компонентами, поэтому к ним можно получить доступ таким же образом: +Отдельные элементы формы - тоже компоненты, поэтому обращаться к ним можно так же: ```php -$input = $form->getComponent('name'); // или $input = $form['name']; -$button = $form->getComponent('send'); // или $button = $form['send']; +$input = $form->getComponent('name'); // либо $input = $form['name']; +$button = $form->getComponent('send'); // либо $button = $form['send']; ``` -Элементы удаляются с помощью unset: +Элементы удаляются через `unset`: ```php unset($form['name']); ``` -Правила валидации -================= +Правила проверки +================ -Здесь прозвучало слово *валидная,* но у формы пока нет никаких правил валидации. Давайте это исправим. +Прозвучало слово *корректна*, но у формы пока нет никаких правил проверки. Исправим это. -Имя будет обязательным, поэтому мы отметим его методом `setRequired()`, аргументом которого является текст сообщения об ошибке, которое отобразится, если пользователь не заполнит имя. Если аргумент не указан, используется сообщение об ошибке по умолчанию. +Имя будет обязательным, поэтому пометим его методом `setRequired()`. Его аргумент - текст сообщения об ошибке, которое отобразится, если пользователь имя не заполнит. Если аргумент опустить, будет использовано стандартное сообщение об ошибке. ```php $form->addText('name', 'Имя:') - ->setRequired('Пожалуйста, введите имя'); + ->setRequired('Введите, пожалуйста, имя.'); ``` -Попробуйте отправить форму без заполненного имени, и вы увидите, что отобразится сообщение об ошибке, и браузер или сервер будет отклонять ее до тех пор, пока вы не заполните поле. +Попробуйте отправить форму, не заполнив имя, и вы увидите, что отобразится сообщение об ошибке, а браузер или сервер будут её отклонять, пока вы поле не заполните. -В то же время систему не обмануть, введя в поле, например, только пробелы. Ни в коем случае. Nette автоматически удаляет пробелы слева и справа. Попробуйте сами. Это то, что вы всегда должны делать с каждым однострочным инпутом, но об этом часто забывают. Nette делает это автоматически. (Можете попробовать обмануть форму и отправить в качестве имени многострочную строку. И здесь Nette не даст себя обмануть и заменит переносы строк на пробелы.) +При этом систему не обмануть, введя в поле, например, только пробелы. Никак. Nette автоматически обрезает пробелы слева и справа. Попробуйте. Это то, что нужно всегда делать с каждым однострочным полем, но об этом часто забывают. Nette делает это автоматически. (Можете попробовать одурачить форму и отправить в качестве имени многострочную строку. И тут Nette не проведёшь, переводы строк будут заменены пробелами.) -Форма всегда валидируется на стороне сервера, но также генерируется JavaScript-валидация, которая выполняется мгновенно, и пользователь узнает об ошибке сразу, без необходимости отправлять форму на сервер. За это отвечает скрипт `netteForms.js`. Вставьте его в шаблон макета: +Форма всегда проверяется на стороне сервера, но порождается и проверка на JavaScript, которая выполняется мгновенно, и пользователь узнаёт об ошибке сразу, без необходимости отправлять форму на сервер. За это отвечает скрипт `netteForms.js`. Подключите его в шаблон макета: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Если вы посмотрите исходный код страницы с формой, вы можете заметить, что Nette вставляет обязательные элементы в элементы с CSS-классом `required`. Попробуйте добавить в шаблон следующую таблицу стилей, и надпись «Имя» станет красной. Таким образом, мы элегантно выделяем обязательные элементы для пользователей: +Если вы посмотрите на исходный код страницы с формой, то заметите, что Nette оборачивает обязательные элементы в элементы с CSS-классом `required`. Попробуйте добавить в шаблон следующий стиль, и метка "Имя" станет красной. Так вы элегантно выделите для пользователей обязательные поля: ```latte <style> @@ -140,85 +142,87 @@ $form->addText('name', 'Имя:') </style> ``` -Другие правила валидации добавляются методом `addRule()`. Первый параметр — это правило, второй — снова текст сообщения об ошибке, и может еще следовать аргумент правила валидации. Что это значит? +Дальнейшие правила проверки мы добавляем методом `addRule()`. Первый параметр - правило, второй - снова текст сообщения об ошибке, а за ним может следовать аргумент правила проверки. Что это значит? -Расширим форму новым необязательным полем «возраст», которое должно быть целым числом (`addInteger()`) и, кроме того, находиться в допустимом диапазоне (`$form::Range`). И здесь мы как раз используем третий параметр метода `addRule()`, которым передадим валидатору требуемый диапазон в виде пары `[от, до]`: +Расширим форму новым необязательным полем "возраст", которое должно быть целым числом (`addInteger()`), да ещё и в допустимом диапазоне (`$form::Range`). Здесь мы используем третий параметр метода `addRule()`, чтобы передать валидатору нужный диапазон парой `[min, max]`: ```php $form->addInteger('age', 'Возраст:') - ->addRule($form::Range, 'Возраст должен быть от 18 до 120', [18, 120]); + ->addRule($form::Range, 'Возраст должен быть от 18 до 120 лет.', [18, 120]); ``` .[tip] -Если пользователь не заполнит поле, правила валидации проверяться не будут, так как элемент необязателен. +Если пользователь поле не заполнит, правила проверки проверяться не будут, потому что элемент необязателен. -Здесь возникает пространство для небольшого рефакторинга. В сообщении об ошибке и в третьем параметре числа указаны дублировано, что не идеально. Если бы мы создавали [многоязычные формы |rendering#Перевод] и сообщение, содержащее числа, было бы переведено на несколько языков, то возможное изменение значений усложнилось бы. По этой причине можно использовать плейсхолдеры `%d`, и Nette дополнит значения: +Здесь появляется место для небольшого рефакторинга. В сообщении об ошибке и в третьем параметре числа дублируются, а это неидеально. Если бы мы создавали [многоязычные формы |rendering#Перевод] и сообщение с числами переводилось бы на несколько языков, менять значения стало бы трудно. Поэтому можно использовать подстановки `%d`, и Nette значения подставит: ```php - ->addRule($form::Range, 'Возраст должен быть от %d до %d лет', [18, 120]); + ->addRule($form::Range, 'Возраст должен быть от %d до %d лет.', [18, 120]); ``` -Вернемся к элементу `password`, который также сделаем обязательным и еще проверим минимальную длину пароля (`$form::MinLength`), снова с использованием плейсхолдера: +Вернёмся к элементу `password`, сделаем его тоже обязательным и заодно проверим минимальную длину пароля (`$form::MinLength`), снова с подстановкой в сообщении: ```php $form->addPassword('password', 'Пароль:') ->setRequired('Выберите пароль') - ->addRule($form::MinLength, 'Пароль должен содержать не менее %d символов', 8); + ->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов.', 8); ``` -Добавим в форму еще поле `passwordVerify`, где пользователь введет пароль еще раз, для проверки. С помощью правил валидации проверим, совпадают ли оба пароля (`$form::Equal`). А в качестве параметра дадим ссылку на первый пароль с помощью [квадратных скобок |#Доступ к элементам]: +Добавим в форму ещё одно поле `passwordVerify`, где пользователь введёт пароль повторно для подтверждения. С помощью правил проверки убедимся, что оба пароля одинаковы (`$form::Equal`). В качестве аргумента передадим ссылку на первый пароль через [квадратные скобки |#Доступ к элементам]: ```php -$form->addPassword('passwordVerify', 'Пароль для проверки:') - ->setRequired('Пожалуйста, введите пароль еще раз для проверки') - ->addRule($form::Equal, 'Пароли не совпадают', $form['password']) +$form->addPassword('passwordVerify', 'Пароль ещё раз:') + ->setRequired('Введите пароль ещё раз для проверки опечатки') + ->addRule($form::Equal, 'Пароли не совпадают.', $form['password']) ->setOmitted(); ``` -С помощью `setOmitted()` мы отметили элемент, значение которого нам на самом деле не важно и который существует только для валидации. Значение не передается в `$data`. +С помощью `setOmitted()` мы пометили элемент, значение которого нас на самом деле не интересует и который существует только ради проверки. Его значение в `$data` не передаётся. -Таким образом, у нас есть полностью рабочая форма с валидацией на PHP и JavaScript. Возможности валидации Nette гораздо шире, можно создавать условия, позволять по ним отображать и скрывать части страницы и т. д. Все это вы узнаете в главе о [валидации форм|validation]. +Тем самым у нас есть полностью работающая форма с проверкой и в PHP, и в JavaScript. Возможности проверки в Nette намного шире: можно создавать условия, по ним показывать и скрывать части страницы и т. д. Обо всём вы узнаете в главе о [проверке форм|validation]. Значения по умолчанию ===================== -Элементам формы обычно устанавливают значения по умолчанию: +Значения по умолчанию для элементов формы мы задаём обычным образом: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -Часто бывает полезно установить значения по умолчанию для всех элементов одновременно. Например, когда форма служит для редактирования записей. Мы читаем запись из базы данных и устанавливаем значения по умолчанию: +Часто бывает полезно задать значения по умолчанию сразу всем элементам. Например, когда форма используется для редактирования записей. Мы считываем запись из базы данных и задаём значения по умолчанию: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Вызывайте `setDefaults()` после определения элементов. +У уже отправленной формы `setDefaults()` ничего не делает: он не перезапишет то, что заполнил пользователь, так что вызывать его в фабрике формы безусловно безопасно. Если вам нужно задать значения принудительно и после отправки, используйте вместо него `setValues()`. + Отрисовка формы =============== -По умолчанию форма отрисовывается в виде таблицы. Отдельные элементы соответствуют основному правилу доступности — все надписи записаны как `<label>` и связаны с соответствующим элементом формы. При клике на надпись курсор автоматически появляется в поле формы. +По умолчанию форма отрисовывается как таблица. Отдельные элементы соблюдают основные правила веб-доступности: все метки записаны как элементы `<label>` и связаны с соответствующими элементами формы. Щелчок по метке автоматически ставит курсор в поле формы. -Каждому элементу мы можем устанавливать любые HTML-атрибуты. Например, добавить placeholder: +Каждому элементу мы можем задать произвольные HTML-атрибуты. Например, добавить placeholder: ```php $form->addInteger('age', 'Возраст:') - ->setHtmlAttribute('placeholder', 'Пожалуйста, укажите возраст'); + ->setHtmlAttribute('placeholder', 'Заполните, пожалуйста, возраст'); ``` -Способов отрисовки формы действительно много, поэтому этому посвящена [отдельная глава об отрисовке|rendering]. +Способов отрисовать форму действительно очень много, поэтому этому посвящена [отдельная глава об отрисовке|rendering]. -Маппинг на классы -================= +Отображение в классы +==================== -Вернемся к методу `formSucceeded()`, который во втором параметре `$data` получает отправленные данные как объект `ArrayHash`. Поскольку это генерический класс, что-то вроде `stdClass`, при работе с ним нам будет не хватать определенного комфорта, такого как автодополнение свойств в редакторах или статический анализ кода. Это можно было бы решить, имея для каждой формы конкретный класс, свойства которого представляют отдельные элементы. Например: +Вернёмся к методу `formSucceeded()`, который получает отправленные данные вторым параметром `$data` как объект `ArrayHash` (либо `stdClass`). Поскольку это универсальный класс, похожий на `stdClass`, при работе с ним нам не хватает определённых удобств, например автодополнения свойств в редакторах или статического анализа кода. Это можно решить, заведя для каждой формы отдельный класс, свойства которого представляют отдельные элементы. Например: ```php class RegistrationFormData @@ -229,43 +233,45 @@ class RegistrationFormData } ``` -Альтернативно можно использовать конструктор: +Как вариант, можно использовать конструктор: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Свойства класса данных также могут быть перечислениями (enum), и они будут автоматически сопоставлены. .{data-version:3.2.4} +Свойства класса данных могут быть и перечислениями, и они будут отображены автоматически. .{data-version:3.2.4} -Как сказать Nette, чтобы он возвращал нам данные в виде объектов этого класса? Проще, чем вы думаете. Достаточно просто указать класс как тип параметра `$data` в методе-обработчике: +Как сказать Nette, чтобы она возвращала данные как объекты этого класса? Проще, чем вы думаете. Достаточно указать класс как тип параметра `$data` в методе-обработчике: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void { - // $data является экземпляром RegistrationFormData + // $data - экземпляр RegistrationFormData $name = $data->name; // ... } ``` -В качестве типа можно также указать `array`, и тогда данные будут переданы в виде массива. +В качестве типа можно указать и `array`, тогда данные будут переданы массивом. -Аналогичным образом можно использовать и функцию `getValues()`, которой мы передаем имя класса или объект для гидратации в качестве параметра: +Точно так же можно использовать метод `getValues()`, передав ему параметром имя класса или объект для наполнения: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Если формы образуют многоуровневую структуру, состоящую из контейнеров, создайте для каждого отдельный класс: +Если вам нужно прочитать значения до проверки формы, обычно внутри обработчика `onValidate`, используйте вместо него метод `getUntrustedValues()`. Он принимает те же параметры, что и `getValues()`, но возвращает отправленные значения без гарантии, что они прошли проверку. + +Если формы имеют многоуровневую структуру из контейнеров, создайте для каждого отдельный класс: ```php $form = new Form; @@ -287,55 +293,58 @@ class RegistrationFormData } ``` -Маппинг тогда по типу свойства `$person` поймет, что контейнер нужно сопоставить с классом `PersonFormData`. Если бы свойство содержало массив контейнеров, укажите тип `array` и передайте класс для маппинга непосредственно контейнеру: +Тогда отображение по типу свойства `$person` выведет, что контейнер нужно отобразить в класс `PersonFormData`. Если бы свойство содержало массив контейнеров, укажите тип `array`, а класс для отображения передайте прямо контейнеру: ```php $person->setMappedType(PersonFormData::class); ``` -Вы можете сгенерировать проект класса данных формы с помощью метода `Nette\Forms\Blueprint::dataClass($form)`, который выведет его на страницу браузера. Затем достаточно кликнуть, чтобы выделить код, и скопировать его в проект. .{data-version:3.1.15} +Заготовку класса данных формы можно породить методом `Nette\Forms\Blueprint::dataClass($form)`, который выведет её на страницу в браузере. Дальше достаточно щелчком выделить код и скопировать его в проект. .{data-version:3.1.15} -Несколько кнопок -================ +Несколько кнопок отправки +========================= -Если у формы больше одной кнопки, нам обычно нужно различать, какая из них была нажата. Мы можем создать для каждой кнопки свою функцию-обработчик. Установим ее как обработчик для [события |nette:glossary#События Events] `onClick`: +Если у формы больше одной кнопки, нам обычно нужно различить, какая была нажата. Для каждой кнопки можно создать отдельную функцию-обработчик. Задайте её как обработчик [события |nette:glossary#События] `onClick`: ```php $form->addSubmit('save', 'Сохранить') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Удалить') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -Эти обработчики вызываются только в случае валидно заполненной формы, так же как и в случае события `onSuccess`. Разница в том, что в качестве первого параметра вместо формы может передаваться кнопка отправки, это зависит от типа, который вы укажете: +.{data-version:3.3.0} +Обработчик можно передать кнопке и напрямую третьим аргументом метода `addSubmit()`. + +Эти обработчики вызываются, только если форма корректно заполнена (если для кнопки не отключена проверка), как и событие `onSuccess`. Разница в том, что первым параметром вместо формы может быть передан объект кнопки отправки, в зависимости от того, какой тип вы укажете: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Когда форма отправляется нажатием клавиши <kbd>Enter</kbd>, это считается так, как если бы она была отправлена первой кнопкой. +Когда форма отправляется нажатием клавиши <kbd>Enter</kbd>, это считается отправкой первой кнопкой отправки. Событие onAnchor ================ -Когда в фабричном методе (например, `createComponentRegistrationForm`) мы собираем форму, она еще не знает, была ли она отправлена и с какими данными. Но есть случаи, когда нам нужно знать отправленные значения, например, от них зависит дальнейший вид формы, или они нужны для зависимых селектбоксов и т. д. +Когда вы строите форму в фабричном методе (вроде `createComponentRegistrationForm`), она ещё не знает, была ли отправлена и с какими данными. Однако бывают случаи, когда нам нужно знать отправленные значения: возможно, от них зависит внешний вид формы или они нужны для зависимых выпадающих списков и т. п. -Поэтому часть кода, собирающего форму, можно вызвать только в момент, когда она так называемо «заякорена», то есть уже связана с презентером и знает свои отправленные данные. Такой код мы передадим в массив `$onAnchor`: +Поэтому вы можете сделать так, чтобы код, строящий форму, вызывался только тогда, когда она "заякорена", то есть уже связана с презентером и знает свои отправленные данные. Поместите такой код в массив `$onAnchor`: ```php $country = $form->addSelect('country', 'Страна:', $this->model->getCountries()); $city = $form->addSelect('city', 'Город:'); $form->onAnchor[] = function () use ($country, $city) { - // эта функция будет вызвана, когда форма будет знать, была ли она отправлена и с какими данными - // поэтому можно использовать метод getValue() + // эта функция будет вызвана, когда форма узнает, с какими данными её отправили, + // так что можно использовать метод getValue() $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; @@ -345,35 +354,34 @@ $form->onAnchor[] = function () use ($country, $city) { Защита от уязвимостей ===================== -Nette Framework уделяет большое внимание безопасности и поэтому тщательно следит за хорошей защитой форм. Он делает это совершенно прозрачно и не требует ручной настройки. +Nette Framework уделяет безопасности огромное внимание и поэтому тщательно следит за безопасностью форм. Делает она это совершенно прозрачно и не требует никакой ручной настройки. -Кроме того, что формы защищены от атаки [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], он выполняет множество мелких мер безопасности, о которых вам уже не нужно думать. +Кроме защиты форм от таких атак, как [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], она выполняет множество мелких мер безопасности, о которых вам больше не нужно думать. -Например, он отфильтровывает из входных данных все управляющие символы и проверяет валидность кодировки UTF-8, так что данные из формы всегда будут чистыми. У селектбоксов и списков радиокнопок он проверяет, что выбранные элементы действительно были из предложенных и не было подделки. Мы уже упоминали, что у однострочных текстовых вводов он удаляет символы конца строки, которые мог отправить злоумышленник. У многострочных вводов он нормализует символы конца строки. И так далее. +Например, она отфильтровывает из ввода все управляющие символы и проверяет корректность кодировки UTF-8, благодаря чему данные из формы всегда чисты. У выпадающих списков и радиосписков она проверяет, что выбранные пункты действительно были среди предложенных и что подделки не произошло. Мы уже упоминали, что у однострочных текстовых полей она заменяет пробелами символы конца строки, которые мог отправить злоумышленник. У многострочных полей она приводит символы конца строки к единому виду. И так далее. Nette решает за вас риски безопасности, о существовании которых многие программисты даже не подозревают. -Упомянутая CSRF-атака заключается в том, что злоумышленник заманивает жертву на страницу, которая незаметно в браузере жертвы выполняет запрос на сервер, на котором жертва авторизована, и сервер полагает, что запрос был выполнен жертвой по собственной воле. Поэтому Nette предотвращает отправку POST-формы с другого домена. Если по какой-то причине вы хотите отключить защиту и разрешить отправку формы с другого домена, используйте: +Упомянутая атака CSRF состоит в том, что злоумышленник заманивает жертву на страницу, которая незаметно выполняет в браузере жертвы запрос к серверу, где жертва авторизована. Сервер тогда считает, что запрос сделала жертва по своей воле. Поэтому Nette отклоняет POST-формы, отправленные с чужого источника; чужим считается даже другой поддомен того же сайта. Если вам нужно разрешить отправку с другого источника, отключите защиту так: ```php -$form->allowCrossOrigin(); // ВНИМАНИЕ! Отключает защиту! +$form->allowCrossOrigin(); // ВНИМАНИЕ! Полностью отключает защиту! ``` -Эта защита использует SameSite cookie с именем `_nss`. Защита с помощью SameSite cookie может быть не 100% надежной, поэтому рекомендуется включить еще и защиту с помощью токена: +Правда, это отключает защиту для любого источника. Чтобы разрешить только определённые источники, отключите защиту и сами сверяйте заголовок `Origin` со своим списком разрешённых. -```php -$form->addProtection(); -``` +Защита опирается на браузерный заголовок `Sec-Fetch-Site` (Fetch Metadata), который браузер отправляет автоматически и который нельзя подделать даже при наличии XSS-уязвимости. Для старых браузеров без его поддержки действует запасная cookie SameSite, которую приложение на Nette устанавливает автоматически. Подробно это описано в статье [Браузер наконец решает CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. -Рекомендуем таким образом защищать формы в административной части сайта, которые изменяют чувствительные данные в приложении. Фреймворк защищается от CSRF-атаки путем генерации и проверки авторизационного токена, который сохраняется в сессии. Поэтому перед отображением формы необходимо иметь открытую сессию. В административной части сайта сессия обычно уже запущена из-за входа пользователя. В противном случае запустите сессию методом `Nette\Http\Session::start()`. +.[note] +Прежняя защита с помощью авторизационного токена в сессии, включаемая через `$form->addProtection()`, больше не нужна и объявлена устаревшей начиная с версии 3.3. -Одна и та же форма в нескольких презентерах -=========================================== +Использование одной формы в нескольких презентерах +================================================== -Если вам нужно использовать одну и ту же форму в нескольких презентерах, рекомендуем создать для нее фабрику, которую затем передать в презентер. Подходящее место для такого класса — например, каталог `app/Forms`. +Если вам нужно использовать одну и ту же форму в нескольких презентерах, мы рекомендуем создать для неё фабрику, которую вы затем внедрите в презентеры. Подходящее место для такого класса - например, каталог `app/Forms`. -Фабричный класс может выглядеть, например, так: +Класс фабрики может выглядеть так: ```php use Nette\Application\UI\Form; @@ -390,7 +398,7 @@ class SignInFormFactory } ``` -Мы попросим класс создать форму в фабричном методе для компонентов в презентере: +Класс, порождающий форму, мы запрашиваем в фабричном методе компонента внутри презентера: ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // мы можем изменить форму, здесь, например, мы меняем надпись на кнопке + // форму можно изменить, здесь мы, например, меняем подпись на кнопке $form['send']->setCaption('Продолжить'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // и добавляем обработчик + $form->onSuccess[] = $this->signInFormSuceeded(...); // и добавляем обработчик return $form; } ``` -Обработчик для обработки формы также может быть предоставлен уже из фабрики: +Обработчик формы может предоставить и сама фабрика: ```php use Nette\Application\UI\Form; @@ -421,11 +429,11 @@ class SignInFormFactory $form->addText('name', 'Имя:'); $form->addSubmit('send', 'Войти'); $form->onSuccess[] = function (Form $form, $data): void { - // здесь мы обрабатываем форму + // здесь мы обрабатываем отправленную форму }; return $form; } } ``` -Итак, мы прошли быстрое введение в формы в Nette. Попробуйте еще заглянуть в каталог [examples|https://github.com/nette/forms/tree/master/examples] в дистрибутиве, где вы найдете дополнительное вдохновение. +Итак, мы прошли беглое знакомство с формами в Nette. За дополнительным вдохновением загляните в каталог [examples |https://github.com/nette/forms/tree/master/examples] в дистрибутиве. diff --git a/forms/ru/rendering.texy b/forms/ru/rendering.texy index 0683fcfc7c..9ff8c99b1d 100644 --- a/forms/ru/rendering.texy +++ b/forms/ru/rendering.texy @@ -1,35 +1,35 @@ Отрисовка форм ************** -Внешний вид форм может быть очень разным. На практике мы можем столкнуться с двумя крайностями. С одной стороны, существует необходимость отрисовывать в приложении множество форм, визуально похожих друг на друга как две капли воды, и мы оценим простую отрисовку без шаблона с помощью `$form->render()`. Обычно это относится к административным интерфейсам. +Внешний вид форм бывает очень разным. На практике мы можем столкнуться с двумя крайностями. С одной стороны, есть потребность отрисовать в приложении множество форм, которые визуально одинаковы, и мы ценим лёгкую отрисовку без шаблона через `$form->render()`. Обычно так бывает у административных интерфейсов. -С другой стороны, существуют разнообразные формы, где действует правило: каждая — уникальна. Их вид лучше всего описать языком HTML в шаблоне формы. И, конечно, помимо этих двух крайностей, мы столкнемся с множеством форм, находящихся где-то посередине. +С другой стороны, есть разнообразные формы, каждая из которых уникальна. Их внешний вид лучше всего описать с помощью HTML в шаблоне формы. И, разумеется, кроме этих двух крайностей мы встречаем множество форм, которые находятся где-то посередине. Отрисовка с помощью Latte ========================= -[Система шаблонов Latte |latte:] существенно упрощает отрисовку форм и их элементов. Сначала мы покажем, как отрисовывать формы вручную по отдельным элементам и тем самым получить полный контроль над кодом. Позже мы покажем, как можно такую отрисовку [автоматизировать |#Автоматическая отрисовка]. +Шаблонизатор [Latte |latte:] существенно упрощает отрисовку форм и их элементов. Сначала мы покажем, как отрисовывать формы вручную, элемент за элементом, чтобы получить полный контроль над кодом. Позже покажем, как такую отрисовку [автоматизировать |#Автоматическая отрисовка]. -Проект Latte-шаблона для формы можно сгенерировать с помощью метода `Nette\Forms\Blueprint::latte($form)`, который выведет его на страницу браузера. Затем достаточно кликнуть, чтобы выделить код, и скопировать его в проект. .{data-version:3.1.15} +Шаблон Latte для формы можно породить методом `Nette\Forms\Blueprint::latte($form)`, который выведет его на страницу в браузере. Дальше достаточно щелчком выделить код и скопировать его в проект. .{data-version:3.1.15} `{control}` ----------- -Самый простой способ отрисовать форму — написать в шаблоне: +Проще всего отрисовать форму, написав в шаблоне: ```latte {control signInForm} ``` -Повлиять на вид отрисованной таким образом формы можно с помощью конфигурации [#Renderer] и [отдельных элементов |#HTML-атрибуты]. +На внешний вид отрисованной формы можно повлиять настройкой [#Renderer] и [отдельных элементов |#HTML-атрибуты]. `n:name` -------- -Определение формы в PHP-коде можно очень легко связать с HTML-кодом. Достаточно лишь добавить атрибуты `n:name`. Это так просто! +Связать определение формы в PHP-коде с HTML-кодом чрезвычайно легко. Достаточно дописать атрибуты `n:name`. Вот так просто! ```php protected function createComponentSignInForm(): Form @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Вид результирующего HTML-кода полностью в ваших руках. Если атрибут `n:name` использовать у элементов `<select>`, `<button>` или `<textarea>`, их внутреннее содержимое автоматически дополнится. Тег `<form n:name>` к тому же создает локальную переменную `$form` с объектом отрисовываемой формы, а закрывающий `</form>` отрисовывает все неотрисованные скрытые элементы (то же самое относится и к `{form} ... {/form}`). +У вас есть полный контроль над видом итогового HTML-кода. Если вы используете атрибут `n:name` у элементов `<select>`, `<button>` или `<textarea>`, их внутреннее содержимое заполняется автоматически. Кроме того, тег `<form n:name>` создаёт локальную переменную `$form` с объектом отрисовываемой формы, а закрывающий тег `</form>` отрисовывает все неотрисованные скрытые элементы (то же относится к `{form} ... {/form}`). -Однако нельзя забывать об отрисовке возможных сообщений об ошибках. Как тех, которые были добавлены методом `addError()` к отдельным элементам (с помощью `{inputError}`), так и тех, что добавлены непосредственно к форме (их возвращает `$form->getOwnErrors()`): +Однако нам нельзя забыть об отрисовке возможных сообщений об ошибках. Речь и об ошибках, добавленных отдельным элементам методом `addError()` (отрисовываются через `{inputError}`), и об ошибках, добавленных прямо форме (их возвращает `$form->getOwnErrors()`): ```latte <form n:name=signInForm class=form> @@ -80,7 +80,7 @@ protected function createComponentSignInForm(): Form </form> ``` -Более сложные элементы формы, такие как RadioList или CheckboxList, можно таким образом отрисовывать по отдельным элементам: +Более сложные элементы формы, такие как RadioList или CheckboxList, можно отрисовать по пунктам вот так: ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ protected function createComponentSignInForm(): Form `{label}` `{input}` ------------------- -Не хотите для каждого элемента думать, какой HTML-элемент использовать для него в шаблоне, будь то `<input>`, `<textarea>` и т. д.? Решением является универсальный тег `{input}`: +Не хотите думать в шаблоне, какой HTML-элемент использовать для каждого элемента формы, `<input>`, `<textarea>` или ещё что-то? Решение - универсальный тег `{input}`: ```latte <form n:name=signInForm class=form> @@ -114,9 +114,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Если форма использует переводчик (translator), текст внутри тегов `{label}` будет переведен. +Если форма использует переводчик, метки, отрисованные из определения формы (например, `{label username /}`), переводятся. Текст, написанный прямо между тегами `{label}` и `{/label}`, - нет. -И в этом случае более сложные элементы формы, такие как RadioList или CheckboxList, можно отрисовывать по отдельным элементам: +Более сложные элементы формы, такие как RadioList или CheckboxList, снова можно отрисовать по пунктам: ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ protected function createComponentSignInForm(): Form {/foreach} ``` -Для отрисовки самого `<input>` в элементе Checkbox используйте `{input myCheckbox:}`. HTML-атрибуты в этом случае всегда отделяйте запятой `{input myCheckbox:, class: required}`. +Чтобы отрисовать только `<input>` элемента Checkbox, используйте `{input myCheckbox:}`. В этом случае всегда отделяйте HTML-атрибуты запятой: `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Выводит сообщение об ошибке для элемента формы, если оно есть. Сообщение обычно оборачивают в HTML-элемент для стилизации. Предотвратить отрисовку пустого элемента, если сообщения нет, можно элегантно с помощью `n:ifcontent`: +Выводит сообщение об ошибке элемента формы, если оно есть. Сообщение обычно оборачивают в HTML-элемент ради оформления. Не отрисовать пустой элемент, когда сообщения нет, можно элегантно с помощью `n:ifcontent`: ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -Наличие ошибки можно проверить методом `hasErrors()` и в зависимости от этого установить класс родительскому элементу: +Наличие ошибки можно выяснить методом `hasErrors()` и по нему задать класс родительскому элементу: ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ protected function createComponentSignInForm(): Form `{form}` -------- -Теги `{form signInForm}...{/form}` являются альтернативой `<form n:name="signInForm">...</form>`. +Теги `{form signInForm}...{/form}` - альтернатива записи `<form n:name="signInForm">...</form>`. Любые аргументы отделяйте от имени запятой: `{form signInForm, class: foo}`. + +.{data-version:3.3.0} +Ключевое слово `scope`, поставленное перед именем, только помещает форму в стек (чтобы `{input}`, `{label}` и прочие к ней привязывались), но тег `<form>` не отрисовывает. Это удобно для отрисовки части формы, например в сниппете. Если форма уже активна, имя разрешается относительно неё, так что `{form scope}` заменяет и `{formContainer}`: + +```latte +{form scope signInForm} + {input username} +{/form} +``` + +.{data-version:3.3.0} +Ключевое слово `detached` отрисовывает пустой `<form></form>` и привязывает к нему каждый элемент через HTML-атрибут `form`. Это позволяет поместить форму внутрь другой формы, чего HTML иначе не допускает. У отделённой формы должен быть HTML-атрибут `id`, который порождается автоматически, когда вы даёте ей имя (как `outerForm` ниже): + +```latte +{form detached outerForm} + ... +{/form} +``` Автоматическая отрисовка ------------------------ -Благодаря тегам `{input}` и `{label}` мы можем легко создать общий шаблон для любой формы. Он будет последовательно итерировать и отрисовывать все ее элементы, кроме скрытых элементов, которые отрисовываются автоматически при завершении формы тегом `</form>`. Имя отрисовываемой формы он будет ожидать в переменной `$form`. +Благодаря тегам `{input}` и `{label}` мы легко создадим универсальный шаблон для любой формы. Он обойдёт и отрисует все её элементы, кроме скрытых, которые отрисовываются автоматически при закрытии формы тегом `</form>`. Он ожидает имя отрисовываемой формы в переменной `$form`. ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ protected function createComponentSignInForm(): Form </form> ``` -Использованные самозакрывающиеся парные теги `{label .../}` отображают метки (labels), взятые из определения формы в PHP-коде. +Использованные здесь самозакрывающиеся парные теги `{label .../}` выводят метки, происходящие из определения формы в PHP-коде. -Сохраните этот общий шаблон, например, в файле `basic-form.latte`, и для отрисовки формы достаточно его включить и передать имя (или экземпляр) формы в параметр `$form`: +Сохраните этот универсальный шаблон, например, в файл `basic-form.latte`. Чтобы отрисовать форму, достаточно его подключить и передать имя формы (или её экземпляр) в параметр `$form`: ```latte {include basic-form.latte, form: signInForm} ``` -Если при отрисовке определенной формы вы захотите изменить ее вид и, например, один элемент отрисовать иначе, то самый простой путь — подготовить в шаблоне блоки, которые можно будет впоследствии переопределить. Блоки могут также иметь [динамические имена |latte:template-inheritance#Динамические имена блоков], в них можно так вставить и имя отрисовываемого элемента. Например: +Если при отрисовке конкретной формы вы захотите изменить её внешний вид, например отрисовать один элемент иначе, проще всего подготовить в шаблоне блоки, которые затем можно переопределить. У блоков могут быть и [динамические имена |latte:template-inheritance#Динамические имена блоков], так что в них можно вставить имя отрисовываемого элемента. Например: ```latte ... @@ -189,7 +207,7 @@ protected function createComponentSignInForm(): Form ... ``` -Для элемента, например `username`, будет создан блок `input-username`, который можно легко переопределить с помощью тега [{embed} |latte:template-inheritance#Модульное наследование]: +Для элемента с именем, например, `username` так возникнет блок `input-username`, который легко переопределить тегом [{embed} |latte:template-inheritance#Блочное наследование]: ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ protected function createComponentSignInForm(): Form {/embed} ``` -Альтернативно, все содержимое шаблона `basic-form.latte` можно [определить |latte:template-inheritance#Определения] как блок, включая параметр `$form`: +Как вариант, всё содержимое шаблона `basic-form.latte` можно [определить |latte:template-inheritance#Определения] как блок, включая параметр `$form`: ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ protected function createComponentSignInForm(): Form {/define} ``` -Благодаря этому его вызов станет немного проще: +Благодаря этому его вызов немного упростится: ```latte {embed basic-form, signInForm} @@ -219,7 +237,7 @@ protected function createComponentSignInForm(): Form {/embed} ``` -При этом блок достаточно импортировать только в одном месте — в начале шаблона макета (layout): +Блок достаточно импортировать в одном месте, в начале шаблона макета: ```latte {import basic-form.latte} @@ -229,7 +247,7 @@ protected function createComponentSignInForm(): Form Особые случаи ------------- -Если вам нужно отрисовать только внутреннюю часть формы без HTML-тегов `<form>`, например, при отправке сниппетов, скройте их с помощью атрибута `n:tag-if`: +Если вам нужно отрисовать только внутреннюю часть формы без HTML-тегов `<form>`, например при отправке сниппетов, скройте их атрибутом `n:tag-if`: ```latte <form n:name=signInForm n:tag-if=false> @@ -240,7 +258,7 @@ protected function createComponentSignInForm(): Form </form> ``` -С отрисовкой элементов внутри контейнера формы поможет тег `{formContainer}`. +С отрисовкой элементов внутри контейнера формы помогает тег `{formContainer}` или более новый [`{form scope}` |#{form}]. ```latte <p>Какие новости вы хотите получать:</p> @@ -257,39 +275,39 @@ protected function createComponentSignInForm(): Form Отрисовка без Latte =================== -Самый простой способ отрисовать форму — вызвать: +Проще всего отрисовать форму вызовом: ```php $form->render(); ``` -Повлиять на вид отрисованной таким образом формы можно с помощью конфигурации [#Renderer] и [отдельных элементов |#HTML-атрибуты]. +На внешний вид отрисованной формы можно повлиять настройкой [#Renderer] и [отдельных элементов |#HTML-атрибуты]. Ручная отрисовка ---------------- -Каждый элемент формы располагает методами, которые генерируют HTML-код поля формы и метки (labels). Они могут возвращать его либо как строку, либо как объект [Nette\Utils\Html |utils:html-elements]: +У каждого элемента формы есть методы, порождающие HTML-код поля формы и его метки. Они могут вернуть его либо строкой, либо объектом [Nette\Utils\Html |utils:html-elements]: - `getControl(): Html|string` возвращает HTML-код элемента -- `getLabel($caption = null): Html|string|null` возвращает HTML-код метки, если она существует +- `getLabel($caption = null): Html|string|null` возвращает HTML-код метки, если она есть -Таким образом, форму можно отрисовывать по отдельным элементам: +Это позволяет отрисовать форму элемент за элементом: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,21 +315,21 @@ $form->render(); <?php $form->render('end') ?> ``` -В то время как для некоторых элементов `getControl()` возвращает единственный HTML-элемент (например, `<input>`, `<select>` и т. д.), для других — целый кусок HTML-кода (CheckboxList, RadioList). В таком случае вы можете использовать методы, которые генерируют отдельные инпуты и метки (labels), для каждого элемента отдельно: +Если у некоторых элементов `getControl()` возвращает один HTML-элемент (например, `<input>`, `<select>` и т. п.), то у других он возвращает целый кусок HTML-кода (CheckboxList, RadioList). В таких случаях можно использовать методы, порождающие отдельные поля и метки для каждого пункта по отдельности: -- `getControlPart($key = null): ?Html` возвращает HTML-код одного элемента -- `getLabelPart($key = null): ?Html` возвращает HTML-код метки одного элемента +- `getControlPart($key = null): Html` возвращает HTML-код одного пункта +- `getLabelPart($key = null): Html` возвращает HTML-код метки одного пункта .[note] -Эти методы по историческим причинам имеют префикс `get`, но лучше было бы `generate`, потому что при каждом вызове они создают и возвращают новый `Html` элемент. +У этих методов приставка `get` по историческим причинам, но уместнее было бы `generate`, потому что при каждом вызове они создают и возвращают новый элемент `Html`. Renderer ======== -Это объект, обеспечивающий отрисовку формы. Его можно установить методом `$form->setRenderer`. Ему передается управление при вызове метода `$form->render()`. +Это объект, отвечающий за отрисовку формы. Задать его можно методом `$form->setRenderer()`. Управление ему передаётся при вызове метода `$form->render()`. -Если мы не установим собственный рендерер, будет использован рендерер по умолчанию [api:Nette\Forms\Rendering\DefaultFormRenderer]. Он отрисует элементы формы в виде HTML-таблицы. Вывод выглядит так: +Если мы не зададим собственный отрисовщик, будет использован стандартный [api:Nette\Forms\Rendering\DefaultFormRenderer]. Он отрисовывает элементы формы в HTML-таблицу. Вывод выглядит так: ```latte <table> @@ -332,11 +350,11 @@ Renderer ... ``` -Использовать таблицу для каркаса формы или нет — спорный вопрос, и многие веб-дизайнеры предпочитают другую разметку. Например, список определений. Поэтому переконфигурируем `DefaultFormRenderer` так, чтобы он отрисовал форму в виде списка. Конфигурация выполняется путем редактирования массива [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Первый индекс всегда представляет область, а второй — ее атрибут. Отдельные области показаны на рисунке: +Использовать ли для структуры формы таблицу - вопрос спорный, и многие веб-дизайнеры предпочитают другую разметку, например список определений. Поэтому мы перенастроим `DefaultFormRenderer` так, чтобы он отрисовывал форму списком. Настройка выполняется правкой массива [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Первый индекс всегда обозначает область, а второй - её свойство. Отдельные области показаны на картинке: -[* defaultformrenderer.webp *] +[* form-areas-en.webp *] -По умолчанию группа элементов `controls` обернута таблицей `<table>`, каждый `pair` представляет строку таблицы `<tr>`, а пара `label` и `control` — ячейки `<th>` и `<td>`. Теперь изменим оборачивающие элементы. Область `controls` вложим в контейнер `<dl>`, область `pair` оставим без контейнера, `label` вложим в `<dt>` и, наконец, `control` обернем тегами `<dd>`: +По умолчанию группа `controls` обёрнута в `<table>`, каждая `pair` представляет строку таблицы `<tr>`, а пара `label` и `control` - ячейки `<th>` и `<td>`. Теперь мы изменим обёртывающие элементы. Область `controls` поместим в контейнер `<dl>`, область `pair` оставим без контейнера, `label` поместим в `<dt>`, а `control` наконец обернём тегами `<dd>`: ```php $renderer = $form->getRenderer(); @@ -348,7 +366,7 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -Результатом является этот HTML-код: +Результатом будет такой HTML-код: ```latte <dl> @@ -367,49 +385,49 @@ $form->render(); </dl> ``` -В массиве wrappers можно повлиять на целый ряд других атрибутов: +Массив wrappers позволяет влиять и на многие другие свойства: - добавлять CSS-классы отдельным типам элементов формы -- различать CSS-классом четные и нечетные строки -- визуально отличать обязательные и необязательные элементы -- определять, будут ли сообщения об ошибках отображаться непосредственно у элементов или над формой +- различать чётные и нечётные строки CSS-классами +- визуально отличать обязательные пункты от необязательных +- определять, показываются ли сообщения об ошибках прямо рядом с элементами или над формой -Options -------- +Параметры +--------- -Поведением Renderer можно управлять также путем установки *options* для отдельных элементов формы. Так можно установить надпись, которая выведется рядом с полем ввода: +Поведением отрисовщика можно управлять и заданием *параметров* у отдельных элементов формы. Так можно задать пояснение, которое появится рядом с полем ввода: ```php $form->addText('phone', 'Номер:') ->setOption('description', 'Этот номер останется скрытым'); ``` -Если мы хотим поместить в него HTML-содержимое, используем класс [Html |utils:html-elements] +Если мы хотим разместить в нём HTML-содержимое, воспользуемся классом [Html |utils:html-elements]: ```php use Nette\Utils\Html; -$form->addText('phone', 'Номер:') +$form->addText('phone', 'Телефон:') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Условия хранения вашего номера</a>') + ->setHtml('<a href="...">Условия использования.</a>') ); ``` .[tip] -Html-элемент можно использовать и вместо метки (label): `$form->addCheckbox('conditions', $label)`. +Элемент Html можно использовать и вместо метки: `$form->addCheckbox('conditions', $label)`. Группировка элементов --------------------- -Renderer позволяет группировать элементы в визуальные группы (fieldset): +Отрисовщик позволяет группировать элементы в визуальные группы (fieldset): ```php $form->addGroup('Личные данные'); ``` -После создания новой группы она становится активной, и каждый вновь добавленный элемент одновременно добавляется и в нее. Так что форму можно строить таким образом: +После создания новой группы она становится активной, и каждый вновь добавленный элемент добавляется и в неё. Так что форму можно строить вот так: ```php $form = new Form; @@ -419,94 +437,94 @@ $form->addInteger('age', 'Ваш возраст:'); $form->addEmail('email', 'Email:'); $form->addGroup('Адрес доставки'); -$form->addCheckbox('send', 'Отправить по адресу'); +$form->addCheckbox('send', 'Доставить по адресу'); $form->addText('street', 'Улица:'); $form->addText('city', 'Город:'); $form->addSelect('country', 'Страна:', $countries); ``` -Renderer сначала отрисовывает группы, а затем элементы, которые не принадлежат ни к какой группе. +Отрисовщик рисует сначала группы, а затем элементы, которые ни в какую группу не входят. Поддержка Bootstrap ------------------- -[В примерах |https://github.com/nette/forms/tree/master/examples] вы найдете примеры, как сконфигурировать Renderer для [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] и [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] +В [каталоге examples |https://github.com/nette/forms/tree/master/examples] вы найдёте примеры того, как настроить отрисовщик для [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] и [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php]. HTML-атрибуты ============= -Для установки любых HTML-атрибутов элементов формы используем метод `setHtmlAttribute(string $name, $value = true)`: +Чтобы задать элементам формы произвольные HTML-атрибуты, используйте метод `setHtmlAttribute(string $name, $value = true)`: ```php $form->addInteger('number', 'Номер:') ->setHtmlAttribute('class', 'big-number'); $form->addSelect('rank', 'Сортировать по:', ['цене', 'названию']) - ->setHtmlAttribute('onchange', 'submit()'); // отправить при изменении + ->setHtmlAttribute('onchange', 'submit()'); // отправить форму при изменении -// Для установки атрибутов самой <form> +// Чтобы задать атрибуты самого элемента <form> $form->setHtmlAttribute('id', 'myForm'); ``` -Спецификация типа элемента: +Указание типа элемента: ```php $form->addText('tel', 'Ваш телефон:') ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'введите телефон'); + ->setHtmlAttribute('placeholder', 'Заполните, пожалуйста, ваш телефон'); ``` .[warning] -Установка типа и других атрибутов служит только для визуальных целей. Проверка правильности ввода должна происходить на сервере, что вы обеспечите выбором подходящего [элемента формы |controls] и указанием [правил валидации |validation]. +Задание типа и других атрибутов служит только для визуальных целей. Проверка правильности ввода должна происходить на стороне сервера, что вы обеспечиваете выбором подходящего [элемента формы |controls] и указанием [правил проверки |validation]. -Отдельным элементам в списках radio или checkbox мы можем установить HTML-атрибут с различными значениями для каждого из них. Обратите внимание на двоеточие после `style:`, которое обеспечит выбор значения по ключу: +Отдельным пунктам радиосписков и списков флажков можно задать HTML-атрибут с разными значениями для каждого. Обратите внимание на двоеточие после `style:`, которое обеспечивает выбор значения по ключу: ```php -$colors = ['r' => 'красный', 'g' => 'зеленый', 'b' => 'синий']; +$colors = ['r' => 'красный', 'g' => 'зелёный', 'b' => 'синий']; $styles = ['r' => 'background:red', 'g' => 'background:green']; $form->addCheckboxList('colors', 'Цвета:', $colors) ->setHtmlAttribute('style:', $styles); ``` -Выведет: +Отрисует: ```latte <label><input type="checkbox" name="colors[]" style="background:red" value="r">красный</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">зеленый</label> +<label><input type="checkbox" name="colors[]" style="background:green" value="g">зелёный</label> <label><input type="checkbox" name="colors[]" value="b">синий</label> ``` -Для установки логических атрибутов, таких как `readonly`, мы можем использовать запись с вопросительным знаком: +Для задания логических атрибутов, например `readonly`, можно использовать запись с вопросительным знаком: ```php $form->addCheckboxList('colors', 'Цвета:', $colors) ->setHtmlAttribute('readonly?', 'r'); // для нескольких ключей используйте массив, например ['r', 'g'] ``` -Выведет: +Отрисует: ```latte <label><input type="checkbox" name="colors[]" readonly value="r">красный</label> -<label><input type="checkbox" name="colors[]" value="g">зеленый</label> +<label><input type="checkbox" name="colors[]" value="g">зелёный</label> <label><input type="checkbox" name="colors[]" value="b">синий</label> ``` -В случае селектбоксов метод `setHtmlAttribute()` устанавливает атрибуты элемента `<select>`. Если мы хотим установить атрибуты отдельным `<option>`, используем метод `setOptionAttribute()`. Работают и записи с двоеточием и вопросительным знаком, указанные выше: +У выпадающих списков метод `setHtmlAttribute()` задаёт атрибуты элемента `<select>`. Если мы хотим задать атрибуты отдельных элементов `<option>`, воспользуемся методом `setOptionAttribute()`. Упомянутые выше записи с двоеточием и вопросительным знаком тоже работают: ```php $form->addSelect('colors', 'Цвета:', $colors) ->setOptionAttribute('style:', $styles); ``` -Выведет: +Отрисует: ```latte <select name="colors"> <option value="r" style="background:red">красный</option> - <option value="g" style="background:green">зеленый</option> + <option value="g" style="background:green">зелёный</option> <option value="b">синий</option> </select> ``` @@ -515,7 +533,7 @@ $form->addSelect('colors', 'Цвета:', $colors) Прототипы --------- -Альтернативный способ установки HTML-атрибутов заключается в изменении прототипа (prototype), из которого генерируется HTML-элемент. Прототипом является объект `Html`, и его возвращает метод `getControlPrototype()`: +Другой способ задавать HTML-атрибуты - изменить шаблон, из которого порождается HTML-элемент. Шаблон - это объект `Html`, и его возвращает метод `getControlPrototype()`: ```php $input = $form->addInteger('number', 'Номер:'); @@ -523,14 +541,14 @@ $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -Этим способом можно модифицировать и прототип метки (label), который возвращает `getLabelPrototype()`: +Так же можно изменить и шаблон метки, который возвращает `getLabelPrototype()`: ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -У элементов Checkbox, CheckboxList и RadioList вы можете влиять на прототип элемента, который оборачивает весь элемент. Его возвращает `getContainerPrototype()`. В исходном состоянии это «пустой» элемент, так что ничего не отрисовывается, но тем, что мы установим ему имя, он будет отрисовываться: +У элементов Checkbox, CheckboxList и RadioList можно повлиять на шаблон элемента, который обёртывает весь элемент формы. Его возвращает `getContainerPrototype()`. По умолчанию это "пустой" элемент, так что ничего не отрисовывается, но если дать ему имя, он отрисуется: ```php $input = $form->addCheckbox('send'); @@ -541,49 +559,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -В случае CheckboxList и RadioList можно влиять и на прототип разделителя отдельных элементов, который возвращает метод `getSeparatorPrototype()`. В исходном состоянии это элемент `<br>`. Если вы измените его на парный элемент, он будет оборачивать отдельные элементы вместо разделения. А далее можно влиять на прототип HTML-элемента метки у отдельных элементов, который возвращает `getItemLabelPrototype()`. +У CheckboxList и RadioList можно повлиять и на шаблон разделителя отдельных пунктов, который возвращает метод `getSeparatorPrototype()`. По умолчанию это элемент `<br>`. Если вы смените его на парный элемент, он будет обёртывать отдельные пункты, а не разделять их. Кроме того, можно повлиять на шаблон HTML-элемента меток отдельных пунктов, который возвращает `getItemLabelPrototype()`. Перевод ======= -Если вы программируете многоязычное приложение, вам, вероятно, потребуется отрисовать форму в различных языковых версиях. Nette Framework для этой цели определяет интерфейс для перевода [api:Nette\Localization\Translator]. В Nette нет реализации по умолчанию, вы можете выбрать в соответствии со своими потребностями из нескольких готовых решений, которые найдете на [Componette |https://componette.org/search/localization]. В их документации вы узнаете, как конфигурировать переводчик (translator). +Если вы разрабатываете многоязычное приложение, вам, скорее всего, понадобится отрисовывать форму в разных языковых версиях. Nette Framework определяет для этого интерфейс перевода: [api:Nette\Localization\Translator]. Стандартной реализации в Nette нет, вы можете выбрать по своим нуждам из нескольких готовых решений, которые найдёте на [Componette |https://componette.org/search/localization]. В их документации написано, как настроить переводчик. -Формы поддерживают вывод текстов через переводчик (translator). Передадим его им с помощью метода `setTranslator()`: +Формы поддерживают вывод текстов через переводчик. Передаём его методом `setTranslator()`: ```php $form->setTranslator($translator); ``` -С этого момента не только все метки (labels), но и все сообщения об ошибках или элементы select box будут переведены на другой язык. +С этого момента на целевой язык будут переводиться не только все метки, но и все сообщения об ошибках, пункты выпадающих списков и подсказки в полях. -При этом для отдельных элементов формы можно установить другой переводчик или полностью отключить перевод, установив значение `null`: +Отдельным элементам формы можно задать другой переводчик или полностью отключить перевод, задав значение `null`: ```php $form->addSelect('carModel', 'Модель:', $cars) ->setTranslator(null); ``` -Для [правил валидации |validation] переводчику (translator) передаются также специфические параметры, например, у правила: +Для [правил проверки |validation] переводчику передаются и конкретные параметры. Например, для правила: ```php $form->addPassword('password', 'Пароль:') - ->addRule($form::MinLength, 'Пароль должен содержать не менее %d символов', 8); + ->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов', 8); ``` -вызывается переводчик (translator) с этими параметрами: +переводчик вызывается с такими параметрами: ```php -$translator->translate('Пароль должен содержать не менее %d символов', 8); +$translator->translate('Пароль должен быть длиной не менее %d символов', 8); ``` -и, следовательно, он может выбрать правильную форму множественного числа для слова `символов` в зависимости от числа. +и, стало быть, может по количеству выбрать правильную форму множественного числа слова `символов`. Событие onRender ================ -Непосредственно перед отрисовкой формы мы можем вызвать наш код. Он может, например, дополнить элементы формы HTML-классами для правильного отображения. Код добавим в массив `onRender`: +Прямо перед отрисовкой формы мы можем дать вызвать свой код. Он может, например, добавить элементам формы HTML-классы для правильного отображения. Код добавляем в массив `onRender`: ```php $form->onRender[] = function ($form) { diff --git a/forms/ru/standalone.texy b/forms/ru/standalone.texy index a1dbc93c28..e56c740acf 100644 --- a/forms/ru/standalone.texy +++ b/forms/ru/standalone.texy @@ -1,16 +1,22 @@ -Использование форм отдельно -*************************** +Формы отдельно от фреймворка +**************************** .[perex] -Nette Forms значительно упрощают создание и обработку веб-форм. Вы можете использовать их в своих приложениях совершенно отдельно от остальной части фреймворка, что мы и покажем в этой главе. +Nette Forms радикально упрощают создание и обработку веб-форм. Вы можете использовать их в своих приложениях совершенно самостоятельно, без остального фреймворка, как показано в этой главе. -Однако, если вы используете Nette Application и презентеры, для вас предназначено руководство по [использованию в презентерах |in-presenter]. +Однако если вы используете Nette Application и презентеры, для вас есть отдельное руководство: [формы в презентерах |in-presenter]. Первая форма ============ -Попробуем написать простую регистрационную форму. Ее код будет следующим ("весь код":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): +Прежде чем начать, установите пакет с помощью [Composer |best-practices:composer]: + +```shell +composer require nette/forms +``` + +Попробуем написать простую регистрационную форму. Её код будет таким ("полный код":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php use Nette\Forms\Form; @@ -21,23 +27,23 @@ $form->addPassword('password', 'Пароль:'); $form->addSubmit('send', 'Зарегистрироваться'); ``` -Мы можем очень легко ее отрисовать: +И отрисуем её совсем просто: ```php $form->render(); ``` -и в браузере она отобразится так: +Результат в браузере должен выглядеть так: -[* form-cs.webp *] +[* form-en.webp *] -Форма — это объект класса `Nette\Forms\Form` (класс `Nette\Application\UI\Form` используется в презентерах). Мы добавили в нее так называемые элементы: имя, пароль и кнопку отправки. +Форма - это объект класса `Nette\Forms\Form` (класс `Nette\Application\UI\Form` используется в презентерах). Мы добавили в неё элементы с именами "name", "password" и кнопку отправки. -А теперь оживим форму. С помощью запроса `$form->isSuccess()` мы узнаем, была ли форма отправлена и была ли она заполнена валидно. Если да, то выведем данные. Дополним определение формы: +Теперь оживим форму. Запросив `$form->isSuccess()`, мы узнаем, была ли форма отправлена и была ли она корректно заполнена. Если да, выведем данные. После определения формы допишите: ```php if ($form->isSuccess()) { - echo 'Форма была правильно заполнена и отправлена'; + echo 'Форма была корректно заполнена и отправлена'; $data = $form->getValues(); // $data->name содержит имя // $data->password содержит пароль @@ -45,24 +51,24 @@ if ($form->isSuccess()) { } ``` -Метод `getValues()` возвращает отправленные данные в виде объекта [ArrayHash |utils:arrays#ArrayHash]. Как это изменить, мы покажем [позже |#Маппинг на классы]. Объект `$data` содержит ключи `name` и `password` с данными, которые ввел пользователь. +Метод `getValues()` возвращает отправленные данные в виде объекта [ArrayHash |utils:arrays#ArrayHash]. Как это изменить, мы покажем [позже |#Отображение в классы]. Объект `$data` содержит ключи `name` и `password` с данными, которые ввёл пользователь. -Обычно мы сразу отправляем данные для дальнейшей обработки, например, для вставки в базу данных. Однако во время обработки может возникнуть ошибка, например, имя пользователя уже занято. В таком случае мы передаем ошибку обратно в форму с помощью `addError()` и позволяем ей отобразиться снова, уже с сообщением об ошибке. +Обычно мы отправляем данные прямо на дальнейшую обработку, например на вставку в базу данных. Однако при обработке может возникнуть ошибка, например если имя пользователя уже занято. В таком случае мы передаём ошибку обратно в форму методом `addError()` и даём отрисовать её снова вместе с сообщением об ошибке. ```php -$form->addError('Извините, это имя пользователя уже используется.'); +$form->addError('Извините, это имя пользователя уже занято.'); ``` -После обработки формы мы перенаправляем на следующую страницу. Это предотвратит нежелательную повторную отправку формы кнопкой *обновить*, *назад* или перемещением по истории браузера. +После обработки формы мы перенаправляем на следующую страницу. Это предотвращает непреднамеренную повторную отправку формы кнопками *обновить* и *назад* или переходом по истории браузера. -По умолчанию форма отправляется методом POST на ту же страницу. Оба параметра можно изменить: +По умолчанию форма отправляется методом POST на ту же страницу. И то и другое можно изменить: ```php $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -И это, собственно, все :-) У нас есть рабочая и идеально [защищенная |#Защита от уязвимостей] форма. +И это, по сути, всё :-) У нас есть работающая и превосходно [защищённая |#Защита от уязвимостей] форма. Попробуйте добавить и другие [элементы формы |controls]. @@ -70,46 +76,46 @@ $form->setMethod('GET'); Доступ к элементам ================== -Форму и ее отдельные элементы мы называем компонентами. Они образуют дерево компонентов, где корнем является сама форма. К отдельным элементам формы можно получить доступ следующим образом: +Форму и её отдельные элементы называют компонентами. Они образуют дерево компонентов, корнем которого является форма. К отдельным элементам формы можно обратиться так: ```php $input = $form->getComponent('name'); -// альтернативный синтаксис: $input = $form['name']; +// альтернативная запись: $input = $form['name']; $button = $form->getComponent('send'); -// альтернативный синтаксис: $button = $form['send']; +// альтернативная запись: $button = $form['send']; ``` -Элементы удаляются с помощью `unset`: +Элементы удаляются через `unset`: ```php unset($form['name']); ``` -Правила валидации -================= +Правила проверки +================ -Здесь прозвучало слово *валидный*, но у формы пока нет никаких правил валидации. Давайте это исправим. +Прозвучало слово *корректна*, но у формы пока нет никаких правил проверки. Исправим это. -Имя будет обязательным, поэтому мы пометим его методом `setRequired()`, аргументом которого является текст сообщения об ошибке, которое отобразится, если пользователь не заполнит имя. Если аргумент не указан, будет использовано сообщение об ошибке по умолчанию. +Имя будет обязательным, поэтому пометим его методом `setRequired()`. Его аргумент - текст сообщения об ошибке, которое отобразится, если пользователь имя не заполнит. Если аргумент не указан, будет использовано стандартное сообщение об ошибке. ```php $form->addText('name', 'Имя:') - ->setRequired('Пожалуйста, введите имя'); + ->setRequired('Введите, пожалуйста, имя.'); ``` -Попробуйте отправить форму без заполненного имени, и вы увидите, что отобразится сообщение об ошибке, и браузер или сервер будут отклонять ее до тех пор, пока вы не заполните поле. +Попробуйте отправить форму, не заполнив имя, и вы увидите, что появится сообщение об ошибке. Браузер или сервер будут её отклонять, пока вы поле не заполните. -В то же время, вы не обманете систему, введя в поле, например, только пробелы. Ни в коем случае. Nette автоматически удаляет пробелы слева и справа. Попробуйте сами. Это то, что вы всегда должны делать с каждым однострочным вводом, но часто об этом забывают. Nette делает это автоматически. (Вы можете попробовать обмануть форму и отправить многострочную строку в качестве имени. И здесь Nette не даст себя обмануть и заменит переносы строк на пробелы.) +При этом систему не обмануть, введя в поле только пробелы. Никак. Nette автоматически обрезает пробелы слева и справа. Попробуйте. Это то, что нужно всегда делать с каждым однострочным полем, но об этом часто забывают. Nette делает это автоматически. (Можете попробовать одурачить форму, отправив в качестве имени многострочную строку. И тут Nette не проведёшь, переводы строк будут заменены пробелами.) -Форма всегда валидируется на стороне сервера, но также генерируется JavaScript-валидация, которая выполняется мгновенно, и пользователь узнает об ошибке сразу, без необходимости отправлять форму на сервер. За это отвечает скрипт `netteForms.js`. Вставьте его на страницу: +Форма всегда проверяется на стороне сервера, но порождается и проверка на JavaScript. Она выполняется мгновенно, и пользователь узнаёт об ошибках сразу, без необходимости отправлять форму на сервер. За это отвечает скрипт `netteForms.js`. Вставьте его в страницу: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Если вы посмотрите исходный код страницы с формой, вы можете заметить, что Nette вставляет обязательные элементы в элементы с CSS-классом `required`. Попробуйте добавить в шаблон следующую таблицу стилей, и метка «Имя» станет красной. Таким образом, мы элегантно выделим обязательные элементы для пользователей: +Если вы посмотрите на исходный код страницы с формой, то заметите, что Nette помещает обязательные элементы в элементы с CSS-классом `required`. Попробуйте добавить в шаблон следующий стиль, и метка "Имя" станет красной. Так вы элегантно выделите для пользователей обязательные элементы: ```latte <style> @@ -117,85 +123,102 @@ $form->addText('name', 'Имя:') </style> ``` -Другие правила валидации мы добавим методом `addRule()`. Первый параметр — это правило, второй — снова текст сообщения об ошибке, а затем может следовать аргумент правила валидации. Что под этим подразумевается? +Другие правила проверки мы добавляем методом `addRule()`. Первый параметр - правило, второй - снова текст сообщения об ошибке, а за ним может следовать необязательный аргумент правила проверки. Что это значит? -Расширим форму новым необязательным полем «возраст», которое должно быть целым числом (`addInteger()`) и, кроме того, находиться в допустимом диапазоне (`$form::Range`). И здесь мы как раз используем третий параметр метода `addRule()`, которым передадим валидатору требуемый диапазон в виде пары `[от, до]`: +Расширим форму новым необязательным полем "возраст", которое должно быть целым числом (`addInteger()`) и находиться в допустимом диапазоне (`$form::Range`). Здесь мы используем третий параметр метода `addRule()`, чтобы передать валидатору нужный диапазон парой `[min, max]`: ```php $form->addInteger('age', 'Возраст:') - ->addRule($form::Range, 'Возраст должен быть от 18 до 120', [18, 120]); + ->addRule($form::Range, 'Возраст должен быть от 18 до 120 лет.', [18, 120]); ``` .[tip] -Если пользователь не заполнит поле, правила валидации проверяться не будут, так как элемент необязательный. +Если пользователь поле не заполнит, правила проверки проверяться не будут, потому что элемент необязателен. -Здесь возникает возможность для небольшого рефакторинга. В сообщении об ошибке и в третьем параметре числа указаны дублировано, что не идеально. Если бы мы создавали [многоязычные формы |rendering#Перевод] и сообщение, содержащее числа, было бы переведено на несколько языков, это усложнило бы возможное изменение значений. По этой причине можно использовать заполнители (placeholders) `%d`, и Nette дополнит значения: +Здесь появляется место для небольшого рефакторинга. Числа дублируются в сообщении об ошибке и в третьем параметре, а это неидеально. Если бы мы создавали [многоязычные формы |rendering#Перевод] и сообщение с числами переводилось бы на несколько языков, менять значения стало бы трудно. Поэтому можно использовать подстановки `%d`, и Nette значения подставит: ```php - ->addRule($form::Range, 'Возраст должен быть от %d до %d лет', [18, 120]); + ->addRule($form::Range, 'Возраст должен быть от %d до %d лет.', [18, 120]); ``` -Вернемся к элементу `password`, который мы также сделаем обязательным и еще проверим минимальную длину пароля (`$form::MinLength`), снова используя заполнитель: +Вернёмся к элементу `password`, сделаем его тоже обязательным и заодно проверим минимальную длину пароля (`$form::MinLength`), снова с подстановкой в сообщении: ```php $form->addPassword('password', 'Пароль:') ->setRequired('Выберите пароль') - ->addRule($form::MinLength, 'Пароль должен содержать не менее %d символов', 8); + ->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов', 8); ``` -Добавим в форму еще поле `passwordVerify`, где пользователь введет пароль еще раз, для проверки. С помощью правил валидации проверим, совпадают ли оба пароля (`$form::Equal`). А в качестве параметра дадим ссылку на первый пароль с помощью [квадратных скобок |#Доступ к элементам]: +Добавим в форму ещё одно поле `passwordVerify`, где пользователь введёт пароль повторно для проверки. С помощью правил проверки убедимся, что оба пароля одинаковы (`$form::Equal`). В качестве параметра передадим ссылку на первый пароль через [квадратные скобки |#Доступ к элементам]: ```php -$form->addPassword('passwordVerify', 'Пароль для проверки:') - ->setRequired('Пожалуйста, введите пароль еще раз для проверки') +$form->addPassword('passwordVerify', 'Пароль ещё раз:') + ->setRequired('Введите, пожалуйста, пароль ещё раз для проверки') ->addRule($form::Equal, 'Пароли не совпадают', $form['password']) ->setOmitted(); ``` -С помощью `setOmitted()` мы пометили элемент, значение которого нас на самом деле не интересует и который существует только для валидации. Значение не будет передано в `$data`. +С помощью `setOmitted()` мы пометили элемент, значение которого нас на самом деле не интересует и который существует только ради проверки. Его значение в `$data` не передаётся. -Таким образом, у нас готова полностью рабочая форма с валидацией на PHP и JavaScript. Возможности валидации Nette гораздо шире, можно создавать условия, отображать и скрывать части страницы в зависимости от них и т. д. Все это вы узнаете в главе о [валидации форм |validation]. +Тем самым у нас есть полностью работающая форма с проверкой и в PHP, и в JavaScript. Возможности проверки в Nette намного шире: можно создавать условия, по ним показывать и скрывать части страницы и т. д. Обо всём вы узнаете в главе о [проверке форм |validation]. Значения по умолчанию ===================== -Элементам формы обычно устанавливают значения по умолчанию: +Значения по умолчанию для элементов формы мы задаём часто: ```php -$form->addEmail('email', 'E-mail') +$form->addEmail('email', 'Email') ->setDefaultValue($lastUsedEmail); ``` -Часто бывает полезно установить значения по умолчанию для всех элементов одновременно. Например, когда форма используется для редактирования записей. Мы читаем запись из базы данных и устанавливаем значения по умолчанию: +Часто бывает полезно задать значения по умолчанию сразу всем элементам, например когда форма используется для редактирования записей. Мы считываем запись из базы данных и задаём её значения как значения по умолчанию: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` Вызывайте `setDefaults()` после определения элементов. +У уже отправленной формы `setDefaults()` ничего не делает: он не перезапишет то, что заполнил пользователь, так что вызывать его в фабрике формы безусловно безопасно. Если вам нужно задать значения принудительно и после отправки, используйте вместо него `setValues()`. + Отрисовка формы =============== -По умолчанию форма отрисовывается в виде таблицы. Отдельные элементы соответствуют основному правилу доступности - все метки записаны как `<label>` и связаны с соответствующим элементом формы. При клике на метку курсор автоматически появляется в поле формы. +По умолчанию форма отрисовывается как таблица. Отдельные элементы соблюдают основные правила доступности: все метки порождаются как элементы `<label>` и связаны с соответствующими элементами формы. Щелчок по метке автоматически ставит курсор в поле формы. -Каждому элементу мы можем устанавливать любые HTML-атрибуты. Например, добавить плейсхолдер: +Каждому элементу мы можем задать произвольные HTML-атрибуты. Например, добавить placeholder: ```php $form->addInteger('age', 'Возраст:') - ->setHtmlAttribute('placeholder', 'Пожалуйста, заполните возраст'); + ->setHtmlAttribute('placeholder', 'Заполните, пожалуйста, возраст'); ``` -Способов отрисовки формы действительно очень много, поэтому этому посвящена [отдельная глава об отрисовке |rendering]. +Способов отрисовать форму много, поэтому [отрисовке посвящена отдельная глава |rendering]. -Маппинг на классы -================= +Отрисовка с помощью Latte +------------------------- -Вернемся к обработке данных формы. Метод `getValues()` возвращал нам отправленные данные как объект `ArrayHash`. Поскольку это обобщенный класс, что-то вроде `stdClass`, при работе с ним нам будет не хватать определенного комфорта, такого как автодополнение свойств в редакторах или статический анализ кода. Это можно было бы решить, создав для каждой формы конкретный класс, свойства которого представляют отдельные элементы. Например: +Если у вас под рукой шаблонизатор [Latte |latte:], вы можете поручить отрисовку формы ему и получить полный контроль над итоговым HTML. Вы создаёте движок, регистрируете расширение форм и передаёте форму в шаблон переменной: + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); +``` + +В шаблоне вы затем работаете с формой через переменную `$form` и теги вроде `{input}`, `{label}` или `n:name`. Полный пример вместе с шаблоном найдёте в каталоге [examples |https://github.com/nette/forms/tree/master/examples] (файлы `latte.php` и `latte/`). Отдельные теги описаны в главе об [отрисовке |rendering]. + + +Отображение в классы +==================== + +Вернёмся к обработке данных формы. Метод `getValues()` возвращал отправленные данные как объект `ArrayHash`. Поскольку это универсальный класс, похожий на `stdClass`, при работе с ним нам не хватает определённых удобств, например автодополнения свойств в редакторах или статического анализа кода. Это можно решить, заведя для каждой формы отдельный класс, свойства которого представляют отдельные элементы. Например: ```php class RegistrationFormData @@ -206,32 +229,32 @@ class RegistrationFormData } ``` -Альтернативно можно использовать конструктор: +Как вариант, можно использовать конструктор: ```php class RegistrationFormData { public function __construct( public string $name, - public int $age, + public ?int $age, public string $password, ) { } } ``` -Свойства класса данных также могут быть перечислениями (enum), и они будут автоматически сопоставлены. .{data-version:3.2.4} +Свойства класса данных могут быть и перечислениями, и они будут отображены автоматически. .{data-version:3.2.4} -Как сказать Nette, чтобы он возвращал нам данные в виде объектов этого класса? Проще, чем вы думаете. Достаточно указать имя класса или объект для гидратации в качестве параметра: +Как сказать Nette, чтобы она возвращала данные как объекты этого класса? Проще, чем вы думаете. Достаточно указать параметром имя класса или объект для наполнения: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -В качестве параметра можно также указать `'array'`, и тогда данные будут возвращены в виде массива. +В качестве параметра можно указать и `'array'`, тогда данные вернутся массивом. -Если формы образуют многоуровневую структуру, состоящую из контейнеров, создайте для каждого отдельный класс: +Если формы состоят из многоуровневой структуры из контейнеров, создайте для каждого отдельный класс: ```php $form = new Form; @@ -253,19 +276,19 @@ class RegistrationFormData } ``` -Маппинг затем по типу свойства `$person` поймет, что контейнер нужно сопоставить с классом `PersonFormData`. Если бы свойство содержало массив контейнеров, укажите тип `array` и передайте класс для маппинга непосредственно контейнеру: +Тогда отображение по типу свойства `$person` понимает, что контейнер нужно отобразить в класс `PersonFormData`. Если бы свойство содержало массив контейнеров, укажите тип `array`, а класс для отображения передайте прямо контейнеру: ```php $person->setMappedType(PersonFormData::class); ``` -Проект класса данных для формы можно сгенерировать с помощью метода `Nette\Forms\Blueprint::dataClass($form)`, который выведет его на страницу браузера. Затем достаточно кликнуть, чтобы выделить код, и скопировать его в проект. .{data-version:3.1.15} +Заготовку класса данных формы можно породить методом `Nette\Forms\Blueprint::dataClass($form)`, который выведет её на страницу в браузере. Дальше достаточно щелчком выделить код и скопировать его в проект. .{data-version:3.1.15} -Несколько кнопок -================ +Несколько кнопок отправки +========================= -Если у формы больше одной кнопки, нам обычно нужно различать, какая из них была нажата. Эту информацию нам вернет метод `isSubmittedBy()` кнопки: +Если у формы больше одной кнопки, нам обычно нужно различить, какая была нажата. Эти сведения возвращает метод кнопки `isSubmittedBy()`: ```php $form->addSubmit('save', 'Сохранить'); @@ -282,36 +305,33 @@ if ($form->isSuccess()) { } ``` -Не пропускайте запрос `$form->isSuccess()`, он проверит валидность данных. +Не опускайте проверку `$form->isSuccess()`, она проверяет корректность данных. -Когда форма отправляется нажатием клавиши <kbd>Enter</kbd>, это считается так, как если бы она была отправлена первой кнопкой. +Когда форма отправляется нажатием клавиши <kbd>Enter</kbd>, это считается отправкой первой кнопкой. Защита от уязвимостей ===================== -Nette Framework уделяет большое внимание безопасности и поэтому тщательно заботится о надежной защите форм. +Nette Framework уделяет безопасности большое внимание и поэтому тщательно следит за должной защитой форм. -Помимо защиты форм от атак [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], он выполняет множество мелких мер безопасности, о которых вам уже не нужно думать. +Кроме защиты форм от известных уязвимостей вроде [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], она выполняет множество мелких мер безопасности, о которых вам больше не нужно думать. -Например, он отфильтровывает из входных данных все управляющие символы и проверяет валидность кодировки UTF-8, так что данные из формы всегда будут чистыми. У select box и radio list он проверяет, что выбранные элементы действительно были из предложенных и не произошло подделки. Мы уже упоминали, что у однострочных текстовых полей ввода он удаляет символы конца строки, которые мог отправить злоумышленник. У многострочных полей ввода он нормализует символы конца строки. И так далее. +Например, она отфильтровывает из ввода все управляющие символы и проверяет корректность кодировки UTF-8, благодаря чему данные из формы всегда будут чистыми. У выпадающих списков и радиосписков она проверяет, что выбранные пункты действительно были среди предложенных и что подделки не произошло. Мы уже упоминали, что у однострочных текстовых полей она заменяет пробелами символы конца строки, которые мог отправить злоумышленник. У многострочных полей она приводит символы конца строки к единому виду. И так далее. -Nette решает за вас риски безопасности, о существовании которых многие программисты даже не подозревают. +Nette решает за вас риски безопасности, о существовании которых многие программисты не подозревают. -Упомянутая CSRF-атака заключается в том, что злоумышленник заманивает жертву на страницу, которая незаметно в браузере жертвы выполняет запрос на сервер, на котором жертва авторизована, и сервер полагает, что запрос был выполнен жертвой по ее собственной воле. Поэтому Nette предотвращает отправку POST-формы с другого домена. Если по какой-то причине вы хотите отключить защиту и разрешить отправку формы с другого домена, используйте: +Упомянутая атака CSRF состоит в том, что злоумышленник заманивает жертву на страницу, которая незаметно выполняет в браузере жертвы запрос к серверу, где жертва в этот момент авторизована. Сервер считает, что запрос сделала жертва по своей воле. Поэтому Nette отклоняет POST-формы, отправленные с чужого источника; чужим считается даже другой поддомен того же сайта. Если вам нужно разрешить отправку с другого источника, отключите защиту так: ```php -$form->allowCrossOrigin(); // ВНИМАНИЕ! Отключает защиту! +$form->allowCrossOrigin(); // ВНИМАНИЕ! Полностью отключает защиту! ``` -Эта защита использует SameSite cookie с именем `_nss`. Поэтому создавайте объект формы еще до отправки первого вывода, чтобы можно было отправить cookie. - -Защита с помощью SameSite cookie может быть не 100% надежной, поэтому рекомендуется включить еще защиту с помощью токена: +Правда, это отключает защиту для любого источника. Чтобы разрешить только определённые источники, отключите защиту и сами сверяйте заголовок `Origin` со своим списком разрешённых. -```php -$form->addProtection(); -``` +Защита опирается на браузерный заголовок `Sec-Fetch-Site` (Fetch Metadata), который браузер отправляет автоматически и который нельзя подделать даже при наличии XSS-уязвимости. Старые браузеры, которые эти заголовки не отправляют, проверку не пройдут. Подробно это описано в статье [Браузер наконец решает CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. -Рекомендуем таким образом защищать формы в административной части сайта, которые изменяют чувствительные данные в приложении. Фреймворк защищается от CSRF-атаки путем генерации и проверки авторизационного токена, который сохраняется в сессии. Поэтому необходимо, чтобы перед отображением формы была открыта сессия. В административной части сайта сессия обычно уже запущена из-за входа пользователя. В противном случае запустите сессию методом `Nette\Http\Session::start()`. +.[note] +Прежняя защита с помощью авторизационного токена в сессии, включаемая через `$form->addProtection()`, больше не нужна и объявлена устаревшей начиная с версии 3.3. -Итак, мы рассмотрели быстрое введение в формы в Nette. Попробуйте еще заглянуть в каталог [examples |https://github.com/nette/forms/tree/master/examples] в дистрибутиве, где вы найдете дополнительное вдохновение. +Итак, мы бегло познакомились с формами в Nette. За дополнительным вдохновением загляните в каталог [examples |https://github.com/nette/forms/tree/master/examples] в дистрибутиве. diff --git a/forms/ru/upgrading.texy b/forms/ru/upgrading.texy new file mode 100644 index 0000000000..7577e9e92b --- /dev/null +++ b/forms/ru/upgrading.texy @@ -0,0 +1,54 @@ +Обновление +********** + + +Обновление до версии 3.3 +======================== + +- автоматическая защита от CSRF перешла с `isSameSite()` на `isFrom(FetchSite::SameOrigin)` и стала строже: запросы, приходящие с поддоменов, больше не проходят +- благодаря этому `addProtection()` больше не нужен, потому что автоматическая защита покрывает те же случаи; в новых формах его не пишите, а из существующих спокойно удаляйте + +Почему токены в сессии больше не нужны, объясняется в статье [Четверть века CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Обновление до версии 3.1 +======================== + +- `getValues()` возвращает только проверенные элементы; если вам нужны значения всех элементов независимо от проверки, используйте новый метод `getUntrustedValues()` +- `$values`, передаваемые в обработчики `onSuccess` и `onClick`, точно так же содержат только проверенные элементы +- самостоятельные формы автоматически защищены от CSRF с помощью cookie с флагом SameSite; отправку с другого источника можно разрешить через `allowCrossOrigin()` +- правило `Form::URL` теперь дополняет отсутствующий протокол значением `https` вместо `http` +- `Form::addImage()` переименован в `addImageButton()` +- `Checkbox::getSeparatorPrototype()` переименован в `getContainerPrototype()` +- формы больше не создают в шаблонах переменную `$_form` + +Подробнее об этих изменениях в статье [Новинки в Nette Forms 3.1 |https://blog.nette.org/en/news-in-nette-forms-3-1]. + + +Обновление до версии 3.0 +======================== + +- теперь все элементы формы по умолчанию необязательны (это изменение появилось в Nette 2.4), так что `setRequired(false)` можно убрать +- обязательно обновите `netteForms.js` до версии 3 (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` и `MultiChoiceControl::$checkAllowedValues` заменены методом `checkDefaultValue()` + + +Обновление до версии 2.4 +======================== + +- если у элемента есть правило через `addRule()` (то есть он фактически обязателен), вы должны пометить его как обязательный и через `setRequired()`; кроме того, `setRequired(false)` теперь делает элемент необязательным, что заменяет ветвления `addCondition($form::FILLED)` +- валидаторы `Form::EMAIL`, `URL` и `INTEGER` автоматически меняют HTML-атрибут `type` соответственно на `email`, `url` и `number` +- отрицательные правила проверки объявлены устаревшими; альтернатива для `~Form::FILLED` - это `Form::BLANK`, а `~Form::EQUAL` можно заменить на `Form::NOT_EQUAL` +- внутренний параметр `do` теперь отправляется методом POST как `_do`, чтобы избежать конфликта +- внутренние переменные с подчёркиванием, такие как `$_form`, объявлены устаревшими +- не забудьте обновить `netteForms.js` + + +Обновление до версии 2.3 +======================== + +- внутренние методы фильтрации вроде `Nette\Forms\Controls\TextBase::filterFloat` удалены +- внутренние методы проверки вроде `TextBase::validateFloat` перенесены в `Nette\Forms\Validator`, как и `Rules::$defaultMessages` +- кнопки и скрытые поля порождаются без HTML-идентификатора; если вам нужен ID, задайте его через `setHtmlId()` +- элементы RadioList тоже порождаются без ID; включить его можно через `$radioList->generateId = true` +- фильтры, добавленные через `TextBase::addFilter()`, обрабатываются во время проверки, и теперь фильтры можно добавлять к условиям: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/ru/validation.texy b/forms/ru/validation.texy index 23ffa7de26..ffbfcca571 100644 --- a/forms/ru/validation.texy +++ b/forms/ru/validation.texy @@ -1,108 +1,108 @@ -Валидация форм -************** +Проверка форм +************* Обязательные элементы ===================== -Обязательные элементы помечаются методом `setRequired()`, аргументом которого является текст [сообщения об ошибке |#Сообщения об ошибках], которое отобразится, если пользователь не заполнит элемент. Если аргумент не указан, будет использовано сообщение об ошибке по умолчанию. +Элементы помечаются как обязательные методом `setRequired()`. Его аргумент - текст [сообщения об ошибке |#Сообщения об ошибках], которое отобразится, если пользователь элемент не заполнит. Если аргумент не указан, будет использовано стандартное сообщение об ошибке. ```php $form->addText('name', 'Имя:') - ->setRequired('Пожалуйста, введите имя'); + ->setRequired('Заполните, пожалуйста, имя.'); ``` Правила ======= -Правила валидации добавляются к элементам методом `addRule()`. Первый параметр — это правило, второй — текст [сообщения об ошибке |#Сообщения об ошибках], а третий — аргумент правила валидации. +Правила проверки мы добавляем элементам методом `addRule()`. Первый параметр - правило, второй - [сообщение об ошибке |#Сообщения об ошибках], а третий - аргумент правила проверки. ```php $form->addPassword('password', 'Пароль:') - ->addRule($form::MinLength, 'Пароль должен содержать не менее %d символов', 8); + ->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов', 8); ``` -**Правила валидации проверяются только в том случае, если пользователь заполнил элемент.** +**Правила проверки проверяются, только если пользователь элемент заполнил.** -Nette поставляется с целым рядом предопределенных правил, названия которых являются константами класса `Nette\Forms\Form`. Для всех элементов можно использовать следующие правила: +Nette содержит несколько заранее определённых правил, имена которых являются константами класса `Nette\Forms\Form`. Эти правила мы можем применить ко всем элементам: | константа | описание | тип аргумента |------- -| `Required` | обязательный элемент, псевдоним для `setRequired()` | - -| `Filled` | обязательный элемент, псевдоним для `setRequired()` | - +| `Required` | обязательный элемент, синоним `setRequired()` | - +| `Filled` | обязательный элемент, синоним `setRequired()` | - | `Blank` | элемент не должен быть заполнен | - -| `Equal` | значение равно параметру | `mixed` -| `NotEqual` | значение не равно параметру | `mixed` -| `IsIn` | значение равно одному из элементов массива | `array` -| `IsNotIn` | значение не равно ни одному из элементов массива | `array` -| `Valid` | элемент заполнен правильно? (для [#Условия]) | - +| `Equal` | значение должно быть равно параметру | `mixed` +| `NotEqual` | значение не должно быть равно параметру | `mixed` +| `IsIn` | значение должно быть одним из элементов массива | `array` +| `IsNotIn` | значение не должно быть ни одним из элементов массива | `array` +| `Valid` | правильно ли заполнен элемент? (только в [addConditionOn() |#Условия]) | - -Текстовые поля ввода --------------------- +Текстовые поля +-------------- -Для элементов `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` можно также использовать некоторые из следующих правил: +К элементам `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` можно применить и некоторые из следующих правил: | `MinLength` | минимальная длина текста | `int` | `MaxLength` | максимальная длина текста | `int` -| `Length` | длина в диапазоне или точная длина | пара `[int, int]` или `int` -| `Email` | валидный адрес электронной почты | - +| `Length` | длина в диапазоне или точная длина | пара `[int, int]` либо `int` +| `Email` | корректный адрес электронной почты | - | `URL` | абсолютный URL | - | `Pattern` | соответствует регулярному выражению | `string` -| `PatternInsensitive` | как `Pattern`, но не зависит от регистра | `string` -| `Integer` | целочисленное значение | - -| `Numeric` | псевдоним для `Integer` | - +| `PatternInsensitive` | как `Pattern`, но без учёта регистра | `string` +| `Integer` | целое число | - +| `Numeric` | неотрицательное целое число (только цифры) | - | `Float` | число | - | `Min` | минимальное значение числового элемента | `int\|float` | `Max` | максимальное значение числового элемента | `int\|float` | `Range` | значение в диапазоне | пара `[int\|float, int\|float]` -Правила валидации `Integer`, `Numeric` и `Float` сразу преобразуют значение в integer или float соответственно. А правило `URL` также принимает адрес без схемы (например, `nette.org`) и дополняет схему (`https://nette.org`). Выражение в `Pattern` и `PatternIcase` должно соответствовать всему значению, т.е. как если бы оно было обернуто символами `^` и `$`. +Правила проверки `Integer` и `Float` автоматически преобразуют значение соответственно в целое или дробное число. Кроме того, правило `URL` принимает и адрес без схемы (например, `nette.org`) и схему дополняет (`https://nette.org`). Выражение в `Pattern` и `PatternInsensitive` должно быть верным для всего значения, то есть как если бы оно было обёрнуто в символы `^` и `$`. Количество элементов -------------------- -Для элементов `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` можно также использовать следующие правила для ограничения количества выбранных элементов или загруженных файлов: +Для элементов `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` можно использовать и следующие правила, ограничивающие количество выбранных пунктов или загруженных файлов: | `MinLength` | минимальное количество | `int` | `MaxLength` | максимальное количество | `int` -| `Length` | количество в диапазоне или точное количество | пара `[int, int]` или `int` +| `Length` | количество в диапазоне или точное количество | пара `[int, int]` либо `int` Загрузка файлов --------------- -Для элементов `addUpload()`, `addMultiUpload()` можно также использовать следующие правила: +Для элементов `addUpload()`, `addMultiUpload()` можно использовать ещё и следующие правила: | `MaxFileSize` | максимальный размер файла в байтах | `int` -| `MimeType` | MIME-тип, разрешены плейсхолдеры (`'video/*'`) | `string\|string[]` +| `MimeType` | MIME-тип, допускаются подстановочные знаки (`'video/*'`) | `string\|string[]` | `Image` | изображение JPEG, PNG, GIF, WebP, AVIF | - | `Pattern` | имя файла соответствует регулярному выражению | `string` -| `PatternInsensitive` | как `Pattern`, но не зависит от регистра | `string` +| `PatternInsensitive` | как `Pattern`, но без учёта регистра | `string` -`MimeType` и `Image` требуют PHP-расширения `fileinfo`. То, что файл или изображение имеет требуемый тип, определяется на основе его сигнатуры, и **не проверяется целостность всего файла.** Не повреждено ли изображение, можно узнать, например, попытавшись его [загрузить |http:request#toImage]. +`MimeType` и `Image` требуют PHP-расширения `fileinfo`. То, является ли файл или изображение нужным типом, определяется по его сигнатуре, а **целостность всего файла не проверяется**. Выяснить, не повреждено ли изображение, можно, например, попыткой [его загрузить |http:request#toImage()]. Сообщения об ошибках ==================== -Все предопределенные правила, за исключением `Pattern` и `PatternInsensitive`, имеют сообщение об ошибке по умолчанию, поэтому его можно опустить. Однако, указав и сформулировав все сообщения индивидуально, вы сделаете форму более удобной для пользователя. +У всех заранее определённых правил, кроме `Pattern` и `PatternInsensitive`, есть стандартное сообщение об ошибке, поэтому его можно опустить. Однако, задав и сформулировав все собственные сообщения под свои нужды, вы сделаете форму дружелюбнее к пользователю. -Изменить сообщения по умолчанию можно в [конфигурации |forms:configuration], отредактировав тексты в массиве `Nette\Forms\Validator::$messages` или используя [переводчик |rendering#Перевод]. +Стандартные сообщения можно изменить в [конфигурации |forms:configuration], правкой текстов в массиве `Nette\Forms\Validator::$messages` или с помощью [переводчика |rendering#Перевод]. -В тексте сообщений об ошибках можно использовать следующие заполнители (placeholders): +В тексте сообщений об ошибках можно использовать следующие подстановки: -| `%d` | заменяется последовательно аргументами правила +| `%d` | последовательно заменяется аргументами правила | `%n$d` | заменяется n-м аргументом правила -| `%label` | заменяется меткой (label) элемента (без двоеточия) +| `%label` | заменяется меткой элемента (без двоеточия) | `%name` | заменяется именем элемента (например, `name`) -| `%value` | заменяется значением, введенным пользователем +| `%value` | заменяется значением, которое ввёл пользователь ```php $form->addText('name', 'Имя:') - ->setRequired('Пожалуйста, заполните %label'); + ->setRequired('Заполните, пожалуйста, %label'); $form->addInteger('id', 'ID:') ->addRule($form::Range, 'не менее %d и не более %d', [5, 10]); @@ -115,65 +115,73 @@ $form->addInteger('id', 'ID:') Условия ======= -Помимо правил, можно добавлять также условия. Они записываются аналогично правилам, только вместо `addRule()` используется метод `addCondition()`, и, разумеется, не указывается никакого сообщения об ошибке (условие только спрашивает): +Кроме правил можно добавлять и условия. Записываются они похоже на правила, но вместо `addRule()` мы используем метод `addCondition()` и, естественно, не указываем сообщение об ошибке (условие только спрашивает): ```php $form->addPassword('password', 'Пароль:') - // если пароль не длиннее 8 символов + // если длина пароля не больше 8 ->addCondition($form::MaxLength, 8) // то он должен содержать цифру ->addRule($form::Pattern, 'Должен содержать цифру', '.*[0-9].*'); ``` -Условие можно привязать и к другому элементу, отличному от текущего, с помощью `addConditionOn()`. В качестве первого параметра указывается ссылка на элемент. В этом примере e-mail будет обязательным только тогда, когда установлен флажок (его значение будет true): +Условие можно привязать не к текущему, а к другому элементу с помощью `addConditionOn()`. Первый параметр - ссылка на элемент. В этом примере email будет обязателен, только если отмечен флажок (то есть его значение true): ```php -$form->addCheckbox('newsletters', 'присылайте мне рассылки'); +$form->addCheckbox('newsletters', 'Присылать мне новости'); -$form->addEmail('email', 'E-mail:') - // если флажок установлен +$form->addEmail('email', 'Email:') + // если флажок отмечен ->addConditionOn($form['newsletters'], $form::Equal, true) - // то требуй e-mail - ->setRequired('Введите адрес электронной почты'); + // то требовать email + ->setRequired('Введите ваш адрес электронной почты'); ``` -Из условий можно создавать сложные структуры с помощью `elseCondition()` и `endCondition()`: +Из условий можно строить сложные конструкции с помощью `elseCondition()` и `endCondition()`: ```php $form->addText(/* ... */) ->addCondition(/* ... */) // если выполнено первое условие - ->addConditionOn(/* ... */) // и второе условие на другом элементе - ->addRule(/* ... */) // требуй это правило + ->addConditionOn(/* ... */) // и выполнено второе условие на другом элементе + ->addRule(/* ... */) // требовать это правило ->elseCondition() // если второе условие не выполнено - ->addRule(/* ... */) // требуй эти правила + ->addRule(/* ... */) // требовать эти правила ->addRule(/* ... */) ->endCondition() // возвращаемся к первому условию ->addRule(/* ... */); ``` -В Nette можно очень легко реагировать на выполнение или невыполнение условия и на стороне JavaScript с помощью метода `toggle()`, см. [#динамический JavaScript]. +Первым аргументом `addCondition()` может быть и логическое значение. Это удобно, когда решение известно уже при построении формы, например чтобы применить правило только при определённых обстоятельствах: + +```php +$form->addText('nickname') + ->addCondition($isRequired) // значение, известное при построении формы + ->setRequired(); +``` + +В Nette очень легко откликаться на выполнение или невыполнение условия на стороне JavaScript методом `toggle()`, см. [#Динамический JavaScript]. Ссылка на другой элемент ======================== -В качестве аргумента правила или условия можно передать и другой элемент формы. Правило тогда будет использовать значение, введенное пользователем позже в браузере. Таким образом можно, например, динамически валидировать, что элемент `password` содержит ту же строку, что и элемент `password_confirm`: +Аргументом правила или условия может быть и другой элемент формы. Правило тогда будет использовать значение, которое пользователь позже введёт в браузере. Это можно использовать, например, для динамической проверки того, что элемент `password` содержит ту же строку, что и элемент `password_confirm`: ```php $form->addPassword('password', 'Пароль'); $form->addPassword('password_confirm', 'Подтвердите пароль') - ->addRule($form::Equal, 'Введенные пароли не совпадают', $form['password']); + ->addRule($form::Equal, 'Пароли не совпадают', $form['password']); ``` -Пользовательские правила и условия -================================== +Собственные правила и условия +============================= -Иногда мы попадаем в ситуацию, когда встроенных правил валидации в Nette недостаточно, и нам нужно валидировать данные от пользователя по-своему. В Nette это очень просто! +Иногда мы сталкиваемся с ситуациями, когда встроенных в Nette правил проверки недостаточно и нам нужно проверить данные пользователя по-своему. В Nette это очень просто! -Методам `addRule()` или `addCondition()` можно в качестве первого параметра передать любой callback. Он принимает в качестве первого параметра сам элемент и возвращает булево значение, определяющее, прошла ли валидация успешно. При добавлении правила с помощью `addRule()` можно указать и другие аргументы, они затем передаются в качестве второго параметра. +Методам `addRule()` и `addCondition()` можно передать первым параметром любой callback. Callback принимает первым параметром сам элемент и возвращает логическое значение - удалась ли проверка. При добавлении правила методом `addRule()` можно указать дополнительные аргументы, которые затем передаются вторым параметром. -Таким образом, мы можем создать собственный набор валидаторов как класс со статическими методами: +Собственный набор валидаторов можно, стало быть, создать как класс со статическими методами: ```php class MyValidators @@ -191,18 +199,18 @@ class MyValidators } ``` -Использование тогда очень простое: +Использование тогда совсем простое: ```php $form->addInteger('num') ->addRule( [MyValidators::class, 'validateDivisibility'], - 'Значение должно быть кратно числу %d', + 'Значение должно быть кратно %d', 8, ); ``` -Пользовательские правила валидации можно добавлять и в JavaScript. Условием является то, что правило должно быть статическим методом. Его имя для JavaScript-валидатора формируется путем соединения имени класса без обратных слешей `\`, подчеркивания `_` и имени метода. Например, `App\MyValidators::validateDivisibility` запишем как `AppMyValidators_validateDivisibility` и добавим в объект `Nette.validators`: +Собственные правила проверки можно добавить и в JavaScript. Условие - правило должно быть статическим методом. Его имя для JavaScript-валидатора складывается из имени класса без обратных слешей `\`, символа подчёркивания `_` и имени метода. Например, `App\MyValidators::validateDivisibility` записывается как `AppMyValidators_validateDivisibility` и добавляется в объект `Nette.validators`: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -214,32 +222,32 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => Событие onValidate ================== -После отправки формы выполняется валидация, во время которой проверяются отдельные правила, добавленные с помощью `addRule()`, и затем вызывается [событие |nette:glossary#События Events] `onValidate`. Его обработчик можно использовать для дополнительной валидации, обычно для проверки правильной комбинации значений в нескольких элементах формы. +После отправки формы выполняется проверка, при которой проверяются отдельные правила, добавленные через `addRule()`, а затем вызывается [событие |nette:glossary#События] `onValidate`. Его обработчик можно использовать для дополнительной проверки, обычно для проверки правильного сочетания значений в нескольких элементах формы. -Если обнаружена ошибка, мы передаем ее в форму методом `addError()`. Его можно вызывать либо на конкретном элементе, либо непосредственно на форме. +Если обнаружена ошибка, она передаётся форме методом `addError()`. Вызвать его можно как у конкретного элемента, так и прямо у формы. ```php protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Эта комбинация невозможна.'); + $form->addError('Такое сочетание невозможно.'); } } ``` -Ошибки при обработке -==================== +Обработка ошибок +================ -Во многих случаях об ошибке мы узнаем только в момент обработки валидной формы, например, при записи новой записи в базу данных и столкновении с дубликатом ключей. В таком случае ошибку снова передаем в форму методом `addError()`. Его можно вызывать либо на конкретном элементе, либо непосредственно на форме: +Во многих случаях мы узнаём об ошибке только при обработке корректной формы, например при записи новой строки в базу данных, где натыкаемся на дублирующийся ключ. В таком случае мы снова передаём ошибку обратно в форму методом `addError()`. Вызвать его можно как у конкретного элемента, так и прямо у формы: ```php try { @@ -254,53 +262,53 @@ try { } ``` -Если это возможно, рекомендуем прикреплять ошибку непосредственно к элементу формы, так как она будет отображаться рядом с ним при использовании рендерера по умолчанию. +Если это возможно, мы рекомендуем добавлять ошибку прямо элементу формы, потому что при использовании стандартного отрисовщика она отобразится рядом с ним. ```php -$form['date']->addError('Извините, но эта дата уже занята.'); +$form['date']->addError('Извините, эта дата уже занята.'); ``` -Вы можете вызывать `addError()` повторно и таким образом передать форме или элементу несколько сообщений об ошибках. Их можно получить с помощью `getErrors()`. +Метод `addError()` можно вызывать многократно и передать форме или элементу несколько сообщений об ошибках. Получить их можно методом `getErrors()`. -Внимание, `$form->getErrors()` возвращает сводку всех сообщений об ошибках, включая те, что были переданы непосредственно отдельным элементам, а не только самой форме. Сообщения об ошибках, переданные только форме, можно получить через `$form->getOwnErrors()`. +Обратите внимание, что `$form->getErrors()` возвращает сводку всех сообщений об ошибках, в том числе переданных прямо отдельным элементам, а не только переданных самой форме. Сообщения об ошибках, переданные только форме, можно получить через `$form->getOwnErrors()`. -Изменение ввода -=============== +Изменение введённых значений +============================ -С помощью метода `addFilter()` мы можем изменить значение, введенное пользователем. В этом примере мы будем допускать и удалять пробелы в почтовом индексе: +Методом `addFilter()` мы можем изменить значение, введённое пользователем. В этом примере мы будем допускать и удалять пробелы в почтовом индексе: ```php $form->addText('zip', 'Почтовый индекс:') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // удалим пробелы из почтового индекса + return str_replace(' ', '', $value); // убираем пробелы из индекса }) - ->addRule($form::Pattern, 'Почтовый индекс не в формате пяти цифр', '\d{5}'); + ->addRule($form::Pattern, 'Почтовый индекс - это не пять цифр', '\d{5}'); ``` -Фильтр встраивается между правилами валидации и условиями, поэтому порядок методов имеет значение, т.е. фильтр и правило вызываются в том порядке, в каком указаны методы `addFilter()` и `addRule()`. +Фильтр встраивается в ряд правил проверки и условий, а значит порядок методов имеет значение: фильтр и правило вызываются в том же порядке, в каком записаны методы `addFilter()` и `addRule()`. -JavaScript-валидация -==================== +Проверка на JavaScript +====================== -Язык для формулирования условий и правил очень мощный. Все конструкции при этом работают как на стороне сервера, так и на стороне JavaScript. Они передаются в HTML-атрибутах `data-nette-rules` в формате JSON. Саму валидацию затем выполняет скрипт, который перехватывает событие формы `submit`, проходит по отдельным элементам и выполняет соответствующую валидацию. +Язык для формулирования условий и правил очень мощный. Все конструкции работают и на стороне сервера, и на стороне клиента в JavaScript. Передаются они в HTML-атрибутах `data-nette-rules` в виде JSON. Саму проверку выполняет скрипт, который перехватывает событие формы `submit`, обходит отдельные элементы и выполняет соответствующую проверку. -Этим скриптом является `netteForms.js`, и он доступен из нескольких возможных источников: +Этот скрипт называется `netteForms.js`, и он доступен из нескольких возможных источников: -Скрипт можно вставить непосредственно в HTML-страницу из CDN: +Скрипт можно вставить прямо в HTML-страницу из CDN: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Или скопировать локально в публичную папку проекта (например, из `vendor/nette/forms/src/assets/netteForms.min.js`): +Либо скопировать локально в публичную папку проекта (например, из `vendor/nette/forms/src/assets/netteForms.min.js`): ```latte <script src="/path/to/netteForms.min.js"></script> ``` -Или установить через [npm |https://www.npmjs.com/package/nette-forms]: +Либо установить через [npm |https://www.npmjs.com/package/nette-forms]: ```shell npm install nette-forms @@ -313,18 +321,24 @@ import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -Альтернативно его можно загрузить прямо из папки `vendor`: +Как вариант, его можно загрузить прямо из папки `vendor`: ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Проверку на стороне клиента можно полностью отключить, добавив форме атрибут `novalidate`. Скрипт `netteForms.js` тогда пропустит её проверку при отправке, и проверка произойдёт только на сервере: + +```php +$form->setHtmlAttribute('novalidate'); +``` + Динамический JavaScript ======================= -Хотите отображать поля для ввода адреса только если пользователь выберет доставку товара почтой? Нет проблем. Ключ — это пара методов `addCondition()` & `toggle()`: +Хотите показывать поля адреса, только если пользователь выберет доставку товара почтой? Не проблема. Ключ к этому - пара методов `addCondition()` и `toggle()`: ```php $form->addCheckbox('send_it') @@ -332,25 +346,25 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -Этот код говорит, что когда условие выполнено, то есть когда флажок установлен, будет виден HTML-элемент `#address-container`. И наоборот. Элементы формы с адресом получателя мы разместим в контейнере с этим ID, и при клике на флажок они будут скрываться или отображаться. Это обеспечивает скрипт `netteForms.js`. +Этот код говорит, что при выполнении условия (то есть когда флажок отмечен) HTML-элемент `#address-container` будет виден, и наоборот. Значит, элементы формы с адресом получателя мы поместим в контейнер с этим идентификатором, и они будут скрываться или показываться по щелчку на флажке. Об этом заботится скрипт `netteForms.js`. -В качестве аргумента метода `toggle()` можно передать любой селектор. По историческим причинам буквенно-цифровая строка без других специальных символов понимается как ID элемента, то есть так же, как если бы ей предшествовал символ `#`. Второй необязательный параметр позволяет инвертировать поведение, т.е. если бы мы использовали `toggle('#address-container', false)`, элемент, наоборот, отображался бы только тогда, когда флажок не был бы установлен. +Аргументом метода `toggle()` может быть любой селектор. По историческим причинам строка, начинающаяся с буквы, цифры или подчёркивания и содержащая только буквы, цифры, подчёркивания, дефисы, точки и двоеточия, считается идентификатором элемента, как если бы перед ней стоял символ `#`. Второй необязательный параметр позволяет обратить поведение: например, если бы мы использовали `toggle('#address-container', false)`, элемент отображался бы, только если флажок *не* отмечен. -Реализация по умолчанию в JavaScript изменяет свойство `hidden` элементов. Однако поведение можно легко изменить, например, добавить анимацию. Достаточно в JavaScript переопределить метод `Nette.toggle` собственным решением: +Стандартная реализация на JavaScript меняет свойство `hidden` элементов. Однако поведение можно легко изменить, например добавить анимацию. Достаточно переопределить в JavaScript метод `Nette.toggle` собственным решением: ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // скроем или покажем 'el' в зависимости от значения 'visible' + // скрываем или показываем 'el' в зависимости от значения 'visible' }); }; ``` -Отключение валидации -==================== +Отключение проверки +=================== -Иногда может потребоваться отключить валидацию. Если нажатие кнопки отправки не должно выполнять валидацию (подходит для кнопок *Cancel* или *Preview*), мы отключаем ее методом `$submit->setValidationScope([])`. Если она должна выполнять только частичную валидацию, мы можем указать, какие поля или контейнеры формы должны валидироваться. +Иногда проверку бывает полезно отключить. Если нажатие кнопки отправки не должно выполнять проверку (подходит для кнопок *Отмена* или *Предпросмотр*), мы отключаем её методом `$submit->setValidationScope([])`. Если она должна выполнять только частичную проверку, можно указать, какие поля или контейнеры формы нужно проверять. ```php $form->addText('name') @@ -362,15 +376,17 @@ $details->addInteger('age') $details->addInteger('age2') ->setRequired('age2'); -$form->addSubmit('send1'); // Валидирует всю форму +$form->addSubmit('send1'); // Проверяет всю форму $form->addSubmit('send2') - ->setValidationScope([]); // Не валидирует вообще + ->setValidationScope([]); // Не проверяет ничего $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Валидирует только элемент name + ->setValidationScope([$form['name']]); // Проверяет только элемент 'name' $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Валидирует только элемент age + ->setValidationScope([$form['details']['age']]); // Проверяет только элемент 'age' $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Валидирует контейнер details + ->setValidationScope([$form['details']]); // Проверяет контейнер 'details' ``` -`setValidationScope` не влияет на [#событие onValidate] у формы, которое будет вызвано всегда. Событие `onValidate` у контейнера будет вызвано только если этот контейнер помечен для частичной валидации. +`setValidationScope` не влияет на [#Событие onValidate] у формы, которое будет вызвано всегда. Событие `onValidate` у контейнера будет вызвано, только если этот контейнер помечен для частичной проверки. + +Частичная проверка влияет и на значения, возвращаемые методом `getValues()`: результат содержит только значения элементов, попадающих в область проверки. Значения элементов вне этой области опускаются. diff --git a/forms/sl/@home.texy b/forms/sl/@home.texy deleted file mode 100644 index b71b6a213f..0000000000 --- a/forms/sl/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms so prinesli revolucijo v ustvarjanje spletnih obrazcev. Naenkrat je bilo dovolj napisati nekaj razumljivih vrstic kode in imeli ste pripravljen obrazec, vključno z izrisovanjem, JavaScript in strežniško validacijo ter poleg tega vrhunsko zaščiten. Pokazali bomo, kako - -- ustvarjati prijazne obrazce -- validirati poslane podatke -- izrisovati elemente natančno po potrebi - -</div> - - -Z uporabo Nette Forms se izognete celi vrsti rutinskih nalog, kot je na primer pisanje validacije (poleg tega dvojne, na strani strežnika in odjemalca), minimizirate verjetnost nastanka napak in varnostnih lukenj. - -Obrazce lahko uporabljate bodisi kot del Nette Aplikacije (torej v presenterjih), bodisi popolnoma samostojno. Ker se v obeh primerih uporaba nekoliko razlikuje, smo za vas pripravili dva navodila: - -<div class="wiki-buttons"> -<div> "Obrazci v presenterjih .[wiki-button]":in-presenter </div> -<div> "Obrazci samostojno .[wiki-button]":standalone </div> -</div> - - -Namestitev ----------- - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/sl/@left-menu.texy b/forms/sl/@left-menu.texy deleted file mode 100644 index 10d9d69543..0000000000 --- a/forms/sl/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Uvod |@home] -- [Obrazci v presenterjih|in-presenter] -- [Obrazci samostojno|standalone] -- [Elementi obrazca |controls] -- [Validacija |validation] -- [Izrisovanje |rendering] -- [Konfiguracija |configuration] - - -Nadaljnje branje -**************** -- [Navodila in postopki |best-practices:] diff --git a/forms/sl/@meta.texy b/forms/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/forms/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/forms/sl/configuration.texy b/forms/sl/configuration.texy deleted file mode 100644 index 6ca404d63c..0000000000 --- a/forms/sl/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Konfiguracija obrazcev -********************** - -.[perex] -V konfiguraciji lahko spremenite privzeta [sporočila o napakah obrazcev|validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Tukaj je slovenski prevod: - -```neon -forms: - messages: - Equal: 'Vnesite %s.' - NotEqual: 'Ta vrednost ne sme biti %s.' - Filled: 'To polje je obvezno.' - Blank: 'To polje mora biti prazno.' - MinLength: 'Vnesite vsaj %d znakov.' - MaxLength: 'Vnesite največ %d znakov.' - Length: 'Vnesite vrednost dolžine med %d in %d znakov.' - Email: 'Vnesite veljaven e-poštni naslov.' - URL: 'Vnesite veljaven URL.' - Integer: 'Vnesite veljavno celo število.' - Float: 'Vnesite veljavno število.' - Min: 'Vnesite vrednost večjo ali enako %d.' - Max: 'Vnesite vrednost manjšo ali enako %d.' - Range: 'Vnesite vrednost med %d in %d.' - MaxFileSize: 'Velikost naložene datoteke je lahko največ %d bajtov.' - MaxPostSize: 'Naloženi podatki presegajo omejitev %d bajtov.' - MimeType: 'Naložena datoteka ni v pričakovanem formatu.' - Image: 'Naložena datoteka mora biti slika v formatu JPEG, GIF, PNG, WebP ali AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Izberite veljavno možnost.' - Nette\Forms\Controls\UploadControl::Valid: 'Pri nalaganju datoteke je prišlo do napake.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Vaša seja je potekla. Vrnite se na domačo stran in poskusite znova.' -``` - -Če ne uporabljate celotnega ogrodja in torej niti konfiguracijskih datotek, lahko spremenite privzeta sporočila o napakah neposredno v polju `Nette\Forms\Validator::$messages`. diff --git a/forms/sl/controls.texy b/forms/sl/controls.texy deleted file mode 100644 index b03b9e0978..0000000000 --- a/forms/sl/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Elementi obrazca -**************** - -.[perex] -Pregled standardnih elementov obrazca. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Doda enovrstično besedilno polje (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Če uporabnik polja ne izpolni, vrne prazen niz `''`, ali pa s pomočjo `setNullable()` lahko določite, da vrne `null`. - -```php -$form->addText('name', 'Ime:') - ->setRequired() - ->setNullable(); -``` - -Samodejno validira UTF-8, obreže leve in desne presledke ter odstrani prelome vrstic, ki bi jih lahko poslal napadalec. - -Maksimalno dolžino lahko omejite s pomočjo `setMaxLength()`. Spreminjanje vrednosti, ki jo vnese uporabnik, omogoča [addFilter() |validation#Spreminjanje vnosa]. - -S pomočjo `setHtmlType()` lahko spremenite vizualni značaj besedilnega polja na tipe, kot so `search`, `tel` ali `url`, glej [specifikacijo|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Ne pozabite, da je sprememba tipa le vizualna in ne nadomešča funkcije validacije. Za tip `url` je priporočljivo dodati specifično validacijsko [pravilo URL |validation#Besedilni vnosi]. - -.[note] -Za druge tipe vnosov, kot so `number`, `range`, `email`, `date`, `datetime-local`, `time` in `color`, uporabite specializirane metode, kot so [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] in [#addColor], ki zagotavljajo strežniško validacijo. Tipi `month` in `week` še niso popolnoma podprti v vseh brskalnikih. - -Elementu lahko nastavite t.i. empty-value, kar je nekaj podobnega privzeti vrednosti, a če je uporabnik ne spremeni, element vrne prazen niz ali `null`. - -```php -$form->addText('phone', 'Telefon:') - ->setHtmlType('tel') - ->setEmptyValue('+386'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Doda polje za vnos večvrstičnega besedila (razred [TextArea |api:Nette\Forms\Controls\TextArea]). Če uporabnik polja ne izpolni, vrne prazen niz `''`, ali pa s pomočjo `setNullable()` lahko določite, da vrne `null`. - -```php -$form->addTextArea('note', 'Opomba:') - ->addRule($form::MaxLength, 'Opomba je predolga', 10000); -``` - -Samodejno validira UTF-8 in normalizira ločila vrstic na `\n`. V nasprotju z enovrstičnim vnosnim poljem ne pride do obrezovanja presledkov. - -Maksimalno dolžino lahko omejite s pomočjo `setMaxLength()`. Spreminjanje vrednosti, ki jo vnese uporabnik, omogoča [addFilter() |validation#Spreminjanje vnosa]. Lahko nastavite t.i. empty-value s pomočjo `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Doda polje za vnos celega števila (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Vrne bodisi integer ali `null`, če uporabnik ničesar ne vnese. - -```php -$form->addInteger('year', 'Leto:') - ->addRule($form::Range, 'Leto mora biti v obsegu od %d do %d.', [1900, 2023]); -``` - -Element se izriše kot `<input type="number">`. Z uporabo metode `setHtmlType()` lahko spremenite tip na `range` za prikaz v obliki drsnika ali na `text`, če preferirate standardno besedilno polje brez posebnega obnašanja tipa `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Doda polje za vnos decimalnega števila (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Vrne bodisi float ali `null`, če uporabnik ničesar ne vnese. - -```php -$form->addFloat('level', 'Raven:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Raven mora biti v obsegu od %d do %d.', [0, 100]); -``` - -Element se izriše kot `<input type="number">`. Z uporabo metode `setHtmlType()` lahko spremenite tip na `range` za prikaz v obliki drsnika ali na `text`, če preferirate standardno besedilno polje brez posebnega obnašanja tipa `number`. - -Nette in brskalnik Chrome sprejemata kot ločilo decimalnih mest tako vejico kot piko. Da bi bila ta funkcionalnost na voljo tudi v Firefoxu, je priporočljivo nastaviti atribut `lang` bodisi za dani element ali za celotno stran, na primer `<html lang="sl">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Doda polje za vnos e-poštnega naslova (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Če uporabnik polja ne izpolni, vrne prazen niz `''`, ali pa s pomočjo `setNullable()` lahko določite, da vrne `null`. - -```php -$form->addEmail('email', 'E-pošta:'); -``` - -Preveri, ali je vrednost veljaven e-poštni naslov. Ne preverja se, ali domena dejansko obstaja, preverja se le sintaksa. Samodejno validira UTF-8, obreže leve in desne presledke. - -Maksimalno dolžino lahko omejite s pomočjo `setMaxLength()`. Spreminjanje vrednosti, ki jo vnese uporabnik, omogoča [addFilter() |validation#Spreminjanje vnosa]. Lahko nastavite t.i. empty-value s pomočjo `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Doda polje za vnos gesla (razred [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Geslo:') - ->setRequired() - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8) - ->addRule($form::Pattern, 'Mora vsebovati števko', '.*[0-9].*'); -``` - -Pri ponovnem prikazu obrazca bo polje prazno. Samodejno validira UTF-8, obreže leve in desne presledke ter odstrani prelome vrstic, ki bi jih lahko poslal napadalec. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Doda potrditveno polje (razred [Checkbox |api:Nette\Forms\Controls\Checkbox]). Vrne vrednost bodisi `true` ali `false`, glede na to, ali je označeno. - -```php -$form->addCheckbox('agree', 'Strinjam se s pogoji') - ->setRequired('Potrebno je strinjanje s pogoji'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Doda potrditvena polja za izbiro več postavk (razred [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Vrne polje ključev izbranih postavk. Metoda `getSelectedItems()` vrne vrednosti namesto ključev. - -```php -$form->addCheckboxList('colors', 'Barve:', [ - 'r' => 'rdeča', - 'g' => 'zelena', - 'b' => 'modra', -]); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. - -S pomočjo `setDisabled(['r', 'g'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da so izbrane postavke dejansko ene izmed ponujenih in niso bile deaktivirane. Z metodo `getRawValue()` lahko pridobite poslane postavke brez tega pomembnega preverjanja. - -Pri nastavitvi privzetih izbranih postavk tudi preverja, da gre za ene izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - -Če pošiljate obrazec z metodo `GET`, lahko izberete kompaktnejši način prenosa podatkov, ki prihrani velikost poizvedbenega niza (query string). Aktivira se z nastavitvijo HTML atributa obrazca: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Doda izbirne gumbe (radio buttons) (razred [RadioList |api:Nette\Forms\Controls\RadioList]). Vrne ključ izbrane postavke ali `null`, če uporabnik ničesar ni izbral. Metoda `getSelectedItem()` vrne vrednost namesto ključa. - -```php -$sex = [ - 'm' => 'moški', - 'f' => 'ženska', -]; -$form->addRadioList('gender', 'Spol:', $sex); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. - -S pomočjo `setDisabled(['m', 'f'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da je izbrana postavka dejansko ena izmed ponujenih in ni bila deaktivirana. Z metodo `getRawValue()` lahko pridobite poslano postavko brez tega pomembnega preverjanja. - -Pri nastavitvi privzete izbrane postavke tudi preverja, da gre za eno izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Doda izbirno polje (select box) (razred [SelectBox |api:Nette\Forms\Controls\SelectBox]). Vrne ključ izbrane postavke ali `null`, če uporabnik ničesar ni izbral. Metoda `getSelectedItem()` vrne vrednost namesto ključa. - -```php -$countries = [ - 'CZ' => 'Češka Republika', - 'SK' => 'Slovaška', - 'GB' => 'Velika Britanija', -]; - -$form->addSelect('country', 'Država:', $countries) - ->setDefaultValue('SK'); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. Postavke so lahko tudi dvodimenzionalno polje: - -```php -$countries = [ - 'Europe' => [ - 'CZ' => 'Češka Republika', - 'SK' => 'Slovaška', - 'GB' => 'Velika Britanija', - ], - 'CA' => 'Kanada', - 'US' => 'ZDA', - '?' => 'druga', -]; -``` - -Pri izbirnih poljih ima pogosto prva postavka poseben pomen, služi kot poziv k akciji. Za dodajanje takšne postavke služi metoda `setPrompt()`. - -```php -$form->addSelect('country', 'Država:', $countries) - ->setPrompt('Izberite državo'); -``` - -S pomočjo `setDisabled(['CZ', 'SK'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da je izbrana postavka dejansko ena izmed ponujenih in ni bila deaktivirana. Z metodo `getRawValue()` lahko pridobite poslano postavko brez tega pomembnega preverjanja. - -Pri nastavitvi privzete izbrane postavke tudi preverja, da gre za eno izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Doda izbirno polje za izbiro več postavk (razred [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Vrne polje ključev izbranih postavk. Metoda `getSelectedItems()` vrne vrednosti namesto ključev. - -```php -$form->addMultiSelect('countries', 'Države:', $countries); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. Postavke so lahko tudi dvodimenzionalno polje. - -S pomočjo `setDisabled(['CZ', 'SK'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da so izbrane postavke dejansko ene izmed ponujenih in niso bile deaktivirane. Z metodo `getRawValue()` lahko pridobite poslane postavke brez tega pomembnega preverjanja. - -Pri nastavitvi privzetih izbranih postavk tudi preverja, da gre za ene izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Doda polje za nalaganje datoteke (razred [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrne objekt [FileUpload |http:request#FileUpload], in to tudi v primeru, da uporabnik nobene datoteke ni poslal, kar lahko ugotovite z metodo `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Avatar mora biti JPEG, PNG, GIF, WebP ali AVIF.') - ->addRule($form::MaxFileSize, 'Maksimalna velikost je 1 MB.', 1024 * 1024); -``` - -Če se datoteka ne uspe pravilno naložiti, obrazec ni uspešno poslan in prikaže se napaka. Tj. pri uspešni oddaji ni treba preverjati metode `FileUpload::isOk()`. - -Nikoli ne zaupajte originalnemu imenu datoteke, vrnjenemu z metodo `FileUpload::getName()`, klient bi lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - -Pravili `MimeType` in `Image` zaznata zahtevani tip na podlagi signature datoteke in ne preverjata njene integritete. Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganja |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Doda polje za nalaganje več datotek hkrati (razred [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrne polje objektov [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` pri vsakem od njih bo vračala `true`. - -```php -$form->addMultiUpload('files', 'Datoteke:') - ->addRule($form::MaxLength, 'Maksimalno lahko naložite %d datotek', 10); -``` - -Če se katera koli datoteka ne uspe pravilno naložiti, obrazec ni uspešno poslan in prikaže se napaka. Tj. pri uspešni oddaji ni treba preverjati metode `FileUpload::isOk()`. - -Nikoli ne zaupajte originalnim imenom datotek, vrnjenim z metodo `FileUpload::getName()`, klient bi lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - -Pravili `MimeType` in `Image` zaznata zahtevani tip na podlagi signature datoteke in ne preverjata njene integritete. Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganja |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Doda polje, ki uporabniku omogoča enostaven vnos datuma, sestavljenega iz leta, meseca in dneva (razred [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Kot privzeto vrednost sprejema bodisi objekte, ki implementirajo vmesnik `DateTimeInterface`, niz s časom ali število, ki predstavlja UNIX časovni žig. Enako velja za argumente pravil `Min`, `Max` ali `Range`, ki definirajo minimalni in maksimalni dovoljeni datum. - -```php -$form->addDate('date', 'Datum:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Datum mora biti vsaj en mesec star.', new DateTime('-1 month')); -``` - -Standardno vrne objekt `DateTimeImmutable`, z metodo `setFormat()` lahko specificirate [besedilni format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ali časovni žig: - -```php -$form->addDate('date', 'Datum:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Doda polje, ki uporabniku omogoča enostaven vnos časa, sestavljenega iz ur, minut in opcijsko tudi sekund (razred [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Kot privzeto vrednost sprejema bodisi objekte, ki implementirajo vmesnik `DateTimeInterface`, niz s časom ali število, ki predstavlja UNIX časovni žig. Iz teh vnosov je uporabljena le časovna informacija, datum je ignoriran. Enako velja za argumente pravil `Min`, `Max` ali `Range`, ki definirajo minimalni in maksimalni dovoljeni čas. Če je nastavljena minimalna vrednost višja od maksimalne, se ustvari časovni obseg, ki presega polnoč. - -```php -$form->addTime('time', 'Čas:', withSeconds: true) - ->addRule($form::Range, 'Čas mora biti v obsegu od %d do %d.', ['12:30', '13:30']); -``` - -Standardno vrne objekt `DateTimeImmutable` (z datumom 1. januarja leta 1), z metodo `setFormat()` lahko specificirate [besedilni format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Čas:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Doda polje, ki uporabniku omogoča enostaven vnos datuma in časa, sestavljenega iz leta, meseca, dneva, ur, minut in opcijsko tudi sekund (razred [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Kot privzeto vrednost sprejema bodisi objekte, ki implementirajo vmesnik `DateTimeInterface`, niz s časom ali število, ki predstavlja UNIX časovni žig. Enako velja za argumente pravil `Min`, `Max` ali `Range`, ki definirajo minimalni in maksimalni dovoljeni datum. - -```php -$form->addDateTime('datetime', 'Datum in čas:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Datum mora biti vsaj en mesec star.', new DateTime('-1 month')); -``` - -Standardno vrne objekt `DateTimeImmutable`, z metodo `setFormat()` lahko specificirate [besedilni format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ali časovni žig: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Doda polje za izbiro barve (razred [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Barva je niz v obliki `#rrggbb`. Če uporabnik izbire ne opravi, se vrne črna barva `#000000`. - -```php -$form->addColor('color', 'Barva:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Doda skrito polje (razred [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -S pomočjo `setNullable()` lahko nastavite, da vrne `null` namesto praznega niza. Spreminjanje poslane vrednosti omogoča [addFilter() |validation#Spreminjanje vnosa]. - -Čeprav je element skrit, je **pomembno se zavedati**, da lahko vrednost še vedno spremeni ali ponaredi napadalec. Vedno temeljito preverjajte in validirajte vse prejete vrednosti na strežniški strani, da preprečite varnostna tveganja, povezana z manipulacijo podatkov. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Doda gumb za oddajo (razred [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Pošlji'); -``` - -V obrazcu je mogoče imeti tudi več gumbov za oddajo: - -```php -$form->addSubmit('register', 'Registriraj'); -$form->addSubmit('cancel', 'Prekliči'); -``` - -Za ugotovitev, na katerega od njih je bilo kliknjeno, uporabite: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Če ne želite validirati celotnega obrazca ob pritisku na gumb (na primer pri gumbih *Prekliči* ali *Predogled*), uporabite [setValidationScope() |validation#Izklop validacije]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Doda gumb (razred [Button |api:Nette\Forms\Controls\Button]), ki nima funkcije oddaje. Lahko ga torej uporabite za kakšno drugo funkcijo, npr. klic JavaScript funkcije ob kliku. - -```php -$form->addButton('raise', 'Zvišaj plačo') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Doda gumb za oddajo v obliki slike (razred [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/pot/do/slike'); -``` - -Pri uporabi več gumbov za oddajo lahko ugotovite, na katerega je bilo kliknjeno, s pomočjo `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Doda podobrazec (razred [Container|api:Nette\Forms\Container]), ali vsebnik, v katerega lahko dodajate druge elemente na enak način, kot jih dodajamo v obrazec. Delujejo tudi metode `setDefaults()` ali `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Vaše ime:'); -$sub1->addEmail('email', 'E-pošta:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Vaše ime:'); -$sub2->addEmail('email', 'E-pošta:'); -``` - -Poslani podatki se nato vrnejo kot večdimenzionalna struktura: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Pregled nastavitev -================== - -Pri vseh elementih lahko kličemo naslednje metode (popoln pregled v [API dokumentaciji|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | nastavi privzeto vrednost -| `getValue()` | pridobi trenutno vrednost -| `setOmitted()` | [#Izpustitev vrednosti] -| `setDisabled()` | [#Deaktivacija elementov] - -Izrisovanje: -.[table-form-methods language-php] -| `setCaption($caption)` | spremeni oznako elementa -| `setTranslator($translator)` | nastavi [prevajalnik |rendering#Prevajanje] -| `setHtmlAttribute($name, $value)` | nastavi [HTML atribut |rendering#HTML atributi] elementa -| `setHtmlId($id)` | nastavi HTML atribut `id` -| `setHtmlType($type)` | nastavi HTML atribut `type` -| `setHtmlName($name)` | nastavi HTML atribut `name` -| `setOption($key, $value)` | [nastavitve za izrisovanje |rendering#Možnosti Options] - -Validacija: -.[table-form-methods language-php] -| `setRequired()` | [obvezni element |validation] -| `addRule()` | nastavitev [validacijsko pravilo |validation#Pravila] -| `addCondition()`, `addConditionOn()` | nastavi [validacijski pogoj |validation#Pogoji] -| `addError($message)` | [predaja sporočila o napaki |validation#Napake pri obdelavi] - -Pri elementih `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` lahko kličemo naslednje metode: - -.[table-form-methods language-php] -| `setNullable()` | nastavi, ali getValue() vrne `null` namesto praznega niza -| `setEmptyValue($value)` | nastavi posebno vrednost, ki se šteje za prazen niz -| `setMaxLength($length)` | nastavi maksimalno število dovoljenih znakov -| `addFilter($filter)` | [prilagoditev vnosa |validation#Spreminjanje vnosa] - - -Izpustitev vrednosti -==================== - -Če nas vrednost, ki jo je vnesel uporabnik, ne zanima, jo lahko s pomočjo `setOmitted()` izpustimo iz rezultata metode `$form->getValues()` ali iz podatkov, predanih handlerjem. To je koristno za različna gesla za preverjanje, antispam elemente itd. - -```php -$form->addPassword('passwordVerify', 'Geslo za preverjanje:') - ->setRequired('Prosimo, vnesite geslo še enkrat za preverjanje') - ->addRule($form::Equal, 'Gesli se ne ujemata', $form['password']) - ->setOmitted(); -``` - - -Deaktivacija elementov -====================== - -Elemente lahko deaktivirate s pomočjo `setDisabled()`. Takšnega elementa uporabnik ne more urejati. - -```php -$form->addText('username', 'Uporabniško ime:') - ->setDisabled(); -``` - -Onemogočeni elementi brskalnik sploh ne pošilja na strežnik, torej jih niti ne najdete v podatkih, vrnjenih s funkcijo `$form->getValues()`. Če pa nastavite `setOmitted(false)`, Nette v te podatke vključi njihovo privzeto vrednost. - -Pri klicu `setDisabled()` se iz varnostnih razlogov **izbriše vrednost elementa**. Če nastavljate privzeto vrednost, je to treba storiti šele po njegovi deaktivaciji: - -```php -$form->addText('username', 'Uporabniško ime:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Alternativa onemogočenim elementom so elementi s HTML atributom `readonly`, ki jih brskalnik pošilja na strežnik. Čeprav je element samo za branje, je **pomembno se zavedati**, da lahko njegovo vrednost še vedno spremeni ali ponaredi napadalec. - - -Lastni elementi -=============== - -Poleg široke palete vgrajenih elementov obrazca lahko v obrazec dodajate lastne elemente na ta način: - -```php -$form->addComponent(new DateInput('Datum:'), 'date'); -// alternativna sintaksa: $form['date'] = new DateInput('Datum:'); -``` - -.[note] -Obrazec je potomec razreda [Container |component-model:#Container] in posamezni elementi so potomci [Component |component-model:#Component]. - -Obstaja način, kako definirati nove metode obrazca, ki služijo za dodajanje lastnih elementov (npr. `$form->addZip()`). Gre za t.i. extension methods. Slabost je, da zanje ne bo delovalo predlaganje v urejevalnikih. - -```php -use Nette\Forms\Container; - -// dodamo metodo addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Vsaj 5 številk', '[0-9]{5}'); -}); - -// uporaba -$form->addZip('zip', 'Poštna številka:'); -``` - - -Nizkonivojski elementi -====================== - -Lahko uporabljamo tudi elemente, ki jih zapišemo samo v predlogi in jih ne dodamo v obrazec z nobeno od metod `$form->addXyz()`. Ko na primer izpisujemo zapise iz podatkovne baze in vnaprej ne vemo, koliko jih bo in kakšne ID-je bodo imeli, in želimo pri vsaki vrstici prikazati potrditveno polje ali izbirni gumb, ga zadostuje kodirati v predlogi: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -In po oddaji vrednost ugotovimo: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -kjer prvi parameter je tip elementa (`DataFile` za `type=file`, `DataLine` za enovrstične vnose kot `text`, `password`, `email` ipd. in `DataText` za vse ostale) in drugi parameter `sel[]` ustreza HTML atributu name. Tip elementa lahko kombiniramo z vrednostjo `DataKeys`, ki ohrani ključe elementov. To je koristno zlasti za `select`, `radioList` in `checkboxList`. - -Bistveno je, da `getHttpData()` vrne sanirano vrednost, v tem primeru bo to vedno polje veljavnih UTF-8 nizov, ne glede na to, kaj bi poskušal napadalec podtakniti strežniku. Gre za analogijo neposrednega dela z `$_POST` ali `$_GET`, vendar s to bistveno razliko, da vedno vrne čiste podatke, tako kot ste navajeni pri standardnih elementih Nette obrazcev. diff --git a/forms/sl/in-presenter.texy b/forms/sl/in-presenter.texy deleted file mode 100644 index e09ea5be98..0000000000 --- a/forms/sl/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Obrazci v presenterjih -********************** - -.[perex] -Nette Forms bistveno olajšajo ustvarjanje in obdelavo spletnih obrazcev. V tem poglavju se boste seznanili z uporabo obrazcev znotraj presenterjev. - -Če vas zanima, kako jih uporabljati popolnoma samostojno brez preostalega ogrodja, je za vas namenjen vodič za [samostojno uporabo|standalone]. - - -Prvi obrazec -============ - -Poskusimo napisati preprost registracijski obrazec. Njegova koda bo naslednja: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Ime:'); -$form->addPassword('password', 'Geslo:'); -$form->addSubmit('send', 'Registriraj'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -in v brskalniku se bo prikazal takole: - -[* form-cs.webp *] - -Obrazec v presenterju je objekt razreda `Nette\Application\UI\Form`, njegov predhodnik `Nette\Forms\Form` je namenjen samostojni uporabi. Dodali smo mu t.i. elemente ime, geslo in gumb za oddajo. In na koncu vrstica z `$form->onSuccess` pove, da se mora po oddaji in uspešni validaciji poklicati metoda `$this->formSucceeded()`. - -Z vidika presenterja je obrazec običajna komponenta. Zato se z njim kot s komponento ravna in ga vključimo v presenter s pomočjo [tovarne metode |application:components#Tovarniške metode]. Izgledalo bo takole: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Ime:'); - $form->addPassword('password', 'Geslo:'); - $form->addSubmit('send', 'Registriraj'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // tukaj obdelamo podatke, poslane z obrazcem - // $data->name vsebuje ime - // $data->password vsebuje geslo - $this->flashMessage('Uspešno ste bili registrirani.'); - $this->redirect('Home:'); - } -} -``` - -In v predlogi obrazec izrišemo z oznako `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Registracija</h1> - -{control registrationForm} -``` - -In to je pravzaprav vse :-) Imamo delujoč in popolnoma [zavarovan |#Zaščita pred ranljivostmi] obrazec. - -In zdaj si verjetno mislite, da je bilo prehitro, razmišljate, kako je mogoče, da se pokliče metoda `formSucceeded()` in kaj so parametri, ki jih prejme. Seveda, imate prav, to si zasluži pojasnilo. - -Nette namreč prihaja s svežim mehanizmom, ki mu pravimo [Hollywood style |application:components#Hollywood style]. Namesto da bi se kot razvijalec morali nenehno spraševati, ali se je nekaj zgodilo („ali je bil obrazec poslan?“, „ali je bil poslan veljavno?“ in „ali ni prišlo do njegovega ponarejanja?“), poveste ogrodju „ko bo obrazec veljavno izpolnjen, pokliči to metodo“ in prepustite nadaljnje delo njemu. Če programirate v JavaScriptu, ta slog programiranja dobro poznate. Pišete funkcije, ki se kličejo, ko nastopi določen [dogodek |nette:glossary#Dogodki eventi]. In jezik jim preda ustrezne argumente. - -Prav tako je zgrajena tudi zgoraj navedena koda presenterja. Polje `$form->onSuccess` predstavlja seznam PHP povratnih klicev (callbackov), ki jih Nette pokliče v trenutku, ko je obrazec poslan in pravilno izpolnjen (tj. je veljaven). V okviru [življenjskega cikla presenterja |application:presenters#Življenjski cikel presenterja] gre za t.i. signal, kličejo se torej po `action*` metodi in pred `render*` metodo. In vsakemu povratnemu klicu preda kot prvi parameter sam obrazec in kot drugega poslane podatke v obliki objekta [ArrayHash |utils:arrays#ArrayHash]. Prvi parameter lahko izpustite, če objekta obrazca ne potrebujete. In drugi parameter zna biti bolj prebrisan, ampak o tem [kasneje |#Mapiranje na razrede]. - -Objekt `$data` vsebuje ključe `name` in `password` s podatki, ki jih je izpolnil uporabnik. Običajno podatke takoj pošljemo k nadaljnji obdelavi, kar je lahko na primer vstavljanje v podatkovno bazo. Med obdelavo pa se lahko pojavi napaka, na primer uporabniško ime je že zasedeno. V takem primeru napako predamo nazaj v obrazec s pomočjo `addError()` in pustimo, da se ponovno izriše, tudi s sporočilom o napaki. - -```php -$form->addError('Oprostite, uporabniško ime že nekdo uporablja.'); -``` - -Poleg `onSuccess` obstaja še `onSubmit`: povratni klici se kličejo vedno po oddaji obrazca, tudi takrat, ko ni pravilno izpolnjen. In dalje `onError`: povratni klici se kličejo le, če oddaja ni veljavna. Pokličejo se celo takrat, če v `onSuccess` ali `onSubmit` znevalidiramo obrazec s pomočjo `addError()`. - -Po obdelavi obrazca preusmerimo na naslednjo stran. S tem se prepreči neželeno ponovno pošiljanje obrazca z gumbom *osveži*, *nazaj* ali premikanjem v zgodovini brskalnika. - -Poskusite dodati tudi druge [elemente obrazca|controls]. - - -Dostop do elementov -=================== - -Obrazec je komponenta presenterja, v našem primeru poimenovana `registrationForm` (po imenu tovarne metode `createComponentRegistrationForm`), tako da kjerkoli v presenterju do obrazca pridete s pomočjo: - -```php -$form = $this->getComponent('registrationForm'); -// alternativna sintaksa: $form = $this['registrationForm']; -``` - -Komponente so tudi posamezni elementi obrazca, zato do njih pridete na enak način: - -```php -$input = $form->getComponent('name'); // ali $input = $form['name']; -$button = $form->getComponent('send'); // ali $button = $form['send']; -``` - -Elementi se odstranijo s pomočjo unset: - -```php -unset($form['name']); -``` - - -Validacijska pravila -==================== - -Padla je beseda *veljaven,* ampak obrazec zaenkrat nima nobenih validacijskih pravil. Popravimo to. - -Ime bo obvezno, zato ga označimo z metodo `setRequired()`, katere argument je besedilo sporočila o napaki, ki se prikaže, če uporabnik imena ne izpolni. Če argumenta ne navedemo, se uporabi privzeto sporočilo o napaki. - -```php -$form->addText('name', 'Ime:') - ->setRequired('Prosimo, vnesite ime'); -``` - -Poskusite poslati obrazec brez izpolnjenega imena in videli boste, da se prikaže sporočilo o napaki in brskalnik ali strežnik ga bo zavračal, dokler polja ne izpolnite. - -Hkrati sistema ne boste prelisičili s tem, da v polje napišete na primer le presledke. Kje pa. Nette leve in desne presledke samodejno odstranjuje. Preizkusite to. To je stvar, ki bi jo morali z vsakim enovrstičnim vnosom vedno narediti, a se nanjo pogosto pozabi. Nette to dela samodejno. (Lahko poskusite prelisičiti obrazec in kot ime poslati večvrstični niz. Tudi tukaj se Nette ne pusti zmesti in prelome vrstic spremeni v presledke.) - -Obrazec se vedno validira na strani strežnika, vendar se generira tudi JavaScript validacija, ki poteka bliskovito in uporabnik se o napaki seznani takoj, brez potrebe po pošiljanju obrazca na strežnik. Za to skrbi skript `netteForms.js`. Vstavite ga v predlogo postavitve: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Če pogledate v izvorno kodo strani z obrazcem, lahko opazite, da Nette obvezne elemente vstavlja v elemente s CSS razredom `required`. Poskusite dodati v predlogo naslednji slogovni list in oznaka „Ime“ bo rdeča. Elegantno tako uporabnikom označimo obvezne elemente: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Druga validacijska pravila dodamo z metodo `addRule()`. Prvi parameter je pravilo, drugi je spet besedilo sporočila o napaki in lahko še sledi argument validacijskega pravila. Kaj se s tem misli? - -Obrazec razširimo z novim neobveznim poljem „starost“, ki mora biti celo število (`addInteger()`) in poleg tega v dovoljenem obsegu (`$form::Range`). In tukaj prav izkoristimo tretji parameter metode `addRule()`, s katerim validatorju predamo zahtevani obseg kot par `[od, do]`: - -```php -$form->addInteger('age', 'Starost:') - ->addRule($form::Range, 'Starost mora biti od 18 do 120', [18, 120]); -``` - -.[tip] -Če uporabnik polja ne izpolni, se validacijska pravila ne bodo preverjala, saj je element neobvezen. - -Tukaj nastane prostor za drobno preoblikovanje (refactoring). V sporočilu o napaki in v tretjem parametru so števila navedena podvojeno, kar ni idealno. Če bi ustvarjali [večjezične obrazce |rendering#Prevajanje] in bi bilo sporočilo, ki vsebuje števila, prevedeno v več jezikov, bi se otežila morebitna sprememba vrednosti. Iz tega razloga je mogoče uporabiti nadomestne znake `%d` in Nette vrednosti dopolni: - -```php - ->addRule($form::Range, 'Starost mora biti od %d do %d let', [18, 120]); -``` - -Vrnimo se k elementu `password`, ki ga prav tako naredimo obveznega in še preverimo minimalno dolžino gesla (`$form::MinLength`), spet z uporabo nadomestnega znaka: - -```php -$form->addPassword('password', 'Geslo:') - ->setRequired('Izberite si geslo') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -Dodamo v obrazec še polje `passwordVerify`, kjer uporabnik vnese geslo še enkrat, za preverjanje. S pomočjo validacijskih pravil preverimo, ali sta obe gesli enaki (`$form::Equal`). In kot parameter damo sklic na prvo geslo s pomočjo [oglatih oklepajev |#Dostop do elementov]: - -```php -$form->addPassword('passwordVerify', 'Geslo za preverjanje:') - ->setRequired('Prosimo, vnesite geslo še enkrat za preverjanje') - ->addRule($form::Equal, 'Gesli se ne ujemata', $form['password']) - ->setOmitted(); -``` - -S pomočjo `setOmitted()` smo označili element, katerega vrednost nas pravzaprav ne zanima in ki obstaja le zaradi validacije. Vrednost se ne preda v `$data`. - -S tem imamo končan popolnoma delujoč obrazec z validacijo v PHP in JavaScriptu. Validacijske sposobnosti Nette so veliko širše, dajo se ustvarjati pogoji, puščati glede na njih prikazovati in skrivati dele strani itd. Vse se boste naučili v poglavju o [validaciji obrazcev|validation]. - - -Privzete vrednosti -================== - -Elementom obrazca običajno nastavljamo privzete vrednosti: - -```php -$form->addEmail('email', 'E-pošta') - ->setDefaultValue($lastUsedEmail); -``` - -Pogosto je koristno nastaviti privzete vrednosti vsem elementom hkrati. Na primer, ko obrazec služi za urejanje zapisov. Preberemo zapis iz podatkovne baze in nastavimo privzete vrednosti: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Kličite `setDefaults()` šele po definiciji elementov. - - -Izrisovanje obrazca -=================== - -Standardno se obrazec izriše kot tabela. Posamezni elementi izpolnjujejo osnovno pravilo dostopnosti - vse oznake so zapisane kot `<label>` in povezane z ustreznim elementom obrazca. Ob kliku na oznako se kazalec samodejno pojavi v polju obrazca. - -Vsakemu elementu lahko nastavljamo poljubne HTML atribute. Na primer dodati placeholder: - -```php -$form->addInteger('age', 'Starost:') - ->setHtmlAttribute('placeholder', 'Prosimo, izpolnite starost'); -``` - -Načinov, kako izrisati obrazec, je res veliko, zato je temu namenjeno [samostojno poglavje o izrisovanju|rendering]. - - -Mapiranje na razrede -==================== - -Vrnimo se k metodi `formSucceeded()`, ki v drugem parametru `$data` prejme poslane podatke kot objekt `ArrayHash`. Ker gre za generični razred, nekaj kot `stdClass`, nam bo pri delu z njim manjkalo določeno udobje, kot je na primer predlaganje lastnosti v urejevalnikih ali statična analiza kode. To bi lahko rešili tako, da bi za vsak obrazec imeli konkreten razred, katerega lastnosti predstavljajo posamezne elemente. Npr.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativno lahko uporabite konstruktor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Lastnosti podatkovnega razreda so lahko tudi enumi in pride do njihovega samodejnega mapiranja. .{data-version:3.2.4} - -Kako povedati Nette, naj nam podatke vrača kot objekte tega razreda? Lažje, kot si mislite. Zadostuje le navesti razred kot tip parametra `$data` v obdelovalni metodi: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $name je instanca RegistrationFormData - $name = $data->name; - // ... -} -``` - -Kot tip lahko navedete tudi `array` in potem podatke preda kot polje. - -Podobno lahko uporabljate tudi funkcijo `getValues()`, ki ji ime razreda ali objekt za hidracijo predamo kot parameter: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Če obrazci tvorijo večnivojsko strukturo, sestavljeno iz vsebnikov, ustvarite za vsakega samostojen razred: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Mapiranje nato iz tipa lastnosti `$person` prepozna, da mora vsebnik mapirati na razred `PersonFormData`. Če bi lastnost vsebovala polje vsebnikov, navedite tip `array` in razred za mapiranje predajte neposredno vsebniku: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Načrt podatkovnega razreda obrazca si lahko pustite generirati s pomočjo metode `Nette\Forms\Blueprint::dataClass($form)`, ki ga izpiše na stran brskalnika. Kodo nato zadostuje s klikom označiti in kopirati v projekt. .{data-version:3.1.15} - - -Več gumbov -========== - -Če ima obrazec več kot en gumb, moramo praviloma razlikovati, kateri od njih je bil pritisnjen. Lahko si za vsak gumb ustvarimo lastno obdelovalno funkcijo. Nastavimo jo kot handler za [dogodek |nette:glossary#Dogodki eventi] `onClick`: - -```php -$form->addSubmit('save', 'Shrani') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Izbriši') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Ti handlerji se kličejo le v primeru veljavno izpolnjenega obrazca, enako kot v primeru dogodka `onSuccess`. Razlika je v tem, da se kot prvi parameter namesto obrazca lahko preda gumb za oddajo, odvisno od tipa, ki ga navedete: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Ko se obrazec odda z gumbom <kbd>Enter</kbd>, se šteje, kot da bi bil oddan s prvim gumbom. - - -Dogodek onAnchor -================ - -Ko v tovarni metodi (kot je npr. `createComponentRegistrationForm`) sestavljamo obrazec, ta še ne ve, ali je bil poslan, niti s kakšnimi podatki. So pa primeri, ko poslane vrednosti potrebujemo poznati, na primer se glede na njih odvija nadaljnja podoba obrazca, ali jih potrebujemo za odvisna izbirna polja itd. - -Del kode, ki sestavlja obrazec, lahko zato pustite poklicati šele v trenutku, ko je t.i. zasidran, torej je že povezan s presenterjem in pozna svoje poslane podatke. Takšno kodo predamo v polje `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Država:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Mesto:'); - -$form->onAnchor[] = function () use ($country, $city) { - // ta funkcija se pokliče šele, ko bo obrazec vedel, ali je bil poslan in s kakšnimi podatki - // lahko torej uporabljamo metodo getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Zaščita pred ranljivostmi -========================= - -Nette Framework daje velik poudarek varnosti in zato skrbno pazi na dobro zavarovanje obrazcev. To počne popolnoma transparentno in ne zahteva ročnega nastavljanja ničesar. - -Poleg tega, da obrazce zaščiti pred napadom [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] in [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], opravlja veliko drobnih zavarovanj, na katere vam ni več treba misliti. - -Tako na primer iz vnosov odfiltrira vse kontrolne znake in preveri veljavnost UTF-8 kodiranja, tako da bodo podatki iz obrazca vedno čisti. Pri izbirnih poljih in seznamih izbirnih gumbov preverja, da so bile izbrane postavke dejansko iz ponujenih in da ni prišlo do ponarejanja. Že smo omenili, da pri enovrstičnih besedilnih vnosih odstranjuje znake konca vrstic, ki bi jih tja lahko poslal napadalec. Pri večvrstičnih vnosih pa normalizira znake za konce vrstic. In tako naprej. - -Nette za vas rešuje varnostna tveganja, za katera veliko programerjev niti ne ve, da obstajajo. - -Omenjeni CSRF napad temelji na tem, da napadalec žrtev zvabi na stran, ki neopazno v brskalniku žrtve izvede zahtevo na strežnik, na katerem je žrtev prijavljena, in strežnik domneva, da je zahtevo izvedla žrtev po svoji volji. Zato Nette preprečuje pošiljanje POST obrazca iz druge domene. Če iz kakršnega koli razloga želite zaščito izklopiti in dovoliti pošiljanje obrazca iz druge domene, uporabite: - -```php -$form->allowCrossOrigin(); // POZOR! Izklopi zaščito! -``` - -Ta zaščita uporablja SameSite piškotek, poimenovan `_nss`. Zaščita s pomočjo SameSite piškotka morda ni 100% zanesljiva, zato je priporočljivo vklopiti še zaščito s pomočjo žetona (token): - -```php -$form->addProtection(); -``` - -Priporočamo, da tako zaščitite obrazce v administrativnem delu spletnega mesta, ki spreminjajo občutljive podatke v aplikaciji. Ogrodje se proti napadu CSRF brani z generiranjem in preverjanjem avtorizacijskega žetona, ki se shranjuje v sejo. Zato je treba pred prikazom obrazca imeti odprto sejo. V administrativnem delu spletnega mesta je običajno seja že zagnana zaradi prijave uporabnika. Sicer sejo zaženite z metodo `Nette\Http\Session::start()`. - - -Enak obrazec v več presenterjih -=============================== - -Če potrebujete en obrazec uporabiti v več presenterjih, priporočamo, da si zanj ustvarite tovarno, ki si jo nato predajte v presenter. Primerna lokacija za tak razred je npr. imenik `app/Forms`. - -Tovarniški razred lahko izgleda na primer takole: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ime:'); - $form->addSubmit('send', 'Prijavi se'); - return $form; - } -} -``` - -Razred prosimo za izdelavo obrazca v tovarni metodi za komponente v presenterju: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // lahko obrazec spremenimo, tukaj na primer spreminjamo napis na gumbu - $form['send']->setCaption('Nadaljuj'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // in dodamo handler - return $form; -} -``` - -Handler za obdelavo obrazca je lahko tudi že dodan iz tovarne: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ime:'); - $form->addSubmit('send', 'Prijavi se'); - $form->onSuccess[] = function (Form $form, $data): void { - // tukaj izvedemo obdelavo obrazca - }; - return $form; - } -} -``` - -Tako, za nami je hiter uvod v obrazce v Nette. Poskusite še pogledati v imenik [examples|https://github.com/nette/forms/tree/master/examples] v distribuciji, kjer najdete dodatno inspiracijo. diff --git a/forms/sl/rendering.texy b/forms/sl/rendering.texy deleted file mode 100644 index e12a057593..0000000000 --- a/forms/sl/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Izrisovanje obrazcev -******************** - -Videz obrazcev je lahko zelo raznolik. V praksi lahko naletimo na dva ekstrema. Na eni strani stoji potreba po izrisovanju številnih obrazcev v aplikaciji, ki so si vizualno podobni kot jajce jajcu, in cenimo enostavno izrisovanje brez predloge s pomočjo `$form->render()`. Gre običajno za primer administrativnih vmesnikov. - -Na drugi strani pa so raznoliki obrazci, kjer velja: vsak kos je original. Njihovo podobo najbolje opišemo z jezikom HTML v predlogi obrazca. In seveda poleg obeh omenjenih ekstremov naletimo na veliko obrazcev, ki se gibljejo nekje vmes. - - -Izrisovanje s pomočjo Latte -=========================== - -[Sistem predlog Latte|latte:] bistveno olajša izrisovanje obrazcev in njihovih elementov. Najprej si bomo pokazali, kako obrazce izrisovati ročno po posameznih elementih in s tem pridobiti popoln nadzor nad kodo. Kasneje si bomo pokazali, kako je mogoče takšno izrisovanje [avtomatizirati |#Samodejno izrisovanje]. - -Načrt Latte predloge obrazca si lahko pustite generirati s pomočjo metode `Nette\Forms\Blueprint::latte($form)`, ki ga izpiše na stran brskalnika. Kodo nato zadostuje s klikom označiti in kopirati v projekt. .{data-version:3.1.15} - - -`{control}` ------------ - -Najenostavnejši način, kako izrisati obrazec, je napisati v predlogi: - -```latte -{control signInForm} -``` - -Vplivati na podobo tako izrisanega obrazca je mogoče s konfiguracijo [Rendererja |#Renderer] in [posameznih elementov |#HTML atributi]. - - -`n:name` --------- - -Definicijo obrazca v PHP kodi je mogoče izjemno enostavno povezati s HTML kodo. Zadostuje le dopolniti atribute `n:name`. Tako je enostavno! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Podobo končne HTML kode imate popolnoma v svojih rokah. Če atribut `n:name` uporabite pri elementih `<select>`, `<button>` ali `<textarea>`, se njihova notranja vsebina samodejno dopolni. Oznaka `<form n:name>` poleg tega ustvari lokalno spremenljivko `$form` z objektom risanega obrazca in zaključna `</form>` izriše vse neizrisane skrite elemente (enako velja tudi za `{form} ... {/form}`). - -Ne smemo pa pozabiti na izrisovanje morebitnih sporočil o napakah. In to tako tistih, ki so se z metodo `addError()` dodala k posameznim elementom (s pomočjo `{inputError}`), kot tudi tistih, dodanih neposredno k obrazcu (vrača jih `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Bolj zapletene elemente obrazca, kot sta RadioList ali CheckboxList, je mogoče tako izrisovati po posameznih postavkah: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Ne želite pri vsakem elementu razmišljati, kateri HTML element zanj uporabiti v predlogi, ali `<input>`, `<textarea>` itd.? Rešitev je univerzalna oznaka `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Če obrazec uporablja prevajalnik|, bo besedilo znotraj oznak `{label}` prevedeno. - -Tudi v tem primeru je mogoče bolj zapletene elemente obrazca, kot sta RadioList ali CheckboxList, izrisovati po posameznih postavkah: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Za izrisovanje samega `<input>` v elementu Checkbox uporabite `{input myCheckbox:}`. HTML atribute v tem primeru vedno ločujte z vejico `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Izpiše sporočilo o napaki k elementu obrazca, če ga ima. Sporočilo običajno zavijemo v HTML element zaradi stiliranja. Preprečiti izrisovanje praznega elementa, če sporočila ni, je mogoče elegantno s pomočjo `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Prisotnost napake lahko ugotovimo z metodo `hasErrors()` in glede na to nastavimo razred nadrejenemu elementu: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Oznake `{form signInForm}...{/form}` so alternativa k `<form n:name="signInForm">...</form>`. - - -Samodejno izrisovanje ---------------------- - -Zahvaljujoč oznakam `{input}` in `{label}` lahko enostavno ustvarimo splošno predlogo za kateri koli obrazec. Postopoma bo iterirala in izrisovala vse njegove elemente, razen skritih elementov, ki se izrišejo samodejno ob zaključku obrazca z oznako `</form>`. Ime izrisovanega obrazca bo pričakovala v spremenljivki `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Uporabljene samozaključujoče parne oznake `{label .../}` prikazujejo oznake, ki izhajajo iz definicije obrazca v PHP kodi. - -To splošno predlogo shranite na primer v datoteko `basic-form.latte` in za izrisovanje obrazca jo zadostuje vključiti ter predati ime (ali instanco) obrazca v parameter `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Če bi pri izrisovanju enega določenega obrazca želeli poseči v njegovo podobo in na primer en element izrisati drugače, potem je najenostavnejša pot, da si v predlogi predpripravite bloke, ki jih bo mogoče kasneje prepisati. Bloki imajo lahko tudi [dinamična imena |latte:template-inheritance#Dinamična imena blokov], vanje je tako mogoče vstaviti tudi ime izrisovanega elementa. Na primer: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Za element npr. `username` tako nastane blok `input-username`, ki ga je mogoče enostavno prepisati z uporabo oznake [{embed} |latte:template-inheritance#Dedovanje enot]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternativno je mogoče celotno vsebino predloge `basic-form.latte` [definirati |latte:template-inheritance#Definicije] kot blok, vključno s parametrom `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Zahvaljujoč temu bo njegov klic nekoliko enostavnejši: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -Blok pri tem zadostuje uvoziti na enem samem mestu in to na začetku predloge postavitve: - -```latte -{import basic-form.latte} -``` - - -Posebni primeri ---------------- - -Če potrebujete izrisati le notranji del obrazca brez HTML oznak `<form>`, na primer pri pošiljanju odrezkov (snippetov), jih skrijte s pomočjo atributa `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Z izrisovanjem elementov znotraj vsebnika obrazca pomaga oznaka `{formContainer}`. - -```latte -<p>Katere novice želite prejemati:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Izrisovanje brez Latte -====================== - -Najenostavnejši način, kako izrisati obrazec, je poklicati: - -```php -$form->render(); -``` - -Vplivati na podobo tako izrisanega obrazca je mogoče s konfiguracijo [Rendererja |#Renderer] in [posameznih elementov |#HTML atributi]. - - -Ročno izrisovanje ------------------ - -Vsak element obrazca ima metode, ki generirajo HTML kodo polja obrazca in oznake. Lahko jo vračajo bodisi kot niz ali objekt [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` vrne HTML kodo elementa -- `getLabel($caption = null): Html|string|null` vrne HTML kodo oznake, če obstaja - -Obrazec je tako mogoče izrisovati po posameznih elementih: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Medtem ko pri nekaterih elementih `getControl()` vrne en sam HTML element (npr. `<input>`, `<select>` ipd.), pri drugih cel kos HTML kode (CheckboxList, RadioList). V takem primeru lahko uporabite metode, ki generirajo posamezne vnose in oznake, za vsako postavko posebej: - -- `getControlPart($key = null): ?Html` vrne HTML kodo ene postavke -- `getLabelPart($key = null): ?Html` vrne HTML kodo oznake ene postavke - -.[note] -Te metode imajo iz zgodovinskih razlogov predpono `get`, vendar bi bil boljši `generate`, ker pri vsakem klicu ustvari in vrne nov element `Html`. - - -Renderer -======== - -Gre za objekt, ki zagotavlja izrisovanje obrazca. Tega je mogoče nastaviti z metodo `$form->setRenderer`. Njemu se preda nadzor ob klicu metode `$form->render()`. - -Če ne nastavimo lastnega rendererja, bo uporabljen privzeti izrisovalnik [api:Nette\Forms\Rendering\DefaultFormRenderer]. Ta elemente obrazca izriše v obliki HTML tabele. Izhod izgleda takole: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Ime:</label></th> - - <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> -</tr> - -<tr class="required"> - <th><label class="required" for="frm-age">Starost:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Spol:</label></th> - ... -``` - -Ali uporabiti ali ne uporabiti za ogrodje obrazca tabelo je sporno in vrsta spletnih oblikovalcev preferira drugačen markup. Na primer definicijski seznam. Prekonfiguriramo zato `DefaultFormRenderer` tako, da obrazec izriše v obliki seznama. Konfiguracija se izvaja z urejanjem polja [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Prvi indeks vedno predstavlja območje in drugi njegov atribut. Posamezna območja ponazarja slika: - -[* defaultformrenderer.webp *] - -Standardno je skupina elementov `controls` ovita s tabelo `<table>`, vsak `pair` predstavlja vrstico tabele `<tr>` in par `label` ter `control` sta celici `<th>` in `<td>`. Zdaj ovojne elemente spremenimo. Območje `controls` vstavimo v vsebnik `<dl>`, območje `pair` pustimo brez vsebnika, `label` vstavimo v `<dt>` in na koncu `control` ovijemo z oznakami `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Rezultat je ta HTML koda: - -```latte -<dl> - <dt><label class="required" for="frm-name">Ime:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Starost:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Spol:</label></dt> - ... -</dl> -``` - -V polju wrappers je mogoče vplivati na celo vrsto drugih atributov: - -- dodajati CSS razrede posameznim tipom elementov obrazca -- razlikovati CSS razred lihih in sodih vrstic -- vizualno ločiti obvezne in neobvezne postavke -- določati, ali se sporočila o napakah prikažejo neposredno pri elementih ali nad obrazcem - - -Možnosti (Options) ------------------- - -Obnašanje Rendererja je mogoče nadzorovati tudi z nastavljanjem *options* na posameznih elementih obrazca. Tako je mogoče nastaviti napis, ki se izpiše poleg vnosnega polja: - -```php -$form->addText('phone', 'Številka:') - ->setOption('description', 'Ta številka bo ostala skrita'); -``` - -Če vanj želimo umestiti HTML vsebino, uporabimo razred [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Številka:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Pogoji shranjevanja Vaše številke</a>') - ); -``` - -.[tip] -Html element je mogoče uporabiti tudi namesto oznake: `$form->addCheckbox('conditions', $label)`. - - -Združevanje elementov ---------------------- - -Renderer omogoča združevanje elementov v vizualne skupine (fieldsete): - -```php -$form->addGroup('Osebni podatki'); -``` - -Po ustvarjanju nove skupine ta postane aktivna in vsak na novo dodan element je hkrati dodan tudi vanjo. Tako je mogoče obrazec graditi na ta način: - -```php -$form = new Form; -$form->addGroup('Osebni podatki'); -$form->addText('name', 'Vaše ime:'); -$form->addInteger('age', 'Vaša starost:'); -$form->addEmail('email', 'E-pošta:'); - -$form->addGroup('Naslov za dostavo'); -$form->addCheckbox('send', 'Pošlji na naslov'); -$form->addText('street', 'Ulica:'); -$form->addText('city', 'Mesto:'); -$form->addSelect('country', 'Država:', $countries); -``` - -Renderer najprej izrisuje skupine in šele nato elemente, ki ne pripadajo nobeni skupini. - - -Podpora za Bootstrap --------------------- - -[V primerih |https://github.com/nette/forms/tree/master/examples] najdete primere, kako konfigurirati Renderer za [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] in [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -HTML atributi -============= - -Za nastavitev poljubnih HTML atributov elementov obrazca uporabimo metodo `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Število:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Razvrsti po:', ['ceni', 'nazivu']) - ->setHtmlAttribute('onchange', 'submit()'); // ob spremembi pošlji - - -// Za nastavitev atributov samega <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Specifikacija tipa elementa: - -```php -$form->addText('tel', 'Vaš telefon:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'napišite telefon'); -``` - -.[warning] -Nastavitev tipa in drugih atributov služi le za vizualne namene. Preverjanje pravilnosti vnosov mora potekati na strežniku, kar zagotovite z izbiro ustreznega [elementa obrazca|controls] in navedbo [validacijskih pravil|validation]. - -Posameznim postavkam v seznamih izbirnih gumbov ali potrditvenih polj lahko nastavimo HTML atribut z različnimi vrednostmi za vsako od njih. Opazite dvopičje za `style:`, ki zagotovi izbiro vrednosti glede na ključ: - -```php -$colors = ['r' => 'rdeča', 'g' => 'zelena', 'b' => 'modra']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Barve:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Izpiše: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">rdeča</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">zelena</label> -<label><input type="checkbox" name="colors[]" value="b">modra</label> -``` - -Za nastavitev logičnih atributov, kot je `readonly`, lahko uporabimo zapis z vprašajem: - -```php -$form->addCheckboxList('colors', 'Barve:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // za več ključev uporabite polje, npr. ['r', 'g'] -``` - -Izpiše: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">rdeča</label> -<label><input type="checkbox" name="colors[]" value="g">zelena</label> -<label><input type="checkbox" name="colors[]" value="b">modra</label> -``` - -V primeru izbirnih polj (selectbox) metoda `setHtmlAttribute()` nastavlja atribute elementa `<select>`. Če želimo nastaviti atribute posameznim `<option>`, uporabimo metodo `setOptionAttribute()`. Delujejo tudi zapisi z dvopičjem in vprašajem, navedeni zgoraj: - -```php -$form->addSelect('colors', 'Barve:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Izpiše: - -```latte -<select name="colors"> - <option value="r" style="background:red">rdeča</option> - <option value="g" style="background:green">zelena</option> - <option value="b">modra</option> -</select> -``` - - -Prototipi ---------- - -Alternativni način nastavljanja HTML atributov temelji na urejanju predloge, iz katere se HTML element generira. Predloga je objekt `Html` in jo vrača metoda `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Število:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Na ta način je mogoče modificirati tudi predlogo oznake, ki jo vrača `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Pri elementih Checkbox, CheckboxList in RadioList lahko vplivate na predlogo elementa, ki celoten element ovija. Vrača jo `getContainerPrototype()`. V privzetem stanju gre za „prazen“ element, tako da se nič ne izrisuje, a s tem, da mu nastavimo ime, se bo izrisoval: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -V primeru CheckboxList in RadioList lahko vplivate tudi na predlogo ločila posameznih postavk, ki ga vrača metoda `getSeparatorPrototype()`. V privzetem stanju je to element `<br>`. Če ga spremenite v parni element, bo posamezne postavke ovijal namesto ločeval. In dalje lahko vplivate na predlogo HTML elementa oznake pri posameznih postavkah, ki ga vrača `getItemLabelPrototype()`. - - -Prevajanje -========== - -Če programirate večjezično aplikacijo, boste verjetno potrebovali obrazec izrisati v različnih jezikovnih mutacijah. Nette Framework za ta namen definira vmesnik za prevajanje [api:Nette\Localization\Translator]. V Nette ni nobene privzete implementacije, lahko si izberete glede na svoje potrebe iz več pripravljenih rešitev, ki jih najdete na [Componette |https://componette.org/search/localization]. V njihovi dokumentaciji boste izvedeli, kako prevajalnik konfigurirati. - -Obrazci podpirajo izpisovanje besedil preko prevajalnika. Predamo jim ga s pomočjo metode `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -Od te točke naprej se ne le vse oznake, ampak tudi vsa sporočila o napakah ali postavke izbirnih polj prevedejo v drug jezik. - -Pri posameznih elementih obrazca je pri tem mogoče nastaviti drug prevajalnik ali prevajanje popolnoma izklopiti z vrednostjo `null`: - -```php -$form->addSelect('carModel', 'Model:', $cars) - ->setTranslator(null); -``` - -Pri [validacijskih pravilih|validation] se prevajalniku predajajo tudi specifični parametri, na primer pri pravilu: - -```php -$form->addPassword('password', 'Geslo:') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -se kliče prevajalnik s temi parametri: - -```php -$translator->translate('Geslo mora imeti vsaj %d znakov', 8); -``` - -in torej lahko izbere pravilno obliko množine pri besedi `znakov` glede na število. - - -Dogodek onRender -================ - -Tik preden se obrazec izriše, lahko pustimo poklicati našo kodo. Ta lahko na primer dopolni elementom obrazca HTML razrede za pravilno prikazovanje. Kodo dodamo v polje `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/sl/standalone.texy b/forms/sl/standalone.texy deleted file mode 100644 index a242f18279..0000000000 --- a/forms/sl/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Obrazci, uporabljeni samostojno -******************************* - -.[perex] -Nette Forms bistveno olajšajo ustvarjanje in obdelavo spletnih obrazcev. Uporabljate jih lahko v svojih aplikacijah popolnoma samostojno brez preostalega ogrodja, kar bomo pokazali v tem poglavju. - -Če pa uporabljate Nette Application in presenterje, je za vas namenjen vodnik za [uporabo v presenterjih|in-presenter]. - - -Prvi obrazec -============ - -Poskusimo napisati preprost obrazec za registracijo. Njegova koda bo naslednja ("celotna koda":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Ime:'); -$form->addPassword('password', 'Geslo:'); -$form->addSubmit('send', 'Registriraj'); -``` - -Zelo enostavno ga izrišemo: - -```php -$form->render(); -``` - -in v brskalniku se prikaže takole: - -[* form-cs.webp *] - -Obrazec je objekt razreda `Nette\Forms\Form` (razred `Nette\Application\UI\Form` se uporablja v presenterjih). Dodali smo mu t.i. elemente ime, geslo in gumb za pošiljanje. - -In zdaj obrazec oživimo. Z vprašanjem `$form->isSuccess()` ugotovimo, ali je bil obrazec poslan in ali je bil veljavno izpolnjen. Če je, podatke izpišemo. Za definicijo obrazca torej dodamo: - -```php -if ($form->isSuccess()) { - echo 'Obrazec je bil pravilno izpolnjen in poslan'; - $data = $form->getValues(); - // $data->name vsebuje ime - // $data->password vsebuje geslo - var_dump($data); -} -``` - -Metoda `getValues()` vrača poslane podatke v obliki objekta [ArrayHash |utils:arrays#ArrayHash]. Kako to spremeniti, si bomo ogledali [kasneje |#Preslikava v razrede]. Objekt `$data` vsebuje ključa `name` in `password` s podatki, ki jih je izpolnil uporabnik. - -Običajno podatke takoj pošljemo v nadaljnjo obdelavo, kar je lahko na primer vstavljanje v bazo podatkov. Med obdelavo pa se lahko pojavi napaka, na primer uporabniško ime je že zasedeno. V takem primeru napako vrnemo nazaj v obrazec z `addError()` in ga pustimo ponovno izrisati, tudi s sporočilom o napaki. - -```php -$form->addError('Oprostite, to uporabniško ime že nekdo uporablja.'); -``` - -Po obdelavi obrazca preusmerimo na naslednjo stran. S tem preprečimo neželeno ponovno pošiljanje obrazca z gumbom *obnovi*, *nazaj* ali premikanjem v zgodovini brskalnika. - -Obrazec se standardno pošilja z metodo POST in to na isto stran. Oboje se da spremeniti: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -In to je pravzaprav vse :-) Imamo delujoč in popolnoma [zaščiten |#Zaščita pred ranljivostmi] obrazec. - -Poskusite dodati tudi druge [elemente obrazca|controls]. - - -Dostop do elementov -=================== - -Obrazec in njegove posamezne elemente imenujemo komponente. Tvorijo drevo komponent, kjer je koren prav obrazec. Do posameznih elementov obrazca dostopamo na ta način: - -```php -$input = $form->getComponent('name'); -// alternativna sintaksa: $input = $form['name']; - -$button = $form->getComponent('send'); -// alternativna sintaksa: $button = $form['send']; -``` - -Elementi se odstranijo z unset: - -```php -unset($form['name']); -``` - - -Validacijska pravila -==================== - -Omenili smo besedo *veljaven,* vendar obrazec zaenkrat nima nobenih validacijskih pravil. Popravimo to. - -Ime bo obvezno, zato ga označimo z metodo `setRequired()`, katere argument je besedilo sporočila o napaki, ki se prikaže, če uporabnik imena ne izpolni. Če argumenta ne navedemo, se uporabi privzeto sporočilo o napaki. - -```php -$form->addText('name', 'Ime:') - ->setRequired('Prosimo, vnesite ime'); -``` - -Poskusite poslati obrazec brez izpolnjenega imena in videli boste, da se prikaže sporočilo o napaki in brskalnik ali strežnik ga bo zavračal, dokler polja ne izpolnite. - -Hkrati sistema ne boste prelisičili s tem, da v polje vpišete samo presledke. Kje pa. Nette samodejno odstranjuje leve in desne presledke. Preizkusite. To je stvar, ki bi jo morali vedno narediti z vsakim enovrstičnim vnosom, vendar se nanjo pogosto pozablja. Nette to naredi samodejno. (Lahko poskusite prelisičiti obrazec in kot ime poslati večvrstični niz. Tudi tu se Nette ne pusti zmesti in prelome vrstic spremeni v presledke.) - -Obrazec se vedno validira na strani strežnika, vendar se generira tudi JavaScript validacija, ki poteka bliskovito in uporabnik se o napaki takoj obvesti, brez potrebe po pošiljanju obrazca na strežnik. Za to skrbi skript `netteForms.js`. Vstavite ga na stran: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Če pogledate izvorno kodo strani z obrazcem, lahko opazite, da Nette obvezne elemente vstavlja v elemente s CSS razredom `required`. Poskusite dodati v predlogo naslednji slog in oznaka "Ime" bo rdeča. Tako elegantno uporabnikom označimo obvezne elemente: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Druga validacijska pravila dodamo z metodo `addRule()`. Prvi parameter je pravilo, drugi je spet besedilo sporočila o napaki in lahko še sledi argument validacijskega pravila. Kaj to pomeni? - -Obrazec razširimo z novim neobveznim poljem "starost", ki mora biti celo število (`addInteger()`) in poleg tega v dovoljenem obsegu (`$form::Range`). In tu prav izkoristimo tretji parameter metode `addRule()`, s katerim validatorju predamo zahtevani obseg kot par `[od, do]`: - -```php -$form->addInteger('age', 'Starost:') - ->addRule($form::Range, 'Starost mora biti od 18 do 120', [18, 120]); -``` - -.[tip] -Če uporabnik polja ne izpolni, se validacijska pravila ne bodo preverjala, saj je element neobvezen. - -Tu nastane prostor za drobno preoblikovanje (refactoring). V sporočilu o napaki in v tretjem parametru so števila navedena dvakrat, kar ni idealno. Če bi ustvarjali [večjezične obrazce |rendering#Prevajanje] in bi bilo sporočilo, ki vsebuje števila, prevedeno v več jezikov, bi se morebitna sprememba vrednosti otežila. Zato je mogoče uporabiti nadomestne znake `%d` in Nette vrednosti dopolni: - -```php - ->addRule($form::Range, 'Starost mora biti od %d do %d let', [18, 120]); -``` - -Vrnimo se k elementu `password`, ki ga prav tako naredimo obveznega in še preverimo minimalno dolžino gesla (`$form::MinLength`), spet z uporabo nadomestnega znaka: - -```php -$form->addPassword('password', 'Geslo:') - ->setRequired('Izberite si geslo') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -Dodamo v obrazec še polje `passwordVerify`, kjer uporabnik vnese geslo še enkrat, za kontrolo. S pomočjo validacijskih pravil preverimo, ali sta obe gesli enaki (`$form::Equal`). In kot parameter damo sklic na prvo geslo z uporabo [oglatimi oklepaji |#Dostop do elementov]: - -```php -$form->addPassword('passwordVerify', 'Geslo za kontrolo:') - ->setRequired('Prosimo, vnesite geslo še enkrat za kontrolo') - ->addRule($form::Equal, 'Gesli se ne ujemata', $form['password']) - ->setOmitted(); -``` - -Z `setOmitted()` smo označili element, katerega vrednost nas pravzaprav ne zanima in ki obstaja le zaradi validacije. Vrednost se ne preda v `$data`. - -S tem imamo končan popolnoma delujoč obrazec z validacijo v PHP in JavaScriptu. Validacijske zmožnosti Nette so veliko širše, dajo se ustvarjati pogoji, puščati po njih prikazovati in skrivati dele strani itd. Vse boste izvedeli v poglavju o [validaciji obrazcev|validation]. - - -Privzete vrednosti -================== - -Elementom obrazca običajno nastavimo privzete vrednosti: - -```php -$form->addEmail('email', 'E-pošta') - ->setDefaultValue($lastUsedEmail); -``` - -Pogosto je koristno nastaviti privzete vrednosti vsem elementom hkrati. Na primer, ko obrazec služi za urejanje zapisov. Preberemo zapis iz baze podatkov in nastavimo privzete vrednosti: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Pokličite `setDefaults()` šele po definiciji elementov. - - -Izris obrazca -============= - -Standardno se obrazec izriše kot tabela. Posamezni elementi izpolnjujejo osnovno pravilo dostopnosti - vse oznake so zapisane kot `<label>` in povezane z ustreznim elementom obrazca. Pri kliku na oznako se kazalec samodejno pojavi v polju obrazca. - -Vsakemu elementu lahko nastavimo poljubne HTML atribute. Na primer, dodamo placeholder: - -```php -$form->addInteger('age', 'Starost:') - ->setHtmlAttribute('placeholder', 'Prosimo, izpolnite starost'); -``` - -Načinov, kako izrisati obrazec, je res veliko, zato je temu namenjeno [ločeno poglavje o izrisu|rendering]. - - -Preslikava v razrede -==================== - -Vrnimo se k obdelavi podatkov obrazca. Metoda `getValues()` nam je vračala poslane podatke kot objekt `ArrayHash`. Ker gre za generični razred, nekaj kot `stdClass`, nam bo pri delu z njim manjkalo določeno udobje, kot je na primer predlaganje lastnosti v urejevalnikih ali statična analiza kode. To bi lahko rešili tako, da bi za vsak obrazec imeli konkreten razred, katerega lastnosti predstavljajo posamezne elemente. Npr.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativno lahko uporabite konstruktor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Lastnosti podatkovnega razreda so lahko tudi enumi in pride do njihove samodejne preslikave. .{data-version:3.2.4} - -Kako Nette sporočiti, naj nam podatke vrača kot objekte tega razreda? Lažje, kot si mislite. Dovolj je le ime razreda ali objekt za hidracijo navesti kot parameter: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Kot parameter lahko navedete tudi `'array'` in potem podatke vrne kot polje. - -Če obrazci tvorijo večnivojsko strukturo, sestavljeno iz vsebnikov, ustvarite za vsakega ločen razred: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Preslikava nato iz tipa lastnosti `$person` prepozna, da mora vsebnik preslikati v razred `PersonFormData`. Če bi lastnost vsebovala polje vsebnikov, navedite tip `array` in razred za preslikavo predajte neposredno vsebniku: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Načrt podatkovnega razreda obrazca si lahko pustite generirati s pomočjo metode `Nette\Forms\Blueprint::dataClass($form)`, ki ga izpiše na stran brskalnika. Kodo nato samo kliknite, označite in kopirajte v projekt. .{data-version:3.1.15} - - -Več gumbov -========== - -Če ima obrazec več kot en gumb, moramo praviloma razlikovati, kateri od njih je bil pritisnjen. To informacijo nam vrne metoda `isSubmittedBy()` gumba: - -```php -$form->addSubmit('save', 'Shrani'); -$form->addSubmit('delete', 'Izbriši'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Vprašanja `$form->isSuccess()` ne izpustite, s tem preverite veljavnost podatkov. - -Ko se obrazec pošlje z gumbom <kbd>Enter</kbd>, se šteje, kot da je bil poslan s prvim gumbom. - - -Zaščita pred ranljivostmi -========================= - -Nette Framework daje velik poudarek varnosti in zato skrbno pazi na dobro zaščito obrazcev. - -Poleg tega, da obrazce zaščiti pred napadom [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] in [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], izvaja veliko drobnih zaščit, na katere vam ni treba več misliti. - -Tako na primer iz vhodov filtrira vse kontrolne znake in preveri veljavnost UTF-8 kodiranja, tako da bodo podatki iz obrazca vedno čisti. Pri izbirnih poljih in seznamih radijskih gumbov preverja, ali so bili izbrani elementi resnično iz ponujenih in ni prišlo do ponarejanja. Že smo omenili, da pri enovrstičnih besedilnih vnosih odstranjuje znake konca vrstice, ki jih je tja lahko poslal napadalec. Pri večvrstičnih vnosih pa normalizira znake za konce vrstic. In tako naprej. - -Nette za vas rešuje varnostna tveganja, za katera veliko programerjev sploh ne ve, da obstajajo. - -Omenjeni napad CSRF temelji na tem, da napadalec žrtev zvabi na stran, ki neopazno v brskalniku žrtve izvede zahtevo na strežnik, na katerem je žrtev prijavljena, in strežnik domneva, da je zahtevo izvedla žrtev po svoji volji. Zato Nette preprečuje pošiljanje obrazca POST iz druge domene. Če iz kakršnega koli razloga želite zaščito izklopiti in dovoliti pošiljanje obrazca iz druge domene, uporabite: - -```php -$form->allowCrossOrigin(); // POZOR! Izklopi zaščito! -``` - -Ta zaščita uporablja SameSite piškotek z imenom `_nss`. Zato ustvarite objekt obrazca še pred pošiljanjem prvega izpisa, da bo mogoče piškotek poslati. - -Zaščita s pomočjo SameSite piškotka morda ni 100% zanesljiva, zato je priporočljivo vklopiti še zaščito s pomočjo žetona: - -```php -$form->addProtection(); -``` - -Priporočamo, da tako zaščitite obrazce v administrativnem delu spletnega mesta, ki spreminjajo občutljive podatke v aplikaciji. Ogrodje se proti napadu CSRF brani z generiranjem in preverjanjem avtorizacijskega žetona, ki se shranjuje v sejo. Zato je treba pred prikazom obrazca imeti odprto sejo. V administrativnem delu spletnega mesta je običajno seja že zagnana zaradi prijave uporabnika. Sicer sejo zaženite z metodo `Nette\Http\Session::start()`. - -Tako, za nami je hiter uvod v obrazce v Nette. Poskusite si še pogledati v imenik [examples|https://github.com/nette/forms/tree/master/examples] v distribuciji, kjer boste našli dodatno inspiracijo. diff --git a/forms/sl/validation.texy b/forms/sl/validation.texy deleted file mode 100644 index da4379ff7c..0000000000 --- a/forms/sl/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Validacija obrazcev -******************* - - -Obvezni elementi -================ - -Obvezne elemente označimo z metodo `setRequired()`, katere argument je besedilo [#sporočila o napakah], ki se prikaže, če uporabnik elementa ne izpolni. Če argumenta ne navedemo, se uporabi privzeto sporočilo o napaki. - -```php -$form->addText('name', 'Ime:') - ->setRequired('Prosimo, vnesite ime'); -``` - - -Pravila -======= - -Validacijska pravila dodajamo elementom z metodo `addRule()`. Prvi parameter je pravilo, drugi je besedilo [#sporočila o napakah] in tretji je argument validacijskega pravila. - -```php -$form->addPassword('password', 'Geslo:') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -**Validacijska pravila se preverjajo samo v primeru, da je uporabnik element izpolnil.** - -Nette prihaja s celo vrsto preddefiniranih pravil, katerih imena so konstante razreda `Nette\Forms\Form`. Pri vseh elementih lahko uporabimo ta pravila: - -| konstanta | opis | tip argumenta -|------- -| `Required` | obvezen element, alias za `setRequired()` | - -| `Filled` | obvezen element, alias za `setRequired()` | - -| `Blank` | element ne sme biti izpolnjen | - -| `Equal` | vrednost je enaka parametru | `mixed` -| `NotEqual` | vrednost ni enaka parametru | `mixed` -| `IsIn` | vrednost je enaka nekateremu elementu v polju | `array` -| `IsNotIn` | vrednost ni enaka nobenemu elementu v polju | `array` -| `Valid` | je element pravilno izpolnjen? (za [pogoje |#Pogoji]) | - - - -Besedilni vnosi ---------------- - -Pri elementih `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` lahko uporabimo tudi nekatera naslednja pravila: - -| `MinLength` | minimalna dolžina besedila | `int` -| `MaxLength` | maksimalna dolžina besedila | `int` -| `Length` | dolžina v obsegu ali natančna dolžina | par `[int, int]` ali `int` -| `Email` | veljaven e-poštni naslov | - -| `URL` | absolutni URL | - -| `Pattern` | ustreza regularnemu izrazu | `string` -| `PatternInsensitive` | kot `Pattern`, vendar neodvisno od velikosti črk | `string` -| `Integer` | celoštevilska vrednost | - -| `Numeric` | alias za `Integer` | - -| `Float` | število | - -| `Min` | minimalna vrednost numeričnega elementa | `int\|float` -| `Max` | maksimalna vrednost numeričnega elementa | `int\|float` -| `Range` | vrednost v obsegu | par `[int\|float, int\|float]` - -Validacijska pravila `Integer`, `Numeric` in `Float` takoj pretvorijo vrednost v integer oz. float. In nadalje pravilo `URL` sprejme tudi naslov brez sheme (npr. `nette.org`) in shemo dopolni (`https://nette.org`). Izraz v `Pattern` in `PatternIcase` mora veljati za celotno vrednost, tj. kot da bi bil obdan z znakoma `^` in `$`. - - -Število elementov ------------------ - -Pri elementih `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` lahko uporabimo tudi naslednja pravila za omejitev števila izbranih elementov oz. naloženih datotek: - -| `MinLength` | minimalno število | `int` -| `MaxLength` | maksimalno število | `int` -| `Length` | število v obsegu ali natančno število | par `[int, int]` ali `int` - - -Nalaganje datotek ------------------ - -Pri elementih `addUpload()`, `addMultiUpload()` lahko uporabimo tudi naslednja pravila: - -| `MaxFileSize` | maksimalna velikost datoteke v bajtih | `int` -| `MimeType` | MIME tip, dovoljeni nadomestni znaki (`'video/*'`) | `string\|string[]` -| `Image` | slika JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | ime datoteke ustreza regularnemu izrazu | `string` -| `PatternInsensitive` | kot `Pattern`, vendar neodvisno od velikosti črk | `string` - -`MimeType` in `Image` zahtevata PHP razširitev `fileinfo`. Da je datoteka ali slika zahtevanega tipa, zaznajo na podlagi njene signature in **ne preverjajo integritete celotne datoteke.** Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganjem |http:request#toImage]. - - -Sporočila o napakah -=================== - -Vsa preddefinirana pravila z izjemo `Pattern` in `PatternInsensitive` imajo privzeto sporočilo o napaki, zato ga lahko izpustite. Vendar z navedbo in oblikovanjem vseh sporočil po meri naredite obrazec uporabniku prijaznejši. - -Spremeniti privzeta sporočila lahko v [konfiguraciji|forms:configuration], s prilagoditvijo besedil v polju `Nette\Forms\Validator::$messages` ali z uporabo [prevajalniku |rendering#Prevajanje]. - -V besedilu sporočil o napakah lahko uporabljate te nadomestne nize: - -| `%d` | postopoma nadomesti z argumenti pravila -| `%n$d` | nadomesti z n-tim argumentom pravila -| `%label` | nadomesti z oznako elementa (brez dvopičja) -| `%name` | nadomesti z imenom elementa (npr. `name`) -| `%value` | nadomesti z vrednostjo, ki jo je vnesel uporabnik - -```php -$form->addText('name', 'Ime:') - ->setRequired('Izpolnite prosim %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'najmanj %d in največ %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'največ %2$d in najmanj %1$d', [5, 10]); -``` - - -Pogoji -====== - -Poleg pravil lahko dodajamo tudi pogoje. Ti se zapisujejo podobno kot pravila, le da namesto `addRule()` uporabimo metodo `addCondition()` in seveda ne navajamo nobenega sporočila o napaki (pogoj se samo sprašuje): - -```php -$form->addPassword('password', 'Geslo:') - // če geslo ni daljše od 8 znakov - ->addCondition($form::MaxLength, 8) - // potem mora vsebovati števko - ->addRule($form::Pattern, 'Mora vsebovati števko', '.*[0-9].*'); -``` - -Pogoj je mogoče vezati tudi na drug element kot trenutni s pomočjo `addConditionOn()`. Kot prvi parameter navedemo sklic na element. V tem primeru bo e-pošta obvezna le takrat, ko se označi potrditveno polje (njegova vrednost bo true): - -```php -$form->addCheckbox('newsletters', 'pošiljajte mi novice'); - -$form->addEmail('email', 'E-pošta:') - // če je potrditveno polje označeno - ->addConditionOn($form['newsletters'], $form::Equal, true) - // potem zahtevaj e-pošto - ->setRequired('Vnesite e-poštni naslov'); -``` - -Iz pogojev je mogoče ustvarjati kompleksne strukture s pomočjo `elseCondition()` in `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // če je izpolnjen prvi pogoj - ->addConditionOn(/* ... */) // in drugi pogoj na drugem elementu - ->addRule(/* ... */) // zahtevaj to pravilo - ->elseCondition() // če drugi pogoj ni izpolnjen - ->addRule(/* ... */) // zahtevaj ta pravila - ->addRule(/* ... */) - ->endCondition() // vračamo se k prvemu pogoju - ->addRule(/* ... */); -``` - -V Nette je mogoče zelo enostavno reagirati na izpolnitev ali neizpolnitev pogoja tudi na strani JavaScripta s pomočjo metode `toggle()`, glej [#Dinamični JavaScript]. - - -Sklic na drug element -===================== - -Kot argument pravila ali pogoja lahko predamo tudi drug element obrazca. Pravilo potem uporabi vrednost, ki jo je kasneje vnesel uporabnik v brskalniku. Tako lahko npr. dinamično validiramo, da element `password` vsebuje enak niz kot element `password_confirm`: - -```php -$form->addPassword('password', 'Geslo'); -$form->addPassword('password_confirm', 'Potrdite geslo') - ->addRule($form::Equal, 'Vneseni gesli se ne ujemata', $form['password']); -``` - - -Pravila in pogoji po meri -========================= - -Včasih se znajdemo v situaciji, ko nam vgrajena validacijska pravila v Nette ne zadostujejo in moramo podatke od uporabnika validirati po svoje. V Nette je to zelo enostavno! - -Metodam `addRule()` ali `addCondition()` lahko kot prvi parameter predamo poljuben povratni klic. Ta sprejme kot prvi parameter sam element in vrača boolean vrednost, ki določa, ali je validacija potekala v redu. Pri dodajanju pravila s pomočjo `addRule()` je mogoče vnesti tudi druge argumente, ti so nato predani kot drugi parameter. - -Lasten nabor validatorjev tako lahko ustvarimo kot razred s statičnimi metodami: - -```php -class MyValidators -{ - // testira, ali je vrednost deljiva z argumentom - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // drugi validatorji - } -} -``` - -Uporaba je nato zelo enostavna: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Vrednost mora biti večkratnik števila %d', - 8, - ); -``` - -Lastna validacijska pravila lahko dodajamo tudi v JavaScript. Pogoj je, da je pravilo statična metoda. Njeno ime za JavaScript validator nastane s spojitvijo imena razreda brez povratnih poševnic `\`, podčrtaja `_` in imena metode. Npr. `App\MyValidators::validateDivisibility` zapišemo kot `AppMyValidators_validateDivisibility` in dodamo v objekt `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Dogodek onValidate -================== - -Po pošiljanju obrazca se izvede validacija, kjer se preverijo posamezna pravila, dodana s pomočjo `addRule()`, in nato se sproži [dogodek |nette:glossary#Dogodki eventi] `onValidate`. Njegov obravnavalnik lahko uporabimo za dodatno validacijo, tipično preverjanje pravilne kombinacije vrednosti v več elementih obrazca. - -Če se odkrije napaka, jo predamo v obrazec z metodo `addError()`. To lahko pokličemo bodisi na konkretnem elementu ali neposredno na obrazcu. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Ta kombinacija ni mogoča.'); - } -} -``` - - -Napake pri obdelavi -=================== - -V mnogih primerih se o napaki zavemo šele v trenutku, ko obdelujemo veljaven obrazec, na primer zapisujemo novo postavko v bazo podatkov in naletimo na podvojitev ključev. V takem primeru napako spet predamo v obrazec z metodo `addError()`. To lahko pokličemo bodisi na konkretnem elementu ali neposredno na obrazcu: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Neveljavno geslo.'); - } -} -``` - -Če je mogoče, priporočamo, da napako priključite neposredno elementu obrazca, ker se bo prikazala poleg njega pri uporabi privzetega rendererja. - -```php -$form['date']->addError('Oprostite, ampak ta datum je že zaseden.'); -``` - -Lahko `addError()` kličete večkrat in tako predaste obrazcu ali elementu več sporočil o napakah. Dobite jih s pomočjo `getErrors()`. - -Pozor, `$form->getErrors()` vrača povzetek vseh sporočil o napakah, tudi tistih, ki so bila predana neposredno posameznim elementom, ne le neposredno obrazcu. Sporočila o napakah, predana samo obrazcu, dobite preko `$form->getOwnErrors()`. - - -Spreminjanje vnosa -================== - -S pomočjo metode `addFilter()` lahko spremenimo vrednost, ki jo je vnesel uporabnik. V tem primeru bomo tolerirali in odstranjevali presledke v poštni številki: - -```php -$form->addText('zip', 'Poštna št.:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // odstranimo presledke iz poštne številke - }) - ->addRule($form::Pattern, 'Poštna št. ni v obliki petih števk', '\d{5}'); -``` - -Filter se vključi med validacijska pravila in pogoje, zato je vrstni red metod pomemben, tj. filter in pravilo se kličeta v takem vrstnem redu, kot je vrstni red metod `addFilter()` in `addRule()`. - - -Validacija JavaScript -===================== - -Jezik za oblikovanje pogojev in pravil je zelo močan. Vse konstrukcije pri tem delujejo tako na strani strežnika kot tudi na strani JavaScripta. Prenašajo se v HTML atributih `data-nette-rules` kot JSON. Samo validacijo nato izvaja skript, ki prestreže dogodek obrazca `submit`, pregleda posamezne elemente in izvede ustrezno validacijo. - -Ta skript je `netteForms.js` in je na voljo iz več možnih virov: - -Skript lahko vstavite neposredno v HTML stran iz CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ali ga kopirate lokalno v javno mapo projekta (npr. iz `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Ali namestite preko [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -In nato naložite in zaženete: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternativno ga lahko naložite neposredno iz mape `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Dinamični JavaScript -==================== - -Želite prikazati polja za vnos naslova samo, če uporabnik izbere pošiljanje blaga po pošti? Ni problema. Ključ je par metod `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Ta koda pravi, da ko je pogoj izpolnjen, torej ko je potrditveno polje označeno, bo viden HTML element `#address-container`. In obratno. Elemente obrazca z naslovom prejemnika tako postavimo v vsebnik s tem ID-jem in ob kliku na potrditveno polje se skrijejo ali prikažejo. To zagotavlja skript `netteForms.js`. - -Kot argument metode `toggle()` je mogoče predati poljuben selektor. Iz zgodovinskih razlogov se alfanumerični niz brez drugih posebnih znakov razume kot ID elementa, torej enako, kot če bi mu predhajal znak `#`. Drugi neobvezni parameter omogoča obrniti vedenje, tj. če bi uporabili `toggle('#address-container', false)`, bi se element nasprotno prikazal samo takrat, če potrditveno polje ne bi bilo označeno. - -Privzeta implementacija v JavaScriptu spreminja elementom lastnost `hidden`. Vedenje pa lahko enostavno spremenimo, na primer dodamo animacijo. Dovolj je, da v JavaScriptu prepišemo metodo `Nette.toggle` z lastno rešitvijo: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // skrijemo ali prikažemo 'el' glede na vrednost 'visible' - }); -}; -``` - - -Izklop validacije -================= - -Včasih se lahko zgodi, da je treba validacijo izklopiti. Če pritisk na gumb za pošiljanje ne sme izvajati validacije (primerno za gumbe *Prekliči* ali *Predogled*), jo izklopimo z metodo `$submit->setValidationScope([])`. Če naj izvaja le delno validacijo, lahko določimo, katera polja ali vsebnikov obrazca se naj validirajo. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Validira celoten obrazec -$form->addSubmit('send2') - ->setValidationScope([]); // Sploh ne validira -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Validira samo element name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Validira samo element age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Validira vsebnik details -``` - -`setValidationScope` ne vpliva na [#dogodek onValidate] pri obrazcu, ki bo vedno poklican. Dogodek `onValidate` pri vsebniku bo sprožen samo, če je ta vsebnik označen za delno validacijo. diff --git a/forms/tr/@home.texy b/forms/tr/@home.texy index 06ba1aeba8..cdb0ce374a 100644 --- a/forms/tr/@home.texy +++ b/forms/tr/@home.texy @@ -3,29 +3,29 @@ Nette Forms <div class=perex> -Nette Forms, web formlarının oluşturulmasında devrim yarattı. Aniden, birkaç anlaşılır kod satırı yazmak yeterli oldu ve oluşturma, JavaScript ve sunucu tarafı doğrulama dahil olmak üzere hazır bir formunuz oldu ve ayrıca en üst düzeyde güvenliydi. Nasıl yapılacağını göstereceğiz +Nette Forms, web formlarının oluşturulmasında devrim yarattı. Birdenbire birkaç anlaşılır satır kod yazmak, render, JavaScript ve sunucu tarafı doğrulama ile birinci sınıf güvenliği kapsayan eksiksiz bir form elde etmeye yetti. Size şunları göstereceğiz: - kullanıcı dostu formlar oluşturma - gönderilen verileri doğrulama -- öğeleri tam olarak gerektiği gibi oluşturma +- öğeleri tam istediğiniz gibi render etme </div> -Nette Forms kullanarak, doğrulama yazma (ayrıca çift, sunucu ve istemci tarafında) gibi birçok rutin görevden kaçınırsınız, hata ve güvenlik açığı olasılığını en aza indirirsiniz. +Nette Forms sayesinde, doğrulama mantığı yazmak (hem sunucu hem istemci tarafında) gibi pek çok rutin işten kaçınabilir, hata ve güvenlik açığı olasılığını en aza indirebilirsiniz. -Formları Nette Uygulamasının bir parçası olarak (yani presenter'larda) veya tamamen bağımsız olarak kullanabilirsiniz. Her iki durumda da kullanım biraz farklı olduğundan, sizin için iki kılavuz hazırladık: +Formları hem bir Nette Application'ın parçası olarak (yani presenter'larda) hem de tümüyle bağımsız kullanabilirsiniz. Kullanım iki durumda biraz farklı olduğundan, sizin için ayrı kılavuzlar hazırladık: <div class="wiki-buttons"> -<div> "Presenter'lardaki Formlar .[wiki-button]":in-presenter </div> -<div> "Bağımsız Formlar .[wiki-button]":standalone </div> +<div> "Presenter'larda formlar .[wiki-button]":in-presenter </div> +<div> "Bağımsız formlar .[wiki-button]":standalone </div> </div> Kurulum ------- -Kütüphaneyi [Composer|best-practices:composer] aracını kullanarak indirip kurabilirsiniz: +Paketi [Composer|best-practices:composer] ile indirip kurun: ```shell composer require nette/forms diff --git a/forms/tr/@left-menu.texy b/forms/tr/@left-menu.texy index f9169bf428..f385e15ef1 100644 --- a/forms/tr/@left-menu.texy +++ b/forms/tr/@left-menu.texy @@ -1,14 +1,16 @@ Nette Forms *********** -- [Giriş |@home] -- [Presenter'lardaki Formlar|in-presenter] -- [Bağımsız Formlar|standalone] -- [Form Öğeleri |controls] +- [Genel bakış |@home] +- [Presenter'larda formlar|in-presenter] +- [Bağımsız formlar|standalone] +- [Form öğeleri |controls] - [Doğrulama |validation] -- [Oluşturma |rendering] -- [Yapılandırma |configuration] +- [Render |rendering] +- [Özel form öğeleri |custom-controls] +- [Yapılandırma|configuration] +- [Yükseltme|upgrading] Daha Fazla Okuma **************** -- [Kılavuzlar ve yöntemler |best-practices:] +- [En iyi uygulamalar |best-practices:] diff --git a/forms/tr/configuration.texy b/forms/tr/configuration.texy index 77fa3c84ba..f70981c954 100644 --- a/forms/tr/configuration.texy +++ b/forms/tr/configuration.texy @@ -2,7 +2,7 @@ Form Yapılandırması ******************* .[perex] -Yapılandırmada, varsayılan [form hata mesajları|validation] değiştirebilirsiniz. +Varsayılan [form hata mesajlarını|validation] yapılandırmada değiştirebilirsiniz. ```neon forms: @@ -17,6 +17,7 @@ forms: 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.' @@ -35,27 +36,28 @@ forms: ```neon forms: messages: - Equal: '%s girin.' + Equal: 'Lütfen %s girin.' NotEqual: 'Bu değer %s olmamalıdır.' Filled: 'Bu alan zorunludur.' Blank: 'Bu alan boş olmalıdır.' MinLength: 'Lütfen en az %d karakter girin.' MaxLength: 'Lütfen en fazla %d karakter girin.' Length: 'Lütfen %d ile %d karakter uzunluğunda bir değer girin.' - Email: 'Geçerli bir e-posta adresi girin.' + Email: 'Lütfen geçerli bir e-posta adresi girin.' URL: 'Lütfen geçerli bir URL girin.' - Integer: 'Geçerli bir tamsayı girin.' - Float: 'Geçerli bir sayı girin.' - Min: 'Lütfen %d veya daha büyük bir değer girin.' - Max: 'Lütfen %d veya daha küçük bir değer girin.' + Integer: 'Lütfen geçerli bir tam sayı girin.' + Numeric: 'Lütfen negatif olmayan bir tam sayı girin.' + Float: 'Lütfen geçerli bir sayı girin.' + Min: 'Lütfen %d değerine eşit ya da ondan büyük bir değer girin.' + Max: 'Lütfen %d değerine eşit ya da ondan küçük bir değer girin.' Range: 'Lütfen %d ile %d arasında bir değer girin.' MaxFileSize: 'Yüklenen dosyanın boyutu en fazla %d bayt olabilir.' - MaxPostSize: 'Yüklenen veriler %d bayt sınırını aşıyor.' + MaxPostSize: 'Yüklenen veri %d bayt sınırını aşıyor.' MimeType: 'Yüklenen dosya beklenen biçimde değil.' - Image: 'Yüklenen dosya JPEG, GIF, PNG, WebP veya AVIF biçiminde bir resim olmalıdır.' + Image: 'Yüklenen dosya JPEG, GIF, PNG ya da WebP biçiminde bir görsel olmalıdır.' Nette\Forms\Controls\SelectBox::Valid: 'Lütfen geçerli bir seçenek seçin.' - Nette\Forms\Controls\UploadControl::Valid: 'Dosya yükleme sırasında bir hata oluştu.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Oturumunuzun süresi doldu. Lütfen ana sayfaya dönüp tekrar deneyin.' + Nette\Forms\Controls\UploadControl::Valid: 'Dosya yüklenirken bir hata oluştu.' + Nette\Forms\Controls\CsrfProtection::Protection: 'Oturumunuzun süresi doldu. Lütfen ana sayfaya dönüp yeniden deneyin.' ``` -Eğer tüm framework'ü ve dolayısıyla yapılandırma dosyalarını kullanmıyorsanız, varsayılan hata mesajlarını doğrudan `Nette\Forms\Validator::$messages` dizisinde değiştirebilirsiniz. +Framework'ün tamamını, dolayısıyla yapılandırma dosyalarını da kullanmıyorsanız, varsayılan hata mesajlarını doğrudan `Nette\Forms\Validator::$messages` dizisinde değiştirebilirsiniz. diff --git a/forms/tr/controls.texy b/forms/tr/controls.texy index b45be2ca83..0c54792627 100644 --- a/forms/tr/controls.texy +++ b/forms/tr/controls.texy @@ -1,71 +1,71 @@ -Form Elemanları -*************** +Form Öğeleri +************ .[perex] -Standart form elemanlarının özeti. +Standart form öğelerine genel bakış. -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== +addText(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +============================================================================================== -Tek satırlık bir metin kutusu ekler (sınıf [TextInput |api:Nette\Forms\Controls\TextInput]). Kullanıcı alanı doldurmazsa, boş bir dize `''` döndürür veya `setNullable()` kullanarak `null` döndürmesini belirleyebilirsiniz. +Tek satırlık bir metin girdi alanı ekler ([TextInput |api:Nette\Forms\Controls\TextInput] sınıfı). Kullanıcı alanı doldurmazsa boş dize `''` döndürür; bunun yerine `null` döndürmesi için `setNullable()` kullanın. ```php -$form->addText('name', 'İsim:') +$form->addText('name', 'Ad:') ->setRequired() ->setNullable(); ``` -UTF-8'i otomatik olarak doğrular, sol ve sağ boşlukları kırpar ve bir saldırganın gönderebileceği satır sonlarını kaldırır. +UTF-8 kodlamasını otomatik doğrular, baştaki ve sondaki boşlukları kırpar ve bir saldırganın gönderebileceği satır sonlarını kaldırır. -Maksimum uzunluk `setMaxLength()` ile sınırlandırılabilir. Kullanıcı tarafından girilen değeri değiştirmek [addFilter() |validation#Girişi Değiştirme] ile mümkündür. +En fazla uzunluk `setMaxLength()` ile sınırlanabilir. [addFilter() |validation#Girdi Değerlerini Değiştirme] metodu, kullanıcının girdiği değeri değiştirmeye olanak tanır. -`setHtmlType()` kullanarak metin kutusunun görsel karakterini `search`, `tel` veya `url` gibi türlere değiştirebilirsiniz, bkz. [şartname|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Tür değişikliğinin yalnızca görsel olduğunu ve doğrulama işlevinin yerini tutmadığını unutmayın. `url` türü için belirli bir [URL kuralı |validation#Metin Girişleri] eklemek uygundur. +`setHtmlType()` ile metin alanının görsel görünümünü, [belirtimde|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types] tanımlandığı gibi `search`, `tel` ya da `url` gibi türlere çevirebilirsiniz. Türü değiştirmenin salt görsel olduğunu ve doğrulama işlevinin yerini almadığını unutmayın. `url` türünde, belirli bir [URL doğrulama kuralı |validation#Metin girdileri] eklemek yerinde olur. .[note] -`number`, `range`, `email`, `date`, `datetime-local`, `time` ve `color` gibi diğer giriş türleri için, sunucu tarafı doğrulaması sağlayan [#addInteger], [#addFloat], [#addEmail], [#addDate], [#addTime], [#addDateTime] ve [#addColor] gibi özel metotları kullanın. `month` ve `week` türleri henüz tüm tarayıcılarda tam olarak desteklenmemektedir. +`number`, `range`, `email`, `date`, `datetime-local`, `time` ve `color` gibi diğer girdi türlerinde, sunucu tarafı doğrulama sağlayan [#addInteger()], [#addFloat()], [#addEmail()], [#addDate()], [#addTime()], [#addDateTime()] ve [#addColor()] gibi özelleşmiş metotları kullanın. `month` ve `week` türleri henüz tüm tarayıcılarda tam desteklenmiyor. -Elemana, varsayılan değere benzeyen ancak kullanıcı değiştirmezse elemanın boş bir dize veya `null` döndürdüğü sözde boş değer (empty-value) atanabilir. +Öğe için bir "boş değer" ayarlanabilir. Bu, bir bakıma varsayılan değer gibi davranır, ama kullanıcı onu değiştirmezse öğe boş dize ya da `null` döndürür. ```php $form->addText('phone', 'Telefon:') ->setHtmlType('tel') - ->setEmptyValue('+90'); + ->setEmptyValue('+420'); ``` -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== +addTextArea(string $name, $label=null): TextArea .[method] +========================================================== -Çok satırlı metin girmek için bir alan ekler (sınıf [TextArea |api:Nette\Forms\Controls\TextArea]). Kullanıcı alanı doldurmazsa, boş bir dize `''` döndürür veya `setNullable()` kullanarak `null` döndürmesini belirleyebilirsiniz. +Çok satırlı bir metin girdi alanı ekler ([TextArea |api:Nette\Forms\Controls\TextArea] sınıfı). Kullanıcı alanı doldurmazsa boş dize `''` döndürür; bunun yerine `null` döndürmesi için `setNullable()` kullanın. ```php $form->addTextArea('note', 'Not:') - ->addRule($form::MaxLength, 'Not çok uzun', 10000); + ->addRule($form::MaxLength, 'Notunuz fazlasıyla uzun', 10000); ``` -UTF-8'i otomatik olarak doğrular ve satır ayırıcılarını `\n` olarak normalleştirir. Tek satırlık giriş alanının aksine, boşluk kırpma işlemi yapılmaz. +UTF-8 kodlamasını otomatik doğrular ve satır sonlarını `\n` biçimine normalleştirir. Tek satırlık girdi alanının aksine boşluk kırpması yapılmaz. -Maksimum uzunluk `setMaxLength()` ile sınırlandırılabilir. Kullanıcı tarafından girilen değeri değiştirmek [addFilter() |validation#Girişi Değiştirme] ile mümkündür. `setEmptyValue()` kullanarak sözde boş değer (empty-value) ayarlanabilir. +En fazla uzunluk `setMaxLength()` ile sınırlanabilir. [addFilter() |validation#Girdi Değerlerini Değiştirme] metodu, kullanıcının girdiği değeri değiştirmeye olanak tanır. Boş değer `setEmptyValue()` ile ayarlanabilir. -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== +addInteger(string $name, $label=null): TextInput .[method] +========================================================== -Tamsayı girmek için bir alan ekler (sınıf [TextInput |api:Nette\Forms\Controls\TextInput]). Kullanıcı hiçbir şey girmezse tamsayı veya `null` döndürür. +Tam sayı girmeye yarayan bir girdi alanı ekler ([TextInput |api:Nette\Forms\Controls\TextInput] sınıfı). Ya bir tam sayı ya da kullanıcı bir şey girmezse `null` döndürür. ```php $form->addInteger('year', 'Yıl:') ->addRule($form::Range, 'Yıl %d ile %d arasında olmalıdır.', [1900, 2023]); ``` -Eleman `<input type="number">` olarak render edilir. `setHtmlType()` metodunu kullanarak türü, kaydırıcı şeklinde görüntülemek için `range` olarak veya `number` türünün özel davranışları olmayan standart bir metin alanı tercih ediyorsanız `text` olarak değiştirebilirsiniz. +Öğe `<input type="number">` olarak render edilir. `setHtmlType()` metoduyla türü, kaydırıcı olarak görüntülemek için `range` ya da `number` türünün özel davranışı olmadan standart bir metin alanı yeğliyorsanız `text` yapabilirsiniz. -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= +addFloat(string $name, $label=null): TextInput .[method]{data-version:3.1.12} +============================================================================= -Ondalık sayı girmek için bir alan ekler (sınıf [TextInput |api:Nette\Forms\Controls\TextInput]). Kullanıcı hiçbir şey girmezse float veya `null` döndürür. +Kayan noktalı sayı girmeye yarayan bir girdi alanı ekler ([TextInput |api:Nette\Forms\Controls\TextInput] sınıfı). Ya bir float ya da kullanıcı bir şey girmezse `null` döndürür. ```php $form->addFloat('level', 'Seviye:') @@ -73,55 +73,55 @@ $form->addFloat('level', 'Seviye:') ->addRule($form::Range, 'Seviye %d ile %d arasında olmalıdır.', [0, 100]); ``` -Eleman `<input type="number">` olarak render edilir. `setHtmlType()` metodunu kullanarak türü, kaydırıcı şeklinde görüntülemek için `range` olarak veya `number` türünün özel davranışları olmayan standart bir metin alanı tercih ediyorsanız `text` olarak değiştirebilirsiniz. +Öğe `<input type="number">` olarak render edilir. `setHtmlType()` metoduyla türü, kaydırıcı olarak görüntülemek için `range` ya da `number` türünün özel davranışı olmadan standart bir metin alanı yeğliyorsanız `text` yapabilirsiniz. -Nette ve Chrome tarayıcısı, ondalık ayırıcı olarak hem virgülü hem de noktayı kabul eder. Bu işlevselliğin Firefox'ta da kullanılabilir olması için, ilgili eleman veya tüm sayfa için `lang` niteliğini ayarlamanız önerilir, örneğin `<html lang="tr">`. +Nette ve Chrome tarayıcısı, ondalık ayırıcı olarak hem virgülü hem noktayı kabul eder. Bu işlevi Firefox'ta da açmak için, `lang` niteliğini ilgili öğede ya da sayfanın tamamında ayarlamak önerilir, örneğin `<html lang="en">`. -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ +addEmail(string $name, $label=null, int $maxLength=255): TextInput .[method] +============================================================================ -E-posta adresi girmek için bir alan ekler (sınıf [TextInput |api:Nette\Forms\Controls\TextInput]). Kullanıcı alanı doldurmazsa, boş bir dize `''` döndürür veya `setNullable()` kullanarak `null` döndürmesini belirleyebilirsiniz. +E-posta adresi girmeye yarayan bir girdi alanı ekler ([TextInput |api:Nette\Forms\Controls\TextInput] sınıfı). Kullanıcı alanı doldurmazsa boş dize `''` döndürür; bunun yerine `null` döndürmesi için `setNullable()` kullanın. ```php $form->addEmail('email', 'E-posta:'); ``` -Değerin geçerli bir e-posta adresi olup olmadığını doğrular. Alan adının gerçekten var olup olmadığı kontrol edilmez, yalnızca sözdizimi doğrulanır. UTF-8'i otomatik olarak doğrular, sol ve sağ boşlukları kırpar. +Değerin geçerli bir e-posta adresi olduğunu doğrular. Alan adının gerçekten var olup olmadığını denetlemez, yalnızca söz dizimini doğrular. UTF-8 kodlamasını otomatik doğrular ve baştaki ile sondaki boşlukları kırpar. -Maksimum uzunluk `setMaxLength()` ile sınırlandırılabilir. Kullanıcı tarafından girilen değeri değiştirmek [addFilter() |validation#Girişi Değiştirme] ile mümkündür. `setEmptyValue()` kullanarak sözde boş değer (empty-value) ayarlanabilir. +En fazla uzunluk `setMaxLength()` ile sınırlanabilir. [addFilter() |validation#Girdi Değerlerini Değiştirme] metodu, kullanıcının girdiği değeri değiştirmeye olanak tanır. Boş değer `setEmptyValue()` ile ayarlanabilir. -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== +addPassword(string $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] +================================================================================================== -Şifre girmek için bir alan ekler (sınıf [TextInput |api:Nette\Forms\Controls\TextInput]). +Parola girdi alanı ekler ([TextInput |api:Nette\Forms\Controls\TextInput] sınıfı). ```php -$form->addPassword('password', 'Şifre:') +$form->addPassword('password', 'Parola:') ->setRequired() - ->addRule($form::MinLength, 'Şifre en az %d karakter olmalıdır', 8) - ->addRule($form::Pattern, 'Bir rakam içermelidir', '.*[0-9].*'); + ->addRule($form::MinLength, 'Parola en az %d karakter uzunluğunda olmalıdır', 8) + ->addRule($form::Pattern, 'Parola bir rakam içermelidir', '.*[0-9].*'); ``` -Form yeniden görüntülendiğinde alan boş olacaktır. UTF-8'i otomatik olarak doğrular, sol ve sağ boşlukları kırpar ve bir saldırganın gönderebileceği satır sonlarını kaldırır. +Form yeniden görüntülendiğinde alan boş olur. UTF-8 kodlamasını otomatik doğrular, baştaki ve sondaki boşlukları kırpar ve bir saldırganın gönderebileceği satır sonlarını kaldırır. -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ +addCheckbox(string $name, $caption=null): Checkbox .[method] +============================================================ -Bir onay kutusu ekler (sınıf [Checkbox |api:Nette\Forms\Controls\Checkbox]). İşaretli olup olmadığına bağlı olarak `true` veya `false` değerini döndürür. +Bir onay kutusu ekler ([Checkbox |api:Nette\Forms\Controls\Checkbox] sınıfı). İşaretli olup olmamasına göre `true` ya da `false` döndürür. ```php -$form->addCheckbox('agree', 'Şartları kabul ediyorum') - ->setRequired('Şartları kabul etmeniz gerekiyor'); +$form->addCheckbox('agree', 'Koşulları kabul ediyorum') + ->setRequired('Koşullarımızı kabul etmelisiniz'); ``` -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== +addCheckboxList(string $name, $label=null, ?array $items=null): CheckboxList .[method] +====================================================================================== -Birden çok öğe seçmek için onay kutuları ekler (sınıf [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Seçilen öğelerin anahtarlarından oluşan bir dizi döndürür. `getSelectedItems()` metodu anahtarlar yerine değerleri döndürür. +Birden çok öğe seçmek için onay kutusu listesi ekler ([CheckboxList |api:Nette\Forms\Controls\CheckboxList] sınıfı). Seçilen öğelerin anahtarlarından oluşan bir dizi döndürür. `getSelectedItems()` metodu, seçilen öğeleri anahtar-değer çiftleri olarak döndürür. ```php $form->addCheckboxList('colors', 'Renkler:', [ @@ -131,155 +131,156 @@ $form->addCheckboxList('colors', 'Renkler:', [ ]); ``` -Sunulan öğelerin dizisini üçüncü parametre olarak veya `setItems()` metoduyla iletiriz. +Sunulan öğe dizisini üçüncü parametre olarak ya da `setItems()` metoduyla verin. `setItems()` metoduna ikinci argüman olarak `false` verirseniz, değerler aynı zamanda anahtar olarak kullanılır. -`setDisabled(['r', 'g'])` kullanarak bireysel öğeleri devre dışı bırakabilirsiniz. +Tek tek öğeleri devre dışı bırakmak için `setDisabled(['r', 'g'])` kullanın. -Eleman, sahtecilik yapılmadığını ve seçilen öğelerin gerçekten sunulanlardan biri olduğunu ve devre dışı bırakılmadığını otomatik olarak kontrol eder. `getRawValue()` metoduyla bu önemli kontrol olmadan gönderilen öğeleri alabilirsiniz. +Öğe, hiçbir sahtecilik yapılmadığını ve seçilen öğelerin gerçekten sunulanlar arasında bulunduğunu ve devre dışı bırakılmadığını otomatik denetler. Gönderilen öğeleri bu önemli denetim olmadan almak için `getRawValue()` metodu kullanılabilir. -Varsayılan seçili öğeleri ayarlarken, bunların sunulanlardan biri olup olmadığını da kontrol eder, aksi takdirde bir istisna fırlatır. Bu kontrol `checkDefaultValue(false)` ile kapatılabilir. +Varsayılan seçili öğeler ayarlanırken de bunların sunulanlar arasında olup olmadığı denetlenir, aksi hâlde istisna fırlatılır. Bu denetim `checkDefaultValue(false)` ile kapatılabilir. -Formu `GET` metoduyla gönderiyorsanız, sorgu dizesinin boyutunu azaltan daha kompakt bir veri aktarım yöntemi seçebilirsiniz. Formun HTML niteliğini ayarlayarak etkinleştirilir: +Formu `GET` metoduyla gönderiyorsanız, query string boyutundan tasarruf eden daha derli toplu bir veri aktarım yöntemi seçebilirsiniz. Onu, formda bir HTML niteliği ayarlayarak etkinleştirin: ```php $form->setHtmlAttribute('data-nette-compact'); ``` -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== +addRadioList(string $name, $label=null, ?array $items=null): RadioList .[method] +================================================================================ -Radyo düğmeleri ekler (sınıf [RadioList |api:Nette\Forms\Controls\RadioList]). Seçilen öğenin anahtarını veya kullanıcı hiçbir şey seçmezse `null` döndürür. `getSelectedItem()` metodu anahtar yerine değeri döndürür. +Radyo düğmeleri ekler ([RadioList |api:Nette\Forms\Controls\RadioList] sınıfı). Seçilen öğenin anahtarını, kullanıcı hiçbir şey seçmediyse `null` döndürür. `getSelectedItem()` metodu anahtar yerine değeri döndürür. ```php $sex = [ 'm' => 'erkek', 'f' => 'kadın', + 'o' => 'diğer', ]; $form->addRadioList('gender', 'Cinsiyet:', $sex); ``` -Sunulan öğelerin dizisini üçüncü parametre olarak veya `setItems()` metoduyla iletiriz. +Sunulan öğe dizisini üçüncü parametre olarak ya da `setItems()` metoduyla verin. -`setDisabled(['m', 'f'])` kullanarak bireysel öğeleri devre dışı bırakabilirsiniz. +Tek tek öğeleri devre dışı bırakmak için `setDisabled(['m'])` kullanın. -Eleman, sahtecilik yapılmadığını ve seçilen öğenin gerçekten sunulanlardan biri olduğunu ve devre dışı bırakılmadığını otomatik olarak kontrol eder. `getRawValue()` metoduyla bu önemli kontrol olmadan gönderilen öğeyi alabilirsiniz. +Öğe, hiçbir sahtecilik yapılmadığını ve seçilen öğenin gerçekten sunulanlardan biri olduğunu ve devre dışı bırakılmadığını otomatik denetler. Gönderilen öğeyi bu önemli denetim olmadan almak için `getRawValue()` metodu kullanılabilir. -Varsayılan seçili öğeyi ayarlarken, bunun sunulanlardan biri olup olmadığını da kontrol eder, aksi takdirde bir istisna fırlatır. Bu kontrol `checkDefaultValue(false)` ile kapatılabilir. +Varsayılan seçili öğe ayarlanırken de bunun sunulanlardan biri olup olmadığı denetlenir, aksi hâlde istisna fırlatılır. Bu denetim `checkDefaultValue(false)` ile kapatılabilir. -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== +addSelect(string $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] +============================================================================================== -Bir seçme kutusu (select box) ekler (sınıf [SelectBox |api:Nette\Forms\Controls\SelectBox]). Seçilen öğenin anahtarını veya kullanıcı hiçbir şey seçmezse `null` döndürür. `getSelectedItem()` metodu anahtar yerine değeri döndürür. +Bir seçim kutusu ekler ([SelectBox |api:Nette\Forms\Controls\SelectBox] sınıfı). Seçilen öğenin anahtarını, kullanıcı hiçbir şey seçmediyse `null` döndürür. `getSelectedItem()` metodu anahtar yerine değeri döndürür. ```php $countries = [ - 'TR' => 'Türkiye', - 'DE' => 'Almanya', - 'GB' => 'Büyük Britanya', + 'CZ' => 'Çek Cumhuriyeti', + 'SK' => 'Slovakya', + 'GB' => 'Birleşik Krallık', ]; $form->addSelect('country', 'Ülke:', $countries) - ->setDefaultValue('TR'); + ->setDefaultValue('SK'); ``` -Sunulan öğelerin dizisini üçüncü parametre olarak veya `setItems()` metoduyla iletiriz. Öğeler iki boyutlu bir dizi de olabilir: +Sunulan öğe dizisini üçüncü parametre olarak ya da `setItems()` metoduyla verin. Öğeler iki boyutlu bir dizi de olabilir (optgroup'ları temsil eder): ```php $countries = [ 'Avrupa' => [ 'CZ' => 'Çek Cumhuriyeti', - 'DE' => 'Almanya', - 'FR' => 'Fransa', + 'SK' => 'Slovakya', + 'GB' => 'Birleşik Krallık', ], - 'TR' => 'Türkiye', + 'CA' => 'Kanada', 'US' => 'ABD', '?' => 'diğer', ]; ``` -Seçme kutularında genellikle ilk öğenin özel bir anlamı vardır, bir eylem çağrısı olarak hizmet eder. Böyle bir öğe eklemek için `setPrompt()` metodu kullanılır. +Seçim kutularında ilk öğenin çoğu zaman özel bir anlamı vardır ve eyleme çağrı görevi görür. Böyle bir öğe eklemek için `setPrompt()` metodunu kullanın. ```php $form->addSelect('country', 'Ülke:', $countries) - ->setPrompt('Ülke seçin'); + ->setPrompt('Bir ülke seçin'); ``` -`setDisabled(['DE', 'FR'])` kullanarak bireysel öğeleri devre dışı bırakabilirsiniz. +Tek tek öğeleri devre dışı bırakmak için `setDisabled(['CZ', 'SK'])` kullanın. -Eleman, sahtecilik yapılmadığını ve seçilen öğenin gerçekten sunulanlardan biri olduğunu ve devre dışı bırakılmadığını otomatik olarak kontrol eder. `getRawValue()` metoduyla bu önemli kontrol olmadan gönderilen öğeyi alabilirsiniz. +Öğe, hiçbir sahtecilik yapılmadığını ve seçilen öğenin gerçekten sunulanlardan biri olduğunu ve devre dışı bırakılmadığını otomatik denetler. Gönderilen öğeyi bu önemli denetim olmadan almak için `getRawValue()` metodu kullanılabilir. -Varsayılan seçili öğeyi ayarlarken, bunun sunulanlardan biri olup olmadığını da kontrol eder, aksi takdirde bir istisna fırlatır. Bu kontrol `checkDefaultValue(false)` ile kapatılabilir. +Varsayılan seçili öğe ayarlanırken de bunun sunulanlardan biri olup olmadığı denetlenir, aksi hâlde istisna fırlatılır. Bu denetim `checkDefaultValue(false)` ile kapatılabilir. -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ +addMultiSelect(string $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] +======================================================================================================== -Birden çok öğe seçmek için bir seçme kutusu ekler (sınıf [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Seçilen öğelerin anahtarlarından oluşan bir dizi döndürür. `getSelectedItems()` metodu anahtarlar yerine değerleri döndürür. +Birden çok öğe seçmek için bir seçim kutusu ekler ([MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox] sınıfı). Seçilen öğelerin anahtarlarından oluşan bir dizi döndürür. `getSelectedItems()` metodu, seçilen öğeleri anahtar-değer çiftleri olarak döndürür. ```php -$form->addMultiSelect('countries', 'Ülke:', $countries); +$form->addMultiSelect('countries', 'Ülkeler:', $countries); ``` -Sunulan öğelerin dizisini üçüncü parametre olarak veya `setItems()` metoduyla iletiriz. Öğeler iki boyutlu bir dizi de olabilir. +Sunulan öğe dizisini üçüncü parametre olarak ya da `setItems()` metoduyla verin. Öğeler iki boyutlu bir dizi de olabilir. -`setDisabled(['DE', 'GB'])` kullanarak bireysel öğeleri devre dışı bırakabilirsiniz. +Tek tek öğeleri devre dışı bırakmak için `setDisabled(['CZ', 'SK'])` kullanın. -Eleman, sahtecilik yapılmadığını ve seçilen öğelerin gerçekten sunulanlardan biri olduğunu ve devre dışı bırakılmadığını otomatik olarak kontrol eder. `getRawValue()` metoduyla bu önemli kontrol olmadan gönderilen öğeleri alabilirsiniz. +Öğe, hiçbir sahtecilik yapılmadığını ve seçilen öğelerin gerçekten sunulanlar arasında bulunduğunu ve devre dışı bırakılmadığını otomatik denetler. Gönderilen öğeleri bu önemli denetim olmadan almak için `getRawValue()` metodu kullanılabilir. -Varsayılan seçili öğeleri ayarlarken, bunların sunulanlardan biri olup olmadığını da kontrol eder, aksi takdirde bir istisna fırlatır. Bu kontrol `checkDefaultValue(false)` ile kapatılabilir. +Varsayılan seçili öğeler ayarlanırken de bunların sunulanlar arasında olup olmadığı denetlenir, aksi hâlde istisna fırlatılır. Bu denetim `checkDefaultValue(false)` ile kapatılabilir. -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= +addUpload(string $name, $label=null): UploadControl .[method] +============================================================= -Dosya yüklemek için bir alan ekler (sınıf [UploadControl |api:Nette\Forms\Controls\UploadControl]). Kullanıcı hiçbir dosya göndermese bile bir [FileUpload |http:request#FileUpload] nesnesi döndürür, bu durum `FileUpload::hasFile()` metoduyla kontrol edilebilir. +Dosya yükleme alanı ekler ([UploadControl |api:Nette\Forms\Controls\UploadControl] sınıfı). Kullanıcı hiç dosya yüklemese bile bir [FileUpload |http:request#FileUpload] nesnesi döndürür; bu, `FileUpload::hasFile()` metoduyla denetlenebilir. `setNullable()` ile, hiç dosya yüklenmediğinde öğenin `FileUpload` nesnesi yerine `null` döndürmesini sağlayabilirsiniz. ```php $form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Avatar JPEG, PNG, GIF, WebP veya AVIF olmalıdır.') - ->addRule($form::MaxFileSize, 'Maksimum boyut 1 MB.', 1024 * 1024); + ->addRule($form::Image, 'Avatar JPEG, PNG, GIF, WebP ya da AVIF olmalıdır.') + ->addRule($form::MaxFileSize, 'En fazla boyut 1 MB.', 1024 * 1024); ``` -Dosya doğru şekilde yüklenemezse, form başarıyla gönderilmez ve bir hata görüntülenir. Yani, başarılı bir gönderimde `FileUpload::isOk()` metodunu doğrulamaya gerek yoktur. +Dosya doğru yüklenemezse form başarıyla gönderilmez ve bir hata görüntülenir. Yani başarılı gönderimde `FileUpload::isOk()` metodunu denetlemeye gerek yoktur. -`FileUpload::getName()` metodu tarafından döndürülen orijinal dosya adına asla güvenmeyin, istemci uygulamanıza zarar vermek veya hacklemek amacıyla kötü niyetli bir dosya adı göndermiş olabilir. +`FileUpload::getName()` metodunun döndürdüğü özgün dosya adına asla güvenmeyin; istemci, uygulamanıza zarar verme ya da onu ele geçirme niyetiyle kötü niyetli bir dosya adı göndermiş olabilir. -`MimeType` ve `Image` kuralları, istenen türü dosya imzasına göre algılar ve bütünlüğünü doğrulamaz. Bir resmin bozuk olup olmadığını, örneğin onu [yükleme |http:request#toImage] deneyerek öğrenebilirsiniz. +`MimeType` ve `Image` kuralları, gereken türü dosyanın imzasına göre saptar ve bütünlüğünü doğrulamaz. Bir görselin bozuk olup olmadığı, örneğin onu [yüklemeyi |http:request#toImage()] deneyerek belirlenebilir. -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== +addMultiUpload(string $name, $label=null): UploadControl .[method] +================================================================== -Aynı anda birden çok dosya yüklemek için bir alan ekler (sınıf [UploadControl |api:Nette\Forms\Controls\UploadControl]). [FileUpload |http:request#FileUpload] nesnelerinden oluşan bir dizi döndürür. Her birinde `FileUpload::hasFile()` metodu `true` döndürür. +Birden çok dosyayı tek seferde yüklemek için bir alan ekler ([UploadControl |api:Nette\Forms\Controls\UploadControl] sınıfı). [FileUpload |http:request#FileUpload] nesnelerinden oluşan bir dizi döndürür. Her birinde `FileUpload::hasFile()` metodu `true` döndürecektir. ```php $form->addMultiUpload('files', 'Dosyalar:') - ->addRule($form::MaxLength, 'En fazla %d dosya yüklenebilir', 10); + ->addRule($form::MaxLength, 'En fazla %d dosya yüklenebilir.', 10); ``` -Dosyalardan herhangi biri doğru şekilde yüklenemezse, form başarıyla gönderilmez ve bir hata görüntülenir. Yani, başarılı bir gönderimde `FileUpload::isOk()` metodunu doğrulamaya gerek yoktur. +Dosyalardan herhangi biri doğru yüklenemezse form başarıyla gönderilmez ve bir hata görüntülenir. Yani başarılı gönderimde her dosya için `FileUpload::isOk()` metodunu denetlemeye gerek yoktur. -`FileUpload::getName()` metodu tarafından döndürülen orijinal dosya adlarına asla güvenmeyin, istemci uygulamanıza zarar vermek veya hacklemek amacıyla kötü niyetli bir dosya adı göndermiş olabilir. +`FileUpload::getName()` metodunun döndürdüğü özgün dosya adlarına asla güvenmeyin; istemci, uygulamanıza zarar verme ya da onu ele geçirme niyetiyle kötü niyetli dosya adları göndermiş olabilir. -`MimeType` ve `Image` kuralları, istenen türü dosya imzasına göre algılar ve bütünlüğünü doğrulamaz. Bir resmin bozuk olup olmadığını, örneğin onu [yükleme |http:request#toImage] deneyerek öğrenebilirsiniz. +`MimeType` ve `Image` kuralları, gereken türü dosyanın imzasına göre saptar ve bütünlüğünü doğrulamaz. Bir görselin bozuk olup olmadığı, örneğin onu [yüklemeyi |http:request#toImage()] deneyerek belirlenebilir. -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== +addDate(string $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} +================================================================================== -Kullanıcının yıl, ay ve günden oluşan bir tarihi kolayca girmesini sağlayan bir alan ekler (sınıf [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Kullanıcının yıl, ay ve günden oluşan bir tarihi kolayca girmesini sağlayan bir alan ekler ([DateTimeControl |api:Nette\Forms\Controls\DateTimeControl] sınıfı). -Varsayılan değer olarak `DateTimeInterface` arayüzünü uygulayan nesneleri, zaman içeren bir dizeyi veya UNIX zaman damgasını temsil eden bir sayıyı kabul eder. Aynı durum, izin verilen minimum ve maksimum tarihi tanımlayan `Min`, `Max` veya `Range` kurallarının argümanları için de geçerlidir. +Varsayılan değer olarak `DateTimeInterface` uygulayan nesneleri, zaman içeren bir dizeyi ya da UNIX zaman damgasını temsil eden bir sayıyı kabul eder. Aynısı, izin verilen en küçük ve en büyük tarihleri tanımlayan `Min`, `Max` ya da `Range` kurallarının argümanları için de geçerlidir. ```php $form->addDate('date', 'Tarih:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Tarih en az bir ay önce olmalıdır.', new DateTime('-1 month')); + ->addRule($form::Min, 'Tarih en az bir ay öncesine ait olmalıdır.', new DateTime('-1 month')); ``` -Standart olarak `DateTimeImmutable` nesnesi döndürür, `setFormat()` metoduyla [metin biçimi|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] veya zaman damgası belirtebilirsiniz: +Varsayılan olarak bir `DateTimeImmutable` nesnesi döndürür. `setFormat()` metoduyla bir [metin biçimi|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ya da zaman damgası belirtebilirsiniz: ```php $form->addDate('date', 'Tarih:') @@ -287,40 +288,40 @@ $form->addDate('date', 'Tarih:') ``` -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== +addTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=========================================================================================================== -Kullanıcının saat, dakika ve isteğe bağlı olarak saniyeden oluşan bir zamanı kolayca girmesini sağlayan bir alan ekler (sınıf [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Kullanıcının saat, dakika ve isteğe bağlı olarak saniyeden oluşan bir zamanı kolayca girmesini sağlayan bir alan ekler ([DateTimeControl |api:Nette\Forms\Controls\DateTimeControl] sınıfı). -Varsayılan değer olarak `DateTimeInterface` arayüzünü uygulayan nesneleri, zaman içeren bir dizeyi veya UNIX zaman damgasını temsil eden bir sayıyı kabul eder. Bu girdilerden yalnızca zaman bilgisi kullanılır, tarih göz ardı edilir. Aynı durum, izin verilen minimum ve maksimum zamanı tanımlayan `Min`, `Max` veya `Range` kurallarının argümanları için de geçerlidir. Ayarlanan minimum değer maksimum değerden yüksekse, gece yarısını aşan bir zaman aralığı oluşturulur. +Varsayılan değer olarak `DateTimeInterface` uygulayan nesneleri, zaman içeren bir dizeyi ya da UNIX zaman damgasını temsil eden bir sayıyı kabul eder. Bu girdilerden yalnızca zaman bilgisi kullanılır, tarih yok sayılır. Aynısı, izin verilen en küçük ve en büyük zamanları tanımlayan `Min`, `Max` ya da `Range` kurallarının argümanları için de geçerlidir. Ayarlanan en küçük değer en büyükten yüksekse, gece yarısını aşan bir zaman aralığı oluşur. ```php -$form->addTime('time', 'Saat:', withSeconds: true) - ->addRule($form::Range, 'Saat %d ile %d arasında olmalıdır.', ['12:30', '13:30']); +$form->addTime('time', 'Zaman:', withSeconds: true) + ->addRule($form::Range, 'Zaman %d ile %d arasında olmalıdır.', ['12:30', '13:30']); ``` -Standart olarak `DateTimeImmutable` nesnesi (1 Ocak yıl 1 tarihiyle) döndürür, `setFormat()` metoduyla [metin biçimi|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] belirtebilirsiniz: +Varsayılan olarak bir `DateTimeImmutable` nesnesi döndürür (tarihi 1 Ocak 1 yılı olarak ayarlanmış). `setFormat()` metoduyla bir [metin biçimi|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] belirtebilirsiniz: ```php -$form->addTime('time', 'Saat:') +$form->addTime('time', 'Zaman:') ->setFormat('H:i'); ``` -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== +addDateTime(string $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} +=============================================================================================================== -Kullanıcının yıl, ay, gün, saat, dakika ve isteğe bağlı olarak saniyeden oluşan bir tarih ve saati kolayca girmesini sağlayan bir alan ekler (sınıf [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). +Kullanıcının yıl, ay, gün, saat, dakika ve isteğe bağlı olarak saniyeden oluşan hem tarihi hem zamanı kolayca girmesini sağlayan bir alan ekler ([DateTimeControl |api:Nette\Forms\Controls\DateTimeControl] sınıfı). -Varsayılan değer olarak `DateTimeInterface` arayüzünü uygulayan nesneleri, zaman içeren bir dizeyi veya UNIX zaman damgasını temsil eden bir sayıyı kabul eder. Aynı durum, izin verilen minimum ve maksimum tarihi tanımlayan `Min`, `Max` veya `Range` kurallarının argümanları için de geçerlidir. +Varsayılan değer olarak `DateTimeInterface` uygulayan nesneleri, zaman içeren bir dizeyi ya da UNIX zaman damgasını temsil eden bir sayıyı kabul eder. Aynısı, izin verilen en küçük ve en büyük tarih ile zamanı tanımlayan `Min`, `Max` ya da `Range` kurallarının argümanları için de geçerlidir. ```php -$form->addDateTime('datetime', 'Tarih ve Saat:') +$form->addDateTime('datetime', 'Tarih ve saat:') ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Tarih en az bir ay önce olmalıdır.', new DateTime('-1 month')); + ->addRule($form::Min, 'Tarih en az bir ay öncesine ait olmalıdır.', new DateTime('-1 month')); ``` -Standart olarak `DateTimeImmutable` nesnesi döndürür, `setFormat()` metoduyla [metin biçimi|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] veya zaman damgası belirtebilirsiniz: +Varsayılan olarak bir `DateTimeImmutable` nesnesi döndürür. `setFormat()` metoduyla bir [metin biçimi|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ya da zaman damgası belirtebilirsiniz: ```php $form->addDateTime('datetime') @@ -328,10 +329,10 @@ $form->addDateTime('datetime') ``` -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== +addColor(string $name, $label=null): ColorPicker .[method]{data-version:3.1.14} +=============================================================================== -Renk seçmek için bir alan ekler (sınıf [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Renk `#rrggbb` biçiminde bir dizedir. Kullanıcı seçim yapmazsa, siyah renk `#000000` döndürülür. +Renk seçme alanı ekler ([ColorPicker |api:Nette\Forms\Controls\ColorPicker] sınıfı). Renk, `#rrggbb` biçiminde bir dize olarak döndürülür. Kullanıcı bir seçim yapmazsa siyah `#000000` döndürür. ```php $form->addColor('color', 'Renk:') @@ -339,37 +340,46 @@ $form->addColor('color', 'Renk:') ``` -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= +addHidden(string $name, mixed $default=null): HiddenField .[method] +=================================================================== -Gizli bir alan ekler (sınıf [HiddenField |api:Nette\Forms\Controls\HiddenField]). +Gizli bir alan ekler ([HiddenField |api:Nette\Forms\Controls\HiddenField] sınıfı). ```php $form->addHidden('userid'); ``` -`setNullable()` kullanarak boş dize yerine `null` döndürmesini ayarlayabilirsiniz. Gönderilen değeri değiştirmek [addFilter() |validation#Girişi Değiştirme] ile mümkündür. +Boş dize yerine `null` döndürmesi için `setNullable()` kullanın. [addFilter() |validation#Girdi Değerlerini Değiştirme] metodu, gönderilen değeri değiştirmeye olanak tanır. -Eleman gizli olsa da, değerin hala bir saldırgan tarafından değiştirilebileceğini veya sahtesinin yapılabileceğini **unutmamak önemlidir**. Veri manipülasyonuyla ilgili güvenlik risklerini önlemek için sunucu tarafında alınan tüm değerleri her zaman dikkatlice doğrulayın ve geçerleyin. +Öğe gizli olsa da, **şunu fark etmek önemlidir**: değeri yine de bir saldırgan tarafından değiştirilebilir ya da taklit edilebilir. Veri manipülasyonuyla ilgili güvenlik risklerini önlemek için alınan tüm değerleri sunucu tarafında her zaman titizlikle doğrulayın. -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== +addSubmit(string $name, $caption=null): SubmitButton .[method] +============================================================== -Bir gönderme düğmesi ekler (sınıf [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). +Bir gönder düğmesi ekler ([SubmitButton |api:Nette\Forms\Controls\SubmitButton] sınıfı). ```php $form->addSubmit('submit', 'Gönder'); ``` -Formda birden fazla gönderme düğmesi olabilir: +.{data-version:3.3.0} +İşleyici, `onClick` olayına bağlanmak yerine doğrudan düğmeye üçüncü parametre `$onSubmit` olarak verilebilir: + +```php +$form->addSubmit('submit', 'Gönder', function (SubmitButton $button, $data): void { + // ... +}); +``` + +Formda birden çok gönder düğmesi bulunabilir: ```php $form->addSubmit('register', 'Kaydol'); $form->addSubmit('cancel', 'İptal'); ``` -Hangisine tıklandığını öğrenmek için şunu kullanın: +Hangisine tıklandığını belirlemek için şunu kullanın: ```php if ($form['register']->isSubmittedBy()) { @@ -377,36 +387,36 @@ if ($form['register']->isSubmittedBy()) { } ``` -Düğmeye basıldığında tüm formu doğrulamak istemiyorsanız (örneğin *İptal* veya *Önizleme* düğmeleri için), [setValidationScope() |validation#Doğrulamayı Devre Dışı Bırakma] kullanın. +Bir düğmeye basıldığında formun tamamını doğrulamak istemiyorsanız (örneğin *İptal* ya da *Önizleme* düğmelerinde), [setValidationScope() |validation#Doğrulamayı Kapatma] kullanın. -addButton(string|int $name, $caption): Button .[method] -======================================================= +addButton(string $name, $caption=null): Button .[method] +======================================================== -Gönderme işlevi olmayan bir düğme ekler (sınıf [Button |api:Nette\Forms\Controls\Button]). Bu nedenle, başka bir işlev için kullanılabilir, örneğin tıklandığında bir JavaScript fonksiyonunu çağırmak için. +Gönderme işlevi olmayan bir düğme ekler ([Button |api:Nette\Forms\Controls\Button] sınıfı). Bu yüzden başka işlevler için, örneğin tıklandığında bir JavaScript fonksiyonu çağırmak için kullanılabilir. ```php -$form->addButton('raise', 'Maaşı Artır') +$form->addButton('raise', 'Maaşı artır') ->setHtmlAttribute('onclick', 'raiseSalary()'); ``` -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= +addImageButton(string $name, ?string $src=null, ?string $alt=null): ImageButton .[method] +========================================================================================= -Resim şeklinde bir gönderme düğmesi ekler (sınıf [ImageButton |api:Nette\Forms\Controls\ImageButton]). +Görsel biçiminde bir gönder düğmesi ekler ([ImageButton |api:Nette\Forms\Controls\ImageButton] sınıfı). ```php -$form->addImageButton('submit', '/path/to/image'); +$form->addImageButton('submit', '/path/to/image.png', 'Gönder'); ``` -Birden fazla gönderme düğmesi kullanırken, hangisine tıklandığını `$form['submit']->isSubmittedBy()` kullanarak öğrenebilirsiniz. +Birden çok gönder düğmesi kullanırken hangisine tıklandığını `$form['submit']->isSubmittedBy()` ile belirleyebilirsiniz. addContainer(string|int $name): Container .[method] =================================================== -Forma bir alt form (sınıf [Container|api:Nette\Forms\Container]), yani bir konteyner ekler, buna forma eklediğimiz gibi diğer elemanları ekleyebiliriz. `setDefaults()` veya `getValues()` metotları da çalışır. +Bir alt form ([Container|api:Nette\Forms\Container] sınıfı), yani bir container ekler; içine, forma eklendiği gibi başka öğeler eklenebilir. `setDefaults()` ya da `getValues()` gibi metotlar da çalışır. ```php $sub1 = $form->addContainer('first'); @@ -418,7 +428,7 @@ $sub2->addText('name', 'Adınız:'); $sub2->addEmail('email', 'E-posta:'); ``` -Gönderilen verileri daha sonra çok boyutlu bir yapı olarak döndürür: +Gönderilen veri o zaman çok boyutlu bir yapı olarak döndürülür: ```php [ @@ -434,69 +444,67 @@ Gönderilen verileri daha sonra çok boyutlu bir yapı olarak döndürür: ``` -Ayarların Özeti -=============== +Ayarlara Genel Bakış +==================== -Tüm elemanlarda aşağıdaki metotları çağırabiliriz ([API dokümantasyonu|https://api.nette.org/forms/master/Nette/Forms/Controls.html] içinde tam liste): +Tüm öğelerde şu metotları çağırabiliriz (eksiksiz bir özet için [API belgelerine|https://api.nette.org/forms/master/Nette/Forms/Controls.html] bakın): .[table-form-methods language-php] -| `setDefaultValue($value)` | varsayılan değeri ayarlar +| `setDefaultValue($value)` | varsayılan değeri ayarlar | `getValue()` | geçerli değeri alır -| `setOmitted()` | [##Değerin Atlanması] -| `setDisabled()` | [##Elemanların Devre Dışı Bırakılması] +| `setOmitted()` | [#Atlanan Değerler] +| `setDisabled()` | [#Öğeleri Devre Dışı Bırakma] -Renderleme: +Render: .[table-form-methods language-php] -| `setCaption($caption)` | eleman etiketini değiştirir +| `setCaption($caption)` | öğenin etiketini değiştirir | `setTranslator($translator)` | [çevirmeni |rendering#Çeviri] ayarlar -| `setHtmlAttribute($name, $value)` | elementin [HTML niteliğini |rendering#HTML Nitelikleri] ayarlar +| `setHtmlAttribute($name, $value)` | eleman için bir [HTML niteliği |rendering#HTML Nitelikleri] ayarlar | `setHtmlId($id)` | HTML `id` niteliğini ayarlar -| `setHtmlType($type)` | HTML `type` niteliğini ayarlar -| `setHtmlName($name)` | HTML `name` niteliğini ayarlar -| `setOption($key, $value)` | [renderleme ayarları |rendering#Options] +| `setOption($key, $value)` | [render seçeneklerini ayarlar |rendering#Seçenekler] Doğrulama: .[table-form-methods language-php] -| `setRequired()` | [zorunlu eleman |validation] -| `addRule()` | [doğrulama kuralı |validation#Kurallar] ayarı -| `addCondition()`, `addConditionOn()` | [doğrulama koşulunu |validation#Koşullar] ayarlar -| `addError($message)` | [hata mesajı iletme |validation#İşleme Sırasındaki Hatalar] +| `setRequired()` | öğeyi [zorunlu |validation] yapar +| `addRule()` | bir [doğrulama kuralı |validation#Kurallar] ekler +| `addCondition()`, `addConditionOn()` | bir [doğrulama koşulu |validation#Koşullar] ayarlar +| `addError($message)` | [bir hata mesajı ekler |validation#İşleme Hataları] -`addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` elemanlarında aşağıdaki metotları çağırabiliriz: +`addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` öğelerinde şu metotlar çağrılabilir: .[table-form-methods language-php] -| `setNullable()` | getValue()'nin boş dize yerine `null` döndürüp döndürmeyeceğini ayarlar -| `setEmptyValue($value)` | boş dize olarak kabul edilen özel bir değer ayarlar -| `setMaxLength($length)` | izin verilen maksimum karakter sayısını ayarlar -| `addFilter($filter)` | [giriş düzenleme |validation#Girişi Değiştirme] +| `setNullable()` | getValue() metodunun boş dize yerine `null` döndürüp döndürmeyeceğini ayarlar +| `setEmptyValue($value)` | boş dize sayılan özel bir değer ayarlar +| `setMaxLength($length)` | izin verilen en fazla karakter sayısını ayarlar +| `addFilter($filter)` | [girdiyi değiştirir |validation#Girdi Değerlerini Değiştirme] -Değerin Atlanması -================= +Atlanan Değerler +================ -Kullanıcı tarafından doldurulan değerle ilgilenmiyorsak, `setOmitted()` kullanarak onu `$form->getValues()` metodunun sonucundan veya işleyicilere iletilen verilerden çıkarabiliriz. Bu, çeşitli kontrol şifreleri, antispam elemanları vb. için kullanışlıdır. +Kullanıcının doldurduğu değer bizi ilgilendirmiyorsa, `setOmitted()` ile onu `$form->getValues()` metodunun sonucundan ya da işleyicilere aktarılan veriden çıkarabiliriz. Bu, çeşitli parola doğrulama alanları, spam önleme öğeleri vb. için yararlıdır. ```php -$form->addPassword('passwordVerify', 'Kontrol için şifre:') - ->setRequired('Lütfen kontrol için şifreyi tekrar girin') - ->addRule($form::Equal, 'Şifreler eşleşmiyor', $form['password']) +$form->addPassword('passwordVerify', 'Parola tekrar:') + ->setRequired('Yazım hatası olmadığını denetlemek için parolanızı yeniden girin') + ->addRule($form::Equal, 'Parolalar eşleşmiyor', $form['password']) ->setOmitted(); ``` -Elemanların Devre Dışı Bırakılması -================================== +Öğeleri Devre Dışı Bırakma +========================== -Elemanlar `setDisabled()` ile devre dışı bırakılabilir. Böyle bir eleman kullanıcı tarafından düzenlenemez. +Öğeler `setDisabled()` ile devre dışı bırakılabilir. Devre dışı bir öğe kullanıcı tarafından düzenlenemez. ```php $form->addText('username', 'Kullanıcı adı:') ->setDisabled(); ``` -Devre dışı bırakılmış elemanlar tarayıcı tarafından sunucuya hiç gönderilmez, bu nedenle onları `$form->getValues()` fonksiyonu tarafından döndürülen verilerde bulamazsınız. Ancak `setOmitted(false)` ayarlarsanız, Nette bu verilere varsayılan değerlerini dahil eder. +Devre dışı öğeler tarayıcı tarafından sunucuya hiç gönderilmez, dolayısıyla onları `$form->getValues()` fonksiyonunun döndürdüğü veride bulamazsınız. Ancak `setOmitted(false)` ayarlarsanız, Nette varsayılan değerlerini bu veriye katar. -`setDisabled()` çağrıldığında, güvenlik nedeniyle elemanın değeri **silinir**. Varsayılan bir değer ayarlıyorsanız, bunu devre dışı bıraktıktan sonra yapmanız gerekir: +`setDisabled()` çağrıldığında, güvenlik nedeniyle **öğenin değeri temizlenir**. Bir varsayılan değer ayarlıyorsanız, bunu devre dışı bıraktıktan sonra yapmanız gerekir: ```php $form->addText('username', 'Kullanıcı adı:') @@ -504,42 +512,26 @@ $form->addText('username', 'Kullanıcı adı:') ->setDefaultValue($userName); ``` -Devre dışı bırakılmış elemanlara alternatif olarak, tarayıcının sunucuya gönderdiği `readonly` HTML niteliğine sahip elemanlar vardır. Eleman yalnızca okunabilir olsa da, değerinin hala bir saldırgan tarafından değiştirilebileceğini veya sahtesinin yapılabileceğini **unutmamak önemlidir**. +Devre dışı öğelere bir alternatif, tarayıcının sunucuya gönderdiği HTML `readonly` niteliğine sahip öğelerdir. Öğe salt okunur olsa da, **şunu fark etmek önemlidir**: değeri yine de bir saldırgan tarafından değiştirilebilir ya da taklit edilebilir. -Özel Elemanlar -============== +Özel Öğeler +=========== -Geniş yerleşik form elemanları yelpazesinin yanı sıra, forma şu şekilde özel elemanlar ekleyebilirsiniz: +Geniş yerleşik form öğesi yelpazesinin yanı sıra forma özel öğeler de ekleyebilirsiniz: ```php $form->addComponent(new DateInput('Tarih:'), 'date'); -// alternatif sözdizimi: $form['date'] = new DateInput('Tarih:'); +// alternatif söz dizimi: $form['date'] = new DateInput('Tarih:'); ``` -.[note] -Form, [Container |component-model:#Container] sınıfının bir alt sınıfıdır ve bireysel elemanlar [Component |component-model:#Component] sınıfının alt sınıflarıdır. - -Özel elemanlar eklemek için formun yeni metotlarını (örneğin `$form->addZip()`) tanımlamanın bir yolu vardır. Buna extension methods denir. Dezavantajı, editörlerde kod tamamlama özelliğinin onlar için çalışmamasıdır. - -```php -use Nette\Forms\Container; - -// addZip(string $name, ?string $label = null) metodunu ekliyoruz -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'En az 5 rakam', '[0-9]{5}'); -}); - -// kullanım -$form->addZip('zip', 'Posta kodu:'); -``` +Böyle bir öğenin, gönderilen veriyi okuma, doğrulama ve render dahil, nasıl yazılacağı [ayrı bir bölümde |custom-controls] anlatılıyor. Orada ayrıca, `$form->addZip()` gibi kendi ekleme metodunuzu yazmanızı sağlayan genişletme metotlarını da öğreneceksiniz. -Düşük Seviyeli Elemanlar -======================== +Düşük Düzeyli Alanlar +===================== -Yalnızca şablonda yazdığımız ve `$form->addXyz()` metotlarından biriyle forma eklemediğimiz elemanları da kullanabilirsiniz. Örneğin, veritabanından kayıtları listelerken ve kaç tane olacağını ve ID'lerinin ne olacağını önceden bilmediğimizde ve her satırda bir onay kutusu veya radyo düğmesi görüntülemek istediğimizde, onu şablonda kodlamak yeterlidir: +Yalnızca şablonda yazılan ve `$form->addXyz()` metotlarından hiçbiriyle forma eklenmeyen öğeleri de kullanmak olanaklıdır. Örneğin veritabanından kayıtları listelerken, kaç tane olacağını ya da ID'lerinin ne olacağını önceden bilmediğimizde ve her satır için bir onay kutusu veya radyo düğmesi göstermek istediğimizde, bunu şablonda basitçe kodlayabiliriz: ```latte {foreach $items as $item} @@ -547,13 +539,13 @@ Yalnızca şablonda yazdığımız ve `$form->addXyz()` metotlarından biriyle f {/foreach} ``` -Ve gönderdikten sonra değeri öğreniriz: +Ve gönderimden sonra değeri alırız: ```php $data = $form->getHttpData($form::DataText, 'sel[]'); $data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); ``` -burada ilk parametre eleman türüdür (`DataFile` `type=file` için, `DataLine` `text`, `password`, `email` vb. gibi tek satırlık girdiler için ve `DataText` diğer tümü için) ve ikinci parametre `sel[]` HTML name niteliğine karşılık gelir. Eleman türünü, elemanların anahtarlarını koruyan `DataKeys` değeriyle birleştirebiliriz. Bu özellikle `select`, `radioList` ve `checkboxList` için kullanışlıdır. +Burada ilk parametre elemanın türüdür (`type=file` için `DataFile`, `text`, `password`, `email` gibi tek satırlık girdiler için `DataLine`, diğerlerinin tümü için `DataText`), ikinci parametre `sel[]` ise HTML name niteliğine karşılık gelir. Eleman türünü, elemanların anahtarlarını koruyan `DataKeys` değeriyle birleştirebiliriz. Bu özellikle `select`, `radioList` ve `checkboxList` için yararlıdır. -Önemli olan, `getHttpData()`'nın temizlenmiş bir değer döndürmesidir, bu durumda her zaman geçerli UTF-8 dizelerinden oluşan bir dizi olacaktır, saldırgan sunucuya ne göndermeye çalışırsa çalışsın. Bu, doğrudan `$_POST` veya `$_GET` ile çalışmaya benzer, ancak önemli farkla her zaman temiz veriler döndürmesidir, tıpkı standart Nette form elemanlarında alıştığınız gibi. +Can alıcı nokta: `getHttpData()` temizlenmiş bir değer döndürür. Bu durumda, bir saldırganın sunucuya ne göndermeye çalıştığından bağımsız olarak, her zaman geçerli UTF-8 dizelerinden oluşan bir dizi olacaktır. Bu, doğrudan `$_POST` ya da `$_GET` ile çalışmaya benzer, ama önemli bir farkla: her zaman temiz veri döndürür; tıpkı standart Nette form öğelerinde alışık olduğunuz gibi. diff --git a/forms/tr/custom-controls.texy b/forms/tr/custom-controls.texy new file mode 100644 index 0000000000..46d43c9da0 --- /dev/null +++ b/forms/tr/custom-controls.texy @@ -0,0 +1,268 @@ +Özel Form Öğeleri +***************** + +.[perex] +Nette geniş bir [yerleşik form öğesi |controls] paleti sunar. Ama aralarında bulunmayan bir gereksinimle karşılaştığınızda hiçbir şeyi dolambaçlı yollarla çözmeniz ya da bir şeyleri birbirine yapıştırmanız gerekmez: kendi öğenizi yazarsınız. O da yerleşik olanların yapabildiği her şeyi yapabilecek: doğrulama, kendini çevirme, render; ve tam olarak aynı biçimde kullanılacak. + +Bunu pratik bir örnekle göstereceğiz: üç alan (gün, ay ve yıl) kullanarak tarih girmeye yarayan bir öğe. Yol boyunca, öğe yazmak hakkında bilmeniz gereken her şeyi öğreneceksiniz. + + +Ne Zaman Özel Öğe Yazmalı, Ne Zaman Yazmamalı +============================================= + +Özel öğe, formların sunduğu en güçlü araçtır. Ve her güçlü araç gibi, ilk değil son seçenek olmalıdır. Pek çok durum daha basit yollarla çözülebilir: + +- **Bir değeri değiştirmeyi** [addFilter() |validation#Girdi Değerlerini Değiştirme] üstlenir. Posta kodunda boşluklara ya da bir kodda küçük harflere göz yummak mı istiyorsunuz? Bir filtre birkaç satırdır. +- **Yinelenen yapılandırmayı** özel bir ekleme metodu sarmalar. On yerde aynı doğrulamayla posta kodu alanı mı ekliyorsunuz? Onlar için adlandırılmış bir kısayol oluşturun, [sonunda göstereceğiz |#Özel Ekleme Metodu]. +- **Birbiriyle ilişkili bir alan grubuna** bir [container |controls#addContainer()] hizmet eder. Sokak, şehir ve posta kodundan oluşan bir adres için özel öğe gerekmez, üç metin alanlı bir container yeter. +- **Farklı bir görünüm** [setHtmlType() |controls#addText()] ve HTML nitelikleriyle ya da [prototiplerle |rendering#Prototipler] elde edilir. + +Özel bir öğe, **özel bir değere** ihtiyaç duyduğunuz anda anlam kazanır: dışarıdan tek değerli tek bir alan gibi davranan, ama içeride birkaç input'tan oluşan ya da değeri gösterdiğinden farklı biçimde saklayan bir öğe. Üç alandan oluşan bir tarih. Haritaya tıklanarak seçilen koordinatlar. Otomatik tamamlamalı bir etiket girişi. + + +Bir Öğenin Anatomisi +==================== + +Her özel öğe, soyut [api:Nette\Forms\Controls\BaseControl] sınıfından türer. Ondan çok büyük miktarda hazır işlev miras alır: değer saklama, doğrulama kuralları ve koşulları, hata mesajları, çeviriler, HTML nitelikleri, etiket ve render bağlantısı. Siz yalnızca öğenizi farklı kılan şeyi yazarsınız. + +Çalışan en küçük öğe şaşırtıcı derecede kısadır: + +```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, + ]); + } +} +``` + +İki metot: biri değerin gönderilen veriden nasıl alınacağını, öteki öğenin nasıl render edileceğini söyler. Birazdan ikisine de yakından bakacağız. Geri kalan her şey (`setRequired()`, `addRule()`, `setDefaultValue()`, çeviriler) kendiliğinden çalışır. + +Öğeyi forma `addComponent()` metoduyla ya da daha kısaca köşeli parantezlerle eklersiniz: + +```php +$form['nickname'] = new SimpleInput('Takma ad:'); +``` + + +Bir Öğenin Yaşam Döngüsü +======================== + +Daha ilginç bir öğeye geçmeden önce, bir öğeye ne zaman ne olduğunu bilmek iyidir. Form ve öğeleri, bir ağaç oluşturan [bileşenlerdir |component-model:]. Bunun hoş bir sonucu var: öğenin hiçbir şeyi kendi başına bulması gerekmez, önemli olan her şeyi framework doğru anda halleder: + +1) Öğeyi gönderilmiş bir forma eklediğiniz anda, formun kendisi onda `loadHttpData()` metodunu çağırır. Öğe orada, birazdan göstereceğimiz gibi, gönderilen değerini okur. Asla doğrudan `$_POST` ile çalışmaz ve container'ların içinde iç içe olup olmadığını hiç dert etmez. + +2) Form gönderildiğinde doğrulama gerçekleşir: `addRule()` ile eklenen kurallar, `getValue()` metodundan gelen değerle çalışarak değerlendirilir. + +3) Sonra `$form->getValues()` ya da öğede `getValue()` çağıran kişi, temiz ve türlenmiş bir değer alır; örneğin formdan gelen üçlü dize değil, bir `DateTimeImmutable` nesnesi. + +Render sırasında ise `getControl()`, etiket içinse `getLabel()` çağrılır. + + +Gönderilen Değeri Okuma +======================= + +`loadHttpData()` metodunda öğe, gönderilen değerini `getHttpData()` metoduyla ister. Parametresi, değerin nasıl temizleneceğini belirleyen bir türdür: + +| tür | anlamı +|------- +| `Form::DataLine` | tek satırlı metin: satır sonlarını boşlukla değiştirir, boşlukları kırpar +| `Form::DataText` | çok satırlı metin: satır sonlarını `\n` biçimine normalleştirir +| `Form::DataFile` | yükleme, bir `Nette\Http\FileUpload` örneği + +Bir saldırgan ne kadar uğraşırsa uğraşsın, sonuç her zaman denetim karakterleri olmayan geçerli bir UTF-8 dizesidir (ya da bir yükleme nesnesi veya `null`). Değeri doğrudan `$_POST` içinden okumamamızın nedeni tam da budur; tüm bu güvenceleri yitirirdik. + +Bizim tarihimiz gibi birkaç input'tan oluşan bir öğe, HTML adının bir parçasını ikinci parametre olarak verir ve tek tek alt değerlerini böyle okur. Onları kendi `$day`, `$month` ve `$year` dize özelliklerinde saklar: + +```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]') ?? ''; +} +``` + +HTML adı `[]` ile biterse bir değer dizisi döndürülür. `Form::DataKeys` türüyle birleştirerek (yani `Form::DataLine | Form::DataKeys`) anahtarlarını da korursunuz: + +```php +$tags = $this->getHttpData(Form::DataLine, '[tags][]'); +``` + +Eksik bir değer `null` olur (dizilerde boş dizi). İstek, öğenin verisini hiç içermek zorunda değildir; hiçbir şey bir saldırganın canının istediğini göndermesini engellemez. Örnekte `?? ''` eklememizin ve bu olasılığı her zaman hesaba katmanız gerektiğinin nedeni budur. + + +Öğenin Değeri +============= + +Öğe değerini tutar ve onu, sözleşmesine uyulması gereken üç metotla dışa açar. + +`setValue()` metodu programcıdan bir değer alır; `setDefaultValue()` ve `$form->setDefaults()` de bu yolu izler. Anlamlı olan her şeyi kabul etmeli, değeri iç biçimine dönüştürmeli ve anlamsız girdide istisna fırlatmalıdır; böylece hata, formun gizemli davranışlarıyla değil hemen ortaya çıkar. Bizim tarihimiz bir `DateTimeInterface`, bir dize, bir zaman damgası ya da `null` kabul eder ve bunları üç alana böler: + +```php +public function setValue(mixed $value): static +{ + if ($value === null) { + $this->day = $this->month = $this->year = ''; + } else { + $date = Nette\Utils\DateTime::from($value); // saçmalık istisna fırlatır + $this->day = $date->format('j'); + $this->month = $date->format('n'); + $this->year = $date->format('Y'); + } + return $this; +} +``` + +`getValue()` metodu ise temiz, türlenmiş bir değer kurar; öğenizin kullanıcısının göreceği tek şey budur. Değer geçerli değilse `null` döndürür. Statik `validateDate()` metodu yalnızca üç alanın var olan bir tarih oluşturup oluşturmadığını denetler: + +```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; +} +``` + +`isFilled()` metodu ise kullanıcının öğeyi doldurup doldurmadığını söyler; onu `setRequired()` kuralı kullanır. Varsayılan gerçekleştirim (boş olmayan bir değer) çoğu zaman yeter, ama bileşik bir öğede onu kendi mantığına göre geçersiz kılın: + +```php +public function isFilled(): bool +{ + return $this->day !== '' || $this->year !== ''; +} +``` + + +Render +====== + +`getControl()` metodu, öğenin HTML biçimini genellikle bir [Html |utils:html-elements] nesnesi olarak döndürür, ama düz bir dize de olur, fark etmez. Html nesnesine başlıca kodu kurarken başvururuz; çünkü ortaya çıkan işaretlemeyi güvenli ve keyifli bir API ile kurmamızı sağlar. Elinizin altında çeşitli yardımcılar var: + +- `getHtmlName()`, container'lardaki olası iç içe geçme de dahil olmak üzere HTML `name` niteliğini döndürür (örneğin `invoice[date]`). Bileşik bir öğede tek tek input'ların ad parçalarını buna eklersiniz: `$name . '[day]'`. +- `getHtmlId()`, etiketle bağlanan `id` niteliğini döndürür. +- `Helpers::exportRules($this->getRules())`, doğrulama kurallarını `data-nette-rules` niteliği için dışa aktarır; bu sayede [JavaScript doğrulaması |validation#JavaScript Doğrulaması] sizin öğenizde de çalışır. Nitelik, öğenin ilk input'una konur. +- `Helpers::createSelectBox($items, $optionAttrs, $selected)`, bir öğe dizisinden `<select>` elemanı kurar (iç içe diziler `<optgroup>` olarak render edilir) ve onu `Html` olarak döndürür; tarihimizin ay alanı için kullanışlıdır. +- `Helpers::createInputList($items, $inputAttrs, $labelAttrs)`, `<label>` içine sarılmış `<input>` elemanlarından oluşan bir liste (radyo düğmeleri ya da onay kutuları) üretir ve onu dize olarak döndürür. + +Tarihimizin ilk alanı böylece şöyle oluşturulur: + +```php +public function getControl(): Html +{ + $name = $this->getHtmlName(); + return Html::el() + ->addHtml(Html::el('input', [ + 'name' => $name . '[day]', + 'id' => $this->getHtmlId(), + 'value' => $this->day, + 'type' => 'number', + 'data-nette-rules' => Helpers::exportRules($this->getRules()) ?: null, + ])) + ->addHtml(/* ... ay için select ve yıl için input ... */); +} +``` + +Etiket `getLabel()` ile render edilir ve varsayılan gerçekleştirimi genellikle uygundur. Yalnızca dikkat: bileşik bir öğede onun `for` niteliği `getHtmlId()` değerine işaret eder, bu yüzden bu id'yi ilk input'a verin; tam da örnekteki gibi. + +Bileşik öğenin şablonda parça parça render edilebilmesi için (örneğin `{input birthdate:day}`), ilgili parça için `Html` elemanını döndüren `getControlPart($key)` ve `getLabelPart($key)` metotlarını geçersiz kılın; `CheckboxList` ve `RadioList` de böyle yapar. + +.[note] +`getControl()` metodunu geçersiz kılarsanız, `BaseControl::getControl()` metodunun aynı zamanda `setOption('rendered', true)` ile öğeyi render edilmiş olarak işaretlediğini unutmayın. Aynı formda elle ve otomatik render'ı birleştirdiğinizde onu da çağırın (ya da `parent::getControl()` çağırın); böylece öğe iki kez render edilmez. (Yukarıdaki `DateInput` örneği bunu kısalık için atlıyor.) + + +Eksiksiz Örnek: DateInput +========================= + +Anlatılan tüm parçalar bir arada, ayın seçilmesi için bir select box'la da tamamlanmış hâlde, bitmiş `DateInput` öğesinde, [doğrudan depodaki örnekler |https://github.com/nette/forms/blob/master/examples/custom-control.php] arasında bulunabilir. + +Öğenin yapıcıda kendisine, tarihin anlamlı olup olmadığını denetleyen bir doğrulama kuralı eklediğine dikkat edin. Böylece 31 Şubat gibi anlamsız bir girdi, sıradan bir form doğrulama hatası olarak ortaya çıkar: + +```php +public function __construct($label = null) +{ + parent::__construct($label); + $this->addRule(self::validateDate(...), 'Tarih geçersiz.'); +} +``` + +Peki kullanımı? Tam olarak yerleşik öğelerdeki gibi: + +```php +$form['birthdate'] = (new DateInput('Doğum tarihi:')) + ->setDefaultValue(new DateTime('2000-01-01')) + ->setRequired('Ne zaman doğdunuz?'); + +$date = $form->getValues()->birthdate; // ?DateTimeImmutable +``` + +Latte şablonunda onu, başka herhangi bir öğe gibi, alışıldık `{input birthdate}` ya da `{label birthdate /}` etiketiyle render edersiniz. + + +Doğrulama +========= + +Yerleşik doğrulama kuralları özel bir öğeyle hemen çalışır; `getValue()` metodundan gelen değer üzerinde işlem yaparlar. Böylece `DateInput` öğemiz örneğin izin verilen en eski tarih için `Form::Min` kullanabilir. JavaScript karşılığı da dahil olmak üzere kendi kurallarınızı nasıl yazacağınız [Özel kurallar ve koşullar |validation#Özel Kurallar ve Koşullar] bölümünde anlatılıyor. + + +Özel Ekleme Metodu +================== + +Yerleşik öğeleri `$form->addText()` ve benzeri elverişli metotlarla ekleriz. Özel bir öğenin böyle bir metodu yoktur, bu yüzden onu düz atamayla eklersiniz; hem formda hem container'da aynı şekilde çalışır ve düzenleyiciler ile statik çözümleme bunu anlar: + +```php +$form['birthdate'] = new DateInput('Doğum tarihi:'); +``` + +Otomatik tamamlamayı korurken eklemeyi kısaltmak isterseniz, doğrudan öğenin üzerindeki statik bir factory metodu işe yarar. `Form` sınıfının bir torunundaki metodun yapamayacağı şeyi, iç içe container'larda bile çalışmayı başarır; iç içe container'lar ondan habersizdir: + +```php +class DateInput extends Nette\Forms\Controls\BaseControl +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): self { + return $container[$name] = new self($label); + } +} + +// formda ve herhangi bir container'da çalışır: +DateInput::addTo($form, 'birthdate', 'Doğum tarihi:'); +``` + +Aynı yaklaşım, yerleşik bir öğenin yinelenen yapılandırması için adlandırılmış bir kısayol olarak da işe yarar: + +```php +final class ZipInput +{ + public static function addTo( + Nette\Forms\Container $container, + string $name, + ?string $label = null, + ): Nette\Forms\Controls\TextInput { + return $container->addText($name, $label) + ->addRule(Nette\Forms\Form::Pattern, 'Posta kodu tam olarak 5 rakam olmalıdır', '[0-9]{5}'); + } +} + +ZipInput::addTo($form, 'zip', 'Posta kodu:'); +``` diff --git a/forms/tr/in-presenter.texy b/forms/tr/in-presenter.texy index 80bf4ba256..d9b175fa9a 100644 --- a/forms/tr/in-presenter.texy +++ b/forms/tr/in-presenter.texy @@ -2,33 +2,33 @@ Presenter'larda Formlar *********************** .[perex] -Nette Forms, web formlarının oluşturulmasını ve işlenmesini önemli ölçüde kolaylaştırır. Bu bölümde, presenter'lar içinde formların kullanımını öğreneceksiniz. +Nette Forms, web formlarının oluşturulmasını ve işlenmesini belirgin biçimde kolaylaştırır. Bu bölümde formları presenter'ların içinde nasıl kullanacağınızı öğreneceksiniz. -Framework'ün geri kalanı olmadan tamamen bağımsız olarak nasıl kullanılacağını merak ediyorsanız, sizin için [bağımsız kullanım|standalone] kılavuzu bulunmaktadır. +Onları framework'ün geri kalanı olmadan tümüyle bağımsız kullanmak istiyorsanız, [bağımsız kullanım|standalone] için bir kılavuz var. İlk Form ======== -Basit bir kayıt formu yazmayı deneyelim. Kodu şöyle olacaktır: +Basit bir kayıt formu yazmayı deneyelim. Kodu şöyle olacak: ```php use Nette\Application\UI\Form; $form = new Form; -$form->addText('name', 'İsim:'); -$form->addPassword('password', 'Şifre:'); +$form->addText('name', 'Ad:'); +$form->addPassword('password', 'Parola:'); $form->addSubmit('send', 'Kaydol'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` -ve tarayıcıda şöyle görünecektir: +ve tarayıcıda şöyle görünecek: -[* form-cs.webp *] +[* form-en.webp *] -Presenter'daki form, `Nette\Application\UI\Form` sınıfının bir nesnesidir, öncülü `Nette\Forms\Form` bağımsız kullanım için tasarlanmıştır. Ona isim, şifre ve gönderme düğmesi olarak adlandırılan elemanları ekledik. Ve son olarak, `$form->onSuccess` satırı, gönderildikten ve başarılı bir şekilde doğrulandıktan sonra `$this->formSucceeded()` metodunun çağrılması gerektiğini söyler. +Presenter'daki bir form, `Nette\Application\UI\Form` sınıfının bir nesnesidir; öncülü `Nette\Forms\Form` ise bağımsız kullanım içindir. name, password adlı öğeleri ve bir gönder düğmesi ekledik. Son olarak `$form->onSuccess` satırı, gönderimden ve başarılı doğrulamadan sonra `$this->formSucceeded()` metodunun çağrılacağını belirtir. -Presenter açısından form, sıradan bir bileşendir. Bu nedenle, bir bileşen olarak ele alınır ve [fabrika metotları |application:components#Fabrika Metotları] kullanılarak presenter'a dahil edilir. Şöyle görünecektir: +Presenter'ın bakış açısından form sıradan bir bileşendir. Bu yüzden bileşen olarak ele alınır ve presenter'a bir [factory metoduyla |application:components#Factory metotları] katılır. Şöyle görünecek: ```php .{file:app/Presentation/Home/HomePresenter.php} use Nette; @@ -39,25 +39,25 @@ class HomePresenter extends Nette\Application\UI\Presenter protected function createComponentRegistrationForm(): Form { $form = new Form; - $form->addText('name', 'İsim:'); - $form->addPassword('password', 'Şifre:'); + $form->addText('name', 'Ad:'); + $form->addPassword('password', 'Parola:'); $form->addSubmit('send', 'Kaydol'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { - // burada form tarafından gönderilen verileri işleyeceğiz - // $data->name ismi içerir - // $data->password şifreyi içerir + // formun gönderdiği veriyi burada işleyeceğiz + // $data->name adı içerir + // $data->password parolayı içerir $this->flashMessage('Başarıyla kaydoldunuz.'); $this->redirect('Home:'); } } ``` -Ve şablonda formu `{control}` etiketiyle render ederiz: +Şablonda ise form `{control}` etiketiyle render edilir: ```latte .{file:app/Presentation/Home/default.latte} <h1>Kayıt</h1> @@ -65,45 +65,47 @@ Ve şablonda formu `{control}` etiketiyle render ederiz: {control registrationForm} ``` -Ve aslında hepsi bu :-) Çalışan ve mükemmel [güvenli |#Güvenlik Açıklarına Karşı Koruma] bir formumuz var. +Ve aslında hepsi bu :-) İşleyen ve kusursuz biçimde [güvenli |#Açıklara Karşı Koruma] bir formumuz var. -Ve şimdi muhtemelen bunun çok hızlı olduğunu düşünüyorsunuz, `formSucceeded()` metodunun nasıl çağrıldığını ve aldığı parametrelerin ne olduğunu merak ediyorsunuz. Evet, haklısınız, bu açıklama gerektiriyor. +Şimdi muhtemelen bunun çok hızlı olduğunu düşünüyor, `formSucceeded()` metodunun nasıl çağrıldığını ve hangi parametreleri aldığını merak ediyorsunuz. Evet, haklısınız, bu bir açıklamayı hak ediyor. -Nette, [Hollywood tarzı |application:components#Hollywood Tarzı] dediğimiz taze bir mekanizma ile birlikte gelir. Bir geliştirici olarak sürekli bir şeylerin olup olmadığını sormak yerine ("form gönderildi mi?", "geçerli bir şekilde gönderildi mi?" ve "sahtesi yapılmadı mı?"), framework'e "form geçerli bir şekilde doldurulduğunda, bu metodu çağır" dersiniz ve geri kalan işi ona bırakırsınız. JavaScript'te programlama yapıyorsanız, bu programlama tarzını yakından tanırsınız. Belirli bir [olay |nette:glossary#Olaylar Events] gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara ilgili argümanları iletir. +Nette, [Hollywood stili |application:components#Hollywood tarzı] adlı ferahlatıcı bir düzenek getirir. Geliştirici olarak sizin sürekli bir şey olup olmadığını sormanız ("form gönderildi mi?", "geçerli biçimde mi gönderildi?", "sahte değil mi?") yerine, framework'e "form geçerli biçimde doldurulduğunda şu metodu çağır" dersiniz ve sonraki işi ona bırakırsınız. JavaScript programlıyorsanız bu programlama stilini yakından biliyorsunuzdur. Belirli bir [olay |nette:glossary#Olaylar] gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara uygun argümanları aktarır. -Yukarıdaki presenter kodu tam olarak bu şekilde oluşturulmuştur. `$form->onSuccess` dizisi, form gönderildiğinde ve doğru bir şekilde doldurulduğunda (yani geçerli olduğunda) Nette'nin çağıracağı PHP geri aramalarının (callback) bir listesini temsil eder. [Presenter yaşam döngüsü |application:presenters#Presenter Yaşam Döngüsü] çerçevesinde, bu sözde bir sinyaldir, yani `action*` metodundan sonra ve `render*` metodundan önce çağrılırlar. Ve her geri aramaya ilk parametre olarak formun kendisini ve ikinci parametre olarak gönderilen verileri [ArrayHash |utils:arrays#ArrayHash] nesnesi şeklinde iletir. Form nesnesine ihtiyacınız yoksa ilk parametreyi atlayabilirsiniz. Ve ikinci parametre daha akıllı olabilir, ancak bunun hakkında [daha sonra |#Sınıflara Eşleme] konuşacağız. +Yukarıdaki presenter kodu tam olarak böyle kurulmuştur. `$form->onSuccess` dizisi, formun gönderildiği ve doğru doldurulduğu (yani geçerli olduğu) anda Nette'in çağırdığı PHP callback'lerinin listesini temsil eder. [Presenter yaşam döngüsünde |application:presenters#Presenter'ın yaşam döngüsü] bu bir sinyaldir, dolayısıyla `action*` metodundan sonra ve `render*` metodundan önce çağrılırlar. Ve her callback'e ilk parametre olarak formun kendisini, ikinci parametre olarak da gönderilen veriyi bir [ArrayHash |utils:arrays#ArrayHash] nesnesi (ya da stdClass veya özel bir sınıf) olarak aktarır. Form nesnesine ihtiyacınız yoksa ilk parametreyi atlayabilirsiniz. İkinci parametre daha akıllı olabilir, ama bunun ayrıntısı [daha sonra |#Sınıflara Eşleme]. -`$data` nesnesi, kullanıcının doldurduğu verilerle `name` ve `password` anahtarlarını içerir. Genellikle verileri doğrudan daha fazla işleme göndeririz, bu örneğin veritabanına ekleme olabilir. Ancak işleme sırasında bir hata oluşabilir, örneğin kullanıcı adı zaten alınmış olabilir. Bu durumda, hatayı `addError()` kullanarak forma geri iletiriz ve hata mesajıyla birlikte yeniden render edilmesini sağlarız. +`$data` nesnesi, kullanıcının girdiği verilerle birlikte `name` ve `password` özelliklerini içerir. Genellikle veriyi doğrudan ileri işleme göndeririz; bu örneğin veritabanına ekleme olabilir. Ancak işleme sırasında bir hata oluşabilir, örneğin kullanıcı adı zaten alınmış olabilir. Böyle bir durumda hatayı `addError()` ile forma geri aktarır ve hata mesajıyla birlikte yeniden render edilmesini sağlarız. ```php $form->addError('Üzgünüz, bu kullanıcı adı zaten kullanılıyor.'); ``` -`onSuccess` dışında bir de `onSubmit` vardır: geri aramalar, form doğru doldurulmamış olsa bile her zaman form gönderildikten sonra çağrılır. Ve ayrıca `onError`: geri aramalar yalnızca gönderim geçerli değilse çağrılır. `onSuccess` veya `onSubmit` içinde formu `addError()` ile geçersiz kılsak bile çağrılırlar. +`onSuccess` dışında `onSubmit` de vardır: callback'leri, form doğru doldurulmamış olsa bile her gönderildiğinde çağrılır. Bir de `onError` vardır: callback'leri yalnızca gönderim geçerli değilse çağrılır. `onSuccess` içinde `addError()` ile formu geçersiz kılarsak bile çağrılırlar. -Formu işledikten sonra bir sonraki sayfaya yönlendiririz. Bu, *yenile*, *geri* düğmesiyle veya tarayıcı geçmişinde gezinerek formun istenmeyen şekilde yeniden gönderilmesini önler. +Formu işledikten sonra başka bir sayfaya yönlendiririz. Bu, *yenile* ya da *geri* düğmesi veya tarayıcı geçmişinde gezinme yoluyla formun istenmeden yeniden gönderilmesini önler. -Diğer [form elemanları|controls] eklemeyi deneyin. +Form AJAX ile gönderilirse, yönlendirme yerine genellikle yeniden render edilmiş formu içeren bir [snippet |application:ajax] yeniden çizersiniz. +Başka [form öğeleri|controls] eklemeyi deneyin. -Elemanlara Erişim -================= -Form, presenter'ın bir bileşenidir, bizim durumumuzda `registrationForm` olarak adlandırılmıştır (fabrika metodu `createComponentRegistrationForm` adına göre), bu nedenle presenter'ın herhangi bir yerinde forma şu şekilde erişebilirsiniz: +Öğelere Erişim +============== + +Form, presenter'ın bir bileşenidir; bizim örneğimizde `registrationForm` adını taşır (factory metodunun adı `createComponentRegistrationForm` olduğundan), dolayısıyla presenter'ın herhangi bir yerinde forma şöyle erişebilirsiniz: ```php $form = $this->getComponent('registrationForm'); -// alternatif sözdizimi: $form = $this['registrationForm']; +// alternatif söz dizimi: $form = $this['registrationForm']; ``` -Bireysel form elemanları da bileşenlerdir, bu nedenle onlara aynı şekilde erişebilirsiniz: +Tek tek form öğeleri de bileşendir, bu yüzden onlara da aynı şekilde erişebilirsiniz: ```php -$input = $form->getComponent('name'); // veya $input = $form['name']; -$button = $form->getComponent('send'); // veya $button = $form['send']; +$input = $form->getComponent('name'); // ya da $input = $form['name']; +$button = $form->getComponent('send'); // ya da $button = $form['send']; ``` -Elemanlar unset ile kaldırılır: +Öğeler `unset` ile kaldırılır: ```php unset($form['name']); @@ -113,26 +115,26 @@ unset($form['name']); Doğrulama Kuralları =================== -*Geçerli* kelimesi geçti, ancak formun henüz herhangi bir doğrulama kuralı yok. Bunu düzeltelim. +*Geçerli* sözcüğü geçti, ama formun henüz hiçbir doğrulama kuralı yok. Bunu düzeltelim. -İsim zorunlu olacak, bu yüzden onu `setRequired()` metoduyla işaretleyeceğiz, argümanı kullanıcı ismi doldurmazsa görüntülenecek hata mesajının metnidir. Argüman belirtmezsek, varsayılan hata mesajı kullanılır. +Ad zorunlu olacak, bu yüzden onu `setRequired()` metoduyla işaretliyoruz. Argümanı, kullanıcı adı doldurmazsa görüntülenecek hata mesajının metnidir. Argüman atlanırsa varsayılan hata mesajı kullanılır. ```php -$form->addText('name', 'İsim:') - ->setRequired('Lütfen ismi girin'); +$form->addText('name', 'Ad:') + ->setRequired('Lütfen adınızı girin.'); ``` -Formu doldurulmuş isim olmadan göndermeyi deneyin ve bir hata mesajının görüntülendiğini ve tarayıcının veya sunucunun alanı doldurana kadar reddedeceğini göreceksiniz. +Formu adı doldurmadan göndermeyi deneyin; bir hata mesajının göründüğünü ve tarayıcının ya da sunucunun siz alanı doldurana dek onu reddettiğini göreceksiniz. -Aynı zamanda, sisteme sadece boşluk yazarak hile yapamazsınız. Hayır. Nette sol ve sağ boşlukları otomatik olarak kaldırır. Deneyin. Bu, her tek satırlık girişle her zaman yapmanız gereken bir şeydir, ancak genellikle unutulur. Nette bunu otomatik olarak yapar. (Formu aldatmayı deneyebilir ve isim olarak çok satırlı bir dize gönderebilirsiniz. Nette burada da aldanmaz ve satır sonlarını boşluklara dönüştürür.) +Aynı zamanda, örneğin alana yalnızca boşluk girerek sistemi kandıramazsınız. Mümkün değil. Nette, baştaki ve sondaki boşlukları otomatik olarak kırpar. Deneyin. Bu, her tek satırlık girdide her zaman yapmanız gereken, ama sık sık unutulan bir şeydir. Nette onu otomatik yapar. (Formu kandırıp ad olarak çok satırlı bir dize göndermeyi deneyebilirsiniz. Burada da Nette kanmaz ve satır sonları boşluğa dönüştürülür.) -Form her zaman sunucu tarafında doğrulanır, ancak aynı zamanda anında gerçekleşen ve kullanıcının hatayı formu sunucuya göndermeye gerek kalmadan hemen öğrendiği JavaScript doğrulaması da üretilir. Bu, `netteForms.js` betiği tarafından yapılır. Bunu layout şablonuna ekleyin: +Form her zaman sunucu tarafında doğrulanır, ama anında çalışan bir JavaScript doğrulaması da üretilir; böylece kullanıcı, formu sunucuya göndermeye gerek kalmadan hatayı hemen öğrenir. Bunu `netteForms.js` betiği üstlenir. Onu yerleşim şablonunuza ekleyin: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Form içeren sayfanın kaynak koduna bakarsanız, Nette'nin zorunlu elemanları `required` CSS sınıfına sahip elemanlara eklediğini fark edebilirsiniz. Şablona aşağıdaki stil sayfasını eklemeyi deneyin ve "İsim" etiketi kırmızı olacaktır. Bu şekilde kullanıcılara zorunlu elemanları zarifçe işaretleriz: +Formun bulunduğu sayfanın kaynak koduna bakarsanız, Nette'in zorunlu öğeleri `required` CSS sınıfına sahip elemanlarla sardığını fark edebilirsiniz. Şablonunuza aşağıdaki stil sayfasını eklemeyi deneyin; "Ad" etiketi kırmızı olacak. Bu, zorunlu alanları kullanıcılar için şık biçimde vurgular: ```latte <style> @@ -140,85 +142,87 @@ Form içeren sayfanın kaynak koduna bakarsanız, Nette'nin zorunlu elemanları </style> ``` -Diğer doğrulama kurallarını `addRule()` metoduyla ekleriz. İlk parametre kuraldır, ikincisi yine hata mesajının metnidir ve bunu doğrulama kuralının bir argümanı takip edebilir. Bununla ne kastediliyor? +Başka doğrulama kurallarını `addRule()` metoduyla ekleriz. İlk parametre kural, ikincisi yine hata mesajının metni, ardından da doğrulama kuralı için bir argüman gelebilir. Bu ne demek? -Formu, tamsayı olması gereken (`addInteger()`) ve ayrıca izin verilen bir aralıkta (`$form::Range`) olması gereken yeni isteğe bağlı "yaş" alanıyla genişleteceğiz. Ve burada tam olarak `addRule()` metodunun üçüncü parametresini kullanacağız, bununla doğrulayıcıya istenen aralığı `[başlangıç, bitiş]` çifti olarak ileteceğiz: +Formu, tam sayı olması (`addInteger()`) ve ayrıca izin verilen bir aralıkta bulunması (`$form::Range`) gereken yeni ve isteğe bağlı bir "yaş" alanıyla genişletelim. Burada, gereken aralığı doğrulayıcıya `[min, max]` çifti olarak aktarmak için `addRule()` metodunun üçüncü parametresini kullanacağız: ```php $form->addInteger('age', 'Yaş:') - ->addRule($form::Range, 'Yaş 18 ile 120 arasında olmalıdır', [18, 120]); + ->addRule($form::Range, 'Yaş 18 ile 120 arasında olmalıdır.', [18, 120]); ``` .[tip] -Kullanıcı alanı doldurmazsa, eleman isteğe bağlı olduğu için doğrulama kuralları kontrol edilmeyecektir. +Kullanıcı alanı doldurmazsa doğrulama kuralları denetlenmez, çünkü öğe isteğe bağlıdır. -Burada küçük bir yeniden düzenleme için yer var. Hata mesajında ve üçüncü parametrede sayılar yinelenmiştir, bu ideal değildir. Eğer [çok dilli formlar |rendering#Çeviri] oluşturuyor olsaydık ve sayıları içeren mesaj birden çok dile çevrilmiş olsaydı, değerlerin olası bir değişikliği zorlaşırdı. Bu nedenle, `%d` yer tutucularını kullanmak mümkündür ve Nette değerleri tamamlayacaktır: +Bu, küçük bir yeniden düzenlemeye yer açar. Hata mesajında ve üçüncü parametrede sayılar yineleniyor; bu ideal değil. [Çok dilli formlar |rendering#Çeviri] yapıyor olsaydık ve sayı içeren mesaj birden çok dile çevrilseydi, değerleri değiştirmek zorlaşırdı. Bu nedenle `%d` yer tutucuları kullanılabilir ve Nette değerleri yerine koyar: ```php - ->addRule($form::Range, 'Yaş %d ile %d arasında olmalıdır', [18, 120]); + ->addRule($form::Range, 'Yaş %d ile %d yaş arasında olmalıdır.', [18, 120]); ``` -Aynı zamanda zorunlu hale getireceğimiz ve ayrıca şifrenin minimum uzunluğunu (`$form::MinLength`) doğrulayacağımız `password` elemanına geri dönelim, yine yer tutucu kullanarak: +`password` öğesine dönelim, onu da zorunlu yapalım ve ayrıca en az parola uzunluğunu (`$form::MinLength`) doğrulayalım; yine mesajda bir yer tutucu kullanarak: ```php -$form->addPassword('password', 'Şifre:') - ->setRequired('Bir şifre seçin') - ->addRule($form::MinLength, 'Şifre en az %d karakter olmalıdır', 8); +$form->addPassword('password', 'Parola:') + ->setRequired('Bir parola seçin') + ->addRule($form::MinLength, 'Parolanız en az %d karakter uzunluğunda olmalıdır.', 8); ``` -Forma bir de `passwordVerify` alanı ekleyelim, burada kullanıcı kontrol için şifreyi tekrar girecektir. Doğrulama kurallarını kullanarak her iki şifrenin aynı olup olmadığını kontrol edeceğiz (`$form::Equal`). Ve parametre olarak ilk şifreye [köşeli parantezler |#Elemanlara Erişim] kullanarak bir referans vereceğiz: +Forma, kullanıcının parolayı doğrulama için yeniden girdiği `passwordVerify` adlı bir alan daha ekleyelim. Doğrulama kurallarıyla iki parolanın aynı olup olmadığını denetliyoruz (`$form::Equal`). Argüman olarak ilk parolaya [köşeli parantezlerle |#Öğelere Erişim] bir referans veriyoruz: ```php -$form->addPassword('passwordVerify', 'Kontrol için şifre:') - ->setRequired('Lütfen kontrol için şifreyi tekrar girin') - ->addRule($form::Equal, 'Şifreler eşleşmiyor', $form['password']) +$form->addPassword('passwordVerify', 'Parola tekrar:') + ->setRequired('Yazım hatası olmadığını denetlemek için parolanızı yeniden girin') + ->addRule($form::Equal, 'Parolalar eşleşmiyor.', $form['password']) ->setOmitted(); ``` -`setOmitted()` kullanarak, değerinin aslında bizim için önemli olmadığı ve yalnızca doğrulama amacıyla var olan elemanı işaretledik. Değer `$data`'ya iletilmez. +`setOmitted()` ile, değeri aslında bizi ilgilendirmeyen ve yalnızca doğrulama amacıyla var olan bir öğeyi işaretledik. Değeri `$data` içine aktarılmaz. -Böylece PHP ve JavaScript'te doğrulaması olan tamamen işlevsel bir formumuz oldu. Nette'nin doğrulama yetenekleri çok daha geniştir, koşullar oluşturabilir, bunlara göre sayfanın bölümlerini gösterebilir ve gizleyebilirsiniz vb. Her şeyi [form doğrulaması|validation] bölümünde öğreneceksiniz. +Böylece hem PHP hem JavaScript doğrulamalı, tam işleyen bir formumuz oldu. Nette'in doğrulama yetenekleri çok daha geniştir; koşullar oluşturulabilir, onlara göre sayfanın parçaları gösterilip gizlenebilir vb. Her şeyi [form doğrulama|validation] bölümünde öğreneceksiniz. Varsayılan Değerler =================== -Form elemanlarına genellikle varsayılan değerler atarız: +Form öğeleri için genellikle varsayılan değerler ayarlarız: ```php $form->addEmail('email', 'E-posta') ->setDefaultValue($lastUsedEmail); ``` -Genellikle tüm elemanlara aynı anda varsayılan değerler atamak kullanışlıdır. Örneğin, form kayıtları düzenlemek için kullanıldığında. Veritabanından kaydı okur ve varsayılan değerleri atarız: +Tüm öğeler için varsayılan değerleri aynı anda ayarlamak çoğu zaman işe yarar. Örneğin form kayıt düzenlemek için kullanıldığında. Kaydı veritabanından okur ve varsayılan değerleri ayarlarız: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -`setDefaults()`'u elemanları tanımladıktan sonra çağırın. +`setDefaults()` metodunu öğeleri tanımladıktan sonra çağırın. + +Zaten gönderilmiş bir formda `setDefaults()` etkisizdir; kullanıcının doldurduğunun üzerine yazmaz, bu yüzden onu form factory'sinde koşulsuz çağırmak güvenlidir. Değerleri gönderimden sonra da zorlamanız gerekiyorsa bunun yerine `setValues()` kullanın. Formun Render Edilmesi ====================== -Standart olarak form bir tablo olarak render edilir. Bireysel elemanlar temel erişilebilirlik kuralını karşılar - tüm etiketler `<label>` olarak yazılır ve ilgili form elemanıyla ilişkilendirilir. Etikete tıklandığında imleç otomatik olarak form alanında görünür. +Form varsayılan olarak bir tablo olarak render edilir. Tek tek öğeler temel web erişilebilirlik kurallarına uyar; tüm etiketler `<label>` elemanı olarak yazılır ve ilgili form öğeleriyle ilişkilendirilir. Etikete tıklamak imleci otomatik olarak form alanına odaklar. -Her elemana istediğimiz HTML niteliklerini atayabiliriz. Örneğin bir yer tutucu ekleyebiliriz: +Her öğe için istediğimiz HTML niteliklerini ayarlayabiliriz. Örneğin bir placeholder ekleyelim: ```php $form->addInteger('age', 'Yaş:') ->setHtmlAttribute('placeholder', 'Lütfen yaşı girin'); ``` -Formu render etmenin gerçekten çok sayıda yolu vardır, bu nedenle buna ayrılmış [renderleme hakkında ayrı bir bölüm|rendering] bulunmaktadır. +Bir formu render etmenin gerçekten pek çok yolu var, bu yüzden ona [ayrı bir render bölümü|rendering] ayrıldı. Sınıflara Eşleme ================ -İkinci parametre `$data`'da gönderilen verileri `ArrayHash` nesnesi olarak alan `formSucceeded()` metoduna geri dönelim. Bu, `stdClass` gibi genel bir sınıf olduğundan, onunla çalışırken belirli bir konfor eksikliği yaşayacağız, örneğin editörlerde özelliklerin önerilmesi veya statik kod analizi gibi. Bu, her form için özelliklerinin bireysel elemanları temsil ettiği belirli bir sınıfa sahip olarak çözülebilir. Örneğin: +`formSucceeded()` metoduna dönelim; ikinci `$data` parametresinde gönderilen veriyi bir `ArrayHash` nesnesi (ya da `stdClass`) olarak alıyor. Bu, `stdClass` gibi genel bir sınıf olduğundan, onunla çalışırken düzenleyicilerde özellik tamamlama ya da statik kod çözümlemesi gibi bazı kolaylıklardan yoksun kalırız. Bu, her form için, özellikleri tek tek öğeleri temsil eden özel bir sınıf yazılarak çözülebilir. Örneğin: ```php class RegistrationFormData @@ -229,7 +233,7 @@ class RegistrationFormData } ``` -Alternatif olarak, yapıcıyı kullanabilirsiniz: +Alternatif olarak bir yapıcı kullanabilirsiniz: ```php class RegistrationFormData @@ -243,9 +247,9 @@ class RegistrationFormData } ``` -Veri sınıfının özellikleri enum'lar da olabilir ve otomatik olarak eşlenirler. .{data-version:3.2.4} +Veri sınıfının özellikleri enum da olabilir ve otomatik olarak eşlenirler. .{data-version:3.2.4} -Nette'ye verileri bu sınıfın nesneleri olarak döndürmesini nasıl söyleriz? Düşündüğünüzden daha kolay. Sınıfı işleyici metodundaki `$data` parametresinin türü olarak belirtmek yeterlidir: +Nette'e veriyi bu sınıfın nesneleri olarak döndürmesini nasıl söyleriz? Sandığınızdan kolay. İşleyici metodunda `$data` parametresinin türü olarak yalnızca sınıfı belirtin: ```php public function formSucceeded(Form $form, RegistrationFormData $data): void @@ -256,16 +260,18 @@ public function formSucceeded(Form $form, RegistrationFormData $data): void } ``` -Tür olarak `array` de belirtebilirsiniz ve o zaman verileri bir dizi olarak iletir. +Tür olarak `array` de belirtebilirsiniz; o zaman veri dizi olarak aktarılır. -Benzer şekilde, sınıf adını veya hidratlanacak nesneyi parametre olarak ilettiğimiz `getValues()` fonksiyonunu da kullanabilirsiniz: +Benzer şekilde, parametre olarak sınıf adını ya da doldurulacak bir nesneyi vererek `getValues()` metodunu kullanabilirsiniz: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Formlar konteynerlerden oluşan çok seviyeli bir yapı oluşturuyorsa, her biri için ayrı bir sınıf oluşturun: +Değerleri form doğrulanmadan önce okumanız gerekiyorsa (tipik olarak bir `onValidate` işleyicisinin içinde), bunun yerine `getUntrustedValues()` metodunu kullanın. `getValues()` ile aynı parametreleri alır, ama gönderilen değerleri doğrulamadan geçtiklerini güvence altına almadan döndürür. + +Formlar container'lardan oluşan çok düzeyli bir yapıya sahipse, her biri için ayrı bir sınıf oluşturun: ```php $form = new Form; @@ -287,93 +293,95 @@ class RegistrationFormData } ``` -Eşleme daha sonra `$person` özelliğinin türünden, konteyneri `PersonFormData` sınıfına eşlemesi gerektiğini anlar. Eğer özellik bir konteyner dizisi içeriyorsa, `array` türünü belirtin ve eşleme için sınıfı doğrudan konteynere iletin: +Eşleme daha sonra `$person` özelliğinin türünden, container'ı `PersonFormData` sınıfına eşlemesi gerektiğini çıkarır. Özellik container dizisi içerecekse `array` türünü belirtin ve eşlenecek sınıfı doğrudan container'a verin: ```php $person->setMappedType(PersonFormData::class); ``` -Formun veri sınıfının tasarımını `Nette\Forms\Blueprint::dataClass($form)` metodunu kullanarak oluşturabilirsiniz, bu da onu tarayıcı sayfasına yazdırır. Kodu daha sonra tıklayarak işaretleyip projeye kopyalamak yeterlidir. .{data-version:3.1.15} +Formun veri sınıfı için bir taslağı, onu tarayıcı sayfasına yazdıran `Nette\Forms\Blueprint::dataClass($form)` metoduyla üretebilirsiniz. Sonra yalnızca tıklayıp kodu seçin ve projenize kopyalayın. .{data-version:3.1.15} -Birden Fazla Düğme -================== +Birden Çok Gönder Düğmesi +========================= -Formun birden fazla düğmesi varsa, genellikle hangisine basıldığını ayırt etmemiz gerekir. Her düğme için kendi işleyici fonksiyonumuzu oluşturabiliriz. Bunu [olay |nette:glossary#Olaylar Events] `onClick` için bir işleyici olarak ayarlayacağız: +Formun birden çok düğmesi varsa, genellikle hangisine basıldığını ayırt etmemiz gerekir. Her düğme için ayrı bir işleyici fonksiyon yazabiliriz. Onu `onClick` [olayının |nette:glossary#Olaylar] işleyicisi olarak ayarlayın: ```php $form->addSubmit('save', 'Kaydet') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Sil') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` -Bu işleyiciler, tıpkı `onSuccess` olayında olduğu gibi, yalnızca geçerli bir şekilde doldurulmuş form durumunda çağrılır. Fark, ilk parametre olarak form yerine gönderme düğmesinin iletilebilmesidir, belirttiğiniz türe bağlıdır: +.{data-version:3.3.0} +Bir işleyici, `addSubmit()` metodunun üçüncü argümanı olarak doğrudan düğmeye de verilebilir. + +Bu işleyiciler, tıpkı `onSuccess` olayı gibi, yalnızca form geçerli biçimde doldurulduğunda çağrılır (düğme için doğrulama kapatılmadıysa). Fark, belirttiğiniz tür bildirimine göre ilk parametre olarak form yerine gönder düğmesi nesnesinin aktarılabilmesidir: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... } ``` -Form <kbd>Enter</kbd> tuşuyla gönderildiğinde, ilk düğmeyle gönderilmiş gibi kabul edilir. +Form <kbd>Enter</kbd> tuşuna basılarak gönderildiğinde, ilk gönder düğmesiyle gönderilmiş gibi ele alınır. onAnchor Olayı ============== -Fabrika metodunda (örneğin `createComponentRegistrationForm` gibi) formu oluştururken, form henüz gönderilip gönderilmediğini veya hangi verilerle gönderildiğini bilmez. Ancak gönderilen değerleri bilmemiz gereken durumlar vardır, örneğin formun sonraki şekli bunlara bağlıdır veya bağımlı seçme kutuları (select box) için onlara ihtiyacımız vardır vb. +Bir formu factory metodunda kurduğunuzda (örneğin `createComponentRegistrationForm`), form henüz gönderilip gönderilmediğini ya da hangi veriyle gönderildiğini bilmez. Ancak gönderilen değerleri bilmemiz gereken durumlar vardır; belki formun görünümü onlara bağlıdır ya da birbirine bağlı seçim kutuları için gerekirler vb. -Bu nedenle, formu oluşturan kodun bir kısmını, yalnızca sözde demirlendiğinde, yani presenter ile zaten bağlantılı olduğunda ve gönderilen verilerini bildiğinde çağrılmasını sağlayabilirsiniz. Böyle bir kodu `$onAnchor` dizisine iletiriz: +Bu yüzden formu kuran kodun yalnızca form "demirlendiğinde", yani presenter'a bağlanıp gönderilen verisini bildiğinde çağrılmasını sağlayabilirsiniz. Böyle bir kodu `$onAnchor` dizisine koyun: ```php $country = $form->addSelect('country', 'Ülke:', $this->model->getCountries()); $city = $form->addSelect('city', 'Şehir:'); $form->onAnchor[] = function () use ($country, $city) { - // bu fonksiyon, formun gönderilip gönderilmediğini ve hangi verilerle gönderildiğini bildiğinde çağrılır - // bu nedenle getValue() metodu kullanılabilir + // bu fonksiyon, form hangi veriyle gönderildiğini bildiğinde çağrılır + // böylece getValue() metodunu kullanabilirsiniz $val = $country->getValue(); $city->setItems($val ? $this->model->getCities($val) : []); }; ``` -Güvenlik Açıklarına Karşı Koruma -================================ +Açıklara Karşı Koruma +===================== -Nette Framework güvenliğe büyük önem verir ve bu nedenle formların iyi bir şekilde korunmasına özen gösterir. Bunu tamamen şeffaf bir şekilde yapar ve manuel olarak hiçbir şey ayarlamayı gerektirmez. +Nette Framework güvenliğe büyük önem verir ve bu yüzden formların güvenliğini titizlikle sağlar. Bunu tümüyle saydam biçimde yapar ve elle hiçbir ayar gerektirmez. -Formları [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] ve [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF] saldırılarına karşı korumanın yanı sıra, sizin artık düşünmeniz gerekmeyen birçok küçük güvenlik önlemi alır. +Formları [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] ve [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)] gibi saldırılara karşı korumanın yanı sıra, artık düşünmenize gerek kalmayan pek çok küçük güvenlik önlemi de alır. -Örneğin, girdilerden tüm kontrol karakterlerini filtreler ve UTF-8 kodlamasının geçerliliğini kontrol eder, böylece formdan gelen veriler her zaman temiz olur. Seçme kutuları (select box) ve radyo listelerinde, seçilen öğelerin gerçekten sunulanlardan olduğunu ve sahtesinin yapılmadığını doğrular. Tek satırlık metin girişlerinde, saldırganın oraya göndermiş olabileceği satır sonu karakterlerini kaldırdığını zaten belirtmiştik. Çok satırlık girişlerde ise satır sonu karakterlerini normalleştirir. Ve böyle devam eder. +Örneğin girdilerdeki tüm denetim karakterlerini süzer ve UTF-8 kodlamasının geçerliliğini denetler; böylece formdan gelen verinin her zaman temiz olmasını sağlar. Seçim kutuları ve radyo listelerinde, seçilen öğelerin gerçekten sunulanlar arasında olduğunu ve hiçbir sahtecilik yapılmadığını doğrular. Tek satırlık metin girdilerinde, bir saldırganın gönderebileceği satır sonu karakterlerini boşlukla değiştirdiğini zaten söylemiştik. Çok satırlı girdilerde satır sonu karakterlerini normalleştirir. Ve böyle sürer. -Nette, birçok programcının varlığından bile haberdar olmadığı güvenlik risklerini sizin için çözer. +Nette, pek çok programcının var olduğunu bile bilmediği güvenlik risklerini sizin yerinize halleder. -Bahsedilen CSRF saldırısı, bir saldırganın kurbanı, kurbanın oturum açtığı sunucuya kurbanın tarayıcısında fark ettirmeden bir istek gerçekleştiren bir sayfaya çekmesinden oluşur ve sunucu, isteğin kurban tarafından kendi isteğiyle gerçekleştirildiğini varsayar. Bu nedenle Nette, POST formunun başka bir alan adından gönderilmesini engeller. Herhangi bir nedenle korumayı kapatmak ve formun başka bir alan adından gönderilmesine izin vermek isterseniz, şunu kullanın: +Sözü edilen CSRF saldırısı, bir saldırganın kurbanı, kurbanın tarayıcısında sessizce, kurbanın oturum açmış olduğu sunucuya bir istek çalıştıran bir sayfaya çekmesinden ibarettir. Sunucu da isteğin kurban tarafından isteyerek yapıldığına inanır. Bu yüzden Nette, yabancı bir kaynaktan gönderilen POST formlarını reddeder; aynı sitenin farklı bir alt alan adı bile yabancı sayılır. Başka bir kaynaktan gönderime izin vermeniz gerekiyorsa korumayı şununla kapatın: ```php -$form->allowCrossOrigin(); // DİKKAT! Korumayı kapatır! +$form->allowCrossOrigin(); // UYARI! Korumayı tümüyle kapatır! ``` -Bu koruma, `_nss` adlı SameSite çerezini kullanır. SameSite çereziyle koruma %100 güvenilir olmayabilir, bu nedenle token ile korumayı da etkinleştirmek önerilir: +Ancak bu, korumayı her kaynak için kapatır. Yalnızca belirli kaynaklara izin vermek için korumayı kapatın ve `Origin` header'ını kendi izin listenize göre kendiniz doğrulayın. -```php -$form->addProtection(); -``` +Koruma, tarayıcının otomatik gönderdiği ve bir XSS açığıyla bile taklit edilemeyen `Sec-Fetch-Site` header'ına (Fetch Metadata) dayanır. Bunları desteklemeyen eski tarayıcılarda, Nette uygulamasının otomatik ayarladığı bir SameSite çerezi yedek olarak devreye girer. [Tarayıcı sonunda CSRF'yi çözüyor |https://blog.nette.org/en/quarter-century-of-csrf] yazısı bunu ayrıntılı anlatıyor. -Uygulamadaki hassas verileri değiştiren sitenin yönetim bölümündeki formları bu şekilde korumanızı öneririz. Framework, oturumda saklanan bir yetkilendirme token'ı üreterek ve doğrulayarak CSRF saldırısına karşı kendini savunur. Bu nedenle, formu görüntülemeden önce oturumun açık olması gerekir. Sitenin yönetim bölümünde, genellikle kullanıcı girişi nedeniyle oturum zaten başlatılmıştır. Aksi takdirde, oturumu `Nette\Http\Session::start()` metoduyla başlatın. +.[note] +Oturumda saklanan bir yetkilendirme token'ıyla yapılan ve `$form->addProtection()` ile etkinleştirilen önceki koruma artık gerekmiyor ve 3.3 sürümünden beri kullanımdan kaldırıldı. -Birden Fazla Presenter'da Aynı Form -=================================== +Aynı Formu Birden Çok Presenter'da Kullanma +=========================================== -Bir formu birden fazla presenter'da kullanmanız gerekiyorsa, bunun için bir fabrika oluşturmanızı ve ardından bunu presenter'a iletmenizi öneririz. Böyle bir sınıf için uygun bir konum, örneğin `app/Forms` dizinidir. +Aynı formu birden çok presenter'da kullanmanız gerekiyorsa, onun için bir factory oluşturup presenter'lara enjekte etmenizi öneririz. Böyle bir sınıf için uygun bir yer örneğin `app/Forms` dizinidir. -Fabrika sınıfı şöyle görünebilir: +Factory sınıfı şöyle görünebilir: ```php use Nette\Application\UI\Form; @@ -383,14 +391,14 @@ class SignInFormFactory public function create(): Form { $form = new Form; - $form->addText('name', 'İsim:'); + $form->addText('name', 'Ad:'); $form->addSubmit('send', 'Giriş yap'); return $form; } } ``` -Sınıftan, presenter'daki bileşenler için fabrika metodunda formu üretmesini isteriz: +Formu üreten sınıfı, presenter'daki bileşen factory metodunda isteriz: ```php public function __construct( @@ -401,14 +409,14 @@ public function __construct( protected function createComponentSignInForm(): Form { $form = $this->formFactory->create(); - // formu değiştirebiliriz, burada örneğin düğme üzerindeki etiketi değiştiriyoruz + // formu değiştirebiliriz, burada örneğin düğmedeki metni değiştiriyoruz $form['send']->setCaption('Devam et'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // ve bir işleyici ekliyoruz + $form->onSuccess[] = $this->signInFormSuceeded(...); // ve işleyici ekliyoruz return $form; } ``` -Form işleme için işleyici, fabrikadan da sağlanabilir: +Form işleme işleyicisi factory'nin kendisi tarafından da sağlanabilir: ```php use Nette\Application\UI\Form; @@ -418,14 +426,14 @@ class SignInFormFactory public function create(): Form { $form = new Form; - $form->addText('name', 'İsim:'); + $form->addText('name', 'Ad:'); $form->addSubmit('send', 'Giriş yap'); $form->onSuccess[] = function (Form $form, $data): void { - // burada form işlemeyi gerçekleştiriyoruz + // gönderilen formumuzu burada işliyoruz }; return $form; } } ``` -İşte, Nette'deki formlara hızlı bir giriş yaptık. Dağıtımdaki [examples|https://github.com/nette/forms/tree/master/examples] dizinine göz atmayı deneyin, burada daha fazla ilham bulacaksınız. +Böylece Nette'te formlara hızlı bir girişi tamamladık. Daha fazla ilham için dağıtımdaki [örnekler |https://github.com/nette/forms/tree/master/examples] dizinine bakmayı deneyin. diff --git a/forms/tr/rendering.texy b/forms/tr/rendering.texy index 2e5f03cddf..cd3698f1ab 100644 --- a/forms/tr/rendering.texy +++ b/forms/tr/rendering.texy @@ -1,35 +1,35 @@ Formların Render Edilmesi ************************* -Formların görünümü çok çeşitli olabilir. Pratikte iki uç durumla karşılaşabiliriz. Bir yanda, uygulamada görsel olarak birbirine çok benzeyen bir dizi formu render etme ihtiyacı vardır ve `$form->render()` kullanarak şablon olmadan kolay render etmeyi takdir ederiz. Bu genellikle yönetim arayüzleri durumudur. +Formların görünümü çok çeşitli olabilir. Pratikte iki uçla karşılaşabiliriz. Bir yanda, bir uygulamada görsel olarak birbirinin aynısı çok sayıda formu render etme ihtiyacı vardır ve `$form->render()` ile şablonsuz kolay render'ı takdir ederiz. Yönetim arayüzlerinde durum tipik olarak böyledir. -Diğer yanda, her birinin orijinal olduğu çeşitli formlar vardır. Görünümleri en iyi HTML diliyle form şablonunda tanımlanır. Ve elbette, bahsedilen her iki uç durumun yanı sıra, arada bir yerde bulunan birçok formla karşılaşacağız. +Öte yanda, her biri kendine özgü olan çeşitli formlar vardır. Görünümleri en iyi, form şablonunda HTML kullanılarak anlatılır. Ve elbette bu iki uç dışında, arada bir yere düşen pek çok formla karşılaşırız. -Latte ile Render Etme -===================== +Latte ile Render +================ -[Latte şablonlama sistemi|latte:] formların ve elemanlarının render edilmesini önemli ölçüde kolaylaştırır. Önce formları manuel olarak tek tek elemanlarla nasıl render edeceğimizi ve böylece kod üzerinde tam kontrol sahibi olacağımızı göstereceğiz. Daha sonra böyle bir render işlemini nasıl [otomatikleştirebileceğinizi |#Otomatik Render Etme] göstereceğiz. +[Latte şablon sistemi |latte:], formların ve öğelerinin render edilmesini belirgin biçimde kolaylaştırır. Önce, kod üzerinde tam denetim kazanmak için formları elle, öğe öğe nasıl render edeceğimizi göstereceğiz. Sonra böyle bir render'ın nasıl [otomatikleştirilebileceğini |#Otomatik Render] göstereceğiz. -Formun Latte şablonu tasarımını `Nette\Forms\Blueprint::latte($form)` metodunu kullanarak oluşturabilirsiniz, bu da onu tarayıcı sayfasına yazdırır. Kodu daha sonra tıklayarak işaretleyip projeye kopyalamak yeterlidir. .{data-version:3.1.15} +Form için Latte şablonunu, onu tarayıcı sayfasına çıktılayan `Nette\Forms\Blueprint::latte($form)` metoduyla üretebilirsiniz. Sonra yalnızca tıklayarak kodu seçin ve projenize kopyalayın. .{data-version:3.1.15} `{control}` ----------- -Formu render etmenin en basit yolu şablonda şunu yazmaktır: +Bir formu render etmenin en basit yolu şablona şunu yazmaktır: ```latte {control signInForm} ``` -Bu şekilde render edilen formun görünümünü [#Renderer] ve [bireysel elemanlar |#HTML Nitelikleri] yapılandırarak etkileyebilirsiniz. +Render edilen formun görünümü, [#Renderer] ve [tek tek öğeler |#HTML Nitelikleri] yapılandırılarak etkilenebilir. `n:name` -------- -PHP kodundaki form tanımını HTML koduyla ilişkilendirmek son derece kolaydır. Sadece `n:name` niteliklerini eklemek yeterlidir. Bu kadar kolay! +Formun PHP kodundaki tanımını HTML koduyla bağlamak son derece kolaydır. Yalnızca `n:name` niteliklerini ekleyin. Bu kadar basit! ```php protected function createComponentSignInForm(): Form @@ -48,7 +48,7 @@ protected function createComponentSignInForm(): Form <label n:name=username>Kullanıcı adı: <input n:name=username size=20 autofocus></label> </div> <div> - <label n:name=password>Şifre: <input n:name=password></label> + <label n:name=password>Parola: <input n:name=password></label> </div> <div> <input n:name=send class="btn btn-default"> @@ -56,9 +56,9 @@ protected function createComponentSignInForm(): Form </form> ``` -Sonuçtaki HTML kodunun görünümü tamamen sizin kontrolünüzdedir. `n:name` niteliğini `<select>`, `<button>` veya `<textarea>` elemanlarında kullanırsanız, iç içerikleri otomatik olarak tamamlanır. `<form n:name>` etiketi ayrıca render edilen formun nesnesiyle `$form` yerel değişkenini oluşturur ve kapanış `</form>` etiketi render edilmemiş tüm gizli elemanları render eder (aynısı `{form} ... {/form}` için de geçerlidir). +Ortaya çıkan HTML kodunun görünümü üzerinde tam denetiminiz olur. `n:name` niteliğini `<select>`, `<button>` ya da `<textarea>` elemanlarıyla kullanırsanız, iç içerikleri otomatik doldurulur. Ayrıca `<form n:name>` etiketi, render edilen form nesnesini içeren yerel bir `$form` değişkeni oluşturur ve kapanış `</form>` etiketi, render edilmemiş gizli öğeleri render eder (aynısı `{form} ... {/form}` için de geçerlidir). -Ancak olası hata mesajlarının render edilmesini unutmamalıyız. Hem `addError()` metoduyla bireysel elemanlara eklenenler ( `{inputError}` kullanarak) hem de doğrudan forma eklenenler ( `$form->getOwnErrors()` tarafından döndürülenler): +Ancak olası hata mesajlarını render etmeyi unutmamalıyız. Buna, `addError()` metoduyla tek tek öğelere eklenen hatalar (`{inputError}` ile render edilir) ve doğrudan forma eklenen hatalar (`$form->getOwnErrors()` ile döndürülür) dahildir: ```latte <form n:name=signInForm class=form> @@ -71,7 +71,7 @@ Ancak olası hata mesajlarının render edilmesini unutmamalıyız. Hem `addErro <span class=error n:ifcontent>{inputError username}</span> </div> <div> - <label n:name=password>Şifre: <input n:name=password></label> + <label n:name=password>Parola: <input n:name=password></label> <span class=error n:ifcontent>{inputError password}</span> </div> <div> @@ -80,7 +80,7 @@ Ancak olası hata mesajlarının render edilmesini unutmamalıyız. Hem `addErro </form> ``` -RadioList veya CheckboxList gibi daha karmaşık form elemanları bu şekilde tek tek öğelerle render edilebilir: +RadioList ya da CheckboxList gibi daha karmaşık form öğeleri, öğe öğe şöyle render edilebilir: ```latte {foreach $form[gender]->getItems() as $key => $label} @@ -92,7 +92,7 @@ RadioList veya CheckboxList gibi daha karmaşık form elemanları bu şekilde te `{label}` `{input}` ------------------- -Her eleman için şablonda hangi HTML elemanını kullanacağınızı düşünmek istemiyor musunuz, `<input>`, `<textarea>` vb. mi? Çözüm evrensel `{input}` etiketidir: +Şablonda her öğe için hangi HTML elemanının kullanılacağını (`<input>` mu, `<textarea>` mı vb.) düşünmemeyi mi yeğlersiniz? Çözüm, evrensel `{input}` etiketidir: ```latte <form n:name=signInForm class=form> @@ -105,7 +105,7 @@ Her eleman için şablonda hangi HTML elemanını kullanacağınızı düşünme {inputError username} </div> <div> - {label password}Şifre: {input password}{/label} + {label password}Parola: {input password}{/label} {inputError password} </div> <div> @@ -114,9 +114,9 @@ Her eleman için şablonda hangi HTML elemanını kullanacağınızı düşünme </form> ``` -Form bir çevirmen kullanıyorsa, `{label}` etiketleri içindeki metin çevrilecektir. +Form bir çevirmen kullanıyorsa, form tanımından render edilen etiketler (örneğin `{label username /}`) çevrilir. Doğrudan `{label}` ve `{/label}` etiketleri arasına yazılan metin çevrilmez. -Bu durumda bile, RadioList veya CheckboxList gibi daha karmaşık form elemanları tek tek öğelerle render edilebilir: +Yine, RadioList ya da CheckboxList gibi daha karmaşık form öğeleri öğe öğe render edilebilir: ```latte {foreach $form[gender]->items as $key => $label} @@ -124,19 +124,19 @@ Bu durumda bile, RadioList veya CheckboxList gibi daha karmaşık form elemanlar {/foreach} ``` -Checkbox elemanındaki `<input>`'ın kendisini render etmek için `{input myCheckbox:}` kullanın. Bu durumda HTML niteliklerini her zaman virgülle ayırın `{input myCheckbox:, class: required}`. +Bir Checkbox öğesinin yalnızca `<input>` kısmını render etmek için `{input myCheckbox:}` kullanın. Bu durumda HTML niteliklerini her zaman virgülle ayırın: `{input myCheckbox:, class: required}`. `{inputError}` -------------- -Bir form elemanı için varsa hata mesajını yazdırır. Mesajı genellikle stil vermek için bir HTML elemanına sararız. Mesaj yoksa boş bir elemanın render edilmesini önlemek, `n:ifcontent` kullanarak zarif bir şekilde yapılabilir: +Varsa bir form öğesinin hata mesajını görüntüler. Mesaj genellikle biçimlendirme için bir HTML elemanının içine alınır. Mesaj yokken boş bir elemanın render edilmesini `n:ifcontent` ile şık biçimde önleyebilirsiniz: ```latte <span class=error n:ifcontent>{inputError $input}</span> ``` -Bir hatanın varlığını `hasErrors()` metoduyla kontrol edebilir ve buna göre üst elemana bir sınıf atayabiliriz: +Bir hata olup olmadığını `hasErrors()` metoduyla denetleyebilir ve üst elemanın sınıfını buna göre ayarlayabiliriz: ```latte <div n:class="$form[username]->hasErrors() ? 'error'"> @@ -149,13 +149,31 @@ Bir hatanın varlığını `hasErrors()` metoduyla kontrol edebilir ve buna gör `{form}` -------- -`{form signInForm}...{/form}` etiketleri `<form n:name="signInForm">...</form>`'a bir alternatiftir. +`{form signInForm}...{/form}` etiketleri, `<form n:name="signInForm">...</form>` yazımının bir alternatifidir. Argümanları addan virgülle ayırın: `{form signInForm, class: foo}`. +.{data-version:3.3.0} +Adın önüne konan `scope` anahtar sözcüğü, formu yalnızca yığına iter (böylece `{input}`, `{label}` vb. ona bağlanır) ama `<form>` etiketini render etmez. Bir formun bir parçasını, örneğin bir snippet içinde render etmek için kullanışlıdır. Zaten etkin bir form varsa, ad ona göre çözülür; dolayısıyla `{form scope}` aynı zamanda `{formContainer}` yerine de geçer: -Otomatik Render Etme --------------------- +```latte +{form scope signInForm} + {input username} +{/form} +``` -`{input}` ve `{label}` etiketleri sayesinde, herhangi bir form için kolayca genel bir şablon oluşturabiliriz. Sırayla tüm elemanlarını yineleyecek ve render edecektir, formun `</form>` etiketiyle sonlandırıldığında otomatik olarak render edilen gizli elemanlar hariç. Render edilen formun adı `$form` değişkeninde beklenecektir. +.{data-version:3.3.0} +`detached` anahtar sözcüğü boş bir `<form></form>` render eder ve her öğeyi ona HTML `form` niteliğiyle bağlar. Bu, HTML'in başka türlü yasakladığı bir şeyi, bir formu başka bir formun içine koymanızı sağlar. Ayrılmış formun bir HTML `id` değeri olmalıdır; ona bir ad verdiğinizde (aşağıdaki `outerForm` gibi) bu otomatik üretilir: + +```latte +{form detached outerForm} + ... +{/form} +``` + + +Otomatik Render +--------------- + +`{input}` ve `{label}` etiketleri sayesinde, herhangi bir form için genel bir şablonu kolayca oluşturabiliriz. Bu şablon, form `</form>` etiketiyle kapatıldığında otomatik render edilen gizli öğeler dışında tüm öğeleri dolaşıp render eder. Render edilecek formun adını `$form` değişkeninde bekler. ```latte <form n:name=$form class=form> @@ -172,15 +190,15 @@ Otomatik Render Etme </form> ``` -Kullanılan kendi kendine kapanan çift etiketler `{label .../}` PHP kodundaki form tanımından gelen etiketleri görüntüler. +Burada kullanılan, kendiliğinden kapanan çiftli `{label .../}` etiketleri, PHP kodundaki form tanımından gelen etiketleri görüntüler. -Bu genel şablonu örneğin `basic-form.latte` dosyasına kaydedin ve formu render etmek için onu dahil etmek ve formun adını (veya örneğini) `$form` parametresine iletmek yeterlidir: +Bu genel şablonu örneğin `basic-form.latte` dosyasına kaydedin. Formu render etmek için onu yalnızca dahil edin ve form adını (ya da örneğini) `$form` parametresine verin: ```latte {include basic-form.latte, form: signInForm} ``` -Belirli bir formu render ederken görünümüne müdahale etmek ve örneğin bir elemanı farklı render etmek isterseniz, en kolay yol şablonda daha sonra üzerine yazılabilecek bloklar hazırlamaktır. Bloklar ayrıca [dinamik isimler |latte:template-inheritance#Dinamik Blok Adları] sahip olabilir, bu nedenle render edilen elemanın adını da içlerine ekleyebilirsiniz. Örneğin: +Belirli bir formun görünümünü render sırasında değiştirmek, belki bir öğeyi farklı render etmek istiyorsanız, en kolay yol şablonda sonradan geçersiz kılınabilecek bloklar hazırlamaktır. Blokların [dinamik adları |latte:template-inheritance#Dinamik blok adları] da olabilir; bu da render edilen öğenin adını eklemenizi sağlar. Örneğin: ```latte ... @@ -189,7 +207,7 @@ Belirli bir formu render ederken görünümüne müdahale etmek ve örneğin bir ... ``` -Örneğin `username` elemanı için, [{embed} |latte:template-inheritance#Birim Kalıtımı] etiketi kullanılarak kolayca üzerine yazılabilecek `input-username` bloğu oluşturulur: +Örneğin `username` adlı bir öğe için bu, [{embed} |latte:template-inheritance#Unit Inheritance] etiketiyle kolayca geçersiz kılınabilen `input-username` bloğunu oluşturur: ```latte {embed basic-form.latte, form: signInForm} @@ -201,7 +219,7 @@ Belirli bir formu render ederken görünümüne müdahale etmek ve örneğin bir {/embed} ``` -Alternatif olarak, `basic-form.latte` şablonunun tüm içeriği, `$form` parametresi dahil olmak üzere bir blok olarak [tanımlanabilir |latte:template-inheritance#Tanımlar]: +Alternatif olarak `basic-form.latte` şablonunun tüm içeriği, `$form` parametresiyle birlikte bir blok olarak [tanımlanabilir |latte:template-inheritance#Definitions]: ```latte {define basic-form, $form} @@ -211,7 +229,7 @@ Alternatif olarak, `basic-form.latte` şablonunun tüm içeriği, `$form` parame {/define} ``` -Bu sayede çağrısı biraz daha basit olacaktır: +Bu, çağrıyı biraz daha basitleştirir: ```latte {embed basic-form, signInForm} @@ -219,7 +237,7 @@ Bu sayede çağrısı biraz daha basit olacaktır: {/embed} ``` -Bloğu yalnızca tek bir yerde, layout şablonunun başında içe aktarmak yeterlidir: +Bloğun yalnızca tek bir yerde, yerleşim şablonunun başında içe aktarılması gerekir: ```latte {import basic-form.latte} @@ -229,7 +247,7 @@ Bloğu yalnızca tek bir yerde, layout şablonunun başında içe aktarmak yeter Özel Durumlar ------------- -Formun yalnızca iç kısmını HTML `<form>` etiketleri olmadan render etmeniz gerekiyorsa, örneğin snippet gönderirken, bunları `n:tag-if` niteliğiyle gizleyin: +Formun yalnızca iç kısmını `<form>` HTML etiketleri olmadan render etmeniz gerekiyorsa, örneğin snippet gönderirken, onları `n:tag-if` niteliğiyle gizleyin: ```latte <form n:name=signInForm n:tag-if=false> @@ -240,7 +258,7 @@ Formun yalnızca iç kısmını HTML `<form>` etiketleri olmadan render etmeniz </form> ``` -Form konteyneri içindeki elemanların render edilmesine `{formContainer}` etiketi yardımcı olur. +Bir form container'ının içindeki öğeleri render etmede `{formContainer}` etiketi ya da daha yeni [`{form scope}` |#{form}] yardımcı olur. ```latte <p>Hangi haberleri almak istersiniz:</p> @@ -254,42 +272,42 @@ Form konteyneri içindeki elemanların render edilmesine `{formContainer}` etike ``` -Latte Olmadan Render Etme -========================= +Latte Olmadan Render +==================== -Formu render etmenin en basit yolu çağırmaktır: +Bir formu render etmenin en kolay yolu şunu çağırmaktır: ```php $form->render(); ``` -Bu şekilde render edilen formun görünümünü [#Renderer] ve [bireysel elemanlar |#HTML Nitelikleri] yapılandırarak etkileyebilirsiniz. +Render edilen formun görünümü, [#Renderer] ve [tek tek öğeler |#HTML Nitelikleri] yapılandırılarak etkilenebilir. -Manuel Render Etme ------------------- +Elle Render +----------- -Her form elemanı, form alanının ve etiketin HTML kodunu üreten metotlara sahiptir. Bunu ya bir dize olarak ya da bir [Nette\Utils\Html|utils:html-elements] nesnesi olarak döndürebilirler: +Her form öğesinin, form alanının ve etiketinin HTML kodunu üreten metotları vardır. Bunları ya dize olarak ya da bir [Nette\Utils\Html |utils:html-elements] nesnesi olarak döndürebilirler: -- `getControl(): Html|string` elemanın HTML kodunu döndürür +- `getControl(): Html|string` öğenin HTML kodunu döndürür - `getLabel($caption = null): Html|string|null` varsa etiketin HTML kodunu döndürür -Form bu nedenle tek tek elemanlarla render edilebilir: +Bu, formun öğe öğe render edilmesine olanak tanır: ```php <?php $form->render('begin') ?> -<?php $form->render('errors') ?> +<?php $form->render('ownerrors') ?> <div> <?= $form['name']->getLabel() ?> <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['name']->getError()) ?></span> </div> <div> <?= $form['age']->getLabel() ?> <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> + <span class=error><?= htmlspecialchars((string) $form['age']->getError()) ?></span> </div> // ... @@ -297,26 +315,26 @@ Form bu nedenle tek tek elemanlarla render edilebilir: <?php $form->render('end') ?> ``` -Bazı elemanlar için `getControl()` tek bir HTML elemanı (örneğin `<input>`, `<select>` vb.) döndürürken, diğerleri için tüm bir HTML kod parçasını (CheckboxList, RadioList) döndürür. Bu durumda, her öğe için ayrı ayrı tek tek girdileri ve etiketleri üreten metotları kullanabilirsiniz: +Bazı öğelerde `getControl()` tek bir HTML elemanı döndürürken (örneğin `<input>`, `<select>` vb.), bazılarında eksiksiz bir HTML kodu parçası döndürür (CheckboxList, RadioList). Böyle durumlarda, her öğe için ayrı ayrı input ve etiket üreten metotları kullanabilirsiniz: -- `getControlPart($key = null): ?Html` bir öğenin HTML kodunu döndürür -- `getLabelPart($key = null): ?Html` bir öğenin etiketinin HTML kodunu döndürür +- `getControlPart($key = null): Html` tek bir öğenin HTML kodunu döndürür +- `getLabelPart($key = null): Html` tek bir öğenin etiketinin HTML kodunu döndürür .[note] -Bu metotların tarihsel nedenlerden dolayı `get` öneki vardır, ancak her çağrıda yeni bir `Html` elemanı oluşturup döndürdükleri için `generate` daha iyi olurdu. +Bu metotlar tarihsel nedenlerle `get` öneki taşır, ama `generate` daha uygun olurdu; çünkü her çağrıda yeni bir `Html` elemanı oluşturup döndürürler. Renderer ======== -Formun render edilmesini sağlayan bir nesnedir. `$form->setRenderer` metoduyla ayarlanabilir. `$form->render()` metodu çağrıldığında kontrol ona devredilir. +Bu, formu render etmekten sorumlu bir nesnedir. `$form->setRenderer()` metoduyla ayarlanabilir. `$form->render()` metodu çağrıldığında denetim ona geçer. -Kendi renderer'ımızı ayarlamazsak, varsayılan [api:Nette\Forms\Rendering\DefaultFormRenderer] render edicisi kullanılacaktır. Bu, form elemanlarını bir HTML tablosu şeklinde render eder. Çıktı şöyle görünür: +Özel bir renderer ayarlamazsak, varsayılan renderer [api:Nette\Forms\Rendering\DefaultFormRenderer] kullanılır. Bu, form öğelerini bir HTML tablosuna render eder. Çıktı şöyle görünür: ```latte <table> <tr class="required"> - <th><label class="required" for="frm-name">İsim:</label></th> + <th><label class="required" for="frm-name">Ad:</label></th> <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> </tr> @@ -332,11 +350,11 @@ Kendi renderer'ımızı ayarlamazsak, varsayılan [api:Nette\Forms\Rendering\Def ... ``` -Form iskeleti için tablo kullanıp kullanmamak tartışmalıdır ve birçok web tasarımcısı farklı bir işaretleme tercih eder. Örneğin, bir tanım listesi. Bu nedenle `DefaultFormRenderer`'ı formu bir liste şeklinde render edecek şekilde yeniden yapılandıracağız. Yapılandırma [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers] dizisini düzenleyerek yapılır. İlk dizin her zaman alanı ve ikincisi onun niteliğini temsil eder. Bireysel alanlar resimde gösterilmiştir: +Formun yapısı için tablo kullanılıp kullanılmayacağı tartışmalıdır ve pek çok web tasarımcısı tanım listesi gibi farklı bir işaretlemeyi yeğler. Bu yüzden `DefaultFormRenderer` sınıfını, formu liste olarak render edecek şekilde yeniden yapılandıracağız. Yapılandırma, [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers] dizisi düzenlenerek yapılır. İlk indeks her zaman bir alanı, ikincisi ise onun niteliğini temsil eder. Tek tek alanlar resimde gösteriliyor: -[* defaultformrenderer.webp *] +[* form-areas-en.webp *] -Standart olarak, `controls` eleman grubu bir tablo `<table>` ile sarılır, her `pair` bir tablo satırını `<tr>` temsil eder ve `label` ve `control` çifti hücreler `<th>` ve `<td>`'dir. Şimdi saran elemanları değiştireceğiz. `controls` alanını `<dl>` konteynerine koyacağız, `pair` alanını konteynersiz bırakacağız, `label`'ı `<dt>`'ye koyacağız ve son olarak `control`'ü `<dd>` etiketleriyle saracağız: +Varsayılan olarak `controls` grubu `<table>` içine sarılır, her `pair` bir tablo satırını `<tr>` temsil eder, `label` ile `control` çifti ise `<th>` ve `<td>` hücreleridir. Şimdi sarmalayan elemanları değiştireceğiz. `controls` alanını bir `<dl>` container'ına koyacak, `pair` alanını container'sız bırakacak, `label` alanını `<dt>` içine koyacak ve en sonunda `control` alanını `<dd>` etiketleriyle saracağız: ```php $renderer = $form->getRenderer(); @@ -348,11 +366,11 @@ $renderer->wrappers['control']['container'] = 'dd'; $form->render(); ``` -Sonuç bu HTML kodudur: +Bu, şu HTML kodunu verir: ```latte <dl> - <dt><label class="required" for="frm-name">İsim:</label></dt> + <dt><label class="required" for="frm-name">Ad:</label></dt> <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> @@ -367,102 +385,102 @@ Sonuç bu HTML kodudur: </dl> ``` -Wrappers dizisinde diğer birçok niteliği etkileyebilirsiniz: +wrappers dizisi başka pek çok niteliği de etkilemeye olanak tanır: -- bireysel form elemanı türlerine CSS sınıfları eklemek -- tek ve çift satırları CSS sınıfıyla ayırt etmek -- zorunlu ve isteğe bağlı öğeleri görsel olarak ayırt etmek -- hata mesajlarının doğrudan elemanların yanında mı yoksa formun üzerinde mi görüntüleneceğini belirlemek +- tek tek form öğesi türlerine CSS sınıfları ekleme +- tek ve çift satırları CSS sınıflarıyla ayırt etme +- zorunlu ve isteğe bağlı öğeleri görsel olarak ayırt etme +- hata mesajlarının doğrudan öğelerin yanında mı yoksa formun üstünde mi görüntüleneceğini belirleme -Options -------- +Seçenekler +---------- -Renderer'ın davranışı, bireysel form elemanlarında *options* ayarlayarak da kontrol edilebilir. Bu şekilde, giriş alanının yanında yazdırılacak açıklamayı ayarlayabilirsiniz: +Renderer'ın davranışı, tek tek form öğelerinde *seçenekler* ayarlanarak da denetlenebilir. Böylece girdi alanının yanında görünen bir açıklama koyabilirsiniz: ```php $form->addText('phone', 'Numara:') - ->setOption('description', 'Bu numara gizli kalacaktır'); + ->setOption('description', 'Bu numara gizli kalacak'); ``` -İçine HTML içeriği yerleştirmek istiyorsak, [Html |utils:html-elements] sınıfını kullanırız +İçine HTML içerik koymak istersek [Html |utils:html-elements] sınıfını kullanırız: ```php use Nette\Utils\Html; -$form->addText('phone', 'Numara:') +$form->addText('phone', 'Telefon:') ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Numaranızın saklanma koşulları</a>') + ->setHtml('<a href="...">Hizmet koşulları.</a>') ); ``` .[tip] -Html elemanı etiket yerine de kullanılabilir: `$form->addCheckbox('conditions', $label)`. +Etiket yerine de bir Html elemanı kullanılabilir: `$form->addCheckbox('conditions', $label)`. -Elemanların Gruplandırılması ----------------------------- +Girdileri Gruplama +------------------ -Renderer, elemanları görsel gruplara (fieldset) ayırmayı sağlar: +Renderer, öğeleri görsel gruplara (fieldset) ayırmaya olanak tanır: ```php -$form->addGroup('Kişisel Veriler'); +$form->addGroup('Kişisel veriler'); ``` -Yeni bir grup oluşturulduktan sonra, bu grup aktif hale gelir ve yeni eklenen her eleman aynı zamanda ona eklenir. Yani formu bu şekilde oluşturabilirsiniz: +Yeni bir grup oluşturulduktan sonra etkin hâle gelir ve yeni eklenen her öğe ona da eklenir. Böylece form şöyle kurulabilir: ```php $form = new Form; -$form->addGroup('Kişisel Veriler'); +$form->addGroup('Kişisel veriler'); $form->addText('name', 'Adınız:'); $form->addInteger('age', 'Yaşınız:'); $form->addEmail('email', 'E-posta:'); -$form->addGroup('Teslimat Adresi'); +$form->addGroup('Teslimat adresi'); $form->addCheckbox('send', 'Adrese gönder'); $form->addText('street', 'Sokak:'); $form->addText('city', 'Şehir:'); $form->addSelect('country', 'Ülke:', $countries); ``` -Renderer önce grupları ve ancak ondan sonra hiçbir gruba ait olmayan elemanları render eder. +Renderer önce grupları, ardından hiçbir gruba ait olmayan öğeleri çizer. Bootstrap Desteği ----------------- -[Örneklerde |https://github.com/nette/forms/tree/master/examples] Renderer'ı [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] ve [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] için nasıl yapılandıracağınıza dair örnekler bulacaksınız. +[Örnekler dizininde |https://github.com/nette/forms/tree/master/examples], Renderer'ın [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] ve [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] için nasıl yapılandırılacağını gösteren örnekler bulabilirsiniz. HTML Nitelikleri ================ -Form elemanlarının herhangi bir HTML niteliğini ayarlamak için `setHtmlAttribute(string $name, $value = true)` metodunu kullanırız: +Form öğelerine herhangi bir HTML niteliği koymak için `setHtmlAttribute(string $name, $value = true)` metodunu kullanın: ```php $form->addInteger('number', 'Numara:') ->setHtmlAttribute('class', 'big-number'); -$form->addSelect('rank', 'Sıralama ölçütü:', ['fiyat', 'isim']) - ->setHtmlAttribute('onchange', 'submit()'); // değişiklikte gönder +$form->addSelect('rank', 'Sıralama ölçütü:', ['fiyat', 'ad']) + ->setHtmlAttribute('onchange', 'submit()'); // değişince formu gönder -// <form> öğesinin kendisinin niteliklerini ayarlamak için +// <form> elemanının kendi niteliklerini ayarlamak için $form->setHtmlAttribute('id', 'myForm'); ``` -Eleman türünün belirtilmesi: +Öğenin türünü belirtme: ```php $form->addText('tel', 'Telefonunuz:') ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'telefonu yazın'); + ->setHtmlAttribute('placeholder', 'Lütfen telefonunuzu girin'); ``` .[warning] -Türün ve diğer niteliklerin ayarlanması yalnızca görsel amaçlıdır. Girdilerin doğruluğunun doğrulanması sunucu tarafında yapılmalıdır, bu da uygun bir [form elemanı|controls] seçerek ve [doğrulama kuralları|validation] belirterek sağlanır. +Türü ve diğer nitelikleri ayarlamak yalnızca görsel amaçlıdır. Girdinin doğruluğunun doğrulanması sunucu tarafında yapılmalıdır; bunu uygun bir [form öğesi |controls] seçerek ve [doğrulama kuralları |validation] belirterek sağlarsınız. -Radyo veya onay kutusu listelerindeki bireysel öğelere, her biri için farklı değerlere sahip HTML niteliği atayabiliriz. Anahtara göre değer seçimini sağlayan `style:` sonrasındaki iki noktaya dikkat edin: +Radyo ya da onay kutusu listelerindeki tek tek öğelerde, her biri için farklı değerlere sahip bir HTML niteliği ayarlayabiliriz. `style:` sonrasındaki iki nokta üst üste işaretine dikkat edin; bu, değerin anahtara göre seçilmesini sağlar: ```php $colors = ['r' => 'kırmızı', 'g' => 'yeşil', 'b' => 'mavi']; @@ -471,7 +489,7 @@ $form->addCheckboxList('colors', 'Renkler:', $colors) ->setHtmlAttribute('style:', $styles); ``` -Yazdırır: +Şunu render eder: ```latte <label><input type="checkbox" name="colors[]" style="background:red" value="r">kırmızı</label> @@ -479,14 +497,14 @@ Yazdırır: <label><input type="checkbox" name="colors[]" value="b">mavi</label> ``` -`readonly` gibi mantıksal nitelikleri ayarlamak için soru işaretiyle yazımı kullanabiliriz: +`readonly` gibi boolean nitelikleri ayarlamak için soru işaretli yazımı kullanabiliriz: ```php $form->addCheckboxList('colors', 'Renkler:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // daha fazla anahtar için bir dizi kullanın, örn. ['r', 'g'] + ->setHtmlAttribute('readonly?', 'r'); // birden çok anahtar için dizi kullanın, örneğin ['r', 'g'] ``` -Yazdırır: +Şunu render eder: ```latte <label><input type="checkbox" name="colors[]" readonly value="r">kırmızı</label> @@ -494,14 +512,14 @@ Yazdırır: <label><input type="checkbox" name="colors[]" value="b">mavi</label> ``` -Seçme kutuları (selectbox) durumunda, `setHtmlAttribute()` metodu `<select>` elemanının niteliklerini ayarlar. Bireysel `<option>`'ların niteliklerini ayarlamak istiyorsak, `setOptionAttribute()` metodunu kullanırız. Yukarıda belirtilen iki nokta ve soru işaretiyle yazımlar da çalışır: +Seçim kutularında `setHtmlAttribute()` metodu `<select>` elemanının niteliklerini ayarlar. Tek tek `<option>` elemanlarına nitelik koymak istersek `setOptionAttribute()` metodunu kullanırız. Yukarıda sözü edilen iki nokta ve soru işareti yazımları burada da çalışır: ```php $form->addSelect('colors', 'Renkler:', $colors) ->setOptionAttribute('style:', $styles); ``` -Yazdırır: +Şunu render eder: ```latte <select name="colors"> @@ -512,10 +530,10 @@ Yazdırır: ``` -Prototypler +Prototipler ----------- -HTML niteliklerini ayarlamanın alternatif bir yolu, HTML elemanının üretildiği şablonu düzenlemektir. Şablon bir `Html` nesnesidir ve `getControlPrototype()` metodu tarafından döndürülür: +HTML nitelikleri ayarlamanın bir başka yolu, HTML elemanının üretildiği şablonu değiştirmektir. Şablon bir `Html` nesnesidir ve `getControlPrototype()` metoduyla döndürülür: ```php $input = $form->addInteger('number', 'Numara:'); @@ -523,14 +541,14 @@ $html = $input->getControlPrototype(); // <input> $html->class('big-number'); // <input class="big-number"> ``` -Bu şekilde, `getLabelPrototype()` tarafından döndürülen etiket şablonunu da değiştirebilirsiniz: +`getLabelPrototype()` ile döndürülen etiket şablonu da bu şekilde değiştirilebilir: ```php $html = $input->getLabelPrototype(); // <label> $html->class('distinctive'); // <label class="distinctive"> ``` -Checkbox, CheckboxList ve RadioList elemanlarında, tüm elemanı saran elemanın şablonunu etkileyebilirsiniz. Bu, `getContainerPrototype()` tarafından döndürülür. Varsayılan durumda bu "boş" bir elemandır, bu yüzden hiçbir şey render edilmez, ancak ona bir ad atayarak render edilecektir: +Checkbox, CheckboxList ve RadioList öğelerinde, öğenin tamamını saran elemanın şablonunu etkileyebilirsiniz. O da `getContainerPrototype()` ile döndürülür. Varsayılan olarak "boş" bir elemandır, dolayısıyla hiçbir şey render edilmez; ama ona bir ad verirseniz render edilir: ```php $input = $form->addCheckbox('send'); @@ -541,49 +559,49 @@ echo $input->getControl(); // <div class="check"><label><input type="checkbox" name="send"></label></div> ``` -CheckboxList ve RadioList durumunda, `getSeparatorPrototype()` tarafından döndürülen bireysel öğelerin ayırıcısının şablonunu da etkileyebilirsiniz. Varsayılan durumda bu `<br>` elemanıdır. Onu çiftli bir elemana değiştirirseniz, bireysel öğeleri ayırmak yerine saracaktır. Ayrıca, `getItemLabelPrototype()` tarafından döndürülen bireysel öğelerdeki etiket HTML elemanının şablonunu da etkileyebilirsiniz. +CheckboxList ve RadioList söz konusu olduğunda, `getSeparatorPrototype()` metoduyla döndürülen, tek tek öğeler arasındaki ayırıcının şablonunu da etkileyebilirsiniz. Varsayılan olarak `<br>` elemanıdır. Onu çiftli bir elemana çevirirseniz, öğeleri ayırmak yerine onları saracaktır. Ayrıca `getItemLabelPrototype()` ile döndürülen, tek tek öğelerin etiketlerine ait HTML eleman şablonunu da etkileyebilirsiniz. Çeviri ====== -Çok dilli bir uygulama programlıyorsanız, muhtemelen formu farklı dil mutasyonlarında render etmeniz gerekecektir. Nette Framework bu amaçla çeviri için [api:Nette\Localization\Translator] arayüzünü tanımlar. Nette'de varsayılan bir uygulama yoktur, ihtiyaçlarınıza göre [Componette |https://componette.org/search/localization] üzerinde bulabileceğiniz birkaç hazır çözüm arasından seçim yapabilirsiniz. Belgelerinde çevirmeni nasıl yapılandıracağınızı öğreneceksiniz. +Çok dilli bir uygulama geliştiriyorsanız, formu büyük olasılıkla farklı dil sürümlerinde render etmeniz gerekecek. Nette Framework bunun için bir çeviri arayüzü tanımlar: [api:Nette\Localization\Translator]. Nette'in varsayılan bir gerçekleştirimi 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ı belgeleri anlatır. -Formlar, metinlerin bir çevirmen aracılığıyla yazdırılmasını destekler. Onu `setTranslator()` metoduyla iletiriz: +Formlar, metinlerin çevirmen üzerinden çıktılanmasını destekler. Onu `setTranslator()` metoduyla veririz: ```php $form->setTranslator($translator); ``` -Bu andan itibaren, yalnızca tüm etiketler değil, aynı zamanda tüm hata mesajları veya seçme kutusu (select box) öğeleri de başka bir dile çevrilecektir. +Bu andan itibaren yalnızca tüm etiketler değil, tüm hata mesajları, seçim kutularındaki öğeler ve girdi placeholder'ları da hedef dile çevrilir. -Bireysel form elemanları için farklı bir çevirmen ayarlamak veya `null` değeriyle çeviriyi tamamen kapatmak mümkündür: +Tek tek form öğeleri için farklı bir çevirmen ayarlamak ya da değeri `null` yaparak çeviriyi tümüyle kapatmak olanaklıdır: ```php $form->addSelect('carModel', 'Model:', $cars) ->setTranslator(null); ``` -[Doğrulama kuralları|validation] için çevirmene özel parametreler de iletilir, örneğin kural için: +[Doğrulama kurallarında |validation] çevirmene belirli parametreler de aktarılır. Örneğin şu kural için: ```php -$form->addPassword('password', 'Şifre:') - ->addRule($form::MinLength, 'Şifre en az %d karakter olmalıdır', 8); +$form->addPassword('password', 'Parola:') + ->addRule($form::MinLength, 'Parola en az %d karakter uzunluğunda olmalıdır', 8); ``` çevirmen şu parametrelerle çağrılır: ```php -$translator->translate('Şifre en az %d karakter olmalıdır', 8); +$translator->translate('Parola en az %d karakter uzunluğunda olmalıdır', 8); ``` -ve dolayısıyla sayıya göre `karakter` kelimesinin doğru çoğul biçimini seçebilir. +ve böylece `karakter` sözcüğü için sayıya göre doğru çoğul biçimi seçebilir. onRender Olayı ============== -Form render edilmeden hemen önce, kodumuzun çağrılmasını sağlayabiliriz. Bu kod, örneğin doğru görüntüleme için form elemanlarına HTML sınıfları ekleyebilir. Kodu `onRender` dizisine ekleriz: +Form render edilmeden hemen önce kendi kodumuzun çağrılmasını sağlayabiliriz. Bu kod örneğin doğru görüntüleme için form öğelerine HTML sınıfları ekleyebilir. Kodu `onRender` dizisine ekleriz: ```php $form->onRender[] = function ($form) { diff --git a/forms/tr/standalone.texy b/forms/tr/standalone.texy index d1b4f1b4c0..f5beb557bb 100644 --- a/forms/tr/standalone.texy +++ b/forms/tr/standalone.texy @@ -1,86 +1,92 @@ -Formları Tek Başına Kullanma -**************************** +Bağımsız Kullanılan Formlar +*************************** .[perex] -Nette Forms, web formları oluşturmayı ve işlemeyi önemli ölçüde kolaylaştırır. Bunları, bu bölümde göstereceğimiz gibi, framework'ün geri kalanı olmadan uygulamalarınızda tamamen bağımsız olarak kullanabilirsiniz. +Nette Forms, web formlarının oluşturulmasını ve işlenmesini çarpıcı biçimde kolaylaştırır. Onları uygulamalarınızda, framework'ün geri kalanı olmadan tümüyle bağımsız kullanabilirsiniz; bu bölümde gösterildiği gibi. -Ancak Nette Application ve presenter'ları kullanıyorsanız, [presenter'larda kullanım |in-presenter] kılavuzu sizin içindir. +Ancak Nette Application ve presenter'ları kullanıyorsanız, size ayrılmış bir kılavuz var: [presenter'larda formlar |in-presenter]. İlk Form ======== -Basit bir kayıt formu yazmaya çalışalım. Kodu aşağıdaki gibi olacaktır ("tüm kod":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): +Başlamadan önce paketi [Composer |best-practices:composer] ile kurun: + +```shell +composer require nette/forms +``` + +Basit bir kayıt formu yazmayı deneyelim. Kodu şöyle olacak ("tam kod":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851): ```php use Nette\Forms\Form; $form = new Form; -$form->addText('name', 'İsim:'); -$form->addPassword('password', 'Şifre:'); -$form->addSubmit('send', 'Kayıt Ol'); +$form->addText('name', 'Ad:'); +$form->addPassword('password', 'Parola:'); +$form->addSubmit('send', 'Kaydol'); ``` -Çok kolay bir şekilde oluşturabiliriz: +Ve onu çok kolayca render edelim: ```php $form->render(); ``` -ve tarayıcıda şöyle görünecektir: +Tarayıcıdaki sonuç şöyle görünmeli: -[* form-cs.webp *] +[* form-en.webp *] -Form, `Nette\Forms\Form` sınıfının bir nesnesidir (`Nette\Application\UI\Form` sınıfı presenter'larda kullanılır). Buna isim, şifre ve gönderme düğmesi gibi öğeler ekledik. +Form, `Nette\Forms\Form` sınıfının bir nesnesidir (presenter'larda `Nette\Application\UI\Form` sınıfı kullanılır). Ona "name", "password" adlı öğeleri ve bir gönder düğmesi ekledik. -Şimdi formu canlandıralım. `$form->isSuccess()` sorgusuyla formun gönderilip gönderilmediğini ve geçerli bir şekilde doldurulup doldurulmadığını öğreneceğiz. Eğer öyleyse, verileri yazdıracağız. Form tanımından sonra şunu ekleyeceğiz: +Şimdi forma can verelim. `$form->isSuccess()` sorgusuyla, formun gönderilip gönderilmediğini ve geçerli biçimde doldurulup doldurulmadığını öğreniriz. Öyleyse veriyi çıktılayacağız. Form tanımından sonra şunu ekleyin: ```php if ($form->isSuccess()) { - echo 'Form doğru bir şekilde dolduruldu ve gönderildi'; + echo 'Form doldurulup başarıyla gönderildi'; $data = $form->getValues(); - // $data->name ismi içerir - // $data->password şifreyi içerir + // $data->name adı içerir + // $data->password parolayı içerir var_dump($data); } ``` -`getValues()` metodu, gönderilen verileri [ArrayHash |utils:arrays#ArrayHash] nesnesi biçiminde döndürür. Bunun nasıl değiştirileceğini [daha sonra |#Sınıflara Eşleme] göstereceğiz. `$data` nesnesi, kullanıcının doldurduğu verilerle birlikte `name` ve `password` anahtarlarını içerir. +`getValues()` metodu, gönderilen veriyi bir [ArrayHash |utils:arrays#ArrayHash] nesnesi olarak döndürür. Bunun nasıl değiştirileceğini [daha sonra |#Sınıflara Eşleme] göstereceğiz. `$data` nesnesi, kullanıcının girdiği verilerle birlikte `name` ve `password` anahtarlarını içerir. -Genellikle verileri doğrudan daha fazla işleme göndeririz, bu da örneğin veritabanına ekleme olabilir. Ancak işleme sırasında bir hata oluşabilir, örneğin kullanıcı adı zaten alınmış olabilir. Bu durumda, hatayı `addError()` kullanarak forma geri iletiriz ve hata mesajıyla birlikte yeniden oluşturulmasını sağlarız. +Genellikle veriyi doğrudan ileri işleme, örneğin veritabanına eklemeye göndeririz. Ancak işleme sırasında bir hata oluşabilir, örneğin kullanıcı adı zaten alınmış olabilir. Bu durumda hatayı `addError()` ile forma geri aktarır ve hata mesajıyla birlikte yeniden render edilmesini sağlarız. ```php -$form->addError('Üzgünüz, bu kullanıcı adı zaten kullanılıyor.'); +$form->addError('Üzgünüz, bu kullanıcı adı zaten alınmış.'); ``` -Formu işledikten sonra bir sonraki sayfaya yönlendiririz. Bu, *yenile*, *geri* düğmesiyle veya tarayıcı geçmişinde gezinerek formun istenmeyen şekilde yeniden gönderilmesini önler. +Formu işledikten sonra bir sonraki sayfaya yönlendiririz. Bu, *yenile* ya da *geri* düğmelerine tıklanarak veya tarayıcı geçmişinde gezinilerek formun istenmeden yeniden gönderilmesini önler. -Form varsayılan olarak POST metoduyla aynı sayfaya gönderilir. Her ikisi de değiştirilebilir: +Form varsayılan olarak POST metoduyla aynı sayfaya gönderilir. İkisi de değiştirilebilir: ```php $form->setAction('/submit.php'); $form->setMethod('GET'); ``` -Ve aslında hepsi bu :-) İşlevsel ve mükemmel bir şekilde [güvenli |#Güvenlik Açıklarına Karşı Koruma] bir formumuz var. +Ve aslında hepsi bu :-) İşleyen ve kusursuz biçimde [güvenli |#Açıklara Karşı Koruma] bir formumuz var. -Diğer [form elemanları |controls] eklemeyi de deneyin. +Başka [form öğeleri |controls] eklemeyi de deneyin. -Elemanlara Erişim -================= +Öğelere Erişim +============== -Formu ve tek tek elemanlarını bileşenler olarak adlandırırız. Kökü form olan bir bileşen ağacı oluştururlar. Formun tek tek elemanlarına şu şekilde erişebiliriz: +Form ve tek tek öğeleri bileşen olarak adlandırılır. Kökü form olan bir bileşen ağacı oluştururlar. Tek tek form öğelerine şöyle erişebilirsiniz: ```php $input = $form->getComponent('name'); -// alternatif sözdizimi: $input = $form['name']; +// alternatif söz dizimi: $input = $form['name']; $button = $form->getComponent('send'); -// alternatif sözdizimi: $button = $form['send']; +// alternatif söz dizimi: $button = $form['send']; ``` -Elemanlar unset kullanılarak kaldırılır: +Öğeler `unset` ile kaldırılır: ```php unset($form['name']); @@ -90,26 +96,26 @@ unset($form['name']); Doğrulama Kuralları =================== -*Geçerli* kelimesini kullandık, ancak formun henüz herhangi bir doğrulama kuralı yok. Bunu düzeltelim. +*Geçerli* sözcüğü geçti, ama formun henüz hiçbir doğrulama kuralı yok. Bunu düzeltelim. -İsim zorunlu olacak, bu yüzden onu `setRequired()` metoduyla işaretleyeceğiz. Argümanı, kullanıcı ismi doldurmazsa görüntülenecek hata mesajının metnidir. Argüman belirtmezsek, varsayılan hata mesajı kullanılır. +Ad zorunlu olacak, bu yüzden onu `setRequired()` metoduyla işaretliyoruz. Argümanı, kullanıcı adı doldurmazsa görüntülenecek hata mesajının metnidir. Argüman verilmezse varsayılan hata mesajı kullanılır. ```php -$form->addText('name', 'İsim:') - ->setRequired('Lütfen bir isim girin'); +$form->addText('name', 'Ad:') + ->setRequired('Lütfen bir ad girin.'); ``` -Formu doldurulmuş bir isim olmadan göndermeyi deneyin ve bir hata mesajının görüntüleneceğini ve tarayıcının veya sunucunun alanı doldurana kadar reddedeceğini göreceksiniz. +Formu adı doldurmadan göndermeyi deneyin; bir hata mesajının çıktığını göreceksiniz. Tarayıcı ya da sunucu, siz alanı doldurana dek onu reddedecek. -Aynı zamanda, alana sadece boşluk yazarak sistemi aldatamazsınız. Hayır. Nette sol ve sağ boşlukları otomatik olarak kaldırır. Deneyin. Bu, her tek satırlık girişle her zaman yapmanız gereken bir şeydir, ancak genellikle unutulur. Nette bunu otomatik olarak yapar. (Formu aldatmayı deneyebilir ve isim olarak çok satırlı bir dize gönderebilirsiniz. Nette burada bile kafası karışmaz ve satır sonlarını boşluklara dönüştürür.) +Aynı zamanda, girdiye yalnızca boşluk yazarak sistemi kandıramazsınız. Mümkün değil. Nette, baştaki ve sondaki boşlukları otomatik olarak kırpar. Deneyin. Bu, her tek satırlık girdide her zaman yapmanız gereken, ama sık sık unutulan bir şeydir. Nette onu otomatik yapar. (Formu kandırmak için ad olarak çok satırlı bir dize göndermeyi deneyebilirsiniz. Burada da Nette kanmaz ve satır sonları boşluğa dönüştürülür.) -Form her zaman sunucu tarafında doğrulanır, ancak aynı zamanda anında çalışan ve kullanıcının hatayı formu sunucuya göndermeye gerek kalmadan hemen öğrenmesini sağlayan JavaScript doğrulaması da oluşturulur. Bu, `netteForms.js` betiği tarafından halledilir. Sayfaya ekleyin: +Form her zaman sunucu tarafında doğrulanır, ama bir JavaScript doğrulaması da üretilir. Bu anında çalışır ve kullanıcı, formu sunucuya göndermeye gerek kalmadan hataları hemen öğrenir. Bunu `netteForms.js` betiği üstlenir. Onu sayfaya ekleyin: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Formu içeren sayfanın kaynak koduna bakarsanız, Nette'nin zorunlu elemanları `required` CSS sınıfına sahip elemanlara eklediğini fark edebilirsiniz. Şablona aşağıdaki stil sayfasını eklemeyi deneyin ve "İsim" etiketi kırmızı olacaktır. Bu şekilde, zorunlu elemanları kullanıcılara zarif bir şekilde işaretleriz: +Formun bulunduğu sayfanın kaynak koduna bakarsanız, Nette'in zorunlu öğeleri `required` CSS sınıfına sahip elemanların içine koyduğunu fark edebilirsiniz. Şablona aşağıdaki stil sayfasını eklemeyi deneyin; "Ad" etiketi kırmızıya dönecek. Bu, zorunlu öğeleri kullanıcılar için şık biçimde vurgular: ```latte <style> @@ -117,85 +123,102 @@ Formu içeren sayfanın kaynak koduna bakarsanız, Nette'nin zorunlu elemanları </style> ``` -Diğer doğrulama kurallarını `addRule()` metoduyla ekleriz. İlk parametre kuraldır, ikincisi yine hata mesajının metnidir ve bunu doğrulama kuralının argümanı takip edebilir. Bu ne anlama geliyor? +Başka doğrulama kurallarını `addRule()` metoduyla ekleriz. İlk parametre kural, ikincisi yine hata mesajının metni, ardından da isteğe bağlı bir doğrulama kuralı argümanı gelebilir. Bu ne demek? -Formu, tamsayı olması gereken (`addInteger()`) ve ayrıca izin verilen bir aralıkta olması gereken (`$form::Range`) yeni bir isteğe bağlı "yaş" alanı ile genişleteceğiz. Ve burada, doğrulayıcıya istenen aralığı `[başlangıç, bitiş]` çifti olarak ilettiğimiz `addRule()` metodunun üçüncü parametresini kullanacağız: +Formu, tam sayı olması (`addInteger()`) ve izin verilen bir aralıkta bulunması (`$form::Range`) gereken yeni ve isteğe bağlı bir "yaş" alanıyla genişletelim. Burada, gereken aralığı doğrulayıcıya `[min, max]` çifti olarak aktarmak için `addRule()` metodunun üçüncü parametresini kullanacağız: ```php $form->addInteger('age', 'Yaş:') - ->addRule($form::Range, 'Yaş 18 ile 120 arasında olmalıdır', [18, 120]); + ->addRule($form::Range, 'Yaş 18 ile 120 arasında olmalıdır.', [18, 120]); ``` .[tip] -Kullanıcı alanı doldurmazsa, eleman isteğe bağlı olduğu için doğrulama kuralları kontrol edilmeyecektir. +Kullanıcı alanı doldurmazsa doğrulama kuralları denetlenmez, çünkü öğe isteğe bağlıdır. -Burada küçük bir yeniden düzenleme için yer var. Hata mesajında ve üçüncü parametrede sayılar yineleniyor, bu ideal değil. Sayıları içeren bir mesaj [çok dilli formlar |rendering#Çeviri] oluşturursak ve birden fazla dile çevrilirse, değerleri değiştirmek zorlaşır. Bu nedenle, `%d` yer tutucularını kullanmak mümkündür ve Nette değerleri tamamlayacaktır: +Bu, küçük bir yeniden düzenlemeye yer açar. Hata mesajında ve üçüncü parametrede sayılar yineleniyor; bu ideal değil. [Çok dilli formlar |rendering#Çeviri] yapıyor olsaydık ve sayı içeren mesaj birden çok dile çevrilseydi, değerleri değiştirmek zorlaşırdı. Bu nedenle `%d` yer tutucuları kullanılabilir ve Nette değerleri doldurur: ```php - ->addRule($form::Range, 'Yaş %d ile %d arasında olmalıdır', [18, 120]); + ->addRule($form::Range, 'Yaş %d ile %d yaş arasında olmalıdır.', [18, 120]); ``` -`password` elemanına geri dönelim, onu da zorunlu hale getireceğiz ve ayrıca şifrenin minimum uzunluğunu (`$form::MinLength`) doğrulayacağız, yine yer tutucu kullanarak: +`password` öğesine dönelim, onu da zorunlu yapalım ve ayrıca en az parola uzunluğunu (`$form::MinLength`) doğrulayalım; yine mesajda bir yer tutucu kullanarak: ```php -$form->addPassword('password', 'Şifre:') - ->setRequired('Bir şifre seçin') - ->addRule($form::MinLength, 'Şifre en az %d karakter uzunluğunda olmalıdır', 8); +$form->addPassword('password', 'Parola:') + ->setRequired('Bir parola seçin') + ->addRule($form::MinLength, 'Parola en az %d karakter uzunluğunda olmalıdır', 8); ``` -Forma bir `passwordVerify` alanı daha ekleyelim, burada kullanıcı kontrol için şifreyi tekrar girecektir. Doğrulama kurallarını kullanarak her iki şifrenin de aynı olup olmadığını kontrol edeceğiz (`$form::Equal`). Ve parametre olarak, [köşeli parantezler |#Elemanlara Erişim] kullanarak ilk şifreye bir referans vereceğiz: +Forma, kullanıcının doğrulama için parolayı yeniden girdiği `passwordVerify` adlı bir alan daha ekleyelim. Doğrulama kurallarıyla iki parolanın aynı olup olmadığını denetliyoruz (`$form::Equal`). Parametre olarak ilk parolaya [köşeli parantezlerle |#Öğelere Erişim] bir referans veriyoruz: ```php -$form->addPassword('passwordVerify', 'Kontrol için şifre:') - ->setRequired('Lütfen kontrol için şifreyi tekrar girin') - ->addRule($form::Equal, 'Şifreler eşleşmiyor', $form['password']) +$form->addPassword('passwordVerify', 'Parola tekrar:') + ->setRequired('Doğrulama için lütfen parolayı yeniden girin') + ->addRule($form::Equal, 'Parolalar eşleşmiyor', $form['password']) ->setOmitted(); ``` -`setOmitted()` kullanarak, değerine aslında önem vermediğimiz ve yalnızca doğrulama amacıyla var olan bir elemanı işaretledik. Değer `$data`'ya iletilmeyecektir. +`setOmitted()` ile, değeri aslında bizi ilgilendirmeyen ve yalnızca doğrulama amacıyla var olan bir öğeyi işaretledik. Değeri `$data` içine aktarılmaz. -Bununla, PHP ve JavaScript'te doğrulamaya sahip tamamen işlevsel bir formumuz var. Nette'nin doğrulama yetenekleri çok daha geniştir, koşullar oluşturabilir, bunlara göre sayfanın bölümlerini gösterebilir ve gizleyebilir vb. Her şeyi [form doğrulama |validation] bölümünde öğreneceksiniz. +Böylece hem PHP hem JavaScript doğrulamalı, tam işleyen bir formumuz oldu. Nette'in doğrulama yetenekleri çok daha geniştir; koşullar oluşturabilir, onlara göre sayfanın parçalarını gösterip gizleyebilirsiniz vb. Her şeyi [form doğrulama |validation] bölümünde öğreneceksiniz. Varsayılan Değerler =================== -Form elemanlarına genellikle varsayılan değerler atarız: +Form öğeleri için sık sık varsayılan değerler ayarlarız: ```php $form->addEmail('email', 'E-posta') ->setDefaultValue($lastUsedEmail); ``` -Genellikle tüm elemanlara aynı anda varsayılan değerler atamak kullanışlıdır. Örneğin, form kayıtları düzenlemek için kullanıldığında. Veritabanından kaydı okur ve varsayılan değerleri ayarlarız: +Tüm öğeler için varsayılan değerleri aynı anda ayarlamak çoğu zaman işe yarar; örneğin form kayıt düzenlemek için kullanıldığında. Kaydı veritabanından okur ve değerlerini varsayılan olarak ayarlarız: ```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; +// $row = ['name' => 'John', 'age' => '33', /* ... */]; $form->setDefaults($row); ``` -Elemanları tanımladıktan sonra `setDefaults()` çağırın. +`setDefaults()` metodunu öğeleri tanımladıktan sonra çağırın. + +Zaten gönderilmiş bir formda `setDefaults()` etkisizdir; kullanıcının doldurduğunun üzerine yazmaz, bu yüzden onu form factory'sinde koşulsuz çağırmak güvenlidir. Değerleri gönderimden sonra da zorlamanız gerekiyorsa bunun yerine `setValues()` kullanın. -Formu Oluşturma -=============== +Formun Render Edilmesi +====================== -Varsayılan olarak, form bir tablo olarak oluşturulur. Tek tek elemanlar temel erişilebilirlik kuralını karşılar - tüm etiketler `<label>` olarak yazılır ve ilgili form elemanıyla ilişkilendirilir. Etikete tıklandığında, imleç otomatik olarak form alanında görünür. +Form varsayılan olarak bir tablo olarak render edilir. Tek tek öğeler temel erişilebilirlik yönergelerine uyar; tüm etiketler `<label>` elemanı olarak üretilir ve ilgili form öğeleriyle ilişkilendirilir. Bir etikete tıklamak imleci otomatik olarak form alanına koyar. -Her elemana herhangi bir HTML niteliği atayabiliriz. Örneğin, bir yer tutucu ekleyin: +Her öğe için istediğimiz HTML niteliklerini ayarlayabiliriz. Örneğin bir placeholder ekleyelim: ```php $form->addInteger('age', 'Yaş:') - ->setHtmlAttribute('placeholder', 'Lütfen yaşınızı girin'); + ->setHtmlAttribute('placeholder', 'Lütfen yaşı girin'); ``` -Bir formu oluşturmanın gerçekten çok sayıda yolu vardır, bu yüzden [rendering hakkında ayrı bölüm |rendering] buna ayrılmıştır. +Bir formu render etmenin pek çok yolu var, bu yüzden [render'a ayrı bir bölüm |rendering] ayrıldı. + + +Latte ile Render +---------------- + +Elinizin altında [Latte |latte:] şablon motoru varsa, formu ona render ettirip ortaya çıkan HTML üzerinde tam denetim kazanabilirsiniz. Motoru oluşturur, forms extension'ını kaydeder ve formu şablona bir değişken olarak aktarırsınız: + +```php +$latte = new Latte\Engine; +$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); + +$latte->render('form.latte', ['form' => $form]); +``` + +Şablonda ise formla `$form` değişkeni ve `{input}`, `{label}` ya da `n:name` gibi etiketler üzerinden çalışırsınız. Şablonu da içeren eksiksiz bir örnek [örnekler |https://github.com/nette/forms/tree/master/examples] dizininde bulunabilir (`latte.php` dosyası ve `latte/` klasörü). Tek tek etiketler [render |rendering] bölümünde anlatılıyor. Sınıflara Eşleme ================ -Form verilerinin işlenmesine geri dönelim. `getValues()` metodu bize gönderilen verileri `ArrayHash` nesnesi olarak döndürdü. Bu genel bir sınıf olduğundan, `stdClass` gibi bir şey olduğundan, onunla çalışırken belirli bir rahatlıktan yoksun olacağız, örneğin editörlerde özelliklerin otomatik tamamlanması veya statik kod analizi. Bu, her form için özelliklerinin tek tek elemanları temsil ettiği belirli bir sınıfa sahip olarak çözülebilir. Örneğin: +Form verisinin işlenmesine dönelim. `getValues()` metodu, gönderilen veriyi bir `ArrayHash` nesnesi olarak döndürdü. Bu, `stdClass` gibi genel bir sınıf olduğundan, onunla çalışırken düzenleyicilerde özellik tamamlama ya da statik kod çözümlemesi gibi bazı kolaylıklardan yoksun kalırız. Bu, her form için, özellikleri tek tek öğeleri temsil eden özel bir sınıf yazılarak çözülebilir. Örneğin: ```php class RegistrationFormData @@ -206,7 +229,7 @@ class RegistrationFormData } ``` -Alternatif olarak, bir kurucu kullanabilirsiniz: +Alternatif olarak yapıcıyı kullanabilirsiniz: ```php class RegistrationFormData @@ -220,18 +243,18 @@ class RegistrationFormData } ``` -Veri sınıfının özellikleri enum'lar da olabilir ve bunlar otomatik olarak eşlenir. .{data-version:3.2.4} +Veri sınıfının özellikleri enum da olabilir ve otomatik olarak eşlenirler. .{data-version:3.2.4} -Nette'ye verileri bu sınıfın nesneleri olarak döndürmesini nasıl söyleyebiliriz? Düşündüğünüzden daha kolay. Sınıf adını veya hidratlanacak nesneyi parametre olarak belirtmeniz yeterlidir: +Nette'e veriyi bu sınıfın nesneleri olarak döndürmesini nasıl söyleriz? Sandığınızdan kolay. Tek yapmanız gereken, parametre olarak sınıf adını ya da doldurulacak nesneyi belirtmek: ```php $data = $form->getValues(RegistrationFormData::class); $name = $data->name; ``` -Parametre olarak `'array'` de belirtilebilir ve ardından veriler dizi olarak döndürülür. +Parametre olarak `'array'` da belirtebilirsiniz; o zaman veri dizi olarak döndürülür. -Formlar konteynerlerden oluşan çok seviyeli bir yapı oluşturuyorsa, her biri için ayrı bir sınıf oluşturun: +Formlar container'lardan oluşan çok düzeyli bir yapıdan oluşuyorsa, her biri için ayrı bir sınıf oluşturun: ```php $form = new Form; @@ -253,19 +276,19 @@ class RegistrationFormData } ``` -Eşleme daha sonra `$person` özelliğinin türünden konteyneri `PersonFormData` sınıfına eşlemesi gerektiğini anlar. Özellik bir konteyner dizisi içeriyorsa, `array` türünü belirtin ve eşlenecek sınıfı doğrudan konteynere iletin: +Eşleme, `$person` özelliğinin türünden container'ı `PersonFormData` sınıfına eşlemesi gerektiğini bilir. Özellik container dizisi içerecekse `array` türünü belirtin ve eşlenecek sınıfı doğrudan container'a verin: ```php $person->setMappedType(PersonFormData::class); ``` -Form veri sınıfının tasarımını `Nette\Forms\Blueprint::dataClass($form)` metodunu kullanarak oluşturabilirsiniz, bu da onu tarayıcı sayfasına yazdırır. Ardından kodu tıklayarak işaretleyip projenize kopyalamanız yeterlidir. .{data-version:3.1.15} +Formun veri sınıfı için bir taslağı, onu tarayıcı sayfasına yazdıran `Nette\Forms\Blueprint::dataClass($form)` metoduyla ürettirebilirsiniz. Sonra yalnızca tıklayıp kodu seçin ve projenize kopyalayın. .{data-version:3.1.15} -Çoklu Düğmeler -============== +Birden Çok Gönder Düğmesi +========================= -Formun birden fazla düğmesi varsa, genellikle hangisinin basıldığını ayırt etmemiz gerekir. Düğmenin `isSubmittedBy()` metodu bize bu bilgiyi döndürür: +Formun birden çok düğmesi varsa, genellikle hangisine basıldığını ayırt etmemiz gerekir. Düğmenin `isSubmittedBy()` metodu bu bilgiyi döndürür: ```php $form->addSubmit('save', 'Kaydet'); @@ -282,36 +305,33 @@ if ($form->isSuccess()) { } ``` -Verilerin geçerliliğini doğrulamak için `$form->isSuccess()` sorgusunu atlamayın. +`$form->isSuccess()` denetimini atlamayın; verinin geçerliliğini o doğrular. -Form <kbd>Enter</kbd> tuşuyla gönderildiğinde, ilk düğmeyle gönderilmiş gibi kabul edilir. +Bir form <kbd>Enter</kbd> tuşuna basılarak gönderildiğinde, ilk düğmeyle gönderilmiş gibi ele alınır. -Güvenlik Açıklarına Karşı Koruma -================================ +Açıklara Karşı Koruma +===================== -Nette Framework güvenliğe büyük önem verir ve bu nedenle formların iyi bir şekilde güvence altına alınmasına özen gösterir. +Nette Framework güvenliğe büyük önem verir ve bu yüzden formların düzgün güvenliğini titizlikle sağlar. -Formları [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] ve [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF] saldırılarına karşı korumanın yanı sıra, artık düşünmeniz gerekmeyen birçok küçük güvenlik önlemi alır. +Formları [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] ve [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)] gibi iyi bilinen açıklara karşı korumanın yanı sıra, artık düşünmenize gerek kalmayan pek çok küçük güvenlik önlemi de alır. -Örneğin, girdilerden tüm kontrol karakterlerini filtreler ve UTF-8 kodlamasının geçerliliğini kontrol eder, böylece form verileri her zaman temiz olur. Seçim kutuları ve radyo listeleri için, seçilen öğelerin gerçekten sunulanlardan olduğunu ve sahtecilik yapılmadığını doğrular. Tek satırlık metin girişlerinde, bir saldırganın oraya göndermiş olabileceği satır sonu karakterlerini kaldırdığını zaten belirtmiştik. Çok satırlı girişler için ise satır sonu karakterlerini normalleştirir. Ve böyle devam eder. +Örneğin girdilerdeki tüm denetim karakterlerini süzer ve UTF-8 kodlamasının geçerliliğini denetler; böylece formdan gelen verinin her zaman temiz olmasını sağlar. Seçim kutuları ve radyo listelerinde, seçilen öğelerin gerçekten sunulan seçenekler arasında olduğunu ve hiçbir sahtecilik yapılmadığını doğrular. Tek satırlık metin girdilerinde, bir saldırganın gönderebileceği satır sonu karakterlerini boşlukla değiştirdiğini zaten söylemiştik. Çok satırlı girdilerde satır sonu karakterlerini normalleştirir. Ve böyle sürer. -Nette, birçok programcının varlığından bile haberdar olmadığı güvenlik risklerini sizin için çözer. +Nette, pek çok programcının var olduğunu bile bilmediği güvenlik risklerini sizin yerinize halleder. -Bahsedilen CSRF saldırısı, bir saldırganın kurbanı, kurbanın oturum açtığı sunucuya fark ettirmeden bir istek gerçekleştiren bir sayfaya çekmesi ve sunucunun isteği kurbanın kendi isteğiyle gerçekleştirdiğini varsaymasıdır. Bu nedenle Nette, POST formunun başka bir alan adından gönderilmesini engeller. Herhangi bir nedenle korumayı kapatmak ve formun başka bir alan adından gönderilmesine izin vermek istiyorsanız, şunu kullanın: +Sözü edilen CSRF saldırısı, bir saldırganın kurbanı, kurbanın tarayıcısında sessizce, kurbanın o an oturum açmış olduğu sunucuya bir istek çalıştıran bir sayfaya çekmesinden ibarettir. Sunucu, isteğin kurban tarafından gönüllü olarak yapıldığına inanır. Bu yüzden Nette, yabancı bir kaynaktan gönderilen POST formlarını reddeder; aynı sitenin farklı bir alt alan adı bile yabancı sayılır. Başka bir kaynaktan gönderime izin vermeniz gerekiyorsa korumayı şununla kapatın: ```php -$form->allowCrossOrigin(); // DİKKAT! Korumayı kapatır! +$form->allowCrossOrigin(); // UYARI! Korumayı tümüyle kapatır! ``` -Bu koruma, `_nss` adlı SameSite çerezini kullanır. Bu nedenle, çerezin gönderilebilmesi için ilk çıktıyı göndermeden önce form nesnesini oluşturun. - -SameSite çerezi ile koruma %100 güvenilir olmayabilir, bu nedenle token ile korumayı da etkinleştirmek iyi bir fikirdir: +Ancak bu, korumayı her kaynak için kapatır. Yalnızca belirli kaynaklara izin vermek için korumayı kapatın ve `Origin` header'ını kendi izin listenize göre kendiniz doğrulayın. -```php -$form->addProtection(); -``` +Koruma, tarayıcının otomatik gönderdiği ve bir XSS açığıyla bile taklit edilemeyen `Sec-Fetch-Site` header'ına (Fetch Metadata) dayanır. Bu header'ları göndermeyen eski tarayıcılar denetimi geçemez. [Tarayıcı sonunda CSRF'yi çözüyor |https://blog.nette.org/en/quarter-century-of-csrf] yazısı bunu ayrıntılı anlatıyor. -Uygulamadaki hassas verileri değiştiren web sitesinin yönetim bölümündeki formları bu şekilde korumanızı öneririz. Framework, oturumda saklanan bir yetkilendirme token'ı oluşturup doğrulayarak CSRF saldırısına karşı kendini savunur. Bu nedenle, formu görüntülemeden önce oturumun açık olması gerekir. Web sitesinin yönetim bölümünde, genellikle kullanıcının oturum açması nedeniyle oturum zaten başlatılmıştır. Aksi takdirde, oturumu `Nette\Http\Session::start()` metoduyla başlatın. +.[note] +Oturumda saklanan bir yetkilendirme token'ıyla yapılan ve `$form->addProtection()` ile etkinleştirilen önceki koruma artık gerekmiyor ve 3.3 sürümünden beri kullanımdan kaldırıldı. -İşte Nette'deki formlara hızlı bir giriş yaptık. Daha fazla ilham almak için dağıtımdaki [examples|https://github.com/nette/forms/tree/master/examples] dizinine göz atmayı deneyin. +Böylece Nette'te formlara hızlı bir giriş yaptık. Daha fazla ilham için dağıtımdaki [örnekler |https://github.com/nette/forms/tree/master/examples] dizinine bakmayı deneyin. diff --git a/forms/tr/upgrading.texy b/forms/tr/upgrading.texy new file mode 100644 index 0000000000..3c7ecbff17 --- /dev/null +++ b/forms/tr/upgrading.texy @@ -0,0 +1,54 @@ +Yükseltme +********* + + +Sürüm 3.3'e Yükseltme +===================== + +- otomatik CSRF koruması `isSameSite()` yerine `isFrom(FetchSite::SameOrigin)` kullanmaya geçti ve katılaştı: alt alan adlarından gelen istekler artık geçmiyor +- bu sayede `addProtection()` artık gerekmiyor, çünkü otomatik koruma aynı durumları kapsıyor; yeni formlarda onu atlayın ve var olanlardan çekinmeden kaldırın + +Oturumdaki token'ların neden artık gerekmediği [Çeyrek yüzyıllık CSRF |https://blog.nette.org/en/quarter-century-of-csrf] yazısında anlatılıyor. + + +Sürüm 3.1'e Yükseltme +===================== + +- `getValues()` yalnızca doğrulanan öğeleri döndürüyor; doğrulamadan bağımsız olarak tüm öğelerin değerlerine ihtiyacınız varsa yeni `getUntrustedValues()` metodunu kullanın +- `onSuccess` ve `onClick` işleyicilerine aktarılan `$values` de aynı şekilde yalnızca doğrulanan öğeleri içeriyor +- bağımsız formlar, SameSite bayraklı bir çerezle CSRF'ye karşı otomatik olarak korunuyor; `allowCrossOrigin()` ile başka bir kaynaktan gönderime izin verebilirsiniz +- `Form::URL` kuralı artık eksik protokolü `http` yerine `https` ile tamamlıyor +- `Form::addImage()` metodunun adı `addImageButton()` oldu +- `Checkbox::getSeparatorPrototype()` metodunun adı `getContainerPrototype()` oldu +- formlar artık şablonlarda `$_form` değişkenini oluşturmuyor + +Bu değişiklikler hakkında daha fazlası [Nette Forms 3.1'deki yenilikler |https://blog.nette.org/en/news-in-nette-forms-3-1] yazısında. + + +Sürüm 3.0'a Yükseltme +===================== + +- artık tüm form öğeleri varsayılan olarak isteğe bağlı (bu değişiklik Nette 2.4'te geldi), dolayısıyla `setRequired(false)` çağrılarını kaldırabilirsiniz +- `netteForms.js` dosyasını 3. sürüme güncellemeyi unutmayın (`npm install nette-forms`) +- `ChoiceControl::$checkAllowedValues` ve `MultiChoiceControl::$checkAllowedValues` yerini `checkDefaultValue()` metoduna bıraktı + + +Sürüm 2.4'e Yükseltme +===================== + +- bir öğenin `addRule()` ile kuralı varsa (yani fiilen zorunluysa), onu `setRequired()` ile de zorunlu işaretlemelisiniz; ayrıca `setRequired(false)` artık öğeyi isteğe bağlı yapıyor ve bu, `addCondition($form::FILLED)` dallarının yerini alıyor +- `Form::EMAIL`, `URL` ve `INTEGER` doğrulayıcıları HTML `type` niteliğini sırasıyla otomatik olarak `email`, `url` ve `number` yapıyor +- olumsuz doğrulama kuralları kullanımdan kaldırıldı; `~Form::FILLED` yerine `Form::BLANK`, `~Form::EQUAL` yerine ise `Form::NOT_EQUAL` kullanılabilir +- iç parametre `do`, çakışmayı önlemek için artık POST ile `_do` olarak gönderiliyor +- `$_form` gibi alt çizgili iç değişkenler kullanımdan kaldırıldı +- `netteForms.js` dosyasını güncellemeyi unutmayın + + +Sürüm 2.3'e Yükseltme +===================== + +- `Nette\Forms\Controls\TextBase::filterFloat` gibi iç filtreleme metotları kaldırıldı +- `TextBase::validateFloat` gibi iç doğrulama metotları, `Rules::$defaultMessages` ile birlikte `Nette\Forms\Validator` sınıfına taşındı +- Button ve Hidden alanları HTML ID'si olmadan üretiliyor; ID istiyorsanız `setHtmlId()` ile ayarlayın +- RadioList öğeleri de ID olmadan üretiliyor; bunu `$radioList->generateId = true` ile açabilirsiniz +- `TextBase::addFilter()` ile eklenen filtreler doğrulama sırasında işleniyor ve artık koşullara da filtre ekleyebilirsiniz: `$input->addCondition(...)->addFilter(...)` diff --git a/forms/tr/validation.texy b/forms/tr/validation.texy index 545442c823..c980acb618 100644 --- a/forms/tr/validation.texy +++ b/forms/tr/validation.texy @@ -2,107 +2,107 @@ Form Doğrulama ************** -Zorunlu Elemanlar -================= +Zorunlu Öğeler +============== -Zorunlu elemanları `setRequired()` metoduyla işaretleriz. Argümanı, kullanıcı elemanı doldurmazsa görüntülenecek [#hata mesajları] metnidir. Argüman belirtmezsek, varsayılan hata mesajı kullanılır. +Öğeler `setRequired()` metoduyla zorunlu işaretlenir. Argümanı, kullanıcı öğeyi doldurmazsa görüntülenecek [hata mesajının |#Hata Mesajları] metnidir. Argüman verilmezse varsayılan hata mesajı kullanılır. ```php -$form->addText('name', 'İsim:') - ->setRequired('Lütfen bir isim girin'); +$form->addText('name', 'Ad:') + ->setRequired('Lütfen adınızı girin.'); ``` Kurallar ======== -Doğrulama kurallarını elemanlara `addRule()` metoduyla ekleriz. İlk parametre kuraldır, ikincisi [#hata mesajları] metnidir ve üçüncüsü doğrulama kuralının argümanıdır. +Öğelere doğrulama kurallarını `addRule()` metoduyla ekleriz. İlk parametre kural, ikincisi [hata mesajı |#Hata Mesajları], üçüncüsü ise doğrulama kuralının argümanıdır. ```php -$form->addPassword('password', 'Şifre:') - ->addRule($form::MinLength, 'Şifre en az %d karakter uzunluğunda olmalıdır', 8); +$form->addPassword('password', 'Parola:') + ->addRule($form::MinLength, 'Parola en az %d karakter uzunluğunda olmalıdır', 8); ``` -**Doğrulama kuralları yalnızca kullanıcı elemanı doldurduğunda kontrol edilir.** +**Doğrulama kuralları yalnızca kullanıcı öğeyi doldurduysa denetlenir.** -Nette, adları `Nette\Forms\Form` sınıfının sabitleri olan bir dizi önceden tanımlanmış kuralla birlikte gelir. Tüm elemanlar için şu kuralları kullanabiliriz: +Nette, adları `Nette\Forms\Form` sınıfının sabitleri olan birkaç önceden tanımlı kuralla gelir. Bu kuralları tüm öğelere uygulayabiliriz: | sabit | açıklama | argüman türü |------- -| `Required` | zorunlu eleman, `setRequired()` için takma ad | - -| `Filled` | zorunlu eleman, `setRequired()` için takma ad | - -| `Blank` | eleman doldurulmamalıdır | - -| `Equal` | değer parametreye eşittir | `mixed` -| `NotEqual` | değer parametreye eşit değildir | `mixed` -| `IsIn` | değer dizideki bazı öğelere eşittir | `array` -| `IsNotIn` | değer dizideki hiçbir öğeye eşit değildir | `array` -| `Valid` | eleman doğru doldurulmuş mu? ([#koşullar] için) | - +| `Required` | zorunlu öğe, `setRequired()` için takma ad | - +| `Filled` | zorunlu öğe, `setRequired()` için takma ad | - +| `Blank` | öğe doldurulmamalı | - +| `Equal` | değer parametreye eşit olmalı | `mixed` +| `NotEqual` | değer parametreye eşit olmamalı | `mixed` +| `IsIn` | değer dizideki öğelerden biri olmalı | `array` +| `IsNotIn` | değer dizideki öğelerden hiçbiri olmamalı | `array` +| `Valid` | öğe doğru doldurulmuş mu? (yalnızca [addConditionOn() |#Koşullar] içinde) | - -Metin Girişleri +Metin girdileri --------------- -`addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` elemanları için aşağıdaki kurallardan bazıları da kullanılabilir: +`addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` öğelerinde aşağıdaki kurallardan bazıları da uygulanabilir: -| `MinLength` | minimum metin uzunluğu | `int` -| `MaxLength` | maksimum metin uzunluğu | `int` -| `Length` | aralıktaki uzunluk veya tam uzunluk | çift `[int, int]` veya `int` +| `MinLength` | en az metin uzunluğu | `int` +| `MaxLength` | en fazla metin uzunluğu | `int` +| `Length` | aralıkta uzunluk ya da tam uzunluk | `[int, int]` çifti ya da `int` | `Email` | geçerli e-posta adresi | - | `URL` | mutlak URL | - -| `Pattern` | düzenli ifadeye uyar | `string` -| `PatternInsensitive` | `Pattern` gibi, ancak büyük/küçük harfe duyarsız | `string` -| `Integer` | tamsayı değeri | - -| `Numeric` | `Integer` için takma ad | - +| `Pattern` | düzenli ifadeyle eşleşir | `string` +| `PatternInsensitive` | `Pattern` gibi, ama büyük/küçük harfe duyarsız | `string` +| `Integer` | tam sayı değeri | - +| `Numeric` | negatif olmayan tam sayı (yalnızca rakamlar) | - | `Float` | sayı | - -| `Min` | sayısal elemanın minimum değeri | `int\|float` -| `Max` | sayısal elemanın maksimum değeri | `int\|float` -| `Range` | aralıktaki değer | çift `[int\|float, int\|float]` +| `Min` | sayısal öğenin en küçük değeri | `int\|float` +| `Max` | sayısal öğenin en büyük değeri | `int\|float` +| `Range` | aralıkta değer | `[int\|float, int\|float]` çifti -`Integer`, `Numeric` ve `Float` doğrulama kuralları değeri doğrudan tamsayıya resp. ondalık sayıya dönüştürür. Ve ayrıca `URL` kuralı şemasız bir adresi de kabul eder (ör. `nette.org`) ve şemayı ekler (`https://nette.org`). `Pattern` ve `PatternIcase` içindeki ifade tüm değer için geçerli olmalıdır, yani `^` ve `$` karakterleriyle çevrelenmiş gibi. +`Integer` ve `Float` doğrulama kuralları, değeri sırasıyla tam sayıya ya da kayan noktalı sayıya otomatik dönüştürür. Ayrıca `URL` kuralı şemasız bir adresi de (örneğin `nette.org`) kabul eder ve şemayı tamamlar (`https://nette.org`). `Pattern` ve `PatternInsensitive` içindeki ifade, değerin tamamı için geçerli olmalıdır; yani `^` ve `$` karakterleriyle sarılmış gibi. Öğe Sayısı ---------- -`addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` elemanları için, seçilen öğelerin resp. yüklenen dosyaların sayısını sınırlamak üzere aşağıdaki kurallar da kullanılabilir: +`addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` öğelerinde, seçilen öğelerin ya da yüklenen dosyaların sayısını sınırlamak için şu kuralları da kullanabilirsiniz: -| `MinLength` | minimum sayı | `int` -| `MaxLength` | maksimum sayı | `int` -| `Length` | aralıktaki sayı veya tam sayı | çift `[int, int]` veya `int` +| `MinLength` | en az sayı | `int` +| `MaxLength` | en fazla sayı | `int` +| `Length` | aralıkta sayı ya da tam sayı | `[int, int]` çifti ya da `int` -Dosya Yüklemeleri ------------------ +Dosya Yükleme +------------- -`addUpload()`, `addMultiUpload()` elemanları için aşağıdaki kurallar da kullanılabilir: +`addUpload()`, `addMultiUpload()` öğelerinde şu kurallar da kullanılabilir: -| `MaxFileSize` | bayt cinsinden maksimum dosya boyutu | `int` +| `MaxFileSize` | bayt cinsinden en fazla dosya boyutu | `int` | `MimeType` | MIME türü, joker karakterlere izin verilir (`'video/*'`) | `string\|string[]` -| `Image` | JPEG, PNG, GIF, WebP, AVIF resmi | - -| `Pattern` | dosya adı düzenli ifadeye uyar | `string` -| `PatternInsensitive` | `Pattern` gibi, ancak büyük/küçük harfe duyarsız | `string` +| `Image` | JPEG, PNG, GIF, WebP, AVIF görseli | - +| `Pattern` | dosya adı düzenli ifadeyle eşleşir | `string` +| `PatternInsensitive` | `Pattern` gibi, ama büyük/küçük harfe duyarsız | `string` -`MimeType` ve `Image`, PHP `fileinfo` uzantısını gerektirir. Bir dosyanın veya resmin istenen türde olup olmadığını imzasına göre algılarlar ve **tüm dosyanın bütünlüğünü doğrulamazlar.** Bir resmin hasarlı olup olmadığını, örneğin onu [yüklemeye |http:request#toImage] çalışarak belirleyebilirsiniz. +`MimeType` ve `Image`, `fileinfo` PHP eklentisini gerektirir. Bir dosyanın ya da görselin gereken türde olup olmadığı imzasına göre saptanır ve **dosyanın tamamının bütünlüğü denetlenmez.** Bir görselin bozuk olup olmadığını örneğin onu [yüklemeyi |http:request#toImage()] deneyerek belirleyebilirsiniz. Hata Mesajları ============== -`Pattern` ve `PatternInsensitive` hariç tüm önceden tanımlanmış kuralların varsayılan bir hata mesajı vardır, bu nedenle atlanabilir. Ancak, tüm mesajları özel olarak belirterek ve formüle ederek formu kullanıcı dostu hale getirebilirsiniz. +`Pattern` ve `PatternInsensitive` dışındaki tüm önceden tanımlı kuralların varsayılan bir hata mesajı vardır, bu yüzden atlanabilirler. Ancak tüm özel mesajları ihtiyacınıza göre verip biçimlendirerek formu daha kullanıcı dostu kılarsınız. -Varsayılan mesajları [yapılandırmada |forms:configuration], `Nette\Forms\Validator::$messages` dizisindeki metinleri düzenleyerek veya [çevirmen |rendering#Çeviri] kullanarak değiştirebilirsiniz. +Varsayılan mesajları [yapılandırmada |forms:configuration], `Nette\Forms\Validator::$messages` dizisindeki metinleri değiştirerek ya da bir [çevirmen |rendering#Çeviri] kullanarak değiştirebilirsiniz. -Hata mesajlarının metninde şu yer tutucu dizeler kullanılabilir: +Hata mesajlarının metninde şu yer tutucu dizeleri kullanılabilir: -| `%d` | kural argümanlarıyla sırayla değiştirilir +| `%d` | sırayla kural argümanlarıyla değiştirilir | `%n$d` | n'inci kural argümanıyla değiştirilir -| `%label` | eleman etiketiyle değiştirilir (iki nokta üst üste olmadan) -| `%name` | eleman adıyla değiştirilir (ör. `name`) -| `%value` | kullanıcı tarafından girilen değerle değiştirilir +| `%label` | öğenin etiketiyle değiştirilir (iki nokta olmadan) +| `%name` | öğenin adıyla değiştirilir (örneğin `name`) +| `%value` | kullanıcının girdiği değerle değiştirilir ```php -$form->addText('name', 'İsim:') - ->setRequired('Lütfen %label girin'); +$form->addText('name', 'Ad:') + ->setRequired('Lütfen %label doldurun'); $form->addInteger('id', 'ID:') ->addRule($form::Range, 'en az %d ve en fazla %d', [5, 10]); @@ -115,70 +115,78 @@ $form->addInteger('id', 'ID:') Koşullar ======== -Kurallara ek olarak koşullar da eklenebilir. Bunlar kurallara benzer şekilde yazılır, ancak `addRule()` yerine `addCondition()` metodunu kullanırız ve tabii ki herhangi bir hata mesajı belirtmeyiz (koşul sadece sorar): +Kuralların yanı sıra koşullar da eklenebilir. Kurallara benzer biçimde yazılırlar, ama `addRule()` yerine `addCondition()` metodunu kullanırız ve doğal olarak hata mesajı vermeyiz (koşul yalnızca sorar): ```php -$form->addPassword('password', 'Şifre:') - // şifre 8 karakterden uzun değilse +$form->addPassword('password', 'Parola:') + // parolanın uzunluğu 8'den büyük değilse ->addCondition($form::MaxLength, 8) - // o zaman bir rakam içermelidir + // o zaman bir rakam içermeli ->addRule($form::Pattern, 'Bir rakam içermelidir', '.*[0-9].*'); ``` -Koşul, `addConditionOn()` kullanarak geçerli olandan başka bir elemana da bağlanabilir. İlk parametre olarak elemana bir referans belirtiriz. Bu örnekte, e-posta yalnızca onay kutusu işaretlendiğinde (değeri true olacaktır) zorunlu olacaktır: +Koşul, `addConditionOn()` ile geçerli öğe dışındaki bir öğeye bağlanabilir. İlk parametre öğeye bir referanstır. Bu örnekte e-posta yalnızca onay kutusu işaretliyse (yani değeri true ise) zorunlu olacak: ```php -$form->addCheckbox('newsletters', 'bana bülten gönder'); +$form->addCheckbox('newsletters', 'Bana bülten gönderin'); $form->addEmail('email', 'E-posta:') // onay kutusu işaretliyse ->addConditionOn($form['newsletters'], $form::Equal, true) - // o zaman e-posta iste - ->setRequired('Bir e-posta adresi girin'); + // o zaman e-postayı zorunlu kıl + ->setRequired('E-posta adresinizi girin'); ``` -Koşullardan `elseCondition()` ve `endCondition()` kullanarak karmaşık yapılar oluşturulabilir: +Koşullar, `elseCondition()` ve `endCondition()` ile karmaşık yapılara dönüştürülebilir: ```php $form->addText(/* ... */) - ->addCondition(/* ... */) // ilk koşul karşılanırsa - ->addConditionOn(/* ... */) // ve başka bir eleman üzerinde ikinci koşul - ->addRule(/* ... */) // bu kuralı iste - ->elseCondition() // ikinci koşul karşılanmazsa - ->addRule(/* ... */) // bu kuralları iste + ->addCondition(/* ... */) // ilk koşul sağlanırsa + ->addConditionOn(/* ... */) // ve başka bir öğedeki ikinci koşul da sağlanırsa + ->addRule(/* ... */) // bu kuralı zorunlu kıl + ->elseCondition() // ikinci koşul sağlanmazsa + ->addRule(/* ... */) // bu kuralları zorunlu kıl ->addRule(/* ... */) - ->endCondition() // ilk koşula geri dönüyoruz + ->endCondition() // ilk koşula geri döneriz ->addRule(/* ... */); ``` -Nette'de, `toggle()` metodunu kullanarak JavaScript tarafında bir koşulun karşılanmasına veya karşılanmamasına çok kolay bir şekilde yanıt verilebilir, bkz. [#dinamik-javascript]. +`addCondition()` metodunun ilk argümanı boolean bir değer de olabilir. Bu, karar form kurulurken zaten belliyse işe yarar; örneğin bir kuralı yalnızca belirli koşullarda uygulamak için: + +```php +$form->addText('nickname') + ->addCondition($isRequired) // form kurulurken bilinen bir değer + ->setRequired(); +``` + +Nette'te, bir koşulun sağlanmasına ya da sağlanmamasına JavaScript tarafında tepki vermek `toggle()` metoduyla çok kolaydır, bkz. [#Dinamik JavaScript]. -Başka Bir Elemana Referans -========================== +Başka Bir Öğeye Referans +======================== -Kural veya koşul argümanı olarak formun başka bir elemanı da iletilebilir. Kural daha sonra tarayıcıda kullanıcı tarafından daha sonra girilen değeri kullanır. Bu şekilde, örneğin `password` elemanının `password_confirm` elemanıyla aynı dizeyi içerip içermediğini dinamik olarak doğrulayabilirsiniz: +Bir kurala ya da koşula argüman olarak başka bir form öğesi de aktarabilirsiniz. Kural o zaman kullanıcının tarayıcıda sonradan girdiği değeri kullanır. Bu, örneğin `password` öğesinin `password_confirm` öğesiyle aynı dizeyi içerdiğini dinamik olarak doğrulamak için kullanılabilir: ```php -$form->addPassword('password', 'Şifre'); -$form->addPassword('password_confirm', 'Şifreyi onayla') - ->addRule($form::Equal, 'Girilen şifreler eşleşmiyor', $form['password']); +$form->addPassword('password', 'Parola'); +$form->addPassword('password_confirm', 'Parolayı doğrulayın') + ->addRule($form::Equal, 'Parolalar eşleşmiyor', $form['password']); ``` Özel Kurallar ve Koşullar ========================= -Bazen Nette'deki yerleşik doğrulama kurallarının yeterli olmadığı ve kullanıcı verilerini kendi yöntemimizle doğrulamamız gereken bir duruma geliriz. Nette'de bu çok basittir! +Bazen Nette'in yerleşik doğrulama kurallarının yetmediği ve kullanıcı verisini kendi yolumuzla doğrulamamız gereken durumlarla karşılaşırız. Nette'te bu çok basittir! -`addRule()` veya `addCondition()` metotlarına ilk parametre olarak herhangi bir geri arama iletilebilir. Bu, ilk parametre olarak elemanın kendisini alır ve doğrulamanın düzgün bir şekilde yapılıp yapılmadığını belirten bir boole değeri döndürür. `addRule()` kullanarak bir kural eklerken, ek argümanlar da belirtilebilir, bunlar daha sonra ikinci parametre olarak iletilir. +`addRule()` ya da `addCondition()` metotlarına ilk parametre olarak herhangi bir callback aktarabilirsiniz. Callback, ilk parametre olarak öğenin kendisini alır ve doğrulamanın başarılı olup olmadığını gösteren boolean bir değer döndürür. `addRule()` ile kural eklerken ek argümanlar verilebilir; bunlar ikinci parametre olarak aktarılır. -Böylece statik metotlara sahip bir sınıf olarak kendi doğrulayıcı setimizi oluşturabiliriz: +Böylece özel bir doğrulayıcı kümesi, statik metotlar içeren bir sınıf olarak yazılabilir: ```php class MyValidators { - // değerin argümana bölünüp bölünemediğini test eder + // değerin argümana bölünüp bölünmediğini sınar public static function validateDivisibility(BaseControl $input, $arg): bool { return $input->getValue() % $arg === 0; @@ -191,7 +199,7 @@ class MyValidators } ``` -Kullanım daha sonra çok basittir: +Kullanımı ise çok dolaysızdır: ```php $form->addInteger('num') @@ -202,7 +210,7 @@ $form->addInteger('num') ); ``` -Özel doğrulama kuralları JavaScript'e de eklenebilir. Koşul, kuralın statik bir metot olmasıdır. JavaScript doğrulayıcısı için adı, ters eğik çizgiler `\` olmadan sınıf adının, alt çizgi `_` ve metot adının birleştirilmesiyle oluşturulur. Örneğin, `App\MyValidators::validateDivisibility` `AppMyValidators_validateDivisibility` olarak yazılır ve `Nette.validators` nesnesine eklenir: +Özel doğrulama kuralları JavaScript'e de eklenebilir. Koşul, kuralın statik bir metot olmasıdır. JavaScript doğrulayıcısı için adı; sınıf adının ters eğik çizgiler `\` olmadan, bir alt çizgi `_` ve metot adının birleştirilmesiyle oluşur. Örneğin `App\MyValidators::validateDivisibility`, `AppMyValidators_validateDivisibility` olarak yazılır ve `Nette.validators` nesnesine eklenir: ```js Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { @@ -214,32 +222,32 @@ Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => onValidate Olayı ================ -Form gönderildikten sonra, `addRule()` kullanılarak eklenen tek tek kuralların kontrol edildiği doğrulama gerçekleştirilir ve ardından [olay |nette:glossary#Olaylar Events] `onValidate` tetiklenir. İşleyicisi, ek doğrulama için kullanılabilir, tipik olarak formun birden fazla elemanındaki değerlerin doğru kombinasyonunu doğrulamak için. +Form gönderildikten sonra doğrulama yapılır, `addRule()` ile eklenen tek tek kurallar denetlenir ve ardından `onValidate` [olayı |nette:glossary#Olaylar] tetiklenir. İşleyicisi ek doğrulama için kullanılabilir; tipik olarak birden çok form öğesindeki değerlerin doğru bileşimini doğrulamak için. -Bir hata tespit edilirse, `addError()` metoduyla forma iletiriz. Bu, belirli bir eleman üzerinde veya doğrudan form üzerinde çağrılabilir. +Bir hata saptanırsa, `addError()` metoduyla forma aktarılır. Bu metot ya belirli bir öğede ya da doğrudan formda çağrılabilir. ```php protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Bu kombinasyon mümkün değil.'); + $form->addError('Bu bileşim olanaklı değil.'); } } ``` -İşleme Sırasındaki Hatalar -========================== +İşleme Hataları +=============== -Birçok durumda, hatayı ancak geçerli formu işlerken, örneğin veritabanına yeni bir öğe yazarken ve yinelenen anahtarlarla karşılaştığımızda öğreniriz. Bu durumda, hatayı tekrar `addError()` metoduyla forma iletiriz. Bu, belirli bir eleman üzerinde veya doğrudan form üzerinde çağrılabilir: +Pek çok durumda bir hatayı ancak geçerli bir formu işlerken keşfederiz; örneğin veritabanına yeni bir kayıt yazarken yinelenen bir anahtarla karşılaşınca. Böyle bir durumda hatayı yine `addError()` metoduyla forma geri aktarırız. Bu metot ya belirli bir öğede ya da doğrudan formda çağrılabilir: ```php try { @@ -249,82 +257,88 @@ try { } catch (Nette\Security\AuthenticationException $e) { if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Geçersiz şifre.'); + $form->addError('Geçersiz parola.'); } } ``` -Mümkünse, varsayılan oluşturucuyu kullanırken yanında görüntüleneceği için hatayı doğrudan form elemanına eklemenizi öneririz. +Olanaklıysa hatayı doğrudan form öğesine eklemenizi öneririz; çünkü varsayılan renderer kullanıldığında onun yanında görüntülenir. ```php -$form['date']->addError('Üzgünüz, ancak bu tarih zaten alınmış.'); +$form['date']->addError('Üzgünüz, bu tarih zaten alınmış.'); ``` -Forma veya elemana birden fazla hata mesajı iletmek için `addError()`'ı tekrar tekrar çağırabilirsiniz. Bunları `getErrors()` kullanarak alırsınız. +Bir forma ya da öğeye birden çok hata mesajı aktarmak için `addError()` metodunu defalarca çağırabilirsiniz. Onları `getErrors()` ile alabilirsiniz. -Dikkat, `$form->getErrors()` tüm hata mesajlarının bir özetini döndürür, doğrudan tek tek elemanlara iletilenler de dahil olmak üzere, yalnızca doğrudan forma iletilenleri değil. Yalnızca forma iletilen hata mesajlarını `$form->getOwnErrors()` aracılığıyla alırsınız. +`$form->getErrors()` metodunun, yalnızca doğrudan forma değil, tek tek öğelere aktarılanlar da dahil olmak üzere tüm hata mesajlarının bir özetini döndürdüğüne dikkat edin. Yalnızca forma aktarılan hata mesajları `$form->getOwnErrors()` ile alınabilir. -Girişi Değiştirme -================= +Girdi Değerlerini Değiştirme +============================ -`addFilter()` metodunu kullanarak kullanıcı tarafından girilen değeri değiştirebiliriz. Bu örnekte, posta kodlarındaki boşlukları tolere edip kaldıracağız: +`addFilter()` metoduyla, kullanıcının girdiği değeri değiştirebiliriz. Bu örnekte posta kodundaki boşluklara göz yumup onları kaldıracağız: ```php -$form->addText('zip', 'Posta Kodu:') +$form->addText('zip', 'Posta kodu:') ->addFilter(function ($value) { - return str_replace(' ', '', $value); // posta kodundaki boşlukları kaldıracağız + return str_replace(' ', '', $value); // posta kodundaki boşlukları kaldır }) - ->addRule($form::Pattern, 'Posta kodu beş basamaklı biçimde değil', '\d{5}'); + ->addRule($form::Pattern, 'Posta kodu beş rakam değil', '\d{5}'); ``` -Filtre, doğrulama kuralları ve koşulları arasına eklenir ve bu nedenle metotların sırası önemlidir, yani filtre ve kural, `addFilter()` ve `addRule()` metotlarının sırasıyla çağrılır. +Filtre, doğrulama kuralları ve koşulları arasına katılır, dolayısıyla metotların sırası önemlidir; yani filtre ve kural, `addFilter()` ile `addRule()` metotlarının sıralandığı düzende çağrılır. JavaScript Doğrulaması ====================== -Koşulları ve kuralları formüle etme dili çok güçlüdür. Tüm yapılar hem sunucu tarafında hem de JavaScript tarafında çalışır. JSON olarak `data-nette-rules` HTML niteliklerinde iletilirler. Doğrulamanın kendisi daha sonra formun `submit` olayını yakalayan, tek tek elemanları gözden geçiren ve ilgili doğrulamayı gerçekleştiren betik tarafından yapılır. +Koşulları ve kuralları ifade etme dili çok güçlüdür. Tüm yapılar hem sunucu tarafında hem de istemci tarafında JavaScript'te çalışır. `data-nette-rules` HTML niteliklerinde JSON olarak aktarılırlar. Doğrulamanın kendisini, formun `submit` olayını yakalayan, tek tek öğeleri dolaşan ve ilgili doğrulamayı yapan bir betik üstlenir. -Bu betik `netteForms.js`'dir ve birden fazla olası kaynaktan edinilebilir: +Bu betik `netteForms.js`'tir ve birkaç olası kaynaktan edinilebilir: -Betiği doğrudan CDN'den HTML sayfasına ekleyebilirsiniz: +Betiği bir CDN'den doğrudan HTML sayfasına gömebilirsiniz: ```latte <script src="https://unpkg.com/nette-forms@3"></script> ``` -Veya yerel olarak projenin genel klasörüne kopyalayın (ör. `vendor/nette/forms/src/assets/netteForms.min.js`'den): +Ya da onu projenizin genel klasörüne yerel olarak kopyalayabilirsiniz (örneğin `vendor/nette/forms/src/assets/netteForms.min.js` dosyasından): ```latte <script src="/path/to/netteForms.min.js"></script> ``` -Veya [npm|https://www.npmjs.com/package/nette-forms] aracılığıyla yükleyin: +Ya da onu [npm |https://www.npmjs.com/package/nette-forms] ile kurabilirsiniz: ```shell npm install nette-forms ``` -Ve ardından yükleyip çalıştırın: +Ve sonra yükleyip çalıştırabilirsiniz: ```js import netteForms from 'nette-forms'; netteForms.initOnLoad(); ``` -Alternatif olarak, doğrudan `vendor` klasöründen yükleyebilirsiniz: +Alternatif olarak onu doğrudan `vendor` klasöründen yükleyebilirsiniz: ```js import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; netteForms.initOnLoad(); ``` +Forma `novalidate` niteliğini ekleyerek istemci tarafı doğrulamayı tümüyle kapatabilirsiniz. `netteForms.js` betiği o zaman gönderimde formu doğrulamayı atlar, dolayısıyla doğrulama yalnızca sunucuda yapılır: + +```php +$form->setHtmlAttribute('novalidate'); +``` + Dinamik JavaScript ================== -Adres girme alanlarını yalnızca kullanıcı malların postayla gönderilmesini seçtiğinde mi görüntülemek istiyorsunuz? Sorun değil. Anahtar, `addCondition()` & `toggle()` metot çiftidir: +Adres alanlarını yalnızca kullanıcı malların postayla gönderilmesini seçtiğinde göstermek mi istiyorsunuz? Sorun değil. Anahtar, `addCondition()` ve `toggle()` metot çiftidir: ```php $form->addCheckbox('send_it') @@ -332,25 +346,25 @@ $form->addCheckbox('send_it') ->toggle('#address-container'); ``` -Bu kod, koşul karşılandığında, yani onay kutusu işaretlendiğinde, `#address-container` HTML öğesinin görünür olacağını söyler. Ve tersi. Alıcının adresini içeren form elemanlarını bu kimliğe sahip bir konteynere yerleştiririz ve onay kutusuna tıklandığında gizlenir veya gösterilirler. Bu, `netteForms.js` betiği tarafından sağlanır. +Bu kod, koşul sağlandığında (yani onay kutusu işaretlendiğinde) `#address-container` HTML elemanının görünür olacağını, tersi durumda ise görünmeyeceğini belirtir. Yani alıcının adresini içeren form öğelerini bu ID'ye sahip bir container'a koyarız ve onay kutusuna tıklandığında gizlenip görünürler. Bunu `netteForms.js` betiği üstlenir. -`toggle()` metodunun argümanı olarak herhangi bir seçici iletilebilir. Tarihsel nedenlerden dolayı, başka özel karakterler içermeyen alfanümerik bir dize, öğenin kimliği olarak anlaşılır, yani önünde `#` karakteri varmış gibi. İkinci isteğe bağlı parametre, davranışı tersine çevirmeyi sağlar, yani `toggle('#address-container', false)` kullanırsak, öğe yalnızca onay kutusu işaretli olmadığında görüntülenir. +`toggle()` metoduna argüman olarak herhangi bir seçici aktarılabilir. Tarihsel nedenlerle, harf, rakam ya da alt çizgiyle başlayan ve yalnızca harf, rakam, alt çizgi, tire, nokta ve iki nokta içeren bir dize, sanki başında `#` karakteri varmış gibi eleman ID'si sayılır. İsteğe bağlı ikinci parametre davranışı tersine çevirmeye olanak tanır; örneğin `toggle('#address-container', false)` kullansaydık, eleman yalnızca onay kutusu işaretli *değilse* görüntülenirdi. -JavaScript'teki varsayılan uygulama, öğelerin `hidden` özelliğini değiştirir. Ancak, davranışı kolayca değiştirebiliriz, örneğin bir animasyon ekleyebiliriz. JavaScript'te `Nette.toggle` metodunu kendi çözümümüzle geçersiz kılmamız yeterlidir: +Varsayılan JavaScript gerçekleştirimi, elemanların `hidden` özelliğini değiştirir. Ancak davranışı, örneğin bir animasyon ekleyerek kolayca değiştirebiliriz. Yalnızca JavaScript'te `Nette.toggle` metodunu kendi çözümünüzle geçersiz kılın: ```js Nette.toggle = (selector, visible, srcElement, event) => { document.querySelectorAll(selector).forEach((el) => { - // 'el' öğesini 'visible' değerine göre gizleyeceğiz veya göstereceğiz + // 'visible' değerine göre 'el' elemanını gizle ya da göster }); }; ``` -Doğrulamayı Devre Dışı Bırakma -============================== +Doğrulamayı Kapatma +=================== -Bazen doğrulamayı devre dışı bırakmak gerekebilir. Gönderme düğmesine basmak doğrulamayı gerçekleştirmemesi gerekiyorsa ( *İptal* veya *Önizleme* düğmeleri için uygundur), `$submit->setValidationScope([])` metoduyla devre dışı bırakırız. Yalnızca kısmi doğrulama yapması gerekiyorsa, hangi alanların veya form konteynerlerinin doğrulanacağını belirleyebiliriz. +Bazen doğrulamayı kapatmak yararlı olabilir. Bir gönder düğmesine basmak doğrulama yapmamalıysa (*İptal* ya da *Önizleme* düğmeleri için uygundur), onu `$submit->setValidationScope([])` metoduyla kapatırız. Yalnızca kısmi doğrulama yapmalıysa, hangi alanların ya da form container'larının doğrulanacağını belirtebiliriz. ```php $form->addText('name') @@ -362,15 +376,17 @@ $details->addInteger('age') $details->addInteger('age2') ->setRequired('age2'); -$form->addSubmit('send1'); // Tüm formu doğrular +$form->addSubmit('send1'); // Formun tamamını doğrular $form->addSubmit('send2') - ->setValidationScope([]); // Hiç doğrulama yapmaz + ->setValidationScope([]); // Hiçbir şeyi doğrulamaz $form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Yalnızca name öğesini doğrular + ->setValidationScope([$form['name']]); // Yalnızca 'name' öğesini doğrular $form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Yalnızca age öğesini doğrular + ->setValidationScope([$form['details']['age']]); // Yalnızca 'age' öğesini doğrular $form->addSubmit('send5') - ->setValidationScope([$form['details']]); // details konteynerini doğrular + ->setValidationScope([$form['details']]); // 'details' container'ını doğrular ``` -`setValidationScope`, her zaman çağrılacak olan formdaki [##onValidate-olayı] etkilemez. Konteynerdeki `onValidate` olayı yalnızca bu konteyner kısmi doğrulama için işaretlenmişse tetiklenir. +`setValidationScope`, formdaki [#onValidate Olayı] olayını etkilemez; o her zaman çağrılır. Bir container'daki `onValidate` olayı yalnızca o container kısmi doğrulama için işaretlenmişse tetiklenir. + +Kısmi doğrulama, `getValues()` metodunun döndürdüğü değerleri de etkiler: sonuç yalnızca doğrulama kapsamına giren öğelerin değerlerini içerir. Bu kapsamın dışındaki öğelerin değerleri atlanır. diff --git a/forms/uk/@home.texy b/forms/uk/@home.texy deleted file mode 100644 index 39bfd4af56..0000000000 --- a/forms/uk/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms здійснили революцію у створенні веб-форм. Раптом стало достатньо написати кілька зрозумілих рядків коду, і ви мали готову форму, включаючи відображення, валідацію на стороні JavaScript та сервера, а також відмінно захищену. Ми покажемо, як - -- створювати зручні форми -- валідувати надіслані дані -- відображати елементи точно за потребою - -</div> - - -Використовуючи Nette Forms, ви уникнете цілої низки рутинних завдань, таких як написання валідації (до того ж подвійної, на стороні сервера та клієнта), мінімізуєте ймовірність виникнення помилок та дірок у безпеці. - -Форми можна використовувати або як частину Nette Application (тобто в презентерах), або повністю самостійно. Оскільки в обох випадках використання трохи відрізняється, ми підготували для вас два посібники: - -<div class="wiki-buttons"> -<div> "Форми в презентерах .[wiki-button]":in-presenter </div> -<div> "Форми самостійно .[wiki-button]":standalone </div> -</div> - - -Встановлення ------------- - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/uk/@left-menu.texy b/forms/uk/@left-menu.texy deleted file mode 100644 index b1497f49e6..0000000000 --- a/forms/uk/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Вступ |@home] -- [Форми в презентерах|in-presenter] -- [Форми самостійно|standalone] -- [Елементи форм |controls] -- [Валідація |validation] -- [Відображення |rendering] -- [Конфігурація |configuration] - - -Додаткове читання -***************** -- [Посібники та практики |best-practices:] diff --git a/forms/uk/@meta.texy b/forms/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/forms/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/forms/uk/configuration.texy b/forms/uk/configuration.texy deleted file mode 100644 index c2e8ab9954..0000000000 --- a/forms/uk/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Конфігурація форм -***************** - -.[perex] -У конфігурації можна змінити стандартні [повідомлення про помилки форм |validation]. - -```neon -forms: - messages: - Equal: 'Please enter %s.' - NotEqual: 'This value should not be %s.' - Filled: 'This field is required.' - Blank: 'This field should be blank.' - MinLength: 'Please enter at least %d characters.' - MaxLength: 'Please enter no more than %d characters.' - Length: 'Please enter a value between %d and %d characters long.' - Email: 'Please enter a valid email address.' - URL: 'Please enter a valid URL.' - Integer: 'Please enter a valid integer.' - Float: 'Please enter a valid number.' - Min: 'Please enter a value greater than or equal to %d.' - Max: 'Please enter a value less than or equal to %d.' - Range: 'Please enter a value between %d and %d.' - MaxFileSize: 'The size of the uploaded file can be up to %d bytes.' - MaxPostSize: 'The uploaded data exceeds the limit of %d bytes.' - MimeType: 'The uploaded file is not in the expected format.' - Image: 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Please select a valid option.' - Nette\Forms\Controls\UploadControl::Valid: 'An error occurred during file upload.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Your session has expired. Please return to the home page and try again.' -``` - -Ось український переклад: - -```neon -forms: - messages: - Equal: 'Введіть %s.' - NotEqual: 'Це значення не повинно бути %s.' - Filled: 'Це поле є обов’язковим.' - Blank: 'Це поле повинно бути порожнім.' - MinLength: 'Будь ласка, введіть щонайменше %d символів.' - MaxLength: 'Будь ласка, введіть не більше %d символів.' - Length: 'Будь ласка, введіть значення довжиною від %d до %d символів.' - Email: 'Введіть дійсну адресу електронної пошти.' - URL: 'Будь ласка, введіть дійсну URL-адресу.' - Integer: 'Введіть дійсне ціле число.' - Float: 'Введіть дійсне число.' - Min: 'Будь ласка, введіть значення, більше або рівне %d.' - Max: 'Будь ласка, введіть значення, менше або рівне %d.' - Range: 'Введіть значення між %d та %d.' - MaxFileSize: 'Розмір завантаженого файлу може бути не більше %d байт.' - MaxPostSize: 'Завантажені дані перевищують ліміт %d байт.' - MimeType: 'Завантажений файл не у очікуваному форматі.' - Image: 'Завантажений файл має бути зображенням у форматі JPEG, GIF, PNG, WebP або AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Будь ласка, виберіть дійсний варіант.' - Nette\Forms\Controls\UploadControl::Valid: 'Під час завантаження файлу сталася помилка.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Ваша сесія закінчилася. Поверніться на головну сторінку та спробуйте ще раз.' -``` - -Якщо ви не використовуєте весь фреймворк, а отже, і конфігураційні файли, ви можете змінити стандартні повідомлення про помилки безпосередньо в масиві `Nette\Forms\Validator::$messages`. diff --git a/forms/uk/controls.texy b/forms/uk/controls.texy deleted file mode 100644 index afa847c3b1..0000000000 --- a/forms/uk/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Елементи форми -************** - -.[perex] -Огляд стандартних елементів форми. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Додає однорядкове текстове поле (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Якщо користувач не заповнює поле, повертається порожній рядок `''`, або за допомогою `setNullable()` можна вказати, щоб повертався `null`. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired() - ->setNullable(); -``` - -Автоматично перевіряє UTF-8, обрізає пробіли зліва та справа та видаляє переноси рядків, які може надіслати зловмисник. - -Максимальну довжину можна обмежити за допомогою `setMaxLength()`. Змінити введене користувачем значення дозволяє [addFilter() |validation#Зміна вводу]. - -За допомогою `setHtmlType()` можна змінити візуальний характер текстового поля на типи, такі як `search`, `tel` або `url`, див. [специфікацію |https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Пам'ятайте, що зміна типу є лише візуальною і не замінює функцію валідації. Для типу `url` доцільно додати специфічне правило валідації [URL |validation#Текстові поля]. - -.[note] -Для інших типів введення, таких як `number`, `range`, `email`, `date`, `datetime-local`, `time` та `color`, використовуйте спеціалізовані методи, такі як [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] та [#addColor], які забезпечують серверну валідацію. Типи `month` та `week` поки що не повністю підтримуються всіма браузерами. - -Елементу можна встановити так зване empty-value, щось на зразок значення за замовчуванням, але якщо користувач його не змінить, елемент поверне порожній рядок або `null`. - -```php -$form->addText('phone', 'Телефон:') - ->setHtmlType('tel') - ->setEmptyValue('+420'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Додає поле для введення багаторядкового тексту (клас [TextArea |api:Nette\Forms\Controls\TextArea]). Якщо користувач не заповнює поле, повертається порожній рядок `''`, або за допомогою `setNullable()` можна вказати, щоб повертався `null`. - -```php -$form->addTextArea('note', 'Примітка:') - ->addRule($form::MaxLength, 'Примітка занадто довга', 10000); -``` - -Автоматично перевіряє UTF-8 і нормалізує роздільники рядків до `\n`. На відміну від однорядкового поля введення, обрізання пробілів не відбувається. - -Максимальну довжину можна обмежити за допомогою `setMaxLength()`. Змінити введене користувачем значення дозволяє [addFilter() |validation#Зміна вводу]. Можна встановити так зване empty-value за допомогою `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Додає поле для введення цілого числа (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Повертає або ціле число (integer), або `null`, якщо користувач нічого не ввів. - -```php -$form->addInteger('year', 'Рік:') - ->addRule($form::Range, 'Рік має бути в діапазоні від %d до %d.', [1900, 2023]); -``` - -Елемент відображається як `<input type="number">`. За допомогою методу `setHtmlType()` можна змінити тип на `range` для відображення у вигляді повзунка, або на `text`, якщо ви віддаєте перевагу стандартному текстовому полю без спеціальної поведінки типу `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Додає поле для введення десяткового числа (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Повертає або float, або `null`, якщо користувач нічого не ввів. - -```php -$form->addFloat('level', 'Рівень:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Рівень має бути в діапазоні від %d до %d.', [0, 100]); -``` - -Елемент відображається як `<input type="number">`. За допомогою методу `setHtmlType()` можна змінити тип на `range` для відображення у вигляді повзунка, або на `text`, якщо ви віддаєте перевагу стандартному текстовому полю без спеціальної поведінки типу `number`. - -Nette та браузер Chrome приймають як роздільник десяткових знаків як кому, так і крапку. Щоб ця функціональність була доступна і у Firefox, рекомендується встановити атрибут `lang` або для даного елемента, або для всієї сторінки, наприклад `<html lang="uk">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Додає поле для введення адреси електронної пошти (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Якщо користувач не заповнює поле, повертається порожній рядок `''`, або за допомогою `setNullable()` можна вказати, щоб повертався `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Перевіряє, чи є значення дійсною адресою електронної пошти. Не перевіряється, чи дійсно існує домен, перевіряється лише синтаксис. Автоматично перевіряє UTF-8, обрізає пробіли зліва та справа. - -Максимальну довжину можна обмежити за допомогою `setMaxLength()`. Змінити введене користувачем значення дозволяє [addFilter() |validation#Зміна вводу]. Можна встановити так зване empty-value за допомогою `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Додає поле для введення пароля (клас [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Пароль:') - ->setRequired() - ->addRule($form::MinLength, 'Пароль повинен містити щонайменше %d символів', 8) - ->addRule($form::Pattern, 'Повинен містити цифру', '.*[0-9].*'); -``` - -При повторному відображенні форми поле буде порожнім. Автоматично перевіряє UTF-8, обрізає пробіли зліва та справа та видаляє переноси рядків, які може надіслати зловмисник. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Додає прапорець (клас [Checkbox |api:Nette\Forms\Controls\Checkbox]). Повертає значення `true` або `false`, залежно від того, чи встановлено прапорець. - -```php -$form->addCheckbox('agree', 'Згоден з умовами') - ->setRequired('Необхідно погодитися з умовами'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Додає прапорці для вибору кількох елементів (клас [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Повертає масив ключів вибраних елементів. Метод `getSelectedItems()` повертає значення замість ключів. - -```php -$form->addCheckboxList('colors', 'Кольори:', [ - 'r' => 'червоний', - 'g' => 'зелений', - 'b' => 'синій', -]); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. - -За допомогою `setDisabled(['r', 'g'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибрані елементи дійсно є одними з пропонованих і не були деактивовані. Методом `getRawValue()` можна отримати надіслані елементи без цієї важливої перевірки. - -При встановленні вибраних елементів за замовчуванням також перевіряє, що вони є одними з пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - -Якщо ви надсилаєте форму методом `GET`, ви можете вибрати компактніший спосіб передачі даних, який економить розмір рядка запиту. Він активується встановленням HTML-атрибута форми: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Додає перемикачі (клас [RadioList |api:Nette\Forms\Controls\RadioList]). Повертає ключ вибраного елемента або `null`, якщо користувач нічого не вибрав. Метод `getSelectedItem()` повертає значення замість ключа. - -```php -$sex = [ - 'm' => 'чоловік', - 'f' => 'жінка', -]; -$form->addRadioList('gender', 'Стать:', $sex); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. - -За допомогою `setDisabled(['m', 'f'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибраний елемент дійсно є одним із пропонованих і не був деактивований. Методом `getRawValue()` можна отримати надісланий елемент без цієї важливої перевірки. - -При встановленні вибраного елемента за замовчуванням також перевіряє, що він є одним із пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Додає select box (клас [SelectBox |api:Nette\Forms\Controls\SelectBox]). Повертає ключ вибраного елемента або `null`, якщо користувач нічого не вибрав. Метод `getSelectedItem()` повертає значення замість ключа. - -```php -$countries = [ - 'CZ' => 'Чеська Республіка', - 'SK' => 'Словаччина', - 'GB' => 'Велика Британія', -]; - -$form->addSelect('country', 'Країна:', $countries) - ->setDefaultValue('SK'); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. Елементи також можуть бути двовимірним масивом: - -```php -$countries = [ - 'Європа' => [ - 'CZ' => 'Чеська Республіка', - 'SK' => 'Словаччина', - 'GB' => 'Велика Британія', - ], - 'CA' => 'Канада', - 'US' => 'США', - '?' => 'інша', -]; -``` - -У select box-ах часто перший елемент має особливе значення, служить як заклик до дії. Для додавання такого елемента служить метод `setPrompt()`. - -```php -$form->addSelect('country', 'Країна:', $countries) - ->setPrompt('Виберіть країну'); -``` - -За допомогою `setDisabled(['CZ', 'SK'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибраний елемент дійсно є одним із пропонованих і не був деактивований. Методом `getRawValue()` можна отримати надісланий елемент без цієї важливої перевірки. - -При встановленні вибраного елемента за замовчуванням також перевіряє, що він є одним із пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Додає select box для вибору кількох елементів (клас [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Повертає масив ключів вибраних елементів. Метод `getSelectedItems()` повертає значення замість ключів. - -```php -$form->addMultiSelect('countries', 'Країна:', $countries); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. Елементи також можуть бути двовимірним масивом. - -За допомогою `setDisabled(['CZ', 'SK'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибрані елементи дійсно є одними з пропонованих і не були деактивовані. Методом `getRawValue()` можна отримати надіслані елементи без цієї важливої перевірки. - -При встановленні вибраних елементів за замовчуванням також перевіряє, що вони є одними з пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Додає поле для завантаження файлу (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Повертає об'єкт [FileUpload |http:request#FileUpload] навіть у випадку, якщо користувач не надіслав жодного файлу, що можна перевірити методом `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Аватар:') - ->addRule($form::Image, 'Аватар має бути у форматі JPEG, PNG, GIF, WebP або AVIF.') - ->addRule($form::MaxFileSize, 'Максимальний розмір 1 МБ.', 1024 * 1024); -``` - -Якщо файл не вдалося коректно завантажити, форма не надсилається успішно і відображається помилка. Тобто при успішному надсиланні не потрібно перевіряти метод `FileUpload::isOk()`. - -Ніколи не довіряйте оригінальній назві файлу, повернутій методом `FileUpload::getName()`, клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати ваш застосунок. - -Правила `MimeType` та `Image` визначають потрібний тип на основі сигнатури файлу і не перевіряють його цілісність. Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Додає поле для одночасного завантаження кількох файлів (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Повертає масив об'єктів [FileUpload |http:request#FileUpload]. Метод `FileUpload::hasFile()` для кожного з них повертатиме `true`. - -```php -$form->addMultiUpload('files', 'Файли:') - ->addRule($form::MaxLength, 'Максимально можна завантажити %d файлів', 10); -``` - -Якщо якийсь із файлів не вдалося коректно завантажити, форма не надсилається успішно і відображається помилка. Тобто при успішному надсиланні не потрібно перевіряти метод `FileUpload::isOk()`. - -Ніколи не довіряйте оригінальним назвам файлів, повернутим методом `FileUpload::getName()`, клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати ваш застосунок. - -Правила `MimeType` та `Image` визначають потрібний тип на основі сигнатури файлу і не перевіряють його цілісність. Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Додає поле, яке дозволяє користувачеві легко ввести дату, що складається з року, місяця та дня (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Як значення за замовчуванням приймає або об'єкти, що реалізують інтерфейс `DateTimeInterface`, рядок з часом, або число, що представляє UNIX timestamp. Те саме стосується аргументів правил `Min`, `Max` або `Range`, які визначають мінімальну та максимальну допустиму дату. - -```php -$form->addDate('date', 'Дата:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Дата повинна бути щонайменше місячної давності.', new DateTime('-1 month')); -``` - -Стандартно повертає об'єкт `DateTimeImmutable`, методом `setFormat()` ви можете вказати [текстовий формат |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] або timestamp: - -```php -$form->addDate('date', 'Дата:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Додає поле, яке дозволяє користувачеві легко ввести час, що складається з годин, хвилин та, за бажанням, секунд (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Як значення за замовчуванням приймає або об'єкти, що реалізують інтерфейс `DateTimeInterface`, рядок з часом, або число, що представляє UNIX timestamp. З цих вхідних даних використовується лише інформація про час, дата ігнорується. Те саме стосується аргументів правил `Min`, `Max` або `Range`, які визначають мінімальний та максимальний допустимий час. Якщо встановлене мінімальне значення більше за максимальне, створюється часовий діапазон, що переходить через північ. - -```php -$form->addTime('time', 'Час:', withSeconds: true) - ->addRule($form::Range, 'Час має бути в діапазоні від %d до %d.', ['12:30', '13:30']); -``` - -Стандартно повертає об'єкт `DateTimeImmutable` (з датою 1 січня 1 року), методом `setFormat()` ви можете вказати [текстовий формат |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Час:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Додає поле, яке дозволяє користувачеві легко ввести дату та час, що складаються з року, місяця, дня, годин, хвилин та, за бажанням, секунд (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Як значення за замовчуванням приймає або об'єкти, що реалізують інтерфейс `DateTimeInterface`, рядок з часом, або число, що представляє UNIX timestamp. Те саме стосується аргументів правил `Min`, `Max` або `Range`, які визначають мінімальну та максимальну допустиму дату. - -```php -$form->addDateTime('datetime', 'Дата і час:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Дата повинна бути щонайменше місячної давності.', new DateTime('-1 month')); -``` - -Стандартно повертає об'єкт `DateTimeImmutable`, методом `setFormat()` ви можете вказати [текстовий формат |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] або timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Додає поле для вибору кольору (клас [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Колір - це рядок у форматі `#rrggbb`. Якщо користувач не зробить вибір, повертається чорний колір `#000000`. - -```php -$form->addColor('color', 'Колір:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Додає приховане поле (клас [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -За допомогою `setNullable()` можна налаштувати, щоб повертався `null` замість порожнього рядка. Змінити надіслане значення дозволяє [addFilter() |validation#Зміна вводу]. - -Хоча елемент прихований, **важливо усвідомлювати**, що значення все ще може бути змінено або підроблено зловмисником. Завжди ретельно перевіряйте та валідуйте всі отримані значення на стороні сервера, щоб запобігти ризикам безпеки, пов'язаним з маніпуляцією даними. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Додає кнопку надсилання (клас [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Надіслати'); -``` - -У формі можна мати кілька кнопок надсилання: - -```php -$form->addSubmit('register', 'Зареєструватися'); -$form->addSubmit('cancel', 'Скасувати'); -``` - -Щоб з'ясувати, на яку з них було натиснуто, використовуйте: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Якщо ви не хочете валідувати всю форму при натисканні кнопки (наприклад, для кнопок *Скасувати* або *Попередній перегляд*), використовуйте [setValidationScope() |validation#Вимкнення валідації]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Додає кнопку (клас [Button |api:Nette\Forms\Controls\Button]), яка не має функції надсилання. Отже, її можна використовувати для іншої функції, наприклад, виклику функції JavaScript при натисканні. - -```php -$form->addButton('raise', 'Підвищити зарплату') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Додає кнопку надсилання у вигляді зображення (клас [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -При використанні кількох кнопок надсилання можна з'ясувати, на яку було натиснуто, за допомогою `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Додає підформу (клас [Container |api:Nette\Forms\Container]), або контейнер, до якого можна додавати інші елементи так само, як ми додаємо їх до форми. Також працюють методи `setDefaults()` або `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Ваше ім\'я:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Ваше ім\'я:'); -$sub2->addEmail('email', 'Email:'); -``` - -Надіслані дані потім повертаються як багатовимірна структура: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Огляд налаштувань -================= - -Для всіх елементів ми можемо викликати наступні методи (повний огляд в [документації API |https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | встановлює значення за замовчуванням -| `getValue()` | отримати поточне значення -| `setOmitted()` | [##пропуск значення] -| `setDisabled()` | [##деактивація елементів] - -Відображення: -.[table-form-methods language-php] -| `setCaption($caption)` | змінює підпис елемента -| `setTranslator($translator)` | встановлює [перекладач |rendering#Переклад] -| `setHtmlAttribute($name, $value)` | встановлює [HTML-атрибут |rendering#HTML атрибути] елемента -| `setHtmlId($id)` | встановлює HTML-атрибут `id` -| `setHtmlType($type)` | встановлює HTML-атрибут `type` -| `setHtmlName($name)` | встановлює HTML-атрибут `name` -| `setOption($key, $value)` | [налаштування для відображення |rendering#Options] - -Валідація: -.[table-form-methods language-php] -| `setRequired()` | [обов'язковий елемент |validation] -| `addRule()` | встановлення [правила валідації |validation#Правила] -| `addCondition()`, `addConditionOn()` | встановлює [умову валідації |validation#Умови] -| `addError($message)` | [передача повідомлення про помилку |validation#Помилки під час обробки] - -Для елементів `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` можна викликати наступні методи: - -.[table-form-methods language-php] -| `setNullable()` | встановлює, чи поверне getValue() `null` замість порожнього рядка -| `setEmptyValue($value)` | встановлює спеціальне значення, яке вважається порожнім рядком -| `setMaxLength($length)` | встановлює максимальну кількість дозволених символів -| `addFilter($filter)` | [зміна вводу |validation#Зміна вводу] - - -Пропуск значення -================ - -Якщо нас не цікавить значення, введене користувачем, ми можемо пропустити його за допомогою `setOmitted()` з результату методу `$form->getValues()` або з даних, що передаються в обробники. Це корисно для різних паролів для перевірки, антиспам-елементів тощо. - -```php -$form->addPassword('passwordVerify', 'Пароль для перевірки:') - ->setRequired('Будь ласка, введіть пароль ще раз для перевірки') - ->addRule($form::Equal, 'Паролі не співпадають', $form['password']) - ->setOmitted(); -``` - - -Деактивація елементів -===================== - -Елементи можна деактивувати за допомогою `setDisabled()`. Такий елемент користувач не може редагувати. - -```php -$form->addText('username', 'Ім\'я користувача:') - ->setDisabled(); -``` - -Вимкнені елементи браузер взагалі не надсилає на сервер, тому ви їх не знайдете в даних, повернутих функцією `$form->getValues()`. Однак, якщо ви встановите `setOmitted(false)`, Nette включить їхнє значення за замовчуванням у ці дані. - -При виклику `setDisabled()` з міркувань безпеки **значення елемента видаляється**. Якщо ви встановлюєте значення за замовчуванням, це необхідно зробити після його деактивації: - -```php -$form->addText('username', 'Ім\'я користувача:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Альтернативою вимкненим елементам є елементи з HTML-атрибутом `readonly`, які браузер надсилає на сервер. Хоча елемент призначений лише для читання, **важливо усвідомлювати**, що його значення все ще може бути змінено або підроблено зловмисником. - - -Власні елементи -=============== - -Поряд із широким спектром вбудованих елементів форми, ви можете додавати власні елементи до форми таким чином: - -```php -$form->addComponent(new DateInput('Дата:'), 'date'); -// альтернативний синтаксис: $form['date'] = new DateInput('Дата:'); -``` - -.[note] -Форма є нащадком класу [Container |component-model:#Container], а окремі елементи є нащадками [Component |component-model:#Component]. - -Існує спосіб визначення нових методів форми для додавання власних елементів (наприклад, `$form->addZip()`). Це так звані extension methods. Недоліком є те, що для них не працюватиме автодоповнення в редакторах. - -```php -use Nette\Forms\Container; - -// додамо метод addZip(string $name, ?string $label = null) -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Щонайменше 5 цифр', '[0-9]{5}'); -}); - -// використання -$form->addZip('zip', 'Поштовий індекс:'); -``` - - -Низькорівневі елементи -====================== - -Можна також використовувати елементи, які ми записуємо лише в шаблоні і не додаємо до форми жодним із методів `$form->addXyz()`. Наприклад, коли ми виводимо записи з бази даних і заздалегідь не знаємо, скільки їх буде і які у них будуть ID, і хочемо біля кожного рядка відобразити прапорець або перемикач, достатньо закодувати його в шаблоні: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -А після надсилання дізнаємося значення: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -де перший параметр - це тип елемента (`DataFile` для `type=file`, `DataLine` для однорядкових полів введення, таких як `text`, `password`, `email` тощо, і `DataText` для всіх інших), а другий параметр `sel[]` відповідає HTML-атрибуту name. Тип елемента можна комбінувати зі значенням `DataKeys`, яке зберігає ключі елементів. Це особливо корисно для `select`, `radioList` та `checkboxList`. - -Важливо те, що `getHttpData()` повертає санітизоване значення, у цьому випадку це завжди буде масив дійсних рядків UTF-8, незалежно від того, що зловмисник спробував би підсунути серверу. Це аналог прямої роботи з `$_POST` або `$_GET`, але з тією суттєвою різницею, що він завжди повертає чисті дані, так, як ви звикли зі стандартними елементами форм Nette. diff --git a/forms/uk/in-presenter.texy b/forms/uk/in-presenter.texy deleted file mode 100644 index 4a23b0bfdb..0000000000 --- a/forms/uk/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Форми в презентерах -******************* - -.[perex] -Nette Forms значно полегшують створення та обробку веб-форм. У цьому розділі ви дізнаєтеся, як використовувати форми всередині презентерів. - -Якщо вас цікавить, як використовувати їх повністю окремо без решти фреймворку, для вас призначений посібник для [самостійного використання |standalone]. - - -Перша форма -=========== - -Спробуємо написати просту форму реєстрації. Її код буде таким: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Ім\'я:'); -$form->addPassword('password', 'Пароль:'); -$form->addSubmit('send', 'Зареєструватися'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -і в браузері вона відобразиться так: - -[* form-cs.webp *] - -Форма в presenter'і є об'єктом класу `Nette\Application\UI\Form`, її попередник `Nette\Forms\Form` призначений для самостійного використання. Ми додали до неї так звані елементи ім'я, пароль та кнопку відправки. І, нарешті, рядок з `$form->onSuccess` говорить, що після відправки та успішної валідації має бути викликаний метод `$this->formSucceeded()`. - -З точки зору presenter'а форма є звичайним компонентом. Тому з нею поводяться як з компонентом і включають її до presenter'а за допомогою [фабричного методу |application:components#Фабричні методи]. Це виглядатиме так: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Ім\'я:'); - $form->addPassword('password', 'Пароль:'); - $form->addSubmit('send', 'Зареєструватися'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // тут ми обробляємо дані, надіслані формою - // $data->name містить ім'я - // $data->password містить пароль - $this->flashMessage('Ви були успішно зареєстровані.'); - $this->redirect('Home:'); - } -} -``` - -А в шаблоні форму відображаємо за допомогою тегу `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Реєстрація</h1> - -{control registrationForm} -``` - -І це, власне, все :-) Ми маємо функціональну та ідеально [захищену |#Захист від вразливостей] форму. - -А тепер ви, мабуть, думаєте, що це було занадто швидко, і розмірковуєте, як можливо, що викликається метод `formSucceeded()` і які параметри він отримує. Звичайно, ви маєте рацію, це заслуговує на пояснення. - -Nette пропонує свіжий механізм, який ми називаємо [Hollywood style |application:components#Голлівудський стиль]. Замість того, щоб ви як розробник постійно запитували, чи щось сталося («чи була форма відправлена?», «чи була вона відправлена валідно?» і «чи не була вона підроблена?»), ви говорите фреймворку «коли форма буде валідно заповнена, виклич цей метод» і залишаєте подальшу роботу йому. Якщо ви програмуєте на JavaScript, цей стиль програмування вам добре знайомий. Ви пишете функції, які викликаються, коли настає певна [подія |nette:glossary#Події události]. І мова передає їм відповідні аргументи. - -Саме так побудований і вищезгаданий код presenter'а. Масив `$form->onSuccess` представляє список PHP callback'ів, які Nette викличе в момент, коли форма буде відправлена і правильно заповнена (тобто є валідною). У рамках [життєвого циклу presenter'а |application:presenters#Життєвий цикл презентера] це так званий сигнал, тому вони викликаються після методу `action*` і перед методом `render*`. І кожному callback'у передає як перший параметр саму форму, а як другий — надіслані дані у вигляді об'єкта [ArrayHash |utils:arrays#ArrayHash]. Перший параметр можна пропустити, якщо об'єкт форми вам не потрібен. А другий параметр може бути хитрішим, але про це [пізніше |#Мапування на класи]. - -Об'єкт `$data` містить ключі `name` та `password` з даними, які заповнив користувач. Зазвичай дані відразу відправляються на подальшу обробку, що може бути, наприклад, вставкою в базу даних. Однак під час обробки може виникнути помилка, наприклад, ім'я користувача вже зайняте. У такому випадку ми передаємо помилку назад у форму за допомогою `addError()` і дозволяємо їй відобразитися знову, вже з повідомленням про помилку. - -```php -$form->addError('Вибачте, це ім\'я користувача вже використовується.'); -``` - -Крім `onSuccess`, існує ще `onSubmit`: callback'и викликаються завжди після відправлення форми, навіть якщо вона заповнена неправильно. А також `onError`: callback'и викликаються тільки якщо відправлення не є валідним. Вони викликаються навіть тоді, якщо в `onSuccess` або `onSubmit` ми зробимо форму невалідною за допомогою `addError()`. - -Після обробки форми ми перенаправляємо на наступну сторінку. Це запобігає небажаному повторному надсиланню форми кнопкою *оновити*, *назад* або рухом в історії браузера. - -Спробуйте додати й інші [елементи форми |controls]. - - -Доступ до елементів -=================== - -Форма є компонентом presenter'а, у нашому випадку названим `registrationForm` (за назвою фабричного методу `createComponentRegistrationForm`), тому будь-де в presenter'і ви можете отримати доступ до форми за допомогою: - -```php -$form = $this->getComponent('registrationForm'); -// альтернативний синтаксис: $form = $this['registrationForm']; -``` - -Окремі елементи форми також є компонентами, тому ви можете отримати доступ до них таким же чином: - -```php -$input = $form->getComponent('name'); // або $input = $form['name']; -$button = $form->getComponent('send'); // або $button = $form['send']; -``` - -Елементи видаляються за допомогою unset: - -```php -unset($form['name']); -``` - - -Правила валідації -================= - -Тут прозвучало слово *валідний,* але форма поки що не має жодних правил валідації. Давайте це виправимо. - -Ім'я буде обов'язковим, тому позначимо його методом `setRequired()`, аргументом якого є текст повідомлення про помилку, яке відобразиться, якщо користувач не заповнить ім'я. Якщо аргумент не вказано, буде використано стандартне повідомлення про помилку. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Будь ласка, введіть ім\'я'); -``` - -Спробуйте надіслати форму без заповненого імені, і ви побачите, що з'явиться повідомлення про помилку, і браузер або сервер відхилятимуть її доти, доки ви не заповните поле. - -Водночас систему не обдуриш, написавши в полі, наприклад, лише пробіли. Ні. Nette автоматично видаляє пробіли зліва та справа. Спробуйте самі. Це те, що ви завжди повинні робити з кожним однорядковим полем введення, але про це часто забувають. Nette робить це автоматично. (Можете спробувати обдурити форму і надіслати як ім'я багаторядковий рядок. Навіть тут Nette не дасть себе обдурити і замінить переноси рядків на пробіли.) - -Форма завжди валідується на стороні сервера, але також генерується JavaScript-валідація, яка відбувається миттєво, і користувач дізнається про помилку відразу, без необхідності надсилати форму на сервер. За це відповідає скрипт `netteForms.js`. Вставте його в шаблон макета: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Якщо ви подивитеся на вихідний код сторінки з формою, то помітите, що Nette вставляє обов'язкові елементи в елементи з CSS-класом `required`. Спробуйте додати до шаблону наступний стиль, і напис "Ім'я" стане червоним. Таким чином, ми елегантно позначаємо обов'язкові елементи для користувачів: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Інші правила валідації додамо методом `addRule()`. Перший параметр — це правило, другий — знову текст повідомлення про помилку, а потім може йти аргумент правила валідації. Що це означає? - -Розширимо форму новим необов'язковим полем "вік", яке має бути цілим числом (`addInteger()`) і, крім того, в дозволеному діапазоні (`$form::Range`). І тут ми використаємо третій параметр методу `addRule()`, яким передамо валідатору необхідний діапазон у вигляді пари `[від, до]`: - -```php -$form->addInteger('age', 'Вік:') - ->addRule($form::Range, 'Вік має бути від 18 до 120', [18, 120]); -``` - -.[tip] -Якщо користувач не заповнить поле, правила валідації не перевірятимуться, оскільки елемент є необов'язковим. - -Тут виникає простір для невеликого рефакторингу. У повідомленні про помилку та в третьому параметрі числа вказані дубльовано, що не ідеально. Якби ми створювали [багатомовні форми |rendering#Переклад], і повідомлення, що містить числа, було б перекладено кількома мовами, це ускладнило б можливу зміну значень. З цієї причини можна використовувати плейсхолдери `%d`, і Nette доповнить значення: - -```php - ->addRule($form::Range, 'Вік має бути від %d до %d років', [18, 120]); -``` - -Повернемося до елемента `password`, який ми також зробимо обов'язковим і ще перевіримо мінімальну довжину пароля (`$form::MinLength`), знову ж таки, використовуючи плейсхолдер: - -```php -$form->addPassword('password', 'Пароль:') - ->setRequired('Виберіть пароль') - ->addRule($form::MinLength, 'Пароль повинен містити щонайменше %d символів', 8); -``` - -Додамо до форми ще поле `passwordVerify`, де користувач введе пароль ще раз для перевірки. За допомогою правил валідації перевіримо, чи обидва паролі однакові (`$form::Equal`). А як параметр дамо посилання на перший пароль за допомогою [квадратних дужок |#Доступ до елементів]: - -```php -$form->addPassword('passwordVerify', 'Пароль для перевірки:') - ->setRequired('Будь ласка, введіть пароль ще раз для перевірки') - ->addRule($form::Equal, 'Паролі не співпадають', $form['password']) - ->setOmitted(); -``` - -За допомогою `setOmitted()` ми позначили елемент, значення якого насправді не має значення і який існує лише для валідації. Значення не передається до `$data`. - -Таким чином, ми маємо готову, повністю функціональну форму з валідацією в PHP та JavaScript. Можливості валідації Nette набагато ширші, можна створювати умови, за якими відображати та приховувати частини сторінки тощо. Все це ви дізнаєтеся в розділі про [валідацію форм |validation]. - - -Значення за замовчуванням -========================= - -Елементам форми зазвичай встановлюють значення за замовчуванням: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Часто буває зручно встановити значення за замовчуванням для всіх елементів одночасно. Наприклад, коли форма використовується для редагування записів. Ми читаємо запис з бази даних і встановлюємо значення за замовчуванням: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Викликайте `setDefaults()` після визначення елементів. - - -Відображення форми -================== - -Стандартно форма відображається як таблиця. Окремі елементи відповідають основному правилу доступності - всі написи записані як `<label>` і пов'язані з відповідним елементом форми. При натисканні на напис курсор автоматично з'являється в полі форми. - -Кожному елементу ми можемо встановлювати будь-які HTML-атрибути. Наприклад, додати placeholder: - -```php -$form->addInteger('age', 'Вік:') - ->setHtmlAttribute('placeholder', 'Будь ласка, заповніть вік'); -``` - -Способів відображення форми дійсно багато, тому цьому присвячено [окремий розділ про відображення |rendering]. - - -Мапування на класи -================== - -Повернемося до методу `formSucceeded()`, який у другому параметрі `$data` отримує надіслані дані як об'єкт `ArrayHash`. Оскільки це загальний клас, щось на зразок `stdClass`, нам при роботі з ним бракуватиме певного комфорту, такого як підказка властивостей в редакторах або статичний аналіз коду. Це можна було б вирішити, маючи для кожної форми конкретний клас, властивості якого представляють окремі елементи. Наприклад: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Альтернативно, ви можете використовувати конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public ?int $age, - public string $password, - ) { - } -} -``` - -Властивості класу даних також можуть бути enum'ами, і вони будуть автоматично зіставлені. .{data-version:3.2.4} - -Як сказати Nette, щоб він повертав нам дані як об'єкти цього класу? Легше, ніж ви думаєте. Достатньо лише вказати клас як тип параметра `$data` в обробному методі: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data є екземпляром RegistrationFormData - $name = $data->name; - // ... -} -``` - -Як тип можна також вказати `array`, і тоді дані передадуться як масив. - -Аналогічним чином можна використовувати і функцію `getValues()`, якій назву класу або об'єкт для гідратації передамо як параметр: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Якщо форми утворюють багаторівневу структуру, що складається з контейнерів, створіть для кожного окремий клас: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапування потім з типу властивості `$person` дізнається, що контейнер потрібно мапувати на клас `PersonFormData`. Якщо властивість містила б масив контейнерів, вкажіть тип `array` і клас для мапування передайте безпосередньо контейнеру: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Проект класу даних форми можна згенерувати за допомогою методу `Nette\Forms\Blueprint::dataClass($form)`, який виведе його на сторінку браузера. Потім код достатньо клацнути, щоб виділити, і скопіювати до проекту. .{data-version:3.1.15} - - -Кілька кнопок -============= - -Якщо форма має більше однієї кнопки, зазвичай потрібно розрізнити, яка з них була натиснута. Ми можемо створити для кожної кнопки власну функцію-обробник. Встановимо її як обробник для [події |nette:glossary#Події události] `onClick`: - -```php -$form->addSubmit('save', 'Зберегти') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Видалити') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Ці обробники викликаються лише у випадку валідно заповненої форми, так само як і у випадку події `onSuccess`. Різниця полягає в тому, що як перший параметр замість форми може передаватися кнопка відправки, залежно від типу, який ви вкажете: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Коли форма надсилається кнопкою <kbd>Enter</kbd>, це вважається так, ніби вона була надіслана першою кнопкою. - - -Подія onAnchor -============== - -Коли у фабричному методі (наприклад, `createComponentRegistrationForm`) ми створюємо форму, вона ще не знає, чи була вона надіслана, і з якими даними. Але є випадки, коли нам потрібно знати надіслані значення, наприклад, від них залежить подальший вигляд форми, або вони потрібні для залежних селектбоксів тощо. - -Тому частину коду, що створює форму, можна викликати лише в момент, коли вона так звано "заякорена", тобто вже пов'язана з presenter'ом і знає свої надіслані дані. Такий код передаємо до масиву `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Країна:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Місто:'); - -$form->onAnchor[] = function () use ($country, $city) { - // ця функція викликається, коли форма вже знає, чи була вона надіслана і з якими даними - // тому можна використовувати метод getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Захист від вразливостей -======================= - -Nette Framework надає великого значення безпеці, тому ретельно дбає про надійний захист форм. Це робиться повністю прозоро і не вимагає ручного налаштування. - -Крім того, що форми захищають від атаки [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] та [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], вони виконують багато дрібних заходів безпеки, про які вам вже не потрібно думати. - -Наприклад, вони фільтрують з вхідних даних усі керуючі символи та перевіряють валідність кодування UTF-8, тому дані з форми завжди будуть чистими. У селектбоксах та радіо-списках перевіряється, що вибрані елементи були дійсно з запропонованих і не відбулося підробки. Ми вже згадували, що в однорядкових текстових полях видаляються символи кінця рядка, які міг надіслати зловмисник. У багаторядкових полях, навпаки, нормалізуються символи кінця рядка. І так далі. - -Nette вирішує за вас ризики безпеки, про існування яких багато програмістів навіть не підозрюють. - -Згадана CSRF-атака полягає в тому, що зловмисник заманює жертву на сторінку, яка непомітно в браузері жертви виконує запит на сервер, на якому жертва авторизована, і сервер вважає, що запит виконала жертва за власною волею. Тому Nette запобігає надсиланню POST-форми з іншого домену. Якщо з якоїсь причини ви хочете вимкнути захист і дозволити надсилати форму з іншого домену, використовуйте: - -```php -$form->allowCrossOrigin(); // УВАГА! Вимикає захист! -``` - -Цей захист використовує SameSite cookie з назвою `_nss`. Захист за допомогою SameSite cookie може бути не 100% надійним, тому бажано увімкнути ще захист за допомогою токена: - -```php -$form->addProtection(); -``` - -Рекомендуємо таким чином захищати форми в адміністративній частині сайту, які змінюють чутливі дані в додатку. Фреймворк захищається від CSRF-атаки шляхом генерації та перевірки авторизаційного токена, який зберігається в сесії. Тому необхідно перед відображенням форми мати відкриту сесію. В адміністративній частині сайту зазвичай сесія вже запущена через авторизацію користувача. В іншому випадку запустіть сесію методом `Nette\Http\Session::start()`. - - -Однакова форма в кількох презентерах -==================================== - -Якщо вам потрібно використовувати одну й ту ж форму в кількох презентерах, рекомендуємо створити для неї фабрику, яку потім передасте до презентера. Підходящим місцем для такого класу є, наприклад, директорія `app/Forms`. - -Клас фабрики може виглядати приблизно так: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ім\'я:'); - $form->addSubmit('send', 'Увійти'); - return $form; - } -} -``` - -Ми просимо клас створити форму у фабричному методі для компонентів у презентері: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // ми можемо змінити форму, тут, наприклад, змінюємо напис на кнопці - $form['send']->setCaption('Продовжити'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // і додаємо обробник - return $form; -} -``` - -Обробник для обробки форми також може бути наданий вже з фабрики: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ім\'я:'); - $form->addSubmit('send', 'Увійти'); - $form->onSuccess[] = function (Form $form, $data): void { - // тут ми виконуємо обробку форми - }; - return $form; - } -} -``` - -Отже, ми пройшли швидкий вступ до форм у Nette. Спробуйте ще заглянути в директорію [examples |https://github.com/nette/forms/tree/master/examples] в дистрибутиві, де знайдете більше натхнення. diff --git a/forms/uk/rendering.texy b/forms/uk/rendering.texy deleted file mode 100644 index d6c1287888..0000000000 --- a/forms/uk/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Відображення форм -***************** - -Зовнішній вигляд форм може бути дуже різноманітним. На практиці ми можемо зіткнутися з двома крайнощами. З одного боку, існує потреба відображати в додатку низку форм, які візуально схожі одна на одну, як дві краплі води, і ми оцінимо легкість відображення без шаблону за допомогою `$form->render()`. Зазвичай це стосується адміністративних інтерфейсів. - -З іншого боку, існують різноманітні форми, де кожна форма є оригінальною. Їхній вигляд найкраще описувати мовою HTML у шаблоні форми. І, звісно, крім обох згаданих крайнощів, ми зустрінемо безліч форм, які знаходяться десь посередині. - - -Відображення за допомогою Latte -=============================== - -[Система шаблонів Latte|latte:] суттєво полегшує відображення форм та їхніх елементів. Спочатку ми покажемо, як відображати форми вручну по окремих елементах, щоб отримати повний контроль над кодом. Пізніше ми покажемо, як таке відображення можна [автоматизувати |#Автоматичне відображення]. - -Ви можете згенерувати дизайн шаблону форми Latte за допомогою методу `Nette\Forms\Blueprint::latte($form)`, який виведе його на сторінку браузера. Потім достатньо клацнути, щоб виділити код, і скопіювати його до вашого проєкту. .{data-version:3.1.15} - - -`{control}` ------------ - -Найпростіший спосіб відобразити форму — написати в шаблоні: - -```latte -{control signInForm} -``` - -Вплинути на вигляд так відображеної форми можна за допомогою конфігурації [#Renderer] та [окремих елементів |#HTML атрибути]. - - -`n:name` --------- - -Визначення форми в PHP-коді можна надзвичайно легко пов'язати з HTML-кодом. Достатньо лише додати атрибути `n:name`. Це так просто! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Вигляд кінцевого HTML-коду повністю у ваших руках. Якщо ви використовуєте атрибут `n:name` для елементів `<select>`, `<button>` або `<textarea>`, їхній внутрішній вміст автоматично заповнюється. Тег `<form n:name>` також створює локальну змінну `$form` з об'єктом відображуваної форми, а закриваючий тег `</form>` відображає всі невідображені приховані елементи (те саме стосується `{form} ... {/form}`). - -Однак не можна забувати про відображення можливих повідомлень про помилки. Як тих, що були додані до окремих елементів за допомогою методу `addError()` (за допомогою `{inputError}`), так і тих, що були додані безпосередньо до форми (повертаються методом `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Складніші елементи форми, такі як RadioList або CheckboxList, можна таким чином відображати по окремих пунктах: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Не хочете думати для кожного елемента, який HTML-елемент використовувати в шаблоні, чи то `<input>`, `<textarea>` тощо? Рішенням є універсальний тег `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Якщо форма використовує перекладач, текст усередині тегів `{label}` буде перекладено. - -Навіть у цьому випадку складніші елементи форми, такі як RadioList або CheckboxList, можна відображати по окремих пунктах: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Для відображення самого `<input>` в елементі Checkbox використовуйте `{input myCheckbox:}`. HTML-атрибути в цьому випадку завжди розділяйте комою `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Виводить повідомлення про помилку для елемента форми, якщо воно є. Повідомлення зазвичай загортають у HTML-елемент для стилізації. Запобігти відображенню порожнього елемента, якщо повідомлення немає, можна елегантно за допомогою `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Наявність помилки можна перевірити методом `hasErrors()` і відповідно встановити клас для батьківського елемента: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Теги `{form signInForm}...{/form}` є альтернативою до `<form n:name="signInForm">...</form>`. - - -Автоматичне відображення ------------------------- - -Завдяки тегам `{input}` і `{label}` ми можемо легко створити загальний шаблон для будь-якої форми. Він буде послідовно ітерувати та відображати всі її елементи, крім прихованих елементів, які відображаються автоматично при закритті форми тегом `</form>`. Назва відображуваної форми очікується у змінній `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Використані самозакривні парні теги `{label .../}` відображають мітки, що походять з визначення форми в PHP-коді. - -Цей загальний шаблон збережіть, наприклад, у файлі `basic-form.latte`, і для відображення форми достатньо його включити та передати назву (або екземпляр) форми в параметр `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Якщо б ви хотіли під час відображення однієї конкретної форми втрутитися в її вигляд і, наприклад, один елемент відобразити інакше, то найпростішим шляхом є підготувати в шаблоні блоки, які можна буде потім перезаписати. Блоки можуть мати також [динамічні імена |latte:template-inheritance#Динамічні назви блоків], тому в них можна вставити й ім'я відображуваного елемента. Наприклад: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Для елемента, наприклад, `username` таким чином виникне блок `input-username`, який можна легко перезаписати за допомогою тегу [{embed} |latte:template-inheritance#Успадкування одиниць embed]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Альтернативно, весь вміст шаблону `basic-form.latte` можна [визначити |latte:template-inheritance#Визначення] як блок, включно з параметром `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Завдяки цьому його виклик буде трохи простішим: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -При цьому блок достатньо імпортувати лише в одному місці, а саме на початку шаблону layout: - -```latte -{import basic-form.latte} -``` - - -Спеціальні випадки ------------------- - -Якщо потрібно відобразити лише внутрішню частину форми без HTML-тегів `<form>`, наприклад, при надсиланні сніпетів, приховайте їх за допомогою атрибута `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -З відображенням елементів усередині контейнера форми допоможе тег `{formContainer}`. - -```latte -<p>Які новини ви бажаєте отримувати:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Відображення без Latte -====================== - -Найпростіший спосіб відобразити форму — викликати: - -```php -$form->render(); -``` - -Вплинути на вигляд так відображеної форми можна за допомогою конфігурації [#Renderer] та [окремих елементів |#HTML атрибути]. - - -Ручне відображення ------------------- - -Кожен елемент форми має методи, які генерують HTML-код поля форми та мітки. Вони можуть повертати його або як рядок, або як об'єкт [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` повертає HTML-код елемента -- `getLabel($caption = null): Html|string|null` повертає HTML-код мітки, якщо вона існує - -Таким чином, форму можна відображати по окремих елементах: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -У той час як для деяких елементів `getControl()` повертає єдиний HTML-елемент (наприклад, `<input>`, `<select>` тощо), для інших — цілий шматок HTML-коду (CheckboxList, RadioList). У такому випадку ви можете використовувати методи, які генерують окремі інпути та мітки для кожного пункту окремо: - -- `getControlPart($key = null): ?Html` повертає HTML-код одного пункту -- `getLabelPart($key = null): ?Html` повертає HTML-код мітки одного пункту - -.[note] -Ці методи з історичних причин мають префікс `get`, але краще було б `generate`, оскільки при кожному виклику вони створюють і повертають новий елемент `Html`. - - -Renderer -======== - -Це об'єкт, що забезпечує відображення форми. Його можна встановити за допомогою методу `$form->setRenderer`. Йому передається управління при виклику методу `$form->render()`. - -Якщо ми не встановимо власний рендерер, буде використано стандартний рендерер [api:Nette\Forms\Rendering\DefaultFormRenderer]. Він відображає елементи форми у вигляді HTML-таблиці. Вивід виглядає так: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Ім'я:</label></th> - - <td><input type="text" class="text" name="name" id="frm-name" required value=""></td> -</tr> - -<tr class="required"> - <th><label class="required" for="frm-age">Вік:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Стать:</label></th> - ... -``` - -Використовувати чи не використовувати таблицю для каркасу форми — питання спірне, і багато вебдизайнерів віддають перевагу іншій розмітці. Наприклад, списку визначень. Тому ми переконфігуруємо `DefaultFormRenderer` так, щоб він відображав форму у вигляді списку. Конфігурація здійснюється редагуванням масиву [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Перший індекс завжди представляє область, а другий — її атрибут. Окремі області зображені на малюнку: - -[* defaultformrenderer.webp *] - -Стандартно група елементів `controls` обгортається таблицею `<table>`, кожен `pair` представляє рядок таблиці `<tr>`, а пара `label` і `control` є комірками `<th>` і `<td>`. Тепер ми змінимо обгортаючі елементи. Область `controls` вставимо в контейнер `<dl>`, область `pair` залишимо без контейнера, `label` вставимо в `<dt>` і, нарешті, `control` обгорнемо тегами `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Результатом є такий HTML-код: - -```latte -<dl> - <dt><label class="required" for="frm-name">Ім'я:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Вік:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Стать:</label></dt> - ... -</dl> -``` - -У масиві wrappers можна вплинути на цілу низку інших атрибутів: - -- додавати CSS-класи окремим типам елементів форми -- розрізняти CSS-класом парні та непарні рядки -- візуально розрізняти обов'язкові та необов'язкові елементи -- визначати, чи відображатимуться повідомлення про помилки безпосередньо біля елементів чи над формою - - -Options -------- - -Поведінку Renderer можна контролювати також встановленням *options* на окремих елементах форми. Таким чином можна встановити опис, який буде виведений поруч із полем введення: - -```php -$form->addText('phone', 'Номер:') - ->setOption('description', 'Цей номер залишиться прихованим'); -``` - -Якщо ми хочемо розмістити в ньому HTML-вміст, використаємо клас [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Номер:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Умови зберігання Вашого номера</a>') - ); -``` - -.[tip] -Елемент Html можна використовувати також замість мітки: `$form->addCheckbox('conditions', $label)`. - - -Групування елементів --------------------- - -Renderer дозволяє групувати елементи у візуальні групи (fieldset): - -```php -$form->addGroup('Особисті дані'); -``` - -Після створення нової групи вона стає активною, і кожен новододаний елемент одночасно додається і до неї. Тож форму можна будувати таким чином: - -```php -$form = new Form; -$form->addGroup('Особисті дані'); -$form->addText('name', 'Ваше ім\'я:'); -$form->addInteger('age', 'Ваш вік:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Адреса доставки'); -$form->addCheckbox('send', 'Надіслати на адресу'); -$form->addText('street', 'Вулиця:'); -$form->addText('city', 'Місто:'); -$form->addSelect('country', 'Країна:', $countries); -``` - -Renderer спочатку відображає групи, а потім елементи, які не належать до жодної групи. - - -Підтримка Bootstrap -------------------- - -[У прикладах |https://github.com/nette/forms/tree/master/examples] ви знайдете приклади, як налаштувати Renderer для [Twitter Bootstrap 2 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap2-rendering.php#L58], [Bootstrap 3 |https://github.com/nette/forms/blob/a0bc775b96b30780270bdec06396ca985168f11a/examples/bootstrap3-rendering.php#L58] та [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -HTML атрибути -============= - -Для встановлення будь-яких HTML-атрибутів елементів форми використовуємо метод `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Число:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Сортувати за:', ['ціною', 'назвою']) - ->setHtmlAttribute('onchange', 'submit()'); // при зміні надіслати - - -// Для встановлення атрибутів самого <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Специфікація типу елемента: - -```php -$form->addText('tel', 'Ваш телефон:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'напишіть телефон'); -``` - -.[warning] -Встановлення типу та інших атрибутів служить лише для візуальних цілей. Перевірка правильності введення має відбуватися на сервері, що забезпечується вибором відповідного [елемента форми|controls] та зазначенням [правил валідації|validation]. - -Окремим пунктам у списках radio або checkbox ми можемо встановити HTML-атрибут з різними значеннями для кожного з них. Зверніть увагу на двокрапку після `style:`, яка забезпечує вибір значення за ключем: - -```php -$colors = ['r' => 'червоний', 'g' => 'зелений', 'b' => 'синій']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Кольори:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Виведе: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">червоний</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">зелений</label> -<label><input type="checkbox" name="colors[]" value="b">синій</label> -``` - -Для встановлення логічних атрибутів, таких як `readonly`, ми можемо використовувати запис зі знаком питання: - -```php -$form->addCheckboxList('colors', 'Кольори:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // для кількох ключів використовуйте масив, напр. ['r', 'g'] -``` - -Виведе: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">червоний</label> -<label><input type="checkbox" name="colors[]" value="g">зелений</label> -<label><input type="checkbox" name="colors[]" value="b">синій</label> -``` - -У випадку selectbox метод `setHtmlAttribute()` встановлює атрибути елемента `<select>`. Якщо ми хочемо встановити атрибути окремим `<option>`, використовуємо метод `setOptionAttribute()`. Також працюють записи з двокрапкою та знаком питання, зазначені вище: - -```php -$form->addSelect('colors', 'Кольори:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Виведе: - -```latte -<select name="colors"> - <option value="r" style="background:red">червоний</option> - <option value="g" style="background:green">зелений</option> - <option value="b">синій</option> -</select> -``` - - -Прототипи ---------- - -Альтернативний спосіб встановлення HTML-атрибутів полягає в модифікації шаблону, з якого генерується HTML-елемент. Шаблоном є об'єкт `Html`, і його повертає метод `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Число:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Таким чином можна модифікувати й шаблон мітки, який повертає `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Для елементів Checkbox, CheckboxList та RadioList ви можете вплинути на шаблон елемента, який обгортає весь елемент. Його повертає `getContainerPrototype()`. За замовчуванням це «порожній» елемент, тому нічого не відображається, але якщо ми встановимо йому назву, він буде відображатися: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -У випадку CheckboxList та RadioList можна також вплинути на шаблон роздільника окремих пунктів, який повертає метод `getSeparatorPrototype()`. За замовчуванням це елемент `<br>`. Якщо ви зміните його на парний елемент, він буде обгортати окремі пункти замість того, щоб розділяти їх. А також можна вплинути на шаблон HTML-елемента мітки біля окремих пунктів, який повертає `getItemLabelPrototype()`. - - -Переклад -======== - -Якщо ви програмуєте багатомовний додаток, вам, ймовірно, знадобиться відображати форму різними мовними версіями. Nette Framework для цієї мети визначає інтерфейс для перекладу [api:Nette\Localization\Translator]. У Nette немає стандартної реалізації, ви можете вибрати відповідно до своїх потреб з кількох готових рішень, які знайдете на [Componette |https://componette.org/search/localization]. У їхній документації ви дізнаєтеся, як конфігурувати перекладач. - -Форми підтримують виведення текстів через перекладач. Ми передаємо його їм за допомогою методу `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -З цього моменту не тільки всі мітки, але й усі повідомлення про помилки або пункти select box перекладаються іншою мовою. - -Для окремих елементів форми при цьому можна встановити інший перекладач або повністю вимкнути переклад значенням `null`: - -```php -$form->addSelect('carModel', 'Модель:', $cars) - ->setTranslator(null); -``` - -Для [правил валідації|validation] перекладачу передаються також специфічні параметри, наприклад, для правила: - -```php -$form->addPassword('password', 'Пароль:') - ->addRule($form::MinLength, 'Пароль повинен мати щонайменше %d символів', 8); -``` - -викликається перекладач з такими параметрами: - -```php -$translator->translate('Пароль повинен мати щонайменше %d символів', 8); -``` - -і таким чином може вибрати правильну форму множини для слова `символів` залежно від кількості. - - -Подія onRender -============== - -Безпосередньо перед тим, як форма буде відображена, ми можемо викликати наш код. Він може, наприклад, додати елементам форми HTML-класи для правильного відображення. Код додаємо до масиву `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/uk/standalone.texy b/forms/uk/standalone.texy deleted file mode 100644 index 2c3f33bcf5..0000000000 --- a/forms/uk/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Форми, що використовуються окремо -********************************* - -.[perex] -Nette Forms значно полегшують створення та обробку веб-форм. Ви можете використовувати їх у своїх програмах абсолютно окремо від решти фреймворку, що ми покажемо в цьому розділі. - -Однак, якщо ви використовуєте Nette Application та презентери, для вас призначений посібник для [використання в презентерах|in-presenter]. - - -Перша форма -=========== - -Спробуємо написати просту реєстраційну форму. Її код буде таким ("повний код":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Ім\'я:'); -$form->addPassword('password', 'Пароль:'); -$form->addSubmit('send', 'Зареєструватися'); -``` - -Дуже легко її відобразимо: - -```php -$form->render(); -``` - -і в браузері вона зобразиться так: - -[* form-cs.webp *] - -Форма — це об'єкт класу `Nette\Forms\Form` (клас `Nette\Application\UI\Form` використовується в презентерах). Ми додали до неї так звані елементи: ім'я, пароль та кнопку відправки. - -А тепер оживимо форму. За допомогою запиту `$form->isSuccess()` ми дізнаємося, чи була форма відправлена і чи була вона заповнена валідно. Якщо так, виведемо дані. Отже, після визначення форми доповнимо: - -```php -if ($form->isSuccess()) { - echo 'Форма була правильно заповнена та відправлена'; - $data = $form->getValues(); - // $data->name містить ім'я - // $data->password містить пароль - var_dump($data); -} -``` - -Метод `getValues()` повертає відправлені дані у вигляді об'єкта [ArrayHash |utils:arrays#ArrayHash]. Як це змінити, ми покажемо [пізніше |#Мапінг на класи]. Об'єкт `$data` містить ключі `name` та `password` з даними, які ввів користувач. - -Зазвичай дані одразу надсилаються для подальшої обробки, наприклад, вставки в базу даних. Однак під час обробки може виникнути помилка, наприклад, ім'я користувача вже зайняте. У такому випадку ми передаємо помилку назад у форму за допомогою `addError()` і дозволяємо їй відобразитися знову, вже з повідомленням про помилку. - -```php -$form->addError('Вибачте, це ім\'я користувача вже використовується.'); -``` - -Після обробки форми перенаправимо на наступну сторінку. Це запобігає небажаному повторному відправленню форми кнопкою *оновити*, *назад* або рухом в історії браузера. - -Форма стандартно надсилається методом POST на ту саму сторінку. Обидва параметри можна змінити: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -І це, власне, все :-) Ми маємо функціональну та ідеально [захищену |#Захист від вразливостей] форму. - -Спробуйте додати й інші [елементи форми|controls]. - - -Доступ до елементів -=================== - -Форму та її окремі елементи ми називаємо компонентами. Вони утворюють дерево компонентів, де коренем є саме форма. До окремих елементів форми можна отримати доступ таким чином: - -```php -$input = $form->getComponent('name'); -// альтернативний синтаксис: $input = $form['name']; - -$button = $form->getComponent('send'); -// альтернативний синтаксис: $button = $form['send']; -``` - -Елементи видаляються за допомогою unset: - -```php -unset($form['name']); -``` - - -Правила валідації -================= - -Тут прозвучало слово *валідна*, але форма поки що не має жодних правил валідації. Давайте це виправимо. - -Ім'я буде обов'язковим, тому позначимо його методом `setRequired()`, аргументом якого є текст повідомлення про помилку, яке відобразиться, якщо користувач не введе ім'я. Якщо аргумент не вказано, використовується стандартне повідомлення про помилку. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Будь ласка, введіть ім\'я'); -``` - -Спробуйте відправити форму без заповненого імені, і ви побачите, що з'явиться повідомлення про помилку, а браузер чи сервер відхилятимуть її доти, доки ви не заповните поле. - -Водночас ви не обдурите систему, написавши в полі, наприклад, лише пробіли. Ні. Nette автоматично видаляє пробіли зліва та справа. Спробуйте самі. Це те, що ви завжди повинні робити з кожним однорядковим полем введення, але часто про це забувають. Nette робить це автоматично. (Можете спробувати обдурити форму і надіслати як ім'я багаторядковий рядок. Навіть тут Nette не дасть себе обдурити і змінить переноси рядків на пробіли.) - -Форма завжди валідується на стороні сервера, але також генерується JavaScript валідація, яка відбувається миттєво, і користувач дізнається про помилку одразу, без необхідності надсилати форму на сервер. За це відповідає скрипт `netteForms.js`. Вставте його на сторінку: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Якщо ви подивитеся на вихідний код сторінки з формою, ви помітите, що Nette вставляє обов'язкові елементи в елементи з CSS-класом `required`. Спробуйте додати до шаблону наступну таблицю стилів, і мітка «Ім'я» стане червоною. Таким чином, ми елегантно позначаємо для користувачів обов'язкові елементи: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Інші правила валідації додамо методом `addRule()`. Перший параметр — це правило, другий — знову текст повідомлення про помилку, а третім може бути аргумент правила валідації. Що це означає? - -Розширимо форму новим необов'язковим полем «вік», яке має бути цілим числом (`addInteger()`) і, крім того, у допустимому діапазоні (`$form::Range`). І саме тут ми використаємо третій параметр методу `addRule()`, яким передамо валідатору потрібний діапазон як пару `[від, до]`: - -```php -$form->addInteger('age', 'Вік:') - ->addRule($form::Range, 'Вік має бути від 18 до 120', [18, 120]); -``` - -.[tip] -Якщо користувач не заповнить поле, правила валідації не перевірятимуться, оскільки елемент є необов'язковим. - -Тут виникає простір для невеликого рефакторингу. У повідомленні про помилку та в третьому параметрі числа вказані дубльовано, що не ідеально. Якби ми створювали [багатомовні форми |rendering#Переклад] і повідомлення, що містить числа, було б перекладено кількома мовами, це ускладнило б можливу зміну значень. З цієї причини можна використовувати плейсхолдери `%d`, і Nette доповнить значення: - -```php - ->addRule($form::Range, 'Вік має бути від %d до %d років', [18, 120]); -``` - -Повернемося до елемента `password`, який також зробимо обов'язковим і ще перевіримо мінімальну довжину пароля (`$form::MinLength`), знову ж таки з використанням плейсхолдера: - -```php -$form->addPassword('password', 'Пароль:') - ->setRequired('Виберіть пароль') - ->addRule($form::MinLength, 'Пароль повинен мати щонайменше %d символів', 8); -``` - -Додамо до форми ще поле `passwordVerify`, де користувач введе пароль ще раз для перевірки. За допомогою правил валідації перевіримо, чи обидва паролі однакові (`$form::Equal`). А як параметр дамо посилання на перший пароль за допомогою [квадратних дужок |#Доступ до елементів]: - -```php -$form->addPassword('passwordVerify', 'Пароль для перевірки:') - ->setRequired('Будь ласка, введіть пароль ще раз для перевірки') - ->addRule($form::Equal, 'Паролі не співпадають', $form['password']) - ->setOmitted(); -``` - -За допомогою `setOmitted()` ми позначили елемент, значення якого насправді не має значення і який існує лише для валідації. Значення не передається до `$data`. - -Таким чином, ми маємо готову повнофункціональну форму з валідацією в PHP та JavaScript. Можливості валідації Nette набагато ширші, можна створювати умови, дозволяти на їх основі показувати та приховувати частини сторінки тощо. Все це ви дізнаєтеся в розділі про [валідацію форм|validation]. - - -Значення за замовчуванням -========================= - -Елементам форми зазвичай встановлюємо значення за замовчуванням: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Часто буває зручно встановити значення за замовчуванням для всіх елементів одночасно. Наприклад, коли форма служить для редагування записів. Читаємо запис з бази даних і встановлюємо значення за замовчуванням: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Викликайте `setDefaults()` після визначення елементів. - - -Відображення форми -================== - -Стандартно форма відображається як таблиця. Окремі елементи відповідають основному правилу доступності — всі мітки записані як `<label>` і пов'язані з відповідним елементом форми. При кліку на мітку курсор автоматично з'являється у полі форми. - -Кожному елементу ми можемо встановлювати будь-які HTML-атрибути. Наприклад, додати placeholder: - -```php -$form->addInteger('age', 'Вік:') - ->setHtmlAttribute('placeholder', 'Будь ласка, заповніть вік'); -``` - -Способів відображення форми є справді багато, тому цьому присвячено [окремий розділ про відображення|rendering]. - - -Мапінг на класи -=============== - -Повернемося до обробки даних форми. Метод `getValues()` повертав нам відправлені дані як об'єкт `ArrayHash`. Оскільки це загальний клас, щось на зразок `stdClass`, при роботі з ним нам бракуватиме певного комфорту, наприклад, автодоповнення властивостей у редакторах або статичного аналізу коду. Це можна було б вирішити, маючи для кожної форми конкретний клас, властивості якого представляють окремі елементи. Наприклад: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Альтернативно, ви можете використовувати конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public ?int $age, - public string $password, - ) { - } -} -``` - -Властивості класу даних також можуть бути enum-ами і будуть автоматично зіставлені. .{data-version:3.2.4} - -Як сказати Nette, щоб він повертав нам дані як об'єкти цього класу? Легше, ніж ви думаєте. Достатньо лише вказати назву класу або об'єкт для гідратації як параметр: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Як параметр можна також вказати `'array'`, і тоді дані повернуться як масив. - -Якщо форми утворюють багаторівневу структуру, що складається з контейнерів, створіть для кожного окремий клас: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапінг потім з типу властивості `$person` дізнається, що контейнер потрібно зіставити з класом `PersonFormData`. Якщо властивість містить масив контейнерів, вкажіть тип `array` і передайте клас для мапінгу безпосередньо контейнеру: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Проект класу даних форми можна згенерувати за допомогою методу `Nette\Forms\Blueprint::dataClass($form)`, який виведе його на сторінку браузера. Потім код достатньо виділити кліком і скопіювати в проект. .{data-version:3.1.15} - - -Кілька кнопок -============= - -Якщо форма має більше однієї кнопки, зазвичай потрібно розрізнити, яка з них була натиснута. Цю інформацію нам поверне метод `isSubmittedBy()` кнопки: - -```php -$form->addSubmit('save', 'Зберегти'); -$form->addSubmit('delete', 'Видалити'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Не пропускайте запит `$form->isSuccess()`, він перевірить валідність даних. - -Коли форма надсилається кнопкою <kbd>Enter</kbd>, це вважається так, ніби вона була надіслана першою кнопкою. - - -Захист від вразливостей -======================= - -Nette Framework приділяє велику увагу безпеці, тому ретельно дбає про надійний захист форм. - -Крім того, що форми захищають від атак [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] та [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], він виконує багато дрібних заходів безпеки, про які вам вже не потрібно думати. - -Наприклад, він фільтрує всі керуючі символи з вхідних даних і перевіряє валідність кодування UTF-8, тому дані з форми завжди будуть чистими. У select box-ах та radio list-ах він перевіряє, чи вибрані елементи дійсно були з запропонованих і чи не відбулася підробка. Ми вже згадували, що для однорядкових текстових полів він видаляє символи кінця рядка, які міг надіслати зловмисник. Для багаторядкових полів він нормалізує символи кінця рядка. І так далі. - -Nette вирішує за вас ризики безпеки, про існування яких багато програмістів навіть не здогадуються. - -Згадана атака CSRF полягає в тому, що зловмисник заманює жертву на сторінку, яка непомітно в браузері жертви виконує запит на сервер, на якому жертва залогінена, і сервер вважає, що запит виконала жертва за власним бажанням. Тому Nette запобігає надсиланню POST-форми з іншого домену. Якщо з якоїсь причини ви хочете вимкнути захист і дозволити надсилати форму з іншого домену, використовуйте: - -```php -$form->allowCrossOrigin(); // УВАГА! Вимикає захист! -``` - -Цей захист використовує SameSite cookie з назвою `_nss`. Тому створюйте об'єкт форми ще до надсилання першого виводу, щоб можна було надіслати cookie. - -Захист за допомогою SameSite cookie може бути не 100% надійним, тому рекомендується увімкнути ще захист за допомогою токена: - -```php -$form->addProtection(); -``` - -Рекомендуємо так захищати форми в адміністративній частині сайту, які змінюють чутливі дані в програмі. Фреймворк захищається від атаки CSRF шляхом генерації та перевірки авторизаційного токена, який зберігається в сесії. Тому необхідно перед відображенням форми мати відкриту сесію. В адміністративній частині сайту зазвичай сесія вже запущена через вхід користувача. В іншому випадку запустіть сесію методом `Nette\Http\Session::start()`. - -Отже, ми пройшли швидкий вступ до форм у Nette. Спробуйте ще заглянути в каталог [examples|https://github.com/nette/forms/tree/master/examples] у дистрибутиві, де ви знайдете більше натхнення. diff --git a/forms/uk/validation.texy b/forms/uk/validation.texy deleted file mode 100644 index 1c2bc94484..0000000000 --- a/forms/uk/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Валідація форм -************** - - -Обов'язкові елементи -==================== - -Обов'язкові елементи позначаємо методом `setRequired()`, аргументом якого є текст [#Повідомлення про помилки], який відобразиться, якщо користувач не заповнить елемент. Якщо аргумент не вказано, використовується стандартне повідомлення про помилку. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Будь ласка, введіть ім\'я'); -``` - - -Правила -======= - -Правила валідації додаємо до елементів методом `addRule()`. Перший параметр — це правило, другий — текст [#Повідомлення про помилки], а третій — аргумент правила валідації. - -```php -$form->addPassword('password', 'Пароль:') - ->addRule($form::MinLength, 'Пароль повинен мати щонайменше %d символів', 8); -``` - -**Правила валідації перевіряються лише в тому випадку, якщо користувач заповнив елемент.** - -Nette постачається з цілою низкою передбачених правил, назви яких є константами класу `Nette\Forms\Form`. Для всіх елементів ми можемо використовувати ці правила: - -| константа | опис | тип аргументу -|------- -| `Required` | обов'язковий елемент, псевдонім для `setRequired()` | - -| `Filled` | обов'язковий елемент, псевдонім для `setRequired()` | - -| `Blank` | елемент не повинен бути заповнений | - -| `Equal` | значення дорівнює параметру | `mixed` -| `NotEqual` | значення не дорівнює параметру | `mixed` -| `IsIn` | значення дорівнює одному з елементів у масиві | `array` -| `IsNotIn` | значення не дорівнює жодному з елементів у масиві | `array` -| `Valid` | чи елемент заповнений правильно? (для [#Умови]) | - - - -Текстові поля -------------- - -Для елементів `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` можна також використовувати деякі з наступних правил: - -| `MinLength` | мінімальна довжина тексту | `int` -| `MaxLength` | максимальна довжина тексту | `int` -| `Length` | довжина в діапазоні або точна довжина | пара `[int, int]` або `int` -| `Email` | дійсна електронна адреса | - -| `URL` | абсолютний URL | - -| `Pattern` | відповідає регулярному виразу | `string` -| `PatternInsensitive` | як `Pattern`, але нечутливий до регістру | `string` -| `Integer` | цілочисельне значення | - -| `Numeric` | псевдонім для `Integer` | - -| `Float` | число | - -| `Min` | мінімальне значення числового елемента | `int\|float` -| `Max` | максимальне значення числового елемента | `int\|float` -| `Range` | значення в діапазоні | пара `[int\|float, int\|float]` - -Правила валідації `Integer`, `Numeric` та `Float` одразу перетворюють значення на integer відповідно float. Крім того, правило `URL` приймає також адресу без схеми (наприклад, `nette.org`) і доповнює схему (`https://nette.org`). Вираз у `Pattern` та `PatternIcase` повинен відповідати всьому значенню, тобто ніби він був обгорнутий символами `^` та `$`. - - -Кількість елементів -------------------- - -Для елементів `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` можна також використовувати наступні правила для обмеження кількості вибраних елементів або завантажених файлів: - -| `MinLength` | мінімальна кількість | `int` -| `MaxLength` | максимальна кількість | `int` -| `Length` | кількість у діапазоні або точна кількість | пара `[int, int]` або `int` - - -Завантаження файлів -------------------- - -Для елементів `addUpload()`, `addMultiUpload()` можна також використовувати наступні правила: - -| `MaxFileSize` | максимальний розмір файлу в байтах | `int` -| `MimeType` | MIME-тип, дозволені плейсхолдери (`'video/*'`) | `string\|string[]` -| `Image` | зображення JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | ім'я файлу відповідає регулярному виразу | `string` -| `PatternInsensitive` | як `Pattern`, але нечутливий до регістру | `string` - -`MimeType` та `Image` вимагають PHP-розширення `fileinfo`. Те, що файл чи зображення є потрібного типу, визначається на основі його сигнатури, і **не перевіряється цілісність усього файлу.** Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |http:request#toImage]. - - -Повідомлення про помилки -======================== - -Усі передбачені правила, за винятком `Pattern` та `PatternInsensitive`, мають стандартне повідомлення про помилку, тому його можна пропустити. Однак, вказавши та сформулювавши всі повідомлення індивідуально, ви зробите форму більш зручною для користувача. - -Змінити стандартні повідомлення можна в [конфігурації|forms:configuration], змінивши тексти в масиві `Nette\Forms\Validator::$messages` або використовуючи [перекладач |rendering#Переклад]. - -У тексті повідомлень про помилки можна використовувати ці рядки-заповнювачі: - -| `%d` | замінюється послідовно на аргументи правила -| `%n$d` | замінюється на n-й аргумент правила -| `%label` | замінюється на мітку елемента (без двокрапки) -| `%name` | замінюється на ім'я елемента (наприклад, `name`) -| `%value` | замінюється на значення, введене користувачем - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Заповніть, будь ласка, %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'щонайменше %d і щонайбільше %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'щонайбільше %2$d і щонайменше %1$d', [5, 10]); -``` - - -Умови -===== - -Крім правил, можна додавати також умови. Вони записуються подібно до правил, тільки замість `addRule()` використовуємо метод `addCondition()` і, зрозуміло, не вказуємо жодного повідомлення про помилку (умова лише запитує): - -```php -$form->addPassword('password', 'Пароль:') - // якщо пароль не довший за 8 символів - ->addCondition($form::MaxLength, 8) - // тоді він повинен містити цифру - ->addRule($form::Pattern, 'Повинен містити цифру', '.*[0-9].*'); -``` - -Умову можна прив'язати і до іншого елемента, ніж поточний, за допомогою `addConditionOn()`. Як перший параметр вкажемо посилання на елемент. У цьому прикладі e-mail буде обов'язковим лише тоді, коли буде відмічено checkbox (його значення буде true): - -```php -$form->addCheckbox('newsletters', 'надсилайте мені розсилки'); - -$form->addEmail('email', 'E-mail:') - // якщо checkbox відмічено - ->addConditionOn($form['newsletters'], $form::Equal, true) - // тоді вимагай e-mail - ->setRequired('Введіть адресу електронної пошти'); -``` - -З умов можна створювати складні структури за допомогою `elseCondition()` та `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // якщо виконана перша умова - ->addConditionOn(/* ... */) // і друга умова на іншому елементі - ->addRule(/* ... */) // вимагай це правило - ->elseCondition() // якщо друга умова не виконана - ->addRule(/* ... */) // вимагай ці правила - ->addRule(/* ... */) - ->endCondition() // повертаємося до першої умови - ->addRule(/* ... */); -``` - -У Nette можна дуже легко реагувати на виконання чи невиконання умови також на стороні JavaScript за допомогою методу `toggle()`, див. [#Динамічний JavaScript]. - - -Посилання на інший елемент -========================== - -Як аргумент правила чи умови можна передати й інший елемент форми. Правило тоді використає значення, введене пізніше користувачем у браузері. Таким чином можна, наприклад, динамічно валідувати, що елемент `password` містить той самий рядок, що й елемент `password_confirm`: - -```php -$form->addPassword('password', 'Пароль'); -$form->addPassword('password_confirm', 'Підтвердіть пароль') - ->addRule($form::Equal, 'Введені паролі не співпадають', $form['password']); -``` - - -Власні правила та умови -======================= - -Іноді ми потрапляємо в ситуацію, коли вбудованих правил валідації в Nette недостатньо, і нам потрібно валідувати дані від користувача по-своєму. У Nette це дуже просто! - -Методам `addRule()` чи `addCondition()` можна як перший параметр передати будь-який callback. Він приймає як перший параметр сам елемент і повертає булеве значення, що визначає, чи валідація пройшла успішно. При додаванні правила за допомогою `addRule()` можна вказати й інші аргументи, які потім передаються як другий параметр. - -Власний набір валідаторів ми можемо створити як клас зі статичними методами: - -```php -class MyValidators -{ - // перевіряє, чи значення ділиться на аргумент - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // інші валідатори - } -} -``` - -Використання тоді дуже просте: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Значення має бути кратним числу %d', - 8, - ); -``` - -Власні правила валідації можна додавати і до JavaScript. Умовою є те, що правило має бути статичним методом. Його назва для JavaScript-валідатора утворюється шляхом об'єднання назви класу без зворотних слешів `\`, підкреслення `_` та назви методу. Наприклад, `App\MyValidators::validateDivisibility` запишемо як `AppMyValidators_validateDivisibility` і додамо до об'єкта `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Подія onValidate -================ - -Після надсилання форми проводиться валідація, під час якої перевіряються окремі правила, додані за допомогою `addRule()`, а потім викликається [подія |nette:glossary#Події události] `onValidate`. Її обробник можна використовувати для додаткової валідації, зазвичай для перевірки правильної комбінації значень у кількох елементах форми. - -Якщо виявлено помилку, передаємо її до форми методом `addError()`. Його можна викликати або на конкретному елементі, або безпосередньо на формі. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Ця комбінація неможлива.'); - } -} -``` - - -Помилки під час обробки -======================= - -У багатьох випадках про помилку ми дізнаємося лише тоді, коли обробляємо валідну форму, наприклад, записуємо новий елемент у базу даних і натрапляємо на дублювання ключів. У такому випадку помилку знову передаємо до форми методом `addError()`. Його можна викликати або на конкретному елементі, або безпосередньо на формі: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Неправильний пароль.'); - } -} -``` - -Якщо можливо, рекомендуємо прикріпити помилку безпосередньо до елемента форми, оскільки вона відобразиться поруч із ним при використанні стандартного візуалізатора. - -```php -$form['date']->addError('Вибачте, але ця дата вже зайнята.'); -``` - -Ви можете викликати `addError()` повторно і таким чином передати формі або елементу кілька повідомлень про помилки. Отримати їх можна за допомогою `getErrors()`. - -Увага, `$form->getErrors()` повертає зведення всіх повідомлень про помилки, включаючи ті, що були передані безпосередньо окремим елементам, а не лише безпосередньо формі. Повідомлення про помилки, передані лише формі, можна отримати через `$form->getOwnErrors()`. - - -Зміна вводу -=========== - -За допомогою методу `addFilter()` ми можемо змінити значення, введене користувачем. У цьому прикладі ми будемо толерувати та видаляти пробіли в поштовому індексі: - -```php -$form->addText('zip', 'Поштовий індекс:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // видалимо пробіли з поштового індексу - }) - ->addRule($form::Pattern, 'Поштовий індекс не у форматі п\'яти цифр', '\d{5}'); -``` - -Фільтр включається між правилами валідації та умовами, тому порядок методів має значення, тобто фільтр і правило викликаються в тому порядку, в якому вказані методи `addFilter()` та `addRule()`. - - -JavaScript валідація -==================== - -Мова для формулювання умов і правил дуже потужна. Усі конструкції при цьому працюють як на стороні сервера, так і на стороні JavaScript. Вони передаються в HTML-атрибутах `data-nette-rules` як JSON. Саму валідацію потім виконує скрипт, який перехоплює подію форми `submit`, проходить по окремих елементах і виконує відповідну валідацію. - -Цим скриптом є `netteForms.js`, і він доступний з кількох можливих джерел: - -Скрипт можна вставити безпосередньо в HTML-сторінку з CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Або скопіювати локально в публічний каталог проекту (наприклад, з `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Або встановити через [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -А потім завантажити та запустити: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Альтернативно, його можна завантажити безпосередньо з каталогу `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Динамічний JavaScript -===================== - -Хочете відображати поля для введення адреси лише якщо користувач вибере доставку товару поштою? Без проблем. Ключем є пара методів `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Цей код говорить, що коли умова виконана, тобто коли відмічено checkbox, буде видимим HTML-елемент `#address-container`. І навпаки. Елементи форми з адресою одержувача ми розмістимо в контейнері з цим ID, і при кліку на checkbox вони будуть приховані або показані. Це забезпечує скрипт `netteForms.js`. - -Як аргумент методу `toggle()` можна передати будь-який селектор. З історичних причин буквено-цифровий рядок без інших спеціальних символів розуміється як ID елемента, тобто так само, якби йому передував символ `#`. Другий необов'язковий параметр дозволяє інвертувати поведінку, тобто якби ми використали `toggle('#address-container', false)`, елемент би, навпаки, відображався лише тоді, коли checkbox не був би відмічений. - -Стандартна реалізація в JavaScript змінює властивість `hidden` елементів. Однак поведінку можна легко змінити, наприклад, додати анімацію. Достатньо в JavaScript перезаписати метод `Nette.toggle` власним рішенням: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // приховаємо або покажемо 'el' залежно від значення 'visible' - }); -}; -``` - - -Вимкнення валідації -=================== - -Іноді може знадобитися вимкнути валідацію. Якщо натискання кнопки відправки не повинно виконувати валідацію (підходить для кнопок *Cancel* або *Preview*), вимкнемо її методом `$submit->setValidationScope([])`. Якщо вона повинна виконувати лише часткову валідацію, ми можемо вказати, які поля або контейнери форми мають валідуватися. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Валідує всю форму -$form->addSubmit('send2') - ->setValidationScope([]); // Не валідує взагалі -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Валідує лише елемент name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Валідує лише елемент age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Валідує контейнер details -``` - -`setValidationScope` не впливає на [#подія onValidate] у формі, яка буде викликана завжди. Подія `onValidate` у контейнері буде викликана лише якщо цей контейнер позначений для часткової валідації. diff --git a/http/bg/@home.texy b/http/bg/@home.texy deleted file mode 100644 index 5dc37c2679..0000000000 --- a/http/bg/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Пакетът `nette/http` капсулира [HTTP request|request] & [response], работа със [сесии|sessions] и [парсване и съставяне на URL |urls]. - - -Инсталация ----------- - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/bg/@left-menu.texy b/http/bg/@left-menu.texy deleted file mode 100644 index 1f9b67ea4c..0000000000 --- a/http/bg/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Въведение |@home] -- [HTTP request|request] -- [HTTP response|response] -- [Сесии |Sessions] -- [URL utilities |urls] -- [Конфигурация |configuration] diff --git a/http/bg/@meta.texy b/http/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/http/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/http/bg/configuration.texy b/http/bg/configuration.texy deleted file mode 100644 index 56fe18000c..0000000000 --- a/http/bg/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -HTTP конфигурация -***************** - -.[perex] -Преглед на опциите за конфигурация за Nette HTTP. - -Ако не използвате целия framework, а само тази библиотека, прочетете [как да заредите конфигурацията|bootstrap:]. - - -HTTP хедъри -=========== - -```neon -http: - # хедъри, които се изпращат с всяка заявка - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # влияе на хедъра X-Frame-Options - frames: ... # (string|bool) по подразбиране е 'SAMEORIGIN' -``` - -Framework-ът по съображения за сигурност изпраща хедъра `X-Frame-Options: SAMEORIGIN`, който казва, че страницата може да бъде показана вътре в друга страница (в елемента `<iframe>`) само ако се намира на същия домейн. Това може да бъде нежелателно в някои ситуации (например, ако разработвате приложение за Facebook), затова поведението може да бъде променено чрез настройка `frames: http://allowed-host.com` или `frames: true`. - - -Content Security Policy ------------------------ - -Лесно могат да се съставят хедъри `Content-Security-Policy` (по-нататък CSP), тяхното описание ще намерите в [описанието на CSP |https://content-security-policy.com]. CSP директивите (като напр. `script-src`) могат да бъдат записани или като низове според спецификацията, или като масив от стойности за по-добра четимост. Тогава не е необходимо около ключовите думи, като например `'self'`, да се пишат кавички. Nette също автоматично генерира стойност `nonce`, така че в хедъра ще има например `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # низ във формат според спецификацията на CSP - default-src: "'self' https://example.com" - - # масив от стойности - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool в случай на превключватели - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -В шаблоните използвайте `<script n:nonce>...</script>` и стойността nonce ще се допълни автоматично. Създаването на безопасни сайтове в Nette е наистина лесно. - -Подобно могат да се съставят и хедъри `Content-Security-Policy-Report-Only` (които могат да се използват паралелно с CSP) и [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP бисквитки --------------- - -Могат да се променят стойностите по подразбиране на някои параметри на метода [Nette\Http\Response::setCookie() |response#setCookie] и сесията. - -```neon -http: - # обхват на бисквитката според пътя - cookiePath: ... # (string) по подразбиране е '/' - - # домейни, които приемат бисквитката - cookieDomain: 'example.com' # (string|domain) по подразбиране не е зададено - - # изпращане на бисквитка само през HTTPS? - cookieSecure: ... # (bool|auto) по подразбиране е auto - - # изключва изпращането на бисквитка, която Nette използва за защита срещу CSRF - disableNetteCookie: ... # (bool) по подразбиране е false -``` - -Атрибутът `cookieDomain` определя кои домейни могат да приемат бисквитката. Ако не е посочен, бисквитката се приема от същия (под)домейн, който я е задал, *но не* и от неговите поддомейни. Ако `cookieDomain` е зададен, са включени и поддомейните. Затова посочването на `cookieDomain` е по-малко ограничаващо от пропускането му. - -Например при `cookieDomain: nette.org` бисквитките са достъпни и на всички поддомейни като `doc.nette.org`. Същото може да се постигне и с помощта на специалната стойност `domain`, т.е. `cookieDomain: domain`. - -Стойността по подразбиране `auto` при атрибута `cookieSecure` означава, че ако сайтът работи на HTTPS, бисквитките ще се изпращат с флаг `Secure` и следователно ще бъдат достъпни само през HTTPS. - - -HTTP прокси ------------ - -Ако сайтът работи зад HTTP прокси, въведете неговия IP адрес, за да работи правилно откриването на връзка през HTTPS и също IP адресите на клиента. Тоест, за да функциите [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] и [isSecured() |request#isSecured] връщат правилните стойности и в шаблоните да се генерират връзки с `https:` протокол. - -```neon -http: - # IP адрес, обхват (напр. 127.0.0.1/8) или масив от тези стойности - proxy: 127.0.0.1 # (string|string[]) по подразбиране не е зададено -``` - - -Сесия -===== - -Основни настройки на [сесиите|sessions]: - -```neon -session: - # показване на панела за сесии в Tracy Bar? - debugger: ... # (bool) по подразбиране е false - - # период на неактивност, след който сесията изтича - expiration: 14 days # (string) по подразбиране е '3 hours' - - # кога да се стартира сесията? - autoStart: ... # (smart|always|never) по подразбиране е 'smart' - - # handler, сървис, имплементиращ интерфейса SessionHandlerInterface - handler: @handlerService -``` - -Опцията `autoStart` контролира кога да се стартира сесията. Стойността `always` означава, че сесията ще се стартира винаги при стартиране на приложението. Стойността `smart` означава, че сесията ще се стартира при стартиране на приложението само тогава, когато вече съществува, или в момента, в който искаме да четем от нея или да записваме в нея. И накрая стойността `never` забранява автоматичното стартиране на сесията. - -Освен това могат да се настройват всички PHP [session директиви |https://www.php.net/manual/en/session.configuration.php] (във формат camelCase) и също [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Пример: - -```neon -session: - # 'session.name' записваме като 'name' - name: MYID - - # 'session.save_path' записваме като 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Бисквитка за сесия ------------------- - -Бисквитката за сесия се изпраща със същите параметри като [други бисквитки |#HTTP бисквитки], но тези можете да промените за нея: - -```neon -session: - # домейни, които приемат бисквитката - cookieDomain: 'example.com' # (string|domain) - - # ограничение при достъп от друг домейн - cookieSamesite: None # (Strict|Lax|None) по подразбиране е Lax -``` - -Атрибутът `cookieSamesite` влияе дали бисквитката ще бъде изпратена при [достъп от друг домейн |nette:glossary#SameSite cookie], което предоставя известна защита срещу атаки [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -DI сървиси -========== - -Тези сървиси се добавят към DI контейнера: - -| Име | Тип | Описание -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP заявка| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP отговор| response] -| `session.session` | [api:Nette\Http\Session] | [управление на сесии| sessions] diff --git a/http/bg/request.texy b/http/bg/request.texy deleted file mode 100644 index a807ef95ff..0000000000 --- a/http/bg/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -HTTP заявка -*********** - -.[perex] -Nette капсулира HTTP заявката в обекти с разбираем API и същевременно предоставя саниращ филтър. - -HTTP заявката представлява обект [api:Nette\Http\Request]. Ако работите с Nette, този обект се създава автоматично от framework-а и можете да го получите чрез [dependency injection |dependency-injection:passing-dependencies]. В презентерите е достатъчно само да извикате метода `$this->getHttpRequest()`. Ако работите извън Nette Framework, можете да създадете обект с помощта на [#RequestFactory]. - -Голямо предимство на Nette е, че при създаването на обекта автоматично почиства всички входни параметри GET, POST, COOKIE, както и URL от контролни знаци и невалидни UTF-8 последователности. С тези данни след това можете безопасно да работите по-нататък. Почистените данни след това се използват в презентерите и формите. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Nette\Http\Request -================== - -Този обект е immutable (непроменлив). Няма никакви сетъри, има само един т.нар. wither `withUrl()`, който не променя обекта, а връща нов екземпляр с променена стойност. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Връща клонинг с различен URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Връща URL на заявката като обект [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Предупреждение: браузърите не изпращат фрагмент на сървъра, така че `$url->getFragment()` ще връща празен низ. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Връща параметрите на GET заявката. - -```php -$all = $httpRequest->getQuery(); // връща масив с всички параметри от URL -$id = $httpRequest->getQuery('id'); // връща GET параметър 'id' (или null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Връща параметрите на POST заявката. - -```php -$all = $httpRequest->getPost(); // връща масив с всички параметри от POST -$id = $httpRequest->getPost('id'); // връща POST параметър 'id' (или null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Връща [качване |#Качени файлове] като обект [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // качен ли е файл? - $file->getUntrustedName(); // име на файла, изпратено от потребителя - $file->getSanitizedName(); // име без опасни символи -} -``` - -За достъп до вложена структура посочете масив от ключове. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Тъй като не може да се вярва на данни отвън и следователно не може да се разчита на структурата на файловете, този начин е по-безопасен от например `$request->getFiles()['my-form']['details']['avatar']`, който може да се провали. - - -getFiles(): array .[method] ---------------------------- -Връща дърво на [всички качвания |#Качени файлове] в нормализирана структура, чиито листа са обекти [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Връща бисквитка или `null`, когато не съществува. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Връща всички бисквитки. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Връща HTTP метода, с който е направена заявката. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Тества HTTP метода, с който е направена заявката. Параметърът е case-insensitive. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Връща HTTP хедър или `null`, ако не съществува. Параметърът е case-insensitive. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Връща всички HTTP хедъри като асоциативен масив. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Връзката шифрована ли е (HTTPS)? За правилната функционалност може да е необходимо [да се настрои прокси |configuration#HTTP прокси]. - - -isSameSite(): bool .[method] ----------------------------- -Заявката идва ли от същия (под)домейн и е инициирана чрез кликване върху връзка? Nette използва бисквитката `_nss` (преди `nette-samesite`) за откриване. - - -isAjax(): bool .[method] ------------------------- -Това AJAX заявка ли е? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Връща IP адреса на потребителя. За правилната функционалност може да е необходимо [да се настрои прокси |configuration#HTTP прокси]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Връща DNS превода на IP адреса на потребителя. За правилната функционалност може да е необходимо [да се настрои прокси |configuration#HTTP прокси]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Връща данните за удостоверяване за [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Връща тялото на HTTP заявката. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Открива езика. Като параметър `$lang` предаваме масив с езиците, които приложението поддържа, и тя връща този, който браузърът на посетителя би предпочел най-много. Това не са никакви магии, просто се използва хедърът `Accept-Language`. Ако не се намери съвпадение, връща `null`. - -```php -// браузърът изпраща напр. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // езици, поддържани от приложението -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Класът [api:Nette\Http\RequestFactory] служи за създаване на екземпляр на `Nette\Http\Request`, който представлява текущата HTTP заявка. (Ако работите с Nette, обектът на HTTP заявката се създава автоматично от framework-а.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Методът `fromGlobals()` създава обект на заявката въз основа на текущите глобални променливи на PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` и `$_SERVER`). При създаването на обекта автоматично почиства всички входни параметри GET, POST, COOKIE, както и URL от контролни знаци и невалидни UTF-8 последователности, което осигурява безопасност при по-нататъшна работа с тези данни. - -RequestFactory може да се конфигурира преди извикването на `fromGlobals()`: - -- с метода `$factory->setBinary()` изключвате автоматичното почистване на входните параметри от контролни знаци и невалидни UTF-8 последователности. -- с метода `$factory->setProxy(...)` посочвате IP адреса на [прокси сървъра |configuration#HTTP прокси], което е необходимо за правилното откриване на IP адреса на потребителя. - -RequestFactory позволява да се дефинират филтри, които автоматично трансформират части от URL на заявката. Тези филтри премахват нежелани знаци от URL, които могат да бъдат вмъкнати там например поради неправилна имплементация на системи за коментари на различни сайтове: - -```php -// премахване на интервали от пътя -$requestFactory->urlFilters['path']['%20'] = ''; - -// премахване на точка, запетая или дясна скоба от края на URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// почистване на пътя от двойни наклонени черти (филтър по подразбиране) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Първият ключ `'path'` или `'url'` определя към коя част на URL ще се приложи филтърът. Вторият ключ е регулярен израз, който трябва да се търси, а стойността е заместителят, който ще се използва вместо намерения текст. - - -Качени файлове -============== - -Методът `Nette\Http\Request::getFiles()` връща масив от всички качвания в нормализирана структура, чиито листа са обекти [api:Nette\Http\FileUpload]. Те капсулират данните, изпратени от елемента на формата `<input type=file>`. - -Структурата отразява именуването на елементите в HTML. В най-простия случай това може да бъде единствен именуван елемент на формата, изпратен като: - -```latte -<input type="file" name="avatar"> -``` - -В този случай `$request->getFiles()` връща масив: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -Обектът `FileUpload` се създава и в случай, че потребителят не е изпратил никакъв файл или изпращането е неуспешно. Дали файлът е бил изпратен връща методът `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -В случай на име на елемент, използващо нотация за масив: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -върнатото дърво изглежда така: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Може да се създаде и масив от файлове: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -В такъв случай структурата изглежда така: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -Достъпът до индекс 1 на вложения масив се осъществява най-добре така: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Тъй като не може да се вярва на данни отвън и следователно не може да се разчита на структурата на файловете, този начин е по-безопасен от например `$request->getFiles()['my-form']['details']['avatars'][1]`, който може да се провали. - - -Преглед на методите на `FileUpload` .{toc: FileUpload} ------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Връща `true`, ако потребителят е качил някакъв файл. - - -isOk(): bool .[method] ----------------------- -Връща `true`, ако файлът е бил качен успешно. - - -getError(): int .[method] -------------------------- -Връща кода на грешката при качване на файла. Това е една от константите [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. В случай, че качването е преминало успешно, връща `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Премества качения файл на ново място. Ако целевият файл вече съществува, той ще бъде презаписан. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Връща съдържанието на качения файл. В случай, че качването не е било успешно, връща `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Открива MIME content type на качения файл въз основа на неговата сигнатура. В случай, че качването не е било успешно или откриването не е успяло, връща `null`. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Връща оригиналното име на файла, както го е изпратил браузърът. - -.[caution] -Не вярвайте на стойността, върната от този метод. Клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - - -getSanitizedName(): string .[method] ------------------------------------- -Връща санираното име на файла. Съдържа само ASCII знаци `[a-zA-Z0-9.-]`. Ако името не съдържа такива знаци, връща `'unknown'`. Ако файлът е изображение във формат JPEG, PNG, GIF, WebP или AVIF, връща и правилното разширение. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Връща подходящо разширение на файла (без точка), съответстващо на открития MIME тип. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Връща оригиналния път до файла, както го е изпратил браузърът при качване на папка. Целият път е достъпен само в PHP 8.1 и по-нови версии. В предишни версии този метод връща оригиналното име на файла. - -.[caution] -Не вярвайте на стойността, върната от този метод. Клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - - -getSize(): int .[method] ------------------------- -Връща размера на качения файл. В случай, че качването не е било успешно, връща `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Връща пътя до временната локация на качения файл. В случай, че качването не е било успешно, връща `''`. - - -isImage(): bool .[method] -------------------------- -Връща `true`, ако каченият файл е изображение във формат JPEG, PNG, GIF, WebP или AVIF. Откриването се извършва въз основа на неговата сигнатура и не се проверява целостта на целия файл. Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |#toImage]. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Връща двойка `[ширина, височина]` с размерите на каченото изображение. В случай, че качването не е било успешно или не е валидно изображение, връща `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Зарежда изображението като обект [Image|utils:images]. В случай, че качването не е било успешно или не е валидно изображение, хвърля изключение `Nette\Utils\ImageException`. diff --git a/http/bg/response.texy b/http/bg/response.texy deleted file mode 100644 index 461e66a71a..0000000000 --- a/http/bg/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -HTTP отговор -************ - -.[perex] -Nette капсулира HTTP отговора в обекти с разбираем API. - -HTTP отговорът представлява обект [api:Nette\Http\Response]. Ако работите с Nette, този обект се създава автоматично от framework-а и можете да го получите чрез [dependency injection |dependency-injection:passing-dependencies]. В презентерите е достатъчно само да извикате метода `$this->getHttpResponse()`. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Nette\Http\Response -=================== - -Обектът, за разлика от [Nette\Http\Request|request], е mutable, т.е. с помощта на сетъри можете да променяте състоянието, например да изпращате хедъри. Не забравяйте, че всички сетъри трябва да бъдат извикани **преди изпращането на какъвто и да е изход.** Дали вече е бил изпратен изход показва методът `isSent()`. Ако връща `true`, всеки опит за изпращане на хедър ще предизвика изключение `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Променя [кода на състоянието на отговора |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. За по-добра разбираемост на изходния код препоръчваме за кода да се използват вместо числа [предварително дефинирани константи |api:Nette\Http\IResponse]. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Връща кода на състоянието на отговора. - - -isSent(): bool .[method] ------------------------- -Връща дали вече са били изпратени хедъри от сървъра към браузъра и следователно вече не е възможно да се изпращат хедъри или да се променя кодът на състоянието. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Изпраща HTTP хедър и **презаписва** предишно изпратен хедър със същото име. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Изпраща HTTP хедър и **не презаписва** предишно изпратен хедър със същото име. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Изтрива предишно изпратен HTTP хедър. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Връща изпратен HTTP хедър или `null`, ако такъв не съществува. Параметърът е case-insensitive. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Връща всички изпратени HTTP хедъри като асоциативен масив. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Променя хедъра `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Пренасочва към друг URL. Не забравяйте след това да прекратите скрипта. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Задава изтичането на HTTP документа с помощта на хедърите `Cache-Control` и `Expires`. Параметърът е или времеви интервал (като текст), или `null`, което забранява кеширането. - -```php -// кешът в браузъра изтича след час -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Отговорът ще бъде изтеглен с помощта на диалоговия прозорец *Запиши като* под посоченото име. Самият файл при това не се изпраща. - -```php -$httpResponse->sendAsFile('faktura.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Изпраща бисквитка. Стойностите по подразбиране на параметрите: - -| `$path` | `'/'` | бисквитката има обхват за всички пътища в (под)домейна *(конфигурируемо)* -| `$domain` | `null` | което означава с обхват за текущия (под)домейн, но не и неговите поддомейни *(конфигурируемо)* -| `$secure` | `true` | ако сайтът работи на HTTPS, иначе `false` *(конфигурируемо)* -| `$httpOnly` | `true` | бисквитката е недостъпна за JavaScript -| `$sameSite` | `'Lax'` | бисквитката може да не бъде изпратена при [достъп от друг домейн |nette:glossary#SameSite cookie] - -Стойностите по подразбиране на параметрите `$path`, `$domain` и `$secure` можете да промените в [конфигурацията |configuration#HTTP бисквитки]. - -Времето може да се посочва като брой секунди или низ: - -```php -$httpResponse->setCookie('lang', 'bg', '100 days'); -``` - -Параметърът `$domain` определя кои домейни могат да приемат бисквитката. Ако не е посочен, бисквитката се приема от същия (под)домейн, който я е задал, но не и от неговите поддомейни. Ако `$domain` е зададен, са включени и поддомейните. Затова посочването на `$domain` е по-малко ограничаващо от пропускането му. Например при `$domain = 'nette.org'` бисквитките са достъпни и на всички поддомейни като `doc.nette.org`. - -За стойността `$sameSite` можете да използвате константите `Response::SameSiteLax`, `SameSiteStrict` и `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Изтрива бисквитка. Стойностите по подразбиране на параметрите са: -- `$path` с обхват за всички директории (`'/'`) -- `$domain` с обхват за текущия (под)домейн, но не и неговите поддомейни -- `$secure` се управлява според настройките в [конфигурацията |configuration#HTTP бисквитки] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/bg/sessions.texy b/http/bg/sessions.texy deleted file mode 100644 index 73b3cb8c28..0000000000 --- a/http/bg/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Сесии -***** - -<div class=perex> - -HTTP е протокол без състояние, но почти всяко приложение трябва да съхранява състояние между заявките, например съдържанието на количката за пазаруване. Именно за това служат сесиите. Ще покажем, - -- как да използваме сесии -- как да предотвратим конфликти на имена -- как да настроим изтичане - -</div> - -При използване на сесии всеки потребител получава уникален идентификатор, наречен session ID, който се предава в бисквитка. Той служи като ключ към данните на сесията. За разлика от бисквитките, които се съхраняват от страна на браузъра, данните в сесията се съхраняват от страна на сървъра. - -Сесията се настройва в [конфигурацията |configuration#Сесия], важен е особено изборът на времето за изтичане. - -Управлението на сесията се осъществява от обекта [api:Nette\Http\Session], до който можете да стигнете, като го получите чрез [dependency injection |dependency-injection:passing-dependencies]. В презентерите е достатъчно само да извикате `$session = $this->getSession()`. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Стартиране на сесия -=================== - -Nette по подразбиране автоматично стартира сесията в момента, когато започнем да четем от нея или да записваме данни в нея. Ръчно сесията се стартира с `$session->start()`. - -PHP изпраща при стартиране на сесията HTTP хедъри, влияещи на кеширането, виж [php:session_cache_limiter], и евентуално и бисквитка със session ID. Затова е необходимо винаги да стартирате сесията преди изпращането на какъвто и да е изход към браузъра, иначе ще бъде хвърлено изключение. Ако знаете, че по време на рендирането на страницата ще се използва сесия, стартирайте я ръчно преди това, например в презентера. - -В режим на разработка сесията се стартира от Tracy, тъй като я използва за показване на ленти с пренасочвания и AJAX заявки в Tracy Bar. - - -Секции -====== - -В чист PHP хранилището на данни на сесията се реализира като масив, достъпен чрез глобалната променлива `$_SESSION`. Проблемът е, че приложенията обикновено се състоят от цяла редица взаимно независими части и ако всички имат на разположение само един масив, рано или късно ще възникне конфликт на имена. - -Nette Framework решава проблема, като разделя цялото пространство на секции (обекти [api:Nette\Http\SessionSection]). Всяка единица след това използва своя собствена секция с уникално име и вече не може да възникне никакъв конфликт. - -Секцията получаваме от сесията: - -```php -$section = $session->getSection('уникално_име'); -``` - -В презентера е достатъчно да използвате `getSession()` с параметър: - -```php -// $this е Presenter -$section = $this->getSession('уникално_име'); -``` - -Проверката за съществуване на секция може да се направи с метода `$session->hasSection('уникално_име')`. - -Със самата секция след това се работи много лесно с помощта на методите `set()`, `get()` и `remove()`: - -```php -// запис на променлива -$section->set('userName', 'franta'); - -// четене на променлива, връща null, ако не съществува -echo $section->get('userName'); - -// изтриване на променлива -$section->remove('userName'); -``` - -За получаване на всички променливи от секцията може да се използва цикъл `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Настройка на изтичане ---------------------- - -За отделни секции или дори отделни променливи е възможно да се настрои изтичане. Можем така да оставим изтичането на влизането на потребителя след 20 минути, но същевременно да продължим да помним съдържанието на количката. - -```php -// секцията изтича след 20 минути -$section->setExpiration('20 minutes'); -``` - -За настройка на изтичането на отделни променливи служи третият параметър на метода `set()`: - -```php -// променливата 'flash' изтича след 30 секунди -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Не забравяйте, че времето за изтичане на цялата сесия (виж [конфигурация на сесията |configuration#Сесия]) трябва да бъде същото или по-голямо от времето, зададено за отделните секции или променливи. - -Отмяната на предишно зададено изтичане се постига с метода `removeExpiration()`. Незабавното отменяне на цялата секция осигурява методът `remove()`. - - -Събития $onStart, $onBeforeWrite --------------------------------- - -Обектът `Nette\Http\Session` има [събития |nette:glossary#Събития events] `$onStart` и `$onBeforeWrite`, така че можете да добавите callback-ове, които се извикват след стартиране на сесията или преди нейното записване на диска и последващо прекратяване. - -```php -$session->onBeforeWrite[] = function () { - // записваме данни в сесията - $this->section->set('basket', $this->basket); -}; -``` - - -Управление на сесии -=================== - -Преглед на методите на класа `Nette\Http\Session` за управление на сесии: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Стартира сесията. - - -isStarted(): bool .[method] ---------------------------- -Сесията стартирана ли е? - - -close(): void .[method] ------------------------ -Прекратява сесията. Сесията автоматично се прекратява в края на изпълнението на скрипта. - - -destroy(): void .[method] -------------------------- -Прекратява и изтрива сесията. - - -exists(): bool .[method] ------------------------- -HTTP заявката съдържа ли бисквитка със session ID? - - -regenerateId(): void .[method] ------------------------------- -Генерира нов случаен session ID. Данните остават запазени. - - -getId(): string .[method] -------------------------- -Връща session ID. - -</div> - - -Конфигурация ------------- - -Сесията се настройва в [конфигурацията |configuration#Сесия]. Ако пишете приложение, което не използва DI контейнер, за конфигурация служат тези методи. Трябва да бъдат извикани преди стартирането на сесията. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Задава името на бисквитката, в която се пренася session ID. Стандартното име е `PHPSESSID`. Полезно е в случай, че в рамките на един сайт поддържате няколко различни приложения. - - -getName(): string .[method] ---------------------------- -Връща името на бисквитката, в която се пренася session ID. - - -setOptions(array $options): static .[method] --------------------------------------------- -Конфигурира сесията. Могат да се настройват всички PHP [session директиви |https://www.php.net/manual/en/session.configuration.php] (във формат camelCase, напр. вместо `session.save_path` записваме `savePath`) и също [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Задава времето на неактивност, след което сесията изтича. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Настройка на параметрите за бисквитката. Стойностите по подразбиране на параметрите можете да промените в [конфигурацията |configuration#Бисквитка за сесия]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Задава директорията, където се съхраняват файловете със сесиите. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Настройка на собствен handler, виж [документацията на PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Сигурността преди всичко -======================== - -Сървърът предполага, че комуникира постоянно със същия потребител, докато заявките са придружени от същия session ID. Задачата на механизмите за сигурност е да гарантират, че това наистина е така и не е възможно идентификаторът да бъде откраднат или подменен. - -Nette Framework затова правилно конфигурира PHP директивите, така че session ID да се пренася само в бисквитка, да го направи недостъпен за JavaScript и да игнорира евентуални идентификатори в URL. Освен това в критични моменти, като например влизане на потребителя, генерира нов session ID. - -.[note] -За конфигурация на PHP се използва функцията ini_set, която за съжаление някои хостинги забраняват. Ако това е случаят и с вашия хостинг, опитайте да се договорите с него да ви разреши функцията или поне да конфигурира сървъра. diff --git a/http/bg/urls.texy b/http/bg/urls.texy deleted file mode 100644 index cdbf111e0a..0000000000 --- a/http/bg/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Работа с URL адреси -******************* - -.[perex] -Класовете [#Url], [#UrlImmutable] и [#UrlScript] позволяват лесно генериране, парсиране и манипулиране на URL адреси. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Url -=== - -Класът [api:Nette\Http\Url] позволява лесно да се работи с URL и неговите отделни компоненти, които са показани на тази схема: - -/--pre - схема потребител парола хост порт път заявка фрагмент - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Генерирането на URL е интуитивно: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Може също да се парсира URL и да се манипулира по-нататък: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Класът `Url` имплементира интерфейса `JsonSerializable` и има метод `__toString()`, така че обектът може да бъде изведен или използван в данни, предавани на `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL компоненти .[method] ------------------------- - -За връщане или промяна на отделните компоненти на URL са ви на разположение тези методи: - -.[language-php] -| Setter | Getter | Върната стойност -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цял URL - -Предупреждение: Когато работите с URL, който е получен от [HTTP заявка|request], имайте предвид, че той няма да съдържа фрагмент, тъй като браузърът не го изпраща на сървъра. - -Можем да работим и с отделните query параметри с помощта на: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Връща дясната или лявата част на хоста. Така работи, ако хостът е `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Проверява дали два URL адреса са идентични. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Проверява дали URL адресът е абсолютен. URL се счита за абсолютен, ако започва със схема (напр. http, https, ftp), последвана от двоеточие. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Нормализира пътя в URL чрез премахване на специалните сегменти `.` и `..`. Методът премахва излишните елементи на пътя по същия начин, както го правят уеб браузърите. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Класът [api:Nette\Http\UrlImmutable] е immutable (непроменлива) алтернатива на класа [#Url] (подобно на това как в PHP `DateTimeImmutable` е непроменлива алтернатива на `DateTime`). Вместо сетъри има т.нар. withery, които не променят обекта, а връщат нови екземпляри с променена стойност: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Класът `UrlImmutable` имплементира интерфейса `JsonSerializable` и има метод `__toString()`, така че обектът може да бъде изведен или използван в данни, предавани на `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL компоненти .[method] ------------------------- - -За връщане или промяна на отделните компоненти на URL служат методите: - -.[language-php] -| Wither | Getter | Върната стойност -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цял URL - -Методът `withoutUserInfo()` премахва `user` и `password`. - -Можем да работим и с отделните query параметри с помощта на: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Връща дясната или лявата част на хоста. Така работи, ако хостът е `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Извежда абсолютен URL по същия начин, по който браузърът обработва връзките на HTML страница: -- ако връзката е абсолютен URL (съдържа схема), тя се използва без промяна -- ако връзката започва с `//`, се приема само схемата от текущия URL -- ако връзката започва с `/`, се създава абсолютен път от корена на домейна -- в останалите случаи URL се съставя относително спрямо текущия път - -```php -$url = new UrlImmutable('https://example.com/path/page'); -echo $url->resolve('../foo'); // 'https://example.com/foo' -echo $url->resolve('/bar'); // 'https://example.com/bar' -echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Проверява дали два URL адреса са идентични. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Класът [api:Nette\Http\UrlScript] е наследник на [#UrlImmutable] и го разширява с допълнителни виртуални компоненти на URL, като например коренната директория на проекта и др. Подобно на родителския клас, той е immutable (непроменлив) обект. - -Следващата диаграма показва компонентите, които UrlScript разпознава: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` е основният URL адрес на приложението, включително домейна и частта от пътя до коренната директория на приложението -- `basePath` е частта от пътя до коренната директория на приложението -- `scriptPath` е пътят до текущия скрипт -- `relativePath` е името на скрипта (евентуално и други сегменти от пътя) относително спрямо basePath -- `relativeUrl` е цялата част от URL след baseUrl, включително query string и фрагмент. -- `pathInfo` днес вече малко използвана част от URL след името на скрипта - -За връщане на части от URL са на разположение методите: - -.[language-php] -| Getter | Върната стойност -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Обектите `UrlScript` обикновено не ги създаваме директно, а ги връща методът [Nette\Http\Request::getUrl()|request] с вече правилно зададени компоненти за текущата HTTP заявка. diff --git a/http/cs/@home.texy b/http/cs/@home.texy index 47814c400d..a3a7450422 100644 --- a/http/cs/@home.texy +++ b/http/cs/@home.texy @@ -2,7 +2,14 @@ Nette HTTP ********** .[perex] -Balíček `nette/http` zapouzdřuje [HTTP request|request] & [response], práci se [sessions] a [parsování a skládání URL |urls]. +Balíček `nette/http` je vaším pomocníkem pro veškerou komunikaci přes HTTP. Poskytuje srozumitelné objektové API nad příchozím požadavkem a odchozí odpovědí, usnadňuje práci se sessions i s URL adresami a navíc se postará o bezpečnost. Co v něm najdete: + +| [HTTP request |request] | příchozí požadavek a sanitizace vstupů +| [HTTP response |response] | odchozí odpověď, hlavičky a cookies +| [Sessions] | bezpečné uchování stavu mezi požadavky +| [Práce s URL |urls] | parsování a skládání URL adres +| [Ochrana proti SSRF |ssrf] | obrana proti útokům Server-Side Request Forgery +| [Konfigurace |configuration] | konfigurační volby balíčku Instalace @@ -13,3 +20,5 @@ Knihovnu stáhnete a nainstalujete pomocí nástroje [Composer|best-practices:co ```shell composer require nette/http ``` + +Balíček vyžaduje PHP verze 8.3 až 8.5. diff --git a/http/cs/@left-menu.texy b/http/cs/@left-menu.texy index 8753fc54a5..98773f362e 100644 --- a/http/cs/@left-menu.texy +++ b/http/cs/@left-menu.texy @@ -4,5 +4,16 @@ Nette HTTP - [HTTP request|request] - [HTTP response|response] - [Sessions] -- [URL utilities |urls] +- [Práce s URL |urls] +- [Ochrana proti SSRF |ssrf] - [Konfigurace |configuration] +- [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/http/cs/configuration.texy b/http/cs/configuration.texy index 4d9bc137d9..47cdb3a9e2 100644 --- a/http/cs/configuration.texy +++ b/http/cs/configuration.texy @@ -4,7 +4,7 @@ Konfigurace HTTP .[perex] Přehled konfiguračních voleb pro Nette HTTP. -Pokud nepoužívate celý framework, ale jen tuto knihovnu, přečtěte si, [jak konfiguraci načíst|bootstrap:]. +Pokud nepoužíváte celý framework, ale jen tuto knihovnu, přečtěte si, [jak konfiguraci načíst|bootstrap:]. HTTP hlavičky @@ -12,17 +12,19 @@ HTTP hlavičky ```neon http: - # hlavičky, které se s každým požadavkem odešlou + # hlavičky, které se s každou odpovědí odešlou headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' # ovlivňuje hlavičku X-Frame-Options - frames: ... # (string|bool) výchozí je 'SAMEORIGIN' + frames: ... # (string|bool|null) výchozí je 'SAMEORIGIN' ``` -Framework z bezpečnostních důvodů odesílá hlavičku `X-Frame-Options: SAMEORIGIN`, která říká, že stránku lze zobrazit uvnitř jiné stránky (v elementu `<iframe>`) pouze pokud se nachází na stejné doméně. To může být v některých situacích nežádoucí (například pokud vyvíjíte aplikaci pro Facebook), chování lze proto změnit nastavením `frames: http://allowed-host.com` nebo `frames: true`. +Framework z bezpečnostních důvodů odesílá hlavičku `X-Frame-Options: SAMEORIGIN`, která říká, že stránku lze zobrazit uvnitř jiné stránky (v elementu `<iframe>`) pouze tehdy, pokud se nachází na stejné doméně. To může být v některých situacích nežádoucí (například pokud vyvíjíte aplikaci pro Facebook); chování lze proto změnit nastavením `frames: http://allowed-host.com` pro povolení konkrétního hostitele, `frames: true` pro povolení vkládání odkudkoli (hlavička se vynechá) nebo `frames: false` pro úplný zákaz (`X-Frame-Options: DENY`). + +Ve výchozím stavu Nette navíc odesílá hlavičky `X-Powered-By: Nette Framework 3` a `Content-Type: text/html; charset=utf-8`. Kteroukoli hlavičku, včetně těchto výchozích, odeberete nastavením její hodnoty na prázdný řetězec. Content Security Policy @@ -72,7 +74,7 @@ http: HTTP cookie ----------- -Lze změnit vychozí hodnoty některých parametrů metody [Nette\Http\Response::setCookie() |response#setCookie] a session. +Lze změnit výchozí hodnoty některých parametrů metody [Nette\Http\Response::setCookie() |response#setCookie] a session. ```neon http: @@ -108,6 +110,32 @@ http: ``` +Vynucení HTTPS .{data-version:3.3.4} +------------------------------------ + +Bezpodmínečně vynutí HTTPS schéma požadavku. Hodí se pro weby běžící výhradně na HTTPS za load balancerem nebo reverzní proxy, která terminuje TLS, ale neposílá hlavičku `X-Forwarded-Proto`, takže by standardní detekce HTTPS (ani s nastavenou [#HTTP proxy]) nezabrala. + +```neon +http: + # vynutí HTTPS schéma u všech požadavků + forceHttps: true # (bool) výchozí je false +``` + + +Základní URL aplikace .{data-version:3.4.1} +------------------------------------------- + +Nette si zjišťuje adresu, na které aplikace běží, z HTTP požadavku, takže není potřeba nic nastavovat. Při spuštění z příkazové řádky (cron, úlohy, MCP server) ale žádný požadavek není a odvozené hodnoty jako `%baseUrl%`, odkazy z `LinkGenerator` nebo `$baseUrl` v šablonách by neměly odkud vzniknout. Pro tyto případy lze zadat základní URL aplikace: + +```neon +http: + # použije se jen tehdy, když nelze adresu zjistit z požadavku + baseUrl: https://example.com/ # (string) výchozí je nenastaveno +``` + +Hodnota se uplatní pouze tehdy, když z prostředí nevzejde žádný host. Na webu má vždy přednost skutečný požadavek, takže stejná konfigurace funguje na localhostu i v produkci. URL musí být absolutní, včetně případného podadresáře (`https://example.com/eshop`). + + Session ======= @@ -118,7 +146,7 @@ session: # zobrazit session panel v Tracy Bar? debugger: ... # (bool) výchozí je false - # doba neaktivity po které session vyexpiruje + # doba neaktivity, po které session vyexpiruje expiration: 14 days # (string) výchozí je '3 hours' # kdy se má startovat session? @@ -128,7 +156,7 @@ session: handler: @handlerService ``` -Volba `autoStart` řídí, kdy se má startovat session. Hodnota `always` znamená, že se session spustí vždy se spuštěním aplikace. Hodnota `smart` znamená, že session se spustí při startu aplikace pouze tehdy, pokud již existuje, nebo ve chvíli, z ní chceme číst nebo do ní zapisovat. A nakonec hodnota `never` zakazuje automatický start session. +Volba `autoStart` řídí, kdy se má startovat session. Hodnota `always` znamená, že se session spustí vždy se spuštěním aplikace. Hodnota `smart` znamená, že session se spustí při startu aplikace pouze tehdy, pokud již existuje, nebo ve chvíli, kdy z ní chceme číst nebo do ní zapisovat. A nakonec hodnota `never` zakazuje automatický start session. Dále lze nastavovat všechny PHP [session direktivy |https://www.php.net/manual/en/session.configuration.php] (ve formátu camelCase) a také [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Příklad: @@ -145,7 +173,7 @@ session: Session cookie -------------- -Session cookie se odesílá se stejnými parametry jako [jiné cookie |#HTTP cookie], ale tyto můžete pro ni změnit: +Session cookie se odesílá se stejnými parametry jako [jiné cookie |#HTTP cookie], ale pro ni je můžete změnit: ```neon session: @@ -169,3 +197,4 @@ Tyto služby se přidávají do DI kontejneru: | `http.request` | [api:Nette\Http\Request] | [HTTP request| request] | `http.response` | [api:Nette\Http\Response] | [HTTP response| response] | `session.session` | [api:Nette\Http\Session] | [správa session| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | továrna vytvářející HTTP request diff --git a/http/cs/request.texy b/http/cs/request.texy index 5dc90de308..db77757b78 100644 --- a/http/cs/request.texy +++ b/http/cs/request.texy @@ -29,7 +29,7 @@ Vrací URL požadavku jako objekt [UrlScript |urls#UrlScript]. ```php $url = $httpRequest->getUrl(); echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org +echo $url->getHost(); // doc.nette.org ``` Upozornění: prohlížeče neodesílají na server fragment, takže `$url->getFragment()` bude vracet prázdný řetězec. @@ -55,8 +55,8 @@ $id = $httpRequest->getPost('id'); // vrací POST parametr 'id' (nebo null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- Vrací [upload |#Uploadované soubory] jako objekt [api:Nette\Http\FileUpload]: ```php @@ -70,11 +70,11 @@ if ($file?->hasFile()) { // byl nějaký soubor nahraný? Pro přístup do zanořené struktury uveďte pole klíčů. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Protože nelze důvěřovat datům zvenčí a tedy ani spoléhat na podobu struktury souborů, je bezpečnější tento způsob než třeba `$request->getFiles()['my-form']['details']['avatar']`, který může selhat. +Protože nelze důvěřovat datům zvenčí, a tedy ani spoléhat na podobu struktury souborů, je tento způsob bezpečnější než třeba `$request->getFiles()['my-form']['details']['avatar']`, který může selhat. getFiles(): array .[method] @@ -86,8 +86,8 @@ $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- +getCookie(string $key): ?string .[method] +----------------------------------------- Vrací cookie nebo `null`, když neexistuje. ```php @@ -131,13 +131,13 @@ $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Vrací všechny HTTP hlavičky jako asociativní pole. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Vrací všechny HTTP hlavičky jako asociativní pole. Klíče jsou převedeny na malá písmena. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` @@ -146,9 +146,41 @@ isSecured(): bool .[method] Je spojení šifrované (HTTPS)? Pro správnou funkčnost může být potřeba [nastavit proxy |configuration#HTTP proxy]. -isSameSite(): bool .[method] ----------------------------- -Přichází požadavek ze stejné (sub)domény a je iniciován kliknutím na odkaz? Nette k detekci používá cookie `_nss` (dříve `nette-samesite`). +isSameSite(): bool .[method deprecated] +--------------------------------------- +Přišel požadavek ze stejné stránky (same-site)? Od verze 3.4 ji nahrazuje schopnější metoda [isFrom() |#isFrom]. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Řekne vám, odkud požadavek přišel a jak ho prohlížeč vytvořil, na základě hlaviček `Sec-Fetch-*` (tzv. [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]), které prohlížeč nastavuje sám a stránka běžící v prohlížeči oběti je nedokáže zfalšovat ani odstranit. Nette ji interně používá k automatické ochraně formulářů a signálů před útoky [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). Hodí se, když chcete ochránit vlastní citlivé akce, jako jsou API endpointy nebo destruktivní odkazy. + +Metoda vrátí `true` pouze tehdy, když požadavek splňuje **všechny** zadané podmínky. První parametr `$site` popisuje vztah mezi stránkou, která požadavek vyvolala, a vaším webem (hlavička `Sec-Fetch-Site`). Přijímá jednu hodnotu nebo pole těchto hodnot výčtu `FetchSite`: + +- `FetchSite::SameOrigin` - ze zcela stejného původu (schéma, host i port) +- `FetchSite::SameSite` - ze stejného webu, případně z jiné subdomény +- `FetchSite::CrossSite` - z cizího webu +- `FetchSite::None` - uživatel jej vyvolal přímo, např. zadáním URL nebo otevřením záložky + +```php +// přišel požadavek z našich vlastních stránek? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // akci zablokujeme +} +``` + +Volitelný parametr `$dest` (hlavička `Sec-Fetch-Dest`) udává, jaký druh zdroje prohlížeč načítá, např. `FetchDest::Document` pro navigaci na stránku nebo `FetchDest::Empty` pro požadavek z JavaScriptu. Volitelný parametr `$user` (hlavička `Sec-Fetch-User`) říká, zda navigaci vyvolala skutečná akce uživatele, jako kliknutí na odkaz či odeslání formuláře; hodnotou `true` ji vyžadujete. + +Kontrola, že je akce dostupná pouze z vlastních stránek a jen skutečnou akcí uživatele, pak vypadá takto: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Starší prohlížeče (Safari před 16.4) hlavičky `Sec-Fetch-*` neposílají. Pro ně Nette použije záložně cookie `SameSite=Strict`, která dokazuje pouze to, že požadavek není cross-site. Kontrolu, která navíc vyžaduje `$dest` nebo `$user`, tak nelze tímto způsobem ověřit a v těchto prohlížečích vrátí `false` - pokud je to příliš striktní, testujte jen `$site`. isAjax(): bool .[method] @@ -163,7 +195,7 @@ Vrací IP adresu uživatele. Pro správnou funkčnost může být potřeba [nast getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Vrací DNS překlad IP adresy uživatele. Pro správnou funkčnost může být potřeba [nastavit proxy |configuration#HTTP proxy]. +Zastaralá, vždy vrací `null`. Reverzní DNS překlad byl pomalý a nespolehlivý; pokud hostname potřebujete, přeložte si ho sami z [getRemoteAddress() |#getRemoteAddress]. getBasicCredentials(): ?array .[method] @@ -184,9 +216,33 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Vrací origin, ze kterého požadavek přišel. Origin se skládá z protokolu, hostname a portu - například `https://example.com:8080`. Vrací `null`, pokud hlavička origin není přítomna nebo je nastavena na `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Prohlížeč posílá hlavičku `Origin` v následujících případech: +- Požadavky mezi doménami (AJAX volání na jinou doménu) +- POST, PUT, DELETE a další modifikující požadavky +- Požadavky provedené pomocí Fetch API + +Prohlížeč NEPOSÍLÁ hlavičku `Origin` při: +- Běžných GET požadavcích na stejnou doménu (navigace v rámci téže domény) +- Přímé navigaci zadáním URL do adresního řádku +- Požadavcích z jiných klientů než prohlížeče + +.[note] +Na rozdíl od hlavičky `Referer` obsahuje `Origin` pouze schéma, host a port - nikoli celou cestu URL. To ji činí vhodnější pro bezpečnostní kontroly při zachování soukromí uživatele. Hlavička `Origin` se primárně používá pro validaci [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing). + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Detekuje jazyk. Jako parametr `$lang` předáme pole s jazyky, které aplikace podporuje, a ona vrátí ten, který by viděl návštěvníkův prohlížeč nejraději. Nejsou to žádná kouzla, jen se využívá hlavičky `Accept-Language`. Pokud nedojde k žádné shodě, vrací `null`. +Detekuje jazyk. Jako parametr `$langs` předáme pole s jazyky, které aplikace podporuje, a ona vrátí ten, který by viděl návštěvníkův prohlížeč nejraději. Nejsou to žádná kouzla, jen se využívá hlavičky `Accept-Language`. Pokud nedojde k žádné shodě, vrací `null`. ```php // prohlížeč odesílá např. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 @@ -212,6 +268,7 @@ RequestFactory lze před zavoláním `fromGlobals()` konfigurovat: - metodou `$factory->setBinary()` vypnete automatické čištění vstupních parametrů od kontrolních znaků a neplatných UTF-8 sekvencí. - metodou `$factory->setProxy(...)` uvedete IP adresu [proxy serveru |configuration#HTTP proxy], což je nezbytné pro správnou detekci IP adresy uživatele. +- metodou `$factory->setForceHttps()` .{data-version:3.3.4} vynutíte HTTPS schéma požadavku bez ohledu na prostředí serveru. RequestFactory umožňuje definovat filtry, které automaticky transformují části URL požadavku. Tyto filtry odstraňují nežádoucí znaky z URL, které tam mohou být vloženy například nesprávnou implementací komentářových systémů na různých webech: @@ -234,7 +291,7 @@ Uploadované soubory Metoda `Nette\Http\Request::getFiles()` vrací pole všech uploadů v normalizované struktuře, jejíž listy jsou objekty [api:Nette\Http\FileUpload]. Ty zapouzdřují data odeslaná formulářovým prvkem `<input type=file>`. -Struktura reflektuje pojmenování prvků v HTML. V nejjednodušším případě to může být jediný pojmenovaný element formuláře odeslaný jako: +Struktura odpovídá pojmenování prvků v HTML. V nejjednodušším případě to může být jediný pojmenovaný element formuláře odeslaný jako: ```latte <input type="file" name="avatar"> @@ -248,7 +305,7 @@ V tomto případě `$request->getFiles()` vrací pole: ] ``` -Objekt `FileUpload` se vytvoří i v případě, že uživatel žádný soubor neodeslal nebo odeslání selhalo. Jestli byl soubor odeslán vrací metoda `hasFile()`: +Objekt `FileUpload` se vytvoří i v případě, že uživatel žádný soubor neodeslal nebo odeslání selhalo. Jestli byl soubor odeslán, zjistíte metodou `hasFile()`: ```php $request->getFile('avatar')?->hasFile(); @@ -303,7 +360,7 @@ if ($file instanceof FileUpload) { } ``` -Protože nelze důvěřovat datům zvenčí a tedy ani spoléhat na podobu struktury souborů, je bezpečnější tento způsob než třeba `$request->getFiles()['my-form']['details']['avatars'][1]`, který může selhat. +Protože nelze důvěřovat datům zvenčí, a tedy ani spoléhat na podobu struktury souborů, je tento způsob bezpečnější než třeba `$request->getFiles()['my-form']['details']['avatars'][1]`, který může selhat. Přehled metod `FileUpload` .{toc: FileUpload} @@ -341,7 +398,7 @@ Vrací obsah uploadovaného souboru. V případě, že upload nebyl úspěšný, getContentType(): ?string .[method] ----------------------------------- -Detekuje MIME content type uploadovaného souboru na základě jeho signatury. V případě, že upload nebyl úspěšný nebo detekce se nezdařila, vrací `null`. +Detekuje MIME content type uploadovaného souboru na základě jeho signatury. V případě, že upload nebyl úspěšný nebo se detekce nezdařila, vrací `null`. .[caution] Vyžaduje PHP rozšíření `fileinfo`. @@ -386,12 +443,17 @@ Vrací velikost uploadovaného souboru. V případě, že upload nebyl úspěšn getTemporaryFile(): string .[method] ------------------------------------ -Vrací cestu k dočasné lokaci uploadovaného souboru. V případě, že upload nebyl úspěšný, vrací `''`. +Vrací cestu k dočasnému umístění uploadovaného souboru. V případě, že upload nebyl úspěšný, vrací `''`. + + +__toString(): string .[method] +------------------------------ +Vrací cestu k dočasnému umístění nahraného souboru. To umožňuje objekt `FileUpload` použít přímo jako řetězec. isImage(): bool .[method] ------------------------- -Vrací `true`, pokud nahraný soubor je obrázek ve formátu JPEG, PNG, GIF, WebP nebo AVIF. Detekce probíhá na základě jeho signatury a neověřuje se integrita celého souboru. Zda není obrázek poškozený lze zjistit například pokusem o jeho [načtení |#toImage]. +Vrací `true`, pokud nahraný soubor je obrázek ve formátu JPEG, PNG, GIF, WebP nebo AVIF. Detekce probíhá na základě jeho signatury a neověřuje se integrita celého souboru. Zda není obrázek poškozený, lze zjistit například pokusem o jeho [načtení |#toImage]. .[caution] Vyžaduje PHP rozšíření `fileinfo`. @@ -404,4 +466,4 @@ Vrací dvojici `[šířka, výška]` s rozměry uploadovaného obrázku. V pří toImage(): Nette\Utils\Image .[method] -------------------------------------- -Načte obrázek jak objekt [Image|utils:images]. V případě, že upload nebyl úspěšný nebo nejde o platný obrázek, vyhodí výjimku `Nette\Utils\ImageException`. +Načte obrázek jako objekt [Image|utils:images]. V případě, že upload nebyl úspěšný nebo nejde o platný obrázek, vyhodí výjimku `Nette\Utils\ImageException`. diff --git a/http/cs/response.texy b/http/cs/response.texy index baea11f900..c83eb952b1 100644 --- a/http/cs/response.texy +++ b/http/cs/response.texy @@ -12,7 +12,7 @@ HTTP odpověď představuje objekt [api:Nette\Http\Response]. Pokud pracujete s Nette\Http\Response =================== -Objekt je na rozdíl od [Nette\Http\Request|request] mutable, tedy pomocí setterů můžete měnit stav, tedy např. odesílat hlavičky. Nezapomeňte, že všechny settery musí být volány **před odesláním jakéhokoli výstupu.** Jestli už byl výstup odeslán prozradí metoda `isSent()`. Pokud vrací `true`, každý pokus o odeslání hlavičky vyvolá výjimku `Nette\InvalidStateException`. +Objekt je na rozdíl od [Nette\Http\Request|request] mutable, tedy pomocí setterů můžete měnit stav, tedy např. odesílat hlavičky. Nezapomeňte, že všechny settery musí být volány **před odesláním jakéhokoli výstupu.** Jestli už byl výstup odeslán, prozradí metoda `isSent()`. Pokud vrací `true`, každý pokus o odeslání hlavičky vyvolá výjimku `Nette\InvalidStateException`. setCode(int $code, ?string $reason=null) .[method] @@ -34,9 +34,9 @@ isSent(): bool .[method] Vrací, zda už došlo k odeslání hlaviček ze serveru do prohlížeče, a tedy již není možné odesílat hlavičky či měnit stavový kód. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Odešle HTTP hlavičku a **přepíše** dříve odeslanou hlavičkou stejného jména. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Odešle HTTP hlavičku a **přepíše** dříve odeslanou hlavičku stejného jména. Pokud je `$value` `null`, bude hlavička odstraněna. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -Odešle HTTP hlavičku a **nepřepíše** dříve odeslanou hlavičkou stejného jména. +Odešle HTTP hlavičku a **nepřepíše** dříve odeslanou hlavičku stejného jména. ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -67,8 +67,8 @@ $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ +getHeaders(): array<string, string> .[method] +--------------------------------------------- Vrací všechny odeslané HTTP hlavičky jako asociativní pole. ```php @@ -96,8 +96,8 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- +setExpiration(?string $expire) .[method] +---------------------------------------- Nastaví expiraci HTTP dokumentu pomocí hlaviček `Cache-Control` a `Expires`. Parametrem je buď časový interval (jako text), nebo `null`, což zakáže kešování. ```php @@ -115,27 +115,36 @@ $httpResponse->sendAsFile('faktura.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Odešle cookie. Výchozí hodnoty parametrů: -| `$path` | `'/'` | cookie má dosah na všechny cesty v (sub)doméně *(konfigurovatelné)* -| `$domain` | `null` | což znamená s dosahem na aktuální (sub)doménu, ale nikoliv její subdomény *(konfigurovatelné)* -| `$secure` | `true` | pokud web běží na HTTPS, jinak `false` *(konfigurovatelné)* -| `$httpOnly` | `true` | cookie je pro JavaScript nepřístupná -| `$sameSite` | `'Lax'` | cookie nemusí být odeslána při [přístupu z jiné domény |nette:glossary#SameSite cookie] +| `$path` | `'/'` | cookie má dosah na všechny cesty v (sub)doméně *(konfigurovatelné)* +| `$domain` | `null` | což znamená s dosahem na aktuální (sub)doménu, ale nikoliv její subdomény *(konfigurovatelné)* +| `$secure` | `auto` | `true`, pokud web běží na HTTPS, jinak `false` (výchozí nastavení frameworku; samotná třída má výchozí `false`) *(konfigurovatelné)* +| `$httpOnly` | `true` | cookie je pro JavaScript nepřístupná +| `$sameSite` | `'Lax'` | cookie nemusí být odeslána při [přístupu z jiné domény |nette:glossary#SameSite cookie] +| `$partitioned` | `false` | zda je cookie partitioned, viz níže *(od verze 3.4)* Výchozí hodnoty parametrů `$path`, `$domain` a `$secure` můžete změnit v [konfiguraci |configuration#HTTP cookie]. -Čas lze uvádět jako počet sekund nebo řetězec: +Expiraci lze uvést jako počet sekund, jako textový interval nebo datum, případně jako objekt `DateTimeInterface`. Hodnota `null` vytvoří session cookie, kterou prohlížeč zahodí při svém zavření. Nette expiraci odesílá v atributu `Expires` i `Max-Age`. ```php -$httpResponse->setCookie('lang', 'cs', '100 days'); +$httpResponse->setCookie('lang', 'cs', '100 days'); // vyprší za 100 dní +$httpResponse->setCookie('lang', 'cs', null); // session cookie ``` Parametr `$domain` určuje, které domény mohou cookie přijímat. Není-li uveden, cookie přijímá stejná (sub)doména, jako ji nastavila, ale nikoliv její subdomény. Pokud je `$domain` zadaný, jsou zahrnuty i subdomény. Proto je uvedení `$domain` méně omezující než vynechání. Například při `$domain = 'nette.org'` jsou cookies dostupné i na všech subdoménách jako `doc.nette.org`. -Pro hodnotu `$sameSite` můžete použít konstanty `Response::SameSiteLax`, `SameSiteStrict` a `SameSiteNone`. +Hodnotu `$sameSite` můžete předat jako enum `Nette\Http\SameSite` - `SameSite::Lax`, `SameSite::Strict` nebo `SameSite::None` (fungují i řetězce `'Lax'`, `'Strict'`, `'None'`). Pokud ji nastavíte na `SameSite::None`, automaticky se zapne atribut `$secure`, protože prohlížeče cookie se `SameSite=None` bez něj odmítají. + +.{data-version:3.4.0} +Partitioned cookies (CHIPS) dávají cookie samostatné úložiště pro každý web nejvyšší úrovně. Když tedy služba třetí strany (například vložený widget) nastaví partitioned cookie, prohlížeč pro každý web, na kterém se widget objeví, uchovává oddělenou kopii a tyto kopie nelze vzájemně propojit pro sledování napříč weby. Zapnete je nastavením `$partitioned` na `true`; to zároveň vyžaduje atribut `$secure`, takže se zapne automaticky. + +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] @@ -148,3 +157,28 @@ Smaže cookie. Výchozí hodnoty parametrů jsou: ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +Objekt [api:Nette\Http\Context] spojuje dohromady požadavek a odpověď a pomáhá s HTTP kešováním. Není registrován jako služba, vytvoříte si jej sami. V presenterech je obvykle pohodlnější použít metodu [lastModified() |application:presenters#HTTP kešování]; context se hodí ve chvíli, kdy odpověď odesíláte sami, například z vlastní třídy response. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Zjišťuje, zda se obsah od poslední návštěvy klienta změnil. Pokud předáte čas poslední změny, odešle hlavičku `Last-Modified`, pokud předáte ETag validátor (krátký řetězec identifikující aktuální verzi obsahu, například jeho hash), odešle hlavičku `ETag`. Obojí pak porovná s hlavičkami `If-Modified-Since` a `If-None-Match`, které poslal prohlížeč. + +Pokud už prohlížeč aktuální verzi má, metoda nastaví kód `304 Not Modified` a vrátí `false` - v takovém případě tělo odpovědi vůbec neodesílejte. V opačném případě vrátí `true`. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Oba parametry jsou volitelné. Pokud čas změny obsahu neznáte, použijte jen ETag, a naopak. diff --git a/http/cs/sessions.texy b/http/cs/sessions.texy index de10e7715f..a4fc0ba0ac 100644 --- a/http/cs/sessions.texy +++ b/http/cs/sessions.texy @@ -13,7 +13,7 @@ HTTP je bezestavový protokol, nicméně takřka každá aplikace potřebuje sta Při použití sessions každý uživatel obdrží jedinečný identifikátor nazývaný session ID, který se předává v cookie. Ten slouží jako klíč k session datům. Na rozdíl od cookies, které se uchovávají na straně prohlížeče, jsou data v session uchovávána na straně serveru. -Session nastavujeme v [konfiguraci |configuration#Session], důležitá je zejména volba doby exipirace. +Session nastavujeme v [konfiguraci |configuration#Session], důležitá je zejména volba doby expirace. Správu session má na starosti objekt [api:Nette\Http\Session], ke kterému se dostanete tak, že si jej necháte předat pomocí [dependency injection |dependency-injection:passing-dependencies]. V presenterech stačí jen zavolat `$session = $this->getSession()`. @@ -50,7 +50,7 @@ V presenteru stačí použít `getSession()` s parametrem: $section = $this->getSession('unikatni nazev'); ``` -Ověřit existenci sekce lze metodou `$session->hasSection('unikatni nazev')`. +Ověřit existenci sekce lze metodou `$session->hasSection('unikatni nazev')`. Seznam názvů všech existujících sekcí vrátí `$session->getSectionNames()`. Se samotnou sekcí se pak pracuje velmi snadno pomocí metod `set()`, `get()` a `remove()`: @@ -94,7 +94,7 @@ $section->set('flash', $message, '30 seconds'); .[note] Nezapomeňte, že doba expirace celé session (viz [konfigurace session |configuration#Session]) musí být stejná nebo vyšší než doba nastavená u jednotlivých sekcí či proměnných. -Zrušení dříve nastavené expirace docílíme metodou `removeExpiration()`. Okamžité zrušení celé sekce zajistí metoda `remove()`. +Zrušení dříve nastavené expirace docílíme metodou `removeExpiration()`; expiraci konkrétní proměnné zrušíte předáním jejího názvu: `removeExpiration('flash')`. Okamžité zrušení celé sekce zajistí metoda `remove()`. Události $onStart, $onBeforeWrite @@ -145,7 +145,7 @@ Obsahuje HTTP požadavek cookie se session ID? regenerateId(): void .[method] ------------------------------ -Vygeneruje nové náhodné session ID. Data zůstávají zachované. +Vygeneruje nové náhodné session ID. Data zůstávají zachována. getId(): string .[method] @@ -178,13 +178,13 @@ setOptions(array $options): static .[method] Konfiguruje session. Lze nastavovat všechny PHP [session direktivy |https://www.php.net/manual/en/session.configuration.php] (ve formátu camelCase, např. místo `session.save_path` zapíšeme `savePath`) a také [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Nastaví dobu neaktivity po které session vyexpiruje. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Nastaví dobu neaktivity, po které session vyexpiruje. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- Nastavení parametrů pro cookie. Výchozí hodnoty parametrů můžete změnit v [konfiguraci |configuration#Session cookie]. @@ -208,4 +208,4 @@ Server předpokládá, že komunikuje stále s tímtéž uživatelem, dokud pož Nette Framework proto správně nakonfiguruje PHP direktivy, aby session ID přenášel pouze v cookie, znepřístupnil jej JavaScriptu a případné identifikátory v URL ignoroval. Navíc v kritických chvílích, jako je třeba přihlášení uživatele, vygeneruje session ID nové. .[note] -Pro konfiguraci PHP se používá funkce ini_set, kterou bohužel některé hostingy zakazují. Pokud je to případ i vašeho hostéra, pokuste se s ním domluvit, aby vám funkci povolil nebo alespoň server nakonfiguroval. +Pro konfiguraci PHP se používá funkce ini_set, kterou bohužel některé hostingy zakazují. Pokud to platí i pro vašeho poskytovatele hostingu, pokuste se s ním domluvit, aby vám funkci povolil nebo alespoň server nakonfiguroval. diff --git a/http/cs/ssrf.texy b/http/cs/ssrf.texy new file mode 100644 index 0000000000..d1e583429f --- /dev/null +++ b/http/cs/ssrf.texy @@ -0,0 +1,183 @@ +Ochrana proti SSRF +****************** + +.[perex] +Když vaše aplikace stahuje URL zadanou uživatelem, může toho útočník zneužít a dostat se do vaší interní sítě. Třídy [#UrlValidator] a [#IPAddress] vám pomohou bránit se těmto útokům Server-Side Request Forgery (SSRF). + +→ [Instalace a požadavky |@home#Instalace] + + +Co je SSRF? +=========== + +Představte si funkci, kde uživatel zadá URL a váš server ji stáhne - avatar ze vzdálené adresy, cíl webhooku, náhled odkazu. Vypadá to neškodně, ale na adresu se připojuje server, ne prohlížeč uživatele. A server vidí místa, kam útočník nedosáhne: loopback rozhraní, privátní síť, cloudové služby. + +Útočník proto pošle URL, která místo na veřejný internet míří dovnitř. Typickými cíli jsou: + +- cloudová metadata na `http://169.254.169.254/`, která mohou prozradit přístupové klíče +- interní administrace a routery jako `http://192.168.1.1/` +- služby bez autentizace, například Redis na `http://localhost:6379/` + +Tato třída zranitelností je tak rozšířená, že patří mezi [OWASP Top 10 |https://owasp.org/Top10/]. Obranou je ověřit URL **dříve**, než ji stáhnete, a odmítnout vše, co se přeloží na neveřejnou adresu. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] ověřuje URL proti konfigurovatelné politice: schéma, port, host, userinfo a IP adresy, na které se host přeloží. Základní použití je jediné volání: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // nebezpečná URL, nestahujte ji +} +``` + +Výchozí politika je záměrně přísná - akceptuje pouze `https` na portu 443 mířící na veřejnou IP adresu. Vše ostatní (loopback, privátní rozsahy, link-local včetně cloudových metadat, rezervované rozsahy) je odmítnuto a multicast je odmítnut bezpodmínečně. To je správný výchozí bod pro stahování libovolných URL zadaných uživatelem. + + +Konfigurace politiky +-------------------- + +Politiku tvarujete přes konstruktor. Například chcete-li povolit prosté `http` na libovolném portu a dosáhnout na privátní adresy (užitečné uvnitř důvěryhodné sítě): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // libovolný port + allowPrivateIps: true, +); +``` + +Častým vzorem je omezit stahování na pevnou sadu partnerských domén pomocí allowlistu hostů. Prefix `*.` odpovídá libovolné hloubce subdomény, ale ne samotné doméně - pokud ji potřebujete, uveďte oba tvary: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +Kompletní sada možností konstruktoru: + +| Parametr | Výchozí | Význam +|--------------------- +| `schemes` | `['https']` | povolená schémata; `[]` odmítne vše +| `ports` | `[443]` | povolené porty, `null` = libovolný; implicitní port ze schématu je respektován +| `allowPrivateIps` | `false` | povolit privátní rozsahy (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | povolit loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | povolit link-local vč. cloudových metadat 169.254.169.254 +| `allowReserved` | `false` | povolit rozsahy rezervované IANA +| `allowUserinfo` | `false` | povolit `user:pass@` v URL +| `hostAllowlist` | `null` | pokud je nastaven, host musí odpovídat jednomu vzoru; `[]` odmítne vše +| `hostBlocklist` | `null` | pokud je nastaven, host nesmí odpovídat žádnému vzoru + + +Metody validace +--------------- + +Validátor nabízí tři metody. `allows()` provede plnou kontrolu včetně překladu DNS - host se přeloží a **každá** A/AAAA adresa musí projít IP politikou: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` přeskakuje překlad DNS a kontroly IP rozsahů. Použijte ji jako rychlý předfiltr, nebo když je validace DNS delegována na stahovací vrstvu: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Obě metody přijímají řetězec, objekt [UrlImmutable |urls#UrlImmutable] nebo `null` (což vždy selže). + + +Obrana proti DNS rebindingu +--------------------------- + +Mezi validací a stažením je záludný časový souběh (race condition): útočník může při ověřování hostu vrátit bezpečnou IP a poté pro samotné stažení přepnout DNS na interní IP. K uzavření této díry vrací `getResolvedIPs()` ověřené IP adresy a vy na ně připnete spojení, aby stahování nešlo přesměrovat jinam: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // nebezpečná URL +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... proveďte požadavek +``` + +Metoda vrací pole IP řetězců (nejprve A záznamy, poté AAAA), které prošly celou politikou, nebo prázdné pole při jakémkoli selhání. Pro IP literál v URL ověří adresu přímo a žádný překlad DNS neprovádí. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] je neměnný hodnotový objekt pro práci s IPv4 a IPv6 adresami. `UrlValidator` jej využívá interně, ale hodí se i samostatně, kdykoli adresy klasifikujete. Konstruktor vyhodí `Nette\InvalidArgumentException` u neplatné adresy: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +Když nechcete výjimku, použijte tovární metodu `tryFrom()` nebo kontrolu `isValid()`: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Klasifikace adres +----------------- + +Predikáty říkají, do jaké třídy adresa patří. Klíčový je `isPublic()` - pravdivý jen pro veřejně směrovatelné adresy, což je přesně to, co obrana proti SSRF potřebuje: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (rozsah cloudových metadat) +``` + +Kompletní sada predikátů: + +| Metoda | Testuje +|-------------------- +| `isPublic()` | veřejně směrovatelná (žádná z níže uvedených) +| `isPrivate()` | privátní rozsahy RFC 1918 / 4193 +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (vč. cloudových metadat), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | rezervováno IANA (dokumentace, CGNAT, budoucí použití, …) + + +Příslušnost k rozsahu +--------------------- + +`isInRange()` testuje, zda adresa spadá do CIDR bloku. Můžete předat síť s prefixem, nebo holou adresu pro přesnou shodu (implicitní /32 pro IPv4, /128 pro IPv6): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (přesná shoda) +``` + +Chybný vstup nebo jiná rodina IP vrací `false`. + + +IPv4-mapped IPv6 +---------------- + +Adresy zapsané jako IPv4-mapped IPv6 (například `::ffff:127.0.0.1`) jsou klasickým způsobem, jak proklouznout naivními filtry. `IPAddress` je normalizuje, takže predikáty rozsahů prohlédnou přestrojení: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +Metody `isIPv4()` a `isIPv6()` hlásí textový tvar: mapovaná adresa je IPv6, nikoli IPv4. diff --git a/http/cs/upgrading.texy b/http/cs/upgrading.texy new file mode 100644 index 0000000000..c1844ab163 --- /dev/null +++ b/http/cs/upgrading.texy @@ -0,0 +1,54 @@ +Upgrade +******* + + +Upgrade na verzi 3.4 +==================== + +Minimální požadovaná verze PHP je 8.3. + +- metoda `Request::isSameSite()` je zastaralá ve prospěch `isFrom()`, která původ požadavku zjišťuje z hlaviček `Sec-Fetch-*`. Automatická ochrana formulářů a signálů se tím zpřesní a mění se jedno chování: přímá navigace (záložka, ručně zadaná adresa, odkaz z e-mailu) se už za same-site nepovažuje. Pokud nějaký signál spoléhá na akční odkazy v e-mailech, označte jej `#[Requires(sameOrigin: false)]`. +- cookie `_nss` se nyní posílá pouze prohlížečům, které neposílají hlavičku `Sec-Fetch-Site` +- `setCookie()` posílá atribut `Max-Age` a pro `SameSite=None` a partitioned cookie vynutí příznak `Secure` +- enum `SameSite` nahrazuje konstanty `IResponse::SameSiteLax` apod., které jsou zastaralé +- expirace se všude vykládá shodně: číslo je relativní počet sekund, text je interval nebo datum. Předávání absolutního UNIX timestampu je zastaralé a session cookie představuje hodnota `null` místo `0`. +- zastaralá metoda `Request::getRemoteHost()` vrací `null` +- dávno zastaralá třída `Nette\Http\UserStorage` byla odstraněna + +Celý příběh přechodu na hlavičky `Sec-Fetch-*` vypráví článek [Čtvrt století s CSRF |https://blog.nette.org/cs/csrf-konecne-resi-prohlizec]. + + +Upgrade na verzi 3.2 +==================== + +- přihlašovací údaje z HTTP Basic Authentication už nejsou součástí objektu `Url`, takže `$url->getUser()` a `$url->getPassword()` vracejí prázdný řetězec. Přečtete je novou metodou `$request->getBasicCredentials()`. + +Důvody této změny vysvětluje článek [Nette Http 3.2: změna přístupu ke credentials |https://blog.nette.org/cs/nette-http-3-2-zmena-pristupu-ke-credentials]. + + +Upgrade na verzi 3.1 +==================== + +- cookie jsou odesílány s příznakem `sameSite: Lax` +- `cookieSecure` je nyní ve výchozím nastavení 'auto' +- volba `session.cookieSecure` je zastaralá, místo ní se používá `http.cookieSecure` +- cookie `nette-samesite` přejmenována na `_nss` +- `Nette\Http\Request::getFile()` přijímá pole klíčů a vrací `FileUpload|null` +- `Nette\Http\Session::getCookieParameters()` je zastaralá +- `Nette\Http\FileUpload::getName()` přejmenována na `getUntrustedName()` +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()` a `getRelativeUrl()` jsou zastaralé (tyto metody jsou součástí `UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` je zastaralé +- `Nette\Http\FileUpload::getImageSize()` vrací dvojici `[width, height]` +- při `autoStart: smart` (výchozí hodnota) se session už nestartuje hned po startu aplikace jen proto, že prohlížeč poslal session cookie; nastartuje se až při prvním čtení nebo zápisu. Přibyly hodnoty `always` a `never`. +- pokud prohlížeč pošle session ID, kterému neodpovídá žádná session, Nette cookie smaže místo toho, aby zakládalo novou session +- pro přístup k sekcím session preferujte metody `set()`, `get()` a `remove()`; na rozdíl od přístupu k proměnným správně rozlišují čtení od zápisu a zbytečně nestartují session +- výchozí hodnoty `cookiePath` a `cookieDomain` lze nastavit v konfiguraci + +Chování session podrobně popisuje článek [Nette Http 3.1: mnohem chytřejší sessions |https://blog.nette.org/cs/nette-http-3-1-mnohem-chytrejsi-sessions]. + + +Upgrade na verzi 3.0 +==================== + +- objekt `Nette\Http\UrlScript` (který vrací třeba `Nette\Http\Request::getUrl()`) je nyní immutable +- v zápisu `new Nette\Http\Url('abcd')` představuje `abcd` cestu, nikoliv doménu; od verze 3.0 tedy `(new Nette\Http\Url('abcd'))->setScheme('http')` korektně vygeneruje `http:abcd` místo dřívějšího `http://abcd` diff --git a/http/cs/urls.texy b/http/cs/urls.texy index 9e722a9d7c..a959ba5afd 100644 --- a/http/cs/urls.texy +++ b/http/cs/urls.texy @@ -10,7 +10,7 @@ Třídy [#Url], [#UrlImmutable] a [#UrlScript] umožňují snadné generování, Url === -Třída [api:Nette\Http\Url] umožňuje snadno pracovat s URL a jednotlivými jeho komponentami, které zachycuje tento nákres: +Třída [api:Nette\Http\Url] umožňuje snadno pracovat s URL a jejími jednotlivými komponentami, které zachycuje tento nákres: /--pre scheme user password host port path query fragment @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -Lze také URL naparsovat a dále s ním manipulovat: +Lze také URL naparsovat a dále s ní manipulovat: ```php $url = new Url( @@ -52,8 +52,8 @@ echo json_encode([$url]); ``` -Komponenty URL .[method] ------------------------- +Komponenty URL +-------------- Pro vrácení nebo změnu jednotlivých komponent URL jsou vám k dispozici tyto metody: @@ -73,6 +73,8 @@ Pro vrácení nebo změnu jednotlivých komponent URL jsou vám k dispozici tyto | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | celá URL +Metody `getUser()`, `getPassword()`, `setUser()` a `setPassword()` jsou zastaralé, protože vkládání přihlašovacích údajů přímo do URL se nedoporučuje. + Upozornění: Když pracujete s URL, které je získáno z [HTTP requestu|request], mějte na paměti, že nebude obsahovat fragment, protože prohlížeč jej neodesílá na server. Můžeme pracovat i s jednotlivými query parametry pomocí: @@ -82,11 +84,12 @@ Můžeme pracovat i s jednotlivými query parametry pomocí: |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] ------------------------------------------- -Vrací pravou či levou část hostitele. Takto funguje, pokud host je `www.nette.org`: +Vrací pravou či levou část hostitele. Takto funguje, pokud je host `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -98,8 +101,8 @@ Vrací pravou či levou část hostitele. Takto funguje, pokud host je `www.nett | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Ověří, zda jsou dvě URL shodné. ```php @@ -107,6 +110,11 @@ $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Převede URL do kanonického tvaru. Převede hostname na malá písmena a normalizuje cestu (percent-encoding a odstranění nadbytečných znaků). Query string zůstává beze změny. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- Ověřuje, zda je URL absolutní. URL je považována za absolutní, pokud začíná schématem (např. http, https, ftp) následovaným dvojtečkou. @@ -137,15 +145,15 @@ Třída [api:Nette\Http\UrlImmutable] je immutable (neměnnou) alternativou tř use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/cs/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); + ->withHost('example.com') + ->withPath('/cs/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/cs/?name=value#footer' ``` Třída `UrlImmutable` implementuje rozhraní `JsonSerializable` a má metodu `__toString()`, takže objekt lze vypsat nebo použít v datech předávaných do `json_encode()`. @@ -156,8 +164,8 @@ echo json_encode([$url]); ``` -Komponenty URL .[method] ------------------------- +Komponenty URL +-------------- Pro vrácení nebo změnu jednotlivých komponent URL slouží metody: @@ -177,7 +185,7 @@ Pro vrácení nebo změnu jednotlivých komponent URL slouží metody: | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | celá URL -Metoda `withoutUserInfo()` odstraňuje `user` a `password`. +Metody `getUser()`, `getPassword()`, `withUser()`, `withPassword()` a `withoutUserInfo()` jsou zastaralé, protože vkládání přihlašovacích údajů přímo do URL se nedoporučuje. Můžeme pracovat i s jednotlivými query parametry pomocí: @@ -190,7 +198,7 @@ Můžeme pracovat i s jednotlivými query parametry pomocí: getDomain(int $level = 2): string .[method] ------------------------------------------- -Vrací pravou či levou část hostitele. Takto funguje, pokud host je `www.nette.org`: +Vrací pravou či levou část hostitele. Takto funguje, pokud je host `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -218,8 +226,8 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Ověří, zda jsou dvě URL shodné. ```php @@ -230,7 +238,7 @@ $url->isEqual('https://nette.org'); UrlScript ========= -Třída [api:Nette\Http\UrlScript] je potomkem [#UrlImmutable] a rozšiřuje jej o další virtuální komponenty URL, jako je kořenový adresáři projektu apod. Stejně jako rodičovská třída je immutable (neměnným) objektem. +Třída [api:Nette\Http\UrlScript] je potomkem [#UrlImmutable] a rozšiřuje ji o další virtuální komponenty URL, jako je kořenový adresář projektu apod. Stejně jako rodičovská třída je immutable (neměnným) objektem. Následující diagram zobrazuje komponenty, které UrlScript rozpoznává: @@ -248,8 +256,8 @@ Následující diagram zobrazuje komponenty, které UrlScript rozpoznává: - `basePath` je část cesty ke kořenovému adresáři aplikace - `scriptPath` je cesta k aktuálnímu skriptu - `relativePath` je název skriptu (případně další segmenty cesty) relativní k basePath -- `relativeUrl` je celá část URL za baseUrl, včetně query string a fragmentu. -- `pathInfo` dnes už málo využívaná část URL za názvem skriptu +- `relativeUrl` je celá část URL za baseUrl, včetně query string a fragmentu +- `pathInfo` je dnes už málo využívaná část URL za názvem skriptu Pro vrácení částí URL jsou k dispozici metody: @@ -259,8 +267,8 @@ Pro vrácení částí URL jsou k dispozici metody: | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Objekty `UrlScript` obvykle přímo nevytváříme, ale vrací jej metoda [Nette\Http\Request::getUrl()|request] s již správně nastavenými komponentami pro aktuální HTTP požadavek. +Objekty `UrlScript` obvykle přímo nevytváříme, ale vrací je metoda [Nette\Http\Request::getUrl()|request] s již správně nastavenými komponentami pro aktuální HTTP požadavek. diff --git a/http/de/@home.texy b/http/de/@home.texy index 17f55bae4f..a9167933f5 100644 --- a/http/de/@home.texy +++ b/http/de/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -Das Paket `nette/http` kapselt den [HTTP-Request|request] & die [Response|response], die Arbeit mit [Sessions] und das [Parsen und Zusammensetzen von URLs |urls]. +Das Paket `nette/http` ist Ihr Begleiter für die gesamte HTTP-Kommunikation. Es bietet eine klare objektorientierte API über den eingehenden Request und die ausgehende Response, vereinfacht die Arbeit mit Sessions und URL-Adressen und kümmert sich obendrein um die Sicherheit. Das finden Sie hier: + +| [HTTP-Request |request] | eingehender Request und Bereinigung der Eingaben +| [HTTP-Response |response] | ausgehende Response, Header und Cookies +| [Sessions |sessions] | sichere Bewahrung des Zustands zwischen Requests +| [URL-Werkzeuge |urls] | Parsen und Zusammensetzen von URL-Adressen +| [SSRF-Schutz |ssrf] | Abwehr von Server-Side-Request-Forgery-Angriffen +| [Konfiguration |configuration] | Konfigurationsoptionen des Pakets Installation ------------ -Sie können die Bibliothek mit dem Werkzeug [Composer|best-practices:composer] herunterladen und installieren: +Das Paket laden und installieren Sie mit [Composer |best-practices:composer]: ```shell composer require nette/http ``` + +Das Paket benötigt PHP in einer Version von 8.3 bis 8.5. diff --git a/http/de/@left-menu.texy b/http/de/@left-menu.texy index 28a6ce308f..5199f4f4a7 100644 --- a/http/de/@left-menu.texy +++ b/http/de/@left-menu.texy @@ -1,8 +1,19 @@ Nette HTTP ********** -- [Einführung |@home] +- [Übersicht |@home] - [HTTP-Request |request] - [HTTP-Response |response] -- [Sessions |Sessions] -- [URL-Dienstprogramme |urls] -- [Konfiguration |configuration] +- [Sessions|sessions] +- [Arbeit mit URLs |urls] +- [SSRF-Schutz |ssrf] +- [Konfiguration|configuration] +- [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/http/de/configuration.texy b/http/de/configuration.texy index 352519fa2f..205da0d85c 100644 --- a/http/de/configuration.texy +++ b/http/de/configuration.texy @@ -4,7 +4,7 @@ HTTP-Konfiguration .[perex] Übersicht der Konfigurationsoptionen für Nette HTTP. -Wenn Sie nicht das gesamte Framework, sondern nur diese Bibliothek verwenden, lesen Sie, [wie die Konfiguration geladen wird|bootstrap:]. +Wenn Sie nicht das gesamte Framework, sondern nur diese Bibliothek verwenden, lesen Sie, [wie man die Konfiguration lädt |bootstrap:]. HTTP-Header @@ -12,29 +12,31 @@ HTTP-Header ```neon http: - # Header, die mit jeder Anfrage gesendet werden + # Header, die mit jeder Response gesendet werden headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' - # beeinflusst den X-Frame-Options Header - frames: ... # (string|bool) Standard ist 'SAMEORIGIN' + # beeinflusst den Header X-Frame-Options + frames: ... # (string|bool|null) Standardwert ist 'SAMEORIGIN' ``` -Das Framework sendet aus Sicherheitsgründen den Header `X-Frame-Options: SAMEORIGIN`, der besagt, dass die Seite nur dann innerhalb einer anderen Seite (im `<iframe>`-Element) angezeigt werden kann, wenn sie sich auf derselben Domain befindet. Dies kann in einigen Situationen unerwünscht sein (z. B. wenn Sie eine Anwendung für Facebook entwickeln), das Verhalten kann daher durch Setzen von `frames: http://allowed-host.com` oder `frames: true` geändert werden. +Aus Sicherheitsgründen sendet das Framework den Header `X-Frame-Options: SAMEORIGIN`, der besagt, dass eine Seite nur dann innerhalb einer anderen Seite (in einem `<iframe>`-Element) angezeigt werden darf, wenn sie auf derselben Domain liegt. In bestimmten Situationen kann das unerwünscht sein (etwa wenn Sie eine Facebook-Anwendung entwickeln), das Verhalten lässt sich also ändern: `frames: http://allowed-host.com` erlaubt einen bestimmten Host, `frames: true` erlaubt das Einbetten von überall (der Header entfällt) und `frames: false` verbietet es vollständig (`X-Frame-Options: DENY`). + +Standardmäßig sendet Nette außerdem die Header `X-Powered-By: Nette Framework 3` und `Content-Type: text/html; charset=utf-8`. Jeden Header, auch diese Standardheader, können Sie entfernen, indem Sie seinen Wert auf einen leeren String setzen. Content Security Policy ----------------------- -Die Header `Content-Security-Policy` (im Folgenden CSP) können einfach zusammengestellt werden, ihre Beschreibung finden Sie in der [CSP-Beschreibung |https://content-security-policy.com]. CSP-Direktiven (wie z. B. `script-src`) können entweder als Strings gemäß der Spezifikation oder als Arrays von Werten zur besseren Lesbarkeit geschrieben werden. Dann ist es nicht notwendig, Schlüsselwörter wie `'self'` in Anführungszeichen zu setzen. Nette generiert auch automatisch den `nonce`-Wert, sodass im Header zum Beispiel `'nonce-y4PopTLM=='` steht. +Die Header `Content-Security-Policy` (CSP) lassen sich leicht konfigurieren; ihre Beschreibung finden Sie in der [CSP-Spezifikation |https://content-security-policy.com]. CSP-Direktiven (etwa `script-src`) können entweder als Strings gemäß der Spezifikation oder für die bessere Lesbarkeit als Arrays von Werten geschrieben werden. Dann müssen um Schlüsselwörter wie `'self'` keine Anführungszeichen gesetzt werden. Nette erzeugt außerdem automatisch einen `nonce`-Wert, sodass im Header etwa `'nonce-y4PopTLM=='` gesendet wird. ```neon http: # Content Security Policy csp: - # String im Format gemäß der CSP-Spezifikation + # String gemäß der CSP-Spezifikation default-src: "'self' https://example.com" # Array von Werten @@ -44,14 +46,14 @@ http: - self - https://example.com - # bool im Falle von Schaltern + # bool im Fall von Schaltern upgrade-insecure-requests: true block-all-mixed-content: false ``` -Verwenden Sie in Templates `<script n:nonce>...</script>` und der Nonce-Wert wird automatisch ergänzt. Sichere Websites in Nette zu erstellen ist wirklich einfach. +Verwenden Sie in Templates `<script n:nonce>...</script>`, und der nonce-Wert wird automatisch eingesetzt. Sichere Websites in Nette zu bauen ist wirklich einfach. -Ähnlich können auch die Header `Content-Security-Policy-Report-Only` (die parallel zu CSP verwendet werden können) und [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] zusammengestellt werden: +Ähnlich lassen sich die Header `Content-Security-Policy-Report-Only` (die parallel zu CSP verwendet werden können) und die [Feature Policy |https://developers.google.com/web/updates/2018/06/feature-policy] konfigurieren: ```neon http: @@ -72,72 +74,84 @@ http: HTTP-Cookie ----------- -Die Standardwerte einiger Parameter der Methode [Nette\Http\Response::setCookie() |response#setCookie] und der Session können geändert werden. +Sie können die Standardwerte einiger Parameter der Methode [Nette\Http\Response::setCookie() |response#setCookie()] und der Session-Behandlung ändern. ```neon http: - # Cookie-Gültigkeitsbereich nach Pfad - cookiePath: ... # (string) Standard ist '/' + # Gültigkeitsbereich des Cookies nach Pfad + cookiePath: ... # (string) Standardwert ist '/' - # Domains, die Cookies akzeptieren - cookieDomain: 'example.com' # (string|domain) Standard ist nicht gesetzt + # Domains, die das Cookie empfangen dürfen + cookieDomain: 'example.com' # (string|domain) standardmäßig nicht gesetzt # Cookies nur über HTTPS senden? - cookieSecure: ... # (bool|auto) Standard ist auto + cookieSecure: ... # (bool|auto) Standardwert ist auto - # Deaktiviert das Senden des Cookies, das Nette als Schutz vor CSRF verwendet - disableNetteCookie: ... # (bool) Standard ist false + # schaltet das Senden des Cookies ab, das Nette zum CSRF-Schutz verwendet + disableNetteCookie: ... # (bool) Standardwert ist false ``` -Das Attribut `cookieDomain` bestimmt, welche Domains Cookies akzeptieren können. Wenn es nicht angegeben ist, akzeptiert die gleiche (Sub-)Domain, die das Cookie gesetzt hat, *aber nicht* ihre Subdomains, das Cookie. Wenn `cookieDomain` angegeben ist, sind auch Subdomains enthalten. Daher ist die Angabe von `cookieDomain` weniger restriktiv als das Weglassen. +Das Attribut `cookieDomain` bestimmt, welche Domains (Origins) das Cookie annehmen dürfen. Wird es nicht angegeben, nimmt es dieselbe (Sub-)Domain an, die es gesetzt hat, *ohne* deren Subdomains. Ist `cookieDomain` angegeben, sind auch die Subdomains eingeschlossen. Die Angabe von `cookieDomain` ist also weniger einschränkend als ihr Weglassen. -Zum Beispiel sind bei `cookieDomain: nette.org` Cookies auch auf allen Subdomains wie `doc.nette.org` verfügbar. Dasselbe kann auch mit dem speziellen Wert `domain` erreicht werden, also `cookieDomain: domain`. +Ist zum Beispiel `cookieDomain: nette.org` gesetzt, sind die Cookies auch auf allen Subdomains wie `doc.nette.org` verfügbar. Dasselbe erreichen Sie mit dem speziellen Wert `domain`, also `cookieDomain: domain`. -Der Standardwert `auto` für das Attribut `cookieSecure` bedeutet, dass, wenn die Website über HTTPS läuft, Cookies mit dem `Secure`-Flag gesendet werden und somit nur über HTTPS verfügbar sind. +Der Standardwert `auto` beim Attribut `cookieSecure` bedeutet, dass die Cookies mit dem Flag `Secure` gesendet werden und damit nur über HTTPS verfügbar sind, wenn die Website über HTTPS läuft. HTTP-Proxy ---------- -Wenn die Website hinter einem HTTP-Proxy läuft, geben Sie dessen IP-Adresse an, damit die Erkennung der Verbindung über HTTPS und auch die IP-Adresse des Clients korrekt funktionieren. Das heißt, damit die Funktionen [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] und [isSecured() |request#isSecured] die korrekten Werte zurückgeben und in Templates Links mit dem `https:`-Protokoll generiert werden. +Läuft die Site hinter einem HTTP-Proxy, geben Sie die IP-Adresse des Proxys an, damit die Erkennung der HTTPS-Verbindung und der IP-Adresse des Clients korrekt funktioniert. Also damit [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()] und [isSecured() |request#isSecured()] die richtigen Werte zurückgeben und die Links in Templates mit dem Protokoll `https:` erzeugt werden. ```neon http: - # IP-Adresse, Bereich (z.B. 127.0.0.1/8) oder Array dieser Werte - proxy: 127.0.0.1 # (string|string[]) Standard ist nicht gesetzt + # IP-Adresse, Bereich (z. B. 127.0.0.1/8) oder ein Array dieser Werte + proxy: 127.0.0.1 # (string|string[]) standardmäßig nicht gesetzt +``` + + +HTTPS erzwingen .{data-version:3.3.4} +------------------------------------- + +Erzwingt bedingungslos das Schema HTTPS für den Request. Das ist nützlich für reine HTTPS-Sites, die hinter einem Load Balancer oder Reverse Proxy laufen, der TLS terminiert, aber den Header `X-Forwarded-Proto` nicht weitergibt, sodass die übliche HTTPS-Erkennung (auch mit konfiguriertem [#HTTP-Proxy]) das nicht bemerken würde. + +```neon +http: + # HTTPS-Schema für alle Requests erzwingen + forceHttps: true # (bool) Standardwert ist false ``` Session ======= -Grundlegende Einstellungen für [Sitzungen|sessions]: +Grundlegende Einstellungen der [Sessions |sessions]: ```neon session: - # Session-Panel in der Tracy Bar anzeigen? - debugger: ... # (bool) Standard ist false + # das Session-Panel in der Tracy Bar anzeigen? + debugger: ... # (bool) Standardwert ist false - # Inaktivitätsdauer, nach der die Session abläuft - expiration: 14 days # (string) Standard ist '3 hours' + # Zeit der Inaktivität, nach der die Session abläuft + expiration: 14 days # (string) Standardwert ist '3 hours' - # Wann soll die Session gestartet werden? - autoStart: ... # (smart|always|never) Standard ist 'smart' + # wann soll die Session starten? + autoStart: ... # (smart|always|never) Standardwert ist 'smart' - # Handler, Dienst, der das SessionHandlerInterface implementiert + # Handler, ein Service, der SessionHandlerInterface implementiert handler: @handlerService ``` -Die Option `autoStart` steuert, wann die Session gestartet werden soll. Der Wert `always` bedeutet, dass die Session immer beim Start der Anwendung gestartet wird. Der Wert `smart` bedeutet, dass die Session beim Start der Anwendung nur dann gestartet wird, wenn sie bereits existiert, oder in dem Moment, in dem wir daraus lesen oder hineinschreiben möchten. Und schließlich verbietet der Wert `never` den automatischen Start der Session. +Die Option `autoStart` steuert, wann die Session starten soll. Der Wert `always` bedeutet, dass die Session immer beim Start der Anwendung startet. Der Wert `smart` bedeutet, dass die Session mit der Anwendung nur dann startet, wenn sie bereits existiert, oder in dem Moment, in dem wir aus ihr lesen oder in sie schreiben wollen. Der Wert `never` schließlich schaltet den automatischen Start der Session ab. -Weiterhin können alle PHP [Session-Direktiven |https://www.php.net/manual/en/session.configuration.php] (im CamelCase-Format) sowie [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] eingestellt werden. Beispiel: +Darüber hinaus können Sie alle [Session-Direktiven |https://www.php.net/manual/en/session.configuration.php] von PHP setzen (im camelCase-Format) und außerdem [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Beispiel: ```neon session: - # 'session.name' schreiben wir als 'name' + # 'session.name' geschrieben als 'name' name: MYID - # 'session.save_path' schreiben wir als 'savePath' + # 'session.save_path' geschrieben als 'savePath' savePath: "%tempDir%/sessions" ``` @@ -145,27 +159,28 @@ session: Session-Cookie -------------- -Das Session-Cookie wird mit denselben Parametern wie [andere Cookies |#HTTP-Cookie] gesendet, aber diese können für es geändert werden: +Das Session-Cookie wird mit denselben Parametern gesendet wie [andere Cookies |#HTTP-Cookie], Sie können sie aber speziell dafür ändern: ```neon session: - # Domains, die Cookies akzeptieren + # Domains, die das Cookie empfangen dürfen cookieDomain: 'example.com' # (string|domain) - # Einschränkung beim Zugriff von einer anderen Domain - cookieSamesite: None # (Strict|Lax|None) Standard ist Lax + # Einschränkung für den Cross-Origin-Zugriff + cookieSamesite: None # (Strict|Lax|None) Standardwert ist Lax ``` -Das Attribut `cookieSamesite` beeinflusst, ob das Cookie beim [Zugriff von einer anderen Domain |nette:glossary#SameSite-Cookie] gesendet wird, was einen gewissen Schutz vor Angriffen durch [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF) bietet. +Das Attribut `cookieSamesite` beeinflusst, ob das Cookie bei [Cross-Origin-Requests |nette:glossary#SameSite-Cookie] gesendet wird, was einen gewissen Schutz gegen Angriffe vom Typ [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF) bietet. -DI-Dienste -========== +DI-Services +=========== -Diese Dienste werden dem DI-Container hinzugefügt: +Diese Services werden dem DI-Container hinzugefügt: | Name | Typ | Beschreibung -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP-Anfrage| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP-Antwort| response] -| `session.session` | [api:Nette\Http\Session] | [Session-Verwaltung| sessions] +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [HTTP-Request |request] +| `http.response` | [api:Nette\Http\Response] | [HTTP-Response |response] +| `session.session`| [api:Nette\Http\Session] | [Session-Verwaltung |sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | Factory, die den HTTP-Request erzeugt diff --git a/http/de/request.texy b/http/de/request.texy index 6e8ba74c57..c1f0a9c2f9 100644 --- a/http/de/request.texy +++ b/http/de/request.texy @@ -1,12 +1,12 @@ -HTTP-Anfrage +HTTP-Request ************ .[perex] -Nette kapselt die HTTP-Anfrage in Objekte mit einer verständlichen API und bietet gleichzeitig einen Bereinigungsfilter. +Nette kapselt den HTTP-Request in Objekte mit einer klaren API und stellt zugleich einen Filter zur Bereinigung der Eingaben bereit. -Die HTTP-Anfrage wird durch das Objekt [api:Nette\Http\Request] repräsentiert. Wenn Sie mit Nette arbeiten, wird dieses Objekt automatisch vom Framework erstellt und Sie können es sich mittels [Dependency Injection |dependency-injection:passing-dependencies] übergeben lassen. In Presentern reicht es aus, die Methode `$this->getHttpRequest()` aufzurufen. Wenn Sie außerhalb des Nette Frameworks arbeiten, können Sie das Objekt mithilfe von [#RequestFactory] erstellen. +Der HTTP-Request wird durch das Objekt [api:Nette\Http\Request] repräsentiert. Wenn Sie mit Nette arbeiten, wird dieses Objekt automatisch vom Framework erzeugt, und Sie können es sich per [Dependency Injection |dependency-injection:passing-dependencies] übergeben lassen. In Presentern genügt der Aufruf der Methode `$this->getHttpRequest()`. Wenn Sie außerhalb des Nette Frameworks arbeiten, können Sie das Objekt mit der [#RequestFactory] erzeugen. -Ein großer Vorteil von Nette ist, dass beim Erstellen des Objekts alle Eingabeparameter GET, POST, COOKIE sowie die URL automatisch von Steuerzeichen und ungültigen UTF-8-Sequenzen bereinigt werden. Mit diesen Daten können Sie dann sicher weiterarbeiten. Die bereinigten Daten werden anschließend in Presentern und Formularen verwendet. +Ein großer Vorteil von Nette ist, dass es beim Erzeugen des Objekts automatisch alle Eingabeparameter (GET, POST, COOKIE) sowie die URL bereinigt und Steuerzeichen und ungültige UTF-8-Sequenzen entfernt. Mit diesen Daten können Sie dann sicher arbeiten. Die bereinigten Daten werden anschließend in Presentern und Formularen verwendet. → [Installation und Anforderungen |@home#Installation] @@ -14,7 +14,7 @@ Ein großer Vorteil von Nette ist, dass beim Erstellen des Objekts alle Eingabep Nette\Http\Request ================== -Dieses Objekt ist immutable (unveränderlich). Es hat keine Setter, sondern nur einen sogenannten Wither `withUrl()`, der das Objekt nicht ändert, sondern eine neue Instanz mit dem geänderten Wert zurückgibt. +Dieses Objekt ist unveränderlich. Es hat keine Setter; es hat nur einen sogenannten Wither, `withUrl()`, der das Objekt nicht verändert, sondern eine neue Instanz mit dem geänderten Wert zurückgibt. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] @@ -24,70 +24,70 @@ Gibt einen Klon mit einer anderen URL zurück. getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -Gibt die URL der Anfrage als [UrlScript |urls#UrlScript]-Objekt zurück. +Gibt die URL des Requests als Objekt [UrlScript |urls#UrlScript] zurück. ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -Warnung: Browser senden kein Fragment an den Server, daher gibt `$url->getFragment()` einen leeren String zurück. +Achtung: Browser senden das Fragment nicht an den Server, `$url->getFragment()` gibt also einen leeren String zurück. getQuery(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Gibt die GET-Parameter der Anfrage zurück. +Gibt die Parameter des GET-Requests zurück. ```php -$all = $httpRequest->getQuery(); // Gibt ein Array aller Parameter aus der URL zurück -$id = $httpRequest->getQuery('id'); // Gibt den GET-Parameter 'id' zurück (oder null) +$all = $httpRequest->getQuery(); // Array aller URL-Parameter +$id = $httpRequest->getQuery('id'); // gibt den GET-Parameter 'id' zurück (oder null) ``` getPost(?string $key=null): string|array|null .[method] ------------------------------------------------------- -Gibt die POST-Parameter der Anfrage zurück. +Gibt die Parameter des POST-Requests zurück. ```php -$all = $httpRequest->getPost(); // Gibt ein Array aller Parameter aus POST zurück -$id = $httpRequest->getPost('id'); // Gibt den POST-Parameter 'id' zurück (oder null) +$all = $httpRequest->getPost(); // Array aller POST-Parameter +$id = $httpRequest->getPost('id'); // gibt den POST-Parameter 'id' zurück (oder null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Gibt den [Upload |#Hochgeladene Dateien] als [api:Nette\Http\FileUpload]-Objekt zurück: +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- +Gibt einen [Upload |#Hochgeladene Dateien] als Objekt [api:Nette\Http\FileUpload] zurück: ```php $file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // Wurde eine Datei hochgeladen? - $file->getUntrustedName(); // Vom Benutzer gesendeter Dateiname +if ($file?->hasFile()) { // wurde überhaupt eine Datei hochgeladen? + $file->getUntrustedName(); // vom Benutzer gesendeter Dateiname $file->getSanitizedName(); // Name ohne gefährliche Zeichen } ``` -Für den Zugriff auf eine verschachtelte Struktur geben Sie ein Array von Schlüsseln an. +Um auf eine verschachtelte Struktur zuzugreifen, geben Sie ein Array von Schlüsseln an. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Da man externen Daten nicht vertrauen kann und sich daher auch nicht auf die Struktur der Dateien verlassen kann, ist dieser Weg sicherer als z.B. `$request->getFiles()['my-form']['details']['avatar']`, was fehlschlagen kann. +Weil Sie externen Daten nicht trauen können und sich deshalb nicht auf die Struktur der Dateien verlassen dürfen, ist dieser Weg sicherer als zum Beispiel `$request->getFiles()['my-form']['details']['avatar']`, das fehlschlagen könnte. getFiles(): array .[method] --------------------------- -Gibt einen Baum [aller Uploads |#Hochgeladene Dateien] in einer normalisierten Struktur zurück, deren Blätter [api:Nette\Http\FileUpload]-Objekte sind: +Gibt einen Baum [aller Uploads |#Hochgeladene Dateien] in einer normalisierten Struktur zurück, deren Blätter Objekte vom Typ [api:Nette\Http\FileUpload] sind: ```php $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- +getCookie(string $key): ?string .[method] +----------------------------------------- Gibt ein Cookie zurück oder `null`, wenn es nicht existiert. ```php @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -Gibt die HTTP-Methode zurück, mit der die Anfrage gemacht wurde. +Gibt die HTTP-Methode zurück, mit der der Request gestellt wurde. ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -Testet die HTTP-Methode, mit der die Anfrage gemacht wurde. Der Parameter ist case-insensitive. +Prüft die HTTP-Methode, mit der der Request gestellt wurde. Beim Parameter wird die Groß- und Kleinschreibung nicht unterschieden. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,51 +124,83 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -Gibt einen HTTP-Header zurück oder `null`, wenn er nicht existiert. Der Parameter ist case-insensitive. +Gibt einen HTTP-Header zurück oder `null`, wenn er nicht existiert. Beim Parameter wird die Groß- und Kleinschreibung nicht unterschieden. ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Gibt alle HTTP-Header als assoziatives Array zurück. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Gibt alle HTTP-Header als assoziatives Array zurück. Die Schlüssel sind auf Kleinbuchstaben normalisiert. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -Ist die Verbindung verschlüsselt (HTTPS)? Für die korrekte Funktion kann es notwendig sein, einen [Proxy einzurichten |configuration#HTTP-Proxy]. +Ist die Verbindung verschlüsselt (HTTPS)? Für das korrekte Funktionieren kann die [Einrichtung eines Proxys |configuration#HTTP-Proxy] nötig sein. -isSameSite(): bool .[method] ----------------------------- -Kommt die Anfrage von derselben (Sub-)Domain und wird sie durch Klicken auf einen Link initiiert? Nette verwendet zur Erkennung das Cookie `_nss` (früher `nette-samesite`). +isSameSite(): bool .[method deprecated] +--------------------------------------- +Kam der Request von derselben Site? Seit Version 3.4 wird sie durch das leistungsfähigere [isFrom() |#isFrom()] ersetzt. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Sagt Ihnen, woher der Request kam und wie der Browser ihn gestellt hat, und zwar anhand der `Sec-Fetch-*`-Header (der sogenannten [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]), die der Browser selbst setzt und die eine im Browser des Opfers laufende Seite weder fälschen noch entfernen kann. Nette nutzt das intern, um Formulare und Signale automatisch gegen [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF) zu schützen. Nützlich ist es, wenn Sie eigene sensible Aktionen absichern wollen, etwa API-Endpunkte oder destruktive Links. + +Die Methode gibt nur dann `true` zurück, wenn der Request **alle** von Ihnen angegebenen Bedingungen erfüllt. Der erste Parameter `$site` beschreibt die Beziehung zwischen der Seite, die den Request ausgelöst hat, und Ihrer Site (der Header `Sec-Fetch-Site`). Er nimmt einen einzelnen Wert oder eine Liste dieser `FetchSite`-Fälle entgegen: + +- `FetchSite::SameOrigin` - vom exakt selben Origin (Schema, Host und Port) +- `FetchSite::SameSite` - von derselben Site, eventuell einer anderen Subdomain +- `FetchSite::CrossSite` - von einer fremden Site +- `FetchSite::None` - der Benutzer hat ihn direkt ausgelöst, z. B. durch Eingabe der URL oder Öffnen eines Lesezeichens + +```php +// stammt der Request von unseren eigenen Seiten? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // die Aktion blockieren +} +``` + +Der optionale Parameter `$dest` (der Header `Sec-Fetch-Dest`) sagt, welche Art von Ressource der Browser lädt, z. B. `FetchDest::Document` für eine Navigation auf oberster Ebene oder `FetchDest::Empty` für einen aus JavaScript gestellten Request. Der optionale Parameter `$user` (der Header `Sec-Fetch-User`) gibt an, ob die Navigation durch eine echte Benutzeraktion wie den Klick auf einen Link oder das Absenden eines Formulars ausgelöst wurde; übergeben Sie `true`, um das zu verlangen. + +Eine Prüfung, dass eine Aktion nur von Ihren eigenen Seiten und nur durch eine echte Benutzeraktion erreichbar ist, sieht dann so aus: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Ältere Browser (Safari vor 16.4) senden die `Sec-Fetch-*`-Header nicht. Für sie greift Nette auf ein `SameSite=Strict`-Cookie zurück, das nur belegt, dass der Request nicht Cross-Site ist. Eine Prüfung, die zusätzlich `$dest` oder `$user` verlangt, lässt sich auf diese Weise nicht bestätigen und gibt in diesen Browsern `false` zurück - wenn das zu streng ist, prüfen Sie nur `$site`. isAjax(): bool .[method] ------------------------ -Ist es eine AJAX-Anfrage? +Handelt es sich um einen AJAX-Request? getRemoteAddress(): ?string .[method] ------------------------------------- -Gibt die IP-Adresse des Benutzers zurück. Für die korrekte Funktion kann es notwendig sein, einen [Proxy einzurichten |configuration#HTTP-Proxy]. +Gibt die IP-Adresse des Benutzers zurück. Für das korrekte Funktionieren kann die [Einrichtung eines Proxys |configuration#HTTP-Proxy] nötig sein. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Gibt die DNS-Auflösung der IP-Adresse des Benutzers zurück. Für die korrekte Funktion kann es notwendig sein, einen [Proxy einzurichten |configuration#HTTP-Proxy]. +Veraltet, gibt immer `null` zurück. Reverse-DNS-Abfragen waren langsam und unzuverlässig; wenn Sie den Hostnamen brauchen, ermitteln Sie ihn selbst aus [getRemoteAddress() |#getRemoteAddress()]. getBasicCredentials(): ?array .[method] --------------------------------------- -Gibt die Anmeldeinformationen für die [Basic HTTP-Authentifizierung |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication] zurück. +Gibt die Zugangsdaten für die [HTTP-Basic-Authentifizierung |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication] zurück. ```php [$user, $password] = $httpRequest->getBasicCredentials(); @@ -177,21 +209,45 @@ Gibt die Anmeldeinformationen für die [Basic HTTP-Authentifizierung |https://de getRawBody(): ?string .[method] ------------------------------- -Gibt den Körper der HTTP-Anfrage zurück. +Gibt den Body des HTTP-Requests zurück. ```php $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Gibt den Origin zurück, von dem der Request kam. Ein Origin besteht aus Schema (Protokoll), Hostname und Port - zum Beispiel `https://example.com:8080`. Gibt `null` zurück, wenn der Origin-Header fehlt oder auf `'null'` gesetzt ist. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Der Browser sendet den Header `Origin` in folgenden Fällen: +- Cross-Origin-Requests (AJAX-Aufrufe an eine andere Domain) +- POST-, PUT-, DELETE- und andere verändernde Requests +- Requests über die Fetch API + +Der Browser sendet den Header `Origin` NICHT bei: +- gewöhnlichen GET-Requests an dieselbe Domain (Same-Origin-Navigation) +- direkter Navigation durch Eingabe einer URL in die Adresszeile +- Requests von Clients, die keine Browser sind + +.[note] +Anders als der Header `Referer` enthält `Origin` nur Schema, Host und Port - nicht den vollständigen Pfad der URL. Das macht ihn für Sicherheitsprüfungen geeigneter und wahrt zugleich die Privatsphäre der Benutzer. Der Header `Origin` wird vor allem für die Prüfung von [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing) verwendet. + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Erkennt die Sprache. Als Parameter `$lang` übergeben wir ein Array mit den von der Anwendung unterstützten Sprachen, und sie gibt diejenige zurück, die der Browser des Besuchers am liebsten sehen würde. Das ist keine Magie, es wird nur der `Accept-Language`-Header verwendet. Wenn keine Übereinstimmung gefunden wird, gibt sie `null` zurück. +Erkennt die Sprache. Übergeben Sie als Parameter `$langs` ein Array der von der Anwendung unterstützten Sprachen, und die Methode gibt diejenige zurück, die der Browser des Besuchers bevorzugt. Das ist keine Magie, sie nutzt nur den Header `Accept-Language`. Wird keine Übereinstimmung gefunden, gibt sie `null` zurück. ```php -// Der Browser sendet z.B. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 +// Der Browser sendet z. B. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 -$langs = ['hu', 'pl', 'en']; // Von der Anwendung unterstützte Sprachen +$langs = ['hu', 'pl', 'en']; // von der Anwendung unterstützte Sprachen echo $httpRequest->detectLanguage($langs); // en ``` @@ -199,42 +255,43 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -Die Klasse [api:Nette\Http\RequestFactory] dient zur Erstellung einer Instanz von `Nette\Http\Request`, die die aktuelle HTTP-Anfrage repräsentiert. (Wenn Sie mit Nette arbeiten, wird das HTTP-Anfrageobjekt automatisch vom Framework erstellt.) +Die Klasse [api:Nette\Http\RequestFactory] dient dazu, eine Instanz von `Nette\Http\Request` zu erzeugen, die den aktuellen HTTP-Request repräsentiert. (Wenn Sie mit Nette arbeiten, wird das Objekt des HTTP-Requests automatisch vom Framework erzeugt.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -Die Methode `fromGlobals()` erstellt das Anfrageobjekt basierend auf den aktuellen globalen PHP-Variablen (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` und `$_SERVER`). Beim Erstellen des Objekts bereinigt sie automatisch alle Eingabeparameter GET, POST, COOKIE sowie die URL von Steuerzeichen und ungültigen UTF-8-Sequenzen, was die Sicherheit bei der weiteren Arbeit mit diesen Daten gewährleistet. +Die Methode `fromGlobals()` erzeugt das Request-Objekt anhand der aktuellen globalen Variablen von PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` und `$_SERVER`). Beim Erzeugen des Objekts bereinigt sie automatisch alle Eingabeparameter (GET, POST, COOKIE) sowie die URL von Steuerzeichen und ungültigen UTF-8-Sequenzen und sorgt so für Sicherheit bei der späteren Arbeit mit diesen Daten. -RequestFactory kann vor dem Aufruf von `fromGlobals()` konfiguriert werden: +Die RequestFactory lässt sich vor dem Aufruf von `fromGlobals()` konfigurieren: -- Mit der Methode `$factory->setBinary()` deaktivieren Sie die automatische Bereinigung der Eingabeparameter von Steuerzeichen und ungültigen UTF-8-Sequenzen. -- Mit der Methode `$factory->setProxy(...)` geben Sie die IP-Adresse des [Proxy-Servers |configuration#HTTP-Proxy] an, was für die korrekte Erkennung der IP-Adresse des Benutzers notwendig ist. +- Die Methode `$factory->setBinary()` schaltet die automatische Bereinigung der Eingabeparameter von Steuerzeichen und ungültigen UTF-8-Sequenzen ab. +- Die Methode `$factory->setProxy(...)` gibt die IP-Adresse des [Proxyservers |configuration#HTTP-Proxy] an, was für die korrekte Erkennung der IP-Adresse des Benutzers nötig ist. +- Die Methode `$factory->setForceHttps()` .{data-version:3.3.4} erzwingt unabhängig von der Serverumgebung das Schema HTTPS für den Request. -RequestFactory ermöglicht die Definition von Filtern, die Teile der Anfrage-URL automatisch transformieren. Diese Filter entfernen unerwünschte Zeichen aus der URL, die dort beispielsweise durch eine falsche Implementierung von Kommentarsystemen auf verschiedenen Websites eingefügt werden können: +Die RequestFactory erlaubt es, Filter zu definieren, die Teile der Request-URL automatisch umschreiben. Diese Filter entfernen unerwünschte Zeichen aus URLs, die zum Beispiel durch fehlerhafte Implementierungen von Kommentarsystemen auf verschiedenen Websites hineingeraten sein können: ```php -// Entfernung von Leerzeichen aus dem Pfad +// Leerzeichen aus dem Pfad entfernen $requestFactory->urlFilters['path']['%20'] = ''; -// Entfernung von Punkt, Komma oder rechter Klammer am Ende der URI +// Punkt, Komma oder schließende Klammer am Ende der URI entfernen $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// Bereinigung des Pfades von doppelten Schrägstrichen (Standardfilter) +// den Pfad von doppelten Schrägstrichen bereinigen (Standardfilter) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -Der erste Schlüssel `'path'` oder `'url'` bestimmt, auf welchen Teil der URL der Filter angewendet wird. Der zweite Schlüssel ist der reguläre Ausdruck, der gesucht werden soll, und der Wert ist der Ersatz, der anstelle des gefundenen Textes verwendet wird. +Der erste Schlüssel, `'path'` oder `'url'`, bestimmt, auf welchen Teil der URL der Filter angewendet wird. Der zweite Schlüssel ist der reguläre Ausdruck, nach dem gesucht wird, und der Wert ist der Ersatz, der anstelle des gefundenen Textes eingesetzt wird. Hochgeladene Dateien ==================== -Die Methode `Nette\Http\Request::getFiles()` gibt ein Array aller Uploads in einer normalisierten Struktur zurück, deren Blätter [api:Nette\Http\FileUpload]-Objekte sind. Diese kapseln die Daten, die von einem Formularelement `<input type=file>` gesendet wurden. +Die Methode `Nette\Http\Request::getFiles()` gibt ein Array aller Uploads in einer normalisierten Struktur zurück, deren Blätter Objekte vom Typ [api:Nette\Http\FileUpload] sind. Diese kapseln die Daten, die über das Formularelement `<input type=file>` gesendet wurden. -Die Struktur spiegelt die Benennung der Elemente in HTML wider. Im einfachsten Fall kann dies ein einzelnes benanntes Formularelement sein, das wie folgt gesendet wird: +Die Struktur bildet die Benennung der Elemente im HTML ab. Im einfachsten Fall kann das ein einzelnes benanntes Formularelement sein, das so gesendet wird: ```latte <input type="file" name="avatar"> @@ -244,17 +301,17 @@ In diesem Fall gibt `$request->getFiles()` ein Array zurück: ```php [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* FileUpload-Instanz */ ] ``` -Das `FileUpload`-Objekt wird auch dann erstellt, wenn der Benutzer keine Datei gesendet hat oder das Senden fehlgeschlagen ist. Ob eine Datei gesendet wurde, gibt die Methode `hasFile()` zurück: +Das Objekt `FileUpload` wird auch dann erzeugt, wenn der Benutzer keine Datei hochgeladen hat oder der Upload fehlgeschlagen ist. Die Methode `hasFile()` gibt true zurück, wenn eine Datei gesendet wurde: ```php $request->getFile('avatar')?->hasFile(); ``` -Im Falle eines Elementnamens, der die Array-Notation verwendet: +Bei einem Elementnamen in Array-Schreibweise: ```latte <input type="file" name="my-form[details][avatar]"> @@ -266,13 +323,13 @@ sieht der zurückgegebene Baum so aus: [ 'my-form' => [ 'details' => [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* FileUpload-Instanz */ ], ], ] ``` -Es kann auch ein Array von Dateien erstellt werden: +Sie können auch Arrays von Dateien erzeugen: ```latte <input type="file" name="my-form[details][avatars][]" multiple> @@ -285,25 +342,25 @@ In einem solchen Fall sieht die Struktur so aus: 'my-form' => [ 'details' => [ 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, + 0 => /* FileUpload-Instanz */, + 1 => /* FileUpload-Instanz */, + 2 => /* FileUpload-Instanz */, ], ], ], ] ``` -Der Zugriff auf den Index 1 des verschachtelten Arrays erfolgt am besten so: +Am besten greifen Sie auf den Index 1 des verschachtelten Arrays so zu: ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -Da man externen Daten nicht vertrauen kann und sich daher auch nicht auf die Struktur der Dateien verlassen kann, ist dieser Weg sicherer als z.B. `$request->getFiles()['my-form']['details']['avatars'][1]`, was fehlschlagen kann. +Weil Sie externen Daten nicht trauen können und sich deshalb nicht auf die Struktur der Dateien verlassen dürfen, ist dieser Weg sicherer als zum Beispiel `$request->getFiles()['my-form']['details']['avatars'][1]`, das fehlschlagen könnte. Übersicht der `FileUpload`-Methoden .{toc: FileUpload} @@ -322,12 +379,12 @@ Gibt `true` zurück, wenn die Datei erfolgreich hochgeladen wurde. getError(): int .[method] ------------------------- -Gibt den Fehlercode beim Hochladen der Datei zurück. Es handelt sich um eine der Konstanten [UPLOAD_ERR_XXX |http://php.net/manual/en/features.file-upload.errors.php]. Wenn der Upload erfolgreich war, gibt sie `UPLOAD_ERR_OK` zurück. +Gibt den Fehlercode zurück, der zur hochgeladenen Datei gehört. Es ist eine der Konstanten [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php]. Wurde die Datei erfolgreich hochgeladen, gibt sie `UPLOAD_ERR_OK` zurück. move(string $dest) .[method] ---------------------------- -Verschiebt die hochgeladene Datei an einen neuen Speicherort. Wenn die Zieldatei bereits existiert, wird sie überschrieben. +Verschiebt eine hochgeladene Datei an einen neuen Ort. Existiert die Zieldatei bereits, wird sie überschrieben. ```php $file->move('/path/to/files/name.ext'); @@ -336,72 +393,77 @@ $file->move('/path/to/files/name.ext'); getContents(): ?string .[method] -------------------------------- -Gibt den Inhalt der hochgeladenen Datei zurück. Wenn der Upload nicht erfolgreich war, gibt sie `null` zurück. +Gibt den Inhalt der hochgeladenen Datei zurück. War der Upload nicht erfolgreich, gibt sie `null` zurück. getContentType(): ?string .[method] ----------------------------------- -Erkennt den MIME-Inhaltstyp der hochgeladenen Datei anhand ihrer Signatur. Wenn der Upload nicht erfolgreich war oder die Erkennung fehlschlug, gibt sie `null` zurück. +Erkennt den MIME-Content-Type der hochgeladenen Datei anhand ihrer Signatur. War der Upload nicht erfolgreich oder ist die Erkennung fehlgeschlagen, gibt sie `null` zurück. .[caution] -Erfordert die PHP-Erweiterung `fileinfo`. +Erfordert die PHP-Extension `fileinfo`. getUntrustedName(): string .[method] ------------------------------------ -Gibt den ursprünglichen Dateinamen zurück, wie er vom Browser gesendet wurde. +Gibt den ursprünglichen Dateinamen zurück, wie ihn der Browser gesendet hat. .[caution] -Vertrauen Sie nicht dem von dieser Methode zurückgegebenen Wert. Ein Client könnte einen bösartigen Dateinamen gesendet haben, um Ihre Anwendung zu beschädigen oder zu hacken. +Trauen Sie dem von dieser Methode zurückgegebenen Wert nicht. Ein Client könnte einen bösartigen Dateinamen senden, um Ihre Anwendung zu beschädigen oder zu kompromittieren. getSanitizedName(): string .[method] ------------------------------------ -Gibt den bereinigten Dateinamen zurück. Enthält nur ASCII-Zeichen `[a-zA-Z0-9.-]`. Wenn der Name keine solchen Zeichen enthält, gibt sie `'unknown'` zurück. Wenn die Datei ein Bild im Format JPEG, PNG, GIF, WebP oder AVIF ist, gibt sie auch die korrekte Erweiterung zurück. +Gibt den bereinigten Dateinamen zurück. Er enthält nur die ASCII-Zeichen `[a-zA-Z0-9.-]`. Enthält der Name keine solchen Zeichen, gibt sie `'unknown'` zurück. Ist die Datei ein JPEG-, PNG-, GIF-, WebP- oder AVIF-Bild, gibt sie außerdem die korrekte Dateiendung zurück. .[caution] -Erfordert die PHP-Erweiterung `fileinfo`. +Erfordert die PHP-Extension `fileinfo`. getSuggestedExtension(): ?string .[method]{data-version:3.2.4} -------------------------------------------------------------- -Gibt die passende Dateierweiterung (ohne Punkt) zurück, die dem erkannten MIME-Typ entspricht. +Gibt die passende Dateiendung (ohne Punkt) zurück, die dem erkannten MIME-Type entspricht. .[caution] -Erfordert die PHP-Erweiterung `fileinfo`. +Erfordert die PHP-Extension `fileinfo`. getUntrustedFullPath(): string .[method] ---------------------------------------- -Gibt den ursprünglichen Dateipfad zurück, wie er vom Browser beim Hochladen eines Ordners gesendet wurde. Der vollständige Pfad ist nur in PHP 8.1 und höher verfügbar. In früheren Versionen gibt diese Methode den ursprünglichen Dateinamen zurück. +Gibt den ursprünglichen Dateipfad zurück, wie ihn der Browser beim Upload eines Verzeichnisses gesendet hat. Der vollständige Pfad ist erst ab PHP 8.1 verfügbar. In früheren Versionen gibt diese Methode den ursprünglichen Dateinamen zurück. .[caution] -Vertrauen Sie nicht dem von dieser Methode zurückgegebenen Wert. Ein Client könnte einen bösartigen Dateinamen gesendet haben, um Ihre Anwendung zu beschädigen oder zu hacken. +Trauen Sie dem von dieser Methode zurückgegebenen Wert nicht. Ein Client könnte einen bösartigen Dateinamen senden, um Ihre Anwendung zu beschädigen oder zu kompromittieren. getSize(): int .[method] ------------------------ -Gibt die Größe der hochgeladenen Datei zurück. Wenn der Upload nicht erfolgreich war, gibt sie `0` zurück. +Gibt die Größe der hochgeladenen Datei zurück. War der Upload nicht erfolgreich, gibt sie `0` zurück. getTemporaryFile(): string .[method] ------------------------------------ -Gibt den Pfad zum temporären Speicherort der hochgeladenen Datei zurück. Wenn der Upload nicht erfolgreich war, gibt sie `''` zurück. +Gibt den Pfad zum temporären Speicherort der hochgeladenen Datei zurück. War der Upload nicht erfolgreich, gibt sie `''` zurück. + + +__toString(): string .[method] +------------------------------ +Gibt den Pfad zum temporären Speicherort der hochgeladenen Datei zurück. Dadurch lässt sich das Objekt `FileUpload` direkt als String verwenden. isImage(): bool .[method] ------------------------- -Gibt `true` zurück, wenn die hochgeladene Datei ein Bild im Format JPEG, PNG, GIF, WebP oder AVIF ist. Die Erkennung erfolgt anhand ihrer Signatur und die Integrität der gesamten Datei wird nicht überprüft. Ob ein Bild beschädigt ist, kann beispielsweise durch den Versuch, es zu [Laden |#toImage], festgestellt werden. +Gibt `true` zurück, wenn die hochgeladene Datei ein JPEG-, PNG-, GIF-, WebP- oder AVIF-Bild ist. Die Erkennung erfolgt anhand der Signatur und prüft nicht die Integrität der gesamten Datei. Ob ein Bild beschädigt ist, lässt sich zum Beispiel dadurch feststellen, dass man es zu [laden |#toImage()] versucht. .[caution] -Erfordert die PHP-Erweiterung `fileinfo`. +Erfordert die PHP-Extension `fileinfo`. getImageSize(): ?array .[method] -------------------------------- -Gibt ein Paar `[Breite, Höhe]` mit den Abmessungen des hochgeladenen Bildes zurück. Wenn der Upload nicht erfolgreich war oder es sich nicht um ein gültiges Bild handelt, gibt sie `null` zurück. +Gibt ein Paar `[Breite, Höhe]` mit den Abmessungen des hochgeladenen Bildes zurück. War der Upload nicht erfolgreich oder handelt es sich nicht um ein gültiges Bild, gibt sie `null` zurück. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Lädt das Bild als [Image |utils:images]-Objekt. Wenn der Upload nicht erfolgreich war oder es sich nicht um ein gültiges Bild handelt, wird eine Ausnahme `Nette\Utils\ImageException` ausgelöst. +Lädt das Bild als Objekt [Image |utils:images]. War der Upload nicht erfolgreich oder handelt es sich nicht um ein gültiges Bild, wirft sie eine `Nette\Utils\ImageException`. diff --git a/http/de/response.texy b/http/de/response.texy index 0665ad2a4f..99fa2120ff 100644 --- a/http/de/response.texy +++ b/http/de/response.texy @@ -1,10 +1,10 @@ -HTTP-Antwort -************ +HTTP-Response +************* .[perex] -Nette kapselt die HTTP-Antwort in Objekte mit einer verständlichen API. +Nette kapselt die HTTP-Response in Objekte mit einer klaren API. -Die HTTP-Antwort wird durch das Objekt [api:Nette\Http\Response] repräsentiert. Wenn Sie mit Nette arbeiten, wird dieses Objekt automatisch vom Framework erstellt und Sie können es sich mittels [Dependency Injection |dependency-injection:passing-dependencies] übergeben lassen. In Presentern reicht es aus, die Methode `$this->getHttpResponse()` aufzurufen. +Die HTTP-Response wird durch das Objekt [api:Nette\Http\Response] repräsentiert. Wenn Sie mit Nette arbeiten, wird dieses Objekt automatisch vom Framework erzeugt, und Sie können es sich per [Dependency Injection |dependency-injection:passing-dependencies] übergeben lassen. In Presentern genügt der Aufruf der Methode `$this->getHttpResponse()`. → [Installation und Anforderungen |@home#Installation] @@ -12,12 +12,12 @@ Die HTTP-Antwort wird durch das Objekt [api:Nette\Http\Response] repräsentiert. Nette\Http\Response =================== -Das Objekt ist im Gegensatz zu [Nette\Http\Request |request] mutable, d.h. Sie können den Zustand mithilfe von Settern ändern, z. B. Header senden. Vergessen Sie nicht, dass alle Setter **vor dem Senden jeglicher Ausgabe** aufgerufen werden müssen. Ob bereits eine Ausgabe gesendet wurde, verrät die Methode `isSent()`. Wenn sie `true` zurückgibt, löst jeder Versuch, einen Header zu senden, eine Ausnahme `Nette\InvalidStateException` aus. +Anders als [Nette\Http\Request |request] ist dieses Objekt veränderlich, Sie können den Zustand also mit Settern ändern, etwa um Header zu senden. Denken Sie daran, dass alle Setter **aufgerufen werden müssen, bevor irgendeine tatsächliche Ausgabe gesendet wird.** Die Methode `isSent()` sagt, ob die Ausgabe bereits gesendet wurde. Gibt sie `true` zurück, wirft jeder Versuch, einen Header zu senden, eine `Nette\InvalidStateException`. setCode(int $code, ?string $reason=null) .[method] -------------------------------------------------- -Ändert den [Antwort-Statuscode |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Zur besseren Lesbarkeit des Quellcodes empfehlen wir, für den Code statt Zahlen [vordefinierte Konstanten |api:Nette\Http\IResponse] zu verwenden. +Ändert den [Statuscode der Response |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Für die bessere Lesbarkeit des Quellcodes empfiehlt es sich, statt tatsächlicher Zahlen die [vordefinierten Konstanten |api:Nette\Http\IResponse] zu verwenden. ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -26,17 +26,17 @@ $httpResponse->setCode(Nette\Http\Response::S404_NotFound); getCode(): int .[method] ------------------------ -Gibt den Statuscode der Antwort zurück. +Gibt den Statuscode der Response zurück. isSent(): bool .[method] ------------------------ -Gibt zurück, ob bereits Header vom Server an den Browser gesendet wurden und es daher nicht mehr möglich ist, Header zu senden oder den Statuscode zu ändern. +Gibt zurück, ob die Header bereits vom Server an den Browser gesendet wurden, es also nicht mehr möglich ist, Header zu senden oder den Statuscode zu ändern. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Sendet einen HTTP-Header und **überschreibt** einen zuvor gesendeten Header mit demselben Namen. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Sendet einen HTTP-Header und **überschreibt** einen zuvor gesendeten Header desselben Namens. Ist `$value` gleich `null`, wird der Header entfernt. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -Sendet einen HTTP-Header und **überschreibt nicht** einen zuvor gesendeten Header mit demselben Namen. +Sendet einen HTTP-Header und **überschreibt keinen** zuvor gesendeten Header desselben Namens. ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -60,15 +60,15 @@ Löscht einen zuvor gesendeten HTTP-Header. getHeader(string $header): ?string .[method] -------------------------------------------- -Gibt den gesendeten HTTP-Header zurück oder `null`, wenn keiner existiert. Der Parameter ist case-insensitive. +Gibt den gesendeten HTTP-Header zurück oder `null`, wenn er nicht existiert. Beim Parameter wird die Groß- und Kleinschreibung nicht unterschieden. ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ +getHeaders(): array<string, string> .[method] +--------------------------------------------- Gibt alle gesendeten HTTP-Header als assoziatives Array zurück. ```php @@ -79,7 +79,7 @@ echo $headers['Pragma']; setContentType(string $type, ?string $charset=null) .[method] ------------------------------------------------------------- -Ändert den `Content-Type`-Header. +Ändert den Header `Content-Type`. ```php $httpResponse->setContentType('text/plain', 'UTF-8'); @@ -88,7 +88,7 @@ $httpResponse->setContentType('text/plain', 'UTF-8'); redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- -Leitet zu einer anderen URL weiter. Vergessen Sie nicht, das Skript danach zu beenden. +Leitet auf eine andere URL weiter. Denken Sie daran, das Skript danach zu beenden. ```php $httpResponse->redirect('http://example.com'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- -Legt die Ablaufzeit des HTTP-Dokuments mithilfe der Header `Cache-Control` und `Expires` fest. Der Parameter ist entweder ein Zeitintervall (als Text) oder `null`, was das Caching deaktiviert. +setExpiration(?string $expire) .[method] +---------------------------------------- +Setzt die Ablaufzeit des HTTP-Dokuments über die Header `Cache-Control` und `Expires`. Der Parameter ist entweder ein Zeitintervall (als Text) oder `null`, was das Caching abschaltet. ```php -// Cache im Browser läuft in einer Stunde ab +// der Browser-Cache läuft in einer Stunde ab $httpResponse->setExpiration('1 hour'); ``` sendAsFile(string $fileName) .[method] -------------------------------------- -Die Antwort wird über das Dialogfeld *Speichern unter* unter dem angegebenen Namen heruntergeladen. Die Datei selbst wird dabei nicht gesendet. +Die Response wird über einen *Speichern unter*-Dialog mit dem angegebenen Namen heruntergeladen. Die Datei selbst wird nicht gesendet. ```php -$httpResponse->sendAsFile('rechnung.pdf'); +$httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Sendet ein Cookie. Standardwerte der Parameter: -| `$path` | `'/'` | Cookie ist für alle Pfade in der (Sub-)Domain gültig *(konfigurierbar)* -| `$domain` | `null` | bedeutet Gültigkeit für die aktuelle (Sub-)Domain, aber nicht deren Subdomains *(konfigurierbar)* -| `$secure` | `true` | wenn die Website über HTTPS läuft, sonst `false` *(konfigurierbar)* -| `$httpOnly` | `true` | Cookie ist für JavaScript unzugänglich -| `$sameSite` | `'Lax'` | Cookie muss möglicherweise nicht beim [Zugriff von einer anderen Domain |nette:glossary#SameSite-Cookie] gesendet werden +| `$path` | `'/'` | das Cookie ist für alle Pfade innerhalb der (Sub-)Domain verfügbar *(konfigurierbar)* +| `$domain` | `null` | also verfügbar für die aktuelle (Sub-)Domain, aber nicht deren Subdomains *(konfigurierbar)* +| `$secure` | `auto` | `true`, wenn die Site über HTTPS läuft, sonst `false` (Standard im Framework; die bloße Klasse hat den Standardwert `false`) *(konfigurierbar)* +| `$httpOnly` | `true` | das Cookie ist für JavaScript unzugänglich +| `$sameSite` | `'Lax'` | das Cookie wird beim [Cross-Origin-Zugriff |nette:glossary#SameSite-Cookie] möglicherweise nicht gesendet +| `$partitioned` | `false` | ob das Cookie partitioniert ist, siehe unten *(seit v3.4)* Die Standardwerte der Parameter `$path`, `$domain` und `$secure` können Sie in der [Konfiguration |configuration#HTTP-Cookie] ändern. -Die Zeit kann als Anzahl von Sekunden oder als String angegeben werden: +Die Ablaufzeit wird als Anzahl von Sekunden, als Textintervall oder Datum oder als Objekt vom Typ `DateTimeInterface` übergeben. Der Wert `null` erzeugt ein Session-Cookie, das der Browser beim Schließen verwirft. Nette sendet die Ablaufzeit sowohl im Attribut `Expires` als auch in `Max-Age`. ```php -$httpResponse->setCookie('lang', 'cs', '100 days'); +$httpResponse->setCookie('lang', 'en', '100 days'); // läuft in 100 Tagen ab +$httpResponse->setCookie('lang', 'en', null); // Session-Cookie ``` -Der Parameter `$domain` bestimmt, welche Domains Cookies akzeptieren können. Wenn er nicht angegeben ist, akzeptiert die gleiche (Sub-)Domain, die das Cookie gesetzt hat, aber nicht deren Subdomains, das Cookie. Wenn `$domain` angegeben ist, sind auch Subdomains enthalten. Daher ist die Angabe von `$domain` weniger restriktiv als das Weglassen. Zum Beispiel sind bei `$domain = 'nette.org'` Cookies auch auf allen Subdomains wie `doc.nette.org` verfügbar. +Der Parameter `$domain` bestimmt, welche Domains das Cookie annehmen dürfen. Wird er nicht angegeben, nimmt es dieselbe (Sub-)Domain an, die es gesetzt hat, aber nicht deren Subdomains. Ist `$domain` angegeben, sind auch die Subdomains eingeschlossen. Die Angabe von `$domain` ist also weniger einschränkend als ihr Weglassen. Mit `$domain = 'nette.org'` sind die Cookies zum Beispiel auch auf allen Subdomains wie `doc.nette.org` verfügbar. + +Den Wert `$sameSite` können Sie als Enum `Nette\Http\SameSite` übergeben - `SameSite::Lax`, `SameSite::Strict` oder `SameSite::None` (die String-Werte `'Lax'`, `'Strict'`, `'None'` funktionieren ebenfalls). Setzen Sie ihn auf `SameSite::None`, wird das Attribut `$secure` automatisch aktiviert, denn Browser weisen ein Cookie mit `SameSite=None` ab, das nicht secure ist. + +.{data-version:3.4.0} +Partitionierte Cookies (CHIPS) geben einem Cookie für jede Top-Level-Site einen eigenen, getrennten Speicher. Setzt also ein Drittanbieterdienst (etwa ein eingebettetes Widget) ein partitioniertes Cookie, hält der Browser für jede Site, auf der das Widget erscheint, eine eigene Kopie, und diese Kopien lassen sich nicht zum seitenübergreifenden Tracking verknüpfen. Sie schalten es ein, indem Sie `$partitioned` auf `true` setzen; das erfordert außerdem das Attribut `$secure`, das deshalb automatisch aktiviert wird. -Für den Wert `$sameSite` können Sie die Konstanten `Response::SameSiteLax`, `SameSiteStrict` und `SameSiteNone` verwenden. +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- -Löscht ein Cookie. Standardwerte der Parameter sind: -- `$path` mit Gültigkeit für alle Verzeichnisse (`'/'`) -- `$domain` mit Gültigkeit für die aktuelle (Sub-)Domain, aber nicht deren Subdomains -- `$secure` richtet sich nach den Einstellungen in der [Konfiguration |configuration#HTTP-Cookie] +Löscht ein Cookie. Die Standardwerte der Parameter sind: +- `$path` mit Geltung für alle Verzeichnisse (`'/'`) +- `$domain` mit Geltung für die aktuelle (Sub-)Domain, aber nicht deren Subdomains +- `$secure` hängt von den Einstellungen in der [Konfiguration |configuration#HTTP-Cookie] ab ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +Das Objekt [api:Nette\Http\Context] verbindet Request und Response miteinander und hilft beim HTTP-Caching. Es ist nicht als Service registriert, Sie erzeugen es also selbst. In Presentern ist es meist einfacher, die Methode [lastModified() |application:presenters#HTTP-Caching] zu verwenden; der Context ist nützlich, wenn Sie die Response selbst senden, zum Beispiel aus einer eigenen Response-Klasse. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Stellt fest, ob sich der Inhalt seit dem letzten Besuch des Clients geändert hat. Übergeben Sie die Zeit der letzten Änderung, sendet sie den Header `Last-Modified`; übergeben Sie einen ETag-Validator (einen kurzen String, der die aktuelle Version des Inhalts kennzeichnet, z. B. dessen Hash), sendet sie den Header `ETag`. Anschließend vergleicht sie beides mit den vom Browser gesendeten Headern `If-Modified-Since` und `If-None-Match`. + +Hat der Browser bereits eine passende Version, setzt die Methode den Code `304 Not Modified` und gibt `false` zurück - senden Sie den Body der Response in diesem Fall gar nicht erst. Andernfalls gibt sie `true` zurück. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Beide Parameter sind optional. Wenn Sie die Änderungszeit des Inhalts nicht kennen, verwenden Sie nur den ETag und umgekehrt. diff --git a/http/de/sessions.texy b/http/de/sessions.texy index 4d0552c62a..e52cf8e99b 100644 --- a/http/de/sessions.texy +++ b/http/de/sessions.texy @@ -3,7 +3,7 @@ Sessions <div class=perex> -HTTP ist ein zustandsloses Protokoll, jedoch muss fast jede Anwendung den Zustand zwischen Anfragen aufrechterhalten, beispielsweise den Inhalt eines Warenkorbs. Genau dafür dienen Sessions oder Sitzungen. Wir zeigen Ihnen, +HTTP ist ein zustandsloses Protokoll; fast jede Anwendung muss jedoch den Zustand zwischen den Requests bewahren, etwa den Inhalt eines Warenkorbs. Genau dafür dienen Sessions. Wir zeigen Ihnen: - wie man Sessions verwendet - wie man Namenskonflikte vermeidet @@ -11,11 +11,11 @@ HTTP ist ein zustandsloses Protokoll, jedoch muss fast jede Anwendung den Zustan </div> -Bei der Verwendung von Sessions erhält jeder Benutzer eine eindeutige Kennung, die sogenannte Session-ID, die in einem Cookie übermittelt wird. Diese dient als Schlüssel zu den Session-Daten. Im Gegensatz zu Cookies, die auf der Browserseite gespeichert werden, werden die Daten in der Session auf der Serverseite gespeichert. +Bei der Verwendung von Sessions erhält jeder Benutzer einen eindeutigen Bezeichner, die Session-ID, die in einem Cookie übertragen wird. Sie dient als Schlüssel zu den Session-Daten. Anders als Cookies, die auf der Seite des Browsers gespeichert werden, liegen die Session-Daten auf der Seite des Servers. -Die Session wird in der [Konfiguration |configuration#Session] eingestellt, wichtig ist insbesondere die Wahl der Ablaufzeit. +Sessions konfigurieren wir in der [Konfiguration |configuration#Session]; besonders wichtig ist die Wahl der Ablaufzeit. -Die Verwaltung der Session übernimmt das Objekt [api:Nette\Http\Session], auf das Sie zugreifen können, indem Sie es sich mittels [Dependency Injection |dependency-injection:passing-dependencies] übergeben lassen. In Presentern reicht es aus, `$session = $this->getSession()` aufzurufen. +Um die Verwaltung der Session kümmert sich das Objekt [api:Nette\Http\Session], das Sie sich per [Dependency Injection |dependency-injection:passing-dependencies] übergeben lassen können. In Presentern genügt der Aufruf `$session = $this->getSession()`. → [Installation und Anforderungen |@home#Installation] @@ -23,49 +23,49 @@ Die Verwaltung der Session übernimmt das Objekt [api:Nette\Http\Session], auf d Session starten =============== -Nette startet die Session standardmäßig automatisch in dem Moment, in dem wir beginnen, Daten daraus zu lesen oder hineinzuschreiben. Manuell wird die Session mit `$session->start()` gestartet. +Standardmäßig startet Nette die Session automatisch in dem Moment, in dem wir beginnen, Daten aus ihr zu lesen oder in sie zu schreiben. Um eine Session manuell zu starten, verwenden Sie `$session->start()`. -PHP sendet beim Starten der Session HTTP-Header, die das Caching beeinflussen, siehe [php:session_cache_limiter], und gegebenenfalls auch ein Cookie mit der Session-ID. Daher ist es notwendig, die Session immer zu starten, bevor irgendeine Ausgabe an den Browser gesendet wird, andernfalls wird eine Ausnahme ausgelöst. Wenn Sie also wissen, dass während des Renderns der Seite die Session verwendet wird, starten Sie sie manuell vorher, zum Beispiel im Presenter. +PHP sendet beim Starten der Session HTTP-Header, die das Caching beeinflussen (siehe [php:session_cache_limiter]), und eventuell ein Cookie mit der Session-ID. Deshalb muss die Session immer gestartet werden, bevor irgendeine Ausgabe an den Browser gesendet wird; sonst wird eine Exception geworfen. Wenn Sie also wissen, dass beim Rendern der Seite eine Session verwendet wird, starten Sie sie vorher manuell, zum Beispiel im Presenter. -Im Entwicklermodus startet Tracy die Session, da sie diese zur Anzeige von Balken mit Weiterleitungen und AJAX-Anfragen in der Tracy Bar verwendet. +Im Entwicklungsmodus startet Tracy die Session, weil sie sie zur Anzeige der Bars für Weiterleitungen und AJAX-Requests in der Tracy Bar nutzt. Abschnitte ========== -In reinem PHP wird der Datenspeicher der Session als Array realisiert, das über die globale Variable `$_SESSION` zugänglich ist. Das Problem dabei ist, dass Anwendungen üblicherweise aus einer Reihe voneinander unabhängiger Teile bestehen, und wenn alle nur ein Array zur Verfügung haben, kommt es früher oder später zu Namenskollisionen. +In reinem PHP ist der Speicher für Session-Daten als Array umgesetzt, das über die globale Variable `$_SESSION` zugänglich ist. Das Problem ist, dass Anwendungen üblicherweise aus vielen unabhängigen Teilen bestehen, und wenn allen nur ein einziges Array zur Verfügung steht, kommt es früher oder später zu einer Namenskollision. -Das Nette Framework löst dieses Problem, indem es den gesamten Raum in Abschnitte (Objekte [api:Nette\Http\SessionSection]) unterteilt. Jede Einheit verwendet dann ihren eigenen Abschnitt mit einem eindeutigen Namen, und es kann zu keiner Kollision mehr kommen. +Das Nette Framework löst dieses Problem, indem es den gesamten Raum in Abschnitte aufteilt (Objekte vom Typ [api:Nette\Http\SessionSection]). Jede Einheit nutzt dann ihren eigenen Abschnitt mit einem eindeutigen Namen, und es kann zu keiner Kollision kommen. -Einen Abschnitt erhalten wir aus der Session: +Einen Abschnitt erhalten wir von der Session: ```php -$section = $session->getSection('eindeutigerName'); +$section = $session->getSection('eindeutiger Name'); ``` -Im Presenter genügt es, `getSession()` mit einem Parameter zu verwenden: +Im Presenter genügt `getSession()` mit einem Parameter: ```php -// $this ist Presenter -$section = $this->getSession('eindeutigerName'); +// $this ist ein Presenter +$section = $this->getSession('eindeutiger Name'); ``` -Die Existenz eines Abschnitts kann mit der Methode `$session->hasSection('eindeutigerName')` überprüft werden. +Ob ein Abschnitt existiert, lässt sich mit der Methode `$session->hasSection('eindeutiger Name')` prüfen. Eine Liste der Namen aller existierenden Abschnitte liefert `$session->getSectionNames()`. -Mit dem Abschnitt selbst arbeitet man dann sehr einfach mithilfe der Methoden `set()`, `get()` und `remove()`: +Die Arbeit mit dem Abschnitt selbst ist dann mit den Methoden `set()`, `get()` und `remove()` sehr einfach: ```php -// Variable schreiben -$section->set('userName', 'franta'); +// eine Variable schreiben +$section->set('userName', 'john'); -// Variable lesen, gibt null zurück, wenn sie nicht existiert +// eine Variable lesen, gibt null zurück, wenn sie nicht existiert echo $section->get('userName'); -// Variable löschen +// eine Variable entfernen $section->remove('userName'); ``` -Um alle Variablen aus einem Abschnitt zu erhalten, kann eine `foreach`-Schleife verwendet werden: +Um alle Variablen eines Abschnitts zu erhalten, können Sie eine `foreach`-Schleife verwenden: ```php foreach ($section as $key => $val) { @@ -77,34 +77,34 @@ foreach ($section as $key => $val) { Ablaufzeit einstellen --------------------- -Für einzelne Abschnitte oder sogar einzelne Variablen kann eine Ablaufzeit eingestellt werden. So können wir die Anmeldung eines Benutzers nach 20 Minuten ablaufen lassen, aber den Inhalt des Warenkorbs weiterhin speichern. +Die Ablaufzeit lässt sich für einzelne Abschnitte oder sogar für einzelne Variablen einstellen. Wir können die Anmeldung eines Benutzers nach 20 Minuten ablaufen lassen und uns den Inhalt des Warenkorbs trotzdem weiter merken. ```php -// Abschnitt läuft nach 20 Minuten ab +// der Abschnitt läuft nach 20 Minuten ab $section->setExpiration('20 minutes'); ``` -Zur Einstellung der Ablaufzeit einzelner Variablen dient der dritte Parameter der Methode `set()`: +Um die Ablaufzeit für einzelne Variablen zu setzen, verwenden Sie den dritten Parameter der Methode `set()`: ```php -// Variable 'flash' läuft bereits nach 30 Sekunden ab +// die Variable 'flash' läuft nach 30 Sekunden ab $section->set('flash', $message, '30 seconds'); ``` .[note] -Vergessen Sie nicht, dass die Ablaufzeit der gesamten Session (siehe [Session-Konfiguration |configuration#Session]) gleich oder länger sein muss als die bei einzelnen Abschnitten oder Variablen eingestellte Zeit. +Denken Sie daran, dass die Ablaufzeit der gesamten Session (siehe [Session-Konfiguration |configuration#Session]) gleich oder größer sein muss als die für einzelne Abschnitte oder Variablen gesetzte Zeit. -Das Löschen einer zuvor eingestellten Ablaufzeit erreichen wir mit der Methode `removeExpiration()`. Das sofortige Löschen des gesamten Abschnitts stellt die Methode `remove()` sicher. +Um eine zuvor gesetzte Ablaufzeit aufzuheben, verwenden Sie die Methode `removeExpiration()`; um die Ablaufzeit einer bestimmten Variablen zu löschen, übergeben Sie ihren Namen: `removeExpiration('flash')`. Um den gesamten Abschnitt sofort zu entfernen, verwenden Sie die Methode `remove()`. -Ereignisse $onStart, $onBeforeWrite ------------------------------------ +Events $onStart, $onBeforeWrite +------------------------------- -Das Objekt `Nette\Http\Session` hat die [Ereignisse |nette:glossary#Events Ereignisse] `$onStart` und `$onBeforeWrite`, Sie können also Callbacks hinzufügen, die nach dem Start der Session oder vor ihrem Schreiben auf die Festplatte und dem anschließenden Beenden aufgerufen werden. +Das Objekt `Nette\Http\Session` hat die [Events |nette:glossary#Events] `$onStart` und `$onBeforeWrite`, Sie können also Callbacks hinzufügen, die nach dem Start der Session bzw. vor dem Schreiben auf die Festplatte und dem anschließenden Beenden aufgerufen werden. ```php $session->onBeforeWrite[] = function () { - // Wir schreiben Daten in die Session + // Daten in die Session schreiben $this->section->set('basket', $this->basket); }; ``` @@ -113,7 +113,7 @@ $session->onBeforeWrite[] = function () { Session-Verwaltung ================== -Übersicht der Methoden der Klasse `Nette\Http\Session` zur Session-Verwaltung: +Übersicht der Methoden der Klasse `Nette\Http\Session` zur Verwaltung der Session: <div class=wiki-methods-brief> @@ -130,7 +130,7 @@ Ist die Session gestartet? close(): void .[method] ----------------------- -Beendet die Session. Die Session wird automatisch am Ende des Skriptlaufs beendet. +Beendet die Session. Die Session endet am Ende der Skriptausführung automatisch. destroy(): void .[method] @@ -140,12 +140,12 @@ Beendet und löscht die Session. exists(): bool .[method] ------------------------ -Enthält die HTTP-Anfrage ein Cookie mit einer Session-ID? +Enthält der HTTP-Request ein Cookie mit einer Session-ID? regenerateId(): void .[method] ------------------------------ -Generiert eine neue zufällige Session-ID. Die Daten bleiben erhalten. +Erzeugt eine neue zufällige Session-ID. Die Daten bleiben erhalten. getId(): string .[method] @@ -158,14 +158,14 @@ Gibt die Session-ID zurück. Konfiguration ------------- -Die Session wird in der [Konfiguration |configuration#Session] eingestellt. Wenn Sie eine Anwendung schreiben, die keinen DI-Container verwendet, dienen diese Methoden zur Konfiguration. Sie müssen aufgerufen werden, bevor die Session gestartet wird. +Die Session konfigurieren wir in der [Konfiguration |configuration#Session]. Wenn Sie eine Anwendung schreiben, die keinen DI-Container verwendet, nutzen Sie zur Konfiguration diese Methoden. Sie müssen vor dem Start der Session aufgerufen werden. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Legt den Namen des Cookies fest, in dem die Session-ID übertragen wird. Der Standardname ist `PHPSESSID`. Dies ist nützlich, wenn Sie mehrere unterschiedliche Anwendungen innerhalb einer Website betreiben. +Setzt den Namen des Cookies, in dem die Session-ID übertragen wird. Der Standardname ist `PHPSESSID`. Das ist nützlich, wenn Sie mehrere verschiedene Anwendungen auf derselben Website betreiben. getName(): string .[method] @@ -175,27 +175,27 @@ Gibt den Namen des Cookies zurück, in dem die Session-ID übertragen wird. setOptions(array $options): static .[method] -------------------------------------------- -Konfiguriert die Session. Es können alle PHP [Session-Direktiven |https://www.php.net/manual/en/session.configuration.php] (im CamelCase-Format, z.B. statt `session.save_path` schreiben wir `savePath`) sowie [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] eingestellt werden. +Konfiguriert die Session. Es lassen sich alle [Session-Direktiven |https://www.php.net/manual/en/session.configuration.php] von PHP setzen (im camelCase-Format, schreiben Sie also z. B. `savePath` statt `session.save_path`) und außerdem [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Legt die Inaktivitätsdauer fest, nach der die Session abläuft. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Setzt die Zeit der Inaktivität, nach der die Session abläuft. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Einstellung der Parameter für das Cookie. Die Standardwerte der Parameter können Sie in der [Konfiguration |configuration#Session-Cookie] ändern. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +Setzt die Parameter für Cookies. Die Standardwerte der Parameter können Sie in der [Konfiguration |configuration#Session-Cookie] ändern. setSavePath(string $path): static .[method] ------------------------------------------- -Legt das Verzeichnis fest, in dem die Session-Dateien gespeichert werden. +Setzt das Verzeichnis, in dem die Session-Dateien gespeichert werden. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Einstellung eines eigenen Handlers, siehe [PHP-Dokumentation |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Setzt einen eigenen Handler, siehe die [PHP-Dokumentation |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> @@ -203,9 +203,9 @@ Einstellung eines eigenen Handlers, siehe [PHP-Dokumentation |https://www.php.ne Sicherheit geht vor =================== -Der Server geht davon aus, dass er immer mit demselben Benutzer kommuniziert, solange die Anfragen von derselben Session-ID begleitet werden. Die Aufgabe der Sicherheitsmechanismen besteht darin, sicherzustellen, dass dies tatsächlich der Fall ist und es nicht möglich ist, die Kennung zu stehlen oder unterzuschieben. +Der Server geht davon aus, dass er mit demselben Benutzer kommuniziert, solange die Requests dieselbe Session-ID mitbringen. Aufgabe der Sicherheitsmechanismen ist es, dafür zu sorgen, dass das auch tatsächlich so ist und der Bezeichner sich weder stehlen noch austauschen lässt. -Das Nette Framework konfiguriert daher die PHP-Direktiven korrekt, um die Session-ID nur im Cookie zu übertragen, sie für JavaScript unzugänglich zu machen und eventuelle Kennungen in der URL zu ignorieren. Darüber hinaus generiert es in kritischen Momenten, wie z. B. bei der Benutzeranmeldung, eine neue Session-ID. +Das Nette Framework konfiguriert die PHP-Direktiven deshalb richtig, sodass die Session-ID nur in Cookies übertragen wird, für JavaScript unzugänglich ist und Bezeichner in der URL ignoriert werden. Außerdem erzeugt es in kritischen Momenten, etwa bei der Anmeldung eines Benutzers, eine neue Session-ID. .[note] -Zur Konfiguration von PHP wird die Funktion ini_set verwendet, die leider von einigen Hostern verboten wird. Wenn dies auch bei Ihrem Hoster der Fall ist, versuchen Sie, mit ihm zu vereinbaren, dass er Ihnen die Funktion erlaubt oder zumindest den Server konfiguriert. +Zur Konfiguration von PHP dient die Funktion `ini_set`, die manche Hoster leider verbieten. Wenn das bei Ihrem Hoster der Fall ist, versuchen Sie mit ihm zu vereinbaren, dass er Ihnen diese Funktion erlaubt oder den Server wenigstens richtig konfiguriert. diff --git a/http/de/ssrf.texy b/http/de/ssrf.texy new file mode 100644 index 0000000000..9bda836dd6 --- /dev/null +++ b/http/de/ssrf.texy @@ -0,0 +1,183 @@ +SSRF-Schutz +*********** + +.[perex] +Wenn Ihre Anwendung eine vom Benutzer angegebene URL herunterlädt, kann ein Angreifer das ausnutzen, um in Ihr internes Netzwerk zu gelangen. Die Klassen [#UrlValidator] und [#IPAddress] helfen Ihnen, sich gegen diese Angriffe vom Typ Server-Side Request Forgery (SSRF) zu schützen. + +→ [Installation und Anforderungen |@home#Installation] + + +Was ist SSRF? +============= + +Stellen Sie sich eine Funktion vor, bei der der Benutzer eine URL eingibt und Ihr Server sie herunterlädt - ein Avatar von einer entfernten Adresse, ein Webhook-Ziel, eine Linkvorschau. Das sieht harmlos aus, aber die Adresse ruft der Server auf, nicht der Browser des Benutzers. Und der Server sieht Orte, die der Angreifer nicht sieht: das Loopback-Interface, das private Netzwerk, Cloud-Dienste. + +Ein Angreifer schickt deshalb eine URL, die nach innen statt ins öffentliche Internet zeigt. Typische Ziele sind: + +- Cloud-Metadaten unter `http://169.254.169.254/`, die Zugangsschlüssel preisgeben können +- interne Adminoberflächen und Router wie `http://192.168.1.1/` +- Dienste ohne Authentifizierung, etwa Redis unter `http://localhost:6379/` + +Diese Klasse von Sicherheitslücken ist so verbreitet, dass sie zu den [OWASP Top 10 |https://owasp.org/Top10/] zählt. Die Abwehr besteht darin, die URL zu validieren, **bevor** Sie sie abrufen, und alles abzulehnen, was auf eine nicht öffentliche Adresse zeigt. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] prüft eine URL gegen eine konfigurierbare Policy: das Schema, den Port, den Host, die Userinfo und die IP-Adressen, auf die der Host auflöst. Die grundlegende Verwendung ist ein einziger Aufruf: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // unsichere URL, nicht abrufen +} +``` + +Die Standard-Policy ist bewusst streng - sie akzeptiert nur `https` auf Port 443, das auf eine öffentliche IP-Adresse zeigt. Alles andere (Loopback, private Bereiche, Link-Local einschließlich Cloud-Metadaten, reservierte Bereiche) wird abgelehnt, und Multicast wird bedingungslos abgelehnt. Das ist der richtige Ausgangspunkt für das Abrufen beliebiger vom Benutzer angegebener URLs. + + +Die Policy konfigurieren +------------------------ + +Die Policy formen Sie über den Konstruktor. Um zum Beispiel einfaches `http` auf beliebigen Ports zu erlauben und private Adressen zu erreichen (nützlich innerhalb eines vertrauenswürdigen Netzwerks): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // beliebiger Port + allowPrivateIps: true, +); +``` + +Ein häufiges Muster ist, das Abrufen mit einer Host-Allowlist auf eine feste Menge von Partnerdomains zu beschränken. Das Präfix `*.` passt auf beliebig viele Subdomain-Ebenen, aber nicht auf die Apex-Domain - führen Sie beide Formen auf, wenn Sie sie brauchen: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +Die vollständige Menge der Konstruktoroptionen: + +| Parameter | Standard | Bedeutung +|--------------------- +| `schemes` | `['https']` | erlaubte Schemata; `[]` lehnt alles ab +| `ports` | `[443]` | erlaubte Ports, `null` = beliebig; der implizite Port des Schemas wird berücksichtigt +| `allowPrivateIps` | `false` | erlaubt private Bereiche (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | erlaubt Loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | erlaubt Link-Local inkl. Cloud-Metadaten 169.254.169.254 +| `allowReserved` | `false` | erlaubt von der IANA reservierte Bereiche +| `allowUserinfo` | `false` | erlaubt `user:pass@` in der URL +| `hostAllowlist` | `null` | falls gesetzt, muss der Host auf ein Muster passen; `[]` lehnt alle ab +| `hostBlocklist` | `null` | falls gesetzt, darf der Host auf kein Muster passen + + +Methoden zur Validierung +------------------------ + +Der Validator bietet drei Methoden. `allows()` führt die vollständige Prüfung einschließlich der DNS-Auflösung durch - der Host wird aufgelöst, und **jede** A/AAAA-Adresse muss die IP-Policy bestehen: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` überspringt die DNS-Auflösung und die Prüfung der IP-Bereiche. Verwenden Sie es als schnellen Vorfilter oder wenn die DNS-Validierung an die Abrufschicht delegiert ist: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Beide Methoden nehmen einen String, ein Objekt [UrlImmutable |urls#UrlImmutable] oder `null` entgegen (was immer fehlschlägt). + + +DNS-Rebinding aushebeln +----------------------- + +Zwischen Validierung und Abruf gibt es eine feine Race Condition: Ein Angreifer kann bei der Validierung des Hosts eine sichere IP zurückgeben und das DNS dann für den eigentlichen Download auf eine interne IP umstellen. Um dieses Loch zu schließen, gibt `getResolvedIPs()` die validierten IP-Adressen zurück, und Sie binden die Verbindung an sie, sodass der Abruf nicht anderswohin umgelenkt werden kann: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // unsichere URL +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... den Request ausführen +``` + +Die Methode gibt ein Array von IP-Strings zurück (zuerst A-Records, dann AAAA), die die vollständige Policy bestanden haben, oder bei jedem Fehlschlag ein leeres Array. Für ein IP-Literal in der URL validiert sie die Adresse direkt und führt keine DNS-Abfrage durch. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] ist ein unveränderliches Value Object für die Arbeit mit IPv4- und IPv6-Adressen. `UrlValidator` verwendet es intern, aber es ist auch für sich nützlich, wann immer Sie Adressen klassifizieren. Der Konstruktor wirft bei einer ungültigen Adresse eine `Nette\InvalidArgumentException`: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +Wenn Sie keine Exception wollen, verwenden Sie die Factory `tryFrom()` oder die Prüfmethode `isValid()`: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Klassifizierung von Adressen +---------------------------- + +Die Prädikate sagen Ihnen, zu welcher Klasse eine Adresse gehört. Das wichtigste ist `isPublic()` - true nur für öffentlich routbare Adressen, und genau das will ein SSRF-Schutz: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (Bereich der Cloud-Metadaten) +``` + +Die vollständige Menge der Prädikate: + +| Methode | Prüft auf +|-------------------- +| `isPublic()` | öffentlich routbar (keines der folgenden) +| `isPrivate()` | private Bereiche nach RFC 1918 / 4193 +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (inkl. Cloud-Metadaten), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | von der IANA reserviert (Dokumentation, CGNAT, künftige Verwendung, …) + + +Zugehörigkeit zu einem Bereich +------------------------------ + +`isInRange()` prüft, ob die Adresse in einen CIDR-Block fällt. Sie können ein Netz mit Präfix übergeben oder eine bloße Adresse für einen exakten Vergleich (implizit /32 bei IPv4, /128 bei IPv6): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (exakter Vergleich) +``` + +Fehlerhafte Eingaben oder eine andere IP-Familie ergeben `false`. + + +IPv4-mapped IPv6 +---------------- + +Adressen, die als IPv4-mapped IPv6 geschrieben sind (etwa `::ffff:127.0.0.1`), sind ein klassischer Weg, an naiven Filtern vorbeizukommen. `IPAddress` normalisiert sie, sodass die Bereichsprädikate die Tarnung durchschauen: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +Die Methoden `isIPv4()` und `isIPv6()` beziehen sich auf die textuelle Form: Eine gemappte Adresse ist IPv6, nicht IPv4. diff --git a/http/de/upgrading.texy b/http/de/upgrading.texy new file mode 100644 index 0000000000..4bc51a3a18 --- /dev/null +++ b/http/de/upgrading.texy @@ -0,0 +1,54 @@ +Upgrade +******* + + +Upgrade auf Version 3.4 +======================= + +Die mindestens erforderliche PHP-Version ist 8.3. + +- die Methode `Request::isSameSite()` ist zugunsten von `isFrom()` veraltet, das die Herkunft des Requests anhand der `Sec-Fetch-*`-Header bestimmt. Der automatische Schutz von Formularen und Signalen wird dadurch genauer, und ein Verhalten ändert sich: Direkte Navigation (ein Lesezeichen, eine von Hand eingetippte Adresse, ein Link in einer E-Mail) gilt nicht mehr als Same-Site. Wenn ein Signal auf Aktionslinks in E-Mails angewiesen ist, kennzeichnen Sie es mit `#[Requires(sameOrigin: false)]`. +- das Cookie `_nss` wird nun nur noch an Browser gesendet, die den Header `Sec-Fetch-Site` nicht senden +- `setCookie()` sendet das Attribut `Max-Age` und erzwingt das Flag `Secure` für `SameSite=None` und für partitionierte Cookies +- das Enum `SameSite` ersetzt die Konstanten `IResponse::SameSiteLax` usw., die veraltet sind +- die Ablaufzeit wird überall gleich interpretiert: Eine Zahl ist eine relative Anzahl von Sekunden, ein String ein Intervall oder ein Datum. Die Übergabe eines absoluten UNIX-Timestamps ist veraltet, und ein Session-Cookie wird durch `null` statt `0` dargestellt. +- die veraltete Methode `Request::getRemoteHost()` gibt `null` zurück +- die seit Langem veraltete Klasse `Nette\Http\UserStorage` wurde entfernt + +Die ganze Geschichte der Umstellung auf die `Sec-Fetch-*`-Header erzählt der Artikel [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Upgrade auf Version 3.2 +======================= + +- die Zugangsdaten aus der HTTP-Basic-Authentifizierung sind nicht mehr Teil des `Url`-Objekts, `$url->getUser()` und `$url->getPassword()` geben also einen leeren String zurück. Lesen Sie sie mit der neuen Methode `$request->getBasicCredentials()`. + +Die Gründe für diese Änderung erklärt der Artikel [Nette Http 3.2: change access to credentials |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]. + + +Upgrade auf Version 3.1 +======================= + +- Cookies werden mit dem Flag `sameSite: Lax` gesendet +- `cookieSecure` hat nun den Standardwert 'auto' +- die Option `session.cookieSecure` ist veraltet; stattdessen wird `http.cookieSecure` verwendet +- das Cookie `nette-samesite` wurde in `_nss` umbenannt +- `Nette\Http\Request::getFile()` nimmt ein Array von Schlüsseln entgegen und gibt `FileUpload|null` zurück +- `Nette\Http\Session::getCookieParameters()` ist veraltet +- `Nette\Http\FileUpload::getName()` wurde in `getUntrustedName()` umbenannt +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()` und `getRelativeUrl()` sind veraltet (diese Methoden gehören zu `UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` ist veraltet +- `Nette\Http\FileUpload::getImageSize()` gibt das Paar `[Breite, Höhe]` zurück +- mit `autoStart: smart` (dem Standard) startet die Session nicht mehr gleich nach dem Start der Anwendung, nur weil der Browser ein Session-Cookie gesendet hat; sie startet beim ersten Lesen oder Schreiben. Die Werte `always` und `never` kamen hinzu. +- wenn der Browser eine Session-ID sendet, zu der keine Session existiert, löscht Nette das Cookie, statt eine neue Session anzulegen +- für den Zugriff auf Session-Abschnitte sind die Methoden `set()`, `get()` und `remove()` vorzuziehen; anders als der Zugriff über Properties unterscheiden sie korrekt zwischen Lesen und Schreiben und starten die Session nicht unnötig +- die Standardwerte von `cookiePath` und `cookieDomain` lassen sich in der Konfiguration setzen + +Das Verhalten der Sessions beschreibt der Artikel [Nette Http 3.1: much smarter sessions |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions] ausführlich. + + +Upgrade auf Version 3.0 +======================= + +- das Objekt `Nette\Http\UrlScript` (das z. B. `Nette\Http\Request::getUrl()` zurückgibt) ist nun unveränderlich +- in `new Nette\Http\Url('abcd')` steht `abcd` für den Pfad, nicht für die Domain; seit 3.0 erzeugt `(new Nette\Http\Url('abcd'))->setScheme('http')` korrekt `http:abcd` statt des früheren `http://abcd` diff --git a/http/de/urls.texy b/http/de/urls.texy index 9f36fb4411..135a07eec8 100644 --- a/http/de/urls.texy +++ b/http/de/urls.texy @@ -2,7 +2,7 @@ Arbeiten mit URLs ***************** .[perex] -Die Klassen [#Url], [#UrlImmutable] und [#UrlScript] ermöglichen das einfache Generieren, Parsen und Manipulieren von URLs. +Die Klassen [#Url], [#UrlImmutable] und [#UrlScript] erleichtern das Erzeugen, Parsen und Verändern von URLs. → [Installation und Anforderungen |@home#Installation] @@ -10,7 +10,7 @@ Die Klassen [#Url], [#UrlImmutable] und [#UrlScript] ermöglichen das einfache G Url === -Die Klasse [api:Nette\Http\Url] ermöglicht das einfache Arbeiten mit URLs und ihren einzelnen Komponenten, die diese Skizze erfasst: +Die Klasse [api:Nette\Http\Url] erlaubt die bequeme Arbeit mit URLs und ihren einzelnen Bestandteilen, wie dieses Diagramm zeigt: /--pre scheme user password host port path query fragment @@ -22,7 +22,7 @@ Die Klasse [api:Nette\Http\Url] ermöglicht das einfache Arbeiten mit URLs und i hostUrl authority \-- -Das Generieren von URLs ist intuitiv: +Das Erzeugen von URLs ist intuitiv: ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -Es ist auch möglich, eine URL zu parsen und weiter zu manipulieren: +Sie können eine URL auch parsen und dann verändern: ```php $url = new Url( @@ -44,7 +44,7 @@ $url = new Url( ); ``` -Die Klasse `Url` implementiert die Schnittstelle `JsonSerializable` und hat die Methode `__toString()`, sodass das Objekt ausgegeben oder in Daten verwendet werden kann, die an `json_encode()` übergeben werden. +Die Klasse `Url` implementiert das Interface `JsonSerializable` und hat eine Methode `__toString()`, das Objekt lässt sich also ausgeben oder in Daten verwenden, die an `json_encode()` übergeben werden. ```php echo $url; @@ -52,13 +52,13 @@ echo json_encode([$url]); ``` -URL-Komponenten .[method] -------------------------- +URL-Komponenten +--------------- -Zum Abrufen oder Ändern einzelner URL-Komponenten stehen Ihnen diese Methoden zur Verfügung: +Zum Auslesen oder Ändern der einzelnen URL-Komponenten stehen folgende Methoden zur Verfügung: .[language-php] -| Setter | Getter | Zurückgegebener Wert +| Setter | Getter | Rückgabewert |-------------------------------------------------------------------------------------------- | `setScheme(string $scheme)` | `getScheme(): string` | `'http'` | `setUser(string $user)` | `getUser(): string` | `'john'` @@ -71,17 +71,20 @@ Zum Abrufen oder Ändern einzelner URL-Komponenten stehen Ihnen diese Methoden z | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | ganze URL +| | `getAbsoluteUrl(): string` | die gesamte URL -Warnung: Wenn Sie mit einer URL arbeiten, die aus einer [HTTP-Anfrage |request] stammt, beachten Sie, dass sie kein Fragment enthalten wird, da der Browser es nicht an den Server sendet. +Die Methoden `getUser()`, `getPassword()`, `setUser()` und `setPassword()` sind veraltet, weil vom Einbetten von Zugangsdaten direkt in die URL abgeraten wird. -Wir können auch mit einzelnen Query-Parametern arbeiten mittels: +Achtung: Wenn Sie mit einer URL arbeiten, die Sie aus einem [HTTP-Request |request] erhalten haben, denken Sie daran, dass sie das Fragment nicht enthält, denn der Browser sendet es nicht an den Server. + +Mit den einzelnen Query-Parametern können wir außerdem so arbeiten: .[language-php] | Setter | Getter |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] @@ -98,18 +101,23 @@ Gibt den rechten oder linken Teil des Hosts zurück. So funktioniert es, wenn de | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Überprüft, ob zwei URLs identisch sind. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +Prüft, ob zwei URLs identisch sind. ```php $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Wandelt die URL in die kanonische Form um. Dabei wird der Hostname in Kleinbuchstaben umgewandelt und der Pfad normalisiert (Percent-Encoding und Entfernen überflüssiger Zeichen). Der Query-String bleibt unverändert. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- -Überprüft, ob die URL absolut ist. Eine URL wird als absolut betrachtet, wenn sie mit einem Schema (z. B. http, https, ftp) gefolgt von einem Doppelpunkt beginnt. +Prüft, ob eine URL absolut ist. Eine URL gilt als absolut, wenn sie mit einem Schema (z. B. http, https, ftp) gefolgt von einem Doppelpunkt beginnt. ```php Url::isAbsolute('https://nette.org'); // true @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -Normalisiert den Pfad in einer URL durch Entfernen der speziellen Segmente `.` und `..`. Die Methode entfernt überflüssige Pfadelemente auf die gleiche Weise, wie es Webbrowser tun. +Normalisiert einen URL-Pfad, indem die speziellen Segmente `.` und `..` entfernt werden. Diese Methode entfernt überflüssige Pfadelemente auf dieselbe Weise wie Webbrowser. ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,24 +139,24 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -Die Klasse [api:Nette\Http\UrlImmutable] ist eine immutable (unveränderliche) Alternative zur Klasse [#Url] (ähnlich wie in PHP `DateTimeImmutable` eine unveränderliche Alternative zu `DateTime` ist). Anstelle von Settern hat sie sogenannte Wither, die das Objekt nicht ändern, sondern neue Instanzen mit dem geänderten Wert zurückgeben: +Die Klasse [api:Nette\Http\UrlImmutable] ist eine unveränderliche Alternative zur Klasse [#Url] (ähnlich wie `DateTimeImmutable` in PHP eine unveränderliche Alternative zu `DateTime` ist). Statt Settern hat sie "Wither", die das Objekt nicht verändern, sondern neue Instanzen mit dem geänderten Wert zurückgeben: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` -Die Klasse `UrlImmutable` implementiert die Schnittstelle `JsonSerializable` und hat die Methode `__toString()`, sodass das Objekt ausgegeben oder in Daten verwendet werden kann, die an `json_encode()` übergeben werden. +Die Klasse `UrlImmutable` implementiert das Interface `JsonSerializable` und hat eine Methode `__toString()`, das Objekt lässt sich also ausgeben oder in Daten verwenden, die an `json_encode()` übergeben werden. ```php echo $url; @@ -156,13 +164,13 @@ echo json_encode([$url]); ``` -URL-Komponenten .[method] -------------------------- +URL-Komponenten +--------------- -Zum Abrufen oder Ändern einzelner URL-Komponenten dienen Methoden: +Zum Auslesen oder Ändern der einzelnen URL-Komponenten stehen folgende Methoden zur Verfügung: .[language-php] -| Wither | Getter | Zurückgegebener Wert +| Wither | Getter | Rückgabewert |-------------------------------------------------------------------------------------------- | `withScheme(string $scheme)` | `getScheme(): string` | `'http'` | `withUser(string $user)` | `getUser(): string` | `'john'` @@ -175,11 +183,11 @@ Zum Abrufen oder Ändern einzelner URL-Komponenten dienen Methoden: | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | ganze URL +| | `getAbsoluteUrl(): string` | die gesamte URL -Die Methode `withoutUserInfo()` entfernt `user` und `password`. +Die Methoden `getUser()`, `getPassword()`, `withUser()`, `withPassword()` und `withoutUserInfo()` sind veraltet, weil vom Einbetten von Zugangsdaten direkt in die URL abgeraten wird. -Wir können auch mit einzelnen Query-Parametern arbeiten mittels: +Mit den einzelnen Query-Parametern können wir außerdem so arbeiten: .[language-php] | Wither | Getter @@ -204,11 +212,11 @@ Gibt den rechten oder linken Teil des Hosts zurück. So funktioniert es, wenn de resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -Leitet eine absolute URL auf die gleiche Weise ab, wie ein Browser Links auf einer HTML-Seite verarbeitet: -- wenn der Link eine absolute URL ist (Schema enthält), wird er unverändert verwendet -- wenn der Link mit `//` beginnt, wird nur das Schema aus der aktuellen URL übernommen -- wenn der Link mit `/` beginnt, wird ein absoluter Pfad vom Domain-Stamm erstellt -- in anderen Fällen wird die URL relativ zum aktuellen Pfad zusammengestellt +Löst eine absolute URL auf dieselbe Weise auf, wie ein Browser Links auf einer HTML-Seite verarbeitet: +- ist der Link eine absolute URL (enthält ein Schema), wird er unverändert verwendet +- beginnt der Link mit `//`, wird nur das Schema der aktuellen URL übernommen +- beginnt der Link mit `/`, entsteht ein absoluter Pfad vom Wurzelverzeichnis der Domain +- in den übrigen Fällen wird die URL relativ zum aktuellen Pfad zusammengesetzt ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,9 +226,9 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Überprüft, ob zwei URLs identisch sind. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +Prüft, ob zwei URLs identisch sind. ```php $url->isEqual('https://nette.org'); @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -Die Klasse [api:Nette\Http\UrlScript] ist ein Nachkomme von [#UrlImmutable] und erweitert diese um weitere virtuelle URL-Komponenten, wie das Stammverzeichnis des Projekts usw. Wie die übergeordnete Klasse ist sie ein immutables (unveränderliches) Objekt. +Die Klasse [api:Nette\Http\UrlScript] ist ein Nachkomme von [#UrlImmutable] und erweitert sie um weitere virtuelle URL-Komponenten, etwa das Wurzelverzeichnis des Projekts usw. Wie ihre Elternklasse ist sie ein unveränderliches Objekt. -Das folgende Diagramm zeigt die Komponenten, die UrlScript erkennt: +Das folgende Diagramm zeigt die Komponenten, die UrlScript kennt: /--pre baseUrl basePath relativePath relativeUrl @@ -244,23 +252,23 @@ Das folgende Diagramm zeigt die Komponenten, die UrlScript erkennt: scriptPath pathInfo \-- -- `baseUrl` ist die Basis-URL der Anwendung einschließlich Domain und Pfadteil zum Stammverzeichnis der Anwendung -- `basePath` ist der Pfadteil zum Stammverzeichnis der Anwendung +- `baseUrl` ist die Basis-URL der Anwendung, einschließlich Domain und Pfadteil zum Wurzelverzeichnis der Anwendung +- `basePath` ist der Pfadteil zum Wurzelverzeichnis der Anwendung - `scriptPath` ist der Pfad zum aktuellen Skript -- `relativePath` ist der Name des Skripts (ggf. weitere Pfadsegmente) relativ zum basePath -- `relativeUrl` ist der gesamte Teil der URL nach baseUrl, einschließlich Query-String und Fragment. -- `pathInfo` ist ein heute selten genutzter Teil der URL nach dem Skriptnamen +- `relativePath` ist der Name des Skripts (und eventuell weitere Pfadsegmente) relativ zu `basePath` +- `relativeUrl` ist der gesamte Teil der URL nach `baseUrl`, einschließlich Query-String und Fragment +- `pathInfo` ist ein heute selten genutzter Teil der URL nach dem Namen des Skripts -Zum Abrufen von URL-Teilen stehen Methoden zur Verfügung: +Zum Auslesen dieser Teile der URL stehen folgende Methoden zur Verfügung: .[language-php] -| Getter | Zurückgegebener Wert +| Getter | Rückgabewert |------------------------------------------------ | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Objekte `UrlScript` erstellen wir normalerweise nicht direkt, sondern sie werden von der Methode [Nette\Http\Request::getUrl() |request] mit bereits korrekt eingestellten Komponenten für die aktuelle HTTP-Anfrage zurückgegeben. +Objekte vom Typ `UrlScript` erzeugen wir normalerweise nicht direkt; stattdessen gibt die Methode [Nette\Http\Request::getUrl() |request] eines zurück, dessen Komponenten für den aktuellen HTTP-Request bereits korrekt gesetzt sind. diff --git a/http/el/@home.texy b/http/el/@home.texy deleted file mode 100644 index 7f99e07aa7..0000000000 --- a/http/el/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Το πακέτο `nette/http` ενσωματώνει το [HTTP request |request] & [response], την εργασία με [sessions] και την [ανάλυση και σύνθεση URL |urls]. - - -Εγκατάσταση ------------ - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/el/@left-menu.texy b/http/el/@left-menu.texy deleted file mode 100644 index a8ca399134..0000000000 --- a/http/el/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Εισαγωγή |@home] -- [HTTP request|request] -- [HTTP response|response] -- [Sessions] -- [Βοηθητικά προγράμματα URL |urls] -- [Διαμόρφωση |configuration] diff --git a/http/el/@meta.texy b/http/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/http/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/http/el/configuration.texy b/http/el/configuration.texy deleted file mode 100644 index 828c56c70c..0000000000 --- a/http/el/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Διαμόρφωση HTTP -*************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette HTTP. - -Εάν δεν χρησιμοποιείτε ολόκληρο το framework, αλλά μόνο αυτή τη βιβλιοθήκη, διαβάστε [πώς να φορτώσετε τη διαμόρφωση|bootstrap:]. - - -Κεφαλίδες HTTP -============== - -```neon -http: - # κεφαλίδες που αποστέλλονται με κάθε αίτημα - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # επηρεάζει την κεφαλίδα X-Frame-Options - frames: ... # (string|bool) προεπιλογή είναι 'SAMEORIGIN' -``` - -Για λόγους ασφαλείας, το framework στέλνει την κεφαλίδα `X-Frame-Options: SAMEORIGIN`, η οποία δηλώνει ότι η σελίδα μπορεί να εμφανιστεί μέσα σε άλλη σελίδα (στο στοιχείο `<iframe>`) μόνο εάν βρίσκεται στο ίδιο domain. Αυτό μπορεί να είναι ανεπιθύμητο σε ορισμένες περιπτώσεις (για παράδειγμα, εάν αναπτύσσετε μια εφαρμογή για το Facebook), οπότε η συμπεριφορά μπορεί να αλλάξει ορίζοντας `frames: http://allowed-host.com` ή `frames: true`. - - -Πολιτική Ασφάλειας Περιεχομένου -------------------------------- - -Μπορείτε εύκολα να δημιουργήσετε κεφαλίδες `Content-Security-Policy` (εφεξής CSP), η περιγραφή τους βρίσκεται στην [περιγραφή CSP |https://content-security-policy.com]. Οι οδηγίες CSP (όπως `script-src`) μπορούν να γραφτούν είτε ως συμβολοσειρές σύμφωνα με την προδιαγραφή, είτε ως πίνακες τιμών για καλύτερη αναγνωσιμότητα. Τότε δεν χρειάζεται να γράψετε εισαγωγικά γύρω από λέξεις-κλειδιά όπως `'self'`. Το Nette δημιουργεί επίσης αυτόματα μια τιμή `nonce`, οπότε η κεφαλίδα θα περιέχει κάτι σαν `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # συμβολοσειρά στη μορφή σύμφωνα με την προδιαγραφή CSP - default-src: "'self' https://example.com" - - # πίνακας τιμών - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool στην περίπτωση διακοπτών - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -Στα templates, χρησιμοποιήστε `<script n:nonce>...</script>` και η τιμή nonce θα συμπληρωθεί αυτόματα. Η δημιουργία ασφαλών ιστότοπων στο Nette είναι πραγματικά εύκολη. - -Ομοίως, μπορείτε να δημιουργήσετε κεφαλίδες `Content-Security-Policy-Report-Only` (οι οποίες μπορούν να χρησιμοποιηθούν παράλληλα με το CSP) και [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP cookie ------------ - -Μπορείτε να αλλάξετε τις προεπιλεγμένες τιμές ορισμένων παραμέτρων της μεθόδου [Nette\Http\Response::setCookie() |response#setCookie] και του session. - -```neon -http: - # εμβέλεια cookie ανά διαδρομή - cookiePath: ... # (string) προεπιλογή είναι '/' - - # domains που δέχονται cookies - cookieDomain: 'example.com' # (string|domain) προεπιλογή είναι μη ορισμένο - - # αποστολή cookies μόνο μέσω HTTPS; - cookieSecure: ... # (bool|auto) προεπιλογή είναι auto - - # απενεργοποιεί την αποστολή του cookie που χρησιμοποιείται από το Nette για προστασία από CSRF - disableNetteCookie: ... # (bool) προεπιλογή είναι false -``` - -Το attribute `cookieDomain` καθορίζει ποια domains μπορούν να δέχονται cookies. Εάν δεν καθοριστεί, το cookie γίνεται αποδεκτό από το ίδιο (υπο)domain που το όρισε, *αλλά όχι* από τα υποdomains του. Εάν το `cookieDomain` καθοριστεί, περιλαμβάνονται και τα υποdomains. Επομένως, ο καθορισμός του `cookieDomain` είναι λιγότερο περιοριστικός από την παράλειψή του. - -Για παράδειγμα, με `cookieDomain: nette.org`, τα cookies είναι επίσης διαθέσιμα σε όλα τα υποdomains όπως το `doc.nette.org`. Αυτό μπορεί επίσης να επιτευχθεί χρησιμοποιώντας την ειδική τιμή `domain`, δηλαδή `cookieDomain: domain`. - -Η προεπιλεγμένη τιμή `auto` για το attribute `cookieSecure` σημαίνει ότι εάν ο ιστότοπος εκτελείται σε HTTPS, τα cookies θα αποστέλλονται με τη σημαία `Secure` και επομένως θα είναι διαθέσιμα μόνο μέσω HTTPS. - - -HTTP proxy ----------- - -Εάν ο ιστότοπος εκτελείται πίσω από ένα HTTP proxy, καθορίστε τη διεύθυνση IP του, ώστε η ανίχνευση σύνδεσης μέσω HTTPS και η διεύθυνση IP του client να λειτουργούν σωστά. Δηλαδή, ώστε οι συναρτήσεις [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] και [isSecured() |request#isSecured] να επιστρέφουν τις σωστές τιμές και οι σύνδεσμοι με το πρωτόκολλο `https:` να δημιουργούνται στα templates. - -```neon -http: - # Διεύθυνση IP, εύρος (π.χ., 127.0.0.1/8), ή πίνακας αυτών των τιμών - proxy: 127.0.0.1 # (string|string[]) προεπιλογή είναι μη ορισμένο -``` - - -Session -======= - -Βασικές ρυθμίσεις [sessions |sessions]: - -```neon -session: - # εμφάνιση του πίνακα session στο Tracy Bar; - debugger: ... # (bool) προεπιλογή είναι false - - # χρόνος αδράνειας μετά τον οποίο λήγει το session - expiration: 14 days # (string) προεπιλογή είναι '3 hours' - - # πότε πρέπει να ξεκινήσει το session; - autoStart: ... # (smart|always|never) προεπιλογή είναι 'smart' - - # handler, μια υπηρεσία που υλοποιεί το interface SessionHandlerInterface - handler: @handlerService -``` - -Η επιλογή `autoStart` ελέγχει πότε πρέπει να ξεκινήσει το session. Η τιμή `always` σημαίνει ότι το session θα ξεκινά πάντα με την εκκίνηση της εφαρμογής. Η τιμή `smart` σημαίνει ότι το session θα ξεκινά κατά την εκκίνηση της εφαρμογής μόνο εάν υπάρχει ήδη, ή τη στιγμή που θέλουμε να διαβάσουμε ή να γράψουμε σε αυτό. Τέλος, η τιμή `never` απενεργοποιεί την αυτόματη έναρξη του session. - -Επιπλέον, μπορείτε να ορίσετε όλες τις PHP [session directives |https://www.php.net/manual/en/session.configuration.php] (σε μορφή camelCase) καθώς και το [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Παράδειγμα: - -```neon -session: - # γράψτε το 'session.name' ως 'name' - name: MYID - - # γράψτε το 'session.save_path' ως 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Session cookie --------------- - -Το session cookie αποστέλλεται με τις ίδιες παραμέτρους όπως [άλλα cookie |#HTTP cookie], αλλά μπορείτε να τις αλλάξετε για αυτό: - -```neon -session: - # domains που δέχονται cookies - cookieDomain: 'example.com' # (string|domain) - - # περιορισμός κατά την πρόσβαση από άλλο domain - cookieSamesite: None # (Strict|Lax|None) προεπιλογή είναι Lax -``` - -Το attribute `cookieSamesite` επηρεάζει εάν το cookie θα αποσταλεί κατά την [πρόσβαση από άλλο domain |nette:glossary#SameSite cookie], το οποίο παρέχει κάποια προστασία έναντι επιθέσεων [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Υπηρεσίες DI -============ - -Αυτές οι υπηρεσίες προστίθενται στο DI container: - -| Όνομα | Τύπος | Περιγραφή -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP request| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP response| response] -| `session.session` | [api:Nette\Http\Session] | [διαχείριση session| sessions] diff --git a/http/el/request.texy b/http/el/request.texy deleted file mode 100644 index 9171ba5cd2..0000000000 --- a/http/el/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Αίτημα HTTP -*********** - -.[perex] -Το Nette ενσωματώνει το αίτημα HTTP σε αντικείμενα με ένα κατανοητό API και ταυτόχρονα παρέχει ένα φίλτρο εξυγίανσης. - -Το αίτημα HTTP αντιπροσωπεύεται από το αντικείμενο [api:Nette\Http\Request]. Εάν εργάζεστε με το Nette, αυτό το αντικείμενο δημιουργείται αυτόματα από το framework και μπορείτε να το λάβετε μέσω [έγχυσης εξάρτησης |dependency-injection:passing-dependencies]. Στους presenters, απλά καλέστε τη μέθοδο `$this->getHttpRequest()`. Εάν εργάζεστε εκτός του Nette Framework, μπορείτε να δημιουργήσετε το αντικείμενο χρησιμοποιώντας το [#RequestFactory]. - -Ένα μεγάλο πλεονέκτημα του Nette είναι ότι κατά τη δημιουργία του αντικειμένου, καθαρίζει αυτόματα όλες τις παραμέτρους εισόδου GET, POST, COOKIE, καθώς και το URL από χαρακτήρες ελέγχου και μη έγκυρες ακολουθίες UTF-8. Στη συνέχεια, μπορείτε να εργαστείτε με ασφάλεια με αυτά τα δεδομένα. Τα καθαρισμένα δεδομένα χρησιμοποιούνται στη συνέχεια σε presenters και φόρμες. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Nette\Http\Request -================== - -Αυτό το αντικείμενο είναι αμετάβλητο (immutable). Δεν έχει setters, έχει μόνο έναν λεγόμενο wither `withUrl()`, ο οποίος δεν αλλάζει το αντικείμενο, αλλά επιστρέφει μια νέα παρουσία με την αλλαγμένη τιμή. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Επιστρέφει έναν κλώνο με διαφορετικό URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Επιστρέφει το URL του αιτήματος ως αντικείμενο [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Προειδοποίηση: οι περιηγητές δεν στέλνουν το fragment στον διακομιστή, οπότε το `$url->getFragment()` θα επιστρέψει μια κενή συμβολοσειρά. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Επιστρέφει τις παραμέτρους GET του αιτήματος. - -```php -$all = $httpRequest->getQuery(); // επιστρέφει έναν πίνακα όλων των παραμέτρων από το URL -$id = $httpRequest->getQuery('id'); // επιστρέφει την παράμετρο GET 'id' (ή null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Επιστρέφει τις παραμέτρους POST του αιτήματος. - -```php -$all = $httpRequest->getPost(); // επιστρέφει έναν πίνακα όλων των παραμέτρων από το POST -$id = $httpRequest->getPost('id'); // επιστρέφει την παράμετρο POST 'id' (ή null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Επιστρέφει το [ανέβασμα |#Ανεβασμένα Αρχεία] ως αντικείμενο [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // ανέβηκε κάποιο αρχείο; - $file->getUntrustedName(); // όνομα αρχείου που στάλθηκε από τον χρήστη - $file->getSanitizedName(); // όνομα χωρίς επικίνδυνους χαρακτήρες -} -``` - -Για πρόσβαση σε μια ένθετη δομή, καθορίστε έναν πίνακα κλειδιών. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Επειδή δεν μπορείτε να εμπιστευτείτε δεδομένα από έξω και επομένως ούτε να βασιστείτε στη μορφή της δομής των αρχείων, αυτή η μέθοδος είναι ασφαλέστερη από, για παράδειγμα, `$request->getFiles()['my-form']['details']['avatar']`, η οποία μπορεί να αποτύχει. - - -getFiles(): array .[method] ---------------------------- -Επιστρέφει ένα δέντρο [όλων των ανεβασμάτων |#Ανεβασμένα Αρχεία] σε μια κανονικοποιημένη δομή, της οποίας τα φύλλα είναι αντικείμενα [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Επιστρέφει ένα cookie ή `null` εάν δεν υπάρχει. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Επιστρέφει όλα τα cookies. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Επιστρέφει τη μέθοδο HTTP με την οποία έγινε το αίτημα. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Ελέγχει τη μέθοδο HTTP με την οποία έγινε το αίτημα. Η παράμετρος δεν κάνει διάκριση πεζών-κεφαλαίων. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Επιστρέφει μια κεφαλίδα HTTP ή `null` εάν δεν υπάρχει. Η παράμετρος δεν κάνει διάκριση πεζών-κεφαλαίων. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Επιστρέφει όλες τις κεφαλίδες HTTP ως συσχετιστικό πίνακα. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Είναι η σύνδεση κρυπτογραφημένη (HTTPS); Μπορεί να χρειαστεί να [ρυθμίσετε έναν proxy |configuration#HTTP proxy] για σωστή λειτουργία. - - -isSameSite(): bool .[method] ----------------------------- -Προέρχεται το αίτημα από το ίδιο (υπο)domain και ξεκίνησε κάνοντας κλικ σε έναν σύνδεσμο; Το Nette χρησιμοποιεί το cookie `_nss` (παλαιότερα `nette-samesite`) για ανίχνευση. - - -isAjax(): bool .[method] ------------------------- -Είναι αυτό ένα αίτημα AJAX; - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Επιστρέφει τη διεύθυνση IP του χρήστη. Μπορεί να χρειαστεί να [ρυθμίσετε έναν proxy |configuration#HTTP proxy] για σωστή λειτουργία. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Επιστρέφει τη μετάφραση DNS της διεύθυνσης IP του χρήστη. Μπορεί να χρειαστεί να [ρυθμίσετε έναν proxy |configuration#HTTP proxy] για σωστή λειτουργία. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Επιστρέφει τα διαπιστευτήρια ελέγχου ταυτότητας για [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Επιστρέφει το σώμα του αιτήματος HTTP. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Ανιχνεύει τη γλώσσα. Ως παράμετρο `$lang`, περνάμε έναν πίνακα με τις γλώσσες που υποστηρίζει η εφαρμογή, και επιστρέφει αυτή που θα προτιμούσε να δει ο περιηγητής του επισκέπτη. Δεν είναι μαγεία, απλά χρησιμοποιεί την κεφαλίδα `Accept-Language`. Εάν δεν βρεθεί αντιστοιχία, επιστρέφει `null`. - -```php -// ο περιηγητής στέλνει π.χ. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // γλώσσες που υποστηρίζονται από την εφαρμογή -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Η κλάση [api:Nette\Http\RequestFactory] χρησιμοποιείται για τη δημιουργία μιας παρουσίας του `Nette\Http\Request`, η οποία αντιπροσωπεύει το τρέχον αίτημα HTTP. (Εάν εργάζεστε με το Nette, το αντικείμενο αιτήματος HTTP δημιουργείται αυτόματα από το framework.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Η μέθοδος `fromGlobals()` δημιουργεί το αντικείμενο αιτήματος με βάση τις τρέχουσες καθολικές μεταβλητές της PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` και `$_SERVER`). Κατά τη δημιουργία του αντικειμένου, καθαρίζει αυτόματα όλες τις παραμέτρους εισόδου GET, POST, COOKIE, καθώς και το URL από χαρακτήρες ελέγχου και μη έγκυρες ακολουθίες UTF-8, γεγονός που διασφαλίζει την ασφάλεια κατά την περαιτέρω εργασία με αυτά τα δεδομένα. - -Το RequestFactory μπορεί να διαμορφωθεί πριν από την κλήση του `fromGlobals()`: - -- με τη μέθοδο `$factory->setBinary()`, απενεργοποιείτε τον αυτόματο καθαρισμό των παραμέτρων εισόδου από χαρακτήρες ελέγχου και μη έγκυρες ακολουθίες UTF-8. -- με τη μέθοδο `$factory->setProxy(...)`, καθορίζετε τη διεύθυνση IP του [proxy server |configuration#HTTP proxy], η οποία είναι απαραίτητη για τη σωστή ανίχνευση της διεύθυνσης IP του χρήστη. - -Το RequestFactory επιτρέπει τον ορισμό φίλτρων που μετασχηματίζουν αυτόματα τμήματα του URL του αιτήματος. Αυτά τα φίλτρα αφαιρούν ανεπιθύμητους χαρακτήρες από το URL, οι οποίοι μπορεί να έχουν εισαχθεί εκεί, για παράδειγμα, από λανθασμένη υλοποίηση συστημάτων σχολιασμού σε διάφορους ιστότοπους: - -```php -// αφαίρεση κενών από τη διαδρομή -$requestFactory->urlFilters['path']['%20'] = ''; - -// αφαίρεση τελείας, κόμματος ή δεξιάς παρένθεσης από το τέλος του URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// καθαρισμός της διαδρομής από διπλές καθέτους (προεπιλεγμένο φίλτρο) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Το πρώτο κλειδί `'path'` ή `'url'` καθορίζει σε ποιο τμήμα του URL θα εφαρμοστεί το φίλτρο. Το δεύτερο κλειδί είναι η κανονική έκφραση που πρέπει να βρεθεί, και η τιμή είναι η αντικατάσταση που θα χρησιμοποιηθεί αντί για το κείμενο που βρέθηκε. - - -Ανεβασμένα Αρχεία -================= - -Η μέθοδος `Nette\Http\Request::getFiles()` επιστρέφει έναν πίνακα όλων των ανεβασμάτων σε μια κανονικοποιημένη δομή, της οποίας τα φύλλα είναι αντικείμενα [api:Nette\Http\FileUpload]. Αυτά ενσωματώνουν τα δεδομένα που αποστέλλονται από το στοιχείο φόρμας `<input type=file>`. - -Η δομή αντικατοπτρίζει την ονομασία των στοιχείων στο HTML. Στην απλούστερη περίπτωση, μπορεί να είναι ένα μόνο ονομασμένο στοιχείο φόρμας που αποστέλλεται ως: - -```latte -<input type="file" name="avatar"> -``` - -Σε αυτή την περίπτωση, το `$request->getFiles()` επιστρέφει έναν πίνακα: - -```php -[ - 'avatar' => /* Παράδειγμα FileUpload */ -] -``` - -Το αντικείμενο `FileUpload` δημιουργείται ακόμη και αν ο χρήστης δεν ανέβασε κανένα αρχείο ή το ανέβασμα απέτυχε. Η μέθοδος `hasFile()` επιστρέφει εάν ένα αρχείο ανέβηκε: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -Στην περίπτωση ενός ονόματος στοιχείου που χρησιμοποιεί σημειογραφία πίνακα: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -το επιστρεφόμενο δέντρο μοιάζει με αυτό: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* Παράδειγμα FileUpload */ - ], - ], -] -``` - -Μπορείτε επίσης να δημιουργήσετε έναν πίνακα αρχείων: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -Σε αυτή την περίπτωση, η δομή μοιάζει με αυτό: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* Παράδειγμα FileUpload */, - 1 => /* Παράδειγμα FileUpload */, - 2 => /* Παράδειγμα FileUpload */, - ], - ], - ], -] -``` - -Η πρόσβαση στο ευρετήριο 1 του ένθετου πίνακα γίνεται καλύτερα ως εξής: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Επειδή δεν μπορείτε να εμπιστευτείτε δεδομένα από έξω και επομένως ούτε να βασιστείτε στη μορφή της δομής των αρχείων, αυτή η μέθοδος είναι ασφαλέστερη από, για παράδειγμα, `$request->getFiles()['my-form']['details']['avatars'][1]`, η οποία μπορεί να αποτύχει. - - -Επισκόπηση των μεθόδων `FileUpload` .{toc: FileUpload} ------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Επιστρέφει `true` εάν ο χρήστης ανέβασε κάποιο αρχείο. - - -isOk(): bool .[method] ----------------------- -Επιστρέφει `true` εάν το αρχείο ανέβηκε με επιτυχία. - - -getError(): int .[method] -------------------------- -Επιστρέφει τον κωδικό σφάλματος κατά το ανέβασμα του αρχείου. Είναι μία από τις σταθερές [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Εάν το ανέβασμα ήταν επιτυχές, επιστρέφει `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Μετακινεί το ανεβασμένο αρχείο σε νέα τοποθεσία. Εάν το αρχείο προορισμού υπάρχει ήδη, θα αντικατασταθεί. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Επιστρέφει τα περιεχόμενα του ανεβασμένου αρχείου. Εάν το ανέβασμα δεν ήταν επιτυχές, επιστρέφει `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Ανιχνεύει τον τύπο περιεχομένου MIME του ανεβασμένου αρχείου με βάση την υπογραφή του. Εάν το ανέβασμα δεν ήταν επιτυχές ή η ανίχνευση απέτυχε, επιστρέφει `null`. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Επιστρέφει το αρχικό όνομα του αρχείου, όπως στάλθηκε από τον περιηγητή. - -.[caution] -Μην εμπιστεύεστε την τιμή που επιστρέφεται από αυτή τη μέθοδο. Ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να παραβιάσει την εφαρμογή σας. - - -getSanitizedName(): string .[method] ------------------------------------- -Επιστρέφει το εξυγιασμένο όνομα αρχείου. Περιέχει μόνο χαρακτήρες ASCII `[a-zA-Z0-9.-]`. Εάν το όνομα δεν περιέχει τέτοιους χαρακτήρες, επιστρέφει `'unknown'`. Εάν το αρχείο είναι εικόνα σε μορφή JPEG, PNG, GIF, WebP ή AVIF, επιστρέφει επίσης τη σωστή επέκταση. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Επιστρέφει την κατάλληλη επέκταση αρχείου (χωρίς την τελεία) που αντιστοιχεί στον ανιχνευμένο τύπο MIME. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Επιστρέφει την αρχική διαδρομή του αρχείου, όπως στάλθηκε από τον περιηγητή κατά το ανέβασμα ενός φακέλου. Η πλήρης διαδρομή είναι διαθέσιμη μόνο σε PHP 8.1 και νεότερες εκδόσεις. Σε προηγούμενες εκδόσεις, αυτή η μέθοδος επιστρέφει το αρχικό όνομα αρχείου. - -.[caution] -Μην εμπιστεύεστε την τιμή που επιστρέφεται από αυτή τη μέθοδο. Ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να παραβιάσει την εφαρμογή σας. - - -getSize(): int .[method] ------------------------- -Επιστρέφει το μέγεθος του ανεβασμένου αρχείου. Εάν το ανέβασμα δεν ήταν επιτυχές, επιστρέφει `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Επιστρέφει τη διαδρομή προς την προσωρινή τοποθεσία του ανεβασμένου αρχείου. Εάν το ανέβασμα δεν ήταν επιτυχές, επιστρέφει `''`. - - -isImage(): bool .[method] -------------------------- -Επιστρέφει `true` εάν το ανεβασμένο αρχείο είναι εικόνα σε μορφή JPEG, PNG, GIF, WebP ή AVIF. Η ανίχνευση βασίζεται στην υπογραφή του και δεν επαληθεύει την ακεραιότητα ολόκληρου του αρχείου. Το αν μια εικόνα είναι κατεστραμμένη μπορεί να προσδιοριστεί, για παράδειγμα, προσπαθώντας να την [φορτώσετε |#toImage]. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Επιστρέφει ένα ζεύγος `[πλάτος, ύψος]` με τις διαστάσεις της ανεβασμένης εικόνας. Εάν το ανέβασμα δεν ήταν επιτυχές ή δεν είναι έγκυρη εικόνα, επιστρέφει `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Φορτώνει την εικόνα ως αντικείμενο [Image|utils:images]. Εάν το ανέβασμα δεν ήταν επιτυχές ή δεν είναι έγκυρη εικόνα, δημιουργεί μια εξαίρεση `Nette\Utils\ImageException`. diff --git a/http/el/response.texy b/http/el/response.texy deleted file mode 100644 index 0d3eea73ab..0000000000 --- a/http/el/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Απόκριση HTTP -************* - -.[perex] -Το Nette ενσωματώνει την απόκριση HTTP σε αντικείμενα με ένα κατανοητό API. - -Η απόκριση HTTP αντιπροσωπεύεται από το αντικείμενο [api:Nette\Http\Response]. Εάν εργάζεστε με το Nette, αυτό το αντικείμενο δημιουργείται αυτόματα από το framework και μπορείτε να το λάβετε μέσω [έγχυσης εξάρτησης |dependency-injection:passing-dependencies]. Στους presenters, απλά καλέστε τη μέθοδο `$this->getHttpResponse()`. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Nette\Http\Response -=================== - -Το αντικείμενο, σε αντίθεση με το [Nette\Http\Request|request], είναι μεταβλητό (mutable), οπότε μπορείτε να αλλάξετε την κατάσταση χρησιμοποιώντας setters, π.χ. να στείλετε κεφαλίδες. Θυμηθείτε ότι όλοι οι setters πρέπει να κληθούν **πριν από την αποστολή οποιασδήποτε εξόδου.** Η μέθοδος `isSent()` υποδεικνύει εάν η έξοδος έχει ήδη σταλεί. Εάν επιστρέφει `true`, κάθε προσπάθεια αποστολής κεφαλίδας θα προκαλέσει μια εξαίρεση `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Αλλάζει τον [κωδικό κατάστασης της απόκρισης |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Για καλύτερη κατανόηση του πηγαίου κώδικα, συνιστούμε τη χρήση [προκαθορισμένων σταθερών |api:Nette\Http\IResponse] αντί για αριθμούς για τον κωδικό. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Επιστρέφει τον κωδικό κατάστασης της απόκρισης. - - -isSent(): bool .[method] ------------------------- -Επιστρέφει εάν οι κεφαλίδες έχουν ήδη σταλεί από τον διακομιστή στον περιηγητή, και επομένως δεν είναι πλέον δυνατό να σταλούν κεφαλίδες ή να αλλάξει ο κωδικός κατάστασης. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Στέλνει μια κεφαλίδα HTTP και **αντικαθιστά** μια προηγουμένως σταλμένη κεφαλίδα με το ίδιο όνομα. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Στέλνει μια κεφαλίδα HTTP και **δεν αντικαθιστά** μια προηγουμένως σταλμένη κεφαλίδα με το ίδιο όνομα. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Διαγράφει μια προηγουμένως σταλμένη κεφαλίδα HTTP. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Επιστρέφει μια σταλμένη κεφαλίδα HTTP ή `null` εάν δεν υπάρχει. Η παράμετρος δεν κάνει διάκριση πεζών-κεφαλαίων. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Επιστρέφει όλες τις σταλμένες κεφαλίδες HTTP ως συσχετιστικό πίνακα. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Αλλάζει την κεφαλίδα `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Ανακατευθύνει σε άλλο URL. Μην ξεχάσετε να τερματίσετε το σενάριο μετά. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Ορίζει τη λήξη του εγγράφου HTTP χρησιμοποιώντας τις κεφαλίδες `Cache-Control` και `Expires`. Η παράμετρος είναι είτε ένα χρονικό διάστημα (ως κείμενο) είτε `null`, το οποίο απενεργοποιεί την προσωρινή αποθήκευση. - -```php -// η cache στον περιηγητή θα λήξει σε μία ώρα -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Η απόκριση θα ληφθεί μέσω του διαλόγου *Αποθήκευση ως* με το καθορισμένο όνομα. Δεν στέλνει το ίδιο το αρχείο. - -```php -$httpResponse->sendAsFile('invoice.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Στέλνει ένα cookie. Οι προεπιλεγμένες τιμές των παραμέτρων είναι: - -| `$path` | `'/'` | το cookie έχει εμβέλεια σε όλες τις διαδρομές στο (υπο)domain *(διαμορφώσιμο)* -| `$domain` | `null` | που σημαίνει με εμβέλεια στο τρέχον (υπο)domain, αλλά όχι στα υποdomains του *(διαμορφώσιμο)* -| `$secure` | `true` | εάν ο ιστότοπος εκτελείται σε HTTPS, διαφορετικά `false` *(διαμορφώσιμο)* -| `$httpOnly` | `true` | το cookie δεν είναι προσβάσιμο από JavaScript -| `$sameSite` | `'Lax'` | το cookie μπορεί να μην αποσταλεί κατά την [πρόσβαση από άλλο domain |nette:glossary#SameSite cookie] - -Μπορείτε να αλλάξετε τις προεπιλεγμένες τιμές των παραμέτρων `$path`, `$domain` και `$secure` στην [διαμόρφωση |configuration#HTTP cookie]. - -Ο χρόνος μπορεί να καθοριστεί ως αριθμός δευτερολέπτων ή ως συμβολοσειρά: - -```php -$httpResponse->setCookie('lang', 'el', '100 days'); -``` - -Η παράμετρος `$domain` καθορίζει ποια domains μπορούν να δέχονται cookies. Εάν δεν καθοριστεί, το cookie γίνεται αποδεκτό από το ίδιο (υπο)domain που το όρισε, αλλά όχι από τα υποdomains του. Εάν το `$domain` καθοριστεί, περιλαμβάνονται και τα υποdomains. Επομένως, ο καθορισμός του `$domain` είναι λιγότερο περιοριστικός από την παράλειψή του. Για παράδειγμα, με `$domain = 'nette.org'`, τα cookies είναι επίσης διαθέσιμα σε όλα τα υποdomains όπως το `doc.nette.org`. - -Για την τιμή `$sameSite`, μπορείτε να χρησιμοποιήσετε τις σταθερές `Response::SameSiteLax`, `SameSiteStrict` και `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Διαγράφει ένα cookie. Οι προεπιλεγμένες τιμές των παραμέτρων είναι: -- `$path` με εμβέλεια σε όλους τους καταλόγους (`'/'`) -- `$domain` με εμβέλεια στο τρέχον (υπο)domain, αλλά όχι στα υποdomains του -- `$secure` καθορίζεται από τις ρυθμίσεις στην [διαμόρφωση |configuration#HTTP cookie] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/el/sessions.texy b/http/el/sessions.texy deleted file mode 100644 index d5d76fc14e..0000000000 --- a/http/el/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sessions -******** - -<div class=perex> - -Το HTTP είναι ένα πρωτόκολλο χωρίς κατάσταση, αλλά σχεδόν κάθε εφαρμογή χρειάζεται να διατηρεί την κατάσταση μεταξύ των αιτημάτων, για παράδειγμα, το περιεχόμενο ενός καλαθιού αγορών. Αυτός είναι ακριβώς ο σκοπός των sessions. Θα δείξουμε: - -- πώς να χρησιμοποιείτε τα sessions -- πώς να αποφύγετε τις συγκρούσεις ονομάτων -- πώς να ορίσετε τη λήξη - -</div> - -Όταν χρησιμοποιείτε sessions, κάθε χρήστης λαμβάνει ένα μοναδικό αναγνωριστικό που ονομάζεται session ID, το οποίο μεταδίδεται σε ένα cookie. Αυτό χρησιμεύει ως κλειδί για τα δεδομένα του session. Σε αντίθεση με τα cookies, τα οποία αποθηκεύονται στην πλευρά του προγράμματος περιήγησης, τα δεδομένα του session αποθηκεύονται στην πλευρά του διακομιστή. - -Ρυθμίζουμε το session στην [διαμόρφωση |configuration#Session], η επιλογή του χρόνου λήξης είναι ιδιαίτερα σημαντική. - -Η διαχείριση του session γίνεται από το αντικείμενο [api:Nette\Http\Session], στο οποίο μπορείτε να αποκτήσετε πρόσβαση ζητώντας το μέσω [έγχυσης εξάρτησης |dependency-injection:passing-dependencies]. Στους presenters, απλά καλέστε `$session = $this->getSession()`. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Έναρξη Session -============== - -Από προεπιλογή, το Nette ξεκινά αυτόματα το session τη στιγμή που αρχίζουμε να διαβάζουμε ή να γράφουμε δεδομένα σε αυτό. Μπορείτε να ξεκινήσετε το session χειροκίνητα χρησιμοποιώντας το `$session->start()`. - -Όταν ξεκινά ένα session, η PHP στέλνει κεφαλίδες HTTP που επηρεάζουν την προσωρινή αποθήκευση, δείτε [php:session_cache_limiter], και ενδεχομένως ένα cookie με το session ID. Επομένως, είναι απαραίτητο να ξεκινάτε πάντα το session πριν στείλετε οποιαδήποτε έξοδο στο πρόγραμμα περιήγησης, διαφορετικά θα προκληθεί εξαίρεση. Εάν γνωρίζετε ότι το session θα χρησιμοποιηθεί κατά την απόδοση της σελίδας, ξεκινήστε το χειροκίνητα εκ των προτέρων, για παράδειγμα, στον presenter. - -Στη λειτουργία ανάπτυξης, το Tracy ξεκινά το session επειδή το χρησιμοποιεί για την εμφάνιση των γραμμών με ανακατευθύνσεις και αιτήματα AJAX στο Tracy Bar. - - -Ενότητες -======== - -Στην καθαρή PHP, ο χώρος αποθήκευσης δεδομένων του session υλοποιείται ως ένας πίνακας προσβάσιμος μέσω της καθολικής μεταβλητής `$_SESSION`. Το πρόβλημα είναι ότι οι εφαρμογές συνήθως αποτελούνται από έναν αριθμό ανεξάρτητων τμημάτων, και εάν όλα έχουν πρόσβαση μόνο σε έναν πίνακα, αργά ή γρήγορα θα προκύψει σύγκρουση ονομάτων. - -Το Nette Framework λύνει αυτό το πρόβλημα διαιρώντας ολόκληρο τον χώρο σε ενότητες (αντικείμενα [api:Nette\Http\SessionSection]). Κάθε μονάδα χρησιμοποιεί τότε τη δική της ενότητα με ένα μοναδικό όνομα, και δεν μπορεί να προκύψει σύγκρουση. - -Λαμβάνουμε μια ενότητα από το session: - -```php -$section = $session->getSection('μοναδικό όνομα'); -``` - -Στον presenter, απλά χρησιμοποιήστε το `getSession()` με μια παράμετρο: - -```php -// $this είναι ένας Presenter -$section = $this->getSession('μοναδικό όνομα'); -``` - -Η ύπαρξη μιας ενότητας μπορεί να ελεγχθεί με τη μέθοδο `$session->hasSection('μοναδικό όνομα')`. - -Η εργασία με την ίδια την ενότητα είναι τότε πολύ εύκολη χρησιμοποιώντας τις μεθόδους `set()`, `get()` και `remove()`: - -```php -// εγγραφή μεταβλητής -$section->set('userName', 'franta'); - -// ανάγνωση μεταβλητής, επιστρέφει null εάν δεν υπάρχει -echo $section->get('userName'); - -// διαγραφή μεταβλητής -$section->remove('userName'); -``` - -Για να λάβετε όλες τις μεταβλητές από την ενότητα, μπορείτε να χρησιμοποιήσετε έναν βρόχο `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Ρύθμιση Λήξης -------------- - -Είναι δυνατό να οριστεί η λήξη για μεμονωμένες ενότητες ή ακόμη και για μεμονωμένες μεταβλητές. Μπορούμε να αφήσουμε τη σύνδεση του χρήστη να λήξει σε 20 λεπτά, αλλά ταυτόχρονα να θυμόμαστε το περιεχόμενο του καλαθιού αγορών. - -```php -// η ενότητα λήγει μετά από 20 λεπτά -$section->setExpiration('20 minutes'); -``` - -Για να ορίσετε τη λήξη μεμονωμένων μεταβλητών, χρησιμοποιήστε την τρίτη παράμετρο της μεθόδου `set()`: - -```php -// η μεταβλητή 'flash' λήγει μετά από 30 δευτερόλεπτα -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Μην ξεχνάτε ότι ο χρόνος λήξης ολόκληρου του session (δείτε [διαμόρφωση session |configuration#Session]) πρέπει να είναι ίσος ή μεγαλύτερος από τον χρόνο που ορίζεται για μεμονωμένες ενότητες ή μεταβλητές. - -Η ακύρωση μιας προηγουμένως ορισμένης λήξης επιτυγχάνεται με τη μέθοδο `removeExpiration()`. Η άμεση ακύρωση ολόκληρης της ενότητας διασφαλίζεται από τη μέθοδο `remove()`. - - -Συμβάντα $onStart, $onBeforeWrite ---------------------------------- - -Το αντικείμενο `Nette\Http\Session` έχει [συμβάντα |nette:glossary#Events] `$onStart` και `$onBeforeWrite`, οπότε μπορείτε να προσθέσετε επανακλήσεις που καλούνται μετά την έναρξη του session ή πριν από την εγγραφή του στον δίσκο και τον επακόλουθο τερματισμό του. - -```php -$session->onBeforeWrite[] = function () { - // γράφουμε δεδομένα στο session - $this->section->set('basket', $this->basket); -}; -``` - - -Διαχείριση Session -================== - -Επισκόπηση των μεθόδων της κλάσης `Nette\Http\Session` για τη διαχείριση του session: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Ξεκινά το session. - - -isStarted(): bool .[method] ---------------------------- -Έχει ξεκινήσει το session; - - -close(): void .[method] ------------------------ -Τερματίζει το session. Το session τερματίζεται αυτόματα στο τέλος της εκτέλεσης του σεναρίου. - - -destroy(): void .[method] -------------------------- -Τερματίζει και διαγράφει το session. - - -exists(): bool .[method] ------------------------- -Περιέχει το αίτημα HTTP ένα cookie με το session ID; - - -regenerateId(): void .[method] ------------------------------- -Δημιουργεί ένα νέο τυχαίο session ID. Τα δεδομένα διατηρούνται. - - -getId(): string .[method] -------------------------- -Επιστρέφει το session ID. - -</div> - - -Διαμόρφωση ----------- - -Ρυθμίζουμε το session στην [διαμόρφωση |configuration#Session]. Εάν γράφετε μια εφαρμογή που δεν χρησιμοποιεί DI container, αυτές οι μέθοδοι χρησιμοποιούνται για τη διαμόρφωση. Πρέπει να κληθούν πριν από την έναρξη του session. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Ορίζει το όνομα του cookie στο οποίο μεταδίδεται το session ID. Το προεπιλεγμένο όνομα είναι `PHPSESSID`. Είναι χρήσιμο εάν εκτελείτε πολλές διαφορετικές εφαρμογές στον ίδιο ιστότοπο. - - -getName(): string .[method] ---------------------------- -Επιστρέφει το όνομα του cookie στο οποίο μεταδίδεται το session ID. - - -setOptions(array $options): static .[method] --------------------------------------------- -Διαμορφώνει το session. Μπορείτε να ορίσετε όλες τις PHP [οδηγίες session |https://www.php.net/manual/en/session.configuration.php] (σε μορφή camelCase, π.χ. αντί για `session.save_path` γράφουμε `savePath`) καθώς και το [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Ορίζει τον χρόνο αδράνειας μετά τον οποίο λήγει το session. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Ρύθμιση παραμέτρων για το cookie. Μπορείτε να αλλάξετε τις προεπιλεγμένες τιμές των παραμέτρων στην [διαμόρφωση |configuration#Session cookie]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Ορίζει τον κατάλογο όπου αποθηκεύονται τα αρχεία session. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Ορίζει έναν προσαρμοσμένο χειριστή, δείτε την [τεκμηρίωση της PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Ασφάλεια Πρώτα -============== - -Ο διακομιστής υποθέτει ότι επικοινωνεί πάντα με τον ίδιο χρήστη, εφόσον τα αιτήματα συνοδεύονται από το ίδιο session ID. Ο ρόλος των μηχανισμών ασφαλείας είναι να διασφαλίσουν ότι αυτό συμβαίνει πραγματικά και ότι δεν είναι δυνατό να κλαπεί ή να πλαστογραφηθεί το αναγνωριστικό. - -Επομένως, το Nette Framework διαμορφώνει σωστά τις οδηγίες PHP έτσι ώστε το session ID να μεταδίδεται μόνο σε cookies, να το καθιστά μη προσβάσιμο από JavaScript και να αγνοεί τυχόν αναγνωριστικά στο URL. Επιπλέον, σε κρίσιμες στιγμές, όπως η σύνδεση του χρήστη, δημιουργεί ένα νέο session ID. - -.[note] -Η συνάρτηση ini_set χρησιμοποιείται για τη διαμόρφωση της PHP, την οποία δυστυχώς ορισμένοι πάροχοι φιλοξενίας απαγορεύουν. Εάν αυτό συμβαίνει και με τον δικό σας πάροχο, προσπαθήστε να διαπραγματευτείτε μαζί του για να σας επιτρέψει τη συνάρτηση ή τουλάχιστον να διαμορφώσει τον διακομιστή. diff --git a/http/el/urls.texy b/http/el/urls.texy deleted file mode 100644 index 129f400099..0000000000 --- a/http/el/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Εργασία με URLs -*************** - -.[perex] -Οι κλάσεις [#Url], [#UrlImmutable] και [#UrlScript] επιτρέπουν την εύκολη δημιουργία, ανάλυση και χειρισμό URLs. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Url -=== - -Η κλάση [api:Nette\Http\Url] επιτρέπει την εύκολη εργασία με URLs και τα μεμονωμένα συστατικά τους, τα οποία αποτυπώνονται σε αυτό το διάγραμμα: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Η δημιουργία URLs είναι διαισθητική: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Μπορείτε επίσης να αναλύσετε ένα URL και να το χειριστείτε περαιτέρω: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Η κλάση `Url` υλοποιεί τη διεπαφή `JsonSerializable` και έχει μια μέθοδο `__toString()`, οπότε το αντικείμενο μπορεί να εκτυπωθεί ή να χρησιμοποιηθεί σε δεδομένα που περνούν στο `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Συστατικά URL .[method] ------------------------ - -Για την επιστροφή ή την αλλαγή μεμονωμένων συστατικών του URL, είναι διαθέσιμες οι ακόλουθες μέθοδοι: - -.[language-php] -| Setter | Getter | Επιστρεφόμενη τιμή -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | ολόκληρο το URL - -Προειδοποίηση: Όταν εργάζεστε με ένα URL που λαμβάνεται από ένα [αίτημα HTTP|request], λάβετε υπόψη ότι δεν θα περιέχει το fragment, καθώς ο περιηγητής δεν το στέλνει στον διακομιστή. - -Μπορούμε επίσης να εργαστούμε με μεμονωμένες παραμέτρους query χρησιμοποιώντας: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Επιστρέφει το δεξί ή το αριστερό τμήμα του host. Λειτουργεί ως εξής εάν ο host είναι `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Επαληθεύει εάν δύο URLs είναι πανομοιότυπα. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Επαληθεύει εάν ένα URL είναι απόλυτο. Ένα URL θεωρείται απόλυτο εάν ξεκινά με ένα σχήμα (π.χ. http, https, ftp) ακολουθούμενο από άνω και κάτω τελεία. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Κανονικοποιεί τη διαδρομή σε ένα URL αφαιρώντας τα ειδικά τμήματα `.` και `..`. Η μέθοδος αφαιρεί τα περιττά στοιχεία διαδρομής με τον ίδιο τρόπο που το κάνουν οι περιηγητές ιστού. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Η κλάση [api:Nette\Http\UrlImmutable] είναι μια αμετάβλητη (immutable) εναλλακτική της κλάσης [#Url] (παρόμοια με το πώς το `DateTimeImmutable` είναι η αμετάβλητη εναλλακτική του `DateTime` στην PHP). Αντί για setters, έχει τους λεγόμενους withers, οι οποίοι δεν αλλάζουν το αντικείμενο, αλλά επιστρέφουν νέες παρουσίες με την τροποποιημένη τιμή: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Η κλάση `UrlImmutable` υλοποιεί τη διεπαφή `JsonSerializable` και έχει μια μέθοδο `__toString()`, οπότε το αντικείμενο μπορεί να εκτυπωθεί ή να χρησιμοποιηθεί σε δεδομένα που περνούν στο `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Συστατικά URL .[method] ------------------------ - -Για την επιστροφή ή την αλλαγή μεμονωμένων συστατικών του URL, χρησιμοποιούνται οι ακόλουθες μέθοδοι: - -.[language-php] -| Wither | Getter | Επιστρεφόμενη τιμή -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | ολόκληρο το URL - -Η μέθοδος `withoutUserInfo()` αφαιρεί τα `user` και `password`. - -Μπορούμε επίσης να εργαστούμε με μεμονωμένες παραμέτρους query χρησιμοποιώντας: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Επιστρέφει το δεξί ή το αριστερό τμήμα του host. Λειτουργεί ως εξής εάν ο host είναι `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Παράγει ένα απόλυτο URL με τον ίδιο τρόπο που ένας περιηγητής επεξεργάζεται συνδέσμους σε μια σελίδα HTML: -- εάν ο σύνδεσμος είναι ένα απόλυτο URL (περιέχει σχήμα), χρησιμοποιείται αμετάβλητος -- εάν ο σύνδεσμος ξεκινά με `//`, λαμβάνεται μόνο το σχήμα από το τρέχον URL -- εάν ο σύνδεσμος ξεκινά με `/`, δημιουργείται μια απόλυτη διαδρομή από τη ρίζα του domain -- σε άλλες περιπτώσεις, το URL δημιουργείται σχετικά με την τρέχουσα διαδρομή - -```php -$url = new UrlImmutable('https://example.com/path/page'); -echo $url->resolve('../foo'); // 'https://example.com/foo' -echo $url->resolve('/bar'); // 'https://example.com/bar' -echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Επαληθεύει εάν δύο URLs είναι πανομοιότυπα. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Η κλάση [api:Nette\Http\UrlScript] είναι απόγονος του [#UrlImmutable] και το επεκτείνει με πρόσθετα εικονικά συστατικά URL, όπως ο ριζικός κατάλογος του έργου κ.λπ. Όπως και η γονική κλάση, είναι ένα αμετάβλητο (immutable) αντικείμενο. - -Το ακόλουθο διάγραμμα δείχνει τα συστατικά που αναγνωρίζει το UrlScript: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` είναι η βασική διεύθυνση URL της εφαρμογής, συμπεριλαμβανομένου του domain και του τμήματος της διαδρομής προς τον ριζικό κατάλογο της εφαρμογής -- `basePath` είναι το τμήμα της διαδρομής προς τον ριζικό κατάλογο της εφαρμογής -- `scriptPath` είναι η διαδρομή προς το τρέχον σενάριο -- `relativePath` είναι το όνομα του σεναρίου (και ενδεχομένως περαιτέρω τμήματα διαδρομής) σχετικά με το basePath -- `relativeUrl` είναι ολόκληρο το τμήμα του URL μετά το baseUrl, συμπεριλαμβανομένης της συμβολοσειράς query και του fragment. -- `pathInfo` είναι ένα τμήμα του URL που χρησιμοποιείται σπάνια σήμερα, μετά το όνομα του σεναρίου - -Για την επιστροφή τμημάτων του URL, είναι διαθέσιμες οι ακόλουθες μέθοδοι: - -.[language-php] -| Getter | Επιστρεφόμενη τιμή -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Συνήθως δεν δημιουργούμε απευθείας αντικείμενα `UrlScript`, αλλά η μέθοδος [Nette\Http\Request::getUrl()|request] τα επιστρέφει με τα συστατικά ήδη σωστά ρυθμισμένα για το τρέχον αίτημα HTTP. diff --git a/http/en/@home.texy b/http/en/@home.texy index 9ae502ba2d..7cb1ca35ba 100644 --- a/http/en/@home.texy +++ b/http/en/@home.texy @@ -2,7 +2,14 @@ Nette HTTP ********** .[perex] -The `nette/http` package encapsulates [HTTP request|request] & [response], working with [sessions] and [URL parsing and building |urls]. +The `nette/http` package is your companion for all HTTP communication. It provides a clear object-oriented API over the incoming request and outgoing response, simplifies working with sessions and URL addresses, and takes care of security on top of that. Here's what you'll find: + +| [HTTP Request |request] | incoming request and input sanitization +| [HTTP Response |response] | outgoing response, headers and cookies +| [Sessions] | secure state persistence between requests +| [URL Utility |urls] | parsing and building URL addresses +| [SSRF Protection |ssrf] | defense against Server-Side Request Forgery attacks +| [Configuration] | configuration options of the package Installation @@ -13,3 +20,5 @@ Download and install the package using [Composer|best-practices:composer]: ```shell composer require nette/http ``` + +The package requires PHP version 8.3 to 8.5. diff --git a/http/en/@left-menu.texy b/http/en/@left-menu.texy index 652706062b..e5a64f0967 100644 --- a/http/en/@left-menu.texy +++ b/http/en/@left-menu.texy @@ -5,4 +5,15 @@ Nette HTTP - [HTTP Response |response] - [Sessions] - [URL Utility |urls] +- [SSRF Protection |ssrf] - [Configuration] +- [Upgrading] + + +Further Reading +*************** +- [Nette Documentation |nette:] +- [Nette Application |application:how-it-works] +- [Utilities |utils:] +- [Best practices |best-practices:] +- [Troubleshooting |nette:troubleshooting] diff --git a/http/en/configuration.texy b/http/en/configuration.texy index 64ce01eb2d..520e5655ef 100644 --- a/http/en/configuration.texy +++ b/http/en/configuration.texy @@ -12,23 +12,25 @@ HTTP Headers ```neon http: - # headers that are sent with each request + # headers that are sent with each response headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' # affects the X-Frame-Options header - frames: ... # (string|bool) defaults to 'SAMEORIGIN' + frames: ... # (string|bool|null) defaults to 'SAMEORIGIN' ``` -For security reasons, the framework sends the `X-Frame-Options: SAMEORIGIN` header, which indicates that a page can be displayed inside another page (in an `<iframe>` element) only if it is on the same domain. This might be undesirable in certain situations (e.g., if you are developing a Facebook application), so the behavior can be changed by setting `frames: http://allowed-host.com` or `frames: true`. +For security reasons, the framework sends the `X-Frame-Options: SAMEORIGIN` header, which indicates that a page can be displayed inside another page (in an `<iframe>` element) only if it is on the same domain. This might be undesirable in certain situations (e.g., if you are developing a Facebook application), so the behavior can be changed by setting `frames: http://allowed-host.com` to allow a specific host, `frames: true` to allow framing from anywhere (the header is omitted), or `frames: false` to forbid it entirely (`X-Frame-Options: DENY`). + +By default, Nette also sends the `X-Powered-By: Nette Framework 3` and `Content-Type: text/html; charset=utf-8` headers. You can remove any header, including these defaults, by setting its value to an empty string. Content Security Policy ----------------------- -Headers `Content-Security-Policy` (CSP) can be easily configured; their description can be found in the [CSP specification |https://content-security-policy.com]. CSP directives (such as `script-src`) can be written either as strings according to the specification or as arrays of values for better readability. Then there is no need to use quotation marks around keywords like `'self'`. Nette will also automatically generate a `nonce` value, so something like `'nonce-y4PopTLM=='` will be sent in the header. +The `Content-Security-Policy` (CSP) headers can be easily configured; their description can be found in the [CSP specification |https://content-security-policy.com]. CSP directives (such as `script-src`) can be written either as strings according to the specification or as arrays of values for better readability. Then there is no need to use quotation marks around keywords like `'self'`. Nette will also automatically generate a `nonce` value, so something like `'nonce-y4PopTLM=='` will be sent in the header. ```neon http: @@ -108,6 +110,32 @@ http: ``` +Force HTTPS .{data-version:3.3.4} +--------------------------------- + +Unconditionally forces the request scheme to HTTPS. This is useful for HTTPS-only sites running behind a load balancer or reverse proxy that terminates TLS but does not pass the `X-Forwarded-Proto` header, so the standard HTTPS detection (even with [#HTTP Proxy] configured) would not catch it. + +```neon +http: + # force HTTPS scheme for all requests + forceHttps: true # (bool) defaults to false +``` + + +Application Base URL .{data-version:3.4.1} +------------------------------------------ + +Nette detects the address the application runs at from the HTTP request, so nothing needs to be configured. When run from the command line (cron, tasks, an MCP server) there is no request, and derived values such as `%baseUrl%`, links from `LinkGenerator` or `$baseUrl` in templates would have nothing to come from. For these cases you can specify the base URL of the application: + +```neon +http: + # used only when the address cannot be detected from the request + baseUrl: https://example.com/ # (string) defaults to not set +``` + +The value applies only when the environment yields no host. On the web the actual request always takes precedence, so the same configuration works on localhost and in production. The URL must be absolute, including any subdirectory (`https://example.com/eshop`). + + Session ======= @@ -169,3 +197,4 @@ These services are added to the DI container: | `http.request` | [api:Nette\Http\Request] | [HTTP request| request] | `http.response` | [api:Nette\Http\Response] | [HTTP response| response] | `session.session`| [api:Nette\Http\Session] | [session management| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | factory that creates the HTTP request diff --git a/http/en/request.texy b/http/en/request.texy index ae45438508..bdd7f168db 100644 --- a/http/en/request.texy +++ b/http/en/request.texy @@ -55,8 +55,8 @@ $id = $httpRequest->getPost('id'); // returns POST parameter 'id' (or null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- Returns an [upload |#Uploaded Files] as a [api:Nette\Http\FileUpload] object: ```php @@ -70,11 +70,11 @@ if ($file?->hasFile()) { // was any file uploaded? To access a nested structure, provide an array of keys. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Since you cannot trust external data and therefore rely on the structure of the files, this approach is safer than, for example, `$request->getFiles()['my-form']['details']['avatar']`, which might fail. +Since you cannot trust external data and therefore cannot rely on the structure of the files, this approach is safer than, for example, `$request->getFiles()['my-form']['details']['avatar']`, which might fail. getFiles(): array .[method] @@ -86,8 +86,8 @@ $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- +getCookie(string $key): ?string .[method] +----------------------------------------- Returns a cookie or `null` if it doesn't exist. ```php @@ -131,13 +131,13 @@ $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Returns all HTTP headers as an associative array. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Returns all HTTP headers as an associative array. The keys are normalized to lowercase. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` @@ -146,9 +146,41 @@ isSecured(): bool .[method] Is the connection encrypted (HTTPS)? Proper functionality might require [setting up a proxy |configuration#HTTP Proxy]. -isSameSite(): bool .[method] ----------------------------- -Is the request coming from the same (sub)domain and initiated by clicking a link? Nette uses the `_nss` cookie (formerly `nette-samesite`) for detection. +isSameSite(): bool .[method deprecated] +--------------------------------------- +Did the request come from the same site? Since version 3.4 it is replaced by the more capable [isFrom() |#isFrom]. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Tells you where the request came from and how the browser made it, based on the `Sec-Fetch-*` headers (so-called [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]) that the browser sets itself and a page running in the victim's browser can neither forge nor remove. Nette uses it internally to automatically protect forms and signals against [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). It is useful when you want to guard your own sensitive actions, such as API endpoints or destructive links. + +The method returns `true` only when the request matches **all** the conditions you provide. The first parameter `$site` describes the relationship between the page that initiated the request and your site (the `Sec-Fetch-Site` header). It accepts a single value or a list of these `FetchSite` cases: + +- `FetchSite::SameOrigin` - from the exact same origin (scheme, host, and port) +- `FetchSite::SameSite` - from the same site, possibly a different subdomain +- `FetchSite::CrossSite` - from a foreign site +- `FetchSite::None` - the user initiated it directly, e.g. by typing the URL or opening a bookmark + +```php +// did the request originate from our own pages? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // block the action +} +``` + +The optional `$dest` parameter (the `Sec-Fetch-Dest` header) says what kind of resource the browser is fetching, e.g. `FetchDest::Document` for a top-level navigation or `FetchDest::Empty` for a request made from JavaScript. The optional `$user` parameter (the `Sec-Fetch-User` header) indicates whether the navigation was triggered by a genuine user action such as clicking a link or submitting a form; pass `true` to require it. + +A check that an action is reachable only from your own pages and only through a real user action then looks like this: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Older browsers (Safari before 16.4) do not send the `Sec-Fetch-*` headers. For them Nette falls back to a `SameSite=Strict` cookie that only proves the request is not cross-site. A check that additionally requires `$dest` or `$user` cannot be verified this way and returns `false` in those browsers - if that is too strict, test only `$site`. isAjax(): bool .[method] @@ -163,7 +195,7 @@ Returns the user's IP address. Proper functionality might require [setting up a getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Returns the DNS translation of the user's IP address. Proper functionality might require [setting up a proxy |configuration#HTTP Proxy]. +Deprecated, it always returns `null`. Reverse DNS lookups were slow and unreliable; if you need the hostname, resolve it yourself from [getRemoteAddress() |#getRemoteAddress]. getBasicCredentials(): ?array .[method] @@ -184,6 +216,30 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Returns the origin from which the request came. An origin consists of the scheme (protocol), hostname, and port - for example, `https://example.com:8080`. Returns `null` if the origin header is not present or is set to `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +The browser sends the `Origin` header in the following cases: +- Cross-origin requests (AJAX calls to a different domain) +- POST, PUT, DELETE, and other modifying requests +- Requests made using the Fetch API + +The browser does NOT send the `Origin` header for: +- Regular GET requests to the same domain (same-origin navigation) +- Direct navigation by typing a URL into the address bar +- Requests from non-browser clients + +.[note] +Unlike the `Referer` header, `Origin` contains only the scheme, host, and port - not the full URL path. This makes it more suitable for security checks while preserving user privacy. The `Origin` header is primarily used for [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing) validation. + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- Detects the language. Pass an array of languages supported by the application as the `$langs` parameter, and it will return the one preferred by the visitor's browser. It's not magic; it just uses the `Accept-Language` header. If no match is found, it returns `null`. @@ -206,12 +262,13 @@ $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -The `fromGlobals()` method creates the request object based on the current PHP global variables (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES`, and `$_SERVER`). When creating the object, it automatically cleanses all input parameters (GET, POST, COOKIE) as well as the URL from control characters and invalid UTF-8 sequences, ensuring security when working with this data later. +The `fromGlobals()` method creates the request object based on the current PHP global variables (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES`, and `$_SERVER`). When creating the object, it automatically cleanses all input parameters (GET, POST, COOKIE) as well as the URL of control characters and invalid UTF-8 sequences, ensuring security when working with this data later. RequestFactory can be configured before calling `fromGlobals()`: -- using the `$factory->setBinary()` method disables automatic cleansing of input parameters from control characters and invalid UTF-8 sequences. +- using the `$factory->setBinary()` method disables the automatic removal of control characters and invalid UTF-8 sequences from input parameters. - using the `$factory->setProxy(...)` method specifies the IP address of the [proxy server |configuration#HTTP Proxy], which is necessary for correct detection of the user's IP address. +- using the `$factory->setForceHttps()` .{data-version:3.3.4} method forces the request scheme to HTTPS regardless of the server environment. RequestFactory allows defining filters that automatically transform parts of the URL request. These filters remove unwanted characters from URLs that might have been inserted, for example, by incorrect implementations of comment systems on various websites: @@ -303,7 +360,7 @@ if ($file instanceof Nette\Http\FileUpload) { } ``` -Since you cannot trust external data and therefore rely on the structure of the files, this approach is safer than, for example, `$request->getFiles()['my-form']['details']['avatars'][1]`, which might fail. +Since you cannot trust external data and therefore cannot rely on the structure of the files, this approach is safer than, for example, `$request->getFiles()['my-form']['details']['avatars'][1]`, which might fail. Overview of `FileUpload` Methods .{toc: FileUpload} @@ -389,6 +446,11 @@ getTemporaryFile(): string .[method] Returns the path to the temporary location of the uploaded file. If the upload was not successful, it returns `''`. +__toString(): string .[method] +------------------------------ +Returns the path to the temporary location of the uploaded file. This allows the `FileUpload` object to be used directly as a string. + + isImage(): bool .[method] ------------------------- Returns `true` if the uploaded file is a JPEG, PNG, GIF, WebP, or AVIF image. Detection is based on its signature and does not verify the integrity of the entire file. Whether an image is corrupted can be determined, for example, by trying to [load it |#toImage]. @@ -404,4 +466,4 @@ Returns a pair `[width, height]` with the dimensions of the uploaded image. If t toImage(): Nette\Utils\Image .[method] -------------------------------------- -Loads the image as an [Image |utils:images] object. If the upload was not successful or it is not a valid image, it throws an `Nette\Utils\ImageException`. +Loads the image as an [Image |utils:images] object. If the upload was not successful or it is not a valid image, it throws a `Nette\Utils\ImageException`. diff --git a/http/en/response.texy b/http/en/response.texy index 7654f039e9..6d5c3afb5a 100644 --- a/http/en/response.texy +++ b/http/en/response.texy @@ -12,7 +12,7 @@ The HTTP response is represented by the [api:Nette\Http\Response] object. If you Nette\Http\Response =================== -Unlike [Nette\Http\Request |request], this object is mutable, so you can use setters to change the state, e.g., to send headers. Remember that all setters **must be called before any actual output is sent.** The `isSent()` method indicates if the output has already been sent. If it returns `true`, any attempt to send a header will throw an `Nette\InvalidStateException`. +Unlike [Nette\Http\Request |request], this object is mutable, so you can use setters to change the state, e.g., to send headers. Remember that all setters **must be called before any actual output is sent.** The `isSent()` method indicates if the output has already been sent. If it returns `true`, any attempt to send a header will throw a `Nette\InvalidStateException`. setCode(int $code, ?string $reason=null) .[method] @@ -34,9 +34,9 @@ isSent(): bool .[method] Returns whether headers have already been sent from the server to the browser, meaning it is no longer possible to send headers or change the status code. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Sends an HTTP header and **overwrites** a previously sent header of the same name. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Sends an HTTP header and **overwrites** a previously sent header of the same name. If `$value` is `null`, the header will be removed. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -67,8 +67,8 @@ $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ +getHeaders(): array<string, string> .[method] +--------------------------------------------- Returns all sent HTTP headers as an associative array. ```php @@ -96,8 +96,8 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- +setExpiration(?string $expire) .[method] +---------------------------------------- Sets the expiration of the HTTP document using the `Cache-Control` and `Expires` headers. The parameter is either a time interval (as text) or `null`, which disables caching. ```php @@ -115,27 +115,36 @@ $httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Sends a cookie. Default parameter values: -| `$path` | `'/'` | cookie is available for all paths within the (sub)domain *(configurable)* -| `$domain` | `null` | meaning available for the current (sub)domain, but not its subdomains *(configurable)* -| `$secure` | `true` | if the site is running on HTTPS, otherwise `false` *(configurable)* -| `$httpOnly` | `true` | cookie is inaccessible to JavaScript -| `$sameSite` | `'Lax'` | cookie might not be sent during [cross-origin access |nette:glossary#SameSite cookie] +| `$path` | `'/'` | cookie is available for all paths within the (sub)domain *(configurable)* +| `$domain` | `null` | meaning available for the current (sub)domain, but not its subdomains *(configurable)* +| `$secure` | `auto` | `true` if the site is running on HTTPS, otherwise `false` (framework default; the bare class defaults to `false`) *(configurable)* +| `$httpOnly` | `true` | cookie is inaccessible to JavaScript +| `$sameSite` | `'Lax'` | cookie might not be sent during [cross-origin access |nette:glossary#SameSite cookie] +| `$partitioned` | `false` | whether the cookie is partitioned, see below *(since v3.4)* You can change the default values of the `$path`, `$domain`, and `$secure` parameters in the [configuration |configuration#HTTP Cookie]. -The time can be specified as a number of seconds or a string: +The expiration is passed as a number of seconds, as a text interval or date, or as a `DateTimeInterface` object. The value `null` creates a session cookie, which the browser discards when it is closed. Nette sends the expiration in both the `Expires` and `Max-Age` attributes. ```php -$httpResponse->setCookie('lang', 'en', '100 days'); +$httpResponse->setCookie('lang', 'en', '100 days'); // expires in 100 days +$httpResponse->setCookie('lang', 'en', null); // session cookie ``` The `$domain` parameter determines which domains can accept the cookie. If not specified, the cookie is accepted by the same (sub)domain that set it, but not its subdomains. If `$domain` is specified, subdomains are also included. Therefore, specifying `$domain` is less restrictive than omitting it. For example, with `$domain = 'nette.org'`, cookies are also available on all subdomains like `doc.nette.org`. -You can use the constants `Response::SameSiteLax`, `Response::SameSiteStrict`, and `Response::SameSiteNone` for the `$sameSite` value. +You can pass the `$sameSite` value as a `Nette\Http\SameSite` enum - `SameSite::Lax`, `SameSite::Strict`, or `SameSite::None` (the string values `'Lax'`, `'Strict'`, `'None'` work too). If you set it to `SameSite::None`, the `$secure` attribute is enabled automatically, because browsers reject a `SameSite=None` cookie that is not secure. + +.{data-version:3.4.0} +Partitioned cookies (CHIPS) give a cookie its own separate storage for each top-level site. So when a third-party service (such as an embedded widget) sets a partitioned cookie, the browser keeps a distinct copy for every site the widget appears on, and these copies cannot be linked together for cross-site tracking. Turn it on by setting `$partitioned` to `true`; this also requires the `$secure` attribute, so it is enabled automatically. + +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] @@ -148,3 +157,28 @@ Deletes a cookie. The default values of the parameters are: ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +The [api:Nette\Http\Context] object joins the request and the response together and helps with HTTP caching. It is not registered as a service, so you create it yourself. In presenters, it is usually easier to use the [lastModified() |application:presenters#HTTP Caching] method; the context is useful when you send the response yourself, for example from your own response class. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Determines whether the content has changed since the client's last visit. If you pass the time of the last modification, it sends the `Last-Modified` header; if you pass an ETag validator (a short string identifying the current version of the content, e.g., its hash), it sends the `ETag` header. It then compares both against the `If-Modified-Since` and `If-None-Match` headers sent by the browser. + +If the browser already holds a matching version, the method sets the code `304 Not Modified` and returns `false` - in that case, do not send the body of the response at all. Otherwise, it returns `true`. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Both parameters are optional. If you do not know the modification time of the content, use only the ETag, and vice versa. diff --git a/http/en/sessions.texy b/http/en/sessions.texy index f0831458d5..95f228ae69 100644 --- a/http/en/sessions.texy +++ b/http/en/sessions.texy @@ -20,8 +20,8 @@ Session management is handled by the [api:Nette\Http\Session] object, which you → [Installation and requirements |@home#Installation] -Starting Session -================ +Starting a Session +================== By default, Nette automatically starts a session the moment we begin reading from or writing data to it. To start a session manually, use `$session->start()`. @@ -50,7 +50,7 @@ In the presenter, just use `getSession()` with a parameter: $section = $this->getSession('unique name'); ``` -The existence of a section can be checked using the `$session->hasSection('unique name')` method. +The existence of a section can be checked using the `$session->hasSection('unique name')` method. A list of the names of all existing sections is returned by `$session->getSectionNames()`. Working with the section itself is then very easy using the `set()`, `get()`, and `remove()` methods: @@ -94,7 +94,7 @@ $section->set('flash', $message, '30 seconds'); .[note] Remember that the expiration time of the entire session (see [session configuration |configuration#Session]) must be equal to or greater than the time set for individual sections or variables. -To cancel a previously set expiration, use the `removeExpiration()` method. To immediately remove the entire section, use the `remove()` method. +To cancel a previously set expiration, use the `removeExpiration()` method; to clear the expiration of a specific variable, pass its name: `removeExpiration('flash')`. To immediately remove the entire section, use the `remove()` method. Events $onStart, $onBeforeWrite @@ -178,13 +178,13 @@ setOptions(array $options): static .[method] Configures the session. It is possible to set all PHP [session directives |https://www.php.net/manual/en/session.configuration.php] (in camelCase format, e.g., write `savePath` instead of `session.save_path`) and also [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- +setExpiration(?string $expire): static .[method] +------------------------------------------------ Sets the inactivity time after which the session expires. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- Sets parameters for cookies. You can change the default parameter values in the [configuration |configuration#Session Cookie]. diff --git a/http/en/ssrf.texy b/http/en/ssrf.texy new file mode 100644 index 0000000000..f4321a5183 --- /dev/null +++ b/http/en/ssrf.texy @@ -0,0 +1,183 @@ +SSRF Protection +*************** + +.[perex] +When your application downloads a URL supplied by a user, an attacker can abuse it to reach your internal network. The [#UrlValidator] and [#IPAddress] classes help you guard against these Server-Side Request Forgery (SSRF) attacks. + +→ [Installation and requirements |@home#Installation] + + +What is SSRF? +============= + +Imagine a feature where the user enters a URL and your server downloads it - an avatar from a remote address, a webhook target, a link preview. It looks harmless, but the server reaches the address, not the user's browser. And the server can see places the attacker can't: the loopback interface, the private network, cloud services. + +An attacker therefore submits a URL that points inward instead of to the public internet. Typical targets are: + +- cloud metadata at `http://169.254.169.254/`, which can leak access keys +- internal admin panels and routers like `http://192.168.1.1/` +- services with no authentication, such as Redis on `http://localhost:6379/` + +This class of vulnerability is so common it ranks among the [OWASP Top 10 |https://owasp.org/Top10/]. The defense is to validate the URL **before** you fetch it and to refuse anything that resolves to a non-public address. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] checks a URL against a configurable policy: the scheme, port, host, userinfo, and the IP addresses the host resolves to. The basic usage is a single call: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // unsafe URL, do not fetch it +} +``` + +The default policy is deliberately strict - it only accepts `https` on port 443 pointing to a public IP address. Everything else (loopback, private ranges, link-local including cloud metadata, reserved ranges) is rejected, and multicast is rejected unconditionally. This is the right starting point for fetching arbitrary user-supplied URLs. + + +Configuring the Policy +---------------------- + +You shape the policy through the constructor. For example, to allow plain `http` on any port and reach private addresses (useful inside a trusted network): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // any port + allowPrivateIps: true, +); +``` + +A common pattern is to restrict fetching to a fixed set of partner domains using a host allowlist. The `*.` prefix matches any subdomain depth but not the apex - list both forms if you need it: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +The full set of constructor options: + +| Parameter | Default | Meaning +|--------------------- +| `schemes` | `['https']` | allowed schemes; `[]` rejects everything +| `ports` | `[443]` | allowed ports, `null` = any; the implicit port from the scheme is honored +| `allowPrivateIps` | `false` | allow private ranges (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | allow loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | allow link-local incl. cloud metadata 169.254.169.254 +| `allowReserved` | `false` | allow IANA-reserved ranges +| `allowUserinfo` | `false` | allow `user:pass@` in the URL +| `hostAllowlist` | `null` | if set, host must match one pattern; `[]` rejects all +| `hostBlocklist` | `null` | if set, host must not match any pattern + + +Validation Methods +------------------ + +The validator offers three methods. `allows()` runs the full check including DNS resolution - the host is resolved and **every** A/AAAA address must pass the IP policy: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` skips DNS resolution and the IP-range checks. Use it as a fast pre-filter, or when DNS validation is delegated to the fetch layer: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Both methods accept a string, a [UrlImmutable |urls#UrlImmutable] object, or `null` (which always fails). + + +Defeating DNS Rebinding +----------------------- + +There is a subtle race between validation and fetching: an attacker can return a safe IP when you validate the host, then switch DNS to an internal IP for the actual download. To close this hole, `getResolvedIPs()` returns the validated IP addresses, and you pin the connection to them so the fetch can't be redirected elsewhere: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // unsafe URL +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... execute the request +``` + +The method returns an array of IP strings (A records first, then AAAA) that passed the full policy, or an empty array on any failure. For an IP literal in the URL it validates the address directly and performs no DNS lookup. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] is an immutable value object for working with IPv4 and IPv6 addresses. `UrlValidator` uses it internally, but it's handy on its own whenever you classify addresses. The constructor throws `Nette\InvalidArgumentException` for an invalid address: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +When you don't want an exception, use the `tryFrom()` factory or the `isValid()` checker: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Address Classification +---------------------- + +The predicates tell you which class an address belongs to. The key one is `isPublic()` - true only for publicly routable addresses, which is exactly what an SSRF guard wants: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (cloud metadata range) +``` + +The full set of predicates: + +| Method | Tests for +|-------------------- +| `isPublic()` | publicly routable (none of the below) +| `isPrivate()` | RFC 1918 / 4193 private ranges +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (incl. cloud metadata), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | IANA-reserved (documentation, CGNAT, future-use, …) + + +Range Membership +---------------- + +`isInRange()` tests whether the address falls within a CIDR block. You can pass a network with a prefix, or a bare address for an exact match (implicit /32 for IPv4, /128 for IPv6): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (exact match) +``` + +Malformed input or a different IP family returns `false`. + + +IPv4-mapped IPv6 +---------------- + +Addresses written as IPv4-mapped IPv6 (such as `::ffff:127.0.0.1`) are a classic way to slip past naive filters. `IPAddress` normalizes them, so the range predicates see through the disguise: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +The `isIPv4()` and `isIPv6()` methods report the textual form: a mapped address is IPv6, not IPv4. diff --git a/http/en/upgrading.texy b/http/en/upgrading.texy new file mode 100644 index 0000000000..7a49104664 --- /dev/null +++ b/http/en/upgrading.texy @@ -0,0 +1,54 @@ +Upgrading +********* + + +Upgrading to Version 3.4 +======================== + +The minimum required PHP version is 8.3. + +- the method `Request::isSameSite()` is deprecated in favor of `isFrom()`, which determines the origin of the request from the `Sec-Fetch-*` headers. The automatic protection of forms and signals becomes more precise, and one behavior changes: direct navigation (a bookmark, a manually typed address, a link in an e-mail) is no longer considered same-site. If a signal relies on action links in e-mails, mark it with `#[Requires(sameOrigin: false)]`. +- the `_nss` cookie is now sent only to browsers that do not send the `Sec-Fetch-Site` header +- `setCookie()` sends the `Max-Age` attribute and forces the `Secure` flag for `SameSite=None` and for partitioned cookies +- the enum `SameSite` replaces the constants `IResponse::SameSiteLax` etc., which are deprecated +- expiration is interpreted the same way everywhere: a number is a relative number of seconds, a string is an interval or a date. Passing an absolute UNIX timestamp is deprecated, and a session cookie is represented by `null` instead of `0`. +- the deprecated method `Request::getRemoteHost()` returns `null` +- the long deprecated class `Nette\Http\UserStorage` was removed + +The whole story of the switch to the `Sec-Fetch-*` headers is told in the article [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Upgrading to Version 3.2 +======================== + +- the credentials from HTTP Basic Authentication are no longer part of the `Url` object, so `$url->getUser()` and `$url->getPassword()` return an empty string. Read them using the new method `$request->getBasicCredentials()`. + +The reasons for this change are explained in the article [Nette Http 3.2: change access to credentials |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]. + + +Upgrading to Version 3.1 +======================== + +- cookies are sent with the `sameSite: Lax` flag +- `cookieSecure` now defaults to 'auto' +- the `session.cookieSecure` option is deprecated; `http.cookieSecure` is used instead +- the `nette-samesite` cookie was renamed to `_nss` +- `Nette\Http\Request::getFile()` accepts an array of keys and returns `FileUpload|null` +- `Nette\Http\Session::getCookieParameters()` is deprecated +- `Nette\Http\FileUpload::getName()` was renamed to `getUntrustedName()` +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()`, and `getRelativeUrl()` are deprecated (these methods are part of `UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` is deprecated +- `Nette\Http\FileUpload::getImageSize()` returns the pair `[width, height]` +- with `autoStart: smart` (the default) the session is no longer started right after the application starts just because the browser sent a session cookie; it starts on the first read or write. The values `always` and `never` were added. +- when the browser sends a session ID for which no session exists, Nette deletes the cookie instead of creating a new session +- for accessing session sections prefer the methods `set()`, `get()` and `remove()`; unlike property access they correctly distinguish reading from writing and do not start the session unnecessarily +- the default values of `cookiePath` and `cookieDomain` can be set in the configuration + +The behavior of sessions is described in detail in the article [Nette Http 3.1: much smarter sessions |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions]. + + +Upgrading to Version 3.0 +======================== + +- the `Nette\Http\UrlScript` object (returned e.g. by `Nette\Http\Request::getUrl()`) is now immutable +- in `new Nette\Http\Url('abcd')`, `abcd` represents the path, not the domain; since 3.0, `(new Nette\Http\Url('abcd'))->setScheme('http')` correctly generates `http:abcd` instead of the previous `http://abcd` diff --git a/http/en/urls.texy b/http/en/urls.texy index 84a947c12f..71baac19f6 100644 --- a/http/en/urls.texy +++ b/http/en/urls.texy @@ -52,8 +52,8 @@ echo json_encode([$url]); ``` -URL Components .[method] ------------------------- +URL Components +-------------- The following methods are available to retrieve or modify individual URL components: @@ -69,10 +69,12 @@ The following methods are available to retrieve or modify individual URL compone | `setPath(string $path)` | `getPath(): string` | `'/en/download'` | `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` +| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | entire URL +The `getUser()`, `getPassword()`, `setUser()`, and `setPassword()` methods are deprecated, because embedding credentials directly in a URL is discouraged. + Warning: When working with a URL obtained from an [HTTP request |request], keep in mind that it will not contain the fragment, as the browser does not send it to the server. We can also work with individual query parameters using: @@ -82,6 +84,7 @@ We can also work with individual query parameters using: |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] @@ -98,8 +101,8 @@ Returns the right or left part of the host. Here's how it works if the host is ` | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Checks if two URLs are identical. ```php @@ -107,6 +110,11 @@ $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Converts the URL to canonical form. This converts the hostname to lowercase and normalizes the path (percent-encoding and removing redundant characters). The query string is left unchanged. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- Checks if a URL is absolute. A URL is considered absolute if it begins with a scheme (e.g., http, https, ftp) followed by a colon. @@ -137,15 +145,15 @@ The [api:Nette\Http\UrlImmutable] class is an immutable alternative to the [#Url use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/en/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/en/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` The `UrlImmutable` class implements the `JsonSerializable` interface and has a `__toString()` method, so the object can be printed or used in data passed to `json_encode()`. @@ -156,8 +164,8 @@ echo json_encode([$url]); ``` -URL Components .[method] ------------------------- +URL Components +-------------- The following methods are available to retrieve or change individual URL components: @@ -173,11 +181,11 @@ The following methods are available to retrieve or change individual URL compone | `withPath(string $path)` | `getPath(): string` | `'/en/download'` | `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` +| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | entire URL -The `withoutUserInfo()` method removes `user` and `password`. +The `getUser()`, `getPassword()`, `withUser()`, `withPassword()`, and `withoutUserInfo()` methods are deprecated, because embedding credentials directly in a URL is discouraged. We can also work with individual query parameters using: @@ -218,8 +226,8 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Checks if two URLs are identical. ```php @@ -259,7 +267,7 @@ The following methods are available to retrieve these parts of the URL: | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` diff --git a/http/es/@home.texy b/http/es/@home.texy index 99ecbd441a..f869669aaf 100644 --- a/http/es/@home.texy +++ b/http/es/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -El paquete `nette/http` encapsula la [petición HTTP|request] y la [respuesta HTTP|response], el trabajo con [sesiones|sessions] y el [análisis y composición de URL |urls]. +El paquete `nette/http` es su compañero para toda la comunicación HTTP. Ofrece una API orientada a objetos y clara sobre la petición entrante y la respuesta saliente, simplifica el trabajo con las sesiones y con las direcciones URL y, además, se ocupa de la seguridad. Esto es lo que encontrará: + +| [Petición HTTP |request] | petición entrante y saneamiento de las entradas +| [Respuesta HTTP |response] | respuesta saliente, cabeceras y cookies +| [Sesiones|sessions] | persistencia segura del estado entre peticiones +| [Trabajar con URLs |urls] | análisis y composición de direcciones URL +| [Protección contra SSRF |ssrf] | defensa frente a los ataques Server-Side Request Forgery +| [Configuración|configuration] | opciones de configuración del paquete Instalación ----------- -Puede descargar e instalar la librería usando [Composer|best-practices:composer]: +Descargue e instale el paquete con [Composer|best-practices:composer]: ```shell composer require nette/http ``` + +El paquete requiere PHP de la versión 8.3 a la 8.5. diff --git a/http/es/@left-menu.texy b/http/es/@left-menu.texy index 4daffef2a0..55a9d08341 100644 --- a/http/es/@left-menu.texy +++ b/http/es/@left-menu.texy @@ -1,8 +1,19 @@ Nette HTTP ********** - [Introducción |@home] -- [Petición HTTP|request] -- [Respuesta HTTP|response] -- [Sesiones|Sessions] -- [Utilidades de URL |urls] -- [Configuración |configuration] +- [Petición HTTP |request] +- [Respuesta HTTP |response] +- [Sesiones|sessions] +- [Trabajar con URLs |urls] +- [Protección contra SSRF |ssrf] +- [Configuración|configuration] +- [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/http/es/@meta.texy b/http/es/@meta.texy index 1670b124ad..3798d9cda4 100644 --- a/http/es/@meta.texy +++ b/http/es/@meta.texy @@ -1 +1 @@ -{{sitename: Nette Documentación}} +{{sitename: Documentación de Nette}} diff --git a/http/es/configuration.texy b/http/es/configuration.texy index c283068e79..4e5c7346e2 100644 --- a/http/es/configuration.texy +++ b/http/es/configuration.texy @@ -2,9 +2,9 @@ Configuración HTTP ****************** .[perex] -Resumen de las opciones de configuración para Nette HTTP. +Resumen de las opciones de configuración de Nette HTTP. -Si no utiliza todo el framework, sino solo esta librería, lea [cómo cargar la configuración|bootstrap:]. +Si no usa el framework entero, sino solo esta biblioteca, lea [cómo cargar la configuración|bootstrap:]. Cabeceras HTTP @@ -12,29 +12,31 @@ Cabeceras HTTP ```neon http: - # cabeceras que se envían con cada petición + # cabeceras que se envían con cada respuesta headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' # afecta a la cabecera X-Frame-Options - frames: ... # (string|bool) el valor por defecto es 'SAMEORIGIN' + frames: ... # (string|bool|null) el valor predeterminado es 'SAMEORIGIN' ``` -El framework, por razones de seguridad, envía la cabecera `X-Frame-Options: SAMEORIGIN`, que indica que la página solo se puede mostrar dentro de otra página (en un elemento `<iframe>`) si se encuentra en el mismo dominio. Esto puede ser no deseado en algunas situaciones (por ejemplo, si está desarrollando una aplicación para Facebook), por lo que el comportamiento se puede cambiar estableciendo `frames: http://allowed-host.com` o `frames: true`. +Por motivos de seguridad, el framework envía la cabecera `X-Frame-Options: SAMEORIGIN`, que indica que una página solo se puede mostrar dentro de otra página (en un elemento `<iframe>`) si está en el mismo dominio. Eso puede no ser deseable en ciertas situaciones (p. ej. si desarrolla una aplicación de Facebook), así que el comportamiento se puede cambiar poniendo `frames: http://allowed-host.com` para permitir un host concreto, `frames: true` para permitir el framing desde cualquier sitio (la cabecera se omite) o `frames: false` para prohibirlo por completo (`X-Frame-Options: DENY`). + +De forma predeterminada, Nette envía también las cabeceras `X-Powered-By: Nette Framework 3` y `Content-Type: text/html; charset=utf-8`. Puede eliminar cualquier cabecera, incluidas estas predeterminadas, poniendo su valor a una cadena vacía. Content Security Policy ----------------------- -Se pueden construir fácilmente las cabeceras `Content-Security-Policy` (en adelante CSP), cuya descripción encontrará en la [descripción de CSP |https://content-security-policy.com]. Las directivas CSP (como `script-src`) pueden escribirse como cadenas según la especificación, o como un array de valores para una mejor legibilidad. Entonces no es necesario escribir comillas alrededor de palabras clave como `'self'`. Nette también genera automáticamente el valor `nonce`, por lo que en la cabecera habrá, por ejemplo, `'nonce-y4PopTLM=='`. +Las cabeceras `Content-Security-Policy` (CSP) se configuran con facilidad; su descripción la encontrará en la [especificación de CSP |https://content-security-policy.com]. Las directivas CSP (como `script-src`) se pueden escribir como cadenas según la especificación o como arrays de valores, para que se lean mejor. Entonces no hace falta usar comillas alrededor de palabras clave como `'self'`. Nette generará además automáticamente un valor `nonce`, así que en la cabecera se enviará algo como `'nonce-y4PopTLM=='`. ```neon http: # Content Security Policy csp: - # cadena en formato según la especificación CSP + # cadena según la especificación de CSP default-src: "'self' https://example.com" # array de valores @@ -44,14 +46,14 @@ http: - self - https://example.com - # bool en caso de interruptores + # bool en el caso de los interruptores upgrade-insecure-requests: true block-all-mixed-content: false ``` -En las plantillas, use `<script n:nonce>...</script>` y el valor nonce se completará automáticamente. Hacer sitios web seguros en Nette es realmente fácil. +Use `<script n:nonce>...</script>` en las plantillas y el valor del nonce se rellenará automáticamente. Hacer sitios web seguros en Nette es realmente fácil. -De manera similar, se pueden construir las cabeceras `Content-Security-Policy-Report-Only` (que se pueden usar simultáneamente con CSP) y [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: +De forma parecida se pueden configurar las cabeceras `Content-Security-Policy-Report-Only` (que se pueden usar a la vez que CSP) y [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: ```neon http: @@ -69,68 +71,80 @@ http: ``` -Cookie HTTP ------------ +Cookies HTTP +------------ -Se pueden cambiar los valores predeterminados de algunos parámetros del método [Nette\Http\Response::setCookie() |response#setCookie] y de la sesión. +Puede cambiar los valores predeterminados de algunos parámetros del método [Nette\Http\Response::setCookie() |response#setCookie()] y del manejo de la sesión. ```neon http: # alcance de la cookie por ruta - cookiePath: ... # (string) el valor por defecto es '/' + cookiePath: ... # (string) el valor predeterminado es '/' - # dominios que aceptan la cookie - cookieDomain: 'example.com' # (string|domain) el valor por defecto es no establecido + # dominios que pueden recibir la cookie + cookieDomain: 'example.com' # (string|domain) de forma predeterminada sin establecer - # ¿enviar cookie solo a través de HTTPS? - cookieSecure: ... # (bool|auto) el valor por defecto es auto + # ¿enviar las cookies solo por HTTPS? + cookieSecure: ... # (bool|auto) el valor predeterminado es auto - # desactiva el envío de la cookie que Nette usa como protección contra CSRF - disableNetteCookie: ... # (bool) el valor por defecto es false + # desactiva el envío de la cookie que Nette usa para la protección CSRF + disableNetteCookie: ... # (bool) el valor predeterminado es false ``` -El atributo `cookieDomain` determina qué dominios pueden aceptar la cookie. Si no se especifica, la cookie es aceptada por el mismo (sub)dominio que la estableció, *pero no* por sus subdominios. Si se especifica `cookieDomain`, también se incluyen los subdominios. Por lo tanto, especificar `cookieDomain` es menos restrictivo que omitirlo. +El atributo `cookieDomain` determina qué dominios (orígenes) pueden aceptar las cookies. Si no se indica, la cookie la acepta el mismo (sub)dominio que la estableció, *excluyendo* sus subdominios. Si se indica `cookieDomain`, se incluyen también los subdominios. Por eso, indicar `cookieDomain` es menos restrictivo que omitirlo. -Por ejemplo, con `cookieDomain: nette.org`, las cookies también están disponibles en todos los subdominios como `doc.nette.org`. Lo mismo se puede lograr también con el valor especial `domain`, es decir, `cookieDomain: domain`. +Por ejemplo, si se pone `cookieDomain: nette.org`, las cookies están disponibles también en todos los subdominios, como `doc.nette.org`. Eso también se consigue con el valor especial `domain`, es decir, `cookieDomain: domain`. -El valor predeterminado `auto` para el atributo `cookieSecure` significa que si el sitio web se ejecuta en HTTPS, las cookies se enviarán con el indicador `Secure` y, por lo tanto, solo estarán disponibles a través de HTTPS. +El valor predeterminado `auto` del atributo `cookieSecure` significa que, si el sitio web funciona con HTTPS, las cookies se enviarán con la bandera `Secure` y por tanto solo estarán disponibles por HTTPS. Proxy HTTP ---------- -Si el sitio web se ejecuta detrás de un proxy HTTP, especifique su dirección IP para que la detección de la conexión a través de HTTPS y también la dirección IP del cliente funcionen correctamente. Es decir, para que las funciones [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] y [isSecured() |request#isSecured] devuelvan los valores correctos y en las plantillas se generen enlaces con el protocolo `https:`. +Si el sitio funciona tras un proxy HTTP, indique la dirección IP del proxy para que la detección de la conexión HTTPS y la dirección IP del cliente funcionen correctamente. Es decir, para que [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()] e [isSecured() |request#isSecured()] devuelvan los valores correctos y los enlaces se generen con el protocolo `https:` en las plantillas. + +```neon +http: + # dirección IP, rango (p. ej. 127.0.0.1/8) o un array de esos valores + proxy: 127.0.0.1 # (string|string[]) de forma predeterminada sin establecer +``` + + +Forzar HTTPS .{data-version:3.3.4} +---------------------------------- + +Fuerza incondicionalmente el esquema HTTPS de la petición. Es útil para sitios solo HTTPS que funcionan tras un balanceador de carga o un proxy inverso que termina el TLS pero no pasa la cabecera `X-Forwarded-Proto`, con lo que la detección estándar de HTTPS (incluso con el [#Proxy HTTP] configurado) no lo captaría. ```neon http: - # Dirección IP, rango (p. ej. 127.0.0.1/8) o array de estos valores - proxy: 127.0.0.1 # (string|string[]) el valor por defecto es no establecido + # fuerza el esquema HTTPS en todas las peticiones + forceHttps: true # (bool) el valor predeterminado es false ``` Sesión ====== -Configuración básica de [sesiones|sessions]: +Ajustes básicos de las [sesiones |sessions]: ```neon session: - # ¿mostrar el panel de sesión en Tracy Bar? - debugger: ... # (bool) el valor por defecto es false + # ¿mostrar el panel de la sesión en la Tracy Bar? + debugger: ... # (bool) el valor predeterminado es false - # tiempo de inactividad después del cual la sesión expira - expiration: 14 days # (string) el valor por defecto es '3 hours' + # tiempo de inactividad tras el cual expira la sesión + expiration: 14 days # (string) el valor predeterminado es '3 hours' # ¿cuándo debe iniciarse la sesión? - autoStart: ... # (smart|always|never) el valor por defecto es 'smart' + autoStart: ... # (smart|always|never) el valor predeterminado es 'smart' - # manejador, servicio que implementa la interfaz SessionHandlerInterface + # handler, un servicio que implementa SessionHandlerInterface handler: @handlerService ``` -La opción `autoStart` controla cuándo debe iniciarse la sesión. El valor `always` significa que la sesión siempre se iniciará cuando se inicie la aplicación. El valor `smart` significa que la sesión se iniciará al inicio de la aplicación solo si ya existe, o en el momento en que queramos leer o escribir en ella. Y finalmente, el valor `never` prohíbe el inicio automático de la sesión. +La opción `autoStart` controla cuándo debe iniciarse la sesión. El valor `always` significa que la sesión se inicia siempre que arranca la aplicación. El valor `smart` significa que la sesión se inicia junto con la aplicación solo si ya existe, o en el momento en que queremos leer de ella o escribir en ella. Por último, el valor `never` desactiva el inicio automático de la sesión. -Además, se pueden configurar todas las [directivas de sesión |https://www.php.net/manual/en/session.configuration.php] de PHP (en formato camelCase) y también [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Ejemplo: +Además puede establecer todas las [directivas de sesión |https://www.php.net/manual/en/session.configuration.php] de PHP (en formato camelCase) y también [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Ejemplo: ```neon session: @@ -145,18 +159,18 @@ session: Cookie de sesión ---------------- -La cookie de sesión se envía con los mismos parámetros que [otras cookies |#Cookie HTTP], pero puede cambiarlos para ella: +La cookie de sesión se envía con los mismos parámetros que las [demás cookies |#Cookies HTTP], pero puede cambiarlos específicamente para ella: ```neon session: - # dominios que aceptan la cookie + # dominios que pueden recibir la cookie cookieDomain: 'example.com' # (string|domain) - # restricción al acceder desde otro dominio - cookieSamesite: None # (Strict|Lax|None) el valor por defecto es Lax + # restricción para el acceso cross-origin + cookieSamesite: None # (Strict|Lax|None) el valor predeterminado es Lax ``` -El atributo `cookieSamesite` afecta si la cookie se enviará al [acceder desde otro dominio |nette:glossary#Cookie SameSite], lo que proporciona cierta protección contra ataques [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). +El atributo `cookieSamesite` afecta a si la cookie se envía en las [peticiones cross-origin |nette:glossary#Cookie SameSite], lo que aporta cierta protección frente a los ataques [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Servicios DI @@ -165,7 +179,8 @@ Servicios DI Estos servicios se añaden al contenedor DI: | Nombre | Tipo | Descripción -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [Petición HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [Respuesta HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [gestión de sesiones| sessions] +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [petición HTTP| request] +| `http.response` | [api:Nette\Http\Response] | [respuesta HTTP| response] +| `session.session`| [api:Nette\Http\Session] | [gestión de la sesión| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | factory que crea la petición HTTP diff --git a/http/es/request.texy b/http/es/request.texy index 49b9cdf1c6..b14267df15 100644 --- a/http/es/request.texy +++ b/http/es/request.texy @@ -2,11 +2,11 @@ Petición HTTP ************* .[perex] -Nette encapsula la petición HTTP en objetos con una API comprensible y, al mismo tiempo, proporciona un filtro de sanitización. +Nette encapsula la petición HTTP en objetos con una API clara y ofrece a la vez un filtro de saneamiento. -La petición HTTP está representada por el objeto [api:Nette\Http\Request]. Si trabaja con Nette, este objeto es creado automáticamente por el framework y puede solicitar que se le pase mediante [inyección de dependencias |dependency-injection:passing-dependencies]. En los presenters, basta con llamar al método `$this->getHttpRequest()`. Si trabaja fuera del Nette Framework, puede crear el objeto usando [#RequestFactory]. +La petición HTTP está representada por el objeto [api:Nette\Http\Request]. Si trabaja con Nette, el framework crea este objeto automáticamente y puede hacer que se lo pasen mediante [inyección de dependencias |dependency-injection:passing-dependencies]. En los presenters basta con llamar al método `$this->getHttpRequest()`. Si trabaja fuera de Nette Framework, puede crear el objeto con [#RequestFactory]. -Una gran ventaja de Nette es que al crear el objeto, limpia automáticamente todos los parámetros de entrada GET, POST, COOKIE y también la URL de caracteres de control y secuencias UTF-8 inválidas. Con estos datos, puede trabajar de forma segura. Los datos limpios se utilizan posteriormente en presenters y formularios. +Una gran ventaja de Nette es que, al crear el objeto, sanea automáticamente todos los parámetros de entrada (GET, POST, COOKIE) y también la URL, eliminando los caracteres de control y las secuencias UTF-8 no válidas. Después puede trabajar con esos datos con seguridad. Los datos saneados se usan a continuación en los presenters y los formularios. → [Instalación y requisitos |@home#Instalación] @@ -14,25 +14,25 @@ Una gran ventaja de Nette es que al crear el objeto, limpia automáticamente tod Nette\Http\Request ================== -Este objeto es inmutable. No tiene setters, solo tiene un llamado wither `withUrl()`, que no modifica el objeto, sino que devuelve una nueva instancia con el valor cambiado. +Este objeto es inmutable. No tiene setters; tiene solo un llamado wither, `withUrl()`, que no cambia el objeto sino que devuelve una nueva instancia con el valor modificado. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ---------------------------------------------------------------- -Devuelve un clon con una URL diferente. +Devuelve un clon con otra URL. getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -Devuelve la URL de la petición como un objeto [UrlScript |urls#UrlScript]. +Devuelve la URL de la petición como objeto [UrlScript |urls#UrlScript]. ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/es/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -Advertencia: los navegadores no envían el fragmento al servidor, por lo que `$url->getFragment()` devolverá una cadena vacía. +Atención: los navegadores no envían el fragmento al servidor, así que `$url->getFragment()` devolverá una cadena vacía. getQuery(?string $key=null): string|array|null .[method] @@ -40,7 +40,7 @@ getQuery(?string $key=null): string|array|null .[method] Devuelve los parámetros GET de la petición. ```php -$all = $httpRequest->getQuery(); // devuelve un array de todos los parámetros de la URL +$all = $httpRequest->getQuery(); // array de todos los parámetros de la URL $id = $httpRequest->getQuery('id'); // devuelve el parámetro GET 'id' (o null) ``` @@ -50,45 +50,45 @@ getPost(?string $key=null): string|array|null .[method] Devuelve los parámetros POST de la petición. ```php -$all = $httpRequest->getPost(); // devuelve un array de todos los parámetros de POST -$id = $httpRequest->getPost('id'); // devuelve el parámetro POST 'id' (o null) +$all = $httpRequest->getPost(); // array de todos los parámetros POST +$id = $httpRequest->getPost('id'); // devuelve el parámetro POST 'id' (o null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Devuelve la [carga |#Archivos cargados] como un objeto [api:Nette\Http\FileUpload]: +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- +Devuelve un [archivo subido |#Archivos subidos] como objeto [api:Nette\Http\FileUpload]: ```php $file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // ¿se ha cargado algún archivo? - $file->getUntrustedName(); // nombre del archivo enviado por el usuario +if ($file?->hasFile()) { // ¿se subió algún archivo? + $file->getUntrustedName(); // nombre de archivo enviado por el usuario $file->getSanitizedName(); // nombre sin caracteres peligrosos } ``` -Para acceder a una estructura anidada, proporcione un array de claves. +Para acceder a una estructura anidada, indique un array de claves. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Dado que no se puede confiar en los datos externos y, por lo tanto, tampoco en la forma de la estructura de archivos, este método es más seguro que, por ejemplo, `$request->getFiles()['my-form']['details']['avatar']`, que podría fallar. +Como no puede fiarse de los datos externos y por tanto tampoco de la estructura de los archivos, este enfoque es más seguro que, por ejemplo, `$request->getFiles()['my-form']['details']['avatar']`, que podría fallar. getFiles(): array .[method] --------------------------- -Devuelve el árbol de [todas las cargas |#Archivos cargados] en una estructura normalizada, cuyas hojas son objetos [api:Nette\Http\FileUpload]: +Devuelve un árbol de [todos los archivos subidos |#Archivos subidos] en una estructura normalizada cuyas hojas son objetos [api:Nette\Http\FileUpload]: ```php $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Devuelve la cookie o `null` si no existe. +getCookie(string $key): ?string .[method] +----------------------------------------- +Devuelve una cookie, o `null` si no existe. ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -Devuelve el método HTTP con el que se realizó la petición. +Devuelve el método HTTP usado en la petición. ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -Comprueba el método HTTP con el que se realizó la petición. El parámetro es insensible a mayúsculas/minúsculas. +Comprueba el método HTTP usado en la petición. El parámetro no distingue mayúsculas de minúsculas. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,31 +124,63 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -Devuelve una cabecera HTTP o `null` si no existe. El parámetro es insensible a mayúsculas/minúsculas. +Devuelve una cabecera HTTP, o `null` si no existe. El parámetro no distingue mayúsculas de minúsculas. ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Devuelve todas las cabeceras HTTP como un array asociativo. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Devuelve todas las cabeceras HTTP como array asociativo. Las claves están normalizadas a minúsculas. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -¿Está la conexión cifrada (HTTPS)? Para un funcionamiento correcto, puede ser necesario [configurar un proxy |configuration#Proxy HTTP]. +¿Está cifrada la conexión (HTTPS)? Para que funcione correctamente puede hacer falta [configurar un proxy |configuration#Proxy HTTP]. -isSameSite(): bool .[method] ----------------------------- -¿Proviene la petición del mismo (sub)dominio y se inicia haciendo clic en un enlace? Nette utiliza la cookie `_nss` (anteriormente `nette-samesite`) para la detección. +isSameSite(): bool .[method deprecated] +--------------------------------------- +¿Llegó la petición desde el mismo sitio? Desde la versión 3.4 lo sustituye el más capaz [isFrom() |#isFrom()]. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Le dice de dónde llegó la petición y cómo la hizo el navegador, a partir de las cabeceras `Sec-Fetch-*` (los llamados [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]), que el navegador pone él mismo y que una página que se ejecute en el navegador de la víctima no puede falsificar ni eliminar. Nette las usa internamente para proteger automáticamente los formularios y las señales contra el [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Es útil cuando quiera proteger sus propias acciones sensibles, como endpoints de API o enlaces destructivos. + +El método devuelve `true` solo cuando la petición cumple **todas** las condiciones que indique. El primer parámetro `$site` describe la relación entre la página que inició la petición y su sitio (la cabecera `Sec-Fetch-Site`). Acepta un único valor o una lista de estos casos de `FetchSite`: + +- `FetchSite::SameOrigin`: del mismo origen exacto (esquema, host y puerto) +- `FetchSite::SameSite`: del mismo sitio, posiblemente de otro subdominio +- `FetchSite::CrossSite`: de un sitio ajeno +- `FetchSite::None`: la inició directamente el usuario, p. ej. escribiendo la URL o abriendo un marcador + +```php +// ¿procede la petición de nuestras propias páginas? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // bloquea la acción +} +``` + +El parámetro opcional `$dest` (la cabecera `Sec-Fetch-Dest`) dice qué tipo de recurso está obteniendo el navegador, p. ej. `FetchDest::Document` para una navegación de nivel superior o `FetchDest::Empty` para una petición hecha desde JavaScript. El parámetro opcional `$user` (la cabecera `Sec-Fetch-User`) indica si la navegación la desencadenó una acción real del usuario, como pulsar un enlace o enviar un formulario; pase `true` para exigirlo. + +Una comprobación de que una acción solo es accesible desde sus propias páginas y solo mediante una acción real del usuario tiene entonces este aspecto: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Los navegadores antiguos (Safari anterior a 16.4) no envían las cabeceras `Sec-Fetch-*`. Para ellos, Nette recurre a una cookie `SameSite=Strict` que solo demuestra que la petición no es cross-site. Una comprobación que exija además `$dest` o `$user` no se puede verificar así y devuelve `false` en esos navegadores; si eso es demasiado estricto, compruebe solo `$site`. isAjax(): bool .[method] @@ -158,20 +190,20 @@ isAjax(): bool .[method] getRemoteAddress(): ?string .[method] ------------------------------------- -Devuelve la dirección IP del usuario. Para un funcionamiento correcto, puede ser necesario [configurar un proxy |configuration#Proxy HTTP]. +Devuelve la dirección IP del usuario. Para que funcione correctamente puede hacer falta [configurar un proxy |configuration#Proxy HTTP]. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Devuelve la resolución DNS de la dirección IP del usuario. Para un funcionamiento correcto, puede ser necesario [configurar un proxy |configuration#Proxy HTTP]. +Obsoleto, devuelve siempre `null`. Las consultas DNS inversas eran lentas y poco fiables; si necesita el nombre del host, resuélvalo usted mismo a partir de [getRemoteAddress() |#getRemoteAddress()]. getBasicCredentials(): ?array .[method] --------------------------------------- -Devuelve las credenciales de autenticación para [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. Devuelve un array `[nombre de usuario, contraseña]` o `null`. +Devuelve las credenciales de autenticación de la [autenticación HTTP Basic |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. ```php -[$user, $password] = $httpRequest->getBasicCredentials() ?? [null, null]; +[$user, $password] = $httpRequest->getBasicCredentials(); ``` @@ -184,12 +216,36 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Devuelve el origen desde el que llegó la petición. Un origen se compone del esquema (protocolo), el nombre de host y el puerto, por ejemplo `https://example.com:8080`. Devuelve `null` si la cabecera de origen no está presente o vale `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +El navegador envía la cabecera `Origin` en los siguientes casos: +- peticiones cross-origin (llamadas AJAX a otro dominio) +- peticiones POST, PUT, DELETE y otras que modifican +- peticiones hechas con la Fetch API + +El navegador NO envía la cabecera `Origin` en: +- las peticiones GET corrientes al mismo dominio (navegación same-origin) +- la navegación directa escribiendo una URL en la barra de direcciones +- las peticiones de clientes que no son navegadores + +.[note] +A diferencia de la cabecera `Referer`, `Origin` contiene solo el esquema, el host y el puerto, no la ruta completa de la URL. Eso la hace más adecuada para las comprobaciones de seguridad y preserva la privacidad del usuario. La cabecera `Origin` se usa sobre todo para la validación [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing). + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Detecta el idioma. Como parámetro `$langs`, pasamos un array con los idiomas que soporta la aplicación, y devuelve el que el navegador del visitante preferiría. No es magia, simplemente utiliza la cabecera `Accept-Language`. Si no hay coincidencia, devuelve `null`. +Detecta el idioma. Pase como parámetro `$langs` un array de los idiomas que soporta la aplicación y devolverá el preferido por el navegador del visitante. No es magia; simplemente usa la cabecera `Accept-Language`. Si no encuentra ninguna coincidencia, devuelve `null`. ```php -// el navegador envía p. ej. Accept-Language: es-ES,es;q=0.9,en;q=0.8 +// El navegador envía p. ej.: Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 $langs = ['hu', 'pl', 'en']; // idiomas soportados por la aplicación echo $httpRequest->detectLanguage($langs); // en @@ -199,48 +255,49 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -La clase [api:Nette\Http\RequestFactory] sirve para crear una instancia de `Nette\Http\Request`, que representa la petición HTTP actual. (Si trabaja con Nette, el objeto de la petición HTTP es creado automáticamente por el framework). +La clase [api:Nette\Http\RequestFactory] sirve para crear una instancia de `Nette\Http\Request`, que representa la petición HTTP actual. (Si trabaja con Nette, el framework crea automáticamente el objeto de la petición HTTP.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -El método `fromGlobals()` crea el objeto de la petición basándose en las variables globales actuales de PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` y `$_SERVER`). Al crear el objeto, limpia automáticamente todos los parámetros de entrada GET, POST, COOKIE y también la URL de caracteres de control y secuencias UTF-8 inválidas, lo que garantiza la seguridad al trabajar posteriormente con estos datos. +El método `fromGlobals()` crea el objeto de la petición a partir de las variables globales actuales de PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` y `$_SERVER`). Al crear el objeto limpia automáticamente todos los parámetros de entrada (GET, POST, COOKIE) y también la URL de caracteres de control y secuencias UTF-8 no válidas, lo que garantiza la seguridad al trabajar después con esos datos. RequestFactory se puede configurar antes de llamar a `fromGlobals()`: -- con el método `$factory->setBinary()` desactiva la limpieza automática de los parámetros de entrada de caracteres de control y secuencias UTF-8 inválidas. -- con el método `$factory->setProxy(...)` indica la dirección IP del [servidor proxy |configuration#Proxy HTTP], lo cual es necesario para la correcta detección de la dirección IP del usuario. +- el método `$factory->setBinary()` desactiva la limpieza automática de los parámetros de entrada de caracteres de control y secuencias UTF-8 no válidas. +- el método `$factory->setProxy(...)` indica la dirección IP del [servidor proxy |configuration#Proxy HTTP], necesaria para detectar correctamente la dirección IP del usuario. +- el método `$factory->setForceHttps()` .{data-version:3.3.4} fuerza el esquema HTTPS de la petición independientemente del entorno del servidor. -RequestFactory permite definir filtros que transforman automáticamente partes de la URL de la petición. Estos filtros eliminan caracteres no deseados de la URL, que pueden haber sido insertados allí, por ejemplo, por una implementación incorrecta de sistemas de comentarios en varios sitios web: +RequestFactory permite definir filtros que transforman automáticamente partes de la URL de la petición. Estos filtros eliminan de las URL caracteres no deseados que pueden haber insertado, por ejemplo, implementaciones incorrectas de los sistemas de comentarios de distintos sitios web: ```php -// eliminación de espacios de la ruta +// elimina los espacios de la ruta $requestFactory->urlFilters['path']['%20'] = ''; -// eliminación de punto, coma o paréntesis derecho del final de la URI +// elimina el punto, la coma o el paréntesis derecho del final del URI $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// limpieza de la ruta de barras dobles (filtro por defecto) +// limpia la ruta de barras dobles (filtro predeterminado) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -La primera clave `'path'` o `'url'` determina a qué parte de la URL se aplica el filtro. La segunda clave es la expresión regular que se debe buscar, y el valor es el reemplazo que se utilizará en lugar del texto encontrado. +La primera clave, `'path'` o `'url'`, determina a qué parte de la URL se aplicará el filtro. La segunda clave es la expresión regular que se busca y el valor es el reemplazo que se usará en lugar del texto encontrado. -Archivos cargados -================= +Archivos subidos +================ -El método `Nette\Http\Request::getFiles()` devuelve un array de todas las cargas en una estructura normalizada, cuyas hojas son objetos [api:Nette\Http\FileUpload]. Estos encapsulan los datos enviados por el elemento de formulario `<input type=file>`. +El método `Nette\Http\Request::getFiles()` devuelve un array de todos los archivos subidos en una estructura normalizada cuyas hojas son objetos [api:Nette\Http\FileUpload]. Estos encapsulan los datos enviados por el elemento de formulario `<input type=file>`. -La estructura refleja la nomenclatura de los elementos en HTML. En el caso más simple, puede ser un único elemento de formulario con nombre enviado como: +La estructura refleja los nombres de los elementos en HTML. En el caso más simple puede tratarse de un único elemento de formulario con nombre, enviado como: ```latte <input type="file" name="avatar"> ``` -En este caso, `$request->getFiles()` devuelve un array: +En ese caso, `$request->getFiles()` devuelve el array: ```php [ @@ -248,19 +305,19 @@ En este caso, `$request->getFiles()` devuelve un array: ] ``` -El objeto `FileUpload` se crea incluso si el usuario no envió ningún archivo o el envío falló. Si se envió un archivo lo devuelve el método `hasFile()`: +El objeto `FileUpload` se crea aunque el usuario no haya subido ningún archivo o la subida haya fallado. El método `hasFile()` devuelve true si se envió un archivo: ```php $request->getFile('avatar')?->hasFile(); ``` -En el caso de un nombre de elemento que utiliza la notación de array: +En el caso de un nombre de elemento con notación de array: ```latte <input type="file" name="my-form[details][avatar]"> ``` -el árbol devuelto se ve así: +el árbol devuelto tiene este aspecto: ```php [ @@ -272,13 +329,13 @@ el árbol devuelto se ve así: ] ``` -También se puede crear un array de archivos: +También puede crear arrays de archivos: ```latte <input type="file" name="my-form[details][avatars][]" multiple> ``` -En tal caso, la estructura se ve así: +En ese caso, la estructura tiene este aspecto: ```php [ @@ -294,40 +351,40 @@ En tal caso, la estructura se ve así: ] ``` -La mejor manera de acceder al índice 1 del array anidado es así: +La mejor forma de acceder al índice 1 del array anidado es esta: ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -Dado que no se puede confiar en los datos externos y, por lo tanto, tampoco en la forma de la estructura de archivos, este método es más seguro que, por ejemplo, `$request->getFiles()['my-form']['details']['avatars'][1]`, que podría fallar. +Como no puede fiarse de los datos externos y por tanto tampoco de la estructura de los archivos, este enfoque es más seguro que, por ejemplo, `$request->getFiles()['my-form']['details']['avatars'][1]`, que podría fallar. -Resumen de métodos `FileUpload` .{toc: FileUpload} --------------------------------------------------- +Resumen de los métodos de `FileUpload` .{toc: FileUpload} +--------------------------------------------------------- hasFile(): bool .[method] ------------------------- -Devuelve `true` si el usuario ha cargado algún archivo. +Devuelve `true` si el usuario subió un archivo. isOk(): bool .[method] ---------------------- -Devuelve `true` si el archivo se cargó correctamente. +Devuelve `true` si el archivo se subió correctamente. getError(): int .[method] ------------------------- -Devuelve el código de error de la carga del archivo. Es una de las constantes [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Si la carga fue exitosa, devuelve `UPLOAD_ERR_OK`. +Devuelve el código de error asociado al archivo subido. Es una de las constantes [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php]. Si el archivo se subió correctamente, devuelve `UPLOAD_ERR_OK`. move(string $dest) .[method] ---------------------------- -Mueve el archivo cargado a una nueva ubicación. Si el archivo de destino ya existe, será sobrescrito. +Mueve un archivo subido a una nueva ubicación. Si el archivo de destino ya existe, se sobrescribirá. ```php $file->move('/path/to/files/name.ext'); @@ -336,72 +393,77 @@ $file->move('/path/to/files/name.ext'); getContents(): ?string .[method] -------------------------------- -Devuelve el contenido del archivo cargado. Si la carga no fue exitosa, devuelve `null`. +Devuelve el contenido del archivo subido. Si la subida no fue correcta, devuelve `null`. getContentType(): ?string .[method] ----------------------------------- -Detecta el tipo de contenido MIME del archivo cargado basándose en su firma. Si la carga no fue exitosa o la detección falló, devuelve `null`. +Detecta el tipo de contenido MIME del archivo subido a partir de su firma. Si la subida no fue correcta o la detección falló, devuelve `null`. .[caution] -Requiere la extensión PHP `fileinfo`. +Requiere la extensión de PHP `fileinfo`. getUntrustedName(): string .[method] ------------------------------------ -Devuelve el nombre original del archivo, tal como lo envió el navegador. +Devuelve el nombre original del archivo tal y como lo envió el navegador. .[caution] -No confíe en el valor devuelto por este método. Un cliente podría haber enviado un nombre de archivo malicioso con la intención de dañar o hackear su aplicación. +No se fíe del valor que devuelve este método. Un cliente podría enviar un nombre de archivo malicioso con la intención de dañar o comprometer su aplicación. getSanitizedName(): string .[method] ------------------------------------ -Devuelve el nombre del archivo sanitizado. Contiene solo caracteres ASCII `[a-zA-Z0-9.-]`. Si el nombre no contiene tales caracteres, devuelve `'unknown'`. Si el archivo es una imagen en formato JPEG, PNG, GIF, WebP o AVIF, también devuelve la extensión correcta. +Devuelve el nombre de archivo saneado. Contiene solo caracteres ASCII `[a-zA-Z0-9.-]`. Si el nombre no contiene esos caracteres, devuelve `'unknown'`. Si el archivo es una imagen JPEG, PNG, GIF, WebP o AVIF, devuelve además la extensión de archivo correcta. .[caution] -Requiere la extensión PHP `fileinfo`. +Requiere la extensión de PHP `fileinfo`. getSuggestedExtension(): ?string .[method]{data-version:3.2.4} -------------------------------------------------------------- -Devuelve la extensión de archivo apropiada (sin el punto) correspondiente al tipo MIME detectado. +Devuelve la extensión de archivo adecuada (sin el punto) que corresponde al tipo MIME detectado. .[caution] -Requiere la extensión PHP `fileinfo`. +Requiere la extensión de PHP `fileinfo`. getUntrustedFullPath(): string .[method] ---------------------------------------- -Devuelve la ruta original del archivo, tal como la envió el navegador al cargar una carpeta. La ruta completa solo está disponible en PHP 8.1 y superior. En versiones anteriores, este método devuelve el nombre original del archivo. +Devuelve la ruta original del archivo tal y como la envió el navegador al subir un directorio. La ruta completa solo está disponible en PHP 8.1 y superior. En versiones anteriores, este método devuelve el nombre original del archivo. .[caution] -No confíe en el valor devuelto por este método. Un cliente podría haber enviado un nombre de archivo malicioso con la intención de dañar o hackear su aplicación. +No se fíe del valor que devuelve este método. Un cliente podría enviar un nombre de archivo malicioso con la intención de dañar o comprometer su aplicación. getSize(): int .[method] ------------------------ -Devuelve el tamaño del archivo cargado. Si la carga no fue exitosa, devuelve `0`. +Devuelve el tamaño del archivo subido. Si la subida no fue correcta, devuelve `0`. getTemporaryFile(): string .[method] ------------------------------------ -Devuelve la ruta a la ubicación temporal del archivo cargado. Si la carga no fue exitosa, devuelve `''`. +Devuelve la ruta a la ubicación temporal del archivo subido. Si la subida no fue correcta, devuelve `''`. + + +__toString(): string .[method] +------------------------------ +Devuelve la ruta a la ubicación temporal del archivo subido. Eso permite usar el objeto `FileUpload` directamente como cadena. isImage(): bool .[method] ------------------------- -Devuelve `true` si el archivo cargado es una imagen en formato JPEG, PNG, GIF, WebP o AVIF. La detección se basa en su firma y no verifica la integridad de todo el archivo. Si una imagen está dañada se puede detectar, por ejemplo, intentando [cargarla |#toImage]. +Devuelve `true` si el archivo subido es una imagen JPEG, PNG, GIF, WebP o AVIF. La detección se basa en su firma y no verifica la integridad del archivo entero. Si una imagen está dañada se puede averiguar, por ejemplo, intentando [cargarla |#toImage()]. .[caution] -Requiere la extensión PHP `fileinfo`. +Requiere la extensión de PHP `fileinfo`. getImageSize(): ?array .[method] -------------------------------- -Devuelve un par `[ancho, alto]` con las dimensiones de la imagen cargada. Si la carga no fue exitosa o no es una imagen válida, devuelve `null`. +Devuelve el par `[width, height]` con las dimensiones de la imagen subida. Si la subida no fue correcta o no es una imagen válida, devuelve `null`. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Carga la imagen como un objeto [Image|utils:images]. Si la carga no fue exitosa o no es una imagen válida, lanza una excepción `Nette\Utils\ImageException`. +Carga la imagen como objeto [Image |utils:images]. Si la subida no fue correcta o no es una imagen válida, lanza una `Nette\Utils\ImageException`. diff --git a/http/es/response.texy b/http/es/response.texy index bdbc10e076..30008e381c 100644 --- a/http/es/response.texy +++ b/http/es/response.texy @@ -2,9 +2,9 @@ Respuesta HTTP ************** .[perex] -Nette encapsula la respuesta HTTP en objetos con una API comprensible. +Nette encapsula la respuesta HTTP en objetos con una API clara. -La respuesta HTTP está representada por el objeto [api:Nette\Http\Response]. Si trabaja con Nette, este objeto es creado automáticamente por el framework y puede solicitar que se le pase mediante [inyección de dependencias |dependency-injection:passing-dependencies]. En los presenters, basta con llamar al método `$this->getHttpResponse()`. +La respuesta HTTP está representada por el objeto [api:Nette\Http\Response]. Si trabaja con Nette, el framework crea este objeto automáticamente y puede hacer que se lo pasen mediante [inyección de dependencias |dependency-injection:passing-dependencies]. En los presenters basta con llamar al método `$this->getHttpResponse()`. → [Instalación y requisitos |@home#Instalación] @@ -12,12 +12,12 @@ La respuesta HTTP está representada por el objeto [api:Nette\Http\Response]. Si Nette\Http\Response =================== -El objeto, a diferencia de [Nette\Http\Request|request], es mutable, es decir, puede cambiar el estado mediante setters, por ejemplo, enviar cabeceras. No olvide que todos los setters deben ser llamados **antes de enviar cualquier salida.** Si ya se ha enviado la salida lo indica el método `isSent()`. Si devuelve `true`, cualquier intento de enviar una cabecera lanzará una excepción `Nette\InvalidStateException`. +A diferencia de [Nette\Http\Request |request], este objeto es mutable, así que puede usar setters para cambiar el estado, p. ej. para enviar cabeceras. Recuerde que todos los setters **deben llamarse antes de enviar cualquier salida real**. El método `isSent()` indica si la salida ya se ha enviado. Si devuelve `true`, cualquier intento de enviar una cabecera lanzará una `Nette\InvalidStateException`. setCode(int $code, ?string $reason=null) .[method] -------------------------------------------------- -Cambia el [código de estado de la respuesta |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Para una mejor legibilidad del código fuente, se recomienda utilizar [constantes predefinidas |api:Nette\Http\IResponse] en lugar de números para el código. +Cambia el [código de estado de la respuesta |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Para que el código fuente se lea mejor, se recomienda usar las [constantes predefinidas |api:Nette\Http\IResponse] en lugar de los números. ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -31,12 +31,12 @@ Devuelve el código de estado de la respuesta. isSent(): bool .[method] ------------------------ -Devuelve si las cabeceras ya han sido enviadas desde el servidor al navegador, por lo que ya no es posible enviar cabeceras ni cambiar el código de estado. +Devuelve si ya se han enviado las cabeceras del servidor al navegador, lo que significa que ya no es posible enviar cabeceras ni cambiar el código de estado. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Envía una cabecera HTTP y **sobrescribe** una cabecera enviada previamente con el mismo nombre. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Envía una cabecera HTTP y **sobrescribe** la cabecera del mismo nombre enviada antes. Si `$value` es `null`, la cabecera se elimina. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -Envía una cabecera HTTP y **no sobrescribe** una cabecera enviada previamente con el mismo nombre. +Envía una cabecera HTTP y **no sobrescribe** la cabecera del mismo nombre enviada antes. ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -55,21 +55,21 @@ $httpResponse->addHeader('Accept', 'application/xml'); deleteHeader(string $name) .[method] ------------------------------------ -Elimina una cabecera HTTP enviada previamente. +Borra una cabecera HTTP enviada antes. getHeader(string $header): ?string .[method] -------------------------------------------- -Devuelve la cabecera HTTP enviada o `null` si no existe. El parámetro es insensible a mayúsculas/minúsculas. +Devuelve la cabecera HTTP enviada, o `null` si no existe. El parámetro no distingue mayúsculas de minúsculas. ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ -Devuelve todas las cabeceras HTTP enviadas como un array asociativo. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Devuelve todas las cabeceras HTTP enviadas como array asociativo. ```php $headers = $httpResponse->getHeaders(); @@ -88,7 +88,7 @@ $httpResponse->setContentType('text/plain', 'UTF-8'); redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- -Redirige a otra URL. No olvide terminar el script después. +Redirige a otra URL. Recuerde terminar el script después. ```php $httpResponse->redirect('http://example.com'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- -Establece la expiración del documento HTTP utilizando las cabeceras `Cache-Control` y `Expires`. El parámetro es un intervalo de tiempo (como texto) o `null`, lo que deshabilita el almacenamiento en caché. +setExpiration(?string $expire) .[method] +---------------------------------------- +Establece la expiración del documento HTTP mediante las cabeceras `Cache-Control` y `Expires`. El parámetro es un intervalo de tiempo (como texto) o `null`, que desactiva la caché. ```php -// la caché del navegador expirará en una hora +// la caché del navegador expira en una hora $httpResponse->setExpiration('1 hour'); ``` sendAsFile(string $fileName) .[method] -------------------------------------- -La respuesta se descargará mediante el cuadro de diálogo *Guardar como* con el nombre especificado. El archivo en sí no se envía. +La respuesta se descargará mediante un cuadro de diálogo *Guardar como* con el nombre indicado. No envía el archivo en sí. ```php -$httpResponse->sendAsFile('factura.pdf'); +$httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Envía una cookie. Valores por defecto de los parámetros: +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +Envía una cookie. Valores predeterminados de los parámetros: -| `$path` | `'/'` | la cookie tiene alcance a todas las rutas en el (sub)dominio *(configurable)* -| `$domain` | `null` | lo que significa con alcance al (sub)dominio actual, pero no a sus subdominios *(configurable)* -| `$secure` | `true` | si el sitio web se ejecuta en HTTPS, de lo contrario `false` *(configurable)* -| `$httpOnly` | `true` | la cookie no es accesible para JavaScript -| `$sameSite` | `'Lax'` | la cookie puede no ser enviada al [acceder desde otro dominio |nette:glossary#Cookie SameSite] +| `$path` | `'/'` | la cookie está disponible para todas las rutas del (sub)dominio *(configurable)* +| `$domain` | `null` | es decir, disponible para el (sub)dominio actual, pero no para sus subdominios *(configurable)* +| `$secure` | `auto` | `true` si el sitio funciona con HTTPS, si no `false` (predeterminado del framework; la clase por sí sola usa `false`) *(configurable)* +| `$httpOnly` | `true` | la cookie es inaccesible para JavaScript +| `$sameSite` | `'Lax'` | la cookie puede no enviarse en un [acceso cross-origin |nette:glossary#Cookie SameSite] +| `$partitioned` | `false` | si la cookie está particionada, véase abajo *(desde la v3.4)* -Puede cambiar los valores predeterminados de los parámetros `$path`, `$domain` y `$secure` en la [configuración |configuration#Cookie HTTP]. +Puede cambiar los valores predeterminados de los parámetros `$path`, `$domain` y `$secure` en la [configuración |configuration#Cookies HTTP]. -El tiempo se puede especificar como un número de segundos o una cadena: +La expiración se pasa como número de segundos, como intervalo o fecha en texto, o como objeto `DateTimeInterface`. El valor `null` crea una cookie de sesión, que el navegador descarta al cerrarse. Nette envía la expiración tanto en el atributo `Expires` como en `Max-Age`. ```php -$httpResponse->setCookie('lang', 'es', '100 days'); +$httpResponse->setCookie('lang', 'en', '100 days'); // expira en 100 días +$httpResponse->setCookie('lang', 'en', null); // cookie de sesión ``` -El parámetro `$domain` determina qué dominios pueden aceptar la cookie. Si no se especifica, la cookie es aceptada por el mismo (sub)dominio que la estableció, pero no por sus subdominios. Si se especifica `$domain`, también se incluyen los subdominios. Por lo tanto, especificar `$domain` es menos restrictivo que omitirlo. Por ejemplo, con `$domain = 'nette.org'`, las cookies también están disponibles en todos los subdominios como `doc.nette.org`. +El parámetro `$domain` determina qué dominios pueden aceptar la cookie. Si no se indica, la cookie la acepta el mismo (sub)dominio que la estableció, pero no sus subdominios. Si se indica `$domain`, se incluyen también los subdominios. Por eso, indicar `$domain` es menos restrictivo que omitirlo. Por ejemplo, con `$domain = 'nette.org'` las cookies están disponibles también en todos los subdominios, como `doc.nette.org`. + +Puede pasar el valor de `$sameSite` como enum `Nette\Http\SameSite`: `SameSite::Lax`, `SameSite::Strict` o `SameSite::None` (los valores de cadena `'Lax'`, `'Strict'`, `'None'` también funcionan). Si lo pone a `SameSite::None`, el atributo `$secure` se activa automáticamente, porque los navegadores rechazan una cookie `SameSite=None` que no sea segura. + +.{data-version:3.4.0} +Las cookies particionadas (CHIPS) le dan a una cookie su propio almacén separado para cada sitio de nivel superior. Así, cuando un servicio de terceros (como un widget incrustado) establece una cookie particionada, el navegador guarda una copia distinta para cada sitio en el que aparece el widget, y esas copias no se pueden enlazar entre sí para el seguimiento entre sitios. Actívelas poniendo `$partitioned` a `true`; esto requiere además el atributo `$secure`, así que se activa automáticamente. -Para el valor `$sameSite`, puede usar las constantes `Response::SameSiteLax`, `SameSiteStrict` y `SameSiteNone`. +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- -Elimina una cookie. Los valores por defecto de los parámetros son: +Borra una cookie. Los valores predeterminados de los parámetros son: - `$path` con alcance a todos los directorios (`'/'`) -- `$domain` con alcance al (sub)dominio actual, pero nikoliv sus subdominios -- `$secure` se rige por la configuración en [configuración |configuration#Cookie HTTP] +- `$domain` con alcance al (sub)dominio actual, pero no a sus subdominios +- `$secure` depende de los ajustes de la [configuración |configuration#Cookies HTTP] ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +El objeto [api:Nette\Http\Context] une la petición y la respuesta y ayuda con la caché HTTP. No está registrado como servicio, así que lo crea usted mismo. En los presenters suele ser más fácil usar el método [lastModified() |application:presenters#Caché HTTP]; el contexto resulta útil cuando envía la respuesta usted mismo, por ejemplo desde su propia clase de respuesta. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Determina si el contenido ha cambiado desde la última visita del cliente. Si le pasa la hora de la última modificación, envía la cabecera `Last-Modified`; si le pasa un validador ETag (una cadena corta que identifica la versión actual del contenido, p. ej. su hash), envía la cabecera `ETag`. Después compara ambos con las cabeceras `If-Modified-Since` e `If-None-Match` que envió el navegador. + +Si el navegador ya tiene una versión coincidente, el método establece el código `304 Not Modified` y devuelve `false`; en ese caso no envíe el cuerpo de la respuesta en absoluto. En caso contrario devuelve `true`. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Ambos parámetros son opcionales. Si no conoce la hora de modificación del contenido, use solo el ETag, y al revés. diff --git a/http/es/sessions.texy b/http/es/sessions.texy index db607e8d98..e1feeeda02 100644 --- a/http/es/sessions.texy +++ b/http/es/sessions.texy @@ -3,69 +3,69 @@ Sesiones <div class=perex> -HTTP es un protocolo sin estado, sin embargo, casi todas las aplicaciones necesitan mantener el estado entre peticiones, por ejemplo, el contenido de un carrito de compras. Precisamente para eso sirven las sesiones. Mostraremos: +HTTP es un protocolo sin estado; sin embargo, casi todas las aplicaciones necesitan mantener el estado entre peticiones, como el contenido de un carrito de la compra. Para eso sirven justamente las sesiones. Mostraremos: - cómo usar las sesiones - cómo evitar conflictos de nombres -- cómo configurar la expiración +- cómo establecer la expiración </div> -Al usar sesiones, cada usuario recibe un identificador único llamado ID de sesión, que se pasa en una cookie. Este sirve como clave para los datos de la sesión. A diferencia de las cookies, que se almacenan en el lado del navegador, los datos de la sesión se almacenan en el lado del servidor. +Al usar sesiones, cada usuario recibe un identificador único llamado ID de sesión, que se transmite en una cookie. Este sirve de clave para los datos de la sesión. A diferencia de las cookies, que se guardan en el lado del navegador, los datos de la sesión se guardan en el lado del servidor. -Configuramos la sesión en la [configuración |configuration#Sesión], la opción de tiempo de expiración es especialmente importante. +Las sesiones se configuran en la [configuración |configuration#Sesión]; la elección del tiempo de expiración es especialmente importante. -La gestión de la sesión está a cargo del objeto [api:Nette\Http\Session], al que puede acceder solicitando que se le pase mediante [inyección de dependencias |dependency-injection:passing-dependencies]. En los presenters, basta con llamar a `$session = $this->getSession()`. +De la gestión de la sesión se encarga el objeto [api:Nette\Http\Session], al que puede acceder haciendo que se lo pasen mediante [inyección de dependencias |dependency-injection:passing-dependencies]. En los presenters basta con llamar a `$session = $this->getSession()`. → [Instalación y requisitos |@home#Instalación] -Iniciar sesión -============== +Iniciar la sesión +================= -Nette, en su configuración predeterminada, inicia automáticamente la sesión en el momento en que comenzamos a leer o escribir datos en ella. La sesión se inicia manualmente usando `$session->start()`. +De forma predeterminada, Nette inicia la sesión automáticamente en el momento en que empezamos a leer o escribir datos en ella. Para iniciarla manualmente use `$session->start()`. -PHP envía al iniciar la sesión cabeceras HTTP que afectan al almacenamiento en caché, consulte [php:session_cache_limiter], y posiblemente también una cookie con el ID de sesión. Por lo tanto, es necesario iniciar siempre la sesión antes de enviar cualquier salida al navegador, de lo contrario se lanzará una excepción. Si sabe que se utilizará la sesión durante la renderización de la página, iníciela manualmente antes, por ejemplo, en el presenter. +Al iniciar la sesión, PHP envía cabeceras HTTP que afectan a la caché (véase [php:session_cache_limiter]) y, eventualmente, una cookie con el ID de sesión. Por eso siempre hay que iniciar la sesión antes de enviar cualquier salida al navegador; de lo contrario se lanzará una excepción. Así que, si sabe que durante el renderizado de la página se usará una sesión, iníciela manualmente antes, por ejemplo en el presenter. -En modo de desarrollo, Tracy inicia la sesión porque la utiliza para mostrar barras con redirecciones y peticiones AJAX en la Tracy Bar. +En modo de desarrollo, Tracy inicia la sesión porque la usa para mostrar las barras de las redirecciones y las peticiones AJAX en la Tracy Bar. Secciones ========= -En PHP puro, el almacenamiento de datos de la sesión se realiza como un array accesible a través de la variable global `$_SESSION`. El problema es que las aplicaciones suelen constar de varias partes independientes entre sí y si todas tienen acceso a un solo array, tarde o temprano se producirá una colisión de nombres. +En PHP puro, el almacén de datos de la sesión está implementado como un array accesible mediante la variable global `$_SESSION`. El problema es que las aplicaciones suelen constar de muchas partes independientes y, si todas ellas tienen a disposición un único array, tarde o temprano se producirá una colisión de nombres. -Nette Framework resuelve el problema dividiendo todo el espacio en secciones (objetos [api:Nette\Http\SessionSection]). Cada unidad utiliza entonces su propia sección con un nombre único y ya no puede producirse ninguna colisión. +Nette Framework resuelve este problema dividiendo todo el espacio en secciones (objetos de [api:Nette\Http\SessionSection]). Cada unidad usa entonces su propia sección con un nombre único y no puede producirse ninguna colisión. -Obtenemos la sección de la sesión: +Obtenemos una sección de la sesión: ```php -$section = $session->getSection('nombreUnico'); +$section = $session->getSection('unique name'); ``` -En el presenter, basta con usar `getSession()` con un parámetro: +En el presenter basta con usar `getSession()` con un parámetro: ```php -// $this es Presenter -$section = $this->getSession('nombreUnico'); +// $this es un Presenter +$section = $this->getSession('unique name'); ``` -La existencia de la sección se puede verificar con el método `$session->hasSection('nombreUnico')`. +La existencia de una sección se puede comprobar con el método `$session->hasSection('unique name')`. La lista de los nombres de todas las secciones existentes la devuelve `$session->getSectionNames()`. -Trabajar con la sección en sí es muy fácil usando los métodos `set()`, `get()` y `remove()`: +Trabajar con la sección en sí es después facilísimo con los métodos `set()`, `get()` y `remove()`: ```php -// escribir variable -$section->set('userName', 'juan'); +// escritura de una variable +$section->set('userName', 'john'); -// leer variable, devuelve null si no existe +// lectura de una variable, devuelve null si no existe echo $section->get('userName'); -// eliminar variable +// eliminación de una variable $section->remove('userName'); ``` -Para obtener todas las variables de la sección, se puede usar el bucle `foreach`: +Para obtener todas las variables de una sección puede usar un bucle `foreach`: ```php foreach ($section as $key => $val) { @@ -74,46 +74,46 @@ foreach ($section as $key => $val) { ``` -Configuración de la expiración ------------------------------- +Cómo establecer la expiración +----------------------------- -Es posible configurar la expiración para secciones individuales o incluso variables individuales. Podemos así dejar que el inicio de sesión del usuario expire en 20 minutos, pero seguir recordando el contenido del carrito. +La expiración se puede establecer para secciones concretas o incluso para variables concretas. Podemos hacer que el acceso de un usuario caduque a los 20 minutos y recordar aun así el contenido del carrito de la compra. ```php -// la sección expirará después de 20 minutos +// la sección expira a los 20 minutos $section->setExpiration('20 minutes'); ``` -Para configurar la expiración de variables individuales, se utiliza el tercer parámetro del método `set()`: +Para establecer la expiración de variables concretas use el tercer parámetro del método `set()`: ```php -// la variable 'flash' expirará después de 30 segundos +// la variable 'flash' expira a los 30 segundos $section->set('flash', $message, '30 seconds'); ``` .[note] -No olvide que el tiempo de expiración de toda la sesión (consulte [configuración de la sesión |configuration#Sesión]) debe ser igual o mayor que el tiempo establecido para secciones o variables individuales. +Recuerde que el tiempo de expiración de toda la sesión (véase la [configuración de la sesión |configuration#Sesión]) debe ser igual o mayor que el tiempo establecido para las secciones o variables concretas. -La cancelación de una expiración previamente establecida se logra con el método `removeExpiration()`. La cancelación inmediata de toda la sección la asegura el método `remove()`. +Para anular una expiración establecida antes use el método `removeExpiration()`; para borrar la expiración de una variable concreta, pase su nombre: `removeExpiration('flash')`. Para eliminar de inmediato toda la sección use el método `remove()`. Eventos $onStart, $onBeforeWrite -------------------------------- -El objeto `Nette\Http\Session` tiene [eventos |nette:glossary#Eventos] `$onStart` y `$onBeforeWrite`, por lo que puede añadir callbacks que se invocarán después de iniciar la sesión o antes de escribirla en el disco y su posterior finalización. +El objeto `Nette\Http\Session` tiene los [eventos |nette:glossary#Eventos] `$onStart` y `$onBeforeWrite`, así que puede añadir callbacks que se invocan después de iniciar la sesión o antes de escribirla en disco y terminarla a continuación. ```php $session->onBeforeWrite[] = function () { - // escribimos datos en la sesión + // escribe datos en la sesión $this->section->set('basket', $this->basket); }; ``` -Gestión de sesiones -=================== +Gestión de la sesión +==================== -Resumen de los métodos de la clase `Nette\Http\Session` para la gestión de sesiones: +Resumen de los métodos de la clase `Nette\Http\Session` para gestionar la sesión: <div class=wiki-methods-brief> @@ -135,12 +135,12 @@ Termina la sesión. La sesión termina automáticamente al final de la ejecució destroy(): void .[method] ------------------------- -Termina y elimina la sesión. +Termina y borra la sesión. exists(): bool .[method] ------------------------ -¿Contiene la petición HTTP una cookie con el ID de sesión? +¿Contiene la petición HTTP una cookie con un ID de sesión? regenerateId(): void .[method] @@ -150,7 +150,7 @@ Genera un nuevo ID de sesión aleatorio. Los datos se conservan. getId(): string .[method] ------------------------- -Devuelve el ID de sesión. +Devuelve el ID de la sesión. </div> @@ -158,14 +158,14 @@ Devuelve el ID de sesión. Configuración ------------- -Configuramos la sesión en la [configuración |configuration#Sesión]. Si escribe una aplicación que no utiliza un contenedor DI, estos métodos sirven para la configuración. Deben llamarse antes de iniciar la sesión. +La sesión se configura en la [configuración |configuration#Sesión]. Si escribe una aplicación que no usa un contenedor DI, use estos métodos para configurarla. Hay que llamarlos antes de iniciar la sesión. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Establece el nombre de la cookie en la que se transmite el ID de sesión. El nombre estándar es `PHPSESSID`. Es útil si ejecuta varias aplicaciones diferentes en el mismo sitio web. +Establece el nombre de la cookie en la que se transmite el ID de sesión. El nombre estándar es `PHPSESSID`. Es útil si ejecuta varias aplicaciones distintas en el mismo sitio web. getName(): string .[method] @@ -175,37 +175,37 @@ Devuelve el nombre de la cookie en la que se transmite el ID de sesión. setOptions(array $options): static .[method] -------------------------------------------- -Configura la sesión. Se pueden establecer todas las [directivas de sesión |https://www.php.net/manual/en/session.configuration.php] de PHP (en formato camelCase, p. ej., en lugar de `session.save_path` escribimos `savePath`) y también [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. +Configura la sesión. Se pueden establecer todas las [directivas de sesión |https://www.php.net/manual/en/session.configuration.php] de PHP (en formato camelCase, p. ej. escriba `savePath` en lugar de `session.save_path`) y también [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Establece el período de inactividad después del cual la sesión expira. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Establece el tiempo de inactividad tras el cual expira la sesión. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Configuración de los parámetros de la cookie. Puede cambiar los valores predeterminados de los parámetros en la [configuración |configuration#Cookie de sesión]. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +Establece los parámetros de las cookies. Puede cambiar los valores predeterminados de los parámetros en la [configuración |configuration#Cookie de sesión]. setSavePath(string $path): static .[method] ------------------------------------------- -Establece el directorio donde se guardan los archivos de sesión. +Establece el directorio donde se guardan los archivos de la sesión. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Configuración de un manejador personalizado, consulte la [documentación de PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Establece un handler propio, véase la [documentación de PHP |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> -La seguridad es lo primero -========================== +La seguridad ante todo +====================== -El servidor asume que se comunica siempre con el mismo usuario mientras las peticiones vayan acompañadas del mismo ID de sesión. La tarea de los mecanismos de seguridad es asegurar que esto sea realmente así y que no sea posible robar o suplantar el identificador. +El servidor da por hecho que se comunica con el mismo usuario mientras las peticiones vayan acompañadas del mismo ID de sesión. La tarea de los mecanismos de seguridad es asegurar que así sea de verdad y que el identificador no se pueda robar ni sustituir. -Por lo tanto, Nette Framework configura correctamente las directivas PHP para que el ID de sesión se transmita únicamente en la cookie, lo haga inaccesible para JavaScript e ignore los posibles identificadores en la URL. Además, en momentos críticos, como el inicio de sesión del usuario, genera un nuevo ID de sesión. +Por eso Nette Framework configura correctamente las directivas de PHP para transmitir el ID de sesión solo en cookies, hacerlo inaccesible para JavaScript e ignorar cualquier identificador que venga en la URL. Además, en momentos críticos, como el inicio de sesión del usuario, genera un nuevo ID de sesión. .[note] -Para la configuración de PHP se utiliza la función `ini_set`, que lamentablemente algunos hostings prohíben. Si es el caso de su proveedor de hosting, intente negociar con él para que le permita la función o al menos configure el servidor. +Para configurar PHP se usa la función `ini_set`, pero por desgracia algunos proveedores de hosting prohíben su uso. Si es el caso de su hosting, intente acordar con ellos que le permitan esa función o, al menos, que configuren el servidor correctamente. diff --git a/http/es/ssrf.texy b/http/es/ssrf.texy new file mode 100644 index 0000000000..648b4e4072 --- /dev/null +++ b/http/es/ssrf.texy @@ -0,0 +1,183 @@ +Protección contra SSRF +********************** + +.[perex] +Cuando su aplicación descarga una URL proporcionada por el usuario, un atacante puede aprovecharlo para llegar a su red interna. Las clases [#UrlValidator] e [#IPAddress] le ayudan a protegerse de estos ataques Server-Side Request Forgery (SSRF). + +→ [Instalación y requisitos |@home#Instalación] + + +¿Qué es el SSRF? +================ + +Imagine una funcionalidad en la que el usuario introduce una URL y su servidor la descarga: un avatar de una dirección remota, el destino de un webhook, la vista previa de un enlace. Parece inofensivo, pero quien accede a la dirección es el servidor, no el navegador del usuario. Y el servidor ve sitios que el atacante no ve: la interfaz de loopback, la red privada, los servicios en la nube. + +Por eso el atacante envía una URL que apunta hacia dentro en lugar de a la internet pública. Los objetivos típicos son: + +- los metadatos de la nube en `http://169.254.169.254/`, que pueden filtrar claves de acceso +- paneles de administración internos y routers como `http://192.168.1.1/` +- servicios sin autenticación, como Redis en `http://localhost:6379/` + +Esta clase de vulnerabilidad es tan habitual que figura en el [OWASP Top 10 |https://owasp.org/Top10/]. La defensa consiste en validar la URL **antes** de descargarla y rechazar todo lo que se resuelva a una dirección no pública. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] comprueba una URL frente a una política configurable: el esquema, el puerto, el host, la información de usuario y las direcciones IP a las que se resuelve el host. El uso básico es una sola llamada: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // URL insegura, no la descargue +} +``` + +La política predeterminada es deliberadamente estricta: solo acepta `https` en el puerto 443 apuntando a una dirección IP pública. Todo lo demás (loopback, rangos privados, link-local incluidos los metadatos de la nube, rangos reservados) se rechaza, y el multicast se rechaza sin condiciones. Es el punto de partida correcto para descargar URL arbitrarias proporcionadas por el usuario. + + +Configurar la política +---------------------- + +La política se moldea con el constructor. Por ejemplo, para permitir `http` a secas en cualquier puerto y llegar a direcciones privadas (útil dentro de una red de confianza): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // cualquier puerto + allowPrivateIps: true, +); +``` + +Un patrón habitual es limitar la descarga a un conjunto fijo de dominios asociados mediante una lista blanca de hosts. El prefijo `*.` casa con cualquier profundidad de subdominio, pero no con el dominio raíz; indique ambas formas si lo necesita: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +El conjunto completo de opciones del constructor: + +| Parámetro | Predeterminado | Significado +|--------------------- +| `schemes` | `['https']` | esquemas permitidos; `[]` lo rechaza todo +| `ports` | `[443]` | puertos permitidos, `null` = cualquiera; se respeta el puerto implícito del esquema +| `allowPrivateIps` | `false` | permite los rangos privados (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | permite el loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | permite link-local, incluidos los metadatos de la nube 169.254.169.254 +| `allowReserved` | `false` | permite los rangos reservados por la IANA +| `allowUserinfo` | `false` | permite `user:pass@` en la URL +| `hostAllowlist` | `null` | si se indica, el host debe encajar con algún patrón; `[]` los rechaza todos +| `hostBlocklist` | `null` | si se indica, el host no debe encajar con ningún patrón + + +Métodos de validación +--------------------- + +El validador ofrece tres métodos. `allows()` ejecuta la comprobación completa, incluida la resolución DNS: el host se resuelve y **todas** las direcciones A/AAAA deben pasar la política de IP: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` se salta la resolución DNS y las comprobaciones de rangos de IP. Úselo como prefiltro rápido, o cuando la validación DNS se delega en la capa de descarga: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Ambos métodos aceptan una cadena, un objeto [UrlImmutable |urls#UrlImmutable] o `null` (que siempre falla). + + +Derrotar el DNS rebinding +------------------------- + +Hay una sutil carrera entre la validación y la descarga: un atacante puede devolver una IP segura cuando valida el host y después cambiar el DNS a una IP interna para la descarga real. Para cerrar ese agujero, `getResolvedIPs()` devuelve las direcciones IP validadas, y usted fija la conexión a ellas para que la descarga no se pueda redirigir a otro sitio: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // URL insegura +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... ejecuta la petición +``` + +El método devuelve un array de cadenas con las IP (primero los registros A, después los AAAA) que pasaron la política completa, o un array vacío ante cualquier fallo. Si la URL contiene una IP literal, valida la dirección directamente y no hace ninguna consulta DNS. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] es un objeto de valor inmutable para trabajar con direcciones IPv4 e IPv6. `UrlValidator` lo usa internamente, pero también resulta práctico por sí solo siempre que clasifique direcciones. El constructor lanza `Nette\InvalidArgumentException` si la dirección no es válida: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +Cuando no quiera una excepción, use la fábrica `tryFrom()` o el comprobador `isValid()`: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Clasificación de las direcciones +-------------------------------- + +Los predicados le dicen a qué clase pertenece una dirección. El clave es `isPublic()`: verdadero solo para las direcciones enrutables públicamente, que es exactamente lo que quiere una protección contra SSRF: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (rango de metadatos de la nube) +``` + +El conjunto completo de predicados: + +| Método | Comprueba +|-------------------- +| `isPublic()` | enrutable públicamente (ninguno de los siguientes) +| `isPrivate()` | rangos privados RFC 1918 / 4193 +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (incl. metadatos de la nube), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | reservados por la IANA (documentación, CGNAT, uso futuro, …) + + +Pertenencia a un rango +---------------------- + +`isInRange()` comprueba si la dirección cae dentro de un bloque CIDR. Puede pasar una red con prefijo, o una dirección a secas para una coincidencia exacta (/32 implícito para IPv4, /128 para IPv6): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (coincidencia exacta) +``` + +Una entrada mal formada o de otra familia de IP devuelve `false`. + + +IPv6 con IPv4 mapeada +--------------------- + +Las direcciones escritas como IPv6 con IPv4 mapeada (como `::ffff:127.0.0.1`) son una forma clásica de colarse por filtros ingenuos. `IPAddress` las normaliza, así que los predicados de rango ven a través del disfraz: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +Los métodos `isIPv4()` e `isIPv6()` informan de la forma textual: una dirección mapeada es IPv6, no IPv4. diff --git a/http/es/upgrading.texy b/http/es/upgrading.texy new file mode 100644 index 0000000000..f96b1f5b4d --- /dev/null +++ b/http/es/upgrading.texy @@ -0,0 +1,54 @@ +Actualización +************* + + +Actualización a la versión 3.4 +============================== + +La versión mínima de PHP requerida es la 8.3. + +- el método `Request::isSameSite()` está obsoleto en favor de `isFrom()`, que determina el origen de la petición a partir de las cabeceras `Sec-Fetch-*`. La protección automática de los formularios y las señales se vuelve más precisa y cambia un comportamiento: la navegación directa (un marcador, una dirección escrita a mano, un enlace en un correo) ya no se considera same-site. Si una señal depende de enlaces de acción en correos, márquela con `#[Requires(sameOrigin: false)]`. +- la cookie `_nss` se envía ahora solo a los navegadores que no envían la cabecera `Sec-Fetch-Site` +- `setCookie()` envía el atributo `Max-Age` y fuerza la bandera `Secure` en las cookies con `SameSite=None` y en las particionadas +- el enum `SameSite` sustituye a las constantes `IResponse::SameSiteLax` etc., que quedan obsoletas +- la expiración se interpreta igual en todas partes: un número es un número relativo de segundos, una cadena es un intervalo o una fecha. Pasar un timestamp UNIX absoluto está obsoleto y una cookie de sesión se representa con `null` en lugar de `0`. +- el método obsoleto `Request::getRemoteHost()` devuelve `null` +- se ha eliminado la clase, obsoleta desde hace tiempo, `Nette\Http\UserStorage` + +Toda la historia del cambio a las cabeceras `Sec-Fetch-*` se cuenta en el artículo [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Actualización a la versión 3.2 +============================== + +- las credenciales de la autenticación HTTP Basic ya no forman parte del objeto `Url`, así que `$url->getUser()` y `$url->getPassword()` devuelven una cadena vacía. Léalas con el nuevo método `$request->getBasicCredentials()`. + +Los motivos de este cambio se explican en el artículo [Nette Http 3.2: change access to credentials |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]. + + +Actualización a la versión 3.1 +============================== + +- las cookies se envían con la bandera `sameSite: Lax` +- `cookieSecure` tiene ahora el valor predeterminado 'auto' +- la opción `session.cookieSecure` está obsoleta; en su lugar se usa `http.cookieSecure` +- la cookie `nette-samesite` se ha renombrado a `_nss` +- `Nette\Http\Request::getFile()` acepta un array de claves y devuelve `FileUpload|null` +- `Nette\Http\Session::getCookieParameters()` está obsoleto +- `Nette\Http\FileUpload::getName()` se ha renombrado a `getUntrustedName()` +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()` y `getRelativeUrl()` están obsoletos (estos métodos forman parte de `UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` está obsoleto +- `Nette\Http\FileUpload::getImageSize()` devuelve el par `[width, height]` +- con `autoStart: smart` (el valor predeterminado), la sesión ya no se inicia justo después de arrancar la aplicación solo porque el navegador haya enviado una cookie de sesión; se inicia en la primera lectura o escritura. Se han añadido los valores `always` y `never`. +- cuando el navegador envía un ID de sesión para el que no existe ninguna sesión, Nette borra la cookie en lugar de crear una sesión nueva +- para acceder a las secciones de la sesión prefiera los métodos `set()`, `get()` y `remove()`; a diferencia del acceso por propiedad, distinguen correctamente la lectura de la escritura y no inician la sesión innecesariamente +- los valores predeterminados de `cookiePath` y `cookieDomain` se pueden establecer en la configuración + +El comportamiento de las sesiones se describe en detalle en el artículo [Nette Http 3.1: much smarter sessions |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions]. + + +Actualización a la versión 3.0 +============================== + +- el objeto `Nette\Http\UrlScript` (que devuelve, por ejemplo, `Nette\Http\Request::getUrl()`) es ahora inmutable +- en `new Nette\Http\Url('abcd')`, `abcd` representa la ruta, no el dominio; desde la 3.0, `(new Nette\Http\Url('abcd'))->setScheme('http')` genera correctamente `http:abcd` en lugar del anterior `http://abcd` diff --git a/http/es/urls.texy b/http/es/urls.texy index 3a124786f6..f08ebb32de 100644 --- a/http/es/urls.texy +++ b/http/es/urls.texy @@ -2,7 +2,7 @@ Trabajar con URLs ***************** .[perex] -Las clases [#Url], [#UrlImmutable] y [#UrlScript] permiten generar, analizar y manipular URLs fácilmente. +Las clases [#Url], [#UrlImmutable] y [#UrlScript] facilitan generar, analizar y manipular URL. → [Instalación y requisitos |@home#Instalación] @@ -10,7 +10,7 @@ Las clases [#Url], [#UrlImmutable] y [#UrlScript] permiten generar, analizar y m Url === -La clase [api:Nette\Http\Url] permite trabajar fácilmente con URLs y sus componentes individuales, que captura este diagrama: +La clase [api:Nette\Http\Url] permite manipular con facilidad las URL y sus distintos componentes, tal y como se ve en este diagrama: /--pre scheme user password host port path query fragment @@ -22,7 +22,7 @@ La clase [api:Nette\Http\Url] permite trabajar fácilmente con URLs y sus compon hostUrl authority \-- -La generación de URLs es intuitiva: +Generar URL es intuitivo: ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -También se puede analizar una URL y manipularla posteriormente: +También puede analizar una URL y manipularla después: ```php $url = new Url( @@ -44,7 +44,7 @@ $url = new Url( ); ``` -La clase `Url` implementa la interfaz `JsonSerializable` y tiene un método `__toString()`, por lo que el objeto se puede imprimir o usar en datos pasados a `json_encode()`. +La clase `Url` implementa la interfaz `JsonSerializable` y tiene el método `__toString()`, así que el objeto se puede imprimir o usar en los datos que se pasan a `json_encode()`. ```php echo $url; @@ -52,10 +52,10 @@ echo json_encode([$url]); ``` -Componentes de URL .[method] ----------------------------- +Componentes de la URL +--------------------- -Para devolver o cambiar los componentes individuales de la URL, tiene a su disposición estos métodos: +Para obtener o modificar los distintos componentes de la URL están disponibles los siguientes métodos: .[language-php] | Setter | Getter | Valor devuelto @@ -69,19 +69,22 @@ Para devolver o cambiar los componentes individuales de la URL, tiene a su dispo | `setPath(string $path)` | `getPath(): string` | `'/en/download'` | `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` +| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL completa +| | `getAbsoluteUrl(): string` | la URL entera -Advertencia: Cuando trabaje con una URL obtenida de una [petición HTTP|request], tenga en cuenta que no contendrá el fragmento, ya que el navegador no lo envía al servidor. +Los métodos `getUser()`, `getPassword()`, `setUser()` y `setPassword()` están obsoletos, porque no se recomienda incrustar las credenciales directamente en la URL. -También podemos trabajar con parámetros de consulta individuales usando: +Atención: al trabajar con una URL obtenida de una [petición HTTP |request], tenga presente que no contendrá el fragmento, porque el navegador no lo envía al servidor. + +También podemos trabajar con los distintos parámetros de consulta mediante: .[language-php] | Setter | Getter |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] @@ -98,18 +101,23 @@ Devuelve la parte derecha o izquierda del host. Así funciona si el host es `www | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Comprueba si dos URLs son idénticas. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +Comprueba si dos URL son idénticas. ```php $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Convierte la URL a su forma canónica. Convierte el nombre de host a minúsculas y normaliza la ruta (codificación por porcentaje y eliminación de caracteres redundantes). La cadena de consulta se deja sin cambios. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- -Comprueba si la URL es absoluta. Una URL se considera absoluta si comienza con un esquema (p. ej., http, https, ftp) seguido de dos puntos. +Comprueba si una URL es absoluta. Una URL se considera absoluta si empieza por un esquema (p. ej. http, https, ftp) seguido de dos puntos. ```php Url::isAbsolute('https://nette.org'); // true @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -Normaliza la ruta en la URL eliminando los segmentos especiales `.` y `..`. El método elimina los elementos redundantes de la ruta de la misma manera que lo hacen los navegadores web. +Normaliza la ruta de una URL eliminando los segmentos especiales `.` y `..`. Este método elimina los elementos redundantes de la ruta igual que hacen los navegadores web. ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,24 +139,24 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -La clase [api:Nette\Http\UrlImmutable] es una alternativa inmutable a la clase [#Url] (similar a como `DateTimeImmutable` en PHP es la alternativa inmutable a `DateTime`). En lugar de setters, tiene los llamados withers, que no modifican el objeto, sino que devuelven nuevas instancias con el valor modificado: +La clase [api:Nette\Http\UrlImmutable] es la alternativa inmutable a la clase [#Url] (de forma parecida a como `DateTimeImmutable` es la alternativa inmutable a `DateTime` en PHP). En lugar de setters tiene "withers", que no cambian el objeto sino que devuelven nuevas instancias con el valor modificado: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/es/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/es/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` -La clase `UrlImmutable` implementa la interfaz `JsonSerializable` y tiene un método `__toString()`, por lo que el objeto se puede imprimir o usar en datos pasados a `json_encode()`. +La clase `UrlImmutable` implementa la interfaz `JsonSerializable` y tiene el método `__toString()`, así que el objeto se puede imprimir o usar en los datos que se pasan a `json_encode()`. ```php echo $url; @@ -156,10 +164,10 @@ echo json_encode([$url]); ``` -Componentes de URL .[method] ----------------------------- +Componentes de la URL +--------------------- -Para devolver o cambiar los componentes individuales de la URL sirven los métodos: +Para obtener o cambiar los distintos componentes de la URL están disponibles los siguientes métodos: .[language-php] | Wither | Getter | Valor devuelto @@ -173,13 +181,13 @@ Para devolver o cambiar los componentes individuales de la URL sirven los métod | `withPath(string $path)` | `getPath(): string` | `'/en/download'` | `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` +| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL completa +| | `getAbsoluteUrl(): string` | la URL entera -El método `withoutUserInfo()` elimina `user` y `password`. +Los métodos `getUser()`, `getPassword()`, `withUser()`, `withPassword()` y `withoutUserInfo()` están obsoletos, porque no se recomienda incrustar las credenciales directamente en la URL. -También podemos trabajar con parámetros de consulta individuales usando: +También podemos trabajar con los distintos parámetros de consulta mediante: .[language-php] | Wither | Getter @@ -204,11 +212,11 @@ Devuelve la parte derecha o izquierda del host. Así funciona si el host es `www resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -Deriva una URL absoluta de la misma manera que un navegador procesa los enlaces en una página HTML: -- si el enlace es una URL absoluta (contiene un esquema), se utiliza sin cambios -- si el enlace comienza con `//`, solo se toma el esquema de la URL actual -- si el enlace comienza con `/`, se crea una ruta absoluta desde la raíz del dominio -- en otros casos, la URL se construye relativamente a la ruta actual +Resuelve una URL absoluta igual que un navegador procesa los enlaces de una página HTML: +- si el enlace es una URL absoluta (contiene un esquema), se usa sin cambios +- si el enlace empieza por `//`, se adopta solo el esquema de la URL actual +- si el enlace empieza por `/`, se crea una ruta absoluta desde la raíz del dominio +- en los demás casos, la URL se construye relativa a la ruta actual ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,9 +226,9 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Comprueba si dos URLs son idénticas. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +Comprueba si dos URL son idénticas. ```php $url->isEqual('https://nette.org'); @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -La clase [api:Nette\Http\UrlScript] es descendiente de [#UrlImmutable] y la extiende con componentes virtuales adicionales de la URL, como el directorio raíz del proyecto, etc. Al igual que la clase padre, es un objeto inmutable. +La clase [api:Nette\Http\UrlScript] es descendiente de [#UrlImmutable] y la amplía con componentes virtuales adicionales de la URL, como el directorio raíz del proyecto, etc. Igual que su clase padre, es un objeto inmutable. -El siguiente diagrama muestra los componentes que UrlScript reconoce: +El siguiente diagrama muestra los componentes que reconoce UrlScript: /--pre baseUrl basePath relativePath relativeUrl @@ -244,14 +252,14 @@ El siguiente diagrama muestra los componentes que UrlScript reconoce: scriptPath pathInfo \-- -- `baseUrl` es la dirección URL base de la aplicación, incluido el dominio y la parte de la ruta al directorio raíz de la aplicación -- `basePath` es la parte de la ruta al directorio raíz de la aplicación +- `baseUrl` es la URL base de la aplicación, incluidos el dominio y la parte de la ruta hasta el directorio raíz de la aplicación +- `basePath` es la parte de la ruta hasta el directorio raíz de la aplicación - `scriptPath` es la ruta al script actual -- `relativePath` es el nombre del script (posiblemente segmentos de ruta adicionales) relativo a basePath -- `relativeUrl` es toda la parte de la URL después de baseUrl, incluida la cadena de consulta y el fragmento. -- `pathInfo` hoy en día una parte de la URL poco utilizada después del nombre del script +- `relativePath` es el nombre del script (y, eventualmente, más segmentos de la ruta) relativo a `basePath` +- `relativeUrl` es toda la parte de la URL posterior a `baseUrl`, incluidas la cadena de consulta y el fragmento +- `pathInfo` es una parte de la URL, hoy poco usada, posterior al nombre del script -Para devolver partes de la URL están disponibles los métodos: +Para obtener estas partes de la URL están disponibles los siguientes métodos: .[language-php] | Getter | Valor devuelto @@ -259,8 +267,8 @@ Para devolver partes de la URL están disponibles los métodos: | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Los objetos `UrlScript` normalmente no los creamos directamente, sino que los devuelve el método [Nette\Http\Request::getUrl()|request] con los componentes ya correctamente configurados para la petición HTTP actual. +Normalmente no creamos los objetos `UrlScript` directamente; en su lugar, el método [Nette\Http\Request::getUrl() |request] lo devuelve con los componentes ya correctamente establecidos para la petición HTTP actual. diff --git a/http/fr/@home.texy b/http/fr/@home.texy index dbef36f4e8..6fae4e54f2 100644 --- a/http/fr/@home.texy +++ b/http/fr/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -Le paquet `nette/http` encapsule la [requête HTTP |request] & la [réponse |response], le travail avec les [sessions |sessions] et [l'analyse et la composition d'URL |urls]. +Le paquet `nette/http` est votre compagnon pour toute la communication HTTP. Il offre une API objet claire au-dessus de la requête entrante et de la réponse sortante, simplifie le travail avec les sessions et les adresses URL, et veille par-dessus le marché à la sécurité. Voici ce que vous y trouverez : + +| [Requête HTTP |request] | requête entrante et assainissement des entrées +| [Réponse HTTP |response] | réponse sortante, en-têtes et cookies +| [Sessions|sessions] | persistance sûre de l'état entre les requêtes +| [Utilitaire URL |urls] | analyse et composition des adresses URL +| [Protection contre le SSRF |ssrf] | défense contre les attaques Server-Side Request Forgery +| [Configuration|configuration] | options de configuration du paquet Installation ------------ -La bibliothèque peut être téléchargée et installée en utilisant l'outil [Composer|best-practices:composer] : +Téléchargez et installez le paquet à l'aide de [Composer|best-practices:composer] : ```shell composer require nette/http ``` + +Le paquet nécessite PHP en version 8.3 à 8.5. diff --git a/http/fr/@left-menu.texy b/http/fr/@left-menu.texy index 57940fc1f4..c7987bdc4b 100644 --- a/http/fr/@left-menu.texy +++ b/http/fr/@left-menu.texy @@ -3,6 +3,17 @@ Nette HTTP - [Introduction |@home] - [Requête HTTP |request] - [Réponse HTTP |response] -- [Sessions |Sessions] -- [Utilitaires URL |urls] -- [Configuration |configuration] +- [Sessions|sessions] +- [Travailler avec les URL |urls] +- [Protection contre le SSRF |ssrf] +- [Configuration|configuration] +- [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/http/fr/configuration.texy b/http/fr/configuration.texy index 8c6b5c7944..cb5e34d45d 100644 --- a/http/fr/configuration.texy +++ b/http/fr/configuration.texy @@ -2,9 +2,9 @@ Configuration HTTP ****************** .[perex] -Aperçu des options de configuration pour Nette HTTP. +Aperçu des options de configuration de Nette HTTP. -Si vous n'utilisez pas l'ensemble du framework, mais seulement cette bibliothèque, lisez [comment charger la configuration |bootstrap:]. +Si vous n'utilisez pas tout le framework, mais seulement cette bibliothèque, lisez [comment charger la configuration|bootstrap:]. En-têtes HTTP @@ -12,29 +12,31 @@ En-têtes HTTP ```neon http: - # en-têtes qui seront envoyés avec chaque requête + # en-têtes envoyés avec chaque réponse headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' - # affecte l'en-tête X-Frame-Options - frames: ... # (string|bool) la valeur par défaut est 'SAMEORIGIN' + # influence l'en-tête X-Frame-Options + frames: ... # (string|bool|null) 'SAMEORIGIN' par défaut ``` -Pour des raisons de sécurité, le framework envoie l'en-tête `X-Frame-Options: SAMEORIGIN`, qui indique que la page ne peut être affichée à l'intérieur d'une autre page (dans un élément `<iframe>`) que si elle se trouve sur le même domaine. Cela peut être indésirable dans certaines situations (par exemple, si vous développez une application pour Facebook), le comportement peut donc être modifié en définissant `frames: http://allowed-host.com` ou `frames: true`. +Pour des raisons de sécurité, le framework envoie l'en-tête `X-Frame-Options: SAMEORIGIN`, qui indique qu'une page ne peut être affichée à l'intérieur d'une autre page (dans un élément `<iframe>`) que si elle se trouve sur le même domaine. Cela peut être indésirable dans certaines situations (par exemple si vous développez une application Facebook) ; le comportement peut donc être modifié en définissant `frames: http://allowed-host.com` pour autoriser un hôte précis, `frames: true` pour autoriser l'inclusion depuis n'importe où (l'en-tête est omis), ou `frames: false` pour l'interdire complètement (`X-Frame-Options: DENY`). + +Par défaut, Nette envoie aussi les en-têtes `X-Powered-By: Nette Framework 3` et `Content-Type: text/html; charset=utf-8`. Vous pouvez supprimer n'importe quel en-tête, y compris ceux par défaut, en fixant sa valeur à une chaîne vide. Content Security Policy ----------------------- -Il est facile de construire des en-têtes `Content-Security-Policy` (ci-après CSP), dont la description se trouve dans la [description de CSP |https://content-security-policy.com]. Les directives CSP (comme par exemple `script-src`) peuvent être écrites soit comme des chaînes de caractères selon la spécification, soit comme des tableaux de valeurs pour une meilleure lisibilité. Il n'est alors pas nécessaire d'écrire des guillemets autour des mots-clés, comme par exemple `'self'`. Nette génère également automatiquement la valeur `nonce`, de sorte que l'en-tête contiendra par exemple `'nonce-y4PopTLM=='`. +Les en-têtes `Content-Security-Policy` (CSP) se configurent facilement ; leur description se trouve dans la [spécification CSP |https://content-security-policy.com]. Les directives CSP (comme `script-src`) peuvent s'écrire soit sous forme de chaînes conformes à la spécification, soit sous forme de tableaux de valeurs, plus lisibles. Il n'est alors pas nécessaire d'entourer de guillemets les mots-clés comme `'self'`. Nette générera aussi automatiquement une valeur `nonce`, si bien que quelque chose comme `'nonce-y4PopTLM=='` sera envoyé dans l'en-tête. ```neon http: # Content Security Policy csp: - # chaîne de caractères au format selon la spécification CSP + # chaîne conforme à la spécification CSP default-src: "'self' https://example.com" # tableau de valeurs @@ -44,14 +46,14 @@ http: - self - https://example.com - # booléen dans le cas des commutateurs + # bool dans le cas des interrupteurs upgrade-insecure-requests: true block-all-mixed-content: false ``` -Dans les templates, utilisez `<script n:nonce>...</script>` et la valeur nonce sera complétée automatiquement. Créer des sites web sécurisés dans Nette est vraiment facile. +Utilisez `<script n:nonce>...</script>` dans les templates et la valeur du nonce sera renseignée automatiquement. Faire des sites web sûrs avec Nette est vraiment facile. -De même, il est possible de construire les en-têtes `Content-Security-Policy-Report-Only` (qui peuvent être utilisés en parallèle avec CSP) et [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] : +De la même façon, on peut configurer les en-têtes `Content-Security-Policy-Report-Only` (utilisables en parallèle de CSP) et la [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] : ```neon http: @@ -72,65 +74,77 @@ http: Cookie HTTP ----------- -Il est possible de modifier les valeurs par défaut de certains paramètres de la méthode [Nette\Http\Response::setCookie() |response#setCookie] et de la session. +Vous pouvez changer les valeurs par défaut de certains paramètres de la méthode [Nette\Http\Response::setCookie() |response#setCookie()] et de la gestion des sessions. ```neon http: - # portée du cookie selon le chemin - cookiePath: ... # (string) la valeur par défaut est '/' + # portée du cookie par chemin + cookiePath: ... # (string) '/' par défaut - # domaines qui acceptent le cookie - cookieDomain: 'example.com' # (string|domain) la valeur par défaut n'est pas définie + # domaines qui peuvent recevoir le cookie + cookieDomain: 'example.com' # (string|domain) non défini par défaut - # envoyer le cookie uniquement via HTTPS ? - cookieSecure: ... # (bool|auto) la valeur par défaut est auto + # n'envoyer les cookies que via HTTPS ? + cookieSecure: ... # (bool|auto) auto par défaut - # désactive l'envoi du cookie utilisé par Nette comme protection contre CSRF - disableNetteCookie: ... # (bool) la valeur par défaut est false + # désactive l'envoi du cookie que Nette utilise pour la protection CSRF + disableNetteCookie: ... # (bool) false par défaut ``` -L'attribut `cookieDomain` détermine quels domaines peuvent accepter le cookie. S'il n'est pas spécifié, le cookie est accepté par le même (sous-)domaine qui l'a défini, *mais pas* par ses sous-domaines. Si `cookieDomain` est spécifié, les sous-domaines sont également inclus. Par conséquent, spécifier `cookieDomain` est moins restrictif que de l'omettre. +L'attribut `cookieDomain` détermine quels domaines (origines) peuvent accepter les cookies. S'il n'est pas indiqué, le cookie est accepté par le même (sous-)domaine que celui qui l'a défini, *à l'exclusion* de ses sous-domaines. Si `cookieDomain` est indiqué, les sous-domaines sont inclus eux aussi. Indiquer `cookieDomain` est donc moins restrictif que de l'omettre. -Par exemple, avec `cookieDomain: nette.org`, les cookies sont également disponibles sur tous les sous-domaines comme `doc.nette.org`. Le même résultat peut également être obtenu en utilisant la valeur spéciale `domain`, c'est-à-dire `cookieDomain: domain`. +Par exemple, si `cookieDomain: nette.org` est défini, les cookies sont aussi disponibles sur tous les sous-domaines comme `doc.nette.org`. On peut aussi y parvenir avec la valeur spéciale `domain`, c'est-à-dire `cookieDomain: domain`. -La valeur par défaut `auto` pour l'attribut `cookieSecure` signifie que si le site fonctionne en HTTPS, les cookies seront envoyés avec l'indicateur `Secure` et ne seront donc disponibles que via HTTPS. +La valeur par défaut `auto` de l'attribut `cookieSecure` signifie que si le site tourne en HTTPS, les cookies seront envoyés avec le drapeau `Secure` et ne seront donc disponibles que via HTTPS. Proxy HTTP ---------- -Si le site fonctionne derrière un proxy HTTP, spécifiez son adresse IP pour que la détection de la connexion via HTTPS et l'adresse IP du client fonctionnent correctement. C'est-à-dire pour que les fonctions [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] et [isSecured() |request#isSecured] retournent les bonnes valeurs et que les liens avec le protocole `https:` soient générés dans les templates. +Si le site tourne derrière un proxy HTTP, indiquez l'adresse IP du proxy afin que la détection de la connexion HTTPS et l'adresse IP du client fonctionnent correctement. Autrement dit, pour que [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()] et [isSecured() |request#isSecured()] renvoient les bonnes valeurs et que les liens soient générés avec le protocole `https:` dans les templates. + +```neon +http: + # adresse IP, plage (par exemple 127.0.0.1/8), ou tableau de ces valeurs + proxy: 127.0.0.1 # (string|string[]) non défini par défaut +``` + + +Forcer HTTPS .{data-version:3.3.4} +---------------------------------- + +Force sans condition le schéma de la requête en HTTPS. C'est utile pour les sites uniquement HTTPS qui tournent derrière un répartiteur de charge ou un reverse proxy terminant le TLS mais ne transmettant pas l'en-tête `X-Forwarded-Proto`, si bien que la détection HTTPS standard (même avec un [#Proxy HTTP] configuré) ne le remarquerait pas. ```neon http: - # Adresse IP, plage (par ex. 127.0.0.1/8) ou tableau de ces valeurs - proxy: 127.0.0.1 # (string|string[]) la valeur par défaut n'est pas définie + # force le schéma HTTPS pour toutes les requêtes + forceHttps: true # (bool) false par défaut ``` Session ======= -Configuration de base des [sessions |sessions] : +Réglages de base des [sessions] : ```neon session: - # afficher le panneau de session dans la barre Tracy ? - debugger: ... # (bool) la valeur par défaut est false + # afficher le panneau de session dans la Tracy Bar ? + debugger: ... # (bool) false par défaut - # durée d'inactivité après laquelle la session expire - expiration: 14 days # (string) la valeur par défaut est '3 hours' + # durée d'inactivité au bout de laquelle la session expire + expiration: 14 days # (string) '3 hours' par défaut # quand la session doit-elle démarrer ? - autoStart: ... # (smart|always|never) la valeur par défaut est 'smart' + autoStart: ... # (smart|always|never) 'smart' par défaut - # gestionnaire, service implémentant l'interface SessionHandlerInterface + # handler, un service implémentant SessionHandlerInterface handler: @handlerService ``` -L'option `autoStart` contrôle quand la session doit démarrer. La valeur `always` signifie que la session démarrera toujours au lancement de l'application. La valeur `smart` signifie que la session ne démarrera au lancement de l'application que si elle existe déjà, ou au moment où nous voulons lire ou écrire dedans. Enfin, la valeur `never` interdit le démarrage automatique de la session. +L'option `autoStart` détermine quand la session doit démarrer. La valeur `always` signifie que la session démarre dès que l'application démarre. La valeur `smart` signifie que la session ne démarre avec l'application que si elle existe déjà, ou au moment où nous voulons y lire ou y écrire. Enfin, la valeur `never` désactive le démarrage automatique de la session. -De plus, il est possible de définir toutes les [directives de session PHP |https://www.php.net/manual/en/session.configuration.php] (au format camelCase) ainsi que [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Exemple : +Vous pouvez en outre définir toutes les [directives de session |https://www.php.net/manual/en/session.configuration.php] de PHP (au format camelCase) ainsi que [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Exemple : ```neon session: @@ -145,18 +159,18 @@ session: Cookie de session ----------------- -Le cookie de session est envoyé avec les mêmes paramètres que les [autres cookies |#Cookie HTTP], mais vous pouvez modifier ceux-ci pour lui : +Le cookie de session est envoyé avec les mêmes paramètres que les [autres cookies |#Cookie HTTP], mais vous pouvez les changer spécialement pour lui : ```neon session: - # domaines qui acceptent le cookie + # domaines qui peuvent recevoir le cookie cookieDomain: 'example.com' # (string|domain) - # restriction lors de l'accès depuis un autre domaine - cookieSamesite: None # (Strict|Lax|None) la valeur par défaut est Lax + # restriction pour l'accès cross-origin + cookieSamesite: None # (Strict|Lax|None) Lax par défaut ``` -L'attribut `cookieSamesite` influence si le cookie sera envoyé lors d'un [accès depuis un domaine différent |nette:glossary#Cookie SameSite], ce qui offre une certaine protection contre les attaques [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). +L'attribut `cookieSamesite` détermine si le cookie est envoyé lors des [requêtes cross-origin |nette:glossary#Cookie SameSite], ce qui offre une certaine protection contre les attaques [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Services DI @@ -165,7 +179,8 @@ Services DI Ces services sont ajoutés au conteneur DI : | Nom | Type | Description -|-----------------------------------------------------| -| `http.request` | [api:Nette\Http\Request] | [Requête HTTP | request] -| `http.response` | [api:Nette\Http\Response] | [Réponse HTTP | response] -| `session.session` | [api:Nette\Http\Session] | [Gestion de session | sessions] +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [requête HTTP| request] +| `http.response` | [api:Nette\Http\Response] | [réponse HTTP| response] +| `session.session`| [api:Nette\Http\Session] | [gestion des sessions| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | factory qui crée la requête HTTP diff --git a/http/fr/request.texy b/http/fr/request.texy index 984a96e9df..162a7f8698 100644 --- a/http/fr/request.texy +++ b/http/fr/request.texy @@ -2,11 +2,11 @@ Requête HTTP ************ .[perex] -Nette encapsule la requête HTTP dans des objets avec une API compréhensible et fournit en même temps un filtre d'assainissement. +Nette encapsule la requête HTTP dans des objets dotés d'une API claire, tout en fournissant un filtre d'assainissement. -La requête HTTP est représentée par l'objet [api:Nette\Http\Request]. Si vous travaillez avec Nette, cet objet est automatiquement créé par le framework et vous pouvez vous le faire passer via l'[injection de dépendances |dependency-injection:passing-dependencies]. Dans les presenters, il suffit d'appeler la méthode `$this->getHttpRequest()`. Si vous travaillez en dehors du Nette Framework, vous pouvez créer l'objet à l'aide de [#RequestFactory]. +La requête HTTP est représentée par l'objet [api:Nette\Http\Request]. Si vous travaillez avec Nette, cet objet est créé automatiquement par le framework et vous pouvez vous le faire passer par [injection de dépendances |dependency-injection:passing-dependencies]. Dans les presenters, il suffit d'appeler la méthode `$this->getHttpRequest()`. Si vous travaillez en dehors de Nette Framework, vous pouvez créer l'objet à l'aide de [#RequestFactory]. -Un grand avantage de Nette est que lors de la création de l'objet, il nettoie automatiquement tous les paramètres d'entrée GET, POST, COOKIE ainsi que l'URL des caractères de contrôle et des séquences UTF-8 invalides. Vous pouvez ensuite travailler en toute sécurité avec ces données. Les données nettoyées sont ensuite utilisées dans les presenters et les formulaires. +Un gros avantage de Nette est qu'à la création de l'objet, il assainit automatiquement tous les paramètres d'entrée (GET, POST, COOKIE) ainsi que l'URL, en supprimant les caractères de contrôle et les séquences UTF-8 invalides. Vous pouvez ensuite travailler avec ces données en toute sécurité. Les données assainies sont ensuite utilisées dans les presenters et les formulaires. → [Installation et prérequis |@home#Installation] @@ -14,81 +14,81 @@ Un grand avantage de Nette est que lors de la création de l'objet, il nettoie a Nette\Http\Request ================== -Cet objet est immutable (immuable). Il n'a pas de setters, il n'a qu'un seul "wither" `withUrl()`, qui ne modifie pas l'objet, mais retourne une nouvelle instance avec la valeur modifiée. +Cet objet est immuable. Il n'a pas de setters ; il possède un seul wither, `withUrl()`, qui ne modifie pas l'objet mais renvoie une nouvelle instance portant la valeur modifiée. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ---------------------------------------------------------------- -Retourne un clone avec une URL différente. +Renvoie un clone portant une URL différente. getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -Retourne l'URL de la requête sous forme d'objet [UrlScript |urls#UrlScript]. +Renvoie l'URL de la requête sous forme d'objet [UrlScript |urls#UrlScript]. ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -Attention : les navigateurs n'envoient pas le fragment au serveur, donc `$url->getFragment()` retournera une chaîne vide. +Attention : les navigateurs n'envoient pas le fragment au serveur, `$url->getFragment()` renverra donc une chaîne vide. getQuery(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Retourne les paramètres GET de la requête. +Renvoie les paramètres GET de la requête. ```php -$all = $httpRequest->getQuery(); // retourne un tableau de tous les paramètres de l'URL -$id = $httpRequest->getQuery('id'); // retourne le paramètre GET 'id' (ou null) +$all = $httpRequest->getQuery(); // tableau de tous les paramètres de l'URL +$id = $httpRequest->getQuery('id'); // renvoie le paramètre GET 'id' (ou null) ``` getPost(?string $key=null): string|array|null .[method] ------------------------------------------------------- -Retourne les paramètres POST de la requête. +Renvoie les paramètres POST de la requête. ```php -$all = $httpRequest->getPost(); // retourne un tableau de tous les paramètres de POST -$id = $httpRequest->getPost('id'); // retourne le paramètre POST 'id' (ou null) +$all = $httpRequest->getPost(); // tableau de tous les paramètres POST +$id = $httpRequest->getPost('id'); // renvoie le paramètre POST 'id' (ou null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Retourne l'[upload |#Fichiers uploadés] sous forme d'objet [api:Nette\Http\FileUpload] : +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- +Renvoie un [upload |#Fichiers envoyés] sous forme d'objet [api:Nette\Http\FileUpload] : ```php $file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // un fichier a-t-il été uploadé ? - $file->getUntrustedName(); // nom du fichier envoyé par l'utilisateur +if ($file?->hasFile()) { // un fichier a-t-il été envoyé ? + $file->getUntrustedName(); // nom de fichier envoyé par l'utilisateur $file->getSanitizedName(); // nom sans caractères dangereux } ``` -Pour accéder à une structure imbriquée, spécifiez un tableau de clés. +Pour accéder à une structure imbriquée, fournissez un tableau de clés. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Comme on ne peut pas faire confiance aux données externes et donc pas non plus à la forme de la structure des fichiers, cette méthode est plus sûre que par exemple `$request->getFiles()['my-form']['details']['avatar']`, qui peut échouer. +Comme vous ne pouvez pas faire confiance aux données externes et donc vous fier à la structure des fichiers, cette approche est plus sûre que, par exemple, `$request->getFiles()['my-form']['details']['avatar']`, qui pourrait échouer. getFiles(): array .[method] --------------------------- -Retourne l'arbre de [tous les uploads |#Fichiers uploadés] dans une structure normalisée, dont les feuilles sont des objets [api:Nette\Http\FileUpload] : +Renvoie l'arbre de [tous les uploads |#Fichiers envoyés] dans une structure normalisée, dont les feuilles sont des objets [api:Nette\Http\FileUpload] : ```php $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Retourne un cookie ou `null` s'il n'existe pas. +getCookie(string $key): ?string .[method] +----------------------------------------- +Renvoie un cookie, ou `null` s'il n'existe pas. ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -97,7 +97,7 @@ $sessId = $httpRequest->getCookie('sess_id'); getCookies(): array .[method] ----------------------------- -Retourne tous les cookies. +Renvoie tous les cookies. ```php $cookies = $httpRequest->getCookies(); @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -Retourne la méthode HTTP avec laquelle la requête a été effectuée. +Renvoie la méthode HTTP utilisée pour la requête. ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -Teste la méthode HTTP avec laquelle la requête a été effectuée. Le paramètre est insensible à la casse. +Teste la méthode HTTP utilisée pour la requête. Le paramètre est insensible à la casse. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,31 +124,63 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -Retourne un en-tête HTTP ou `null` s'il n'existe pas. Le paramètre est insensible à la casse. +Renvoie un en-tête HTTP, ou `null` s'il n'existe pas. Le paramètre est insensible à la casse. ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Retourne tous les en-têtes HTTP sous forme de tableau associatif. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Renvoie tous les en-têtes HTTP sous forme de tableau associatif. Les clés sont normalisées en minuscules. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -La connexion est-elle chiffrée (HTTPS) ? Pour un fonctionnement correct, il peut être nécessaire de [configurer le proxy |configuration#Proxy HTTP]. +La connexion est-elle chiffrée (HTTPS) ? Un fonctionnement correct peut exiger de [configurer un proxy |configuration#Proxy HTTP]. -isSameSite(): bool .[method] ----------------------------- -La requête provient-elle du même (sous-)domaine et est-elle initiée par un clic sur un lien ? Nette utilise le cookie `_nss` (auparavant `nette-samesite`) pour la détection. +isSameSite(): bool .[method deprecated] +--------------------------------------- +La requête vient-elle du même site ? Depuis la version 3.4, elle est remplacée par la plus complète [isFrom() |#isFrom()]. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Vous dit d'où vient la requête et comment le navigateur l'a émise, à partir des en-têtes `Sec-Fetch-*` (ce qu'on appelle les [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]), que le navigateur pose lui-même et qu'une page tournant dans le navigateur de la victime ne peut ni falsifier ni supprimer. Nette s'en sert en interne pour protéger automatiquement les formulaires et les signaux contre le [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). C'est utile lorsque vous voulez protéger vos propres actions sensibles, comme des endpoints d'API ou des liens destructeurs. + +La méthode ne renvoie `true` que lorsque la requête satisfait **toutes** les conditions que vous fournissez. Le premier paramètre `$site` décrit la relation entre la page qui a initié la requête et votre site (l'en-tête `Sec-Fetch-Site`). Il accepte une seule valeur ou une liste de ces cas de `FetchSite` : + +- `FetchSite::SameOrigin` - exactement de la même origine (schéma, hôte et port) +- `FetchSite::SameSite` - du même site, éventuellement d'un autre sous-domaine +- `FetchSite::CrossSite` - d'un site étranger +- `FetchSite::None` - l'utilisateur l'a initiée directement, par exemple en tapant l'URL ou en ouvrant un favori + +```php +// la requête provient-elle de nos propres pages ? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // bloquer l'action +} +``` + +Le paramètre facultatif `$dest` (l'en-tête `Sec-Fetch-Dest`) dit quel type de ressource le navigateur récupère, par exemple `FetchDest::Document` pour une navigation de premier niveau ou `FetchDest::Empty` pour une requête émise depuis JavaScript. Le paramètre facultatif `$user` (l'en-tête `Sec-Fetch-User`) indique si la navigation a été déclenchée par une véritable action de l'utilisateur, comme un clic sur un lien ou l'envoi d'un formulaire ; passez `true` pour l'exiger. + +Un contrôle vérifiant qu'une action n'est accessible que depuis vos propres pages et uniquement par une action réelle de l'utilisateur ressemble alors à ceci : + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Les navigateurs plus anciens (Safari avant 16.4) n'envoient pas les en-têtes `Sec-Fetch-*`. Pour eux, Nette se rabat sur un cookie `SameSite=Strict`, qui prouve seulement que la requête n'est pas cross-site. Un contrôle exigeant en plus `$dest` ou `$user` ne peut pas être vérifié ainsi et renvoie `false` dans ces navigateurs : si c'est trop strict, ne testez que `$site`. isAjax(): bool .[method] @@ -158,17 +190,17 @@ S'agit-il d'une requête AJAX ? getRemoteAddress(): ?string .[method] ------------------------------------- -Retourne l'adresse IP de l'utilisateur. Pour un fonctionnement correct, il peut être nécessaire de [configurer le proxy |configuration#Proxy HTTP]. +Renvoie l'adresse IP de l'utilisateur. Un fonctionnement correct peut exiger de [configurer un proxy |configuration#Proxy HTTP]. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Retourne la résolution DNS de l'adresse IP de l'utilisateur. Pour un fonctionnement correct, il peut être nécessaire de [configurer le proxy |configuration#Proxy HTTP]. +Obsolète, elle renvoie toujours `null`. Les résolutions DNS inverses étaient lentes et peu fiables ; si vous avez besoin du nom d'hôte, résolvez-le vous-même à partir de [getRemoteAddress() |#getRemoteAddress()]. getBasicCredentials(): ?array .[method] --------------------------------------- -Retourne les informations d'identification pour l'[authentification HTTP de base |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. +Renvoie les identifiants d'authentification pour l'[authentification HTTP Basic |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. ```php [$user, $password] = $httpRequest->getBasicCredentials(); @@ -177,21 +209,45 @@ Retourne les informations d'identification pour l'[authentification HTTP de base getRawBody(): ?string .[method] ------------------------------- -Retourne le corps de la requête HTTP. +Renvoie le corps de la requête HTTP. ```php $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Renvoie l'origine d'où venait la requête. Une origine se compose du schéma (protocole), du nom d'hôte et du port - par exemple `https://example.com:8080`. Renvoie `null` si l'en-tête origin n'est pas présent ou vaut `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Le navigateur envoie l'en-tête `Origin` dans les cas suivants : +- requêtes cross-origin (appels AJAX vers un autre domaine) +- requêtes POST, PUT, DELETE et autres requêtes modifiantes +- requêtes émises à l'aide de l'API Fetch + +Le navigateur N'ENVOIE PAS l'en-tête `Origin` pour : +- les requêtes GET ordinaires vers le même domaine (navigation same-origin) +- la navigation directe par saisie d'une URL dans la barre d'adresse +- les requêtes émises par des clients qui ne sont pas des navigateurs + +.[note] +Contrairement à l'en-tête `Referer`, `Origin` ne contient que le schéma, l'hôte et le port - pas le chemin complet de l'URL. Cela le rend plus adapté aux contrôles de sécurité tout en préservant la vie privée des utilisateurs. L'en-tête `Origin` sert principalement à la validation [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing). + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Détecte la langue. Comme paramètre `$lang`, nous passons un tableau des langues que l'application supporte, et elle retourne celle que le navigateur du visiteur préférerait voir. Ce n'est pas de la magie, on utilise simplement l'en-tête `Accept-Language`. S'il n'y a pas de correspondance, retourne `null`. +Détecte la langue. Passez dans le paramètre `$langs` un tableau des langues prises en charge par l'application, et elle renverra celle que préfère le navigateur du visiteur. Ce n'est pas magique, elle se contente d'utiliser l'en-tête `Accept-Language`. Si aucune correspondance n'est trouvée, elle renvoie `null`. ```php -// le navigateur envoie par ex. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 +// Le navigateur envoie par exemple Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 -$langs = ['hu', 'pl', 'en']; // langues supportées par l'application +$langs = ['hu', 'pl', 'en']; // langues prises en charge par l'application echo $httpRequest->detectLanguage($langs); // en ``` @@ -199,80 +255,81 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -La classe [api:Nette\Http\RequestFactory] sert à créer une instance de `Nette\Http\Request`, qui représente la requête HTTP actuelle. (Si vous travaillez avec Nette, l'objet de requête HTTP est automatiquement créé par le framework.) +La classe [api:Nette\Http\RequestFactory] sert à créer une instance de `Nette\Http\Request`, qui représente la requête HTTP courante. (Si vous travaillez avec Nette, l'objet de requête HTTP est créé automatiquement par le framework.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -La méthode `fromGlobals()` crée l'objet de requête sur la base des variables globales PHP actuelles (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` et `$_SERVER`). Lors de la création de l'objet, elle nettoie automatiquement tous les paramètres d'entrée GET, POST, COOKIE ainsi que l'URL des caractères de contrôle et des séquences UTF-8 invalides, ce qui garantit la sécurité lors du travail ultérieur avec ces données. +La méthode `fromGlobals()` crée l'objet de requête à partir des variables globales actuelles de PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` et `$_SERVER`). À la création de l'objet, elle nettoie automatiquement tous les paramètres d'entrée (GET, POST, COOKIE) ainsi que l'URL des caractères de contrôle et des séquences UTF-8 invalides, ce qui garantit la sécurité du travail ultérieur avec ces données. -RequestFactory peut être configuré avant d'appeler `fromGlobals()` : +RequestFactory peut être configurée avant l'appel de `fromGlobals()` : -- avec la méthode `$factory->setBinary()` , vous désactivez le nettoyage automatique des paramètres d'entrée des caractères de contrôle et des séquences UTF-8 invalides. -- avec la méthode `$factory->setProxy(...)`, vous indiquez l'adresse IP du [serveur proxy |configuration#Proxy HTTP], ce qui est nécessaire pour une détection correcte de l'adresse IP de l'utilisateur. +- la méthode `$factory->setBinary()` désactive le nettoyage automatique des paramètres d'entrée des caractères de contrôle et des séquences UTF-8 invalides. +- la méthode `$factory->setProxy(...)` indique l'adresse IP du [serveur proxy |configuration#Proxy HTTP], nécessaire à la détection correcte de l'adresse IP de l'utilisateur. +- la méthode `$factory->setForceHttps()` .{data-version:3.3.4} force le schéma de la requête en HTTPS, quel que soit l'environnement serveur. -RequestFactory permet de définir des filtres qui transforment automatiquement des parties de l'URL de la requête. Ces filtres suppriment les caractères indésirables de l'URL, qui peuvent y être insérés par exemple par une implémentation incorrecte des systèmes de commentaires sur différents sites web : +RequestFactory permet de définir des filtres qui transforment automatiquement des parties de l'URL de la requête. Ces filtres suppriment des URL les caractères indésirables qui ont pu y être insérés, par exemple, par des implémentations défaillantes de systèmes de commentaires sur divers sites : ```php -// suppression des espaces du chemin +// supprime les espaces du chemin $requestFactory->urlFilters['path']['%20'] = ''; -// suppression du point, de la virgule ou de la parenthèse droite de la fin de l'URI +// supprime le point, la virgule ou la parenthèse fermante à la fin de l'URI $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// nettoyage du chemin des doubles barres obliques (filtre par défaut) +// nettoie le chemin des doubles barres obliques (filtre par défaut) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -La première clé `'path'` ou `'url'` détermine à quelle partie de l'URL le filtre s'applique. La deuxième clé est une expression régulière à rechercher, et la valeur est le remplacement qui sera utilisé à la place du texte trouvé. +La première clé, `'path'` ou `'url'`, détermine à quelle partie de l'URL le filtre sera appliqué. La deuxième clé est l'expression régulière à rechercher, et la valeur est le remplacement à utiliser à la place du texte trouvé. -Fichiers uploadés -================= +Fichiers envoyés +================ -La méthode `Nette\Http\Request::getFiles()` retourne un tableau de tous les uploads dans une structure normalisée, dont les feuilles sont des objets [api:Nette\Http\FileUpload]. Ceux-ci encapsulent les données envoyées par l'élément de formulaire `<input type=file>`. +La méthode `Nette\Http\Request::getFiles()` renvoie un tableau de tous les uploads dans une structure normalisée, dont les feuilles sont des objets [api:Nette\Http\FileUpload]. Ceux-ci encapsulent les données envoyées par l'élément de formulaire `<input type=file>`. -La structure reflète la dénomination des éléments en HTML. Dans le cas le plus simple, il peut s'agir d'un seul élément de formulaire nommé envoyé comme : +La structure reflète le nommage des éléments en HTML. Dans le cas le plus simple, il peut s'agir d'un unique élément de formulaire nommé, envoyé ainsi : ```latte <input type="file" name="avatar"> ``` -Dans ce cas, `$request->getFiles()` retourne un tableau : +Dans ce cas, `$request->getFiles()` renvoie un tableau : ```php [ - 'avatar' => /* Instance FileUpload */ + 'avatar' => /* instance de FileUpload */ ] ``` -L'objet `FileUpload` est créé même si l'utilisateur n'a envoyé aucun fichier ou si l'envoi a échoué. La méthode `hasFile()` retourne si un fichier a été envoyé : +L'objet `FileUpload` est créé même si l'utilisateur n'a envoyé aucun fichier ou si l'envoi a échoué. La méthode `hasFile()` renvoie true si un fichier a été envoyé : ```php $request->getFile('avatar')?->hasFile(); ``` -Dans le cas d'un nom d'élément utilisant la notation pour les tableaux : +Dans le cas d'un nom d'élément utilisant la notation tableau : ```latte <input type="file" name="my-form[details][avatar]"> ``` -l'arbre retourné ressemble à ceci : +l'arbre renvoyé ressemble à ceci : ```php [ 'my-form' => [ 'details' => [ - 'avatar' => /* Instance FileUpload */ + 'avatar' => /* instance de FileUpload */ ], ], ] ``` -Il est également possible de créer un tableau de fichiers : +Vous pouvez aussi créer des tableaux de fichiers : ```latte <input type="file" name="my-form[details][avatars][]" multiple> @@ -285,16 +342,16 @@ Dans ce cas, la structure ressemble à ceci : 'my-form' => [ 'details' => [ 'avatars' => [ - 0 => /* Instance FileUpload */, - 1 => /* Instance FileUpload */, - 2 => /* Instance FileUpload */, + 0 => /* instance de FileUpload */, + 1 => /* instance de FileUpload */, + 2 => /* instance de FileUpload */, ], ], ], ] ``` -Accéder à l'index 1 du tableau imbriqué se fait de préférence comme ceci : +La meilleure façon d'accéder à l'index 1 du tableau imbriqué est la suivante : ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); @@ -303,31 +360,31 @@ if ($file instanceof Nette\Http\FileUpload) { } ``` -Comme on ne peut pas faire confiance aux données externes et donc pas non plus à la forme de la structure des fichiers, cette méthode est plus sûre que par exemple `$request->getFiles()['my-form']['details']['avatars'][1]`, qui peut échouer. +Comme vous ne pouvez pas faire confiance aux données externes et donc vous fier à la structure des fichiers, cette approche est plus sûre que, par exemple, `$request->getFiles()['my-form']['details']['avatars'][1]`, qui pourrait échouer. -Aperçu des méthodes `FileUpload` .{toc: FileUpload} ---------------------------------------------------- +Aperçu des méthodes de `FileUpload` .{toc: FileUpload} +------------------------------------------------------ hasFile(): bool .[method] ------------------------- -Retourne `true` si l'utilisateur a uploadé un fichier. +Renvoie `true` si l'utilisateur a envoyé un fichier. isOk(): bool .[method] ---------------------- -Retourne `true` si le fichier a été uploadé avec succès. +Renvoie `true` si le fichier a été envoyé avec succès. getError(): int .[method] ------------------------- -Retourne le code d'erreur lors de l'upload du fichier. Il s'agit de l'une des constantes [UPLOAD_ERR_XXX|https://www.php.net/manual/en/features.file-upload.errors.php]. Si l'upload s'est déroulé correctement, retourne `UPLOAD_ERR_OK`. +Renvoie le code d'erreur associé au fichier envoyé. C'est l'une des constantes [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php]. Si le fichier a été envoyé avec succès, elle renvoie `UPLOAD_ERR_OK`. move(string $dest) .[method] ---------------------------- -Déplace le fichier uploadé vers un nouvel emplacement. Si le fichier de destination existe déjà, il sera écrasé. +Déplace un fichier envoyé vers un nouvel emplacement. Si le fichier de destination existe déjà, il sera écrasé. ```php $file->move('/path/to/files/name.ext'); @@ -336,12 +393,12 @@ $file->move('/path/to/files/name.ext'); getContents(): ?string .[method] -------------------------------- -Retourne le contenu du fichier uploadé. Si l'upload n'a pas réussi, retourne `null`. +Renvoie le contenu du fichier envoyé. Si l'envoi n'a pas réussi, elle renvoie `null`. getContentType(): ?string .[method] ----------------------------------- -Détecte le type de contenu MIME du fichier uploadé sur la base de sa signature. Si l'upload n'a pas réussi ou si la détection a échoué, retourne `null`. +Détecte le type de contenu MIME du fichier envoyé d'après sa signature. Si l'envoi n'a pas réussi ou si la détection a échoué, elle renvoie `null`. .[caution] Nécessite l'extension PHP `fileinfo`. @@ -349,15 +406,15 @@ Nécessite l'extension PHP `fileinfo`. getUntrustedName(): string .[method] ------------------------------------ -Retourne le nom original du fichier, tel qu'envoyé par le navigateur. +Renvoie le nom de fichier d'origine tel qu'envoyé par le navigateur. .[caution] -Ne faites pas confiance à la valeur retournée par cette méthode. Le client aurait pu envoyer un nom de fichier malveillant dans l'intention d'endommager ou de pirater votre application. +Ne faites pas confiance à la valeur renvoyée par cette méthode. Un client pourrait envoyer un nom de fichier malveillant dans l'intention d'endommager ou de pirater votre application. getSanitizedName(): string .[method] ------------------------------------ -Retourne le nom de fichier assaini. Il ne contient que des caractères ASCII `[a-zA-Z0-9.-]`. Si le nom ne contient pas de tels caractères, retourne `'unknown'`. Si le fichier est une image au format JPEG, PNG, GIF, WebP ou AVIF, retourne également la bonne extension. +Renvoie le nom de fichier assaini. Il ne contient que les caractères ASCII `[a-zA-Z0-9.-]`. Si le nom ne contient pas de tels caractères, elle renvoie `'unknown'`. Si le fichier est une image JPEG, PNG, GIF, WebP ou AVIF, elle renvoie aussi la bonne extension de fichier. .[caution] Nécessite l'extension PHP `fileinfo`. @@ -365,7 +422,7 @@ Nécessite l'extension PHP `fileinfo`. getSuggestedExtension(): ?string .[method]{data-version:3.2.4} -------------------------------------------------------------- -Retourne l'extension de fichier appropriée (sans le point) correspondant au type MIME détecté. +Renvoie l'extension de fichier appropriée (sans le point) correspondant au type MIME détecté. .[caution] Nécessite l'extension PHP `fileinfo`. @@ -373,25 +430,30 @@ Nécessite l'extension PHP `fileinfo`. getUntrustedFullPath(): string .[method] ---------------------------------------- -Retourne le chemin d'accès original du fichier, tel qu'envoyé par le navigateur lors de l'upload d'un dossier. Le chemin complet n'est disponible qu'en PHP 8.1 et supérieur. Dans les versions précédentes, cette méthode retourne le nom de fichier original. +Renvoie le chemin de fichier d'origine tel qu'envoyé par le navigateur lors de l'envoi d'un répertoire. Le chemin complet n'est disponible qu'à partir de PHP 8.1. Dans les versions antérieures, cette méthode renvoie le nom de fichier d'origine. .[caution] -Ne faites pas confiance à la valeur retournée par cette méthode. Le client aurait pu envoyer un nom de fichier malveillant dans l'intention d'endommager ou de pirater votre application. +Ne faites pas confiance à la valeur renvoyée par cette méthode. Un client pourrait envoyer un nom de fichier malveillant dans l'intention d'endommager ou de pirater votre application. getSize(): int .[method] ------------------------ -Retourne la taille du fichier uploadé. Si l'upload n'a pas réussi, retourne `0`. +Renvoie la taille du fichier envoyé. Si l'envoi n'a pas réussi, elle renvoie `0`. getTemporaryFile(): string .[method] ------------------------------------ -Retourne le chemin d'accès à l'emplacement temporaire du fichier uploadé. Si l'upload n'a pas réussi, retourne `''`. +Renvoie le chemin de l'emplacement temporaire du fichier envoyé. Si l'envoi n'a pas réussi, elle renvoie `''`. + + +__toString(): string .[method] +------------------------------ +Renvoie le chemin de l'emplacement temporaire du fichier envoyé. Cela permet d'utiliser l'objet `FileUpload` directement comme une chaîne. isImage(): bool .[method] ------------------------- -Retourne `true` si le fichier uploadé est une image au format JPEG, PNG, GIF, WebP ou AVIF. La détection est basée sur sa signature et ne vérifie pas l'intégrité de l'ensemble du fichier. On peut vérifier si une image n'est pas endommagée, par exemple, en essayant de la [charger |#toImage]. +Renvoie `true` si le fichier envoyé est une image JPEG, PNG, GIF, WebP ou AVIF. La détection se fait d'après sa signature et ne vérifie pas l'intégrité du fichier entier. Pour savoir si une image est endommagée, on peut par exemple essayer de la [charger |#toImage()]. .[caution] Nécessite l'extension PHP `fileinfo`. @@ -399,9 +461,9 @@ Nécessite l'extension PHP `fileinfo`. getImageSize(): ?array .[method] -------------------------------- -Retourne une paire `[largeur, hauteur]` avec les dimensions de l'image uploadée. Si l'upload n'a pas réussi ou s'il ne s'agit pas d'une image valide, retourne `null`. +Renvoie la paire `[largeur, hauteur]` avec les dimensions de l'image envoyée. Si l'envoi n'a pas réussi ou s'il ne s'agit pas d'une image valide, elle renvoie `null`. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Charge l'image comme un objet [Image|utils:images]. Si l'upload n'a pas réussi ou s'il ne s'agit pas d'une image valide, lève une exception `Nette\Utils\ImageException`. +Charge l'image sous forme d'objet [Image |utils:images]. Si l'envoi n'a pas réussi ou s'il ne s'agit pas d'une image valide, elle lève une `Nette\Utils\ImageException`. diff --git a/http/fr/response.texy b/http/fr/response.texy index 0c9483cb66..7ac4620b25 100644 --- a/http/fr/response.texy +++ b/http/fr/response.texy @@ -2,9 +2,9 @@ Réponse HTTP ************ .[perex] -Nette encapsule la réponse HTTP dans des objets avec une API compréhensible. +Nette encapsule la réponse HTTP dans des objets dotés d'une API claire. -La réponse HTTP est représentée par l'objet [api:Nette\Http\Response]. Si vous travaillez avec Nette, cet objet est automatiquement créé par le framework et vous pouvez vous le faire passer via l'[injection de dépendances |dependency-injection:passing-dependencies]. Dans les presenters, il suffit d'appeler la méthode `$this->getHttpResponse()`. +La réponse HTTP est représentée par l'objet [api:Nette\Http\Response]. Si vous travaillez avec Nette, cet objet est créé automatiquement par le framework et vous pouvez vous le faire passer par [injection de dépendances |dependency-injection:passing-dependencies]. Dans les presenters, il suffit d'appeler la méthode `$this->getHttpResponse()`. → [Installation et prérequis |@home#Installation] @@ -12,12 +12,12 @@ La réponse HTTP est représentée par l'objet [api:Nette\Http\Response]. Si vou Nette\Http\Response =================== -L'objet, contrairement à [Nette\Http\Request|request], est mutable, c'est-à-dire qu'à l'aide de setters, vous pouvez modifier l'état, par exemple envoyer des en-têtes. N'oubliez pas que tous les setters doivent être appelés **avant l'envoi de toute sortie.** La méthode `isSent()` indique si la sortie a déjà été envoyée. Si elle retourne `true`, toute tentative d'envoi d'un en-tête lèvera une exception `Nette\InvalidStateException`. +Contrairement à [Nette\Http\Request |request], cet objet est modifiable : vous pouvez donc utiliser des setters pour changer son état, par exemple pour envoyer des en-têtes. Rappelez-vous que tous les setters **doivent être appelés avant qu'une quelconque sortie ne soit envoyée.** La méthode `isSent()` indique si la sortie a déjà été envoyée. Si elle renvoie `true`, toute tentative d'envoi d'un en-tête lèvera une `Nette\InvalidStateException`. setCode(int $code, ?string $reason=null) .[method] -------------------------------------------------- -Modifie le [code de statut de la réponse |https://developer.mozilla.org/fr/docs/Web/HTTP/Status]. Pour une meilleure lisibilité du code source, nous recommandons d'utiliser des [constantes prédéfinies |api:Nette\Http\IResponse] pour le code au lieu de chiffres. +Change le [code de statut |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10] de la réponse. Pour une meilleure lisibilité du code source, il est recommandé d'utiliser les [constantes prédéfinies |api:Nette\Http\IResponse] plutôt que les nombres eux-mêmes. ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -26,17 +26,17 @@ $httpResponse->setCode(Nette\Http\Response::S404_NotFound); getCode(): int .[method] ------------------------ -Retourne le code de statut de la réponse. +Renvoie le code de statut de la réponse. isSent(): bool .[method] ------------------------ -Retourne si les en-têtes ont déjà été envoyés du serveur au navigateur, et donc s'il n'est plus possible d'envoyer des en-têtes ou de modifier le code de statut. +Indique si les en-têtes ont déjà été envoyés du serveur au navigateur, autrement dit s'il n'est plus possible d'envoyer des en-têtes ni de changer le code de statut. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Envoie un en-tête HTTP et **écrase** un en-tête précédemment envoyé du même nom. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Envoie un en-tête HTTP et **écrase** un en-tête du même nom envoyé précédemment. Si `$value` vaut `null`, l'en-tête sera supprimé. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -Envoie un en-tête HTTP et **n'écrase pas** un en-tête précédemment envoyé du même nom. +Envoie un en-tête HTTP et **n'écrase pas** un en-tête du même nom envoyé précédemment. ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -55,21 +55,21 @@ $httpResponse->addHeader('Accept', 'application/xml'); deleteHeader(string $name) .[method] ------------------------------------ -Supprime un en-tête HTTP précédemment envoyé. +Supprime un en-tête HTTP envoyé précédemment. getHeader(string $header): ?string .[method] -------------------------------------------- -Retourne un en-tête HTTP envoyé ou `null` s'il n'existe pas. Le paramètre est insensible à la casse. +Renvoie l'en-tête HTTP envoyé, ou `null` s'il n'existe pas. Le paramètre est insensible à la casse. ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ -Retourne tous les en-têtes HTTP envoyés sous forme de tableau associatif. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Renvoie tous les en-têtes HTTP envoyés sous forme de tableau associatif. ```php $headers = $httpResponse->getHeaders(); @@ -79,7 +79,7 @@ echo $headers['Pragma']; setContentType(string $type, ?string $charset=null) .[method] ------------------------------------------------------------- -Modifie l'en-tête `Content-Type`. +Change l'en-tête `Content-Type`. ```php $httpResponse->setContentType('text/plain', 'UTF-8'); @@ -88,7 +88,7 @@ $httpResponse->setContentType('text/plain', 'UTF-8'); redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- -Redirige vers une autre URL. N'oubliez pas de terminer ensuite le script. +Redirige vers une autre URL. Pensez à terminer le script ensuite. ```php $httpResponse->redirect('http://example.com'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- +setExpiration(?string $expire) .[method] +---------------------------------------- Définit l'expiration du document HTTP à l'aide des en-têtes `Cache-Control` et `Expires`. Le paramètre est soit un intervalle de temps (sous forme de texte), soit `null`, ce qui désactive la mise en cache. ```php -// le cache du navigateur expirera dans une heure +// le cache du navigateur expire dans une heure $httpResponse->setExpiration('1 hour'); ``` sendAsFile(string $fileName) .[method] -------------------------------------- -La réponse sera téléchargée via la boîte de dialogue *Enregistrer sous* sous le nom spécifié. Le fichier lui-même n'est pas envoyé. +La réponse sera téléchargée via une boîte de dialogue *Enregistrer sous* portant le nom indiqué. Elle n'envoie pas le fichier lui-même. ```php -$httpResponse->sendAsFile('facture.pdf'); +$httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Envoie un cookie. Valeurs par défaut des paramètres : -| `$path` | `'/'` | le cookie a une portée sur tous les chemins du (sous-)domaine *(configurable)* -| `$domain` | `null` | ce qui signifie avec une portée sur le (sous-)domaine actuel, mais pas ses sous-domaines *(configurable)* -| `$secure` | `true` | si le site fonctionne en HTTPS, sinon `false` *(configurable)* -| `$httpOnly` | `true` | le cookie est inaccessible à JavaScript -| `$sameSite` | `'Lax'` | le cookie peut ne pas être envoyé lors d'un [accès depuis un domaine différent |nette:glossary#Cookie SameSite] +| `$path` | `'/'` | le cookie est disponible pour tous les chemins du (sous-)domaine *(configurable)* +| `$domain` | `null` | c'est-à-dire disponible pour le (sous-)domaine courant, mais pas pour ses sous-domaines *(configurable)* +| `$secure` | `auto` | `true` si le site tourne en HTTPS, sinon `false` (valeur par défaut du framework ; la classe seule vaut `false` par défaut) *(configurable)* +| `$httpOnly` | `true` | le cookie est inaccessible au JavaScript +| `$sameSite` | `'Lax'` | le cookie peut ne pas être envoyé lors d'un [accès cross-origin |nette:glossary#Cookie SameSite] +| `$partitioned` | `false` | indique si le cookie est partitionné, voir plus bas *(depuis la v3.4)* -Vous pouvez modifier les valeurs par défaut des paramètres `$path`, `$domain` et `$secure` dans la [configuration |configuration#Cookie HTTP]. +Vous pouvez changer les valeurs par défaut des paramètres `$path`, `$domain` et `$secure` dans la [configuration |configuration#Cookie HTTP]. -Le temps peut être spécifié en secondes ou sous forme de chaîne de caractères : +L'expiration se passe sous forme de nombre de secondes, d'intervalle ou de date en texte, ou d'objet `DateTimeInterface`. La valeur `null` crée un cookie de session, que le navigateur jette à sa fermeture. Nette envoie l'expiration à la fois dans les attributs `Expires` et `Max-Age`. ```php -$httpResponse->setCookie('lang', 'fr', '100 days'); +$httpResponse->setCookie('lang', 'en', '100 days'); // expire dans 100 jours +$httpResponse->setCookie('lang', 'en', null); // cookie de session ``` -Le paramètre `$domain` détermine quels domaines peuvent accepter le cookie. S'il n'est pas spécifié, le cookie est accepté par le même (sous-)domaine qui l'a défini, mais pas par ses sous-domaines. Si `$domain` est spécifié, les sous-domaines sont également inclus. Par conséquent, spécifier `$domain` est moins restrictif que de l'omettre. Par exemple, avec `$domain = 'nette.org'`, les cookies sont également disponibles sur tous les sous-domaines comme `doc.nette.org`. +Le paramètre `$domain` détermine quels domaines peuvent accepter le cookie. S'il n'est pas indiqué, le cookie est accepté par le même (sous-)domaine que celui qui l'a défini, mais pas par ses sous-domaines. Si `$domain` est indiqué, les sous-domaines sont inclus eux aussi. Indiquer `$domain` est donc moins restrictif que de l'omettre. Par exemple, avec `$domain = 'nette.org'`, les cookies sont aussi disponibles sur tous les sous-domaines comme `doc.nette.org`. + +Vous pouvez passer la valeur `$sameSite` sous forme d'enum `Nette\Http\SameSite` - `SameSite::Lax`, `SameSite::Strict` ou `SameSite::None` (les valeurs chaîne `'Lax'`, `'Strict'`, `'None'` fonctionnent aussi). Si vous la fixez à `SameSite::None`, l'attribut `$secure` est activé automatiquement, car les navigateurs refusent un cookie `SameSite=None` qui n'est pas sécurisé. + +.{data-version:3.4.0} +Les cookies partitionnés (CHIPS) donnent à un cookie son propre stockage distinct pour chaque site de premier niveau. Ainsi, lorsqu'un service tiers (par exemple un widget intégré) pose un cookie partitionné, le navigateur en conserve une copie distincte pour chaque site où le widget apparaît, et ces copies ne peuvent pas être reliées entre elles à des fins de pistage inter-sites. Activez-les en fixant `$partitioned` à `true` ; cela exige aussi l'attribut `$secure`, qui est donc activé automatiquement. -Pour la valeur `$sameSite`, vous pouvez utiliser les constantes `Response::SameSiteLax`, `Response::SameSiteStrict` et `Response::SameSiteNone`. +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- Supprime un cookie. Les valeurs par défaut des paramètres sont : - `$path` avec une portée sur tous les répertoires (`'/'`) -- `$domain` avec une portée sur le (sous-)domaine actuel, mais pas ses sous-domaines -- `$secure` est régi par les paramètres de la [configuration |configuration#Cookie HTTP] +- `$domain` avec une portée sur le (sous-)domaine courant, mais pas sur ses sous-domaines +- `$secure` dépend des réglages de la [configuration |configuration#Cookie HTTP] ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +L'objet [api:Nette\Http\Context] réunit la requête et la réponse et aide à la mise en cache HTTP. Il n'est pas enregistré comme service, vous le créez donc vous-même. Dans les presenters, il est généralement plus simple d'utiliser la méthode [lastModified() |application:presenters#Cache HTTP] ; le contexte est utile quand vous envoyez vous-même la réponse, par exemple depuis votre propre classe de réponse. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Détermine si le contenu a changé depuis la dernière visite du client. Si vous passez la date de dernière modification, elle envoie l'en-tête `Last-Modified` ; si vous passez un validateur ETag (une courte chaîne identifiant la version actuelle du contenu, par exemple son hachage), elle envoie l'en-tête `ETag`. Elle compare ensuite les deux aux en-têtes `If-Modified-Since` et `If-None-Match` envoyés par le navigateur. + +Si le navigateur détient déjà une version correspondante, la méthode fixe le code `304 Not Modified` et renvoie `false` : dans ce cas, n'envoyez pas du tout le corps de la réponse. Sinon, elle renvoie `true`. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Les deux paramètres sont facultatifs. Si vous ne connaissez pas la date de modification du contenu, n'utilisez que l'ETag, et inversement. diff --git a/http/fr/sessions.texy b/http/fr/sessions.texy index d95463fa3e..e8cac9877d 100644 --- a/http/fr/sessions.texy +++ b/http/fr/sessions.texy @@ -3,7 +3,7 @@ Sessions <div class=perex> -HTTP est un protocole sans état, mais presque toutes les applications ont besoin de conserver un état entre les requêtes, par exemple le contenu d'un panier d'achat. C'est précisément à cela que servent les sessions. Nous allons montrer : +HTTP est un protocole sans état ; presque toutes les applications ont pourtant besoin de conserver un état entre les requêtes, par exemple le contenu d'un panier d'achat. C'est exactement à cela que servent les sessions. Nous allons montrer : - comment utiliser les sessions - comment éviter les conflits de noms @@ -11,33 +11,33 @@ HTTP est un protocole sans état, mais presque toutes les applications ont besoi </div> -Lors de l'utilisation des sessions, chaque utilisateur reçoit un identifiant unique appelé ID de session, qui est transmis dans un cookie. Celui-ci sert de clé pour les données de session. Contrairement aux cookies, qui sont stockés côté navigateur, les données de session sont stockées côté serveur. +Lors de l'utilisation des sessions, chaque utilisateur reçoit un identifiant unique appelé ID de session, transmis dans un cookie. Il sert de clé vers les données de la session. Contrairement aux cookies, qui sont stockés du côté du navigateur, les données de session sont stockées du côté du serveur. -Nous configurons la session dans la [configuration |configuration#Session], le choix de la durée d'expiration est particulièrement important. +Nous configurons les sessions dans la [configuration |configuration#Session] ; le choix de la durée d'expiration est particulièrement important. -La gestion de la session est assurée par l'objet [api:Nette\Http\Session], auquel vous accédez en vous le faisant passer via l'[injection de dépendances |dependency-injection:passing-dependencies]. Dans les presenters, il suffit d'appeler `$session = $this->getSession()`. +La gestion des sessions est assurée par l'objet [api:Nette\Http\Session], auquel vous accédez en vous le faisant passer par [injection de dépendances |dependency-injection:passing-dependencies]. Dans les presenters, il suffit d'appeler `$session = $this->getSession()`. → [Installation et prérequis |@home#Installation] -Démarrage de la session -======================= +Démarrer la session +=================== -Par défaut, Nette démarre automatiquement la session au moment où nous commençons à lire ou à écrire des données dedans. Manuellement, la session est démarrée à l'aide de `$session->start()`. +Par défaut, Nette démarre automatiquement une session dès que nous commençons à y lire ou à y écrire des données. Pour démarrer une session manuellement, utilisez `$session->start()`. -PHP envoie lors du démarrage de la session des en-têtes HTTP affectant la mise en cache, voir [php:session_cache_limiter], et éventuellement aussi un cookie avec l'ID de session. Il est donc nécessaire de toujours démarrer la session avant d'envoyer toute sortie au navigateur, sinon une exception sera levée. Si vous savez donc que la session sera utilisée pendant le rendu de la page, démarrez-la manuellement avant, par exemple dans le presenter. +PHP envoie au démarrage de la session des en-têtes HTTP influençant la mise en cache (voir [php:session_cache_limiter]), et éventuellement un cookie contenant l'ID de session. Il est donc toujours nécessaire de démarrer la session avant d'envoyer la moindre sortie au navigateur ; sinon, une exception sera levée. Si vous savez qu'une session sera utilisée pendant le rendu de la page, démarrez-la donc manuellement au préalable, par exemple dans le presenter. -En mode développeur, Tracy démarre la session car elle l'utilise pour afficher les barres avec les redirections et les requêtes AJAX dans la barre Tracy. +En mode développement, Tracy démarre la session, car elle s'en sert pour afficher dans la Tracy Bar les barres relatives aux redirections et aux requêtes AJAX. Sections ======== -En PHP pur, le stockage des données de session est réalisé sous forme de tableau accessible via la variable globale `$_SESSION`. Le problème est que les applications sont généralement composées de nombreuses parties indépendantes les unes des autres, et si toutes n'ont qu'un seul tableau à leur disposition, tôt ou tard un conflit de noms se produira. +En PHP pur, le stockage des données de session est implémenté comme un tableau accessible via la variable globale `$_SESSION`. Le problème est que les applications se composent généralement de nombreuses parties indépendantes et que, si toutes ne disposent que d'un seul tableau, une collision de noms finira tôt ou tard par se produire. -Nette Framework résout le problème en divisant tout l'espace en sections (objets [api:Nette\Http\SessionSection]). Chaque unité utilise alors sa propre section avec un nom unique et aucune collision ne peut plus se produire. +Nette Framework résout ce problème en divisant tout l'espace en sections (objets [api:Nette\Http\SessionSection]). Chaque unité utilise alors sa propre section portant un nom unique, et aucune collision ne peut se produire. -Nous obtenons la section de la session : +Nous obtenons une section depuis la session : ```php $section = $session->getSection('nom unique'); @@ -50,22 +50,22 @@ Dans le presenter, il suffit d'utiliser `getSession()` avec un paramètre : $section = $this->getSession('nom unique'); ``` -On peut vérifier l'existence de la section avec la méthode `$session->hasSection('nom unique')`. +L'existence d'une section peut être vérifiée à l'aide de la méthode `$session->hasSection('nom unique')`. La liste des noms de toutes les sections existantes est renvoyée par `$session->getSectionNames()`. -Travailler avec la section elle-même est ensuite très facile à l'aide des méthodes `set()`, `get()` et `remove()` : +Le travail avec la section elle-même est ensuite très simple grâce aux méthodes `set()`, `get()` et `remove()` : ```php // écriture d'une variable -$section->set('userName', 'franta'); +$section->set('userName', 'john'); -// lecture d'une variable, retourne null si elle n'existe pas +// lecture d'une variable, renvoie null si elle n'existe pas echo $section->get('userName'); // suppression d'une variable $section->remove('userName'); ``` -Pour obtenir toutes les variables de la section, il est possible d'utiliser une boucle `foreach` : +Pour obtenir toutes les variables d'une section, vous pouvez utiliser une boucle `foreach` : ```php foreach ($section as $key => $val) { @@ -74,37 +74,37 @@ foreach ($section as $key => $val) { ``` -Définition de l'expiration --------------------------- +Comment définir l'expiration +---------------------------- -Il est possible de définir une expiration pour des sections individuelles ou même des variables individuelles. Nous pouvons ainsi faire expirer la connexion de l'utilisateur après 20 minutes, tout en continuant à mémoriser le contenu du panier. +L'expiration peut être définie pour chaque section, voire pour chaque variable. Nous pouvons faire expirer la connexion d'un utilisateur au bout de 20 minutes tout en continuant de mémoriser le contenu du panier. ```php -// la section expirera après 20 minutes +// la section expire au bout de 20 minutes $section->setExpiration('20 minutes'); ``` -Pour définir l'expiration de variables individuelles, le troisième paramètre de la méthode `set()` est utilisé : +Pour définir l'expiration de variables individuelles, utilisez le troisième paramètre de la méthode `set()` : ```php -// la variable 'flash' expirera déjà après 30 secondes +// la variable 'flash' expire au bout de 30 secondes $section->set('flash', $message, '30 seconds'); ``` .[note] -N'oubliez pas que la durée d'expiration de toute la session (voir [configuration de session |configuration#Session]) doit être égale ou supérieure à la durée définie pour les sections ou variables individuelles. +Rappelez-vous que la durée d'expiration de la session entière (voir la [configuration des sessions |configuration#Session]) doit être égale ou supérieure à la durée définie pour les sections ou variables individuelles. -L'annulation d'une expiration précédemment définie est réalisée avec la méthode `removeExpiration()`. La suppression immédiate de toute la section est assurée par la méthode `remove()`. +Pour annuler une expiration définie précédemment, utilisez la méthode `removeExpiration()` ; pour effacer l'expiration d'une variable précise, passez son nom : `removeExpiration('flash')`. Pour supprimer immédiatement toute la section, utilisez la méthode `remove()`. Événements $onStart, $onBeforeWrite ----------------------------------- -L'objet `Nette\Http\Session` a des [événements |nette:glossary#Événements events] `$onStart` et `$onBeforeWrite`, vous pouvez donc ajouter des callbacks qui seront appelés après le démarrage de la session ou avant son écriture sur le disque et sa fermeture ultérieure. +L'objet `Nette\Http\Session` possède les [événements |nette:glossary#Événements] `$onStart` et `$onBeforeWrite`, vous pouvez donc ajouter des callbacks invoqués après le démarrage de la session ou avant qu'elle ne soit écrite sur le disque puis terminée. ```php $session->onBeforeWrite[] = function () { - // nous écrivons les données dans la session + // écriture de données dans la session $this->section->set('basket', $this->basket); }; ``` @@ -113,7 +113,7 @@ $session->onBeforeWrite[] = function () { Gestion de la session ===================== -Aperçu des méthodes de la classe `Nette\Http\Session` pour la gestion de la session : +Aperçu des méthodes de la classe `Nette\Http\Session` pour la gestion des sessions : <div class=wiki-methods-brief> @@ -140,17 +140,17 @@ Termine et supprime la session. exists(): bool .[method] ------------------------ -La requête HTTP contient-elle un cookie avec l'ID de session ? +La requête HTTP contient-elle un cookie avec un ID de session ? regenerateId(): void .[method] ------------------------------ -Génère un nouvel ID de session aléatoire. Les données restent conservées. +Génère un nouvel ID de session aléatoire. Les données sont conservées. getId(): string .[method] ------------------------- -Retourne l'ID de session. +Renvoie l'ID de session. </div> @@ -158,34 +158,34 @@ Retourne l'ID de session. Configuration ------------- -Nous configurons la session dans la [configuration |configuration#Session]. Si vous écrivez une application qui n'utilise pas de conteneur DI, ces méthodes servent à la configuration. Elles doivent être appelées avant le démarrage de la session. +Nous configurons la session dans la [configuration |configuration#Session]. Si vous écrivez une application qui n'utilise pas de conteneur DI, utilisez ces méthodes pour la configurer. Elles doivent être appelées avant le démarrage de la session. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Définit le nom du cookie dans lequel l'ID de session est transmis. Le nom standard est `PHPSESSID`. Utile si vous exécutez plusieurs applications différentes sur le même site web. +Définit le nom du cookie dans lequel l'ID de session est transmis. Le nom standard est `PHPSESSID`. C'est utile si vous faites tourner plusieurs applications différentes sur le même site. getName(): string .[method] --------------------------- -Retourne le nom du cookie dans lequel l'ID de session est transmis. +Renvoie le nom du cookie dans lequel l'ID de session est transmis. setOptions(array $options): static .[method] -------------------------------------------- -Configure la session. Il est possible de définir toutes les [directives de session PHP |https://www.php.net/manual/en/session.configuration.php] (au format camelCase, par ex. au lieu de `session.save_path`, nous écrivons `savePath`) ainsi que [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. +Configure la session. Il est possible de définir toutes les [directives de session |https://www.php.net/manual/en/session.configuration.php] de PHP (au format camelCase, par exemple écrire `savePath` au lieu de `session.save_path`) ainsi que [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Définit la durée d'inactivité après laquelle la session expire. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Définit la durée d'inactivité au bout de laquelle la session expire. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Définition des paramètres pour le cookie. Vous pouvez modifier les valeurs par défaut des paramètres dans la [configuration |configuration#Cookie de session]. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +Définit les paramètres des cookies. Vous pouvez changer les valeurs par défaut des paramètres dans la [configuration |configuration#Cookie de session]. setSavePath(string $path): static .[method] @@ -195,17 +195,17 @@ Définit le répertoire où sont stockés les fichiers de session. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Définition d'un gestionnaire personnalisé, voir la [documentation PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Définit un handler personnalisé, voir la [documentation PHP |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> -Sécurité avant tout -=================== +La sécurité avant tout +====================== -Le serveur suppose qu'il communique toujours avec le même utilisateur tant que les requêtes sont accompagnées du même ID de session. La tâche des mécanismes de sécurité est de garantir que ce soit réellement le cas et qu'il ne soit pas possible de voler ou de substituer l'identifiant. +Le serveur part du principe qu'il communique avec le même utilisateur tant que les requêtes sont accompagnées du même ID de session. Le rôle des mécanismes de sécurité est de garantir qu'il en va bien ainsi et que l'identifiant ne peut être ni volé ni substitué. -Nette Framework configure donc correctement les directives PHP pour que l'ID de session soit transmis uniquement dans le cookie, le rende inaccessible à JavaScript et ignore les éventuels identifiants dans l'URL. De plus, dans les moments critiques, comme la connexion de l'utilisateur, il génère un nouvel ID de session. +Nette Framework configure donc correctement les directives de PHP pour ne transmettre l'ID de session que dans les cookies, le rendre inaccessible au JavaScript et ignorer tout identifiant présent dans l'URL. De plus, aux moments critiques, comme la connexion de l'utilisateur, il génère un nouvel ID de session. .[note] -Pour la configuration de PHP, la fonction ini_set est utilisée, que certains hébergeurs interdisent malheureusement. Si c'est le cas de votre hébergeur, essayez de négocier avec lui pour qu'il vous autorise la fonction ou au moins configure le serveur. +La fonction `ini_set` est utilisée pour configurer PHP, mais malheureusement certains hébergeurs en interdisent l'usage. Si c'est le cas du vôtre, essayez de convenir avec lui qu'il vous autorise cette fonction ou qu'il configure au moins correctement le serveur. diff --git a/http/fr/ssrf.texy b/http/fr/ssrf.texy new file mode 100644 index 0000000000..47ea16c871 --- /dev/null +++ b/http/fr/ssrf.texy @@ -0,0 +1,183 @@ +Protection contre le SSRF +************************* + +.[perex] +Lorsque votre application télécharge une URL fournie par un utilisateur, un attaquant peut en abuser pour atteindre votre réseau interne. Les classes [#UrlValidator] et [#IPAddress] vous aident à vous prémunir contre ces attaques Server-Side Request Forgery (SSRF). + +→ [Installation et prérequis |@home#Installation] + + +Qu'est-ce que le SSRF ? +======================= + +Imaginez une fonctionnalité où l'utilisateur saisit une URL et où votre serveur la télécharge : un avatar depuis une adresse distante, la cible d'un webhook, l'aperçu d'un lien. Cela paraît anodin, mais c'est le serveur qui atteint l'adresse, pas le navigateur de l'utilisateur. Et le serveur voit des endroits que l'attaquant ne voit pas : l'interface loopback, le réseau privé, les services cloud. + +Un attaquant soumet donc une URL qui pointe vers l'intérieur au lieu de l'internet public. Les cibles typiques sont : + +- les métadonnées cloud sur `http://169.254.169.254/`, qui peuvent laisser fuir des clés d'accès +- les panneaux d'administration internes et les routeurs comme `http://192.168.1.1/` +- les services sans authentification, comme Redis sur `http://localhost:6379/` + +Cette catégorie de failles est si répandue qu'elle figure dans le [Top 10 de l'OWASP |https://owasp.org/Top10/]. La défense consiste à valider l'URL **avant** de la récupérer et à refuser tout ce qui se résout vers une adresse non publique. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] contrôle une URL par rapport à une politique configurable : le schéma, le port, l'hôte, les userinfo et les adresses IP vers lesquelles l'hôte se résout. L'usage de base tient en un seul appel : + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // URL non sûre, ne pas la récupérer +} +``` + +La politique par défaut est délibérément stricte : elle n'accepte que `https` sur le port 443 pointant vers une adresse IP publique. Tout le reste (loopback, plages privées, link-local y compris les métadonnées cloud, plages réservées) est refusé, et le multicast est refusé sans condition. C'est le bon point de départ pour récupérer des URL quelconques fournies par les utilisateurs. + + +Configurer la politique +----------------------- + +Vous façonnez la politique par le constructeur. Par exemple, pour autoriser le simple `http` sur n'importe quel port et atteindre des adresses privées (utile au sein d'un réseau de confiance) : + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // n'importe quel port + allowPrivateIps: true, +); +``` + +Un usage courant consiste à restreindre la récupération à un ensemble fixe de domaines partenaires à l'aide d'une liste blanche d'hôtes. Le préfixe `*.` correspond à n'importe quelle profondeur de sous-domaine, mais pas au domaine apex : indiquez les deux formes si vous en avez besoin : + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +L'ensemble des options du constructeur : + +| Paramètre | Valeur par défaut | Signification +|--------------------- +| `schemes` | `['https']` | schémas autorisés ; `[]` refuse tout +| `ports` | `[443]` | ports autorisés, `null` = n'importe lequel ; le port implicite du schéma est pris en compte +| `allowPrivateIps` | `false` | autorise les plages privées (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | autorise le loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | autorise le link-local, y compris les métadonnées cloud 169.254.169.254 +| `allowReserved` | `false` | autorise les plages réservées par l'IANA +| `allowUserinfo` | `false` | autorise `user:pass@` dans l'URL +| `hostAllowlist` | `null` | si définie, l'hôte doit correspondre à un motif ; `[]` refuse tout +| `hostBlocklist` | `null` | si définie, l'hôte ne doit correspondre à aucun motif + + +Méthodes de validation +---------------------- + +Le validateur offre trois méthodes. `allows()` effectue le contrôle complet, résolution DNS comprise : l'hôte est résolu et **chaque** adresse A/AAAA doit satisfaire la politique IP : + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` saute la résolution DNS et les contrôles de plages IP. Utilisez-la comme préfiltre rapide, ou lorsque la validation DNS est déléguée à la couche de récupération : + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Les deux méthodes acceptent une chaîne, un objet [UrlImmutable |urls#UrlImmutable] ou `null` (qui échoue toujours). + + +Déjouer le DNS rebinding +------------------------ + +Il existe une subtile course entre la validation et la récupération : un attaquant peut renvoyer une IP sûre au moment où vous validez l'hôte, puis basculer le DNS vers une IP interne pour le téléchargement réel. Pour combler cette faille, `getResolvedIPs()` renvoie les adresses IP validées, et vous épinglez la connexion sur elles afin que la récupération ne puisse pas être détournée ailleurs : + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // URL non sûre +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... exécution de la requête +``` + +La méthode renvoie un tableau de chaînes IP (les enregistrements A d'abord, puis les AAAA) ayant satisfait toute la politique, ou un tableau vide en cas d'échec. Pour une IP littérale dans l'URL, elle valide l'adresse directement et n'effectue aucune résolution DNS. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] est un objet valeur immuable permettant de travailler avec les adresses IPv4 et IPv6. `UrlValidator` l'utilise en interne, mais il est pratique en lui-même dès que vous classez des adresses. Le constructeur lève une `Nette\InvalidArgumentException` pour une adresse invalide : + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +Quand vous ne voulez pas d'exception, utilisez la fabrique `tryFrom()` ou le contrôleur `isValid()` : + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Classification des adresses +--------------------------- + +Les prédicats vous disent à quelle classe appartient une adresse. Le principal est `isPublic()` : vrai uniquement pour les adresses routables publiquement, ce qui est exactement ce que veut une protection anti-SSRF : + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (plage des métadonnées cloud) +``` + +L'ensemble des prédicats : + +| Méthode | Teste +|-------------------- +| `isPublic()` | routable publiquement (aucun des cas ci-dessous) +| `isPrivate()` | plages privées RFC 1918 / 4193 +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (métadonnées cloud comprises), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | réservées par l'IANA (documentation, CGNAT, usage futur, …) + + +Appartenance à une plage +------------------------ + +`isInRange()` teste si l'adresse tombe dans un bloc CIDR. Vous pouvez passer un réseau avec un préfixe, ou une simple adresse pour une correspondance exacte (/32 implicite pour IPv4, /128 pour IPv6) : + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (correspondance exacte) +``` + +Une entrée mal formée ou une famille IP différente renvoie `false`. + + +IPv6 mappée IPv4 +---------------- + +Les adresses écrites en IPv6 mappée IPv4 (comme `::ffff:127.0.0.1`) sont un moyen classique de passer à travers des filtres naïfs. `IPAddress` les normalise, si bien que les prédicats de plage voient à travers le déguisement : + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +Les méthodes `isIPv4()` et `isIPv6()` rendent compte de la forme textuelle : une adresse mappée est IPv6, pas IPv4. diff --git a/http/fr/upgrading.texy b/http/fr/upgrading.texy new file mode 100644 index 0000000000..4872183089 --- /dev/null +++ b/http/fr/upgrading.texy @@ -0,0 +1,54 @@ +Mise à niveau +************* + + +Mise à niveau vers la version 3.4 +================================= + +La version minimale de PHP requise est 8.3. + +- la méthode `Request::isSameSite()` est obsolète au profit d'`isFrom()`, qui détermine l'origine de la requête à partir des en-têtes `Sec-Fetch-*`. La protection automatique des formulaires et des signaux devient plus précise, et un comportement change : la navigation directe (un favori, une adresse tapée à la main, un lien dans un e-mail) n'est plus considérée comme same-site. Si un signal repose sur des liens d'action dans des e-mails, marquez-le par `#[Requires(sameOrigin: false)]`. +- le cookie `_nss` n'est désormais envoyé qu'aux navigateurs qui n'envoient pas l'en-tête `Sec-Fetch-Site` +- `setCookie()` envoie l'attribut `Max-Age` et impose le drapeau `Secure` pour `SameSite=None` et pour les cookies partitionnés +- l'enum `SameSite` remplace les constantes `IResponse::SameSiteLax` etc., qui sont obsolètes +- l'expiration s'interprète partout de la même façon : un nombre est un nombre relatif de secondes, une chaîne est un intervalle ou une date. Passer un timestamp UNIX absolu est obsolète, et un cookie de session est représenté par `null` au lieu de `0`. +- la méthode obsolète `Request::getRemoteHost()` renvoie `null` +- la classe `Nette\Http\UserStorage`, obsolète depuis longtemps, a été supprimée + +Toute l'histoire du passage aux en-têtes `Sec-Fetch-*` est racontée dans l'article [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Mise à niveau vers la version 3.2 +================================= + +- les identifiants de l'authentification HTTP Basic ne font plus partie de l'objet `Url`, si bien que `$url->getUser()` et `$url->getPassword()` renvoient une chaîne vide. Lisez-les à l'aide de la nouvelle méthode `$request->getBasicCredentials()`. + +Les raisons de ce changement sont expliquées dans l'article [Nette Http 3.2: change access to credentials |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]. + + +Mise à niveau vers la version 3.1 +================================= + +- les cookies sont envoyés avec le drapeau `sameSite: Lax` +- `cookieSecure` vaut désormais 'auto' par défaut +- l'option `session.cookieSecure` est obsolète ; c'est `http.cookieSecure` qui est utilisée à la place +- le cookie `nette-samesite` a été renommé en `_nss` +- `Nette\Http\Request::getFile()` accepte un tableau de clés et renvoie `FileUpload|null` +- `Nette\Http\Session::getCookieParameters()` est obsolète +- `Nette\Http\FileUpload::getName()` a été renommée en `getUntrustedName()` +- `Nette\Http\Url` : `getBasePath()`, `getBaseUrl()` et `getRelativeUrl()` sont obsolètes (ces méthodes font partie d'`UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` est obsolète +- `Nette\Http\FileUpload::getImageSize()` renvoie la paire `[largeur, hauteur]` +- avec `autoStart: smart` (la valeur par défaut), la session n'est plus démarrée juste après le lancement de l'application au seul motif que le navigateur a envoyé un cookie de session ; elle démarre à la première lecture ou écriture. Les valeurs `always` et `never` ont été ajoutées. +- lorsque le navigateur envoie un ID de session pour lequel aucune session n'existe, Nette supprime le cookie au lieu de créer une nouvelle session +- pour accéder aux sections de session, préférez les méthodes `set()`, `get()` et `remove()` ; contrairement à l'accès par propriété, elles distinguent correctement la lecture de l'écriture et ne démarrent pas la session inutilement +- les valeurs par défaut de `cookiePath` et `cookieDomain` peuvent être définies dans la configuration + +Le comportement des sessions est décrit en détail dans l'article [Nette Http 3.1: much smarter sessions |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions]. + + +Mise à niveau vers la version 3.0 +================================= + +- l'objet `Nette\Http\UrlScript` (renvoyé par exemple par `Nette\Http\Request::getUrl()`) est désormais immuable +- dans `new Nette\Http\Url('abcd')`, `abcd` représente le chemin, pas le domaine ; depuis la 3.0, `(new Nette\Http\Url('abcd'))->setScheme('http')` génère correctement `http:abcd` au lieu du précédent `http://abcd` diff --git a/http/fr/urls.texy b/http/fr/urls.texy index dcf73b8d0e..e70fdbefe4 100644 --- a/http/fr/urls.texy +++ b/http/fr/urls.texy @@ -2,7 +2,7 @@ Travailler avec les URL *********************** .[perex] -Les classes [#Url], [#UrlImmutable] et [#UrlScript] permettent de générer, parser et manipuler facilement les URL. +Les classes [#Url], [#UrlImmutable] et [#UrlScript] facilitent la génération, l'analyse et la manipulation des URL. → [Installation et prérequis |@home#Installation] @@ -10,7 +10,7 @@ Les classes [#Url], [#UrlImmutable] et [#UrlScript] permettent de générer, par Url === -La classe [api:Nette\Http\Url] permet de travailler facilement avec les URL et leurs différentes composantes, illustrées par ce schéma : +La classe [api:Nette\Http\Url] permet de manipuler facilement les URL et leurs différents composants, comme le montre ce schéma : /--pre scheme user password host port path query fragment @@ -22,7 +22,7 @@ La classe [api:Nette\Http\Url] permet de travailler facilement avec les URL et l hostUrl authority \-- -La génération d'URL est intuitive : +Générer des URL est intuitif : ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -Il est également possible de parser une URL et de la manipuler ensuite : +Vous pouvez aussi analyser une URL puis la manipuler : ```php $url = new Url( @@ -44,7 +44,7 @@ $url = new Url( ); ``` -La classe `Url` implémente l'interface `JsonSerializable` et a une méthode `__toString()`, de sorte que l'objet peut être affiché ou utilisé dans des données passées à `json_encode()`. +La classe `Url` implémente l'interface `JsonSerializable` et possède une méthode `__toString()`, l'objet peut donc être affiché ou utilisé dans les données passées à `json_encode()`. ```php echo $url; @@ -52,13 +52,13 @@ echo json_encode([$url]); ``` -Composantes de l'URL .[method] ------------------------------- +Composants de l'URL +------------------- -Pour retourner ou modifier les différentes composantes de l'URL, les méthodes suivantes sont à votre disposition : +Les méthodes suivantes permettent d'obtenir ou de modifier les différents composants de l'URL : .[language-php] -| Setter | Getter | Valeur retournée +| Setter | Getter | Valeur renvoyée |-------------------------------------------------------------------------------------------- | `setScheme(string $scheme)` | `getScheme(): string` | `'http'` | `setUser(string $user)` | `getUser(): string` | `'john'` @@ -71,22 +71,25 @@ Pour retourner ou modifier les différentes composantes de l'URL, les méthodes | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL complète +| | `getAbsoluteUrl(): string` | l'URL entière -Attention : Lorsque vous travaillez avec une URL obtenue à partir d'une [requête HTTP |request], gardez à l'esprit qu'elle ne contiendra pas le fragment, car le navigateur ne l'envoie pas au serveur. +Les méthodes `getUser()`, `getPassword()`, `setUser()` et `setPassword()` sont obsolètes, car intégrer des identifiants directement dans une URL est déconseillé. -Nous pouvons également travailler avec les paramètres de requête individuels en utilisant : +Attention : lorsque vous travaillez avec une URL obtenue depuis une [requête HTTP |request], gardez à l'esprit qu'elle ne contiendra pas le fragment, car le navigateur ne l'envoie pas au serveur. + +Nous pouvons aussi travailler sur les différents paramètres de la query string à l'aide de : .[language-php] | Setter | Getter |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] ------------------------------------------- -Retourne la partie droite ou gauche de l'hôte. Voici comment cela fonctionne si l'hôte est `www.nette.org` : +Renvoie la partie droite ou gauche de l'hôte. Voici comment cela fonctionne si l'hôte est `www.nette.org` : .[language-php] | `getDomain(1)` | `'org'` @@ -98,8 +101,8 @@ Retourne la partie droite ou gauche de l'hôte. Voici comment cela fonctionne si | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Vérifie si deux URL sont identiques. ```php @@ -107,9 +110,14 @@ $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Convertit l'URL en forme canonique. Cela met le nom d'hôte en minuscules et normalise le chemin (encodage pour cent et suppression des caractères superflus). La query string reste inchangée. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- -Vérifie si l'URL est absolue. Une URL est considérée comme absolue si elle commence par un schéma (par ex. http, https, ftp) suivi de deux points. +Vérifie si une URL est absolue. Une URL est considérée comme absolue si elle commence par un schéma (par exemple http, https, ftp) suivi de deux-points. ```php Url::isAbsolute('https://nette.org'); // true @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -Normalise le chemin dans l'URL en supprimant les segments spéciaux `.` et `..`. La méthode supprime les éléments de chemin superflus de la même manière que le font les navigateurs web. +Normalise le chemin d'une URL en supprimant les segments spéciaux `.` et `..`. Cette méthode supprime les éléments de chemin superflus de la même façon que les navigateurs web. ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,24 +139,24 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -La classe [api:Nette\Http\UrlImmutable] est une alternative immutable (immuable) à la classe [#Url] (similaire à la façon dont `DateTimeImmutable` en PHP est une alternative immuable à `DateTime`). Au lieu de setters, elle a des withers, qui ne modifient pas l'objet, mais retournent de nouvelles instances avec la valeur modifiée : +La classe [api:Nette\Http\UrlImmutable] est une alternative immuable à la classe [#Url] (de la même façon que `DateTimeImmutable` est l'alternative immuable de `DateTime` en PHP). Au lieu de setters, elle possède des "withers", qui ne modifient pas l'objet mais renvoient de nouvelles instances portant la valeur modifiée : ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/fr/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/fr/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` -La classe `UrlImmutable` implémente l'interface `JsonSerializable` et a une méthode `__toString()`, de sorte que l'objet peut être affiché ou utilisé dans des données passées à `json_encode()`. +La classe `UrlImmutable` implémente l'interface `JsonSerializable` et possède une méthode `__toString()`, l'objet peut donc être affiché ou utilisé dans les données passées à `json_encode()`. ```php echo $url; @@ -156,13 +164,13 @@ echo json_encode([$url]); ``` -Composantes de l'URL .[method] ------------------------------- +Composants de l'URL +------------------- -Pour retourner ou modifier les différentes composantes de l'URL, les méthodes suivantes sont utilisées : +Les méthodes suivantes permettent d'obtenir ou de changer les différents composants de l'URL : .[language-php] -| Wither | Getter | Valeur retournée +| Wither | Getter | Valeur renvoyée |-------------------------------------------------------------------------------------------- | `withScheme(string $scheme)` | `getScheme(): string` | `'http'` | `withUser(string $user)` | `getUser(): string` | `'john'` @@ -175,11 +183,11 @@ Pour retourner ou modifier les différentes composantes de l'URL, les méthodes | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL complète +| | `getAbsoluteUrl(): string` | l'URL entière -La méthode `withoutUserInfo()` supprime `user` et `password`. +Les méthodes `getUser()`, `getPassword()`, `withUser()`, `withPassword()` et `withoutUserInfo()` sont obsolètes, car intégrer des identifiants directement dans une URL est déconseillé. -Nous pouvons également travailler avec les paramètres de requête individuels en utilisant : +Nous pouvons aussi travailler sur les différents paramètres de la query string à l'aide de : .[language-php] | Wither | Getter @@ -190,7 +198,7 @@ Nous pouvons également travailler avec les paramètres de requête individuels getDomain(int $level = 2): string .[method] ------------------------------------------- -Retourne la partie droite ou gauche de l'hôte. Voici comment cela fonctionne si l'hôte est `www.nette.org` : +Renvoie la partie droite ou gauche de l'hôte. Voici comment cela fonctionne si l'hôte est `www.nette.org` : .[language-php] | `getDomain(1)` | `'org'` @@ -204,11 +212,11 @@ Retourne la partie droite ou gauche de l'hôte. Voici comment cela fonctionne si resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -Dérive une URL absolue de la même manière qu'un navigateur traite les liens sur une page HTML : +Résout une URL absolue de la même façon qu'un navigateur traite les liens d'une page HTML : - si le lien est une URL absolue (contient un schéma), il est utilisé tel quel -- si le lien commence par `//`, seul le schéma de l'URL actuelle est repris -- si le lien commence par `/`, un chemin absolu est créé à partir de la racine du domaine -- dans les autres cas, l'URL est construite relativement au chemin actuel +- si le lien commence par `//`, seul le schéma de l'URL courante est repris +- si le lien commence par `/`, un chemin absolu depuis la racine du domaine est créé +- dans les autres cas, l'URL est construite relativement au chemin courant ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,8 +226,8 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Vérifie si deux URL sont identiques. ```php @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -La classe [api:Nette\Http\UrlScript] est un descendant de [#UrlImmutable] et l'étend avec d'autres composantes URL virtuelles, telles que le répertoire racine du projet, etc. Tout comme la classe parente, c'est un objet immutable (immuable). +La classe [api:Nette\Http\UrlScript] est un descendant d'[#UrlImmutable] et l'étend de composants d'URL virtuels supplémentaires, comme le répertoire racine du projet, etc. Comme sa classe parente, c'est un objet immuable. -Le diagramme suivant illustre les composantes que UrlScript reconnaît : +Le schéma suivant montre les composants que reconnaît UrlScript : /--pre baseUrl basePath relativePath relativeUrl @@ -244,23 +252,23 @@ Le diagramme suivant illustre les composantes que UrlScript reconnaît : scriptPath pathInfo \-- -- `baseUrl` est l'URL de base de l'application, y compris le domaine et la partie du chemin vers le répertoire racine de l'application -- `basePath` est la partie du chemin vers le répertoire racine de l'application -- `scriptPath` est le chemin vers le script actuel -- `relativePath` est le nom du script (éventuellement d'autres segments de chemin) relatif à basePath -- `relativeUrl` est toute la partie de l'URL après baseUrl, y compris la chaîne de requête et le fragment. -- `pathInfo` est une partie de l'URL peu utilisée aujourd'hui après le nom du script +- `baseUrl` est l'URL de base de l'application, domaine compris, jusqu'au répertoire racine de l'application +- `basePath` est la partie chemin menant au répertoire racine de l'application +- `scriptPath` est le chemin du script courant +- `relativePath` est le nom du script (et éventuellement d'autres segments de chemin) relativement à `basePath` +- `relativeUrl` est toute la partie de l'URL située après `baseUrl`, query string et fragment compris +- `pathInfo` est une partie de l'URL, aujourd'hui rarement utilisée, située après le nom du script -Pour retourner les parties de l'URL, les méthodes suivantes sont disponibles : +Les méthodes suivantes permettent d'obtenir ces parties de l'URL : .[language-php] -| Getter | Valeur retournée +| Getter | Valeur renvoyée |------------------------------------------------ | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Les objets `UrlScript` ne sont généralement pas créés directement, mais sont retournés par la méthode [Nette\Http\Request::getUrl()|request] avec les composantes déjà correctement définies pour la requête HTTP actuelle. +Nous ne créons généralement pas les objets `UrlScript` directement ; c'est la méthode [Nette\Http\Request::getUrl() |request] qui le renvoie, avec les composants déjà correctement renseignés pour la requête HTTP courante. diff --git a/http/hu/@home.texy b/http/hu/@home.texy deleted file mode 100644 index 3539e4b800..0000000000 --- a/http/hu/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -A `nette/http` csomag magába foglalja a [HTTP kérést|request] & [választ|response], a [sessionok |sessions] kezelését és az [URL-ek feldolgozását és összeállítását |urls]. - - -Telepítés ---------- - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/http -``` diff --git a/http/hu/@left-menu.texy b/http/hu/@left-menu.texy deleted file mode 100644 index 09f9a24d93..0000000000 --- a/http/hu/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Bevezetés |@home] -- [HTTP kérés|request] -- [HTTP válasz|response] -- [Sessions |sessions] -- [URL utilities |urls] -- [Konfiguráció |configuration] diff --git a/http/hu/@meta.texy b/http/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/http/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/http/hu/configuration.texy b/http/hu/configuration.texy deleted file mode 100644 index 9c8242f099..0000000000 --- a/http/hu/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -HTTP konfiguráció -***************** - -.[perex] -A Nette HTTP konfigurációs opcióinak áttekintése. - -Ha nem a teljes keretrendszert használja, csak ezt a könyvtárat, olvassa el, [hogyan kell betölteni a konfigurációt|bootstrap:]. - - -HTTP fejlécek -============= - -```neon -http: - # fejlécek, amelyek minden kéréssel elküldésre kerülnek - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # befolyásolja az X-Frame-Options fejlécet - frames: ... # (string|bool) alapértelmezett 'SAMEORIGIN' -``` - -A keretrendszer biztonsági okokból elküldi az `X-Frame-Options: SAMEORIGIN` fejlécet, amely azt mondja, hogy az oldalt csak akkor lehet megjeleníteni egy másik oldalon belül (az `<iframe>` elemben), ha ugyanazon a domainen található. Ez bizonyos helyzetekben nem kívánatos lehet (például ha Facebook alkalmazást fejleszt), a viselkedés ezért megváltoztatható a `frames: http://allowed-host.com` vagy `frames: true` beállítással. - - -Content Security Policy ------------------------ - -Könnyen összeállíthatók a `Content-Security-Policy` (továbbiakban CSP) fejlécek, leírásukat a [CSP leírásában |https://content-security-policy.com] találja. A CSP direktívák (mint pl. `script-src`) megadhatók akár stringként a specifikáció szerint, akár értékek tömbjeként a jobb olvashatóság érdekében. Ekkor nincs szükség idézőjelek írására a kulcsszavak, mint például a `'self'`, köré. A Nette automatikusan generál egy `nonce` értéket is, így a fejlécben például `'nonce-y4PopTLM=='` lesz. - -```neon -http: - # Content Security Policy - csp: - # string a CSP specifikáció szerinti formátumban - default-src: "'self' https://example.com" - - # értékek tömbje - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool kapcsolók esetén - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -A sablonokban használja a `<script n:nonce>...</script>`-et, és a nonce érték automatikusan kiegészül. Biztonságos webhelyek készítése a Nette-ben valóban egyszerű. - -Hasonlóan összeállíthatók a `Content-Security-Policy-Report-Only` (amelyek a CSP-vel párhuzamosan használhatók) és a [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] fejlécek is: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP cookie ------------ - -Megváltoztathatók a [Nette\Http\Response::setCookie() |response#setCookie] metódus és a session egyes paramétereinek alapértelmezett értékei. - -```neon -http: - # cookie hatóköre útvonal szerint - cookiePath: ... # (string) alapértelmezett '/' - - # domainek, amelyek elfogadják a cookie-t - cookieDomain: 'example.com' # (string|domain) alapértelmezett nincs beállítva - - # csak HTTPS-en keresztül küldeni a cookie-t? - cookieSecure: ... # (bool|auto) alapértelmezett auto - - # kikapcsolja a Nette által CSRF védelemként használt cookie küldését - disableNetteCookie: ... # (bool) alapértelmezett false -``` - -A `cookieDomain` attribútum meghatározza, mely domainek fogadhatják el a cookie-t. Ha nincs megadva, a cookie-t ugyanaz a (sub)domain fogadja el, amelyik beállította, *de nem* annak aldomainjei. Ha a `cookieDomain` meg van adva, az aldomainek is beletartoznak. Ezért a `cookieDomain` megadása kevésbé korlátozó, mint annak elhagyása. - -Például a `cookieDomain: nette.org` esetén a cookie-k minden aldomainen, mint például a `doc.nette.org`, is elérhetők. Ugyanezt elérhetjük a speciális `domain` értékkel is, tehát `cookieDomain: domain`. - -A `cookieSecure` attribútum `auto` alapértelmezett értéke azt jelenti, hogy ha a webhely HTTPS-en fut, a cookie-k a `Secure` jelzővel kerülnek elküldésre, és így csak HTTPS-en keresztül lesznek elérhetők. - - -HTTP proxy ----------- - -Ha a webhely HTTP proxy mögött fut, adja meg annak IP címét, hogy a HTTPS-en keresztüli kapcsolat és a kliens IP címének észlelése megfelelően működjön. Tehát hogy a [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] és [isSecured() |request#isSecured] függvények helyes értékeket adjanak vissza, és a sablonokban a linkek `https:` protokollal generálódjanak. - -```neon -http: - # IP cím, tartomány (pl. 127.0.0.1/8) vagy ezen értékek tömbje - proxy: 127.0.0.1 # (string|string[]) alapértelmezett nincs beállítva -``` - - -Session -======= - -Alapvető [session |sessions] beállítások: - -```neon -session: - # session panel megjelenítése a Tracy Bar-ban? - debugger: ... # (bool) alapértelmezett false - - # inaktivitási idő, amely után a session lejár - expiration: 14 days # (string) alapértelmezett '3 hours' - - # mikor kell elindítani a sessiont? - autoStart: ... # (smart|always|never) alapértelmezett 'smart' - - # handler, a SessionHandlerInterface interfészt implementáló szolgáltatás - handler: @handlerService -``` - -Az `autoStart` opció vezérli, hogy mikor kell elindítani a sessiont. Az `always` érték azt jelenti, hogy a session mindig elindul az alkalmazás indításakor. A `smart` érték azt jelenti, hogy a session csak akkor indul el az alkalmazás indításakor, ha már létezik, vagy abban a pillanatban, amikor olvasni vagy írni akarunk belőle. Végül a `never` érték letiltja a session automatikus indítását. - -Továbbá beállíthatók az összes PHP [session direktíva |https://www.php.net/manual/en/session.configuration.php] (camelCase formátumban) és a [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] is. Példa: - -```neon -session: - # 'session.name' írjuk 'name'-ként - name: MYID - - # 'session.save_path' írjuk 'savePath'-ként - savePath: "%tempDir%/sessions" -``` - - -Session cookie --------------- - -A session cookie ugyanazokkal a paraméterekkel kerül elküldésre, mint a [más cookie-k |#HTTP cookie], de ezeket megváltoztathatja számára: - -```neon -session: - # domainek, amelyek elfogadják a cookie-t - cookieDomain: 'example.com' # (string|domain) - - # korlátozások más domainről való hozzáférés esetén - cookieSamesite: None # (Strict|Lax|None) alapértelmezett Lax -``` - -A `cookieSamesite` attribútum befolyásolja, hogy a cookie elküldésre kerül-e [más domainről való hozzáférés |nette:glossary#SameSite cookie] esetén, ami bizonyos védelmet nyújt a [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF) támadások ellen. - - -DI szolgáltatások -================= - -Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez: - -| Név | Típus | Leírás -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP kérés| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP válasz| response] -| `session.session`| [api:Nette\Http\Session] | [session kezelés| sessions] diff --git a/http/hu/request.texy b/http/hu/request.texy deleted file mode 100644 index c3bac10ef1..0000000000 --- a/http/hu/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -HTTP kérés -********** - -.[perex] -A Nette a HTTP kérést érthető API-val rendelkező objektumokba zárja, és egyúttal szanitizáló szűrőt is biztosít. - -A HTTP kérést a [api:Nette\Http\Request] objektum képviseli. Ha a Nette-tel dolgozik, ezt az objektumot a keretrendszer automatikusan létrehozza, és [dependency injection |dependency-injection:passing-dependencies] segítségével átadhatja magának. A presenterekben elég csak a `$this->getHttpRequest()` metódust meghívni. Ha a Nette Frameworkön kívül dolgozik, létrehozhatja az objektumot a [#RequestFactory] segítségével. - -A Nette nagy előnye, hogy az objektum létrehozásakor automatikusan megtisztítja az összes GET, POST, COOKIE bemeneti paramétert, valamint az URL-t a vezérlőkarakterektől és az érvénytelen UTF-8 szekvenciáktól. Ezekkel az adatokkal ezután biztonságosan dolgozhat tovább. A megtisztított adatokat ezután a presenterekben és az űrlapokban használják. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Nette\Http\Request -================== - -Ez az objektum immutable (megváltoztathatatlan). Nincsenek setterei, csak egy ún. wither `withUrl()` metódusa van, amely nem változtatja meg az objektumot, hanem egy új példányt ad vissza megváltozott értékkel. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Egy klónt ad vissza más URL-lel. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Visszaadja a kérés URL-jét [UrlScript |urls#UrlScript] objektumként. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Figyelmeztetés: a böngészők nem küldik el a fragmentet a szerverre, így a `$url->getFragment()` üres stringet fog visszaadni. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Visszaadja a GET kérés paramétereit. - -```php -$all = $httpRequest->getQuery(); // visszaadja az összes paraméter tömbjét az URL-ből -$id = $httpRequest->getQuery('id'); // visszaadja a 'id' GET paramétert (vagy null-t) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Visszaadja a POST kérés paramétereit. - -```php -$all = $httpRequest->getPost(); // visszaadja az összes paraméter tömbjét a POST-ból -$id = $httpRequest->getPost('id'); // visszaadja a 'id' POST paramétert (vagy null-t) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Visszaadja a [feltöltést |#Feltöltött fájlok] [api:Nette\Http\FileUpload] objektumként: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // feltöltöttek valamilyen fájlt? - $file->getUntrustedName(); // a felhasználó által küldött fájlnév - $file->getSanitizedName(); // név veszélyes karakterek nélkül -} -``` - -A beágyazott struktúrához való hozzáféréshez adjon meg egy kulcsokból álló tömböt. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Mivel nem lehet megbízni a kívülről érkező adatokban, és így a fájlok struktúrájának formájában sem, ez a módszer biztonságosabb, mint például a `$request->getFiles()['my-form']['details']['avatar']`, amely meghiúsulhat. - - -getFiles(): array .[method] ---------------------------- -Visszaadja az [összes feltöltés |#Feltöltött fájlok] fáját normalizált struktúrában, amelynek levelei [api:Nette\Http\FileUpload] objektumok: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Visszaadja a cookie-t vagy `null`-t, ha nem létezik. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Visszaadja az összes cookie-t. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Visszaadja a HTTP metódust, amellyel a kérés történt. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Teszteli a HTTP metódust, amellyel a kérés történt. A paraméter kis- és nagybetű érzéketlen. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Visszaadja a HTTP fejlécet vagy `null`-t, ha nem létezik. A paraméter kis- és nagybetű érzéketlen. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Visszaadja az összes HTTP fejlécet asszociatív tömbként. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Titkosított a kapcsolat (HTTPS)? A megfelelő működéshez szükség lehet a [proxy beállítására |configuration#HTTP proxy]. - - -isSameSite(): bool .[method] ----------------------------- -Ugyanarról a (sub)domainről érkezik a kérés, és egy linkre kattintással indították? A Nette a `_nss` cookie-t (korábban `nette-samesite`) használja az észleléshez. - - -isAjax(): bool .[method] ------------------------- -AJAX kérésről van szó? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Visszaadja a felhasználó IP címét. A megfelelő működéshez szükség lehet a [proxy beállítására |configuration#HTTP proxy]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Visszaadja a felhasználó IP címének DNS fordítását. A megfelelő működéshez szükség lehet a [proxy beállítására |configuration#HTTP proxy]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Visszaadja a [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication] hitelesítési adatait. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Visszaadja a HTTP kérés törzsét. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Észleli a nyelvet. Paraméterként `$lang` átadjuk az alkalmazás által támogatott nyelvek tömbjét, és visszaadja azt, amelyet a látogató böngészője legszívesebben látna. Ez nem varázslat, csak az `Accept-Language` fejlécet használja. Ha nincs egyezés, `null`-t ad vissza. - -```php -// a böngésző pl. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 küld - -$langs = ['hu', 'pl', 'en']; // az alkalmazás által támogatott nyelvek -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -A [api:Nette\Http\RequestFactory] osztály egy `Nette\Http\Request` példány létrehozására szolgál, amely az aktuális HTTP kérést reprezentálja. (Ha a Nette-tel dolgozik, a HTTP kérés objektumot a keretrendszer automatikusan létrehozza.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -A `fromGlobals()` metódus létrehozza a kérés objektumot az aktuális PHP globális változók (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` és `$_SERVER`) alapján. Az objektum létrehozásakor automatikusan megtisztítja az összes GET, POST, COOKIE bemeneti paramétert, valamint az URL-t a vezérlőkarakterektől és az érvénytelen UTF-8 szekvenciáktól, ami biztosítja a biztonságot ezen adatok további feldolgozása során. - -A RequestFactory konfigurálható a `fromGlobals()` meghívása előtt: - -- a `$factory->setBinary()` metódussal kikapcsolhatja a bemeneti paraméterek automatikus tisztítását a vezérlőkarakterektől és az érvénytelen UTF-8 szekvenciáktól. -- a `$factory->setProxy(...)` metódussal megadhatja a [proxy szerver |configuration#HTTP proxy] IP címét, ami szükséges a felhasználó IP címének helyes észleléséhez. - -A RequestFactory lehetővé teszi szűrők definiálását, amelyek automatikusan átalakítják a kérés URL-jének részeit. Ezek a szűrők eltávolítják a nem kívánt karaktereket az URL-ből, amelyeket például a különböző webhelyeken lévő kommentrendszerek helytelen implementációja miatt helyezhettek oda: - -```php -// szóközök eltávolítása az útvonalból -$requestFactory->urlFilters['path']['%20'] = ''; - -// pont, vessző vagy jobb zárójel eltávolítása az URI végéről -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// az útvonal tisztítása a dupla perjelektől (alapértelmezett szűrő) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Az első kulcs, a `'path'` vagy `'url'`, meghatározza, hogy a szűrő az URL melyik részére vonatkozik. A második kulcs a keresendő reguláris kifejezés, az érték pedig a helyettesítés, amelyet a talált szöveg helyett használnak. - - -Feltöltött fájlok -================= - -A `Nette\Http\Request::getFiles()` metódus visszaadja az összes feltöltés tömbjét normalizált struktúrában, amelynek levelei [api:Nette\Http\FileUpload] objektumok. Ezek az `<input type=file>` űrlap elem által küldött adatokat zárják magukba. - -A struktúra tükrözi az elemek elnevezését a HTML-ben. A legegyszerűbb esetben ez egyetlen elnevezett űrlap elem lehet, amelyet így küldtek: - -```latte -<input type="file" name="avatar"> -``` - -Ebben az esetben a `$request->getFiles()` a következő tömböt adja vissza: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -A `FileUpload` objektum akkor is létrejön, ha a felhasználó nem küldött fájlt, vagy a küldés sikertelen volt. Azt, hogy a fájl elküldésre került-e, a `hasFile()` metódus adja vissza: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -Ha az elem neve tömb jelölést használ: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -a visszaadott fa így néz ki: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Létrehozhatunk fájlok tömbjét is: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -Ebben az esetben a struktúra így néz ki: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -A beágyazott tömb 1-es indexéhez való hozzáférés a legjobb módja a következő: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Mivel nem lehet megbízni a kívülről érkező adatokban, és így a fájlok struktúrájának formájában sem, ez a módszer biztonságosabb, mint például a `$request->getFiles()['my-form']['details']['avatars'][1]`, amely meghiúsulhat. - - -A `FileUpload` metódusainak áttekintése .{toc: FileUpload} ----------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Visszaadja a `true` értéket, ha a felhasználó feltöltött valamilyen fájlt. - - -isOk(): bool .[method] ----------------------- -Visszaadja a `true` értéket, ha a fájl sikeresen feltöltésre került. - - -getError(): int .[method] -------------------------- -Visszaadja a fájlfeltöltés hibakódját. Ez az egyik [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php] konstans. Ha a feltöltés rendben lezajlott, `UPLOAD_ERR_OK`-t ad vissza. - - -move(string $dest) .[method] ----------------------------- -Áthelyezi a feltöltött fájlt egy új helyre. Ha a célfájl már létezik, felülíródik. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Visszaadja a feltöltött fájl tartalmát. Ha a feltöltés sikertelen volt, `null`-t ad vissza. - - -getContentType(): ?string .[method] ------------------------------------ -Észleli a feltöltött fájl MIME content type-ját az aláírása alapján. Ha a feltöltés sikertelen volt, vagy az észlelés nem sikerült, `null`-t ad vissza. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getUntrustedName(): string .[method] ------------------------------------- -Visszaadja a fájl eredeti nevét, ahogy a böngésző küldte. - -.[caution] -Ne bízzon a metódus által visszaadott értékben. A kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy károsítsa vagy feltörje az alkalmazását. - - -getSanitizedName(): string .[method] ------------------------------------- -Visszaadja a szanitizált fájlnevet. Csak ASCII karaktereket `[a-zA-Z0-9.-]` tartalmaz. Ha a név nem tartalmaz ilyen karaktereket, `'unknown'`-t ad vissza. Ha a fájl JPEG, PNG, GIF, WebP vagy AVIF formátumú kép, akkor a helyes kiterjesztést is visszaadja. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Visszaadja a fájl megfelelő kiterjesztését (pont nélkül), amely megfelel az észlelt MIME típusnak. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Visszaadja a fájl eredeti elérési útját, ahogy a böngésző küldte a mappa feltöltésekor. A teljes elérési út csak PHP 8.1 és újabb verziókban érhető el. Korábbi verziókban ez a metódus az eredeti fájlnevet adja vissza. - -.[caution] -Ne bízzon a metódus által visszaadott értékben. A kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy károsítsa vagy feltörje az alkalmazását. - - -getSize(): int .[method] ------------------------- -Visszaadja a feltöltött fájl méretét. Ha a feltöltés sikertelen volt, `0`-t ad vissza. - - -getTemporaryFile(): string .[method] ------------------------------------- -Visszaadja a feltöltött fájl ideiglenes helyének elérési útját. Ha a feltöltés sikertelen volt, `''`-t ad vissza. - - -isImage(): bool .[method] -------------------------- -Visszaadja a `true` értéket, ha a feltöltött fájl JPEG, PNG, GIF, WebP vagy AVIF formátumú kép. Az észlelés az aláírása alapján történik, és nem ellenőrzi az egész fájl integritását. Azt, hogy a kép nem sérült-e, például a [betöltésével |#toImage] lehet megállapítani. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getImageSize(): ?array .[method] --------------------------------- -Visszaadja a `[szélesség, magasság]` párt a feltöltött kép méreteivel. Ha a feltöltés sikertelen volt, vagy nem érvényes képről van szó, `null`-t ad vissza. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Betölti a képet [Image|utils:images] objektumként. Ha a feltöltés sikertelen volt, vagy nem érvényes képről van szó, `Nette\Utils\ImageException` kivételt dob. diff --git a/http/hu/response.texy b/http/hu/response.texy deleted file mode 100644 index 96f9da71cb..0000000000 --- a/http/hu/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -HTTP válasz -*********** - -.[perex] -A Nette a HTTP választ érthető API-val rendelkező objektumokba zárja. - -A HTTP választ a [api:Nette\Http\Response] objektum képviseli. Ha a Nette-tel dolgozik, ezt az objektumot a keretrendszer automatikusan létrehozza, és [dependency injection |dependency-injection:passing-dependencies] segítségével átadhatja magának. A presenterekben elég csak a `$this->getHttpResponse()` metódust meghívni. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Nette\Http\Response -=================== - -Az objektum, ellentétben a [Nette\Http\Request|request]-tel, mutable (megváltoztatható), tehát setterek segítségével megváltoztathatja az állapotot, például fejléceket küldhet. Ne felejtse el, hogy minden settert **bármilyen kimenet elküldése előtt** kell meghívni. Azt, hogy a kimenet már elküldésre került-e, az `isSent()` metódus árulja el. Ha `true`-t ad vissza, minden fejléc küldési kísérlet `Nette\InvalidStateException` kivételt vált ki. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Megváltoztatja a [válasz állapotkódját |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. A forráskód jobb érthetősége érdekében javasoljuk, hogy a kódhoz számok helyett [előre definiált konstansokat |api:Nette\Http\IResponse] használjon. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Visszaadja a válasz állapotkódját. - - -isSent(): bool .[method] ------------------------- -Visszaadja, hogy a fejlécek már elküldésre kerültek-e a szerverről a böngészőbe, és így már nem lehet fejléceket küldeni vagy az állapotkódot megváltoztatni. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Elküld egy HTTP fejlécet és **felülírja** a korábban elküldött, azonos nevű fejlécet. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Elküld egy HTTP fejlécet és **nem írja felül** a korábban elküldött, azonos nevű fejlécet. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Törli a korábban elküldött HTTP fejlécet. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Visszaadja az elküldött HTTP fejlécet vagy `null`-t, ha ilyen nem létezik. A paraméter kis- és nagybetű érzéketlen. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Visszaadja az összes elküldött HTTP fejlécet asszociatív tömbként. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Megváltoztatja a `Content-Type` fejlécet. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Átirányít egy másik URL-re. Ne felejtse el utána leállítani a szkriptet. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Beállítja a HTTP dokumentum lejáratát a `Cache-Control` és `Expires` fejlécek segítségével. A paraméter vagy egy időintervallum (szövegként), vagy `null`, ami letiltja a gyorsítótárazást. - -```php -// a böngésző gyorsítótára egy óra múlva lejár -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -A választ a *Mentés másként* párbeszédablak segítségével tölti le a megadott néven. Magát a fájlt nem küldi el. - -```php -$httpResponse->sendAsFile('faktura.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Elküld egy cookie-t. A paraméterek alapértelmezett értékei: - -| `$path` | `'/'` | a cookie hatóköre az összes útvonalra kiterjed a (sub)domainen *(konfigurálható)* -| `$domain` | `null` | ami azt jelenti, hogy a hatókör az aktuális (sub)domainre terjed ki, de nem annak aldomainjeire *(konfigurálható)* -| `$secure` | `true` | ha a webhely HTTPS-en fut, egyébként `false` *(konfigurálható)* -| `$httpOnly` | `true` | a cookie JavaScript számára nem hozzáférhető -| `$sameSite` | `'Lax'` | a cookie nem feltétlenül kerül elküldésre [más domainről való hozzáférés |nette:glossary#SameSite cookie] esetén - -A `$path`, `$domain` és `$secure` paraméterek alapértelmezett értékeit megváltoztathatja a [konfigurációban |configuration#HTTP cookie]. - -Az időt megadhatja másodpercek számaként vagy stringként: - -```php -$httpResponse->setCookie('lang', 'cs', '100 days'); -``` - -A `$domain` paraméter meghatározza, mely domainek fogadhatják el a cookie-t. Ha nincs megadva, a cookie-t ugyanaz a (sub)domain fogadja el, amelyik beállította, de nem annak aldomainjei. Ha a `$domain` meg van adva, az aldomainek is beletartoznak. Ezért a `$domain` megadása kevésbé korlátozó, mint annak elhagyása. Például a `$domain = 'nette.org'` esetén a cookie-k minden aldomainen, mint például a `doc.nette.org`, is elérhetők. - -A `$sameSite` értékhez használhatja a `Response::SameSiteLax`, `Response::SameSiteStrict` és `Response::SameSiteNone` konstansokat. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Törli a cookie-t. A paraméterek alapértelmezett értékei: -- `$path` hatókörrel az összes könyvtárra (`'/'`) -- `$domain` hatókörrel az aktuális (sub)domainre, de nem annak aldomainjeire -- `$secure` a [konfigurációban |configuration#HTTP cookie] beállítottak szerint - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/hu/sessions.texy b/http/hu/sessions.texy deleted file mode 100644 index e6719c82d3..0000000000 --- a/http/hu/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sessionök -********* - -<div class=perex> - -A HTTP egy állapot nélküli protokoll, azonban szinte minden alkalmazásnak szüksége van az állapot megőrzésére a kérések között, például a bevásárlókosár tartalmának megőrzésére. Pontosan erre szolgál a session vagy munkamenet. Megmutatjuk, - -- hogyan használjuk a sessionöket -- hogyan kerüljük el a névütközéseket -- hogyan állítsuk be a lejárati időt - -</div> - -Sessionök használatakor minden felhasználó egyedi azonosítót kap, az úgynevezett session ID-t, amelyet cookie-ban továbbítanak. Ez kulcsként szolgál a session adatokhoz. Ellentétben a cookie-kkal, amelyek a böngésző oldalán tárolódnak, a session adatok a szerver oldalán tárolódnak. - -A sessiont a [konfigurációban |configuration#Session] állítjuk be, különösen fontos a lejárati idő megválasztása. - -A session kezeléséért a [api:Nette\Http\Session] objektum felelős, amelyhez úgy juthat hozzá, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével átadja magának. A presenterekben elég csak a `$session = $this->getSession()` metódust meghívni. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Session indítása -================ - -A Nette alapértelmezés szerint automatikusan elindítja a sessiont abban a pillanatban, amikor elkezdünk olvasni belőle vagy adatokat írni bele. Manuálisan a session a `$session->start()` segítségével indítható el. - -A PHP a session indításakor HTTP fejléceket küld, amelyek befolyásolják a gyorsítótárazást, lásd [php:session_cache_limiter], és adott esetben a session ID-t tartalmazó cookie-t is. Ezért mindig el kell indítani a sessiont még azelőtt, hogy bármilyen kimenetet küldenénk a böngészőbe, különben kivétel váltódik ki. Ha tehát tudja, hogy az oldal megjelenítése során sessiont fog használni, indítsa el manuálisan előtte, például a presenterben. - -Fejlesztői módban a Tracy indítja el a sessiont, mert azt használja az átirányítási és AJAX kérések sávjainak megjelenítésére a Tracy Barban. - - -Szekciók -======== - -Tiszta PHP-ban a session adattárolója egy tömbként valósul meg, amely a `$_SESSION` globális változón keresztül érhető el. A probléma az, hogy az alkalmazások általában számos egymástól független részből állnak, és ha mindegyiknek csak egy tömb áll rendelkezésére, előbb-utóbb névütközés következik be. - -A Nette Framework ezt a problémát úgy oldja meg, hogy az egész teret szekciókra ( [api:Nette\Http\SessionSection] objektumokra) osztja. Minden egység ezután saját, egyedi nevű szekciót használ, és így már nem fordulhat elő ütközés. - -A szekciót a sessionből kapjuk meg: - -```php -$section = $session->getSection('unikatni nazev'); -``` - -A presenterben elég a `getSession()`-t használni paraméterrel: - -```php -// $this egy Presenter -$section = $this->getSession('unikatni nazev'); -``` - -A szekció létezését a `$session->hasSection('unikatni nazev')` metódussal ellenőrizhetjük. - -Magával a szekcióval ezután nagyon egyszerűen dolgozhatunk a `set()`, `get()` és `remove()` metódusokkal: - -```php -// változó írása -$section->set('userName', 'franta'); - -// változó olvasása, null-t ad vissza, ha nem létezik -echo $section->get('userName'); - -// változó törlése -$section->remove('userName'); -``` - -Az összes változó megszerzéséhez a szekcióból használhatjuk a `foreach` ciklust: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Lejárati idő beállítása ------------------------ - -Az egyes szekciókhoz vagy akár egyes változókhoz is beállítható lejárati idő. Így például hagyhatjuk, hogy a felhasználó bejelentkezése 20 perc múlva lejárjon, miközben továbbra is megjegyezzük a kosár tartalmát. - -```php -// a szekció 20 perc múlva lejár -$section->setExpiration('20 minutes'); -``` - -Az egyes változók lejárati idejének beállítására a `set()` metódus harmadik paramétere szolgál: - -```php -// a 'flash' változó már 30 másodperc múlva lejár -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Ne felejtse el, hogy az egész session lejárati ideje (lásd [session konfiguráció |configuration#Session]) meg kell hogy egyezzen vagy magasabb legyen, mint az egyes szekciókhoz vagy változókhoz beállított idő. - -A korábban beállított lejárati idő törlését a `removeExpiration()` metódussal érhetjük el. Az egész szekció azonnali törlését a `remove()` metódus biztosítja. - - -$onStart, $onBeforeWrite események ----------------------------------- - -A `Nette\Http\Session` objektumnak vannak [$onStart és $onBeforeWrite eseményei |nette:glossary#Eventek események], így hozzáadhat callbackeket, amelyek a session indítása után vagy a lemezre írása és az azt követő befejezése előtt hívódnak meg. - -```php -$session->onBeforeWrite[] = function () { - // adatokat írunk a sessionbe - $this->section->set('basket', $this->basket); -}; -``` - - -Session kezelés -=============== - -A `Nette\Http\Session` osztály metódusainak áttekintése a session kezeléséhez: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Elindítja a sessiont. - - -isStarted(): bool .[method] ---------------------------- -El van indítva a session? - - -close(): void .[method] ------------------------ -Befejezi a sessiont. A session automatikusan befejeződik a szkript futásának végén. - - -destroy(): void .[method] -------------------------- -Befejezi és törli a sessiont. - - -exists(): bool .[method] ------------------------- -Tartalmaz a HTTP kérés cookie-t session ID-vel? - - -regenerateId(): void .[method] ------------------------------- -Új, véletlenszerű session ID-t generál. Az adatok megmaradnak. - - -getId(): string .[method] -------------------------- -Visszaadja a session ID-t. - -</div> - - -Konfiguráció ------------- - -A sessiont a [konfigurációban |configuration#Session] állítjuk be. Ha olyan alkalmazást ír, amely nem használ DI konténert, ezek a metódusok szolgálnak a konfigurációhoz. Ezeket még a session elindítása előtt kell meghívni. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Beállítja annak a cookie-nak a nevét, amelyben a session ID-t továbbítják. A standard név `PHPSESSID`. Hasznos abban az esetben, ha egy webhelyen belül több különböző alkalmazást üzemeltet. - - -getName(): string .[method] ---------------------------- -Visszaadja annak a cookie-nak a nevét, amelyben a session ID-t továbbítják. - - -setOptions(array $options): static .[method] --------------------------------------------- -Konfigurálja a sessiont. Beállíthatók az összes PHP [session direktíva |https://www.php.net/manual/en/session.configuration.php] (camelCase formátumban, pl. `session.save_path` helyett `savePath`-t írunk) és a [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] is. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Beállítja az inaktivitási időt, amely után a session lejár. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Cookie paraméterek beállítása. A paraméterek alapértelmezett értékeit megváltoztathatja a [konfigurációban |configuration#Session cookie]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Beállítja a könyvtárat, ahová a session fájlok mentésre kerülnek. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Saját handler beállítása, lásd [PHP dokumentáció|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Biztonság mindenekelőtt -======================= - -A szerver feltételezi, hogy ugyanazzal a felhasználóval kommunikál, amíg a kéréseket ugyanaz a session ID kíséri. A biztonsági mechanizmusok feladata annak biztosítása, hogy ez valóban így legyen, és ne lehessen az azonosítót ellopni vagy meghamisítani. - -A Nette Framework ezért helyesen konfigurálja a PHP direktívákat, hogy a session ID-t csak cookie-ban továbbítsa, JavaScript számára hozzáférhetetlenné tegye, és az URL-ben lévő esetleges azonosítókat figyelmen kívül hagyja. Ezenkívül kritikus pillanatokban, mint például a felhasználó bejelentkezésekor, új session ID-t generál. - -.[note] -A PHP konfigurálásához az ini_set függvényt használják, amelyet sajnos néhány hosting szolgáltató letilt. Ha ez az Ön szolgáltatójának esete is, próbáljon meg velük megegyezni, hogy engedélyezzék a függvényt, vagy legalább konfigurálják a szervert. diff --git a/http/hu/urls.texy b/http/hu/urls.texy deleted file mode 100644 index d53b089012..0000000000 --- a/http/hu/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -URL-ekkel való munka -******************** - -.[perex] -Az [#Url], [#UrlImmutable] és [#UrlScript] osztályok lehetővé teszik az URL-ek egyszerű generálását, elemzését és manipulálását. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Url -=== - -A [api:Nette\Http\Url] osztály lehetővé teszi az URL-ekkel és azok egyes komponenseivel való egyszerű munkát, amelyeket ez a rajz ábrázol: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Az URL generálása intuitív: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Lehetőség van az URL elemzésére és további manipulálására is: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -A `Url` osztály implementálja a `JsonSerializable` interfészt, és rendelkezik a `__toString()` metódussal, így az objektumot ki lehet írni, vagy fel lehet használni a `json_encode()`-nak átadott adatokban. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL komponensek .[method] -------------------------- - -Az URL egyes komponenseinek visszaadására vagy megváltoztatására a következő metódusok állnak rendelkezésre: - -.[language-php] -| Setter | Getter | Visszaadott érték -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | teljes URL - -Figyelmeztetés: Amikor olyan URL-lel dolgozik, amelyet a [HTTP kérésből|request] szereztek be, vegye figyelembe, hogy nem fogja tartalmazni a fragmentet, mivel a böngésző nem küldi el azt a szerverre. - -Az egyes query paraméterekkel is dolgozhatunk a következők segítségével: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Visszaadja a hoszt jobb vagy bal részét. Így működik, ha a hoszt `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Ellenőrzi, hogy két URL megegyezik-e. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Ellenőrzi, hogy az URL abszolút-e. Az URL abszolútnak tekintendő, ha sémával kezdődik (pl. http, https, ftp), amelyet kettőspont követ. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normalizálja az URL elérési útját a speciális `.` és `..` szegmensek eltávolításával. A metódus eltávolítja a felesleges elérési út elemeket ugyanúgy, ahogy a webböngészők teszik. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -A [api:Nette\Http\UrlImmutable] osztály az [#Url] osztály immutable (megváltoztathatatlan) alternatívája (hasonlóan ahhoz, ahogy a PHP-ban a `DateTimeImmutable` a `DateTime` megváltoztathatatlan alternatívája). Setterek helyett ún. withereket használ, amelyek nem változtatják meg az objektumot, hanem új példányokat adnak vissza módosított értékkel: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -A `UrlImmutable` osztály implementálja a `JsonSerializable` interfészt, és rendelkezik a `__toString()` metódussal, így az objektumot ki lehet írni, vagy fel lehet használni a `json_encode()`-nak átadott adatokban. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL komponensek .[method] -------------------------- - -Az URL egyes komponenseinek visszaadására vagy megváltoztatására a következő metódusok szolgálnak: - -.[language-php] -| Wither | Getter | Visszaadott érték -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | teljes URL - -A `withoutUserInfo()` metódus eltávolítja a `user`-t és a `password`-öt. - -Az egyes query paraméterekkel is dolgozhatunk a következők segítségével: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Visszaadja a hoszt jobb vagy bal részét. Így működik, ha a hoszt `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Abszolút URL-t vezet le ugyanúgy, ahogy a böngésző feldolgozza a HTML oldalon lévő linkeket: -- ha a link abszolút URL (sémát tartalmaz), változatlanul használja -- ha a link `//`-vel kezdődik, csak a sémát veszi át az aktuális URL-ből -- ha a link `/`-vel kezdődik, abszolút elérési utat hoz létre a domain gyökerétől -- egyéb esetekben az URL-t relatívan állítja össze az aktuális elérési úthoz képest - -```php -$url = new UrlImmutable('https://example.com/path/page'); -echo $url->resolve('../foo'); // 'https://example.com/foo' -echo $url->resolve('/bar'); // 'https://example.com/bar' -echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Ellenőrzi, hogy két URL megegyezik-e. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -A [api:Nette\Http\UrlScript] osztály az [#UrlImmutable] leszármazottja, és további virtuális URL komponensekkel bővíti ki, mint például a projekt gyökérkönyvtára stb. Ugyanúgy, mint a szülő osztálya, immutable (megváltoztathatatlan) objektum. - -A következő diagram azokat a komponenseket ábrázolja, amelyeket az UrlScript felismer: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` az alkalmazás alap URL címe, beleértve a domaint és az alkalmazás gyökérkönyvtárához vezető útvonalrészt -- `basePath` az alkalmazás gyökérkönyvtárához vezető útvonalrész -- `scriptPath` az aktuális szkripthez vezető útvonal -- `relativePath` a szkript neve (esetleg további útvonalszegmensek) a basePath-hoz képest relatívan -- `relativeUrl` az URL teljes része a baseUrl után, beleértve a query stringet és a fragmentet. -- `pathInfo` ma már ritkán használt URL rész a szkript neve után - -Az URL részeinek visszaadására a következő metódusok állnak rendelkezésre: - -.[language-php] -| Getter | Visszaadott érték -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Az `UrlScript` objektumokat általában nem közvetlenül hozzuk létre, hanem a [Nette\Http\Request::getUrl()|request] metódus adja vissza őket már helyesen beállított komponensekkel az aktuális HTTP kéréshez. diff --git a/http/it/@home.texy b/http/it/@home.texy index 6435ce6d19..6440b6b164 100644 --- a/http/it/@home.texy +++ b/http/it/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -Il pacchetto `nette/http` incapsula la [HTTP request|request] & [response], il lavoro con le [sessions] e il [parsing e la composizione degli URL |urls]. +Il pacchetto `nette/http` è il vostro compagno per tutta la comunicazione HTTP. Offre un'API a oggetti chiara sulla richiesta in ingresso e sulla risposta in uscita, semplifica il lavoro con le sessioni e con gli indirizzi URL e per di più si occupa della sicurezza. Ecco cosa ci trovate: + +| [Richiesta HTTP |request] | richiesta in ingresso e sanificazione degli input +| [Risposta HTTP |response] | risposta in uscita, header e cookie +| [Sessioni|sessions] | conservazione sicura dello stato tra le richieste +| [Strumenti per gli URL |urls] | analisi e costruzione degli indirizzi URL +| [Protezione SSRF |ssrf] | difesa contro gli attacchi Server-Side Request Forgery +| [Configurazione|configuration] | opzioni di configurazione del pacchetto Installazione ------------- -È possibile scaricare e installare la libreria utilizzando lo strumento [Composer|best-practices:composer]: +Il pacchetto si scarica e si installa con [Composer|best-practices:composer]: ```shell composer require nette/http ``` + +Il pacchetto richiede PHP dalla versione 8.3 alla 8.5. diff --git a/http/it/@left-menu.texy b/http/it/@left-menu.texy index 38e13b9ee6..e651900927 100644 --- a/http/it/@left-menu.texy +++ b/http/it/@left-menu.texy @@ -1,8 +1,19 @@ Nette HTTP ********** -- [Introduzione |@home] -- [HTTP request|request] -- [HTTP response|response] -- [Sessioni |sessions] -- [Utilità URL |urls] -- [Configurazione |configuration] +- [Panoramica |@home] +- [Richiesta HTTP |request] +- [Risposta HTTP |response] +- [Sessioni|sessions] +- [Strumenti per gli URL |urls] +- [Protezione SSRF |ssrf] +- [Configurazione|configuration] +- [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/http/it/configuration.texy b/http/it/configuration.texy index eba6f97a55..baf1a0a3fe 100644 --- a/http/it/configuration.texy +++ b/http/it/configuration.texy @@ -2,9 +2,9 @@ Configurazione HTTP ******************* .[perex] -Panoramica delle opzioni di configurazione per Nette HTTP. +Panoramica delle opzioni di configurazione di Nette HTTP. -Se non utilizzate l'intero framework, ma solo questa libreria, leggete [come caricare la configurazione|bootstrap:]. +Se non usate tutto il framework, ma solo questa libreria, leggete [come caricare la configurazione|bootstrap:]. Header HTTP @@ -12,29 +12,31 @@ Header HTTP ```neon http: - # header che vengono inviati con ogni request + # header inviati con ogni risposta headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' # influisce sull'header X-Frame-Options - frames: ... # (string|bool) il default è 'SAMEORIGIN' + frames: ... # (string|bool|null) predefinito 'SAMEORIGIN' ``` -Per motivi di sicurezza, il framework invia l'header `X-Frame-Options: SAMEORIGIN`, che indica che la pagina può essere visualizzata all'interno di un'altra pagina (nell'elemento `<iframe>`) solo se si trova sullo stesso dominio. Questo può essere indesiderato in alcune situazioni (ad esempio, se state sviluppando un'applicazione per Facebook), quindi il comportamento può essere modificato impostando `frames: http://allowed-host.com` o `frames: true`. +Per motivi di sicurezza il framework invia l'header `X-Frame-Options: SAMEORIGIN`, che dice che una pagina può essere mostrata dentro un'altra pagina (in un elemento `<iframe>`) solo se si trova sullo stesso dominio. In alcune situazioni questo può essere indesiderato (per esempio se state sviluppando un'applicazione per Facebook), quindi il comportamento si può cambiare impostando `frames: http://allowed-host.com` per consentire un host specifico, `frames: true` per consentire l'inserimento in frame da qualsiasi parte (l'header viene omesso) oppure `frames: false` per vietarlo del tutto (`X-Frame-Options: DENY`). + +Per impostazione predefinita Nette invia anche gli header `X-Powered-By: Nette Framework 3` e `Content-Type: text/html; charset=utf-8`. Potete rimuovere qualsiasi header, compresi questi predefiniti, impostandone il valore a una stringa vuota. Content Security Policy ----------------------- -È facile costruire gli header `Content-Security-Policy` (di seguito CSP), la cui descrizione si trova nella [descrizione CSP |https://content-security-policy.com]. Le direttive CSP (come `script-src`) possono essere scritte sia come stringhe secondo la specifica, sia come array di valori per una migliore leggibilità. In tal caso, non è necessario racchiudere tra virgolette le parole chiave come `'self'`. Nette genera anche automaticamente il valore `nonce`, quindi nell'header ci sarà ad esempio `'nonce-y4PopTLM=='`. +Gli header `Content-Security-Policy` (CSP) si configurano facilmente; la loro descrizione la trovate nella [specifica CSP |https://content-security-policy.com]. Le direttive CSP (per esempio `script-src`) si possono scrivere come stringhe secondo la specifica oppure come array di valori, per una migliore leggibilità. In tal caso non serve mettere le virgolette attorno a parole chiave come `'self'`. Nette genererà automaticamente anche il valore `nonce`, quindi nell'header verrà inviato per esempio `'nonce-y4PopTLM=='`. ```neon http: # Content Security Policy csp: - # stringa nel formato secondo la specifica CSP + # stringa secondo la specifica CSP default-src: "'self' https://example.com" # array di valori @@ -44,14 +46,14 @@ http: - self - https://example.com - # bool nel caso di switch + # bool nel caso degli interruttori upgrade-insecure-requests: true block-all-mixed-content: false ``` -Nei template, usate `<script n:nonce>...</script>` e il valore nonce verrà aggiunto automaticamente. Creare siti web sicuri in Nette è davvero facile. +Nei template usate `<script n:nonce>...</script>` e il valore nonce verrà inserito automaticamente. Fare siti web sicuri in Nette è davvero facile. -Allo stesso modo, è possibile costruire gli header `Content-Security-Policy-Report-Only` (che possono essere utilizzati contemporaneamente a CSP) e [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: +Allo stesso modo si configurano gli header `Content-Security-Policy-Report-Only` (che si possono usare insieme alla CSP) e la [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: ```neon http: @@ -72,39 +74,51 @@ http: Cookie HTTP ----------- -È possibile modificare i valori predefiniti di alcuni parametri del metodo [Nette\Http\Response::setCookie() |response#setCookie] e della sessione. +Potete cambiare i valori predefiniti di alcuni parametri del metodo [Nette\Http\Response::setCookie() |response#setCookie()] e della gestione delle sessioni. ```neon http: - # scope del cookie per percorso - cookiePath: ... # (string) il default è '/' + # ambito del cookie secondo il percorso + cookiePath: ... # (string) predefinito '/' - # domini che accettano i cookie - cookieDomain: 'example.com' # (string|domain) il default è non impostato + # domini che possono ricevere il cookie + cookieDomain: 'example.com' # (string|domain) predefinito non impostato # inviare i cookie solo tramite HTTPS? - cookieSecure: ... # (bool|auto) il default è auto + cookieSecure: ... # (bool|auto) predefinito auto - # disabilita l'invio del cookie usato da Nette per la protezione CSRF - disableNetteCookie: ... # (bool) il default è false + # disattiva l'invio del cookie che Nette usa per la protezione CSRF + disableNetteCookie: ... # (bool) predefinito false ``` -L'attributo `cookieDomain` specifica quali domini possono accettare il cookie. Se non specificato, il cookie viene accettato dallo stesso (sotto)dominio che lo ha impostato, *ma non* dai suoi sottodomini. Se `cookieDomain` è specificato, vengono inclusi anche i sottodomini. Pertanto, specificare `cookieDomain` è meno restrittivo che ometterlo. +L'attributo `cookieDomain` determina quali domini (origini) possono accettare i cookie. Se non è indicato, il cookie viene accettato dallo stesso (sotto)dominio che lo ha impostato, *escluse* le sue sottodomini. Se `cookieDomain` è indicato, sono compresi anche i sottodomini. Indicare `cookieDomain` è quindi meno restrittivo che ometterlo. -Ad esempio, con `cookieDomain: nette.org`, i cookie sono disponibili anche su tutti i sottodomini come `doc.nette.org`. Lo stesso si può ottenere anche con il valore speciale `domain`, cioè `cookieDomain: domain`. +Se per esempio è impostato `cookieDomain: nette.org`, i cookie sono disponibili anche su tutti i sottodomini come `doc.nette.org`. Lo stesso si ottiene con il valore speciale `domain`, cioè `cookieDomain: domain`. -Il valore predefinito `auto` per l'attributo `cookieSecure` significa che se il sito web viene eseguito su HTTPS, i cookie verranno inviati con il flag `Secure` e quindi saranno disponibili solo tramite HTTPS. +Il valore predefinito `auto` dell'attributo `cookieSecure` significa che, se il sito gira su HTTPS, i cookie verranno inviati con il flag `Secure` e saranno quindi disponibili solo tramite HTTPS. Proxy HTTP ---------- -Se il sito web viene eseguito dietro un proxy HTTP, specificare il suo indirizzo IP affinché il rilevamento della connessione tramite HTTPS e l'indirizzo IP del client funzionino correttamente. Cioè, affinché le funzioni [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] e [isSecured() |request#isSecured] restituiscano i valori corretti e nei template vengano generati link con il protocollo `https:`. +Se il sito gira dietro a un proxy HTTP, indicate l'indirizzo IP del proxy perché il rilevamento della connessione HTTPS e l'indirizzo IP del client funzionino correttamente. Cioè perché [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()] e [isSecured() |request#isSecured()] restituiscano i valori corretti e nei template i link vengano generati con il protocollo `https:`. + +```neon +http: + # indirizzo IP, intervallo (per esempio 127.0.0.1/8) oppure un array di questi valori + proxy: 127.0.0.1 # (string|string[]) predefinito non impostato +``` + + +Forzare HTTPS .{data-version:3.3.4} +----------------------------------- + +Forza incondizionatamente lo schema della richiesta a HTTPS. Torna utile per i siti solo HTTPS che girano dietro a un load balancer o a un reverse proxy che termina il TLS ma non passa l'header `X-Forwarded-Proto`, così che il consueto rilevamento di HTTPS (anche con il [#Proxy HTTP] configurato) non lo intercetterebbe. ```neon http: - # Indirizzo IP, intervallo (es. 127.0.0.1/8) o array di questi valori - proxy: 127.0.0.1 # (string|string[]) il default è non impostato + # forza lo schema HTTPS per tutte le richieste + forceHttps: true # (bool) predefinito false ``` @@ -115,57 +129,58 @@ Impostazioni di base delle [sessioni |sessions]: ```neon session: - # visualizzare il pannello della sessione nella Tracy Bar? - debugger: ... # (bool) il default è false + # mostrare il pannello della sessione nella Tracy Bar? + debugger: ... # (bool) predefinito false - # periodo di inattività dopo il quale la sessione scade - expiration: 14 days # (string) il default è '3 hours' + # tempo di inattività dopo il quale la sessione scade + expiration: 14 days # (string) predefinito '3 hours' - # quando deve iniziare la sessione? - autoStart: ... # (smart|always|never) il default è 'smart' + # quando deve avviarsi la sessione? + autoStart: ... # (smart|always|never) predefinito 'smart' - # handler, servizio che implementa l'interfaccia SessionHandlerInterface + # handler, un servizio che implementa SessionHandlerInterface handler: @handlerService ``` -L'opzione `autoStart` controlla quando deve iniziare la sessione. Il valore `always` significa che la sessione si avvierà sempre all'avvio dell'applicazione. Il valore `smart` significa che la sessione si avvierà all'avvio dell'applicazione solo se esiste già, o nel momento in cui vogliamo leggere o scrivere dati in essa. Infine, il valore `never` disabilita l'avvio automatico della sessione. +L'opzione `autoStart` governa quando la sessione deve avviarsi. Il valore `always` significa che la sessione si avvia sempre all'avvio dell'applicazione. Il valore `smart` significa che la sessione si avvia con l'applicazione solo se esiste già, oppure nel momento in cui vogliamo leggerla o scriverla. Infine il valore `never` disattiva l'avvio automatico della sessione. -Inoltre, è possibile impostare tutte le [direttive di sessione |https://www.php.net/manual/en/session.configuration.php] PHP (in formato camelCase) e anche [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Esempio: +Potete inoltre impostare tutte le [direttive di sessione |https://www.php.net/manual/en/session.configuration.php] di PHP (in formato camelCase) e anche [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Esempio: ```neon session: - # 'session.name' lo scriviamo come 'name' + # 'session.name' si scrive come 'name' name: MYID - # 'session.save_path' lo scriviamo come 'savePath' + # 'session.save_path' si scrive come 'savePath' savePath: "%tempDir%/sessions" ``` -Cookie di Sessione +Cookie di sessione ------------------ -Il cookie di sessione viene inviato con gli stessi parametri degli [altri cookie |#Cookie HTTP], ma è possibile modificarli per esso: +Il cookie di sessione viene inviato con gli stessi parametri degli [altri cookie |#Cookie HTTP], ma potete cambiarli specificamente per esso: ```neon session: - # domini che accettano i cookie + # domini che possono ricevere il cookie cookieDomain: 'example.com' # (string|domain) - # restrizione sull'accesso da un altro dominio - cookieSamesite: None # (Strict|Lax|None) il default è Lax + # limitazione per l'accesso cross-origin + cookieSamesite: None # (Strict|Lax|None) predefinito Lax ``` -L'attributo `cookieSamesite` influisce sul fatto che il cookie venga inviato durante l'[accesso da un altro dominio |nette:glossary#Cookie SameSite], il che fornisce una certa protezione contro gli attacchi [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). +L'attributo `cookieSamesite` influisce sul fatto che il cookie venga inviato con le [richieste cross-origin |nette:glossary#Cookie SameSite], il che offre una certa protezione contro gli attacchi [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Servizi DI ========== -Questi servizi vengono aggiunti al container DI: +Al container DI vengono aggiunti questi servizi: | Nome | Tipo | Descrizione -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP request| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP response| response] +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [richiesta HTTP| request] +| `http.response` | [api:Nette\Http\Response] | [risposta HTTP| response] | `session.session`| [api:Nette\Http\Session] | [gestione delle sessioni| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | factory che crea la richiesta HTTP diff --git a/http/it/request.texy b/http/it/request.texy index c5f31907f7..26b3cad5da 100644 --- a/http/it/request.texy +++ b/http/it/request.texy @@ -1,12 +1,12 @@ -HTTP Request -************ +Richiesta HTTP +************** .[perex] -Nette incapsula la richiesta HTTP in oggetti con un'API comprensibile e fornisce allo stesso tempo un filtro di sanificazione. +Nette incapsula la richiesta HTTP in oggetti con un'API chiara e offre allo stesso tempo un filtro di sanificazione. -La richiesta HTTP è rappresentata dall'oggetto [api:Nette\Http\Request]. Se lavorate con Nette, questo oggetto viene creato automaticamente dal framework e potete riceverlo tramite [dependency injection |dependency-injection:passing-dependencies]. Nei presenter, basta chiamare il metodo `$this->getHttpRequest()`. Se lavorate al di fuori del Nette Framework, potete creare l'oggetto usando [#RequestFactory]. +La richiesta HTTP è rappresentata dall'oggetto [api:Nette\Http\Request]. Se lavorate con Nette, questo oggetto viene creato automaticamente dal framework e potete farvelo passare con la [dependency injection |dependency-injection:passing-dependencies]. Nei presenter basta chiamare il metodo `$this->getHttpRequest()`. Se lavorate fuori dal Nette Framework, potete creare l'oggetto con [#RequestFactory]. -Un grande vantaggio di Nette è che durante la creazione dell'oggetto, pulisce automaticamente tutti i parametri di input GET, POST, COOKIE e anche l'URL dai caratteri di controllo e dalle sequenze UTF-8 non valide. Potete quindi lavorare in sicurezza con questi dati. I dati puliti vengono successivamente utilizzati nei presenter e nei form. +Un grande vantaggio di Nette è che, quando crea l'oggetto, sanifica automaticamente tutti i parametri in ingresso (GET, POST, COOKIE) e anche l'URL, rimuovendo i caratteri di controllo e le sequenze UTF-8 non valide. Con questi dati potete poi lavorare in sicurezza. I dati sanificati vengono usati in seguito nei presenter e nei form. → [Installazione e requisiti |@home#Installazione] @@ -14,7 +14,7 @@ Un grande vantaggio di Nette è che durante la creazione dell'oggetto, pulisce a Nette\Http\Request ================== -Questo oggetto è immutabile. Non ha setter, ha solo un cosiddetto wither `withUrl()`, che non modifica l'oggetto, ma restituisce una nuova istanza con il valore modificato. +Questo oggetto è immutabile. Non ha setter, ha un solo cosiddetto wither, `withUrl()`, che non modifica l'oggetto ma restituisce una nuova istanza con il valore modificato. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] @@ -28,7 +28,7 @@ Restituisce l'URL della richiesta come oggetto [UrlScript |urls#UrlScript]. ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` @@ -37,27 +37,27 @@ Attenzione: i browser non inviano il frammento al server, quindi `$url->getFragm getQuery(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Restituisce i parametri GET della richiesta. +Restituisce i parametri della richiesta GET. ```php -$all = $httpRequest->getQuery(); // restituisce un array di tutti i parametri dall'URL -$id = $httpRequest->getQuery('id'); // restituisce il parametro GET 'id' (o null) +$all = $httpRequest->getQuery(); // array di tutti i parametri dell'URL +$id = $httpRequest->getQuery('id'); // restituisce il parametro GET 'id' (oppure null) ``` getPost(?string $key=null): string|array|null .[method] ------------------------------------------------------- -Restituisce i parametri POST della richiesta. +Restituisce i parametri della richiesta POST. ```php -$all = $httpRequest->getPost(); // restituisce un array di tutti i parametri da POST -$id = $httpRequest->getPost('id'); // restituisce il parametro POST 'id' (o null) +$all = $httpRequest->getPost(); // array di tutti i parametri POST +$id = $httpRequest->getPost('id'); // restituisce il parametro POST 'id' (oppure null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Restituisce l'[upload |#File Caricati] come oggetto [api:Nette\Http\FileUpload]: +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- +Restituisce un [upload |#File caricati] come oggetto [api:Nette\Http\FileUpload]: ```php $file = $httpRequest->getFile('avatar'); @@ -67,28 +67,28 @@ if ($file?->hasFile()) { // è stato caricato qualche file? } ``` -Per accedere alla struttura nidificata, specificare un array di chiavi. +Per accedere a una struttura annidata indicate un array di chiavi. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Poiché non ci si può fidare dei dati provenienti dall'esterno e quindi nemmeno fare affidamento sulla forma della struttura dei file, questo metodo è più sicuro rispetto, ad esempio, a `$request->getFiles()['my-form']['details']['avatar']`, che potrebbe fallire. +Poiché non potete fidarvi dei dati esterni e quindi contare sulla struttura dei file, questo approccio è più sicuro di per esempio `$request->getFiles()['my-form']['details']['avatar']`, che potrebbe fallire. getFiles(): array .[method] --------------------------- -Restituisce l'albero di [tutti gli upload |#File Caricati] in una struttura normalizzata, le cui foglie sono oggetti [api:Nette\Http\FileUpload]: +Restituisce un albero di [tutti gli upload |#File caricati] in una struttura normalizzata, le cui foglie sono oggetti [api:Nette\Http\FileUpload]: ```php $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Restituisce un cookie o `null` se non esiste. +getCookie(string $key): ?string .[method] +----------------------------------------- +Restituisce un cookie oppure `null` se non esiste. ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -Restituisce il metodo HTTP con cui è stata effettuata la richiesta. +Restituisce il metodo HTTP con cui è stata fatta la richiesta. ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -Verifica il metodo HTTP con cui è stata effettuata la richiesta. Il parametro è case-insensitive. +Verifica il metodo HTTP con cui è stata fatta la richiesta. Il parametro non fa distinzione tra maiuscole e minuscole. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,51 +124,83 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -Restituisce un header HTTP o `null` se non esiste. Il parametro è case-insensitive. +Restituisce un header HTTP oppure `null` se non esiste. Il parametro non fa distinzione tra maiuscole e minuscole. ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Restituisce tutti gli header HTTP come array associativo. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Restituisce tutti gli header HTTP come array associativo. Le chiavi sono normalizzate in minuscolo. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -La connessione è crittografata (HTTPS)? Potrebbe essere necessario [impostare il proxy |configuration#Proxy HTTP] per un corretto funzionamento. +La connessione è cifrata (HTTPS)? Perché funzioni correttamente può servire [impostare il proxy |configuration#Proxy HTTP]. -isSameSite(): bool .[method] ----------------------------- -La richiesta proviene dallo stesso (sotto)dominio ed è stata avviata facendo clic su un link? Nette utilizza il cookie `_nss` (precedentemente `nette-samesite`) per il rilevamento. +isSameSite(): bool .[method deprecated] +--------------------------------------- +La richiesta proviene dallo stesso sito? Dalla versione 3.4 è sostituito dal più capace [isFrom() |#isFrom()]. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Vi dice da dove è arrivata la richiesta e in che modo il browser l'ha fatta, in base agli header `Sec-Fetch-*` (i cosiddetti [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]) che il browser imposta da sé e che una pagina in esecuzione nel browser della vittima non può né falsificare né rimuovere. Nette lo usa internamente per proteggere automaticamente form e segnali dal [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Torna utile quando volete proteggere vostre azioni sensibili, per esempio endpoint di API o link distruttivi. + +Il metodo restituisce `true` solo quando la richiesta soddisfa **tutte** le condizioni che indicate. Il primo parametro `$site` descrive la relazione tra la pagina che ha originato la richiesta e il vostro sito (l'header `Sec-Fetch-Site`). Accetta un singolo valore o un elenco di questi casi di `FetchSite`: + +- `FetchSite::SameOrigin` - dalla stessa identica origine (schema, host e porta) +- `FetchSite::SameSite` - dallo stesso sito, eventualmente da un sottodominio diverso +- `FetchSite::CrossSite` - da un sito estraneo +- `FetchSite::None` - l'ha originata direttamente l'utente, per esempio digitando l'URL o aprendo un segnalibro + +```php +// la richiesta proviene dalle nostre pagine? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // blocchiamo l'azione +} +``` + +Il parametro opzionale `$dest` (l'header `Sec-Fetch-Dest`) dice che tipo di risorsa il browser sta scaricando, per esempio `FetchDest::Document` per una navigazione di primo livello oppure `FetchDest::Empty` per una richiesta fatta da JavaScript. Il parametro opzionale `$user` (l'header `Sec-Fetch-User`) indica se la navigazione è stata provocata da una vera azione dell'utente, come cliccare un link o inviare un form; passate `true` per richiederlo. + +Un controllo che un'azione sia raggiungibile solo dalle vostre pagine e solo tramite una vera azione dell'utente appare allora così: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +I browser più vecchi (Safari prima della 16.4) non inviano gli header `Sec-Fetch-*`. Per essi Nette ripiega su un cookie `SameSite=Strict` che prova solo che la richiesta non è cross-site. Un controllo che richieda anche `$dest` o `$user` non si può verificare in questo modo e in quei browser restituisce `false`: se è troppo severo, verificate solo `$site`. isAjax(): bool .[method] ------------------------ -È una richiesta AJAX? +Si tratta di una richiesta AJAX? getRemoteAddress(): ?string .[method] ------------------------------------- -Restituisce l'indirizzo IP dell'utente. Potrebbe essere necessario [impostare il proxy |configuration#Proxy HTTP] per un corretto funzionamento. +Restituisce l'indirizzo IP dell'utente. Perché funzioni correttamente può servire [impostare il proxy |configuration#Proxy HTTP]. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Restituisce la traduzione DNS dell'indirizzo IP dell'utente. Potrebbe essere necessario [impostare il proxy |configuration#Proxy HTTP] per un corretto funzionamento. +Deprecato, restituisce sempre `null`. Le ricerche DNS inverse erano lente e inaffidabili; se vi serve il nome host, risolvetelo voi da [getRemoteAddress() |#getRemoteAddress()]. getBasicCredentials(): ?array .[method] --------------------------------------- -Restituisce le credenziali di autenticazione per [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. +Restituisce le credenziali di autenticazione per la [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. ```php [$user, $password] = $httpRequest->getBasicCredentials(); @@ -184,12 +216,36 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Restituisce l'origine da cui è arrivata la richiesta. Un'origine è composta da schema (protocollo), nome host e porta, per esempio `https://example.com:8080`. Restituisce `null` se l'header origin non è presente oppure è impostato a `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Il browser invia l'header `Origin` in questi casi: +- richieste cross-origin (chiamate AJAX verso un dominio diverso) +- richieste POST, PUT, DELETE e altre che modificano +- richieste fatte con la Fetch API + +Il browser NON invia l'header `Origin` per: +- normali richieste GET verso lo stesso dominio (navigazione same-origin) +- navigazione diretta digitando un URL nella barra degli indirizzi +- richieste da client che non sono browser + +.[note] +A differenza dell'header `Referer`, `Origin` contiene solo schema, host e porta, non l'intero percorso dell'URL. Questo lo rende più adatto ai controlli di sicurezza preservando la privacy dell'utente. L'header `Origin` si usa soprattutto per la validazione [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing). + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Rileva la lingua. Come parametro `$lang` passiamo un array con le lingue supportate dall'applicazione, e restituirà quella che il browser del visitatore preferirebbe vedere. Non c'è magia, si utilizza semplicemente l'header `Accept-Language`. Se non c'è corrispondenza, restituisce `null`. +Rileva la lingua. Come parametro `$langs` passate un array di lingue supportate dall'applicazione e restituirà quella preferita dal browser del visitatore. Non è magia, usa solo l'header `Accept-Language`. Se non trova corrispondenze, restituisce `null`. ```php -// il browser invia ad es. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 +// il browser invia per esempio Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 $langs = ['hu', 'pl', 'en']; // lingue supportate dall'applicazione echo $httpRequest->detectLanguage($langs); // en @@ -199,62 +255,63 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -La classe [api:Nette\Http\RequestFactory] serve per creare un'istanza di `Nette\Http\Request`, che rappresenta la richiesta HTTP corrente. (Se lavorate con Nette, l'oggetto della richiesta HTTP viene creato automaticamente dal framework.) +La classe [api:Nette\Http\RequestFactory] serve a creare un'istanza di `Nette\Http\Request`, che rappresenta la richiesta HTTP corrente. (Se lavorate con Nette, l'oggetto della richiesta HTTP viene creato automaticamente dal framework.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -Il metodo `fromGlobals()` crea l'oggetto della richiesta in base alle variabili globali PHP correnti (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` e `$_SERVER`). Durante la creazione dell'oggetto, pulisce automaticamente tutti i parametri di input GET, POST, COOKIE e anche l'URL dai caratteri di controllo e dalle sequenze UTF-8 non valide, garantendo la sicurezza nel successivo lavoro con questi dati. +Il metodo `fromGlobals()` crea l'oggetto della richiesta in base alle attuali variabili globali di PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` e `$_SERVER`). Quando crea l'oggetto, ripulisce automaticamente tutti i parametri in ingresso (GET, POST, COOKIE) e anche l'URL dai caratteri di controllo e dalle sequenze UTF-8 non valide, il che garantisce sicurezza nel lavoro successivo con questi dati. -RequestFactory può essere configurata prima di chiamare `fromGlobals()`: +RequestFactory si può configurare prima di chiamare `fromGlobals()`: -- con il metodo `$factory->setBinary()` disabilitate la pulizia automatica dei parametri di input dai caratteri di controllo e dalle sequenze UTF-8 non valide. -- con il metodo `$factory->setProxy(...)` specificate l'indirizzo IP del [server proxy |configuration#Proxy HTTP], necessario per il corretto rilevamento dell'indirizzo IP dell'utente. +- il metodo `$factory->setBinary()` disattiva la pulizia automatica dei parametri in ingresso dai caratteri di controllo e dalle sequenze UTF-8 non valide. +- il metodo `$factory->setProxy(...)` indica l'indirizzo IP del [server proxy |configuration#Proxy HTTP], necessario per rilevare correttamente l'indirizzo IP dell'utente. +- il metodo `$factory->setForceHttps()` .{data-version:3.3.4} forza lo schema della richiesta a HTTPS indipendentemente dall'ambiente del server. -RequestFactory consente di definire filtri che trasformano automaticamente parti dell'URL della richiesta. Questi filtri rimuovono caratteri indesiderati dall'URL, che possono essere inseriti lì, ad esempio, da un'implementazione errata dei sistemi di commento su vari siti web: +RequestFactory permette di definire filtri che trasformano automaticamente parti dell'URL della richiesta. Questi filtri rimuovono dagli URL i caratteri indesiderati che vi possono essere finiti per esempio a causa di implementazioni sbagliate dei sistemi di commenti su vari siti: ```php -// rimozione degli spazi dal percorso +// rimuove gli spazi dal percorso $requestFactory->urlFilters['path']['%20'] = ''; -// rimozione di punto, virgola o parentesi destra dalla fine dell'URI +// rimuove il punto, la virgola o la parentesi chiusa dalla fine dell'URI $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// pulizia del percorso dalle doppie barre (filtro predefinito) +// ripulisce il percorso dalle doppie barre (filtro predefinito) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -La prima chiave `'path'` o `'url'` specifica a quale parte dell'URL applicare il filtro. La seconda chiave è l'espressione regolare da cercare e il valore è la sostituzione da utilizzare al posto del testo trovato. +La prima chiave, `'path'` oppure `'url'`, determina a quale parte dell'URL verrà applicato il filtro. La seconda chiave è l'espressione regolare da cercare e il valore è la sostituzione da usare al posto del testo trovato. -File Caricati +File caricati ============= -Il metodo `Nette\Http\Request::getFiles()` restituisce un array di tutti gli upload in una struttura normalizzata, le cui foglie sono oggetti [api:Nette\Http\FileUpload]. Questi incapsulano i dati inviati dall'elemento del form `<input type=file>`. +Il metodo `Nette\Http\Request::getFiles()` restituisce un array di tutti gli upload in una struttura normalizzata, le cui foglie sono oggetti [api:Nette\Http\FileUpload]. Essi incapsulano i dati inviati dall'elemento di form `<input type=file>`. -La struttura riflette la denominazione degli elementi in HTML. Nel caso più semplice, può essere un singolo elemento del form nominato inviato come: +La struttura rispecchia i nomi degli elementi in HTML. Nel caso più semplice può essere un singolo elemento di form con un nome, inviato come: ```latte <input type="file" name="avatar"> ``` -In questo caso, `$request->getFiles()` restituisce un array: +In questo caso `$request->getFiles()` restituisce l'array: ```php [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* istanza di FileUpload */ ] ``` -L'oggetto `FileUpload` viene creato anche nel caso in cui l'utente non abbia inviato alcun file o l'invio sia fallito. Il metodo `hasFile()` restituisce se il file è stato inviato: +L'oggetto `FileUpload` viene creato anche se l'utente non ha caricato alcun file o se l'upload è fallito. Il metodo `hasFile()` restituisce true se un file è stato inviato: ```php $request->getFile('avatar')?->hasFile(); ``` -Nel caso di un nome di elemento che utilizza la notazione per array: +Nel caso di un nome di elemento che usa la notazione ad array: ```latte <input type="file" name="my-form[details][avatar]"> @@ -266,48 +323,48 @@ l'albero restituito appare così: [ 'my-form' => [ 'details' => [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* istanza di FileUpload */ ], ], ] ``` -È possibile creare anche un array di file: +Potete anche creare array di file: ```latte <input type="file" name="my-form[details][avatars][]" multiple> ``` -In tal caso, la struttura appare così: +In tal caso la struttura appare così: ```php [ 'my-form' => [ 'details' => [ 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, + 0 => /* istanza di FileUpload */, + 1 => /* istanza di FileUpload */, + 2 => /* istanza di FileUpload */, ], ], ], ] ``` -Il modo migliore per accedere all'indice 1 dell'array nidificato è il seguente: +Il modo migliore di accedere all'indice 1 dell'array annidato è questo: ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -Poiché non ci si può fidare dei dati provenienti dall'esterno e quindi nemmeno fare affidamento sulla forma della struttura dei file, questo metodo è più sicuro rispetto, ad esempio, a `$request->getFiles()['my-form']['details']['avatars'][1]`, che potrebbe fallire. +Poiché non potete fidarvi dei dati esterni e quindi contare sulla struttura dei file, questo approccio è più sicuro di per esempio `$request->getFiles()['my-form']['details']['avatars'][1]`, che potrebbe fallire. -Panoramica dei metodi `FileUpload` .{toc: FileUpload} ------------------------------------------------------ +Panoramica dei metodi di `FileUpload` .{toc: FileUpload} +-------------------------------------------------------- hasFile(): bool .[method] @@ -322,7 +379,7 @@ Restituisce `true` se il file è stato caricato con successo. getError(): int .[method] ------------------------- -Restituisce il codice di errore durante il caricamento del file. È una delle costanti [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Se il caricamento è avvenuto correttamente, restituisce `UPLOAD_ERR_OK`. +Restituisce il codice di errore associato al file caricato. È una delle costanti [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php]. Se il file è stato caricato con successo, restituisce `UPLOAD_ERR_OK`. move(string $dest) .[method] @@ -330,18 +387,18 @@ move(string $dest) .[method] Sposta il file caricato in una nuova posizione. Se il file di destinazione esiste già, verrà sovrascritto. ```php -$file->move('/path/to/files/name.ext'); +$file->move('/percorso/verso/i/file/nome.ext'); ``` getContents(): ?string .[method] -------------------------------- -Restituisce il contenuto del file caricato. Se il caricamento non è andato a buon fine, restituisce `null`. +Restituisce il contenuto del file caricato. Se l'upload non è riuscito, restituisce `null`. getContentType(): ?string .[method] ----------------------------------- -Rileva il tipo di contenuto MIME del file caricato in base alla sua firma. Se il caricamento non è andato a buon fine o il rilevamento fallisce, restituisce `null`. +Rileva il tipo MIME del contenuto del file caricato in base alla sua firma. Se l'upload non è riuscito o il rilevamento è fallito, restituisce `null`. .[caution] Richiede l'estensione PHP `fileinfo`. @@ -349,15 +406,15 @@ Richiede l'estensione PHP `fileinfo`. getUntrustedName(): string .[method] ------------------------------------ -Restituisce il nome originale del file, come inviato dal browser. +Restituisce il nome originale del file così come lo ha inviato il browser. .[caution] -Non fidatevi del valore restituito da questo metodo. Il client potrebbe aver inviato un nome di file dannoso con l'intenzione di danneggiare o hackerare la vostra applicazione. +Non fidatevi del valore restituito da questo metodo. Un client potrebbe inviare un nome di file malevolo con l'intento di danneggiare o violare la vostra applicazione. getSanitizedName(): string .[method] ------------------------------------ -Restituisce il nome del file sanificato. Contiene solo caratteri ASCII `[a-zA-Z0-9.-]`. Se il nome non contiene tali caratteri, restituisce `'unknown'`. Se il file è un'immagine in formato JPEG, PNG, GIF, WebP o AVIF, restituisce anche l'estensione corretta. +Restituisce il nome del file sanificato. Contiene solo caratteri ASCII `[a-zA-Z0-9.-]`. Se il nome non contiene caratteri di questo tipo, restituisce `'unknown'`. Se il file è un'immagine JPEG, PNG, GIF, WebP o AVIF, restituisce anche l'estensione corretta. .[caution] Richiede l'estensione PHP `fileinfo`. @@ -365,7 +422,7 @@ Richiede l'estensione PHP `fileinfo`. getSuggestedExtension(): ?string .[method]{data-version:3.2.4} -------------------------------------------------------------- -Restituisce un'estensione di file appropriata (senza punto) corrispondente al tipo MIME rilevato. +Restituisce l'estensione di file appropriata (senza il punto) corrispondente al tipo MIME rilevato. .[caution] Richiede l'estensione PHP `fileinfo`. @@ -373,25 +430,30 @@ Richiede l'estensione PHP `fileinfo`. getUntrustedFullPath(): string .[method] ---------------------------------------- -Restituisce il percorso originale del file, come inviato dal browser durante il caricamento di una cartella. Il percorso completo è disponibile solo in PHP 8.1 e versioni successive. Nelle versioni precedenti, questo metodo restituisce il nome originale del file. +Restituisce il percorso originale del file così come lo ha inviato il browser durante l'upload di una directory. Il percorso completo è disponibile solo in PHP 8.1 e successivi. Nelle versioni precedenti questo metodo restituisce il nome originale del file. .[caution] -Non fidatevi del valore restituito da questo metodo. Il client potrebbe aver inviato un nome di file dannoso con l'intenzione di danneggiare o hackerare la vostra applicazione. +Non fidatevi del valore restituito da questo metodo. Un client potrebbe inviare un nome di file malevolo con l'intento di danneggiare o violare la vostra applicazione. getSize(): int .[method] ------------------------ -Restituisce la dimensione del file caricato. Se il caricamento non è andato a buon fine, restituisce `0`. +Restituisce la dimensione del file caricato. Se l'upload non è riuscito, restituisce `0`. getTemporaryFile(): string .[method] ------------------------------------ -Restituisce il percorso della posizione temporanea del file caricato. Se il caricamento non è andato a buon fine, restituisce `''`. +Restituisce il percorso della posizione temporanea del file caricato. Se l'upload non è riuscito, restituisce `''`. + + +__toString(): string .[method] +------------------------------ +Restituisce il percorso della posizione temporanea del file caricato. Questo permette di usare l'oggetto `FileUpload` direttamente come stringa. isImage(): bool .[method] ------------------------- -Restituisce `true` se il file caricato è un'immagine in formato JPEG, PNG, GIF, WebP o AVIF. Il rilevamento avviene in base alla sua firma e non viene verificata l'integrità dell'intero file. È possibile verificare se un'immagine è danneggiata, ad esempio, provando a [caricarla |#toImage]. +Restituisce `true` se il file caricato è un'immagine JPEG, PNG, GIF, WebP o AVIF. Il rilevamento si basa sulla sua firma e non verifica l'integrità dell'intero file. Se un'immagine sia danneggiata lo si può scoprire per esempio provando a [caricarla |#toImage()]. .[caution] Richiede l'estensione PHP `fileinfo`. @@ -399,9 +461,9 @@ Richiede l'estensione PHP `fileinfo`. getImageSize(): ?array .[method] -------------------------------- -Restituisce la coppia `[larghezza, altezza]` con le dimensioni dell'immagine caricata. Se il caricamento non è andato a buon fine o non si tratta di un'immagine valida, restituisce `null`. +Restituisce la coppia `[larghezza, altezza]` con le dimensioni dell'immagine caricata. Se l'upload non è riuscito o non si tratta di un'immagine valida, restituisce `null`. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Carica l'immagine come oggetto [Image|utils:images]. Se il caricamento non è andato a buon fine o non si tratta di un'immagine valida, lancia un'eccezione `Nette\Utils\ImageException`. +Carica l'immagine come oggetto [Image |utils:images]. Se l'upload non è riuscito o non si tratta di un'immagine valida, lancia una `Nette\Utils\ImageException`. diff --git a/http/it/response.texy b/http/it/response.texy index 86f7e99187..129cbb4099 100644 --- a/http/it/response.texy +++ b/http/it/response.texy @@ -1,10 +1,10 @@ -HTTP Response +Risposta HTTP ************* .[perex] -Nette incapsula la risposta HTTP in oggetti con un'API comprensibile. +Nette incapsula la risposta HTTP in oggetti con un'API chiara. -La risposta HTTP è rappresentata dall'oggetto [api:Nette\Http\Response]. Se lavorate con Nette, questo oggetto viene creato automaticamente dal framework e potete riceverlo tramite [dependency injection |dependency-injection:passing-dependencies]. Nei presenter, basta chiamare il metodo `$this->getHttpResponse()`. +La risposta HTTP è rappresentata dall'oggetto [api:Nette\Http\Response]. Se lavorate con Nette, questo oggetto viene creato automaticamente dal framework e potete farvelo passare con la [dependency injection |dependency-injection:passing-dependencies]. Nei presenter basta chiamare il metodo `$this->getHttpResponse()`. → [Installazione e requisiti |@home#Installazione] @@ -12,12 +12,12 @@ La risposta HTTP è rappresentata dall'oggetto [api:Nette\Http\Response]. Se lav Nette\Http\Response =================== -L'oggetto, a differenza di [Nette\Http\Request|request], è mutabile, quindi potete modificare lo stato usando i setter, ad esempio inviare header. Ricordate che tutti i setter devono essere chiamati **prima di inviare qualsiasi output.** Il metodo `isSent()` indica se l'output è già stato inviato. Se restituisce `true`, ogni tentativo di inviare un header lancerà un'eccezione `Nette\InvalidStateException`. +A differenza di [Nette\Http\Request |request], questo oggetto è mutabile, quindi potete cambiarne lo stato con i setter, per esempio per inviare gli header. Ricordate che tutti i setter **vanno chiamati prima che venga inviato qualsiasi output.** Il metodo `isSent()` dice se l'output è già stato inviato. Se restituisce `true`, ogni tentativo di inviare un header lancia una `Nette\InvalidStateException`. setCode(int $code, ?string $reason=null) .[method] -------------------------------------------------- -Modifica il [codice di stato della risposta |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Per una migliore comprensibilità del codice sorgente, si consiglia di utilizzare le [costanti predefinite |api:Nette\Http\IResponse] per il codice invece dei numeri. +Cambia il [codice di stato |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10] della risposta. Per una migliore leggibilità del codice sorgente si consiglia di usare le [costanti predefinite |api:Nette\Http\IResponse] invece dei numeri veri e propri. ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -31,12 +31,12 @@ Restituisce il codice di stato della risposta. isSent(): bool .[method] ------------------------ -Restituisce se gli header sono già stati inviati dal server al browser, e quindi non è più possibile inviare header o modificare il codice di stato. +Restituisce se gli header sono già stati inviati dal server al browser, cioè se non è più possibile inviare header o cambiare il codice di stato. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Invia un header HTTP e **sovrascrive** un header precedentemente inviato con lo stesso nome. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Invia un header HTTP e **sovrascrive** l'header dello stesso nome inviato in precedenza. Se `$value` è `null`, l'header viene rimosso. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -Invia un header HTTP e **non sovrascrive** un header precedentemente inviato con lo stesso nome. +Invia un header HTTP e **non sovrascrive** l'header dello stesso nome inviato in precedenza. ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -55,20 +55,20 @@ $httpResponse->addHeader('Accept', 'application/xml'); deleteHeader(string $name) .[method] ------------------------------------ -Elimina un header HTTP precedentemente inviato. +Cancella un header HTTP inviato in precedenza. getHeader(string $header): ?string .[method] -------------------------------------------- -Restituisce l'header HTTP inviato o `null` se non esiste. Il parametro è case-insensitive. +Restituisce l'header HTTP inviato oppure `null` se non esiste. Il parametro non fa distinzione tra maiuscole e minuscole. ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ +getHeaders(): array<string, string> .[method] +--------------------------------------------- Restituisce tutti gli header HTTP inviati come array associativo. ```php @@ -79,7 +79,7 @@ echo $headers['Pragma']; setContentType(string $type, ?string $charset=null) .[method] ------------------------------------------------------------- -Modifica l'header `Content-Type`. +Cambia l'header `Content-Type`. ```php $httpResponse->setContentType('text/plain', 'UTF-8'); @@ -88,7 +88,7 @@ $httpResponse->setContentType('text/plain', 'UTF-8'); redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- -Reindirizza a un altro URL. Non dimenticate di terminare lo script successivamente. +Reindirizza a un altro URL. Ricordate di terminare poi lo script. ```php $httpResponse->redirect('http://example.com'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- -Imposta la scadenza del documento HTTP usando gli header `Cache-Control` e `Expires`. Il parametro è un intervallo di tempo (come testo) o `null`, che disabilita la cache. +setExpiration(?string $expire) .[method] +---------------------------------------- +Imposta la scadenza del documento HTTP con gli header `Cache-Control` e `Expires`. Il parametro è un intervallo di tempo (come testo) oppure `null`, che disattiva la cache. ```php -// la cache nel browser scadrà tra un'ora +// la cache del browser scade tra un'ora $httpResponse->setExpiration('1 hour'); ``` sendAsFile(string $fileName) .[method] -------------------------------------- -La risposta verrà scaricata tramite la finestra di dialogo *Salva con nome* con il nome specificato. Il file stesso non viene inviato. +La risposta verrà scaricata tramite la finestra *Salva con nome* con il nome indicato. Non invia il file stesso. ```php -$httpResponse->sendAsFile('faktura.pdf'); +$httpResponse->sendAsFile('fattura.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Invia un cookie. I valori predefiniti dei parametri sono: +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +Invia un cookie. Valori predefiniti dei parametri: -| `$path` | `'/'` | il cookie ha scope su tutti i percorsi nel (sotto)dominio *(configurabile)* -| `$domain` | `null` | che significa con scope sul (sotto)dominio corrente, ma non sui suoi sottodomini *(configurabile)* -| `$secure` | `true` | se il sito web viene eseguito su HTTPS, altrimenti `false` *(configurabile)* -| `$httpOnly` | `true` | il cookie non è accessibile da JavaScript -| `$sameSite` | `'Lax'` | il cookie potrebbe non essere inviato durante l'[accesso da un altro dominio |nette:glossary#Cookie SameSite] +| `$path` | `'/'` | il cookie è disponibile per tutti i percorsi del (sotto)dominio *(configurabile)* +| `$domain` | `null` | cioè disponibile per il (sotto)dominio corrente, ma non per i suoi sottodomini *(configurabile)* +| `$secure` | `auto` | `true` se il sito gira su HTTPS, altrimenti `false` (valore predefinito del framework; la classe da sola usa `false`) *(configurabile)* +| `$httpOnly` | `true` | il cookie non è accessibile a JavaScript +| `$sameSite` | `'Lax'` | il cookie può non essere inviato durante l'[accesso cross-origin |nette:glossary#Cookie SameSite] +| `$partitioned` | `false` | se il cookie è partizionato, vedi sotto *(dalla v3.4)* -I valori predefiniti dei parametri `$path`, `$domain` e `$secure` possono essere modificati nella [configurazione |configuration#Cookie HTTP]. +I valori predefiniti dei parametri `$path`, `$domain` e `$secure` li potete cambiare nella [configurazione |configuration#Cookie HTTP]. -Il tempo può essere specificato come numero di secondi o stringa: +La scadenza si passa come numero di secondi, come intervallo o data testuale, oppure come oggetto `DateTimeInterface`. Il valore `null` crea un cookie di sessione, che il browser scarta alla chiusura. Nette invia la scadenza sia nell'attributo `Expires` sia in `Max-Age`. ```php -$httpResponse->setCookie('lang', 'cs', '100 days'); +$httpResponse->setCookie('lang', 'it', '100 days'); // scade tra 100 giorni +$httpResponse->setCookie('lang', 'it', null); // cookie di sessione ``` -Il parametro `$domain` specifica quali domini possono accettare il cookie. Se non specificato, il cookie viene accettato dallo stesso (sotto)dominio che lo ha impostato, ma non dai suoi sottodomini. Se `$domain` è specificato, vengono inclusi anche i sottodomini. Pertanto, specificare `$domain` è meno restrittivo che ometterlo. Ad esempio, con `$domain = 'nette.org'`, i cookie sono disponibili anche su tutti i sottodomini come `doc.nette.org`. +Il parametro `$domain` determina quali domini possono accettare il cookie. Se non è indicato, il cookie viene accettato dallo stesso (sotto)dominio che lo ha impostato, ma non dai suoi sottodomini. Se `$domain` è indicato, sono compresi anche i sottodomini. Indicare `$domain` è quindi meno restrittivo che ometterlo. Con `$domain = 'nette.org'`, per esempio, i cookie sono disponibili anche su tutti i sottodomini come `doc.nette.org`. + +Il valore `$sameSite` lo potete passare come enum `Nette\Http\SameSite`: `SameSite::Lax`, `SameSite::Strict` oppure `SameSite::None` (funzionano anche i valori stringa `'Lax'`, `'Strict'`, `'None'`). Se impostate `SameSite::None`, l'attributo `$secure` si attiva automaticamente, perché i browser rifiutano un cookie `SameSite=None` che non sia sicuro. + +.{data-version:3.4.0} +I cookie partizionati (CHIPS) danno al cookie uno spazio di archiviazione separato per ogni sito di primo livello. Quando quindi un servizio di terze parti (per esempio un widget incorporato) imposta un cookie partizionato, il browser ne conserva una copia distinta per ogni sito in cui il widget compare, e queste copie non si possono collegare tra loro per il tracciamento cross-site. Lo attivate impostando `$partitioned` a `true`; richiede anche l'attributo `$secure`, che perciò si attiva automaticamente. -Per il valore `$sameSite` potete usare le costanti `Response::SameSiteLax`, `Response::SameSiteStrict` e `Response::SameSiteNone`. +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- -Elimina un cookie. I valori predefiniti dei parametri sono: -- `$path` con scope su tutte le directory (`'/'`) -- `$domain` con scope sul (sotto)dominio corrente, ma non sui suoi sottodomini -- `$secure` è determinato dalle impostazioni nella [configurazione |configuration#Cookie HTTP] +Cancella un cookie. I valori predefiniti dei parametri sono: +- `$path` con ambito su tutte le directory (`'/'`) +- `$domain` con ambito sul (sotto)dominio corrente, ma non sui suoi sottodomini +- `$secure` dipende dalle impostazioni nella [configurazione |configuration#Cookie HTTP] ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +L'oggetto [api:Nette\Http\Context] unisce la richiesta e la risposta e aiuta con la cache HTTP. Non è registrato come servizio, quindi ve lo create voi. Nei presenter di solito è più comodo usare il metodo [lastModified() |application:presenters#Caching HTTP]; il context torna utile quando inviate la risposta da soli, per esempio da una vostra classe di risposta. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Determina se il contenuto è cambiato dall'ultima visita del client. Se passate l'ora dell'ultima modifica, invia l'header `Last-Modified`; se passate un validatore ETag (una breve stringa che identifica la versione attuale del contenuto, per esempio il suo hash), invia l'header `ETag`. Poi li confronta con gli header `If-Modified-Since` e `If-None-Match` inviati dal browser. + +Se il browser ha già una versione corrispondente, il metodo imposta il codice `304 Not Modified` e restituisce `false`: in tal caso non inviate affatto il corpo della risposta. Altrimenti restituisce `true`. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Entrambi i parametri sono opzionali. Se non conoscete l'ora di modifica del contenuto, usate solo l'ETag, e viceversa. diff --git a/http/it/sessions.texy b/http/it/sessions.texy index 136fd0a282..8adededb9f 100644 --- a/http/it/sessions.texy +++ b/http/it/sessions.texy @@ -3,39 +3,39 @@ Sessioni <div class=perex> -HTTP è un protocollo stateless, tuttavia quasi ogni applicazione ha bisogno di mantenere lo stato tra le richieste, ad esempio il contenuto del carrello degli acquisti. A questo servono le sessioni. Vedremo: +HTTP è un protocollo senza stato, eppure quasi ogni applicazione ha bisogno di mantenere lo stato tra le richieste, per esempio il contenuto di un carrello. È esattamente a questo che servono le sessioni. Mostreremo: - come usare le sessioni -- come prevenire conflitti di nomi +- come evitare i conflitti di nomi - come impostare la scadenza </div> -Quando si usano le sessioni, ogni utente riceve un identificatore univoco chiamato ID di sessione, che viene passato in un cookie. Questo funge da chiave per i dati della sessione. A differenza dei cookie, che vengono memorizzati lato browser, i dati della sessione vengono memorizzati lato server. +Quando si usano le sessioni, ogni utente riceve un identificatore univoco chiamato session ID, che viene trasmesso in un cookie. Serve da chiave per i dati della sessione. A differenza dei cookie, che sono conservati dal lato del browser, i dati della sessione sono conservati dal lato del server. -Impostiamo la sessione nella [configurazione |configuration#Sessione], l'opzione del tempo di scadenza è particolarmente importante. +Le sessioni le configuriamo nella [configurazione |configuration#Sessione]; è particolarmente importante la scelta del tempo di scadenza. -La gestione della sessione è affidata all'oggetto [api:Nette\Http\Session], a cui potete accedere facendovelo passare tramite [dependency injection |dependency-injection:passing-dependencies]. Nei presenter, basta chiamare `$session = $this->getSession()`. +Della gestione delle sessioni si occupa l'oggetto [api:Nette\Http\Session], al quale arrivate facendovelo passare con la [dependency injection |dependency-injection:passing-dependencies]. Nei presenter basta chiamare `$session = $this->getSession()`. → [Installazione e requisiti |@home#Installazione] -Avvio della Sessione +Avvio della sessione ==================== -Per default, Nette avvia automaticamente la sessione nel momento in cui iniziamo a leggere o scrivere dati in essa. Manualmente, la sessione si avvia con `$session->start()`. +Per impostazione predefinita Nette avvia automaticamente la sessione nel momento in cui cominciamo a leggerne o scriverne i dati. Per avviare la sessione a mano usate `$session->start()`. -PHP invia header HTTP che influenzano la cache all'avvio della sessione, vedi [php:session_cache_limiter], e potenzialmente anche un cookie con l'ID di sessione. Pertanto, è necessario avviare sempre la sessione prima di inviare qualsiasi output al browser, altrimenti verrà lanciata un'eccezione. Quindi, se sapete che la sessione verrà utilizzata durante il rendering della pagina, avviatela manualmente prima, ad esempio nel presenter. +Quando avvia la sessione, PHP invia gli header HTTP che influenzano la cache (vedi [php:session_cache_limiter]) ed eventualmente il cookie con il session ID. Perciò bisogna sempre avviare la sessione prima di inviare qualsiasi output al browser, altrimenti verrà lanciata un'eccezione. Se quindi sapete che durante il rendering della pagina verrà usata una sessione, avviatela prima a mano, per esempio nel presenter. -In modalità sviluppatore, Tracy avvia la sessione perché la utilizza per visualizzare le barre con i reindirizzamenti e le richieste AJAX nella Tracy Bar. +In modalità di sviluppo la sessione viene avviata da Tracy, perché la usa per mostrare nella Tracy Bar le barre per i redirect e le richieste AJAX. Sezioni ======= -In PHP puro, l'archivio dati della sessione è implementato come un array accessibile tramite la variabile globale `$_SESSION`. Il problema è che le applicazioni sono comunemente composte da molte parti indipendenti e se tutte hanno accesso a un solo array, prima o poi si verificherà una collisione di nomi. +In PHP puro l'archivio dei dati di sessione è realizzato come un array accessibile tramite la variabile globale `$_SESSION`. Il problema è che le applicazioni sono di solito composte da molte parti indipendenti e, se tutte hanno a disposizione un solo array, prima o poi si verificherà una collisione di nomi. -Nette Framework risolve il problema dividendo l'intero spazio in sezioni (oggetti [api:Nette\Http\SessionSection]). Ogni unità utilizza quindi la propria sezione con un nome univoco e non possono più verificarsi collisioni. +Il Nette Framework risolve il problema dividendo tutto lo spazio in sezioni (oggetti [api:Nette\Http\SessionSection]). Ogni unità usa poi la propria sezione con un nome univoco e nessuna collisione può verificarsi. Otteniamo la sezione dalla sessione: @@ -43,29 +43,29 @@ Otteniamo la sezione dalla sessione: $section = $session->getSection('nome univoco'); ``` -Nel presenter, basta usare `getSession()` con un parametro: +Nel presenter basta usare `getSession()` con un parametro: ```php -// $this è Presenter +// $this è un Presenter $section = $this->getSession('nome univoco'); ``` -È possibile verificare l'esistenza di una sezione con il metodo `$session->hasSection('nome univoco')`. +L'esistenza di una sezione si può verificare con il metodo `$session->hasSection('nome univoco')`. L'elenco dei nomi di tutte le sezioni esistenti lo restituisce `$session->getSectionNames()`. -Lavorare con la sezione stessa è quindi molto facile usando i metodi `set()`, `get()` e `remove()`: +Lavorare con la sezione stessa è poi molto facile grazie ai metodi `set()`, `get()` e `remove()`: ```php -// scrittura della variabile -$section->set('userName', 'franta'); +// scrittura di una variabile +$section->set('userName', 'john'); -// lettura della variabile, restituisce null se non esiste +// lettura di una variabile, restituisce null se non esiste echo $section->get('userName'); -// cancellazione della variabile +// rimozione di una variabile $section->remove('userName'); ``` -Per ottenere tutte le variabili da una sezione, è possibile utilizzare un ciclo `foreach`: +Per ottenere tutte le variabili di una sezione potete usare un ciclo `foreach`: ```php foreach ($section as $key => $val) { @@ -74,33 +74,33 @@ foreach ($section as $key => $val) { ``` -Impostazione della Scadenza ---------------------------- +Come impostare la scadenza +-------------------------- -È possibile impostare una scadenza per singole sezioni o addirittura per singole variabili. Possiamo quindi far scadere il login dell'utente dopo 20 minuti, ma continuare a ricordare il contenuto del carrello. +La scadenza si può impostare per le singole sezioni o perfino per le singole variabili. Possiamo far scadere il login di un utente dopo 20 minuti e ricordare comunque il contenuto del carrello. ```php -// la sezione scadrà dopo 20 minuti +// la sezione scade dopo 20 minuti $section->setExpiration('20 minutes'); ``` -Per impostare la scadenza delle singole variabili, si usa il terzo parametro del metodo `set()`: +Per impostare la scadenza delle singole variabili serve il terzo parametro del metodo `set()`: ```php -// la variabile 'flash' scadrà dopo soli 30 secondi +// la variabile 'flash' scade dopo 30 secondi $section->set('flash', $message, '30 seconds'); ``` .[note] -Non dimenticate che il tempo di scadenza dell'intera sessione (vedi [configurazione della sessione |configuration#Sessione]) deve essere uguale o superiore al tempo impostato per le singole sezioni o variabili. +Ricordate che il tempo di scadenza dell'intera sessione (vedi [configurazione della sessione |configuration#Sessione]) deve essere uguale o maggiore del tempo impostato per le singole sezioni o variabili. -L'annullamento di una scadenza precedentemente impostata si ottiene con il metodo `removeExpiration()`. La cancellazione immediata dell'intera sezione è garantita dal metodo `remove()`. +Per annullare una scadenza impostata in precedenza serve il metodo `removeExpiration()`; per cancellare la scadenza di una singola variabile passate il suo nome: `removeExpiration('flash')`. Per rimuovere subito tutta la sezione serve il metodo `remove()`. Eventi $onStart, $onBeforeWrite ------------------------------- -L'oggetto `Nette\Http\Session` ha [eventi |nette:glossary#Eventi] `$onStart` e `$onBeforeWrite`, quindi potete aggiungere callback che vengono invocati dopo l'avvio della sessione o prima della sua scrittura su disco e successiva chiusura. +L'oggetto `Nette\Http\Session` ha gli [eventi |nette:glossary#Eventi] `$onStart` e `$onBeforeWrite`, quindi potete aggiungere callback che vengono richiamati dopo l'avvio della sessione oppure prima che venga scritta su disco e poi terminata. ```php $session->onBeforeWrite[] = function () { @@ -110,7 +110,7 @@ $session->onBeforeWrite[] = function () { ``` -Gestione della Sessione +Gestione della sessione ======================= Panoramica dei metodi della classe `Nette\Http\Session` per la gestione della sessione: @@ -135,22 +135,22 @@ Termina la sessione. La sessione termina automaticamente alla fine dell'esecuzio destroy(): void .[method] ------------------------- -Termina ed elimina la sessione. +Termina e cancella la sessione. exists(): bool .[method] ------------------------ -La richiesta HTTP contiene un cookie con l'ID di sessione? +La richiesta HTTP contiene un cookie con il session ID? regenerateId(): void .[method] ------------------------------ -Genera un nuovo ID di sessione casuale. I dati rimangono conservati. +Genera un nuovo session ID casuale. I dati restano conservati. getId(): string .[method] ------------------------- -Restituisce l'ID di sessione. +Restituisce il session ID. </div> @@ -158,34 +158,34 @@ Restituisce l'ID di sessione. Configurazione -------------- -Impostiamo la sessione nella [configurazione |configuration#Sessione]. Se state scrivendo un'applicazione che non utilizza un container DI, questi metodi vengono utilizzati per la configurazione. Devono essere chiamati prima dell'avvio della sessione. +La sessione la configuriamo nella [configurazione |configuration#Sessione]. Se scrivete un'applicazione che non usa il container DI, usate questi metodi per configurarla. Vanno chiamati prima di avviare la sessione. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Imposta il nome del cookie in cui viene trasmesso l'ID di sessione. Il nome standard è `PHPSESSID`. È utile nel caso in cui si eseguano diverse applicazioni distinte all'interno dello stesso sito web. +Imposta il nome del cookie in cui viene trasmesso il session ID. Il nome standard è `PHPSESSID`. Torna utile se fate girare più applicazioni diverse sullo stesso sito. getName(): string .[method] --------------------------- -Restituisce il nome del cookie in cui viene trasmesso l'ID di sessione. +Restituisce il nome del cookie in cui viene trasmesso il session ID. setOptions(array $options): static .[method] -------------------------------------------- -Configura la sessione. È possibile impostare tutte le [direttive di sessione |https://www.php.net/manual/en/session.configuration.php] PHP (in formato camelCase, ad esempio, invece di `session.save_path` scriviamo `savePath`) e anche [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. +Configura la sessione. Si possono impostare tutte le [direttive di sessione |https://www.php.net/manual/en/session.configuration.php] di PHP (in formato camelCase, scrivete per esempio `savePath` invece di `session.save_path`) e anche [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Imposta il periodo di inattività dopo il quale la sessione scade. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Imposta il tempo di inattività dopo il quale la sessione scade. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Impostazione dei parametri per i cookie. I valori predefiniti dei parametri possono essere modificati nella [configurazione |configuration#Cookie di Sessione]. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +Imposta i parametri dei cookie. I valori predefiniti dei parametri li potete cambiare nella [configurazione |configuration#Cookie di sessione]. setSavePath(string $path): static .[method] @@ -195,17 +195,17 @@ Imposta la directory in cui vengono salvati i file di sessione. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Impostazione di un handler personalizzato, vedi [documentazione PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Imposta un handler personalizzato, vedi la [documentazione di PHP |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> -Sicurezza Prima di Tutto -======================== +La sicurezza prima di tutto +=========================== -Il server presume di comunicare sempre con lo stesso utente finché le richieste sono accompagnate dallo stesso ID di sessione. Il compito dei meccanismi di sicurezza è garantire che ciò avvenga effettivamente e che non sia possibile rubare o falsificare l'identificatore. +Il server presume di comunicare con lo stesso utente finché le richieste sono accompagnate dallo stesso session ID. Il compito dei meccanismi di sicurezza è garantire che sia davvero così e che l'identificatore non possa essere rubato o sostituito. -Nette Framework configura quindi correttamente le direttive PHP in modo che l'ID di sessione venga trasmesso solo nei cookie, lo renda inaccessibile a JavaScript e ignori eventuali identificatori nell'URL. Inoltre, nei momenti critici, come il login dell'utente, genera un nuovo ID di sessione. +Il Nette Framework configura perciò correttamente le direttive PHP perché il session ID venga trasmesso solo nei cookie, sia inaccessibile a JavaScript e gli eventuali identificatori nell'URL vengano ignorati. Nei momenti critici, come il login dell'utente, genera inoltre un nuovo session ID. .[note] -Per la configurazione di PHP si utilizza la funzione ini_set, che purtroppo alcuni hosting vietano. Se questo è il caso del vostro hoster, cercate di concordare con lui l'abilitazione della funzione o almeno la configurazione del server. +Per configurare PHP si usa la funzione `ini_set`, che purtroppo alcuni hosting vietano. Se è il caso del vostro hosting, provate a mettervi d'accordo con loro perché ve la consentano o almeno configurino il server correttamente. diff --git a/http/it/ssrf.texy b/http/it/ssrf.texy new file mode 100644 index 0000000000..fda55d4250 --- /dev/null +++ b/http/it/ssrf.texy @@ -0,0 +1,183 @@ +Protezione SSRF +*************** + +.[perex] +Quando la vostra applicazione scarica un URL fornito dall'utente, un attaccante può abusarne per raggiungere la vostra rete interna. Le classi [#UrlValidator] e [#IPAddress] vi aiutano a difendervi da questi attacchi Server-Side Request Forgery (SSRF). + +→ [Installazione e requisiti |@home#Installazione] + + +Che cos'è l'SSRF? +================= + +Immaginate una funzione in cui l'utente inserisce un URL e il vostro server lo scarica: un avatar da un indirizzo remoto, la destinazione di un webhook, l'anteprima di un link. Sembra innocuo, ma a raggiungere l'indirizzo è il server, non il browser dell'utente. E il server vede posti che l'attaccante non vede: l'interfaccia di loopback, la rete privata, i servizi cloud. + +L'attaccante invia perciò un URL che punta verso l'interno invece che verso l'internet pubblica. Gli obiettivi tipici sono: + +- i metadati cloud su `http://169.254.169.254/`, da cui possono uscire chiavi di accesso +- pannelli di amministrazione interni e router come `http://192.168.1.1/` +- servizi senza autenticazione, per esempio Redis su `http://localhost:6379/` + +Questa classe di vulnerabilità è così diffusa da figurare nella [OWASP Top 10 |https://owasp.org/Top10/]. La difesa consiste nel validare l'URL **prima** di scaricarlo e nel rifiutare tutto ciò che si risolve in un indirizzo non pubblico. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] verifica un URL rispetto a una policy configurabile: lo schema, la porta, l'host, le userinfo e gli indirizzi IP in cui l'host si risolve. L'uso di base è una sola chiamata: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // URL non sicuro, non scaricarlo +} +``` + +La policy predefinita è volutamente severa: accetta solo `https` sulla porta 443 che punta a un indirizzo IP pubblico. Tutto il resto (loopback, intervalli privati, link-local compresi i metadati cloud, intervalli riservati) viene rifiutato, e il multicast viene rifiutato senza condizioni. È il punto di partenza giusto per scaricare URL arbitrari forniti dagli utenti. + + +Configurare la policy +--------------------- + +La policy la modellate tramite il costruttore. Per esempio, per consentire il semplice `http` su qualsiasi porta e raggiungere indirizzi privati (utile dentro una rete fidata): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // qualsiasi porta + allowPrivateIps: true, +); +``` + +Un modello frequente è limitare lo scaricamento a un insieme fisso di domini partner con una allowlist di host. Il prefisso `*.` corrisponde a qualsiasi profondità di sottodominio ma non al dominio principale: se vi serve, elencate entrambe le forme: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +L'insieme completo delle opzioni del costruttore: + +| Parametro | Predefinito | Significato +|--------------------- +| `schemes` | `['https']` | schemi consentiti; `[]` rifiuta tutto +| `ports` | `[443]` | porte consentite, `null` = qualsiasi; la porta implicita dello schema viene rispettata +| `allowPrivateIps` | `false` | consente gli intervalli privati (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | consente il loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | consente il link-local compresi i metadati cloud 169.254.169.254 +| `allowReserved` | `false` | consente gli intervalli riservati IANA +| `allowUserinfo` | `false` | consente `user:pass@` nell'URL +| `hostAllowlist` | `null` | se impostato, l'host deve corrispondere a un pattern; `[]` rifiuta tutto +| `hostBlocklist` | `null` | se impostato, l'host non deve corrispondere ad alcun pattern + + +Metodi di validazione +--------------------- + +Il validatore offre tre metodi. `allows()` esegue il controllo completo compresa la risoluzione DNS: l'host viene risolto e **ogni** indirizzo A/AAAA deve superare la policy sugli IP: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` salta la risoluzione DNS e i controlli sugli intervalli di IP. Usatelo come pre-filtro veloce, oppure quando la validazione DNS è delegata al livello che scarica: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Entrambi i metodi accettano una stringa, un oggetto [UrlImmutable |urls#UrlImmutable] oppure `null` (che fallisce sempre). + + +Sconfiggere il DNS rebinding +---------------------------- + +Tra la validazione e lo scaricamento c'è una sottile corsa: l'attaccante può restituire un IP sicuro quando validate l'host e poi spostare il DNS su un IP interno per lo scaricamento vero e proprio. Per chiudere questa falla, `getResolvedIPs()` restituisce gli indirizzi IP validati e voi fissate la connessione a essi, così lo scaricamento non può essere dirottato altrove: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // URL non sicuro +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... esecuzione della richiesta +``` + +Il metodo restituisce un array di stringhe IP (prima i record A, poi gli AAAA) che hanno superato la policy completa, oppure un array vuoto in caso di fallimento. Per un IP scritto letteralmente nell'URL valida direttamente l'indirizzo e non esegue alcuna ricerca DNS. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] è un oggetto valore immutabile per lavorare con gli indirizzi IPv4 e IPv6. `UrlValidator` lo usa internamente, ma torna utile anche da solo ogni volta che classificate degli indirizzi. Il costruttore lancia `Nette\InvalidArgumentException` per un indirizzo non valido: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +Quando non volete un'eccezione, usate la factory `tryFrom()` oppure il controllo `isValid()`: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Classificazione degli indirizzi +------------------------------- + +I predicati vi dicono a quale classe appartiene un indirizzo. Quello chiave è `isPublic()`: vero solo per gli indirizzi instradabili pubblicamente, che è esattamente ciò che serve a una difesa SSRF: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (intervallo dei metadati cloud) +``` + +L'insieme completo dei predicati: + +| Metodo | Verifica +|-------------------- +| `isPublic()` | instradabile pubblicamente (nessuno dei casi sotto) +| `isPrivate()` | intervalli privati RFC 1918 / 4193 +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (compresi i metadati cloud), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | riservati IANA (documentazione, CGNAT, uso futuro, ...) + + +Appartenenza a un intervallo +---------------------------- + +`isInRange()` verifica se l'indirizzo rientra in un blocco CIDR. Potete passare una rete con un prefisso, oppure un semplice indirizzo per una corrispondenza esatta (implicitamente /32 per IPv4, /128 per IPv6): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (corrispondenza esatta) +``` + +Un input malformato o una famiglia di IP diversa restituisce `false`. + + +IPv6 con IPv4 mappato +--------------------- + +Gli indirizzi scritti come IPv6 con IPv4 mappato (per esempio `::ffff:127.0.0.1`) sono un modo classico per aggirare i filtri ingenui. `IPAddress` li normalizza, così i predicati sugli intervalli vedono oltre il travestimento: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +I metodi `isIPv4()` e `isIPv6()` riportano la forma testuale: un indirizzo mappato è IPv6, non IPv4. diff --git a/http/it/upgrading.texy b/http/it/upgrading.texy new file mode 100644 index 0000000000..61c5cf3e4c --- /dev/null +++ b/http/it/upgrading.texy @@ -0,0 +1,54 @@ +Aggiornamento +************* + + +Aggiornamento alla versione 3.4 +=============================== + +La versione minima richiesta di PHP è la 8.3. + +- il metodo `Request::isSameSite()` è deprecato a favore di `isFrom()`, che determina l'origine della richiesta dagli header `Sec-Fetch-*`. La protezione automatica dei form e dei segnali diventa più precisa e cambia un comportamento: la navigazione diretta (un segnalibro, un indirizzo digitato a mano, un link in una email) non è più considerata same-site. Se un segnale conta sui link di azione nelle email, contrassegnatelo con `#[Requires(sameOrigin: false)]`. +- il cookie `_nss` viene ora inviato solo ai browser che non inviano l'header `Sec-Fetch-Site` +- `setCookie()` invia l'attributo `Max-Age` e forza il flag `Secure` per `SameSite=None` e per i cookie partizionati +- l'enum `SameSite` sostituisce le costanti `IResponse::SameSiteLax` ecc., che sono deprecate +- la scadenza viene interpretata ovunque allo stesso modo: un numero è un numero relativo di secondi, una stringa è un intervallo o una data. Passare un timestamp UNIX assoluto è deprecato e il cookie di sessione si rappresenta con `null` invece che con `0`. +- il metodo deprecato `Request::getRemoteHost()` restituisce `null` +- la classe deprecata da tempo `Nette\Http\UserStorage` è stata rimossa + +Tutta la storia del passaggio agli header `Sec-Fetch-*` è raccontata nell'articolo [Quarter Century of CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Aggiornamento alla versione 3.2 +=============================== + +- le credenziali dell'HTTP Basic Authentication non fanno più parte dell'oggetto `Url`, quindi `$url->getUser()` e `$url->getPassword()` restituiscono una stringa vuota. Leggetele con il nuovo metodo `$request->getBasicCredentials()`. + +I motivi di questo cambiamento sono spiegati nell'articolo [Nette Http 3.2: change access to credentials |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]. + + +Aggiornamento alla versione 3.1 +=============================== + +- i cookie vengono inviati con il flag `sameSite: Lax` +- `cookieSecure` ha ora il valore predefinito 'auto' +- l'opzione `session.cookieSecure` è deprecata, al suo posto si usa `http.cookieSecure` +- il cookie `nette-samesite` è stato rinominato in `_nss` +- `Nette\Http\Request::getFile()` accetta un array di chiavi e restituisce `FileUpload|null` +- `Nette\Http\Session::getCookieParameters()` è deprecato +- `Nette\Http\FileUpload::getName()` è stato rinominato in `getUntrustedName()` +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()` e `getRelativeUrl()` sono deprecati (questi metodi fanno parte di `UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` è deprecato +- `Nette\Http\FileUpload::getImageSize()` restituisce la coppia `[larghezza, altezza]` +- con `autoStart: smart` (il valore predefinito) la sessione non viene più avviata subito dopo l'avvio dell'applicazione solo perché il browser ha inviato un cookie di sessione; si avvia alla prima lettura o scrittura. Sono stati aggiunti i valori `always` e `never`. +- quando il browser invia un ID di sessione per il quale non esiste alcuna sessione, Nette cancella il cookie invece di creare una nuova sessione +- per accedere alle sezioni della sessione preferite i metodi `set()`, `get()` e `remove()`; a differenza dell'accesso tramite proprietà distinguono correttamente la lettura dalla scrittura e non avviano la sessione inutilmente +- i valori predefiniti di `cookiePath` e `cookieDomain` si possono impostare nella configurazione + +Il comportamento delle sessioni è descritto in dettaglio nell'articolo [Nette Http 3.1: much smarter sessions |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions]. + + +Aggiornamento alla versione 3.0 +=============================== + +- l'oggetto `Nette\Http\UrlScript` (restituito per esempio da `Nette\Http\Request::getUrl()`) è ora immutabile +- in `new Nette\Http\Url('abcd')` la stringa `abcd` rappresenta il percorso, non il dominio; dalla 3.0 `(new Nette\Http\Url('abcd'))->setScheme('http')` genera correttamente `http:abcd` invece del precedente `http://abcd` diff --git a/http/it/urls.texy b/http/it/urls.texy index 16565823c8..5efb9d971e 100644 --- a/http/it/urls.texy +++ b/http/it/urls.texy @@ -2,7 +2,7 @@ Lavorare con gli URL ******************** .[perex] -Le classi [#Url], [#UrlImmutable] e [#UrlScript] consentono di generare, analizzare e manipolare facilmente gli URL. +Le classi [#Url], [#UrlImmutable] e [#UrlScript] permettono di generare, analizzare e manipolare facilmente gli URL. → [Installazione e requisiti |@home#Installazione] @@ -10,7 +10,7 @@ Le classi [#Url], [#UrlImmutable] e [#UrlScript] consentono di generare, analizz Url === -La classe [api:Nette\Http\Url] consente di lavorare facilmente con gli URL e i suoi singoli componenti, catturati in questo diagramma: +La classe [api:Nette\Http\Url] permette di manipolare facilmente gli URL e le loro singole componenti, come illustrato in questo diagramma: /--pre scheme user password host port path query fragment @@ -22,7 +22,7 @@ La classe [api:Nette\Http\Url] consente di lavorare facilmente con gli URL e i s hostUrl authority \-- -La generazione di URL è intuitiva: +Generare gli URL è intuitivo: ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -È anche possibile analizzare un URL e manipolarlo ulteriormente: +Potete anche analizzare un URL e poi manipolarlo: ```php $url = new Url( @@ -44,7 +44,7 @@ $url = new Url( ); ``` -La classe `Url` implementa l'interfaccia `JsonSerializable` e ha un metodo `__toString()`, quindi l'oggetto può essere stampato o utilizzato nei dati passati a `json_encode()`. +La classe `Url` implementa l'interfaccia `JsonSerializable` e ha il metodo `__toString()`, quindi l'oggetto si può stampare o usare nei dati passati a `json_encode()`. ```php echo $url; @@ -52,10 +52,10 @@ echo json_encode([$url]); ``` -Componenti URL .[method] ------------------------- +Componenti dell'URL +------------------- -Per restituire o modificare i singoli componenti dell'URL, sono disponibili i seguenti metodi: +Per ottenere o modificare le singole componenti dell'URL sono disponibili questi metodi: .[language-php] | Setter | Getter | Valore restituito @@ -71,22 +71,25 @@ Per restituire o modificare i singoli componenti dell'URL, sono disponibili i se | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | intero URL +| | `getAbsoluteUrl(): string` | URL completo + +I metodi `getUser()`, `getPassword()`, `setUser()` e `setPassword()` sono deprecati, perché inserire le credenziali direttamente nell'URL è sconsigliato. -Attenzione: Quando si lavora con un URL ottenuto da una [HTTP request|request], tenere presente che non conterrà il frammento, poiché il browser non lo invia al server. +Attenzione: quando lavorate con un URL ottenuto da una [richiesta HTTP |request], tenete presente che non conterrà il frammento, perché il browser non lo invia al server. -Possiamo anche lavorare con i singoli parametri query usando: +Possiamo lavorare anche con i singoli parametri della query: .[language-php] | Setter | Getter |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] ------------------------------------------- -Restituisce la parte destra o sinistra dell'host. Funziona così se l'host è `www.nette.org`: +Restituisce la parte destra o sinistra dell'host. Ecco come funziona se l'host è `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -98,8 +101,8 @@ Restituisce la parte destra o sinistra dell'host. Funziona così se l'host è `w | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Verifica se due URL sono identici. ```php @@ -107,9 +110,14 @@ $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Converte l'URL in forma canonica. Converte il nome host in minuscolo e normalizza il percorso (percent-encoding e rimozione dei caratteri superflui). La query string resta invariata. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- -Verifica se l'URL è assoluto. Un URL è considerato assoluto se inizia con uno schema (es. http, https, ftp) seguito da due punti. +Verifica se un URL è assoluto. Un URL è considerato assoluto se inizia con uno schema (per esempio http, https, ftp) seguito da due punti. ```php Url::isAbsolute('https://nette.org'); // true @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -Normalizza il percorso nell'URL rimuovendo i segmenti speciali `.` e `..`. Il metodo rimuove gli elementi superflui del percorso nello stesso modo in cui lo fanno i browser web. +Normalizza il percorso di un URL rimuovendo i segmenti speciali `.` e `..`. Questo metodo rimuove gli elementi superflui del percorso nello stesso modo dei browser web. ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,24 +139,24 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -La classe [api:Nette\Http\UrlImmutable] è un'alternativa immutabile alla classe [#Url] (simile a come `DateTimeImmutable` è l'alternativa immutabile a `DateTime` in PHP). Invece dei setter, ha i cosiddetti wither, che non modificano l'oggetto, ma restituiscono nuove istanze con il valore modificato: +La classe [api:Nette\Http\UrlImmutable] è l'alternativa immutabile alla classe [#Url] (in modo simile a come `DateTimeImmutable` è l'alternativa immutabile a `DateTime` in PHP). Invece dei setter ha i cosiddetti wither, che non modificano l'oggetto ma restituiscono nuove istanze con il valore modificato: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` -La classe `UrlImmutable` implementa l'interfaccia `JsonSerializable` e ha un metodo `__toString()`, quindi l'oggetto può essere stampato o utilizzato nei dati passati a `json_encode()`. +La classe `UrlImmutable` implementa l'interfaccia `JsonSerializable` e ha il metodo `__toString()`, quindi l'oggetto si può stampare o usare nei dati passati a `json_encode()`. ```php echo $url; @@ -156,10 +164,10 @@ echo json_encode([$url]); ``` -Componenti URL .[method] ------------------------- +Componenti dell'URL +------------------- -Per restituire o modificare i singoli componenti dell'URL, servono i seguenti metodi: +Per ottenere o modificare le singole componenti dell'URL sono disponibili questi metodi: .[language-php] | Wither | Getter | Valore restituito @@ -175,11 +183,11 @@ Per restituire o modificare i singoli componenti dell'URL, servono i seguenti me | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | intero URL +| | `getAbsoluteUrl(): string` | URL completo -Il metodo `withoutUserInfo()` rimuove `user` e `password`. +I metodi `getUser()`, `getPassword()`, `withUser()`, `withPassword()` e `withoutUserInfo()` sono deprecati, perché inserire le credenziali direttamente nell'URL è sconsigliato. -Possiamo anche lavorare con i singoli parametri query usando: +Possiamo lavorare anche con i singoli parametri della query: .[language-php] | Wither | Getter @@ -190,7 +198,7 @@ Possiamo anche lavorare con i singoli parametri query usando: getDomain(int $level = 2): string .[method] ------------------------------------------- -Restituisce la parte destra o sinistra dell'host. Funziona così se l'host è `www.nette.org`: +Restituisce la parte destra o sinistra dell'host. Ecco come funziona se l'host è `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -204,11 +212,11 @@ Restituisce la parte destra o sinistra dell'host. Funziona così se l'host è `w resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -Deriva un URL assoluto nello stesso modo in cui il browser elabora i link su una pagina HTML: -- se il link è un URL assoluto (contiene uno schema), viene utilizzato senza modifiche +Risolve un URL assoluto nello stesso modo in cui un browser elabora i link di una pagina HTML: +- se il link è un URL assoluto (contiene lo schema), viene usato invariato - se il link inizia con `//`, viene preso solo lo schema dall'URL corrente - se il link inizia con `/`, viene creato un percorso assoluto dalla radice del dominio -- negli altri casi, l'URL viene costruito relativamente al percorso corrente +- negli altri casi l'URL viene costruito relativamente al percorso corrente ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,8 +226,8 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Verifica se due URL sono identici. ```php @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -La classe [api:Nette\Http\UrlScript] è un discendente di [#UrlImmutable] e lo estende con ulteriori componenti URL virtuali, come la directory radice del progetto, ecc. Come la classe genitore, è un oggetto immutabile. +La classe [api:Nette\Http\UrlScript] è discendente di [#UrlImmutable] e la estende con altre componenti virtuali dell'URL, per esempio la directory radice del progetto ecc. Come la classe genitore è un oggetto immutabile. -Il seguente diagramma mostra i componenti che UrlScript riconosce: +Il diagramma seguente mostra le componenti che UrlScript riconosce: /--pre baseUrl basePath relativePath relativeUrl @@ -244,14 +252,14 @@ Il seguente diagramma mostra i componenti che UrlScript riconosce: scriptPath pathInfo \-- -- `baseUrl` è l'URL di base dell'applicazione, inclusi il dominio e la parte del percorso alla directory radice dell'applicazione -- `basePath` è la parte del percorso alla directory radice dell'applicazione -- `scriptPath` è il percorso allo script corrente -- `relativePath` è il nome dello script (eventualmente altri segmenti del percorso) relativo a basePath -- `relativeUrl` è l'intera parte dell'URL dopo baseUrl, inclusi query string e frammento. -- `pathInfo` è una parte dell'URL, oggi poco utilizzata, dopo il nome dello script +- `baseUrl` è l'URL di base dell'applicazione, compresi il dominio e la parte di percorso fino alla directory radice dell'applicazione +- `basePath` è la parte di percorso fino alla directory radice dell'applicazione +- `scriptPath` è il percorso dello script corrente +- `relativePath` è il nome dello script (ed eventualmente altri segmenti di percorso) relativo a `basePath` +- `relativeUrl` è tutta la parte di URL dopo `baseUrl`, compresi query string e frammento +- `pathInfo` è la parte di URL dopo il nome dello script, oggi usata di rado -Per restituire parti dell'URL, sono disponibili i seguenti metodi: +Per ottenere queste parti dell'URL sono disponibili questi metodi: .[language-php] | Getter | Valore restituito @@ -259,8 +267,8 @@ Per restituire parti dell'URL, sono disponibili i seguenti metodi: | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Gli oggetti `UrlScript` di solito non li creiamo direttamente, ma vengono restituiti dal metodo [Nette\Http\Request::getUrl()|request] con i componenti già correttamente impostati per la richiesta HTTP corrente. +Di solito non creiamo direttamente gli oggetti `UrlScript`; li restituisce invece il metodo [Nette\Http\Request::getUrl() |request] con le componenti già impostate correttamente per la richiesta HTTP corrente. diff --git a/http/ja/@home.texy b/http/ja/@home.texy index 289124a99d..523fc775f4 100644 --- a/http/ja/@home.texy +++ b/http/ja/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -`nette/http` パッケージは、[HTTP リクエスト|request]と[レスポンス |response]、[セッション |sessions]の操作、および[URL の解析と構築 |urls]をカプセル化します。 +`nette/http` のパッケージは、HTTP のやり取りすべてであなたの相棒になります。届いたリクエストと送り出すレスポンスに分かりやすいオブジェクト指向の API を与え、セッションと URL アドレスの扱いを簡単にし、そのうえ安全の面倒まで見ます。ここにあるものは次のとおりです。 + +| [HTTP リクエスト |request] | 届いたリクエストと入力の清め方 +| [HTTP レスポンス |response] | 送り出すレスポンス、ヘッダー、クッキー +| [セッション|sessions] | リクエストをまたいで状態を安全に保つ +| [URL の操作 |urls] | URL アドレスの解析と組み立て +| [SSRF 対策 |ssrf] | Server-Side Request Forgery の攻撃からの守り +| [設定|configuration] | このパッケージの設定オプション インストール ------ -[Composer|best-practices:composer]を使用してライブラリをダウンロードし、インストールします: +パッケージは [Composer|best-practices:composer]でダウンロードしてインストールします。 ```shell composer require nette/http ``` + +このパッケージには PHP のバージョン 8.3 から 8.5 が要ります。 diff --git a/http/ja/@left-menu.texy b/http/ja/@left-menu.texy index f05c3fa47c..033331232e 100644 --- a/http/ja/@left-menu.texy +++ b/http/ja/@left-menu.texy @@ -1,8 +1,19 @@ Nette HTTP ********** -- [はじめに |@home] -- [HTTP リクエスト|request] -- [HTTP レスポンス|response] -- [セッション |sessions] -- [URL ユーティリティ |urls] -- [設定 |configuration] +- [概要 |@home] +- [HTTP リクエスト |request] +- [HTTP レスポンス |response] +- [セッション|sessions] +- [URL の操作 |urls] +- [SSRF 対策 |ssrf] +- [設定|configuration] +- [アップグレード|upgrading] + + +関連情報 +**** +- [Nette ドキュメント |nette:] +- [Nette Application |application:how-it-works] +- [Utilities |utils:] +- [ベストプラクティス |best-practices:] +- [トラブルシューティング |nette:troubleshooting] diff --git a/http/ja/@meta.texy b/http/ja/@meta.texy index d3c41dc3d7..43b85f3cac 100644 --- a/http/ja/@meta.texy +++ b/http/ja/@meta.texy @@ -1 +1 @@ -{{sitename: Nette ドキュメンテーション}} +{{sitename: Nette ドキュメント}} diff --git a/http/ja/configuration.texy b/http/ja/configuration.texy index aee3772453..8c688b79b0 100644 --- a/http/ja/configuration.texy +++ b/http/ja/configuration.texy @@ -1,40 +1,42 @@ -HTTP 設定 -******* +HTTP の設定 +******** .[perex] -Nette HTTP の設定オプションの概要です。 +Nette HTTP の設定オプションの一覧です。 -フレームワーク全体を使用せず、このライブラリのみを使用する場合は、[設定の読み込み方法|bootstrap:]をお読みください。 +フレームワーク全体ではなくこのライブラリだけを使っているなら、[設定の読み込み方|bootstrap:]をご覧ください。 -HTTP ヘッダー -========= +HTTP のヘッダー +========== ```neon http: - # 各リクエストで送信されるヘッダー + # すべてのレスポンスとともに送られるヘッダー headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' # X-Frame-Options ヘッダーに影響します - frames: ... # (string|bool) デフォルトは 'SAMEORIGIN' + frames: ... # (string|bool|null) 既定は 'SAMEORIGIN' ``` -フレームワークはセキュリティ上の理由から、ヘッダー `X-Frame-Options: SAMEORIGIN` を送信します。これは、ページが同じドメインにある場合にのみ、別のページ内(`<iframe>` 要素内)に表示できることを示します。これは特定の状況(例えば、Facebook アプリケーションを開発している場合)では望ましくない場合があるため、`frames: http://allowed-host.com` または `frames: true` を設定することで動作を変更できます。 +安全のために、フレームワークは `X-Frame-Options: SAMEORIGIN` のヘッダーを送ります。これは、ページを別のページの中(`<iframe>` 要素の中)に表示してよいのは同じドメインの場合だけだと伝えます。場面によっては(たとえば Facebook のアプリケーションを作っているとき)これが望ましくないこともあるので、`frames: http://allowed-host.com` で特定のホストを許したり、`frames: true` でどこからでも埋め込めるようにしたり(ヘッダーが省かれます)、`frames: false` で完全に禁じたり(`X-Frame-Options: DENY`)して、振る舞いを変えられます。 + +既定では Nette は `X-Powered-By: Nette Framework 3` と `Content-Type: text/html; charset=utf-8` のヘッダーも送ります。これらの既定のものも含め、どのヘッダーも値を空の文字列にすれば取り除けます。 Content Security Policy ----------------------- -`Content-Security-Policy`(以下 CSP)ヘッダーを簡単に作成できます。その説明は[CSP の説明|https://content-security-policy.com]にあります。CSP ディレクティブ(例:`script-src`)は、仕様に従って文字列として記述するか、読みやすさのために値の配列として記述できます。その場合、`'self'` のようなキーワードの周りに引用符を書く必要はありません。Nette は `nonce` 値も自動的に生成するため、ヘッダーには例えば `'nonce-y4PopTLM=='` が含まれます。 +`Content-Security-Policy`(CSP)のヘッダーは簡単に設定できます。その説明は [CSP の仕様 |https://content-security-policy.com]にあります。CSP のディレクティブ(`script-src` など)は、仕様に沿った文字列としても、読みやすさのために値の配列としても書けます。配列なら `'self'` のようなキーワードを引用符で囲む必要がありません。Nette は `nonce` の値も自動的に生成するので、ヘッダーには `'nonce-y4PopTLM=='` のようなものが送られます。 ```neon http: # Content Security Policy csp: - # CSP 仕様に従った形式の文字列 + # CSP の仕様に沿った文字列 default-src: "'self' https://example.com" # 値の配列 @@ -44,14 +46,14 @@ http: - self - https://example.com - # スイッチの場合は bool + # 切り替えの場合は bool upgrade-insecure-requests: true block-all-mixed-content: false ``` -テンプレートでは `<script n:nonce>...</script>` を使用し、nonce 値は自動的に補完されます。Nette で安全なウェブサイトを作成するのは本当に簡単です。 +テンプレートでは `<script n:nonce>...</script>` を使えば、nonce の値が自動的に埋められます。Nette で安全なウェブサイトを作るのは本当に簡単です。 -同様に、`Content-Security-Policy-Report-Only` ヘッダー(CSP と並行して使用可能)と [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] を作成できます: +同じように `Content-Security-Policy-Report-Only` のヘッダー(CSP と同時に使えます)と [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]も設定できます。 ```neon http: @@ -69,103 +71,116 @@ http: ``` -HTTP クッキー ---------- +HTTP のクッキー +---------- -[Nette\Http\Response::setCookie() |response#setCookie] メソッドとセッションの一部のパラメータのデフォルト値を変更できます。 +[Nette\Http\Response::setCookie() |response#setCookie()]メソッドとセッションの扱いの、いくつかのパラメータの既定値を変えられます。 ```neon http: - # パスによるクッキーの到達範囲 - cookiePath: ... # (string) デフォルトは '/' + # パスによるクッキーの適用範囲 + cookiePath: ... # (string) 既定は '/' - # クッキーを受け入れるドメイン - cookieDomain: 'example.com' # (string|domain) デフォルトは未設定 + # クッキーを受け取れるドメイン + cookieDomain: 'example.com' # (string|domain) 既定では未設定 - # HTTPS 経由でのみクッキーを送信しますか? - cookieSecure: ... # (bool|auto) デフォルトは auto + # クッキーを HTTPS でだけ送りますか + cookieSecure: ... # (bool|auto) 既定は auto - # Nette が CSRF 保護として使用するクッキーの送信を無効にします - disableNetteCookie: ... # (bool) デフォルトは false + # Nette が CSRF 対策に使うクッキーの送信を止めます + disableNetteCookie: ... # (bool) 既定は false ``` -`cookieDomain` 属性は、どのドメインがクッキーを受け入れることができるかを指定します。指定されていない場合、クッキーは設定したのと同じ(サブ)ドメインを受け入れますが、そのサブドメインは*受け入れません*。`cookieDomain` が指定されている場合、サブドメインも含まれます。したがって、`cookieDomain` を指定する方が、省略するよりも制限が緩くなります。 +`cookieDomain` の属性は、どのドメイン(オリジン)がクッキーを受け取れるかを決めます。指定しなければ、クッキーはそれを設定したのと同じ(サブ)ドメインだけが受け取り、そのサブドメインは**含みません**。`cookieDomain` を指定すると、サブドメインも含まれます。ですから `cookieDomain` の指定は、省くよりも制限が緩くなります。 + +たとえば `cookieDomain: nette.org` を設定すると、クッキーは `doc.nette.org` のようなすべてのサブドメインでも使えます。これは特別な値 `domain`、つまり `cookieDomain: domain` でも実現できます。 + +`cookieSecure` の属性の既定値 `auto` は、ウェブサイトが HTTPS で動いていればクッキーが `Secure` のフラグ付きで送られ、したがって HTTPS でしか使えないことを意味します。 + -例えば、`cookieDomain: nette.org` の場合、クッキーは `doc.nette.org` のようなすべてのサブドメインでも利用可能です。これは特別な値 `domain`、つまり `cookieDomain: domain` を使用しても達成できます。 +HTTP のプロキシ +---------- -`cookieSecure` 属性のデフォルト値 `auto` は、ウェブサイトが HTTPS で実行されている場合、クッキーは `Secure` フラグ付きで送信され、したがって HTTPS 経由でのみ利用可能になることを意味します。 +サイトが HTTP のプロキシの後ろで動いているなら、HTTPS の接続の判別とクライアントの IP アドレスが正しく働くように、プロキシの IP アドレスを書いてください。つまり [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()]と [isSecured() |request#isSecured()]が正しい値を返し、テンプレートでリンクが `https:` のプロトコルで生成されるようにです。 + +```neon +http: + # IP アドレス、範囲(たとえば 127.0.0.1/8)、またはそれらの値の配列 + proxy: 127.0.0.1 # (string|string[]) 既定では未設定 +``` -HTTP プロキシ ---------- +HTTPS を強制する .{data-version:3.3.4} +--------------------------------- -ウェブサイトが HTTP プロキシの背後で実行されている場合は、HTTPS 経由の接続検出とクライアントの IP アドレスが正しく機能するように、その IP アドレスを指定します。つまり、[Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] 関数と [isSecured() |request#isSecured] 関数が正しい値を返し、テンプレートで `https:` プロトコルを持つリンクが生成されるようにします。 +リクエストのスキームを無条件に HTTPS にします。これは、TLS を終端しつつ `X-Forwarded-Proto` のヘッダーを渡さない負荷分散装置やリバースプロキシの後ろで動く、HTTPS だけのサイトに役立ちます。そうした場合、標準の HTTPS の判別は([#HTTP のプロキシ]を設定していても)働かないからです。 ```neon http: - # IP アドレス、範囲(例:127.0.0.1/8)、またはこれらの値の配列 - proxy: 127.0.0.1 # (string|string[]) デフォルトは未設定 + # すべてのリクエストで HTTPS のスキームを強制します + forceHttps: true # (bool) 既定は false ``` セッション ===== -[セッション |sessions]の基本設定: +[セッション |sessions]の基本の設定です。 ```neon session: - # Tracy Bar にセッションパネルを表示しますか? - debugger: ... # (bool) デフォルトは false + # Tracy Bar にセッションのパネルを表示しますか + debugger: ... # (bool) 既定は false - # セッションが期限切れになるまでの非アクティブ期間 - expiration: 14 days # (string) デフォルトは '3 hours' + # セッションが切れるまでの無操作の時間 + expiration: 14 days # (string) 既定は '3 hours' - # セッションはいつ開始されるべきですか? - autoStart: ... # (smart|always|never) デフォルトは 'smart' + # セッションはいつ始めますか + autoStart: ... # (smart|always|never) 既定は 'smart' - # ハンドラ、SessionHandlerInterface インターフェースを実装するサービス + # ハンドラ。SessionHandlerInterface を実装するサービス handler: @handlerService ``` -`autoStart` オプションは、セッションをいつ開始するかを制御します。値 `always` は、アプリケーションの起動時に常にセッションが開始されることを意味します。値 `smart` は、セッションが既に存在する場合、または読み取りまたは書き込みを行いたい場合にのみ、アプリケーションの起動時にセッションが開始されることを意味します。そして最後に、値 `never` はセッションの自動開始を禁止します。 +`autoStart` のオプションは、セッションをいつ始めるかを決めます。値 `always` は、アプリケーションが始まればいつでもセッションを始めることを意味します。値 `smart` は、すでにセッションが存在する場合、あるいはそこから読み書きしようとした瞬間にだけ、アプリケーションとともにセッションを始めることを意味します。最後に値 `never` は、セッションの自動の開始を止めます。 -さらに、すべての PHP [セッションディレクティブ|https://www.php.net/manual/en/session.configuration.php](camelCase 形式)と [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] を設定できます。例: +さらに PHP のすべての[セッションのディレクティブ |https://www.php.net/manual/en/session.configuration.php](camelCase の形で)と [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]も設定できます。例です。 ```neon session: - # 'session.name' は 'name' として記述します + # 'session.name' は 'name' と書きます name: MYID - # 'session.save_path' は 'savePath' として記述します + # 'session.save_path' は 'savePath' と書きます savePath: "%tempDir%/sessions" ``` -セッションクッキー ---------- +セッションのクッキー +---------- -セッションクッキーは[他のクッキー |#HTTP クッキー]と同じパラメータで送信されますが、これらを変更できます: +セッションのクッキーは[ほかのクッキー |#HTTP のクッキー]と同じパラメータで送られますが、これだけを別に変えられます。 ```neon session: - # クッキーを受け入れるドメイン + # クッキーを受け取れるドメイン cookieDomain: 'example.com' # (string|domain) - # 他のドメインからのアクセス時の制限 - cookieSamesite: None # (Strict|Lax|None) デフォルトは Lax + # 別オリジンからのアクセスの制限 + cookieSamesite: None # (Strict|Lax|None) 既定は Lax ``` -`cookieSamesite` 属性は、[他のドメインからのアクセス |nette:glossary#SameSite cookie]時にクッキーが送信されるかどうかに影響します。これは、[クロスサイトリクエストフォージェリ |nette:glossary#Cross-Site Request Forgery CSRF](CSRF)攻撃に対するある程度の保護を提供します。 +`cookieSamesite` の属性は、[別オリジンのリクエスト |nette:glossary#SameSite cookie]でクッキーが送られるかどうかに影響し、[クロスサイトリクエストフォージェリ |nette:glossary#Cross-Site Request Forgery (CSRF)](CSRF)の攻撃からある程度守ってくれます。 -DI サービス -======= +DI のサービス +======== -これらのサービスは DI コンテナに追加されます: +DI コンテナには次のサービスが足されます。 -| 名前 | 型 | 説明 -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP リクエスト| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP レスポンス| response] -| `session.session` | [api:Nette\Http\Session] | [セッション管理| sessions] +| 名前 | 型 | 説明 +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [HTTP リクエスト| request] +| `http.response` | [api:Nette\Http\Response] | [HTTP レスポンス| response] +| `session.session`| [api:Nette\Http\Session] | [セッションの管理| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | HTTP のリクエストを作るファクトリ diff --git a/http/ja/request.texy b/http/ja/request.texy index e72003b226..187c1242f5 100644 --- a/http/ja/request.texy +++ b/http/ja/request.texy @@ -2,11 +2,11 @@ HTTP リクエスト ********** .[perex] -Nette は HTTP リクエストを分かりやすい API を持つオブジェクトにカプセル化し、同時にサニタイズフィルタを提供します。 +Nette は HTTP のリクエストを、分かりやすい API を持つオブジェクトに包み、あわせて清めのフィルタも用意します。 -HTTP リクエストは [api:Nette\Http\Request] オブジェクトによって表されます。Nette を使用している場合、このオブジェクトはフレームワークによって自動的に作成され、[依存性注入|dependency-injection:passing-dependencies] を使用して渡すことができます。Presenter では、単に `$this->getHttpRequest()` メソッドを呼び出すだけです。Nette Framework の外部で作業している場合は、[#RequestFactory] を使用してオブジェクトを作成できます。 +HTTP のリクエストは [api:Nette\Http\Request]オブジェクトが表します。Nette を使っているなら、このオブジェクトはフレームワークが自動的に作るので、[dependency injection |dependency-injection:passing-dependencies]で渡してもらえます。プレゼンターでは `$this->getHttpRequest()` メソッドを呼ぶだけです。Nette Framework の外で作業しているなら、[#RequestFactory]でこのオブジェクトを作れます。 -Nette の大きな利点は、オブジェクトを作成する際に、すべての入力パラメータ GET、POST、COOKIE、および URL から制御文字と無効な UTF-8 シーケンスを自動的にクリーンアップすることです。その後、これらのデータを安全にさらに処理できます。クリーンアップされたデータは、Presenter とフォームで使用されます。 +Nette の大きな利点は、オブジェクトを作るときにすべての入力のパラメータ(GET、POST、COOKIE)と URL を自動的に清め、制御文字と正しくない UTF-8 の並びを取り除くことです。そのあとはそのデータを安全に扱えます。清められたデータはそのあとプレゼンターやフォームで使われます。 → [インストールと要件 |@home#インストール] @@ -14,81 +14,81 @@ Nette の大きな利点は、オブジェクトを作成する際に、すべ Nette\Http\Request ================== -このオブジェクトはイミュータブル(不変)です。セッターはなく、`withUrl()` というウィザーが 1 つだけあり、これはオブジェクトを変更せず、変更された値を持つ新しいインスタンスを返します。 +このオブジェクトは変更できません。セッターはなく、いわゆる wither である `withUrl()` だけがあります。これはオブジェクトを変えずに、値を変えた新しいインスタンスを返します。 withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ---------------------------------------------------------------- -異なる URL を持つクローンを返します。 +URL を変えた複製を返します。 getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -リクエストの URL を [UrlScript |urls#UrlScript] オブジェクトとして返します。 +リクエストの URL を [UrlScript |urls#UrlScript]オブジェクトとして返します。 ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -注意:ブラウザはサーバーにフラグメントを送信しないため、`$url->getFragment()` は空の文字列を返します。 +注意: ブラウザはフラグメントをサーバーへ送らないので、`$url->getFragment()` は空の文字列を返します。 getQuery(?string $key=null): string|array|null .[method] -------------------------------------------------------- -GET リクエストのパラメータを返します。 +GET のリクエストのパラメータを返します。 ```php -$all = $httpRequest->getQuery(); // URL からのすべてのパラメータの配列を返します -$id = $httpRequest->getQuery('id'); // GET パラメータ 'id' を返します(または null) +$all = $httpRequest->getQuery(); // URL のすべてのパラメータの配列 +$id = $httpRequest->getQuery('id'); // GET のパラメータ 'id' を返します(なければ null) ``` getPost(?string $key=null): string|array|null .[method] ------------------------------------------------------- -POST リクエストのパラメータを返します。 +POST のリクエストのパラメータを返します。 ```php -$all = $httpRequest->getPost(); // POST からのすべてのパラメータの配列を返します -$id = $httpRequest->getPost('id'); // POST パラメータ 'id' を返します(または null) +$all = $httpRequest->getPost(); // すべての POST のパラメータの配列 +$id = $httpRequest->getPost('id'); // POST のパラメータ 'id' を返します(なければ null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -[アップロード |#アップロードされたファイル]を [api:Nette\Http\FileUpload] オブジェクトとして返します: +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- +[アップロード |#アップロードされたファイル]を [api:Nette\Http\FileUpload]オブジェクトとして返します。 ```php $file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // 何かファイルがアップロードされましたか? - $file->getUntrustedName(); // ユーザーによって送信されたファイル名 - $file->getSanitizedName(); // 危険な文字を含まない名前 +if ($file?->hasFile()) { // ファイルはアップロードされましたか + $file->getUntrustedName(); // 利用者が送ったファイル名 + $file->getSanitizedName(); // 危険な文字を取り除いた名前 } ``` -ネストされた構造にアクセスするには、キーの配列を指定します。 +入れ子の構造にアクセスするには、キーの配列を渡します。 ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -外部からのデータを信頼できず、したがってファイル構造の形式に依存できないため、例えば `$request->getFiles()['my-form']['details']['avatar']` のように失敗する可能性がある方法よりも、この方法の方が安全です。 +外から来るデータは信じられず、したがってファイルの構造にも頼れないので、このやり方はたとえば失敗しかねない `$request->getFiles()['my-form']['details']['avatar']` より安全です。 getFiles(): array .[method] --------------------------- -正規化された構造で[すべてのアップロード |#アップロードされたファイル]のツリーを返します。そのリーフは [api:Nette\Http\FileUpload] オブジェクトです: +[すべてのアップロード |#アップロードされたファイル]を、葉が [api:Nette\Http\FileUpload]オブジェクトになる整えられた構造の木として返します。 ```php $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -クッキーを返すか、存在しない場合は `null` を返します。 +getCookie(string $key): ?string .[method] +----------------------------------------- +クッキーを返します。なければ `null` を返します。 ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -リクエストが行われた HTTP メソッドを返します。 +そのリクエストで使われた HTTP のメソッドを返します。 ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -リクエストが行われた HTTP メソッドをテストします。パラメータは大文字と小文字を区別しません。 +そのリクエストで使われた HTTP のメソッドを調べます。パラメータは大文字と小文字を区別しません。 ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,51 +124,83 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -HTTP ヘッダーを返すか、存在しない場合は `null` を返します。パラメータは大文字と小文字を区別しません。 +HTTP のヘッダーを返します。なければ `null` を返します。パラメータは大文字と小文字を区別しません。 ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -すべての HTTP ヘッダーを連想配列として返します。 +getHeaders(): array<string, string> .[method] +--------------------------------------------- +すべての HTTP のヘッダーを連想配列として返します。キーは小文字にそろえられます。 ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -接続は暗号化されていますか(HTTPS)?正しく機能させるためには、[プロキシの設定 |configuration#HTTP プロキシ]が必要になる場合があります。 +接続は暗号化されていますか(HTTPS)。正しく働くには[プロキシの設定 |configuration#HTTP のプロキシ]が要ることがあります。 -isSameSite(): bool .[method] ----------------------------- -リクエストは同じ(サブ)ドメインから来ており、リンクのクリックによって開始されましたか?Nette は検出にクッキー `_nss`(以前は `nette-samesite`)を使用します。 +isSameSite(): bool .[method deprecated] +--------------------------------------- +リクエストは同じサイトから来ましたか。バージョン 3.4 からは、より力のある [isFrom() |#isFrom()]に取って代わられました。 + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +リクエストがどこから来て、ブラウザがそれをどう行ったかを教えてくれます。もとにするのは `Sec-Fetch-*` のヘッダー(いわゆる [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header])で、これはブラウザ自身が設定するもので、被害者のブラウザで動くページには偽ることも取り除くこともできません。Nette はこれを内部で使い、フォームとシグナルを[クロスサイトリクエストフォージェリ |nette:glossary#Cross-Site Request Forgery (CSRF)](CSRF)から自動的に守っています。API のエンドポイントや破壊的なリンクのような、あなた自身の機微な操作を守りたいときに役立ちます。 + +このメソッドが `true` を返すのは、リクエストがあなたの渡した条件を**すべて**満たすときだけです。第 1 パラメータ `$site` は、リクエストを起こしたページとあなたのサイトの関係(`Sec-Fetch-Site` のヘッダー)を表します。ひとつの値も、次の `FetchSite` の値の並びも受け取ります。 + +- `FetchSite::SameOrigin` - まったく同じオリジンから(スキーム、ホスト、ポート) +- `FetchSite::SameSite` - 同じサイトから。サブドメインは違ってもかまいません +- `FetchSite::CrossSite` - よそのサイトから +- `FetchSite::None` - 利用者が直接起こしました。たとえば URL を打ち込んだか、ブックマークを開きました + +```php +// リクエストは私たち自身のページから来ましたか +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // その操作を止めます +} +``` + +省略できる `$dest` パラメータ(`Sec-Fetch-Dest` のヘッダー)は、ブラウザがどんな種類の資源を取りに来たかを伝えます。たとえば最上位の移動なら `FetchDest::Document`、JavaScript から行われたリクエストなら `FetchDest::Empty` です。省略できる `$user` パラメータ(`Sec-Fetch-User` のヘッダー)は、その移動がリンクのクリックやフォームの送信のような本物の利用者の操作で起きたかを示します。それを求めるなら `true` を渡します。 + +ある操作が自分のページからだけ、しかも本物の利用者の操作でだけたどり着けることを確かめるなら、次のようになります。 + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +古いブラウザ(16.4 より前の Safari)は `Sec-Fetch-*` のヘッダーを送りません。それらには Nette が `SameSite=Strict` のクッキーで代わりを務めますが、これはリクエストが別サイトからでないことしか証明できません。さらに `$dest` や `$user` を求める確認はこの方法では確かめられず、そうしたブラウザでは `false` を返します。それが厳しすぎるなら `$site` だけを調べてください。 isAjax(): bool .[method] ------------------------ -AJAX リクエストですか? +AJAX のリクエストですか。 getRemoteAddress(): ?string .[method] ------------------------------------- -ユーザーの IP アドレスを返します。正しく機能させるためには、[プロキシの設定 |configuration#HTTP プロキシ]が必要になる場合があります。 +利用者の IP アドレスを返します。正しく働くには[プロキシの設定 |configuration#HTTP のプロキシ]が要ることがあります。 getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -ユーザーの IP アドレスの DNS 解決を返します。正しく機能させるためには、[プロキシの設定 |configuration#HTTP プロキシ]が必要になる場合があります。 +非推奨で、いつも `null` を返します。逆引き DNS の問い合わせは遅く、あてにならないものでした。ホスト名が必要なら、[getRemoteAddress() |#getRemoteAddress()]から自分で引いてください。 getBasicCredentials(): ?array .[method] --------------------------------------- -[Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication] の認証情報を返します。 +[HTTP Basic 認証 |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]の資格情報を返します。 ```php [$user, $password] = $httpRequest->getBasicCredentials(); @@ -177,21 +209,45 @@ getBasicCredentials(): ?array .[method] getRawBody(): ?string .[method] ------------------------------- -HTTP リクエストの本文を返します。 +HTTP のリクエストの本文を返します。 ```php $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +リクエストが来たオリジンを返します。オリジンはスキーム(プロトコル)、ホスト名、ポートから成ります。たとえば `https://example.com:8080` です。origin のヘッダーがないか `'null'` に設定されている場合は `null` を返します。 + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +ブラウザが `Origin` のヘッダーを送るのは次の場合です。 +- 別オリジンのリクエスト(別のドメインへの AJAX の呼び出し) +- POST、PUT、DELETE などの変更を伴うリクエスト +- Fetch API を使って行われたリクエスト + +ブラウザが `Origin` のヘッダーを送らないのは次の場合です。 +- 同じドメインへのふつうの GET のリクエスト(同一オリジンの移動) +- アドレス欄に URL を打ち込む直接の移動 +- ブラウザ以外のクライアントからのリクエスト + +.[note] +`Referer` のヘッダーと違って、`Origin` にはスキーム、ホスト、ポートだけが入り、URL のパス全体は入りません。おかげで利用者の私生活を守りつつ、安全の確認に向いています。`Origin` のヘッダーは主に [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)](Cross-Origin Resource Sharing)の検証に使われます。 + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -言語を検出します。パラメータ `$lang` として、アプリケーションがサポートする言語の配列を渡し、訪問者のブラウザが最も好む言語を返します。これは魔法ではなく、単に `Accept-Language` ヘッダーを使用します。一致するものがない場合は `null` を返します。 +言語を見分けます。`$langs` パラメータにアプリケーションが対応している言語の配列を渡すと、訪問者のブラウザが好むものを返します。魔法ではなく、`Accept-Language` のヘッダーを使っているだけです。合うものがなければ `null` を返します。 ```php -// ブラウザは例えば Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 を送信します +// ブラウザはたとえば Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 を送ります -$langs = ['hu', 'pl', 'en']; // アプリケーションがサポートする言語 +$langs = ['hu', 'pl', 'en']; // アプリケーションが対応している言語 echo $httpRequest->detectLanguage($langs); // en ``` @@ -199,135 +255,136 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -[api:Nette\Http\RequestFactory] クラスは、現在の HTTP リクエストを表す `Nette\Http\Request` インスタンスを作成するために使用されます。(Nette を使用している場合、HTTP リクエストオブジェクトはフレームワークによって自動的に作成されます。) +[api:Nette\Http\RequestFactory]クラスは、今の HTTP のリクエストを表す `Nette\Http\Request` のインスタンスを作るのに使います。(Nette を使っているなら、HTTP のリクエストのオブジェクトはフレームワークが自動的に作ります。) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -`fromGlobals()` メソッドは、現在の PHP グローバル変数(`$_GET`、`$_POST`、`$_COOKIE`、`$_FILES`、`$_SERVER`)に基づいてリクエストオブジェクトを作成します。オブジェクトを作成する際に、すべての入力パラメータ GET、POST、COOKIE、および URL から制御文字と無効な UTF-8 シーケンスを自動的にクリーンアップし、これらのデータをさらに処理する際の安全性を確保します。 +`fromGlobals()` メソッドは、今の PHP の大域変数(`$_GET`、`$_POST`、`$_COOKIE`、`$_FILES`、`$_SERVER`)をもとにリクエストのオブジェクトを作ります。オブジェクトを作るときにすべての入力のパラメータ(GET、POST、COOKIE)と URL から、制御文字と正しくない UTF-8 の並びを自動的に取り除くので、そのあとそのデータを扱うときに安全です。 -RequestFactory は `fromGlobals()` を呼び出す前に設定できます: +RequestFactory は `fromGlobals()` を呼ぶ前に設定できます。 -- `$factory->setBinary()` メソッドで、入力パラメータから制御文字と無効な UTF-8 シーケンスの自動クリーニングを無効にします。 -- `$factory->setProxy(...)` メソッドで、[プロキシサーバー |configuration#HTTP プロキシ]の IP アドレスを指定します。これは、ユーザーの IP アドレスを正しく検出するために必要です。 +- `$factory->setBinary()` メソッドを使うと、入力のパラメータから制御文字と正しくない UTF-8 の並びを自動的に取り除く働きを止められます。 +- `$factory->setProxy(...)` メソッドで[プロキシのサーバー |configuration#HTTP のプロキシ]の IP アドレスを指定します。これは利用者の IP アドレスを正しく判別するのに必要です。 +- `$factory->setForceHttps()` .{data-version:3.3.4} メソッドは、サーバーの環境に関わらずリクエストのスキームを HTTPS にします。 -RequestFactory を使用すると、リクエスト URL の一部を自動的に変換するフィルタを定義できます。これらのフィルタは、例えば、さまざまなウェブサイト上のコメントシステムの不適切な実装によって挿入される可能性のある、URL からの不要な文字を削除します: +RequestFactory では、URL のリクエストの一部を自動的に変えるフィルタも定義できます。これらのフィルタは、たとえばさまざまなウェブサイトのコメントの仕組みの誤った実装によって差し込まれたかもしれない、望まない文字を URL から取り除きます。 ```php -// パスからスペースを削除 +// パスから空白を取り除きます $requestFactory->urlFilters['path']['%20'] = ''; -// URI の末尾からドット、カンマ、または右括弧を削除 +// URI の終わりからドット、コンマ、閉じかっこを取り除きます $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// パスから重複したスラッシュをクリーンアップ(デフォルトフィルタ) +// パスから二重のスラッシュを取り除きます(既定のフィルタ) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -最初のキー `'path'` または `'url'` は、フィルタが適用される URL の部分を指定します。2番目のキーは検索する正規表現であり、値は見つかったテキストの代わりに使用される置換です。 +最初のキー `'path'` か `'url'` は、そのフィルタを URL のどの部分に当てるかを決めます。2 つめのキーは探すための正規表現で、値は見つかった文の代わりに使われる置き換えです。 アップロードされたファイル ============= -`Nette\Http\Request::getFiles()` メソッドは、正規化された構造ですべてのアップロードの配列を返します。そのリーフは [api:Nette\Http\FileUpload] オブジェクトです。これらは、フォームコントロール `<input type=file>` によって送信されたデータをカプセル化します。 +`Nette\Http\Request::getFiles()` メソッドは、すべてのアップロードを、葉が [api:Nette\Http\FileUpload]オブジェクトになる整えられた構造の配列として返します。これらは `<input type=file>` のフォームの要素で送られたデータを包みます。 -構造は HTML のコントロールの命名を反映しています。最も単純なケースでは、次のように送信された単一の名前付きフォームコントロールである可能性があります: +その構造は HTML の要素の名前の付け方を映します。もっとも単純な場合、ひとつの名前の付いたフォームの要素が次のように送られます。 ```latte <input type="file" name="avatar"> ``` -この場合、`$request->getFiles()` は配列を返します: +この場合、`$request->getFiles()` は次の配列を返します。 ```php [ - 'avatar' => /* FileUpload インスタンス */ + 'avatar' => /* FileUpload のインスタンス */ ] ``` -`FileUpload` オブジェクトは、ユーザーがファイルを送信しなかった場合や送信が失敗した場合でも作成されます。ファイルが送信されたかどうかは `hasFile()` メソッドが返します: +`FileUpload` オブジェクトは、利用者がファイルをアップロードしなかった場合やアップロードが失敗した場合にも作られます。ファイルが送られたなら `hasFile()` メソッドが true を返します。 ```php $request->getFile('avatar')?->hasFile(); ``` -配列表記を使用するコントロール名の場合: +要素の名前に配列の書き方を使った場合は、 ```latte <input type="file" name="my-form[details][avatar]"> ``` -返されるツリーは次のようになります: +返される木は次のようになります。 ```php [ 'my-form' => [ 'details' => [ - 'avatar' => /* FileUpload インスタンス */ + 'avatar' => /* FileUpload のインスタンス */ ], ], ] ``` -ファイルの配列を作成することもできます: +ファイルの配列も作れます。 ```latte <input type="file" name="my-form[details][avatars][]" multiple> ``` -この場合、構造は次のようになります: +その場合の構造は次のようになります。 ```php [ 'my-form' => [ 'details' => [ 'avatars' => [ - 0 => /* FileUpload インスタンス */, - 1 => /* FileUpload インスタンス */, - 2 => /* FileUpload インスタンス */, + 0 => /* FileUpload のインスタンス */, + 1 => /* FileUpload のインスタンス */, + 2 => /* FileUpload のインスタンス */, ], ], ], ] ``` -ネストされた配列のインデックス 1 にアクセスする最良の方法は次のとおりです: +入れ子の配列の添字 1 にアクセスするいちばんよい方法は次のとおりです。 ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -外部からのデータを信頼できず、したがってファイル構造の形式に依存できないため、例えば `$request->getFiles()['my-form']['details']['avatars'][1]` のように失敗する可能性がある方法よりも、この方法の方が安全です。 +外から来るデータは信じられず、したがってファイルの構造にも頼れないので、このやり方はたとえば失敗しかねない `$request->getFiles()['my-form']['details']['avatars'][1]` より安全です。 -`FileUpload` メソッドの概要 .{toc: FileUpload} ---------------------------------------- +`FileUpload` のメソッドの一覧 .{toc: FileUpload} +---------------------------------------- hasFile(): bool .[method] ------------------------- -ユーザーがファイルをアップロードした場合に `true` を返します。 +利用者がファイルをアップロードしたなら `true` を返します。 isOk(): bool .[method] ---------------------- -ファイルが正常にアップロードされた場合に `true` を返します。 +ファイルのアップロードが成功したなら `true` を返します。 getError(): int .[method] ------------------------- -ファイルアップロード時のエラーコードを返します。これは [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php] 定数の 1 つです。アップロードが正常に行われた場合、`UPLOAD_ERR_OK` を返します。 +アップロードされたファイルにまつわるエラーのコードを返します。これは [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php]の定数のどれかです。アップロードが成功したなら `UPLOAD_ERR_OK` を返します。 move(string $dest) .[method] ---------------------------- -アップロードされたファイルを新しい場所に移動します。宛先ファイルが既に存在する場合、上書きされます。 +アップロードされたファイルを新しい場所へ移します。移し先のファイルがすでにあれば上書きされます。 ```php $file->move('/path/to/files/name.ext'); @@ -336,72 +393,77 @@ $file->move('/path/to/files/name.ext'); getContents(): ?string .[method] -------------------------------- -アップロードされたファイルの内容を返します。アップロードが成功しなかった場合、`null` を返します。 +アップロードされたファイルの中身を返します。アップロードが成功しなかった場合は `null` を返します。 getContentType(): ?string .[method] ----------------------------------- -アップロードされたファイルの MIME コンテンツタイプを、その署名に基づいて検出します。アップロードが成功しなかった場合、または検出が失敗した場合、`null` を返します。 +アップロードされたファイルの MIME の内容の型を、その署名から見分けます。アップロードが成功しなかったか、見分けに失敗した場合は `null` を返します。 .[caution] -PHP 拡張機能 `fileinfo` が必要です。 +PHP の `fileinfo` 拡張が要ります。 getUntrustedName(): string .[method] ------------------------------------ -ブラウザが送信した元のファイル名を返します。 +ブラウザが送ってきたもとのファイル名を返します。 .[caution] -このメソッドによって返される値を信頼しないでください。クライアントは、アプリケーションを破損またはハッキングする意図で悪意のあるファイル名を送信した可能性があります。 +このメソッドが返す値を信じないでください。クライアントは、あなたのアプリケーションを壊したり乗っ取ったりするつもりで、悪意のあるファイル名を送っているかもしれません。 getSanitizedName(): string .[method] ------------------------------------ -サニタイズされたファイル名を返します。ASCII 文字 `[a-zA-Z0-9.-]` のみを含みます。名前にそのような文字が含まれていない場合、`'unknown'` を返します。ファイルが JPEG、PNG、GIF、WebP、または AVIF 形式の画像である場合、正しい拡張子も返します。 +清められたファイル名を返します。ASCII の文字 `[a-zA-Z0-9.-]` だけを含みます。名前にそうした文字が入っていなければ `'unknown'` を返します。ファイルが JPEG、PNG、GIF、WebP、AVIF の画像なら、正しい拡張子も付けて返します。 .[caution] -PHP 拡張機能 `fileinfo` が必要です。 +PHP の `fileinfo` 拡張が要ります。 getSuggestedExtension(): ?string .[method]{data-version:3.2.4} -------------------------------------------------------------- -検出された MIME タイプに対応する適切なファイル拡張子(ドットなし)を返します。 +見分けられた MIME の型に対応する、ふさわしいファイルの拡張子(ドットなし)を返します。 .[caution] -PHP 拡張機能 `fileinfo` が必要です。 +PHP の `fileinfo` 拡張が要ります。 getUntrustedFullPath(): string .[method] ---------------------------------------- -フォルダのアップロード時にブラウザが送信した元のファイルパスを返します。完全なパスは PHP 8.1 以降でのみ利用可能です。以前のバージョンでは、このメソッドは元のファイル名を返します。 +ディレクトリのアップロードのときにブラウザが送ってきたもとのファイルのパスを返します。完全なパスが得られるのは PHP 8.1 以降だけです。それより前のバージョンでは、このメソッドはもとのファイル名を返します。 .[caution] -このメソッドによって返される値を信頼しないでください。クライアントは、アプリケーションを破損またはハッキングする意図で悪意のあるファイル名を送信した可能性があります。 +このメソッドが返す値を信じないでください。クライアントは、あなたのアプリケーションを壊したり乗っ取ったりするつもりで、悪意のあるファイル名を送っているかもしれません。 getSize(): int .[method] ------------------------ -アップロードされたファイルのサイズを返します。アップロードが成功しなかった場合、`0` を返します。 +アップロードされたファイルの大きさを返します。アップロードが成功しなかった場合は `0` を返します。 getTemporaryFile(): string .[method] ------------------------------------ -アップロードされたファイルの一時的な場所へのパスを返します。アップロードが成功しなかった場合、`''` を返します。 +アップロードされたファイルの一時的な置き場所へのパスを返します。アップロードが成功しなかった場合は `''` を返します。 + + +__toString(): string .[method] +------------------------------ +アップロードされたファイルの一時的な置き場所へのパスを返します。おかげで `FileUpload` オブジェクトをそのまま文字列として使えます。 isImage(): bool .[method] ------------------------- -アップロードされたファイルが JPEG、PNG、GIF、WebP、または AVIF 形式の画像である場合に `true` を返します。検出はその署名に基づいて行われ、ファイル全体の整合性は検証されません。画像が破損していないかどうかは、例えば[読み込み |#toImage]を試みることで確認できます。 +アップロードされたファイルが JPEG、PNG、GIF、WebP、AVIF の画像なら `true` を返します。判別はその署名をもとに行われ、ファイル全体の健全さは確かめません。画像が壊れているかどうかは、たとえば[読み込んでみる |#toImage()]ことで判断できます。 .[caution] -PHP 拡張機能 `fileinfo` が必要です。 +PHP の `fileinfo` 拡張が要ります。 getImageSize(): ?array .[method] -------------------------------- -アップロードされた画像の寸法を含むペア `[幅, 高さ]` を返します。アップロードが成功しなかった場合、または有効な画像でない場合、`null` を返します。 +アップロードされた画像の寸法の組 `[幅, 高さ]` を返します。アップロードが成功しなかったか、正しい画像でない場合は `null` を返します。 toImage(): Nette\Utils\Image .[method] -------------------------------------- -画像を [Image|utils:images] オブジェクトとして読み込みます。アップロードが成功しなかった場合、または有効な画像でない場合、`Nette\Utils\ImageException` 例外をスローします。 +画像を [Image |utils:images]オブジェクトとして読み込みます。アップロードが成功しなかったか、正しい画像でない場合は `Nette\Utils\ImageException` を投げます。 diff --git a/http/ja/response.texy b/http/ja/response.texy index 1bc9d82c00..dda2a97de9 100644 --- a/http/ja/response.texy +++ b/http/ja/response.texy @@ -2,9 +2,9 @@ HTTP レスポンス ********** .[perex] -Nette は HTTP レスポンスを分かりやすい API を持つオブジェクトにカプセル化します。 +Nette は HTTP のレスポンスを、分かりやすい API を持つオブジェクトに包みます。 -HTTP レスポンスは [api:Nette\Http\Response] オブジェクトによって表されます。Nette を使用している場合、このオブジェクトはフレームワークによって自動的に作成され、[依存性注入|dependency-injection:passing-dependencies] を使用して渡すことができます。Presenter では、単に `$this->getHttpResponse()` メソッドを呼び出すだけです。 +HTTP のレスポンスは [api:Nette\Http\Response]オブジェクトが表します。Nette を使っているなら、このオブジェクトはフレームワークが自動的に作るので、[dependency injection |dependency-injection:passing-dependencies]で渡してもらえます。プレゼンターでは `$this->getHttpResponse()` メソッドを呼ぶだけです。 → [インストールと要件 |@home#インストール] @@ -12,12 +12,12 @@ HTTP レスポンスは [api:Nette\Http\Response] オブジェクトによって Nette\Http\Response =================== -このオブジェクトは [Nette\Http\Request|request] とは異なり、ミュータブル(可変)です。つまり、セッターを使用して状態を変更できます。例えば、ヘッダーを送信するなどです。すべてのセッターは、**任意の出力が送信される前に**呼び出す必要があることを忘れないでください。出力が既に送信されたかどうかは `isSent()` メソッドが示します。`true` を返す場合、ヘッダーを送信しようとするたびに `Nette\InvalidStateException` 例外がスローされます。 +[Nette\Http\Request |request]と違って、このオブジェクトは変更できます。ですからセッターで状態を変えられ、たとえばヘッダーを送れます。すべてのセッターは、**実際の出力が送られる前に呼ばなければならない**ことを忘れないでください。出力がすでに送られたかどうかは `isSent()` メソッドが教えてくれます。それが `true` を返す場合、ヘッダーを送ろうとすると `Nette\InvalidStateException` が投げられます。 setCode(int $code, ?string $reason=null) .[method] -------------------------------------------------- -[レスポンスステータスコード|https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]を変更します。ソースコードの可読性を高めるために、コードには数値の代わりに[事前定義された定数|api:Nette\Http\IResponse]を使用することをお勧めします。 +[レスポンスの状態のコード |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]を変えます。ソースコードを読みやすくするために、実際の数ではなく[あらかじめ用意された定数 |api:Nette\Http\IResponse]を使うことをおすすめします。 ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -26,17 +26,17 @@ $httpResponse->setCode(Nette\Http\Response::S404_NotFound); getCode(): int .[method] ------------------------ -レスポンスのステータスコードを返します。 +レスポンスの状態のコードを返します。 isSent(): bool .[method] ------------------------ -ヘッダーがサーバーからブラウザに既に送信されたかどうかを返します。したがって、ヘッダーを送信したり、ステータスコードを変更したりすることはできなくなります。 +ヘッダーがすでにサーバーからブラウザへ送られたかどうかを返します。送られていれば、ヘッダーを送ることも状態のコードを変えることもできません。 -setHeader(string $name, string $value) .[method] ------------------------------------------------- -HTTP ヘッダーを送信し、以前に送信された同じ名前のヘッダーを**上書き**します。 +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +HTTP のヘッダーを送り、同じ名前の以前に送ったヘッダーを**上書きします**。`$value` が `null` なら、そのヘッダーは取り除かれます。 ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -HTTP ヘッダーを送信し、以前に送信された同じ名前のヘッダーを**上書きしません**。 +HTTP のヘッダーを送り、同じ名前の以前に送ったヘッダーを**上書きしません**。 ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -55,21 +55,21 @@ $httpResponse->addHeader('Accept', 'application/xml'); deleteHeader(string $name) .[method] ------------------------------------ -以前に送信された HTTP ヘッダーを削除します。 +以前に送った HTTP のヘッダーを消します。 getHeader(string $header): ?string .[method] -------------------------------------------- -送信された HTTP ヘッダーを返すか、存在しない場合は `null` を返します。パラメータは大文字と小文字を区別しません。 +送られた HTTP のヘッダーを返します。なければ `null` を返します。パラメータは大文字と小文字を区別しません。 ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ -送信されたすべての HTTP ヘッダーを連想配列として返します。 +getHeaders(): array<string, string> .[method] +--------------------------------------------- +送られたすべての HTTP のヘッダーを連想配列として返します。 ```php $headers = $httpResponse->getHeaders(); @@ -79,7 +79,7 @@ echo $headers['Pragma']; setContentType(string $type, ?string $charset=null) .[method] ------------------------------------------------------------- -`Content-Type` ヘッダーを変更します。 +`Content-Type` のヘッダーを変えます。 ```php $httpResponse->setContentType('text/plain', 'UTF-8'); @@ -88,7 +88,7 @@ $httpResponse->setContentType('text/plain', 'UTF-8'); redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- -別の URL にリダイレクトします。その後、スクリプトを終了することを忘れないでください。 +別の URL へリダイレクトします。そのあとスクリプトを終わらせるのを忘れないでください。 ```php $httpResponse->redirect('http://example.com'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- -`Cache-Control` および `Expires` ヘッダーを使用して HTTP ドキュメントの有効期限を設定します。パラメータは時間間隔(テキストとして)または `null` で、キャッシュを無効にします。 +setExpiration(?string $expire) .[method] +---------------------------------------- +`Cache-Control` と `Expires` のヘッダーで HTTP の文書の有効期限を設定します。パラメータは時間の間隔(文として)か、キャッシュを切る `null` です。 ```php -// ブラウザのキャッシュは1時間後に期限切れになります +// ブラウザのキャッシュは 1 時間で切れます $httpResponse->setExpiration('1 hour'); ``` sendAsFile(string $fileName) .[method] -------------------------------------- -レスポンスは、指定された名前で *名前を付けて保存* ダイアログボックスを使用してダウンロードされます。ファイル自体は送信しません。 +レスポンスは指定した名前で *名前を付けて保存* のダイアログを通じてダウンロードされます。ファイルそのものは送りません。 ```php $httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -クッキーを送信します。パラメータのデフォルト値: +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +クッキーを送ります。パラメータの既定値です。 -| `$path` | `'/'` | クッキーは(サブ)ドメイン内のすべてのパスに適用されます *(設定可能)* -| `$domain` | `null` | これは現在の(サブ)ドメインに適用されますが、そのサブドメインには適用されません *(設定可能)* -| `$secure` | `true` | ウェブサイトが HTTPS で実行されている場合、それ以外は `false` *(設定可能)* -| `$httpOnly` | `true` | クッキーは JavaScript からアクセスできません -| `$sameSite` | `'Lax'` | クッキーは[他のドメインからのアクセス |nette:glossary#SameSite cookie]時に送信されない場合があります +| `$path` | `'/'` | クッキーは(サブ)ドメインのすべてのパスで使えます *(設定できます)* +| `$domain` | `null` | つまり今の(サブ)ドメインでは使えますが、そのサブドメインでは使えません *(設定できます)* +| `$secure` | `auto` | サイトが HTTPS で動いていれば `true`、そうでなければ `false`(フレームワークの既定。クラス単体では `false`)*(設定できます)* +| `$httpOnly` | `true` | クッキーは JavaScript から触れません +| `$sameSite` | `'Lax'` | [別オリジンからのアクセス |nette:glossary#SameSite cookie]のときクッキーが送られないことがあります +| `$partitioned` | `false` | クッキーを分割するかどうか。下をご覧ください *(v3.4 以降)* -パラメータ `$path`、`$domain`、`$secure` のデフォルト値は[設定 |configuration#HTTP クッキー]で変更できます。 +`$path`、`$domain`、`$secure` のパラメータの既定値は[設定 |configuration#HTTP のクッキー]で変えられます。 -時間は秒数または文字列として指定できます: +有効期限は秒数、間隔や日付の文、あるいは `DateTimeInterface` オブジェクトとして渡します。値 `null` はセッションのクッキーを作り、ブラウザを閉じると捨てられます。Nette は有効期限を `Expires` と `Max-Age` の両方の属性で送ります。 ```php -$httpResponse->setCookie('lang', 'ja', '100 days'); +$httpResponse->setCookie('lang', 'en', '100 days'); // 100 日で切れます +$httpResponse->setCookie('lang', 'en', null); // セッションのクッキー ``` -`$domain` パラメータは、どのドメインがクッキーを受け入れることができるかを指定します。指定されていない場合、クッキーは設定したのと同じ(サブ)ドメインを受け入れますが、そのサブドメインは受け入れません。`$domain` が指定されている場合、サブドメインも含まれます。したがって、`$domain` を指定する方が、省略するよりも制限が緩くなります。例えば、`$domain = 'nette.org'` の場合、クッキーは `doc.nette.org` のようなすべてのサブドメインでも利用可能です。 +`$domain` のパラメータは、どのドメインがクッキーを受け取れるかを決めます。指定しなければ、クッキーはそれを設定したのと同じ(サブ)ドメインだけが受け取り、そのサブドメインは受け取りません。`$domain` を指定すると、サブドメインも含まれます。ですから `$domain` の指定は、省くよりも制限が緩くなります。たとえば `$domain = 'nette.org'` なら、クッキーは `doc.nette.org` のようなすべてのサブドメインでも使えます。 + +`$sameSite` の値は `Nette\Http\SameSite` の enum、つまり `SameSite::Lax`、`SameSite::Strict`、`SameSite::None` として渡せます(文字列の値 `'Lax'`、`'Strict'`、`'None'` も使えます)。`SameSite::None` にすると `$secure` の属性が自動的に有効になります。ブラウザは secure でない `SameSite=None` のクッキーを拒むからです。 + +.{data-version:3.4.0} +分割されたクッキー(CHIPS)は、最上位のサイトごとに自分だけの別の保管場所を持ちます。ですから第三者のサービス(埋め込まれたウィジェットなど)が分割されたクッキーを設定すると、ブラウザはそのウィジェットが現れるサイトごとに別々の複製を持ち、それらの複製はサイトをまたいだ追跡のために結び付けられません。有効にするには `$partitioned` を `true` にします。これには `$secure` の属性も要るので、自動的に有効になります。 -`$sameSite` の値には、定数 `Response::SameSiteLax`、`SameSiteStrict`、`SameSiteNone` を使用できます。 +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- -クッキーを削除します。パラメータのデフォルト値は次のとおりです: -- `$path` すべてのディレクトリに適用されます(`'/'`) -- `$domain` 現在の(サブ)ドメインに適用されますが、そのサブドメインには適用されません -- `$secure` は[設定 |configuration#HTTP クッキー]の設定に従います +クッキーを消します。パラメータの既定値は次のとおりです。 +- `$path` はすべてのディレクトリを範囲にします(`'/'`) +- `$domain` は今の(サブ)ドメインを範囲にし、そのサブドメインは含みません +- `$secure` は[設定 |configuration#HTTP のクッキー]の内容によります ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +[api:Nette\Http\Context]オブジェクトはリクエストとレスポンスを結び付け、HTTP のキャッシュを助けます。サービスとしては登録されていないので、自分で作ります。プレゼンターではふつう [lastModified() |application:presenters#HTTP キャッシュ]メソッドを使うほうが簡単です。context は、たとえば自分のレスポンスのクラスからのように、自分でレスポンスを送るときに役立ちます。 + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +クライアントが前に訪れてから内容が変わったかどうかを判断します。最後に変わった時刻を渡すと `Last-Modified` のヘッダーを送り、ETag の検証子(今の内容を表す短い文字列、たとえばそのハッシュ)を渡すと `ETag` のヘッダーを送ります。そしてそれらを、ブラウザが送ってきた `If-Modified-Since` と `If-None-Match` のヘッダーと比べます。 + +ブラウザがすでに合う版を持っているなら、このメソッドはコード `304 Not Modified` を設定して `false` を返します。その場合、レスポンスの本文はまったく送らないでください。そうでなければ `true` を返します。 + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +どちらのパラメータも省略できます。内容が変わった時刻が分からないなら ETag だけを使い、その逆も同じです。 diff --git a/http/ja/sessions.texy b/http/ja/sessions.texy index c1c8169880..5b0ff862ac 100644 --- a/http/ja/sessions.texy +++ b/http/ja/sessions.texy @@ -3,69 +3,69 @@ <div class=perex> -HTTP はステートレスプロトコルですが、ほぼすべてのアプリケーションはリクエスト間で状態を維持する必要があります。例えば、ショッピングカートの内容などです。セッションはまさにこの目的のために使用されます。ここでは、 +HTTP は状態を持たないプロトコルですが、ほとんどのアプリケーションはリクエストをまたいで状態を保つ必要があります。買い物かごの中身などです。まさにそのためにセッションを使います。ここでは次のことをお見せします。 -- セッションの使用方法 -- 名前の衝突を回避する方法 -- 有効期限の設定方法 +- セッションの使い方 +- 名前の衝突を防ぐ方法 +- 有効期限の決め方 </div> -セッションを使用する場合、各ユーザーはセッション ID と呼ばれる一意の識別子を受け取り、これはクッキーで渡されます。これはセッションデータのキーとして機能します。ブラウザ側に保存されるクッキーとは異なり、セッションデータはサーバー側に保存されます。 +セッションを使うと、それぞれの利用者はセッション ID と呼ばれる一意の識別子を受け取り、それがクッキーで運ばれます。これはセッションのデータへの鍵の役目を果たします。ブラウザ側に保存されるクッキーと違って、セッションのデータはサーバー側に保存されます。 -セッションは[設定 |configuration#セッション]で設定します。特に有効期限の選択が重要です。 +セッションは[設定 |configuration#セッション]で設定します。とりわけ有効期限の時間の選び方が大事です。 -セッション管理は [api:Nette\Http\Session] オブジェクトが担当します。これには、[依存性注入|dependency-injection:passing-dependencies] を使用して渡すことでアクセスできます。Presenter では、単に `$session = $this->getSession()` を呼び出すだけです。 +セッションの管理は [api:Nette\Http\Session]オブジェクトが受け持ちます。これは [dependency injection |dependency-injection:passing-dependencies]で渡してもらえます。プレゼンターでは `$session = $this->getSession()` を呼ぶだけです。 → [インストールと要件 |@home#インストール] -セッションの開始 -======== +セッションを始める +========= -Nette はデフォルト設定では、データの読み取りまたは書き込みを開始したときに自動的にセッションを開始します。手動でセッションを開始するには `$session->start()` を使用します。 +既定では、Nette はデータを読み書きしはじめた瞬間にセッションを自動的に始めます。セッションを手で始めるには `$session->start()` を使います。 -PHP はセッションの開始時にキャッシュに影響を与える HTTP ヘッダー([php:session_cache_limiter]参照)と、場合によってはセッション ID を含むクッキーを送信します。そのため、ブラウザに何らかの出力が送信される前に常にセッションを開始する必要があります。そうしないと例外がスローされます。したがって、ページのレンダリング中にセッションが使用されることがわかっている場合は、事前に手動で開始してください。例えば Presenter で。 +PHP はセッションを始めるときに、キャッシュに影響する HTTP のヘッダー([php:session_cache_limiter]をご覧ください)と、場合によってはセッション ID のクッキーを送ります。ですからセッションは、ブラウザへ何かを出力する前に必ず始めなければなりません。そうしないと例外が投げられます。ですからページを描いているあいだにセッションを使うと分かっているなら、たとえばプレゼンターの中で、前もって手で始めてください。 -開発モードでは、Tracy がセッションを開始します。これは、Tracy Bar でのリダイレクトや AJAX リクエストのバーを表示するために使用するためです。 +開発モードでは Tracy がセッションを始めます。Tracy Bar でリダイレクトと AJAX のリクエストの帯を表示するのにセッションを使うからです。 -セクション -===== +区画 +=== -純粋な PHP では、セッションデータストレージはグローバル変数 `$_SESSION` を介してアクセス可能な配列として実装されます。問題は、アプリケーションが通常、相互に独立した多数の部分で構成されており、すべてが 1 つの配列しか利用できない場合、遅かれ早かれ名前の衝突が発生することです。 +素の PHP では、セッションのデータの保管場所は大域変数 `$_SESSION` で触れる配列として作られています。困るのは、アプリケーションがふつう多くの独立した部分から成っていて、そのすべてがひとつの配列しか使えないなら、遅かれ早かれ名前の衝突が起きることです。 -Nette Framework は、スペース全体をセクション([api:Nette\Http\SessionSection] オブジェクト)に分割することでこの問題を解決します。各ユニットは一意の名前を持つ独自のセクションを使用するため、衝突は発生しません。 +Nette Framework はこの問題を、空間全体を区画([api:Nette\Http\SessionSection]のオブジェクト)に分けることで解決します。それぞれの部分は一意の名前を持つ自分の区画を使うので、衝突は起きません。 -セッションからセクションを取得します: +区画はセッションから取り出します。 ```php -$section = $session->getSection('unique_name'); +$section = $session->getSection('unique name'); ``` -Presenter では、パラメータ付きで `getSession()` を使用するだけです: +プレゼンターでは `getSession()` にパラメータを渡すだけです。 ```php -// $this は Presenter です -$section = $this->getSession('unique_name'); +// $this はプレゼンターです +$section = $this->getSession('unique name'); ``` -セクションの存在は `$session->hasSection('unique_name')` メソッドで確認できます。 +区画があるかどうかは `$session->hasSection('unique name')` メソッドで調べられます。存在するすべての区画の名前の一覧は `$session->getSectionNames()` が返します。 -セクション自体は、`set()`、`get()`、`remove()` メソッドを使用して非常に簡単に操作できます: +区画そのものを扱うのは、`set()`、`get()`、`remove()` のメソッドでとても簡単です。 ```php -// 変数の書き込み +// 変数を書き込みます $section->set('userName', 'john'); -// 変数の読み取り、存在しない場合は null を返します +// 変数を読み出します。なければ null を返します echo $section->get('userName'); -// 変数の削除 +// 変数を取り除きます $section->remove('userName'); ``` -セクションからすべての変数を取得するには、`foreach` ループを使用できます: +区画のすべての変数を得るには `foreach` のループを使えます。 ```php foreach ($section as $key => $val) { @@ -74,33 +74,33 @@ foreach ($section as $key => $val) { ``` -有効期限の設定 -------- +有効期限の決め方 +-------- -個々のセクション、または個々の変数に対して有効期限を設定できます。これにより、ユーザーのログインを 20 分後に期限切れにすることができますが、カートの内容は引き続き記憶されます。 +有効期限は区画ごとに、さらには変数ごとに決められます。利用者のログインを 20 分で切らしつつ、買い物かごの中身は覚えておくことができます。 ```php -// セクションは 20 分後に期限切れになります +// この区画は 20 分で切れます $section->setExpiration('20 minutes'); ``` -個々の変数の有効期限を設定するには、`set()` メソッドの 3 番目のパラメータを使用します: +変数ごとに有効期限を決めるには、`set()` メソッドの第 3 パラメータを使います。 ```php -// 変数 'flash' は 30 秒後に期限切れになります +// 変数 'flash' は 30 秒で切れます $section->set('flash', $message, '30 seconds'); ``` .[note] -セッション全体の有効期限([セッション設定 |configuration#セッション]参照)は、個々のセクションまたは変数に設定された時間と同じかそれ以上でなければならないことを忘れないでください。 +セッション全体の有効期限([セッションの設定 |configuration#セッション]をご覧ください)は、区画や変数ごとに決めた時間と同じか、それより長くなければならないことを忘れないでください。 -以前に設定された有効期限をキャンセルするには `removeExpiration()` メソッドを使用します。セクション全体を即座にキャンセルするには `remove()` メソッドを使用します。 +前に決めた有効期限を取り消すには `removeExpiration()` メソッドを使います。特定の変数の有効期限を消すには、その名前を渡します。`removeExpiration('flash')` のようにです。区画全体をすぐに取り除くには `remove()` メソッドを使います。 -イベント $onStart, $onBeforeWrite +$onStart、$onBeforeWrite のイベント ----------------------------- -`Nette\Http\Session` オブジェクトには[イベント |nette:glossary#イベント] `$onStart` と `$onBeforeWrite` があります。したがって、セッションの開始後またはディスクへの書き込みとそれに続く終了前に呼び出されるコールバックを追加できます。 +`Nette\Http\Session` オブジェクトには[イベント |nette:glossary#イベント] `$onStart` と `$onBeforeWrite` があるので、セッションが始まったあとや、ディスクに書き込まれて終わる前に呼ばれるコールバックを足せます。 ```php $session->onBeforeWrite[] = function () { @@ -110,42 +110,42 @@ $session->onBeforeWrite[] = function () { ``` -セッション管理 -======= +セッションの管理 +======== -セッション管理のための `Nette\Http\Session` クラスのメソッドの概要: +セッションを管理する `Nette\Http\Session` クラスのメソッドの一覧です。 <div class=wiki-methods-brief> start(): void .[method] ----------------------- -セッションを開始します。 +セッションを始めます。 isStarted(): bool .[method] --------------------------- -セッションは開始されていますか? +セッションは始まっていますか。 close(): void .[method] ----------------------- -セッションを終了します。セッションはスクリプトの実行終了時に自動的に終了します。 +セッションを終わらせます。セッションはスクリプトの実行の終わりに自動的に終わります。 destroy(): void .[method] ------------------------- -セッションを終了して削除します。 +セッションを終わらせて消します。 exists(): bool .[method] ------------------------ -HTTP リクエストにセッション ID を含むクッキーが含まれていますか? +HTTP のリクエストにセッション ID のクッキーが入っていますか。 regenerateId(): void .[method] ------------------------------ -新しいランダムなセッション ID を生成します。データは保持されます。 +新しい無作為なセッション ID を生成します。データはそのまま残ります。 getId(): string .[method] @@ -156,56 +156,56 @@ getId(): string .[method] 設定 ------------ +--- -セッションは[設定 |configuration#セッション]で設定します。DI コンテナを使用しないアプリケーションを作成している場合は、これらのメソッドを使用して設定します。これらはセッションを開始する前に呼び出す必要があります。 +セッションは[設定 |configuration#セッション]で設定します。DI コンテナを使わないアプリケーションを書いているなら、設定にはこれらのメソッドを使います。セッションを始める前に呼ばなければなりません。 <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -セッション ID が転送されるクッキーの名前を設定します。標準の名前は `PHPSESSID` です。これは、1 つのウェブサイト内で複数の異なるアプリケーションを運用する場合に便利です。 +セッション ID を運ぶクッキーの名前を設定します。標準の名前は `PHPSESSID` です。同じウェブサイトでいくつかの違うアプリケーションを動かしているときに役立ちます。 getName(): string .[method] --------------------------- -セッション ID が転送されるクッキーの名前を返します。 +セッション ID を運ぶクッキーの名前を返します。 setOptions(array $options): static .[method] -------------------------------------------- -セッションを設定します。すべての PHP [セッションディレクティブ|https://www.php.net/manual/en/session.configuration.php](camelCase 形式、例:`session.save_path` の代わりに `savePath` と記述)と [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] を設定できます。 +セッションを設定します。PHP のすべての[セッションのディレクティブ |https://www.php.net/manual/en/session.configuration.php](camelCase の形で。たとえば `session.save_path` は `savePath` と書きます)と [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]を設定できます。 -setExpiration(?string $time): static .[method] ----------------------------------------------- -セッションが期限切れになるまでの非アクティブ期間を設定します。 +setExpiration(?string $expire): static .[method] +------------------------------------------------ +セッションが切れるまでの無操作の時間を設定します。 -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -クッキーのパラメータを設定します。パラメータのデフォルト値は[設定 |configuration#セッションクッキー]で変更できます。 +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +クッキーのパラメータを設定します。パラメータの既定値は[設定 |configuration#セッションのクッキー]で変えられます。 setSavePath(string $path): static .[method] ------------------------------------------- -セッションファイルが保存されるディレクトリを設定します。 +セッションのファイルを保存するディレクトリを設定します。 setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -カスタムハンドラを設定します。[PHP ドキュメント|https://www.php.net/manual/en/class.sessionhandlerinterface.php]を参照してください。 +独自のハンドラを設定します。[PHP のドキュメント |https://www.php.net/manual/en/class.sessionhandlerinterface.php]をご覧ください。 </div> -セキュリティ第一 -======== +安全第一 +==== -サーバーは、リクエストが同じセッション ID を伴う限り、常に同じユーザーと通信していると想定します。セキュリティメカニズムのタスクは、これが実際にそうであり、識別子を盗んだり偽装したりできないことを保証することです。 +サーバーは、リクエストに同じセッション ID が伴っている限り、同じ利用者とやり取りしていると考えます。安全のしくみの仕事は、それが本当にそのとおりであり、識別子が盗まれたりすり替えられたりしないようにすることです。 -したがって、Nette Framework は PHP ディレクティブを正しく設定し、セッション ID をクッキーでのみ転送し、JavaScript からアクセスできないようにし、URL 内の識別子を無視します。さらに、ユーザーのログインなどの重要な瞬間には、新しいセッション ID を生成します。 +ですから Nette Framework は PHP のディレクティブを正しく設定し、セッション ID をクッキーだけで運び、JavaScript から触れないようにし、URL の中の識別子は無視します。さらに利用者のログインのような大事な瞬間には、新しいセッション ID を生成します。 .[note] -PHP の設定には ini_set 関数が使用されますが、残念ながら一部のホスティングプロバイダーはこの関数を禁止しています。これがあなたのホスティングプロバイダーの場合、関数を有効にするか、少なくともサーバーを設定するように交渉してみてください。 +PHP の設定には `ini_set` 関数を使いますが、残念ながらホスティングによってはその使用を禁じています。あなたのホスティングがそうなら、その関数を使えるようにしてもらうか、少なくともサーバーをきちんと設定してもらえるよう相談してみてください。 diff --git a/http/ja/ssrf.texy b/http/ja/ssrf.texy new file mode 100644 index 0000000000..d1fcda88e9 --- /dev/null +++ b/http/ja/ssrf.texy @@ -0,0 +1,183 @@ +SSRF 対策 +******* + +.[perex] +アプリケーションが利用者から渡された URL をダウンロードするとき、攻撃者はそれを悪用してあなたの内部のネットワークに手を伸ばせます。[#UrlValidator]と [#IPAddress]のクラスは、こうした Server-Side Request Forgery(SSRF)の攻撃から守るのを助けます。 + +→ [インストールと要件 |@home#インストール] + + +SSRF とは何か +========= + +利用者が URL を入力すると、あなたのサーバーがそれをダウンロードする機能を思い浮かべてください。遠くのアドレスからのアバター、webhook の宛先、リンクのプレビューなどです。害はなさそうに見えますが、そのアドレスに手を伸ばすのは利用者のブラウザではなくサーバーです。そしてサーバーは、攻撃者には見えない場所を見られます。ループバックのインターフェース、私設のネットワーク、クラウドのサービスです。 + +ですから攻撃者は、公のインターネットではなく内側を指す URL を送ります。よくある標的は次のとおりです。 + +- `http://169.254.169.254/` のクラウドのメタデータ。アクセスの鍵が漏れることがあります +- `http://192.168.1.1/` のような内部の管理画面やルーター +- `http://localhost:6379/` の Redis のような、認証のないサービス + +この種の弱点はとてもよくあるもので、[OWASP Top 10 |https://owasp.org/Top10/]にも数えられています。守り方は、取りに行く**前に** URL を検証し、公でないアドレスに解決されるものはすべて拒むことです。 + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator]は、URL を設定できる方針と照らし合わせます。スキーム、ポート、ホスト、userinfo、そしてそのホストが解決される IP アドレスです。基本の使い方は 1 回の呼び出しです。 + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // 安全でない URL なので取りに行きません +} +``` + +既定の方針は意図して厳しく、ポート 443 の `https` で公の IP アドレスを指すものだけを受け付けます。そのほか(ループバック、私設の範囲、クラウドのメタデータを含むリンクローカル、予約された範囲)はすべて拒まれ、マルチキャストは無条件に拒まれます。利用者から渡される任意の URL を取りに行くなら、これが正しい出発点です。 + + +方針の設定 +----- + +方針はコンストラクタで形づくります。たとえば素の `http` をどのポートでも許し、私設のアドレスにも届くようにするなら(信頼できるネットワークの中で役立ちます)次のようにします。 + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // どのポートでも + allowPrivateIps: true, +); +``` + +よくあるやり方は、ホストの許可の一覧を使って、取りに行く先を決まった提携先のドメインだけに絞ることです。`*.` の接頭辞はどんな深さのサブドメインにも合いますが、頂点のドメインには合いません。必要なら両方を並べてください。 + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +コンストラクタのオプションの一覧です。 + +| パラメータ | 既定 | 意味 +|--------------------- +| `schemes` | `['https']` | 許されるスキーム。`[]` はすべてを拒みます +| `ports` | `[443]` | 許されるポート。`null` はどれでも。スキームから決まる暗黙のポートも認められます +| `allowPrivateIps` | `false` | 私設の範囲を許します(10/8、172.16/12、192.168/16、fc00::/7) +| `allowLoopback` | `false` | ループバックを許します(127.0.0.0/8、::1) +| `allowLinkLocal` | `false` | クラウドのメタデータ 169.254.169.254 を含むリンクローカルを許します +| `allowReserved` | `false` | IANA が予約した範囲を許します +| `allowUserinfo` | `false` | URL の中の `user:pass@` を許します +| `hostAllowlist` | `null` | 設定すると、ホストはひとつの形に合わなければなりません。`[]` はすべてを拒みます +| `hostBlocklist` | `null` | 設定すると、ホストはどの形にも合ってはいけません + + +検証のメソッド +------- + +この検証器には 3 つのメソッドがあります。`allows()` は DNS の解決も含めた完全な確認を行います。ホストが解決され、**すべての** A/AAAA のアドレスが IP の方針を通らなければなりません。 + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` は DNS の解決と IP の範囲の確認を飛ばします。速い事前の絞り込みとして、あるいは DNS の検証を取得の層に任せている場合に使ってください。 + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +どちらのメソッドも、文字列、[UrlImmutable |urls#UrlImmutable]オブジェクト、`null`(これはいつも失敗します)を受け取ります。 + + +DNS リバインディングを封じる +---------------- + +検証と取得のあいだには微妙な競争があります。攻撃者はホストを検証するときには安全な IP を返し、実際のダウンロードのときには DNS を内部の IP に切り替えられます。この穴をふさぐために `getResolvedIPs()` は検証を通った IP アドレスを返すので、接続をそれらに固定すれば、取得がよそへ向け直されることはありません。 + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // 安全でない URL +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... リクエストを実行します +``` + +このメソッドは、完全な方針を通った IP の文字列の配列(先に A レコード、次に AAAA)を返します。何かに失敗すると空の配列を返します。URL の中に IP そのものが書かれている場合は、そのアドレスを直接検証し、DNS の問い合わせは行いません。 + + +IPAddress +========= + +[api:Nette\Http\IPAddress]は、IPv4 と IPv6 のアドレスを扱う変更できない値のオブジェクトです。`UrlValidator` が内部で使っていますが、アドレスを分類するときにはそれ自体でも便利です。コンストラクタは正しくないアドレスに対して `Nette\InvalidArgumentException` を投げます。 + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +例外が欲しくないなら、`tryFrom()` のファクトリか `isValid()` の確認を使います。 + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +アドレスの分類 +------- + +これらの述語は、アドレスがどの種類に属するかを教えてくれます。大事なのは `isPublic()` で、公に経路のあるアドレスにだけ true になります。これはまさに SSRF の守り手が求めるものです。 + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true(クラウドのメタデータの範囲) +``` + +述語の一覧です。 + +| メソッド | 調べる対象 +|-------------------- +| `isPublic()` | 公に経路がある(下のどれでもない) +| `isPrivate()` | RFC 1918 / 4193 の私設の範囲 +| `isLoopback()` | 127.0.0.0/8、::1 +| `isLinkLocal()` | 169.254.0.0/16(クラウドのメタデータを含む)、fe80::/10 +| `isMulticast()` | 224.0.0.0/4、ff00::/8 +| `isReserved()` | IANA が予約したもの(文書用、CGNAT、将来のための予約など) + + +範囲に含まれるか +-------- + +`isInRange()` は、そのアドレスが CIDR のブロックに入るかを調べます。接頭辞の付いたネットワークも、ちょうど一致させるための裸のアドレスも渡せます(IPv4 なら暗黙の /32、IPv6 なら /128)。 + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false(ちょうどの一致) +``` + +形の崩れた入力や、違う IP の系統は `false` を返します。 + + +IPv4 を写した IPv6 +-------------- + +IPv4 を写した IPv6(`::ffff:127.0.0.1` など)として書かれたアドレスは、素朴なフィルタをすり抜ける古典的な手です。`IPAddress` はそれを整えるので、範囲の述語はその変装を見抜きます。 + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +`isIPv4()` と `isIPv6()` のメソッドは文字としての形を伝えます。写されたアドレスは IPv4 ではなく IPv6 です。 diff --git a/http/ja/upgrading.texy b/http/ja/upgrading.texy new file mode 100644 index 0000000000..277a85d1b2 --- /dev/null +++ b/http/ja/upgrading.texy @@ -0,0 +1,54 @@ +アップグレード +******* + + +バージョン 3.4 へのアップグレード +=================== + +必要な PHP の最小のバージョンは 8.3 です。 + +- `Request::isSameSite()` メソッドは非推奨になり、`Sec-Fetch-*` のヘッダーからリクエストの出どころを判断する `isFrom()` に取って代わられました。フォームとシグナルの自動の保護はより正確になり、振る舞いがひとつ変わります。直接の移動(ブックマーク、手で打ったアドレス、メールの中のリンク)は、もう同一サイトとは見なされません。シグナルがメールの中の操作のリンクに頼っているなら、`#[Requires(sameOrigin: false)]` で印を付けてください。 +- `_nss` のクッキーは、`Sec-Fetch-Site` のヘッダーを送らないブラウザにだけ送られるようになりました +- `setCookie()` は `Max-Age` の属性を送り、`SameSite=None` と分割されたクッキーには `Secure` のフラグを強制します +- enum の `SameSite` が定数 `IResponse::SameSiteLax` などに取って代わり、それらは非推奨になりました +- 有効期限はどこでも同じように解釈されます。数は相対の秒数、文字列は間隔か日付です。絶対の UNIX タイムスタンプを渡すのは非推奨で、セッションのクッキーは `0` ではなく `null` で表します。 +- 非推奨のメソッド `Request::getRemoteHost()` は `null` を返します +- ずっと前から非推奨だったクラス `Nette\Http\UserStorage` は取り除かれました + +`Sec-Fetch-*` のヘッダーへ切り替えるまでの物語は、記事 [CSRF の四半世紀 |https://blog.nette.org/en/quarter-century-of-csrf]で語られています。 + + +バージョン 3.2 へのアップグレード +=================== + +- HTTP Basic 認証の資格情報は `Url` オブジェクトの一部ではなくなったので、`$url->getUser()` と `$url->getPassword()` は空の文字列を返します。新しいメソッド `$request->getBasicCredentials()` で読んでください。 + +この変更の理由は、記事 [Nette Http 3.2: 資格情報へのアクセスの変更 |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]で説明されています。 + + +バージョン 3.1 へのアップグレード +=================== + +- クッキーは `sameSite: Lax` のフラグ付きで送られます +- `cookieSecure` の既定値は 'auto' になりました +- `session.cookieSecure` のオプションは非推奨で、代わりに `http.cookieSecure` が使われます +- `nette-samesite` のクッキーは `_nss` に改名されました +- `Nette\Http\Request::getFile()` はキーの配列を受け取り、`FileUpload|null` を返します +- `Nette\Http\Session::getCookieParameters()` は非推奨です +- `Nette\Http\FileUpload::getName()` は `getUntrustedName()` に改名されました +- `Nette\Http\Url`: `getBasePath()`、`getBaseUrl()`、`getRelativeUrl()` は非推奨です(これらのメソッドは `UrlScript` の一部です) +- `Nette\Http\Response::$cookieHttpOnly` は非推奨です +- `Nette\Http\FileUpload::getImageSize()` は組 `[幅, 高さ]` を返します +- `autoStart: smart`(既定)では、ブラウザがセッションのクッキーを送ったというだけで、アプリケーションの開始直後にセッションが始まることはもうありません。最初の読み書きのときに始まります。値 `always` と `never` が足されました。 +- ブラウザが、存在しないセッションの ID を送ってきた場合、Nette は新しいセッションを作る代わりにそのクッキーを消します +- セッションの区画へのアクセスには `set()`、`get()`、`remove()` のメソッドを使ってください。プロパティへのアクセスと違って、読みと書きを正しく見分け、必要もないのにセッションを始めません +- `cookiePath` と `cookieDomain` の既定値は設定で決められます + +セッションの振る舞いは、記事 [Nette Http 3.1: ずっと賢くなったセッション |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions]で詳しく説明されています。 + + +バージョン 3.0 へのアップグレード +=================== + +- `Nette\Http\UrlScript` のオブジェクト(たとえば `Nette\Http\Request::getUrl()` が返すもの)は変更できなくなりました +- `new Nette\Http\Url('abcd')` では `abcd` はドメインではなくパスを表します。3.0 からは `(new Nette\Http\Url('abcd'))->setScheme('http')` が、以前の `http://abcd` ではなく正しく `http:abcd` を生成します diff --git a/http/ja/urls.texy b/http/ja/urls.texy index 0aa3600224..d5b383ca3f 100644 --- a/http/ja/urls.texy +++ b/http/ja/urls.texy @@ -2,7 +2,7 @@ URL の操作 ******* .[perex] -クラス [#Url]、[#UrlImmutable]、[#UrlScript] を使用すると、URL の生成、解析、操作が簡単になります。 +[#Url]、[#UrlImmutable]、[#UrlScript]のクラスは、URL の生成、解析、加工を簡単にします。 → [インストールと要件 |@home#インストール] @@ -10,7 +10,7 @@ URL の操作 Url === -[api:Nette\Http\Url] クラスを使用すると、URL とその個々のコンポーネントを簡単に操作できます。これらは次の図で示されています: +[api:Nette\Http\Url]クラスは、URL とその個々の部分を簡単に扱えるようにします。図のとおりです。 /--pre scheme user password host port path query fragment @@ -22,7 +22,7 @@ Url hostUrl authority \-- -URL の生成は直感的です: +URL の生成は直感的です。 ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -URL を解析してさらに操作することもできます: +URL を解析してから加工することもできます。 ```php $url = new Url( @@ -44,7 +44,7 @@ $url = new Url( ); ``` -`Url` クラスは `JsonSerializable` インターフェースを実装し、`__toString()` メソッドを持っているため、オブジェクトを出力したり、`json_encode()` に渡されるデータで使用したりできます。 +`Url` クラスは `JsonSerializable` インターフェースを実装し、`__toString()` メソッドを持つので、このオブジェクトはそのまま出力できますし、`json_encode()` に渡すデータの中でも使えます。 ```php echo $url; @@ -52,10 +52,10 @@ echo json_encode([$url]); ``` -URL コンポーネント .[method] ---------------------- +URL の部分 +------- -URL の個々のコンポーネントを返すか変更するには、次のメソッドを使用できます: +URL の個々の部分を取り出したり変えたりするのに、次のメソッドが使えます。 .[language-php] | セッター | ゲッター | 返される値 @@ -71,22 +71,25 @@ URL の個々のコンポーネントを返すか変更するには、次のメ | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | 完全な URL +| | `getAbsoluteUrl(): string` | URL 全体 -注意:[HTTP リクエスト|request]から取得した URL を操作する場合、ブラウザはフラグメントをサーバーに送信しないため、フラグメントは含まれないことに注意してください。 +`getUser()`、`getPassword()`、`setUser()`、`setPassword()` のメソッドは非推奨です。資格情報を URL に直接埋め込むことは勧められないからです。 -個々のクエリパラメータも次のように操作できます: +注意: [HTTP のリクエスト |request]から得た URL を扱うときは、ブラウザがフラグメントをサーバーへ送らないので、そこにフラグメントは入っていないことを忘れないでください。 + +個々のクエリのパラメータも次のもので扱えます。 .[language-php] | セッター | ゲッター |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] ------------------------------------------- -ホストの右側または左側の部分を返します。ホストが `www.nette.org` の場合、次のように機能します: +ホストの右側または左側の部分を返します。ホストが `www.nette.org` のときの働きは次のとおりです。 .[language-php] | `getDomain(1)` | `'org'` @@ -98,18 +101,23 @@ getDomain(int $level = 2): string .[method] | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -2 つの URL が同じかどうかを確認します。 +isEqual(string|Url $url): bool .[method] +---------------------------------------- +2 つの URL が同一かを調べます。 ```php $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +URL を正式な形に変えます。ホスト名を小文字にし、パスを整えます(パーセントの符号化と余計な文字の除去)。クエリの文字列はそのままです。 + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- -URL が絶対 URL かどうかを確認します。URL は、スキーム(例:http、https、ftp)で始まり、その後にコロンが続く場合に絶対 URL と見なされます。 +URL が絶対かを調べます。URL はスキーム(たとえば http、https、ftp)とそれに続くコロンで始まるとき、絶対と見なされます。 ```php Url::isAbsolute('https://nette.org'); // true @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -特別なセグメント `.` と `..` を削除して URL のパスを正規化します。このメソッドは、Web ブラウザと同じ方法で余分なパス要素を削除します。 +特別な部分 `.` と `..` を取り除いて URL のパスを整えます。このメソッドは、ウェブブラウザと同じやり方で余計なパスの要素を取り除きます。 ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,24 +139,24 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -[api:Nette\Http\UrlImmutable] クラスは、[#Url] クラスのイミュータブル(不変)な代替です(PHP の `DateTimeImmutable` が `DateTime` の不変な代替であるのと同様です)。セッターの代わりに、オブジェクトを変更せず、変更された値を持つ新しいインスタンスを返すウィザーがあります: +[api:Nette\Http\UrlImmutable]クラスは [#Url]クラスの、変更できない側の選択肢です(PHP で `DateTimeImmutable` が `DateTime` の変更できない側の選択肢であるのと同じです)。セッターの代わりに wither があり、それはオブジェクトを変えずに、値を変えた新しいインスタンスを返します。 ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` -`UrlImmutable` クラスは `JsonSerializable` インターフェースを実装し、`__toString()` メソッドを持っているため、オブジェクトを出力したり、`json_encode()` に渡されるデータで使用したりできます。 +`UrlImmutable` クラスは `JsonSerializable` インターフェースを実装し、`__toString()` メソッドを持つので、このオブジェクトはそのまま出力できますし、`json_encode()` に渡すデータの中でも使えます。 ```php echo $url; @@ -156,13 +164,13 @@ echo json_encode([$url]); ``` -URL コンポーネント .[method] ---------------------- +URL の部分 +------- -URL の個々のコンポーネントを返すか変更するには、次のメソッドを使用します: +URL の個々の部分を取り出したり変えたりするのに、次のメソッドが使えます。 .[language-php] -| ウィザー | ゲッター | 返される値 +| Wither | ゲッター | 返される値 |-------------------------------------------------------------------------------------------- | `withScheme(string $scheme)` | `getScheme(): string` | `'http'` | `withUser(string $user)` | `getUser(): string` | `'john'` @@ -175,14 +183,14 @@ URL の個々のコンポーネントを返すか変更するには、次のメ | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | 完全な URL +| | `getAbsoluteUrl(): string` | URL 全体 -`withoutUserInfo()` メソッドは `user` と `password` を削除します。 +`getUser()`、`getPassword()`、`withUser()`、`withPassword()`、`withoutUserInfo()` のメソッドは非推奨です。資格情報を URL に直接埋め込むことは勧められないからです。 -個々のクエリパラメータも次のように操作できます: +個々のクエリのパラメータも次のもので扱えます。 .[language-php] -| ウィザー | ゲッター +| Wither | ゲッター |----------------------------------------------- | `withQuery(string\|array $query)` | `getQueryParameters(): array` | `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` @@ -190,7 +198,7 @@ URL の個々のコンポーネントを返すか変更するには、次のメ getDomain(int $level = 2): string .[method] ------------------------------------------- -ホストの右側または左側の部分を返します。ホストが `www.nette.org` の場合、次のように機能します: +ホストの右側または左側の部分を返します。ホストが `www.nette.org` のときの働きは次のとおりです。 .[language-php] | `getDomain(1)` | `'org'` @@ -204,11 +212,11 @@ getDomain(int $level = 2): string .[method] resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -ブラウザが HTML ページ上のリンクを処理するのと同じ方法で絶対 URL を導出します: -- リンクが絶対 URL(スキームを含む)の場合、変更せずに使用されます -- リンクが `//` で始まる場合、現在の URL からスキームのみが引き継がれます -- リンクが `/` で始まる場合、ドメインのルートからの絶対パスが作成されます -- その他の場合、URL は現在のパスに対して相対的に構築されます +ブラウザが HTML のページのリンクを処理するのと同じやり方で、絶対 URL を解決します。 +- リンクが絶対 URL(スキームを含む)なら、そのまま使われます +- リンクが `//` で始まるなら、今の URL のスキームだけが受け継がれます +- リンクが `/` で始まるなら、ドメインの根からの絶対パスが作られます +- そのほかの場合、URL は今のパスからの相対で組み立てられます ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,9 +226,9 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -2 つの URL が同じかどうかを確認します。 +isEqual(string|Url $url): bool .[method] +---------------------------------------- +2 つの URL が同一かを調べます。 ```php $url->isEqual('https://nette.org'); @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -[api:Nette\Http\UrlScript] クラスは [#UrlImmutable] の子孫であり、プロジェクトのルートディレクトリなどの追加の仮想 URL コンポーネントで拡張します。親クラスと同様に、イミュータブル(不変)なオブジェクトです。 +[api:Nette\Http\UrlScript]クラスは [#UrlImmutable]の子孫で、プロジェクトの根のディレクトリなど、仮想的な URL の部分をさらに足します。親のクラスと同じく、変更できないオブジェクトです。 -次の図は、UrlScript が認識するコンポーネントを示しています: +次の図は UrlScript が見分ける部分を示します。 /--pre baseUrl basePath relativePath relativeUrl @@ -244,14 +252,14 @@ UrlScript scriptPath pathInfo \-- -- `baseUrl` は、ドメインとアプリケーションのルートディレクトリへのパスの一部を含む、アプリケーションのベース URL アドレスです -- `basePath` は、アプリケーションのルートディレクトリへのパスの一部です -- `scriptPath` は、現在のスクリプトへのパスです -- `relativePath` は、basePath に対して相対的なスクリプトの名前(および場合によっては追加のパスセグメント)です -- `relativeUrl` は、クエリ文字列とフラグメントを含む、baseUrl の後の URL の全体の部分です。 -- `pathInfo` は、今日ではあまり使用されない、スクリプト名の後の URL の部分です +- `baseUrl` はアプリケーションの基本の URL で、ドメインとアプリケーションの根のディレクトリまでのパスの部分を含みます +- `basePath` はアプリケーションの根のディレクトリまでのパスの部分です +- `scriptPath` は今のスクリプトへのパスです +- `relativePath` は `basePath` からの相対でのスクリプトの名前(とそれに続くパスの部分)です +- `relativeUrl` は `baseUrl` のあとの URL の部分すべてで、クエリの文字列とフラグメントも含みます +- `pathInfo` は今ではめったに使われない、スクリプトの名前のあとの URL の部分です -URL の部分を返すには、次のメソッドを使用できます: +これらの URL の部分を取り出すのに、次のメソッドが使えます。 .[language-php] | ゲッター | 返される値 @@ -259,8 +267,8 @@ URL の部分を返すには、次のメソッドを使用できます: | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -`UrlScript` オブジェクトは通常、直接作成しませんが、現在の HTTP リクエストに対して正しく設定されたコンポーネントを持つ [Nette\Http\Request::getUrl()|request] メソッドによって返されます。 +ふつう `UrlScript` オブジェクトを自分で作ることはありません。代わりに [Nette\Http\Request::getUrl() |request]メソッドが、今の HTTP のリクエストに合わせて各部分が正しく設定されたものを返します。 diff --git a/http/meta.json b/http/meta.json index 6422f8f332..3e9d3e56d5 100644 --- a/http/meta.json +++ b/http/meta.json @@ -1,5 +1,6 @@ { - "version": "4.0", + "version": "4.x", "repo": "nette/http", - "composer": "nette/http" + "composer": "nette/http", + "api": "https://api.nette.org/http/" } diff --git a/http/pl/@home.texy b/http/pl/@home.texy index bb86d6830c..28dded6dcf 100644 --- a/http/pl/@home.texy +++ b/http/pl/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -Pakiet `nette/http` enkapsuluje [Żądanie HTTP|request] & [Odpowiedź HTTP|response], pracę z [sesjami |sessions] oraz [parsowanie i składanie URL |urls]. +Pakiet `nette/http` to Twój towarzysz w całej komunikacji HTTP. Udostępnia przejrzyste, obiektowe API nad przychodzącym żądaniem i wychodzącą odpowiedzią, upraszcza pracę z sesjami i adresami URL, a w dodatku dba o bezpieczeństwo. Oto, co w nim znajdziesz: + +| [Żądanie HTTP |request] | przychodzące żądanie i oczyszczanie wejść +| [Odpowiedź HTTP |response] | wychodząca odpowiedź, nagłówki i cookies +| [Sesje|sessions] | bezpieczne utrzymywanie stanu między żądaniami +| [Narzędzia URL |urls] | parsowanie i budowanie adresów URL +| [Ochrona przed SSRF |ssrf] | obrona przed atakami Server-Side Request Forgery +| [Konfiguracja|configuration] | opcje konfiguracyjne pakietu Instalacja ---------- -Bibliotekę pobierzesz i zainstalujesz za pomocą narzędzia [Composer|best-practices:composer]: +Pobierz i zainstaluj pakiet za pomocą [Composera|best-practices:composer]: ```shell composer require nette/http ``` + +Pakiet wymaga PHP w wersji od 8.3 do 8.5. diff --git a/http/pl/@left-menu.texy b/http/pl/@left-menu.texy index 6022278eab..7208497e78 100644 --- a/http/pl/@left-menu.texy +++ b/http/pl/@left-menu.texy @@ -1,8 +1,19 @@ Nette HTTP ********** -- [Wprowadzenie |@home] -- [Żądanie HTTP|request] -- [Odpowiedź HTTP|response] -- [Sesje |Sessions] -- [Narzędzia URL |urls] -- [Konfiguracja |configuration] +- [Przegląd |@home] +- [Żądanie HTTP |request] +- [Odpowiedź HTTP |response] +- [Sesje|sessions] +- [Praca z adresami URL |urls] +- [Ochrona przed SSRF |ssrf] +- [Konfiguracja|configuration] +- [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/http/pl/configuration.texy b/http/pl/configuration.texy index 5ef5f5a7e8..f801315040 100644 --- a/http/pl/configuration.texy +++ b/http/pl/configuration.texy @@ -4,7 +4,7 @@ Konfiguracja HTTP .[perex] Przegląd opcji konfiguracyjnych dla Nette HTTP. -Jeśli nie używasz całego frameworka, ale tylko tej biblioteki, przeczytaj, [jak wczytać konfigurację|bootstrap:]. +Jeśli nie używasz całego frameworku, tylko tej biblioteki, przeczytaj, [jak wczytać konfigurację|bootstrap:]. Nagłówki HTTP @@ -12,29 +12,31 @@ Nagłówki HTTP ```neon http: - # nagłówki, które są wysyłane z każdym żądaniem + # nagłówki wysyłane z każdą odpowiedzią headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' # wpływa na nagłówek X-Frame-Options - frames: ... # (string|bool) domyślnie 'SAMEORIGIN' + frames: ... # (string|bool|null) domyślnie 'SAMEORIGIN' ``` -Framework ze względów bezpieczeństwa wysyła nagłówek `X-Frame-Options: SAMEORIGIN`, który mówi, że stronę można wyświetlić wewnątrz innej strony (w elemencie `<iframe>`) tylko wtedy, gdy znajduje się ona na tej samej domenie. Może to być w niektórych sytuacjach niepożądane (na przykład jeśli tworzysz aplikację dla Facebooka), zachowanie można zatem zmienić, ustawiając `frames: http://allowed-host.com` lub `frames: true`. +Ze względów bezpieczeństwa framework wysyła nagłówek `X-Frame-Options: SAMEORIGIN`, który mówi, że stronę można wyświetlić wewnątrz innej strony (w elemencie `<iframe>`) tylko wtedy, gdy znajduje się w tej samej domenie. W pewnych sytuacjach może to być niepożądane (na przykład gdy tworzysz aplikację dla Facebooka), więc zachowanie można zmienić, ustawiając `frames: http://allowed-host.com`, żeby dopuścić konkretny host, `frames: true`, żeby dopuścić osadzanie skądkolwiek (nagłówek zostaje pominięty), albo `frames: false`, żeby całkowicie tego zabronić (`X-Frame-Options: DENY`). + +Domyślnie Nette wysyła też nagłówki `X-Powered-By: Nette Framework 3` i `Content-Type: text/html; charset=utf-8`. Dowolny nagłówek, także te domyślne, możesz usunąć, ustawiając jego wartość na pusty ciąg. Content Security Policy ----------------------- -Łatwo można tworzyć nagłówki `Content-Security-Policy` (dalej CSP), ich opis znajdziesz w [opisie CSP |https://content-security-policy.com]. Dyrektywy CSP (jak np. `script-src`) mogą być zapisane albo jako ciągi znaków zgodnie ze specyfikacją, albo jako tablica wartości dla lepszej czytelności. Wtedy nie trzeba wokół słów kluczowych, jak na przykład `'self'`, pisać cudzysłowów. Nette również automatycznie wygeneruje wartość `nonce`, więc w nagłówku będzie na przykład `'nonce-y4PopTLM=='`. +Nagłówki `Content-Security-Policy` (CSP) da się łatwo skonfigurować; ich opis znajdziesz w [specyfikacji CSP |https://content-security-policy.com]. Dyrektywy CSP (jak `script-src`) można zapisać albo jako ciągi zgodne ze specyfikacją, albo jako tablice wartości dla lepszej czytelności. Wtedy nie trzeba używać cudzysłowów wokół słów kluczowych jak `'self'`. Nette wygeneruje też automatycznie wartość `nonce`, więc w nagłówku wyśle się coś w rodzaju `'nonce-y4PopTLM=='`. ```neon http: # Content Security Policy csp: - # ciąg znaków zgodny ze specyfikacją CSP + # ciąg zgodny ze specyfikacją CSP default-src: "'self' https://example.com" # tablica wartości @@ -51,7 +53,7 @@ http: W szablonach używaj `<script n:nonce>...</script>`, a wartość nonce zostanie uzupełniona automatycznie. Tworzenie bezpiecznych stron w Nette jest naprawdę łatwe. -Podobnie można tworzyć nagłówki `Content-Security-Policy-Report-Only` (które można używać równolegle z CSP) i [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: +Podobnie można skonfigurować nagłówki `Content-Security-Policy-Report-Only` (których można używać równolegle z CSP) oraz [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: ```neon http: @@ -69,103 +71,116 @@ http: ``` -Ciasteczka HTTP ---------------- +Cookie HTTP +----------- -Można zmienić domyślne wartości niektórych parametrów metody [Nette\Http\Response::setCookie() |response#setCookie] i sesji. +Możesz zmienić domyślne wartości niektórych parametrów metody [Nette\Http\Response::setCookie() |response#setCookie()] i obsługi sesji. ```neon http: - # zasięg ciasteczka według ścieżki + # zasięg cookie według ścieżki cookiePath: ... # (string) domyślnie '/' - # domeny, które akceptują ciasteczka + # domeny, które mogą przyjąć cookie cookieDomain: 'example.com' # (string|domain) domyślnie nieustawione - # wysyłać ciasteczka tylko przez HTTPS? + # wysyłać cookies tylko przez HTTPS? cookieSecure: ... # (bool|auto) domyślnie auto - # wyłącza wysyłanie ciasteczka używanego przez Nette jako ochronę przed CSRF + # wyłącza wysyłanie cookie, którego Nette używa do ochrony przed CSRF disableNetteCookie: ... # (bool) domyślnie false ``` -Atrybut `cookieDomain` określa, które domeny mogą akceptować ciasteczka. Jeśli nie jest podany, ciasteczko akceptuje ta sama (sub)domena, która je ustawiła, *ale nie* jej subdomeny. Jeśli `cookieDomain` jest podany, uwzględniane są również subdomeny. Dlatego podanie `cookieDomain` jest mniej ograniczające niż jego pominięcie. +Atrybut `cookieDomain` określa, które domeny (origins) mogą przyjmować cookies. Jeśli nie zostanie podany, cookie przyjmuje ta sama (sub)domena, która je ustawiła, *z wyłączeniem* jej subdomen. Jeśli `cookieDomain` zostanie podany, subdomeny również są objęte. Podanie `cookieDomain` jest więc mniej restrykcyjne niż jego pominięcie. -Na przykład przy `cookieDomain: nette.org` ciasteczka są dostępne również na wszystkich subdomenach, takich jak `doc.nette.org`. Tego samego można dokonać również za pomocą specjalnej wartości `domain`, czyli `cookieDomain: domain`. +Na przykład jeśli ustawione jest `cookieDomain: nette.org`, cookies są dostępne również we wszystkich subdomenach, jak `doc.nette.org`. To samo osiągniesz też wartością specjalną `domain`, czyli `cookieDomain: domain`. -Domyślna wartość `auto` dla atrybutu `cookieSecure` oznacza, że jeśli strona działa na HTTPS, ciasteczka będą wysyłane z flagą `Secure` i będą dostępne tylko przez HTTPS. +Domyślna wartość `auto` dla atrybutu `cookieSecure` oznacza, że jeśli witryna działa na HTTPS, cookies będą wysyłane z flagą `Secure` i tym samym będą dostępne tylko przez HTTPS. Proxy HTTP ---------- -Jeśli strona działa za proxy HTTP, podaj jej adres IP, aby poprawnie działało wykrywanie połączenia przez HTTPS oraz adres IP klienta. Czyli aby funkcje [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] i [isSecured() |request#isSecured] zwracały poprawne wartości, a w szablonach generowały się linki z protokołem `https:`. +Jeśli witryna działa za proxy HTTP, podaj adres IP proxy, żeby poprawnie działało wykrywanie połączenia HTTPS i adresu IP klienta. Czyli żeby [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()] i [isSecured() |request#isSecured()] zwracały poprawne wartości, a w szablonach generowały się odnośniki z protokołem `https:`. ```neon http: - # Adres IP, zakres (np. 127.0.0.1/8) lub tablica tych wartości + # adres IP, zakres (np. 127.0.0.1/8) albo tablica tych wartości proxy: 127.0.0.1 # (string|string[]) domyślnie nieustawione ``` +Wymuszenie HTTPS .{data-version:3.3.4} +-------------------------------------- + +Bezwarunkowo wymusza schemat żądania HTTPS. Przydaje się to dla witryn działających wyłącznie na HTTPS za load balancerem albo reverse proxy, które terminuje TLS, ale nie przekazuje nagłówka `X-Forwarded-Proto`, przez co standardowe wykrywanie HTTPS (nawet przy skonfigurowanym [#Proxy HTTP]) by tego nie wychwyciło. + +```neon +http: + # wymusza schemat HTTPS dla wszystkich żądań + forceHttps: true # (bool) domyślnie false +``` + + Sesja ===== -Podstawowe ustawienia [sesji|sessions]: +Podstawowe ustawienia [sesji |sessions]: ```neon session: # pokazać panel sesji w Tracy Bar? debugger: ... # (bool) domyślnie false - # czas nieaktywności, po którym sesja wygaśnie + # czas nieaktywności, po którym sesja wygasa expiration: 14 days # (string) domyślnie '3 hours' - # kiedy uruchomić sesję? + # kiedy sesja ma się uruchomić? autoStart: ... # (smart|always|never) domyślnie 'smart' - # handler, usługa implementująca interfejs SessionHandlerInterface + # handler, usługa implementująca SessionHandlerInterface handler: @handlerService ``` -Opcja `autoStart` kontroluje, kiedy ma się uruchamiać sesja. Wartość `always` oznacza, że sesja uruchomi się zawsze wraz z uruchomieniem aplikacji. Wartość `smart` oznacza, że sesja uruchomi się przy starcie aplikacji tylko wtedy, gdy już istnieje, lub w chwili, gdy chcemy z niej czytać lub do niej zapisywać. A na koniec wartość `never` zabrania automatycznego startu sesji. +Opcja `autoStart` steruje tym, kiedy sesja ma się uruchomić. Wartość `always` oznacza, że sesja uruchamia się zawsze przy starcie aplikacji. Wartość `smart` oznacza, że sesja uruchamia się przy starcie aplikacji tylko wtedy, gdy już istnieje, albo w momencie, gdy chcemy z niej czytać albo do niej pisać. Wreszcie wartość `never` wyłącza automatyczne uruchamianie sesji. -Dalej można ustawiać wszystkie [dyrektywy sesji |https://www.php.net/manual/en/session.configuration.php] PHP (w formacie camelCase) oraz [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Przykład: +Poza tym możesz ustawić wszystkie [dyrektywy sesji |https://www.php.net/manual/en/session.configuration.php] PHP (w formacie camelCase), a także [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Przykład: ```neon session: - # 'session.name' zapisujemy jako 'name' + # 'session.name' zapisane jako 'name' name: MYID - # 'session.save_path' zapisujemy jako 'savePath' + # 'session.save_path' zapisane jako 'savePath' savePath: "%tempDir%/sessions" ``` -Ciasteczko sesji ----------------- +Cookie sesji +------------ -Ciasteczko sesji jest wysyłane z tymi samymi parametrami co [inne ciasteczka |#Ciasteczka HTTP], ale te możesz dla niego zmienić: +Cookie sesji wysyłane jest z tymi samymi parametrami co [pozostałe cookies |#Cookie HTTP], ale możesz je zmienić specjalnie dla niego: ```neon session: - # domeny, które akceptują ciasteczka + # domeny, które mogą przyjąć cookie cookieDomain: 'example.com' # (string|domain) - # ograniczenie przy dostępie z innej domeny + # ograniczenie przy dostępie cross-origin cookieSamesite: None # (Strict|Lax|None) domyślnie Lax ``` -Atrybut `cookieSamesite` wpływa na to, czy ciasteczko zostanie wysłane podczas [dostępu z innej domeny |nette:glossary#SameSite cookie], co zapewnia pewną ochronę przed atakami [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). +Atrybut `cookieSamesite` wpływa na to, czy cookie jest wysyłane przy [żądaniach cross-origin |nette:glossary#Cookie SameSite], co daje pewną ochronę przed atakami [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Usługi DI ========= -Te usługi są dodawane do kontenera DI: +Do kontenera DI dodawane są te usługi: | Nazwa | Typ | Opis -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [Żądanie HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [Odpowiedź HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [zarządzanie sesją| sessions] +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [żądanie HTTP| request] +| `http.response` | [api:Nette\Http\Response] | [odpowiedź HTTP| response] +| `session.session`| [api:Nette\Http\Session] | [zarządzanie sesją| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | fabryka tworząca żądanie HTTP diff --git a/http/pl/request.texy b/http/pl/request.texy index 09a2e581df..bbc835f3be 100644 --- a/http/pl/request.texy +++ b/http/pl/request.texy @@ -2,11 +2,11 @@ ************ .[perex] -Nette enkapsuluje żądanie HTTP w obiekty z zrozumiałym API i jednocześnie zapewnia filtr sanityzujący. +Nette zamyka żądanie HTTP w obiektach o przejrzystym API, a przy okazji udostępnia filtr oczyszczający. -Żądanie HTTP reprezentuje obiekt [api:Nette\Http\Request]. Jeśli pracujesz z Nette, ten obiekt jest automatycznie tworzony przez framework i możesz go otrzymać za pomocą [wstrzykiwania zależności |dependency-injection:passing-dependencies]. W prezenterach wystarczy tylko wywołać metodę `$this->getHttpRequest()`. Jeśli pracujesz poza Nette Framework, możesz utworzyć obiekt za pomocą [#RequestFactory]. +Żądanie HTTP reprezentuje obiekt [api:Nette\Http\Request]. Jeśli pracujesz z Nette, obiekt ten tworzy framework automatycznie, a możesz sobie go pozwolić przekazać przez [wstrzykiwanie zależności |dependency-injection:passing-dependencies]. W presenterach wystarczy wywołać metodę `$this->getHttpRequest()`. Jeśli pracujesz poza Nette Framework, możesz utworzyć obiekt za pomocą [#RequestFactory]. -Dużą zaletą Nette jest to, że podczas tworzenia obiektu automatycznie oczyszcza wszystkie parametry wejściowe GET, POST, COOKIE oraz URL z znaków kontrolnych i nieprawidłowych sekwencji UTF-8. Z tymi danymi możesz następnie bezpiecznie dalej pracować. Oczyszczone dane są następnie używane w prezenterach i formularzach. +Ogromną zaletą Nette jest to, że przy tworzeniu obiektu automatycznie oczyszcza wszystkie parametry wejściowe (GET, POST, COOKIE) oraz URL ze znaków sterujących i nieprawidłowych sekwencji UTF-8. Z tymi danymi możesz potem bezpiecznie pracować. Oczyszczone dane są następnie używane w presenterach i formularzach. → [Instalacja i wymagania |@home#Instalacja] @@ -14,25 +14,25 @@ Dużą zaletą Nette jest to, że podczas tworzenia obiektu automatycznie oczysz Nette\Http\Request ================== -Ten obiekt jest immutable (niezmienny). Nie ma żadnych setterów, ma tylko jeden tzw. wither `withUrl()`, który nie zmienia obiektu, ale zwraca nową instancję ze zmienioną wartością. +Obiekt ten jest niezmienny. Nie ma setterów, ma tylko jeden tak zwany wither, `withUrl()`, który nie zmienia obiektu, tylko zwraca nową instancję ze zmienioną wartością. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ---------------------------------------------------------------- -Zwraca klona z innym adresem URL. +Zwraca klon z innym URL. getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -Zwraca adres URL żądania jako obiekt [UrlScript |urls#UrlScript]. +Zwraca URL żądania jako obiekt [UrlScript |urls#UrlScript]. ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -Uwaga: przeglądarki nie wysyłają fragmentu na serwer, więc `$url->getFragment()` będzie zwracać pusty ciąg znaków. +Uwaga: przeglądarki nie wysyłają fragmentu na serwer, więc `$url->getFragment()` zwróci pusty ciąg. getQuery(?string $key=null): string|array|null .[method] @@ -40,8 +40,8 @@ getQuery(?string $key=null): string|array|null .[method] Zwraca parametry żądania GET. ```php -$all = $httpRequest->getQuery(); // zwraca tablicę wszystkich parametrów z adresu URL -$id = $httpRequest->getQuery('id'); // zwraca parametr GET 'id' (lub null) +$all = $httpRequest->getQuery(); // tablica wszystkich parametrów URL +$id = $httpRequest->getQuery('id'); // zwraca parametr GET 'id' (albo null) ``` @@ -50,45 +50,45 @@ getPost(?string $key=null): string|array|null .[method] Zwraca parametry żądania POST. ```php -$all = $httpRequest->getPost(); // zwraca tablicę wszystkich parametrów z POST -$id = $httpRequest->getPost('id'); // zwraca parametr POST 'id' (lub null) +$all = $httpRequest->getPost(); // tablica wszystkich parametrów POST +$id = $httpRequest->getPost('id'); // zwraca parametr POST 'id' (albo null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Zwraca [przesłany plik |#Przesłane pliki] jako obiekt [api:Nette\Http\FileUpload]: +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- +Zwraca [upload |#Wysłane pliki] jako obiekt [api:Nette\Http\FileUpload]: ```php $file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // czy jakiś plik został przesłany? +if ($file?->hasFile()) { // czy jakiś plik został wysłany? $file->getUntrustedName(); // nazwa pliku wysłana przez użytkownika $file->getSanitizedName(); // nazwa bez niebezpiecznych znaków } ``` -Aby uzyskać dostęp do zagnieżdżonej struktury, podaj tablicę kluczy. +Żeby dostać się do struktury zagnieżdżonej, podaj tablicę kluczy. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Ponieważ nie można ufać danym z zewnątrz, a zatem polegać na strukturze plików, ten sposób jest bezpieczniejszy niż na przykład `$request->getFiles()['my-form']['details']['avatar']`, który może zawieść. +Ponieważ danym zewnętrznym nie można ufać i tym samym polegać na strukturze plików, to podejście jest bezpieczniejsze niż na przykład `$request->getFiles()['my-form']['details']['avatar']`, które mogłoby zawieść. getFiles(): array .[method] --------------------------- -Zwraca drzewo [wszystkich przesłanych plików |#Przesłane pliki] w znormalizowanej strukturze, której liśćmi są obiekty [api:Nette\Http\FileUpload]: +Zwraca drzewo [wszystkich uploadów |#Wysłane pliki] w znormalizowanej strukturze, której liśćmi są obiekty [api:Nette\Http\FileUpload]: ```php $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Zwraca ciasteczko lub `null`, jeśli nie istnieje. +getCookie(string $key): ?string .[method] +----------------------------------------- +Zwraca cookie albo `null`, jeśli nie istnieje. ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -97,7 +97,7 @@ $sessId = $httpRequest->getCookie('sess_id'); getCookies(): array .[method] ----------------------------- -Zwraca wszystkie ciasteczka. +Zwraca wszystkie cookies. ```php $cookies = $httpRequest->getCookies(); @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -Zwraca metodę HTTP, za pomocą której zostało wykonane żądanie. +Zwraca metodę HTTP użytą w żądaniu. ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -Testuje metodę HTTP, za pomocą której zostało wykonane żądanie. Parametr jest niewrażliwy na wielkość liter. +Testuje metodę HTTP użytą w żądaniu. Parametr nie rozróżnia wielkości liter. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,46 +124,78 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -Zwraca nagłówek HTTP lub `null`, jeśli nie istnieje. Parametr jest niewrażliwy na wielkość liter. +Zwraca nagłówek HTTP albo `null`, jeśli nie istnieje. Parametr nie rozróżnia wielkości liter. ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Zwraca wszystkie nagłówki HTTP jako tablicę asocjacyjną. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Zwraca wszystkie nagłówki HTTP jako tablicę asocjacyjną. Klucze są znormalizowane do małych liter. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -Czy połączenie jest szyfrowane (HTTPS)? Aby zapewnić prawidłowe działanie, może być konieczne [skonfigurowanie proxy |configuration#Proxy HTTP]. +Czy połączenie jest szyfrowane (HTTPS)? Poprawne działanie może wymagać [ustawienia proxy |configuration#Proxy HTTP]. -isSameSite(): bool .[method] ----------------------------- -Czy żądanie pochodzi z tej samej (sub)domeny i jest inicjowane przez kliknięcie linku? Nette używa ciasteczka `_nss` (wcześniej `nette-samesite`) do detekcji. +isSameSite(): bool .[method deprecated] +--------------------------------------- +Czy żądanie przyszło z tej samej witryny? Od wersji 3.4 zastępuje je bardziej wszechstronne [isFrom() |#isFrom()]. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Mówi Ci, skąd żądanie przyszło i w jaki sposób przeglądarka je wykonała, na podstawie nagłówków `Sec-Fetch-*` (tak zwane [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]), które przeglądarka ustawia sama i których strona działająca w przeglądarce ofiary nie może ani podrobić, ani usunąć. Nette używa tego wewnętrznie do automatycznej ochrony formularzy i sygnałów przed [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Przydaje się, gdy chcesz zabezpieczyć własne wrażliwe akcje, na przykład endpointy API albo destrukcyjne odnośniki. + +Metoda zwraca `true` tylko wtedy, gdy żądanie spełnia **wszystkie** podane przez Ciebie warunki. Pierwszy parametr `$site` opisuje relację między stroną, która zainicjowała żądanie, a Twoją witryną (nagłówek `Sec-Fetch-Site`). Przyjmuje pojedynczą wartość albo listę tych przypadków `FetchSite`: + +- `FetchSite::SameOrigin` - z dokładnie tego samego origin (schemat, host i port) +- `FetchSite::SameSite` - z tej samej witryny, ewentualnie z innej subdomeny +- `FetchSite::CrossSite` - z obcej witryny +- `FetchSite::None` - użytkownik zainicjował je bezpośrednio, np. wpisując URL albo otwierając zakładkę + +```php +// czy żądanie pochodzi z naszych własnych stron? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // zablokuj akcję +} +``` + +Opcjonalny parametr `$dest` (nagłówek `Sec-Fetch-Dest`) mówi, jakiego rodzaju zasób przeglądarka pobiera, np. `FetchDest::Document` dla nawigacji najwyższego poziomu albo `FetchDest::Empty` dla żądania wykonanego z JavaScriptu. Opcjonalny parametr `$user` (nagłówek `Sec-Fetch-User`) wskazuje, czy nawigacja została wywołana prawdziwą akcją użytkownika, jak kliknięcie odnośnika albo wysłanie formularza; przekaż `true`, żeby tego wymagać. + +Kontrola, że akcja jest osiągalna tylko z Twoich własnych stron i tylko przez prawdziwą akcję użytkownika, wygląda wtedy tak: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Starsze przeglądarki (Safari przed 16.4) nie wysyłają nagłówków `Sec-Fetch-*`. Dla nich Nette wraca do cookie `SameSite=Strict`, które dowodzi tylko tego, że żądanie nie jest cross-site. Kontroli, która dodatkowo wymaga `$dest` albo `$user`, nie da się w ten sposób zweryfikować i w tych przeglądarkach zwraca `false`; jeśli to zbyt surowe, testuj tylko `$site`. isAjax(): bool .[method] ------------------------ -Czy to jest żądanie AJAX? +Czy to żądanie AJAX? getRemoteAddress(): ?string .[method] ------------------------------------- -Zwraca adres IP użytkownika. Aby zapewnić prawidłowe działanie, może być konieczne [skonfigurowanie proxy |configuration#Proxy HTTP]. +Zwraca adres IP użytkownika. Poprawne działanie może wymagać [ustawienia proxy |configuration#Proxy HTTP]. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Zwraca tłumaczenie DNS adresu IP użytkownika. Aby zapewnić prawidłowe działanie, może być konieczne [skonfigurowanie proxy |configuration#Proxy HTTP]. +Przestarzała, zawsze zwraca `null`. Odwrotne zapytania DNS były wolne i zawodne; jeśli potrzebujesz nazwy hosta, rozwiąż ją sam z [getRemoteAddress() |#getRemoteAddress()]. getBasicCredentials(): ?array .[method] @@ -184,12 +216,36 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Zwraca origin, z którego przyszło żądanie. Origin składa się ze schematu (protokołu), nazwy hosta i portu, na przykład `https://example.com:8080`. Zwraca `null`, jeśli nagłówek origin nie jest obecny albo ma wartość `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Przeglądarka wysyła nagłówek `Origin` w tych przypadkach: +- żądania cross-origin (wywołania AJAX do innej domeny) +- POST, PUT, DELETE i inne żądania modyfikujące +- żądania wykonane przez Fetch API + +Przeglądarka NIE wysyła nagłówka `Origin` dla: +- zwykłych żądań GET do tej samej domeny (nawigacja same-origin) +- bezpośredniej nawigacji przez wpisanie URL w pasku adresu +- żądań od klientów niebędących przeglądarką + +.[note] +W przeciwieństwie do nagłówka `Referer` `Origin` zawiera tylko schemat, host i port, a nie pełną ścieżkę URL. Czyni to go odpowiedniejszym do kontroli bezpieczeństwa przy zachowaniu prywatności użytkownika. Nagłówek `Origin` używany jest przede wszystkim do walidacji [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing). + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Wykrywa język. Jako parametr `$lang` przekazujemy tablicę z językami obsługiwanymi przez aplikację, a ona zwraca ten, który przeglądarka odwiedzającego preferuje najbardziej. To nie są żadne czary, po prostu wykorzystuje nagłówek `Accept-Language`. Jeśli nie ma dopasowania, zwraca `null`. +Wykrywa język. Jako parametr `$langs` przekaż tablicę języków obsługiwanych przez aplikację, a metoda zwróci ten, który preferuje przeglądarka odwiedzającego. Nie ma w tym magii, wykorzystywany jest po prostu nagłówek `Accept-Language`. Jeśli nie znajdzie dopasowania, zwraca `null`. ```php -// przeglądarka wysyła np. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 +// Przeglądarka wysyła np. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 $langs = ['hu', 'pl', 'en']; // języki obsługiwane przez aplikację echo $httpRequest->detectLanguage($langs); // en @@ -199,40 +255,41 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -Klasa [api:Nette\Http\RequestFactory] służy do tworzenia instancji `Nette\Http\Request`, która reprezentuje bieżące żądanie HTTP. (Jeśli pracujesz z Nette, obiekt żądania HTTP jest automatycznie tworzony przez framework.) +Klasa [api:Nette\Http\RequestFactory] służy do utworzenia instancji `Nette\Http\Request`, która reprezentuje bieżące żądanie HTTP. (Jeśli pracujesz z Nette, obiekt żądania HTTP tworzy framework automatycznie.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -Metoda `fromGlobals()` tworzy obiekt żądania na podstawie bieżących globalnych zmiennych PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` i `$_SERVER`). Podczas tworzenia obiektu automatycznie oczyszcza wszystkie parametry wejściowe GET, POST, COOKIE oraz URL z znaków kontrolnych i nieprawidłowych sekwencji UTF-8, co zapewnia bezpieczeństwo podczas dalszej pracy z tymi danymi. +Metoda `fromGlobals()` tworzy obiekt żądania na podstawie bieżących zmiennych globalnych PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` i `$_SERVER`). Przy tworzeniu obiektu automatycznie oczyszcza wszystkie parametry wejściowe (GET, POST, COOKIE) oraz URL ze znaków sterujących i nieprawidłowych sekwencji UTF-8, co zapewnia bezpieczeństwo przy późniejszej pracy z tymi danymi. RequestFactory można skonfigurować przed wywołaniem `fromGlobals()`: -- metodą `$factory->setBinary()` wyłączysz automatyczne czyszczenie parametrów wejściowych z znaków kontrolnych i nieprawidłowych sekwencji UTF-8. -- metodą `$factory->setProxy(...)` podasz adres IP [serwera proxy |configuration#Proxy HTTP], co jest niezbędne do poprawnego wykrywania adresu IP użytkownika. +- metoda `$factory->setBinary()` wyłącza automatyczne oczyszczanie parametrów wejściowych ze znaków sterujących i nieprawidłowych sekwencji UTF-8. +- metoda `$factory->setProxy(...)` podaje adres IP [serwera proxy |configuration#Proxy HTTP], co jest potrzebne do poprawnego wykrywania adresu IP użytkownika. +- metoda `$factory->setForceHttps()` .{data-version:3.3.4} wymusza schemat żądania HTTPS niezależnie od środowiska serwera. -RequestFactory umożliwia definiowanie filtrów, które automatycznie transformują części adresu URL żądania. Filtry te usuwają niepożądane znaki z adresu URL, które mogą się tam znaleźć na przykład z powodu nieprawidłowej implementacji systemów komentarzy na różnych stronach: +RequestFactory pozwala definiować filtry automatycznie przekształcające części URL żądania. Filtry te usuwają z URL-i niepożądane znaki, które mogły zostać do nich wstawione na przykład przez niepoprawne implementacje systemów komentarzy na różnych stronach: ```php -// usunięcie spacji ze ścieżki +// usuwa spacje ze ścieżki $requestFactory->urlFilters['path']['%20'] = ''; -// usunięcie kropki, przecinka lub prawego nawiasu z końca URI +// usuwa kropkę, przecinek albo prawy nawias z końca URI $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// oczyszczenie ścieżki z podwójnych ukośników (filtr domyślny) +// czyści ścieżkę z podwójnych ukośników (filtr domyślny) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -Pierwszy klucz `'path'` lub `'url'` określa, do której części adresu URL filtr zostanie zastosowany. Drugi klucz to wyrażenie regularne, które ma zostać wyszukane, a wartość to zamiennik, który zostanie użyty zamiast znalezionego tekstu. +Pierwszy klucz, `'path'` albo `'url'`, określa, do której części URL filtr zostanie zastosowany. Drugi klucz to wyrażenie regularne do wyszukania, a wartość to zamiennik, który ma zostać użyty zamiast znalezionego tekstu. -Przesłane pliki -=============== +Wysłane pliki +============= -Metoda `Nette\Http\Request::getFiles()` zwraca tablicę wszystkich przesłanych plików w znormalizowanej strukturze, której liśćmi są obiekty [api:Nette\Http\FileUpload]. Enkapsulują one dane wysłane przez element formularza `<input type=file>`. +Metoda `Nette\Http\Request::getFiles()` zwraca tablicę wszystkich uploadów w znormalizowanej strukturze, której liśćmi są obiekty [api:Nette\Http\FileUpload]. Zamykają one w sobie dane wysłane elementem formularza `<input type=file>`. Struktura odzwierciedla nazewnictwo elementów w HTML. W najprostszym przypadku może to być pojedynczy nazwany element formularza wysłany jako: @@ -240,21 +297,21 @@ Struktura odzwierciedla nazewnictwo elementów w HTML. W najprostszym przypadku <input type="file" name="avatar"> ``` -W tym przypadku `$request->getFiles()` zwraca tablicę: +W takim przypadku `$request->getFiles()` zwraca tablicę: ```php [ - 'avatar' => /* Instancja FileUpload */ + 'avatar' => /* instancja FileUpload */ ] ``` -Obiekt `FileUpload` jest tworzony nawet wtedy, gdy użytkownik nie wysłał żadnego pliku lub wysyłanie nie powiodło się. Czy plik został wysłany, zwraca metoda `hasFile()`: +Obiekt `FileUpload` powstaje także wtedy, gdy użytkownik żadnego pliku nie wysłał albo wysyłanie się nie powiodło. To, czy plik został wysłany, zwraca metoda `hasFile()`: ```php $request->getFile('avatar')?->hasFile(); ``` -W przypadku nazwy elementu używającej notacji dla tablic: +W przypadku nazwy elementu z zapisem tablicowym: ```latte <input type="file" name="my-form[details][avatar]"> @@ -266,13 +323,13 @@ zwrócone drzewo wygląda tak: [ 'my-form' => [ 'details' => [ - 'avatar' => /* Instancja FileUpload */ + 'avatar' => /* instancja FileUpload */ ], ], ] ``` -Można również utworzyć tablicę plików: +Możesz też tworzyć tablice plików: ```latte <input type="file" name="my-form[details][avatars][]" multiple> @@ -285,25 +342,25 @@ W takim przypadku struktura wygląda tak: 'my-form' => [ 'details' => [ 'avatars' => [ - 0 => /* Instancja FileUpload */, - 1 => /* Instancja FileUpload */, - 2 => /* Instancja FileUpload */, + 0 => /* instancja FileUpload */, + 1 => /* instancja FileUpload */, + 2 => /* instancja FileUpload */, ], ], ], ] ``` -Dostęp do indeksu 1 zagnieżdżonej tablicy najlepiej uzyskać w ten sposób: +Do indeksu 1 zagnieżdżonej tablicy najlepiej dostać się tak: ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -Ponieważ nie można ufać danym z zewnątrz, a zatem polegać na strukturze plików, ten sposób jest bezpieczniejszy niż na przykład `$request->getFiles()['my-form']['details']['avatars'][1]`, który może zawieść. +Ponieważ danym zewnętrznym nie można ufać i tym samym polegać na strukturze plików, to podejście jest bezpieczniejsze niż na przykład `$request->getFiles()['my-form']['details']['avatars'][1]`, które mogłoby zawieść. Przegląd metod `FileUpload` .{toc: FileUpload} @@ -312,22 +369,22 @@ Przegląd metod `FileUpload` .{toc: FileUpload} hasFile(): bool .[method] ------------------------- -Zwraca `true`, jeśli użytkownik przesłał jakiś plik. +Zwraca `true`, jeśli użytkownik wysłał plik. isOk(): bool .[method] ---------------------- -Zwraca `true`, jeśli plik został pomyślnie przesłany. +Zwraca `true`, jeśli plik został wysłany pomyślnie. getError(): int .[method] ------------------------- -Zwraca kod błędu podczas przesyłania pliku. Jest to jedna ze stałych [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Jeśli przesyłanie przebiegło pomyślnie, zwraca `UPLOAD_ERR_OK`. +Zwraca kod błędu związany z wysłanym plikiem. Jest to jedna ze stałych [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php]. Jeśli plik został wysłany pomyślnie, zwraca `UPLOAD_ERR_OK`. move(string $dest) .[method] ---------------------------- -Przenosi przesłany plik do nowej lokalizacji. Jeśli plik docelowy już istnieje, zostanie nadpisany. +Przenosi wysłany plik w nowe miejsce. Jeśli plik docelowy już istnieje, zostanie nadpisany. ```php $file->move('/path/to/files/name.ext'); @@ -336,12 +393,12 @@ $file->move('/path/to/files/name.ext'); getContents(): ?string .[method] -------------------------------- -Zwraca zawartość przesłanego pliku. Jeśli przesyłanie nie powiodło się, zwraca `null`. +Zwraca zawartość wysłanego pliku. Jeśli wysyłanie się nie powiodło, zwraca `null`. getContentType(): ?string .[method] ----------------------------------- -Wykrywa typ zawartości MIME przesłanego pliku na podstawie jego sygnatury. Jeśli przesyłanie nie powiodło się lub wykrywanie nie powiodło się, zwraca `null`. +Wykrywa typ zawartości MIME wysłanego pliku na podstawie jego sygnatury. Jeśli wysyłanie się nie powiodło albo wykrywanie zawiodło, zwraca `null`. .[caution] Wymaga rozszerzenia PHP `fileinfo`. @@ -352,12 +409,12 @@ getUntrustedName(): string .[method] Zwraca oryginalną nazwę pliku wysłaną przez przeglądarkę. .[caution] -Nie ufaj wartości zwracanej przez tę metodę. Klient mógł wysłać złośliwą nazwę pliku w celu uszkodzenia lub zhakowania aplikacji. +Nie ufaj wartości zwracanej przez tę metodę. Klient mógł wysłać złośliwą nazwę pliku z zamiarem uszkodzenia albo zhakowania Twojej aplikacji. getSanitizedName(): string .[method] ------------------------------------ -Zwraca oczyszczoną nazwę pliku. Zawiera tylko znaki ASCII `[a-zA-Z0-9.-]`. Jeśli nazwa nie zawiera takich znaków, zwraca `'unknown'`. Jeśli plik jest obrazem w formacie JPEG, PNG, GIF, WebP lub AVIF, zwraca również poprawne rozszerzenie. +Zwraca oczyszczoną nazwę pliku. Zawiera tylko znaki ASCII `[a-zA-Z0-9.-]`. Jeśli nazwa takich znaków nie zawiera, zwraca `'unknown'`. Jeśli plik jest obrazkiem JPEG, PNG, GIF, WebP albo AVIF, zwraca też poprawne rozszerzenie pliku. .[caution] Wymaga rozszerzenia PHP `fileinfo`. @@ -373,25 +430,30 @@ Wymaga rozszerzenia PHP `fileinfo`. getUntrustedFullPath(): string .[method] ---------------------------------------- -Zwraca oryginalną ścieżkę do pliku, tak jak została wysłana przez przeglądarkę podczas przesyłania folderu. Pełna ścieżka jest dostępna tylko w PHP 8.1 i nowszych. W poprzednich wersjach ta metoda zwraca oryginalną nazwę pliku. +Zwraca oryginalną ścieżkę pliku wysłaną przez przeglądarkę przy wysyłaniu katalogu. Pełna ścieżka dostępna jest tylko w PHP 8.1 i nowszym. W poprzednich wersjach ta metoda zwraca oryginalną nazwę pliku. .[caution] -Nie ufaj wartości zwracanej przez tę metodę. Klient mógł wysłać złośliwą nazwę pliku w celu uszkodzenia lub zhakowania aplikacji. +Nie ufaj wartości zwracanej przez tę metodę. Klient mógł wysłać złośliwą nazwę pliku z zamiarem uszkodzenia albo zhakowania Twojej aplikacji. getSize(): int .[method] ------------------------ -Zwraca rozmiar przesłanego pliku. Jeśli przesyłanie nie powiodło się, zwraca `0`. +Zwraca rozmiar wysłanego pliku. Jeśli wysyłanie się nie powiodło, zwraca `0`. getTemporaryFile(): string .[method] ------------------------------------ -Zwraca ścieżkę do tymczasowej lokalizacji przesłanego pliku. Jeśli przesyłanie nie powiodło się, zwraca `''`. +Zwraca ścieżkę do tymczasowej lokalizacji wysłanego pliku. Jeśli wysyłanie się nie powiodło, zwraca `''`. + + +__toString(): string .[method] +------------------------------ +Zwraca ścieżkę do tymczasowej lokalizacji wysłanego pliku. Pozwala to używać obiektu `FileUpload` bezpośrednio jako ciągu. isImage(): bool .[method] ------------------------- -Zwraca `true`, jeśli przesłany plik jest obrazem w formacie JPEG, PNG, GIF, WebP lub AVIF. Wykrywanie odbywa się na podstawie jego sygnatury i nie weryfikuje integralności całego pliku. Czy obraz nie jest uszkodzony, można sprawdzić na przykład próbując go [załadować |#toImage]. +Zwraca `true`, jeśli wysłany plik jest obrazkiem JPEG, PNG, GIF, WebP albo AVIF. Wykrywanie odbywa się na podstawie jego sygnatury i nie weryfikuje integralności całego pliku. To, czy obrazek nie jest uszkodzony, można ustalić na przykład, próbując go [wczytać |#toImage()]. .[caution] Wymaga rozszerzenia PHP `fileinfo`. @@ -399,9 +461,9 @@ Wymaga rozszerzenia PHP `fileinfo`. getImageSize(): ?array .[method] -------------------------------- -Zwraca parę `[szerokość, wysokość]` z wymiarami przesłanego obrazu. Jeśli przesyłanie nie powiodło się lub nie jest to prawidłowy obraz, zwraca `null`. +Zwraca parę `[width, height]` z wymiarami wysłanego obrazka. Jeśli wysyłanie się nie powiodło albo nie jest to poprawny obrazek, zwraca `null`. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Ładuje obraz jako obiekt [Image|utils:images]. Jeśli przesyłanie nie powiodło się lub nie jest to prawidłowy obraz, zgłasza wyjątek `Nette\Utils\ImageException`. +Wczytuje obrazek jako obiekt [Image |utils:images]. Jeśli wysyłanie się nie powiodło albo nie jest to poprawny obrazek, rzuca `Nette\Utils\ImageException`. diff --git a/http/pl/response.texy b/http/pl/response.texy index d7468ec9d1..34fd6df939 100644 --- a/http/pl/response.texy +++ b/http/pl/response.texy @@ -2,9 +2,9 @@ Odpowiedź HTTP ************** .[perex] -Nette enkapsuluje odpowiedź HTTP w obiekty z zrozumiałym API. +Nette zamyka odpowiedź HTTP w obiektach o przejrzystym API. -Odpowiedź HTTP reprezentuje obiekt [api:Nette\Http\Response]. Jeśli pracujesz z Nette, ten obiekt jest automatycznie tworzony przez framework i możesz go otrzymać za pomocą [wstrzykiwania zależności |dependency-injection:passing-dependencies]. W prezenterach wystarczy tylko wywołać metodę `$this->getHttpResponse()`. +Odpowiedź HTTP reprezentuje obiekt [api:Nette\Http\Response]. Jeśli pracujesz z Nette, obiekt ten tworzy framework automatycznie, a możesz sobie go pozwolić przekazać przez [wstrzykiwanie zależności |dependency-injection:passing-dependencies]. W presenterach wystarczy wywołać metodę `$this->getHttpResponse()`. → [Instalacja i wymagania |@home#Instalacja] @@ -12,12 +12,12 @@ Odpowiedź HTTP reprezentuje obiekt [api:Nette\Http\Response]. Jeśli pracujesz Nette\Http\Response =================== -Obiekt jest w przeciwieństwie do [Nette\Http\Request|request] mutable, czyli za pomocą setterów możesz zmieniać stan, czyli np. wysyłać nagłówki. Pamiętaj, że wszystkie settery muszą być wywołane **przed wysłaniem jakiegokolwiek wyjścia.** Czy wyjście zostało już wysłane, powie metoda `isSent()`. Jeśli zwraca `true`, każda próba wysłania nagłówka wywoła wyjątek `Nette\InvalidStateException`. +W przeciwieństwie do [Nette\Http\Request |request] obiekt ten jest zmienny, więc możesz setterami zmieniać stan, na przykład wysyłać nagłówki. Pamiętaj, że wszystkie settery **muszą być wywołane przed wysłaniem jakiegokolwiek faktycznego wyjścia.** To, czy wyjście zostało już wysłane, mówi metoda `isSent()`. Jeśli zwraca `true`, każda próba wysłania nagłówka rzuci `Nette\InvalidStateException`. setCode(int $code, ?string $reason=null) .[method] -------------------------------------------------- -Zmienia [kod stanu odpowiedzi |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Dla lepszej czytelności kodu źródłowego zalecamy używanie [predefiniowanych stałych |api:Nette\Http\IResponse] zamiast liczb. +Zmienia statusowy [kod odpowiedzi |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Dla lepszej czytelności kodu źródłowego zaleca się używanie zamiast liczb [predefiniowanych stałych |api:Nette\Http\IResponse]. ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -26,17 +26,17 @@ $httpResponse->setCode(Nette\Http\Response::S404_NotFound); getCode(): int .[method] ------------------------ -Zwraca kod stanu odpowiedzi. +Zwraca kod statusu odpowiedzi. isSent(): bool .[method] ------------------------ -Zwraca informację, czy nagłówki zostały już wysłane z serwera do przeglądarki, a zatem nie jest już możliwe wysyłanie nagłówków ani zmiana kodu stanu. +Zwraca informację, czy nagłówki zostały już wysłane z serwera do przeglądarki, czyli czy nie da się już wysyłać nagłówków ani zmieniać kodu statusu. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Wysyła nagłówek HTTP i **nadpisuje** wcześniej wysłany nagłówek o tej samej nazwie. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Wysyła nagłówek HTTP i **nadpisuje** wcześniej wysłany nagłówek o tej samej nazwie. Jeśli `$value` to `null`, nagłówek zostanie usunięty. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -60,15 +60,15 @@ Usuwa wcześniej wysłany nagłówek HTTP. getHeader(string $header): ?string .[method] -------------------------------------------- -Zwraca wysłany nagłówek HTTP lub `null`, jeśli taki nie istnieje. Parametr jest niewrażliwy na wielkość liter. +Zwraca wysłany nagłówek HTTP albo `null`, jeśli nie istnieje. Parametr nie rozróżnia wielkości liter. ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ +getHeaders(): array<string, string> .[method] +--------------------------------------------- Zwraca wszystkie wysłane nagłówki HTTP jako tablicę asocjacyjną. ```php @@ -88,7 +88,7 @@ $httpResponse->setContentType('text/plain', 'UTF-8'); redirect(string $url, int $code=self::S302_Found): void .[method] ----------------------------------------------------------------- -Przekierowuje na inny adres URL. Pamiętaj, aby zakończyć skrypt po tym. +Przekierowuje na inny URL. Pamiętaj, żeby potem zakończyć skrypt. ```php $httpResponse->redirect('http://example.com'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- -Ustawia wygaśnięcie dokumentu HTTP za pomocą nagłówków `Cache-Control` i `Expires`. Parametrem jest interwał czasowy (jako tekst) lub `null`, co wyłącza buforowanie. +setExpiration(?string $expire) .[method] +---------------------------------------- +Ustawia wygaśnięcie dokumentu HTTP za pomocą nagłówków `Cache-Control` i `Expires`. Parametrem jest albo interwał czasowy (jako tekst), albo `null`, co wyłącza cache. ```php -// cache w przeglądarce wygaśnie za godzinę +// cache przeglądarki wygasa za godzinę $httpResponse->setExpiration('1 hour'); ``` sendAsFile(string $fileName) .[method] -------------------------------------- -Odpowiedź zostanie pobrana za pomocą okna dialogowego *Zapisz jako* pod podaną nazwą. Sam plik nie jest wysyłany. +Odpowiedź zostanie pobrana przez okno dialogowe *Zapisz jako* pod podaną nazwą. Samego pliku nie wysyła. ```php -$httpResponse->sendAsFile('faktura.pdf'); +$httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Wysyła ciasteczko. Domyślne wartości parametrów: +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +Wysyła cookie. Domyślne wartości parametrów: -| `$path` | `'/'` | ciasteczko ma zasięg na wszystkie ścieżki w (sub)domenie *(konfigurowalne)* -| `$domain` | `null` | co oznacza zasięg na bieżącą (sub)domenę, ale nie jej subdomeny *(konfigurowalne)* -| `$secure` | `true` | jeśli strona działa na HTTPS, w przeciwnym razie `false` *(konfigurowalne)* -| `$httpOnly` | `true` | ciasteczko jest niedostępne dla JavaScript -| `$sameSite` | `'Lax'` | ciasteczko może nie być wysyłane podczas [dostępu z innej domeny |nette:glossary#SameSite cookie] +| `$path` | `'/'` | cookie jest dostępne dla wszystkich ścieżek w (sub)domenie *(konfigurowalne)* +| `$domain` | `null` | czyli dostępne dla bieżącej (sub)domeny, ale nie jej subdomen *(konfigurowalne)* +| `$secure` | `auto` | `true`, jeśli witryna działa na HTTPS, w przeciwnym razie `false` (domyślnie we frameworku; sama klasa ma domyślnie `false`) *(konfigurowalne)* +| `$httpOnly` | `true` | cookie jest niedostępne dla JavaScriptu +| `$sameSite` | `'Lax'` | cookie może nie zostać wysłane przy [dostępie cross-origin |nette:glossary#Cookie SameSite] +| `$partitioned` | `false` | czy cookie jest partycjonowane, patrz niżej *(od v3.4)* -Domyślne wartości parametrów `$path`, `$domain` i `$secure` możesz zmienić w [konfiguracji |configuration#Ciasteczka HTTP]. +Wartości domyślne parametrów `$path`, `$domain` i `$secure` możesz zmienić w [konfiguracji |configuration#Cookie HTTP]. -Czas można podawać jako liczbę sekund lub ciąg znaków: +Wygaśnięcie przekazuje się jako liczbę sekund, jako tekstowy interwał albo datę, albo jako obiekt `DateTimeInterface`. Wartość `null` tworzy cookie sesyjne, które przeglądarka odrzuca przy zamknięciu. Nette wysyła wygaśnięcie w atrybutach `Expires` i `Max-Age`. ```php -$httpResponse->setCookie('lang', 'pl', '100 days'); +$httpResponse->setCookie('lang', 'en', '100 days'); // wygasa za 100 dni +$httpResponse->setCookie('lang', 'en', null); // cookie sesyjne ``` -Parametr `$domain` określa, które domeny mogą akceptować ciasteczka. Jeśli nie jest podany, ciasteczko akceptuje ta sama (sub)domena, która je ustawiła, ale nie jej subdomeny. Jeśli `$domain` jest podany, uwzględniane są również subdomeny. Dlatego podanie `$domain` jest mniej ograniczające niż jego pominięcie. Na przykład przy `$domain = 'nette.org'` ciasteczka są dostępne również na wszystkich subdomenach, takich jak `doc.nette.org`. +Parametr `$domain` określa, które domeny mogą przyjąć cookie. Jeśli nie zostanie podany, cookie przyjmuje ta sama (sub)domena, która je ustawiła, ale nie jej subdomeny. Jeśli `$domain` zostanie podany, subdomeny również są objęte. Podanie `$domain` jest więc mniej restrykcyjne niż jego pominięcie. Na przykład przy `$domain = 'nette.org'` cookies są dostępne również we wszystkich subdomenach, jak `doc.nette.org`. + +Wartość `$sameSite` możesz przekazać jako enum `Nette\Http\SameSite`: `SameSite::Lax`, `SameSite::Strict` albo `SameSite::None` (wartości tekstowe `'Lax'`, `'Strict'`, `'None'` też działają). Jeśli ustawisz `SameSite::None`, atrybut `$secure` włącza się automatycznie, bo przeglądarki odrzucają cookie `SameSite=None`, które nie jest secure. + +.{data-version:3.4.0} +Cookies partycjonowane (CHIPS) dają cookie własny, osobny magazyn dla każdej witryny najwyższego poziomu. Gdy więc usługa zewnętrzna (na przykład osadzony widget) ustawi cookie partycjonowane, przeglądarka trzyma osobną kopię dla każdej witryny, na której widget się pojawia, a kopii tych nie da się ze sobą powiązać na potrzeby śledzenia międzywitrynowego. Włączysz je, ustawiając `$partitioned` na `true`; wymaga to również atrybutu `$secure`, więc włącza się on automatycznie. -Dla wartości `$sameSite` możesz użyć stałych `Response::SameSiteLax`, `SameSiteStrict` i `SameSiteNone`. +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- -Usuwa ciasteczko. Domyślne wartości parametrów to: +Usuwa cookie. Domyślne wartości parametrów to: - `$path` z zasięgiem na wszystkie katalogi (`'/'`) - `$domain` z zasięgiem na bieżącą (sub)domenę, ale nie jej subdomeny -- `$secure` jest zgodny z ustawieniami w [konfiguracji |configuration#Ciasteczka HTTP] +- `$secure` zależy od ustawień w [konfiguracji |configuration#Cookie HTTP] ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +Obiekt [api:Nette\Http\Context] łączy żądanie i odpowiedź razem i pomaga przy cache HTTP. Nie jest zarejestrowany jako usługa, więc tworzysz go sam. W presenterach zwykle łatwiej użyć metody [lastModified() |application:presenters#Cache HTTP]; kontekst przydaje się, gdy odpowiedź wysyłasz sam, na przykład z własnej klasy odpowiedzi. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Ustala, czy treść zmieniła się od ostatniej wizyty klienta. Jeśli przekażesz czas ostatniej modyfikacji, wysyła nagłówek `Last-Modified`; jeśli przekażesz walidator ETag (krótki ciąg identyfikujący bieżącą wersję treści, np. jej hash), wysyła nagłówek `ETag`. Następnie porównuje oba z nagłówkami `If-Modified-Since` i `If-None-Match` wysłanymi przez przeglądarkę. + +Jeśli przeglądarka ma już pasującą wersję, metoda ustawia kod `304 Not Modified` i zwraca `false`: w takim przypadku w ogóle nie wysyłaj ciała odpowiedzi. W przeciwnym razie zwraca `true`. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Oba parametry są opcjonalne. Jeśli nie znasz czasu modyfikacji treści, użyj tylko ETag i odwrotnie. diff --git a/http/pl/sessions.texy b/http/pl/sessions.texy index e95ca6aa73..069aa83e52 100644 --- a/http/pl/sessions.texy +++ b/http/pl/sessions.texy @@ -3,19 +3,19 @@ Sesje <div class=perex> -HTTP jest protokołem bezstanowym, jednak niemal każda aplikacja potrzebuje przechowywać stan między żądaniami, na przykład zawartość koszyka. Właśnie do tego służy sesja. Pokażemy, +HTTP to protokół bezstanowy, jednak prawie każda aplikacja potrzebuje utrzymywać stan między żądaniami, na przykład zawartość koszyka. Dokładnie do tego służą sesje. Pokażemy: - jak używać sesji -- jak unikać konfliktów nazw +- jak zapobiec konfliktom nazw - jak ustawić wygaśnięcie </div> -Podczas używania sesji każdy użytkownik otrzymuje unikalny identyfikator zwany ID sesji, który jest przekazywany w ciasteczku. Służy on jako klucz do danych sesji. W przeciwieństwie do ciasteczek, które są przechowywane po stronie przeglądarki, dane w sesji są przechowywane po stronie serwera. +Przy używaniu sesji każdy użytkownik otrzymuje unikalny identyfikator zwany session ID, przekazywany w cookie. Służy on jako klucz do danych sesji. W przeciwieństwie do cookies, które przechowywane są po stronie przeglądarki, dane sesji przechowywane są po stronie serwera. -Sesję ustawiamy w [konfiguracji |configuration#Sesja], ważny jest zwłaszcza wybór czasu wygaśnięcia. +Sesję konfigurujemy w [konfiguracji |configuration#Sesja]; szczególnie ważny jest wybór czasu wygaśnięcia. -Zarządzaniem sesją zajmuje się obiekt [api:Nette\Http\Session], do którego dostaniesz się, prosząc o jego przekazanie za pomocą [wstrzykiwania zależności |dependency-injection:passing-dependencies]. W prezenterach wystarczy tylko wywołać `$session = $this->getSession()`. +Zarządzaniem sesją zajmuje się obiekt [api:Nette\Http\Session], do którego dostaniesz się, pozwalając sobie go przekazać przez [wstrzykiwanie zależności |dependency-injection:passing-dependencies]. W presenterach wystarczy wywołać `$session = $this->getSession()`. → [Instalacja i wymagania |@home#Instalacja] @@ -23,40 +23,40 @@ Zarządzaniem sesją zajmuje się obiekt [api:Nette\Http\Session], do którego d Uruchomienie sesji ================== -Nette w domyślnych ustawieniach automatycznie rozpoczyna sesję w momencie, gdy zaczynamy z niej czytać lub do niej zapisywać dane. Ręcznie sesję rozpoczyna się za pomocą `$session->start()`. +Domyślnie Nette uruchamia sesję automatycznie w momencie, gdy zaczniemy z niej czytać albo do niej pisać dane. Ręcznie sesję uruchomisz za pomocą `$session->start()`. -PHP wysyła przy uruchomieniu sesji nagłówki HTTP wpływające na buforowanie, zobacz [php:session_cache_limiter], a ewentualnie również ciasteczko z ID sesji. Dlatego konieczne jest zawsze uruchomienie sesji jeszcze przed wysłaniem jakiegokolwiek wyjścia do przeglądarki, w przeciwnym razie zostanie zgłoszony wyjątek. Jeśli więc wiesz, że w trakcie renderowania strony będzie używana sesja, uruchom ją ręcznie wcześniej, na przykład w prezenterze. +PHP przy uruchamianiu sesji wysyła nagłówki HTTP wpływające na cache (patrz [php:session_cache_limiter]), a ewentualnie także cookie z session ID. Dlatego zawsze trzeba uruchomić sesję przed wysłaniem jakiegokolwiek wyjścia do przeglądarki, w przeciwnym razie zostanie rzucony wyjątek. Jeśli więc wiesz, że w trakcie renderowania strony będzie używana sesja, uruchom ją wcześniej ręcznie, na przykład w presenterze. -W trybie deweloperskim sesję uruchamia Tracy, ponieważ używa jej do wyświetlania pasków z przekierowaniami i żądaniami AJAX w Tracy Bar. +W trybie deweloperskim Tracy uruchamia sesję, bo używa jej do wyświetlania pasków dla przekierowań i żądań AJAX w Tracy Barze. Sekcje ====== -W czystym PHP magazyn danych sesji jest realizowany jako tablica dostępna przez zmienną globalną `$_SESSION`. Problem polega na tym, że aplikacje zwykle składają się z wielu wzajemnie niezależnych części i jeśli wszystkie mają do dyspozycji tylko jedną tablicę, wcześniej czy później dojdzie do kolizji nazw. +W czystym PHP magazyn danych sesji zrealizowany jest jako tablica dostępna przez zmienną globalną `$_SESSION`. Problem w tym, że aplikacje zwykle składają się z wielu niezależnych części, a jeśli wszystkie mają do dyspozycji tylko jedną tablicę, prędzej czy później dojdzie do kolizji nazw. -Nette Framework rozwiązuje problem, dzieląc całą przestrzeń na sekcje (obiekty [api:Nette\Http\SessionSection]). Każda jednostka używa następnie swojej sekcji z unikalną nazwą i do żadnej kolizji już dojść nie może. +Nette Framework rozwiązuje ten problem, dzieląc całą przestrzeń na sekcje (obiekty [api:Nette\Http\SessionSection]). Każda jednostka używa wtedy własnej sekcji o unikalnej nazwie i do żadnej kolizji nie może dojść. Sekcję uzyskujemy z sesji: ```php -$section = $session->getSection('unikalna nazwa'); +$section = $session->getSection('unique name'); ``` -W prezenterze wystarczy użyć `getSession()` z parametrem: +W presenterze wystarczy użyć `getSession()` z parametrem: ```php // $this to Presenter -$section = $this->getSession('unikalna nazwa'); +$section = $this->getSession('unique name'); ``` -Sprawdzić istnienie sekcji można metodą `$session->hasSection('unikalna nazwa')`. +Istnienie sekcji można sprawdzić metodą `$session->hasSection('unique name')`. Listę nazw wszystkich istniejących sekcji zwraca `$session->getSectionNames()`. -Z samą sekcją pracuje się następnie bardzo łatwo za pomocą metod `set()`, `get()` i `remove()`: +Praca z samą sekcją jest potem bardzo łatwa za pomocą metod `set()`, `get()` i `remove()`: ```php // zapis zmiennej -$section->set('userName', 'franta'); +$section->set('userName', 'john'); // odczyt zmiennej, zwraca null, jeśli nie istnieje echo $section->get('userName'); @@ -65,7 +65,7 @@ echo $section->get('userName'); $section->remove('userName'); ``` -Aby uzyskać wszystkie zmienne z sekcji, można użyć pętli `foreach`: +Żeby uzyskać wszystkie zmienne z sekcji, możesz użyć pętli `foreach`: ```php foreach ($section as $key => $val) { @@ -74,33 +74,33 @@ foreach ($section as $key => $val) { ``` -Ustawienie wygaśnięcia ----------------------- +Jak ustawić wygaśnięcie +----------------------- -Dla poszczególnych sekcji lub nawet poszczególnych zmiennych można ustawić wygaśnięcie. Możemy w ten sposób pozwolić na wygaśnięcie logowania użytkownika po 20 minutach, ale jednocześnie nadal pamiętać zawartość koszyka. +Wygaśnięcie można ustawić dla poszczególnych sekcji, a nawet dla poszczególnych zmiennych. Możemy pozwolić, żeby logowanie użytkownika wygasło po 20 minutach, a jednocześnie dalej pamiętać zawartość koszyka. ```php -// sekcja wygaśnie po 20 minutach +// sekcja wygasa po 20 minutach $section->setExpiration('20 minutes'); ``` Do ustawienia wygaśnięcia poszczególnych zmiennych służy trzeci parametr metody `set()`: ```php -// zmienna 'flash' wygaśnie już po 30 sekundach +// zmienna 'flash' wygasa po 30 sekundach $section->set('flash', $message, '30 seconds'); ``` .[note] -Pamiętaj, że czas wygaśnięcia całej sesji (zobacz [konfiguracja sesji |configuration#Sesja]) musi być taki sam lub dłuższy niż czas ustawiony dla poszczególnych sekcji lub zmiennych. +Pamiętaj, że czas wygaśnięcia całej sesji (patrz [konfiguracja sesji |configuration#Sesja]) musi być równy czasowi ustawionemu dla poszczególnych sekcji albo zmiennych, albo od niego dłuższy. -Anulowanie wcześniej ustawionego wygaśnięcia uzyskamy metodą `removeExpiration()`. Natychmiastowe anulowanie całej sekcji zapewni metoda `remove()`. +Do anulowania wcześniej ustawionego wygaśnięcia służy metoda `removeExpiration()`; żeby wyczyścić wygaśnięcie konkretnej zmiennej, przekaż jej nazwę: `removeExpiration('flash')`. Do natychmiastowego usunięcia całej sekcji służy metoda `remove()`. Zdarzenia $onStart, $onBeforeWrite ---------------------------------- -Obiekt `Nette\Http\Session` ma [zdarzenia |nette:glossary#Eventy zdarzenia] `$onStart` i `$onBeforeWrite`, możesz więc dodać callbacki, które zostaną wywołane po starcie sesji lub przed jej zapisem na dysk i późniejszym zakończeniem. +Obiekt `Nette\Http\Session` ma [zdarzenia |nette:glossary#Zdarzenia] `$onStart` i `$onBeforeWrite`, więc możesz dodać callbacki wywoływane po uruchomieniu sesji albo przed jej zapisem na dysk i późniejszym zakończeniem. ```php $session->onBeforeWrite[] = function () { @@ -113,24 +113,24 @@ $session->onBeforeWrite[] = function () { Zarządzanie sesją ================= -Przegląd metod klasy `Nette\Http\Session` do zarządzania sesją: +Przegląd metod klasy `Nette\Http\Session` służących do zarządzania sesją: <div class=wiki-methods-brief> start(): void .[method] ----------------------- -Rozpoczyna sesję. +Uruchamia sesję. isStarted(): bool .[method] --------------------------- -Czy sesja jest rozpoczęta? +Czy sesja jest uruchomiona? close(): void .[method] ----------------------- -Kończy sesję. Sesja jest automatycznie kończona na końcu działania skryptu. +Kończy sesję. Sesja kończy się automatycznie na końcu wykonywania skryptu. destroy(): void .[method] @@ -140,17 +140,17 @@ Kończy i usuwa sesję. exists(): bool .[method] ------------------------ -Czy żądanie HTTP zawiera ciasteczko z ID sesji? +Czy żądanie HTTP zawiera cookie z session ID? regenerateId(): void .[method] ------------------------------ -Generuje nowy losowy ID sesji. Dane pozostają zachowane. +Generuje nowe losowe session ID. Dane pozostają zachowane. getId(): string .[method] ------------------------- -Zwraca ID sesji. +Zwraca session ID. </div> @@ -158,44 +158,44 @@ Zwraca ID sesji. Konfiguracja ------------ -Sesję ustawiamy w [konfiguracji |configuration#Sesja]. Jeśli piszesz aplikację, która nie używa kontenera DI, do konfiguracji służą te metody. Muszą być wywołane jeszcze przed uruchomieniem sesji. +Sesję konfigurujemy w [konfiguracji |configuration#Sesja]. Jeśli piszesz aplikację, która nie używa kontenera DI, do konfiguracji użyj tych metod. Muszą być wywołane przed uruchomieniem sesji. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Ustawia nazwę ciasteczka, w którym przekazywane jest ID sesji. Standardowa nazwa to `PHPSESSID`. Przydatne, gdy w ramach jednej strony internetowej działa kilka różnych aplikacji. +Ustawia nazwę cookie, w którym przesyłane jest session ID. Standardowa nazwa to `PHPSESSID`. Przydaje się to, gdy na tej samej witrynie uruchamiasz kilka różnych aplikacji. getName(): string .[method] --------------------------- -Zwraca nazwę ciasteczka, w którym przekazywane jest ID sesji. +Zwraca nazwę cookie, w którym przesyłane jest session ID. setOptions(array $options): static .[method] -------------------------------------------- -Konfiguruje sesję. Można ustawiać wszystkie [dyrektywy sesji |https://www.php.net/manual/en/session.configuration.php] PHP (w formacie camelCase, np. zamiast `session.save_path` zapisujemy `savePath`) oraz [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. +Konfiguruje sesję. Można ustawić wszystkie [dyrektywy sesji |https://www.php.net/manual/en/session.configuration.php] PHP (w formacie camelCase, np. zamiast `session.save_path` pisz `savePath`), a także [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Ustawia czas nieaktywności, po którym sesja wygaśnie. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Ustawia czas nieaktywności, po którym sesja wygasa. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Ustawienie parametrów dla ciasteczka. Domyślne wartości parametrów można zmienić w [konfiguracji |configuration#Ciasteczko sesji]. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +Ustawia parametry cookies. Domyślne wartości parametrów możesz zmienić w [konfiguracji |configuration#Cookie sesji]. setSavePath(string $path): static .[method] ------------------------------------------- -Ustawia katalog, w którym zapisywane są pliki sesji. +Ustawia katalog, w którym przechowywane są pliki sesji. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Ustawienie własnego handlera, zobacz [dokumentację PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Ustawia własny handler, patrz [dokumentacja PHP |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> @@ -203,9 +203,9 @@ Ustawienie własnego handlera, zobacz [dokumentację PHP|https://www.php.net/man Bezpieczeństwo przede wszystkim =============================== -Serwer zakłada, że komunikuje się ciągle z tym samym użytkownikiem, dopóki żądania towarzyszy ten sam ID sesji. Zadaniem mechanizmów bezpieczeństwa jest zapewnienie, aby tak rzeczywiście było i nie było możliwe kradzież lub podstawienie identyfikatora. +Serwer zakłada, że komunikuje się z tym samym użytkownikiem, dopóki żądaniom towarzyszy to samo session ID. Zadaniem mechanizmów bezpieczeństwa jest zapewnić, żeby tak rzeczywiście było i żeby identyfikatora nie dało się ukraść ani podmienić. -Nette Framework dlatego poprawnie konfiguruje dyrektywy PHP, aby ID sesji przekazywał tylko w ciasteczku, uniemożliwił dostęp do niego JavaScriptowi i ignorował ewentualne identyfikatory w URL. Ponadto w krytycznych momentach, jak na przykład logowanie użytkownika, generuje nowy ID sesji. +Nette Framework dlatego poprawnie konfiguruje dyrektywy PHP tak, żeby session ID przesyłane było wyłącznie w cookies, było niedostępne dla JavaScriptu i żeby ewentualne identyfikatory w URL były ignorowane. Poza tym w krytycznych momentach, jak logowanie użytkownika, generuje nowe session ID. .[note] -Do konfiguracji PHP używana jest funkcja ini_set, której niestety niektóre hostingi zabraniają. Jeśli jest to przypadek również Twojego hostingu, spróbuj się z nim dogadać, aby pozwolił Ci na użycie funkcji lub przynajmniej skonfigurował serwer. +Do konfiguracji PHP używana jest funkcja `ini_set`, którą niestety niektórzy hostingodawcy zabraniają. Jeśli tak jest u Twojego hostingodawcy, spróbuj się z nim umówić, żeby tę funkcję Ci udostępnił albo przynajmniej odpowiednio skonfigurował serwer. diff --git a/http/pl/ssrf.texy b/http/pl/ssrf.texy new file mode 100644 index 0000000000..5ca2b59b0b --- /dev/null +++ b/http/pl/ssrf.texy @@ -0,0 +1,183 @@ +Ochrona przed SSRF +****************** + +.[perex] +Gdy Twoja aplikacja pobiera URL podany przez użytkownika, atakujący może to wykorzystać, żeby dosięgnąć Twojej sieci wewnętrznej. Klasy [#UrlValidator] i [#IPAddress] pomagają Ci bronić się przed tymi atakami Server-Side Request Forgery (SSRF). + +→ [Instalacja i wymagania |@home#Instalacja] + + +Czym jest SSRF? +=============== + +Wyobraź sobie funkcję, w której użytkownik wpisuje URL, a Twój serwer go pobiera: awatar spod zdalnego adresu, cel webhooka, podgląd odnośnika. Wygląda niewinnie, ale pod adres sięga serwer, a nie przeglądarka użytkownika. A serwer widzi miejsca, których atakujący nie widzi: interfejs loopback, sieć prywatną, usługi chmurowe. + +Atakujący podaje więc URL wskazujący do wewnątrz zamiast do publicznego internetu. Typowe cele to: + +- metadane chmury pod `http://169.254.169.254/`, z których mogą wyciec klucze dostępowe +- wewnętrzne panele administracyjne i routery, jak `http://192.168.1.1/` +- usługi bez uwierzytelniania, na przykład Redis pod `http://localhost:6379/` + +Ta klasa podatności jest tak powszechna, że plasuje się w [OWASP Top 10 |https://owasp.org/Top10/]. Obroną jest zwalidowanie URL **przed** jego pobraniem i odrzucenie wszystkiego, co rozwiązuje się do adresu niepublicznego. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] sprawdza URL względem konfigurowalnej polityki: schematu, portu, hosta, userinfo i adresów IP, do których host się rozwiązuje. Podstawowe użycie to jedno wywołanie: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // niebezpieczny URL, nie pobieraj go +} +``` + +Domyślna polityka jest celowo surowa: przyjmuje wyłącznie `https` na porcie 443 wskazujące na publiczny adres IP. Wszystko inne (loopback, zakresy prywatne, link-local wraz z metadanymi chmury, zakresy zarezerwowane) jest odrzucane, a multicast odrzucany jest bezwarunkowo. To właściwy punkt wyjścia przy pobieraniu dowolnych URL-i podanych przez użytkownika. + + +Konfiguracja polityki +--------------------- + +Politykę kształtujesz przez konstruktor. Na przykład żeby dopuścić zwykłe `http` na dowolnym porcie i sięgać po adresy prywatne (przydatne wewnątrz zaufanej sieci): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // dowolny port + allowPrivateIps: true, +); +``` + +Częstym wzorcem jest ograniczenie pobierania do ustalonego zbioru domen partnerskich za pomocą whitelisty hostów. Przedrostek `*.` dopasowuje subdomeny dowolnej głębokości, ale nie samą domenę: jeśli tego potrzebujesz, wypisz obie formy: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +Pełny zestaw opcji konstruktora: + +| Parametr | Domyślnie | Znaczenie +|--------------------- +| `schemes` | `['https']` | dozwolone schematy; `[]` odrzuca wszystko +| `ports` | `[443]` | dozwolone porty, `null` = dowolny; niejawny port ze schematu jest respektowany +| `allowPrivateIps` | `false` | dopuszcza zakresy prywatne (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | dopuszcza loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | dopuszcza link-local wraz z metadanymi chmury 169.254.169.254 +| `allowReserved` | `false` | dopuszcza zakresy zarezerwowane przez IANA +| `allowUserinfo` | `false` | dopuszcza `user:pass@` w URL +| `hostAllowlist` | `null` | jeśli ustawione, host musi pasować do jednego ze wzorców; `[]` odrzuca wszystkie +| `hostBlocklist` | `null` | jeśli ustawione, host nie może pasować do żadnego wzorca + + +Metody walidacyjne +------------------ + +Walidator oferuje trzy metody. `allows()` przeprowadza pełną kontrolę wraz z rozwiązaniem DNS: host jest rozwiązywany i **każdy** adres A/AAAA musi przejść politykę IP: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` pomija rozwiązanie DNS i kontrole zakresów IP. Użyj jej jako szybkiego filtra wstępnego albo wtedy, gdy walidacja DNS jest delegowana do warstwy pobierającej: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Obie metody przyjmują ciąg, obiekt [UrlImmutable |urls#UrlImmutable] albo `null` (który zawsze nie przechodzi). + + +Pokonanie DNS rebindingu +------------------------ + +Między walidacją a pobraniem istnieje subtelny wyścig: atakujący może zwrócić bezpieczne IP, gdy walidujesz host, a potem przełączyć DNS na wewnętrzne IP przy właściwym pobraniu. Żeby zamknąć tę lukę, `getResolvedIPs()` zwraca zwalidowane adresy IP, a Ty przypinasz do nich połączenie, żeby pobrania nie dało się przekierować gdzie indziej: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // niebezpieczny URL +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... wykonanie żądania +``` + +Metoda zwraca tablicę ciągów IP (najpierw rekordy A, potem AAAA), które przeszły pełną politykę, albo pustą tablicę przy jakimkolwiek niepowodzeniu. Dla literału IP w URL waliduje adres bezpośrednio i nie wykonuje zapytania DNS. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] to niezmienny obiekt wartości do pracy z adresami IPv4 i IPv6. `UrlValidator` używa go wewnętrznie, ale przydaje się też sam w sobie zawsze, gdy klasyfikujesz adresy. Konstruktor rzuca `Nette\InvalidArgumentException` dla nieprawidłowego adresu: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +Gdy nie chcesz wyjątku, użyj fabryki `tryFrom()` albo testu `isValid()`: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Klasyfikacja adresów +-------------------- + +Predykaty mówią Ci, do której klasy należy adres. Kluczowy jest `isPublic()`: true tylko dla adresów publicznie routowalnych, czyli dokładnie tego, czego chce ochrona przed SSRF: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (zakres metadanych chmury) +``` + +Pełny zestaw predykatów: + +| Metoda | Testuje +|-------------------- +| `isPublic()` | publicznie routowalny (żaden z poniższych) +| `isPrivate()` | zakresy prywatne RFC 1918 / 4193 +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (wraz z metadanymi chmury), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | zarezerwowane przez IANA (dokumentacja, CGNAT, przyszłe użycie, …) + + +Przynależność do zakresu +------------------------ + +`isInRange()` testuje, czy adres mieści się w bloku CIDR. Możesz przekazać sieć z prefiksem albo goły adres dla dokładnego dopasowania (niejawne /32 dla IPv4, /128 dla IPv6): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (dokładne dopasowanie) +``` + +Zniekształcone wejście albo inna rodzina IP zwraca `false`. + + +IPv6 z mapowanym IPv4 +--------------------- + +Adresy zapisane jako IPv6 z mapowanym IPv4 (jak `::ffff:127.0.0.1`) to klasyczny sposób na prześlizgnięcie się obok naiwnych filtrów. `IPAddress` je normalizuje, więc predykaty zakresów przejrzą to przebranie: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +Metody `isIPv4()` i `isIPv6()` raportują postać tekstową: adres mapowany jest IPv6, a nie IPv4. diff --git a/http/pl/upgrading.texy b/http/pl/upgrading.texy new file mode 100644 index 0000000000..ed66475e0c --- /dev/null +++ b/http/pl/upgrading.texy @@ -0,0 +1,54 @@ +Aktualizacja +************ + + +Aktualizacja do wersji 3.4 +========================== + +Minimalna wymagana wersja PHP to 8.3. + +- metoda `Request::isSameSite()` jest przestarzała na rzecz `isFrom()`, która ustala pochodzenie żądania z nagłówków `Sec-Fetch-*`. Automatyczna ochrona formularzy i sygnałów staje się precyzyjniejsza, a jedno zachowanie się zmienia: bezpośrednie wejście (zakładka, ręcznie wpisany adres, odnośnik w e-mailu) nie jest już uznawane za same-site. Jeśli sygnał opiera się na odnośnikach akcji w e-mailach, oznacz go `#[Requires(sameOrigin: false)]`. +- cookie `_nss` jest teraz wysyłane tylko do przeglądarek, które nie wysyłają nagłówka `Sec-Fetch-Site` +- `setCookie()` wysyła atrybut `Max-Age` i wymusza flagę `Secure` dla `SameSite=None` oraz dla cookies partycjonowanych +- enum `SameSite` zastępuje stałe `IResponse::SameSiteLax` itd., które są przestarzałe +- wygaśnięcie interpretowane jest wszędzie tak samo: liczba to względna liczba sekund, ciąg to interwał albo data. Przekazywanie absolutnego uniksowego timestampu jest przestarzałe, a cookie sesyjne reprezentuje `null` zamiast `0`. +- przestarzała metoda `Request::getRemoteHost()` zwraca `null` +- dawno przestarzała klasa `Nette\Http\UserStorage` została usunięta + +Całą historię przejścia na nagłówki `Sec-Fetch-*` opowiada artykuł [Ćwierć wieku CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Aktualizacja do wersji 3.2 +========================== + +- dane uwierzytelniające z HTTP Basic Authentication nie są już częścią obiektu `Url`, więc `$url->getUser()` i `$url->getPassword()` zwracają pusty ciąg. Odczytasz je nową metodą `$request->getBasicCredentials()`. + +Powody tej zmiany wyjaśnia artykuł [Nette Http 3.2: zmiana dostępu do danych uwierzytelniających |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]. + + +Aktualizacja do wersji 3.1 +========================== + +- cookies wysyłane są z flagą `sameSite: Lax` +- `cookieSecure` ma teraz domyślnie wartość 'auto' +- opcja `session.cookieSecure` jest przestarzała, używane jest zamiast niej `http.cookieSecure` +- cookie `nette-samesite` zostało przemianowane na `_nss` +- `Nette\Http\Request::getFile()` przyjmuje tablicę kluczy i zwraca `FileUpload|null` +- `Nette\Http\Session::getCookieParameters()` jest przestarzałe +- `Nette\Http\FileUpload::getName()` zostało przemianowane na `getUntrustedName()` +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()` i `getRelativeUrl()` są przestarzałe (metody te są częścią `UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` jest przestarzałe +- `Nette\Http\FileUpload::getImageSize()` zwraca parę `[width, height]` +- przy `autoStart: smart` (domyślne) sesja nie jest już uruchamiana zaraz po starcie aplikacji tylko dlatego, że przeglądarka wysłała cookie sesyjne; uruchamia się przy pierwszym odczycie albo zapisie. Dodano wartości `always` i `never`. +- gdy przeglądarka wyśle ID sesji, dla którego żadna sesja nie istnieje, Nette usuwa cookie zamiast tworzyć nową sesję +- do dostępu do sekcji sesji preferuj metody `set()`, `get()` i `remove()`; w przeciwieństwie do dostępu przez właściwości poprawnie rozróżniają odczyt od zapisu i nie uruchamiają sesji niepotrzebnie +- wartości domyślne `cookiePath` i `cookieDomain` można ustawić w konfiguracji + +Zachowanie sesji opisuje szczegółowo artykuł [Nette Http 3.1: znacznie sprytniejsze sesje |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions]. + + +Aktualizacja do wersji 3.0 +========================== + +- obiekt `Nette\Http\UrlScript` (zwracany np. przez `Nette\Http\Request::getUrl()`) jest teraz niezmienny +- w `new Nette\Http\Url('abcd')` `abcd` reprezentuje ścieżkę, a nie domenę; od 3.0 `(new Nette\Http\Url('abcd'))->setScheme('http')` poprawnie generuje `http:abcd` zamiast wcześniejszego `http://abcd` diff --git a/http/pl/urls.texy b/http/pl/urls.texy index c2d29b6fcd..3d88b6b756 100644 --- a/http/pl/urls.texy +++ b/http/pl/urls.texy @@ -2,7 +2,7 @@ Praca z adresami URL ******************** .[perex] -Klasy [#Url], [#UrlImmutable] i [#UrlScript] umożliwiają łatwe generowanie, parsowanie i manipulowanie adresami URL. +Klasy [#Url], [#UrlImmutable] i [#UrlScript] ułatwiają generowanie, parsowanie i manipulowanie adresami URL. → [Instalacja i wymagania |@home#Instalacja] @@ -10,7 +10,7 @@ Klasy [#Url], [#UrlImmutable] i [#UrlScript] umożliwiają łatwe generowanie, p Url === -Klasa [api:Nette\Http\Url] umożliwia łatwą pracę z adresami URL i ich poszczególnymi komponentami, które przedstawia ten schemat: +Klasa [api:Nette\Http\Url] pozwala łatwo manipulować adresami URL i ich poszczególnymi składowymi, jak pokazuje ten diagram: /--pre scheme user password host port path query fragment @@ -22,7 +22,7 @@ Klasa [api:Nette\Http\Url] umożliwia łatwą pracę z adresami URL i ich poszcz hostUrl authority \-- -Generowanie URL jest intuicyjne: +Generowanie URL-i jest intuicyjne: ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -Można również sparsować URL i dalej nim manipulować: +URL możesz też sparsować, a następnie nim manipulować: ```php $url = new Url( @@ -44,7 +44,7 @@ $url = new Url( ); ``` -Klasa `Url` implementuje interfejs `JsonSerializable` i ma metodę `__toString()`, więc obiekt można wypisać lub użyć w danych przekazywanych do `json_encode()`. +Klasa `Url` implementuje interfejs `JsonSerializable` i ma metodę `__toString()`, więc obiekt można wypisać albo użyć w danych przekazywanych do `json_encode()`. ```php echo $url; @@ -52,10 +52,10 @@ echo json_encode([$url]); ``` -Komponenty URL .[method] ------------------------- +Składowe URL +------------ -Do zwracania lub zmiany poszczególnych komponentów URL dostępne są następujące metody: +Do odczytu albo zmiany poszczególnych składowych URL służą poniższe metody: .[language-php] | Setter | Getter | Zwracana wartość @@ -73,20 +73,23 @@ Do zwracania lub zmiany poszczególnych komponentów URL dostępne są następuj | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | cały URL -Uwaga: Pracując z adresem URL uzyskanym z [żądania HTTP|request], pamiętaj, że nie będzie on zawierał fragmentu, ponieważ przeglądarka go nie wysyła na serwer. +Metody `getUser()`, `getPassword()`, `setUser()` i `setPassword()` są przestarzałe, bo osadzanie danych uwierzytelniających bezpośrednio w URL jest odradzane. + +Uwaga: przy pracy z URL uzyskanym z [żądania HTTP |request] pamiętaj, że nie będzie zawierał fragmentu, bo przeglądarka nie wysyła go na serwer. -Możemy również pracować z poszczególnymi parametrami zapytania za pomocą: +Z poszczególnymi parametrami query możemy pracować za pomocą: .[language-php] | Setter | Getter |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] ------------------------------------------- -Zwraca prawą lub lewą część hosta. Działa w ten sposób, jeśli host to `www.nette.org`: +Zwraca prawą albo lewą część hosta. Oto jak to działa, jeśli hostem jest `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -98,18 +101,23 @@ Zwraca prawą lub lewą część hosta. Działa w ten sposób, jeśli host to `w | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Sprawdza, czy dwa adresy URL są identyczne. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +Sprawdza, czy dwa URL-e są identyczne. ```php $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Konwertuje URL do postaci kanonicznej. Zamienia nazwę hosta na małe litery i normalizuje ścieżkę (percent-encoding i usunięcie zbędnych znaków). Query string pozostaje niezmieniony. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- -Sprawdza, czy adres URL jest absolutny. Adres URL jest uważany za absolutny, jeśli zaczyna się od schematu (np. http, https, ftp) poprzedzonego dwukropkiem. +Sprawdza, czy URL jest absolutny. URL uznaje się za absolutny, jeśli zaczyna się schematem (np. http, https, ftp), po którym następuje dwukropek. ```php Url::isAbsolute('https://nette.org'); // true @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -Normalizuje ścieżkę w adresie URL, usuwając specjalne segmenty `.` i `..`. Metoda usuwa zbędne elementy ścieżki w taki sam sposób, jak robią to przeglądarki internetowe. +Normalizuje ścieżkę URL, usuwając specjalne segmenty `.` i `..`. Metoda usuwa zbędne elementy ścieżki tak samo, jak robią to przeglądarki. ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,24 +139,24 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -Klasa [api:Nette\Http\UrlImmutable] jest immutable (niezmienną) alternatywą klasy [#Url] (podobnie jak w PHP `DateTimeImmutable` jest niezmienną alternatywą `DateTime`). Zamiast setterów ma tzw. withery, które nie zmieniają obiektu, ale zwracają nowe instancje ze zmodyfikowaną wartością: +Klasa [api:Nette\Http\UrlImmutable] to niezmienna alternatywa dla klasy [#Url] (podobnie jak `DateTimeImmutable` jest w PHP niezmienną alternatywą dla `DateTime`). Zamiast setterów ma "withery", które nie zmieniają obiektu, tylko zwracają nowe instancje ze zmienioną wartością: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` -Klasa `UrlImmutable` implementuje interfejs `JsonSerializable` i ma metodę `__toString()`, więc obiekt można wypisać lub użyć w danych przekazywanych do `json_encode()`. +Klasa `UrlImmutable` implementuje interfejs `JsonSerializable` i ma metodę `__toString()`, więc obiekt można wypisać albo użyć w danych przekazywanych do `json_encode()`. ```php echo $url; @@ -156,10 +164,10 @@ echo json_encode([$url]); ``` -Komponenty URL .[method] ------------------------- +Składowe URL +------------ -Do zwracania lub zmiany poszczególnych komponentów URL służą metody: +Do odczytu albo zmiany poszczególnych składowych URL służą poniższe metody: .[language-php] | Wither | Getter | Zwracana wartość @@ -177,9 +185,9 @@ Do zwracania lub zmiany poszczególnych komponentów URL służą metody: | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | cały URL -Metoda `withoutUserInfo()` usuwa `user` i `password`. +Metody `getUser()`, `getPassword()`, `withUser()`, `withPassword()` i `withoutUserInfo()` są przestarzałe, bo osadzanie danych uwierzytelniających bezpośrednio w URL jest odradzane. -Możemy również pracować z poszczególnymi parametrami zapytania za pomocą: +Z poszczególnymi parametrami query możemy pracować za pomocą: .[language-php] | Wither | Getter @@ -190,7 +198,7 @@ Możemy również pracować z poszczególnymi parametrami zapytania za pomocą: getDomain(int $level = 2): string .[method] ------------------------------------------- -Zwraca prawą lub lewą część hosta. Działa w ten sposób, jeśli host to `www.nette.org`: +Zwraca prawą albo lewą część hosta. Oto jak to działa, jeśli hostem jest `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -204,11 +212,11 @@ Zwraca prawą lub lewą część hosta. Działa w ten sposób, jeśli host to `w resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -Wyprowadza absolutny adres URL w taki sam sposób, w jaki przeglądarka przetwarza linki na stronie HTML: -- jeśli link jest absolutnym adresem URL (zawiera schemat), jest używany bez zmian -- jeśli link zaczyna się od `//`, pobierany jest tylko schemat z bieżącego adresu URL -- jeśli link zaczyna się od `/`, tworzona jest ścieżka absolutna od korzenia domeny -- w pozostałych przypadkach adres URL jest budowany względnie do bieżącej ścieżki +Rozwiązuje absolutny URL tak samo, jak przeglądarka przetwarza odnośniki na stronie HTML: +- jeśli odnośnik jest absolutnym URL (zawiera schemat), używany jest bez zmian +- jeśli odnośnik zaczyna się od `//`, przejmowany jest tylko schemat z bieżącego URL +- jeśli odnośnik zaczyna się od `/`, tworzona jest absolutna ścieżka od korzenia domeny +- w pozostałych przypadkach URL składany jest względem bieżącej ścieżki ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,9 +226,9 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Sprawdza, czy dwa adresy URL są identyczne. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +Sprawdza, czy dwa URL-e są identyczne. ```php $url->isEqual('https://nette.org'); @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -Klasa [api:Nette\Http\UrlScript] jest potomkiem [#UrlImmutable] i rozszerza go o dodatkowe wirtualne komponenty URL, takie jak katalog główny projektu itp. Podobnie jak klasa macierzysta, jest obiektem immutable (niezmiennym). +Klasa [api:Nette\Http\UrlScript] to potomek [#UrlImmutable] rozszerzający ją o kolejne wirtualne składowe URL, jak katalog główny projektu itd. Podobnie jak klasa nadrzędna jest obiektem niezmiennym. -Poniższy diagram przedstawia komponenty, które UrlScript rozpoznaje: +Poniższy diagram pokazuje składowe, które UrlScript rozpoznaje: /--pre baseUrl basePath relativePath relativeUrl @@ -244,14 +252,14 @@ Poniższy diagram przedstawia komponenty, które UrlScript rozpoznaje: scriptPath pathInfo \-- -- `baseUrl` to podstawowy adres URL aplikacji, w tym domena i część ścieżki do katalogu głównego aplikacji +- `baseUrl` to bazowy URL aplikacji wraz z domeną i częścią ścieżki do katalogu głównego aplikacji - `basePath` to część ścieżki do katalogu głównego aplikacji - `scriptPath` to ścieżka do bieżącego skryptu -- `relativePath` to nazwa skryptu (ewentualnie dodatkowe segmenty ścieżki) względna do `basePath` -- `relativeUrl` to cała część adresu URL za `baseUrl`, w tym query string i fragment. -- `pathInfo` to dziś rzadko używana część adresu URL za nazwą skryptu +- `relativePath` to nazwa skryptu (i ewentualnie kolejne segmenty ścieżki) względem `basePath` +- `relativeUrl` to cała część URL za `baseUrl` wraz z query stringiem i fragmentem +- `pathInfo` to dziś rzadko używana część URL za nazwą skryptu -Do zwracania części URL dostępne są metody: +Do odczytu tych części URL służą poniższe metody: .[language-php] | Getter | Zwracana wartość @@ -259,8 +267,8 @@ Do zwracania części URL dostępne są metody: | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Obiekty `UrlScript` zwykle nie tworzymy bezpośrednio, ale zwraca je metoda [Nette\Http\Request::getUrl()|request] z już poprawnie ustawionymi komponentami dla bieżącego żądania HTTP. +Obiektów `UrlScript` zwykle nie tworzymy bezpośrednio; zamiast tego zwraca go metoda [Nette\Http\Request::getUrl() |request] z już poprawnie ustawionymi składowymi dla bieżącego żądania HTTP. diff --git a/http/pt/@home.texy b/http/pt/@home.texy deleted file mode 100644 index 6921df5499..0000000000 --- a/http/pt/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -O pacote `nette/http` encapsula a [requisição HTTP|request] & [resposta HTTP|response], trabalho com [sessões|sessions] e [análise e composição de URLs |urls]. - - -Instalação ----------- - -Faça o download e instale a biblioteca usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/pt/@left-menu.texy b/http/pt/@left-menu.texy deleted file mode 100644 index f20249494e..0000000000 --- a/http/pt/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Introdução |@home] -- [Requisição HTTP|request] -- [Resposta HTTP|response] -- [Sessões|Sessions] -- [Utilitários de URL |urls] -- [Configuração |configuration] diff --git a/http/pt/@meta.texy b/http/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/http/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/http/pt/configuration.texy b/http/pt/configuration.texy deleted file mode 100644 index e13f7301c1..0000000000 --- a/http/pt/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Configuração HTTP -***************** - -.[perex] -Visão geral das opções de configuração para Nette HTTP. - -Se você não usa o framework inteiro, mas apenas esta biblioteca, leia [como carregar a configuração|bootstrap:]. - - -Cabeçalhos HTTP -=============== - -```neon -http: - # cabeçalhos que são enviados com cada requisição - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # afeta o cabeçalho X-Frame-Options - frames: ... # (string|bool) padrão é 'SAMEORIGIN' -``` - -O framework, por razões de segurança, envia o cabeçalho `X-Frame-Options: SAMEORIGIN`, que diz que a página só pode ser exibida dentro de outra página (no elemento `<iframe>`) se estiver no mesmo domínio. Isso pode ser indesejável em algumas situações (por exemplo, se você estiver desenvolvendo uma aplicação para o Facebook), o comportamento pode, portanto, ser alterado definindo `frames: http://allowed-host.com` ou `frames: true`. - - -Content Security Policy ------------------------ - -É fácil construir cabeçalhos `Content-Security-Policy` (doravante CSP), sua descrição pode ser encontrada na [descrição do CSP |https://content-security-policy.com]. As diretivas CSP (como `script-src`) podem ser escritas como strings de acordo com a especificação, ou como um array de valores para melhor legibilidade. Então não é necessário colocar aspas em torno de palavras-chave, como `'self'`. Nette também gera automaticamente o valor `nonce`, então no cabeçalho haverá, por exemplo, `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # string no formato de acordo com a especificação CSP - default-src: "'self' https://example.com" - - # array de valores - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool no caso de flags - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -Nos templates, use `<script n:nonce>...</script>` e o valor nonce será preenchido automaticamente. Criar sites seguros em Nette é realmente fácil. - -Da mesma forma, é possível construir os cabeçalhos `Content-Security-Policy-Report-Only` (que podem ser usados ​​simultaneamente com CSP) e [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -Cookie HTTP ------------ - -É possível alterar os valores padrão de alguns parâmetros do método [Nette\Http\Response::setCookie() |response#setCookie] e da sessão. - -```neon -http: - # escopo do cookie pelo caminho - cookiePath: ... # (string) padrão é '/' - - # domínios que aceitam o cookie - cookieDomain: 'example.com' # (string|domain) padrão é não definido - - # enviar cookie apenas via HTTPS? - cookieSecure: ... # (bool|auto) padrão é auto - - # desativa o envio do cookie que o Nette usa como proteção contra CSRF - disableNetteCookie: ... # (bool) padrão é false -``` - -O atributo `cookieDomain` determina quais domínios podem aceitar o cookie. Se não for especificado, o cookie é aceito pelo mesmo (sub)domínio que o definiu, *mas não* por seus subdomínios. Se `cookieDomain` for especificado, os subdomínios também são incluídos. Portanto, especificar `cookieDomain` é menos restritivo do que omiti-lo. - -Por exemplo, com `cookieDomain: nette.org`, os cookies também estão disponíveis em todos os subdomínios como `doc.nette.org`. O mesmo pode ser alcançado usando o valor especial `domain`, ou seja, `cookieDomain: domain`. - -O valor padrão `auto` para o atributo `cookieSecure` significa que, se o site estiver rodando em HTTPS, os cookies serão enviados com o sinalizador `Secure` e, portanto, estarão disponíveis apenas via HTTPS. - - -Proxy HTTP ----------- - -Se o site estiver rodando atrás de um proxy HTTP, forneça seu endereço IP para que a detecção de conexão via HTTPS e também o endereço IP do cliente funcionem corretamente. Ou seja, para que as funções [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] e [isSecured() |request#isSecured] retornem os valores corretos e nos templates sejam gerados links com o protocolo `https:`. - -```neon -http: - # Endereço IP, intervalo (por exemplo, 127.0.0.1/8) ou array desses valores - proxy: 127.0.0.1 # (string|string[]) padrão é não definido -``` - - -Sessão -====== - -Configurações básicas de [sessões|sessions]: - -```neon -session: - # exibir painel de sessão na Tracy Bar? - debugger: ... # (bool) padrão é false - - # tempo de inatividade após o qual a sessão expira - expiration: 14 days # (string) padrão é '3 hours' - - # quando a sessão deve ser iniciada? - autoStart: ... # (smart|always|never) padrão é 'smart' - - # handler, serviço implementando a interface SessionHandlerInterface - handler: @handlerService -``` - -A opção `autoStart` controla quando a sessão deve ser iniciada. O valor `always` significa que a sessão será iniciada sempre com o início da aplicação. O valor `smart` significa que a sessão será iniciada no início da aplicação apenas se já existir, ou no momento em que quisermos ler ou escrever nela. E, finalmente, o valor `never` proíbe o início automático da sessão. - -Além disso, é possível definir todas as [diretivas de sessão |https://www.php.net/manual/en/session.configuration.php] do PHP (no formato camelCase) e também [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Exemplo: - -```neon -session: - # 'session.name' escrevemos como 'name' - name: MYID - - # 'session.save_path' escrevemos como 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Cookie de sessão ----------------- - -O cookie de sessão é enviado com os mesmos parâmetros que [outros cookies |#Cookie HTTP], mas você pode alterá-los para ele: - -```neon -session: - # domínios que aceitam o cookie - cookieDomain: 'example.com' # (string|domain) - - # restrição ao acessar de outro domínio - cookieSamesite: None # (Strict|Lax|None) padrão é Lax -``` - -O atributo `cookieSamesite` afeta se o cookie será enviado durante o [acesso de outro domínio |nette:glossary#SameSite cookie], o que fornece alguma proteção contra ataques [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Serviços DI -=========== - -Estes serviços são adicionados ao contêiner de DI: - -| Nome | Tipo | Descrição -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [Requisição HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [Resposta HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [gerenciamento de sessão| sessions] diff --git a/http/pt/request.texy b/http/pt/request.texy deleted file mode 100644 index e9b9fe6086..0000000000 --- a/http/pt/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Requisição HTTP -*************** - -.[perex] -Nette encapsula a requisição HTTP em objetos com uma API compreensível e, ao mesmo tempo, fornece um filtro de sanitização. - -A requisição HTTP é representada pelo objeto [api:Nette\Http\Request]. Se trabalha com Nette, este objeto é criado automaticamente pelo framework e pode recebê-lo por meio de [injeção de dependência |dependency-injection:passing-dependencies]. Nos presenters, basta chamar o método `$this->getHttpRequest()`. Se trabalha fora do Nette Framework, pode criar o objeto usando [#RequestFactory]. - -Uma grande vantagem de Nette é que, ao criar o objeto, ele limpa automaticamente todos os parâmetros de entrada GET, POST, COOKIE e também a URL de caracteres de controlo e sequências UTF-8 inválidas. Com esses dados, pode trabalhar com segurança. Os dados limpos são então usados em presenters e formulários. - -→ [Instalação e requisitos |@home#Instalação] - - -Nette\Http\Request -================== - -Este objeto é imutável. Não possui setters, tem apenas um chamado wither `withUrl()`, que não altera o objeto, mas retorna uma nova instância com o valor alterado. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Retorna um clone com uma URL diferente. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Retorna a URL da requisição como um objeto [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Aviso: os navegadores não enviam o fragmento para o servidor, então `$url->getFragment()` retornará uma string vazia. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Retorna os parâmetros GET da requisição. - -```php -$all = $httpRequest->getQuery(); // retorna um array de todos os parâmetros da URL -$id = $httpRequest->getQuery('id'); // retorna o parâmetro GET 'id' (ou null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Retorna os parâmetros POST da requisição. - -```php -$all = $httpRequest->getPost(); // retorna um array de todos os parâmetros do POST -$id = $httpRequest->getPost('id'); // retorna o parâmetro POST 'id' (ou null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Retorna o [upload |#Upload de ficheiros] como um objeto [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // algum ficheiro foi enviado? - $file->getUntrustedName(); // nome do ficheiro enviado pelo utilizador - $file->getSanitizedName(); // nome sem caracteres perigosos -} -``` - -Para aceder à estrutura aninhada, forneça um array de chaves. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Como não se pode confiar nos dados externos e, portanto, nem na forma da estrutura dos ficheiros, este método é mais seguro do que, por exemplo, `$request->getFiles()['my-form']['details']['avatar']`, que pode falhar. - - -getFiles(): array .[method] ---------------------------- -Retorna a árvore de [todos os uploads |#Upload de ficheiros] numa estrutura normalizada, cujas folhas são objetos [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Retorna um cookie ou `null` se não existir. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Retorna todos os cookies. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Retorna o método HTTP com o qual a requisição foi feita. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Testa o método HTTP com o qual a requisição foi feita. O parâmetro é insensível a maiúsculas/minúsculas. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Retorna um cabeçalho HTTP ou `null` se não existir. O parâmetro é insensível a maiúsculas/minúsculas. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Retorna todos os cabeçalhos HTTP como um array associativo. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -A conexão é criptografada (HTTPS)? Para o funcionamento correto, pode ser necessário [configurar o proxy |configuration#Proxy HTTP]. - - -isSameSite(): bool .[method] ----------------------------- -A requisição vem do mesmo (sub)domínio e é iniciada clicando num link? Nette usa o cookie `_nss` (anteriormente `nette-samesite`) para deteção. - - -isAjax(): bool .[method] ------------------------- -É uma requisição AJAX? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Retorna o endereço IP do utilizador. Para o funcionamento correto, pode ser necessário [configurar o proxy |configuration#Proxy HTTP]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Retorna a resolução DNS do endereço IP do utilizador. Para o funcionamento correto, pode ser necessário [configurar o proxy |configuration#Proxy HTTP]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Retorna as credenciais de autenticação para [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Retorna o corpo da requisição HTTP. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Deteta o idioma. Como parâmetro `$lang`, passamos um array com os idiomas que a aplicação suporta, e ela retorna aquele que o navegador do visitante preferiria ver. Não há mágica, apenas o cabeçalho `Accept-Language` é usado. Se não houver correspondência, retorna `null`. - -```php -// o navegador envia, por exemplo, Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // idiomas suportados pela aplicação -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -A classe [api:Nette\Http\RequestFactory] serve para criar uma instância de `Nette\Http\Request`, que representa a requisição HTTP atual. (Se trabalha com Nette, o objeto da requisição HTTP é criado automaticamente pelo framework.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -O método `fromGlobals()` cria o objeto da requisição com base nas variáveis globais atuais do PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` e `$_SERVER`). Ao criar o objeto, ele limpa automaticamente todos os parâmetros de entrada GET, POST, COOKIE e também a URL de caracteres de controlo e sequências UTF-8 inválidas, o que garante a segurança ao trabalhar posteriormente com esses dados. - -A RequestFactory pode ser configurada antes de chamar `fromGlobals()`: - -- com o método `$factory->setBinary()`, desativa a limpeza automática dos parâmetros de entrada de caracteres de controlo e sequências UTF-8 inválidas. -- com o método `$factory->setProxy(...)`, indica o endereço IP do [servidor proxy |configuration#Proxy HTTP], o que é necessário para a deteção correta do endereço IP do utilizador. - -A RequestFactory permite definir filtros que transformam automaticamente partes da URL da requisição. Esses filtros removem caracteres indesejados da URL, que podem ser inseridos lá, por exemplo, por implementações incorretas de sistemas de comentários em vários sites: - -```php -// remoção de espaços do caminho -$requestFactory->urlFilters['path']['%20'] = ''; - -// remoção de ponto, vírgula ou parêntese direito do final da URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// limpeza do caminho de barras duplicadas (filtro padrão) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -A primeira chave `'path'` ou `'url'` determina a qual parte da URL o filtro se aplica. A segunda chave é a expressão regular a ser pesquisada, e o valor é a substituição a ser usada no lugar do texto encontrado. - - -Upload de ficheiros -=================== - -O método `Nette\Http\Request::getFiles()` retorna um array de todos os uploads numa estrutura normalizada, cujas folhas são objetos [api:Nette\Http\FileUpload]. Eles encapsulam os dados enviados pelo controlo de formulário `<input type=file>`. - -A estrutura reflete a nomenclatura dos controlos em HTML. No caso mais simples, pode ser um único elemento de formulário nomeado enviado como: - -```latte -<input type="file" name="avatar"> -``` - -Neste caso, `$request->getFiles()` retorna um array: - -```php -[ - 'avatar' => /* Instância FileUpload */ -] -``` - -O objeto `FileUpload` é criado mesmo que o utilizador não tenha enviado nenhum ficheiro ou o envio tenha falhado. Se o ficheiro foi enviado é retornado pelo método `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -No caso do nome do elemento usando a notação de array: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -a árvore retornada parece-se com isto: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* Instância FileUpload */ - ], - ], -] -``` - -Também é possível criar um array de ficheiros: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -Nesse caso, a estrutura parece-se com isto: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* Instância FileUpload */, - 1 => /* Instância FileUpload */, - 2 => /* Instância FileUpload */, - ], - ], - ], -] -``` - -A melhor maneira de aceder ao índice 1 do array aninhado é assim: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Como não se pode confiar nos dados externos e, portanto, nem na forma da estrutura dos ficheiros, este método é mais seguro do que, por exemplo, `$request->getFiles()['my-form']['details']['avatars'][1]`, que pode falhar. - - -Visão geral dos métodos `FileUpload` .{toc: FileUpload} -------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Retorna `true` se o utilizador enviou algum ficheiro. - - -isOk(): bool .[method] ----------------------- -Retorna `true` se o ficheiro foi carregado com sucesso. - - -getError(): int .[method] -------------------------- -Retorna o código de erro durante o upload do ficheiro. É uma das constantes [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Caso o upload tenha ocorrido corretamente, retorna `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Move o ficheiro carregado para um novo local. Se o ficheiro de destino já existir, ele será sobrescrito. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Retorna o conteúdo do ficheiro carregado. Caso o upload não tenha sido bem-sucedido, retorna `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Deteta o tipo de conteúdo MIME do ficheiro carregado com base na sua assinatura. Caso o upload não tenha sido bem-sucedido ou a deteção falhe, retorna `null`. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Retorna o nome original do ficheiro, como enviado pelo navegador. - -.[caution] -Não confie no valor retornado por este método. O cliente pode ter enviado um nome de ficheiro malicioso com a intenção de danificar ou hackear a sua aplicação. - - -getSanitizedName(): string .[method] ------------------------------------- -Retorna o nome do ficheiro sanitizado. Contém apenas caracteres ASCII `[a-zA-Z0-9.-]`. Se o nome não contiver tais caracteres, retorna `'unknown'`. Se o ficheiro for uma imagem no formato JPEG, PNG, GIF, WebP ou AVIF, retorna também a extensão correta. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Retorna a extensão de ficheiro apropriada (sem o ponto) correspondente ao tipo MIME detetado. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Retorna o caminho original do ficheiro, como enviado pelo navegador ao fazer upload de uma pasta. O caminho completo está disponível apenas no PHP 8.1 e superior. Em versões anteriores, este método retorna o nome original do ficheiro. - -.[caution] -Não confie no valor retornado por este método. O cliente pode ter enviado um nome de ficheiro malicioso com a intenção de danificar ou hackear a sua aplicação. - - -getSize(): int .[method] ------------------------- -Retorna o tamanho do ficheiro carregado. Caso o upload não tenha sido bem-sucedido, retorna `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Retorna o caminho para o local temporário do ficheiro carregado. Caso o upload não tenha sido bem-sucedido, retorna `''`. - - -isImage(): bool .[method] -------------------------- -Retorna `true` se o ficheiro carregado for uma imagem no formato JPEG, PNG, GIF, WebP ou AVIF. A deteção ocorre com base na sua assinatura e não verifica a integridade de todo o ficheiro. Se a imagem não está danificada pode ser verificado, por exemplo, tentando [carregá-la |#toImage]. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Retorna o par `[largura, altura]` com as dimensões da imagem carregada. Caso o upload não tenha sido bem-sucedido ou não seja uma imagem válida, retorna `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Carrega a imagem como um objeto [Image|utils:images]. Caso o upload não tenha sido bem-sucedido ou não seja uma imagem válida, lança a exceção `Nette\Utils\ImageException`. diff --git a/http/pt/response.texy b/http/pt/response.texy deleted file mode 100644 index 2e9692d2be..0000000000 --- a/http/pt/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Resposta HTTP -************* - -.[perex] -Nette encapsula a resposta HTTP em objetos com uma API compreensível. - -A resposta HTTP é representada pelo objeto [api:Nette\Http\Response]. Se trabalha com Nette, este objeto é criado automaticamente pelo framework e pode recebê-lo por meio de [injeção de dependência |dependency-injection:passing-dependencies]. Nos presenters, basta chamar o método `$this->getHttpResponse()`. - -→ [Instalação e requisitos |@home#Instalação] - - -Nette\Http\Response -=================== - -O objeto, ao contrário de [Nette\Http\Request|request], é mutável, ou seja, usando setters pode alterar o estado, por exemplo, enviar cabeçalhos. Lembre-se de que todos os setters devem ser chamados **antes de enviar qualquer saída.** Se a saída já foi enviada é indicado pelo método `isSent()`. Se retornar `true`, qualquer tentativa de enviar um cabeçalho lançará a exceção `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Altera o [código de status da resposta |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Para melhor clareza do código-fonte, recomendamos usar [constantes predefinidas |api:Nette\Http\IResponse] em vez de números para o código. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Retorna o código de status da resposta. - - -isSent(): bool .[method] ------------------------- -Retorna se os cabeçalhos já foram enviados do servidor para o navegador e, portanto, não é mais possível enviar cabeçalhos ou alterar o código de status. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Envia um cabeçalho HTTP e **sobrescreve** um cabeçalho enviado anteriormente com o mesmo nome. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Envia um cabeçalho HTTP e **não sobrescreve** um cabeçalho enviado anteriormente com o mesmo nome. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Exclui um cabeçalho HTTP enviado anteriormente. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Retorna o cabeçalho HTTP enviado ou `null` se não existir. O parâmetro é insensível a maiúsculas/minúsculas. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Retorna todos os cabeçalhos HTTP enviados como um array associativo. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Altera o cabeçalho `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Redireciona para outra URL. Lembre-se de encerrar o script depois. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Define a expiração do documento HTTP usando os cabeçalhos `Cache-Control` e `Expires`. O parâmetro é um intervalo de tempo (como texto) ou `null`, que desativa o cache. - -```php -// o cache no navegador expirará em uma hora -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -A resposta será baixada usando a caixa de diálogo *Salvar como* com o nome fornecido. O ficheiro em si não é enviado. - -```php -$httpResponse->sendAsFile('fatura.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Envia um cookie. Os valores padrão dos parâmetros são: - -| `$path` | `'/'` | o cookie tem alcance para todos os caminhos no (sub)domínio *(configurável)* -| `$domain` | `null` | o que significa com alcance para o (sub)domínio atual, mas não seus subdomínios *(configurável)* -| `$secure` | `true` | se o site estiver rodando em HTTPS, caso contrário `false` *(configurável)* -| `$httpOnly` | `true` | o cookie é inacessível para JavaScript -| `$sameSite` | `'Lax'` | o cookie pode não ser enviado durante o [acesso de outro domínio |nette:glossary#SameSite cookie] - -Os valores padrão dos parâmetros `$path`, `$domain` e `$secure` podem ser alterados na [configuração |configuration#Cookie HTTP]. - -O tempo pode ser especificado como um número de segundos ou uma string: - -```php -$httpResponse->setCookie('lang', 'pt', '100 days'); // Traduzido 'cs' para 'pt' como exemplo -``` - -O parâmetro `$domain` determina quais domínios podem aceitar o cookie. Se não for especificado, o cookie é aceito pelo mesmo (sub)domínio que o definiu, mas não por seus subdomínios. Se `$domain` for especificado, os subdomínios também são incluídos. Portanto, especificar `$domain` é menos restritivo do que omiti-lo. Por exemplo, com `$domain = 'nette.org'`, os cookies também estão disponíveis em todos os subdomínios como `doc.nette.org`. - -Para o valor `$sameSite`, pode usar as constantes `Response::SameSiteLax`, `SameSiteStrict` e `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Exclui um cookie. Os valores padrão dos parâmetros são: -- `$path` com alcance para todos os diretórios (`'/'`) -- `$domain` com alcance para o (sub)domínio atual, mas não seus subdomínios -- `$secure` é regido pelas configurações na [configuração |configuration#Cookie HTTP] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/pt/sessions.texy b/http/pt/sessions.texy deleted file mode 100644 index 96c4aeb392..0000000000 --- a/http/pt/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sessões -******* - -<div class=perex> - -HTTP é um protocolo sem estado, no entanto, quase toda aplicação precisa manter o estado entre as requisições, por exemplo, o conteúdo de um carrinho de compras. É exatamente para isso que servem as sessões. Vamos mostrar: - -- como usar sessões -- como evitar conflitos de nomes -- como definir a expiração - -</div> - -Ao usar sessões, cada utilizador recebe um identificador único chamado ID de sessão, que é passado num cookie. Ele serve como chave para os dados da sessão. Ao contrário dos cookies, que são armazenados no lado do navegador, os dados da sessão são armazenados no lado do servidor. - -Configuramos a sessão na [configuração |configuration#Sessão], a escolha do tempo de expiração é especialmente importante. - -O gerenciamento da sessão é feito pelo objeto [api:Nette\Http\Session], ao qual pode aceder solicitando-o por meio de [injeção de dependência |dependency-injection:passing-dependencies]. Nos presenters, basta chamar `$session = $this->getSession()`. - -→ [Instalação e requisitos |@home#Instalação] - - -Iniciar sessão -============== - -Nette, por padrão, inicia automaticamente a sessão no momento em que começamos a ler ou escrever dados nela. Manualmente, a sessão é iniciada usando `$session->start()`. - -O PHP envia cabeçalhos HTTP que afetam o cache ao iniciar a sessão, veja [php:session_cache_limiter], e possivelmente também um cookie com o ID da sessão. Portanto, é sempre necessário iniciar a sessão antes de enviar qualquer saída para o navegador, caso contrário, uma exceção será lançada. Se sabe que a sessão será usada durante a renderização da página, inicie-a manualmente antes, por exemplo, no presenter. - -No modo de desenvolvimento, o Tracy inicia a sessão porque a usa para exibir barras com redirecionamentos e requisições AJAX na Tracy Bar. - - -Seções -====== - -Em PHP puro, o armazenamento de dados da sessão é implementado como um array acessível através da variável global `$_SESSION`. O problema é que as aplicações geralmente consistem em várias partes independentes e, se todas tiverem acesso a apenas um array, mais cedo ou mais tarde ocorrerá uma colisão de nomes. - -O Nette Framework resolve o problema dividindo todo o espaço em seções (objetos [api:Nette\Http\SessionSection]). Cada unidade então usa a sua própria seção com um nome exclusivo e nenhuma colisão pode mais ocorrer. - -Obtemos a seção da sessão: - -```php -$section = $session->getSection('nome unico'); -``` - -No presenter, basta usar `getSession()` com um parâmetro: - -```php -// $this é Presenter -$section = $this->getSession('nome unico'); -``` - -A existência da seção pode ser verificada com o método `$session->hasSection('nomeUnico')`. - -Trabalhar com a própria seção é então muito fácil usando os métodos `set()`, `get()` e `remove()`: - -```php -// escrever variável -$section->set('userName', 'franta'); - -// ler variável, retorna null se não existir -echo $section->get('userName'); - -// cancelar variável -$section->remove('userName'); -``` - -Para obter todas as variáveis da seção, é possível usar o ciclo `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Definir expiração ------------------ - -É possível definir a expiração para seções individuais ou até mesmo variáveis individuais. Podemos, assim, deixar a sessão do utilizador expirar em 20 minutos, mas ainda lembrar o conteúdo do carrinho. - -```php -// a seção expirará após 20 minutos -$section->setExpiration('20 minutes'); -``` - -Para definir a expiração de variáveis individuais, serve o terceiro parâmetro do método `set()`: - -```php -// a variável 'flash' expirará em 30 segundos -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Não se esqueça que o tempo de expiração de toda a sessão (veja [configuração da sessão |configuration#Sessão]) deve ser igual ou maior que o tempo definido para seções ou variáveis individuais. - -A remoção da expiração definida anteriormente é feita pelo método `removeExpiration()`. A remoção imediata de toda a seção é garantida pelo método `remove()`. - - -Eventos $onStart, $onBeforeWrite --------------------------------- - -O objeto `Nette\Http\Session` possui os [eventos |nette:glossary#Eventos] `$onStart` e `$onBeforeWrite`, então pode adicionar callbacks que serão chamados após o início da sessão ou antes da sua escrita no disco e subsequente encerramento. - -```php -$session->onBeforeWrite[] = function () { - // escrevemos dados na sessão - $this->section->set('basket', $this->basket); -}; -``` - - -Gerenciamento de sessão -======================= - -Visão geral dos métodos da classe `Nette\Http\Session` para gerenciamento de sessão: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Inicia a sessão. - - -isStarted(): bool .[method] ---------------------------- -A sessão está iniciada? - - -close(): void .[method] ------------------------ -Encerra a sessão. A sessão é encerrada automaticamente no final da execução do script. - - -destroy(): void .[method] -------------------------- -Encerra e exclui a sessão. - - -exists(): bool .[method] ------------------------- -A requisição HTTP contém um cookie com o ID da sessão? - - -regenerateId(): void .[method] ------------------------------- -Gera um novo ID de sessão aleatório. Os dados permanecem preservados. - - -getId(): string .[method] -------------------------- -Retorna o ID da sessão. - -</div> - - -Configuração ------------- - -Configuramos a sessão na [configuração |configuration#Sessão]. Se está a escrever uma aplicação que não usa um contêiner DI, estes métodos são usados para configuração. Devem ser chamados antes de iniciar a sessão. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Define o nome do cookie no qual o ID da sessão é transmitido. O nome padrão é `PHPSESSID`. É útil caso execute várias aplicações diferentes no mesmo site. - - -getName(): string .[method] ---------------------------- -Retorna o nome do cookie no qual o ID da sessão é transmitido. - - -setOptions(array $options): static .[method] --------------------------------------------- -Configura a sessão. É possível definir todas as [diretivas de sessão |https://www.php.net/manual/en/session.configuration.php] do PHP (no formato camelCase, por exemplo, em vez de `session.save_path` escrevemos `savePath`) e também [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Define o tempo de inatividade após o qual a sessão expira. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Define os parâmetros para o cookie. Os valores padrão dos parâmetros podem ser alterados na [configuração |configuration#Cookie de sessão]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Define o diretório onde os ficheiros de sessão são armazenados. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Define um manipulador personalizado, veja a [documentação do PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Segurança em primeiro lugar -=========================== - -O servidor assume que está a comunicar sempre com o mesmo utilizador, desde que as requisições sejam acompanhadas pelo mesmo ID de sessão. A tarefa dos mecanismos de segurança é garantir que isso realmente aconteça e que não seja possível roubar ou falsificar o identificador. - -O Nette Framework, portanto, configura corretamente as diretivas PHP para que o ID da sessão seja transmitido apenas em cookies, o torne inacessível ao JavaScript e ignore quaisquer identificadores na URL. Além disso, em momentos críticos, como o login do utilizador, ele gera um novo ID de sessão. - -.[note] -Para configurar o PHP, usa-se a função ini_set, que infelizmente alguns provedores de hospedagem proíbem. Se este for o caso do seu provedor, tente negociar com ele para permitir a função ou pelo menos configurar o servidor. diff --git a/http/pt/urls.texy b/http/pt/urls.texy deleted file mode 100644 index ba39376048..0000000000 --- a/http/pt/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Trabalhando com URLs -******************** - -.[perex] -As classes [#Url], [#UrlImmutable] e [#UrlScript] permitem gerar, analisar e manipular URLs facilmente. - -→ [Instalação e requisitos |@home#Instalação] - - -Url -=== - -A classe [api:Nette\Http\Url] permite trabalhar facilmente com URLs e os seus componentes individuais, que são capturados neste diagrama: - -/--pre - schema user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -A geração de URLs é intuitiva: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Também é possível analisar uma URL e manipulá-la posteriormente: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -A classe `Url` implementa a interface `JsonSerializable` e possui o método `__toString()`, então o objeto pode ser impresso ou usado em dados passados para `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentes da URL .[method] ----------------------------- - -Para retornar ou alterar os componentes individuais da URL, estes métodos estão disponíveis: - -.[language-php] -| Setter | Getter | Valor retornado -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL completa - -Aviso: Ao trabalhar com uma URL obtida de uma [requisição HTTP|request], lembre-se de que ela não conterá o fragmento, pois o navegador não o envia para o servidor. - -Também podemos trabalhar com parâmetros de consulta individuais usando: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Retorna a parte direita ou esquerda do host. Funciona assim se o host for `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Verifica se duas URLs são idênticas. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Verifica se a URL é absoluta. Uma URL é considerada absoluta se começa com um esquema (por exemplo, http, https, ftp) seguido por dois pontos. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normaliza o caminho na URL removendo os segmentos especiais `.` e `..`. O método remove elementos de caminho redundantes da mesma forma que os navegadores web fazem. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -A classe [api:Nette\Http\UrlImmutable] é uma alternativa imutável à classe [#Url] (semelhante a como `DateTimeImmutable` do PHP é a alternativa imutável a `DateTime`). Em vez de setters, ela possui os chamados withers, que não alteram o objeto, mas retornam novas instâncias com o valor modificado: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -A classe `UrlImmutable` implementa a interface `JsonSerializable` e possui o método `__toString()`, então o objeto pode ser impresso ou usado em dados passados para `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentes da URL .[method] ----------------------------- - -Para retornar ou alterar os componentes individuais da URL, servem os métodos: - -.[language-php] -| Wither | Getter | Valor retornado -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL completa - -O método `withoutUserInfo()` remove `user` e `password`. - -Também podemos trabalhar com parâmetros de consulta individuais usando: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Retorna a parte direita ou esquerda do host. Funciona assim se o host for `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Deriva uma URL absoluta da mesma forma que um navegador processa links numa página HTML: -- se o link for uma URL absoluta (contém esquema), ele é usado sem alterações -- se o link começar com `//`, apenas o esquema da URL atual é adotado -- se o link começar com `/`, um caminho absoluto da raiz do domínio é criado -- em outros casos, a URL é construída relativamente ao caminho atual - -```php -$url = new UrlImmutable('https://example.com/path/page'); -echo $url->resolve('../foo'); // 'https://example.com/foo' -echo $url->resolve('/bar'); // 'https://example.com/bar' -echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Verifica se duas URLs são idênticas. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -A classe [api:Nette\Http\UrlScript] é descendente de [#UrlImmutable] e a estende com outros componentes virtuais da URL, como o diretório raiz do projeto, etc. Assim como a classe pai, é um objeto imutável. - -O diagrama a seguir mostra os componentes que UrlScript reconhece: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` é o endereço URL base da aplicação, incluindo o domínio e a parte do caminho para o diretório raiz da aplicação -- `basePath` é a parte do caminho para o diretório raiz da aplicação -- `scriptPath` é o caminho para o script atual -- `relativePath` é o nome do script (eventualmente outros segmentos do caminho) relativo a basePath -- `relativeUrl` é toda a parte da URL após baseUrl, incluindo a query string e o fragmento. -- `pathInfo` hoje em dia é uma parte da URL pouco utilizada após o nome do script - -Para retornar partes da URL, estão disponíveis os métodos: - -.[language-php] -| Getter | Valor retornado -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Objetos `UrlScript` geralmente não são criados diretamente, mas são retornados pelo método [Nette\Http\Request::getUrl()|request] com os componentes já configurados corretamente para a requisição HTTP atual. diff --git a/http/ro/@home.texy b/http/ro/@home.texy deleted file mode 100644 index 77caa66684..0000000000 --- a/http/ro/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Pachetul `nette/http` încapsulează [cererea HTTP|request] & [răspunsul|response], lucrul cu [sesiunile|sessions] și [parsarea și compunerea URL-urilor |urls]. - - -Instalare ---------- - -Descărcați și instalați biblioteca folosind [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/ro/@left-menu.texy b/http/ro/@left-menu.texy deleted file mode 100644 index df87435c9c..0000000000 --- a/http/ro/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Introducere |@home] -- [Cerere HTTP|request] -- [Răspuns HTTP|response] -- [Sesiuni |Sessions] -- [Utilități URL |urls] -- [Configurație |configuration] diff --git a/http/ro/@meta.texy b/http/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/http/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/http/ro/configuration.texy b/http/ro/configuration.texy deleted file mode 100644 index 74ded7164e..0000000000 --- a/http/ro/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Configurare HTTP -**************** - -.[perex] -Prezentare generală a opțiunilor de configurare pentru Nette HTTP. - -Dacă nu utilizați întregul framework, ci doar această bibliotecă, citiți [cum se încarcă configurația|bootstrap:]. - - -Antete HTTP -=========== - -```neon -http: - # antete care sunt trimise cu fiecare cerere - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # afectează antetul X-Frame-Options - frames: ... # (string|bool) implicit este 'SAMEORIGIN' -``` - -Framework-ul, din motive de securitate, trimite antetul `X-Frame-Options: SAMEORIGIN`, care specifică faptul că pagina poate fi afișată în interiorul altei pagini (în elementul `<iframe>`) doar dacă se află pe același domeniu. Acest lucru poate fi nedorit în anumite situații (de exemplu, dacă dezvoltați o aplicație pentru Facebook), comportamentul putând fi modificat prin setarea `frames: http://allowed-host.com` sau `frames: true`. - - -Content Security Policy ------------------------ - -Se pot construi ușor antetele `Content-Security-Policy` (în continuare CSP), descrierea lor o găsiți în [descrierea CSP |https://content-security-policy.com]. Directivele CSP (cum ar fi `script-src`) pot fi scrise fie ca șiruri conform specificației, fie ca array-uri de valori pentru o mai bună lizibilitate. Atunci nu este nevoie să puneți ghilimele în jurul cuvintelor cheie, cum ar fi `'self'`. Nette generează, de asemenea, automat valoarea `nonce`, astfel încât antetul va conține, de exemplu, `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # șir în format conform specificației CSP - default-src: "'self' https://example.com" - - # array de valori - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool în cazul comutatoarelor - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -În șabloane utilizați `<script n:nonce>...</script>` și valoarea nonce se va completa automat. Crearea site-urilor web sigure în Nette este într-adevăr ușoară. - -Similar se pot construi și antetele `Content-Security-Policy-Report-Only` (care pot fi utilizate concomitent cu CSP) și [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -Cookie HTTP ------------ - -Se pot modifica valorile implicite ale unor parametri ai metodei [Nette\Http\Response::setCookie() |response#setCookie] și ale sesiunii. - -```neon -http: - # domeniul cookie-ului în funcție de cale - cookiePath: ... # (string) implicit este '/' - - # domenii care acceptă cookie-uri - cookieDomain: 'example.com' # (string|domain) implicit este nesetat - - # trimite cookie-uri doar prin HTTPS? - cookieSecure: ... # (bool|auto) implicit este auto - - # dezactivează trimiterea cookie-ului utilizat de Nette pentru protecția CSRF - disableNetteCookie: ... # (bool) implicit este false -``` - -Atributul `cookieDomain` specifică ce domenii pot accepta cookie-uri. Dacă nu este specificat, cookie-ul este acceptat de același (sub)domeniu care l-a setat, *dar nu* și de subdomeniile sale. Dacă `cookieDomain` este specificat, sunt incluse și subdomeniile. Prin urmare, specificarea `cookieDomain` este mai puțin restrictivă decât omiterea sa. - -De exemplu, cu `cookieDomain: nette.org`, cookie-urile sunt disponibile și pe toate subdomeniile precum `doc.nette.org`. Același lucru se poate realiza și cu valoarea specială `domain`, adică `cookieDomain: domain`. - -Valoarea implicită `auto` pentru atributul `cookieSecure` înseamnă că, dacă site-ul rulează pe HTTPS, cookie-urile vor fi trimise cu flag-ul `Secure` și, prin urmare, vor fi disponibile doar prin HTTPS. - - -Proxy HTTP ----------- - -Dacă site-ul rulează în spatele unui proxy HTTP, specificați adresa sa IP pentru ca detectarea conexiunii prin HTTPS și a adresei IP a clientului să funcționeze corect. Adică, pentru ca funcțiile [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] și [isSecured() |request#isSecured] să returneze valorile corecte și în șabloane să se genereze linkuri cu protocolul `https:`. - -```neon -http: - # Adresă IP, interval (ex. 127.0.0.1/8) sau array cu aceste valori - proxy: 127.0.0.1 # (string|string[]) implicit este nesetat -``` - - -Sesiune -======= - -Setări de bază pentru [sesiuni|sessions]: - -```neon -session: - # afișează panoul de sesiune în Tracy Bar? - debugger: ... # (bool) implicit este false - - # perioada de inactivitate după care sesiunea expiră - expiration: 14 days # (string) implicit este '3 hours' - - # când ar trebui să pornească sesiunea? - autoStart: ... # (smart|always|never) implicit este 'smart' - - # handler, serviciu care implementează interfața SessionHandlerInterface - handler: @handlerService -``` - -Opțiunea `autoStart` controlează când trebuie să pornească sesiunea. Valoarea `always` înseamnă că sesiunea va porni întotdeauna la pornirea aplicației. Valoarea `smart` înseamnă că sesiunea va porni la începutul aplicației doar dacă există deja, sau în momentul în care dorim să citim sau să scriem în ea. Și, în final, valoarea `never` interzice pornirea automată a sesiunii. - -În plus, se pot seta toate [directivele de sesiune |https://www.php.net/manual/en/session.configuration.php] PHP (în format camelCase) și, de asemenea, [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Exemplu: - -```neon -session: - # 'session.name' se scrie ca 'name' - name: MYID - - # 'session.save_path' se scrie ca 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Cookie de sesiune ------------------ - -Cookie-ul de sesiune este trimis cu aceiași parametri ca [alte cookie-uri |#Cookie HTTP], dar îi puteți modifica pentru acesta: - -```neon -session: - # domenii care acceptă cookie-uri - cookieDomain: 'example.com' # (string|domain) - - # restricții la accesul de pe alt domeniu - cookieSamesite: None # (Strict|Lax|None) implicit este Lax -``` - -Atributul `cookieSamesite` afectează dacă cookie-ul va fi trimis la [accesul de pe alt domeniu |nette:glossary#Cookie SameSite], ceea ce oferă o anumită protecție împotriva atacurilor [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Servicii DI -=========== - -Aceste servicii sunt adăugate în containerul DI: - -| Nume | Tip | Descriere -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [Cerere HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [Răspuns HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [Gestionarea sesiunii| sessions] diff --git a/http/ro/request.texy b/http/ro/request.texy deleted file mode 100644 index 87c68f188b..0000000000 --- a/http/ro/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Cerere HTTP -*********** - -.[perex] -Nette încapsulează cererea HTTP în obiecte cu o API inteligibilă și, în același timp, oferă un filtru de igienizare. - -Cererea HTTP este reprezentată de obiectul [api:Nette\Http\Request]. Dacă lucrați cu Nette, acest obiect este creat automat de framework și îl puteți primi prin [injecție de dependențe |dependency-injection:passing-dependencies]. În presentere, este suficient să apelați metoda `$this->getHttpRequest()`. Dacă lucrați în afara Nette Framework, puteți crea obiectul folosind [#RequestFactory]. - -Un mare avantaj al Nette este că, la crearea obiectului, curăță automat toți parametrii de intrare GET, POST, COOKIE și, de asemenea, URL-ul de caractere de control și secvențe UTF-8 invalide. Cu aceste date puteți lucra în siguranță în continuare. Datele curățate sunt apoi utilizate în presentere și formulare. - -→ [Instalare și cerințe |@home#Instalare] - - -Nette\Http\Request -================== - -Acest obiect este imuabil (nu poate fi modificat). Nu are setteri, are doar un așa-numit wither `withUrl()`, care nu modifică obiectul, ci returnează o nouă instanță cu valoarea modificată. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Returnează o clonă cu o altă adresă URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Returnează URL-ul cererii ca obiect [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Atenție: browserele nu trimit fragmentul către server, așa că `$url->getFragment()` va returna un șir gol. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Returnează parametrii GET ai cererii. - -```php -$all = $httpRequest->getQuery(); // returnează un array cu toți parametrii din URL -$id = $httpRequest->getQuery('id'); // returnează parametrul GET 'id' (sau null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Returnează parametrii POST ai cererii. - -```php -$all = $httpRequest->getPost(); // returnează un array cu toți parametrii din POST -$id = $httpRequest->getPost('id'); // returnează parametrul POST 'id' (sau null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Returnează [încărcarea |#Fișiere încărcate] ca obiect [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // a fost încărcat vreun fișier? - $file->getUntrustedName(); // numele fișierului trimis de utilizator - $file->getSanitizedName(); // nume fără caractere periculoase -} -``` - -Pentru a accesa structura imbricată, specificați un array de chei. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Deoarece nu se poate avea încredere în datele din exterior și, prin urmare, nici în structura fișierelor, această metodă este mai sigură decât, de exemplu, `$request->getFiles()['my-form']['details']['avatar']`, care poate eșua. - - -getFiles(): array .[method] ---------------------------- -Returnează arborele [tuturor încărcărilor |#Fișiere încărcate] într-o structură normalizată, ale cărei frunze sunt obiecte [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Returnează cookie-ul sau `null` dacă nu există. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Returnează toate cookie-urile. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Returnează metoda HTTP cu care a fost făcută cererea. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Testează metoda HTTP cu care a fost făcută cererea. Parametrul este case-insensitive. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Returnează antetul HTTP sau `null` dacă nu există. Parametrul este case-insensitive. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Returnează toate antetele HTTP ca un array asociativ. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Este conexiunea criptată (HTTPS)? Pentru o funcționare corectă, poate fi necesar să [configurați proxy-ul |configuration#Proxy HTTP]. - - -isSameSite(): bool .[method] ----------------------------- -Cererea provine de pe același (sub)domeniu și este inițiată printr-un clic pe un link? Nette utilizează cookie-ul `_nss` (anterior `nette-samesite`) pentru detectare. - - -isAjax(): bool .[method] ------------------------- -Este o cerere AJAX? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Returnează adresa IP a utilizatorului. Pentru o funcționare corectă, poate fi necesar să [configurați proxy-ul |configuration#Proxy HTTP]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Returnează rezoluția DNS a adresei IP a utilizatorului. Pentru o funcționare corectă, poate fi necesar să [configurați proxy-ul |configuration#Proxy HTTP]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Returnează datele de autentificare pentru [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Returnează corpul cererii HTTP. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Detectează limba. Ca parametru `$lang`, transmitem un array cu limbile suportate de aplicație, iar aceasta va returna limba preferată de browserul vizitatorului. Nu este magie, ci doar utilizează antetul `Accept-Language`. Dacă nu există nicio potrivire, returnează `null`. - -```php -// browserul trimite, de ex., Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // limbi suportate de aplicație -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Clasa [api:Nette\Http\RequestFactory] servește la crearea unei instanțe `Nette\Http\Request`, care reprezintă cererea HTTP curentă. (Dacă lucrați cu Nette, obiectul cererii HTTP este creat automat de framework.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Metoda `fromGlobals()` creează obiectul cererii pe baza variabilelor globale PHP curente (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` și `$_SERVER`). La crearea obiectului, curăță automat toți parametrii de intrare GET, POST, COOKIE și, de asemenea, URL-ul de caractere de control și secvențe UTF-8 invalide, ceea ce asigură siguranța în lucrul ulterior cu aceste date. - -RequestFactory poate fi configurat înainte de a apela `fromGlobals()`: - -- prin metoda `$factory->setBinary()` dezactivați curățarea automată a parametrilor de intrare de caractere de control și secvențe UTF-8 invalide. -- prin metoda `$factory->setProxy(...)` specificați adresa IP a [serverului proxy |configuration#Proxy HTTP], ceea ce este necesar pentru detectarea corectă a adresei IP a utilizatorului. - -RequestFactory permite definirea filtrelor care transformă automat părți ale URL-ului cererii. Aceste filtre elimină caracterele nedorite din URL, care pot fi introduse acolo, de exemplu, printr-o implementare incorectă a sistemelor de comentarii pe diverse site-uri web: - -```php -// eliminarea spațiilor din cale -$requestFactory->urlFilters['path']['%20'] = ''; - -// eliminarea punctului, virgulei sau parantezei drepte de la sfârșitul URI-ului -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// curățarea căii de slash-uri duplicate (filtru implicit) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Prima cheie `'path'` sau `'url'` specifică la ce parte a URL-ului se aplică filtrul. A doua cheie este expresia regulată care trebuie căutată, iar valoarea este înlocuirea care se utilizează în locul textului găsit. - - -Fișiere încărcate -================= - -Metoda `Nette\Http\Request::getFiles()` returnează un array cu toate încărcările într-o structură normalizată, ale cărei frunze sunt obiecte [api:Nette\Http\FileUpload]. Acestea încapsulează datele trimise de elementul de formular `<input type=file>`. - -Structura reflectă denumirea elementelor în HTML. În cel mai simplu caz, poate fi un singur element de formular numit, trimis ca: - -```latte -<input type="file" name="avatar"> -``` - -În acest caz, `$request->getFiles()` returnează un array: - -```php -[ - 'avatar' => /* Instanță FileUpload */ -] -``` - -Obiectul `FileUpload` este creat chiar și în cazul în care utilizatorul nu a trimis niciun fișier sau trimiterea a eșuat. Dacă fișierul a fost trimis, returnează metoda `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -În cazul numelui elementului care utilizează notația pentru array-uri: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -arborele returnat arată astfel: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* Instanță FileUpload */ - ], - ], -] -``` - -Se poate crea și un array de fișiere: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -În acest caz, structura arată astfel: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* Instanță FileUpload */, - 1 => /* Instanță FileUpload */, - 2 => /* Instanță FileUpload */, - ], - ], - ], -] -``` - -Accesarea indexului 1 al array-ului imbricat se face cel mai bine astfel: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Deoarece nu se poate avea încredere în datele din exterior și, prin urmare, nici în structura fișierelor, această metodă este mai sigură decât, de exemplu, `$request->getFiles()['my-form']['details']['avatars'][1]`, care poate eșua. - - -Prezentare generală a metodelor `FileUpload` .{toc: FileUpload} ---------------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Returnează `true` dacă utilizatorul a încărcat un fișier. - - -isOk(): bool .[method] ----------------------- -Returnează `true` dacă fișierul a fost încărcat cu succes. - - -getError(): int .[method] -------------------------- -Returnează codul de eroare la încărcarea fișierului. Este una dintre constantele [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Dacă încărcarea a avut succes, returnează `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Mută fișierul încărcat într-o nouă locație. Dacă fișierul țintă există deja, acesta va fi suprascris. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Returnează conținutul fișierului încărcat. Dacă încărcarea nu a avut succes, returnează `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Detectează tipul de conținut MIME al fișierului încărcat pe baza semnăturii sale. Dacă încărcarea nu a avut succes sau detectarea a eșuat, returnează `null`. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Returnează numele original al fișierului, așa cum a fost trimis de browser. - -.[caution] -Nu aveți încredere în valoarea returnată de această metodă. Clientul ar fi putut trimite un nume de fișier dăunător cu intenția de a deteriora sau de a pirata aplicația dvs. - - -getSanitizedName(): string .[method] ------------------------------------- -Returnează numele de fișier igienizat. Conține doar caractere ASCII `[a-zA-Z0-9.-]`. Dacă numele nu conține astfel de caractere, returnează `'unknown'`. Dacă fișierul este o imagine în format JPEG, PNG, GIF, WebP sau AVIF, returnează și extensia corectă. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Returnează extensia de fișier potrivită (fără punct) corespunzătoare tipului MIME detectat. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Returnează calea originală a fișierului, așa cum a fost trimisă de browser la încărcarea unui folder. Calea completă este disponibilă numai în PHP 8.1 și versiunile ulterioare. În versiunile anterioare, această metodă returnează numele original al fișierului. - -.[caution] -Nu aveți încredere în valoarea returnată de această metodă. Clientul ar fi putut trimite un nume de fișier dăunător cu intenția de a deteriora sau de a pirata aplicația dvs. - - -getSize(): int .[method] ------------------------- -Returnează dimensiunea fișierului încărcat. Dacă încărcarea nu a avut succes, returnează `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Returnează calea către locația temporară a fișierului încărcat. Dacă încărcarea nu a avut succes, returnează `''`. - - -isImage(): bool .[method] -------------------------- -Returnează `true` dacă fișierul încărcat este o imagine în format JPEG, PNG, GIF, WebP sau AVIF. Detectarea se bazează pe semnătura sa și nu verifică integritatea întregului fișier. Dacă imaginea este deteriorată poate fi determinat, de exemplu, încercând să o [încărcați |#toImage]. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Returnează o pereche `[lățime, înălțime]` cu dimensiunile imaginii încărcate. Dacă încărcarea nu a avut succes sau nu este o imagine validă, returnează `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Încarcă imaginea ca obiect [Image|utils:images]. Dacă încărcarea nu a avut succes sau nu este o imagine validă, aruncă o excepție `Nette\Utils\ImageException`. diff --git a/http/ro/response.texy b/http/ro/response.texy deleted file mode 100644 index e5dcb254f3..0000000000 --- a/http/ro/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Răspuns HTTP -************ - -.[perex] -Nette încapsulează răspunsul HTTP în obiecte cu o API inteligibilă. - -Răspunsul HTTP este reprezentat de obiectul [api:Nette\Http\Response]. Dacă lucrați cu Nette, acest obiect este creat automat de framework și îl puteți primi prin [injecție de dependențe |dependency-injection:passing-dependencies]. În presentere, este suficient să apelați metoda `$this->getHttpResponse()`. - -→ [Instalare și cerințe |@home#Instalare] - - -Nette\Http\Response -=================== - -Obiectul, spre deosebire de [Nette\Http\Request|request], este mutabil, adică puteți modifica starea folosind setteri, de exemplu, trimițând antete. Nu uitați că toți setterii trebuie apelați **înainte de a trimite orice ieșire.** Dacă ieșirea a fost deja trimisă, indică metoda `isSent()`. Dacă returnează `true`, orice încercare de a trimite un antet va arunca o excepție `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Modifică [codul de stare al răspunsului |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Pentru o mai bună lizibilitate a codului sursă, se recomandă utilizarea [constantelor predefinite |api:Nette\Http\IResponse] în loc de numere pentru cod. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Returnează codul de stare al răspunsului. - - -isSent(): bool .[method] ------------------------- -Returnează dacă antetele au fost deja trimise de la server la browser și, prin urmare, nu mai este posibil să se trimită antete sau să se modifice codul de stare. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Trimite un antet HTTP și **suprascrie** antetul trimis anterior cu același nume. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Trimite un antet HTTP și **nu suprascrie** antetul trimis anterior cu același nume. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Șterge un antet HTTP trimis anterior. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Returnează antetul HTTP trimis sau `null` dacă nu există. Parametrul este case-insensitive. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Returnează toate antetele HTTP trimise ca un array asociativ. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Modifică antetul `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Redirecționează către o altă adresă URL. Nu uitați să terminați scriptul după aceea. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Setează expirarea documentului HTTP folosind antetele `Cache-Control` și `Expires`. Parametrul este fie un interval de timp (ca text), fie `null`, ceea ce dezactivează stocarea în cache. - -```php -// cache-ul din browser va expira într-o oră -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Răspunsul va fi descărcat folosind caseta de dialog *Salvare ca* sub numele specificat. Fișierul în sine nu este trimis. - -```php -$httpResponse->sendAsFile('factura.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Trimite un cookie. Valorile implicite ale parametrilor: - -| `$path` | `'/'` | cookie-ul are acoperire pentru toate căile din (sub)domeniu *(configurabil)* -| `$domain` | `null` | ceea ce înseamnă cu acoperire pentru (sub)domeniul curent, dar nu și subdomeniile sale *(configurabil)* -| `$secure` | `true` | dacă site-ul rulează pe HTTPS, altfel `false` *(configurabil)* -| `$httpOnly` | `true` | cookie-ul este inaccesibil pentru JavaScript -| `$sameSite` | `'Lax'` | cookie-ul poate să nu fie trimis la [accesul de pe alt domeniu |nette:glossary#Cookie SameSite] - -Valorile implicite ale parametrilor `$path`, `$domain` și `$secure` le puteți modifica în [configurație |configuration#Cookie HTTP]. - -Timpul poate fi specificat ca număr de secunde sau șir: - -```php -$httpResponse->setCookie('lang', 'ro', '100 days'); -``` - -Parametrul `$domain` specifică ce domenii pot accepta cookie-uri. Dacă nu este specificat, cookie-ul este acceptat de același (sub)domeniu care l-a setat, dar nu și de subdomeniile sale. Dacă `$domain` este specificat, sunt incluse și subdomeniile. Prin urmare, specificarea `$domain` este mai puțin restrictivă decât omiterea sa. De exemplu, cu `$domain = 'nette.org'`, cookie-urile sunt disponibile și pe toate subdomeniile precum `doc.nette.org`. - -Pentru valoarea `$sameSite` puteți utiliza constantele `Response::SameSiteLax`, `SameSiteStrict` și `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Șterge un cookie. Valorile implicite ale parametrilor sunt: -- `$path` cu acoperire pentru toate directoarele (`'/'`) -- `$domain` cu acoperire pentru (sub)domeniul curent, dar nu și subdomeniile sale -- `$secure` se ghidează după setările din [configurație |configuration#Cookie HTTP] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/ro/sessions.texy b/http/ro/sessions.texy deleted file mode 100644 index 02af57148e..0000000000 --- a/http/ro/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sesiuni -******* - -<div class=perex> - -HTTP este un protocol fără stare, însă aproape orice aplicație are nevoie să păstreze starea între cereri, de exemplu conținutul coșului de cumpărături. Tocmai pentru aceasta servesc sesiunile. Vom arăta, - -- cum să utilizați sesiunile -- cum să preveniți conflictele de nume -- cum să setați expirarea - -</div> - -La utilizarea sesiunilor, fiecare utilizator primește un identificator unic numit ID de sesiune, care este transmis într-un cookie. Acesta servește drept cheie pentru datele sesiunii. Spre deosebire de cookie-uri, care sunt stocate pe partea browserului, datele din sesiune sunt stocate pe partea serverului. - -Sesiunea o setăm în [configurație |configuration#Sesiune], importantă fiind în special alegerea timpului de expirare. - -Gestionarea sesiunii este responsabilitatea obiectului [api:Nette\Http\Session], la care ajungeți solicitându-l prin [injecție de dependențe |dependency-injection:passing-dependencies]. În presentere, este suficient să apelați `$session = $this->getSession()`. - -→ [Instalare și cerințe |@home#Instalare] - - -Pornirea sesiunii -================= - -Nette, în setarea implicită, pornește automat sesiunea în momentul în care începem să citim sau să scriem date în ea. Manual, sesiunea se pornește folosind `$session->start()`. - -PHP trimite la pornirea sesiunii antete HTTP care afectează stocarea în cache, vezi [php:session_cache_limiter], și eventual și un cookie cu ID-ul sesiunii. De aceea, este necesar să porniți întotdeauna sesiunea înainte de a trimite orice ieșire către browser, altfel se va arunca o excepție. Deci, dacă știți că în timpul randării paginii se va utiliza sesiunea, porniți-o manual înainte, de exemplu în presenter. - -În modul de dezvoltare, Tracy pornește sesiunea, deoarece o utilizează pentru afișarea barelor cu redirecționări și cereri AJAX în Tracy Bar. - - -Secțiuni -======== - -În PHP pur, stocarea datelor sesiunii este realizată ca un array accesibil prin variabila globală `$_SESSION`. Problema este că aplicațiile sunt compuse în mod obișnuit dintr-o serie de părți independente reciproc și dacă toate au la dispoziție doar un singur array, mai devreme sau mai târziu va apărea o coliziune de nume. - -Nette Framework rezolvă problema împărțind întregul spațiu în secțiuni (obiecte [api:Nette\Http\SessionSection]). Fiecare unitate utilizează apoi propria secțiune cu un nume unic și nicio coliziune nu mai poate avea loc. - -Obținem secțiunea din sesiune: - -```php -$section = $session->getSection('nume-unic'); -``` - -În presenter este suficient să folosim `getSession()` cu parametru: - -```php -// $this este Presenter -$section = $this->getSession('nume-unic'); -``` - -Existența secțiunii poate fi verificată cu metoda `$session->hasSection('nume-unic')`. - -Cu secțiunea însăși se lucrează apoi foarte ușor folosind metodele `set()`, `get()` și `remove()`: - -```php -// scriere variabilă -$section->set('userName', 'franta'); - -// citire variabilă, returnează null dacă nu există -echo $section->get('userName'); - -// anulare variabilă -$section->remove('userName'); -``` - -Pentru a obține toate variabilele dintr-o secțiune, se poate utiliza bucla `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Setarea expirării ------------------ - -Pentru secțiuni individuale sau chiar variabile individuale se poate seta expirarea. Putem astfel lăsa autentificarea utilizatorului să expire după 20 de minute, dar în același timp să păstrăm conținutul coșului. - -```php -// secțiunea expiră după 20 de minute -$section->setExpiration('20 minutes'); -``` - -Pentru setarea expirării variabilelor individuale servește al treilea parametru al metodei `set()`: - -```php -// variabila 'flash' va expira după 30 de secunde -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Nu uitați că timpul de expirare al întregii sesiuni (vezi [configurarea sesiunii |configuration#Sesiune]) trebuie să fie egal sau mai mare decât timpul setat pentru secțiunile sau variabilele individuale. - -Anularea expirării setate anterior se realizează cu metoda `removeExpiration()`. Anularea imediată a întregii secțiuni este asigurată de metoda `remove()`. - - -Evenimentele $onStart, $onBeforeWrite -------------------------------------- - -Obiectul `Nette\Http\Session` are [evenimente |nette:glossary#Evenimente] `$onStart` și `$onBeforeWrite`, deci puteți adăuga callback-uri care se declanșează după pornirea sesiunii sau înainte de scrierea ei pe disc și închiderea ulterioară. - -```php -$session->onBeforeWrite[] = function () { - // scriem datele în sesiune - $this->section->set('basket', $this->basket); -}; -``` - - -Gestionarea sesiunii -==================== - -Prezentare generală a metodelor clasei `Nette\Http\Session` pentru gestionarea sesiunii: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Pornește sesiunea. - - -isStarted(): bool .[method] ---------------------------- -Sesiunea este pornită? - - -close(): void .[method] ------------------------ -Închide sesiunea. Sesiunea se închide automat la sfârșitul rulării scriptului. - - -destroy(): void .[method] -------------------------- -Închide și șterge sesiunea. - - -exists(): bool .[method] ------------------------- -Cererea HTTP conține un cookie cu ID-ul sesiunii? - - -regenerateId(): void .[method] ------------------------------- -Generează un nou ID de sesiune aleatoriu. Datele rămân păstrate. - - -getId(): string .[method] -------------------------- -Returnează ID-ul sesiunii. - -</div> - - -Configurație ------------- - -Sesiunea o setăm în [configurație |configuration#Sesiune]. Dacă scrieți o aplicație care nu utilizează containerul DI, pentru configurare servesc aceste metode. Trebuie apelate înainte de pornirea sesiunii. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Setează numele cookie-ului în care se transmite ID-ul sesiunii. Numele standard este `PHPSESSID`. Este util în cazul în care pe același site web rulați mai multe aplicații diferite. - - -getName(): string .[method] ---------------------------- -Returnează numele cookie-ului în care se transmite ID-ul sesiunii. - - -setOptions(array $options): static .[method] --------------------------------------------- -Configurează sesiunea. Se pot seta toate [directivele de sesiune |https://www.php.net/manual/en/session.configuration.php] PHP (în format camelCase, de ex. în loc de `session.save_path` scriem `savePath`) și, de asemenea, [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Setează perioada de inactivitate după care sesiunea expiră. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Setarea parametrilor pentru cookie. Valorile implicite ale parametrilor le puteți modifica în [configurație |configuration#Cookie de sesiune]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Setează directorul unde se salvează fișierele cu sesiuni. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Setarea unui handler personalizat, vezi [documentația PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Securitatea înainte de toate -============================ - -Serverul presupune că comunică în continuare cu același utilizator, atâta timp cât cererile sunt însoțite de același ID de sesiune. Sarcina mecanismelor de securitate este să asigure că acest lucru se întâmplă într-adevăr și că nu este posibilă furtul sau substituirea identificatorului. - -Nette Framework configurează, prin urmare, corect directivele PHP, astfel încât ID-ul sesiunii să fie transmis doar în cookie, să fie inaccesibil pentru JavaScript și să ignore eventualii identificatori din URL. În plus, în momente critice, cum ar fi autentificarea utilizatorului, generează un nou ID de sesiune. - -.[note] -Pentru configurarea PHP se utilizează funcția ini_set, pe care, din păcate, unele hostinguri o interzic. Dacă este și cazul hosterului dvs., încercați să discutați cu el pentru a vă permite funcția sau cel puțin pentru a configura serverul. diff --git a/http/ro/urls.texy b/http/ro/urls.texy deleted file mode 100644 index a15fa0903b..0000000000 --- a/http/ro/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Lucrul cu URL-uri -***************** - -.[perex] -Clasele [#Url], [#UrlImmutable] și [#UrlScript] permit generarea, parsarea și manipularea ușoară a URL-urilor. - -→ [Instalare și cerințe |@home#Instalare] - - -Url -=== - -Clasa [api:Nette\Http\Url] permite lucrul ușor cu URL-uri și componentele sale individuale, pe care le surprinde această schiță: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Generarea URL-urilor este intuitivă: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Se poate, de asemenea, parsa un URL și manipula ulterior: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Clasa `Url` implementează interfața `JsonSerializable` și are metoda `__toString()`, astfel încât obiectul poate fi afișat sau utilizat în datele transmise către `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentele URL .[method] --------------------------- - -Pentru returnarea sau modificarea componentelor individuale ale URL-ului, aveți la dispoziție aceste metode: - -.[language-php] -| Setter | Getter | Valoare returnată -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL complet - -Atenție: Când lucrați cu un URL obținut dintr-o [cerere HTTP|request], rețineți că nu va conține fragmentul, deoarece browserul nu îl trimite către server. - -Putem lucra și cu parametrii query individuali folosind: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name): ?string` - - -getDomain(int $level = 2): ?string .[method] --------------------------------------------- -Returnează partea dreaptă sau stângă a gazdei. Funcționează astfel dacă gazda este `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `null` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Verifică dacă două URL-uri sunt identice. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Verifică dacă URL-ul este absolut. Un URL este considerat absolut dacă începe cu o schemă (de ex., http, https, ftp) urmată de două puncte. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normalizează calea în URL prin eliminarea segmentelor speciale `.` și `..`. Metoda elimină elementele redundante ale căii în același mod în care o fac browserele web. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Clasa [api:Nette\Http\UrlImmutable] este o alternativă imuabilă (nu poate fi modificată) a clasei [#Url] (similar cu modul în care în PHP `DateTimeImmutable` este alternativa imuabilă a `DateTime`). În loc de setteri, are așa-numiți witheri, care nu modifică obiectul, ci returnează noi instanțe cu valoarea modificată: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Clasa `UrlImmutable` implementează interfața `JsonSerializable` și are metoda `__toString()`, astfel încât obiectul poate fi afișat sau utilizat în datele transmise către `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentele URL .[method] --------------------------- - -Pentru returnarea sau modificarea componentelor individuale ale URL-ului servesc metodele: - -.[language-php] -| Wither | Getter | Valoare returnată -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL complet - -Metoda `withoutUserInfo()` elimină `user` și `password`. - -Putem lucra și cu parametrii query individuali folosind: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name): ?string` - - -getDomain(int $level = 2): ?string .[method] --------------------------------------------- -Returnează partea dreaptă sau stângă a gazdei. Funcționează astfel dacă gazda este `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `null` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Derivă un URL absolut în același mod în care un browser procesează linkurile pe o pagină HTML: -- dacă linkul este un URL absolut (conține o schemă), este utilizat neschimbat -- dacă linkul începe cu `//`, se preia doar schema din URL-ul curent -- dacă linkul începe cu `/`, se creează o cale absolută de la rădăcina domeniului -- în celelalte cazuri, URL-ul este construit relativ la calea curentă - -```php -$url = new UrlImmutable('https://example.com/path/page'); -echo $url->resolve('../foo'); // 'https://example.com/foo' -echo $url->resolve('/bar'); // 'https://example.com/bar' -echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' -``` - - -isEqual(string|UrlImmutable $anotherUrl): bool .[method] --------------------------------------------------------- -Verifică dacă două URL-uri sunt identice. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Clasa [api:Nette\Http\UrlScript] este un descendent al [#UrlImmutable] și îl extinde cu alte componente virtuale ale URL-ului, cum ar fi directorul rădăcină al proiectului etc. La fel ca clasa părinte, este un obiect imuabil (nu poate fi modificat). - -Următoarea diagramă afișează componentele pe care UrlScript le recunoaște: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` este adresa URL de bază a aplicației, inclusiv domeniul și partea căii către directorul rădăcină al aplicației -- `basePath` este partea căii către directorul rădăcină al aplicației -- `scriptPath` este calea către scriptul curent -- `relativePath` este numele scriptului (eventual alte segmente ale căii) relativ la basePath -- `relativeUrl` este întreaga parte a URL-ului după baseUrl, inclusiv query string și fragment. -- `pathInfo` este o parte a URL-ului, astăzi puțin utilizată, după numele scriptului - -Pentru returnarea părților URL-ului sunt disponibile metodele: - -.[language-php] -| Getter | Valoare returnată -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Obiectele `UrlScript` de obicei nu le creăm direct, ci le returnează metoda [Nette\Http\Request::getUrl()|request] cu componentele deja setate corect pentru cererea HTTP curentă. diff --git a/http/ru/@home.texy b/http/ru/@home.texy index afeb7a8938..2867956939 100644 --- a/http/ru/@home.texy +++ b/http/ru/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -Пакет `nette/http` инкапсулирует [HTTP-запрос|request] и [ответ|response], работу с [сессиями|sessions] и [парсинг и составление URL |urls]. +Пакет `nette/http` - ваш спутник во всём, что касается HTTP-общения. Он предоставляет понятный объектный API над входящим запросом и исходящим ответом, упрощает работу с сессиями и URL-адресами, а вдобавок заботится о безопасности. Вот что вы здесь найдёте: + +| [HTTP-запрос |request] | входящий запрос и очистка входных данных +| [HTTP-ответ |response] | исходящий ответ, заголовки и cookie +| [Сессии|sessions] | безопасное сохранение состояния между запросами +| [Работа с URL |urls] | разбор и построение URL-адресов +| [Защита от SSRF |ssrf] | защита от атак Server-Side Request Forgery +| [Конфигурация|configuration] | параметры конфигурации пакета Установка --------- -Скачать и установить библиотеку можно с помощью [Composer|best-practices:composer]: +Скачайте и установите пакет с помощью [Composer|best-practices:composer]: ```shell composer require nette/http ``` + +Пакет требует PHP версии от 8.3 до 8.5. diff --git a/http/ru/@left-menu.texy b/http/ru/@left-menu.texy index 37b139824b..175ac5f60e 100644 --- a/http/ru/@left-menu.texy +++ b/http/ru/@left-menu.texy @@ -1,8 +1,19 @@ Nette HTTP ********** -- [Введение |@home] -- [HTTP-запрос|request] -- [HTTP-ответ|response] -- [Сессии |sessions] -- [Утилиты URL |urls] -- [Конфигурация |configuration] +- [Обзор |@home] +- [HTTP-запрос |request] +- [HTTP-ответ |response] +- [Сессии|sessions] +- [Работа с URL |urls] +- [Защита от SSRF |ssrf] +- [Конфигурация|configuration] +- [Обновление|upgrading] + + +Дополнительные материалы +************************ +- [Документация Nette |nette:] +- [Nette Application |application:how-it-works] +- [Utilities |utils:] +- [Лучшие практики |best-practices:] +- [Устранение неполадок |nette:troubleshooting] diff --git a/http/ru/configuration.texy b/http/ru/configuration.texy index 2000b80e7e..dbaef9bc23 100644 --- a/http/ru/configuration.texy +++ b/http/ru/configuration.texy @@ -2,9 +2,9 @@ ***************** .[perex] -Обзор опций конфигурации для Nette HTTP. +Обзор параметров конфигурации Nette HTTP. -Если вы не используете весь фреймворк, а только эту библиотеку, прочитайте, [как загрузить конфигурацию|bootstrap:]. +Если вы используете не весь фреймворк, а только эту библиотеку, прочитайте, [как загрузить конфигурацию|bootstrap:]. HTTP-заголовки @@ -12,29 +12,31 @@ HTTP-заголовки ```neon http: - # заголовки, которые отправляются с каждым запросом + # заголовки, которые отправляются с каждым ответом headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' # влияет на заголовок X-Frame-Options - frames: ... # (string|bool) по умолчанию 'SAMEORIGIN' + frames: ... # (string|bool|null) по умолчанию 'SAMEORIGIN' ``` -Фреймворк из соображений безопасности отправляет заголовок `X-Frame-Options: SAMEORIGIN`, который говорит, что страницу можно отображать внутри другой страницы (в элементе `<iframe>`) только если она находится на том же домене. Это может быть нежелательно в некоторых ситуациях (например, если вы разрабатываете приложение для Facebook), поэтому поведение можно изменить, установив `frames: http://allowed-host.com` или `frames: true`. +Из соображений безопасности фреймворк отправляет заголовок `X-Frame-Options: SAMEORIGIN`, который говорит, что страницу можно показывать внутри другой страницы (в элементе `<iframe>`), только если та находится на том же домене. В некоторых ситуациях это может быть нежелательно (например, если вы разрабатываете приложение для Facebook), поэтому поведение можно изменить: `frames: http://allowed-host.com` разрешает конкретный хост, `frames: true` разрешает вставку откуда угодно (заголовок опускается), а `frames: false` запрещает её полностью (`X-Frame-Options: DENY`). + +По умолчанию Nette отправляет ещё и заголовки `X-Powered-By: Nette Framework 3` и `Content-Type: text/html; charset=utf-8`. Любой заголовок, в том числе эти стандартные, можно убрать, задав его значением пустую строку. Content Security Policy ----------------------- -Легко можно составить заголовки `Content-Security-Policy` (далее CSP), их описание вы найдете в [описании CSP |https://content-security-policy.com]. Директивы CSP (например, `script-src`) могут быть записаны либо как строки согласно спецификации, либо как массив значений для лучшей читаемости. Тогда не нужно вокруг ключевых слов, таких как `'self'`, писать кавычки. Nette также автоматически генерирует значение `nonce`, так что в заголовке будет, например, `'nonce-y4PopTLM=='`. +Заголовки `Content-Security-Policy` (CSP) легко настраиваются; их описание вы найдёте в [спецификации CSP |https://content-security-policy.com]. Директивы CSP (например, `script-src`) можно записывать либо строками согласно спецификации, либо массивами значений для лучшей читаемости. Тогда не нужно использовать кавычки вокруг ключевых слов вроде `'self'`. Nette также автоматически породит значение `nonce`, так что в заголовке будет отправлено что-то вроде `'nonce-y4PopTLM=='`. ```neon http: # Content Security Policy csp: - # строка в формате согласно спецификации CSP + # строка согласно спецификации CSP default-src: "'self' https://example.com" # массив значений @@ -49,9 +51,9 @@ http: block-all-mixed-content: false ``` -В шаблонах используйте `<script n:nonce>...</script>`, и значение nonce будет добавлено автоматически. Делать безопасные сайты в Nette действительно легко. +В шаблонах используйте `<script n:nonce>...</script>`, и значение nonce подставится автоматически. Делать безопасные сайты в Nette действительно легко. -Аналогично можно составить и заголовки `Content-Security-Policy-Report-Only` (которые можно использовать параллельно с CSP) и [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: +Похожим образом настраиваются заголовки `Content-Security-Policy-Report-Only` (их можно использовать одновременно с CSP) и [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: ```neon http: @@ -72,91 +74,103 @@ http: HTTP cookie ----------- -Можно изменить значения по умолчанию некоторых параметров метода [Nette\Http\Response::setCookie() |response#setCookie] и сессии. +Вы можете изменить значения по умолчанию некоторых параметров метода [Nette\Http\Response::setCookie() |response#setCookie()] и работы с сессией. ```neon http: # область действия cookie по пути cookiePath: ... # (string) по умолчанию '/' - # домены, которые принимают cookie - cookieDomain: 'example.com' # (string|domain) по умолчанию не установлено + # домены, которые могут принимать cookie + cookieDomain: 'example.com' # (string|domain) по умолчанию не задано # отправлять cookie только через HTTPS? cookieSecure: ... # (bool|auto) по умолчанию auto - # отключает отправку cookie, которую использует Nette как защиту от CSRF + # отключает отправку cookie, которую Nette использует для защиты от CSRF disableNetteCookie: ... # (bool) по умолчанию false ``` -Атрибут `cookieDomain` определяет, какие домены могут принимать cookie. Если он не указан, cookie принимает тот же (суб)домен, который его установил, *но не* его субдомены. Если `cookieDomain` указан, субдомены также включаются. Поэтому указание `cookieDomain` менее ограничивающее, чем его отсутствие. +Атрибут `cookieDomain` определяет, какие домены (источники) могут принимать cookie. Если он не указан, cookie принимает тот же (под)домен, который её задал, *исключая* его поддомены. Если `cookieDomain` указан, поддомены тоже включаются. Поэтому указание `cookieDomain` менее ограничительно, чем его отсутствие. -Например, при `cookieDomain: nette.org` cookie доступны и на всех субдоменах, таких как `doc.nette.org`. Того же можно достичь также с помощью специального значения `domain`, то есть `cookieDomain: domain`. +Например, если задано `cookieDomain: nette.org`, cookie доступны и на всех поддоменах вроде `doc.nette.org`. Того же можно добиться особым значением `domain`, то есть `cookieDomain: domain`. -Значение по умолчанию `auto` у атрибута `cookieSecure` означает, что если сайт работает по HTTPS, cookie будут отправляться с флагом `Secure` и, следовательно, будут доступны только через HTTPS. +Значение по умолчанию `auto` у атрибута `cookieSecure` означает, что если сайт работает по HTTPS, cookie будут отправляться с флагом `Secure` и, стало быть, будут доступны только по HTTPS. HTTP-прокси ----------- -Если сайт работает за HTTP-прокси, укажите его IP-адрес, чтобы правильно работало обнаружение соединения через HTTPS, а также IP-адреса клиента. То есть, чтобы функции [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] и [isSecured() |request#isSecured] возвращали правильные значения, и в шаблонах генерировались ссылки с протоколом `https:`. +Если сайт работает за HTTP-прокси, укажите IP-адрес прокси, чтобы правильно работали определение HTTPS-соединения и IP-адреса клиента. То есть чтобы [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()] и [isSecured() |request#isSecured()] возвращали верные значения, а ссылки в шаблонах порождались с протоколом `https:`. + +```neon +http: + # IP-адрес, диапазон (например, 127.0.0.1/8) или массив таких значений + proxy: 127.0.0.1 # (string|string[]) по умолчанию не задано +``` + + +Принудительный HTTPS .{data-version:3.3.4} +------------------------------------------ + +Безусловно принуждает схему запроса к HTTPS. Это полезно для сайтов, работающих только по HTTPS, за балансировщиком нагрузки или обратным прокси, который завершает TLS, но не передаёт заголовок `X-Forwarded-Proto`, так что стандартное определение HTTPS (даже с настроенным [#HTTP-прокси]) его не поймает. ```neon http: - # IP-адрес, диапазон (например, 127.0.0.1/8) или массив этих значений - proxy: 127.0.0.1 # (string|string[]) по умолчанию не установлено + # принудительно использовать схему HTTPS для всех запросов + forceHttps: true # (bool) по умолчанию false ``` Сессия ====== -Базовые настройки [сессий|sessions]: +Основные настройки [сессий |sessions]: ```neon session: - # отображать панель сессии в Tracy Bar? + # показывать панель сессии в Tracy Bar? debugger: ... # (bool) по умолчанию false - # время неактивности, после которого сессия истечет + # время бездействия, после которого сессия истекает expiration: 14 days # (string) по умолчанию '3 hours' - # когда должна запускаться сессия? + # когда сессия должна запускаться? autoStart: ... # (smart|always|never) по умолчанию 'smart' - # обработчик, сервис, реализующий интерфейс SessionHandlerInterface + # обработчик, сервис, реализующий SessionHandlerInterface handler: @handlerService ``` -Опция `autoStart` управляет тем, когда должна запускаться сессия. Значение `always` означает, что сессия запустится всегда при запуске приложения. Значение `smart` означает, что сессия запустится при старте приложения только тогда, когда она уже существует, или в момент, когда мы хотим из нее читать или в нее записывать. И наконец, значение `never` запрещает автоматический запуск сессии. +Параметр `autoStart` управляет тем, когда должна запускаться сессия. Значение `always` означает, что сессия запускается всегда при старте приложения. Значение `smart` означает, что сессия запускается вместе с приложением, только если она уже существует, либо в момент, когда мы хотим из неё читать или в неё писать. Наконец, значение `never` отключает автоматический запуск сессии. -Далее можно настраивать все PHP [директивы сессии |https://www.php.net/manual/en/session.configuration.php] (в формате camelCase) и также [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Пример: +Кроме того, можно задать все [директивы сессии |https://www.php.net/manual/en/session.configuration.php] PHP (в формате camelCase), а также [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Например: ```neon session: - # 'session.name' запишем как 'name' + # 'session.name' записывается как 'name' name: MYID - # 'session.save_path' запишем как 'savePath' + # 'session.save_path' записывается как 'savePath' savePath: "%tempDir%/sessions" ``` -Session cookie --------------- +Cookie сессии +------------- -Session cookie отправляется с теми же параметрами, что и [другие cookie |#HTTP cookie], но эти вы можете для нее изменить: +Cookie сессии отправляется с теми же параметрами, что и [другие cookie |#HTTP cookie], но именно для неё их можно изменить: ```neon session: - # домены, которые принимают cookie + # домены, которые могут принимать cookie cookieDomain: 'example.com' # (string|domain) - # ограничение при доступе с другого домена + # ограничение доступа с другого источника cookieSamesite: None # (Strict|Lax|None) по умолчанию Lax ``` -Атрибут `cookieSamesite` влияет на то, будет ли cookie отправлена при [доступе с другого домена |nette:glossary#SameSite cookie], что обеспечивает определенную защиту от атак [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). +Атрибут `cookieSamesite` влияет на то, отправляется ли cookie при [запросах с другого источника |nette:glossary#SameSite cookie], что даёт некоторую защиту от атак [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Сервисы DI @@ -165,7 +179,8 @@ session: Эти сервисы добавляются в DI-контейнер: | Имя | Тип | Описание -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP-запрос| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP-ответ| response] -| `session.session` | [api:Nette\Http\Session] | [управление сессией| sessions] +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [HTTP-запрос| request] +| `http.response` | [api:Nette\Http\Response] | [HTTP-ответ| response] +| `session.session`| [api:Nette\Http\Session] | [работа с сессией| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | фабрика, создающая HTTP-запрос diff --git a/http/ru/request.texy b/http/ru/request.texy index 1d49bdd389..19405c2555 100644 --- a/http/ru/request.texy +++ b/http/ru/request.texy @@ -2,11 +2,11 @@ HTTP-запрос *********** .[perex] -Nette инкапсулирует HTTP-запрос в объекты с понятным API и одновременно предоставляет санитайзирующий фильтр. +Nette инкапсулирует HTTP-запрос в объекты с понятным API и при этом предоставляет фильтр очистки. -HTTP-запрос представляет собой объект [api:Nette\Http\Request]. Если вы работаете с Nette, этот объект автоматически создается фреймворком, и вы можете получить его с помощью [внедрения зависимостей |dependency-injection:passing-dependencies]. В презентерах достаточно просто вызвать метод `$this->getHttpRequest()`. Если вы работаете вне Nette Framework, вы можете создать объект с помощью [#RequestFactory]. +HTTP-запрос представлен объектом [api:Nette\Http\Request]. Если вы работаете с Nette, этот объект создаёт фреймворк автоматически, и вы можете получить его через [внедрение зависимостей |dependency-injection:passing-dependencies]. В презентерах достаточно вызвать метод `$this->getHttpRequest()`. Если вы работаете вне Nette Framework, объект можно создать с помощью [#RequestFactory]. -Большим преимуществом Nette является то, что при создании объекта он автоматически очищает все входные параметры GET, POST, COOKIE, а также URL от управляющих символов и невалидных UTF-8 последовательностей. С этими данными затем можно безопасно работать дальше. Очищенные данные впоследствии используются в презентерах и формах. +Большое преимущество Nette в том, что при создании объекта он автоматически очищает все входные параметры (GET, POST, COOKIE), а также URL от управляющих символов и некорректных последовательностей UTF-8. С этими данными вы затем можете безопасно работать. Очищенные данные потом используются в презентерах и формах. → [Установка и требования |@home#Установка] @@ -14,7 +14,7 @@ HTTP-запрос представляет собой объект [api:Nette\Ht Nette\Http\Request ================== -Этот объект является immutable (неизменяемым). У него нет сеттеров, есть только один так называемый wither `withUrl()`, который не изменяет объект, а возвращает новый экземпляр с измененным значением. +Этот объект неизменяем. У него нет сеттеров, есть только один так называемый виттер `withUrl()`, который объект не меняет, а возвращает новый экземпляр с изменённым значением. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] @@ -28,11 +28,11 @@ getUrl(): Nette\Http\UrlScript .[method] ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -Внимание: браузеры не отправляют фрагмент на сервер, поэтому `$url->getFragment()` будет возвращать пустую строку. +Внимание: браузеры фрагмент на сервер не отправляют, поэтому `$url->getFragment()` вернёт пустую строку. getQuery(?string $key=null): string|array|null .[method] @@ -40,8 +40,8 @@ getQuery(?string $key=null): string|array|null .[method] Возвращает параметры GET-запроса. ```php -$all = $httpRequest->getQuery(); // возвращает массив всех параметров из URL -$id = $httpRequest->getQuery('id'); // возвращает GET-параметр 'id' (или null) +$all = $httpRequest->getQuery(); // массив всех параметров URL +$id = $httpRequest->getQuery('id'); // вернёт GET-параметр 'id' (либо null) ``` @@ -50,31 +50,31 @@ getPost(?string $key=null): string|array|null .[method] Возвращает параметры POST-запроса. ```php -$all = $httpRequest->getPost(); // возвращает массив всех параметров из POST -$id = $httpRequest->getPost('id'); // возвращает POST-параметр 'id' (или null) +$all = $httpRequest->getPost(); // массив всех параметров POST +$id = $httpRequest->getPost('id'); // вернёт POST-параметр 'id' (либо null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- Возвращает [загруженный файл |#Загруженные файлы] как объект [api:Nette\Http\FileUpload]: ```php $file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // был ли загружен какой-либо файл? +if ($file?->hasFile()) { // был ли загружен какой-нибудь файл? $file->getUntrustedName(); // имя файла, отправленное пользователем $file->getSanitizedName(); // имя без опасных символов } ``` -Для доступа к вложенной структуре укажите массив ключей. +Для доступа к вложенной структуре передайте массив ключей. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Поскольку нельзя доверять данным извне и, следовательно, полагаться на структуру файлов, этот способ безопаснее, чем, например, `$request->getFiles()['my-form']['details']['avatar']`, который может не сработать. +Поскольку внешним данным доверять нельзя и, стало быть, нельзя полагаться на структуру файлов, такой подход безопаснее, чем, например, `$request->getFiles()['my-form']['details']['avatar']`, который может дать сбой. getFiles(): array .[method] @@ -86,9 +86,9 @@ $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Возвращает cookie или `null`, если она не существует. +getCookie(string $key): ?string .[method] +----------------------------------------- +Возвращает cookie или `null`, если её нет. ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -Возвращает HTTP-метод, с которым был сделан запрос. +Возвращает HTTP-метод, которым был выполнен запрос. ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -Проверяет HTTP-метод, с которым был сделан запрос. Параметр нечувствителен к регистру. +Проверяет HTTP-метод, которым был выполнен запрос. Параметр нечувствителен к регистру. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,51 +124,83 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -Возвращает HTTP-заголовок или `null`, если он не существует. Параметр нечувствителен к регистру. +Возвращает HTTP-заголовок или `null`, если его нет. Параметр нечувствителен к регистру. ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Возвращает все HTTP-заголовки как ассоциативный массив. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Возвращает все HTTP-заголовки ассоциативным массивом. Ключи приводятся к нижнему регистру. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -Является ли соединение шифрованным (HTTPS)? Для правильной работы может потребоваться [настроить прокси |configuration#HTTP-прокси]. +Зашифровано ли соединение (HTTPS)? Для правильной работы может потребоваться [настройка прокси |configuration#HTTP-прокси]. -isSameSite(): bool .[method] ----------------------------- -Приходит ли запрос с того же (суб)домена и инициирован ли он кликом по ссылке? Nette для обнаружения использует cookie `_nss` (ранее `nette-samesite`). +isSameSite(): bool .[method deprecated] +--------------------------------------- +Пришёл ли запрос с того же сайта? Начиная с версии 3.4 его заменяет более способный [isFrom() |#isFrom()]. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +Говорит, откуда пришёл запрос и как браузер его выполнил, на основе заголовков `Sec-Fetch-*` (так называемых [Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header]), которые браузер задаёт сам и которые страница, работающая в браузере жертвы, не может ни подделать, ни удалить. Nette использует его внутри для автоматической защиты форм и сигналов от [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF). Он полезен, когда вы хотите защитить собственные чувствительные действия, например конечные точки API или разрушительные ссылки. + +Метод возвращает `true`, только когда запрос отвечает **всем** заданным вами условиям. Первый параметр `$site` описывает отношение между страницей, которая инициировала запрос, и вашим сайтом (заголовок `Sec-Fetch-Site`). Он принимает одно значение или список таких вариантов `FetchSite`: + +- `FetchSite::SameOrigin` - ровно с того же источника (схема, хост и порт) +- `FetchSite::SameSite` - с того же сайта, возможно с другого поддомена +- `FetchSite::CrossSite` - с чужого сайта +- `FetchSite::None` - пользователь инициировал его напрямую, например введя URL или открыв закладку + +```php +// пришёл ли запрос с наших собственных страниц? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // блокируем действие +} +``` + +Необязательный параметр `$dest` (заголовок `Sec-Fetch-Dest`) говорит, какой ресурс браузер получает, например `FetchDest::Document` для перехода верхнего уровня или `FetchDest::Empty` для запроса, выполненного из JavaScript. Необязательный параметр `$user` (заголовок `Sec-Fetch-User`) обозначает, был ли переход вызван настоящим действием пользователя, например щелчком по ссылке или отправкой формы; передайте `true`, чтобы этого требовать. + +Проверка того, что действие доступно только с ваших собственных страниц и только через реальное действие пользователя, выглядит тогда так: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Старые браузеры (Safari до 16.4) заголовки `Sec-Fetch-*` не отправляют. Для них Nette откатывается к cookie `SameSite=Strict`, которая доказывает лишь то, что запрос не межсайтовый. Проверку, дополнительно требующую `$dest` или `$user`, так подтвердить нельзя, и в таких браузерах она возвращает `false`; если это слишком строго, проверяйте только `$site`. isAjax(): bool .[method] ------------------------ -Является ли это AJAX-запросом? +Это AJAX-запрос? getRemoteAddress(): ?string .[method] ------------------------------------- -Возвращает IP-адрес пользователя. Для правильной работы может потребоваться [настроить прокси |configuration#HTTP-прокси]. +Возвращает IP-адрес пользователя. Для правильной работы может потребоваться [настройка прокси |configuration#HTTP-прокси]. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Возвращает DNS-преобразование IP-адреса пользователя. Для правильной работы может потребоваться [настроить прокси |configuration#HTTP-прокси]. +Устаревший, всегда возвращает `null`. Обратные запросы к DNS были медленными и ненадёжными; если вам нужно имя хоста, разрешите его сами из [getRemoteAddress() |#getRemoteAddress()]. getBasicCredentials(): ?array .[method] --------------------------------------- -Возвращает учетные данные для [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. +Возвращает учётные данные для [базовой HTTP-аутентификации |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. ```php [$user, $password] = $httpRequest->getBasicCredentials(); @@ -184,12 +216,36 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Возвращает источник, из которого пришёл запрос. Источник состоит из схемы (протокола), имени хоста и порта, например `https://example.com:8080`. Возвращает `null`, если заголовка origin нет или он равен `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Браузер отправляет заголовок `Origin` в следующих случаях: +- запросы с другого источника (AJAX-вызовы к другому домену) +- POST, PUT, DELETE и другие изменяющие запросы +- запросы, выполненные через Fetch API + +Браузер НЕ отправляет заголовок `Origin` при: +- обычных GET-запросах к тому же домену (переход в рамках того же источника) +- прямом переходе вводом URL в адресную строку +- запросах от клиентов, не являющихся браузерами + +.[note] +В отличие от заголовка `Referer`, `Origin` содержит только схему, хост и порт, а не полный путь URL. Это делает его более подходящим для проверок безопасности и при этом сохраняет приватность пользователя. Заголовок `Origin` в первую очередь используется для проверки [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing). + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Определяет язык. В качестве параметра `$langs` передаем массив языков, которые поддерживает приложение, и она возвращает тот, который предпочел бы видеть браузер посетителя. Это не магия, просто используется заголовок `Accept-Language`. Если совпадений нет, возвращает `null`. +Определяет язык. Параметром `$langs` передайте массив языков, которые поддерживает приложение, и метод вернёт тот, который предпочитает браузер посетителя. Никакого волшебства, он просто использует заголовок `Accept-Language`. Если совпадений нет, возвращает `null`. ```php -// браузер отправляет, например, Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 +// Браузер отправляет, например, Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 $langs = ['hu', 'pl', 'en']; // языки, поддерживаемые приложением echo $httpRequest->detectLanguage($langs); // en @@ -199,34 +255,35 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -Класс [api:Nette\Http\RequestFactory] служит для создания экземпляра `Nette\Http\Request`, который представляет текущий HTTP-запрос. (Если вы работаете с Nette, объект HTTP-запроса автоматически создается фреймворком.) +Класс [api:Nette\Http\RequestFactory] служит для создания экземпляра `Nette\Http\Request`, представляющего текущий HTTP-запрос. (Если вы работаете с Nette, объект HTTP-запроса создаёт фреймворк автоматически.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -Метод `fromGlobals()` создает объект запроса на основе текущих глобальных переменных PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` и `$_SERVER`). При создании объекта он автоматически очищает все входные параметры GET, POST, COOKIE, а также URL от управляющих символов и невалидных UTF-8 последовательностей, что обеспечивает безопасность при дальнейшей работе с этими данными. +Метод `fromGlobals()` создаёт объект запроса на основе текущих глобальных переменных PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` и `$_SERVER`). При создании объекта он автоматически очищает все входные параметры (GET, POST, COOKIE), а также URL от управляющих символов и некорректных последовательностей UTF-8, что обеспечивает безопасность при дальнейшей работе с этими данными. -RequestFactory можно сконфигурировать перед вызовом `fromGlobals()`: +RequestFactory можно настроить до вызова `fromGlobals()`: -- методом `$factory->setBinary()` отключите автоматическую очистку входных параметров от управляющих символов и невалидных UTF-8 последовательностей. -- методом `$factory->setProxy(...)` укажите IP-адрес [прокси-сервера |configuration#HTTP-прокси], что необходимо для правильного определения IP-адреса пользователя. +- метод `$factory->setBinary()` отключает автоматическую очистку входных параметров от управляющих символов и некорректных последовательностей UTF-8. +- метод `$factory->setProxy(...)` задаёт IP-адрес [прокси-сервера |configuration#HTTP-прокси], что необходимо для правильного определения IP-адреса пользователя. +- метод `$factory->setForceHttps()` .{data-version:3.3.4} принудительно задаёт схему запроса HTTPS независимо от окружения сервера. -RequestFactory позволяет определить фильтры, которые автоматически трансформируют части URL запроса. Эти фильтры удаляют нежелательные символы из URL, которые могут быть туда вставлены, например, из-за неправильной реализации систем комментариев на различных веб-сайтах: +RequestFactory позволяет определить фильтры, которые автоматически преобразуют части URL запроса. Эти фильтры убирают из URL нежелательные символы, которые могли попасть туда, например, из-за неправильных реализаций систем комментариев на разных сайтах: ```php -// удаление пробелов из пути +// убираем пробелы из пути $requestFactory->urlFilters['path']['%20'] = ''; -// удаление точки, запятой или правой скобки с конца URI +// убираем точку, запятую или правую скобку с конца URI $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// очистка пути от удвоенных слешей (фильтр по умолчанию) +// очищаем путь от двойных слешей (фильтр по умолчанию) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -Первый ключ `'path'` или `'url'` определяет, к какой части URL применяется фильтр. Второй ключ — это регулярное выражение, которое нужно найти, а значение — это замена, которая будет использована вместо найденного текста. +Первый ключ, `'path'` или `'url'`, определяет, к какой части URL будет применён фильтр. Второй ключ - регулярное выражение для поиска, а значение - замена, которая будет использована вместо найденного текста. Загруженные файлы @@ -234,27 +291,27 @@ $requestFactory->urlFilters['path']['/{2,}'] = '/'; Метод `Nette\Http\Request::getFiles()` возвращает массив всех загруженных файлов в нормализованной структуре, листьями которой являются объекты [api:Nette\Http\FileUpload]. Они инкапсулируют данные, отправленные элементом формы `<input type=file>`. -Структура отражает именование элементов в HTML. В простейшем случае это может быть единственный именованный элемент формы, отправленный как: +Структура отражает именование элементов в HTML. В простейшем случае это может быть один именованный элемент формы, отправленный так: ```latte <input type="file" name="avatar"> ``` -В этом случае `$request->getFiles()` возвращает массив: +В этом случае `$request->getFiles()` вернёт массив: ```php [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* экземпляр FileUpload */ ] ``` -Объект `FileUpload` создается и в случае, если пользователь не отправил ни одного файла или отправка не удалась. Был ли файл отправлен, возвращает метод `hasFile()`: +Объект `FileUpload` создаётся, даже если пользователь никакого файла не загрузил или загрузка не удалась. Метод `hasFile()` возвращает true, если файл был отправлен: ```php $request->getFile('avatar')?->hasFile(); ``` -В случае имени элемента, использующего нотацию для массива: +В случае имени элемента с записью через массив: ```latte <input type="file" name="my-form[details][avatar]"> @@ -266,13 +323,13 @@ $request->getFile('avatar')?->hasFile(); [ 'my-form' => [ 'details' => [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* экземпляр FileUpload */ ], ], ] ``` -Можно создать и массив файлов: +Можно создавать и массивы файлов: ```latte <input type="file" name="my-form[details][avatars][]" multiple> @@ -285,25 +342,25 @@ $request->getFile('avatar')?->hasFile(); 'my-form' => [ 'details' => [ 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, + 0 => /* экземпляр FileUpload */, + 1 => /* экземпляр FileUpload */, + 2 => /* экземпляр FileUpload */, ], ], ], ] ``` -Доступ к индексу 1 вложенного массива лучше всего получить так: +Лучше всего обращаться к элементу с индексом 1 вложенного массива так: ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -Поскольку нельзя доверять данным извне и, следовательно, полагаться на структуру файлов, этот способ безопаснее, чем, например, `$request->getFiles()['my-form']['details']['avatars'][1]`, который может не сработать. +Поскольку внешним данным доверять нельзя и, стало быть, нельзя полагаться на структуру файлов, такой подход безопаснее, чем, например, `$request->getFiles()['my-form']['details']['avatars'][1]`, который может дать сбой. Обзор методов `FileUpload` .{toc: FileUpload} @@ -312,7 +369,7 @@ if ($file instanceof FileUpload) { hasFile(): bool .[method] ------------------------- -Возвращает `true`, если пользователь загрузил какой-либо файл. +Возвращает `true`, если пользователь загрузил файл. isOk(): bool .[method] @@ -322,12 +379,12 @@ isOk(): bool .[method] getError(): int .[method] ------------------------- -Возвращает код ошибки при загрузке файла. Это одна из констант [UPLOAD_ERR_XXX |http://php.net/manual/en/features.file-upload.errors.php]. В случае, если загрузка прошла успешно, возвращает `UPLOAD_ERR_OK`. +Возвращает код ошибки, связанный с загруженным файлом. Это одна из констант [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php]. Если файл был загружен успешно, возвращает `UPLOAD_ERR_OK`. -move(string $dest): void .[method] ----------------------------------- -Перемещает загруженный файл в новое местоположение. Если целевой файл уже существует, он будет перезаписан. +move(string $dest) .[method] +---------------------------- +Перемещает загруженный файл в новое место. Если целевой файл уже существует, он будет перезаписан. ```php $file->move('/path/to/files/name.ext'); @@ -336,72 +393,77 @@ $file->move('/path/to/files/name.ext'); getContents(): ?string .[method] -------------------------------- -Возвращает содержимое загруженного файла. В случае, если загрузка не была успешной, возвращает `null`. +Возвращает содержимое загруженного файла. Если загрузка не удалась, возвращает `null`. getContentType(): ?string .[method] ----------------------------------- -Определяет MIME content type загруженного файла на основе его сигнатуры. В случае, если загрузка не была успешной или определение не удалось, возвращает `null`. +Определяет MIME-тип содержимого загруженного файла по его сигнатуре. Если загрузка не удалась или определение не удалось, возвращает `null`. .[caution] -Требует PHP-расширение `fileinfo`. +Требует PHP-расширения `fileinfo`. getUntrustedName(): string .[method] ------------------------------------ -Возвращает оригинальное имя файла, как его отправил браузер. +Возвращает исходное имя файла в том виде, в каком его отправил браузер. .[caution] -Не доверяйте значению, возвращаемому этим методом. Клиент мог отправить вредоносное имя файла с намерением повредить или взломать ваше приложение. +Не доверяйте значению, которое возвращает этот метод. Клиент мог отправить вредоносное имя файла с намерением повредить или взломать ваше приложение. getSanitizedName(): string .[method] ------------------------------------ -Возвращает очищенное (sanitized) имя файла. Содержит только ASCII-символы `[a-zA-Z0-9.-]`. Если имя не содержит таких символов, возвращает `'unknown'`. Если файл является изображением в формате JPEG, PNG, GIF, WebP или AVIF, возвращает и правильное расширение. +Возвращает очищенное имя файла. Оно содержит только ASCII-символы `[a-zA-Z0-9.-]`. Если имя таких символов не содержит, возвращает `'unknown'`. Если файл - изображение JPEG, PNG, GIF, WebP или AVIF, возвращает ещё и правильное расширение файла. .[caution] -Требует PHP-расширение `fileinfo`. +Требует PHP-расширения `fileinfo`. getSuggestedExtension(): ?string .[method]{data-version:3.2.4} -------------------------------------------------------------- -Возвращает подходящее расширение файла (без точки), соответствующее определенному MIME-типу. +Возвращает подходящее расширение файла (без точки), соответствующее определённому MIME-типу. .[caution] -Требует PHP-расширение `fileinfo`. +Требует PHP-расширения `fileinfo`. getUntrustedFullPath(): string .[method] ---------------------------------------- -Возвращает оригинальный путь к файлу, как его отправил браузер при загрузке папки. Полный путь доступен только в PHP 8.1 и выше. В предыдущих версиях этот метод возвращает оригинальное имя файла. +Возвращает исходный путь к файлу в том виде, в каком его отправил браузер при загрузке каталога. Полный путь доступен только в PHP 8.1 и новее. В предыдущих версиях этот метод возвращает исходное имя файла. .[caution] -Не доверяйте значению, возвращаемому этим методом. Клиент мог отправить вредоносное имя файла с намерением повредить или взломать ваше приложение. +Не доверяйте значению, которое возвращает этот метод. Клиент мог отправить вредоносное имя файла с намерением повредить или взломать ваше приложение. getSize(): int .[method] ------------------------ -Возвращает размер загруженного файла. В случае, если загрузка не была успешной, возвращает `0`. +Возвращает размер загруженного файла. Если загрузка не удалась, возвращает `0`. getTemporaryFile(): string .[method] ------------------------------------ -Возвращает путь к временному местоположению загруженного файла. В случае, если загрузка не была успешной, возвращает `''`. +Возвращает путь ко временному расположению загруженного файла. Если загрузка не удалась, возвращает `''`. + + +__toString(): string .[method] +------------------------------ +Возвращает путь ко временному расположению загруженного файла. Это позволяет использовать объект `FileUpload` прямо как строку. isImage(): bool .[method] ------------------------- -Возвращает `true`, если загруженный файл является изображением в формате JPEG, PNG, GIF, WebP или AVIF. Определение происходит на основе его сигнатуры и не проверяется целостность всего файла. Не повреждено ли изображение, можно узнать, например, попытавшись его [загрузить |#toImage]. +Возвращает `true`, если загруженный файл - изображение JPEG, PNG, GIF, WebP или AVIF. Определение выполняется по его сигнатуре и не проверяет целостность всего файла. Выяснить, не повреждено ли изображение, можно, например, попыткой [его загрузить |#toImage()]. .[caution] -Требует PHP-расширение `fileinfo`. +Требует PHP-расширения `fileinfo`. getImageSize(): ?array .[method] -------------------------------- -Возвращает пару `[ширина, высота]` с размерами загруженного изображения. В случае, если загрузка не была успешной или это не валидное изображение, возвращает `null`. +Возвращает пару `[width, height]` с размерами загруженного изображения. Если загрузка не удалась или это не корректное изображение, возвращает `null`. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Загружает изображение как объект [Image |utils:images]. В случае, если загрузка не была успешной или это не валидное изображение, выбрасывает исключение `Nette\Utils\ImageException`. +Загружает изображение как объект [Image |utils:images]. Если загрузка не удалась или это не корректное изображение, выбрасывает `Nette\Utils\ImageException`. diff --git a/http/ru/response.texy b/http/ru/response.texy index f9ba4307f1..ef5925a225 100644 --- a/http/ru/response.texy +++ b/http/ru/response.texy @@ -4,7 +4,7 @@ HTTP-ответ .[perex] Nette инкапсулирует HTTP-ответ в объекты с понятным API. -HTTP-ответ представляет собой объект [api:Nette\Http\Response]. Если вы работаете с Nette, этот объект автоматически создается фреймворком, и вы можете получить его с помощью [внедрения зависимостей |dependency-injection:passing-dependencies]. В презентерах достаточно просто вызвать метод `$this->getHttpResponse()`. +HTTP-ответ представлен объектом [api:Nette\Http\Response]. Если вы работаете с Nette, этот объект создаёт фреймворк автоматически, и вы можете получить его через [внедрение зависимостей |dependency-injection:passing-dependencies]. В презентерах достаточно вызвать метод `$this->getHttpResponse()`. → [Установка и требования |@home#Установка] @@ -12,12 +12,12 @@ HTTP-ответ представляет собой объект [api:Nette\Http Nette\Http\Response =================== -Объект, в отличие от [Nette\Http\Request |request], является mutable (изменяемым), то есть с помощью сеттеров вы можете изменять состояние, например, отправлять заголовки. Не забывайте, что все сеттеры должны быть вызваны **перед отправкой любого вывода.** Был ли уже отправлен вывод, показывает метод `isSent()`. Если он возвращает `true`, любая попытка отправить заголовок вызовет исключение `Nette\InvalidStateException`. +В отличие от [Nette\Http\Request |request], этот объект изменяемый, так что вы можете сеттерами менять состояние, например отправлять заголовки. Помните, что все сеттеры **нужно вызывать до отправки какого-либо реального вывода**. Метод `isSent()` говорит, был ли вывод уже отправлен. Если он вернёт `true`, любая попытка отправить заголовок выбросит `Nette\InvalidStateException`. -setCode(int $code, ?string $reason=null): static .[method] ----------------------------------------------------------- -Изменяет [код состояния ответа |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Для лучшей понятности исходного кода рекомендуем для кода использовать вместо чисел [предопределенные константы |api:Nette\Http\IResponse]. +setCode(int $code, ?string $reason=null) .[method] +-------------------------------------------------- +Меняет [код состояния ответа |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Для лучшей читаемости исходного кода вместо самих чисел рекомендуется использовать [заранее определённые константы |api:Nette\Http\IResponse]. ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -31,20 +31,20 @@ getCode(): int .[method] isSent(): bool .[method] ------------------------ -Возвращает, были ли уже отправлены заголовки с сервера в браузер, и, следовательно, уже невозможно отправлять заголовки или изменять код состояния. +Возвращает, были ли заголовки уже отправлены с сервера в браузер, то есть что отправлять заголовки или менять код состояния уже нельзя. -setHeader(string $name, string $value): static .[method] --------------------------------------------------------- -Отправляет HTTP-заголовок и **перезаписывает** ранее отправленный заголовок с тем же именем. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Отправляет HTTP-заголовок и **перезаписывает** ранее отправленный заголовок с тем же именем. Если `$value` равно `null`, заголовок будет удалён. ```php $httpResponse->setHeader('Pragma', 'no-cache'); ``` -addHeader(string $name, string $value): static .[method] --------------------------------------------------------- +addHeader(string $name, string $value) .[method] +------------------------------------------------ Отправляет HTTP-заголовок и **не перезаписывает** ранее отправленный заголовок с тем же именем. ```php @@ -53,23 +53,23 @@ $httpResponse->addHeader('Accept', 'application/xml'); ``` -deleteHeader(string $name): static .[method] --------------------------------------------- +deleteHeader(string $name) .[method] +------------------------------------ Удаляет ранее отправленный HTTP-заголовок. getHeader(string $header): ?string .[method] -------------------------------------------- -Возвращает отправленный HTTP-заголовок или `null`, если такой не существует. Параметр нечувствителен к регистру. +Возвращает отправленный HTTP-заголовок или `null`, если его нет. Параметр нечувствителен к регистру. ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ -Возвращает все отправленные HTTP-заголовки как ассоциативный массив. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Возвращает все отправленные HTTP-заголовки ассоциативным массивом. ```php $headers = $httpResponse->getHeaders(); @@ -77,18 +77,18 @@ echo $headers['Pragma']; ``` -setContentType(string $type, ?string $charset=null): static .[method] ---------------------------------------------------------------------- -Изменяет заголовок `Content-Type`. +setContentType(string $type, ?string $charset=null) .[method] +------------------------------------------------------------- +Меняет заголовок `Content-Type`. ```php $httpResponse->setContentType('text/plain', 'UTF-8'); ``` -redirect(string $url, int $code = self::S302_Found): void .[method] -------------------------------------------------------------------- -Перенаправляет на другой URL. Не забудьте после этого завершить скрипт. +redirect(string $url, int $code=self::S302_Found): void .[method] +----------------------------------------------------------------- +Перенаправляет на другой URL. Не забудьте затем завершить скрипт. ```php $httpResponse->redirect('http://example.com'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time): static .[method] ----------------------------------------------- -Устанавливает срок действия (expiration) HTTP-документа с помощью заголовков `Cache-Control` и `Expires`. Параметром является либо временной интервал (как текст), либо `null`, что запрещает кеширование. +setExpiration(?string $expire) .[method] +---------------------------------------- +Задаёт срок действия HTTP-документа с помощью заголовков `Cache-Control` и `Expires`. Параметр - это либо временной интервал (текстом), либо `null`, который отключает кеширование. ```php -// кеш в браузере истечет через час +// кеш браузера истекает через час $httpResponse->setExpiration('1 hour'); ``` -sendAsFile(string $fileName): static .[method] ----------------------------------------------- -Ответ будет скачан с помощью диалогового окна *Сохранить как* под указанным именем. Сам файл при этом не отправляется. +sendAsFile(string $fileName) .[method] +-------------------------------------- +Ответ будет скачан через диалог *Сохранить как* с указанным именем. Сам файл при этом не отправляется. ```php -$httpResponse->sendAsFile('faktura.pdf'); +$httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Отправляет cookie. Значения по умолчанию параметров: +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +Отправляет cookie. Значения параметров по умолчанию: -| `$path` | `'/'` | cookie имеет область действия на все пути в (суб)домене *(настраиваемый)* -| `$domain` | `null` | что означает с областью действия на текущий (суб)домен, но не его субдомены *(настраиваемый)* -| `$secure` | `true` | если сайт работает по HTTPS, иначе `false` *(настраиваемый)* -| `$httpOnly` | `true` | cookie недоступен для JavaScript -| `$sameSite` | `'Lax'` | cookie может не отправляться при [доступе с другого домена |nette:glossary#SameSite cookie] +| `$path` | `'/'` | cookie доступна для всех путей в рамках (под)домена *(настраивается)* +| `$domain` | `null` | то есть доступна для текущего (под)домена, но не для его поддоменов *(настраивается)* +| `$secure` | `auto` | `true`, если сайт работает по HTTPS, иначе `false` (по умолчанию во фреймворке; у голого класса по умолчанию `false`) *(настраивается)* +| `$httpOnly` | `true` | cookie недоступна для JavaScript +| `$sameSite` | `'Lax'` | cookie может не отправляться при [доступе с другого источника |nette:glossary#SameSite cookie] +| `$partitioned` | `false` | является ли cookie секционированной, см. ниже *(начиная с v3.4)* -Значения по умолчанию параметров `$path`, `$domain` и `$secure` вы можете изменить в [конфигурации |configuration#HTTP cookie]. +Значения по умолчанию для параметров `$path`, `$domain` и `$secure` можно изменить в [конфигурации |configuration#HTTP cookie]. -Время можно указывать как количество секунд или строку: +Срок действия передаётся количеством секунд, текстовым интервалом или датой либо объектом `DateTimeInterface`. Значение `null` создаёт сессионную cookie, которую браузер выбрасывает при закрытии. Nette отправляет срок действия и в атрибуте `Expires`, и в `Max-Age`. ```php -$httpResponse->setCookie('lang', 'cs', '100 days'); +$httpResponse->setCookie('lang', 'en', '100 days'); // истекает через 100 дней +$httpResponse->setCookie('lang', 'en', null); // сессионная cookie ``` -Параметр `$domain` определяет, какие домены могут принимать cookie. Если он не указан, cookie принимает тот же (суб)домен, который его установил, но не его субдомены. Если `$domain` указан, субдомены также включаются. Поэтому указание `$domain` менее ограничивающее, чем его отсутствие. Например, при `$domain = 'nette.org'` cookie доступны и на всех субдоменах, таких как `doc.nette.org`. +Параметр `$domain` определяет, какие домены могут принимать cookie. Если он не указан, cookie принимает тот же (под)домен, который её задал, но не его поддомены. Если `$domain` указан, поддомены тоже включаются. Поэтому указание `$domain` менее ограничительно, чем его отсутствие. Например, при `$domain = 'nette.org'` cookie доступны и на всех поддоменах вроде `doc.nette.org`. + +Значение `$sameSite` можно передать перечислением `Nette\Http\SameSite`: `SameSite::Lax`, `SameSite::Strict` или `SameSite::None` (строковые значения `'Lax'`, `'Strict'`, `'None'` тоже работают). Если вы зададите `SameSite::None`, атрибут `$secure` включится автоматически, потому что браузеры отвергают cookie с `SameSite=None`, которая не является secure. -Для значения `$sameSite` вы можете использовать константы `Response::SameSiteLax`, `Response::SameSiteStrict` и `Response::SameSiteNone`. +.{data-version:3.4.0} +Секционированные cookie (CHIPS) дают cookie собственное отдельное хранилище для каждого сайта верхнего уровня. Поэтому когда сторонний сервис (например, встроенный виджет) устанавливает секционированную cookie, браузер хранит отдельную копию для каждого сайта, на котором виджет появляется, и эти копии нельзя связать между собой для межсайтовой слежки. Включается это заданием `$partitioned` в `true`; для этого требуется и атрибут `$secure`, поэтому он включается автоматически. + +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- -Удаляет cookie. Значения по умолчанию параметров: +Удаляет cookie. Значения параметров по умолчанию: - `$path` с областью действия на все каталоги (`'/'`) -- `$domain` с областью действия на текущий (суб)домен, но не его субдомены -- `$secure` определяется настройками в [конфигурации |configuration#HTTP cookie] +- `$domain` с областью действия на текущий (под)домен, но не на его поддомены +- `$secure` зависит от настроек в [конфигурации |configuration#HTTP cookie] ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +Объект [api:Nette\Http\Context] соединяет запрос и ответ вместе и помогает с HTTP-кешированием. Как сервис он не зарегистрирован, так что вы создаёте его сами. В презентерах обычно проще использовать метод [lastModified() |application:presenters#HTTP-кеширование]; контекст пригодится, когда вы отправляете ответ сами, например из собственного класса ответа. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +Определяет, изменилось ли содержимое со времени последнего посещения клиента. Если вы передадите время последнего изменения, он отправит заголовок `Last-Modified`; если вы передадите валидатор ETag (короткую строку, обозначающую текущую версию содержимого, например его хеш), он отправит заголовок `ETag`. Затем он сравнивает оба с заголовками `If-Modified-Since` и `If-None-Match`, отправленными браузером. + +Если у браузера уже есть подходящая версия, метод задаёт код `304 Not Modified` и возвращает `false` - в этом случае тело ответа вообще не отправляйте. Иначе он возвращает `true`. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Оба параметра необязательны. Если вы не знаете времени изменения содержимого, используйте только ETag, и наоборот. diff --git a/http/ru/sessions.texy b/http/ru/sessions.texy index 45fd7c9517..167488e9c2 100644 --- a/http/ru/sessions.texy +++ b/http/ru/sessions.texy @@ -3,19 +3,19 @@ <div class=perex> -HTTP — это протокол без состояния, однако почти каждое приложение нуждается в сохранении состояния между запросами, например, содержимого корзины покупок. Именно для этого служат сессии. Мы покажем, +HTTP - протокол без состояния, однако почти каждому приложению нужно сохранять состояние между запросами, например содержимое корзины покупок. Именно для этого служат сессии. Мы покажем: - как использовать сессии -- как избежать конфликтов имен -- как установить срок действия +- как предотвратить конфликты имён +- как задать срок действия </div> -При использовании сессий каждый пользователь получает уникальный идентификатор, называемый ID сессии, который передается в cookie. Он служит ключом к данным сессии. В отличие от cookie, которые хранятся на стороне браузера, данные в сессии хранятся на стороне сервера. +При использовании сессий каждый пользователь получает уникальный идентификатор, называемый session ID, который передаётся в cookie. Он служит ключом к данным сессии. В отличие от cookie, которые хранятся на стороне браузера, данные сессии хранятся на стороне сервера. -Сессию мы настраиваем в [конфигурации |configuration#Сессия], особенно важен выбор срока действия (expiration). +Сессию мы настраиваем в [конфигурации |configuration#Сессия]; особенно важен выбор времени истечения. -Управление сессией осуществляет объект [api:Nette\Http\Session], к которому вы можете получить доступ, запросив его с помощью [внедрения зависимостей |dependency-injection:passing-dependencies]. В презентерах достаточно просто вызвать `$session = $this->getSession()`. +Работой с сессией занимается объект [api:Nette\Http\Session], который вы получите, попросив передать его через [внедрение зависимостей |dependency-injection:passing-dependencies]. В презентерах достаточно вызвать `$session = $this->getSession()`. → [Установка и требования |@home#Установка] @@ -23,21 +23,21 @@ HTTP — это протокол без состояния, однако поч Запуск сессии ============= -Nette по умолчанию автоматически запускает сессию в тот момент, когда мы начинаем читать из нее или записывать в нее данные. Вручную сессия запускается с помощью `$session->start()`. +По умолчанию Nette запускает сессию автоматически в момент, когда мы начинаем из неё читать или в неё писать данные. Запустить сессию вручную можно через `$session->start()`. -PHP при запуске сессии отправляет HTTP-заголовки, влияющие на кеширование, см. [php:session_cache_limiter], и, возможно, cookie с ID сессии. Поэтому необходимо всегда запускать сессию еще до отправки любого вывода в браузер, иначе будет выброшено исключение. Если вы знаете, что в процессе рендеринга страницы будет использоваться сессия, запустите ее вручную заранее, например, в презентере. +PHP при запуске сессии отправляет HTTP-заголовки, влияющие на кеширование (см. [php:session_cache_limiter]), и, возможно, cookie с session ID. Поэтому всегда нужно запускать сессию до отправки какого-либо вывода в браузер, иначе будет выброшено исключение. Так что если вы знаете, что при отрисовке страницы будет использоваться сессия, запустите её заранее вручную, например в презентере. -В режиме разработки сессию запускает Tracy, так как он использует ее для отображения полос с перенаправлениями и AJAX-запросами в Tracy Bar. +В режиме разработки сессию запускает Tracy, потому что использует её для отображения полос при перенаправлениях и AJAX-запросах в Tracy Bar. Секции ====== -В чистом PHP хранилище данных сессии реализовано как массив, доступный через глобальную переменную `$_SESSION`. Проблема в том, что приложения обычно состоят из целого ряда взаимно независимых частей, и если все они имеют доступ только к одному массиву, рано или поздно произойдет конфликт имен. +В чистом PHP хранилище данных сессии реализовано как массив, доступный через глобальную переменную `$_SESSION`. Проблема в том, что приложения обычно состоят из множества независимых частей, и если всем им доступен только один массив, рано или поздно возникнет конфликт имён. -Nette Framework решает эту проблему, разделяя все пространство на секции (объекты [api:Nette\Http\SessionSection |api:Nette\Http\SessionSection]). Каждая единица затем использует свою секцию с уникальным именем, и никакой коллизии уже произойти не может. +Nette Framework решает эту проблему разделением всего пространства на секции (объекты [api:Nette\Http\SessionSection]). Каждая часть затем использует собственную секцию с уникальным именем, и никакого конфликта возникнуть не может. -Секцию получаем из сессии: +Секцию мы получаем из сессии: ```php $section = $session->getSection('уникальное имя'); @@ -46,26 +46,26 @@ $section = $session->getSection('уникальное имя'); В презентере достаточно использовать `getSession()` с параметром: ```php -// $this - это Presenter +// $this - презентер $section = $this->getSession('уникальное имя'); ``` -Проверить существование секции можно методом `$session->hasSection('уникальное имя')`. +Существование секции можно проверить методом `$session->hasSection('уникальное имя')`. Список имён всех существующих секций возвращает `$session->getSectionNames()`. -С самой секцией затем работать очень легко с помощью методов `set()`, `get()` и `remove()`: +Работать с самой секцией затем очень легко с помощью методов `set()`, `get()` и `remove()`: ```php // запись переменной -$section->set('userName', 'franta'); +$section->set('userName', 'john'); -// чтение переменной, вернет null, если не существует +// чтение переменной, возвращает null, если её нет echo $section->get('userName'); // удаление переменной $section->remove('userName'); ``` -Для получения всех переменных из секции можно использовать цикл `foreach`: +Чтобы получить все переменные из секции, можно использовать цикл `foreach`: ```php foreach ($section as $key => $val) { @@ -74,46 +74,46 @@ foreach ($section as $key => $val) { ``` -Установка срока действия +Как задать срок действия ------------------------ -Для отдельных секций или даже отдельных переменных можно установить срок действия. Мы можем, например, установить истечение срока действия входа пользователя через 20 минут, но при этом продолжать помнить содержимое корзины. +Срок действия можно задать отдельным секциям и даже отдельным переменным. Мы можем дать входу пользователя истечь через 20 минут, но при этом продолжать помнить содержимое корзины покупок. ```php -// секция истечет через 20 минут +// срок действия секции истекает через 20 минут $section->setExpiration('20 minutes'); ``` -Для установки срока действия отдельных переменных служит третий параметр метода `set()`: +Для задания срока действия отдельным переменным служит третий параметр метода `set()`: ```php -// переменная 'flash' истечет уже через 30 секунд +// срок действия переменной 'flash' истекает через 30 секунд $section->set('flash', $message, '30 seconds'); ``` .[note] -Не забывайте, что срок экспирации всей сессии (см. [конфигурацию сессии |configuration#Сессия]) должен быть равен или больше срока, установленного для отдельных секций или переменных. +Помните, что время истечения всей сессии (см. [конфигурацию сессии |configuration#Сессия]) должно быть равно времени, заданному отдельным секциям или переменным, или больше него. -Отмену ранее установленного срока действия обеспечивает метод `removeExpiration()`. Немедленное удаление всей секции обеспечивает метод `remove()`. +Для отмены ранее заданного срока действия служит метод `removeExpiration()`; чтобы сбросить срок действия конкретной переменной, передайте её имя: `removeExpiration('flash')`. Чтобы немедленно удалить всю секцию, используйте метод `remove()`. События $onStart, $onBeforeWrite -------------------------------- -Объект `Nette\Http\Session` имеет [события |nette:glossary#События Events] `$onStart` и `$onBeforeWrite`, поэтому вы можете добавить колбэки, которые будут вызваны после запуска сессии или перед ее записью на диск и последующим завершением. +У объекта `Nette\Http\Session` есть [события |nette:glossary#События] `$onStart` и `$onBeforeWrite`, так что вы можете добавить callback'и, которые вызываются после запуска сессии или перед её записью на диск и последующим завершением. ```php $session->onBeforeWrite[] = function () { - // запишем данные в сессию + // записываем данные в сессию $this->section->set('basket', $this->basket); }; ``` -Управление сессией -================== +Работа с сессией +================ -Обзор методов класса `Nette\Http\Session` для управления сессией: +Обзор методов класса `Nette\Http\Session` для работы с сессией: <div class=wiki-methods-brief> @@ -130,7 +130,7 @@ isStarted(): bool .[method] close(): void .[method] ----------------------- -Завершает сессию. Сессия автоматически завершается в конце выполнения скрипта. +Завершает сессию. Сессия завершается автоматически по окончании выполнения скрипта. destroy(): void .[method] @@ -140,17 +140,17 @@ destroy(): void .[method] exists(): bool .[method] ------------------------ -Содержит ли HTTP-запрос cookie с ID сессии? +Содержит ли HTTP-запрос cookie с session ID? regenerateId(): void .[method] ------------------------------ -Генерирует новый случайный ID сессии. Данные остаются сохраненными. +Порождает новый случайный session ID. Данные сохраняются. getId(): string .[method] ------------------------- -Возвращает ID сессии. +Возвращает session ID. </div> @@ -158,44 +158,44 @@ getId(): string .[method] Конфигурация ------------ -Сессию настраиваем в [конфигурации |configuration#Сессия]. Если вы пишете приложение, которое не использует DI-контейнер, для конфигурации служат следующие методы. Они должны быть вызваны еще до запуска сессии. +Сессию мы настраиваем в [конфигурации |configuration#Сессия]. Если вы пишете приложение, которое не использует DI-контейнер, для настройки служат эти методы. Вызывать их нужно до запуска сессии. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Устанавливает имя cookie, в котором передается ID сессии. Стандартное имя — `PHPSESSID`. Пригодится в случае, когда в рамках одного веб-сайта вы запускаете несколько различных приложений. +Задаёт имя cookie, в которой передаётся session ID. Стандартное имя - `PHPSESSID`. Это полезно, если на одном сайте вы запускаете несколько разных приложений. getName(): string .[method] --------------------------- -Возвращает имя cookie, в котором передается ID сессии. +Возвращает имя cookie, в которой передаётся session ID. setOptions(array $options): static .[method] -------------------------------------------- -Конфигурирует сессию. Можно устанавливать все PHP [директивы сессии |https://www.php.net/manual/en/session.configuration.php] (в формате camelCase, например, вместо `session.save_path` запишем `savePath`) и также [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. +Настраивает сессию. Можно задать все [директивы сессии |https://www.php.net/manual/en/session.configuration.php] PHP (в формате camelCase, то есть вместо `session.save_path` пишите `savePath`), а также [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Устанавливает время неактивности, после которого сессия истечет. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Задаёт время бездействия, после которого сессия истекает. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Настройка параметров для cookie. Значения по умолчанию параметров вы можете изменить в [конфигурации |configuration#Session cookie]. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +Задаёт параметры cookie. Значения параметров по умолчанию можно изменить в [конфигурации |configuration#Cookie сессии]. setSavePath(string $path): static .[method] ------------------------------------------- -Устанавливает каталог, куда сохраняются файлы сессий. +Задаёт каталог, в котором хранятся файлы сессии. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Установка собственного обработчика, см. [документацию PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Задаёт собственный обработчик, см. [документацию PHP |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> @@ -203,9 +203,9 @@ setHandler(\SessionHandlerInterface $handler): static .[method] Безопасность прежде всего ========================= -Сервер предполагает, что он постоянно взаимодействует с одним и тем же пользователем, пока запросы сопровождаются одним и тем же ID сессии. Задачей механизмов безопасности является обеспечение того, чтобы это действительно было так, и чтобы идентификатор нельзя было украсть или подменить. +Сервер исходит из того, что общается с одним и тем же пользователем, пока запросы сопровождаются одним и тем же session ID. Задача механизмов безопасности - обеспечить, чтобы это действительно было так и чтобы идентификатор нельзя было украсть или подменить. -Поэтому Nette Framework правильно конфигурирует PHP-директивы, чтобы ID сессии передавался только в cookie, делал его недоступным для JavaScript и игнорировал возможные идентификаторы в URL. Кроме того, в критические моменты, такие как вход пользователя, он генерирует новый ID сессии. +Поэтому Nette Framework правильно настраивает директивы PHP так, чтобы session ID передавался только в cookie, был недоступен для JavaScript и чтобы любые идентификаторы в URL игнорировались. Более того, в критические моменты, например при входе пользователя, он порождает новый session ID. .[note] -Для конфигурации PHP используется функция ini_set, которую, к сожалению, некоторые хостинги запрещают. Если это ваш случай, попробуйте договориться с хостером, чтобы он разрешил вам эту функцию или хотя бы настроил сервер. +Для настройки PHP используется функция `ini_set`, но, к сожалению, некоторые хостинги её использование запрещают. Если это случай вашего хостинга, попробуйте договориться, чтобы вам эту функцию разрешили или хотя бы правильно настроили сервер. diff --git a/http/ru/ssrf.texy b/http/ru/ssrf.texy new file mode 100644 index 0000000000..ede10196aa --- /dev/null +++ b/http/ru/ssrf.texy @@ -0,0 +1,183 @@ +Защита от SSRF +************** + +.[perex] +Когда ваше приложение скачивает URL, заданный пользователем, злоумышленник может злоупотребить этим, чтобы добраться до вашей внутренней сети. Классы [#UrlValidator] и [#IPAddress] помогают защититься от таких атак Server-Side Request Forgery (SSRF). + +→ [Установка и требования |@home#Установка] + + +Что такое SSRF? +=============== + +Представьте себе возможность, при которой пользователь вводит URL, а ваш сервер его скачивает: аватар с удалённого адреса, цель вебхука, предпросмотр ссылки. Выглядит безобидно, но по адресу идёт сервер, а не браузер пользователя. А сервер видит места, которых злоумышленник не видит: интерфейс loopback, частную сеть, облачные сервисы. + +Поэтому злоумышленник отправляет URL, который указывает внутрь, а не в публичный интернет. Типичные цели: + +- метаданные облака по адресу `http://169.254.169.254/`, откуда могут утечь ключи доступа +- внутренние административные панели и маршрутизаторы вроде `http://192.168.1.1/` +- сервисы без аутентификации, например Redis по адресу `http://localhost:6379/` + +Этот класс уязвимостей настолько распространён, что входит в [OWASP Top 10 |https://owasp.org/Top10/]. Защита состоит в том, чтобы проверить URL **до** того, как вы его скачаете, и отклонить всё, что разрешается в непубличный адрес. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator] проверяет URL по настраиваемой политике: схему, порт, хост, userinfo и IP-адреса, в которые хост разрешается. Базовое использование - один вызов: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // небезопасный URL, не скачивайте его +} +``` + +Политика по умолчанию намеренно строгая: она принимает только `https` на порту 443, указывающий на публичный IP-адрес. Всё остальное (loopback, частные диапазоны, link-local, включая метаданные облака, зарезервированные диапазоны) отклоняется, а multicast отклоняется безусловно. Это правильная отправная точка для скачивания произвольных URL, заданных пользователем. + + +Настройка политики +------------------ + +Политику вы задаёте через конструктор. Например, чтобы разрешить обычный `http` на любом порту и доступ к частным адресам (что удобно внутри доверенной сети): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // любой порт + allowPrivateIps: true, +); +``` + +Частый приём - ограничить скачивание фиксированным набором партнёрских доменов с помощью списка разрешённых хостов. Приставка `*.` соответствует любой глубине поддоменов, но не самому домену; при необходимости перечислите обе формы: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +Полный набор параметров конструктора: + +| Параметр | По умолчанию | Значение +|--------------------- +| `schemes` | `['https']` | разрешённые схемы; `[]` отклоняет всё +| `ports` | `[443]` | разрешённые порты, `null` = любой; неявный порт из схемы учитывается +| `allowPrivateIps` | `false` | разрешить частные диапазоны (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | разрешить loopback (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | разрешить link-local, включая метаданные облака 169.254.169.254 +| `allowReserved` | `false` | разрешить диапазоны, зарезервированные IANA +| `allowUserinfo` | `false` | разрешить `user:pass@` в URL +| `hostAllowlist` | `null` | если задан, хост должен соответствовать одному из образцов; `[]` отклоняет все +| `hostBlocklist` | `null` | если задан, хост не должен соответствовать ни одному образцу + + +Методы проверки +--------------- + +Валидатор предлагает три метода. `allows()` выполняет полную проверку, включая разрешение DNS: хост разрешается, и **каждый** адрес A/AAAA должен пройти политику IP: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()` пропускает разрешение DNS и проверки диапазонов IP. Используйте его как быстрый предварительный фильтр или когда проверка DNS передана слою скачивания: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Оба метода принимают строку, объект [UrlImmutable |urls#UrlImmutable] или `null` (который всегда не проходит). + + +Борьба с DNS rebinding +---------------------- + +Между проверкой и скачиванием есть тонкая гонка: злоумышленник может вернуть безопасный IP, когда вы проверяете хост, а затем переключить DNS на внутренний IP для самого скачивания. Чтобы закрыть эту дыру, `getResolvedIPs()` возвращает проверенные IP-адреса, а вы привязываете к ним соединение, чтобы скачивание нельзя было перенаправить в другое место: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // небезопасный URL +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... выполняем запрос +``` + +Метод возвращает массив строк с IP (сначала записи A, затем AAAA), прошедших полную политику, либо пустой массив при любой неудаче. Для IP-литерала в URL он проверяет адрес напрямую и запрос к DNS не выполняет. + + +IPAddress +========= + +[api:Nette\Http\IPAddress] - неизменяемый объект-значение для работы с адресами IPv4 и IPv6. `UrlValidator` использует его внутри, но он удобен и сам по себе, всегда когда вы классифицируете адреса. Конструктор выбрасывает `Nette\InvalidArgumentException` для некорректного адреса: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +Когда исключение вам не нужно, используйте фабрику `tryFrom()` или проверку `isValid()`: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Классификация адресов +--------------------- + +Предикаты говорят, к какому классу относится адрес. Ключевой из них - `isPublic()`: он истинен только для публично маршрутизируемых адресов, а именно этого и хочет защита от SSRF: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (диапазон метаданных облака) +``` + +Полный набор предикатов: + +| Метод | Проверяет +|-------------------- +| `isPublic()` | публично маршрутизируемый (ни один из перечисленных ниже) +| `isPrivate()` | частные диапазоны RFC 1918 / 4193 +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (включая метаданные облака), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | зарезервированные IANA (документация, CGNAT, будущее использование, …) + + +Принадлежность к диапазону +-------------------------- + +`isInRange()` проверяет, попадает ли адрес в блок CIDR. Можно передать сеть с префиксом либо голый адрес для точного совпадения (неявно /32 для IPv4 и /128 для IPv6): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (точное совпадение) +``` + +Некорректный ввод или другое семейство IP возвращает `false`. + + +IPv6, отображающий IPv4 +----------------------- + +Адреса, записанные как IPv4-mapped IPv6 (например, `::ffff:127.0.0.1`), - классический способ проскользнуть мимо наивных фильтров. `IPAddress` их нормализует, так что предикаты диапазонов видят сквозь маскировку: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +Методы `isIPv4()` и `isIPv6()` сообщают о текстовой форме: отображённый адрес - это IPv6, а не IPv4. diff --git a/http/ru/upgrading.texy b/http/ru/upgrading.texy new file mode 100644 index 0000000000..45564a6f37 --- /dev/null +++ b/http/ru/upgrading.texy @@ -0,0 +1,54 @@ +Обновление +********** + + +Обновление до версии 3.4 +======================== + +Минимально требуемая версия PHP - 8.3. + +- метод `Request::isSameSite()` объявлен устаревшим в пользу `isFrom()`, который определяет источник запроса по заголовкам `Sec-Fetch-*`. Автоматическая защита форм и сигналов становится точнее, и одно поведение меняется: прямой переход (закладка, вручную введённый адрес, ссылка в письме) больше не считается same-site. Если сигнал полагается на ссылки-действия в письмах, пометьте его атрибутом `#[Requires(sameOrigin: false)]`. +- cookie `_nss` теперь отправляется только браузерам, которые не отправляют заголовок `Sec-Fetch-Site` +- `setCookie()` отправляет атрибут `Max-Age` и принудительно ставит флаг `Secure` для `SameSite=None` и для секционированных cookie +- перечисление `SameSite` заменяет константы `IResponse::SameSiteLax` и им подобные, которые объявлены устаревшими +- срок действия везде истолковывается одинаково: число - это относительное количество секунд, строка - интервал или дата. Передача абсолютной временной метки UNIX объявлена устаревшей, а сессионную cookie обозначает `null` вместо `0`. +- устаревший метод `Request::getRemoteHost()` возвращает `null` +- давно устаревший класс `Nette\Http\UserStorage` был удалён + +Вся история перехода на заголовки `Sec-Fetch-*` рассказана в статье [Четверть века CSRF |https://blog.nette.org/en/quarter-century-of-csrf]. + + +Обновление до версии 3.2 +======================== + +- учётные данные из HTTP Basic Authentication больше не входят в объект `Url`, поэтому `$url->getUser()` и `$url->getPassword()` возвращают пустую строку. Считывайте их новым методом `$request->getBasicCredentials()`. + +Причины этого изменения объясняются в статье [Nette Http 3.2: изменение доступа к учётным данным |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials]. + + +Обновление до версии 3.1 +======================== + +- cookie отправляются с флагом `sameSite: Lax` +- `cookieSecure` теперь по умолчанию равен 'auto' +- параметр `session.cookieSecure` объявлен устаревшим, вместо него используется `http.cookieSecure` +- cookie `nette-samesite` переименована в `_nss` +- `Nette\Http\Request::getFile()` принимает массив ключей и возвращает `FileUpload|null` +- `Nette\Http\Session::getCookieParameters()` объявлен устаревшим +- `Nette\Http\FileUpload::getName()` переименован в `getUntrustedName()` +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()` и `getRelativeUrl()` объявлены устаревшими (эти методы входят в `UrlScript`) +- `Nette\Http\Response::$cookieHttpOnly` объявлен устаревшим +- `Nette\Http\FileUpload::getImageSize()` возвращает пару `[width, height]` +- при `autoStart: smart` (по умолчанию) сессия больше не запускается сразу после старта приложения только потому, что браузер отправил сессионную cookie; она запускается при первом чтении или записи. Добавлены значения `always` и `never`. +- когда браузер отправляет ID сессии, для которого сессии не существует, Nette удаляет cookie вместо создания новой сессии +- для доступа к секциям сессии предпочитайте методы `set()`, `get()` и `remove()`; в отличие от обращения через свойства они правильно различают чтение и запись и не запускают сессию без надобности +- значения по умолчанию для `cookiePath` и `cookieDomain` можно задать в конфигурации + +Поведение сессий подробно описано в статье [Nette Http 3.1: намного умнее сессии |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions]. + + +Обновление до версии 3.0 +======================== + +- объект `Nette\Http\UrlScript` (который возвращает, например, `Nette\Http\Request::getUrl()`) теперь неизменяем +- в `new Nette\Http\Url('abcd')` строка `abcd` представляет путь, а не домен; начиная с 3.0 `(new Nette\Http\Url('abcd'))->setScheme('http')` правильно порождает `http:abcd` вместо прежнего `http://abcd` diff --git a/http/ru/urls.texy b/http/ru/urls.texy index 57caebed27..7abfeaaebc 100644 --- a/http/ru/urls.texy +++ b/http/ru/urls.texy @@ -2,7 +2,7 @@ ************ .[perex] -Классы [#Url], [#UrlImmutable] и [#UrlScript] позволяют легко генерировать, парсить и манипулировать URL. +Классы [#Url], [#UrlImmutable] и [#UrlScript] облегчают порождение, разбор и изменение URL-адресов. → [Установка и требования |@home#Установка] @@ -10,10 +10,10 @@ Url === -Класс [api:Nette\Http\Url |api:Nette\Http\Url] позволяет легко работать с URL и его отдельными компонентами, которые отражены на этой схеме: +Класс [api:Nette\Http\Url] позволяет легко работать с URL и его отдельными составными частями, как показано на этой схеме: /--pre - схема пользователь пароль хост порт путь запрос фрагмент + scheme user password host port path query fragment | | | | | | | | /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> @@ -22,7 +22,7 @@ Url hostUrl authority \-- -Генерация URL интуитивно понятна: +Порождение URL интуитивно понятно: ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -Также можно распарсить URL и далее манипулировать им: +URL можно и разобрать, а затем изменять: ```php $url = new Url( @@ -52,10 +52,10 @@ echo json_encode([$url]); ``` -Компоненты URL .[method] ------------------------- +Составные части URL +------------------- -Для возврата или изменения отдельных компонентов URL вам доступны следующие методы: +Для получения или изменения отдельных составных частей URL доступны следующие методы: .[language-php] | Сеттер | Геттер | Возвращаемое значение @@ -73,20 +73,23 @@ echo json_encode([$url]); | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | весь URL -Предупреждение: Когда вы работаете с URL, полученным из [HTTP-запроса|request], имейте в виду, что он не будет содержать фрагмент, так как браузер не отправляет его на сервер. +Методы `getUser()`, `getPassword()`, `setUser()` и `setPassword()` объявлены устаревшими, потому что встраивать учётные данные прямо в URL не рекомендуется. + +Внимание: работая с URL, полученным из [HTTP-запроса |request], помните, что фрагмента в нём не будет, потому что браузер его на сервер не отправляет. -Мы можем работать и с отдельными query-параметрами с помощью: +С отдельными параметрами запроса мы тоже можем работать: .[language-php] | Сеттер | Геттер |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] ------------------------------------------- -Возвращает правую или левую часть хоста. Так это работает, если хост `www.nette.org`: +Возвращает правую или левую часть хоста. Вот как это работает, если хост - `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -98,8 +101,8 @@ getDomain(int $level = 2): string .[method] | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Проверяет, идентичны ли два URL. ```php @@ -107,6 +110,11 @@ $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Приводит URL к канонической форме. При этом имя хоста переводится в нижний регистр, а путь нормализуется (процентное кодирование и удаление лишних символов). Строка запроса остаётся без изменений. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- Проверяет, является ли URL абсолютным. URL считается абсолютным, если он начинается со схемы (например, http, https, ftp), за которой следует двоеточие. @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -Нормализует путь в URL, удаляя специальные сегменты `.` и `..`. Метод удаляет избыточные элементы пути тем же способом, как это делают веб-браузеры. +Нормализует путь URL, убирая особые сегменты `.` и `..`. Этот метод убирает лишние элементы пути так же, как это делают веб-браузеры. ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,21 +139,21 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -Класс [api:Nette\Http\UrlImmutable |api:Nette\Http\UrlImmutable] является immutable (неизменяемой) альтернативой классу [#Url] (подобно тому, как в PHP `DateTimeImmutable` является неизменяемой альтернативой `DateTime`). Вместо сеттеров у него есть так называемые withers, которые не изменяют объект, а возвращают новые экземпляры с измененным значением: +Класс [api:Nette\Http\UrlImmutable] - неизменяемая альтернатива классу [#Url] (подобно тому, как `DateTimeImmutable` в PHP - неизменяемая альтернатива `DateTime`). Вместо сеттеров у него есть "виттеры", которые объект не меняют, а возвращают новые экземпляры с изменённым значением: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` Класс `UrlImmutable` реализует интерфейс `JsonSerializable` и имеет метод `__toString()`, так что объект можно вывести или использовать в данных, передаваемых в `json_encode()`. @@ -156,13 +164,13 @@ echo json_encode([$url]); ``` -Компоненты URL .[method] ------------------------- +Составные части URL +------------------- -Для возврата или изменения отдельных компонентов URL служат методы: +Для получения или изменения отдельных составных частей URL доступны следующие методы: .[language-php] -| Wither | Геттер | Возвращаемое значение +| Виттер | Геттер | Возвращаемое значение |-------------------------------------------------------------------------------------------- | `withScheme(string $scheme)` | `getScheme(): string` | `'http'` | `withUser(string $user)` | `getUser(): string` | `'john'` @@ -177,12 +185,12 @@ echo json_encode([$url]); | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` | | `getAbsoluteUrl(): string` | весь URL -Метод `withoutUserInfo()` удаляет `user` и `password`. +Методы `getUser()`, `getPassword()`, `withUser()`, `withPassword()` и `withoutUserInfo()` объявлены устаревшими, потому что встраивать учётные данные прямо в URL не рекомендуется. -Мы можем работать и с отдельными query-параметрами с помощью: +С отдельными параметрами запроса мы тоже можем работать: .[language-php] -| Wither | Геттер +| Виттер | Геттер |----------------------------------------------- | `withQuery(string\|array $query)` | `getQueryParameters(): array` | `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` @@ -190,7 +198,7 @@ echo json_encode([$url]); getDomain(int $level = 2): string .[method] ------------------------------------------- -Возвращает правую или левую часть хоста. Так это работает, если хост `www.nette.org`: +Возвращает правую или левую часть хоста. Вот как это работает, если хост - `www.nette.org`: .[language-php] | `getDomain(1)` | `'org'` @@ -204,11 +212,11 @@ getDomain(int $level = 2): string .[method] resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -Выводит абсолютный URL тем же способом, каким браузер обрабатывает ссылки на HTML-странице: -- если ссылка является абсолютным URL (содержит схему), она используется без изменений -- если ссылка начинается с `//`, берется только схема из текущего URL -- если ссылка начинается с `/`, создается абсолютный путь от корня домена -- в остальных случаях URL составляется относительно текущего пути +Разрешает абсолютный URL так же, как браузер обрабатывает ссылки на HTML-странице: +- если ссылка - абсолютный URL (содержит схему), она используется без изменений +- если ссылка начинается с `//`, из текущего URL берётся только схема +- если ссылка начинается с `/`, создаётся абсолютный путь от корня домена +- в остальных случаях URL строится относительно текущего пути ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,8 +226,8 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ +isEqual(string|Url $url): bool .[method] +---------------------------------------- Проверяет, идентичны ли два URL. ```php @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -Класс [api:Nette\Http\UrlScript |api:Nette\Http\UrlScript] является потомком [#UrlImmutable] и расширяет его дополнительными виртуальными компонентами URL, такими как корневой каталог проекта и т.д. Как и родительский класс, это immutable (неизменяемый) объект. +Класс [api:Nette\Http\UrlScript] - потомок [#UrlImmutable], расширяющий его дополнительными виртуальными составными частями URL, например корневым каталогом проекта и т. п. Как и родительский класс, это неизменяемый объект. -Следующая диаграмма отображает компоненты, которые распознает UrlScript: +Следующая схема показывает части, которые различает UrlScript: /--pre baseUrl basePath relativePath relativeUrl @@ -244,14 +252,14 @@ UrlScript scriptPath pathInfo \-- -- `baseUrl` — это базовый URL-адрес приложения, включая домен и часть пути к корневому каталогу приложения -- `basePath` — это часть пути к корневому каталогу приложения -- `scriptPath` — это путь к текущему скрипту -- `relativePath` — это имя скрипта (возможно, с дополнительными сегментами пути) относительно basePath -- `relativeUrl` — это вся часть URL после baseUrl, включая строку запроса и фрагмент. -- `pathInfo` — сегодня уже малоиспользуемая часть URL после имени скрипта +- `baseUrl` - базовый URL приложения, включая домен и часть пути до корневого каталога приложения +- `basePath` - часть пути до корневого каталога приложения +- `scriptPath` - путь к текущему скрипту +- `relativePath` - имя скрипта (и, возможно, дополнительные сегменты пути) относительно `basePath` +- `relativeUrl` - вся часть URL после `baseUrl`, включая строку запроса и фрагмент +- `pathInfo` - ныне редко используемая часть URL после имени скрипта -Для возврата частей URL доступны методы: +Для получения этих частей URL доступны следующие методы: .[language-php] | Геттер | Возвращаемое значение @@ -259,8 +267,8 @@ UrlScript | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -Объекты `UrlScript` обычно не создаются напрямую, но их возвращает метод [Nette\Http\Request::getUrl()|request] с уже правильно настроенными компонентами для текущего HTTP-запроса. +Обычно мы не создаём объекты `UrlScript` напрямую; вместо этого его возвращает метод [Nette\Http\Request::getUrl() |request] уже с правильно заданными для текущего HTTP-запроса частями. diff --git a/http/sl/@home.texy b/http/sl/@home.texy deleted file mode 100644 index aa5ad2508b..0000000000 --- a/http/sl/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Paket `nette/http` zaobjema [HTTP zahtevo|request] & [odgovor|response], delo s [sejami|sessions] ter [razčlenjevanje in sestavljanje URL-jev |urls]. - - -Namestitev ----------- - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/sl/@left-menu.texy b/http/sl/@left-menu.texy deleted file mode 100644 index e024b050e0..0000000000 --- a/http/sl/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Uvod |@home] -- [HTTP zahteva|request] -- [HTTP odgovor|response] -- [Seje |Sessions] -- [Pripomočki za URL |urls] -- [Konfiguracija |configuration] diff --git a/http/sl/@meta.texy b/http/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/http/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/http/sl/configuration.texy b/http/sl/configuration.texy deleted file mode 100644 index 1e57c793c9..0000000000 --- a/http/sl/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Konfiguracija HTTP -****************** - -.[perex] -Pregled konfiguracijskih možnosti za Nette HTTP. - -Če ne uporabljate celotnega ogrodja, ampak samo to knjižnico, preberite, [kako naložiti konfiguracijo|bootstrap:]. - - -Glave HTTP -========== - -```neon -http: - # glave, ki se pošljejo z vsako zahtevo - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # vpliva na glavo X-Frame-Options - frames: ... # (string|bool) privzeto je 'SAMEORIGIN' -``` - -Ogrodje iz varnostnih razlogov pošilja glavo `X-Frame-Options: SAMEORIGIN`, ki pravi, da se stran lahko prikaže znotraj druge strani (v elementu `<iframe>`) samo, če se nahaja na isti domeni. To je lahko v nekaterih situacijah nezaželeno (na primer, če razvijate aplikacijo za Facebook), vedenje lahko zato spremenite z nastavitvijo `frames: http://allowed-host.com` ali `frames: true`. - - -Content Security Policy ------------------------ - -Enostavno je mogoče sestaviti glave `Content-Security-Policy` (v nadaljevanju CSP), njihov opis najdete v [opisu CSP |https://content-security-policy.com]. CSP direktive (kot npr. `script-src`) so lahko zapisane bodisi kot nizi po specifikaciji ali kot polja vrednosti zaradi boljše čitljivosti. Potem ni treba okoli ključnih besed, kot na primer `'self'`, pisati narekovajev. Nette tudi samodejno generira vrednost `nonce`, tako da bo v glavi na primer `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # niz v obliki po specifikaciji CSP - default-src: "'self' https://example.com" - - # polje vrednosti - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool v primeru stikal - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -V predlogah uporabljajte `<script n:nonce>...</script>` in vrednost nonce se dopolni samodejno. Delati varne spletne strani v Nette je res enostavno. - -Podobno je mogoče sestaviti tudi glave `Content-Security-Policy-Report-Only` (ki jih je mogoče uporabljati sočasno s CSP) in [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -Piškotki HTTP -------------- - -Lahko spremenite privzete vrednosti nekaterih parametrov metode [Nette\Http\Response::setCookie() |response#setCookie] in seje. - -```neon -http: - # doseg piškotka glede na pot - cookiePath: ... # (string) privzeto je '/' - - # domene, ki sprejemajo piškotek - cookieDomain: 'example.com' # (string|domain) privzeto je nenastavljeno - - # pošiljati piškotek samo preko HTTPS? - cookieSecure: ... # (bool|auto) privzeto je auto - - # izklopi pošiljanje piškotka, ki ga uporablja Nette kot zaščito pred CSRF - disableNetteCookie: ... # (bool) privzeto je false -``` - -Atribut `cookieDomain` določa, katere domene lahko sprejemajo piškotek. Če ni naveden, piškotek sprejema ista (pod)domena, kot ga je nastavila, *vendar ne* njenih poddomen. Če je `cookieDomain` določen, so vključene tudi poddomene. Zato je navedba `cookieDomain` manj omejujoča kot izpustitev. - -Na primer, pri `cookieDomain: nette.org` so piškotki dostopni tudi na vseh poddomenah kot `doc.nette.org`. Istega lahko dosežemo tudi s pomočjo posebne vrednosti `domain`, torej `cookieDomain: domain`. - -Privzeta vrednost `auto` pri atributu `cookieSecure` pomeni, da če spletno mesto teče na HTTPS, se bodo piškotki pošiljali z zastavico `Secure` in bodo torej dostopni samo preko HTTPS. - - -HTTP proxy ----------- - -Če spletno mesto teče za HTTP proxyjem, vnesite njegov IP naslov, da bo pravilno delovalo zaznavanje povezave preko HTTPS in tudi IP naslova odjemalca. Torej, da bosta funkciji [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] in [isSecured() |request#isSecured] vračali pravilne vrednosti in se bodo v predlogah generirale povezave s `https:` protokolom. - -```neon -http: - # IP naslov, obseg (npr. 127.0.0.1/8) ali polje teh vrednosti - proxy: 127.0.0.1 # (string|string[]) privzeto je nenastavljeno -``` - - -Seja -==== - -Osnovne nastavitve [sej|sessions]: - -```neon -session: - # prikazati ploščo seje v Tracy Bar? - debugger: ... # (bool) privzeto je false - - # čas neaktivnosti, po katerem seja poteče - expiration: 14 days # (string) privzeto je '3 hours' - - # kdaj naj se zažene seja? - autoStart: ... # (smart|always|never) privzeto je 'smart' - - # handler, storitev, ki implementira vmesnik SessionHandlerInterface - handler: @handlerService -``` - -Možnost `autoStart` nadzoruje, kdaj naj se zažene seja. Vrednost `always` pomeni, da se seja zažene vedno ob zagonu aplikacije. Vrednost `smart` pomeni, da se seja zažene ob zagonu aplikacije samo takrat, ko že obstaja, ali v trenutku, ko želimo iz nje brati ali vanjo pisati. In končno vrednost `never` prepoveduje samodejni zagon seje. - -Nadalje je mogoče nastavljati vse PHP [direktive seje |https://www.php.net/manual/en/session.configuration.php] (v formatu camelCase) in tudi [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Primer: - -```neon -session: - # 'session.name' zapišemo kot 'name' - name: MYID - - # 'session.save_path' zapišemo kot 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Piškotek seje -------------- - -Piškotek seje se pošilja z enakimi parametri kot [drugi piškotki |#Piškotki HTTP], vendar te lahko zanj spremenite: - -```neon -session: - # domene, ki sprejemajo piškotek - cookieDomain: 'example.com' # (string|domain) - - # omejitve pri dostopu iz druge domene - cookieSamesite: None # (Strict|Lax|None) privzeto je Lax -``` - -Atribut `cookieSamesite` vpliva na to, ali bo piškotek poslan pri [dostopu iz druge domene |nette:glossary#SameSite cookie], kar zagotavlja določeno zaščito pred napadi [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Storitve DI -=========== - -Te storitve se dodajo v DI vsebnik: - -| Ime | Tip | Opis -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [zahteva HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [odgovor HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [upravljanje sej| sessions] diff --git a/http/sl/request.texy b/http/sl/request.texy deleted file mode 100644 index 8c991d201f..0000000000 --- a/http/sl/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Zahteva HTTP -************ - -.[perex] -Nette inkapsulira HTTP zahtevo v objekte z razumljivim API-jem in hkrati zagotavlja sanacijski filter. - -HTTP zahtevo predstavlja objekt [api:Nette\Http\Request]. Če delate z Nette, ta objekt samodejno ustvari ogrodje in si ga lahko pustite predati s pomočjo [dependency injection |dependency-injection:passing-dependencies]. V presenterjih je dovolj le poklicati metodo `$this->getHttpRequest()`. Če delate izven Nette Frameworka, si lahko ustvarite objekt s pomočjo [#RequestFactory]. - -Velika prednost Nette je, da pri ustvarjanju objekta samodejno očisti vse vhodne parametre GET, POST, COOKIE in tudi URL kontrolnih znakov in neveljavnih UTF-8 sekvenc. S temi podatki lahko nato varno nadalje delate. Očiščeni podatki se nato uporabljajo v presenterjih in obrazcih. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Nette\Http\Request -================== - -Ta objekt je nespremenljiv (immutable). Nima nobenih setterjev, ima le en t.i. wither `withUrl()`, ki objekta ne spreminja, ampak vrača novo instanco s spremenjeno vrednostjo. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Vrača klon z drugim URL-jem. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Vrača URL zahteve kot objekt [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Opozorilo: brskalniki ne pošiljajo fragmenta na strežnik, zato bo `$url->getFragment()` vračal prazen niz. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Vrača parametre GET zahteve. - -```php -$all = $httpRequest->getQuery(); // vrača polje vseh parametrov iz URL-ja -$id = $httpRequest->getQuery('id'); // vrača GET parameter 'id' (ali null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Vrača parametre POST zahteve. - -```php -$all = $httpRequest->getPost(); // vrača polje vseh parametrov iz POST-a -$id = $httpRequest->getPost('id'); // vrača POST parameter 'id' (ali null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Vrača [naloženo datoteko |#Naložene datoteke] kot objekt [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // je bila kakšna datoteka naložena? - $file->getUntrustedName(); // ime datoteke, ki ga je poslal uporabnik - $file->getSanitizedName(); // ime brez nevarnih znakov -} -``` - -Za dostop do ugnezdene strukture navedite polje ključev. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Ker ni mogoče zaupati podatkom od zunaj in se torej tudi ne zanašati na obliko strukture datotek, je ta način varnejši kot na primer `$request->getFiles()['my-form']['details']['avatar']`, ki lahko odpove. - - -getFiles(): array .[method] ---------------------------- -Vrne drevo [vseh naloženih datotek |#Naložene datoteke] v normalizirani strukturi, katere listi so objekti [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Vrača piškotek ali `null`, če ne obstaja. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Vrača vse piškotke. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Vrača HTTP metodo, s katero je bila narejena zahteva. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Testira HTTP metodo, s katero je bila narejena zahteva. Parameter je neobčutljiv na velikost črk. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Vrača HTTP glavo ali `null`, če ne obstaja. Parameter je neobčutljiv na velikost črk. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Vrača vse HTTP glave kot asociativno polje. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Je povezava šifrirana (HTTPS)? Za pravilno delovanje je morda treba [nastaviti proxy |configuration#HTTP proxy]. - - -isSameSite(): bool .[method] ----------------------------- -Ali zahteva prihaja iz iste (pod)domene in je sprožena s klikom na povezavo? Nette za zaznavanje uporablja piškotek `_nss` (prej `nette-samesite`). - - -isAjax(): bool .[method] ------------------------- -Gre za AJAX zahtevo? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Vrača IP naslov uporabnika. Za pravilno delovanje je morda treba [nastaviti proxy |configuration#HTTP proxy]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Vrača DNS prevod IP naslova uporabnika. Za pravilno delovanje je morda treba [nastaviti proxy |configuration#HTTP proxy]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Vrača podatke za preverjanje pristnosti za [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Vrača telo HTTP zahteve. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Zazna jezik. Kot parameter `$lang` predamo polje z jeziki, ki jih aplikacija podpira, in ona vrne tistega, ki bi ga brskalnik obiskovalca najraje videl. To niso nobene čarovnije, le uporablja se glava `Accept-Language`. Če ne pride do nobenega ujemanja, vrača `null`. - -```php -// brskalnik pošilja npr. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // jeziki, ki jih podpira aplikacija -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Razred [api:Nette\Http\RequestFactory] služi za ustvarjanje instance `Nette\Http\Request`, ki predstavlja trenutno HTTP zahtevo. (Če delate z Nette, objekt HTTP zahteve samodejno ustvari ogrodje.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Metoda `fromGlobals()` ustvari objekt zahteve na podlagi trenutnih globalnih spremenljivk PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` in `$_SERVER`). Pri ustvarjanju objekta samodejno očisti vse vhodne parametre GET, POST, COOKIE in tudi URL kontrolnih znakov in neveljavnih UTF-8 sekvenc, kar zagotavlja varnost pri nadaljnjem delu s temi podatki. - -RequestFactory lahko pred klicem `fromGlobals()` konfigurirate: - -- z metodo `$factory->setBinary()` izklopite samodejno čiščenje vhodnih parametrov kontrolnih znakov in neveljavnih UTF-8 sekvenc. -- z metodo `$factory->setProxy(...)` navedete IP naslov [proxy strežniku |configuration#HTTP proxy], kar je nujno za pravilno zaznavanje IP naslova uporabnika. - -RequestFactory omogoča definiranje filtrov, ki samodejno transformirajo dele URL zahteve. Ti filtri odstranjujejo nezaželene znake iz URL-ja, ki so tja lahko vstavljeni na primer z nepravilno implementacijo sistemov za komentarje na različnih spletnih mestih: - -```php -// odstranitev presledkov iz poti -$requestFactory->urlFilters['path']['%20'] = ''; - -// odstranitev pike, vejice ali desnega oklepaja s konca URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// čiščenje poti od podvojenih poševnic (privzeti filter) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Prvi ključ `'path'` ali `'url'` določa, na kateri del URL-ja se filter uporabi. Drugi ključ je regularni izraz, ki se najde, in vrednost je nadomestilo, ki se uporabi namesto najdenega besedila. - - -Naložene datoteke -================= - -Metoda `Nette\Http\Request::getFiles()` vrača polje vseh naloženih datotek v normalizirani strukturi, katere listi so objekti [api:Nette\Http\FileUpload]. Ti inkapsulirajo podatke, poslane z elementom obrazca `<input type=file>`. - -Struktura odraža poimenovanje elementov v HTML. V najpreprostejšem primeru je to lahko en sam poimenovan element obrazca, poslan kot: - -```latte -<input type="file" name="avatar"> -``` - -V tem primeru `$request->getFiles()` vrača polje: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -Objekt `FileUpload` se ustvari tudi v primeru, da uporabnik ni poslal nobene datoteke ali je pošiljanje spodletelo. Ali je bila datoteka poslana, vrača metoda `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -V primeru imena elementa, ki uporablja notacijo za polja: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -izgleda vrnjeno drevo takole: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Lahko ustvarite tudi polje datotek: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -V takem primeru izgleda struktura takole: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -Dostop do indeksa 1 ugnezdenega polja je najbolje izvesti tako: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Ker ni mogoče zaupati podatkom od zunaj in se torej tudi ne zanašati na obliko strukture datotek, je ta način varnejši kot na primer `$request->getFiles()['my-form']['details']['avatars'][1]`, ki lahko odpove. - - -Pregled metod `FileUpload` .{toc: FileUpload} ---------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Vrača `true`, če je uporabnik naložil kakšno datoteko. - - -isOk(): bool .[method] ----------------------- -Vrača `true`, če je bila datoteka uspešno naložena. - - -getError(): int .[method] -------------------------- -Vrača kodo napake pri nalaganju datoteke. Gre za eno od konstant [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. V primeru, da je nalaganje potekalo v redu, vrača `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Premakne naloženo datoteko na novo lokacijo. Če ciljna datoteka že obstaja, bo prepisana. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Vrača vsebino naložene datoteke. V primeru, da nalaganje ni bilo uspešno, vrača `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Zazna MIME content type naložene datoteke na podlagi njene signature. V primeru, da nalaganje ni bilo uspešno ali zaznavanje ni uspelo, vrača `null`. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Vrača originalno ime datoteke, kot ga je poslal brskalnik. - -.[caution] -Ne zaupajte vrednosti, ki jo vrne ta metoda. Odjemalec je lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - - -getSanitizedName(): string .[method] ------------------------------------- -Vrača sanirano ime datoteke. Vsebuje samo ASCII znake `[a-zA-Z0-9.-]`. Če ime takih znakov ne vsebuje, vrne `'unknown'`. Če je datoteka slika v formatu JPEG, PNG, GIF, WebP ali AVIF, vrne tudi pravilno končnico. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Vrača primerno končnico datoteke (brez pike), ki ustreza zaznanemu MIME tipu. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Vrača originalno pot do datoteke, kot jo je poslal brskalnik pri nalaganju mape. Celotna pot je na voljo samo v PHP 8.1 in višjih. V prejšnjih različicah ta metoda vrača originalno ime datoteke. - -.[caution] -Ne zaupajte vrednosti, ki jo vrne ta metoda. Odjemalec je lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - - -getSize(): int .[method] ------------------------- -Vrača velikost naložene datoteke. V primeru, da nalaganje ni bilo uspešno, vrača `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Vrača pot do začasne lokacije naložene datoteke. V primeru, da nalaganje ni bilo uspešno, vrača `''`. - - -isImage(): bool .[method] -------------------------- -Vrača `true`, če je naložena datoteka slika v formatu JPEG, PNG, GIF, WebP ali AVIF. Zaznavanje poteka na podlagi njene signature in se ne preverja integriteta celotne datoteke. Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganjem |#toImage]. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Vrača par `[širina, višina]` z dimenzijami naložene slike. V primeru, da nalaganje ni bilo uspešno ali ne gre za veljavno sliko, vrača `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Naloži sliko kot objekt [Image|utils:images]. V primeru, da nalaganje ni bilo uspešno ali ne gre za veljavno sliko, vrže izjemo `Nette\Utils\ImageException`. diff --git a/http/sl/response.texy b/http/sl/response.texy deleted file mode 100644 index e93fd0e234..0000000000 --- a/http/sl/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Odgovor HTTP -************ - -.[perex] -Nette inkapsulira HTTP odgovor v objekte z razumljivim API-jem. - -HTTP odgovor predstavlja objekt [api:Nette\Http\Response]. Če delate z Nette, ta objekt samodejno ustvari ogrodje in si ga lahko pustite predati s pomočjo [dependency injection |dependency-injection:passing-dependencies]. V presenterjih je dovolj le poklicati metodo `$this->getHttpResponse()`. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Nette\Http\Response -=================== - -Objekt je za razliko od [Nette\Http\Request|request] spremenljiv (mutable), torej s pomočjo nastavitvenih metod lahko spreminjate stanje, torej npr. pošiljate glave. Ne pozabite, da morajo biti vse nastavitvene metode poklicane **pred pošiljanjem kakršnega koli izpisa.** Ali je bil izpis že poslan, pove metoda `isSent()`. Če vrača `true`, vsak poskus pošiljanja glave sproži izjemo `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Spremeni [statusno kodo odgovora |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Zaradi boljše razumljivosti izvorne kode priporočamo, da za kodo namesto številk uporabljate [preddefinirane konstante |api:Nette\Http\IResponse]. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Vrača statusno kodo odgovora. - - -isSent(): bool .[method] ------------------------- -Vrača, ali so bile glave že poslane s strežnika v brskalnik, in torej ni več mogoče pošiljati glav ali spreminjati statusne kode. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Pošlje HTTP glavo in **prepiše** prej poslano glavo istega imena. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Pošlje HTTP glavo in **ne prepiše** prej poslane glave istega imena. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Izbriše prej poslano HTTP glavo. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Vrača poslano HTTP glavo ali `null`, če takšna ne obstaja. Parameter je neobčutljiv na velikost črk. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Vrača vse poslane HTTP glave kot asociativno polje. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Spremeni glavo `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Preusmeri na drug URL. Ne pozabite nato končati skripta. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Nastavi potek HTTP dokumenta s pomočjo glav `Cache-Control` in `Expires`. Parameter je bodisi časovni interval (kot besedilo) ali `null`, kar onemogoči predpomnjenje. - -```php -// predpomnilnik v brskalniku poteče čez eno uro -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Odgovor bo prenesen s pomočjo pogovornega okna *Shrani kot* pod navedenim imenom. Same datoteke pri tem ne pošilja. - -```php -$httpResponse->sendAsFile('faktura.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Pošlje piškotek. Privzete vrednosti parametrov: - -| `$path` | `'/'` | piškotek ima doseg na vse poti v (pod)domeni *(nastavljivo)* -| `$domain` | `null` | kar pomeni z dosegom na trenutno (pod)domeno, vendar ne njenih poddomen *(nastavljivo)* -| `$secure` | `true` | če spletno mesto teče na HTTPS, sicer `false` *(nastavljivo)* -| `$httpOnly` | `true` | piškotek je za JavaScript nedostopen -| `$sameSite` | `'Lax'` | piškotek ni nujno poslan pri [dostopu iz druge domene |nette:glossary#SameSite cookie] - -Privzete vrednosti parametrov `$path`, `$domain` in `$secure` lahko spremenite v [konfiguraciji |configuration#Piškotki HTTP]. - -Čas lahko navajate kot število sekund ali niz: - -```php -$httpResponse->setCookie('lang', 'cs', '100 days'); -``` - -Parameter `$domain` določa, katere domene lahko sprejemajo piškotek. Če ni naveden, piškotek sprejema ista (pod)domena, kot ga je nastavila, vendar ne njenih poddomen. Če je `$domain` določen, so vključene tudi poddomene. Zato je navedba `$domain` manj omejujoča kot izpustitev. Na primer, pri `$domain = 'nette.org'` so piškotki dostopni tudi na vseh poddomenah kot `doc.nette.org`. - -Za vrednost `$sameSite` lahko uporabite konstante `Response::SameSiteLax`, `SameSiteStrict` in `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Izbriše piškotek. Privzete vrednosti parametrov so: -- `$path` z dosegom na vse imenike (`'/'`) -- `$domain` z dosegom na trenutno (pod)domeno, vendar ne njenih poddomen -- `$secure` se ravna po nastavitvah v [konfiguraciji |configuration#Piškotki HTTP] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/sl/sessions.texy b/http/sl/sessions.texy deleted file mode 100644 index dd1bec2462..0000000000 --- a/http/sl/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Seje -**** - -<div class=perex> - -HTTP je brezstanje protokol, vendar skoraj vsaka aplikacija potrebuje ohranjati stanje med zahtevami, na primer vsebino nakupovalne košarice. Prav temu služijo seje ali relacije. Pokazali si bomo, - -- kako uporabljati seje -- kako preprečiti konflikte imen -- kako nastaviti potek - -</div> - -Pri uporabi sej vsak uporabnik prejme edinstven identifikator, imenovan ID seje, ki se prenaša v piškotku. Ta služi kot ključ do podatkov seje. Za razliko od piškotkov, ki se shranjujejo na strani brskalnika, se podatki v seji shranjujejo na strani strežnika. - -Sejo nastavljamo v [konfiguraciji |configuration#Seja], pomembna je zlasti izbira časa poteka. - -Upravljanje sej ima na skrbi objekt [api:Nette\Http\Session], do katerega pridete tako, da si ga pustite predati s pomočjo [dependency injection |dependency-injection:passing-dependencies]. V presenterjih je dovolj le poklicati `$session = $this->getSession()`. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Zagon seje -========== - -Nette v privzeti nastavitvi samodejno zažene sejo samodejno v trenutku, ko iz nje začnemo brati ali vanjo zapisovati podatke. Ročno se seja zažene s pomočjo `$session->start()`. - -PHP ob zagonu seje pošlje HTTP glave, ki vplivajo na predpomnjenje, glej [php:session_cache_limiter], in po potrebi tudi piškotek z ID-jem seje. Zato je treba vedno sejo zagnati še pred pošiljanjem kakršnega koli izpisa v brskalnik, sicer pride do sprožitve izjeme. Če torej veste, da se bo med izrisovanjem strani uporabljala seja, jo zaženite ročno prej, na primer v presenterju. - -V razvijalskem načinu sejo zažene Tracy, ker jo uporablja za prikazovanje trakov s preusmeritvami in AJAX zahtevami v Tracy Baru. - - -Sekcije -======= - -V čistem PHP je podatkovno skladišče seje realizirano kot polje, dostopno preko globalne spremenljivke `$_SESSION`. Problem je v tem, da se aplikacije običajno sestojijo iz cele vrste medsebojno neodvisnih delov in če imajo vsi na voljo le eno polje, prej ali slej pride do kolizije imen. - -Nette Framework problem rešuje tako, da celoten prostor razdeli na sekcije (objekte [api:Nette\Http\SessionSection]). Vsaka enota nato uporablja svojo sekcijo z edinstvenim imenom in do nobene kolizije več ne more priti. - -Sekcijo dobimo iz seje: - -```php -$section = $session->getSection('unikatno ime'); -``` - -V presenterju je dovolj uporabiti `getSession()` s parametrom: - -```php -// $this je Presenter -$section = $this->getSession('unikatno ime'); -``` - -Preveriti obstoj sekcije je mogoče z metodo `$session->hasSection('unikatno ime')`. - -S samo sekcijo se nato dela zelo enostavno s pomočjo metod `set()`, `get()` in `remove()`: - -```php -// zapis spremenljivke -$section->set('userName', 'franta'); - -// branje spremenljivke, vrne null če ne obstaja -echo $section->get('userName'); - -// preklic spremenljivke -$section->remove('userName'); -``` - -Za pridobitev vseh spremenljivk iz sekcije je mogoče uporabiti zanko `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Nastavitev poteka ------------------ - -Za posamezne sekcije ali celo posamezne spremenljivke je mogoče nastaviti potek. Lahko tako pustimo poteči prijavo uporabnika čez 20 minut, vendar si pri tem še naprej zapomnimo vsebino košarice. - -```php -// sekcija poteče po 20 minutah -$section->setExpiration('20 minutes'); -``` - -Za nastavitev poteka posameznih spremenljivk služi tretji parameter metode `set()`: - -```php -// spremenljivka 'flash' poteče že po 30 sekundah -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Ne pozabite, da mora biti čas poteka celotne seje (glej [konfiguracija seje |configuration#Seja]) enak ali daljši od časa, nastavljenega pri posameznih sekcijah ali spremenljivkah. - -Preklic prej nastavljenega poteka dosežemo z metodo `removeExpiration()`. Takojšen preklic celotne sekcije zagotovi metoda `remove()`. - - -Dogodka $onStart, $onBeforeWrite --------------------------------- - -Objekt `Nette\Http\Session` ima [dogodke |nette:glossary#Dogodki eventi] `$onStart` in `$onBeforeWrite`, lahko torej dodate povratne klice, ki se sprožijo po zagonu seje ali pred njenim zapisom na disk in posledičnim zaključkom. - -```php -$session->onBeforeWrite[] = function () { - // zapišemo podatke v sejo - $this->section->set('basket', $this->basket); -}; -``` - - -Upravljanje sej -=============== - -Pregled metod razreda `Nette\Http\Session` za upravljanje sej: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Zažene sejo. - - -isStarted(): bool .[method] ---------------------------- -Je seja zagnana? - - -close(): void .[method] ------------------------ -Zaključi sejo. Seja se samodejno zaključi na koncu izvajanja skripta. - - -destroy(): void .[method] -------------------------- -Zaključi in izbriše sejo. - - -exists(): bool .[method] ------------------------- -Ali HTTP zahteva vsebuje piškotek z ID-jem seje? - - -regenerateId(): void .[method] ------------------------------- -Generira nov naključni ID seje. Podatki ostanejo ohranjeni. - - -getId(): string .[method] -------------------------- -Vrne ID seje. - -</div> - - -Konfiguracija -------------- - -Sejo nastavljamo v [konfiguraciji |configuration#Seja]. Če pišete aplikacijo, ki ne uporablja DI vsebnika, služijo za konfiguracijo te metode. Morajo biti poklicane še pred zagonom seje. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Nastavi ime piškotka, v katerem se prenaša ID seje. Standardno ime je `PHPSESSID`. Koristno je v primeru, ko v okviru enega spletnega mesta poganjate več različnih aplikacij. - - -getName(): string .[method] ---------------------------- -Vrača ime piškotka, v katerem se prenaša ID seje. - - -setOptions(array $options): static .[method] --------------------------------------------- -Konfigurira sejo. Lahko nastavljate vse PHP [direktive seje |https://www.php.net/manual/en/session.configuration.php] (v formatu camelCase, npr. namesto `session.save_path` zapišemo `savePath`) in tudi [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Nastavi čas neaktivnosti, po katerem seja poteče. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Nastavitev parametrov za piškotek. Privzete vrednosti parametrov lahko spremenite v [konfiguraciji |configuration#Piškotek seje]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Nastavi imenik, kamor se shranjujejo datoteke s sejo. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Nastavitev lastnega obravnavalnika, glej [dokumentacija PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Varnost na prvem mestu -====================== - -Strežnik predpostavlja, da komunicira vedno z istim uporabnikom, dokler zahteve spremlja isti ID seje. Naloga varnostnih mehanizmov je zagotoviti, da je temu res tako in da ni mogoče identifikatorja ukrasti ali podtakniti. - -Nette Framework zato pravilno konfigurira PHP direktive, da ID seje prenaša samo v piškotku, ga onemogoči JavaScriptu in morebitne identifikatorje v URL-ju ignorira. Poleg tega v kritičnih trenutkih, kot je na primer prijava uporabnika, generira nov ID seje. - -.[note] -Za konfiguracijo PHP se uporablja funkcija ini_set, ki jo na žalost nekateri gostitelji prepovedujejo. Če je to primer tudi vašega gostitelja, se poskusite z njim dogovoriti, da vam funkcijo dovoli ali vsaj strežnik konfigurira. diff --git a/http/sl/urls.texy b/http/sl/urls.texy deleted file mode 100644 index 64f5164a46..0000000000 --- a/http/sl/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Delo z URL-ji -************* - -.[perex] -Razreda [#Url], [#UrlImmutable] in [#UrlScript] omogočata enostavno generiranje, razčlenjevanje in manipulacijo z URL-ji. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Url -=== - -Razred [api:Nette\Http\Url] omogoča enostavno delo z URL-ji in njihovimi posameznimi komponentami, ki jih zajema ta skica: - -/--pre - shema uporabnik geslo gostitelj vrata pot poizvedba fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl avtoriteta -\-- - -Generiranje URL-jev je intuitivno: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Lahko tudi URL razčlenite in ga nadalje manipulirate: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Razred `Url` implementira vmesnik `JsonSerializable` in ima metodo `__toString()`, tako da lahko objekt izpišete ali uporabite v podatkih, predanih v `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Komponente URL .[method] ------------------------- - -Za vračanje ali spreminjanje posameznih komponent URL-ja so vam na voljo te metode: - -.[language-php] -| Setter | Getter | Vrnjena vrednost -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | celoten URL - -Opozorilo: Ko delate z URL-jem, ki je pridobljen iz [zahteve HTTP|request], imejte v mislih, da ne bo vseboval fragmenta, ker ga brskalnik ne pošilja na strežnik. - -Lahko delamo tudi s posameznimi query parametri s pomočjo: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Vrača desni ali levi del gostitelja. Tako deluje, če je gostitelj `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Preveri, ali sta dva URL-ja enaka. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Preverja, ali je URL absoluten. URL se šteje za absoluten, če se začne s shemo (npr. http, https, ftp), ki ji sledi dvopičje. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normalizira pot v URL-ju z odstranitvijo posebnih segmentov `.` in `..`. Metoda odstranjuje odvečne elemente poti na enak način, kot to počnejo spletni brskalniki. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Razred [api:Nette\Http\UrlImmutable] je nespremenljiva (immutable) alternativa razredu [#Url] (podobno kot je v PHP `DateTimeImmutable` nespremenljiva alternativa `DateTime`). Namesto nastavitvenih metod ima t.i. wither metode, ki objekta ne spreminjajo, ampak vračajo nove instance s prilagojeno vrednostjo: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Razred `UrlImmutable` implementira vmesnik `JsonSerializable` in ima metodo `__toString()`, tako da lahko objekt izpišete ali uporabite v podatkih, predanih v `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Komponente URL .[method] ------------------------- - -Za vračanje ali spreminjanje posameznih komponent URL-ja služijo metode: - -.[language-php] -| Wither | Getter | Vrnjena vrednost -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | celoten URL - -Metoda `withoutUserInfo()` odstranjuje `user` in `password`. - -Lahko delamo tudi s posameznimi query parametri s pomočjo: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Vrača desni ali levi del gostitelja. Tako deluje, če je gostitelj `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Izpelje absolutni URL na enak način, kot brskalnik obdeluje povezave na HTML strani: -- če je povezava absolutni URL (vsebuje shemo), se uporabi nespremenjena -- če se povezava začne z `//`, se prevzame samo shema iz trenutnega URL-ja -- če se povezava začne z `/`, se ustvari absolutna pot od korena domene -- v ostalih primerih se URL sestavi relativno glede na trenutno pot - -```php -$url = new UrlImmutable('https://example.com/path/page'); -echo $url->resolve('../foo'); // 'https://example.com/foo' -echo $url->resolve('/bar'); // 'https://example.com/bar' -echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Preveri, ali sta dva URL-ja enaka. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Razred [api:Nette\Http\UrlScript] je potomec [#UrlImmutable] in ga razširja z dodatnimi virtualnimi komponentami URL-ja, kot je korenski imenik projekta ipd. Tako kot starševski razred je nespremenljiv (immutable) objekt. - -Naslednji diagram prikazuje komponente, ki jih UrlScript prepoznava: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` je osnovni URL naslov aplikacije, vključno z domeno in delom poti do korenskega imenika aplikacije -- `basePath` je del poti do korenskega imenika aplikacije -- `scriptPath` je pot do trenutnega skripta -- `relativePath` je ime skripta (po potrebi dodatni segmenti poti) relativno glede na basePath -- `relativeUrl` je celoten del URL-ja za baseUrl, vključno s query stringom in fragmentom. -- `pathInfo` danes že malo uporabljen del URL-ja za imenom skripta - -Za vračanje delov URL-ja so na voljo metode: - -.[language-php] -| Getter | Vrnjena vrednost -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Objektov `UrlScript` običajno ne ustvarjamo neposredno, ampak jih vrača metoda [Nette\Http\Request::getUrl()|request] z že pravilno nastavljenimi komponentami za trenutno HTTP zahtevo. diff --git a/http/tr/@home.texy b/http/tr/@home.texy index 316b44dbb5..73c634afff 100644 --- a/http/tr/@home.texy +++ b/http/tr/@home.texy @@ -2,14 +2,23 @@ Nette HTTP ********** .[perex] -`nette/http` paketi [HTTP isteğini|request] & [yanıtını|response], [oturumlarla|sessions] çalışmayı ve [URL'lerin ayrıştırılmasını ve birleştirilmesini |urls] kapsar. +`nette/http` paketi, tüm HTTP iletişiminde yol arkadaşınızdır. Gelen istek ve giden yanıt üzerinde anlaşılır, nesne yönelimli bir API sunar, oturumlar ile URL adresleriyle çalışmayı kolaylaştırır ve üstüne güvenliği de üstlenir. İçinde bulacaklarınız: + +| [HTTP isteği |request] | gelen istek ve girdinin temizlenmesi +| [HTTP yanıtı |response] | giden yanıt, header'lar ve çerezler +| [Oturumlar|sessions] | istekler arasında güvenli durum sürdürme +| [URL araçları |urls] | URL adreslerini ayrıştırma ve kurma +| [SSRF koruması |ssrf] | Server-Side Request Forgery saldırılarına karşı savunma +| [Yapılandırma|configuration] | paketin yapılandırma seçenekleri Kurulum ------- -Kütüphaneyi [Composer|best-practices:composer] aracını kullanarak indirip kurabilirsiniz: +Paketi [Composer|best-practices:composer] ile indirip kurun: ```shell composer require nette/http ``` + +Paket, PHP 8.3 ile 8.5 arasında bir sürüm gerektirir. diff --git a/http/tr/@left-menu.texy b/http/tr/@left-menu.texy index d575519204..07320a0310 100644 --- a/http/tr/@left-menu.texy +++ b/http/tr/@left-menu.texy @@ -1,8 +1,19 @@ Nette HTTP ********** -- [Giriş |@home] -- [HTTP isteği|request] -- [HTTP yanıtı|response] -- [Oturumlar |Sessions] -- [URL yardımcı programları |urls] -- [Yapılandırma |configuration] +- [Genel bakış |@home] +- [HTTP isteği |request] +- [HTTP yanıtı |response] +- [Oturumlar|sessions] +- [URL araçları |urls] +- [SSRF koruması |ssrf] +- [Yapılandırma|configuration] +- [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/http/tr/configuration.texy b/http/tr/configuration.texy index 7b545523f4..01ecc6592f 100644 --- a/http/tr/configuration.texy +++ b/http/tr/configuration.texy @@ -2,39 +2,41 @@ HTTP Yapılandırması ******************* .[perex] -Nette HTTP için yapılandırma seçeneklerine genel bakış. +Nette HTTP'nin yapılandırma seçeneklerine genel bakış. -Tüm framework'ü değil de yalnızca bu kütüphaneyi kullanıyorsanız, [yapılandırmayı nasıl yükleyeceğiniz |bootstrap:] hakkında bilgi edinin. +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. -HTTP Başlıkları -=============== +HTTP Header'ları +================ ```neon http: - # her istekle gönderilecek başlıklar + # her yanıtla birlikte gönderilen header'lar headers: X-Powered-By: MyCMS X-Content-Type-Options: nosniff X-XSS-Protection: '1; mode=block' - # X-Frame-Options başlığını etkiler - frames: ... # (string|bool) varsayılan 'SAMEORIGIN' + # X-Frame-Options header'ını etkiler + frames: ... # (string|bool|null) varsayılan 'SAMEORIGIN' ``` -Framework, güvenlik nedeniyle, sayfanın başka bir sayfa içinde ( `<iframe>` öğesinde) yalnızca aynı alan adında bulunuyorsa görüntülenebileceğini söyleyen `X-Frame-Options: SAMEORIGIN` başlığını gönderir. Bu bazı durumlarda istenmeyebilir (örneğin, Facebook için bir uygulama geliştiriyorsanız), davranış bu nedenle `frames: http://allowed-host.com` veya `frames: true` ayarlanarak değiştirilebilir. +Nette, güvenlik nedeniyle `X-Frame-Options: SAMEORIGIN` header'ını gönderir; bu, bir sayfanın başka bir sayfanın içinde (bir `<iframe>` elemanında) yalnızca aynı alan adındaysa görüntülenebileceğini belirtir. Bu, belirli durumlarda istenmeyebilir (örneğin bir Facebook uygulaması geliştiriyorsanız); dolayısıyla davranış, belirli bir host'a izin vermek için `frames: http://allowed-host.com`, her yerden çerçevelemeye izin vermek için `frames: true` (header atlanır) ya da tümüyle yasaklamak için `frames: false` (`X-Frame-Options: DENY`) ayarlanarak değiştirilebilir. + +Nette ayrıca varsayılan olarak `X-Powered-By: Nette Framework 3` ve `Content-Type: text/html; charset=utf-8` header'larını gönderir. Bu varsayılanlar dahil herhangi bir header'ı, değerini boş dize yaparak kaldırabilirsiniz. Content Security Policy ----------------------- -`Content-Security-Policy` (bundan sonra CSP) başlıklarını kolayca oluşturabilirsiniz, açıklamaları [CSP açıklaması |https://content-security-policy.com] içinde bulunabilir. CSP yönergeleri (ör. `script-src`) ya belirtimlere göre dizeler olarak ya da daha iyi okunabilirlik için değer dizileri olarak yazılabilir. O zaman `'self'` gibi anahtar kelimelerin etrafına tırnak işareti koymaya gerek yoktur. Nette ayrıca otomatik olarak bir `nonce` değeri oluşturur, böylece başlıkta örneğin `'nonce-y4PopTLM=='` olacaktır. +`Content-Security-Policy` (CSP) header'ları kolayca yapılandırılabilir; açıklamaları [CSP belirtiminde |https://content-security-policy.com] bulunabilir. CSP yönergeleri (örneğin `script-src`) ya belirtime göre dize olarak ya da daha iyi okunurluk için değer dizileri olarak yazılabilir. O zaman `'self'` gibi anahtar sözcüklerin çevresinde tırnak kullanmaya gerek kalmaz. Nette ayrıca bir `nonce` değerini otomatik üretir, dolayısıyla header'da `'nonce-y4PopTLM=='` gibi bir şey gönderilir. ```neon http: # Content Security Policy csp: - # CSP belirtimine göre dize biçimi + # CSP belirtimine göre dize default-src: "'self' https://example.com" # değer dizisi @@ -44,14 +46,14 @@ http: - self - https://example.com - # anahtarlar durumunda bool + # anahtarlarda bool upgrade-insecure-requests: true block-all-mixed-content: false ``` -Şablonlarda `<script n:nonce>...</script>` kullanın ve nonce değeri otomatik olarak eklenecektir. Nette'de güvenli web siteleri yapmak gerçekten kolaydır. +Şablonlarda `<script n:nonce>...</script>` kullanın; nonce değeri otomatik doldurulur. Nette'te güvenli web siteleri yapmak gerçekten kolaydır. -Benzer şekilde, `Content-Security-Policy-Report-Only` (CSP ile eş zamanlı olarak kullanılabilir) ve [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] başlıkları da oluşturulabilir: +Benzer şekilde, `Content-Security-Policy-Report-Only` header'ları (CSP ile eşzamanlı kullanılabilir) ve [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] de yapılandırılabilir: ```neon http: @@ -69,103 +71,116 @@ http: ``` -HTTP çerezi +HTTP Çerezi ----------- -[Nette\Http\Response::setCookie() |response#setCookie] metodunun ve oturumun bazı parametrelerinin varsayılan değerlerini değiştirebilirsiniz. +[Nette\Http\Response::setCookie() |response#setCookie()] metodunun bazı parametrelerinin ve oturum yönetiminin varsayılan değerlerini değiştirebilirsiniz. ```neon http: - # yola göre çerez kapsamı + # çerezin yola göre kapsamı cookiePath: ... # (string) varsayılan '/' - # çerezleri kabul eden alan adları - cookieDomain: 'example.com' # (string|domain) varsayılan ayarlanmamış + # çerezi alabilecek alan adları + cookieDomain: 'example.com' # (string|domain) varsayılan olarak ayarsız - # çerezi yalnızca HTTPS üzerinden gönder? + # çerezler yalnızca HTTPS üzerinden mi gönderilsin? cookieSecure: ... # (bool|auto) varsayılan auto - # Nette tarafından CSRF koruması olarak kullanılan çerezin gönderilmesini devre dışı bırakır + # Nette'in CSRF koruması için kullandığı çerezin gönderilmesini kapatır disableNetteCookie: ... # (bool) varsayılan false ``` -`cookieDomain` niteliği, hangi alan adlarının çerezi kabul edebileceğini belirtir. Belirtilmezse, çerez onu ayarlayan aynı (alt) alan adı tarafından kabul edilir, *ancak* alt alan adları tarafından değil. `cookieDomain` belirtilirse, alt alan adları da dahil edilir. Bu nedenle, `cookieDomain` belirtmek, atlamaktan daha az kısıtlayıcıdır. +`cookieDomain` niteliği, çerezleri hangi alan adlarının (kaynakların) kabul edebileceğini belirler. Belirtilmezse çerez, onu ayarlayan aynı (alt) alan adı tarafından kabul edilir, alt alan adları *hariç*. `cookieDomain` belirtilirse alt alan adları da dahil olur. Bu yüzden `cookieDomain` belirtmek, onu atlamaktan daha az kısıtlayıcıdır. -Örneğin, `cookieDomain: nette.org` ile çerezler `doc.nette.org` gibi tüm alt alan adlarında da kullanılabilir. Aynı şey özel `domain` değeriyle, yani `cookieDomain: domain` ile de elde edilebilir. +Örneğin `cookieDomain: nette.org` ayarlanırsa, çerezler `doc.nette.org` gibi tüm alt alan adlarında da kullanılabilir. Bu, özel `domain` değeriyle, yani `cookieDomain: domain` ile de sağlanabilir. -`cookieSecure` niteliğindeki varsayılan `auto` değeri, web sitesi HTTPS üzerinde çalışıyorsa, çerezlerin `Secure` bayrağıyla gönderileceği ve dolayısıyla yalnızca HTTPS üzerinden erişilebilir olacağı anlamına gelir. +`cookieSecure` niteliğinin varsayılan `auto` değeri, web sitesi HTTPS üzerinde çalışıyorsa çerezlerin `Secure` bayrağıyla gönderileceği ve dolayısıyla yalnızca HTTPS üzerinden kullanılabileceği anlamına gelir. -HTTP proxy +HTTP Proxy ---------- -Web sitesi bir HTTP proxy arkasında çalışıyorsa, HTTPS üzerinden bağlantı algılamasının ve ayrıca istemcinin IP adresinin doğru çalışması için IP adresini belirtin. Yani [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] ve [isSecured() |request#isSecured] fonksiyonlarının doğru değerleri döndürmesi ve şablonlarda `https:` protokolü ile bağlantıların oluşturulması için. +Site bir HTTP proxy'nin arkasında çalışıyorsa, HTTPS bağlantısı saptamasının ve istemcinin IP adresinin doğru çalışması için proxy'nin IP adresini girin. Yani [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress()] ve [isSecured() |request#isSecured()] metotlarının doğru değerleri döndürmesi ve şablonlarda bağlantıların `https:` protokolüyle üretilmesi için. ```neon http: - # IP adresi, aralık (ör. 127.0.0.1/8) veya bu değerlerin dizisi - proxy: 127.0.0.1 # (string|string[]) varsayılan ayarlanmamış + # IP adresi, aralık (örneğin 127.0.0.1/8) ya da bu değerlerden oluşan bir dizi + proxy: 127.0.0.1 # (string|string[]) varsayılan olarak ayarsız ``` -Oturum (Session) -================ +HTTPS'i Zorlama .{data-version:3.3.4} +------------------------------------- + +İsteğin şemasını koşulsuz olarak HTTPS'e zorlar. Bu, TLS'i sonlandıran ama `X-Forwarded-Proto` header'ını iletmeyen bir yük dengeleyici ya da ters proxy arkasında çalışan, yalnızca HTTPS kullanan siteler için yararlıdır; çünkü standart HTTPS saptaması ([#HTTP Proxy] yapılandırılmış olsa bile) bunu yakalayamaz. + +```neon +http: + # tüm isteklerde HTTPS şemasını zorla + forceHttps: true # (bool) varsayılan false +``` + + +Oturum +====== Temel [oturum |sessions] ayarları: ```neon session: - # Tracy Bar'da oturum panelini göster? + # oturum paneli Tracy Bar'da gösterilsin mi? debugger: ... # (bool) varsayılan false - # oturumun sona ereceği etkinlik dışı kalma süresi + # oturumun sona ereceği hareketsizlik süresi expiration: 14 days # (string) varsayılan '3 hours' - # oturum ne zaman başlatılmalı? + # oturum ne zaman başlasın? autoStart: ... # (smart|always|never) varsayılan 'smart' - # handler, SessionHandlerInterface arayüzünü uygulayan servis + # handler, SessionHandlerInterface uygulayan bir servis handler: @handlerService ``` -`autoStart` seçeneği, oturumun ne zaman başlatılacağını kontrol eder. `always` değeri, oturumun her zaman uygulamanın başlatılmasıyla birlikte başlatılacağı anlamına gelir. `smart` değeri, oturumun yalnızca zaten varsa veya ondan okumak veya ona yazmak istediğimiz anda uygulamanın başlangıcında başlatılacağı anlamına gelir. Ve son olarak, `never` değeri oturumun otomatik olarak başlatılmasını yasaklar. +`autoStart` seçeneği, oturumun ne zaman başlaması gerektiğini denetler. `always` değeri, oturumun uygulama her başladığında başlaması demektir. `smart` değeri, oturumun uygulamayla birlikte yalnızca zaten varsa ya da ondan okumak veya ona yazmak istediğimiz anda başlaması demektir. Son olarak `never` değeri, oturumun otomatik başlamasını kapatır. -Ayrıca, tüm PHP [oturum yönergeleri |https://www.php.net/manual/en/session.configuration.php] (camelCase biçiminde) ve ayrıca [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] ayarlanabilir. Örnek: +Ayrıca tüm PHP [oturum yönergelerini |https://www.php.net/manual/en/session.configuration.php] (camelCase biçiminde) ve ayrıca [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] seçeneğini ayarlayabilirsiniz. Örnek: ```neon session: - # 'session.name' 'name' olarak yazılır + # 'session.name', 'name' olarak yazılır name: MYID - # 'session.save_path' 'savePath' olarak yazılır + # 'session.save_path', 'savePath' olarak yazılır savePath: "%tempDir%/sessions" ``` -Oturum çerezi +Oturum Çerezi ------------- -Oturum çerezi [diğer çerezler |#HTTP çerezi] ile aynı parametrelerle gönderilir, ancak bunlar sizin için değiştirilebilir: +Oturum çerezi [diğer çerezlerle |#HTTP Çerezi] aynı parametrelerle gönderilir, ama bunları özellikle onun için değiştirebilirsiniz: ```neon session: - # çerezleri kabul eden alan adları + # çerezi alabilecek alan adları cookieDomain: 'example.com' # (string|domain) - # başka bir alan adından erişimde kısıtlama + # kaynaklar arası erişim kısıtı cookieSamesite: None # (Strict|Lax|None) varsayılan Lax ``` -`cookieSamesite` niteliği, çerezin [başka bir alan adından erişim |nette:glossary#SameSite Çerezi] sırasında gönderilip gönderilmeyeceğini etkiler, bu da [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF) saldırılarına karşı bir miktar koruma sağlar. +`cookieSamesite` niteliği, çerezin [kaynaklar arası isteklerde |nette:glossary#SameSite çerezi] gönderilip gönderilmeyeceğini etkiler; bu da [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF) saldırılarına karşı bir ölçüde koruma sağlar. DI Servisleri ============= -Bu servisler DI konteynerine eklenir: +DI container'a şu servisler eklenir: -| İsim | Tip | Açıklama -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP isteği| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP yanıtı| response] -| `session.session` | [api:Nette\Http\Session] | [oturum yönetimi| sessions] +| Ad | Tür | Açıklama +|-----------------|----------------------------|--------------------------- +| `http.request` | [api:Nette\Http\Request] | [HTTP isteği| request] +| `http.response` | [api:Nette\Http\Response] | [HTTP yanıtı| response] +| `session.session`| [api:Nette\Http\Session] | [oturum yönetimi| sessions] +| `http.requestFactory`| [api:Nette\Http\RequestFactory] | HTTP isteğini oluşturan factory diff --git a/http/tr/request.texy b/http/tr/request.texy index ba5ec9c1bc..af805f3310 100644 --- a/http/tr/request.texy +++ b/http/tr/request.texy @@ -2,11 +2,11 @@ HTTP İsteği *********** .[perex] -Nette, HTTP isteğini anlaşılır bir API'ye sahip nesneler içinde kapsüller ve aynı zamanda bir temizleme filtresi sağlar. +Nette, HTTP isteğini anlaşılır bir API'ye sahip nesnelerin içine alır ve bunu yaparken bir temizleme filtresi de sunar. -HTTP isteği [api:Nette\Http\Request] nesnesi tarafından temsil edilir. Nette ile çalışıyorsanız, bu nesne framework tarafından otomatik olarak oluşturulur ve [bağımlılık enjeksiyonu |dependency-injection:passing-dependencies] aracılığıyla size iletilmesini sağlayabilirsiniz. Presenter'larda sadece `$this->getHttpRequest()` metodunu çağırmanız yeterlidir. Nette Framework dışında çalışıyorsanız, [#RequestFactory] kullanarak bir nesne oluşturabilirsiniz. +HTTP isteği [api:Nette\Http\Request] nesnesiyle temsil edilir. Nette ile çalışıyorsanız bu nesne framework tarafından otomatik oluşturulur ve [bağımlılık enjeksiyonuyla |dependency-injection:passing-dependencies] size aktarılmasını sağlayabilirsiniz. Presenter'larda yalnızca `$this->getHttpRequest()` metodunu çağırın. Nette Framework'ün dışında çalışıyorsanız nesneyi [#RequestFactory] ile oluşturabilirsiniz. -Nette'nin büyük bir avantajı, nesneyi oluştururken tüm GET, POST, COOKIE giriş parametrelerini ve ayrıca URL'yi kontrol karakterlerinden ve geçersiz UTF-8 dizilerinden otomatik olarak temizlemesidir. Daha sonra bu verilerle güvenle çalışabilirsiniz. Temizlenmiş veriler daha sonra presenter'larda ve formlarda kullanılır. +Nette'in büyük bir üstünlüğü, nesneyi oluştururken tüm girdi parametrelerini (GET, POST, COOKIE) ve URL'yi otomatik olarak temizlemesi, denetim karakterlerini ve geçersiz UTF-8 dizilerini kaldırmasıdır. Bu veriyle sonra güvenle çalışabilirsiniz. Temizlenen veri ardından presenter'larda ve formlarda kullanılır. → [Kurulum ve gereksinimler |@home#Kurulum] @@ -14,81 +14,81 @@ Nette'nin büyük bir avantajı, nesneyi oluştururken tüm GET, POST, COOKIE gi Nette\Http\Request ================== -Bu nesne değişmezdir (immutable). Hiçbir ayarlayıcısı yoktur, yalnızca nesneyi değiştirmeyen ancak değiştirilmiş bir değere sahip yeni bir örnek döndüren `withUrl()` adlı bir wither'ı vardır. +Bu nesne değişmezdir. Setter'ı yoktur; yalnızca bir wither'ı, `withUrl()`, vardır; bu metot nesneyi değiştirmez, değiştirilmiş değere sahip yeni bir örnek döndürür. withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ---------------------------------------------------------------- -Farklı bir URL ile bir klon döndürür. +Farklı bir URL'ye sahip bir klon döndürür. getUrl(): Nette\Http\UrlScript .[method] ---------------------------------------- -İsteğin URL'sini [UrlScript |urls#UrlScript] nesnesi olarak döndürür. +İsteğin URL'sini bir [UrlScript |urls#UrlScript] nesnesi olarak döndürür. ```php $url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit +echo $url; // https://nette.org/en/documentation?action=edit echo $url->getHost(); // nette.org ``` -Uyarı: tarayıcılar sunucuya fragment göndermez, bu nedenle `$url->getFragment()` boş bir dize döndürür. +Uyarı: Tarayıcılar fragment'ı sunucuya göndermez, dolayısıyla `$url->getFragment()` boş bir dize döndürür. getQuery(?string $key=null): string|array|null .[method] -------------------------------------------------------- -GET isteğinin parametrelerini döndürür. +GET isteği parametrelerini döndürür. ```php -$all = $httpRequest->getQuery(); // URL'deki tüm parametrelerin dizisini döndürür -$id = $httpRequest->getQuery('id'); // 'id' GET parametresini döndürür (veya null) +$all = $httpRequest->getQuery(); // tüm URL parametrelerinin dizisi +$id = $httpRequest->getQuery('id'); // 'id' GET parametresini döndürür (ya da null) ``` getPost(?string $key=null): string|array|null .[method] ------------------------------------------------------- -POST isteğinin parametrelerini döndürür. +POST isteği parametrelerini döndürür. ```php -$all = $httpRequest->getPost(); // POST'taki tüm parametrelerin dizisini döndürür -$id = $httpRequest->getPost('id'); // 'id' POST parametresini döndürür (veya null) +$all = $httpRequest->getPost(); // tüm POST parametrelerinin dizisi +$id = $httpRequest->getPost('id'); // 'id' POST parametresini döndürür (ya da null) ``` -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -[Yüklemeyi |#Yüklenen Dosyalar] [api:Nette\Http\FileUpload] nesnesi olarak döndürür: +getFile(string|string[] $key): ?Nette\Http\FileUpload .[method] +--------------------------------------------------------------- +Bir [yüklemeyi |#Yüklenen Dosyalar] [api:Nette\Http\FileUpload] nesnesi olarak döndürür: ```php $file = $httpRequest->getFile('avatar'); if ($file?->hasFile()) { // herhangi bir dosya yüklendi mi? - $file->getUntrustedName(); // kullanıcı tarafından gönderilen dosya adı - $file->getSanitizedName(); // tehlikeli karakterler içermeyen ad + $file->getUntrustedName(); // kullanıcının gönderdiği dosya adı + $file->getSanitizedName(); // tehlikeli karakterler olmadan ad } ``` -İç içe geçmiş yapıya erişmek için anahtar dizisi belirtin. +İç içe bir yapıya erişmek için bir anahtar dizisi verin. ```php -//<input type="file" name="my-form[details][avatar]" multiple> +// <input type="file" name="my-form[details][avatar]"> $file = $request->getFile(['my-form', 'details', 'avatar']); ``` -Dışarıdan gelen verilere güvenilemediği ve dolayısıyla dosya yapısının biçimine güvenilemediği için, bu yöntem, örneğin başarısız olabilecek `$request->getFiles()['my-form']['details']['avatar']`'dan daha güvenlidir. +Dış veriye güvenemeyeceğiniz ve dolayısıyla dosyaların yapısına dayanamayacağınız için, bu yaklaşım örneğin başarısız olabilecek `$request->getFiles()['my-form']['details']['avatar']` ifadesinden daha güvenlidir. getFiles(): array .[method] --------------------------- -[Tüm yüklemeler |#Yüklenen Dosyalar] ağacını, yaprakları [api:Nette\Http\FileUpload] nesneleri olan normalleştirilmiş bir yapıda döndürür: +[Tüm yüklemelerin |#Yüklenen Dosyalar] normalleştirilmiş bir yapıdaki ağacını döndürür; yapraklar [api:Nette\Http\FileUpload] nesneleridir: ```php $files = $httpRequest->getFiles(); ``` -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Çerezi veya mevcut değilse `null` döndürür. +getCookie(string $key): ?string .[method] +----------------------------------------- +Bir çerezi ya da yoksa `null` döndürür. ```php $sessId = $httpRequest->getCookie('sess_id'); @@ -106,7 +106,7 @@ $cookies = $httpRequest->getCookies(); getMethod(): string .[method] ----------------------------- -İsteğin yapıldığı HTTP metodunu döndürür. +İstek için kullanılan HTTP metodunu döndürür. ```php $httpRequest->getMethod(); // GET, POST, HEAD, PUT @@ -115,7 +115,7 @@ $httpRequest->getMethod(); // GET, POST, HEAD, PUT isMethod(string $method): bool .[method] ---------------------------------------- -İsteğin yapıldığı HTTP metodunu test eder. Parametre büyük/küçük harfe duyarsızdır. +İstek için kullanılan HTTP metodunu sınar. Parametre büyük/küçük harfe duyarsızdır. ```php if ($httpRequest->isMethod('GET')) // ... @@ -124,31 +124,63 @@ if ($httpRequest->isMethod('GET')) // ... getHeader(string $header): ?string .[method] -------------------------------------------- -HTTP başlığını veya mevcut değilse `null` döndürür. Parametre büyük/küçük harfe duyarsızdır. +Bir HTTP header'ını ya da yoksa `null` döndürür. Parametre büyük/küçük harfe duyarsızdır. ```php $userAgent = $httpRequest->getHeader('User-Agent'); ``` -getHeaders(): array .[method] ------------------------------ -Tüm HTTP başlıklarını ilişkisel bir dizi olarak döndürür. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Tüm HTTP header'larını ilişkisel bir dizi olarak döndürür. Anahtarlar küçük harfe normalleştirilir. ```php $headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; +echo $headers['content-type']; ``` isSecured(): bool .[method] --------------------------- -Bağlantı şifreli mi (HTTPS)? Doğru işlevsellik için [proxy ayarlanması |configuration#HTTP proxy] gerekebilir. +Bağlantı şifreli mi (HTTPS)? Düzgün çalışması [proxy kurulumunu |configuration#HTTP Proxy] gerektirebilir. -isSameSite(): bool .[method] ----------------------------- -İstek aynı (alt) alan adından mı geliyor ve bir bağlantıya tıklanarak mı başlatıldı? Nette algılama için `_nss` (önceden `nette-samesite`) çerezini kullanır. +isSameSite(): bool .[method deprecated] +--------------------------------------- +İstek aynı siteden mi geldi? 3.4 sürümünden beri yerini daha yetenekli [isFrom() |#isFrom()] aldı. + + +isFrom(FetchSite|array $site, FetchDest|array|null $dest=null, ?bool $user=null): bool .[method]{data-version:3.4.0} +-------------------------------------------------------------------------------------------------------------------- +İsteğin nereden geldiğini ve tarayıcının onu nasıl yaptığını, tarayıcının kendisinin ayarladığı ve kurbanın tarayıcısında çalışan bir sayfanın ne taklit edebildiği ne de kaldırabildiği `Sec-Fetch-*` header'larına ([Fetch Metadata |https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header] denir) dayanarak söyler. Nette bunu, formları ve sinyalleri [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery (CSRF)] (CSRF) saldırılarına karşı otomatik korumak için içeride kullanır. Kendi hassas eylemlerinizi, örneğin API uçlarını ya da yıkıcı bağlantıları korumak istediğinizde işe yarar. + +Metot, yalnızca istek verdiğiniz **tüm** koşullara uyduğunda `true` döndürür. İlk parametre `$site`, isteği başlatan sayfa ile sitenizin arasındaki ilişkiyi anlatır (`Sec-Fetch-Site` header'ı). Tek bir değer ya da şu `FetchSite` durumlarından oluşan bir liste kabul eder: + +- `FetchSite::SameOrigin` - tam olarak aynı kaynaktan (şema, host ve port) +- `FetchSite::SameSite` - aynı siteden, belki farklı bir alt alan adından +- `FetchSite::CrossSite` - yabancı bir siteden +- `FetchSite::None` - kullanıcı onu doğrudan başlattı, örneğin URL'yi yazarak ya da bir yer imini açarak + +```php +// istek kendi sayfalarımızdan mı geldi? +if (!$httpRequest->isFrom([FetchSite::SameOrigin, FetchSite::SameSite])) { + // eylemi engelle +} +``` + +İsteğe bağlı `$dest` parametresi (`Sec-Fetch-Dest` header'ı), tarayıcının ne tür bir kaynak getirdiğini söyler; örneğin üst düzey bir gezinme için `FetchDest::Document`, JavaScript'ten yapılan bir istek için `FetchDest::Empty`. İsteğe bağlı `$user` parametresi (`Sec-Fetch-User` header'ı), gezinmenin bir bağlantıya tıklamak ya da form göndermek gibi gerçek bir kullanıcı eylemiyle tetiklenip tetiklenmediğini gösterir; bunu zorunlu kılmak için `true` verin. + +Bir eylemin yalnızca kendi sayfalarınızdan ve yalnızca gerçek bir kullanıcı eylemiyle erişilebilir olduğunun denetimi o zaman şöyle görünür: + +```php +if (!$httpRequest->isFrom(FetchSite::SameOrigin, FetchDest::Document, user: true)) { + $this->error(); +} +``` + +.[note] +Eski tarayıcılar (16.4 öncesi Safari) `Sec-Fetch-*` header'larını göndermez. Onlar için Nette, isteğin yalnızca siteler arası olmadığını kanıtlayan bir `SameSite=Strict` çerezine geri düşer. Ek olarak `$dest` ya da `$user` gerektiren bir denetim bu yolla doğrulanamaz ve o tarayıcılarda `false` döndürür; bu fazla katıysa yalnızca `$site` değerini sınayın. isAjax(): bool .[method] @@ -158,12 +190,12 @@ Bu bir AJAX isteği mi? getRemoteAddress(): ?string .[method] ------------------------------------- -Kullanıcının IP adresini döndürür. Doğru işlevsellik için [proxy ayarlanması |configuration#HTTP proxy] gerekebilir. +Kullanıcının IP adresini döndürür. Düzgün çalışması [proxy kurulumunu |configuration#HTTP Proxy] gerektirebilir. getRemoteHost(): ?string .[method deprecated] --------------------------------------------- -Kullanıcının IP adresinin DNS çözümlemesini döndürür. Doğru işlevsellik için [proxy ayarlanması |configuration#HTTP proxy] gerekebilir. +Kullanımdan kaldırıldı, her zaman `null` döndürür. Ters DNS sorguları yavaş ve güvenilmezdi; host adına ihtiyacınız varsa onu [getRemoteAddress() |#getRemoteAddress()] değerinden kendiniz çözün. getBasicCredentials(): ?array .[method] @@ -184,14 +216,38 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +İsteğin geldiği kaynağı döndürür. Bir kaynak; şema (protokol), host adı ve porttan oluşur; örneğin `https://example.com:8080`. Origin header'ı yoksa ya da `'null'` olarak ayarlıysa `null` döndürür. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Tarayıcı `Origin` header'ını şu durumlarda gönderir: +- Kaynaklar arası istekler (farklı bir alan adına yapılan AJAX çağrıları) +- POST, PUT, DELETE ve diğer değiştirici istekler +- Fetch API ile yapılan istekler + +Tarayıcı `Origin` header'ını şunlarda GÖNDERMEZ: +- Aynı alan adına yapılan sıradan GET istekleri (aynı kaynakta gezinme) +- Adres çubuğuna URL yazarak doğrudan gezinme +- Tarayıcı olmayan istemcilerden gelen istekler + +.[note] +`Referer` header'ının aksine `Origin` yalnızca şemayı, host'u ve portu içerir; URL yolunun tamamını değil. Bu, kullanıcı gizliliğini korurken onu güvenlik denetimleri için daha uygun kılar. `Origin` header'ı öncelikle [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing) doğrulamasında kullanılır. + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- -Dili algılar. Parametre olarak `$lang`, uygulamanın desteklediği dilleri içeren bir dizi iletiriz ve ziyaretçinin tarayıcısının en çok görmek isteyeceği dili döndürür. Bu sihir değil, sadece `Accept-Language` başlığı kullanılır. Eşleşme olmazsa `null` döndürür. +Dili saptar. `$langs` parametresi olarak uygulamanın desteklediği dillerden oluşan bir dizi verin; metot, ziyaretçinin tarayıcısının yeğlediği dili döndürür. Sihir değil; yalnızca `Accept-Language` header'ını kullanır. Eşleşme bulunmazsa `null` döndürür. ```php -// tarayıcı örneğin Accept-Language gönderir: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 +// Tarayıcı örneğin şunu gönderir: Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 -$langs = ['hu', 'pl', 'en']; // uygulama tarafından desteklenen diller +$langs = ['hu', 'pl', 'en']; // uygulamanın desteklediği diller echo $httpRequest->detectLanguage($langs); // en ``` @@ -199,62 +255,63 @@ echo $httpRequest->detectLanguage($langs); // en RequestFactory ============== -[api:Nette\Http\RequestFactory] sınıfı, mevcut HTTP isteğini temsil eden bir `Nette\Http\Request` örneği oluşturmak için kullanılır. (Nette ile çalışıyorsanız, HTTP isteği nesnesi framework tarafından otomatik olarak oluşturulur.) +[api:Nette\Http\RequestFactory] sınıfı, geçerli HTTP isteğini temsil eden bir `Nette\Http\Request` örneği oluşturmak için kullanılır. (Nette ile çalışıyorsanız HTTP istek nesnesi framework tarafından otomatik oluşturulur.) ```php $factory = new Nette\Http\RequestFactory; $httpRequest = $factory->fromGlobals(); ``` -`fromGlobals()` metodu, mevcut PHP genel değişkenlerine (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` ve `$_SERVER`) dayalı olarak bir istek nesnesi oluşturur. Nesneyi oluştururken, tüm GET, POST, COOKIE giriş parametrelerini ve ayrıca URL'yi kontrol karakterlerinden ve geçersiz UTF-8 dizilerinden otomatik olarak temizler, bu da bu verilerle daha fazla çalışırken güvenliği sağlar. +`fromGlobals()` metodu, istek nesnesini geçerli PHP genel değişkenlerine (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` ve `$_SERVER`) dayanarak oluşturur. Nesneyi oluştururken tüm girdi parametrelerini (GET, POST, COOKIE) ve URL'yi denetim karakterlerinden ve geçersiz UTF-8 dizilerinden otomatik olarak temizler; böylece bu veriyle sonradan çalışırken güvenlik sağlanır. RequestFactory, `fromGlobals()` çağrılmadan önce yapılandırılabilir: -- `$factory->setBinary()` metoduyla, giriş parametrelerinin kontrol karakterlerinden ve geçersiz UTF-8 dizilerinden otomatik olarak temizlenmesini devre dışı bırakırsınız. -- `$factory->setProxy(...)` metoduyla, kullanıcının IP adresinin doğru algılanması için gerekli olan [proxy sunucusu |configuration#HTTP proxy] IP adresini belirtirsiniz. +- `$factory->setBinary()` metodu, girdi parametrelerinin denetim karakterlerinden ve geçersiz UTF-8 dizilerinden otomatik temizlenmesini kapatır. +- `$factory->setProxy(...)` metodu, kullanıcının IP adresinin doğru saptanması için gereken [proxy sunucusunun |configuration#HTTP Proxy] IP adresini belirtir. +- `$factory->setForceHttps()` .{data-version:3.3.4} metodu, sunucu ortamından bağımsız olarak isteğin şemasını HTTPS'e zorlar. -RequestFactory, URL isteğinin bölümlerini otomatik olarak dönüştüren filtreler tanımlamanıza olanak tanır. Bu filtreler, URL'den, örneğin çeşitli web sitelerindeki yorum sistemlerinin yanlış uygulanmasıyla oraya eklenebilecek istenmeyen karakterleri kaldırır: +RequestFactory, URL isteğinin parçalarını otomatik dönüştüren filtreler tanımlamaya olanak tanır. Bu filtreler, örneğin çeşitli web sitelerindeki yorum sistemlerinin hatalı gerçekleştirimleri tarafından eklenmiş olabilecek istenmeyen karakterleri URL'lerden kaldırır: ```php -// yoldan boşlukların kaldırılması +// yoldan boşlukları kaldır $requestFactory->urlFilters['path']['%20'] = ''; -// URI sonundan nokta, virgül veya sağ parantezin kaldırılması +// URI'nin sonundaki noktayı, virgülü ya da sağ parantezi kaldır $requestFactory->urlFilters['url']['[.,)]$'] = ''; -// yolun çift eğik çizgilerden temizlenmesi (varsayılan filtre) +// yolu çift eğik çizgilerden temizle (varsayılan filtre) $requestFactory->urlFilters['path']['/{2,}'] = '/'; ``` -İlk anahtar `'path'` veya `'url'`, filtrenin URL'nin hangi bölümüne uygulanacağını belirtir. İkinci anahtar, aranacak düzenli ifadedir ve değer, bulunan metnin yerine kullanılacak olan değiştirmedir. +İlk anahtar, `'path'` ya da `'url'`, filtrenin URL'nin hangi parçasına uygulanacağını belirler. İkinci anahtar aranacak düzenli ifade, değer ise bulunan metnin yerine konacak değiştirmedir. Yüklenen Dosyalar ================= -`Nette\Http\Request::getFiles()` metodu, tüm yüklemelerin normalleştirilmiş bir yapıda bir dizisini döndürür, yaprakları [api:Nette\Http\FileUpload] nesneleridir. Bunlar, `<input type=file>` form elemanı tarafından gönderilen verileri kapsüller. +`Nette\Http\Request::getFiles()` metodu, tüm yüklemeleri normalleştirilmiş bir yapıda döndürür; yapraklar [api:Nette\Http\FileUpload] nesneleridir. Bunlar, `<input type=file>` form elemanının gönderdiği veriyi içine alır. -Yapı, HTML'deki elemanların adlandırılmasını yansıtır. En basit durumda, bu, şu şekilde gönderilen tek bir adlandırılmış form elemanı olabilir: +Yapı, HTML'deki eleman adlandırmasını yansıtır. En basit durumda bu, şöyle gönderilen tek ve adlandırılmış bir form elemanı olabilir: ```latte <input type="file" name="avatar"> ``` -Bu durumda `$request->getFiles()` bir dizi döndürür: +Bu durumda `$request->getFiles()` şu diziyi döndürür: ```php [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* FileUpload örneği */ ] ``` -`FileUpload` nesnesi, kullanıcı hiçbir dosya göndermese veya gönderme başarısız olsa bile oluşturulur. Dosyanın gönderilip gönderilmediğini `hasFile()` metodu döndürür: +`FileUpload` nesnesi, kullanıcı hiç dosya yüklemese ya da yükleme başarısız olsa bile oluşturulur. Bir dosya gönderildiyse `hasFile()` metodu true döndürür: ```php $request->getFile('avatar')?->hasFile(); ``` -Dizi gösterimini kullanan eleman adı durumunda: +Dizi yazımı kullanan bir eleman adı söz konusu olduğunda: ```latte <input type="file" name="my-form[details][avatar]"> @@ -266,44 +323,44 @@ döndürülen ağaç şöyle görünür: [ 'my-form' => [ 'details' => [ - 'avatar' => /* FileUpload instance */ + 'avatar' => /* FileUpload örneği */ ], ], ] ``` -Dosya dizileri de oluşturulabilir: +Dosya dizileri de oluşturabilirsiniz: ```latte <input type="file" name="my-form[details][avatars][]" multiple> ``` -Bu durumda yapı şöyle görünür: +Böyle bir durumda yapı şöyle görünür: ```php [ 'my-form' => [ 'details' => [ 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, + 0 => /* FileUpload örneği */, + 1 => /* FileUpload örneği */, + 2 => /* FileUpload örneği */, ], ], ], ] ``` -İç içe geçmiş dizinin 1 indeksine erişmenin en iyi yolu şudur: +İç içe dizinin 1 numaralı indeksine erişmenin en iyi yolu şudur: ```php $file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { +if ($file instanceof Nette\Http\FileUpload) { // ... } ``` -Dışarıdan gelen verilere güvenilemediği ve dolayısıyla dosya yapısının biçimine güvenilemediği için, bu yöntem, örneğin başarısız olabilecek `$request->getFiles()['my-form']['details']['avatars'][1]`'dan daha güvenlidir. +Dış veriye güvenemeyeceğiniz ve dolayısıyla dosyaların yapısına dayanamayacağınız için, bu yaklaşım örneğin başarısız olabilecek `$request->getFiles()['my-form']['details']['avatars'][1]` ifadesinden daha güvenlidir. `FileUpload` Metotlarına Genel Bakış .{toc: FileUpload} @@ -312,7 +369,7 @@ Dışarıdan gelen verilere güvenilemediği ve dolayısıyla dosya yapısının hasFile(): bool .[method] ------------------------- -Kullanıcı herhangi bir dosya yüklediyse `true` döndürür. +Kullanıcı bir dosya yüklediyse `true` döndürür. isOk(): bool .[method] @@ -322,12 +379,12 @@ Dosya başarıyla yüklendiyse `true` döndürür. getError(): int .[method] ------------------------- -Dosya yükleme sırasındaki hata kodunu döndürür. Bu, [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php] sabitlerinden biridir. Yükleme başarılı olursa `UPLOAD_ERR_OK` döndürür. +Yüklenen dosyayla ilişkili hata kodunu döndürür. [UPLOAD_ERR_XXX |https://php.net/manual/en/features.file-upload.errors.php] sabitlerinden biridir. Dosya başarıyla yüklendiyse `UPLOAD_ERR_OK` döndürür. move(string $dest) .[method] ---------------------------- -Yüklenen dosyayı yeni bir konuma taşır. Hedef dosya zaten varsa, üzerine yazılır. +Yüklenen dosyayı yeni bir konuma taşır. Hedef dosya zaten varsa üzerine yazılır. ```php $file->move('/path/to/files/name.ext'); @@ -341,42 +398,42 @@ Yüklenen dosyanın içeriğini döndürür. Yükleme başarılı olmadıysa `nu getContentType(): ?string .[method] ----------------------------------- -Yüklenen dosyanın MIME içerik türünü imzasına göre algılar. Yükleme başarılı olmadıysa veya algılama başarısız olursa `null` döndürür. +Yüklenen dosyanın MIME içerik türünü imzasına göre saptar. Yükleme başarılı olmadıysa ya da saptama başarısız olduysa `null` döndürür. .[caution] -PHP `fileinfo` uzantısını gerektirir. +`fileinfo` PHP eklentisini gerektirir. getUntrustedName(): string .[method] ------------------------------------ -Tarayıcı tarafından gönderildiği şekliyle dosyanın orijinal adını döndürür. +Tarayıcının gönderdiği özgün dosya adını döndürür. .[caution] -Bu metodun döndürdüğü değere güvenmeyin. İstemci, uygulamanıza zarar vermek veya hacklemek amacıyla kötü niyetli bir dosya adı göndermiş olabilir. +Bu metodun döndürdüğü değere güvenmeyin. Bir istemci, uygulamanızı bozma ya da ele geçirme niyetiyle kötü niyetli bir dosya adı gönderebilir. getSanitizedName(): string .[method] ------------------------------------ -Temizlenmiş dosya adını döndürür. Yalnızca ASCII karakterleri `[a-zA-Z0-9.-]` içerir. Ad bu tür karakterleri içermiyorsa `'unknown'` döndürür. Dosya JPEG, PNG, GIF, WebP veya AVIF biçiminde bir resimse, doğru uzantıyı da döndürür. +Temizlenmiş dosya adını döndürür. Yalnızca ASCII karakterleri `[a-zA-Z0-9.-]` içerir. Ad böyle karakterler içermiyorsa `'unknown'` döndürür. Dosya JPEG, PNG, GIF, WebP ya da AVIF görseliyse doğru dosya uzantısını da döndürür. .[caution] -PHP `fileinfo` uzantısını gerektirir. +`fileinfo` PHP eklentisini gerektirir. getSuggestedExtension(): ?string .[method]{data-version:3.2.4} -------------------------------------------------------------- -Algılanan MIME türüne karşılık gelen uygun dosya uzantısını (nokta olmadan) döndürür. +Saptanan MIME türüne karşılık gelen uygun dosya uzantısını (nokta olmadan) döndürür. .[caution] -PHP `fileinfo` uzantısını gerektirir. +`fileinfo` PHP eklentisini gerektirir. getUntrustedFullPath(): string .[method] ---------------------------------------- -Klasör yüklenirken tarayıcı tarafından gönderildiği şekliyle dosyanın orijinal yolunu döndürür. Tam yol yalnızca PHP 8.1 ve sonraki sürümlerde kullanılabilir. Önceki sürümlerde bu metot dosyanın orijinal adını döndürür. +Dizin yüklemesi sırasında tarayıcının gönderdiği özgün dosya yolunu döndürür. Tam yol yalnızca PHP 8.1 ve sonrasında kullanılabilir. Önceki sürümlerde bu metot özgün dosya adını döndürür. .[caution] -Bu metodun döndürdüğü değere güvenmeyin. İstemci, uygulamanıza zarar vermek veya hacklemek amacıyla kötü niyetli bir dosya adı göndermiş olabilir. +Bu metodun döndürdüğü değere güvenmeyin. Bir istemci, uygulamanızı bozma ya da ele geçirme niyetiyle kötü niyetli bir dosya adı gönderebilir. getSize(): int .[method] @@ -389,19 +446,24 @@ getTemporaryFile(): string .[method] Yüklenen dosyanın geçici konumunun yolunu döndürür. Yükleme başarılı olmadıysa `''` döndürür. +__toString(): string .[method] +------------------------------ +Yüklenen dosyanın geçici konumunun yolunu döndürür. Bu, `FileUpload` nesnesinin doğrudan dize olarak kullanılmasını sağlar. + + isImage(): bool .[method] ------------------------- -Yüklenen dosya JPEG, PNG, GIF, WebP veya AVIF biçiminde bir resimse `true` döndürür. Algılama imzasına göre yapılır ve tüm dosyanın bütünlüğü doğrulanmaz. Bir resmin hasarlı olup olmadığını, örneğin onu [yüklemeye |#toImage] çalışarak belirleyebilirsiniz. +Yüklenen dosya JPEG, PNG, GIF, WebP ya da AVIF görseliyse `true` döndürür. Saptama imzasına dayanır ve dosyanın tamamının bütünlüğünü doğrulamaz. Bir görselin bozuk olup olmadığı, örneğin onu [yüklemeyi |#toImage()] deneyerek belirlenebilir. .[caution] -PHP `fileinfo` uzantısını gerektirir. +`fileinfo` PHP eklentisini gerektirir. getImageSize(): ?array .[method] -------------------------------- -Yüklenen resmin boyutlarını içeren `[genişlik, yükseklik]` çiftini döndürür. Yükleme başarılı olmadıysa veya geçerli bir resim değilse `null` döndürür. +Yüklenen görselin boyutlarını içeren `[genişlik, yükseklik]` çiftini döndürür. Yükleme başarılı olmadıysa ya da geçerli bir görsel değilse `null` döndürür. toImage(): Nette\Utils\Image .[method] -------------------------------------- -Resmi [Image|utils:images] nesnesi olarak yükler. Yükleme başarılı olmadıysa veya geçerli bir resim değilse `Nette\Utils\ImageException` istisnası atar. +Görseli bir [Image |utils:images] nesnesi olarak yükler. Yükleme başarılı olmadıysa ya da geçerli bir görsel değilse `Nette\Utils\ImageException` fırlatır. diff --git a/http/tr/response.texy b/http/tr/response.texy index a3d20eba24..c2ca68743b 100644 --- a/http/tr/response.texy +++ b/http/tr/response.texy @@ -2,9 +2,9 @@ HTTP Yanıtı *********** .[perex] -Nette, HTTP yanıtını anlaşılır bir API'ye sahip nesneler içinde kapsüller. +Nette, HTTP yanıtını anlaşılır bir API'ye sahip nesnelerin içine alır. -HTTP yanıtı [api:Nette\Http\Response] nesnesi tarafından temsil edilir. Nette ile çalışıyorsanız, bu nesne framework tarafından otomatik olarak oluşturulur ve [bağımlılık enjeksiyonu |dependency-injection:passing-dependencies] aracılığıyla size iletilmesini sağlayabilirsiniz. Presenter'larda sadece `$this->getHttpResponse()` metodunu çağırmanız yeterlidir. +HTTP yanıtı [api:Nette\Http\Response] nesnesiyle temsil edilir. Nette ile çalışıyorsanız bu nesne framework tarafından otomatik oluşturulur ve [bağımlılık enjeksiyonuyla |dependency-injection:passing-dependencies] size aktarılmasını sağlayabilirsiniz. Presenter'larda yalnızca `$this->getHttpResponse()` metodunu çağırın. → [Kurulum ve gereksinimler |@home#Kurulum] @@ -12,12 +12,12 @@ HTTP yanıtı [api:Nette\Http\Response] nesnesi tarafından temsil edilir. Nette Nette\Http\Response =================== -Nesne, [Nette\Http\Request|request] aksine değiştirilebilirdir (mutable), yani ayarlayıcılar kullanarak durumu değiştirebilirsiniz, örneğin başlıkları gönderebilirsiniz. Tüm ayarlayıcıların **herhangi bir çıktı gönderilmeden önce** çağrılması gerektiğini unutmayın. Çıktının zaten gönderilip gönderilmediğini `isSent()` metodu söyler. `true` döndürürse, başlık göndermeye yönelik her girişim `Nette\InvalidStateException` istisnası atar. +[Nette\Http\Request |request] nesnesinin aksine bu nesne değiştirilebilirdir, dolayısıyla durumu değiştirmek için (örneğin header göndermek için) setter'ları kullanabilirsiniz. Tüm setter'ların **herhangi bir gerçek çıktı gönderilmeden önce çağrılması gerektiğini** unutmayın. `isSent()` metodu, çıktının gönderilip gönderilmediğini gösterir. `true` döndürüyorsa, header gönderme girişimi `Nette\InvalidStateException` fırlatır. setCode(int $code, ?string $reason=null) .[method] -------------------------------------------------- -[Yanıt kodu |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10] durumunu değiştirir. Kaynak kodunun daha iyi anlaşılması için, kod için sayılar yerine [önceden tanımlanmış sabitler |api:Nette\Http\IResponse] kullanmanızı öneririz. +[Yanıt durum kodunu |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10] değiştirir. Kaynak kodun okunurluğu için, gerçek sayılar yerine [önceden tanımlı sabitleri |api:Nette\Http\IResponse] kullanmanız önerilir. ```php $httpResponse->setCode(Nette\Http\Response::S404_NotFound); @@ -31,12 +31,12 @@ Yanıtın durum kodunu döndürür. isSent(): bool .[method] ------------------------ -Başlıkların sunucudan tarayıcıya zaten gönderilip gönderilmediğini ve dolayısıyla artık başlık göndermenin veya durum kodunu değiştirmenin mümkün olup olmadığını döndürür. +Header'ların sunucudan tarayıcıya gönderilip gönderilmediğini, yani artık header göndermenin ya da durum kodunu değiştirmenin olanaklı olup olmadığını döndürür. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Bir HTTP başlığı gönderir ve daha önce gönderilen aynı addaki başlığın **üzerine yazar**. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Bir HTTP header'ı gönderir ve aynı adla daha önce gönderilmiş header'ın **üzerine yazar**. `$value` `null` ise header kaldırılır. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -45,7 +45,7 @@ $httpResponse->setHeader('Pragma', 'no-cache'); addHeader(string $name, string $value) .[method] ------------------------------------------------ -Bir HTTP başlığı gönderir ve daha önce gönderilen aynı addaki başlığın **üzerine yazmaz**. +Bir HTTP header'ı gönderir ve aynı adla daha önce gönderilmiş header'ın **üzerine yazmaz**. ```php $httpResponse->addHeader('Accept', 'application/json'); @@ -55,21 +55,21 @@ $httpResponse->addHeader('Accept', 'application/xml'); deleteHeader(string $name) .[method] ------------------------------------ -Daha önce gönderilen bir HTTP başlığını siler. +Daha önce gönderilmiş bir HTTP header'ını siler. getHeader(string $header): ?string .[method] -------------------------------------------- -Gönderilen HTTP başlığını veya böyle bir başlık yoksa `null` döndürür. Parametre büyük/küçük harfe duyarsızdır. +Gönderilen HTTP header'ını, yoksa `null` döndürür. Parametre büyük/küçük harfe duyarsızdır. ```php $pragma = $httpResponse->getHeader('Pragma'); ``` -getHeaders(): array .[method] ------------------------------ -Gönderilen tüm HTTP başlıklarını ilişkisel bir dizi olarak döndürür. +getHeaders(): array<string, string> .[method] +--------------------------------------------- +Gönderilen tüm HTTP header'larını ilişkisel bir dizi olarak döndürür. ```php $headers = $httpResponse->getHeaders(); @@ -79,7 +79,7 @@ echo $headers['Pragma']; setContentType(string $type, ?string $charset=null) .[method] ------------------------------------------------------------- -`Content-Type` başlığını değiştirir. +`Content-Type` header'ını değiştirir. ```php $httpResponse->setContentType('text/plain', 'UTF-8'); @@ -96,55 +96,89 @@ exit; ``` -setExpiration(?string $time) .[method] --------------------------------------- -`Cache-Control` ve `Expires` başlıklarını kullanarak HTTP belgesinin sona erme süresini ayarlar. Parametre ya bir zaman aralığıdır (metin olarak) ya da `null`'dır, bu da önbelleğe almayı devre dışı bırakır. +setExpiration(?string $expire) .[method] +---------------------------------------- +HTTP belgesinin son kullanma süresini `Cache-Control` ve `Expires` header'larıyla ayarlar. Parametre ya bir zaman aralığıdır (metin olarak) ya da önbelleklemeyi kapatan `null`. ```php -// tarayıcı önbelleği bir saat içinde sona erecek +// tarayıcı önbelleği bir saat içinde dolar $httpResponse->setExpiration('1 hour'); ``` sendAsFile(string $fileName) .[method] -------------------------------------- -Yanıt, belirtilen ad altında *Farklı Kaydet* iletişim kutusu kullanılarak indirilecektir. Dosyanın kendisini göndermez. +Yanıt, belirtilen adla bir *Farklı kaydet* iletişim kutusu üzerinden indirilir. Dosyanın kendisini göndermez. ```php -$httpResponse->sendAsFile('faktura.pdf'); +$httpResponse->sendAsFile('invoice.pdf'); ``` -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Bir çerez gönderir. Parametrelerin varsayılan değerleri: +setCookie(string $name, string $value, $expire, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, SameSite|string $sameSite='Lax', bool $partitioned=false) .[method] +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +Bir çerez gönderir. Varsayılan parametre değerleri: -| `$path` | `'/'` | çerez, (alt) alan adındaki tüm yollarda kapsama sahiptir *(yapılandırılabilir)* -| `$domain` | `null` | bu, geçerli (alt) alan adında kapsama sahip olduğu, ancak alt alan adlarında olmadığı anlamına gelir *(yapılandırılabilir)* -| `$secure` | `true` | web sitesi HTTPS üzerinde çalışıyorsa, aksi takdirde `false` *(yapılandırılabilir)* -| `$httpOnly` | `true` | çerez JavaScript için erişilemez -| `$sameSite` | `'Lax'` | çerez [başka bir alan adından erişim |nette:glossary#SameSite Çerezi] sırasında gönderilmeyebilir +| `$path` | `'/'` | çerez, (alt) alan adı içindeki tüm yollarda kullanılabilir *(yapılandırılabilir)* +| `$domain` | `null` | yani geçerli (alt) alan adında kullanılabilir, ama onun alt alan adlarında değil *(yapılandırılabilir)* +| `$secure` | `auto` | site HTTPS üzerinde çalışıyorsa `true`, aksi hâlde `false` (framework varsayılanı; sınıfın kendi varsayılanı `false`) *(yapılandırılabilir)* +| `$httpOnly` | `true` | çerez JavaScript'ten erişilemez +| `$sameSite` | `'Lax'` | çerez, [kaynaklar arası erişimde |nette:glossary#SameSite çerezi] gönderilmeyebilir +| `$partitioned` | `false` | çerezin bölümlenip bölümlenmediği, aşağıya bakın *(v3.4'ten beri)* -`$path`, `$domain` ve `$secure` parametrelerinin varsayılan değerlerini [yapılandırma |configuration#HTTP çerezi] içinde değiştirebilirsiniz. +`$path`, `$domain` ve `$secure` parametrelerinin varsayılan değerlerini [yapılandırmada |configuration#HTTP Çerezi] değiştirebilirsiniz. -Zaman, saniye sayısı veya bir dize olarak belirtilebilir: +Son kullanma; saniye sayısı olarak, metinsel bir aralık ya da tarih olarak veya bir `DateTimeInterface` nesnesi olarak verilir. `null` değeri, tarayıcının kapatıldığında attığı bir oturum çerezi oluşturur. Nette, son kullanmayı hem `Expires` hem de `Max-Age` niteliklerinde gönderir. ```php -$httpResponse->setCookie('lang', 'tr', '100 days'); // 'cs' changed to 'tr' as an example +$httpResponse->setCookie('lang', 'en', '100 days'); // 100 gün içinde dolar +$httpResponse->setCookie('lang', 'en', null); // oturum çerezi ``` -`$domain` parametresi, hangi alan adlarının çerezi kabul edebileceğini belirtir. Belirtilmezse, çerez onu ayarlayan aynı (alt) alan adı tarafından kabul edilir, ancak alt alan adları tarafından değil. `$domain` belirtilirse, alt alan adları da dahil edilir. Bu nedenle, `$domain` belirtmek, atlamaktan daha az kısıtlayıcıdır. Örneğin, `$domain = 'nette.org'` ile çerezler `doc.nette.org` gibi tüm alt alan adlarında da kullanılabilir. +`$domain` parametresi, çerezi hangi alan adlarının kabul edebileceğini belirler. Belirtilmezse çerez, onu ayarlayan aynı (alt) alan adı tarafından kabul edilir, ama onun alt alan adları tarafından edilmez. `$domain` belirtilirse alt alan adları da dahil olur. Bu yüzden `$domain` belirtmek, onu atlamaktan daha az kısıtlayıcıdır. Örneğin `$domain = 'nette.org'` ile çerezler `doc.nette.org` gibi tüm alt alan adlarında da kullanılabilir. + +`$sameSite` değerini bir `Nette\Http\SameSite` enum'u olarak verebilirsiniz: `SameSite::Lax`, `SameSite::Strict` ya da `SameSite::None` (`'Lax'`, `'Strict'`, `'None'` dize değerleri de çalışır). Onu `SameSite::None` yaparsanız `$secure` niteliği otomatik açılır; çünkü tarayıcılar güvenli olmayan bir `SameSite=None` çerezini reddeder. + +.{data-version:3.4.0} +Bölümlenmiş çerezler (CHIPS), bir çereze her üst düzey site için kendi ayrı deposunu verir. Böylece üçüncü taraf bir servis (örneğin gömülü bir widget) bölümlenmiş bir çerez ayarladığında, tarayıcı widget'ın göründüğü her site için ayrı bir kopya tutar ve bu kopyalar siteler arası izleme için birbirine bağlanamaz. Onu `$partitioned` değerini `true` yaparak açın; bu, `$secure` niteliğini de gerektirir, dolayısıyla otomatik açılır. -`$sameSite` değeri için `Response::SameSiteLax`, `Response::SameSiteStrict` ve `Response::SameSiteNone` sabitlerini kullanabilirsiniz. +```php +$httpResponse->setCookie('theme', 'dark', '1 year', sameSite: SameSite::None, partitioned: true); +``` deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] -------------------------------------------------------------------------------------------------------- -Bir çerezi siler. Parametrelerin varsayılan değerleri şunlardır: -- `$path` tüm dizinlerde kapsama sahip (`'/'`) -- `$domain` geçerli (alt) alan adında kapsama sahip, ancak alt alan adlarında değil -- `$secure`, [yapılandırma |configuration#HTTP çerezi] içindeki ayarlara göre yönetilir +Bir çerezi siler. Parametrelerin varsayılan değerleri: +- tüm dizinleri kapsayan `$path` (`'/'`) +- geçerli (alt) alan adını kapsayan, ama onun alt alan adlarını kapsamayan `$domain` +- `$secure`, [yapılandırmadaki |configuration#HTTP Çerezi] ayarlara bağlıdır ```php $httpResponse->deleteCookie('lang'); ``` + + +Nette\Http\Context +================== + +[api:Nette\Http\Context] nesnesi, isteği ve yanıtı bir araya getirir ve HTTP önbelleklemesine yardım eder. Servis olarak kaydedilmez, dolayısıyla onu kendiniz oluşturursunuz. Presenter'larda genellikle [lastModified() |application:presenters#HTTP önbelleklemesi] metodunu kullanmak daha kolaydır; context, yanıtı örneğin kendi yanıt sınıfınızdan kendiniz gönderdiğinizde işe yarar. + + +isModified(string|int|\DateTimeInterface|null $lastModified=null, ?string $etag=null): bool .[method] +----------------------------------------------------------------------------------------------------- +İçeriğin istemcinin son ziyaretinden bu yana değişip değişmediğini belirler. Son değiştirilme zamanını verirseniz `Last-Modified` header'ını gönderir; bir ETag doğrulayıcısı (içeriğin geçerli sürümünü tanımlayan kısa bir dize, örneğin hash'i) verirseniz `ETag` header'ını gönderir. Sonra ikisini de tarayıcının gönderdiği `If-Modified-Since` ve `If-None-Match` header'larıyla karşılaştırır. + +Tarayıcıda zaten uyan bir sürüm varsa, metot `304 Not Modified` kodunu ayarlar ve `false` döndürür; o durumda yanıtın gövdesini hiç göndermeyin. Aksi hâlde `true` döndürür. + +```php +public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void +{ + $context = new Nette\Http\Context($request, $response); + if ($context->isModified(filemtime($this->file), md5_file($this->file))) { + readfile($this->file); + } +} +``` + +Her iki parametre de isteğe bağlıdır. İçeriğin değiştirilme zamanını bilmiyorsanız yalnızca ETag kullanın, tersi de geçerlidir. diff --git a/http/tr/sessions.texy b/http/tr/sessions.texy index 2f3e3cbb2f..1321268965 100644 --- a/http/tr/sessions.texy +++ b/http/tr/sessions.texy @@ -1,21 +1,21 @@ -Oturumlar (Sessions) -******************** +Oturumlar +********* <div class=perex> -HTTP durumsuz bir protokoldür, ancak neredeyse her uygulamanın istekler arasında durumu koruması gerekir, örneğin alışveriş sepetinin içeriği. İşte bu noktada oturumlar devreye girer. Göstereceğimiz konular: +HTTP durumsuz bir protokoldür; yine de neredeyse her uygulamanın istekler arasında bir durumu, örneğin alışveriş sepetinin içeriğini sürdürmesi gerekir. Oturumlar tam da bunun için kullanılır. Şunları göstereceğiz: -- oturumları nasıl kullanacağınız -- isim çakışmalarını nasıl önleyeceğiniz -- sona erme süresini nasıl ayarlayacağınız +- oturumların nasıl kullanılacağı +- ad çakışmalarının nasıl önleneceği +- son kullanma süresinin nasıl ayarlanacağı </div> -Oturumları kullanırken, her kullanıcıya oturum kimliği adı verilen benzersiz bir tanımlayıcı verilir ve bu tanımlayıcı bir çerezde iletilir. Bu, oturum verileri için bir anahtar görevi görür. Tarayıcı tarafında saklanan çerezlerin aksine, oturum verileri sunucu tarafında saklanır. +Oturum kullanıldığında her kullanıcı, bir çerezde aktarılan ve oturum ID'si denen benzersiz bir tanımlayıcı alır. Bu, oturum verisinin anahtarı görevini görür. Tarayıcı tarafında saklanan çerezlerin aksine, oturum verisi sunucu tarafında saklanır. -Oturumu [yapılandırma |configuration#Oturum Session] içinde ayarlarız, özellikle sona erme süresi seçeneği önemlidir. +Oturumları [yapılandırmada |configuration#Oturum] ayarlarız; özellikle son kullanma süresinin seçimi önemlidir. -Oturum yönetimi [api:Nette\Http\Session] nesnesi tarafından gerçekleştirilir. Bu nesneye [bağımlılık enjeksiyonu |dependency-injection:passing-dependencies] aracılığıyla erişebilirsiniz. Presenter'larda sadece `$session = $this->getSession()` çağırmanız yeterlidir. +Oturum yönetimini [api:Nette\Http\Session] nesnesi üstlenir; ona, [bağımlılık enjeksiyonuyla |dependency-injection:passing-dependencies] aktarılmasını sağlayarak erişebilirsiniz. Presenter'larda yalnızca `$session = $this->getSession()` çağırın. → [Kurulum ve gereksinimler |@home#Kurulum] @@ -23,49 +23,49 @@ Oturum yönetimi [api:Nette\Http\Session] nesnesi tarafından gerçekleştirilir Oturumu Başlatma ================ -Nette, varsayılan olarak oturumu okumaya veya veri yazmaya başladığımızda otomatik olarak başlatır. Oturum manuel olarak `$session->start()` ile başlatılır. +Nette varsayılan olarak, ondan veri okumaya ya da ona veri yazmaya başladığımız anda oturumu otomatik başlatır. Oturumu elle başlatmak için `$session->start()` kullanın. -PHP, oturum başlatıldığında önbelleğe almayı etkileyen HTTP başlıklarını gönderir, bkz. [php:session_cache_limiter], ve muhtemelen oturum kimliği içeren bir çerez de gönderir. Bu nedenle, herhangi bir çıktıyı tarayıcıya göndermeden önce oturumu her zaman başlatmak gerekir, aksi takdirde bir istisna atılır. Bu nedenle, sayfa oluşturma sırasında oturumun kullanılacağını biliyorsanız, önceden manuel olarak başlatın, örneğin presenter'da. +PHP, oturum başlatılırken önbelleklemeyi etkileyen HTTP header'larını (bkz. [php:session_cache_limiter]) ve olasılıkla oturum ID'sini taşıyan bir çerez gönderir. Bu yüzden oturumu, tarayıcıya herhangi bir çıktı göndermeden önce başlatmak her zaman gereklidir; aksi hâlde bir istisna fırlatılır. Yani sayfa render edilirken bir oturumun kullanılacağını biliyorsanız, onu önceden elle, örneğin presenter'da başlatın. -Geliştirme modunda, Tracy oturumu başlatır çünkü onu Tracy Bar'daki yönlendirme ve AJAX istekleri çubuklarını görüntülemek için kullanır. +Geliştirici kipinde Tracy oturumu başlatır; çünkü onu Tracy Bar'da yönlendirmeler ve AJAX istekleri için çubuk göstermekte kullanır. Bölümler ======== -Saf PHP'de, oturum veri deposu `$_SESSION` genel değişkeni aracılığıyla erişilebilen bir dizi olarak gerçekleştirilir. Sorun şu ki, uygulamalar genellikle birbirine bağlı olmayan bir dizi parçadan oluşur ve hepsi yalnızca bir diziye erişebiliyorsa, er ya da geç bir isim çakışması meydana gelir. +Saf PHP'de oturum verisi deposu, genel `$_SESSION` değişkeniyle erişilen bir dizi olarak gerçekleştirilir. Sorun şu ki uygulamalar tipik olarak pek çok bağımsız parçadan oluşur ve hepsinin elinde tek bir dizi varsa, er ya da geç bir ad çakışması olur. -Nette Framework, tüm alanı bölümlere ( [api:Nette\Http\SessionSection] nesneleri) ayırarak sorunu çözer. Her birim daha sonra benzersiz bir ada sahip kendi bölümünü kullanır ve artık çakışma olamaz. +Nette Framework bu sorunu, tüm alanı bölümlere ([api:Nette\Http\SessionSection] nesneleri) ayırarak çözer. Her birim böylece benzersiz adı olan kendi bölümünü kullanır ve hiçbir çakışma olamaz. -Bölümü oturumdan alırız: +Bir bölümü oturumdan alırız: ```php -$section = $session->getSection('benzersiz_isim'); // 'unikatni nazev' translated +$section = $session->getSection('benzersiz ad'); ``` -Presenter'da sadece parametre ile `getSession()` kullanın: +Presenter'da yalnızca `getSession()` metodunu bir parametreyle kullanın: ```php // $this bir Presenter'dır -$section = $this->getSession('benzersiz_isim'); // 'unikatni nazev' translated +$section = $this->getSession('benzersiz ad'); ``` -Bir bölümün varlığı `$session->hasSection('benzersiz_isim')` metoduyla kontrol edilebilir. +Bir bölümün varlığı `$session->hasSection('benzersiz ad')` metoduyla denetlenebilir. Var olan tüm bölümlerin adlarının listesini `$session->getSectionNames()` döndürür. -Bölümün kendisiyle çalışmak daha sonra `set()`, `get()` ve `remove()` metotlarıyla çok kolaydır: +Bölümün kendisiyle çalışmak `set()`, `get()` ve `remove()` metotlarıyla çok kolaydır: ```php -// değişken yazma -$section->set('userName', 'franta'); +// bir değişken yazma +$section->set('userName', 'john'); -// değişken okuma, yoksa null döndürür +// bir değişkeni okuma, yoksa null döndürür echo $section->get('userName'); -// değişkeni kaldırma +// bir değişkeni kaldırma $section->remove('userName'); ``` -Bölümdeki tüm değişkenleri almak için `foreach` döngüsü kullanılabilir: +Bir bölümdeki tüm değişkenleri almak için bir `foreach` döngüsü kullanabilirsiniz: ```php foreach ($section as $key => $val) { @@ -74,37 +74,37 @@ foreach ($section as $key => $val) { ``` -Sona Erme Süresini Ayarlama ---------------------------- +Son Kullanma Nasıl Ayarlanır +---------------------------- -Tek tek bölümler veya hatta tek tek değişkenler için sona erme süresi ayarlamak mümkündür. Böylece kullanıcının oturum açma süresini 20 dakika sonra sona erdirebilir, ancak sepetin içeriğini hatırlamaya devam edebiliriz. +Son kullanma, tek tek bölümler ve hatta tek tek değişkenler için ayarlanabilir. Bir kullanıcının oturumunun 20 dakika sonra sona ermesini sağlarken, alışveriş sepetinin içeriğini anımsamayı sürdürebiliriz. ```php -// bölüm 20 dakika sonra sona erecek +// bölüm 20 dakika sonra sona erer $section->setExpiration('20 minutes'); ``` -Tek tek değişkenler için sona erme süresini ayarlamak için `set()` metodunun üçüncü parametresi kullanılır: +Tek tek değişkenler için son kullanmayı ayarlamak amacıyla `set()` metodunun üçüncü parametresini kullanın: ```php -// 'flash' değişkeni 30 saniye sonra sona erecek +// 'flash' değişkeni 30 saniye sonra sona erer $section->set('flash', $message, '30 seconds'); ``` .[note] -Tüm oturumun sona erme süresinin (bkz. [oturum yapılandırması |configuration#Oturum Session]) tek tek bölümler veya değişkenler için ayarlanan süreye eşit veya daha uzun olması gerektiğini unutmayın. +Oturumun tamamının son kullanma süresinin (bkz. [oturum yapılandırması |configuration#Oturum]) tek tek bölümler ya da değişkenler için ayarlanan süreye eşit ya da ondan büyük olması gerektiğini unutmayın. -Daha önce ayarlanan sona erme süresinin iptali `removeExpiration()` metoduyla sağlanır. Tüm bölümün anında iptali `remove()` metoduyla sağlanır. +Daha önce ayarlanmış bir son kullanmayı iptal etmek için `removeExpiration()` metodunu kullanın; belirli bir değişkenin son kullanmasını temizlemek için onun adını verin: `removeExpiration('flash')`. Bölümün tamamını hemen kaldırmak için `remove()` metodunu kullanın. $onStart, $onBeforeWrite Olayları --------------------------------- -`Nette\Http\Session` nesnesinin [olaylar |nette:glossary#Olaylar Events] `$onStart` ve `$onBeforeWrite` vardır, bu nedenle oturum başlatıldıktan sonra veya diske yazılmadan ve ardından sonlandırılmadan önce çağrılacak geri aramalar ekleyebilirsiniz. +`Nette\Http\Session` nesnesinin `$onStart` ve `$onBeforeWrite` [olayları |nette:glossary#Olaylar] vardır; böylece oturum başladıktan sonra ya da diske yazılıp ardından sonlandırılmadan önce çağrılan callback'ler ekleyebilirsiniz. ```php $session->onBeforeWrite[] = function () { - // oturum verilerini yazacağız + // veriyi oturuma yaz $this->section->set('basket', $this->basket); }; ``` @@ -130,7 +130,7 @@ Oturum başlatıldı mı? close(): void .[method] ----------------------- -Oturumu sonlandırır. Oturum, betiğin çalışması sonunda otomatik olarak sonlandırılır. +Oturumu sonlandırır. Oturum, betiğin çalışması bittiğinde otomatik olarak sonlanır. destroy(): void .[method] @@ -140,17 +140,17 @@ Oturumu sonlandırır ve siler. exists(): bool .[method] ------------------------ -HTTP isteği oturum kimliği içeren bir çerez içeriyor mu? +HTTP isteği oturum ID'si taşıyan bir çerez içeriyor mu? regenerateId(): void .[method] ------------------------------ -Yeni rastgele bir oturum kimliği oluşturur. Veriler korunur. +Yeni ve rastgele bir oturum ID'si üretir. Veri korunur. getId(): string .[method] ------------------------- -Oturum kimliğini döndürür. +Oturum ID'sini döndürür. </div> @@ -158,34 +158,34 @@ Oturum kimliğini döndürür. Yapılandırma ------------ -Oturumu [yapılandırmada |configuration#Oturum Session] ayarlarız. DI konteyneri kullanmayan bir uygulama yazıyorsanız, yapılandırma için şu metotlar kullanılır. Oturum başlatılmadan önce çağrılmalıdırlar. +Oturumu [yapılandırmada |configuration#Oturum] ayarlarız. DI container kullanmayan bir uygulama yazıyorsanız, yapılandırma için bu metotları kullanın. Oturumu başlatmadan önce çağrılmalıdırlar. <div class=wiki-methods-brief> setName(string $name): static .[method] --------------------------------------- -Oturum kimliğinin iletildiği çerezin adını ayarlar. Standart ad `PHPSESSID`'dir. Aynı web sitesi içinde birkaç farklı uygulama çalıştırıyorsanız kullanışlıdır. +Oturum ID'sinin aktarıldığı çerezin adını ayarlar. Standart ad `PHPSESSID`'dir. Bu, aynı web sitesinde birkaç farklı uygulama çalıştırıyorsanız yararlıdır. getName(): string .[method] --------------------------- -Oturum kimliğinin iletildiği çerezin adını döndürür. +Oturum ID'sinin aktarıldığı çerezin adını döndürür. setOptions(array $options): static .[method] -------------------------------------------- -Oturumu yapılandırır. Tüm PHP [oturum yönergeleri |https://www.php.net/manual/en/session.configuration.php] (camelCase biçiminde, ör. `session.save_path` yerine `savePath` yazılır) ve ayrıca [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] ayarlanabilir. +Oturumu yapılandırır. Tüm PHP [oturum yönergelerini |https://www.php.net/manual/en/session.configuration.php] (camelCase biçiminde, örneğin `session.save_path` yerine `savePath` yazın) ve ayrıca [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] seçeneğini ayarlamak olanaklıdır. -setExpiration(?string $time): static .[method] ----------------------------------------------- -Oturumun sona ereceği etkinlik dışı kalma süresini ayarlar. +setExpiration(?string $expire): static .[method] +------------------------------------------------ +Oturumun sona ereceği hareketsizlik süresini ayarlar. -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Çerez parametrelerini ayarlar. Parametrelerin varsayılan değerlerini [yapılandırmada |configuration#Oturum çerezi] değiştirebilirsiniz. +setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, SameSite|string|null $samesite=null): static .[method] +---------------------------------------------------------------------------------------------------------------------------------- +Çerezler için parametreleri ayarlar. Varsayılan parametre değerlerini [yapılandırmada |configuration#Oturum Çerezi] değiştirebilirsiniz. setSavePath(string $path): static .[method] @@ -195,7 +195,7 @@ Oturum dosyalarının saklandığı dizini ayarlar. setHandler(\SessionHandlerInterface $handler): static .[method] --------------------------------------------------------------- -Özel bir işleyici ayarlar, bkz. [PHP belgeleri |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. +Özel bir handler ayarlar, bkz. [PHP belgeleri |https://www.php.net/manual/en/class.sessionhandlerinterface.php]. </div> @@ -203,9 +203,9 @@ setHandler(\SessionHandlerInterface $handler): static .[method] Önce Güvenlik ============= -Sunucu, istekler aynı oturum kimliğiyle eşlik ettiği sürece sürekli olarak aynı kullanıcıyla iletişim kurduğunu varsayar. Güvenlik mekanizmalarının görevi, bunun gerçekten böyle olmasını ve tanımlayıcının çalınmasının veya sahtesinin yapılmasının mümkün olmamasını sağlamaktır. +Sunucu, istekler aynı oturum ID'siyle geldiği sürece aynı kullanıcıyla iletişim kurduğunu varsayar. Güvenlik düzeneklerinin görevi, bunun gerçekten böyle olduğunu ve tanımlayıcının çalınamayacağını ya da değiştirilemeyeceğini güvence altına almaktır. -Nette Framework bu nedenle PHP yönergelerini, oturum kimliğini yalnızca çerezde iletecek, JavaScript'e erişilemez hale getirecek ve URL'deki olası tanımlayıcıları yok sayacak şekilde doğru bir şekilde yapılandırır. Ayrıca, kullanıcının oturum açması gibi kritik anlarda yeni bir oturum kimliği oluşturur. +Bu yüzden Nette Framework, PHP yönergelerini oturum ID'sini yalnızca çerezlerde aktaracak, JavaScript'ten erişilemez kılacak ve URL'deki tanımlayıcıları yok sayacak şekilde doğru yapılandırır. Üstelik kullanıcı girişi gibi kritik anlarda yeni bir oturum ID'si üretir. .[note] -PHP yapılandırması için ini_set fonksiyonu kullanılır, ancak maalesef bazı hostingler bunu yasaklar. Bu sizin hostinginiz için de geçerliyse, onlarla fonksiyonu etkinleştirmelerini veya en azından sunucuyu yapılandırmalarını istemeyi deneyin. +PHP'yi yapılandırmak için `ini_set` fonksiyonu kullanılır, ama ne yazık ki bazı barındırma sağlayıcıları onun kullanımını yasaklar. Barındırmanızda durum böyleyse, bu fonksiyonu size açmalarını ya da hiç değilse sunucuyu düzgün yapılandırmalarını ayarlamayı deneyin. diff --git a/http/tr/ssrf.texy b/http/tr/ssrf.texy new file mode 100644 index 0000000000..7aff63ec63 --- /dev/null +++ b/http/tr/ssrf.texy @@ -0,0 +1,183 @@ +SSRF Koruması +************* + +.[perex] +Uygulamanız kullanıcının verdiği bir URL'yi indirdiğinde, bir saldırgan bunu iç ağınıza ulaşmak için kötüye kullanabilir. [#UrlValidator] ve [#IPAddress] sınıfları, bu Server-Side Request Forgery (SSRF) saldırılarına karşı korunmanıza yardım eder. + +→ [Kurulum ve gereksinimler |@home#Kurulum] + + +SSRF Nedir? +=========== + +Kullanıcının bir URL girdiği ve sunucunuzun onu indirdiği bir özellik düşünün: uzak bir adresten alınan avatar, bir webhook hedefi, bir bağlantı önizlemesi. Zararsız görünür, ama adrese kullanıcının tarayıcısı değil sunucu ulaşır. Ve sunucu, saldırganın göremeyeceği yerleri görebilir: loopback arayüzünü, özel ağı, bulut servislerini. + +Bu yüzden saldırgan, genel internet yerine içeriye işaret eden bir URL gönderir. Tipik hedefler şunlardır: + +- erişim anahtarlarını sızdırabilen `http://169.254.169.254/` adresindeki bulut meta verileri +- `http://192.168.1.1/` gibi iç yönetim panelleri ve yönlendiriciler +- `http://localhost:6379/` adresindeki Redis gibi kimlik doğrulaması olmayan servisler + +Bu açık sınıfı o kadar yaygın ki [OWASP Top 10 |https://owasp.org/Top10/] listesinde yer alıyor. Savunma, URL'yi getirmeden **önce** doğrulamak ve genel olmayan bir adrese çözülen her şeyi reddetmektir. + + +UrlValidator +============ + +[api:Nette\Http\UrlValidator], bir URL'yi yapılandırılabilir bir ilkeye göre denetler: şema, port, host, userinfo ve host'un çözüldüğü IP adresleri. Temel kullanım tek bir çağrıdır: + +```php +use Nette\Http\UrlValidator; + +if (!(new UrlValidator)->allows($userUrl)) { + return; // güvensiz URL, onu getirme +} +``` + +Varsayılan ilke bilinçli olarak katıdır; yalnızca genel bir IP adresine işaret eden, 443 portundaki `https` adresini kabul eder. Geri kalan her şey (loopback, özel aralıklar, bulut meta verileri dahil link-local, ayrılmış aralıklar) reddedilir; multicast ise koşulsuz reddedilir. Kullanıcının verdiği rastgele URL'leri getirmek için doğru başlangıç noktası budur. + + +İlkeyi Yapılandırma +------------------- + +İlkeyi yapıcı üzerinden biçimlendirirsiniz. Örneğin, herhangi bir portta düz `http` kullanımına ve özel adreslere ulaşmaya izin vermek için (güvenilir bir ağın içinde yararlıdır): + +```php +$validator = new UrlValidator( + schemes: ['http', 'https'], + ports: null, // herhangi bir port + allowPrivateIps: true, +); +``` + +Yaygın bir kalıp, bir host izin listesiyle getirmeyi sabit bir iş ortağı alan adı kümesiyle sınırlamaktır. `*.` öneki herhangi bir alt alan derinliğiyle eşleşir, ama kök alan adıyla eşleşmez; gerekiyorsa her iki biçimi de listeleyin: + +```php +$validator = new UrlValidator( + hostAllowlist: ['example.com', '*.example.com'], +); +``` + +Yapıcı seçeneklerinin tamamı: + +| Parametre | Varsayılan | Anlamı +|--------------------- +| `schemes` | `['https']` | izin verilen şemalar; `[]` her şeyi reddeder +| `ports` | `[443]` | izin verilen portlar, `null` = herhangi biri; şemadan gelen örtük port dikkate alınır +| `allowPrivateIps` | `false` | özel aralıklara izin ver (10/8, 172.16/12, 192.168/16, fc00::/7) +| `allowLoopback` | `false` | loopback'e izin ver (127.0.0.0/8, ::1) +| `allowLinkLocal` | `false` | bulut meta verileri 169.254.169.254 dahil link-local'a izin ver +| `allowReserved` | `false` | IANA tarafından ayrılmış aralıklara izin ver +| `allowUserinfo` | `false` | URL'de `user:pass@` kullanımına izin ver +| `hostAllowlist` | `null` | ayarlıysa host bir desene uymalı; `[]` hepsini reddeder +| `hostBlocklist` | `null` | ayarlıysa host hiçbir desene uymamalı + + +Doğrulama Metotları +------------------- + +Doğrulayıcı üç metot sunar. `allows()`, DNS çözümü dahil tam denetimi çalıştırır; host çözülür ve **her** A/AAAA adresi IP ilkesini geçmelidir: + +```php +(new UrlValidator)->allows($url); // bool +``` + +`allowsWithoutDns()`, DNS çözümünü ve IP aralığı denetimlerini atlar. Onu hızlı bir ön süzgeç olarak ya da DNS doğrulaması getirme katmanına devredildiğinde kullanın: + +```php +(new UrlValidator)->allowsWithoutDns($url); // bool +``` + +Her iki metot da bir dize, bir [UrlImmutable |urls#UrlImmutable] nesnesi ya da `null` (her zaman başarısız olur) kabul eder. + + +DNS Rebinding'i Etkisiz Kılma +----------------------------- + +Doğrulama ile getirme arasında ince bir yarış vardır: bir saldırgan, siz host'u doğrularken güvenli bir IP döndürüp, asıl indirme için DNS'i iç bir IP'ye çevirebilir. Bu açığı kapatmak için `getResolvedIPs()`, doğrulanmış IP adreslerini döndürür; siz de bağlantıyı onlara sabitlersiniz, böylece getirme başka bir yere yönlendirilemez: + +```php +$ips = (new UrlValidator)->getResolvedIPs($url); +if (!$ips) { + return; // güvensiz URL +} + +$ch = curl_init($url); +$host = parse_url($url, PHP_URL_HOST); +curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]); +// ... isteği çalıştır +``` + +Metot, tam ilkeyi geçen IP dizelerinden oluşan bir dizi (önce A kayıtları, sonra AAAA) ya da herhangi bir başarısızlıkta boş bir dizi döndürür. URL'deki bir IP sabiti için adresi doğrudan doğrular ve hiçbir DNS sorgusu yapmaz. + + +IPAddress +========= + +[api:Nette\Http\IPAddress], IPv4 ve IPv6 adresleriyle çalışmaya yarayan değişmez bir değer nesnesidir. `UrlValidator` onu içeride kullanır, ama adresleri sınıflandırdığınız her yerde kendi başına da kullanışlıdır. Yapıcı, geçersiz bir adreste `Nette\InvalidArgumentException` fırlatır: + +```php +use Nette\Http\IPAddress; + +$ip = new IPAddress('169.254.169.254'); +echo $ip; // '169.254.169.254' +``` + +İstisna istemediğinizde `tryFrom()` factory'sini ya da `isValid()` denetleyicisini kullanın: + +```php +$ip = IPAddress::tryFrom($input); // ?IPAddress +IPAddress::isValid($input); // bool +``` + + +Adres Sınıflandırması +--------------------- + +Yüklemler, bir adresin hangi sınıfa ait olduğunu söyler. En önemlisi `isPublic()`; yalnızca genel olarak yönlendirilebilir adreslerde true döndürür ve bir SSRF koruması tam da bunu ister: + +```php +$ip = new IPAddress('169.254.169.254'); +$ip->isPublic(); // false +$ip->isLinkLocal(); // true (bulut meta verileri aralığı) +``` + +Yüklemlerin tamamı: + +| Metot | Neyi sınar +|-------------------- +| `isPublic()` | genel olarak yönlendirilebilir (aşağıdakilerin hiçbiri değil) +| `isPrivate()` | RFC 1918 / 4193 özel aralıkları +| `isLoopback()` | 127.0.0.0/8, ::1 +| `isLinkLocal()` | 169.254.0.0/16 (bulut meta verileri dahil), fe80::/10 +| `isMulticast()` | 224.0.0.0/4, ff00::/8 +| `isReserved()` | IANA tarafından ayrılmış (dokümantasyon, CGNAT, gelecekte kullanım, …) + + +Aralık Üyeliği +-------------- + +`isInRange()`, adresin bir CIDR bloğunun içine düşüp düşmediğini sınar. Önekli bir ağ ya da tam eşleşme için çıplak bir adres verebilirsiniz (IPv4'te örtük /32, IPv6'da /128): + +```php +$ip = new IPAddress('192.168.1.50'); +$ip->isInRange('192.168.0.0/16'); // true +$ip->isInRange('10.0.0.1'); // false (tam eşleşme) +``` + +Bozuk girdi ya da farklı bir IP ailesi `false` döndürür. + + +IPv4 Eşlemeli IPv6 +------------------ + +IPv4 eşlemeli IPv6 olarak yazılan adresler (`::ffff:127.0.0.1` gibi), saf filtreleri atlatmanın klasik bir yoludur. `IPAddress` onları normalleştirir, böylece aralık yüklemleri kılık değiştirmeyi görür: + +```php +$ip = new IPAddress('::ffff:127.0.0.1'); +$ip->isLoopback(); // true +$ip->isIPv4Mapped(); // true +$ip->toIPv4(); // IPAddress('127.0.0.1') +``` + +`isIPv4()` ve `isIPv6()` metotları metinsel biçimi bildirir: eşlemeli bir adres IPv4 değil IPv6'dır. diff --git a/http/tr/upgrading.texy b/http/tr/upgrading.texy new file mode 100644 index 0000000000..f300542bc7 --- /dev/null +++ b/http/tr/upgrading.texy @@ -0,0 +1,54 @@ +Yükseltme +********* + + +Sürüm 3.4'e Yükseltme +===================== + +Gereken en düşük PHP sürümü 8.3'tür. + +- `Request::isSameSite()` metodu kullanımdan kaldırıldı; yerine, isteğin kaynağını `Sec-Fetch-*` header'larından belirleyen `isFrom()` geldi. Formların ve sinyallerin otomatik koruması daha isabetli oluyor ve bir davranış değişiyor: doğrudan gezinme (bir yer imi, elle yazılan bir adres, e-postadaki bir bağlantı) artık aynı site sayılmıyor. Bir sinyal e-postadaki eylem bağlantılarına dayanıyorsa, onu `#[Requires(sameOrigin: false)]` ile işaretleyin. +- `_nss` çerezi artık yalnızca `Sec-Fetch-Site` header'ını göndermeyen tarayıcılara gönderiliyor +- `setCookie()`, `Max-Age` niteliğini gönderiyor ve `SameSite=None` ile bölümlenmiş çerezlerde `Secure` bayrağını zorluyor +- `SameSite` enum'u, kullanımdan kaldırılan `IResponse::SameSiteLax` gibi sabitlerin yerini alıyor +- son kullanma her yerde aynı şekilde yorumlanıyor: sayı göreli saniye sayısı, dize ise bir aralık ya da tarihtir. Mutlak bir UNIX zaman damgası vermek kullanımdan kaldırıldı ve oturum çerezi `0` yerine `null` ile temsil ediliyor. +- kullanımdan kaldırılmış `Request::getRemoteHost()` metodu `null` döndürüyor +- uzun süredir kullanımdan kaldırılmış `Nette\Http\UserStorage` sınıfı kaldırıldı + +`Sec-Fetch-*` header'larına geçişin tüm hikâyesi [Çeyrek yüzyıllık CSRF |https://blog.nette.org/en/quarter-century-of-csrf] yazısında anlatılıyor. + + +Sürüm 3.2'ye Yükseltme +====================== + +- HTTP Basic Authentication kimlik bilgileri artık `Url` nesnesinin parçası değil, dolayısıyla `$url->getUser()` ve `$url->getPassword()` boş dize döndürüyor. Onları yeni `$request->getBasicCredentials()` metoduyla okuyun. + +Bu değişikliğin nedenleri [Nette Http 3.2: kimlik bilgilerine erişim değişiyor |https://blog.nette.org/en/nette-http-3-2-change-access-to-credentials] yazısında açıklanıyor. + + +Sürüm 3.1'e Yükseltme +===================== + +- çerezler `sameSite: Lax` bayrağıyla gönderiliyor +- `cookieSecure` artık varsayılan olarak 'auto' +- `session.cookieSecure` seçeneği kullanımdan kaldırıldı; yerine `http.cookieSecure` kullanılıyor +- `nette-samesite` çerezinin adı `_nss` oldu +- `Nette\Http\Request::getFile()` bir anahtar dizisi kabul ediyor ve `FileUpload|null` döndürüyor +- `Nette\Http\Session::getCookieParameters()` kullanımdan kaldırıldı +- `Nette\Http\FileUpload::getName()` metodunun adı `getUntrustedName()` oldu +- `Nette\Http\Url`: `getBasePath()`, `getBaseUrl()` ve `getRelativeUrl()` kullanımdan kaldırıldı (bu metotlar `UrlScript` sınıfının parçası) +- `Nette\Http\Response::$cookieHttpOnly` kullanımdan kaldırıldı +- `Nette\Http\FileUpload::getImageSize()` `[genişlik, yükseklik]` çiftini döndürüyor +- `autoStart: smart` (varsayılan) ile oturum, yalnızca tarayıcı bir oturum çerezi gönderdi diye uygulama başlar başlamaz artık başlatılmıyor; ilk okuma ya da yazmada başlıyor. `always` ve `never` değerleri eklendi. +- tarayıcı, karşılığında oturum bulunmayan bir oturum ID'si gönderdiğinde Nette yeni bir oturum oluşturmak yerine çerezi siliyor +- oturum bölümlerine erişmek için `set()`, `get()` ve `remove()` metotlarını yeğleyin; özellik erişiminin aksine okumayı yazmadan doğru ayırt eder ve oturumu gereksiz yere başlatmazlar +- `cookiePath` ve `cookieDomain` varsayılan değerleri yapılandırmada ayarlanabilir + +Oturumların davranışı [Nette Http 3.1: çok daha akıllı oturumlar |https://blog.nette.org/en/nette-http-3-1-much-smarter-sessions] yazısında ayrıntılı anlatılıyor. + + +Sürüm 3.0'a Yükseltme +===================== + +- `Nette\Http\UrlScript` nesnesi (örneğin `Nette\Http\Request::getUrl()` metodunun döndürdüğü) artık değişmez +- `new Nette\Http\Url('abcd')` içinde `abcd` alan adını değil yolu temsil ediyor; 3.0'dan beri `(new Nette\Http\Url('abcd'))->setScheme('http')` önceki `http://abcd` yerine doğru biçimde `http:abcd` üretiyor diff --git a/http/tr/urls.texy b/http/tr/urls.texy index 232879609c..6b7599fdfa 100644 --- a/http/tr/urls.texy +++ b/http/tr/urls.texy @@ -2,7 +2,7 @@ URL'lerle Çalışma ***************** .[perex] -[#Url], [#UrlImmutable] ve [#UrlScript] sınıfları, URL'leri kolayca oluşturmayı, ayrıştırmayı ve işlemeyi sağlar. +[#Url], [#UrlImmutable] ve [#UrlScript] sınıfları, URL'leri üretmeyi, ayrıştırmayı ve onlarla çalışmayı kolaylaştırır. → [Kurulum ve gereksinimler |@home#Kurulum] @@ -10,10 +10,10 @@ URL'lerle Çalışma Url === -[api:Nette\Http\Url] sınıfı, URL'ler ve bu çizimde yakalanan tek tek bileşenleriyle kolayca çalışmanıza olanak tanır: +[api:Nette\Http\Url] sınıfı, URL'lerle ve tek tek bileşenleriyle kolayca çalışmaya olanak tanır; şu şemada gösterildiği gibi: /--pre - şema kullanıcı şifre ana bilgisayar port yol sorgu fragment + scheme user password host port path query fragment | | | | | | | | /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> @@ -22,7 +22,7 @@ Url hostUrl authority \-- -URL oluşturma sezgiseldir: +URL üretmek sezgiseldir: ```php use Nette\Http\Url; @@ -36,7 +36,7 @@ $url->setScheme('https') echo $url; // 'https://localhost/edit?foo=bar' ``` -Ayrıca bir URL'yi ayrıştırabilir ve daha fazla işleyebilirsiniz: +Bir URL'yi ayrıştırıp sonra onunla çalışabilirsiniz de: ```php $url = new Url( @@ -44,7 +44,7 @@ $url = new Url( ); ``` -`Url` sınıfı `JsonSerializable` arayüzünü uygular ve `__toString()` metoduna sahiptir, böylece nesne yazdırılabilir veya `json_encode()`'a iletilen verilerde kullanılabilir. +`Url` sınıfı `JsonSerializable` arayüzünü uygular ve bir `__toString()` metodu vardır; dolayısıyla nesne yazdırılabilir ya da `json_encode()` fonksiyonuna verilen veride kullanılabilir. ```php echo $url; @@ -52,13 +52,13 @@ echo json_encode([$url]); ``` -URL Bileşenleri .[method] -------------------------- +URL Bileşenleri +--------------- -Tek tek URL bileşenlerini döndürmek veya değiştirmek için şu metotlar kullanılabilir: +Tek tek URL bileşenlerini almak ya da değiştirmek için şu metotlar kullanılabilir: .[language-php] -| Ayarlayıcı | Alıcı | Döndürülen değer +| Setter | Getter | Döndürülen değer |-------------------------------------------------------------------------------------------- | `setScheme(string $scheme)` | `getScheme(): string` | `'http'` | `setUser(string $user)` | `getUser(): string` | `'john'` @@ -71,22 +71,25 @@ Tek tek URL bileşenlerini döndürmek veya değiştirmek için şu metotlar kul | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | tüm URL +| | `getAbsoluteUrl(): string` | URL'nin tamamı -Uyarı: [HTTP isteğinden |request] alınan bir URL ile çalışırken, tarayıcı sunucuya göndermediği için fragment içermeyeceğini unutmayın. +`getUser()`, `getPassword()`, `setUser()` ve `setPassword()` metotları kullanımdan kaldırıldı; çünkü kimlik bilgilerini doğrudan URL'ye gömmek önerilmez. -Ayrıca tek tek sorgu parametreleriyle de çalışabiliriz: +Uyarı: Bir [HTTP isteğinden |request] alınan URL ile çalışırken, tarayıcı fragment'ı sunucuya göndermediği için onun fragment içermeyeceğini unutmayın. + +Tek tek sorgu parametreleriyle de şunları kullanarak çalışabiliriz: .[language-php] -| Ayarlayıcı | Alıcı +| Setter | Getter |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] ------------------------------------------- -Ana bilgisayarın sağ veya sol kısmını döndürür. Ana bilgisayar `www.nette.org` ise şu şekilde çalışır: +Host'un sağ ya da sol parçasını döndürür. Host `www.nette.org` ise şöyle çalışır: .[language-php] | `getDomain(1)` | `'org'` @@ -98,18 +101,23 @@ Ana bilgisayarın sağ veya sol kısmını döndürür. Ana bilgisayar `www.nett | `getDomain(-3)` | `''` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -İki URL'nin aynı olup olmadığını doğrular. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +İki URL'nin özdeş olup olmadığını denetler. ```php $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +URL'yi kurallı biçime dönüştürür. Bu, host adını küçük harfe çevirir ve yolu normalleştirir (yüzde kodlaması ve gereksiz karakterlerin kaldırılması). Sorgu dizesi değiştirilmeden bırakılır. + + Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ---------------------------------------------------------------- -Bir URL'nin mutlak olup olmadığını doğrular. Bir URL, bir şema (ör. http, https, ftp) ve ardından iki nokta üst üste ile başlıyorsa mutlak kabul edilir. +Bir URL'nin mutlak olup olmadığını denetler. Bir URL, bir şemayla (örneğin http, https, ftp) ve ardından iki nokta üst üste ile başlıyorsa mutlak sayılır. ```php Url::isAbsolute('https://nette.org'); // true @@ -119,7 +127,7 @@ Url::isAbsolute('//nette.org'); // false Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} -------------------------------------------------------------------------- -Özel `.` ve `..` segmentlerini kaldırarak URL'deki yolu normalleştirir. Metot, gereksiz yol öğelerini web tarayıcılarının yaptığı gibi kaldırır. +Özel `.` ve `..` parçalarını kaldırarak bir URL yolunu normalleştirir. Bu metot, gereksiz yol öğelerini web tarayıcılarının yaptığı gibi kaldırır. ```php Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' @@ -131,24 +139,24 @@ Url::removeDotSegments('./today/../file.txt'); // 'file.txt' UrlImmutable ============ -[api:Nette\Http\UrlImmutable] sınıfı, [#Url] sınıfının değişmez (immutable) bir alternatifidir (PHP'deki `DateTimeImmutable`'ın `DateTime`'ın değişmez alternatifi olması gibi). Ayarlayıcılar yerine, nesneyi değiştirmeyen ancak değiştirilmiş bir değere sahip yeni örnekler döndüren wither'lara sahiptir: +[api:Nette\Http\UrlImmutable] sınıfı, [#Url] sınıfının değişmez alternatifidir (PHP'de `DateTimeImmutable` sınıfının `DateTime` sınıfının değişmez alternatifi olmasına benzer). Setter yerine "wither"ları vardır; bunlar nesneyi değiştirmez, değiştirilmiş değere sahip yeni örnekler döndürür: ```php use Nette\Http\UrlImmutable; $url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', + 'https://nette.org:8080/en/download?name=param#footer', ); $newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); + ->withHost('example.com') + ->withPath('/en/') + ->withQueryParameter('name', 'value'); -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' +echo $newUrl; // 'https://example.com:8080/en/?name=value#footer' ``` -`UrlImmutable` sınıfı `JsonSerializable` arayüzünü uygular ve `__toString()` metoduna sahiptir, böylece nesne yazdırılabilir veya `json_encode()`'a iletilen verilerde kullanılabilir. +`UrlImmutable` sınıfı `JsonSerializable` arayüzünü uygular ve bir `__toString()` metodu vardır; dolayısıyla nesne yazdırılabilir ya da `json_encode()` fonksiyonuna verilen veride kullanılabilir. ```php echo $url; @@ -156,13 +164,13 @@ echo json_encode([$url]); ``` -URL Bileşenleri .[method] -------------------------- +URL Bileşenleri +--------------- -Tek tek URL bileşenlerini döndürmek veya değiştirmek için şu metotlar kullanılır: +Tek tek URL bileşenlerini almak ya da değiştirmek için şu metotlar kullanılabilir: .[language-php] -| Wither | Alıcı | Döndürülen değer +| Wither | Getter | Döndürülen değer |-------------------------------------------------------------------------------------------- | `withScheme(string $scheme)` | `getScheme(): string` | `'http'` | `withUser(string $user)` | `getUser(): string` | `'john'` @@ -175,14 +183,14 @@ Tek tek URL bileşenlerini döndürmek veya değiştirmek için şu metotlar kul | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` | | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` | | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | tüm URL +| | `getAbsoluteUrl(): string` | URL'nin tamamı -`withoutUserInfo()` metodu `user` ve `password`'ü kaldırır. +`getUser()`, `getPassword()`, `withUser()`, `withPassword()` ve `withoutUserInfo()` metotları kullanımdan kaldırıldı; çünkü kimlik bilgilerini doğrudan URL'ye gömmek önerilmez. -Ayrıca tek tek sorgu parametreleriyle de çalışabiliriz: +Tek tek sorgu parametreleriyle de şunları kullanarak çalışabiliriz: .[language-php] -| Wither | Alıcı +| Wither | Getter |----------------------------------------------- | `withQuery(string\|array $query)` | `getQueryParameters(): array` | `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` @@ -190,7 +198,7 @@ Ayrıca tek tek sorgu parametreleriyle de çalışabiliriz: getDomain(int $level = 2): string .[method] ------------------------------------------- -Ana bilgisayarın sağ veya sol kısmını döndürür. Ana bilgisayar `www.nette.org` ise şu şekilde çalışır: +Host'un sağ ya da sol parçasını döndürür. Host `www.nette.org` ise şöyle çalışır: .[language-php] | `getDomain(1)` | `'org'` @@ -204,11 +212,11 @@ Ana bilgisayarın sağ veya sol kısmını döndürür. Ana bilgisayar `www.nett resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ---------------------------------------------------------------------- -Mutlak URL'yi tarayıcının HTML sayfasındaki bağlantıları işlediği gibi türetir: -- bağlantı mutlak bir URL ise (şema içeriyorsa), değişiklik yapılmadan kullanılır -- bağlantı `//` ile başlıyorsa, yalnızca geçerli URL'den şema alınır -- bağlantı `/` ile başlıyorsa, alan adının kökünden mutlak bir yol oluşturulur -- diğer durumlarda, URL geçerli yola göre göreceli olarak oluşturulur +Mutlak bir URL'yi, bir tarayıcının HTML sayfasındaki bağlantıları işlediği şekilde çözer: +- bağlantı mutlak bir URL ise (şema içeriyorsa) değiştirilmeden kullanılır +- bağlantı `//` ile başlıyorsa yalnızca geçerli URL'nin şeması alınır +- bağlantı `/` ile başlıyorsa alan adı kökünden mutlak bir yol oluşturulur +- diğer durumlarda URL, geçerli yola göre kurulur ```php $url = new UrlImmutable('https://example.com/path/page'); @@ -218,9 +226,9 @@ echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.ht ``` -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -İki URL'nin aynı olup olmadığını doğrular. +isEqual(string|Url $url): bool .[method] +---------------------------------------- +İki URL'nin özdeş olup olmadığını denetler. ```php $url->isEqual('https://nette.org'); @@ -230,9 +238,9 @@ $url->isEqual('https://nette.org'); UrlScript ========= -[api:Nette\Http\UrlScript] sınıfı, [#UrlImmutable] sınıfının bir alt sınıfıdır ve onu projenin kök dizini vb. gibi ek sanal URL bileşenleriyle genişletir. Üst sınıfı gibi, değişmez (immutable) bir nesnedir. +[api:Nette\Http\UrlScript] sınıfı, [#UrlImmutable] sınıfının bir torunudur ve onu projenin kök dizini gibi ek sanal URL bileşenleriyle genişletir. Üst sınıfı gibi o da değişmez bir nesnedir. -Aşağıdaki diyagram, UrlScript'in tanıdığı bileşenleri gösterir: +Aşağıdaki şema, UrlScript'in tanıdığı bileşenleri gösterir: /--pre baseUrl basePath relativePath relativeUrl @@ -244,23 +252,23 @@ Aşağıdaki diyagram, UrlScript'in tanıdığı bileşenleri gösterir: scriptPath pathInfo \-- -- `baseUrl`, alan adı ve uygulamanın kök dizinine giden yolun bir bölümü dahil olmak üzere uygulamanın temel URL adresidir -- `basePath`, uygulamanın kök dizinine giden yolun bir bölümüdür +- `baseUrl`, uygulamanın temel URL'sidir; alan adını ve uygulamanın kök dizinine giden yol parçasını içerir +- `basePath`, uygulamanın kök dizinine giden yol parçasıdır - `scriptPath`, geçerli betiğe giden yoldur -- `relativePath`, basePath'e göre betiğin adıdır (ve muhtemelen ek yol segmentleri) -- `relativeUrl`, sorgu dizesi ve fragment dahil olmak üzere baseUrl'den sonraki URL'nin tüm bölümüdür. -- `pathInfo`, günümüzde betik adından sonraki URL'nin nadiren kullanılan bir bölümüdür +- `relativePath`, `basePath` değerine göre betiğin adıdır (ve olasılıkla ek yol parçalarıdır) +- `relativeUrl`, URL'nin `baseUrl` sonrasındaki tüm parçasıdır; sorgu dizesi ve fragment dahil +- `pathInfo`, URL'nin betik adından sonraki, artık ender kullanılan parçasıdır -URL'nin bölümlerini döndürmek için şu metotlar kullanılabilir: +URL'nin bu parçalarını almak için şu metotlar kullanılabilir: .[language-php] -| Alıcı | Döndürülen değer +| Getter | Döndürülen değer |------------------------------------------------ | `getScriptPath(): string` | `'/admin/script.php'` | `getBasePath(): string` | `'/admin/'` | `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` +| `getRelativePath(): string` | `'script.php/pathinfo/'` | `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` | `getPathInfo(): string` | `'/pathinfo/'` -`UrlScript` nesneleri genellikle doğrudan oluşturulmaz, ancak geçerli HTTP isteği için zaten doğru şekilde ayarlanmış bileşenlerle [Nette\Http\Request::getUrl()|request] metodu tarafından döndürülür. +`UrlScript` nesnelerini genellikle doğrudan oluşturmayız; bunun yerine [Nette\Http\Request::getUrl() |request] metodu onu, bileşenleri geçerli HTTP isteği için zaten doğru ayarlanmış olarak döndürür. diff --git a/http/uk/@home.texy b/http/uk/@home.texy deleted file mode 100644 index dfc6127791..0000000000 --- a/http/uk/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Пакет `nette/http` інкапсулює [HTTP request|request] та [response |response], роботу з [sessions |sessions] та [парсинг і складання URL |urls]. - - -Встановлення ------------- - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/uk/@left-menu.texy b/http/uk/@left-menu.texy deleted file mode 100644 index fb27b98ab9..0000000000 --- a/http/uk/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Вступ |@home] -- [HTTP запит|request] -- [HTTP відповідь|response] -- [Сесії |Sessions] -- [Утиліти URL |urls] -- [Конфігурація |configuration] diff --git a/http/uk/@meta.texy b/http/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/http/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/http/uk/configuration.texy b/http/uk/configuration.texy deleted file mode 100644 index 1fddd1b3be..0000000000 --- a/http/uk/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Конфігурація HTTP -***************** - -.[perex] -Огляд параметрів конфігурації для Nette HTTP. - -Якщо ви не використовуєте весь фреймворк, а лише цю бібліотеку, прочитайте, [як завантажити конфігурацію|bootstrap:]. - - -HTTP-заголовки -============== - -```neon -http: - # заголовки, які надсилаються з кожним запитом - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # впливає на заголовок X-Frame-Options - frames: ... # (string|bool) за замовчуванням 'SAMEORIGIN' -``` - -Фреймворк з міркувань безпеки надсилає заголовок `X-Frame-Options: SAMEORIGIN`, який вказує, що сторінку можна відображати всередині іншої сторінки (в елементі `<iframe>`) лише якщо вона знаходиться на тому ж домені. Це може бути небажаним у деяких ситуаціях (наприклад, якщо ви розробляєте програму для Facebook), тому поведінку можна змінити, встановивши `frames: http://allowed-host.com` або `frames: true`. - - -Content Security Policy ------------------------ - -Легко можна створювати заголовки `Content-Security-Policy` (далі CSP), їх опис ви знайдете в [опису CSP |https://content-security-policy.com]. Директиви CSP (наприклад, `script-src`) можуть бути записані або як рядки відповідно до специфікації, або як масив значень для кращої читабельності. Тоді не потрібно навколо ключових слів, як-от `'self'`, ставити лапки. Nette також автоматично генерує значення `nonce`, тому в заголовку буде, наприклад, `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # рядок у форматі відповідно до специфікації CSP - default-src: "'self' https://example.com" - - # масив значень - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool у випадку перемикачів - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -У шаблонах використовуйте `<script n:nonce>...</script>`, і значення nonce доповниться автоматично. Робити безпечні сайти в Nette справді легко. - -Подібно можна створити й заголовки `Content-Security-Policy-Report-Only` (які можна використовувати одночасно з CSP) та [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP cookie ------------ - -Можна змінити стандартні значення деяких параметрів методу [Nette\Http\Response::setCookie() |response#setCookie] та сесії. - -```neon -http: - # область дії cookie за шляхом - cookiePath: ... # (string) за замовчуванням '/' - - # домени, які приймають cookie - cookieDomain: 'example.com' # (string|domain) за замовчуванням не встановлено - - # надсилати cookie лише через HTTPS? - cookieSecure: ... # (bool|auto) за замовчуванням auto - - # вимкне надсилання cookie, яку Nette використовує як захист від CSRF - disableNetteCookie: ... # (bool) за замовчуванням false -``` - -Атрибут `cookieDomain` визначає, які домени можуть приймати cookie. Якщо він не вказаний, cookie приймає той самий (під)домен, що й встановив його, *але не* його піддомени. Якщо `cookieDomain` вказаний, піддомени також включаються. Тому вказання `cookieDomain` є менш обмежувальним, ніж його відсутність. - -Наприклад, при `cookieDomain: nette.org` cookies доступні також на всіх піддоменах, таких як `doc.nette.org`. Того ж можна досягти також за допомогою спеціального значення `domain`, тобто `cookieDomain: domain`. - -Стандартне значення `auto` для атрибута `cookieSecure` означає, що якщо сайт працює на HTTPS, cookies будуть надсилатися з прапором `Secure` і, отже, будуть доступні лише через HTTPS. - - -HTTP-проксі ------------ - -Якщо сайт працює за HTTP-проксі, вкажіть його IP-адресу, щоб правильно працювало визначення з'єднання через HTTPS, а також IP-адреси клієнта. Тобто, щоб функції [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] та [isSecured() |request#isSecured] повертали правильні значення, а в шаблонах генерувалися посилання з протоколом `https:`. - -```neon -http: - # IP-адреса, діапазон (напр. 127.0.0.1/8) або масив цих значень - proxy: 127.0.0.1 # (string|string[]) за замовчуванням не встановлено -``` - - -Сесія -===== - -Базові налаштування [сесій |sessions]: - -```neon -session: - # показувати панель сесії в Tracy Bar? - debugger: ... # (bool) за замовчуванням false - - # час неактивності, після якого сесія закінчиться - expiration: 14 days # (string) за замовчуванням '3 hours' - - # коли має запускатися сесія? - autoStart: ... # (smart|always|never) за замовчуванням 'smart' - - # обробник, сервіс, що реалізує інтерфейс SessionHandlerInterface - handler: @handlerService -``` - -Опція `autoStart` керує тим, коли має запускатися сесія. Значення `always` означає, що сесія запуститься завжди при запуску програми. Значення `smart` означає, що сесія запуститься при старті програми лише тоді, коли вона вже існує, або в момент, коли ми хочемо з неї читати або в неї записувати. І нарешті, значення `never` забороняє автоматичний запуск сесії. - -Далі можна налаштовувати всі PHP [директиви сесії |https://www.php.net/manual/en/session.configuration.php] (у форматі camelCase) та також [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Приклад: - -```neon -session: - # 'session.name' запишемо як 'name' - name: MYID - - # 'session.save_path' запишемо як 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Session cookie --------------- - -Session cookie надсилається з тими ж параметрами, що й [інші cookie |#HTTP cookie], але ці ви можете для неї змінити: - -```neon -session: - # домени, які приймають cookie - cookieDomain: 'example.com' # (string|domain) - - # обмеження при доступі з іншого домену - cookieSamesite: None # (Strict|Lax|None) за замовчуванням Lax -``` - -Атрибут `cookieSamesite` впливає на те, чи буде cookie надіслано при [доступі з іншого домену |nette:glossary#SameSite cookie], що забезпечує певний захист від атак [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Сервіси DI -========== - -Ці сервіси додаються до DI-контейнера: - -| Назва | Тип | Опис -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP-запит| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP-відповідь| response] -| `session.session` | [api:Nette\Http\Session] | [керування сесіями| sessions] diff --git a/http/uk/request.texy b/http/uk/request.texy deleted file mode 100644 index b3537c1f0d..0000000000 --- a/http/uk/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -HTTP-запит -********** - -.[perex] -Nette інкапсулює HTTP-запит в об'єкти зі зрозумілим API і водночас надає фільтр санітизації. - -HTTP-запит представляє об'єкт [api:Nette\Http\Request]. Якщо ви працюєте з Nette, цей об'єкт автоматично створюється фреймворком, і ви можете отримати його за допомогою [впровадження залежностей |dependency-injection:passing-dependencies]. У презентерах достатньо лише викликати метод `$this->getHttpRequest()`. Якщо ви працюєте поза Nette Framework, ви можете створити об'єкт за допомогою [#RequestFactory]. - -Великою перевагою Nette є те, що при створенні об'єкта він автоматично очищає всі вхідні параметри GET, POST, COOKIE, а також URL від керуючих символів та недійсних UTF-8 послідовностей. З цими даними потім можна безпечно працювати далі. Очищені дані потім використовуються в презентерах та формах. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Nette\Http\Request -================== - -Цей об'єкт є immutable (незмінним). Він не має жодних сеттерів, має лише один так званий wither `withUrl()`, який не змінює об'єкт, а повертає новий екземпляр зі зміненим значенням. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Повертає клон з іншим URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Повертає URL запиту як об'єкт [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/uk/?action=edit -echo $url->getHost(); // nette.org -``` - -Попередження: браузери не надсилають на сервер фрагмент, тому `$url->getFragment()` повертатиме порожній рядок. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Повертає параметри GET-запиту. - -```php -$all = $httpRequest->getQuery(); // повертає масив усіх параметрів з URL -$id = $httpRequest->getQuery('id'); // повертає GET-параметр 'id' (або null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Повертає параметри POST-запиту. - -```php -$all = $httpRequest->getPost(); // повертає масив усіх параметрів з POST -$id = $httpRequest->getPost('id'); // повертає POST-параметр 'id' (або null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Повертає [завантаження |#Завантажені файли] як об'єкт [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // чи був якийсь файл завантажений? - $file->getUntrustedName(); // ім'я файлу, надіслане користувачем - $file->getSanitizedName(); // ім'я без небезпечних символів -} -``` - -Для доступу до вкладеної структури вкажіть масив ключів. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Оскільки не можна довіряти даним ззовні і, отже, покладатися на структуру файлів, цей спосіб є безпечнішим, ніж, наприклад, `$request->getFiles()['my-form']['details']['avatar']`, який може зазнати невдачі. - - -getFiles(): array .[method] ---------------------------- -Повертає дерево [всіх завантажень |#Завантажені файли] у нормалізованій структурі, листками якої є об'єкти [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Повертає cookie або `null`, якщо вона не існує. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Повертає всі cookies. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Повертає HTTP-метод, яким був зроблений запит. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Перевіряє HTTP-метод, яким був зроблений запит. Параметр нечутливий до регістру. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Повертає HTTP-заголовок або `null`, якщо він не існує. Параметр нечутливий до регістру. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Повертає всі HTTP-заголовки як асоціативний масив. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Чи є з'єднання зашифрованим (HTTPS)? Для правильної роботи може знадобитися [налаштувати проксі |configuration#HTTP-проксі]. - - -isSameSite(): bool .[method] ----------------------------- -Чи надходить запит з того самого (під)домену і чи ініційований він кліком на посилання? Nette для визначення використовує cookie `_nss` (раніше `nette-samesite`). - - -isAjax(): bool .[method] ------------------------- -Чи це AJAX-запит? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Повертає IP-адресу користувача. Для правильної роботи може знадобитися [налаштувати проксі |configuration#HTTP-проксі]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Повертає DNS-перетворення IP-адреси користувача. Для правильної роботи може знадобитися [налаштувати проксі |configuration#HTTP-проксі]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Повертає облікові дані для [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Повертає тіло HTTP-запиту. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Визначає мову. Як параметр `$langs` передаємо масив мов, які підтримує програма, і вона поверне ту, яку браузер відвідувача хотів би бачити найбільше. Це не магія, просто використовується заголовок `Accept-Language`. Якщо збігу не знайдено, повертає `null`. - -```php -// браузер надсилає, напр., Accept-Language: uk,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'uk']; // мови, підтримувані програмою -echo $httpRequest->detectLanguage($langs); // uk -``` - - -RequestFactory -============== - -Клас [api:Nette\Http\RequestFactory] служить для створення екземпляра `Nette\Http\Request`, який представляє поточний HTTP-запит. (Якщо ви працюєте з Nette, об'єкт HTTP-запиту автоматично створюється фреймворком.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Метод `fromGlobals()` створює об'єкт запиту на основі поточних глобальних змінних PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` та `$_SERVER`). При створенні об'єкта він автоматично очищає всі вхідні параметри GET, POST, COOKIE, а також URL від керуючих символів та недійсних UTF-8 послідовностей, що забезпечує безпеку при подальшій роботі з цими даними. - -RequestFactory можна конфігурувати перед викликом `fromGlobals()`: - -- методом `$factory->setBinary()` вимкнете автоматичне очищення вхідних параметрів від керуючих символів та недійсних UTF-8 послідовностей. -- методом `$factory->setProxy(...)` вкажете IP-адресу [проксі-сервера |configuration#HTTP-проксі], що необхідно для правильного визначення IP-адреси користувача. - -RequestFactory дозволяє визначати фільтри, які автоматично трансформують частини URL запиту. Ці фільтри видаляють небажані символи з URL, які там можуть бути вставлені, наприклад, неправильною реалізацією систем коментарів на різних сайтах: - -```php -// видалення пробілів зі шляху -$requestFactory->urlFilters['path']['%20'] = ''; - -// видалення крапки, коми або правої дужки з кінця URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// очищення шляху від подвійних слешів (стандартний фільтр) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Перший ключ `'path'` або `'url'` визначає, до якої частини URL застосовується фільтр. Другий ключ — це регулярний вираз, який потрібно знайти, а значення — це заміна, яка використовується замість знайденого тексту. - - -Завантажені файли -================= - -Метод `Nette\Http\Request::getFiles()` повертає масив усіх завантажень у нормалізованій структурі, листками якої є об'єкти [api:Nette\Http\FileUpload]. Вони інкапсулюють дані, надіслані елементом форми `<input type=file>`. - -Структура відображає іменування елементів у HTML. У найпростішому випадку це може бути єдиний іменований елемент форми, надісланий як: - -```latte -<input type="file" name="avatar"> -``` - -У цьому випадку `$request->getFiles()` повертає масив: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -Об'єкт `FileUpload` створюється навіть у випадку, якщо користувач не надіслав жодного файлу або надсилання не вдалося. Чи був файл надісланий, повертає метод `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -У випадку назви елемента, що використовує нотацію для масиву: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -повернене дерево виглядає так: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Можна створити і масив файлів: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -У такому випадку структура виглядає так: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -Доступ до індексу 1 вкладеного масиву найкраще отримати так: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Оскільки не можна довіряти даним ззовні і, отже, покладатися на структуру файлів, цей спосіб є безпечнішим, ніж, наприклад, `$request->getFiles()['my-form']['details']['avatars'][1]`, який може зазнати невдачі. - - -Огляд методів `FileUpload` .{toc: FileUpload} ---------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Повертає `true`, якщо користувач завантажив якийсь файл. - - -isOk(): bool .[method] ----------------------- -Повертає `true`, якщо файл був завантажений успішно. - - -getError(): int .[method] -------------------------- -Повертає код помилки при завантаженні файлу. Це одна з констант [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. У випадку, якщо завантаження пройшло успішно, повертає `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Переміщує завантажений файл у нове місце. Якщо цільовий файл вже існує, він буде перезаписаний. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Повертає вміст завантаженого файлу. У випадку, якщо завантаження не було успішним, повертає `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Визначає MIME content type завантаженого файлу на основі його сигнатури. У випадку, якщо завантаження не було успішним або визначення не вдалося, повертає `null`. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Повертає оригінальну назву файлу, як її надіслав браузер. - -.[caution] -Не довіряйте значенню, повернутому цим методом. Клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати вашу програму. - - -getSanitizedName(): string .[method] ------------------------------------- -Повертає санітизовану назву файлу. Містить лише ASCII-символи `[a-zA-Z0-9.-]`. Якщо назва не містить таких символів, поверне `'unknown'`. Якщо файл є зображенням у форматі JPEG, PNG, GIF, WebP або AVIF, поверне також правильне розширення. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Повертає відповідне розширення файлу (без крапки), що відповідає виявленому MIME-типу. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Повертає оригінальний шлях до файлу, як його надіслав браузер при завантаженні папки. Повний шлях доступний лише в PHP 8.1 та вище. У попередніх версіях цей метод повертає оригінальну назву файлу. - -.[caution] -Не довіряйте значенню, повернутому цим методом. Клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати вашу програму. - - -getSize(): int .[method] ------------------------- -Повертає розмір завантаженого файлу. У випадку, якщо завантаження не було успішним, повертає `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Повертає шлях до тимчасового розташування завантаженого файлу. У випадку, якщо завантаження не було успішним, повертає `''`. - - -isImage(): bool .[method] -------------------------- -Повертає `true`, якщо завантажений файл є зображенням у форматі JPEG, PNG, GIF, WebP або AVIF. Визначення відбувається на основі його сигнатури і не перевіряється цілісність усього файлу. Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |#toImage]. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Повертає пару `[ширина, висота]` з розмірами завантаженого зображення. У випадку, якщо завантаження не було успішним або це не дійсне зображення, повертає `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Завантажує зображення як об'єкт [Image|utils:images]. У випадку, якщо завантаження не було успішним або це не дійсне зображення, викине виняток `Nette\Utils\ImageException`. diff --git a/http/uk/response.texy b/http/uk/response.texy deleted file mode 100644 index d9dcc7e50f..0000000000 --- a/http/uk/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -HTTP-відповідь -************** - -.[perex] -Nette інкапсулює HTTP-відповідь в об'єкти зі зрозумілим API. - -HTTP-відповідь представляє об'єкт [api:Nette\Http\Response]. Якщо ви працюєте з Nette, цей об'єкт автоматично створюється фреймворком, і ви можете отримати його за допомогою [впровадження залежностей |dependency-injection:passing-dependencies]. У презентерах достатньо лише викликати метод `$this->getHttpResponse()`. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Nette\Http\Response -=================== - -Об'єкт, на відміну від [Nette\Http\Request|request], є mutable (змінним), тобто за допомогою сеттерів ви можете змінювати стан, наприклад, надсилати заголовки. Не забувайте, що всі сеттери повинні бути викликані **перед надсиланням будь-якого виводу.** Чи був вже надісланий вивід, покаже метод `isSent()`. Якщо він повертає `true`, кожна спроба надіслати заголовок викличе виняток `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Змінює [код стану відповіді |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Для кращої зрозумілості вихідного коду рекомендуємо для коду використовувати замість чисел [передбачені константи |api:Nette\Http\IResponse]. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Повертає код стану відповіді. - - -isSent(): bool .[method] ------------------------- -Повертає, чи вже відбулося надсилання заголовків з сервера до браузера, і отже, вже неможливо надсилати заголовки чи змінювати код стану. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Надсилає HTTP-заголовок і **перезаписує** раніше надісланий заголовок з тією ж назвою. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Надсилає HTTP-заголовок і **не перезаписує** раніше надісланий заголовок з тією ж назвою. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Видаляє раніше надісланий HTTP-заголовок. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Повертає надісланий HTTP-заголовок або `null`, якщо такий не існує. Параметр нечутливий до регістру. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Повертає всі надіслані HTTP-заголовки як асоціативний масив. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Змінює заголовок `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Перенаправляє на інший URL. Не забудьте потім завершити скрипт. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Встановлює термін дії HTTP-документа за допомогою заголовків `Cache-Control` та `Expires`. Параметром є або часовий інтервал (як текст), або `null`, що заборонить кешування. - -```php -// кеш у браузері закінчиться через годину -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Відповідь буде завантажена за допомогою діалогового вікна *Зберегти як* під вказаною назвою. Сам файл при цьому не надсилається. - -```php -$httpResponse->sendAsFile('invoice.pdf'); // Змінено назву файлу на англійську для прикладу -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Надсилає cookie. Значення параметрів за замовчуванням: - -| `$path` | `'/'` | cookie має область дії на всі шляхи в (під)домені *(конфігурується)* -| `$domain` | `null` | що означає з областю дії на поточний (під)домен, але не на його піддомени *(конфігурується)* -| `$secure` | `true` | якщо сайт працює на HTTPS, інакше `false` *(конфігурується)* -| `$httpOnly` | `true` | cookie недоступна для JavaScript -| `$sameSite` | `'Lax'` | cookie може не надсилатися при [доступі з іншого домену |nette:glossary#SameSite cookie] - -Значення параметрів `$path`, `$domain` та `$secure` за замовчуванням можна змінити в [конфігурації |configuration#HTTP cookie]. - -Час можна вказувати як кількість секунд або рядок: - -```php -$httpResponse->setCookie('lang', 'uk', '100 days'); // Змінено мову на 'uk' -``` - -Параметр `$domain` визначає, які домени можуть приймати cookie. Якщо він не вказаний, cookie приймає той самий (під)домен, що й встановив його, але не його піддомени. Якщо `$domain` вказаний, піддомени також включаються. Тому вказання `$domain` є менш обмежувальним, ніж його відсутність. Наприклад, при `$domain = 'nette.org'` cookies доступні також на всіх піддоменах, таких як `doc.nette.org`. - -Для значення `$sameSite` ви можете використовувати константи `Response::SameSiteLax`, `Response::SameSiteStrict` та `Response::SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Видаляє cookie. Значення параметрів за замовчуванням: -- `$path` з областю дії на всі каталоги (`'/'`) -- `$domain` з областю дії на поточний (під)домен, але не на його піддомени -- `$secure` керується налаштуваннями в [конфігурації |configuration#HTTP cookie] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/uk/sessions.texy b/http/uk/sessions.texy deleted file mode 100644 index e69a4d6490..0000000000 --- a/http/uk/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Сесії -***** - -<div class=perex> - -HTTP — це протокол без стану, однак майже кожна програма потребує зберігати стан між запитами, наприклад, вміст кошика покупок. Саме для цього служать сесії або сеанси. Покажемо, - -- як використовувати сесії -- як уникнути конфліктів імен -- як налаштувати термін дії - -</div> - -При використанні сесій кожен користувач отримує унікальний ідентифікатор, який називається ID сесії, що передається в cookie. Він служить ключем до даних сесії. На відміну від cookies, які зберігаються на стороні браузера, дані в сесії зберігаються на стороні сервера. - -Сесію налаштовуємо в [конфігурації |configuration#Сесія], особливо важливим є вибір терміну дії. - -Керування сесіями здійснює об'єкт [api:Nette\Http\Session], до якого ви можете отримати доступ, попросивши передати його за допомогою [впровадження залежностей |dependency-injection:passing-dependencies]. У презентерах достатньо лише викликати `$session = $this->getSession()`. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Запуск сесії -============ - -Nette за замовчуванням автоматично розпочинає сесію в момент, коли ми починаємо з неї читати або в неї записувати дані. Ручний запуск сесії здійснюється за допомогою `$session->start()`. - -PHP надсилає при запуску сесії HTTP-заголовки, що впливають на кешування, див. [php:session_cache_limiter], і, можливо, cookie з ID сесії. Тому необхідно завжди запускати сесію ще до надсилання будь-якого виводу в браузер, інакше буде викинуто виняток. Якщо ви знаєте, що під час відображення сторінки буде використовуватися сесія, запустіть її вручну заздалегідь, наприклад, у презентері. - -У режимі розробки сесію запускає Tracy, оскільки вона використовує її для відображення смуг з перенаправленнями та AJAX-запитами в Tracy Bar. - - -Секції -====== - -У чистому PHP сховище даних сесії реалізовано як масив, доступний через глобальну змінну `$_SESSION`. Проблема полягає в тому, що програми зазвичай складаються з цілої низки взаємно незалежних частин, і якщо всі вони мають доступ лише до одного масиву, рано чи пізно виникне колізія імен. - -Nette Framework вирішує цю проблему, розділяючи весь простір на секції (об'єкти [api:Nette\Http\SessionSection]). Кожна одиниця потім використовує свою секцію з унікальною назвою, і жодної колізії вже виникнути не може. - -Секцію отримуємо з сесії: - -```php -$section = $session->getSection('unique_name'); // Змінено на англійську для прикладу -``` - -У презентері достатньо використати `getSession()` з параметром: - -```php -// $this є Presenter -$section = $this->getSession('unique_name'); // Змінено на англійську для прикладу -``` - -Перевірити існування секції можна методом `$session->hasSection('unique_name')`. - -З самою секцією потім працювати дуже легко за допомогою методів `set()`, `get()` та `remove()`: - -```php -// запис змінної -$section->set('userName', 'frank'); // Змінено на англійську для прикладу - -// читання змінної, поверне null, якщо не існує -echo $section->get('userName'); - -// видалення змінної -$section->remove('userName'); -``` - -Для отримання всіх змінних із секції можна використовувати цикл `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Налаштування терміну дії ------------------------- - -Для окремих секцій або навіть окремих змінних можна встановити термін дії. Ми можемо, наприклад, дозволити закінчитися терміну дії входу користувача через 20 хвилин, але при цьому продовжувати пам'ятати вміст кошика. - -```php -// секція закінчиться через 20 хвилин -$section->setExpiration('20 minutes'); -``` - -Для налаштування терміну дії окремих змінних служить третій параметр методу `set()`: - -```php -// змінна 'flash' закінчиться вже через 30 секунд -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Не забувайте, що термін дії всієї сесії (див. [конфігурацію сесії |configuration#Сесія]) повинен бути таким самим або більшим, ніж термін, встановлений для окремих секцій чи змінних. - -Скасування раніше встановленого терміну дії досягається методом `removeExpiration()`. Негайне скасування всієї секції забезпечує метод `remove()`. - - -Події $onStart, $onBeforeWrite ------------------------------- - -Об'єкт `Nette\Http\Session` має [події |nette:glossary#Події události] `$onStart` та `$onBeforeWrite`, тому ви можете додати callback-и, які будуть викликані після запуску сесії або перед її записом на диск та подальшим завершенням. - -```php -$session->onBeforeWrite[] = function () { - // запишемо дані в сесію - $this->section->set('basket', $this->basket); -}; -``` - - -Керування сесіями -================= - -Огляд методів класу `Nette\Http\Session` для керування сесіями: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Розпочинає сесію. - - -isStarted(): bool .[method] ---------------------------- -Чи розпочата сесія? - - -close(): void .[method] ------------------------ -Завершує сесію. Сесія автоматично завершується в кінці виконання скрипта. - - -destroy(): void .[method] -------------------------- -Завершує та видаляє сесію. - - -exists(): bool .[method] ------------------------- -Чи містить HTTP-запит cookie з ID сесії? - - -regenerateId(): void .[method] ------------------------------- -Генерує нове випадкове ID сесії. Дані залишаються збереженими. - - -getId(): string .[method] -------------------------- -Повертає ID сесії. - -</div> - - -Конфігурація ------------- - -Сесію налаштовуємо в [конфігурації |configuration#Сесія]. Якщо ви пишете програму, яка не використовує DI-контейнер, для конфігурації служать ці методи. Вони повинні бути викликані ще до запуску сесії. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Встановлює назву cookie, в якій передається ID сесії. Стандартна назва — `PHPSESSID`. Це корисно у випадку, коли в рамках одного сайту ви запускаєте кілька різних програм. - - -getName(): string .[method] ---------------------------- -Повертає назву cookie, в якій передається ID сесії. - - -setOptions(array $options): static .[method] --------------------------------------------- -Конфігурує сесію. Можна налаштовувати всі PHP [директиви сесії |https://www.php.net/manual/en/session.configuration.php] (у форматі camelCase, наприклад, замість `session.save_path` запишемо `savePath`), а також [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Встановлює час неактивності, після якого сесія закінчиться. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Налаштування параметрів для cookie. Значення параметрів за замовчуванням можна змінити в [конфігурації |configuration#Session cookie]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Встановлює каталог, куди зберігаються файли сесій. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Налаштування власного обробника, див. [документацію PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Безпека перш за все -=================== - -Сервер припускає, що він спілкується постійно з тим самим користувачем, доки запити супроводжуються тим самим ID сесії. Завданням механізмів безпеки є забезпечення того, щоб це справді було так, і щоб неможливо було ідентифікатор вкрасти або підсунути. - -Тому Nette Framework правильно конфігурує PHP-директиви, щоб ID сесії передавався лише в cookie, зробив його недоступним для JavaScript та ігнорував можливі ідентифікатори в URL. Крім того, у критичні моменти, наприклад, при вході користувача, він генерує нове ID сесії. - -.[note] -Для конфігурації PHP використовується функція ini_set, яку, на жаль, деякі хостинги забороняють. Якщо це стосується і вашого хостера, спробуйте домовитися з ним, щоб він дозволив вам використовувати цю функцію або хоча б налаштував сервер. diff --git a/http/uk/urls.texy b/http/uk/urls.texy deleted file mode 100644 index 72448ef8be..0000000000 --- a/http/uk/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Робота з URL -************ - -.[perex] -Класи [#Url], [#UrlImmutable] та [#UrlScript] дозволяють легко генерувати, парсити та маніпулювати URL. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Url -=== - -Клас [api:Nette\Http\Url] дозволяє легко працювати з URL та його окремими компонентами, які відображає ця схема: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Генерування URL є інтуїтивним: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Можна також розпарсити URL і далі з ним маніпулювати: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Клас `Url` реалізує інтерфейс `JsonSerializable` і має метод `__toString()`, тому об'єкт можна вивести або використовувати в даних, переданих до `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Компоненти URL .[method] ------------------------- - -Для повернення або зміни окремих компонентів URL вам доступні ці методи: - -.[language-php] -| Setter | Getter | Значення, що повертається -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цілий URL - -Попередження: Коли ви працюєте з URL, отриманим з [HTTP-запиту|request], майте на увазі, що він не міститиме фрагмент, оскільки браузер його не надсилає на сервер. - -Ми можемо працювати і з окремими query-параметрами за допомогою: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Повертає праву чи ліву частину хоста. Так це працює, якщо хост — `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Перевіряє, чи два URL однакові. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Перевіряє, чи є URL абсолютним. URL вважається абсолютним, якщо він починається зі схеми (наприклад, http, https, ftp), за якою слідує двокрапка. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Нормалізує шлях в URL, видаляючи спеціальні сегменти `.` та `..`. Метод видаляє надлишкові елементи шляху так само, як це роблять веб-браузери. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Клас [api:Nette\Http\UrlImmutable] є immutable (незмінною) альтернативою класу [#Url] (подібно до того, як у PHP `DateTimeImmutable` є незмінною альтернативою `DateTime`). Замість сеттерів він має так звані wither-и, які не змінюють об'єкт, а повертають нові екземпляри зі зміненим значенням: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/uk/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/uk/?name=param#footer' -``` - -Клас `UrlImmutable` реалізує інтерфейс `JsonSerializable` і має метод `__toString()`, тому об'єкт можна вивести або використовувати в даних, переданих до `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Компоненти URL .[method] ------------------------- - -Для повернення або зміни окремих компонентів URL служать методи: - -.[language-php] -| Wither | Getter | Значення, що повертається -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цілий URL - -Метод `withoutUserInfo()` видаляє `user` та `password`. - -Ми можемо працювати і з окремими query-параметрами за допомогою: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Повертає праву чи ліву частину хоста. Так це працює, якщо хост — `www.nette.org`: - -.[language-php] -| `getDomain(1)` | `'org'` -| `getDomain(2)` | `'nette.org'` -| `getDomain(3)` | `'www.nette.org'` -| `getDomain(0)` | `'www.nette.org'` -| `getDomain(-1)` | `'www.nette'` -| `getDomain(-2)` | `'www'` -| `getDomain(-3)` | `''` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Виводить абсолютний URL так само, як браузер обробляє посилання на HTML-сторінці: -- якщо посилання є абсолютним URL (містить схему), воно використовується без змін -- якщо посилання починається з `//`, переймається лише схема з поточного URL -- якщо посилання починається з `/`, створюється абсолютний шлях від кореня домену -- в інших випадках URL складається відносно поточного шляху - -```php -$url = new UrlImmutable('https://example.com/path/page'); -echo $url->resolve('../foo'); // 'https://example.com/foo' -echo $url->resolve('/bar'); // 'https://example.com/bar' -echo $url->resolve('sub/page.html'); // 'https://example.com/path/sub/page.html' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Перевіряє, чи два URL однакові. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Клас [api:Nette\Http\UrlScript] є нащадком [#UrlImmutable] і розширює його додатковими віртуальними компонентами URL, такими як кореневий каталог проєкту тощо. Так само, як і батьківський клас, він є immutable (незмінним) об'єктом. - -Наступна діаграма відображає компоненти, які розпізнає UrlScript: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` — це базова URL-адреса програми, включаючи домен та частину шляху до кореневого каталогу програми -- `basePath` — це частина шляху до кореневого каталогу програми -- `scriptPath` — це шлях до поточного скрипта -- `relativePath` — це назва скрипта (можливо, з додатковими сегментами шляху) відносно basePath -- `relativeUrl` — це вся частина URL після baseUrl, включаючи query string та фрагмент. -- `pathInfo` — сьогодні вже мало використовувана частина URL після назви скрипта - -Для повернення частин URL доступні методи: - -.[language-php] -| Getter | Значення, що повертається -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Об'єкти `UrlScript` зазвичай безпосередньо не створюємо, але їх повертає метод [Nette\Http\Request::getUrl()|request] з уже правильно налаштованими компонентами для поточного HTTP-запиту. diff --git a/latte/bg/@home.texy b/latte/bg/@home.texy deleted file mode 100644 index a9fdedf8d8..0000000000 --- a/latte/bg/@home.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{maintitle: Latte – най-сигурните & наистина интуитивни шаблони за PHP}} -{{description: Latte е най-сигурната система за шаблони за PHP. Предотвратява много уязвимости в сигурността. Ще оцените неговия интуитивен синтаксис и много полезни функции.}} diff --git a/latte/bg/@left-menu.texy b/latte/bg/@left-menu.texy deleted file mode 100644 index d1c5d99428..0000000000 --- a/latte/bg/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -- [Да започнем с Latte |guide] -- [Защо да използваме шаблони? |why-use] -- Концепции ⚗️ - - [Безопасността преди всичко |safety-first] - - [Наследяване на шаблони |Template Inheritance] - - [Типова система |type-system] - - [Sandbox |Sandbox] - -- За дизайнери 🎨 - - [Синтаксис |syntax] - - [Тагове |tags] - - [Филтри |filters] - - [Функции |functions] - - [Съвети и трикове |recipes] - -- За разработчици 🧮 - - [Процедури за разработчици |develop] - - [Разширяване на Latte |extending-latte] - -- [Ръководства и процедури 💡|cookbook/@home] - - [Миграция от Twig |cookbook/migration-from-twig] - - [… други |cookbook/@home] - -- "Плейграунд .[link-external]":https://fiddle.nette.org/latte/ .{padding-top:1em} diff --git a/latte/bg/@menu.texy b/latte/bg/@menu.texy deleted file mode 100644 index c6324fc01c..0000000000 --- a/latte/bg/@menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -<ul> -- [Въведение |@home] -- [Документация |guide] -- "GitHub .[link-external]":https://github.com/nette/latte -<li class="dropdown"><a class="dropdown-toggle" href="#">Инструменти</a> - <ul class="dropdown-flyout"> -- [fiddle |https://fiddle.nette.org] -- [php2Latte |https://fiddle.nette.org/php2latte/] -- [twig2Latte|https://fiddle.nette.org/twig2latte/] - </ul> -</li> -</ul> diff --git a/latte/bg/@meta.texy b/latte/bg/@meta.texy deleted file mode 100644 index 4297aeff19..0000000000 --- a/latte/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Latte}} diff --git a/latte/bg/compiler-passes.texy b/latte/bg/compiler-passes.texy deleted file mode 100644 index 198667999f..0000000000 --- a/latte/bg/compiler-passes.texy +++ /dev/null @@ -1,555 +0,0 @@ -Компилационни проходи -********************* - -.[perex] -Компилационните проходи предоставят мощен механизъм за анализ и модификация на Latte шаблони *след* тяхното парсване в абстрактно синтактично дърво (AST) и *преди* генерирането на финалния PHP код. Това позволява напреднала манипулация на шаблони, оптимизации, проверки за сигурност (като Sandbox) и събиране на информация за шаблоните. Това ръководство ще ви преведе през създаването на собствени компилационни проходи. - - -Какво е компилационен проход? -============================= - -За да разберете ролята на компилационните проходи, погледнете [процеса на компилация на Latte |custom-tags#Разбиране на процеса на компилация]. Както можете да видите, компилационните проходи оперират в ключова фаза, позволявайки дълбока намеса между първоначалното парсване и финалния изход на кода. - -По същество, компилационният проход е просто PHP callable обект (като функция, статичен метод или метод на инстанция), който приема един аргумент: коренния възел на AST на шаблона, който винаги е инстанция на `Latte\Compiler\Nodes\TemplateNode`. - -Основната цел на компилационния проход обикновено е една или и двете от следните: - -- Анализ: Обхождане на AST и събиране на информация за шаблона (напр. намиране на всички дефинирани блокове, проверка на използването на специфични тагове, осигуряване на спазването на определени ограничения за сигурност). -- Модификация: Промяна на структурата на AST или атрибутите на възлите (напр. автоматично добавяне на HTML атрибути, оптимизиране на определени комбинации от тагове, замяна на остарели тагове с нови, прилагане на правила на sandbox). - - -Регистрация -=========== - -Компилационните проходи се регистрират с помощта на метода [`getPasses()` на разширението |extending-latte#getPasses]. Този метод връща асоциативен масив, където ключовете са уникални имена на проходите (използвани вътрешно и за сортиране), а стойностите са PHP callable обекти, имплементиращи логиката на прохода. - -```php -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Extension; - -class MyExtension extends Extension -{ - public function getPasses(): array - { - return [ - 'modificationPass' => $this->modifyTemplateAst(...), - // ... други проходи ... - ]; - } - - public function modifyTemplateAst(TemplateNode $templateNode): void - { - // Имплементация... - } -} -``` - -Проходите, регистрирани от основните разширения на Latte и вашите собствени разширения, се изпълняват последователно. Редът може да бъде важен, особено ако един проход зависи от резултатите или модификациите на друг. Latte предоставя помощен механизъм за контрол на този ред, ако е необходимо; вижте документацията за [`Extension::getPasses()` |extending-latte#getPasses] за подробности. - - -Пример за AST -============= - -За по-добра представа за AST, добавяме пример. Това е изходният шаблон: - -```latte -{foreach $category->getItems() as $item} - <li>{$item->name|upper}</li> - {else} - no items found -{/foreach} -``` - -А това е неговото представяне под формата на AST: - -/--pre -Latte\Compiler\Nodes\<b>TemplateNode</b>( - Latte\Compiler\Nodes\<b>FragmentNode</b>( - - Latte\Essential\Nodes\<b>ForeachNode</b>( - expression: Latte\Compiler\Nodes\Php\Expression\<b>MethodCallNode</b>( - object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$category') - name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('getItems') - ) - value: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item') - content: Latte\Compiler\Nodes\<b>FragmentNode</b>( - - Latte\Compiler\Nodes\<b>TextNode</b>(' ') - - Latte\Compiler\Nodes\<b>Html\ElementNode</b>('li')( - content: Latte\Essential\Nodes\<b>PrintNode</b>( - expression: Latte\Compiler\Nodes\Php\Expression\<b>PropertyFetchNode</b>( - object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item') - name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('name') - ) - modifier: Latte\Compiler\Nodes\Php\<b>ModifierNode</b>( - filters: - - Latte\Compiler\Nodes\Php\<b>FilterNode</b>('upper') - ) - ) - ) - ) - else: Latte\Compiler\Nodes\<b>FragmentNode</b>( - - Latte\Compiler\Nodes\<b>TextNode</b>('no items found') - ) - ) - ) -) -\-- - - -Обхождане на AST с помощта на `NodeTraverser` -============================================= - -Ръчното писане на рекурсивни функции за обхождане на сложната структура на AST е уморително и податливо на грешки. Latte предоставя специален инструмент за тази цел: [api:Latte\Compiler\NodeTraverser]. Този клас имплементира [дизайн патърна Visitor |https://en.wikipedia.org/wiki/Visitor_pattern], благодарение на който обхождането на AST става систематично и лесно управляемо. - -Основното използване включва създаване на инстанция на `NodeTraverser` и извикване на нейния метод `traverse()`, като се предаде коренният възел на AST и един или два "visitor" callable обекта: - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; - -(new NodeTraverser)->traverse( - $templateNode, - - // 'enter' visitor: Извиква се при влизане във възел (преди неговите деца) - enter: function (Node $node) { - echo "Влизане във възел от тип: " . $node::class . "\n"; - // Тук можете да изследвате възела - if ($node instanceof Nodes\TextNode) { - // echo "Намерен текст: " . $node->content . "\n"; - } - }, - - // 'leave' visitor: Извиква се при напускане на възел (след неговите деца) - leave: function (Node $node) { - echo "Напускане на възел от тип: " . $node::class . "\n"; - // Тук можете да извършвате действия след обработка на децата - }, -); -``` - -Можете да предоставите само `enter` visitor, само `leave` visitor, или и двата, в зависимост от вашите нужди. - -**`enter(Node $node)`:** Тази функция се изпълнява за всеки възел **преди** обхождащият да посети което и да е от децата на този възел. Полезна е за: - -- Събиране на информация при обхождане на дървото надолу. -- Вземане на решения *преди* обработката на децата (като решение за тяхното пропускане, вижте [#Оптимизиране на обхождането]). -- Потенциални корекции на възела преди посещение на децата (по-рядко). - -**`leave(Node $node)`:** Тази функция се изпълнява за всеки възел **след** като всички негови деца (и техните цели поддървета) са напълно посетени (както влизане, така и напускане). Това е най-честото място за: - -И двата визитора `enter` и `leave` могат по избор да връщат стойност, за да повлияят на процеса на обхождане. Връщането на `null` (или нищо) продължава обхождането нормално, връщането на инстанция на `Node` замества текущия възел, а връщането на специални константи като `NodeTraverser::RemoveNode` или `NodeTraverser::StopTraversal` модифицира потока, както е обяснено в следващите секции. - - -Как работи обхождането ----------------------- - -`NodeTraverser` вътрешно използва метода `getIterator()`, който трябва да бъде имплементиран от всеки клас `Node` (както беше обсъдено в [Създаване на собствени тагове |custom-tags#Имплементиране на getIterator за подвъзли]). Итерира през децата, получени с помощта на `getIterator()`, рекурсивно извиква `traverse()` върху тях и гарантира, че `enter` и `leave` визиторите се извикват в правилния ред „първо в дълбочина“ за всеки възел в дървото, достъпен чрез итератори. Това отново подчертава защо правилно имплементираният `getIterator()` във вашите собствени тагови възли е абсолютно необходим за правилното функциониране на компилационните проходи. - -Нека напишем прост проход, който брои колко пъти в шаблона е използван тагът `{do}` (представен от `Latte\Essential\Nodes\DoNode`). - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Essential\Nodes\DoNode; - -function countDoTags(TemplateNode $templateNode): void -{ - $count = 0; - (new NodeTraverser)->traverse( - $templateNode, - enter: function (Node $node) use (&$count): void { - if ($node instanceof DoNode) { - $count++; - } - }, - // 'leave' visitor не е необходим за тази задача - ); - - echo "Намерен таг {do} $count пъти.\n"; -} - -$latte = new Latte\Engine; -$ast = $latte->parse($templateSource); -countDoTags($ast); -``` - -В този пример ни беше необходим само visitor `enter`, за да проверим типа на всеки посетен възел. - -След това ще разгледаме как тези визитори действително модифицират AST. - - -Модификация на AST -================== - -Една от основните цели на компилационните проходи е модификацията на абстрактното синтактично дърво. Това позволява мощни трансформации, оптимизации или налагане на правила директно върху структурата на шаблона преди генерирането на PHP код. `NodeTraverser` предоставя няколко начина за постигане на това в рамките на визиторите `enter` и `leave`. - -**Важна забележка:** Модификацията на AST изисква внимание. Неправилните промени – като премахване на основни възли или замяна на възел с несъвместим тип – могат да доведат до грешки по време на генерирането на код или да причинят неочаквано поведение по време на изпълнение на програмата. Винаги тествайте обстойно вашите модификационни проходи. - - -Промяна на свойствата на възлите --------------------------------- - -Най-простият начин за модифициране на дървото е директната промяна на **публичните свойства** на възлите, посетени по време на обхождането. Всички възли съхраняват своите парснати аргументи, съдържание или атрибути в публични свойства. - -**Пример:** Нека създадем проход, който намира всички статични текстови възли (`TextNode`, представляващи обикновен HTML или текст извън Latte тагове) и преобразува тяхното съдържание на главни букви *директно в AST*. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\TextNode; - -function uppercaseStaticText(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Можем да използваме 'enter', тъй като TextNode няма деца за обработка - enter: function (Node $node) { - // Този възел статичен текстов блок ли е? - if ($node instanceof TextNode) { - // Да! Директно ще променим неговото публично свойство 'content'. - $node->content = mb_strtoupper(html_entity_decode($node->content)); - } - // Не е необходимо нищо да се връща; промяната е приложена директно. - }, - ); -} -``` - -В този пример visitor `enter` проверява дали текущият `$node` е от тип `TextNode`. Ако е така, директно актуализираме неговото публично свойство `$content` с помощта на `mb_strtoupper()`. Това директно променя съдържанието на статичния текст, съхранен в AST *преди* генерирането на PHP код. Тъй като модифицираме обекта директно, не е необходимо да връщаме нищо от визитора. - -Ефект: Ако шаблонът съдържаше `<p>Hello</p>{= $var }<span>World</span>`, след този проход AST ще представя нещо като: `<p>HELLO</p>{= $var }<span>WORLD</span>`. Това НЕ ВЛИЯЕ на съдържанието на $var. - - -Замяна на възли ---------------- - -По-мощна техника за модификация е пълната замяна на възел с друг. Това се извършва чрез **връщане на нова инстанция на `Node`** от визитора `enter` или `leave`. `NodeTraverser` след това замества оригиналния възел с върнатия в структурата на родителския възел. - -**Пример:** Нека създадем проход, който намира всички употреби на константата `PHP_VERSION` (представена от `ConstantFetchNode`) и ги заменя директно с низов литерал (`StringNode`), съдържащ *действителната* версия на PHP, открита *по време на компилация*. Това е форма на оптимизация по време на компилация. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode; -use Latte\Compiler\Nodes\Php\Scalar\StringNode; - -function inlinePhpVersion(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // 'leave' често се използва за замяна, като гарантира, че децата (ако има такива) - // се обработват първо, въпреки че 'enter' също би работил тук. - leave: function (Node $node) { - // Този възел достъп до константа ли е и името на константата 'PHP_VERSION' ли е? - if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') { - // Създаваме нов StringNode, съдържащ текущата версия на PHP - $newNode = new StringNode(PHP_VERSION); - - // Незадължително, но добра практика: копираме информацията за позицията - $newNode->position = $node->position; - - // Връщаме новия StringNode. Traverser ще замени - // оригиналния ConstantFetchNode с този $newNode. - return $newNode; - } - // Ако не върнем Node, оригиналният $node се запазва. - }, - ); -} -``` - -Тук visitor `leave` идентифицира специфичния `ConstantFetchNode` за `PHP_VERSION`. След това създава изцяло нов `StringNode`, съдържащ стойността на константата `PHP_VERSION` *по време на компилация*. Връщайки този `$newNode`, той казва на обхождащия да замени оригиналния `ConstantFetchNode` в AST. - -Ефект: Ако шаблонът съдържаше `{= PHP_VERSION }` и компилацията се изпълнява на PHP 8.2.1, AST след този проход ефективно ще представя `{= '8.2.1' }`. - -**Избор на `enter` срещу `leave` за замяна:** - -- Използвайте `leave`, ако създаването на новия възел зависи от резултатите от обработката на децата на стария възел, или ако просто искате да гарантирате, че децата са посетени преди замяната (често срещана практика). -- Използвайте `enter`, ако искате да замените възел *преди* неговите деца изобщо да бъдат посетени. - - -Премахване на възли -------------------- - -Можете напълно да премахнете възел от AST, като върнете специалната константа `NodeTraverser::RemoveNode` от визитора. - -**Пример:** Нека премахнем всички коментари на шаблона (`{* ... *}`), които са представени от `CommentNode` в AST, генериран от ядрото на Latte (въпреки че обикновено се обработват по-рано, това служи като пример). - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\CommentNode; - -function removeCommentNodes(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // 'enter' тук е добре, тъй като не се нуждаем от информация за децата, за да премахнем коментара - enter: function (Node $node) { - if ($node instanceof CommentNode) { - // Сигнализираме на обхождащия да премахне този възел от AST - return NodeTraverser::RemoveNode; - } - }, - ); -} -``` - -**Внимание:** Използвайте `RemoveNode` внимателно. Премахването на възел, който съдържа основно съдържание или влияе на структурата (като премахване на съдържателния възел на цикъл), може да доведе до повредени шаблони или невалиден генериран код. Най-безопасно е за възли, които са наистина незадължителни или самостоятелни (като коментари или дебъгващи тагове) или за празни структурни възли (напр. празен `FragmentNode` може да бъде безопасно премахнат в някои контексти чрез проход за почистване). - -Тези три метода - промяна на свойства, замяна на възли и премахване на възли - предоставят основните инструменти за манипулиране на AST в рамките на вашите компилационни проходи. - - -Оптимизиране на обхождането -=========================== - -AST на шаблоните може да бъде доста голям, потенциално съдържащ хиляди възли. Обхождането на всеки отделен възел може да бъде ненужно и да повлияе на производителността на компилацията, ако вашият проход се интересува само от специфични части на дървото. `NodeTraverser` предлага начини за оптимизиране на обхождането: - - -Пропускане на деца ------------------- - -Ако знаете, че щом срещнете определен тип възел, нито един от неговите потомци не може да съдържа възли, които търсите, можете да кажете на обхождащия да пропусне посещението на неговите деца. Това се извършва чрез връщане на константата `NodeTraverser::DontTraverseChildren` от визитора **`enter`**. По този начин пропускате цели клонове при обхождането, което потенциално спестява значително време, особено в шаблони със сложни PHP изрази вътре в тагове. - - -Спиране на обхождането ----------------------- - -Ако вашият проход трябва да намери само *първото* срещане на нещо (специфичен тип възел, изпълнение на условие), можете напълно да спрете целия процес на обхождане, щом го намерите. Това се постига чрез връщане на константата `NodeTraverser::StopTraversal` от визитора `enter` или `leave`. Методът `traverse()` спира да посещава всякакви други възли. Това е изключително ефективно, ако се нуждаете само от първото съвпадение в потенциално много голямо дърво. - - -Полезен помощник `NodeHelpers` -============================== - -Въпреки че `NodeTraverser` предлага фин контрол, Latte също предоставя практичен помощен клас, [api:Latte\Compiler\NodeHelpers], който капсулира `NodeTraverser` за няколко често срещани задачи за търсене и анализ, често изискващи по-малко подготвителен код. - - -find(Node $startNode, callable $filter): array .[method] --------------------------------------------------------- - -Този статичен метод намира **всички** възли в поддървото, започващо от `$startNode` (включително), които отговарят на callback `$filter`. Връща масив от съответстващи възли. - -**Пример:** Намиране на всички възли на променливи (`VariableNode`) в целия шаблон. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Php\Expression\VariableNode; -use Latte\Compiler\Nodes\TemplateNode; - -function findAllVariables(TemplateNode $templateNode): array -{ - return NodeHelpers::find( - $templateNode, - fn($node) => $node instanceof VariableNode, - ); -} -``` - - -findFirst(Node $startNode, callable $filter): ?Node .[method] --------------------------------------------------------------- - -Подобно на `find`, но спира обхождането незабавно след намиране на **първия** възел, който отговаря на callback `$filter`. Връща намерения обект `Node` или `null`, ако не е намерен съответстващ възел. Това е по същество практична обвивка около `NodeTraverser::StopTraversal`. - -**Пример:** Намиране на възела `{parameters}` (същото като ръчния пример преди, но по-кратко). - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Essential\Nodes\ParametersNode; - -function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode -{ - return NodeHelpers::findFirst( - $templateNode->head, // Търсене само в главната секция за ефективност - fn($node) => $node instanceof ParametersNode, - ); -} -``` - - -toValue(ExpressionNode $node, bool $constants = false): mixed .[method] ------------------------------------------------------------------------ - -Този статичен метод се опитва да *изчисли стойността* на `ExpressionNode` **по време на компилация** и да върне неговата съответстваща PHP стойност. Работи надеждно само за прости литерални възли (`StringNode`, `IntegerNode`, `FloatNode`, `BooleanNode`, `NullNode`) и инстанции на `ArrayNode`, съдържащи само такива изчислими елементи. - -Ако `$constants` е зададено на `true`, той също ще се опита да разреши `ConstantFetchNode` и `ClassConstantFetchNode` чрез проверка с `defined()` и използване на `constant()`. - -Ако възелът съдържа променливи, извиквания на функции или други динамични елементи, той не може да бъде изчислен по време на компилация и методът ще хвърли `InvalidArgumentException`. - -**Случай на употреба:** Получаване на статичната стойност на аргумент на таг по време на компилация за вземане на решения по време на компилация. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Php\ExpressionNode; - -function getStaticStringArgument(ExpressionNode $argumentNode): ?string -{ - try { - $value = NodeHelpers::toValue($argumentNode); - return is_string($value) ? $value : null; - } catch (\InvalidArgumentException $e) { - // Аргументът не беше статичен литерален низ - return null; - } -} -``` - - -toText(?Node $node): ?string .[method] --------------------------------------- - -Този статичен метод е полезен за извличане на обикновено текстово съдържание от прости възли. Работи предимно с: -- `TextNode`: Връща неговото `$content`. -- `FragmentNode`: Конкатенира резултата от `toText()` за всички негови деца. Ако някое дете не може да се преобразува в текст (напр. съдържа `PrintNode`), връща `null`. -- `NopNode`: Връща празен низ. -- Други типове възли: Връща `null`. - -**Случай на употреба:** Получаване на статичното текстово съдържание на стойността на HTML атрибут или прост HTML елемент за анализ по време на компилационен проход. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Html\AttributeNode; - -function getStaticAttributeValue(AttributeNode $attr): ?string -{ - // $attr->value обикновено е AreaNode (като FragmentNode или TextNode) - return NodeHelpers::toText($attr->value); -} - -// Пример за използване в проход: -// if ($node instanceof Html\ElementNode && $node->name === 'meta') { -// $nameAttrValue = getStaticAttributeValue($node->getAttributeNode('name')); -// if ($nameAttrValue === 'description') { ... } -// } -``` - -`NodeHelpers` може да опрости вашите компилационни проходи, като предостави готови решения за често срещани задачи за обхождане и анализ на AST. - - -Практически примери -=================== - -Нека приложим концепциите за обхождане и модификация на AST за решаване на някои практически проблеми. Тези примери демонстрират често срещани модели, използвани в компилационните проходи. - - -Автоматично добавяне на `loading="lazy"` към `<img>` ----------------------------------------------------- - -Съвременните браузъри поддържат вградено мързеливо зареждане за изображения с помощта на атрибута `loading="lazy"`. Нека създадем проход, който автоматично добавя този атрибут към всички тагове `<img>`, които все още нямат атрибут `loading`. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; -use Latte\Compiler\Nodes\Html; - -function addLazyLoading(Nodes\TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Можем да използваме 'enter', тъй като модифицираме възела директно - // и не зависим от децата за това решение. - enter: function (Node $node) { - // Това HTML елемент с име 'img' ли е? - if ($node instanceof Html\ElementNode && $node->name === 'img') { - // Гарантираме, че възелът на атрибутите съществува - $node->attributes ??= new Nodes\FragmentNode; - - // Проверяваме дали вече съществува атрибут 'loading' (без значение от регистъра) - foreach ($node->attributes->children as $attrNode) { - if ($attrNode instanceof Html\AttributeNode - && $attrNode->name instanceof Nodes\TextNode // Статично име на атрибут - && strtolower($attrNode->name->content) === 'loading' - ) { - return; // Атрибутът 'loading' вече съществува, не правим нищо - } - } - - // Добавяме интервал, ако атрибутите не са празни и последният не е интервал - if ($node->attributes->children) { - $node->attributes->children[] = new Nodes\TextNode(' '); - } - - // Създаваме нов възел на атрибута: loading="lazy" - $node->attributes->children[] = new Html\AttributeNode( - name: new Nodes\TextNode('loading'), - value: new Nodes\TextNode('lazy'), - quote: '"', - ); - // Промяната се прилага директно в обекта, не е необходимо нищо да се връща. - } - }, - ); -} -``` - -Обяснение: -- Visitor `enter` търси възли `Html\ElementNode` с име `img`. -- Итерира през съществуващите атрибути (`$node->attributes->children`) и проверява дали атрибутът `loading` вече присъства. -- Ако не е намерен, създава нов `Html\AttributeNode`, представляващ `loading="lazy"`. - - -Проверка на извиквания на функции ---------------------------------- - -Компилационните проходи са основата на Latte Sandbox. Въпреки че истинският Sandbox е сложен, можем да демонстрираме основния принцип на проверка за забранени извиквания на функции. - -**Цел:** Предотвратяване на използването на потенциално опасната функция `shell_exec` в рамките на изрази в шаблона. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; -use Latte\Compiler\Nodes\Php; -use Latte\SecurityViolationException; - -function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void -{ - $forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Прост списък - - $traverser = new NodeTraverser; - (new NodeTraverser)->traverse( - $templateNode, - enter: function (Node $node) use ($forbiddenFunctions) { - // Това възел на директно извикване на функция ли е? - if ($node instanceof Php\Expression\FunctionCallNode - && $node->name instanceof Php\NameNode - && isset($forbiddenFunctions[strtolower((string) $node->name)]) - ) { - throw new SecurityViolationException( - "Функцията {$node->name}() не е разрешена.", - $node->position, - ); - } - }, - ); -} -``` - -Обяснение: -- Дефинираме списък със забранени имена на функции. -- Visitor `enter` проверява `FunctionCallNode`. -- Ако името на функцията (`$node->name`) е статичен `NameNode`, проверяваме неговото представяне като низ с малки букви спрямо нашия забранен списък. -- Ако е намерена забранена функция, хвърляме `Latte\SecurityViolationException`, която ясно показва нарушение на правилото за сигурност и спира компилацията. - -Тези примери показват как компилационните проходи с използването на `NodeTraverser` могат да бъдат използвани за анализ, автоматични модификации и налагане на ограничения за сигурност чрез директно взаимодействие със структурата на AST на шаблона. - - -Добри практики -============== - -При писане на компилационни проходи имайте предвид тези насоки за създаване на стабилни, поддържаеми и ефективни разширения: - -- **Редът на изпълнение е важен:** Бъдете наясно с реда, в който се изпълняват проходите. Ако вашият проход зависи от структурата на AST, създадена от друг проход (напр. основни проходи на Latte или друг персонализиран проход), или ако други проходи могат да зависят от вашите модификации, използвайте механизма за сортиране, предоставен от `Extension::getPasses()`, за да дефинирате зависимости (`before`/`after`). Вижте документацията за [`Extension::getPasses()` |extending-latte#getPasses] за подробности. -- **Една отговорност:** Стремете се към проходи, които изпълняват една добре дефинирана задача. За сложни трансформации обмислете разделянето на логиката на няколко прохода – може би един за анализ и друг за модификация, базирана на резултатите от анализа. Това подобрява прегледността и тестваемостта. -- **Производителност:** Помнете, че компилационните проходи добавят време към компилацията на шаблона (въпреки че това обикновено се случва само веднъж, докато шаблонът не се промени). Избягвайте изчислително скъпи операции във вашите проходи, ако е възможно. Използвайте оптимизации на обхождането като `NodeTraverser::DontTraverseChildren` и `NodeTraverser::StopTraversal` винаги, когато знаете, че не е необходимо да посещавате определени части от AST. -- **Използвайте `NodeHelpers`:** За често срещани задачи като търсене на специфични възли или статично изчисляване на прости изрази, проверете дали `Latte\Compiler\NodeHelpers` не предлага подходящ метод, преди да пишете собствена логика с `NodeTraverser`. Това може да спести време и да намали количеството подготвителен код. -- **Обработка на грешки:** Ако вашият проход открие грешка или невалидно състояние в AST на шаблона, хвърлете `Latte\CompileException` (или `Latte\SecurityViolationException` за проблеми със сигурността) с ясно съобщение и релевантен обект `Position` (обикновено `$node->position`). Това предоставя полезна обратна връзка на разработчика на шаблона. -- **Идемпотентност (ако е възможно):** В идеалния случай, изпълнението на вашия проход няколко пъти върху същия AST трябва да произведе същия резултат като еднократното му изпълнение. Това не винаги е изпълнимо, но опростява отстраняването на грешки и разсъжденията за взаимодействията на проходите, ако бъде постигнато. Например, уверете се, че вашият модификационен проход проверява дали модификацията вече е приложена, преди да я приложи отново. - -Следвайки тези практики, можете ефективно да използвате компилационните проходи, за да разширите възможностите на Latte по мощен и надежден начин, допринасяйки за по-безопасна, по-оптимизирана или функционално по-богата обработка на шаблони. diff --git a/latte/bg/cookbook/@home.texy b/latte/bg/cookbook/@home.texy deleted file mode 100644 index 019aa6cd32..0000000000 --- a/latte/bg/cookbook/@home.texy +++ /dev/null @@ -1,13 +0,0 @@ -Ръководства и процедури -*********************** - -.[perex] -Примери за кодове и рецепти за изпълнение на често срещани задачи с помощта на Latte. - -- [Процедури за разработчици |/develop] -- [Предаване на променливи между шаблони |passing-variables] -- [Всичко, което някога сте искали да знаете за групирането |grouping] -- [Как да пишем SQL заявки в Latte? |how-to-write-sql-queries-in-latte] -- [Миграция от PHP |migration-from-php] -- [Миграция от Twig |migration-from-twig] -- [Използване на Latte със Slim 4 |slim-framework] diff --git a/latte/bg/cookbook/@meta.texy b/latte/bg/cookbook/@meta.texy deleted file mode 100644 index 64e87d1168..0000000000 --- a/latte/bg/cookbook/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Latte}} -{{leftbar: /@left-menu}} diff --git a/latte/bg/cookbook/grouping.texy b/latte/bg/cookbook/grouping.texy deleted file mode 100644 index fe6ac48fe5..0000000000 --- a/latte/bg/cookbook/grouping.texy +++ /dev/null @@ -1,251 +0,0 @@ -Всичко, което някога сте искали да знаете за групирането -******************************************************** - -.[perex] -При работа с данни в шаблони често можете да срещнете нуждата от тяхното групиране или специфично показване според определени критерии. Latte за тази цел предлага няколко силни инструмента. - -Филтърът и функцията `|group` позволяват ефективно групиране на данни според зададен критерий, филтърът `|batch` пък улеснява разделянето на данни на предварително зададени партиди, а тагът `{iterateWhile}` предоставя възможност за по-сложно управление на протичането на цикли с условия. Всеки от тези тагове предлага специфични възможности за работа с данни, което ги прави незаменими инструменти за динамично и структурирано показване на информация в Latte шаблони. - - -Филтър и функция `group` .{data-version:3.0.16} -=============================================== - -Представете си таблица в база данни `items` с елементи, разделени на категории: - -| id | categoryId | name -|------------------ -| 1 | 1 | Apple -| 2 | 1 | Banana -| 3 | 2 | PHP -| 4 | 3 | Green -| 5 | 3 | Red -| 6 | 3 | Blue - -Прост списък на всички елементи с помощта на Latte шаблон би изглеждал така: - -```latte -<ul> -{foreach $items as $item} - <li>{$item->name}</li> -{/foreach} -</ul> -``` - -Ако обаче искахме елементите да бъдат подредени в групи според категорията, трябва да ги разделим така, че всяка категория да има свой собствен списък. Резултатът тогава трябва да изглежда по следния начин: - -```latte -<ul> - <li>Apple</li> - <li>Banana</li> -</ul> - -<ul> - <li>PHP</li> -</ul> - -<ul> - <li>Green</li> - <li>Red</li> - <li>Blue</li> -</ul> -``` - -Задачата може лесно и елегантно да се реши с помощта на `|group`. Като параметър посочваме `categoryId`, което означава, че елементите ще се разделят на по-малки масиви според стойността на `$item->categoryId` (ако `$item` беше масив, ще се използва `$item['categoryId']`): - -```latte -{foreach ($items|group: categoryId) as $categoryId => $categoryItems} - <ul> - {foreach $categoryItems as $item} - <li>{$item->name}</li> - {/foreach} - </ul> -{/foreach} -``` - -Филтърът може в Latte да се използва и като функция, което ни дава алтернативен синтаксис: `{foreach group($items, categoryId) ...}`. - -Ако искате да групирате елементи според по-сложни критерии, можете в параметъра на филтъра да използвате функция. Например, групиране на елементи според дължината на името би изглеждало така: - -```latte -{foreach ($items|group: fn($item) => strlen($item->name)) as $items} - ... -{/foreach} -``` - -Важно е да се осъзнае, че `$categoryItems` не е обикновен масив, а обект, който се държи като итератор. За достъп до първия елемент на групата можете да използвате функцията [`first()` |latte:functions#first]. - -Тази гъвкавост в групирането на данни прави `group` изключително полезен инструмент за представяне на данни в шаблони Latte. - - -Вложени цикли -------------- - -Представете си, че имаме база данни с допълнителна колона `subcategoryId`, която дефинира подкатегориите на отделните елементи. Искаме да покажем всяка основна категория в отделен списък `<ul>` и всяка подкатегория в отделен вложен списък `<ol>`: - -```latte -{foreach ($items|group: categoryId) as $categoryItems} - <ul> - {foreach ($categoryItems|group: subcategoryId) as $subcategoryItems} - <ol> - {foreach $subcategoryItems as $item} - <li>{$item->name} - {/foreach} - </ol> - {/foreach} - </ul> -{/foreach} -``` - - -Връзка с Nette Database ------------------------ - -Нека покажем как ефективно да използваме групирането на данни в комбинация с Nette Database. Да предположим, че работим с таблицата `items` от уводния пример, която чрез колоната `categoryId` е свързана с тази таблица `categories`: - -| categoryId | name | -|------------|------------| -| 1 | Fruits | -| 2 | Languages | -| 3 | Colors | - -Данните от таблицата `items` зареждаме с помощта на Nette Database Explorer с командата `$items = $db->table('items')`. По време на итерацията над тези данни имаме възможност да достъпваме не само атрибути като `$item->name` и `$item->categoryId`, но благодарение на връзката с таблицата `categories` също и свързания ред в нея чрез `$item->category`. На тази връзка може да се демонстрира интересно използване: - -```latte -{foreach ($items|group: category) as $category => $categoryItems} - <h1>{$category->name}</h1> - <ul> - {foreach $categoryItems as $item} - <li>{$item->name}</li> - {/foreach} - </ul> -{/foreach} -``` - -В този случай използваме филтъра `|group` за групиране според свързания ред `$item->category`, а не само според колоната `categoryId`. Благодарение на това в променливата ключ имаме директно `ActiveRow` на дадената категория, което ни позволява директно да изписваме нейното име с `{$category->name}`. Това е практичен пример за това как групирането може да изясни шаблоните и да улесни работата с данни. - - -Филтър `|batch` -=============== - -Филтърът позволява да се раздели списък от елементи на групи с предварително определен брой елементи. Този филтър е идеален за ситуации, когато искате да представите данните в няколко по-малки групи, например за по-добра прегледност или визуално подреждане на страницата. - -Представете си, че имаме списък с елементи и искаме да ги покажем в списъци, където всеки съдържа максимум три елемента. Използването на филтъра `|batch` в такъв случай е много практично: - -```latte -<ul> -{foreach ($items|batch: 3) as $batch} - {foreach $batch as $item} - <li>{$item->name}</li> - {/foreach} -{/foreach} -</ul> -``` - -В този пример списъкът `$items` е разделен на по-малки групи, като всяка група (`$batch`) съдържа до три елемента. Всяка група след това се показва в отделен `<ul>` списък. - -Ако последната група не съдържа достатъчно елементи за достигане на желания брой, вторият параметър на филтъра позволява да се дефинира с какво ще бъде допълнена тази група. Това е идеално за естетическо подравняване на елементите там, където непълният ред би могъл да изглежда неподреден. - -```latte -{foreach ($items|batch: 3, '—') as $batch} - ... -{/foreach} -``` - - -Таг `{iterateWhile}` -==================== - -Същите задачи, които решавахме с филтъра `|group`, ще покажем с използването на тага `{iterateWhile}`. Основната разлика между двата подхода е в това, че `group` първо обработва и групира всички входни данни, докато `{iterateWhile}` управлява протичането на цикли с условия, така че итерацията протича постепенно. - -Първо ще рендираме таблицата с категориите с помощта на iterateWhile: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <li>{$item->name}</li> - {/iterateWhile $item->categoryId === $iterator->nextValue->categoryId} - </ul> -{/foreach} -``` - -Докато `{foreach}` обозначава външната част на цикъла, т.е. рендирането на списъци за всяка категория, тагът `{iterateWhile}` обозначава вътрешната част, т.е. отделните елементи. Условието в крайния таг казва, че повторението ще продължи дотогава, докато текущият и следващият елемент принадлежат към същата категория (`$iterator->nextValue` е [следващият елемент |/tags#iterator]). - -Ако условието беше изпълнено винаги, тогава във вътрешния цикъл ще се рендират всички елементи: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <li>{$item->name} - {/iterateWhile true} - </ul> -{/foreach} -``` - -Резултатът ще изглежда така: - -```latte -<ul> - <li>Apple</li> - <li>Banana</li> - <li>PHP</li> - <li>Green</li> - <li>Red</li> - <li>Blue</li> -</ul> -``` - -За какво е полезно такова използване на iterateWhile? Когато таблицата е празна и не съдържа никакви елементи, няма да се изпише празно `<ul></ul>`. - -Ако посочим условие в отварящия таг `{iterateWhile}`, тогава поведението се променя: условието (и преходът към следващия елемент) се изпълнява още в началото на вътрешния цикъл, а не в края. Тоест, докато в `{iterateWhile}` без условие се влиза винаги, в `{iterateWhile $cond}` само при изпълнение на условието `$cond`. И същевременно с това в `$item` се записва следващият елемент. - -Което е полезно например в ситуация, когато искаме първия елемент във всяка категория да рендираме по различен начин, например така: - -```latte -<h1>Apple</h1> -<ul> - <li>Banana</li> -</ul> - -<h1>PHP</h1> -<ul> -</ul> - -<h1>Green</h1> -<ul> - <li>Red</li> - <li>Blue</li> -</ul> -``` - -Ще променим оригиналния код така, че първо да рендираме първия елемент и след това във вътрешния цикъл `{iterateWhile}` да рендираме другите елементи от същата категория: - -```latte -{foreach $items as $item} - <h1>{$item->name}</h1> - <ul> - {iterateWhile $item->categoryId === $iterator->nextValue->categoryId} - <li>{$item->name}</li> - {/iterateWhile} - </ul> -{/foreach} -``` - -В рамките на един цикъл можем да създаваме повече вътрешни цикли и дори да ги влагаме. Така биха могли да се групират например подкатегории и т.н. - -Да предположим, че в таблицата има още една колона `subcategoryId` и освен това, че всяка категория ще бъде в отделен `<ul>`, всяка подкатегория в отделен `<ol>`: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <ol> - {iterateWhile} - <li>{$item->name} - {/iterateWhile $item->subcategoryId === $iterator->nextValue->subcategoryId} - </ol> - {/iterateWhile $item->categoryId === $iterator->nextValue->categoryId} - </ul> -{/foreach} -``` diff --git a/latte/bg/cookbook/how-to-write-sql-queries-in-latte.texy b/latte/bg/cookbook/how-to-write-sql-queries-in-latte.texy deleted file mode 100644 index b6a002275e..0000000000 --- a/latte/bg/cookbook/how-to-write-sql-queries-in-latte.texy +++ /dev/null @@ -1,40 +0,0 @@ -Как да пишем SQL заявки в Latte? -******************************** - -.[perex] -Latte може да бъде полезен и за генериране на наистина сложни SQL заявки. - -Ако създаването на SQL заявка съдържа редица условия и променливи, може да бъде наистина по-прегледно да я напишете в Latte. Много прост пример: - -```latte -SELECT users.* FROM users - LEFT JOIN users_groups ON users.user_id = users_groups.user_id - LEFT JOIN groups ON groups.group_id = users_groups.group_id - {ifset $country} LEFT JOIN country ON country.country_id = users.country_id {/ifset} -WHERE groups.name = 'Admins' {ifset $country} AND country.name = {$country} {/ifset} -``` - -С помощта на `$latte->setContentType()` казваме на Latte да третира съдържанието като обикновен текст (а не като HTML) и след това подготвяме функция за екраниране, която ще екранира низовете директно с драйвера на базата данни: - -```php -$db = new PDO(/* ... */); - -$latte = new Latte\Engine; -$latte->setContentType(Latte\ContentType::Text); -$latte->addFilter('escape', fn($val) => match (true) { - is_string($val) => $db->quote($val), - is_int($val), is_float($val) => (string) $val, - is_bool($val) => $val ? '1' : '0', - is_null($val) => 'NULL', - default => throw new Exception('Unsupported type'), -}); -``` - -Използването би изглеждало така: - -```php -$sql = $latte->renderToString('query.sql.latte', ['country' => $country]); -$result = $db->query($sql); -``` - -*Посоченият пример изисква Latte v3.0.5 или по-нова версия.* diff --git a/latte/bg/cookbook/migration-from-php.texy b/latte/bg/cookbook/migration-from-php.texy deleted file mode 100644 index 73be04cf53..0000000000 --- a/latte/bg/cookbook/migration-from-php.texy +++ /dev/null @@ -1,70 +0,0 @@ -Миграция от PHP към Latte -************************* - -.[perex] -Преобразувате стар проект, написан на чист PHP, към Latte? Имаме за вас инструмент, който ще ви улесни миграцията. [Изпробвайте го онлайн |https://fiddle.nette.org/php2latte/]. - -Можете да изтеглите инструмента от [GitHub|https://github.com/nette/latte-tools] или да го инсталирате с помощта на Composer: - -```shell -composer create-project latte/tools -``` - -Преобразувателят не използва прости замени с помощта на регулярни изрази, а напротив, използва директно PHP парсера, така че може да се справи с всякакъв сложен синтаксис. - -За преобразуване от PHP към Latte служи скриптът `php-to-latte.php`: - -```shell -php php-to-latte.php input.php [output.latte] -``` - - -Пример ------- - -Входният файл може да изглежда например така (това е част от кода на форума PunBB): - -```php -<h1><span><?= $lang_common['User list'] ?></span></h1> - -<div class="blockform"> - <form id="userlist" method="get" action="userlist.php"> - <div class="infldset"> -<?php -foreach ($result as $cur_group) { - if ($cur_group['g_id'] == $show_group) { - echo "\n\t\t" . '<option value="' . $cur_group['g_id'] . '" selected="selected">' - . htmlspecialchars($cur_group['g_title']) . '</option>'; - } else { - echo "\n\t\t" . '<option value="' . $cur_group['g_id'] . '">' - . htmlspecialchars($cur_group['g_title']) . '</option>'; - } -} -?> - </select> - <p class="clearb"><?= $lang_ul['User search info'] ?></p> - </div> - </form> -</div> -``` - -Ще генерира този шаблон: - -```latte -<h1><span>{$lang_common['User list']}</span></h1> - -<div class="blockform"> - <form id="userlist" method="get" action="userlist.php"> - <div class="infldset"> -{foreach $result as $cur_group} - {if $cur_group[g_id] == $show_group} - <option value="{$cur_group[g_id]}" selected="selected">{$cur_group[g_title]}</option> - {else} - <option value="{$cur_group[g_id]}">{$cur_group[g_title]}</option> - {/if} -{/foreach} </select> - <p class="clearb">{$lang_ul['User search info']}</p> - </div> - </form> -</div> -``` diff --git a/latte/bg/cookbook/migration-from-twig.texy b/latte/bg/cookbook/migration-from-twig.texy deleted file mode 100644 index 04e356b464..0000000000 --- a/latte/bg/cookbook/migration-from-twig.texy +++ /dev/null @@ -1,79 +0,0 @@ -Миграция от Twig към Latte -************************** - -.[perex] -Преобразувате проект, написан на Twig, към по-модерния Latte? Имаме за вас инструмент, който ще ви улесни миграцията. [Изпробвайте го онлайн |https://fiddle.nette.org/twig2latte/]. - -Можете да изтеглите инструмента от [GitHub|https://github.com/nette/latte-tools] или да го инсталирате с помощта на Composer: - -```shell -composer create-project latte/tools -``` - -Преобразувателят не използва прости замени с помощта на регулярни изрази, а напротив, използва директно Twig парсера, така че може да се справи с всякакъв сложен синтаксис. - -За преобразуване от Twig към Latte служи скриптът `twig-to-latte.php`: - -```shell -php twig-to-latte.php input.twig.html [output.latte] -``` - - -Конверсия ---------- - -Преобразуването предполага ръчна корекция на резултата, тъй като конверсията не може да се извърши еднозначно. Twig използва точков синтаксис, където `{{ a.b }}` може да означава `$a->b`, `$a['b']` или `$a->getB()`, което не може да се разграничи при компилация. Преобразувателят затова преобразува всичко на `$a->b`. - -Някои функции, филтри или тагове нямат аналог в Latte, или могат да се държат леко по-различно. - - -Пример ------- - -Входният файл може да изглежда например така: - -```twig -{% use "blocks.twig" %} -<!DOCTYPE html> -<html> - <head> - <title>{{ block("title") }} - - -

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

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

    {block title}My Web{/block}

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

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

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

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

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

    ` или `

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

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

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

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

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

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

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

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

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

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

    -{else} -

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

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

    {$heading|upper}

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

    {$heading|lower|capitalize}

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

    {$heading|truncate:20,''}

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

    {$text|shortify}

    -

    {$text|shortify:100}

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

    Hello

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

    Hello

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

    one < two

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

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

    {clamp($num, 1, 100)}

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

    {shortify($text)}

    -

    {shortify($text, 100)}

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

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

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

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

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

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

    {$movie}

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

    Amarcord & 8 1/2

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

    {$item}

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

    I

    -

    ♥

    -

    Latte

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

    {$item}

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

    I

    -
    -

    ♥

    -
    -

    Latte

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

    Title

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

    Title

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

    Title

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

    {$heading|upper}

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

    {$heading|lower|capitalize}

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

    {$heading|truncate:20,''}

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

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

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

    {=' Hello world '|trim}

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

    Heading

    ` или `

    Heading

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

    {='one < two'}

    {* извежда: '

    one < two

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

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

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

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

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

    {$name[0]}

    {/ifchanged} - -

    {$name}

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

    {$name[0]}

    - -

    {$name}

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

    {$item->title}

    -

    {$item->description}

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

    Foo

    -

    Lorem ipsum.

    -

    Bar

    -

    Sit dolor.

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

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

    {/first} - -

    {$row->name}

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

    {$i}

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

    Съобщения

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

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

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

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

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

    {$title}

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

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

    ...

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

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

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

    Поръчка

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

    Welcome to my awesome homepage.

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

    Welcome to my awesome homepage.

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

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

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

    {$post->title}

    -

    {$post->body}

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

    {include title}

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

    Hello

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

    Hello

    *} - -{define bar}

    World

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

    World

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

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

    -

    {include textarea, 'comment'}

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

    Welcome to my awesome homepage

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

    - {block title}{/block} -

    - -
    - {block content}{/block} -
    -
    -``` - -Таговете `{block}` дефинират два блока, които дъщерните шаблони могат да запълнят. Да, както в случая с родителския шаблон при наследяването на лейаути. Виждате също променливата `$modifierClass`. - -Нека използваме нашия елемент в шаблона. Тук идва ред на `{embed}`. Това е изключително мощен таг, който ни позволява да правим всичко: да вмъкваме съдържанието на шаблона на елемента, да добавяме променливи към него и да добавяме блокове със собствен HTML: - -```latte -{embed 'collapsible.latte', modifierClass: my-style} - {block title} - Hello World - {/block} - - {block content} -

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

    - {/block} -{/embed} -``` - -Изходът може да изглежда така: - -```latte -
    -

    - Hello World -

    - -
    -

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

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

    - {block title}{/block} -

    - ... -
    -{/define} - - -{embed collapsible, modifierClass: my-style} - {block title} - Hello World - {/block} - ... -{/embed} -``` - -Ако предадем израз на `{embed}` и не е ясно дали това е име на блок или файл, добавяме ключовата дума `block` или `file`: - -```latte -{embed block $name} ... {/embed} -``` - - -Случаи на употреба -================== - -В Latte съществуват различни типове наследяване и повторно използване на код. Нека обобщим основните концепции за по-голяма яснота: - - -`{include template}` --------------------- - -**Случай на употреба**: Използване на `header.latte` и `footer.latte` вътре в `layout.latte`. - -`header.latte` - -```latte - -``` - -`footer.latte` - -```latte -
    -
    Copyright
    -
    -``` - -`layout.latte` - -```latte -{include 'header.latte'} - -
    {block main}{/block}
    - -{include 'footer.latte'} -``` - - -`{layout}` ----------- - -**Случай на употреба**: Разширяване на `layout.latte` вътре в `homepage.latte` и `about.latte`. - -`layout.latte` - -```latte -{include 'header.latte'} - -
    {block main}{/block}
    - -{include 'footer.latte'} -``` - -`homepage.latte` - -```latte -{layout 'layout.latte'} - -{block main} -

    Homepage

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

    About page

    -{/block} -``` - - -`{import}` ----------- - -**Случай на употреба**: `sidebar.latte` в `single.product.latte` и `single.service.latte`. - -`sidebar.latte` - -```latte -{block sidebar}{/block} -``` - -`single.product.latte` - -```latte -{layout 'product.layout.latte'} - -{import 'sidebar.latte'} - -{block main}
    Product page
    {/block} -``` - -`single.service.latte` - -```latte -{layout 'service.layout.latte'} - -{import 'sidebar.latte'} - -{block main}
    Service page
    {/block} -``` - - -`{define}` ----------- - -**Случай на употреба**: Функции, на които предаваме променливи и те рендират нещо. - -`form.latte` - -```latte -{define form-input, $name, $value, $type = 'text'} - -{/define} -``` - -`profile.service.latte` - -```latte -{import 'form.latte'} - -
    -
    {include form-input, username}
    -
    {include form-input, password}
    -
    {include form-input, submit, Submit, submit}
    -
    -``` - - -`{embed}` ---------- - -**Случай на употреба**: Вмъкване на `pagination.latte` в `product.table.latte` и `service.table.latte`. - -`pagination.latte` - -```latte - -``` - -`product.table.latte` - -```latte -{embed 'pagination.latte', min: 1, max: $products->count} - {block first}First Product Page{/block} - {block last}Last Product Page{/block} -{/embed} -``` - -`service.table.latte` - -```latte -{embed 'pagination.latte', min: 1, max: $services->count} - {block first}First Service Page{/block} - {block last}Last Service Page{/block} -{/embed} -``` diff --git a/latte/bg/type-system.texy b/latte/bg/type-system.texy deleted file mode 100644 index 8f052cf074..0000000000 --- a/latte/bg/type-system.texy +++ /dev/null @@ -1,73 +0,0 @@ -Типова система -************** - -
    - -Типовата система е ключова за разработването на стабилни приложения. Latte въвежда поддръжка на типове и в шаблоните. Благодарение на това, че знаем какъв тип данни или обект има във всяка променлива, може - -- IDE да подсказва правилно (вижте [интеграция |recipes#Редактори и IDE]) -- статичният анализ да открива грешки - -И двете значително повишават качеството и удобството на разработката. - -
    - -.[note] -Декларираните типове са информативни и Latte в момента не ги проверява. - -Как да започнете да използвате типове? Създайте клас на шаблона, напр. `CatalogTemplateParameters`, представляващ предаваните параметри, техните типове и евентуално стойности по подразбиране: - -```php -class CatalogTemplateParameters -{ - public function __construct( - public string $langs, - /** @var ProductEntity[] */ - public array $products, - public Address $address, - ) {} -} - -$latte->render('template.latte', new CatalogTemplateParameters( - address: $userAddress, - lang: $settings->getLanguage(), - products: $entityManager->getRepository('Product')->findAll(), -)); -``` - -След това в началото на шаблона поставете тага `{templateType}` с пълното име на класа (включително namespace). Това дефинира, че в шаблона има променливи `$langs` и `$products`, включително съответните типове. Типовете на локалните променливи можете да посочите с помощта на таговете [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Дефиниции]. - -От този момент IDE може да ви подсказва правилно. - -Как да си спестите работа? Как най-лесно да напишете клас с параметри на шаблон или тагове `{varType}`? Нека бъдат генерирани за вас. За това съществуват двойка тагове `{templatePrint}` и `{varPrint}`. Ако ги поставите в шаблона, вместо нормалното рендиране ще се покаже предложение за код на клас или съответно списък с тагове `{varType}`. След това е достатъчно да маркирате кода с едно кликване и да го копирате в проекта. - - -`{templateType}` ----------------- -Типовете на параметрите, предавани към шаблона, се декларират с помощта на клас: - -```latte -{templateType MyApp\CatalogTemplateParameters} -``` - - -`{varType}` ------------ -Как да декларираме типовете на променливите? За това служат таговете `{varType}` за съществуващи променливи или [`{var}` |tags#var default]: - -```latte -{varType Nette\Security\User $user} -{varType string $lang} -``` - - -`{templatePrint}` ------------------ -Можете също така да генерирате класа с помощта на тага `{templatePrint}`. Ако го поставите в началото на шаблона, вместо нормалното рендиране ще се покаже предложение за клас. След това е достатъчно да маркирате кода с едно кликване и да го копирате в проекта. - - -`{varPrint}` ------------- -Тагът `{varPrint}` ще ви спести време за писане. Ако го поставите в шаблона, вместо нормалното рендиране ще се покаже предложение за тагове `{varType}` за локални променливи. След това е достатъчно да маркирате кода с едно кликване и да го копирате в шаблона. - -Самото `{varPrint}` извежда само локални променливи, които не са параметри на шаблона. Ако искате да изведете всички променливи, използвайте `{varPrint all}`. diff --git a/latte/bg/why-use.texy b/latte/bg/why-use.texy deleted file mode 100644 index 5f87c55670..0000000000 --- a/latte/bg/why-use.texy +++ /dev/null @@ -1,80 +0,0 @@ -Защо да използваме шаблони? -*************************** - - -Защо трябва да използвам система за шаблони в PHP? --------------------------------------------------- - -Защо да използваме система за шаблони в PHP, когато самият PHP е език за шаблони? - -Нека първо накратко да обобщим историята на този език, която е пълна с интересни обрати. Един от първите езици за програмиране, използвани за генериране на HTML страници, беше езикът C. Скоро обаче се оказа, че използването му за тази цел е непрактично. Затова Расмус Лердорф създаде PHP, което улесни генерирането на динамичен HTML с езика C в бекенда. Така PHP първоначално е проектиран като език за шаблони, но с течение на времето придобива допълнителни функции и се превръща в пълноценен език за програмиране. - -Въпреки това той все още функционира и като език за шаблони. В PHP файл може да бъде записана HTML страница, в която с помощта на `` се извеждат променливи и т.н. - -Още в началото на историята на PHP възникна системата за шаблони Smarty, чиято цел беше стриктно да отдели външния вид (HTML/CSS) от логиката на приложението. Тоест, тя умишлено предоставяше по-ограничен език от самия PHP, така че разработчикът да не може например да изпълни заявка към базата данни от шаблона и т.н. От друга страна, тя представляваше допълнителна зависимост в проектите, увеличаваше тяхната сложност и програмистите трябваше да учат новия език Smarty. Такава полза беше спорна и за шаблони продължи да се използва обикновен PHP. - -С течение на времето системите за шаблони започнаха да стават полезни. Те въведоха концепцията за [наследяване |template-inheritance], [режим sandbox|sandbox] и редица други функции, които значително опростиха създаването на шаблони в сравнение с чистия PHP. На преден план излезе темата за сигурността, съществуването на [уязвимости като XSS|safety-first] и необходимостта от [екраниране |#Какво е екраниране]. Системите за шаблони въведоха автоматично екраниране, за да изчезне рискът програмистът да забрави за това и да възникне сериозна дупка в сигурността (след малко ще покажем, че това има известни клопки). - -Ползите от системите за шаблони днес значително надвишават разходите, свързани с тяхното внедряване. Затова има смисъл да се използват. - - -Защо Latte е по-добър от Twig или Blade? ----------------------------------------- - -Причините са няколко – някои са приятни, а други са изключително полезни. Latte е комбинация от приятното с полезното. - -*Първо приятната причина:* Latte има същия [синтаксис като PHP |syntax#Latte разбира PHP]. Различава се само записът на таговете, вместо `` предпочита по-кратките `{` и `}`. Това означава, че не е нужно да учите нов език. Разходите за обучение са минимални. И най-важното, по време на разработката не е нужно постоянно да "превключвате" между езика PHP и езика на шаблона, тъй като и двата са еднакви. За разлика от шаблоните на Twig, които използват езика Python, и програмистът трябва да превключва между два различни езика. - -*А сега изключително полезната причина*: Всички системи за шаблони, като Twig, Blade или Smarty, в хода на еволюцията си въведоха защита срещу XSS под формата на автоматично [екраниране |#Какво е екраниране]. По-точно, автоматично извикване на функцията `htmlspecialchars()`. Създателите на Latte обаче осъзнаха, че това изобщо не е правилното решение. Защото на различни места в документа екранирането се извършва по различни начини. Наивното автоматично екраниране е опасна функция, защото създава фалшиво усещане за сигурност. - -За да бъде автоматичното екраниране функционално и надеждно, то трябва да разпознава къде в документа се извеждат данните (наричаме ги контексти) и според това да избира функцията за екраниране. Тоест, трябва да бъде [контекстно-чувствително |safety-first#Контекстно-чувствително екраниране]. И точно това умее Latte. То разбира HTML. Не възприема шаблона само като низ от знаци, а разбира какво са тагове, атрибути и т.н. И затова екранира по различен начин в HTML текст, по различен начин вътре в HTML таг, по различен начин вътре в JavaScript и т.н. - -Latte е първата и единствена система за шаблони в PHP, която има контекстно-чувствително екраниране. По този начин тя представлява единствената наистина сигурна система за шаблони. - -*И още една приятна причина*: Благодарение на това, че Latte разбира HTML, той предлага други много приятни функции. Например [n:атрибути |syntax#n:атрибути]. Или способността да [проверява връзки |safety-first#Проверка на връзки]. И много други. - - -Какво е екраниране? -------------------- - -Екранирането е процес, който се състои в замяна на знаци със специално значение със съответните им последователности при вмъкване на един низ в друг, за да се предотвратят нежелани явления или грешки. Например, когато вмъкваме низ в HTML текст, в който знакът `<` има специално значение, тъй като обозначава началото на таг, го заменяме със съответната последователност, която е HTML ентитетът `<`. Благодарение на това браузърът правилно ще покаже символа `<`. - -Прост пример за екраниране директно при писане на код в PHP е вмъкването на кавичка в низ, като преди нея напишем обратна наклонена черта. - -Разглеждаме екранирането по-подробно в главата [Как да се защитим от XSS |safety-first#Как да се защитим от XSS]. - - -Възможно ли е да се изпълни заявка към базата данни от шаблон в Latte? ----------------------------------------------------------------------- - -В шаблоните може да се работи с обекти, които програмистът им предава. Следователно, ако програмистът иска, той може да предаде обект на база данни към шаблона и да изпълни заявка върху него. Ако има такова намерение, няма причина да му се пречи. - -Друга ситуация възниква, ако искате да дадете възможност на клиенти или външни кодери да редактират шаблоните. В такъв случай определено не искате те да имат достъп до базата данни. Разбира се, няма да предадете обект на база данни на шаблона, но какво ще стане, ако до нея може да се стигне чрез друг обект? Решението е [режим sandbox|sandbox], който позволява да се дефинира кои методи могат да се извикват в шаблоните. Благодарение на това не е нужно да се притеснявате за нарушаване на сигурността. - - -Какви са основните разлики между системите за шаблони като Latte, Twig и Blade? -------------------------------------------------------------------------------- - -Разликите между системите за шаблони Latte, Twig и Blade се състоят главно в синтаксиса, сигурността и начина на интеграция във фреймуърците - -- Latte: използва синтаксиса на езика PHP, което улеснява ученето и използването. Предоставя върхова защита срещу XSS атаки. -- Twig: използва синтаксиса на езика Python, който доста се различава от PHP. Екранира без разграничаване на контекста. Добре е интегриран в Symfony framework. -- Blade: използва смес от PHP и собствен синтаксис. Екранира без разграничаване на контекста. Тясно е интегриран с функциите и екосистемата на Laravel. - - -Изгодно ли е за фирмите да използват система за шаблони? --------------------------------------------------------- - -Първо, разходите, свързани с обучението, използването и общата полза, се различават значително в зависимост от системата. Системата за шаблони Latte, благодарение на това, че използва синтаксиса на PHP, значително улеснява ученето за програмисти, които вече са запознати с този език. Обикновено отнема няколко часа, докато програмистът се запознае достатъчно с Latte. По този начин намалява разходите за обучение. Същевременно ускорява усвояването на технологията и преди всичко ефективността при ежедневна употреба. - -Освен това Latte предоставя високо ниво на защита срещу уязвимостта XSS благодарение на уникалната технология за контекстно-чувствително екраниране. Тази защита е ключова за гарантиране на сигурността на уеб приложенията и минимизиране на риска от атаки, които биха могли да застрашат потребителите или фирмените данни. Защитата на сигурността на уеб приложенията е важна и за поддържането на добрата репутация на фирмата. Проблемите със сигурността могат да доведат до загуба на доверие от страна на клиентите и да навредят на репутацията на фирмата на пазара. - -Използването на Latte също така намалява общите разходи за разработка и поддръжка на приложението, като улеснява и двете. Следователно използването на система за шаблони определено си заслужава. - - -Влияе ли Latte на производителността на уеб приложенията? ---------------------------------------------------------- - -Въпреки че шаблоните на Latte се обработват бързо, този аспект всъщност няма значение. Причината е, че парсирането на файловете се извършва само веднъж при първото показване. След това те се компилират в PHP код, съхраняват се на диска и се изпълняват при всяка следваща заявка, без да е необходимо повторно компилиране. - -Това е начинът на работа в продукционна среда. По време на разработката шаблоните на Latte се прекомпилират всеки път, когато съдържанието им се промени, така че разработчикът винаги да вижда актуалната версия. diff --git a/latte/cs/@home.texy b/latte/cs/@home.texy index 7cae06fabb..ce78127c9c 100644 --- a/latte/cs/@home.texy +++ b/latte/cs/@home.texy @@ -1,2 +1,3 @@ {{maintitle: Latte – nejbezpečnější & opravdu intuitivní šablony pro PHP}} -{{description: Latte je nejbezpečnější šablonovací systém pro PHP. Zabraňuje spoustě bezpečnostních zranitelností. Oceníte jeho intuitivní syntaxi a oceníte spoustu užitečných vychytávek.}} +{{description: Latte je nejbezpečnější šablonovací systém pro PHP. Zabraňuje spoustě bezpečnostních zranitelností. Oceníte jeho intuitivní syntaxi a potěší vás spousta užitečných vychytávek.}} +{{leftbar: no}} diff --git a/latte/cs/@left-menu.texy b/latte/cs/@left-menu.texy index 31a85bcaf7..b59ccb9611 100644 --- a/latte/cs/@left-menu.texy +++ b/latte/cs/@left-menu.texy @@ -5,6 +5,7 @@ - [Dědičnost šablon |Template Inheritance] - [Typový systém |type-system] - [Sandbox] + - [HTML atributy |html-attributes] - Pro designéry 🎨 - [Syntaxe |syntax] @@ -16,9 +17,15 @@ - Pro vývojáře 🧮 - [Vývojářské postupy |develop] - [Rozšiřujeme Latte |extending-latte] + - [Vlastní filtry |custom-filters] + - [Vlastní funkce |custom-functions] + - [Vlastní tagy |custom-tags] + - [Kompilační průchody |compiler-passes] + - [Loadery |loaders] - [Návody a postupy 💡|cookbook/@home] - [Migrace z Twigu |cookbook/migration-from-twig] - [… další |cookbook/@home] -- "Hřiště .[link-external]":https://fiddle.nette.org/latte/ .{padding-top:1em} +- "Kurz: Latte by Example .[link-external]":https://github.com/nette-examples/latte-by-example .{padding-top:1em} +- "Hřiště .[link-external]":https://fiddle.nette.org/latte/ diff --git a/latte/cs/compiler-passes.texy b/latte/cs/compiler-passes.texy index 8100b2416e..380fadc137 100644 --- a/latte/cs/compiler-passes.texy +++ b/latte/cs/compiler-passes.texy @@ -2,17 +2,17 @@ Kompilační průchody ******************* .[perex] -Kompilační průchody poskytují výkonný mechanismus pro analýzu a modifikaci Latte šablon *po* jejich parsování do abstraktního syntaktického stromu (AST) a *před* vygenerováním finálního PHP kódu. To umožňuje pokročilou manipulaci s šablonami, optimalizace, bezpečnostní kontroly (jako je Sandbox) a sběr informací o šablonách. Tento průvodce vás provede vytvořením vlastních kompilačních průchodů. +Kompilační průchody poskytují mocný mechanismus pro analýzu a modifikaci Latte šablon *po* jejich parsování do abstraktního syntaktického stromu (AST) a *před* vygenerováním finálního PHP kódu. To umožňuje pokročilou manipulaci s šablonami, optimalizace, bezpečnostní kontroly (jako je Sandbox) a sběr informací o šablonách. Tento průvodce vás provede vytvořením vlastních kompilačních průchodů. Co je kompilační průchod? ========================= -Pro pochopení role kompilačních průchodů se podívejte na [kompilační proces Latte |custom-tags#Pochopení procesu kompilace]. Jak můžete vidět, kompilační průchody operují v klíčové fázi, umožňující hluboký zásah mezi počátečním parsováním a finálním výstupem kódu. +Pro pochopení role kompilačních průchodů se podívejte na [kompilační proces Latte |custom-tags#Pochopení procesu kompilace]. Jak můžete vidět, kompilační průchody pracují v klíčové fázi, která umožňuje hluboký zásah mezi počátečním parsováním a finálním výstupem kódu. -V jádru je kompilační průchod jednoduše PHP volatelný objekt (jako funkce, statická metoda nebo metoda instance), který přijímá jeden argument: kořenový uzel AST šablony, což je vždy instance `Latte\Compiler\Nodes\TemplateNode`. +V podstatě je kompilační průchod jednoduše PHP volatelný objekt (jako funkce, statická metoda nebo metoda instance), který přijímá jeden argument: kořenový uzel AST šablony, což je vždy instance `Latte\Compiler\Nodes\TemplateNode`. -Primárním cílem kompilačního průchodu je obvykle jeden nebo oba z následujících: +Kompilační průchod obvykle plní jeden nebo oba tyto cíle: - Analýza: Procházet AST a shromažďovat informace o šabloně (např. najít všechny definované bloky, zkontrolovat použití specifických tagů, zajistit splnění určitých bezpečnostních omezení). - Modifikace: Změnit strukturu AST nebo atributy uzlů (např. automaticky přidat HTML atributy, optimalizovat určité kombinace tagů, nahradit zastaralé tagy novými, implementovat pravidla sandboxu). @@ -44,13 +44,13 @@ class MyExtension extends Extension } ``` -Průchody registrované základními rozšířeními Latte a vašimi vlastními rozšířeními běží sekvenčně. Pořadí může být důležité, zejména pokud jeden průchod závisí na výsledcích nebo modifikacích jiného. Latte poskytuje pomocný mechanismus pro kontrolu tohoto pořadí, pokud je potřeba; viz dokumentaci k [`Extension::getPasses()` |extending-latte#getPasses] pro podrobnosti. +Průchody registrované základními rozšířeními Latte a vašimi vlastními rozšířeními běží sekvenčně. Pořadí může být důležité, zejména pokud jeden průchod závisí na výsledcích nebo modifikacích jiného. Latte poskytuje pomocný mechanismus, kterým lze v případě potřeby toto pořadí řídit; podrobnosti najdete v dokumentaci k [`Extension::getPasses()` |extending-latte#getPasses]. Příklad AST =========== -Pro lepší představu o AST, přidáváme ukázku. Toto je zdrojová šablona: +Pro lepší představu o AST přidáváme ukázku. Toto je zdrojová šablona: ```latte {foreach $category->getItems() as $item} @@ -74,7 +74,7 @@ Latte\Compiler\Nodes\TemplateNode( content: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode(' ') - Latte\Compiler\Nodes\Html\ElementNode('li')( - content: Latte\Essential\Nodes\PrintNode( + content: Latte\Compiler\Nodes\PrintNode( expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') name: Latte\Compiler\Nodes\Php\IdentifierNode('name') @@ -100,7 +100,7 @@ Procházení AST pomocí `NodeTraverser` Ruční psaní rekurzivních funkcí pro procházení komplexní struktury AST je únavné a náchylné k chybám. Latte poskytuje speciální nástroj pro tento účel: [api:Latte\Compiler\NodeTraverser]. Tato třída implementuje [návrhový vzor Visitor |https://en.wikipedia.org/wiki/Visitor_pattern], díky kterému je procházení AST systematické a snadno zvládnutelné. -Základní použití zahrnuje vytvoření instance `NodeTraverser` a volání její metody `traverse()`, předání kořenového uzlu AST a jednoho nebo dvou "visitor" volatelných objektů: +Základní použití spočívá ve vytvoření instance `NodeTraverser` a zavolání její metody `traverse()`, které předáme kořenový uzel AST a jeden nebo dva "visitor" volatelné objekty: ```php use Latte\Compiler\Node; @@ -127,23 +127,27 @@ use Latte\Compiler\Nodes; ); ``` -Můžete poskytnout pouze `enter` visitor, pouze `leave` visitor, nebo oba, v závislosti na vašich potřebách. +Podle svých potřeb můžete použít jen visitor `enter`, jen `leave`, nebo oba. -**`enter(Node $node)`:** Tato funkce je provedena pro každý uzel **před** tím, než procházející navštíví kterékoliv z dětí tohoto uzlu. Je užitečná pro: +**`enter(Node $node)`:** Tato funkce se provede pro každý uzel **předtím**, než traverser navštíví kterékoliv z jeho dětí. Je užitečná pro: - Sbírání informací při průchodu stromem směrem dolů. - Rozhodování *před* zpracováním dětí (jako rozhodnutí je přeskočit, viz [#Optimalizace procházení]). - Potenciální úpravy uzlu před návštěvou dětí (méně časté). -**`leave(Node $node)`:** Tato funkce je provedena pro každý uzel **po** tom, co všechny jeho děti (a jejich celé podstromy) byly plně navštíveny (jak vstup tak opuštění). Je nejčastějším místem pro: +**`leave(Node $node)`:** Tato funkce se provede pro každý uzel **poté**, co byly všechny jeho děti (a jejich celé podstromy) plně navštíveny (jak vstup, tak opuštění). Je nejčastějším místem pro: -Oba visitoři `enter` a `leave` mohou volitelně vracet hodnotu pro ovlivnění procesu procházení. Vrácení `null` (nebo nic) pokračuje v procházení normálně, vrácení instance `Node` nahradí aktuální uzel, a vrácení speciálních konstant jako `NodeTraverser::RemoveNode` nebo `NodeTraverser::StopTraversal` modifikuje tok, jak je vysvětleno v následujících sekcích. +- Nahrazení uzlu poté, co byly zpracovány jeho děti. +- Odstraňování uzlů z AST. +- Agregaci informací shromážděných z celého podstromu. + +Oba visitory, `enter` i `leave`, mohou volitelně vrátit hodnotu a tím ovlivnit průběh procházení. Vrácení `null` (nebo ničeho) nechá procházení pokračovat normálně, vrácení instance `Node` nahradí aktuální uzel a vrácení speciálních konstant jako `NodeTraverser::RemoveNode` nebo `NodeTraverser::StopTraversal` změní jeho tok, jak vysvětlují následující sekce. Jak procházení funguje ---------------------- -`NodeTraverser` interně používá metodu `getIterator()`, kterou musí implementovat každá třída `Node` (jak bylo diskutováno v [Vytváření vlastních tagů |custom-tags#Implementace getIterator pro poduzly]). Iteruje přes děti získané pomocí `getIterator()`, rekurzivně volá `traverse()` na nich a zajišťuje, že `enter` a `leave` visitoři jsou voláni ve správném hloubkově-prvním pořadí pro každý uzel ve stromě dostupný přes iterátory. To znovu zdůrazňuje, proč správně implementovaný `getIterator()` ve vašich vlastních tagových uzlech je naprosto nezbytný pro správné fungování kompilačních průchodů. +`NodeTraverser` interně používá metodu `getIterator()`, kterou musí implementovat každá třída `Node` (jak jsme probrali ve [Vytváření vlastních tagů |custom-tags#Implementace getIterator pro poduzly]). Iteruje přes děti získané pomocí `getIterator()`, rekurzivně na nich volá `traverse()` a zajišťuje, že se visitory `enter` a `leave` volají ve správném pořadí procházení do hloubky pro každý uzel ve stromě dostupný přes iterátory. To znovu zdůrazňuje, proč je správně implementovaný `getIterator()` ve vašich vlastních tagových uzlech naprosto nezbytný pro správné fungování kompilačních průchodů. Pojďme napsat jednoduchý průchod, který počítá, kolikrát je v šabloně použit tag `{do}` (reprezentovaný `Latte\Essential\Nodes\DoNode`). @@ -166,7 +170,7 @@ function countDoTags(TemplateNode $templateNode): void // 'leave' visitor není pro tento úkol potřeba ); - echo "Nalezen tag {do} $count krát.\n"; + echo "Nalezen tag {do} {$count}krát.\n"; } $latte = new Latte\Engine; @@ -176,15 +180,15 @@ countDoTags($ast); V tomto příkladu jsme potřebovali pouze visitor `enter` ke kontrole typu každého navštíveného uzlu. -Dále prozkoumáme, jak tyto visitoři skutečně modifikují AST. +Dále prozkoumáme, jak tyto visitory použít ke skutečné modifikaci AST. Modifikace AST ============== -Jedním z hlavních účelů kompilačních průchodů je modifikace abstraktního syntaktického stromu. To umožňuje výkonné transformace, optimalizace nebo vynucení pravidel přímo na struktuře šablony před generováním PHP kódu. `NodeTraverser` poskytuje několik způsobů, jak toho dosáhnout v rámci visitorů `enter` a `leave`. +Jedním z hlavních účelů kompilačních průchodů je modifikace abstraktního syntaktického stromu. To umožňuje mocné transformace, optimalizace nebo vynucení pravidel přímo na struktuře šablony před generováním PHP kódu. `NodeTraverser` poskytuje několik způsobů, jak toho dosáhnout v rámci visitorů `enter` a `leave`. -**Důležitá poznámka:** Modifikace AST vyžaduje opatrnost. Nesprávné změny – jako odstranění základních uzlů nebo nahrazení uzlu nekompatibilním typem – mohou vést k chybám během generování kódu nebo způsobit neočekávané chování během běhu programu. Vždy důkladně testujte své modifikační průchody. +**Důležitá poznámka:** Modifikace AST vyžaduje opatrnost. Nesprávné změny - jako odstranění základních uzlů nebo nahrazení uzlu nekompatibilním typem - mohou vést k chybám během generování kódu nebo způsobit neočekávané chování během běhu programu. Vždy důkladně testujte své modifikační průchody. Úprava atributů uzlů @@ -219,13 +223,13 @@ function uppercaseStaticText(TemplateNode $templateNode): void V tomto příkladu visitor `enter` kontroluje, zda je aktuální `$node` typu `TextNode`. Pokud ano, přímo aktualizujeme jeho veřejnou vlastnost `$content` pomocí `mb_strtoupper()`. To přímo mění obsah statického textu uloženého v AST *před* generováním PHP kódu. Protože modifikujeme objekt přímo, nemusíme nic vracet z visitoru. -Efekt: Pokud šablona obsahovala `

    Hello

    {= $var }World`, po tomto průchodu bude AST reprezentovat něco jako: `

    HELLO

    {= $var }WORLD`. To NEOVLIVNÍ obsah $var. +Efekt: Pokud šablona obsahovala `

    Hello

    {= $var }World`, po tomto průchodu bude AST reprezentovat něco jako: `

    HELLO

    {= $var }WORLD`. To NEOVLIVNÍ obsah `$var`. Nahrazování uzlů ---------------- -Výkonnější technikou modifikace je kompletní nahrazení uzlu jiným. To se provádí **vrácením nové instance `Node`** z visitoru `enter` nebo `leave`. `NodeTraverser` pak nahradí původní uzel vráceným ve struktuře rodičovského uzlu. +Mocnější technikou modifikace je kompletní nahrazení uzlu jiným. To se provádí **vrácením nové instance `Node`** z visitoru `enter` nebo `leave`. `NodeTraverser` pak nahradí původní uzel vráceným ve struktuře rodičovského uzlu. **Příklad:** Vytvořme průchod, který najde všechna použití konstanty `PHP_VERSION` (reprezentované `ConstantFetchNode`) a nahradí je přímo řetězcovým literálem (`StringNode`) obsahujícím *skutečnou* verzi PHP detekovanou *během kompilace*. Toto je forma optimalizace v době kompilace. @@ -276,15 +280,15 @@ Odstraňování uzlů Můžete zcela odstranit uzel z AST vrácením speciální konstanty `NodeTraverser::RemoveNode` z visitoru. -**Příklad:** Odstraňme všechny komentáře šablony (`{* ... *}`), které jsou reprezentovány `CommentNode` v AST generovaném jádrem Latte (ačkoli typicky zpracovány dříve, toto slouží jako příklad). +**Příklad:** Odstraňme z výstupu všechny HTML komentáře (``). Komentáře Latte `{* ... *}` takto zacílit nelze, protože parser jejich obsah zahodí a nahradí je prázdným uzlem `NopNode`, nikoli samostatným uzlem komentáře, ale HTML komentáře zůstávají zachovány jako uzly `Html\CommentNode`, takže je zde můžeme odstranit. ```php use Latte\Compiler\Node; use Latte\Compiler\NodeTraverser; use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\CommentNode; +use Latte\Compiler\Nodes\Html\CommentNode; -function removeCommentNodes(TemplateNode $templateNode): void +function removeHtmlComments(TemplateNode $templateNode): void { (new NodeTraverser)->traverse( $templateNode, @@ -299,7 +303,7 @@ function removeCommentNodes(TemplateNode $templateNode): void } ``` -**Upozornění:** Používejte `RemoveNode` opatrně. Odstranění uzlu, který obsahuje základní obsah nebo ovlivňuje strukturu (jako odstranění obsahového uzlu cyklu), může vést k poškozeným šablonám nebo neplatnému generovanému kódu. Nejbezpečnější je pro uzly, které jsou skutečně volitelné nebo samostatné (jako komentáře nebo ladící tagy) nebo pro prázdné strukturální uzly (např. prázdný `FragmentNode` může být v některých kontextech bezpečně odstraněn průchodem pro vyčištění). +**Upozornění:** Používejte `RemoveNode` opatrně. Odstranění uzlu, který obsahuje základní obsah nebo ovlivňuje strukturu (jako odstranění obsahového uzlu cyklu), může vést k poškozeným šablonám nebo neplatnému generovanému kódu. Nejbezpečnější je odstraňovat uzly, které jsou skutečně volitelné nebo samostatné (jako komentáře nebo ladicí tagy) nebo pro prázdné strukturální uzly (např. prázdný `FragmentNode` může být v některých kontextech bezpečně odstraněn průchodem pro vyčištění). Tyto tři metody - úprava vlastností, nahrazování uzlů a odstraňování uzlů - poskytují základní nástroje pro manipulaci s AST v rámci vašich kompilačních průchodů. @@ -307,7 +311,7 @@ Tyto tři metody - úprava vlastností, nahrazování uzlů a odstraňování uz Optimalizace procházení ======================= -AST šablon může být poměrně velký, potenciálně obsahující tisíce uzlů. Procházení každého jednotlivého uzlu může být zbytečné a ovlivnit výkon kompilace, pokud váš průchod má zájem pouze o specifické části stromu. `NodeTraverser` nabízí způsoby optimalizace procházení: +AST šablon může být poměrně velký, potenciálně obsahující tisíce uzlů. Procházení každého jednotlivého uzlu může být zbytečné a ovlivnit výkon kompilace, pokud váš průchod potřebuje jen určité části stromu. `NodeTraverser` nabízí způsoby optimalizace procházení: Přeskakování dětí @@ -319,19 +323,19 @@ Pokud víte, že jakmile narazíte na určitý typ uzlu, žádný z jeho potomk Zastavení procházení -------------------- -Pokud váš průchod potřebuje najít pouze *první* výskyt něčeho (specifický typ uzlu, splnění podmínky), můžete úplně zastavit celý proces procházení, jakmile to najdete. Toho je dosaženo vrácením konstanty `NodeTraverser::StopTraversal` z visitoru `enter` nebo `leave`. Metoda `traverse()` přestane navštěvovat jakékoliv další uzly. To je vysoce efektivní, pokud potřebujete pouze první shodu v potenciálně velmi velkém stromě. +Pokud váš průchod potřebuje najít pouze *první* výskyt něčeho (specifický typ uzlu, splnění podmínky), můžete úplně zastavit celý proces procházení, jakmile to najdete. Docílíte toho vrácením konstanty `NodeTraverser::StopTraversal` z visitoru `enter` nebo `leave`. Metoda `traverse()` přestane navštěvovat jakékoliv další uzly. To je vysoce efektivní, pokud potřebujete pouze první shodu v potenciálně velmi velkém stromě. Užitečný pomocník `NodeHelpers` =============================== -Zatímco `NodeTraverser` nabízí jemně odstupňovanou kontrolu, Latte také poskytuje praktickou pomocnou třídu, [api:Latte\Compiler\NodeHelpers], která zapouzdřuje `NodeTraverser` pro několik běžných úloh vyhledávání a analýzy, často vyžadující méně přípravného kódu. +Zatímco `NodeTraverser` nabízí detailní kontrolu, Latte také poskytuje praktickou pomocnou třídu, [api:Latte\Compiler\NodeHelpers], která zapouzdřuje `NodeTraverser` pro několik běžných úloh vyhledávání a analýzy, často vyžadující méně přípravného kódu. find(Node $startNode, callable $filter): array .[method] -------------------------------------------------------- -Tato statická metoda nachází **všechny** uzly v podstromu začínajícím na `$startNode` (včetně), které splňují callback `$filter`. Vrací pole odpovídajících uzlů. +Tato statická metoda najde **všechny** uzly v podstromu začínajícím na `$startNode` (včetně), které splňují callback `$filter`. Vrací pole odpovídajících uzlů. **Příklad:** Najít všechny uzly proměnných (`VariableNode`) v celé šabloně. @@ -355,7 +359,7 @@ findFirst(Node $startNode, callable $filter): ?Node .[method] Podobné jako `find`, ale zastaví procházení okamžitě po nalezení **prvního** uzlu, který splňuje callback `$filter`. Vrací nalezený objekt `Node` nebo `null`, pokud není nalezen žádný odpovídající uzel. Toto je v podstatě praktický obal kolem `NodeTraverser::StopTraversal`. -**Příklad:** Najít uzel `{parameters}` (stejné jako manuální příklad předtím, ale kratší). +**Příklad:** Najít uzel `{parameters}`. ```php use Latte\Compiler\NodeHelpers; @@ -365,13 +369,25 @@ use Latte\Essential\Nodes\ParametersNode; function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode { return NodeHelpers::findFirst( - $templateNode->head, // Hledat pouze v hlavní sekci pro efektivitu + $templateNode->head, // Kvůli efektivitě hledáme jen v hlavičkové sekci fn($node) => $node instanceof ParametersNode, ); } ``` +clone(Latte\Compiler\Node $node): Node .[method] +------------------------------------------------ + +Tato statická metoda vytvoří **hlubokou kopii** uzlu a celého jeho podstromu. Hodí se, když potřebujete duplikovat větev AST, například vložit upravenou kopii uzlu a přitom ponechat originál nedotčený. + +```php +use Latte\Compiler\NodeHelpers; + +$copy = NodeHelpers::clone($node); +``` + + toValue(ExpressionNode $node, bool $constants = false): mixed .[method] ----------------------------------------------------------------------- @@ -423,7 +439,7 @@ function getStaticAttributeValue(AttributeNode $attr): ?string // Příklad použití v průchodu: // if ($node instanceof Html\ElementNode && $node->name === 'meta') { -// $nameAttrValue = getStaticAttributeValue($node->getAttributeNode('name')); +// $nameAttrValue = $node->getAttribute('name'); // if ($nameAttrValue === 'description') { ... } // } ``` @@ -434,7 +450,7 @@ function getStaticAttributeValue(AttributeNode $attr): ?string Praktické příklady ================== -Pojďme aplikovat koncepty procházení a modifikace AST k řešení některých praktických problémů. Tyto příklady demonstrují běžné vzory používané v kompilačních průchodech. +Pojďme použít koncepty procházení a modifikace AST na řešení několika praktických problémů. Tyto příklady demonstrují běžné vzory používané v kompilačních průchodech. Automatické přidání `loading="lazy"` k `` @@ -457,16 +473,13 @@ function addLazyLoading(Nodes\TemplateNode $templateNode): void enter: function (Node $node) { // Je to HTML element s názvem 'img'? if ($node instanceof Html\ElementNode && $node->name === 'img') { - // Zajistíme, že uzel atributů existuje - $node->attributes ??= new Nodes\FragmentNode; - // Zkontrolujeme, zda již existuje atribut 'loading' (bez ohledu na velikost písmen) foreach ($node->attributes->children as $attrNode) { if ($attrNode instanceof Html\AttributeNode && $attrNode->name instanceof Nodes\TextNode // Statický název atributu && strtolower($attrNode->name->content) === 'loading' ) { - return; + return; // Již existuje, nic neděláme } } @@ -491,7 +504,7 @@ function addLazyLoading(Nodes\TemplateNode $templateNode): void Vysvětlení: - Visitor `enter` hledá uzly `Html\ElementNode` s názvem `img`. - Iteruje přes existující atributy (`$node->attributes->children`) a kontroluje, zda je atribut `loading` již přítomen. -- Pokud není nalezen, vytvoří nový `Html\AttributeNode` reprezentující `loading="lazy"`. +- Pokud není nalezen, vytvoří nový `Html\AttributeNode` reprezentující `loading="lazy"` a přidá jej (v případě potřeby s mezerou před ním). Kontrola volání funkcí @@ -512,7 +525,6 @@ function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void { $forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Jednoduchý seznam - $traverser = new NodeTraverser; (new NodeTraverser)->traverse( $templateNode, enter: function (Node $node) use ($forbiddenFunctions) { @@ -534,22 +546,22 @@ function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void Vysvětlení: - Definujeme seznam zakázaných názvů funkcí. - Visitor `enter` kontroluje `FunctionCallNode`. -- Pokud je název funkce (`$node->name`) statický `NameNode`, kontrolujeme jeho řetězcovou reprezentaci v malých písmenech proti našemu zakázanému seznamu. -- Pokud je nalezena zakázaná funkce, vyhodíme `Latte\SecurityViolationException`, která jasně indikuje porušení bezpečnostního pravidla a zastaví kompilaci. +- Pokud je název funkce (`$node->name`) statický `NameNode`, porovnáme jeho řetězcovou reprezentaci převedenou na malá písmena se seznamem zakázaných funkcí. +- Pokud je nalezena zakázaná funkce, vyhodíme `Latte\SecurityViolationException`, která jasně signalizuje porušení bezpečnostního pravidla a zastaví kompilaci. -Tyto příklady ukazují, jak mohou být kompilační průchody s použitím `NodeTraverser` využity pro analýzu, automatické modifikace a vynucení bezpečnostních omezení interakcí přímo se strukturou AST šablony. +Tyto příklady ukazují, jak lze pomocí `NodeTraverser` využít kompilační průchody k analýze, automatickým úpravám i vynucení bezpečnostních omezení přímo nad strukturou AST šablony. Osvědčené postupy ================= -Při psaní kompilačních průchodů mějte na paměti tyto směrnice pro vytváření robustních, udržovatelných a efektivních rozšíření: +Při psaní kompilačních průchodů mějte na paměti tyto zásady pro vytváření robustních, udržovatelných a efektivních rozšíření: -- **Pořadí je důležité:** Buďte si vědomi pořadí, v jakém průchody běží. Pokud váš průchod závisí na struktuře AST vytvořené jiným průchodem (např. základní průchody Latte nebo jiný vlastní průchod), nebo pokud jiné průchody mohou záviset na vašich modifikacích, použijte mechanismus řazení poskytovaný `Extension::getPasses()` k definování závislostí (`before`/`after`). Viz dokumentaci k [`Extension::getPasses()` |extending-latte#getPasses] pro podrobnosti. -- **Jedna odpovědnost:** Snažte se o průchody, které provádějí jednu dobře definovanou úlohu. Pro komplexní transformace zvažte rozdělení logiky do více průchodů – možná jeden pro analýzu a další pro modifikaci založenou na výsledcích analýzy. To zlepšuje přehlednost a testovatelnost. -- **Výkon:** Pamatujte, že kompilační průchody přidávají čas kompilace šablony (i když to obvykle nastává pouze jednou, dokud se šablona nezmění). Vyhněte se výpočetně náročným operacím ve vašich průchodech, pokud je to možné. Využívejte optimalizace procházení jako `NodeTraverser::DontTraverseChildren` a `NodeTraverser::StopTraversal` kdykoliv víte, že nepotřebujete navštívit určité části AST. -- **Používejte `NodeHelpers`:** Pro běžné úlohy jako hledání specifických uzlů nebo statické vyhodnocování jednoduchých výrazů, zkontrolujte, zda `Latte\Compiler\NodeHelpers` nenabízí vhodnou metodu před psaním vlastní logiky `NodeTraverser`. Může to ušetřit čas a snížit množství přípravného kódu. +- **Pořadí je důležité:** Mějte na paměti pořadí, v jakém průchody běží. Pokud váš průchod závisí na struktuře AST vytvořené jiným průchodem (např. základní průchody Latte nebo jiný vlastní průchod), nebo pokud jiné průchody mohou záviset na vašich modifikacích, použijte k definování závislostí (`before`/`after`) mechanismus řazení, který nabízí `Extension::getPasses()`. Podrobnosti najdete v dokumentaci k [`Extension::getPasses()` |extending-latte#getPasses]. +- **Jedna odpovědnost:** Snažte se o průchody, které provádějí jednu dobře definovanou úlohu. Pro komplexní transformace zvažte rozdělení logiky do více průchodů - možná jeden pro analýzu a další pro modifikaci založenou na výsledcích analýzy. To zlepšuje přehlednost a testovatelnost. +- **Výkon:** Pamatujte, že kompilační průchody prodlužují dobu kompilace šablony (i když to obvykle nastává pouze jednou, dokud se šablona nezmění). Vyhněte se výpočetně náročným operacím ve vašich průchodech, pokud je to možné. Využívejte optimalizace procházení jako `NodeTraverser::DontTraverseChildren` a `NodeTraverser::StopTraversal` kdykoliv víte, že nepotřebujete navštívit určité části AST. +- **Používejte `NodeHelpers`:** U běžných úloh, jako je hledání konkrétních uzlů nebo statické vyhodnocování jednoduchých výrazů, se nejprve podívejte, zda `Latte\Compiler\NodeHelpers` nenabízí vhodnou metodu, a teprve pak pište vlastní logiku s `NodeTraverser`. Může to ušetřit čas a snížit množství přípravného kódu. - **Zpracování chyb:** Pokud váš průchod detekuje chybu nebo neplatný stav v AST šablony, vyhoďte `Latte\CompileException` (nebo `Latte\SecurityViolationException` pro bezpečnostní problémy) s jasnou zprávou a relevantním objektem `Position` (obvykle `$node->position`). To poskytuje užitečnou zpětnou vazbu vývojáři šablony. -- **Idempotence (pokud možno):** Ideálně by spuštění vašeho průchodu vícekrát na stejném AST mělo produkovat stejný výsledek jako jeho jednorázové spuštění. To není vždy proveditelné, ale zjednodušuje ladění a uvažování o interakcích průchodů, pokud je toho dosaženo. Například zajistěte, aby váš modifikační průchod kontroloval, zda modifikace již byla aplikována, než ji aplikuje znovu. +- **Idempotence (pokud možno):** Ideálně by spuštění vašeho průchodu vícekrát na stejném AST mělo produkovat stejný výsledek jako jeho jednorázové spuštění. Není to vždy proveditelné, ale pokud se to podaří, zjednoduší to ladění i uvažování o vzájemných interakcích průchodů. Například zajistěte, aby váš modifikační průchod kontroloval, zda modifikace již byla aplikována, než ji aplikuje znovu. -Dodržováním těchto postupů můžete efektivně využít kompilační průchody k rozšíření schopností Latte výkonným a spolehlivým způsobem, což přispívá k bezpečnějšímu, optimalizovanějšímu nebo funkčně bohatšímu zpracování šablon. +Dodržováním těchto postupů můžete kompilační průchody využít k rozšíření možností Latte mocným a spolehlivým způsobem, což vede k bezpečnějšímu, lépe optimalizovanému nebo funkčně bohatšímu zpracování šablon. diff --git a/latte/cs/cookbook/@home.texy b/latte/cs/cookbook/@home.texy index b4d54b0b7e..d269a008e6 100644 --- a/latte/cs/cookbook/@home.texy +++ b/latte/cs/cookbook/@home.texy @@ -7,7 +7,8 @@ Příklady kódů a receptů pro provádění běžných úkolů pomocí Latte. - [Postupy pro vývojáře |/develop] - [Předávání proměnných napříč šablonami |passing-variables] - [Všechno, co jste kdy chtěli vědět o seskupování |grouping] -- [Jak psát SQL queries v Latte? |how-to-write-sql-queries-in-latte] +- [Jak psát SQL dotazy v Latte? |how-to-write-sql-queries-in-latte] +- [Migrace z Latte 3.0 |migration-from-latte-30] - [Migrace z Latte 2 |migration-from-latte2] - [Migrace z PHP |migration-from-php] - [Migrace z Twigu |migration-from-twig] diff --git a/latte/cs/cookbook/grouping.texy b/latte/cs/cookbook/grouping.texy index bdb563efb5..526b3c2dc9 100644 --- a/latte/cs/cookbook/grouping.texy +++ b/latte/cs/cookbook/grouping.texy @@ -2,15 +2,17 @@ Všechno, co jste kdy chtěli vědět o seskupování *********************************************** .[perex] -Při práci s daty ve šablonách můžete často narazit na potřebu jejich seskupování nebo specifického zobrazení podle určitých kritérií. Latte pro tento účel nabízí hned několik silných nástrojů. +Při práci s daty v šablonách často potřebujete položky seskupit, rozdělit do dávek nebo je procházet podle podmínky. Latte k tomu nabízí tři nástroje, z nichž každý se hodí na trochu jinou situaci. -Filtr a funkce `|group` umožňují efektivní seskupení dat podle zadaného kritéria, filtr `|batch` zase usnadňuje rozdělení dat do pevně daných dávek a značka `{iterateWhile}` poskytuje možnost složitějšího řízení průběhu cyklů s podmínkami. Každá z těchto značek nabízí specifické možnosti pro práci s daty, čímž se stávají nepostradatelnými nástroji pro dynamické a strukturované zobrazení informací v Latte šablonách. +Filtr `|group` a funkce `group()` seskupí položky podle zadaného kritéria, filtr `|batch` je rozdělí do dávek pevné velikosti a značka `{iterateWhile}` prochází data postupně a sama si určuje, kdy přerušit vnitřní smyčku. V textu si je postupně projdeme. Filtr a funkce `group` .{data-version:3.0.16} ============================================= -Představte si databázovou tabulku `items` s položkami rozdělenou do kategorií: +Nástroj lze používat ve dvou tvarech: jako filtr `$items|group: …` nebo jako funkci `group($items, …)`. Sémanticky jsou ekvivalentní, vyberte si podle čitelnosti. + +Představte si databázovou tabulku `items`, jejíž položky patří do různých kategorií: | id | categoryId | name |------------------ @@ -62,19 +64,17 @@ Pokud bychom ale chtěli, aby položky byly uspořádány do skupin podle katego {/foreach} ``` -Filtr lze v Latte použít i jako funkci, což nám dává alternativní syntaxi: `{foreach group($items, categoryId) ...}`. - -Chcete-li seskupovat položky podle složitějších kritérií, můžete v parametru filtru použít funkci. Například, seskupení položek podle délky názvu by vypadalo takto: +Chcete-li seskupovat položky podle složitějších kritérií, můžete v parametru filtru použít funkci. Klíčem každé skupiny pak bude návratová hodnota funkce - například při seskupení podle délky názvu to bude počet znaků: ```latte -{foreach ($items|group: fn($item) => strlen($item->name)) as $items} +{foreach ($items|group: fn($item) => strlen($item->name)) as $length => $group} ... {/foreach} ``` -Je důležité si uvědomit, že `$categoryItems` není běžné pole, ale objekt, který se chová jako iterátor. Pro přístup k první položce skupiny můžete použít funkci [`first()` |latte:functions#first]. +Je důležité si uvědomit, že každá skupina (tedy i `$categoryItems`) není běžné pole, ale objekt chovající se jako iterátor - nelze proto přistupovat k položkám přes index, např. `$categoryItems[0]`. Položky ale můžete spočítat pomocí `count($categoryItems)` a pro přístup k první položce skupiny použijte funkci [`first()` |latte:functions#first]. -Tato flexibilita v seskupování dat činí `group` výjimečně užitečným nástrojem pro prezentaci dat v šablonách Latte. +Tato flexibilita činí `|group` výjimečně užitečným nástrojem pro prezentaci dat. Vnořené smyčky @@ -97,8 +97,8 @@ Představme si, že máme databázovou tabulku s dalším sloupcem `subcategoryI ``` -Spojení s Nette Database ------------------------- +Společně s Nette Database +------------------------- Pojďme si ukázat, jak efektivně využít seskupování dat v kombinaci s Nette Database. Předpokládejme, že pracujeme s tabulkou `items` z úvodního příkladu, která je prostřednictvím sloupce `categoryId` spojená s touto tabulkou `categories`: @@ -121,27 +121,27 @@ Data z tabulky `items` načteme pomocí Nette Database Explorer příkazem `$ite {/foreach} ``` -V tomto případě používáme filtr `|group` k seskupení podle propojeného řádku `$item->category`, nikoliv jen dle sloupce `categoryId`. Díky tomu v proměnné klíči přímo `ActiveRow` dané kategorie, což nám umožňuje přímo vypisovat její název pomocí `{$category->name}`. Toto je praktický příklad, jak může seskupování zpřehlednit šablony a usnadnit práci s daty. +V tomto případě používáme filtr `|group` k seskupení podle propojeného řádku `$item->category`, nikoliv jen dle sloupce `categoryId`. Díky tomu je v klíči (`$category`) rovnou `ActiveRow` dané kategorie, což nám umožňuje vypisovat její název pomocí `{$category->name}` a přistupovat k libovolnému dalšímu sloupci, aniž bychom museli dělat zvláštní dotaz na `categories`. Filtr `|batch` ============== -Filtr umožňuje rozdělit seznam prvků do skupin s předem určeným počtem prvků. Tento filtr je ideální pro situace, kdy chcete data prezentovat ve více menších skupinách, například pro lepší přehlednost nebo vizuální uspořádání na stránce. +Filtr rozdělí seznam prvků do dávek o pevně daném počtu. Hodí se třeba pro grid layout, sloupcové rozložení nebo jakékoli vizuální seskupení. -Představme si, že máme seznam položek a chceme je zobrazit v seznamech, kde každý obsahuje maximálně tři položky. Použití filtru `|batch` je v takovém případě velmi praktické: +Představme si, že chceme zobrazit položky v seznamech, kde každý obsahuje maximálně tři položky: ```latte -
      {foreach ($items|batch: 3) as $batch} - {foreach $batch as $item} -
    • {$item->name}
    • - {/foreach} +
        + {foreach $batch as $item} +
      • {$item->name}
      • + {/foreach} +
      {/foreach} -
    ``` -V tomto příkladu je seznam `$items` rozdělen do menších skupin, přičemž každá skupina (`$batch`) obsahuje až tři položky. Každá skupina je poté zobrazena v samostatném `